2、机器人模型与传感器配置:URDF模型构建、传感器插件配置、TF坐标变换树、机器人状态发布

好,咱们正式开始搭建导航系统的第一步——让机器人“认识自己”。

说白了,导航就是让机器人知道“我在哪”、“要去哪”、“怎么去”。但在这之前,你得先告诉它:你长什么样?你的轮子在哪?你的激光雷达装在哪个位置?这些信息,全都写在URDF文件里。

本章核心脉络:URDF描述物理结构 → 传感器插件赋予感知能力 → TF树建立坐标关系 → robot_state_publisher让一切动起来。

URDF 模型构建 link / joint / 几何 传感器插件 Gazebo 仿真感知 TF 坐标变换树 base_link → odom → map 状态发布 robot_state_publisher 依赖关系 四步走:先有模型,再配感知,然后建立坐标,最后发布状态

2.1 URDF 模型构建——给机器人画个像

URDF,全称 Unified Robot Description Format。其实就是个XML文件,描述机器人的连杆(link)和关节(joint)。

我个人习惯把URDF分成三块来写:

  1. 基础底盘——车体、轮子、万向轮
  2. 传感器支架——激光雷达、IMU、摄像头的安装位置
  3. 外观与碰撞——可视化用的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无全局地图坐标系,固定不变
odommap里程计坐标系,随机器人移动
base_linkodom机器人本体中心
laser_linkbase_link激光雷达安装位置
imu_linkbase_linkIMU安装位置

在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树是否正确——这个习惯能帮你省下大量调试时间。