1、实验目的
实现的功能:系统运行时,数码管右 3 位显示 0,按 K1 键将数据写入到 EEPROM 内保存,按 K2 键读取 EEPROM 内保存的数据,按 K3 键显示数据加 1,按 K4 键显示数据清零,最大能写入的数据是 255。
(本次实验采用普中51实验开发板,用c语言编写)
2、实验前置知识
2.1 i2c协议介绍
i2c也叫IIC(Inter-Integrated Circuit)总线。
- I2C是一种串行通信总线(半双工通信方式),用于连接微控制器、传感器、存储器和其他外设。
- i2c是同步通信的一种特殊形式,具有接口线少,控制方式简单, 器件封装形式小,通信速率较高等优点。
- I2C 总线只有两根双向信号线。一根是数据线 SDA,另一根是时钟线 SCL。由于其管脚少,硬件实现简单,可扩展性强等特点,因此被广泛的使用在各大集成芯片内。
- 单工通信,半双工通信,全双工通信
- 串行数据线SDA: 负责在设备间传输串行数据
- 串行时钟线SCL:负责产生同步时钟脉冲
它们通常由上拉电阻连接到VCC供电线上。
2.2 i2c物理层
- 物理层的概述
在通信协议中物理层是通信协议的底层,主要负责传输原始比特流(bit stream),即在通信网络中传输0和1的电信号。它关注的是如何在物理媒介上传输数据,并确保数据从发送端到接收端能够可靠传输。物理层的主要任务包括:
- 传输介质的选择和规范:确定使用的传输介质(如光纤、同轴电缆、无线电波等),以及介质的物理特性(如传输速率、距离、噪声等)。
- 数据编码和调制:将数字数据转换成适合传输介质的模拟信号或数字信号,包括调制和解调过程。
- 数据的传输和同步:管理数据的传输顺序和时序,确保发送端和接收端的时钟同步,以便正确接收和解释数据。
I2C 通信设备常用的连接方式如下图所示:
- MCU:主机
- Device:从机
- i2c物理层特点:它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
2.2.1 总线结构
I2C总线的拓扑结构通常有两种主要形式:
- 单主机(一主多从):一种简单的结构,只有一个主控设备(通常是微控制器),它通过总线与多个从设备通信。
- 多主机(多主多从):多个主控设备可以在总线上发送和接收数据。这种结构需要一些协调机制,以避免冲突,如总线仲裁(bus arbitration)
注意:I2C总线内部使用漏极开路输出驱动器,因此SDA和SCL可以被拉低为低电平,但是无法被驱动为高电平,因此在每条线上都要使用一个上拉电阻,默认情况下需要置为高电平(本次实验使用的普中51——实验板中vcc已经连接了上拉电阻)
2.2.2 物理连接
总线线缆
- 串行数据线(SDA):双向数据传输线
- 串行时钟线(SCL):提供时钟信号
电气特性
I2C总线通常采用开漏或开集电路来实现逻辑电平的转换。这意味着在逻辑“低”时,设备将线拉低(低电平阈值),而在逻辑“高”时,设备将线释放(高阻态)(高电平阈值)。
起始条件与停止条件:SDA由高变低(SCL为高)为起始,SDA由低变高(SCL为高)为停止
总线仲裁:多个主设备同时发送起始条件时,通过SCL线的“线与”功能解决冲突(确保只有一个设备能够成功发送数据)
- 上拉电阻:确保总线在空闲时保持高电平
2.2.3设备地址
- 每个从设备在总线上都有一个唯一的7位或10位地址,用于主控设备选择要通信的目标设备。
2.3 i2c协议层
- I2C 协议层明确了通信过程
定义了通信的发送方(主机)与接收方(从机)角色、读写位、起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
2.3.1 数据有效性
- I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定
- I2C总线进行数据传送时,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
- 每次数据传输都以字节为单位,每次传输的字节数不受限制
2.3.2 起始和停止信号
- SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号
- SCL线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号
- 起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态
- 起始
void iic_start(void)
{
IIC_SDA=1;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=0; //当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
delay_10us(1);
}
- 终止
void iic_stop(void)
{
IIC_SDA=0;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=1; //当SCL为高电平时,SDA由低变为高
delay_10us(1);
}
2.3.3 数据传输
I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
应答响应
- 响应是什么
每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位(该校验位即为响应),这个校验位是接收端通过控制SDA(数据线)来实现的,以提醒发送端数据我这边已经接收完成,数据传送可以继续进行。
- 响应包括“应答(ACK)”和“非应答(NACK)”两种信号
应答:作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号即特定的低电平脉冲,发送方会继续发送下一个数据;
void iic_ack(void)//应答
{
IIC_SCL=0;
IIC_SDA=0; //SDA为低电平
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
非应答 :若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号即特定的高电平脉冲,发送方接收到该信号后会产生一个停止信号,结束信号传输。
时序图如下
- 非应答的应用举例
由于某种原因从机不对主机寻址信号应答时(如从机正在进行实时性的处理工作而无法接收总线上的数据),它必须将数据线(SDA)置于高电平,而由主机产生一个终止信号以结束总线的数据传送。
如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的“非应答”通知主机,主机则应发出终止信号以结束数据的继续传送。
当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放SDA线,以允许主机产生终止信号。
void iic_nack(void)
{
IIC_SCL=0;
IIC_SDA=1; //SDA为高电平
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
字节传输
每一个字节必须保证是 8 位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)
- 传输一个字节代码实现:
SCL低电平期间,从机将数据位依次放在SDA线上,(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据,在SCL高电平期间,SDA数据不允许发生变化,接收完成后,receive通过左移,方便读取下一个二进制位。依次循环8次,即可接收一个字节(主机在前接收前,需要释放SDA)。
for(i=0;i<8;i++ ) //循环8次将一个字节读出,先读高再传低位
{
IIC_SCL=0;
delay_10us(1);
IIC_SCL=1;
receive<<=1;
if(IIC_SDA) receive++;
delay_10us(1);
}
return receive;//返回接收的数据
总线的寻址方式
- I2C 总线寻址按照从机地址位数可分为两种: 7 位和10位
采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如下:
D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“ 0”时表示主机 向从机写数据,为“1”时表示主机由从机读数据。
10 位寻址和 7 位寻址兼容,而且可以结合使用。10 位寻址不会影响已有 的 7 位寻址,有 7 位和 10 位地址的器件可以连接到相同的I2C 总线。
以 7 位寻址为例进行介绍:
当主机发送了一个地址后,总线上的每个器件都将头 7 位与它自己的地址 比较,如果一样,器件会判定它被主机寻址,其他地址不同的器件将被忽略后面
的数据信号。至于是从机接收器还是从机发送器,都由 R/W 位决定的。
- 从机地址中可编程部分决定了可接入总线该类器件的最大数目
如一个从机的 7 位寻址位有 4 位是固定位,3 位是可编程位,这时仅能寻址 8 个同样的器件,即可以有 8 个同样的器件接入到该 I2C总线系统中
0 | 0 | 0 |
---|---|---|
0 | 0 | 1 |
0 | 1 | 0 |
0 | 1 | 1 |
1 | 0 | 0 |
1 | 0 | 1 |
1 | 1 | 0 |
1 | 1 | 1 |
即三个可编程位可以接入八个地址
数据传输的过程
在起始信号后必须传送一个从机的地址(7 位),第 8 位是数据的传送方向位(R/W),用“0”表示主机发送(写)数据(W),“1”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
在总线的一次数据传送过程中,可以有以下几种组合方式
- 1)主机向从机发送数据,数据传送方向在整个传送过程中不变
注意:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S表示起始信号,P 表示终止信号
-
2)主机在第一个字节后,立即从从机读数据
-
3)在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好相反
在I2C-EEPROM实验中24c02.c中的代码可以体现该性质:
iic_start();
//1)命令
iic_write_byte(0XA0);//对应写入地址
iic_wait_ack();//等待应答
//2)地址
iic_write_byte(addr);//指定写入数据的地址
iic_wait_ack();//等待应答
//通信方向切换
iic_start();
//进入数据接收模式
iic_write_byte(0XA1);//对应读取地址
iic_wait_ack();//等待应答```
2.3.4 同步与速度
时钟同步:从机根据SCL信号调整自身数据传输速率
共有三种模式:
- 标准模式(100kbps)
- 快速模式(400kbps)
- 高速模式(3.4Mbps,需特殊硬件支持)
2.3.5 错误处理
- 仲裁丢失:检测到SCL电平与自身输出不符时,自动转为从机或停止通信
- 数据完整性检查:通过应答位和停止条件检测
2.4 i2c应用实例
- 传感器应用
温度传感器:例如DS18B20或DHT系列温湿度传感器,这些传感器通过I2C接口与微控制器通信,提供环境温度和湿度数据。 - 存储器和存储设备
EEPROM:比如这次实验涉及的AT24C02芯片,用于存储设备配置数据、参数和校准信息。 - 显示器和LCD控制器
字符型LCD:如HD44780,通过I2C适配器模块与微控制器通信,简化了控制信号线的使用,减少了连接线的数量。 - 音频设备
音频编解码器:例如WM8960,通过I2C控制音频输入和输出的增益、等化器设置以及数字/模拟转换器的控制。 - 工业控制和自动化
PLC(可编程逻辑控制器):PLC中的各种模块可以通过I2C进行数据交换和通信,例如输入输出模块、温度传感器模块等。
i2c的简要介绍结束
2.5 AT24C02 介绍
EEPROM(Electrically Erasable Programmable Read-OnlyMemory)是一种非易失性存储器,可以通过电气信号进行写入和擦除操作,而无需移除芯片或使用紫外线。
AT24C01/02/04/08/16…是一个 1K/2K/4K/8K/16K 位串行 CMOS,内部含有128/256/512/1024/2048 个 8 位字节,AT24C01 有一个 8 字节页写缓冲器, AT24C02/04/08/16有一个 16 字节页写缓冲器。
2.5.1 特点
- 容量:AT24C02是一款2K位(256字节)容量的串行EEPROM芯片。
- 接口:使用标准的I2C(Inter-Integrated Circuit)串行总线接口通信。
- 速度:支持标准模式(100 kHz)和快速模式(400 kHz)的I2C通信速率。
- 供电电压:工作电压范围广泛,通常支持从2.5V到5.5V的电压供应。
- 封装:提供多种封装选项,例如DIP(双列直插式)、SOIC(表面安装)、TSSOP(超薄小尺寸)等。
- 数据保持性:数据可以在供电断开后长时间保持,典型保存时间超过100年。
- 写入耐久性:支持超过百万次的写入(擦写)次数,具有良好的数据可靠性和耐用性。
- 自动地址增量:支持连续写入和读取操作,能够自动增量地址,简化数据存储和检索过程。
2.5.2 芯片引脚
AT24C02 器件地址为 7 位,高 4 位固定为 1010,低 3 位由 A0/A1/A2 信 号线的电平决定。
因为传输地址或数据是以字节为单位传送的,当传送地址时, 器件地址占 7 位,还有最后一位(最低位 R/W)用来选择读写方向,它与地址无关。
格式如下:
开发板将芯片的 A0/A1/A2 连接到 GND,所以器件地址为 1010000,即0x50(未计算最低位)。如果要对芯片进行写操作时,R/W 即为 0, 写器件地址即为 0XA0;如果要对芯片进行读操作时,R/W 即为1,此时读器件 地址为 0XA1。开发板上也将 WP 引脚直接接在 GND 上,此时芯片允许数据正常读写
2.5.3 器件寻址
- 器件地址信息由"0","1"序列组成,对于所有串行EEPROM都是一样的
- 对于24c02/32/64,随后三位A2、A1、A0为器件地址位,必须和硬件输入引脚保持一致
- 器件地址信息的LSB为读/写操作选择位,高为读操作,低为写操作
- 若比较地址一致,EEPROM将输出应答“0”。如果不一致则返回到待机状态
2.5.4 器件操作
待机操作:
- EEPROM具有低功耗待机的特点
- 待机条件
1)电源上电
2)接收停止条件及完成任何内部操作后
存储复位
- 当协议中产生中断,掉电,系统复位后,i2c总线可以通过以下操作复位
1)产生9个时钟周期
2)当SCL为高时,SDA也置为高
3)产生一个起始条件
void i2c_reset() {
unsigned char i;
// 1)产生9个时钟周期
for (i = 0; i < 9; i++) {
SCL = 1; // SCL置高
delay(); // 等待一段时间,保证时钟周期
SCL = 0; // SCL置低,产生一个时钟周期
delay(); // 再次等待
}
// 2)当SCL为高时,SDA也置为高
SDA = 1;
delay(); // 等待一段时间
// 3)产生一个起始条件
SCL = 1;
delay(); // SCL为高,形成起始条件
SDA = 0; // 起始条件中,SDA由高变低
delay(); // 等待一段时间
SCL = 0; // SCL变低,保持起始条件
delay(); // 再次等待
}
写操作
字节写(本次实验采用了字节写):适合写入少量数据
- 操作步骤:
1)发送启动条件。
2)发送EEPROM的设备地址和写入标志(R/W = 0)。
3)发送要写入的内存地址。
4)发送要写入的数据字节。
5)发送停止条件。
页写:适合写入大量数据/连续数据
- 操作步骤:
1)发送启动条件。
2)发送EEPROM的设备地址和写入标志(R/W = 0)。
3)发送要写入的内存地址的起始地址。
4)依次发送要写入的多个数据字节,最多可以写入一页(8字节)的数据。
5)发送停止条件。
本次实验代码涉及到写操作
void at24c02_write_one_byte(u8 addr,u8 dat)//EEPROM指定地址addr传入数据 dat
{
iic_start();
//1)命令
iic_write_byte(0XA0);//写入命令地址
iic_wait_ack();//等待应答
//2)地址
iic_write_byte(addr);//指定写入数据的地址
iic_wait_ack();//等待应答
//3)数据
iic_write_byte(dat);//传输一个字节的数据
iic_wait_ack();//等待应答
//传输指令完成,结束本次通信
iic_stop();//产生停止条件,结束通信
delay_10us(10);
//等待 EEPROM 完成写入操作,以确保数据写入是稳定的和可靠的。
}
- 应答查询
当内部写周期启动,EEPROM输入无效,此时即可启动应答查询:发送起始位置和器件地址。只有当内部写周期结束,EEPROM才应答“0”。之后可进行下次读/写操作
芯片手册的流程图
读操作
分为三种:当前地址读、随机读、顺序读
1)当前地址读
指从指定的内存地址读取数据,可以用于逐个读取存储器中的数据
2)随机读
随机读取允许从EEPROM中的任意地址开始读取数据,而不需要每次都重新指定地址,提高了读取的灵活性和效率。
3)顺序读
顺序读取允许从EEPROM的起始地址开始顺序地读取多个连续的字节数据,非常适合于读取大块数据或者数据块的连续访问。
操作步骤(以当前地址读举例)
- 发送启动条件。
- 发送EEPROM的设备地址和写入标志(R/W = 0)。
- 发送要读取的内存地址。
- 发送重复启动条件。
- 发送EEPROM的设备地址和读取标志(R/W = 1)。
- 读取从EEPROM返回的数据。
- 发送停止条件。
实验的读操作代码
u8 at24c02_read_one_byte(u8 addr)
{
u8 temp=0;
iic_start();
//1)命令
iic_write_byte(0XA0);//对应写入地址
iic_wait_ack();//等待应答
//2)地址
iic_write_byte(addr);//指定写入数据的地址
iic_wait_ack();//等待应答
//通信方向切换
iic_start();
//进入数据接收模式
iic_write_byte(0XA1);//对应读取地址
iic_wait_ack();//等待应答
//获取数据
temp=iic_read_byte(0);//读取一个字节
iic_stop();//产生停止条件,结束本次通信
return temp;//返回读取的数据
}
2.5.5 存储结构
I2C 总线时序图如下
2.6 独立按键介绍
概述
按键是一种电子开关,使用时轻轻按开关按钮就可使开关接通,当松开手时, 开关断开。开发板上使用的按键及内部简易图如下图所示:
通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,电压信号 如下图所示:
由于机械点的弹性作用,按键开关在闭合时不会马上稳定的接通,在断开时也不会一下子断开,因而在闭合和断开的瞬间均伴随着一连串的抖动。抖动有可能会造成误读的情况(确保 CPU 对按键的一次闭合仅作一次处理),因此我们需要去消抖。
消抖的分类
为了简化电路,我们通常采用软件消抖。(本次实验采用软件消抖)
软件消抖操作步骤
1)先设置 IO 口为高电平(由于开发板 IO 都有上拉电阻,所以默认 IO 为高 电平)
2)读取 IO 口电平确认是否有按键按下。if(mode)
3)如有 IO 电平为低电平后,延时几个毫秒。delay_10us(1000)
4)再读取该 IO 电平,如果仍然为低电平,说明按键按下。if(KEY1_PRESS)
5)执行按键控制程序
独立按键模块原理图
从上图中可以看出,4 个独立按键的控制管脚连接到 51 单片机的 P3.0-P3.3 脚上。其中 K1 连接在 P3.1 上,K2 连接在P3.0 上,K3 连接在 P3.2 上,K4 连接 在 P3.3 上。4 个按键另一端全部连接在 GND,当按键按下后,对应 IO口即为低电平。
本次实验参考代码:
u8 key_scan(u8 mode)
{
static u8 key=1;
if(mode) key=1;//连续扫描按键
if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))
{
delay_10us(1000);//消抖
key=0;
//如果有按键按下-->返回对应键值
if(KEY1==0) return KEY1_PRESS;
else if(KEY2==0) return KEY2_PRESS;
else if(KEY3==0) return KEY3_PRESS;
else if(KEY4==0) return KEY4_PRESS;
}
else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) key=1; //无按键按下
return KEY_UNPRESS;
}
2.7 动态数码管介绍
- 位选:指选择要显示的数码管的位置。当有多个数码管(如4位数码管)时,需要通过位选信号依次选中每个数码管,以便向其发送正确的数码数据。
- 段选:指控制数码管每个数字的显示,例如选择显示数字的哪些段(如a、b、c、d、e、f、g等)。不同的段选码对应于数码管显示不同的数字或字符。
概述
通常是指通过控制多个数码管的显示内容,以实现动态变化的效果。一般来说,使用7段数码管作为显示元件,通过逐个刷新每个数码管的显示内容,从而呈现出连续的数字、字符或者其他符号。(动态显示,就是利用减少段选线,分开位选线,利用位选线不同时选择通断,改变段选数据来实现的)
原理:利用人眼视觉的暂留效应。
优点:
- 节省IO口资源:通过逐位刷新显示,可以减少需要的IO口数量。
- 显示灵活:可以实现多种动态显示效果,如计数器、时钟、计时器等。
- 节约功耗:与直接驱动多个数码管相比,动态显示只需驱动一个数码管的功率。
2 个 4 位一体的共阴数码管的位选线有 8 根,直接让单片机 IO 口控制是没有任何问题的,但考虑到 51 单片机 IO口资源的限制,那么,如何通过少量IO口工作控制多个LED呢,这里我们简要介绍两个外部驱动芯片:74HC245 和 74HC138
74HC245芯片
- 74HC245 是一种三态输出、八路信号收发器,主要应用于大屏显示,以及其它的消费类电子产品中增加驱动。
(1)主要特性
①采用 CMOS 工艺
②宽电压工作范围:3.0V-5.0V
③双向三态输出
④八线双向收发器
⑤封装形式:SOP20、SOP20-2、TSSOP20、DIP20
(2)管脚功能定义
(3)功能真值表
从上面的管脚功能定义说明及真值表可以知道该芯片使用方法很简单,给 OE 使能管脚低电平,DIR 管脚为高电平传输方向是 A->B输出,DIR 为低电平传输方 向是 B->A,至于输出高电平还是输出低电平取决于输入端的状态,如果输入为 低电平,输出即为低;输入为高电平,输出即为高。如果 OE 使能管脚为高电平, 不论 DIR 管脚是高还是低,输出是高组态。
74HC138芯片
- 74HC138是一种集成数字电路,属于74HC系列(74HC逻辑家族)的一员。它是一个3-8译码器/解码器,用于将一个三位地址码转换为八个输出线的有效逻辑低电平,主要应用于消费类电子产品。
(1)主要特性
①采用 CMOS 工艺
②低功耗
③工作电压:3.0V-5.0V
④封装形式:SOP16
(2)管脚功能定义
(3)功能真值表
文档的方法总结:
A0、A1、A2 输入就相当于 3 位 2 进制数,A0 是 低位,A1 是次高位,A2 是高位。而 Y0-Y7具体哪一个输出有效电平,就看输入二进制对应的十进制数值。比如输入是 101(A2,A1,A0),其对应的十进制数 是 5,所以 Y5输出有效电平(低电平)。
动态数码管模块原理图如下
消影
- 动态数码管消影——可以参考大佬的博客(详细解释了为什么要消影、如何消影) 51单片机动态数码管及其消影问题
这里做简单陈述: - 数码管显示原理是由位选和段选交替实现的(段选——位选——段选——位选——段选……)
“影”的产生:位选转换为段选过程中,由于还未把新的段选数据传输到端口,因此当前数码管显示的还是上一个段选的数据——>这段时间我们称为“空白时间”,也就是我们所说的“影”。
- 消影:
1)利用延时函数消影(不完全消影)delay_10us(100)
2)重置段选段码(完全消影)SMG_A_DP_PORT=0x00
本次实验参考代码
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
void smg_display(u8 dat[],u8 pos)
{
u8 i=0;
u8 pos_temp=pos-1;
for(i=pos_temp;i<8;i++)
{
switch(i)
{
case 0:LSC=1;LSB=1;LSA=1;break;
case 1:LSC=1;LSB=1;LSA=0;break;
case 2:LSC=1;LSB=0;LSA=1;break;
case 3:LSC=1;LSB=0;LSA=0;break;
case 4:LSC=0;LSB=1;LSA=1;break;
case 5:LSC=0;LSB=1;LSA=0;break;
case 6:LSC=0;LSB=0;LSA=1;break;
case 7:LSC=0;LSB=0;LSA=0;break;
}
SMG_A_DP_PORT=gsmg_code[dat[i-pos_temp]];//传送段选数据
delay_10us(100);//延时,等待显示稳定
SMG_A_DP_PORT=0x00;//消影
}
}
至此,本次实验的前置知识介绍到这里。
3、硬件实现
本次实验用到了多个硬件资源
(1)独立按键(K1-K4)
(2)动态数码管
(3)EEPROM 模块电路(AT24C02)
单片机通过GPIO模拟I2C总线与EEPROM通信连接图
+--------------+ +-----------------+
| 单片机 | | I2C EEPROM |
| | | |
| P1.0 (SCL) -----> SCL |
| P1.1 (SDA) -----> SDA |
| | | |
| Vcc -------------> Vcc |
| GND -------------> GND |
+--------------+ +-----------------+
注意:本次实验使用的是普中51实验开发板,开发板单片机 IO 都外接了 10K 上拉电阻,当单片机 IO 口连接到芯片的 SCL 和 SDA脚时即相当于它们外接上拉电阻。
4、软件实现
由于用到了多个硬件资源,我们创建多文件工程——为了方便日后工程管理、程序移植和维护、易读等。
- App 文件夹:用于存放外设驱动文件,如 LED、数码管、定时器等。
- Obj 文件夹:用于存放编译产生的 c/汇编/链接的列表清单、调试信息、 hex
文件、预览信息、封装库等文件。 - Public 文件夹:用于存放 51 单片机公共的文件,如延时、51 头文件、变量
类型重定义等。 - User 文件夹:用于存放用户主函数文件,如 main.c。
Public
public.h
#ifndef _public_H
#define _public_H
#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
void delay_10us(u16 ten_us);
void delay_ms(u16 ms);
#endif
public.c
#include "public.h"
/*******************************************************************************
* 函 数 名 : delay_10us
* 函数功能 : 延时函数,ten_us=1时,大约延时10us
* 输 入 : ten_us
* 输 出 : 无
*******************************************************************************/
void delay_10us(u16 ten_us)
{
while(ten_us--);
}
/*******************************************************************************
* 函 数 名 : delay_ms
* 函数功能 : ms延时函数,ms=1时,大约延时1ms
* 输 入 : ms:ms延时时间
* 输 出 : 无
*******************************************************************************/
void delay_ms(u16 ms)
{
u16 i,j;
for(i=ms;i>0;i--)
for(j=110;j>0;j--);
}
APP
1)24c02
24c02.h
#ifndef _24c02_H
#define _24c02_H
#include "public.h"
void at24c02_write_one_byte(u8 addr,u8 dat);//向 AT24C02 EEPROM 指定地址写入一个字节的数据
u8 at24c02_read_one_byte(u8 addr);// 从 AT24C02 EEPROM 指定地址读入一个字节的数据
#endif
24c02.c
#include "24c02.h"
#include "iic.h"
//向 AT24C02 EEPROM 写入一个字节的数据
void at24c02_write_one_byte(u8 addr,u8 dat)//EEPROM指定地址addr传入数据 dat
{
iic_start();
//1)命令
iic_write_byte(0XA0);//对应写入地址
iic_wait_ack();//等待应答
//2)地址
iic_write_byte(addr);//指定写入数据的地址
iic_wait_ack();//等待应答
//3)数据
iic_write_byte(dat);//传输一个字节的数据
iic_wait_ack();//等待应答
//传输指令完成,结束本次通信
iic_stop();//产生停止条件,结束通信
delay_10us(10);
//等待 EEPROM 完成写入操作,以确保数据写入是稳定的和可靠的。
}
//从 AT24CXX 指定地址读出一个数据
u8 at24c02_read_one_byte(u8 addr)
{
u8 temp=0;
iic_start();
//1)命令
iic_write_byte(0XA0);//对应写入地址
iic_wait_ack();//等待应答
//2)地址
iic_write_byte(addr);//指定写入数据的地址
iic_wait_ack();//等待应答
//通信方向切换
iic_start();
//进入数据接收模式
iic_write_byte(0XA1);//对应读取地址
iic_wait_ack();//等待应答
//获取数据
temp=iic_read_byte(0);//读取一个字节
iic_stop();//产生停止条件,结束本次通信
return temp;//返回读取的数据
}
2)iic(集成电路总线)
iic.h
#ifndef _iic_H
#define _iic_H
#include "public.h"
//定义EEPROM控制脚
sbit IIC_SCL=P2^1;//SCL时钟线
sbit IIC_SDA=P2^0;//SDA数据线
//IIC所有操作函数
//发送IIC开始信号
void iic_start(void);
//发送IIC停止信号
void iic_stop(void);
//IIC发送一个字节
void iic_write_byte(u8 txd);
//IIC读取一个字节
u8 iic_read_byte(u8 ack);
//IIC等待ACK信号
void iic_wait_ack(void);
//IIC发送ACK信号
void iic_ack(void);
//IIC不发送ACK信号
void iic_nack(void);
#endif
iic.c
#include "iic.h"
//产生IIC开始信号
void iic_start(void)
{
IIC_SDA=1;
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=0;//在时钟线SCL为高电平时,数据线SDA由高变低
delay_10us(1);
IIC_SCL=0;//钳住I2C总线,准备发送或接受数据
}
//产生IIC停止信号
void iic_stop(void)
{
IIC_SDA=0;
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=1;//当SCL为高电平时,SDA由低变高
delay_10us(1);//为什么这里还要延时,等待数据稳定??
}
//产生ACK应答
void iic_ack(void)
{
IIC_SCL=0;
IIC_SDA=0;//置数据线为低电平,说明接受端希望对方继续发送数据
//接收端接收一个字节的数据
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
//产生NACK非应答
void iic_nack(void)
{
IIC_SCL=0;
IIC_SDA=1;//置数据线为低电平,说明接受端希望对方结束数据传输
//接收端接收一个字节的数据
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
//发送后等待接收端应答
//Q:为什么不能void:
//I2C通信中,等待从设备的应答信号后
//需要知道是否成功接收到应答
u8 iic_wait_ack(void)
{
u8 time_temp=0;
//此时如果没有应答,主机主动结束数据传输
//因此这里使用了IIC的stop信号
IIC_SCL=1;
while(IIC_SDA)
{
time_temp++;
if(time_temp>100)//超过时间限制强制结束IIC通信
{
iic_stop();
return 1;
}
}
IIC_SCL=0;//时钟线置低电平,希望数据继续传输
return 0;
//return问题:
//1 表示未收到应答信号,0 表示收到了应答信号(取决于SDA的高低电平)
}
//通过I2C总线(或称为IIC总线)向外部设备写入一个字节数据的函数
void iic_write_byte(u8 dat)//dat保存了一个字节的数据
{
u8 i=0;
IIC_SCL=0;//确保时钟线为低电平
for(i=0;i<8;i++)//循环8次将一个字节传出,从高位到低位
{
if((dat&0x80)>0)//0x80为 1000 0000 用于判断最高为是否为1
IIC_SDA=1;//保持空闲状态,不发送当前数据位
else
IIC_SDA=0;//发送当前数据位4
dat<<=1;// 将数据左移一位,下一个循环处理下一个数据位
//数据传输
delay_10us(1);
IIC_SCL=1;//数据线(SDA)上的数据可以被接收或发送
delay_10us(1);
IIC_SCL=0;//数据传输完成或准备发送下一个数据位
}
}
//I2C总线上读取一个字节
u8 iic_read_byte(u8 ack)//需要返回读取的数据,因此用u8
{
u8 i=0,receive=0;
for(i=0;i<8;i++)//遍历一个字节
{
IIC_SCL=0;//允许接收下一个数据位,时钟周期开始
delay_10us(1);//延时,等待时钟稳定
IIC_SCL=1;//时钟周期结束,该数据位已经读入
receive<<=1;//最多左移七次
if(IIC_SDA) receive++;//最低位加1
delay_10us(1);
}
if(!ack) IIC_SDA=1;//如果ack为0,发送不应答信号
else IIC_SDA=0;//发送应答信号
return receive;//返回已经读好的数据
}
3)key(独立按键)
key.h
#ifndef _key_H//防止头文件被重复包含,避免引起编译错误
#define _key_H //#define”关键字定义上面判断的标号“ _key_H
#include "public.h"
//定义独立按键
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0
u8 key_scan(u8 mode);
#endif//从#inf…到这里都被包含
key.c
#include "key.h" #include "key.h"
u8 key_scan(u8 mode)
{
static u8 key=1;
if(mode) key=1;//连续扫描按键
if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))
{
delay_10us(1000);//消抖
key=0;
if(KEY1==0) return KEY1_PRESS;
else if(KEY2==0) return KEY2_PRESS;
else if(KEY3==0) return KEY3_PRESS;
else if(KEY4==0) return KEY4_PRESS;
}
else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) key=1; //无按键按下
return KEY_UNPRESS;
}
4)smg(数码管)
smg.h
#ifndef _smg_H
#define _smg_H
#include "public.h"
//使用宏定义数码管断码口
#define SMG_A_DP_PORT P0
//定义数码管位选信号控制脚
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;
void smg_display(u8 dat[],u8 pos);
#endif
smg.c
#include "smg.h"
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
void smg_display(u8 dat[],u8 pos)
{
u8 i=0;
u8 pos_temp=pos-1;
for(i=pos_temp;i<8;i++)
{
switch(i)
{
case 0:LSC=1;LSB=1;LSA=1;break;
case 1:LSC=1;LSB=1;LSA=0;break;
case 2:LSC=1;LSB=0;LSA=1;break;
case 3:LSC=1;LSB=0;LSA=0;break;
case 4:LSC=0;LSB=1;LSA=1;break;
case 5:LSC=0;LSB=1;LSA=0;break;
case 6:LSC=0;LSB=0;LSA=1;break;
case 7:LSC=0;LSB=0;LSA=0;break;
}
SMG_A_DP_PORT=gsmg_code[dat[i-pos_temp]];//传送段选数据
delay_10us(100);//延时,等待显示稳定
SMG_A_DP_PORT=0x00;//消音
}
}
User
main.c
//数码管右 4 位显示 0??,按 K1 键将数据写入到 EEPROM 内保存,
//按 K2 键读取 EEPROM 内保存的数据,按 K3 键显示数据加 1,按 K4 键显示数据清零
#include "public.h"
#include "24c02.h"
#include "key.h"
#include "smg.h"
#define EEPROM_ADDRESS 0
void main()
{
u8 key_temp=0;
u8 save_value=0;
u8 save_buf[3];
while(1)
{
key_temp=key_scan(0);//扫描一次
if(key_temp==KEY1_PRESS)
{
at24c02_write_one_byte(EEPROM_ADDRESS,save_value);//将保存数据写入 EEPROM
}
else if(key_temp==KEY2_PRESS)
{
save_value=at24c02_read_one_byte(EEPROM_ADDRESS);//从 EEPROM 读取一个字节的数据
}
else if(key_temp==KEY3_PRESS)
{
save_value++;
if(save_value==255) save_value=255;//三位数码管显示上限是255
}
else if(key_temp==KEY4_PRESS)
{
save_value=0;//清除保存数据
}
//存入已保存数据
save_buf[0]=save_value/100;
save_buf[1]=(save_value%100)/10;
save_buf[2]=(save_value%100)%10;
//显示数码管
smg_display(save_buf,6);
}
}
5、实验现象
系统运行时,数码管右 3 位显示 0,按 K1 键将数据写入到 EEPROM 内保存,按 K2 键读取 EEPROM 内保存的数据,按 K3键显示数据加 1,按 K4 键显示数据清零,最大能写入的数据是 255。
i2c-eeprom
总结:本篇博客我们通过i2c总线控制eeprom实验,学习了i2c协议(物理层和协议层)、at24c02芯片、独立按键、动态数码管的相关知识,并学会运用硬件资源完成实验的搭建,利用软件实现实验的功能。
这是作者第一篇51单片机博客,希望对大家的学习有所帮助,如果内容有误/知识点有遗漏请可以及时指出。希望大家多多支持!