资讯详情

用N76E003模拟AT24C02:I2C从机状态机与掉电数据保存

📅 2026/10/16 4:42:22 | 华诺云谱 👁 阅读
用N76E003模拟AT24C02:I2C从机状态机与掉电数据保存
简介一套基于N76E003单片机的I2C从机模拟AT24C02程序代码已通过实际测试适合嵌入式开发者、电子竞赛学生以及需要低成本非易失数据存储方案的项目参考。程序覆盖I2C引脚与时钟初始化、0xA0从机地址匹配、读写命令解析、256字节存储区模拟、ACK应答时序及常见错误处理可直接移植或用于学习协议细节。包内共16个文件包含5个H头文件、3个C源文件以及启动文件、HEX烧录文件、Keil工程文件等整体仅56KB结构紧凑目录划分清晰便于按模块阅读。目前已有1441人学习下载说明该方案在I2C从机与EEPROM模拟实践中具有一定参考价值。借助源码可以掌握SCL/SDA时序控制、主从应答机制、存储映射方法以及模拟外设的调试思路为后续类似外设模拟开发提供可复用的模板也能对照真实AT24C02芯片数据手册排查I2C通信中的读写异常。1. 模拟AT24C02的本质把N76E003变成一颗“不丢数据”的EEPROM做某个跨平台系统的显示面板时主板上本来有一颗AT24C02负责存亮度、音量、校准参数结果量产阶段EEPROM缺货临时换料又得改硬件改驱动。我改用N76E003单片机自带的硬件I2C从机模式让它直接“扮演”AT24C02主机端一行驱动不用改扫描地址还是0x50读写的存储空间还是256字节实测跑通后整条产线的备料问题直接消失。这篇文章就把这套已测试的模拟方案拆开讲I2C从机地址怎么对齐、内置外设怎么初始化、状态机怎么写最后整理几个我实测中踩过的坑。适合正在做I2C从机通信、或者想省掉一颗EEPROM的开发者。2. 先盯时序再写代码AT24C02的寄存器模型与读写协议2.1 从机地址到底是谁在决定0xA0/0xA1与N76E003的地址匹配逻辑AT24C02是2Kbit串行EEPROM内部按256字节组织I2C的7位从机地址是0x50加方向位之后写地址就是0xA0、读地址是0xA1。A2、A1、A0三个引脚决定7位地址的低3位贴片封装里基本都接GND所以地址固定成0x50。这里有个特别容易翻车的地方N76E003的I2CADDR寄存器保存的是“7位地址左移后”的值。也就是说主机在总线上发0xA08位写地址你要让I2CADDR等于0xA0而不是0x50。寄存器bit0是广播呼叫总使能位bit7:1才是真正参与地址比对的部分硬件拿这7位去和总线上地址字节的高7位匹配。我第一次把这个模拟程序烧进板子时I2CADDR填的是0x50结果逻辑分析仪上能清楚看到主机发0xA0从机却一直不回应总线挂着不动最后看门狗复位。后来把这一位想明白发现一点都不玄学I2CADDR 7位从机地址 1要启用广播呼叫时再把bit0补上1。这个错误之所以隐蔽是很多人拿AT24C02数据手册里的“Device Address”直接填进寄存器忽略了两者的位宽定义差异。下面是A2/A1/A0与地址字节的对应关系烧录前可以自己对一遍。A2A1A07位从机地址8位写首字节8位读首字节0000x500xA00xA10010x510xA20xA30100x520xA40xA50110x530xA60xA71000x540xA80xA9如果你用软件模拟I2C从机地址匹配完全靠中断里手动比较那个7位对齐的规则反而看得清楚。N76E003的硬件I2C把地址匹配做进外设里省事的同时也把问题藏起来了配置错了只能靠示波器或逻辑分析仪才能发现。我现在的习惯是每次新板子先写一个最小的从机初始化程序主机侧不停发地址扫描从机这边把收到0xA0的LED点亮确认物理链路和地址匹配都正常再往上叠状态机。2.2 三种读操作的差异当前地址读、随机读、顺序读AT24C02的读操作分三种当前地址读、随机读、顺序读。当前地址读最简单芯片内部维护一个地址指针上电后默认是0x00写完数据后指针会停在“最后访问地址1”的位置主机直接发SLAR就能从这个位置读出数据。随机读要分两步先向从机写入目标字地址然后发重复起始REPEATED START方向位从写切到读再连续拉数据。顺序读本质上是“读操作重复执行”每读一个字节主机回ACK从机内部地址自动加1地址到0xFF之后回卷到0x00。模拟EEPROM时最容易露馅的就是随机读和顺序读的衔接。很多人把这两个功能分开写结果漏了对重复起始状态的判断状态机卡在“等写地址”那一帧总线直接锁死。我一般把I2C从机状态机拆成四类状态空闲、等字地址、收数据、发数据。收到SLAW之后进入“等字地址”等字地址状态下再收到数据字节就把这个字节当字地址存起来之后收到的字节才真正写入存储区。收到SLAR则进入“发数据”地址沿用刚才保存的字地址每发一字节加1加满256自动回卷。这样不管主机用哪种读方式状态机都只有一条路径不会因为读模式不同而写出多套分支。还要注意AT24C02的页写行为。真实芯片的页大小是8字节页内写地址会自动回卷不会跨页。比如写了0x03这个地址连续写5字节实际写到0x03、0x04、0x05、0x06、0x07再往下就回卷到0x00把页首覆盖掉。模拟时有两种路线严格复现这个回卷或者把整块256字节当成一个“大页”处理。我倾向于后者因为上位机驱动通常都是按固定偏移写极少依赖页回卷特性如果哪天遇到较真的驱动再补一段页边界判断也不难。2.3 存储映射的选择RAM数组、片内Flash还是两级方案模拟AT24C02数据到底放哪是个要提前决定的问题。最省事的办法是定义一块256字节的RAM数组I2C读写直接命中数组速度最快、逻辑最简单但掉电数据全丢。很多应用场景其实只要求“运行期间保存配置”主机每次上电都会主动把参数下发一遍这种就完全够用。要掉电保存就得用N76E003的片内Flash。这颗芯片支持IAP自编程可以拿一部分空间模拟数据存储。但这里有个代价Flash擦写要时间而且擦写期间CPU会停止取指令如果在I2C中断里做Flash操作SCL被从机拉低的时间会超过主机允许的等待上限轻则超时重则整个I2C总线卡死。所以我不建议直接在中断里擦Flash更稳的做法是两级方案RAM做主缓冲区所有I2C读写都落在RAM上中断立刻返回主循环里检测“脏页标记”有空时把改动过的数据页搬进Flash。这样既保证I2C响应速度又有掉电保存能力。代码里先定义好这两块区域#define EEPROM_SIZE 256 #define EEPROM_PAGE 8 uint8_t eeprom_buf[EEPROM_SIZE]; uint8_t eeprom_dirty[EEPROM_SIZE / EEPROM_PAGE];EEPROM_SIZE固定为256对应AT24C02的256字节空间不要随便改除非你同时改主机端驱动里的长度定义。EEPROM_PAGE取8是沿用AT24C02页写的大小每个字节对应RAM里8字节数据脏标记置1表示这一页数据在RAM里被改过、还没同步进Flash。这里EEPROM_PAGE的取值不影响I2C实时读写只影响后续Flash搬运的粒度取值越小搬运越频繁、单次耗时越短取值越大搬运次数少但单次更久。我一般取8和真实EEPROM页对齐出问题也好对照硬件行为。3. N76E003内置I2C从机初始化引脚配置与中断入口3.1 引脚复用P1.4/SCL、P1.5/SDA的开漏与上拉配置N76E003的硬件I2C外设固定挂在P1.4SCL和P1.5SDA上不需要额外切换引脚功能。这点比用普通IO模拟I2C要省心得多起始、停止、应答都是硬件检测我们只需要在中断里处理数据。第一步是把这两个引脚配成开漏输出同时在板子上接上拉电阻。电阻阻值的选择和I2C总线频率有关100kHz标准模式用4.7kΩ没问题400kHz快速模式建议降到2.2kΩ左右具体还要看总线上挂了多少设备、走线有多长。端口模式是通过N76E003的P1M1/P1M2寄存器配置的把bit4和bit5同时置1就切到开漏输出// P1.4 - SCL, P1.5 - SDA开漏输出 P1M1 | 0x30; P1M2 | 0x30;P1M1和P1M2对应位都为1时端口是开漏模式。开漏的好处很直接输出高电平时只靠外部上拉电阻拉高这是I2C协议的基本要求也避免两个设备同时驱动SDA时出现一个拉低一个拉高的冲突。如果板子已经焊了上拉电阻用准双向模式也能跑但开漏是最规范的选择。引脚功能上还要注意一点N76E003的引脚复用比一般8051多一些P1.4/P1.5还可能是其它外设的通道如果工程里同时开了UART或定时器捕捉功能要检查是否把引脚抢走了。我习惯在初始化早期就把I2C引脚模式设好并且把用不到的外设中断都关掉。3.2 I2C外设寄存器初始化I2CON、I2CADDR与中断开关从机模式的初始化代码很简短但每一行的含义要清楚#define I2C_SLAVE_ADDR_7BIT 0x50 void i2c_slave_init(void) { // 7位地址0x50左移一位bit0是广播呼叫控制不启用 I2CADDR (I2C_SLAVE_ADDR_7BIT 1); // 先把控制寄存器清零再逐位置位 I2CON 0x00; // bit6 I2CEN使能I2C外设 I2CON | 0x40; // bit2 AA应答位使能地址匹配后自动回ACK I2CON | 0x04; // bit3 SI串行中断标志清掉可能残留的旧值 I2CON 0xF7; // 打开I2C中断与全局中断 EI2C 1; EA 1; }这段代码的逻辑是I2CADDR装的是7位地址左移后的值我直接传入0x50让编译器左移避免手算出错。接着把I2CON清零保证复位后的未知状态不影响外设。然后置位I2CEN和AA前者让I2C模块开始工作后者让从机在地址匹配后自动回应ACK如果AA是0从机对自身地址不回应总线上相当于没有这个设备。之后清一次SI标志是为了把上电过程中可能因为引脚抖动产生的挂起中断清掉。最后打开EI2C和EA两个中断开关缺一不可只开EI2C不开EAI2C中断永远不会进入主机端表现为ACK之后无反应。有个参数可以按项目需要调整I2CADDR的低位bit0是广播呼叫使能位默认0表示不响应广播地址0x00。如果主机那边有设备用广播呼叫同步时间你就要把I2CADDR的bit0置1让从机也参与广播。这里不置1是正常的因为AT24C02本身也不响应广播呼叫严格模拟就不该开。3.3 从机中断入口与状态变量不是所有中断都要处理N76E003的I2C中断向量在编译环境里对应interrupt 7。中断服务函数进入后第一步要读I2CSTATUS状态寄存器硬件已经把当前所在状态分类好了我们只需按状态码分支处理。这里列一张常用的从机状态码表写代码时对照着看状态码含义下一步动作0x60收到自身地址写方向SLAW已回ACK准备接收字地址0x80收到数据字节已回ACK保存数据或作为字地址保存0xA0收到STOP条件或重复起始复位本次会话状态0xA8收到自身地址读方向SLAR已回ACK准备发送数据0xB8数据字节已发送主机回ACK地址加1继续发下一字节0xC0数据字节已发送主机回NACK结束本次读操作中断服务函数的基本骨架如下void I2C_IRQ(void) interrupt 7 { uint8_t sta I2CSTATUS; // 读取状态后立刻清SI避免重复触发 I2CON 0xF7; switch (sta) { case 0x60: // SLAW进入等待字地址状态 i2c_state I2C_STATE_WAIT_ADDR; break; // 其余状态在下一章补全 default: break; } }这里有个细节要提醒SI标志是I2C串行中断标志进入中断后把它清掉是常规操作但清得太早会有风险。如果从机内部还没准备好下一拍数据新到的字节就会把中断标志再次置位状态机可能读到旧状态。所以更稳的顺序是“先读I2CSTATUS再清SI最后根据状态码准备数据或应答位”。这套顺序在下一章完整状态机里会反复用到。另外不要在I2C中断服务函数里加延时函数N76E003主频16MHz一条delay语句就可能让SCL低电平保持超过主机容忍值总线就容易出问题。4. 模拟读写状态机从地址匹配到页写入的完整实现4.1 状态变量与会话流程把AT24C02的“字地址”当成内部指针状态机实现前先定义好状态常量和两个关键变量一个保存当前状态一个保存字地址#define I2C_STATE_IDLE 0 #define I2C_STATE_WAIT_ADDR 1 #define I2C_STATE_WAIT_DATA 2 #define I2C_STATE_READ 3 static uint8_t i2c_state I2C_STATE_IDLE; static uint8_t mem_addr 0x00;i2c_state表示当前会话处于哪个阶段mem_addr就是AT24C02内部的字地址指针。用uint8_t定义mem_addr是有意为之当地址从0xFF加1时uint8_t自动回卷成0x00正好符合EEPROM顺序读的地址回卷行为。如果这里用了uint16_t反而要额外写一段判断属于给自己找麻烦。整个会话流程是这样走的主机发起起始发送SLAWN76E003地址匹配后进0x60中断状态切到等待字地址。主机再发一个字地址字节进0x80中断此时我们不知道这个字节是“字地址”还是“数据”只能靠状态区分。收到字地址后状态切到等待数据。往后每次0x80中断都是真正的数据写入直接写进eeprom_buf。当主机发出STOP或重复起始进0xA0中断本次会话结束状态复位到IDLE。4.2 写流程字地址与数据字节怎么区分写方向最关键的代码就是0x80状态里那段分支很多人写错都是因为没区分“等字地址”和“等数据”两个阶段。完整的中断处理如下void I2C_IRQ(void) interrupt 7 { uint8_t sta I2CSTATUS; I2CON 0xF7; switch (sta) { case 0x60: // 收到SLAW主机要开始写了 i2c_state I2C_STATE_WAIT_ADDR; I2CON | 0x04; // 保持AA继续应答数据 break; case 0x80: // 收到数据字节 if (i2c_state I2C_STATE_WAIT_ADDR) { // 这一字节是字地址决定后续写入的位置 mem_addr I2CDAT; i2c_state I2C_STATE_WAIT_DATA; } else if (i2c_state I2C_STATE_WAIT_DATA) { // 这一字节是真正的数据写入RAM缓冲 eeprom_buf[mem_addr] I2CDAT; eeprom_dirty[mem_addr 3] 1; mem_addr; } I2CON | 0x04; // 继续ACK等下一个字节 break; case 0xA0: // 收到STOP或重复起始复位会话 i2c_state I2C_STATE_IDLE; break; // 读方向的case在下一节补上 } }逻辑是这样第一次进入0x80时状态还是WAIT_ADDR数据当成字地址保存下一次再进0x80状态已经切到WAIT_DATA数据直接写入缓冲。这里没有判断数据长度因为在RAM模式里可以连续写任意字节mem_addr从0开始写255字节再继续写就会自动回卷到0行为和真实AT24C02连续跨页写基本一致。写入的同时置位脏标记mem_addr 3就是把地址除以8定位到对应的页标记位。如果你不需要Flash掉电保存这一行可以删掉RAM方案里它只是给主循环搬运Flash用的。还有一个小地方要处理写方向时主机会在最后一字节后发STOP这时进0xA0将状态复位。但有一种特殊顺序主机写完数据后不立即停而是紧跟着发重复起始来切到读方向这时候0xA0也复位移交给下一个SLAR帧处理。所以0xA0的复位对随机读尤其重要。4.3 读流程地址自增、0xFF回卷与NACK收尾读方向的实现补进同一个中断函数里case 0xA8: // 收到SLAR主机要开始读了 // 从当前字地址读数据先放到发送缓冲 I2CDAT eeprom_buf[mem_addr]; i2c_state I2C_STATE_READ; I2CON | 0x04; // 保持AA准备接收主机的ACK break; case 0xB8: // 上一字节已发出主机回ACK继续发下一字节 mem_addr; I2CDAT eeprom_buf[mem_addr]; break; case 0xC0: // 上一字节已发出主机回NACK本次读结束 i2c_state I2C_STATE_IDLE; break;0xA8状态发生时主机刚刚发送SLAR从机要立刻把第一字节放到I2CDAT发送寄存器。这里数据来源是eeprom_buf[mem_addr]mem_addr可能就是之前随机读或当前地址读指定的位置。主机读完第一字节后回ACK0xB8再把地址加1继续发下一个字节。这个循环会一直持续到主机回NACK也就是0xC0。收到NACK说明主机不再需要数据状态复位准备下一帧。整个过程没有在代码里显式判断地址回卷因为uint8_t的加法自动把0xFF变成0x00。有一点要单独说明当前地址读和随机读完全共用这套状态机。随机读的前半段走写流程把字地址存进mem_addr后半段SLAR进入0xA8直接从这个地址发数据。当前地址读则直接以0xA8进入mem_addr还是上一次写操作后残留的值行为上和真实EEPROM一致。顺序读则是0xB8持续循环直到主机NACK。完整代码里还需要一个默认分支处理那些不关心的状态码比如0x70、0x78广播呼叫0x88收到数据后回NACK。这些在本工程里保持AA1的情况下不会出现但写个default分支总没坏处default: break;把所有case组合起来这个中断函数就是模拟AT24C02的核心。把它和前面的初始化代码放一起编译烧录后接上主机I2C控制器只要地址对齐读写就已经能跑通。剩下的问题基本都集中在边界情况上。5. 常见问题与排查5次实测踩过的坑5.1 现象主机一直NAK从机不响应——I2CADDR地址位没对齐现象主机用0xA0做写地址扫描逻辑分析仪上能看到SCL正常翻转但SDA上只有主机自己在驱动从机始终不拉低SDA回ACK。原因I2CADDR寄存器写的是0x50没有左移一位地址比对永远失败或者A2/A1/A0引脚悬空、板子有上拉下拉干扰导致实际从机地址漂移。解决把I2CADDR改成0x501重新烧录后先扫描地址。给A2/A1/A0焊上下拉电阻固定电平不要靠引脚悬空碰运气。我后来养成的习惯是写一个地址扫描固件主机逐个地址探测确认能回ACK的那个地址再固化到驱动里。5.2 现象读写数据错位——中断里拖了太长时间现象写入256字节后回读前面几个字节正常越往后错位越严重严重时主机直接报I2C超时。原因I2C中断服务函数里做了耗时的Flash擦写、串口打印或者长延时SCL被从机拉低的时间超过了主机允许的等待上限。真实AT24C02写周期有5ms主机驱动是会等这5ms的但模拟时不能在中断里真等5ms。解决把耗时操作全部移出中断。RAM缓冲直接写脏标记只置位不落FlashFlash搬运放到主循环或者定时器回调里执行。中断函数保持精简只做状态转移和数据存取。5.3 现象掉电后数据丢失——RAM模拟的物理局限现象断电再上电配置参数恢复成默认值主机回读全是对应RAM初始化的内容。原因用的是纯RAM数组模拟掉电瞬间RAM内容全部清零没有把数据同步到非易失介质。解决改用两级方案。RAM做主存主循环里检查脏标记把改过的页搬进片内Flash。注意Flash擦写有寿命不能每个字节都触发整页擦除还要处理好“断电瞬间正在擦Flash”的情况搬运前关中断搬运完再开避免写到一半被I2C新数据打断。5.4 现象和板上其它I2C设备地址冲突——0x50太常用了现象同一根I2C总线上挂了别的存储或传感器设备主机设备扫描时发现两个从机都响应0x50数据读写出现随机性串扰。原因AT24C02的默认地址0x50太常见很多传感器芯片的地址也落在附近或者另一个AT24C02也是同地址。解决把A2/A1/A0引脚接到高电平把从机地址改到0x52、0x54甚至0x56如果硬件引脚已经固定接GND就在主机驱动里改设备地址同时把模拟程序里的I2CADDR同步修改。这种问题在设计阶段就要排查等PCBA贴完再改就麻烦。5.5 现象随机读时死锁——重复起始漏处理现象主机执行随机读先写字地址再发重复起始切换读方向从机直接卡死总线一直挂在低电平。原因收到重复起始时状态码是0xA0但代码里没有处理这一帧从机还停留在等待数据状态后续SLAR进来时状态错乱主机等不到数据。解决在case 0xA0里复位状态到IDLE同时保留前面写入的mem_addr这样后面SLAR到达0xA8时能直接从正确地址送数据。判断方法是看逻辑分析仪波形重复起始之后如果从机没有按时回ACK基本就是0xA0处理丢了。6. 给模拟EEPROM加“写保护”软件锁、校验回读与磨损均衡模拟器跑通之后我还加了几个小功能让这颗“软件EEPROM”更贴近真实芯片的工程习惯。第一个是软件写保护。AT24C02有WP引脚拉高就禁止写但单片机模拟时不好动引脚我用一个解锁序列替代主机要连续写两个固定字节程序才放开写操作否则写命令直接丢弃。#define UNLOCK_KEY1 0x5A #define UNLOCK_KEY2 0xA5 static uint8_t unlock_state 0x00; uint8_t eeprom_check_unlock(uint8_t dat) { if (unlock_state 0x00 dat UNLOCK_KEY1) { unlock_state 0x01; return 0xFF; } if (unlock_state 0x01 dat UNLOCK_KEY2) { unlock_state 0x02; return 0xFF; } return 0x00; }实际写入前先检查unlock_state是否为0x02不是就拒绝写入。这个机制能挡住很多意外写操作缺点是主机驱动要做两次额外写操作适合对安全要求高的项目。第二个是数据校验回读。我在这套程序里对Flash里的数据块存了魔数和CRC16读出来先校验校验不过就返回默认配置防止“写到一半掉电”把坏参数当作好参数用。第三个是磨损均衡把频繁更新的参数轮流写在多个Flash页里每页记录一个序列号下次写入就找序号最旧的一页。从那以后我每次做模拟EEPROM这类I2C从机外设都会先用逻辑分析仪把地址字节和数据波形完整抓一遍确认状态码和预期一致再往上叠应用逻辑。模拟外设最大的成本不是协议本身而是这些地址错位、状态码漏处理的小坑。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑