简介:MPU6050六轴传感器数据实时显示资源,基于STM32ZET6实现三轴加速度与三轴角速度的采集,并通过LCD实时展示,内置配套上位机,适合嵌入式初学者、物联网开发者以及无人机、机器人等姿态检测项目设计者。压缩包共94个文件,以C源码与H头文件为主(43个h、40个c),附带STM32完整工程、hex烧录文件、匿名四轴上位机以及说明文档,整体仅2.4MB,轻量易下载。当前已有817人学习下载。通过该项目可完整掌握I2C驱动MPU6050的方式、加速度与角速度的数据读取格式、互补滤波姿态解算思路、LCD屏显驱动,以及串口/上位机通信联动,目录划分清晰,方便按模块理解与二次开发。项目源码注释到位,适合对照学习与移植。
1. 为什么MPU6050这类六轴传感器,值得把数据链路完整跑一遍
MPU6050集成了三轴加速度计和三轴陀螺仪,出货十几年,依然是平衡车、低成本四轴、机器人关节姿态检测的入门首选。它的难点不在传感器本身,而在“读不到有效数据”和“读到数据不知道对不对”这两件事上。这个项目把STM32F103ZET6、MPU6050、TFTLCD和匿名四轴上位机组在一起,正好是一条完整的采集-解算-显示-上传链路,对想做姿态控制的人来说,是一个可以直接跑通的参考实现。它能帮你理清三件事:一是I2C寄存器怎么配,二是原始数据怎么变成角度,三是数据如何通过串口送到上位机实时绘制波形。也适合做物联网或智能硬件开发的人快速验证传感器选型方案。
2. MPU6050的I2C寄存器配置与STM32驱动实现
2.1 I2C从设备地址与总线时序细节
MPU6050的7位I2C地址由AD0引脚决定,电路接地时地址是0x68,接高电平则是0x69。这个地址参与总线上的每一次读写:写寄存器时发送地址+写位(0xD0),读数据时发送地址+读位(0xD1)。如果PCB上AD0正好被电阻拉起,而代码却写死0x68,I2C通信会一直得不到ACK,读出来的数据自然全是0xFF或0。所以拿到任何一块MPU6050模块,第一步不是写驱动,而是确认AD0引脚电平,或者在代码里把设备地址做成宏,方便替换。
I2C总线上还有两个细节直接影响稳定性:上拉电阻和速率。MPU6050数据手册约定标准模式100kHz、快速模式400kHz,STM32的IO配置成开漏输出后必须外接4.7kΩ上拉电阻到3.3V。很多人在STM32上配置了复用开漏模式之后,忘了确认外部上拉,结果SCL/SDA一直拉不高。这个工程的HARDWARE/MPU6050目录下用的是软件模拟I2C,没有走STM32硬件I2C外设。原因也简单:STM32F1系列的硬件I2C在使用上对中断优先级和时序要求更苛刻,模拟I2C则更容易控制时序,也方便用逻辑分析仪抓波形定位问题。
2.2 关键寄存器配置与初始化代码
初始化MPU6050之前,先列出几个必配寄存器,方便对照后续代码。PWR_MGMT_1是电源管理寄存器,控制复位和睡眠;SMPLRT_DIV和CONFIG决定采样率与滤波带宽;GYRO_CONFIG和ACCEL_CONFIG决定量程,直接影响后续的灵敏度换算。
| 寄存器地址 | 名称 | 功能说明 | 常用值 |
|---|---|---|---|
| 0x6B | PWR_MGMT_1 | 电源管理、复位、睡眠控制 | 0x00 唤醒 |
| 0x19 | SMPLRT_DIV | 采样率分频 | 0x07 得到1kHz采样率 |
| 0x1A | CONFIG | 数字低通滤波器配置 | 0x06 约5Hz带宽 |
| 0x1B | GYRO_CONFIG | 陀螺仪量程选择 | 0x18 ±2000°/s |
| 0x1C | ACCEL_CONFIG | 加速度计量程选择 | 0x10 ±8g |
对应初始化代码如下,这里用的是标准外设库写法,MPU6050_WriteReg内部完成模拟I2C的起始、地址、寄存器地址和数据发送。
void MPU6050_Init(void) { IIC_Init(); MPU6050_WriteReg(PWR_MGMT_1, 0x80); // 复位整个芯片 delay_ms(100); MPU6050_WriteReg(PWR_MGMT_1, 0x00); // 退出睡眠,选择内部时钟 MPU6050_WriteReg(SMPLRT_DIV, 0x07); // 采样率 = 8000Hz / (1+7) = 1kHz MPU6050_WriteReg(CONFIG, 0x06); // 开启低通滤波,约5Hz带宽 MPU6050_WriteReg(GYRO_CONFIG, 0x18); // 陀螺仪量程 ±2000°/s,灵敏度16.4 LSB/(°/s) MPU6050_WriteReg(ACCEL_CONFIG, 0x10); // 加速度计量程 ±8g,灵敏度4096 LSB/g }第一行写0x80是让芯片内部所有寄存器回到复位值,避免上一次运行留下的配置影响这次工作。写完后必须留出足够时间,一般延时100ms再写0x00唤醒。如果跳过复位直接工作,芯片可能还处于睡眠状态,后面读加速度计数据会一直是0。CONFIG写0x06并不代表截止频率精确等于5Hz,实际值会随温度有一点点漂移,但对LCD显示和上位机观察足够;如果后面要做姿态控制,这个带宽需要适当提高,否则控制环路的相位滞后会很严重。
注意:模拟I2C的GPIO不要选在JTAG复用引脚上,调试器和I2C同时操作时会出现不可预期的干扰,这是最小系统板上很常见的坑。
2.3 连续读取14字节原始数据并换算物理量
MPU6050把加速度计、温度、陀螺仪的寄存器连续排布在0x3B到0x48,所以一次突发读取14个字节是最常用的方式。读取时只需要一个I2C起始信号,写完寄存器地址后连续收数,代码实现很短。
void MPU6050_ReadData(int16_t* accel, int16_t* gyro) { uint8_t buf[14]; MPU6050_ReadRegs(0x3B, buf, 14); accel[0] = (int16_t)((buf[0] << 8) | buf[1]); accel[1] = (int16_t)((buf[2] << 8) | buf[3]); accel[2] = (int16_t)((buf[4] << 8) | buf[5]); // buf[6]~buf[7] 是温度寄存器,本项目不处理 gyro[0] = (int16_t)((buf[8] << 8) | buf[9]); gyro[1] = (int16_t)((buf[10] << 8) | buf[11]); gyro[2] = (int16_t)((buf[12] << 8) | buf[13]); }读出来的原始值是带符号的16位整数,范围由量程决定。以±2000°/s的陀螺仪量程为例,每个单位代表16.4°/s,真实角速度是raw/16.4;加速度计在±8g下,每个单位代表1/4096g。换算时要避免整数除法,正确做法是先转成float再除。这套数据理论上输出率能到1kHz,但LCD刷新和串口往往到不了这个频率,所以实际循环里会加延时,把输出频率降到20Hz到50Hz,上位机波形看起来才平稳。
3. 姿态解算:互补滤波与四元数更新的工程实现
3.1 加速度计测角和陀螺仪积分的互补逻辑
加速度计静止时测到的是重力加速度的分量。板子水平放置时,Z轴读到约1g,X和Y约0g;倾斜后重力被分解到三个轴上,用atan2f(Y, Z)可以算出绕X轴的roll角,用atan2f(-X, sqrt(YY+ZZ))算出绕Y轴的pitch角。这个方法只依赖当前重力方向,不依赖时间,静止时是准确的。问题在于物体一旦加减速运动,加速度计读到的不再只是重力,角度会跟着运动方向跳动甚至完全错误。
陀螺仪输出的角速度不会受运动加速度干扰,但它测的是变化率,必须积分才有角度。积分一次就会引入零漂,如果静止时陀螺仪有0.1°/s的零偏,积分10秒就产生1°误差。互补滤波的核心是用一个系数α把两个来源结合:对陀螺仪积分的角度做高通处理,保留快速变化;对加速度计计算的角度做低通处理,约束长期漂移。α越接近1越信任陀螺仪,动态响应好但收敛慢;α越小越信任加速度计,角度稳但动态迟钝。一般在0.95到0.98之间试。
3.2 能直接落地的互补滤波代码
工程里的main.c使用的是一阶互补滤波实现,下面这个写法把计算和换算放在一起,方便在LCD和串口里同时输出原始值和角度值。
#define FILTER_ALPHA 0.96f #define RAD_TO_DEG 57.29578f float roll, pitch; void Attitude_Update(float ax, float ay, float az, float gyro_x, float gyro_y, float dt) { float roll_acc, pitch_acc; roll_acc = atan2f(ay, az) * RAD_TO_DEG; pitch_acc = atan2f(-ax, sqrtf(ay * ay + az * az)) * RAD_TO_DEG; roll = FILTER_ALPHA * (roll + gyro_x * dt) + (1 - FILTER_ALPHA) * roll_acc; pitch = FILTER_ALPHA * (pitch + gyro_y * dt) + (1 - FILTER_ALPHA) * pitch_acc; }使用前要先按量程把原始值换算成度每秒。如果陀螺仪量程是±2000°/s,传入的gyro_x应该是raw/16.4,而不是寄存器原值。dt是相邻两次调用的时间差,单位秒,一般用一个毫秒时间戳算出来。代码里的atan2f是四象限反正切,和atan的区别在于,当分母是0时atan会除零,而atan2f能正确返回±90°,所以姿态角计算必须用atan2f。这个简化公式在roll或pitch接近±90°时会遇到奇点,做翻转类应用就得用四元数。
3.3 Mahony四元数更新的可替换方案
一阶互补滤波覆盖不了yaw角,因为Z轴上的航向角无法用重力加速度直接测量,只能靠陀螺仪积分。实际项目里更常用Mahony算法,它属于显式互补滤波,用加速度计修正陀螺仪的横滚和俯仰误差,再更新四元数,还可以带陀螺仪零偏估计。相比扩展卡尔曼滤波要反复调Q和R矩阵,Mahony在STM32F103上的计算量很小,单次更新不到100μs,是性价比最高的方案。
在没有磁力计的场景下,yaw方向完全依赖积分,四元数更新会退化成只修正roll和pitch的形式。如果你想把解算放进MPU6050内部,也可以加载DMP固件,由传感器自己输出四元数,主控只负责读取。DMP的好处是主控负载更低,缺点是需要额外初始化流程,且寄存器读写时序要求更严格。用USMART组件注册对应的DMP读取函数后,可以在不重新编译的情况下切换两种解算方式对比效果。
| 姿态解算方案 | 主控开销 | 动态性能 | 调试难度 |
|---|---|---|---|
| 一阶互补滤波 | 低 | 一般 | 低 |
| Mahony四元数 | 低 | 较好 | 中 |
| 扩展卡尔曼滤波 | 高 | 好 | 高 |
| MPU6050内部DMP | 最低 | 稳定 | 中 |
4. LCD实时刷新与匿名四轴上位机的串口对接
4.1 FSMC驱动的LCD接入方式
HARDWARE/LCD目录下是TFTLCD驱动,常见控制器有ILI9341或ILI9325。这类屏幕数据总线接在STM32的FSMC接口上,通过地址线A2区分命令和数据:地址0x60000000写命令,0x60020000写数据。FSMC的好处是LCD刷屏不需要CPU逐位翻转IO,CPU往内存地址写数据,时序由硬件完成,整屏刷新也能控制在几十毫秒内。
初始化代码精简后如下,主要通过寄存器命令配置扫描方向、像素格式和电源参数。
void LCD_Init(void) { // 使能FSMC和GPIO时钟,并配置为复用推挽输出 LCD_WriteReg(0xCF, 0x0000C100); LCD_WriteReg(0xED, 0x01640303); LCD_WriteReg(0xE8, 0x00010303); LCD_WriteReg(0xC0, 0x262626); LCD_WriteReg(0x36, 0x00A8); // RGB扫描方向 LCD_WriteReg(0x3A, 0x0055); // 16位像素格式 LCD_WriteReg(0x11, 0); // 退出睡眠模式 delay_ms(120); }每款屏的控制器不完全一样,命令值不能从头抄到尾,需要看屏的ID或驱动手册确认。0x36是扫描方向寄存器,决定文字和图像是正向还是倒向显示,接反了画面就是镜像的。实时显示传感器数据时,我一般会固定一块刷新区域,比如只刷新数值所在的行,先用背景色矩形清掉旧数据,再写新值,避免整屏重绘的闪烁。数据刷新频率和LCD刷新率要匹配,20Hz到30Hz就足够看到连续变化。
4.2 匿名四轴上位机协议帧格式
zip里带的匿名四轴上位机通过串口接收数据包,协议帧结构非常固定,解析起来也简单。以下是姿态数据功能字0x01的数据帧格式,数据区由多个int16字段组成,高位在前。
| 字节位置 | 内容 | 说明 |
|---|---|---|
| 0~1 | 0xAA 0xAA | 帧头 |
| 2 | 0x01 | 功能字,0x01对应姿态数据 |
| 3 | 0x1C | 数据区长度,这里是28字节 |
| 4~27 | int16数组 | 加速度、角速度、角度等数据体 |
| 28 | 累加和 | 前面所有字节累加,溢出丢弃 |
这套协议本身就是匿名官方上位机使用的,不需要自己造上位机协议。实际使用时要特别注意两点:串口波特率默认115200,上位机软件运行在Windows上,通过虚拟串口接入。如果帧头之前出现了调试打印信息,或者波特率不匹配,上位机就会一直停在连接状态。先把MPU6050的数据停了,在main循环里只发一组固定角度,确认上位机能画出曲线再接入真实传感器,这样能把通信问题和解算问题分开处理。
注意:匿名上位机的版本较多,V2.6和V4.x在功能字定义上有差异,使用前先打开随包的协议文档确认长度字段含义,避免按旧版本格式解析新版本数据。
4.3 串口数据发送函数实现
发送函数核心是拆字节和累加校验。下面这段代码把10个int16数值按高字节在前打包,同时计算校验和。
void Send_Ano(int16_t* data, uint8_t len) { uint8_t sum = 0; uint8_t i; USART_SendByte(0xAA); USART_SendByte(0xAA); USART_SendByte(0x01); USART_SendByte(len * 2); for (i = 0; i < len; i++) { USART_SendByte(data[i] >> 8); USART_SendByte(data[i] & 0xFF); sum += data[i] >> 8; sum += data[i] & 0xFF; } USART_SendByte(sum); }调用时传入一个长度为10的int16数组,前3个放加速度原始值,后3个放陀螺仪原始值,再放roll、pitch、yaw,剩余位置可以放温度或状态字,正好填满匿名上位机的显示窗口。校验和只对数据区累加,帧头和长度字节不参与,这是匿名上位机V2.6之后形成的约定。如果发现数据偶尔丢帧,先检查串口是否在115200波特率下稳定,再降发送频率。20Hz到50Hz足够观察,过高反而会让上位机绘图线程跟不上。
5. USMART串口调试与MPU6050数据异常定位技巧
5.1 USMART让函数调用变成串口命令
USMART是工程里的调试组件,把函数调用映射成串口命令。调试时不需重新编译烧录,直接在上位机串口助手输入“MPU6050_Init()”就能重新初始化传感器,输入“LCD_Clear(0xFFFF)”验证屏幕。注册方式就是往usmart_config.c的映射表里加一行函数名和函数指针。
struct _m_usmart_nametab usmart_nametab[] = { #if USMART_USE_WFUNCTIONS == 1 (void*)MPU6050_Init, "MPU6050_Init", (void*)MPU6050_ReadData, "MPU6050_ReadData", (void*)Send_Ano, "Send_Ano", #endif };USMART解析串口收到的字符串,通过函数名查表执行。参数个数和函数原型绑定在usmart_str.c里,带参数的函数也能调,比如“MPU6050_WriteReg(0x6B, 0x00)”可以直接操作寄存器。调试时可以一边改角度解算参数,一边从串口看结果,不用反复刷flash。注意别注册带指针参数的自定义函数,USMART会把地址数值当参数传进去,容易跑到未定义状态。
5.2 数据异常排查表与零偏校准
数据异常基本集中在几个固定原因,按硬件、驱动、算法顺序排查,效率最高。
| 现象 | 可能原因 | 优先处理 |
|---|---|---|
| 读到的数据全为0 | 芯片处于睡眠状态 | 检查PWR_MGMT_1是否写0x00 |
| 数据恒为0xFF或ACK异常 | AD0地址与代码不符 | 测量AD0引脚电平 |
| 角度持续漂移 | 陀螺仪零偏未校准 | 静止采样取平均作为零偏 |
| LCD有数字但上位机空白 | 帧头或波特率不匹配 | 先用固定数据包测试 |
| 角度随运动剧烈跳动 | 数字低通带宽过高 | 调CONFIG寄存器或加大α |
最后一个技巧是零偏校准。上电后不要动板子,连续采集1000次陀螺仪原始值求平均,之后每次读取先减掉这个零偏。这比写复杂的滤波算法更直接,能把静态漂移从每分钟几度降到几十秒不到1度。加速度计也可以用同样思路:静止状态下计算三轴模长sqrt(ax²+ay²+az²),如果和1g偏差超过20mg,需要做六面标定。拿这个工程做验证时,我建议按USMART调原始数据、LCD调显示、上位机对比波形的顺序推进,哪一步断了就回到对应寄存器去查,而不是直接怀疑算法。
本文还有配套的精品资源,点击获取