2、仿真环境搭建:基于Python的简易四旋翼模型与可视化

说实话,很多初学者一上来就怼着PX4或者ArduPilot的源码啃,结果被一堆宏定义和回调函数搞得晕头转向。我个人习惯是,先搭一个自己能完全掌控的仿真环境,把核心逻辑跑通了再说。这一章,我们就用Python手撸一个简易的四旋翼模型,再配上实时可视化,让你亲眼看到PID调参的效果。

2.1 为什么选Python?

你想想看,做飞控仿真最怕什么?最怕改个参数要编译半小时,然后一运行就炸。Python的好处就是快——改完就能跑,跑完就能看。我在项目里做原型验证时,经常先用Python把算法跑通,再移植到C++里。说白了,Python就是你的“草稿纸”。

核心原则:仿真环境要满足三个条件——够简单、够直观、够快。Python + NumPy + Matplotlib 正好满足。

2.2 四旋翼的简易数学模型

嗯,这里要注意,我们不需要搞太复杂的空气动力学。对于PID调参来说,一个简化的刚体模型就够用了。核心就两个部分:动力学方程运动学方程

2.2.1 状态变量定义

我习惯用12维状态向量来描述四旋翼:

状态向量 x = [x, y, z, vx, vy, vz, phi, theta, psi, p, q, r]

其中:

  • 位置:x, y, z(世界坐标系)
  • 速度:vx, vy, vz(世界坐标系)
  • 姿态角:phi(横滚), theta(俯仰), psi(偏航)
  • 角速度:p, q, r(机体坐标系)

2.2.2 控制输入

四旋翼有4个电机,但控制输入通常抽象为4个通道:

通道 符号 说明
油门 u1 总升力,控制高度
横滚 u2 左右电机差速,控制横滚角
俯仰 u3 前后电机差速,控制俯仰角
偏航 u4 对角电机差速,控制偏航角

小技巧:实际项目中,u1到u4会映射到4个电机的PWM值。但在仿真里,我们直接把它们当作力/力矩来用,省去中间转换的麻烦。

2.3 代码实现:核心仿真类

下面是我写的一个简化版四旋翼仿真类。代码不长,但五脏俱全。

import numpy as np

class QuadrotorSim:
    def __init__(self, dt=0.01):
        self.dt = dt  # 仿真步长
        self.g = 9.81  # 重力加速度
        self.m = 1.0   # 质量
        self.I = np.diag([0.01, 0.01, 0.02])  # 转动惯量
        
        # 状态初始化
        self.x = np.zeros(12)
        self.x[2] = 1.0  # 初始高度1米
        
    def dynamics(self, x, u):
        """四旋翼动力学方程"""
        # 解包状态
        pos = x[0:3]
        vel = x[3:6]
        euler = x[6:9]
        omega = x[9:12]
        
        # 旋转矩阵(从机体到世界)
        phi, theta, psi = euler
        R = self._euler_to_rot(phi, theta, psi)
        
        # 推力(世界坐标系)
        thrust_body = np.array([0, 0, u[0]])
        thrust_world = R @ thrust_body
        
        # 加速度
        acc = thrust_world / self.m - np.array([0, 0, self.g])
        
        # 姿态角速度转换
        euler_dot = self._omega_to_euler_dot(omega, euler)
        
        # 角加速度(简化版)
        omega_dot = np.linalg.inv(self.I) @ (u[1:4] - np.cross(omega, self.I @ omega))
        
        return np.concatenate([vel, acc, euler_dot, omega_dot])
    
    def step(self, u):
        """单步仿真(RK4积分)"""
        k1 = self.dynamics(self.x, u)
        k2 = self.dynamics(self.x + 0.5*self.dt*k1, u)
        k3 = self.dynamics(self.x + 0.5*self.dt*k2, u)
        k4 = self.dynamics(self.x + self.dt*k3, u)
        self.x += (self.dt/6) * (k1 + 2*k2 + 2*k3 + k4)
        return self.x
    
    def _euler_to_rot(self, phi, theta, psi):
        """欧拉角转旋转矩阵"""
        # 标准ZYX顺序
        cphi, sphi = np.cos(phi), np.sin(phi)
        ctheta, stheta = np.cos(theta), np.sin(theta)
        cpsi, spsi = np.cos(psi), np.sin(psi)
        
        R = np.array([
            [cpsi*ctheta, cpsi*stheta*sphi - spsi*cphi, cpsi*stheta*cphi + spsi*sphi],
            [spsi*ctheta, spsi*stheta*sphi + cpsi*cphi, spsi*stheta*cphi - cpsi*sphi],
            [-stheta, ctheta*sphi, ctheta*cphi]
        ])
        return R
    
    def _omega_to_euler_dot(self, omega, euler):
        """角速度转欧拉角速率"""
        phi, theta = euler[0], euler[1]
        W = np.array([
            [1, np.sin(phi)*np.tan(theta), np.cos(phi)*np.tan(theta)],
            [0, np.cos(phi), -np.sin(phi)],
            [0, np.sin(phi)/np.cos(theta), np.cos(phi)/np.cos(theta)]
        ])
        return W @ omega

我曾经踩过的坑:欧拉角在俯仰角接近±90度时会出现万向锁。如果你要做特技飞行,建议换成四元数。不过对于常规的PID调参,欧拉角完全够用。

2.4 可视化:让数据“活”起来

光有数据不够,你得看到飞机在飞。我习惯用Matplotlib做实时3D可视化,再加一个姿态指示器。

2.4.1 3D轨迹显示

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

class Visualizer:
    def __init__(self):
        self.fig = plt.figure(figsize=(12, 5))
        
        # 3D轨迹图
        self.ax3d = self.fig.add_subplot(121, projection='3d')
        self.ax3d.set_xlim([-2, 2])
        self.ax3d.set_ylim([-2, 2])
        self.ax3d.set_zlim([0, 3])
        self.ax3d.set_xlabel('X (m)')
        self.ax3d.set_ylabel('Y (m)')
        self.ax3d.set_zlabel('Z (m)')
        
        # 姿态指示器(2D)
        self.ax2d = self.fig.add_subplot(122)
        self.ax2d.set_xlim([-1, 1])
        self.ax2d.set_ylim([-1, 1])
        self.ax2d.set_aspect('equal')
        self.ax2d.set_title('姿态指示器')
        
        # 初始化绘图元素
        self.traj_line, = self.ax3d.plot([], [], [], 'b-', lw=1)
        self.quad_body, = self.ax3d.plot([], [], [], 'ro-', lw=2)
        
    def update(self, state, trajectory):
        """更新可视化"""
        # 更新轨迹
        if len(trajectory) > 1:
            traj = np.array(trajectory)
            self.traj_line.set_data(traj[:, 0], traj[:, 1])
            self.traj_line.set_3d_properties(traj[:, 2])
        
        # 更新四旋翼机体(简化:画一个十字)
        pos = state[0:3]
        self.quad_body.set_data([pos[0]], [pos[1]])
        self.quad_body.set_3d_properties([pos[2]])
        
        plt.pause(0.01)

2.4.2 运行主循环

# 主循环示例
sim = QuadrotorSim(dt=0.01)
vis = Visualizer()
trajectory = []

# 简单的悬停控制(后面章节会讲PID)
for t in range(1000):
    # 悬停:油门补偿重力
    u = [sim.m * sim.g, 0, 0, 0]
    state = sim.step(u)
    trajectory.append(state[0:3].copy())
    
    if t % 10 == 0:  # 每10步刷新一次
        vis.update(state, trajectory)

plt.show()

2.5 知识体系总览

下面这张图概括了本章的核心逻辑。你看,从数学模型到代码实现,再到可视化,是一条完整的链路。

仿真环境搭建核心逻辑 数学模型 代码实现 可视化 12维状态向量 4通道控制输入 动力学 + 运动学 QuadrotorSim类 RK4积分器 旋转矩阵转换 3D轨迹显示 姿态指示器 实时数据刷新 目标:快速验证PID算法,看到调参效果

2.6 一些实用建议

代码写完了,但有几个点我想特别提醒你:

  • 仿真步长别太大:0.01秒是底线,再大数值就不稳了。我试过0.05秒,结果飞机直接飞出了银河系。
  • 初始状态要合理:别让飞机从0高度开始,否则重力一拉就穿地了。给个1米初始高度比较安全。
  • 可视化别每步都刷:每10步刷新一次就够了,否则Matplotlib会卡到你怀疑人生。

我的个人习惯:在仿真代码里加一个“暂停”功能。按空格键暂停/继续,这样你可以仔细看某个时刻的状态变化。调试PID时特别好用。

好了,仿真环境搭好了。下一章我们就要往里面塞PID控制器了。你想想看,当你的代码让这架虚拟飞机稳稳悬停在空中时,那种成就感——嗯,我到现在还记得第一次调通时的心情。

专注资料整理