Skip to content

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> 里面可以放三类描述,它们各管一摊、互不干扰:

子标签作用谁在用
<visual>外观:形状、颜色、贴图RViz2 显示、人眼看
<collision>碰撞体:探测碰撞用的简化几何物理仿真、运动规划
<inertial>惯性:质量和转动惯量动力学仿真

三者形状可以不一样。比如视觉上用精细的网格模型(mesh)看起来漂亮,碰撞体却用一个粗糙的圆柱——因为碰撞检测需要快,用精细模型纯属自找麻烦。

<geometry> 支持四种形状:

xml
<!-- 长方体:三个方向的尺寸(米) -->
<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 的六种类型 ​

类型自由度典型用途
fixed0(刚性连接)传感器固定安装在底盘上
continuous1(旋转,无限位)轮子——可以一直转下去
revolute1(旋转,有上下限)机械臂关节——转到 90° 就到头了
prismatic1(直线滑动,有上下限)导轨、升降机构、夹爪
floating6很少用
planar3很少用

continuous 和 revolute 的区别就是有没有限位。轮子没有"转到头"的概念,用 continuous;机械臂关节有物理限位,用 revolute 并给出 lower / upper。

3. joint 的必备子标签 ​

xml
<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
<?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 树是:

text
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. 属性:把重复的数字抽出来 ​

xml
<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 最值钱的功能。机械臂的每一段、机器人的每一条腿,结构都高度相似,用宏可以只写一遍:

xml
<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. 引入其他文件 ​

xml
<!-- 把公共部分拆到单独文件,主文件按需引入 -->
<xacro:include filename="$(find my_robot)/urdf/common/wheel.xacro"/>
<xacro:include filename="$(find my_robot)/urdf/materials.xacro"/>

4. 转换与使用 ​

Xacro 文件本身不是 URDF,用之前要转换。命令行手工转:

bash
# 展开宏、算出所有表达式,输出标准 URDF
xacro my_robot.urdf.xacro > my_robot.urdf

但在实际项目里没人手工转——都是让 launch 文件在运行时自动处理。这一点下面实践部分会讲。

五、学习内容:从零写一个机器人并在 RViz2 中显示 ​

(一)创建功能包 ​

bash
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:只放模型描述的包,社区惯例都这么命名。

建几个目录存放文件:

bash
cd ~/ros_code/src/my_robot_description
mkdir -p urdf launch rviz

(二)编写 URDF 文件 ​

把前面那份小车 URDF 存成 urdf/my_car.urdf。

(三)用命令行校验语法 ​

写 URDF 最容易犯语法错,好在有现成的检查工具:

bash
# 安装工具(只需一次)
sudo apt install liburdfdom-tools
bash
# 检查语法与结构
check_urdf ~/ros_code/src/my_robot_description/urdf/my_car.urdf

输出类似这样说明没问题:

text
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

还可以直接生成一张结构图,比在脑子里推树结构快得多:

bash
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,把尺寸抽成属性、把两个轮子改成宏(参照上一节的写法)。转换一下对比输出:

bash
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:

python
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 里加上安装规则:

cmake
install(
  DIRECTORY urdf launch rviz
  DESTINATION share/${PROJECT_NAME}
)

(六)编译运行 ​

bash
cd ~/ros_code
colcon build --packages-select my_robot_description
source install/setup.bash
ros2 launch my_robot_description display.launch.py

RViz2 打开后默认是空的,需要手工加上显示项:左侧 Add → 选 RobotModel(机器人模型)和 TF(坐标树)。如果模型没出现,先看下面第六节的排查表——多半是 Fixed Frame 的问题。

拖动 joint_state_publisher_gui 的滑块,轮子会跟着转,RViz 里的 TF 也在实时变化。到这一步,"URDF → TF 树 → 可视化"这条链路就完整打通了。

(七)顺带一提:官方示例包 ​

如果只是想先看看标准 URDF 长什么样,可以装官方教程包:

bash
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 关节缺 limitrevolute 和 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 单独讲透——它是你以后调试机器人花时间最多的地方。