简介
IIC(Inter-Integrated Circuit)是 IIC Bus 简称,中文叫集成电路总线。它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。 IIC使用两根信号线进行通信:一根时钟线SCL,一根数据线SDA。IIC将SCL处于高时SDA拉低的动作作为开始信号,SCL处于高时SDA拉高的动作作为结束信号;传输数据时,SDA在SCL低电平时改变数据,在SCL高电平时保持数据,每个SCL脉冲的高电平传递1位数据。
主要特征
- IIC有两根数据线:串行数据线SDA,串行时钟线SCL。
- IIC采用开漏输出结构,需通过外接上拉电阻实现信号线的逻辑控制。
- IIC 支持 7 位和 10 位地址模式。7 位地址模式下,首字节包含 7 位从机地址和 1 位读写位(0 表示写,1 表示读)。10 位地址模式需要两个字节传输地址信息,首字节以特殊保留地址开头,确保与 7 位设备兼容。
- 任何时候只存在简单的主从关系,主机可以是发送器也可以是接收器。
- 支持多主机。在总线上存在多个主机时,通过冲突检测和仲裁机制防止多个主机同时发起数据传输时存在的冲突。
- IIC 总线的设备数量受总线电容限制,通常不超过 400pF。
硬件电路
IIC协议通过两根线,要求两根线都使用开漏输出接上拉电阻。其连接方式如下图所示:
由上图可以看出主机从机连接在相同的SCL和SDA总线上,在SCL和SDA总线上分别连接一个上拉电阻,SCL和SDA都要设置位开漏输出模式。
工作时序
了解一个协议除了了解该协议的特征以及硬件电路连接之外,最重要的就是了解该协议的工作时序,了解该协议是如何控制电路来产生起始结束以及传送数据的条件的,接下来我们通过图片来了解I2C协议的开始和结束的时序。
开始信号::SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。我们把开始信号分为三部分来理解。
- 状态一:SDA=1,SCL=1。
- 状态二:SDA=0,SCL=1。
- 状态三:SDA=0,SCL=0。
根据上图我我们可以看出,当SCL为高电平的时候,SDA由高电平向低电平跳变表示开始条件。
结束信号:SCL是高电平的时候,SDA由低电平变为高电平,数据传输结束。同理,我们把它分为三部分来理解。
- 状态一:SDA=0,SCL=1。
- 状态二:SDA=1,SCL=1。
- 状态三:SDA=1,SCL=1。 根据上面的信息,我们可以得到当SCL是高电平时,SDA由低电平变为高电平表示信号结束。
读取数据:数据的发送我们遵循SCL为低电平的时候发送数据,SCL为高电平的时候读取数据。我们根据一个图片来对这句话进行理解。
我们根据图片可以看到SCL = 1,SDA = 1的时候设备处于空闲状态,当SCL由高电平变为低电平的时候,会通知SDA准备传输数据,过一段时间以后SDA会根据要传输的数据来改变自己的电平状态,高电平传输1,低电平传输0.当SDA准备好以后,SCL由低电平变为高电平,产生上升沿,告诉设备准备读取数据。如果传输8位数据的话,重复上述过程即可。
数据应答:在上面的过程中我们已经了解了I2C是如何产生开始和结束信号以及传输数据的过程中信号是如何变化的,但是当设备接收到信号后,设备如何确定已经接收到数据呢?因此就会有应答机制,那么我们一起来了解一下应答信号是怎么样的吧! 当接收到数据后我们会回复一个应答信号,此时SCL=1,SDA=1。 当没有接收到数据我们会回复一个非应答信号,此时SCL=1,SDA=0。
至此我们已经对I2C时序已经有了一个基础的了解,那么我们一起来用GPIO口来模拟一下I2C的时序吧!
程序编写
首先我们先看一下我的定义:
#define SCL_Port GPIOB
#define SCL_Pin GPIO_Pin_6
#define SDA_Port GPIOB
#define SDA_Pin GPIO_Pin_7
#define SCL_L (GPIO_WriteBit(SCL_Port,SCL_Pin,0));
#define SCL_H (GPIO_WriteBit(SCL_Port,SCL_Pin,1));
#define SDA_L (GPIO_WriteBit(SDA_Port,SDA_Pin,0));
#define SDA_H (GPIO_WriteBit(SDA_Port,SDA_Pin,1));
#define SDA_RD (GPIO_ReadInputDataBit(SDA_Port,SDA_Pin))
在上面我们定义了GPIO口以及SCL和SDA的高低电平,为后续写代码提供了便利。
开始信号的代码:
void IIC_Start(void)
{
SDA_H;
SCL_H;//起始确保其都是在高电平
Delay_usnop(10);
SDA_L;
Delay_usnop(10);
SCL_L;
Delay_usnop(10);
}
根据原理图,我们一开始先将SDA和SCL拉高,确保能够产生低电平,然后在SCL为高电平的时候,拉低SDA,最后拉低SCL,完美对应我们前文说的三个步骤。
结束信号的代码:
void IIC_Stop(void)
{
SCL_H;
SDA_L;
Delay_usnop(10);
SDA_H;
Delay_usnop(10);//结束信号
}
在开始时我们先将SCL与SDA的电平信号一个拉高,一个拉低,确保SDA一会可以产生上升沿。然后拉高SDA,此时在SCL为高电平的时候,SDA从低电平变为高电平完美符合上面我们结束信号的条件。
发送数据的代码:
void IIC_SendData(uint8_t data)
{
//因为在起始的时候已经把SCL电平拉低了
for(int i = 0; i < 8; i++)
{
if(data & (1<<(7-i)))
{
SDA_H;
}else{
SDA_L;
}
SCL_H;
Delay_usnop(10);//读取数据
SCL_L;
}
}
在产生开始信号的时候我们已经将SCL的信号拉低了,因此我们在此时不用再拉低SCL的信号通知设备准备数据。因此我们只用后续准备数据即可,在次我们词用for循环来进行数据移位,因为I2C传输数据的时候是从高位传输的,因此我们从高位进行判断人如果为1,SDA=1,如果为0,SDA=0。在SDA信号线改变后拉高SCL,让其读数据,最后在拉低SCL让其准备数据。
读取数据的代码:
uint8_t IIC_ReadData(void)
{
uint8_t Data;
SDA_H;//释放总线
for(int i = 0;i < 8;i++)
{
SCL_H;
Delay_usnop(10);//产生高电平
if(SDA_RD)
{
Data |= (1<<(7-i));
}else{
Data &= (1<<(7-i));
}
SCL_L;
Delay_usnop(10);
}
return Data;
}
在读取数据的时候我们只根据SCL电平的高低来读取数据,在发送数据的最后SCL为低电平,在读取数据前将SCL拉高产生上升沿,然后判断SDA_RD读取电平的状态来读取数据,最后再拉低,再让其准备数据。因此达到读取数据的目的。
发送ACK的代码:
void IIC_SendACK(uint8_t ACK)
{
if(ACK == 0)
{
SDA_L;
}else{
SDA_H;
}
SCL_H;
Delay_usnop(10);
SCL_L;
Delay_usnop(10);
}
在发送应答的时候,我们是根据我们要发送的应答信号来改变SDA的状态,以此让设备读取SDA的状态确定应答信号的。因此我们应该根据我们要发送的ACK来确定SDA的状态,当ACK=0,SDA=0,在这里我们产生应答信号。相反SDA=1,SDA=1,我们产生非应答信号。
接收ACK的代码
uint8_t IIC_ReceiveACK(void)
{
uint8_t ACK;
SDA_H;//释放总线
SCL_H;
Delay_usnop(10);
ACK = SDA_RD;
SCL_L;
Delay_usnop(10);
return ACK;
}
在此我们软件模拟I2C协议的代码就结束了,如果那里有不对的地方希望大家批评指正!