news 2026/10/20 12:46:34

C++从零实现eVTOL六自由度仿真:动力学、控制与推力分配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++从零实现eVTOL六自由度仿真:动力学、控制与推力分配

简介:eVTOL_Simulation 是一份面向 C++ 学习者与仿真爱好者的轻量级项目源码,围绕电动垂直起降飞行器的调度场景展开。程序设定 5 家公司共 20 架飞机同时起飞,每架飞机拥有巡航速度、电池容量、充电时间、巡航能耗与每小时故障概率等参数,电量耗尽后依次前往仅配备 3 个充电桩的充电站补能,充满后重新起飞,并以三分钟模拟实际三小时,飞行速度保持恒定。资源包共 21 个文件,以 8 个 cc 源文件与 7 个 h 头文件为核心,辅以 makefile 构建脚本、company_data 与 globa_data 数据文件、README 说明及状态示意图,压缩包约 115KB,结构紧凑。项目采用单线程实现,通过飞机级 FLY、充电、等待状态与充电站级队列调度,配合单元测试校验飞行、等待、充电帧数之和是否等于总帧数,便于理解状态机与队列调度思路。目前已有 335 人学习,适合作为 C++ 面向对象建模与离散事件仿真的练手案例。

1. eVTOL_Simulation:用 C++ 把一架电动垂直起降飞行器搬进终端里

很多人第一次接触 eVTOL 这个词,是在低空经济、城市空中交通的新闻里,但真到动手环节,能跑起来的开源仿真项目并不多。eVTOL_Simulation 就是冲着这个空档来的:它用 C++ 从零搭了一套电动垂直起降飞行器的仿真骨架,把六自由度动力学、电机推力分配、姿态控制和状态输出串成一条完整链路。你不需要真机、不需要飞控硬件,编译完就能在终端里看到飞行器从悬停到前飞的状态变化。它适合两类人:一类是想搞懂 eVTOL 飞控逻辑但不想被硬件门槛卡住的在校学生和爱好者,另一类是想拿一套干净 C++ 工程做二次开发、验证自己控制算法的工程师。整个项目不依赖重型仿真平台,纯 C++ 实现,编译快、改起来也快。

2. 工程结构与技术选型:为什么是纯 C++ 而不是上仿真平台

2.1 目录布局与模块划分

拿到源码包,第一件事是看目录。eVTOL_Simulation 的结构很直白,没有花哨的分层,基本是按“物理模型—控制—仿真循环—输出”这条线切的:

eVTOL_Simulation/ ├── include/ │ ├── dynamics.h // 六自由度动力学模型 │ ├── motor_model.h // 电机与推力分配 │ ├── controller.h // 姿态/位置控制器 │ ├── vehicle_state.h // 状态量结构体定义 │ └── simulator.h // 仿真主循环与积分器 ├── src/ │ ├── dynamics.cpp │ ├── motor_model.cpp │ ├── controller.cpp │ ├── simulator.cpp │ └── main.cpp ├── config/ │ └── vehicle_params.json // 质量、惯量、旋翼布局等参数 ├── CMakeLists.txt └── README.md

这个划分的好处是每个模块职责单一:dynamics只管受力算加速度,motor_model只管把控制指令翻译成各旋翼推力,controller只管根据状态误差算指令,simulator负责把时间往前推。你要换控制算法,只动controller;要改气动参数,只动config。常见做法是把参数外置成 JSON 或 YAML,避免改一个数就重编译,这个项目也是这么干的。

2.2 技术选型:Eigen 做线性代数,CMake 做构建

纯 C++ 写仿真,绕不开矩阵运算。eVTOL_Simulation 用的是 Eigen 头文件库,做四元数姿态更新、矩阵求逆、状态空间运算都很顺手,而且不用链接二进制,CMake 里find_package(Eigen3)一行就够。构建系统用 CMake,最低要求 C++17,因为用到了结构化绑定和std::optional这类特性。

cmake_minimum_required(VERSION 3.16) project(eVTOL_Simulation CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Eigen3 REQUIRED) add_executable(evtol_sim src/main.cpp src/dynamics.cpp src/motor_model.cpp src/controller.cpp src/simulator.cpp ) target_include_directories(evtol_sim PRIVATE include) target_link_libraries(evtol_sim PRIVATE Eigen3::Eigen)

这里CMAKE_CXX_STANDARD 17是硬性要求,低版本编译会报错。target_link_libraries只链 Eigen,没有其他外部依赖,所以你在任何装了 Eigen 和 CMake 的机器上都能编。参数说明:add_executable里列出的五个 cpp 缺一不可,main.cpp是入口,其余四个是模块实现。如果你要加新模块,记得在这里补上对应的 cpp,否则链接阶段会报未定义符号。

2.3 状态量与坐标系约定

仿真项目最容易翻车的地方是坐标系没统一。eVTOL_Simulation 用的是 NED(北-东-地)坐标系描述位置,机体坐标系描述力和力矩,姿态用四元数表示避免万向锁。状态结构体大致长这样:

// include/vehicle_state.h struct VehicleState { Eigen::Vector3d position; // NED 位置 (m) Eigen::Vector3d velocity; // NED 速度 (m/s) Eigen::Quaterniond attitude; // 机体到 NED 的姿态四元数 Eigen::Vector3d angular_rate; // 机体角速度 (rad/s) double motor_rpm[6]; // 六个旋翼转速 };

position和velocity都在 NED 下,attitude是从机体系转到 NED 的旋转,angular_rate是机体系下的角速度。这四组量构成了六自由度仿真的最小状态集。motor_rpm单独拎出来是因为电机动态比刚体动态快得多,常见做法是给它单独做一阶惯性环节,而不是和刚体方程一起积分。你如果只关心轨迹不关心转速细节,可以把这一项简化掉,但姿态控制会失真。

3. 核心模块拆解:从控制指令到飞行器状态更新

3.1 动力学模型:力和力矩怎么变成加速度

动力学模块是整个仿真的心脏。它接收当前状态和六个旋翼的推力,算出合力和合力矩,再根据牛顿-欧拉方程得到线加速度和角加速度。

// src/dynamics.cpp Eigen::Vector3d computeAcceleration(const VehicleState& s, const Eigen::Vector3d& force_body, double mass) { // 机体系合力转到 NED,加上重力 Eigen::Vector3d force_ned = s.attitude * force_body; Eigen::Vector3d gravity(0.0, 0.0, 9.81); // NED 下重力朝下为正 return (force_ned + gravity * mass) / mass; } Eigen::Vector3d computeAngularAcceleration(const VehicleState& s, const Eigen::Vector3d& torque_body, const Eigen::Matrix3d& inertia) { // 欧拉方程:I * omega_dot = tau - omega x (I * omega) Eigen::Vector3d gyro = s.angular_rate.cross(inertia * s.angular_rate); return inertia.inverse() * (torque_body - gyro); }

computeAcceleration里先把机体系合力用四元数转到 NED,再加重量。注意重力向量写成(0,0,9.81)而不是(0,0,-9.81),因为 NED 的 Z 轴朝下,重力沿正 Z 方向。这个符号搞反是新手最常见的错误,飞机会直接往上飘。computeAngularAcceleration里那个gyro项是陀螺力矩,低速时影响小,但做快速机动仿真时不能省。inertia是 3x3 惯量矩阵,从配置文件读入,单位是 kg·m²。

3.2 电机与推力分配:控制指令怎么分到六个旋翼

eVTOL 常见构型是六旋翼或复合翼,这个项目按六旋翼处理。控制器输出的是总推力和三轴力矩指令,需要解算成每个旋翼的转速。

// src/motor_model.cpp Eigen::Matrix<double, 6, 1> mixControl(const Eigen::Vector4d& cmd, const Eigen::Matrix<double, 6, 4>& mix_matrix) { // cmd = [总推力, 滚转力矩, 俯仰力矩, 偏航力矩] // mix_matrix 是 6x4 的分配矩阵,由旋翼布局决定 Eigen::Matrix<double, 6, 1> thrust = mix_matrix * cmd; // 推力转转速:T = k * rpm^2 Eigen::Matrix<double, 6, 1> rpm; for (int i = 0; i < 6; ++i) { rpm(i) = std::sqrt(std::max(thrust(i), 0.0) / k_thrust); } return rpm; }

mix_matrix是分配矩阵,每一列对应一个控制通道,每一行对应一个旋翼。它由旋翼的位置和旋转方向决定,通常放在配置文件里而不是硬编码。k_thrust是推力系数,单位是 N/(rpm²),从电机参数表查。std::max(thrust(i), 0.0)是防止解算出负推力——旋翼不能反转,负推力直接截断到零。这里有个细节:如果分配矩阵不可逆或者条件数很差,解出的转速会剧烈震荡,表现为飞行器抖动。排查方法是打印mix_matrix的奇异值,条件数超过 100 就要重新设计旋翼布局。

3.3 控制器:串级 PID 怎么稳住姿态

控制器用的是串级 PID:外环位置控制输出期望姿态,内环姿态控制输出力矩指令。这是多旋翼飞控最成熟的方案,参数好调,也好理解。

// src/controller.cpp Eigen::Vector4d Controller::update(const VehicleState& s, const Eigen::Vector3d& target_pos) { // 外环:位置误差 -> 期望速度 -> 期望加速度 Eigen::Vector3d pos_err = target_pos - s.position; Eigen::Vector3d vel_target = pos_pid_.compute(pos_err, s.velocity); Eigen::Vector3d acc_target = vel_target - Eigen::Vector3d(0, 0, 9.81); // 期望推力(机体系 Z 轴) double thrust = mass_ * acc_target.norm(); // 内环:姿态误差 -> 力矩 Eigen::Quaterniond att_target = computeTargetAttitude(acc_target); Eigen::Vector3d att_err = quaternionToError(s.attitude, att_target); Eigen::Vector3d torque = att_pid_.compute(att_err, s.angular_rate); return Eigen::Vector4d(thrust, torque.x(), torque.y(), torque.z()); }

pos_pid_和att_pid_是两个 PID 对象,增益从配置文件读。acc_target里减掉(0,0,9.81)是因为悬停时推力要抵消重力,期望加速度是零,但推力不能是零。computeTargetAttitude根据期望加速度方向反推机体 Z 轴朝向,这是几何控制的标准做法。quaternionToError把姿态误差转成旋转向量,避免四元数双覆盖问题。调参顺序是先调内环姿态,再调外环位置,内环带宽至少是外环的三到五倍,否则两个环会互相打架,表现为低频振荡。

3.4 仿真主循环:积分器怎么选、步长怎么定

主循环负责把时间往前推,每一步调用控制器、电机模型、动力学,然后积分状态。

// src/simulator.cpp void Simulator::step(double dt) { // 1. 控制器算指令 Eigen::Vector4d cmd = controller_.update(state_, target_pos_); // 2. 推力分配 auto rpm = mixControl(cmd, mix_matrix_); // 3. 电机一阶惯性 for (int i = 0; i < 6; ++i) { motor_rpm_[i] += (rpm(i) - motor_rpm_[i]) * dt / motor_tau_; } // 4. 动力学 Eigen::Vector3d force_body = computeBodyForce(motor_rpm_); Eigen::Vector3d torque_body = computeBodyTorque(motor_rpm_); Eigen::Vector3d acc = computeAcceleration(state_, force_body, mass_); Eigen::Vector3d ang_acc = computeAngularAcceleration(state_, torque_body, inertia_); // 5. 半隐式欧拉积分 state_.velocity += acc * dt; state_.position += state_.velocity * dt; state_.angular_rate += ang_acc * dt; state_.attitude = integrateQuaternion(state_.attitude, state_.angular_rate, dt); }

积分器用的是半隐式欧拉,先更新速度再更新位置,比显式欧拉稳定。dt一般取 0.001 秒,也就是 1 kHz,和真实飞控的调度频率对齐。motor_tau_是电机时间常数,典型值 0.02 到 0.05 秒。如果你把dt调到 0.01 秒以上,姿态环会发散,因为积分误差累积太快。常见做法是物理步长固定 1 ms,控制步长可以放宽到 5 ms,中间用零阶保持。这个项目为了简单,物理和控制同频,你二次开发时可以拆开。

4. 编译与运行:从 clone 到看见第一条轨迹

4.1 环境准备与依赖安装

在 Ubuntu 或 macOS 上,先装 CMake、编译器和 Eigen。Eigen 是头文件库,装完不用配环境变量。

# Ubuntu / Debian sudo apt update sudo apt install -y build-essential cmake libeigen3-dev # macOS (Homebrew) brew install cmake eigen

Windows 上建议用 MSYS2 或 WSL,原生 MSVC 也能编,但 Eigen 路径要手动指定。装完用cmake --version和ls /usr/include/eigen3确认一下。如果find_package(Eigen3)报找不到,多半是 Eigen 装到了非标准路径,在 CMake 命令里加-DEigen3_DIR=/your/path/share/eigen3/cmake就行。

4.2 编译与首次运行

git clone <repo_url> eVTOL_Simulation cd eVTOL_Simulation mkdir build && cd build cmake .. make -j4 ./evtol_sim

编译大概十几秒,取决于机器。运行后终端会按固定间隔打印状态,包括时间、位置、姿态四元数和各旋翼转速。默认场景是起飞到 10 米悬停,然后前飞 50 米。如果你看到位置 Z 轴一直增大不收敛,检查重力符号;如果姿态四元数模长偏离 1,检查四元数积分有没有归一化。

4.3 配置文件怎么改

config/vehicle_params.json控制所有物理参数,改完不用重编译,直接重跑就行。

{ "mass": 12.5, "inertia": [0.8, 0.0, 0.0, 0.0, 1.2, 0.0, 0.0, 0.0, 1.5], "motor_tau": 0.03, "k_thrust": 1.2e-5, "rotor_positions": [ [0.35, 0.35, 0.0], [-0.35, 0.35, 0.0], [0.35, -0.35, 0.0], [-0.35, -0.35, 0.0], [0.0, 0.5, 0.0], [0.0, -0.5, 0.0] ] }

mass单位 kg,inertia是 3x3 矩阵按行展开,k_thrust单位 N/(rpm²),rotor_positions是每个旋翼在机体系的位置。改rotor_positions会直接影响分配矩阵,进而改变控制效果。比如你把某个旋翼往外移,它产生的力矩臂变长,同样的推力能产生更大力矩,但也会让分配矩阵条件数变差。改完参数建议先跑悬停场景,确认能稳住再试机动。

5. 避坑与排查:那些让仿真跑不起来的细节

5.1 现象:飞行器一启动就往上飘,位置不收敛

原因:重力符号写反了。NED 坐标系下重力沿正 Z 方向,如果写成(0,0,-9.81),等于给了一个向上的力,飞行器自然往上飞。解决:检查computeAcceleration里的重力向量,确保是(0,0,9.81),并且推力方向在机体系是负 Z(因为机体 Z 轴朝下,推力朝上)。

5.2 现象:姿态四元数模长越来越偏离 1

原因:四元数积分后没有归一化。数值积分会引入误差,跑几千步后模长可能变成 1.001 或 0.999,虽然小,但会累积。解决:在integrateQuaternion返回前加一行q.normalize()。这是血泪经验,不加的话跑长航时仿真,姿态会慢慢漂。

5.3 现象:编译报错 “undefined reference to Eigen::...”

原因:Eigen 是头文件库,但 CMake 里没正确链接。常见情况是find_package找到了,但target_link_libraries漏了Eigen3::Eigen。解决:确认 CMakeLists 里有target_link_libraries(evtol_sim PRIVATE Eigen3::Eigen),并且target_include_directories包含了 Eigen 的头文件路径。

5.4 现象:控制输出剧烈震荡,旋翼转速在 0 和最大值之间跳

原因:分配矩阵条件数太大,或者 PID 增益过高。分配矩阵条件数大意味着某些方向的力矩需要很大的转速差才能产生,解算时容易饱和。解决:先打印mix_matrix的奇异值,条件数超过 100 就调整旋翼布局;如果布局没问题,把姿态环的 P 增益降到原来的三分之一,再逐步加。

5.5 现象:仿真跑几百步后数值爆炸,位置变成 NaN

原因:积分步长太大,或者某个参数单位搞错。比如k_thrust如果误填成 1.2e-3,推力会大两个数量级,加速度爆表。解决:先确认dt是 0.001 而不是 0.01,再逐项核对配置文件里每个参数的单位。常见做法是在main.cpp里加一个参数打印,启动时把所有关键参数输出一遍,方便对照。

6. 二次开发:换控制算法、加风扰、接可视化

拿到这套代码,最值的改造方向有三个:换控制算法、加环境扰动、接可视化。换控制算法最直接,controller.cpp里的update函数就是接口,你把它换成 LQR、MPC 或者滑模,只要输入输出不变,其他模块不用动。我一般会先写一个单元测试,给定固定状态和期望位置,对比新旧控制器的输出,确认量纲和符号一致再上仿真。

加风扰也不难,在dynamics.cpp的computeAcceleration里加一个风速项就行。常见做法是用 Dryden 模型生成连续风,或者简单点用常值风加高斯噪声。注意风是在 NED 下定义的,要转到机体系再和推力叠加。加完风扰后,悬停场景会看到位置有小幅漂移,这是正常的,正好用来验证控制器的抗扰能力。

接可视化是提升调试效率的关键。终端打印状态只能看数字,看不出姿态变化。最省事的方案是把状态按 CSV 格式输出,用 Python 的 matplotlib 画三维轨迹和姿态曲线。如果你想要实时可视化,可以接 ROS 2 或者轻量级的 ImGui,但那样依赖就重了。我的习惯是先用 CSV 离线看,确认逻辑对了再考虑实时。

// 在 Simulator::step 末尾加 CSV 输出 if (log_file_.is_open()) { log_file_ << t_ << "," << state_.position.x() << "," << state_.position.y() << "," << state_.position.z() << "," << state_.attitude.w() << "," << state_.attitude.x() << "," << state_.attitude.y() << "," << state_.attitude.z() << "\n"; }

这个 CSV 每行是时间、位置三轴、姿态四元数四分量,Python 里pandas.read_csv直接读,画图十行代码搞定。注意log_file_要在构造函数里打开,析构里关闭,否则文件句柄泄漏。跑完仿真用python3 plot.py就能看到轨迹,比盯着终端强太多。

从那以后我每次改完控制器,都强制走一遍“单元测试 → 悬停 → 前飞 → 风扰”这条链路,少一步都可能漏掉符号错误或者参数单位问题。这套 eVTOL_Simulation 的骨架不复杂,但胜在干净,你可以在它上面快速验证自己的想法,不用从零搭积分器和分配矩阵。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 12:46:23

Java动漫链接生成器:自动化链接拼接与请求验证实战

简介&#xff1a;这是一款面向动漫资源采集与自动化处理场景的轻量级工具&#xff0c;基于Java开发&#xff0c;适合具备一定Java基础、希望批量生成动漫链接并输出结构化数据的开发者使用。程序通过命令行参数控制生成数量、文件名与可选标签&#xff0c;最终输出ID递增的JSON…

作者头像 李华
网站建设 2026/10/20 12:44:24

Qt + miniblink轻量WebView封装:C++桌面应用嵌入网页实战

简介&#xff1a;本资源是一套基于Qt框架封装miniblink轻量级Web渲染引擎的完整C工程&#xff0c;面向Windows平台中高级C开发者及Qt GUI应用工程师&#xff0c;解决嵌入式WebView组件跨编译器&#xff08;MinGW与MSVC&#xff09;兼容性难题。项目提供从底层封装到界面集成的全…

作者头像 李华
网站建设 2026/10/20 12:43:23

让Agent长出“手脚”:工具调用完整实现与避坑指南

最近在折腾一个内部 Agent 项目&#xff0c;说白了就是让大模型替人干活的小助手。项目做到一半&#xff0c;撞上了几乎所有 Agent 开发者都会遇到的墙&#xff1a;模型很聪明&#xff0c;但没有“手”。它能把你的需求理解得丝丝入扣&#xff0c;可一旦需要查实时数据、算一个…

作者头像 李华
网站建设 2026/10/20 12:43:01

从零手写神经网络:深度学习底层能力进阶指南

最近在社区里看到不少人在问同一个问题&#xff1a;学深度学习到底该从哪儿开始&#xff1f;框架教程刷了一堆&#xff0c;却总觉得像在看说明书&#xff0c;能跑起来&#xff0c;但不明白里面发生了什么。我花了两周时间&#xff0c;把一位顶尖AI研究者公开分享的技能清单从头…

作者头像 李华
网站建设 2026/10/20 12:42:58

AI生成App原型实测:Text-to-Prototype与D2C工具的真实能力边界

一句话丢给 AI&#xff0c;让它直接吐出一套能点、能跳转、能填表单的 App 原型界面&#xff0c;这事在两年前听起来像是产品经理的白日梦。但最近一年&#xff0c;从 Text-to-Prototype&#xff08;文本生成原型&#xff09;到 D2C&#xff08;Design to Code&#xff0c;设计…

作者头像 李华