四元数微分方程:从运动学本质到工程实现

各位同学,今天我们来啃一块硬骨头——四元数微分方程。说实话,我刚入行那会儿,看到这个方程也是一头雾水。但后来在项目里被它折磨过几次后,我反而觉得它特别有意思。你想想看,一个只有四个数的东西,居然能完整描述三维空间的旋转,这本身就是件很酷的事。

为什么非得是四元数?

在讲微分方程之前,我先说说个人习惯。我为什么偏爱四元数?因为欧拉角有万向锁,方向余弦矩阵有9个参数太冗余。四元数呢?紧凑、无奇点、计算快。我在做无人机飞控时,就吃过欧拉角的亏——飞机抬头到90度时,姿态直接崩了。从那以后,我的代码里再也没出现过欧拉角积分。

四元数的运动学方程,说白了就是描述「四元数随时间怎么变」。它的标准形式是:

dq/dt = 0.5 * q ⊗ ω

其中q是四元数,ω是角速度向量(通常表示为纯四元数[0, ωx, ωy, ωz]),⊗表示四元数乘法。

核心要点:这个方程的本质是——角速度驱动四元数旋转。你给它一个角速度,它就能告诉你姿态怎么演化。

推导过程:一步步来

我们从一个简单的事实出发:刚体旋转可以用旋转矩阵R(t)描述。R(t)满足:

dR/dt = R · [ω]×

这里[ω]×是角速度的反对称矩阵。嗯,这个方程在机器人学里很常见。

四元数和旋转矩阵之间有个映射关系。我记得第一次推导时,花了整整一个下午。其实核心思路就是:把旋转矩阵的微分方程,通过四元数参数化,转换到四元数空间。

具体来说,假设四元数q = [q0, q1, q2, q3]^T,对应的旋转矩阵是R(q)。那么:

dR(q)/dt = R(q) · [ω]×

两边对时间求导,利用链式法则,就能得到:

dq/dt = 0.5 · Ω(ω) · q

其中Ω(ω)是:

Ω(ω) = [ 0,   -ωx,  -ωy,  -ωz;
          ωx,   0,    ωz,  -ωy;
          ωy,  -ωz,   0,    ωx;
          ωz,   ωy,  -ωx,   0  ]

我的小技巧:实际编程时,我从来不会手动展开这个矩阵。直接用四元数乘法函数,把ω构造成纯四元数,然后调用乘法。代码更干净,也更容易调试。

解析解:理想情况下的优雅

如果角速度ω是恒定的,那这个方程就有解析解。说白了就是:

q(t) = exp(0.5 · Ω(ω) · t) · q(0)

这个指数矩阵可以进一步化简。因为Ω(ω)的平方有个漂亮的性质:

Ω(ω)^2 = -||ω||^2 · I_4

利用这个性质,我们可以写出封闭形式:

q(t) = [cos(||ω||·t/2), 
        (ωx/||ω||)·sin(||ω||·t/2),
        (ωy/||ω||)·sin(||ω||·t/2),
        (ωz/||ω||)·sin(||ω||·t/2)] ⊗ q(0)

你看,这个形式多漂亮。它本质上就是绕固定轴旋转了||ω||·t的角度。

注意:解析解只在角速度恒定时成立。实际项目中,陀螺仪数据是离散采样的,角速度每时每刻都在变。所以解析解只能作为理论参考,不能直接用于工程。

数值解:工程中的真功夫

既然解析解不实用,那我们就得用数值方法。我常用的有三种:

方法 精度 计算量 适用场景
欧拉法 一阶 快速原型、低精度需求
龙格-库塔法(RK4) 四阶 大多数嵌入式系统
解析离散法 高精度IMU、航空级

欧拉法最简单:

q_new = q_old + dt · 0.5 · q_old ⊗ ω

但精度差,长时间积分会漂移。我曾经在一个低成本IMU项目里用过,半小时后姿态误差就超过10度了。

RK4是工程中的主力:

k1 = 0.5 · q ⊗ ω(t)
k2 = 0.5 · (q + 0.5·dt·k1) ⊗ ω(t + dt/2)
k3 = 0.5 · (q + 0.5·dt·k2) ⊗ ω(t + dt/2)
k4 = 0.5 · (q + dt·k3) ⊗ ω(t + dt)
q_new = q + (dt/6)·(k1 + 2·k2 + 2·k3 + k4)

我个人习惯用RK4。它在大多数MCU上都能跑,精度也够用。

解析离散法是我最推荐的。它假设在一个采样间隔内角速度恒定,然后直接用解析解更新:

Δθ = ||ω|| · dt
q_rot = [cos(Δθ/2), (ωx/||ω||)·sin(Δθ/2), 
         (ωy/||ω||)·sin(Δθ/2), (ωz/||ω||)·sin(Δθ/2)]
q_new = q_rot ⊗ q_old

这个方法精度高,计算量还小。为什么?因为它利用了微分方程的解析结构,而不是盲目地数值逼近。

避坑指南:我曾经在一个项目中直接用欧拉法积分,结果姿态发散得一塌糊涂。后来换成解析离散法,问题立刻解决。记住:永远不要用欧拉法做四元数积分,除非你只需要几秒钟的数据。

归一化:一个不能忘的步骤

无论你用哪种数值方法,积分后的四元数都会偏离单位长度。这是因为数值误差会累积。所以每次更新后,必须做归一化:

q = q / ||q||

我见过有人偷懒,隔几步才归一化一次。结果姿态慢慢就歪了。我的建议是:每次积分后立即归一化,这步不能省。

代码实现示例

下面是我在STM32上用的代码片段,用的是解析离散法:

// 四元数更新,使用解析离散法
void quaternion_update(quat_t *q, float gx, float gy, float gz, float dt) {
    float half_dt = 0.5f * dt;
    float wx = gx * half_dt;
    float wy = gy * half_dt;
    float wz = gz * half_dt;
    
    float angle = sqrtf(wx*wx + wy*wy + wz*wz);
    
    if (angle < 1e-6f) {
        // 角速度太小,不更新
        return;
    }
    
    float sin_half = sinf(angle) / angle;
    float cos_half = cosf(angle);
    
    // 构造旋转四元数
    quat_t dq;
    dq.w = cos_half;
    dq.x = wx * sin_half;
    dq.y = wy * sin_half;
    dq.z = wz * sin_half;
    
    // 四元数乘法
    quat_t result;
    result.w = q->w*dq.w - q->x*dq.x - q->y*dq.y - q->z*dq.z;
    result.x = q->w*dq.x + q->x*dq.w + q->y*dq.z - q->z*dq.y;
    result.y = q->w*dq.y - q->x*dq.z + q->y*dq.w + q->z*dq.x;
    result.z = q->w*dq.z + q->x*dq.y - q->y*dq.x + q->z*dq.w;
    
    // 归一化
    float norm = sqrtf(result.w*result.w + result.x*result.x + 
                       result.y*result.y + result.z*result.z);
    q->w = result.w / norm;
    q->x = result.x / norm;
    q->y = result.y / norm;
    q->z = result.z / norm;
}

工程经验:注意代码里那个if判断。当角速度很小时,直接跳过更新。这能避免除零错误,也能减少不必要的计算。我在实际测试中发现,这个阈值设在1e-6比较合适。

本章知识体系

为了让你更直观地理解四元数微分方程在整个惯性导航中的位置,我画了张图:

四元数微分方程知识体系 陀螺仪角速度 ω 四元数微分方程 dq/dt = 0.5·q⊗ω 解析解(恒定角速度) 数值解(实际工程) 欧拉法 RK4法 解析离散法 更新后的四元数 q 归一化 q = q/||q||

这张图把整个流程串起来了。从陀螺仪读到的角速度,经过四元数微分方程,选择适当的数值解法,最后得到更新后的姿态四元数。别忘了归一化那一步,它虽然小,但至关重要。

好了,四元数微分方程的内容就讲到这里。记住:理解它的物理意义比死记公式更重要。下次你写代码时,想想这个方程到底在干什么——它就是在告诉你,角速度如何驱动姿态变化。


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