关节控制器跑几小时就复位,串口却只有一行启动信息——问题往往不在电源,而在 HardFault 没留下证据。本文基于 《Cortex-M3 技术参考手册》,讲清 CFSR、HFSR、MMFAR、BFAR 四个寄存器的位定义,FORCED 升级链怎么读,入栈帧里的 PC 怎么取,附 STM32F103 可落地代码与七步定位清单。
机器人关节控制器在整机联调时"跑几个小时就停转,看门狗复位后串口只打一行启动信息",这是嵌入式现场最难查的一类问题。结论先给:STM32F103 这类 Cortex-M3 内核把死机原因分别记在四个寄存器里——CFSR(0xE000ED28)、HFSR(0xE000ED2C)、MMFAR(0xE000ED34)、BFAR(0xE000ED38)。只要在 HardFault 处理函数里把这四个值和入栈的 PC 一起保存,多数死机能定位到具体函数与地址,而不是靠换电源、加 TVS 碰运气。
以一个典型的关节控制器为例:20kHz 电流环、14 位磁编码器、CAN 总线 1Mbps,外加一路 4G 模组做远程诊断。单板跑功能测试一切正常,装到整机做 8 小时老化,三台里有一台在 2~5 小时之间停转。看门狗把 MCU 拉回复位,串口只打印版本号,没有任何异常信息。
这时候最容易走的弯路是"先换电源、再加 TVS、最后怀疑看门狗"。电源纹波和 EMC 当然要查,但它们只是嫌疑人,不是证据。Cortex-M3 在进入异常时已经把证据写进内核寄存器:是取指出错、数据访问越界、总线错误,还是执行了非法指令,CFSR 的位会直接告诉你。问题在于这些寄存器复位后会被清零,必须"死前保存"。
| 寄存器 | 地址 | 记录什么 | 关键位 | 能回答的问题 |
|---|---|---|---|---|
| CFSR | 0xE000ED28 | 局部故障汇总,32 位分三段 | MMARVALID[7]、PRECISERR[1]、UNDEFINSTR[0] | 到底是哪一类故障 |
| HFSR | 0xE000ED2C | 硬故障状态 | FORCED[30]、VECTTBL[1] | 是不是"升级"上来的 |
| MMFAR | 0xE000ED34 | 存储器管理故障地址 | MMARVALID[7] | 越界访问了哪个地址 |
| BFAR | 0xE000ED38 | 总线故障地址 | BFARVALID[7] | 访问了哪个非法总线地址 |
先说明一个前提:MemManage、BusFault、UsageFault 这三个"可配置故障"默认是禁用的,开关在系统处理器控制与状态寄存器 SHCSR(0xE000ED24)的 bit16/17/18,即 MEMFAULTENA、BUSFAULTENA、USGFAULTENA。禁用时,任何局部故障都会升级成 HardFault,HFSR 的 FORCED 位置 1——这也是现场最常见的情形。
CFSR 是一个 32 位寄存器,按字节分成三段:低 8 位是存储器管理故障状态,中间 8 位是总线故障状态,高 8 位是使用故障状态。读的时候整体读一次再按段拆位,不要分别去读三个字节地址——这是读-清除寄存器,分开读容易把标志清掉。
| 位 | 名称 | 置位含义 | 下一步 |
|---|---|---|---|
| [31] | DEBUGEVT | 与调试有关的故障 | 检查调试器连接与 BKPT 指令 |
| [30] | FORCED | 局部故障因优先级或未使能而升级为硬故障 | 必读 CFSR,真正原因在局部故障状态里 |
| [1] | VECTTBL | 异常处理过程中读向量表出错 | 查 VTOR 与向量表 128 字节对齐 |
实际排查顺序应该是:读 HFSR → 若 FORCED=1,再读 CFSR → 根据置位段决定去看 MMAR 还是 BFAR。如果只盯着 HFSR,永远只能得到"发生了硬故障"这一句废话。
VECTTBL 单独说一下:它表示在异常处理过程中读向量表失败,通常是向量表地址没设对(VTOR 指向未初始化区域)或者向量表没有按 128 字节对齐。这类问题往往一上电就出现,和"跑几小时"的时间特征对不上,可以据此先排除。
| 段 | 位 | 名称 | 典型触发场景 |
|---|---|---|---|
| MMFSR | [0] | IACCVIOL | 在禁止取指的区域取指(XN 区或 MPU 未授权区) |
| MMFSR | [1] | DACCVIOL | 向只读区或未授权区写数据 |
| MMFSR | [3] | MUNSTKERR | 异常返回出栈时访问违例 |
| MMFSR | [4] | MSTKERR | 异常进入压栈时访问违例,常见于栈溢出 |
| MMFSR | [7] | MMARVALID | MMAR 中的故障地址有效 |
| BFSR | [0] | IBUSERR | 取指总线错误 |
| BFSR | [1] | PRECISERR | 精确数据总线错误,PC 指向出错指令 |
| BFSR | [2] | IMPRECISERR | 不精确总线错误,PC 不可信 |
| BFSR | [3] | UNSTKERR | 异常返回出栈时总线错误 |
| BFSR | [4] | STKERR | 异常进入压栈时总线错误 |
| BFSR | [7] | BFARVALID | BFAR 中的故障地址有效 |
| UFSR 位 | 名称 | 含义 | 常见成因 |
|---|---|---|---|
| [0] | UNDEFINSTR | 未定义指令 | 函数指针跑飞、跳进数据区 |
| [1] | INVSTATE | EPSR 与指令组合非法 | 跳转目标 Thumb 位为 0 |
| [2] | INVPC | 非法 EXC_RETURN 装入 PC | 异常返回地址被改写 |
| [3] | NOCP | 使用不存在的协处理器 | 在无 FPU 的 M3 上执行浮点指令 |
| [8] | UNALIGNED | 非对齐访问 | 需 CCR.UNALIGN_TRP=1 才触发 |
| [9] | DIVBYZERO | 除数为零 | 需 CCR.DIV_0_TRP=1 才触发 |
把常见组合记住,现场能省很多时间:DACCVIOL + MMARVALID=1,直接去 MMAR 看地址,多半是空指针或数组越界;PRECISERR + BFARVALID=1,去 BFAR 看地址,多半是结构体指针没初始化就写外设寄存器,或者外设时钟没开;UNDEFINSTR,查函数指针表与中断向量表,常见于 OTA 升级后跳转地址错误;INVSTATE,跳转目标不是 Thumb 代码,检查函数指针是否被数据覆盖。
UNALIGNED 和 DIVBYZERO 默认不触发,要在配置控制寄存器 CCR(0xE000ED14)里打开 UNALIGN_TRP(bit3)和 DIV_0_TRP(bit4)。调试阶段建议打开,量产固件可按需关闭——打开后能提前暴露问题,但也会让原本"能跑"的代码开始报故障。
异常进入时,硬件会自动把 R0~R3、R12、LR、PC、xPSR 八个字压到当前栈上,这个"入栈帧"里的 PC 就是出错指令的地址(精确故障时)。用哪个栈取决于 EXC_RETURN 的 bit2:为 0 用 MSP,为 1 用 PSP。所以在 HardFault 里第一件事不是打印,而是判断栈指针、取出帧地址。
typedef struct {
uint32_t r0, r1, r2, r3, r12, lr, pc, xpsr;
} stack_frame_t;
volatile uint32_t g_cfsr, g_hfsr, g_mmfar, g_bfar;
volatile uint32_t g_pc, g_lr;
/* GCC:naked 函数只负责取栈指针,然后跳 C */
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile(
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"b hardfault_c \n");
}
void hardfault_c(stack_frame_t *sf)
{
g_cfsr = *(volatile uint32_t *)0xE000ED28UL;
g_hfsr = *(volatile uint32_t *)0xE000ED2CUL;
g_mmfar = *(volatile uint32_t *)0xE000ED34UL;
g_bfar = *(volatile uint32_t *)0xE000ED38UL;
g_pc = sf->pc; /* 出错指令地址 */
g_lr = sf->lr; /* 返回地址 */
fault_save_to_noinit(); /* 写 noinit RAM,见第七节 */
NVIC_SystemReset();
}
拿到 PC 后,用 map 文件或 arm-none-eabi-addr2line -e firmware.elf 0x0800xxxx 直接反查函数与行号。ARMCC 的 naked 写法是 __asm void HardFault_Handler(void),取栈指针的汇编相同。注意不要在 handler 里放大局部数组或调用 printf——此时栈可能已经坏了,二次故障会把现场彻底丢掉。
| 步骤 | 动作 | 判据 | 产出 |
|---|---|---|---|
| 1 | 打开三个可配置故障 | SHCSR bit16/17/18 置 1 | 局部故障不再一律升级 |
| 2 | 读 HFSR | FORCED=1 转第 3 步;VECTTBL 查向量表 | 确定故障入口 |
| 3 | 读 CFSR | 看哪一段有标志 | 确定故障类别 |
| 4 | 读 MMAR/BFAR | 对应 VALID 位为 1 | 得到故障地址 |
| 5 | 取入栈 PC 与 LR | 结合 map 文件与 addr2line | 定位函数与行号 |
| 6 | 对照位定义组合判断 | 空指针/越界/跑飞/栈溢出 | 形成原因假设 |
| 7 | 复现并加防护 | 断言、栈水位、MPU 分区 | 形成回归用例 |
这七步里第 5 步最容易漏:很多人只打印 CFSR 就结束,结果知道"数据访问违例"却不知道哪一行。PC 才是把寄存器现象落到代码上的桥。
CFSR、HFSR、MMFAR、BFAR 在复位后回到 0。必须把值写进不会被启动代码清掉的 RAM(链接脚本里划一段 .noinit),或者写进备份寄存器;复位后再由主循环读出上报。只在 HardFault 里打印、然后立刻复位,等于什么都没留下。
不精确总线错误由写缓冲延迟报告,返回 PC 和真正出错的指令没有关系,BFAR 也可能无效。遇到 IMPRECISERR,先执行 DSB 再读状态,或者把编译优化降一级、把可疑的指针访问改成 volatile 逐步缩小范围。别拿着一个错误的 PC 去骂编译器。
压栈或出栈阶段出错,说明 SP 已经指到了不可写区域,最常见原因是栈溢出或中断嵌套过深。此时入栈帧本身可能不完整,PC 也不可信。先统计最坏情况下的栈用量(含所有中断嵌套),再决定是加栈还是削函数调用层级。
关节控制器不是消费类产品,现场停一次机可能就是一条产线停工。建议把故障处理做成产品能力:HardFault 里保存 CFSR、HFSR、MMFAR、BFAR、PC、LR、xPSR 七项,加上复位原因、运行时长、当前任务号,写入 noinit RAM;复位后由低优先级任务读出,通过 CAN 或 4G 上报。这样每一次死机都留下一条可检索的记录,几次之后就能看出是固定函数、固定地址,还是随机跑飞。
一句话总结:Cortex-M3 的死机不是玄学——CFSR 说明类型、MMFAR/BFAR 给出地址、入栈 PC 指向指令,三者合起来就是一条完整的证据链。先把证据保存下来,再谈电源和 EMC,排查效率会完全不同。
本文基于《Cortex-M3 技术参考手册》第 5 章(异常)与第 8 章(嵌套向量中断控制器)整理优化。
沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付
电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应