ROS2 Navigation2 完全指南:从行为树到多机器人协同导航 🚀
分类: 12 | 标签: ROS2, Nav2, Navigation2, 机器人导航, 行为树
ROS2 Navigation2 完全指南:从行为树到多机器人协同导航 🚀
深入拆解 Nav2 核心架构,手把手带你掌握现代机器人自主导航的每个关键环节——从零搭建导航栈、自定义行为树插件、调试避障算法、到多机器人协同调度
目录
- Nav2 是什么?为什么它改变了ROS导航游戏规则
- 核心架构深度解析:BT + 动作服务器 + 插件生态
- 从零搭建一个 Nav2 导航栈
- 全局规划器与局部控制器:算法选型对比
- 行为树(BT)自定义:当默认流程不够用
- Costmap 避障调优与参数调参实战
- 多机器人协同导航:从单机到集群
- 疑难排查与常见问题 FAQ
- 总结与进阶学习路线
1. Nav2 是什么?为什么它改变了ROS导航游戏规则
1.1 从 move_base 到 Nav2 的进化
如果你接触过 ROS1 的 move_base,你一定对那个庞大的单体内核记忆犹新——全局规划、局部规划、恢复行为全部塞在一个节点里,扩展性约等于零。想加一个新规划算法?你得 fork 整个 move_base 仓库。ROS2 Navigation2(简称 Nav2)彻底改变了一切。
最大的变革是什么? 三个字:**行为树(Behavior Tree,BT)**取代了僵硬的状态机。
在 ROS1 时代,导航流程是一个写死的状态机:全局规划 → 局部规划 → 如果卡住了 → 旋转 → 如果还卡住 → 后退 → 放弃。这个流程是硬编码在 C++ 源码里的,改一行就需要重编译、重部署。生产环境里最常见的需求——比如「先检查电量,如果低于30%就先回充电桩,否则去目标点」——在 ROS1 里几乎实现不了。
而在 Nav2 中,你只需要编辑一份 XML 文件。这就像把机器人的「大脑指令」从一个写死的 C 程序变成了可编排的交响乐谱——每一段逻辑都可以独立替换、任意重组。
| 特性 | ROS1 move_base | ROS2 Nav2 |
|---|---|---|
| 架构模式 | 单一节点,硬编码状态机 | 多节点分布式,BT 驱动流程 |
| 扩展方式 | 修改源码 + 重新编译 | 插件化,YAML 配置 + XML 编排 |
| 恢复行为 | 内置 3 种,不可定制 | BT 节点自由组合,数量不设上限 |
| 传感器融合 | 单一障碍物层 costmap | 多层 costmap 分层管理(静态、动态、体素) |
| 多机器人支持 | 不原生支持,每个机器人需要独立的 ROS Master | 命名空间隔离,一份 launch 文件启动 N 个实例 |
| 调试可视化 | 依赖 rostopic echo 看数据流 | rviz2 面板 + BT 执行树实时可视化 |
| 生命周期 | 无状态管理,启动即运行 | Managed Nodes,支持配置→激活→停用→清理的生命周期 |
| 动作接口 | Actionlib(非标准) | ROS2 Actions(原生支持抢占和反馈) |
| 路径平滑 | 无内置平滑器 | 集成 Smoother 节点,支持约束平滑 |
| 计算效率 | 单线程事件循环 | 多线程 Action Server,并行执行 |
Nav2 的设计哲学可以浓缩为一句口号:一切皆插件,一切皆行为树。规划器是插件,控制器是插件,恢复行为也是插件,连 costmap 的每一层都是一个插件。你可以像搭乐高积木一样组装导航栈。
1.2 谁需要学 Nav2?
Nav2 不是只给博士生准备的。只要你和「让机器人自己动起来」打交道,迟早会用到它:
- 入门级:大学生毕设、ROS2 初学者——用默认配置就能让 TurtleBot3 在 Gazebo 里跑起来
- 进阶级:移动机器人开发者——扫地机、物流 AMR、送餐机器人的底层移动能力
- 迁移级:ROS1 老项目正在迁移到 ROS2 的团队——move_base 到 Nav2 的平滑过渡全攻略
- 算法级:想在真实机器人上验证 SLAM + Path Planning 算法的科研人员——Nav2 提供了干净的插件接口
- 工业级:AGV/AMR 产线部署——需要可靠、可定制、多机器人协同的导航方案
1.3 Nav2 与 ROS2 生态的关系
Nav2 不是孤立存在的。它位于 ROS2 生态中承上启下的位置:
物理层 → 驱动层 (ros2_control) → 定位层 (AMCL / SLAM) → 导航层 (Nav2) → 任务层 (行为树)
上游依赖 SLAM 提供地图(occupancy grid map),依赖 AMCL 提供定位(粒子滤波估计位姿),依赖 URDF 提供机器人模型。下游为更高级的任务编排(如巡逻、巡检、自动充电)提供 navigate_to_pose 等标准化 action 接口。
2. 核心架构深度解析:BT 动作服务器 插件生态
2.1 整体架构一览
Nav2 不是一个节点,而是一个节点集群。每个节点各司其职,通过话题(Topic)、服务(Service)和动作(Action)通信:
┌──────────────────────────────────────────────────────┐
│ User Application │
│ (Python/C++ 任务编排层) │
└──────────────────────┬───────────────────────────────┘
│ Action: navigate_to_pose
┌──────────────────────┴───────────────────────────────┐
│ Navigator Node (BT Engine) │
│ 解析 BT XML → 调度各 Action Server │
└──┬────────┬──────────┬──────────┬───────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
│Plan- │ │Cont- │ │Recov-│ │Waypoint │
│ner │ │roller│ │eries │ │Follower │
│Server│ │Server│ │Server│ │ │
└──┬───┘ └──┬───┘ └──┬───┘ └──────────┘
│ │ │
└────────┼────────┘
│
┌────────┴────────┐
│ Costmap 2D │
│ (global+local) │
└────────┬────────┘
│
┌────────┴────────┐
│ Sensor Data │
│ (Laser/Camera) │
└─────────────────┘
核心组件职责:
- Navigator Node:行为树执行引擎,这是 Nav2 的大脑。它加载 BT XML 文件,按树形结构逐节点 tick,产生对各个 Action Server 的调用。所有导航逻辑的编排都在这一层完成
- Planner Server:全局路径规划器。接收起点和目标点,在全局地图上计算一条无碰撞路径。内置支持 Smac(Hybrid-A*)、NavFn(Dijkstra/A*)、Theta* 三种算法,也可以加载自定义规划插件
- Controller Server:局部控制器。在全局路径的指导下,结合实时传感器数据,计算机器人下一步的线速度和角速度。内置 DWB(动态窗口法)、MPPI(模型预测路径积分)、RPP(受控纯跟踪)
- Recoveries Server:恢复行为服务器。当机器人卡住时——比如前面出现一个突然走过来的行人——恢复服务器接管,执行旋转、后退、等待等策略尝试脱困
- Costmap 2D:代价地图。分为
global_costmap(全局,覆盖整个已知地图)和local_costmap(局部,以机器人为中心的滑动窗口)。每张 costmap 由多个 layer 叠加而成 - Waypoint Follower:多航点顺序导航。给定一串 GPS 坐标或地图点位,机器人依次前往。适用于巡检、巡逻等场景
- Smoother:路径平滑后处理器。规划器输出了离散坐标点后,Smoother 使用优化算法(如约束 B 样条)生成连续平滑的轨迹
2.2 行为树(Behavior Tree)的核心原理
行为树由节点(Node)组成。每个节点在被调用(tick)时返回三种状态之一:
| 状态 | 含义 | 触发后续动作 |
|---|---|---|
| SUCCESS | 当前行为成功完成 | 父节点继续执行下一个子节点 |
| FAILURE | 当前行为失败 | 父节点根据类型决定重试或放弃 |
| RUNNING | 行为正在执行中 | 下次 tick 继续此节点,不跳过 |
四种基本节点类型:
| 节点类型 | 符号 | 行为逻辑 | 生活类比 |
|---|---|---|---|
| Sequence | → | 依次执行子节点,任一返回 FAILURE 则整体失败 | "先洗手,再吃饭,然后洗碗"——第一步不洗就不往下走 |
| Fallback(Selector) | ? | 依次尝试子节点,任一返回 SUCCESS 则整体成功 | "尝试 Plan A,不行就 Plan B,再不行就 Plan C" |
| Action | ⚡ | 执行一个具体动作(如 ComputePathToPose) | "去拿水杯"——一个原子操作 |
| Condition | ❓ | 检查条件是否满足,返回 SUCCESS 或 FAILURE | "水杯是否为空?"——不改变世界状态,只判断 |
| Decorator | ⇢ | 装饰/包装子节点(速率控制、重试 N 次、超时等) | "最多尝试 3 次,每次间隔 5 秒" |
一个具体例子:假设你要机器人「先去充电桩,如果电量低于 20% 的话」。用行为树表达:
<Sequence name="SmartGoToGoal">
<Condition ID="BatteryBelow20"/>
<!-- 如果电量低于 20%,Fallback 会执行充电导航 -->
<Fallback>
<Sequence name="GoChargeFirst">
<Action ID="NavigateToCharger"/>
<Action ID="StartCharging"/>
<Condition ID="BatteryAbove80"/>
</Sequence>
</Fallback>
<!-- 电量充足,直接去目标 -->
<Action ID="NavigateToGoal"/>
</Sequence>
当 BatteryBelow20 返回 FAILURE 时,Fallback 的 GoChargeFirst 整个子序列会被跳过(不需要充电)。当返回 SUCCESS 时,Fallback 会进入第一个子节点尝试充电导航。这种声明式的逻辑编排在 ROS1 时代是无法想象的。
2.3 插件系统加载机制详解
Nav2 使用 ROS2 的 pluginlib 框架动态加载插件。这意味着「换一个规划算法」不需要修改 Nav2 框架代码,甚至不需要重编译 Nav2 本身。
插件的加载流程:
YAML 配置指定插件列表 → pluginlib ClassLoader 搜索库 → 动态加载 .so → 实例化插件
在配置文件中声明:
planner_server:
ros__parameters:
# 可以加载多个插件,每个有独立的 ID
planner_plugin_types:
- "nav2_smac_planner/SmacPlannerHybrid"
- "nav2_navfn_planner/NavfnPlanner"
planner_plugin_ids:
- "GridBased"
- "NavfnDefault"
# behavior tree 调用时用 planner_id 指定用哪个
关键之处:多个插件可以同时存在于同一个 Planner Server 中。行为树在调用 ComputePathToPose 时,通过 planner_id 选择使用哪一个。这意味着你可以在一次导航中动态切换规划器——比如先用 Hybrid-A* 规划主路径,如果发现不合理再切换到 NavFn 重规划。
同样,Controller Server 也能加载 DWB 和 MPPI 两个控制器,按场景切换:
- 室内狭窄走廊 → 切换到更保守的 DWB
- 室外开阔广场 → 切换到更平滑的 MPPI
3. 从零搭建一个 Nav2 导航栈
3.1 前置准备:地图 + 定位 + TF 树
导航的「黄金三角」前置条件:
- 地图(Map):通过 SLAM 构建的 occupancy grid map。推荐
slam_toolbox(在线建图)或直接用已建好的.pgm/.yaml地图文件 - 定位(Localization):AMCL(自适应蒙特卡洛定位)持续估计机器人在 map 坐标系下的位姿。定位质量直接决定导航精度
- TF 变换树:完整的坐标系链
map → odom → base_footprint → base_link → lidar_frame。TF 断裂或延迟超过容差,导航会直接拒绝工作
快速验证前置条件:
# 检查 TF 树完整性
ros2 run tf2_tools view_frames
# 应该在 frames.pdf 中看到完整的链
# 检查 AMCL 是否在发布位姿
ros2 topic echo /amcl_pose --once
# 检查地图是否被正确加载
ros2 topic echo /map --once
3.2 使用 Gazebo 模拟器快速上手
对于没有真实机器人的开发者,Gazebo + TurtleBot3 是最快的学习路径:
# 1. 安装必要包
sudo apt install ros-humble-turtlebot3-gazebo ros-humble-nav2-bringup
# 2. 设置 TurtleBot3 型号
export TURTLEBOT3_MODEL=waffle
# 3. 启动 Gazebo 世界(含障碍物)
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
# 4. 启动 AMCL 定位
ros2 launch nav2_bringup localization_launch.py \
map:=/opt/ros/humble/share/turtlebot3_navigation2/map/map.yaml \
use_sim_time:=true
# 5. 启动 Nav2 导航
ros2 launch nav2_bringup navigation_launch.py \
use_sim_time:=true \
params_file:=/opt/ros/humble/share/turtlebot3_navigation2/param/waffle.yaml
# 6. 在 rviz2 中用 "2D Goal Pose" 工具点击目标点
ros2 run rviz2 rviz2
3.3 参数配置文件完全解析
以下是一个面向真实差速驱动机器人的完整 Nav2 参数配置,每个关键参数都附有说明:
# nav2_params.yaml — 完整实用配置
# ========================== AMCL 定位参数 ==========================
amcl:
ros__parameters:
use_sim_time: false # 真实机器人必须设为 false!
alpha1: 0.2 # 旋转噪声(来自旋转动作)
alpha2: 0.2 # 旋转噪声(来自平移动作)
alpha3: 0.2 # 平移噪声(来自平移动作)
alpha4: 0.2 # 平移噪声(来自旋转动作)
base_frame_id: "base_footprint"
odom_frame_id: "odom"
global_frame_id: "map"
robot_model_type: "differential" # 差速/全向/阿克曼
update_min_d: 0.1 # 移动超过0.1m才更新粒子
update_min_a: 0.1 # 旋转超过0.1rad才更新粒子
laser_model_type: "likelihood_field" # 似然场模型(比 beam model 更鲁棒)
max_beams: 60 # 用的激光束数量(降采样加速)
max_particles: 2000 # 粒子数上限
min_particles: 500 # 粒子数下限
resample_interval: 1 # 每隔多少次更新重采样一次
# ========================= BT 导航器参数 =========================
bt_navigator:
ros__parameters:
use_sim_time: false
bt_xml_filename: "navigate_w_replanning_and_recovery.xml"
default_nav_to_pose_bt_xml: "navigate_to_pose_w_replanning_and_recovery.xml"
plugin_lib_names:
- "nav2_compute_path_to_pose_action_bt_node"
- "nav2_compute_path_through_poses_action_bt_node"
- "nav2_follow_path_action_bt_node"
- "nav2_back_up_action_bt_node"
- "nav2_spin_action_bt_node"
- "nav2_wait_action_bt_node"
- "nav2_clear_costmap_service_bt_node"
- "nav2_is_stuck_condition_bt_node"
- "nav2_goal_reached_condition_bt_node"
- "nav2_initial_pose_received_condition_bt_node"
transform_tolerance: 0.1 # TF 变换容差(秒)
global_frame: "map"
robot_base_frame: "base_link"
# ========================= 规划器参数 =========================
planner_server:
ros__parameters:
expected_planner_frequency: 1.0 # 规划频率(Hz)
planner_plugin_types:
- "nav2_smac_planner/SmacPlannerHybrid"
planner_plugin_ids:
- "GridBased"
downsample_costmap: false # 是否降采样 costmap 加速规划
tolerance: 0.25 # 目标点距离容差(米)
use_astar: true # A* 搜索(false= Dijkstra)
allow_unknown: true # 允许穿越未知区域
max_iterations: 100000 # 最大搜索迭代次数
max_planning_time: 5.0 # 单次规划超时时间(秒)
motion_model_for_search: "DUBIN" # 搜索时的运动模型
angle_quantization_bins: 72 # 角度量化(每5度一个bin)
# ========================= 控制器参数 =========================
controller_server:
ros__parameters:
controller_plugin_types:
- "dwb_core::DWBLocalPlanner"
controller_plugin_ids:
- "FollowPath"
min_vel_x: -0.1 # 最小线速度(允许后退)
max_vel_x: 0.26 # 最大线速度(m/s)
max_vel_theta: 1.0 # 最大角速度(rad/s)
min_speed_xy: 0.1 # 最低速度阈值
max_speed_xy: 0.26 # 最高速度阈值
goal_checker_plugin: "nav2_simple_goal_checker/SimpleGoalChecker"
xy_goal_tolerance: 0.15 # 位置到达容差
yaw_goal_tolerance: 0.1 # 朝向到达容差(约5.7度)
# DWB 内嵌参数
FollowPath.plugin: "dwb_core::DWBLocalPlanner"
FollowPath.critics:
- "dwb_critics/BaseObstacle" # 避障评估器
- "dwb_critics/GoalAlign" # 目标朝向评估器
- "dwb_critics/GoalDist" # 目标距离评估器
- "dwb_critics/PathAlign" # 路径对齐评估器
- "dwb_critics/PathDist" # 路径距离评估器
- "dwb_critics/PreferForward" # 前向偏好评估器
- "dwb_critics/Oscillation" # 振荡检测评估器
# ========================= 恢复行为参数 =========================
recoveries_server:
ros__parameters:
costmap_topic: "local_costmap/costmap_raw"
footprint_topic: "local_costmap/published_footprint"
cycle_frequency: 10.0
recovery_plugin_ids: ["spin", "backup", "wait"]
spin:
plugin: "nav2_recoveries/Spin"
backup:
plugin: "nav2_recoveries/BackUp"
wait:
plugin: "nav2_recoveries/Wait"
# ========================= 局部代价地图 =========================
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0
global_frame: "odom" # ★ 局部代价地图用 odom 框架
robot_base_frame: "base_link"
robot_radius: 0.22 # 机器人投影半径
resolution: 0.05 # 分辨率(m/cell)
footprint: "[]" # 空=用 robot_radius 计算圆形
width: 3 # 窗口宽度(m)
height: 3 # 窗口高度(m)
always_send_full_costmap: true
plugins: ["obstacle_layer", "inflation_layer"]
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
enabled: true
observation_sources: "scan"
scan:
topic: "/scan" # ★ 匹配你的激光雷达话题名
max_obstacle_height: 2.0
clearing: true
marking: true
data_type: "LaserScan"
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
enabled: true
inflation_radius: 0.55 # ★ 安全膨胀半径
cost_scaling_factor: 3.0 # ★ 代价衰减因子
# ========================= 全局代价地图 =========================
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 0.5
global_frame: "map" # ★ 全局代价地图用 map 框架
robot_base_frame: "base_link"
robot_radius: 0.22
resolution: 0.05
width: 5 # 窗口宽度(米),5m / 0.05 = 100 cells
height: 5 # 窗口高度(米),5m / 0.05 = 100 cells
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: true
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
enabled: true
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
enabled: true
inflation_radius: 0.55
cost_scaling_factor: 3.0
3.4 程序化发送导航目标
Nav2 通过 ROS2 Action 接口接收导航指令。以下 Python 示例演示如何编写一个简单的导航客户端:
#!/usr/bin/env python3
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from nav2_msgs.action import NavigateToPose
from geometry_msgs.msg import PoseStamped, Quaternion
class Nav2Commander(Node):
"""向 Nav2 发送导航指令的客户端节点"""
def __init__(self):
super().__init__('nav2_commander')
self._client = ActionClient(self, NavigateToPose, 'navigate_to_pose')
self._goal_handle = None
def send_goal(self, x: float, y: float, yaw: float = 0.0):
"""发送一个导航目标点"""
self.get_logger().info(f'Sending goal: x={x}, y={y}, yaw={yaw}')
if not self._client.wait_for_server(timeout_sec=5.0):
self.get_logger().error('Nav2 action server 不可用!')
return False
goal = NavigateToPose.Goal()
goal.pose.header.frame_id = 'map'
goal.pose.header.stamp = self.get_clock().now().to_msg()
goal.pose.pose.position.x = x
goal.pose.pose.position.y = y
# 四元数表示朝向
import math
goal.pose.pose.orientation.z = math.sin(yaw / 2.0)
goal.pose.pose.orientation.w = math.cos(yaw / 2.0)
send_future = self._client.send_goal_async(
goal,
feedback_callback=self._feedback_cb
)
send_future.add_done_callback(self._goal_response_cb)
return True
def _goal_response_cb(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().error('导航目标被拒绝!')
return
self._goal_handle = goal_handle
result_future = goal_handle.get_result_async()
result_future.add_done_callback(self._result_cb)
def _feedback_cb(self, feedback_msg):
feedback = feedback_msg.feedback
remaining = feedback.distance_remaining
eta = feedback.estimated_time_remaining
self.get_logger().info(f'剩余距离: {remaining:.2f}m | 预计: {eta:.1f}s')
def _result_cb(self, future):
result = future.result().result
self.get_logger().info(f'导航完成!错误码: {result.error_code}')
def cancel_goal(self):
if self._goal_handle:
self._goal_handle.cancel_goal_async()
def main():
rclpy.init()
commander = Nav2Commander()
# 发送三个目标点
commander.send_goal(2.0, 1.5)
rclpy.spin(commander)
rclpy.shutdown()
if __name__ == '__main__':
main()
也可以用命令行快速测试:
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: 'map'},
pose: {position: {x: 2.0, y: 1.5, z: 0.0},
orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}}}}" --feedback
4. 全局规划器与局部控制器:算法选型对比
4.1 全局规划器深度对比
Nav2 内置了三种成熟规划器,各自有其设计哲学和适用场景:
| 维度 | NavFn Planner | Smac Planner 2D | Smac Planner Hybrid | Theta* |
|---|---|---|---|---|
| 核心算法 | Dijkstra / A* on grid | Hybrid-A* (SE2) | Hybrid-A* with Reeds-Shepp | Any-angle A* |
| 路径表示 | 格点坐标(整数 grid cell) | 连续坐标(浮点 x,y) | 连续坐标 + 朝向角 | 网格但不限方向 |
| 路径平滑度 | 低(阶梯状路径) | 高(连续平滑) | 高(考虑最小转弯半径) | 中(不限于 8 方向) |
| 运动学约束 | 不考虑 | 考虑(Dubins/Reeds-Shepp) | 考虑(阿克曼模型) | 不考虑 |
| 搜索空间 | 2D grid | 2D continuous + heading | 3D (x,y,theta) | 2D grid, any-angle |
| 计算速度 | 快 | 中等 | 较慢 | 中快 |
| 适用底盘 | 全向移动 | 差速驱动 | 阿克曼(车类) | 全向/差速 |
| 成熟度 | ★★★★★ | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
选择策略:
- 室内差速驱动 (TurtleBot/DiffBot):默认推荐
SmacPlanner2D。路径平滑自然,转弯流畅,不会出现 NavFn 那种"先旋转 45° 再直走"的奇怪路径 - 阿克曼转向 (车模/AGV):唯一选择是
SmacPlannerHybrid。它能在搜索时即考虑车辆的最小转弯半径,确保生成的路径真实可跟踪 - 快速原型/教学:用
NavFn。最简单的配置,行为可预期,适合理解导航原理 - 需要对角线移动:
Theta\*能生成比 A* 更短的路径,在开阔场景中有优势
4.2 局部控制器深度对比
局部控制器决定了机器人「怎么走」。不同的控制器思路迥异:
| 维度 | DWB | MPPI | RPP | TEB |
|---|---|---|---|---|
| 核心思路 | 生成多条轨迹→评分→选最优 | 模型预测+随机采样+积分 | 纯跟踪+自适应速度 | 时间弹性带优化 |
| 输入 | 全局路径 + costmap | 全局路径 + costmap + robot dynamics | 全局路径 + 当前位姿 | 全局路径 + 障碍物 |
| 输出 | cmd_vel (vx, vy, vth) |
cmd_vel (vx, vy, vth) |
cmd_vel (vx, vth) |
cmd_vel (vx, vth) |
| 动态环境适应性 | ★★★☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ |
| 调参难度 | ★★★★☆(参数多) | ★★★☆☆(中等) | ★★☆☆☆(简单) | ★★★★★(非常复杂) |
| 输出平滑度 | ★★★☆☆ | ★★★★☆ | ★★★★☆ | ★★★★★ |
| 计算开销 | ★★☆☆☆ | ★★★★☆ | ★☆☆☆☆ | ★★★★☆ |
| 适合底盘 | 差速 | 差速/全向 | 阿克曼(车类) | 差速/全向 |
| ROS2 集成 | ✅ 内置 | ✅ 内置 | ✅ 内置 | ⚠️ 第三方包 |
场景决策指南:
- 室内慢速差速 → DWB(最经典、文档最多、社区最活跃)
- 动态人群环境 → MPPI(路径积分让机器人在静态障碍 + 动态行人之间找到「概率最优」路径)
- 阿克曼车模 → RPP(专门为转向半径约束设计,几乎零调参)
- 需要发表论文 → TEB + MPPI 对比实验(TEB 在 ROS1 时代就是学术标配)
4.3 规划器/控制器实时切换实例
插件化的威力在于「不需要重新编译」。切换到 MPPI 只需修改 YAML:
controller_server:
ros__parameters:
# 从 DWB 切到 MPPI
controller_plugin_types:
- "nav2_mppi_controller::MPPIController"
controller_plugin_ids:
- "FollowPath"
# MPPI 特有的评估器(critics)配置
FollowPath.model_trajectory_generator: "DiffDrive"
FollowPath.critics:
- "nav2_mppi_controller::ConstraintCritic"
- "nav2_mppi_controller::GoalAngleCritic"
- "nav2_mppi_controller::PathAlignCritic"
- "nav2_mppi_controller::PathFollowCritic"
- "nav2_mppi_controller::PreferForwardCritic"
- "nav2_mppi_controller::ObstacleCritic"
# MPPI 核心参数
FollowPath.iteration_count: 1
FollowPath.lookahead_dist: 0.3
FollowPath.batch_size: 500 # 随机采样轨迹数量
FollowPath.time_steps: 20 # 时间步数
FollowPath.model_dt: 0.05 # 离散时间步长
FollowPath.vx_max: 0.3
FollowPath.vy_max: 0.0
FollowPath.wz_max: 1.2
FollowPath.temperature: 0.2 # 探索-利用温度参数
你可以同时加载两个控制器,在行为树中动态切换:
controller_plugin_types:
- "dwb_core::DWBLocalPlanner"
- "nav2_mppi_controller::MPPIController"
controller_plugin_ids:
- "FollowPath" # DWB
- "SmoothFollow" # MPPI
然后在 BT XML 中用不同的 controller_id 调用不同的控制器,实现「走廊用 DWB、大堂用 MPPI」的场景自适应导航。
5. 行为树(BT)自定义:当默认流程不够用
5.1 默认行为树的局限性
Nav2 的默认 BT navigate_to_pose_w_replanning_and_recovery.xml 提供了可靠的「计算路径→跟踪路径→重规划→恢复」循环。但真实世界的需求总比默认流程复杂:
- 「出发前先检查电池,低于 20% 就拒绝导航」
- 「到达目标后拍照上传云端,确认成功才算完成任务」
- 「导航过程中每 10 秒播报一次状态语音」
- 「如果遇到不可逾越的障碍,发送报警消息并原地等待」
这些需求在默认流程中无法实现——你必须自定义行为树。
5.2 自定义 BT 完整开发流程
以一个实际需求为例:机器人导航前先通过 TTS 播报 「请注意,机器人即将移动」。
Step 1:创建自定义 BT Action Node(C++)
// include/say_hello_bt_node.hpp
#ifndef MY_NAV2_PLUGINS__SAY_HELLO_BT_NODE_HPP_
#define MY_NAV2_PLUGINS__SAY_HELLO_BT_NODE_HPP_
#include <string>
#include <memory>
#include "behaviortree_cpp_v3/action_node.h"
#include "rclcpp/rclcpp.hpp"
namespace my_robot_navigation {
class SayHelloAction : public BT::SyncActionNode {
public:
explicit SayHelloAction(const std::string& name,
const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config) {
node_ = rclcpp::Node::make_shared("say_hello_bt_node");
}
// 声明该节点支持的端口(参数)
static BT::PortsList providedPorts() {
return {
BT::InputPort<std::string>("message", "Hello from robot!"),
BT::InputPort<double>("duration", 1.0)
};
}
// tick() 是核心——每次被唤醒时执行
BT::NodeStatus tick() override {
std::string message;
double duration;
// 从行为树 XML 中读取参数
if (!getInput("message", message)) {
RCLCPP_ERROR(node_->get_logger(), "Missing 'message' input port");
return BT::NodeStatus::FAILURE;
}
getInput("duration", duration);
RCLCPP_INFO(node_->get_logger(), "播报: %s", message.c_str());
// 这里可以调用 TTS 服务、播放声音文件等
// 示例:通过 ROS2 service 调用语音合成服务
// auto client = node_->create_client<tts_srv::TTS>("/tts/speak");
// ...
return BT::NodeStatus::SUCCESS;
}
private:
rclcpp::Node::SharedPtr node_;
};
} // namespace my_robot_navigation
#endif
Step 2:注册插件到 BT Factory
// src/bt_plugin_export.cpp
#include "my_robot_navigation/say_hello_bt_node.hpp"
#include "behaviortree_cpp_v3/bt_factory.h"
#include "pluginlib/class_list_macros.hpp"
// 向 pluginlib 注册(让 ROS2 能找到这个库)
PLUGINLIB_EXPORT_CLASS(my_robot_navigation::SayHelloAction, BT::SyncActionNode)
// 向 BehaviorTree.CPP 注册
BT_REGISTER_NODES(factory) {
factory.registerNodeType<my_robot_navigation::SayHelloAction>("SayHello");
}
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(my_nav2_plugins)
find_package(ament_cmake REQUIRED)
find_package(nav2_common REQUIRED)
find_package(rclcpp REQUIRED)
find_package(behaviortree_cpp_v3 REQUIRED)
add_library(my_say_hello_bt_node SHARED
src/bt_plugin_export.cpp
)
target_include_directories(my_say_hello_bt_node PUBLIC include)
ament_target_dependencies(my_say_hello_bt_node
rclcpp
behaviortree_cpp_v3
)
# 插件描述文件
nav2_common::generate_behavior_tree_plugin_description(
my_say_hello_bt_node
my_say_hello_bt_node.xml
)
install(TARGETS my_say_hello_bt_node
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/ DESTINATION include/)
install(DIRECTORY behavior_trees/ DESTINATION share/${PROJECT_NAME}/behavior_trees)
ament_package()
Step 3:编写自定义行为树 XML
<!-- my_navigate_bt.xml -->
<?xml version="1.0"?>
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<!-- ★ 自定义节点:导航前打招呼 -->
<SayHello message="请注意,机器人即将移动" duration="2.0"/>
<!-- 电池电量检查:低于25%拒绝导航 -->
<Fallback name="BatteryCheck">
<Sequence>
<Condition ID="CheckBattery">
<threshold>25</threshold>
</Condition>
<SayHello message="警告:电量不足,无法执行导航任务"/>
</Sequence>
</Fallback>
<!-- 标准导航循环 -->
<PipelineSequence name="NavigateWithReplanning">
<RateController hz="1.0">
<RecoveryNode number_of_retries="1" name="ComputePath">
<ComputePathToPose goal="{goal}" path="{path}"
planner_id="GridBased"
error_code_id="{compute_path_error_code}"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Context"
service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode>
</RateController>
<RecoveryNode number_of_retries="1" name="FollowPath">
<FollowPath path="{path}" controller_id="FollowPath"/>
<Sequence name="ClearingActions">
<ClearEntireCostmap name="ClearLocalCostmap-Subtree"
service_name="local_costmap/clear_entirely_local_costmap"/>
</Sequence>
</RecoveryNode>
</PipelineSequence>
<!-- ★ 导航完成后打招呼 -->
<SayHello message="已到达目标位置,任务完成"/>
</BehaviorTree>
</root>
Step 4:配置 Nav2 加载自定义 BT
bt_navigator:
ros__parameters:
bt_xml_filename: "/path/to/my_navigate_bt.xml"
plugin_lib_names:
- "nav2_compute_path_to_pose_action_bt_node"
- "my_say_hello_bt_node" # ★ 加载自定义插件库
5.3 进阶 BT 设计模式
| 模式 | BT 结构 | 说明 |
|---|---|---|
| 前置检查 | Sequence[Check → Navigate] |
条件不满足则跳过导航 |
| 多路由恢复 | Fallback[Spin → Backup → Wait] |
依次尝试多种脱困方式 |
| 巡逻循环 | Repeat × [Nav(A)→Wait(10s)→Nav(B)→Wait(10s)] |
两点间无限循环巡逻 |
| 条件路由 | Fallback[Seq[LowBat→Charge], Navigate] |
低电量时自动切换任务 |
| 带超时 | Timeout(30s) → Navigate |
30 秒内不完成则放弃 |
6. Costmap 避障调优与参数调参实战
6.1 Costmap 的分层架构
Nav2 的代价地图遵循「分层叠加」的设计模式。不要把 costmap 想象成一张图,而是 N 张独立计算的图层按「取最大值」的规则合并:
┌─────────────────────────────────┐
│ Final Costmap(最终代价地图) │ ← 各层最大值合并
├─────────────────────────────────┤
│ Inflation Layer(膨胀层) │ ← 以障碍物为中心向外衰减的高斯/指数代价
├─────────────────────────────────┤
│ Obstacle Layer(动态障碍物层) │ ← 激光雷达/深度相机实时扫描的障碍物
├─────────────────────────────────┤
│ Static Layer(静态地图层) │ ← SLAM 地图(已占/未知/空闲)
├─────────────────────────────────┤
│ Voxel Layer(3D体素→2D投影层) │ ← 深度相机点云投影到 2D
├─────────────────────────────────┤
│ Range Layer(测距层) │ ← 超声波传感器数据
└─────────────────────────────────┘
每一层独立维护自己的 cost 数组。最终 costmap 的每个 cell 取所有 layer 的最大值——这是保守策略,宁可多标记障碍也不漏掉。
6.2 膨胀半径调优:最重要的参数
inflation_radius 决定了障碍物周围「不可通行」区域的大小。代价函数数学形式:
cost(d) = 253 × exp(-k × (d - r_inscribed) / (r_inflation - r_inscribed))
其中 k = cost_scaling_factor,d 是距离障碍物的距离,r_inscribed 是机器人的内切圆半径。
调参实践:
local_costmap:
ros__parameters:
inflation_layer:
# 膨胀半径 = 2.5 × 机器人半径(保守)
inflation_radius: 0.55
# 代价衰减因子:
# 1.0-2.0:缓慢衰减,远离障碍物仍有较高代价 → 路径远离障碍物
# 3.0-5.0:快速衰减,仅障碍物附近代价高 → 路径贴近障碍物
# 10.0+:急剧衰减,几乎只有障碍物 cell 本身代价高 → 激进穿行
cost_scaling_factor: 3.0
口诀:
- 室内窄门场景 → 降低
inflation_radius(否则门被膨胀层封死,永远找不到路) - 动态人群环境 → 增大
inflation_radius(给障碍物更多安全缓冲) - 如果机器人「撞墙才转弯」→ 加大
cost_scaling_factor - 如果机器人「离墙还有 1 米就绕」→ 减小
cost_scaling_factor
6.3 常见避障故障诊断流程
当机器人撞到障碍物或停在原地不动时,按以下顺序排查:
- 可视化 costmap:在 rviz2 中添加
/local_costmap/costmap和/global_costmap/costmap。如果传感器看到了障碍物但 costmap 上没有标记 → 检查observation_sources话题配置 - 检查 TF:
ros2 run tf2_tools view_frames。最常见的问题是base_link到laser_frame的变换有静态偏移错误——障碍物被标记在错误的位置 - 传感器数据:
ros2 topic echo /scan --once确认激光雷达数据正常且frame_id正确 - 时间同步:
use_sim_time参数是否与真实机器人一致。真实机器人用false,Gazebo 仿真用true - 膨胀半径过大:如果 costmap 显示整个走廊都是「红色」(致命代价),降低
inflation_radius - 频率不匹配:
update_frequency太低(< 2 Hz)会导致高速运动时避障延迟
6.4 自定义 Keepout 区域
有时你需要让机器人避开某些固定区域(如禁行区、员工休息区)。可以用 KeepoutFilter 插件:
local_costmap:
ros__parameters:
plugins: ["obstacle_layer", "inflation_layer", "keepout_filter"]
keepout_filter:
plugin: "nav2_costmap_2d::KeepoutFilter"
enabled: true
filter_info_topic: "/keepout_zones" # 订阅禁行区数据
然后发布禁行区多边形:
ros2 topic pub /keepout_zones nav2_msgs/msg/CostmapFilterInfo \
"{header: {frame_id: 'map'},
type: 0, filter_mask_topic: '/filter_mask'}"
7. 多机器人协同导航:从单机到集群
7.1 命名空间隔离方案
ROS2 的命名空间机制让多机器人导航变得极为简洁。每个机器人运行完全独立的 Nav2 实例:
# Robot 1 (TurtleBot Waffle)
ros2 launch nav2_bringup navigation_launch.py \
namespace:=robot1 \
params_file:=config/robot1_nav.yaml \
use_sim_time:=true
# Robot 2 (TurtleBot Burger)
ros2 launch nav2_bringup navigation_launch.py \
namespace:=robot2 \
params_file:=config/robot2_nav.yaml \
use_sim_time:=true
# Robot 3
ros2 launch nav2_bringup navigation_launch.py \
namespace:=robot3 \
params_file:=config/robot3_nav.yaml \
use_sim_time:=true
每个机器人的话题自动带上前缀:/robot1/navigate_to_pose、/robot2/local_costmap/costmap、/robot3/amcl_pose……相互完全隔离,不需要任何额外配置。
7.2 多机器人碰撞避免
当多个机器人共享同一物理空间时,需要让它们互相「看到」彼此。方案是每个机器人发布自己的位姿,其他机器人将其作为动态障碍物标记在 costmap 中:
#!/usr/bin/env python3
"""多机器人感知节点:将同类的位姿发布为障碍物"""
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseWithCovarianceStamped
class MultiRobotAwareness(Node):
def __init__(self):
super().__init__('multi_robot_awareness')
self.declare_parameter('my_namespace', 'robot1')
self.declare_parameter('peer_namespaces', ['robot2', 'robot3'])
my_ns = self.get_parameter('my_namespace').value
peers = self.get_parameter('peer_namespaces').value
# 为每个同类机器人的位置发布 costmap 障碍物标记
self.publishers = {}
for peer in peers:
topic = f'/{my_ns}/peer_{peer}_pose'
self.publishers[peer] = self.create_publisher(
PoseWithCovarianceStamped, topic, 10)
# 订阅同类的位姿
self.create_subscription(
PoseWithCovarianceStamped,
f'/{peer}/amcl_pose',
lambda msg, p=peer: self.peer_pose_cb(msg, p),
10)
def peer_pose_cb(self, msg, peer_ns):
self.publishers[peer_ns].publish(msg)
self.get_logger().debug(
f'Peer {peer_ns} at ({msg.pose.pose.position.x:.2f}, '
f'{msg.pose.pose.position.y:.2f})')
def main():
rclpy.init()
node = MultiRobotAwareness()
rclpy.spin(node)
rclpy.shutdown()
7.3 集中式 vs 分布式调度对比
| 维度 | 集中式调度 | 分布式调度 |
|---|---|---|
| 决策方式 | 中央服务器为所有机器人计算路径 | 每个机器人独立计算 |
| 全局最优性 | ✅ 可以做到全局最优 | ❌ 可能局部最优但全局冲突 |
| 单点故障风险 | ❌ 中央服务器宕机=所有机器人停摆 | ✅ 任一机器人故障不影响其他 |
| 通信带宽 | ❌ 所有传感器数据汇入中央,压力大 | ✅ 仅需协调信息,带宽需求低 |
| 扩展性 | ❌ 机器人数量增加时中央不堪重负 | ✅ 线性扩展 |
| 实现复杂度 | ★★★☆☆ | ★★☆☆☆ |
| 死锁风险 | ✅ 中央可检测并解决 | ❌ 可能出现循环等待死锁 |
工业实践推荐:混合式——顶层由中央任务调度器分配「去哪」,底层每个机器人用 Nav2 自主规划「怎么去」。任务层集中、执行层分布。
8. 疑难排查与常见问题 FAQ
Q1:机器人到达目标点后不停原地旋转,为什么?
原因:位置偏差(xy_tolerance)已满足,但朝向偏差(yaw_tolerance)未满足。机器人尝试通过旋转对准目标朝向,但由于里程计噪声或机械回程差,永远无法精确达到设置的 yaw 角度。
解决:
controller_server:
ros__parameters:
xy_goal_tolerance: 0.15
yaw_goal_tolerance: 0.1 # 约 5.7°,如果还旋转则增大到 0.15-0.2
也可以换用更宽松的 GoalChecker:nav2_rotation_shim_controller::RotationShimController。
Q2:规划器报「No valid path found」,但地图上明显有路?
排查清单:
- 检查目标点是否在 costmap 范围外(global_costmap 默认 100×100 cells = 5m×5m,如果目标在 10m 外需要增大 width/height)
- 目标点是否落在膨胀半径内(靠墙太近,对于 planner 来说等于点在障碍物里)
- 地图上通向目标的路是否被膨胀层封死(窄门+大膨胀半径=看起来有路实际没有)
- 尝试打开
allow_unknown: true(允许规划器穿越未知区域)
Q3:DWB 控制器让机器人走「之」字形,轨迹扭来扭去?
原因:PathAlign 评估器的权重相对 GoalDist 过高,导致控制器更关心「贴近路径」而不是「到达目标」。
解决:在 DWB 参数中调整评估器比例:
FollowPath.PathAlign.scale: 10.0 # 降低这个值
FollowPath.GoalDist.scale: 30.0 # 提高这个值
FollowPath.Oscillation.scale: 5.0 # 增大振荡惩罚
Q4:Nav2 启动后 costmap 一片空白,没有任何障碍物标记?
原因:传感器数据没有被 costmap 的障碍物层正确订阅。
排查:
- 确认
observation_sources中声明的 topic 名与实际传感器话题一致:ros2 topic list | grep scan找到实际话题名 - 确认传感器的
frame_id在 TF 树中存在 - 确认
data_type与传感器消息类型匹配(LaserScan vs PointCloud2)
Q5:AMCL 定位发散(粒子云越来越散,机器人位姿跳来跳去)怎么办?
原因:里程计噪声过大或粒子数不足。
解决:
amcl:
ros__parameters:
max_particles: 5000 # 大幅增加粒子数
min_particles: 1000
update_min_d: 0.05 # 减少更新距离,让粒子更频繁重采样
recovery_alpha_slow: 0.001
recovery_alpha_fast: 0.1
Q6:行为树中的自定义节点没有被加载,日志显示「Node not found: SayHello」?
三步确认:
- 插件是否用
BT_REGISTER_NODES和PLUGINLIB_EXPORT_CLASS双重注册 bt_navigator的plugin_lib_names中是否包含你的库名(不含lib前缀和.so后缀)- 库文件的路径是否在 ROS2 的库搜索路径中:
echo $LD_LIBRARY_PATH
Q7:多机器人导航时 local_costmap 显示其他机器人的轨迹,互相干扰?
原因:不同机器人的 costmap 话题如果使用了相同的命名空间,会互相覆盖。
解决:确保每个机器人有独立的命名空间,local_costmap 话题自然隔离为 /robot1/local_costmap/costmap、/robot2/local_costmap/costmap。
Q8:在真机上运行 Nav2 时控制器输出抖动严重(cmd_vel 忽大忽小)?
原因:局部 costmap 更新频率与控制器频率不匹配,或者 costmap 噪声太大。
解决:
local_costmap:
ros__parameters:
update_frequency: 10.0 # 提高到 10 Hz
publish_frequency: 5.0
controller_server:
ros__parameters:
min_speed_xy: 0.05 # 设置最低速度,避免完全停止-启动的抖动
max_vel_theta: 0.8 # 降低最大角速度,减少惯性摆动
9. 总结与进阶学习路线
9.1 核心要点回顾
| 方面 | 核心认知 |
|---|---|
| BT 驱动架构 | 行为树替代状态机,XML 编排取代硬编码——导航流程的灵活性有了质的飞跃 |
| 插件化设计 | 规划器、控制器、恢复行为、costmap 层——一切都是可替换的插件,按 YAML 配置组装 |
| 分层 costmap | 静态地图 + 动态障碍 + 膨胀缓冲——多层独立计算、取最大值合并 |
| 多机器人 | ROS2 命名空间原生隔离 + keepout 防碰撞——从单机到集群几乎是线性的 |
| 关键参数 | inflation_radius(安全距离)+ cost_scaling_factor(衰减速度)决定 90% 的避障表现 |
| 黄金组合 | Smac Planner + DWB Controller ——目前最成熟、文档最全、社区最活跃的组合 |
9.2 四阶段学习路线
第一阶段:新手村(1-2 周)
- 在 Gazebo + TurtleBot3 上跑通标准导航 demo
- 理解 rviz2 中的 costmap 可视化、AMCL 粒子云
- 用「2D Goal Pose」工具手动点击目标点,观察机器人的完整自主导航过程
- 目标:能独立启动仿真环境并完成一次导航
第二阶段:理解期(2-4 周)
- 逐行阅读默认 BT XML,理解每个节点的作用
- 对比 NavFn / Smac / Theta* 三种规划器的行为差异
- 修改 inflation_radius 观察避障行为的变化
- 目标:能根据机器人尺寸和环境自主调参
第三阶段:实战篇(4-8 周)
- 编写第一个自定义 BT 节点(如 CheckBattery)
- 在真实机器人上部署 Nav2,处理 TF 树配置和传感器对应
- 对比 DWB / MPPI 在同一环境下的表现差异
- 目标:能在真实机器人上稳定运行 Nav2
第四阶段:进阶篇(8+ 周)
- 多机器人协同调度——集中任务分配 + 分布导航执行
- 自定义 costmap layer——如社交避障层(Social Layer)
- BT 与更高级任务编排框架(如 BehaviorTree.CPP + ros2_control)的深度集成
- 贡献 Nav2 开源社区
9.3 延伸阅读与资源
| 资源 | 链接 / 关键词 |
|---|---|
| Nav2 官方文档 | navigation.ros.org——最权威的参考,每个参数都有说明 |
| BehaviorTree.CPP | github.com/BehaviorTree/BehaviorTree.CPP——理解 BT 引擎底层 |
| Smac Planner 论文 | "Practical Search Techniques in Path Planning for Autonomous Driving" |
| MPPI Controller 论文 | "Information Theoretic MPC for Model-Based Reinforcement Learning" |
| ROS2 Discourse | discourse.ros.org/c/ros2——遇到问题先搜这里 |
| TurtleBot3 Nav2 | github.com/ROBOTIS-GIT/turtlebot3——最完整的学习示例 |
| Gazebo 仿真 | classic.gazebosim.org——仿真环境搭建指南 |
本文由 MarkShareX AI 自动创作,分类:ROS2,方向:Nav2 导航