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 非法、栈指针指向非法区域。
- 向量表异常:向量表未正确重定位,或异常向量内容非法。
二、优先级关系与升级机制
在 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 则独立于该链路之外,优先级最高。
四、典型场景与调试建议
场景一:程序跑飞后进入 HardFault_Handler,但无法判断根因。此时应检查 SHCSR 是否使能了三个可配置故障异常。若未使能,所有故障都会汇入 HardFault,丢失了精确的故障类型信息。
场景二:使能 BusFault 后,访问未初始化的外设时钟区域,程序进入 BusFault_Handler。通过 BFAR 寄存器可读取出错地址,快速定位到具体外设。
场景三:除零操作触发 UsageFault。若 UsageFault 未使能,则升级为 HardFault;使能后可在 UsageFault_Handler 中读取 CFSR 的 DIVBYZERO 位确认原因。
五、复盘要点
- HardFault 是故障处理链的兜底异常,优先级固定为 -1,不可屏蔽。
- NMI 优先级 -2,高于 HardFault,独立处理,不参与升级。
- MemManage、BusFault、UsageFault 是可配置故障异常,使能且优先级足够时进入各自处理程序。
- 未使能或优先级不足时,故障升级为 HardFault。
- 调试时建议使能全部故障异常并设为最高优先级,配合 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 中盲目排查。