用N76E003模拟AT24C02:I2C从机状态机设计全解析
2026/9/9 23:18:18 网站建设 项目流程

简介:针对N76E003单片机实现I2C从机模拟AT24C02的完整工程代码,面向嵌入式开发、电子设计以及外设扩展应用场景,尤其适合需要低成本替代EEPROM芯片或深入理解I2C从机协议的项目参考。程序涵盖I2C引脚初始化、从机地址0xA0识别、主从通信起始与停止条件检测、读写命令解析、256字节存储区模拟、应答信号控制以及超时/数据冲突等异常处理,几乎覆盖AT24C02核心功能;源码采用模块化组织,注释清晰,已在实际板卡上跑通全部读写流程。压缩包共16个文件,包括头文件、C源文件、启动文件、HEX固件、Keil工程及调试辅助文件,整体仅56KB,结构紧凑便于学习;其中HEX可直接烧录,省去重新编译的麻烦,也可直接基于Keil工程二次开发。已有1439人学习下载,对掌握N76E003的I2C从模式开发、模拟标准EEPROM行为以及I2C时序调试均有直接帮助。 前阵子帮人做一块老设备的升级板,主控板上原本挂了一片AT24C02,用来存配置参数。改硬件容易,但改主控程序要动产线、要重新烧录,麻烦得很。后来我想了个办法:干脆用N76E003单片机模拟一片AT24C02挂到I2C总线上,让主控完全无感,照常读写EEPROM,实际上数据全落在我这块N76E003里。项目做完以后读写都测试通过,主控端从头到尾不知道“EEPROM”已经换掉了。

这篇文章就把这个方案的完整思路、协议细节、代码框架和测试过程整理出来。适合手里有N76E003、想搞懂I2C从机怎么写的朋友,也适合正在纠结“如何不改主控程序就换掉一颗EEPROM”的工程师。我会把AT24C02的四种读写时序、从机状态机的设计逻辑、以及实测中容易踩的坑都过一遍,尽量做到看完就能自己动手。

1. 为什么非要用单片机去“冒充”EEPROM

先说清楚应用场景。如果你只是想在原有电路板上换一颗容量更大的EEPROM,或者改造一下存储内容,为什么不直接换芯片或者改主控程序?

原因很现实:老设备的主控程序往往没有源码,或者固件被加密保护,你根本动不了。但系统里要存的参数又确实需要变化,比如设备编号、校准系数、不同客户定制的配置字。这时候把I2C总线上的AT24C02“偷梁换柱”成一颗价格差不多的N76E003,既不动主控代码,又能让存储内容变成“活的”,是最省事的方案。

1.1 这类方案通常用在什么产品里

我实际接触到的需求大概有这么几类:

  • 给老设备增加序列号和校准信息,不重新烧录主控固件;
  • 需要根据外部条件动态生成配置,比如设备每天切换工作模式;
  • 在调试阶段做数据注入:让主控读到指定的测试值,而不用反复擦写EEPROM;
  • 想给设备加一层数据保护,主机读写的内容经过N76E003做校验或解密。

N76E003选它的理由也很直接:便宜,一片几块钱;自带硬件I2C外设,不需要完全靠GPIO模拟时序;512字节SRAM足够放一块256字节的模拟存储区。最关键的是它和AT24C02一样都是I2C从机,总线行为可以用状态机精确匹配。

1.2 “模拟”到底模拟的是什么

有人以为模拟就是把I2C引脚接上去、收到数据存起来就行。其实远不止。主机读AT24C02时,依赖的是一套固定的“行为习惯”:

  • 主机发送设备地址0xA0(写)/ 0xA1(读)后,片子必须在第九个时钟周期拉低SDA作为应答;
  • 主机写入寄存器地址后,地址指针要记住;
  • 连续读时,每读一个字节地址要自动加1,读到0xFF再加1要回卷到0x00;
  • 页写时写到本页最后一个字节后,地址要回卷到本页开头,不能串到下一页;
  • 主机认为读够了,会在最后一个字节后回一个NACK,从机就要立刻释放总线。

这些行为如果有一个对不上,主控程序就会卡死或者读到错误数据。所以模拟AT24C02的本质,是把这颗芯片的“协议状态机”搬到N76E003里。

2. 硬件准备与I2C物理层三个容易出错的地方

硬件接线不算复杂:N76E003的SDA和SCL接主机对应引脚,共地、供电,然后在两条线上各接一只上拉电阻到电源。但就这么简单的电路,我在实际项目里还是踩过几个坑。

2.1 N76E003的I2C引脚和电气特性

N76E003是增强型8051内核,I2C外设默认映射在P1组引脚上,具体是P1.3、P1.4,有些封装可以通过配置位切换。我在Keil C51环境下开发,I2C寄存器直接操作I2CON、I2STAT、I2DAT这套。建议开始写代码前先把手头数据手册里I2C章节翻出来,确认引脚映射和中断向量号,不同批次/封装略有差异。

供电方面,N76E003工作电压范围比较宽,通常在2.4V到5.5V之间。如果你的主控I2C总线是3.3V,就给N76E003供3.3V;如果是5V,直接供5V。总之要保证两边IO电平一致,否则会出现“主机以为发了1,从机读到0”的怪问题。

2.2 上拉电阻到底选多大

I2C总线是开漏结构,SCL和SDA必须靠外部上拉电阻拉高。阻值选择看通信速率和总线电容:

  • 标准模式100kHz,上拉电阻可以用10kΩ;
  • 快速模式400kHz,建议用4.7kΩ;
  • 如果总线上挂的设备多、走线长,再降到2.2kΩ。

我通常先用4.7kΩ起步,再用示波器看边沿。上拉太小,低电平被拉不到0.3VDD以下,从机会误判;上拉太大,上升沿太缓,高速通信会出错。特别是在面包板上验证时,面包板的寄生电容比PCB大,10kΩ就会有明显的圆角波形。

2.3 最容易忽略的硬件冲突

画板或者飞线之前,一定要确认目标总线上没有另外一颗同地址的AT24C02还挂在上面。AT24C02地址是7位0x50(8位写地址0xA0),如果板上原来那颗芯片还在且地址没改,N76E003一上去就是地址冲突,两个从机同时应答,数据直接乱掉。替换测试时把原芯片摘了,或者把它的A0/A1/A2地址引脚挑到别的地址。

另外,N76E003的I2C引脚建议配置成准双向模式。不要设成强推挽输出,否则和主机之间可能出现电平“打架”。这一点在官方例程的引脚初始化里一般都有,但很多人漏看。

3. AT24C02的协议拆解:不搞懂这4种时序就写不对程序

AT24C02容量2Kbit,也就是256字节,挂在I2C总线上,7位地址是0x50。它支持字节写、页写、当前地址读、随机读、顺序读。模拟器的核心就是把这五种时序用状态机翻译成N76E003的行为。

3.1 设备和寄存器地址怎么算

AT24C02的设备寻址字节格式是这样的:高四位固定1010,接着是A2、A1、A0三个引脚电平,最后一位是读写方向位。当A2=A1=A0=0时:

  • 写方向设备地址 = 0b10100000 = 0xA0
  • 读方向设备地址 = 0b10100001 = 0xA1

很多人用Linux的i2cdetect扫描时看到的0x50是7位地址,读0xA1、写0xA0是8位地址,两者相差一位左移关系。调试时别搞混。

存储器地址就是0x00到0xFF,正好一字节,对应AT24C02的256字节容量。

3.2 五种时序的差别

我把主机侧的操作整理成表格,后面写状态机就是照着这个表来:

操作类型时序过程
字节写START → 0xA0 → 寄存器地址 → 1字节数据 → STOP
页写START → 0xA0 → 寄存器地址 → 2~8字节数据 → STOP
当前地址读START → 0xA1 → 读1字节 → 主机NACK → STOP
随机读START → 0xA0 → 寄存器地址 → 重复START → 0xA1 → 读1字节 → 主机NACK → STOP
顺序读任意读方式开始后,主机每ACK一次,从机继续发下一字节,直到主机NACK

注意随机读里面有重复起始条件,不是简单的“写完再读”。很多第一次写模拟器的人在这里翻车,因为他没有处理重复START。

3.3 页写回卷和地址指针自增

页写是AT24C02最容易模拟错的地方。AT24C02的页大小是8字节,主机连续写入时,如果跨越页边界,地址会“回卷”到本页开头,而不是自动进入下一页。举个例子:寄存器地址为0x07,连续写入5字节,真实芯片上这5字节会依次落在0x07、0x00、0x01、0x02、0x03,而不是0x07、0x08、0x09。

读操作的地址指针规则又不一样:读多字节时,地址在每个字节后自动加1,超过0xFF后回卷到0x00。所以写地址回卷用“页内掩码”,读地址回卷用“全局递增”。

4. 从机状态机与关键代码(含“先写数据再清SI”的技巧)

搞懂协议之后,代码设计就有方向了。我见过有人用顺序查状态的方式写从机,也就是收到一个字节就if判断一次。这在主机时序简单时能用,但一旦遇到重复起始、主机NACK、页写回卷交织在一起,就会变得一团乱麻。正确的做法是维护一个清晰的状态机。

4.1 状态机需要关心的几个状态

我的程序里只用两个变量加一个枚举状态就够:

  • ee_mem[256]:模拟EEPROM存储区;
  • ee_addr:当前地址指针;
  • addr_phase:标记下一个收到的数据是不是寄存器地址;
  • 一个枚举用来看当前处于“写地址阶段”还是“写数据阶段”。

从机的状态流转基本是:

  1. 空闲状态,收到匹配的自身地址+写方向:进入写地址接收;
  2. 写地址接收状态下,收到的第一个字节是寄存器地址;
  3. 之后再收到的字节都是数据,写入模拟存储区,并按页回卷规则更新地址;
  4. 如果中途收到重复起始+读方向,说明主机要做随机读,地址指针保留;
  5. 读状态下,收到SLA+R立即把当前地址的数据放进发送寄存器,之后每次主机ACK就继续发下个字节,主机NACK就结束。

4.2 核心代码框架

提供一个精简但能跑通核心逻辑的框架,具体寄存器操作细节以N76E003官方头文件和例程为准:

#define I2C_STAT_SLA_W 0x60 // 收到自身地址+写方向 #define I2C_STAT_SLA_R 0xA8 // 收到自身地址+读方向 #define I2C_STAT_RX_DATA 0x80 // 从机接收模式下收到数据 #define I2C_STAT_TX_ACK 0xB8 // 从机发送数据后主机回ACK #define I2C_STAT_TX_NACK 0xC0 // 从机发送数据后主机回NACK #define I2C_STAT_STOP 0xA0 // 收到STOP或重复START volatile uint8_t ee_mem[256]; volatile uint8_t ee_addr = 0; volatile uint8_t addr_phase = 0; void I2C_IRQHandler(void) interrupt 6 // 中断号以你的头文件为准 { uint8_t sta = I2STAT & 0xF8; // 状态码取高5位 uint8_t dat; switch (sta) { case I2C_STAT_SLA_W: addr_phase = 1; // 等待寄存器地址 break; case I2C_STAT_SLA_R: // 收到读方向,立即预载第一个数据字节 I2DAT = ee_mem[ee_addr]; ee_addr++; // 地址自增 break; case I2C_STAT_RX_DATA: dat = I2DAT; if (addr_phase) { ee_addr = dat; // 第一个字节是寄存器地址 addr_phase = 0; } else { ee_mem[ee_addr] = dat; // 页写回卷:只允许在本页8字节内滚动 ee_addr = (ee_addr & 0xF8) | ((ee_addr + 1) & 0x07); } break; case I2C_STAT_TX_ACK: // 主机ACK,继续发下一个字节 I2DAT = ee_mem[ee_addr]; ee_addr++; break; case I2C_STAT_TX_NACK: // 主机NACK,读操作结束 break; case I2C_STAT_STOP: addr_phase = 0; break; default: break; } // 清SI标志,释放总线继续传输 I2CON &= ~0x08; // SI位清0方式以N76E003例程为准 }

这套代码的逻辑和真实AT24C02基本一致。关键点在中断里收到SLA+R后,必须立刻把ee_mem[ee_addr]写入I2DAT,再清SI。如果顺序反了或者漏写,主机第一个读时钟到来时数据寄存器还是空的,读回来的数据就是0xFF或者乱码。

4.3 “先写数据再清SI”背后的原因

I2C从机发送数据是跟着SCL时钟一位一位移出去的。N76E003的硬件I2C一旦清掉SI,总线就继续跑,如果这时发送寄存器还没有准备好数据,硬件只能把线上的数据拉成不确定状态。而且在清SI之前写入I2DAT,数据才能被硬件锁存到移位寄存器里。

顺带说一下主机的NACK处理。很多人在读操作里只处理“发数据”的状态,忘了处理“主机不想读了,回NACK”的状态。如果你不处理NACK,总线会一直停在等待状态,主机等不到STOP后的空闲,后续通信全部卡死。我在代码里虽然没有在NACK状态做额外动作,但清SI和STOP后回到空闲状态是必须的。

5. 实测验证:从地址扫描到跨页读写

标题里写了“已测试”,这里把测试过程完整放出来。测试主机我用了一块STM32F407来做模拟I2C主机,因为调试方便,又想脱离具体主机环境验证协议的正确性。当然也可以用树莓派、另一块N76E003、甚至逻辑分析仪直接抓波形,思路一样。

5.1 测试平台和接线

测试环境是面包板:

  • N76E003供电3.3V,外部晶振16MHz;
  • SDA、SCL各接4.7kΩ上拉到3.3V;
  • 主机STM32F407的I2C引脚也接同一组总线,共地;
  • 没有挂真实AT24C02,避免地址冲突。

5.2 测试步骤和结果

我用主机依次执行这几组操作,每组都串口打印结果:

测试项操作预期结果实测结果
地址扫描主机扫描I2C总线0x50地址应答通过
字节写读向0x10写入0xA5,回读0x10读到0xA5通过
页写验证从0x06连续写8字节数据落在0x06~0x05(页内回卷)通过
顺序读从0x20连续读256字节地址自动递增到0xFF后回卷0x00通过
随机读先写0x30再重复起始读读到0x30地址数据通过

页写回卷的验证方法是:向0x06写入[0,1,2,3,4,5,6,7],再回头读0x00到0x0F,会发现0x06和0x07是前两个数,0x00到0x05是后六个数。如果读出来数据是“顺序落进内存”,说明页回卷逻辑没写对。

5.3 测试过程中踩到的坑

第一个坑:地址扫描时主机发的是当前地址读,而不是写。N76E003在刚上电时ee_addr初始化为0,所以主机一上来读0x00地址是能正常应答的。如果初始化代码里忘了设初值,ee_addr是随机值,虽然也能应答,但主机读走的数据不可控,容易让人误判程序有问题。

第二个坑:中断优先级。I2C中断如果和定时器中断同时触发,而I2C中断优先级不够高,主机时钟不等人——一次延迟就可能错过SCL的采样窗口,通信直接失败。实测中我把I2C中断设成最高优先级,问题立刻消失。

第三个坑:页写回卷写成普通自增。这是逻辑bug,不是硬件坑。很多人写ee_addr++就完事,结果主机做跨页写时数据写到了下一页。改成我上面的掩码写法后,跨页行为就和真实芯片一致了。

6. 扩展方向:把“替身EEPROM”做成“智能外设”

基础模拟器跑通之后,可以往两个方向扩展,这些都是我在实际项目里验证过有价值的做法。

一是动态数据注入。既然主机读写的数据都经N76E003,我完全可以在中断外面定时更新ee_mem里的某些地址。比如用ADC采集温度,把温度值换算后填到某个地址,主机按老协议读EEPROM时拿到的就是实时温度。某块地址做成只读,主机写入时直接忽略,相当于加了写保护。这类功能在真实AT24C02上要么做不到,要么得外加逻辑电路。

二是掉电保存。N76E003本身没有EEPROM,但内置DataFlash。可以在主循环里检测ee_mem是否有变化,有变化就把整个或部分数据搬进DataFlash,上电再恢复。注意千万别在I2C中断里做DataFlash擦写,那会卡住总线,主机分分钟超时。我建议的做法:中断里只改RAM,主循环里有专门标志位触发保存,保存期间如果I2C有读写,数据先从RAM走,等总线空闲再落Flash。

三是模拟多个地址。N76E003的I2C地址寄存器支持多地址匹配,可以把一块芯片同时伪装成0x50、0x51两颗EEPROM,节省硬件成本。如果做产品,还可以把设备序列号、出厂日期写死在DataFlash区,主机读固定地址就能拿到身份信息。

最后再分享一个小技巧:调试时不要只盯着代码,尽量用逻辑分析仪抓一下总线波形。I2C从机问题很多时候不是“数据不对”,而是“时序不对”——应答位少了一个时钟、重复起始没被识别、NACK之后总线没释放,这些东西靠串口打印很难定位。我第一次调通这个项目,最大的功臣就是一台十几块钱的逻辑分析仪,把地址扫描和单字节读写的波形抓出来对照着看,状态机哪里有问题一目了然。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询