第二讲:机器人建模与URDF——从零搭建你的第一个机器人
大家好,欢迎来到第二讲。
上一讲我们搭好了环境,这一讲我们来干点实在的——给机器人建模。说白了,就是告诉计算机:你的机器人长什么样?有几个轮子?传感器装在哪?
我刚开始学ROS的时候,觉得URDF就是个XML文件,随便写写就行。后来在Gazebo里跑仿真,机器人直接翻了个跟头……嗯,从那以后我再也不敢小看这个文件了。
URDF是什么?为什么非学不可?
URDF,全称Unified Robot Description Format,统一机器人描述格式。它是ROS里描述机器人几何、运动学、传感器布局的标准语言。
你可以把它想象成机器人的「身份证」+「骨架图」。没有它,Nav2不知道你的机器人多大、激光雷达装在哪、轮子怎么转。说白了,URDF是导航一切计算的基础。
核心要点:URDF只描述刚性连杆和旋转/平移关节。如果你要做柔性机器人(比如机械臂带弹簧),URDF搞不定,得用其他格式。但做轮式移动机器人,URDF绰绰有余。
连杆与关节:机器人的骨头和关节
URDF里两个最核心的概念:link(连杆)和joint(关节)。
- link:机器人的刚体部件。比如底盘、轮子、激光雷达支架。每个link有几何形状、尺寸、颜色、惯性参数。
- joint:连接两个link的约束。比如轮子绕轴旋转(revolute)、底盘和雷达之间的固定连接(fixed)。
我个人的习惯是:先画一张草图,标出所有link和joint,再写代码。你想想看,如果连机器人有几个零件都没想清楚,直接写URDF,大概率要返工。
一个最简单的URDF例子
下面这个例子描述了一个底盘加两个轮子的差速驱动机器人:
<robot name="my_robot">
<!-- 底盘 -->
<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>
<!-- 左轮 -->
<link name="left_wheel">
<visual>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
</link>
<!-- 左轮到底盘的关节 -->
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="-0.15 -0.2 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
</robot>
避坑指南:我曾经在写joint的origin时,把xyz和rpy搞反了,结果轮子悬在半空中。记住:xyz是位置偏移,rpy是旋转角度(弧度制)。
添加传感器:给机器人装上眼睛和耳朵
导航需要感知环境。最常用的传感器:激光雷达、IMU、里程计。在URDF里,传感器也是link,只不过我们给它加上一些特殊的标签。
激光雷达
激光雷达通常用一个圆柱体表示,装在底盘上方。我习惯把它放在一个单独的link里,通过固定关节连接到底盘。这样方便调整高度。
<link name="laser_frame">
<visual>
<geometry>
<cylinder radius="0.03" length="0.05"/>
</geometry>
<material name="red">
<color rgba="1 0 0 1"/>
</material>
</visual>
</link>
<joint name="laser_joint" type="fixed">
<parent link="base_link"/>
<child link="laser_frame"/>
<origin xyz="0 0 0.15" rpy="0 0 0"/>
</joint>
IMU
IMU通常放在机器人质心附近。注意:IMU的坐标系约定是X向前、Y向左、Z向上(ROS标准)。
<link name="imu_link">
<visual>
<geometry>
<box size="0.02 0.02 0.02"/>
</geometry>
</visual>
</link>
<joint name="imu_joint" type="fixed">
<parent link="base_link"/>
<child link="imu_link"/>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
</joint>
里程计
里程计通常不单独建模,而是通过轮子编码器推算。但我们需要一个虚拟的里程计坐标系(odom),这个在后续的TF树里定义。URDF里只需要保证轮子关节正确即可。
注意:IMU和激光雷达的坐标系必须与真实硬件一致。我见过有人把IMU的Z轴朝下装,结果融合出来的姿态全是反的。检查坐标系!检查坐标系!检查坐标系!
在Gazebo中加载机器人模型
URDF写好了,怎么在Gazebo里看到它?你需要一个launch文件,把URDF加载到参数服务器,然后启动Gazebo。
我常用的做法是:
- 把URDF文件放在
urdf/目录下 - 写一个launch文件,用
robot_state_publisher发布TF - 用
spawn_entity把机器人放到Gazebo里
<launch>
<!-- 加载URDF到参数服务器 -->
<param name="robot_description"
command="$(find xacro) $(find my_robot)/urdf/my_robot.urdf"/>
<!-- 启动机器人状态发布器 -->
<node name="robot_state_publisher"
pkg="robot_state_publisher"
type="robot_state_publisher"/>
<!-- 启动Gazebo -->
<include file="$(find gazebo_ros)/launch/empty_world.launch">
<arg name="paused" value="false"/>
<arg name="use_sim_time" value="true"/>
</include>
<!-- 在Gazebo中生成机器人 -->
<node name="spawn_urdf"
pkg="gazebo_ros"
type="spawn_model"
args="-param robot_description -urdf -model my_robot"/>
</launch>
小技巧:我建议用xacro代替纯URDF。xacro支持宏定义、数学运算、条件判断。比如轮子参数可以定义成变量,改尺寸时只改一处就行。后面课程我们会专门讲xacro。
本章知识体系
下面这张图帮你理清URDF建模的核心逻辑:
常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Gazebo里机器人沉入地面 | collision几何与visual不一致,或惯性参数缺失 | 确保每个link都有collision和inertial标签 |
| 激光雷达扫描数据全为零 | 雷达坐标系与Gazebo中传感器插件不匹配 | 检查<origin>的rpy是否正确 |
| 轮子不转 | joint类型错误或未添加传动 | 差速轮用continuous类型,并添加<transmission> |
| TF树报错"no transform from ..." | URDF中link名称拼写错误 | 用tf2_tools view_frames检查TF树 |
重要提醒:URDF里的单位是米和千克。我见过有人把尺寸写成厘米,结果Gazebo里出现一个巨无霸机器人。所有长度单位用米,质量单位用千克。
好了,这一讲的内容就是这些。URDF是机器人建模的基石,看似简单,但细节很多。我建议你亲自动手写一个完整的URDF,从底盘到传感器,再到Gazebo里跑起来。遇到问题很正常,对照上面的表格排查就行。
下一讲我们会深入xacro,让URDF变得更优雅、更可维护。到时候见。