ROS 2 URDF 机器人建模:从概念到开发与调试全记录

问题背景

在 ROS 2 中开发移动机器人、机械臂或任何有多关节结构的机器人时,都需要回答一个基础问题:“这个机器人长什么样、各部分怎么连接的?” 导航需要知道机器人的外形轮廓来做碰撞检测,MoveIt 需要知道关节的运动范围和连杆的几何关系,RViz 需要知道怎么把机器人画出来,Gazebo 需要知道每个连杆的质量、惯性和碰撞形状来做物理仿真。

URDF(Unified Robot Description Format)就是 ROS 中描述机器人模型的标准格式。它用 XML 定义机器人的连杆(link)、关节(joint)和它们之间的拓扑关系,是整个机器人软件栈的基础数据源。

很多开发者能从教程里抄一个 URDF 跑起来,但在实际项目中遇到"robot_state_publisher 发布 TF 不全"、“Gazebo 模型塌了”、“MoveIt 规划结果和实际不符”、"xacro 宏展开报错"这些问题时,往往不知道从哪里排查。这篇笔记侧重开发和调试视角,把 URDF 从概念、原理、编写、工具链到常见踩坑完整串一遍。

本笔记基于 ROS 2 Humble Hawksbill(2022 年 5 月发布,LTS 版本,官方支持到 2027 年 5 月)编写。

URDF 的核心概念

什么是 URDF

URDF 是一个 XML 文件,描述一个单机器人的以下信息:

  • 连杆(Link):刚体部件,有几何形状(可视化)、碰撞包络(碰撞检测)和物理属性(质量、惯性)。
  • 关节(Joint):连接两个连杆的运动副,定义运动类型(旋转、平移、固定等)、运动范围和运动轴。
  • 拓扑结构:所有 link 和 joint 组成一棵树形结构,每个 link 最多有一个父 joint。

URDF 只描述一个机器人。如果场景中有多个机器人,需要用多个 URDF + namespace 来管理,或者用 SDF(Simulation Description Format)。

URDF 的树形结构

base_link
 ├── joint1 (revolute) → link1
 │    └── joint2 (revolute) → link2
 │         └── joint3 (revolute) → link3
 └── joint_base_laser (fixed) → laser_link

关键约束:

  1. 严格的树形结构。没有环——不能出现一个 link 有多个父 joint 的情况。
  2. 每个 link 最多一个父 joint。但一个 link 可以有多个子 joint。
  3. 必须有且只有一个根 link(没有任何 joint 指向它的 link)。

如果违反这些约束,URDF 解析器会直接报错拒绝加载。

Link 的三大组成部分

<link name="base_link">
  <!-- 1. 可视化:RViz 中显示的样子 -->
  <visual>
    <geometry>
      <box size="0.4 0.3 0.1"/>
    </geometry>
    <material name="blue">
      <color rgba="0 0 0.8 1"/>
    </material>
  </visual>

  <!-- 2. 碰撞:碰撞检测用的包络(可以和 visual 不同) -->
  <collision>
    <geometry>
      <box size="0.4 0.3 0.1"/>
    </geometry>
  </collision>

  <!-- 3. 惯性:物理仿真用的质量和惯量矩阵 -->
  <inertial>
    <mass value="5.0"/>
    <inertia ixx="0.01" ixy="0" ixz="0"
             iyy="0.01" iyz="0" izz="0.01"/>
  </inertial>
</link>
部分 用途 是否必须
<visual> RViz 渲染、可视化 可选(没有就不显示)
<collision> 碰撞检测(MoveIt、Gazebo) 可选(没有就不做碰撞检测)
<inertial> 物理仿真(Gazebo) Gazebo 仿真必须,纯 RViz 可视化可以不要

visual 和 collision 可以不同。实践中,collision 通常用更简单的几何体(盒子、圆柱)近似复杂外形,减少碰撞检测的计算量。

Joint 的类型

<joint name="wheel_joint" type="continuous">
  <parent link="base_link"/>
  <child link="left_wheel"/>
  <origin xyz="0 0.15 -0.05" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
</joint>
Joint 类型 自由度 运动范围 典型用途
revolute 1(旋转) 有限(需指定 limit) 机械臂关节
continuous 1(旋转) 无限(无限制) 车轮
prismatic 1(平移) 有限(需指定 limit) 直线导轨
fixed 0 不能动 传感器安装、结构连接
floating 6 任意 自由漂浮体(少用)
planar 2 平面内 平面移动平台(少用)

revolutecontinuous 的区别:revolute 有角度限制(比如 -π 到 π),continuous 没有(可以无限旋转)。机械臂关节用 revolute,车轮用 continuous

Joint 的 origin 和 axis

  • <origin>:描述 child link 相对于 parent link 的安装位置。xyz 是平移,rpy 是旋转(roll-pitch-yaw,单位弧度)。
  • <axis>:关节的运动轴方向,在 child link 的坐标系中描述。默认 (1, 0, 0) 即 X 轴。
<!-- 绕 Z 轴旋转的关节 -->
<joint name="shoulder_yaw" type="revolute">
  <parent link="base_link"/>
  <child link="upper_arm"/>
  <origin xyz="0 0 0.3" rpy="0 0 0"/>
  <axis xyz="0 0 1"/>
  <limit lower="-3.14" upper="3.14" effort="100" velocity="1.5"/>
</joint>

<limit> 只在 revoluteprismatic 中必须:

属性 含义
lower 下界(弧度或米)
upper 上界(弧度或米)
effort 最大力/力矩(N 或 Nm)
velocity 最大速度(m/s 或 rad/s)

URDF 的底层原理

URDF 解析流程

URDF 文件 (XML)
    │
    ▼
urdf::Model::initFile() / initString()
    │
    ▼
解析为 C++ 数据结构 (urdf::Model)
    │
    ├── 遍历所有 link,构建 Link 对象(visual/collision/inertial)
    ├── 遍历所有 joint,构建 Joint 对象(type/parent/child/axis/limit)
    └── 验证树形结构完整性

解析后的 urdf::Model 可以被多个模块使用:

  • robot_state_publisher:读取 URDF,发布所有 fixed joint 的静态 TF 变换,并订阅 /joint_states 话题来发布活动 joint 的动态 TF 变换。
  • rviz2:通过 RobotModel 显示插件渲染机器人外观。
  • gazebo_ros:将 URDF 转换为 SDF 并加载到物理仿真引擎。
  • moveit:构建运动学模型,用于路径规划。

robot_state_publisher 的工作原理

robot_state_publisher 是 URDF 与 TF 系统之间的桥梁:

URDF 文件 ──────────┐
                    ├──→ robot_state_publisher ──→ /tf + /tf_static
/joint_states ──────┘

它的工作分为两部分:

  1. 静态变换:所有 fixed 类型的 joint 产生的变换,在启动时通过 /tf_static 发布一次。比如传感器安装位置、结构件之间的固定连接。

  2. 动态变换:所有非 fixed 类型的 joint(revolutecontinuousprismatic),根据 /joint_states 话题中报告的关节角度/位移,实时计算并发布对应变换到 /tf

这意味着:如果 robot_state_publisher 没启动,URDF 中定义的所有 link 之间的 TF 变换都不会发布。这是调试 TF 问题时最常见的遗漏之一。

joint_states 话题

/joint_states 是关节状态的统一接口:

sensor_msgs/msg/JointState
├── header
│   └── stamp
├── name: string[]       ← 关节名称列表
├── position: float64[]  ← 关节位置(弧度或米)
├── velocity: float64[]  ← 关节速度
└── effort: float64[]    ← 关节力/力矩

robot_state_publisher 订阅这个话题,根据每个关节的当前位置重新计算 TF 并发布。

URDF 中的几何类型

类型 XML 写法 说明
盒子 <box size="x y z"/> 长宽高(米)
圆柱 <cylinder radius="r" length="l"/> 半径和长度
<sphere radius="r"/> 半径
Mesh <mesh filename="package://pkg/meshes/part.dae"/> 3D 模型文件

Mesh 文件支持 .dae(Collada)、.stl.obj 格式。推荐使用 .dae(自带颜色和纹理)做 visual,.stl 做 collision(更轻量)。

开发:编写 URDF

纯 URDF 写法(适合简单模型)

一个最简单的两轮差速机器人 URDF:

<?xml version="1.0"?>
<robot name="diffbot">

  <!-- 底盘 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.4 0.3 0.1"/>
      </geometry>
      <material name="blue">
        <color rgba="0 0 0.8 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.4 0.3 0.1"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="5.0"/>
      <inertia ixx="0.02" ixy="0" ixz="0"
               iyy="0.03" iyz="0" izz="0.04"/>
    </inertial>
  </link>

  <!-- 左轮 -->
  <link name="left_wheel">
    <visual>
      <geometry>
        <cylinder radius="0.05" length="0.03"/>
      </geometry>
      <material name="black">
        <color rgba="0.1 0.1 0.1 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.05" length="0.03"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.5"/>
      <inertia ixx="0.001" ixy="0" ixz="0"
               iyy="0.001" iyz="0" izz="0.001"/>
    </inertial>
  </link>

  <joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel"/>
    <origin xyz="0 0.15 -0.05" rpy="-1.5708 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- 右轮 -->
  <link name="right_wheel">
    <visual>
      <geometry>
        <cylinder radius="0.05" length="0.03"/>
      </geometry>
      <material name="black">
        <color rgba="0.1 0.1 0.1 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.05" length="0.03"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.5"/>
      <inertia ixx="0.001" ixy="0" ixz="0"
               iyy="0.001" iyz="0" izz="0.001"/>
    </inertial>
  </link>

  <joint name="right_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="right_wheel"/>
    <origin xyz="0 -0.15 -0.05" rpy="1.5708 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- 雷达(固定安装) -->
  <link name="laser_link">
    <visual>
      <geometry>
        <cylinder radius="0.03" length="0.02"/>
      </geometry>
      <material name="red">
        <color rgba="0.8 0 0 1"/>
      </material>
    </visual>
  </link>

  <joint name="laser_joint" type="fixed">
    <parent link="base_link"/>
    <child link="laser_link"/>
    <origin xyz="0.15 0 0.07" rpy="0 0 0"/>
  </joint>

</robot>

纯 URDF 的问题:如果机器人有对称结构(比如左右轮、前后腿),每个对称部分都要写一遍几乎一样的代码,维护成本高。这就是 xacro 要解决的问题。

开发:使用 xacro 简化模型

xacro 是什么

xacro(XML Macro)是 URDF 的扩展工具,在 XML 基础上增加了三个核心能力:

  1. 变量(Properties):定义可复用的数值。
  2. 宏(Macros):定义可复用的结构模板。
  3. 数学表达式:在属性值中做计算。

xacro 文件通过 xacro 命令行工具展开为纯 URDF:

xacro robot.urdf.xacro > robot.urdf

或者在 launch 文件中直接展开(推荐):

from launch import LaunchDescription
from launch_ros.actions import Node
import xacro
from ament_index_python.packages import get_package_share_directory
import os

def generate_launch_description():
    pkg_path = get_package_share_directory('my_robot_description')
    xacro_file = os.path.join(pkg_path, 'urdf', 'robot.urdf.xacro')
    robot_description = {
        'robot_description': xacro.process_file(xacro_file).toxml()
    }

    return LaunchDescription([
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            parameters=[robot_description]
        ),
    ])

xacro 变量

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="diffbot">

  <xacro:property name="chassis_length" value="0.4"/>
  <xacro:property name="chassis_width" value="0.3"/>
  <xacro:property name="chassis_height" value="0.1"/>
  <xacro:property name="wheel_radius" value="0.05"/>
  <xacro:property name="wheel_width" value="0.03"/>
  <xacro:property name="wheel_separation" value="0.3"/>

  <link name="base_link">
    <visual>
      <geometry>
        <box size="${chassis_length} ${chassis_width} ${chassis_height}"/>
      </geometry>
    </visual>
  </link>

</robot>

变量用 ${} 语法引用。可以在 <xacro:property> 中定义,也可以从外部传入。

xacro 宏

宏是 xacro 最强大的功能,可以把重复的结构抽象为模板:

<!-- 定义轮子宏 -->
<xacro:macro name="wheel" params="prefix y_offset">
  <link name="${prefix}_wheel">
    <visual>
      <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
      </geometry>
      <material name="black">
        <color rgba="0.1 0.1 0.1 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.5"/>
      <inertia ixx="0.001" ixy="0" ixz="0"
               iyy="0.001" iyz="0" izz="0.001"/>
    </inertial>
  </link>

  <joint name="${prefix}_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="${prefix}_wheel"/>
    <origin xyz="0 ${y_offset} -0.05"
            rpy="${-pi/2 if y_offset > 0 else pi/2} 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>
</xacro:macro>

<!-- 调用宏生成左右轮 -->
<xacro:wheel prefix="left" y_offset="${wheel_separation/2}"/>
<xacro:wheel prefix="right" y_offset="${-wheel_separation/2}"/>

params 中定义宏的参数,调用时通过属性传入。宏内部可以使用变量和数学表达式。

xacro 条件与循环

<!-- 条件:是否安装相机 -->
<xacro:arg name="use_camera" default="true"/>

<xacro:if value="${use_camera}">
  <link name="camera_link">
    <!-- ... -->
  </link>
  <joint name="camera_joint" type="fixed">
    <parent link="base_link"/>
    <child link="camera_link"/>
    <origin xyz="0.2 0 0.05" rpy="0 0 0"/>
  </joint>
</xacro:if>

<!-- 也支持 unless -->
<xacro:unless value="${use_camera}">
  <!-- 不安装相机时的替代结构 -->
</xacro:unless>

xacro include

多文件组织是大型机器人模型的标准做法:

my_robot_description/
├── urdf/
│   ├── robot.urdf.xacro          ← 主文件
│   ├── chassis.urdf.xacro        ← 底盘
│   ├── wheels.urdf.xacro         ← 轮子宏
│   ├── sensors.urdf.xacro        ← 传感器
│   └── inertial_macros.xacro     ← 惯性计算宏
├── meshes/
│   ├── chassis.dae
│   └── wheel.stl
└── config/
    └── joint_names.yaml

主文件通过 <xacro:include> 引入子文件:

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="my_robot">

  <xacro:include filename="$(find my_robot_description)/urdf/inertial_macros.xacro"/>
  <xacro:include filename="$(find my_robot_description)/urdf/chassis.urdf.xacro"/>
  <xacro:include filename="$(find my_robot_description)/urdf/wheels.urdf.xacro"/>
  <xacro:include filename="$(find my_robot_description)/urdf/sensors.urdf.xacro"/>

</robot>

$(find package_name) 是 ROS 的标准路径查找语法。在 Humble 中,$(find ...) 在 xacro 处理时被解析为对应功能包的 share 目录路径。

惯性计算的实用宏

惯性矩阵的计算容易出错(特别是非均匀形状)。实践中常用一组宏来自动生成标准几何体的惯性:

<!-- inertial_macros.xacro -->
<xacro:macro name="box_inertia" params="m x y z">
  <inertial>
    <mass value="${m}"/>
    <inertia
      ixx="${m*(y*y+z*z)/12}" ixy="0" ixz="0"
      iyy="${m*(x*x+z*z)/12}" iyz="0"
      izz="${m*(x*x+y*y)/12}"/>
  </inertial>
</xacro:macro>

<xacro:macro name="cylinder_inertia" params="m r h">
  <inertial>
    <mass value="${m}"/>
    <inertia
      ixx="${m*(3*r*r+h*h)/12}" ixy="0" ixz="0"
      iyy="${m*(3*r*r+h*h)/12}" iyz="0"
      izz="${m*r*r/2}"/>
  </inertial>
</xacro:macro>

<xacro:macro name="sphere_inertia" params="m r">
  <inertial>
    <mass value="${m}"/>
    <inertia
      ixx="${2*m*r*r/5}" ixy="0" ixz="0"
      iyy="${2*m*r*r/5}" iyz="0"
      izz="${2*m*r*r/5}"/>
  </inertial>
</xacro:macro>

使用时直接调用:

<link name="base_link">
  <visual>...</visual>
  <collision>...</collision>
  <xacro:box_inertia m="5.0" x="0.4" y="0.3" z="0.1"/>
</link>

开发:Gazebo 仿真集成

URDF 到 Gazebo 的桥接

Gazebo 使用 SDF(Simulation Description Format)作为内部格式。ROS 2 中通过 gazebo_ros 插件将 URDF 转换为 SDF 并加载。需要在 URDF 中添加 Gazebo 专用标签:

<!-- 在 robot 标签内添加 Gazebo 插件 -->
<gazebo>
  <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
    <parameters>$(find my_robot_bringup)/config/controllers.yaml</parameters>
  </plugin>
</gazebo>

Gazebo 材质和传感器

URDF 的 <material> 只管 RViz 可视化颜色。Gazebo 的材质需要单独设置:

<!-- Gazebo 专用材质 -->
<gazebo reference="base_link">
  <material>Gazebo/Blue</material>
</gazebo>

<!-- Gazebo 传感器(如相机) -->
<gazebo reference="camera_link">
  <sensor type="camera" name="camera">
    <update_rate>30.0</update_rate>
    <camera>
      <horizontal_fov>1.3962634</horizontal_fov>
      <image>
        <width>640</width>
        <height>480</height>
        <format>R8G8B8</format>
      </image>
    </camera>
    <plugin filename="libgazebo_ros_camera.so" name="camera_controller">
      <ros>
        <namespace>/camera</namespace>
      </ros>
    </plugin>
  </sensor>
</gazebo>

ros2_control 集成

ros2_control 通过 URDF 中的 <ros2_control> 标签定义硬件接口:

<ros2_control name="my_robot_hw" type="system">
  <hardware>
    <plugin>gazebo_ros2_control/GazeboSystem</plugin>
  </hardware>
  <joint name="left_wheel_joint">
    <command_interface name="velocity">
      <param name="min">-10</param>
      <param name="max">10</param>
    </command_interface>
    <state_interface name="position"/>
    <state_interface name="velocity"/>
  </joint>
  <joint name="right_wheel_joint">
    <command_interface name="velocity">
      <param name="min">-10</param>
      <param name="max">10</param>
    </command_interface>
    <state_interface name="position"/>
    <state_interface name="velocity"/>
  </joint>
</ros2_control>

这些标签告诉 gazebo_ros2_control 插件:哪些关节需要控制、控制方式(速度/位置/力矩)、以及提供哪些状态反馈。

开发:robot_state_publisher 配置

在 Launch 文件中加载 URDF

import os
import xacro
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    pkg_description = get_package_share_directory('my_robot_description')
    xacro_file = os.path.join(pkg_description, 'urdf', 'robot.urdf.xacro')

    # 展开 xacro 为 URDF XML
    robot_description = {
        'robot_description': xacro.process_file(xacro_file).toxml()
    }

    robot_state_publisher = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        parameters=[
            robot_description,
            {'publish_frequency': 50.0},  # TF 发布频率
        ]
    )

    # joint_state_publisher(用于手动调试,弹出 GUI 滑块)
    joint_state_publisher = Node(
        package='joint_state_publisher',
        executable='joint_state_publisher',
    )

    # joint_state_publisher_gui(可选,提供图形界面)
    joint_state_publisher_gui = Node(
        package='joint_state_publisher_gui',
        executable='joint_state_publisher_gui',
    )

    return LaunchDescription([
        robot_state_publisher,
        joint_state_publisher_gui,
    ])

向 xacro 传参

在 launch 文件中可以给 xacro 传外部参数(比如不同配置):

robot_description = {
    'robot_description': xacro.process_file(
        xacro_file,
        mappings={'use_camera': 'true', 'wheel_type': 'mecanum'}
    ).toxml()
}

xacro 文件中接收:

<xacro:arg name="use_camera" default="false"/>
<xacro:arg name="wheel_type" default="standard"/>

<xacro:if value="${use_camera}">
  <!-- 相机相关结构 -->
</xacro:if>

joint_state_publisher 的作用

joint_state_publisher 是一个调试工具节点,它会:

  1. 读取 URDF 中所有非 fixed 关节的默认位置(通常是 0)。
  2. /joint_states 话题上发布这些关节状态。

配合 robot_state_publisher,机器人模型就能在 RViz 中正确显示——即使没有真实的硬件驱动。joint_state_publisher_gui 还提供一个 GUI 窗口,用滑块手动调整每个关节角度,方便验证 URDF 的运动学是否正确。

调试工具

check_urdf

ROS 2 提供了 URDF 语法检查工具:

# 检查纯 URDF
ros2 run urdfdom check_urdf robot.urdf

# 先展开 xacro 再检查
xacro robot.urdf.xacro | check_urdf -

输出类似:

robot name is: diffbot
---------- Successfully Parsed XML ---------------
root Link: base_link has 3 child(ren)
    child(1):  left_wheel
    child(2):  right_wheel
    child(3):  laser_link

如果有语法错误或结构问题,会直接报错并指出位置。

xacro 展开调试

# 展开 xacro 并查看结果
xacro robot.urdf.xacro

# 展开并保存
xacro robot.urdf.xacro > /tmp/robot.urdf

# 展开时传参
xacro robot.urdf.xacro use_camera:=true

如果 xacro 展开报错,错误信息通常会指出文件名和行号。常见错误:

  • ${} 中的变量未定义
  • 宏参数名拼写错误
  • include 路径找不到文件

RViz2 可视化验证

在 RViz2 中验证 URDF 的步骤:

  1. 启动 robot_state_publisher + joint_state_publisher_gui
  2. 打开 RViz2,添加 RobotModel 显示。
  3. RobotModelDescription SourceTopicDescription Topic/robot_description
  4. 或者设 Description SourceFile,直接指定 URDF/xacro 文件路径。

检查要点:

  • 所有 link 是否在正确位置显示?
  • 拖动 joint_state_publisher_gui 的滑块,各关节运动方向和范围是否正确?
  • 碰撞包络是否合理(可以在 RViz 中开启 Collision Enabled 选项)?

通过 topic 验证 TF 发布

# 检查 /robot_description 是否发布
ros2 topic echo /robot_description --once

# 检查 /joint_states 是否有数据
ros2 topic hz /joint_states

# 检查 TF 树是否完整
ros2 run tf2_tools view_frames

# 查看特定 link 的变换
ros2 run tf2_ros tf2_echo base_link laser_link

用 gz sdf 验证 Gazebo 兼容性

# 将 URDF 转为 SDF 并检查
gz sdf -p robot.urdf

如果 URDF 中有 Gazebo 不支持的标签或缺失必要属性(如 inertia),会给出警告。

常见异常与调试

“Could not find the ‘robot’ element in the xml file”

原因:URDF 文件的根标签不是 <robot>,或者 XML 格式有误(比如文件头有 BOM 字符、编码不是 UTF-8)。

排查

head -5 robot.urdf
# 确认第一行是 <?xml version="1.0"?> 且第二行是 <robot name="xxx">

robot_state_publisher 不发布某些 link 的 TF

原因:通常是以下几种情况:

  1. URDF 没有正确加载。检查 /robot_description 话题是否有数据:

    ros2 topic echo /robot_description --once
    
  2. joint_states 没有包含某些活动关节。如果某个 revolute/continuous joint 在 /joint_states 中没有数据,robot_state_publisher 无法计算该 joint 对应的变换,该 link 的 TF 就不会发布。

    ros2 topic echo /joint_states
    # 检查 name 字段是否包含所有活动关节
    
  3. URDF 中有循环结构。URDF 必须是严格的树,如果有环(比如闭环连杆机构),解析器会拒绝加载。

Gazebo 中模型"塌了"或关节乱动

原因:惯量参数不合理。

Gazebo 的物理引擎对惯量矩阵非常敏感。常见问题:

  1. 惯量太小或太大。一个 5kg 的连杆,惯量设成了 0.0001,Gazebo 会认为它几乎没有质量,物理仿真不稳定。
  2. 惯量矩阵不满足正定性。惯量矩阵必须是对称正定矩阵,且满足三角不等式(ixx + iyy ≥ izz 等)。
  3. 质量为零。某个 link 没有 <inertial> 标签或 mass=0,Gazebo 会给它一个默认极小质量,导致仿真异常。

排查

# 用 Gazebo 的模型检查
gz sdf -p robot.urdf
# 查看输出的 warnings

解决方案:使用前面的惯性计算宏,基于标准几何体公式生成合理的惯量。

“No inertial data for link”

原因:Gazebo 要求所有参与仿真的 link 都有 <inertial> 标签。纯可视化用的 link(比如装饰件)可以没有,但如果有 joint 连接,Gazebo 会要求惯性数据。

解决方案:给每个 link 都加上 <inertial>,即使质量很小(如 0.001kg)。

xacro 展开报错:“Property wasn’t found”

原因:引用了未定义的变量。通常是:

  1. 变量名拼写错误。
  2. 变量定义在另一个文件中,但该文件没有被 include。
  3. include 路径错误导致文件没有被正确加载。

排查

# 展开时显示详细信息
xacro --verbosity=4 robot.urdf.xacro

joint 运动方向反了

原因<axis> 的方向或 <origin> 中的 rpy 设置不正确。

排查

  1. joint_state_publisher_gui 打开滑块,拖动某个关节。
  2. 在 RViz 中观察运动方向是否符合预期。
  3. 如果方向反了,翻转 <axis> 的方向(比如 xyz="0 0 1" 改为 xyz="0 0 -1"),或者调整 <origin>rpy

Mesh 文件加载失败

原因:路径不正确或格式不支持。

<!-- 正确的 package:// 路径 -->
<mesh filename="package://my_robot_description/meshes/base.dae"/>

<!-- 错误:缺少 package:// 前缀 -->
<mesh filename="meshes/base.dae"/>

<!-- 错误:用了绝对路径(换台电脑就失效) -->
<mesh filename="/home/user/ws/src/my_robot_description/meshes/base.dae"/>

排查

# 确认文件存在
ros2 pkg prefix my_robot_description
ls $(ros2 pkg prefix my_robot_description)/share/my_robot_description/meshes/

多机器人 URDF 冲突

原因:两个机器人有同名的 link 或 joint,TF 树会冲突。

解决方案:给每个机器人加 TF 前缀,在 xacro 中通过参数实现:

<xacro:macro name="robot" params="prefix">
  <link name="${prefix}base_link">...</link>
  <joint name="${prefix}wheel_joint" type="continuous">
    <parent link="${prefix}base_link"/>
    <child link="${prefix}wheel"/>
  </joint>
</xacro:macro>

<xacro:robot prefix="robot1_"/>
<xacro:robot prefix="robot2_"/>

或者在 launch 文件中给 robot_state_publishertf_prefix 参数(Humble 中通过 namespace 实现)。

踩坑记录

第一个坑:URDF 中 <inertia> 标签的 ixy、ixz、iyz 写成非零值导致 Gazebo 崩溃。 对于对称的均匀几何体(盒子、圆柱、球),交叉惯性积(ixy、ixz、iyz)应该为 0。如果不确定,全部设 0 通常也能让仿真跑起来(精度略差但不会崩溃)。

第二个坑:fixed joint 没有 <axis> 标签但加了 <limit> 标签。 fixed joint 不需要 axis 也不需要 limit。加了不会报错但容易产生混淆。fixed 的含义就是完全固定,axis 和 limit 都会被忽略。

第三个坑:robot_state_publisher 和手动 StaticTransformBroadcaster 冲突。 URDF 中定义了 base_link → laser_link 的 fixed joint,robot_state_publisher 会自动发布这个静态变换。如果同时又手动用 StaticTransformBroadcaster 发布同一个变换,TF 会报"multiple parents"错误。二选一,不要重复发布。

第四个坑:xacro 中的 $(find ...) 在非 colcon 环境下不工作。 $(find ...) 依赖 ROS 的包索引。如果没有先 source install/setup.bash,xacro 找不到包路径。确保在展开 xacro 之前,已经 source 了工作空间。

第五个坑:collision 形状太大导致导航时"幽灵障碍"。 collision 包络比实际外形大了一圈,MoveIt 或 Nav2 在做碰撞检测时认为机器人会撞到一个实际上不存在的障碍。调试时在 RViz 中开启 collision 显示,对比 visual 和 collision 的大小。

第六个坑:URDF 中的 <origin rpy> 顺序是 roll-pitch-yaw 但单位是弧度。 很多人习惯用角度来写,结果旋转量完全不对。记住 URDF 中所有角度单位都是弧度。π/2 ≈ 1.5708π ≈ 3.14159

第七个坑:Gazebo 中 continuous joint 没有配置 <axis> 虽然 URDF 规范中 axis 默认是 (1, 0, 0),但 Gazebo 对缺失 axis 的处理有时不一致。建议所有非 fixed joint 都显式指定 <axis>

第八个坑:joint_state_publisher 和真实硬件驱动同时发布 /joint_states 如果硬件驱动已经在发布关节状态,再跑一个 joint_state_publisher 会导致消息冲突(两个节点同时发布同一个话题,数据互相覆盖)。真实机器人上只跑硬件驱动,不跑 joint_state_publisher。后者仅用于无硬件时的调试。

第九个坑:Mesh 文件的坐标系和 URDF 不一致。 有些 CAD 导出的 mesh 文件自身带了一个旋转(比如 Z 轴朝上但 URDF 期望 Y 轴朝上)。需要在 <visual><origin> 中额外加一个旋转来补偿,或者在 CAD 软件中导出时调整坐标系。

总结与延伸

URDF 是 ROS 2 机器人软件栈的基础数据源,几乎所有的空间相关功能都依赖它。掌握 URDF 的开发和调试,关键在于把握三条线:

  1. 结构线:URDF 是一棵严格的 link-joint 树,每个 link 最多一个父 joint,不能有环。xacro 提供了变量、宏和条件来管理复杂度。
  2. 发布线robot_state_publisher 是 URDF 与 TF 系统的桥梁,fixed joint 走 /tf_static,活动 joint 根据 /joint_states/tf
  3. 仿真线:Gazebo 需要 <inertial>、Gazebo 专用材质和插件标签,ros2_control 需要在 URDF 中声明硬件接口。

调试 URDF 问题的标准流程:先用 check_urdf 验证语法,再用 xacro 展开检查内容,然后用 joint_state_publisher_gui + RViz 验证运动学,最后用 view_frames 确认 TF 树完整性。

掌握 URDF 后,可以继续深入:MoveIt 中的 SRDF(Semantic Robot Description Format)配置、Gazebo 中的 ros2_control 插件开发、从 CAD 软件(SolidWorks/Fusion360)导出 URDF 的工作流、以及 SDF 与 URDF 的差异与互转。

参考资料

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐