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姿态传感器的通信代码。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐