前面几部分我们一直在"单关节模组"的尺度工作:MCU 上跑固件、CAN/串口收发帧、控制一个关节。但一台人形机器人是另一个量级:20~40+ 个关节、相机/激光雷达/IMU、2~3 台上位机,还要接仿真与强化学习环境——天然是分布式系统。
如果每个模块都自己写通信代码,就会出现大量重复难维护的"轮子":进程/跨设备通信、节点发现、消息类型、参数配置、日志、时钟同步……中间件(middleware)把这层公共能力抽出来,让大家只写业务逻辑。ROS2 正是人形/足式机器人领域事实标准的开源中间件框架,由三部分组成:
通信(进程内/进程间/跨设备,1 对 1、1 对 N、N 对 M 分发)、解耦(发布者不用知道订阅者是谁、在哪台机器、什么语言)、组织(节点生命周期、参数配置、日志诊断、时间同步)、实时与可靠(用 QoS 权衡实时性与可靠性:传感器可丢帧,控制指令不能丢)。
ROS1 诞生于 2007 年,面向"单机、研究原型",存在单点故障(必须有一个 roscore)、实时性差、多机协同难等硬伤;ROS2 从 2015 年重写。工程上最关键的差异:
| 维度 | ROS1 | ROS2 |
|---|---|---|
| 通信中间件 | 自研 TCPROS/UDPROS,基于 TCP 连接 | 采用 DDS(Data Distribution Service)标准,多机互通有标准可循 |
| 架构中心 | 需要 roscore 中心节点,它挂了全系统瘫痪 | 去中心化,节点经 DDS 自动发现、直连,可独立增删重启 |
| 生命周期 | 节点生命周期由用户自行管理 | 内置生命周期管理(未配置→未激活→激活→关闭) |
| 实时性 | 不保证,受 TCP 与中心架构限制 | 通过 QoS 与 DDS 实时实现,可满足部分硬实时场景 |
| 通信模型 | 话题/服务/动作/参数 | 话题/服务/动作/参数,全部支持 QoS 与类型安全 |
| 平台与语言 | 主要是 Linux + C++/Python | Windows/macOS/嵌入式也支持,上位机与板载系统可统一 |
| 安全性 | 几乎无安全机制 | 内置 SROS2 加密/认证/权限控制 |
ROS2 以"发行版(distribution)"为单位发布,锁定一组兼容包并绑定特定 Ubuntu。自 2022 年起 LTS 每两年一个(偶数年)、支持 5 年,非 LTS 约 18 个月。截至 2025 年主流版本(以官方 Releases 页为准):
| 发行版 | 发布日期 | 支持类型 | EOL(停止支持) | 绑定 Ubuntu | 说明 |
|---|---|---|---|---|---|
| Humble Hawksbill | 2022-05 | LTS(5 年) | 2027-05 | 22.04 | 生态最成熟,宇树等厂商包普遍支持,新手首选 |
| Iron Irwini | 2023-05 | 非 LTS(18 个月) | 2024-11 | 22.04 | 已 EOL,不建议新项目采用 |
| Jazzy Jalisco | 2024-05 | LTS(5 年) | 2029-05 | 24.04 | 当前最新 LTS,新项目推荐 |
| Kilted | 2025-05 | 非 LTS | 约 2026-11 | 24.04 | 非 LTS,尝鲜用 |
这六个概念是 ROS2 的"语法":人形机器人的状态估计、运动规划、关节控制,都是它们的组合。
节点是 ROS2 图(Graph)中的基本计算单元:一个进程可含多个节点,各负责一项职责(如"状态估计节点"、"左腿控制节点")。节点之间不直接调用函数,而是通过话题/服务/动作通信,因此节点可被独立启动、停止、替换。
# 最小节点(run 后阻塞等待回调)
import rclpy
from rclpy.node import Node
def main(args=None):
rclpy.init(args=args)
node = Node("hello_node")
node.get_logger().info("节点已启动!")
rclpy.spin(node)
rclpy.shutdown()
话题是最常用的通信方式:发布者广播,订阅者按需接收。发布者不知道谁在听,订阅者不知道谁在发——这是解耦的核心。/joint_states(关节角)、/odom、/scan 都是典型话题。下面是一对可运行示例:
# talker.py —— 发布者:每 1 秒向 /chatter 发一条字符串
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Talker(Node):
def __init__(self):
super().__init__("talker")
self.pub = self.create_publisher(String, "chatter", 10) # 队列深度 10
self.timer = self.create_timer(1.0, self.tick)
self.count = 0
def tick(self):
msg = String()
msg.data = "hello %d" % self.count
self.pub.publish(msg)
self.get_logger().info("发布: %s" % msg.data)
self.count += 1
def main(args=None):
rclpy.init(args=args)
rclpy.spin(Talker())
rclpy.shutdown()
# listener.py —— 订阅者:收到 /chatter 消息并打印
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Listener(Node):
def __init__(self):
super().__init__("listener")
self.sub = self.create_subscription(String, "chatter", self.cb, 10)
def cb(self, msg):
self.get_logger().info("收到: %s" % msg.data)
def main(args=None):
rclpy.init(args=args)
rclpy.spin(Listener())
rclpy.shutdown()
验证:两个终端分别运行发布者与订阅者,再开第三个终端 ros2 topic echo /chatter 即可旁听数据流——这正是 ROS2 调试的日常姿势。
需要"问一句、答一句"(如查询机器人是否就绪、让关节归零并等待结果)时用服务。
# add_server.py —— 服务端:两个整数相加
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AddServer(Node):
def __init__(self):
super().__init__("add_server")
self.srv = self.create_service(AddTwoInts, "add_two_ints", self.cb)
def cb(self, req, res):
res.sum = req.a + req.b
return res
def main(args=None):
rclpy.init(args=args)
rclpy.spin(AddServer())
rclpy.shutdown()
客户端既可用 Python 的 call_async 发起,也可直接用命令行测试:
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 3, b: 5}"
任务要跑几十秒(如"走到门口")、需要进度反馈、还能中途取消时,用动作。动作是"服务 + 话题"的组合:目标像服务请求,反馈用话题持续推送,结果结束时返回。
# 动作服务端骨架:执行"移动到目标位姿"的长任务
class MoveServer(Node):
def __init__(self):
super().__init__("move_server")
self.act = self.create_action_server(MoveToPose, "move_to_pose", self.execute)
def execute(self, goal_handle):
goal_handle.publish_feedback(MoveToPose.Feedback(progress=0.3)) # 推送进度
if goal_handle.is_cancel_requested: # 支持取消
goal_handle.canceled()
return MoveToPose.Result(success=False)
# …… 长任务执行 ……
goal_handle.succeed()
return MoveToPose.Result(success=True)
命令行:ros2 action send_goal /move_to_pose my_msgs/action/MoveToPose "{x: 1.0, y: 0.0}" --feedback
参数让节点行为不重新编译、不重启就能调整,例如把"最大关节速度"做成参数,方便标定调参。
# 节点内声明与读取参数
self.declare_parameter("max_speed", 1.5) # 默认 1.5
speed = self.get_parameter("max_speed").value
# 命令行: ros2 param get /talker max_speed
# ros2 param set /talker max_speed 2.0
命名空间给话题/服务/节点名加前缀,把不同机器人或子系统隔开:两台 H1 同时在线的场景下,一台发 /robot1/joint_states,另一台发 /robot2/joint_states,互不干扰。
# 节点放进命名空间 /robot1,话题自动变为 /robot1/chatter
ros2 run demo_nodes_py talker --ros-args -r __ns:=/robot1
ros2 topic list # 应看到 /robot1/chatter
| 概念 | 通信模型 | 适合场景 | 日常类比 |
|---|---|---|---|
| 节点 | — | 承载一个功能模块 | 公司里的一个部门 |
| 话题 | 发布/订阅(异步) | 传感器数据、状态流、指令流 | 广播电台 |
| 服务 | 请求/响应(同步) | 查询状态、一次性操作 | 打电话问客服 |
| 动作 | 目标+反馈+结果 | 长任务、需要进度、可取消 | 外卖下单(可跟踪、可取消) |
| 参数 | 键值存储 | 运行时可调配置 | 旋钮/配置文件 |
| 命名空间 | 名字前缀 | 多机器人、多模块隔离 | 部门前缀(研发-张三) |
ROS2 用 colcon 构建(替代 ROS1 的 catkin)。一个典型工作空间长这样:
dev_ws/
├── src/ 源码(包都放这里)
├── build/ 构建中间产物
├── install/ 安装结果(可执行/库)
└── log/ 构建日志
mkdir -p ~/dev_ws/src
cd ~/dev_ws/src
ros2 pkg create my_first_pkg --build-type ament_python --dependencies rclpy std_msgs
cd ~/dev_ws
colcon build --symlink-install
source install/setup.bash
ros2 run my_first_pkg talker
source install/setup.bash 会报 "Package not found"。建议把系统与工作空间的 setup.bash 都写进 ~/.bashrc,且先 source 系统 ROS2,再 source 工作空间。
package.xml 描述包的名字、依赖、许可;Python 包用 setup.py 声明入口,C++ 包用 CMakeLists.txt 描述编译规则。
<?xml version="1.0"?>
<package format="3">
<name>my_first_pkg</name>
<version>0.0.1</version>
<description>我的第一个 ROS2 包</description>
<maintainer email="you@example.com">you</maintainer>
<license>Apache-2.0</license>
<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
# CMakeLists.txt(C++ 包)关键片段
cmake_minimum_required(VERSION 3.8)
project(my_cpp_pkg)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(talker src/talker.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS talker DESTINATION lib/${PROJECT_NAME})
ament_package()
Python 包还要在 setup.py 的 entry_points 里声明 "talker = my_first_pkg.talker:main" 这类可执行入口。
launch 文件用 Python 编写,把要启动的节点、参数、命名空间一次拉起——人形机器人的"开机流程"就是一个 launch 文件,还能 include 别的 launch 形成层级结构。
# my_first_pkg/launch/demo.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package="demo_nodes_py", executable="talker",
name="my_talker", output="screen"),
Node(package="demo_nodes_py", executable="listener",
name="my_listener", output="screen"),
Node(package="my_first_pkg", executable="talker",
parameters=[{"max_speed": 2.0}]), # 带参数启动
])
ros2 launch my_first_pkg demo.launch.py
| 命令 | 作用 |
|---|---|
ros2 node list / ros2 node info <node> | 列出节点 / 查看节点的发布、订阅、服务 |
ros2 topic list -t | 列出话题及其消息类型 |
ros2 topic echo <topic> | 实时打印话题消息(调试神器) |
ros2 topic info <topic> -v | 话题详情:发布者、订阅者、QoS |
ros2 topic hz <topic> | 统计发布频率,检查节点是否卡死 |
ros2 service list / ros2 service type | 列出服务 / 查询服务类型 |
ros2 service call <srv> <type> "{...}" | 直接调用一个服务 |
ros2 action list -t / ros2 action send_goal ... --feedback | 列出动作 / 发送动作目标并打印反馈 |
ros2 param list / get / set / dump | 参数查看、读取、修改、导出 |
ros2 run <pkg> <executable> | 运行包里的节点 |
ros2 launch <pkg> <file> | 按 launch 文件启动多节点系统 |
ros2 doctor | 系统自检:发现网络、配置、环境问题 |
URDF 是用 XML 描述机器人结构的标准格式:link 是刚体(含视觉几何、碰撞几何、惯量),joint 是 link 间的连接(含类型、旋转轴、运动范围)。"pelvis → 髋 → 大腿 → 小腿 → 踝"的链就是一条腿,左右两条腿加躯干和手臂组成人形机器人。
<!-- 一个 link + 一个 revolute 关节的最小示例 -->
<link name="pelvis">
<visual>
<geometry><box size="0.25 0.4 0.1"/></geometry>
<origin xyz="0 0 0" rpy="0 0 0"/>
</visual>
</link>
<joint name="l_hip_pitch" type="revolute">
<parent link="pelvis"/>
<child link="l_upper_leg"/>
<origin xyz="0 0.1 0" rpy="0 0 0"/> <!-- 关节在父坐标系中的位姿 -->
<axis xyz="1 0 0"/> <!-- 旋转轴 -->
<limit lower="-1.57" upper="1.57" effort="300" velocity="8"/>
</joint>
关节类型:revolute(有限角度,如髋/膝/踝)、continuous(无限旋转)、prismatic(平移)、fixed(固定)、floating(6 自由度浮动,常模拟"基座-世界"关系)。
手写 URDF 时左右腿几乎一样,复制粘贴又长又难维护。xacro 提供宏、属性、数学表达式与文件包含,是人形 URDF 的标配写法:
<?xml version="1.0"?>
<robot name="my_humanoid" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="leg" params="side">
<link name="${side}_upper_leg">
<visual>
<geometry><cylinder length="0.35" radius="0.04"/></geometry>
</visual>
</link>
<joint name="${side}_hip_pitch" type="revolute">
<parent link="pelvis"/>
<child link="${side}_upper_leg"/>
</joint>
</xacro:macro>
<xacro:leg side="left"/> <!-- 两行生成两条腿 -->
<xacro:leg side="right"/>
</robot>
ros2 run xacro xacro robot.xacro > robot.urdf # 展开成标准 URDF
本站 00 部分提供了 H1/G1/X1 的解剖视图与 3D 模型素材,学习"把真实关节布局映射成 URDF"时非常有用:
要"看到"机器人,需要三个角色:robot_state_publisher(读 URDF 发 TF)、joint_state_publisher(发关节角度,带 GUI 可拖动)、RViz2(渲染)。
# 手动三件套:robot_state_publisher + joint_state_publisher_gui + rviz2
ros2 run robot_state_publisher robot_state_publisher \
--ros-args -p robot_description:="$(cat robot.urdf)"
ros2 run joint_state_publisher_gui joint_state_publisher_gui
rviz2
RViz2 关键设置:Fixed Frame 设为 pelvis/base_link(否则报"frame 不在 TF 树中");Add → RobotModel 显示模型,再 Add → TF 显示坐标系;拖动关节滑块观察腿部运动。
TF(Transform)管理所有坐标系间的相对位姿,构成一棵 TF 树:map → odom → base_link → 各关节坐标系 → 传感器坐标系。为什么需要它?每个数据都诞生在某个坐标系里(点云在雷达系、图像在相机系、关节角在关节系),要融合数据就必须能随时做坐标变换。常用命令:
# 发布固定不变的静态变换(如 map → odom 初始关系)
ros2 run tf2_ros static_transform_publisher 0 0 0 0 0 0 map odom
ros2 run tf2_tools view_frames # 可视化 TF 树(frames.pdf)
ros2 run tf2_ros tf2_echo base_link l_ankle # 检查坐标系间变换
robot_state_publisher 没读到 URDF。先 view_frames 看树,再查 ros2 topic hz /tf。
真机调试代价高、风险大,几乎必须先仿真。ROS2 的价值之一就是"同一套代码,换一个仿真后端":机器人侧代码不变,只换与硬件之间的"桥"。
gazebo_ros_pkgs 提供 gazebo_ros(导入模型、spawn)与 gazebo_ros2_control(把仿真关节暴露成 ros2_control 接口,与真机控制代码一致);新版用 ros_gz 桥接。流程:写 URDF/xacro → 加仿真插件 → ros2 launch → 用话题驱动。isaacsim-ros2-bridges 把图像/深度/里程计/TF 发布成 ROS2 话题,并订阅关节指令,实现"仿真 ⇄ ROS2 同栈",适合先训策略再 sim2real。| 仿真器 | 桥接包/方式 | 优势 | 典型用途 |
|---|---|---|---|
| Gazebo Classic | gazebo_ros / gazebo_ros2_control | 成熟稳定、生态大、免费 | 传统仿真、测试 ROS2 栈 |
| Gazebo(gz-sim) | ros_gz / gz_ros2_control | 新一代,插件架构、性能更好 | 新项目默认选择 |
| Isaac Sim | isaacsim-ros2-bridges | 高保真、GPU 物理、RL 集成 | 人形 sim2real、RL 训练 |
| MuJoCo | mjros / 自写桥 | 极快、接触稳定、轻量 | RL 迭代、控制算法验证 |
/joint_states、/odom),同时把 ROS2 上的指令(如 /cmd_vel)翻译回仿真器。学会这层抽象,真机与仿真切换只是换一个"驱动"。
MoveIt2 是 ROS2 生态最主流的运动规划框架(官方站点 moveit.ai),把"机械臂如何从 A 姿态走到 B 姿态"工具化。核心组件:
典型 Python 用法(moveit_commander;新项目也可用官方 moveit_py):
import moveit_commander
from geometry_msgs.msg import Pose
moveit_commander.roscpp_initialize([])
robot = moveit_commander.RobotCommander()
scene = moveit_commander.PlanningSceneInterface()
arm = moveit_commander.MoveGroupCommander("arm")
# 1) 关节空间规划:直接给目标关节角
arm.set_joint_value_target({"shoulder_pitch": 0.5, "elbow": -1.2})
plan = arm.plan(); arm.execute(plan, wait=True)
# 2) 笛卡尔空间规划:给末端位姿,自动逆解+插值
pose = Pose(); pose.position.x = 0.4; pose.position.y = 0.1; pose.position.z = 0.8
arm.set_pose_target(pose); arm.go(wait=True)
MoveIt2 的设计前提是固定基座机械臂:基座不动,只需规划关节角。而人形机器人是浮动基座 + 全身协调 + 平衡约束的系统:
| 方案 | 定位 | 说明 |
|---|---|---|
| MoveIt2(局部使用) | 上肢/手臂规划 | 腰部以上若是机械臂式结构可单独规划手臂 |
| 轨迹平滑(tosr_ts 等) | 轨迹后处理 | 社区常用 tosr_ts 等把步态/足端轨迹平滑成时间最优、加速度受限的轨迹,供执行层跟踪 |
| 自研 WBC(全身控制) | 实时控制层 | Whole-Body Control:把平衡/姿态/任务优先级建模成 QP,毫秒级求解关节力矩,人形标配 |
| MPC(模型预测控制) | 动态规划 | 基于简化动力学模型在线滚动优化,厂商常用于步态与平衡 |
| 强化学习策略 | 学习式规划 | 如 NVIDIA GR00T WBC / Isaac Lab 训练的策略,直接输出关节指令 |
宇树(Unitree)官方维护了两个开源仓库,是学习"真实人形机器人如何接 ROS2"的一手资料:
unitree_api(DDS 通信接口)、unitree_go/unitree_hg(消息定义)、unitree_ros2_real(真机桥)与 unitree_ros2_sim(Gazebo 仿真桥),并附可直接 colcon build 的工作空间;通信原理:宇树机器人本体通过 DDS(而非 CAN/串口)与上位机交换数据,ROS2 侧把"状态量→DDS 主题"包装成标准消息(关节状态、IMU、触地信息)并接收高层指令。拿到真机后 ros2 topic list 就能看到一整套状态话题——ROS2 扮演"整机软件总线"。
把上面的概念串起来,一台真实人形机器人的软件栈大致分层如下(各层之间全部通过 ROS2 通信):感知层(相机/LiDAR/IMU/编码器)→ 状态估计层(ESKF/因子图融合,发布 /odom 与 TF)→ 决策规划层(步态/WBC/MPC/RL,输出关节目标)→ 关节控制层(三环、阻抗、ros2_control)→ 执行层(CAN/以太网到关节模组,参见本站 Hdrive)。
ros2_control 抽象"控制器↔硬件",同一套代码无缝跑仿真与真机;ROS2 的 QoS 由多个策略组成,其中最常踩坑的是 reliability(可靠/尽力)与 durability(易失/暂存)两个维度。下表给出 4 种典型组合的适用场景与典型坑:
| 组合 | 典型用途 | 何时用 | 典型坑 |
|---|---|---|---|
RELIABLE + VOLATILE(ROS2 默认) |
传感器数据、命令指令 | 大多数场景的稳妥默认:不允许丢消息,订阅者上线后才收后续消息 | 无线/弱网下重传堆积造成延迟;高频大消息(如点云)用默认可靠 QoS 会丢帧反而更糟 |
RELIABLE + TRANSIENT_LOCAL |
latched 配置、地图、静态 TF | "后加入者也要拿到最新一帧"的场景:地图、代价地图、标定参数、/tf_static |
发布端历史深度(depth)默认只有 1,发布多条只保留最后几条;订阅端晚启动时拿到的是"旧值"而非"无值",调试时容易误判时序 |
BEST_EFFORT + VOLATILE |
高频传感器流 | 相机、IMU、点云等高频流:丢一帧没关系,要的是低延迟不堆积 | 必须与订阅端 best_effort 匹配;若订阅端写的是默认 reliable,会静默断连收不到任何数据(见下方排查三步) |
BEST_EFFORT + TRANSIENT_LOCAL |
少用 | 仅在"允许丢 + 要最新值"的极特殊场景(如低频状态心跳)考虑 | 两头的坑都占:既可能丢消息,又只有最后一次发布可补发;语义容易让人误解,团队协作时尽量不用 |
QoS 不兼容的表现非常隐蔽:话题在 ros2 topic list 里存在,但订阅端一条数据都收不到,也没有任何报错(例如发布端 best_effort、订阅端默认 reliable)。排查三步:
ros2 topic info /your_topic -v,分别核对 Publisher 与 Subscription 的 Reliability、Durability、History 与 depth;reliable 订阅端只能配 reliable 发布端,durability 上 transient_local 订阅端只能配 transient_local 发布端——任何一项"要求高于对方提供"即不兼容;# 第一步:查看话题双方 QoS 配置
ros2 topic info /camera/image_raw -v
# 输出重点看两段:
# Publisher: Reliability: BEST_EFFORT, Durability: VOLATILE
# Subscription: Reliability: RELIABLE ← 与发布端不兼容,静默断连
#
# 第三步:用 yaml 给驱动类节点覆盖 QoS(免编译),再重启节点
# qos_override.yaml:
# /camera/image_raw:
# data_type: "image"
# reliability: best_effort
ros2 run rclcpp_components component_container_mt ...
# 启动时追加: --params-file qos_override.yaml
apt 官方源(ros-humble-xxx),勿混用不同发行版包。
RMW_IMPLEMENTATION 不一致且跨机时可能互相发现不了——两端统一(export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp),复杂网络再用 CYCLONEDDS_URI 指定网卡与多播。
ROS_DOMAIN_ID(多机器人务必分开);本机调试可 export ROS_LOCALHOST_ONLY=1。跨机要求同一网段、放行多播 UDP,用 ping + ros2 doctor 排查;节点"消失"也可能是 daemon 缓存,ros2 daemon stop 重试。
socketcan_bridge(CAN 帧 → can_msgs/Frame)、canopen_402(CiA402 伺服),或自研"读帧 → 解析协议 → 发布 JointState"。桥进程要稳定、周期要准(丢帧直接表现为关节抖动),高频反馈建议独立进程 + 高优先级线程。
best_effort + 深队列,控制指令用 reliable;两端不兼容会静默断连(无报错只是没数据),用 ros2 topic info -v 核对。仿真要 use_sim_time:=true 并读 /clock。
ros2 doctor / view_frames / ros2 topic hz。
/joint_states 会出什么问题?如何用命名空间隔离?