一、单片机调试进阶:IDE中的Register与Memory窗口
前言
调试是区分新手与老鸟最明显的标志,新手遇到 Bug,第一反应是加 printf("Here 1\n"); 老手遇到 Bug,第一反应是挂上 J-Link,打开 Register 和 Memory 窗口,直接看芯片的"五脏六腑"。
printf 是有延迟的、有侵入性的(会改变时序),而硬件调试器是从本质看问题。
1. 为什么要看 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 初始化覆盖了。寄存器里的值,是芯片硬件当前真实的物理状态,它不会撒谎。
2. SFR (Special Function Register) 排查法
场景一:串口发不出数据
-
Printf 现象: 程序卡死在
HAL_UART_Transmit。 -
Register 排查:
-
看
USART1 -> CR1:TE(Transmitter Enable) 位是不是 1?(检查是否使能) -
看
USART1 -> SR(状态寄存器):TC(Transmission Complete) 位是不是 1? -
看
RCC -> APB2ENR。USART1 的时钟使能位是不是 1?很多时候是因为你忘了开时钟,导致怎么写寄存器都写不进去(读出来全是 0)。
-
场景二:定时器时间不对
-
现象: 设定 1秒中断,结果 0.5秒就中断了。
-
Register 排查:
-
看
TIMx -> PSC(预分频器)。是不是7199?(72MHz / 7200 = 10kHz) -
看
TIMx -> ARR(自动重装载)。是不是9999?(10kHz / 10000 = 1Hz) -
常见坑: PSC 是 16 位的,如果你填了
100000,它会溢出截断,导致频率变快。看寄存器一眼就能发现数值不对。
-
3. Memory 窗口:透视内存的问题
Watch 窗口(变量观察)很好用,但它只能看"变量的值"。Memory 窗口 能让你看到"变量在内存里的布局"。这对于排查指针越界 、结构体对齐 、字节序问题是绝杀。
技巧一:检查结构体对齐 (Struct Alignment)
你定义了一个通信协议结构体:
cpp
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 = 100)
你会发现中间有 3 个字节的空洞!如果你直接把这个结构体 memcpy 发给上位机,解析一定错位。解决: 加 __packed 或 __attribute__((packed)),再看 Memory 窗口,空洞消失了。
技巧二:抓捕"栈溢出" (Stack Overflow)
程序莫名其妙死机,怀疑栈溢出了?
-
在 Memory 窗口找到栈的地址(比如
0x2000 8000附近)。 -
一般栈的末尾会有大量的
00 00 00 00(未使用的区域)。 -
程序跑一会儿,暂停。
-
如果你发现那些
00全部变成了乱七八糟的数据,而且一直顶到了栈底(Stack Limit),说明栈溢出了。
4. 实时更新 (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 立刻暂停!"
寻找到底谁破坏了内存?
cpp
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(Ctrl+B)。在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(i=0; i<10000; i++)。你发现 i=5000 的时候逻辑有问题。 你不能手按 F5 按 5000 次吧?
设置方法: 在代码断点属性里,输入 Condition: i == 5000。
-
注意: 这种断点通常是软件模拟的。
-
副作用: 调试器会在这一行自动插入"暂停-检查-恢复"的微代码。这会让程序运行变得极其慢(可能慢 1000 倍)。
-
优化: 更好的办法是在代码里写个临时的:
cpp
if (i == 5000) {
__NOP(); // 在这里打个普通断点
}
观察点 (Watchpoint) 的其他用途
-
检测栈溢出:
-
把断点设在栈顶(Stack Limit)的地址。
-
一旦有人写这个地址,说明栈炸了,立即暂停。
-
-
检测 DMA 误写:
-
有时候不是 CPU 写的,是 DMA 还在搬运数据,而你以为它停了,就把缓冲区挪作他用。
-
DWT 也能监控到总线上的 DMA 写入操作(取决于具体芯片的总线矩阵设计)。
-
本章总结
-
Code Breakpoint: 查逻辑流程。
-
Data Breakpoint: 查内存破坏、野指针、越界。
-
不要吝啬使用: 遇到"变量莫名其妙改变"的问题,第一时间上数据断点,能节省你 90% 的瞎猜时间。
好了,我们用断点找到了问题。 但是,如果bug直接把 CPU 搞死了(进入了 HardFault 异常),调试器停下来时,只看到满屏的汇编,连是哪个函数调用的都看不出来,怎么办?下一章我们讲如何"分析死机现场的堆栈信息"。