1I2C总线概述
1.1
l2C总线(InterICBus)由PHILIPS公司推出是近年来广泛应用于微电子通信控制领域的一种新型总线标准。它是一种特殊的同步通信形式,具有界面线少、控制简单、设备包装形式小、通信速率高等优点。主从通信个主从通信l2C同时接收总线设备l2C总线上,所有与I2C兼容设备有标准接口,通过地址识别通信对象,使其能够通过l2C总线相互直接通信。 I2C总线由数据线组成SDA和时钟线SCL两条线构成通信线,可以发送数据或接收数据。在CPU与被控IC之间、IC与IC双向传输可在两者之间进行,最高传输速率为400kbps,各种被控设备并联在总线上,但每个设备都有唯一的地址。在信息传输过程中,l2C根据其要完成的功能,总线上并联的每个设备都是由被控器(或主控器)和发送器(或接收器)组成的。CPU发出的控制信号分为地址代码和数据代码两部分:地址代码用于选址,即连接需要控制的电路;数据代码是通信的内容,所以IC虽然控制电路挂在同一条总线上,但它们是独立的。
1.2
下图为I2C其中,总线系统的硬件结构图,。总线上所有设备均采用漏极开路结构与总线连接,因此,即各器件的SDA及SCL都是线和关系。
I2C总线支持多主和主从两种工作方式,通常为主从工作方式。系统中只有一个主器件(单片机),其他器件都有I2C设备从总线外围。在主工作模式下,发送主器件启动数据(发出启动信号),产生时钟信号,发出停止信号。
1.3
下图为l2C数据传输的通信格式在总线上进行。
1.4
I2C当总线传输数据时,时钟信号为高电平时,数据线上的数据必须保持稳定。只有当时钟信号为低电平时,数据线上的高电平或低电平状态才能发生变化,如下图所示。
1.5
在利用l2C当总线进行数据传输时,主机首先发出启动信号l2C总线。在SCL高电平期,SDA出现上升沿则为启动信号。此时,具有I2C从设备中检测到总线接口的信号,如下图所示。
1.6
发送启动信号后,主机发送搜索信号。有七位和十位两种设备地址,这里只介绍七位地址寻址方式。搜索字节的位置定义如下图所示。搜索信号由一个字节组成,高7位为地址位,最低位为方向位,以显示主机和从设备的数据传输方向。方向为0,表示主机下一步写下从器件;方向为1,表示主机下一步读取从器件。
当主机发送地址时,总线上的每个从机器都将这7个地址代码与自己的地址进行比较。如果是一样的,他们认为自己正在被主机搜索,根据R/W将自己确定为发送器或接收器。 地址由固定部分和可编程部分组成。可能希望在一个系统中访问多个相同的从机器,从机器地址中的可编程部分决定了可访问总线的最大数量。例如,从机器的7位搜索位置中有4位是固定位,3位是可编程位。此时,只能找到8个相同的设备,即可以连接8个相同的设备l2C在总线系统中。
1.7
l2C根据总线协议,每次传输字节数据(包括地址和命令字)时,都应有响应信号,以确定数据传输是否被对方接收。接收设备产生响应信号SCL当信号为高电平时,接收设备将被接收SDA拉到低电平,表示数据传输正确,产生响应,时序图如下图所示。
1.8
主机发送搜索信号并从设备响应后,可以传输数据,每个字节,但每个传输应在获得响应信号后传输下一个字节。
1.9
当主机接收设备时,主机不回应最后一个字节,以表示数据传输结束。
1.10
所有数据传输后,主机发送停止信号SCL高电平期,SDA停止时序图如下图所示。
2单片机模拟I2c总线通信
目前市场上很多单片机都有硬件l2C总线控制单元,这种单片机工作时,总线状态由硬件监控,无需用户介入,操作非常方便。但是有很多单片机没有l2C总线接口,如51单片机,但我们可以通过软件模拟单片机应用系统l2C在使用总线时,只需正确调用每个函数即可轻松扩展总线的工作顺序l2C总线接口设备。 在总线的数据传输过程中,可以 1)主机向从机发送数据,数据传输方向在整个传输过程中保持不变。 2)主机在第一个字节后立即从机器读取数据。 3)在传输过程中,当需要改变传输方向时,需要重复一次启动信号和从机地址,而两次读写方向正好相反。 为保证数据传输的可靠性,标准I2C总线数据传输有严格的时序要求。I2C总线的起始信号、终止信号、响应或发送"0"、非应答或发送"模拟时序如下图所示。
模拟单片机l2C在通信总线时,应编写以下关键部分的程序:。以下是具体函数的写作方法供您参考。阅读代码时,请参考前面相关部分的文本描述和及时序列图。
2.1
void init()
{
SCL=1;
delay();
SDA=1;
delay();
}
将总线都拉高以释放总线。
2.2
void start()
{
SDA=1;
delay();
SCL=1;
delay();
SDA=0;
delay();
}
SCL在高电平期间,SDA一个下降沿启动信号。
2.3
void respons()
{
uchar i=0;
SCL=1;
delay();
while((SDA==1)&&(i<255))
i++;
SCL=0;
delay();
}
SCL在高电平期间,SDA被从设备拉为低电平表示应答。上面代码中有一个(SDA==1)和(i<255)相与的关系,表示若在一段时间内没有收到从器件的应答则主器件默认从器件已经收到数据而不在等待应答信号,这一点是作者后加的一步,大家可不必深究,因为如果不加这个延时退出,一旦从器件没有发送应答信号,程序将永远停止在这里,而真正的程序中是不允许这样的情况发生的。
2.4
void stop()
{
SDA=0;
delay();
SCL=1;
delay();
SDA=1;
delay();
}
SCL在高电平期间,SDA个上升沿停止信号。
void writebyte(uchar date)
{
uchar1,temp;
temp=date;
for(1=0;1<8;1++)
{
temp=temp<<1;
SCL=0;
delay();
SDA=CY;
delay();
SCL=1;
delay();
}
SCL=0;
delay();
SDA=1;
delay();
}
串行发送一个字节时,需要把这个字节中的8位一位一位地发出去,"temp=temp<<1;"表示将temp左移一位,最高位将移入PSW寄存器的CY位中,然后将CY赋给SDA进而在SCL的控制下发送出去。
2.6
uchar readbyte()
{
uchar i,k;
SCL=0;
delay();
SDA=1;
for(i=0;i<8;i++)
{
SCL=1;
delay();
k=(k<<1) | SDA;
SCL=0;
delay();
}
delay();
return k;
}
同样的,串行接收一个字节时需将8位一位一位地接收,然后再组合成一个字节,上面代码中我们定义了一个临时变量k,将K左移一位后与SDA进行”或“运算,依次把8个独立的位放入一个字节中来完成接收。
3 E2PROM AT24C02与单片机的通信实例
具有l2C总线接口的E2PROM有多个厂家的多种类型产品。在此仅介绍ATMEL公司生产的AT24C系列E2PROM,主要型号有AT24CO1/02/04/08/16等,其对应的存储容量分别为128x8/256x8/512x8/1024x8/2048x8。采用这类芯片可解决掉电数据保存问题,可对所存数据保存100年,并可多次擦写,擦写次数可达10万次以上。 在一些应用系统设计中,有时需要对工作数据进行掉电保护,如电子式电能表等智能化产品。若采用普通存储器,在掉电时需要备用电池供电,并需要在硬件上增加掉电检测电路,但存在电池不可靠及扩展存储芯片占用单片机过多口线的缺点。采用具有l2C总线接口的串行E2PROM器件可很好地解决掉电数据保存问题,且硬件电路简单。下面以AT24C02芯片为例,介绍具有I2C总线接口的E2PROM的具体应用。
3.1
AT24C02芯片的常用封装形式有直插(DIP8)式和贴片(S0-8)式两种,实物图分别如左下图和右下图所示。
无论是直插式还是贴片式,其引脚功能与序号都一样,引脚图如下图所示。
各引脚功能如下: —可编程地址输入端。 —电源地。 —串行数据输入/输出端。 一串行时钟输入端。 —写保护输入端,用于硬件数据保护。当其为低电平时,可以对整个存储器进行正常的读/写操作;当其为高电平时,存储器具有写保护功能,但读操作不受影响。 —电源正端。
3.2
AT24C02的存储容量为2KB,内部分成32页,每页8B,共256B,操作时有两种寻址方式:芯片寻址和片内子地址寻址。 1)。AT24C02的芯片地址为1010,其地址控制字格式为1010 A2A1A0R/W。 其中A2、A1、A0为可编程地址选择位。A2、A1、A0引脚接高、低电平后得到确定的三位编码,与1010形成7位编码,即为该器件的地址码。R/W为芯片读写控制位,该位为0,表示对芯片进行写操作;该位为1,表示对芯片进行读操作。 2)。芯片寻址可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单元。
3.3
:一种是,另一种是页写入方式允许在一个写周期内(10ms左右)对一个字节到一页的若干字节进行编程写入,AT24C02的页面大小为8B。采用页写方式可提高写入效率,但也容易发生事故。AT24C系列片内地址在接收到每一个数据字节后自动加1,故装载一页以内数据字节时,只需输入首地址,如果写到此页的最后一个字节,主器件继续发送数据,数据将重新从该页的首地址写入,进而造成原来的数据丢失,这就是页地址空间的“上卷“现象。 解决“上卷"的方法是:在第8个数据后将地址强制加1,或是将下一页的首地址重新赋给寄存器。 1)。单片机在一次数据帧中只访问E2PROM一个单元。该方式下,单片机先发送启动信号,然后送一个字节的控制字,再送一个字节的存储器单元子地址,上述几个字节都得到E2PROM响应后,再发送8位数据,最后发送1位停止信号。发送格式如下图所示。
2)。单片机在一个数据写周期内可以连续访问1页(8个)E2PROM存储单元。在该方式中,单片机先发送启动信号,接着送一个字节的控制字,再送1个字节的存储器起始单元地址,上述几个字节都得到E2PROM应答后就可以发送最多1页的数据,并顺序存放在以指定起始地址开始的相继单元中,最后以停止信号结束。页写入帧格式如下图所示。
3)。读指定地址单元的数据。单片机在启动信号后先发送含有片选地址的写操作控制字,E2PROM应答后再发送1个(2KB以内的E2PROM)字节的指定单元的地址,E2PROM应答后再发送1个含有片选地址的读操作控制字,此时如果E2PROM做出应答,被访问单元的数据就会按SCL信号同步出现在串行数据/地址线SDA上。这种读操作的数据帧格式如下图所示。
4)。此种方式的读地址控制与前面指定地址读相同。单片机接收到每个字节数据后应做出应答,只要E2PROM检测到应答信号,其内部的地址寄存器就自动加1指向下一单元,并顺序将指向的单元的数据送到SDA串行数据线上。当需要结束读操作时,单片机接收到数据后在需要应答的时刻发送一个非应答信号,接着再发送一个停止信号即可。这种读操作的数据帧格式如下图所示。
3.4
实验板上AT24C02与单片机连接如下图所示,其中A0、A1、A2与WP都接地,SDA接单片机P2.0脚,SCL接单片机P2.1脚,SDA与SCL分别与Vcc之间接一1KΩ上拉电阻,因为AT24C02总线内部是漏极开路形式,不接上拉电阻无法确定总线空闲时的电平状态。
实例:用C语言编写程序,具体操作:① 按设置按钮保存按确认按钮读取EEPROM值 ;② 按时间-按钮减数 ;③ 按时间+按钮加数 ; 新建文件Temp.c,程序代码如下:
#include <reg52.h>
#include "boardinit_Sum.h"
#include "24c02_Sum.h"
#include "1602_Sum.h"
sbit k1=P3^7;
sbit k2=P3^6;
sbit k3=P3^5;
sbit k4=P3^4;
sbit k5=P3^0;
unsigned char Count1;
unsigned int idata USEC;
void main()
{
unsigned char pDat[1];
boardinit();
lcdinit_1602();
k5=0;
TMOD|= 0x11;
TH1 = 0xfe; //11.0592
TL1 = 0x33;
TR1 = 1;
IE =0x8A;
Disp_1602(1,1," EEPROM-24C02 ",16);
Disp_1602(1,2," DATA: 0000 ",16);
while(1)
{
//*********************************************************************
//第一个按钮按下保存数据
if(k1==0)
{
pDat[0]=Count1;
ISendStr(0xa0,0,&pDat[0],1);
}
//*********************************************************************
// 第二个按钮按下读取数据
if(k2==0)
{
IRcvStr(0xa0,0 , &pDat[0], 1);
Count1 =pDat[0];
}
//*********************************************************************
write_twoline_1602(9,Count1); //把读出的数据送 1602显示
//*********************************************************************
}
}
void T1zd(void) interrupt 3 //3 为定时器1的中断号 1 定时器0的中断号 0 外部中断1 2 外部中断2 4 串口中断
{
TH1 = 0xfe; //12M
TL1 = 0x33;
if(USEC++==200)
{ USEC=0;
if (k3==1) Count1++; //改变数据
if (k4==1) Count1--;
}
}
库函数如下:
/*
* 文 件 名:24c02.c
* 芯 片:24c02
* 晶 振:11.0592MHz
* 创 建 者:XK
* 创建日期:2011.9.17
* 修 改 者:
* 修改日期:
* 功能描述:24c02,读写数据函数
*/
#include <reg52.h>
#include <intrins.h>
#include "24c02_Sum.h"
#define NOP() _nop_() /*定义空指令*/
#define _Nop() _nop_() /*定义空指令*/
sbit SCL=P2^1; //I2C 时钟
sbit SDA=P2^0; //I2C 数据
bit ack; /*应答标志位*/
//AT2402的功能函数
/*******************************************************************
向有子地址器件发送多字节数据函数
函数原型: bit ISendStr(UCHAR sla,UCHAR suba,ucahr *s,UCHAR no);
功能: 从启动总线到发送地址,子地址,数据,结束总线的全过程,从器件
地址sla,子地址suba,发送内容是s指向的内容,发送no个字节。
如果返回1表示操作成功,否则操作有误。
注意: 使用前必须已结束总线。
********************************************************************/
bit ISendStr(unsigned char sla,unsigned char suba,unsigned char *s,unsigned char no)
{
unsigned char i;
Start_I2c(); /*启动总线*/
SendByte(sla); /*发送器件地址*/
if(ack==0)return(0);
SendByte(suba); /*发送器件子地址*/
if(ack==0)return(0);
for(i=0;i<no;i++)
{
SendByte(*s); /*发送数据*/
if(ack==0)return(0);
s++;
}
Stop_I2c(); /*结束总线*/
return(1);
}
/*******************************************************************
向有子地址器件读取多字节数据函数
函数原型: bit RecndStr(UCHAR sla,UCHAR suba,ucahr *s,UCHAR no);
功能: 从启动总线到发送地址,子地址,读数据,结束总线的全过程,从器件
地址sla,子地址suba,读出的内容放入s指向的存储区,读no个字节。
如果返回1表示操作成功,否则操作有误。
注意: 使用前必须已结束总线。
********************************************************************/
bit IRcvStr(unsigned char sla,unsigned char suba,unsigned char *s,unsigned char no)
{
unsigned char i;
Start_I2c(); /*启动总线*/
SendByte(sla); /*发送器件地址*/
if(ack==0)return(0);
SendByte(suba); /*发送器件子地址*/
if(ack==0)return(0);
Start_I2c(); /*重新启动总线*/
SendByte(sla+1);
if(ack==0)return(0);
for(i=0;i<no-1;i++)
{
*s=RcvByte(); /*发送数据*/
Ack_I2c(0); /*发送就答位*/
s++;
}
*s=RcvByte();
Ack_I2c(1); /*发送非应位*/
Stop_I2c(); /*结束总线*/
return(1);
}
/*******************************************************************
/*******************************************************************
起动总线函数
函数原型: void Start_I2c();
功能: 启动I2C总线,即发送I2C起始条件.
********************************************************************/
void Start_I2c()
{
SDA=1; /*发送起始条件的数据信号*/
_Nop();
SCL=1;
_Nop(); /*起始条件建立时间大于4.7us,延时*/
_Nop();
_Nop();
_Nop();
_Nop();
SDA=0; /*发送起始信号*/
_Nop(); /* 起始条件锁定时间大于4μs*/
_Nop();
_Nop();
_Nop();
_Nop();
SCL=0; /*钳住I2C总线,准备发送或接收数据 */
_Nop();
_Nop();
}
/*******************************************************************
结束总线函数
函数原型: void Stop_I2c();
功能: 结束I2C总线,即发送I2C结束条件.
********************************************************************/
void Stop_I2c()
{
SDA=0; /*发送结束条件的数据信号*/
_Nop(); /*发送结束条件的时钟信号*/
SCL=1; /*结束条件建立时间大于4μs*/
_Nop();
_Nop();
_Nop();
_Nop();
_Nop();
SDA=1; /*发送I2C总线结束信号*/
_Nop();
_Nop();
_Nop();
_Nop();
}
/*******************************************************************
字节数据发送函数
函数原型: void SendByte(UCHAR c);
功能: 将数据c发送出去,可以是地址,也可以是数据,发完后等待应答,并对
此状态位进行操作.(不应答或非应答都使ack=0)
发送数据正常,ack=1; ack=0表示被控器无应答或损坏。
********************************************************************/
void SendByte(unsigned char c)
{
unsigned char BitCnt;
for(BitCnt=0;BitCnt<8;BitCnt++) /*要传送的数据长度为8位*/
{
if((c<<BitCnt)&0x80)SDA=1; /*判断发送位*/
else SDA=0;
_Nop();
SCL=1; /*置时钟线为高,通知被控器开始接收数据位*/
_Nop();
_Nop(); /*保证时钟高电平周期大于4μs*/
_Nop();
_Nop();
_Nop();
SCL=0;
}
_Nop();
_Nop();
SDA=1; /*8位发送完后释放数据线,准备接收应答位*/
_Nop();
_Nop();
SCL=1;
_Nop();
_Nop();
_Nop();
if(SDA==1)ack=0;
else ack=1; /*判断是否接收到应答信号*/
SCL=0;
_Nop();
_Nop();
}
/*******************************************************************
字节数据接收函数
函数原型: UCHAR RcvByte();
功能: 用来接收从器件传来的数据,并判断总线错误(不发应答信号),
发完后请用应答函数应答从机。
********************************************************************/
unsigned char RcvByte()
{
unsigned char retc;
unsigned char BitCnt;
retc=0;
SDA=1; /*置数据线为输入方式*/
for(BitCnt=0;BitCnt<8;BitCnt++)
{
_Nop();
SCL=0; /*置时钟线为低,准备接收数据位*/
_Nop();
_Nop(); /*时钟低电平周期大于4.7μs*/
_Nop();
_Nop();
_Nop();
SCL=1; /*置时钟线为高使数据线上数据有效*/
_Nop();
_Nop();
retc=retc<<1;
if(SDA==1)retc=retc+1; /*读数据位,接收的数据位放入retc中 */
_Nop();
_Nop();
}
SCL=0;
_Nop();
_Nop();
return(retc);
}
/********************************************************************
应答子函数
函数原型: void Ack_I2c(bit a);
功能: 主控器进行应答信号(可以是应答或非应答信号,由位参数a决定)
********************************************************************/
void Ack_I2c(bit a)
{
if(a==0)SDA=0; /*在此发出应答或非应答信号 */
else SDA=1;
_Nop();
_Nop();
_Nop();
SCL=1;
_Nop();
_Nop(); /*时钟低电平周期大于4μs*/
_Nop();
_Nop();
_Nop();
SCL=0; /*清时钟线,钳住I2C总线以便继续接收*/
_Nop();
_Nop();
}
分析如下: 1)是一个微秒级延时函数,以前编写的延时函数内部都是用变量递增或是递减来实现延时,而这个函数是用空语句来实现短时间延时的,在Keil软件中设置晶振为11.0592MHz时,该延时函数延时大概4~5微秒,用来操作l2C总线时用。 2)和两个函数分别实现向AT24C02的任一地址写一字节的数据和从AT24C02中任一地址读取一字节数据的功能,函数操作步骤完全遵循前面讲解的操作原理,请大家参考对照。 3)//读出保存的数据赋给sec //防止首次读取出错误数据 在主程序的开始处先读取上次写入AT24C02的数据,下面两句是为了防止第一次操作AT24C02时出现意外而加的,若是全新的AT24C02芯片或是以前已经被别人写过的不知道是什么内容的芯片,首次上电后读出来的数据我们无法知道,若是大于100的数将无法在数码管上显示而造成乱码,若是100以内的数还好处理。大家可自行修改程序使错误出现,再尝试修改程序看能否将错误排除。 实例演示实际现象可以达到实例的要求。