ROS 2 机器人仿真实战(Gazebo 与 MoveIt2)
前言
真机器人很贵,摔一下要修、要等人、还可能伤人。Gazebo 就是机器人的"飞行模拟器"——你在电脑里造一个和真机一模一样的虚拟替身,先在里面把动作练熟,再去碰真家伙。这篇要做的,就是给你一台能在屏幕里跑起来的三关节机械臂,并让它听 MoveIt2 的指挥。
在上一篇 ROS 2 入门与环境搭建 装好环境之后,本文带你走完一条完整链路:用 URDF 画出机器人长什么样 → 用 ros2_control 接上"虚拟电机" → 用 Gazebo 让它动起来 → 用 MoveIt2 给它下"去抓那个杯子"的指令。读完你就能在屏幕上拖一下机械臂末端,看它自己规划路线伸过去——这是所有机器人操作任务的起点。
Gazebo 概述
Gazebo Classic vs Gazebo Sim(Ignition)
| 特性 | Gazebo Classic | Gazebo Sim (Ignition) |
|---|---|---|
| 版本 | Gazebo 11(最终版) | Gazebo Harmonic(最新) |
| 通信 | ROS 1 原生支持 | 通过 ros_gz 桥接 ROS 2 |
| 架构 | 单体应用 | 模块化微服务架构 |
| 物理引擎 | ODE / Bullet / Simbody | DART / ODE / Bullet |
| GUI | Qt5 | Web-based(可无头运行) |
ROS 2 Kilted 默认集成了 Gazebo Harmonic。本教程主要使用 Gazebo Sim,因为它原生支持 ROS 2 通信。
一句话记住 Gazebo:它是机器人界的"模拟飞行训练舱"——不用烧电、不用怕撞,反复练到熟为止。下面先把这套训练舱和它需要的几个搭档装好。
实战:安装仿真环境
# 安装 Gazebo ROS 2 集成包
sudo apt install ros-kilted-ros-gz
# 安装 ros2_control 相关包
sudo apt install ros-kilted-ros2-control ros-kilted-ros2-controllers
# 安装 MoveIt2
sudo apt install ros-kilted-moveit
# 安装 xacro(URDF 宏处理工具)
sudo apt install ros-kilted-xacro
训练舱装好了,下一步就是"造机器人"——先得有它的三维模型,Gazebo 才知道往屏幕里放个什么东西。
实战:创建机器人 URDF 模型
项目结构
~/ros2_ws/src/my_robot/
├── launch/
│ └── display.launch.py
├── config/
│ └── ros2_control.yaml
├── urdf/
│ ├── my_robot.urdf.xacro
│ └── ros2_control.xacro
├── config/
│ └── moveit_config.yaml
├── CMakeLists.txt
└── package.xml
URDF 模型定义
创建 urdf/my_robot.urdf.xacro,定义一个简单的三关节机械臂:
URDF 是什么?把它想成机器人的"出生证明"——用一个个 <link>(连杆,比如一根胳膊)和 <joint>(关节,比如肘关节)拼出机器人长什么样、能怎么动。下面这段不用逐行看懂,先抓住结构:base_link 是固定基座,joint1/2/3 是三个能转的关节,end_effector 是末端的夹爪。<limit> 里写的 lower/upper 就是关节能转的上下限。跑起来后你会在 Gazebo 里看到这根蓝色三节棍。
<robot xmlns:xacro="https://www.ros.org/wiki/xacro" name="my_robot">
<!-- 包含 ros2_control 硬件描述 -->
<xacro:include filename="$(find my_robot)/urdf/ros2_control.xacro"/>
<!-- 材质定义 -->
<material name="blue">
<color rgba="0 0 0.8 1"/>
</material>
<material name="gray">
<color rgba="0.5 0.5 0.5 1"/>
</material>
<!-- Base Link(固定基座) -->
<link name="base_link">
<visual>
<geometry>
<cylinder radius="0.05" length="0.02"/>
</geometry>
<material name="gray"/>
</visual>
</link>
<!-- Joint 1:基座旋转 -->
<!-- revolute=绕固定轴旋转;axis xyz="0 0 1" 即绕竖直 Z 轴(机械臂的「腰」);
limit 的 lower/upper 是弧度制转角上下限,effort/velocity 是力矩与速度上限 -->
<joint name="joint1" type="revolute">
<parent link="base_link"/>
<child link="link1"/>
<origin xyz="0 0 0.01" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit lower="-3.14" upper="3.14" effort="10" velocity="1.0"/>
</joint>
<link name="link1">
<!-- link 三要素:visual(外观)+ collision(碰撞体)+ inertial(质量/惯性)。
本文入门只画 visual;要在仿真里「有力学反应」必须补另两个 -->
<visual>
<geometry>
<cylinder radius="0.02" length="0.3"/>
</geometry>
<origin xyz="0 0 0.15"/>
<material name="blue"/>
</visual>
</link>
<!-- Joint 2/3(肩、肘)与 joint1 完全同构,只换轴向与限位:
| 关节 | 轴 | 限位 | 连杆长 |
|------|------|-------|--------|
| j1 腰| Z 轴 | ±180° | 0.30 m |
| j2 肩| Y 轴 | ±90° | 0.25 m |
| j3 肘| Y 轴 | ±90° | 0.20 m | -->
<joint name="joint2" type="revolute">
<!-- ...(joint2/link2 折叠:格式同 joint1,axis 换 "0 1 0",limit ±1.57)-->
</joint>
<joint name="joint3" type="revolute">
<!-- ...(joint3/link3 折叠:同上)-->
</joint>
<link name="link3">
<!-- ...(link2/link3 折叠:蓝色圆柱,尺寸逐节递减)-->
</link>
<!-- 末端执行器(夹爪) -->
<link name="end_effector">
<visual>
<geometry>
<box size="0.04 0.04 0.02"/>
</geometry>
<material name="gray"/>
</visual>
</link>
<joint name="gripper_joint" type="fixed">
<parent link="link3"/>
<child link="end_effector"/>
<origin xyz="0 0 0.2"/>
</joint>
</robot>
ros2_control 硬件接口
创建 urdf/ros2_control.xacro:
这段代码在声明"这个机器人的关节该怎么被控制"。 它给每个关节列了两类接口:command(往里发命令,比如"转到 0.5 弧度")和 state(从里读状态,比如当前位置、速度)。在仿真里,真正的执行方是 Gazebo,所以末尾的 plugin 写的是 gazebo_ros2_control。先记住"声明接口"这件事——等下加载控制器时,名字得和它一一对上。
<robot xmlns:xacro="https://www.ros.org/wiki/xacro">
<ros2_control name="MyRobotHardware" type="system">
<hardware>
<!-- 仿真环境使用 gazebo_ros2_control -->
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
</hardware>
<joint name="joint1">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">0.0</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<!-- joint2/joint3 声明与 joint1 逐字相同(复制改名即可) -->
<joint name="joint2">
<!-- ...(position command + position/velocity state,同 joint1)-->
</joint>
<joint name="joint3">
<!-- ...(同上)-->
</joint>
</ros2_control>
<!-- Gazebo 仿真扩展 -->
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<robot_param>robot_description</robot_param>
<robot_param_node>robot_state_publisher</robot_param_node>
<parameters>$(find my_robot)/config/ros2_control.yaml</parameters>
</plugin>
</gazebo>
</robot>
Controller 配置
创建 config/ros2_control.yaml:
这份 YAML 回答的是「谁来控制、控制谁」:上半段 controller_manager 登记两个控制器(一个负责广播关节状态、一个负责转发位置命令),下半段 forward_position_controller 圈定它管哪三个关节。名字必须和 URDF 里声明的关节一一对上——对不上就是启动即崩的 "controller not found",这也是 ros2_control 新手第一大坑。100Hz 的 update_rate 是仿真起步值,真机力控通常要 500~1000Hz。
controller_manager:
ros__parameters:
update_rate: 100 # 100Hz 控制频率
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
forward_position_controller:
type: forward_command_controller/ForwardCommandController
forward_position_controller:
ros__parameters:
joints:
- joint1
- joint2
- joint3
interface_name: position
ros2_control 框架详解
ros2_control 是 ROS 2 中连接高层规划和底层硬件的核心框架。把它想成 ros2_control 的"公司组织架构":最上面 Controller Manager 是老板,负责招人、排班、按固定频率催活;中间各种 Controller 是干活的员工(轨迹跟踪、差速驱动等);底下 Hardware Interface 是标准化的"传声筒",把员工指令翻译成硬件听得懂的话;最底层 Hardware Component 才是真电机或 Gazebo。其架构如下:
┌─────────────────────────────────────────────┐
│ Controller Manager │
│ (生命周期管理 / 实时循环调度) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────┐ │
│ │ JointTraj. │ │ ForwardPos. │ │ Diff │ │
│ │ Controller │ │ Controller │ │ Drive│ │
│ └──────┬──────┘ └──────┬───────┘ └──┬───┘ │
│ └────────┬──────┴────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Hardware Interface│ │
│ │ (标准化读写接口) │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │Hardware Component│ │
│ │ (硬件/Gazebo适配)│ │
│ └─────────────────┘ │
└─────────────────────────────────────────────┘
| 组件 | 职责 |
|---|---|
| Controller Manager | 管理所有 Controller 的生命周期(加载、配置、激活),以固定频率调度 |
| Controller | 实现控制逻辑(轨迹跟踪、PID 等),通过接口读写关节状态 |
| Hardware Interface | 标准化抽象层,解耦 Controller 和硬件 |
| Hardware Component | 适配真实硬件或仿真器(如 Gazebo) |
常用 Controller 类型
| Controller | 用途 |
|---|---|
JointTrajectoryController |
关节轨迹跟踪(配合 MoveIt2 使用) |
ForwardCommandController |
直接转发位置/速度/力矩命令 |
DifferentialDriveController |
差速驱动移动底盘 |
JointStateBroadcaster |
发布 /joint_states 话题 |
JointGroupPositionController |
多关节同步位置控制 |
启动仿真
编译工作空间
cd ~/ros2_ws
colcon build --packages-select my_robot
source install/setup.bash
启动 Gazebo 仿真
# 1. 启动 robot_state_publisher(发布 URDF 到参数服务器)
ros2 launch my_robot display.launch.py
# 2. 启动 Gazebo(加载机器人模型)
ros2 launch my_robot gazebo.launch.py
# 3. 加载 Controller
ros2 control load_controller --set-state active joint_state_broadcaster
ros2 control load_controller --set-state active forward_position_controller
发送关节命令
# 发送关节位置命令(单位:弧度)
ros2 topic pub /forward_position_controller/commands \
std_msgs/msg/Float64MultiArray \
"{data: [0.5, -0.3, 0.8]}"
刚才我们是手动发角度让关节动——这像用手一个个掰关节。真正的机器人该听"去抓杯子"这种指令,而不是"joint2 转到 -0.3"。把"目标"翻译成"每个关节转多少度"、还顺带绕开障碍,正是 MoveIt2 的活儿。
MoveIt2 快速入门
什么是 MoveIt2?
MoveIt2 是 ROS 2 中最主流的运动规划框架。把它想成机械臂的"教练兼裁判":你只说"把末端移到杯子旁边",它负责算出怎么绕开障碍、走哪条平滑路线,还会一边走一边盯着别撞上——你完全不用自己算每个关节该转多少度。 它提供:
- 运动规划:RRT*、RRT-connect、PRM 等算法
- 碰撞检测:基于 FCL 的实时碰撞检查
- 轨迹执行:平滑轨迹插值
- 逆运动学:KDL / IKFast 求解器
- 抓取规划:Pick & Place 流程
使用 MoveIt Setup Assistant
# 启动配置助手
ros2 launch moveit_setup_assistant setup_assistant.launch.py
配置步骤:
- 加载 URDF:选择
urdf/my_robot.urdf.xacro - 自碰撞矩阵:自动生成,检查并排除不需要的碰撞对
- 规划组:创建
arm组,包含joint1、joint2、joint3 - 机器人位姿:定义 Home、Ready 等命名姿态
- 末端执行器:配置
gripper组 - 控制器:绑定
forward_position_controller - 生成配置:输出到
config/moveit/
启动 MoveIt2 + Gazebo 联合仿真
# 一键启动 MoveIt2(自动启动 RViz2 + Controller)
ros2 launch my_robot moveit.launch.py
在 RViz2 中可以:
- 拖拽末端执行器进行运动规划预览
- 点击 Plan & Execute 执行规划轨迹
- 查看 碰撞检测结果(红色表示碰撞)
- 使用 MotionPlanning 面板切换不同规划算法
Python 运动规划脚本
下面这段脚本把前面所有东西串成一次完整的"指哪打哪"。它在做什么:先初始化 MoveIt2,告诉机械臂"从当前姿势出发,目标关节角是 [0.5, -0.3, 0.8]",再让它规划一条不撞墙的路并实际执行。跑起来你会看到:RViz2 里机械臂先亮出一条半透明预览轨迹,再真正动起来;如果规划失败,终端会打印 Planning failed。
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from moveit.planning import MoveItPy
def main():
rclpy.init()
node = Node("moveit_example")
# 初始化 MoveIt2
moveit = MoveItPy(node=node)
arm = moveit.get_planning_component("arm")
# 设置起始状态和目标姿态
arm.set_start_state_to_current_state()
arm.set_goal_state(configuration=[0.5, -0.3, 0.8])
# 规划并执行
plan_success, plan = arm.plan()
if plan_success:
arm.execute(plan)
print("Motion executed successfully!")
else:
print("Planning failed!")
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
机器人能动、能规划了,但屏幕里一团线你看不清哪根连杆在哪——RViz2 和 rqt 就是帮你"看见"这一切的仪表盘。
调试与可视化工具
RViz2
# 启动 RViz2(带机器人模型)
rviz2
RViz2 是 ROS 2 的 3D 可视化工具,支持:
- 显示 URDF 机器人模型
- TF 坐标变换树
- 激光雷达 / 点云 / 图像数据
- MoveIt2 运动规划轨迹
- 导航路径
rqt 工具集
rqt # 启动完整 rqt 界面
rqt_graph # 查看节点通信图
rqt_topic # 实时话题监控
rqt_service_caller # 服务调用工具
rqt_controller_manager # 控制器管理 GUI
下一步
这篇跑通的是「最小仿真闭环」:模型能加载、控制器能转、规划能执行。但它是「开环」的——仿真世界里机器人只是照命令动,还没有感知(不认得障碍)、没有真机(不用管延迟和噪声)。往深走有两条路:
- 把模型做准:本文 URDF 只写了 visual,惯性参数全是默认值——仿真行为会和真机对不上。进阶篇从 CAD 到数字孪生:URDF 建模与惯性参数校准专治这个。
- 把控制做实:想一套代码在仿真和真机间无缝切换,看ros2_control 硬件在环:仿真与实机无缝切换——本文的
gazebo_ros2_control插件换一个实现,代码一行不改就能驱动真电机。
系列下一篇外骨骼机器人技术概述则从仿真进入应用领域,看这些技术怎么组装成一台真正穿在人身上的机器人。