1. 从零搭建一套能跑Offboard的仿真链路,到底难在哪
PX4、Gazebo、XRCE-DDS、QGC这四个词单独拎出来,每一个都有大把现成文档,但把它们串成一条能稳定跑Offboard控制的链路,坑的数量远超多数人的预期。我自己前前后后搭过五六套环境,从Ubuntu 18.04一路折腾到22.04,每次换机器或者换系统版本,总会有新的问题冒出来。这套组合的本质是:Gazebo负责提供物理仿真世界和传感器数据,PX4负责飞控逻辑解算,XRCE-DDS负责在PX4和外部程序之间搭一条实时通信的桥,QGC负责地面监控和指令下发。四者缺一不可,但四者之间的版本匹配、启动顺序、网络配置,每一步都有讲究。
这篇文章面向的是已经了解PX4基本概念、想从“能起飞”进阶到“能写Offboard程序控制飞机”的开发者。如果你还在纠结PX4是什么、Gazebo怎么装,那建议先补一下基础,因为这里讲的是整条链路的打通和实战调试。我会把每个环节的选型理由、参数配置、启动流程、常见报错和排查思路都摊开讲,尽量做到你照着做就能复现。全文基于Ubuntu 22.04 + ROS 2 Humble这个目前最主流的组合来写,其他版本我会在关键位置标注差异。
先说一个核心结论:这套链路的搭建,难点不在安装,而在版本对齐和通信验证。很多人装完了发现QGC连不上、Offboard模式切不进去、DDS话题列表是空的,八成都是这两个环节出了问题。下面我按实际搭建顺序,从环境准备一路讲到Offboard程序跑通。
2. 环境准备与版本选型:为什么Ubuntu 22.04 + ROS 2 Humble是当前最优解
2.1 系统版本与ROS发行版的对应关系
选Ubuntu 22.04不是随便选的。PX4的官方工具链脚本对Ubuntu 20.04和22.04支持最好,24.04虽然也能跑但部分依赖包还没完全跟上。ROS 2这边,Humble是22.04的官方对应版本,长期支持到2027年,社区资料最全。如果你用20.04,对应的是ROS 2 Foxy,也能跑,但Foxy已经停止维护,遇到问题查资料会麻烦一些。
Gazebo的版本要特别注意。Ubuntu 22.04默认apt安装的是Gazebo Classic 11,这也是PX4官方sitl仿真默认使用的版本。网上有些教程会让你装Ignition Gazebo(现在叫Gazebo Sim),那是新一代的仿真器,和PX4的gz桥接还在完善中,新手不建议碰。所以这里明确:用Gazebo Classic 11,别被新名字绕晕。
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| 操作系统 | Ubuntu 22.04 LTS | 20.04可用但资料渐少 |
| ROS | ROS 2 Humble | 22.04官方对应版本 |
| Gazebo | Gazebo Classic 11 | PX4 SITL默认支持 |
| PX4 | v1.14或v1.15 | 稳定分支,别用main |
| QGC | v4.3以上 | 支持DDS配置 |
| XRCE-DDS | 随PX4源码自带 | 无需单独装 |
2.2 PX4源码的获取与工具链安装
PX4的安装不要用apt,必须从源码编译,因为XRCE-DDS的客户端是编译进固件的,apt装的版本不一定带。克隆的时候用--recursive把子模块一起拉下来,否则编译会缺东西。
git clone https://github.com/PX4/PX4-Autopilot.git --recursive cd PX4-Autopilot git checkout v1.14.0 bash Tools/setup/ubuntu.shubuntu.sh这个脚本会装一堆依赖,包括Gazebo、Python包、编译工具等。跑完之后一定要重启或者重新登录,因为脚本会把你加入dialout组,不重新登录权限不生效,后面连串口或者跑仿真会报权限错误。这个坑我踩过不止一次,明明脚本跑完了,一跑仿真就提示权限不足,查半天才发现是组权限没刷新。
编译的时候第一次会比较慢,建议用make px4_sitl gazebo-classic直接编译并启动,这样能一次性验证工具链是否完整。如果编译报错说找不到某个包,大概率是ubuntu.sh没跑完或者中途断了,重新跑一遍即可。
2.3 QGC的安装与版本选择
QGC在Ubuntu上有AppImage和源码编译两种方式。AppImage最省事,下载下来加执行权限直接跑。但要注意,QGC v4.2以前的版本对DDS支持不完整,如果你要用QGC通过DDS看PX4的内部话题,必须用v4.3以上。我目前用的是v4.3.0的AppImage,稳定。
chmod +x QGroundControl.AppImage ./QGroundControl.AppImage如果跑起来提示缺少FUSE,装一下libfuse2就行。另外QGC第一次启动会问你要不要跑设置向导,仿真环境下可以直接跳过,因为很多参数是给真机用的。
注意:QGC和PX4的通信在仿真里走的是UDP 14550端口,如果你同时开了多个QGC实例或者别的程序占用了这个端口,会连不上。排查的时候先用
netstat -tunlp | grep 14550看一眼端口占用。
3. XRCE-DDS桥接机制拆解:PX4和外部程序到底怎么对话
3.1 为什么需要XRCE-DDS,而不是直接用MAVLink
很多人会问,PX4已经有MAVLink了,QGC也是通过MAVLink通信的,为什么Offboard还要搞个XRCE-DDS?原因在于MAVLink是面向消息的协议,适合发指令和收状态,但它的数据类型和话题机制比较受限。而Offboard控制需要频繁读写PX4的内部uORB话题,比如vehicle_local_position、vehicle_attitude_setpoint这些,用MAVLink做会很别扭。
XRCE-DDS的本质是在PX4内部跑一个客户端,把uORB话题映射成DDS话题,外部程序通过DDS中间件订阅和发布。这样一来,你在ROS 2里就能像订阅普通话题一样订阅PX4的内部状态,发布设定值也一样自然。这是目前PX4和ROS 2集成最官方、最推荐的方式,比以前的MAVROS更底层、延迟更低。
3.2 桥接的启动流程与验证方法
XRCE-DDS的客户端在PX4 SITL启动时会自动运行,但你需要确认它真的起来了。启动仿真后,在PX4的nsh终端里输入uxrce_dds_client status,如果显示running就说明客户端在跑。默认配置下,它会尝试连接本机的8888端口,也就是XRCE-DDS Agent监听的端口。
Agent需要单独启动,它是运行在电脑上的一个程序,负责在DDS网络和PX4客户端之间转发。安装方式有两种,一是从源码编译,二是用snap。我推荐源码编译,因为版本可控。
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/装完之后直接运行MicroXRCEAgent udp4 -p 8888,看到它打印出等待连接的日志就对了。这时候再启动PX4 SITL,Agent那边会显示客户端已连接,PX4这边uxrce_dds_client status也会显示已连接。
验证桥接是否成功,最直接的方法是在ROS 2里ros2 topic list,如果能看到/fmu/out/vehicle_local_position这类话题,说明整条链路通了。如果话题列表是空的,先检查Agent是否在跑,再检查PX4的DDS配置参数UXRCE_DDS_CFG是否指向了正确的端口。
3.3 话题命名规则与常用话题清单
XRCE-DDS映射出来的话题有固定命名规则,输出话题(PX4发出来的)前缀是/fmu/out/,输入话题(外部发给PX4的)前缀是/fmu/in/。这个规则要记牢,写Offboard程序的时候全靠它。
| 话题名 | 方向 | 用途 |
|---|---|---|
| /fmu/out/vehicle_local_position | 输出 | 本地位置、速度估计 |
| /fmu/out/vehicle_attitude | 输出 | 姿态四元数 |
| /fmu/out/vehicle_status | 输出 | 飞行模式、解锁状态 |
| /fmu/in/offboard_control_mode | 输入 | Offboard控制模式心跳 |
| /fmu/in/trajectory_setpoint | 输入 | 位置/速度设定值 |
| /fmu/in/vehicle_command | 输入 | 解锁、切模式等指令 |
这里有个关键点:Offboard模式要求以至少2Hz的频率持续发送offboard_control_mode和设定值,否则PX4会自动退出Offboard。这是很多人程序跑起来飞机不动或者直接掉下来的原因,后面实操部分会详细讲。
4. Gazebo仿真环境配置与模型加载实战
4.1 默认机型和世界的启动方式
PX4 SITL配合Gazebo启动非常简单,一条命令就行:
make px4_sitl gazebo-classic这条命令会编译(如果没编译过)并启动PX4 SITL和Gazebo,默认加载的是iris机型加empty世界。如果你想换机型,比如用固定翼或者带深度相机的机型,可以指定:
make px4_sitl gazebo-classic_iris_depth_cameraGazebo启动后,你会看到一个空旷的世界和一架小四旋翼。这时候QGC应该能自动连上,显示飞机在地面上。如果QGC没反应,检查一下是不是防火墙挡了UDP,或者QGC的自动连接设置被关了。
4.2 自定义世界与模型加载的注意事项
默认的empty世界太单调,做视觉或者避障仿真需要加障碍物。Gazebo的世界文件放在PX4-Autopilot/Tools/sitl_gazebo/worlds/下面,你可以复制一个现有的world文件改。加载自定义世界用:
export PX4_SITL_WORLD=/path/to/your.world make px4_sitl gazebo-classic模型加载这块有个常见坑:Gazebo的模型路径依赖GAZEBO_MODEL_PATH环境变量,PX4的启动脚本会自动设置,但如果你手动启动Gazebo或者用别的launch文件,可能会找不到模型。解决办法是在~/.bashrc里手动加上:
export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:~/PX4-Autopilot/Tools/sitl_gazebo/models另外,Gazebo Classic 11在虚拟机里跑容易出现屏幕闪烁或者渲染异常,这是OpenGL驱动的问题。如果你用VMware,建议开启3D加速,并且在Gazebo的渲染设置里把阴影关掉,能明显改善。实在不行就用双系统或者物理机,仿真体验会好很多。
4.3 传感器数据的验证与话题对应
Gazebo里的传感器数据要通过PX4的仿真桥接才能变成uORB话题,再通过XRCE-DDS变成DDS话题。验证传感器是否正常工作,可以在Gazebo里看飞机的相机画面,或者在ROS 2里订阅对应话题。
比如深度相机,Gazebo里加载iris_depth_camera机型后,ROS 2里应该能看到/camera/depth/image_raw这类话题。如果没有,检查PX4的SYS_AUTOSTART参数是否和机型匹配,以及Gazebo的传感器插件是否加载成功。这一步排查起来比较繁琐,建议先用gz topic -l看Gazebo层面有没有数据,再看ROS 2层面,逐层往上查。
5. QGC地面站配置与Offboard模式切换要点
5.1 QGC连接仿真与参数配置
QGC连仿真飞机是自动的,启动后几秒内应该就能看到飞机图标变成绿色。如果连不上,在Application Settings里检查Comm Links,确认有UDP监听14550端口。仿真环境下不需要手动添加连接,QGC会自动扫描。
连上之后,建议先去参数页面把几个关键参数确认一下。COM_RC_IN_MODE设为1或者4,表示允许Offboard控制。NAV_RCL_ACT设为0,避免遥控器丢失时触发返航。这些参数在真机上要谨慎,仿真里可以放开。
5.2 Offboard模式切不进去的常见原因
这是问得最多的问题:程序发了切换Offboard的指令,但QGC上飞行模式还是显示Position或者Hold。原因通常有三个:
第一,没有持续发送offboard_control_mode心跳。PX4要求进入Offboard前必须先收到这个心跳,而且频率要够。很多人只发了一次就等着切模式,PX4根本不认。
第二,设定值话题没有数据。切Offboard之前,PX4会检查是否有有效的设定值流。如果trajectory_setpoint没在发,切模式会被拒绝。
第三,飞机没有解锁。Offboard模式要求飞机处于解锁状态,顺序应该是先解锁,再切Offboard,或者同时进行。如果飞机还锁着,切模式指令会被忽略。
正确的顺序是:先持续发送offboard_control_mode和trajectory_setpoint,然后发解锁指令,等解锁确认后再发切Offboard指令。整个过程心跳不能断。
5.3 QGC的DDS配置与话题监控
QGC v4.3以上支持通过DDS直接看PX4的内部话题,这个功能在调试Offboard程序时非常有用。在Application Settings里找到DDS配置,启用后QGC会自己起一个DDS客户端,连接到XRCE-DDS Agent。配置对了之后,你可以在QGC里实时看到vehicle_local_position等话题的数据变化,不用开ROS 2就能验证链路。
不过这个功能对网络配置有点要求,如果Agent和QGC不在同一台机器上,要确保UDP端口互通。仿真环境下一般都在本机,问题不大。
6. Offboard控制程序编写与联调实录
6.1 ROS 2节点的最小骨架
写一个能跑的Offboard程序,不需要多复杂,但几个关键点必须到位。下面是一个最小骨架,用Python写,基于px4_msgs包。这个包需要从PX4的ROS 2工作空间编译,或者直接clone官方的px4_msgs仓库。
import rclpy from rclpy.node import Node from px4_msgs.msg import OffboardControlMode, TrajectorySetpoint, VehicleCommand, VehicleStatus 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.status_sub = self.create_subscription(VehicleStatus, '/fmu/out/vehicle_status', self.status_cb, 10) self.timer = self.create_timer(0.1, self.timer_cb) self.counter = 0 def timer_cb(self): self.publish_offboard_mode() self.publish_setpoint() self.counter += 1 if self.counter == 20: self.arm() if self.counter == 40: self.switch_to_offboard() 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_setpoint(self): msg = TrajectorySetpoint() msg.position = [0.0, 0.0, -5.0] msg.yaw = 0.0 msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.setpoint_pub.publish(msg) def arm(self): msg = VehicleCommand() msg.command = VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM msg.param1 = 1.0 msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.command_pub.publish(msg) def switch_to_offboard(self): msg = VehicleCommand() msg.command = VehicleCommand.VEHICLE_CMD_DO_SET_MODE msg.param1 = 1.0 msg.param2 = 6.0 msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.command_pub.publish(msg) def status_cb(self, msg): pass这个骨架的核心逻辑是:以10Hz持续发心跳和设定值,第2秒解锁,第4秒切Offboard。设定值给的是高度5米的位置,飞机切进Offboard后应该会爬升到5米。
6.2 时间戳与坐标系的关键细节
上面代码里有个容易忽略的点:timestamp用的是微秒,而且必须是PX4的时间基准。self.get_clock().now().nanoseconds // 1000拿到的是ROS 2的时间,如果ROS 2和PX4的时间没同步,消息可能会被丢弃。仿真环境下一般没问题,但真机上要用/fmu/out/timesync做时间同步。
坐标系方面,PX4用的是NED(北东地),所以高度是负值,-5.0表示向上5米。这个和ROS 2常用的ENU(东北天)是反的,写设定值的时候千万别搞混,否则飞机会往地下钻。
6.3 联调过程中的典型现象与处理
程序跑起来后,正常现象是:Gazebo里的飞机先解锁(螺旋桨开始转),然后爬升到5米悬停。QGC上飞行模式会显示Offboard。如果飞机不动,按下面的顺序排查:
先看ros2 topic hz /fmu/in/offboard_control_mode,确认心跳在发。再看ros2 topic echo /fmu/out/vehicle_status,确认nav_state变成了14(Offboard对应的值)。如果nav_state一直是3(Position),说明切模式没成功,检查解锁是否完成。
还有一个常见现象是飞机解锁后原地转圈或者抖动,这通常是设定值的yaw或者位置有跳变。确保设定值从当前位置平滑过渡,不要一上来就给一个很远的目标点。
7. 常见问题速查与避坑经验汇总
7.1 编译与启动阶段的问题
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 编译报错找不到包 | 子模块没拉全 | git submodule update --init --recursive |
| 仿真启动后Gazebo黑屏 | 显卡驱动问题 | 关阴影,开3D加速,或换物理机 |
| QGC连不上飞机 | 端口占用或防火墙 | 检查14550端口,关防火墙 |
| Agent启动报错 | 库路径没配 | sudo ldconfig /usr/local/lib/ |
7.2 通信与Offboard阶段的问题
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| ros2 topic list为空 | Agent没连上 | 检查Agent是否运行,端口是否对 |
| 切Offboard被拒绝 | 心跳或设定值没发 | 确认10Hz心跳和设定值流 |
| 飞机解锁后不动 | 设定值坐标系错 | 检查NED,高度用负值 |
| 飞机飞着飞着退出Offboard | 心跳断了 | 检查定时器频率和线程阻塞 |
7.3 几条用血泪换来的经验
第一,每次改完PX4参数或者DDS配置,重启仿真和Agent。有些参数是启动时读取的,热改不生效,重启能省掉大量排查时间。
第二,先用ros2 topic echo确认数据流,再写控制逻辑。很多人一上来就写复杂的控制算法,结果底层数据都没通,调半天以为是算法问题。
第三,Gazebo的实时性不如真机,仿真里能跑的程序真机上不一定行。仿真里10Hz心跳很稳,真机上可能因为计算负载导致丢包,所以真机调试时心跳频率要留余量,建议20Hz以上。
第四,QGC的飞行模式显示有延迟,别完全依赖它判断模式切换是否成功,以vehicle_status话题里的nav_state为准。
这套链路搭通之后,后面做编队、视觉伺服、路径规划都是在这个基础上加东西。我个人的建议是先把单机的Offboard位置控制跑稳,再考虑多机或者复杂任务,否则问题会指数级增加。仿真环境最大的价值就是让你在不摔飞机的前提下把逻辑验证清楚,所以别嫌搭建麻烦,这一步的投入后面都会省回来。