发布于 

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 里看到这根蓝色三节棍。

<?xml version="1.0"?>
<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。先记住"声明接口"这件事——等下加载控制器时,名字得和它一一对上。

<?xml version="1.0"?>
<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

配置步骤:

  1. 加载 URDF:选择 urdf/my_robot.urdf.xacro
  2. 自碰撞矩阵:自动生成,检查并排除不需要的碰撞对
  3. 规划组:创建 arm 组,包含 joint1、joint2、joint3
  4. 机器人位姿:定义 Home、Ready 等命名姿态
  5. 末端执行器:配置 gripper 组
  6. 控制器:绑定 forward_position_controller
  7. 生成配置:输出到 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

下一步

这篇跑通的是「最小仿真闭环」:模型能加载、控制器能转、规划能执行。但它是「开环」的——仿真世界里机器人只是照命令动,还没有感知(不认得障碍)、没有真机(不用管延迟和噪声)。往深走有两条路:

系列下一篇外骨骼机器人技术概述则从仿真进入应用领域,看这些技术怎么组装成一台真正穿在人身上的机器人。

参考资料