3、飞行动力学基础:刚体运动方程、牛顿-欧拉方程、空气动力学简化模型

好,咱们进入第三讲。说实话,飞行动力学是很多做控制的人觉得「劝退」的一章。为什么?因为公式多、概念杂,而且看起来跟控制算法没啥直接关系。

但我得说句实话:你如果不懂飞机怎么动,你写的控制律就是空中楼阁。我在项目里见过太多人,PID参数调得飞起,结果真机一飞就炸——原因很简单,他压根没搞懂飞机在空中的运动规律。

这一章,咱们就把飞行动力学的核心骨架拆出来。不搞复杂的推导,只讲你写代码、调参数真正需要的东西。

3.1 刚体运动方程:飞机到底怎么动?

先问个问题:一架四旋翼在空中,它有几个自由度?

答案是6个。3个平动(前后、左右、上下),3个转动(俯仰、滚转、偏航)。这就是所谓的「六自由度刚体运动」。

我个人习惯把运动方程拆成两部分来看:

  • 运动学:描述位置和姿态怎么随时间变化,跟力没关系
  • 动力学:描述力和力矩怎么产生加速度和角加速度

先看运动学。位置变化很简单:

// 位置变化率 = 速度
p_dot = v

姿态变化稍微麻烦点。我建议用四元数,别用欧拉角。为什么?欧拉角有万向锁问题,而且插值不线性。我在做倾转旋翼机项目时,就吃过欧拉角的亏——飞到90度俯仰时,姿态解算直接炸了。

四元数运动学方程:

// q = [qw, qx, qy, qz] 是单位四元数
// ω = [p, q, r] 是机体角速度
q_dot = 0.5 * q ⊗ ω

这里 ⊗ 表示四元数乘法。代码实现时,我习惯写成矩阵形式:

void quaternion_kinematics(float q[4], float omega[3], float q_dot[4]) {
    q_dot[0] = -0.5 * (q[1]*omega[0] + q[2]*omega[1] + q[3]*omega[2]);
    q_dot[1] =  0.5 * (q[0]*omega[0] - q[3]*omega[1] + q[2]*omega[2]);
    q_dot[2] =  0.5 * (q[3]*omega[0] + q[0]*omega[1] - q[1]*omega[2]);
    q_dot[3] = -0.5 * (q[2]*omega[0] - q[1]*omega[1] - q[0]*omega[2]);
}
我的经验:每次更新完四元数,记得归一化。数值误差会让它慢慢偏离单位长度,飞久了姿态会漂。我一般每10个控制周期做一次归一化。

3.2 牛顿-欧拉方程:力和力矩怎么算?

好,运动学搞定了。接下来是动力学——说白了就是牛顿第二定律的空中版。

平动部分(牛顿方程):

m * a = F_gravity + F_thrust + F_aero

这里要注意坐标系。重力在地面系,推力和气动力在机体系。我建议统一转到地面系再算加速度,这样位置积分方便。

转动部分(欧拉方程):

J * ω_dot = M - ω × (J * ω)

其中 J 是转动惯量矩阵,M 是合力矩。这个 × 是叉乘,在代码里要小心实现:

void euler_dynamics(float J[3][3], float omega[3], float M[3], float omega_dot[3]) {
    float J_omega[3];
    mat_vec_mul(J, omega, J_omega);  // J * ω
    
    float cross[3];
    cross_product(omega, J_omega, cross);  // ω × (J*ω)
    
    float temp[3];
    for(int i=0; i<3; i++) temp[i] = M[i] - cross[i];
    
    // 解 J * ω_dot = temp
    solve_linear_system(J, temp, omega_dot);
}
注意:转动惯量 J 不是标量!对于四旋翼,通常假设 J 是对角矩阵(Ixx, Iyy, Izz),但实际飞机会有耦合项。我见过有人偷懒设成对角阵,结果偏航和滚转耦合时控制律完全失效。

3.3 空气动力学简化模型:别被复杂公式吓到

说到空气动力学,很多做控制的同学就头疼。其实对于飞行控制来说,你不需要搞CFD仿真,只需要一个能用的简化模型。

我个人常用的简化思路:

  • 升力:跟迎角成正比,跟速度平方成正比
  • 阻力:跟速度平方成正比,方向与速度相反
  • 力矩:跟角速度成正比(阻尼项)

对于固定翼,升力系数可以近似为:

CL = CL0 + CL_alpha * alpha

其中 alpha 是迎角。对于多旋翼,更简单——升力直接跟螺旋桨转速平方成正比:

F_thrust = k_f * omega_motor^2
M_torque = k_m * omega_motor^2

这里 k_f 和 k_m 是螺旋桨的力系数和力矩系数,一般通过实验标定得到。

参数 物理意义 典型值(四旋翼)
k_f 力系数 (N/(rad/s)^2) 1.0e-5 ~ 1.0e-4
k_m 力矩系数 (Nm/(rad/s)^2) 1.0e-7 ~ 1.0e-6
CL_alpha 升力线斜率 (1/rad) 4.0 ~ 6.0

避坑指南:我曾经在调试一架大型四旋翼时,发现悬停时电机转速比理论值高了20%。查了半天,原来是螺旋桨的 k_f 值在低转速和高转速下不一样。后来我改用分段线性模型,问题就解决了。

所以我的建议是:别迷信一个固定的 k_f。有条件的话,做一下转速-拉力标定曲线。

3.4 把这些方程串起来:仿真框架

好了,现在我们把所有东西拼在一起。一个完整的飞行动力学仿真,大概长这样:

// 伪代码:一个控制周期
void dynamics_step(float dt) {
    // 1. 计算力和力矩
    float F[3], M[3];
    compute_forces_and_torques(state, controls, &F, &M);
    
    // 2. 平动动力学(地面系)
    float acc[3];
    acc[0] = F[0] / mass + gravity[0];  // 包含重力
    acc[1] = F[1] / mass + gravity[1];
    acc[2] = F[2] / mass + gravity[2];
    
    // 3. 转动动力学(机体系)
    float omega_dot[3];
    euler_dynamics(J, state.omega, M, omega_dot);
    
    // 4. 积分更新
    state.vel += acc * dt;
    state.pos += state.vel * dt;
    state.omega += omega_dot * dt;
    
    // 5. 姿态更新(四元数)
    quaternion_kinematics(state.q, state.omega, q_dot);
    state.q += q_dot * dt;
    normalize_quaternion(&state.q);
}

这个框架看起来简单,但实际用的时候有几个坑:

  • 积分方法:我用的是最简单的欧拉法,但如果你要精度高,建议用四阶龙格-库塔
  • 坐标系转换:力和力矩一定要搞清楚是在哪个坐标系下算的
  • 数值稳定性:dt 太大时,欧拉法会发散。我一般控制在 1ms 以内
我的习惯:在仿真里故意加一些噪声和延迟,模拟真实传感器。比如 IMU 加个高斯噪声,控制指令加个 5ms 延迟。这样仿真出来的结果,跟真机飞起来更接近。

嗯,这一章内容不少。但说白了,飞行动力学就是「力产生加速度,加速度积分成速度,速度积分成位置」——只不过在三维空间里,多了旋转和坐标系变换。

下一章我们会讲控制算法本身。但在这之前,我建议你把这一章的仿真框架自己写一遍。代码跑通了,你对飞机怎么动就有了直觉。这个直觉,比背一百个公式都管用。