【STM32】I²CC通信外设&硬件I²CC读写MPU6050(学习笔记)

发布于:2025-03-21 ⋅ 阅读:(37) ⋅ 点赞:(0)

 相关文章笔记:

【STM32】I2C通信协议&MPU6050芯片-学习笔记-CSDN博客

【江协科技STM32】软件I2C协议层读写MPU6050驱动层-CSDN博客

I²C通信外设

I²C外设简介

  • STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
  • 支持多主机模型
  • 支持7位/10位地址模式
  • 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
  • 支持DMA,如果想要连续写或者读多个字节,那就可以用DMA帮我们转运一下数据,过程效率就会大大提升
  • 兼容SMBus协议(系统管理总线)
  • STM32F103C8T6 硬件I2C资源:I2C1、I2C2

I²C功能框图 

 硬件I²C固定引脚:

 I²C基本结构

 I²C复用引脚输入输出:

为什么要用复用开漏模式?

①开漏是I2C协议设计要求

②复用就是GPIO的控制权要交给硬件外设,硬件I2C控制引脚的任务肯定得交给外设来做。 

主机发送  

需要对应着图片下方解释和数据手册看 

 

实在看不懂听多两遍:主机发送讲解 

主机接收 

需要对应着图片下方解释和数据手册看  

软件/硬件波形对比

 硬件I²C读写MPU6050

接线图:

配置I²C外设

1、 配置I²C外设,对I²C2外设进行初始化

①开启I2C外设对应的GPIO口时钟

②把I2C外设对应的GPIO口初始化为复用开漏输出模式

③使用结构体对I2C进行配置

④使能I2C

void MPU6050_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);		//开启I2C2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	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);					//将PB10和PB11引脚初始化为复用开漏输出
	
	/*I2C初始化*/
	I2C_InitTypeDef I2C_InitStructure;						//定义结构体变量
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;				//模式,选择为I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;				//时钟速度,选择为50KHz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;		//时钟占空比,选择Tlow/Thigh = 2
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;				//应答,选择使能
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	//应答地址,选择7位,从机模式下才有效
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;				//自身地址,从机模式下才有效
	I2C_Init(I2C2, &I2C_InitStructure);						//将结构体变量交给I2C_Init,配置I2C2
	
	/*I2C使能*/
	I2C_Cmd(I2C2, ENABLE);									//使能I2C2,开始运行
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);				//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);				//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);				//采样率分频寄存器,配置采样率
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);					//配置寄存器,配置DLPF
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);			//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);			//加速度计配置寄存器,选择满量程为±16g
}

SCL得时钟频率和占空比得研究:不同频率的占空比

对I2C_CheckEvent函数 进行封装

/**
  * 函    数:MPU6050等待事件
  * 参    数:同I2C_CheckEvent
  * 返 回 值:无
  */
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)								//自减到0后,等待超时
		{
			/*超时的错误处理代码,可以添加到此处*/
			break;										//跳出等待,不等了
		}
	}
}

2、 控制外设电路,实现指定地址写时序

注意看数据手册写写代码 ,库函数配置和时序图对应

​ 

时序图看7位主发送 

​ 

总结:当我们发送多个字节的数据流时,中间的字节,写入DR后,需要等待EV8事件,也就是 I2C_EVENT_MASTER_BYTE_TRANSMITTING。最后一个字节,写入DR后,需要等待的是EV8_2事件,也就是 I2C_EVENT_MASTER_BYTE_TRANSMITTED

/**
  * 函    数:MPU6050写寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 参    数:Data 要写入寄存器的数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8
	
	I2C_SendData(I2C2, Data);												//硬件I2C发送数据
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	I2C_GenerateSTOP(I2C2, ENABLE);											//硬件I2C生成终止条件
}

为什么等待标志位? 

之前软件I2C的函数,有delay函数操作,是一种阻塞式的流程;函数运行完成,对应的波形也肯定发送完。但是库函数的这些函数都不是阻塞式的函数,这些硬件I2C函数只管寄存器的位置置1,或者只在DR写入数据,就结束,然后退出函数,至于波形是否发送完毕,它就不管的。所以对于这种非阻塞式的函数,在函数结束之后,我们都要等待相应的标志位,来确保函数的操作执行到位了。如何操作?那就要用到状态监控函数了。

ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)//检查最后一个I2Cx事件是否等于传递的事件

参数选择: 

这里注意一下有两个EV6事件,一个是发送一个是接收,不要选错了。 

​ 

返回值:

​ 

3、 控制外设电路,实现指定地址读时序 

这里代码写的是指定地址读时序,手册没有给出指定地址读的时序,只能参考指定地址写和当前地址读时序图,两者结合就是指定地址读(符合格式)。

时序图看7位主发送  

为什么要提前置应答位0并置停止位STOP呢?看不懂再看看视频:为什么这样设置

因为在发送或者接收一个字节的同时会顺带发一个应答位默认应答,如果我们只需要接收一个字节,那就必须要提前置非应答位并置停止位,如果不这样子做,那看时序图,应答位会随着接收到的数据过来,应答位已经收到了,如果这时再说给非应答,已经晚了,数据已经发送出去了,应答位已经收到了,只能在下一个数据之后给非应答。时序不等人,所以要提前给非应答。这个STOP停止条件也不会截断当前字节,他会等到当前字节接收完再产生终止条件波形。

 总结:如果想要读取多个字节,那就直接等待EV7事件,读取DR,就能收到数据,这样依次接收,在接收最后一个字节之前,也就是EV_7事件,需要提前置应答位0并置停止位STOP,如果只需要读取一个字节,那就需要在EV6事件之后就立刻置 Ack0和 STOP停止位

 最后应答位为什么置回1?解释

开始ACK默认是应答的 中间接收一个字节 把ACK置0 防止多接收字节,接收完一个字节后 再给ACK恢复为原来的1 

/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成重复起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);		//硬件I2C发送从机地址,方向为接收
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);		//等待EV6
	
	I2C_AcknowledgeConfig(I2C2, DISABLE);									//在接收最后一个字节之前提前将应答失能
	I2C_GenerateSTOP(I2C2, ENABLE);											//在接收最后一个字节之前提前申请停止条件
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);				//等待EV7
	Data = I2C_ReceiveData(I2C2);											//接收数据寄存器
	
	I2C_AcknowledgeConfig(I2C2, ENABLE);									//将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
	
	return Data;
}

重复起始条件后有个小细节: 听一下

函数解释:

void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct)//I2C初始化

参数 说明
I2Cx 其中x可以是1或2来选择I2C外设
I2C_InitStruct 指向I2C_InitTypeDef结构体的指针,其中*为指定I2C外设的配置信息

I2C初始化结构定义 

参数 说明
I2C_ClockSpeed 时钟频率 ,该参数必须设置为低于400kHz的值
I2C_Mode I2C模式
I2C_DutyCycle I2C快速模式占空比
I2C_OwnAddress1 指定第一个设备自己的地址
I2C_Ack 启用或禁用应答确认
I2C_AcknowledgedAddress 指定确认7位或10位地址

void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState)//生成I2Cx通信START条件

void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState)//产生I2Cx通信停止条件 

 参数一样:

参数 说明
I2Cx 其中x可以是1或2来选择I2C外设
NewState ENABLE or DISABLE

 void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState)//启用或禁用指定的I2C确认应答特性

参数 说明
I2Cx 其中x可以是1或2来选择I2C外设
NewState ENABLE(给应答) or DISABLE(不给应答)

 void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data)//通过I2Cx外设发送一个数据字节

参数 说明
I2Cx 其中x可以是1或2来选择I2C外设
Data 要传输的字节

uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx)//返回I2Cx外设最近接收到的数据

参数 说明
I2Cx 其中x可以是1或2来选择I2C外设

 返回值:接收到的数据值

void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction)//发送地址字节以选择从设备

参数 说明
I2Cx 其中x可以是1或2来选择I2C外设
Address 指定要传输的从地址
I2C_Direction

I2C方向是否为一个发送器或接收器;

该参数可以是以下值之:I2C_Direction_Transmitter:发送模式
 I2C_Direction_Receiver:接收模式

void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState)//启用或禁用指定的I2C外设 

参数 说明
I2Cx 其中x可以是1或2来选择I2C外设
NewState ENABLE or DISABLE

写代码过程中要查数据手册对于的寄存器来写。