恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
N76E003模拟AT24C02:完整I2C从机实现与踩坑记录
首页
资讯中心
/
N76E003模拟AT24C02:完整I2C从机实现与踩坑记录
N76E003模拟AT24C02:完整I2C从机实现与踩坑记录
发布时间:2026/9/10 1:40:00
简介面向嵌入式初学者的N76E003单片机I2C从机模拟AT24C02完整程序包作者已实测通过。程序基于新唐N76E003内置I2C外设完整实现与AT24C02兼容的从机应答、地址匹配、读写命令处理及256字节存储区模拟覆盖初始化、从机地址设置、命令解析、数据存取、应答与错误处理等关键环节适合需要理解I2C协议细节或希望用单片机替代外部EEPROM的开发者参考。压缩包共16个文件以C源码、H头文件为主另含STARTUP.A51启动文件、HEX烧录文件、Keil工程文件uvproj/uvopt及中间文件整体仅56KB目录结构清晰便于直接编译与下载测试。目前已有1439人学习下载。通过该工程可快速掌握N76E003的I2C从机编程方法获得可直接移植的模拟EEPROM代码与常见问题排错思路对于嵌入式底层驱动开发实战具有很好的参考价值。1. 为什么要让单片机冒充一颗EEPROM把一颗不到两块钱的 8051 内核单片机 N76E003调教成一颗“人见人爱”的 AT24C02这件事听起来有点魔幻但在实际项目里真的能救场。最近我做了一个 N76E003 模拟 AT24C02 的 I2C 从机程序接在 STM32、树莓派这类主机上读写都正常这篇把完整思路、协议细节、踩坑过程全部整理出来给后面做同类工作的朋友省点时间。可能有人会问AT24C02 芯片零售也就几毛钱直接焊一颗不就行了何必用单片机去模拟这个问题的答案恰恰是这个方案的核心价值。1.1 这个需求是怎么来的第一种常见场景是省料降本。消费电子、小家电方案里如果主控芯片本身的 Flash 不够存参数或者不想把参数区写在程序 Flash 里因为擦写会影响代码区稳定性一般会外挂一颗 I2C EEPROM。但如果板子上本来就有一颗单片机空闲着哪怕只是用来做按键扫描、LED 控制它的资源完全够再虚拟出一颗 EEPROM这样就能省掉一颗物料。第二种场景更实用老平台上电时序不对、配置参数需要动态调整但硬件设计已经定型没有多余的 I2C 从机地址可用或者上位机只认 AT24C02 的地址0xA0/0xA1此时让单片机去“冒充”反而比改主机代码更省事。主机侧完全感知不到区别它以为自己就是在跟一颗标准 EEPROM 通信。第三种是功能增强。利用模拟从机可以在数据写入时做过滤、加密、备份甚至让同一个 I2C 地址在不同逻辑区域映射到不同数据源。真实 EEPROM 做不到这些单片机模拟却能轻松扩展。1.2 N76E003 做这件事的优势N76E003 是新唐推出的一款增强型 8051 内核单片机TSSOP20 封装工作电压 2.4V~5.5V主频最高 16MHz内置 18KB Flash、1KB SRAM还有硬件 I2C、SPI、UART、定时器这些外设。拿它来模拟 AT24C02 有几个天然优势第一它本身就能在 5V/3.3V 系统里工作和大多数 I2C 总线电平兼容不需要电平转换第二1KB RAM 装下 256 字节的 EEPROM 模拟缓冲区绰绰有余第三价格低、货源稳定SOP20 封装手工焊接也方便打样调试成本极低。最关键的还是 I2C 从机对实时性要求高而 N76E003 的 8051 内核在 16MHz 下执行指令速度足够快用中断配合状态机处理 100kHz 甚至 400kHz 的 I2C 时序完全没有压力。1.3 这个方案适合谁如果你正在做单片机 I2C 从机开发或者手里有一批闲置的 N76E003又或者项目里确实遇到“没有 EEPROM 但 I2C 主机非要读 EEPROM”的怪需求那这篇就很适合你。它需要的基础是能看懂 C 语言了解 I2C 的基本概念起始、停止、ACK、字节传输上手很快。2. AT24C02 从机协议先要装得像想用单片机模拟一颗 EEPROM第一步不是写代码而是把 AT24C02 的 I2C 协议行为吃透。模拟的核心不是物理层那几个时序波形而是逻辑层的“记忆”地址指针怎么变、页写怎么回绕、随机读怎么响应。主机端会用各种方式去访问一颗 EEPROM你模拟得越像兼容性就越好。2.1 地址、寄存器与写操作规则AT24C02 的容量是 2Kbit也就是 256 字节。在 I2C 总线上它的设备地址是 7 位地址 0x50二进制 1010000写操作用的 8 位地址是 0xA0设备地址左移一位再补 0读操作地址是 0xA1。设备地址的低三位 A2、A1、A0 是硬件引脚决定的画板子时一般接地所以默认地址就是 0x50。如果同一个 I2C 总线上挂多颗 AT24C02靠的就是把这几个引脚接高接低来错开地址。写操作分两种字节写起始 0xA0 字地址 1 字节数据 停止。页写起始 0xA0 字地址 最多 8 字节数据 停止。这里有个特别容易被忽略的细节——AT24C02 的页大小是 8 字节。也就是说一次页写最多只能连续写 8 个字节超过 8 个字节后后面写的数据会覆盖当前页开头的位置而不是连续写到下一页。这是 EEPROM 硬件设计的典型限制页写操作是为了配合内部页缓冲区的容量。模拟器如果没有处理这个“页回绕”在跨页写数据时就会和真实芯片行为不一致导致主机数据错乱。2.2 读操作的三种方式与重复起始条件读操作是模拟 EEPROM 的重点也是最容易出问题的地方。主机访问 EEPROM 数据一般有三种方式当前地址读直接发起始 0xA1读取上一次操作完成后的内部地址指针指向的数据读完地址指针自动加 1。随机读先发起始 0xA0 字地址然后再发一个重复起始条件Sr 0xA1从刚才指定的字地址开始读。顺序读在随机读或当前地址读的基础上主机继续发时钟而不发停止从机就会连续输出数据地址指针持续自增直到主机在第 9 个时钟周期发送 NACK 并发出停止条件。随机读里的重复起始条件是从机模拟器必须处理的特殊时序。很多初学者在 GPIO 模拟 I2C 从机时只检测了起始和停止把重复起始误判成“新的一帧数据传输”结果随机读出来的第一个字节永远不对。2.3 页写回绕模拟器最容易翻车的地方页写回绕的具体行为是这样的如果写入页内偏移是 5主机连续写了 5 个字节那么地址会从页内偏移 5 走到偏移 15510超过 7 后回绕到 0、1而不是沿着 5、6、7、8、9 继续跨页写入。模拟 EEPROM 时我建议干脆按真实芯片的行为去实现而不是“好心”做跨页连续写。因为很多主机的驱动和上层逻辑就是按照真实 EEPROM 的规格书编写的你在模拟器上改了这个行为短期内可能感觉更“好用”但一旦换回真实芯片同样的代码就会出问题。模拟器最重要的原则就是尽量还原真实器件的每一个行为细节。3. N76E003 从机端整体架构明确了协议行为接下来就是架构选型。N76E003 自带硬件 I2C 外设也支持 GPIO 模拟 I2C。这个方案我用硬件 I2C 试过也全部换成 GPIO 模拟重写过实际对比下来的结论很明确从机模拟场景GPIO 模拟方案更可控调试也更方便。3.1 硬件 I2C 外设还是 GPIO 模拟N76E003 的硬件 I2C 支持主机和从机模式从机模式也有地址匹配中断。听起来很美好但实际做 EEPROM 模拟时我从机模式遇到几个难题硬件从机的状态机是芯片厂家设计好的它会在字节接收完成后自动处理 ACK也能在地址匹配时产生中断但问题是 EEPROM 模拟需要维护一个“内部地址指针”并且要区分当前帧是写模式还是读模式。硬件 I2C 外设的中断时机和寄存器状态有些情况下没法直观反映协议层面的状态尤其处理重复起始条件时需要同时维护 SI、STA、STO 这些标志位逻辑复杂而且可读性差。最头疼的是当模拟 EEPROM 要在中断里判断“现在是读第几个字节要不要继续发数据”时硬件外设的中断时机并不总能保证读数据时 SDA 转换及时稍有偏差就丢 ACK 或产生错误数据。GPIO 模拟方案则完全掌握主动权。SDA 和 SCL 都用普通 GPIO 配置成开漏模式配合外部上拉电阻再用边沿中断检测 SCL 和 SDA 的变化自己维护一切状态。实测下来GPIO 模拟的代码逻辑更直观出问题也容易通过逻辑分析仪定位。3.2 引脚与电气配置N76E003 的 P1.3 用作 SDAP1.4 用作 SCL。这两个引脚配置成准双向模式开漏输出外部各接一颗 4.7kΩ 上拉电阻到 VCC。I2C 协议本身就要求 SDA 和 SCL 是开漏结构这样主机和从机才能通过拉低总线来通信任何一端都不能强行推挽输出高电平。初始化时两个引脚都输出高电平让总线处于空闲状态。引脚模式配置如下P1M1 ~0x18; // P1.3、P1.4 清零 P1M2 ~0x18; // P1M1/P1M2 0设置为准双向模式 SDA 1; SCL 1;需要注意的是N76E003 的准双向模式内部有弱上拉但 I2C 总线建议再外接上拉电阻尤其是总线速度快、挂载设备多的情况下内部弱上拉驱动能力不足会导致上升沿变缓影响时序。3.3 从机状态机设计我采用的方案是用 P1 中断两个引脚共用配合主循环超时看门狗核心逻辑全部在中断服务程序里完成。中断服务程序要处理的信号变化包括SCL 上升沿数据线 SDA 电平被主机采样从机要把当前位的值读进来。SCL 下降沿主机释放 SDA从机可以切换 SDA 方向。SDA 下降沿且 SCL 为高检测到起始条件。SDA 上升沿且 SCL 为高检测到停止条件。第 9 个时钟周期生成 ACK 或读取主机的 ACK/NACK。状态机只需要记住几个关键变量当前状态等待设备地址/等待字地址/等待数据/发送数据、内部地址指针 addr、上次访问的地址 lastAddr、当前已接收的位数 bitCount。核心循环如下起始后进入 SLAVE_ADDR 状态接收 8 位判断设备地址是否为 0x50。地址匹配后根据第 8 位R/W 位决定是写模式还是读模式。写模式先收字地址再收数据写入缓冲区按页回绕规则更新地址指针。读模式从地址指针指向的位置发送数据每发完一字节根据主机的 ACK/NACK 决定继续还是结束。整个状态机最关键的一点遇到重复起始条件必须重置到 SLAVE_ADDR 状态但保留 lastAddr 指针供随机读使用。实现上检测到 SDA 下降沿且 SCL 为高时如果当前状态不是 IDLE就先把状态重置到 SLAVE_ADDR而不是当成错误丢弃。这样随机读第一次发地址、第二次发读命令逻辑上就是连续两次设备地址帧自然就能正确处理。4. 关键代码实现与解释下面把核心代码拆开讲。代码用 N76E003 的标准 C 语言编写编译环境是 Keil C51单片机主频 16MHz。4.1 GPIO 初始化与中断配置// 引脚定义 #define SDA P13 #define SCL P14 void I2C_Slave_Init(void) { P1M1 ~0x18; P1M2 ~0x18; SDA 1; SCL 1; PIF 0; // 清除 P1 中断标志 P1IEN | 0x18; // 使能 P1.3、P1.4 边沿中断 set_EPI; // 使能 P1 中断 EA 1; }P1 中断是 N76E003 所有 P1 引脚共用的触发后需要读取 PIF 寄存器判断具体哪个引脚发生变化然后分别处理。SDA 和 SCL 都在这个中断向量里所以中断服务程序第一件事就是记录引脚状态快照避免后续读引脚时电平已经变化。4.2 起始/停止/重复起始检测void PINT_ISR(void) interrupt 12 { uint8_t pif PIF; PIF 0; // 清标志防止重复进入 if (pif 0x10) { // SCL 变化 if (SCL !lastSCL) { // 上升沿采样 SDA bitVal SDA; process_bit(bitVal); } lastSCL SCL; } if (pif 0x08) { // SDA 变化 if (SCL) { if (lastSDA !SDA) { // 下降沿且 SCL 高 起始/重复起始 handle_start(); } else if (!lastSDA SDA) { // 上升沿且 SCL 高 停止 handle_stop(); } } lastSDA SDA; } }这里有个细节检测到起始条件或停止条件时SDA 的电平变化和 SCL 的电平变化几乎同时发生但不完全同步。我实测时发现有时候中断标志里只有 SDA 变化SCL 没有变化所以必须在 SDA 变化分支里加 SCL1 的判断来区分起始/停止和数据位变化。重复起始条件的处理就在 handle_start() 里如果当前状态已经接收过设备地址说明这是随机读的重复起始此时重置 bitCount0状态回到 SLAVE_ADDR但不清空 lastAddr。这个判断是整个随机读实现的关键。4.3 地址接收与 ACK 控制void process_bit(uint8_t bitVal) { bitCount; rxBuffer (rxBuffer 1) | bitVal; if (bitCount 8) { if (state STATE_SLAVE_ADDR) { // 判断设备地址 devAddr rxBuffer 1; isRead rxBuffer 0x01; if (devAddr 0x50) { ack_slave(); // 地址匹配拉低 SDA 给 ACK if (isRead) { state STATE_READ_DATA; sdaOutput 1; // 释放 SDA准备发送数据 // 加载当前地址指针 addr lastAddr; } else { state STATE_WORD_ADDR; } } else { nack_slave(); // 地址不匹配释放 SDA state STATE_IDLE; } } else if (state STATE_WORD_ADDR) { addr rxBuffer; // 写入的字地址 lastAddr addr; ack_slave(); state STATE_WRITE_DATA; } else if (state STATE_WRITE_DATA) { write_byte(addr, rxBuffer); ack_slave(); // 更新地址指针页回绕 if ((addr 0x07) 0x07) { addr 0xF8; // 回绕到页首 } else { addr; } lastAddr addr; } bitCount 0; } }ACK 生成的时机要卡在第 9 个时钟的高电平期间。N76E003 的准双向模式下往引脚写 0 就是拉低写 1 就是释放。我的实现里第 8 个数据位接收完成后立即把 SDA 拉低写 0第 9 个时钟高电平时主机检测到 SDA 为低即收到 ACK。等第 9 个时钟下降沿到来时把 SDA 重新释放写 1准备好下一个字节。这里踩过的坑是如果第 9 个时钟下降沿还没到就把 SDA 释放主机在 ACK 采样点会读到高电平判断为 NACK通信直接中断。所以 ACK 释放的时机必须放在第 9 个时钟的下降沿中断里做不能用简单延时代替。4.4 读数据时的 ACK/NACK 检测读取模式下从机是数据发送方主机是接收方。第 8 个数据位发送完后从机需要释放 SDA让主机拉低表示 ACK 或保持高表示 NACK。代码如下// 读取模式下第 8 位已经发完 if (state STATE_READ_DATA) { // 在 SCL 下降沿释放 SDA等主机在第 9 个时钟给出 ACK/NACK if (sdaOutput) { SDA 1; // 释放 SDA } // 等第 9 个时钟下降沿时读取 SDA if (bitCount 9) { if (SDA 0) { // 主机 ACK继续发送下一个字节 addr; if (addr 0xFF) addr 0; lastAddr addr; sdaOutput 1; } else { // 主机 NACK意味着读操作结束 state STATE_IDLE; } bitCount 0; } }顺序读的核心就是主机每发一个 ACK从机就继续发下一个地址的数据地址指针自增。当主机不需要更多数据时会在最后一个字节的第 9 个时钟不发 ACK 而是释放 SDA从机检测到 NACK 后就把状态机复位到 IDLE等待下一个起始条件。读模式下位计数要在不同的时刻采不同的逻辑第 1~8 位是从机往 SDA 上放数据主机采样第 9 位是主机往 SDA 上放 ACK从机采样。所以我的中断服务程序里根据 bitCount 的值区分当前是数据位还是 ACK 位处理方式不同。4.5 总线超时保护I2C 从机最怕总线异常导致状态机卡死。比如主机在传输过程中突然复位停止条件永远不会来从机如果一直停在“等待数据”状态总线上的其他通信也会受影响。我加了一个 10ms 的定时器看门狗每次中断服务程序检测到起始/停止/数据位变化时都会刷新定时器。如果 10ms 内没有任何总线活动就强制复位状态机、清空地址指针、释放 SDA让总线恢复到空闲状态。这个超时时间要大于主机最长的单字节传输时间又不能太短导致正常慢速传输被误杀。标准模式下 100kHz 波特率一个字节加 ACK 大约 90us10ms 已经留了很大余量。5. 实测结果和踩坑记录项目标注“已测试”这里把真实的测试过程和踩坑记录分享出来。测试环境用 STM32F407 的 GPIO 模拟 I2C 主机逻辑分析仪抓取总线波形再用 USB 转 I2C 工具做辅助验证整个测试持续了三天中间抓出不少隐蔽问题。5.1 逻辑分析仪实测波形验证测试的第一步是验证基础读写。用主机的字节写操作往地址 0x00 写入 0x55 和 0xAA然后用当前地址读和随机读两种方式读回来逻辑分析仪抓到的波形和真实 AT24C02 几乎完全一致设备地址 ACK 正常、数据字节 ACK 正常、随机读的重复起始条件正确处理、最后的 NACK 和停止条件时序正确。第二步测试页写回绕。向地址 0x05 连续写 5 字节按 AT24C02 的规则写入顺序应该是 0x05、0x06、0x07、0x00、0x01。我用逻辑分析仪抓取 SDA 上的数据确认回绕行为精确匹配。这一步验证了模拟器和真实 AT24C02 在该细节上完全一致。第三步测试顺序读。主机连续读 10 字节从机依次输出 lastAddr 开始、地址自增的 10 个字节最后一次主机回 NACK 后从机正确停止发送。5.2 常见问题速查表现象可能原因解决办法从机完全没有 ACK 响应SDA/SCL 引脚模式配置错误或者外部上拉电阻缺失确认准双向模式、外接 4.7kΩ 上拉用示波器看 SCL/SDA 是否正常拉低设备地址能匹配但写入后读出来全 0xFF数据写在缓冲区但地址指针没有更新或页回绕逻辑错误检查 process_bit 中 addr 更新逻辑打印内部缓冲区确认写入位置随机读第一个字节正确后续字节错乱重复起始条件没有重置状态机导致第二次设备地址未被识别在 handle_start 中把状态重置到 SLAVE_ADDR但保留 lastAddr顺序读多出一个字节ACK/NACK 检测时机太早把主机的 NACK 误判为 ACK确保第 9 个时钟下降沿再采样 SDA不要在上升沿采样总线偶尔锁死主机一直等不到 ACK异常时序导致状态机卡在中间状态增加 10ms 总线超时看门狗超时强制复位状态机拔出主机 USB 转 I2C 工具后重新插入无法通信上电时 SDA/SCL 电平不确定从机进入了错误状态代码初始化阶段等待 SCL/SDA 稳定 100ms再使能中断5.3 这个方案还能怎么扩展EEPROM 模拟器最直接的扩展是换容量。把协议里的一字节字地址改成两字节页大小从 8 字节改成 16 字节就能模拟 AT24C64、AT24C128 这些大容量 EEPROM。改动并不大但必须同步修改页回绕的掩码AT24C64 是 16 字节一页回绕判定要从(addr 0x0F) 0x0F判断。如果想让模拟的数据掉电不丢可以把 N76E003 的 18KB DataFlash 利用起来。每次写入数据时先在 RAM 缓冲区里更新再在空闲时把整个缓冲区备份到 DataFlash。注意 DataFlash 擦写次数有限制通常几万到十万次这种方案只适合写入频率不高的场景。还能做多地址映射。用 N76E003 的 GPIO 读外部跳线状态决定从机响应 0x50~0x57 中的哪个地址相当于一颗芯片模拟多颗 EEPROM。这个功能在做 I2C 总线地址调试、产品快速改版时特别有用不用改硬件就能调整地址分配。写保护功能也可以做。把 AT24C02 的 WP 引脚用一个 GPIO 模拟高电平时拒绝写入只允许读取和真实芯片的 WP 行为完全一致。这个细节虽然小但能提升模拟器的完成度也是我在做完基础功能后顺手加的。最后提醒一句EEPROM 模拟器毕竟是软件实现对中断响应时间比较敏感如果工程里还有其他高优先级中断必须合理设置优先级。我实测时发现如果 I2C 中断被其他中断阻塞超过一个位周期100kHz 下是 10us就会丢数据。所以除了 I2C 中断其他中断程序尽量保持精简必要时可以暂时关闭其他中断响应确保 I2C 时序不被打断。本文还有配套的精品资源点击获取