☰
PX4与ROS协同仿真:从键盘控制到自主飞行航点任务全指南
2026/9/29 19:23:08 网站建设 项目流程

我见过太多人,刚燃起做无人机的心,就兴冲冲买了套件回来捣鼓真机,结果往往是一天改飞控参数,一台飞机摔了;再改第二天,电机烧了;GPS没校准好,飞起来像个醉汉。我自己早年也走过这条路,直到切换到PX4 与 ROS 协同仿真这条开发管线,才真正把“改代码、试飞、调参”这个循环从按天计算压缩到按分钟计算。这篇博文就围绕这条主线展开,从最基础的键盘控制一台虚拟无人机开始,一步步搭建仿真环境,最后实现自主飞行航点任务。适合刚入门飞控开发、准备做无人机方向毕设,或者已经玩过 ROS 小车想横向拓展到无人机的朋友。

很多人在这一步最大的误区,是想直接跳过基础,一上来就跑 SLAM、跑避障、跑视觉跟随。真出了问题,根本分不清是 PX4 飞控的问题、MAVROS 转发的问题,还是上层算法的问题。我建议老老实实先把键盘控制这条链路跑通,再谈自主飞行。

1. 协同仿真为什么是一笔划算的“零成本试错”投资

1.1 三个软件到底各管什么

很多初学者容易把 PX4、ROS、Gazebo 三者的边界搞混,先把这个搞清楚,后面才不会两眼一抹黑。

PX4是开源飞控固件,它把 IMU、GPS、气压计、磁力计这些传感器数据读进来,跑姿态估计、位置估计、姿态控制和位置控制,最终输出电机转速指令。在仿真环境里,它编译成普通 Linux 进程运行,叫 SITL(Software In The Loop)。对 PX4 来说,它感知到的世界就是从 Gazebo 模拟出来的传感器数据,它输出的控制指令也真实作用在虚拟飞机上。

ROS是连接飞控和上层算法的“神经网络”。飞控本身并不理解什么叫“去三号航点”,它只理解速度指令、位置指令、姿态指令这些底层控制目标。ROS 负责把人的意图或算法的决策,翻译成飞控能看懂的指令,再通过 MAVROS 这个桥接节点,以 MAVLink 协议发给 PX4。同时,PX4 的状态估计结果、传感器数据也会通过 MAVROS 回传到 ROS,供上层算法使用。

Gazebo是一个物理仿真环境,它负责提供一个虚拟世界:重力、空气阻力、碰撞、地面、光照、传感器噪声。没有 Gazebo,PX4 就成了没有身体的“大脑”,没有物理反馈,控制算法根本无法闭环。

1.2 键盘控制是整个开发链路的“最小闭环”

很多人觉得用键盘控制无人机太小儿科,实际上这恰恰是理解协同仿真数据流的最佳方式。你按下一个按键,数据要从键盘事件一路流到 Gazebo 里的虚拟电机,再流回你的终端显示,这个闭环里覆盖了 ROS 话题通信、MAVROS 协议转换、PX4 控制模式切换、Gazebo 物理反馈,几乎是整个开发框架的微缩版。

把这条链路走通之后,再去做自主飞行,本质上只是把“人按键盘”替换成“代码发指令”,整体架构没有任何变化。所以我在带人入门的时候,一定会要求先跑通键盘控制,否则后面遇到问题会非常难排查。

1.3 这套方案适合哪些人

我总结了一下,下面几类人最适合从这套仿真流程入手:

  • 无人机相关专业学生,毕设或课程项目需要验证控制算法、路径规划算法。
  • ROS 开发者,想从地面机器人扩展到空中机器人,但不想一开始就冒着炸机风险。
  • 硬件爱好者,想在买新飞控和机架之前,比较不同参数对飞行的影响,仿真先行。
  • 行业项目预研人员,比如做电力巡检、农业植保航线规划的方案验证。

硬件方面要求不高,一台 8GB 内存以上的普通电脑就能跑得很舒服。如果你有 NVIDIA 显卡,Gazebo 渲染会更流畅,但并不是必须。

2. 从零搭环境:PX4 + ROS + Gazebo 的版本选型和安装实战

2.1 版本搭配:为什么我推荐 Ubuntu 20.04 + Noetic + PX4 1.14

版本选型是整个环境搭建里最容易被轻视的一步。很多人图新鲜直接用 Ubuntu 22.04 + ROS 2 Humble,然后发现很多老教程是基于 ROS 1 写的,Ctrl+C 复制下来的命令跑都跑不通,心态直接爆炸。

我的建议非常明确:新手老老实实用 Ubuntu 20.04 + ROS Noetic + PX4 v1.14.x + Gazebo 11 + MAVROS。

组件推荐版本说明
操作系统Ubuntu 20.04生态最成熟,ROS Noetic 官方支持
ROSNoeticROS 1 最后一个长期支持版本,教程数量碾压 ROS 2
PX4v1.14.3当前最稳定的版本,官方文档丰富
GazeboGazebo 11Ubuntu 20.04 默认版本,PX4 支持度高
MAVROSNoetic 对应版本稳定可靠,社区排错经验多

有朋友会问,现在 ROS 2 不是大势所趋吗?确实,但从学习成本和资源丰富度来看,ROS 1 Noetic 在无人机生态里依然是主流。PX4 官方大量示例、MAVROS 的文档、各种开源项目,绝大多数都是 ROS 1 环境的。先用 Noetic 把整个机制跑通,后面想迁移到 ROS 2 时,概念都是相通的。

2.2 安装步骤与常用命令

这里我把完整过程写出来,你跟着敲就行。先装 ROS Noetic:

sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' sudo apt install curl curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.key | sudo apt-key add - sudo apt update sudo apt install ros-noetic-desktop-full

如果国外源速度不理想,可以换成清华或中科大镜像,把packages.ros.org替换成镜像地址即可,这是常规操作。安装完成后初始化 rosdep:

echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc source ~/.bashrc sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential sudo rosdep init rosdep update

提示:rosdep update偶尔会因为网络问题失败,多试几次一般能过。如果你所在网络环境访问 GitHub 和 ROS 官方源不稳定,可以考虑配置一下rosdep的国内镜像源,具体方法在 ROS 社区里有很多人分享过。

然后编译 PX4 源码:

cd ~ git clone https://github.com/PX4/PX4-Autopilot.git --recursive cd PX4-Autopilot git checkout v1.14.3 git submodule update --init --recursive

这里有一个需要特别注意的点:PX4 仓库包含大量的子模块,git clone --recursive如果因为网络原因中断,后面一定要手动补拉子模块,否则编译到一半会报各种头文件找不到的错误。

接下来运行依赖安装脚本:

bash ./Tools/setup/ubuntu.sh

这个脚本会自动安装 GCC、CMake、Python 依赖和 Gazebo 等一整套工具链,中间会询问几次是否继续,一路回车即可。等它跑完之后,就可以尝试编译并启动仿真了:

make px4_sitl gazebo

第一次编译需要下载大量依赖并编译整个固件,慢的话二三十分钟很正常。看到终端里出现[INFO] [px4] Startup script returned并且弹出 Gazebo 界面,说明 PX4 SITL 已经跑起来了。

最后安装 MAVROS:

sudo apt install ros-noetic-mavros ros-noetic-mavros-extras sudo /usr/lib/ros/noetic/lib/mavros/install_geographiclib_datasets.sh

第二个命令是下载地理数据集的脚本,MAVROS 在启动时需要加载这些数据来做 UTM 坐标转换。这个脚本在有些网络环境下会卡住。如果你只是做仿真,暂时卡住也可以强行跳过,很多基础功能不受影响,但后续如果要跑 GPS 相关的真实定位,还是建议找时间把它装好。

2.3 编译启动时最容易卡住的三个点

第一个卡点是git submodule update --init --recursive反复失败。这时候不要重复make,而是先检查子模块是否完整。在PX4-Autopilot目录下执行git submodule status,看到前面有-的说明对应子模块没有拉下来,多试几次,或者用git config --global submodule.recurse true后再 update 一次。

第二个卡点是 Gazebo 启动后黑屏或卡在加载界面。Gazebo 首次运行会尝试下载一些标准模型文件,如果下载不了就会一直卡住。一个稳妥的做法是提前把模型库整理好,放到~/.gazebo/models目录下,然后重启 Gazebo。

第三个卡点是编译到一半内存不足导致进程被杀。X86 平台上同时跑编译和后续的 Gazebo 仿真,内存占用很容易超过 4GB。建议 8GB 内存起步,如果物理内存确实小,可以加 4GB 的 Swap:

sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile

3. 键盘控制一台虚拟无人机:按下按键背后发生了什么

3.1 数据链路:从按键事件到电机模型

按下键盘上的i键,到无人机动起来,中间走了一条非常清晰的链路,我先把这条链路讲清楚,后面每一步操作你都知道自己在干什么。

键盘按键事件被teleop_twist_keyboard节点捕获,它根据你按的键,生成一个geometry_msgs/Twist消息,包含线速度x/y/z和角速度yaw。在 ROS 里,这种“速度指令”通常发布在/cmd_vel话题上。我们把/cmd_vel重映射到 MAVROS 的速度指令话题,MAVROS 把这个 Twist 消息打包成 MAVLink 的SET_POSITION_TARGET_LOCAL_NED消息,通过 UDP 发送给本机运行的 PX4 SITL 进程。PX4 在收到指令后,基于当前的状态估计和外部目标,计算出电机转速,再把这个结果通过仿真接口发给 Gazebo 里的电机模型,Gazebo 根据物理引擎更新飞机的姿态和位置,同时把新的传感器读数反馈给 PX4。

PX4 的状态变化会通过 MAVLink 回传给 MAVROS,MAVROS 再发布成 ROS 话题,比如/mavros/local_position/pose、/mavros/state。也就是说,你在终端里看到的飞机状态,和实际 Gazebo 虚拟世界中的飞机状态,是一条完整的闭环数据流。

3.2 启动仿真和 MAVROS

需要开两个终端。

第一个终端启动 PX4 和 Gazebo:

cd ~/PX4-Autopilot make px4_sitl gazebo

第二个终端启动 MAVROS 与 PX4 的连接:

roslaunch mavros px4.launch fcu_url:="udp://:14540@127.0.0.1:14557"

这个fcu_url指定了 PX4 SITL 的通信端口。PX4 在 SITL 模式下默认通过 UDP 14557 端口接收 GCS 或外部设备的数据,MAVROS 则监听 14540 端口。启动完成后,可以在另一个新终端验证连接状态:

rostopic echo /mavros/state

看到connected: True,同时mode是MANUAL或AUTO.LOITER,说明链路已经通了。这一步非常重要,如果你发现connected: False,后面的操作全部无法进行。

3.3 先把无人机稳住:持续发布位置指令的悬停脚本

这里要说一个最关键的概念:PX4 的Offboard 模式下,要求外部持续不断地发送设定值(setpoint),如果它一段时间内没有收到新的指令,会认为外部控制失效,自动退出 Offboard 模式,甚至触发失控保护。所以不能一启动就切 Offboard,必须先有一个节点在持续发布指令。

我通常先跑一个最简单的悬停脚本,让无人机先稳定在 2 米高度:

#!/usr/bin/env python3 import rospy from geometry_msgs.msg import PoseStamped rospy.init_node('hold_position') pub = rospy.Publisher('/mavros/setpoint_position/local', PoseStamped, queue_size=10) rate = rospy.Rate(20) pose = PoseStamped() pose.pose.position.x = 0.0 pose.pose.position.y = 0.0 pose.pose.position.z = 2.0 while not rospy.is_shutdown(): pub.publish(pose) rate.sleep()

把这段代码保存为hold_position.py,加上执行权限:

chmod +x hold_position.py

在第三个终端运行它。这里解释一下为什么发布频率要 20Hz:PX4 的 Offboard 指令过期时间通常是 0.5 秒左右,如果发布频率太低,超过这个时间窗口,飞控就会认为指令超时,所以 10~20Hz 比较稳妥。

3.4 解锁并切换到 Offboard 模式

等待悬停脚本跑起来几秒钟之后,在第四终端执行服务调用:

rosservice call /mavros/set_mode "custom_mode: 'OFFBOARD'" rosservice call /mavros/cmd/arming "value: true"

执行成功后,你会看到 Gazebo 里的无人机四个旋翼开始转动,然后缓缓升起到两米高度。这里有一个先后顺序的细节:必须先切 Offboard,后解锁。如果先解锁再切 Offboard,PX4 有可能会因为没有收到“足够多”的外部指令而拒绝切换。实际上 PX4 要求进入 Offboard 模式前,外部已经以一定频率持续发送了至少 1 秒的 setpoint,所以悬停脚本先跑起来是最稳妥的。

3.5 用键盘接管方向控制

现在无人机已经稳稳悬停在空中了,接下来让键盘接管方向控制。新开一个终端:

rosrun teleop_twist_keyboard teleop_twist_keyboard.py cmd_vel:=/mavros/setpoint_velocity/cmd_vel_unstamped

注意后面的cmd_vel:=是 ROS 话题重映射语法,把 teleop 节点默认发布的/cmd_vel重定向到 MAVROS 的速度指令话题。MAVROS 同时提供了cmd_vel(TwistStamped)和cmd_vel_unstamped(Twist)两个速度话题,而 teleop_twist_keyboard 发布的是不带时间戳的 Twist,所以必须用cmd_vel_unstamped。

启动之后按一下i,无人机应该会向前移动;按j/l控制左右偏航;按k停住;按u/o/m/,/.控制各种斜向和旋转组合。在终端窗口里会打印出每个按键对应的速度值,默认线速度是 0.5 m/s,角速度是 1.0 rad/s,在仿真里这个速度比较合适,不用去改。

注意:如果你按了键但无人机没反应,优先检查 MAVROS 是否还在运行、悬停脚本是否停止了发布,以及当前模式是否还是 Offboard。很多时候是因为悬停脚本被 Ctrl+C 关掉了,PX4 因为收不到新的设定值而退出了 Offboard 模式。

键盘控制之所以用速度指令而不是位置指令,是因为键盘按键本身是“二元”的,按下代表正向速度、松开代表零速度。如果改用位置控制,每按一次键都得算一个增量目标点,不仅啰嗦,而且很难实现平滑的手动操作。

4. 从手动转向自主:用航点任务让无人机自己飞

4.1 自主飞行的本质:把“人按键盘”替换成“代码发指令”

键盘控制跑通之后,自主飞行其实只是换了个“发指令的人”。在 Offboard 模式下,PX4 并不关心指令是来自键盘还是来自一个 Python 脚本,它只关心能不能持续收到外部控制目标。

所以做自主飞行的核心,就是写一个上层节点,让它按照预定的逻辑,依次发布位置目标,并且判断飞机是否到达目标点,到了就发下一个目标。这个逻辑在工程上叫“航点任务状态机”,实现起来不复杂,但它是后面所有智能飞行功能的地基。不管是巡检、搜救还是表演编队,本质上都是航点任务的不同变体。

4.2 航点飞行完整代码

下面这段代码实现了一个矩形航线:起飞到 2 米高度,依次飞过 (0,0)、(4,0)、(4,4)、(0,4),最后回到原点。我加了不少注释,方便你对照着理解。

#!/usr/bin/env python3 import rospy import math from geometry_msgs.msg import PoseStamped from mavros_msgs.msg import State from mavros_msgs.srv import CommandBool, SetMode current_pose = None current_state = None def pose_cb(msg): global current_pose current_pose = msg.pose def state_cb(msg): global current_state current_state = msg rospy.init_node('waypoint_flight') rospy.Subscriber('/mavros/local_position/pose', PoseStamped, pose_cb) rospy.Subscriber('/mavros/state', State, state_cb) setpoint_pub = rospy.Publisher('/mavros/setpoint_position/local', PoseStamped, queue_size=10) arming_srv = rospy.ServiceProxy('/mavros/cmd/arming', CommandBool) set_mode_srv = rospy.ServiceProxy('/mavros/set_mode', SetMode) rate = rospy.Rate(20) # 等待 MAVROS 连接 for _ in range(100): if current_state is not None and current_state.connected: break rate.sleep() # 先发布几秒原地悬停指令,让 PX4 有足够时间接收 setpoint hold = PoseStamped() hold.pose.position.z = 2.0 for _ in range(100): setpoint_pub.publish(hold) rate.sleep() # 切换到 Offboard 并解锁 set_mode_srv(custom_mode='OFFBOARD') arming_srv(value=True) # 矩形航点 waypoints = [ (0.0, 0.0, 2.0), (4.0, 0.0, 2.0), (4.0, 4.0, 2.0), (0.0, 4.0, 2.0), (0.0, 0.0, 2.0), ] for wp in waypoints: target = PoseStamped() target.pose.position.x = wp[0] target.pose.position.y = wp[1] target.pose.position.z = wp[2] rospy.loginfo(f"前往航点 {wp}") while not rospy.is_shutdown(): # 每次循环都把当前目标发出去 setpoint_pub.publish(target) if current_pose is None: rate.sleep() continue # 计算当前位置与目标点的距离 dist = math.sqrt( (current_pose.position.x - wp[0]) ** 2 + (current_pose.position.y - wp[1]) ** 2 + (current_pose.position.z - wp[2]) ** 2 ) if dist < 0.5: rospy.loginfo(f"到达航点 {wp}") break rate.sleep() rospy.loginfo("全部航点完成,切换为悬停")

把脚本保存为waypoint_flight.py,运行前先确保当前没有在跑键盘控制或悬停脚本(否则会产生指令冲突):

chmod +x waypoint_flight.py python3 waypoint_flight.py

本机如果没有配置 conda 或者虚拟环境,建议直接用python3运行。这个脚本里的距离判断阈值 0.5 米是经验值:太小了可能在目标点附近反复震荡,导致判断超时;太大了误差积累明显。在不同的 PX4 参数配置下,你可以适当调大或调小这个阈值。

4.3 关于调参的思考:为什么飞机会在航点之间画弧线

很多朋友第一次跑航点飞行,会发现飞机并不是直线飞向下一个航点,而是在航点之间画了一条弧线。这是正常的,原因在于 PX4 的默认位置控制器规划的是加加速度受限的平滑轨迹,不是简单的“点到点直线”。它在计算轨迹时会考虑到最大速度、最大加速度和加加速度限制,所以飞机会转一个小弯再切入目标点。

如果这种弧线飞行不符合你的任务要求,可以调整 PX4 的MPC_XY_VEL_MAX(最大水平速度)、MPC_ACC_HOR(最大水平加速度)和MPC_JERK_AUTO(自动模式下的加加速度限制)等参数。在 SITL 仿真里调参非常安全,随便试错,这也是协同仿真相比真机最大的优势。真机上你根本不敢把加速度参数调到离谱,仿真里可以随意折腾。

4.4 从航点飞行到避障导航的进阶路线

航点飞行只是自主飞行最基础的一层。如果你后面想做类似 ROS 小车自主导航那样的事情,在无人机上是这样展开的:

  • 给 Gazebo 里的无人机模型挂载一个激光雷达或深度相机插件,一般会模拟出/scan或/camera/depth/points话题。
  • 运行 SLAM 算法(例如hector_slam、gmapping、cartographer)或使用 PX4 自带的 EKF 定位结果,构建环境的代价地图。
  • 引入全局路径规划器(A*、RRT*)和局部规划器(TEB、DWA),输出速度指令,再通过 MAVROS 发送给 PX4,跟在键盘控制里重映射/cmd_vel的套路一模一样。
  • 在做避障时要注意,PX4 的 Offboard 模式接收速度指令时,飞行高度控制要单独处理,通常建议把 z 轴速度或高度保持在一个固定值,避免避障过程中忽上忽下。

5. 仿真中的常见坑与排查思路

5.1 无人机起飞后乱飘或位置漂移

这个现象在 SITL 仿真里不算罕见,尤其是 Gazebo 物理引擎负载较高或者传感器模拟出现异常时。排查思路是先看/mavros/local_position/pose的话题输出是否正常,确认 PX4 是否拿到了稳定的位置估计。如果话题输出跳变得很厉害,最直接的办法就是重启 Gazebo 和 PX4 进程,让模型重新加载,一般能解决一大部分“玄学”问题。

另外要注意,如果你之前跑过真机参数、改过 EKF 相关配置,那仿真里可能会出现定位异常,建议用默认参数启动 PX4 SITL。

5.2 无法切换到 Offboard 模式

这是整个流程里最常见的问题,原因归纳起来基本是这几个:

  • 悬停节点没有持续发布 setpoint,或者发布频率太低。用rostopic hz /mavros/setpoint_position/local查看频率是否达到 10Hz 以上。
  • 切模式和解锁的命令写错了模式名。PX4 的 Offboard 模式在 MAVROS 里拼写是OFFBOARD,注意全大写。
  • 当前模式和指令类型不匹配。如果你之前跑的是速度控制,切模式前又切换了位置指令,PX4 内部可能会因为指令类型变化而拒绝切换,保持一个稳定的话题类型持续发送几次再切模式。

5.3 Gazebo 运行卡顿

Gazebo 是比较吃资源的大型软件,卡顿会直接影响飞行体验。如果你的电脑配置一般,有几个降负载的办法:

  • 启动 PX4 时使用不带复杂场景的模型,例如make px4_sitl gazebo_iris或make px4_sitl gazebo_iris_empty,可以省去加载地形和建筑物的开销。
  • 关闭 Gazebo 的 GUI 界面,在启动参数里加GUI=false,虽然看不到飞机样子,但能明显减少渲染占用。配合rqt_image_view或rviz看状态数据,调试体验不会差太多。
  • 增加 Swap 空间,避免内存不足导致的卡顿甚至进程被杀。
  • 在终端里执行export GAZEBO_MODEL_DATABASE_URI=""可以在部分情况下减少模型下载相关的阻塞,不过慎重使用,可能导致某些模型无法加载。

5.4 编译和启动的其他细节

编译时如果报fatal error: *.h: No such file or directory,九成是子模块没拉全,先git submodule update --init --recursive。启动 MAVROS 报端口冲突,检查你是否同时开了多个 MAVROS 进程。终端环境变量丢了,重新source ~/.bashrc。这些看起来是小问题,但每一件都可能浪费半小时以上的时间。

网上也有人把 ROS 安装封装成了一键脚本,确实能省去手动配源的麻烦,但我个人建议用之前扫一眼脚本内容,确认它执行了哪些操作,对后续排错有帮助。环境这东西,自己亲手搭过一遍,后面遇到问题才知道去哪看。

从键盘控制到自主飞行,中间跨越的其实不只是代码量,更是对“谁在什么时刻给飞控发什么指令”这件事的掌控感。我在带新手的过程中发现,愿意老老实实从键盘控制开始、把链路一步步验证清楚的人,后面做路径规划、目标跟踪这些复杂任务时基本不会遇到难以定位的玄学问题。反而是那些急着跳级的人,经常在三个系统之间来回折腾。如果你正在学 PX4,建议按下键盘那一步多停留一会儿,把发布的话题、转发的协议、飞控的模式变化都亲手观察一遍,这会比看十篇教程都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询