1. 为什么这套组合值得折腾:从零搭建无人机仿真开发链路
搞无人机软件开发的人,绕不开一个现实问题:真机调试成本太高,摔一次可能就是几千块,而且很多极端场景根本没法在真实环境里反复测试。我刚开始接触飞控开发那会儿,每次改完代码要验证都得挑天气、约场地、检查电池,一天下来有效调试时间不到两小时。后来转向仿真环境,效率直接翻了好几倍。
这套PX4 + Gazebo + XRCE-DDS + QGC的组合,本质上是在电脑里搭一个完整的无人机虚拟测试台。PX4 负责飞控核心逻辑,Gazebo 提供物理仿真环境和传感器数据,XRCE-DDS 打通飞控和外部程序之间的数据通道,QGC 则是你观察和控制无人机的窗口。四个组件各司其职,串起来之后你就能在完全虚拟的环境里做 Offboard 控制、编队算法验证、传感器融合测试,甚至跑一些真机上不敢轻易尝试的激进机动。
适合谁来参考这篇内容?如果你正在做无人机控制算法研究、想入门 PX4 二次开发、或者需要一套可复现的仿真环境来验证代码逻辑,这套方案基本是当前社区里最成熟的选择。不需要你有很深的 Linux 功底,但至少要能看懂基本的终端命令和 Python 脚本。我下面会从环境搭建一路讲到 Offboard 控制实战,把踩过的坑和关键细节都摊开说。
2. 环境搭建:Ubuntu 22.04 下的完整工具链部署
2.1 系统选择与基础依赖安装
Ubuntu 22.04 是目前 PX4 官方支持最稳定的 LTS 版本,ROS 2 Humble 也正好对应这个版本,省去了很多版本兼容的麻烦。我试过在 Ubuntu 20.04 上折腾,虽然也能跑,但 Gazebo 的渲染问题和 ROS 2 的依赖冲突会多不少。如果你还在用 20.04,建议直接重装或者开新分区,别在这上面省时间。
装完系统后先更新源,然后安装基础工具:
sudo apt update && sudo apt upgrade -y sudo apt install git cmake build-essential python3-pip -y这几样是后面编译 PX4 和运行脚本的基础。git用来拉代码,cmake和build-essential负责编译,python3-pip装 Python 依赖。别小看这些,我见过有人因为cmake版本太老导致 PX4 编译报一堆莫名其妙的错,排查半天才发现是基础环境没弄好。
接下来装 Gazebo。Ubuntu 22.04 默认软件源里的 Gazebo 版本是 Fortress,对应 Ignition Gazebo 的第六代。PX4 目前对 Gazebo Classic 和 Gazebo Fortress 都有支持,但新版本 PX4 已经逐步转向 Fortress 了。安装命令:
sudo apt install ignition-fortress -y装完之后可以跑一下ign gazebo看看能不能正常启动。如果提示命令找不到,检查一下是不是ignition-fortress这个包名在你这台机器上叫别的名字,用apt search ignition搜一下就能确认。
注意:如果你之前装过 Gazebo Classic(也就是
gazebo那个包),建议先卸载干净再装 Fortress,两个版本共存容易出玄学问题。卸载命令是sudo apt remove gazebo*,然后再清理一下残留配置。
2.2 PX4 源码获取与首次编译
PX4 的代码仓库在 GitHub 上,直接克隆下来:
git clone https://github.com/PX4/PX4-Autopilot.git --recursive cd PX4-Autopilot--recursive这个参数很关键,因为 PX4 依赖很多子模块,不加的话后面编译会缺东西。克隆完之后跑官方提供的一键环境配置脚本:
bash ./Tools/setup/ubuntu.sh这个脚本会自动安装一堆依赖,包括 Python 包、编译器工具链、还有 Gazebo 相关的插件。跑完之后建议重启一下终端,让环境变量生效。
首次编译用这条命令:
make px4_sitl gazebo这里px4_sitl是软件在环仿真的目标,gazebo指定用 Gazebo 作为仿真后端。第一次编译会比较慢,我实测在 i7 加 16G 内存的机器上大概要十几分钟。编译过程中如果报错说某个 Python 包找不到,大概率是ubuntu.sh脚本没跑完或者中途断了,重新跑一遍就行。
编译成功后你会看到 Gazebo 窗口弹出来,里面有一架小四旋翼停在跑道上,同时终端里 PX4 的 shell 也启动了。这时候在 PX4 终端里输入commander takeoff,飞机就能起飞。不过先别急着飞,把 QGC 和 XRCE-DDS 也配好,后面调试会方便很多。
2.3 QGC 地面站安装与连接配置
QGC 的安装方式有好几种,最省事的是直接下载 AppImage:
wget https://d176tv9ibo4jno.cloudfront.net/latest/QGroundControl.AppImage chmod +x QGroundControl.AppImage ./QGroundControl.AppImageAppImage 的好处是不用管依赖,双击就能跑。如果你更喜欢用包管理器,也可以走apt或者snap,但版本可能不是最新的。我一般推荐 AppImage,因为 QGC 更新比较频繁,新版本对 PX4 新特性的支持更好。
启动 QGC 之后,它会自动扫描串口和 UDP 端口。PX4 的 SITL 默认会往 UDP 14550 端口发 MAVLink 数据,QGC 监听到之后就会自动连上。如果没连上,检查一下 QGC 的 Application Settings 里 Comm Links 有没有自动添加 UDP 连接。手动加的话,端口填 14550,地址留空或者填localhost都行。
连上之后你应该能在 QGC 里看到飞机的姿态、电池电压(仿真是虚拟的)、GPS 状态等信息。这时候可以试着在 QGC 里解锁并起飞,验证一下整条链路是通的。
实操心得:QGC 有时候会因为防火墙或者端口占用连不上。如果自动连接失败,先用
netstat -tulnp | grep 14550看看端口有没有被占用。另外 QGC 的日志在~/.config/QGroundControl/下面,连不上的时候翻日志比瞎猜快得多。
2.4 XRCE-DDS 代理编译与启动
XRCE-DDS 是这套组合里最容易被忽略但最关键的一环。它的作用是让 PX4 和外部程序(比如你的 Python 控制脚本)通过 DDS 协议通信。没有它,你只能通过 MAVLink 跟 PX4 交互,能做的东西比较有限。有了 XRCE-DDS,你可以直接读写 PX4 的 uORB 话题,实现真正的 Offboard 控制。
编译 XRCE-DDS 代理:
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/编译完之后,先启动 PX4 SITL,然后在另一个终端里跑:
MicroXRCEAgent udp4 -p 8888这时候 PX4 那边会自动检测到代理并建立连接。你可以在 PX4 终端里输入uxrce_dds_client status看看连接状态。如果显示 running 就说明通了。
注意:XRCE-DDS 的端口默认是 8888,如果你改了 PX4 的配置或者同时跑多个实例,记得端口别冲突。另外
sudo ldconfig那步不能省,不然运行代理的时候会报找不到动态库。
3. Offboard 控制实战:从解锁到轨迹飞行
3.1 Offboard 模式的核心逻辑与切换条件
Offboard 模式是 PX4 里最强大的控制接口之一,它允许外部程序直接给飞控发送位置、速度或姿态设定值。但 PX4 对 Offboard 的切换有严格的安全检查,不是你想切就能切。核心条件有三个:飞机必须已经解锁、必须已经收到外部程序发来的设定值流、而且设定值的发送频率要稳定在 2Hz 以上。
我刚开始玩 Offboard 的时候,经常遇到切换失败的情况,QGC 里点 Offboard 按钮没反应。后来查文档才发现,PX4 要求在你发送切换命令之前,外部程序必须已经在持续发送设定值了。也就是说,你得先让程序跑起来开始发数据,然后再发切换命令。这个顺序反了就会一直切不过去。
用 Python 写 Offboard 控制脚本的话,推荐用px4_msgs和rclpy。px4_msgs是 PX4 官方维护的 ROS 2 消息包,里面定义了所有 uORB 话题对应的消息类型。安装方式:
cd ~/ros2_ws/src git clone https://github.com/PX4/px4_msgs.git cd ~/ros2_ws colcon build source install/setup.bash编译px4_msgs的时候要注意,它的分支要跟你的 PX4 版本对应。如果你用的是 PX4 主分支,px4_msgs也用主分支;如果用的是某个 release 版本,记得切到对应的分支。
3.2 编写第一个 Offboard 控制节点
下面是一个最简化的 Offboard 控制节点,功能是让飞机起飞到 5 米高度然后悬停:
import rclpy from rclpy.node import Node from px4_msgs.msg import OffboardControlMode, TrajectorySetpoint, VehicleCommand, VehicleStatus from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy 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.status_sub = self.create_subscription(VehicleStatus, '/fmu/out/vehicle_status', self.status_cb, qos) self.timer = self.create_timer(0.1, self.timer_cb) self.counter = 0 self.armed = False def status_cb(self, msg): self.armed = (msg.arming_state == VehicleStatus.ARMING_STATE_ARMED) def timer_cb(self): self.counter += 1 offboard_msg = OffboardControlMode() offboard_msg.position = True offboard_msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.offboard_pub.publish(offboard_msg) traj_msg = TrajectorySetpoint() traj_msg.position = [0.0, 0.0, -5.0] traj_msg.yaw = 0.0 traj_msg.timestamp = self.get_clock().now().nanoseconds // 1000 self.traj_pub.publish(traj_msg) if self.counter == 50: self.send_command(VehicleCommand.VEHICLE_CMD_DO_SET_MODE, 1.0, 6.0) if self.counter == 100: self.send_command(VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0) def send_command(self, command, param1=0.0, param2=0.0): msg = VehicleCommand() msg.command = command msg.param1 = param1 msg.param2 = param2 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(args=None): rclpy.init(args=args) node = OffboardControl() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()这段代码的逻辑是:先以 10Hz 的频率持续发送 OffboardControlMode 和 TrajectorySetpoint,让 PX4 知道外部程序在正常工作。等到第 50 个周期(也就是 5 秒后)发送切换 Offboard 模式的命令,第 100 个周期(10 秒后)发送解锁命令。解锁之后飞机就会自动起飞到 5 米高度。
有几个细节特别容易出错。第一,timestamp字段必须填,而且要用微秒。我一开始没填这个字段,PX4 直接忽略消息,排查了好久才发现。第二,QoS 配置要用BEST_EFFORT,因为 PX4 的 uORB 桥接默认就是这个可靠性级别,用RELIABLE反而收不到消息。第三,切换 Offboard 的命令参数是param1=1.0, param2=6.0,其中 6 代表 Offboard 模式,这个数字在 PX4 的文档里有对照表,别填错了。
3.3 运行节点与验证飞行效果
跑这个节点之前,确保 PX4 SITL、Gazebo、XRCE-DDS 代理都已经在运行。然后:
source ~/ros2_ws/install/setup.bash python3 offboard_control.py如果一切正常,你会在 Gazebo 里看到飞机先解锁,然后螺旋桨转起来,接着缓缓升到 5 米高度并保持悬停。QGC 里也能看到飞行模式变成了 Offboard,高度数据在变化。
实操心得:如果飞机解锁后没有起飞,先检查 TrajectorySetpoint 的坐标系。PX4 用的是 NED 坐标系,z 轴向下为正,所以起飞到 5 米高度要填
-5.0而不是5.0。这个坑我踩过不止一次,每次隔一段时间不写就忘了。
另外,如果你发现飞机起飞后往一个方向飘,大概率是 TrajectorySetpoint 里的位置设定值和实际位置有偏差。可以在 QGC 的 MAVLink Inspector 里看一下vehicle_local_position话题,对比一下设定值和实际值。仿真环境下一般不会有太大偏差,但如果 Gazebo 的物理步长设置不对,可能会出现积分误差累积。
4. 常见问题排查与避坑指南
4.1 Gazebo 启动失败与渲染问题
Gazebo 在虚拟机里跑经常遇到屏幕闪烁或者直接黑屏的问题。这通常是因为虚拟机的 3D 加速没开或者显卡驱动不兼容。如果你用的是 VMware,先在虚拟机设置里把“加速 3D 图形”勾上,然后分配足够的内存给显存。如果还是闪,试试在启动 Gazebo 的时候加--render-engine ogre参数,换一个渲染引擎。
另一个常见问题是 Gazebo 启动后模型加载不出来,一直卡在加载界面。这多半是模型库下载不全导致的。Gazebo 第一次运行会从网上下载模型,如果网络不稳定就会卡住。解决办法是手动下载模型库放到~/.gazebo/models/目录下,或者设置GAZEBO_MODEL_PATH环境变量指向本地的模型目录。
还有一种情况是 PX4 编译时提示找不到 Gazebo 相关的头文件。这通常是因为ignition-fortress装了但环境变量没配好。检查一下/usr/share/gazebo/setup.sh或者/usr/share/ignition/setup.sh有没有被 source 过。可以在~/.bashrc里加一行source /usr/share/gazebo/setup.sh一劳永逸。
4.2 QGC 连接异常与飞行模式无响应
QGC 连不上 PX4 的情况我遇到过好几次,原因五花八门。最常见的是端口被占用,比如你之前跑过一个 QGC 实例没关干净,或者别的程序占了 14550 端口。用lsof -i :14550查一下,有占用就杀掉。
还有一种情况是 QGC 连上了但显示“飞行模式没反应”,点解锁按钮没动静。这通常是因为 PX4 还没完成初始化,或者 GPS 定位没收敛。仿真环境下 GPS 是虚拟的,一般几秒钟就能收敛,但如果 Gazebo 的时钟和真实时钟不同步,可能会一直卡在等待 GPS 的状态。可以在 PX4 终端里输入commander status看看当前状态,如果显示GPS: OK但模式切换还是没反应,试试用commander arm命令直接解锁。
注意:QGC 的手机版和桌面版在功能上有差异,手机版主要是给户外飞行用的,仿真调试还是用桌面版。另外 QGC 的版本要和 PX4 的 MAVLink 版本匹配,太老的 QGC 可能不支持新的 MAVLink 消息。
4.3 XRCE-DDS 连接超时与话题订阅失败
XRCE-DDS 代理启动后,PX4 那边显示连接成功,但 ROS 2 节点就是收不到话题数据。这个问题我排查过好几次,最后发现是 ROS 2 的域 ID 和 XRCE-DDS 的配置不一致。默认情况下 ROS 2 用域 0,XRCE-DDS 也用域 0,但如果你改过ROS_DOMAIN_ID环境变量,两边就对不上了。检查一下echo $ROS_DOMAIN_ID,确保和 XRCE-DDS 启动时的配置一致。
另一个常见问题是话题名称对不上。PX4 的 uORB 话题通过 XRCE-DDS 桥接之后,在 ROS 2 里的名称会加上/fmu/out/或/fmu/in/前缀。比如vehicle_status在 ROS 2 里是/fmu/out/vehicle_status。如果你订阅的话题名称不对,自然收不到数据。可以用ros2 topic list看看当前有哪些话题,确认名称后再写订阅代码。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| Gazebo 黑屏/闪烁 | 3D 加速未开或驱动不兼容 | 检查虚拟机设置,换渲染引擎 |
| QGC 连不上 | 端口占用或防火墙拦截 | lsof -i :14550查占用 |
| Offboard 切换失败 | 设定值流未提前发送 | 确保先发数据再切模式 |
| ROS 2 收不到话题 | 域 ID 不一致或话题名错误 | ros2 topic list确认 |
| 飞机起飞后飘移 | 坐标系搞反或物理步长不对 | 检查 NED 坐标系和 Gazebo 参数 |
4.4 编译报错与依赖缺失的通用处理思路
PX4 和 ROS 2 的编译报错有时候很隐晦,但大部分都能归到几类:依赖包没装、版本不匹配、环境变量没配。我的习惯是先把报错信息完整看一遍,找到第一个 error 而不是最后一个,因为后面的错误往往是前面引起的连锁反应。
如果报错说某个头文件找不到,先用apt search或者dpkg -l确认那个包有没有装。如果装了但还是找不到,可能是路径问题,用find / -name "头文件名"搜一下实际位置,然后检查CMAKE_PREFIX_PATH有没有包含那个路径。
Python 相关的报错多半是 pip 包版本冲突。建议用虚拟环境隔离,别在系统 Python 里瞎装。python3 -m venv px4_env建一个干净的环境,需要什么装什么,比在系统环境里折腾省心得多。
5. 进阶扩展:从单机仿真到多机编队
5.1 多实例启动与端口分配策略
单机仿真跑通之后,下一步自然是多机编队。PX4 支持同时启动多个 SITL 实例,每个实例有独立的端口和 Gazebo 模型。启动多机的命令大概是这样:
./Tools/simulation/gazebo-classic/sitl_multiple_run.sh -n 3这个脚本会启动 3 架飞机,分别监听不同的 UDP 端口。QGC 那边需要手动添加多个连接,每个连接对应一个端口。XRCE-DDS 代理也要为每个实例单独启动,端口依次递增。
多机仿真最头疼的是端口冲突和资源占用。3 架飞机同时跑,CPU 占用会明显上升,如果机器配置不够,Gazebo 的实时性会下降,导致仿真时间变慢。我的建议是先用 2 架飞机验证编队逻辑,跑通了再往上加。另外每架飞机的 XRCE-DDS 代理要用不同的端口,比如 8888、8889、8890,启动的时候用-p参数指定。
5.2 编队控制的基本思路与代码框架
编队控制的核心是让每架飞机跟踪一个相对于编队中心的偏移位置。假设编队中心在 (0, 0, -5),三架飞机分别偏移 (-2, 0, 0)、(2, 0, 0)、(0, 2, 0),那么每架飞机的目标位置就是编队中心加上各自的偏移量。
代码层面,每架飞机跑一个独立的 Offboard 节点,订阅同一个编队中心话题,然后计算自己的目标位置并发送 TrajectorySetpoint。编队中心可以是一个虚拟点,由地面站或者另一个节点动态生成。这种架构的好处是每架飞机的控制逻辑独立,耦合度低,加飞机只需要多跑一个节点。
实际调试的时候,编队飞行的难点在于时间同步。如果各架飞机的仿真时钟不一致,编队就会散架。PX4 的 SITL 默认用锁步模式,所有实例共享同一个仿真时钟,这个问题基本不用担心。但如果你的机器性能不够,Gazebo 的实时性下降,可能会出现某架飞机的控制周期变长,导致编队误差增大。这时候可以适当降低控制频率,或者简化 Gazebo 的物理模型。
5.3 从仿真到真机的迁移注意事项
仿真跑通不代表真机就能直接用。真机上的传感器噪声、通信延迟、电池电压变化都是仿真里没有的。把 Offboard 代码往真机上搬的时候,有几个地方必须改:首先是安全阈值,仿真里可以随便飞,真机上必须加地理围栏和低电量返航逻辑。其次是控制参数,仿真里的 PID 参数在真机上大概率要重新调,因为电机响应特性和仿真模型有差异。
还有一点容易被忽略的是通信链路。仿真里 XRCE-DDS 走的是本地 UDP,延迟几乎为零。真机上如果用数传电台或者 WiFi,延迟可能几十毫秒甚至上百毫秒,Offboard 的控制频率要相应降低,不然设定值流断掉飞控就会退出 Offboard 模式。我一般会在代码里加一个看门狗,如果设定值发送失败超过一定次数就自动切回 Position 模式。
实操心得:从仿真迁移到真机的时候,先在真机上跑一遍纯手动飞行,确认硬件没问题再上 Offboard。Offboard 第一次试飞建议在开阔场地,高度不要超过 3 米,随时准备切回手动模式。别问我怎么知道这些的,说多了都是泪。
5.4 性能优化与仿真加速技巧
Gazebo 的仿真速度受物理引擎和渲染影响很大。如果你的机器跑起来很卡,可以试试关掉 Gazebo 的图形界面,用无头模式运行:
make px4_sitl gazebo HEADLESS=1无头模式下 Gazebo 不渲染画面,CPU 占用会低很多,适合跑长时间编队测试。需要看画面的时候再开 QGC 或者用 Gazebo 的远程可视化工具。
另一个加速技巧是调整 Gazebo 的物理步长。默认步长是 1ms,改成 2ms 或者 4ms 可以显著降低计算量,但会牺牲一些仿真精度。对于编队控制这种对精度要求不是特别极端的场景,适当放宽步长完全可接受。在 Gazebo 的 world 文件里找到<physics>标签,修改<max_step_size>的值就行。
PX4 那边也可以关掉一些不必要的模块来省资源。比如仿真的时候不需要真实的传感器驱动,可以在启动参数里禁用。具体哪些模块可以关,取决于你的仿真场景需要哪些数据。我一般会保留sensors、commander、navigator这几个核心模块,其他的按需开启。
6. 个人经验总结与后续学习方向
这套环境我从头搭过不下十次,每次都有新坑,但也每次都能学到东西。最开始觉得 XRCE-DDS 是多余的,直接用 MAVLink 不就行了?后来做 Offboard 控制才发现,MAVLink 的消息类型和频率限制太多,根本没法做精细控制。XRCE-DDS 虽然配置麻烦一点,但一旦跑通,后面的开发效率是指数级提升。
QGC 这边,我建议养成看日志的习惯。QGC 的 MAVLink Inspector 能实时显示所有消息,调试的时候比瞎猜有用得多。另外 QGC 的自定义命令功能可以保存常用的指令序列,比如解锁、切模式、起飞,一键执行,省去每次手动点的麻烦。
后续如果想深入,可以看看 ROS 2 和 PX4 的深度集成,比如用ros2_control做更复杂的控制算法,或者接 MoveIt 做机械臂和无人机的协同仿真。Gazebo 那边也可以试试 Ignition Fortress 的新特性,比如更好的传感器仿真和分布式仿真支持。这条路很长,但每走一步都能看到实实在在的进步。