第3章 URDF建模实战:两轮差速底盘 + 传感器 + xacro

这一章咱们直接动手。前面两章讲了 URDF 的标签和语法,说实话,光看理论容易犯困。我个人习惯是边写模型边理解,所以这一章我们拿一个真实的两轮差速底盘开刀,加上激光雷达和摄像头,最后用 xacro 把模型收拾得干干净净。

第3章 建模知识体系 两轮差速底盘 左轮 / 右轮 (连续) 传感器: 激光雷达+摄像头 xacro 参数化优化 最终输出: 可配置的机器人模型

3.1 底盘结构设计

两轮差速底盘,说白了就是两个主动轮 + 一个或两个万向轮。我最早做机器人时,以为底盘越复杂越好,后来发现差速驱动是最皮实、最直观的方案。你想想看,控制左右轮速度差就能转弯,连 PID 都省了一半力气。

我们先定义底盘主体(base_link),然后挂两个驱动轮(left_wheel_link, right_wheel_link),再加一个后部支撑轮(caster_link)。

核心思路: 所有轮子都通过 <joint> 连接到 base_link,驱动轮用 continuous 类型(可无限旋转),支撑轮用 fixed 或 continuous 都行,但一般用 fixed 加一个虚拟旋转。
<!-- 底盘主体 -->
<link name="base_link">
  <visual>
    <geometry><box size="0.4 0.3 0.12"/></geometry>
    <material name="blue"><color rgba="0.2 0.4 0.8 1.0"/></material>
  </visual>
  <collision>
    <geometry><box size="0.4 0.3 0.12"/></geometry>
  </collision>
</link>

嗯,这里要注意:碰撞模型尽量和视觉模型一致,否则 Gazebo 或者 MoveIt 会“撞空气”。我遇到过好几次,视觉上看着好好的,一仿真轮子直接陷到地里……后来发现碰撞 box 比视觉小了一圈。

3.2 驱动轮与支撑轮

左右轮分别偏移 y 轴 ±0.2 m,使用 continuous 关节。支撑轮放在后方,用 fixed 关节,但为了滚动效果,可以加一个虚拟旋转关节——不过初学者先固定住也行。

<joint name="left_wheel_joint" type="continuous">
  <parent link="base_link"/>
  <child link="left_wheel_link"/>
  <origin xyz="-0.1 0.22 -0.06" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
</joint>

<link name="left_wheel_link">
  <visual>
    <geometry><cylinder radius="0.05" length="0.04"/></geometry>
    <material name="black"/>
  </visual>
</link>

右轮类似,把 y 改成 -0.22 就行。支撑轮我一般用一个球体,简单省事。

我个人习惯把轮子的半径和宽度定义成常量,后面用 xacro 统一改。比如半径 0.05 m,宽度 0.04 m,这样调参数时不用满文件找。

3.3 添加激光雷达

激光雷达我习惯放在底盘前上方,这样视野好。用固定关节把它固定在 base_link 上,z 轴抬高 0.15 m,x 轴前移 0.05 m。

<link name="laser_link">
  <visual>
    <geometry><cylinder radius="0.03" length="0.04"/></geometry>
    <material name="red"/>
  </visual>
</link>

<joint name="laser_joint" type="fixed">
  <parent link="base_link"/>
  <child link="laser_link"/>
  <origin xyz="0.12 0.0 0.15" rpy="0 0 0"/>
</joint>

你可能会问:为什么不用旋转关节?因为激光雷达一般自己带旋转,我们只提供固定安装座就行。在 MoveIt 里,它就是一个感知数据源,不需要运动学。

3.4 添加摄像头

摄像头我习惯装在底盘前部,稍微俯视地面。用 fixed 关节,加一个俯仰角 pitch 约 0.2 rad(大概 11°)。

<link name="camera_link">
  <visual>
    <geometry><box size="0.03 0.05 0.02"/></geometry>
    <material name="dark_gray"/>
  </visual>
</link>

<joint name="camera_joint" type="fixed">
  <parent link="base_link"/>
  <child link="camera_link"/>
  <origin xyz="0.18 0.0 0.08" rpy="0 0.2 0"/>
</joint>
我曾经把摄像头装在底盘正中间,结果被轮子挡住一半视野。后来才意识到,摄像头一定要尽量靠前、靠上,避免车体自身遮挡。尤其是差速底盘转弯时,车头会摆动,遮挡更明显。

3.5 用 xacro 优化模型

写到这里,你会发现很多数字重复出现。比如轮子半径、底盘尺寸、关节偏移。如果每个文件都手写,改一个参数得改七八个地方。xacro 就是干这个的。

我们把常用参数定义成 xacro 属性:

<?xml version="1.0"?>
<robot name="diff_drive_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <xacro:property name="wheel_radius" value="0.05"/>
  <xacro:property name="wheel_width" value="0.04"/>
  <xacro:property name="base_length" value="0.4"/>
  <xacro:property name="base_width" value="0.3"/>
  <xacro:property name="base_height" value="0.12"/>

  <!-- 然后所有 link/joint 里用 ${wheel_radius} 代替 -->
  <link name="left_wheel_link">
    <visual>
      <geometry><cylinder radius="${wheel_radius}" length="${wheel_width}"/></geometry>
    </visual>
  </link>
</robot>

这样改起来就舒服多了。我一般还会把传感器安装位置也参数化,比如 laser_z_offset、camera_pitch,方便不同车型复用。

xacro 宏(macro) 是更高级的用法。比如你可以写一个“轮子宏”,传入半径、宽度、颜色,自动生成 link 和 joint。我团队里现在所有机器人模型都用宏,一个底盘模型从 200 行缩到 40 行。

3.6 完整模型结构一览

部件link 名称关节类型父 link备注
底盘主体base_link——根 link
左驱动轮left_wheel_linkcontinuousbase_link绕 y 轴旋转
右驱动轮right_wheel_linkcontinuousbase_link绕 y 轴旋转
支撑轮caster_linkfixedbase_link球体,被动
激光雷达laser_linkfixedbase_link前上方
摄像头camera_linkfixedbase_link前部,俯仰角 0.2 rad

有了这个表格,你基本可以照着搭积木。我个人建议先写纯 URDF 验证每个关节,再迁移到 xacro。别一上来就上宏,容易 debug 到怀疑人生。

如果你用 VS Code,可以装 URDF 预览插件,边写边看。我经常开着 rviz 实时刷新,比肉眼检查快多了。

最后,把整个模型保存为 diff_drive.xacro,用 roslaunch urdf_tutorial display.launch model:=diff_drive.xacro 就能看到。嗯,看到轮子转起来的那一刻,还是挺有成就感的。