3. 异常与中断机制

在 STM32 的 Cortex-M 内核中,“中断”只是“异常”的一个子集。理解异常与中断机制,是定位 HardFault 的根基:HardFault 本身就是一种异常,它之所以被触发、被谁抢占、返回时栈里留下什么,全部由异常模型决定。本节从异常类型、优先级、向量表、响应流程到异常返回,建立一条完整的因果链。

一、异常类型:内核异常与外设中断

Cortex-M 把异常分为两大类:编号 1~15 的内核异常(系统异常),以及编号 16 及以上的外部中断(IRQ)。内核异常由处理器自身或系统服务触发,外设中断由 NVIC 管理的外设请求线触发。二者共用同一套优先级与响应机制,区别只在于来源和编号区间。

编号名称类型典型触发源与 HardFault 的关系
1Reset内核异常上电、复位引脚、看门狗复位后从向量表取初始值
2NMI内核异常不可屏蔽中断源优先级固定最高,可抢占一切
3HardFault内核异常取指/访存/总线错误、升级失败本节核心对象
4MemManage内核异常MPU 访问违规未使能时升级为 HardFault
5BusFault内核异常总线访问错误未使能时升级为 HardFault
6UsageFault内核异常非法指令、除零、非对齐未使能时升级为 HardFault
11SVCall内核异常SVC 指令常用于 RTOS 系统调用
14PendSV内核异常软件挂起RTOS 任务切换常用
15SysTick内核异常系统定时器时基与调度节拍
16+IRQn外设中断GPIO、USART、TIM 等配置错误可间接引发故障

关键点在于:MemManage、BusFault、UsageFault 默认是“关闭”的,一旦发生对应错误,硬件会把它升级为 HardFault。这就是为什么很多工程只看到 HardFault,却看不到更精确的故障类型——不是没有原因,而是原因被“合并”了。

二、优先级:抢占与子优先级

Cortex-M 的优先级数值越小,优先级越高。每个异常都有一个 8 位优先级寄存器,但 STM32 通常只实现高 4 位,因此可配置 16 级。优先级被划分为“抢占优先级”和“子优先级”两组,由 NVIC 的优先级分组决定。

  • 抢占优先级:决定能否打断正在执行的异常。高抢占优先级可以嵌套进低抢占优先级的处理函数。
  • 子优先级:仅在多个异常同时挂起、抢占优先级相同时决定谁先执行,不能嵌套。
  • NMI 与 HardFault:优先级固定为负值,高于所有可配置异常,任何外设中断都无法抢占它们。
调试建议:在排查 HardFault 时,先确认优先级分组设置。若分组配置与 RTOS 或库函数预期不一致,可能出现“该抢占的没抢占、不该嵌套的嵌套了”,从而让故障现场更难复现。

三、向量表:异常入口的地址簿

向量表是一张存放在 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,形成“故障套故障”。

Cortex-M 异常响应与返回流程 主程序执行 Thread 模式 / MSP 异常请求 硬件自动压栈 xPSR, PC, LR, R12 R3, R2, R1, R0 取向量并跳转 VTOR + 4×异常号 加载入口地址 异常处理函数 ISR LR = EXC_RETURN,标识返回模式与使用栈 可读取 SCB->CFSR / HFSR 定位故障原因 异常返回 写 EXC_RETURN 到 PC 触发硬件出栈 恢复现场 寄存器出栈 回到被中断指令 HardFault 分支 升级机制触发 优先级固定为 -1 硬件自动压栈 取向量跳转 ISR 执行 异常返回 HardFault 升级

四、中断响应流程:硬件自动完成的三件事

当异常被内核接受后,硬件会按固定顺序完成以下动作,无需软件干预:

  1. 压栈:把 xPSR、PC、LR、R12、R3、R2、R1、R0 共 8 个寄存器压入当前使用的栈。若当前使用 PSP,则压入 PSP;若使用 MSP,则压入 MSP。
  2. 取向量:从 VTOR 指向的向量表中取出对应异常入口地址,加载到 PC。
  3. 更新 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 任务切换后常见。
常见误区:在 ISR 中手动修改 LR 或直接跳转,会破坏 EXC_RETURN 语义,导致出栈时使用错误的栈指针,最终表现为“返回后跑飞”或再次 HardFault。若必须在 ISR 中切换任务,应通过 PendSV 完成,而不是强行改写返回路径。

六、案例与复盘:一次“看不见的 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 分析的第一步,往往不是“猜原因”,而是先确认更精确的故障异常是否被使能。异常类型、优先级、向量表、响应流程与返回机制,共同构成了这条诊断链的骨架。