1. 异常与调试基础:嵌入式世界的“蓝屏”与“手术刀”
各位同学,咱们开始第一讲。做嵌入式开发,最怕什么?不是需求改来改去,也不是加班到凌晨。最怕的是——板子跑着跑着,突然就“死”了。没有任何打印,没有任何征兆,就像被人拔了电源。
这时候,你手里得有两样东西:一是知道“死”的原因(异常类型),二是知道怎么“救活”它(调试工具)。这一章,咱们就把这两样东西聊透。
1.1 嵌入式异常:不只是“死机”那么简单
ARM Cortex-M 内核定义了四种异常,它们就像操作系统的“蓝屏代码”。我刚开始做项目时,一看到 HardFault 就头皮发麻。后来才发现,其实每种异常都有自己的“脾气”。
| 异常类型 | 触发原因(典型) | 优先级(固定) | 我遇到过的坑 |
|---|---|---|---|
| HardFault | 其他异常无法处理时的“兜底”异常 | -1(最高) | 最常见,但信息最少,需要靠寄存器反推 |
| MemManage | 访问禁止区域、MPU 违规 | 可配置 | RTOS 任务栈溢出时经常触发 |
| BusFault | 总线访问错误(地址不对、时序不对) | 可配置 | 外设寄存器地址写错,一写就挂 |
| UsageFault | 指令未定义、对齐错误、除零 | 可配置 | 函数指针跳转到非法地址,最常见 |
核心原则:HardFault 是“最后一道防线”。如果 MemManage、BusFault、UsageFault 都没被使能,它们会“升级”为 HardFault。所以,我建议你一开始就把这三个异常都打开,这样定位问题会快得多。
1.2 异常向量表:CPU 的“紧急联络人”
异常向量表,说白了就是一张“电话本”。CPU 遇到异常时,会查这个表,找到对应的处理函数地址,然后跳过去执行。
这张表放在哪里?默认在 Flash 起始地址(0x00000000)。但很多芯片支持重定位,比如放到 RAM 里。我记得有一次调试,发现中断进不去,查了半天,原来是向量表地址配错了。
// 典型向量表定义(汇编启动文件)
__Vectors DCD __initial_sp ; 栈顶指针
DCD Reset_Handler ; 复位
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; HardFault
DCD MemManage_Handler ; MemManage
DCD BusFault_Handler ; BusFault
DCD UsageFault_Handler ; UsageFault
; ... 后面还有中断向量
调试技巧:用 GDB 查看向量表:(gdb) x/16wx 0x00000000。第一个字是栈顶指针,第二个字是复位入口。如果这两个值看起来不对劲,那大概率是启动文件有问题。
1.3 中断优先级:谁先谁后,有讲究
Cortex-M 内核支持可嵌套中断。优先级高的可以打断优先级低的。但这里有个容易搞混的点:数值越小,优先级越高。
举个例子:
- 优先级 0 的异常,可以打断优先级 1 的异常。
- HardFault 的优先级是 -1,所以它谁都能打断。
- Reset 的优先级是 -3,最高,但复位后一般不谈优先级。
我见过一个案例:某同事把两个中断设成了相同优先级,结果一个中断里调用了另一个中断的 API,导致死锁。嗯,优先级分组也很重要,这个咱们后面讲 RTOS 时会细说。
1.4 调试工具链:你的“手术刀”
没有调试器,嵌入式开发就像蒙着眼睛走钢丝。我个人习惯用这三件套:
1.4.1 JTAG vs SWD:选哪个?
| 特性 | JTAG | SWD |
|---|---|---|
| 引脚数 | 5(TMS, TCK, TDI, TDO, nTRST) | 2(SWDIO, SWCLK) |
| 速度 | 快,但受线缆影响 | 够用,一般 4MHz 以上 |
| 适用场景 | FPGA、多核调试 | 绝大多数 MCU 调试 |
| 我的建议 | 除非你有特殊需求,否则用 SWD | 省引脚,布线方便,够用 |
避坑指南:我曾经遇到过 SWD 接口被复用为 GPIO 的情况。程序一跑,调试口就失效了,再也连不上。解决办法:要么在启动代码里留个窗口期,要么用 JTAG 的复位线强制停止 CPU。
1.4.2 GDB:不只是“跑”和“停”
GDB 是调试界的“瑞士军刀”。很多人只会用 continue 和 break,其实它还有很多高级玩法:
backtrace:查看函数调用栈,定位崩溃点。info registers:查看所有寄存器,包括异常时的状态。monitor reset halt:复位并停在第一条指令。set {int}0x20000000 = 0x1234:直接修改内存,比改代码重新下载快多了。
你想想看,如果不用 GDB,每次改个参数都要重新编译、下载、运行,那得多浪费时间?
1.4.3 OpenOCD:连接调试器和 GDB 的“桥梁”
OpenOCD 负责和调试器硬件(比如 J-Link、ST-Link)通信,然后对外提供 GDB 服务。它的配置文件是关键:
# openocd.cfg 示例
source [find interface/stlink.cfg]
source [find target/stm32f4x.cfg]
# 设置 SWD 模式
transport select swd
# 设置速度
adapter speed 4000
# 初始化
init
targets
reset halt
启动 OpenOCD 后,GDB 就可以通过 target remote localhost:3333 连上去了。整个过程就像搭积木:OpenOCD 是中间件,GDB 是前端,调试器是硬件。
1.5 知识体系总览
下面这张图,把咱们这一章的核心内容串起来了。你可以把它当作一个“地图”,后面每一章都会在这个地图上展开。
这张图里,左边是异常类型,中间是向量表,右边是优先级,下面是调试工具。它们之间的关系是:异常触发 → CPU 查向量表 → 跳转到处理函数 → 我们用调试工具去分析。这个流程,就是咱们这门课的核心逻辑。
我的习惯:每接手一个新项目,第一件事就是确认异常向量表的位置和内容。然后写一个简单的异常处理函数,至少能打印出异常类型和寄存器状态。这样,即使后面出了问题,也能快速定位。