资讯详情

资讯详情

AT24C64 EEPROM驱动开发实战:I2C读写、跨页处理与避坑指南

简介AT24C64驱动文件面向嵌入式开发人员尤其是需要在微控制器系统中读写I²C接口EEPROM的工程师与学习者。AT24C64常用于保存设备配置信息或小量数据本资源提供一套可直接集成的驱动组件解决与AT24C64通信时初始化、读写及错误处理等底层实现问题。压缩包共3个文件包含1个c源文件、1个h头文件和1个txt文本整体约1KB体积轻巧便于嵌入工程。其中c文件实现I²C总线初始化、指定地址的数据读取与写入、通信失败时的错误检查以及等待应答等辅助操作h文件提供宏定义、函数原型与配置结构体声明方便其他模块调用。已有450人学习下载适合希望快速完成EEPROM读写功能、提升代码可移植性与可维护性的开发者参考借鉴。1. 从一颗 EEPROM 说起AT24C64 驱动文件到底解决什么问题如果你手头有一块带 I2C 接口的存储芯片型号是 AT24C64容量 64Kbit也就是 8KB想用它保存配置参数、校准数据或者掉电不能丢的小数据那你大概率会卡在第一步怎么让 MCU 稳定地读写它。这份 AT24C64 驱动文件就是干这个的——它把 I2C 底层时序、器件地址、页写边界、写周期等待这些琐碎但容易翻车的细节封装成几个干净的函数让你在应用层只关心“读哪个地址、写什么数据”。它适合正在做嵌入式存储方案、需要快速把 EEPROM 跑通的开发者也适合想拿一份可移植驱动做二次开发的人。我见过太多人因为页写跨页没处理、ACK 轮询没做导致数据写一半丢一半最后怀疑是芯片坏了——其实问题全在驱动层。2. 驱动文件结构拆解从器件地址到页写边界的完整链路2.1 器件地址与 I2C 帧格式AT24C64 的 7 位器件地址是1010加上 A2/A1/A0 三个引脚电平实际发送时左移一位再或上读写位。驱动文件里通常用一个宏或者结构体来保存这个地址方便同一套代码挂多颗 EEPROM。下面这段是典型的地址定义和字节写函数开头/* AT24C64 7位地址1010 A2A1A0这里假设三个引脚都接地 */ #define AT24C64_ADDR_BASE 0x50 #define AT24C64_ADDR_WRITE (AT24C64_ADDR_BASE 1) #define AT24C64_ADDR_READ ((AT24C64_ADDR_BASE 1) | 0x01) /* 页大小 32 字节写操作不能跨页 */ #define AT24C64_PAGE_SIZE 32 #define AT24C64_TOTAL_SIZE 8192 /* 写一个字节到指定地址addr 为 16 位内部地址 */ int at24c64_write_byte(uint16_t addr, uint8_t data) { uint8_t buf[3]; buf[0] (uint8_t)(addr 8); /* 地址高字节 */ buf[1] (uint8_t)(addr 0xFF); /* 地址低字节 */ buf[2] data; return i2c_master_write(AT24C64_ADDR_WRITE, buf, 3); }这里的关键参数是addrAT24C64 是 8KB 容量需要 13 位地址线所以用两个字节传输高字节只有低 5 位有效。i2c_master_write是平台相关的底层发送函数返回 0 表示成功。很多新手会直接把 7 位地址当参数传进去结果 I2C 从机根本不响应示波器上看就是地址阶段 NACK——这是最常见的翻车点之一。2.2 页写与跨页处理AT24C64 支持 32 字节页写但前提是这 32 字节必须落在同一个页内。页的边界是地址低 5 位为 0 的地方比如 0x0000、0x0020、0x0040。如果你从 0x001F 开始写 10 个字节写到第 2 个字节时地址会回卷到 0x0000把前面的数据覆盖掉。驱动文件里必须做跨页拆分下面是一个常见的处理方式/* 写任意长度数据自动处理跨页 */ int at24c64_write(uint16_t addr, const uint8_t *data, uint16_t len) { uint16_t written 0; while (written len) { /* 计算当前页剩余空间 */ uint16_t page_remain AT24C64_PAGE_SIZE - (addr % AT24C64_PAGE_SIZE); uint16_t chunk (len - written) page_remain ? (len - written) : page_remain; if (at24c64_write_page(addr, data written, chunk) ! 0) return -1; /* 等待写周期完成典型 5ms这里用 ACK 轮询更稳 */ if (at24c64_wait_ready() ! 0) return -1; addr chunk; written chunk; } return 0; }page_remain是当前地址到本页末尾的字节数chunk取剩余长度和页剩余空间的较小值。每次写完一页必须等写周期结束AT24C64 的典型写周期是 5ms手册标最大 5ms但实际用 ACK 轮询比死等延时更可靠。at24c64_wait_ready就是反复发起写地址帧直到器件返回 ACK说明内部写完成。2.3 读操作与随机读时序读操作分当前地址读、随机读和顺序读。驱动文件里最常用的是随机读先写目标地址伪写再发起读操作。下面这段是随机读的实现/* 随机读先写地址再读数据 */ int at24c64_read(uint16_t addr, uint8_t *buf, uint16_t len) { uint8_t addr_buf[2]; addr_buf[0] (uint8_t)(addr 8); addr_buf[1] (uint8_t)(addr 0xFF); /* 伪写只发地址不发数据 */ if (i2c_master_write(AT24C64_ADDR_WRITE, addr_buf, 2) ! 0) return -1; /* 重新发起读 */ return i2c_master_read(AT24C64_ADDR_READ, buf, len); }注意i2c_master_write之后要发 STOP 还是重复起始条件不同平台实现不一样。标准做法是发重复起始条件Repeated Start这样总线不会被其他主机抢走。如果你的底层 I2C 驱动不支持重复起始那就只能发 STOP 再发 START但中间要确保没有其他主机介入。len参数在顺序读时不能超过 8KB 边界否则地址会回卷到 0。2.4 写保护与 WP 引脚处理AT24C64 有一个 WP 引脚高电平时禁止写操作。驱动文件里通常不直接控制这个引脚但会在初始化时检查或者提示用户。如果你发现写操作全部失败读出来还是旧数据先量一下 WP 引脚电平。有些板子把 WP 直接接地那就永远可写有些板子接到 GPIO那驱动里就要加一个at24c64_write_enable()函数去拉低。这个细节在驱动文件里往往以宏或者注释形式存在二次开发时别忽略。3. 把驱动跑起来移植、初始化与读写验证3.1 移植前需要准备的底层接口这份驱动文件本身不包含 I2C 硬件抽象层它依赖几个外部函数i2c_master_write、i2c_master_read、delay_ms或者get_tick。移植到你的平台时第一件事就是把这些函数对接好。以 STM32 HAL 库为例常见做法是包一层/* 平台适配层把 HAL 的 I2C 接口映射到驱动需要的函数 */ int i2c_master_write(uint8_t dev_addr, const uint8_t *buf, uint16_t len) { /* HAL 要求地址是 8 位左对齐这里 dev_addr 已经是 8 位 */ if (HAL_I2C_Master_Transmit(hi2c1, dev_addr, (uint8_t *)buf, len, 100) ! HAL_OK) return -1; return 0; } int i2c_master_read(uint8_t dev_addr, uint8_t *buf, uint16_t len) { if (HAL_I2C_Master_Receive(hi2c1, dev_addr, buf, len, 100) ! HAL_OK) return -1; return 0; }hi2c1是你的 I2C 句柄100是超时时间毫秒。注意 HAL 的HAL_I2C_Master_Transmit第二个参数要求是 8 位地址也就是已经左移过的所以驱动里定义的AT24C64_ADDR_WRITE正好可以直接用。如果你用的是软件模拟 I2C那就要自己实现起始、停止、应答时序但接口签名保持一致驱动文件不用改。3.2 初始化与首字节读写测试移植完底层接口后先做一次最简单的读写测试往地址 0x0000 写一个字节再读回来比较。下面是一个典型的测试流程void at24c64_test(void) { uint8_t wr_data 0xA5; uint8_t rd_data 0; /* 写一个字节 */ if (at24c64_write_byte(0x0000, wr_data) ! 0) { printf(write failed\n); return; } /* 等待写周期完成 */ at24c64_wait_ready(); /* 读回 */ if (at24c64_read(0x0000, rd_data, 1) ! 0) { printf(read failed\n); return; } if (rd_data wr_data) printf(AT24C64 test pass\n); else printf(AT24C64 test fail: got 0x%02X\n, rd_data); }如果这一步就失败先别急着改驱动用逻辑分析仪或者示波器抓 I2C 波形。重点看三个地方起始条件后第一个字节是不是0xA0假设 A2A1A0 接地每个字节后从机有没有拉低 SDA 应答停止条件是否正常。我遇到过因为上拉电阻太大导致上升沿太缓、ACK 采样出错的情况换 4.7k 电阻就好了。3.3 批量读写与边界测试单字节通过后接着测跨页写和整片读写。跨页测试要故意从页边界前几个字节开始写比如从 0x001E 写 10 个字节然后读回验证。整片测试可以写一个递增序列读回比对。下面是一个批量测试的片段/* 跨页写测试从 0x001E 开始写 10 字节 */ uint8_t test_buf[10]; for (int i 0; i 10; i) test_buf[i] i 1; at24c64_write(0x001E, test_buf, 10); at24c64_wait_ready(); uint8_t read_buf[10]; at24c64_read(0x001E, read_buf, 10); /* 比对 */ for (int i 0; i 10; i) { if (read_buf[i] ! test_buf[i]) { printf(mismatch at offset %d\n, i); break; } }如果跨页写失败读回的数据会在页边界处出现错位比如第 2 个字节变成了第 0 个字节的值。这就是典型的页回卷说明驱动里的跨页拆分没生效检查page_remain的计算是不是用了addr % AT24C64_PAGE_SIZE。3.4 写周期等待的两种实现写周期等待有两种常见做法固定延时和 ACK 轮询。固定延时简单但 5ms 是最大值实际可能 2ms 就完成了死等浪费 CPU。ACK 轮询更高效但需要底层 I2C 支持“只发地址不传数据”的操作。下面是一个 ACK 轮询的实现/* ACK 轮询反复发起写地址帧直到从机应答 */ int at24c64_wait_ready(void) { uint32_t timeout 1000; /* 超时计数防止死循环 */ while (timeout--) { if (i2c_master_write(AT24C64_ADDR_WRITE, NULL, 0) 0) return 0; delay_us(100); } return -1; }i2c_master_write传NULL和0表示只发地址帧不发数据。如果从机还在写周期内它不会应答函数返回错误一旦写完成从机会 ACK函数返回成功。timeout是保险丝防止器件掉电或者总线短路导致死循环。如果你的底层 I2C 不支持零长度传输那就只能用delay_ms(5)代替。4. 避坑与排查AT24C64 驱动最常见的 5 个翻车现场4.1 现象写进去读出来全是 0xFF原因WP 引脚被拉高或者器件地址不对或者 I2C 总线上没有从机应答。先量 WP 电平再确认 A2A1A0 引脚的电平和你代码里的地址是否一致。如果地址不对示波器上会看到地址字节后没有 ACK主机直接发 STOP。解决把 WP 接地或者用 GPIO 拉低用扫描程序遍历 0x50 到 0x57 七个地址看哪个能应答。扫描程序很简单就是循环调用i2c_master_write发地址帧返回 0 的就是存在。4.2 现象跨页写数据错乱页边界处数据被覆盖原因驱动没有做跨页拆分或者拆分逻辑里page_remain算错了。AT24C64 的页大小是 32 字节地址低 5 位相同的属于同一页。如果你从 0x001F 开始写第一个字节写到 0x001F第二个字节会回卷到 0x0000。解决在at24c64_write里按页拆分每次写不超过page_remain个字节写完一页等写周期再写下一页。测试时故意从 0x001E、0x001F、0x0020 这几个地址开始写覆盖边界情况。4.3 现象连续写多页时偶尔丢数据原因写周期等待没做或者等待时间不够。AT24C64 的写周期最大 5ms但如果你在写周期内发起下一次写器件不会应答数据就丢了。固定延时 5ms 在大多数情况下够用但高温或者低压时写周期可能变长。解决用 ACK 轮询代替固定延时每次写完一页调用at24c64_wait_ready确认器件就绪后再写下一页。如果底层不支持零长度传输那就把延时加到 6ms 留余量。4.4 现象读出来的数据整体偏移一个字节原因随机读的伪写阶段地址发错了或者读操作前没有正确发送重复起始条件。有些 I2C 底层在i2c_master_write之后会自动发 STOP然后i2c_master_read再发 START这中间如果总线被其他主机占用地址就乱了。解决确认底层 I2C 驱动支持重复起始条件或者在两次操作之间关中断防止总线被抢。如果只能发 STOP 再 START那就在两次操作之间加一个极短的延时确保总线空闲。4.5 现象整片读写时后半部分数据全错原因AT24C64 是 8KB 容量地址范围 0x0000 到 0x1FFF。如果你用uint16_t存地址但循环时超过了 0x1FFF地址会回卷到 0x0000把前面的数据覆盖掉。另外有些驱动里页大小宏定义错了比如把 32 写成 64导致跨页拆分失效。解决检查地址范围确保不超过AT24C64_TOTAL_SIZE - 1。页大小宏必须是 32不是 64。整片测试时打印每次读写的地址看是否在 0x0000 到 0x1FFF 之间。5. 二次开发改造把驱动做成可复用组件的几个技巧5.1 用结构体封装器件实例如果你板子上挂了多颗 AT24C64或者以后要换成 AT24C32、AT24C128最好把器件参数抽成结构体驱动函数接收结构体指针。这样同一套代码可以实例化多个对象也方便替换底层 I2C 接口。下面是一个改造示例typedef struct { uint8_t dev_addr; /* 8位写地址 */ uint16_t page_size; /* 页大小AT24C64 是 32 */ uint16_t total_size; /* 总容量AT24C64 是 8192 */ int (*write)(uint8_t addr, const uint8_t *buf, uint16_t len); int (*read)(uint8_t addr, uint8_t *buf, uint16_t len); } at24cxx_dev_t; /* 初始化实例 */ void at24c64_init(at24cxx_dev_t *dev, uint8_t a2a1a0) { dev-dev_addr (0x50 | (a2a1a0 0x07)) 1; dev-page_size 32; dev-total_size 8192; dev-write i2c_master_write; dev-read i2c_master_read; }a2a1a0是三个引脚的电平组合范围 0 到 7。dev_addr左移一位后就是写地址。write和read函数指针指向底层 I2C 接口换平台时只改这两个指针驱动逻辑不用动。这样改造后AT24C32 只需要把page_size改成 32、total_size改成 4096其他代码复用。5.2 增加写缓存减少擦写次数EEPROM 的擦写寿命是 100 万次虽然听起来很多但如果你在循环里频繁写同一个地址很快就会耗尽。一个实用的技巧是在驱动层加一个写缓存应用层调用at24c64_write时数据先写到 RAM 缓存只有缓存满了或者显式调用at24c64_flush时才真正写 EEPROM。下面是一个简单的缓存结构#define CACHE_SIZE 64 typedef struct { uint16_t addr; uint8_t data[CACHE_SIZE]; uint16_t len; uint8_t dirty; } at24c64_cache_t; /* 带缓存的写先更新缓存必要时刷入 EEPROM */ int at24c64_write_cached(at24c64_cache_t *cache, uint16_t addr, const uint8_t *data, uint16_t len) { /* 如果地址不连续或者缓存已满先刷入 */ if (cache-dirty (addr ! cache-addr cache-len || cache-len len CACHE_SIZE)) { if (at24c64_write(cache-addr, cache-data, cache-len) ! 0) return -1; cache-dirty 0; cache-len 0; } /* 更新缓存 */ if (cache-len 0) cache-addr addr; memcpy(cache-data[cache-len], data, len); cache-len len; cache-dirty 1; return 0; }这个缓存适合保存配置参数的场景参数在 RAM 里频繁修改但只在掉电前或者定时刷入 EEPROM。dirty标志表示缓存里有未写入的数据addr和len记录缓存的起始地址和长度。注意缓存大小不要超过页大小太多否则刷入时跨页拆分次数增加。5.3 用校验和保证数据完整性EEPROM 数据在掉电瞬间可能写了一半导致读出来是坏数据。一个简单的做法是在数据末尾加一个 CRC 或者校验和。写的时候先算校验和读的时候再算一遍比对。下面是一个校验和示例/* 计算校验和所有字节累加取反 */ uint8_t checksum(const uint8_t *data, uint16_t len) { uint8_t sum 0; for (uint16_t i 0; i len; i) sum data[i]; return ~sum; } /* 写带校验的数据 */ int at24c64_write_with_crc(uint16_t addr, const uint8_t *data, uint16_t len) { uint8_t buf[len 1]; memcpy(buf, data, len); buf[len] checksum(data, len); return at24c64_write(addr, buf, len 1); } /* 读并校验 */ int at24c64_read_with_crc(uint16_t addr, uint8_t *data, uint16_t len) { uint8_t buf[len 1]; if (at24c64_read(addr, buf, len 1) ! 0) return -1; if (buf[len] ! checksum(buf, len)) return -2; /* 校验失败 */ memcpy(data, buf, len); return 0; }checksum函数把所有字节累加后取反实现简单检错能力一般但够用。at24c64_write_with_crc在数据末尾追加一个校验字节at24c64_read_with_crc读出来先校验再拷贝。如果返回 -2说明数据损坏应用层可以决定是重新初始化还是报错。5.4 移植到不同 I2C 外设的注意事项不同 MCU 的 I2C 外设行为有差异。比如有些硬件 I2C 在发送地址后会自动处理 ACK有些需要手动读写 ACK 位有些支持重复起始条件有些必须发 STOP。移植时重点确认三件事地址是 7 位还是 8 位、是否支持重复起始、超时机制怎么实现。我一般会在驱动文件顶部写一个#if defined(STM32F1)之类的条件编译把不同平台的底层接口包在同一个函数名下这样上层驱动完全不用改。从那以后我每次移植 I2C 驱动都强制先跑一遍地址扫描和单字节读写确认底层通了再上批量操作。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →