1.什么是IIC?
我们可以想象一下,两个芯片该怎么通信呢?这就要用的通信协议(如IIC,USART,SPI等)了,通信协议就是规范数据交换的规则。 I2C(Inter-Integrated Circuit)就是是一种串行通信总线协议,其目的是用于连接集成电路(IC)之间的通信(交换数据)。I2C由飞利浦公司(现在的NXP半导体)于上世纪80年代开发,是一种双向通信协议,需要两根线路:时钟线(SCL)和数据线(SDA)。其中:SCL是由主设备提供时钟信号来同步数据传输(SCL的作用)。SDA:用来传输数据的(双向传输)。
I2C协议的设计使得多个设备可以通过同一条总线进行通信,其中每个设备都有一个唯一的地址。这使得微控制器可以轻松地与多个设备进行通信,并能确定与哪个设备进行通信(也称寻址)。
在 I2C总线通信中,有主机(Master)和从机(Slave)两种角色。I2C总线中的主机负责控制整个通信过程,发起数据传输请求和驱动时钟信号,而从机则被动地接受并响应主机的请求,进行数据的接收和发送。
下面是拥有IIC外设的芯片,其通信特点是同步,半双工,带数据应答,支持总线挂载多设备(一主多从、多主多从)
通信协议通常包括硬件规定和软件规定两个方面:
2.IIC硬件规定
硬件规定包括:硬件如何连接、端口的输入输出模式等。
主机对SCL线:任何时候都完全控制;从机任何时候都不能口直SCL线。
主机对SDA线:在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据与从机应答的时候,主机才会转交SDA的控制权给从机。
IIC原理图
所有I2C设备的SCL连在一起,SDA连在一起
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
设备的SCL和SDA均要配置成开漏输出模式(强下拉),在通过上拉电阻实现弱上拉。
开漏加若上拉模式,同时兼具了输入与输出的功能,如果想输入数据,直接观察电平变化。
在开漏模式下,输出高电平就相当于断开引脚。这么做避免了输入输出的来回切换。
3.IIC软件规定
软件规定包括:时序定义,字节传输、高位or低位先行、一个完整的时序有哪些部分构成等。
IIC时序的基本单元
起始条件
SCL高电平期间,SDA从高电平切换到低电平,即SCL不动,把SDA拽下来,产生一个下降沿,当从机捕获到SCL高电平,SDA下降沿时,就会进行自身复位,等待主句召唤。
终止条件
SCL高电平期间,SDA从低电平切换到高电平
一个完整的数据帧总是以起始条件开始,终止条件结束。
起始与终止都是由主机产生,所以在空闲状态时,从机必须双手放开。
发送一个字节
发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。总结:低电平切换(放)数据,高电平读取(读)数据。
接收一个字节
接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后主机释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。
发送应答
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据。
规则:数据0表示应答,数据1表示非应答。
发送应答是为了告诉从机:主机是否还要数据(命令从机发不发数据)。
目的:通知从机是否继续发送数据。
接收应答
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。
规则:数据0表示应答,数据1表示非应答
从机告诉主机接收到了主机传递的数据了,主机刚发送一个字节,就立刻说有没有人收到数据(SDA高),如果有接收数据就会回应(SDA拉低)。目的:判断从机是否接收到了主机发送的数据。
4.IIC时序
通过拼接基本的时序单元,可以组成读写时序,进而实现芯片之间的通信。
IIC指定地址写时序
对于指定设备地址(Slave Address),在指定寄存器地址(Reg Address)下,写入指定数据(Data)。该图意思为:在0xD0地址的设备中,在0x19寄存器下写入0xAA数据。其中第一个字节(7位地址位+1位读写位,读:1,写:0)
在从机中,所有的寄存器被分配到一个线性区域中,且有个单独的指针变量,指示着其中的一个寄存器。上电默认指向0地址,并且每写入一个字节后与读出一个字节后,这个指针就会自动自增一次。移动到下一个位置。
注:从机设备可以自己定义第二个字节和后续字节的用途。如用途:寄存器地址;指令控制字;存储器地址等
主机首先发送一个字节(地址),所有从机都会收到这一个字节,和自己的地址进行比较,如果不是自己的地址,则认为主机没有叫它,之后的时序就不用管了。
不同型号的芯片的地址都还是不同的,相同型号的芯片地址都是一样的,如果有相同的芯片挂载到同一条总线呢?
地址中的可变部分,一般器件的最后几位可以在电路中改变的,通过接对应的电路,可以有效的改变地址。
顺序:开始条件→7位从机地址+1为写(0)位→接收从机的应答位(Receive ACK,RA)→发送寄存器地址→从机应答→发送一个字节的数据→从机应答→结束条件。
IIC指定地址读时序
指定地址读后,会接收一个从机应答,之后,总线控制权交给从机。
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)。该图意思为:在0xD0地址的设备中,读取0x19寄存器中的数据(0xAA)。
顺序:开始条件→7位从机地址+1位写(0)位→从机应答(Receive ACK,RA)→发送寄存器地址→从机应答位→重新开始→7位地址+读(1)位→从机应答(0)→读取一个字节→主机发送非应答(1)→结束。
注:指定地址读写只能跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件,因为IC协议规定了起始之后必须是地址。
5.指定地址写多个字节
顺序:开始条件→7位从机地址+1为写(0)位→接收从机的应答位(Receive ACK,RA)→发送寄存器地址→从机应答→发送一个字节的数据→从机应答→发送字节的数据→从机应答→结束条件。可以多次重复绿色的步骤。
只需要将发送字节的数据→从机应答重复即可。
6.指定地址读多个字节
顺序:开始条件→7位从机地址+1为写(0)位→接收从机的应答位(Receive ACK,RA)→发送寄存器地址→从机应答→重新开始→7位地址+读(1)位→发送应答→读取一个字节→主机发送应答→读取一个字节→主机发送非应答→结束。可以多次重复绿色的步骤。
补充:寄存器也是存储器,普通的存储器只能写与读,没有实际的意义。寄存器的每一位数据都对应了硬件的电路状态,寄存器与外界的硬件电路是可以互动的。
7.IIC外设
数据收发的核心部分是数据寄存器与数据移位寄存器。
发送数据(橙色线):
当我们发送数据时,可以把一个字节的数据写到数据寄存器DR中,当数据移位寄存器没有数据移位时,这个数据寄存器的值会进一步转到移位寄存器里,在移位的过程中,我们就可以直接把下一个数据放到数据寄存器中里等着,一旦前一个数据移位完成,下一个数据就可以无缝衔接继续发送,当数据由数据寄存器转移到数据移位寄存器时,置状态寄存器TXE位为1,表示发送寄存器为空。--发送流程。
接收数据(绿色线):
输入的数据一位一位的从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就会整体从移位寄存器转到数据寄存器,同时置标志位RXNE为1,表示接收寄存器非空,这时就可以把数据从数据寄存器读出来。在IIC中数据收发是用一组数据寄存器与数据移位寄存器。通过控制寄存器的对应位控制什么时候收什么时候发。
注:在stm32的IIC中,读与写不是同一个寄存器。
I2C外设,利用硬件电路实现I2C的通信。一般外设寄存器有:控制寄存器CR、数据寄存器DR,状态寄存器SR:
CR:控制寄存器:具有控制功能,比如开关灯等(就像,踩油门、打方向盘,控制汽车)
DR:数据寄存器:存储数据,比如汽车运行时产出的速度、温度等
SR:状态寄存器:描述当前状态,比如汽车是否启动等(通过观看仪表盘,了解汽车运行状态)
8.IIC外设简约框图
9.IIC代码(软件模拟IIC)
#include "stm32f10x.h" // Device header #include "Delay.h" //参数给1或0,就可以释放或拉低SCL了。 void MyI2C_W_SCL(uint8_t BitValue) { GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue); Delay_us(10); } //写数据 void MyI2C_W_SDA(uint8_t BitValue) { GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue); Delay_us(10); } //读与写不是一个寄存器 //读数据 uint8_t MyI2C_R_SDA(void) { uint8_t BitValue; BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11); Delay_us(10); return BitValue; } //IIC的SDA与SCL引脚初始化 void MyI2C_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); //将两根线置为高电平,初始化为开漏输出且释放总线 GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11); } //开始 void MyI2C_Start(void) { MyI2C_W_SDA(1); MyI2C_W_SCL(1); MyI2C_W_SDA(0); MyI2C_W_SCL(0); } //结束 void MyI2C_Stop(void) { MyI2C_W_SDA(0); MyI2C_W_SCL(1); MyI2C_W_SDA(1); } //发送单元 void MyI2C_SendByte(uint8_t Byte) { uint8_t i; for(i = 0; i < 8; i++) { MyI2C_W_SDA(Byte & (0x80 >> i)); MyI2C_W_SCL(1); MyI2C_W_SCL(0); } } //接收单元 uint8_t MyI2C_ReceiveByte(void) { uint8_t i,Byte = 0x00; //MyI2C_W_SCL(1); MyI2C_W_SDA(1); for (i = 0; i< 8;i++) { MyI2C_W_SCL(1); if (MyI2C_R_SDA() == 1) { Byte |= (0x80>>i); } MyI2C_W_SCL(0); } return Byte; } //发送应答 void MyI2C_SendAck(uint8_t AckBit) { MyI2C_W_SDA(AckBit); MyI2C_W_SCL(1); MyI2C_W_SCL(0); } //接收应答 uint8_t MyI2C_ReceiveAck(void) { uint8_t AckBit; //主机释放SDA MyI2C_W_SDA(1); MyI2C_W_SCL(1); AckBit = MyI2C_R_SDA(); MyI2C_W_SCL(0); return AckBit; }
10.IIC代码(硬件外设IIC)
#include "stm32f10x.h" // Device header #include "MPU6050_Reg.h" #define MPU6050_ADDRESS 0XD0 //带有超时退出机制的WaitEvent函数 void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT) { uint32_t Timeout; Timeout = 10000; while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) { Timeout --; if (Timeout == 0) { break; } } } //主机给从机写数据(单片机给MPU6050写数据) void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data) { I2C_GenerateSTART(I2C2,ENABLE); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter); //在库函数中,发送数据自带接收应答,接收数据自带发送应答。 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); I2C_SendData(I2C2,RegAddress); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING); I2C_SendData(I2C2,Data); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); I2C_GenerateSTOP(I2C2,ENABLE); } //主机读从机的数据(单片机读MPU6050的数据) uint8_t MPU6050_ReadReg(uint8_t RegAddress) { uint8_t Data; I2C_GenerateSTART(I2C2,ENABLE); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter); //在库函数中,发送数据自带接收应答,接收数据自带发送应答。 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); I2C_SendData(I2C2,RegAddress); //问题:如果使用硬件操作I2C,在重复开始条件时,EVENT_MASTER_BYTE_TRANSMITTING这个状态正在发送数据(数据并没有发生完成),但是实验结果显示数据也能正常的接收,这是为什么? MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); I2C_GenerateSTART(I2C2,ENABLE); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); I2C_AcknowledgeConfig(I2C2,DISABLE); I2C_GenerateSTOP(I2C2,ENABLE); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED); Data = I2C_ReceiveData(I2C2); I2C_AcknowledgeConfig(I2C2,ENABLE); return Data; } //获取数据寄存器的函数 void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ, int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ) { uint8_t DataH,DataL; DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); *AccX = (DataH << 8) | DataL; DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); *AccY = (DataH << 8) | DataL; DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); *AccZ = (DataH << 8) | DataL; DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); *GyroX = (DataH << 8) | DataL; DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); *GyroY = (DataH << 8) | DataL; DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); *GyroZ = (DataH << 8) | DataL; } //初始化MPU6050 void MPU6050_Init(void) { //第一步:开启时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //第二步:配置pin引脚为复用开漏输出模式 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); //第三步:初始化I2C外设 I2C_InitTypeDef I2C_InitStructure; //接收一个字节后是否给从机应答 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //stm32从机可以响应几位的地址(stm32作为从机才会用到) I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 10000; //占空比:这里指的是SCL低电平与高电平的比,因为SCL是弱上拉,所以在快速状态下,SDA波形翻转需要一些时间(低电平需要更多的时间) I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //自身地址1(stm32作为从机才会用到),指定stm32的自身地址,方便别的主机呼叫。 I2C_InitStructure.I2C_OwnAddress1 = 0x00; I2C_Init(I2C2,&I2C_InitStructure); //第三步:I2C使能 I2C_Cmd(I2C2,ENABLE); MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); MPU6050_WriteReg(MPU6050_CONFIG, 0x06); MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); } //获取MPU6050的ID uint8_t MPU6050_GetID(void) { return MPU6050_ReadReg(MPU6050_WHO_AM_I); }
以上是利用IIC实现stm32与MPU6050姿态传感器的通信代码。