问题背景
公司三年前部署了一批环境采集终端,主控是 STM32F103,固件把设备配置和累计运行小时数放在内部 Flash 的最后一个 1KB 页里。配置包括采样间隔、报警阈值和传感器校准系数,运行小时数用于质保和维保提醒。设备分布在各厂区,共 500 多台,早期运行一直稳定。
从今年年中开始,售后反馈逐渐增多:部分设备断电重启后,采样间隔变回默认的 60 秒,校准系数丢失,测量值出现明显偏差;有几台设备重启后直接进入默认配置,之前设置的报警阈值全部失效。故障设备的生产批次集中在最早部署的一批,数量每月递增。
故障现象
- 设备断电重启后配置恢复出厂值,重新设置后能正常工作,但下次断电又可能丢失。
- 丢失的不只是配置,累计运行小时数也会归零或跳变。
- 部分设备在保存设置时死机,看门狗复位后回到默认配置。
- 用 ST-Link 读回故障设备的 Flash 配置页,CRC 校验不通过;本该是
0xFF的空白区域出现零星0x00位。 - 故障率与运行时间正相关:早期批次故障率明显更高,新批次暂时正常。
- 重新刷写固件后设备能恢复一段时间,但几周后问题再次出现,说明不是偶发写坏,而是存储介质在持续退化。
排查过程
第一步确认故障范围。把返修的 12 台设备逐一读回配置页,发现其中 9 台的 CRC 校验失败,配置结构体里出现非法值;另外 3 台能通过校验,但运行小时数与实际使用时间明显不符。问题集中在存储环节,不是传感器或电源。
第二步看 Flash 内容。用 STM32CubeProgrammer 读出配置页,正常情况下擦除后的位应该是 0xFF,但故障设备出现了若干 0x00 位——这些位已经擦不掉,说明存储单元的擦写寿命接近或已经耗尽。部分设备在擦除时置位 FLASH_SR 的 PGERR,固件没有检查这个标志,仍然继续写入,最后写进去的是一份坏数据。
第三步查固件写入逻辑。翻出三年前的代码,save_runtime() 每小时执行一次:先擦除整个 1KB 配置页,再把配置结构体和运行小时数一起写回去。配置和计数共用同一页,没有 A/B 备份,没有磨损均衡,也没有写入失败处理。也就是说,设备每运行一小时,这一页就被擦一次。
第四步算账。STM32F103 的内部 Flash 标称每个页可擦写 1 万次;按每小时一次计算,一年是 8760 次,大约 14 个月就会达到标称寿命。设备已经运行三年,累计擦写约 2.6 万次,是额定值的两倍多。早期批次先坏,完全符合这个模型。
第五步复现。拿一块备用板,把保存周期改成每分钟一次做加速测试,记录每次擦除后的校验结果。大约一万次之后,开始出现擦除不干净、CRC 偶发失败;继续写到一万两千次,配置页彻底无法保存。现象与现场设备一致,根因闭环。
解决方案
- 降低写入频率:运行小时数改为保存在 RAM 中,每小时累加但不落 Flash;每天零点或检测到市电掉电时再持久化一次。配置只在用户修改时写入,不再随计数一起擦写。这样年擦写量从 8760 次降到 365 次以内。
- A/B 双页备份:把配置页拆成 A、B 两个槽位,每个槽位带序号和 CRC。写入时擦除非当前槽位、写入新数据、校验通过后再切换有效标志;任何一次掉电最多损失一份新数据,旧配置仍然可用。两页交替使用,磨损也减半。
- 磨损均衡:在 Flash 尾部划出 4 个页做环形轮转,每次写入换下一页,写满一圈再回到第一页。单页寿命 1 万次,四页合计可承受 4 万次擦写,配合降低后的写入频率,设计寿命覆盖十年以上。
- 写入失败检测:每次擦除和编程后检查
FLASH_SR的PGERR、WRPRTERR标志;发现错误就把当前页标记为坏页,自动切换到备用页并上报告警,避免固件在坏页上反复写、静默丢配置。 - 高频数据换介质:如果确实需要频繁保存计数或日志,改用外部 FRAM(擦写次数可达 10¹² 量级)或带磨损均衡的 EEPROM,不要拿内部 Flash 当日志盘用。
- 存量设备处置:通过 OTA 推送新固件,升级前先读回并备份配置;对已经出现坏页的设备,固件自动重映射到备用页,无法重映射的安排换板。升级后连续观察一个月的 CRC 错误和配置丢失告警。
根因分析
直接根因是 固件每小时擦写同一 Flash 页保存运行计数,累计擦写次数超过 1 万次的额定寿命,存储单元出现无法擦除的坏位,配置 CRC 校验失败后回退默认值。
更深一层,是把 Flash 当成了“免费的持久化内存”。设计时只考虑了“掉电不能丢数据”,没有计算“写多少次会坏”;配置这种低频数据和运行计数这种高频数据混在同一页,导致配置被动地跟着计数一起被擦写。再加上没有 A/B 备份、没有写入失败检测,坏页出现后固件既不报警也不切换,而是静默回退默认值——设备看起来“恢复了”,实际上是把故障藏了起来。
还有一层是验证方式的缺失。三年前的功能测试只验证了“保存后能读回”,没有做加速擦写测试,也没有算过写入预算。功能正确掩盖了寿命问题,而寿命问题只会在几年后以“偶发丢配置”的形式出现。
预防措施
- 存储设计评审必须算写入预算:每天写入次数 × 365 × 设计年限,与介质额定擦写次数对比,留出至少三倍余量。
- 按写入频率给数据分类:配置、校准系数属于低频数据;运行计数、日志属于高频数据。高频数据放 RAM、RTC 备份寄存器或 FRAM,不落内部 Flash。
- A/B 双槽 + CRC + 磨损均衡列为嵌入式存储的标准模板,新项目直接复用,不再每次手写。
- 出厂老化测试加入加速擦写项:按十年写入量压缩到几天跑完,提前暴露坏页和掉电竞态。
- 设备遥测上报擦写计数、CRC 错误数和坏页标记,把“存储寿命”变成可观测指标,而不是等售后反馈。
总结
Flash 是消耗品,每一次擦除都是一次支取。嵌入式设备的故障不只有死机、跑飞和通信中断,还有这种以年为单位的缓慢磨损:功能一直正确,直到某一天存储单元再也写不进去。把写入预算写进设计评审,把配置和计数分开,把 A/B 备份和坏页检测做成默认能力,付出的只是一点代码和几个 Flash 页,换来的却是设备在十年后仍然记得自己是谁。