I2C总线与E2PROM
一、I2C总线
I2C总线是两线式串行总线,有两根双向信号线。一根是数据线SDA,一根是时钟线SCL。I2C总线的特点是:接口方式简单,两条线可以挂多个参与通信的器件,即多机模式,并且任何一个器件都可以作为主机(同一时刻只能有一个主机)。
1、I2C协议和UART协议的区别
-
从原理上看:UART属于异步通信,比如计算机发送给单片机,计算机只负责把数据通过TXD发送出来即可,接收数据是单片机自己的事情。而 I2C属于同步通信,SCL时钟线负责收发双方的时钟节拍,SDA数据线负责传输数据。I2C的发送方和接收方都以SCL
这个时钟节拍为基准进行数据的发送和接收。 -
从应用上看:UART通信名用于板间通信,比如单片机和计算机,一个设备和另一个设备之间的通信;而 I2C多用王板内通信,比如单片机和EPROM之间的通信。
2、I2C时序认识
处理器和芯片间的通信可以形象的比喻成两个人的说话:①双方说的是同一种语言:双方约定好通信协议(在这里就是I2C通信协议)②双方的语速彼此都能接受:双方满足时序要求。
I2C总线由时钟总线SCL和数据总线SDA构成,连接到总显示上的所有器件上的SCL都连到一起,所有SDA都连到一起。
I2C总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平。连到总线上的任一器件输出的低电平,都将使总线的信号变低,即各器件的SDA及SCL都是线“与”关系,这意味着任何一个器件都可以作为主机。51开发板上挂接了两个I2C设备,24C02和PCF8591。
UART通信流程包括起始位、数据位、·停止位三部分,其中每个字节都有一个起始位、8个数据位和1个停止位;I2C通信对应的包括起始信号、数据传输和停止信号三部分。数据传输部分可以在一次通信过程中传输很多个字节,每个字节后面都跟以为应答位,用ACK表示。I2C时序流程图如下:
3、I2C总线的数据传送
(1)数据位的有效性规定
I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,此时接收方在读取数据线的内容,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
(2)起始信号和停止信号
UART通信的起始位标志是默认高电平的情况下出现一个低电平,停止位标志是一位固定的高电平信号。
对于I2C通信,SCL线为高电平期间,SDA线由高电平向低电平变化,产生一个下降沿,表示起始信号;SCL线为高电平期间,SDA线由低电平向高电平变化,产生一个上升沿,表示终止信号。
起始和终止信号都是由主机发出的,在起始信号产生后,总线一直处于被占用的状态;终止信号产生后,总线恢复空闲状态。连接到 I2C总线上的器件,若具有I2C总线的硬件接口,则可检测到起始和终止信号。
接收器件收到一个完整的数据字节后,有可能需要完成一些其它工作,如处理内部中断服务等,可能无法立刻接收下一个字节,这时接收器件可以将SCL线拉成低电平,从而使主机处于等待状态。直到接收器件准备好接收下一个字节时,再释放SCL线使之为高电平,从而使数据传送可以继续进行。
(3)数据传输
UART 的数据传输是低位在前,高位在后;数据位是固定长度,波特率分之一,每一位在固定的时间内发送完毕。
而 I2C通信是高位在前,低位在后;没有固定的波特率,但是有时序的要求,只要在SCL为低电平的时候,数据线SDA才可以发生变化。具体时序图如下:
每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。
- 主机发送数据时:如果从机不能对主机的寻址信号进行应答,则必须将SDA(从机的数据线)置高电平,再由主机产生一个终止信号结束总线的数据传送;如果从机对主机进行了应答,在数据传输一段时间后,无法继续接收更多的数据,从机可以对无法接收的第一个数据字节 “非应答”,以通知主机,再由主机产生一个终止信号结束数据的继续传送。
- 主机接收数据时:收到最后一个数据字节后,必须向从机发送一个结束传送的信号。这个信号通过对从机“非应答”来实现。然后,从机释放SDA线,允许主机产生终止信号。
每次数据传送总是由 主机 产生终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
(4)整体数据传送过程
I2C总线传送的数据信号是广义的,包括地址信号和真正的数据信号。 在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/W),“0”表示主机发送数据(写),“1”表示主机接收数据(读)。
在总线的一次数据传送过程中,可以有以下几种组合方式:
- 主机向从机发送数据,数据传送在整个传送过程中不变:
注:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。 A表示应答, A非表示非应答(高电平)。S表示起始信号,P表示终止信号。
首先主机发送起始信号,紧接着发送7位从机地址和0(发送数据),此时地址匹配成功的从机发送应答位。接着不断重复主机发送数据,从机发送应答位,直到从机接收完毕后,发送非应答位,主机发送停止信号。
- 主机在第一个字节后,立即从从机读数据
- 在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,两次读/写方向位正好反相。
4、I2C总线的寻址模式
I2C总线协议有明确的规定:采用7位的寻址字节(寻址字节是起始信号后的第一个字节)。
- 寻址字节的位定义
D7~D1位组成从机的地址。D0位是数据传送方向位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。
- 主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,如果相同,则认为自己正被主机寻址,根据R/T位将自己确定为发送器或接收器。
- 从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的7位寻址位有4位是固定位,3位是可编程位,这时仅能寻址8个同样的器件,即可以有8个同样的器件接入到该I2C总线系统中。
二、I2C总线器件的扩展
1、扩展电路
2、E2PROM
在实际应用中,保存在单片机RAM中的数据断电即丢失,保存在单片机的FLASH中的数据不能随意改变,所以不能用来记录变化的数值。当需要满足断电不丢失,记录完数据之后还能更改时,例如记录家用电表度数,电视剧里的频道记忆,一般就需要使用E2PROM来保存数据。使用的是AT24C系列器件,比如AT24C02,容量是2Kb,即256字节的E2PROM,可以反复写入30万 ~ 100万次,读取次数无限。
I2C是一种通信协议,24C02是基于T2C通信协议的一个器件,只是这个器件采用了I2C协议的接口与单片机相连而已,二者没有必然联系。E2PROM可以用其他接口,I2C也可以用在很多其他器件上。
(1)向E2PROM写数据流程
注:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。 A表示应答, A非表示非应答(高电平)。S表示起始信号,P表示终止信号。
①首先是 I2C的起始信号,接着是 I2C的器件地址,并且在读写方向上选择 “写” 操作 “0”(共8位,即一个字节)。发送完后释放SDA线并在SCL线上产生第9个时钟信号。被选中的存储器器件在确认是自己的地址后,在SDA线上产生一个应答信号作为相应,单片机收到应答后就可以传送数据了。
②发送数据的存储地址。24C02一共256个字节的存储空间,地址从0x00 ~ 0xFF,想要把数据存储在哪个位置,此刻写的就是哪个地址。
③发送要存储数据的第一个字节、第二个字节…在写数据的过程中,E2PROM(从机)每个字节都会回应一个 “应答位0” ,告诉我们写E2PROM数据成功,如果没有回应应答位,则表示写入不成功。当要写入的数据传送完后,单片机应发出终止信号以结束写入操作。
在写数据的过程中,每成功写入一个字节,E2PROM存储空间的地址就会自动加1,当加到0xFF后,再写一个字节,地址会溢出又变成0x00。
(2)从E2PROM读数据流程
①首先是 I2C的起始信号,接着是 I2C的器件地址,并且在读写方向上选择 “写” 操作(伪读)(共8位,即一个字节)。发送完后释放SDA线并在SCL线上产生第9个时钟信号。被选中的存储器器件在确认是自己的地址后,在SDA线上产生一个应答信号作为回应。
为什么明明是读数据,却要先选择写操作呢?因为不管是向E2PROM里面写数据还是从E2PROM里面读数据,都需要先进行地址的匹配,即需要主机先写入存储器地址,各个存储器器件进行地址匹配,确定选中的存储器件,紧接着再进行主机和选中从机之间的读/写操作。
②发送要读取的数据的地址。需要从24C02的哪一个地址中读取数据,就发送哪一个地址。地址存在,则24C02(从机)发送应答信号。
③重新发送 I2C 起始信号和器件地址,并且在方向位上选择“读”操作。核对无误后,从机发送应答位。
④主机读取从E2PROM(24C02)里面发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个应答位ACK(0),如果不想读了,就发送一个非应答位NAK1,告诉E2PROM不想读了。
在写数据的过程一样,每读一个字节,E2PROM存储空间的地址就会自动加1。如果想继续读,就给E2PROM一个ACK(0)低电平,再继续给SCL完整的时序,E2PROM就会继续往外送数据,如果不想读,直接给一个NAK(1)高电平即可。
要注意以下几点:①在以上所述内容中,默认单片机是主机,E2PROM(24C02)是从机;②无论读写,都需要先写入存储器的地址,根据这个地址匹配到正确的从机。且SCL始终都是由主机控制的;③写的时候应答信号由从机给出,表示从机是否正确接收到数据;④读的时候应答信号则由主机给出,表示是否读下去。
三、模块化代码
1、i2c.h
#ifndef __I2C_h
#define __I2C_h
#include"system.h"
sbit SCL = P2^0;
sbit SDA = P2^1;
void I2cDelay(u16 t);
void I2cStart();
void I2cStop();
void I2cWrite(u8 dat);
u8 I2cRead();
bit Ack();
void E2Write(u8 addr,u8 dat);
u8 E2Read(u8 addr);
#endif
2、i2c.c
//延迟信号
void I2cDelay(u16 t)
{
do
{
_nop_();
}while(t--);
}
//起始信号
void I2cStart()
{
SCL = 1;
SDA = 1;
I2cDelay(5);
SDA = 0;
I2cDelay(5);
SCL = 0;
I2cDelay(5);
}
//停止信号
void I2cStop()
{
SCL = 0;
SDA = 0;
I2cDelay(5);
SCL = 1;
I2cDelay(5);
SDA = 1;
I2cDelay(5);
}
//总线读操作
u8 I2cRead()
{
u8 dat ,i;
for(i=0;i<8;i++)
{
SCL = 1; //读之前必须保证SCL为高电平
I2cDelay(5);
dat <<= 1;
if(SDA)
{
dat|=0x01;
}
SCL = 0;
}
return dat;
}
//总线写操作
void I2cWrite(u8 dat)
{
u8 i;
for(i=0;i<8;i++)
{
SCL = 0; //写之前确保SCL为低电平
SDA = dat & 0x80;
SCL = 1;
dat <<= 1;
}
SCL = 0;
}
//产生(非)应答信号
bit ACK() //返回值为1,表示发送应答信号
{
SCL = 1;//确保SCL为高电平,读取此时SDA的值,即(非)应答信号
I2cDelay(5);
if(SDA) //SDA = 1,表示发送非应答信号
{
SCL = 0;//拉低SCL完成应答位,保持住总线
I2cStop();
return 0;
}
else //SDA = 0,表示发送应答信号
{
SCL = 0; //拉低SCL完成应答位,保持住总线
return 1;
}
}
//从E2PROM读取一个字节
u8 E2Read(u8 addr)
{
u8 dat;
I2cStart();
I2cWrite(0xa0);//发送读器件的地址,后续为写操作
Ack();
I2cWrite(addr);//发送要读取数据的存储地址
Ack();
I2cStop();
I2cStart();
I2cWrite(0xa1);//发送读器件的地址,后续为读操作
Ack();
dat = I2cRead();
Ack();
return dat;
}
//向E2PROM写入一个字节
void E2Write(u8 addr,u8 dat)
{
I2cStart();
I2cWrite(0xa0); //发送写器件的地址,后续为写操作
Ack();
I2cWrite(addr); //发送要写入内存的地址
Ack();
I2cWrite(dat); //发送要写的内容
Ack();
I2cStop();
}
四、官方驱动源码改写
1、i2c.h
#ifndef __I2C_h
#define __I2C_h
#include "system.h"
sbit SCL = P2^0; //时钟线
sbit SDA = P2^1; //数据线
void IIC_Start(); //起始信号
void IIC_Stop(); //停止信号
bit IIC_WaitAck(); //等待应答信号
void IIC_SendAck(bit ackbit); //发送(非)应答信号
void IIC_SendByte(u8 byt);
u8 IIC_RecByte();
void IIC_Write(u8 addr ,u8 dat);//此处选择没有返回值
//或者选择为应答信号的返回值
u8 IIC_Read(u8 addr,u8 ack);
#endif
2、i2c.c
/*
程序说明: IIC总线驱动程序
软件环境: Keil uVision 4.10
硬件环境: CT107单片机综合实训平台 8051,12MHz
日 期: 2011-8-9
*/
#include "i2C.h"
//#define DELAY_TIME 5
#define SlaveAddrW 0xA0
#define SlaveAddrR 0xA1
#define somenop {_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();}
//定义延时时间,33个nop
//void IIC_Delay(unsigned char i)
//{
//do{_nop_();}
//while(i--);
//}
//总线启动条件
void IIC_Start(void)
{
SDA = 1;
SCL = 1;
somenop
SDA = 0;
somenop
SCL = 0;
}
//总线停止条件
void IIC_Stop(void)
{
SDA = 0;
SCL = 1;
somenop
SDA = 1;
somenop
}
//发送应答(写过程)
void IIC_SendAck(bit ackbit)
{
SCL = 0; //发送前确保时钟线为低电平
SDA = ackbit; // 0:应答,1:非应答
somenop
SCL = 1; //拉高时钟位
somenop
SCL = 0;
SDA = 1; //释放总线
somenop
}
//等待应答
bit IIC_WaitAck()
{
bit ackbit;
SCL = 1;
somenop
ackbit = SDA;
SCL = 0;
somenop
return ~ackbit; //取反之后,有应答返回值为1,无应答返回值为0
}
//通过I2C总线发送数据
void IIC_SendByte(u8 byt)
{
unsigned char i;
for(i=0; i<8; i++)
{
SCL = 0; //写之前确保时钟在下降沿
somenop
if(byt & 0x80) SDA = 1;
else SDA = 0;
somenop
SCL = 1; //拉高时钟完成数据发送
byt <<= 1;
somenop
}
SCL = 0;
}
//从I2C总线上接收数据
u8 IIC_RecByte(void)
{
unsigned char i, da;
for(i=0; i<8; i++)
{
SCL = 1;
somenop
da <<= 1;
if(SDA) da |= 1;
SCL = 0;
somenop
}
return da;
}
/*********************FOR_24C02***************/
//向从机中写入数据
void IIC_Write(u8 addr ,u8 dat)
{
//bit ack;
do{
IIC_Start();
IIC_SendByte(SlaveAddrW); //寻址器件,后续为写操作
IIC_Stop();
}while(!IIC_WaitAck());//检测是否接受到应答,
//如果接收到,IIC_WaitAck()返回值为1,取反为0,跳出循环
IIC_SendByte(addr);//写入存储地址
IIC_WaitAck();
IIC_SendByte(dat);//发送要写入的数据
//ack = IIC_WaitAck();
IIC_WaitAck();
IIC_Stop();
//return ack;
}
//读取E2PROM中的一个字节
u8 IIC_Read (u8 addr,u8 ack)
{
u8 dat;
do{
IIC_Start();
IIC_SendByte(SlaveAddrW);//发送器件地址+写
IIC_Stop();
}while(!IIC_WaitAck());//检查是否收到应答
IIC_SendByte(addr);//发送存储地址
IIC_WaitAck();
IIC_Start();
IIC_SendByte(SlaveAddrR);//发送器件地址+读
IIC_WaitAck();//检查是否收到应答
dat=IIC_RecByte();//读取待返回的数据
IIC_SendAck(ack);//应答读取结束发1,继续发0
//IIC_Stop();
return dat;
}
总结,改动的地方有以下几点:
①延时函数替换成somenop,定义如下
#define somenop {_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();}
一共33个_nop_();
官方驱动代码给的延时使用于89C52,蓝桥用的板子时速相对快的多,所以一直加到了33个_nop_()。
②等待应答函数 bit IIC_WaitAck()
返回值取反,返回1表示应答,返回0表示非应答,符合正逻辑。
③有关24C02部分需要自己写。