最近项目使用到温湿度模块AHT20,其为IIC协议,看了大佬们的文章后自己参照着写了一下代码,本文章主要是为了加深自己对IIC的理解所需写,可能存在很多不足,还望见谅。
进入正题,这里先对IIC进行简单的介绍,本介绍来自互联网。IIC也称I2C,是一个多主从的串行总线,由飞利浦公司发明的通讯总线,属于半双工同步传输类总线,仅由两条线就能完成多机通讯,一条SCL时钟线,另外一条双向数据线SDA,IIC总线要求每个设备SCL/SDA线都是漏极开路模式,因此必须带上拉电阻才能正常工作。I2C协议占用引脚少,硬件实现简单,可扩展性强,I2C数据传输速率有标准模式(100kbps)、快速模式(400kbps)和高速模式(3.4Mbps)。
在多个设备中采用 IIC进行通讯时,主控制器按地址选择分配主从通讯,所以每个连接总线的从机都有一个特殊的地址,从机地址必须与 IIC装置的固有地址一致,并在制造时由厂商自行分配,用户可以按照说明书进行查询,注意总线必须有外接上拉电阻,以确保空闲状态的稳定而且 IIC总线时序是影响通讯工作能否顺利进行的重要因素,下图是IIC通信的总时序图如下图所示。
从上图可以看到,IIC时序图主要包括几个方面,起始信号,停止信号以及应答/非应答信号,下面将一一对其讲解。
本次示例代码为STM32F103系列。
为了方便后续开发,决定写一个IIC驱动程序
首先初始化IIC的IO口,为方便后续修改,这里选择宏定义方式。
#define SDA GPIO_Pin_5
#define SCL GPIO_Pin_3
#define IIC_Port GPIOB
#define SDA_Hight GPIO_SetBits(IIC_Port,SDA)//置高
#define SDA_Low GPIO_ResetBits(IIC_Port,SDA)
#define SCL_Hight GPIO_SetBits(IIC_Port,SCL)
#define SCL_Low GPIO_ResetBits(IIC_Port,SCL)
#define OUT 1
#define INPUT 0
由于IIC通信时,IO口应当由输出或者输入模式,来发送和接受信息,所以,还需要写一个配置输入输出函数来进行切换,代码如下所示。
void I2C_SDA_Mode(u8 addr)//切换管脚模式
{
GPIO_InitTypeDef GPIO_Initstructrue;
if(addr )//输出模式(写)
{
GPIO_Initstructrue.GPIO_Pin=SDA ;
GPIO_Initstructrue.GPIO_Speed =GPIO_Speed_50MHz ;
GPIO_Initstructrue.GPIO_Mode=GPIO_Mode_Out_PP ;
GPIO_Init (IIC_Port,&GPIO_Initstructrue);
}
else //输入模式(读)
{
GPIO_Initstructrue.GPIO_Pin=SDA ;
GPIO_Initstructrue.GPIO_Mode=GPIO_Mode_IPU;
GPIO_Init (IIC_Port,&GPIO_Initstructrue);
}
接下来便是对IIC时序进行代码模拟编写及理解。以下为IIC协议的几种重要信号,分别是起始信号,终止信号,应答及非应答信号。
从图中可以清楚的看到,主机发送IIC启始信号经历以下过程,SCL保持高电平,SDA由高电平变为低电平后,延时(>4.7us),SCL变为低电平,因此,我们可以写,先配置模式为输出模式,将SCL线拉高,延迟5us后拉低SDA线,延迟5us后拉低SCL,至此,发送了IIC的开始信号,相应代码如下所示。
void I2C_start(void)
{
I2C_SDA_Mode (OUT );//配置为输出模式
SCL_Hight ;//拉高SCL
delay_us(5);//延时5MS
SDA_Low ;//拉低SDA
delay_us(5);//延时
SCL_Low ;//拉低SCL
}
同样的,主机发送结束信号的过程为,SCL保持高电平。SDA由低电平变为高电平。因此将其配为输出模式,将SDA拉低,延迟5us后再将SDA拉高,延迟5us后拉低SCL,完成结束信号,代码如下所示。
void I2C_Stop(void)
{
I2C_SDA_Mode (OUT );//配置为输出模式
SDA_Low ;//拉低SDA
delay_us(5);//延时5us
SCL_Hight ;//拉高SCL线
delay_us(5);//延时5us
SDA_Hight ;//拉高SDA线
}
什么是应答信号呢,这个过程就好比如你与朋友间的交流一样,你向朋友传达信息,例如,小明问小红,你今天吃饭没有?小红不管回答吃了与没吃,都对你发送了一个应答信号,表明她已经接收到你的消息了,否则她可能没有听到你的问题。无论何时,主机将一个字节发送到从机,总要等待从机发出回应信号,以确定从机是否已成功地收到了信息,这是应答信号。
应答信号:主机SCL拉高,读取从机SDA的电平,为低电平表示产生应答应答信号为低电平,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功, 如图下图所示。
因此,配置为输入模式,将主机SCL拉高,等待应答,若有应答则返回0,无应答返回1,相应代码如下。
u8 I2C_Write_Ack(void)
{
I2C_SDA_Mode (INPUT );//配置为输入模式,接受从机的应答信号
u8 tempack;
SCL_Hight ;
delay_us(4);
while(GPIO_ReadInputDataBit (IIC_Port ,SDA ))//读取SDA管脚状态,若有应答,其IO口状态应为0
{
if(++tempack >250)//若超时,则退出
{
I2C_Stop ();//发送结束信号
return 1;//无应答返回一
}
}
SCL_Low ;//否则拉低SCL
delay_us(4);//延时4us
return 0;//有应答返回0
}
由于交流是互相的,有时候主机也要发送应答或是非应答信号,
主机产生应答信号:应答:1.先拉低SCL,再拉低SDA,2.拉高SCL 3.拉低SCL 4发送停止信号
非应答:1先拉低SCL,再拉高SDA , 3.拉高SCL ,4发送停止信号
因此,程序这样写,通过传入参数来选择发送应答或非应答, 代码如下所示。
void I2C_send_Ack(char flag)//传入参数代表应答或是非应答
{
I2C_SDA_Mode (OUT );//配置为输出模式
SCL_Low;//拉低SCL
delay_us(4);
if(flag==0)//产生非应答信号
{
SDA_Low;
delay_us(4);
SCL_Hight;
delay_us(4);
SCL_Low;
delay_us(4);
I2C_SDA_Mode (INPUT );
}
else if(flag==1)产生应答信号
{
SDA_Hight;
delay_us(4);
SCL_Hight;
delay_us(4);
SCL_Low;
delay_us(4);
SDA_Low;
delay_us(4);
}
}
SDA线上的数据在SCL时钟“高”期间必须是稳定的,只有当SCL线上的时钟信号为低时,数据线上的“高”或“低”状态才可以改变。输出到SDA线上的每个字节必须是8位,数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位),当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位ACK, 此时才认为一个字节真正的被传输完成 ,如果一段时间内没有收到从机的应答信号,则自动认为从机已正确接收到数据[16]。因此,所写函数应传入一个数据形参,通过与运算,用传入的数&上0X80,来判断数据线拉高拉低,先判断高位,例如传入1000 0000 时,左移0位&上0X80,则为1,SDA拉高,下一次循环时,左移1位,补0在结尾,则&上0X80为0,则SDA拉低,以此类推,具体代码下所示。
void I2C_write_Byte(u8 Date)//1101 1010 写入从机
{
SCL_Low ;
delay_us(4);
for(u8 i=0;i<8;i++)
{
I2C_SDA_Mode (OUT);//必须放进内部
if((Date <
设置为输入模式,读取8次SDA电平信号,使用或运算,从高位写到低位,若检测到SDA为高,则写1,否则写0,每次循环后,都左移一位,循环8次后结束,代码如下所示。
u8 I2C_Read_Date(void )//从从机里面读
{
u8 Date;
for(u8 i=0;i<8;i++)
{
I2C_SDA_Mode (INPUT );//必须放进内部
SCL_Hight ;
Date<<=1;//0000 0001 1000 0000
if(GPIO_ReadInputDataBit (IIC_Port ,SDA )==1)
{
Date|=0X01;
}
SCL_Low ;
delay_us(4);
}
return Date;
}
至此,IIC驱动程序已完成,后续我将演示使用它来驱动温湿度传感器AHT20。