前言
最近经常遇到各种各样的 Bug,有些只是逻辑不正常,还有一些会直接导致 MCU 进入 HardFault 等异常,整个程序基本卡死。
因此最近需要频繁进行调试,想办法找到 Bug 到底是从哪里冒出来的。
逻辑 Bug 倒还好说,至少程序通常还能继续运行,可以打日志、看变量,一点一点缩小问题范围。
真正麻烦的是程序直接崩掉。
所以这篇就简单记录一下,我目前知道或者实际用过的一些 MCU 调试方法。
前提
前提是做好备份!
比如利用 Git 单独开一个 fix 分支,专门用来调试和修复当前的问题。
总之,在开始修 Bug 前,最好保留:
- Bug 能稳定复现的原始版本。
- 一个确认没有这个 Bug 的旧版本。
- 调试过程中每一个比较关键的修改版本。
每次修改后,也要记得及时用 Git 或者其他方式保存。
不然改着改着,Bug 突然消失了,结果你自己都不知道到底是哪一处修改把它修好的,那就有点尴尬了。
更糟糕的是,原来的 Bug 没找到,又因为调试过程中到处乱改代码,引入了一个新的 Bug。
所以修 Bug 的第一步,可能不是马上开始改代码,而是先保证自己随时可以回到问题出现时的现场。
方法
下面是我目前知晓的一些调试方法。
调试器
这个应该是最直接、最好用的办法了。
一般人学习 STM32 的时候,肯定接触过类似 ST-LINK 这样的调试器,再配合 GDB、OpenOCD 或者 IDE 自带的调试功能,就可以进行断点、单步执行、查看变量、寄存器、内存、调用栈等操作。
程序卡在哪里,哪个变量突然变成了奇怪的值,函数是怎么一层一层调用过来的,很多问题都可以直接看到。
所以这里就不详细介绍了。
你如果能搜到这篇文章,大概至少也是入门水平了,应该不需要我从“什么是断点”开始讲起。
总之:
有条件用调试器的时候,优先用调试器。
它能保留最多的现场信息,通常也是定位问题最方便的方法。
HardFault 等异常 + 串口
但是有的时候,因为硬件条件、接口占用、设备已经装机或者其他各种各样的原因,就是没办法一直挂着调试器。
这个时候如果程序出现问题,就只能尽量依靠串口日志,以及 MCU 的异常处理机制来留下线索了。
串口最基本的用途当然就是输出日志。
比如:
- 当前执行到了哪里。
- 某个变量的值是多少。
- 某个函数有没有执行。
- 某个状态什么时候发生了变化。
对于普通的逻辑 Bug,这些日志往往已经很好用了。
而如果程序发生了更严重的错误,比如非法访问内存、执行了错误的指令地址,Cortex-M 内核可能会进入 HardFault、MemManage、BusFault、UsageFault 等异常处理流程。
这个时候可以在异常处理函数里尽量保存并输出一些关键现场信息,例如:
PC:出错时正在执行或者即将执行的位置。LR:链接寄存器,可以辅助判断调用来源。SP:当前栈指针。R0 ~ R12等通用寄存器。xPSR。CFSR、HFSR、MMFAR、BFAR等 Fault 状态寄存器。- 当前线程、任务或者调用栈相关信息。
拿到这些地址以后,再配合发生崩溃时对应版本的 *.elf 文件,就可以使用 addr2line、GDB 等工具把地址解析到具体的函数、源文件甚至代码行。
例如 addr2line 本质上就是利用 ELF 中的符号和调试信息,把:
0x1032e115
这种看起来完全不知道是什么东西的地址,转换成类似:
xxx_function
src/xxx.c:123
这样的信息。
当然,前提是 ELF 必须和设备当时运行的固件对应。
代码重新编译过以后,函数地址很可能已经发生变化。拿一个新版本的 ELF 去分析旧版本程序留下来的崩溃地址,很可能直接查歪。
所以这也再次证明了前面为什么要强调:
一定要保留出问题版本对应的固件和 ELF。
比较麻烦,如果不能用调试器,还是丢给 AI 吧……
转储文件
如果系统本身支持保存 Crash Dump、Core Dump 或者类似的崩溃信息,那调试起来通常会方便很多。
所谓转储,本质上就是尽可能把程序崩溃那一刻的“现场”保存下来。
里面可能包括:
- CPU 寄存器。
- 当前线程信息。
- 调用栈。
- 一部分 RAM。
- Fault 状态寄存器。
- 任务列表。
- 其他和系统有关的运行状态。
具体能保存多少,就看系统本身实现到了什么程度。
有些系统会生成比较完整的转储文件,有些 MCU 项目可能只是把寄存器、栈和关键内存保存到 Flash,之后再导出来分析。
拿到转储以后,通常不需要重新连接当时出问题的设备,也不一定需要调试器。
只要保留了对应版本的 *.elf 文件,再配合 GDB、addr2line 或者厂商提供的分析工具,就可以进行离线分析。
不过这里要注意一点:
并不是随便拿一个“dump 文件”就能直接执行:
gdb xxx.elf xxx.dump
然后什么都自动恢复出来。
这取决于转储文件的格式。
如果系统生成的是 GDB 能识别的 Core Dump,自然可以直接加载;如果只是自己保存的一段寄存器和内存数据,就可能还需要脚本、专用工具或者手动指定内存地址进行分析。
所以“转储文件”更准确地说,是一种保存崩溃现场的方法,至于具体怎么读取,要看项目和系统的实现。
经常被遗忘的方法,主要是获取和分析流程有时候确实挺麻烦。
强大的 AI
如今 AI 已经比 2023 年那时候强大太多了。
尤其是在代码分析和调试方面,它已经不仅仅是“根据一段报错猜原因”。
如果能给它足够的信息,比如:
- Fault 日志。
- 寄存器值。
- 调用栈。
*.map文件。*.elf文件。- 对应的源码。
- 修改前后的 Git diff。
- 系统日志。
现在很多 AI Agent 已经可以主动调用 addr2line、objdump、GDB 等工具,把地址解析成函数,再结合源码继续往下追。
有时候你只看到:
PC = 0x1032e115
LR = 0x10136639
完全不知道发生了什么。
AI 可以先解析地址,再检查对应函数,然后结合寄存器值、调用关系和最近的代码修改,快速帮你缩小范围。
对于自己不熟悉的大型项目,这一点尤其有用。
因为很多时候最困难的不是“怎么修”,而是:
我甚至不知道应该从哪一个文件开始看。
当然,AI 也不是万能的。
如果给它的信息不完整,比如 ELF 和实际运行版本对不上、日志缺失、Bug 无法稳定复现,它一样可能分析错。
所以比较靠谱的方式还是:
让工具负责留下现场,让 AI 帮你整理和分析现场,人最后负责判断结果是否合理。
学 AI 就行了,前面的都不用学(bushi)
总结
遇到普通的逻辑 Bug 其实还好。
只要设备上的程序没有彻底崩溃,还能输出日志,就可以通过打印变量、增加日志、对比代码修改等方式一点一点排查。
最麻烦的还是程序直接崩溃。
比如进入 HardFault 后,正常业务逻辑已经无法继续执行,这个时候能留下来的现场信息非常有限。
因此最好提前做好异常处理和崩溃信息保存机制。
至少应该知道怎么获取:
- 出错时的
PC、LR等关键寄存器。 - Fault 状态寄存器。
- 调用栈或者栈内存。
- 对应版本的
*.elf和*.map文件。
然后学会使用 addr2line、GDB 等工具,把这些看起来毫无意义的地址重新对应到源码上。
调试器当然最好用。
没有调试器,就尽量靠日志、Fault 信息和转储留下现场。
实在看不懂,还可以把这些东西交给 AI 帮忙分析。
不过说到底,调试工具再强,也只是 Bug 出现之后用来救火的。
真正最需要关注的还是代码质量。
不出 Bug 才是最好的调试方法。
文章作者:成元
上次更新:2026-07-22