Chinaunix首页 | 论坛 | 博客
  • 博客访问: 119328
  • 博文数量: 29
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 242
  • 用 户 组: 普通用户
  • 注册时间: 2014-07-17 13:36
文章分类

全部博文(29)

文章存档

2015年(29)

我的朋友

分类: 嵌入式

2015-04-15 12:12:36

I2C软件实现

对于一些高级的处理器,可能提供专门的I2C控制单元,对于这些处理器来说,编程人员只需要通过Datasheet了解如何操作该I2C控制器即可,而不需要了解I2C协议以及I2C时序;只需要设置好对应的控制寄存器,并将数据放入到控制器的发送缓存器中,I2C控制器自动会将数据发送出去,这是因为在控制器中已经对I2C协议进行了封装,该封装可能是以软件逻辑的形式进行,也可能在硬件级别上就行了封装,不管是在软件级别还是硬件级别的封装,反正,我们只需要通过I2C软件的控制接口,就可以实现I2C的数据传输。

 

那对于一些较为低级的单片机,没有I2C控制器该怎么办?

那就只能自己动手,丰衣足食。根据I2C的时序图(即协议),在软件级别完成I2C协议了。

 

一、I2C原理

1、   二线制结构。即双向的串行数据线 SDA、串行同步时钟线 SCL。总线上的所有器件其同名端都分别挂在 SDASCL 线上,如下图:

2I C总线所有器件的SDASCL引脚的输出驱动都为漏极开路结构,通过外接上拉电阻将总线上所有节点的SDASCL信号电平实现“线与”的逻辑关系。这不仅可以将多个节点器件按同名端引脚直接挂在SDASCL线上,还使I2C总线具备了“时钟同步”、确保不同工作速度的器件同步工作;

3、系统中的所有外围器件都具有一个 7 位的 “从器件专用地址码”,其中高 4 位为器件类型地址(由生产厂家制定),低 3 位为器件引脚定义地址(由使用者定义),主控器件通过地址码建立多机通信的机制。因此I2C总线省去了外围器件的片选线,这样无论总线上挂接多少器件,其系统仍然为简约的二线结构;

4I2C总线上的所有器件都具有“自动应答”功能,保证了数据交换的正确性;

5I2C总线系统具有“时钟同步”功能。利用SCL线的“线与”逻辑协调不同器件之间的速

度问题;

6、在I2C总线系统中可以实现“多主机(主控器)”结构。依靠“总线仲裁”机制确保系统

中任何一个主控器都可以掌握总线的控制权。任何一个主控器之间没有优先级,没有中心主机的特权。当多主机竞争总线时,依靠主控器对其SDA信号的“线与”逻辑,自动实现“总线仲裁”功能;

7I2C总线系统中的主控器必须是带CPU 的逻辑模块;而被控器可以是无CPU 的普通外围器件,也可以是具有CPU 的逻辑模块。主控器与被控器的区别在于SCL的发送权,即对总线的控制权;

8I2C总线不仅广泛应用于电路板级的“内部通信”场合,还可以通过I2C总线驱动器进行不同系统间的通信;

9I2C总线的工作速度分为3种版本:S(标准模式),速率为100kb/s。主要用于简单的检   测与控制场合;F(快速模式),速率为400kb/s Hs(高速模式),速率为3.4Mb/s  

 

二、I2C工作过程与时序

 

总线上的所有通信都是由主控器引发的。在一次通信中,主控器与被控器总是在扮演着两种不同的角色。

 

1、  主控制器向被控器发送数据

操作过程如下:

(1)       主控器在检测到总线为“空闲状态”(即 SDASCL 线均为高电平)时,发送一个启动信号“S”,开始一次通信的开始;

(2)       主控器接着发送一个命令字节。该字节由 7 位的外围器件地址和 1 位读写控制位 R/W组成(此时 R/W=0 );

(3)       相对应的被控器收到命令字节后向主控器回馈应答信号ACKACK=0 );

(4)       主控器收到被控器的应答信号后开始发送第一个字节的数据;

(5)       被控器收到数据后返回一个应答信号 ACK

(6)       主控器收到应答信号后再发送下一个数据字节;  … … 

(7)       当主控器发送最后一个数据字节并收到被控器的 ACK         后,通过向被控器发送一个停止信号P 结束本次通信并释放总线。被控器收到P 信号后也退出与主控器之间的通信。

如图,为通讯过程示意图:


现在的问题时,如何实现上述的过程?

那就需要看时序图了。如下图为主控向被控单元写一个字节的时序图:


通过该时序图就可以进行编程了:

1、  初始化,从上图可以看出,初始时刻的SDASCL都为1。初始化函数如下:

void I2C_init(void)

{

    SCL = 1;

    delay();

    SDA = 1;

    delay();

}

说明:挺简单,就是把SCLSDA都拉高,其中,对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产生上升沿,此时SCLSDA都为1,表示该主控放弃总线控制

    delay();

}

 

4、  主控在发送数据后,等待从机返回ACK。如下为等待ACK的时序图。


说明:等待从机应答的过程中,SCL信号还是由主控进行控制,而SDA交个从机,如果从机在上升沿来临之前,将SDA拉低,就代表发送一个ACK,未拉低,代表无ACK,主控根据是否有ACK来决定下一步动作,比如数据重发。代码如下:

bit I2C_WaitACK()

{

bit ret_bit = 1;     //保存返回值

SCL = 0;             //保证改变SDA时,SCL=0,否则,就是发送SP信号

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,发送ACKsda_val=1,不发送ACK

    delay();

    SCL = 1;           //SCL拉高,告诉从机,我已经把ACK信号送到SDA上了

    delay();

    SCL = 0;           //重新获得SCL总线控制权

}

 

6、  主控向从机写一个字节。如下为写一个字节的时序图:


说明:主控在SCL0是改变SDA,改变完成后,使SCL1,从机自动接收数据,代码如下:

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_MasterACKI2C_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的地址引脚A2A1A0决定的,如果该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;          //返回接收到的信号

}

 

 

 

阅读(1937) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~