MPU6050+OLED读取姿态角(超级细讲)

news2024/9/22 17:37:04

STM32F103C8T6读取陀螺仪MPU6050的角度数据,使用6050自带DMP库姿态解算出各个方向的角度,并使用OLED实时刷新显示,同时可以将数据通过串口发送到计算机,每一组数据50ms。本操作过程简单,方便移植,显示屏接PA5/7,陀螺仪接PB6/7,串口为PA9/10

改变俯仰角Pitch

改变横滚角Roll

改变航偏角Yaw

上面是姿态角的示例,下面是重点:

IIC驱动

STM32的硬件IIC是存在一定问题的,初始化MPU6050时可能导致初始化失败,所以推荐使用软件IIC。

MPU6050的摆放

默认情况下相对于载体坐标系而言,绕x轴旋转为横滚(roll),绕y轴旋转是俯仰角(pitch),绕z轴旋转是偏航(yaw)。注意是绕。

如果把MPU6050看成一架飞机,摆动机翼是横滚角,上下摆动机头是俯仰角,左右摆动机头是偏航角。

下图摆放方式是错误的

图2.1

下图的摆放方式才是正确的,如上文描述可知。

图2.2

注意图2.2(上方第二张图),是指默认情况下。在官方的DMP驱动中有这样一段代码

static signed char gyro_orientation[9] = { 1, 0, 0,
                                           0, 1, 0,
                                           0, 0, 1};

这个是MPU6050的坐标矩阵,可根据实际摆放的位置修改此矩阵。如果不修改这个矩阵,需要按照图2.2摆放,不然数值肯定是错误的。

MPU6050初始化

MPU6050初始化有可能会卡死。可参考正点原子的例程,实测没什么问题。
MPU6050 DMP 是带有自检功能的

run_self_test();

run_self_test 这个函数用于自检,MPU6050会以初始状态作为欧拉角的0度,不管有没有放平MPU6050都会以初始状态作为欧拉角的0度,初始化之后MPU6050会稍微校准一下,那么前几秒的数据是可以适当舍弃的。
虽然MPU6050会稍微校准,但是初始化时一定要放平。如果不放平大概率报错。当然也有可能成功,但是MPU6050校准的数据会有偏差。

MPU6050偏航角(yaw)零飘

MPU6050的偏航角是不正常的,即使静止不动也会跳动。这个纯粹是硬件问题,就算算法怎么牛逼也解决不了(不可避免的)。只能外加磁力计解决,也可以直接使用MPU9250,这个是自带磁力计的。使用时注意不要和电机靠太近,磁场很容易受到干扰。

MPU6050万向节锁

默认Z-Y-X顺序下,不知道大家有没有发现这个问题。当MPU6050只转动俯仰角,俯仰角接近90度或者等于90度时,偏航角的角度会发生很大的偏差。那么这个就是万向节锁,此时如果比作一架飞机,当飞机垂直90度向上时,按照Z-Y-X顺序无论怎么改变方向飞机都只能向上飞。
所以不要让俯仰角垂直,这样可以减少bug。

还是要再细讲

本文章主要讲述mpu6050在应用中遇到的问题、问题的本质分析和解决办法。在此之前我们需要知道姿态(x,y,z轴角度)解算是通过mpu6050返回的各轴的“加速度”和“角速度”来解算的。

mpu6050存在的问题:

1. mpu6050零漂问题

2.mpu6050由于离心力的问题

3.mpu6050温漂问题

一个个来讲

1、零漂问题是指传感器在没有输入信号时输出的偏差,这是MEMS传感器常见的问题,因为它们的制造工艺和材料特性可能导致的。(不可避免的)

2、小车在直行过程只有支持力与重力的受力。而转弯时有向心力

 小车在急弯行驶时,受到自身的重力和地面的支持力和理论上完全水平向圆心方向的“向心力”。不过在事实中小车由于自身的车身或者自己的机械结构的差异,导致此时的向心力不是完全水平向里的了,就会导致向心力会有一些分力分解到垂直方向上,那么此时的z轴加速度数据就会受到影响。

那我们回来,还记得我们说过姿态解算是通过mpu6050传回来的加速度和角速度进行解算的嘛。那么此时的mpu6050传感器z轴数据就变得不是那么准确了,那我们用不准确的数据解算出来的数据自然也是有问题的。

3.温度变化会影响MEMS传感器的物理性质,如材料的膨胀系数、弹性模量、粘附力等,这些变化会导致传感器的零点漂移和灵敏度变化,进而影响传感器的测量准确性。(不可避免的)

解决办法

1.零漂可以通过漂移的差值来补偿。原理就是一开始mpu6050先测出1秒内的六轴数据的漂移平均值(此过程mpu6050是静止状态)并记录下来,然后此后再读取六轴数据时,把每个轴的数据减去漂移平均值就是约等于此时的六轴数据了。

#include "ti_msp_dl_config.h"
#include "drv_oled.h"
#include "delay.h"
#include "mympu6050.h"

//mpu6050读取出来的原始数据
int16_t ax_Primitve,ay_Primitve,az_Primitve,gx_Primitve,gy_Primitve,gz_Primitve;

//1秒内得到的误差偏移量平均值//取双精度浮点型小数
float MPU6050ERROR[6]={0.00f,0.00f,0.00f,0.00f,0.00f,0.00f};

#define DELAY1 (8000000)

#define MPU6050_ADDRESS		0x68

//指定地址写
//EquiAddr从机未移位的原7位地址
//RegAddr从机寄存器的地址
//Byte要写入的字节
//static
void IIC_WriteByte(uint8_t EquiAddr,uint8_t RegAddr,uint8_t Byte)
{		
	
	uint8_t buff[2]={RegAddr,Byte};
	
	DL_I2C_fillControllerTXFIFO(I2C1,buff,2);
	
	while(!(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_IDLE));
	DL_I2C_startControllerTransfer(I2C1,EquiAddr,DL_I2C_CONTROLLER_DIRECTION_TX,2);
	while(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_BUSY_BUS);		
	
}

//指定地址连续写
//EquiAddr从机未移位的原7位地址
//RegAddr从机寄存器的地址
//GetArray要发送数据存放的数组
//len数据长度

void IIC_ScanWrite(uint8_t EquiAddr,uint8_t RegAddr,uint8_t *GetArray,uint16_t len)
{
	uint8_t array[256];
	array[0]=RegAddr;
	for(uint16_t i=0;i<len;i++)
	{array[i+1]=GetArray[i];}
	
	//填充主机发送缓存区
	DL_I2C_fillControllerTXFIFO(I2C1,array,len+1);
	while(!(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_IDLE));
	DL_I2C_startControllerTransfer(I2C1,EquiAddr,DL_I2C_CONTROLLER_DIRECTION_TX,len+1);
	//主机开始发送,指定IIC外设,从机地址,读写方向(写方向),数据长度(包含寄存器地址)
	while(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_BUSY_BUS);
		
}

//指定地址读(读取一个字节)
//EquiAddr从机未移位的原7位地址
//RegAddr从机寄存器的地址
uint8_t IIC_ReadByte(uint8_t EquiAddr,uint8_t RegAddr)
{
	//填充主机发送缓存区
	DL_I2C_fillControllerTXFIFO(I2C1,&RegAddr,1);
	
	// 确保I2C控制器处于空闲状态
    while(!(DL_I2C_getControllerStatus(I2C1) & DL_I2C_CONTROLLER_STATUS_IDLE));
	
	//主机开始发送,指定IIC外设,从机地址,读写方向(写方向),数据长度(包含寄存器地址)
	DL_I2C_startControllerTransfer(I2C1,EquiAddr,DL_I2C_CONTROLLER_DIRECTION_TX,1);
	
	while(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_BUSY_BUS);
	
	while(!(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_IDLE));
	//主机开始接收,指定IIC外设,从机地址,读写方向(读方向),要读取的数据长度
	DL_I2C_startControllerTransfer(I2C1,EquiAddr,DL_I2C_CONTROLLER_DIRECTION_RX,1);

	while(DL_I2C_isControllerRXFIFOEmpty(I2C1));
	
	return DL_I2C_receiveControllerData(I2C1);
}

//指定地址连续读
//EquiAddr从机未移位的原7位地址
//RegAddr从机寄存器的地址
//*GetArray接收数据的数组
//len接收数据的数组长度
void IIC_ScanRead(uint8_t EquiAddr,uint8_t RegAddr,uint8_t *GetArray,uint8_t len)
{
//	if(len==0)return;
//	//填充主机发送缓存区
	DL_I2C_fillControllerTXFIFO(I2C1,&RegAddr,1);
	while(!(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_IDLE));
	//主机开始发送,指定IIC外设,从机地址,读写方向(写方向),数据长度(包含寄存器地址)
	DL_I2C_startControllerTransfer(I2C1,EquiAddr,DL_I2C_CONTROLLER_DIRECTION_TX,1);
	
	while(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_BUSY_BUS);
	
	while(!(DL_I2C_getControllerStatus(I2C1)&DL_I2C_CONTROLLER_STATUS_IDLE));
	
	DL_I2C_startControllerTransfer(I2C1,EquiAddr,DL_I2C_CONTROLLER_DIRECTION_RX,len);
	//主机开始接收,指定IIC外设,从机地址,读写方向(读方向),要读取的数据长度
	while(len--)
	{
		while(DL_I2C_isControllerRXFIFOEmpty(I2C1));
		*(GetArray++)=DL_I2C_receiveControllerData(I2C1);		
	}
}




//往MPU6050指定寄存器写入数据
void MPU6050_WriteByte(uint8_t RegAddr,uint8_t Data)
{
	IIC_WriteByte(MPU6050_ADDRESS,RegAddr,Data);
}

//往MPU6050指定寄存器连续写入数据
void MPU6050_scanWrite(uint8_t RegAddr,uint8_t *Data,uint8_t len)
{
	IIC_ScanWrite(MPU6050_ADDRESS,RegAddr,Data,len);
}

//从寄存器读取一个字节
uint8_t MPU6050_ReadByte(uint8_t RegAddr)
{
return IIC_ReadByte(MPU6050_ADDRESS,RegAddr);
}
//从MPU6050指定起始寄存器读取数据
void MPU6050_ScanRead(uint8_t RegAddr,uint8_t *Data,uint8_t len)
{
	IIC_ScanRead(MPU6050_ADDRESS,RegAddr,Data,len);
}
//MPU6050初始化
void MPU6050_Init(void)
{
//	//mpu6050EN
//	DL_GPIO_setPins(GPIOB,DL_GPIO_PIN_1);	
		
	MPU6050_WriteByte(MPU6050_PWR_MGMT_1,0x01);
	delay_cycles(DELAY1);
	
	MPU6050_WriteByte(MPU6050_PWR_MGMT_2, 0x00);
	MPU6050_WriteByte(MPU6050_SMPLRT_DIV, 0x09);
	MPU6050_WriteByte(MPU6050_CONFIG, 0x06);
	MPU6050_WriteByte(MPU6050_GYRO_CONFIG, 0x18);
	MPU6050_WriteByte(MPU6050_ACCEL_CONFIG, 0x18);
	
	
//	uint8_t wake[6]={0x09,0x06,0x18,0x18,0x01,0x00};
//	MPU6050_scanWrite(MPU6050_PWR_MGMT_1,&wake[4],2);
//	MPU6050_scanWrite(MPU6050_SMPLRT_DIV,&wake[0],4);

	delay_cycles(DELAY1);
}

//获取MPU6050的ID
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadByte(MPU6050_WHO_AM_I);
}

//获取MPU6050的六轴数据
void MPU6050_GetData(int16_t *ACCX,int16_t *ACCY,int16_t *ACCZ,int16_t *GYROX,int16_t *GYROY,int16_t *GYROZ)
{
	uint8_t GetArray[12];
	MPU6050_ScanRead(MPU6050_ACCEL_XOUT_H,&GetArray[0],6);
	MPU6050_ScanRead(MPU6050_GYRO_XOUT_H,&GetArray[6],6);
	
	*ACCX=(GetArray[0]<<8)+GetArray[1];
	*ACCY=(GetArray[2]<<8)+GetArray[3];
	*ACCZ=(GetArray[4]<<8)+GetArray[5];
	*GYROX=(GetArray[6]<<8)+GetArray[7];
	*GYROY=(GetArray[8]<<8)+GetArray[9];
	*GYROZ=(GetArray[10]<<8)+GetArray[11];
	
}

//注:在读取mpu6050偏移量的时候,不能让mpu6050晃动
// 1秒内得到静止状态下的测量的100个误差值
void getDataErrorSum(void) 
{
    for(int i = 0; i < 100; i++) 
		{
        MPU6050_GetData(&ax_Primitve,&ay_Primitve,&az_Primitve,&gx_Primitve,&gy_Primitve,&gz_Primitve);
        MPU6050ERROR[0] += ax_Primitve;
        MPU6050ERROR[1] += ay_Primitve;
        MPU6050ERROR[2] += az_Primitve - 9.8;//重力加速度
        MPU6050ERROR[3] += gx_Primitve;
        MPU6050ERROR[4] += gy_Primitve;
        MPU6050ERROR[5] +=gz_Primitve;
        delay_ms(10);//10ms刷新一次
		}
}

// 将100个误差值取均值,得到对应的偏移误差
void getDataError(void) 
	{
    for(int i = 0; i < 6; i++) 
	{
        MPU6050ERROR[i] /= 100.0;
    }
}

// 得到滤波之后的数值
void dataGetAndFilter(void) 
{
	MPU6050_GetData(&ax_Primitve,&ay_Primitve,&az_Primitve,&gx_Primitve,&gy_Primitve,&gz_Primitve);
	
	ax_Primitve=ax_Primitve-MPU6050ERROR[0];
	ay_Primitve=ax_Primitve-MPU6050ERROR[1];
	az_Primitve=ax_Primitve-MPU6050ERROR[2];
	gx_Primitve=ax_Primitve-MPU6050ERROR[3];
	gy_Primitve=ax_Primitve-MPU6050ERROR[4];
	gz_Primitve=ax_Primitve-MPU6050ERROR[5];
} 


getDataErrorSum函数

这个函数的目的是在传感器静止时收集数据,以便计算误差的总和。这样做的原因是,在静止状态下,加速度计应该只测量到重力加速度,而陀螺仪应该读到接近于零的值(理想情况下)。

MPU6050_GetData 函数被用来读取原始的传感器数据,然后累加到 MPU6050ERROR 数组的相应元素中。Z轴加速度数据在累加前减去9.8,这是因为在静止状态下,Z轴应该只测量到地球的重力加速度。

getDataError 函数

这个函数通过将 getDataErrorSum 函数收集的误差总和除以数据点的数量(在这个例子中是100),来计算误差的平均值。这个平均误差可以视为传感器在静止状态下的偏移误差,通常用于后续的数据校准。

dataGetAndFilter 函数

这个函数使用前面计算出的平均误差来滤波最新的传感器数据,以消除或减小偏移误差的影响。

在 dataGetAndFilter 函数中,首先调用 MPU6050_GetData 读取最新的传感器数据,然后从每个通道的数据中减去对应的偏移误差,得到滤波后的数据。

这种简单的偏移补偿方法适用于静态误差的校正,但在动态条件下可能不够准确。在实际应用中,可能需要更复杂的滤波算法,如卡尔曼滤波器,来提供更准确的数据融合和噪声抑制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2037006.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ppt中添加页码(幻灯片编号)及问题解决方案

在幻灯片母版中&#xff0c;选择插入 幻灯片编号 右下角显示幻灯片编号 问题一&#xff1a;母版中没有显示编号 原因可能是母版版式中没有设置显示&#xff0c;勾选即可。 问题二&#xff1a;子母版中没有显示幻灯片 将母版中的编号复制到子母版中。 问题三&#xff1a;应用…

Element-UI自学实践

概述 Element-UI 是由饿了么前端团队推出的一款基于 Vue.js 2.0 的桌面端 UI 组件库。它为开发者提供了一套完整、易用、美观的组件解决方案&#xff0c;极大地提升了前端开发的效率和质量。本文为自学实践记录&#xff0c;详细内容见 &#x1f4da; ElementUI官网 1. 基础组…

2024年7月文章一览

2024年7月编程人总共更新了5篇文章&#xff1a; 1.2024年6月文章一览 2.《Programming from the Ground Up》阅读笔记&#xff1a;p19-p48 3.《Programming from the Ground Up》阅读笔记&#xff1a;p49-p74 4.《Programming from the Ground Up》阅读笔记&#xff1a;p75…

深入理解Kafka核心设计与实践原理_03

深入理解Kafka核心设计与实践原理_03 03_消费者3.1消费者与消费者组3.2客户端开发3.2.1 必要的参数配置3.2.2 订阅主题与分区 草稿 03_消费者 与生产者对应的是消费者&#xff0c;应用程序可以通过KafkaConsumer来订阅主题&#xff0c;并从订阅的主题中拉取消息。不过在使用Ka…

Redis17-服务端优化

目录 持久化配置 慢查询 什么是慢查询 如何查看慢查询 命令及安全配置 内存配置 集群优化 持久化配置 Redis的持久化虽然可以保证数据安全&#xff0c;但也会带来很多额外的开销&#xff0c;因此持久化请遵循下列建议&#xff1a; 用来做缓存的Redis实例尽量不要开启持…

springboot项目打包jar 并打包为exe启动

springboot项目打包jar 并打包为exe启动&#xff08;在无jdk环境下运行&#xff09; 环境 SpringBoot Windows IDEA 实现 1.springboot打包为可执行jar&#xff08;这里使用maven install&#xff09; maven工具栏选择项目->Plugins ->install 注&#xff1a;如果…

Golang | Leetcode Golang题解之第332题重新安排行程

题目&#xff1a; 题解&#xff1a; func findItinerary(tickets [][]string) []string {var (m map[string][]string{}res []string)for _, ticket : range tickets {src, dst : ticket[0], ticket[1]m[src] append(m[src], dst)}for key : range m {sort.Strings(m[key])…

python对接vertx中踩的坑

需求 因为我们的硬件sdk只提供了python的版本&#xff0c;故需要python作为采集端来获取数据&#xff0c;然后将数据发送给java作为数据中心处理。 分析 这里就涉及到跨语言跨进程的数据的中转。有以下的几种解决方法 tcp:基于tcp自己拆包粘包&#xff0c;做心跳。一看就pa…

PV 与 PVC 状态迁移

文章目录 一、概述1、PV2、PVC 二、状态变化三、实例1、单独创建 PV1.1、创建并应用 PV1.2、查看刚创建的 PV 状态 2、单独创建 PVC2.1、创建并应用 PV2.2、查看刚创建的 PVC 状态 3、等待绑定4、删除 PV4.1、查看 PV&#xff0c;PVC 状态4.2、真正删除 PV4.3、查看PV PVC 状态…

PaLM-E: An Embodied Multimodal Language Model

发表时间&#xff1a;arXiv 6 Mar 2023 作者单位&#xff1a;Robotics at Google Motivation&#xff1a;大型语言模型已被证明可以执行复杂的任务。然而&#xff0c;在现实世界中启用一般推理&#xff0c;例如对于机器人问题&#xff0c;提出了落地的挑战。 解决方法&#…

探索全新AI编码代理框架:Agent Zero

引言 在科技的不断进步中&#xff0c;人工智能&#xff08;AI&#xff09;正越来越多地融入我们的日常生活。今天&#xff0c;我要为大家介绍一款全新的AI编码代理框架——Agent Zero。这款框架不仅可以自动化处理编码任务&#xff0c;还能操作文本、应用程序前端等&#xff0…

【微信小程序】页面导航和导航传参

什么是页面导航 小程序中实现页面导航的两种方式 1.声明式导航 (1). 导航到 tabBar 页面 (2). 导航到非 tabBar 页面 (3).后退导航 2.编程式导航 (1).导航到 tabBar 页面 示例代码如下&#xff1a; (2).导航到非 tabBar 页面 示例代码如下&#xff1a; (3).后退导航 示例…

WSL2Linux 子系统(十)

WSL 中启用图形界面 上一篇文章 《WSL2Linux 子系统(九)》 讲解详细讲 WSL&#xff08;Windows Subsystem for Linux&#xff09;解几种常见硬盘挂载方法。本篇讲述 WSL 显示画面的几种方法。WSL 本身不直接支持图形界面&#xff0c;在 WSL 环境中播放视频画面需要额外处理或者…

进程的执行和进程的结束

exec函数族&#xff1a; 作用&#xff1a;可以在一个程序中启动另外一个功能,用新的进程段替换当前进程的段. exec函数族用于替换当前进程的映像&#xff0c;允许程序在运行时加载并执行新的程序代码。 execl: l:list path:要执行的文件的路径 arg:要执行的文件的参数&a…

C#利用ffmpeg和opencv进行视频的解码播放

目录 说明 效果 项目 代码 下载 说明 利用周杰大佬的开源项目 Sdcb.FFmpeg 项目地址&#xff1a;https://github.com/sdcb/Sdcb.FFmpeg/ 效果 C#利用ffmpeg和opencv进行视频的解码播放 项目 代码 using OpenCvSharp; using Sdcb.FFmpeg.Codecs; using Sdcb.FFmpeg.F…

【SpringBoot】【autopoi】java生成word,基于模版生成(文本、图片、表格)

基于模版生成word 1、引入maven2、word模版编写3、java代码4、效果 AutoPoi的主要特点 参考文献 https://help.jeecg.com/autopoi/autopoi/prequel/test.html 1.设计精巧,使用简单 2.接口丰富,扩展简单 3.默认值多,write less do more 4.spring mvc支持,web导出可以简单明了 1、…

【大模型从入门到精通17】openAI API 构建和评估大型语言模型(LLM)应用5

这里写目录标题 理论问题&#xff1a;实践问题&#xff1a;理论实践 理论问题&#xff1a; 1.描述评估LLM应用程序输出的重要性&#xff0c;并提及至少三个维度&#xff0c;这些输出应该在这几个维度上被评估。 2.解释在评估LLM应用程序时开发稳健的性能指标的作用&#xff0c…

uniapp多图上传uni.chooseImage上传照片uni.uploadFile,默认上传9张图

uniapp多图上传uni.chooseImage上传照片uni.uploadFile 代码示例&#xff1a; /**上传照片 多图*/getImage() {uni.chooseImage({count: 9, //默认9sizeType: [original, compressed], //可以指定是原图还是压缩图&#xff0c;默认二者都有sourceType: [album], //从相册选择/…

【论文理解】Pixel-level Extrinsic Calibration LiDAR and Camera

Pixel-level Extrinsic Self Calibration of High Resolution LiDAR and Camera in Targetless Environments 无目标环境中高分辨率激光雷达和相机的像素级外参估计 摘要 Our approach does not require checkerboards but can achieve pixel-level accuracy by aligning na…

数据结构8.13

作业&#xff1a;链栈&#xff0c;自己实现一遍&#xff0c;但是节点存储不是整数&#xff0c;存储学生信息&#xff08;年龄&#xff0c;分数&#xff0c;姓名&#xff09;三级引用。 1、建立学生信息结构体&#xff0c;将data改为学生信息结构体类型。 #include <myhead…