【机器人开发四】从零开始创建一个ROS2机器人工程(1):创建一个ROS2小车,掌握ROS2工程以及消息订阅机制
【机器人开发四】从零开始创建一个ROS2机器人工程(1):熟悉ROS2工程目录,做一个turtle3小车
本文将手把手教你从零开始搭建一个能用键盘控制移动的两轮机器人。基于 ROS 2 Jazzy + Gazebo Harmonic 环境。
本文的目的
- 深入理解一个标准的ROS2工程的目录结构
- 理解ROS2工程中最核心的文件
- 掌握ROS2项目的启动方法
- 创建一个turtle3机器人,熟悉话题的发布订阅机制
文章目录
- 【机器人开发四】从零开始创建一个ROS2机器人工程(1):熟悉ROS2工程目录,做一个turtle3小车
1. 创建工作空间和功能包
一个标准的 ROS 工程源码目录应有如下结构。ros2_ws 是项目目录,src 子目录是所有源码的目录。
# 创建工作空间mkdir-p ~/ros2_ws/src cd ~/ros2_ws/src # 创建 ROS 2 功能包(使用 ament_cmake 类型,支持 Gazebo 插件) ros2 pkg create --build-type ament_cmake my_robot_description # 进入包目录并创建标准子目录cd my_robot_description mkdir urdf launch worlds config meshes models 执行完后,目录结构如下:
ros2_ws/ # 工作空间根目录 └── src/ # 源代码目录(所有功能包放这里) └── my_robot_description/ # 机器人描述功能包目录 ├── CMakeLists.txt # 编译脚本(自动生成) ├── package.xml # 包描述文件(自动生成) ├── src/ # C++源码目录,用于机器人控制编程(自动生成) ├── include/ # C++头文件目录(自动生成) ├── urdf/ # 机器人 URDF/Xacro 文件(核心) ├── launch/ # Launch 启动文件(核心) ├── worlds/ # Gazebo 世界场景文件(可选) ├── config/ # RViz/Gazebo 配置文件(可选) ├── meshes/ # 3D 网格模型文件(可选) └── models/ # Gazebo 模型定义(可选) 提示:标注为"核心"的文件和目录是必须创建的,机器人缺少它们无法工作标注为"自动生成"的文件由 ros2 pkg create 命令自动生成,一般不需要手动修改标注为"可选"的目录可以根据需要创建,不是完成本教程的必要条件提示:标准 ROS 2 工程的目录结构让代码易于维护,colcon build 能正确识别所有资源。
注意:功能包必须放在 src/ 目录下。如果放错位置(如直接在 ros2_ws/ 下创建),colcon build 将无法识别。
2. 编写 Launch 文件
在 ~/ros2_ws/src/my_robot_description/launch/ 目录下创建 rsp.launch.py:
import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node import xacro defgenerate_launch_description(): pkg_name ='my_robot_description'# 声明 use_sim_time 参数(仿真工程的标配) declare_use_sim_time = DeclareLaunchArgument('use_sim_time', default_value='false', description='Use simulation (Gazebo) clock if true')# 获取参数值 use_sim_time = LaunchConfiguration('use_sim_time')# 解析 Xacro 文件 xacro_file = os.path.join(get_package_share_directory(pkg_name),'urdf','robot.urdf.xacro') robot_description_config = xacro.process_file(xacro_file).toxml()# 配置 robot_state_publisher 节点 node_robot_state_publisher = Node( package='robot_state_publisher', executable='robot_state_publisher', output='screen', parameters=[{'robot_description': robot_description_config,'use_sim_time': use_sim_time }])return LaunchDescription([ declare_use_sim_time, node_robot_state_publisher ])提示:get_package_share_directory() 会自动找到包的安装路径,避免硬编码路径。注意:路径不要写死(如 /home/kason/...),否则换一台电脑或换一个用户名就会失效。
3. 配置 CMakeLists.txt
编辑 ~/ros2_ws/src/my_robot_description/CMakeLists.txt,在末尾添加:
# 安装 launch, urdf, worlds 等目录到 share 路径下 install(DIRECTORY launch urdf worlds config DESTINATION share/${PROJECT_NAME} ) 然后编译:
cd ~/ros2_ws colcon build --symlink-install 看到以下输出说明编译成功:
Starting >>> my_robot_description Finished <<< my_robot_description 如果使用 --symlink-install,以后修改 launch 文件或 urdf 文件后,直接运行即可生效,不需要重新编译。
刷新环境:
source install/setup.bash 验证 launch 文件是否被正确安装:
ros2 launch my_robot_description rsp.launch.py --show-args 成功时会显示:
Arguments: 'use_sim_time': Use simulation (Gazebo) clock if true (default: 'false') 注意:如果忘记修改 CMakeLists.txt,直接编译后运行会报错 file 'rsp.launch.py' was not found in the share directory。这是因为 ros2 launch 只会去 install/ 目录查找文件,不会去 src/。
4. 编写机器人 URDF 模型
在 ~/ros2_ws/src/my_robot_description/urdf/ 目录下创建 robot.urdf.xacro:
<?xml version="1.0"?><robotxmlns:xacro="http://www.ros.org/wiki/xacro"name="my_robot"><materialname="white"><colorrgba="1 1 1 1"/></material><!-- base_link 是机器人投影到地面的中心点,所有传感器和轮子都相对于它偏移 --><linkname="base_link"></link><jointname="chassis_joint"type="fixed"><parentlink="base_link"/><childlink="chassis"/><originxyz="-0.1 0 0"/></joint><linkname="chassis"><visual><originxyz="0.15 0 0.075"/><geometry><boxsize="0.3 0.3 0.15"/></geometry><materialname="white"/></visual></link></robot>注意:机器人模型必须以 base_link 作为根节点。Nav2 导航算法需要以它为旋转中心,直接以 chassis 开头会导致后续导航功能无法使用。
5. 在 RViz2 中可视化机器人
运行 Launch 文件:
ros2 launch my_robot_description rsp.launch.py 成功启动后你会看到类似输出:
[INFO] [launch]: All log files can be found below /home/user/.ros/log/... [INFO] [launch]: Default logging verbosity is set to INFO [INFO] [robot_state_publisher-1]: process started with pid [1234] [robot_state_publisher-1] [INFO] [1774688498.607462389] [robot_state_publisher]: Robot initialized 关键信号是看到 Robot initialized 这行字,说明机器人模型已被成功发布。
新开一个终端,打开 RViz2:
rviz2 配置步骤:
- Fixed Frame:将
map改为base_link - Add → 选择 RobotModel
- Add → 选择 TF
配置正确后,你将在 RViz 中央看到一个白色方块(chassis)。
图1 Fixed Frame,Add,RobotModel的位置:

图2 TF的位置:

图3 Description的位置,显示的白色方块

提示:如果看不到机器人,检查 RobotModel 下的 Description Topic 是否为 /robot_description。如果看到 TF 坐标轴但看不到白色方块,说明 RViz2 默认不知道去哪个话题找机器人描述,需要在 RobotModel 配置中手动选择 /robot_description。
6. 添加轮子
修改 robot.urdf.xacro,在 </robot> 前添加轮子定义:
<materialname="blue"><colorrgba="0.2 0.2 1 1"/></material><!-- 左轮 --><jointname="left_wheel_joint"type="continuous"><parentlink="base_link"/><childlink="left_wheel"/><originxyz="0 0.175 0"rpy="-${pi/2} 0 0"/><axisxyz="0 0 1"/></joint><linkname="left_wheel"><visual><geometry><cylinderradius="0.05"length="0.04"/></geometry><materialname="blue"/></visual></link><!-- 右轮 --><jointname="right_wheel_joint"type="continuous"><parentlink="base_link"/><childlink="right_wheel"/><originxyz="0 -0.175 0"rpy="-${pi/2} 0 0"/><axisxyz="0 0 1"/></joint><linkname="right_wheel"><visual><geometry><cylinderradius="0.05"length="0.04"/></geometry><materialname="blue"/></visual></link>提示:continuous 类型的关节可以无限旋转,适合用于动力轮。重新运行 ros2 launch my_robot_description rsp.launch.py,RViz 中将显示白色底盘和两个蓝色轮子。
如果 RViz2 显示如下错误:
Status: Error - right_wheel: No transform from [right_wheel] to [base_link] - left_wheel: No transform from [left_wheel] to [base_link] 这是因为 robot_state_publisher 知道轮子存在,但不知道轮子的实时角度。需要在 rsp.launch.py 的 generate_launch_description() 函数中,在 return LaunchDescription([ 之前添加 joint_state_publisher_gui 节点:
defgenerate_launch_description():# ... 前面的代码保持不变 ... node_joint_state_publisher_gui = Node( package='joint_state_publisher_gui', executable='joint_state_publisher_gui', output='screen')return LaunchDescription([ declare_use_sim_time, node_robot_state_publisher, node_joint_state_publisher_gui # <-- 新增])如果报错找不到包,运行:
sudoaptinstall ros-jazzy-joint-state-publisher-gui 添加后重新运行,将弹出一个带滑块的小窗口,拖动滑块可以看到轮子随之转动,RViz 中的错误也会消失。

7. 添加物理属性
为了让机器人在 Gazebo 中不"穿墙"或"飞天",需要添加 <collision> 和 <inertial> 标签。
定义惯性计算宏
修改 robot.urdf.xacro,将下列代码添加到文件的开头,注意要在 <robot> 标签内、第一个 <link> 定义之前添加:
<!-- 长方体惯性宏 --><xacro:macroname="inertial_box"params="mass x y z *origin"><inertial><xacro:insert_blockname="origin"/><massvalue="${mass}"/><inertiaixx="${(1/12) * mass * (y*y+z*z)}"ixy="0.0"ixz="0.0"iyy="${(1/12) * mass * (x*x+z*z)}"iyz="0.0"izz="${(1/12) * mass * (x*x+y*y)}"/></inertial></xacro:macro><!-- 圆柱体惯性宏(轮子用) --><xacro:macroname="inertial_cylinder"params="mass radius length *origin"><inertial><xacro:insert_blockname="origin"/><massvalue="${mass}"/><inertiaixx="${(1/12) * mass * (3*radius*radius + length*length)}"ixy="0.0"ixz="0.0"iyy="${(1/12) * mass * (3*radius*radius + length*length)}"iyz="0.0"izz="${(1/2) * mass * (radius*radius)}"/></inertial></xacro:macro>为底盘添加物理属性
<linkname="chassis"><visual><originxyz="0.15 0 0.075"/><geometry><boxsize="0.3 0.3 0.15"/></geometry><materialname="white"/></visual><collision><originxyz="0.15 0 0.075"/><geometry><boxsize="0.3 0.3 0.15"/></geometry></collision><xacro:inertial_boxmass="0.5"x="0.3"y="0.3"z="0.15"><originxyz="0.15 0 0.075"rpy="0 0 0"/></xacro:inertial_box></link>为轮子添加物理属性
<linkname="left_wheel"><visual><geometry><cylinderradius="0.05"length="0.04"/></geometry><materialname="blue"/></visual><collision><geometry><cylinderradius="0.05"length="0.04"/></geometry></collision><xacro:inertial_cylindermass="0.1"radius="0.05"length="0.04"><originxyz="0 0 0"rpy="0 0 0"/></xacro:inertial_cylinder></link>同样为 right_wheel 添加 collision 和 inertial。
注意:如果 link 中缺少 collision,轮子在 Gazebo 中会穿过地面;如果缺少 inertial,Gazebo 无法计算物理,机器人会"飘走"或"坠落"。
8. 创建 Gazebo 仿真环境
安装 ros_gz 桥接包
sudoapt update sudoaptinstall ros-jazzy-ros-gz 创建 sim.launch.py
在 ~/ros2_ws/src/my_robot_description/launch/ 目录下创建:
import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription from launch.actions import IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource from launch_ros.actions import Node defgenerate_launch_description(): pkg_name ='my_robot_description'# 1. 启动 robot_state_publisher rsp = IncludeLaunchDescription( PythonLaunchDescriptionSource([os.path.join( get_package_share_directory(pkg_name),'launch','rsp.launch.py')]), launch_arguments={'use_sim_time':'true'}.items())# 2. 启动 Gazebo Sim gazebo = IncludeLaunchDescription( PythonLaunchDescriptionSource([os.path.join( get_package_share_directory('ros_gz_sim'),'launch','gz_sim.launch.py')]), launch_arguments={'gz_args':'-r empty.sdf'}.items())# 3. 将机器人模型放入 Gazebo spawn_entity = Node( package='ros_gz_sim', executable='create', arguments=['-topic','robot_description','-name','my_cool_robot'], output='screen')return LaunchDescription([ rsp, gazebo, spawn_entity,])注意:现在使用了gazebo以后,sim.launch.py 中不要包含 joint_state_publisher_gui 节点,否则会和 Gazebo 的物理引擎产生冲突,会导致电脑崩溃。
编译并运行
注意这次增加了新的文件,因此需要再次使用colcon进行项目构建。并且用须要再次使用source命令,让ubuntu找到launch文件。
cd ~/ros2_ws colcon build --symlink-install source install/setup.bash ros2 launch my_robot_description sim.launch.py 成功启动后,你会看到 Gazebo 窗口弹出,里面有一个网格地面,以及你的机器人(白色底盘加两个蓝色轮子)出现在世界中央。
9. 添加万向轮
为了让机器人站稳(在两轮之外增加支撑点),修改 robot.urdf.xacro,在 </robot> 前添加:
<!-- 万向轮:gazebo reference 用于设置摩擦系数 --><jointname="caster_wheel_joint"type="fixed"><parentlink="chassis"/><childlink="caster_wheel"/><originxyz="0.24 0 0"/></joint><linkname="caster_wheel"><visual><geometry><sphereradius="0.05"/></geometry><materialname="black"/></visual><collision><geometry><sphereradius="0.05"/></geometry></collision><inertial><originxyz="0 0 0"rpy="0 0 0"/><massvalue="0.1"/><inertiaixx="0.0001"ixy="0.0"ixz="0.0"iyy="0.0001"iyz="0.0"izz="0.0001"/></inertial></link><gazeboreference="caster_wheel"><mu1value="0.001"/><mu2value="0.001"/></gazebo>提示:万向轮需要极低的摩擦系数(mu1, mu2 接近 0),否则机器人转弯时会像被粘住。
重新运行 sim.launch.py,机器人将端端正正地站在地面上,不再向前或向后倾倒。
注意:动力轮的摩擦系数必须设置得比较高(mu1, mu2 > 1.0),如果也设成 0.001,轮子会原地打滑无法前进。
10. 添加差速驱动插件
在 robot.urdf.xacro 的 </robot> 前添加:
<gazebo><pluginfilename="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><max_wheel_torque>20</max_wheel_torque><max_wheel_acceleration>1.0</max_wheel_acceleration><topic>cmd_vel</topic><odom_topic>odom</odom_topic><frame_id>odom</frame_id><child_frame_id>base_link</child_frame_id><publish_odom>true</publish_odom><publish_odom_tf>true</publish_odom_tf><publish_wheel_tf>true</publish_wheel_tf></plugin><pluginfilename="gz-sim-joint-state-publisher-system"name="gz::sim::systems::JointStatePublisher"></plugin></gazebo>重新运行 sim.launch.py,机器人依然出现在 Gazebo 中,只是现在它已经准备好接收控制指令了。
11. 键盘控制机器人
建立 ROS 2 与 Gazebo 的通信桥接
新开一个终端:
ros2 run ros_gz_bridge parameter_bridge /cmd_vel@geometry_msgs/msg/[email protected] 成功运行后会显示类似:
[INFO] [gz_ros_bridge]: Creating GzROS2Bridge: topic /cmd_vel (ros -> gz)" 启动键盘控制
再开一个新终端:
ros2 run teleop_twist_keyboard teleop_twist_keyboard 成功后终端显示操作指南:
Reading from the keyboard and publishing to Twist! --------------------------- Moving around: u i o j k l m , . anything else : stop q/z : increase/decrease max speeds by 10% w/x : increase/decrease only linear speed by 10% e/c : increase/decrease only angular speed by 10% Currently: speed 0.5 turn 1.0 现在点击这个终端使其激活,按 i 键让机器人前进,按 l 键向右转,按 ,(逗号)后退。观察 Gazebo 中的机器人是否随之移动。

避坑总结
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
file not found | CMakeLists.txt 没有 install 指令 | 添加 install(DIRECTORY …) |
No transform from xxx | 缺少 joint_state_publisher | 添加 joint_state_publisher_gui |
| Gazebo 黑屏/崩溃 | joint_state_publisher_gui 冲突 | 仿真时注释掉它 |
| 轮子穿过地面 | 缺少 collision | 添加 collision 标签 |
| 机器人飘走 | 缺少 inertial | 添加惯性参数 |
| 机器人原地打滑 | 动力轮摩擦系数太低 | 设置 mu1, mu2 > 1.0 |
恭喜!
你已完成一个能用键盘控制的两轮机器人。掌握的知识:
- ROS 2 工程标准结构
- Xacro 机器人建模
- Gazebo 物理仿真
- ROS 2 与 Gazebo 桥接通信
12. 原理和总结
当你运行 teleop_twist_keyboard 并按下按键时,机器人之所以能动,是因为在 ROS 2 的世界里发生了一场精准的**“接力赛”**。
我们可以把这个过程拆解为三个核心环节:消息协议(Twist)、发布订阅机制(Topic) 和 物理执行器(Gazebo Plugin)。
1)消息类型:geometry_msgs/msg/Twist
ROS 2 中,几乎所有的移动机器人都使用同一种指令格式——Twist。
当你按下 i(前进)时,teleop_twist_keyboard 会生成一个结构如下的数据包:
- Linear (线速度): x = 0.5 , y = 0.0 , z = 0.0 x=0.5, y=0.0, z=0.0 x=0.5,y=0.0,z=0.0 (代表向前冲)
- Angular (角速度): z = 0.0 z=0.0 z=0.0 (代表不转弯)
这个数据包不关心你的机器人是长方形还是圆形,它只表达一个抽象的运动意图。
输入ros2 interface show geometry_msgs/msg/Twist可以查看Twist数据类型的结构

2)传递的信道:/cmd_vel 话题
teleop_twist_keyboard 就像一个广播电台,它持续向一个名为 /cmd_vel (Command Velocity) 的频道发送上述的 Twist 消息。
- 它是标准约定:在 ROS 社区中,默认所有移动机器人都应该监听
/cmd_vel来获取运动指令。这就像所有收音机都要调到同一个频率才能听到广播一样。
在仿真运行期间输入ros2 topic list 可以查看所有话题

输入ros2 topic info /cmd_vel查看 /cmd_vel 的详细信息(包含类型)

3)关键的接棒者:Gazebo 差速驱动插件
这是最关键的一步。由于你的机器人是在仿真环境里的,它没有真实的电机。你在 Xacro 中加入的 DiffDrive 插件 充当了“翻译官”和“虚拟电机”的角色:
- 监听:插件在后台一直盯着
/cmd_vel话题。 - 计算 (逆运动学):当它收到“线速度 0.5m/s”的指令时,它会根据你在 Xacro 里设置的参数(
wheel_separation轮距 和wheel_radius轮径)进行计算。- 公式示例:为了达到 0.5m/s,左右两个轮子分别需要转多快?
- 施力:计算出转速后,插件直接向 Gazebo 物理引擎中的
left_wheel_joint和right_wheel_joint施加扭矩(Torque)。
4)为什么需要那个 ros_gz_bridge?
你在操作时运行了一个特殊的命令:ros_gz_bridge。这是因为:
- Teleop 节点 跑在 ROS 2 协议上。
- Gazebo 仿真器 跑在 Gazebo Transport 协议上。
这个“桥梁”就像一个同声传译,它把 ROS 2 频道里的 /cmd_vel 抓过来,实时翻译成 Gazebo 插件能听懂的格式并扔进仿真世界里。
5)核心特性:异步与多对多
这是 ROS 2 话题机制最强大的地方:
异步性:
发布者发完就走,不需要等订阅者回话。这保证了机器人控制的实时性。
多对多:
一个话题可以有多个发布者:比如你可以同时用键盘和自动导航算法给小车发指令(虽然这会导致小车打架)。
一个话题可以有多个订阅者:比如 /cmd_vel 话题,除了 Gazebo 插件在听,你还可以开一个日志节点记录小车的运动轨迹,它们互不干扰。
5)总结:小车运动的全路径
- 你按下
i->teleop_twist_keyboard发出 Twist 消息。 - Twist 消息 进入
/cmd_vel话题。 - Bridge 桥梁 将消息从 ROS 传给 Gazebo。
- DiffDrive 插件 收到消息,计算轮速,给轮子加力。
- Gazebo 物理引擎 计算摩擦力,机器人向前滑动。
接下来,我将为小车增加一个激光雷达,用于感知周围环境。下一节的链接:【从零开始创建一个ROS2机器人工程(2)】为小车增加激光雷达感知