简介:这份文档面向使用I2C接口存储器的嵌入式开发者与电子爱好者,聚焦24C256在数据存储中最易出错的地址管理问题,帮助读者理解其存储原理并规避连续写入时的数据覆盖风险。资源包共1个doc文件,约33KB,以文字说明配合MSP430F449平台的控制代码示例,梳理了16位地址中低6位页内地址与高9位页码的划分方式,并剖析了每次写入11字节时地址递增至63后回卷导致出错的典型现象,给出存储块大小取64、32、16、8、4、2及不足补零的解决思路。文中还附有延时、启动、停止、发送0与1、读写单字符、应答与非应答等IIC底层模块的实现片段,便于对照移植。目前已有1916人学习下载,适合需要排查24C256连续存储异常、完善驱动细节的读者参考。
1. 24C256 的页写陷阱:为什么地址“看起来对”数据却错了
写 EEPROM 最让人抓狂的一类 bug,是地址算得清清楚楚、I2C 波形也干净利落,读回来却发现后半段数据把前半段覆盖了。24C256 这颗 512×64 的 I2C 存储器,容量 32768 字节,地址是 16 位整型数,有效范围 0~32767,其中真正参与寻址的有效位只有 15 位:低 6 位(0~5)是页内偏移,范围 0~63;高 9 位(6~14)是页号,范围 0~511。问题就出在这条分界线上——芯片内部地址计数器在页内会自动累加,跨页时却不会自动进位,而是回卷到本页起始地址重新覆盖。这个机制不是缺陷,是成本与工艺的取舍,但它是无数嵌入式项目翻车的源头。搞懂它,你才能明白为什么“连续写”在 24C256 上必须被当成“分页写”来对待。
2. 地址计数器:15 位有效地址是怎么被拆成页号和页内偏移的
2.1 从 32768 字节反推地址位宽
24C256 的型号里,256 指的是 256 Kbit,换算成字节就是 32768。要寻址 32768 个单元,理论上需要 15 位地址(2^15 = 32768)。但 I2C 写时序里地址是按两个字节发送的,高字节在前、低字节在后,所以对外表现为 16 位整型数。第 16 位(最高位)在芯片内部被丢弃,真正进入地址锁存器的是低 15 位。这就解释了一个常见困惑:你传0x8000和传0x0000,芯片行为完全一样,因为最高位根本没被使用。
常见的实现思路是把这 15 位切成两段:
| 位段 | 位编号 | 含义 | 取值范围 |
|---|---|---|---|
| 低 6 位 | bit0~bit5 | 页内偏移 | 0~63 |
| 高 9 位 | bit6~bit14 | 页号 | 0~511 |
| 最高位 | bit15 | 无效 | 被忽略 |
这个切分不是软件约定,而是芯片内部地址计数器的物理结构决定的。页内偏移是一个 6 位计数器,页号是一个 9 位计数器,两者串联。写操作时,每写入一个字节,6 位页内计数器加一;当它从 63 溢出回 0 时,9 位页号计数器并不加一,而是保持不变。这就是“页内自动累加、跨页不回卷”的硬件根源。
2.2 页写缓冲区的真实大小
24C256 支持页写(Page Write),一次连续写入最多 64 字节。芯片内部有一个 64 字节的页缓冲区,主机连续送来的数据先进入这个缓冲区,收到停止条件后才整体写入 EEPROM 阵列。缓冲区地址指针同样是 6 位页内计数器,溢出即回卷。所以当你从地址 63 开始连续写 2 个字节时,第一个字节落在第 0 页第 63 格,第二个字节的页内计数器溢出回 0,落在第 0 页第 0 格,把原来的数据覆盖掉。地址 63 的二进制是111111,地址 64 是1,000000,前者是第 0 页最后一格,后者是第 1 页第一格,但连续写模式下芯片不会替你完成这个进位。
提示:页缓冲区回卷是硬件行为,不会报错、不会置位任何标志,读回来才发现数据被覆盖。这是它最隐蔽的地方。
2.3 为什么“每次写 11 字节”也会错
原文举了一个很典型的例子:每次写 11 字节,起始地址依次是 0、11、22、33、44、55、66……表面看地址严格递增,没有任何越界。但把每个起始地址拆成页号和页内偏移就露馅了:
- 地址 55:页号 0,页内偏移 55,剩余页内空间 63−55+1 = 9 字节,不够 11 字节。
- 写到第 10 个字节时页内计数器从 63 溢出回 0,第 10、11 个字节落到第 0 页第 0、1 格,覆盖了最早写入的数据。
所以判断一次连续写是否安全,不能只看“起始地址 + 长度 ≤ 32768”,而要看“页内偏移 + 长度 ≤ 64”。这个条件比总容量约束严格得多,也是新手最容易忽略的一条。
3. 连续写模块的源码拆解:地址高字节、低字节与 ACK 轮询
3.1 WriteNbyte 的时序骨架
原文给出的 MSP430F449 平台WriteNbyte函数,把一次页写拆成了清晰的几步:起始条件、设备地址、高地址字节、低地址字节、数据流、停止条件。核心逻辑如下(整理自原文,补全了函数声明与注释):
// 向 24C256 连续写入 number 个字节,addr 为 16 位起始地址 void WriteNbyte(unsigned char *p, unsigned int addr, unsigned char number) { start_IIC(); // 产生起始条件 send_char(0xa2); // 设备地址 + 写方向(0xa2 = 1010 0010) iic_ACK(); // 等待从机应答 send_char(addr / 256); // 地址高字节,等价于 addr >> 8 iic_ACK(); send_char(addr % 256); // 地址低字节,等价于 addr & 0xFF iic_ACK(); do { send_char(*p); // 逐字节送入页缓冲区 p++; iic_ACK(); } while (--number); stop_IIC(); // 停止条件触发内部写周期 delay(10); // 等待内部写周期完成,典型 5ms }0xa2是 7 位设备地址1010000左移一位后加上写方向位 0 得到的;读方向则是0xa3。地址拆分用addr / 256和addr % 256,在编译器优化后就是移位和掩码,但写成除法取模更直观,也避免了手写移位时把高低字节搞反的低级错误。delay(10)对应的是内部写周期,24C256 的典型值是 5ms,取 10ms 是留余量。这里有个细节:停止条件之后芯片才启动内部擦写,这段时间它不应答任何总线访问,所以延时或 ACK 轮询是必须的。
3.2 位操作模拟 I2C 的引脚方向控制
原文的底层函数没有用硬件 I2C 外设,而是用 GPIO 方向寄存器模拟开漏输出。以start_IIC为例:
void start_IIC(void) { P2OUT &= 0xf9; // 清 P2.1、P2.2 输出锁存 P2DIR &= 0xFD; // P2.1 设为输入,靠上拉电阻拉高 SDA P2DIR &= 0xFB; // P2.2 设为输入,靠上拉电阻拉高 SCL P2DIR |= 0x02; // P2.1 改为输出,输出 0,SDA 拉低 P2DIR |= 0x04; // P2.2 改为输出,输出 0,SCL 拉低 }这里用“方向寄存器切换输入/输出”来模拟开漏:要输出高电平时把引脚设为输入,让外部上拉电阻把线拉高;要输出低电平时设为输出并写 0。这种写法在 MSP430 上很常见,因为它的 GPIO 没有真正的开漏模式。0xFD是1111 1101,清的是 bit1;0xFB是1111 1011,清的是 bit2。停止条件则反过来:先拉低 SDA,再释放 SCL 为高,最后释放 SDA 为高,形成上升沿。
3.3 ACK 检查与 NACK 的时序差异
iic_ACK的实现是:释放 SDA 为高,释放 SCL 为高,然后while((P2IN & BIT1) == BIT1);死等 SDA 被从机拉低。如果从机忙或地址错误,SDA 一直为高,这里就会卡死。生产代码里通常要加超时计数,原文没写,但这是实际项目里必须补的。iic_NACK用于读操作的最后一个字节:主机释放 SDA、拉高 SCL、拉低 SCL 之后,主动把 SDA 拉低,告诉从机“不要再发了”。读连续数据时,前 number−1 个字节回 ACK,最后一个字节回 NACK,这个顺序在ReadNbyte里体现得很清楚。
4. 连续读的伪写时序:为什么读之前要先“写”一次地址
4.1 ReadNbyte 的两段式结构
24C256 的读操作不是直接给个地址就能读,而是要先做一次“伪写”(dummy write)把内部地址指针设置好,再发重复起始条件切换到读方向。原文的ReadNbyte正是这个结构:
void ReadNbyte(unsigned char *p, unsigned int addr, unsigned char number) { start_IIC(); send_char(0xa2); // 伪写:设备地址 + 写 iic_ACK(); send_char(addr / 256); // 送地址高字节 iic_ACK(); send_char(addr % 256); // 送地址低字节 iic_ACK(); start_IIC(); // 重复起始条件,不先发停止 send_char(0xa3); // 设备地址 + 读 iic_ACK(); do { *p = read_char(); p++; if (number != 1) S_ACK(); // 非最后一字节,回 ACK 继续读 } while (--number); iic_NACK(); // 最后一字节回 NACK stop_IIC(); }关键点在第二个start_IIC():它产生的是重复起始条件(Repeated Start),中间没有停止条件。如果这里误发了停止条件,地址指针会被复位,后续读到的就是错误位置的数据。这是 I2C 读 EEPROM 时非常经典的一个坑。
4.2 读操作的地址回卷行为
读操作同样受页边界约束。从某个地址开始连续读,页内计数器溢出后同样回卷到本页起始,而不是进入下一页。也就是说,如果你从地址 60 开始读 10 个字节,读到的第 5 个字节其实是第 0 页第 0 格的内容。这一点和写操作对称,但很多人只防了写、没防读,调试时看到数据“错位”却找不到原因。稳妥的做法是读操作也按页对齐切分,或者每次读的长度不超过“64 − 页内偏移”。
4.3 位翻转读字节的循环
read_char用tmp = 0x80起手,每轮先释放 SDA 为高,拉高 SCL,采样P2IN & 0x02,若为高则data1 |= tmp,然后拉低 SCL,tmp /= 2。这是标准的 MSB first 逐位采样。注意采样发生在 SCL 高电平期间,且要在拉低 SCL 之前完成,否则从机可能已经切换下一位。IIC_Delay()里三个_NOP()是为了满足时序,具体够不够取决于主频,主频高时要加长。
5. 避坑与排查:页写翻车的五条血泪经验
5.1 现象:写入后读回,后半段数据变成前半段的副本
原因:连续写跨越了 64 字节页边界,页内计数器回卷覆盖。解决:写之前计算offset = addr & 0x3F,若offset + len > 64,拆成两次写,第一次写到页尾,第二次从下一页起始继续。这是最根本的修法,比“补零对齐”更通用。
5.2 现象:每次写 30 字节,按 32 对齐补零后仍然偶发错位
原因:补零只解决了单次写的长度对齐,没解决起始地址对齐。如果起始地址是 30,补到 32 后仍然跨页。解决:起始地址也要按块大小对齐,即addr = (addr / 32) * 32,长度补到 32 的整数倍,保证每次写的区间完整落在某个 32 字节块内,而 32 字节块必然不跨 64 字节页。
5.3 现象:连续写多页数据,第一页正确,后续页全错
原因:两次页写之间没有等待内部写周期,第二页的起始条件在芯片忙时发出,被忽略或错位。解决:每次stop_IIC()后加 5~10ms 延时,或者用 ACK 轮询(发设备地址,直到收到 ACK 再继续)。轮询比固定延时更稳,也不浪费总线时间。
5.4 现象:读操作返回的数据整体偏移一个字节
原因:伪写阶段之后误发了停止条件,地址指针被复位,或者重复起始条件的时序不对。解决:确认伪写和读之间只有 Repeated Start,没有 Stop;用逻辑分析仪抓波形,看第二个起始条件前 SDA/SCL 是否满足建立保持时间。
5.5 现象:iic_ACK死循环,程序卡住
原因:从机未应答(地址错、芯片忙、总线被拉死),while((P2IN & BIT1) == BIT1);没有超时。解决:加计数器超时,超时后发停止条件并复位总线;同时检查上拉电阻是否缺失、设备地址0xa2是否与实际 A2/A1/A0 引脚电平匹配。
6. 分页对齐的取舍:为什么“补零到 2 的幂”是工程上的最优解
回到那个核心矛盾:24C256 的页写缓冲区只有 64 字节且不回卷,而实际采集的数据长度往往不是 64 的约数。原文给出的方案是“存储块大小取 64、32、16、8、4、2,不足补零”。这个方案的本质,是把变长记录强行规整成 2 的幂长度,使得任意一个块的起始地址天然满足“页内偏移 + 块长 ≤ 64”——因为 2 的幂块长一定能整除 64,块与页边界对齐。
代价是存储空间浪费。30 字节的数据按 32 存,浪费 2 字节,约 6%;如果数据是 33 字节,就得按 64 存,浪费接近一半。这是典型的空间换正确性。另一种思路是“跨页拆分写”:不补零,遇到页边界就拆成两次页写,每次都不跨页。这样空间零浪费,但写次数变多,且需要维护一个“当前写到哪”的状态机,代码复杂度上升。我一般会这么理解:数据量小、写入不频繁的场景,补零对齐更省心;数据量大、空间敏感的场景,拆分写更合适。没有绝对优劣,只有取舍。
还有一个容易被忽略的点:24C256 的页大小是 64 字节,但并不是所有 I2C EEPROM 都一样。同系列的 24C02 页大小是 8 字节,24C64 是 32 字节。如果把 24C256 的“按 64 对齐”习惯直接搬到小容量型号上,会写出跨页 bug。所以对齐逻辑里的页大小参数应该做成宏或配置项,而不是硬编码 64。
验证方法上,最直接的是写一个“边界扫描”测试:从地址 0 到 32767,每隔一个偏移量写一段已知模式的数据,读回比对,重点覆盖 63/64、127/128 这些页边界。用逻辑分析仪抓一次跨页写的波形,看停止条件是否在页缓冲区满之前发出,比读代码更快定位问题。从那以后我每次碰 EEPROM 的连续写,都强制先算一遍addr & 0x3F和len的关系,再决定拆不拆——这个习惯帮我省掉了太多深夜调试。希望帮到你。
本文还有配套的精品资源,点击获取