51单片机+I2C:AT24C02掉电保存按键计数的Proteus仿真 简介面向51单片机学习者和电子设计开发者这份资料基于89C51/89C52通用平台演示了将按键操作次数写入AT24C02掉电存储芯片再通过1602液晶屏实时读出的完整流程。工程采用C语言编写支持在Keil5中直接编译并配套Proteus 7.8仿真环境即使没有实体硬件也能直观观察IIC总线的读写时序和液晶显示效果。压缩包内共含5个文件包括C语言源程序、编译生成的hex烧录文件、Proteus仿真工程DSN文件以及PWI和DBK辅助文件整体仅39KB结构精简便于快速解压与查阅。目前已有941人学习下载说明该案例对理解AT24C02的掉电存储特性、EEPROM读写操作及LCD1602显示控制具有很好的参考价值。借助这份可运行、可移植的工程读者既能深入掌握IIC通信协议的实际应用也能学习按键扫描、数据存储与显示的综合编程思路是单片机入门与课程设计的实用范例。1. 按键计数为什么要落到 AT24C02 上按键计数在开发板上被实现过无数次但大多数写法是定义一个 int 变量在 RAM 里累加一断电就归零。真正做产品时计数值必须放进非易失存储。AT24C02 是 2Kbit 的 I2C 串行 EEPROM256 字节容量存按键次数绰绰有余51 单片机用两个普通 IO 口就能软件模拟出 I2C 时序不依赖硬件外设所以它是学习 I2C 协议和掉电保存最合适的载体。这套工程从 Keil5 编写 C 语言源码开始编译出 HEX 文件后拖进 Proteus 7.8 跑仿真89C51 和 89C52 通用包含 ex86.c、ex86.hex、ex86.DSN 完整文件打开仿真图即可运行。适合正在学 I2C 写读流程的嵌入式入门者也适合需要快速验证 EEPROM 时序和掉电保持特性的工程师。2. I2C 时序与 AT24C02 读写软件模拟从起始位到 ACK2.1 在 51 单片机上用 IO 口模拟 I2C 总线I2C 总线只有 SCL时钟和 SDA数据两根线半双工、主从结构。标准模式 100kHz快速模式 400kHz对 12MHz 晶振下的 AT89C51 来说用几个 NOP 循环就能压住时序。两条硬规则必须记住SDA 只能在 SCL 为低电平时变化SCL 拉高期间 SDA 必须保持稳定总线空闲时两根线都是高电平。起始条件的定义是 SCL 保持高电平时 SDA 从高拉低停止条件则是 SCL 高电平时 SDA 从低拉高。这两个边沿是所有读写操作的发令枪代码顺序不能颠倒#include reg52.h #include intrins.h sbit I2C_SCL P2^0; // 时钟线 sbit I2C_SDA P2^1; // 数据线 void I2C_Delay(void) { _nop_(); _nop_(); _nop_(); _nop_(); } void I2C_Start(void) { I2C_SDA 1; I2C_SCL 1; I2C_Delay(); I2C_SDA 0; // SCL 高电平期间 SDA 下降沿产生起始位 I2C_Delay(); I2C_SCL 0; // 拉低 SCL释放总线准备传数据 } void I2C_Stop(void) { I2C_SDA 0; I2C_SCL 1; I2C_Delay(); I2C_SDA 1; // SCL 高电平期间 SDA 上升沿产生停止位 I2C_Delay(); }起始位之后 SCL 必须立刻拉低否则从机会认为你在发无效电平。停止位同样要在 SCL 高电平期间构造 SDA 边沿所以先拉高 SCL 再抬 SDA。_nop_()来自 intrins.h一条约 1 个机器周期4 条在 12MHz 下大约 4us能满足 100kHz 的 SCL 半周期要求如果晶振换成 11.0592MHz这个延时依然成立因为实际只影响总线速率不破坏协议。2.2 器件地址 0xA0/0xA1 与 A0-A2 引脚映射AT24C02 的通信首字节不是数据而是器件地址加读写标志。8 位组成是高四位固定 1010中间三位是硬件引脚 A2/A1/A0 的电平最低位是 R/W0 写 1 读。芯片有 3 个地址脚最多能在同一条 I2C 总线上挂 8 片 AT24C02靠引脚电平区分片选。绝大多数开发板和这套 Proteus 工程里A0/A1/A2 直接接地。A2/A1/A0 引脚接法写器件地址读器件地址GND / GND / GND0xA00xA1VCC / GND / GND0xA20xA3GND / VCC / GND0xA40xA5VCC / VCC / GND0xA60xA7Proteus 连线时这三个引脚接到哪代码里的首字节就要对应改成哪。最容易犯的错是原理图上 A0 接了 VCC代码里还写 0xA0结果 ACK 永远收不到。另外 WP 写保护脚必须接 GND否则写入操作会被芯片拒掉。软件模拟 I2C 用不到单片机硬件 I2C 外设所以 P2.0/P2.1 上挂了普通 LED 或者按键也不会冲突只是注意这两个引脚要配置成准双向口。2.3 字节写、随机读与页写边界往 AT24C02 写一个字节的标准流程起始位、发 0xA0、等 ACK、发内部存储地址、等 ACK、发数据、等 ACK、停止位。读一个字节则先“假写”一次把内部地址指针指过去再用重复起始位切换成读方向读完必须由主机回 NACK 加停止位从机才会释放总线void AT24C02_WriteByte(unsigned char addr, unsigned char dat) { I2C_Start(); I2C_SendByte(0xA0); // 器件地址 写标志 I2C_WaitAck(); I2C_SendByte(addr); // 目标存储地址 0x00~0xFF I2C_WaitAck(); I2C_SendByte(dat); // 要写入的数据 I2C_WaitAck(); I2C_Stop(); DelayMs(5); // 等待片内写周期 tWR 结束 } unsigned char AT24C02_ReadByte(unsigned char addr) { unsigned char dat; I2C_Start(); I2C_SendByte(0xA0); // 先按写方向发地址 I2C_WaitAck(); I2C_SendByte(addr); I2C_WaitAck(); I2C_Start(); // 重复起始位不产生停止条件 I2C_SendByte(0xA1); // 切换为读方向 I2C_WaitAck(); dat I2C_RecvByte(); I2C_SendAck(1); // 主机发非应答表示这是最后一字节 I2C_Stop(); return dat; }读函数里那个重复起始位是关键动作它和停止位再起始的区别在于总线上没有出现 SDA 上升沿从机不会误判传输结束。I2C_SendAck(1)参数为 1 表示发送 NACK主机的 SDA 保持高电平并给一个时钟脉冲。写函数末尾的DelayMs(5)不能省略AT24C02 的写周期 tWR 典型值是 5ms数据是在停止位后才真正写入存储单元的提前发起下一次操作会读到旧值甚至丢数据。页写是 8 字节一个 page连续写超过 8 字节时地址只在页内回绕不会自动跨页。也就是说从地址 0x07 连写第二个字节它会落到 0x00 而不是 0x08。要写超过 8 字节的数据必须手动分页对齐或用字节写循环。这个工程的按键计数只占用 2 个字节用字节写最稳妥页写留给批量数据的场景。3. 1602LCD 并口驱动指令时序、忙检测与计数显示3.1 数据口选型与 RS/RW/E 控制线含义1602LCD 是 16x2 字符液晶内部有 DDRAM 地址对应屏幕上的每个字符位。控制信号有三个RS 选寄存器低电平写指令、高电平写数据RW 选方向低电平写、高电平读E 是使能脉冲下降沿锁存数据。数据线 D0-D7 可以走 8 位模式也可以只接高四位走 4 位模式省 IO。这套工程用的是 8 位模式数据口挂在 P0。P0 在 51 上是开漏输出内部没有上拉电阻所以必须外接 4.7k-10k 排阻或独立电阻到 VCC。Proteus 里如果省了这排上拉最常见的结果是 LCD 显示乱码或者干脆黑屏因为高电平拉不上去。控制线 RS/RW/E 接在 P3.4/P3.5/P3.6 上这几个口是准双向口内部带上拉不需要外加电阻。指令码功能0x388 位总线、双行显示、5x7 点阵0x0C开显示、关光标、不闪烁0x01清屏DDRAM 地址归 00x06写入数据后地址指针自动加 10x80第一行 DDRAM 首地址0xC0第二行 DDRAM 首地址双行 5x7 点阵的屏幕必须初始化成 0x38这个指令同时决定了总线宽度写错一个字后面所有显示都错位。0x80 和 0xC0 是定位指令屏幕上第 1 行第 1 列对应 DDRAM 地址 0x00加上 0x80 的标志位就是 0x80第二行首地址是 0x40加标志位就是 0xC0。3.2 初始化顺序与忙标志检测1602 上电后有大约 15ms 的内部复位时间这期间不能发任何指令。标准初始化顺序是延时 15ms、写 0x38、写 0x0C、写 0x01、写 0x06。每次写指令或写数据之前最好先查忙标志D7 位为 1 表示控制器正在处理上一条指令这比固定延时可靠因为不同厂商模块的处理速度有差异sbit LCD_RS P3^5; sbit LCD_RW P3^6; sbit LCD_E P3^4; #define LCD_DATA P0 void LCD_WaitBusy(void) { unsigned char status; LCD_RS 0; // 读状态寄存器RS 必须为低 LCD_RW 1; // 读方向 LCD_DATA 0xFF; // 释放数据口 do { LCD_E 1; DelayMs(1); status LCD_DATA 0x80; // 只关心 D7 忙标志 LCD_E 0; } while (status); LCD_RW 0; // 复位为写方向 } void LCD_WriteCommand(unsigned char cmd) { LCD_WaitBusy(); LCD_RS 0; LCD_RW 0; LCD_DATA cmd; LCD_E 1; DelayMs(1); LCD_E 0; // 下降沿锁存 }忙检测时数据口先写 0xFF 再读是因为 P0 开漏不写 1 就读不到高电平。LCD_E 的高电平脉宽要大于 450ns12MHz 下延时 1ms 完全够。有些 Proteus 工程为了简化把查忙换成每条指令后固定延时 2ms也能跑通但移植到真实的 STC 或 Winbond 芯片上时如果液晶模块响应偏慢就可能丢字符。3.3 按键次数从二进制到 ASCII 字符的换算1602 显示的是字符编码数字 0-9 对应的 ASCII 码是 0x30-0x39所以整数拆位后要加上 0x30 才能正确显示。按键次数用 unsigned int 存最大 65535显示 5 位数字足够。拆位用连续除法从万位一路除到个位void LCD_ShowCount(unsigned int count) { LCD_WriteCommand(0xC0); // 定位到第二行首地址 LCD_WriteData(0 count / 10000 % 10); LCD_WriteData(0 count / 1000 % 10); LCD_WriteData(0 count / 100 % 10); LCD_WriteData(0 count / 10 % 10); LCD_WriteData(0 count % 10); }写字符时 RS 必须为高LCD_WriteData内部已经通过LCD_WaitBusy保证了控制器不忙。0 数字是 C 语言里字符运算的惯用写法编译后等价于0x30 数字。如果想把高位 0 显示成空格而不是 0需要在拆位前判断这一位前面是否全是 0这个工程直接显示 5 位数字方便对齐也方便观察每次按键的变化。4. 按键消抖与写入策略把计数可靠地存进 EEPROM4.1 独立按键电路与电平判断按键电路在 Proteus 里直接用一个 BUTTON 元件一端接 P3.2一端接 GND。平时 P3.2 靠外部上拉电阻维持高电平按下时接地变成低电平。AT89C51 的 P3 口内部虽然有弱上拉但驱动能力不足以对抗长线干扰习惯上还是外接 10k 上拉电阻配合 0.1uF 电容做硬件滤波。判断按键状态只看电平读到 0 表示按下读到 1 表示松开。P3.2 的第二功能是外部中断 0 输入理论上可以用下降沿中断来触发计数。但 EEPROM 写入需要 5ms 等待如果在中断里直接写会长时间阻塞主循环而且按键抖动会产生多个下降沿中断方式反而要多一层标志位处理。这个项目用轮询方式最合适主循环里不断读取 P3.2代码直观时序也好控制。4.2 软件消抖与松手检测机械按键在按下和松开的瞬间簧片会震荡 5-10ms这期间电平反复跳变。只读一次电平就计数一次按压可能被记成三五次。软件消抖的常见做法是第一次检测到低电平后延时 10ms再读一次如果还是低电平就确认是有效按下之后进入松手等待防止长按期间重复计数sbit KEY_PIN P3^2; void Key_Scan(void) { if (KEY_PIN 0) { // 第一次检测到按下 DelayMs(10); // 跳过抖动区间 if (KEY_PIN 0) { // 二次确认仍然按下 key_count; AT24C02_WriteWord(key_count); // 立即写入 EEPROM LCD_ShowCount(key_count); while (KEY_PIN 0); // 等待松手 DelayMs(10); // 松手也要消抖 } } }while (KEY_PIN 0)空等松手在演示工程里最直观但如果在产品代码里这么做按键按住不放时主循环就卡死了定时器、显示刷新全部停摆。实际项目里应该用状态机把“检测按下、确认按下、等待释放”拆成三个状态主循环每次只查询一次状态机的跳转条件。这里保留空等是为了让 Proteus 仿真里按键手感符合直觉——按一下进位一次。4.3 写入时机、tWR 等待与掉电边界计数变化后立刻写 EEPROM是最简单也最能验证功能的策略。真实产品里要考虑两个边界一是 tWR 的 5ms 窗口内如果断电本次写会失败二是 EEPROM 的擦写寿命约 100 万次如果按键频率很高每次都写会快速耗尽寿命。按平均 1 秒按 1 次算100 万次大约能撑 11 天对长期运行的产品显然不够。更稳的方案是掉电检测用电源监控芯片检测 VCC 跌落触发外部中断在电压降到单片机最低工作电压之前利用电容储能完成最后一次 EEPROM 写入。或者退一步计数变化后只更新 RAM每隔 1 秒检查一次数值有变化才落盘。这套工程按一次写一次是因为要观察完整的写入-读回链路Proteus 仿真里通过 I2C 调试器能直观看到每次按键对应的写帧。主循环里还要处理开机时的读出。按两字节方式存储低字节放 0x00高字节放 0x01读回后拼成 unsigned int 再显示这样计数范围能到 65535超过单字节的 255 上限void AT24C02_WriteWord(unsigned int dat) { AT24C02_WriteByte(0x00, dat 0xFF); // 低字节 DelayMs(5); // 上一次 tWR 必须完整结束 AT24C02_WriteByte(0x01, (dat 8) 0xFF); // 高字节 } unsigned int AT24C02_ReadWord(void) { unsigned char lo, hi; lo AT24C02_ReadByte(0x00); hi AT24C02_ReadByte(0x01); return (unsigned int)hi 8 | lo; }两次字节写之间必须间隔一个完整的 tWR即使第一次写已经调用了带延时的AT24C02_WriteByte第二次写也要从函数内部重新等待这里再补一个DelayMs(5)是双保险。函数返回时把高字节左移 8 位再和低字节按位或(unsigned int)hi的强制转换不能省否则hi 8会按 int 运算高字节超过 0x7F 时符号位会出问题。5. Proteus 仿真验证上拉配置、I2C 调试器与典型故障5.1 仿真图连线检查清单AT89C51 的复位电路用 10uF 电容加 10k 电阻晶振用 12MHz 配两个 30pF 电容。AT24C02 的 SCL 接 P2.0、SDA 接 P2.1两根线上各接一个 4.7k 电阻到 VCC这是 I2C 总线正常工作的前提——没有上拉主机无法给从机提供可靠的高电平。A0/A1/A2 全部接地WP 接地。1602LCD 数据口接 P0P0 同样需要上拉排阻不然显示会花屏。加载 HEX 时双击 AT89C51Program File 路径指向 Keil5 编译输出的 ex86.hex晶振频率要和代码里的延时假设一致。注意Keil5 编译 51 工程需要安装 C51 编译器套件也就是 Keil C51而不是 MDK-ARM。用 ARM 版新建的工程无法生成 89C51 的 HEX。5.2 用 I2C Debugger 抓取写帧验证 ACKProteus 左侧工具栏里选择 I2C 调试器I2C DEBUGGER把 SCL 和 SDA 两个输入分别接到总线网络上然后把调试窗口打开运行仿真。按下按键时调试窗口会逐帧显示主机发出的字节。正常的字节写流程应该是1010 00000xA0跟一个 ACK接着是存储地址跟一个 ACK然后是数据字节跟一个 ACK最后是 STOP。如果某个 BYTE 后面显示的是 N说明从机没有应答优先检查器件地址和上拉电阻。调试器看到的地址帧可以直接和代码里的 0xA0 对照。地址读操作时窗口里会出现两次 START第一次是 0xA0第二次是 0xA1中间没有 STOP这正是随机读的标志。多观察几次写帧和读帧比单步调试 MCU 更容易理解 I2C 的应答机制。5.3 典型失败现象与处理对照现象原因处理方式LCD 有背光但无字符对比度脚 V0 悬空或初始化时序不对V0 接 1k-2k 电位器到地检查 0x38 是否先发读回全是 0xFF器件地址与 A0-A2 接线不一致核对 0xA0/0xA1 与引脚电平按键一次计数加多次消抖延时不够或没做松手检测延长到 10ms补while (KEY_PIN 0)写入后立即读仍是旧值tWR 等待不足写后至少延时 5ms 再发起读操作LCD 花屏乱码P0 缺少上拉电阻数据口加 4.7k 排阻仿真运行时按一次写一帧都看不到调试器没挂到网络节点确认 I2C Debugger 的 SCL/SDA 接在 P2.0/P2.1 所在网络上最后一个验证技巧在 Proteus 里双击 AT24C02 模型找到初始数据设置把 0x00 地址预填一个值比如 0x28也就是十进制的 40。启动仿真后如果 LCD 第二行开机就显示 40说明读通路和显示通路都正常然后再按几次键触发写入停止仿真重新启动数字保持而不是归零就同时验证了写入、读出、非易失三条链路。0x01 地址存放的是高字节修改初值时注意高低字节要配对填否则读回的 16 位数值会错位。本文还有配套的精品资源点击获取