记录一下今天遇到的一个 bug,由于软硬件错配导致遇到 eeprom 写回绕的,最终解决方案比较简单,但搜集线索和排查问题耗费了较多时间。
现象
设备从远端持续下载数据过程中始终失败,预期应该持续接收直到传输完成。
线索
通过与触发问题的同事交流以及自己调试排查,收集到几条线索:
- 问题只在带真实 EEPROM/Flash 的硬件上复现,在 mock 掉 EEPROM 和 Flash 的纯软件仿真环境里,下载流程完全正常。
- 出问题的数据都集中在 EEPROM 地址的起始区域,几份数据的存储地址很近。
- 问题点并不集中于传输,设备启动阶段也观察到异常,共同点是两者都依赖 EEPROM 里的同一片区域。
- 仅一款设备存在问题,其它型号正常。
- 连续操作下,第一次下载前数据正常;第一次下载失败后读出"异常 A";第二次下载失败后读出"异常 B"。异常本身有确定的模式,不是随机扰动。
推测
基于上述几条线索,基本可以确定
- 代码逻辑无问题:因为 mock 的 eeprom 和 flash 并不存在问题。而且若程序有问题,如何解释问题不集中在一处呢?
- 问题与 eeprom 有关,但与 eeprom 硬件故障无关:异常点处数据集中,异常模式稳定,若 eeprom 硬件故障,则不会保持稳定的异常模式
验证
查看工程配置,发现 EEPROM 的 PAGE_SIZE 被配置为了 512 bytes。
翻芯片 datasheet:
Page Write The Page Write mode allows up to 256 bytes to be written in a single Write cycle, provided that they are all located in the same page in the memory: that is, the most significant memory address bits, b16–b8, are the same. If more bytes are sent than will fit up to the end of the page, a “roll-over” occurs, i.e. the bytes exceeding the page end are written on the same page, from location 0.
实际硬件的 page size 是 256 bytes。
至此问题定位完毕。
分析
EEPROM 的写操作不是按字节随机写的,而是按 page buffer:主控通过 I²C 发起始地址 + 一串数据,芯片把数据写进自己的 page buffer,然后整页烧到存储 cell。page buffer 的物理大小由芯片决定,地址低 N 位是 buffer 内偏移,高位是 page 号。
软件以为 page size 是 512 bytes,于是允许一次写入跨过物理 256 bytes 边界。但芯片硬件只认它自己的 256 bytes 边界:超出当前 page 末尾的字节,地址计数器在 page 内回绕(roll-over),从该 page 的偏移 0 开始覆盖写。
用一张抽象的示意图说明(由 claude 生成):
| |
这就解释了所有现象:
- 为什么数据会变?跨边界写入时,后半段数据回绕,把同一个 page 起始位置(也就是前半段写入的内容)覆盖了。
- 为什么集中在低地址?低地址区域的数据块更可能跨过 256 边界(因为多个数据块紧邻排布,第一个块的写入就可能溢出到第二个块所在 page 的起点)。
- 为什么问题具有确定模式?覆盖逻辑由 page 内偏移决定,是纯硬件行为,给定相同输入必然产生相同输出——所以才有"第一次失败 → 异常 A,第二次失败 → 异常 B"。
- 为什么 mock 程序无法复现?mock 并没有模拟真实硬件的 wrap-around,因此行为和硬件不一致,反而掩盖了 bug。
- 为什么仅一款设备出现问题?这一款设备用的 EEPROM 芯片型号与配置不匹配,其他型号恰好 page size 真是 512 bytes(或者从未触发跨页写)。
修复
直接修复很简单:把在 bsp 中将该设备的 EEPROM_PAGE_SIZE 改回 256 bytes。
同时新增两个防御性措施:
- Mock 行为对齐硬件:mock 的实现也按真实的 page size 模拟 wrap-around 行为,否则会导致 mock 反而掩盖了问题。
- 在 mock 程序上新增几组 unit test:跨页写入、起始地址恰好在 page 末尾、单次写满整页等。
结论
Mock 必须以复现硬件的关键约束为目标,包括 page size、写入耗时、边界行为等。