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 知识体系总览

下面这张图,把咱们这一章的核心内容串起来了。你可以把它当作一个“地图”,后面每一章都会在这个地图上展开。

嵌入式异常与调试基础 · 知识体系 异常类型 HardFault MemManage BusFault UsageFault 异常向量表 栈顶指针 复位入口 异常处理函数 中断处理函数 中断优先级 HardFault (-1) 可配置 (0~255) 优先级分组 调试工具链 JTAG / SWD GDB OpenOCD 数值越小 优先级越高

这张图里,左边是异常类型,中间是向量表,右边是优先级,下面是调试工具。它们之间的关系是:异常触发 → CPU 查向量表 → 跳转到处理函数 → 我们用调试工具去分析。这个流程,就是咱们这门课的核心逻辑。

我的习惯:每接手一个新项目,第一件事就是确认异常向量表的位置和内容。然后写一个简单的异常处理函数,至少能打印出异常类型和寄存器状态。这样,即使后面出了问题,也能快速定位。