开头
把PX4的offboard开发跑通,是我这几年在无人机方向做过最值得的一件事。如果你在网上搜过“PX4开发”,大概率会看到一堆零零散散的资料:有人讲怎么编译固件,有人贴一段mavros的Python代码,还有人直接扔给你一套仿真环境让你自己折腾。但真到了自己上手,从环境搭建到第一个offboard程序跑起来,中间隔着的不是一道坎,而是一整条沟。这篇文章就是帮你把这条沟填平用的。
先说清楚offboard是什么。简单讲,offboard是PX4的一种飞行模式,飞行控制权从遥控器或地面站转移到机载计算机,由你写的程序直接下发速度、姿态或位置指令。它解决的核心问题是:无人机能不能脱离人的操作,按照算法自主飞行。无论是航点巡航、视觉跟随、轨迹规划,还是集群编队,底层都得靠offboard模式把控制指令送进飞控。这篇文章适合刚搭好PX4环境、想跑第一个自主飞行程序的开发者,也适合那些已经在仿真里折腾过、但还没理清消息流和控制链路的同学。内容以软件在环仿真(SITL)为主线,穿插实机注意事项,全程基于我实际操作过的经验来写。
1. 先想清楚offboard模式到底在做什么
1.1 一条飞控指令背后的消息流
刚接触offboard时,最容易犯的错就是把“发指令”想得太简单。你可能以为飞控像黑盒,程序发一个坐标它就飞过去了。实际上offboard的逻辑是分层级的:机载计算机通过MAVLink协议,把期望的姿态、速度或位置打包成消息发给飞控,飞控内部的姿态控制器和位置控制器再根据当前状态去解算电机输出。
这里有个关键点:PX4默认的地面控制链路是遥测电台,速率低、延迟高,而offboard场景下机载计算机和飞控之间通常走的是高带宽的串口或WiFi。消息流大概是:机载程序 -> MAVLink消息(如SET_POSITION_TARGET_LOCAL_NED) -> PX4的mavlink模块 -> 根据当前飞行模式分发到对应的控制器 -> 控制输出 -> 电机。理解这条链路,你才能明白为什么很多offboard问题最后都出在“消息没送到”或“模式没切对”上。
1.2 控制层级:位置、速度、还是姿态?
offboard指令可以下到三个层级:姿态(ATTITUDE)、速度(VELOCITY)、位置(POSITION)。PX4内部对这些指令的处理方式完全不一样。位置指令会经过位置控制器的P和D解算,输出期望速度给速度控制器,再输出期望姿态和油门给姿态控制器,最后才是混控器。速度指令则跳过位置控制器,直接进速度控制环。
我的建议是:新手先做位置控制,再做速度,最后再做姿态。原因很简单,位置控制容错空间最大,悬停时就算指令有点抖动,飞机也只会在测量精度级别附近来回飘,不会出现姿态模式下一脚油门窜出去的情况。等你把位置控制玩明白了,对PX4的控制层级也就有了真正的体感。
2. 开发环境搭建:从Ubuntu到PX4仿真
2.1 基础环境准备
我用的环境是Ubuntu 20.04 + ROS 2 Foxy + PX4 v1.13,这套组合资料最多,踩坑最少。如果你用Ubuntu 22.04配ROS 2 Humble,也不是不行,只是PX4 v1.13对Humble的官方支持比较晚,某些中间件版本有兼容问题,新手不建议一上来就挑战。
依赖安装这些基础操作我就不流水账了。说几个容易忽略的点:PX4编译工具链里面,g++版本不能太老,否则编译px4_sitl的C++代码会报一堆模板错误;python3-jinja2、python3-numpy这些Python依赖必须装,否则生成混合器定义文件时直接报错。另外,建议把~/src这个目录专门用来放PX4和ROS2工作空间,路径里不要出现中文和空格。
# 拉取PX4固件 git clone https://github.com/PX4/PX4-Autopilot.git --recursive cd PX4-Autopilot git checkout v1.13.0 # 安装依赖(Ubuntu 20.04) bash ./Tools/setup/ubuntu.sh # 编译 make px4_sitl px42.2 编译PX4固件与运行软件在环仿真
编译完成后,运行仿真只需要一条命令:
make px4_sitl gazebo这一步会启动Gazebo仿真环境,并加载一架默认的Iris四旋翼模型。很多人到这里就卡住了:Gazebo窗口开起来,但无人机不动,控制台也没有任何输出。其实这是个正常现象,因为飞控在SITL模式下默认处于手动模式,等待遥控器输入。如果你想让它直接从offboard代码接管,需要在启动仿真前把MAV_0_CONFIG等参数提前设好,但初期建议还是保留默认参数,因为你还要在里面手动切模式、看日志。
顺便说一句,SITL仿真里PX4和Gazebo是共享时钟的吗?答案是否定的,PX4内部有自己的一套模拟时间,两者有偏移是正常的。很多人在QGroundControl里看到GPS时间对不上就以为程序错了,其实不影响控制效果。
2.3 把ROS2接进来
ROS2和PX4之间的桥接层,我推荐直接用官方维护的px4_msgs和px4_ros_com仓库。这套桥接方案最大的好处在于:不需要额外运行mavros那样的中间件,消息类型直接基于PX4的uORB。
mkdir -p ~/ws/src cd ~/ws/src git clone https://github.com/PX4/px4_msgs.git git clone https://github.com/PX4/px4_ros_com.git cd ~/ws colcon build source install/setup.bash这里有个细节:px4_ros_com默认是在PX4固件源码的同级目录下找PX4-Autopilot的,如果你的目录结构不一样,需要在CMakeLists里改PX4_SOURCE_DIR路径。我因为一开始把工作空间分开建,编译卡了好一阵,最后发现是路径没对上。
启动顺序也有讲究:先启动PX4仿真,再启动ros2桥接节点,最后才运行你的offboard程序。顺序反了会导致消息订阅不到,程序起不来或者飞控收不到指令。
3. 第一个offboard程序:从起飞悬停开始
3.1 程序框架与代码实现
我第一个offboard程序是用ROS2的C++写的。为什么不用Python?因为ROS2的Python接口在实时性上不如C++,而且PX4的offboard指令频率建议不低于10Hz、最好20Hz以上,Python的GIL和消息序列化在高频发送时容易丢帧。
下面是核心代码,我简化掉了错误处理,只保留了主干逻辑:
// offboard_test.cpp #include <chrono> #include <cmath> #include <rclcpp/rclcpp.hpp> #include <px4_msgs/msg/offboard_control_mode.hpp> #include <px4_msgs/msg/trajectory_setpoint.hpp> #include <px4_msgs/msg/vehicle_command.hpp> #include <px4_msgs/msg/vehicle_attitude.hpp> using namespace std::chrono_literals; class OffboardTest : public rclcpp::Node { public: OffboardTest() : Node("offboard_test") { offboard_control_pub_ = this->create_publisher<px4_msgs::msg::OffboardControlMode>( "/fmu/offboard_control_mode/in", 10); trajectory_pub_ = this->create_publisher<px4_msgs::msg::TrajectorySetpoint>( "/fmu/trajectory_setpoint/in", 10); command_pub_ = this->create_publisher<px4_msgs::msg::VehicleCommand>( "/fmu/vehicle_command/in", 10); timer_ = this->create_wall_timer(50ms, [this]() { publish_offboard_command(); }); } void arm() { publish_vehicle_command(px4_msgs::msg::VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0); } void engage_offboard() { publish_vehicle_command(px4_msgs::msg::VehicleCommand::VEHICLE_CMD_DO_SET_MODE, 1.0, 6.0); // 6 = OFFBOARD } private: void publish_offboard_command() { px4_msgs::msg::OffboardControlMode offboard_msg{}; offboard_msg.timestamp = this->now().nanoseconds() / 1000; offboard_msg.position = true; offboard_msg.velocity = false; offboard_msg.acceleration = false; offboard_msg.attitude = false; offboard_msg.body_rate = false; offboard_control_pub_->publish(offboard_msg); px4_msgs::msg::TrajectorySetpoint trajectory_msg{}; trajectory_msg.timestamp = this->now().nanoseconds() / 1000; trajectory_msg.position = {0.0f, 0.0f, -5.0f}; // NED坐标,Z轴向下,-5即飞行到5米高度 trajectory_msg.yaw = 0.0f; trajectory_pub_->publish(trajectory_msg); } void publish_vehicle_command(uint32_t command, float param1, float param2 = 0.0f) { px4_msgs::msg::VehicleCommand msg{}; msg.timestamp = this->now().nanoseconds() / 1000; msg.param1 = param1; msg.param2 = param2; msg.command = command; msg.target_system = 1; msg.target_component = 1; msg.source_system = 1; msg.source_component = 1; msg.from_external = true; command_pub_->publish(msg); } rclcpp::Publisher<px4_msgs::msg::OffboardControlMode>::SharedPtr offboard_control_pub_; rclcpp::Publisher<px4_msgs::msg::TrajectorySetpoint>::SharedPtr trajectory_pub_; rclcpp::Publisher<px4_msgs::msg::VehicleCommand>::SharedPtr command_pub_; rclcpp::TimerBase::SharedPtr timer_; };3.2 参数设置与模式切换
在启动程序前,有两个参数必须确认。第一个是COM_RC_IN_MODE,如果设置为默认,飞控会等待遥控器信号,没有RC信号时offboard指令不会生效。在纯offboard场景下,建议设为0(禁用RC),这样飞控就不会因为遥控器失联而自动退出offboard。第二个是NAV_RCL_ACT,这个是“遥控器失联行为”,我建议设为0(保持offboard),但前提是你对程序有绝对信心——不然就设成1(返航),给自己留条后路。
还有一个容易忽略的细节:PX4对offboard有一个“超时保护”机制。如果飞控在COM_OF_LOSS_T(默认1秒)内没有收到新的offboard指令,会自动退出offboard模式。所以程序里发布指令的循环一定不能停,频率也不能低于10Hz。我见过有人把指令放在一个if条件里,条件不满足就一直不发,结果飞机飞着飞着突然掉回自稳模式,当场翻车。
3.3 怎么验证它真的在“offboard”运行
跑起来之后,怎么确认程序真的把指令送进飞控了?最直接的验证方式是看QGroundControl的飞行模式指示。如果切到了offboard,界面上会显示绿色的“OFFBOARD”。但更可靠的方式是看PX4控制台日志:
pxh> commander: using new offboard control mode这条消息出现,说明飞控已经收到了offboard模式切换指令。接下来观察飞机位置是否在朝着-5米高度移动。SITL仿真里的GZCS模型有大约1米的定位误差,飞机稳定在-4.7米到-5.3米之间都算正常。
另外一个验证技巧:直接把代码里的目标位置改成{1.0f, 2.0f, -5.0f},看看飞机是否会从悬停点向量(1, 2, -5)方向移动。如果飞机反应了但方向不对,先检查坐标系定义;如果完全没反应,多半是消息没进飞控,先去排查桥接节点是否启动。
4. 进阶:从悬停到轨迹跟踪与编队的思考
4.1 串级PID与控制周期
跑通悬停之后,你可能会想:为什么我的飞机飞得不“直”?为什么轨迹上一顿一顿的?这里就要提到PX4控制器的核心——串级PID。PX4的位置控制环和姿态控制环是分内外层的:外环是位置或速度,内环是姿态角。外环输出作为内环的输入,率在10~50Hz,姿态环则跑到250~400Hz。
如果轨迹跟踪效果差,八成不是PID参数没调好,而是你的指令频率太低或时间戳不连续。PX4的轨迹指令里带时间戳,如果时间戳忽大忽小,控制器会认为指令乱序,直接忽略或者产生跳变。我在仿真里测过:20Hz发送和50Hz发送,同样是(1, 2, -5) -> (2, -1, -3) -> (0, 0, -4)这条轨迹,50Hz下的位置误差能小一个量级,而且没有明显折线感。
4.2 从单机到编队的扩展问题
单机offboard跑通后,很多人会想玩编队。这里给你泼盆冷水:直接用多机SITL做编队仿真,结构复杂度不比实机低多少。
编队的核心问题有两个:一是时间同步,二是消息路由。PX4的target_system和target_component字段在单机里可能无所谓,但在多机场景下一定要精确区分,否则A飞机的指令可能发到B飞机上。我在多机仿真里用的方案是:每架机跑一个独立的PX4 SITL实例,通过不同的UDP端口通信,机载计算机端用ROS2的命名空间区分话题,比如/uav1/fmu/offboard_control_mode/in、/uav2/fmu/offboard_control_mode/in。
编队控制算法倒是相对独立,可以是虚拟结构法、领航跟随法或者一致性方法。我推荐新手先试领航跟随:一架飞机飞固定轨迹,其他飞机跟踪领航者的相对位置。实现时只需要把每架飞机的目标位置都换算成世界系坐标,再用单机的offboard下发就行了。实验时先在地面把所有轨迹算好,再一次性启动所有节点,不要在飞行过程中动态改参数——多机场景下任何一个节点掉链子,整个队形都会散。
5. 开发踩坑实录:这些问题我几乎都遇到过
5.1 编译与仿真阶段的坑
第一个坑:PX4固件编译到一半报[Errno 13] Permission denied。这个八成是ubuntu.sh装的依赖没给全,或者PX4源码目录的属主不对。解决办法很简单:
sudo chown -R $USER:$USER ~/src/PX4-Autopilot第二个坑:make px4_sitl gazebo启动后Gazebo黑屏或闪退。这个一般是显卡驱动问题,或者是Gazebo版本和PX4自带的models不匹配。我遇到过最离谱的一次是系统显卡驱动更新后,PX4自带的Iris模型渲染不出机体纹理,看起来像隐身了。解决办法是检查~/.gazebo/models下是否有Iris模型缓存,有就删掉重新下。
第三个坑:ROS2桥接节点启动后,px4_msgs的消息定义和固件实际uORB消息定义不匹配。这个多发生在你不小心把px4_msgs从main分支拉下来,而固件是旧的v1.13。两个仓库的px4_msgs是跟着固件版本走的,一定要自己切分支,不要盲信main是最新版就会自动适配。
5.2 消息通信与参数设置的坑
offboard不生效,先检查这个:vehicle_command消息里的confirmation字段不是0。很多人用其他语言的MAVLink库发消息时,会顺手把confirmation设成1(表示“需要确认”),但PX4对confirmation非0的响应处理完全不同,会导致切换指令不被执行。在ROS2的px4_msgs里,这个字段默认是0,但如果你自己拼MAVLink裸消息,一定注意别踩。
还有一个大坑:在SITL仿真里,vehicle_command的source_system和source_component要填1,但实机上必须填机载计算机的真实MAVLink系统ID,否则地面站和机载程序会打架,都认为自己的指令是合法的。我自己的习惯是机载计算机系统ID固定为240,地面站为255,互不冲突。
参数设置上,MAV_1_CONFIG和MAV_1_MODE这两个参数直接影响机载计算机的MAVLink通道能不能正常工作。默认的MAV_1_CONFIG对应的是TELEM 1口,如果你接的是TELEM 2或者USB,没改参数之前,机载程序的消息根本不会进飞控。
5.3 实机前的安全检查清单
仿真跑通、实机前,有一份清单我每次必过。你现在可以先收藏,后面一定用得上:
| 检查项 | 操作方式 | 通过标准 |
|---|---|---|
| 遥控器安全模式 | 设置RC_MAP_MODE_SW与RC_MAP_ARM_SW | 可随时切回自稳/定高并手动解锁/上锁 |
| offboard超时保护 | 确认COM_OF_LOSS_T设置 | 断链时按预设行为执行,而不是失控乱飞 |
| 电池电压阈值 | 校验BAT_CRIT_THR与BAT_EMER_THR | 自动返航或紧急降落触发时机合理 |
| 罗盘/加速度计校准 | 完成全部磁力计与加速度计校准 | QGC界面无红色告警 |
| 桨叶方向与混控器 | 手动推油确认电机方向 | 与实际桨叶安装完全一致 |
| GPS信号质量 | 至少搜到10颗星以上,HDOP小于1.5 | 位置环定位稳定,无漂移 |
| 代码异常保护 | 程序内加“指令冻结”逻辑 | 意外异常时能保持悬停而不是飞向未知方向 |
最后一条要展开讲讲。即使你做了所有参数和保护设置,代码逻辑本身也可能出bug。如果程序因为数组越界崩溃,ROS2节点直接退出,飞控在COM_OF_LOSS_T秒后自动退出offboard,回到之前的落体状态——这非常危险。所以我建议:程序里加入“死亡时间”逻辑,循环里正常发offboard指令,但一旦超过500毫秒没有任何主循环心跳,就切换到位置保持的setpoint(原地悬停),同时打印告警日志。这个机制在实机测试中救过我两次。
结尾
老规矩,最后再分享一个小技巧。不管是SITL还是实机,第一次飞offboard,目标高度永远设低一点,比如1米到2米之间。不要一上来就设5米。不是因为飞机飞不上去,而是你在初期调试的时候,大概率需要多次调整参数、重启程序,飞机离地越低,容错空间越大。等位置控制足够稳定,再一层一层往上升高度、加轨迹复杂度就行。
我个人在实际操作中的体会是:PX4的offboard开发,70%的精力都消耗在环境搭建和消息通信上,真正写控制逻辑只占了很小一部分。但这不意味着控制算法不重要——恰恰相反,只有把前面那70%的基础打牢,你对控制算法的理解才能真正落到代码里。把这篇内容里的环境、流程和坑都过一遍,你的第一个自主飞行程序就不远了。