接手ROS2项目之后,你迟早会碰到一个问题:要给机器人写描述文件。可能是要给机械臂做运动规划,可能是要在仿真里跑轮式机器人,还可能是刚装完ROS2,想把手上的CAD模型导入到rviz2里看一看。我处理过不少这类需求,踩过不少坑,发现很多人对URDF的理解停留在“就是一个XML文件”这个层面,结果一写就错,一仿真就飘。这次我打算把URDF从入门到实战的路径完整走一遍,讲清楚建模的脑回路、每段XML的含义、怎么让关节动起来、怎么从SolidWorks把模型转出来,最后把常见的坑全部过一遍。适合正在学ROS2导航、机械臂控制、仿真集成,或者刚把SolidWorks模型导出来准备接入机器人生态的读者。
1. URDF解决的是什么问题——先搞清楚模型在机器人系统里的位置
1.1 一台没有模型的ROS2机器人有多“抓瞎”
很多朋友问我URDF到底有什么用,我都会反问一句:你觉得MoveIt2、Nav2、Gazebo、rviz2是怎么知道你的机器人有两只手臂、四个轮子、摄像头装在哪个位置的?答案是它们都不知道,它们只认URDF。URDF的全称是Universal Robot Description Format,翻译过来就是“通用机器人描述格式”,说白了,这是ROS2生态里各个工具共同约定的“机器人身份证”。
没有URDF的ROS2机器人,你可以正常启动一堆节点,但MoveIt2不知道你的机械臂有多少个关节,Gazebo不知道你车轮的碰撞体积,rviz2只能看到一个孤零零的坐标系原点。导航、运动规划、物理仿真,几乎都无从谈起。很多初学者在跑通小海龟之后,接着想干点正事,第一道坎就是这里:我手上明明有个真实机器人,怎么让它在ROS2里被“看见”?答案就是先写URDF。
1.2 URDF在机器人系统里到底管哪一段
URDF在ROS2里主要承担两件事。第一件事是几何描述,把机器人拆成“连杆+关节”的树状结构,每个连杆有形状、尺寸、材质,每个关节有类型、位置、旋转轴。第二件事是为上层算法提供接口:robot_state_publisher监听URDF和关节状态,算出每个坐标系在世界坐标系里的位姿,然后rviz2、MoveIt2、Gazebo才能共用这一套数据。
所以URDF决定的是“机器人长什么样、关节怎么活动”,至于它怎么动、怎么规划、怎么避障,那是上游算法的事。这个边界先搞清楚了,你才知道写URDF的时候哪些细节必须抠到毫米级,哪些可以先拿默认值凑合。比如你在Gazebo里做物理仿真,collision和inertial就绝对不能省;如果只是想在rviz2里看个样子调试算法,那collision甚至可以先用一个粗糙的box代替。
1.3 link与joint:把机器人的“骨头”和“关节”分开看待
URDF的核心概念只有两个:link(连杆)和joint(关节)。把机器人想象成人体:前臂是一根link,上臂是一根link,连接它们的肘关节就是一个joint。每个joint连接一个parent link和一个child link,整个机器人就是一棵树,根节点通常叫base_link或者base_footprint。
link负责描述“这一段机构长什么样”,内部可以分成visual(可视化网格)、collision(碰撞网格)、inertial(惯性参数)三块。joint则描述“两个link之间怎么相对运动”,类型有fixed、revolute、prismatic、continuous、floating、planar六种。理解这层关系之后,再去翻别人的URDF文件就不会一脸懵了——满屏的标签,本质上就是在描述两件事:哪几段是骨头,它们之间用什么关节相连。
2. URDF核心语法解析:link、joint与坐标系,第一关必须过关
2.1 写link最容易漏掉的三块内容
写link是最容易让新人在头大的部分,因为标签多、缩进多,稍不留神就少了个闭合标签。一个完整的link实际上由三部分构成:visual、collision、inertial。visual负责“长什么样”,collision负责“碰撞体积”,inertial负责“质量分布和转动惯量”。
我最常看到的问题,是有人只写了visual,缺了collision和inertial。在rviz2里看起来完全没问题,一上Gazebo就各种崩溃,因为物理引擎找不到碰撞体,也计算不了惯性。给你一个标准的最小link写法:
<link name="base_link"> <visual> <geometry> <box size="0.2 0.1 0.05"/> </geometry> <origin xyz="0 0 0" rpy="0 0 0"/> <material name="base_mat"/> </visual> <collision> <geometry> <box size="0.2 0.1 0.05"/> </geometry> </collision> <inertial> <mass value="0.5"/> <inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/> </inertial> </link>visual和collision里的geometry支持box、cylinder、sphere、mesh四种类型。visual里的material可以用颜色,也可以引用DAE材质的网格文件;collision一般直接用简化几何体,目的是减少碰撞检测的计算量,没人会用几万个三角面的外壳去做物理碰撞。
2.2 joint的六种类型和你实际会用到哪几种
joint决定两个link之间的运动关系,以下这个表可以帮你快速判断该用哪种类型:
| joint类型 | 自由度 | 典型场景 | 注意事项 |
|---|---|---|---|
| fixed | 0 | 摄像头支架、焊接件、末端工具 | 最不容易出错,先拿它练手 |
| revolute | 1个旋转 | 舵机驱动的机械臂关节 | 必须写limit,否则MoveIt2和Gazebo都拒绝加载 |
| continuous | 1个连续旋转 | 轮式机器人车轮、电机直驱关节 | 不需要limit,轮子可以一直转 |
| prismatic | 1个平移 | 升降台、导轨、滑台 | 必须写limit,axis指定平移方向 |
| planar | 3自由度 | 仿真测试用 | 真实机器人很少用 |
| floating | 6自由度 | 仿真测试、无人机 | 真实机器人很少用 |
在joint里,origin的xyz是parent link坐标系下关节原点的偏移,rpy是child link坐标系相对于parent link坐标系的旋转。axis是旋转轴或平移方向在child link坐标系下的向量。很多人把axis理解成“全局坐标系下的轴”,这是错的,axis一定是相对于child link自己的坐标系。
2.3 一个最简两连杆模型,当场跑起来
直接给一个能跑的最小示例。它有两个link、一个旋转joint,刚好能完整验证URDF链路。
<?xml version="1.0"?> <robot name="two_link_arm"> <link name="base_link"> <visual> <geometry> <box size="0.2 0.1 0.05"/> </geometry> <origin xyz="0 0 0" rpy="0 0 0"/> <material name="base_mat"/> </visual> <collision> <geometry> <box size="0.2 0.1 0.05"/> </geometry> </collision> <inertial> <mass value="0.5"/> <inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/> </inertial> </link> <link name="arm_link"> <visual> <geometry> <cylinder radius="0.02" length="0.3"/> </geometry> <origin xyz="0 0 0.15" rpy="0 0 0"/> <material name="arm_mat"/> </visual> <collision> <geometry> <cylinder radius="0.02" length="0.3"/> </geometry> </collision> <inertial> <mass value="0.2"/> <inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0001"/> </inertial> </link> <joint name="arm_joint" type="revolute"> <parent link="base_link"/> <child link="arm_link"/> <origin xyz="0 0 0.05" rpy="0 0 0"/> <axis xyz="0 0 1"/> <limit lower="-3.14" upper="3.14" effort="10" velocity="1.0"/> </joint> <material name="base_mat"> <color rgba="0.2 0.4 0.8 1.0"/> </material> <material name="arm_mat"> <color rgba="0.8 0.2 0.2 1.0"/> </material> </robot>注意arm_link里的cylinder,origin的z写了0.15。因为cylinder默认的几何中心在原点,如果你不把视觉网格往上移半个高度,圆筒就会一半埋在base_link里。这里的0.15就是圆筒长度0.3的一半。这个细节看着不起眼,但它决定了机械臂看起来是从“关节处”长出来,还是从“link中心”长出来。
3. 让模型真正“动”起来——关节定义、状态发布与rviz2联动
3.1 revolute关节如何指定转轴
revolute关节需要一个明确的axis。axis是定义在child link坐标系下的旋转轴向量,很多坑都出在这个axis上:你想让臂绕z轴转,但child link坐标系本身已经被joint的origin旋转过了,实际结果就偏了。稳妥做法是先保证joint的origin的rpy为0,让child link坐标系与joint坐标系先对齐,再写axis。这样axis写0 0 1就是绕模型的全局竖直方向转,写1 0 0就是绕水平方向转,直观且不容易错。
另外revolute必须有limit标签,包含lower、upper、effort、velocity四项。lower和upper是关节角度的最小值和最大值,单位是弧度;effort是最大力矩,velocity是最大速度。哪怕你不想限制角度,也得把lower填成-3.14、upper填成3.14,缺了MoveIt2直接不认你这个模型。continuous关节则不需要limit,但如果有真实的机械限位,还是应该在结构上再加约束。
3.2 robot_state_publisher和joint_state_publisher的分工
这里有个关键概念:URDF本身是静态的,关节转多少度,需要外部节点发布joint_states消息。robot_state_publisher订阅joint_states和robot_description这两个输入,然后计算出每个link的TF。joint_state_publisher则负责给关节提供一个初始状态值。两者名字很像,但职责完全不同:前者是通用TF计算器,后者只是为了演示和测试方便,把关节值从GUI滑块发出来。
如果只是在终端里快速验证URDF,我一般这样起:
sudo apt install ros-${ROS_DISTRO}-urdf-tutorial ros-${ROS_DISTRO}-joint-state-publisher-gui ros2 launch urdf_tutorial display.launch.py model:=/path/to/two_link.urdfurdf_tutorial自带display.launch.py,它会自动启动robot_state_publisher和joint_state_publisher_gui。如果你不想依赖示例包,也可以手动起:
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(cat two_link.urdf)" ros2 run joint_state_publisher_gui joint_state_publisher_gui注意在Humble及以上版本,joint_state_publisher的GUI版需要单独安装,很多人只装了joint_state_publisher,结果启动时找不到滑块界面,就是这个原因。
3.3 在rviz2中看到真实运动效果的排查顺序
启动之后,rviz2里会看到两个link,拖动GUI里的滑块,手臂会跟着转动。看到这个效果,说明URDF语法没问题、TF链路通了。这里我要反复提醒一句话:如果rviz2里什么都没显示,先看三个地方——参数是否传了、joint_state_publisher是否在运行、Fixed Frame是不是base_link。
我帮人排查“模型加载了但在rviz2里看不到”这种问题,超过一半的结论都是Fixed Frame设成了odom或者map,而模型发布的是base_link的TF。把Fixed Frame改成base_link,模型立刻出现。另外一种情况是robot_description参数没传进去,用命令行启动时字符串里的引号被shell吞掉了,导致参数为空。排查方法很简单:另开一个终端跑ros2 param get /robot_state_publisher robot_description,如果输出为空或者报错,就是参数没传对。
4. 从两连杆到六轴机械臂——完整URDF的层次拆分
4.1 机械臂的link与joint序列怎么安排
一个六轴机械臂,从上到下的典型结构是base_link、joint1、link1、joint2、link2,一直到joint6、link6,最后加一个tool0。每两个link之间必须有一个joint,链条不能断。很多人想把末端执行器直接挂在link6上,少写一个tool0的link和固定joint,结果后面做手眼标定、装夹爪、对接MoveIt2时找不到坐标系挂载点,只能回来补。
以最常见的六轴构型为例,关节轴的安排一般是这样的:
| 关节 | 类型 | 旋转轴方向 | 说明 |
|---|---|---|---|
| joint1 | revolute | 绕z轴 | 腰部旋转 |
| joint2 | revolute | 绕y轴 | 肩部俯仰 |
| joint3 | revolute | 绕y轴 | 肘部俯仰 |
| joint4 | revolute | 绕x轴 | 前臂旋转 |
| joint5 | revolute | 绕y轴 | 腕部俯仰 |
| joint6 | revolute | 绕x轴 | 末端法兰旋转 |
每个joint的origin xyz必须和实际机械结构尺寸严格对应,否则后面做运动学反解时,算出来的末端位姿和实物对不上。很多人从CAD导出之后,发现自己机械臂在仿真里能运动,但真实机器人一执行就撞,十有八九是关节的origin偏了几个毫米,测量误差被累积放大到了末端。
4.2 惯性参数到底怎么填,为什么不能随手写个1
这是URDF里最容易被忽视的部分。Gazebo做物理仿真时,inertia参数直接决定动力学行为。很多人随便填mass=1、inertia=1,仿真结果一塌糊涂,机械臂“轻飘飘的”,转个角度自带各种诡异晃动。
惯性张量的单位是kg·m²,一个0.5米长、质量0.5kg的连杆,转动惯量大概在0.01量级,写成1.0就偏大了两个数量级。更麻烦的是,惯性张量矩阵如果不满足正定性,Gazebo会直接报错。我之前遇到一个case,link导入后仿真器整个崩溃,查了半天发现是SolidWorks导出时,惯性张量某个非对角线元素填错了符号。
手动估算的话,圆柱体绕轴线转动的惯量可以近似用izz = 0.5 * m * r^2,绕垂直于轴线的方向的惯量近似用ixx = iyy = (1/12) * m * L^2 + (1/4) * m * r^2。真实项目的惯性参数,尽量从CAD软件的质量属性工具里读取,尤其是对于高速运动的机械臂,惯性参数不准,控制器设计全白搭。
4.3 贴图和材质让模型在可视化中更真实
URDF支持两种视觉外观:一种是直接给material颜色rgba,另一种是通过mesh标签引用DAE、STL、OBJ文件,DAE还能携带纹理贴图。机械臂视觉上看起来顺不顺眼,主要取决于这个环节。
从SolidWorks导出的STL,通常只有一个灰色,在rviz2里看过去像一个没有灵魂的模型。可以在URDF的robot根节点下定义多个material,然后给每个link的visual指定不同材质:
<material name="joint_gray"> <color rgba="0.6 0.6 0.7 1.0"/> </material> <material name="link_blue"> <color rgba="0.1 0.2 0.8 1.0"/> </material>注意material不能定义在link内部,必须定义在robot顶层,否则rviz2会提示材质不存在。颜色rgba的四个分量都是0到1的小数,不是0到255,这一点也容易踩坑。很多人把255填进去,结果颜色变成纯白或读不出来。
5. 从SolidWorks到URDF——CAD模型转换与坐标对齐的完整流程
5.1 为什么手动建模不够,得靠CAD转
手动用box、cylinder、sphere搭一个简化URDF很容易,五分钟就能跑通,但真实机器人的外壳、结构件都是复杂曲面,用基本几何体去拼要么做不到,要么看起来像玩具。工业项目更常用的路径是:在CAD软件里设计好模型,通过插件导出URDF。
最常用的是SolidWorks的solidworks_to_urdf插件,另外还有Autodesk Inventor的inventor_to_urdf工具、Fusion 360的URDF Exporter,Blender也有对应的URDF导出插件。无论用哪个,核心思路都一样:CAD里已有装配体,指定哪些零件属于哪个link,哪些装配配合属于哪个joint,导出时自动生成URDF XML和mesh文件。
5.2 solidworks_to_urdf导出流程与坐标系选择
整个流程分四步。第一步,在SolidWorks里把装配体的每个零件单独另存为STL或者DAE格式,注意单位要选米。第二步,在每个joint位置创建坐标系,坐标系的原点必须和旋转轴重合,z轴指向旋转轴方向,这个直接决定导出的URDF里axis是否正确。第三步,在SolidWorks里逐个选择joint坐标系、填joint类型和运动范围。第四步,配置输出路径,生成URDF文件夹,里面会自动带上meshes、config、launch目录。
整个过程中最容易翻车的步骤就是第二步:坐标系没有对齐。我见过有人把坐标系放到了零件的一个角上而不是旋转轴中心,结果导出后机械臂的每一节关节在旋转时都漫天乱飞。正确做法是在装配体里为每个joint单独插入一个坐标系,让新坐标系的原点与配合的圆形面中心重合,z轴沿着旋转轴线。不要图省事直接用零件默认坐标系,CAD默认坐标系往往在特征树的初始位置,跟实际轴心没有任何关系。
5.3 导出后必须做三处检查
导出不是终点,检查才是。我先检查每个joint的origin xyz/rpy是否和设计一致,CAD模型里看着没问题不代表导出后没问题。然后检查link的collision网格是否完整,有些插件默认不生成碰撞体,这会导致Gazebo里机器人直接穿过地面。我的习惯是给每个link单独指定一个简化碰撞体,哪怕是一个粗糙的box,都比没有强。
最后检查meshes目录下的文件名是否和URDF里引用的路径一致。Windows上导出的项目偶尔会出现路径分隔符转义问题,URDF里引用的路径写着meshes/link1.STL,但实际文件叫link1.sldprt.STL,这种错误最坑,因为rviz2报错信息往往很模糊,只会说mesh loading failed。另外,导出后先用check_urdf命令行工具验证一遍,能查link是否重复、joint是否悬空、是否有孤立分支,比你在一堆坐标系里肉眼检查效率高得多。
6. 踩坑实录:单位、坐标朝向、仿真导入与链接错误
6.1 单位:米还是毫米,一个错误让模型飞到天上去
URDF规定长度单位是米,角度单位是弧度。但SolidWorks默认单位是毫米,CAD模型里一个100的尺寸就是100毫米,转换时如果没做单位换算,URDF里就会出现一个原本0.1m的零件被写成100m,rviz2里要么模型缩成一个点,要么镜头拉都拉不回来。
有一回我帮同事排查一个四轮机器人模型,base_link尺寸是500mm,导出后忘调单位,整个机器人看起来就像一座山一样巨大。后来我养成了一个习惯:导出前先在SolidWorks里把文档单位改成MKS(米-千克-秒),或者导完之后用check_urdf看一眼尺寸量级。手动写URDF时,也要注意尺寸数值是否和实物一致,别把毫米当米直接填。
6.2 rpy与坐标朝向,急死人的“转了90度”
joint的origin里包含xyz和rpy,rpy的旋转顺序是绕固定轴XYZ,也就是先绕z、再绕y、最后绕x。很多人觉得rpy="1.57 0 0"是绕x转90度,结果在rviz2里看到模型绕另一个轴转了个角度,原因其实是rpy的数值单位是弧度,1.5708才是90度,1就是57.3度。这个细节会让整个关节朝向完全错位。
如果你不确定某个joint的朝向对不对,别急着一次性把整个URDF写完再去启动rviz2。先把rviz2里的Axes显示打开,然后逐个joint检查,看看某个关节的旋转轴是否和坐标轴显示方向一致。一次只调一个关节,确认正确再写下一个,比把所有关节堆完再排查快得多。这个排查方法救过我很多次。
6.3 URDF导入其他仿真平台的衔接问题
URDF不是万能格式。Coppeliasim可以直接导入URDF,但经常碰到关节运动方向反了、惯性参数读取异常的情况,建议导入后逐个检查关节属性。Isaac Sim导入URDF需要先转成USD格式,NVIDIA官方提供了urdf_to_usd工具,转换时需要注意mesh文件路径,URDF里如果写的是绝对路径,换一台机器就会全部失效,应该改成相对路径。
Gazebo对URDF的要求最严格,没有collision或inertial的link会直接报错。另外Gazebo实际用的是SDF格式,ROS2启动时会自动把URDF转成SDF,转换过程中如果原URDF写得不规范,问题会特别多,比如joint类型不匹配、material路径错误。还有很多人遇到的“Gazebo加载后机械臂倒地”问题,大概率是link质量量级太小,或者joint的effort限幅太小,被物理引擎判定为力矩不足。这种问题只能把URDF和controller参数一起调,单改URDF是没用的。
7. 进阶:xacro参数化、多机台复用与仿真集成
7.1 用xacro把重复几何封装成宏
当你的机器人link多起来后,URDF文件会膨胀得特别快。四个一模一样的轮子,就得写四段重复的link。xacro就是来解决这个问题的,它是URDF的宏语言,支持属性、宏函数、数学表达式。我通常把每个标准零件定义成xacro宏,实例化时只需要传入name、radius、width等参数。改一个轮子参数时,其它轮子会同步更新,不会因为复制粘贴漏改一个数值导致模型不一致。
下面是一个典型的轮子宏:
<?xml version="1.0"?> <robot name="wheeled_robot" xmlns:xacro="http://www.ros.org/wiki/xacro"> <xacro:macro name="wheel_link" params="prefix radius width color"> <link name="${prefix}_wheel"> <visual> <geometry> <cylinder radius="${radius}" length="${width}"/> </geometry> <material name="${color}"/> </visual> <collision> <geometry> <cylinder radius="${radius}" length="${width}"/> </geometry> </collision> <inertial> <mass value="0.1"/> <inertia ixx="0.0001" ixy="0.0" ixz="0.0" iyy="0.0001" iyz="0.0" izz="0.0002"/> </inertial> </link> </xacro:macro> <xacro:wheel_link prefix="left" radius="0.05" width="0.03" color="black_mat"/> <xacro:wheel_link prefix="right" radius="0.05" width="0.03" color="black_mat"/> </robot>使用xacro后,link的名字会带上${prefix}前缀,这样左右轮子就不会重名。没有xacro,你大概率会复制粘贴出一个轮子尺寸正确、另一个尺寸错误还找不出原因的尴尬局面。
7.2 直接把URDF塞进Gazebo/Isaac Sim要做什么
URDF进入Gazebo之前,通常需要给joint加transmission标签、给link加gazebo reference标签,用来设置摩擦系数、阻尼等物理参数,否则Gazebo不知道关节力矩如何传递给机器人。transmission的写法有固定模板,配合ros2_control使用的话,还需要在URDF里添加ros2_control的插件配置。这部分内容本身就能写一篇长文,你只需要记住:URDF只是地基,物理仿真需要在它之上继续盖楼。
Isaac Sim导入URDF时,我推荐用isaac sim自带的URDF Importer扩展直接导入。导入过程中有个fix_base_link选项,勾选后base_link会被固定住,这样在Isaac Sim里做机械臂实验,基座不会因为重力直接砸向地面。我第一次没勾这个选项,一按Play,整个机械臂直接飞出去,后来才发现就是这一个选项的事。Coppeliasim导入时要注意它会把URDF的关节mimic属性解释成自己的动态关节,碰到复杂gripper的时候可能会多出一堆奇怪的约束,需要手动调整。
7.3 下一步可以怎么扩展
等你把URDF吃透之后,可以顺路研究三条延伸线。一条是MoveIt2运动规划,URDF只是它的输入,它真正吃的是SRDF、配置文件和控制接口。另一条是Nav2导航,轮式机器人需要URDF来提供传感器位姿、底盘轮廓、里程计坐标系,模型错了导航规划出的路径绝对不对。第三条是数字孪生和仿真验证,URDF转成SDF或USD后,可以接入更多仿真器做复杂验证。模型永远是连接真实机器和仿真世界之间的那座桥,这座桥只要搭对了,后面的路会顺很多。
我个人实际干下来的体会是,URDF建模最关键的从来不是语法,而是坐标系思维。你能不能在建模型时就把每个关节的转动轴、每个传感器的安装位姿在坐标系里规划清楚,决定了后续所有工作能不能顺畅推进。刚接触ROS2的话,建议别急着追求模型美观,先拿简单的两连杆把TF链路和关节发布跑通,再去找一个现成的机械臂URDF拆开读一遍,最后再动手写自己的。拆模型、改模型、再写模型,这个顺序比直接照着文档硬写要有效得多。希望这篇整理能帮你少走几段弯路。