
无人机仿真开发这条路上PX4、Gazebo、XRCE-DDS、QGC 这四个东西凑齐的那一刻基本就意味着你从只会飞真机跨进了能在电脑里把飞机拆了重装的阶段。我最早搭这套环境的时候光让 QGC 连上仿真器就折腾了整整两天中间踩的坑现在回头看全是低级错误——版本对不上、DDS 代理没起来、Gazebo 模型库拉不下来。所以这篇东西我不打算写成官方文档的复读机而是按一个真正从零搭过好几遍的人的视角把每个环节为什么这么做、哪里最容易翻车讲清楚。适合已经装好 Ubuntu、知道终端怎么用、但还没把仿真链路跑通的开发者也适合那些跑通了但说不清 XRCE-DDS 到底在中间干了什么的人。1. 先把四个组件的关系理清楚别急着敲命令很多人一上来就照着某个教程复制粘贴结果卡在某个报错上完全不知道是哪一层出了问题。根本原因是没有先建立整体图景。这套环境本质上是四个进程在互相说话你得先知道谁跟谁说话、用什么协议说话。1.1 四个组件各自的角色定位PX4 是飞控固件也就是大脑。它负责姿态解算、位置控制、任务调度这些核心逻辑。在仿真场景里PX4 跑的不是真实硬件上的固件而是编译出来的一个可执行文件它通过特定的通信接口和外部世界交换数据。Gazebo 是物理仿真引擎负责世界这部分。重力、碰撞、电机推力、传感器噪声全都是 Gazebo 在算。它把飞机的物理状态算出来喂给 PX4PX4 算完控制量再喂回来形成一个闭环。QGC 是地面站是遥控器加仪表盘。你在地面站上看到飞机的姿态、位置、电池状态也通过它发送起飞、降落、切换飞行模式的指令。XRCE-DDS 是中间那座桥。这是最容易让人困惑的部分。PX4 内部用的是 uORB 消息机制一种发布订阅模型各个模块之间通过 uORB 主题通信。但外部程序比如你自己写的 Offboard 控制节点不可能直接读 uORB。XRCE-DDS 的作用就是在 PX4 内部跑一个客户端在外部跑一个代理把 uORB 主题映射成 DDS 话题这样你用 ROS 2 就能直接订阅和发布 PX4 的数据了。注意很多人把 XRCE-DDS 和 MAVLink 搞混。MAVLink 是 QGC 和 PX4 之间的协议走的是串口或 UDP。XRCE-DDS 是 PX4 和 ROS 2 之间的协议。两者互不替代同时存在。1.2 数据流向的完整链路把链路拆开看是这样的Gazebo 算出飞机当前的位置和姿态通过 Gazebo 和 PX4 之间的桥接模块通常是gz_bridge或者旧版的gazebo_ros接口把传感器数据送给 PX4。PX4 拿到这些数据后跑控制算法输出电机转速指令再通过同一个桥接送回 Gazebo。Gazebo 根据电机转速更新物理状态循环继续。与此同时PX4 内部通过 XRCE-DDS 客户端把 uORB 主题发布出去外部的 XRCE-DDS 代理接收到后转成 DDS 话题。你的 ROS 2 节点订阅这些话题就能拿到飞机状态也可以发布控制指令话题反向传回 PX4。QGC 则通过 MAVLink 协议和 PX4 通信走的是 UDP 端口。默认情况下 PX4 的仿真模式会监听 14550 端口QGC 自动连接这个端口就能看到飞机。理解了这个链路后面遇到问题你就能快速定位是 Gazebo 没起来是 PX4 没连上 Gazebo是 DDS 代理没跑还是 QGC 端口不对每一层都有明确的排查方法。1.3 为什么选这套组合而不是别的有人会问为什么不用 AirSim 或者 FlightGearAirSim 偏向视觉仿真物理精度和 PX4 的原生集成不如 Gazebo 深。FlightGear 偏固定翼的气动仿真多旋翼支持一般。Gazebo 的优势在于和 ROS 生态的无缝衔接传感器插件丰富模型库庞大而且 PX4 官方对 Gazebo 的支持最完整。XRCE-DDS 取代了早期的 MAVROS 方案。MAVROS 走的是 MAVLink 转 ROS 话题延迟高、消息类型受限。XRCE-DDS 直接在 uORB 层面做映射延迟低、支持的消息类型几乎覆盖全部 uORB 主题。如果你要做 Offboard 控制或者多机编队XRCE-DDS 是更好的选择。2. 环境搭建版本匹配比什么都重要这一节是我踩坑最多的地方。Ubuntu 版本、ROS 2 版本、Gazebo 版本、PX4 版本这四个东西的兼容性矩阵如果你不提前查清楚后面会浪费大量时间在解决依赖冲突上。2.1 Ubuntu 与 ROS 2 的版本选择截至我写这篇内容的时候最稳妥的组合是 Ubuntu 22.04 加 ROS 2 Humble。Humble 是 LTS 版本维护周期长社区资料多。Ubuntu 24.04 加 ROS 2 Jazzy 也可以但部分 PX4 相关的 ROS 包可能还没完全适配遇到问题搜到的解决方案会少很多。安装 ROS 2 Humble 的步骤这里不展开官方文档写得很清楚。但有一个细节要注意安装类型选ros-humble-desktop而不是ros-humble-ros-base因为后面编译 PX4 的 ROS 2 接口需要一些额外的工具包desktop 版本省得你一个个补装。安装完成后验证一下source /opt/ros/humble/setup.bash ros2 topic list如果能看到/parameter_events和/rosout两个话题说明 ROS 2 基础环境没问题。2.2 Gazebo 的安装与版本坑Ubuntu 22.04 默认的 Gazebo 版本是 Fortress也叫 Ignition Gazebo。但 PX4 对 Gazebo 的支持分两个阶段旧版用的是 Gazebo Classicgazebo11新版用的是 Gazebo SimFortress 或 Garden。这里有个关键决策点你到底用 Gazebo Classic 还是 Gazebo Sim我的建议是如果你用的是 PX4 v1.14 及以上版本直接用 Gazebo SimFortress。PX4 官方从 v1.14 开始主推 Gazebo SimGazebo Classic 的支持在逐步弱化。但如果你参考的教程比较老或者你用的 PX4 版本低于 v1.14那就得用 Gazebo Classic。安装 Gazebo Sim Fortresssudo apt install ros-humble-ros-gz这个包会把 Gazebo Sim 和 ROS 2 的桥接一起装上。验证安装gz sim --version如果输出了版本号说明安装成功。如果提示命令找不到试试ign gazebo --versionFortress 版本的命令名还是ign。提示Gazebo Classic 和 Gazebo Sim 可以共存但环境变量会冲突。如果你两个都装了记得在切换时清理GAZEBO_MODEL_PATH和IGN_GAZEBO_RESOURCE_PATH这些变量。2.3 PX4 源码的获取与子模块更新PX4 的源码必须用 git clone 加递归子模块的方式获取直接下载 zip 包会缺少子模块导致编译失败。git clone https://github.com/PX4/PX4-Autopilot.git --recursive cd PX4-Autopilot git submodule update --init --recursive子模块更新这一步非常关键而且经常因为网络问题失败。如果中途断了重新执行git submodule update --init --recursive就行git 会接着上次的进度继续。克隆完成后切到一个稳定的 tag 而不是 main 分支。main 分支随时在变可能引入未测试的改动。比如git checkout v1.14.3 git submodule update --init --recursive2.4 编译 PX4 仿真目标PX4 的仿真编译和硬件编译是分开的。仿真目标不需要交叉编译工具链直接在本地编译就行。make px4_sitl这个命令会编译 SITLSoftware In The Loop版本的 PX4。第一次编译会比较久大概十几分钟取决于你的机器性能。编译完成后可执行文件在build/px4_sitl_default/bin/目录下。如果你要用 Gazebo Sim 而不是 Gazebo Classic编译命令是make px4_sitl gz_x500这个命令会同时启动 PX4 和 Gazebo Sim加载 x500 机型。第一次运行会自动下载 Gazebo 的模型资源如果网络不好可能会卡住。可以提前设置模型路径或者手动下载模型库。3. XRCE-DDS 代理的编译与配置XRCE-DDS 是整条链路里最容易被忽略但出问题最多的环节。很多人 PX4 和 Gazebo 都跑起来了QGC 也能连上但 ROS 2 就是收不到数据八成是 XRCE-DDS 代理没配对。3.1 代理的两种获取方式第一种方式是直接用 apt 安装sudo apt install ros-humble-xrce-dds-agent这种方式最省事但版本可能不是最新的。第二种方式是从源码编译git clone https://github.com/eProsima/Micro-XRCE-DDS-Agent.git cd Micro-XRCE-DDS-Agent mkdir build cd build cmake .. make sudo make install sudo ldconfig /usr/local/lib/源码编译的好处是你能拿到最新版本而且可以自己改配置。缺点是编译过程中可能遇到依赖缺失需要手动装libasio-dev和libtinyxml2-dev。3.2 代理的启动参数详解启动代理的基本命令是MicroXRCEAgent udp4 -p 8888这里的udp4表示用 IPv4 的 UDP 传输-p 8888是监听端口。PX4 那边的 XRCE-DDS 客户端默认也是连 8888 端口所以两边要对上。如果你想用串口而不是 UDP命令是MicroXRCEAgent serial --dev /dev/ttyUSB0 -b 921600但在仿真场景下UDP 更方便不需要额外的硬件。注意代理必须在 PX4 启动之前或者同时启动。如果 PX4 先启动了但代理还没起来PX4 的 XRCE-DDS 客户端会不断重试连接通常几秒后就能连上。但如果代理一直没起来PX4 会报错但不会崩溃只是 ROS 2 那边收不到任何数据。3.3 验证 DDS 链路是否打通代理启动后另开一个终端source ROS 2 环境然后ros2 topic list如果能看到/fmu/out/vehicle_attitude、/fmu/out/vehicle_local_position这些话题说明 DDS 链路已经通了。如果只看到/parameter_events和/rosout说明代理没连上 PX4检查端口和启动顺序。还可以用ros2 topic echo看具体数据ros2 topic echo /fmu/out/vehicle_attitude --once如果输出了四元数数据说明整条链路完全正常。3.4 常见连接失败的原因排查连接失败通常有这几个原因端口被占用、防火墙拦截、PX4 的 XRCE-DDS 客户端没启动、代理版本和 PX4 内置客户端版本不兼容。排查顺序是这样的先用netstat -tulnp | grep 8888看端口有没有被占用。然后确认 PX4 启动日志里有没有XRCE-DDS client started这样的输出。如果 PX4 那边没启动客户端检查编译时有没有加CONFIG_XRCE_DDS相关的选项默认的 SITL 编译是包含的。版本不兼容的表现是连上了但话题列表为空或者某些话题有数据某些没有。这种情况换一个代理版本试试通常用 PX4 官方推荐的那个版本最稳。4. QGC 连接与 Offboard 模式实战前面三节把链路搭通了这一节讲怎么真正用起来。QGC 连接是最直观的验证方式Offboard 模式则是这套环境最有价值的应用场景。4.1 QGC 的安装与自动连接QGC 有 AppImage 和源码编译两种安装方式。AppImage 最省事chmod x QGroundControl.AppImage ./QGroundControl.AppImage启动后 QGC 会自动扫描 UDP 端口。PX4 SITL 默认在 14550 端口发送 MAVLink 数据QGC 通常能自动连上。如果没自动连在 Application Settings 里手动添加一个 UDP 连接端口填 14550。连接成功后你会在 QGC 左上角看到飞机的姿态指示器开始动说明 Gazebo 里的飞机状态已经传到 QGC 了。提示如果你同时跑了多个 PX4 实例比如多机仿真每个实例的 MAVLink 端口会不同需要手动配置 QGC 的连接端口。第一个实例通常是 14550第二个是 14560以此类推。4.2 Offboard 模式的控制逻辑Offboard 模式允许外部程序通过 MAVLink 或 XRCE-DDS 给 PX4 发送位置、速度或姿态设定值。PX4 收到这些设定值后用自己的控制器去跟踪。用 XRCE-DDS 做 Offboard 的流程是这样的ROS 2 节点发布/fmu/in/offboard_control_mode话题告诉 PX4 当前是哪种控制模式位置、速度、姿态。然后发布/fmu/in/trajectory_setpoint话题给出具体的设定值。PX4 收到后开始跟踪。关键点在于Offboard 模式有一个安全机制PX4 要求外部程序在切换到 Offboard 模式之前必须先持续发送设定值至少一秒。这是为了防止外部程序崩溃后飞机失控。4.3 一个最小可用的 Offboard 示例下面是一个用 Python 写的 ROS 2 节点让飞机起飞到 5 米高度import rclpy from rclpy.node import Node from px4_msgs.msg import OffboardControlMode, TrajectorySetpoint, VehicleCommand class OffboardControl(Node): def __init__(self): super().__init__(offboard_control) self.offboard_pub self.create_publisher(OffboardControlMode, /fmu/in/offboard_control_mode, 10) self.setpoint_pub self.create_publisher(TrajectorySetpoint, /fmu/in/trajectory_setpoint, 10) self.command_pub self.create_publisher(VehicleCommand, /fmu/in/vehicle_command, 10) self.timer self.create_timer(0.1, self.timer_callback) self.counter 0 def timer_callback(self): offboard_msg OffboardControlMode() offboard_msg.position True offboard_msg.timestamp self.get_clock().now().nanoseconds // 1000 self.offboard_pub.publish(offboard_msg) setpoint_msg TrajectorySetpoint() setpoint_msg.position [0.0, 0.0, -5.0] setpoint_msg.yaw 0.0 setpoint_msg.timestamp self.get_clock().now().nanoseconds // 1000 self.setpoint_pub.publish(setpoint_msg) self.counter 1 if self.counter 10: self.arm() self.set_offboard_mode() def arm(self): cmd VehicleCommand() cmd.command VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM cmd.param1 1.0 cmd.target_system 1 cmd.target_component 1 cmd.source_system 1 cmd.source_component 1 cmd.from_external True cmd.timestamp self.get_clock().now().nanoseconds // 1000 self.command_pub.publish(cmd) def set_offboard_mode(self): cmd VehicleCommand() cmd.command VehicleCommand.VEHICLE_CMD_DO_SET_MODE cmd.param1 1.0 cmd.param2 6.0 cmd.target_system 1 cmd.target_component 1 cmd.source_system 1 cmd.source_component 1 cmd.from_external True cmd.timestamp self.get_clock().now().nanoseconds // 1000 self.command_pub.publish(cmd) def main(): rclpy.init() node OffboardControl() rclpy.spin(node) rclpy.shutdown()这段代码的逻辑是先持续发送 OffboardControlMode 和 TrajectorySetpoint计数器到 10也就是一秒后发送解锁指令和切换 Offboard 模式指令。飞机解锁后会按照设定值飞到 5 米高度。注意px4_msgs这个包需要单独编译。从 PX4 源码的msg/目录下可以找到消息定义用ros2 pkg create创建包后把消息文件放进去编译。4.4 Offboard 模式没反应的排查思路QGC 上切 Offboard 模式没反应或者飞机不动通常有这几个原因第一设定值发送频率不够。PX4 要求 Offboard 设定值的发送频率至少 2Hz低于这个频率会自动退出 Offboard 模式。上面代码里定时器是 0.1 秒也就是 10Hz足够了。第二时间戳不对。PX4 对消息的时间戳有要求必须是微秒级的 Unix 时间。用self.get_clock().now().nanoseconds // 1000获取的是 ROS 时间如果 ROS 用的是仿真时间而不是系统时间时间戳会对不上。解决办法是设置use_sim_time参数为 false或者用系统时间。第三飞机没解锁。Offboard 模式必须在飞机解锁后才能切换。如果解锁指令没发成功切模式也会失败。可以在 QGC 上手动解锁试试如果手动能解锁说明是代码问题如果手动也不行说明是飞控状态不对。第四遥控器开关没打到 Offboard 档位。如果你用了真实遥控器需要把模式切换开关打到对应位置。纯仿真环境下可以忽略这条。5. 仿真环境中的模型与场景定制跑通默认的 x500 机型只是开始实际项目里你往往需要自定义机型、添加传感器、搭建特定场景。这一节讲怎么改。5.1 机型配置文件的修改PX4 的机型定义文件在ROMFS/px4fmu_common/init.d-posix/airframes/目录下。每个机型一个文件比如4001_gz_x500就是 x500 在 Gazebo Sim 下的配置。文件内容大致是这样的#!/bin/sh PX4_SIM_MODELgz_x500PX4_SIM_MODEL这个变量告诉 PX4 用哪个 Gazebo 模型。如果你想用自己的模型把模型文件放到 Gazebo 的模型搜索路径下然后改这个变量。Gazebo 模型的搜索路径由GZ_SIM_RESOURCE_PATH环境变量控制。你可以把自己的模型目录加进去export GZ_SIM_RESOURCE_PATH$GZ_SIM_RESOURCE_PATH:~/my_models5.2 添加传感器插件给飞机加一个深度相机需要在模型的 SDF 文件里添加传感器定义。找到模型目录下的model.sdf在model标签内添加sensor namedepth_camera typedepth_camera update_rate30/update_rate camera horizontal_fov1.047/horizontal_fov image width640/width height480/height /image clip near0.1/near far10.0/far /clip /camera /sensor然后在plugin部分添加对应的桥接插件把 Gazebo 的相机话题桥接到 ROS 2。Gazebo Sim 用的是ros_gz_bridgeros2 run ros_gz_bridge parameter_bridge /depth_camera/imagesensor_msgs/msg/Imagegz.msgs.Image这条命令把 Gazebo 的/depth_camera/image话题桥接成 ROS 2 的sensor_msgs/msg/Image类型。5.3 场景搭建的实用技巧Gazebo Sim 的场景文件是 SDF 格式可以手动编辑也可以从 Gazebo 的模型库下载现成模型。模型库地址在 Gazebo 的 Fuel 平台上可以直接在 Gazebo 界面里搜索和拖拽。如果你要搭建一个室内场景建议先用简单的几何体盒子、圆柱搭出大致布局跑通后再替换成精细模型。精细模型会显著增加仿真计算量如果机器性能一般帧率会掉得很厉害。提示Gazebo Sim 支持--render-engine参数切换渲染引擎。默认是 ogre2如果你的显卡驱动有问题导致画面闪烁或者黑屏可以试试换成 ogre。在 VMware 虚拟机里跑 Gazebo 经常遇到这个问题换渲染引擎通常能解决。6. 多机仿真与 ROS 2 命名空间隔离单机跑通后下一步往往是多机。多机仿真的核心问题是命名空间隔离每架飞机的 ROS 2 话题、MAVLink 端口、DDS 代理端口都要区分开。6.1 启动多个 PX4 实例PX4 支持通过-i参数指定实例编号make px4_sitl gz_x500 -i 0 make px4_sitl gz_x500 -i 1实例 0 的 MAVLink 端口是 14550实例 1 是 14560。Gazebo 那边会启动两个 x500 模型位置需要错开否则会重叠。6.2 DDS 代理的端口分配每个 PX4 实例需要一个独立的 XRCE-DDS 代理端口不能冲突MicroXRCEAgent udp4 -p 8888 MicroXRCEAgent udp4 -p 8889PX4 那边的客户端端口也要对应修改。在启动 PX4 时通过环境变量指定export PX4_XRCE_DDS_PORT88896.3 ROS 2 命名空间的使用在 ROS 2 节点里用命名空间区分不同飞机的数据self.create_subscription(VehicleAttitude, /px4_0/fmu/out/vehicle_attitude, callback, 10) self.create_subscription(VehicleAttitude, /px4_1/fmu/out/vehicle_attitude, callback, 10)或者在启动节点时用--ros-args -r __ns:/px4_0重映射命名空间。多机仿真的坑主要在于资源竞争。两个 Gazebo 实例同时跑CPU 和 GPU 占用会翻倍。如果机器性能不够仿真会变慢PX4 的时间同步会出问题。建议多机仿真时降低 Gazebo 的渲染质量或者用无头模式HEADLESS1跑。7. 那些教程里不会写的踩坑记录这一节是我自己踩过的坑按时间顺序列出来希望能帮你省点时间。7.1 Gazebo 模型下载卡住第一次运行make px4_sitl gz_x500时Gazebo 会从 Fuel 平台下载模型。国内网络环境下这一步经常卡住或者超时。解决办法是提前手动下载模型库放到~/.gz/fuel/目录下。或者设置GZ_SIM_RESOURCE_PATH指向本地已有的模型目录。如果下载一直失败可以试试用gz sim单独启动 Gazebo在界面里手动下载模型这样能看到下载进度比命令行里干等强。7.2 QGC 连上了但显示未连接QGC 界面上飞机图标是灰色的但端口配置没问题。这种情况通常是 MAVLink 心跳包没收到。检查 PX4 启动日志里有没有MAVLink started的输出。如果没有可能是编译时没包含 MAVLink 模块。默认的 SITL 编译是包含的但如果你改过编译配置可能会漏掉。另一个原因是 QGC 的版本太老不支持 PX4 当前的 MAVLink 版本。更新 QGC 到最新版通常能解决。7.3 Offboard 模式切换后飞机原地不动前面 4.4 节讲了几个原因这里补充一个飞机的初始位置和设定值位置太近。如果你设定的目标位置就是飞机当前所在位置飞机当然不动。把设定值改到明显不同的位置比如高度改成 -5.0PX4 的 NED 坐标系里 Z 轴向下为正所以 -5.0 表示向上 5 米。7.4 ROS 2 话题有数据但时间戳不对用ros2 topic echo看数据时发现timestamp字段是 0 或者一个很大的数。这是因为 PX4 用的是微秒级的 Unix 时间而 ROS 2 默认用的是系统时间。如果两边时间基准不一致数据在 RViz 里会显示不出来或者位置跳变。解决办法是在 ROS 2 节点里用self.get_clock().now().nanoseconds // 1000获取时间戳并确保use_sim_time参数设置正确。如果 Gazebo 用的是仿真时间ROS 2 也要用仿真时间两边要一致。7.5 编译 PX4 时内存不足PX4 编译比较吃内存尤其是并行编译的时候。如果你的机器内存小于 8GB可能会遇到编译器被 kill 的情况。解决办法是减少并行编译的线程数make px4_sitl -j2-j2表示只用两个线程编译。虽然慢一点但不会因为内存不足而失败。8. 从仿真到实机的过渡要点仿真跑通后最终目标往往是部署到真实飞机上。仿真和实机的差异主要体现在传感器噪声、通信延迟、电机响应特性这几个方面。仿真里的传感器是理想的没有噪声。实机上 IMU 有零偏气压计有漂移磁力计有干扰。如果你在仿真里调了一套参数直接用到实机上大概率会出问题。建议在仿真里加入传感器噪声模型让仿真环境更接近真实。通信延迟方面仿真里 XRCE-DDS 的延迟通常在毫秒级实机上通过数传电台的延迟可能到几十毫秒甚至上百毫秒。如果你的 Offboard 控制对延迟敏感需要在仿真里人为加入延迟来测试鲁棒性。电机响应特性方面仿真里的电机模型是简化的实机上电机有启动时间、转速波动、电池电压下降导致的推力变化。这些在仿真里很难完全模拟但可以通过调整 Gazebo 的电机模型参数来近似。我个人的经验是仿真主要用来验证逻辑和算法参数整定还是要在实机上做。仿真里调出来的参数可以作为初始值但不要指望直接能用。另外从仿真切换到实机时记得把PX4_SIM_MODEL相关的配置去掉改用真实的机型配置。这套环境搭起来确实麻烦但一旦跑通后面做算法开发和测试的效率会高很多。我现在的习惯是任何新的控制逻辑先在仿真里跑一遍确认没问题再上实机。这样即使代码有 bug最多是仿真里飞机乱飞不会摔真机。