作者:sumjess



一、I2c总线概述:

1、I2c总线介绍

      l2C总线(InterICBus)由PHILIPS公司推出,是近年来微电子通信控制领域广泛采用的一种新型总线标准,它是同步通信的一种特殊形式,具有接口线少、控制简单、器件封装形式小、通信速率较高等优点。在主从通信中,可以有多个l2C总线器件同时接到l2C总线上,所有与I2C兼容的器件都具有标准的接口,通过地址来识别通信对象,使它们可以经由l2C总线互相直接通信。

      I2C总线由数据线SDA和时钟线SCL两条线构成通信线路,既可发送数据,也可接收数据。在CPU与被控IC之间、IC与IC之间都可进行双向传送,最高传送速率为400kbps,各种被控器件均并联在总线上,但每个器件都有唯一的地址。在信息传输过程中,l2C总线上并联的每一个器件既是被控器(或主控器),又是发送器(或接收器),这取决于它所要完成的功能。CPU发出的控制信号分为地址码和数据码两部分:地址码用来选址,即接通需要控制的电路;数据码是通信的内容,这样各IC控制电路虽然挂在同一条总线上,却彼此独立。

2、I2c总线硬件结构图

      下图为I2C总线系统的硬件结构图,其中,SCL是时钟线,SDA是数据线。总线上各器件都采用漏极开路结构与总线相连,因此SCL和SDA均需接上拉电阻,总线在空闲状态下均保持高电平,连到总线上的任一器件输出的低电平,都将使总线的信号变低,即各器件的SDA及SCL都是线”与“关系。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_IIC总线


      I2C总线支持多主和主从两种工作方式,通常为主从工作方式。在主从工作方式中,系统中只有一个主器件(单片机),其他器件都是具有I2C总线的外围从器件。在主从工作方式中,主器件启动数据的发送(发出启动信号),产生时钟信号,发出停止信号。

3、I2c总线通信格式

      图8.1.2为l2C总线上进行一次数据传输的通信格式。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_AT24C02_02


4、数据位的有效性规定

      I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟信号为低电平期间,数据线上的高电平或低电平状态才允许变化,如图8.1.3所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_STC51_03


5、发送启动(始)信号

      在利用l2C总线进行一次数据传输时,首先由主机发出启动信号,启动l2C总线。在SCL为高电平期间,SDA出现上升沿则为启动信号。此时,具有I2C总线接口的从器件会检测到该信号,启动时序如图8.1.4所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_STC51_04


6、发送寻址信号

      主机发送启动信号后,再发出寻址信号。器件地址有7位和10位两种,这里只介绍7位地址寻址方式。寻址字节的位定义如图8.1.5所示,寻址信号由一个字节构成,高7位为地址位,最低位为方向位,用以表明主机与从器件的数据传送方向。方向位为0,表明主机接下来对从器件进行写操作;方向位为1,表明主机接下来对从器件进行读操作。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_IIC总线_05


      主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,如果相同,则认为自己正被主机寻址,根据RJW位将自己确定为发送器或接收器。

      从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的7位寻址位有4位是固定位,3位是可编程位,这时仅能寻址8个同样的器件,即可以有8个同样的器件接入到该l2C总线系统中。

7.、应答信号

      l2C总线协议规定,每传送一个字节数据(含地址及命令字)后,都要有一个应答信号,以确定数据传送是否被对方收到。应答信号由接收设备产生,在SCL信号为高电平期间,接收设备将SDA拉为低电平,表示数据传输正确,产生应答,时序图如图8.1.6所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_AT24C02芯片的应用_06


8、数据传输

主机发送寻址信号并得到从器件应答后,便可进行数据传输,每次一个字节,但每次传输都应在得到应答信号后再进行下一字节传送。

9、非应答信号

当主机为接收设备时,主机对最后一个字节不应答,以向发送设备表示数据传送结束。

10、发送停止信号

在全部数据传送完毕后,主机发送停止信号,即在SCL为高电平期间,SDA上产生一上升沿信号,停止时序图如图8.1.7所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_IIC总线_07

二、单片机模拟I2c总线通信:

      目前市场上很多单片机都已经具有硬件l2C总线控制单元,这类单片机在工作时,总线状态由硬件监测,无须用户介入,操作非常方便。但是还有许多单片机并不具有l2C总线接口,如51单片机,不过我们可以在单片机应用系统中通过软件模拟l2C总线的工作时序,在使用时,只需正确调用各个函数就能方便地扩展l2C总线接口器件。

在总线的一次数据传送过程中,可以有以下几种组合方式

      (1)主机向从机发送数据,数据传送方向在整个传送过程中不变。

      (2)主机在第一个字节后,立即从从机读数据。

      (3)在传送过程中,当需要改变传送方向时,需将起始信号和从机地址各重复产生一次,而两次读/写方向位正好相反。

      为了保证数据传送的可靠性,标准I2C总线的数据传送有严格的时序要求。I2C总线的起始信号、终止信号、应答或发送"0"、非应答或发送"1”的模拟时序如图8.2.1所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_IIC总线_08


      单片机在模拟l2C总线通信时,需写出如下几个关键部分的程序:总线初始化、启动信号、应答信号、停止信号、写一个字节、读一个字节。下面分别给出具体函数的写法供大家参考,在阅读代码时请参考前面相关部分的文字描述及时序图。

1、总线初始化

void init()

SCL=1;
delay();
SDA=1;
delay();

将总线都拉高以释放总线。

2、启动信号

void start()

SDA=1;
delay();
SCL=1;
delay();
SDA=0;
delay();

SCL在高电平期间,SDA一个下降沿启动信号。

3、应答信号

void respons()

uchar i=0;
SCL=1;
delay();
while((SDA==1)&&(i<255))
i++;
SCL=0;
delay();

SCL在高电平期间,SDA被从设备拉为低电平表示应答。上面代码中有一个(SDA==1)和(i<255)相与的关系,表示若在一段时间内没有收到从器件的应答则主器件默认从器件已经收到数据而不在等待应答信号,这一点是作者后加的一步,大家可不必深究,因为如果不加这个延时退出,一旦从器件没有发送应答信号,程序将永远停止在这里,而真正的程序中是不允许这样的情况发生的。
4、停止信号

void stop()

SDA=0;
delay();
SCL=1;
delay();
SDA=1;
delay();

SCL在高电平期间,SDA个上升沿停止信号。

5、写一个字节

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的控制下发送出去。

6、读一个字节

uchar readbyte()

uchar i,k;
SCL=0;
delay();
SDA=1;
for(i=0;i<8;i++)

SCL=1;
delay();
k=(k<<l) I SDA;
SCL=0;
delay();

delay();
return k;

同样的,串行接收一个字节时需将8位一位一位地接收,然后再组合成一个字节,上面代码中我们定义了一个临时变量k,将K左移一位后与SDA进行”或“运算,依次把8个独立的位放入一个字节中来完成接收。

三、E2PROM AT24C02与单片机的通信实例:

具有l2C总线接口的E2PROM有多个厂家的多种类型产品。在此仅介绍ATMEL公司生产的AT24C系列E2PROM,主要型号有AT24CO1/02/04/08/16等,其对应的存储容量分别为128x8/256x8/512x8/1024x8/2048x8。采用这类芯片可解决掉电数据保存问题,可对所存数据保存100年,并可多次擦写,擦写次数可达10万次以上。

在一些应用系统设计中,有时需要对工作数据进行掉电保护,如电子式电能表等智能化产品。若采用普通存储器,在掉电时需要备用电池供电,并需要在硬件上增加掉电检测电路,但存在电池不可靠及扩展存储芯片占用单片机过多口线的缺点。采用具有l2C总线接口的串行E2PROM器件可很好地解决掉电数据保存问题,且硬件电路简单。下面以AT24C02芯片为例,介绍具有I2C总线接口的E2PROM的具体应用。

1、AT24C02引脚配置与引脚功能:

      AT24C02芯片的常用封装形式有直插(DIP8)式和贴片(S0-8)式两种,实物图分别如图8.3.1和图8.3.2所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_I2C通信_09


各引脚功能如下:

1,2,3(A0、A1、A2)—可编程地址输入端。

4(GND) —电源地。

5(SDA) —串行数据输入/输出端。

6(SCL) 一串行时钟输入端。

7(WP) —写保护输入端,用于硬件数据保护。当其为低电平时,可以对整个存储器进行正常的读/写操作;当其为高电平时,存储器具有写保护功能,但读操作不受影响。

8(Vee) —电源正端。2、存储结构与寻址

AT24C02的存储容量为2KB,内部分成32页,每页8B,共256B,操作时有两种寻址方式:芯片寻址和片内子地址寻址。

(1)芯片寻址。AT24C02的芯片地址为1010, 其地址控制字格式为1010A2A1A0R/w。

其中A2,A1,A0为可编程地址选择位。A2,A1, A0引脚接高、低电平后得到确定的三位编码,与1010形成7位编码,即为该器件的地址码。RJw为芯片读写控制位,该位为0,表示对芯片进行写操作;该位为1,表示对芯片进行读操作。

(2)片内子地址寻址。芯片寻址可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF, 共256个寻址单元。

3、读/写操作时序

      串行E2PROM一般有两种写入方式:一种是字节写入方式,另一种是页写入方式。页写入方式允许在一个写周期内(10ms左右)对一个字节到一页的若干字节进行编程写入,AT24C02的页面大小为8B。采用页写方式可提高写入效率,但也容易发生事故。AT24C系列片内地址在接收到每一个数据字节后自动加1,故装载一页以内数据字节时,只需输入首地址,如果写到此页的最后一个字节,主器件继续发送数据,数据将重新从该页的首地址写入,进而造成原来的数据丢失,这就是页地址空间的“上卷“现象。

      解决“上卷"的方法是:在第8个数据后将地址强制加1,或是将下一页的首地址重新赋给寄存器。

      (1)字节写入方式。单片机在一次数据帧中只访问E2PROM一个单元。该方式下,单片机先发送启动信号,然后送一个字节的控制字,再送一个字节的存储器单元子地址,上述几个字节都得到E2PROM响应后,再发送8位数据,最后发送1位停止信号。发送格式如图8.3.5所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_STC51_10


      (2)页写入方式。单片机在一个数据写周期内可以连续访问1页(8个)E2PROM存储单元。在该方式中,单片机先发送启动信号,接着送一个字节的控制字,再送1个字节的存储器起始单元地址,上述几个字节都得到E2PROM应答后就可以发送最多1页的数据,并顺序存放在以指定起始地址开始的相继单元中,最后以停止信号结束。页写入帧格式如图8.3.6所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_I2C通信_11


(3)指定地址读操作。读指定地址单元的数据。单片机在启动信号后先发送含有片选地址的写操作控制字,E2PROM应答后再发送1个(2KB以内的E2PROM)字节的指定单元的地址,E2PROM应答后再发送1个含有片选地址的读操作控制字,此时如果E2PROM做出应答,被访问单元的数据就会按SCL信号同步出现在串行数据/地址线SDA上。这种读操作的数据帧格式如图8.3.7所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_AT24C02_12


(4)指定地址连续读。此种方式的读地址控制与前面指定地址读相同。单片机接收到每个字节数据后应做出应答,只要E2PROM检测到应答信号,其内部的地址寄存器就自动加1指向下一单元,并顺序将指向的单元的数据送到SDA串行数据线上。当需要结束读操作时,单片机接收到数据后在需要应答的时刻发送一个非应答信号,接着再发送一个停止信号即可。这种读操作的数据帧格式如图8.3.8所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_STC51_13


4、TX-1C实验板上AT24C02连接图

      TX-1C实验板上AT24C02与单片机连接如图8.3.9所示,其中A0,A1,A2与WP都接地,SDA接单片机P2.0脚,SCL接单片机P2.1脚,SDA与SCL分别与Vcc之间接一1KΩ上拉电阻,因为AT24C02总线内部是漏极开路形式,不接上拉电阻无法确定总线空闲时的电平状态。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_AT24C02芯片的应用_14


      【例8.3.1】用C语言编写程序,具体操作:① 按设置按钮保存按确认按钮读取EEPROM值 ;② 按时间-按钮减数 ;③ 按时间+按钮加数 ; 新建文件Sumjess2.6_1.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)"void delay(){;;}"是一个微秒级延时函数,以前编写的延时函数内部都是用变量递增或是递减来实现延时,而这个函数是用空语句来实现短时间延时的,在Keil软件中设置晶振为11.0592MHz时,该延时函数延时大概4~5微秒,用来操作l2C总线时用。
(2)"void write_add(uchar address,uchar date)"和"uchar read_add(uchar address)"两个函数分别实现向AT24C02的任一地址写一字节的数据和从AT24C02中任一地址读取一字节数据的功能,函数操作步骤完全遵循前面讲解的操作原理,请大家参考对照。
(3)sec=read_add(2); //读出保存的数据赋给sec
if(sec>100) //防止首次读取出错误数据
sec=0;
      在主程序的开始处先读取上次写入AT24C02的数据,下面两句是为了防止第一次操作AT24C02时出现意外而加的,若是全新的AT24C02芯片或是以前已经被别人写过的不知道是什么内容的芯片,首次上电后读出来的数据我们无法知道,若是大于100的数将无法在数码管上显示而造成乱码,若是100以内的数还好处理。大家可自行修改程序使错误出现,再尝试修改程序看能否将错误排除。
      实例演示实际现象如图 8.3.10 所示。

STC51入门笔记(郭天祥C语言)---第七节:l2C总线AT24C02芯片应用_AT24C02_15