ROS2 URDF 与 Xacro
一、URDF 是什么
上一节我们把坐标系之间的关系搭好了,但那些坐标系还是抽象的——base_link 到底是个什么东西?多大、什么样?
URDF(Unified Robot Description Format,统一机器人描述格式)就是回答这个问题的:它用一份 XML 文件,把机器人的"身体"描述清楚——有哪些部件、每个部件多大、用什么颜色、部件之间怎么连接、能怎么动。
URDF 里只有两种基本元素:
| 元素 | 对应现实 | 说明 |
|---|---|---|
<link> | 连杆 / 部件 | 一块刚体。车体、轮子、机械臂的一节,都是一个 link |
<joint> | 关节 / 连接 | 两个 link 之间的连接方式,决定了它们能否相对运动 |
说明URDF 与 TF 树的关系 一个机器人有 N 个 link 和 N-1 个 joint,恰好构成一棵树——和上一节的 TF 树是一一对应的。每个 link 对应一个坐标系,每个 joint 定义了两者之间的变换。 所以 URDF 不是"模型文件",它同时是几何描述和坐标关系的定义。后面
robot_state_publisher就是拿这份文件 + 各关节的当前角度,实时算出整棵 TF 树发出去的。
二、link 与 joint 详解
1. link 的三个子标签
每个 <link> 里面可以放三类描述,它们各管一摊、互不干扰:
| 子标签 | 作用 | 谁在用 |
|---|---|---|
<visual> | 外观:形状、颜色、贴图 | RViz2 显示、人眼看 |
<collision> | 碰撞体:探测碰撞用的简化几何 | 物理仿真、运动规划 |
<inertial> | 惯性:质量和转动惯量 | 动力学仿真 |
三者形状可以不一样。比如视觉上用精细的网格模型(mesh)看起来漂亮,碰撞体却用一个粗糙的圆柱——因为碰撞检测需要快,用精细模型纯属自找麻烦。
<geometry> 支持四种形状:
<!-- 长方体:三个方向的尺寸(米) -->
<box size="0.40 0.30 0.15"/>
<!-- 圆柱:半径 + 长度(默认沿 z 轴) -->
<cylinder radius="0.06" length="0.03"/>
<!-- 球:半径 -->
<sphere radius="0.05"/>
<!-- 网格模型:需要提供 .stl / .dae 文件路径 -->
<mesh filename="package://my_robot/meshes/base.dae"/>2. joint 的六种类型
| 类型 | 自由度 | 典型用途 |
|---|---|---|
fixed | 0(刚性连接) | 传感器固定安装在底盘上 |
continuous | 1(旋转,无限位) | 轮子——可以一直转下去 |
revolute | 1(旋转,有上下限) | 机械臂关节——转到 90° 就到头了 |
prismatic | 1(直线滑动,有上下限) | 导轨、升降机构、夹爪 |
floating | 6 | 很少用 |
planar | 3 | 很少用 |
continuous 和 revolute 的区别就是有没有限位。轮子没有"转到头"的概念,用 continuous;机械臂关节有物理限位,用 revolute 并给出 lower / upper。
3. joint 的必备子标签
<joint name="left_wheel_joint" type="continuous">
<!-- 父连杆(这棵树的上一级) -->
<parent link="base_link"/>
<!-- 子连杆(下一级) -->
<child link="left_wheel"/>
<!-- 子连杆原点在父坐标系下的位姿:xyz 平移(米)、rpy 旋转(弧度) -->
<origin xyz="0 0.17 -0.05" rpy="0 0 0"/>
<!-- 运动轴:绕哪个轴转 / 沿哪个轴滑 -->
<axis xyz="0 1 0"/>
<!-- 限位:continuous 类型不需要写 limit -->
<!-- revolute / prismatic 必须写,且必须有 lower 和 upper -->
<!-- <limit effort="10.0" velocity="5.0" lower="-1.57" upper="1.57"/> -->
</joint>⚠️ 警告三个新手必踩的坑rpy 的单位是弧度,不是角度。想转 90° 要写
1.5708。 同样是"一个父节点"规则。一个 link 只能出现在一个 joint 的<child>里,否则 URDF 解析直接失败——和 TF 树那条铁律是同一回事。 revolute / prismatic 缺 limit 会报错,而且lower/upper不能省。只写effort和velocity不算合格。
三、完整示例:造一辆两轮小车
下面这份 URDF 描述一台差速小车:车体一个盒子,左右两个轮子可以转,前方一个万向轮支撑,顶上固定一个雷达。
<?xml version="1.0"?>
<robot name="my_car">
<!-- ==================== 车体 ==================== -->
<link name="base_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="0.40 0.30 0.15"/>
</geometry>
<material name="blue">
<color rgba="0.2 0.4 0.8 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="0.40 0.30 0.15"/>
</geometry>
</collision>
<inertial>
<mass value="5.0"/>
<inertia ixx="0.05" ixy="0.0" ixz="0.0"
iyy="0.08" iyz="0.0"
izz="0.10"/>
</inertial>
</link>
<!-- ==================== 左轮 ==================== -->
<link name="left_wheel">
<visual>
<!-- 圆柱默认沿 z 轴,绕 x 轴转 90° 才能立起来当轮子 -->
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.06" length="0.03"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.06" length="0.03"/>
</geometry>
</collision>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<!-- 车体左侧:y 为正方向(ROS 约定:x 前、y 左、z 上) -->
<origin xyz="0.0 0.17 -0.05" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<!-- ==================== 右轮 ==================== -->
<link name="right_wheel">
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.06" length="0.03"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1.0"/>
</material>
</visual>
</link>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<origin xyz="0.0 -0.17 -0.05" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<!-- ==================== 万向轮(固定,纯支撑) ==================== -->
<link name="caster">
<visual>
<geometry>
<sphere radius="0.03"/>
</geometry>
<material name="grey">
<color rgba="0.5 0.5 0.5 1.0"/>
</material>
</visual>
</link>
<joint name="caster_joint" type="fixed">
<parent link="base_link"/>
<child link="caster"/>
<origin xyz="-0.15 0.0 -0.075" rpy="0 0 0"/>
</joint>
<!-- ==================== 雷达(固定在车顶) ==================== -->
<link name="laser">
<visual>
<geometry>
<cylinder radius="0.04" length="0.05"/>
</geometry>
<material name="red">
<color rgba="0.9 0.2 0.2 1.0"/>
</material>
</visual>
</link>
<joint name="laser_joint" type="fixed">
<parent link="base_link"/>
<child link="laser"/>
<origin xyz="0.10 0.0 0.10" rpy="0 0 0"/>
</joint>
</robot>这份文件的 TF 树是:
base_link
├── left_wheel_joint ──→ left_wheel (continuous)
├── right_wheel_joint ──→ right_wheel (continuous)
├── caster_joint ──→ caster (fixed)
└── laser_joint ──→ laser (fixed)注意 base_link 有四个子节点但自己是根——完全合法的树。另外你可能注意到轮子的 rpy="1.5708 0 0" 写在 <visual> 里而不是 joint 里:这是因为轮子的关节不动(只是绕 y 轴旋转),但圆柱默认立着,得把它转 90° 躺下来才是轮子的样子。这两件事要分清楚,一个描述运动,一个描述外观朝向。
四、Xacro:让 URDF 不再重复
上面那台小车只有 4 个 link,文件就快 100 行了。如果是一台六轴机械臂,六段结构几乎一样、只有尺寸和名字不同,全写出来会又长又容易错。
Xacro 就是为此而生的:它是 URDF 的"宏扩展版",支持变量、数学运算和可复用的宏。写法上基本兼容 URDF,只在需要的地方加上 xacro: 前缀。
1. 属性:把重复的数字抽出来
<robot name="my_car" xmlns:xacro="http://www.ros.org/wiki/xacro">
<!-- 定义属性 -->
<xacro:property name="wheel_radius" value="0.06"/>
<xacro:property name="wheel_width" value="0.03"/>
<xacro:property name="wheel_y" value="0.17"/>
<link name="left_wheel">
<visual>
<!-- ${} 里可以写表达式 -->
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
</visual>
</link>
<!-- 表达式支持四则运算 -->
<xacro:property name="half_width" value="${wheel_y / 2}"/>
</robot>2. 宏:一次定义,多次复用
这是 Xacro 最值钱的功能。机械臂的每一段、机器人的每一条腿,结构都高度相似,用宏可以只写一遍:
<robot name="my_arm" xmlns:xacro="http://www.ros.org/wiki/xacro">
<!-- 定义一个"轮子"宏,参数是要替换的部分 -->
<xacro:macro name="wheel" params="prefix y_offset">
<link name="${prefix}_wheel">
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.06" length="0.03"/>
</geometry>
</visual>
</link>
<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel"/>
<origin xyz="0 ${y_offset} -0.05" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
</xacro:macro>
<!-- 用两次,各给一组参数 -->
<xacro:wheel prefix="left" y_offset="0.17"/>
<xacro:wheel prefix="right" y_offset="-0.17"/>
</robot>以后要改轮子半径,只改宏里那一处,两个轮子同时生效。
3. 引入其他文件
<!-- 把公共部分拆到单独文件,主文件按需引入 -->
<xacro:include filename="$(find my_robot)/urdf/common/wheel.xacro"/>
<xacro:include filename="$(find my_robot)/urdf/materials.xacro"/>4. 转换与使用
Xacro 文件本身不是 URDF,用之前要转换。命令行手工转:
# 展开宏、算出所有表达式,输出标准 URDF
xacro my_robot.urdf.xacro > my_robot.urdf但在实际项目里没人手工转——都是让 launch 文件在运行时自动处理。这一点下面实践部分会讲。
五、学习内容:从零写一个机器人并在 RViz2 中显示
(一)创建功能包
cd ~/ros_code/src
ros2 pkg create --build-type ament_cmake my_robot_description \
--dependencies urdf xacro robot_state_publisher joint_state_publisher_gui说明一下这个包名为什么叫 description:只放模型描述的包,社区惯例都这么命名。
建几个目录存放文件:
cd ~/ros_code/src/my_robot_description
mkdir -p urdf launch rviz(二)编写 URDF 文件
把前面那份小车 URDF 存成 urdf/my_car.urdf。
(三)用命令行校验语法
写 URDF 最容易犯语法错,好在有现成的检查工具:
# 安装工具(只需一次)
sudo apt install liburdfdom-tools# 检查语法与结构
check_urdf ~/ros_code/src/my_robot_description/urdf/my_car.urdf输出类似这样说明没问题:
robot name is: my_car
---------- Successfully Parsed XML ---------------
root Link: base_link has 4 child(ren)
child(1): left_wheel
child(2): right_wheel
child(3): caster
child(4): laser还可以直接生成一张结构图,比在脑子里推树结构快得多:
urdf_to_graphiz ~/ros_code/src/my_robot_description/urdf/my_car.urdf
# 会生成 my_car.pdf 和 my_car.gv(四)改写为 Xacro 版本
新建 urdf/my_car.urdf.xacro,把尺寸抽成属性、把两个轮子改成宏(参照上一节的写法)。转换一下对比输出:
cd ~/ros_code/src/my_robot_description/urdf
xacro my_car.urdf.xacro > /tmp/expanded.urdf
check_urdf /tmp/expanded.urdf打开 /tmp/expanded.urdf 看一眼,你会发现宏已经被展开成完整的 XML——Xacro 只是写法上的糖,最终产物还是标准 URDF。
(五)在 RViz2 中显示
要让模型动起来,需要三个节点配合。新建 launch/display.launch.py:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.substitutions import Command
def generate_launch_description():
pkg_share = get_package_share_directory('my_robot_description')
xacro_file = os.path.join(pkg_share, 'urdf', 'my_car.urdf.xacro')
# 用 xacro 命令把文件展开成字符串,作为 robot_description 参数
robot_description = {
'robot_description': Command(['xacro ', xacro_file])
}
return LaunchDescription([
# 1. 发布 robot_description(模型本体)并计算 TF
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[robot_description, {'use_sim_time': False}],
output='screen',
),
# 2. 提供可拖动的关节滑块,模拟关节角度
Node(
package='joint_state_publisher_gui',
executable='joint_state_publisher_gui',
output='screen',
),
# 3. 可视化
Node(
package='rviz2',
executable='rviz2',
name='rviz2',
output='screen',
),
])说明三个节点各干什么
robot_state_publisher:读robot_description参数(URDF 内容),再结合关节角度,算出并发布整棵 TF 树。固定关节的变换它直接发静态变换,可动关节则等/joint_states话题的数据。
joint_state_publisher_gui:弹出一堆滑块,让你手工拖关节角度,发布到/joint_states。没有真机时,就靠它来"手动驱动"机器人。
rviz2:把模型画出来。
⚠️ 警告robot_description 要的是内容,不是路径
robot_state_publisher的参数是 URDF 的字符串内容,不是文件路径。这就是上面要用Command(['xacro ', xacro_file])的原因——它在启动时执行 xacro 命令,把展开后的内容塞进去。直接传路径会报解析失败。
把文件存到 launch/display.launch.py,然后在 CMakeLists.txt 里加上安装规则:
install(
DIRECTORY urdf launch rviz
DESTINATION share/${PROJECT_NAME}
)(六)编译运行
cd ~/ros_code
colcon build --packages-select my_robot_description
source install/setup.bash
ros2 launch my_robot_description display.launch.pyRViz2 打开后默认是空的,需要手工加上显示项:左侧 Add → 选 RobotModel(机器人模型)和 TF(坐标树)。如果模型没出现,先看下面第六节的排查表——多半是 Fixed Frame 的问题。
拖动 joint_state_publisher_gui 的滑块,轮子会跟着转,RViz 里的 TF 也在实时变化。到这一步,"URDF → TF 树 → 可视化"这条链路就完整打通了。
(七)顺带一提:官方示例包
如果只是想先看看标准 URDF 长什么样,可以装官方教程包:
sudo apt install ros-jazzy-urdf-tutorial
ros2 launch urdf_tutorial display.launch.py model:=urdf/01-myfirst.urdf它自带一系列循序渐进的示例(从最简单的单个方块,到多种形状、材质、可动关节),改 model:= 后面的文件名就能切换,是很好的对照材料。
六、常见问题排查
| 现象 | 原因与处理 |
|---|---|
check_urdf 报 XML 解析错误 | 多半是标签没闭合、尖括号写错、或者属性值少了引号。报错信息里会给出具体行号,对着改。 |
| 报"link xxx has multiple parents" | 同一个 link 出现在了两个 joint 的 <child> 里。URDF 必须是树,每个 link 只能有一个父节点。 |
| 报 revolute 关节缺 limit | revolute 和 prismatic 必须写 <limit>,且 lower / upper 一个都不能少。continuous 和 fixed 不需要。 |
| RViz 里模型整体看不见 | 九成是 Fixed Frame 设错了。把它改成 URDF 里的根 link 名(本例是 base_link),并且确认 robot_state_publisher 已经正常启动。 |
| 模型出现但关节拖不动 | joint_state_publisher_gui 没启动,或者 URDF 里全是 fixed 关节——固定关节当然没有滑块。 |
| 改了 URDF 但 RViz 里没变化 | robot_description 是启动时读取的,需要重启 launch 文件。另外别忘了先重新 colcon build,否则改的是源码、跑的是旧的安装副本。 |
| 轮子/圆柱方向不对(躺倒或竖着) | 圆柱默认沿 z 轴。要做成轮子,得在 <visual> 和 <collision> 的 <origin> 里加 rpy="1.5708 0 0" 把它转 90°。 |
说明本节小结 URDF 用
<link>和<joint>两种元素描述机器人,整体是一棵树——和 TF 树完全对应。link 里visual管好看、collision管碰撞、inertial管动力学,三者可以不一样。Xacro 是 URDF 的宏扩展,用属性、表达式、宏来消除重复,项目里基本都用它。写完之后一定用
check_urdf过一遍,别等到 RViz 里发现不对劲再回头找。下一节我们把 RViz2 单独讲透——它是你以后调试机器人花时间最多的地方。