先说结论:XTDrone这套东西,拿来跑2D激光雷达的二维运动规划,如果你的目标不是发论文而是真正把无人机自主导航这套流程跑通、跑稳,那它绝对是目前开源方案里最省心的一条路。但省心不等于没坑,尤其当你从官方demo切到自己改模型、自己配导航参数的时候,各种报错和诡异现象会接连不断。这篇文章我就围绕XTDrone平台上基于2D激光Lidar做二维运动规划的完整过程,把那些文档里不会写、教程里一笔带过的细节和报错处理方案全部摊开讲。
先说清楚“二维运动规划”在这个项目里到底是什么意思。它不是让无人机在三维空间里自由飞,而是把无人机固定在一个高度平面上,用2D激光雷达感知周围障碍物,再在这个平面上规划出一条从当前点到目标点的无碰撞路径。这个场景特别贴近实际工程里的室内巡检、仓库盘点、管道巡查等任务——无人机不需要上下翻飞,只需要在一个楼层高度上稳定巡航就行。
我自己是把这套流程跑在Ubuntu 18.04 + ROS Melodic + PX4 1.11 + Gazebo 9的组合上,这也是XTDrone官方文档最推荐的版本组合。如果你用的是Ubuntu 20.04 + ROS Noetic的组合,部分依赖包需要自己编译,坑会多不少,后面我会单独提到。
1. 项目脉络与整体架构
1.1 XTDrone平台与二维规划任务的关系
XTDrone这个平台本质上是一套基于PX4和Gazebo的无人机仿真系统,它帮你把飞控、传感器、模型、通信这些底层的东西全部打通了。你在命令行里敲一个脚本,就能看到一架无人机在Gazebo世界里缓缓起飞,同时ROS话题里铺满了IMU、GPS、光流、激光雷达等各种数据。这种“开箱即用”的体验,对做规划算法的同学来说太重要了——你没时间也没必要去折腾飞控底层的PWM信号、串口通信、姿态解算这些东西。
那二维运动规划在这个平台里是怎么落地的?核心是三个节点协同工作:
- SLAM节点:拿2D激光雷达的数据实时构建栅格地图,这个项目里最常用的是gmapping,如果你想要更好的效果,cartographer也是现成可以切的。
- move_base节点:这是ROS导航栈的核心,它接收地图、定位信息和目标点,通过全局规划器和局部规划器输出速度指令。
- PX4飞控节点:负责把move_base发过来的速度指令转换成无人机实际的位置和姿态变化。
三者之间的数据流大概是:激光雷达发布/scan话题→gmapping订阅激光数据并发布/map→move_base订阅map和TF树,接收/move_base_simple/goal目标点→输出/cmd_vel速度指令→PX4 offboard控制器订阅/cmd_vel并驱动仿真无人机。
XTDrone比原生PX4 SITL多出来的价值也正在这里:它把整个无人机模型和环境、传感器参数都配好了,你可以直接在~/XTDrone/launch下面找到各种启动脚本,不用自己从零搭一套仿真。
1.2 为什么选2D激光雷达而不是视觉或3D雷达
很多刚开始接触无人机导航的人会有个疑问:现在视觉方案这么成熟,为什么还要用2D激光雷达?我的看法是,在二维平面导航这个特定任务上,2D激光雷达有着不可替代的优势。
- 计算量小:单线激光雷达一帧数据量大约是几百到几千个点,相比视觉的几十万像素和3D雷达的几十万点云,CPU开销完全是降维打击。
- 精度高且稳定:激光测距的精度是毫米级的,不受光照影响。视觉在暗光、强光、纹理缺失环境下会崩,激光不会。
- 语义简单:2D激光在固定高度扫描得到的轮廓,天然就是平面上的障碍物边界,不需要像视觉那样做目标检测和深度估计。
在XTDrone里,官方iris模型搭载的2D激光雷达实际上是用Gazebo的gazebo_ros_laser插件模拟的。它的扫描范围是360度,最大测距30米,扫描频率可以配置到10Hz左右,和真实场景中rplidar A2/A3的规格非常接近。也就是说你在仿真里调通的算法,迁移到真实雷达上不需要做大的改动。
2. 环境准备与无人机模型改造
2.1 基础环境检查清单
如果你是从零开始装XTDrone,我建议严格按官方文档走一遍,千万别跳步骤。装完之后,命令行里逐条检查下面这些项是否正常:
# 检查ROS主目录是否正常 echo $ROS_PACKAGE_PATH # 检查PX4固件是否编译通过 cd ~/PX4_Firmware make px4_sitl_default # 检查XTDrone脚本是否有执行权限 cd ~/XTDrone ls -la *.sh最容易出现的坑是.bashrc里的环境变量。XTDrone官方要求你把PX4、Gazebo、XTDrone的路径全部写进.bashrc,但很多人电脑上以前装过别的ROS包,环境变量顺序乱了,就会导致Gazebo启动时找不到模型。我的建议是每次打开新终端都先执行:
source ~/.bashrc然后执行echo $GAZEBO_MODEL_PATH,如果返回的路径里没有~/XTDrone/models,那你后面所有仿真都会报模型加载失败。
2.2 给无人机挂载2D激光雷达
XTDrone默认的iris无人机模型是不带激光雷达的,需要你自己往模型里加。这一步看着简单,其实隐藏了很多细节。
首先明确你要改哪个文件。我用的方式是直接修改~/PX4_Firmware/Tools/sitl_gazebo/models/iris_with_lidar/iris_with_lidar.sdf——如果你在XTDrone的模型目录里没找到现成的,就自己创建一个iris_with_lidar的模型文件夹,从官方iris模型拷贝一份再改。
SDF文件里加激光雷达插件的关键代码是:
<sensor name="laser" type="gpu_lidar"> <always_on>true</always_on> <update_rate>10</update_rate> <visualize>true</visualize> <topic_name>scan</topic_name> <plugin name="laser_node" filename="libgazebo_ros_laser.so"> <ros> <namespace>/</namespace> <remapping>~/out:=scan</remapping> </ros> <output_type>sensor_msgs/LaserScan</output_type> <fov>6.28319</fov> <samples>360</samples> <min>0.2</min> <max>30.0</max> <resolution>0.01</resolution> </plugin> </sensor>这里有几个参数要特别注意:
type我建议用gpu_lidar而不是ray。两者都能干活,但gpu_lidar的渲染性能好很多,仿真跑起来不容易卡。update_rate设成10Hz,就是每秒10帧,gmapping和move_base对这个频率都很友好。如果你设得太低比如5Hz,建图会明显卡顿;太高比如50Hz,CPU直接爆炸。fov是扫描角度范围,6.28319就是2π,代表360度全向扫描。由于是二维平面规划,传感器坐标系Z轴要和无人机机体Z轴平行。
改完模型之后,还有个关键步骤:配置TF变换。激光雷达的数据只有转换到base_link坐标系下才有意义。XTDrone里这个变换通常在iris_with_lidar.sdf的同名配置文件里定义,但更常见的做法是在launch文件里加一个静态变换:
<node pkg="tf2_ros" type="static_transform_publisher" name="laser_to_base" args="0 0 0.1 0 0 0 base_link laser"/>这行代码的意思是把laser坐标系固定在base_link坐标系正上方0.1米处,三轴无旋转。实际项目中这个高度值要根据你雷达的安装位置来填,装高了会漏掉低矮障碍物,装低了会打到地面产生大量异常点。
3. 建图与定位
3.1 gmapping建图原理简析
gmapping是基于粒子滤波的2D SLAM算法。它的核心思想是:维护一群“粒子”,每个粒子代表机器人(无人机)的一条可能轨迹和对应的地图假设。每来一帧激光数据,算法就根据当前粒子的位姿和激光观测去更新地图置信度,同时按照观测匹配程度给粒子打分,分数低的淘汰,分数高的复制,如此迭代收敛出最优轨迹和地图。
但在XTDrone里跑gmapping有一个很多人没意识到的BUG:无人机和地面机器人不一样,它在空中会晃动。即使你在Gazebo里把无人机定高锁在一个平面上,微小的姿态波动都会让激光雷达扫出来的数据产生扭曲。所以启动gmapping之前,一定要把无人机的姿态控制切到offboard模式并且先让它稳定悬停一两分钟,等位姿收敛了再开始建图。
启动gmapping的核心命令:
roslaunch xtdrone_nav gmapping.launch里面的关键参数我建议这样设:
| 参数 | 推荐值 | 说明 |
|---|---|---|
map_update_interval | 5.0 | 地图更新间隔,太短CPU狂转,太长地图滞后 |
linearUpdate | 1.0 | 移动多少米触发一次扫描匹配 |
angularUpdate | 0.5 | 旋转多少弧度触发一次扫描匹配 |
iterations | 5 | 扫描匹配迭代次数 |
particles | 30 | 粒子数,室内小场景30够了 |
xmin/xmax/ymin/ymax | 根据场景设置 | 地图边界,设太小会导致墙被截断 |
3.2 手动遥控建图的关键操作
XTDrone里启动建图后,你需要通过QGC地面站或者键盘控制脚本遥控无人机在环境中飞行一圈,把整个地图扫出来。这一步比大多数人想象的要费劲,因为无人机不像小车,它不能原地旋转,也不能急停,控制不好就会飞出边界。
我试过两种方式,最终推荐用XTDrone自带的键盘控制脚本:
cd ~/XTDrone/misc/script python3 keyboard_control.py iris 1 100这个脚本启动后,你可以用键盘的WASD控制前后左右,方向键控制旋转。注意100这个数字代表的是控制增益,它决定了键盘输入的灵敏度,数值越大飞机反应越猛,新手建议从50开始练。
实操中我建议的建图路径是:先让飞机原地缓慢旋转360度,让雷达把整个房间扫一圈;然后沿墙边飞,把房间的边界打出来;最后再横穿几次房间内部,把中间区域的障碍物补全。这样扫出来的地图闭合性好,后续move_base规划也不会出问题。
4. move_base导航栈的配置
4.1 全局代价地图与局部代价地图参数
move_base是ROS导航栈的核心,它内部包含global_costmap(全局代价地图)和local_costmap(局部代价地图)两层,分别服务于全局路径规划和局部避障。
XTDrone项目里,这两层代价地图的配置通常放在move_base.launch或者独立的yaml文件里。我把几个最容易影响无人机任务成败的参数拎出来讲:
global_costmap参数:
global_costmap: global_frame: map robot_base_frame: base_link update_frequency: 1.0 publish_frequency: 0.5 static_map: true inflation_radius: 0.5 cost_scaling_factor: 3.0static_map设为true意味着全局地图是固定的(就是gmapping建好的那张图),不会每次观测都更新。这在建图完成后的导航阶段完全没问题,但如果有人在环境中动了障碍物,就需要重新建图或者把static_map改成false。
local_costmap参数:
local_costmap: global_frame: odom robot_base_frame: base_link update_frequency: 5.0 publish_frequency: 2.0 static_map: false rolling_window: true width: 4.0 height: 4.0 resolution: 0.05 inflation_radius: 0.4 cost_scaling_factor: 5.0局部代价地图的global_frame要设成odom而不是map,这是个非常容易踩的坑。因为局部规划器需要的是一个连续增量式的定位参考,而map坐标系是全局地图的绝对参考,两者混用会导致局部规划器认为机器人一直在剧烈“跳动”。
4.2 move_base与PX4飞控的接口衔接
这是这个项目里最核心、也最容易出问题的地方。
move_base输出的速度指令是geometry_msgs/Twist,发布在/cmd_vel话题上。而XTDrone的PX4 SITL仿真里,无人机的位置和速度控制是由一个叫做offboard的控制器来接收的。你需要有一个转换节点,把/cmd_vel转换成PX4的/mavros/setpoint_velocity/cmd_vel_unstamped。
XTDrone里这个转换节点写在xtdrone_nav功能包里,核心逻辑是:
def cmd_vel_callback(msg): setpoint = PositionTarget() setpoint.type_mask = PositionTarget.IGNORE_PX | PositionTarget.IGNORE_PY | PositionTarget.IGNORE_PZ | PositionTarget.IGNORE_AFX | PositionTarget.IGNORE_AFY | PositionTarget.IGNORE_AFZ setpoint.coordinate_frame = PositionTarget.FRAME_LOCAL_NED setpoint.velocity.x = msg.linear.x setpoint.velocity.y = msg.linear.y setpoint.velocity.z = 0 setpoint.yaw = 0 velocity_pub.publish(setpoint)这里需要特别强调一个坐标系的坑:ROS的cmd_vel用的是ENU(东北天)坐标系,而PX4的姿态和速度控制用的是NED(北东地)坐标系。所以从move_base到飞控的转换绝对不能只是简单的转发,必须做坐标变换。XTDrone的offboard模式里通常是在setpoint里把Y轴速度取反,X轴和Y轴交换,具体要看你的PX4的MAV_FRAME设置。如果你发现无人机收到速度指令后乱飞、横着走,百分之九十是这个转换出了问题。
为了debug,我建议你建一个脚本专门监听/cmd_vel这条线:
rostopic echo /cmd_vel rostopic echo /mavros/setpoint_velocity/cmd_vel_unstamped两个话题同时打印,一组一组对比,看看转换关系是否符合预期。
5. 实操全过程与核心环节实现
5.1 完整启动流程
在XTDrone上跑通整个二维运动规划,我总结了一套稳定的启动顺序,写在这里:
第一步,启动XTDrone基础仿真:
cd ~/XTDrone ./launch/launch_xtdrone.sh这个脚本会启动Gazebo、PX4 SITL和QGC地面站。等Gazebo完全加载出来,你看到那架iris无人机在跑到上,再等QGC右上角显示连接成功后,进行下一步。
第二步,启动通信桥接和无人机控制脚本:
cd ~/XTDrone/misc/script python3 communication.py iris 0这一步是在ROS和PX4之间建立offboard通信通道,同时完成坐标系转换。脚本跑起来后,你的rostopic list里会出现/mavros/setpoint_position/global、/mavros/setpoint_velocity等话题,说明通信链路已经建立了。
第三步,解锁并起飞到指定高度:
rostopic pub -r 10 /xtdrone/iris/cmd_pose_enu geometry_msgs/Pose "position: {x: 0.0, y: 0.0, z: 1.5}"注意这个命令是持续发布的,必须用-r 10保持10Hz发送频率,否则PX4会认为offboard指令丢失,自动退出offboard模式。这个高度z: 1.5很关键,无人机飞到1.5米高度后,激光雷达离地面有一定距离,既能避开地面的随机噪点,又不会因为太高而漏掉大部分室内障碍物。
第四步,启动SLAM建图:
cd ~/XTDrone/navigation roslaunch xtdrone_nav gmapping.launch等Rviz里出现地图轮廓后,启动键盘控制脚本遥控无人机把整个环境扫一遍:
cd ~/XTDrone/misc/script python3 keyboard_control.py iris 1 100第五步,保存地图并启动move_base:
rosrun map_server map_saver -f ~/map roslaunch xtdrone_nav move_base.launchmove_base.launch里需要指定地图文件路径和各类代价地图参数,这个我在第4节已经详细讲了。
第六步,发送导航目标点:
rostopic pub /move_base_simple/goal geometry_msgs/PoseStamped "header: {frame_id: 'map'}, pose: {position: {x: 5.0, y: 3.0, z: 0.0}, orientation: {w: 1.0}}"执行完这一步,你会看到Rviz里出现一条从当前点到目标点的规划路径,无人机开始自动飞行。整个过程里面试了很多次,最稳的还是要保证目标点是在free空间里,离地图边界和障碍物至少留0.5米的安全距离。
5.2 一个完整飞行任务的现场记录
我举一个实际在XTDrone跑的室内房间任务做例子。环境是一个约10米×8米的房间,中间放了四个方柱障碍物,目标是把无人机从房间一角导航到对角位置。
实际执行中,无人机的表现是这样的:起飞爬升到1.5米高度,原地旋转扫描确认周围无障碍,然后开始沿着全局规划路径直线飞行。当它飞近中间第一根柱子时,局部代价地图先检测到柱子轮廓,局部规划器(我用的TEB)开始调整航向,绕行。绕行结束后,无人机自动恢复向目标点的直线飞行。整个过程大概耗时45秒,全程没有触发move_base的“无法规划路径”警告。
这45秒里其实隐含了大量参数在起作用。TEB算法有两个参数对无人机影响最大:min_obstacle_dist(最小避障距离)和max_vel_x(最大速度)。我把min_obstacle_dist设成了0.3米,max_vel_x设成了1.0米/秒,这样既保证了安全性,又不会让无人机因为速度太快导致惯性冲出安全边界。
6. 高频报错排查与细节提醒
这一节我决定直接上实战记录。以下每一个报错、每一个现象,都是我在XTDrone上实际碰到过并且花了不少时间解决的。整理成一个速查表性质的内容,方便你遇到了直接对照排查。
6.1 TF树异常导致gmapping地图不更新
现象:gmapping启动后,Rviz里看不到地图,或者地图只更新几帧就停住不动了。终端里不断刷Waiting for transform from map to scan...。
排查思路:TF树不完整是首要嫌疑。执行:
rosrun tf view_frames会生成一个frames.pdf,用浏览器打开看整体结构。正常情况下必须有map→odom→base_link→laser这条完整链路。如果odom→base_link缺失,说明PX4的里程计发布有问题;如果base_link→laser缺失,说明你模型文件里的静态坐标变换没加载成功。
我遇到过一次奇怪的情况:TF树完整,但laser坐标系的Z高度是0,也就是雷达和base_link完全重合。原因是static_transform_publisher的launch文件里,参数顺序写错了,前三个数字是xyz,后三个数字是rpy,我把xyz的0.1填到了rpy的位置。这种低级错误排查起来特别费眼神,建议直接一行一行对。
终极解决方案:检查坐标变换顺序后,如果还是不行,直接重启整个仿真环境,不要试图在运行状态下热修复TF。XTDrone的TF树一旦乱掉,很难手工恢复,不如重启一次来得干净。
6.2 move_base启动即崩溃或规划失败
现象:启动move_base节点后,终端马上刷红色报错,或者启动成功但在发送目标点时提示Aborting because the planner failed。
常见原因和解决:
第一个原因是代价地图size设置太小。我之前用默认的10x10米地图,结果房间里到处都是障碍物膨胀区域,目标点在代价地图边界之外,规划器直接判定无解。把local_costmap的width和height调大,或者把global_costmap的resolution调低,都能解决。
第二个原因是inflation_radius设置过大。如果膨胀半径比房间通道还宽,规划的路径会被切得支离破碎,甚至完全无法通过。一个小技巧是先用costmap_visualization话题把代价地图可视化出来看,膨胀区域会不会把通道堵死,再做参数调整。
第三个原因最隐蔽——ros::Time问题。XTDrone在Gazebo里跑久了,/use_sim_time参数如果和系统时钟不同步,会导致move_base认为所有传感器数据都过期,从而拒绝规划。你的launch文件里必须显式设置:
<param name="/use_sim_time" value="true"/>同时确保roscore和Gazebo启动顺序是先gazebo后roscore,否则模拟时间戳会错乱。
6.3 无人机高度漂移或乱飞
现象:move_base开始规划后,无人机不是水平飞到目标点,反而上下乱窜,或者向某个方向猛冲直接飞丢。
这个问题我调试了整整两天,最后定位到两个原因。
第一个原因是对接节点的速度指令没有控制Z轴。许多自己写的转换节点只处理了X和Y轴速度,Z轴直接置零,但PX4的offboard控制器要求你发的是完整的位姿或速度setpoint,PZ(Z轴位置)或者VZ(Z轴速度)都得给一个明确的数值。我给的是位置接口,所以Z位置必须恒定设置为1.5米,不能是0。
第二个原因是PX4的MPC_XY_VEL_MAX参数。这个参数限制了机体在水平方向的最大速度,默认值是3.5米/秒。如果你把move_base的max_vel_x设得比这个大还好,但如果你设置的速度上限太小,PX4的内环控制和外环期望之间会产生矛盾,具体表现就是飞机抖、绕圈、轨迹歪斜。解决方式是直接调PX4的参数:
param set MPC_XY_VEL_MAX 2.0 param set MPC_Z_VEL_MAX 1.0这两个参数在QGC的MAVLink控制台里也能设。调完以后就会稳很多。
6.4 Gazebo仿真卡顿与建图异常
现象:Gazebo界面帧率极低,建图时地图出现大量“重影”或漂移。
XTDrone的仿真对CPU要求非常高,尤其是GPU雷达和TEB局部规划器同时启动时。你可以在启动之前把Gazebo渲染的垂直同步关掉,然后在~/.bashrc里限制物理引擎频率:
export GAZEBO_PHYSICS_FREQ=500物理频率越低CPU占用越少,但也不能低于250,否则无人机在仿真里的动力学会明显失真。
重影问题通常是雷达数据帧间匹配误差导致的。把激光的update_rate改成和gmapping的linearUpdate匹配,比如雷达10Hz,gmapping每5帧(0.5秒)更新一次地图,这样匹配窗口更合理。还有一个建议是把house这类复杂环境换成一个空旷的厂房模型来跑,等流程完全跑通后再回归复杂场景。
6.5 cartographer切换中的常见坑
如果gmapping效果不好,想切换成cartographer,XTDrone也有对应的launch文件。但cartographer对传感器时间戳的一致性要求极高,激光雷达数据和IMU数据的时钟必须同步。实际使用中经常遇到报错:Failed to transform cloud from ... frame to tracking frame——这就是雷达到IMU的外参TF没配好。
解决方式是在cartographer_occupancy_grid_node里面单独指定:
tracking_frame: "base_link" laser_frame: "laser" baselink_frame: "base_link"同时要保证雷达数据不丢帧。rosbag record /scan看实际发布频率如果波动太大,说明系统实时性不足,需要把update_rate降到8Hz来换取稳定帧率。
7. 项目扩展思考与个人体会
这个项目跑通之后,我最大的感受是:XTDrone这套仿真平台的价值远不止于跑通一个demo,它其实是把“无人机导航”从工程问题变成了算法问题。你在仿真里可以随心所欲地换环境、换传感器、换算法,而不用担心炸机风险。甚至可以批量跑几百组随机环境来做路径规划算法的鲁棒性测试。
另外一个很实用的扩展方向是:把2D激光导航升级为2.5D导航。做法很简单,在move_base的costmap里加入高度维度,也就是“膨胀层”和“障碍物层”分别配置最小和最大高度范围。这样无人机可以在1.2米和1.8米两个高度之间切换飞行,极大拓展了避障能力。虽然严格来说不是纯三维规划,但在实际工程中非常常用。
把二维运动规划在XTDrone上跑通之后,再回看那些教你怎么启动demo的教程,你会觉得它们其实没讲透。真正的坑全在参数之间、话题之间、坐标系之间那些看不见的关联里。希望这篇文章能让你少走一些弯路,至少别像我一样,在TF树上和Z轴速度上各浪费一整天。如果你在实操中遇到我上面没提到的报错,欢迎按着“先查TF、再查时间戳、再查坐标系、最后查参数边界”这个顺序去排查,大概率能快速定位问题。