<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Flash 磨损 on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/flash-%E7%A3%A8%E6%8D%9F/</link>
        <description>Recent content in Flash 磨损 on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Sun, 25 Oct 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/flash-%E7%A3%A8%E6%8D%9F/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Flash 写穿：三年后设备配置频繁丢失</title>
            <link>https://blog.5772447.xyz/posts/8c9e4882/</link>
            <pubDate>Sun, 25 Oct 2026 09:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/8c9e4882/</guid>
            <description>&lt;h2 id=&#34;问题背景&#34;&gt;&lt;a href=&#34;#%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;问题背景&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;公司三年前部署了一批环境采集终端，主控是 STM32F103，固件把设备配置和累计运行小时数放在内部 Flash 的最后一个 1KB 页里。配置包括采样间隔、报警阈值和传感器校准系数，运行小时数用于质保和维保提醒。设备分布在各厂区，共 500 多台，早期运行一直稳定。&lt;/p&gt;&#xA;&lt;p&gt;从今年年中开始，售后反馈逐渐增多：部分设备断电重启后，采样间隔变回默认的 60 秒，校准系数丢失，测量值出现明显偏差；有几台设备重启后直接进入默认配置，之前设置的报警阈值全部失效。故障设备的生产批次集中在最早部署的一批，数量每月递增。&lt;/p&gt;&#xA;&lt;h2 id=&#34;故障现象&#34;&gt;&lt;a href=&#34;#%e6%95%85%e9%9a%9c%e7%8e%b0%e8%b1%a1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;故障现象&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;设备断电重启后配置恢复出厂值，重新设置后能正常工作，但下次断电又可能丢失。&lt;/li&gt;&#xA;&lt;li&gt;丢失的不只是配置，累计运行小时数也会归零或跳变。&lt;/li&gt;&#xA;&lt;li&gt;部分设备在保存设置时死机，看门狗复位后回到默认配置。&lt;/li&gt;&#xA;&lt;li&gt;用 ST-Link 读回故障设备的 Flash 配置页，CRC 校验不通过；本该是 &lt;code&gt;0xFF&lt;/code&gt; 的空白区域出现零星 &lt;code&gt;0x00&lt;/code&gt; 位。&lt;/li&gt;&#xA;&lt;li&gt;故障率与运行时间正相关：早期批次故障率明显更高，新批次暂时正常。&lt;/li&gt;&#xA;&lt;li&gt;重新刷写固件后设备能恢复一段时间，但几周后问题再次出现，说明不是偶发写坏，而是存储介质在持续退化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;排查过程&#34;&gt;&lt;a href=&#34;#%e6%8e%92%e6%9f%a5%e8%bf%87%e7%a8%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;排查过程&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;第一步确认故障范围。把返修的 12 台设备逐一读回配置页，发现其中 9 台的 CRC 校验失败，配置结构体里出现非法值；另外 3 台能通过校验，但运行小时数与实际使用时间明显不符。问题集中在存储环节，不是传感器或电源。&lt;/p&gt;&#xA;&lt;p&gt;第二步看 Flash 内容。用 STM32CubeProgrammer 读出配置页，正常情况下擦除后的位应该是 &lt;code&gt;0xFF&lt;/code&gt;，但故障设备出现了若干 &lt;code&gt;0x00&lt;/code&gt; 位——这些位已经擦不掉，说明存储单元的擦写寿命接近或已经耗尽。部分设备在擦除时置位 &lt;code&gt;FLASH_SR&lt;/code&gt; 的 &lt;code&gt;PGERR&lt;/code&gt;，固件没有检查这个标志，仍然继续写入，最后写进去的是一份坏数据。&lt;/p&gt;&#xA;&lt;p&gt;第三步查固件写入逻辑。翻出三年前的代码，&lt;code&gt;save_runtime()&lt;/code&gt; 每小时执行一次：先擦除整个 1KB 配置页，再把配置结构体和运行小时数一起写回去。配置和计数共用同一页，没有 A/B 备份，没有磨损均衡，也没有写入失败处理。也就是说，设备每运行一小时，这一页就被擦一次。&lt;/p&gt;&#xA;&lt;p&gt;第四步算账。STM32F103 的内部 Flash 标称每个页可擦写 1 万次；按每小时一次计算，一年是 8760 次，大约 14 个月就会达到标称寿命。设备已经运行三年，累计擦写约 2.6 万次，是额定值的两倍多。早期批次先坏，完全符合这个模型。&lt;/p&gt;&#xA;&lt;p&gt;第五步复现。拿一块备用板，把保存周期改成每分钟一次做加速测试，记录每次擦除后的校验结果。大约一万次之后，开始出现擦除不干净、CRC 偶发失败；继续写到一万两千次，配置页彻底无法保存。现象与现场设备一致，根因闭环。&lt;/p&gt;&#xA;&lt;h2 id=&#34;解决方案&#34;&gt;&lt;a href=&#34;#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;解决方案&#xD;&#xA;&lt;/h2&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;降低写入频率&lt;/strong&gt;：运行小时数改为保存在 RAM 中，每小时累加但不落 Flash；每天零点或检测到市电掉电时再持久化一次。配置只在用户修改时写入，不再随计数一起擦写。这样年擦写量从 8760 次降到 365 次以内。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A/B 双页备份&lt;/strong&gt;：把配置页拆成 A、B 两个槽位，每个槽位带序号和 CRC。写入时擦除非当前槽位、写入新数据、校验通过后再切换有效标志；任何一次掉电最多损失一份新数据，旧配置仍然可用。两页交替使用，磨损也减半。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;磨损均衡&lt;/strong&gt;：在 Flash 尾部划出 4 个页做环形轮转，每次写入换下一页，写满一圈再回到第一页。单页寿命 1 万次，四页合计可承受 4 万次擦写，配合降低后的写入频率，设计寿命覆盖十年以上。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;写入失败检测&lt;/strong&gt;：每次擦除和编程后检查 &lt;code&gt;FLASH_SR&lt;/code&gt; 的 &lt;code&gt;PGERR&lt;/code&gt;、&lt;code&gt;WRPRTERR&lt;/code&gt; 标志；发现错误就把当前页标记为坏页，自动切换到备用页并上报告警，避免固件在坏页上反复写、静默丢配置。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;高频数据换介质&lt;/strong&gt;：如果确实需要频繁保存计数或日志，改用外部 FRAM（擦写次数可达 10¹² 量级）或带磨损均衡的 EEPROM，不要拿内部 Flash 当日志盘用。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;存量设备处置&lt;/strong&gt;：通过 OTA 推送新固件，升级前先读回并备份配置；对已经出现坏页的设备，固件自动重映射到备用页，无法重映射的安排换板。升级后连续观察一个月的 CRC 错误和配置丢失告警。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;根因分析&#34;&gt;&lt;a href=&#34;#%e6%a0%b9%e5%9b%a0%e5%88%86%e6%9e%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;根因分析&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;直接根因是 &lt;strong&gt;固件每小时擦写同一 Flash 页保存运行计数，累计擦写次数超过 1 万次的额定寿命，存储单元出现无法擦除的坏位，配置 CRC 校验失败后回退默认值&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;更深一层，是把 Flash 当成了“免费的持久化内存”。设计时只考虑了“掉电不能丢数据”，没有计算“写多少次会坏”；配置这种低频数据和运行计数这种高频数据混在同一页，导致配置被动地跟着计数一起被擦写。再加上没有 A/B 备份、没有写入失败检测，坏页出现后固件既不报警也不切换，而是静默回退默认值——设备看起来“恢复了”，实际上是把故障藏了起来。&lt;/p&gt;&#xA;&lt;p&gt;还有一层是验证方式的缺失。三年前的功能测试只验证了“保存后能读回”，没有做加速擦写测试，也没有算过写入预算。功能正确掩盖了寿命问题，而寿命问题只会在几年后以“偶发丢配置”的形式出现。&lt;/p&gt;&#xA;&lt;h2 id=&#34;预防措施&#34;&gt;&lt;a href=&#34;#%e9%a2%84%e9%98%b2%e6%8e%aa%e6%96%bd&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;预防措施&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;存储设计评审必须算写入预算：每天写入次数 × 365 × 设计年限，与介质额定擦写次数对比，留出至少三倍余量。&lt;/li&gt;&#xA;&lt;li&gt;按写入频率给数据分类：配置、校准系数属于低频数据；运行计数、日志属于高频数据。高频数据放 RAM、RTC 备份寄存器或 FRAM，不落内部 Flash。&lt;/li&gt;&#xA;&lt;li&gt;A/B 双槽 + CRC + 磨损均衡列为嵌入式存储的标准模板，新项目直接复用，不再每次手写。&lt;/li&gt;&#xA;&lt;li&gt;出厂老化测试加入加速擦写项：按十年写入量压缩到几天跑完，提前暴露坏页和掉电竞态。&lt;/li&gt;&#xA;&lt;li&gt;设备遥测上报擦写计数、CRC 错误数和坏页标记，把“存储寿命”变成可观测指标，而不是等售后反馈。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;总结&#34;&gt;&lt;a href=&#34;#%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;Flash 是消耗品，每一次擦除都是一次支取。嵌入式设备的故障不只有死机、跑飞和通信中断，还有这种以年为单位的缓慢磨损：功能一直正确，直到某一天存储单元再也写不进去。把写入预算写进设计评审，把配置和计数分开，把 A/B 备份和坏页检测做成默认能力，付出的只是一点代码和几个 Flash 页，换来的却是设备在十年后仍然记得自己是谁。&lt;/p&gt;&#xA;&lt;h2 id=&#34;延伸阅读&#34;&gt;&lt;a href=&#34;#%e5%bb%b6%e4%bc%b8%e9%98%85%e8%af%bb&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;延伸阅读&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/7fec59ab/&#34; &gt;OTA 固件升级断电变砖：A/B 双分区缺位&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/3e0783d8/&#34; &gt;任务栈溢出致随机死机：HardFault 现场取证&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
        </item></channel>
</rss>
