3、URDF机器人建模:从零搭建你的第一个机器人
各位同学,今天我们来聊聊URDF。这东西说白了就是机器人的「身份证」——它告诉Gazebo你的机器人长什么样、轮子装在哪、传感器怎么摆。
我记得刚接触ROS那会儿,总觉得URDF就是个简单的XML文件,随便写写就行。结果第一次在Gazebo里加载模型,轮子直接飞到了天上……嗯,从那以后我再也不敢小看它了。
3.1 URDF语法基础——别被XML吓到
URDF全称是Unified Robot Description Format。你把它想象成一份「机器人零件清单+装配图」就行。
核心就两个标签:<link> 和 <joint>。
- link:描述一个刚体零件。比如底盘、轮子、摄像头支架。
- joint:描述两个link之间的连接关系。比如轮子怎么装在底盘上。
我习惯把link比作「骨头」,joint比作「关节」。骨头不能动,关节决定怎么动。
重要概念:每个link必须有且只有一个inertial属性(质量+惯性矩阵),否则Gazebo会报错。我在项目里吃过这个亏——模型加载成功但一跑就崩,查了半天发现是惯性参数没写。
来看一个最简单的link示例:
<link name="base_link">
<visual>
<geometry>
<box size="0.4 0.3 0.1"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 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.01"/>
</inertial>
</link>
注意看,visual是外观,collision是碰撞体,inertial是物理属性。三者可以不一样——比如你可以用精细的3D模型做外观,用简单的长方体做碰撞检测,这样性能更好。
3.2 创建机器人底盘——从一块平板开始
底盘是机器人的基础。我个人习惯先画一个简单的长方体,把尺寸、颜色、质量定下来。
为什么先定质量?因为Gazebo的物理引擎需要质量来计算惯性。你想想看,一个2kg的底盘和20kg的底盘,运动特性完全不一样。
这里有个小技巧:惯性矩阵可以用公式估算。对于长方体,绕Z轴的惯性矩是 Izz = m * (width² + length²) / 12。我刚开始都是手算,后来发现ROS有个工具叫urdf_to_graphiz,能帮你检查模型结构。
我的经验:底盘的origin坐标尽量放在几何中心。这样后续加轮子、加传感器时,坐标变换会简单很多。我曾经把原点放在角落,结果算坐标算到怀疑人生。
底盘代码示例:
<link name="chassis">
<visual>
<geometry>
<box size="0.5 0.4 0.15"/>
</geometry>
<origin xyz="0 0 0" rpy="0 0 0"/>
<material name="silver">
<color rgba="0.75 0.75 0.75 1"/>
</material>
</visual>
<collision>
<geometry>
<box size="0.5 0.4 0.15"/>
</geometry>
</collision>
<inertial>
<mass value="3.0"/>
<inertia ixx="0.025" ixy="0" ixz="0"
iyy="0.04" iyz="0"
izz="0.04"/>
</inertial>
</link>
3.3 添加轮子和传感器——让机器人动起来
底盘有了,接下来装轮子。轮子一般用cylinder几何体,通过continuous类型的joint连接到底盘。
为什么是continuous?因为轮子要无限旋转。如果是舵机关节,就用revolute。
我曾经犯过一个低级错误:轮子的joint坐标写错了,导致轮子悬在半空。后来我学乖了——先在Rviz里可视化检查,再进Gazebo仿真。
<joint name="left_wheel_joint" type="continuous">
<parent link="chassis"/>
<child link="left_wheel"/>
<origin xyz="-0.2 0.25 -0.08" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<link name="left_wheel">
<visual>
<geometry>
<cylinder radius="0.08" length="0.04"/>
</geometry>
<material name="black">
<color rgba="0.1 0.1 0.1 1"/>
</material>
</visual>
<collision>
<geometry>
<cylinder radius="0.08" length="0.04"/>
</geometry>
</collision>
<inertial>
<mass value="0.2"/>
<inertia ixx="0.0001" ixy="0" ixz="0"
iyy="0.0001" iyz="0"
izz="0.0002"/>
</inertial>
</link>
传感器怎么加?比如加一个激光雷达,装在底盘正上方:
<joint name="laser_joint" type="fixed">
<parent link="chassis"/>
<child link="laser"/>
<origin xyz="0.15 0 0.1" rpy="0 0 0"/>
</joint>
<link name="laser">
<visual>
<geometry>
<cylinder radius="0.03" length="0.05"/>
</geometry>
<material name="red">
<color rgba="1 0 0 1"/>
</material>
</visual>
</link>
注意:传感器的collision可以省略,因为Gazebo里传感器通常不参与物理碰撞。但如果你省略了inertial,Gazebo会报warning。我建议给传感器也加上质量,哪怕只有0.01kg。
3.4 在Gazebo中加载URDF模型——见证奇迹的时刻
模型写好了,怎么加载到Gazebo?
有两种方式:
- 直接加载URDF文件:用
spawn_model命令 - 通过ROS launch文件:更规范,适合复杂项目
我个人推荐第二种。为什么?因为launch文件可以同时启动Gazebo、加载模型、启动控制器,一键搞定。
来看一个简单的launch文件:
<launch>
<!-- 启动Gazebo -->
<include file="$(find gazebo_ros)/launch/empty_world.launch">
<arg name="paused" value="false"/>
<arg name="use_sim_time" value="true"/>
</include>
<!-- 加载机器人模型 -->
<param name="robot_description"
textfile="$(find my_robot)/urdf/my_robot.urdf"/>
<node name="spawn_urdf"
pkg="gazebo_ros"
type="spawn_model"
args="-param robot_description
-urdf
-model my_robot
-x 0 -y 0 -z 0.1"/>
</launch>
这里有个细节:-z 0.1是为了让机器人稍微抬高地面,避免刚加载时陷入地面。我刚开始没加这个参数,结果机器人直接「遁地」了……
3.5 知识体系总览
下面这张图是我自己画的,帮你理清URDF建模的核心脉络:
3.6 避坑指南——我踩过的那些坑
最后分享几个实战中容易翻车的地方:
- 惯性矩阵不对:Gazebo会直接忽略你的模型,或者运动起来像果冻。建议用
urdf_to_graphiz检查。 - joint坐标写反:parent和child搞混,轮子会跑到底盘里面去。我建议画个草图再写代码。
- 忘记加
<gazebo>标签:有些传感器需要额外的gazebo插件才能工作,比如libgazebo_ros_laser.so。 - 模型加载后不动:检查一下
use_sim_time是否设为true,以及是否有控制器在驱动关节。
我的小习惯:每次修改URDF后,先用check_urdf命令检查语法,再用urdf_to_graphiz生成结构图。这两步能避免80%的低级错误。
好了,URDF建模就聊到这儿。记住一句话:模型是仿真的基础,基础不牢,地动山摇。动手写一个你自己的机器人模型吧,哪怕只是一个带轮子的盒子——相信我,当你看到它在Gazebo里动起来的那一刻,成就感爆棚。