【ROS 2 全栈入门指南四】:从静态模型到可控制移动机器人
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,然后在左侧面板中:
- 展开
RobotModel - 取消勾选
Visual Enabled - 勾选
Mass Properties→Show Inertia
你会看到每个link都有一个半透明的惯性框。检查:
- 惯性框是否和几何体完全贴合
- 轮子的惯性轴是否和旋转轴一致
- 底盘的惯性框是否在正确的高度
2.2 碰撞(collision):让机器人不穿过地面
碰撞体定义了物体在物理世界中的形状。当两个物体的碰撞体相交时,物理引擎就会产生碰撞力。
2.2.1 碰撞体设计原则
核心原则:碰撞体越简单,仿真越稳定、越快。
- 能用盒子、圆柱、球体的,绝对不用复杂的mesh
- 碰撞体不需要和视觉完全一致,只要能近似表示物体的物理边界就行
- 接触地面的部件(如轮子)尽量用球体,减少接触点,避免抖动
2.2.2 实战:给底盘和轮子加碰撞体
我在上面的代码里已经加了碰撞体,这里重点讲一个Gazebo工程里的"反直觉"技巧:
轮子的碰撞体不要用圆柱体,要用球体!
为什么?因为圆柱体和地面接触是一条线,会产生很多接触点,导致奇怪的摩擦和抖动。而球体和地面接触是一个点,摩擦更可控,运动更平滑。
虽然这看起来有点"作弊",但在Gazebo仿真中非常常见,效果也确实更好。
2.2.3 用RViz验证碰撞体
同样在RViz中验证:
- 展开
RobotModel - 取消勾选
Visual Enabled - 勾选
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内部的程序,用来模拟真实的硬件。我们需要两个最基础的插件:
- DiffDrive系统:接收速度命令,驱动左右轮转动
- 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
我们需要桥接三个最关键的话题:
/cmd_vel:ROS 2 → Gazebo,速度控制命令/joint_states:Gazebo → ROS 2,关节状态/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_linkbase_link → left_wheel_linkbase_link → right_wheel_linkbase_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 学习三阶段:打基础→探索→专精
学习任何技术都遵循同样的规律:
-
打基础:掌握所有项目都会用到的最小核心
- 节点、Topic/Service/Action
- 参数、Launch文件
- TF、URDF
- 基本工具链(RViz、Gazebo、rqt)
✅ 你已经完成了这一阶段!
-
探索:通过不同方向的项目"连点成线"
- 学习现成的大栈(Nav2、MoveIt2)
- 补硬基础(Linux、C++)
- 做2-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仿真配方
本文介绍的仿真流程可以套用到任何机器人上:
- 写URDF描述机器人结构(links + joints)
- 给每个物理link添加inertial和collision
- 用RViz验证惯性和碰撞体
- 添加Gazebo系统插件(驱动、传感器等)
- 用ros_gz_bridge桥接关键话题
- 测试并调试
7.2 行动建议
现在你已经掌握了完整的仿真流程。接下来我建议你:
- 把你自己的URDF改成能在Gazebo里跑的版本
- 用键盘遥控让它动起来
- 导出TF Tree,检查是否完整
- 选择一个你感兴趣的方向(Nav2或MoveIt2),开始做一个小项目
记住:别过度规划,先做起来。探索阶段最重要的是"持续动手 + 快速迭代"。
当你能独立完成一个完整的机器人仿真项目时,你就已经是一名合格的ROS 2开发者了。
更多推荐

所有评论(0)