拓冰建站拓冰建站
首页 / 资讯中心 / 正文

24LC512 EEPROM读写例程:I2C页写、写周期等待与避坑指南

简介基于I²C总线的24LC512 EEPROM程序示例面向嵌入式开发者和单片机学习者代码结构简洁、注释详细可直接加入项目使用。示例完整演示了从I²C接口初始化、从机地址匹配到字节随机读写、地址指针递增、写周期等待及错误检查的全过程并针对EEPROM写入耗时较长的特点加入延时处理适合智能仪表、数据记录仪等需要非易失性存储的场合亦可用于设备参数保存、系统配置存储等应用。压缩包共13个文件除C语言源程序主程序与模块实现和头文件外还包含IAR工程配置.eww/.ewp、调试描述.ewd以及编译调试生成的中间文件可直接打开工程进行仿真验证整体仅24KB。已有1036人学习下载。通过这套代码开发者能快速掌握I²C时序和24LC512512K位/64KB的分页读写机制并能方便地移植到其他微控制器平台减少底层驱动开发工作量。资源虽小但驱动逻辑完整对初学者和实际项目都有参考价值。 做嵌入式项目这么多年我有个很强烈的体会凡是要存标定参数、设备序列号、历史告警记录、用户配置的项目方案兜兜转转最后都会落到外挂EEPROM上而24LC512就是这类“存得多、用得省心”的代名词。最近社区里好几个朋友都在伸手要这颗芯片的例程诉求出奇一致——“别给我讲一堆理论代码要能直接跑”。我上一款工业传感器仪表正好用的24LC512做数据记录驱动器层代码到现在还在用所以这篇直接把能复制的完整读写程序、页写边界处理、写周期等待逻辑全部整理出来。不管你是第一次用外部EEPROM还是已经踩过坑回来搜例程下面这套东西都能直接搬进工程。1. 为什么说24LC512是嵌入式存储里最省心的选择很多初学者不理解单片机内部不是有Flash吗为什么还要外挂一颗EEPROM答案是使用方式和寿命逻辑完全不同。内部Flash通常按扇区擦除、按字或半字写入写之前必须先擦而擦写次数大多在1万到10万次这个量级。你用内部Flash去保存仪表每秒钟刷新一次的运行参数一个月下来磨损就接近警戒线。EEPROM则天生支持按字节读写不需要先擦除Microchip标称的擦写寿命是100万次数据保持时间在25摄氏度环境下是200年以上。这颗芯片定位就是“频繁小量保存关键数据”和内部Flash构成互补关系。容量上24LC512是512Kbit换算下来正好64KB也就是65536个字节。这个容量在外部EEPROM里属于中大型放配置表、字库、标定曲线、故障记录都够用还能把多份固件参数分区存放。1.1 三种存储方案的真实差异选型时经常有人拿RAM、内部Flash和24LC512对比实际使用差别很大RAM读写最快但掉电数据全没只能做运行期缓存。内部Flash免多加芯片可擦写寿命短而且擦除粒度大频繁小规模更新太浪费。24LC512等I2C EEPROM按字节写、掉电不丢、寿命长缺点是写入速度受I2C总线频率限制但参数保存这种低频操作根本无所谓。实际项目里我通常把两者结合运行时的临时数据放RAM设备掉电前要把关键状态写进24LC512。频繁更新的磨损计数器、标定参数也放24LC512心里踏实。1.2 选型前必须看懂的芯片参数24LC512有一组参数直接影响代码设计和稳定性选型时先确认清楚参数数值说明容量512Kbit / 64KB / 65536字节地址需要2字节I2C地址1010 A2 A1 A0A0-A2引脚决定最多级联8片页写大小128字节页内连续写跨页地址会回卷写周期时间典型2ms最大5ms写完必须等待否则下一个命令被忽略电压范围1.8V-5.5V24LC512后缀对应工业级电压适应宽最大总线频率400kHz常规/ 1MHz5V时不要当漏极开路死等这里最需要警觉的是“页写回卷”和“写周期等待”这两个点都是实测中最容易翻车的地方后面专门细说。芯片封装常见的是DIP-8、SOIC-8、TSSOP-8我习惯选SOIC-8回流焊方便调试时用DIP转接板也容易飞线。2. 先弄懂I2C帧格式24LC512的地址和普通24C02不一样I2C总线本身不难难在器件手册里的时序细节。24LC512和常见的24C02最大的区别就在于它需要两字节地址。别小看这个差异不少人拿24C02的例程直接改结果写进去的数据总是错位、读出全是FF卡了好几天找不到原因。2.1 设备地址字节与片选24LC512的I2C从机地址格式是固定的高4位固定1010后3位A2、A1、A0引脚电平最低位读/写标志0表示写1表示读以A0A1A2全部接地为例完整设备地址就是0b1010000。写操作用0xA0读操作用0xA1。如果A0、A1、A2引脚接了不同的高电平设备地址会跟着变所以同一个I2C总线上最多能挂8片24LC512每片的总容量就是64KB乘以8。很多人在电路上把A0-A2悬空这是不可靠的内部没有默认下拉必须明确接GND或VCC否则静电或噪声会让地址漂掉导致总线丢失设备。2.2 两字节地址新手最大分水岭24C02容量256字节地址用一个字节就够了发完设备地址再发一个地址字节就能操作。24LC512有65536字节地址必须是16位顺序是先高字节、再低字节。举个例子想访问地址0x1234I2C帧里要依次发送0x12、0x34。这听起来简单实际容易踩坑的有两处。第一地址变量必须用16位无符号类型如果用8位访问地址超过255后高字节被截断数据全写到低256字节里。第二写代码时地址高低字节顺序不能搞反先发高字节再发低字节一旦反过来芯片会把“高地址”当作要操作的页读写区域完全错乱。我见过最典型的错误是把24LC512当24C02用发完设备地址后只发一个字节地址然后开始写数据。芯片本身不会报错它会认为第二字节也就是数据是16位地址的低字节然后数据全被吞进地址里之后发的内容又变成了真正要写的数据结果整片数据排列全部错位。调试这类问题最好的办法就是拿逻辑分析仪抓I2C时序一眼就能看出帧格式对不对。2.3 写周期与ACK Polling任何写操作单字节写或页写完成后24LC512会进入一段内部写周期把数据真正写入存储阵列。这个期间芯片不响应任何I2C命令典型时长2ms到5ms。代码上处理等待有两条路固定延时每次都等5ms以上简单粗暴但写大量数据时效率很低用ACK Polling这是官方推荐的做法。ACK Polling的原理是每次写操作结束后芯片在内部写周期中不会拉低SDA线来回应ACK。你只要不断发送“START 设备写地址”直到芯片回一个ACK就说明内部写周期结束可以继续下一条命令。这个方式既不会等太久也不会出现时序竞争。后续代码里我会把两个方案都写出来。3. 可直接抄走的读写代码从单字节到页写完整实现下面这套代码是我在STM32的软I2C环境下实测通过的不依赖特定厂商的HAL库底层只要求提供SDA/SCL两个GPIO引脚的拉高、拉低和读取操作移植到任何单片机平台都很顺利。为了方便理解先给底层驱动再给EEPROM操作函数最后给一个完整验证例程。3.1 先搭底层I2C接口这里用GPIO模拟I2C好处是非常直观也方便在裸机工程里直接调试。SCL和SDA引脚需要外接上拉电阻常规4.7k总线频率高就换成2.2k甚至1k。引脚宏根据你自己的工程修改即可。#include stm32g0xx_hal.h // 按实际MCU头文件修改 #include string.h #define IIC_SCL_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET) #define IIC_SCL_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET) #define IIC_SDA_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET) #define IIC_SDA_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET) #define IIC_SDA_READ() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) #define EEPROM_ADDR_W 0xA0 #define EEPROM_ADDR_R 0xA1 #define EEPROM_PAGE_SIZE 128 static void iic_delay_us(void) { // 普通循环延时频率约400kHz时每次延时约1-2us for (volatile int i 0; i 8; i) ; } static void iic_start(void) { IIC_SDA_H(); IIC_SCL_H(); iic_delay_us(); IIC_SDA_L(); iic_delay_us(); IIC_SCL_L(); iic_delay_us(); } static void iic_stop(void) { IIC_SDA_L(); IIC_SCL_H(); iic_delay_us(); IIC_SDA_H(); iic_delay_us(); } static uint8_t iic_write_byte(uint8_t byte) { for (int i 7; i 0; i--) { if (byte (1 i)) IIC_SDA_H(); else IIC_SDA_L(); IIC_SCL_H(); iic_delay_us(); IIC_SCL_L(); iic_delay_us(); } // 释放SDA等待从机ACK IIC_SDA_H(); iic_delay_us(); IIC_SCL_H(); iic_delay_us(); uint8_t ack (IIC_SDA_READ() 0) ? 0 : 1; // 0表示ACK IIC_SCL_L(); iic_delay_us(); return ack; } static uint8_t iic_read_byte(uint8_t ack) { uint8_t data 0; IIC_SDA_H(); for (int i 7; i 0; i--) { IIC_SCL_H(); iic_delay_us(); if (IIC_SDA_READ()) data | (1 i); IIC_SCL_L(); iic_delay_us(); } // 主机发送ACK(0)继续读发送NACK(1)表示最后一字节 if (ack) IIC_SDA_H(); // NACK else IIC_SDA_L(); // ACK iic_delay_us(); IIC_SCL_H(); iic_delay_us(); IIC_SCL_L(); IIC_SDA_H(); iic_delay_us(); return data; }底层这段逻辑是所有I2C器件操作的基础虽然每个平台寄存器不同但时序骨架完全一致。移植时唯一要确认的是延时是否符合所选总线的速度如果出现SDA边沿过缓优先检查上拉电阻而不是盲改延时。3.2 核心读写函数单字节、页写、顺序读在底层驱动之上我封装了下面这组函数。单字节写每次读写一个字节适合参数量小的场景页写一次能写最多128字节顺序读是连续读多个字节适合导出日志或加载配置表。void EEPROM_WaitReady(void) { uint16_t timeout 10000; while (timeout--) { iic_start(); if (iic_write_byte(EEPROM_ADDR_W) 0) { iic_stop(); return; } iic_stop(); } } void EEPROM_WriteByte(uint16_t addr, uint8_t data) { iic_start(); iic_write_byte(EEPROM_ADDR_W); iic_write_byte((uint8_t)(addr 8)); // 高字节地址 iic_write_byte((uint8_t)(addr 0xFF)); // 低字节地址 iic_write_byte(data); iic_stop(); EEPROM_WaitReady(); // 等待本次写周期完成 } void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint16_t len) { if (len 0) return; // 防止跨页一次最多写到页边界 uint16_t page_left EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE); if (len page_left) len page_left; iic_start(); iic_write_byte(EEPROM_ADDR_W); iic_write_byte((uint8_t)(addr 8)); iic_write_byte((uint8_t)(addr 0xFF)); for (uint16_t i 0; i len; i) { iic_write_byte(data[i]); } iic_stop(); EEPROM_WaitReady(); } void EEPROM_WriteBytes(uint16_t addr, uint8_t *data, uint16_t len) { while (len 0) { uint16_t page_left EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE); uint16_t chunk (len page_left) ? len : page_left; EEPROM_WritePage(addr, data, chunk); addr chunk; data chunk; len - chunk; } } uint8_t EEPROM_ReadByte(uint16_t addr) { uint8_t data 0; iic_start(); iic_write_byte(EEPROM_ADDR_W); iic_write_byte((uint8_t)(addr 8)); iic_write_byte((uint8_t)(addr 0xFF)); // 重复START转为读操作 iic_start(); iic_write_byte(EEPROM_ADDR_R); data iic_read_byte(1); // 最后一字节回NACK iic_stop(); return data; } void EEPROM_ReadBytes(uint16_t addr, uint8_t *out, uint16_t len) { if (len 0) return; iic_start(); iic_write_byte(EEPROM_ADDR_W); iic_write_byte((uint8_t)(addr 8)); iic_write_byte((uint8_t)(addr 0xFF)); iic_start(); iic_write_byte(EEPROM_ADDR_R); for (uint16_t i 0; i len; i) { uint8_t ack (i len - 1) ? 1 : 0; // 最后一字节发NACK out[i] iic_read_byte(ack); } iic_stop(); }关键点再说一遍页写内部计数器在页边界会回卷所以EEPROM_WriteBytes才要做分片处理每写一片最多到128字节边界这能彻底避免跨页写入导致的数据乱序。另外EEPROM_WriteByte和EEPROM_WritePage都会等写周期真实项目里不要省掉这一步。3.3 一个完整的验证例程下面这段例程完成一次完整的“写入-读回-比对”可以作为上电自检或者产线测试的模板。测试通过后基本说明硬件和驱动都是正常的。void EEPROM_SelfTest(void) { uint8_t write_buf[256]; uint8_t read_buf[256]; for (int i 0; i 256; i) write_buf[i] (uint8_t)i; // 从地址0x100开始写入256字节 EEPROM_WriteBytes(0x0100, write_buf, 256); memset(read_buf, 0x00, sizeof(read_buf)); EEPROM_ReadBytes(0x0100, read_buf, 256); uint8_t pass 1; for (int i 0; i 256; i) { if (read_buf[i] ! write_buf[i]) { pass 0; break; } } if (pass) { // 通过数据一致 } else { // 失败检查硬件连接和I2C上拉 } }这个自测试写入的数据是0x00到0xFF回读比对能覆盖全地址位和全数据位。如果失败优先查地址帧、A0-A2接线和上拉电阻大概率就是这三样。4. 实测踩坑记录写不进去、读出FF、死等ACK的根源代码是一回事到了真实电路板上又是另一回事。这一章记录的都是我实际调试中踩过的坑每一个都有对应的现象和定位过程。能帮你省下几天的排查时间。4.1 写周期没有等待页写后立刻读就翻车现象单字节写偶发正常连续写多个字节再读回中间某些字节是0xFF。原因上一次写还没结束芯片正处于内部写周期新的I2C命令发过去芯片根本不理会主机却认为已经收到ACK实际上是用超时或误读代替后续数据自然丢了。这个坑在固定延时方案中特别容易踩到因为延时不足或系统中断打断了延时时序就错位了。解决所有写命令后面都调用EEPROM_WaitReady()。如果用了MCU自带的I2C硬件外设也要注意必须在发送停止条件后检查总线忙状态再发下一次写命令不能看到发送完成中断就立刻关外设。4.2 页写跨128字节边界数据被写飞现象从地址250开始写10字节结果250-255地址的内容正确但后面几个字节出现在128附近的页首完全不符合预期。原因24LC512页写入时内部地址计数器在页尾回卷到页首不会自动进位到下一页。跨页写入时后续字节会覆盖本页开头区域。解决要么在设计时保证所有写入都从页边界对齐比如按128字节分区要么像我的EEPROM_WriteBytes那样在驱动层自动分片。我在多个项目里都用分片方案业务层不用管地址边界省心很多。4.3 随机读少了一个重复START现象读操作返回的数据是上次地址的值无论怎么改地址都读不对。原因随机读的时序不能停在写地址之后直接发读位。正确流程是START 设备写地址 地址高字节 地址低字节然后重新发START 设备读地址再读数据。缺少重复START芯片会认为还是在执行写操作的后续地址指针不会切换到读模式。用stop再start理论上也能凑效但重复START更规范兼容性也更好。解决严格按照EEPROM_ReadByte的实现来这个例程是从手册时序直接抄下来的没必要自己diy。4.4 上拉电阻与总线电平的隐性麻烦现象短距离调试一切正常换上稍长的排线后偶发写失败、数据校验错误。原因I2C是开漏总线SDA/SCL必须通过上拉电阻拉高。上拉电阻太大比如10k且线缆电容大时上升沿过慢主设备和从设备对电平采样不一致上拉电阻太小比如100欧则电流过大倒灌到器件引脚可能损坏器件也可能导致总线无法拉低。解决400kHz总线下推荐4.7k到2.2k之间线较长或者多设备并联时用1k到2.2k。3.3V系统和5V系统选型不同最好用示波器看边沿要求上升沿时间不超过1us。另外如果EEPROM用的是5V供电而MCU是3.3V需要确认SDA/SCL引脚是否容忍5V否则要加电平转换。4.5 复位状态把总线拉死现象单片机程序跑飞或人工复位后I2C总线SDA一直为低24LC512不再响应任何操作。原因如果主设备在传输中途复位此时SDA可能正处于低电平状态EEPROM则认为传输还在进行在等待SCL的后续时钟。复位后主设备重新初始化I2C但总线状态并不干净双方时序对不上。解决在I2C初始化前把SCL和SDA都配置为输出高电平然后手动翻转SCL九次以上这样可以让从设备从错误状态中恢复。更保险的做法是在EEPROM驱动初始化函数里写一个bus_recover函数void EEPROM_BusRecover(void) { // 模拟9个时钟周期让从机退出异常状态 IIC_SCL_H(); IIC_SDA_H(); for (int i 0; i 9; i) { IIC_SCL_L(); iic_delay_us(); IIC_SCL_H(); iic_delay_us(); } iic_start(); iic_stop(); }初始化时先调用这个能解决大部分“上电后第一笔读写失败”的怪问题。这个函数在量产设备里尤其有用因为工业现场的电源毛刺和复位更频繁。老生常谈但必须强调外部EEPROM虽然寿命高达100万次也不是无限次。关键参数写入前最好做数据有效性校验比如在参数区头部写一个魔数每次上电先读魔数判断是否合法。如果某个区域反复高频写建议做磨损均衡把写入地址轮换到不同页不要永远怼着同一个地址写。这是个细心活但做习惯了产品可靠性会明显上一截。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门