在这里插入图片描述
从2023年开始,ROS2已经全面取代ROS1成为机器人开发的主流框架。无论是工业机器人、服务机器人还是自动驾驶领域,ROS2的分布式架构、实时性保障和跨平台支持都展现出了巨大的优势。然而,很多初学者在学习ROS2时都会遇到同样的问题:官方文档过于理论化,网上的教程大多零散不成体系,代码过时无法运行,缺少从基础到项目的完整学习路径。

我在过去两年里带领团队完成了多个基于ROS2的机器人项目,从最初的简单小车到复杂的工业巡检机器人,踩过的坑不计其数。本文将从最基础的节点通信讲起,一步步带你掌握ROS2的核心技术,并通过一个完整的SLAM建图项目,让你真正具备独立开发ROS2机器人的能力。所有代码都基于ROS2 Humble版本编写,经过实际测试,确保你能直接复制运行。

一、ROS2核心架构与基础概念

1.1 ROS2系统架构

ROS2采用了基于DDS(Data Distribution Service)的分布式通信架构,彻底解决了ROS1中存在的单点故障问题。与ROS1的主从架构不同,ROS2中的所有节点都是对等的,它们通过DDS中间件直接进行通信,不需要中央节点的协调。

发布/订阅

发布/订阅

发布/订阅

发布/订阅

DDS中间件

节点1

节点2

节点3

节点4

这种架构带来了几个显著的优势:

  • 高可靠性:没有中央节点,单个节点故障不会影响整个系统
  • 实时性:DDS提供了多种QoS策略,可以满足不同实时性要求
  • 分布式:节点可以运行在不同的计算机上,自动发现和通信
  • 跨平台:支持Linux、Windows、macOS和嵌入式系统

1.2 核心概念详解

节点(Node)

节点是ROS2系统中最基本的执行单元,每个节点负责一个特定的功能。一个完整的机器人系统通常由多个节点组成,例如:

  • 激光雷达驱动节点
  • 相机驱动节点
  • 电机控制节点
  • SLAM建图节点
  • 导航规划节点

每个节点都有一个唯一的名称,用于在系统中标识自己。节点可以通过发布话题、提供服务或执行动作来与其他节点进行通信。

话题(Topic)

话题是ROS2中最常用的通信方式,采用发布-订阅模式。一个节点可以向某个话题发布消息,其他节点可以订阅这个话题来接收消息。发布者和订阅者之间是解耦的,它们不需要知道对方的存在。

订阅者节点2 订阅者节点1 DDS中间件 发布者节点 订阅者节点2 订阅者节点1 DDS中间件 发布者节点 发布消息到话题/chatter 转发消息 转发消息
服务(Service)

服务是一种请求-响应模式的通信方式,适用于需要立即得到结果的场景。一个节点提供服务,其他节点向该服务发送请求并等待响应。

动作(Action)

动作是一种更复杂的通信方式,适用于需要长时间执行的任务。动作包含三个部分:目标、反馈和结果。客户端向服务器发送目标,服务器在执行过程中不断向客户端发送反馈,执行完成后返回最终结果。

QoS(服务质量)

QoS是ROS2的一个重要特性,它允许你为通信配置不同的服务质量策略,包括:

  • 可靠性(Reliability):可靠传输或尽力而为传输
  • 耐久性(Durability):是否保留历史消息
  • 历史(History):保留多少条历史消息
  • 期限(Deadline):消息的有效期限

二、环境搭建与工作空间管理

2.1 系统要求与ROS2安装

重要提醒:强烈推荐使用Ubuntu 22.04 LTS 64位系统! 这是ROS2 Humble的官方支持系统,兼容性最好,问题最少。

  1. 设置系统语言环境
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
  1. 添加ROS2软件源
sudo apt install curl gnupg2 lsb-release
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] https://mirrors.tuna.tsinghua.edu.cn/ros2/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
  1. 安装ROS2 Humble桌面版
sudo apt update
sudo apt install ros-humble-desktop
  1. 配置环境变量
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
echo "source /usr/share/colcon_argcomplete/hook/colcon-argcomplete.bash" >> ~/.bashrc
source ~/.bashrc
  1. 验证安装
ros2 version
# 输出应该是:ros-humble 0.14.0

2.2 工作空间创建与使用

ROS2使用colcon作为构建系统,工作空间是存放ROS2项目的目录。

  1. 创建工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
  1. 构建工作空间
colcon build
  1. 配置工作空间环境变量
echo "source ~/ros2_ws/install/setup.bash" >> ~/.bashrc
source ~/.bashrc
  1. 创建功能包
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake my_robot_cpp
ros2 pkg create --build-type ament_python my_robot_py

三、节点通信与话题订阅实战

3.1 编写C++发布者节点

创建~/ros2_ws/src/my_robot_cpp/src/publisher_node.cpp

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

class MinimalPublisher : public rclcpp::Node
{
public:
    MinimalPublisher()
    : Node("minimal_publisher"), count_(0)
    {
        publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
        timer_ = this->create_wall_timer(
            500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }

private:
    void timer_callback()
    {
        auto message = std_msgs::msg::String();
        message.data = "Hello, ROS2! " + std::to_string(count_++);
        RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
        publisher_->publish(message);
    }
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    size_t count_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<MinimalPublisher>());
    rclcpp::shutdown();
    return 0;
}

3.2 编写C++订阅者节点

创建~/ros2_ws/src/my_robot_cpp/src/subscriber_node.cpp

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
public:
    MinimalSubscriber()
    : Node("minimal_subscriber")
    {
        subscription_ = this->create_subscription<std_msgs::msg::String>(
            "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

private:
    void topic_callback(const std_msgs::msg::String & msg) const
    {
        RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
    }
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<MinimalSubscriber>());
    rclcpp::shutdown();
    return 0;
}

3.3 配置CMakeLists.txt

编辑~/ros2_ws/src/my_robot_cpp/CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(my_robot_cpp)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(publisher_node src/publisher_node.cpp)
ament_target_dependencies(publisher_node rclcpp std_msgs)

add_executable(subscriber_node src/subscriber_node.cpp)
ament_target_dependencies(subscriber_node rclcpp std_msgs)

install(TARGETS
  publisher_node
  subscriber_node
  DESTINATION lib/${PROJECT_NAME})

ament_package()

3.4 编译并运行

cd ~/ros2_ws
colcon build --packages-select my_robot_cpp
source install/setup.bash

# 终端1:运行发布者
ros2 run my_robot_cpp publisher_node

# 终端2:运行订阅者
ros2 run my_robot_cpp subscriber_node

3.5 QoS配置详解

QoS是ROS2通信中最容易被忽略但又非常重要的部分。很多初学者遇到的"节点运行正常但收不到消息"的问题,都是因为QoS配置不兼容导致的。

// 创建自定义QoS配置
rclcpp::QoS qos_profile(10);
qos_profile.reliability(rclcpp::ReliabilityPolicy::RELIABLE);
qos_profile.durability(rclcpp::DurabilityPolicy::VOLATILE);
qos_profile.history(rclcpp::HistoryPolicy::KEEP_LAST);

// 使用自定义QoS创建发布者
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", qos_profile);

// 使用相同的QoS创建订阅者
subscription_ = this->create_subscription<std_msgs::msg::String>(
    "topic", qos_profile, std::bind(&MinimalSubscriber::topic_callback, this, _1));

常用QoS策略对比

策略 适用场景 特点
RELIABLE 重要数据传输 确保消息可靠到达,重传丢失的消息
BEST_EFFORT 传感器数据 尽力而为传输,不重传丢失的消息,延迟低
TRANSIENT_LOCAL 地图、配置信息 发布者保留历史消息,新订阅者可以收到
VOLATILE 实时数据 不保留历史消息,新订阅者只能收到之后的消息

四、服务与动作通信

4.1 服务通信示例

创建服务定义文件~/ros2_ws/src/my_robot_cpp/srv/AddTwoInts.srv

int64 a
int64 b
---
int64 sum

编写服务端节点:

#include "rclcpp/rclcpp.hpp"
#include "my_robot_cpp/srv/add_two_ints.hpp"

using namespace std::placeholders;

class AddTwoIntsServer : public rclcpp::Node
{
public:
    AddTwoIntsServer()
    : Node("add_two_ints_server")
    {
        service_ = this->create_service<my_robot_cpp::srv::AddTwoInts>(
            "add_two_ints", std::bind(&AddTwoIntsServer::handle_service, this, _1, _2));
        RCLCPP_INFO(this->get_logger(), "AddTwoInts server ready");
    }

private:
    void handle_service(
        const std::shared_ptr<my_robot_cpp::srv::AddTwoInts::Request> request,
        std::shared_ptr<my_robot_cpp::srv::AddTwoInts::Response> response)
    {
        response->sum = request->a + request->b;
        RCLCPP_INFO(this->get_logger(), "Incoming request: a=%ld, b=%ld", request->a, request->b);
        RCLCPP_INFO(this->get_logger(), "Sending response: sum=%ld", response->sum);
    }
    rclcpp::Service<my_robot_cpp::srv::AddTwoInts>::SharedPtr service_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<AddTwoIntsServer>());
    rclcpp::shutdown();
    return 0;
}

4.2 动作通信示例

动作通信适用于需要长时间执行的任务,比如机器人移动到目标位置。它允许客户端在执行过程中取消任务,并接收实时反馈。

五、SLAM项目实战:从仿真到建图

5.1 安装依赖包

sudo apt install -y ros-humble-gazebo-ros \
ros-humble-gazebo-ros-pkgs \
ros-humble-robot-state-publisher \
ros-humble-joint-state-publisher \
ros-humble-joint-state-publisher-gui \
ros-humble-rviz2 \
ros-humble-slam-toolbox \
ros-humble-navigation2 \
ros-humble-nav2-bringup \
ros-humble-turtlebot3-gazebo

5.2 机器人URDF建模

创建~/ros2_ws/src/my_robot/urdf/my_robot.urdf

<?xml version="1.0"?>
<robot name="my_robot">
  <!-- 底盘 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.2 0.15 0.05"/>
      </geometry>
      <material name="blue">
        <color rgba="0 0 1 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.2 0.15 0.05"/>
      </geometry>
    </collision>
  </link>

  <!-- 左轮 -->
  <link name="left_wheel">
    <visual>
      <geometry>
        <cylinder length="0.02" radius="0.035"/>
      </geometry>
      <material name="black">
        <color rgba="0 0 0 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder length="0.02" radius="0.035"/>
      </geometry>
    </collision>
  </link>

  <!-- 右轮 -->
  <link name="right_wheel">
    <visual>
      <geometry>
        <cylinder length="0.02" radius="0.035"/>
      </geometry>
      <material name="black">
        <color rgba="0 0 0 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder length="0.02" radius="0.035"/>
      </geometry>
    </collision>
  </link>

  <!-- 激光雷达 -->
  <link name="laser_link">
    <visual>
      <geometry>
        <cylinder length="0.05" radius="0.05"/>
      </geometry>
      <material name="red">
        <color rgba="1 0 0 1"/>
      </material>
    </visual>
  </link>

  <!-- 关节定义 -->
  <joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel"/>
    <origin xyz="0 0.075 0" rpy="1.5707 0 0"/>
    <axis xyz="0 1 0"/>
  </joint>

  <joint name="right_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="right_wheel"/>
    <origin xyz="0 -0.075 0" rpy="1.5707 0 0"/>
    <axis xyz="0 1 0"/>
  </joint>

  <joint name="laser_joint" type="fixed">
    <parent link="base_link"/>
    <child link="laser_link"/>
    <origin xyz="0 0 0.05" rpy="0 0 0"/>
  </joint>
</robot>

5.3 Gazebo仿真环境搭建

创建启动文件~/ros2_ws/src/my_robot/launch/simulation.launch.py

import os
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    urdf_file = os.path.join(
        get_package_share_directory('my_robot'),
        'urdf',
        'my_robot.urdf'
    )
    
    with open(urdf_file, 'r') as infp:
        robot_desc = infp.read()
    
    return LaunchDescription([
        # 启动Gazebo
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource([
                os.path.join(get_package_share_directory('gazebo_ros'), 'launch', 'gazebo.launch.py')
            ]),
            launch_arguments={'world': os.path.join(
                get_package_share_directory('turtlebot3_gazebo'),
                'worlds',
                'turtlebot3_world.world'
            )}.items()
        ),
        
        # 发布机器人状态
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            name='robot_state_publisher',
            output='screen',
            parameters=[{'robot_description': robot_desc, 'use_sim_time': True}]
        ),
        
        # 在Gazebo中生成机器人
        Node(
            package='gazebo_ros',
            executable='spawn_entity.py',
            arguments=['-topic', 'robot_description', '-entity', 'my_robot'],
            output='screen'
        ),
        
        # 启动关节状态发布器
        Node(
            package='joint_state_publisher',
            executable='joint_state_publisher',
            name='joint_state_publisher',
            output='screen'
        )
    ])

5.4 SLAM建图实战

创建SLAM启动文件~/ros2_ws/src/my_robot/launch/slam.launch.py

import os
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    slam_params_file = os.path.join(
        get_package_share_directory('my_robot'),
        'config',
        'slam_params.yaml'
    )
    
    return LaunchDescription([
        # 启动仿真环境
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource([
                os.path.join(get_package_share_directory('my_robot'), 'launch', 'simulation.launch.py')
            ])
        ),
        
        # 启动SLAM Toolbox
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource([
                os.path.join(get_package_share_directory('slam_toolbox'), 'launch', 'online_async_launch.py')
            ]),
            launch_arguments={'params_file': slam_params_file, 'use_sim_time': 'True'}.items()
        ),
        
        # 启动RViz
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource([
                os.path.join(get_package_share_directory('nav2_bringup'), 'launch', 'rviz_launch.py')
            ]),
            launch_arguments={'rviz_config': os.path.join(
                get_package_share_directory('nav2_bringup'),
                'rviz',
                'nav2_default_view.rviz'
            )}.items()
        )
    ])

创建SLAM参数文件~/ros2_ws/src/my_robot/config/slam_params.yaml

slam_toolbox:
  ros__parameters:
    solver_plugin: solver_plugins::CeresSolver
    ceres_linear_solver: SPARSE_NORMAL_CHOLESKY
    ceres_preconditioner: SCHUR_JACOBI
    ceres_trust_strategy: LEVENBERG_MARQUARDT
    ceres_dogleg_type: TRADITIONAL_DOGLEG
    ceres_loss_function: None

    odom_frame: odom
    map_frame: map
    base_frame: base_link
    scan_topic: /scan
    use_map_saver: true
    mode: mapping

    resolution: 0.05
    max_laser_range: 3.5
    minimum_travel_distance: 0.1
    minimum_travel_heading: 0.1
    scan_buffer_size: 10
    scan_buffer_maximum_scan_distance: 3.0
    link_match_minimum_response_fine: 0.1
    link_scan_maximum_distance: 1.5
    loop_search_maximum_distance: 3.0
    do_loop_closing: true
    loop_match_minimum_chain_size: 10
    loop_match_maximum_variance_coarse: 3.0
    loop_match_minimum_response_coarse: 0.35
    loop_match_minimum_response_fine: 0.45

5.5 启动建图流程

# 编译工作空间
cd ~/ros2_ws
colcon build
source install/setup.bash

# 启动SLAM建图
ros2 launch my_robot slam.launch.py

# 新终端:启动键盘控制
ros2 run teleop_twist_keyboard teleop_twist_keyboard

使用键盘控制机器人在环境中缓慢移动,确保覆盖所有区域。建图完成后,保存地图:

ros2 run nav2_map_server map_saver_cli -f ~/my_map

六、常见问题与踩坑总结

6.1 环境配置问题

问题1:rosdep init失败
解决方案

sudo rosdep init
rosdep update

如果还是失败,可以使用国内镜像源:

sudo sed -i 's|https://raw.githubusercontent.com/ros/rosdistro/master|https://mirrors.tuna.tsinghua.edu.cn/github-raw/ros/rosdistro/master|g' /etc/ros/rosdep/sources.list.d/20-default.list
rosdep update

问题2:colcon build时内存不足
解决方案

MAKEFLAGS=-j1 colcon build

6.2 节点通信问题

问题1:节点运行正常但收不到消息
解决方案

  1. 检查话题名称是否一致
  2. 检查消息类型是否一致
  3. 检查QoS配置是否兼容
  4. 检查ROS_DOMAIN_ID是否相同

问题2:多机通信失败
解决方案

  1. 确保两台计算机在同一网络
  2. 确保防火墙允许UDP通信
  3. 设置相同的ROS_DOMAIN_ID
  4. 检查/etc/hosts文件配置

6.3 SLAM建图问题

问题1:建图漂移严重
解决方案

  1. 降低机器人移动速度,建议不超过0.2m/s
  2. 增加激光雷达扫描频率
  3. 校准里程计参数
  4. 启用回环检测

问题2:地图中有很多噪点
解决方案

  1. 调整激光雷达最小和最大测距范围
  2. 增加scan_buffer_size参数
  3. 启用激光雷达角度补偿

七、总结与展望

本文从ROS2的基础架构讲起,详细介绍了节点、话题、服务、动作等核心概念,并通过完整的代码示例演示了如何实现节点通信。然后,我们通过一个SLAM建图项目,从URDF建模、Gazebo仿真到最终的地图保存,一步步带你掌握了ROS2机器人开发的完整流程。

通过本文的学习,你应该已经具备了以下能力:

  • 独立搭建ROS2开发环境
  • 编写发布者和订阅者节点
  • 配置QoS策略
  • 进行机器人URDF建模
  • 在Gazebo中进行仿真
  • 使用SLAM Toolbox进行建图

下一步学习建议

  1. 深入学习Nav2导航系统,实现自主避障和路径规划
  2. 学习ROS2参数服务器和动态参数配置
  3. 学习TF坐标变换系统
  4. 尝试在实际硬件上部署ROS2系统
  5. 学习ROS2的高级特性,如组件、生命周期节点等

ROS2是一个非常庞大和复杂的系统,本文只是一个入门指南。要真正精通ROS2,还需要在实际项目中不断学习和积累经验。希望本文能为你的ROS2学习之路提供一些帮助。

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐