(转载)嵌入式Linux内核I2C子系统详解

2019-07-12 17:02发布

本文转自: http://www.embeddedlinux.org.cn/html/yingjianqudong/201303/17-2502.html
   为防止作者删除原文,也方便个人记录查找,特地转载了过来。这篇文章对于初学者去了解,学习 I2C是很有益处的。文章从原理到应用(代码),条理清晰,内容通俗易懂。

1.1 I2C总线知识

1.1.1  I2C总线物理拓扑结构



     I2C总线在物理连接上非常简单,分别由SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成。 通信原理是通过对SCL和SDA线高低电平时序的控制,来产生I2C总线协议所需要的信号进行数据的 传递。在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。

1.1.2  I2C总线特征

    I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址 (可以从I2C器件的数据手册得知),主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常 的应用中,我们把CPU带I2C总线接口的模块作为主设备,把挂接在总线上的其他设备都作为从设备。    I2C总线上可挂接的设备数量受总线的最大电容400pF 限制,如果所挂接的是相同型号的器件, 则还受器件地址位的限制。
   I2C总线数据传输速率在标准模式下可达100kbit/s,快速模式下可达400kbit/s,高速模式下
可达3.4Mbit/s。一般通过I2C总线接口可编程时钟来实现传输速率的调整,同时也跟所接的上拉电阻 的阻值有关。    I2C总线上的主设备与从设备之间以字节(8位)为单位进行双向的数据传输。

1.1.3  I2C总线协议

    I2C协议规定,总线上数据的传输必须以一个起始信号作为开始条件,以一个结束信号作为传输 的停止条件。起始和结束信号总是由主设备产生。总线在空闲状态时,SCL和SDA都保持着高电平, 当SCL为高电平而SDA由高到低的跳变,表示产生一个起始条件;当SCL为高而SDA由低到高的跳变, 表示产生一个停止条件。在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占, 其他I2C器件无法访问总线;而在停止条件产生后,本次数据传输的主从设备将释放总线,总线再次 处于空闲状态。如图所示:

    在了解起始条件和停止条件后,我们再来看看在这个过程中数据的传输是如何进行的。前面 我们已经提到过,数据传输以字节为单位。主设备在SCL线上产生每个时钟脉冲的过程中将在SDA 线上传输一个数据位,当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线, 回传给主设备一个应答位,此时才认为一个字节真正的被传输完成。当然,并不是所有的字节传输都 必须有一个应答位,比如:当从设备不能再接收主设备发送的数据时,从设备将回传一个否定应答位。 数据传输的过程如图所示:

   在前面我们还提到过,I2C总线上的每一个设备都对应一个唯一的地址,主从设备之间的数据传输 是建立在地址的基础上,也就是说,主设备在传输有效数据之前要先指定从设备的地址,地址指定的 过程和上面数据传输的过程一样,只不过大多数从设备的地址是7位的,然后协议规定再给地址添加 一个最低位用来表示接下来数据传输的方向,0表示主设备向从设备写数据,1表示主设备向从设备 读数据。如图所示:

1.1.4  I2C总线操作

    对I2C总线的操作实际就是主从设备之间的读写操作。大致可分为以下三种操作情况:
    第一,主设备往从设备中写数据。数据传输格式如下:



    第二,主设备从从设备中读数据。数据传输格式如下:

    第三,主设备往从设备中写数据,然后重启起始条件,紧接着从从设备中读取数据; 或者是主设备从从设备中读数据,然后重启起始条件,紧接着主设备往从设备中写数据。 数据传输格式如下:

    第三种操作在单个主设备系统中,重复的开启起始条件机制要比用STOP终止传输后 又再次开启总线更有效率。

1.2 I2C总线硬件接口电路示例

1.2.1 I2C总线硬件接口电路示例一



    这个电路是基于LPC2368 ARM7芯片进行设计的,使用其内部的I2C接口作为主设备, 使用ADT75和SC16IS740作为两个从设备的I2C总线应用。          ADT75是一个带I2C接口的温度传感器器件,数据手册上对其地址的描述如下:
    由此,其地址跟A0、A1、A2引脚的接法有关,我们这里的实例是将A0、A1、A2全部接到高电平上, 因此其地址是:1001111(即0x4F),又因根据协议再给地址添加一个最低位(方向位,默认给写方向), 因此最后这个温度传感器作为从设备的地址是:10011110(即0x9E)。     SC16IS740是一个具有I2C或者SPI接口的扩展UART的器件(通过第8脚来决定使用I2C还是SPI接口,我们 这里要求使用I2C接口,因此将第8脚接到高电平)。根据数据手册,我们同样的可以知道地址跟A0、A1的接法 有关,我们这里的A0接高电平,A1接低电平。因此这个器件作为从设备的地址是:10010010(即0x92)。

1.2.2 I2C总线硬件接口电路示例二


    这个电路是Mini2440开发板上I2C总线接口的应用。我们可以看到,SDA和SCL线上接了一个10K 的上拉排阻。AT24C08是一个容量为8Kbit的EEPROM存储器件(注意是8Kbit,也就是1KB) ,根据
数据手册中器件地址部分的描述,AT24C08的地址是:1010+A2A1A0+方向位,其中1010是EEPROM
的类型识别符;仅仅使用A2来确定总线访问本器件的从设备地址,这里接的低电平,所以为0;A1和A0 是器件内部页地址,在对器件擦除或者编程时使用,虽然这里也接的低电平,但器件内部并不使用引脚的 输入值,也就是说A1和A0的值是由软件进行设定的。

1.3 脱离操作系统的I2C总线驱动示例(以电路示例一为例)

1.3.1 LPC2368中I2C接口寄存器描述

    LPC2368中有三个I2C总线接口,分别表示为I2C0、I2C1和I2C2,每个I2C接口都包含7个寄存器。 它们分别是:I2C控制置位寄存器(I2CONSET): 8位寄存器,各位不同的设置是对I2C总线不同的控制。   符号 描述 复位值 1:0 - 保留,用户软件不要向其写入1。从保留位读出的值未被定义 NA 2 AA 声明应答标志。为1时将为需要应答的情况产生一个应答 0 3 SI I2C中断标志。当I2C状态改变时该位置位 0 4 STO 总线停止条件控制。1发出一个停止条件,当总线检测到停止条件时,STO自动清零 0 5 STA 总线起始条件控制。1进入主模式并发出一个起始条件 0 6 I2EN 总线使能控制。1为使能 0 7 - 保留,用户软件不要向其写入1。从保留位读出的值未被定义 NA  
    I2C控制清零寄存器(I2CONCLR): 8位寄存器,对I2CONSET寄存器中的相应为清零。
  符号 描述 复位值 1:0 - 保留,用户软件不要向其写入1。从保留位读出的值未被定义 NA 2 AAC 声明应答标志清零位。向该位写入1清零I2CONSET寄存器中的AA 0 3 SIC 中断标志清零位。向该位写入1清零I2CONSET寄存器中的SI 0 4 - 保留,用户软件不要向其写入1。从保留位读出的值未被定义 NA 5 STAC 起始条件清零位。向该位写入1清零I2CONSET寄存器中的STA 0 6 I2ENC 总线禁能控制。写入1清零I2CONSET寄存器中的I2EN 0 7 - 保留,用户软件不要向其写入1。从保留位读出的值未被定义 NA  
I2C状态寄存器(I2STAT): 8位只读寄存器,用于监控总线的实时状态(可能存在26种状态)。
符号描述复位值2:0-3个位不使用且总是为0 07:3Status这些位给出I2C接口的实时状态,不同的值代表不同的状态,状态码请参考数据手册0x1F
I2C数据寄存器(I2DAT): 8位寄存器,在SI置位期间,I2DAT中的数据保持稳定。
符号描述复位值7:0Data该寄存器保留已经接收到或者准备要发送的数据值 0
I2C从地址寄存器(I2ADR): 8位寄存器,I2C总线为从模式时才使用。主模式中该寄存器无效。
符号描述复位值0GC通用调用使能位 07:1Address从模式的I2C器件地址0x00
SCH占空比寄存器(I2SCLH): 16位寄存器,用于定义SCL高电平所保持的PCLK周期数。
符号描述复位值15:0SCLHSCL高电平周期选择计数0x0004
SCL占空比寄存器(I2SCLL): 16位寄存器,用于定义SCL低电平所保持的PCLK周期数。
符号描述复位值15:0SCLLSCL低电平周期选择计数0x0004
         在前面的I2C总线特征中我们提到过,I2C总线的速率通过可编程时钟来调整,即必须通过软件对I2SCLH 和I2SCLL寄存器进行设置来选择合适的数据频率和占空比。 频率由下面的公式得出(fPCLK是PCLK的频率)。

1.3.2 LPC2368中I2C总线操作

    在1.1.4中我们已经讲过了对I2C总线的操作,但那只是从协议和时序上的描述,那我们如何 从软件上去体现出来呢?接下来我们就讨论这个问题。
    对I2C总线上主从设备的读写可使用两种方法,一是使用轮询的方式,二是使用中断的方式。
轮询方式即是在一个循环中判断I2C状态寄存器当前的状态值来确定总线当前所处的状态,然后根据 这个状态来进行下一步的操作。 中断方式即是使能I2C中断,注册I2C中断服务程序,在服务程序中读取I2C状态寄存器的当前状态值, 再根据状态值来确定下一步的操作。
    不管使用哪种方法,看来I2C状态寄存器的值是至关重要的。这些状态值代表什么意思呢?
下面我们描述一些常用的状态值(详细的状态值含义请参考数据手册)。
0x08: 表明主设备向总线已发出了一个起始条件;
0x10: 表明主设备向总线已发出了一个重复的起始条件;
0x18: 表明主设备向总线已发送了一个从设备地址(写方向)并且接收到从设备的应答;
0x20: 表明主设备向总线已发送了一个从设备地址(写方向)并且接收到从设备的非应答;
0x28: 表明主设备向总线已发送了一个数据字节并且接收到从设备的应答;
0x30: 表明主设备向总线已发送了一个数据字节并且接收到从设备的非应答;
0x40: 表明主设备向总线已发送了一个从设备地址(读方向)并且接收到从设备的应答;
0x48: 表明主设备向总线已发送了一个从设备地址(读方向)并且接收到从设备的非应答;
0x50: 表明主设备从总线上已接收一个数据字节并且返回了应答;
0x58: 表明主设备从总线上已接收一个数据字节并且返回了非应答;

1.3.3 示例代码

一、 轮询方式读写总线:     对于代码中从设备内部寄存器的操作请参考该设备的数据手册。例如,要读取温度传感器的温度值 只需要调用:I2C0_ReadRegister(CHANNEL_TEMPERATURE, ADT75A_TEMP, &value), 如果读取成功,则value中的数据就是通过I2C总线读取温度传感器中的温度数据。 二、 中断方式读写总线:
    这里的从设备地址定义、I2C控制寄存器宏定义和I2C初始化与上面轮询中的类似,只是要在初始化
函数中加上中断申请的代码,中断服务程序名称为:I2C0_Exception。这里不再贴出以上代码了, 这里只贴出关键性的代码。

/*定义I2C状态标志*/ typedef enum { I2C_IDLE = 0, I2C_STARTED = 1, I2C_RESTARTED = 2, I2C_REPEATED_START = 3, I2C_DATA_ACK = 4, I2C_DATA_NACK = 5 } I2C_STATUS_FLAG; /*定义I2C数据传输缓冲区大小和传输超时大小*/ #define I2C_BUFSIZE 0x200 #define I2C_TIMEOUT 0x00FFFFFF /*定义I2C当前状态标志*/ volatile I2C_STATUS_FLAG I2C_Flag; /*I2C当前的模式,0为主发送器模式,1为主接收器模式*/ volatile uint32 I2CMasterMode = 0; /*分别定义I2C接收和发送缓冲区、要发送或要接收的字节数、实际发送或接收的字节数*/ volatile uint8 I2CReadBuf[I2C_BUFSIZE], I2CWriteBuf[I2C_BUFSIZE]; volatile uint32 I2CReadLength, I2CWriteLength; volatile uint32 I2C_RD_Index, I2C_WR_Index; /**************************************************************************** ** Function name: I2C0_Exception ** Descriptions : I2C0中断服务程序 ** Input : 无 ** Output : 无 ** Created Date : 2011-03-24 *****************************************************************************/ void I2C0_Exception(void) { volatile uint32 stat_value; stat_value = I20STAT; switch(stat_value) { case 0x08: /*发出了一个起始条件,接下来将发送从地址然后清零SI位和STA位*/ I2C_Flag = I2C_STARTED; I20DAT = I2CWriteBuf[I2C_WR_Index]; I2C_WR_Index++; I20CONCLR = I2C_STA | I2C_SI; break; case 0x10: /*一个重复的起始条件发送完成,接下来要将发送从地址然后清零SI位和STA位*/ I2C_Flag = I2C_RESTARTED; if(I2CMasterMode == 1) { /*注意I2CWriteBuf中的第0位是设备从地址和写方向位,因这里是读操作,故将第0位的方向位变为读*/ I20DAT = I2CWriteBuf[0] | 0x01; } I20CONCLR = I2C_STA | I2C_SI; break; case 0x18 /*(注:SLA+W表示从设备地址+写方向)*/ /*发送SLA+W后已接收到ACK,接下来开始发送数据字节到数据寄存器然后清零SI位*/ if(I2C_Flag == I2C_STARTED) { I2C_Flag = I2C_DATA_ACK; I20DAT = I2CWriteBuf[I2C_WR_Index]; I2C_WR_Index++; } I20CONCLR = I2C_SI; break; case 0x28: /*此状态表明已发送I2DAT中的字节且接收到ACK,接下来继续发送下一个字节*/ case 0x30: /*已发送I2DAT中的字节且接收到非ACK,接下来可能发出停止条件或重启起始条件*/ if(I2C_WR_Index != I2CWriteLength) { /*实际发送的字节数与要发送的不相等则继续发送,但可能是最后一次*/ I20DAT = I2CWriteBuf[I2C_WR_Index]; I2C_WR_Index++; if(I2C_WR_Index != I2CWriteLength) { I2C_Flag = I2C_DATA_ACK; } else { /*如果实际发送与要发送的相等了,表明主发送端数据发送完成*/ I2C_Flag = I2C_DATA_NACK; if(I2CReadLength != 0) { /*如果主发送端有等待接收的字节,则切换为主接收模式,重启起始条件*/ I2C_Flag = I2C_REPEATED_START; I20CONSET = I2C_STA | I2C_SI; } } } else { /*如果实际发送与要发送的相等了,表明主发送端数据发送完成*/ I2C_Flag = I2C_DATA_NACK; if(I2CReadLength != 0) { /*如果主发送端有等待接收的字节,则表明需切换为主接收模式,重启起始条件*/ I2C_Flag = I2C_REPEATED_START; I20CONSET = I2C_STA; } } I20CONCLR = I2C_SI; break; case 0x40: /*此状态表明已发送SLA+R后已接收到ACK*/ I20CONCLR = I2C_SI; break; case 0x50: /*此状态表明已接收数据字节后已接收到ACK*/ case 0x58: /*此状态表明已接收数据字节后已接收到非ACK*/ I2CReadBuf[I2C_RD_Index] = I20DAT; I2C_RD_Index++; if(I2C_RD_Index != I2CReadLength) { /*如果实际接收的字节与要接收的不相等,则继续接收*/ I2C_Flag = I2C_DATA_ACK; } else { /*否则接收完毕*/ I2C_RD_Index = 0; I2C_Flag = I2C_DATA_NACK; } I20CONCLR = I2C_AA | I2C_SI; break; case 0x20: /*此状态表明已发送SLA+W后已接收到非ACK*/ case 0x48: /*此状态表明已发送SLA+R后已接收到非ACK*/ I2C_Flag = I2C_DATA_NACK; I20CONCLR = I2C_SI; break; default: I20CONCLR = I2C_SI; break; } VICVectAddr = 0x00; } /**************************************************************************** ** Function name: I2C0_Start ** Descriptions : 设置I2C0总线传输起始条件 ** Input : 无 ** Output : 返回TRUE/FALSE, FALSE为设置超时 ** Created Date : 2011-03-24 *****************************************************************************/ BOOL I2C0_Start(void) { uint32 timeout = 0; BOOL retVal = FALSE; /*设置配置寄存器STA位开始条件*/ I20CONSET = I2C_STA | I2C_SI; I20CONCLR = I2C_SI; /*等待起始条件完成*/ while(1) { if(I2C_Flag == I2C_STARTED) { retVal = TRUE; break; } if(timeout >= I2C_TIMEOUT) { retVal = FALSE; break; } timeout++; } return retVal; } /**************************************************************************** ** Function name: I2C0_Stop ** Descriptions : 设置I2C0总线传输停止条件 ** Input : 无 ** Output : 返回TRUE ** Created Date : 2011-03-24 *****************************************************************************/ BOOL I2C0_Stop(void) { /*设置配置寄存器STO位停止条件和清除SI标志*/ I20CONSET = I2C_STO; I20CONCLR = I2C_SI; /*等待停止条件完成*/ while(I20CONSET & I2C_STO); return TRUE; } /**************************************************************************** ** Function name: I2C0_Engine ** Descriptions : 完成I2C0总线从开始到停止的传输,传输过程在中断服务程序中进行 ** Input : 无 ** Output : 返回TRUE/FALSE ** Created Date: 2011-03-24 *****************************************************************************/ BOOL I2C0_Engine(void) { I2C_Flag = I2C_IDLE; I2C_RD_Index = 0; I2C_WR_Index = 0; if(I2C0_Start() != TRUE) { I2C0_Stop(); return FALSE; } while(1) { if(I2C_Flag == I2C_DATA_NACK) { I2C0_Stop(); break; } } return TRUE; }
    从上面代码中看,如果要使用I2C总线启动一次数据传输只需要先初始化好发送或接收缓冲区,然后调用