I2C-EEPROM 实验
这一章我们来学习如何使用 51 单片机的 IO 口模拟 I2C 时序,并实现与AT24C02(EEPROM)之间的双向通信。开发板板载了 1 个 EEPROM 模块,可实现IIC 通信。本章要实现的功能是:系统运行时,数码管右 3 位显示 0,按 K1 键将数据写入到 EEPROM 内保存,按 K2 键读取 EEPROM 内保存的数据,按 K3 键显示数据加 1,按 K4 键显示数据清零,最大能写入的数据是 255。
I2C介绍
I2C(Inter-Integrated Circuit)总线是由 PHILIPS 公司开发的两线式串行总线,用于连接微控制器及其外围设备。是微电子通信控制领域广泛采用的一种总线标准。它是同步通信的一种特殊形式,具有接口线少,控制方式简单,器件封装形式小,通信速率较高等优点。I2C 总线只有两根双向信号线。一根是数据线 SDA,另一根是时钟线 SCL。由于其管脚少,硬件实现简单,可扩展性强等特点,因此被广泛的使用在各大集成芯片内。下面我们就从 I2C 的物理层与协议层来了解 I2C。
I2C 物 理 层(IIC)
I2C 通信设备常用的连接方式如下图所示:
它的物理层有如下特点:
(1)它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2)一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(3)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
(4)总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
(5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线(根据每个器件的地址来识别)。
(6)具有三种传输模式:标准模式传输速率为 100kbit/s,快速模式为
400kbit/s,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模
式。
(7)连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。
下面我们来了解下 I2C 总线常用的一些术语:
主 机:启动数据传送并产生时钟信号的设备(MCU);
从 机:被主机寻址的器件;
多 主 机:同时有多于一个主机尝试控制总线但不破坏传输;
主 模 式:用 I2CNDAT 支持自动字节计数的模式; 位 I2CRM,I2CSTT,I2CSTP控制数据的接收和发送;
从 模 式:发送和接收操作都是由 I2C 模块自动控制的;比如主机想要读取
仲 裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并使传输不被破坏的过程;
同 步:两个或多个器件同步时钟信号的过程;
发 送 器:发送数据到总线的器件;
接 收 器:从总线接收数据的器件。
I2C 协 议 层
I2C 的协议定义了通信的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。下面我们就来简单介绍下。
(1)数据有效性规定
I2C 总线进行数据传送时,时钟信号SCL为高电平期间,数据线SDA上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。如下图:
每次数据传输都以字节(8bit)为单位,先传高位再传低位,每次传输的字节数不受限制。
(2)起始和停止信号
SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号;SCL线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号。如下图:
起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。
(3)应答响应
每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位,这个校验位是接收端通过控制 SDA(数据线)来实现的,以提醒发送端数据我这边已经接收完成,数据传送可以继续进行。这个校验位其实就是数据或地址传输过程中的响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号即特定的低电平脉冲,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号即特定的高电平脉冲,发送方接收到该信号后会产生一个停止信号,结束信号传输。应答响应时序图如下:
每一个字节必须保证是 8 位长度。 数 据 传 送 时 , 先 传 送 最 高 位 (MSB) , 每一 个 被 传 送 的 字 节 后 面 都 必 须 跟 随 一 位 应 答 位 ( 即 一 帧 共 有 9 位 )。由于某种原因从机不对主机寻址信号应答时(如从机正在进行实时性的处理工作而无法接收总线上的数据),它必须将数据线置于高电平,而由主机产生一个终止信号以结束总线的数据传送。
如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的“非应答”通知主机,主机则应发出终止信号以结束数据的继续传送。
当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放 SDA线,以允许主机产生终止信号。
这些信号中,起始信号是必需的,结束信号和应答信号都可以不要。
(4) 总 线 的 寻 址 方 式
I2C 总线寻址按照从机地址位数可分为两种,一种是 7 位,另一种是 10位。采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如下:
D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“ 0”时表示W非:主机向从机写数据,为“1”时表示R:主机由从机读数据。
10 位寻址和 7 位寻址兼容,而且可以结合使用。10 位寻址不会影响已有的 7 位寻址,有 7 位和 10 位地址的器件可以连接到相同的 I2C 总线。我们就以 7 位寻址为例进行介绍。
当主机发送了一个地址后,总线上的每个器件都将头 7 位与它自己的地址比较,如果一样,器件会判定它被主机寻址,其他地址不同的器件将被忽略后面的数据信号。至于是从机接收器还是从机发送器,都由 R/W 位决定的。从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的 7 位寻址位有 4 位是固定位,3 位是可编程位,这时仅能寻址 8 个同样的器件,即可以有 8 个同样的器件接入到该 I2C 总线系统中。
(5)数据传输
I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。在起始信号后必须传送一个从机的地址(7 位),第 8 位是数据的传送方向位(R/W),用“ 0”表示主机发送(写)数据(W),“ 1”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
在总线的一次数据传送过程中,可以有以下几种组合方式:
a、主机向从机发送数据,数据传送方向在整个传送过程中不变
注意:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。
b、主机在第一个字节后,立即从从机读数据
c、在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好相反(方法c经常使用)
到这里我们就介绍完 I2C 总线,由于 51 单片机没有硬件 IIC 接口,即使有硬件接口我们通常还是采用软件模拟 I2C。主要原因是硬件 IIC 设计的比较复杂,而且稳定性不怎么好,程序移植比较麻烦,而用软件模拟 IIC,最大的好处就是移植方便,同一个代码兼容所有单片机,任何一个单片机只要有 IO 口(不需要特定 IO),都可以很快的移植过去(使用IO模拟I2C)。
AT24C02 介 绍
AT24C01/02/04/08/16…是一个 1K/2K/4K/8K/16K 位串行 CMOS,内部含有128/256/512/1024/2048 个 8 位字节,AT24C01 有一个 8 字节页写缓冲器,AT24C02/04/08/16 有一个 16 字节页写缓冲器。该器件通过 I2C 总线接口进行操作,它有一个专门的写保护功能。我们开发板上使用的是 AT24C02(EEPROM)芯片,此芯片具有 I2C 通信接口,芯片内保存的数据在掉电情况下都不丢失,所以通常用于存放一些比较重要的数据等AT24C02 芯片管脚及外观图如下图所示:
芯片管脚说明如下图所示:
AT24C02 器件地址为 7 位,高 4 位固定为 1010,低 3 位由 A0/A1/A2 信号线的电平决定。 因为传输地址或数据是以字节为单位传送的,当传送地址时,器件地址占 7 位,还有最后一位(最低位 R/W)用来选择读写方向,它与地址无关。其格式如下:
因为单片机的板载上只有一个EEPROM芯片,所以我们开发板已经将芯片的 A0/A1/A2固定连接到GND,所以器件地址为1010000,即 0x50(未计算最低位)。如果要对芯片进行写操作时,R/W 即为 0,写器件地址即为 0XA0;如果要对芯片进行读操作时,R/W 即为 1,此时读器件地址为 0XA1。开发板上也将 WP 引脚直接接在 GND 上,此时芯片允许数据正常读写。
I2C 总线时序如下图所示:
硬件设计
左图为A2-A4的板载电路图,SCL和SDA分别固定连接了P21和P20管脚。右图为A5-A7的板载电路图,SCL和SDA可以自由选择管脚;R51和R52为上拉电阻,来控制总线(总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平)。而A2-A4上没有上拉电阻是因为,P20和P21管脚本身自带上拉电阻。而A5-A7若SCL和SDA连接了P0管脚,因为P0管脚没有上拉电阻,因此24C02芯片就必须附加上拉电阻。
软件设计
多文件工程
本章带领大家创建一个全新的工程,目的是方便日后工程管理、程序移植和
维护、易读等。下面开始:
在电脑上创建一个实验文件夹,为了与教程配套,这里命名为“I2C-EEPROM
实验”,然后在该文件夹内新建App、Obj、Public、User四个文件夹,如下所
示:
**App文件夹:**用于存放外设驱动文件,如LED、数码管、定时器等。
**Obj文件夹:**用于存放编译产生的c/汇编/链接的列表清单、调试信息、hex文件、预览信息、封装库等文件。
**Public文件夹:**用于存放51单片机公共的文件,如延时、51头文件、变量类型重定义等。
**User文件夹:**用于存放用户主函数文件,如main.c。
为何要定义这么多文件夹呢?为何不按照之前实验那样直接在main.c文件里面写代码?我们先来分析本章实验所需用到哪些功能程序,要用到独立按键、数码管、EEPROM,所以需要将前面实验编写的独立按键、数码管、延时等程序全部放到main.c中来,此时在不增加EEPROM驱动程序时,main.c文件中的代码量已经非常冗长,极其不便于程序的阅读、移植和维护,因此可用多文件管理的办法来使工程易于阅读、移植和维护。操作方法如下:
新建工程
首先打开KEILC51软件,新建一个工程,这个在前面章节已经讲解过,这里不再重复。将工程命名为template并保存在“I2C-EEPROM实验”文件夹下,然后选择芯片类型为“AT89C52”,不使用系统创建启动文件,如下所示:
上述操作与前面介绍的创建工程模板是一样的,下面才开始在工程中进行分组管理。
向工程添加文件
按照需要给工程分组并添加对应文件,这里我们在工程中分3组,User、App、Publi,至于前面创建的Obj文件夹是在工程中无需体现,因为只是编译器生成的一些中间文件和.hex执行文件。通常在工程组的命名与创建的文件夹名保持一致,方便查找到源文件位置。如下所示:P
分组后,在工程中就会出现刚才的分组列表,如下所示:
然后就是给每个组添加对应的.c源文件,这些源文件和头文件在实验例程中已经给出,大家可直接从那复制到新建的文件夹内,复制后如下所示:
在App文件夹中可以看到根据不同外设分别创建了文件夹保存各自驱动文件,这样非常方便程序移植和维护。然后就将复制过来的文件添加到刚才的工程分组中,如下所示:
可以选择到要添加的.c文件(红色标记5),然后鼠标左键双击也可直接添加进去,就免去了点击红色标记6这步。添加好后,在右侧就会显示对应组中已添加的文件,如下所示:
同样的方法,将App、Public工程组中文件也添加进去。如下所示:
此时可以看到,在工程栏下每组都已经加入了刚才添加的源文件,如下所示:
如果有遗漏的文件未添加进来,可按照上述方法重新添加。接下来如果直接编译工程肯定会出现一大堆错误,因为在KEIL中指定那些文件夹中的头文件路径。
配置魔术棒选项卡
这一步的配置工作非常重要,很多人编写完程序编译后发现找不到HEX文件,还有的人直接编译前面添加好文件的工程出现报错,这些问题都是在这个地方没有配置好导致的。
(1)Output选项卡中把输出文件夹定位到我们实验目录下的Obj文件夹,如果想在编译的过程中生成hex文件,那么CreateHEXFile选项勾上。配置如下:
(3)Listing选项卡中把输出文件夹也定位到我们实验目录下的Obj文件夹。其它设置默认,配置如下:
(4)C51选项卡配置,此处目的是将我们前面添加到工程组中的文件路径包括进来,否则程序中调用其他文件夹的头文件则会报错找不到头文件路径,具体步骤如下:
添加的头文件路径是指,在I2C-EEPROM实验文件夹下里面,哪些文件夹内含有.h头文件,并且需要被调用到的,通常我们会把只要含有头文件的文件夹都选择进去。比如本例程中App内含有很多子文件夹,它们里面都含有头文件,因此要分别添加,Public也含有头文件,所以也要添加。添加完成后如下:
仿真器配置
有关51仿真器配置可参考前面章节内容,此处不再重复。
然后编译一下工程,编译后结果0错误0警告,表明我们创建的多文件工程没有问题。如下:
至此,我们就成功创建好一个多文件工程模板。在以后的实验中,凡是涉及到多个外设资源模块的都可以使用该工程模板,尤其是对重复利用已编写过的外设驱动。
实验代码
public.h:
#ifndef _public_H
#define _public_H//名称与头文件名字相同
#include"reg52.h"
typedef unsigned int u16;
typedef unsigned char u8;
void delay_1ms(u16 ms);
void delay_10us(u16 ten_us);
#endif
public.c:
#include"public.h"
void delay_1ms(u16 ms)
{
u16 i,j;
for(i=ms;i>0;i++)
for(j=110;j>0;j--);
}
void delay_10us(u16 ten_us)
{
while(ten_us--);
}
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;
extern u8 gsmg_code[16];//必须加extern
void smg_display(u8 dat[],u8 pos);
#endif
smg.c:
#include "smg.h"
u8 gsmg_code[16]={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(7-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;//消音
}
}
key.h:
#ifndef _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
key.c:
#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;
}
iic.h:
#ifndef _iic_H
#define _iic_H
#include"public.h"
sbit IIC_SCL=P2^1;
sbit IIC_SDA=P2^0;
void iic_start(void);//函数功能 : 产生 IIC 起始信号
void iic_stop(void);//函数功能 :产生 IIC 停止信号
void iic_ack(void);//函数功能 :产生 ACK 应答
void iic_nack(void);//函数功能 :产生 NACK 非应答
u8 iic_wait_ack(void);//函数功能 :读取应答
void iic_write_byte(u8 dat);//函数功能 :IIC 发送一个字节
u8 iic_read_byte(u8 ack);//函数功能:IIC 读一个字节
#endif
iic.c:
#include "iic.h"
void iic_start(void)//函数功能 : 产生 IIC 起始信号
{
IIC_SCL=1;
IIC_SDA=1;
delay_10us(1);//当 SCL 为高电平时,SDA 由高变为低
IIC_SDA=0;
delay_10us(1);//钳住 I2C 总线,准备发送或接收数据
IIC_SCL=0;
}
void iic_stop(void)//函数功能 :产生 IIC 停止信号
{
IIC_SCL=1;
IIC_SDA=0;
delay_10us(1);
IIC_SDA=1; //当 SCL 为高电平时,SDA 由低变为高
delay_10us(1);
}
void iic_ack(void)//函数功能 :产生 ACK 应答
{
IIC_SCL=0;
IIC_SDA=0;//SDA 为低电平,发出应答信号
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
void iic_nack(void)//函数功能 :产生 NACK 非应答
{
IIC_SCL=0;
IIC_SDA=1;//SDA 为高电平,发出非应答信号
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
u8 iic_wait_ack(void)//函数功能 :读取应答
{
u8 time_temp=0;
IIC_SCL=1;
delay_10us(1);
while(IIC_SDA) //等待 SDA 为低电平
{
time_temp++;
if(time_temp>100)//超时则强制结束 IIC 通信
{
iic_stop();
return 1;
}
} //输 出 : 1,接收nack,不希望主机继续发送数据;0,接收ack,希望主机继续发送数据
IIC_SCL=0;
return 0;
}
void iic_write_byte(u8 dat)//函数功能 :IIC 发送一个字节
{
u8 i=0;
IIC_SCL=0;
for(i=0;i<8;i++) //循环 8 次将一个字节传出,先传高再传低位
{
if((dat&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
dat<<=1;
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
delay_10us(1);
}
}
u8 iic_read_byte(u8 ack)//函数功能:IIC 读一个字节
{
u8 i=0,receive=0;
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);
}
if (!ack) //ack=1 时,发送 ACK,ack=0,发送 nACK
iic_nack();
else
iic_ack();
return receive;
}
24c02.h:
#ifndef _24c02_H
#define _24c02_H
#include "public.h"
void at24c02_write_one_byte(u8 addr,u8 dat);
u8 at24c02_read_one_byte(u8 addr);
#endif
24c02.c:
#include "24c02.h"
#include "iic.h"
/********************************************************************
***********
* 函 数 名 : at24c02_write_one_byte
* 函数功能 : 在 AT24CXX 指定地址写入一个数据
* 输 入 : addr:写入数据的目的地址
dat:要写入的数据
* 输 出 : 无
*********************************************************************
**********/
void at24c02_write_one_byte(u8 addr,u8 dat)
{
iic_start();
iic_write_byte(0xa0);//发送写命令
iic_wait_ack();
iic_write_byte(addr);//发送写地址
iic_wait_ack();
iic_write_byte(dat); //发送字节
iic_wait_ack();
iic_stop(); //产生一个停止条件
delay_1ms(10);
}
/********************************************************************
***********
* 函 数 名 : at24c02_read_one_byte
* 函数功能 : 在 AT24CXX 指定地址读出一个数据
* 输 入 : addr:开始读数的地址
* 输 出 : 读到的数据
*********************************************************************
**********/
u8 at24c02_read_one_byte(u8 addr)
{
u8 temp=0;
iic_start();
iic_write_byte(0XA0); //发送写命令
iic_wait_ack();
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; //返回读取的数据
}
main.c:
#include"public.h"
#include"key.h"
#include"smg.h"
#include"24c02.h"
#define EEPROM_ADDRESS 0 //定义数据存入 EEPROM 的起始地址
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);
}
else if(key_temp==KEY2_PRESS)
{
save_value=at24c02_read_one_byte(EEPROM_ADDRESS);
}
else if(key_temp==KEY3_PRESS)
{
save_value++;
if(save_value==255)save_value=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);
}
}