Gazebo仿真与ros2_control:从虚拟环境到闭环控制
> 基于Jazzy Jalisco LTS + Gazebo Harmonic,讲解仿真环境搭建、ros_gz桥接机制、自定义机器人SDF建模、ros2_control闭环控制框架和传感器插件集成。所有命令可直接运行验证。
一、Gazebo Harmonic概述
Gazebo Harmonic是Open Robotics推出的新一代仿真器,替代已退役的Gazebo Classic(Gazebo 11)。架构重构后,物理引擎可插拔、渲染管线升级、ROS2集成方式从直连改为桥接——不是换个版本号,是换了一套骨架。
Gazebo Classic vs Gazebo Harmonic
核心差异
| 特性 | Gazebo Classic | Gazebo Harmonic |
|---|---|---|
| 版本号 | Gazebo 11 | Gazebo 8(Harmonic) |
| 模型格式 | URDF/Xacro | SDFormat(原生) |
| ROS2集成 | gazebo_ros_pkgs |
ros_gz(桥接) |
| 物理引擎 | ODE | DART(默认)/ Bullet / ODE |
| 渲染引擎 | OGRE 1.x | OGRE 2.x(PBR材质) |
| 推荐Ubuntu | 22.04 | 24.04 |
| 维护状态 | 退役 | 活跃 |
二、环境安装与配置
安装Gazebo Harmonic
# Ubuntu 24.04 + ROS2 Jazzy
# 1. 安装Gazebo Harmonic
sudo apt update
sudo apt install gz-harmonic
# 2. 安装ros_gz桥接器
sudo apt install ros-jazzy-ros-gz-bridge
sudo apt install ros-jazzy-ros-gz-sim
sudo apt install ros-jazzy-ros-gz
# 3. 安装ros2_control相关包
sudo apt install ros-jazzy-ros2-control
sudo apt install ros-jazzy-ros2-controllers
sudo apt install ros-jazzy-gz-ros2-control
# 4. 验证安装
gz sim --version
# 输出: Gazebo Sim, version 8.x.x
验证ros_gz桥接
# 加载ROS2环境
source /opt/ros/jazzy/setup.bash
# 检查桥接包是否安装
dpkg -l | grep ros-jazzy-ros-gz
# 期望输出: ros-jazzy-ros-gz-bridge / ros-jazzy-ros-gz-sim
三、ros_gz桥接架构
ros_gz_bridge是ROS2和Gazebo之间的消息翻译器。ROS2用DDS通信,Gazebo用自己的Transport协议,两者不能直接对话——bridge在中间做双向翻译。
桥接架构图
parameter_bridge命令行用法
parameter_bridge是ros_gz_bridge提供的命令行工具,用于快速建立单条话题的桥接。
# 语法: 话题名@ROS2消息类型@Gazebo消息类型
# 双向桥接(两个@)
ros2 run ros_gz_bridge parameter_bridge \
/cmd_vel@geometry_msgs/msg/Twist@gz.msgs.Twist
# 单向桥接(Gazebo→ROS2,一个@)
ros2 run ros_gz_bridge parameter_bridge \
/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan
# 单向桥接(ROS2→Gazebo,一个@)
ros2 run ros_gz_bridge parameter_bridge \
/cmd_vel@geometry_msgs/msg/Twist]gz.msgs.Twist
> 注意:方括号方向表示数据流向。[ 表示Gazebo→ROS2,] 表示ROS2→Gazebo。双向用两个 @ 分隔。
四、TurtleBot3仿真实战
TurtleBot3是ROS2仿真的标准入门平台,ROBOTIS官方提供了完整的Gazebo Harmonic适配包。
安装TurtleBot3仿真包
# 安装TurtleBot3全部相关包
sudo apt install ros-jazzy-turtlebot3*
设置环境变量
# 设置TurtleBot3型号(burger/waffle/waffle_pi)
echo 'export TURTLEBOT3_MODEL=waffle' >> ~/.bashrc
source ~/.bashrc
启动仿真
# 终端1:启动Gazebo仿真世界
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
# 终端2:启动键盘遥控
ros2 run turtlebot3_teleop teleop_keyboard
# 终端3:启动RViz2可视化
ros2 launch turtlebot3_gazebo turtlebot3 rviz.launch.py
桥接话题验证
# 查看Gazebo发布到ROS2的话题
ros2 topic list | grep -E "cmd_vel|scan|odom|camera"
# 期望输出:
# /cmd_vel
# /odom
# /scan
# /camera/image_raw
# 查看激光雷达数据
ros2 topic echo /scan --once
# 查看里程计
ros2 topic echo /odom --once
# 手动发送速度指令
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.2}, angular: {z: 0.5}}"
五、自定义机器人仿真
SDFormat模型文件
Gazebo Harmonic原生使用SDFormat(Simulation Description Format)。相比URDF,SDF支持多模型、传感器插件、物理材质等URDF不具备的特性。
SDF示例:底盘+激光雷达+差速驱动
<?xml version="1.0"?>
<sdf version="1.8">
<model name="diff_bot">
<!-- 底盘连杆 -->
<link name="chassis">
<pose>0 0 0.1 0 0 0</pose>
<inertial>
<mass>5.0</mass>
<inertia>
<ixx>0.1</ixx><ixy>0</ixy><ixz>0</ixz>
<iyy>0.1</iyy><iyz>0</iyz>
<izz>0.1</izz>
</inertia>
</inertial>
<visual name="visual">
<geometry><box><size>0.4 0.3 0.2</size></box></geometry>
<material><ambient>0 0 0.8 1</ambient></material>
</visual>
<collision name="collision">
<geometry><box><size>0.4 0.3 0.2</size></box></geometry>
</collision>
</link>
<!-- 左轮 -->
<link name="left_wheel">
<pose>-0.1 0.175 0.1 -1.5707 0 0</pose>
<inertial>
<mass>0.5</mass>
<inertia>
<ixx>0.001</ixx><ixy>0</ixy><ixz>0</ixz>
<iyy>0.001</iyy><iyz>0</iyz>
<izz>0.001</izz>
</inertia>
</inertial>
<visual name="visual">
<geometry><cylinder><radius>0.05</radius><length>0.04</length></cylinder></geometry>
<material><ambient>0.5 0.5 0.5 1</ambient></material>
</visual>
<collision name="collision">
<geometry><cylinder><radius>0.05</radius><length>0.04</length></cylinder></geometry>
</collision>
</link>
<!-- 右轮 -->
<link name="right_wheel">
<pose>-0.1 -0.175 0.1 -1.5707 0 0</pose>
<inertial>
<mass>0.5</mass>
<inertia>
<ixx>0.001</ixx><ixy>0</ixy><ixz>0</ixz>
<iyy>0.001</iyy><iyz>0</iyz>
<izz>0.001</izz>
</inertia>
</inertial>
<visual name="visual">
<geometry><cylinder><radius>0.05</radius><length>0.04</length></cylinder></geometry>
<material><ambient>0.5 0.5 0.5 1</ambient></material>
</visual>
<collision name="collision">
<geometry><cylinder><radius>0.05</radius><length>0.04</length></cylinder></geometry>
</collision>
</link>
<!-- 万向轮(前) -->
<link name="caster">
<pose>0.15 0 0.05 0 0 0</pose>
<inertial>
<mass>0.2</mass>
<inertia>
<ixx>0.0001</ixx><ixy>0</ixy><ixz>0</ixz>
<iyy>0.0001</iyy><iyz>0</iyz>
<izz>0.0001</izz>
</inertia>
</inertial>
<visual name="visual">
<geometry><sphere><radius>0.05</radius></sphere></geometry>
<material><ambient>0.3 0.3 0.3 1</ambient></material>
</visual>
<collision name="collision">
<geometry><sphere><radius>0.05</radius></sphere></geometry>
<surface>
<friction>
<ode><mu>0</mu><mu2>0</mu2></ode>
</friction>
</surface>
</collision>
</link>
<!-- 关节定义 -->
<joint name="left_wheel_joint" type="revolute">
<parent>chassis</parent>
<child>left_wheel</child>
<axis><xyz>0 1 0</xyz></axis>
<limit><lower>-1e+16</lower><upper>1e+16</upper></limit>
</joint>
<joint name="right_wheel_joint" type="revolute">
<parent>chassis</parent>
<child>right_wheel</child>
<axis><xyz>0 1 0</xyz></axis>
<limit><lower>-1e+16</lower><upper>1e+16</upper></limit>
</joint>
<joint name="caster_joint" type="ball">
<parent>chassis</parent>
<child>caster</child>
</joint>
<!-- 激光雷达传感器 -->
<link name="laser">
<pose>0.15 0 0.15 0 0 0</pose>
<sensor name="laser_sensor" type="lidar">
<lidar>
<scan>
<horizontal>
<samples>360</samples>
<resolution>1</resolution>
<min_angle>-3.14159</min_angle>
<max_angle>3.14159</max_angle>
</horizontal>
</scan>
<range>
<min>0.1</min><max>12.0</max>
<resolution>0.01</resolution>
</range>
</lidar>
<always_on>true</always_on>
<update_rate>10</update_rate>
<visualize>true</visualize>
<topic>/scan</topic>
</sensor>
</link>
<joint name="laser_joint" type="fixed">
<parent>chassis</parent>
<child>laser</child>
</joint>
<!-- 差速驱动插件 -->
<plugin
filename="gz-sim-diff-drive-system"
name="gz::sim::systems::DiffDrive">
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.35</wheel_separation>
<wheel_radius>0.05</wheel_radius>
<topic>/cmd_vel</topic>
<odom_topic>/odom</odom_topic>
<frame_id>odom</frame_id>
<child_frame_id>base_link</child_frame_id>
</plugin>
</model>
</sdf>
启动自定义机器人
# 创建世界文件
cat > my_world.sdf << 'EOF'
<?xml version="1.0"?>
<sdf version="1.8">
<world name="my_world">
<include>
<uri>https://fuel.gazebosim.org/1.0/OpenRobotics/models/diff_bot</uri>
</include>
<include>
<uri>https://fuel.gazebosim.org/1.0/OpenRobotics/models/ground_plane</uri>
</include>
<include>
<uri>https://fuel.gazebosim.org/1.0/OpenRobotics/models/sun</uri>
</include>
</world>
</sdf>
EOF
# 启动Gazebo仿真
gz sim my_world.sdf
# 新终端:启动ros_gz桥接
ros2 run ros_gz_bridge parameter_bridge \
/cmd_vel@geometry_msgs/msg/Twist@gz.msgs.Twist \
/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan \
/odom@nav_msgs/msg/Odometry[gz.msgs.Odometry
六、ros2_control框架
ros2_control是ROS2的机器人控制框架。它将"控制器逻辑"与"硬件驱动"解耦——控制器只管算,硬件接口只管执行,中间通过CommandInterface和StateInterface对接。仿真里用GazeboSystem替代真实硬件,代码不用改。
控制架构
控制器配置YAML
# diff_drive_controller.yaml
controller_manager:
ros__parameters:
update_rate: 50 # 控制循环频率50Hz
diff_drive_controller:
type: diff_drive_controller/DiffDriveController
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
diff_drive_controller:
ros__parameters:
left_wheel_names: ["left_wheel_joint"]
right_wheel_names: ["right_wheel_joint"]
wheel_separation: 0.35
wheel_radius: 0.05
# 速度和加速度限制
linear.x.max_velocity: 0.5
angular.z.max_velocity: 1.0
linear.x.max_acceleration: 2.0
angular.z.max_acceleration: 2.0
# 里程计配置
publish_rate: 50.0
odom_frame_id: odom
base_frame_id: base_link
enable_odom_tf: true
# 指令话题
cmd_vel_timeout: 0.5 # 0.5秒无指令则停止
use_stamped_vel: false # 使用Unstamped速度指令
URDF中声明ros2_control硬件接口
在URDF中需要声明<ros2_control> 标签,告诉ControllerManager有哪些关节、用什么接口:
<!-- 添加到URDF的<robot>标签内 -->
<ros2_control name="GazeboSystem" type="system">
<hardware>
<plugin>gz_ros2_control/GazeboSystem</plugin>
</hardware>
<joint name="left_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="right_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
启动带控制的仿真
# 安装ros2_control相关包(如未安装)
sudo apt install ros-jazzy-ros2-control ros-jazzy-ros2-controllers
sudo apt install ros-jazzy-gz-ros2-control
# 启动仿真(包含ros2_control)
# 假设已有完整的launch文件
ros2 launch my_robot_gazebo my_robot_sim.launch.py
# 查看控制器状态
ros2 control list_controllers
# 期望输出:
# diff_drive_controller [diff_drive_controller/DiffDriveController] active
# joint_state_broadcaster [joint_state_broadcaster/JointStateBroadcaster] active
# 查看硬件接口
ros2 control list_hardware_interfaces
# 期望输出:
# left_wheel_joint/velocity [command]
# right_wheel_joint/velocity [command]
# left_wheel_joint/position [state]
# left_wheel_joint/velocity [state]
# right_wheel_joint/position [state]
# right_wheel_joint/velocity [state]
# 发送速度指令控制机器人
ros2 topic pub /diff_drive_controller/cmd_vel_unstamped \
geometry_msgs/msg/Twist "{linear: {x: 0.2}, angular: {z: 0.0}}" \
--rate 10
ros2 control常用命令
# 列出所有控制器
ros2 control list_controllers
# 列出硬件接口
ros2 control list_hardware_interfaces
# 列出控制器类型
ros2 control list_controller_types
# 手动激活/停用控制器
ros2 control set_controller_state diff_drive_controller active
ros2 control set_controller_state diff_drive_controller inactive
# 查看控制器参数
ros2 param get /controller_manager diff_drive_controller.type
七、常用传感器插件
Gazebo Harmonic通过插件系统提供传感器仿真,数据通过ros_gz_bridge发布到ROS2话题。
| 传感器类型 | Gazebo插件 | ROS2话题 | ROS2消息类型 | Gazebo消息类型 |
|:-----------|:-----------|:---------|:-------------|:---------------|
| 2D激光雷达 | gz-sim-lidar-system | /scan | sensor_msgs/LaserScan | gz.msgs.LaserScan |
| 深度相机 | gz-sim-depth-camera-system | /depth_camera/image | sensor_msgs/Image | gz.msgs.Image |
| RGB相机 | gz-sim-camera-system | /camera/image_raw | sensor_msgs/Image | gz.msgs.Image |
| IMU | gz-sim-imu-system | /imu | sensor_msgs/Imu | gz.msgs.IMU |
| 接触传感器 | gz-sim-contact-system | /contact | gazebo_msgs/ContactsState | gz.msgs.Contacts |
传感器桥接命令示例
# 激光雷达
ros2 run ros_gz_bridge parameter_bridge \
/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan
# RGB相机
ros2 run ros_gz_bridge parameter_bridge \
/camera/image_raw@sensor_msgs/msg/Image[gz.msgs.Image
# 深度相机(需桥接多个话题)
ros2 run ros_gz_bridge parameter_bridge \
/depth_camera/image@sensor_msgs/msg/Image[gz.msgs.Image \
/depth_camera/points@sensor_msgs/msg/PointCloud2[gz.msgs.PointCloudPacked
# IMU
ros2 run ros_gz_bridge parameter_bridge \
/imu@sensor_msgs/msg/Imu[gz.msgs.IMU
八、URDF与SDF转换
Gazebo Harmonic原生使用SDF,但ROS2生态大量使用URDF。实际项目中两种格式需要互相转换。
URDF转SDF
# 使用gz工具将URDF转为SDF
gz sdf -p my_robot.urdf > my_robot.sdf
# 检查SDF是否有效
gz sdf -k my_robot.sdf
robot_state_publisher在Gazebo中的使用
robot_state_publisher读取URDF并发布TF变换树。在Gazebo仿真中,它仍然有用——负责发布机器人各连杆之间的静态TF。
# launch文件中同时使用robot_state_publisher和Gazebo
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
pkg_share = get_package_share_directory('my_robot_description')
# robot_state_publisher:发布URDF的TF
robot_state_publisher = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{
'robot_description': open(
os.path.join(pkg_share, 'urdf', 'my_robot.urdf')
).read()
}]
)
# Gazebo仿真
gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(
get_package_share_directory('ros_gz_sim'),
'launch', 'gz_sim.launch.py'
)
),
launch_arguments={
'gz_args': os.path.join(pkg_share, 'worlds', 'my_world.sdf')
}.items()
)
# ros_gz桥接
bridge = Node(
package='ros_gz_bridge',
executable='parameter_bridge',
arguments=[
'/cmd_vel@geometry_msgs/msg/Twist@gz.msgs.Twist',
'/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan',
'/odom@nav_msgs/msg/Odometry[gz.msgs.Odometry',
]
)
return LaunchDescription([
robot_state_publisher,
gazebo,
bridge,
])
URDF与SDF选择建议
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| ROS2节点间通信 | URDF | robot_state_publisher原生支持 |
| Gazebo仿真 | SDF | 传感器插件、物理材质等SDF独有特性 |
| MoveIt2运动规划 | URDF | MoveIt2依赖URDF+SRDF |
| 项目同时需要两种 | URDF为主,运行时转SDF | 维护一份URDF,用 gz sdf -p 自动转换 |
九、常见问题
Q1:Gazebo Harmonic启动后黑屏无渲染?
检查显卡驱动和OpenGL支持。虚拟机中Gazebo使用CPU渲染(llvmpipe),性能低但能跑。WSL2需要配置GPU直通(参考WSL2搭建文档)。排查命令:
# 查看详细日志
gz sim -v 4
# 检查渲染后端
gz sim --render-engine-gui-api-backend opengl
Q2:ros_gz_bridge桥接话题无数据?
消息类型必须精确匹配。用 ros2 topic list 和 gz topic -l 对比两侧话题,确认桥接进程已启动且参数正确:
# 检查ROS2侧话题
ros2 topic list
# 检查Gazebo侧话题
gz topic -l
# 查看桥接节点是否在运行
ros2 node list | grep bridge
Q3:URDF模型无法在Gazebo Harmonic中加载?
Gazebo Harmonic原生支持SDFormat。URDF需要先转换。
# URDF转SDF
gz sdf -p my_robot.urdf > my_robot.sdf
# 或使用ros_gz_sim工具加载URDF
ros2 run ros_gz_sim create -file my_robot.urdf -topic robot_description
Q4:ros2_control控制器无法激活?
检查URDF中<ros2_control> 标签是否正确声明了硬件接口,关节名称是否与SDF/URDF中的joint name完全一致:
# 查看控制器状态和错误信息
ros2 control list_controllers -v
# 检查硬件接口是否注册
ros2 control list_hardware_interfaces
十、总结
Gazebo Harmonic + ros_gz桥接 + ros2_control,构成了ROS2 Jazzy时代仿真验证的完整链路:Gazebo负责物理仿真和传感器模拟,ros_gz_bridge做消息翻译,ros2_control做闭环控制。仿真代码和真机代码共享同一套控制器逻辑,切换硬件接口即可从仿真迁移到实车。
速查表
| 仿真任务 | 核心工具 |
|---|---|
| 启动仿真环境 | gz sim + ros2 launch |
| ROS2↔Gazebo消息桥接 | ros_gz_bridge / parameter_bridge |
| 机器人建模 | SDFormat(.sdf)原生,URDF用 gz sdf -p 转换 |
| 差速运动控制 | ros2_control + DiffDriveController |
| 关节状态发布 | JointStateBroadcaster |
| 传感器数据获取 | Gazebo传感器插件→ros_gz_bridge→ROS2话题 |
| 可视化调试 | RViz2 + Gazebo GUI |
| 控制器管理 | ros2 control list_controllers |
本文首发于linuxros.cn,转载请注明出处。