ROS 2 Jazzy 机器人建模全指南:TF坐标变换、URDF模型编写与TF发布实战

在机器人开发的世界里,有两个概念如同空气和水一样无处不在:TF(坐标变换) 和 URDF(统一机器人描述格式)。它们是所有机器人应用的"骨架"——没有TF,你永远不知道相机相对于机械臂末端在哪里;没有URDF,你就无法描述机器人的结构,更谈不上仿真和控制。

本文将带你从零开始,通过一步步实战,从在RViz中直观理解TF,到亲手编写一个完整的两轮差速移动底盘URDF,最后将其打包成标准ROS 2包并自动发布TF。掌握这三个技能,你就真正踏入了机器人系统开发的大门。

在这里插入图片描述

一、TF:机器人世界的"骨骼系统"

想象一下,你正在控制一个机械臂抓取桌子上的杯子。你需要知道:

  • 机械臂底座相对于世界坐标系在哪里?
  • 机械臂末端相对于底座在哪里?
  • 相机相对于机械臂末端在哪里?
  • 杯子相对于相机在哪里?

这些"谁相对谁在哪里"的问题,就是TF要解决的。

1.1 先动手:在RViz中直观感受TF

空谈概念不如动手实践。我们先用ROS 2自带的示例包,在RViz中直观地看到TF是什么样子。

步骤1:安装依赖
# 安装urdf_tutorial示例包(将<distro>替换为jazzy)
sudo apt install ros-jazzy-urdf-tutorial ros-jazzy-tf2-tools
步骤2:启动示例机器人
# 启动urdf_tutorial的显示程序
ros2 launch urdf_tutorial display.launch.py model:=/opt/ros/jazzy/share/urdf_tutorial/urdf/07-physics.urdf

启动后你会看到两个窗口:

  • RViz主窗口:3D视图中显示一个机器人模型
  • Joint State Publisher:一堆滑条,用来模拟关节角度

在这里插入图片描述

现在拖动滑条,你会看到机器人的各个部件跟着动起来。同时,机器人身上会出现很多彩色的坐标轴,还有连接它们的箭头——这些就是Frame和TF。

1.2 三个核心概念:Link / Frame / TF 一次讲清

很多初学者会混淆这三个概念,其实它们的关系非常简单:

  • Link(连杆):机器人的一个刚体部件,比如底盘、轮子、机械臂的一节。它本身是不会动的。
  • Frame(坐标系):每个Link都有一个自己的坐标系原点和朝向。在RViz中就是那组红绿蓝的坐标轴。
  • TF(变换):两个Frame之间的空间关系,包括平移和旋转。在RViz中就是连接两个Frame的箭头。

通俗解释:
把机器人比作你的身体:

  • Link = 你的上臂、前臂、手掌这些骨头(刚体)
  • Frame = 每个骨头的关节点(肘关节、腕关节)
  • TF = 肘关节和肩关节之间的角度和距离,腕关节和肘关节之间的角度和距离

当你拖动Joint State Publisher的滑条时,你改变的是关节的角度,也就是两个Frame之间的TF关系。TF一变,所有子Frame都会跟着动——这就是为什么你转动肘关节时,前臂和手掌会一起跟着动。

1.3 TF Tree:为什么它一定是一棵树?

TF不是乱连的,它被严格组织成一棵树状结构。这是ROS 2中一个非常重要的设计原则。

父子关系

每个TF都有明确的父子关系:

  • parent frame:父坐标系
  • child frame:子坐标系

一个Frame可以有多个子Frame,但只能有一个父Frame。这样整个关系就是一棵树,而不是一张网。

这样做的好处是:从任意一个Frame到另一个Frame的路径是唯一的,计算它们之间的变换时不会产生歧义。

导出TF Tree

ROS 2提供了一个非常好用的工具,可以把整棵TF Tree导出成PDF文件:

# 保持RViz和机器人模型运行,新开终端执行
ros2 run tf2_tools view_frames

这个命令会监听/tf话题5秒钟,然后在当前目录生成两个文件:

  • frames.pdf:完整的TF Tree图
  • frames.gv:Graphviz源文件

打开PDF,你会看到类似这样的结构:

base_link
├── base_footprint
├── right_wheel_link
├── left_wheel_link
├── caster_wheel_link
└── shoulder_link
    └── elbow_link
        └── wrist_link
            └── gripper_link

图1 TF Tree结构示意图(出自原文Figure 1)

从图中可以清晰地看到,base_link是根节点,所有其他Frame都是它的子孙。当base_link移动时,整棵树上的所有Frame都会跟着移动。

1.4 TF的本质:一个话题而已

你在RViz里看到的所有TF,本质上都来自一个话题:/tf(以及静态变换的/tf_static)。

你可以直接在终端查看这个话题的内容:

ros2 topic echo /tf

你会看到大量的变换数据,每条变换都包含以下核心信息:

transforms:
  - header:
      stamp:
        sec: 1719900000
        nanosec: 123456789
      frame_id: "base_link"
    child_frame_id: "right_wheel_link"
    transform:
      translation:
        x: -0.15
        y: -0.225
        z: 0.0
      rotation:
        x: 0.0
        y: 0.0
        z: 0.0
        w: 1.0
  • stamp:时间戳,表示这个变换是在哪个时刻的
  • frame_id:父坐标系
  • child_frame_id:子坐标系
  • translation:平移量(x, y, z),单位米
  • rotation:旋转量,用四元数(x, y, z, w)表示

注意:旋转用四元数表示,虽然对人来说不直观,但在计算机里计算更稳定,不会出现"万向节死锁"问题。你不需要手动计算四元数,ROS 2的tf2库会帮你处理所有变换计算。

1.5 TF到底解决了什么问题?

总结一下,TF系统解决了机器人开发中三个最核心的问题:

  1. 空间关系查询:任意两个坐标系之间的相对位置和姿态
  2. 时间一致性:所有变换都带时间戳,确保你查询的是同一时刻的关系
  3. 自动链式计算:你只需要定义相邻Frame之间的变换,tf2库会自动计算任意两个Frame之间的变换

举个例子:如果你想知道机械臂末端相对于世界坐标系的位置,你不需要自己计算world -> base_link -> shoulder -> elbow -> wrist -> end_effector这一长串变换,只需要调用tf2库的一个函数:

transform = tf_buffer.lookup_transform("world", "end_effector", rclpy.time.Time())

tf2会自动帮你沿着TF Tree找到路径并计算出最终结果。

二、URDF:机器人的"说明书"

现在你已经理解了TF是什么,以及它有多重要。那问题来了:TF是从哪里来的?

答案是:从URDF来的。

URDF(Unified Robot Description Format)是ROS生态中标准的机器人描述格式。它是一个XML文件,用来描述机器人的:

  • 刚体部件(Links)
  • 部件之间的连接方式(Joints)
  • 外观和物理属性

robot_state_publisher节点会读取URDF文件,结合关节状态,自动发布TF。

2.1 最小可用URDF:先让机器人"看得见"

我们从最简单的URDF开始,写一个只有一个底盘的机器人。

创建my_robot.urdf文件:

<?xml version="1.0"?>
<robot name="my_robot">
  <!-- 定义绿色材质 -->
  <material name="green">
    <color rgba="0 0.6 0 1"/>
  </material>

  <!-- 底盘Link -->
  <link name="base_link">
    <visual>
      <geometry>
        <!-- 长方体:长0.6m,宽0.4m,高0.2m -->
        <box size="0.6 0.4 0.2"/>
      </geometry>
      <!-- 把visual沿z轴上移0.1m,让link原点在盒子底部中心 -->
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <material name="green"/>
    </visual>
  </link>
</robot>

用RViz可视化这个URDF:

ros2 launch urdf_tutorial display.launch.py model:=/home/<你的用户名>/my_robot.urdf

你会在RViz中看到一个绿色的长方体。这就是你的第一个机器人模型!

关键细节:
注意<origin xyz="0 0 0.1" rpy="0 0 0"/>这一行。它的意思是:

  • 这个长方体的视觉中心在link原点的正上方0.1m处
  • 这样link原点就正好在长方体的底部中心,而不是中心

这一点非常重要,因为后面我们要把轮子放在底盘的底部,让机器人正好放在地面上。

2.2 URDF最难的部分:两个Link怎么正确拼起来?

很多初学者写URDF时,都会在这里卡很久:两个Link怎么用Joint正确连接?

不要靠"乱调参数试出来",那会让你越写越崩。正确的做法是遵循一个固定的5步流程,以后每加一个Link都照做。

我们以在底盘上加一个肩部关节为例,演示这个流程:

Step 1:先加新Link,所有origin先写0
<!-- 肩部Link -->
<link name="shoulder_link">
  <visual>
    <geometry>
      <!-- 圆柱:半径0.1m,长度0.3m -->
      <cylinder radius="0.1" length="0.3"/>
    </geometry>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <material name="gray"/>
  </visual>
</link>
Step 2:加Joint,建立parent/child关系,origin先写0
<!-- 底盘和肩部之间的关节 -->
<joint name="base_shoulder_joint" type="fixed">
  <parent link="base_link"/>
  <child link="shoulder_link"/>
  <origin xyz="0 0 0" rpy="0 0 0"/>
</joint>

重要:如果没有Joint,你会遇到经典报错:Two root links found。因为URDF只能有一个根Link。

Step 3:修改Joint的origin,把child的Frame放到正确位置

这是最关键的一步!TF是由Joint决定的,和Visual无关。所以先把坐标系放对,再管外观。

我们想把肩部放在底盘的正上方,底盘高度是0.2m,所以Joint的origin的z值应该是0.2:

<origin xyz="0 0 0.2" rpy="0 0 0"/>
Step 4:设置Joint的类型、轴和限制(如果需要运动)

常用的Joint类型:

  • fixed:固定关节,不能动(用于传感器、结构件)
  • revolute:有限角度旋转关节(用于机械臂关节)
  • continuous:无限旋转关节(用于轮子)
  • prismatic:直线滑动关节(用于升降结构)

我们让肩部可以绕Z轴旋转±180°:

<joint name="base_shoulder_joint" type="revolute">
  <parent link="base_link"/>
  <child link="shoulder_link"/>
  <origin xyz="0 0 0.2" rpy="0 0 0"/>
  <!-- 旋转轴:Z轴 -->
  <axis xyz="0 0 1"/>
  <!-- 限制:角度范围±π弧度,最大速度100rad/s,最大力矩100Nm -->
  <limit lower="-3.14" upper="3.14" velocity="100" effort="100"/>
</joint>
Step 5:最后修改child Link的visual origin,让外形贴合坐标系

现在坐标系已经放对了,但圆柱的中心在坐标系原点,我们想让圆柱从坐标系向上"长出去"。圆柱长度是0.3m,所以把visual沿z轴上移0.15m:

<origin xyz="0 0 0.15" rpy="0 0 0"/>

到此,肩部就完美地拼接到底盘上了!现在拖动RViz里的滑条,你会看到肩部绕Z轴旋转。

2.3 实战:写一个完整的两轮差速移动底盘

现在我们用上面的5步流程,写一个完整的两轮差速移动底盘。它包含:

  • base_footprint:虚拟Link,作为地面上的2D原点
  • base_link:底盘主体
  • right_wheel_link:右轮
  • left_wheel_link:左轮
  • caster_wheel_link:前方脚轮
完整URDF代码
<?xml version="1.0"?>
<robot name="my_diff_drive_robot">
  <!-- 材质定义 -->
  <material name="green">
    <color rgba="0 0.6 0 1"/>
  </material>
  <material name="black">
    <color rgba="0 0 0 1"/>
  </material>
  <material name="gray">
    <color rgba="0.5 0.5 0.5 1"/>
  </material>

  <!-- 1. base_footprint:虚拟Link,地面上的2D原点 -->
  <link name="base_footprint"/>

  <!-- 2. base_link:底盘主体 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.6 0.4 0.2"/>
      </geometry>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <material name="green"/>
    </visual>
  </link>

  <!-- base_footprint和base_link之间的固定关节 -->
  <joint name="base_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link"/>
    <!-- 把base_link抬高0.1m(轮半径) -->
    <origin xyz="0 0 0.1" rpy="0 0 0"/>
  </joint>

  <!-- 3. 右轮 -->
  <link name="right_wheel_link">
    <visual>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
      <!-- 轮子绕X轴转90度,让它立起来 -->
      <origin xyz="0 0 0" rpy="1.5708 0 0"/>
      <material name="black"/>
    </visual>
  </link>

  <joint name="base_right_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="right_wheel_link"/>
    <!-- 位置:x=-0.15(靠后),y=-0.225(右侧),z=0(底部) -->
    <origin xyz="-0.15 -0.225 0" rpy="0 0 0"/>
    <!-- 旋转轴:Y轴 -->
    <axis xyz="0 1 0"/>
  </joint>

  <!-- 4. 左轮(和右轮对称,y坐标为正) -->
  <link name="left_wheel_link">
    <visual>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
      <origin xyz="0 0 0" rpy="1.5708 0 0"/>
      <material name="black"/>
    </visual>
  </link>

  <joint name="base_left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel_link"/>
    <origin xyz="-0.15 0.225 0" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
  </joint>

  <!-- 5. 前方脚轮(被动轮,固定关节) -->
  <link name="caster_wheel_link">
    <visual>
      <geometry>
        <sphere radius="0.05"/>
      </geometry>
      <material name="gray"/>
    </visual>
  </link>

  <joint name="base_caster_wheel_joint" type="fixed">
    <parent link="base_link"/>
    <child link="caster_wheel_link"/>
    <!-- 位置:x=0.2(前方),y=0(居中),z=-0.05(底部) -->
    <origin xyz="0.2 0 -0.05" rpy="0 0 0"/>
  </joint>
</robot>

现在用RViz打开这个URDF,你会看到一个完整的两轮差速底盘。拖动滑条,两个轮子会独立旋转。

2.4 Xacro:让URDF变得可维护

当你的URDF越来越长,你会发现硬编码的数字到处都是,重复代码越来越多。比如左右轮的代码几乎一模一样,只是y坐标符号不同。

Xacro就是为了解决这个问题而生的。它是URDF的模板语言,提供了三个核心功能:

  • Properties(变量):把硬编码的数字定义成变量
  • Macros(宏):把重复的代码写成模板
  • Include(包含):把大文件拆分成多个小文件
把URDF改成Xacro

把文件名改成my_robot.urdf.xacro,然后修改内容:

<?xml version="1.0"?>
<robot name="my_diff_drive_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <!-- 定义变量 -->
  <xacro:property name="base_length" value="0.6"/>
  <xacro:property name="base_width" value="0.4"/>
  <xacro:property name="base_height" value="0.2"/>
  <xacro:property name="wheel_radius" value="0.1"/>
  <xacro:property name="wheel_length" value="0.05"/>
  <xacro:property name="pi" value="3.141592653589793"/>

  <!-- 材质定义 -->
  <material name="green">
    <color rgba="0 0.6 0 1"/>
  </material>
  <material name="black">
    <color rgba="0 0 0 1"/>
  </material>
  <material name="gray">
    <color rgba="0.5 0.5 0.5 1"/>
  </material>

  <!-- 轮子宏:传入prefix参数(left/right) -->
  <xacro:macro name="wheel" params="prefix">
    <link name="${prefix}_wheel_link">
      <visual>
        <geometry>
          <cylinder radius="${wheel_radius}" length="${wheel_length}"/>
        </geometry>
        <origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
        <material name="black"/>
      </visual>
    </link>

    <joint name="base_${prefix}_wheel_joint" type="continuous">
      <parent link="base_link"/>
      <child link="${prefix}_wheel_link"/>
      <origin 
        xyz="-0.15 ${prefix == 'left' ? base_width/2 + wheel_length/2 : -(base_width/2 + wheel_length/2)} 0" 
        rpy="0 0 0"
      />
      <axis xyz="0 1 0"/>
    </joint>
  </xacro:macro>

  <!-- base_footprint -->
  <link name="base_footprint"/>

  <!-- base_link -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="${base_length} ${base_width} ${base_height}"/>
      </geometry>
      <origin xyz="0 0 ${base_height/2}" rpy="0 0 0"/>
      <material name="green"/>
    </visual>
  </link>

  <joint name="base_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link"/>
    <origin xyz="0 0 ${wheel_radius}" rpy="0 0 0"/>
  </joint>

  <!-- 调用宏,生成左右轮 -->
  <xacro:wheel prefix="left"/>
  <xacro:wheel prefix="right"/>

  <!-- 脚轮 -->
  <link name="caster_wheel_link">
    <visual>
      <geometry>
        <sphere radius="${wheel_radius/2}"/>
      </geometry>
      <material name="gray"/>
    </visual>
  </link>

  <joint name="base_caster_wheel_joint" type="fixed">
    <parent link="base_link"/>
    <child link="caster_wheel_link"/>
    <origin xyz="${base_length/2 - wheel_radius/2} 0 ${-wheel_radius/2}" rpy="0 0 0"/>
  </joint>
</robot>

现在代码清爽多了!所有尺寸都定义成了变量,左右轮的重复代码用一个宏解决了。而且Xacro文件仍然可以用同样的方式在RViz中打开:

ros2 launch urdf_tutorial display.launch.py model:=/home/<你的用户名>/my_robot.urdf.xacro

三、从URDF到TF:发布自己的坐标变换

现在你已经写出了自己的URDF,但我们一直依赖urdf_tutorial来帮我们启动和发布TF。在真实项目中,你需要自己发布TF,并且把所有文件打包成标准的ROS 2包。

3.1 发布TF的核心节点:robot_state_publisher

发布TF的核心节点是robot_state_publisher。它是ROS 2的官方节点,几乎所有机器人项目都会用到。

它需要两个输入:

  1. robot_description参数:URDF文件的内容(Xacro需要先展开)
  2. /joint_states话题:每个关节的当前状态(位置、速度等)

工作流程:

  1. robot_state_publisher读取URDF,了解机器人的结构
  2. 它订阅/joint_states话题,获取每个关节的当前角度
  3. 根据URDF和关节状态,计算所有Frame之间的变换
  4. 把变换发布到/tf和/tf_static话题

在这里插入图片描述

robot_state_publisher工作流程

3.2 不用urdf_tutorial:手动在终端发布TF

在写launch文件之前,我们先在终端手动跑通整个流程,这样你会清楚每个组件的作用。

Terminal 1:启动robot_state_publisher
# 用xacro命令展开Xacro文件,然后作为参数传给robot_state_publisher
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(xacro /home/<你的用户名>/my_robot.urdf.xacro)"

如果正常启动,你会看到它输出识别到的所有Link。

Terminal 2:启动joint_state_publisher_gui
# 这个节点提供滑条界面,用来模拟关节状态,发布到/joint_states话题
ros2 run joint_state_publisher_gui joint_state_publisher_gui

现在拖动滑条,/joint_states话题就会有数据,robot_state_publisher就会开始发布TF。

Terminal 3:验证TF是否发布成功
# 查看/tf话题
ros2 topic echo /tf

# 导出TF Tree
ros2 run tf2_tools view_frames
Terminal 4:启动RViz并配置
rviz2

在RViz中做三个关键配置:

  1. Global Options → Fixed Frame:改成base_footprint
  2. 点击左下角Add → 选择RobotModel → 点击OK
  3. 点击左下角Add → 选择TF → 点击OK

现在你应该能在RViz中看到你的机器人模型和所有TF坐标轴了。拖动滑条,机器人会跟着动起来。

3.3 工程最佳实践:创建标准的_description包

在真实项目中,你不应该把URDF文件散落在home目录里。ROS 2的标准做法是:为机器人模型单独创建一个_description包。

这个包不写任何业务代码,只用来存放:

  • urdf/:URDF/Xacro文件
  • meshes/:CAD导出的模型文件(STL/DAE)
  • rviz/:RViz配置文件
  • launch/:启动文件
步骤1:创建工作空间和包
# 创建工作空间
mkdir -p ~/my_robot_ws/src
cd ~/my_robot_ws
colcon build

# 创建description包
cd src
ros2 pkg create my_robot_description --build-type ament_cmake

# 清理不需要的目录
cd my_robot_description
rm -r include/ src/
步骤2:创建目录结构
mkdir urdf meshes rviz launch
步骤3:把文件移到对应的目录
  • 把my_robot.urdf.xacro移到urdf/目录
  • 把RViz配置保存为rviz/display.rviz
步骤4:修改Xacro中的include路径

如果你有多个Xacro文件互相include,要改成从包中查找的写法:

<!-- 错误写法 -->
<xacro:include filename="common_properties.xacro"/>

<!-- 正确写法 -->
<xacro:include filename="$(find my_robot_description)/urdf/common_properties.xacro"/>
步骤5:编写CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(my_robot_description)

find_package(ament_cmake REQUIRED)

# 安装所有资源目录
install(
  DIRECTORY urdf meshes rviz launch
  DESTINATION share/${PROJECT_NAME}/
)

ament_package()

3.4 写一个launch文件:一键启动TF发布和RViz

现在我们写一个launch文件,一键启动所有需要的节点:

  • robot_state_publisher
  • joint_state_publisher_gui
  • rviz2

创建launch/display.launch.xml:

<launch>
  <!-- 定义路径变量 -->
  <let name="urdf_path" value="$(find-pkg-share my_robot_description)/urdf/my_robot.urdf.xacro"/>
  <let name="rviz_config_path" value="$(find-pkg-share my_robot_description)/rviz/display.rviz"/>

  <!-- 启动robot_state_publisher -->
  <node pkg="robot_state_publisher" exec="robot_state_publisher">
    <param name="robot_description" value="$(command 'xacro $(var urdf_path)')"/>
  </node>

  <!-- 启动joint_state_publisher_gui -->
  <node pkg="joint_state_publisher_gui" exec="joint_state_publisher_gui"/>

  <!-- 启动RViz,加载配置文件 -->
  <node pkg="rviz2" exec="rviz2" args="-d $(var rviz_config_path)"/>
</launch>
构建并运行
cd ~/my_robot_ws
colcon build --packages-select my_robot_description
source install/setup.bash

# 一键启动
ros2 launch my_robot_description display.launch.xml

启动后,你会看到RViz自动打开,显示你的机器人模型,并且滑条可以控制关节运动。TF也会自动发布。

总结

本文完整介绍了ROS 2机器人建模的三个核心技能:

  1. TF坐标变换:理解Link、Frame、TF的关系,学会用RViz可视化和调试TF
  2. URDF机器人描述:掌握编写URDF的5步流程,用Xacro让模型更可维护
  3. TF发布与打包:用robot_state_publisher发布TF,创建标准的_description包

这套流程是所有ROS机器人项目的基础,无论是移动机器人、机械臂还是无人机,都遵循同样的模式。掌握了这些,你就为下一步的Gazebo仿真、导航和控制打下了坚实的基础。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐