ROS2 Navigation2 完全指南:从行为树到多机器人协同导航 🚀

ROS2 1 次阅读
ROS2 Navigation2 完全指南:从行为树到多机器人协同导航 🚀

分类: 12 | 标签: ROS2, Nav2, Navigation2, 机器人导航, 行为树

ROS2 Navigation2 完全指南:从行为树到多机器人协同导航 🚀

深入拆解 Nav2 核心架构,手把手带你掌握现代机器人自主导航的每个关键环节——从零搭建导航栈、自定义行为树插件、调试避障算法、到多机器人协同调度

目录

  1. Nav2 是什么?为什么它改变了ROS导航游戏规则
  2. 核心架构深度解析:BT + 动作服务器 + 插件生态
  3. 从零搭建一个 Nav2 导航栈
  4. 全局规划器与局部控制器:算法选型对比
  5. 行为树(BT)自定义:当默认流程不够用
  6. Costmap 避障调优与参数调参实战
  7. 多机器人协同导航:从单机到集群
  8. 疑难排查与常见问题 FAQ
  9. 总结与进阶学习路线

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 样条)生成连续平滑的轨迹

Nav2 系统架构全景图

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 树

导航的「黄金三角」前置条件:

  1. 地图(Map):通过 SLAM 构建的 occupancy grid map。推荐 slam_toolbox(在线建图)或直接用已建好的 .pgm / .yaml 地图文件
  2. 定位(Localization):AMCL(自适应蒙特卡洛定位)持续估计机器人在 map 坐标系下的位姿。定位质量直接决定导航精度
  3. 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"    # ★ 加载自定义插件库

Nav2 行为树结构图

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 的最大值——这是保守策略,宁可多标记障碍也不漏掉。

Nav2 Costmap 分层架构图

6.2 膨胀半径调优:最重要的参数

inflation_radius 决定了障碍物周围「不可通行」区域的大小。代价函数数学形式:

cost(d) = 253 × exp(-k × (d - r_inscribed) / (r_inflation - r_inscribed))

其中 k = cost_scaling_factord 是距离障碍物的距离,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 常见避障故障诊断流程

当机器人撞到障碍物或停在原地不动时,按以下顺序排查:

  1. 可视化 costmap:在 rviz2 中添加 /local_costmap/costmap/global_costmap/costmap。如果传感器看到了障碍物但 costmap 上没有标记 → 检查 observation_sources 话题配置
  2. 检查 TFros2 run tf2_tools view_frames。最常见的问题是 base_linklaser_frame 的变换有静态偏移错误——障碍物被标记在错误的位置
  3. 传感器数据ros2 topic echo /scan --once 确认激光雷达数据正常且 frame_id 正确
  4. 时间同步use_sim_time 参数是否与真实机器人一致。真实机器人用 false,Gazebo 仿真用 true
  5. 膨胀半径过大:如果 costmap 显示整个走廊都是「红色」(致命代价),降低 inflation_radius
  6. 频率不匹配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」,但地图上明显有路?

排查清单:

  1. 检查目标点是否在 costmap 范围外(global_costmap 默认 100×100 cells = 5m×5m,如果目标在 10m 外需要增大 width/height)
  2. 目标点是否落在膨胀半径内(靠墙太近,对于 planner 来说等于点在障碍物里)
  3. 地图上通向目标的路是否被膨胀层封死(窄门+大膨胀半径=看起来有路实际没有)
  4. 尝试打开 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 的障碍物层正确订阅。

排查

  1. 确认 observation_sources 中声明的 topic 名与实际传感器话题一致:ros2 topic list | grep scan 找到实际话题名
  2. 确认传感器的 frame_id 在 TF 树中存在
  3. 确认 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」?

三步确认:

  1. 插件是否用 BT_REGISTER_NODESPLUGINLIB_EXPORT_CLASS 双重注册
  2. bt_navigatorplugin_lib_names 中是否包含你的库名(不含 lib 前缀和 .so 后缀)
  3. 库文件的路径是否在 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 导航