核心内容摘要
麻豆糖心:解锁舌尖上的甜蜜密码,品味那份独属于你的心动
单片机调试进阶IDE中的Register与Memory窗口前言调试是区分新手与老鸟最明显的标志新手遇到 Bug第一反应是加printf(Here 1\n) 老手遇到 Bug第一反应是挂上 J-Link打开Register和Memory窗口直接看芯片的“五脏六腑”。
printf是有延迟的、有侵入性的会改变时序而硬件调试器是从本质看问题。
为什么要看 Register (寄存器)你写了HAL_GPIO_Init(GPIOA, init_struct)把 PA5 配置为推挽输出。
但是灯就是不亮。
你开始怀疑是时钟没开是引脚复用没配对还是速度等级不对如果你只是看代码你永远在分析。
因为代码逻辑可能是对的但也许库函数可能有 Bug或者被后面的代码覆盖了配置。
正确的做法是在 IDE (Keil/IAR/CubeIDE) 中进入 Debug 模式。
打开System Viewer或Registers窗口。
找到GPIOA-MODER寄存器。
看什么检查 PA5 对应的两个位是否是01(Output Mode)。
如果是00(Input)说明你的初始化代码根本没生效可能时钟没开写不进去。
如果是11(Analog)说明被后面的 ADC 初始化覆盖了。
寄存器里的值是芯片硬件当前真实的物理状态它不会撒谎。
SFR (Special Function Register) 排查法场景一串口发不出数据Printf 现象程序卡死在HAL_UART_Transmit。
Register 排查看USART1 - CR1TE(Transmitter Enable) 位是不是 1检查是否使能看USART1 - SR(状态寄存器)TC(Transmission Complete) 位是不是 1看RCC - APB2ENR。
USART1 的时钟使能位是不是 1很多时候是因为你忘了开时钟导致怎么写寄存器都写不进去读出来全是 0。
场景二定时器时间不对现象设定 1秒中断结果
5秒就中断了。
Register 排查看TIMx - PSC(预分频器)。
是不是7199(72MHz / 7200 10kHz)看TIMx - ARR(自动重装载)。
是不是9999(10kHz / 10000 1Hz)常见坑PSC 是 16 位的如果你填了100000它会溢出截断导致频率变快。
看寄存器一眼就能发现数值不对。
Memory 窗口透视内存的问题Watch窗口变量观察很好用但它只能看“变量的值”。
Memory 窗口能让你看到“变量在内存里的布局”。
这对于排查指针越界、结构体对齐、字节序问题是绝杀。
技巧一检查结构体对齐 (Struct Alignment)你定义了一个通信协议结构体struct { uint8_t Head; uint32_t Len; } Packet;你以为Len紧挨着Head 打开 Memory 窗口输入Packet地址0x20000000:AA(Head)地址0x20000001:00(Padding/填充字节)地址0x20000002:00(Padding/填充字节)地址0x20000003:00(Padding/填充字节)地址0x20000004:64 00 00 00(Len
你会发现中间有 3 个字节的空洞如果你直接把这个结构体memcpy发给上位机解析一定错位。
解决加__packed或__attribute__((packed))再看 Memory 窗口空洞消失了。
技巧二抓捕“栈溢出” (Stack Overflow)程序莫名其妙死机怀疑栈溢出了在 Memory 窗口找到栈的地址比如0x2000 8000附近。
一般栈的末尾会有大量的00 00 00 00未使用的区域。
程序跑一会儿暂停。
如果你发现那些00全部变成了乱七八糟的数据而且一直顶到了栈底Stack Limit说明栈溢出了。
实时更新 (Live Watch)Keil 和 IAR 必须暂停才能看内存吗不。
J-Link 支持Live Watch。
原理ARM Cortex-M 内核支持在 CPU 全速运行的同时通过调试接口DAP偷偷读取内存不影响 CPU 执行。
用法勾选Periodic Window Update。
场景观察 PID 控制中的Current_Error变量你可以看到数值像示波器一样跳动而不需要停下电机。
5.
总结本章不要用printf调试底层驱动了。
Register 窗口告诉你如何验证配置Memory 窗口告诉你如何验证对齐和越界当你习惯了看寄存器你会发现你不再需要翻几百页的 Reference Manual 去找位的定义因为 IDE 已经把每一位的含义RW, BitName都列在旁边了。
但是有时候 Bug 很狡猾。
全局变量g_State莫名其妙从 0 变成了 1但你搜遍全代码也没找到哪里改了它。
难道是野指针还是 DMA 误写 这时候你需要一个一个机制一旦有人改这个变量立马能报警暂停
调试进阶断点与观察点 (Watchpoint)前言前面我们学会了用“静态”的视角寄存器和内存窗口去检查系统状态。
但有些 Bug 是动态的、瞬时的甚至是很难琢磨的。
比如你定义了一个全局变量g_MotorState你发誓代码里只有在Stop()函数里才会把它置为 0。
但程序跑着跑着它突然变成了 0而你根本没调用Stop()。
难道是堆栈溢出野指针乱指还是 DMA 搬运数据搬歪了平常我们最熟悉的断点叫代码断点 (Code Breakpoint)。
你点一下行号左边出现一个红点。
当 CPU 执行到这一行指令时停下来。
但如果你不知道到底哪里代码出问题了只知道出现某个问题了怎么办 你需要数据断点 (Data Breakpoint / Watchpoint)。
它的逻辑是“只要有任何人指令/DMA试图修改这个内存地址CPU 立刻暂停”寻找到底谁破坏了内存uint8_t g_Mode 1; uint8_t RxBuffer[10]; void Parsr_Data(void) { // 你的逻辑是解析 RxBuffer // 但因为下标算错了RxBuffer[11] 0x55; 越界了 // 恰好 g_Mode 就在 RxBuffer 后面 // 于是 g_Mode 被改成了 0x55 }这种 Bug 极其隐蔽。
g_Mode被改了但程序当时还在跑Parsr_Data离真正使用g_Mode的地方很远。
当你发现g_Mode错的时候现场早就没了。
使用数据断点找到地址在 Watch 窗口或者 Map 文件中找到g_Mode的内存地址比如0x2000 0014。
设置断点Keil:点击Debug-Breakpoints(CtrlB)。
在Expression里填0x20000014在Access里勾选Write。
IAR:右键变量 -Set Data Breakpoint-Write。
J-Link (Ozone):直接右键变量 -Break on Write。
全速运行 (Go):程序会全速奔跑。
当那个越界的RxBuffer[11] 0x55指令执行的瞬间CPU 就像撞墙一样自动暂停。
抓bug此时你看 call stack调用栈光标停在Parsr_Data函数里。
你一看代码RxBuffer[i] ...而此时i是 11。
解决了问题就是这个循环越界。
Data Watchpoint and Trace单元你可能会问调试器是不是一直在轮询这个地址那岂不是会让程序变慢完全不会。
这是硬件断点。
Cortex-M3/M4/M7 内核里有一个专门的单元叫DWT (Data Watchpoint and Trace)。
它有 4 个硬件比较器。
你把地址写进 DWT 寄存器。
CPU 每次访问总线时硬件会自动比较地址。
一旦匹配DWT 会发送信号给内核让它停下。
这对 CPU 的执行速度是 0 影响的限制因为 DWT 比较器通常只有 4 个所以你最多同时设置 4 个数据断点或者 2 个范围断点。
省着点用。
条件断点 (Conditional Breakpoint)有时候你不需要变量一变就停而是它变成特定值时才停。
场景一个循环for(i0; i10000; i)。
你发现i5000的时候逻辑有问题。
你不能手按 F5 按 5000 次吧设置方法在代码断点属性里输入Condition:i 5000。
注意这种断点通常是软件模拟的。
副作用调试器会在这一行自动插入“暂停-检查-恢复”的微代码。
这会让程序运行变得极其慢可能慢 1000 倍。
优化更好的办法是在代码里写个临时的if (i
{ __NOP(); // 在这里打个普通断点 }观察点 (Watchpoint) 的其他用途检测栈溢出把断点设在栈顶Stack Limit的地址。
一旦有人写这个地址说明栈炸了立即暂停。
检测 DMA 误写有时候不是 CPU 写的是 DMA 还在搬运数据而你以为它停了就把缓冲区挪作他用。
DWT 也能监控到总线上的 DMA 写入操作取决于具体芯片的总线矩阵设计。
本章
总结Code Breakpoint:查逻辑流程。
Data Breakpoint:查内存破坏、野指针、越界。
不要吝啬使用遇到“变量莫名其妙改变”的问题第一时间上数据断点能节省你 90% 的瞎猜时间。
好了我们用断点找到了问题。
但是如果bug直接把 CPU 搞死了进入了 HardFault 异常调试器停下来时只看到满屏的汇编连是哪个函数调用的都看不出来怎么办下一章我们讲如何“分析死机现场的堆栈信息”。