3、姿态解算入门:欧拉角与四元数、互补滤波算法原理、代码实现与调试

姿态解算,说白了就是让飞控知道「我现在是头朝上还是脚朝下」。你想想看,无人机在空中飘着,如果没有一个准确的姿态信息,它连自己歪没歪都不知道,更别提稳定飞行了。

我个人习惯把姿态解算比作「给飞控装一个内耳前庭」。我们人类靠内耳感知倾斜,飞控则靠陀螺仪和加速度计。但这两样东西各有毛病——陀螺仪长期漂移,加速度计短期抖动。怎么把它们捏合到一起?这就是本章要聊的核心。

3.1 欧拉角:最直观的姿态表示

欧拉角就是三个角度:横滚(Roll)、俯仰(Pitch)、偏航(Yaw)。

我在项目中遇到过一个问题:用欧拉角做姿态控制时,到了90度俯仰突然就失控了。后来才意识到,这就是著名的「万向锁」问题。说白了,当俯仰角接近±90°时,横滚和偏航会耦合在一起,丢失一个自由度。

欧拉角 范围 说明
横滚角 φ -180° ~ 180° 绕X轴旋转,左右倾斜
俯仰角 θ -90° ~ 90° 绕Y轴旋转,前后倾斜
偏航角 ψ -180° ~ 180° 绕Z轴旋转,机头朝向
注意:欧拉角在工程中只适合做「显示」和「小角度控制」。如果你要做全姿态飞行(比如倒飞、筋斗),千万别用欧拉角做解算,否则你会被万向锁坑得很惨。

3.2 四元数:没有死角的姿态神器

四元数是什么?你可以把它理解成一个「带方向的旋转轴 + 旋转角度」。它用四个数表示姿态:q = [w, x, y, z],其中 w 是标量部分,[x, y, z] 是矢量部分。

为什么飞控圈都爱用四元数?因为它没有万向锁,而且计算效率高。我刚开始学的时候也觉得四元数很抽象,但后来发现,你根本不需要理解它的几何意义——你只需要知道怎么用它做运算就够了。

嗯,这里要注意:四元数必须归一化。也就是 w² + x² + y² + z² = 1。如果不归一化,姿态会越算越歪。

核心公式:四元数更新方程(陀螺仪积分)
q_dot = 0.5 * q ⊗ ω
其中 ω 是角速度向量,⊗ 表示四元数乘法。

3.3 互补滤波:把两个传感器的优点结合起来

陀螺仪动态响应快,但会漂移。加速度计静态准确,但怕振动。互补滤波的思路很简单:

  • 陀螺仪:负责短期姿态变化,高通滤波
  • 加速度计:负责长期姿态校准,低通滤波

说白了就是:陀螺仪算出来的姿态,用加速度计慢慢「拉回来」。这个拉回来的力度,由一个参数 Kp 控制。

我曾经在调试一款四轴时,发现悬停时姿态总在缓慢漂移。查了半天,原来是互补滤波的 Kp 设得太小了,加速度计拉不回来。调大 Kp 后,问题立刻解决。

3.4 代码实现:STM32上的互补滤波

下面是我在项目中实际用过的互补滤波代码。注意,这里用的是 Mahony 算法的一个简化版本,非常适合 STM32F4 系列跑。

// 互补滤波姿态解算
// gx, gy, gz: 陀螺仪角速度 (rad/s)
// ax, ay, az: 加速度计测量值 (归一化后)
// dt: 采样时间 (s)

void complementary_filter(float gx, float gy, float gz,
                         float ax, float ay, float az, float dt)
{
    float recipNorm;
    float halfvx, halfvy, halfvz;
    float halfex, halfey, halfez;

    // 1. 归一化加速度计数据
    recipNorm = invSqrt(ax*ax + ay*ay + az*az);
    ax *= recipNorm;
    ay *= recipNorm;
    az *= recipNorm;

    // 2. 用当前四元数估计重力方向
    halfvx = q1*q3 - q0*q2;
    halfvy = q0*q1 + q2*q3;
    halfvz = q0*q0 - 0.5f + q3*q3;

    // 3. 计算误差(加速度计测量值与估计值的叉积)
    halfex = ay*halfvz - az*halfvy;
    halfey = az*halfvx - ax*halfvz;
    halfez = ax*halfvy - ay*halfvx;

    // 4. 误差积分(消除稳态误差)
    integralFBx += halfex * Ki * dt;
    integralFBy += halfey * Ki * dt;
    integralFBz += halfez * Ki * dt;

    // 5. 补偿陀螺仪
    gx += halfex * Kp + integralFBx;
    gy += halfey * Kp + integralFBy;
    gz += halfez * Kp + integralFBz;

    // 6. 四元数更新(一阶龙格-库塔)
    q0 += (-q1*gx - q2*gy - q3*gz) * 0.5f * dt;
    q1 += ( q0*gx + q2*gz - q3*gy) * 0.5f * dt;
    q2 += ( q0*gy - q1*gz + q3*gx) * 0.5f * dt;
    q3 += ( q0*gz + q1*gy - q2*gx) * 0.5f * dt;

    // 7. 归一化四元数
    recipNorm = invSqrt(q0*q0 + q1*q1 + q2*q2 + q3*q3);
    q0 *= recipNorm;
    q1 *= recipNorm;
    q2 *= recipNorm;
    q3 *= recipNorm;
}
调试技巧:Kp 和 Ki 的调参口诀——
Kp 越大,加速度计拉回越快,但容易引入高频噪声。
Ki 用来消除稳态误差,但太大会导致超调。
我一般从 Kp=0.5, Ki=0.0 开始,先调 Kp 让姿态不漂移,再加一点 Ki 消除小误差。

3.5 调试实战:从串口看姿态

代码写完了,怎么知道它算得对不对?我建议你先把四元数转成欧拉角,通过串口打印出来,然后手动转动飞控板观察数值变化。

// 四元数转欧拉角
float roll  = atan2f(2.0f*(q0*q1 + q2*q3), 1.0f - 2.0f*(q1*q1 + q2*q2));
float pitch = asinf(2.0f*(q0*q2 - q3*q1));
float yaw   = atan2f(2.0f*(q0*q3 + q1*q2), 1.0f - 2.0f*(q2*q2 + q3*q3));

// 弧度转角度
roll  = roll  * 180.0f / M_PI;
pitch = pitch * 180.0f / M_PI;
yaw   = yaw   * 180.0f / M_PI;

printf("Roll:%.1f Pitch:%.1f Yaw:%.1f\r\n", roll, pitch, yaw);

调试时有个坑:如果你把飞控板平放,Roll 和 Pitch 应该是 0° 左右。如果发现偏了好几度,先检查加速度计有没有校准。我曾经因为加速度计零偏没去掉,折腾了一下午才发现问题。

避坑指南:千万不要在飞控板上电时用手去掰传感器!很多初学者喜欢用手转动板子看姿态变化,但手抖会引入大量噪声,让你误以为算法有问题。正确的做法是:把板子固定在一个已知角度,然后观察输出是否稳定。

3.6 小结

姿态解算是飞控的基石。欧拉角直观但有限制,四元数强大但抽象。互补滤波则是把两者结合起来的实用方案。我个人建议:

  • 做显示和日志记录时,用欧拉角
  • 做控制解算时,全程用四元数
  • 互补滤波的 Kp 和 Ki 一定要根据实际硬件调,别照搬别人的参数

下一章我们会聊到 PID 控制器的实现。到时候你会发现,姿态解算的精度直接决定了 PID 能不能调好。所以,这一章的内容一定要亲手跑一遍代码,别只看不练。