资讯动态

51单片机读写24C02 EEPROM:I2C时序详解与Proteus仿真实践

发布时间:2026/9/16 12:45:02 来源:尧图企业网站定制
简介这是一份面向 51 单片机学习者的 EEPROM 24C02 读写实例采用 C 语言编写并附带 Proteus 仿真工程可帮助理解和掌握 I2C 总线的操作流程适合高校学生、电子爱好者以及需要快速上手 I2C 通信的开发者参考。压缩包内共 11 个文件主要包含 C 语言源码、HEX 烧录文件、Keil 工程配置文件及备份、编译中间文件等资源包整体仅约 17KB内容精简而完整既可以直接阅读源码学习编程思路也可以加载到 Keil 和 Proteus 中进行仿真验证。目前已有 1295 人浏览学习该例程虽然体量小巧但涵盖了从工程配置、编译输出到仿真调试的完整环节非常适合课堂实验、课后作业或自学对照。通过该例程可以掌握 51 单片机 I2C 通信的软件实现理解 EEPROM 的写时序、读时序以及地址寻址方式并能在 Proteus 中直观观察数据写入与读回的全过程有助于巩固单片机与存储器接口设计的基础知识。1. 51 单片机读 24C02 卡在无响应这个 Proteus 仿真工程把时序摊开给你看EEPROM24C02 是 51 单片机项目里最常见的掉电保存方案2Kbit 容量存参数刚好走 I2C 总线却让不少人在第一期软件开发时翻车。新手写第一版 C 代码通常有两种症状发完设备地址收不到 ACKSDA 被外部拉死不下来或者写进去马上读回来得到 0xFF仿佛数据从没进去过。234-EEPROM24C02 这套实例工程把 Keil C51 源码、Proteus 仿真和预生成的 24c02.hex 整理在一起打开仿真就能观察完整读写现象想改逻辑也能对着 24c02.c 直接改隐患出在哪一层都看得见。适合正在学 51 单片机 C 语言实例的人照着拆 I2C 底层也适合已有产品经验、只想核对 ACK 处理和页写边界的人快速定位问题。2. 器件结构、设备地址、起始停止位软件模拟 I2C 的三个正确姿势2.1 24C02 的内部组织与软模拟为什么“慢一点更稳”24C02 的 2Kbit 容量实际是 2048 位正好组成 256 字节地址范围 0x00 到 0xFF。它内部又分成 16 页每页 8 字节这个结构直接决定了后面页写函数的边界逻辑。与并行 EEPROM 不同24C02 只把 SCL、SDA 两根线引到主控访问时需要先发一个器件地址字节说明“找的是哪颗芯片、接下来是写还是读”再发一个 8 位内部地址然后才是数据。51 单片机没有硬件 I2C 模块时用普通 IO 口做位操作是最通用的方案缺点是要自己保证时序落在协议范围内。软件模拟 I2C 有硬件等效性问题AT24C02 标准模式要求 SCL 高电平宽度不低于 4.0µs低电平宽度不低于 4.7µs起始停止条件的建立时间也在 4.7µs 附近。12MHz 晶振下一段简单的 C 空循环加几条赋值语句单次延时大约能到 5µs 级别刚好处在标准模式区间。把延时拉大一些不影响功能最多是总线频率从 100kHz 掉到 50kHz 甚至更低所以老代码里的人为延时写得很随意。真正要避免的是延时太短一旦 SCL 高电平时间低于 4.0µs24C02 在采样时可能漏掉某一位表现就是地址写进去但校验始终不通过。I2C 时序参数AT24C02 要求本工程软件目标SCL 高电平 tHIGH≥ 4.0µs约 5µsSCL 低电平 tLOW≥ 4.7µs约 5µs起始条件建立 tSU:STA≥ 4.7µs约 5µs停止条件建立 tSU:STO≥ 4.0µs约 5µs写周期 tWR典型 5ms上限 10ms固定延时 10msProteus 仿真中的 24C02 模型会按真实片子的写入周期模拟不会因为仿真速度加快就自动跳过 tWR所以代码里的 10ms 等待在仿真里照样生效现象和真板基本一致。2.2 C51 的 sbit 引脚定义与延时基准SCL、SDA 放在 P3.6 和 P3.7 是这套代码最常见的接法P1 口留给 LED 和按键P2 口还能外扩其他设备。C51 中定义如下#include reg51.h sbit I2C_SCL P3^6; // 时钟线接 24C02 的 SCL 引脚 sbit I2C_SDA P3^7; // 数据线接 24C02 的 SDA 引脚 void i2c_delay(void) { unsigned char i 10; while (i--); }这段延时没有用定时器纯粹靠空循环消耗时间。在 12MHz、12 时钟周期模式的 8051 内核下while(i--)每轮迭代需要几个机器周期最终单次延时大约 5µs。如果你用的是 STC 的 6T 模式需要把i的初值调大一点否则总线上实际频率会逼近甚至超过 400kHz 限制。判断标准不是初值本身而是最终量到的 SCL 高电平宽度这个放到第 5 章用逻辑分析仪验证。注意 P3.6、P3.7 是准双向 IO向引脚写 1 时实际是关断内部下拉把电平交给外部上拉电阻决定。也就是说 SDA 能否被拉高依赖外部电阻而不是单片机内部驱动这也是为什么 Proteus 仿真电路里漏画上拉电阻时I2C 会表现出“奇奇怪怪”的行为SDA 一直被拉低起始位放不出来发送任何字节后收到的一律是 NACK。2.3 起始停止条件与字节收发实现I2C 协议里起始和停止条件定义的是 SCL 高电平期间 SDA 的跳变沿而不是电平本身。这段代码把两个条件分别实现并且每次执行完都主动把 SCL 拉低void i2c_start(void) { I2C_SDA 1; I2C_SCL 1; i2c_delay(); I2C_SDA 0; // SCL1 时 SDA 由 1 变 0起始条件 i2c_delay(); I2C_SCL 0; // 拉低时钟准备发送数据 i2c_delay(); } void i2c_stop(void) { I2C_SDA 0; I2C_SCL 1; i2c_delay(); I2C_SDA 1; // SCL1 时 SDA 由 0 变 1停止条件 i2c_delay(); }为什么每次结束后要把 SCL 拉低因为后续发送数据时所有位变化都要求 SCL 处于低电平。如果让 SCL 保持高电平SDA 上的任何跳变都会被识别成新的起始或停止条件整帧数据直接作废。这是软件模拟 I2C 中最常见也最难查的隐性错误。字节发送和接收函数// 返回 0 表示收到 ACK返回 1 表示收到 NACK unsigned char i2c_send_byte(unsigned char dat) { unsigned char i, ack; for (i 0; i 8; i) { I2C_SDA (dat 0x80) ? 1 : 0; // 高位先出 dat 1; i2c_delay(); I2C_SCL 1; // 从机在 SCL 高电平时采样 SDA i2c_delay(); I2C_SCL 0; i2c_delay(); } I2C_SDA 1; // 释放数据线准备接收 ACK i2c_delay(); I2C_SCL 1; i2c_delay(); ack I2C_SDA; // 从机回低电平表示应答 I2C_SCL 0; i2c_delay(); return ack; } unsigned char i2c_recv_byte(unsigned char ack_flag) { unsigned char i, dat 0; I2C_SDA 1; // 主机释放总线进入输入状态 for (i 0; i 8; i) { dat 1; I2C_SCL 1; i2c_delay(); if (I2C_SDA) dat | 0x01; // 读取当前位 I2C_SCL 0; i2c_delay(); } I2C_SDA ack_flag ? 1 : 0; // ack_flag0 回 ACK1 回 NACK i2c_delay(); I2C_SCL 1; i2c_delay(); I2C_SCL 0; i2c_delay(); I2C_SDA 1; return dat; }ack_flag的语义需要记清楚主机读最后一个字节前必须回 NACK否则从机会认为还要继续发送把 SDA 继续拉低导致下一字节数据错位。很多初学者固定传 0跑顺序读时会多出 8 个时钟周期读出来的数组整体往后移了一个字节。ACK 位是在第 9 个时钟里采样的发送方要先把 SDA 释放掉这一点在 51 上尤其重要因为准双向口不会自动切换方向必须手动写 1。3. 字节地址、页写边界与 tWR 等待读写函数不能再裸奔3.1 器件控制字节和存储地址的对应关系访问 24C02 时第一个发送的字节叫器件地址字格式是 1 0 1 0 A2 A1 A0 R/W。Proteus 仿真图里 A0/A1/A2 都接地所以写方向地址是 0xA0读方向是 0xA1。如果板子上这三个引脚有接 VCC 的地址要相应改变这也是“把别人的程序抄过来后读不出数据”的高频原因——地址引脚配置不一样。写入一个字节的完整帧是起始 → 0xA0 → 内部地址0x00~0xFF→ 数据 → 停止 → 等待 tWR。随机读取一个字节则是起始 → 0xA0 → 内部地址 → 停止 → 起始 → 0xA1 → 读一字节 → 主机回 NACK → 停止。这里先做一次“伪写”只是为了把芯片内部的地址指针指到目标位置真实要发的内容只在后面的读帧里出现。操作类型第一个字节第二个字节后续内容结束单字节写0xA0目标地址1 字节数据停止页写0xA0页内起始地址最多 8 字节数据停止随机读取0xA0 伪写目标地址停止后重新起始0xA1 读数据当前地址读0xA1读数据主机回 NACK停止“当前地址读”和“随机读取”的差别值得利用如果刚写完某个地址下一次读取同一个地址时芯片内部的地址指针仍然停留在写操作结束的位置直接发 0xA1 就能读出来省去一次伪写。在多字节参数连续读写的场合这个技巧能减少总线占用。3.2 单字节写、随机读取与写周期等待代码实现如下其中delay_ms是工程里已有的毫秒级延时函数void at24c02_write_byte(unsigned char addr, unsigned char dat) { i2c_start(); i2c_send_byte(0xA0); // 器件地址写方向 i2c_send_byte(addr); // 目标存储地址 i2c_send_byte(dat); // 要写入的数据 i2c_stop(); delay_ms(10); // 等待内部写周期结束 } unsigned char at24c02_read_byte(unsigned char addr) { unsigned char dat; i2c_start(); i2c_send_byte(0xA0); // 伪写只设置内部地址指针 i2c_send_byte(addr); i2c_stop(); i2c_start(); i2c_send_byte(0xA1); // 切换为读方向 dat i2c_recv_byte(1); // 单字节读必须回 NACK i2c_stop(); return dat; }写后固定延时 10ms 是保证不出错的做法但不高效。更专业的处理是 ACK 轮询24C02 在内部写周期结束前对任何器件地址都不会应答所以主机可以不断发送“0xA0 停止”直到收到 ACK 再继续执行下一步。void at24c02_wait_ready(void) { unsigned char ack 1; do { i2c_start(); ack i2c_send_byte(0xA0); // 只发器件地址不发数据 i2c_stop(); } while (ack ! 0); }轮询方式把等待时间从固定 10ms 缩短到芯片实际写完的那一刻连续写入多条参数时累计节省的时间非常可观而且在 Proteus 里也能真实反映 24C02 的忙碌状态。提示at24c02_wait_ready里要加一个超时计数器。如果总线上根本没有 24C02 或者地址错误这个循环会一直执行下去程序看起来像死机。加了超时后至少能在调试串口里报出“EEPROM 无应答”的位置。3.3 页写不是“随便写几个字节”它有一条 8 字节边界24C02 把 256 字节分成 16 页每页 8 字节。页写允许在一个写周期内连续写入最多 8 字节但内部地址计数器在页内递增一旦跨过本页最后一位就会回卷到页首而不是滚到下一页。例如从 0x06 开始写 4 字节实际落点是 0x06、0x07、0x00、0x01后两字节会把页首数据覆盖掉。页写起始地址写入字节数实际落点0x0640x06、0x07、0x00、0x010x0460x04~0x07、0x00、0x010x0080x00~0x070x3850x38~0x3B、0x00 回卷会被拒绝的部分实现最后一行的表现取决于厂商具体实现部分芯片会拒绝超出页边界的写入部分会静默回卷。所以写函数不能只做一个“连续 send 字节”的循环调用前必须自己拆分边界unsigned char page_left 8 - (addr 0x07); // 当前页剩余空间 unsigned char chunk (len page_left) ? page_left : len; at24c02_write_page(addr, buf, chunk); // 先写本页剩余部分 if (len chunk) { at24c02_write_page(addr chunk, buf chunk, len - chunk); }(addr 0x07)取的是低 3 位恰好是页内偏移。把这段拆分逻辑统一收口在一个入口函数里调用方只关心数据长度不必每次手工算边界。换用 25C02 这类 SPI 接口 EEPROM 时页长变成 16 字节甚至 32 字节但“边界拆分”的套路完全一样很多人在换芯片后才发现原函数在页边界处丢数据就是因为只改了器件地址宏而没改页长参数。4. Proteus 仿真图、Keil 编译链路与实际故障排查4.1 先认清 24C02 在仿真图里的引脚连接压缩包里的 Proteus 仿真部分直接打开就能看不需要重新画一遍。如果自己搭建最典型的最小系统是AT89C51、24C02、两个 4.7k 上拉电阻、12MHz 晶振、两个 30pF 电容和复位电路。SCL、SDA 不是直接接到单片机就能工作两根线都必须有上拉电阻到 VCC。4.7k 对 100kHz 总线是安全取值10k 也能工作但边沿更缓如果后续要跑 400kHz最好换 2.2k 或 1k。另外文件清单里的24c02.plg是 Keil 编译日志24c02.M51是链接器生成的存储映射文件24c02.LST是汇编列表24c02.OBJ和24c02.hex都是构建产物真正要改的只有24c02.c其他都可以视为可再生成的中间文件。24C02 引脚连接网络说明1 A0GND地址引脚 02 A1GND地址引脚 13 A2GND地址引脚 25 SDAP3.7 4.7k 上拉数据线6 SCLP3.6 4.7k 上拉时钟线7 WPGND写保护关闭8 VCC5V供电WP 引脚是新手容易踩的坑。WP 接 VCC 后整个芯片只读不写写帧照样 ACK但数据进不了存储单元读回全是旧值。做带写保护的设备时WP 应该由单片机 IO 口控制用它区分“正常保存”和“固化参数”两种场景。仿真阶段直接接地最简单别让 WP 悬空悬空在某些模型里会被读成高电平。4.2 Keil C51 编译与 HEX 加载链路用 Keil μVision 打开24c02.Uv2在 Options for Target 的 Output 选项卡里勾选 “Create HEX File”按 F7 编译通过后Proteus 才能拿到最新的机器码。接着在 Proteus 中双击 AT89C51 元件在 Program File 路径里选择生成的24c02.hexCrystal Frequency 填 12MHz与 Keil 里的频率保持一致。频率不一致不会导致 I2C 完全失败但会让软件延时和串口波特率一起跑偏排查时容易误判成时序问题。命令行下确认 hex 内容时可以用 srec_cat 转成二进制再检查srec_cat 24c02.hex -Intel -o dump.bin -Binary xxd dump.bin | head能看到代码里确实存在 0xA0、0xA1 这样的设备地址常量。这个检查在“Proteus 里完全没波形”时很管用先确认程序真的烧进去了再回头查仿真电路否则容易在电路上浪费时间。4.3 仿真异常的三类现象与排查顺序Proteus 跑起来后可以把问题分成几类排查顺序从硬件到软件现象最常见原因排查步骤SDA/SCL 一直是高电平无波形上拉电阻缺失或阻值过大加 4.7k 上拉到 VCCACK 始终为 1器件地址错误检查 A0/A1/A2 接线和发送字节写后读回 0xFF未等待 tWR 或 WP 接了高加延时并确认 WPGND写入顺序错乱页写跨越页边界按低 3 位拆分写入仿真一运行就暂停单片机 hex 路径失效重新加载 24c02.hexP1 口接一个 LED 做运行指示最直观写成功亮写失败灭。调试这类 I2C 问题不要一上来怀疑时序分析先用示波器挂在 SCL、SDA 上看有没有时钟输出。没有时钟输出时问题几乎都在单片机侧程序没进 main、晶振频率设置错误、或者 hex 根本没加载进去。时钟有输出但数据不对时再去抓 ACK 和页边界问题效率会高得多。5. 用虚拟逻辑分析仪和循环自检验证 24C02 真的写进去5.1 Proteus 中挂接虚拟逻辑分析仪Proteus 左侧工具栏选择 Virtual Instruments放入 Logic Analyser。把 D0 接到 P3.6 也就是 SCLD1 接到 P3.7 也就是 SDA运行仿真后就能同时看到两根线的波形。判断起始位的方法很简单SCL 高电平期间 SDA 由高变低停止位反过来。再量一下 SCL 高电平宽度如果明显小于 4µs说明i2c_delay的初值偏小在标准模式下 24C02 可能采不到稳定数据。如果波形里 SCL 低电平时间几乎为 0那是拉高后立刻拉低导致的数据位还没稳定就被采样需要把循环里两句 SCL 操作之间至少保留一次i2c_delay()。5.2 一段可重复验证的页写对比测试把下面这段放进 main 循环跑起来后 LED 能长期指示读写状态。P1.0 定义为 LED正常时低电平点亮出错时熄灭。#include reg51.h sbit LED P1^0; unsigned char data test_data[8] {0x12,0x34,0x56,0x78,0x9A,0xBC,0xDE,0xF0}; unsigned char read_buf[8]; unsigned char i, ok; void main(void) { while (1) { at24c02_write_page(0x10, test_data, 8); // 从 0x10 开始写一整页 delay_ms(10); for (i 0; i 8; i) { read_buf[i] at24c02_read_byte(0x10 i); // 逐字节随机读回 } ok 1; for (i 0; i 8; i) { if (read_buf[i] ! test_data[i]) { ok 0; } } LED ok ? 0 : 1; } }测试数据要放在可写的 RAM 里不能用code段常量直接传给写函数。Keil C51 里 code 指针和 data 指针属于不同存储空间直接把 ROM 数组传给unsigned char *形参会编译报错。更稳妥的做法是像上面这样声明普通数组并初始化确保数据真的来自内存。这段循环跑满 500 次 LED 一直亮才能说明读写路径稳定中途只要灭一次就在出错点前后加多个 LED 指示定位是写失败还是读失败。5.3 用 EEPROM 内容窗口做终点校验Proteus 仿真暂停后右键点击 24C02在 Edit Properties 里打开 EEPROM 内容窗口直接查看 0x10 到 0x17 地址的值比对刚才写入的 8 字节。这个窗口里显示的是芯片内部逻辑状态比 LED 闪烁更直观能确认数据确实进入了存储单元而不是只通过了 ACK 握手。如果把写周期时间改成 0仿真会立刻完成写入但真实芯片不会这么快建议保留默认参数让 tWR 等待逻辑得到验证。这个测试函数建议扩展成带错误计数的统一校验模块连续写 0x00 到 0xFF 全部 256 个字节再从 0x00 开始完整读回逐字节比对错误累计到阈值后点亮红色 LED0 错误持续一段时间后再切换绿色 LED。把这一整套做成可复用函数后续接密码表、设备参数和校准数据时直接调用比每次临时手写读改写逻辑可靠得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价