2015年(29)
分类: 嵌入式
2015-04-15 12:12:36
对于一些高级的处理器,可能提供专门的I2C控制单元,对于这些处理器来说,编程人员只需要通过Datasheet了解如何操作该I2C控制器即可,而不需要了解I2C协议以及I2C时序;只需要设置好对应的控制寄存器,并将数据放入到控制器的发送缓存器中,I2C控制器自动会将数据发送出去,这是因为在控制器中已经对I2C协议进行了封装,该封装可能是以软件逻辑的形式进行,也可能在硬件级别上就行了封装,不管是在软件级别还是硬件级别的封装,反正,我们只需要通过I2C软件的控制接口,就可以实现I2C的数据传输。
那对于一些较为低级的单片机,没有I2C控制器该怎么办?
那就只能自己动手,丰衣足食。根据I2C的时序图(即协议),在软件级别完成I2C协议了。
一、I2C原理
1、 二线制结构。即双向的串行数据线 SDA、串行同步时钟线 SCL。总线上的所有器件其同名端都分别挂在 SDA、SCL 线上,如下图:
2、I C总线所有器件的SDA、SCL引脚的输出驱动都为漏极开路结构,通过外接上拉电阻将总线上所有节点的SDA、SCL信号电平实现“线与”的逻辑关系。这不仅可以将多个节点器件按同名端引脚直接挂在SDA、SCL线上,还使I2C总线具备了“时钟同步”、确保不同工作速度的器件同步工作;
3、系统中的所有外围器件都具有一个 7 位的 “从器件专用地址码”,其中高 4 位为器件类型地址(由生产厂家制定),低 3 位为器件引脚定义地址(由使用者定义),主控器件通过地址码建立多机通信的机制。因此I2C总线省去了外围器件的片选线,这样无论总线上挂接多少器件,其系统仍然为简约的二线结构;
4、I2C总线上的所有器件都具有“自动应答”功能,保证了数据交换的正确性;
5、I2C总线系统具有“时钟同步”功能。利用SCL线的“线与”逻辑协调不同器件之间的速
度问题;
6、在I2C总线系统中可以实现“多主机(主控器)”结构。依靠“总线仲裁”机制确保系统
中任何一个主控器都可以掌握总线的控制权。任何一个主控器之间没有优先级,没有中心主机的特权。当多主机竞争总线时,依靠主控器对其SDA信号的“线与”逻辑,自动实现“总线仲裁”功能;
7、I2C总线系统中的主控器必须是带CPU 的逻辑模块;而被控器可以是无CPU 的普通外围器件,也可以是具有CPU 的逻辑模块。主控器与被控器的区别在于SCL的发送权,即对总线的控制权;
8、I2C总线不仅广泛应用于电路板级的“内部通信”场合,还可以通过I2C总线驱动器进行不同系统间的通信;
9、I2C总线的工作速度分为3种版本:S(标准模式),速率为100kb/s。主要用于简单的检 测与控制场合;F(快速模式),速率为400kb/s ;Hs(高速模式),速率为3.4Mb/s 。
二、I2C工作过程与时序
总线上的所有通信都是由主控器引发的。在一次通信中,主控器与被控器总是在扮演着两种不同的角色。
1、 主控制器向被控器发送数据
操作过程如下:
(1) 主控器在检测到总线为“空闲状态”(即 SDA、SCL 线均为高电平)时,发送一个启动信号“S”,开始一次通信的开始;
(2) 主控器接着发送一个命令字节。该字节由 7 位的外围器件地址和 1 位读写控制位 R/W组成(此时 R/W=0 );
(3) 相对应的被控器收到命令字节后向主控器回馈应答信号ACK(ACK=0 );
(4) 主控器收到被控器的应答信号后开始发送第一个字节的数据;
(5) 被控器收到数据后返回一个应答信号 ACK;
(6) 主控器收到应答信号后再发送下一个数据字节; … …
(7) 当主控器发送最后一个数据字节并收到被控器的 ACK 后,通过向被控器发送一个停止信号P 结束本次通信并释放总线。被控器收到P 信号后也退出与主控器之间的通信。
如图,为通讯过程示意图:
现在的问题时,如何实现上述的过程?
那就需要看时序图了。如下图为主控向被控单元写一个字节的时序图:
通过该时序图就可以进行编程了:
1、 初始化,从上图可以看出,初始时刻的SDA和SCL都为1。初始化函数如下:
void I2C_init(void)
{
SCL = 1;
delay();
SDA = 1;
delay();
}
说明:挺简单,就是把SCL和SDA都拉高,其中,对SDA拉高认为是对数据线的释放。
2、 主控器发送开始信号START。如下图为START信号的时序:
说明:在SCL为高电平时,SDA出现一个SDA,就代表产生一个START信号。
程序如下:
void I2C_Start(void)
{
SDA = 1; //使SDA变为高,使之能在后面产生下降沿
delay();
SCL = 1; //SCL拉高
delay();
SDA = 0; //SCL为高电平情况下,SDA拉低,产生下降沿,开始信号完成
delay();
SCL = 0; //主控获取SCL,记住,在开始后,主控要将SCL信号把握在自己手里
delay();
SDA = 1; //释放数据总线,在SCL=0时,可以随时改变SDA
}
3、 主控器发送停止信号P。如下为停止信号的时序图。
说明:在SCL为高电平时,SDA产生一个上升沿,就是停止信号,代码如下:
void I2C_Stop(void)
{
SDA = 0; //拉低SDA,使后续产生上升沿
delay();
SCL = 1;
delay();
SDA = 1; //SDA产生上升沿,此时SCL、SDA都为1,表示该主控放弃总线控制
delay();
}
4、 主控在发送数据后,等待从机返回ACK。如下为等待ACK的时序图。
说明:等待从机应答的过程中,SCL信号还是由主控进行控制,而SDA交个从机,如果从机在上升沿来临之前,将SDA拉低,就代表发送一个ACK,未拉低,代表无ACK,主控根据是否有ACK来决定下一步动作,比如数据重发。代码如下:
bit I2C_WaitACK()
{
bit ret_bit = 1; //保存返回值
SCL = 0; //保证改变SDA时,SCL=0,否则,就是发送S或P信号
delay();
SDA = 1; //将SDA拉高,释放数据总线,交给从机控制
//此时,从机根据实际情况决定是否拉低SDA
delay();
SCL = 1; //从机做好了决定,拉高SCL,查看SDA线
delay();
if(SDA)ret_bit = 0; //如果SDA没被拉高,说明从机不打算发送ACK,返回变0
SCL = 0; //主控获取SCL总线
delay();
return ret_bit; //ret_bit=0,无ACK; ret_bit=1,有ACK
}
5、 主控作为接收端,需要发送ACK。
时序图与情况4相同,只是,此时SDA由主控控制,代码如下:
void I2C_MasterACK(bit sda_val)
{
SCL = 0; //改变SDA,保证SCL=0
delay();
SDA = sda_val; //sda_val=0,发送ACK;sda_val=1,不发送ACK
delay();
SCL = 1; //SCL拉高,告诉从机,我已经把ACK信号送到SDA上了
delay();
SCL = 0; //重新获得SCL总线控制权
}
6、 主控向从机写一个字节。如下为写一个字节的时序图:
说明:主控在SCL为0是改变SDA,改变完成后,使SCL为1,从机自动接收数据,代码如下:
void I2C_Write(unsigned char byte)
{
int i = 7; //发送7位数据,用于计数
SCL = 0; //拉低SCL,可以改变SDA
delay();
while(i >= 0)
{
SDA = (byte >> i) & 1; //发送第i位,i取值7~0
delay();
SCL = 1; //SDA稳定后,SCL拉高,告诉从机数据已上线
delay();
i--;
SCL = 0; //拉低SCL,重新改变SDA
delay();
}
}
7、 主控从从机出读取一个字节,如下为主控读取一个字节的时序:
说明:是不是感觉和主控发送一个字节的时序很相似?的确,但是,这里有两点区别:
A、 在读取数据时,主控将SDA拉高,放弃对SDA的控制,SDA完全交给从机去写数据,主控通过SDA来获取数据;
B、 发送ACK的对象变为主控,而非从机。
读字节代码如下:
unsigned char I2C_Read()
{
unsigned char cur_val = 0; //cur_val存放从从机发送的数据
int i = 0; //计数,接收8为数据
SCL = 0; //拉低SCL,之后从机去改变SDA
delay();
SDA = 1; //主机放弃对SDA的控制
delay();
for(;i<8;++i)
{
SCL = 1; //拉高SCL,开始去数据的一位
delay();
cur_val <<= 1; //为新取得的位准备存放位置
cur_val |= SDA; //将新取得的位放入cur_val
SCL = 0; //拉低SCL,告诉从机又可以改变SDA了
delay();
}
I2C_MasterACK(0); //发送ACK,如果参数为1,不是不发送ACK
return cur_val; //返回从从机取得的值
}
到现在为止,I2C的所有操作协议都变为代码了,码农就可以调用函数进行数据传输了,说明要注意的几个地方:
1、 在开始写,或者读数据开始前,先要向从机写一个地址控制字节,这个别忘了。结构为”4bit器件类型 + 3bit地址 + 1bit R/W“;注意在从读变为写时,一定先发送起始信号S,即调用I2C_Start,然后发送该字节,否则出错。
2、 在写完后一个字节后,一定要有一个等待ACK的过程,即在I2C_Write函数后,一定要有对I2C_WaitACK函数的调用。
3、 在读完一个字节后,要有一个发送ACK的过程,即在I2C_Read函数中,要有一个I2C_MasterACK函数的调用,当然,也可以将I2C_MasterACK从I2C_Read中提取出来,这样就和I2C_Write一样,对ACK的处理由用户调用。
4、 在通讯结束后,别忘了发送停止信号,即调用I2C_Stop。
在上述功能函数的基础上实现对AT24C02的控制,对AT24C02进行按位读写。
对AT24C02写操作如下:
第一步:发送开始信号S;
第二步:将AT24C02的地址信息写到总线;
第三步:等待从机的ACK;
第四步:给出要写的ROM地址写到总线;
第五步:将要写的数据写入到总线;
第六步:等待从机的ACK;
第七步:发送停止信号P;
代码如下:
void write_add(unsigned char address,unsigned char dat)
{
I2C_Start(); //发送S
I2C_Write(0xa0); //写AT24C02设备地址
I2C_WaitACK(); //等待从机ACK
I2C_Write(address); //写所操作的ROM的地址
I2C_WaitACK(); //等待从机ACK
I2C_Write(dat); //写数据
I2C_WaitACK(); //等待从机ACK
I2C_Stop(); //发送P
}
说明:
1、 设备地址为0xa0的原因,AT24C02的器件类型为1010b,这是由厂商决定的,所以高4位为a,地址是通过AT24C02的地址引脚A2、A1、A0决定的,如果该3个引脚都为低电平,地址就为0,最后一位为R/W位,我们这里要写,所以为0;
2、 该函数中没有对I2C_WaitACK的返回值进行判断,按理说,应该通过判断从机是否有ACK发送到总线来决定程序的执行流程,这里为了简单起见,就忽略了返回值;
对AT24C02的读过程:
对AT24C02的读过程比写过程步骤稍微复杂一点,因为需要先写入读取的ROM地址;然后在将I2C转变为读模式,将数据信息读到主机中,代码如下:
unsigned char read_add(unsigned char address)
{
unsigned char date; //定义存储从EEPROM中读取数据的变量
I2C_Start(); //发送S信号
I2C_Write(0xa0); //在总线上发送地址控制信号,写模式
I2C_WaitACK(); //等待从机ACK
I2C_Write(address); //在总线上发送ROM地址
I2C_WaitACK(); //等待从机ACK
I2C_Start(); //重新开始,发送S信号,当模式转换时必须重新开始
I2C_Write(0xa1); //发送地址控制信号,读模式
I2C_WaitACK(); //等待从机ACK
date = I2C_Read(); //从SDA中读取数据,并存放到date中,在I2C_Read
//中有I2C_MasterACK,自动发送ACK给从机
I2C_Stop(); //发送停止信号P
return date; //返回接收到的信号
}