2013年(45)
分类: LINUX
2013-03-16 00:16:12
原文地址:嵌入式Linux之我行--内核I2C子系统详解 作者:hbhuanggang
嵌入式Linux之我行,主要讲述和总结了本人在学习嵌入式linux中的每个步骤。一为总结经验,二希望能给想入门嵌入式Linux的朋友提供方便。如有错误之处,谢请指正。
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 | 0 |
7:3 | Status | 这些位给出I2C接口的实时状态,不同的值代表不同的状态,状态码请参考数据手册 | 0x1F |
I2C数据寄存器(I2DAT): 8位寄存器,在SI置位期间,I2DAT中的数据保持稳定。
位 | 符号 | 描述 | 复位值 |
7:0 | Data | 该寄存器保留已经接收到或者准备要发送的数据值 | 0 |
I2C从地址寄存器(I2ADR): 8位寄存器,I2C总线为从模式时才使用。主模式中该寄存器无效。
位 | 符号 | 描述 | 复位值 |
0 | GC | 通用调用使能位 | 0 |
7:1 | Address | 从模式的I2C器件地址 | 0x00 |
SCH占空比寄存器(I2SCLH): 16位寄存器,用于定义SCL高电平所保持的PCLK周期数。
位 | 符号 | 描述 | 复位值 |
15:0 | SCLH | SCL高电平周期选择计数 | 0x0004 |
SCL占空比寄存器(I2SCLL): 16位寄存器,用于定义SCL低电平所保持的PCLK周期数。
位 | 符号 | 描述 | 复位值 |
15:0 | SCLL | SCL低电平周期选择计数 | 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 示例代码
一、 轮询方式读写总线:
/*I2C从设备地址*/
#define SC16IS740_ADDR 0x92 /*I2C转UART设备*/
#define ADT75A_ADDR 0x9E /*温度传感器设备*/
#define ADT75A_TEMP 0x00 /*温度传感器内部寄存器*/
/*从设备选择标识*/
#define CHANNEL_GPRS 0
#define CHANNEL_TEMPERATURE 1
/*定义I2C控制寄存器各位操作宏*/
#define BIT(x) (1 << x)
#define I2C_EN BIT(6)
#define I2C_STA BIT(5)
#define I2C_STO BIT(4)
#define I2C_SI BIT(3)
#define I2C_AA BIT(2)
/*用作超时计数*/
#define SAFETY_COUNTER_LIMIT 3000
/******************************************************************
** Function name: I2C0_Init
** Descriptions : I2C0初始化
** Input : 无
** Output : 无
** Created Date : 2011-03-24
*******************************************************************/
void I2C0_Init(void)
{
/*设置P0.0,P0.1为I2C0接口的SDA和SCL功能*/
PINSEL0 |= (0x03 << 0) | (0x03 << 2);
/*设置I2C0接口功率/时钟控制位*/
PCONP |= (0x01 << 7 );
/*清空I2C0配置寄存器的各位*/
I20CONCLR = (0x01 << 2) | (0x01 << 3) | (0x01 << 5) | (0x01 << 6);
/*使能I2C0为主发送器模式*/
I20CONSET = (0x01 << 6);
/*设置I2C0总线速率为100 KHz */
I20SCLH = 0x5A;
I20SCLL = 0x5A;
}
/****************************************************************************
** Function name: I2C0_ReadRegister
** Descriptions : 从I2C0总线上读从设备的数据
** Input : 从设备选择标识、从设备内部寄存器地址、读出的字节数据
** Output : 读取是否成功
** Created Date : 2011-03-28
*****************************************************************************/
BOOL I2C0_ReadRegister(uint32 channel, uint8 registerAddress, uint8 *pData)
{
/*用作延时等待计数*/
uint32 loopSafetyCounter = 0;
uint32 addressSendSafetyCounter = 0;
/*使用循环判断I2C状态寄存器I20STAT 的值*/
do
{
/*向总线发送I2C起始条件*/
I20CONSET = I2C_STA | I2C_SI;
I20CONCLR = I2C_SI;
/*等待起始条件发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
/*发送从设备地址*/
if(channel == CHANNEL_GPRS)
I20DAT = SC16IS740_ADDR;
else if(channel == CHANNEL_TEMPERATURE)
I20DAT = ADT75A_ADDR;
I20CONCLR = I2C_STA | I2C_SI;
/*等待从设备地址发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
addressSendSafetyCounter ++;
if (addressSendSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
} while (I20STAT != 0x18); /*在前面已经描述了0x18的含义*/
/*发送从设备内部寄存器地址,根据数据手册描述该内部地址要左移3位*/
I20DAT = registerAddress << 3;
I20CONCLR = I2C_SI;
/*等待从设备内部寄存器地址发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
/*重启I2C起始条件进行总线读*/
I20CONSET = I2C_STA | I2C_SI;
I20CONCLR = I2C_SI;
/*等待重启条件发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
/*发送从设备地址(方向位为读,注意与上0x01将地址最低位变为1即为读方向)*/
if(channel == CHANNEL_GPRS)
I20DAT = SC16IS740_ADDR | 0x01;
else if(channel == CHANNEL_TEMPERATURE)
I20DAT = ADT75A_ADDR | 0x01;
I20CONCLR = I2C_STA | I2C_SI;
/*等待从设备地址发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
/*开始准备读取数据*/
I20CONCLR = I2C_SI | I2C_AA;
/*等待数据接收*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
/*数据接收*/
*pData = I20DAT;
/*发送I2C停止条件*/
I20CONSET = I2C_STO;
I20CONCLR = I2C_SI;
/*等待停止条件发送完成*/
loopSafetyCounter = 0;
while (I20CONSET & I2C_STO)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
return TRUE;
}
/****************************************************************************
** Function name: I2C0_WriteRegister
** Descriptions : 从I2C0总线上写从设备的数据
** Input : 从设备选择标识、从设备内部寄存器地址、要写入的数据字节
** Output : 写入是否成功
** Created Date : 2011-03-28
*****************************************************************************/
BOOL I2C0_WriteRegister(uint32 channel, uint8 registerAddress, uint8 data)
{
uint32 loopSafetyCounter = 0;
uint32 addressSendSafetyCounter = 0;
/*使用循环判断I2C状态寄存器I20STAT 的值*/
do
{
/*向总线发送I2C起始条件*/
I20CONSET = I2C_STA | I2C_SI;
I20CONCLR = I2C_SI;
/*等待起始条件发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
/*发送从设备地址*/
if(channel == CHANNEL_GPRS)
I20DAT = SC16IS740_ADDR;
else if(channel == CHANNEL_TEMPERATURE)
I20DAT = ADT75A_ADDR;
I20CONCLR = I2C_STA | I2C_SI;
/*等待从设备地址发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
addressSendSafetyCounter ++;
if (addressSendSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
} while (I20STAT != 0x18);
/*发送从设备内部寄存器地址*/
I20DAT = registerAddress << 3;
I20CONCLR = I2C_SI;
/*等待寄存器地址发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
/*开始发送数据*/
I20DAT = data;
I20CONCLR = I2C_SI;
/*等待数据发送完成*/
loopSafetyCounter = 0;
while (~I20CONSET & I2C_SI)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
/*发送I2C停止条件*/
I20CONSET = I2C_STO;
I20CONCLR = I2C_SI;
/*等待停止条件发送完成*/
loopSafetyCounter = 0;
while (I20CONSET & I2C_STO)
{
loopSafetyCounter ++;
if (loopSafetyCounter > SAFETY_COUNTER_LIMIT)
{
return FALSE; /*超时退出*/
}
}
return TRUE;
}
对于代码中从设备内部寄存器的操作请参考该设备的数据手册。例如,要读取温度传感器的温度值只需要调用:I2C0_ReadRegister(CHANNEL_TEMPERATURE, ADT75A_TEMP, &value),如果读取成功,则value中的数据就是通过I2C总线读取温度传感器中的温度数据。
二、 中断方式读写总线:
这里的从设备地址定义、I2C控制寄存器宏定义和I2C初始化与上面轮询中的类似,只是要在初始化函数中加上中断申请的代码,中断服务程序名称为:I2C0_Exception。这里不再贴出以上代码了,这里只贴出关键性的代码。
|
|
1.4 Linux下I2C子系统框架
在Linux下要使用I2C总线并没有像无系统中的那样简单,为了体现Linux中的模块架构,Linux把I2C总线的使用进行了结构化。这种结构分三部分组成,他们分别是:I2C核心部分、I2C总线驱动部分和I2C设备驱动。结构图如下:
由此看来,在Linux下驱动I2C总线不像单片机中那样简单的操作几个寄存器了,而是把I2C总线结构化、抽象化了,符合通用性和Linux设备模型。
1.4.1 I2C核心
1.4.2 I2C总线驱动
1.4.3 I2C设备驱动
1.5 Linux下I2C总线与设备驱动示例(以电路示例二为例)