问题背景
公司在全国客户现场部署了约 200 台环境数据采集终端:STM32F407 + FreeRTOS,通过 4G 或以太网把传感器数据上报到平台。设备装在客户机房和户外机柜里,没有调试器、没有维护通道,只能靠远程升级和日志排障。
从两个月前开始,各地陆续反馈同一类问题:设备会"随机重启",重启后自己恢复正常,业务表现是上报数据出现一段几分钟到几十分钟的缺口。现场人员的第一反应通常是"断电了"或"信号不好",换电源、重新上电、加强散热,折腾一圈,问题照旧。
故障现象
- 设备运行 2~6 周后随机死机,随后看门狗复位、自动恢复,时间点毫无规律。
- 掉电保持的复位原因记录显示:绝大多数是独立看门狗(IWDG)复位。
- 死机前串口和日志没有任何错误输出——死机瞬间根本来不及打印。
- 故障率与数据上报频率呈正相关:上报越频繁的站点,重启越密。
- 更换电源模块、检查晶振、加装散热之后,复现率没有变化。
排查过程
第一步,先查看门狗。 把 IWDG 超时从 8 秒放宽到 30 秒,逐任务核对喂狗逻辑——复发时间没变。看门狗只是"收尸"的,不是"凶手",排除。
第二步,查硬件。 换电源、测晶振、加散热,故障率纹丝不动;而"上报越频繁越容易死"这个相关性越来越明显——信号指向软件时序问题:负载越重,某个边界越容易被踩到。
第三步,给死机装"黑匣子"。 设备死机时没有任何输出,等于每次案发现场都被看门狗复位冲掉了。我们在固件里加入 HardFault 处理:把故障状态寄存器(CFSR、HFSR、MMFAR、BFAR)和异常自动压栈的寄存器帧(R0~R3、R12、LR、PC、xPSR)写入不参与启动清零的 noinit 内存段——看门狗复位不断电,RAM 内容可以保留;下次上电由启动代码读出并上报。同时记录 RCC_CSR 的复位标志,确认复位来源。
第四步,等现场"抓到一次"。 两周后一台高频上报的终端触发了取证,读出的数据非常明确:
- 复位标志:IWDG 复位,说明确实是看门狗兜底;
- CFSR =
0x00020000,即 INVSTATE(无效执行状态)——典型的返回地址被破坏后跳到非法地址; - 保存的 PC 落在上报处理函数的中间,而 LR 指向的地址不在任何已知函数入口附近——栈被踩了。
第五步,验证栈溢出假设。 打开 FreeRTOS 的栈溢出检测(configCHECK_FOR_STACK_OVERFLOW = 2),并在任务创建时用固定模式填充栈空间;随后在实验室把上报频率拉到正常值的 5 倍做压力测试。结果:上报任务的 uxTaskGetStackHighWaterMark 长期只剩十几个字节的余量,压力测试下栈溢出钩子直接命中,崩溃时的 PC 与现场抓到的完全一致。
第六步,找到具体是哪个函数。 用 arm-none-eabi-addr2line 对照固件 ELF,把保存的 PC 翻译到源码行:问题出在上一版固件新增的"报文组装"函数——函数内声明了一个约 600 字节的局部数组,叠加原有调用链,在这个任务 1KB 的栈里几乎没有余量;一旦上报队列积压或中断嵌套加深,最后一点空间被压垮,返回地址被覆盖,异常返回跳到非法地址,HardFault 触发,看门狗复位收场。
解决方案
- 立即处理:把上报任务栈从 1KB 增加到 2KB,留足余量。
- 根治问题:报文组装的大局部数组改为静态缓冲区(启动时一次性分配),函数内部不再放大栈帧;同时排查其他任务的局部大变量。
- 保留黑匣子:HardFault 取证代码保留在正式固件里,故障信息纳入平台日志上报;现场设备远程升级新固件。
- 加监控:把上报任务的栈高水位随心跳上报,低于阈值即告警,让"栈快满了"在死机之前就被看见。
验证:现场 20 台设备升级后连续运行 8 周零死机;实验室 10 倍上报频率压力测试 72 小时无异常。
根因分析
直接根因是任务栈空间不足叠加新增函数的大局部数组:平时栈余量就只剩十几字节,遇到上报队列积压或中断嵌套加深,栈溢出覆盖返回地址,异常返回跳向非法地址触发 HardFault,看门狗兜底复位。
深层根因有三层:开发阶段没有做栈用量评估,栈大小是随手填的;新增功能只测"功能对不对",没测"资源够不够";设备部署在现场,没有调试器和日志,缺少故障取证手段——前两个问题注定会发生,第三个问题让定位拖了整整两个月。
预防措施
- 任务栈按实测高水位的 1.5~2 倍预留,关键任务启用栈溢出检测与填充模式校验;
- 禁止在任务函数里声明大局部数组,统一用静态缓冲区或启动时一次性分配的堆内存;
- 新固件上线前必须跑满负载压力测试(高频率上报、弱网重传、队列积压),把资源边界打出来;
- 所有现场设备固件内置 HardFault 取证(寄存器加栈帧保存、复位原因上报),把"黑匣子"做成标准能力;
- 排查纪律:先拿到证据再动手,避免"换电源、放宽看门狗"这类盲目试错。
总结
嵌入式设备的随机死机,难的不是修,而是"看见"——设备在客户现场,没有调试器、没有串口,死机瞬间什么都不会留下。这次能破案,靠的是把 Cortex-M 的异常机制用起来:让 HardFault 在死之前把寄存器现场写进不掉电的内存,等下一条命开始时"开口说话"。栈溢出是嵌入式世界最古老的死因之一,但只有当证据链补齐,它才会从玄学变成一行可以改的代码。