2. 机器人模型与TF树:URDF模型构建、xacro优化、TF静态变换与动态变换、robot_state_publisher配置

好,我们正式开始第二讲。上一章我们把Nav2的宏观架构理清了,但有个问题一直悬着:机器人到底长什么样?它的轮子、激光雷达、IMU之间是什么位置关系?

说白了,导航系统要干活,首先得知道自己的“身体”结构。URDF就是干这个的——它用XML描述机器人的连杆(link)和关节(joint)。而TF树,则是这些部件在空间中的实时坐标变换关系。

我个人习惯,每次搭建新机器人,第一件事就是画URDF。不画清楚,后面导航肯定出幺蛾子。

核心要点:URDF定义“有什么部件”,TF树定义“部件之间怎么动”。两者结合,机器人才能知道自己手脚在哪。

2.1 URDF模型构建:从零搭一个差速底盘

我们先写一个最简单的两轮差速底盘。别怕,代码不长,但每个字段都有用。

<robot name="my_robot">
  <!-- 底盘 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.4 0.3 0.1"/>
      </geometry>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <material name="blue">
        <color rgba="0 0 1 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.4 0.3 0.1"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="2.0"/>
      <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.01"/>
    </inertial>
  </link>

  <!-- 左轮 -->
  <link name="left_wheel">
    <visual>
      <geometry>
        <cylinder radius="0.05" length="0.04"/>
      </geometry>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <material name="black"/>
    </visual>
  </link>

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

嗯,这里要注意几个坑:

  • collision 和 visual 可以不一样。我做过一个项目,为了避障安全,把碰撞模型故意放大了一圈。
  • inertial 必须写,否则Gazebo会报错。惯性矩阵不会算?用inertia_calculator.py工具生成。
  • joint type:轮子用continuous(连续旋转),激光雷达支架用fixed(固定)。

我的习惯:每个link的origin尽量从(0,0,0)开始,偏移量全放在joint里。这样调试时一眼就能看出父子关系。

2.2 xacro优化:别写重复代码

URDF写多了你会发现,左右轮子除了名字和位置,其他一模一样。复制粘贴?太low了。xacro就是干这个的——它像C语言的宏,帮你参数化、模块化。

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="my_robot">
  <xacro:property name="wheel_radius" value="0.05"/>
  <xacro:property name="wheel_length" value="0.04"/>
  <xacro:property name="base_width" value="0.3"/>

  <xacro:macro name="wheel" params="name side">
    <link name="${name}_wheel">
      <visual>
        <geometry>
          <cylinder radius="${wheel_radius}" length="${wheel_length}"/>
        </geometry>
      </visual>
    </link>
    <joint name="${name}_wheel_joint" type="continuous">
      <parent link="base_link"/>
      <child link="${name}_wheel"/>
      <origin xyz="-0.15 ${side * base_width/2} -0.05" rpy="0 0 0"/>
      <axis xyz="0 1 0"/>
    </joint>
  </xacro:macro>

  <xacro:wheel name="left" side="-1"/>
  <xacro:wheel name="right" side="1"/>
</robot>

看到没?${side * base_width/2} 自动算出左右轮的位置。我曾经维护过一个6轮机器人,用xacro后代码量从800行缩到150行。你想想看,少写多少bug?

避坑:xacro文件后缀必须是.xacro,但最终被robot_state_publisher解析时,需要先转换成URDF。命令:ros2 run xacro xacro robot.xacro > robot.urdf。我见过有人直接拿.xacro喂给robot_state_publisher,结果报错半天。

2.3 TF树:静态变换与动态变换

URDF定义好了,但导航系统怎么知道激光雷达装在底盘前方0.2米处?靠TF。

TF树本质上是一棵坐标系变换树。每个节点是一个坐标系(frame),每条边是一个变换(transform)。

TF树结构示例 base_link left_wheel right_wheel laser_frame odom 静态变换:base_link → laser_frame(固定偏移) 动态变换:base_link → left_wheel(随轮子旋转变化)

TF有两种:

  • 静态变换(static_transform_publisher):比如激光雷达相对于底盘的安装位置,固定不变。发布一次,永不过期。
  • 动态变换(dynamic_transform):比如轮子相对于底盘的旋转,每时每刻都在变。由joint_state_publisher或里程计节点发布。

关键区别:静态变换用ros2 run tf2_ros static_transform_publisher命令行发布;动态变换必须写节点,用tf2_ros::TransformBroadcaster实时广播。

2.4 robot_state_publisher配置:让URDF活起来

URDF写好了,TF树也懂了,怎么把它们跑起来?robot_state_publisher就是那个桥梁。

它读取URDF,监听关节状态(joint_states),然后自动发布所有link之间的TF变换。

# 启动 robot_state_publisher
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(cat robot.urdf)"

但更常见的做法是写在launch文件里:

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.substitutions import Command
import os

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

    return LaunchDescription([
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            parameters=[{
                'robot_description': Command(['xacro ', urdf_path])
            }]
        ),
        Node(
            package='joint_state_publisher_gui',
            executable='joint_state_publisher_gui'
        )
    ])

这里有个细节:Command(['xacro ', urdf_path]) 会在启动时自动把xacro转成URDF。省去了手动转换的步骤。

我的经验:调试阶段一定要开joint_state_publisher_gui,它提供一个滑块界面,可以手动转动每个关节。我曾经用它排查过一个轮子装反的问题——滑块一拉,发现左轮转了右轮没动,立马定位到joint名字写错了。

2.5 验证TF树:用工具说话

配置完了,怎么知道对不对?别靠猜,用工具:

工具 命令 作用
tf2_monitor ros2 run tf2_ros tf2_monitor 查看所有frame的发布频率、延迟
tf2_echo ros2 run tf2_ros tf2_echo base_link laser_frame 查看两个frame之间的实时变换
view_frames ros2 run tf2_tools view_frames 生成TF树的PDF图
rviz2 rviz2 -d display.rviz 可视化机器人模型和TF坐标轴

我曾经踩过的坑:有一次导航定位总跳变,查了半天,用tf2_monitor发现odom到base_link的变换延迟高达500ms。原因是里程计节点CPU占用过高,发布频率掉到了2Hz。所以,养成习惯:每次启动系统,先跑tf2_monitor看一眼。

2.6 本章小结

这一章我们干了三件事:

  • 用URDF搭了一个差速底盘模型,区分了visual、collision、inertial的作用。
  • 用xacro把重复代码抽象成宏,参数化轮子半径和底盘宽度。
  • 理解了TF树的静态变换和动态变换,配置了robot_state_publisher。