I2C外设介绍

1.STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担

2.支持多主机模型(主机就是拥有主动控制总线的权利,从机只能在主机允许的情况下,才能控制总线,对于多主机模型又可分为固定多主机和可变多主机)

2.1固定多主机:就是总线上有两个或更多个固定的主机

2.2可变多主机:总线上没有固定的主机和从机,任何一个设备都可以在总线空闲时跳出来作为主机,指定其他任何一个设备进行通信

3.支持7位/10位地址模式(最常用的是七位地址)

4.支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)

5.支持DMA(数据转运)

6.兼容SMBus协议(系统管理总线)

7.STM32F103C8T6 硬件I2C资源:I2C1、I2C2

I2C框图

I2C基本结构

首先移位寄存器数据寄存器DR是通信的核心部分

发送的时候:高位先行(所以是向左移位)从高位到低位,依次放到SDA线上

接收的时候:数据通过GPIO口从右边依次移进来(移动8次,一个字节就接收完成了)

之后GPIO口(使用硬件I2C)

对应的GPIO口(I2C1 I2C2)

其中I2C1可以通过重映射到PB8、PB9两个引脚

都要配置成复用开漏输出的模式(Mode)

复用:就是GPIO的状态是由片上外设来控制的

然后是SCL这里

时钟控制器通过GPIO去控制时钟线(这里简化成一主多从的模型,所以时钟这里只画了输出的方向)

如果是多主机的模型,时钟线也会进行输入的

最后是开关控制(cmd

I2C_cmd 配置好了就使能外设,外设就能正常工作了

I2C操作流程

主机发送

产生起始条件:在控制寄存器 CR1 中,在 START 位写 1,就可以产生起始条件了

EV(Event 事件)x:来代替标志位的

1. 为什么要用 EV 来代替标志位?

因为有的状态会产生多个标志位,可以这么理解 EVx(1,2,3...等等)就是组合了多个标志位的大标志位,在库函数中也有对应的检查 EVx 事件是否发生的函数

2.EV5 事件:(意味着)SB=1 这一位置 1,表示起始条件已发送(这个状态寄存器不需要手动清零的)

3.EV6 事件:ADDR=1 这一位置 1,在主模式下表示地址发送结束

4.EV8_1 事件:TxE=1 移位寄存器空,数据寄存器空,一旦检测到 EV8_1 事件,这时需要我们写入数据寄存器 DR 进行数据发送

5.EV8 事件:TxE=1 移位寄存器非空,数据寄存器空,一旦检测到 EV8 事件,就可以写入下一个数据,最后当想要发送的数据写完后,这时就没有新的数据可以写入到数据寄存器了

6.EV8_2 事件:TxE=1 BTF=1 当检测到 EV8_2 时,就可以产生终止条件

总结:写入控制寄存器 CR 或数据寄存器 DR,控制时序单元的发生,比如:产生起始条件、发送一个字节数据等等,时序单元发生后检查相应的 EV 事件,来等待时序单元发送完成,依次按照这个流程,操作、等待、操作、等待...这样就能实现时序了

主机接收

产生起始条件:在控制寄存器 CR1 中,在 START 位写 1,就可以产生起始条件了

EV(Event 事件)x:来代替标志位的

1.EV5 事件:(意味着)SB=1 这一位置 1,表示起始条件已发送(这个状态寄存器不需要手动清零的)

2.EV6 事件:ADDR=1 这一位置 1,在主模式下表示地址发送结束

3.EV6_1 事件:数据 1 还正在移位,还没收到,所以这个事件没有标志位,当这个时序完成时硬件会根据我们的配置,把应答位发送出去

3.1.如何配置是否给应答?控制寄存器 CR 中,在 ACK(使能应答)位写 1,就在接收到一个字节后就返回一个应答,写 0,就是不给应答

4.EV7 事件:RxNE=1,表示数据寄存器非空,也就是收到一个字节的数据,但我们把数据读走后,这个事件就没有了,上面这边当 EV7 事件没有了,说明此时数据 1 被读走,当然数据 1 还没被读走时,数据 2 就可以直接移入移位寄存器了,之后,数据 2 移位完成,收到数据 2,产生 EV7 事件,读走数据 2,EV7 事件就没有了,按照这个流程,就可以一直接收数据了

5.EV7_1 事件:当我们不需要继续接收时,需要在最后一个时序单元发生时,把应答位 ACK 置 0,并且设置终止条件请求,也就是RxNE=1 ACK=0 STOP请求

总结:写入控制寄存器 CR 和读取数据寄存器 DR,产生时序单元,然后等待相应的事件,确保时序单元完成

硬件 I2C 读取 MPU6050

根据前面的介绍和框图和流程来实现读取 MPU6050

I2C 常用库函数

I2C_Init 初始化(以下是参数配置):

I2C_Mode 模式

I2C_DutyCycle 配置 SCL的时钟频率(数值越高,SCL的频率越高,最大值小于 400KHz)

I2C_DutyCycle 时钟占空比(只有在快速状态时才有用(100KHz 以上)在小于 100KHz 的标准速度下占空比是固定的 1:1)

I2C_Ack 应答位配置 (配置收到一个字节后是否给从机应答)

I2C_AcknowledgedAddress 作为从机响应几位的地址

I2C_OwnAddress1 作为从机指定自身地址,方便别的主机呼叫(暂时不需要做从机被使用的话,地址可以随便给,只要不和总线其他设备的地址重复就行)

I2C_Cmd 使能或失能

I2C_GenerateSTART 生成起始条件

I2C_GenerateSTOP 生成终止条件

I2C_AcknowledgeConfig 配置收到一个字节后是否给从机应答(初始化配置好后,想修改可以用这个函数修改)

I2C_SendData 发送数据

I2C_ReceiveData 接收数据

I2C_Send7bitAddress 发送七位地址

I2C_CheckEvent 基本状态监控 同时判断一个或多个标志位(来确定 EVx 这个状态是否发生)

I2C_GetFlagStatus 读取标志位

I2C_ClearFlag 清除标志位

I2C_GetITStatus 读取中断标志位

I2C_ClearITPendingBit 清除中断标志位

I2C 的初始化代码

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	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_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	I2C_InitStructure.I2C_ClockSpeed = 50000;
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;
	I2C_Init(I2C2,&I2C_InitStructure);
	
	I2C_Cmd(I2C2,ENABLE);

I2C 的写入(发送)

这些硬件 I2C 函数只管给寄存器的位置 1 或在 DR 写入数据,就结束,波形是否发送完毕,它是不管的,对于这种非阻塞式的程序,在函数结束后,要等待相应的标志位,来确保这个函数的操作执行到位

	I2C_GenerateSTART(I2C2,ENABLE);											//起始
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);		//监测EV5事件是否发生
	
	I2C_Send7bitAddress(I2C2,MPU6050__ADDRESS,I2C_Direction_Transmitter);		//从机地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//监测EV6(主机发送)事件是否发生
	
	I2C_SendData(I2C2,RegAddress);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);	//监测EV8事件是否发生
	
	I2C_SendData(I2C2,Data);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);	//监测EV8_2事件是否发生
	
	I2C_GenerateSTOP(I2C2,ENABLE);	

I2C 的读取

因为是指定地址读一个字节,所以在EV6事件之后,要把 ACK 置 0,同时把停止条件生成位 STOP 置 1

此时不是接收数据吗?数据都没收到,就要产生停止条件?

因为在接收最后一个字节之前(指定地址读一个字节的话),就要提前把 ACK 置 0,同时把停止条件生成位 STOP 置 1,时序不等人,如果没有提前的话会导致多一个字节出来

	uint8_t Data;	
	
	I2C_GenerateSTART(I2C2,ENABLE);											//起始
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);	//监测EV5事件是否发生
	
	I2C_Send7bitAddress(I2C2,MPU6050__ADDRESS,I2C_Direction_Transmitter);		//从机地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//监测EV6事件是否发生
	
	I2C_SendData(I2C2,RegAddress);
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);	//监测EV8事件是否发生
	
	I2C_GenerateSTART(I2C2,ENABLE);											//起始
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);	//监测EV5事件是否发生
	I2C_Send7bitAddress(I2C2,MPU6050__ADDRESS,I2C_Direction_Receiver);		//从机地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);	//监测EV6(主机接收)事件是否发生
	
	I2C_AcknowledgeConfig(I2C2,DISABLE);										//应答位置0
	I2C_GenerateSTOP(I2C2,ENABLE);												//停止

	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);	//监测EV7事件是否发生
	Data = I2C_ReceiveData(I2C2);
	
	I2C_AcknowledgeConfig(I2C2,ENABLE);										
	
	return Data;

简单的超时退出机制

因为用了许多的死循环,为了避免卡死的超时退出机制

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;	//跳出循环
		}
	}
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐