【51单片机系列】EEPROM-IIC模块
本文是关于IIC(I2C)通信协议的相关内容。
一、 I2C介绍
I2C,Inter-Integrated Circuit总线是由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备,是微电子通信控制领域广泛采用的一种总线标准。
I2C是同步通信的一种特殊形式,具有接口线少,控制方式简单,器件封装形式小,通信速率较高等优点。
I2C总线只有两根双向信号线,一根是数据线SDA,一根是时钟线SCL。
由于I2C管脚少,硬件实现简单,可扩展性强等特点,因此被广泛使用在各大集成芯片内。
1.1、I2C总线常用术语
主机:启动数据传送并产生时钟信号的设备。
从机:被主机寻址的器件。
多主机:同时有多于一个主机尝试控制总线但不破坏传输;
主模式:用I2CNDAT支持自动字节计数的模式;位I2CRM,I2CSTT,I2CSTP控制数据的接收和发送。
从模式:发送和接收操作都是由I2C模块自动控制的。
仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并使传输不被破坏的过程。
同步:两个或多个器件同步时钟信号的过程。
发送器:发送数据到总线的器件。
接收器:从总线接收数据的器件。
1.2、I2C物理层
I2C通信设备常用的连接方式如下所示:
I2C的物理层有如下特点:
① 它是一个支持多设备的总线。总线指多个设备共用的信号线。在一个I2C通信总线中,可以连接多个I2C通信设备,支持多个通信主机及多个通信从机。
② 一个I2C总线只使用两条总线线路,一条双向串行数据线SDA,一条串行时钟线SCL。数据线即用来表示数据,时钟线用于数据收发同步。
③ 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
④ 总线通过上拉电阻连接到电源。当I2C设备空闲时,会输出高阻态;当所有设备都空闲时,都输出高阻态,由上拉电阻把总线拉成高电平。
⑤ 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定哪个设备占用总线。
⑥ 具有三种传输模式:标准模式传输速率为100kbit/s、快速模式传输速率为400kbit/s、高速模式传输速率为3.4Mbit/s,但目前大多I2C设备上不支持高速模式。
⑦ 连接到相同总线的IC数量受到总线的最大电容400pF限制。
1.3、I2C协议层
I2C的协议定义了通信的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
(1) 数据有效性规定
I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。每次数据传输都以字节为单位,每次传输的字节数不受限制。如下图所示:
(2) 起始和停止信号
时钟线SCL为高电平期间,数据线SDA由高电平向低电平变化表示起始信号。
时钟线SCL为高电平期间,数据线SDA由低电平向高电平变化表示终止信号。
如下图:
起始和终止信号都是由主机发出的。
在起始信号产生后,总线处于被占用的状态。
在终止信号产生后,总线处于空闲状态。
(3) 应答响应
当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位。这个校验位是接收端通过控制数据线SDA来实现的,以提醒发送端“数据已经接收完成,数据传送可以继续进行”。
这个校验位是数据或地址传输过程中的响应。响应包括应答ACK和非应答NACK两种信号。
作为数据接收端时,当设备接收到I2C传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送应答ACK信号,即特定的低电平脉冲,发送方接收到该信号会继续发送下一个数据。若接收端希望结束数据传输,则向对方发送非应答ACK信号,即特定的高电平脉冲,发送方接收到该信号后会产生一个停止信号,结束信号传输。
应答响应时序图如下:
每一个字节必须保证是8位长度。数据传送时,先传送最高位MSB,每一个被传送的字节后面都必须跟随一位应答位,即一帧共有9位。
由于某种原因从机不对主机寻址信号应答时,如从机正在进行实时性的处理工作而无法接收总线上的数据时,它必须将数据线SDA置为高电平,由主机产生一个终止信号来结束总线的数据传送。
如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的非应答通知主机,主机则应发出终止信号以结束数据的继续传送。
当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的非应答来实现的。然后从机释放SDA线以允许主机产生终止信号。
这些信号中,起始信号是必需的,终止信号和应答信号都可以省略。
(4) 总线的寻址方式
I2C总线寻址按照从机地址位数可分为两种,一种是7位,另一种是10位。
采用7位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如下:
D7-D1位组成从机的地址。D0位是数据传送方向位,D0=0表示主机向从机写数据,D0=1表示主机由从机读数据
10位寻址和7为寻址兼容,且可以结合使用。10位寻址不会影响已有的7位寻址,有7位和10位地址的器件可以连接到相同的I2C总线。
以7位寻址为例,当主机发送了一个地址后,总线上的每个器件都将头7位与它自己的地址比较,如果一样,器件会判定它被主机寻址,其它地址不同的器件将被忽略后面的数据信号。
R/W决定从机是接收端还是发送端。
从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。比如一个从机的7位寻址位有4位是固定位,3位是可编程位,这时仅能寻址8个同样的器件,即可以有8个同样的器件介入到该I2C总线系统中。
(5) 数据传输
I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向(R/W),0表示主机发送(写)数据,1表示主机接收数据(读)。
每次数据传送由主机产生的终止信号结束。但是,如果主机希望继续占用总线进行新的数据传送,可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
在总线的一次数据传送过程中,可以有以下几种组合方式:
a. 主机向从机发送数据,数据传送方向在整个传送过程中不变。
阴影部分表示数据由主机向从机发送,无阴影部分表示数据由从机向主机传送。A表示应答,非A表示非应答(高电平),S表示起始信号,P表示终止信号。
b. 主机在第一个字节后,立即从从机读数据。
c. 在传送过程中,当需要改变传送方向时,起始信号和从机地址都要重复产生一次,但两次读/写方向位正好相反。
以上就是I2C总线。51单片机没有硬件IIC接口,即使有接口通常也是采用软件模拟I2C。主要原因是硬件IIC设计的比较复杂,而且稳定性不怎么号好,程序移植比较麻烦,用软件模拟IIC最大的好处是移植方便。
二、 AT24C02芯片介绍
AT24C01/02/04/08/16是一个1K/2K/4K/8K/16K的串行CMOS,内部含有128/256/512/1024/2048个8位字节,AT24C02有一个8字节页缓冲器,AT24C02/04/08/16有一个16字节页缓冲器。
该器件通过I2C总线接口进行操作,有一个专门的写保护功能。
以AT24C02芯片为例,该芯片具有I2C通信接口,芯片内保存的数据在掉电情况下都不丢失,芯片管脚及外观如下图:
芯片管脚说明:
管脚号 | 管脚名称 | 功能说明 |
---|---|---|
1/2/3 | A0/A1/A2 | 地址输入,A2、A1和A0是器件地址输入引脚。 24C02/32/64使用A2、A1、A0输入引脚作为硬件地址,总线上课同时级联8个24C02/32/63器件。 24C04使用A2和A1输入引脚作为硬件地址,总线上可同时级联4个24C04器件,A0位空脚,可接地。 24C08使用A2输入引脚作为硬件地址,总线上可同时级联2个24C08器件,A0和A1为空脚,可接地。 24C16未使用器件地址引脚,总线上最多只可连接一个16K器件,A2、A1和A0为空脚,可接地。 |
5 | SDA | 串行地址和数据输入/输出,SDA是双向串行数据传输引脚,漏极开路,需外接上拉电阻到VCC,典型值10kΩ。 |
6 | SCL | 串行时钟输入,SCL同步数据传输,上升沿数据写入,下降沿数据读出。 |
7 | WP | 写保护,WP引脚提供硬件数据保护。当WP接地时,允许数据正常读写操作;当WP接VCC时,写保护,只读。 |
4 | GND | 地 |
8 | VCC | 正电源 |
AT24C02器件地址为7位,高4位固定是1010,低3位由A0/A1/A2信号线的电平决定。因为传输地址或数据是以字节为单位传送的,当传送地址时,器件地址占7位,还有最后一位(最低位R/W)用来选择读写方向,它与地址无关,格式如下:
如果将A0/A1/A2连接到GND,那么器件地址为1010000,。如果要对芯片进行写操作时,R/W=0,写器件地址即为0xA0;如果要对芯片进行读操作时,R/W=1,读器件地址为0xA1。
如果将WP连接到GND,此时芯片允许数据正常读写。
I2C总线时序如下图所示:
三、 I2C使用示例
本示例实现的功能:系统运行时,数码管后4为显示0,按K1将数据写入到EEPROM内保存,按K2读取EEPROM内保存的数据,按K3显示数据加1,按K4显示数据清零,最大能写入的数据是255。
proteus中仿真使用到的资源有:四个独立按键、动态数码管、EEPROM模块电路。连接如下图所示,其中EEPROM模块A2/A1/A0连接低,表示当前EEPROM的地址为1010000;WP接地关闭写保护,可以正常读/写数据;SCL和SDA分别连接单片机的P2.1和P2.0口。
软件设计,首先编写I2C通信相关代码,如下:
i2c.h
内容如下:
#ifndef __I2C_H_
#define __I2C_H_
#include "typedef.h"
/*************************************************************************
* 函数名: I2CStart
* 函数功能: I2C起始信号,SCL高电平期间,SDA产生一个下降沿。
* 起始信号后SDA和SCL都为0
* 输入: void
* 输出: void
**************************************************************************/
void I2CStart();
/*************************************************************************
* 函数名: I2CStop
* 函数功能: I2C终止信号,SCL高电平期间,SDA产生一个上升沿。
* 终止信号后SCL和SDA都为1,表示总线空闲
* 输入: void
* 输出: void
**************************************************************************/
void I2CStop();
/*************************************************************************
* 函数名: I2CSendByte
* 函数功能: I2C发送一个字节。在SCL高电平期间,保持SDA稳定
* 发送完一个字节后SCL=0,SDA=1
* 输入: void
* 输出: 0或1,0表示发送失败,1表示发送成功
**************************************************************************/
u8 I2CSendByte(u8 dat);
/*************************************************************************
* 函数名: I2CReadByte
* 函数功能: I2C读取一个字节。接收完一个字节后SCL=0,SDA=1
* 输入: void
* 输出: dat,读取的数据
**************************************************************************/
u8 I2CReadByte();
/*************************************************************************
* 函数名: AT24C02Write
* 函数功能: 往AT24C02中的一个地址写一个数据
* 输入: addr:写入的地址
* dat:写入的数据
* 输出: void
**************************************************************************/
void AT24C02Write(u8 addr, u8 dat);
/*************************************************************************
* 函数名: AT24C02Read
* 函数功能: 读取AT24C02中的一个地址的一个数据
* 输入: addr:读取的地址
* 输出: dat:写入的数据
**************************************************************************/
u8 AT24C02Read(u8 addr);
#endif
i2c.c
内容如下:
#include "reg52.h"
#include "Delay.h"
#include "i2c.h"
sbit SCL = P2^1;
sbit SDA = P2^0;
/*************************************************************************
* 函数名: I2CStart
* 函数功能: I2C起始信号,SCL高电平期间,SDA产生一个下降沿。
* 起始信号后SDA和SCL都为0
* 输入: void
* 输出: void
**************************************************************************/
void I2CStart()
{
SDA=1; // SDA初始状态为高电平
delay10us(); // 延时10us
SCL=1; // 设置SCL为高电平
delay10us(); // 延时10us,起始信号建立保持时间>4.7us
SDA=0; // SDA产生一个下降沿
delay10us(); // 起始信号保持时间>4us
SCL=0; // 起始信号后置SCL为0
delay10us(); // SCL低电平周期>4.7us
}
/*************************************************************************
* 函数名: I2CStop
* 函数功能: I2C终止信号,SCL高电平期间,SDA产生一个上升沿。
* 终止信号后SCL和SDA都为1,表示总线空闲
* 输入: void
* 输出: void
**************************************************************************/
void I2CStop()
{
SDA=0;
delay10us();
SCL=1;
delay10us(); // 停止信号建立时间>4us
SDA=1;
delay10us(); // 新的发送开始前总线空闲时间>4.7us
}
/*************************************************************************
* 函数名: I2CSendByte
* 函数功能: I2C发送一个字节。在SCL高电平期间,保持SDA稳定
* 发送完一个字节后SCL=0,SDA=1
* 输入: void
* 输出: 0或1,0表示发送失败,1表示发送成功
**************************************************************************/
u8 I2CSendByte(u8 dat)
{
u8 a=0, b=0; //
// 发送8位,从最高位开始
for(a=0;a<8;a++)
{
SDA=dat>>7; // 起始信号之后SCL为0,可以直接改变SDA
dat=dat<<1;
delay10us();
SCL=1; // SCL置1
delay10us(); // 数据建立时间>4.7us
SCL=0;
delay10us();
}
// 一个字节的数据发送结束
SDA=1;
delay10us();
SCL=1;
while(SDA) // 等待应答,等待从设备把SDA拉低
{
b++;
if(b>200) // 如果超过2000us没有应答发送失败,或者为非应答表示接收结束
{
SCL=0;
delay10us();
return 0;
}
}
SCL=0;
delay10us();
return 1;
}
/*************************************************************************
* 函数名: I2CReadByte
* 函数功能: I2C读取一个字节。接收完一个字节后SCL=0,SDA=1
* 输入: void
* 输出: dat,读取的数据
**************************************************************************/
u8 I2CReadByte()
{
u8 a=0, dat=0;
SDA=1; // 起始和发送一个字节后SCL都是0
delay10us();
for(a=0;a<8;a++)
{
SCL=1;
delay10us();
dat<<=1;
dat|=SDA;
delay10us();
SCL=0;
delay10us();
}
return dat;
}
/*************************************************************************
* 函数名: AT24C02Write
* 函数功能: 往AT24C02中的一个地址写一个数据
* 输入: addr:写入的地址
* dat:写入的数据
* 输出: void
**************************************************************************/
void AT24C02Write(u8 addr, u8 dat)
{
I2CStart(); // 起始信号
I2CSendByte(0xa0); // 发送器件地址
I2CSendByte(addr); // 发送写入的内存地址
I2CSendByte(dat); // 发送写入的数据
I2CStop(); // 终止信号
}
/*************************************************************************
* 函数名: AT24C02Read
* 函数功能: 读取AT24C02中的一个地址的一个数据
* 输入: addr:读取的地址
* 输出: dat:写入的数据
**************************************************************************/
u8 AT24C02Read(u8 addr)
{
u8 dat;
I2CStart(); // 发送起始信号
I2CSendByte(0xa0); // 发送写器件地址
I2CSendByte(addr); // 发送写入的内存地址
I2CStart(); // 改变方向,发送起始信号
I2CSendByte(0xa1); // 发送读器件地址
dat=I2CReadByte(); // 读取数据
I2CStop(); // 终止信号
return dat;
}
主函数如下:
/*
实现功能:独立按键控制EEPROM读写并将数据显示到数码管中
具体实现:有四个独立按键,
按K1将数据写入到EEPROM内保存,
按K2读取EEPROM内保存的数据,
按K3显示数据加1,
按K4显示数据清零,最大能写入的数据是255
[2023-12-22] zoya
*/
#include "reg52.h"
#include "typedef.h"
#include "Delay.h"
#include "i2c.h"
#define GPIO_DISPLAY P0
sbit LSA=P1^0;
sbit LSB=P1^1;
sbit LSC=P1^2;
sbit K1=P3^0;
sbit K2=P3^1;
sbit K3=P3^2;
sbit K4=P3^3;
// 共阴极数码管的码表,0-9
u8 code smg[] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6F, };
static u8 addr=0x01;
u8 dat;
u8 display[4]; // 数码管显示字符
// 按键处理函数
void KeyPros()
{
// K1写入EEPROM
if(0==K1)
{
delayms(10); // 延时消抖
if(0==K1)
{
AT24C02Write(addr,dat); // 在EEPROM的0x01地址写入数据dat
}
while(!K1);
}
// K2读取EEPROM
if(0==K2)
{
delayms(10);
if(0==K2)
{
dat=AT24C02Read(addr);
}
while(!K2);
}
// K3加1
if(0==K3)
{
delayms(10);
if(0==K3)
{
dat++;
if(dat>255)
dat=0;
}
while(!K3);
}
// K4清零
if(0==K4)
{
delayms(10);
if(0==K4)
{
dat=0;
}
while(!K4);
}
}
// 数码管显示函数
void DigDisplay()
{
LSA=0; LSB=0; LSC=0; GPIO_DISPLAY = smg[dat/1000];
delayms(1);
LSA=1; LSB=0; LSC=0; GPIO_DISPLAY = smg[dat%1000/100];
delayms(1);
LSA=0; LSB=1; LSC=0; GPIO_DISPLAY = smg[dat%1000%100/10];
delayms(1);
LSA=1; LSB=1; LSC=0; GPIO_DISPLAY = smg[dat%1000%100%10];
delayms(1);
}
void main()
{
GPIO_DISPLAY = 0x00;
while(1)
{
KeyPros();
DigDisplay();
}
}
仿真结果:
四、扩展实验:通过AT24C02芯片对功能程序进行加密
扩展实验实现的功能:通过读取AT24C02芯片内0x02地址的数据,判断是否执行按键对应的操作。如果数据为0不执行,否则执行操作。前提是AT24C02中已经有了数据。
在上面程序的基础上添加一个全局变量u8 readdat
,表示从地址0x02中读出的数据。在主函数进入循环之前添加一行readdat = AT24C02Read(0x02);
从0x02地址中读出数据。在按键处理函数中添加if(0==readdat) return;
,当0x02地址读出的数据是0时不执行按键处理函数。
实现结果:
当0x02地址读出是0时
当0x02地址读出不是0时