IIC概要
IIC是一种通信协议,其工作方式为半双工,即不能同时进行发送和接收数据。该协议可以是多主机模式,但多主机是分时复用的关系,有两条线,分别为时钟线(SCL)和数据线(SDA),这两根线控制着数据的传输。地线对传输数据没啥影响。
IIC通信协议
通信步骤
1、主机向从机发送起始信号。
SCL和SDA在空闲时,一般都处于高电平信号,所以将要开始通信时,主机在SCL为高电平时,将SDA的信号拉低,表示一个起始信号,准备开始通信,此时从机不必应答。
2、主机向从机发送从机的地址信息(7为)和读写使能信号(1位)。
由于在IIC总线上很可能挂载的不止一个器件,所以利用地址信息可以对比出主机将要发送给哪个器件(从机)。当读写使能信号为0时,表示主机向从机发送数据(主机写出),读写使能信号为1时,表示传输数据的方向为从机到主机(主机读入)。
3、从机给主机发送应答信号。
IIC在每次发送数据完成之后,都需要从机产生一个应答信号之后才会开始下一个字节数据的传输。
4、开始数据的传输.
发送方往接收方传输数据时,SCL信号在低电平时,数据可以发生改变,即SCL=0时,数据可以被发送。当SCL=1时,接收方读取SDA线上的数据,SDA上的数据不能改变。特别注意的是,IIC协议一次传输的是8位数据,高位在前,低位在后。
5、从机产生应答信号。
6、重复步骤4和5直到数据传输完成。
7、当数据传输完成之后,主机产生停止信号。
在SCL=1时,SDA拉高,表示通信结束,即产生一个停止信号。特别注意的是主机给从机发送数据时,主机可以在从机发送应答信号之后主动发送停止信号,也可以在从机不产生应答信号之后被迫产生应答信号,即只要从机不产生应答信号,主机便会产生停止信号。从机给主机发送数据时,主机在停止之前不用产生应答信号,就可以产生停止信号。
IIC帧格式
主机向从机传送数据
从机向主机发送数据
IIC时序图
起始信号:SCL=1,SDA=0(拉低)。
传输:写入:SCL=0。读取:SCL=1。
停止信号:SCL=1,SDA=1(拉高)。
STM32之IIC的应用
硬件IIC
这里以型号为LM75a的温度传感器为例,在实际的开发中,在发送器件地址之后,无论是读取还是写入,都需要再发送一个子地址,这个子地址对应着器件里面的一个具有特殊含义的地址。
首先需要初始化IIC的结构体。
void I2C_Configuration(void){ //I2C初始化
I2C_InitTypeDef I2C_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); //启动I2C功能
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6| GPIO_Pin_7; //选择端口号
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //选择IO接口工作方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置IO接口速度(2/10/50MHz)
GPIO_Init(GPIOB, &GPIO_InitStructure);
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//设置为I2C模式
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//low/high的值
I2C_InitStructure.I2C_OwnAddress1 = 0xc0; //主机地址(从机不得用此地址),这里应该是随意设置的
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//允许应答
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //7位地址模式
I2C_InitStructure.I2C_ClockSpeed = 200000; //总线速度设置,不得超过400k
I2C_Init(I2C1,&I2C_InitStructure);
I2C_Cmd(I2C1,ENABLE);//开启I2C
}
这里的GPIO模式应该选择复用模式。
上面就完成了对IIC端口的初始化,下面编写IIC的时序功能。
单个字节的写入
void I2C_SAND_BYTE(u8 SlaveAddr,u8 writeAddr,u8 pBuffer){ //I2C发送一个字节(从地址,内部地址,内容)
I2C_GenerateSTART(I2C1,ENABLE); //发送开始信号
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); //等待完成
I2C_Send7bitAddress(I2C1,SlaveAddr, I2C_Direction_Transmitter); //发送从器件地址及状态(写入)
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //等待完成
I2C_SendData(I2C1,writeAddr); //发送从器件内部寄存器地址
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //等待完成
I2C_SendData(I2C1,pBuffer); //发送要写入的内容
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //等待完成
I2C_GenerateSTOP(I2C1,ENABLE); //发送结束信号
}
可以看到,在写入器件地址之后,还需要发送一个子地址,这个子地址和器件的主控芯片的命令指令相关,可以通过查询手册得到,比如芯片手册可以规定,在发送器件地址,表示已经相互配对,再发送一个子地址用于告诉器件我需要往配对的器件的哪个寄存器发送数据,完成这两部之后就可以传输真正的数据了。
发送一组数据
void I2C_SAND_BUFFER(u8 SlaveAddr,u8 WriteAddr,u8* pBuffer,u16 NumByteToWrite){ //I2C发送数据串(器件地址,寄存器内部地址,发送内容,数量)
I2C_GenerateSTART(I2C1,ENABLE);//产生起始位
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); //清除EV5
I2C_Send7bitAddress(I2C1,SlaveAddr,I2C_Direction_Transmitter);//发送器件地址
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));//清除EV6
I2C_SendData(I2C1,WriteAddr); //内部功能地址
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED));//移位寄存器非空,数据寄存器已空,产生EV8,发送数据到DR既清除该事件
while(NumByteToWrite--){ //循环发送数据
I2C_SendData(I2C1,*pBuffer); //发送数据
pBuffer++; //数据指针移位
while (!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED));//清除EV8
}
I2C_GenerateSTOP(I2C1,ENABLE);//产生停止信号
}
读取一个字节的数据
u8 I2C_READ_BYTE(u8 SlaveAddr,u8 readAddr){ //I2C读取一个字节
u8 a;
//等待IIC总线空闲
while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY));
//产生起始信号
I2C_GenerateSTART(I2C1,ENABLE);
//等待稳定
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT));
//发送器件地址
I2C_Send7bitAddress(I2C1,SlaveAddr, I2C_Direction_Transmitter);
//等待稳定
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1,readAddr);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTART(I2C1,ENABLE);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1,SlaveAddr, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
I2C_AcknowledgeConfig(I2C1,DISABLE); //最后有一个数据时关闭应答位
I2C_GenerateSTOP(I2C1,ENABLE); //最后一个数据时使能停止位
a = I2C_ReceiveData(I2C1);
return a;
}
在读取时,同样得在发送器件完成之后发送读取的具体地址,额外注意的是,只要产生这个时序之后,单片机的相应寄存器就已经读到这个值了,所以I2C_ReceiveData(I2C1)可以在最后面,但我觉得还是放在应答之前比较好理解。
读取一组数据
void I2C_READ_BUFFER(u8 SlaveAddr,u8 readAddr,u8* pBuffer,u16 NumByteToRead){ //I2C读取数据串(器件地址,寄存器,内部地址,数量)
while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1,ENABLE);//开启信号
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); //清除 EV5
I2C_Send7bitAddress(I2C1,SlaveAddr, I2C_Direction_Transmitter); //写入器件地址
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));//清除 EV6
// I2C_Cmd(I2C1,ENABLE);
I2C_SendData(I2C1,readAddr); //发送读的地址
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //清除 EV8
I2C_GenerateSTART(I2C1,ENABLE); //开启信号
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); //清除 EV5
I2C_Send7bitAddress(I2C1,SlaveAddr,I2C_Direction_Receiver); //将器件地址传出,主机为读
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); //清除EV6
while(NumByteToRead){
if(NumByteToRead == 1){ //只剩下最后一个数据时进入 if 语句
I2C_AcknowledgeConfig(I2C1,DISABLE); //最后有一个数据时关闭应答位
I2C_GenerateSTOP(I2C1,ENABLE); //最后一个数据时使能停止位
}
if(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)){ //读取数据
*pBuffer = I2C_ReceiveData(I2C1);//调用库函数将数据取出到 pBuffer
pBuffer++; //指针移位
NumByteToRead--; //字节数减 1
}
}
I2C_AcknowledgeConfig(I2C1,ENABLE);
}