简介:这份中文资料围绕I2C接口存储器24C256的说明与使用展开,面向单片机开发初学者及需要驱动该芯片的嵌入式工程师,重点解决连续存储时地址管理不当导致的数据覆盖与写入错误问题。压缩包内共1个doc文档,约33KB,以文字讲解配合代码片段的形式呈现,便于边读边对照调试。资料系统梳理了24C256的存储原理、16位地址结构、页内与页地址划分、连续存储模式及IIC控制模块实现,并给出MSP430F449平台下的延时、启动、停止、发送0与1、读写单字符、应答检查等基础函数示例。针对每次写入11字节在地址55处出错的现象,文档分析了跨页覆盖原因,提出存储块大小应取64、32、16、8、4、2的解决思路,并说明不足字节补零的处理方式。目前已有1913人学习下载,适合需要快速掌握24C256读写时序与排错方法的读者参考。
1. 24C256 中文资料怎么用:从一颗存储芯片到可复现的读写方案
24C256 是一颗 256Kbit(32KB)的 I2C 串行 EEPROM,很多做嵌入式、传感器模组、工控板卡的工程师第一次接触它,往往是在一块现成板子上看到一颗 8 脚小芯片,丝印写着 24C256,然后开始找中文资料。问题在于,网上能搜到的中文资料大多是零散的引脚图、时序截图和一段来路不明的驱动代码,真正要落地时,你会发现卡点不在“看不懂”,而在“参数对不上、地址算错、写进去读出来是 0xFF”。这篇笔记就按一线调试顺序,把 24C256 的容量换算、I2C 地址、页写边界、跨页处理、写周期等待和常见翻车点讲清楚,让新手能照着跑通,熟手能拿去核对边界。适合正在用 MCU、Linux 用户态 I2C 或逻辑分析仪调 EEPROM 的从业者。
2. 24C256 的容量、地址与页结构:先把账算对
2.1 256Kbit 到底是多少字节,地址线为什么是 15 根
24C256 的命名里,24C 是 I2C EEPROM 系列,256 表示 256Kbit。换算成字节是 256 × 1024 ÷ 8 = 32768 字节,也就是 32KB。这个容量决定了片内地址需要 15 根地址线,因为 2^15 = 32768。很多中文资料只写“32K 字节”,但实际访问时,地址是两字节传输:高字节在前,低字节在后。高字节里只有低 7 位有效,最高位在多数型号里是 don't care,但为了兼容性,建议固定写 0。
这里有一个容易混淆的点:24C256 的“256”不是 256 字节,也不是 256KB,而是 256Kbit。如果你在代码里按 256 字节去算页边界,后面一定翻车。我一般会在驱动里直接定义宏:
#define EEPROM_24C256_SIZE_BYTES (32768U) #define EEPROM_24C256_PAGE_SIZE (64U) #define EEPROM_24C256_PAGE_MASK (EEPROM_24C256_PAGE_SIZE - 1U)逻辑说明:SIZE_BYTES 用于边界检查,PAGE_SIZE 用于跨页拆分,PAGE_MASK 用于计算当前地址在页内的偏移。参数说明:不同厂家 24C256 的页大小可能不同,常见有 32 字节、64 字节、128 字节三种,必须查你手上那颗料的具体手册。如果你不确定,最稳妥的办法是写一个跨页测试:从地址 0 开始连续写 200 字节,再读回来比对,如果每 64 字节断一次,说明页大小是 64。
2.2 7 位 I2C 地址怎么定,A0/A1/A2 不是随便接的
24C256 的 I2C 从机地址是 7 位,高 4 位固定为 1010,低 3 位由 A2、A1、A0 引脚决定。也就是说,同一根 I2C 总线上最多挂 8 片 24C256,地址范围是 0x50 到 0x57。注意,这里说的是 7 位地址,不是 8 位读写地址。很多中文资料里写“写地址 0xA0,读地址 0xA1”,那是把 7 位地址左移一位后再加上读写位的结果。如果你在 Linux 用户态用 i2c-tools,它用的是 7 位地址,所以应该写 0x50,而不是 0xA0。
A0/A1/A2 的接法直接决定地址。常见做法是全部接地,地址就是 0x50。如果你板子上有多片,就要按二进制组合去接。这里有一个血泪经验:有些板子为了省事,把 A0/A1/A2 直接接到 VCC 或 GND,但走线很长,导致地址识别不稳定。如果你发现 i2cdetect 能扫到地址,但读写偶尔失败,先查这三根线的电平是否干净。
# Linux 下扫描 I2C 总线上的 24C256 i2cdetect -y 1 # 预期输出中会出现 0x50 到 0x57 中的一个或多个逻辑说明:i2cdetect 会遍历 7 位地址空间,发送快速写探测。参数说明:-y 表示跳过交互确认,1 是 I2C 总线编号,具体编号用 i2cdetect -l 查看。如果扫不到任何地址,先确认总线编号、上拉电阻和供电,再确认 A0/A1/A2 是否接对。
2.3 页写边界:64 字节一页,跨页必须拆
24C256 支持页写,一次 I2C 事务最多可以连续写一页。页大小常见是 64 字节。页写的内部机制是:你给出起始地址,然后连续送数据,芯片内部地址指针会自动递增,但只递增页内偏移,不会自动进位到下一页。也就是说,如果你从地址 0x3F 开始写 10 个字节,前 1 个字节落在第 0 页的最后一个位置,后面 9 个字节会回卷到第 0 页的开头,覆盖掉原来的数据。这就是跨页翻车的典型现象:写进去的数据顺序乱了,或者老数据被意外覆盖。
正确的做法是:在驱动层把任意长度的写操作拆成“页内写 + 跨页写”的组合。下面是一个可复现的拆分逻辑:
int eeprom_write_bytes(uint16_t addr, const uint8_t *buf, uint16_t len) { uint16_t written = 0; while (written < len) { uint16_t cur_addr = addr + written; uint16_t page_offset = cur_addr & EEPROM_24C256_PAGE_MASK; uint16_t space_in_page = EEPROM_24C256_PAGE_SIZE - page_offset; uint16_t chunk = (len - written) < space_in_page ? (len - written) : space_in_page; if (eeprom_page_write(cur_addr, buf + written, chunk) != 0) { return -1; } eeprom_wait_ready(); written += chunk; } return 0; }逻辑说明:每次循环计算当前地址在页内的偏移,得出本页还能写多少字节,然后只写这么多。写完一页后等待写周期完成,再继续下一页。参数说明:addr 是 15 位片内地址,buf 是数据缓冲区,len 是总长度。eeprom_wait_ready 的实现见下一章。这个函数不处理地址回卷,因为拆分后每次 chunk 都不会跨页。
3. 用 MCU 和 Linux 用户态两种方式跑通读写
3.1 裸机 MCU 的 I2C 时序:起始、地址、应答、写周期
在裸机 MCU 上,24C256 的写操作分两种:字节写和页写。字节写就是一次只写一个字节,页写是一次写一页。无论哪种,时序都是:起始条件 → 发送 7 位地址 + 写位 → 等待 ACK → 发送高字节地址 → 等待 ACK → 发送低字节地址 → 等待 ACK → 发送数据 → 等待 ACK → 停止条件。写完之后,芯片进入内部写周期,典型 5ms,最大 10ms。在这段时间内,芯片不会应答任何 I2C 事务,所以你必须等待。
等待写周期有两种做法:固定延时和应答轮询。固定延时简单,但效率低;应答轮询是反复发送起始条件 + 地址 + 写位,如果芯片回 ACK,说明写周期结束。我一般用应答轮询,因为不同批次的芯片写周期差异不小,固定延时容易在低温或低压下不够。
void eeprom_wait_ready(void) { uint32_t timeout = 100000; while (timeout--) { if (i2c_start() == 0) { if (i2c_send_byte(0xA0) == 0) { // 7 位地址 0x50 左移一位 + 写位 i2c_stop(); return; } i2c_stop(); } } // 超时处理:记录错误或复位 I2C 总线 }逻辑说明:每次尝试发送起始条件和写地址,如果收到 ACK,说明芯片已经准备好。参数说明:0xA0 是 7 位地址 0x50 的写方向字节,如果你用的是 0x51,这里就是 0xA2。timeout 是轮询次数,按 I2C 速率和芯片写周期估算,100000 次在 100kHz 下大约几百毫秒,足够覆盖 10ms 写周期。
读操作相对简单:起始 → 发送地址 + 写位 → 发送高字节地址 → 发送低字节地址 → 重复起始 → 发送地址 + 读位 → 读取 N 个字节 → 最后一个字节回 NACK → 停止。注意,读操作没有写周期,读完就可以继续。
3.2 Linux 用户态 i2c-tools 与 eeprom 驱动的读写命令
在 Linux 上,如果你只是临时验证,可以用 i2c-tools 的 i2cget 和 i2cset。但要注意,i2cset 默认是 SMBus 写字节,不是 I2C 块写,对 24C256 不一定适用。更可靠的方式是用 i2ctransfer,它可以构造任意 I2C 消息。
# 向地址 0x50 的 24C256 的片内地址 0x0000 写入 0x55 i2ctransfer -y 1 w3@0x50 0x00 0x00 0x55 # 从片内地址 0x0000 读取 1 字节 i2ctransfer -y 1 w2@0x50 0x00 0x00 r1逻辑说明:w3@0x50 表示向 7 位地址 0x50 写 3 个字节,分别是高地址、低地址、数据。r1 表示读 1 个字节。参数说明:-y 跳过确认,1 是总线编号。如果你要读多个字节,把 r1 改成 rN。注意,i2ctransfer 不会自动处理跨页和写周期,连续写多字节时仍然要自己拆页和等待。
如果你要在应用层做批量读写,更常见的做法是内核的 at24 驱动。它会把 EEPROM 映射成一个二进制文件,通常在 /sys/bus/i2c/devices/1-0050/eeprom。你可以用 dd 或 hexdump 直接读写。
# 读取前 64 字节 hexdump -C /sys/bus/i2c/devices/1-0050/eeprom | head -n 4 # 写入一个字节到偏移 0x10 printf '\xAA' | dd of=/sys/bus/i2c/devices/1-0050/eeprom bs=1 seek=16 conv=notrunc逻辑说明:at24 驱动内部已经处理了页写拆分和写周期等待,所以你不需要在应用层再拆。参数说明:bs=1 表示块大小 1 字节,seek=16 表示偏移 16,conv=notrunc 表示不截断文件。注意,eeprom 文件的大小就是芯片容量,写入超出范围会报错。
3.3 用逻辑分析仪抓一次完整写时序,确认 ACK 和停止位
如果你手上有逻辑分析仪,抓一次完整写时序是最快的排错方式。重点看几个点:起始条件是否干净,7 位地址和读写位是否正确,每个字节后的 ACK 是否拉低,停止条件是否在第九个时钟后出现。常见问题是停止条件太早,导致最后一个字节没写进去。另一个问题是上拉电阻太大,上升沿太缓,ACK 被误判。
我一般会抓一次“写两个字节到地址 0x0000”的波形,然后对照手册的时序图。如果 ACK 位是高,说明从机没有应答,先查地址和供电。如果数据位在时钟高电平期间不稳定,查走线和上拉。逻辑分析仪不需要很贵,几十兆采样率就够看 100kHz 的 I2C。
4. 24C256 避坑与排查:5 个真实翻车记录
4.1 写进去读出来全是 0xFF
现象:用 i2cset 写了一个字节,再用 i2cget 读,结果是 0xFF。原因:i2cset 默认走 SMBus 写字节,只发送一个字节数据,没有发送两字节片内地址,24C256 把第一个数据字节当成了高地址,后续行为完全错位。解决:改用 i2ctransfer,明确发送高地址、低地址、数据。或者用 at24 驱动的 eeprom 文件。
4.2 连续写 100 字节,只有前 64 字节对
现象:从地址 0 开始写 100 字节,读回来发现前 64 字节正确,后 36 字节是旧数据或 0xFF。原因:没有拆页,芯片在页内回卷,后 36 字节覆盖了第 0 页开头,而不是写到第 1 页。解决:在驱动层按页大小拆分,每写一页等待写周期。如果你用的是 at24 驱动,这个问题不存在,因为内核已经处理了。
4.3 写完后立刻读,数据没更新
现象:写操作返回成功,但立刻读同一个地址,读到的还是旧值。原因:没有等待写周期。24C256 在写周期内不应答,如果你在写周期结束前发起读,芯片不会响应,有些驱动会返回旧缓存或错误。解决:每次写操作后调用 eeprom_wait_ready,或者固定延时 10ms。用 at24 驱动时,内核会阻塞直到写完成。
4.4 i2cdetect 能扫到 0x50,但读写报 Remote I/O error
现象:i2cdetect 显示 0x50 存在,但 i2ctransfer 报 Remote I/O error。原因:A0/A1/A2 的电平在扫描时是浮空的,被误判为某个地址,实际读写时电平被拉偏,导致地址不匹配。解决:检查 A0/A1/A2 是否直接接 GND 或 VCC,不要浮空。如果必须浮空,加上拉或下拉电阻。
4.5 低温下写周期变长,固定延时不够
现象:常温下写 10ms 延时没问题,到了零下 20 度,偶尔写失败。原因:EEPROM 的写周期随温度降低而变长,有些批次在低温下最大写周期超过 10ms。解决:改用应答轮询,不要用固定延时。如果必须用延时,把延时加到 20ms 以上,并做写后读校验。
5. 进阶技巧:用写后校验和磨损均衡延长 24C256 寿命
24C256 的擦写寿命典型是 100 万次,听起来很多,但如果你每秒钟写一次同一个地址,不到 12 天就用完了。实际项目中,我一般会做两件事:写后校验和磨损均衡。
写后校验很简单:每次写完一个块,立刻读回来比对,如果不一致,重写一次,最多重试 3 次。这个习惯能挡住大部分偶发写入失败,尤其是电源波动或 I2C 干扰导致的位翻转。
int eeprom_write_verify(uint16_t addr, const uint8_t *buf, uint16_t len) { uint8_t tmp[64]; for (int retry = 0; retry < 3; retry++) { if (eeprom_write_bytes(addr, buf, len) != 0) continue; if (eeprom_read_bytes(addr, tmp, len) != 0) continue; if (memcmp(buf, tmp, len) == 0) return 0; } return -1; }逻辑说明:写完后立刻读回,用 memcmp 比对。参数说明:tmp 的大小要大于等于 len,这里假设单次校验不超过 64 字节。如果 len 很大,可以分块校验。重试 3 次仍然失败,说明硬件有问题,应该上报错误而不是继续写。
磨损均衡稍微复杂一点。如果你的应用是频繁写一个配置参数,不要每次都写同一个地址,而是把 32KB 分成多个槽位,轮流写。每个槽位带一个序号和 CRC,启动时扫描所有槽位,找序号最大且 CRC 正确的那个。这样写入次数被分散到整个芯片,寿命从 100 万次变成 100 万 × 槽位数。常见做法是每 64 字节一个槽,32KB 就是 512 个槽,寿命直接拉到 5 亿次以上。
还有一个细节:24C256 的页写是 64 字节,但如果你只写 1 个字节,芯片内部仍然会擦写整个页。所以频繁写单字节时,磨损均衡的收益更明显。我一般会在配置区预留 4KB 做槽位,剩下的 28KB 存日志或只读数据。
最后说一个我自己的习惯:每次拿到一颗新的 24C256,先做三件事——用 i2cdetect 确认地址,用 i2ctransfer 写一个字节再读回来,用逻辑分析仪抓一次波形。这三步做完,后面基本不会出大问题。希望帮到你。
本文还有配套的精品资源,点击获取