3. 异常与中断机制
在 STM32 的 Cortex-M 内核中,“中断”只是“异常”的一个子集。理解异常与中断机制,是定位 HardFault 的根基:HardFault 本身就是一种异常,它之所以被触发、被谁抢占、返回时栈里留下什么,全部由异常模型决定。本节从异常类型、优先级、向量表、响应流程到异常返回,建立一条完整的因果链。
一、异常类型:内核异常与外设中断
Cortex-M 把异常分为两大类:编号 1~15 的内核异常(系统异常),以及编号 16 及以上的外部中断(IRQ)。内核异常由处理器自身或系统服务触发,外设中断由 NVIC 管理的外设请求线触发。二者共用同一套优先级与响应机制,区别只在于来源和编号区间。
| 编号 | 名称 | 类型 | 典型触发源 | 与 HardFault 的关系 |
|---|---|---|---|---|
| 1 | Reset | 内核异常 | 上电、复位引脚、看门狗 | 复位后从向量表取初始值 |
| 2 | NMI | 内核异常 | 不可屏蔽中断源 | 优先级固定最高,可抢占一切 |
| 3 | HardFault | 内核异常 | 取指/访存/总线错误、升级失败 | 本节核心对象 |
| 4 | MemManage | 内核异常 | MPU 访问违规 | 未使能时升级为 HardFault |
| 5 | BusFault | 内核异常 | 总线访问错误 | 未使能时升级为 HardFault |
| 6 | UsageFault | 内核异常 | 非法指令、除零、非对齐 | 未使能时升级为 HardFault |
| 11 | SVCall | 内核异常 | SVC 指令 | 常用于 RTOS 系统调用 |
| 14 | PendSV | 内核异常 | 软件挂起 | RTOS 任务切换常用 |
| 15 | SysTick | 内核异常 | 系统定时器 | 时基与调度节拍 |
| 16+ | IRQn | 外设中断 | GPIO、USART、TIM 等 | 配置错误可间接引发故障 |
关键点在于:MemManage、BusFault、UsageFault 默认是“关闭”的,一旦发生对应错误,硬件会把它升级为 HardFault。这就是为什么很多工程只看到 HardFault,却看不到更精确的故障类型——不是没有原因,而是原因被“合并”了。
二、优先级:抢占与子优先级
Cortex-M 的优先级数值越小,优先级越高。每个异常都有一个 8 位优先级寄存器,但 STM32 通常只实现高 4 位,因此可配置 16 级。优先级被划分为“抢占优先级”和“子优先级”两组,由 NVIC 的优先级分组决定。
- 抢占优先级:决定能否打断正在执行的异常。高抢占优先级可以嵌套进低抢占优先级的处理函数。
- 子优先级:仅在多个异常同时挂起、抢占优先级相同时决定谁先执行,不能嵌套。
- NMI 与 HardFault:优先级固定为负值,高于所有可配置异常,任何外设中断都无法抢占它们。
三、向量表:异常入口的地址簿
向量表是一张存放在 Flash 起始处的地址数组,每个表项 4 字节,保存对应异常的入口地址。复位时,内核从地址 0x00000000 取初始 MSP,从 0x00000004 取 Reset_Handler 地址。之后每个异常发生时,硬件自动从向量表取出对应入口并跳转。
__Vectors:
DCD __initial_sp ; 0x00 初始栈顶
DCD Reset_Handler ; 0x04 复位
DCD NMI_Handler ; 0x08 NMI
DCD HardFault_Handler ; 0x0C HardFault
DCD MemManage_Handler ; 0x10 MemManage
DCD BusFault_Handler ; 0x14 BusFault
DCD UsageFault_Handler ; 0x18 UsageFault
...
DCD WWDG_IRQHandler ; 外设中断入口
向量表可以通过 SCB->VTOR 重定位,例如把向量表搬到 SRAM 或由 Bootloader 指定新位置。若 VTOR 被错误修改,异常入口就会指向非法地址,直接引发新的 HardFault,形成“故障套故障”。
四、中断响应流程:硬件自动完成的三件事
当异常被内核接受后,硬件会按固定顺序完成以下动作,无需软件干预:
- 压栈:把 xPSR、PC、LR、R12、R3、R2、R1、R0 共 8 个寄存器压入当前使用的栈。若当前使用 PSP,则压入 PSP;若使用 MSP,则压入 MSP。
- 取向量:从 VTOR 指向的向量表中取出对应异常入口地址,加载到 PC。
- 更新 LR:把 EXC_RETURN 值写入 LR,用于异常返回时判断返回模式和使用哪个栈。
压栈顺序是固定的,这为故障分析提供了极大便利:只要找到异常发生时的栈指针,就能按固定偏移还原现场。HardFault 处理函数中常见的做法,就是判断 LR 的 bit2 来确定使用的是 MSP 还是 PSP,再取出压栈的 PC 值,定位出错指令。
void HardFault_Handler(void)
{
__asm volatile (
"tst lr, #4 \n" // 判断 EXC_RETURN bit2
"ite eq \n"
"mrseq r0, msp \n" // 使用 MSP
"mrsne r0, psp \n" // 使用 PSP
"b hardfault_report\n"
);
}
// 栈帧中偏移 0x18 处即为出错时的 PC
五、异常返回机制:EXC_RETURN 的语义
异常返回不是普通的函数返回,而是通过向 PC 写入 EXC_RETURN 值触发。EXC_RETURN 的高 28 位全为 1,低 4 位携带返回信息:bit2 表示返回后使用哪个栈,bit3 表示返回后进入 Thread 还是 Handler 模式,bit4 表示是否恢复浮点上下文。
- 0xFFFFFFF1:返回 Handler 模式,使用 MSP。
- 0xFFFFFFF9:返回 Thread 模式,使用 MSP。
- 0xFFFFFFFD:返回 Thread 模式,使用 PSP,RTOS 任务切换后常见。
六、案例与复盘:一次“看不见的 BusFault”
某工程访问了一个未使能时钟的外设寄存器,理论上应触发 BusFault,但调试时只看到 HardFault。复盘发现:BusFault 使能位未打开,硬件按规则将其升级为 HardFault。处理方式是在系统初始化中打开 SCB->SHCSR 的 BUSFAULTENA、USGFAULTENA、MEMFAULTENA,让故障类型精确暴露。
SCB->SHCSR |= SCB_SHCSR_USGFAULTENA_Msk
| SCB_SHCSR_BUSFAULTENA_Msk
| SCB_SHCSR_MEMFAULTENA_Msk;
打开后再次复现,BusFault 处理函数被正确触发,读取 SCB->CFSR 的 BFARVALID 与 BFAR,直接定位到非法访问地址。这说明:HardFault 分析的第一步,往往不是“猜原因”,而是先确认更精确的故障异常是否被使能。异常类型、优先级、向量表、响应流程与返回机制,共同构成了这条诊断链的骨架。