简介:这是一份面向嵌入式开发初学者与单片机工程师的24C256存储器中文技术资料,围绕I2C接口EEPROM的存储原理、地址管理与数据传输控制展开,帮助读者理解并规避连续存储中常见的地址越界与数据覆盖问题。资源包内含1个doc文档,压缩包约33KB,篇幅精炼,适合作为MSP430等平台驱动24C256时的速查与排错参考。资料重点剖析了16位地址中低6位页内地址与高9位页码的划分方式,指出连续写入时若存储块大小不是64、32、16、8、4、2的整数倍,数据会在页边界处回卷覆盖,并给出按32字节对齐、不足补零的解决思路。同时附有MSP430F449的IIC控制模块代码片段,涵盖延时、启动、停止、发送0/1、读写单字符及应答检查等函数,便于读者直接对照移植。目前已有1913人学习下载,适合需要快速掌握24C256页写机制与I2C底层时序的开发者参考。
1. 24C256 中文资料拆解:512×64 存储结构下,为什么连续写 11 字节也会翻车
如果你手头正调一块 I2C 的 EEPROM,型号是 24C256,大概率会遇到一个很玄学的现象:单字节读写都正常,连续写一小段数据,回读却发现后半段被改写了,或者干脆从本页开头重新覆盖。这份中文资料的价值就在这——它没有停在“I2C 时序怎么拉高拉低”这种层面,而是直接点破了 24C256 最容易被忽视的存储地址问题。24C256 是 512×64 的存储结构,总容量 32768 字节,地址是 16 位整型数,但有效数据只有 15 位,低 6 位表示页内偏移 0~63,高 9 位表示页码 0~511。资料里给了一个很典型的反直觉例子:每次连续写 11 字节,地址按 0、11、22、33、44、55、66 递增,看上去没问题,但写到地址 55 时,本页只剩 9 个字节空间,凑不够 11 个,芯片内部地址指针翻回本页 0 地址继续写,数据就被覆盖了。这份资料适合正在用 MSP430 或类似单片机裸机驱动 24C256 的工程师,尤其是那些“时序对了、ACK 也回了、数据就是不对”的现场调试人员。
2. 地址位与页边界:把 15 位有效地址拆成页号和页内偏移
2.1 为什么 32768 字节的地址只有 15 位有效
24C256 的数据容量是 32768 字节,也就是 32K,对应地址范围 0~32767。16 位地址线理论上能表示 65536 个地址,但 24C256 只用到了低 15 位,最高位不参与译码。资料里写得很清楚:低 6 位(0~5)表示页内偏移,范围 0~63;连续的 9 位(6~14)表示页码,范围 0~511。512 页 × 64 字节/页 = 32768 字节,刚好对上。
这个拆分方式决定了你在写驱动时不能把地址当成一个单纯的线性整数来用。很多新手写 EEPROM 驱动,习惯性地把地址直接塞进两个字节发出去,然后循环写 N 个字节,认为芯片会自己处理翻页。但 24C256 的连续写模式有一个硬边界:一页最多 64 字节,写到页尾后地址指针自动回卷到本页起始地址,而不是跳到下一页。
我一般会在驱动层加一个地址分解函数,把线性地址拆成页号和页内偏移,写之前先判断这次要写的数据会不会跨页。如果会跨页,就拆成多次写,每次不超过当前页剩余空间。
// 地址分解:从线性地址提取页号和页内偏移 // addr: 0 ~ 32767 // page: 0 ~ 511 // offset: 0 ~ 63 void addr_decode(unsigned int addr, unsigned int *page, unsigned char *offset) { *offset = addr & 0x3F; // 低6位:页内偏移 *page = (addr >> 6) & 0x1FF; // 高9位:页码 } // 计算当前地址到本页末尾还能写多少字节 // 返回值:剩余可连续写的字节数(1~64) unsigned char page_remain(unsigned int addr) { unsigned char offset = addr & 0x3F; return 64 - offset; }逻辑说明:addr_decode把 16 位地址拆成页码和页内偏移,方便你在调试时打印出来看当前落在哪一页。page_remain返回从当前地址到本页末尾的剩余字节数,如果这个值小于你要写的长度,就必须先写一部分、等 EEPROM 内部写周期完成、再重新发起下一次写。
参数说明:addr是线性字节地址,范围 0~32767;page和offset是输出参数,分别返回页码和页内偏移。page_remain的返回值最小是 1(当地址正好在页末时),最大是 64(当地址正好在页首时)。
2.2 页写模式下的地址回卷现象
资料里举的例子非常典型:每次写 11 字节,地址序列是 0、11、22、33、44、55、66。前五次写都没问题,因为 0~54 这段地址覆盖的页内偏移分别是 0~10、11~21、22~32、33~43、44~54,都在第 0 页的 0~63 范围内。但第六次从地址 55 开始写 11 字节,55~65 已经超出了第 0 页的 63 边界。芯片内部地址指针在写到 63 之后,自动回卷到第 0 页的 0 地址,继续把剩下的字节写到 0、1、2……位置,把之前存的数据覆盖掉。
这个行为不是 bug,是 24C256 连续写模式的固有特性。资料里给出的解决办法是:如果使用连续存储模式,地址安排上要使存储块的大小为 64、32、16、8、4、2,除此之外都不能使用连续地址存储。如果每次采集的有效数据是 30 字节,在连续存储模式下要按照 32 为单位存储,不足的字节补零处理。
我一般会在应用层做一个对齐写入的封装:不管上层要写多少字节,都按 2 的幂次对齐到页边界。比如每次写 30 字节,就分配 32 字节的空间,写 30 字节有效数据加 2 字节补零。这样每次写的起始地址都是 32 的倍数,永远不会跨页。
// 按32字节对齐写入,不足补零 // p: 数据缓冲区指针 // addr: 线性地址,必须是32的倍数 // len: 有效数据长度,1~32 void write_aligned_32(unsigned char *p, unsigned int addr, unsigned char len) { unsigned char buf[32]; unsigned char i; // 拷贝有效数据 for(i = 0; i < len; i++) { buf[i] = p[i]; } // 不足32字节的部分补零 for(; i < 32; i++) { buf[i] = 0x00; } // 调用底层连续写函数,一次写32字节 WriteNbyte(buf, addr, 32); // 等待EEPROM内部写周期完成,典型值5ms delay(10); }逻辑说明:write_aligned_32先把有效数据拷贝到 32 字节的临时缓冲区,不足的部分补零,然后一次性写入 32 字节。因为 32 是 64 的因数,只要起始地址是 32 的倍数,这次写入就不会跨页。
参数说明:p是上层传入的数据指针;addr必须是 32 的倍数,否则对齐逻辑失效;len是有效数据长度,范围 1~32。delay(10)是等待 EEPROM 内部写周期完成,24C256 的典型写周期是 5ms,留 10ms 余量比较稳妥。
3. I2C 底层驱动:从 start_IIC 到 WriteNbyte 的完整链路
3.1 起始、停止与位发送的时序实现
资料里给出的 MSP430F449 驱动代码是典型的软件模拟 I2C 实现,通过操作 P2 口的方向寄存器来控制 SDA 和 SCL 的拉高拉低。这种写法在 MSP430 上很常见,因为很多型号的硬件 I2C 模块用起来限制较多,软件模拟反而更灵活。
先看起始条件start_IIC:先把 P2OUT 的低两位清零,然后通过 P2DIR 寄存器控制 SDA 和 SCL 的方向。P2DIR 某位为 1 表示输出,为 0 表示输入。P2DIR&=0XFD把 P2.1 设为输入,外部上拉电阻把 SDA 拉高;P2DIR&=0XFB把 P2.2 设为输入,SCL 也被拉高。然后P2DIR|=0X02把 SDA 拉低,P2DIR|=0X04把 SCL 拉低。这个顺序保证了在 SCL 高电平期间 SDA 产生下降沿,符合 I2C 起始条件。
停止条件stop_IIC反过来:先拉低 SDA,再拉高 SCL,最后拉高 SDA,在 SCL 高电平期间产生 SDA 上升沿。
发送 0 和发送 1 的区别在于 SDA 的状态:send_zero先拉低 SDA,再拉高 SCL,再拉低 SCL;send_one先释放 SDA(设为输入,靠上拉电阻拉高),再拉高 SCL,再拉低 SCL。每个位发送后都有一个IIC_Delay(),用三个_NOP()做短延时,保证时序满足 24C256 的要求。
// 发送一个字节:MSB先行 // data_out: 要发送的字节 void send_char(unsigned char data_out) { unsigned char i, tmp = 0x80; for(i = 0; i < 8; i++) { if((data_out & tmp) > 0) send_one(); // 当前位为1 else send_zero(); // 当前位为0 tmp /= 2; // 掩码右移,从bit7到bit0 } }逻辑说明:send_char从最高位开始逐位发送,tmp初始为 0x80,每次循环除以 2,依次对应 bit7 到 bit0。判断data_out & tmp的结果决定发送 1 还是 0。
参数说明:data_out是要发送的字节,调用前需要确保 I2C 总线处于起始条件之后的状态。这个函数不处理 ACK,ACK 由单独的iic_ACK函数处理。
3.2 连续写与连续读的地址发送顺序
WriteNbyte是连续写的核心函数。它先发起始条件,然后发设备地址 0xA2(24C256 的写地址,7 位地址 0x51 左移一位加写标志 0),等 ACK,然后发地址高字节addr/256,等 ACK,再发地址低字节addr%256,等 ACK,然后循环发送数据字节,每发一个等一次 ACK,最后发停止条件,延时 10ms 等待内部写周期完成。
这里有一个容易踩坑的地方:24C256 的地址是 16 位的,但设备地址字节只占 8 位,所以地址需要分两个字节发送,先高后低。资料里的send_char(addr/256)和send_char(addr%256)就是做这件事。如果你只发一个字节的地址,芯片会把它当成高字节,低字节默认是 0,结果就是所有数据都写到 256 的倍数地址上。
ReadNbyte的流程稍微复杂一点:先发起始条件,发写地址 0xA2,等 ACK,发地址高字节,等 ACK,发地址低字节,等 ACK,然后重新发起始条件(Restart),发读地址 0xA3,等 ACK,然后循环读数据。每读一个字节后,如果不是最后一个字节,发 ACK 告诉芯片继续给数据;如果是最后一个字节,发 NACK 告诉芯片停止,然后发停止条件。
// 连续读:先写地址,再重启读 // p: 接收缓冲区 // addr: 起始地址 // number: 要读的字节数 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(); }逻辑说明:ReadNbyte先用写模式发送要读的起始地址,然后发 Restart 条件,切换到读模式。循环读数据时,每读一个字节判断是否还有后续字节,有就发 ACK,没有就发 NACK,最后发停止条件。
参数说明:p是接收缓冲区指针,调用者需要保证缓冲区足够大;addr是要读的起始地址,16 位;number是要读的字节数,注意这个变量在while(--number)中被递减,传入后会被修改。
4. 避坑与排查:24C256 连续写数据出错的五个现场记录
4.1 现象:写 11 字节,第 6 次开始数据覆盖本页开头
原因:地址 55 开始写 11 字节,55+11=66,超出第 0 页的 63 边界,芯片内部地址指针回卷到本页 0 地址继续写,覆盖了之前地址 0~2 的数据。
解决:写之前用page_remain计算当前页剩余空间,如果剩余空间小于要写的长度,就拆成两次写。第一次写到页末,延时等待写周期完成,第二次从下一页页首继续写。
4.2 现象:单字节读写正常,连续写超过 64 字节后数据错乱
原因:24C256 的连续写模式一页最多 64 字节,超过 64 字节后地址指针回卷,不是跳到下一页。很多新手以为芯片会自动翻页,实际上不会。
解决:在驱动层限制单次连续写的最大长度为 64 字节,超过 64 字节的数据拆成多次写,每次写之前重新发送起始地址。
4.3 现象:写入后立即回读,数据不对,延时 10ms 后回读正常
原因:24C256 在收到停止条件后启动内部写周期,典型时间 5ms,最大可能到 10ms。在写周期内芯片不响应任何 I2C 命令,回读会失败或读到旧数据。
解决:每次写操作后加delay(10),或者用 ACK 轮询的方式判断芯片是否完成内部写周期。ACK 轮询的做法是:发起始条件,发写地址 0xA2,如果芯片回 ACK 说明写周期完成,如果没回 ACK 就继续等。
4.4 现象:地址超过 255 后写入的数据全部跑到 256 的倍数地址上
原因:地址只发送了一个字节,芯片把低字节默认当成 0。24C256 的地址是 16 位的,必须分两个字节发送,先高后低。
解决:检查WriteNbyte和ReadNbyte中是否发送了addr/256和addr%256两个字节。如果只发了一个字节,补上高字节的发送逻辑。
4.5 现象:I2C 总线上挂多个设备,24C256 不响应 ACK
原因:24C256 的 7 位设备地址是 0x51,写地址 0xA2,读地址 0xA3。如果总线上有其他 EEPROM 型号(比如 24C02 的地址是 0xA0),地址冲突会导致 24C256 不响应。
解决:确认总线上每个设备的地址不冲突。24C256 的 A0、A1、A2 引脚可以配置地址的低 3 位,如果多个 24C256 挂同一条总线,通过这三个引脚区分地址。
5. 进阶技巧:用 ACK 轮询替代固定延时,把写入周期压到最短
固定delay(10)虽然简单,但每次写都等 10ms,批量写入时累积起来很可观。24C256 的典型写周期是 5ms,如果每次都能在写周期刚结束时立刻发起下一次写,整体写入速度能提升接近一倍。我一般会用 ACK 轮询来做这件事。
具体做法是:每次写操作发完停止条件后,不直接延时,而是循环发起起始条件加写地址 0xA2,如果芯片回 ACK 说明内部写周期已完成,可以立即进行下一次写;如果没回 ACK,就继续轮询,直到超时。
// ACK轮询:等待EEPROM内部写周期完成 // 返回值:0=成功,1=超时 unsigned char wait_ack_polling(void) { unsigned int timeout = 1000; // 超时计数,根据主频调整 while(timeout--) { start_IIC(); send_char(0xA2); // 发写地址 // 读SDA状态判断ACK P2DIR &= 0xFD; // SDA设为输入 IIC_Delay(); P2DIR &= 0xFB; // SCL拉高 IIC_Delay(); if((P2IN & 0x02) == 0x00) { // SDA为低,收到ACK P2DIR |= 0x04; // SCL拉低 stop_IIC(); return 0; // 写周期完成 } P2DIR |= 0x04; // SCL拉低 stop_IIC(); IIC_Delay(); } return 1; // 超时 }逻辑说明:wait_ack_polling在循环中反复发起起始条件并发送写地址,每次读 SDA 线状态。如果 SDA 为低,说明芯片拉低了 SDA 表示 ACK,内部写周期已完成。如果循环结束还没收到 ACK,返回超时错误。
参数说明:timeout的初始值需要根据主频和 I2C 速率调整。在 1MHz 主频、软件模拟 I2C 的情况下,1000 次循环大约对应几十毫秒,足够覆盖 24C256 的最大写周期。如果主频更高,可以适当减小这个值。
用 ACK 轮询替代固定延时后,连续写 32KB 数据的总时间从原来的 512 页 × 10ms = 5120ms 降到大约 512 页 × 5ms = 2560ms,节省了一半时间。这个技巧在数据采集频率较高、需要频繁存储的场景下很有用。
从那以后我每次调 EEPROM 驱动,都会先写一个页边界测试:从地址 0 开始,每次写 11 字节,写 10 次,然后回读全部数据,看第 6 次写的数据有没有覆盖前面的内容。这个测试能在一分钟内暴露地址回卷问题,比在完整应用里调试快得多。希望帮到你。
本文还有配套的精品资源,点击获取