Soma Zero Tutorials
🔍 搜索功能尚未开启,敬请期待。

3.9 机械臂 URDF 模型与可视化

一、本章目标

在前面的章节中,我们学习了 ROS2 的核心通信机制(Topic、Service、Action)以及 TF2 坐标系管理。但还有一个重要问题没有解决:

机器人的 3D 模型从哪里来?RViz 是怎么知道机械臂长什么样的?

想象以下场景:

  • 你在 RViz 中看到一个漂亮的机械臂模型,可以实时显示各关节的位置
  • 拖动滑块,机械臂模型跟着动,各个坐标系(TF)也实时更新
  • 这一切是怎么实现的?

答案就是 URDF(Unified Robot Description Format) —— 统一机器人描述格式。

1.1 学习路线

1步:理解 URDF 是什么、解决什么问题 ↓ 第2步:通过 2-link 简单示例掌握 URDF 基本语法 ↓ 第3步:了解 CAD 导出 URDF 的主流方法(实际工程中常用) ↓ 第4步:分析 Episode 1 机械臂的完整 URDF 结构 ↓ 第5步:理解核心原理:URDF 如何变成 TF 坐标系并在 RViz 中显示

1.2 本章目标

完成本章后,你将能够:

  • 理解 URDF 文件的结构和核心元素(<link><joint>
  • 看懂 CAD 软件导出的 URDF 文件
  • 理解 robot_state_publisher 如何将 URDF 转换为 TF 坐标系
  • 理解 RViz 如何订阅话题来渲染机器人模型

1.3 与其他章节的关系

章节 内容 与本章的关系
第 4 章 Topic 发布/订阅消息 robot_state_publisher 订阅 /joint_states,发布 /tf
第 5 章 自定义 Interface 自定义消息类型 /joint_states 使用 sensor_msgs/JointState
第 8 章 TF2 手动发布坐标变换 本章的 TF 由 robot_state_publisher 自动发布
第 9 章 URDF(本章) 机器人模型描述 定义机器人结构,驱动 TF 自动生成

核心理念:URDF 是机器人的"身份证",定义了它的外观和关节结构。robot_state_publisher 读取 URDF 后,自动帮你完成第 8 章中手动做的 TF 广播工作。

二、URDF 概述

2.1 什么是 URDF?

URDF(Unified Robot Description Format) 是一种基于 XML 的机器人描述格式,用于定义:

内容 说明
外观 机器人的 3D 模型(几何形状、颜色、纹理)
结构 连杆(link)和关节(joint)的层级关系
物理属性 质量、惯性矩阵(用于动力学仿真)
运动约束 关节类型、运动范围、速度限制

一句话总结:URDF 告诉 ROS2 "机器人长什么样、怎么连接、怎么动"。

2.2 URDF 的核心元素

URDF 文件由两个核心元素组成:Link(连杆)Joint(关节)

相关课程:《大模型 3D/6D 视觉抓取 6 轴机械臂》 八、运动控制:DH 参数建模与正逆运动学

┌─────────────────────────────────────────────────────────────────┐ │ URDF 核心结构 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ <robot name="my_robot"> │ │ │ │ │ ├── <link name="base_link"> ← 连杆:机器人的"骨骼" │ │ │ ├── <visual> ← 外观(RViz显示用) │ │ │ ├── <collision> ← 碰撞体(仿真碰撞检测用) │ │ │ └── <inertial> ← 惯性(动力学仿真用) │ │ │ │ │ ├── <link name="link1"> │ │ │ │ │ └── <joint name="joint1"> ← 关节:连接两个连杆 │ │ ├── <parent link="base_link"/> │ │ ├── <child link="link1"/> │ │ ├── <origin xyz="..." rpy="..."/> ← 相对位置 │ │ ├── <axis xyz="0 0 1"/> ← 转轴方向 │ │ └── <limit lower="..." upper="..."/> │ │ </robot> │ │ │ └─────────────────────────────────────────────────────────────────┘

Link vs Joint 类比

概念 类比 说明
Link(连杆) 人体的骨骼 刚性部件,有形状、质量
Joint(关节) 人体的关节 连接两根骨骼,定义运动方式

2.3 关节类型

URDF 支持多种关节类型:

类型 说明 自由度 典型应用
revolute 旋转关节(有限位) 1 机械臂关节
continuous 旋转关节(无限位) 1 轮子
prismatic 滑动关节 1 升降台
fixed 固定连接 0 传感器安装
floating 6 自由度浮动 6 飞行器
planar 平面运动 2 2D 移动平台

机械臂最常用revolute(旋转关节)和 fixed(固定连接,如末端工具)。

2.4 URDF 的树状结构

URDF 描述的机器人必须是树状结构

  • 有且只有一个根节点(通常是 base_link
  • 每个 Link 只能有一个父 Joint
  • 不允许闭环(并联机构如 Delta 机器人需使用 SDF 格式或 Gazebo 约束插件)
URDF 树状结构示例(6轴机械臂): base_link (根节点) │ └── joint1 (revolute) │ └── link1 │ └── joint2 (revolute) │ └── link2 │ └── joint3 (revolute) │ └── link3 │ ... (继续到 link6)

在分析复杂的机械臂 URDF 之前,我们先用一个简单的 3-Link 示例来理解 URDF 的基本语法。

3.1 示例:一个简单的机械臂

我们要创建一个这样的机器人:

┌─────────────────────────────────────────────────────────────────┐ │ 3-Link 简单机械臂 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ╳ end_effector (坐标系) │ │ ↑ │ │ ┌───┴───┐ │ │ │link2 │ ← 小方块末端(可绕 Y 轴旋转) │ │ └───┬───┘ │ │ │ joint2 (revolute, Y轴旋转) │ │ ┌───┴───┐ │ │ │ │ │ │ │ link1 │ ← 长方体手臂(可绕 Z 轴旋转) │ │ │ (box) │ │ │ └───┬───┘ │ │ │ joint1 (revolute, Z轴旋转) │ │ ┌───┴────┐ │ │ │ │ │ │ │base_link│ ← 圆柱体底座(固定不动) │ │ │(cylinder)│ │ │ └────────┘ │ │ ═══════════════ 地面 │ │ │ └─────────────────────────────────────────────────────────────────┘

3.2 完整 URDF 代码

<?xml version="1.0"?> <robot name="simple_3link_robot"> <!-- ==================== 材质定义 ==================== --> <material name="blue"> <color rgba="0 0 0.8 1"/> </material> <material name="white"> <color rgba="1 1 1 1"/> </material> <!-- ==================== Link 1: 底座 ==================== --> <link name="base_link"> <!-- 可视化外观 --> <visual> <origin xyz="0 0 0.05" rpy="0 0 0"/> <!-- 圆柱中心上移0.05m --> <geometry> <cylinder length="0.1" radius="0.05"/> <!-- 高0.1m,半径0.05m --> </geometry> <material name="blue"/> </visual> <!-- 碰撞体(通常与visual相同或简化) --> <collision> <origin xyz="0 0 0.05" rpy="0 0 0"/> <geometry> <cylinder length="0.1" radius="0.05"/> </geometry> </collision> <!-- 惯性属性(仿真需要) --> <inertial> <origin xyz="0 0 0.05" rpy="0 0 0"/> <mass value="1.0"/> <inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/> </inertial> </link> <!-- ==================== Link 2: 手臂 ==================== --> <link name="link1"> <visual> <origin xyz="0 0 0.15" rpy="0 0 0"/> <!-- 长方体中心在关节上方0.15m --> <geometry> <box size="0.04 0.04 0.3"/> <!-- 4cm x 4cm x 30cm 的长方体 --> </geometry> <material name="white"/> </visual> <collision> <origin xyz="0 0 0.15" rpy="0 0 0"/> <geometry> <box size="0.04 0.04 0.3"/> </geometry> </collision> <inertial> <origin xyz="0 0 0.15" rpy="0 0 0"/> <mass value="0.5"/> <inertia ixx="0.004" ixy="0" ixz="0" iyy="0.004" iyz="0" izz="0.0001"/> </inertial> </link> <!-- ==================== Joint 1: 连接底座和手臂 ==================== --> <joint name="joint1" type="revolute"> <parent link="base_link"/> <!-- 父连杆 --> <child link="link1"/> <!-- 子连杆 --> <!-- 关节位置:在底座顶部(Z=0.1m) --> <origin xyz="0 0 0.1" rpy="0 0 0"/> <!-- 旋转轴:绕 Z 轴旋转 --> <axis xyz="0 0 1"/> <!-- 关节限位:-180° 到 +180° --> <limit lower="-3.14159" upper="3.14159" effort="10" velocity="1.0"/> </joint> <!-- ==================== Link 3: 末端 ==================== --> <link name="link2"> <visual> <origin xyz="0 0 0.1" rpy="0 0 0"/> <!-- 小方块中心在关节上方0.1m --> <geometry> <box size="0.06 0.06 0.2"/> <!-- 6cm x 6cm x 20cm 的方块 --> </geometry> <material name="blue"/> </visual> <collision> <origin xyz="0 0 0.1" rpy="0 0 0"/> <geometry> <box size="0.06 0.06 0.2"/> </geometry> </collision> <inertial> <origin xyz="0 0 0.1" rpy="0 0 0"/> <mass value="0.3"/> <inertia ixx="0.002" ixy="0" ixz="0" iyy="0.002" iyz="0" izz="0.001"/> </inertial> </link> <!-- ==================== Joint 2: 连接手臂和末端 ==================== --> <joint name="joint2" type="revolute"> <parent link="link1"/> <!-- 父连杆 --> <child link="link2"/> <!-- 子连杆 --> <!-- 关节位置:在 link1 顶部(Z=0.3m) --> <origin xyz="0 0 0.3" rpy="0 0 0"/> <!-- 旋转轴:绕 Y 轴旋转 --> <axis xyz="0 1 0"/> <!-- 关节限位:-180° 到 +180° --> <limit lower="-3.14159" upper="3.14159" effort="10" velocity="1.0"/> </joint> <!-- ==================== Link 4: 末端坐标系 ==================== --> <link name="end_effector"> <!-- 这是一个纯坐标系,不需要 visual,只用于 TF 显示 --> </link> <!-- ==================== Fixed Joint: 连接到末端坐标系 ==================== --> <joint name="joint_end_effector" type="fixed"> <parent link="link2"/> <!-- 父连杆 --> <child link="end_effector"/> <!-- 子连杆:末端坐标系 --> <!-- 关节位置:在 link2 顶部(Z=0.2m) --> <origin xyz="0 0 0.2" rpy="0 0 0"/> </joint> </robot>

先别急着理解每一行代码,我们先把它跑起来,在 RViz 中看到效果!

步骤 1:保存 URDF 文件

将上面的 URDF 代码保存为文件:

# 创建目录 mkdir -p ~/ros2_ws/src/urdf_test # 创建 URDF 文件(复制 3.2 节的完整代码) nano ~/ros2_ws/src/urdf_test/simple_3link.urdf

步骤 2:启动 robot_state_publisher

# 终端 1:启动 robot_state_publisher ros2 run robot_state_publisher robot_state_publisher ~/ros2_ws/src/urdf_test/simple_3link.urdf

步骤 3:启动 joint_state_publisher_gui

# 终端 2:启动关节滑块 GUI ros2 run joint_state_publisher_gui joint_state_publisher_gui ~/ros2_ws/src/urdf_test/simple_3link.urdf

步骤 4:启动 RViz2

# 终端 3:启动 RViz ros2 run rviz2 rviz2

步骤 5:配置 RViz 显示机器人

URDF

现在你应该能看到:

  • 一个蓝色圆柱体底座(base_link)
  • 一个白色长方体手臂(link1)
  • 一个蓝色小方块末端(link2)
  • 一个末端坐标系(end_effector)显示为 RGB 三轴
  • joint_state_publisher_gui 窗口中拖动 joint1joint2 滑块
  • 机械臂会绕 Z 轴旋转!

快速验证命令

# 查看话题 ros2 topic list # 输出 /clicked_point /goal_pose /initialpose /joint_states /parameter_events /robot_description /rosout /tf /tf_static # 查看 joint_states ros2 topic echo /joint_states # 输出 --- header: stamp: sec: 1769566439 nanosec: 767450374 frame_id: '' name: - joint1 - joint2 position: - -1.656874566 - -0.8633089320000003 velocity: [] effort: [] ---

提示:如果 RViz 中看不到模型,检查 Fixed Frame 是否设置为 base_link,以及 RobotModel 的 Description Topic 是否正确。

现在我们来逐个理解 URDF 中的每个元素。

3.4 逐元素解析

3.4.1 <robot> 根元素

<robot name="simple_3link_robot"> ... </robot>
  • 整个 URDF 的根元素
  • name 属性定义机器人名称,用于在 ROS2 系统中唯一标识这个机器人模型

3.4.2 <material> 材质定义

<!-- ==================== 材质定义 ==================== --> <!-- 材质通常在 URDF 文件开头定义,可被多个 Link 复用 --> <material name="blue"> <color rgba="0 0 0.8 1"/> <!-- R G B Alpha,范围 0~1 --> </material> <material name="white"> <color rgba="1 1 1 1"/> <!-- 白色:R=1 G=1 B=1,完全不透明 --> </material>

材质定义说明

属性 说明
name 材质名称,供后续引用
rgba 颜色值:红(R) 绿(G) 蓝(B) 透明度(Alpha),范围 0~1

<visual> 中使用材质

<link name="base_link"> <visual> <geometry> <cylinder length="0.1" radius="0.05"/> </geometry> <!-- 方式1:引用已定义的材质 --> <material name="blue"/> </visual> </link> <link name="link1"> <visual> <geometry> <box size="0.04 0.04 0.3"/> </geometry> <!-- 方式2:直接定义颜色(不推荐,不可复用) --> <material name=""> <color rgba="1 1 1 1"/> </material> </visual> </link>

最佳实践:在 <robot> 标签后、第一个 <link> 前集中定义所有材质,便于统一管理和复用。

3.4.3 <link> 连杆

<link> 是 URDF 中描述机器人刚性部件的元素,包含三个主要子元素:

<link name="base_link"> <visual>...</visual> <!-- 可视化(必需) --> <collision>...</collision> <!-- 碰撞检测(可选) --> <inertial>...</inertial> <!-- 惯性属性(仿真需要) --> </link>
子元素 用途 是否必需
<visual> RViz 显示用 是(否则看不到)
<collision> 物理仿真碰撞检测 仅仿真需要
<inertial> 动力学仿真 仅仿真需要
3.4.3.1 <visual> 可视化
<visual> <origin xyz="0 0 0.05" rpy="0 0 0"/> <!-- 几何体中心相对于 link 原点的位置 --> <geometry> <cylinder length="0.1" radius="0.05"/> <!-- 几何形状 --> </geometry> <material name="blue"/> <!-- 引用材质 --> </visual>

支持的基本几何体

几何体 语法 说明
圆柱 <cylinder length="0.1" radius="0.05"/> 高度、半径
长方体 <box size="0.1 0.2 0.3"/> X Y Z 三个方向的尺寸
球体 <sphere radius="0.05"/> 半径
网格 <mesh filename="package://pkg/meshes/file.stl"/> STL/DAE 文件
3.4.3.2 <collision> 碰撞体

<collision> 定义用于物理仿真中碰撞检测的几何体,通常与 <visual> 相同或简化:

<collision> <origin xyz="0 0 0.05" rpy="0 0 0"/> <geometry> <cylinder length="0.1" radius="0.05"/> </geometry> </collision>

关键点

  • 用途:Gazebo 等物理仿真器中用于碰撞检测
  • 简化策略:可以使用简化的几何体(如用圆柱近似复杂网格)以提高计算效率
  • 本教程中:仅做可视化展示时可省略
3.4.3.3 <inertial> 惯性属性

<inertial> 定义连杆的质量和惯性参数,用于动力学仿真:

<inertial> <origin xyz="0 0 0.05" rpy="0 0 0"/> <!-- 质心位置 --> <mass value="1.0"/> <!-- 质量(kg) --> <inertia <!-- 惯性张量 --> ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/> </inertial>

关键点

  • 质心位置origin):相对于 Link 原点
  • 质量mass):单位为千克(kg)
  • 惯性张量inertia):6 个独立分量,描述物体绕不同轴的转动惯量
  • 用途:Gazebo 动力学仿真、力矩计算
  • 本教程中:仅做可视化展示时可省略

提示:CAD 软件导出的 URDF 会自动计算精确的惯性参数。手写 URDF 时,如果不做动力学仿真,可以填写近似值。

3.4.5 <origin> 位姿(通用元素)

<origin> 是 URDF 中的通用元素,用于定义相对位姿。它在不同上下文中有不同含义:

<origin xyz="0 0 0.1" rpy="0 0 0"/>
属性 说明 单位
xyz X Y Z 平移
rpy Roll Pitch Yaw 旋转 弧度

在不同上下文中的含义

使用位置 相对关系 说明
<visual><origin> 几何体中心相对于 Link 原点 定义可视化几何体如何摆放
<collision><origin> 碰撞体中心相对于 Link 原点 定义碰撞体如何摆放
<inertial><origin> 质心相对于 Link 原点 定义质心位置
<joint><origin> 子 Link 原点相对于父 Link 原点 定义关节位置和姿态

示例对比

<!-- 在 <visual> 中:定义几何体相对于 link 原点的位置 --> <link name="base_link"> <visual> <origin xyz="0 0 0.05" rpy="0 0 0"/> <!-- 圆柱中心在 link 原点上方 5cm --> <geometry> <cylinder length="0.1" radius="0.05"/> </geometry> </visual> </link> <!-- 在 <joint> 中:定义子 link 相对于父 link 的位置 --> <joint name="joint1" type="revolute"> <parent link="base_link"/> <child link="link1"/> <origin xyz="0 0 0.1" rpy="0 0 0"/> <!-- link1 的原点在 base_link 原点上方 10cm --> ... </joint>

注意

  • rpy 使用的是弧度,不是角度!90° = π/2 ≈ 1.5708
  • rpy 的旋转顺序:先 Roll(绕 X 轴)→ 再 Pitch(绕 Y 轴)→ 最后 Yaw(绕 Z 轴)

3.4.6 <joint> 关节

<joint name="joint1" type="revolute"> <parent link="base_link"/> <!-- 父连杆 --> <child link="link1"/> <!-- 子连杆 --> <origin xyz="0 0 0.1" rpy="0 0 0"/> <!-- 子 link 原点相对于父 link 原点的位置 --> <axis xyz="0 0 1"/> <!-- 旋转轴方向 --> <limit lower="-3.14" upper="3.14" effort="10" velocity="1.0"/> <!-- 限位 --> </joint>

Joint 参数说明(以 joint1 为例)

元素 joint1 实际值 说明
name "joint1" 关节名称,在整个 URDF 中唯一标识这个关节
type revolute 关节类型:旋转关节(有限位)其他类型:continuous(无限旋转)、prismatic(滑动)、fixed(固定)
<parent> link="base_link" 父连杆:joint1 连接在 base_link 上
<child> link="link1" 子连杆:link1 是 joint1 的下级连杆
<origin> xyz="0 0 0.1" link1 的原点相对于 base_link 的原点的位置(单位:米)这里表示 link1 在 base_link 上方 0.1m
rpy="0 0 0" link1 坐标系相对于 base_link 坐标系的旋转(单位:弧度)这里无旋转,两个坐标系方向一致
<axis> xyz="0 0 1" 关节旋转轴方向:正 Z 轴(在 link1 的坐标系下)表示 link1 绕垂直向上的轴旋转
<limit> lower="-3.14" 关节角度下限:约 -180°(-π 弧度)
upper="3.14" 关节角度上限:约 +180°(π 弧度)
effort="10" 最大力矩限制:10 N⋅m(仅动力学仿真需要)
velocity="1.0" 最大速度限制:1.0 rad/s(仅动力学仿真需要)

关键理解

  • type 决定关节行为

    • revolute:有限位的旋转关节(最常用于机械臂)
    • continuous:无限旋转(如车轮)
    • prismatic:直线滑动(如升降台)
    • fixed:固定连接(如传感器安装)
  • <origin> 的含义:定义子连杆坐标系相对于父连杆坐标系的初始位姿

    • 当关节角度为 0 时,子连杆按照这个 origin 定义的位置和姿态放置
    • 当关节旋转时,子连杆绕 <axis> 定义的轴旋转
  • <axis> 的方向:在子连杆(link1)的坐标系下定义

    • xyz="0 0 1" 表示绕正 Z 轴旋转
    • 这个轴方向在 link1 坐标系中是固定的,会随 link1 一起运动

3.5 坐标系关系图解

┌─────────────────────────────────────────────────────────────────┐ │ 坐标系关系图解 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Z │ │ ↑ link2 坐标系原点 │ │ ┌───┼───┐ (在 joint2 位置) │ │ │ │ │ link2 (box) │ │ │ │ │ visual origin: (0,0,0.1) │ │ └───┼───┘ │ │ │ │ │ joint2 ─────────●───→ X joint origin: (0,0,0.3) │ │ (revolute) │ 表示 joint2 在 link1 上方 0.3m │ │ ┌──┴──┐ │ │ │ │ link1 (box) │ │ │ │ visual origin: (0,0,0.15) │ │ │ │ 表示 box 中心在 link1 原点上方 0.15m │ │ └──┬──┘ │ │ │ │ │ joint1 ────────●───→ X joint origin: (0,0,0.1) │ │ (revolute) │ 表示 joint1 在 base_link 上方 0.1m │ │ ┌─┴──┐ │ │ │ │ base_link (cylinder) │ │ │ ●──┼──→ X base_link 坐标系原点 │ │ │ │ │ visual origin: (0,0,0.05) │ │ └─┼──┘ 表示圆柱中心在原点上方 0.05m │ │ ═════════════╧═════════════════════════════════════════════ │ │ 地面 (Z=0) │ │ │ │ 【几何体尺寸说明】 │ │ • base_link: 圆柱 高0.1m × 半径0.05m │ │ • link1: 长方体 0.04m × 0.04m × 0.3m (高) │ │ • link2: 长方体 0.06m × 0.06m × 0.2m (高) │ │ │ └─────────────────────────────────────────────────────────────────┘

关键理解

  1. Link 的原点 ≠ 几何体的中心,<visual><origin> 定义几何体中心相对于 link 原点的偏移
  2. Joint 的 origin 定义的是 child link 的原点相对于 parent link 原点的位置
  3. 当 joint 角度为 0 时,child link 按照 joint origin 定义的位姿放置

3.6 URDF 如何自动发布 TF?

8.ROS2 TF2 机械臂坐标系实战中,我们手动使用 TransformBroadcaster 发布 TF 变换。但在使用 URDF 时,这个过程是自动完成的

3.6.1 核心概念:URDF 只是"图纸"

重要理解:URDF 文件本身不会发布任何 TF,它只是一个静态的 XML 文档,描述了机器人的结构。

3.6.2 robot_state_publisher:将 URDF 变成 TF 的"翻译器"

要让 URDF 变成活的 TF 树,需要 robot_state_publisher 节点:

启动命令回顾

# 终端 1:启动 robot_state_publisher ros2 run robot_state_publisher robot_state_publisher ~/ros2_ws/src/urdf_test/simple_3link.urdf

这行命令做了什么?

  • ros2 run robot_state_publisher robot_state_publisher:启动 robot_state_publisher 节点
  • ~/ros2_ws/src/urdf_test/simple_3link.urdf:传入 URDF 文件路径,告诉节点要加载哪个机器人模型

同时需要关节状态数据

# 终端 2:启动关节滑块 GUI ros2 run joint_state_publisher_gui joint_state_publisher_gui ~/ros2_ws/src/urdf_test/simple_3link.urdf

这行命令做了什么?

  • joint_state_publisher_gui:启动一个带图形界面的节点,提供滑块来手动设置关节角度
  • 作用:拖动滑块 → 改变关节角度 → 发布到 /joint_statesrobot_state_publisher 计算新 TF → RViz 更新显示
  • 真实机械臂中:这个话题由机械臂驱动节点发布(如 Episode 1 的 robot_interface_node
  • Randomize:随机设置所有关节角度
  • Center:将所有关节归零

工作流程

URDF
┌─────────────────────────────────────────────────────────────────┐ URDF TF:robot_state_publisher 的作用 ├─────────────────────────────────────────────────────────────────┤ 步骤 1: 读取 URDF 文件 ┌──────────────────┐ simple_3link.urdf│ └────────┬─────────┘ 启动时加载 ┌─────────────────────────────────────────┐ robot_state_publisher 节点 (步骤2启动的节点) 解析 URDF,提取: 所有 Link 的名称 所有 Joint 的类型、位置、轴向 构建机器人运动学树 └─────────────────────────────────────────┘ 步骤 2: 订阅关节状态 ┌─────────────────────────────────────────┐ /joint_states 话题 (sensor_msgs/JointState) joint_state_publisher_gui 发布 (步骤3启动的节点) name: ['joint1', 'joint2'] position: [0.5, -0.3] # 弧度 │ │ └─────────────────────────────────────────┘ 步骤 3: 计算正运动学 ┌─────────────────────────────────────────┐ 根据 Joint origin 和当前角度: T_base_to_link1 = T_joint1_origin × Rot(Z, θ1) T_link1_to_link2 = T_joint2_origin × Rot(Y, θ2) └─────────────────────────────────────────┘ 步骤 4: 发布 TF 变换 ┌─────────────────────────────────────────┐ /tf 话题 (tf2_msgs/TFMessage) base_link link1 的变换 link1 link2 的变换 link2 end_effector 的变换 (fixed) └─────────────────────────────────────────┘ 步骤 5: RViz 可视化 ┌─────────────────────────────────────────┐ RViz 订阅 /tf /robot_description 根据 TF 变换渲染机器人模型 └─────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘

3.6.3 与第 8 章手动发布 TF 的对比

对比项 第 8 章:手动发布 TF 第 9 章:URDF 自动发布 TF
TF 来源 代码中硬编码坐标变换 URDF 文件中的 ``
发布节点 自己编写的 Python/C++ 节点 robot_state_publisher
动态更新 需要手动计算新的变换并发布 订阅 /joint_states 自动计算
运动学计算 需要自己实现正运动学 robot_state_publisher 内置
适用场景 额外的传感器、工具坐标系 机器人本体的关节坐标系

四、CAD 导出 URDF:工程实践中的主流方法

4.1 为什么不手写 URDF?

上一节的 3-link 示例大约 120 行,看起来还能手写。但实际的机械臂 URDF:

对比项 3-Link 示例 6 轴机械臂
Link 数量 3 7(base + 6 link)
Joint 数量 2 6
几何体 基本形状 复杂 STL 网格
惯性参数 估算 需要精确计算
尺寸精度 随意 必须与实物一致
代码行数 ~120 ~400

结论:实际工程中,几乎没有人手写复杂的 URDF,而是从 CAD 软件导出。

4.2 主流 CAD 导出工具

更多参考:Generating an URDF File

4.4 导出后的标准目录结构

比如 Episode1 机械臂的 URDF 就是用 SolidWorks URDF Exporter 导出的, 导出后,通常会形成以下 ROS2 包结构:

episode1_urdf_1113/ ├── config │ └── gz_bridge.yaml # Gazebo-ROS2 话题桥接配置 ├── episode1_urdf_1113 │ └── __init__.py ├── launch │ ├── gz_simulator_launch.py # Gazebo 仿真启动 │ └── launch.py # RViz 可视化启动 ├── meshes │ ├── collision # 碰撞检测网格 │ │ ├── base_link.STL │ │ ├── link1.STL │ │ ├── link2.STL │ │ ├── link3.STL │ │ ├── link4.STL │ │ ├── link5.STL │ │ └── link6.STL │ └── visual # 可视化网格 │ ├── base_link.STL │ ├── link1.STL │ ├── link2.STL │ ├── link3.STL │ ├── link4.STL │ ├── link5.STL │ └── link6.STL ├── package.xml # 功能包清单 ├── resource │ └── episode1_urdf_1113 ├── setup.py # Python 包安装配置 ├── urdf │ ├── episode1_urdf_1113.urdf # URDF 模型(ROS2/RViz) │ └── robot.sdf # SDF 模型(Gazebo) └── world └── depot.sdf # Gazebo 仿真场景

最佳实践:将 URDF 相关文件(urdf/、meshes/、launch/)放在独立的功能包中,便于复用和维护。

五、Episode 1 机械臂 URDF 解析

现在我们来分析 Episode 1 机械臂的实际 URDF 文件。

5.2 机械臂结构概览

属性
机器人名称 episode1_urdf_1113
Link 数量 7 个(base_link + link1~link6)
Joint 数量 6 个(joint1~joint6,全部为 revolute)
自由度 6 DOF
网格格式 STL

5.3 结构树状图

episode1_urdf_1113 (机器人) │ └── base_link (底座) │ └── joint1 (revolute, Z轴) │ └── link1 (肩部) │ └── joint2 (revolute, -Z轴) │ └── link2 (大臂) │ └── joint3 (revolute, -Z轴) │ └── link3 (肘部) │ └── joint4 (revolute, -Z轴) │ └── link4 (小臂) │ └── joint5 (revolute, -Z轴) │ └── link5 (腕部) │ └── joint6 (revolute, Z轴) │ └── link6 (末端法兰)

link1 为例:

<link name="link1"> <!-- 惯性属性:用于动力学仿真 --> <inertial> <origin xyz="0.0129 0.00484 0.1429" rpy="0 0 0" /> <!-- 质心位置 --> <mass value="0.623" /> <!-- 质量:0.623 kg --> <inertia <!-- 惯性张量 --> ixx="0.000787" ixy="3.99e-05" ixz="-0.000208" iyy="0.00117" iyz="-3.07e-06" izz="0.00135" /> </inertial> <!-- 可视化:RViz 显示用 --> <visual> <origin xyz="0 0 0" rpy="0 0 0" /> <geometry> <mesh filename="package://episode1_urdf_1113/meshes/visual/link1.STL" /> </geometry> <material name=""> <color rgba="1 1 1 1" /> <!-- 白色 --> </material> </visual> <!-- 碰撞体:物理仿真用 --> <collision> <origin xyz="0 0 0" rpy="0 0 0" /> <geometry> <mesh filename="package://episode1_urdf_1113/meshes/visual/link1.STL" /> </geometry> </collision> </link>

Visual 参数说明(以 link1 为例)

元素 link1 实际值 说明
<origin> xyz="0 0 0" 网格中心与 link 原点重合(CAD 中已设置好)
rpy="0 0 0" 无旋转(网格已按正确姿态建模)
<geometry> <mesh filename="package://episode1_urdf_1113/meshes/visual/link1.STL" /> 使用 STL 网格文件,包含从 SolidWorks 导出的精确 3D 模型
<material> <color rgba="1 1 1 1" /> 白色(R=1 G=1 B=1 Alpha=1,完全不透明)

为什么 Episode 1 的 origin 是 (0,0,0)?

注意到一个关键差异:

对比项 CAD 导出的 URDF(Episode 1) 手动创建的 URDF(3-Link 示例)
<visual><origin> xyz="0 0 0" xyz="0 0 0.05"(需要偏移)
几何体类型 <mesh> STL 网格文件 <cylinder> 等基本几何体
原因 STL 文件在 CAD 中建模时已定义好坐标 基本几何体默认以自身中心为原点

详细解释

  1. CAD 导出的网格文件(如 Episode 1):

    • SolidWorks 等 CAD 软件在建模时,设计师会为每个零件设置坐标系原点
    • 导出的 STL 文件包含的顶点坐标已经是相对于这个原点的
    • 所以 URDF 中 <visual><origin> 通常是 (0,0,0),网格本身已经在正确位置
  2. 手动创建的基本几何体(如 3-Link 示例):

    • <cylinder> 等几何体默认以其几何中心为原点
    • 如果想让圆柱底部在 link 原点(地面),需要通过 <origin> 向上偏移半个高度
    • 例如:高 0.1m 的圆柱,origin xyz="0 0 0.05" 使其底部在 Z=0

网格文件路径解析package://episode1_urdf_1113/meshes/visual/link1.STL → 实际路径:~/ros2_ws/install/episode1_urdf_1113/share/episode1_urdf_1113/meshes/visual/link1.STL

5.5 Joint 结构详解

joint2 为例(最具代表性,因为它有复杂的 origin 变换):

<joint name="joint2" type="revolute"> <!-- 子连杆原点相对于父连杆原点的位置和姿态 --> <origin xyz="0.055 -0.02 0.166" <!-- 平移:X +5.5cm, Y -2cm, Z +16.6cm --> rpy="1.5708 0 0" /> <!-- 旋转:绕 X 轴旋转 90°(π/2 弧度) --> <parent link="link1" /> <!-- 父连杆 --> <child link="link2" /> <!-- 子连杆 --> <axis xyz="0 0 -1" /> <!-- 旋转轴:负 Z 轴方向 --> <limit <!-- 关节限位 --> lower="-1.5708" <!-- 下限:-90° --> upper="1.5708" <!-- 上限:+90° --> effort="0" <!-- 力矩限制(0 表示未设置) --> velocity="0" /> <!-- 速度限制(0 表示未设置) --> </joint>

Joint 参数说明(以 joint2 为例)

元素 joint2 实际值 说明
type revolute 关节类型:旋转关节(有限位)
<parent> link="link1" 父连杆:joint2 连接在 link1 上
<child> link="link2" 子连杆:link2 是 joint2 的下级连杆
<origin> xyz="0.055 -0.02 0.166" link2 的原点相对于 link1 的原点的位置(单位:米)
rpy="1.5708 0 0" link2 坐标系相对于 link1 坐标系的旋转(单位:弧度)这里是绕 X 轴旋转 90°(π/2 弧度)
<axis> xyz="0 0 -1" 关节旋转轴方向:负 Z 轴(在 link2 的坐标系下)
<limit> lower="-1.5708" 关节角度下限:-90°(-π/2 弧度)
upper="1.5708" 关节角度上限:+90°(π/2 弧度)
effort="0" 最大力矩限制(0 表示未设置,仅可视化时可忽略)
velocity="0" 最大速度限制(0 表示未设置,仅可视化时可忽略)

关键理解

  • <origin> 的含义:定义子连杆坐标系相对于父连杆坐标系的位置和姿态

    • 当关节角度为 0 时,link2 的坐标系按照这个 origin 定义的位置和姿态放置
    • 当关节旋转时,link2 绕 <axis> 定义的轴旋转
  • <axis> 的方向:在子连杆(link2)的坐标系下定义

    • xyz="0 0 -1" 表示绕负 Z 轴旋转
    • 这个轴方向在 link2 坐标系中是固定的,会随 link2 一起运动
  • 与 Visual 的 origin 区别

    • <visual><origin>:定义几何体中心相对于 Link 原点的偏移
    • <joint><origin>:定义子 Link 整个坐标系相对于父 Link 坐标系的变换

5.6 所有 Joint 参数汇总

Joint 父 → 子 位置 (xyz) 旋转 (rpy) 轴向 限位 (°)
joint1 base→link1 (0, 0, 0) (0, 0, 0) +Z -180 ~ +160
joint2 link1→link2 (0.055, -0.02, 0.166) (90°, 0, 0) -Z -90 ~ +90
joint3 link2→link3 (0, 0.2, 0) (0, 0, 180°) -Z -83 ~ +80
joint4 link3→link4 (0, -0.056, -0.02) (-90°, 0, -90°) -Z -30 ~ +305
joint5 link4→link5 (0, 0, -0.192) (90°, -90°, 0) -Z -110 ~ +110
joint6 link5→link6 (-0.055, 0, 0) (90°, 0, -90°) +Z -30 ~ +305

注意:表中的角度是从弧度换算的近似值,实际 URDF 中使用弧度。