3、姿态表示(四元数):四元数定义、乘法、共轭、旋转操作,为什么飞控偏爱四元数?

聊到飞控的姿态表示,四元数是个绕不开的话题。我记得刚入行那会儿,看到四元数公式就头疼——四个数怎么就能表示旋转了?后来在项目里吃过欧拉角万向锁的亏,才真正体会到四元数的好。今天咱们就把这东西掰开揉碎了讲清楚。

3.1 四元数的定义

说白了,四元数就是一个超复数。普通复数有一个实部一个虚部,四元数有一个实部和三个虚部:

q = w + xi + yj + zk

其中 i、j、k 满足:

i² = j² = k² = ijk = -1
ij = k, jk = i, ki = j
ji = -k, kj = -i, ik = -j

嗯,这里要注意,乘法不满足交换律。我在项目中第一次写四元数乘法时,就因为这个吃了亏——顺序搞反了,结果姿态解算直接炸了。

通常我们把四元数写成向量形式:

q = [w, x, y, z]ᵀ  或  q = w + v

w 是标量部分,v = (x, y, z) 是矢量部分。

3.2 四元数的基本运算

3.2.1 四元数乘法

两个四元数 q₁ = w₁ + v₁ 和 q₂ = w₂ + v₂ 相乘:

q₁ ⊗ q₂ = (w₁w₂ - v₁·v₂) + (w₁v₂ + w₂v₁ + v₁×v₂)

写成矩阵形式更直观:

q₁ ⊗ q₂ = 
[ w₁  -x₁  -y₁  -z₁ ] [ w₂ ]
[ x₁   w₁  -z₁   y₁ ] [ x₂ ]
[ y₁   z₁   w₁  -x₁ ] [ y₂ ]
[ z₁  -y₁   x₁   w₁ ] [ z₂ ]

我的经验:实际写代码时,我习惯用这个矩阵形式。它比直接算叉积点积要快,而且不容易出错。你想想看,在飞控里每秒要算几千次姿态更新,这点效率差异就放大了。

3.2.2 共轭与模

四元数的共轭很简单,就是把矢量部分取反:

q* = w - xi - yj - zk

模长:

||q|| = √(w² + x² + y² + z²)

单位四元数的模为1,这是飞控里最常用的。为什么?因为单位四元数才能表示纯旋转,不引入缩放。

3.2.3 逆四元数

对于单位四元数,逆就等于共轭:

q⁻¹ = q*

这个性质太棒了。我在做姿态插值时,经常用这个来算相对旋转。

3.3 用四元数表示旋转

这是核心。一个三维向量 p = (x, y, z),用四元数表示为 p_q = (0, x, y, z)。绕单位轴 u 旋转 θ 角度:

q = cos(θ/2) + u·sin(θ/2)

旋转后的向量:

p' = q ⊗ p_q ⊗ q*

注意顺序:先乘 q,再乘 q*,不能搞反。

避坑指南:我曾经在代码里把 q 和 q* 的顺序写反了,结果旋转方向完全反了。调试了整整一个下午才发现。所以建议你写个单元测试,旋转一个已知向量验证一下。

3.4 为什么飞控偏爱四元数?

这个问题面试常考。我总结了几个关键点:

特性 四元数 欧拉角 旋转矩阵
万向锁 有(俯仰±90°时)
参数数量 4个 3个 9个
计算效率
插值平滑 容易(球面插值) 困难 困难
奇异性

说白了,飞控选四元数就三个原因:

  1. 没有万向锁——飞机做大机动时不会突然丢失自由度
  2. 计算量小——嵌入式MCU资源有限,少算一点是一点
  3. 容易融合——IMU数据融合时,四元数更新公式简洁优雅

你想想看,飞机在做筋斗动作时,俯仰角会经过90°,如果用欧拉角,这时候就炸了。但四元数完全没问题。

3.5 代码实现

下面是我在飞控里常用的四元数操作代码:

// 四元数结构体
typedef struct {
    float w;
    float x;
    float y;
    float z;
} Quaternion;

// 四元数乘法
Quaternion quat_mult(Quaternion q1, Quaternion q2) {
    Quaternion result;
    result.w = q1.w*q2.w - q1.x*q2.x - q1.y*q2.y - q1.z*q2.z;
    result.x = q1.w*q2.x + q1.x*q2.w + q1.y*q2.z - q1.z*q2.y;
    result.y = q1.w*q2.y - q1.x*q2.z + q1.y*q2.w + q1.z*q2.x;
    result.z = q1.w*q2.z + q1.x*q2.y - q1.y*q2.x + q1.z*q2.w;
    return result;
}

// 用四元数旋转向量
Vector3 quat_rotate(Quaternion q, Vector3 v) {
    Quaternion p = {0, v.x, v.y, v.z};
    Quaternion q_inv = {q.w, -q.x, -q.y, -q.z};
    Quaternion temp = quat_mult(q, p);
    Quaternion result = quat_mult(temp, q_inv);
    return (Vector3){result.x, result.y, result.z};
}

// 从角速度更新四元数(一阶龙格库塔)
Quaternion quat_update(Quaternion q, Vector3 gyro, float dt) {
    float wx = gyro.x * dt / 2.0f;
    float wy = gyro.y * dt / 2.0f;
    float wz = gyro.z * dt / 2.0f;
    
    Quaternion dq = {1, wx, wy, wz};
    Quaternion q_new = quat_mult(q, dq);
    
    // 归一化,防止累积误差
    float norm = sqrt(q_new.w*q_new.w + q_new.x*q_new.x + 
                      q_new.y*q_new.y + q_new.z*q_new.z);
    q_new.w /= norm;
    q_new.x /= norm;
    q_new.y /= norm;
    q_new.z /= norm;
    
    return q_new;
}

注意:每次更新后一定要归一化!我见过有人忘了这步,结果姿态越飘越远。浮点误差会累积,归一化就是定期校准。

3.6 知识体系总览

下面这张图把四元数的核心逻辑串起来了:

四元数知识体系 四元数 q = w + xi + yj + zk 定义与性质 • 单位四元数 ||q||=1 • 共轭 q* = w - v • 逆 q⁻¹ = q*(单位时) 基本运算 • 乘法:q₁⊗q₂(不交换) • 旋转:p' = q⊗p⊗q* • 更新:qₙ₊₁ = qₙ⊗dq 飞控优势 • 无万向锁 • 计算量小(4参数) • 插值平滑 姿态解算 • IMU数据融合 • 互补滤波/Mahony • EKF状态估计 控制输出 • 期望姿态计算 • 误差四元数 • PID控制输入 飞控姿态表示的最优选择

从这张图能看出来,四元数从定义出发,通过基本运算支撑起姿态解算和控制输出,最终成为飞控系统的核心。我个人觉得,理解四元数的关键不在于背公式,而在于理解它为什么能避免欧拉角的那些坑。

一个小建议:刚开始接触四元数时,别急着看复杂的推导。先写个简单的旋转demo,转几个向量看看效果。等有了直观感受,再回头看公式,会发现豁然开朗。


公众号:蓝海资料掘金营,微信deep3321