第一次把 MPU6050 焊上自己的小四轴时,我踩过一个大坑:直接用加速度计反三角算角度,结果电机一转,上位机里的角度曲线就像得了帕金森一样疯狂抖动。改成陀螺仪积分,静止时确实丝滑,可过了半分多钟就开始慢慢往一个方向漂,越漂越远。后来真正把飞控算法的姿态解算这层窗户纸捅破——用互补滤波把两者按频率特性揉在一起,世界才清净了。这篇就聊聊我理解的互补滤波:加速度计和陀螺仪为什么单独都不好用,一阶互补滤波公式到底在干什么,以及在 STM32 + MPU6050 上怎么把它写成能飞的代码。想认真搞飞控算法的朋友,不管是自己打板做小四轴,还是纯粹想弄懂姿态解算的原理,这篇都适合你。
1. 传感器原罪:加速度计和陀螺仪为什么单独都不可用
1.1 加速度计测姿态:静止很准,一动就骗人
先看加速度计。它的物理模型是“比力传感器”,测量的是机体受到的加速度矢量。飞行器静止悬停时,加速度计唯一感受到的加速度就是重力加速度的反方向,所以从三轴分量就能反推出当前姿态角。
以常规机体坐标系为例,x 轴朝前、y 轴朝右、z 轴朝下,静止时加速度计读数约为 (0, 0, 1g)。此时:
- roll 滚转角:
atan2(ay, az) - pitch 俯仰角:
atan2(-ax, sqrt(ay*ay + az*az))
这里面没有任何积分过程,测量是绝对式的,所以长期来看它不会漂移。这正是加速度计最宝贵的特性:低频稳定。
但问题也极其明显。飞行器一旦开始加减速,加速度计上叠加的就不再只有重力了,发动机拉力、空气阻力、旋翼气流都会变成额外的“运动加速度”。比如飞机以 0.5g 的水平加速度往前冲,加速度计合成矢量的方向会明显偏离真正的重力方向,解算出来的俯仰角可能凭空偏出好几度。你看着姿态角在跳,但机体其实根本没转,这就是运动加速度污染。
更要命的是电机高速旋转带来的振动。无刷电机的换向振动、螺旋桨高频切割空气的气流扰动,全都会被加速度计接收。直接拿原始数据算角度,出来的曲线全是毛刺。就算在软件里加低通滤波器,滤波也会引入相位延迟,姿态响应变迟钝,飞控的角速度环和姿态环都会受影响。
1.2 陀螺仪积分:瞬间贼准,时间一长全是债
陀螺仪测量的是角速度,单位是 deg/s 或 rad/s。要得到角度,必须对时间积分:
θ(t) = θ(0) + ∫ ω(τ) dτ积分的好处是它完全不受线加速度干扰,你飞机怎么乱飞,只要转速真是那么多,角度增量就是准的。而且陀螺仪高频响应极好,动态性能碾压加速度计。
坏处呢?它有一个隐藏的“债主”,叫零偏。
陀螺仪静止时输出不应该是 0,但实际芯片总存在一个微小的偏置电压或者说 bias,比如 0.1°/s。你即使做了上电校准,把静止时的平均值减掉了,可温度一变化、供电电压一波动,bias 还会偷偷跑回来。0.1°/s 的零偏听起来微不足道,积上 300 秒就是 30° 的漂移。四轴飞个五分钟,姿态从“水平”漂成“倒扣”都不是玩笑。
用一个生活类比:陀螺仪像记忆力特别好但从来不检查作业的学生,短时间能背得滚瓜烂熟,时间一长错题就累积到没法收拾。加速度计像每道题都要翻书对答案的同学,答案正确但速度太慢,还经常被窗外的动静带偏。
1.3 频域里它俩是互补的
把两个传感器的误差拿到频域里看,就特别有意思:
- 陀螺仪:高频段准确,低频段有漂移,误差集中在低频;
- 加速度计:低频段准确,高频段噪声大,误差集中在高频。
一个是高通友好型,一个是低通友好型。那自然的处理方式就是:让陀螺仪信号通过高通滤波器,让加速度计信号通过低通滤波器,再把两者加起来,不就得到一个全频段都靠谱的姿态估计了吗?这就是“互补滤波”这个名字的真正来源——不是简单的加权平均,而是在频率域上做互补拼接。
这也是为什么互补滤波特别适合飞控:算法简单、计算量小、不需要复杂的噪声协方差模型,在 MCU 资源紧张的场景下非常有价值。
2. 一阶互补滤波公式拆解:一个 α 值背后的权衡
2.1 离散递推式怎么理解
最经典的一阶互补滤波离散形式长这样:
θ[k] = (1 - α) · (θ[k-1] + ω[k] · Δt) + α · θ_acc[k]拆开看,每一项都很直白:
θ[k-1] + ω[k]·Δt:陀螺仪从上一时刻积分得到的当前角度预测值;θ_acc[k]:加速度计当前直接解算出的角度观测值;α:加速度计的信任权重,取值范围 0~1。
如果α = 0,滤波输出就是纯陀螺仪积分;如果α = 1,输出就是纯加速度计角度。实际飞控里 α 一般取 0.02~0.2 之间,也就是大多数时候信任陀螺仪,加速度计只负责慢慢地把积分漂移“拽回来”。
这个公式的工程含义很舒服:短期剧烈运动时,陀螺仪积分主导,姿态跟手;长期静止或匀速时,加速度计观测持续修正,消除漂移。
2.2 α 与时间常数:为什么不能随手填一个数
很多新手看到 α 就直接填 0.5,觉得“一半一半”最平衡,这是典型误区。
滤波器的响应速度可以用等效时间常数 τ 来描述。对一阶互补滤波,当 α 远小于 1 时,近似有:
τ ≈ Δt / α对应的截止频率:
fc ≈ α / (2π · Δt)举个例子。假设姿态解算跑在 500Hz,也就是 Δt = 2ms:
- 如果 α = 0.1,τ ≈ 20ms,修正收敛得很快,但加速度计噪声也会泄进来;
- 如果 α = 0.02,τ ≈ 100ms,滤波输出更丝滑,但姿态估计对真实角度的跟随会明显变慢;
- 如果 α = 0.5,τ ≈ 4ms,那基本上就是加速度计在主导,陀螺仪积分反而成了配角,动态性能直接崩掉。
调参的时候,不要凭空猜 α,先想明白你希望姿态估计在多长时间内收敛到加速度计的参考方向。想要更跟手的动态响应,α 可以稍微大一点;想要更平滑的静态输出,α 就小一点。这个时间尺度的概念,比死记“α 取 0.1”靠谱得多。
2.3 欧拉角版本的互补滤波:roll/pitch 能补,yaw 没救
网上很多入门例程会先给一个最朴素的欧拉角互补滤波,把 roll 和 pitch 先用加速度计算出来,再套递推式:
float roll_est = 0.0f, pitch_est = 0.0f; float alpha = 0.1f; float gx_rad, gy_rad; // 陀螺仪角速度,单位 rad/s float ax, ay, az; // 加速度计,单位 g float dt; // 采样周期,单位 s float roll_acc = atan2f(ay, az) * 57.29578f; float pitch_acc = atan2f(-ax, sqrtf(ay*ay + az*az)) * 57.29578f; roll_est = (1.0f - alpha) * (roll_est + gx_rad * dt * 57.29578f) + alpha * roll_acc; pitch_est = (1.0f - alpha) * (pitch_est + gy_rad * dt * 57.29578f) + alpha * pitch_acc;这个版本入门看没有问题,但它有个天生缺陷:yaw 偏航角补不了。因为重力方向永远垂直于水平面,加速度计根本无法分辨飞机绕重力轴转了多少度,也就是重力矢量在 yaw 轴上没有任何投影信息,所以 yaw 只能靠陀螺仪积分,漂移是必然的。
要解决 yaw 漂移,要么加磁力计做绝对航向参考,要么升级到四元数版本,把加速度计和磁力计的参考矢量统一放进一个框架里修正。这也是为什么后面我们最终会走到 Mahony 风格的四元数互补滤波。
3. STM32+MPU6050 的工程实现:从寄存器配置到 Mahony 风格互补滤波
3.1 传感器初始化和单位换算
先说硬件底子。MPU6050 通过 I2C 接口通信,AD0 接地时从机地址是 0x68。飞控应用里我建议加速度计量程选 ±8g 或 ±16g,因为真实飞行中机体运动加速度很容易超过 2g,选 ±2g 虽然精度高但在大机动时会饱和。陀螺仪量程建议选 ±500dps 或 ±1000dps,±250dps 在快速翻滚时也可能爆量程。
初始化代码大致如下:
#define MPU6050_ADDR 0x68 void MPU6050_Init(void) { // 复位传感器 MPU_Reg_Write(MPU6050_ADDR, 0x6B, 0x80); delay_ms(100); // 唤醒并选择 Z 轴陀螺作为内部时钟源,稳定性更好 MPU_Reg_Write(MPU6050_ADDR, 0x6B, 0x03); // 陀螺仪配置:±500dps,灵敏度 65.5 LSB/(°/s) MPU_Reg_Write(MPU6050_ADDR, 0x1B, 0x08); // 加速度计配置:±8g,灵敏度 4096 LSB/g MPU_Reg_Write(MPU6050_ADDR, 0x1C, 0x10); // 数字低通滤波器 DLPF 配置,这里选了一个折中频段 // 实际值需要结合你的采样率和控制频率调整 MPU_Reg_Write(MPU6050_ADDR, 0x1A, 0x05); }关键在于单位换算,这一步是新手翻车重灾区。加速度计原始值是 int16,要除以灵敏度系数得到 g;陀螺仪原始值要除以灵敏度得到 deg/s,再乘上 0.0174533 转成 rad/s。因为后面的四元数积分公式明确要求角速度单位是 rad/s,如果你拿 deg/s 直接算,姿态会疯狂乱转。
int16_t ax_raw, ay_raw, az_raw; int16_t gx_raw, gy_raw, gz_raw; float ax = ax_raw / 4096.0f; // 单位 g float ay = ay_raw / 4096.0f; float az = az_raw / 4096.0f; float gx = (gx_raw - gyro_bias[0]) / 65.5f * 0.0174533f; // 单位 rad/s float gy = (gy_raw - gyro_bias[1]) / 65.5f * 0.0174533f; float gz = (gz_raw - gyro_bias[2]) / 65.5f * 0.0174533f;3.2 零偏校准:上电第一件事
陀螺仪零偏不校掉,后面所有功夫都白费。最可靠的校准方式就是上电后静置采样若干次求平均,把平均原始值作为 bias 存起来。注意校准时机尽量避开刚上电那两三秒,因为芯片内部时钟和电源还没稳定,此时 bias 会突变。
#define GYRO_SAMPLE_NUM 500 float gyro_bias[3] = {0.0f, 0.0f, 0.0f}; void Gyro_Calib(void) { float sum[3] = {0.0f, 0.0f, 0.0f}; for (int i = 0; i < GYRO_SAMPLE_NUM; i++) { read_gyro_raw(&gx_raw, &gy_raw, &gz_raw); sum[0] += gx_raw; sum[1] += gy_raw; sum[2] += gz_raw; delay_ms(1); } gyro_bias[0] = sum[0] / GYRO_SAMPLE_NUM; gyro_bias[1] = sum[1] / GYRO_SAMPLE_NUM; gyro_bias[2] = sum[2] / GYRO_SAMPLE_NUM; }更讲究的场景还会做温度补偿:把零偏随温度变化的曲线拟合出来,在飞行中实时查表校正。但对大多数自研小四轴来说,上电校准加“静止时定期重新校准”已经够用了。
3.3 四元数更新:为什么内部状态量不用欧拉角
初次接触姿态解算的人都会问:既然最后输出还是要转成欧拉角,我直接用欧拉角当内部状态不行吗?
答案是不行,核心原因是万向节锁。当俯仰角达到 ±90° 时,滚转和偏航的旋转轴会重合,欧拉角自由度退化,姿态解算会出现奇异点。四轴飞行器理论上确实可以飞出不大于 90° 的俯仰,但你想做翻滚动作、倒飞,或者面对复杂机动时,用欧拉角当内部状态就是埋雷。
四元数用四个分量描述三维旋转,没有奇异点,代价只是多了一维约束——模长必须恒等于 1。更新公式长这样:
q_dot = 0.5 · q ⊗ (0, ωx, ωy, ωz)离散化后就是常见的一阶龙格库塔:
q0 += 0.5f * dt * (-q1 * gx - q2 * gy - q3 * gz); q1 += 0.5f * dt * ( q0 * gx + q2 * gz - q3 * gy); q2 += 0.5f * dt * ( q0 * gy - q1 * gz + q3 * gx); q3 += 0.5f * dt * ( q0 * gz + q1 * gy - q2 * gx);每步更新后必须归一化,否则积分的模长误差会不断累积,姿态就会逐渐失真。
3.4 Mahony 互补滤波的落地代码
Mahony 算法本质上就是一种互补滤波:用加速度计的重力参考矢量,和四元数推算出的重力投影矢量做叉积,叉积结果近似等于姿态误差,再把这个误差用 PI 补偿器反馈到陀螺仪角速度上。最关键的一点,是用修正后的角速度去做四元数积分。
完整实现如下,这个函数建议放在固定采样周期的定时器中断里调用:
typedef struct { float q0, q1, q2, q3; // 四元数 float ex_int, ey_int, ez_int; // 积分误差项 float dt; // 姿态解算采样周期,单位 s } ahrs_t; void MahonyAHRSupdate(ahrs_t *ahrs, float gx, float gy, float gz, // 陀螺仪,rad/s,已减零偏 float ax, float ay, float az) // 加速度计,单位 g { float norm; float vx, vy, vz; float ex, ey, ez; float halfT = 0.5f * ahrs->dt; float Kp = 2.0f; float Ki = 0.005f; // 加速度计归一化 norm = sqrtf(ax * ax + ay * ay + az * az); if (norm < 1e-6f) return; ax /= norm; ay /= norm; az /= norm; // 根据当前四元数,计算重力在机体坐标系下的投影 vx = 2.0f * (ahrs->q1 * ahrs->q3 - ahrs->q0 * ahrs->q2); vy = 2.0f * (ahrs->q0 * ahrs->q1 + ahrs->q2 * ahrs->q3); vz = ahrs->q0 * ahrs->q0 - ahrs->q1 * ahrs->q1 - ahrs->q2 * ahrs->q2 + ahrs->q3 * ahrs->q3; // 叉积误差:加速度计测量方向与推算方向之间的偏差 ex = ay * vz - az * vy; ey = az * vx - ax * vz; ez = ax * vy - ay * vx; // 误差积分项,用来消除稳态漂移 ahrs->ex_int += Ki * ex * ahrs->dt; ahrs->ey_int += Ki * ey * ahrs->dt; ahrs->ez_int += Ki * ez * ahrs->dt; // 补偿角速度 gx += Kp * ex + ahrs->ex_int; gy += Kp * ey + ahrs->ey_int; gz += Kp * ez + ahrs->ez_int; // 四元数一阶积分更新 ahrs->q0 += halfT * (-ahrs->q1 * gx - ahrs->q2 * gy - ahrs->q3 * gz); ahrs->q1 += halfT * ( ahrs->q0 * gx + ahrs->q2 * gz - ahrs->q3 * gy); ahrs->q2 += halfT * ( ahrs->q0 * gy - ahrs->q1 * gz + ahrs->q3 * gx); ahrs->q3 += halfT * ( ahrs->q0 * gz + ahrs->q1 * gy - ahrs->q2 * gx); // 归一化 norm = sqrtf(ahrs->q0 * ahrs->q0 + ahrs->q1 * ahrs->q1 + ahrs->q2 * ahrs->q2 + ahrs->q3 * ahrs->q3); if (norm < 1e-6f) return; ahrs->q0 /= norm; ahrs->q1 /= norm; ahrs->q2 /= norm; ahrs->q3 /= norm; }最后,需要把四元数转成欧拉角给上位机显示或者给控制环用:
float roll = atan2f(2.0f * (q0*q1 + q2*q3), q0*q0 - q1*q1 - q2*q2 + q3*q3) * 57.29578f; float pitch = asinf(2.0f * (q0*q2 - q1*q3)) * 57.29578f; float yaw = atan2f(2.0f * (q0*q3 + q1*q2), q0*q0 + q1*q1 - q2*q2 - q3*q3) * 57.29578f;如果你发现 roll 或者 pitch 的符号是反的,优先检查你的机体坐标轴定义和 MPU6050 安装方向,不要急着乱改公式。
4. 调参与飞控应用现场:我在实际调试中踩过的具体坑
4.1 Kp 和 Ki 的调参手感
Mahony 互补滤波的调参其实比很多人想得简单。Kp 是比例修正增益,决定“加速度计把陀螺仪漂移拉回来”的力度;Ki 是积分增益,负责消除长时间静置时残留的稳态误差。
我的经验是先设 Ki = 0,只调 Kp:
- Kp 太小,姿态估计对加速度计的修正不敏感,长时间悬停还是会有缓慢漂移;
- Kp 太大,加速度计的高频噪声会被放大,姿态曲线重新变成“心电图”,而且大机动时运动加速度干扰也会被放大。
一个可行的调参起点:200~500Hz 采样率下,Kp 取 1.0~3.0,Ki 取 0.001~0.01。调 Ki 时一点一点加,加到静止几分钟不再漂为止。加多了,你会发现猛烈转动后姿态会有明显的“过冲回弹”。
| 现象 | 可能原因 | 调参/排查方向 |
|---|---|---|
| 姿态曲线高频抖动 | Kp 过大 / 加速度计振动噪声 | 降低 Kp,检查减震安装 |
| 长时间缓慢漂移 | 零偏没校干净 / Ki 太小 | 重新校准,缓慢增大 Ki |
| 快速转动后角度跟手度差 | Kp 太小 / 采样周期不稳 | 增大 Kp,固定采样周期 |
| 推油门瞬间角度跳变 | 运动加速度干扰 | 用加速度模长检测并降权 |
| 静止时角度慢慢朝一个方向翻 | 符号反了 / 零偏没减干净 | 检查坐标轴方向和叉积符号 |
4.2 保持采样周期稳定是第一优先级
姿态解算对 dt 极其敏感。如果把 Mahony 更新放在主循环里,而主循环同时又处理遥控器接收、电机输出、LED 闪烁,那么每次循环的时间都不一样。dt 一会儿 2ms 一会儿 8ms,积分的随机误差会直接转化为姿态的随机漂移,而且这种漂移极难排查。
我踩过一次特别深的坑:芯片在冷启动时定时器初始化慢了半拍,前几百个姿态解算周期 dt 全是乱的,导致解锁后飞控差点直接翻机。后来把所有跟“时间”相关的逻辑都用硬件定时器统一管理,姿态解算固定放在 1ms 定时器中断里,问题立刻消失。
如果实在没办法把解算放进中断,至少要在主循环里用时间戳测量真实 dt,喂给滤波函数,而不是用固定常量。
4.3 电机振动:加速度计噪声的工程来源
电机一转,加速度计输出就会叠加大量高频振动。这种噪声在飞行中很难完全靠软件消除。你可以给加速度计信号加数字低通,但滤波本身会带来相位延迟,姿态环性能反而可能下降。
工程上更推荐先做硬件减震:传感器和飞控板之间加一层硅胶减震垫或海绵,把高频振动从物理上隔掉。很多商品飞控的传感器都封装在减震结构里,不是因为传感器怕摔,而是因为高频振动会直接毁掉姿态解算的精度。
软件层面,如果发现静止时姿态曲线已经足够平,但解锁后噪声骤增,优先检查减震,不要一上来就把 Kp 往下猛降。
4.4 大机动下加速度计会“说谎”
加速度计的参考矢量在正常情况下就是重力方向,但大机动时运动加速度会混进来,导致加速度计给出的方向偏离真实重力。典型现象是快速推油门时,姿态角瞬间跳一下,然后慢慢回正。
一个实用的保护策略:实时计算加速度计模长norm = sqrt(ax²+ay²+az²)。如果 norm 明显偏离 1g,比如大于 1.3 或小于 0.7,就认为此刻运动加速度干扰很大,应该降低加速度计的修正权重,甚至直接切换到纯陀螺仪积分模式,等 norm 恢复到 1g 附近再恢复互补修正。
这也符合互补滤波的思想:故障信号不可信时,就少信它一些。
4.5 一个判断算法是否正常的小技巧
把四元数转出来的 roll/pitch/yaw 和角速度曲线同时打印到上位机。
- 静止时,roll 和 pitch 应当保持在 ±1° 以内,yaw 允许有小幅漂移但速度很慢;
- 快速来回转动机体,角度曲线应当跟手但不过冲;
- 猛地停下,姿态应当迅速收敛到当前真实姿态,没有长时间的回摆。
如果静止时 roll/pitch 在缓慢爬升,多半是陀螺仪零偏没校干净;如果一转动就疯狂跳变,多半是 Kp 太大或者坐标系符号反了。用这条流程排查,比瞎调参数高效得多。
5. 在飞控体系里看互补滤波:它处在什么位置,还有哪些路可走
5.1 开源飞控里的互补滤波影子
很多初级开发者以为开源飞控现在都用卡尔曼滤波,互补滤波已经过时了。实际上不是这样。
ArduPilot 早期版本大量使用的 DCM 方向余弦矩阵算法,本质上就是方向余弦矩阵框架下的互补滤波:用加速度计和磁力计修正陀螺仪积分的方向误差。PX4 也保留了基于四元数互补滤波的姿态估计器,因为它在计算量、鲁棒性、调试复杂度之间取得了很好的平衡。只有在需要更高精度、更多传感器融合的场景,才会升级到 EKF 这类基于状态协方差估计的算法。
所以互补滤波不是玩具,而是一套有深厚工程基础的姿态融合方案。你在学习阶段把它彻底搞懂,后面看 EKF 也不会那么劝退。
5.2 为什么 MPU6050 有 DMP,还要自己写互补滤波
MPU6050 内部集成了一个数字运动处理器 DMP,可以直接输出四元数,省掉你写滤波算法的工作。听起来很美好,但实际工程里 DMP 有几个不太舒服的点:
- DMP 固件是 InvenSense 提供的库,闭源、有授权限制,在产品化时存在法律和可控性风险;
- DMP 输出频率和融合逻辑是封死的,你想临时改一下融合权重或者接入自定义传感器数据,根本插不进手;
- 姿态解算的学习价值几乎为零,因为内部怎么运作你完全看不见。
相比之下,自己写 Mahony 互补滤波也就几十行代码,算力开销极小,代码完全可控,还能随意扩展磁力计。如果你想做的是自研飞控或者深度定制,我强烈建议至少把互补滤波版本调通,再考虑是否上 DMP。
5.3 什么情况下该继续用互补滤波,什么情况下该上卡尔曼
我的判断标准很简单:
- 学习阶段 / 低成本小四轴 / 固定翼姿态参考:互补滤波足够,别自我感动式上卡尔曼;
- 多旋翼长时间高精度悬停、混入GPS/气压计/磁力计/视觉等多种传感器:上扩展卡尔曼或误差状态卡尔曼更合适,因为卡尔曼框架能显式建模传感器噪声和观测模型;
- 传感器只有一个 IMU,且算力极其紧张:互补滤波依然是性价比极高的选择。
从互补滤波过渡到卡尔曼时,你会发现卡尔曼里的“预测”对应陀螺仪积分,“更新”对应加速度计观测修正,思路和互补滤波一脉相承,只是把固定增益换成了随时间变化的卡尔曼增益。所以先把互补滤波的血肉吃透,真的不是弯路。
我自己调完这套算法后最大的体会是:姿态解算这件事,传感器原始数据的质量远比算法本身重要。零偏没校干净、采样周期不稳定、坐标系符号反了,这些问题靠再高级的滤波算法也救不回来。先把原始数据、时间基准、坐标定义这三样东西弄得干干净净,哪怕只用一个简单的一阶互补滤波,也能让飞机稳稳地飞起来。如果你正被姿态角乱跳折磨,先别急着折腾复杂算法,回头看看这三样,八成问题就出在那里。