ROS 2 自主移动机器人简明教程
本指南基于机器人学的基础概念(如 Bug1 和 Bug2 规划方法),通过一个演示性的 2D 示例,全面而易懂地描述了移动机器人技术。
AI模型价格对比 | AI工具导航 | ONNX模型库 | Vibe Coding教程 | PLC在线仿真器 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo
对于任何在现实世界中自主运行的物理智能移动机器人来说,成功的导航是一项关键能力。这一主题的需求也与各行各业高度相关,包括物流、自动驾驶和搜救应用。此外,高级机器人任务(如物体操作)通常依赖于精确导航提供的基础稳定性。虽然具体的解决方案必须适应环境参数(如室内/室外或已知/未知地图),但所有有效的系统都从根本上依赖于强大的路径规划。
本指南基于机器人学的基础概念(如 Bug1 和 Bug2 规划方法),通过一个演示性的 2D 示例,全面而易懂地描述了移动机器人技术。我们将使用流行的 ROS2 框架和 C++,探索在模拟 2D 环境中管理基本轮式机器人导航以到达目标并避开障碍物所需的核心思想,同时解释理论和代码。
1、引言

自主移动机器人(AMR)在许多领域都有广泛的应用,这也是为什么它是机器人学重要组成部分的原因。目前,许多初创公司正在研究在以下领域使用 AMR 的解决方案:
- 物流:在仓库中使用机器人搬运货物已经在许多地方实现。
- 农业:控制和监测农田,甚至可能进行收割也已得到应用。
- 家务:自主吸尘器已经在商业上可用。
本例中的任务看起来相当简单:导航到已知目标,同时避开障碍物。如下图 3 所示。然而,一旦我们开始以实际应用的方式分析这个任务,就会发现它相当复杂。例如:
- 机器人如何感知环境
- 机器人如何被控制(通过速度、相对或绝对位置)
- 机器人的运动可能性有哪些
- 如何精确规划绕过障碍物的路径
- 如何同步机器人的感知、控制和规划
这些问题也出现在许多其他自主移动机器人(AMR)项目中。在这里,我们将在示例中开始回答其中一些问题,而将其他问题留待后续文章讨论。
一个帮助解决这些常见构建问题的流行工具是机器人操作系统 2(ROS 2)框架。在 ROS2 中,控制、规划和感知部分之间的通信中间件会自动且安全地处理。这为开发者节省了大量时间,更不用说围绕 ROS2 工具和包的巨大社区,在那里你可以找到许多流行机器人算法的实现。
换句话说,ROS 2 是"最广泛采用的"(我个人的自信声明)机器人通用语言。因此,如果你的项目使用 ROS,你可以期待更多的读者(或合作者),或者如果你精通 ROS,你就有能力阅读许多其他项目。也可以说 ROS2 是许多机器人工具(包括硬件驱动程序)的包管理器。
在 ROS2 中,你可以使用多种语言编写代码;然而,C++ 是最流行/高效的选择。如果你曾使用过 Python、Matlab 等解释型语言,你会发现它们开发起来容易得多。然而,将它们用于机器人项目(以及许多其他性能敏感的应用)的问题是它们对底层过程的简化。在机器人学中,你希望明智地使用你的内存/计算能力。即使你的硬件很强大,让程序运行得更快也能让你的机器人更高效。
开源链接该项目的开源代码(将逐步维护)可在 Gitlab 上获取。
2、当前的导航任务
作为移动机器人学中最重要的任务之一,导航是自主移动机器人的主要目的。在安全高效地完成目标的同时,通常也是成功导航的主要标准。
在导航中,输入是传感器数据流,无论是激光雷达扫描、RGB 摄像头帧还是车轮里程数据。然而,输出主要是导航运动命令,这些命令应该在全局层面为整个任务规划,同时也要处理必要的局部变化,如避障。这些规划需要知道机器人位置(定位)和周围环境(建图),然后可以用来定义机器人的未来轨迹。
这些基本上是任何移动机器人的主要任务。然而,确切的设置,包括传感器分辨率、执行器灵活性、计算能力以及手头的实际任务,将决定需要哪些调整和适配来解决这些步骤。
2.1 仿真思路
作为第一个入门演示,我们的目标非常简单:在 2D 模拟环境中定义一个目标,然后导航到该目标。在这里,我们模拟所有的传感器、执行器和环境,全部仅为 2D,如下图所示。
注意这个假设实际上并不过分,因为地面上的移动机器人在平坦水平面上以二维方式运行(除非有楼梯或地面不平),它们很少需要第三个高度维度。

正如你可以在下面的图像中看到的,除了在环境中设置随机目标点(蓝色圆圈)之外,我们还随机散布障碍物(显示为绿色方框)以使任务更具挑战性,并模拟避障任务。

本项目中的图形使用 C++ 中流行的 OpenCV 库。
2.2 自行车模型
定义仿真环境后,定义控制下的确切机器人状态非常重要。这实际上主要取决于我们拥有什么样的执行器,例如我们可以发送下一个点坐标 (x,y) 作为输出命令,但然后它需要使用另一个运动模型转换为实际的车轮旋转速度和方向。由于额外的转换步骤,这可能会引入另一种类型的错误,因此我们这里更喜欢使用速度 v 和车轮相对航向角 γ 作为直接和实际的控制信号。
然而,为了模拟给定前向速度和前轮航向的机器人确切运动,文献提出了一个近似模型,称为"自行车模型",用于类车轮式机器人,我们可以如下图所示,在给定这些信号命令的情况下计算机器人位置和航向的更新。
另一方面,作为一个缺点,这个模型忽略了车轮打滑和摩擦,这也引入了控制机器人的误差。

2.3 路径规划入门
如前所述,AMR 中要解决的任务是三方面的:定位、建图和规划。作为初始教程,这里的重点将仅仅放在规划部分(而将其他两个留待后续文章),换句话说:
我们将假设这里完全了解机器人姿态及其周围环境。剩下的是定义它每一步应该采取的速度和旋转以到达目的地。
这个任务被称为路径规划,在机器人领域有多种解决方法,包括:图搜索、强化学习或力场向量。
下面,我们将快速概览这些类型的方法。这里唯一实现的方法将是 Bug 2 方法:一种经典的基于目标的规划算法。后续文章将包含更多实现。

如上图所示,路径规划中的任务是找到机器人运动轨迹(一系列 x,y 坐标)以到达目的地,同时避免与障碍物碰撞。路径规划方法也期望找到到达目的地的最短路径,或者更一般地说,是最廉价的路径,其中成本可以与避开某些地方而不仅仅是距离相关。

3、编码部分
在对理论方面有了初步了解之后,我们现在开始介绍 ROS 和 C++ 中的实际代码。在接下来的内容中,我们将从头开始实现主要的导航程序(在 2D 模拟器中),利用 Bug 2 规划算法和自行车模型作为演示示例的机器人运动。
首先,我们将提供一个非常简短但全面的 ROS2 介绍、其安装和主要关键概念(更多信息请查看 ros.org 上出色的 ROS 文档)。然后我们将介绍如何使用它来构建和创建项目,以及如何设计其数据接口。
接下来,我们将介绍主程序骨架,这将是 ROS 中的节点图。目前我们将保持其通用性,但肯定存在其他替代方案。之后我们将解释 Bug 2 规划器代码,然后是在多个终端或作为启动文件运行完整项目的方法。
最后但同样重要的是,将展示和分析一个用 C++ 编写的 ROS 节点模板,然后是程序仍然存在的一些失败案例,最后总结本文的主要结论。
3.1 安装 ROS
我们这里使用 ROS2 Jazzy 版本,这是撰写本文时 Ubuntu 24 操作系统的当前维护版本。
对于任何其他 ROS 版本,只需将安装代码中的 <distro> 部分替换为其名称即可安装。
sudo apt install ros-<distro>-desktop
sudo apt install ros-dev-tools
这将在你的计算机上安装 ROS,要测试它,你可以运行一个默认安装的演示程序,使用以下命令:
source /opt/ros/jazzy/setup.bash
ros2 run turtlesim turtlesim_node
每次运行 ROS 命令时都需要执行 source 步骤,因此将其添加到终端的启动脚本(即:.bashrc)中是有意义的
3.2 ROS 核心概念
ROS 被认为是机器人学中的中间件框架,因为它将底层的硬件驱动程序和通信协议与更高层次的传感器和执行器数据处理连接起来。在其主要优势中,我们提到:
- 机器人功能之间更好的通信
- 更好的协作(统一的流行框架)
- 巨大的流行算法开源库
通过查看 ROS 程序,我们注意到它包含以下主要概念:
- 节点:它们代表 ROS 程序的构建基石,每个节点负责一个任务甚至是算法中的一个步骤。
- 主题:通过它们,节点可以在发布者/订阅者方案中相互交换数据:可以有多个订阅者订阅一个发布者,但反过来不行
- 服务:当数据交换被视为在客户端/服务器方案中执行一个任务的命令时,使用服务会更合适
- 动作:当被执行的服务持续较长时间(如执行到达目的地的动作),并且需要定期跟踪此服务的状态时,那么使用动作模板将是 ROS 兼容的选项。
3.3 在 ROS 中创建包
对于 ROS2 中的任何项目,我们都注意到应该遵循特定的文件夹结构。任何 ROS2 项目的这个主要层次结构包含以下级别:
- 工作区级别(属于完整的机器人硬件平台,理论上任何使用它的项目都可以在那里编写)
- 包级别(你正在该特定机器人上进行的完整预期应用)
- 节点级别(为实现应用而承担的确切功能)
将这些级别视为使项目更具可读性的组织结构。
所以在我们的例子中,我们需要一个工作区,可能一个或两个包(仅用于组织目的,因为我们目前只做一个项目),以及多个节点。我们还将仅使用主题作为通信手段,尽管服务和动作也可以在这里使用,我们将把它们留待后续文章。
创建项目目录
现在,我们准备好开始编写项目了。我们从创建空的项目目录开始,如下所示:
$ source opt/ros/jazzy/setup.bash
$ mkdir amr_ws
$ cd src/
$ cd ..
$ colcon build // always at ws level
$ source install/setup.bash
colcon 是 ROS2 中的构建命令(相当于 C++ 中的 CMAKE)。此外,我们看到每次构建后都需要 source 项目的设置脚本,以及主要的 ROS setup.bash 脚本。
然后我们创建包(作为 C++ 包):
$ cd src/
$ ros2 pkg create planners_pkg --build-type ament_cmake --dependencies rclcpp
对于 C++ 项目,我们需要以下构建类型 --build-type ament_cmake 和依赖项 --dependencies rclcpp。
很好,现在我们有了空的包,我们可以创建第一个节点:
$ cd amr_ws/src/planners_pkg/src
$ touch planners.cpp
从这里你可以在 planners.cpp 中编辑节点代码。要使更改生效,你需要再次构建整个包(使用 colcon)并再次 source 项目。
要运行节点,请使用以下命令(可能使用 alternative_name):
$ros2 run planners_pkg planners --ros-args -r __node:=alternative_name
我们可以通过输入 ros2 node list 来检查代码是否正在运行。我们还可以通过输入以下命令来检查特定主题的状态:
$ ros2 node list
$ ros2 node info /topic_name
其他有助于调试和查看主题信息的命令是:
ros2 topic echo /topic_name
ros2 interface show example/interface
ros2 topic pub -r 2.0 /topic_name interface "{key: value}"
第一个命令显示主题消息的内容,第二个显示主题消息(称为接口)的结构,最后一个命令显示如何从命令行发布具有给定名称的新主题,仅用于调试目的。
使用的接口
我们提到了接口,它们是定义主题的不可或缺的部分,即它们定义了将在主题、服务或动作中作为消息发送的数据结构。因此明智地选择它们很重要。ROS 提供了各种用途的接口定义,但在某些情况下可能需要创建新的自定义接口。
在这个项目中,我们主要需要发送以下消息:
- 控制命令:为此我们将使用 Twist 消息。可以通过以下命令查看:
ros2 interface show geometry_msgs/msg/Twist
# This expresses velocity in free space broken into its linear and angular parts.
Vector3 linear
float64 x
float64 y
float64 z
Vector3 angular
float64 x
float64 y
float64 z
- 机器人姿态:由于我们只有 2D 姿态(由 x,y 和 θ 坐标组成),我们将使用 Pose 2D 消息(尽管它已弃用)
ros2 interface show geometry_msgs/msg/Pose2D
# This expresses a position and orientation on a 2D manifold.
float64 x
float64 y
float64 theta
- 环境图像:为此我们将使用图像消息
ros2 interface show sensor_msgs/msg/Image
# This message contains an uncompressed image
# (0, 0) is at top-left corner of image
std_msgs/Header header # Header timestamp should be acquisition time of image
builtin_interfaces/Time stamp
int32 sec
uint32 nanosec
string frame_id
uint32 height # image height, that is, number of rows
uint32 width # image width, that is, number of columns
string encoding # Encoding of pixels
uint8 is_bigendian # is this data bigendian?
uint32 step # Full row length in bytes
uint8[] data # actual matrix data, size is (step * rows)
- 目标坐标:我们将使用 Vec3 接口(其中 z 始终被忽略)
ros2 interface show geometry_msgs/msg/Vector3
# This represents a vector in free space.
float64 x
float64 y
float64 z
还有许多其他内置接口。要查看所有接口并从中选择合适的接口,你可以运行:
$ ros2 interface list
此命令还将显示服务和动作的接口。
C++ 中节点及其发布者/订阅者的主要代码框架
在定义每个节点的输入和输出消息结构之后,我们需要开始在代码中实现逻辑,即通过执行以下 ROS2/C++ 程序步骤:
- 在包的
\src目录中创建节点文件,作为具有.cpp扩展名的文件。如果你的代码很大或需要在其他地方使用,请使用额外的.hpp文件并将其包含在.cpp文件中。 - 给节点文件和节点实例类赋予反映其实际功能的描述性名称,如 Planner/Robot Model 等。
- 编写确切的代码来实现你的逻辑。作为示例,我们下面查看 robot_model 节点的基本模板,以演示功能节点的主要部分:
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/pose2_d.hpp"
#include "geometry_msgs/msg/vector3.hpp"
#include <random>
#include <vector>
#include <sstream>
#include "utils_funs.hpp"
class Robot: public rclcpp::Node
{
public:
Robot(int H, int W, int RobotSize):Node("robot_model") {
std::printf("Robot is created");
sub_ = this->create_subscription<geometry_msgs::msg::Vector3>("robot_commands",10,
std::bind(&Robot::update_model,this,std::placeholders::_1));
pub_ = this->create_publisher<geometry_msgs::msg::Pose2D>("robot_pos",10);
pos = getInitialPos(W,H,RobotSize);
old_pos.assign(pos.begin(), pos.end());
pose_.set__x(pos[0]);
pose_.set__y(pos[1]);
pose_.set__theta(pos[2]);
RCLCPP_INFO(this->get_logger(),"built");
timer_ = this->create_wall_timer(std::chrono::milliseconds(40),
std::bind(&Robot::send_location,this));
}
void update_model(const geometry_msgs::msg::Vector3::SharedPtr message){
// update based on bicycle model
double gamma_rel = message->x*M_PI/180;
double speed = message->y;
double dtheta = (speed*(tan(gamma_rel))/RobotSize);
double dc = speed * std::cos(pos[2]+dtheta);
double dr = -speed * std::sin(pos[2]+dtheta); // y-axis is reversed
old_pos.assign(pos.begin(), pos.end());
std::cout << dr << " " << dc << " " << dtheta << std::endl;
pos[1] = pos[1]+dr;
pos[0] = pos[0]+dc;
pos[2] = pos[2]+dtheta;
std::cout << pos[0] << " " << pos[1] << " " << pos[2] << std::endl;
this->send_location();
}
void send_location(){
this->pose_.set__x(this->pos[0]);
this->pose_.set__y(this->pos[1]);
this->pose_.set__theta(this->pos[2]);
pub_->publish(this->pose_);
}
void reset_pose(){
pos.assign(old_pos.begin(), old_pos.end());
}
private:
geometry_msgs::msg::Pose2D pose_;
rclcpp::Subscription<geometry_msgs::msg::Vector3>::SharedPtr sub_;
rclcpp::Publisher<geometry_msgs::msg::Pose2D>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
std::vector<double> pos;
std::vector<double> old_pos;
int RobotSize = 50;
};
int main(int argc, char **argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<Robot>(1200,1200,50);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
- 按照以下方式更新你的
CMakeLists.txt,以指示它构建新节点:
cmake_minimum_required(VERSION 3.8)
project(planners_pkg)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(nav_msgs REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(cv_bridge REQUIRED)
find_package(image_transport REQUIRED)
find_package(OpenCV REQUIRED )
include_directories( ${OpenCV_INCLUDE_DIRS} )
include_directories( include/${PROJECT_NAME}/ )
# first node: robot
add_executable(robot_model src/robot_model.cpp )
ament_target_dependencies(robot_model rclcpp geometry_msgs )
add_executable(env_model src/env_model.cpp)
ament_target_dependencies(env_model rclcpp geometry_msgs nav_msgs cv_bridge sensor_msgs std_msgs image_transport OpenCV)
add_executable(planner_node src/planners.cpp)
ament_target_dependencies(planner_node rclcpp geometry_msgs sensor_msgs std_msgs image_transport)
install(TARGETS
robot_model
env_model
planner_node
DESTINATION lib/${PROJECT_NAME}/)
- 通过运行以下命令构建包:从主工作区文件夹运行
colcon build(默认情况下将构建所有包)或使用--package-select构建一个包,再次 source 并测试新构建:
ros_workspace/$ colcon build --package-select the_needed_package
ros_workspace/$ source install/setup.bash
ros_workspace/$ ros2 run pkg_name node_name
4、绘制节点网络图
下图描述了程序的一般思想,其重要部分(通常是任何真实机器人所需的)是控制逻辑和建图部分,以及传感器和执行器设备的其他硬件驱动程序。由于我们这里也在模拟环境,我们需要节点来表示环境和机器人模型。

在我们的示例中,运行所有节点后,我们可以使用命令 $rqt_graph 查看显示节点及其发布或订阅主题的图,如下图所示。
请注意,除了三个主要节点(机器人模型、环境模型和规划器)之外,我们还在使用另一个名为 image-tools 的内置 ROS 包中的 image_view 节点(默认安装)。此外,我们这里不使用建图节点,因为我们假设了一个具有静态环境完整知识的简单案例。在本项目的未来迭代中,我们将添加建图功能。

还要注意,我们在 planner_node 中使用的只是目标和机器人姿态,以及一个指示是否碰到障碍物的布尔标志。虽然订阅了场景图像主题,但规划器函数实际上并未使用它。
从图中我们注意到,节点之间的同步是必要的,以确保数据的有效性(为特定时间点计算的值也来自该时间点):因此机器人模型节点设置计算频率,而其余节点依赖于接收它们订阅的特定主题消息,这会触发它们的计算周期。
在下一小节中,我们将重点介绍规划器节点逻辑,即其实现 Bug2 方法。
5、Bug 2 算法步骤
为了这个基本示例的目的,我们将从基本的 Bug 算法开始。在 Bug 1 算法中(参见此处参考),步骤很简单,如下所示:
- 定义从机器人位置到目标位置的向量,并引导机器人沿其移动
- 当机器人遇到障碍物时,它会检查其两个方向的边界,然后确定离目标最近的点并向其移动。
- 然后它重新计算方向向量并继续跟随它,直到达到设定的目标邻近区域。
这种方法看起来相当简单,但其主要重要性来自于执行这些步骤的算法逻辑的确切定义。后来这个算法在 Bug 2 中得到了改进,其中障碍物边界的检查(在步骤 2 中)仅从一个方向(左或右)进行,而不是两个方向。这导致更快地绕过障碍物和更短/更便宜的路线。
本文开头显示的 GIF 展示了 Bug 2 思想的演示实现及其路径可能的样子。
在下面的代码部分,我们展示了实现项目路径规划部分的函数。它主要由 3 个步骤组成:
- 找到运动向量:
diffx, diffy, targetT - 找到机器人与该向量之间的旋转差异(需要限制在 π 和 −π 之间):
dirDiff - 最后发送旋转和运动命令,其中:
- 旋转与旋转差异 + 障碍物的存在相关:
act.x。 - 运动速度是恒定的始终向前,除非遇到障碍物,然后机器人向后移动:
act.y。
注意:有些机器人不能向后行驶,所以在我们的情况下,假设它可以
void plan_bug2(const sensor_msgs::msg::Image::SharedPtr message){
// Step 1: find direction vector
double diffx = goal_pos[0] - robot_pos[0];
double diffy = goal_pos[1] - robot_pos[1];
// Step 2: find difference of direction rotation and transfer it to range [-pi,pi]
float targetT = atan2(-diffy,diffx)*180/M_PI;
double dirDiff = (int)(targetT-(robot_pos[2]*180/M_PI))%360;
dirDiff = dirDiff -((180<dirDiff)*360) +((-180>dirDiff)*360);
// Step 3: set rotation speed (with obstacles turn by +25 degree),
// with fixed linear speed (with obstacle)
this->act.x = (dirDiff*0.5)+(hitObst*25);
this->act.y = 10-(hitObst*30);
this->send_command();
}
6、运行完整程序
在为图 6 中显示的所有节点编写代码后,我们可以通过一起启动所有相关节点来运行完整项目,这样它们就开始通过主题和服务交换数据并操作程序的不同功能。
这可以通过两种方式完成:
- 在各自的终端中运行每个节点。我们通过在每个终端中 source 项目然后运行命令来完成:
ros2 run pkg_name node_name --ros-args -r __node:=alternative_name- 将
pkg_name和node_name分别替换为包名和节点名。 - 或者运行一个启动命令,在一个终端中一起启动所有节点。此命令需要启动文件,可以是 python 文件或
.xml文件。
对于我们的源代码,我们使用 .xml 启动文件(为了更好的组织写在自己的包中),如下所示:
<launch>
<node pkg="planners_pkg" exec="robot_model"/>
<node pkg="planners_pkg" exec="env_model"/>
<node pkg="image_tools" exec="showimage"/>
<node pkg="planners_pkg" exec="planner_node"/>
</launch>
这看起来比 python 脚本替代方案简单得多。请注意,showimage 节点需要不同的包(image_tools)。
要运行此文件,命令将是(source 项目后):
$ ros2 launch planners_launch planners.launch.xml
就这么简单。
7、失败案例
虽然许多实验成功结束,机器人在特定半径内到达目标,但有些案例显示机器人无法或难以到达其目标,即:
- 机器人应该向目标点做一个急转弯,但无法执行。这导致围绕目标的圆形路径,永远无法到达,如下图所示

- 通过遵循错误的方向来避开障碍物。由于 Bug 2 总是采用一个固定的方向绕过障碍物,无论哪个更快,它无法保证最短路径。
- 最后,我们用于避开障碍物的确切控制方法不是最优的。即当遇到障碍物时,机器人会后退并转弯,但只进行一步。其他操作,如原地转弯(如果可能)或多步后退与障碍物边界平行应该会产生更好的路径。
8、结束语
太棒了!到现在,带有最小 2D 示例(以及其自己的模拟环境)的主要 ROS2 项目结构已经完成。不要低估这一点,因为它代表了任何移动机器人任务的基础工作:A* 规划、强化学习规划、定位和建图。此外,ROS2 和 C++ 本身被认为是机器人项目的行业标准。欢迎评论提出问题或反馈,我很乐意回答。下一篇文章将关于使用 Gitlab 自动化作业进行该项目的持续集成和部署(CICD)。
原文链接:Up and Running with Autonomous Mobile Robotics in ROS 2 and C++
汇智网翻译整理,转载请标明出处