MCU开发学习记录(3):崩溃的调试

2026-07-22 / 约 2678 字 / 预计阅读 6 分钟 { 教程, 笔记 } [ GD32, STM32, MCU ]

前言

最近经常遇到各种各样的 Bug,有些只是逻辑不正常,还有一些会直接导致 MCU 进入 HardFault 等异常,整个程序基本卡死。

因此最近需要频繁进行调试,想办法找到 Bug 到底是从哪里冒出来的。

逻辑 Bug 倒还好说,至少程序通常还能继续运行,可以打日志、看变量,一点一点缩小问题范围。

真正麻烦的是程序直接崩掉。

所以这篇就简单记录一下,我目前知道或者实际用过的一些 MCU 调试方法。

前提

前提是做好备份!

比如利用 Git 单独开一个 fix 分支,专门用来调试和修复当前的问题。

总之,在开始修 Bug 前,最好保留:

每次修改后,也要记得及时用 Git 或者其他方式保存。

不然改着改着,Bug 突然消失了,结果你自己都不知道到底是哪一处修改把它修好的,那就有点尴尬了。

更糟糕的是,原来的 Bug 没找到,又因为调试过程中到处乱改代码,引入了一个新的 Bug。

所以修 Bug 的第一步,可能不是马上开始改代码,而是先保证自己随时可以回到问题出现时的现场

方法

下面是我目前知晓的一些调试方法。

调试器

这个应该是最直接、最好用的办法了。

一般人学习 STM32 的时候,肯定接触过类似 ST-LINK 这样的调试器,再配合 GDB、OpenOCD 或者 IDE 自带的调试功能,就可以进行断点、单步执行、查看变量、寄存器、内存、调用栈等操作。

程序卡在哪里,哪个变量突然变成了奇怪的值,函数是怎么一层一层调用过来的,很多问题都可以直接看到。

所以这里就不详细介绍了。

你如果能搜到这篇文章,大概至少也是入门水平了,应该不需要我从“什么是断点”开始讲起。

总之:

有条件用调试器的时候,优先用调试器。

它能保留最多的现场信息,通常也是定位问题最方便的方法。

HardFault 等异常 + 串口

但是有的时候,因为硬件条件、接口占用、设备已经装机或者其他各种各样的原因,就是没办法一直挂着调试器。

这个时候如果程序出现问题,就只能尽量依靠串口日志,以及 MCU 的异常处理机制来留下线索了。

串口最基本的用途当然就是输出日志。

比如:

对于普通的逻辑 Bug,这些日志往往已经很好用了。

而如果程序发生了更严重的错误,比如非法访问内存、执行了错误的指令地址,Cortex-M 内核可能会进入 HardFaultMemManageBusFaultUsageFault 等异常处理流程。

这个时候可以在异常处理函数里尽量保存并输出一些关键现场信息,例如:

拿到这些地址以后,再配合发生崩溃时对应版本的 *.elf 文件,就可以使用 addr2line、GDB 等工具把地址解析到具体的函数、源文件甚至代码行。

例如 addr2line 本质上就是利用 ELF 中的符号和调试信息,把:

0x1032e115

这种看起来完全不知道是什么东西的地址,转换成类似:

xxx_function
src/xxx.c:123

这样的信息。

当然,前提是 ELF 必须和设备当时运行的固件对应。

代码重新编译过以后,函数地址很可能已经发生变化。拿一个新版本的 ELF 去分析旧版本程序留下来的崩溃地址,很可能直接查歪。

所以这也再次证明了前面为什么要强调:

一定要保留出问题版本对应的固件和 ELF。

比较麻烦,如果不能用调试器,还是丢给 AI 吧……

转储文件

如果系统本身支持保存 Crash Dump、Core Dump 或者类似的崩溃信息,那调试起来通常会方便很多。

所谓转储,本质上就是尽可能把程序崩溃那一刻的“现场”保存下来。

里面可能包括:

具体能保存多少,就看系统本身实现到了什么程度。

有些系统会生成比较完整的转储文件,有些 MCU 项目可能只是把寄存器、栈和关键内存保存到 Flash,之后再导出来分析。

拿到转储以后,通常不需要重新连接当时出问题的设备,也不一定需要调试器。

只要保留了对应版本的 *.elf 文件,再配合 GDB、addr2line 或者厂商提供的分析工具,就可以进行离线分析。

不过这里要注意一点:

并不是随便拿一个“dump 文件”就能直接执行:

gdb xxx.elf xxx.dump

然后什么都自动恢复出来。

这取决于转储文件的格式。

如果系统生成的是 GDB 能识别的 Core Dump,自然可以直接加载;如果只是自己保存的一段寄存器和内存数据,就可能还需要脚本、专用工具或者手动指定内存地址进行分析。

所以“转储文件”更准确地说,是一种保存崩溃现场的方法,至于具体怎么读取,要看项目和系统的实现。

经常被遗忘的方法,主要是获取和分析流程有时候确实挺麻烦。

强大的 AI

如今 AI 已经比 2023 年那时候强大太多了。

尤其是在代码分析和调试方面,它已经不仅仅是“根据一段报错猜原因”。

如果能给它足够的信息,比如:

现在很多 AI Agent 已经可以主动调用 addr2lineobjdump、GDB 等工具,把地址解析成函数,再结合源码继续往下追。

有时候你只看到:

PC = 0x1032e115
LR = 0x10136639

完全不知道发生了什么。

AI 可以先解析地址,再检查对应函数,然后结合寄存器值、调用关系和最近的代码修改,快速帮你缩小范围。

对于自己不熟悉的大型项目,这一点尤其有用。

因为很多时候最困难的不是“怎么修”,而是:

我甚至不知道应该从哪一个文件开始看。

当然,AI 也不是万能的。

如果给它的信息不完整,比如 ELF 和实际运行版本对不上、日志缺失、Bug 无法稳定复现,它一样可能分析错。

所以比较靠谱的方式还是:

让工具负责留下现场,让 AI 帮你整理和分析现场,人最后负责判断结果是否合理。

学 AI 就行了,前面的都不用学(bushi)

总结

遇到普通的逻辑 Bug 其实还好。

只要设备上的程序没有彻底崩溃,还能输出日志,就可以通过打印变量、增加日志、对比代码修改等方式一点一点排查。

最麻烦的还是程序直接崩溃。

比如进入 HardFault 后,正常业务逻辑已经无法继续执行,这个时候能留下来的现场信息非常有限。

因此最好提前做好异常处理和崩溃信息保存机制。

至少应该知道怎么获取:

然后学会使用 addr2line、GDB 等工具,把这些看起来毫无意义的地址重新对应到源码上。

调试器当然最好用。

没有调试器,就尽量靠日志、Fault 信息和转储留下现场。

实在看不懂,还可以把这些东西交给 AI 帮忙分析。

不过说到底,调试工具再强,也只是 Bug 出现之后用来救火的。

真正最需要关注的还是代码质量。

不出 Bug 才是最好的调试方法。


文章作者:成元
上次更新:2026-07-22