(附赠开源源码链接)手把手带你走一遍URDF 机器人建模:连杆、关节与可视化

《URDF 机器人建模:连杆、关节与可视化》

当机器人拥有多个零部件时,只靠一张外观图无法让 ROS 2 理解“哪个部件连接在哪里、能够怎样运动、每个坐标系之间是什么关系”。URDF 用 XML 把这些结构信息写成机器可读取的模型:刚体由 link 表示,连接关系由 joint 表示,robot_state_publisher 再根据关节状态生成 TF,RViz2 负责把模型和坐标系显示出来。

本文以 ROS 2 Humble 为基准,从一个最小连杆开始,逐步完成固定三连杆模型和可转动机械臂模型,并将它们整理成可构建的 urdf_demo 功能包。完成后,你不仅能够看懂 visual、collision、inertial、origin、axis 和 limit,还能够使用 check_urdf、TF 与 RViz2 建立一套可重复的建模验证流程。

1. 版本、环境与最终成果

1.1 推荐环境

本文采用以下环境作为可复现基线:

项目 推荐值 说明
操作系统 Ubuntu 22.04 ROS 2 Humble 的主要支持平台
ROS 2 Humble Hawksbill 建议安装 ros-humble-desktop
构建工具 colcon 使用 ament_python 功能包
可视化工具 RViz2 显示 RobotModel 与 TF

Jetson Orin Nano 使用 Ubuntu 22.04 与 ROS 2 Humble 时,可以直接沿用本文命令。其他 ROS 2 发行版的 URDF 核心语法基本相同,但包名、RViz2 属性或启动方式可能有小幅差异;若实际环境不是 Humble,应以对应发行版的官方文档为准。

先确认基础环境:

source /opt/ros/humble/setup.bash
printenv ROS_DISTRO
ros2 --help
colcon --help

printenv ROS_DISTRO 应输出 humble。如果 ros2 或 colcon 找不到,先完成 ROS 2 Desktop 与开发工具安装,不要在环境尚未正确加载时继续排查 URDF。

本文使用以下依赖,joint-state-publisher-gui依赖会在10.5小节用到,务必检查并安装:

sudo apt update
sudo apt install -y \
  liburdfdom-tools \
  ros-humble-joint-state-publisher-gui \
  ros-humble-robot-state-publisher \
  ros-humble-rviz2

其中,liburdfdom-tools 提供 check_urdf,其余包分别负责 TF、测试关节状态和模型显示。固定关节模型不需要关节状态 GUI。

1.2 本文最终文件

最终功能包包含三个模型:

文件 用途 是否包含可动关节
urdf/single_link.urdf 理解最小 URDF 结构 否
urdf/simple_demo.urdf 固定三连杆完整示例 否
urdf/articulated_demo.urdf 验证 revolute、axis 和 limit 是

同时提供 rviz_view.launch.py、joint_view.launch.py 和 rviz/urdf.rviz。文章最后会给出标准构建、运行、验证流程,配套目录中的 example/urdf_demo 可直接放入 ROS 2 工作空间的 src 目录。

2. URDF 在 ROS 2 中解决什么问题

2.1 URDF 是结构描述,不是控制程序

URDF 是 Unified Robot Description Format,即统一机器人描述格式。它是一种 XML 文档,用来声明机器人的连杆、关节、几何外观、碰撞形状、质量和惯性等静态信息。URDF 能回答以下问题:

  • 机器人有哪些刚体部件?

  • 哪个连杆是父连杆,哪个是子连杆?

  • 两个连杆在零位时如何安装?

  • 关节能绕哪根轴旋转,或沿哪根轴移动?

  • RViz2 应显示什么形状和颜色?

  • 仿真器进行碰撞与动力学计算时应使用哪些参数?

URDF 不能替代驱动节点、控制器、传感器驱动或运动规划器。电机是否真的转动、编码器如何读取、速度闭环如何计算,都需要其他 ROS 2 节点完成。更准确地说,URDF 是机器人“结构与坐标关系的数字说明书”。

完整链路可以概括为:URDF 文本被写入 robot_description 参数和同名话题,robot_state_publisher 读取模型拓扑,并结合 /joint_states 中的关节位置计算 TF;RViz2 的 RobotModel 显示项读取机器人描述,同时根据 TF 将每个连杆放到正确位置。固定关节的变换不会变化,通常发布到 /tf_static;可动关节的变换会随关节状态发布到 /tf。

2.2 实体机器人与模型边界

实体机器人通常包含机械结构、驱动系统、控制器、计算平台和传感器。URDF 主要描述机械结构,以及传感器相对机身的安装位置。例如,相机节点负责发布图像,URDF 则描述 camera_link 位于 base_link 前方多少米、朝向如何;雷达驱动负责发布扫描数据,URDF 则描述 laser_link 的安装高度和姿态。

若 RViz2 中相机点云整体偏移,问题可能是相机固定关节的 origin;若外形正确但不能运动,应检查 /joint_states 和关节名称,而不是 visual。

2.3 URDF 的树形约束

一个标准 URDF 模型是一棵树,而不是任意图结构:

  • 必须有且只有一个根连杆。

  • 除根连杆外,每个连杆只能有一个父关节。

  • 每个关节连接一个父连杆和一个子连杆。

  • link 名称和 joint 名称在各自范围内必须唯一。

  • 普通 URDF 不能直接描述闭环机构。

如果同一个子连杆被两个关节指向,或关节首尾形成环,check_urdf 通常会报告无法构建树。闭环机构需要专门处理,不能把物理闭环直接写进普通 URDF。

3. 连杆 link:描述一个刚体

3.1 link 的三个核心维度

link 表示一个理想刚体。最常见的内部元素是 visual、collision 和 inertial:

元素 主要消费者 作用 是否影响 RViz2 外观
visual RViz2、Gazebo 渲染器 定义可见几何和材质 是
collision 碰撞检测、物理仿真 定义碰撞几何 默认不显示
inertial 动力学与物理仿真 定义质量、质心和惯性 否

对只在 RViz2 中显示的最小模型,可以先只写 visual。但是进入 Gazebo、MoveIt 碰撞检测或动力学仿真后,collision 和 inertial 不能长期缺省。视觉模型追求真实,碰撞模型追求计算稳定和高效,二者没有必要完全相同。

3.2 visual、geometry 与 material

下面是一个完整的可视元素:

<visual>
  <origin xyz="0 0 0.1" rpy="0 0 0"/>
  <geometry>
    <box size="0.4 0.3 0.2"/>
  </geometry>
  <material name="blue">
    <color rgba="0.1 0.3 0.8 1.0"/>
  </material>
</visual>

geometry 支持四类常用形状:

几何体 关键属性 单位或含义
box size="x y z" 三个方向的边长,单位为米
cylinder radius、length 半径与沿自身 Z 轴的长度
sphere radius 球半径,单位为米
mesh filename、scale 网格 URI 与三轴缩放

rgba 的四个数字依次是红、绿、蓝、透明度,范围通常为 0 到 1。透明度为 1 表示完全不透明。重复使用的材质宜声明在 <robot> 下,再通过 <material name="blue"/> 引用,避免每个连杆重复写颜色。

3.3 为什么 visual 和 collision 应分开

真实机器人外壳可能包含倒角、孔位、线缆和曲面。如果把高面数网格同时用于碰撞检测,仿真和规划速度会明显下降。常见工程做法是:

  • visual 使用精细网格或组合几何,保证外观接近实物。

  • collision 使用盒体、圆柱、球体或低面数凸包,保证计算稳定。

  • 两者分别设置自己的 origin,但都相对同一个连杆坐标系定义。

上图中的 frame 是连杆坐标系,visual geometry 是渲染形状,collision geometry 是碰撞包络。即使两个几何体外形不同,只要相对坐标正确,它们仍属于同一个 link。排查碰撞错位时,应在 RViz2 中启用 RobotModel 的 Collision 显示,不能只看视觉外观。

3.4 inertial 的含义

典型惯性配置如下:

<inertial>
  <origin xyz="0 0 0" rpy="0 0 0"/>
  <mass value="1.0"/>
  <inertia
    ixx="0.01" ixy="0" ixz="0"
    iyy="0.01" iyz="0" izz="0.01"/>
</inertial>

origin 表示惯性坐标系位姿,平移通常对应质心;mass 单位为千克;inertia 是对称惯性矩阵的六个独立元素。质量必须大于零,矩阵应物理可实现。本文固定模型的数值用于结构示范,真实仿真时应重新计算。

3.5 连杆坐标系比外形中心更重要

link 本身没有尺寸,坐标系才是它真正的参考。几何体中心不必与连杆原点重合。例如机械臂连杆常把原点放在关节轴心,再把一根长度为 0.6 m 的盒体沿 X 轴平移 0.3 m。这样盒体从关节处向外延伸,而下一级关节可以直接放在 xyz="0.6 0 0"。

建模时应先确定坐标系,再放几何体,否则容易出现轴心偏移和 TF 难以解释的问题。

4. 关节 joint:连接父连杆与子连杆

4.1 一个关节包含什么

joint 不表示实体零件,而表示两个连杆之间的运动学约束。下面是一个带限位的旋转关节:

<joint name="shoulder_joint" type="revolute">
  <parent link="base_link"/>
  <child link="arm_link"/>
  <origin xyz="0 0 0.2" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
  <limit lower="-1.5708" upper="1.5708"
         effort="10" velocity="1.0"/>
</joint>

各字段的职责如下:

字段 含义 常见错误
parent 指定父连杆 名称与 link name 不一致
child 指定子连杆 子连杆被多个关节重复使用
origin 零位时子坐标系相对父坐标系的安装位姿 把几何偏移误写成关节偏移
axis 关节运动轴,表达在关节坐标系中 轴向与预期旋转方向不符
limit 位置、速度和力矩限制 revolute 或 prismatic 遗漏必需限制

关节值为零时,origin 决定父、子连杆坐标系的基础变换;关节值变化时,再绕 axis 旋转或沿 axis 平移。理解这一点后,才能区分“部件安装在哪里”和“部件围绕什么运动”。

4.2 常用关节类型

类型 自由度 是否需要 limit 典型用途
fixed 0 否 机身与传感器固定安装
revolute 单轴旋转 是 舵机、带机械限位的关节
continuous 单轴连续旋转 不设位置上下限 车轮、无限旋转轴
prismatic 单轴直线移动 是 升降台、直线模组
planar 平面运动 特殊 少量二维机构
floating 六自由度 特殊 少量自由体模型

fixed 只声明固定安装关系,不需要 axis 与 limit。continuous 需要 axis,但没有角度上下限。revolute 与 prismatic 通常必须给出 lower、upper、effort 和 velocity。位置限制的单位取决于运动类型:旋转关节使用弧度,直线关节使用米;velocity 对旋转关节为 rad/s,对直线关节为 m/s。

4.3 固定关节实例

固定三连杆模型中的右腿由以下关节连接:

<joint name="base_to_right_leg" type="fixed">
  <parent link="base_link"/>
  <child link="right_leg"/>
  <origin xyz="0 -0.22 0.25" rpy="0 0 0"/>
</joint>

这表示 right_leg 坐标系相对 base_link 沿 Y 轴移动 -0.22 m、沿 Z 轴移动 0.25 m,并保持相同朝向。它不表示右腿几何体的中心位置;右腿几何体还会在自身 visual/origin 中继续偏移和旋转。

4.4 固定模型与可动模型的数据差异

只有固定关节时,robot_state_publisher 读取 URDF 后就能确定所有连杆之间的变换,不需要额外关节状态。存在 revolute、continuous 或 prismatic 时,仅有 URDF 还不够,因为系统不知道当前角度或位移;此时必须由驱动、控制器、仿真器或 joint_state_publisher_gui 发布 sensor_msgs/msg/JointState。

JointState.name 中的名称必须与 URDF 的 joint name 完全一致。若 URDF 写的是 shoulder_joint,发布端却写成 shoulder,模型文件可以正常解析,但 TF 不会按预期更新。

5. origin、坐标轴与模型树

5.1 xyz 和 rpy

origin 的基本格式是:

<origin xyz="x y z" rpy="roll pitch yaw"/>

xyz 单位为米。rpy 依次表示绕 X、Y、Z 轴的滚转、俯仰和偏航角,单位为弧度。常用换算如下:

角度 弧度近似值
30° 0.5236
45° 0.7854
90° 1.5708
180° 3.14159

ROS 坐标约定通常遵循 REP-103:X 向前、Y 向左、Z 向上。URDF 语法不会强制你遵循该方向,但整个项目保持一致能显著降低传感器、导航和 TF 集成成本。

5.2 两层 origin 不要混淆

同一个子连杆常同时出现两类 origin:

  • joint/origin:定义子连杆坐标系相对父连杆坐标系的安装位置。

  • visual/origin、collision/origin、inertial/origin:定义各自属性相对当前连杆坐标系的位置。

以右腿为例,joint/origin 先把 right_leg 坐标系放到基座右侧和上方;visual/origin 再把长方体中心沿自身 Z 轴移动 -0.3 m,并绕 Y 轴旋转约 90°。最终外形位置是两级变换叠加的结果。若把这两个偏移全部塞进关节,坐标系会落在几何中心之外,后续传感器安装和子关节定位会变得难以维护。

5.3 三连杆模型的树结构

固定三连杆模型有一个根连杆和两个分支:

base_link
├── base_to_left_leg  -> left_leg
└── base_to_right_leg -> right_leg

左右腿关节的 X、Z 坐标相同,Y 坐标分别为 0.22 和 -0.22,由此形成镜像布局。树图是排查 URDF 的第一工具:每增加一个关节,都应能明确指出它的父连杆、子连杆、零位安装关系和运动轴。如果只能描述“它看起来在这里”,却说不清坐标树,模型通常还没有真正完成。

5.4 先定零位,再定运动范围

对于可动关节,建模顺序建议固定为:

  1. 确定父连杆坐标系。

  2. 用 joint/origin 放置关节轴心和关节坐标系。

  3. 用 axis 指定正方向。

  4. 在关节值为零时摆正子连杆。

  5. 最后填写 lower、upper、velocity 和 effort。

工程中应统一 CAD 装配零位、编码器零点与软件零位,并通过驱动偏置或标定处理硬件误差,不要反复修改 URDF 迁就当前读数。

6. 创建功能包与最小模型

6.1 获取完整示例

完整工程:GitHub 仓库(按住Ctrl同时鼠标单击链接即可跳转Git页面)任选一种方式获取。

下载 ZIP: 打开 ZIP 压缩包 后执行:

mkdir -p ~/ros2_ws/src
unzip ~/Downloads/URDF_demo-main.zip -d ~/ros2_ws/src

Git 克隆:

mkdir -p ~/ros2_ws/src
git -C ~/ros2_ws/src clone --depth 1 \
  https://github.com/2303886347/URDF_demo.git

两种目录中的 urdf_demo 均可被 colcon 发现,根据示例压缩包或git界面的readme文档操作编译功能包后,可直接跳到第 12 章。

6.2 从零创建功能包

在 ~/ros2_ws/src 创建功能包:

source /opt/ros/humble/setup.bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create urdf_demo \
  --build-type ament_python \
  --license Apache-2.0
​
mkdir -p urdf_demo/urdf
mkdir -p urdf_demo/launch
mkdir -p urdf_demo/rviz

本文使用 ament_python 和 setup.py 安装模型资源。URDF 不是可执行程序,无需执行 chmod +x。

完成后的核心目录如下:

ros2_ws/
└── src/
    └── urdf_demo/
        ├── launch/
        │   ├── joint_view.launch.py
        │   └── rviz_view.launch.py
        ├── resource/
        │   └── urdf_demo
        ├── rviz/
        │   └── urdf.rviz
        ├── urdf/
        │   ├── articulated_demo.urdf
        │   ├── simple_demo.urdf
        │   └── single_link.urdf
        ├── urdf_demo/
        │   └── __init__.py
        ├── package.xml
        ├── setup.cfg
        └── setup.py

6.3 最小可解析 URDF

第一个示例只解决一件事:确认 XML、robot、link、visual 和基础几何体的最小组合。文件路径为 ~/ros2_ws/src/urdf_demo/urdf/single_link.urdf,完整内容如下:

<?xml version="1.0"?>
<robot name="single_link_demo">
  <material name="blue">
    <color rgba="0.1 0.3 0.8 1.0"/>
  </material>
​
  <link name="base_link">
    <visual>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <geometry>
        <box size="0.4 0.3 0.2"/>
      </geometry>
      <material name="blue"/>
    </visual>
  </link>
</robot>

运行前先做结构验证:

check_urdf ~/ros2_ws/src/urdf_demo/urdf/single_link.urdf

预期能看到类似以下结果:

robot name is: single_link_demo
---------- Successfully Parsed XML ---------------
root Link: base_link has 0 child(ren)

这个模型足以在 RViz2 中显示,但没有 collision 和 inertial,因此它只适合作为显示与语法练习,不是完整物理仿真模型。最重要的注意事项是:check_urdf 成功只证明 XML 和树结构可解析,不证明尺寸、坐标方向、碰撞外形和惯性参数符合真实机器人。

6.4 XML 编写习惯

建议把每个元素的职责保持清晰:

  • 所有名称只使用 ASCII 字母、数字和下划线。

  • 数值显式写成米和弧度,不在属性中附加单位字符。

  • XML 属性使用英文半角引号,避免中文引号。

  • 每个开始标签都要有对应结束标签,自闭合标签以 /> 结束。

  • robot name、文件名和功能包名使用能表达含义的名称。

编辑器中的自动格式化和 XML 高亮很有帮助,但不要依赖编辑器替代 check_urdf。复制代码时尤其要检查 - 是否被替换成特殊短横线、引号是否变成排版引号。

7. 完成固定三连杆模型

7.1 示例目标与文件位置

这一主线示例将一个蓝色圆柱基座与两条白色长方体腿连接起来,用来完整演示材质、visual、collision、inertial 和 fixed 关节。文件路径为 ~/ros2_ws/src/urdf_demo/urdf/simple_demo.urdf。

7.2 simple_demo.urdf 完整代码

<?xml version="1.0"?>
<robot name="simple_demo">
  <material name="blue">
    <color rgba="0 0 0.8 1"/>
  </material>
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>
​
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
      <material name="blue"/>
    </visual>
    <collision>
      <geometry>
        <cylinder length="0.6" radius="0.2"/>
      </geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <mass value="1.0"/>
      <inertia
        ixx="0.01" ixy="0" ixz="0"
        iyy="0.01" iyz="0" izz="0.01"/>
    </inertial>
  </link>
​
  <link name="right_leg">
    <visual>
      <origin xyz="0 0 -0.3" rpy="0 1.5708 0"/>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
      <material name="white"/>
    </visual>
    <collision>
      <origin xyz="0 0 -0.3" rpy="0 1.5708 0"/>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 -0.3" rpy="0 0 0"/>
      <mass value="0.2"/>
      <inertia
        ixx="0.002" ixy="0" ixz="0"
        iyy="0.002" iyz="0" izz="0.002"/>
    </inertial>
  </link>
​
  <link name="left_leg">
    <visual>
      <origin xyz="0 0 -0.3" rpy="0 1.5708 0"/>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
      <material name="white"/>
    </visual>
    <collision>
      <origin xyz="0 0 -0.3" rpy="0 1.5708 0"/>
      <geometry>
        <box size="0.6 0.1 0.2"/>
      </geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 -0.3" rpy="0 0 0"/>
      <mass value="0.2"/>
      <inertia
        ixx="0.002" ixy="0" ixz="0"
        iyy="0.002" iyz="0" izz="0.002"/>
    </inertial>
  </link>
​
  <joint name="base_to_right_leg" type="fixed">
    <parent link="base_link"/>
    <child link="right_leg"/>
    <origin xyz="0 -0.22 0.25" rpy="0 0 0"/>
  </joint>
​
  <joint name="base_to_left_leg" type="fixed">
    <parent link="base_link"/>
    <child link="left_leg"/>
    <origin xyz="0 0.22 0.25" rpy="0 0 0"/>
  </joint>
</robot>

保存后执行:

check_urdf ~/ros2_ws/src/urdf_demo/urdf/simple_demo.urdf

预期根连杆为 base_link,并显示两个子链。check_urdf 输出中的树应与第 5 章的结构一致。正式构建与 RViz2 启动命令统一放在第 12 章,避免在每节重复。

8. 逐段理解三连杆模型

8.1 全局材质与基座

blue 和 white 定义在 <robot> 的直接子级,因此多个 visual 可以按名称复用。base_link 的圆柱没有额外 origin,其中心与 base_link 原点重合,圆柱自身 Z 轴方向的长度为 0.6 m,半径为 0.2 m。

如果希望 base_link 原点位于圆柱底面而不是中心,应把圆柱的 visual/origin 和 collision/origin 都改成 xyz="0 0 0.3",并同步考虑惯性坐标系。不要只移动 visual,否则外观看起来正确,碰撞体和质心仍停留在原位置。

8.2 腿部几何为什么既平移又旋转

box size="0.6 0.1 0.2" 默认在 X 方向最长。rpy="0 1.5708 0" 使盒体绕 Y 轴旋转约 90°,长轴因此转到接近 Z 方向;xyz="0 0 -0.3" 再把盒体中心下移 0.3 m。结果是腿部从自身连杆原点向下延伸。

这正体现了“连杆原点不等于外形中心”。关节连接到的是 right_leg 或 left_leg 坐标系,不是盒体中心。未来如果要在腿末端增加脚部关节,可以基于清晰的腿部坐标系继续定位,而不用从渲染网格中心反推位置。

8.3 左右对称只改变 Y 符号

右腿关节使用 xyz="0 -0.22 0.25",左腿使用 xyz="0 0.22 0.25"。在 X 向前、Y 向左、Z 向上的约定下,正 Y 位于机器人左侧,负 Y 位于机器人右侧。两侧使用相同的腿部几何和局部偏移,仅关节 Y 坐标符号相反,因此天然形成镜像。

如果模型左右颠倒,首先检查坐标约定和 Y 符号,不要急于通过修改相机视角掩盖问题。RViz2 视角可以旋转,但 TF 的左右关系不会因此改变。

8.4 可视化结果应观察什么

正确结果不是“窗口里出现了一个物体”这么简单,还应逐项确认:

  • 蓝色圆柱体和两条白色长方体均可见。

  • 两条腿相对基座左右对称,没有重叠到同一侧。

  • RViz2 的 Fixed Frame 为 base_link,Global Status 为 OK。

  • RobotModel 不报告 No transform 或 No link。

  • TF 显示中存在 base_link、left_leg、right_leg。

原图反映的是模型的三维显示效果,不应把它当作物理仿真结果。RViz2 不计算重力、摩擦、碰撞响应或电机动力学;即使惯性参数不合理,模型也可能在 RViz2 中正常显示。

8.5 本例惯性参数的边界

示例采用简化惯性值。进入 Gazebo 前,应按真实几何和质量计算惯量,确认质心、惯性坐标系与几何方向一致。建议先验证视觉和 TF,再加入碰撞体,最后依据 CAD 或测量数据填写质量和惯量。

9. 从 fixed 进阶到可动关节

9.1 示例解决的问题

固定模型能够验证连杆树,却无法直观看出 axis、limit 和 /joint_states 的作用。本节创建一个简化机械臂:圆柱基座通过 revolute 关节连接长方体机械臂,末端再通过 fixed 关节连接工具。joint_state_publisher_gui 提供肩关节滑块,拖动时 RViz2 中的机械臂应同步转动。

文件路径为 ~/ros2_ws/src/urdf_demo/urdf/articulated_demo.urdf。

9.2 articulated_demo.urdf 完整代码

<?xml version="1.0"?>
<robot name="articulated_demo">
  <material name="blue">
    <color rgba="0.1 0.3 0.8 1.0"/>
  </material>
  <material name="orange">
    <color rgba="0.95 0.45 0.05 1.0"/>
  </material>
  <material name="green">
    <color rgba="0.1 0.65 0.35 1.0"/>
  </material>
​
  <link name="base_link">
    <visual>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <geometry>
        <cylinder radius="0.18" length="0.2"/>
      </geometry>
      <material name="blue"/>
    </visual>
    <collision>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <geometry>
        <cylinder radius="0.18" length="0.2"/>
      </geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <mass value="2.0"/>
      <inertia
        ixx="0.02287" ixy="0" ixz="0"
        iyy="0.02287" iyz="0" izz="0.03240"/>
    </inertial>
  </link>
​
  <link name="arm_link">
    <visual>
      <origin xyz="0.3 0 0" rpy="0 0 0"/>
      <geometry>
        <box size="0.6 0.1 0.1"/>
      </geometry>
      <material name="orange"/>
    </visual>
    <collision>
      <origin xyz="0.3 0 0" rpy="0 0 0"/>
      <geometry>
        <box size="0.6 0.1 0.1"/>
      </geometry>
    </collision>
    <inertial>
      <origin xyz="0.3 0 0" rpy="0 0 0"/>
      <mass value="0.5"/>
      <inertia
        ixx="0.00083" ixy="0" ixz="0"
        iyy="0.01542" iyz="0" izz="0.01542"/>
    </inertial>
  </link>
​
  <joint name="shoulder_joint" type="revolute">
    <parent link="base_link"/>
    <child link="arm_link"/>
    <origin xyz="0 0 0.2" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-1.5708" upper="1.5708"
           effort="10" velocity="1.0"/>
  </joint>
​
  <link name="tool_link">
    <visual>
      <origin xyz="0.1 0 0" rpy="0 1.5708 0"/>
      <geometry>
        <cylinder radius="0.06" length="0.2"/>
      </geometry>
      <material name="green"/>
    </visual>
    <collision>
      <origin xyz="0.1 0 0" rpy="0 1.5708 0"/>
      <geometry>
        <cylinder radius="0.06" length="0.2"/>
      </geometry>
    </collision>
    <inertial>
      <origin xyz="0.1 0 0" rpy="0 1.5708 0"/>
      <mass value="0.2"/>
      <inertia
        ixx="0.00085" ixy="0" ixz="0"
        iyy="0.00085" iyz="0" izz="0.00036"/>
    </inertial>
  </link>
​
  <joint name="arm_to_tool" type="fixed">
    <parent link="arm_link"/>
    <child link="tool_link"/>
    <origin xyz="0.6 0 0" rpy="0 0 0"/>
  </joint>
</robot>

先验证结构:

check_urdf ~/ros2_ws/src/urdf_demo/urdf/articulated_demo.urdf

预期树结构为:

base_link
└── shoulder_joint -> arm_link
    └── arm_to_tool -> tool_link

9.3 肩关节的零位、轴与限位

shoulder_joint 位于基座上表面附近,即 xyz="0 0 0.2"。axis="0 1 0" 表示绕关节坐标系的 Y 轴运动。lower="-1.5708" 和 upper="1.5708" 将范围限制在约 -90° 到 90°。当 GUI 滑块从 0 向正方向移动时,机械臂按右手定则绕正 Y 轴旋转。

arm_link 的盒体中心位于自身 X 轴正方向 0.3 m,因此盒体近端恰好位于肩关节附近;arm_to_tool 再放在 X 轴 0.6 m 处,即机械臂末端。这个布局展示了一条可维护的坐标链:关节原点放轴心,连杆几何沿自身轴延伸,下一级关节放在连杆长度处。

9.4 验证时应观察什么

运行 joint_view.launch.py 后,joint_state_publisher_gui 应只显示一个名为 shoulder_joint 的滑块。拖动滑块时:

  • base_link 保持不动。

  • arm_link 围绕肩部 Y 轴旋转。

  • tool_link 因固定在 arm_link 末端而一起运动。

  • /joint_states 中的 name 为 shoulder_joint。

  • base_link 到 arm_link 的 TF 随角度变化。

最重要的注意事项是不要同时启动两个互相竞争的关节状态来源。真实驱动、控制器和 joint_state_publisher_gui 若同时发布同名关节,RViz2 可能出现跳变;GUI 只应作为没有真实硬件状态时的测试工具。

10. 让资源随功能包安装

10.1 为什么不能只把文件放进 src

直接运行源码路径下的 URDF 看似可行,但安装后的 Launch 文件通常通过 ROS 2 包索引查找 share/urdf_demo。如果 setup.py 没有安装 urdf、launch 和 rviz 目录,colcon build 可以成功,启动时却会报告文件不存在。

目标是让以下命令返回安装空间路径:

ros2 pkg prefix --share urdf_demo

预期结果类似 ~/ros2_ws/install/urdf_demo/share/urdf_demo,该目录下应能看到三个资源子目录。

10.2 完整 setup.py

文件路径为 ~/ros2_ws/src/urdf_demo/setup.py:

import os
from glob import glob
​
from setuptools import find_packages, setup
​
​
package_name = 'urdf_demo'
​
​
setup(
    name=package_name,
    version='0.1.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        (
            os.path.join(
                'share', 'ament_index', 'resource_index', 'packages'
            ),
            ['resource/' + package_name],
        ),
        (os.path.join('share', package_name), ['package.xml']),
        (
            os.path.join('share', package_name, 'launch'),
            glob(os.path.join('launch', '*.launch.py')),
        ),
        (
            os.path.join('share', package_name, 'rviz'),
            glob(os.path.join('rviz', '*.rviz')),
        ),
        (
            os.path.join('share', package_name, 'urdf'),
            glob(os.path.join('urdf', '*.*')),
        ),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='tutorial',
    maintainer_email='tutorial@example.com',
    description='URDF link, joint, TF, and RViz2 tutorial examples.',
    license='Apache-2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [],
    },
)

glob 在构建时收集匹配的文件,os.path.join 生成安装目标路径。新增 URDF 或 Launch 文件后必须重新构建,安装空间才会包含它。--symlink-install 对 Python 与资源开发很方便,但不能把“忘记构建”变成“永远自动安装新文件”。

10.3 完整 package.xml

文件路径为 ~/ros2_ws/src/urdf_demo/package.xml:

<?xml version="1.0"?>
<package format="3">
  <name>urdf_demo</name>
  <version>0.1.0</version>
  <description>URDF link, joint, TF, and RViz2 tutorial examples.</description>
​
  <maintainer email="tutorial@example.com">tutorial</maintainer>
  <license>Apache-2.0</license>
​
  <buildtool_depend>ament_python</buildtool_depend>
​
  <exec_depend>ament_index_python</exec_depend>
  <exec_depend>joint_state_publisher_gui</exec_depend>
  <exec_depend>launch</exec_depend>
  <exec_depend>launch_ros</exec_depend>
  <exec_depend>robot_state_publisher</exec_depend>
  <exec_depend>rviz2</exec_depend>
​
  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

依赖按运行期实际使用声明。功能包没有自定义 rclpy 节点,因此不需要为了“这是 ROS 2 包”而额外依赖 rclpy。如果以后添加 Python 节点,再把对应依赖加入 package.xml。

10.4 固定模型 Launch 文件

文件路径为 ~/ros2_ws/src/urdf_demo/launch/rviz_view.launch.py:

from pathlib import Path
​
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
​
​
def generate_launch_description():
    package_share = Path(get_package_share_directory('urdf_demo'))
    urdf_path = package_share / 'urdf' / 'simple_demo.urdf'
    rviz_path = package_share / 'rviz' / 'urdf.rviz'
    robot_description = urdf_path.read_text(encoding='utf-8')
​
    return LaunchDescription([
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            name='robot_state_publisher',
            parameters=[{'robot_description': robot_description}],
            output='screen',
        ),
        Node(
            package='rviz2',
            executable='rviz2',
            name='rviz2',
            arguments=['-d', str(rviz_path)],
            output='screen',
        ),
    ])

这里从安装后的包共享目录读取 URDF,而不是写死 ~/ros2_ws/src。因此功能包移动到另一台机器或另一个工作空间后仍能工作。固定模型不启动 joint_state_publisher_gui,因为两个关节的变换都可直接由 URDF 确定。

10.5 可动模型 Launch 文件

文件路径为 ~/ros2_ws/src/urdf_demo/launch/joint_view.launch.py:

from pathlib import Path
​
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
​
​
def generate_launch_description():
    package_share = Path(get_package_share_directory('urdf_demo'))
    urdf_path = package_share / 'urdf' / 'articulated_demo.urdf'
    rviz_path = package_share / 'rviz' / 'urdf.rviz'
    robot_description = urdf_path.read_text(encoding='utf-8')
​
    return LaunchDescription([
        Node(
            package='joint_state_publisher_gui',
            executable='joint_state_publisher_gui',
            name='joint_state_publisher_gui',
            parameters=[{'robot_description': robot_description}],
            output='screen',
        ),
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            name='robot_state_publisher',
            parameters=[{'robot_description': robot_description}],
            output='screen',
        ),
        Node(
            package='rviz2',
            executable='rviz2',
            name='rviz2',
            arguments=['-d', str(rviz_path)],
            output='screen',
        ),
    ])

两个状态相关节点读取同一份 robot_description,保证 GUI 展示的关节名称与状态发布器解析的模型一致。对于真实机器人,应将 GUI 替换成实际驱动或 ros2_control 控制器,不应让 Launch 文件长期依赖人工滑块。

11. 在 RViz2 中显示模型与 TF

11.1 RViz 配置要点

配套工程已经提供 rviz/urdf.rviz。其中最关键的设置是:Global Options 的 Fixed Frame 为 base_link;RobotModel 的 Description Source 为 Topic;Description Topic 为 /robot_description;Durability Policy 为 Transient Local;同时启用 TF 显示。

Transient Local 很重要。robot_state_publisher 会发布机器人描述,RViz2 若稍晚启动,需要以兼容的 QoS 获取已保存的描述;若使用不合适的 Volatile 配置,可能出现节点都正常但 RobotModel 偶发为空的竞态。

如果不使用配套配置,可手动执行以下操作:

  1. 启动 RViz2 后,将 Fixed Frame 改为 base_link。

  2. 点击 Add,添加 RobotModel。

  3. 将 Description Source 设为 Topic。

  4. 将 Description Topic 设为 /robot_description。

  5. 再添加 TF,观察连杆名称、坐标轴和树结构。

  6. 使用 File → Save Config As 保存到功能包的 rviz/urdf.rviz。

Fixed Frame 不应写成默认的 map,也不要写成 /base_link。如果全局状态提示 Fixed Frame 不存在,优先检查 robot_state_publisher 是否启动以及模型根连杆是否确实命名为 base_link。

11.2 RobotModel 与 TF 各自证明什么

RobotModel 正常说明视觉资源和 TF 可用;TF 树正确说明父子关系与状态链路基本成立。碰撞体和惯性仍需单独验收。

12. 标准构建、运行与验证流程

12.1 安装依赖并构建

在工作空间根目录执行:

source /opt/ros/humble/setup.bash
cd ~/ros2_ws
colcon build --symlink-install --packages-select urdf_demo
source install/setup.bash

构建后检查安装资源:

ros2 pkg prefix --share urdf_demo
find install/urdf_demo/share/urdf_demo -maxdepth 2 -type f | sort

结果中应包含 launch、rviz、urdf 和 package.xml。若源码中有文件而安装目录没有,检查 setup.py 的 data_files,然后重新构建并重新 source。

12.2 启动固定模型

ros2 launch urdf_demo rviz_view.launch.py

预期 RViz2 显示蓝色基座和两条白色腿,TF 树以 base_link 为根。另开终端并加载工作空间后验证:

source ~/ros2_ws/install/setup.bash
ros2 node list
ros2 topic info /robot_description --verbose
ros2 run tf2_ros tf2_echo base_link right_leg

tf2_echo 的平移应接近 0 -0.22 0.25,旋转应为单位旋转。由于模型全部使用固定关节,不出现 /joint_states 也不属于错误。

12.3 启动可动模型

先关闭上一套 Launch,再执行:

ros2 launch urdf_demo joint_view.launch.py

拖动 shoulder_joint 滑块,同时检查:

ros2 topic echo /joint_states --once
ros2 run tf2_ros tf2_echo base_link arm_link

/joint_states 应包含 shoulder_joint,TF 的旋转值应随滑块变化。如果 GUI 有滑块但模型不动,核对关节名称、robot_state_publisher 节点和 /joint_states 发布者。

推荐始终遵循“先 check_urdf,再构建,再启动,再查 TF,最后看 RViz2”的顺序。这样能把 XML 错误、安装错误、状态发布错误和显示配置错误分层定位,而不是在 RViz2 空白窗口里盲目尝试。

13. 网格、碰撞、惯性与复用

13.1 加载 mesh 资源

真实外观可使用包 URI:

<geometry>
  <mesh filename="package://urdf_demo/meshes/base.stl"
        scale="0.001 0.001 0.001"/>
</geometry>

package:// 比绝对路径可移植。scale="0.001 0.001 0.001" 常用于把毫米网格换算为米,但不能看到 STL 就机械套用;应确认 CAD 导出单位。新增 meshes 目录后,还要在 setup.py 中加入对应安装规则。

13.2 工程化原则

高面数视觉网格应配低复杂度碰撞体;惯性应来自 CAD、称重和公式,而不是照抄其他机器人;重复轮子、腿或传感器安装结构可在模型稳定后迁移到 Xacro 宏。Xacro 能减少重复,但不会修复错误坐标,因此先用普通 URDF 理清一条链,再做参数化复用。

14. 常用元素与命令速查

项目 作用 关键点
link 声明刚体 名称唯一
joint 连接父子连杆 模型保持树形
origin 声明相对位姿 米、弧度
axis 声明运动轴 位于关节坐标系
limit 声明运动约束 revolute、prismatic 必查
check_urdf 解析 XML 和树结构 不验证物理真实性
robot_state_publisher 由模型和关节状态发布 TF 固定关节发布静态变换
RobotModel 在 RViz2 显示 URDF 依赖描述与 TF

常用命令集中如下:

check_urdf path/to/model.urdf
colcon build --symlink-install --packages-select urdf_demo
ros2 launch urdf_demo rviz_view.launch.py
ros2 launch urdf_demo joint_view.launch.py
ros2 topic info /robot_description --verbose
ros2 topic echo /joint_states --once
ros2 run tf2_ros tf2_echo base_link right_leg

15. 常见问题

15.1 常见问题

check_urdf 报 XML 错误。 检查标签闭合、半角引号和属性拼写,并确认没有把 Markdown 围栏复制进 URDF 文件。

提示找不到子连杆或存在多个根。 检查 parent、child 是否与 link name 完全一致,是否有孤立连杆,或同一子连杆是否被多个关节连接。

RViz2 中 RobotModel 为空。 检查 Fixed Frame、/robot_description、RobotModel 的 Topic 与 QoS,再检查资源是否安装。不要只反复重启 RViz2。

模型出现但位置错误。 分开检查 joint/origin 和 visual/origin。前者放置连杆坐标系,后者放置几何体;注意米、弧度和 Y 轴正负号。

可动关节不动。 检查 /joint_states、关节名称、axis 和 limit。固定关节不会出现在 GUI 滑块中,这是正常行为。

源码修改后效果不变。 确认 Launch 读取的是安装空间文件,重新构建并执行 source ~/ros2_ws/install/setup.bash。

16. 参考资料

研究一下,补短板

跑起来了,妙哉,补短板