3、机器人建模与URDF:从零构建一个带夹爪的移动操作臂URDF模型

URDF,全称是 Unified Robot Description Format。说白了,它就是机器人的「身份证」加「体检报告」。你要让 MoveIt 和 Nav2 认识你的机器人,就得先给它一张详细的「简历」——关节在哪、连杆多长、长什么样、能怎么动。

我记得刚入行那会儿,总觉得 URDF 就是个 XML 文件,随便写写就行。结果第一次仿真,机器人直接在地上「劈叉」,轮子飞出去两米远。嗯,从那以后我再也不敢小看这个文件了。

核心认知:URDF 不是画图工具,它是机器人运动学与碰撞检测的「骨架」。写得好,仿真跑得顺;写得烂,后面所有工作都是白搭。

3.1 移动操作臂的结构拆解

我们要做的,是一个「会跑的机械臂」。它由两部分组成:

  • 移动底盘:两轮差速驱动,负责全地图移动
  • 5自由度机械臂 + 夹爪:固定在底盘上,负责抓取操作

你想想看,这种结构在仓储物流、家庭服务里特别常见。我在一个 AGV 项目里就遇到过类似需求——底盘跑到货架前,机械臂伸出去取料箱。当时 URDF 写了整整三天,因为夹爪的坐标系没对齐,抓取位姿总是偏 5 毫米。

下面这张图,是我个人习惯的建模思路。先看整体,再拆细节:

移动操作臂 URDF 建模流程 移动操作臂 URDF 建模流程 移动操作臂 URDF 移动底盘(差速轮) 5自由度机械臂 夹爪(2指) 底盘细节 base_link → base_footprint 左轮/右轮/支撑轮 机械臂细节 arm_base → shoulder → elbow wrist_1 → wrist_2 → wrist_3 夹爪细节 gripper_base → left_finger gripper_base → right_finger 坐标系层级:base_link → arm_base → 各关节 → gripper_base → finger

3.2 从 base_link 开始:底盘建模

所有 URDF 的起点,都是 base_link。它是整个机器人的「世界原点」。我个人习惯把 base_link 放在底盘几何中心的地面投影点上。

先看一个最简底盘 URDF:

<robot name="mobile_manipulator">
  <!-- 底盘本体 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.5 0.4 0.15"/>
      </geometry>
      <origin xyz="0 0 0.075" rpy="0 0 0"/>
      <material name="blue">
        <color rgba="0.2 0.4 0.8 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.5 0.4 0.15"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="10.0"/>
      <inertia ixx="0.1" ixy="0" ixz="0" iyy="0.1" iyz="0" izz="0.1"/>
    </inertial>
  </link>
</robot>

我的习惯:visual 和 collision 用完全相同的几何体,除非你明确要做「简化碰撞模型」。否则 Gazebo 仿真时会出现「看得见但撞不到」的诡异情况。我踩过这个坑,调试了整整一个下午。

3.3 轮子与关节:让底盘动起来

底盘要动,就得有轮子。每个轮子是一个独立的 link,通过 continuous 关节连接到 base_link。

两轮差速底盘,至少需要:

  • 左轮:left_wheel_link,绕 Y 轴旋转
  • 右轮:right_wheel_link,绕 Y 轴旋转
  • 支撑轮:caster_wheel_link,固定或被动旋转

关节定义示例:

<joint name="left_wheel_joint" type="continuous">
  <parent link="base_link"/>
  <child link="left_wheel_link"/>
  <origin xyz="-0.15 0.25 0.05" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
  <limit effort="10" velocity="5"/>
</joint>

这里要注意 origin 的坐标。轮子相对于 base_link 的位置,决定了机器人的「轮距」和「轴距」。我在一个项目中把轮距写小了 2 厘米,结果仿真时底盘转弯半径完全不对,导航路径规划一直报错。

避坑指南:我曾经把左右轮的 origin 写反了符号,结果左轮在右边、右轮在左边。仿真一启动,机器人原地转圈。检查 URDF 时一定要用 check_urdf 工具,再用 urdf_to_graphiz 看坐标系树。

3.4 机械臂:5个关节的串联链

机械臂的 URDF 核心是「关节-连杆」链。每个关节定义运动类型和父子关系,每个连杆定义几何和惯性。

我常用的 5 自由度臂结构:

关节名称 类型 父 link 子 link 旋转轴
shoulder_pan_joint revolute arm_base_link shoulder_link Z
shoulder_lift_joint revolute shoulder_link upper_arm_link Y
elbow_joint revolute upper_arm_link forearm_link Y
wrist_flex_joint revolute forearm_link wrist_link Y
wrist_rot_joint revolute wrist_link flange_link Z

每个关节都要加 <limit> 标签,定义角度范围。MoveIt 的规划器依赖这个范围做碰撞检测。我见过有人忘了写 limit,结果规划出来的轨迹让机械臂「反关节」运动,直接穿模。

<joint name="shoulder_pan_joint" type="revolute">
  <parent link="arm_base_link"/>
  <child link="shoulder_link"/>
  <origin xyz="0 0 0.2" rpy="0 0 0"/>
  <axis xyz="0 0 1"/>
  <limit lower="-3.14" upper="3.14" effort="20" velocity="1.5"/>
</joint>

3.5 夹爪:两指平行开合

夹爪的 URDF 稍微特殊一点。它有两个手指,通过 prismatic 关节实现平移运动。

结构如下:

  • gripper_base_link:固定在 flange_link 上
  • left_finger_link:沿 X 轴正方向平移
  • right_finger_link:沿 X 轴负方向平移

注意:左右手指的 axis 方向相反,才能实现对称开合。

<joint name="left_finger_joint" type="prismatic">
  <parent link="gripper_base_link"/>
  <child link="left_finger_link"/>
  <origin xyz="0.03 0 0" rpy="0 0 0"/>
  <axis xyz="1 0 0"/>
  <limit lower="0" upper="0.04" effort="5" velocity="0.2"/>
</joint>

<joint name="right_finger_joint" type="prismatic">
  <parent link="gripper_base_link"/>
  <child link="right_finger_link"/>
  <origin xyz="-0.03 0 0" rpy="0 0 0"/>
  <axis xyz="-1 0 0"/>
  <limit lower="0" upper="0.04" effort="5" velocity="0.2"/>
</joint>

小技巧:夹爪的 collision 几何体可以比 visual 稍微小一圈。这样在抓取仿真时,手指能「插进」物体一点点,避免因为碰撞检测过于严格导致抓取失败。我在一个物流分拣项目里试过,成功率从 60% 提升到了 85%。

3.6 坐标系树:用工具验证

URDF 写完后,千万别直接扔进 MoveIt。先用命令行工具检查:

# 检查语法和结构
check_urdf my_robot.urdf

# 生成坐标系树图
urdf_to_graphiz my_robot.urdf

# 查看生成的 PDF
evince my_robot.pdf

坐标系树应该是一个「有向无环图」。根节点是 base_link,所有其他 link 都能通过关节追溯到它。如果出现「孤儿 link」或者「循环关节」,MoveIt 会直接崩溃。

我记得有一次,我把 gripper_base_link 的父节点写成了 wrist_link 而不是 flange_link,结果坐标系树里多了一个分支。MoveIt 加载时提示「multiple roots」,排查了半小时才发现。

3.7 完整 URDF 文件结构

把上面所有部分拼起来,一个完整的移动操作臂 URDF 大概长这样:

<robot name="mobile_manipulator">
  <!-- 底盘 -->
  <link name="base_link">...</link>
  <link name="left_wheel_link">...</link>
  <link name="right_wheel_link">...</link>
  <link name="caster_wheel_link">...</link>
  
  <joint name="left_wheel_joint" type="continuous">...</joint>
  <joint name="right_wheel_joint" type="continuous">...</joint>
  <joint name="caster_wheel_joint" type="fixed">...</joint>
  
  <!-- 机械臂基座 -->
  <link name="arm_base_link">...</link>
  <joint name="arm_base_joint" type="fixed">
    <parent link="base_link"/>
    <child link="arm_base_link"/>
    <origin xyz="0.1 0 0.2" rpy="0 0 0"/>
  </joint>
  
  <!-- 机械臂5个关节 -->
  <link name="shoulder_link">...</link>
  <joint name="shoulder_pan_joint" type="revolute">...</joint>
  <!-- ... 省略中间关节 ... -->
  <link name="flange_link">...</link>
  <joint name="wrist_rot_joint" type="revolute">...</joint>
  
  <!-- 夹爪 -->
  <link name="gripper_base_link">...</link>
  <joint name="gripper_base_joint" type="fixed">
    <parent link="flange_link"/>
    <child link="gripper_base_link"/>
    <origin xyz="0 0 0.05" rpy="0 0 0"/>
  </joint>
  
  <link name="left_finger_link">...</link>
  <link name="right_finger_link">...</link>
  <joint name="left_finger_joint" type="prismatic">...</joint>
  <joint name="right_finger_joint" type="prismatic">...</joint>
</robot>

关键检查点:

  • 每个 link 必须有 inertial 标签,否则 Gazebo 会报错
  • 每个关节的 origin 坐标要精确到毫米级,差 1 厘米可能影响抓取
  • 所有 revolute 关节必须有 limit,否则 MoveIt 规划器无法工作
  • 夹爪的 prismatic 关节 lower 必须为 0,否则手指会「穿模」

URDF 写到这里,你已经有了一个完整的移动操作臂模型。下一步就是把它喂给 MoveIt 和 Nav2,让它们认识这个机器人。嗯,这部分内容,我们后面会详细展开。