
干这行这么久我最大的感触是无人机轨迹跟踪看着不难但真正做起来七八成时间都耗在环境搭建和调试上。尤其是刚接触PX4的朋友一上来就想在真机上跑轨迹我一般都会劝一句先别急。把PX4、Gazebo、ROS2这套仿真环境跑通把轨迹跟踪这条路完整走一遍成本低、迭代快而且是通向真机验证最稳妥的一条路。这套组合之所以值得花时间搭是因为它覆盖了无人机开发的完整链路——从飞控固件编译、仿真物理环境到任务指令下发、状态反馈闭环每一步都能在仿真里提前踩坑。这篇文章我不打算给你抄官方文档式的步骤堆砌而是以我实际搭建过程为线把“为什么这么配”“卡在哪”“怎么排查”都讲清楚。内容偏全栈实战适合已经装过Ubuntu、对ROS2有基本概念、但还没把PX4仿真跑通的同学。当然如果你是零基础耐心对着每一步做也能跑起来就是有些概念可能需要额外查一下。1. 为什么选PX4GazeboROS2这套组合做轨迹跟踪先聊点实在的。轨迹跟踪本质上是“期望轨迹的生成”加“位置/速度控制器跟踪”两层问题。而PX4本身就带一套相当完整的姿态内环和位置外环控制器Gazebo提供带物理属性的模拟环境ROS2负责把期望轨迹和状态反馈串起来。三者合在一起刚好覆盖了从“发一条轨迹指令”到“飞机实际飞过这条轨迹”的完整闭环。1.1 三个组件各自干了什么PX4在这里不只是“一个飞控固件”那么简单。它内部有姿态估计、状态估计、位置控制、姿态控制、混控输出等一整套模块。我们在轨迹跟踪场景里通常用它的Offboard模式也就是外部ROS2节点通过MAVLink/MAVSDK把期望位置、速度或姿态指令发给它PX4自己完成底层控制。这意味着你不需要自己写姿态环只需要关注“怎么生成期望轨迹”和“怎么发指令”。Gazebo在这套组合里的角色是“物理沙盒”。它负责模拟无人机质量、惯量、气动阻尼、地面接触、传感器噪声等。这也意味着你写出来的控制器不是在一个完美世界里跑的Gazebo里感受到的延时、噪声、漂移和真机有一定的可比性。Gazebo界面闪、模型不加载这类问题后面我会专门写一节排查。ROS2则负责三件事通信、任务编排、可视化。它把PX4的状态消息接进来把控制指令发出去同时还能把轨迹点、当前位置实时发到RVIZ2里显示。说实话没有ROS2这套组合也能跑因为PX4自带仿真脚本但那样你就只能看着Gazebo里的飞机飞拿不到中间数据更不方便自定义轨迹。加上ROS2之后整个链路变得透明可控。1.2 版本选型为什么我建议Ubuntu 22.04 ROS2 Humble PX4 v1.14.3版本选型是这套环境里最容易踩坑的地方没有之一。我在早期用Ubuntu 20.04 ROS2 Foxy PX4 1.12Gazebo 9结果因为Gazebo和ROS2的插件版本不兼容光编译就耗掉一整天。后来整体切换到Ubuntu 22.04 ROS2 Humble PX4 v1.14.3稳定性高了很多。Ubuntu 22.04是目前ROS2 Humble官方支持最好的系统版本而Humble又是LTS长期支持社区的教程、答疑数量都远超其他发行版。PX4 v1.14.3这个版本对Gazebo Classic也就是Gazebo 11的支持非常成熟自带的sitl_gazebo插件接口稳定同时支持Offboard模式的标准MAVLink消息流。相比之下更新的PX4版本开始迁移到Gazebo Sim也就是Ignition插件改名、坐标系变化、话题重映射很多老教程直接失效新手很容易一头雾水。提示如果你是第一次搭建议严格按照“Ubuntu 22.04 ROS2 Humble PX4 v1.14.3 Gazebo 11”这套组合来不要追求最新版。稳定压倒一切。1.3 数据流视角一条轨迹指令是怎么让仿真飞机动起来的理解了数据流你的排查能力会提升一个档次。整条链路是ROS2节点发布期望轨迹话题比如期望位置→ PX4通过特定消息通道通常是MAVLink的SET_POSITION_TARGET_LOCAL_NED接收 → PX4内部的位置控制器计算期望姿态 → 姿态控制器计算期望角转速 → 混控器把角转速映射到四个电机PWM → Gazebo里的多旋翼模型被物理引擎推动。反过来Gazebo里的模型状态会被imu和气压计/ GPS插件采样经PX4的EKF2或LPE估计器融合成位置、速度、姿态估计再通过MAVLink发布给ROS2节点。也就是说ROS2节点既是“发令员”又是“观察员”。理解了这条数据流后面你看到“飞机不动”“飞机乱飞”“轨迹跟踪滞后”这类问题就能顺着链路一层层定位。2. 环境搭建Ubuntu 22.04 ROS2 Humble PX4 从零起步这一节是实际操作量最大的部分我会把我跑通的步骤完整过一遍同时标出哪些地方容易出问题。2.1 系统准备与ROS2 Humble安装系统建议直接装Ubuntu 22.04桌面版虚拟机也能跑但如果你要Gazebo的图形界面流畅显示我还是建议双系统或物理机安装。虚拟机里Gazebo渲染容易卡尤其是界面一直闪的问题虚拟机里出现的概率高很多。ROS2 Humble的安装我推荐两种方式。一种是手动按官方APT源装逐步配置适合想搞懂依赖关系的朋友。另一种是直接用一些社区维护的一键安装脚本比如“鱼香ROS”的一键安装实测下来确实省心它会自动配置镜像源、安装基础组件、初始化rosdep。我的建议是网络条件好就手动装装的过程本身能帮你理解ROS2的包管理逻辑赶时间或遇到奇奇怪怪的依赖问题就用一键脚本先把环境跑起来再说。装完以后务必手动验证一遍source /opt/ros/humble/setup.bash ros2 run demo_nodes_cpp talker另开终端source /opt/ros/humble/setup.bash ros2 run demo_nodes_cpp listener如果两端能互相收到消息说明ROS2的基础通信没问题。这里我提醒一句每次新开终端都要source为了省事可以在~/.bashrc里加上echo source /opt/ros/humble/setup.bash ~/.bashrc2.2 PX4源码获取与编译PX4源码克隆时需要注意太大建议加--recursive把子模块一次性拉下来。默认分支可能已经更新到新版本所以我建议直接切到你想要的稳定版本标签。git clone https://github.com/PX4/PX4-Autopilot.git --recursive cd PX4-Autopilot git checkout v1.14.3 git submodule update --init --recursive编译之前需要做两件事安装依赖和初始化子模块。PX4官方提供了一个自动脚本bash ./Tools/setup/ubuntu.sh这个脚本会把编译工具链、Python依赖、Gazebo相关包一次性装齐。注意脚本执行过程中可能会提示你重启或重新登录务必照做否则后续编译会报工具链版本错误。装完依赖后编译PX4 SITLmake px4_sitl gazebo-classic第一次编译会非常久十几分钟到半小时都很正常取决于机器性能。编译成功后应该会自动弹出Gazebo窗口里面有一架iris无人机终端里出现pxh提示符。看到这个恭喜你PX4仿真已经跑起来了。2.3 Gazebo模型加载与PX4-ROS2桥接配置PX4自带Gazebo仿真虽然能跑但要把数据接到ROS2里还需要安装px4-ros2-interface或者使用官方推荐的Micro XRCE-DDS Agent方式。在PX4 v1.14.x里PX4和ROS2通过eProsima Micro XRCE-DDS桥接。具体来说PX4固件侧编译时已经包含了XRCE-DDS客户端我们需要在宿主机上运行一个Micro XRCE-DDS Agent来建立UDP通信。实测下来最顺的方法是直接用Docker镜像跑Agentdocker run -it --rm -v /dev:/dev -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY$DISPLAY --network host px4io/px4-micrortps-agent:humbled如果Docker不方便也可以从源码编译Agent。这一步完成之后PX4和ROS2之间会形成话题映射比如/fmu/out/vehicle_local_position会映射到ROS2侧的/px4_ros2/vehicle_local_position。你可以在ROS2终端里验证ros2 topic list如果看到一堆/px4_ros2/...开头的话题说明桥接成功数据在正常流动。3. 轨迹跟踪核心原理位置环、速度环和指令优先级环境跑通只是第一步。真要做轨迹跟踪你得理解PX4是怎么响应你的指令的否则会出现“我发了位置指令它不动”“它飞了但完全不是我要的轨迹”这类问题。3.1 PX4多层控制结构从期望位置到电机转速PX4的控制结构可以简化成三层位置控制外环、速度控制中间环、姿态与角速度控制内环。当你在Offboard模式下发送一个期望位置时PX4内部的位置控制器会先算出期望速度速度控制器再算出期望姿态即期望的Roll/Pitch角和推力最后姿态控制器算出期望角速度由混控器分配成四个电机的PWM输出。这个结构对轨迹跟踪意味着什么意味着你发送的轨迹指令如果只是“离散的位置点”PX4会自己规划点与点之间的速度曲线跟踪效果受它内部参数影响。如果你想做更精细的轨迹跟踪比如最小加加速度轨迹或多项式轨迹就应该把期望位置、期望速度甚至期望加速度一起发给PX4它才能跟踪得更平滑。3.2 Offboard模式怎么正确地发指令Offboard是PX4里最容易让人“劝退”的模式因为它的进入条件和超时机制非常严格。PX4规定要进入Offboard模式飞控必须先连续收到频率不低于2Hz的期望指令通常要求20Hz以上并且维持一段时间否则会自动切回降落模式。很多人的飞机“一进入Offboard就掉高”就是因为指令频率不够或者第一个指令就是非零位置导致飞控状态估计还没稳住。我的建议是先把无人机用QGroundControl切到Offboard之前让它在仿真里先起飞或者直接发送悬停指令保持当前高度再逐渐切换。更规范的流程是程序启动时先以20Hz发送当前飞机位置作为期望位置持续1到2秒然后再切换Offboard再发目标轨迹。3.3 PX4里的状态估计卡尔曼滤波在轨迹跟踪中的作用PX4的状态估计器EKF2扩展卡尔曼滤波是整个控制链路的“眼睛”。在Gazebo仿真里EKF2融合IMU、GPS、气压计和磁力计的数据输出位置、速度和姿态估计。轨迹跟踪的性能上限很大程度上取决于状态估计的精度和延迟。很多人忽略的一点是仿真里的GPS是理想模型而真机的GPS会有漂移和更新率限制所以你在仿真里调好的轨迹跟踪参数拿到真机上往往会过冲或震荡。我习惯在Gazebo的GPS插件里人为加一点高斯噪声让仿真更接近真机。EKF2有完整的卡尔曼增益推导和协方差更新逻辑虽然平时不需要手动改它的源码但理解它的方差来源对调试“轨迹跟踪飘了”这类问题非常有帮助。4. 实战编写ROS2节点发布期望轨迹下面进入核心环节。我会以一个简单的“起飞后跟踪圆形轨迹”为例完整写一个ROS2节点并通过PX4的Offboard接口发布期望轨迹。4.1 创建ROS2功能包与目录结构先创建你的工作空间。我习惯把PX4源码和工作空间分开这样更新固件时不会污染自己的代码。mkdir -p ~/ws_trajectory/src cd ~/ws_trajectory/src ros2 pkg create --build-type ament_python px4_trajectory_tracking cd ~/ws_trajectory colcon build source install/setup.bash这个包结构里我们需要关心的是px4_trajectory_tracking/px4_trajectory_tracking/下面那个Python包的源码目录。ROS2的Python包默认入口在setup.py里配置后面写好了节点可以逐个加。4.2 核心话题与消息类型车辆指令与状态订阅PX4和ROS2桥接之后常用的话题有这些/px4_ros2/vehicle_local_position车辆本地位置NED坐标系/px4_ros2/vehicle_attitude车辆姿态四元数/fmu/in/offboard_control_modeOffboard控制模式使能/fmu/in/trajectory_setpoint轨迹设定点位置/速度/加速度/fmu/in/vehicle_command模式切换指令需要注意PX4的坐标系是NED北东地而很多轨迹规划算法习惯用ENU东东北上。转换不当会导致飞机朝反方向飞。我在代码里会统一用NED如果你对IMU的ENU坐标系更熟务必在接口处做转换。4.3 代码实现起飞、圆形轨迹跟踪、降落三段式下面是一个简化但完整的Python节点代码我加了很多注释方便你理解逻辑。import rclpy from rclpy.node import Node from px4_msgs.msg import OffboardControlMode, TrajectorySetpoint, VehicleCommand, VehicleLocalPosition import math import time class TrajectoryTracker(Node): def __init__(self): super().__init__(trajectory_tracker) # 发布Offboard控制模式、轨迹设定点和模式切换指令 self.offboard_mode_pub self.create_publisher(OffboardControlMode, /fmu/in/offboard_control_mode, 10) self.trajectory_pub self.create_publisher(TrajectorySetpoint, /fmu/in/trajectory_setpoint, 10) self.vehicle_command_pub self.create_publisher(VehicleCommand, /fmu/in/vehicle_command, 10) # 订阅本地位置用于初始化悬停点 self.local_pos_sub self.create_subscription(VehicleLocalPosition, /px4_ros2/vehicle_local_position, self.local_pos_callback, 10) self.local_position None self.timer self.create_timer(0.05, self.timer_callback) # 20Hz控制指令 self.state ARM_AND_OFFBOARD self.start_time time.time() self.hover_position None # 起飞后悬停点 def local_pos_callback(self, msg): self.local_position msg def publish_offboard_control_mode(self): msg OffboardControlMode() msg.timestamp int(time.time() * 1e6) msg.position True msg.velocity False msg.acceleration False msg.attitude False msg.body_rate False self.offboard_mode_pub.publish(msg) def publish_vehicle_command(self, command, param10.0, param20.0): msg VehicleCommand() msg.timestamp int(time.time() * 1e6) 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 self.vehicle_command_pub.publish(msg) def arm_and_offboard(self): # 先发布当前坐标为期望点持续一段时间 if self.local_position is None: return setpoint TrajectorySetpoint() setpoint.timestamp int(time.time() * 1e6) setpoint.position[0] self.local_position.x setpoint.position[1] self.local_position.y setpoint.position[2] self.local_position.z setpoint.yaw 0.0 self.trajectory_pub.publish(setpoint) # 按顺序发送arming和offboard指令 if ARM in self.state: self.publish_vehicle_command(VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0) self.get_logger().info(Arming...) self.state WAIT_ARM elif OFFBOARD in self.state: self.publish_vehicle_command(VehicleCommand.VEHICLE_CMD_DO_SET_MODE, 1.0, 6.0) # 6 Offboard self.get_logger().info(Switching to Offboard...) self.state WAIT_OFFBOARD def takeoff_and_track(self): if self.hover_position is None and self.local_position is not None: self.hover_position [self.local_position.x, self.local_position.y, -2.0] # 上升到2米高度 if self.local_position is None: return current_z self.local_position.z # 先上升到悬停高度 if current_z -1.8: setpoint TrajectorySetpoint() setpoint.timestamp int(time.time() * 1e6) setpoint.position[0] self.hover_position[0] setpoint.position[1] self.hover_position[1] setpoint.position[2] self.hover_position[2] setpoint.yaw 0.0 self.trajectory_pub.publish(setpoint) else: # 圆形轨迹半径2米周期20秒 t time.time() - self.start_time radius 2.0 omega 2.0 * math.pi / 20.0 x radius * math.cos(omega * t) y radius * math.sin(omega * t) z -2.0 setpoint TrajectorySetpoint() setpoint.timestamp int(time.time() * 1e6) setpoint.position[0] x setpoint.position[1] y setpoint.position[2] z setpoint.yaw 0.0 self.trajectory_pub.publish(setpoint) def timer_callback(self): self.publish_offboard_control_mode() if self.state.startswith(ARM) or self.state.startswith(WAIT): self.arm_and_offboard() elif self.state TRACK: self.takeoff_and_track() def main(argsNone): rclpy.init(argsargs) node TrajectoryTracker() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()这段代码核心逻辑分三部分第一阶段发布当前点为期望点并解锁、切Offboard第二阶段等待飞机上升到设定高度第三阶段发送圆形轨迹点。注意PX4在Gazebo里默认是模拟起飞状态可能已经有一个初始高度所以实际使用时需要根据你的飞机状态调整悬停高度判断。不要硬套我的-1.8阈值要看你飞控输出的局部Z坐标。4.4 启动顺序与完整运行流程运行之前先启动Micro XRCE-DDS Agent再启动PX4 SITL仿真最后运行你的ROS2节点。# 终端1启动PX4 SITL Gazebo cd ~/PX4-Autopilot make px4_sitl gazebo-classic # 终端2启动Agent docker run -it --rm --network host px4io/px4-micrortps-agent:humbled # 终端3source工作空间并运行节点 cd ~/ws_trajectory source install/setup.bash ros2 run px4_trajectory_tracking trajectory_tracker运行后打开QGroundControl你应该能看到无人机先原地解锁然后上升到2米接着开始画圆。如果一切顺利在RVIZ2里订阅/px4_ros2/vehicle_local_position和你的期望轨迹话题可以画出两条轨迹做对比。这里的对比很重要控制好不好看轨迹误差一目了然。5. 常见问题与排查技巧实录这一节是我最想写的部分因为这些问题全是实际运行中高频出现的而且很多坑不是看官方文档能知道的。5.1 Gazebo界面一直闪或黑屏热词里有个高频问题“为什么gazebo界面一直在闪”我深有体会。最常见的原因是显卡驱动或OpenGL渲染问题。解决思路按顺序试关闭Gazebo的GPU加速。在~/.bashrc里加export LIBGL_ALWAYS_SOFTWARE1强制用CPU渲软能解决大部分闪屏问题代价是有点卡。如果是虚拟机务必安装VMware/VirtualBox的增强工具并启用3D加速。升级显卡驱动。NVIDIA显卡建议安装官方驱动不要用开源的nouveau。我在自己的NVIDIA机器上遇到过一次Gazebo界面每隔几秒闪一下的情况最后发现是驱动版本和Xorg不匹配升级驱动后彻底解决。记住Gazebo渲染依赖OpenGL显卡驱动的锅占了一半。5.2 PX4编译失败子模块和Python依赖问题编译PX4最常见的失败原因是子模块没拉全。如果你git clone的时候没有加--recursive或者网络中断导致部分子模块缺失编译就会报各种奇怪的找不到头文件错误。解决办法cd ~/PX4-Autopilot git submodule update --init --recursive另一个常见问题是Python依赖版本冲突。PX4编译脚本会用到一些特定版本的Python包比如jinja2、numpy、empy。如果你之前装过ROS2或Gazebo可能已经把某些包升级到不兼容的版本。建议用一个干净的Python虚拟环境跑PX4编译脚本或者严格按官方ubuntu.sh脚本安装依赖。5.3 ROS2话题收不到PX4数据桥接成功后ros2 topic list能看到话题但ros2 topic echo收不到数据这种情况一般是Agent版本和PX4固件版本不匹配。我在PX4 v1.14.3上曾经误用了为旧版本编译的Agent导致消息格式对不上。重新拉最新版本的Agent镜像后恢复正常。还有一种情况是网段问题。PX4 SITL的微RTPS默认绑定UDP端口是8888Agent需要和它在同一网络命名空间下。用Docker跑Agent时必须加--network host否则容器内无法访问宿主机的UDP端口。5.4 飞机进入Offboard后直接掉落或乱飞这个问题几乎每个做Offboard的人都遇到过。核心原因是“解锁和Offboard切换时飞控还没收到稳定的期望位置指令”。飞机在Gazebo里初始位置可能不为零你的程序如果一上来就发(0,0,-2)的期望点飞控就会认为要飞到原点导致飞机水平漂移。我的经验是程序启动后先订阅几秒钟/px4_ros2/vehicle_local_position把当前位置作为期望位置以20Hz持续发送然后再切Offboard。等飞控进入Offboard稳定后再慢慢给目标轨迹。另外Offboard模式下如果控制指令中断超过0.5秒PX4会自动退出Offboard并执行降落所以你的控制循环频率至少要稳定在10Hz以上最好20Hz。5.5 轨迹跟踪误差大或明显滞后如果飞机能飞但跟踪圆形轨迹时明显滞后或者轨迹变成一个畸形的椭圆先别急着调PX4的PID。先确认你有把期望速度也发给PX4。只发位置点PX4内部的位置控制器会自己算速度但这个速度规划往往比较保守跟踪快速轨迹时必然滞后。改进方案是同时开启速度前馈即把期望轨迹点计算位置的同时计算出该点的期望速度一并填入TrajectorySetpoint里。PX4的轨迹设定点消息同时支持位置、速度和加速度字段你用得多跟踪效果就会越好。6. 几个值得记住的调参和开发习惯环境能跑、代码能飞之后你会发现真正花时间的不是“跑通”而是“调好”。这里我分享几个自己的习惯希望能帮你少走弯路。6.1 用RVIZ2做轨迹对比不要只盯GazeboGazebo里看飞机飞只能感受个大概。真正判断轨迹跟踪好坏建议在RVIZ2里同时显示期望轨迹和实际轨迹。具体做法是ROS2节点里把期望轨迹点以visualization_msgs/msg/Marker或Path消息发布实际位置从/px4_ros2/vehicle_local_position读取并拼成Path。两个轨迹叠在一起误差一目了然。初期调参时我甚至会把期望轨迹的Marker尺寸调大一些方便肉眼观察偏差方向。6.2 PX4调参别猛调学会用日志回放很多同学一见到跟踪误差大就疯狂调位置环的P值结果飞机越调越抖。实际上PX4的日志系统非常强大仿真模式下会用.ulg格式自动记录完整的飞行数据。你可以用ulog2csv命令把日志转成CSV在Python里画出期望位置、实际位置、速度误差、姿态指令的曲线先定位是哪一个环节出了问题再针对性地调参。这个习惯能让你少炸很多次“机”。6.3 扩展方向从圆形轨迹到任意轨迹再到真机圆形轨迹只是第一步。你可以很自然地把轨迹生成器改成多项式轨迹或者读取一组航点做线性插值再进阶一点可以结合最小加加速度规划或贝塞尔曲线。等轨迹生成和跟踪稳定了可以去尝试PX4的二次开发比如修改位置控制器的参数适应快速机动或者把EKF2的噪声参数调得更接近真机甚至接入八叉树地图做避障导航这也是现在研究里常做的事。历经多少次失败后才有的体会搭建这套环境我第一次用时将近一周中间踩了无数个坑。现在回头看最值钱的不是“搭好了”而是“知道为什么搭不好”。PX4、Gazebo、ROS2每一层都有大量的隐藏细节版本兼容性、坐标系、话题频率、消息格式任何一个环节出了问题都会以各种奇怪的现象呈现。建议你一步一步来每完成一个阶段就做好记录这对后面排查问题会非常有帮助。先跑通再调好这是我在这个项目上最大的体会。