3、机器人建模与URDF:从零构建一个带夹爪的移动操作臂URDF模型
URDF,全称是 Unified Robot Description Format。说白了,它就是机器人的「身份证」加「体检报告」。你要让 MoveIt 和 Nav2 认识你的机器人,就得先给它一张详细的「简历」——关节在哪、连杆多长、长什么样、能怎么动。
我记得刚入行那会儿,总觉得 URDF 就是个 XML 文件,随便写写就行。结果第一次仿真,机器人直接在地上「劈叉」,轮子飞出去两米远。嗯,从那以后我再也不敢小看这个文件了。
核心认知:URDF 不是画图工具,它是机器人运动学与碰撞检测的「骨架」。写得好,仿真跑得顺;写得烂,后面所有工作都是白搭。
3.1 移动操作臂的结构拆解
我们要做的,是一个「会跑的机械臂」。它由两部分组成:
- 移动底盘:两轮差速驱动,负责全地图移动
- 5自由度机械臂 + 夹爪:固定在底盘上,负责抓取操作
你想想看,这种结构在仓储物流、家庭服务里特别常见。我在一个 AGV 项目里就遇到过类似需求——底盘跑到货架前,机械臂伸出去取料箱。当时 URDF 写了整整三天,因为夹爪的坐标系没对齐,抓取位姿总是偏 5 毫米。
下面这张图,是我个人习惯的建模思路。先看整体,再拆细节:
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,让它们认识这个机器人。嗯,这部分内容,我们后面会详细展开。