> 芯片:TI TM4C123GH6PZ
> 外设:CAT24C64(64Kbit I2C EEPROM)
> 编译器:Keil MDK AC5
> 排查工具:逻辑分析仪 + ZCANPRO 抓包验证
---
## 一、现象
某天上电,设备所有参数全部丢失:
- CAN 节点 ID 从 12 变成 0
- 报警阈值从 2.0 变成 0.0
- 波特率从 500k 变成 0
- Heartbeat 从 100ms 变成 0ms(CANopen 直接废了)
读 EEPROM 全部返回 `0xFF`。看起来像是 EEPROM 被清空了,或者压根没写进去。
但昨天还正常工作。
---
## 二、第一步:怀疑硬件
先查最基础的:
```
□ 万用表量 WP 写保护引脚 → 0V,写保护没生效 ✓
□ 量 I2C 上拉电阻 → 4.7kΩ,正常 ✓
□ 示波器看 SCL/SDA 波形 → 400kHz 方波,ACK 正常 ✓
□ 单独写 1 个字节再读出来 → 正确 ✓
□ 单独写 8 个字节再读出来 → 正确 ✓
```
硬件没问题。小数据量读写全部正常。
---
## 三、第二步:定位到"全量参数写入"
继续排查——设备有一个功能是在 EEPROM 参数损坏时自动恢复出厂配置。恢复函数里会**把整个 `ModBusEDS` 结构体(约 500 字节)一口气写进 EEPROM 地址 `0x0000`**。
当时的代码(问题版本):
c
void Main_EEPROMDefaultsInit(void)
{
// 填充默认参数到 ModBusEDS...
// ...
// 一次性写入整个结构体
ExtEEPROMWrite(0x0000, sizeof(StructModBusEDS), (unsigned char *)&ModBusEDS);
}
```
`sizeof(StructModBusEDS)` ≈ 500 字节,远大于 CAT24C64 的 **32 字节页大小**。
---
## 四、第三步:翻 CAT24C64 数据手册
到这个时候我还在怀疑"是不是没等写完就去读了"。然后翻到数据手册里页写入时序这里:
```
Page Write:
START + DeviceAddr(W) + ACK
+ MemoryAddr_H + ACK
+ MemoryAddr_L + ACK
+ Data[0] + ACK
+ Data[1] + ACK
+ ...
+ Data[30] + ACK
+ Data[31] + ACK
+ STOP ← STOP 之后才启动内部写入周期
如果 Data[n] 超出了当前页的末尾(32 字节边界),
CAT24C64 不会自动跳到下一页——
它会把地址回卷到当前页的开头,覆盖你已经写进去的数据!
```
用图表示就是:
```
页0: [0x00──0x1F] ← 32 字节
页1: [0x20──0x3F] ← 32 字节
页2: [0x40──0x5F] ← 32 字节
...
你以为写 500 字节:
0x00 → 0x01 → 0x02 → ... → 0x1F → 0x20 → 0x21 → ... ✓ 自动跨页
实际 CAT24C64 的硬件行为:
0x00 → 0x01 → ... → 0x1F → 0x00 → 0x01 → ... ← 回卷到页头!
```
**第 33 字节覆盖掉了第 1 字节,第 34 字节覆盖掉了第 2 字节……最终整页是最后一次"绕回"的数据碎片,其余全部错乱。**
这就是为什么 EEPROM 全丢——不是被清空了,而是**被自己的写入流程摧毁了**。
---
## 四、为什么之前没暴露
之前写 EEPROM 的通常用量:
- 写 6 字节序列号 → 不跨页 ✓
- 写单个配置参数 → 不跨页 ✓
- 写 500 字节出厂配置 → **跨了 16 个页边界** ✗
出厂配置这个功能很少调用(只在 EEPROM 首次空白或损坏时才触发),所以之前没暴露。
---
## 五、修复方案
核心思路:**每一笔写入不超过当前页的剩余字节数。写完后等 10ms(内部写入周期),再发下一笔。**
```c
void Main_EEPROMDefaultsInit(void)
{
unsigned short total = (unsigned short)sizeof(StructModBusEDS);
unsigned short offset = 0;
unsigned short chunk;
while(offset < total)
{
// ═══ 关键:计算本页还能写几个字节 ═══
// offset & 0x1F = 当前在 32 字节页内的偏移
// 32 - (offset & 0x1F) = 本页剩余可写字节数
chunk = 32 - (offset & 0x1F);
if(chunk > (total - offset))
chunk = total - offset; // 最后一块不超过总长度
ExtEEPROMWrite(offset, chunk, (unsigned char *)&ModBusEDS + offset);
// ═══ 关键:等待内部写入周期完成(~5ms),留 10ms 余量 ═══
SysCtlDelay(SysCtlClockGet() / 100); // ≈10ms @40MHz
offset += chunk;
}
}
```
### 分页逻辑拆解
假设总长 500 字节,写入过程:
| 轮次 | offset | offset & 0x1F | chunk (本页剩余) | 实际操作 |
|:---:|:---:|:---:|:---:|------|
| 1 | 0x0000 | 0 | 32 | 写 0x00~0x1F |
| 2 | 0x0020 | 0 | 32 | 写 0x20~0x3F |
| 3 | 0x0040 | 0 | 32 | 写 0x40~0x5F |
| ... | | | | |
| 16 | 0x01E0 | 0 | 32 | 写 0x1E0~0x1FF |
| 17 | 0x0200 | 0 | `total - offset` | 写最后剩余 |
**`offset & 0x1F`** 这行是整段代码的灵魂。
---
## 六、写保护也不能忘
CAT24C64 的 WP 引脚在设备量产时建议接 IO 控制:
```c
// 写入前拉低 WP(释放写保护)
GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_2, GPIO_PIN_2);
ExtEEPROMWrite(offset, chunk, ...);
// 写完后拉高 WP(加锁,防止误写入)
GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_2, 0);
```
---
## 七、教训总结
| 误区 | 实际情况 |
|------|------|
| "EEPROM 按地址寻址,写哪都行" | **页写入模式地址会回卷**,超过 32 字节不跨页 |
| "写大块数据一次写到底" | 必须按页拆分,**每次 ≤ (32 - 当前页内偏移) 字节** |
| "写完就能读" | 每写完一页,芯片进入内部写入周期(~5ms),此期间不响应 I2C |
| "CAT24C64 和 24C02 一样" | CAT24C64 页大小 32 字节,24C02 是 8 字节,**跨页行为一致但边界不同** |
这次排查断断续续花了两天。前 80% 的时间浪费在"怀疑硬件坏了、怀疑 I2C 波形不对、怀疑写保护没拉对"上。最后翻数据手册看到"page roll-over"那一行字的时候——妈的,就这样。
希望下一个人搜到这个帖子的时候,能少花一天。