进入第九阶段:调试与底层透视。
这是区分"代码民工"和"技术专家"最明显的标志。 新手遇到 Bug,第一反应是加 printf("Here 1\n"); 专家遇到 Bug,第一反应是挂上 J-Link,打开 Register 和 Memory 窗口,直接看芯片的"五脏六腑"。
printf 是有延迟的、有侵入性的(会改变时序),而硬件调试器是上帝视角。
让我们进入 第42期,学会像外科医生一样解剖 MCU。
抛弃 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)
你定义了一个通信协议结构体:
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 误写? 这时候,你需要一个**"监控摄像头"**,一旦有人改这个变量,立马报警暂停。