1.1 什么是Gazebo

Gazebo机器人专用的 3D 物理仿真平台,能加载 URDF/SDF 机器人模型,并结合 ROS/ROS2 实现 虚拟机器人测试。
Gazebo在虚拟环境中搭建机器人、传感器和场景,用物理引擎(如 ODE、Bullet、DART)来计算运动学、动力学、碰撞检测。

1.1 Gazebo 的功能

  • 物理仿真
    • 支持重力、摩擦力、碰撞、关节约束
    • 可以真实地模拟机器人在环境中的运动
  • 传感器仿真
    • 提供虚拟的 激光雷达 (LiDAR)、相机 (Camera)、深度相机 (RGB-D)、IMU、GPS 等
    • 模拟真实传感器的数据输出,方便算法测试
  • 机器人建模
    • 通过 URDF/Xacro/SDF 文件导入机器人模型(如 TurtleBot、机械臂、无人机等)
  • 环境建模
    • 可以创建复杂的场景(建筑物、地形、障碍物等),进行导航和 SLAM 测试
  • 与 ROS 集成
    • 可以把 Gazebo 作为 虚拟机器人实验环境
    • ROS 节点可以直接订阅/发布来自 Gazebo 的话题(例如 /cmd_vel 控制小车,/scan 获取激光数据)

1.2 为什么要用 Gazebo?

  • 节省成本:不用真实机器人也能开发/调试算法
  • 安全:在虚拟环境中调试复杂动作,避免损坏真实硬件
  • 快速迭代:更容易测试不同的机器人参数、传感器配置

1.3 硬件要求

由于3D 仿真,一般需要在带有英伟达显卡的X86 机器上运行 才能满足性能。官方要求 :Ampere或更高版本的NVIDIA GPU架构,配备8GB或更高显示RAM。

我们先从安装演示包及其依赖项开始。

sudo apt install ros-humble-urdf-sim-tutorial

或者该使用github 仓库https://github.com/ros/urdf_sim_tutorial/tree/ros2代码。

2 从一个简单的实例开始

2.1 将一个“死”的模型加载进在Gazebo 中

我们可以使用 gazebo.launch.py 将已创建的模型生成到 Gazebo 中。

ros2 launch urdf_sim_tutorial gazebo.launch.py

这个启动文件会:

  • 启动 Gazebo 仿真服务
  • 读取 URDF/Xacro 文件,生成 robot_description 参数,提供给仿真/可视化节点使用。
  • 订阅 /robot_description 参数,在 Gazebo 生成机器人实体。
    默认情况下,Gazebo GUI 也会显示,其界面如下:
    在这里插入图片描述
    然而,它什么也没做,而且缺少很多 ROS 使用这个机器人所需的关键信息。这只是一个“” 模型放进了Gazebo中。 它并不能和ROS 进行交互。

目前,Gazebo 只会显示机器人模型,但:

  • 不会自动移动关节
  • 不会发布传感器数据
  • ROS 无法控制机器人

要让机器人可以与您和 ROS 进行交互,我们需要指定两样东西:插件(Plugins)控制器(Controllers)

2.1 Gazebo Plugins(插件)

在 Gazebo 中,插件是用于 扩展机器人模型功能的程序模块。插件可以:

  • 控制关节运动
  • 发布传感器数据(如激光雷达、IMU、相机)
  • 响应 ROS 消息或服务
  • 添加自定义逻辑(例如碰撞检测、控制算法)

插件是仿真里的“虚拟硬件驱动”,负责把物理动作/传感器数据暴露给 ROS。
插件就是“Gazebo/机器人与 ROS 之间的桥梁”,实现交互功能。
例如,常见插件, gazebo_ros_control:让机器人关节可以被 ROS 控制。 gazebo_ros_camera:发布相机图像到 ROS topic。 gazebo_ros_joint_state_publisher:发布关节状态。

2.1.1 gazebo_ros_control

gazebo_ros_control 是 ROS 2 中的一个 桥接插件,作用是将 ROS2世界 (ROS 2 中运行的 ros2_control 控制器)与 Gazebo 世界的仿真机器人连接起来,实现仿真中的机器人关节控制和状态反馈。
它主要解决两个问题:

  • 仿真控制:在 Gazebo 中控制机器人关节(位置、速度、力/扭矩)。
  • 状态反馈:把仿真机器人关节状态(位置、速度、力)发布到 ROS 2,使控制器能读取。

gazebo_ros_control就是一个库, 这个库就像一个“翻译官”,把 ROS 发出的命令(比如移动关节、读取传感器)翻译成 Gazebo 能理解的动作。

2.1.2 如何申明使用gazebo_ros_control 插件?

要使用gazebo_ros_control 插件,需要在URDF/Xacro 中进行申明。我们在 URDF 文件中 紧接着关闭的 标签之前,添加以下内容:

<!-- 用于告诉 Gazebo 仿真器:这个 URDF 模型要加载额外的 Gazebo 插件。-->
<gazebo>
  <!-- filename指定要加载的 插件库文件,这里是 Gazebo 提供的 ROS 2控制插件。-->
  <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
  <!--
  1.指定gazebo_ros2_control插件加载的YAML 配置文件	
  2. 让 GazeboSystem 知道如何按照 ROS 2 控制逻辑去控制机器人模型-->
    <parameters>$(find urdf_sim_tutorial)/config/09a-minimal.yaml</parameters>
  </plugin>
</gazebo>

任何在 内部声明的插件都会在 Gazebo 启动模型时自动加载

插件加载的YAML 配置文件本身描述的是 ros2_control (后面会讲)的 ControllerManager 配置,包括:

  • 控制器类型(JointTrajectoryController、JointStateBroadcaster 等)
  • 控制哪些关节
  • 命令接口 / 状态接口
  • 更新频率

它不是 Gazebo 的插件配置,Gazebo/插件只负责读取和加载,但并不解析具体控制逻辑。

gazebo_ros2_control 的角色

gazebo_ros2_control 插件做的事情可以理解为:

  1. 加载启动 ControllerManager
    • 插件在加载时会启动(不是创建) ROS 2 的 ControllerManager 节点(这是 ros2_control 的核心节点)
    • ControllerManager 本身负责解析 YAML,创建控制器实例
  2. 绑定仿真关节
    • 插件把 ControllerManager 创建的控制器Gazebo 仿真关节绑定。
    • 插件在 Gazebo 仿真循环中把控制器命令作用到关节,并把状态回写给 ControllerManager。
  3. 控制器仍然由 ros2_control 管理,插件不直接创建控制器

其简化流程大致如下:

[Gazebo 启动][插件加载] 
      ↓
(启动 ControllerManager 节点) ← 插件做的事
      ↓
(ControllerManager 根据 YAML 创建控制器实例) ← ros2_control 做的事
      ↓
(控制器命令 → 插件 → 仿真关节) 
      ↑
(仿真状态 → 插件 → 控制器 → ROS 2 topic)

2.2 Controllers(控制器)

前面讲了Controllers 是由ControllerManager 根据gazebo_ros2_control 传入的yaml 创建的。而ControllerManager 又是ros2_control核心节点。 所以我们先来了解ros2_control。

2.2.1 ros2_control

ros2_control 是 ROS 2 官方的硬件抽象层(Hardware Abstraction Layer, HAL)和控制框架,它的作用是:

  • 统一接口:让控制器(controllers)可以用同样的方式操作机器人关节,不管底层是 真实硬件 还是 仿真环境
  • 模块化控制器:你可以加载不同类型的控制器,比如:
    • JointStateBroadcaster(读取关节状态)
    • PositionJointController(位置控制)
    • VelocityJointController(速度控制)
    • EffortJointController(力矩控制)
ros2_control中的核心概念&架构
          ┌─────────────┐
          │ ROS 2 Nodes │
          └─────┬───────┘
                │ ROS 2 topics / actions
                v
       ┌───────────────────┐
       │ ControllerManager  │  ← 核心节点
       └───┬───────────────┘
           │ manages
           v
    ┌─────────────┐    ┌─────────────┐
    │ Controller 1│    │ Controller 2│
    └─────┬───────┘    └─────┬───────┘
          │                 │
          │ commands        │ commands
          v                 v
   ┌─────────────────────────────────┐
   │ Hardware Interface (Abstract HW)│
   │ - position, velocity, effort    │
   │ - reads joint states            │
   └─────────────────────────────────┘
          │
          v
    ┌───────────────┐
    │ Robot Hardware│
    └───────────────┘

概念详解如下:

  • ControllerManager
    • 创建/启动/停止控制器
    • 调度控制循环(根据 update_rate)
    • 提供 ROS 2 接口:Topic/Service/Action
  • Controllers
    • 类型:
      • ointTrajectoryController:轨迹控制
      • JointPositionController:单关节位置控制
      • JointVelocityController:速度控制
      • JointEffortController:力/扭矩控制
      • JointStateBroadcaster:发布关节状态到 /joint_states
    • 每个控制器需要知道:
      • 哪些关节被控制
      • 命令接口类型
      • 状态接口类型
  • Hardware Interface
    • 抽象硬件 API,必须实现:
      • read():读取关节状态
      • write():写入控制命令
    • 常用实现:
      • 仿真: GazeboSystem是由 gazebo_ros2_control 插件提供的一个类,实现了 ros2_control 的 HardwareInterface 接口:
        • read():从 Gazebo 里读取当前关节角度、速度
        • write():把控制器输出的命令写入 Gazebo 仿真模型
      • 真实硬件:基于 EtherCAT, serial, CAN 总线等
  • Robot Hardware
    • 实际机器人或仿真关节
  • 前面讲的 插件 就是ros2_control 的一个插件
    • gazebo_ros2_control 是 ros2_control 的一个 “硬件接口插件(Hardware Interface Plugin)”。

2.2.2 如何申明使用ros2_control?

和gazebo_ros2_control 申明方式一样,一般需要在 URDF 文件中 紧接着关闭的 标签之前。通常在紧贴 之前。其格式如下:

<!-- 声明这个机器人模型要使用 ROS 2 Control。
GazeboSystem:给硬件接口/控制系统起名
类型为 “system”,通常用于 Gazebo 仿真系统级别的硬件接口-->
<ros2_control name="GazeboSystem" type="system">
  <!-- 硬件接口-->	
  <hardware>
  	<!-- 使用 Gazebo 插件GazeboSystem作为硬件接口。GazeboSystem是  	gazebo_ros2_control 插件提供的 Hardware Interface 实现。
  	gazebo_ros2_control/GazeboSystem让 Gazebo 模拟的机器人「假装」成真实硬件。-->
    <plugin>gazebo_ros2_control/GazeboSystem</plugin>
  </hardware>
 <!--  声明了一个机器人关节(例如头部旋转关节)将由此控制系统管理。-->
  <joint name="head_swivel" />
</ros2_control>

gazebo_ros2_control 插件创建并启动 ControllerManager。 在 ControllerManager 初始化过程中,ros2_control 会解析 <ros2_control> 标签并动态加载 hardware_interface 插件(也就是 gazebo_ros2_control/GazeboSystem)。

2.2.3 ros2_control 配置YAML

章节2.2.1 已经描述过,gazebo_ros2_control 插件加载了这个配置yaml, 传递给ControllerManager(ros2_control)用的配置:告诉它 update_rate、要创建哪些控制器(类型、绑定哪些关节、需要哪些接口),以及硬件接口(hardware)需要的参数。我们来先看一个demo:

controller_manager: #固定键(ControllerManager 的参数命名空间)
  ros__parameters: #符合 ROS 2 参数声明风格
    update_rate: 100 #控制循环频率(Hz),ControllerManager 调度控制回路的频率。
    # 控制器(controllers) ...
    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster
      joints: [joint1, joint2]
    my_position_controller:
      type: joint_trajectory_controller/JointTrajectoryController
      joints: [joint1, joint2]
      command_interfaces: [position]
      state_interfaces: [position]
update_rate

控制循环频率(read → update controllers → write)

  • 类型:整数(Hz)
  • 建议:仿真常用 100,速度控制或高频控制可能需要更高。

2.2.3 控制器( Controller)

每个控制器以一个 key(名字)出现,其值包含若干字段:

  • type:控制器类型(package/ControllerClass,例如 joint_state_broadcaster/JointStateBroadcaster 或 joint_trajectory_controller/JointTrajectoryController)。
  • **joints:控制的关节列表(数组)**这些关节的名称 必须与 URDF 模型中定义的 joint 名称完全一致。
  • command_interfaces:控制命令接口(如 position, velocity, effort)。
  • state_interfaces:状态接口(如 position, velocity, effort)。
  • 其它 controller 特定参数(见具体控制器文档)。
控制器如何使用这些 joints

控制器类型不同,处理方式也不同,但逻辑一致:

控制器类型 作用 joints 的意义
JointStateBroadcaster 读取状态并发布 /joint_states 从 hardware 读取这些关节的位置、速度、力矩
JointTrajectoryController 接收轨迹命令并控制关节位置/速度 控制这些关节的运动
EffortController 发送力矩命令 对这些关节施加 effort
ForwardCommandController 直接写入命令 把 command 写入这些 joints 对应的接口

我们给出一个样例,两个关节的控制器:

arm_controller:
  # 这是一个 位置控制器。
  type: joint_trajectory_controller/JointTrajectoryController
  joints:
   # 它控制 shoulder_joint 和 elbow_joint 两个关节。
    - shoulder_joint
    - elbow_joint
  # 它会向硬件(或仿真中的 GazeboSystem)发送位置命令。
  command_interfaces: [position]
  #控制器需要从硬件接口中读取的 位置 和 速度 状态。
  state_interfaces: [position, velocity]

画成图如下:

                      ┌──────────────────────────────┐
                      │        Gazebo (仿真世界)      │
                      │   模拟机械臂、物理动力学         │
                      └──────────────┬───────────────┘
                                     │
                                     │ 提供/接收关节状态与命令
                                     │
                      ┌──────────────▼───────────────┐
                      │ gazebo_ros2_control 插件层   │
                      │ plugin: gazebo_ros2_control/ │
                      │          GazeboSystem        │
                      │  ←  实现 Hardware Interface  │
                      └──────────────┬───────────────┘
                                     │
                state_interfaces:    │   command_interfaces:
                position, velocity   │   position
                                     │
       ┌─────────────────────────────┴──────────────────────────┐
       │               ros2_control Controller Manager          │
       │--------------------------------------------------------│
       │  # Controller                                                      │
       │  arm_controller (JointTrajectoryController)             │
       │   ├─ joints: shoulder_joint, elbow_joint               │
       │   ├─ 从 GazeboSystem 读取状态 (position, velocity)     │
       │   └─ 发送控制命令 (position) 到 GazeboSystem           │
       │                                                        │
       └────────────────────────────────────────────────────────┘
                                     │
                                     │ ROS 2 话题接口
                                     │  (发布状态 / 订阅轨迹)
                                     ▼
                      ┌───────────────────────────────────┐
                      │       ROS 2 Topics 层             │
                      │  /joint_states (发布状态)         │
                      │  /arm_controller/joint_trajectory │
                      │    (接收 MoveIt / Planner 轨迹)   │
                      └───────────────────────────────────┘

其数据流如下运行:

  1. Controller Manager (简称CM) 定时调用每个 Controller 的 update()。
  2. Controller 从 HardwareInterface 获取关节状态(state_interfaces)。
  3. Controller 根据控制算法计算命令,写入 HardwareInterface(command_interfaces)。
  4. 硬件接口把命令作用到关节或 Gazebo 模型上。
  5. 下一次循环继续重复。

2.2.4 关节( Joint)

在 URDF(Unified Robot Description Format)中,joint 表示机器人中两个连杆(link)之间的连接关系。Gazebo 和 ros2_control 都通过它了解:

  • 这个关节的类型(转动、滑动等);
  • 它的运动范围(最大/最小角度或位移);
  • 控制方式(由谁来驱动、是否受力限制等)。

我们已经研究了如何在URDF 中定义一个关节,这里不再赘述。但是ros2_control并不知道这些信息。ros2_control不能直接从 URDF 自动读取所有关节,虽然可以解析 URDF 获取基本信息,但它只使用 <ros2_control> 标签中指定的硬件接口和关节进行控制。 所以,我们必须要在 <ros2_control> 中声明我们使用的关节

声明我们要使用的关节( Joint)

在 URDF 中这样声明:

<ros2_control name="GazeboSystem" type="system">
  <hardware>
    <plugin>gazebo_ros2_control/GazeboSystem</plugin>
  </hardware>
  <joint name="head_swivel" />
</ros2_control>

<ros2_control> 中的 <joint> 不是重新定义(之前在URDF的其它地方已经被定义),而是告诉 ros2_control 我要控制这个关节。

<joint> 表示 Hardware Interface 会管理这个关节的状态接口(position/velocity/effort)以及命令接口(position/velocity/effort)。可以在 内显式指定接口:

<joint name="head_swivel">
  <command_interface name="position"/>
  <state_interface name="position"/>
  <state_interface name="velocity"/>
</joint>
  • command_interface:控制器发送命令的接口
  • state_interface:控制器读取状态的接口
  • 如果省略,ros2_control 会使用默认接口。
常见的命令&状态接口

position、velocity、effort 等 就是约定好的标准名字控制器和 Hardware Interface 都知道这些名字代表什么
command_interface(控制命令接口)
用于向硬件或仿真关节发送命令。常见的有:

接口名称 说明 常用场景
position 控制关节目标位置(角度或位移) JointPositionController, JointTrajectoryController
velocity 控制关节速度 VelocityJointController
effort / torque 控制关节力矩或力 EffortJointController, 力控
position_raw 原始位置命令,绕过内部滤波 高级应用,通常不常用
velocity_raw 原始速度命令 高级应用
effort_raw 原始力矩/力命令 高级应用

state_interface(状态接口)
用于让控制器读取关节当前状态。常见的有:

接口名称 说明 常用场景
position 当前关节位置 所有位置控制器都需要
velocity 当前关节速度 轨迹控制、速度控制器
effort / torque 当前关节力矩或力 力控、PID 控制反馈
acceleration 当前关节加速度 高级控制器或动力学估计
temperature 关节/电机温度 安全监控
current 电机电流 安全监控或高级控制

2.3 将Gazebo中的机器人“活”起来 。

有了以上 插件控制器 的概念后, 让我们继续2.1 章节的机器人的例子。要让Gazebo中的机器人 能动起来, 就需要将这个机器人和ROS2 的世界连接起来。 就需要我们上面提到两个概念。

2.3.1 将ROS2 世界和 Gazebo 世界连接起来

根据前面的概念, 为了让 Gazebo 机器人能按照预期的行为动作, 就需要ros2_control(ROS2 世界) + 它的插件gazebo_ros2_control (gazebo 世界)来完成对应动。我们需要在URDF 中来声明它。我们在 URDF 文件中 紧接着关闭的 标签之前,添加以下内容:

<ros2_control name="GazeboSystem" type="system">
  <hardware>
    <plugin>gazebo_ros2_control/GazeboSystem</plugin>
  </hardware>
  <joint name="head_swivel" />
</ros2_control>

<gazebo>
  <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
    <parameters>$(find urdf_sim_tutorial)/config/09a-minimal.yaml</parameters>
  </plugin>
</gazebo>

其中09a-minimal.yaml 的内容如下

controller_manager:
  ros__parameters:
    update_rate: 100

使用以下命令运行:

ros2 launch urdf_sim_tutorial 09a-minimal.launch.py

发现和死的差别并不大,因为这个yaml 并没有定义controller,也就没有controller 被启动。只是启用gazebo_ros2_control 插件。

通过给URDF 中添加以上内容,我们就将ROS2 世界和 Gazebo 世界连接起来了。

2.3.2 添加一个Controller

将ROS2 世界和 Gazebo 世界连接起来了,但没有controller,就无法控制关节,获取关节的状态,Gazebo 机器人就无法动起来。我们添加一个Controller其内容如下:

controller_manager:
  ros__parameters:
    update_rate: 100
    use_sim_time: true

    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

通过以上配置文档,我们创建了一个joint_state_broadcaster 的manager。该manager从 gazebo_ros2_control/GazeboSystem 中读取这些关节的位置、速度、力矩, 并将其状态并发布 /joint_states 的ROS2 topic。 这样gazebo 世界中的关节状态就可以在ROS2 世界中订阅了。

使用以下命令启动:

ros2 launch urdf_sim_tutorial 09-joints.launch.py

使用ros2 topic 监听功能,查看对应的topic:

$ ros2 topic echo /joint_states
header:
  stamp:
    sec: 13
    nanosec: 331000000
  frame_id: ''
name: []
position: []
velocity: []
effort: []

哇, joint_state_broadcaster 这个controller 确实将关节的状态的发布到了ROS2 世界。但细心的你以定发现,这个里面竟然没有数据,为什么呢? 很简单,因为你的yaml 中并没有定义关节。所以控制器不能获取关节的状态,只能发布空消息。

2.3.2 添加一个关节(joint)

根据以前的概念, controller 的一个作用就是获取关节的状态,控制关节。
对于每一个非固定关节,我们需要在 ros2_control 标签中添加关于该关节的信息,以告诉它支持哪些接口。让我们从头部关节开始。将你的 URDF 中的关节标签修改为如下内容:

<ros2_control name="GazeboSystem" type="system">
  <hardware>
    <plugin>gazebo_ros2_control/GazeboSystem</plugin>
  </hardware>
  <joint name="head_swivel">
     <command_interface name="position" />
     <command_interface name="velocity" />
     <state_interface name="position"/>
     <state_interface name="velocity"/>
   </joint>
</ros2_control> 
<!-- 
* 注意,这里的关节名称应与标准 URDF <joint> 标签中的关节名称一致。
* 目前,我们先关注 state_interface,在这里我们指定希望发布该关节的位置和速度信息。-->

你可以使用我们之前的启动配置来运行这个 URDF。

ros2 launch urdf_sim_tutorial 09-joints.launch.py urdf_package_path:=urdf/10-firsttransmission.urdf.xacro

现在,头部已经能在 RViz 中正确显示了,因为头部关节已被包含在 joint_states 消息中。

$ ros2 topic echo /joint_states
header:
  stamp:
    sec: 4
    nanosec: 707000000
  frame_id: ''
name:
- head_swivel
position:
- -2.9051283156888985e-08
velocity:
- 7.575990694887896e-06
effort:
- .nan

2.3.2 定义关节控制(Joint Control)

在ros2_control 标签中声明关节后,如果没有在controller的YAML 里没有指定 joints: […]默认情况下,joint_state_broadcaster 依然会发布所有已注册关节的状态。已注册的关节指通过URDF中 <ros2_control> + 定义的 joints。
对于关节的state interface, controller 的 YAML 中不需要声明,Controller 会 自动读取硬件接口提供的所有 state interfaces。

但对于command interfacecontroller 的 YAML 中必须指定(同一个关节可能同时支持位置、速度、力矩控制,Controller 需要明确你想用哪一种),interface_name 指定的是 controller 向硬件发送命令的接口,也就是 command interface

接下来,我们就在yaml 中定义一个名为 head_controller 的“关节组位置控制器,来指定我要控制的关节。并且说明我们要控制的是位置(position)。

controller_manager:
  ros__parameters:
    update_rate: 100
    use_sim_time: true

    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

    head_controller:
      #这是一个 位置控制器,可同时控制一组关节的位置
      type: position_controllers/JointGroupPositionController
      joints: #要控制的关节
         - head_swivel
      interface_name: position

之所以能这样做,是因为在 URDF 文件里的 标签中,我们已经写了:

<command_interface name="position" />

这表示这个关节支持“位置控制接口”,也就是可以接收目标位置的命令(例如旋转到某个角度)。
使用以下命令运行:

$ ros2 launch urdf_sim_tutorial 10-head.launch.py

position_controllers/JointGroupPositionController

JointGroupPositionController 是 ROS 2 控制器(ros2_control)库提供的一个 位置控制器(position controller),它控制一组关节的 位置(position)。其有以下功能:

  • 接收ROS 2 目标关节位置控制消息
  • 从硬件接口读取关节的当前状态(position、velocity,可选 effort)
  • 根据控制算法(通常是 PID)计算命令(根据状态)
  • 将计算出的命令写入硬件接口

ROS 2 的话题的常见topic

功能 话题名 消息类型
单关节控制 /head_controller/commands std_msgs/msg/Float64MultiArray
多关节轨迹 /arm_controller/joint_trajectory trajectory_msgs/JointTrajectory

了解了以上信息,我们知道JointGroupPositionController 订阅一个topic :/head_controller/commands 来接收position 。我们可以通过在 ROS 中发布一个值来控制机器人的头部位置。

ros2 topic pub /head_controller/commands std_msgs/msg/Float64MultiArray "data: [-0.707]"

当这个命令被发布时,关节的位置会立即变为指定的值。

至此,我们终于让 Gazebo中的机器人“活”起来了!!!

3. 控制多个关节与动作模仿

解下来,我们将使用类似的方法,修改夹爪(Gripper)关节的 URDF & ROS 参数,但在这种情况下,我们会将多个关节关联到同一个控制器。
URDF 的文件中添加以下内容:

<ros2_control name="GazeboSystem" type="system">
    <hardware>
      <plugin>gazebo_ros2_control/GazeboSystem</plugin>
    </hardware>
    <joint name="head_swivel">
      <command_interface name="position" />
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
    </joint>
    <!-- 夹爪(gripper)伸缩 机构的关节-->
    <joint name="gripper_extension">
      <command_interface name="position" />
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
    </joint>
    <!-- 夹爪左指头关节-->
    <joint name="left_gripper_joint">
      <command_interface name="position" />
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
    </joint>
    <!-- 夹爪右指头关节-->
    <joint name="right_gripper_joint">
      <command_interface name="position" />
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
    </joint>
  </ros2_control>

  <gazebo>
    <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
      <parameters>$(find urdf_sim_tutorial)/config/gripper.yaml</parameters>
    </plugin>
  </gazebo>

gripper.yaml 描述如下:

controller_manager:
  ros__parameters:
    update_rate: 100
    use_sim_time: true

    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

    head_controller:
      type: position_controllers/JointGroupPositionController

    gripper_controller:
      type: position_controllers/JointGroupPositionController

head_controller:
  ros__parameters:
    joints:
    - head_swivel
    interface_name: position

gripper_controller:
  ros__parameters:
    joints:
    # 多个关节关联到同一个控制器
    - gripper_extension
    - left_gripper_joint
    - right_gripper_joint
    interface_name: position

启动机器人:

ros2 launch urdf_sim_tutorial 12-gripper.launch.py

3.1 如何控制gripper 呢?

首先我们来看下这个消息类型结构:std_msgs/msg/Float64MultiArray
Float64MultiArray是 ROS 2 的一个标准消息类型,用于发送多维浮点数组

$ ros2 interface show std_msgs/msg/Float64MultiArray
MultiArrayLayout  layout        # specification of data layout
	MultiArrayDimension[] dim #
		string label   #
		uint32 size    #
		uint32 stride  #
	uint32 data_offset        #
float64[]         data          # array of data
  • layout:描述数组的多维结构(行列数、步长等),一般在一维数组时可以忽略
  • data:真正的浮点数数组内容。

给出一个样例, data: [0.0, 0.5, 0.5] ,该data是一个长度为 3 的浮点数组,对应三个要控制的 关节命令值。根据上面的 URDF:

<joint name="gripper_extension" ... />
<joint name="left_gripper_joint" ... />
<joint name="right_gripper_joint" ... />

通常这三个浮点值分别对应于这三个关节的目标位置(position command),
顺序取决于 YAML 控制器配置文件(gripper.yaml)中 joints: 的定义。
根据上面的gripper.yaml 中的:

gripper_controller:
  type: position_controllers/JointGroupPositionController
  joints:
    - gripper_extension
    - left_gripper_joint
    - right_gripper_joint

那么就表示:

数组索引 关节名称 命令值 物理含义
0 gripper_extension 0.0 不伸出(夹爪底座缩回)
1 left_gripper_joint 0.5 左夹爪张开 0.5 弧度
2 right_gripper_joint 0.5 右夹爪张开 0.5 弧度

所以gripper_controller 中 有几个joints, Float64MultiArray的data 长度就由几个

让我们执行以下命令,让 爪夹 张开并伸出

ros2 topic pub /gripper_controller/commands std_msgs/msg/Float64MultiArray "data: [0.4, 0.5, 0.5]"

闭合并收回:

ros2 topic pub /gripper_controller/commands std_msgs/msg/Float64MultiArray "data: [-0.4, 0.0, 0.0]"

我们始终希望左夹爪关节(left_gripper_joint)和右夹爪关节(right_gripper_joint)保持相同的数值。
通过以下几个步骤,我们可以在 URDF 和 控制器 中实现这种“联动(mimic)”关系。

  • 在 URDF 中的 right_gripper_joint 定义里插入:
<param name="mimic">left_gripper_joint</param>
  • 在 ros2_control 的 right_gripper_joint 接口定义中加入:
<param name="mimic">left_gripper_joint</param>
  • 在新的控制参数文件中(例如控制器的 YAML 配置)
    • 只需在夹爪控制器下列出 两个关节(例如 gripper_extension 和 left_gripper_joint),
      不再列出 right_gripper_joint,因为它会自动跟随左夹爪关节的动作。

其URDF XML 如下:

 <xacro:gripper prefix="right" reflect="-1">
     <mimic joint="left_gripper_joint"/>
  </xacro:gripper>
  ....
  <ros2_control name="GazeboSystem" type="system">
    ......
    <joint name="right_gripper_joint">
      <command_interface name="position" />
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
      <param name="mimic">left_gripper_joint</param>
      <param name="multiplier">1</param>
    </joint>
  </ros2_control>

yaml 配置如下:

.....
gripper_controller:
  ros__parameters:
    joints:
    - gripper_extension
    - left_gripper_joint
    interface_name: position

启动命令如下:

ros2 launch urdf_sim_tutorial 12-gripper.launch.py urdf_package_path:=urdf/12a-mimic-gripper.urdf.xacro

现在我们只需要使用两个数值就可以控制夹爪了,例如:

ros2 topic pub /gripper_controller/commands std_msgs/msg/Float64MultiArray "data: [0.0, 0.5]"

4. 让机器人上的轮子转起来

要让机器人移动起来,我们首先必须在 URDF 文件的 <ros2_control> 标签中为四个轮子分别指定更多的接口,不过这次只需要 速度(velocity)控制接口。

 <ros2_control name="GazeboSystem" type="system">
    ....
    <joint name="left_front_wheel_joint">  <!-- 左前轮-->
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
    </joint>
    <joint name="left_back_wheel_joint"> <!-- 左后轮-->
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
    </joint>
    <joint name="right_front_wheel_joint"> <!-- 右前轮-->
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
    </joint>
    <joint name="right_back_wheel_joint"> <!-- 右后轮-->
      <command_interface name="velocity" />
      <state_interface name="position"/>
      <state_interface name="velocity"/>
    </joint>
  </ros2_control>

当然,我们可以为每个轮子分别定义控制器,但那样就太繁琐了,也没什么意思。相反,我们希望能够同时控制所有轮子。

为此,我们需要在 ROS 中配置更多参数,以便使用 DiffDriveController(差速驱动控制器) —— 它会订阅标准的 Twist 类型的 /cmd_vel 消息,并根据该消息驱动机器人运动

4.1 Differential Drive Controller

在讲解yaml 之前, 我们先来看差速驱动控制器, 它的主要作用是:

  • 接收速度指令(如ROS2 topic /cmd_vel 的线速度与角速度);
  • 计算出两侧驱动轮的转速
  • 将速度命令发布到各轮的关节接口
  • 同时通过编码器反馈计算里程(odometry);
  • 发布 TF(odom → base_link)和 /odom 话题。

这类控制器非常常见于:

  • 移动机器人底盘(AGV、小车);
  • 双轮差速驱动平台;
  • 两轮+万向轮结构(如 TurtleBot)。

控制器运行逻辑如下图:

/cmd_vel   ─────┐
                │
                ▼
        +------------------+
        | DiffDriveController |
        +------------------+
           │         │
           ▼         ▼
   left_wheel   right_wheel
      joint         joint
4.1.1关节声明
left_wheel_names:  [left_front_wheel_joint, left_back_wheel_joint]
right_wheel_names: [right_front_wheel_joint, right_back_wheel_joint]

对于其它控制器,joint 都是独立控制的, 所以使用joint 来描述。对于 DiffDriveController,左右两侧车轮是成组控制的,控制器内部会自动

  • 把左右轮的速度平均
  • 计算里程
  • 推导出机器人整体的 linear.x 与 angular.z
  • 再通过几何模型反推出每个轮子的速度

所以它必须区分「左轮组」和「右轮组」。 控制器会自动认为:

  • 左前轮和左后轮速度相同(平均处理);
  • 右前轮和右后轮速度相同;
  • 最终控制器只输出两个命令:
    • 左轮组转速
    • 右轮组转速

4.1.2 几何参数

wheel_separation: 0.44    # 左右轮中心的距离 (m)
wheel_radius: 0.035       # 轮半径 (m)

wheel_separation这是机器人两个驱动轮中心点之间的直线距离。你可以简单地理解成两个轮子之间的“轴距”在宽度方向上的体现。
wheel_radius这是机器人驱动轮的半径

我们定义:
v: 机器人本体的线速度(前进/后退速度,单位:米/秒)
ω: 机器人本体的角速度(转弯速度,单位:弧度/秒)
v_left: 左轮的线速度(单位:米/秒)
v_right: 右轮的线速度(单位:米/秒)
r = wheel_radius = 0.035 m
l = wheel_separation = 0.44 m

计算机器人线速度 v:

v = (v_right + v_left) / 2

机器人的整体前进速度是两个轮子速度的平均值。

计算机器人角速度 ω:

ω = (v_right - v_left) / l

机器人的转弯速度是由两个轮子的速度差决定的,而这个速度差的效果会被轮间距 l 放大或缩小。轮间距 l 越大,产生同样的角速度就需要越大的速度差。

由此可见,wheel_separation 和wheel_radius, 是计算机器人各种速度的重要参数。

4.1.3 话题发布与帧设置

publish_rate: 50.0           # 控制器发布数据的频率为每秒50次
base_frame_id: base_link     # TF 的基准坐标系
use_stamped_vel: false       # 是否用带时间戳的速度命令

base_frame_id 指定了控制器和里程计计算的参考框架。所有的运动和里程计数据都将相对于这个坐标系来发布。

map (世界固定坐标系)
│
└── odom (里程计坐标系)
    │
    └── base_link (机器人本体坐标系) ← 就是这个!
        │
        ├── laser_link (激光雷达坐标系)
        ├── camera_link (摄像头坐标系)
        ├── imu_link (IMU坐标系)
        └── wheel_left_link (左轮坐标系)

4.1.4 速度与加速度限制

这些限制主要用于:

  • 保护机器人安全(不会突然加速或旋转过快)
  • 仿真中保持物理合理性
  • 与导航栈或控制算法一致
linear:
  x:
    has_velocity_limits: true
    max_velocity: 0.2
    min_velocity: -0.2
    has_acceleration_limits: true
    max_acceleration: 0.6
angular:
  z:
    has_velocity_limits: true
    max_velocity: 2.0
    min_velocity: -2.0
    has_acceleration_limits: true
    max_acceleration: 6.0
    min_acceleration: -1.0

线速度(linear)

  • linear:线性速度,通常只有 x 方向(前进/后退)有效。
  • has_velocity_limits:是否启用速度限制。
    • true 表示会限制机器人前进和后退速度。
  • max_velocity:允许的最大前进速度(m/s)。
    • 这里是 0.2 m/s,机器人前进不会超过 0.2 m/s。
  • min_velocity:允许的最大后退速度(负值,m/s)。
    • 这里是 -0.2 m/s,机器人后退不会超过 0.2 m/s。
  • has_acceleration_limits:是否启用加速度限制。
    • true 表示加速度会被限制。
  • max_acceleration:最大线加速度(m/s²)。
    • 这里是 0.6 m/s²,机器人加速或减速不会超过 0.6 m/s²。
  • 注意:线加速度没有 min_acceleration,通常是对称的,正负方向都限制为 0.6 m/s²。

角速度(angular)

  • angular:角速度,通常只有 z 方向有效(绕垂直轴旋转,即转向)。
  • z:绕 z 轴的旋转速度限制。
  • has_velocity_limits:是否启用角速度限制。
  • max_velocity:最大顺时针旋转速度(rad/s)。
    • 这里是 2.0 rad/s。
  • min_velocity:最大逆时针旋转速度(负值,rad/s)。
    • 这里是 -2.0 rad/s。
  • has_acceleration_limits:是否启用角加速度限制。
  • max_acceleration:最大角加速度(rad/s²)。
    • 这里是 6.0 rad/s²,表示机器人旋转加速不会超过这个值。
  • min_acceleration:最小角加速度(负方向,rad/s²)。
    • 这里是 -1.0 rad/s²,表示旋转减速时的限制不同于加速。
  • 注意:角加速度可以不对称,这里设置了正向和反向不同的限制。

4.1.5 里程计与噪声建模

ROS 中的里程计(nav_msgs/Odometry)消息包含两个关键部分:

  • pose:机器人在地图或基座坐标系下的位置和姿态
    • 类型:geometry_msgs/Pose
  • twist:机器人在自身坐标系下的线速度和角速度
    • 类型:geometry_msgs/Twist
      每个部分都可以有 协方差矩阵,用来表示测量误差或不确定性。
  • 协方差矩阵是 6×6 的矩阵,对应 [x, y, z, roll, pitch, yaw]
  • ROS 里通常只配置对角线(非零的对角线),即 pose_covariance_diagonal 和 twist_covariance_diagon
pose_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.03]
twist_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.03]

长度 6 对应顺序:[x, y, z, roll, pitch, yaw]
pose_covariance_diagonal

位置 含义
x 0.001 x 方向位置的不确定性(m²)
y 0.001 y 方向位置的不确定性(m²)
z 0.001 z 方向位置的不确定性(m²)
roll 0.001 绕 x 轴旋转的不确定性(rad²)
pitch 0.001 绕 y 轴旋转的不确定性(rad²)
yaw 0.03 绕 z 轴旋转的不确定性(rad²)

差分驱动机器人主要在平面上移动,所以 x, y, yaw 最重要,roll/pitch/z 通常很小。这里 yaw 的不确定性比 roll/pitch 大,合理。

twist_covariance_diagonal

位置 含义
x 0.001 x 方向速度的不确定性(m²/s²)
y 0.001 y 方向速度的不确定性(m²/s²)
z 0.001 z 方向速度的不确定性(m²/s²)
roll 0.001 绕 x 轴角速度的不确定性(rad²/s²)
pitch 0.001 绕 y 轴角速度的不确定性(rad²/s²)
yaw 0.03 绕 z 轴角速度的不确定性(rad²/s²)

差分驱动机器人主要绕 z 轴旋转,所以 yaw 的误差最大,线速度 x/y 不确定性很小。

4.1.6 配置参数

diffdrive.yaml 内容如下

controller_manager:
  ros__parameters:
    update_rate: 100
    use_sim_time: true

    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

    head_controller:
      type: position_controllers/JointGroupPositionController

    gripper_controller:
      type: position_controllers/JointGroupPositionController

    diff_drive_base_controller:
      type: diff_drive_controller/DiffDriveController

head_controller:
  ros__parameters:
    joints:
    - head_swivel
    interface_name: position

gripper_controller:
  ros__parameters:
    joints:
    - gripper_extension
    - left_gripper_joint
    interface_name: position

diff_drive_base_controller:
  ros__parameters:

    publish_rate: 50.0

    left_wheel_names: [left_front_wheel_joint, left_back_wheel_joint]
    right_wheel_names: [right_front_wheel_joint, right_back_wheel_joint]

    wheel_separation: 0.44
    wheel_radius: 0.035

    # Odometry covariances for the encoder output of the robot. These values should
    # be tuned to your robot's sample odometry data, but these values are a good place
    # to start
    pose_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.03]
    twist_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.03]

    # Top level frame (link) of the robot description
    base_frame_id: base_link

    use_stamped_vel: false

    # Velocity and acceleration limits for the robot
    linear:
      x:
        has_velocity_limits: true
        max_velocity: 0.2              # m/s
        min_velocity: -0.2             # m/s
        has_acceleration_limits: true
        max_acceleration: 0.6          # m/s^2
    angular:
      z:
        has_velocity_limits: true
        max_velocity: 2.0              # rad/s
        min_velocity: -2.0
        has_acceleration_limits: true
        max_acceleration: 6.0          # rad/s^2
        min_acceleration: -1.0

运行如下命令

ros2 launch urdf_sim_tutorial 13-diffdrive.launch.py

除了加载上述配置外,这还会打开 RobotSteering 面板,使你能够操控 R2D2 机器人移动,同时观察它在 Gazebo 中的实际运动行为,以及在 RViz 中的可视化行为。
在这里插入图片描述

4.1.7 diff_drive_base_controller/cmd_vel

ros2 interface show geometry_msgs/msg/Twist
linear:
  x: float64   # 前后速度,单位 m/s
  y: float64   # 侧向速度,一般为 0
  z: float64   # 垂直速度,一般为 0
angular:
  x: float64   # 绕前后轴旋转,一般为 0
  y: float64   # 绕左右轴旋转,一般为 0
  z: float64   # 绕垂直轴旋转,单位 rad/s

前进 0.1 m/s:

ros2 topic pub /diff_drive_base_controller/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"

原地逆时针旋转 0.5 rad/s:

ros2 topic pub /diff_drive_base_controller/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.5}}"

前进并向右转:

ros2 topic pub /diff_drive_base_controller/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1}, angular: {z: -0.2}}"

恭喜!现在你已经可以使用 URDF 进行机器人仿真了。

Logo

立足具身智能前沿赛道,致力于搭建全球化、开源化、全栈式技术交流与实践共创平台。

更多推荐