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结构体
typedef struct {
uint32_t I2C_ClockSpeed; /*!< 设置SCL时钟频率,此值要低于400000*/
uint16_t I2C_Mode; /*!< 指定工作模式,可选I2C模式及SMBUS模式 */
uint16_t I2C_DutyCycle; /*指定时钟占空比,可选low/high = 2:1及16:9模式*/
uint16_t I2C_OwnAddress1; /*!< 指定自身的I2C设备地址 */
uint16_t I2C_Ack; /*!< 使能或关闭响应(一般都要使能) */
uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为7位及10位 */
} I2C_InitTypeDef;
1.I2C_ClockSpeed
本成员设置的是I2C的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到I2C的时钟控制寄存器CCR。 而我们写入的这个参数值不得高于400KHz。实际上由于CCR寄存器不能写入小数类型的时钟因子,影响到SCL的实际频率可能会低于本成员设置的参数值, 这时除了通讯稍慢一点以外,不会对I2C的标准通讯造成其它影响。
2.I2C_Mode
本成员是选择I2C的使用方式,有I2C模式(I2C_Mode_I2C)和SMBus主、 从模式(I2C_Mode_SMBusHost、 I2C_Mode_SMBusDevice ) 。 I2C不需要在此处区分主从模式,直接设置I2C_Mode_I2C即可。
3.I2C_DutyCycle
本成员设置的是I2C的SCL线时钟的占空比。该配置有两个选择, 分别为低电平时间比高电平时间为2:1 ( I2C_DutyCycle_2)和16:9 (I2C_DutyCycle_16_9)。 其实这两个模式的比例差别并不大,一般要求都不会如此严格,这里随便选就可以。
4.I2C_OwnAddress1
本成员配置的是STM32的I2C设备自己的地址,每个连接到I2C总线上的设备都要有一个自己的地址,作为主机也不例外。 地址可设置为7位或10位(受下面I2C_AcknowledgeAddress成员决定),只要该地址是I2C总线上唯一的即可。
STM32的I2C外设可同时使用两个地址,即同时对两个地址作出响应,这个结构成员I2C_OwnAddress1配置的是默认的、OAR1寄存器存储的地址, 若需要设置第二个地址寄存器OAR2,可使用I2C_OwnAddress2Config函数来配置,OAR2不支持10位地址,只有7位。
5.I2C_Ack_Enable
本成员是关于I2C应答设置,设置为使能则可以发送响应信号。本实验配置为允许应答(I2C_Ack_Enable), 这是绝大多数遵循I2C标准的设备的通讯要求,改为禁止应答(I2C_Ack_Disable)往往会导致通讯错误。
6.I2C_AcknowledgeAddress
本成员选择I2C的寻址模式是7位还是10位地址。这需要根据实际连接到I2C总线上设备的地址进行选择,这个成员的配置也影响到I2C_OwnAddress1成员, 只有这里设置成10位模式时,I2C_OwnAddress1才支持10位地址。
配置完这些结构体成员值,调用库函数I2C_Init即可把结构体的配置写入到寄存器中。
10.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;
}
11.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姿态传感器的通信代码。