尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
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最低要求 C17因为用到了结构化绑定和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::Matrixdouble, 6, 1 mixControl(const Eigen::Vector4d cmd, const Eigen::Matrixdouble, 6, 4 mix_matrix) { // cmd [总推力, 滚转力矩, 俯仰力矩, 偏航力矩] // mix_matrix 是 6x4 的分配矩阵由旋翼布局决定 Eigen::Matrixdouble, 6, 1 thrust mix_matrix * cmd; // 推力转转速T k * rpm^2 Eigen::Matrixdouble, 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 eigenWindows 上建议用 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单位 kginertia是 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 的骨架不复杂但胜在干净你可以在它上面快速验证自己的想法不用从零搭积分器和分配矩阵。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

技能管理实战:从技能盘点到团队能力可视化,一套可落地的评估体系

技能管理实战:从技能盘点到团队能力可视化,一套可落地的评估体系

如果你带过一支超过十人的团队,大概率经历过这种场面:新项目启动要挑人,翻简历信息早就过时了,问 leader 只能得到"他好像用过"这种模糊答案,最后派活全靠"谁嗓门大谁上",项目做完了才…

📅 2026/10/20 12:46:40
C++ eVTOL仿真工程实战:六自由度动力学与姿态控制代码拆解

C++ eVTOL仿真工程实战:六自由度动力学与姿态控制代码拆解

简介:eVTOL_Simulation 是一份面向 C 学习者与仿真编程爱好者的轻量级项目源码,围绕电动垂直起降飞行器的调度场景展开。程序设定 5 家公司共 20 架飞机同时起飞,每架飞机拥有巡航速度、电池容量、充电时间、巡航能耗及每小时故障概率等参数&…

📅 2026/10/20 12:46:40
STM32F407轻量级实时频谱分析与波形识别系统

STM32F407轻量级实时频谱分析与波形识别系统

简介:本资源是一套基于STM32平台的嵌入式频谱分析与波形识别完整工程,面向嵌入式初学者、电子类课程设计学生及STM32进阶开发者,解决低频段(50Hz–200Hz)信号实时分析与常见周期波形自动判别难题。压缩包含395个文件&a…

📅 2026/10/20 12:46:40
MORE NEWS

更多资讯

📰

宏智树AI文献综述实操:从检索筛选到成稿避坑全指南

写文献综述写到想退学的朋友,这篇内容大概率能帮到你。文献综述这四个字,对很多研究生和科研新人来说,比开题答辩还让人头大——几百篇论文堆在面前,概念定义满天飞,研究方法五花八门,从哪开始读、怎么分类…

📰

三节点集群为何挂一台能用、挂两台瘫痪?多数派协议详解

看到这个标题,我猜你八成是做后端或者运维的朋友。当年我刚开始管三节点分布式集群的时候,也问过一模一样的问题:明明有三台机器,挂一台读写一切正常,挂两台就整个服务瘫痪,这也太离谱了吧?后来…

📰

唯一ID生成实战:从UUID到雪花算法,一行代码背后的原理与坑

做后端开发的,迟早都会跟“ID”这个东西正面撞上。订单要单号,消息要消息ID,日志要traceId,分库分表之后数据库主键不能再靠自增了,分布式文件上传也得有全局不重名的文件名。麻烦永远比想象中来得早。几乎每周都能看到…

📰

PS2手柄HAL库驱动:CubeMX工程与GPIO模拟时序源码详解

简介:面向STM32开发者与嵌入式初学者的PS2手柄通信实战源码包,依托HAL库和CubeMX工具,解决从PS2协议解析到外设驱动编写的完整流程。资源包内共970个文件,以c/h源码、s汇编及icf链接配置为主,同时包含ioc配置文件、uvp…

📰

Gompertz各向异性扩散图像去噪Matlab实现与调参技巧

简介:基于Gompertz分布函数的各向异性扩散滤波Matlab代码包,专为图像去噪与边缘保持场景设计,适用于计算机、电子信息工程、数学等专业学生的课程设计、期末大作业和毕业设计。包内共15个文件,含2个m格式主程序、7个tif与6个png格…

📰

Python疗愈日志分析:情感打分与趋势可视化实践

要说明白这件事,得先亮个底:我自己有一套用Python写的疗愈日志分析脚本,它不提供诊断、不替代心理咨询,只做一件事——把我记录下来的情绪文本,转换成可以回看的趋势图、关键词和时间线。这套代码我自己断续用了一年多…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬