开始之前想先问你一个问题:是不是收藏了一堆 ROS 入门资料,结果打开全是 Ubuntu 18.04 + ROS1 Melodic?或者下载好 Gazebo 之后,小车模型还没动起来,半天就过去了?
目前 ROS2 已经成为机器人开发的主流选择,无论是学校里的课题、毕业设计,还是企业里的导航应用,绝大多数新项目都开始基于 ROS2 落地。Gazebo 仿真、SLAM 建图、Nav2 自主导航这三件事,基本上构成了一个完整机器人项目的“最小闭环”:机器人能在仿真环境中感知周围环境、构建地图、规划路径并自主移动。
这篇文章会围绕 ROS2 Humble 版本,从环境搭建开始,逐步完成一个差速小车(就是大家喜欢叫“小黄鸭”的那种模型)的仿真环境搭建,再实现激光 SLAM 建图,最后用 Nav2 完成自主导航。整个流程会尽量给出完整命令、配置文件和代码,每一步说明“为什么要这么做”。无论你是刚装好 Ubuntu 的零基础新手,还是准备把 ROS2 项目直接复用到毕业设计中的同学,这篇文章都值得收藏备用。
文章内容以 Gazebo + RViz2 + Nav2 为主线,不涉及 TurtleBot3 之外的自定义复杂底盘,也不讨论真实硬件部署,重点把仿真环境下的整套流程跑通。
1. 先弄清几个核心概念
很多同学刚接触这套技术栈时,经常被 SLAM、导航、仿真这几个词绕晕。先把核心概念理清楚,后面操作时会更有方向。
1.1 ROS2 是什么
ROS2(Robot Operating System 2)是一套面向机器人开发的分布式通信框架。它不是操作系统,而是运行在 Linux 等系统之上的一层中间件,负责解决机器人各个模块之间的通信问题。比如激光雷达数据要发给建图算法,建图结果要发给导航模块,导航结果要发给底盘驱动,这些模块之间的数据传递、进程管理、消息定义,都是 ROS2 负责的事情。
ROS2 相比 ROS1 最大的变化是使用了 DDS(Data Distribution Service)作为底层通信中间件,天然支持分布式、实时性和多机通信。对于做毕设或者入门学习的同学来说,主要感受到的区别就是:启动方式变了(不再用 roscore)、工作空间编译方式变了(用 colcon 而不是 catkin)、功能包结构也变了。
1.2 Gazebo 仿真环境
Gazebo 是一个三维机器人仿真环境,内置物理引擎、传感器模型和环境模型。在 Gazebo 里可以给机器人添加激光雷达、摄像头、IMU 等传感器,也可以设置墙壁、障碍物、坡度等环境元素。
使用仿真环境的最大价值在于:在没有实体硬件的情况下,可以验证 SLAM 算法、导航算法、路径规划逻辑是否正确。比如你不想直接在小车上烧录代码测试撞墙,可以先在 Gazebo 里把参数调好。
1.3 SLAM 建图
SLAM 全称 Simultaneous Localization and Mapping,中文意思是“同时定位与建图”。简单理解就是:机器人在一个未知环境中移动时,一边确定自己所在的位置,一边构建环境地图。
在 ROS2 生态中,常用的是 Cartographer 和 SLAM Toolbox。本文会以 SLAM Toolbox 为例,因为它在 ROS2 中集成度高、配置简洁,对新手很友好。建图输出的地图是一个二维栅格地图(通常以 pgm 图片和 yaml 配置文件保存),这张地图后续会直接交给导航模块使用。
1.4 Nav2 自主导航
Nav2 是 ROS2 的导航框架,功能非常庞大,包含全局路径规划、局部路径规划、行为树、代价地图、AMCL 定位等模块。一句话概括它的作用:机器人已经有一张地图,也知道自己在地图中的位置,Nav2 负责规划出一条从当前位置到目标点的路径,并控制机器人沿路径安全移动。
导航过程中,机器人需要避开动态障碍物、在狭窄通道调整方向、到达目标点后停止。这些复杂决策在 Nav2 中通过“行为树”来组织管理。
用一张简图理解整个流程:
Gazebo 仿真环境(小车 + 激光雷达 + 障碍物) ↓ SLAM 建图(SLAM Toolbox + RViz2) ↓ 保存地图(pgm + yaml) ↓ Nav2 自主导航(地图载入 + AMCL 定位 + 路径规划)2. 环境准备:从零搭建 ROS2 Humble 开发环境
在做任何功能包开发之前,先把环境准备好。下面给出的是基于 Ubuntu 22.04 + ROS2 Humble 的方案,这也是目前稳定性最高、教程最多的组合。如果你想用 Ubuntu 24.04 + ROS2 Jazzy,部分命令需要微调,建议先按 Humble 跑通全流程。
2.1 安装 Ubuntu 22.04
如果你已经安装好 Ubuntu,跳过这一节。如果是从零开始,推荐在 Windows 上使用 VMware 或 VirtualBox 安装虚拟机,也可以直接双系统安装。虚拟机方式对新手最友好,建议分配至少 4 核 CPU、8GB 内存,硬盘 60GB 以上,因为 Gazebo 和 ROS2 整体占用空间不小。
2.2 安装 ROS2 Humble
ROS2 官方提供了一键式安装脚本,但为了让大家理解每一步,这里拆开执行。
先设置编码和软件源:
sudo apt update && sudo apt install locales sudo locale-gen en_US en_US.UTF-8 sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 export LANG=en_US.UTF-8添加 ROS2 软件源:
sudo apt install software-properties-common curl sudo add-apt-repository universe sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg写入软件源列表:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null更新并安装 ROS2 Humble 桌面版(包含 RViz2、演示程序等):
sudo apt update sudo apt upgrade sudo apt install ros-humble-desktop安装完成后,把 ROS2 环境配置写入~/.bashrc,这样每次打开终端不用手动 source:
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc source ~/.bashrc验证是否安装成功:
ros2 --version如果输出类似ros2 0.19.x或更高版本号,说明安装成功。
2.3 安装 Gazebo 与 Navigation2
ROS2 Humble 默认集成了 Gazebo 11(即ros-humble-gazebo-ros-pkgs),直接安装即可:
sudo apt install ros-humble-gazebo-ros-pkgs安装 Nav2 导航框架和 SLAM Toolbox:
sudo apt install ros-humble-nav2-bringup sudo apt install ros-humble-slam-toolbox安装建图、导航过程中需要用到的一些功能包:
sudo apt install ros-humble-cartographer ros-humble-cartographer-ros sudo apt install ros-humble-navigation2 ros-humble-nav2-bringup sudo apt install ros-humble-turtlebot3-gazebo这里说明一下:TurtleBot3 在 Gazebo 中有现成的模型包,非常适合做教学演示。本文后面的案例将从 TurtleBot3 的差速小车模型入手,你完全可以把它替换成自己的小黄鸭模型或自制底盘模型,核心原理一致。
2.4 安装开发工具
ROS2 中,功能包编译使用colcon,需要单独安装:
sudo apt install python3-colcon-common-extensions sudo apt install python3-rosdep sudo apt install gitrosdep用于安装功能包的依赖项,后面编译自己写的功能包时非常常用。
2.5 创建并初始化工作空间
ROS2 的代码组织方式是“工作空间(workspace)+ 功能包(package)”。先创建自己的工作空间:
mkdir -p ~/ros2_ws/src cd ~/ros2_ws colcon build如果当前还没有任何功能包,colcon build会直接构建一个空环境。构建完以后,每次打开新终端需要 source 工作空间:
echo "source ~/ros2_ws/install/setup.bash" >> ~/.bashrc source ~/.bashrc到这里,环境准备基本完成。接下来我们创建一个自己的 ROS2 功能包,为后面编写小车模型和控制逻辑做准备。
3. 创建 ROS2 功能包:写第一个可运行节点
很多入门教程会在这一步直接让你启动 TurtleBot3 的 Gazebo 仿真,但那样很难理解功能包的组织结构。所以我们先手动创建一个功能包,写一个最简单的节点,把“编译-运行-通信”这个链路理解清楚。
3.1 功能包结构说明
在 ROS2 中,功能包是最基本的组织单元。一个 Python 功能包通常包含:
my_robot/ ├── package.xml # 功能包元信息:名称、依赖、版本 ├── setup.py # Python 包的安装配置(仅 Python 功能包) ├── setup.cfg # 插件入口配置 ├── resource/ # 资源标记文件 ├── launch/ # 存放 launch 文件,启动多个节点 ├── urdf/ # 存放机器人模型文件(URDF/Xacro) ├── config/ # 存放参数配置文件 └── my_robot/ # Python 包目录,存放节点代码3.2 创建功能包
用ros2 pkg create命令创建功能包:
cd ~/ros2_ws/src ros2 pkg create my_robot --build-type ament_python --dependencies rclpy geometry_msgs nav_msgs sensor_msgs tf2_ros这个命令会自动生成一个 Python 功能包,并声明依赖rclpy(ROS2 Python客户端库)、geometry_msgs(几何消息)、nav_msgs(导航消息)、sensor_msgs(传感器消息)、tf2_ros(TF 坐标变换)。
3.3 编写第一个发布者节点
在my_robot/my_robot/目录下,创建一个publisher_node.py文件,写一个最简单的速度发布器:
#!/usr/bin/env python3 # 文件路径:src/my_robot/my_robot/publisher_node.py import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist class VelocityPublisher(Node): def __init__(self): super().__init__('velocity_publisher') self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10) self.timer = self.create_timer(1.0, self.publish_velocity) def publish_velocity(self): msg = Twist() msg.linear.x = 0.2 msg.angular.z = 0.0 self.publisher_.publish(msg) self.get_logger().info('Publishing: linear.x=%.2f, angular.z=%.2f' % (msg.linear.x, msg.angular.z)) def main(args=None): rclpy.init(args=args) node = VelocityPublisher() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()这段代码逻辑很简单:创建一个节点,每隔 1 秒向/cmd_vel话题发布一个速度消息,消息内容为线速度 0.2 m/s、角速度 0 rad/s。
需要注意的是,/cmd_vel是 ROS2 中底盘速度控制的约定话题,格式是geometry_msgs/Twist。Gazebo 中的差速小车插件会订阅这个话题,从而驱动机器人移动。
3.4 配置 setup.py 与 setup.cfg
修改setup.py,把入口点配置进去:
# 文件路径:src/my_robot/setup.py from setuptools import setup package_name = 'my_robot' setup( name=package_name, version='0.0.0', packages=[package_name], data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), ], install_requires=['setuptools'], zip_safe=True, maintainer='your_name', maintainer_email='your_email@example.com', description='My robot package for ROS2', license='Apache-2.0', entry_points={ 'console_scripts': [ 'velocity_publisher = my_robot.publisher_node:main', ], }, )3.5 编译并运行
cd ~/ros2_ws colcon build --packages-select my_robot source install/setup.bash ros2 run my_robot velocity_publisher如果终端中持续输出Publishing: linear.x=0.20, angular.z=0.00,说明节点运行正常。
这时再开一个终端,用ros2 topic echo查看话题消息:
source ~/.bashrc ros2 topic echo /cmd_vel你会看到类似下面的输出:
linear: x: 0.2 y: 0.0 z: 0.0 angular: x: 0.0 y: 0.0 z: 0.0 ---到这里,ROS2 的“创建功能包 → 编写节点 → 编译 → 运行 → 查看话题”基础链路已经走通。接下来进入重点:在 Gazebo 中搭建仿真环境。
4. 仿真环境搭建:URDF 模型 + Gazebo + RViz2
Gazebo 仿真需要一个机器人模型。这里使用 TurtleBot3 的模型示例,它自带差速驱动插件和激光雷达插件,非常适合 SLAM 和导航演示。官方提供了一键式启动方式,我们先从最简单的启动命令开始,再看如何拆解模型文件。
4.1 一键启动 TurtleBot3 仿真
安装完ros-humble-turtlebot3-gazebo后,先设置模型环境变量:
echo "export TURTLEBOT3_MODEL=burger" >> ~/.bashrc source ~/.bashrc启动一个空的 Gazebo 世界:
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py这个命令会同时启动:
- Gazebo 仿真环境,加载 TurtleBot3 Burger 模型;
- 机器人模型中的激光雷达传感器,发布
/scan话题; - 机器人模型中的 TF 坐标变换,发布
odom、base_link、laser等坐标系。
启动完成后,Gazebo 窗口中应当能看到一个小车模型,周围有墙壁和障碍物。RViz2 也会自动打开,可以看到激光雷达的点云数据。
如果 Gazebo 窗口长时间不出现,常见原因是显卡兼容性问题,可以尝试在终端设置:
export LIBGL_ALWAYS_SOFTWARE=1再重新启动。
4.2 让小车动起来
在启动 TurtleBot3 仿真的同时,打开另一个终端,发布速度指令:
ros2 topic pub --rate 1 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.15, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.2}}"观察 Gazebo 窗口,小车应该会边前进边转弯。这个操作验证了仿真环境中的差速驱动插件正常工作。
按Ctrl+C停止发布。
4.3 自定义模型的思路
如果你不想使用 TurtleBot3 的默认模型,也可以自己写 URDF/Xacro 文件。这里以一个极简差速小车为例,展示模型文件的基本结构。
在my_robot/urdf/目录下创建my_robot.urdf.xacro:
<?xml version="1.0"?> <robot name="my_robot" xmlns:xacro="http://www.ros.org/wiki/xacro"> <!-- 底盘 --> <link name="base_link"> <visual> <geometry> <box size="0.3 0.2 0.08"/> </geometry> <origin rpy="0 0 0" xyz="0 0 0.04"/> </visual> <collision> <geometry> <box size="0.3 0.2 0.08"/> </geometry> <origin rpy="0 0 0" xyz="0 0 0.04"/> </collision> <inertial> <mass value="2.0"/> <inertia ixx="0.01" iyy="0.01" izz="0.01" ixy="0.0" ixz="0.0" iyz="0.0"/> </inertial> </link> <!-- 激光雷达 --> <link name="laser_link"> <visual> <geometry> <cylinder radius="0.03" length="0.02"/> </geometry> <origin rpy="0 0 0" xyz="0 0 0.09"/> </visual> </link> <joint name="laser_joint" type="fixed"> <parent link="base_link"/> <child link="laser_link"/> <origin xyz="0 0 0.08" rpy="0 0 0"/> </joint> </robot>这只是一个最简单的模型描述,没有添加 Gazebo 插件(差速驱动和激光雷达插件)。实际项目中,还需要在模型文件尾部添加<gazebo>标签引入插件。考虑到篇幅,这里不展开全部插件配置,重点是想让大家理解:URDF/Xacro 描述的只是机器人长什么样,而 Gazebo 插件才决定它在仿真中能不能动、能不能感知环境。
4.4 使用 RViz2 查看传感器数据
RViz2 是 ROS2 的可视化工具。在仿真运行过程中,打开一个新终端:
ros2 run rviz2 rviz2在 RViz2 窗口中,需要手动添加若干个显示项:
| 显示项 | 话题/类型 | 作用 |
|---|---|---|
| RobotModel | /robot_description | 显示机器人模型 |
| LaserScan | /scan | 显示激光雷达点云 |
| TF | /tf | 显示坐标系变换 |
| Map | /map | 显示地图(建图或导航时使用) |
添加 LaserScan 后,需要把 Fixed Frame 设置为base_link或odom,否则可能没有数据。这个设置项在 RViz2 左上角的 Global Options 里。
到这里,仿真环境已经搭建完成。接下来实现 SLAM 建图。
5. SLAM 建图:让机器人构建环境地图
机器人在未知环境中移动,通过激光雷达扫描周围环境,逐步构建出环境地图。这个过程就是 SLAM。
5.1 为什么要用 SLAM Toolbox
ROS2 中建图方案比较多,主流的有:
- Cartographer:谷歌开源方案,精度高,但配置复杂,特别依赖调参;
- SLAM Toolbox:专为 ROS2 设计的异步 SLAM 库,配置简洁,适合入门和学习;
- 基于 GMapping 的移植版本:ROS1 时代常用,ROS2 中整洁度不高。
从学习成本角度,推荐使用 SLAM Toolbox。它支持在线建图,也能在导航时提供持续性地图匹配。
5.2 启动仿真环境
先把小车放置在 Gazebo 世界环境中。前面已经启动过一次 TurtleBot3 世界,如果关掉了,重新启动即可:
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py5.3 启动 SLAM Toolbox 在线建图
新建一个终端,启动 SLAM Toolbox 节点:
ros2 run slam_toolbox async_slam_toolbox_node为了让这个节点正确工作,需要给它提供必要的参数:激光雷达话题名称、机器人坐标系、里程计坐标系等。更推荐的做法是写一个 launch 文件。在my_robot/launch/目录下创建slam_launch.py:
# 文件路径:src/my_robot/launch/slam_launch.py import os from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, ExecuteProcess from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node def generate_launch_description(): use_sim_time = LaunchConfiguration('use_sim_time', default='true') return LaunchDescription([ DeclareLaunchArgument( 'use_sim_time', default_value='true', description='Use simulation time if true'), Node( package='slam_toolbox', executable='async_slam_toolbox_node', name='slam_toolbox', output='screen', parameters=[{ 'use_sim_time': use_sim_time, 'base_frame': 'base_link', 'odom_frame': 'odom', 'map_frame': 'map', 'scan_topic': '/scan', 'mode': 'mapping', }], ), ])启动方式改为:
ros2 launch my_robot slam_launch.py注意:launch 文件中的包名必须是my_robot,而且 launch 目录需要在setup.py的data_files中声明,否则ros2 launch可能找不到文件。
修改setup.py,在data_files中添加 launch 目录:
data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), ('share/' + package_name + '/launch', ['launch/slam_launch.py']), ],5.4 保存地图
建图时,手动发布速度指令让小车在地图中缓慢穿行。RViz2 中可以实时观察地图构建情况。
地图构建完成后,执行:
ros2 run nav2_map_server map_saver_cli -f ~/ros2_ws/map这条命令会在~/ros2_ws/目录下生成两个文件:
map.pgm:栅格地图图片;map.yaml:地图描述文件,包含分辨率、原点坐标、占据概率阈值等参数。
查看map.yaml:
image: map.pgm mode: trinary resolution: 0.05 origin: [-4.0, -4.0, 0.0] negate: 0 occupied_thresh: 0.65 free_thresh: 0.25其中resolution表示每个像素对应的实际距离(米),origin表示地图左下角在世界坐标系中的位置。导航时,Nav2 会读取这个文件,把地图加载到代价地图中。
5.5 建图过程中的操作技巧
- 控制小车速度不要过快,建议线速度 0.15 m/s 以下,角速度 0.3 rad/s 以下;
- 遇到回环时(绕一圈回到之前经过的位置),可以停下几秒钟,让 SLAM 算法完成回环检测;
- 地图边缘不需要完全闭合,但主要的墙壁、障碍物边界必须清晰;
- 如果发现地图明显漂移,优先检查 TF 是否正确发布,特别是
odom → base_link的变换。
6. 自主导航:Nav2 完整接入
地图构建完成后,接下来就是重头戏:让机器人在已经构建好的地图中自主导航到目标点。这涉及 Nav2 框架的核心模块:定位(AMCL)、全局路径规划、局部路径规划、代价地图更新等。
6.1 Nav2 的核心模块
Nav2 的架构可以粗略分成三层:
| 模块 | 作用 | 配置项 |
|---|---|---|
| Map Server | 加载静态地图,提供/map话题 | map 文件路径 |
| AMCL | 蒙特卡洛定位,在地图中估计机器人位置 | 粒子数量、激光模型参数 |
| Planner | 全局路径规划,搜索从起点到终点的路径 | planner_plugins |
| Controller | 局部路径规划,沿全局路径行驶并避障 | controller_plugins |
| Behavior Tree | 组织导航行为,决定导航过程中先做什么再做什么 | bt_navigator 配置 |
在 Nav2 的配置中,最核心的是nav2_params.yaml文件。这个文件包含 Nav2 所有模块的参数,是整个导航系统的心脏。
6.2 编写 Nav2 参数文件
在my_robot/config/目录下创建nav2_params.yaml。下面给出一个精简但能运行的参数配置:
# 文件路径:src/my_robot/config/nav2_params.yaml amcl: ros__parameters: use_sim_time: True alpha1: 0.2 alpha2: 0.2 alpha3: 0.2 alpha4: 0.2 alpha5: 0.2 base_frame_id: "base_link" global_frame_id: "map" lambda_short: 0.1 laser_likelihood_max_dist: 2.0 laser_max_range: 100.0 laser_min_range: -1.0 laser_model_type: "likelihood_field" max_beams: 60 max_particles: 2000 min_particles: 200 odom_frame_id: "odom" pf_err: 0.05 pf_z: 0.99 recovery_alpha_fast: 0.0 recovery_alpha_slow: 0.0 resample_interval: 1 robot_model_type: "differential" save_pose_rate: 0.5 sigma_hit: 0.2 tf_broadcast: true transform_tolerance: 1.0 update_min_a: 0.2 update_min_d: 0.25 z_hit: 0.5 z_max: 0.05 z_rand: 0.5 z_short: 0.05 scan_topic: /scan bt_navigator: ros__parameters: use_sim_time: True global_frame: map robot_base_frame: base_link odom_topic: /odom bt_loop_duration: 10 default_bt_xml_filename: "navigate_to_pose_w_replanning_and_recovery.xml" plugin_lib_names: - nav2_compute_path_to_pose_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_assisted_teleop_action_bt_node - nav2_drive_on_heading_bt_node - nav2_clear_costmap_service_bt_node - nav2_is_stuck_condition_bt_node - nav2_goal_reached_condition_bt_node - nav2_goal_updated_condition_bt_node - nav2_initial_pose_received_condition_bt_node - nav2_reinitialize_global_localization_service_bt_node - nav2_rate_controller_bt_node - nav2_distance_controller_bt_node - nav2_speed_controller_bt_node - nav2_truncate_path_action_bt_node - nav2_goal_updater_node_bt_node - nav2_recovery_node_bt_node - nav2_pipeline_sequence_bt_node - nav2_round_robin_node_bt_node - nav2_transform_available_condition_bt_node - nav2_time_expired_condition_bt_node - nav2_distance_traveled_condition_bt_node controller_server: ros__parameters: use_sim_time: True controller_frequency: 20.0 min_x_velocity_threshold: 0.001 min_y_velocity_threshold: 0.001 min_theta_velocity_threshold: 0.001 progress_checker_plugins: ["progress_checker"] goal_checker_plugins: ["general_goal_checker"] controller_plugins: ["FollowPath"] progress_checker: plugin: "nav2_controller::SimpleProgressChecker" required_movement_radius: 0.5 movement_time_allowance: 10.0 general_goal_checker: stateful: True xy_goal_tolerance: 0.25 yaw_goal_tolerance: 0.25 FollowPath: plugin: "nav2_rotation_shim_controller::RotationShimController" primary_controller: "dwb_core::DWBLocalPlanner" angular_dist_threshold: 0.785 forward_sampling_distance: 0.5 rotate_to_heading_angular_vel: 1.8 transform_tolerance: 0.1 # DWB 参数 min_vel_x: 0.0 min_vel_y: 0.0 max_vel_x: 0.22 max_vel_y: 0.0 max_vel_theta: 1.0 min_speed_xy: 0.0 max_speed_xy: 0.22 min_speed_theta: 0.0 # 采样参数 sim_time: 1.7 vx_samples: 20 vy_samples: 5 vtheta_samples: 20 path_distance_bias: 32.0 goal_distance_bias: 24.0 occdist_scale: 0.01 forward_point_distance: 0.325 stop_on_obstacle: false scaling_dist: 0.3 max_scaling_factor: 0.2 publish_trajectory_cloud: true local_costmap: local_costmap: ros__parameters: use_sim_time: True robot_base_frame: base_link global_frame: odom rolling_window: true width: 3 height: 3 resolution: 0.05 footprint: "[[-0.16, -0.16], [-0.16, 0.16], [0.16, 0.16], [0.16, -0.16]]" plugins: ["voxel_layer", "inflation_layer"] inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" cost_scaling_factor: 3.0 inflation_radius: 0.55 voxel_layer: plugin: "nav2_costmap_2d::VoxelLayer" enabled: True publish_voxel_map: True origin_z: 0.0 z_resolution: 0.2 z_voxels: 16 max_obstacle_height: 2.0 mark_threshold: 0 observation_sources: scan scan: topic: /scan max_obstacle_height: 2.0 clearing: True marking: True data_type: "LaserScan" raytrace_max_range: 3.0 raytrace_min_range: 0.1 obstacle_max_range: 2.5 obstacle_min_range: 0.1 global_costmap: global_costmap: ros__parameters: use_sim_time: True robot_base_frame: base_link global_frame: map robot_radius: 0.2 resolution: 0.05 track_unknown_space: true plugins: ["static_layer", "inflation_layer"] static_layer: plugin: "nav2_costmap_2d::StaticLayer" map_subscribe_transient_local: True inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" cost_scaling_factor: 3.0 inflation_radius: 0.55 map_server: ros__parameters: use_sim_time: True yaml_filename: "/home/your_name/ros2_ws/map/map.yaml"这个文件看起来很长,但真正需要关注的就是几个关键点:
controller_server中的max_vel_x和max_vel_theta控制小车最大行驶速度和旋转速度;local_costmap和global_costmap中的inflation_radius控制膨胀半径,值越大,路径离障碍物越远;map_server中的yaml_filename必须改成你自己的地图路径。
6.3 编写导航启动文件
在my_robot/launch/目录下创建navigation_launch.py,一次性启动 Nav2 的各个模块:
# 文件路径:src/my_robot/launch/navigation_launch.py import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node def generate_launch_description(): use_sim_time = LaunchConfiguration('use_sim_time', default='true') map_yaml_path = LaunchConfiguration('map_yaml_path', default=os.path.join( os.path.expanduser('~'), 'ros2_ws', 'map', 'map.yaml')) nav2_params_file = os.path.join( get_package_share_directory('my_robot'), 'config', 'nav2_params.yaml') return LaunchDescription([ DeclareLaunchArgument( 'map_yaml_path', default_value=map_yaml_path, description='Full path to map yaml file'), # 地图服务 Node( package='nav2_map_server', executable='map_server', name='map_server', output='screen', parameters=[{'use_sim_time': use_sim_time, 'yaml_filename': map_yaml_path}], ), # AMCL 定位 Node( package='nav2_amcl', executable='amcl', name='amcl', output='screen', parameters=[os.path.join(get_package_share_directory('my_robot'), 'config', 'nav2_params.yaml')], ), # 生命周期管理 Node( package='nav2_lifecycle_manager', executable='lifecycle_manager', name='lifecycle_manager_navigation', output='screen', parameters=[{'use_sim_time': use_sim_time, 'autostart': True, 'node_names': ['map_server', 'amcl', 'controller_server', 'planner_server', 'bt_navigator']}], ), ])注意:这段代码使用了nav2_params.yaml中多个节点的共享参数,启动时 AMCL 等节点会从同一个 yaml 文件中读取各自命名空间下的参数。因此nav2_params.yaml中需要包含planner_server和bt_navigator等节点的配置,前面给出的配置已覆盖大部分核心参数,实际使用时可结合官方参数文件补全。
还需要把config目录在setup.py中声明:
data_files=[ ... ('share/' + package_name + '/config', ['config/nav2_params.yaml']), ('share/' + package_name + '/launch', ['launch/slam_launch.py', 'launch/navigation_launch.py']), ],6.4 启动自主导航
导航必须在 Gazebo 仿真和 SLAM 建图完成之后进行。如果之前已经退出,按顺序重新执行:
终端 1 —— 启动仿真环境:
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py终端 2 —— 启动导航:
ros2 launch my_robot navigation_launch.py终端 3 —— 打开 RViz2:
ros2 run rviz2 rviz2在 RViz2 中:
- 把 Fixed Frame 设置为
map; - 添加 RobotModel、Map、LaserScan、TF、Path、ParticleCloud 等显示项;
- 点击顶部菜单栏的
2D Pose Estimate,在地图中点击并拖动,手动给机器人一个初始位置估计。这一步非常重要,如果初始位置给得不准,后续导航定位会失败; - 点击
Nav2 Goal,在地图上选择一个目标点,Nav2 会开始规划路径并控制小车移动。
6.5 导航过程中的关键技术点
- AMCL 定位粒子收敛后(RViz2 中的绿色箭头会聚集到机器人真实位置附近),说明定位成功;
- 如果机器人导航方向不对,可能是初始位姿给反了,重新用
2D Pose Estimate校正; - 导航过程中遇到动态障碍物,局部代价地图会实时更新,机器人会规划局部避障路径;
- 目标点设置超出地图范围时,导航会失败,需要在 RViz2 中把目标点设置在地图内部。
7. 导航参数调优:从“能跑”到“跑得好”
很多同学完成上面的步骤后,发现机器人虽然能导航,但路径很绕、速度很慢,或者在窄通道里反复横跳。这些都是参数问题。
7.1 速度参数调节
在nav2_params.yaml的controller_server中:
FollowPath: min_vel_x: 0.0 max_vel_x: 0.22 max_vel_theta: 1.0如果觉得机器人太慢,可以适当调大max_vel_x;如果觉得转弯太猛,可以调小max_vel_theta。
7.2 膨胀半径调节
inflation_radius越大,机器人离障碍物越远,路径越保守;inflation_radius越小,机器人越容易贴近障碍物穿过窄缝,但碰撞风险增大。
TurtleBot3 这类小车,建议inflation_radius设置为 0.2 ~ 0.55 之间。如果你自己的机器人宽度更大,需要按比例调整。
7.3 代价地图的 footprint 设置
footprint参数描述机器人在地图上的轮廓,单位是米。如果设置得比实际模型小,机器人可能会在仿真中穿过墙壁;设置得过大,机器人在窄通道中会明显感觉无法通行。
TurtleBot3 Burger 使用:
footprint: "[[-0.16, -0.16], [-0.16, 0.16], [0.16, 0.16], [0.16, -0.16]]"这表示一个边长 0.32 米的正方形轮廓。
7.4 定位失败时的恢复策略
如果 AMCL 粒子发散(RViz2 中粒子非常分散),说明定位丢失。解决方式:
- 停止导航,重新用
2D Pose Estimate手动指定初始位置; - 调大
max_particles,例如改为 3000,提高定位精度; - 调小
update_min_a和update_min_d,让定位更新频率更高,但计算量也会增加。
8. 常见问题与排查思路
下面整理 ROS2 + Gazebo + SLAM + Nav2 常见问题,做一个排查表格。遇到问题时,可以在命令行中配合ros2 topic list、ros2 topic echo、ros2 node list等工具逐步定位。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
ros2 launch找不到 launch 文件 | launch 目录未在setup.py中声明 | 在data_files中增加 launch 路径后重新colcon build |
| Gazebo 启动后无小车模型 | 未设置TURTLEBOT3_MODEL环境变量 | 执行export TURTLEBOT3_MODEL=burger |
| RViz2 中 LaserScan 无数据 | Fixed Frame 设置错误或/scan话题未发布 | 将 Fixed Frame 改为base_link,执行ros2 topic list检查/scan |
| 建图时地图漂移严重 | 里程计 TF 不准或小车速度过快 | 降低车速,检查odom → base_link变换 |
| 导航启动后机器人不动 | AMCL 定位未收敛或初始位姿未设置 | 在 RViz2 中手动指定初始位姿,观察粒子分布 |
| 导航规划失败 | 代价地图膨胀半径过大或地图未正确加载 | 检查map_server的 yaml 路径,调小inflation_radius |
| 导航到目标点后停不准 | xy_goal_tolerance过大 | 将xy_goal_tolerance调小,例如 0.1 |
| 编译功能包报依赖错误 | 缺少对应依赖安装 | 执行sudo apt install ros-humble-<package> |
| 终端中文乱码或 ROS2 环境变量失效 | ~/.bashrc中 source 冲突 | 注释旧版 ROS 的 source 行,保留 ROS2 的 source |
排查问题时,可以按以下顺序检查:
ros2 node list:确认所有节点是否启动;ros2 topic list:确认话题是否正常发布;ros2 topic echo /scan:确认激光雷达数据是否正常;ros2 topic echo /odom:确认里程计是否发布;ros2 run tf2_tools view_frames:生成 TF 树,检查坐标系是否完整。
9. 最佳实践与毕设复用建议
这套 ROS2 仿真 + SLAM + 导航技术栈,不仅适合学习,也非常适合作为毕业设计的技术底座。下面给一些实用建议。
9.1 毕设题目如何从这套流程中落地
如果你准备把 ROS2 导航作为毕设题目,可以考虑以下几个方向:
- 基于 Gazebo 的移动机器人导航仿真平台设计;
- 基于 ROS2 的室内自主导航系统设计与实现;
- 融合激光雷达和里程计的多传感器建图算法研究;
- 面向仓储场景的 ROS2 自主搬运机器人仿真;
- 基于 Nav2 的差速机器人动态避障策略研究。
省去找一台昂贵实体机器人、调试驱动、处理电池续航等硬件问题的时间,把核心精力放在算法和系统设计上,这是仿真方案在毕设中的最大优势。
9.2 工程化建议
- 版本锁定:在文档中记录你使用的 Ubuntu 版本、ROS2 发行版、Gazebo 版本、Nav2 版本,方便复现;
- 模型驱动:启动仿真、建图、导航,尽量都用 launch 文件完成,不要每次都敲一长串命令;
- 参数配置分离:所有调优参数放在
config/目录下的 yaml 文件中,不要硬编码在节点代码里; - 地图管理:建图完成的地图文件建议单独备份,并命名包含日期或版本,例如
map_20260101.yaml; - 日志记录:在节点代码中用
self.get_logger().info()输出关键信息,方便定位问题; - 适度使用 rosdep:在新环境编译功能包前,执行
rosdep install --from-paths src --ignore-src -r -y检查依赖。
9.3 进一步学习路径
完成本文的全流程后,下一步可以围绕这些方向继续深入:
- Cartographer 调参:学习栅格建图、子图匹配、回环检测原理;
- 代价地图详解:深入理解 StaticLayer、ObstacleLayer、InflationLayer 的代价计算方式;
- DWB 局部规划器:调整采样参数,理解轨迹评价函数;
- 行为树:学习如何自定义导航行为树 XML,加入自定义节点;
- 多传感器融合:在仿真中加入摄像头、IMU,结合 Kalman 滤波或 EKF 进行位姿估计;
- 真机迁移:将 Nav2 参数迁移到真实底盘,重点校准里程计和激光雷达外参。
9.4 一个可以立刻上手的改进任务
如果你觉得当前导航效果还不够理想,可以尝试做一个简单的改进任务:在 Gazebo 世界中添加一个移动障碍物。
在 Gazebo 中直接插入一个小的立方体或球体,然后重新启动导航,观察机器人遇到动态障碍物时是否会停下来、重新规划路径。通过这个实验,你能直观理解局部代价地图的更新机制,也能在答辩时展示“动态避障”相关的结果。
10. 写在最后
这篇文章从 ROS2 环境搭建开始,完成了工作空间创建、功能包编写、Gazebo 仿真、SLAM 建图、Nav2 自主导航的全链路实践。你可以把整套流程当作一个“最小可复用的机器人项目模板”:换掉 URDF 模型、换掉地图、调一组参数,就能适配不同的机器人平台。
最后提醒一点:ROS2 版本迭代较快,Gazebo 和 Nav2 的参数也在持续更新。如果你使用的不是 Humble 版本,或者 Ubuntu 版本不同,个别命令和参数文件需要按官方文档调整。遇到问题时,善用ros2 topic list、ros2 node list、ros2 doctor这些基础工具,再结合日志信息定位,远比盲目重装环境高效。
如果你在实操过程中遇到问题,欢迎在评论区留言,带上终端报错信息和版本号,我也会尽量帮忙排查。觉得这篇文章有用的话,可以收藏备用,后续我会继续更新 Cartographer 调参、代价地图原理、自定义机器人建模等进阶内容。