简介:面向STM32四旋翼无人机开发爱好者,二阶段项目资源专门针对MPU6050姿态解算与匿名上位机串口通信场景,涵盖传感器初始化、原始数据预处理、互补滤波姿态解算、UART协议封装及电机PWM控制等完整链路。压缩包共1033个文件,以C源码和头文件为主,另含IAR/Keil工程配置、链接脚本、Hex固件、ARM官方数学库及匿名上位机工具,整体约28.44MB,可直接用于代码研读或工程参考。目前已有12945人学习下载,热度较高。资源附带工程结构清晰,对理解卡尔曼/互补滤波的工程实现、串口波特率与帧格式设计、STM32中断与定时器配置都有实际帮助,能有效降低飞控开发门槛,适合需要对照调试和进阶学习的嵌入式开发者。 做四旋翼无人机,姿态解算是绕不过去的一关。飞机能不能稳,悬停会不会晃,转弯顺不顺,反馈到代码层面就是姿态这个数算得准不准、更新得快不快。这一篇是系列第二篇,接着上一篇的硬件搭建往下走,把STM32F103C8T6主控和MPU6050六轴传感器的数据链路打通:用I2C读取加速度计和陀螺仪的原始数据,做姿态解算得到roll、pitch、yaw,最后把姿态值通过串口发给匿名上位机,在电脑上实时看到飞机姿态的3D变化。文章里贴的代码是我实际跑通的完整版本,包含MPU6050读取、四元数姿态解算、匿名上位机协议打包三个模块,去掉了不必要的工程结构,可以直接照着移植。
1. 整体设计思路:传感器选型与通讯链路
1.1 为什么选择了MPU6050
先说说为什么选MPU6050。四旋翼上要做姿态闭环控制,最常见的选择就是六轴IMU,也就是三轴加速度计加三轴陀螺仪的组合。MPU6050虽然是2013年前后的老片子,但现在依然活跃在各种入门飞控和毕业设计里,原因很简单:便宜、够用、资料全。一片模块几块钱,I2C接口两根线就能接,自带数字输出,用STM32标准库或者HAL库都能轻松驱动起来。
对于四旋翼这种应用场景,MPU6050的精度是够用的。加速度计量程默认±2g,陀螺仪量程可以配置到±2000dps,内部还集成了温度传感器和DMP运动处理器。DMP是芯片里固化的姿态解算单元,能直接输出四元数。很多教程会让你用DMP,把官方Motion Driver库一移植就能出数据,省事是真的,但我不推荐在飞控项目里直接用DMP,后面会详细说原因。
1.2 通讯链路与数据流向
整个系统的数据流向是这样的:MPU6050通过I2C把六轴原始数据给STM32,STM32跑姿态解算得到欧拉角,解算结果通过USART1串口发到上位机,上位机绘制姿态球实时显示。
这里有个容易被忽略的点:I2C总线的稳定性。MPU6050的SCL和SDA两根线,如果模块上没有上拉电阻,必须自己在板子上加4.7k上拉到3.3V。我早期做实验的时候直接用杜邦线把模块和单片机连起来,结果I2C读取时好时坏,后来用示波器看波形才发现是上拉问题。另外I2C线尽量短,超过20cm就开始容易受干扰,飞控内部走线足够,但如果要做外部调试,线长了必须降低I2C速率或者加上拉加强,这是很实际的坑。
2. MPU6050数据读取:I2C寄存器操作细节
2.1 初始化配置要点
MPU6050的I2C地址是0x68或者0x69,由AD0引脚的电平决定,绝大多数模块AD0默认接地,所以地址固定是0x68。这个地址特别容易在有多个I2C设备时产生迷惑,我习惯在初始化之后先读WHO_AM_I寄存器(地址0x75)验证一下,返回值固定是0x68,如果读出来不是0x68就说明通讯有问题。
初始化需要配置几个关键寄存器:
- 0x6B(PWR_MGMT_1):解除睡眠模式,写0x00让芯片进入正常工作态。芯片上电默认是睡眠模式,不解除睡眠读出来的数据全是0。
- 0x1A(CONFIG):配置数字低通滤波器DLPF和采样率。我一般配置成0x03,对应的DLPF带宽约44Hz,延迟4.8ms,这样既滤掉高频噪声又不会让数据太滞后。
- 0x19(SMPLRT_DIV):采样率分频。配合DLPF,采样率=1kHz/(1+SMPLRT_DIV)。我常用SMPLRT_DIV=7,采样率125Hz,跟解算频率保持一致。
- 0x1B(GYRO_CONFIG):陀螺仪量程。我配置±2000dps,对应16.4 LSB/(dps),量程大一些虽然分辨率低,但四旋翼机动时角速度比较大,力竭量程才是关键。
- 0x1C(ACCEL_CONFIG):加速度计量程。我配置±2g,对应16384 LSB/g,这个量程下分辨率最高,足够四旋翼使用。
配置的代码片段:
void MPU6050_Init(void) { // 解除睡眠 MPU6050_WriteReg(0x6B, 0x00); // 禁止FIFO,配置DLPF带宽约44Hz MPU6050_WriteReg(0x1A, 0x03); // 采样率分频,125Hz MPU6050_WriteReg(0x19, 0x07); // 陀螺仪量程±2000dps MPU6050_WriteReg(0x1B, 0x18); // 加速度计量程±2g MPU6050_WriteReg(0x1C, 0x00); // 关闭所有中断 MPU6050_WriteReg(0x38, 0x00); }0x1B配置寄存器里,bit4和bit3是FS_SEL,0x18对应的二进制是00011000,也就是FS_SEL=3,表示±2000dps。0x1C默认值就是0x00,对应的AFS_SEL=0,即±2g。所以0x1C那一行其实可写可不写,但写上更清楚,以后改量程也方便。
2.2 连续读取14字节
MPU6050的加速度和陀螺仪数据分别在寄存器地址0x3B和0x43,加上温度传感器数据,从0x3B到0x48一共14个字节。I2C支持连续读操作,一次性把这14个字节读出来是最推荐的做法。这样做有两个好处:一是只发起一次I2C通信,耗时更短;二是避免逐个寄存器读取时,刚好赶上传感器数据更新,导致加速度和陀螺仪的数据不是同一时刻采样的,也就是数据撕裂。
我在项目里用标准库模拟I2C和硬件I2C都试过。硬件I2C效率更高,但STM32F1的硬件I2C用起来要小心Busy标志的问题,很多人就是在这里卡住。我最终选的是硬件I2C正常模式下的连续读取,按下面这个流程走,稳定跑几个月都没问题:
void MPU6050_ReadRaw(int16_t *accel, int16_t *gyro, int16_t *temp) { uint8_t buf[14]; // 从0x3B开始连续读14字节 I2C_ReadBuffer(MPU6050_ADDR, 0x3B, buf, 14); accel[0] = (buf[0] << 8) | buf[1]; // ACCEL_X accel[1] = (buf[2] << 8) | buf[3]; // ACCEL_Y accel[2] = (buf[4] << 8) | buf[5]; // ACCEL_Z *temp = (buf[6] << 8) | buf[7]; // TEMP gyro[0] = (buf[8] << 8) | buf[9]; // GYRO_X gyro[1] = (buf[10] << 8) | buf[11]; // GYRO_Y gyro[2] = (buf[12] << 8) | buf[13]; // GYRO_Z }读取的原始数据要转换成物理量。加速度数据除以16384得到g为单位的值,陀螺仪数据除以16.4得到dps为单位的值。这里有个易错点:很多人把陀螺仪量程配置搞混了,量程是±250dps时灵敏度是131,改成±2000dps后就变成了16.4,转换系数必须跟着改。如果换算后角度值小得离谱,先查查是不是量程和灵敏度不匹配。
3. 姿态解算:为什么用Mahony而不是DMP
3.1 加速度计和陀螺仪各自的缺陷
姿态解算的核心问题,是融合加速度计和陀螺仪的数据。先说清楚为什么不能直接用某一类传感器出姿态。
陀螺仪测量的是角速度,对角速度积分就能得到角度增量。但积分会把零漂误差累积起来,时间越长偏差越大,几分钟躺着不动,姿态角就能飘出去好几度。这就是陀螺仪的积分漂移问题。
加速度计在静止时,通过测量重力加速度在三轴的分量,可以反推出roll和pitch角,不会累积误差。但加速度计扛不住震动,电机的震动和机体的加速度都会混进测量值里,让角度来回跳。只看加速度计,悬停时姿态数据抖得像筛子。
所以必须把两者融合:用陀螺仪做主姿态更新,用加速度计修正陀螺仪的漂移。这就是姿态解算的本质。
3.2 为什么不用芯片自带的DMP
MPU6050内部有DMP,可以直接输出四元数。很多教程都是这么教的,把Motion Driver库移植进去,调用几个API就能拿到数据。我为什么不推荐在飞控项目里用DMP?
第一,DMP的核心算法是固化在芯片里的黑盒,没法修改内部的姿态融合策略,也没办法精细调节修正速度。第二,DMP的输出频率和外部定时采集的同步性可控性差,实际飞控用的是控制周期严格固定的姿态数据,DMP的异步输出处理起来反而麻烦。第三,Motion Driver库代码量不小,占用很多片内Flash和内存,F103C8的库存本来就不宽裕。第四,姿态解算本身是飞控最核心的算法,自己写一遍才能深入理解,后面要加磁力计做yaw修正或者做更高级的卡尔曼滤波,也才有基础。
所以我建议自己写解算算法。有两条路线:一阶互补滤波和Mahony四元数融合。一阶互补滤波实现极其简单,几行代码就能出结果,但动态响应和抗扰性能一般。Mahony算法在开源飞控里很常见,计算量也不大,F103跑200Hz没有问题,效果比一阶互补滤波好得多。这个项目我用Mahony。
3.3 Mahony核心逻辑与代码
Mahony的思路可以这么理解:加速度计测量的重力方向是“观测值”,陀螺仪积分出来的姿态对应的期望重力方向是“预测值”,两者做叉积得到误差。然后把这个误差通过PI控制器修正陀螺仪的角速度,再用修正后的角速度去更新四元数。
这样既保留了陀螺仪短时高精度的动态优势,又通过加速度计持续修正累积漂移。核心代码:
#define sampleFreq 200.0f #define twoKpDef (2.0f * 0.5f) #define twoKiDef (2.0f * 0.0f) float q0 = 1.0f, q1 = 0.0f, q2 = 0.0f, q3 = 0.0f; void Mahony_Update(float gx, float gy, float gz, float ax, float ay, float az) { float halfvx, halfvy, halfvz; float halfex, halfey, halfez; float qa, qb, qc; gx *= 0.0174533f; // 度转弧度 gy *= 0.0174533f; gz *= 0.0174533f; // 由当前四元数推算出重力方向 halfvx = q1*q3 - q0*q2; halfvy = q0*q1 + q2*q3; halfvz = q0*q0 - 0.5f + q3*q3; // 加速度计观测值与推算值叉积 halfex = ay*halfvz - az*halfvy; halfey = az*halfvx - ax*halfvz; halfez = ax*halfvy - ay*halfvx; // PI修正陀螺仪角速度 gx += twoKpDef * halfex; gy += twoKpDef * halfey; gz += twoKpDef * halfez; // 四元数微分方程更新 qa = q0; qb = q1; qc = q2; q0 += (-qb*gx - qc*gy - q3*gz) * (1.0f/sampleFreq) * 0.5f; q1 += ( qa*gx + qc*gz - q3*gy) * (1.0f/sampleFreq) * 0.5f; q2 += ( qa*gy - qb*gz + q3*gx) * (1.0f/sampleFreq) * 0.5f; q3 += ( qa*gz + qb*gy - qc*gx) * (1.0f/sampleFreq) * 0.5f; // 四元数归一化 float norm = sqrtf(q0*q0 + q1*q1 + q2*q2 + q3*q3); q0 /= norm; q1 /= norm; q2 /= norm; q3 /= norm; }注意看代码里的几个关键点。第一,陀螺仪数据要转成弧度每秒,这个很容易漏掉,漏了之后整个解算等于白跑。第二,PI控制器里我用的是0.5f的Kp,Ki设成0。对于静止测试和室内慢速飞行这个参数够用,如果飞机动作幅度大,或者觉得姿态跟随太软、修正太慢,可以把Kp调到1.0甚至2.0试试,Kp太大则高频噪声会放大,姿态会抖。第三,四元数归一化是必须的,四元数不归一化,后面转欧拉角会算出非法的值,完成一次更新就驱散一次发散。
3.4 四元数转欧拉角
四元数虽然计算方便,但人眼没法直观理解。最终控制里面用的和上位机显示的都是欧拉角。转换公式:
float roll = atan2f(2*(q0*q1 + q2*q3), 1 - 2*(q1*q1 + q2*q2)) * 57.29578f; float pitch = asinf(2*(q0*q2 - q3*q1)) * 57.29578f; float yaw = atan2f(2*(q0*q3 + q1*q2), 1 - 2*(q2*q2 + q3*q3)) * 57.29578f;乘57.29578是把弧度转成度。需要注意pitch角的asinf定义域,如果姿态接近±90度,pitch接近±1时可能出现NaN,这在四旋翼的基本动作里很少遇到,但特技飞行动作要注意。另外,欧拉角在pitch=±90度附近会出现万向锁问题,导致yaw和roll的耦合跳变。对于一般的姿态控制,我们控制角度都限制在小范围,问题不大,但如果做全姿态运动,就得考虑用四元数直接做控制,绕开欧拉角。
4. 串口通讯与匿名上位机协议打包
4.1 串口参数与初始化
STM32和上位机的通讯我用USART1,波特率115200,8N1。匿名上位机对波特率的支持很宽松,115200够用,每包姿态数据的数据量也不大。
这里提一个操作习惯:串口的发送和接收我都在中断里处理,发送用DMA。但简单演示项目用阻塞发送就够了,因为一包姿态数据就几十个字节,USART1在115200波特率下,发送一包数据大约2毫秒,而解算和控制周期是5毫秒,只要发送安排在解算完成后,不影响主循环。不过如果以后加数传、加遥控指令,总线冲突会越来越明显,那时候再想办法。我的做法是先跑通,再去优化。
4.2 匿名上位机协议的帧格式
匿名上位机的通讯协议,不同版本细节有差异,我用的是V2.x系列比较通用的格式:
- 帧头:0xAA
- 帧头校验:0xAA
- 功能字:0x01(表示发送主子式的姿态数据)
- 数据长度:0x10(16字节数据区)
- 数据:5个short(roll、pitch、yaw、相对高度、预留)
- 校验和:从帧头到数据区最后一个字节全部累加
需要特别提醒,匿名上位机的协议出了很多个版本,老版本的帧头可能是0xAA 0xFF,数据长度字段的定义方式也不一样。直接照抄网上的代码,如果上位机版本对不上,永远收不到数据。最靠谱的做法是下载上位机的时候顺手把它的协议说明文档一起存下来,核对每个字段的定义。我在这个项目里用的就是上面这个格式,搭配我下载的匿名上位机V2.6,稳定显示没有问题。
数据打包代码:
uint8_t tx_buffer[64]; void Send_Attitude_To_PC(float roll, float pitch, float yaw) { uint8_t i; uint16_t sum = 0; int16_t a = (int16_t)(roll * 100); int16_t b = (int16_t)(pitch * 100); int16_t c = (int16_t)(yaw * 100); tx_buffer[0] = 0xAA; tx_buffer[1] = 0xAA; tx_buffer[2] = 0x01; // 功能字 tx_buffer[3] = 0x10; // 数据长度16字节 tx_buffer[4] = a >> 8; tx_buffer[5] = a & 0xFF; tx_buffer[6] = b >> 8; tx_buffer[7] = b & 0xFF; tx_buffer[8] = c >> 8; tx_buffer[9] = c & 0xFF; tx_buffer[10] = 0x00; tx_buffer[11] = 0x00; // 剩余数据区填0 for (i = 12; i < 19; i++) tx_buffer[i] = 0x00; sum = 0; for (i = 0; i < 19; i++) sum += tx_buffer[i]; tx_buffer[19] = sum & 0xFF; for (i = 0; i < 20; i++) USART1_SendByte(tx_buffer[i]); }注意姿态角扩大100倍再转short,上位机那边才能看到带两位小数的角度值。角度是0.01度,我发送的是乘以100之后的整数,上位机收到再除以100显示。这样既节省带宽,又避免了浮点数在串口协议里传输的对端大小端问题。如果直接把float的四个字节发过去,看起来省事,但上位机侧字节序和浮点表示法只要有一点点不一致,显示出来就是个天文数字。
4.3 主循环任务调度
最后看主循环。整个程序的结构是:定时器中断里以固定频率读取MPU6050原始数据,跑Mahony解算,主循环里把最新算好的欧拉角打包发送给上位机。也可以反过来,解算在定时器中断完成,发送也放在中断里,主循环就做别的事情。我习惯用SysTick定时器做2ms周期调度,解算频率稳定在约200Hz,和Mahony里的sampleFreq保持一致。
实际项目中我观察到,如果解算周期波动,姿态角会有周期性的微弱跳动。尤其当你用while循环加Delay来控频时,一旦主循环里插入耗时操作,采样间隔就会抖动,解算质量直接受影响。所以解算频率必须由定时器保证,不能依赖主循环的速度。这是一个飞控项目里必须养成的好习惯。
5. 调试实录:常见问题与排查技巧
5.1 读取不到MPU6050数据
现象:I2C读出来的原始数据全是0,或者WHO_AM_I读不到0x68。
排查顺序:先查电源,VCC和GND在模块上有没有短接;再查SDA和SCL有没有焊反,这个比想象中常见;然后查上拉电阻,很多模块PCB上已经焊了上拉,但有的模块是没有的,必须外部加4.7kΩ;最后查I2C地址,AD0引脚悬空或者拉高会把地址变成0x69,如果固件里写死0x68就读不到。
我的经验是,MPU6050读不到数据时,首先用逻辑分析仪或者示波器看I2C引脚波形,看ACK位有没有正常拉低。直接看波形能省很多猜测的时间。如果没有示波器,就在初始化后读WHO_AM_I寄存器,对照读返回值,用小灯泡逻辑判断哪一步出错了。
5.2 姿态角缓慢漂移
现象:静止放置时roll和pitch还会缓慢变化,几分钟内飘了几度。
原因大多是陀螺仪零漂。每个传感器出厂时的零点不完全在0,存放环境温度变化,零漂还会变化。最简单的处理办法是上电校准:在上电后先让飞控静止两三秒,采100组陀螺仪数据求平均,把这个平均值记为GyroOffset,在每次读取原始数据之后先减去这个偏移量,再做单位换算。这种方法能显著改善姿态漂移。
还有一种漂移是因为积分漂移。如果陀螺仪零漂偏大,但加速度计对解算的修正力度不够,误差就会慢慢积累。这时可以把Mahony的Kp调大一点,让加速度计的修正权重更高。Kp太小姿态懒洋洋的,Kp太大抗震动会变差,我一般从0.5开始,步进0.1往上调,观察悬停时的表现。
5.3 上位机数据跳动
现象:上位机曲线一直都在,但角度在±10度以上来回跳,或者飞行时姿态角剧烈抖动。
如果静止时数据跳,通常先是震动影响加速度计,电机转动产生的高频振动会被加速度计捕捉到,造成修正方向混乱。处理办法:确认DLPF配置是否合理,带宽太高会把电机的高频振动放进来;检查安装,传感器尽量用减振泡棉跟机架隔离看看效果。
如果飞行时跳,还有一个常见原因是控制周期和解算周期不匹配。比如你以200Hz解算,但控制是100Hz,两次控制之间姿态数据已经换了好几次了,控制输出就会滞后。把解算和控制统一到同一个频率上,问题就自然消失了。
5.4 上位机完全不显示
现象:串口助手能看到数据,但匿名上位机界面上没反应。
优先级最高的可能性是帧格式不匹配,功能字或者长度字段对不上。先在上位机的协议说明里确认帧头、功能字、长度字段、校验方式。校验和有一个很隐蔽的坑:匿名上位机的部分版本里,校验和是从帧头第一个字节开始算到数据区最后一个字节,但有些版本是把校验和单独按16位累加再取低8位,而有些是8位溢出截断。我测试下来我用的这个版本是8位累加,如果你照抄的代码校验方式对应的是另一个版本,就会一直校验失败。
遇到串口乱码时,先看波特率、数据位、停止位、校验位是否和上位机一致,再看接地是否共用,模块和USB转TTL一定要共地,不然数据会乱。
5.5 解算出来的角度和实际相反
最后补一个很容易踩的逻辑坑:有些MPU6050模块安装方向不同,X、Y、Z轴和机体的方向对不上,直接解算出来的roll和pitch会和实际姿态相反,或者X轴加速度数据和Y轴数据反了。这个不是代码问题,而是传感器坐标系和机体坐标系的安装关系没有对齐。
解决办法:在初始化阶段定义一个传感器方向映射表,比如把加速度计的X、Y轴做个符号翻转,或者交换轴序,让传感器的坐标系和机体坐标系对准。我一般会在桌上用软件逐轴转动飞控,对比上位机显示的姿态变化方向,来确认是否需要翻转。这个步骤在整机装配后一定要做,千万不要假设PCB上印的轴方向就一定对。
5.6 调试问题速查表
把上面这些问题整理成一个速查表,现场调试的时候直接对着查:
| 现象 | 大概率原因 | 快速检查方法 |
|---|---|---|
| I2C读不到WHO_AM_I | 接线、地址、上拉问题 | 示波器看ACK,检查0x68/0x69 |
| 静态姿态漂移 | 陀螺仪零漂未校准 | 上电静止求平均偏移量 |
| 数据跳动 | 震动影响加速度计 | 检查DLPF带宽和减震安装 |
| 上位机无显示 | 协议版本不匹配、校验错误 | 对照协议文档核对功能字和校验方式 |
| 串口乱码 | 波特率不一致、未共地 | 检查串口配置和GND连接 |
| 角度与实际相反 | 传感器坐标轴未对齐 | 逐轴转动飞控,对比姿态显示方向 |
这篇就写到这里。个人经验之谈:姿态解算这种模块,代码本身不难,难的是调试时把算法当作黑盒去猜。建议拿到我的代码后,自己把四元数更新公式和欧拉角转换公式都画一画,搞清楚每一步数据是怎么变换的。后面系列第三篇,我会把姿态数据接到PID控制器里,再做自稳模式,到时候你会发现,这篇打下的解算基础直接决定了飞控能不能飞稳。自己写一遍解算算法,比抄十遍DMP库都值得。
本文还有配套的精品资源,点击获取