【ROS2速成 - Day2】ROS2五大核心概念吃透(嵌入式类比记忆,超好懂)
前言
大家好,我是深耕嵌入式 15 年的老林。上一篇 Day1 我们搭好了 ROS2 的开发环境,很多同学私信我说,ROS2 的概念太多了,什么节点、话题、服务,听着就头大,完全不知道和我们平时写单片机代码有什么关系。
其实我刚开始学 ROS2 的时候也有同样的感觉,觉得这东西太 "上层" 了,和我们天天写的寄存器操作、任务调度、串口通信完全是两个世界。但后来我发现,ROS2 的核心设计思想和嵌入式开发是完全相通的,只是换了一套名词而已。
今天这篇文章,我就用嵌入式工程师最熟悉的语言,把 ROS2 的五大核心组件全部翻译过来。看完你会发现,原来 ROS2 这么简单,就是把我们嵌入式里天天用的东西,封装成了一套标准的框架而已。
一、核心思想:ROS2 就是一个分布式的嵌入式操作系统
先给大家建立一个整体认知:ROS2 本质上就是一个分布式的、跨平台的嵌入式操作系统。
- 我们平时用的 FreeRTOS 是单芯片的操作系统,管理同一个 MCU 上的多个任务
- ROS2 是跨设备的操作系统,管理同一个网络上的多个节点(可以是 PC、单片机、树莓派、机器人)
所有的 ROS2 程序,都是围绕着 "节点之间如何通信" 这个核心问题展开的。而通信的方式,就是我们今天要讲的话题、服务、参数。
二、五大核心组件详解(嵌入式类比记忆法)
2.1 节点 (Node) = FreeRTOS 独立任务线程
最核心的类比:一个 ROS2 节点,就相当于 FreeRTOS 里用 xTaskCreate 创建的一个独立任务。
我们写嵌入式代码的时候都知道:
- 不能把所有功能都写在 main 函数的 while (1) 里,那样耦合度太高,出了问题不好排查
- 我们会把不同的功能拆成不同的任务:比如一个任务负责采集传感器数据,一个任务负责电机控制,一个任务负责串口通信
- 每个任务有自己的栈空间,独立运行,互不干扰
- 任务之间通过消息队列、信号量等方式通信
ROS2 的节点就是完全一样的设计思想:
- 一个节点只负责一个功能:比如
camera_node负责采集摄像头数据,imu_node负责采集 IMU 数据,motor_control_node负责控制电机 - 每个节点是一个独立的进程,有自己的内存空间,独立运行,崩溃了不会影响其他节点
- 节点可以运行在同一个设备上,也可以运行在不同的设备上(比如树莓派跑传感器节点,PC 跑路径规划节点)
- 节点之间通过话题、服务、参数等方式通信
举个例子:一个最简单的差分驱动机器人,至少需要 3 个节点:
imu_node:采集 IMU 数据,发布 IMU 话题motor_driver_node:订阅速度话题,控制电机转动teleop_node:接收键盘输入,发布速度话题
这就相当于我们在 FreeRTOS 里创建了 3 个任务,每个任务做一件事,通过消息队列传递数据。
2.2 话题 (Topic) = 消息队列 + 串口广播
最核心的类比:一个 ROS2 话题,就相当于一个带广播功能的消息队列,或者说一个串口总线。
我们先回忆一下嵌入式里的消息队列:
- 一个任务往消息队列里发数据(生产者)
- 一个或多个任务从消息队列里取数据(消费者)
- 异步通信:发完就走,不用等对方接收
- 数据是先进先出的
再回忆一下串口广播:
- 一个设备往串口总线上发数据
- 所有挂在总线上的设备都能收到这个数据
- 谁需要谁处理,不需要的可以忽略
ROS2 的话题,就是把这两个东西结合起来了:
- 有一个 ** 发布者 (Publisher)** 往话题里发消息(相当于往消息队列里写数据,或者往串口总线上发数据)
- 有一个或多个 ** 订阅者 (Subscriber)** 从话题里收消息(相当于从消息队列里读数据,或者监听串口总线)
- 异步通信:发布者发完消息就继续干自己的事,不用等订阅者处理
- 一对多通信:一个发布者可以对应多个订阅者
- 数据是先进先出的
举个例子:上面的teleop_node往/cmd_vel话题里发布速度消息,motor_driver_node订阅这个话题,收到消息后控制电机转动。如果我们再加一个odometry_node,也订阅/cmd_vel话题,就可以根据速度计算机器人的里程计。
这就相当于teleop_task往一个消息队列里发速度数据,motor_control_task和odometry_task都从这个消息队列里取数据。
注意:话题的命名很重要,就像串口的设备名/dev/ttyUSB0一样,只有发布者和订阅者使用完全相同的话题名,才能通信。
2.3 消息 (Message) = C 语言通信结构体
最核心的类比:一个 ROS2 消息,就相当于我们在 C 语言里定义的一个通信结构体。
这个类比绝对是 ROS2 入门最重要的一个点,理解了这个,你就理解了 ROS2 通信的本质。
我们写嵌入式代码的时候,两个任务之间要传递数据,或者两个设备之间通过串口通信,会怎么做?
- 我们会先定义一个结构体,约定好数据的格式:比如第 1 个字节是命令字,第 2-3 个字节是速度,第 4-5 个字节是角度
- 发送方把数据打包成这个结构体,然后发送出去
- 接收方按照同样的结构体格式解析数据
如果双方约定的结构体不一样,就会解析出乱码,通信失败。这是我们写嵌入式代码天天都会遇到的问题。
ROS2 的消息,就是帮你把这个 "约定结构体" 的过程标准化了。ROS2 已经预定义了大量常用的消息类型,你直接用就行,不用自己再去定义协议了。
举个例子:最常用的速度消息geometry_msgs/Twist,它的结构体定义是这样的:
// 对应C语言的结构体 typedef struct Twist { Vector3 linear; // 线速度,单位m/s Vector3 angular; // 角速度,单位rad/s } Twist; typedef struct Vector3 { float x; float y; float z; } Vector3;对于差分驱动机器人来说,我们只需要用linear.x(前后速度)和angular.z(转向速度)就够了。
当teleop_node往/cmd_vel话题发布一个Twist消息时,就相当于它把一个这样的结构体打包发了出去。motor_driver_node收到这个结构体后,解析出linear.x和angular.z的值,然后转换成左右轮的 PWM 输出。
就是这么简单!没有任何魔法,就是结构体的打包和解包而已。
2.4 服务 (Service) = 函数调用 + 串口问答
最核心的类比:一个 ROS2 服务,就相当于一个远程函数调用,或者说串口的问答式通信。
我们先回忆一下函数调用:
- 你调用一个函数,传入参数
- 函数执行相应的操作
- 函数返回一个结果给你
再回忆一下串口的问答式通信:
- 你给设备发一个指令:"请读取传感器 A 的值"
- 设备收到指令后,读取传感器的值
- 设备给你回复:"传感器 A 的值是 123"
这两种都是同步通信:你发完请求后,必须等对方回复,才能继续往下执行。
ROS2 的服务,就是这种同步的、一对一的通信方式:
- 有一个 ** 客户端 (Client)** 发送请求(相当于调用函数,传入参数)
- 有一个 ** 服务端 (Server)** 处理请求,执行相应的操作
- 服务端给客户端返回一个响应(相当于函数的返回值)
- 一对一通信:一个服务端只能同时处理一个客户端的请求
- 同步通信:客户端发完请求后会阻塞,直到收到响应
举个例子:我们想让机器人执行一个拍照的操作,就可以用服务来实现:
- 客户端发送请求:"请拍一张照片"
- 服务端(摄像头节点)收到请求后,控制摄像头拍照
- 服务端返回响应:"拍照成功,照片数据是 xxx"
话题 vs 服务 怎么选?
| 通信方式 | 适用场景 | 嵌入式类比 |
|---|---|---|
| 话题 | 连续不断的数据流 | 消息队列、串口广播 |
| 服务 | 一次性的、有返回值的操作 | 函数调用、串口问答 |
简单来说:一直发的用话题,偶尔问的用服务。
2.5 参数 (Parameter) = 全局变量 + Flash 配置参数
最核心的类比:ROS2 参数,就相当于嵌入式里的全局变量,或者存在 Flash 里的配置参数。
我们写嵌入式代码的时候,经常会用到一些配置参数:比如电机的 PID 参数、传感器的校准值、串口的波特率。这些参数我们会:
- 定义成全局变量,程序运行时可以读写
- 保存在 Flash 里,掉电不丢失
- 可以通过串口指令修改,不用重新烧写程序
ROS2 的参数,就是实现了这个功能:
- 参数是节点的配置项,以键值对的形式存在(比如
"motor_pid_p": 1.0) - 节点运行时可以读写自己的参数
- 参数可以保存在 YAML 文件里,启动节点时加载
- 可以通过命令行或其他节点修改参数,不用重新编译代码
举个例子:我们的motor_control_node可以定义三个参数:pid_p、pid_i、pid_d。启动节点时从配置文件加载这些参数,运行时如果发现 PID 参数不合适,可以直接通过命令行修改,不用重新编译代码,非常方便。
三、ROS2 常用基础消息类型(必须熟记)
ROS2 已经为我们预定义了大量常用的消息类型,覆盖了机器人开发的绝大多数场景。下面这几个是你开发中天天都会用到的,必须熟记。
3.1 基础数据类型 (std_msgs)
最基础的消息类型,封装了 C 语言的基本数据类型:
std_msgs/String:字符串消息,最常用的调试消息string datastd_msgs/Int32、std_msgs/Float32:整数、浮点数消息std_msgs/Bool:布尔值消息
3.2 运动控制类型 (geometry_msgs)
机器人运动控制相关的消息类型:
geometry_msgs/Twist:速度消息,控制机器人运动的核心消息Vector3 linear # 线速度(x:前后, y:左右, z:上下) Vector3 angular # 角速度(x:翻滚, y:俯仰, z:偏航)geometry_msgs/Pose:位姿消息,表示机器人的位置和姿态Point position # 位置(x,y,z) Quaternion orientation # 姿态(四元数)geometry_msgs/PoseStamped:带时间戳的位姿消息,最常用的位姿消息Header header # 包含时间戳和坐标系 Pose pose
3.3 传感器类型 (sensor_msgs)
各种传感器数据的消息类型:
sensor_msgs/Imu:IMU 数据消息,包含加速度、角速度、姿态Header header Quaternion orientation # 姿态四元数 Vector3 angular_velocity # 角速度 Vector3 linear_acceleration # 线加速度sensor_msgs/Image:图像消息,摄像头数据sensor_msgs/LaserScan:激光雷达数据消息
四、总结:一张表记住五大核心组件
最后给大家整理了一张对比表,把今天讲的所有内容都浓缩进去了,建议截图保存。
| ROS2 组件 | 嵌入式类比 | 核心功能 | 通信模式 | 典型使用场景 |
|---|---|---|---|---|
| 节点 (Node) | FreeRTOS 任务 | 执行具体功能 | 无 | 传感器采集、电机控制、路径规划 |
| 话题 (Topic) | 消息队列 + 串口广播 | 连续数据流传输 | 异步、一对多 | 发布传感器数据、速度指令 |
| 消息 (Message) | C 语言结构体 | 定义数据格式 | 无 | 所有通信的数据载体 |
| 服务 (Service) | 函数调用 + 串口问答 | 一次性请求响应 | 同步、一对一 | 拍照、启动设备、查询状态 |
| 参数 (Parameter) | 全局变量 + Flash 配置 | 节点配置管理 | 无 | PID 参数、校准值、设备地址 |
写在最后
今天这篇文章,我用嵌入式工程师最熟悉的语言,把 ROS2 的五大核心组件全部翻译了一遍。相信你现在已经对 ROS2 有了一个清晰的整体认知,再也不会觉得 ROS2 的概念晦涩难懂了。
其实 ROS2 的本质就是一套标准化的通信框架,它把我们嵌入式开发中天天都在重复做的 "拆任务、定义协议、写通信" 这些工作,全部封装成了标准的组件。我们不用再去关心底层的通信细节,只需要专注于自己的业务逻辑就行。
这就是 ROS2 最大的价值:让机器人开发变得像搭积木一样简单。
📢 下期连载预告
【ROS2 速成 - Day3】ROS2 命令行全套实操(开发天天用,必须练熟)
下一篇我会带大家手把手实操 ROS2 的所有常用命令,包括如何查看节点、话题、服务,如何发布消息、调用服务,如何设置参数。这些命令是你开发 ROS2 的时候天天都会用到的,必须练熟。
码字不易,欢迎点赞 + 收藏 + 关注,后续我会按【ROS2 速成 - DayX】的格式持续连载更新,分享 ROS2 嵌入式实战代码、硬件对接源码、项目工程模板,帮大家少踩坑,快速从嵌入式工程师转型 ROS2 开发。
有任何问题欢迎在评论区留言,我会一一回复。
