Cortex-M HardFault 现场信息的本地保存
问题背景
现场设备的崩溃往往难以复现:没有调试器、串口日志来不及输出,看门狗复位后现场又被清空。比较实用的做法是:在 HardFault 中只采集必要的 CPU 现场,先写入保留 RAM,重启后再由正常代码持久化或上报。
需要先区分:电源跌落、外部复位和看门狗通常表现为“复位”,不一定会进入 HardFault。HardFault 更常见于非法取指、错误的函数指针、栈溢出、访问未映射地址,或其他可配置 Fault 向上升级。
Cortex-M 型号差异
- Cortex-M0/M0+ 主要提供 HardFault,没有 M3/M4/M7 上那套可分开处理的 MemManage、BusFault 和 UsageFault。
- 对支持可配置 Fault 的内核,使能位在
SCB->SHCSR,不是SCB->CCR。CMSIS 常用掩码为SCB_SHCSR_MEMFAULTENA_Msk、SCB_SHCSR_BUSFAULTENA_Msk和SCB_SHCSR_USGFAULTENA_Msk。 - CFSR、MMFAR、BFAR 等寄存器是否存在、哪些位有效,必须以具体内核和芯片手册为准。
正确找到故障栈帧
异常进入时,处理器会把 R0–R3、R12、LR、PC 和 xPSR 压入当时使用的栈。进入 Handler 后 CPU 使用 MSP,所以在普通 C 函数里直接调 __get_MSP() 并不能保证拿到故障前的栈帧。常见做法是在裸函数里检查 EXC_RETURN(LR)的 bit 2,把 MSP 或 PSP 传给 C 函数:
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile (
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"mov r1, lr \n"
"b hardfault_save \n");
}这段汇编语法适用于 GCC/armclang 风格工具链;旧版 ARMCC/IAR 需要换成对应语法。hardfault_save(stack, exc_return) 可以保存原始栈指针、EXC_RETURN 和 SCB 状态寄存器。若系统使用 FPU,EXC_RETURN 的 bit 4 可用来判断是否涉及扩展浮点栈帧;懒堆栈还会带来额外边界,建议保存原始值后离线解码,不要在 Fault Handler 里做复杂判断。
建议保存的信息
- 自动压栈的 R0–R3、R12、LR、PC、xPSR;
- EXC_RETURN 和原始 MSP/PSP;
- 该内核实际支持的 HFSR、CFSR、MMFAR、BFAR、SHCSR;
- 任务 ID、固件版本、复位计数和校验码。
对 MMFAR/BFAR 要先检查对应的地址有效位,不能默认寄存器中总是有效地址。
先写保留 RAM,重启后再持久化
直接在 HardFault 中擦写内部 Flash 风险很高:故障本身可能来自总线、Flash 或栈损坏,Flash 擦除还需要时序、对齐和中断约束。更稳妥的两阶段方案是:
- 预留一块启动代码不清零的 RAM,Fault Handler 只写固定长度记录、魔数和 CRC;
- 复位后在正常上下文校验记录,再写入 Flash/文件系统或上报,成功后清除魔数。
这要求确认目标 MCU 在相应复位类型下会保留 SRAM;掉电时当然无法依靠普通 RAM。如果必须在 Handler 中写非易失存储,应使用经过故障注入测试的最小驱动,不调用 printf、malloc 或文件系统。
验证建议
- 分别构造未定义指令、非法地址访问和错误函数指针,对照调试器检查记录的 PC/LR 和 Fault 状态。
- 在使用 PSP 的 RTOS 任务中触发故障,确认捕获的不是 Handler 自己的 MSP。
- 测试普通栈帧和 FPU 扩展栈帧(若芯片支持)。
- 做故障注入:模拟保存代码再次出错、保留 RAM 校验失败以及快速连续复位。
小结
HardFault 日志的重点不是在异常里做更多事,而是以最少依赖保留可靠的原始现场。先用 EXC_RETURN 找对栈帧,再按内核型号读取有效的 SCB 寄存器,最后把持久化和解码放到重启后完成。
<!-- csdn-article-id: 129227864 -->本文最初于 2023/2/26 发布在 CSDN。
相关文章
评论
正在读取评论…