机器人关节控制器死机怎么查:STM32F103 四个故障寄存器七步定位

关节控制器跑几小时就复位,串口却只有一行启动信息——问题往往不在电源,而在 HardFault 没留下证据。本文基于 《Cortex-M3 技术参考手册》,讲清 CFSR、HFSR、MMFAR、BFAR 四个寄存器的位定义,FORCED 升级链怎么读,入栈帧里的 PC 怎么取,附 STM32F103 可落地代码与七步定位清单。

2026-10-21
STM32F103HardFaultCFSR故障定位机器人关节嵌入式

机器人关节控制器在整机联调时"跑几个小时就停转,看门狗复位后串口只打一行启动信息",这是嵌入式现场最难查的一类问题。结论先给: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 的位会直接告诉你。问题在于这些寄存器复位后会被清零,必须"死前保存"。

二、四个寄存器一张表看完

寄存器地址记录什么关键位能回答的问题
CFSR0xE000ED28局部故障汇总,32 位分三段MMARVALID[7]、PRECISERR[1]、UNDEFINSTR[0]到底是哪一类故障
HFSR0xE000ED2C硬故障状态FORCED[30]、VECTTBL[1]是不是"升级"上来的
MMFAR0xE000ED34存储器管理故障地址MMARVALID[7]越界访问了哪个地址
BFAR0xE000ED38总线故障地址BFARVALID[7]访问了哪个非法总线地址

先说明一个前提:MemManage、BusFault、UsageFault 这三个"可配置故障"默认是禁用的,开关在系统处理器控制与状态寄存器 SHCSR(0xE000ED24)的 bit16/17/18,即 MEMFAULTENA、BUSFAULTENA、USGFAULTENA。禁用时,任何局部故障都会升级成 HardFault,HFSR 的 FORCED 位置 1——这也是现场最常见的情形。

CFSR 是一个 32 位寄存器,按字节分成三段:低 8 位是存储器管理故障状态,中间 8 位是总线故障状态,高 8 位是使用故障状态。读的时候整体读一次再按段拆位,不要分别去读三个字节地址——这是读-清除寄存器,分开读容易把标志清掉。

三、先看 HFSR:FORCED 是"请去 CFSR"的路标

位名称置位含义下一步
[31]DEBUGEVT与调试有关的故障检查调试器连接与 BKPT 指令
[30]FORCED局部故障因优先级或未使能而升级为硬故障必读 CFSR,真正原因在局部故障状态里
[1]VECTTBL异常处理过程中读向量表出错查 VTOR 与向量表 128 字节对齐

实际排查顺序应该是:读 HFSR → 若 FORCED=1,再读 CFSR → 根据置位段决定去看 MMAR 还是 BFAR。如果只盯着 HFSR,永远只能得到"发生了硬故障"这一句废话。

VECTTBL 单独说一下:它表示在异常处理过程中读向量表失败,通常是向量表地址没设对(VTOR 指向未初始化区域)或者向量表没有按 128 字节对齐。这类问题往往一上电就出现,和"跑几小时"的时间特征对不上,可以据此先排除。

四、CFSR 三段:把"死法"分成三类

段位名称典型触发场景
MMFSR[0]IACCVIOL在禁止取指的区域取指(XN 区或 MPU 未授权区)
MMFSR[1]DACCVIOL向只读区或未授权区写数据
MMFSR[3]MUNSTKERR异常返回出栈时访问违例
MMFSR[4]MSTKERR异常进入压栈时访问违例,常见于栈溢出
MMFSR[7]MMARVALIDMMAR 中的故障地址有效
BFSR[0]IBUSERR取指总线错误
BFSR[1]PRECISERR精确数据总线错误,PC 指向出错指令
BFSR[2]IMPRECISERR不精确总线错误,PC 不可信
BFSR[3]UNSTKERR异常返回出栈时总线错误
BFSR[4]STKERR异常进入压栈时总线错误
BFSR[7]BFARVALIDBFAR 中的故障地址有效
UFSR 位名称含义常见成因
[0]UNDEFINSTR未定义指令函数指针跑飞、跳进数据区
[1]INVSTATEEPSR 与指令组合非法跳转目标 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)。调试阶段建议打开,量产固件可按需关闭——打开后能提前暴露问题,但也会让原本"能跑"的代码开始报故障。

五、把现场取出来:入栈帧里才有 PC

异常进入时,硬件会自动把 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读 HFSRFORCED=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 里打印、然后立刻复位,等于什么都没留下。

IMPRECISERR 的 PC 不能信

不精确总线错误由写缓冲延迟报告,返回 PC 和真正出错的指令没有关系,BFAR 也可能无效。遇到 IMPRECISERR,先执行 DSB 再读状态,或者把编译优化降一级、把可疑的指针访问改成 volatile 逐步缩小范围。别拿着一个错误的 PC 去骂编译器。

MSTKERR/STKERR 先查栈

压栈或出栈阶段出错,说明 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小时内响应