ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 HardFault 排查教程:3 步从寄存器反推崩溃现场

STM32 HardFault 排查教程:3 步从寄存器反推崩溃现场 背景HardFault 为什么难排查嵌入式崩溃里HardFault 是最让人头疼的一种。别的 bug 好歹留点线索——log、调用栈、core dumpHardFault 一进 handler你面对的往往只有一行串口输出HARD FAULT外加一个卡死在while(1)里的处理器连是哪个任务、哪行代码触发的都不知道。但这里有个反直觉的事实Cortex-M 在跳进 HardFault 之前硬件已经把现场保存好了。本文说明如何从寄存器反推崩溃现场三步定位到出事的 C 代码每一步都有可直接使用的代码。适用环境ARM Cortex-M3 / M4 / M7M0 无 UsageFault/BusFault 细分部分位不可用工具链 arm-none-eabi-gccbinutils 含 addr2line。一、处理器给你留了什么现场进 HardFault 前硬件自动压栈栈帧布局固定栈帧布局硬件自动压栈顺序固定 [SP0] r0 [SP4] r1 [SP8] r2 [SP12] r3 [SP16] r12 [SP20] LR ← 调用者的返回地址 [SP24] PC ← 触发异常的指令地址 [SP28] xPSR ← 程序状态字八个寄存器正好 32 字节出事的指令地址就放在SP24。问题只剩一个该用哪根栈指针去读。Cortex-M 有两根栈指针——MSP 和 PSP裸机默认用 MSP带 RTOS 的 task 级代码用 PSP。异常进入时用的是哪根编码在 LR 寄存器的 bit 2 里。除了栈帧两个系统寄存器告诉你「出了什么事」寄存器地址含义CFSR0xE000ED28故障类型非法指令、除零、非对齐访问、总线错误、MPU 违规HFSR0xE000ED2CHardFault 是被强制的下级 handler 升级还是向量表问题CFSR 是 32 位实际是三个寄存器拼的CFSR 字节级结构 [31:16] UFSR (UsageFault) — 非法指令、除零、非对齐、未定义指令 [15:8] BFSR (BusFault) — 总线访问错误、精确/不精确数据访问 [7:0] MMFSR (MemManage) — MPU 违规、在 XN 区域执行代码一个容易漏掉的细节CFSR 是 write-1-to-clear读不会清读完要往对应位写 1 清零否则旧标志会残留到下次故障。完整的寄存器定义见 ARMv7-M 架构参考手册ARM DDI 0403故障分析的经典参考资料是 Memfault 的 Cortex-M 故障调试指南。二、三步定位核心思路LR 告诉你栈在哪 → CFSR 告诉你什么类型的错 → 栈帧里的 PC 告诉你谁干的。第一步从 LR 确定栈指针voidHardFault_Handler(void){uint32_t*stack_frame;__asmvolatile(TST lr, #4 \n// 检查 LR bit 2ITE EQ \n// If-Then-ElseMRSEQ %[sf], msp \n// bit20 → MSPMRSNE %[sf], psp \n// bit21 → PSP:[sf]r(stack_frame));uint32_tfault_pcstack_frame[6];// SP24uint32_tfault_returnstack_frame[5];// SP20uint32_tfault_xpsrstack_frame[7];// SP28}这段汇编是整件事最值钱的三行。跑 FreeRTOS 的 taskLR 的 bit 2 几乎一定是 1——干活的是 PSP。拿到 fault_pc 后挂着 GDB 的话info line *fault_pc直接看源码行产品已发出去、只剩串口 log就往下走。第二步读 CFSR分类故障类型volatileuint32_t*cfsr(volatileuint32_t*)0xE000ED28;uint32_tcfsr_val*cfsr;对照下表定位故障类型常用位CFSR 位名称含义bit 25DIVBYZERO除以零使能 DIV_0_TRP 时bit 24UNALIGNED非对齐访问uint32_t*指到奇数地址bit 19NOCPFPU 没使能却用了浮点指令bit 18INVPCEXC_RETURN 非法栈被写烂bit 17INVSTATE非 Thumb 态执行指令函数指针指到数据区bit 16UNDEFINSTR未定义指令栈被写烂或跳到 BSSbit 15BFARVALIDBFAR 存了总线故障地址bit 12STKERR异常入栈总线故障栈溢出典型标志bit 11UNSTKERR异常出栈总线故障栈在 handler 被写烂bit 9PRECISERR精确总线故障能定位地址bit 8IMPRECISERR不精确总线故障最麻烦bit 7MMARVALIDMMFAR 存了 MPU 违规地址bit 1DACCVIOL数据访问触发 MPU 违规bit 0IACCVIOL在 XN 区域执行代码一个够用的判断骨架if(cfsr_val(112)){// STKERR 栈溢出查 task 栈大小FreeRTOS 看 uxTaskGetStackHighWaterMark}elseif(cfsr_val(125)){// DIVBYZERO查 fault_pc 附近除法}elseif(cfsr_val(124)){// UNALIGNED查 fault_pc 附近指针有 BFARVALID 则读 *(uint32_t*)0xE000ED38}elseif(cfsr_val(18)){// IMPRECISERR见文末局限}典型翻车案例蓝牙配对后频繁 HardFaultCFSR 0x00001000STKERR 置位。读 PSP 发现离栈底只剩 64 字节——配对时栈需求暴增原来 1024 字节的栈不够。第三步反向符号化产品发出去不可能挂 OpenOCD串口只能打出 fault_pc 的十六进制地址。编译时保留符号运行时把地址吐出来回编译机对照# 编译时保留 .elfarm-none-eabi-gcc...-ofirmware.elf# 拿到 fault_pc比如 0x08001634arm-none-eabi-addr2line-efirmware.elf 0x08001634# 输出: src/uart_handler.c:89加-f -C连函数名一起打出来-C还原 C 符号。没有 .elf 的退路用 linker 的.map文件搜地址落在哪个函数范围能到函数精确不到行连 .elf 都丢了就arm-none-eabi-objdump -d反汇编搜地址。三、写一个「会说话」的 HardFault Handler上面三步可以全自动化把HardFault_Handler改造成这样typedefstruct__attribute__((packed)){uint32_tr0,r1,r2,r3,r12,lr,pc,xpsr;}FaultFrame;voiddump_fault(FaultFrame*frame){volatileuint32_t*cfsr(volatileuint32_t*)0xE000ED28;volatileuint32_t*hfsr(volatileuint32_t*)0xE000ED2C;volatileuint32_t*bfar(volatileuint32_t*)0xE000ED38;printf( HARDFAULT \n);printf(PC: 0x%08lX LR: 0x%08lX\n,frame-pc,frame-lr);printf(CFSR: 0x%08lX HFSR: 0x%08lX\n,*cfsr,*hfsr);if(*cfsr(115))printf(BFAR: 0x%08lX\n,*bfar);if(*cfsr(112))printf(TYPE: Stack overflow on exception entry\n);if(*cfsr(125))printf(TYPE: Divide by zero\n);if(*cfsr(124))printf(TYPE: Unaligned access\n);if(*cfsr(18))printf(TYPE: Imprecise bus fault\n);printf(R0:0x%08lX R1:0x%08lX R2:0x%08lX R3:0x%08lX\n,frame-r0,frame-r1,frame-r2,frame-r3);while(1);}voidHardFault_Handler(void){FaultFrame*frame;__asmvolatile(TST lr, #4 \nITE EQ \nMRSEQ %[sf], msp \nMRSNE %[sf], psp \n:[sf]r(frame));dump_fault(frame);}这套代码相当于一个「产线版调试器」设备一炸串口直接吐出 PC、CFSR、HFSR 和所有通用寄存器回编译机一跑addr2line就是源码行号。更进一步可以把 fault_pc CFSR 存进 Flash 日志区下次上电上报后台。另外 FreeRTOS、Zephyr、mbed-os 的发行版都自带增强版HardFault_Handler用这些 RTOS 的话先翻zephyr/fatal.c或mbed_fault_handler.c轮子可能早就有了。四、三种救不了的情况1. 不精确总线故障IMPRECISERR。CFSR bit 8 置位时栈帧里的 PC 不指向触发指令——写操作被 buffering 延迟处理器先执行了后续指令写操作才落到总线被拒。Cortex-M3/M4 可关写缓冲*(volatileuint32_t*)0xE000E008|(11);// ACTLR.DISDEFWBUF让 IMPRECISE 变 PRECISE注意 Cortex-M7 没有这个开关写缓冲是硬连线行为关不掉只能检查异常地址附近所有写操作或靠 ETM 回放指令历史。2. 锁死状态Lockup。HardFault_Handler 内部又出 HardFault处理器进入不可恢复循环反复取指0xFFFFFFFE。根因几乎总是栈在进 handler 前就被完全写毁STKERR硬件连 8 个寄存器都压不进栈。3. 栈帧本身被破坏。崩溃前 PSP 已被指向非法地址比如被 DMA 写坏硬件压栈的 32 字节写进无效区域读到的frame-pc是垃圾值。这三种到了寄存器分析的尽头得上更重的工具MPU 栈保护提前捕获、ETM 指令追踪、watchpoint 打断点。五、总结HardFault 不可怕可怕的是不知道寄存器里有现成的「化验单」。三步——LR 定栈、CFSR 分类、addr2line 符号化——加上一个会说话的 handler能把「客户那边死机了」从玄学变成可定位的工程问题。下次设备再吐 HARD FAULT先别猜去读那三个寄存器。
返回列表