1. 从零搭建一套能跑 Offboard 的仿真链路,到底难在哪
如果你正在看 PX4、Gazebo、XRCE-DDS、QGC 这几个词凑在一起,大概率你已经不是第一次尝试搭仿真环境了。我见过太多人卡在同一个地方:Gazebo 里飞机能显示,QGC 也能连上,但一跑 Offboard 例程就报心跳超时,或者uxrce_dds_client死活起不来,最后折腾两三天,环境删了装、装了删,心态直接崩掉。这套链路的坑不在于某一个软件难装,而在于四个组件之间的通信关系是串联的,任何一环的版本、端口、启动顺序对不上,整条链路就是死的。
先把这套东西是什么讲清楚。PX4 是飞控固件,负责姿态控制、位置估计、任务调度;Gazebo 是物理仿真引擎,负责模拟飞机在虚拟世界里的动力学和传感器数据;XRCE-DDS 是 PX4 和外部程序之间的通信中间件,它让 PX4 内部的话题能以 DDS 的形式暴露给 ROS 2 或者其他客户端;QGC 是地面站,负责监控状态、发送指令、解锁起飞。四者串起来之后,你就能在纯软件环境里完成从解锁、起飞、Offboard 控制到降落的完整闭环,不用碰真机,不用担心炸机。
这套环境适合谁?如果你是做无人机算法验证的学生、做集群编队的工程师、或者想基于 RK3588 这类嵌入式平台做飞控二次开发的从业者,这套仿真链路就是你的日常生产力工具。它解决的核心问题是:把算法开发和飞行测试解耦,让 90% 的调试在桌面上完成,只把最后 10% 的验证留给真机。下面我按实际搭建顺序,把每个环节的选型逻辑、操作细节和踩坑经验全部摊开讲。
2. 环境整体设计与版本选型思路
2.1 为什么版本匹配比安装步骤更重要
这套环境最大的误区是“照着教程一步步敲就行”。实际上 PX4、Gazebo、ROS 2、XRCE-DDS 四者之间存在严格的版本耦合关系。举个最常见的例子:PX4 v1.14 默认搭配 Gazebo Classic 11,而 PX4 v1.15 开始主推 Gazebo Harmonic(也就是新的 Ignition Gazebo 体系)。如果你在 Ubuntu 22.04 上装了 ROS 2 Humble,又随手装了最新版 Gazebo,很可能出现 PX4 编译时找不到gz-sim相关头文件,或者仿真启动后模型加载不出来。
我的建议是先定 PX4 版本,再倒推其他组件。截至我最近一次完整搭建,比较稳的组合是:Ubuntu 22.04 + PX4 v1.14.x + Gazebo Classic 11 + ROS 2 Humble + XRCE-DDS Agent(对应 Micro XRCE-DDS 2.x)。这套组合的资料最全,社区问题最容易搜到答案。如果你非要用 Gazebo Harmonic,那 PX4 得选 v1.15 以上,ROS 2 建议 Humble 或 Jazzy,但要注意 Harmonic 的插件接口和 Classic 差异很大,很多老教程里的模型文件直接拿过来会报错。
提示:不要在同一台机器上同时装 Gazebo Classic 和 Gazebo Harmonic 的完整桌面版,两者共享部分环境变量和库路径,容易出现
gz命令指向混乱。如果确实需要,用容器隔离是最省心的做法。
2.2 四个组件的通信关系拆解
理解通信关系能帮你快速定位问题。PX4 和 Gazebo 之间通过gz_bridge(Classic 下是gazebo_ros相关插件)交换数据,PX4 把电机指令发给 Gazebo,Gazebo 把 IMU、GPS、气压计等传感器数据回传给 PX4。PX4 和 XRCE-DDS Agent 之间通过串口或 UDP 通信,Agent 再把数据转成 DDS 话题给 ROS 2。QGC 则通过 UDP 14550 端口和 PX4 的mavlink模块通信。
这里有个关键点:XRCE-DDS 和 MAVLink 是两条独立的通道。很多人以为 QGC 连上了就说明 XRCE-DDS 也通了,其实完全不是一回事。QGC 走的是 MAVLink,默认 UDP 14550;XRCE-DDS 走的是uxrce_dds_client模块,默认端口是 8888。你在排查 Offboard 问题时,要分别确认这两条链路的状态。
| 组件 | 通信对象 | 协议 | 默认端口/设备 |
|---|---|---|---|
| PX4 ↔ Gazebo | 仿真引擎 | Gazebo Transport | 内部共享内存 |
| PX4 ↔ XRCE-DDS Agent | 中间件 | XRCE-DDS over UDP | 8888 |
| PX4 ↔ QGC | 地面站 | MAVLink over UDP | 14550 |
| ROS 2 ↔ XRCE-DDS Agent | 算法程序 | DDS | 域 ID 0 |
2.3 硬件与系统底层的准备
如果你是在 x86 笔记本或台式机上搭,基本没什么额外要求,16GB 内存、能跑 Ubuntu 22.04 就行。但如果你打算在 RK3588 这类 ARM 平台上做开发,情况会复杂一些。RK3588 的算力足够跑 PX4 SITL 和 Gazebo,但 Gazebo 的图形渲染对 GPU 驱动有要求,建议用带桌面环境的 Ubuntu 22.04 镜像,并且确认 OpenGL ES 驱动正常。我实测过在 RK3588 上跑 Gazebo Classic,帧率大概在 15 到 25 之间,做基础的位置控制验证够用,但复杂场景会卡。
系统层面,先确保你的用户有串口权限,把用户加入dialout组。另外 Ubuntu 22.04 默认的python3是 3.10,PX4 的构建脚本依赖这个版本,不要随意升级到 3.12,否则empy等工具会出兼容问题。
sudo usermod -aG dialout $USER sudo apt update sudo apt install git cmake build-essential python3-pip3. 核心组件安装与配置实操
3.1 PX4 源码克隆与子模块处理
PX4 的源码仓库带大量子模块,直接git clone之后必须执行git submodule update --init --recursive,否则编译时会缺各种依赖。这一步在国内网络环境下容易断,建议加上--depth 1减少拉取量,或者配置好 Git 的代理重试策略。
git clone https://github.com/PX4/PX4-Autopilot.git --branch v1.14.0 cd PX4-Autopilot git submodule update --init --recursive克隆完成后,运行官方的一键安装脚本Tools/setup/ubuntu.sh。这个脚本会装 Gazebo、MAVLink 相关工具、Python 依赖等。注意这个脚本执行时间较长,中途不要中断。执行完之后建议重启一次终端,让环境变量生效。
注意:
ubuntu.sh默认会安装 Gazebo Classic 11。如果你之前手动装过其他版本,脚本可能会跳过或报冲突,建议在干净的 Ubuntu 22.04 上操作。
3.2 Gazebo 模型库与插件配置
Gazebo 启动时如果模型加载慢或者报“Unable to find model”,通常是模型库路径没配好。PX4 的仿真模型放在Tools/sitl_gazebo/models下,Gazebo 需要知道这个路径。可以在~/.gazebo/models下建软链接,或者设置GAZEBO_MODEL_PATH环境变量。
echo 'export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:~/PX4-Autopilot/Tools/sitl_gazebo/models' >> ~/.bashrc source ~/.bashrc另外,Gazebo Classic 的在线模型库有时候拉取会卡死,如果你遇到“gazebo 保存地图卡死”这类情况,多半是它在尝试联网下载模型。解决办法是提前把常用模型下载到本地,或者在 Gazebo 设置里关闭在线模型库。我一般会把~/.gazebo/gui.ini里的[model_database_uri]相关项注释掉,避免启动时联网。
3.3 XRCE-DDS Agent 的编译与启动
XRCE-DDS Agent 是 PX4 和 ROS 2 之间的桥梁,需要单独编译安装。官方推荐用源码编译,因为 apt 源里的版本可能和 PX4 不匹配。
git clone https://github.com/eProsima/Micro-XRCE-DDS-Agent.git cd Micro-XRCE-DDS-Agent mkdir build && cd build cmake .. make -j$(nproc) sudo make install sudo ldconfig /usr/local/lib/编译完成后,启动 Agent 的命令是MicroXRCEAgent udp4 -p 8888。这个命令要在 PX4 SITL 启动之前或之后都可以,但建议先启动 Agent,再启动 PX4,这样 PX4 的uxrce_dds_client一启动就能连上。
提示:如果你用的是串口模式(比如真机上的 TELEM2),命令要改成
MicroXRCEAgent serial --dev /dev/ttyS2 -b 921600。仿真环境下用 UDP 就够了。
3.4 QGC 安装与连接参数
QGC 可以直接下载 AppImage 或者用 apt 安装。Ubuntu 22.04 下我建议用 AppImage,版本更新更及时。下载后赋予执行权限直接运行即可。
chmod +x QGroundControl.AppImage ./QGroundControl.AppImageQGC 启动后会自动扫描 UDP 14550 端口。PX4 SITL 启动时会自动向 14550 发送 MAVLink 心跳,所以正常情况下 QGC 会自动连上。如果连不上,检查 PX4 启动日志里有没有mavlink相关的报错,或者手动在 QGC 的“应用设置-链路”里添加 UDP 链路,端口填 14550。
4. 完整仿真链路启动与 Offboard 验证
4.1 启动顺序与命令组合
这套环境的启动顺序有讲究,我推荐的顺序是:先启动 XRCE-DDS Agent,再启动 PX4 SITL(带 Gazebo),最后启动 QGC。这样每一步的通信链路都能单独确认。
第一步,开一个终端启动 Agent:
MicroXRCEAgent udp4 -p 8888第二步,开另一个终端启动 PX4 SITL:
cd ~/PX4-Autopilot make px4_sitl gazebo-classic这个命令会同时启动 PX4 和 Gazebo,并加载默认的 iris 机型。启动成功后,你会在终端看到 PX4 的 nsh 控制台,Gazebo 窗口里会出现一架四旋翼。
第三步,启动 QGC,确认能连上并显示飞机状态。
4.2 确认 XRCE-DDS 链路是否打通
在 PX4 的 nsh 控制台里输入uxrce_dds_client status,如果看到Running并且有收发计数,说明 Agent 连接正常。如果显示Not running,先检查 Agent 是否在监听 8888 端口,可以用netstat -anu | grep 8888确认。
另一个验证方法是在 ROS 2 终端里执行ros2 topic list,如果能看到/fmu/out/vehicle_status、/fmu/in/offboard_control_mode这类话题,说明 DDS 链路完全通了。这一步是 Offboard 能否跑通的前提,务必先确认。
4.3 Offboard 模式切换的关键参数
Offboard 模式不是随便就能切的,PX4 有一组安全检查。最常见的问题是“内八解锁要设置哪个参数”这类疑问,其实解锁和 Offboard 是两回事。解锁用commander arm或者遥控器内八,Offboard 切换用commander mode offboard。
但 Offboard 切换有个硬性条件:必须先持续发送OffboardControlMode消息,并且频率要高于 2Hz,PX4 才会允许切换。如果你直接发commander mode offboard而不发控制模式消息,会被拒绝。这就是为什么 Offboard 例程里都是先起一个定时器持续发心跳,再切模式。
相关参数方面,COM_OBL_RC_ACT决定 Offboard 丢失时的行为,COM_OF_LOSS_T是 Offboard 丢失超时时间。仿真阶段这些用默认值就行,但如果你做真机开发,这两个参数必须根据实际安全策略调整。
4.4 用 ROS 2 跑一个最小 Offboard 例程
下面是一个最小化的 Offboard 控制节点,用 Python 写,基于px4_msgs和rclpy。它的逻辑是:持续发送OffboardControlMode和TrajectorySetpoint,然后切换 Offboard 模式并解锁,最后让飞机起飞到 5 米高度。
import rclpy from rclpy.node import Node from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy from px4_msgs.msg import OffboardControlMode, TrajectorySetpoint, VehicleCommand class OffboardControl(Node): def __init__(self): super().__init__('offboard_control') qos = QoSProfile( reliability=ReliabilityPolicy.BEST_EFFORT, history=HistoryPolicy.KEEP_LAST, depth=1 ) self.offboard_pub = self.create_publisher( OffboardControlMode, '/fmu/in/offboard_control_mode', qos) self.traj_pub = self.create_publisher( TrajectorySetpoint, '/fmu/in/trajectory_setpoint', qos) self.cmd_pub = self.create_publisher( VehicleCommand, '/fmu/in/vehicle_command', qos) self.timer = self.create_timer(0.1, self.timer_callback) self.counter = 0 def timer_callback(self): self.publish_offboard_mode() self.publish_trajectory() if self.counter == 10: self.arm() self.set_offboard_mode() if self.counter == 20: self.publish_trajectory(z=-5.0) self.counter += 1 def publish_offboard_mode(self): msg = OffboardControlMode() msg.position = True msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.offboard_pub.publish(msg) def publish_trajectory(self, z=0.0): msg = TrajectorySetpoint() msg.position = [0.0, 0.0, z] msg.yaw = 0.0 msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.traj_pub.publish(msg) def arm(self): msg = VehicleCommand() msg.command = VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM msg.param1 = 1.0 msg.target_system = 1 msg.target_component = 1 msg.source_system = 1 msg.source_component = 1 msg.from_external = True msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.cmd_pub.publish(msg) def set_offboard_mode(self): msg = VehicleCommand() msg.command = VehicleCommand.VEHICLE_CMD_DO_SET_MODE msg.param1 = 1.0 msg.param2 = 6.0 msg.target_system = 1 msg.target_component = 1 msg.source_system = 1 msg.source_component = 1 msg.from_external = True msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.cmd_pub.publish(msg) def main(): rclpy.init() node = OffboardControl() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()这个例程的关键在于timer_callback里的计数逻辑:前 10 个周期(1 秒)只发心跳,让 PX4 确认 Offboard 信号稳定;第 10 个周期发解锁和切模式命令;第 20 个周期发目标位置。这个节奏是经过验证的,太快切模式会被拒绝,太慢则浪费时间。
注意:
px4_msgs需要单独克隆并编译到你的 ROS 2 工作空间里,版本要和 PX4 固件匹配。v1.14 的 PX4 对应px4_msgs的 release/1.14 分支。
5. 常见问题排查与避坑经验
5.1 仿真启动类问题速查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| Gazebo 启动后黑屏 | GPU 驱动或 OpenGL 问题 | 尝试export LIBGL_ALWAYS_SOFTWARE=1用软件渲染 |
| 模型加载卡死 | 在线模型库联网超时 | 注释~/.gazebo/gui.ini中的在线库配置 |
| PX4 编译报 empy 错误 | Python 版本不匹配 | 确认 python3 为 3.10,重装 empy |
| Gazebo 里飞机掉下去 | 传感器插件未加载 | 检查GAZEBO_MODEL_PATH和模型文件完整性 |
5.2 XRCE-DDS 连接失败排查
最常见的是 Agent 启动了但 PX4 连不上。先在 PX4 nsh 里执行uxrce_dds_client status,如果显示Not running,检查 Agent 的监听端口。有时候 Agent 启动时绑定了 IPv6 地址,而 PX4 发的是 IPv4,就会连不上。解决办法是启动 Agent 时明确指定udp4。
另一个坑是防火墙。Ubuntu 默认的 ufw 如果开了,可能会拦 UDP 8888。仿真环境下直接sudo ufw disable最省事,生产环境再按需放行。
5.3 Offboard 切换被拒绝的典型原因
Offboard 切换失败时,PX4 会在 nsh 里打印拒绝原因。常见的有这几种:一是OffboardControlMode消息频率不够,PX4 要求至少 2Hz;二是飞机没有解锁,Offboard 模式要求先解锁;三是 GPS 或位置估计没就绪,仿真环境下通常没问题,但如果你改了机型配置可能会触发。
我踩过的一个坑是:在timer_callback里先发了解锁命令,但解锁需要时间生效,紧接着就切 Offboard,结果因为还没解锁成功被拒。后来改成解锁后等 1 秒再切模式,就稳定了。这种时序问题在仿真里不明显,但真机上必须考虑。
5.4 QGC 显示异常与参数同步
QGC 有时候会显示“未连接”但实际 MAVLink 是通的,这通常是 QGC 的链路扫描没扫到。手动添加 UDP 链路即可。另外 QGC 修改参数后,如果 PX4 那边没确认,参数不会真正生效。建议改完参数后在 nsh 里用param show确认一下。
还有一个细节:QGC 和 XRCE-DDS 同时连接时,如果 QGC 发了某些模式切换命令,可能会和 Offboard 控制冲突。做 Offboard 测试时,建议把 QGC 只当监视器用,不要在上面手动切模式。
6. 从仿真到真机与集群扩展的几点体会
这套仿真链路跑通之后,往真机迁移的改动其实很小,主要换的是 XRCE-DDS 的连接方式,从 UDP 改成串口,以及调整COM_OBL_RC_ACT等安全参数。但真机上有个仿真里不存在的问题:传感器噪声和延迟。仿真里的 IMU 是理想的,真机上有振动和漂移,所以 Offboard 控制器的鲁棒性要在真机上重新验证。
如果你要做多机集群,Gazebo 支持同时启动多个机型实例,每个实例用不同的PX4_SIM_MODEL和端口。XRCE-DDS 这边每个实例对应一个 Agent 或者用命名空间区分。这块的配置量比较大,建议先用两机编队验证通信和避碰逻辑,再往上加。
我在 RK3588 上跑这套环境时,最大的感受是 Gazebo 的图形渲染是瓶颈,如果只做算法验证,可以用HEADLESS=1模式启动 Gazebo,省掉图形界面,帧率能提升不少。另外 PX4 的 SITL 编译在 RK3588 上大概要十几分钟,建议在 x86 上交叉编译好再部署,或者直接用预编译的二进制。
最后分享一个实用技巧:把常用的启动命令写成脚本,比如start_sim.sh里按顺序启动 Agent、PX4、QGC,省得每次开三个终端手动敲。脚本里加上sleep控制启动间隔,能避免很多“启动太快导致连接失败”的问题。这个习惯在后期频繁重启调试时能省下大量时间。