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有两种:
- 静态变换(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。