因为stm32的硬件I2C有问题(好像F4以后的没问题了)
我用的gd32,所以可以用硬件,但我不用这是个用硬件i2c的文章
硬件i2c工作时不能被中断打断,
然后工作在低频率下还好,频率高了就要寄了(这是有问题的硬件i2c,正常的话应该模拟的工作更慢)
这里的工作就是把普通io模拟i2c时序,与mpu6050通讯

模拟I2C

因为是上拉的,空闲都是高电平

时序

顺序:
起始信号,数据,应答信号,停止信号
image-1677205528039
起始信号:SCL高电平时,SDA有下降沿(高电平变低电平)
image-1677205888000
发送数据:SCL高电平时,【稳定】的SDA高就是1,低就是0;以8bit的数据发送。先发送高电平。 【发送数据这里,SDA一定要再SCL低电平时变化】
image-1677206277071
应答信号(ACK):高电平是表示没收到数据(nack),低电平是收到了数据(ack);在发送数据的后面。
image-1677206715774
停止信号:SCL高电平时,SDA有上升沿
image-1677205993076

初始化

这里我们创建个i2c.c和i2c.h专门放这些函数吧,看着好看。

延时

为什么要延时呢,在这说一下看这个表,i2c时序
image-1677378216244
这是有最低的高低电平时间的,为了超过最低延时时间,就设置了2us延时。

写在i2c.c文件里
首先定义一个iic_delay函数,目的就是控制IIC的读写速度,通过示波器检测读写速度在250KHz内,所以一秒钟传送500Kb数据,换算一下即一个bit位需要2us,在这个延时时间内可以让器件进行获得一个稳定性的数据采集。
不想写delay_us的函数,用__nop代替吧,image-1677564756139,写144个__nop();

void IIC_DELAY(){
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();
}

gpio

SCL可以推挽也可以开漏
SDA一定要配置开漏输出,因为SDA又要输出数据,又要接收数据
开漏输出可以输入输出共用,避免了io模式频繁切换
先初始化引脚开漏输出,高速(gpio电平设置高)
image-1677213429012

一些宏

写在i2c.h文件里
上面初始化gpio我改了user label,所以引脚名字是SCL_Pin和SDA_Pin,
这里写一下设置引脚高低和读取引脚电平的宏,后面用着方便。

#include "stm32f1xx_hal.h"
#include "gpio.h"

#define IIC_SCL(x) do{ x ? \
	HAL_GPIO_WritePin(GPIOB,SCL_Pin,GPIO_PIN_SET): \
	HAL_GPIO_WritePin(GPIOB,SCL_Pin,GPIO_PIN_RESET);\
}while(0)

#define IIC_SDA(x) do{ x ? \
	HAL_GPIO_WritePin(GPIOB,SDA_Pin,GPIO_PIN_SET): \
	HAL_GPIO_WritePin(GPIOB,SDA_Pin,GPIO_PIN_RESET);\
}while(0)
	
#define IIC_READ_SDA	HAL_GPIO_ReadPin(GPIOB, SDA_Pin)

起始信号

写在i2c.c文件里
SCL高电平时,SDA有下降沿
先让SDA为高电平,保证SCL高的时候只产生下降沿。

void IIC_START()
{	
	IIC_SDA(1);
	IIC_SCL(1);
	IIC_DELAY();
	
	IIC_SDA(0);		/* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
	IIC_DELAY();
	
	IIC_SCL(0);		/* 钳住I2C总线,准备发送或接收数据 */
	IIC_DELAY();
}

停止信号

SCL高电平时,SDA有上升沿
先让SDA为低电平,保证SCL高的时候只产生上升沿。

void IIC_STOP(){
	IIC_SDA(0);     
	IIC_DELAY();
	
	IIC_SCL(1);	/* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
	IIC_DELAY();
	IIC_SDA(1);     /* 发送I2C总线结束信号 */
	IIC_DELAY();
}

检测ACK信号

发送完数据后,SCL高时,SDA高就表示没收到,SDA低就是收到了
因为要返回值,所以不用void了
说一下循环里面:
数据发送完,SDA是释放掉的,是高电平,直接会进入循环。然后等待一段时间
如果收到了ack信号,那么IIC_READ_SDA会变成0,跳出while循环,rack=0,返回0表示接收成功。
如果收到nack信号,也就是SDA一直是1,那么一段时间后进入if语句,停止IIC,让rack变成1,break退出while循环,返回1表示接收失败

检测完成后记得让SCL变成0(钳住SCL),以便于下一个SDA的变化。

uint8_t IIC_WAIT_ACK(){
	uint8_t waittime=0 ,rack=0;
	
	//在发送数据那里释放过SDA线,所以不重复写浪费时间了
	//在SCL高电平时读取SDA发送来的信号,高电平是nack,低电平是ack
	IIC_SCL(1);
	IIC_DELAY();
	
	while(IIC_READ_SDA){
		waittime++;
		if(waittime>250)
		{
			IIC_STOP();
			rack=1;
			break;
		}
	}
	IIC_SCL(0);
	IIC_DELAY();
	return rack;
}

发送ack信号

MCU也会接收数据的,那么也要给从设备发送ack信号
接收数据结束后,SDA被释放,当SCL高时,SDA高就是接收失败 (nack),SDA低就是接收成功(ack)
这里写发送接收成功的ack
SCL高电平时有一个稳定的SDA低电平
记得发送完后让SCL变0以便于之后SDA的变化,并且释放掉SDA线,让从机使用。

void IIC_ACK(){
	IIC_SDA(0);
	IIC_DELAY();
	IIC_SCL(1);
	IIC_DELAY();
	//钳住SCL并释放SDA线
	IIC_SCL(0);
	IIC_DELAY();
	IIC_SDA(1);
	IIC_DELAY();
}

发送nack信号

SCL高电平时有一个稳定的SDA高电平

void IIC_NACK(){
	IIC_SDA(1);
	IIC_DELAY();
	IIC_SCL(1);
	IIC_DELAY();
	//钳住SCL并释放SDA线,不过本来SDA就是高,就不写了浪费时间了
	IIC_SCL(0);
	IIC_DELAY();
}

发送1byte数据

iic一次发送8位数据,用一个循环
iic从高位开始发送,所以读取高位,设置SDA
SCL低的时候更改SDA,SCL高的时候SDA要稳定
发送完成,为了获取从机的应答信号,要释放SDA线,让从机可以选择下拉SDA,给出应答

void IIC_SEND_BYTE(uint8_t data)
{
	uint8_t t;
	for(t=0;t<8;t++)
	{
		IIC_SDA((data & 0x80)>>7);	//高位先发送,读取data高位
		IIC_DELAY();
		IIC_SCL(1);
		IIC_DELAY();
		IIC_SCL(0);
		data<<=1;	//左移1位,用于下一次发送;uint8,最高位左移没了,次高位变最高位
	}
	IIC_SDA(1);	//发送完成, 主机释放SDA线
}

读取1BYTE数据

IIC又要发送数据,又要接收数据,那读取也必不可少
定义一个变量用来存放数据,最后返回这个变量
说一下第一次循环里的左移操作,因为本来就是0,所以左移没有意义,不要纠结这个。之后的循环里,左移会让低位变成高位,从而把8位数据都存进去

uint8_t IIC_READ_BYTE(uint8_t ack)
{
	uint8_t receive=0;
	for(uint8_t t=0;t<8;t++){
		//先来的数据是高位的,那么接收一次就向左移位一次。最低为就变0
		receive<<=1;
		
		IIC_SCL(1);
		IIC_DELAY();
		
		if(IIC_READ_SDA)receive++;	//如果接收到是1,receive最低位就变1.
		
		IIC_SCL(0);
		IIC_DELAY();
	}
	if(ack){
		IIC_ACK();
	}
	else{
		IIC_NACK();
	}
	return receive;
}

MPU6050

看之前最好准备一下MPU6050的数据手册寄存器手册;主要得看寄存器手册。

基本的函数

这一部分函数依然写在i2c.c里面
上面我们做好了IIC的基本操作的函数,可以发送接收数据;但还不够,我们要直接的给从设备发送信息。
但是我们一个IIC上面挂了很多设备,怎么才能确定这个数据是给谁的呢
image-1677301808130

起始信号-----从机地址+读/写-----从机的应答信号-----从机的寄存器地址-----【数据–应答信号】-----【数据–应答信号】–停止信号
写入:
image-1677319184747
读取:
image-1677319721295

那么从机地址是多少呢

MPU-60X0 的Slave 地址为b110100X,7 位字长,最低有效位X 由AD0 管脚上的逻辑电平决定。这样就可以允许两个MPU-60X0 连接到同一条I2C 总线,此时,一个设备的
地址为b1101000(AD0 为逻辑低电平),另一个为b1101001(AD0 为逻辑高)。
image-1677317128519
咱们这个AD0是下拉的,0x68
image-1677317594729
0x68,写成2进制,0110 1000,而我们要发送的应该是高7位地址,最低位1/0(读/写)。
那么到时候数据记得把0x68左移一位,再加上最低位的读或者写。

读取写入IIC设备的数据

image-1677319721295
看着表写
主机发送【开始信号】,发送【从机地址+写】,等待【从机ACK信号】,发送【寄存器地址】,等待【从机ACK信号】,发送【开始信号】,发送【从机地址+读】,等待【从机ACK信号】,接收【从机数据】,主机发送【ACK信号】,接收【从机数据】……主机发送【NACK】,主机发送【停止信号】。
当然,连续读或写,不是连续往这一个寄存器重复操作,里面有递增寄存器,会自动往后面的寄存器操作的。
读取的最后一个应答信号一定是NACK,不然结束不了,等着后面操作报错吧

读取IIC设备1Byte数据

先写一个只读取1Byte数据的代码,连续读取下面再写。
三个参数,从机地址,寄存器地址,用来存数据的变量

uint8_t IIC_ReadByteFromSlave(uint8_t I2C_Addr,uint8_t reg,uint8_t *buf)
{

	IIC_START();
	IIC_SEND_BYTE(I2C_Addr<<1|0);	//写
	if(IIC_WAIT_ACK()){
		//printf("从机地址发送出错 \r\n");
		return 1;
	}
	IIC_SEND_BYTE(reg);
	if(IIC_WAIT_ACK()){
		//printf("寄存器reg地址发送出错 \r\n");
		return 2;
	}
	IIC_START();
	IIC_SEND_BYTE(I2C_Addr<<1|1);	//读
	IIC_WAIT_ACK();
	*buf = IIC_READ_BYTE(0);
	IIC_STOP();
	return 0;
}

看这个代码: IIC_SEND_BYTE(I2C_Addr<<1|1);,也许有人会犯傻比,想着I2C_ADDR与1相或,那不I2C_ADDR的8位全变成1了吗。那你就得想想0110 1000和0000 0001相与,结果是啥了。

读取whoami寄存器

用这个函数,就可以尝试读取一下寄存器了,这个寄存器可以让你知道你是不是被坑了,比如买9250,买到6500
image-1677333460334
只要你是6050,你读whoami寄存器,就一定读到0x68
寄存器地址这个表里写了,0x75

		test=IIC_ReadByteFromSlave(0x68,0x75,&IIC_BUFFER);
		printf("MPU6050 0x%x\r\n",IIC_BUFFER);
		HAL_Delay(1000);

在main函数的while循环复制下这个,就OK了
记得去cubeMX初始化一下usart1,把print函数重写一下,不知道可以看这,最下面的重写printf,代码找个地方复制一下就行,不在主函数也没问题,重写了到处都能用。
image-1677334016455

连续读取IIC数据

不细说了,对着上面的时许表来自己看吧
四个参数:从设备地址,寄存器地址,连续读取长度,存放数据的数组

uint8_t IIC_ReadMultibyteFromSlave(uint8_t I2C_Addr,uint8_t reg,uint8_t len,uint8_t *data)
{

	uint8_t count = 0;
	IIC_START();
	IIC_SEND_BYTE(I2C_Addr<<1|0);	//写
	if(IIC_WAIT_ACK()){
		//printf("从机地址发送出错 \r\n");
		return 1;
	}
	IIC_SEND_BYTE(reg);
	if(IIC_WAIT_ACK()){
		//printf("寄存器reg地址发送出错 \r\n");
		return 2;
	}
	IIC_START();
	IIC_SEND_BYTE(I2C_Addr<<1|1);	//读
	IIC_WAIT_ACK();
	for(count=0;count<(len-1);count++)	//除了最后一次接收数据,都是发送ACK应答
	{
		*data = IIC_READ_BYTE(1);
		data ++;

	}
	*data = IIC_READ_BYTE(0);	//最后一次接收数据,发送NACK应答
	IIC_STOP();
	return 0;
}

写1BYTE数据

uint8_t IIC_WriteByteFromSlave(uint8_t I2C_Addr,uint8_t reg,uint8_t data)
{
	IIC_START();
	IIC_SEND_BYTE(I2C_Addr<<1 | 0);
	if(IIC_WAIT_ACK()){
		//printf("从机地址发送出错 \r\n");
		return 1;
	}
	IIC_SEND_BYTE(reg);
	if(IIC_WAIT_ACK()){
		//printf("寄存器reg地址发送出错 \r\n");
		return 2;
	}
	IIC_SEND_BYTE(data);
	if(IIC_WAIT_ACK()){
		//printf("发送数据出错 \r\n");
		return 3;
	}
	IIC_STOP();
	return 0;
}

连续写数据

仔细看的人就会发现了,连续读与连续写两个函数里面的for循环,我用了两种方法获取数据,
当然,这没什么意义,但你们可以看着这两种方法想想自己还记不记得。

uint8_t IIC_WriteMultibyteFromSlave(uint8_t I2C_Addr,uint8_t reg,uint8_t len,uint8_t* data)
{
	uint8_t count=0;
	IIC_START();
	IIC_SEND_BYTE(I2C_Addr<<1 | 0);
	if(IIC_WAIT_ACK()){
		//printf("从机地址发送出错 \r\n");
		return 1;
	}
	IIC_SEND_BYTE(reg);
	if(IIC_WAIT_ACK()){
		//printf("寄存器reg地址发送出错 \r\n");
		return 2;
	}
	for(count=0;count<len;count++)
	{
		IIC_SEND_BYTE(data[count]);
		if(IIC_WAIT_ACK()){
			//printf("发送数据出错 \r\n");
			return (count+3);
		}
	}
	IIC_STOP();
	return 0;
}

与6050通讯的流程

这一部分函数在mpu6050.c里面写
MPU6050一上电(刚复位过)是睡眠状态的,读不到whoami寄存器以外的信息很正常

初始化MPU6050

1,检查MCU(主机)与MPU6050(姿态传感器)是否通信成功
2,设置MPU6050为复位状态,并且100ms延时,确保复位完成
3,唤醒MPU6050,选择PLL为时钟源
4,配置6轴数据全部输出(3轴加速度与3轴陀螺仪)
5,禁止所有中断(避免传感器读取到一个数据就中断,而耽误系统进程)
6,设置MPU6050的内部采样频率以及低通滤波器(小的飞行器一一般设施在20~30Hz即可)
7,设置陀螺仪与加速度计的满量程范围

1.检查是否通讯成功

这一步在上面做过了,去看读取whoami寄存器这一块
whoami寄存器什么时候都能通讯,也就可以拿来测试通讯是否成功

void MPU6050_TestConnection(){
	uint8_t buf;
	if(IIC_ReadByteFromSlave(0x68,0x75,&buf)){
		printf("MPU6050连接成功:0x%x",buf);
	}
}

2.设置复位状态

看表格,这个电源寄存器1比较重要
最高位就是复位
image-1677385654930
image-1677385774392
设置DEVICE_RESET为1,就会复位,然后这一位会自动清零
都复位了,就不读取其他寄存器是什么,再或上1了,直接让最高位是1,其他位随便搞【0x80】;当然,复位需要时间,具体要多久我不清楚,但大家都设置延时上100ms再操作。
那么复位以后各种寄存器都变成什么呢,我们在寄存器手册里面的第三章能看到
image-1677386857082
很多博客里面写复位后107寄存器复位值是0010 0000,那是他们写错了,好像是9250的是这样的,被他们复制到了6050的教程里面

IIC_WriteByteFromSlave(0x68,0x6B,0x80);
HAL_Delay(100);

3.唤醒并配置时钟

还是上面哪个107寄存器,电源寄存器1
这个寄存器配置的还蛮多的,一块说了吧:
1.bit7最高位复位:不说了
2.bit6次高位休眠:休眠的时候功耗低,但我们像要让MPU6050工作,就不能让他睡眠,后面要设置成0
3.bit5CYCLE,循环模式:这一位如果置1,并且SLEEP置0(没休眠),那么设备会再睡眠模式与唤醒模式中循环。我们要让他一直工作,所以这一位置0
4.bit3使能温度传感器:置0的时候就使能温度传感器了,如果温度高了,要通过温度传感器来抑制温漂。(我不用)
5.bit2~0时钟:看上面的表,我们用X轴PLL时钟,也就是1配置001;选0的话内置8M RC时钟精度不高,不如锁相环时钟,外部时钟的话我也不清楚哪来的外部时钟,所以选内部的X锁相环。

总结下来就是配置0x01(0000 0001)

IIC_WriteByteFromSlave(0x68,0x6B,0x01);

4.配置六轴数据输出

这里要用的是108寄存器;电源管理寄存器2
image-1677390229083
如果之前配置循环模式,那么用到bit7,bit6
但我们没用循环,就直接唤醒的,我们要高频率的使用他,所以让六轴全部使能,LP_WAKE_CTRL没用,随便设置;我设置0x00(00000000)

IIC_WriteByteFromSlave(0x68,0x6C,0x00);

5.禁止中断

不知道说啥,我用不到中断,所以我禁止了中断0x00(0000 0000)
这里用的56寄存器;中断使能寄存器
image-1677390938485
MOT_EN:用到运动检测中断,打开他
FIFO_OFLOW_EN:这里的FIFO一般用在DMP解算,如果用到可以打开它
I2C_MST_INT_EN:使能I2C主机所有中断源产生中断,,,,抱歉没看明白,不知道咋说
DATA_RDY_EN:数据就绪中断,所有寄存器,只要有写操作完成,就产生中断(在IMU_INT引脚产生)

IIC_WriteByteFromSlave(0x68,0x38,0x00);

6.设置内部采样频率和低通滤波器

内部采样频率

加速度计获取加速度,陀螺仪获取角速度,都需要一个采样频率,就在这里设置
那么采样频率要设置多少才合适呢,这要根据姿态解算来设置
比如我们姿态解算用500Hz,那么我们采样频率起码不能低于这个500Hz,我记得哪一门课有讲过采样频率多少比较好,好像是数字信号处理这门课是吧,记得的人在下面说说。那么我们设置1KHz的采样频率
 采样定理,又称香农采样定理,奈奎斯特采样定理,只要采样频率大于或等于有效信号最高频率的两倍,采样值就可以包含原始信号的所有信息,被采样的信号就可以不失真地还原成原始信号。
用到25寄存器,采样频率分频器
image-1677391817979
image-1677564982304
当输出频率为1K的时候,SMPLRT_DIV配置0,那么image-1677565010880
那么我们知道了分配系数要怎么选

数字低通滤波器

这时候聪明的小伙伴就要问了,哎我屮你说这么多,他陀螺仪输出频率是多少呢
我们上面图里,有提到过DLPF(数字低通滤波器),当低通滤波器disabled,输出频率是8K,当低通滤波器enabled,输出频率是1K,下面这个表就更清楚了
image-1677394976814
滤波就很重要,想想我们板子如果震动,对数据影响有多大。【滤波的Bandwidth设置多大就与板子的震动有关了】
但是我们要配置让多大的频率导通呢-----(为了消除干扰,把一些杂乱的信号去掉)
从原子哥那里看到的:设置 image-1677565048540
希望知道为什么设置1/2采样率的说一下,我不懂
重点来了,我不知道该配置多少,我去抄了一个别人的无人机的代码,他用的采样频率1K(也就是25寄存器写0x00),DLPF设置20Hz。我也这样吧
前面哪个FSYNC帧同步的,咱们也不用,配置0就行。
那么25寄存器配置0x00(0000 0000),DLPF配置4(20Hz带宽)26寄存器配置0x04(0000 0100)

IIC_WriteByteFromSlave(0x68,0x19,0x00);	//分频0(采样频率1K)
IIC_WriteByteFromSlave(0x68,0x1A,0x04);	//数字低通滤波器带宽20Hz

7.陀螺仪与加速度计满量程范围配置

陀螺仪

陀螺仪的ADC是16位的,±32768,也就是能有65536个数值;他的量程范围有±250,500,1000,2000(°/S),一般设置±2000的范围,他的灵敏度是image-1677565103138
测量范围大,灵敏度就低。
image-1677413341476
如果用9250,这里低两位还要设置一个参数,用来配合使用数字低通滤波器
从这个bit3,bit4设置,另外三位是设置是否自检的,我就不自检了
那就是FS_SEL设置3(11)

IIC_WriteByteFromSlave(0x68,0x1B,0x18);	//设置陀螺仪量程±2000,不自检
加速度计

加速度计ADC也是16位的,量程范围有±2,4,8,16g
想想这小飞机能飞多块,咱们设置±4g吧,从下面看是设置AFS_SEL 为1(01)
image-1677413876351
image-1677413885785
还是三位是设置自检,两位设置量程

IIC_WriteByteFromSlave(0x68,0x1C,0x08);	//设置加速度计量程±4g,不自检

一些可能会以后用,但这个项目不会用的常见寄存器

FIFO使能寄存器(0x23):用DMP的时候少不了用这个
INT引脚/旁路有效 使能寄存器(0x37):这里能设置让主设备直接访问MPU6050的从设备(可以和MPU6050同时和主设备通讯)(不过这样为什么不挂在一根I2C上呢)

总体看下来

void MPU6050_Init()
{
	MPU6050_TestConnection();	//检测连接是否成功
	IIC_WriteByteFromSlave(0x68,0x6B,0x80);	//复位
	HAL_Delay(100);
	IIC_WriteByteFromSlave(0x68,0x6B,0x01);	//唤醒并启用时钟
	IIC_WriteByteFromSlave(0x68,0x6C,0x00);	//配置六轴输出
	IIC_WriteByteFromSlave(0x68,0x38,0x00);	//关闭中断
	IIC_WriteByteFromSlave(0x68,0x19,0x00);	//分频0(采样频率1K)
	IIC_WriteByteFromSlave(0x68,0x1A,0x04);	//数字低通滤波器带宽20Hz
	IIC_WriteByteFromSlave(0x68,0x1B,0x18);	//设置陀螺仪量程±2000,不自检
	IIC_WriteByteFromSlave(0x68,0x1C,0x08);	//设置加速度计量程±4g,不自检
}

获取原始数据

6050初始化完了,就可以拿数据了,为什么是原始数据,因为后面我们还要处理一下才能用

加速度计原始数据

image-1677416547648
看表格就能知道,连续的六个寄存器,放着XYZ轴的加速度数据
我们要做的是创建一个函数,在里面读取这六个寄存器,然后X的高低两个寄存器的数据放到一块,Y的放一块,Z的放一块
对了,这个ADC要表示有符号的数,咱们的用来保存数据的参数就不要用uint了,用int
6个寄存器,用长度为6的数组存放,然后高位寄存器左移8位,或上低位寄存器,传给参数。

void MPU6050_AccRead(int16_t *accData)
{
	uint8_t buf[6];
	IIC_ReadMultibyteFromSlave(0x68,0x3B,6,buf);
	accData[0] = (int16_t)((buf[0]<<8)|buf[1]);	//X
	accData[1] = (int16_t)((buf[2]<<8)|buf[3]);	//Y
	accData[2] = (int16_t)((buf[4]<<8)|buf[5]);	//Z
}

陀螺仪原始数据

image-1677417200370
陀螺仪就和上面一样的操作

void MPU6050_GyroRead(int16_t *gyroData)
{
	uint8_t buf[6];
	IIC_ReadMultibyteFromSlave(0x68,0x3B,6,buf);
	gyroData[0] = (int16_t)((buf[0]<<8)|buf[1]);	//X
	gyroData[1] = (int16_t)((buf[2]<<8)|buf[3]);	//Y
	gyroData[2] = (int16_t)((buf[4]<<8)|buf[5]);	//Z
}

温度数据

image-1677417661223
也是差不多的,只是这个只有两个寄存器
温度就不算什么原始数据了,拿到这个温度就是最终数据
像什么36.8℃之类的,咱们用的是浮点对吧,所有传参数用浮点类型
看上面图片,单位是摄氏度,计算公式为:
image-1677564903101

void MPU6050_tempRead(float *tempData)
{
	uint8_t buf[2];
	short data;
	IIC_ReadMultibyteFromSlave(0x68,0x41,2,buf);
	data = (int16_t)((buf[0]<<8)|buf[1]);	
	*tempData = (data/340)+36.53;
}

为什么用short类型数据,因为short是16位能存下数据,而且有符号

中途测试

做这么多有没有人还没去测试的,下面是我的main函数,你可以对着写一下,看看输出出来是多少

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */
	int16_t gyroData[3],accData[3];
	float tempData;
  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
	RGB_Color(33,125,155);
	RGB_Color(255,0,0);
	RGB_Color(0,255,0);
	RGB_Color(0,0,255);
	IIC_STOP();
	MPU6050_Init();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
		
		MPU6050_tempRead(&tempData);
		MPU6050_GyroRead(gyroData);
		MPU6050_AccRead(accData);
		printf("温度:%f \r\n",tempData);
		printf("角速度:X:%d,Y:%d,Z:%d \r\n",gyroData[0],gyroData[1],gyroData[2]);
		printf("加速度:X:%d,Y:%d,Z:%d \r\n",accData[0],accData[1],accData[2]);
		HAL_Delay(1000);
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

image-1677419296814

姿态解算

我们现在手里的数据,很难用到,我们要把他变成欧拉角,来方便我们使用
欧拉角也就是航向角(yaw),横滚角(roll),俯向角(pitch)
MPU6050里面内置了DMP数字运动处理器。可以将原始数据直接转化为四元数输出,然后我们再通过四元数转换成欧拉角;用DMP可以大大简化代码设计,并且MCU的工作减少了,可以省出资源拿去做其他工作
这一篇太长了,我们下一篇再讲吧,来下一篇看姿态解算