AT24C64 I2C EEPROM驱动开发:跨页写、应答轮询与避坑指南
简介AT24C64驱动文件面向嵌入式开发人员尤其是需要在微控制器系统中读写I²C接口EEPROM的工程师与学习者。资源提供drv_at24c64.c与drv_at24c64.h两个核心文件前者实现初始化I²C总线、按地址读取与写入数据、通信错误检查及等待应答等操作后者给出I²C地址、页写入大小、地址位数等宏定义与函数原型便于在其他模块中直接调用。压缩包共3个文件包含1个c源文件、1个h头文件和1个txt说明文档整体约1KB结构精简适合快速集成到固件工程中。已有450人学习下载。借助这套驱动读者可掌握AT24C64的典型读写流程与I²C通信要点用于保存设备序列号、配置参数等小量数据提升代码可移植性与可维护性也可作为嵌入式存储驱动开发的参考模板。1. 从一颗 EEPROM 说起AT24C64 驱动文件到底解决什么问题如果你手头有一块带 I2C 接口的存储芯片丝印写着 AT24C64而你的 MCU 工程里恰好缺一个能直接编译、能读写、能跨页写、能处理应答轮询的驱动文件那这份资源就是冲着你来的。AT24C64 是 64Kbit 容量的 I2C EEPROM换算成字节是 8192 字节页大小 32 字节7 位从机地址由 A2/A1/A0 三根引脚决定。它常出现在需要掉电保存参数、校准系数、设备序列号的板子上。驱动文件的价值不在于“能读写”而在于把跨页写、写周期等待、地址回卷、连续读这些容易翻车的细节一次性封装好。适合正在调试 I2C 存储的嵌入式工程师也适合刚接触 EEPROM 时序、想找一个可复现参考实现的新手。下面按“先讲清原理再落到代码最后说坑”的顺序拆开。2. AT24C64 的 I2C 时序与地址规则驱动前必须对齐的四个参数2.1 从机地址、页边界与容量换算AT24C64 的 7 位从机地址高四位固定为1010低三位由 A2/A1/A0 引脚电平决定。写操作地址是0xA0 | (A2A1A0)读操作地址是写地址加 1。很多驱动翻车就翻在这里有人直接把0xA0写死结果换了板子 A0 拉高就通信失败。容量 8192 字节意味着内部地址是 13 位需要两个字节传输高字节在前。页大小 32 字节写操作不能跨页这是硬件层面的限制不是驱动能绕过的。参数取值说明容量8192 字节64Kbit / 8页大小32 字节单次写不能跨页地址宽度13 位需两字节地址从机地址1010 A2A1A0读地址为写地址 1最大写周期5ms典型值轮询应答更稳2.2 写周期与应答轮询的必要性AT24C64 每次写操作后芯片进入内部写周期典型 5ms期间不响应任何 I2C 命令。如果你写完立刻读大概率读到旧数据或者 NACK。常见做法是延时 5ms 再操作但更稳的是应答轮询反复发起写地址直到芯片返回 ACK说明内部写完成。这个细节决定了驱动在批量写参数时会不会丢数据。我一般会在驱动里提供两个版本一个带固定延时一个带轮询默认用轮询。/* 应答轮询反复发起写地址直到 ACK 或超时 */ static int at24c64_wait_ready(uint8_t dev_addr, uint32_t timeout_ms) { uint32_t start get_tick_ms(); while ((get_tick_ms() - start) timeout_ms) { if (i2c_probe(dev_addr) 0) { /* 收到 ACK */ return 0; } } return -1; /* 超时芯片可能未焊接或总线异常 */ }逻辑说明i2c_probe只发送从机地址不发送数据收到 ACK 即表示芯片空闲。参数timeout_ms建议给 10 到 20留出余量。如果一直超时先查总线上拉电阻和焊接而不是改超时时间。2.3 连续读与地址回卷连续读是 AT24C64 的强项给一个起始地址然后一直读地址会自动递增到 8192 边界回卷到 0。这意味着你可以一次读出一整块参数区不需要每读一字节就重新发地址。但要注意回卷是硬件行为如果你读的长度超过剩余空间数据会从头开始覆盖驱动层最好做长度校验避免调用者误用。/* 连续读从 addr 开始读 len 字节自动处理地址回卷 */ int at24c64_read(uint8_t dev_addr, uint16_t addr, uint8_t *buf, uint16_t len) { if (addr len AT24C64_SIZE) { return -1; /* 驱动层拦截越界不依赖硬件回卷 */ } i2c_start(dev_addr | 0x01); /* 读地址 */ for (uint16_t i 0; i len; i) { buf[i] i2c_read_byte(i (len - 1)); } i2c_stop(); return 0; }参数说明addr是 13 位内部地址len由调用者保证不越界。最后一个字节发送 NACK 再 STOP这是 I2C 连续读的标准收尾。3. 驱动文件落地从写一字节到跨页写的完整实现3.1 单字节写与页写封装单字节写是最小单元发写地址、发地址高字节、发地址低字节、发数据、STOP、等写周期。页写则是在同一页内连续写多个字节地址低 5 位决定页内偏移高 8 位决定页号。如果写入长度超过页尾硬件会回卷到本页开头覆盖而不是进入下一页。这是 AT24C64 最经典的坑驱动必须在页写函数里做拆分。/* 页写自动按 32 字节页边界拆分避免跨页回卷 */ int at24c64_write_page(uint8_t dev_addr, uint16_t addr, const uint8_t *buf, uint16_t len) { while (len 0) { uint16_t page_remain AT24C64_PAGE_SIZE - (addr % AT24C64_PAGE_SIZE); uint16_t chunk (len page_remain) ? len : page_remain; i2c_start(dev_addr 0xFE); /* 写地址 */ i2c_write_byte(addr 8); /* 地址高字节 */ i2c_write_byte(addr 0xFF); /* 地址低字节 */ for (uint16_t i 0; i chunk; i) { i2c_write_byte(buf[i]); } i2c_stop(); if (at24c64_wait_ready(dev_addr, 20) ! 0) { return -1; /* 写周期未完成 */ } addr chunk; buf chunk; len - chunk; } return 0; }逻辑说明page_remain算出当前地址到页尾还剩多少字节chunk取剩余长度和页剩余的最小值。每写完一页就轮询等待再继续下一页。参数dev_addr是写地址addr是 13 位内部地址。这个函数是驱动文件的核心跨页写、写周期等待、地址递增都在里面。3.2 参数区读写与校验实际项目里不会只写一字节通常是保存一个结构体参数区。常见做法是在驱动之上再封一层把结构体序列化成字节数组写入指定地址读回时做 CRC 校验。这样即使写入过程中掉电也能通过 CRC 判断参数是否有效回退到默认值。/* 参数区写入先写数据再写 CRC最后回读校验 */ int at24c64_save_params(uint8_t dev_addr, uint16_t base, const void *params, uint16_t size) { uint8_t crc crc8(params, size); if (at24c64_write_page(dev_addr, base, (const uint8_t *)params, size) ! 0) { return -1; } if (at24c64_write_page(dev_addr, base size, crc, 1) ! 0) { return -1; } return 0; }参数说明base是参数区起始地址建议避开 0 地址因为 0 地址有时被用作上电标志。size是结构体字节数CRC 存在参数区末尾。读回时先读数据再读 CRC比对失败就加载默认参数。3.3 上电初始化与设备探测驱动文件通常提供一个初始化函数做两件事探测芯片是否存在、检查参数区是否有效。探测就是发写地址看 ACK参数区检查就是读 CRC。如果芯片不存在初始化返回错误上层决定是否继续运行。这个设计比“默认成功”要稳避免在芯片虚焊时写出莫名其妙的行为。/* 初始化探测芯片 校验参数区 */ int at24c64_init(uint8_t dev_addr, uint16_t param_base, uint16_t param_size) { if (i2c_probe(dev_addr) ! 0) { return -1; /* 芯片未响应 */ } uint8_t crc_stored, crc_calc; uint8_t *buf malloc(param_size); if (at24c64_read(dev_addr, param_base, buf, param_size) ! 0) { free(buf); return -2; } at24c64_read(dev_addr, param_base param_size, crc_stored, 1); crc_calc crc8(buf, param_size); free(buf); return (crc_calc crc_stored) ? 0 : 1; /* 1 表示参数无效 */ }返回值设计0 表示芯片正常且参数有效1 表示芯片正常但参数无效需要加载默认值负值表示通信失败。调用者根据返回值决定后续流程比单一布尔值信息量大。4. 避坑与排查AT24C64 调试中最容易翻车的五个点4.1 现象写入后立刻读数据没变原因芯片处于内部写周期不响应命令读回的是旧数据或随机值。解决每次写操作后调用at24c64_wait_ready轮询或者至少延时 5ms。不要用“读一次不对再延时读第二次”这种补丁批量写时会累积出错。4.2 现象跨页写时后半段数据覆盖了前半段原因页写超过 32 字节边界硬件回卷到本页开头。解决用at24c64_write_page自动拆分不要自己拼一个大数组直接发。如果已经写了裸驱动检查页写函数里有没有addr % 32的判断。4.3 现象换一块板子就通信失败原因A2/A1/A0 引脚电平不同从机地址变了而驱动里写死了0xA0。解决把从机地址作为参数传入或者用宏定义在板级头文件里配置。探测阶段可以先扫描0xA0到0xAE的偶地址确认实际地址。4.4 现象连续读长度超过剩余空间数据从头开始原因AT24C64 地址到 8192 后自动回卷到 0这是硬件行为。解决在at24c64_read里加addr len 8192的判断直接返回错误。不要依赖硬件回卷那会让调用者困惑。4.5 现象参数区偶尔校验失败原因写入过程中掉电数据写了一半CRC 自然对不上。解决这是 CRC 机制正常工作的表现不是 bug。上层检测到校验失败后加载默认参数并重新保存即可。如果想更稳可以用双备份区加标志位但会增加代码量按项目需求取舍。5. 进阶技巧把驱动做成可移植层与批量写入优化5.1 用函数指针隔离 I2C 硬件层驱动文件如果直接调用某个 MCU 的 I2C 寄存器换平台就要重写。常见做法是定义一个 I2C 操作结构体把 start、stop、write_byte、read_byte、probe 做成函数指针初始化时传入。这样 AT24C64 的时序逻辑完全复用只替换底层实现。typedef struct { int (*probe)(uint8_t addr); void (*start)(uint8_t addr); void (*stop)(void); int (*write_byte)(uint8_t data); uint8_t (*read_byte)(int ack); } i2c_bus_t; static const i2c_bus_t *bus; void at24c64_bind_bus(const i2c_bus_t *b) { bus b; /* 绑定后所有操作走函数指针 */ }参数说明probe返回 0 表示 ACK非 0 表示 NACK。read_byte的ack参数为 1 时发送 ACK为 0 时发送 NACK。绑定后驱动不再依赖具体硬件移植时只写一个适配层。5.2 批量写入时合并轮询等待如果你要写多个不连续的参数块每块都等 5ms 会拖慢启动时间。一个技巧是连续发起多个页写中间不等待最后统一轮询一次。但要注意芯片在写周期内不响应任何命令所以第二个页写的起始信号会 NACK。正确做法是每页写完后轮询但轮询本身很快实际开销主要是写周期。如果参数区是连续的直接用at24c64_write_page一次调用内部自动拆分和等待比手动分块更省心。5.3 验证方法用已知图案做全片读写测试驱动写完后我习惯做一次全片测试从 0 到 8191 写入递增图案再读回比对。这个测试能暴露页边界、地址回卷、写周期等待的所有问题。测试代码不复杂但跑一遍心里有底。/* 全片测试写入递增图案并回读比对 */ int at24c64_self_test(uint8_t dev_addr) { uint8_t wbuf[32], rbuf[32]; for (uint16_t addr 0; addr AT24C64_SIZE; addr 32) { for (int i 0; i 32; i) { wbuf[i] (uint8_t)((addr i) 0xFF); } if (at24c64_write_page(dev_addr, addr, wbuf, 32) ! 0) { return -1; } if (at24c64_read(dev_addr, addr, rbuf, 32) ! 0) { return -2; } if (memcmp(wbuf, rbuf, 32) ! 0) { return -3; /* 比对失败定位到具体地址 */ } } return 0; }逻辑说明每次写 32 字节正好一页写完立即读回比对。如果失败返回值能区分是写失败、读失败还是数据不一致。这个测试会擦写全片量产固件里不要保留只在调试阶段用。从那以后我每次拿到新的 EEPROM 驱动都强制先跑一遍全片读写测试再跑一遍掉电模拟确认 CRC 机制真的能兜住。希望帮到你。本文还有配套的精品资源点击获取