4.8 I2C总线
I2C总线(Inter-Integrated Circuit Bus)是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。某些书籍或者文档中也写作IIC,读作“I方C”。
I2C是嵌入式中最常见的,也是最重要的总线通信协议之一。很多传感器、外围芯片都使用I2C协议。它具有如下特点:
(1)硬件线路简单:I2C总线只需要一根数据线和一根时钟线两根线,
(2)灵活:数据传输和地址设定由软件设定,非常灵活。总线上的器件增加和删除不影响其他器件正常工作。
(3)可以连接设备数量多:连接到相同总线上的IC数量只受总线最大电容的限制。
I2C总线是一个主从结构的总线,所有的数据传输都必须由主机发起,通常单片机做主机,其他连接在I2C总线的设备称之为从机或者器件。
I2C还有一个重要的概念:器件地址。连接在I2C总线上的设备,除了主机之外,每个器件都有自己的地址。主机想要和某个器件通信时,先往I2C总线发送器件地址。
I2C器件地址一般为8位,最后一位是读写标志位。0表示主机要读取器件的数据;1表示主机要往器件写数据。
I2C总线只需要两根线,分别是时钟线(SCL)、数据线(SDA)。其中时钟线提供时间周期,时间周期越短则数据传输速率越快。数据线用来传输起始位、应答位,数据等。时序图如4.37所示。
图4.37 I2C时序图
数据格式主要有起始位、停止位、数据位、应答位(ACK)、NACK。
1. 起始位
当主机想要启动I2C数据传输时,需要要先往I2C总线发送起始位。起始位的条件是SCL线为高电平时,SDA线从高电平向低电平切换。
2. 停止位
当主机想要终止I2C数据传输时,需要往I2C总线发送停止位,释放I2C总线的占用。停止位的条件是SCL线为高电平时,SDA线从低电平向高电平切换。
3. 数据位
SDA数据线上的每个字节必须是8位,每次传输的字节数量没有限制。每个字节后必须跟一个响应位(ACK)。首先传输的数据是最高位(MSB),SDA上的数据必须在SCL高电平周期时保持稳定,数据的高低电平翻转变化发生在SCL低电平时期。
4. 应答位
每个字节传输必须带响应位,相关的响应时钟也由主机产生,在响应的时钟脉冲期间(第9个时钟周期),发送端释放SDA线,接收端把SDA拉低。
5. NACK位
以下情况会导致出现NACK位:
(1)接收机没有发送机响应的地址,接收端没有任何ACK发送给发射机
(2)由于接收机正在忙碌处理实时程序导致接无法接收或者发送
(3)传输过程中,接收机识别不了发送机的数据或命令
(4)接收机无法接收
(5)主机接收完成读取数据后,要发送NACK结束告知从机
I2C属于比较简单的总线,完全可以根据I2C的时序,使用I/O模拟I2C。本文将使用STM32的GPIO口实现模拟I2C的功能,帮助读者理解I2C的时序控制。
打开Chapter4\05_I2C_24c02\mdk\IIC24c02.uvproj工程文件,接着打开24c02.c文件,模拟IIC的代码都在这个文件中。
1. I2C初始化
I2C的初始化部分代码主要是对STM32的GPIO进行初始化。GPIOB_9作为数据引脚(SDA),GPIOB_8作为时钟引脚(SCL),代码如下:
//Chapter4\05_I2C_24c02\USER\24C02\24c02.c???? 5行
//I2C初始化
void IIC_Init(void)
{????????????????????
? GPIO_InitTypeDef? GPIO_InitStructure;
? RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);? //打开GPIOB时钟
? //GPIOB8,B9
? GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
? GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;????????? //输出模式
? GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;???????? //开漏输出
? GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;???? //100MHz
? GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;?????????? //上拉
? GPIO_Init(GPIOB, &GPIO_InitStructure);???????????????? //初始化
????
? IIC_Stop();?? //先给停止信号,复位I2C总线上所有的设备
}
?
2. 起始信号
当SCL为高电平时,SDA出现一个下降沿表示I2C总线启动信号,代码如下:
//Chapter4\05_I2C_24c02\USER\24C02\24c02.c???? 23行
//I2C启动信号
void IIC_Start(void)
{
???? //先把SDA输出引脚置高
???? IIC_SDAOUT=1;???
???? //SCL引脚置高? ?? ??
???? IIC_SCL=1;
???? //等待4us
???? delay_us(4);
???? //SDA引脚拉低
???? IIC_SDAOUT=0;
???? //等待4us
???? delay_us(4);
???? //SCL引脚拉低
???? IIC_SCL=0;???? //准备发送数据或者接收数据
}
?
IIC_SCL指I2C的SCL引脚,IIC_SDAOUT 指I2C的SDA引脚输出,在24c02.h文件中分别被定义成PBout(8)和PBout(9),代码如下:
//Chapter4\05_I2C_24c02\USER\24C02\24c02.h???? 8行
#define IIC_SCL????? PBout(8)? //SCL
#define IIC_SDAOUT?? PBout(9) // SDA??
?
IIC_SDAOUT=1表示SDA引脚输出高电平。这里是GPIO输出高低电平的另外一种写法,等价于之前的GPIO_WriteBit(GPIOB, GPIO_Pin_9, Bit_SET)。
IIC_Start函数使用SDA、SCL引脚,通过输出高低电平和延时的操作,模拟了I2C启动信号。其时序如图4.38所示。
图4.38 I2C启动信号时序
3. 停止信号
当SCL高电平时,SDA出现一个上升沿表示I2C总线停止信号,代码如下:
//Chapter4\05_I2C_24c02\USER\24C02\24c02.c???? 34行
void IIC_Stop(void)
{
???? //SDA先低电平,这样才能出现上升沿
???? IIC_SDAOUT=0;
???? delay_us(4);
???? //SCL高电平
???? IIC_SCL=1;
? ?? delay_us(4);
???? //SDA由低电平变高电平,此时出现一个上升沿。????????????????????????????
???? IIC_SDAOUT=1;???????????????????????????? ?? ?????
}
?
4.应答信号
I2C总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,在响应的时钟脉冲期间(第9个时钟周期),由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK)。主机等待从机应答信号的相关代码如下:
//Chapter4\05_I2C_24c02\USER\24C02\24c02.c???? 50行
//返回值:1表示NACK, 0表示 ACK
u8 MCU_Wait_Ack(void)
{
???? u8 ack;
?
???? IIC_SDAOUT=1;
???? delay_us(1);???? ??
???? IIC_SCL=1;
???? delay_us(1);
???? //读取SDA总线电平
???? if (IIC_SDAIN)??????????
???? {
???????????? ack = 1;???????? //高电平则表示NACK应答
???? }
???? else
???? {
???????????? ack = 0;???????? //低电平则表示NACK应答
???? }
???? IIC_SCL=0;
???? delay_us(1);
???? return ack;?
}
?
5.数据位发送
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应。在SCL呈现高电平期间,SDA上的电平必须保持稳定,低电平为数据0,高电平为数据1。只有在SCL为低电平期间,才允许SDA上的电平改变状态。逻辑0的电平为低电压,而逻辑1则为高电平。时序如图4.39所示。
图4.39 数据位发送时序
6.发送一个字节
I2C写一个字节相当于往I2C总线发送了8个数据位,根据图4.39数据位发送时序,我们可以用I/O模拟,代码如下:
//Chapter4\05_I2C_24c02\USER\24C02\24c02.c? 113行
//参数:Senddata 要发送的数据
void IIC_write_OneByte(u8 Senddata)
{???????????????????????
???? u8 t;??
????????????
???? IIC_SCL=0;???
???? for(t=0;t<8;t++)
???? {
???????????? //先发送高位
???????????? IIC_SDAOUT=(Senddata&0x80)>>7;
???????????? //左移1位
???????????? Senddata=(Senddata<<1); ? ??
???????????? delay_us(2);??
???????????? IIC_SCL=1;
???????????? delay_us(2);
???????????? IIC_SCL=0;??????
???????????? delay_us(2);
???? }???????
}
?
其中比较关键的代码是Senddata的移位操作。
根据 & 和 >> 的特性,(Senddata&0x80)>>7相当于保留Senddata的最高位,其它位清零,同时再把最高位右移到最低位。相当于把Senddata最高位的数值赋给IIC_SDAOUT,从而实现SDA引脚根据Senddata的最高位输出响应的高低电平。
之后Senddata=(Senddata<<1),把Senddata的第2高位通过左移1位的方式,使Senddata的第2高位变成最高位。
再通过for循环,重复这两步操作,把Senddata的每一位都发送出去。为了方便直观理解,我们假设Senddata等于170,十六进制为:0xAA,二进制为:10101010。整个for循环的移位操作可以用图4.40直观的表示出来。
图4.40 移位操作流程图
?
7.读一个字节
读时序和发送时序相同,不同的是发送时需要在SCL低电平的时候更改SDA数据位,而读时需要在SCL高电平的时候读取SDA数据位。同时,每读取一位数据,都需要左移1位,保证高位在前。读完数据后需要发送ACK或者NACK应答信号。代入如下:
//Chapter4\05_I2C_24c02\USER\24C02\24c02.c? 137行
u8 IIC_Read_OneByte(u8 ack)
{
?? u8 i,receivedata=0;
??? for(i=0;i<8;i++ )
?? {
??????? IIC_SCL=0;
??????? delay_us(2);
????? IIC_SCL=1;
??????? receivedata<<=1;
??????? if(IIC_SDAIN)
????? {
???????????? receivedata++;??
????? }
????? delay_us(1);
??? }?????????????????????????????????????????
??? if (!ack)
??????? MCU_NOAck();
??? else
??????? MCU_Send_Ack();
??? return receivedata;
}
?
I2C是嵌入式中最常见的总线通信协议,读者需要熟练掌握,了解IIC的时序,并能使用I/O模拟I2C操作。