2、机器人模型与传感器配置:URDF模型构建、传感器插件配置、TF坐标变换树、机器人状态发布
好,咱们正式开始搭建导航系统的第一步——让机器人“认识自己”。
说白了,导航就是让机器人知道“我在哪”、“要去哪”、“怎么去”。但在这之前,你得先告诉它:你长什么样?你的轮子在哪?你的激光雷达装在哪个位置?这些信息,全都写在URDF文件里。
本章核心脉络:URDF描述物理结构 → 传感器插件赋予感知能力 → TF树建立坐标关系 → robot_state_publisher让一切动起来。
2.1 URDF 模型构建——给机器人画个像
URDF,全称 Unified Robot Description Format。其实就是个XML文件,描述机器人的连杆(link)和关节(joint)。
我个人习惯把URDF分成三块来写:
- 基础底盘——车体、轮子、万向轮
- 传感器支架——激光雷达、IMU、摄像头的安装位置
- 外观与碰撞——可视化用的mesh,碰撞检测用的简化几何体
来看一个最简单的差速底盘例子:
<robot name="my_robot">
<!-- 底盘 -->
<link name="base_link">
<visual>
<geometry>
<box size="0.4 0.3 0.1"/>
</geometry>
<material name="blue"/>
</visual>
<collision>
<geometry>
<box size="0.4 0.3 0.1"/>
</geometry>
</collision>
</link>
<!-- 左轮 -->
<link name="left_wheel">
<visual>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
</visual>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="-0.15 0.15 0" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
</joint>
</robot>
💡 我的经验:写URDF时,origin的xyz值一定要和实际机械尺寸对得上。我曾经因为把轮子y轴偏移写成了0.1,结果机器人跑起来像个瘸子——一边轮子悬空。查了半小时才发现是小数点写错了。
2.2 传感器插件配置——让机器人有感知
模型建好了,但机器人还是“瞎子”。在Gazebo仿真里,我们需要给传感器添加插件(plugin)。
说白了,插件就是一段C++代码,编译成动态库,URDF里引用一下,Gazebo就会自动加载它,模拟真实传感器的数据输出。
最常见的激光雷达插件配置:
<gazebo reference="laser_link">
<sensor type="ray" name="head_laser">
<pose>0 0 0 0 0 0</pose>
<visualize>true</visualize>
<update_rate>10</update_rate>
<ray>
<scan>
<horizontal>
<samples>360</samples>
<resolution>1</resolution>
<min_angle>-3.14159</min_angle>
<max_angle>3.14159</max_angle>
</horizontal>
</scan>
<range>
<min>0.10</min>
<max>12.0</max>
<resolution>0.01</resolution>
</range>
</ray>
<plugin name="laser_controller" filename="libgazebo_ros_ray_sensor.so">
<output_type>sensor_msgs/LaserScan</output_type>
<topic_name>/scan</topic_name>
</plugin>
</sensor>
</gazebo>
⚠️ 注意:插件的filename路径取决于你安装的Gazebo版本。ROS2 Humble用的是Gazebo Fortress,插件名是libignition-gazebo-sensors-system.so。如果你用的是旧版教程,直接复制可能会报错。
2.3 TF 坐标变换树——机器人的“空间坐标系”
嗯,这里要重点说一下。TF(Transform)是ROS2里最核心的机制之一。它维护了机器人各个部件之间的坐标关系。
你想想看,激光雷达扫描到前方1米有个障碍物——这个“前方1米”是相对于激光雷达自己的坐标系。但导航模块需要知道这个障碍物相对于机器人底盘的位置,甚至相对于地图的位置。TF就是干这个的。
一个典型的导航TF树长这样:
| 坐标系 | 父坐标系 | 描述 |
|---|---|---|
| map | 无 | 全局地图坐标系,固定不变 |
| odom | map | 里程计坐标系,随机器人移动 |
| base_link | odom | 机器人本体中心 |
| laser_link | base_link | 激光雷达安装位置 |
| imu_link | base_link | IMU安装位置 |
在URDF里,我们通过
<joint name="laser_joint" type="fixed">
<parent link="base_link"/>
<child link="laser_link"/>
<origin xyz="0.1 0 0.15" rpy="0 0 0"/>
</joint>
这表示激光雷达安装在底盘前方0.1米,高度0.15米的位置。
关键点:导航系统要求map → odom → base_link这条链路必须完整。如果中间断了,AMCL和Nav2都会报"Could not find transform"错误。我调试时遇到最多的问题就是这个。
2.4 机器人状态发布——让TF树“活”起来
URDF写好了,TF关系也定义了,但谁来把这些信息发布到ROS2系统里?
答案是robot_state_publisher。这个节点读取你的URDF文件,解析所有的link和joint,然后自动发布:
/tf_static—— 固定关节的变换(比如激光雷达到底盘)/tf—— 运动关节的变换(比如轮子转动)/robot_description—— 完整的URDF模型字符串
启动方式很简单:
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(cat my_robot.urdf)"
或者用launch文件:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{'robot_description': open('my_robot.urdf').read()}]
)
])
💡 调试小技巧:启动后,用ros2 topic echo /tf_static看看有没有数据。再用rviz2添加TF显示,如果能看到完整的坐标轴树,说明一切正常。如果看不到,八成是URDF里某个joint的parent或child写错了。
2.5 完整示例:一个带激光雷达的差速底盘
把上面所有内容整合起来,一个完整的URDF文件大概长这样:
<robot name="nav_robot">
<!-- 底盘 -->
<link name="base_link">
<visual>
<geometry><box size="0.4 0.3 0.08"/></geometry>
<material name="blue"/>
</visual>
</link>
<!-- 左右驱动轮 -->
<link name="left_wheel"/>
<link name="right_wheel"/>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/> <child link="left_wheel"/>
<origin xyz="-0.15 0.18 0"/> <axis xyz="0 0 1"/>
</joint>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/> <child link="right_wheel"/>
<origin xyz="-0.15 -0.18 0"/> <axis xyz="0 0 1"/>
</joint>
<!-- 激光雷达 -->
<link name="laser_link">
<visual>
<geometry><cylinder radius="0.03" length="0.05"/></geometry>
</visual>
</link>
<joint name="laser_joint" type="fixed">
<parent link="base_link"/> <child link="laser_link"/>
<origin xyz="0.15 0 0.1"/>
</joint>
<!-- 万向轮 -->
<link name="caster_wheel"/>
<joint name="caster_joint" type="fixed">
<parent link="base_link"/> <child link="caster_wheel"/>
<origin xyz="0.2 0 -0.04"/>
</joint>
<!-- Gazebo传感器插件 -->
<gazebo reference="laser_link">
<sensor type="ray" name="lidar">
<update_rate>10</update_rate>
<ray>
<scan><horizontal samples="360"/></scan>
<range><min>0.1</min><max>12.0</max></range>
</ray>
<plugin name="lidar_plugin" filename="libgazebo_ros_ray_sensor.so">
<topic_name>/scan</topic_name>
</plugin>
</sensor>
</gazebo>
</robot>
启动后,你会看到:
/tf_static里发布base_link → laser_link的固定变换/tf里发布odom → base_link的里程计变换(由其他节点提供)/scan话题上源源不断的激光数据
⚠️ 避坑指南:我曾经在项目里把laser_link的父坐标系写成了odom,结果导航模块一直报错。记住:传感器坐标系必须是base_link的子坐标系,而不是odom或map的子坐标系。odom和map是导航模块自己维护的,你只需要提供base_link以下的静态TF即可。
好了,到这里,你的机器人已经有了“身体”、“感官”和“空间意识”。下一章我们会让这个模型真正跑起来,开始处理传感器数据。但在此之前,我建议你亲手写一遍URDF,在rviz2里看看TF树是否正确——这个习惯能帮你省下大量调试时间。