ROS 2 Jazzy Gazebo仿真全指南:从静态模型到可控制移动机器人

从URDF补全到键盘遥控,掌握机器人仿真标准工程流程

恭喜你!通过前面的学习,你已经能写出一个完整的机器人URDF模型,并且能在RViz里看到它的结构和TF树了。但现在的机器人还只是一个"静态模型"——它没有质量、没有碰撞、不能运动。

这篇笔记将带你把这个静态模型变成一个"有血有肉"的机器人:让它在Gazebo物理仿真引擎里拥有重力、碰撞和运动能力,并且能用ROS 2的标准话题控制它。最后,我会给你一份清晰的ROS 2进阶学习路线图,告诉你学完基础后该往哪个方向走。

在这里插入图片描述

一、为什么需要Gazebo?RViz和Gazebo的分工

很多初学者都会问:我在RViz里已经能看到机器人模型和TF了,为什么还要用Gazebo?

这是一个非常好的问题。答案很简单:RViz是可视化工具,Gazebo是物理仿真引擎

工具 核心功能 特点
RViz 显示数据 只负责"看",不产生数据,没有物理
Gazebo 模拟物理世界 有重力、摩擦、碰撞、关节约束,能模拟硬件行为

通俗解释
把机器人开发比作开工厂:

  • RViz是工厂的监控大屏,它显示各个设备的状态,但不参与生产
  • Gazebo是真实的工厂车间,机器在这里运转,产品在这里生产
  • ROS 2是连接大屏和车间的通信系统

所以不是"RViz vs Gazebo",而是"Gazebo + RViz"配合使用:Gazebo负责模拟物理世界和硬件,产生传感器数据和关节状态;RViz负责把这些数据可视化,方便你调试。

1.1 安装与启动Gazebo

ROS 2 Jazzy默认使用Gazebo Harmonic版本。安装ROS-Gazebo集成包:

sudo apt install ros-jazzy-ros-gz

启动空的Gazebo世界:

# 方式1:直接启动Gazebo
gz sim empty.sdf -r

# 方式2:用ROS launch启动(推荐,集成更方便)
ros2 launch ros_gz_sim gz_sim.launch.py gz_args:="empty.sdf -r"

-r参数表示自动开始仿真,不用手动点击播放按钮。

二、给URDF装上"物理引擎":补齐inertial和collision

你现在的URDF在RViz里能正常显示,但Gazebo是物理仿真引擎,它需要两个RViz不需要的关键信息:

  • :质量和惯性矩(没有它,Gazebo可能直接不加载模型)
  • :碰撞体(没有它,机器人会穿过地面一直往下掉)

这是从"静态模型"到"物理模型"最关键的一步。

2.1 惯性(inertial):让机器人有质量

惯性描述了物体抵抗运动变化的能力。没有惯性,物理引擎就不知道物体在受力时会如何运动。

2.1.1 基础刚体惯性矩公式

对于均匀密度的基础形状,我们可以用公式直接计算惯性矩:

长方体惯性矩
{Ixx=112m(y2+z2)Iyy=112m(x2+z2)Izz=112m(x2+y2)Ixy=Ixz=Iyz=0 \begin{cases} I_{xx} = \frac{1}{12}m(y^2 + z^2) \\ I_{yy} = \frac{1}{12}m(x^2 + z^2) \\ I_{zz} = \frac{1}{12}m(x^2 + y^2) \\ I_{xy} = I_{xz} = I_{yz} = 0 \end{cases} Ixx=121m(y2+z2)Iyy=121m(x2+z2)Izz=121m(x2+y2)Ixy=Ixz=Iyz=0
其中:

  • mmm:物体质量(kg)
  • x,y,zx, y, zx,y,z:长方体在X、Y、Z轴上的长度(m)

圆柱体惯性矩(绕中心轴旋转)
{Ixx=Iyy=112m(3r2+l2)Izz=12mr2Ixy=Ixz=Iyz=0 \begin{cases} I_{xx} = I_{yy} = \frac{1}{12}m(3r^2 + l^2) \\ I_{zz} = \frac{1}{2}mr^2 \\ I_{xy} = I_{xz} = I_{yz} = 0 \end{cases} Ixx=Iyy=121m(3r2+l2)Izz=21mr2Ixy=Ixz=Iyz=0
其中:

  • mmm:质量(kg)
  • rrr:圆柱体半径(m)
  • lll:圆柱体长度(m)

球体惯性矩
Ixx=Iyy=Izz=25mr2 I_{xx} = I_{yy} = I_{zz} = \frac{2}{5}mr^2 Ixx=Iyy=Izz=52mr2
其中:

  • mmm:质量(kg)
  • rrr:球体半径(m)
2.1.2 编写可复用的惯性宏

我们把这些公式写成Xacro宏,这样所有项目都能复用。在common_properties.xacro中添加:

<!-- 长方体惯性宏 -->
<xacro:macro name="box_inertia" params="m x y z o_xyz='0 0 0' o_rpy='0 0 0'">
  <inertial>
    <mass value="${m}"/>
    <origin xyz="${o_xyz}" rpy="${o_rpy}"/>
    <inertia
      ixx="${m*(y*y + z*z)/12.0}"
      ixy="0.0"
      ixz="0.0"
      iyy="${m*(x*x + z*z)/12.0}"
      iyz="0.0"
      izz="${m*(x*x + y*y)/12.0}"
    />
  </inertial>
</xacro:macro>

<!-- 圆柱体惯性宏 -->
<xacro:macro name="cylinder_inertia" params="m r l o_xyz='0 0 0' o_rpy='0 0 0'">
  <inertial>
    <mass value="${m}"/>
    <origin xyz="${o_xyz}" rpy="${o_rpy}"/>
    <inertia
      ixx="${m*(3*r*r + l*l)/12.0}"
      ixy="0.0"
      ixz="0.0"
      iyy="${m*(3*r*r + l*l)/12.0}"
      iyz="0.0"
      izz="${m*r*r/2.0}"
    />
  </inertial>
</xacro:macro>

<!-- 球体惯性宏 -->
<xacro:macro name="sphere_inertia" params="m r o_xyz='0 0 0' o_rpy='0 0 0'">
  <inertial>
    <mass value="${m}"/>
    <origin xyz="${o_xyz}" rpy="${o_rpy}"/>
    <inertia
      ixx="${2.0*m*r*r/5.0}"
      ixy="0.0"
      ixz="0.0"
      iyy="${2.0*m*r*r/5.0}"
      iyz="0.0"
      izz="${2.0*m*r*r/5.0}"
    />
  </inertial>
</xacro:macro>
2.1.3 给每个物理link添加惯性

现在我们给两轮差速底盘的每个link添加惯性。注意:base_footprint是虚拟link,不需要惯性。

mobile_base.xacro中修改:

<!-- base_link -->
<link name="base_link">
  <visual>
    <geometry>
      <box size="${base_length} ${base_width} ${base_height}"/>
    </geometry>
    <origin xyz="0 0 ${base_height/2}" rpy="0 0 0"/>
    <material name="green"/>
  </visual>

  <!-- 碰撞体(后面会加) -->
  <collision>
    <geometry>
      <box size="${base_length} ${base_width} ${base_height}"/>
    </geometry>
    <origin xyz="0 0 ${base_height/2}" rpy="0 0 0"/>
  </collision>

  <!-- 惯性:质量5kg,origin和visual一致 -->
  <xacro:box_inertia
    m="5.0"
    x="${base_length}"
    y="${base_width}"
    z="${base_height}"
    o_xyz="0 0 ${base_height/2}"
  />
</link>

<!-- 右轮 -->
<link name="right_wheel_link">
  <visual>
    <geometry>
      <cylinder radius="${wheel_radius}" length="${wheel_length}"/>
    </geometry>
    <origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
    <material name="black"/>
  </visual>

  <collision>
    <!-- 注意:轮子碰撞体用球体!这是Gazebo工程经验 -->
    <geometry>
      <sphere radius="${wheel_radius}"/>
    </geometry>
  </collision>

  <!-- 惯性:质量1kg,origin旋转和visual一致 -->
  <xacro:cylinder_inertia
    m="1.0"
    r="${wheel_radius}"
    l="${wheel_length}"
    o_rpy="${pi/2} 0 0"
  />
</link>

<!-- 左轮和右轮完全一样,只是y坐标相反 -->
<xacro:wheel prefix="left"/>

<!-- 脚轮 -->
<link name="caster_wheel_link">
  <visual>
    <geometry>
      <sphere radius="${wheel_radius/2}"/>
    </geometry>
    <material name="gray"/>
  </visual>

  <collision>
    <geometry>
      <sphere radius="${wheel_radius/2}"/>
    </geometry>
  </collision>

  <!-- 惯性:质量0.5kg -->
  <xacro:sphere_inertia
    m="0.5"
    r="${wheel_radius/2}"
  />
</link>
2.1.4 用RViz验证惯性

这一步非常重要!很多人写完惯性就直接去Gazebo里试,结果出了问题不知道哪里错了。

用你之前写的display.launch.xml启动RViz,然后在左侧面板中:

  1. 展开RobotModel
  2. 取消勾选Visual Enabled
  3. 勾选Mass PropertiesShow Inertia

你会看到每个link都有一个半透明的惯性框。检查:

  • 惯性框是否和几何体完全贴合
  • 轮子的惯性轴是否和旋转轴一致
  • 底盘的惯性框是否在正确的高度

2.2 碰撞(collision):让机器人不穿过地面

碰撞体定义了物体在物理世界中的形状。当两个物体的碰撞体相交时,物理引擎就会产生碰撞力。

2.2.1 碰撞体设计原则

核心原则:碰撞体越简单,仿真越稳定、越快

  • 能用盒子、圆柱、球体的,绝对不用复杂的mesh
  • 碰撞体不需要和视觉完全一致,只要能近似表示物体的物理边界就行
  • 接触地面的部件(如轮子)尽量用球体,减少接触点,避免抖动
2.2.2 实战:给底盘和轮子加碰撞体

我在上面的代码里已经加了碰撞体,这里重点讲一个Gazebo工程里的"反直觉"技巧

轮子的碰撞体不要用圆柱体,要用球体!

为什么?因为圆柱体和地面接触是一条线,会产生很多接触点,导致奇怪的摩擦和抖动。而球体和地面接触是一个点,摩擦更可控,运动更平滑。

虽然这看起来有点"作弊",但在Gazebo仿真中非常常见,效果也确实更好。

2.2.3 用RViz验证碰撞体

同样在RViz中验证:

  1. 展开RobotModel
  2. 取消勾选Visual Enabled
  3. 勾选Collision Enabled

你会看到:

  • 底盘是一个长方体
  • 两个轮子是两个球体
  • 脚轮是一个小球体

这就是机器人在Gazebo物理世界中的样子。

三、把机器人"放进"Gazebo:spawn与工程结构

现在我们的URDF已经包含了所有Gazebo需要的信息。接下来我们要把它"生成"到Gazebo世界中。

3.1 手动跑通spawn流程(3个终端)

在写launch文件之前,强烈建议先在终端手动跑通整个流程,这样你会清楚每个组件的作用。

Terminal 1:启动robot_state_publisher
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(xacro ~/my_robot_ws/src/my_robot_description/urdf/my_robot.urdf.xacro)"
Terminal 2:启动Gazebo空世界
ros2 launch ros_gz_sim gz_sim.launch.py gz_args:="empty.sdf -r"
Terminal 3:把机器人生成到Gazebo
ros2 run ros_gz_sim create -topic /robot_description

如果一切正常,你会在Gazebo窗口中看到你的机器人模型落在地面上。

3.2 标准工程结构:_description + _bringup

现在我们要把这些零散的命令组织成标准的ROS 2工程。

最佳实践

  • my_robot_description:存放URDF、Xacro、mesh、RViz配置
  • my_robot_bringup:存放launch文件、参数配置、桥接配置

这种分层结构的好处是:

  • 模型资源和启动配置完全解耦
  • 依赖清晰:bringup包只依赖description包
  • 后续添加导航、控制等功能时,都可以放在bringup包里

创建bringup包:

cd ~/my_robot_ws/src
ros2 pkg create my_robot_bringup --build-type ament_cmake
cd my_robot_bringup
rm -r include/ src/
mkdir launch config

3.3 编写bringup包的launch文件

创建launch/gazebo.launch.xml

<launch>
  <!-- 定义路径变量 -->
  <let name="urdf_path" value="$(find-pkg-share my_robot_description)/urdf/my_robot.urdf.xacro"/>
  <let name="world_path" value="$(find-pkg-share ros_gz_sim)/worlds/empty.sdf"/>

  <!-- 1. 启动robot_state_publisher -->
  <node pkg="robot_state_publisher" exec="robot_state_publisher">
    <param name="robot_description" value="$(command 'xacro $(var urdf_path)')"/>
  </node>

  <!-- 2. 启动Gazebo -->
  <include file="$(find-pkg-share ros_gz_sim)/launch/gz_sim.launch.py">
    <arg name="gz_args" value="$(var world_path) -r"/>
  </include>

  <!-- 3. 生成机器人到Gazebo -->
  <node pkg="ros_gz_sim" exec="create">
    <arg name="-topic" value="/robot_description"/>
  </node>
</launch>

构建并运行:

cd ~/my_robot_ws
colcon build --packages-select my_robot_bringup
source install/setup.bash

ros2 launch my_robot_bringup gazebo.launch.xml

现在你可以用一个命令启动Gazebo并生成机器人了!

四、让机器人"能动起来":Gazebo插件与桥接

现在机器人已经在Gazebo里了,但它还是不能动。因为我们还没有给它添加"驱动"。

4.1 Gazebo系统插件:虚拟硬件驱动

Gazebo插件是运行在Gazebo内部的程序,用来模拟真实的硬件。我们需要两个最基础的插件:

  1. DiffDrive系统:接收速度命令,驱动左右轮转动
  2. JointStatePublisher系统:发布关节状态,供robot_state_publisher计算TF
4.1.1 编写Gazebo专用Xacro文件

为了保持代码干净,我们新建一个urdf/mobile_base_gazebo.xacro文件,专门放Gazebo相关的内容:

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
  <!-- 差速驱动插件 -->
  <gazebo>
    <plugin filename="libgz-sim-diff-drive-system.so" name="gz::sim::systems::DiffDrive">
      <left_joint>base_left_wheel_joint</left_joint>
      <right_joint>base_right_wheel_joint</right_joint>
      <wheel_separation>0.45</wheel_separation> <!-- 轮距:底盘宽 + 2*轮半厚 -->
      <wheel_radius>0.1</wheel_radius>
      <odom_frame_id>odom</odom_frame_id>
      <child_frame_id>base_footprint</child_frame_id>
      <publish_odom_tf>true</publish_odom_tf>
    </plugin>
  </gazebo>

  <!-- 关节状态发布插件 -->
  <gazebo>
    <plugin filename="libgz-sim-joint-state-publisher-system.so" name="gz::sim::systems::JointStatePublisher">
    </plugin>
  </gazebo>

  <!-- 调低脚轮摩擦系数,避免拖慢运动 -->
  <gazebo reference="caster_wheel_link">
    <collision name="caster_collision">
      <surface>
        <friction>
          <ode>
            <mu>0.001</mu>
            <mu2>0.001</mu2>
          </ode>
        </friction>
      </surface>
    </collision>
  </gazebo>
</robot>

然后在my_robot.urdf.xacro中include它:

<xacro:include filename="$(find my_robot_description)/urdf/mobile_base_gazebo.xacro"/>
4.1.2 关键参数解释
  • wheel_separation:左右轮之间的距离,计算方式:底盘宽度 + 2 * (轮子厚度 / 2)
  • wheel_radius:轮子半径,必须和URDF中一致
  • odom_frame_id:里程计坐标系名称
  • child_frame_id:机器人基坐标系名称
  • publish_odom_tf:是否发布odom → base_footprint的TF

4.2 ros_gz_bridge:ROS2和Gazebo的翻译官

现在Gazebo内部已经有了驱动,但它的话题是Gazebo自己的,ROS 2看不到。我们需要一个"翻译官"把Gazebo的话题转换成ROS 2的话题,反之亦然。

这个翻译官就是ros_gz_bridge

4.2.1 编写桥接配置YAML

创建config/gazebo_bridge.yaml

- ros_topic_name: "/cmd_vel"
  gz_topic_name: "/model/my_robot/cmd_vel"
  ros_type_name: "geometry_msgs/msg/Twist"
  gz_type_name: "gz.msgs.Twist"
  direction: ROS_TO_GZ

- ros_topic_name: "/joint_states"
  gz_topic_name: "/world/empty/model/my_robot/joint_state"
  ros_type_name: "sensor_msgs/msg/JointState"
  gz_type_name: "gz.msgs.Model"
  direction: GZ_TO_ROS

- ros_topic_name: "/tf"
  gz_topic_name: "/model/my_robot/tf"
  ros_type_name: "tf2_msgs/msg/TFMessage"
  gz_type_name: "gz.msgs.Pose_V"
  direction: GZ_TO_ROS

我们需要桥接三个最关键的话题:

  1. /cmd_vel:ROS 2 → Gazebo,速度控制命令
  2. /joint_states:Gazebo → ROS 2,关节状态
  3. /tf:Gazebo → ROS 2,里程计TF
4.2.2 在launch中启动桥接

修改gazebo.launch.xml,添加桥接节点:

<!-- 4. 启动ros_gz_bridge -->
<node pkg="ros_gz_bridge" exec="parameter_bridge">
  <param name="config_file" value="$(find-pkg-share my_robot_bringup)/config/gazebo_bridge.yaml"/>
</node>

<!-- 5. 启动RViz(可选) -->
<node pkg="rviz2" exec="rviz2" args="-d $(find-pkg-share my_robot_description)/rviz/display.rviz"/>

五、测试与验证:用ROS2控制Gazebo机器人

现在所有组件都准备好了!让我们测试一下机器人是否能正常运动。

5.1 键盘遥控测试

安装键盘遥控包:

sudo apt install ros-jazzy-teleop-twist-keyboard

启动所有节点:

ros2 launch my_robot_bringup gazebo.launch.xml

新开终端,启动键盘遥控:

ros2 run teleop_twist_keyboard teleop_twist_keyboard

按照提示,按i键前进,按,键后退,按j/l键左右转,按k键停止。你会看到Gazebo里的机器人跟着你的指令运动!

5.2 验证话题和TF

检查话题是否正常:

# 查看所有话题
ros2 topic list

# 查看/cmd_vel话题
ros2 topic echo /cmd_vel

# 查看/joint_states话题
ros2 topic echo /joint_states

# 查看/tf话题
ros2 topic echo /tf

导出完整的TF Tree:

ros2 run tf2_tools view_frames

你会看到TF Tree现在包含:

  • odom → base_footprint(里程计TF)
  • base_footprint → base_link
  • base_link → left_wheel_link
  • base_link → right_wheel_link
  • base_link → caster_wheel_link

5.3 RViz可视化效果

在RViz中把Fixed Frame改成odom,你会看到机器人在Gazebo里移动时,RViz里的机器人也会跟着移动。这说明里程计和TF都正常工作了!

5.4 常见问题与解决方案

问题 原因 解决方案
机器人穿过地面往下掉 没有加collision,或者collision位置不对 检查collision的origin,确保碰撞体在地面以上
机器人一启动就飞出去 惯性参数错误,或者重心太高 检查inertial的质量和惯性矩,降低重心
运动时抖动严重 轮子用了圆柱体碰撞体,或者摩擦系数不对 把轮子碰撞体改成球体,调低脚轮摩擦系数
RViz里轮子不转 没有桥接/joint_states话题,或者joint名称不对 检查桥接配置中的joint名称是否和URDF一致
只能前进不能转弯 轮距参数错误 重新计算wheel_separation参数

六、学完基础后该干什么?ROS2进阶学习路线

恭喜你!你已经完成了ROS 2的基础学习阶段。现在你能写节点、能通信、能建模、能仿真,已经具备了开发简单机器人应用的能力。

但ROS 2的生态非常庞大,机器人更是一个跨学科的大系统。接下来该往哪个方向走?

6.1 学习三阶段:打基础→探索→专精

学习任何技术都遵循同样的规律:

  1. 打基础:掌握所有项目都会用到的最小核心

    • 节点、Topic/Service/Action
    • 参数、Launch文件
    • TF、URDF
    • 基本工具链(RViz、Gazebo、rqt)

    ✅ 你已经完成了这一阶段!

  2. 探索:通过不同方向的项目"连点成线"

    • 学习现成的大栈(Nav2、MoveIt2)
    • 补硬基础(Linux、C++)
    • 做2-3个完整的小项目
  3. 专精:在一个方向上深入研究

    • 移动机器人导航
    • 机械臂运动规划
    • 硬件控制
    • 视觉感知

6.2 探索阶段三大核心栈

当你掌握了基础后,下一步最应该学习的是三个"现成的大栈"。它们的意义是:别重复造轮子

6.2.1 Nav2:移动机器人导航

如果你想做移动机器人,Nav2是绕不开的。它帮你解决了从A点走到B点的所有问题:

  • 建图(SLAM)
  • 定位
  • 全局路径规划
  • 局部避障
  • 恢复行为

即使你未来不做移动机器人,也建议至少花几小时做一个入门体验。因为Nav2是ROS 2生态中最成熟、应用最广的大栈。

6.2.2 MoveIt2:机械臂运动规划

如果你想做机械臂,MoveIt2是事实上的标准。它帮你解决了:

  • 正逆运动学求解
  • 轨迹规划
  • 碰撞检测
  • 多关节同步控制
  • 抓取规划
6.2.3 ros2_control:硬件抽象层

当你要控制真实硬件时,ros2_control是最佳选择。它提供了一套标准的硬件接口,让你的上层代码不用关心具体的硬件细节。

建议学习顺序:先会写简单的硬件驱动,再上ros2_control。不要一开始就硬啃ros2_control,它的门槛比较高。

6.3 必须补的硬基础

很多人ROS 2学着学着卡住,不是卡在ROS,而是卡在底层能力上:

  • Linux命令行:熟练使用tab补全、理解.bashrc、会SSH远程、会查进程端口
  • Python面向对象:写节点时会用到大量的类和继承
  • C++:越往后你会发现,大量底层控制和性能敏感的代码都是C++写的

6.4 按目标定路线:项目驱动 vs 求职导向

最好的学习方式永远是:做项目。不要照着清单刷技能,而是先定一个项目目标,然后在项目中按需学习。

项目目标 学习重点
移动机器人自动导航 Nav2 + SLAM + 摄像头集成
机械臂抓取分拣 MoveIt2 + 相机标定 + 视觉识别
仓库多机器人调度 Nav2 + 多机器人协同 + 任务分配
真实硬件控制 ros2_control + 通信协议(CAN/I2C/SPI)
求职找工作 做1-2个完整的项目,放到GitHub上,写清楚README

七、总结:可复用的Gazebo仿真配方与行动建议

7.1 可复用的Gazebo仿真配方

本文介绍的仿真流程可以套用到任何机器人上:

  1. 写URDF描述机器人结构(links + joints)
  2. 给每个物理link添加inertial和collision
  3. 用RViz验证惯性和碰撞体
  4. 添加Gazebo系统插件(驱动、传感器等)
  5. 用ros_gz_bridge桥接关键话题
  6. 测试并调试

7.2 行动建议

现在你已经掌握了完整的仿真流程。接下来我建议你:

  1. 把你自己的URDF改成能在Gazebo里跑的版本
  2. 用键盘遥控让它动起来
  3. 导出TF Tree,检查是否完整
  4. 选择一个你感兴趣的方向(Nav2或MoveIt2),开始做一个小项目

记住:别过度规划,先做起来。探索阶段最重要的是"持续动手 + 快速迭代"。

当你能独立完成一个完整的机器人仿真项目时,你就已经是一名合格的ROS 2开发者了。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐