ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Proteus的24C02 EEPROM读写仿真:I2C通信从入门到实践

基于Proteus的24C02 EEPROM读写仿真:I2C通信从入门到实践 简介这是一份面向初学者的Proteus EEPROM仿真入门资料围绕I²C总线下的24C02/24C08读写操作展开主要解决刚接触单片机与I²C协议的学习者缺乏可运行示例的问题。工程选用24C02替代24C08进行模拟配合较全面的中文注释逐段解释设备地址、SCL/SDA时序、应答信号等关键概念并附有对应的C语言源代码与可运行Hex文件。压缩包共14个文件以DSN仿真电路、C源文件、UV2工程文件及Hex烧录文件为主整体仅41KB可在Proteus中直接打开并加载运行。目前已有1065人学习下载适合正在学习单片机I²C通信或Proteus仿真的初学者参考。通过这份资料读者不仅能搭建EEPROM读写电路、编写通信代码还能排查常见I²C故障同时对比24C02/24C04/24C08的容量差异理解不同型号的选型思路。 最近整理单片机学习资料翻出来一个特别适合新手拿来练手的Proteus仿真项目项目名叫“proteus_24C08—有注释”。名字看着像24C08但作者在Proteus里实际用的是24C02代码注释写得很全几乎每一行都有解释摆明了就是给刚学I2C通信的初学者准备的。这个实验解决的事情很具体用单片机通过I2C协议往EEPROM芯片里写数据再把数据读回来通过虚拟终端或者LED显示结果全程在Proteus里跑不用焊板子、不用买芯片零成本把I2C时序、EEPROM寻址、应答机制这些抽象概念搞明白。适合三类人刚学完单片机基础想碰总线通信的、准备做单片机课设但怕烧硬件的、以及想搞清楚24C02和24C08到底差在哪的。下面我把这个项目的完整思路、原理、代码和Proteus操作细节都拆开讲一遍照着做就能跑通。1. 项目背景与整体设计思路1.1 这个实验到底在做什么EEPROM是一种掉电不丢数据的存储芯片常用于保存设备配置参数、校准数据、用户设置等。I2C是一种只需要两根线的通信总线一根时钟线SCL一根数据线SDA就能在多个设备之间传数据。把EEPROM和I2C放在一起学的价值在于芯片结构简单、协议通用性极强掌握了这一套后面玩传感器、RTC时钟、OLED屏幕都会轻松很多。在Proteus里做这个实验最大优势是可视化。你能在仿真中直接观察到SCL时钟波形、SDA数据线上的电平变化甚至可以用Proteus自带的I2C调试器把总线上传输的每个字节都解析出来。物理世界里的逻辑分析仪可能要几百上千块仿真软件里这些工具全是免费的对初学者来说是非常友好的学习环境。1.2 为什么标题写24C08仿真却用24C02这个细节其实是一个很典型的知识点。24C02和24C08核心区别在容量24C02是2Kbit也就是256字节24C08是8Kbit也就是1024字节。它们都是I2C接口的EEPROM指令时序完全一样唯一需要调整的是器件地址的分配方式。在Proteus元件库里如果你搜“24C08”会发现仿真模型的行为和24C02几乎一致。很多教程和参考项目干脆直接用24C02代替24C08来做仿真因为对于学I2C协议来说操作逻辑完全不受影响。作者在标题里专门标注这一点就是提醒后人别纠结型号把时序和读写函数整明白换哪颗芯片都能用。落实到器件地址上24C02和24C08的差别很关键24C02的器件地址格式是1010 A2 A1 A0 R/WA0、A1、A2是引脚地址可以接高接低理论上一条总线上最多挂8颗24C02。24C08的器件地址格式是1010 A2 B1 B0 R/WA2还是引脚地址但原来A1、A0的位置变成了块选择位B1、B0。因为24C08容量大内部被分成4个256字节的块用B1、B0来选择当前操作哪一块。所以在24C08里即使A0引脚悬空也不影响寻址而24C02必须正确配置A0、A1、A2三个引脚。学习时先用24C02地址逻辑更简单不容易绕晕。1.3 软硬件准备清单做这个实验需要准备的东西不多软件为主用途工具说明电路仿真Proteus 8.x我用的是8.17版本其他版本操作大同小异程序编译AVR GCC / CodeVisionAVR若用51内核则用Keil C51烧录文件HEX文件编译后加载到Proteus单片机中仿真观察虚拟终端、I2C调试器均在Proteus左侧虚拟仪器面板中电路层面Proteus里需要拖出来的元件包括ATmega16单片机或者你自己熟悉的51单片机、24C02、两个4.7k电阻上拉、若干LED、一个虚拟终端。如果你以前没搭过Proteus仿真工程建议先把“如何新建工程、放置元件、加载HEX”这步走通再做这个实验会顺畅很多。2. 原理先行I2C协议与EEPROM操作基础2.1 I2C总线时序起始、停止、应答I2C的通信基础是四个动作起始条件、停止条件、发送数据、应答信号。起始条件SCL保持高电平时SDA从高电平跳变到低电平。这相当于通知总线上所有设备“我要开始通信了”。停止条件正好相反SCL保持高电平时SDA从低电平跳变到高电平表示“本次通信结束”。发送数据时主机在SCL的低电平期间改变SDA电平在SCL高电平期间保持SDA稳定。每次发送8位数据第9个时钟周期是应答位接收方把SDA拉低表示应答ACK不拉低表示非应答NACK。初学者可以类比成敲门递纸条起始条件是敲门停止条件是关门应答是对方回了一句“收到了”数据是纸条本身。Proteus仿真中这些时序都能在I2C调试器或者虚拟示波器上看得清清楚楚。我第一次用I2C调试器看时序的时候原来书上怎么都看不懂的时序图一下子通了。2.2 设备地址和内部地址两个地址别搞混操作EEPROM需要发送两个地址设备地址和内部存储地址。设备地址用于确定和哪颗芯片通信。以24C02为例如果A0、A1、A2全部接地那么写操作的设备地址就是0xA0二进制1010 0000读操作的设备地址是0xA1。这就是硬件地址跟着电路接线走。内部存储地址用于确定芯片内部哪一个存储单元。24C02有256个字节地址范围0x00到0xFF。这一步好比芯片已经确认“你在和我说话”接下来要告诉它“请打开第几个抽屉”。完整写一个字节的I2C通信过程是主机发送起始条件 主机发送设备地址(写): 0xA0 等待EEPROM应答 主机发送内部地址: 0x00 等待EEPROM应答 主机发送数据: 0x5A 等待EEPROM应答 主机发送停止条件读一个字节的过程稍微多一步需要先发送一个“假的写操作”指定内部地址然后重新发送起始条件再发送设备地址(读)0xA1主机接收数据后发送非应答最后停止。这一步很多初学者第一次写代码会卡住原因就是忘了读操作前要先“定位抽屉”。2.3 写周期、页写、跨页回卷EEPROM写入不是瞬间完成的芯片内部需要时间把数据真正“烧”进存储单元。24C02典型写周期约5毫秒写完后才能执行下一次操作。正规工业代码会查询EEPROM的应答信号来判断写周期是否结束仿真阶段最省事的做法就是延时5到10毫秒。页写是EEPROM的一个批量操作一次可以连续写入多个字节而不用每个字节都发一次设备地址。24C02的页大小是8字节24C08的页大小是16字节。页写的坑在于回卷如果连续写入的字节数超过了当前页的边界多余的数据会回到该页开头覆盖已有数据而不是自动写入下一页。跨页写是新手最容易翻车的地方严谨做法是计算剩余页空间分段写入。3. 完整代码实现注释比代码多代码我以ATmega16为例用软件模拟I2C的方式实现。选ATmega16的原因一是前几年那本《单片机C语言程序设计实训100例——基于AVR Proteus仿真》让这颗芯片在仿真圈很常见二是AVR的GPIO操作直观学完能自然过渡到51或STM32。我特意把注释写得密一些每一步对应的时序都在代码里标注。3.1 I2C底层驱动函数软件模拟I2C的核心是控制两根GPIO的电平时序。我选择PC0作SCL、PC1作SDA具体可以根据自己电路改。// // I2C底层驱动(软件模拟) // MCU: ATmega16, SCLPC0, SDAPC1 // #define SCL_DDR DDRC #define SCL_PORT PORTC #define SCL_PIN PINC #define SCL_BIT PC0 #define SDA_DDR DDRC #define SDA_PORT PORTC #define SDA_PIN PINC #define SDA_BIT PC1 // 宏定义控制SCL/SDA电平 #define SCL_H() (SCL_PORT | (1 SCL_BIT)) #define SCL_L() (SCL_PORT ~(1 SCL_BIT)) #define SDA_H() (SDA_PORT | (1 SDA_BIT)) #define SDA_L() (SDA_PORT ~(1 SDA_BIT)) #define SDA_READ() ((SDA_PIN (1 SDA_BIT)) ! 0) // SDA切换为输入模式(释放总线,配合上拉电阻输出高电平) #define SDA_IN() (SDA_DDR ~(1 SDA_BIT)) // SDA切换为输出模式(拉低总线) #define SDA_OUT() (SDA_DDR | (1 SDA_BIT)) // 启动I2C: SCL高电平期间, SDA产生下降沿 void i2c_start(void) { SDA_OUT(); // SDA设为输出 SDA_H(); // SDA初始高 SCL_H(); // SCL先置高 _delay_us(5); SDA_L(); // SCL为高时,SDA拉低 起始条件 _delay_us(5); SCL_L(); // 拉低SCL,准备传输数据 } // 停止I2C: SCL高电平期间, SDA产生上升沿 void i2c_stop(void) { SDA_OUT(); SDA_L(); // SDA先拉低 SCL_H(); // SCL拉高 _delay_us(5); SDA_H(); // SCL为高时,SDA拉高 停止条件 _delay_us(5); } // 主机发送一个字节, 返回从机应答位(0ACK, 1NACK) unsigned char i2c_send_byte(unsigned char dat) { unsigned char i, ack; for(i 0; i 8; i) { // 数据在SCL低电平时改变 if(dat 0x80) SDA_H(); // 当前位为1, SDA置高 else SDA_L(); // 当前位为0, SDA拉低 dat 1; // 左移,准备发送下一位 _delay_us(3); SCL_H(); // SCL拉高,从机在此时采样 _delay_us(5); SCL_L(); // SCL拉低,完成一位传输 _delay_us(3); } // 释放SDA,由从机拉低应答 SDA_IN(); _delay_us(3); SCL_H(); _delay_us(5); ack SDA_READ(); // 读取应答电平 SCL_L(); _delay_us(3); return ack; // 0表示从机应答正常 }这个层级的函数是整个项目的基石。实际调试中如果发现ack一直返回1优先怀疑设备地址对不对、上拉电阻有没有接、SCL和SDA引脚有没有接反。顺序不要乱。接收字节的函数结构类似只是方向反过来// 主机接收一个字节 // ack_flag: 1表示主机发送非应答(NACK), 0表示主机发送应答(ACK) unsigned char i2c_recv_byte(unsigned char ack_flag) { unsigned char i, dat 0; SDA_IN(); // 释放SDA,交给从机驱动 for(i 0; i 8; i) { dat 1; // 先左移,准备填入每一位 SCL_H(); // SCL拉高,从机输出数据 _delay_us(5); if(SDA_READ()) dat | 0x01; // 读取SDA电平 SCL_L(); // SCL拉低,通知从机输出下一位 _delay_us(5); } // 主机发送应答/非应答信号 if(ack_flag) { SDA_IN(); // 非应答:释放SDA保持高 } else { SDA_OUT(); SDA_L(); // 应答:拉低SDA } _delay_us(3); SCL_H(); // 应答时钟脉冲 _delay_us(5); SCL_L(); _delay_us(3); SDA_OUT(); SDA_H(); return dat; }3.2 EEPROM读写函数有了底层I2C驱动操作EEPROM就变成拼积木。写一个字节的函数// 向24C02指定地址写入一个字节 void eeprom_write_byte(unsigned int addr, unsigned char dat) { i2c_start(); // 起始条件 i2c_send_byte(0xA0); // 设备地址写标志(24C02, A0A1A2全接地) i2c_send_byte((unsigned char)addr); // 内部存储地址 i2c_send_byte(dat); // 写入数据 i2c_stop(); // 停止条件 _delay_ms(10); // 等待内部写周期完成 }这里要注意24C02内部地址是8位所以addr直接强制转换成unsigned char即可。如果换用24C08内部地址就变成10位需要额外处理块选择位。这也是为什么仿真用24C02起步更友好。读取函数需要两次通信过程// 从24C02指定地址读取一个字节 unsigned char eeprom_read_byte(unsigned int addr) { unsigned char dat; i2c_start(); // 起始条件 i2c_send_byte(0xA0); // 第一次:写操作,用于设置内部地址 i2c_send_byte((unsigned char)addr); // 发送内部地址 i2c_start(); // 重启起始条件(RESTART) i2c_send_byte(0xA1); // 第二次:设备地址读标志 dat i2c_recv_byte(1); // 接收数据,最后发送非应答 i2c_stop(); // 停止条件 return dat; }如果是连续读多个字节最后一个字节之前要发送应答最后一个字节发送非应答否则芯片不知道什么时候停止输出。页写函数同理写超过一页前要主动截断。代码里就把这些情况注释出来对初学者特别友好。3.3 主程序演示流程主程序做成一个完整的演示写测试数据、读回验证、通过UART输出到虚拟终端、LED闪烁指示运行状态。// // 主程序: 24C02读写测试 // 现象: 虚拟终端显示读写结果, PA0 LED闪烁 // #include avr/io.h #include util/delay.h // UART初始化: 9600bps, 8N1 void uart_init(void) { UCSRA 0x00; UCSRB (1 TXEN); // 使能发送 UCSRC (1 URSEL) | (1 UCSZ1) | (1 UCSZ0); UBRRL 51; // 8MHz晶振, 9600bps UBRRH 0x00; } void uart_send_char(unsigned char c) { while(!(UCSRA (1 UDRE))); // 等待发送缓冲区空闲 UDR c; } void uart_send_string(const char *s) { while(*s) { uart_send_char(*s); } } int main(void) { unsigned char test_data 0x5A; unsigned char read_data; DDRB 0xFF; // 调试用, 接LED灯 DDRA 0xFF; uart_init(); _delay_ms(100); uart_send_string(24C02 Test Start\r\n); // 写入数据 eeprom_write_byte(0x10, test_data); uart_send_string(Write addr0x10 dat0x5A\r\n); // 延时等待写周期 _delay_ms(10); // 读取数据 read_data eeprom_read_byte(0x10); if(read_data test_data) { uart_send_string(READ OK: 0x5A\r\n); PORTA 0x00; // 点亮LED, 表示成功 } else { uart_send_string(READ FAIL\r\n); PORTA 0xFF; } while(1) { PORTB ^ 0xFF; // PB口LED翻转, 示意程序在运行 _delay_ms(500); } }4. Proteus仿真实操与调试技巧4.1 电路搭建步骤与连线要点Proteus工程搭建从新建项目开始芯片选择ATmega16搜索关键词ATmega16元器件放置后开始连线。几个关键连接点ATmega16的VCC接5V电源GND接地AVCC也接5VAREF通过电容接地。24C02的VCC接5VGND接地A0、A1、A2全部接地。SCL接PC0SDA接PC1同时两个引脚都要通过4.7k电阻上拉到VCC。虚拟终端连接ATmega16的PD0RXD和PD1TXD注意交叉连接单片机TXD接虚拟终端RXD单片机RXD接虚拟终端TXD。PA0通过限流电阻接一个LED到GND做结果指示。上拉电阻是I2C总线正常工作的必要条件不能省。Open-Drain结构的协议里设备只能拉低电平高电平全靠上拉电阻提供。如果你看到SDA和SCL始终是低电平或者波形异常第一反应就查上拉。双击ATmega16芯片在Program File一栏选择编译生成的HEX文件设置Clock Frequency为8MHz。然后点击左下角运行按钮虚拟终端就会打印读写结果。4.2 用I2C调试器看总线数据我觉得这个项目里最值得花时间的地方是Proteus的I2C调试器。在左侧工具栏选择虚拟仪器模式列表里能找到I2C Debugger。把它放进原理图SCL接总线SCLSDA接总线SDA连接时注意方向即可。仿真运行时打开I2C调试器的窗口就能看到总线上每次通信的完整解析起始位、设备地址、读写方向、ACK应答、数据字节、停止位。这个工具的价值在于把抽象时序变成可读的协议帧哪怕你代码写得不对也能清楚看到芯片收到的是什么、你在哪一步少了应答。我建议初学者拿到代码后先不急着看读写结果就盯着I2C调试器里的数据对比时序图对着代码逐帧去理解。这个习惯养成了后面调试任何I2C传感器都会快很多。4.3 新手常见问题排查表结合我带学生做这个实验时遇到的高频问题整理成一张排查表现象可能原因解决办法虚拟终端无任何输出HEX文件未加载、串口接线反了检查单片机属性Program FileRXD/TXD交叉连接程序卡死在等待应答设备地址错误、上拉电阻遗漏核对A0A1A2接线补上4.7k上拉电阻读回数据全是0xFFSDA未接上拉、写时序不完整检查SDA上拉用I2C调试器逐帧对比时序24C02写不进去写周期延时不够增加_delay_ms(10)页写时数据错乱跨页回卷写操作前判断剩余页空间仿真运行极慢启用了过多调试工具关闭I2C调试器窗口提高仿真速度其中“程序卡死在等待应答”是最常见的现象。初学者要理解一个关键点如果设备地址是错误的芯片根本不会拉低SDA应答主机就会一直等。这时候打开I2C调试器看一眼设备地址是不是0xA0一眼就能定位问题。5. 仿真转实战的几个关键提醒5.1 代码移植到51或STM32的思路Proteus跑通后很多人想烧到真机上。代码移植的核心还是I2C时序只是GPIO操作方式变了。51单片机上用sbit定义SCL和SDA即可STM32则可以选择模拟I2C逻辑不变或者硬件I2C外设。建议第一次移植仍然用软件模拟I2C因为逻辑你已经跑通了只需要改引脚操作宏直接用硬件I2C反而容易踩配置坑。真机实验时上拉电阻必须接4.7k到10k都可以。供电电压要注意24C02支持1.8V到5.5V多数单片机是5V或者3.3V匹配好电平再连。5.2 从24C02换到24C08要注意什么仿真用24C02没毛病但如果你想实际用24C08要改两个地方。第一器件地址。24C08的地址中A1、A0位置变成了块选择位B1、B0内部1024字节分成4块每块256字节。比如要操作第0块地址0x00到0xFF设备地址仍可发0xA0要操作第1块地址0x100到0x1FF设备地址要改为0xA2。这个细节写代码的时候特别容易漏。第二页大小。24C02页大小8字节24C08页大小16字节。写缓冲超过页大小时回卷行为是相同的但页边界位置完全不同。代码里建议写一个通用的页写函数传入地址后自动计算跨页长度做成通用函数后换芯片只改宏定义。另外提一句Proteus里24C08的仿真模型常见版本行为等同于24C02所以你在仿真中换用24C08可能看不出差别这不代表真机也没差别真机换芯片时要严格按数据手册来。我自己的习惯是这个实验跑通后把I2C的启停、收发字节函数单独存成一个模块后面做温度传感器、RTC、OLED屏幕全都复用这套代码。I2C协议学会一次能受益很久。最后再分享个小技巧调试时把延时函数的时间放大十倍用Proteus的示波器或者I2C调试器去观察每一步动作会看得非常清楚比上来就全速跑效率高得多。本文还有配套的精品资源点击获取
返回列表