4、URDF实战:搭建一个两轮差速底盘机器人模型(含驱动轮与万向轮)

好,咱们直接动手。这一章我带你从零搭一个两轮差速底盘,带驱动轮和万向轮。你想想看,差速底盘几乎是移动机器人的“Hello World”,但真正把URDF写对、写规范,还是有不少细节的。我个人习惯先把整体结构理清楚,再一行一行写代码。

两轮差速底盘 URDF 结构 base_link left_wheel_link right_wheel_link caster_wheel_link left_wheel_joint right_wheel_joint caster_wheel_joint

4.1 底盘整体设计思路

差速底盘的核心就三个轮子:两个主动驱动轮,一个万向支撑轮。驱动轮负责前进、后退和转向,万向轮只跟着跑,不提供动力。嗯,这里要注意:万向轮在URDF里一般用连续关节(continuous)或者固定关节表示,但为了仿真更真实,我习惯用连续关节加一个小的旋转轴。

我记得第一次做差速底盘时,把万向轮做成了固定关节,结果仿真里一转弯就卡死。后来才明白,万向轮必须能自由旋转,否则运动学会出问题。

4.2 定义 base_link 与底盘尺寸

base_link 是所有轮子的父坐标系。我一般把底盘简化为一个扁长方体,方便碰撞检测。下面是我常用的参数:

部件几何形状尺寸 (m)质量 (kg)
底盘 (base_link)box0.4 × 0.3 × 0.12.0
驱动轮 (左右)cylinder半径0.06, 长度0.040.3
万向轮sphere半径0.030.1

你可能会问,为什么驱动轮用圆柱,万向轮用球体?说白了,球体在仿真里滚动最自然,而且碰撞计算简单。驱动轮用圆柱才能体现轮子宽度。

4.3 驱动轮关节配置

驱动轮必须用连续关节(continuous),这样才能无限旋转。左右轮分别命名为 left_wheel_joint 和 right_wheel_joint。我个人习惯把左轮放在 x 轴负方向,右轮放在正方向,y 轴对齐轮轴。

关键点: 驱动轮的 origin 要精确。左轮相对 base_link 的坐标一般是 (x: 0.0, y: -0.18, z: -0.05),右轮 (x: 0.0, y: 0.18, z: -0.05)。z 轴负值表示轮子在底盘下方。
<joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel_link"/>
    <origin xyz="0.0 -0.18 -0.05" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>  <!-- 绕 y 轴旋转 -->
</joint>

<joint name="right_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="right_wheel_link"/>
    <origin xyz="0.0 0.18 -0.05" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
</joint>

注意轴的方向。差速底盘两个轮子绕 y 轴旋转,但方向相反才能转弯。这个在后续的控制器里处理,URDF 只负责运动学描述。

4.4 万向轮关节与避坑

万向轮我推荐用 continuous 类型,并设置一个垂直的旋转轴(绕 z 轴)。这样它就能像真实万向轮一样自由摆动。我曾经踩过一个坑:把万向轮做成 fixed 关节,结果在 Gazebo 里一推就翻车。

避坑指南: 万向轮的 origin 一定要放在底盘底部中心偏后位置。比如 (x: -0.12, y: 0.0, z: -0.08)。如果放得太靠前,机器人急刹车时会前倾。
<joint name="caster_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="caster_wheel_link"/>
    <origin xyz="-0.12 0.0 -0.08" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>  <!-- 绕 z 轴自由旋转 -->
</joint>

万向轮本身是一个小球体,用 sphere 几何体。如果你希望更逼真,也可以用圆柱加一个小滚轮,但初学者用球体最省事。

4.5 完整 URDF 文件结构

下面我把整个底盘模型串起来。你直接复制就能用,但记得根据你的机器人实际尺寸微调 origin。

<robot name="two_wheel_diff_drive">
    <!-- 底盘 -->
    <link name="base_link">
        <visual>
            <geometry><box size="0.4 0.3 0.1"/></geometry>
            <material name="blue"><color rgba="0 0.4 0.8 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.02"/>
        </inertial>
    </link>

    <!-- 左驱动轮 -->
    <link name="left_wheel_link">
        <visual>
            <geometry><cylinder radius="0.06" length="0.04"/></geometry>
            <material name="black"><color rgba="0.1 0.1 0.1 1"/></material>
        </visual>
        <collision>
            <geometry><cylinder radius="0.06" length="0.04"/></geometry>
        </collision>
        <inertial>
            <mass value="0.3"/>
            <inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0002"/>
        </inertial>
    </link>

    <joint name="left_wheel_joint" type="continuous">
        <parent link="base_link"/>
        <child link="left_wheel_link"/>
        <origin xyz="0.0 -0.18 -0.05" rpy="0 0 0"/>
        <axis xyz="0 1 0"/>
    </joint>

    <!-- 右驱动轮 -->
    <link name="right_wheel_link">
        <visual>
            <geometry><cylinder radius="0.06" length="0.04"/></geometry>
            <material name="black"/>
        </visual>
        <collision>
            <geometry><cylinder radius="0.06" length="0.04"/></geometry>
        </collision>
        <inertial>
            <mass value="0.3"/>
            <inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0002"/>
        </inertial>
    </link>

    <joint name="right_wheel_joint" type="continuous">
        <parent link="base_link"/>
        <child link="right_wheel_link"/>
        <origin xyz="0.0 0.18 -0.05" rpy="0 0 0"/>
        <axis xyz="0 1 0"/>
    </joint>

    <!-- 万向轮 -->
    <link name="caster_wheel_link">
        <visual>
            <geometry><sphere radius="0.03"/></geometry>
            <material name="gray"><color rgba="0.5 0.5 0.5 1"/></material>
        </visual>
        <collision>
            <geometry><sphere radius="0.03"/></geometry>
        </collision>
        <inertial>
            <mass value="0.1"/>
            <inertia ixx="0.00001" ixy="0" ixz="0" iyy="0.00001" iyz="0" izz="0.00001"/>
        </inertial>
    </link>

    <joint name="caster_wheel_joint" type="continuous">
        <parent link="base_link"/>
        <child link="caster_wheel_link"/>
        <origin xyz="-0.12 0.0 -0.08" rpy="0 0 0"/>
        <axis xyz="0 0 1"/>
    </joint>
</robot>
小技巧: 用 check_urdf 命令验证你的模型。我每次写完都会跑一遍 check_urdf my_robot.urdf,它能快速发现缺失的惯性参数或关节循环。

4.6 可视化与调试

在终端运行 urdf_to_graphiz my_robot.urdf 可以生成结构图。我个人习惯用 rviz 实时查看,加上 robot_state_publisher,就能看到轮子是否在正确位置。

嗯,这里要注意:如果轮子显示穿透底盘,多半是 origin 的 z 值没算对。底盘厚度 0.1,轮子半径 0.06,所以轮子中心应该在 z = -0.05 左右(底盘下表面)。

实战心得: 差速底盘的 URDF 模型是后续导航、路径规划的基础。我见过很多团队在轮子坐标上差几厘米,导致里程计漂移严重。所以,测量和建模一定要精确。