4. HardFault异常深入

在 Cortex-M 内核的异常模型中,HardFault 是一个特殊的存在:它既是“最后一道防线”,也是很多开发者最不愿看到的异常。当其他可配置的故障异常无法处理、被禁用或升级时,处理器会统一进入 HardFault。理解 HardFault 的触发条件、优先级,以及它与 NMI、MemManage、BusFault、UsageFault 的关系,是定位 STM32 死机问题的关键。

一、HardFault 的定位与触发条件

Cortex-M 将异常分为系统异常和外部中断。HardFault 属于系统异常,异常号为 3,优先级固定为 -1。它不依赖 NVIC 的使能位,也不能被屏蔽,只要内核判定当前故障无法由其他机制处理,就会立即进入 HardFault。常见触发条件可以归纳为以下几类:

  • 升级触发:MemManage、BusFault、UsageFault 被禁用,或在其优先级低于当前执行优先级时发生,故障会升级为 HardFault。
  • 取指与总线错误:从非法地址取指、访问不存在的外设区域、总线返回错误响应。
  • 内存保护错误:MPU 配置后越权访问,且 MemManage 未使能或无法处理。
  • 用法错误:除零、非对齐访问、非法状态切换、未定义指令等,且 UsageFault 未使能。
  • 异常返回错误:压栈内容被破坏、EXC_RETURN 非法、栈指针指向非法区域。
  • 向量表异常:向量表未正确重定位,或异常向量内容非法。
关键点:HardFault 不是“独立产生”的故障,而是故障处理链的最终汇聚点。多数 HardFault 的根因,其实在 MemManage、BusFault 或 UsageFault 中。

二、优先级关系与升级机制

在 Cortex-M 中,优先级数值越小,优先级越高。HardFault 固定为 -1,NMI 固定为 -2,因此 NMI 高于 HardFault,HardFault 高于所有可配置异常。MemManage、BusFault、UsageFault 的优先级由 SHPR 寄存器配置,通常可设为高于或低于当前执行优先级。

升级机制的核心规则是:当某个故障异常被禁用,或者其优先级不高于当前执行优先级时,该故障无法被单独处理,处理器会将其升级为 HardFault。例如,若 BusFault 未使能,一次外设总线访问错误不会进入 BusFault_Handler,而是直接进入 HardFault_Handler。若 BusFault 已使能但优先级低于当前中断,同样会升级。

异常 异常号 优先级 是否可屏蔽 典型触发
NMI 2 -2 否 外部 NMI 引脚、时钟安全系统
HardFault 3 -1 否 故障升级、异常返回错误
MemManage 4 可配置 可 MPU 越权访问
BusFault 5 可配置 可 总线访问错误、外设不存在
UsageFault 6 可配置 可 除零、非对齐、非法指令

三、与 NMI、MemManage、BusFault、UsageFault 的关系

NMI 与 HardFault 都是不可屏蔽异常,但 NMI 优先级更高。NMI 通常用于时钟失效、外部紧急事件等场景,它不会升级为 HardFault,而是直接抢占。若 NMI 服务程序本身触发故障,则可能进一步进入 HardFault。

MemManage、BusFault、UsageFault 是“可配置故障异常”。它们与 HardFault 的关系可以理解为“分流”与“兜底”:使能并正确配置优先级后,故障进入各自处理程序,便于精确定位;未使能或优先级不足时,故障统一汇入 HardFault。因此,调试阶段建议使能全部故障异常,并给它们分配较高优先级。

// 使能 MemManage、BusFault、UsageFault
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk
           |  SCB_SHCSR_BUSFAULTENA_Msk
           |  SCB_SHCSR_USGFAULTENA_Msk;

// 设置优先级(数值越小优先级越高)
NVIC_SetPriority(MemoryManagement_IRQn, 0);
NVIC_SetPriority(BusFault_IRQn, 0);
NVIC_SetPriority(UsageFault_IRQn, 0);

下面这张图展示了故障异常的分流与升级关系:可配置故障异常在使能且优先级足够时进入各自处理程序,否则统一升级为 HardFault;NMI 则独立于该链路之外,优先级最高。

Cortex-M 故障异常分流与升级关系 NMI 优先级 -2 · 不可屏蔽 故障源 MPU越权 / 总线错误 / 用法错误 MemManage 异常号 4 · 可配置 MPU 越权访问 BusFault 异常号 5 · 可配置 总线访问错误 UsageFault 异常号 6 · 可配置 除零 / 非对齐 / 非法指令 HardFault 异常号 3 · 优先级 -1 · 不可屏蔽 升级条件:故障异常未使能 或 优先级不高于当前执行优先级 NMI 独立抢占,不参与升级 故障分流 升级为 HardFault NMI 独立路径

四、典型场景与调试建议

场景一:程序跑飞后进入 HardFault_Handler,但无法判断根因。此时应检查 SHCSR 是否使能了三个可配置故障异常。若未使能,所有故障都会汇入 HardFault,丢失了精确的故障类型信息。

场景二:使能 BusFault 后,访问未初始化的外设时钟区域,程序进入 BusFault_Handler。通过 BFAR 寄存器可读取出错地址,快速定位到具体外设。

场景三:除零操作触发 UsageFault。若 UsageFault 未使能,则升级为 HardFault;使能后可在 UsageFault_Handler 中读取 CFSR 的 DIVBYZERO 位确认原因。

常见误区:认为“使能了 MemManage/BusFault/UsageFault 就不会进 HardFault”。实际上,若这些异常的优先级低于当前执行优先级,仍然会升级为 HardFault。调试阶段应将其优先级设为最高(0),确保故障能被精确捕获。

五、复盘要点

  1. HardFault 是故障处理链的兜底异常,优先级固定为 -1,不可屏蔽。
  2. NMI 优先级 -2,高于 HardFault,独立处理,不参与升级。
  3. MemManage、BusFault、UsageFault 是可配置故障异常,使能且优先级足够时进入各自处理程序。
  4. 未使能或优先级不足时,故障升级为 HardFault。
  5. 调试时建议使能全部故障异常并设为最高优先级,配合 CFSR、HFSR、BFAR、MMFAR 寄存器定位根因。
// 读取关键故障状态寄存器
uint32_t hfsr  = SCB->HFSR;   // HardFault 状态
uint32_t cfsr  = SCB->CFSR;   // 可配置故障状态
uint32_t bfar  = SCB->BFAR;   // 总线故障地址
uint32_t mmfar = SCB->MMFAR;  // MPU 故障地址

掌握 HardFault 与 NMI、MemManage、BusFault、UsageFault 的优先级和升级关系,才能在 STM32 出现死机时快速判断故障来源,避免在 HardFault_Handler 中盲目排查。