STM32标准库I2C协议与MPU6050

目录

一、I2C协议

1.I2C通信

2.硬件电路

3.I2C时序单元

(1)起始和终止

(2)发送字节

(3)接收字节

(4)发送应答和接收应答

4.I2C时序

(1)指定地址写

(2)当前地址读

(3)指定地址读

二、MPU6050介绍

1.简介

2.参数

3.硬件电路

4.框图

三、软件I2C读写MPU6050

四、硬件I2C读写MPU6050

1.I2C外设简介

2.I2C框图

3.I2C基本结构

4.主机发送

5.主机接收

6.软件/硬件波形对比

 7.硬件I2C读写MPU6050


一、I2C协议

1.I2C通信

2.硬件电路

3.I2C时序单元

(1)起始和终止

(2)发送字节

(3)接收字节

(4)发送应答和接收应答

4.I2C时序

(1)指定地址写

  • 在这里上面的线是SCL,下面的线是SDA空闲状态都是高电平,然后主机需要给从机写入数据的时候,首先SCL高电平期间,拉低SDA产生起始条件,在起始条件之后,紧跟着的时序,必须是发送一个字节的时序,字节的内容必须是从机地址+读写位,正好从机地址是7位,读写位是1位,加起来是一个字节8位,发送从机地址,就是确定通信的对象,发送读写位,就是确认我接下来是要写入还是要读出,具体发送的时候呢,在这里低电平期间SDA变换数据,高电平期间从机读取SDA,这里我用绿色的线来标明了从机读到的数据,比如这样的波形,那从机收到的第一位就是高电平1,然后SCL低电平期间主机继续变换数据,因为第二位还是1,所以这里SDA电平并没有变换,然后SCL高电平,从机读到第二位是1,之后继续低电平变换数据,高电平读取数据,第三位就是0,这样持续8次就发送了一个字节数据,其中这个数据的定义:高7位表示从机地址,比如这个波形下,主机寻找的统计地址就是1101000,这个就是MPU6050的地址,然后最低位表示读写位,0表示之后的时序主机要进行写入操作,1表示之后的时序主机要进行读出操作,这里是0,说明之后我们要进行写入操作,那目前主机是发生了一个字节,字节内容转化为16进制,高位先行就是0xD0 ,然后根据协议规定,紧跟着的单元就得是接收从机的应答位(Receive Ack(RA)),在这个时刻主机要释放SDA,释放SDA之后引脚电平回弹到高电平,.但是根据协议规定,从机要在这个位拉低SDA,所以单看从机的波形,该应答的时候从机立刻拽住SDA,然后应答结束之后,从机再放开SDA,那现在综合两者的波形,结合线与的特性,在主机释放SDA之后,由于SDA也被从机拽住了,所以主机松手后,SDA并没有回弹高电平。
  • 应答结束后,我们要继续发送一个字节,同样的时序再来一遍,第二个字节就可以送到指定设备的内部来,从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址,或者是指令控制字等,比如MPU16050定义的第二个字节就是寄存器地址,比如AD转换器,第二个字节可能就是指令控制字,比如存储器,第二个字节可能就是存储器地址,那图示这里主机发送这样一个波形,我们一一判定,数据为00011001,即主机向从机发送了0x19 这个数据,第一部分解读的前七位出来的是MPU6050,在MPU6050 里,就表示我要操作你0x19地址下的寄存器了,接着同样是从机应答,主机释放SDA,从机拽住SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答,然后继续同样的流程。
  • 再来一遍,主机再发送一个字节,这个字节就是主机想要写入到0x19地址下寄存器的内容了,比如这里发送了0xAA的波形,就表示我在0x19地址下写入0xAA,最后是接收应答位,如果主机不需要继续传输了,就可以产生停止条件,在停止条件之前先拉低SDA,为后续SDA的上升沿做准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间SDA的上升沿,这样一个完整的数据帧就拼接完成了,那套用上面这句话呢,这个数据帧的目的,就是对于指定从机地址为1001000的设备,在其内部0x19地址下的寄存器中,写入0xAA这个数据。

(2)当前地址读

如果主机想要读取从机的数据,就可以执行这个时序,那最开始还是SCL高电平期间,拉低SDA产生起始条件,起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,比如图示的波形,表示本次寻址的目标是1101000的设备,同时最后一位读写标志为1,表示主机接下来想要读取数据,紧跟着发送一个字节之后接收一下从机应答位,从机应答为0代表从机收到了第一个字节,在从机应答之后,从这里开始数据的传输方向就要反过来了,因为刚才主机发出了读的命令,所以这之后主机就不能继续发送了,要把SDA的控制权交给从机,主机调用接收一个字节的时序进行接收操作,然后在这一块从机就得到了主机的允许,可以在SCL低电平之间写入SDA,然后主机在SCL高电平期间读取SDA,那最终主机在SCL高电平期间,依次读取8位,就接收到了从机发送的一个字节数据,00001111,也就是0x0f,那现在问题就来了,这个0x0f是从机哪个寄存器的数据呢,我们看到在读的时序中,I2C协议的规定是,主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马转为读的时序,所以主机还来不及指定,我想要读哪个寄存器就得开始接收了,所以这里就没有指定地址这个环节,那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢,这需要用到我们上面说的当前地址指针了,在从机中,所有的寄存器被分配到了一个线性区域中,并且会有个单独的指针变量,指示着其中一个寄存器,这个指针上电默认一般指向0地址,并且每写入一个字节和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值,那假设我刚刚调用了这个指定地址写的时序,在0x19 的位置写出了0xAA,那么指针就会加1移动到0x1A(0x19+1=0x1A)的位置,我再调用这个当前地址读的时序,返回的就是0x1A地址下的值,如果再调用一次,返回的就是0x1B地址下的值,以此类推,这就是当前地址读时序的操作逻辑,由于当前地址读并不能指定读的地址,所以这个时序用的不是很多。

(3)指定地址读

首先最开始仍然是启动条件,然后发送一个字节进行寻址,这里指定从机地址是1101000,读写标志位是0,代表我要进行写的操作,经过从机应答之后,再发送一个字节,第二个字节用来指定地址,这个数据就写入到了从机的地址指针里了,也就是说从机接收到这个数据之后,它的寄存器指针就指向了0x19 这个位置,之后我们要写入的数据,不给他发,而是直接再来个起始条件,这个Sr的意思就是重复起始条件,相当于另起一个时序,因为指定读写标志位,只能是跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件,然后起始条件后重新寻址并且指定读写标志位,此时读写标志位是1代表我要开始读了,接着主机接收一个字节,这个字节是不是就是0x19 地址下的数据,这就是指定地址读,你也可以再加一个停止条件,这样也行哈,这样的话就是两个完整的时序了,先起始写入地址停止,因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失,我们就可以再提示读当前位置停止,这样两条时序也可以完成任务,但是I2C协议官方规定的复合格式是一整个数据帧,就是先起始再重复起始再停止,相当于把两条时序拼接成一条。

二、MPU6050介绍

1.简介

2.参数

芯片进行I2C通信的从机地址,这个可以在手册里查到,当AD0等于0,地址为1001000,当AD0等于1时,地址为1001001,AD0就是板子引出来的一个引脚,可以调节I2C从机地址的最低位,这里地址是七位的。 如果像这样用二进制来表示的话,一般没啥问题,如果在程序中用16进制表示的话,一般会有两种表示方式,以这个1001000的地址为例,第一种就是单纯的把这七位的二进制转化为16进制,这里1001000低4位和高3位切开转换,16进制就是0x68(100 1000前面补了个0) ,所以有的地方就说MPU6050的从机地址是0x68 ,然后我们看一下之前I2C通信的时序,这里第一个字节的高7位是从机地址,最低位是读写位,所以如果你认为0x68是从机地址的话,在发送第一个字节时,要先把0x68 左移一位,再按位或上读写位,读1写0,这是认为从机地址是0x68 的操作,当然目前还有另一种常见的表示方式,就是把0x68 左移移位后的数据当做从机地址,0x68 左移1位之后是0xD0 ,那这样MPU6050的从机地址就是0xD0 ,这时在实际发送第一个字节时,如果你要写,就直接把0xD0 当做第一个字节;如果你要读就把0xd0或上0x01 即0xD1当做第一个字节,这种表示方式就不需要进行左移的操作了,或者说这种表示方式是把读写位也融入到了从机地址里来,0xD0 是写地址,0xD1是读地址,这样表示的,所以你之后看到有地方说0xD0是MPU6050的从机地址,那它就是融入了读写位的从机地址,如果你看到有地方说0x68是MPU6050的从机地址,这也不要奇怪,这种方式就是直接把7位地址转换16进制得到的,在实际发送第一个字节时,不要忘了先左移一位,再或上读写位,这是两种统计地址的表示方式。

3.硬件电路

  • 左上角是一个LDO低压差线性稳压器,这部分是供电的逻辑,手册里介绍这个MPU6050 芯片的VDD供电是2.375~3.46V属于3.3V供电的设备,不能直接接5V,所以为了扩大供电范围,这个模块的设计者就加了个3.3V的稳压器,输入端电压vcc_5v可以在3.3v到5v之间,然后经过3.3伏的稳压器输出稳定的3.3伏电压给芯片端供电,然后这一块是电源指示灯,只要3.3v端有电,电源指示灯就会亮。
  • 左下角是一个八针的排针,有VCC和GND这两个引脚是电源供电,然后SCL和SDA这两个引脚是I2C通信的引脚,在这里可以看到,SCL和SDA模块已经内置了两个4.7k的上拉电阻了,所以在我们接线的时候,直接把SCL和SDA接在GPIO口就行了,不需要再在外面另外接上拉电阻了,接着下面有XCL和XDA这两个是芯片里面的主机I2C通信引脚,设计这两个引脚是为了扩展芯片功能,之前我们说过,MPU6050是一个六轴姿态传感器,这是九轴姿态传感器多出的磁力计的作用,另外如果你要制作无人机,需要定高飞行,这时候就还需要增加气压计,扩展为十轴提供一个高度信息的稳定参考,所以根据项目要求啊,这个六轴传感器可能不够用,需要进行扩展,那这个时候这个XCL和XDA就可以起作用了,XCL和XDA通常就是用于外接磁力计或者气压计,当接上磁力计或气压计之后,MPU6050的主机接口可以直接访问这些扩展芯片的数据,把这些扩展芯片的数据读取到MPU6050 里面,在MPU6050 里面会有DMP单元进行数据融合和姿态解算,如果你不需要按MPU6050 的解算功能的话,也可以把这个磁力计或者气压计直接挂载在XCL和XDA这条总线上,因为I2C本来就可以挂载多设备,所以把多个设备都挂载在一起也是没问题的。下面AD0引脚,这个之前说过,他是从机地址的最低位,接低电平的话七位从机地址就是1001000,接高电平的话七位从机地址就是1001001,这里电路中可以看到有一个电阻默认弱下拉到低电平了,所以引脚悬空的话就是低电平,如果想接高电平,就可以把AD0直接引到VCC,强上拉至高电平。最后一个引脚是INT,也就是中断输出引脚,可以配置芯片内部的一些事件来触发中断引脚的输出,比如数据准备好了、I2C主机错误等,另外芯片内部还内置了一些实用的小功能、比如自由落体检测、运动检测、零运动检测等,这些信号都可以触发INT引脚产生电平跳变,需要的话可以进行中断信号的配置,但如果不要的话,那也可以不配置这个引脚。

4.框图

  • 左上角是时钟系统,有时钟输入脚和输出脚,不过我们一般使用内部时钟,硬件电路这里CLKIN直接接了地,CLKOUT没有引出所以这部分不需要过多关心,然后下面这些灰色的部分就是芯片内部的传感器,包括x y z轴的陀螺仪陀螺仪,另外这个芯片还内置了一个温度传感器,你要是想用它来测量温度也是没问题的,那这么多传感器本质上也都相当于可变电阻,通过分压后输出模拟电压,然后通过ADC进行模数转换,转化完成之后呢,这些传感器的数据统一都放到数据寄存器中,我们读取数据寄存器就能得到传感器测量的值了,这个芯片内部的转换都是全自动进行的,就类似我们之前学的AD连续转换加DMA转运,每个ADC输出,对应16位的数据寄存器,不存在数据覆盖的问题,我们配置好转换频率之后,每个数据就自动以我们设置的频率刷新到数据寄存器,我们需要数据的时候直接来读就行。
  • 接着每个传感器都有个自测单元self test,这部分是用来验证芯片好坏的,当启动自测后,芯片内部就会模拟一个外力施加在传感器上,这个外力导致传感器数据会比平时大一些,那如何进行自测呢,我们可以先使能自测读取数据,再失能自测读取数据,两个数据相减得到的数据叫自测响应,芯片手册里给出了一个范围,如果自测响应在这个范围内就说明芯片没问题,如果不在就说明芯片可能坏了,使用的时候就要小心点,这个是自测的功能。
  • 右边这一大块就是寄存器和通信接口部分了,中断状态寄存器可以控制内部的哪些事件到中断引脚的输出,FIFO是先入先出寄存器,可以对数据流进行缓存,我们本节暂时不用,配置寄存器,可以对内部的各个电路进行配置,传感器寄存器也就是数据寄存器,存储在各个传感器的数据,工厂校准这个意思就是内部的传感器都进行了校准我们不用了解,然后右边这个数字运动处理器简称DMP,还是芯片内部自带的一个姿态解算的硬件算法,配合官方的DMP库可以进行姿态解算,因为姿态解算还是比较难的,而且算法也很复杂,所以如果使用了内部的DMP进行姿态解算,姿态解算就会方便一些,暂时不涉及,这个FSYNC是帧同步,我们用不到,最后上面这块就是通信接口部分,上面一部分就是从机的I2C和SPI通信接口,用于和STM32通信,下面这一部分是主机的I2C通信接口,用于和MPU6050扩展的设备进行通信,这里有个接口旁路选择器(MUX)就是一个开关,如果拨到上面,辅助的I2C引脚就和正常的I2C引脚接到一起,这样两路总线就合在一起了,STM32可以控制所有设备,这时STM32就是大哥MPU6050和这个扩展设备都是stm32的小弟,如果拨到下面,辅助的I2C引脚就由mpu6050控制,两条I2C总线独立分开,这时STM32是MPU6050的大哥,MPU6050又是扩展设备的大哥。

三、软件I2C读写MPU6050

面包板接线:

代码示例:

MyI2C.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);		//根据BitValue,设置SCL引脚的电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);		//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);		//读取SDA电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
	return BitValue;											//返回SDA电平
}

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟
	
	/*GPIO初始化*/
	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);					//将PB10和PB11引脚初始化为开漏输出
	
	/*设置默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);			//设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}

/*协议层*/

/**
  * 函    数:I2C起始
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);							//释放SDA,确保SDA为高电平
	MyI2C_W_SCL(1);							//释放SCL,确保SCL为高电平
	MyI2C_W_SDA(0);							//在SCL高电平期间,拉低SDA,产生起始信号
	MyI2C_W_SCL(0);							//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}

/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * 函    数:I2C发送一个字节
  * 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)				//循环8次,主机依次发送数据的每一位
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));	//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
		MyI2C_W_SCL(1);						//释放SCL,从机在SCL高电平期间读取SDA
		MyI2C_W_SCL(0);						//拉低SCL,主机开始发送下一位数据
	}
}

/**
  * 函    数:I2C接收一个字节
  * 参    数:无
  * 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	for (i = 0; i < 8; i ++)				//循环8次,主机依次接收数据的每一位
	{
		MyI2C_W_SCL(1);						//释放SCL,主机机在SCL高电平期间读取SDA
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量
														//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
		MyI2C_W_SCL(0);						//拉低SCL,从机在SCL低电平期间写入SDA
	}
	return Byte;							//返回接收到的一个字节数据
}

/**
  * 函    数:I2C发送应答位
  * 参    数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
  * 返 回 值:无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);					//主机把应答位数据放到SDA线
	MyI2C_W_SCL(1);							//释放SCL,从机在SCL高电平期间,读取应答位
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
}

/**
  * 函    数:I2C接收应答位
  * 参    数:无
  * 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;							//定义应答位变量
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	MyI2C_W_SCL(1);							//释放SCL,主机机在SCL高电平期间读取SDA
	AckBit = MyI2C_R_SDA();					//将应答位存储到变量里
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
	return AckBit;							//返回定义应答位变量
}

MyI2C.h

#ifndef __MYI2C_H
#define __MYI2C_H

void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);

#endif

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS		0xD0		//MPU6050的I2C从机地址

/**
  * 函    数:MPU6050写寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 参    数:Data 要写入寄存器的数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();						//I2C起始
	MyI2C_SendByte(MPU6050_ADDRESS);	//发送从机地址,读写位为0,表示即将写入
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(RegAddress);			//发送寄存器地址
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(Data);				//发送要写入寄存器的数据
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_Stop();						//I2C终止
}

/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	MyI2C_Start();						//I2C起始
	MyI2C_SendByte(MPU6050_ADDRESS);	//发送从机地址,读写位为0,表示即将写入
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(RegAddress);			//发送寄存器地址
	MyI2C_ReceiveAck();					//接收应答
	
	MyI2C_Start();						//I2C重复起始
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);	//发送从机地址,读写位为1,表示即将读取
	MyI2C_ReceiveAck();					//接收应答
	Data = MyI2C_ReceiveByte();			//接收指定寄存器的数据
	MyI2C_SendAck(1);					//发送应答,给从机非应答,终止从机的数据输出
	MyI2C_Stop();						//I2C终止
	
	return Data;
}

/**
  * 函    数:MPU6050初始化
  * 参    数:无
  * 返 回 值:无
  */
void MPU6050_Init(void)
{
	MyI2C_Init();									//先初始化底层的I2C
	
	/*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
}

/**
  * 函    数:MPU6050获取ID号
  * 参    数:无
  * 返 回 值:MPU6050的ID号
  */
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);		//返回WHO_AM_I寄存器的值
}

/**
  * 函    数:MPU6050获取数据
  * 参    数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
  * 参    数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
  * 返 回 值:无
  */
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;								//定义数据高8位和低8位的变量
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
}

MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);

void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);

#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

uint8_t ID;								//定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ;			//定义用于存放各个数据的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	MPU6050_Init();		//MPU6050初始化
	
	/*显示ID号*/
	OLED_ShowString(1, 1, "ID:");		//显示静态字符串
	ID = MPU6050_GetID();				//获取MPU6050的ID号
	OLED_ShowHexNum(1, 4, ID, 2);		//OLED显示ID号
	
	while (1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);		//获取MPU6050的数据
		OLED_ShowSignedNum(2, 1, AX, 5);					//OLED显示数据
		OLED_ShowSignedNum(3, 1, AY, 5);
		OLED_ShowSignedNum(4, 1, AZ, 5);
		OLED_ShowSignedNum(2, 8, GX, 5);
		OLED_ShowSignedNum(3, 8, GY, 5);
		OLED_ShowSignedNum(4, 8, GZ, 5);
	}
}

四、硬件I2C读写MPU6050

1.I2C外设简介

2.I2C框图

  • 首先左边这里是外设的通信引脚SDA和SCL,下面SMBALERT是SMBus用到的,像这种外设模块引出来的引脚,一般都是借助GPIO口的复用模式与外部世界相连的,具体是复用在了哪个GPIO口呢,还是查询这个引脚定义表,在复用功能这两栏里找一下,因为内部电路设计的时候引脚就是连接好了的,所以如果想使用硬件I2C,就只能使用它连接好的指定硬件,不像软件I2C那样引脚可以任意指定。(这里选用I2C2 起对应的引脚是Pin10和Pin11)
  • 数据控制部分,数据收发的核心部分是这里的数据寄存器和数据移位寄存器,当我们需要发送数据时,可以把一个字节数据写到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步转到移位寄存器里,在移位的过程中我们就可以直接把下个数据放到数据寄存器器里等着了,一旦前个数据移位完成,下一个数据就可以无缝衔接继续发送,当数据由数据寄存器器转到移位寄存器时,就会置位状态寄存器的TXE位为1,表示发送寄存器器为空,那在接收时也是这一路,输入的数据一位一位的从引脚移入到移位寄存器里,当一个字节的数据收起之后,数据就整体从移位寄存器转到数据寄存器,同时置位标志位RXNE表示接收寄存器器非空,这时候我们就可以把数据从数据寄存器读出来了,这个流程和之前串口是一样的,只不过串口是全双工,这里是半双工。
  • 比较器和地址寄存器,这是从机模式使用的,stm32是基于可变多主机模型设计的,不进行通信的时候就是从机,就可以被别人召唤,想被别人召唤,它就应该有从机地址,就可以由这个自身地址寄存器制定,我们可以自定义一个从机地址写到这个寄存器,当stm32作为从机在被寻址时,如果收到的寻址通过比较器判断和自身地址相同,那stm32就作为从机响应外部主机的召唤,并且这个stm32 支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器。

3.I2C基本结构

移位进寄存器和数据寄存器DR的配合是通信的核心部分,这里因为I2C是高位先行,所以这个移位寄存器是向左移位,在发送的时候最高位先移出去,一个SCL时钟移位一次,八次这样就能把一个字节由高位到低位依次放到SDA线上了,那在接收的时候呢,数据通过GPIO口从右边依次移进来,最终移8次1个字节就接收完成了,使用硬件I2C的时候GPIO口都要配置成复用开漏输出的模式,交由片上外设来控制,这里即使是开漏输出模式,GPIO口也是可以进行输入的,然后SCL这里时钟控制器,通过GPIO去控制时钟线,这里我简化成一主多从的模型了,所以时钟这里只画了输出的方向,实际上前面这里如果是多主机的模型,时钟线也是会进行输入的。

4.主机发送

  • 七位地址的主发送和十位地址的主发送,它们的区别就是七位地址,起始条件后的一个字节是寻址,十位地址,起始条件后的两个字节都是寻址,其中前一个字节,这里写的是帧头,内容是五位的标志位,11110+2位地址+1位读写位,然后后一个字节内容就是纯粹的八位地址了,两个字节加一起构成十位的寻址,这是十位地的选择模式啊,我们主要关注七位地址的就行了。
  • 首先初始化之后,总线默认空闲状态,stm32 默认是从模式,为了产生一个起始条件,stm32 需要写入控制寄存器,这个得看一下手册的寄存器描述,在控制寄存器中有个START位,在这一位写1就可以产生起始条件了,当起始条件发出后,这一位可以由硬件清除,所以只要在这一位写1,stm32就自动产生起始条件了,之后stm32 由从模式转为主模式,也就是多主机模型下,stm32有数据要发就要跳出来这个意思。

5.主机接收

首先写入控制寄存器器的start位,产生起始条件,然后等待Ev5事件,下面解释和刚才一样,Ev5事件就代表起始条件已发送,最后是寻址、接收应答、结束后产生Ev6事件,这数据1这一块代表数据正在通过移位寄存器进行输入,Ev6_1事件,下面解释是没有对应的事件标志,只适于接收一个字节的情况,这个Ev6_1可以看到数据1其实还正在移位,还没收到呢,所以这个事件就没有标志位,之后当这个时序单元完成时,硬件会自动根据我们的配置,把应答位发送出去,如何配置是否要给应答呢,也是手册控制净寄存器CR1里,这里有一位ACK应答使能,如果写1在接收一个字节后就返回一个应答,写零就是不给应答,就是应答位的配置,之后继续当这个时序单元结束后,就说明移位寄存器器已经成功移入一个字节的数据1了,这时移入的一个字节就整体转移到数据寄存器,同时置RxNE标志位,表示数据寄存器非空,也就是收到了一个字节的数据,这个状态就是Ev7事件,下面解释是RxNE等于1,数据寄存器非空,读DR寄存器清除该事件,也就是收到数据了,当把这个数据读走之后,这个事件就没有了,上面这里Ev7事件没有了,说明此时数据1被读走,当然数据1还没读走的时候啊,数据2就可以直接移入移位寄存器了,之后数据2移位完成,收到数据2产生Ev7事件,读走数据2Ev7 事件没有了,然后按照这个流程就可以一直接收数据了,最后当我们不需要继续接收时,需要在最后一个时序单元发生时,提前把刚才说的应答位控制寄存器ack置0,并且设置终止条件请求,这就是Ev7_1事件。

6.软件/硬件波形对比

 7.硬件I2C读写MPU6050

硬件I2C读写MPU6050有固定引脚如下:

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS		0xD0		//MPU6050的I2C从机地址

/**
  * 函    数: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;										//跳出等待,不等了
		}
	}
}

/**
  * 函    数: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生成终止条件
}

/**
  * 函    数: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;
}

/**
  * 函    数:MPU6050初始化
  * 参    数:无
  * 返 回 值:无
  */
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
}


/**
  * 函    数:MPU6050获取ID号
  * 参    数:无
  * 返 回 值:MPU6050的ID号
  */
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);		//返回WHO_AM_I寄存器的值
}

/**
  * 函    数:MPU6050获取数据
  * 参    数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
  * 参    数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
  * 返 回 值:无
  */
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;								//定义数据高8位和低8位的变量
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
}

MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);

void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);

#endif

MPU6050.Reg.h

#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

#define	MPU6050_SMPLRT_DIV		0x19
#define	MPU6050_CONFIG			0x1A
#define	MPU6050_GYRO_CONFIG		0x1B
#define	MPU6050_ACCEL_CONFIG	0x1C

#define	MPU6050_ACCEL_XOUT_H	0x3B
#define	MPU6050_ACCEL_XOUT_L	0x3C
#define	MPU6050_ACCEL_YOUT_H	0x3D
#define	MPU6050_ACCEL_YOUT_L	0x3E
#define	MPU6050_ACCEL_ZOUT_H	0x3F
#define	MPU6050_ACCEL_ZOUT_L	0x40
#define	MPU6050_TEMP_OUT_H		0x41
#define	MPU6050_TEMP_OUT_L		0x42
#define	MPU6050_GYRO_XOUT_H		0x43
#define	MPU6050_GYRO_XOUT_L		0x44
#define	MPU6050_GYRO_YOUT_H		0x45
#define	MPU6050_GYRO_YOUT_L		0x46
#define	MPU6050_GYRO_ZOUT_H		0x47
#define	MPU6050_GYRO_ZOUT_L		0x48

#define	MPU6050_PWR_MGMT_1		0x6B
#define	MPU6050_PWR_MGMT_2		0x6C
#define	MPU6050_WHO_AM_I		0x75

#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

uint8_t ID;								//定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ;			//定义用于存放各个数据的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	MPU6050_Init();		//MPU6050初始化
	
	/*显示ID号*/
	OLED_ShowString(1, 1, "ID:");		//显示静态字符串
	ID = MPU6050_GetID();				//获取MPU6050的ID号
	OLED_ShowHexNum(1, 4, ID, 2);		//OLED显示ID号
	
	while (1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);		//获取MPU6050的数据
		OLED_ShowSignedNum(2, 1, AX, 5);					//OLED显示数据
		OLED_ShowSignedNum(3, 1, AY, 5);
		OLED_ShowSignedNum(4, 1, AZ, 5);
		OLED_ShowSignedNum(2, 8, GX, 5);
		OLED_ShowSignedNum(3, 8, GY, 5);
		OLED_ShowSignedNum(4, 8, GZ, 5);
	}
}
  • 42
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 25
    评论
以下是基于 STM32 标准库MPU6050 Yaw修正零点漂移代码: ```c #include "stm32f10x.h" #define MPU6050_ADDRESS 0xD0 #define MPU6050_SMPLRT_DIV 0x19 #define MPU6050_CONFIG 0x1A #define MPU6050_GYRO_CONFIG 0x1B #define MPU6050_ACCEL_CONFIG 0x1C #define MPU6050_WHO_AM_I 0x75 #define MPU6050_PWR_MGMT_1 0x6B #define MPU6050_PWR_MGMT_2 0x6C #define MPU6050_ACCEL_XOUT_H 0x3B #define MPU6050_ACCEL_XOUT_L 0x3C #define MPU6050_ACCEL_YOUT_H 0x3D #define MPU6050_ACCEL_YOUT_L 0x3E #define MPU6050_ACCEL_ZOUT_H 0x3F #define MPU6050_ACCEL_ZOUT_L 0x40 #define MPU6050_TEMP_OUT_H 0x41 #define MPU6050_TEMP_OUT_L 0x42 #define MPU6050_GYRO_XOUT_H 0x43 #define MPU6050_GYRO_XOUT_L 0x44 #define MPU6050_GYRO_YOUT_H 0x45 #define MPU6050_GYRO_YOUT_L 0x46 #define MPU6050_GYRO_ZOUT_H 0x47 #define MPU6050_GYRO_ZOUT_L 0x48 #define MPU6050_GYRO_LSB_SENSITIVITY 131.0f void MPU6050_Init(void); void MPU6050_Write(uint8_t addr, uint8_t data); void MPU6050_Read(uint8_t addr, uint8_t *buf, uint16_t len); float MPU6050_GetYaw(void); int main(void) { float yaw = 0.0f; MPU6050_Init(); while (1) { yaw = MPU6050_GetYaw(); // do something with yaw value // delay for some time for (int i = 0; i < 1000000; i++); } } void MPU6050_Init(void) { // reset MPU6050 MPU6050_Write(MPU6050_PWR_MGMT_1, 0x80); // delay to let the MPU6050 reset for (int i = 0; i < 1000000; i++); // set clock source to PLL with X-axis gyroscope reference MPU6050_Write(MPU6050_PWR_MGMT_1, 0x01); // set gyroscope full scale range to +/- 250 degrees/sec MPU6050_Write(MPU6050_GYRO_CONFIG, 0x00); // set accelerometer full scale range to +/- 2g MPU6050_Write(MPU6050_ACCEL_CONFIG, 0x00); // set sample rate divider to 0 (500Hz sample rate) MPU6050_Write(MPU6050_SMPLRT_DIV, 0x00); } void MPU6050_Write(uint8_t addr, uint8_t data) { I2C_StartTransmission(MPU6050_ADDRESS, I2C_Direction_Transmitter); I2C_WriteData(addr); I2C_WriteData(data); I2C_StopTransmission(); } void MPU6050_Read(uint8_t addr, uint8_t *buf, uint16_t len) { I2C_StartTransmission(MPU6050_ADDRESS, I2C_Direction_Transmitter); I2C_WriteData(addr); I2C_StopTransmission(); I2C_StartTransmission(MPU6050_ADDRESS, I2C_Direction_Receiver); for (uint16_t i = 0; i < len; i++) { buf[i] = I2C_ReadData(len - i - 1); if (i == len - 1) { I2C_AcknowledgeConfig(I2C_NACK); } } I2C_StopTransmission(); } float MPU6050_GetYaw(void) { uint8_t buf[6] = {0}; int16_t gyro_x = 0, gyro_y = 0, gyro_z = 0; float yaw = 0.0f; MPU6050_Read(MPU6050_GYRO_XOUT_H, buf, 6); gyro_x = ((int16_t)buf[0] << 8) | buf[1]; gyro_y = ((int16_t)buf[2] << 8) | buf[3]; gyro_z = ((int16_t)buf[4] << 8) | buf[5]; // Yaw calculation yaw = (float)gyro_z / MPU6050_GYRO_LSB_SENSITIVITY; // Yaw zero drift correction static float yaw_offset = 0.0f; static uint32_t yaw_offset_cnt = 0; if (yaw_offset_cnt < 1000) { yaw_offset += yaw; yaw_offset_cnt++; } else { yaw_offset /= 1000.0f; yaw -= yaw_offset; } return yaw; } ``` 在代码中,通过使用 MPU6050 陀螺仪,获取当前的 Yaw 值,并对零点漂移进行修正。Yaw 值的计算公式为: $$Yaw = \frac{gyro_z}{131.0}$$ 其中 131.0 是 MPU6050 的陀螺仪 LSB 灵敏度值。Yaw 零点漂移的修正是通过使用一个静态变量 yaw_offset 和一个计数器 yaw_offset_cnt,将前1000次获取到的 Yaw 值的平均值作为零点漂移的修正量。然后,每次获取到 Yaw 值时,都减去这个修正量。这样可以让 Yaw 值趋近于真实值,从而提高程序的精度。
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

热爱嵌入式的小佳同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值