URDF 与 Xacro:给机器人造一份数字骨架
URDF,全称 Unified Robot Description Format(统一机器人描述格式),是 Rviz2/Gazebo/TF2/运动规划的唯一模型输入——rviz2 里那台蓝底盘黑轮的小车,源头就是一份这样的 XML。Xacro = XML + Macro(XML 宏),即"在 XML 里嵌宏"的宏语言:用参数与宏把 150 行模型压缩到 80 行。本实验讲透 link 三重属性与 joint 六类型的原理,并让惯性矩阵从"魔法数字"变成公式推导。
目标本课学完,你应该能做到
能阐述 URDF 中 link 三重属性各自的作用及其分离设计的工程意义——为什么渲染、碰撞检测、动力学计算需要独立定义。
能正确编写含多个 link 与 joint 的 URDF,用 Xacro 参数化建模,并用 xacro 工具把 .xacro 展开为标准 URDF。
独立构建简化差分驱动机器人(底盘 + 双轮 + 万向轮),在 rviz2 中加载并验证其运动学结构。
能为 B2 实验箱上的实际机器人编写含真实尺寸参数的 URDF,并在 rviz2 中验证。
难点:惯性矩阵不能拍脑袋——非法值会让 Gazebo 求解器发散、模型原地爆炸。
01问题引入:屏幕里的小车,是谁画的
robot_state_publisher 把它变成 TF 变换和三维模型,rviz2 里那台小车就是从这里"画"出来的。robot_description——它的内容就是一份 URDF 文件02先看现象:一份 XML,一台会动的车
car1.urdf(SolidWorks 导出,约 1200 行)认识 URDF:不整篇贴文件,按不同写法摘片段举例——link 怎么写、joint 怎么分类、xyz/rpy 装在哪、什么能一直转、什么只能有限转。每个片段都点开看注释;常用标签的完整说明在 03 节。例 1 · 文件头与空 link — robot 是唯一根标签,空 link 就是纯坐标系
节选自 car1.urdf 开头(robot 标签横跨全文到文件末尾) <?xml version="1.0" encoding="utf-8"?> <robot name="car1"> <!-- 空 link:没有几何也没有质量,只当坐标系用——base_footprint 是整车"落地点" --> <link name="base_footprint" /> <!-- 同款空 link 还有 tcp_link(末端工具点)、camera_depth_optical_frame(相机光轴系)…全文十几处 --> </robot>
读法:name="car1" 就是 check_urdf 第一行 robot name is: car1 的出处;空 link 不显示任何东西,只为给 TF 树提供一个原点。
例 2 · link 三重属性 — base_link 把 inertial / visual / collision 写全
节选自 car1.urdf 的 base_link(惯性矩阵共 9 个数,此处原样摘录) <link name="base_link"> <!-- inertial:动力学读它——质量 1.97 kg + SolidWorks 算好的 3×3 惯性矩阵 --> <inertial> <origin xyz="0.00119523538791205 0.00195957701632652 0.0457542650231294" rpy="0 0 0"/> <mass value="1.97049077979221"/> <inertia ixx="0.00486152635724755" ixy="2.00512337711509E-06" ixz="0.000162020407436135" iyy="0.00915836901735741" iyz="2.72820673876569E-06" izz="0.0130028044572217"/> </inertial> <!-- visual:RViz2 读它——STL 网格 + 颜色(rgba 值域是 [0,1],不是 0~255) --> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry><mesh filename="package://car1_description/meshes/base_link.STL"/></geometry> <material name=""><color rgba="1 1 1 1"/></material> </visual> <!-- collision:Gazebo 碰撞检测读它,这里直接复用同一 STL(真实工程可简化成包围盒) --> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry><mesh filename="package://car1_description/meshes/base_link.STL"/></geometry> </collision> </link>
读法:三块内容消费方各不相同——渲染吃 visual、碰撞吃 collision、动力学吃 inertial;package:// 是"包内相对路径",装到哪台机器都能找到网格。三属性的完整作用表在 03 节。
例 3 · joint 之 fixed + xyz — 焊死的安装关节,xyz 就是安装位置
节选自 car1.urdf:base_footprint 与 base_link 之间的"垫高"关节 <joint name="base_joint" type="fixed"> <origin xyz="0 0 0.029" rpy="0 0 0"/> <!-- 子 link 相对父 link:仅沿 Z 抬高 2.9 cm --> <parent link="base_footprint"/> <child link="base_link"/> </joint>
读法:fixed = 0 自由度、焊死不能动,只负责定安装位姿;xyz 三个数是平移(米),遵循 REP-103(X 前 / Y 左 / Z 上)。激光雷达 laser_joint 的 xyz="0.146 0 0.087" 同理——"往前 14.6 cm、往上 8.7 cm"装雷达。
例 4 · joint 之 rpy — 旋转姿态:相机光轴系掰了两个 90°
节选自 car1.urdf:深度相机末端的光学坐标系关节 <joint name="camera_depth_optical_joint" type="fixed"> <origin xyz="0 0.01655 0" rpy="-1.57079633 0 -1.57079633"/> <!-- -π/2 ≈ -1.5708,两个 90° 把车体系掰成相机光学系约定 --> <parent link="camera_depth_link"/> <child link="camera_depth_optical_frame"/> </joint>
读法:rpy 三个数 = 绕 X、Y、Z 的旋转弧度(roll / pitch / yaw),求值先 rpy 旋转、再 xyz 平移;±1.5708 就是 90°。joint 里的 origin = 子 link 相对父 link 的位姿——与 link 内 visual 下的 origin(几何体对自己的偏移)语义完全不同,详见 03 节。
例 5 · joint 之 continuous — 一直转动:四个驱动轮,转圈无尽头
节选自 car1.urdf:左前驱动轮(其余三个轮同构,仅 name/xyz 不同) <joint name="wheel_lf_joint" type="continuous"> <origin xyz="0.097300000000001 0.105834744636228 0.0195000000000009" rpy="0 0 0"/> <parent link="base_link"/> <child link="wheel_lf_link"/> <axis xyz="0 1 0"/> <!-- 轮轴沿 Y(车宽方向)——绕它一直转 --> </joint>
读法:continuous = 一直转动,无限转圈,因此没有也不允许 <limit>;只要给 axis(转轴方向)。拖滑块时轮子转不到"头"——这正是它和例 6 的分界。
例 6 · joint 之 revolute — 有限转动:机械臂关节必须写 limit
节选自 car1.urdf:机械臂第一轴(arm_joint2~5、夹爪诸关节同款写法) <joint name="arm_joint1" type="revolute"> <origin xyz="0.0100000000000003 0 0.0434999999999999" rpy="0 0 0"/> <parent link="arm_base_link"/> <child link="arm_link1"/> <axis xyz="0 0 1"/> <!-- 绕 Z 转(肩关节回转) --> <limit lower="-3.14" upper="3.14" effort="100" velocity="1"/> <!-- 有限转动,四件套写全 --> </joint>
读法:revolute = 有限转动,角度被 limit 锁死在 −3.14 ~ +3.14 rad(±180°);effort=100 最大力矩、velocity=1 最大角速度。拖滑块到头即停、check_urdf 缺 limit 直接报错——revolute 的 limit 是必填项。
例 7 · joint 之 mimic — 其他分类:左指跟着右指动
节选自 car1.urdf:左外指关节,从动于右关节 <joint name="l_joint" type="revolute"> <origin xyz="0.01900706 0.01375021 0.0025" rpy="0 0 0"/> <parent link="arm_link6"/> <child link="l_link"/> <axis xyz="0 0 -1"/> <limit lower="-3.14" upper="3.14" effort="100" velocity="1"/> <mimic joint="r_joint" multiplier="-1" offset="0"/> <!-- 跟随 r_joint:角度 ×(-1) 反向联动 --> </joint>
读法:mimic 让本关节成为从动关节——只驱动 r_joint,l_joint 按 multiplier 自动反向跟动(夹爪对开就是这么省出来的)。这是 car1 文件里除 fixed / continuous / revolute 之外的又一种分类。
关节分类对照(car1 实例)— 哪些一直转、哪些有限转、哪些根本不能转?
| 分类 | car1.urdf 实例 | 特征与判定 |
|---|---|---|
fixed | base_joint、laser_joint、imu_joint | 0 自由度焊死,只定安装位姿 |
continuous | wheel_lf_joint 等 4 个驱动轮 | 一直转动:只写 axis,无 limit |
revolute | arm_joint1~5、夹爪诸关节 | 有限转动:必须 limit lower/upper |
prismatic | 本车未用 | 直线滑轨(升降台类),同样必须 limit |
floating / planar | 本车未用 | 6 / 3 自由度,实践中几乎不手写 |
mimic(从动标记) | l_joint → 跟随 r_joint | 不加自由度,按倍率复制另一关节的角度 |
car1.urdf 的骨架读法——1200 行静态 XML,谁把它变成 RViz2 里会动的整车?03 节把常用元素列全并讲透,04 节直接上加载代码。03原理讲解
link 三重属性 × joint 六类型
建立在 EXP-19 的 TF 树之上——URDF 就是那棵树的源头文件:link 是节点,joint 是边。
| link 属性 | 作用 | 取值/几何体 | 何时必须 |
|---|---|---|---|
<visual> | 渲染外观(给 Rviz2 看) | box / cylinder / sphere / mesh(数万三角形也行) | 只想可视化时——可单独存在 |
<collision> | 物理碰撞检测(给 Gazebo 看) | 同上,但应简化为包围盒以降低实时计算量 | 仿真必须 |
<inertial> | 动力学:mass + 3×3 惯性矩阵 | ixx iyy izz ixy ixz iyz | 仿真必须(否则模型飞走) |
一句话总表(PPT 可直接放)· joint 四大常用类型 —— 哪些一直转、哪些有限转、哪些滑直线、哪些根本不动?
| 类型 | 运动方式 | 有没有限位 | 典型例子 |
|---|---|---|---|
continuous | 绕单轴旋转 | ✅ 无角度上下限,可以一直转 | 小车驱动轮(car1:wheel_lf_joint 等 4 轮)、云台无限制旋转 |
revolute | 绕单轴旋转 | ✅ 有角度上下限(单位:rad) | 机械臂舵机、夹爪转轴(car1:arm_joint1~5,±3.14)、舵机 0~180° |
prismatic | 沿单轴平移滑动 | ✅ 有位置上下限(单位:m) | 电动推杆、伸缩气缸、升降滑台(car1 未用) |
fixed | 完全不动,刚性锁死 | ❌ 不能动 | 底盘上固定的相机、雷达(car1:laser_joint)、base_footprint→base_link |
补充两行凑齐常说的"joint 六类型":floating(6 自由度)与 planar(3 自由度)实践中几乎不手写;mimic 不是独立类型而是从动标记(02 节例 7)。
① continuous 无限旋转关节 — 绕轴不停旋转,没有转角限制
- 绕一根轴不停旋转,没有转角限制,可以转很多圈
- 不需要
<limit>标签 - 示例:小车轮子,向前向后无限滚(car1 四个驱动轮,原文见 02 节例 5)
<joint name="wheel_joint" type="continuous"> <parent link="base_link"/> <child link="wheel_link"/> <axis xyz="0 1 0"/> <!-- 只给转轴,不写 limit —— 它可以一直转 --> </joint>
② revolute 有限旋转关节 — 也绕轴旋转,但到最大/最小角度就卡住
- 也是绕轴旋转,但是有最大、最小角度,到角度就卡住
- 必须写
<limit lower upper effort velocity/>—— 角度单位是弧度 rad(check_urdf 缺了直接报错) - 示例:机械臂关节、舵机,只能 0~90° 来回摆,不能一直转
# car1 机械臂第一轴的原值四件套(02 节例 6 同款) <joint name="arm_joint1" type="revolute"> <parent link="arm_base_link"/> <child link="arm_link1"/> <axis xyz="0 0 1"/> <limit lower="-3.14" upper="3.14" effort="100" velocity="1"/> <!-- 弧度限位 + 最大力矩 + 最大角速度 --> </joint>
③ prismatic 平移(棱柱)关节 — 不转,沿轴直线推拉滑动
- 不是转,是沿着一根轴直线推拉滑动(滑轨、丝杆、气缸的动作)
- 带位置限位,单位是米 m(不是弧度)
- 示例:升降杆、伸缩夹爪、升降滑台(car1 本车未用,属通用写法)
<joint name="lift_joint" type="prismatic"> <parent link="base_link"/> <child link="lift_link"/> <axis xyz="0 0 1"/> <!-- 沿 Z 直线推 --> <limit lower="0" upper="0.3" effort="50" velocity="0.2"/> <!-- 行程 0~0.3 m,单位是米! --> </joint>
④ fixed 固定关节 — 没有任何相对运动,父子焊死
- 没有任何相对运动,父子连杆焊死在一起,相对位置姿态永久不变
- 不需要
axis、limit、dynamics - 你前面那张 TF 图里
base_footprint → base_link就是 fixed(02 节例 3 原文:xyz="0 0 0.029") - 作用:把雷达、相机、外壳固定到底盘上,只用来传递静态 TF
continuous② fixed 虽然叫 "joint",本质不是运动关节,只是用来建立静态坐标变换——robot_state_publisher 就是靠 fixed 关节输出静态 TF
③ revolute 单位是弧度,prismatic 单位是米
④ joint_state 话题分工:continuous / revolute 发布角度;prismatic 发布位移;fixed 不发布任何关节状态
revolute:来回摆动有角度
prismatic:前后伸缩滑直线
fixed:死死固定不能动
另注:URDF 坐标系遵循 REP-103 —— X 前、Y 左、Z 上;origin 的求值顺序是先 rpy 旋转、再 xyz 平移。
两个发布者如何合力把车"画"出来
robot_description = open(urdf_path).read() Node(package='robot_state_publisher', parameters=[{'robot_description': robot_description}]) Node(package='joint_state_publisher_gui')少了注入或少了 GUI 节点,就是下面分解图里的两种断链。
惯性矩阵——从魔法数字到物理公式
例:底盘 0.40×0.25×0.08 m、质量 3.0 kg → ixx=0.017、iyy=0.042、izz=0.053。沿某轴的惯量 = 质量对该轴分布的"转动惰性",垂直于该轴的边长平方贡献最大。
轮轴沿 Y:绕轴(Y)转动惯量小、绕直径大时看 h 贡献。r=0.05、L=0.03、m=0.2 代入即可,杜绝拍脑袋的 0.01。
ixx、iyy、izz 全部 > 0,且满足三角不等式 ixx+iyy≥izz 等。非法值会让 Gazebo 求解器发散——模型原地爆炸或瞬间飞出场景。
只做 Rviz2 可视化时惯性可省;进 Gazebo 前若懒得算,统一写 ixx=iyy=izz=1e-3(对角等值必正定),先跑通再精算。Xacro 则用公式自动算(见本段末 Xacro 主场,实操在 04 节)。
Xacro:XML + Macro,展开即 URDF
建立在 link / joint 写法之上——URDF 是纯静态 XML;Xacro 是它的"带宏超集":变量、宏、子文件引入,全部在展开那一步翻译回普通 URDF。
<xacro:include> 原样引入总装——car1 工程正是把 car1.urdf 一行不改地 include 进仿真 xacro(见案例 B)。| 维度 | URDF | Xacro |
|---|---|---|
| 本质 | XML 方言,标签固定 | XML 超集:xacro:* 标签 + ${} 表达式 |
| 重复结构 | 复制粘贴(car1 四个轮关节手写四遍) | macro 定义一次、多处调用,${prefix} 防撞名 |
| 数值来源 | 逐个写死(或由导出器代算) | property / arg 收编 + ${} 算式推导 |
| 子文件复用 | 不支持,只能合并全文 | xacro:include 把多个文件拼成总装 |
| 工具链直读 | 直接吃 | 先 xacro 展开(命令行或 launch 内)再吃 |
| 兼容关系 | 是 Xacro 的子集,可被原样 include | 展开 = URDF,向下兼容 |
car1_description/gazebo/ 与 car1_gazebo/urdf/),注释逐行标好——点开看宏怎么定义、怎么调用、怎么把 URDF 拼进来。案例 A · 宏定义与调用 — car1.gazebo.xacro:params 形参 + ${} 占位,一处定义全车复用
<robot xmlns:xacro="http://ros.org/wiki/xacro">
<!-- Gazebo 物理参数宏:params="name" 声明形参,${name} 是调用处传入的实参 -->
<xacro:macro name="gazebo_link" params="name">
<gazebo reference="${name}">
<kp>0</kp><kd>0</kd>
<mu1>1</mu1><mu2>1</mu2> <!-- 摩擦系数 -->
<material>Gazebo/White</material>
</gazebo>
</xacro:macro>
<!-- 夹爪换高刚度宏:kp=1e15 抓得稳、kd=1e13 减震——换个宏名即换整套参数 -->
<xacro:macro name="gazebo_gripper" params="name">
<gazebo reference="${name}">
<kp>1e15</kp><kd>1e13</kd>
<mu1>1.0</mu1><mu2>1.0</mu2>
<material>Gazebo/Black</material>
</gazebo>
</xacro:macro>
<!-- 宏调用:像普通标签一样使用;${prefix} 拼在每个名字前,多车不撞名 -->
<xacro:macro name="car1_gazebo" params="prefix">
<xacro:gazebo_link name="${prefix}arm_base_link"/>
<xacro:gazebo_link name="${prefix}arm_link1"/> …(7 节臂各一行)
<xacro:gazebo_gripper name="${prefix}r_link"/> …(6 个夹爪件)
<xacro:gazebo_link name="${prefix}base_link"/>
</xacro:macro>
</robot>
读法:宏 = 带参数的 XML 模板——定义一次,car1_gazebo 里连调十几行;${name}、${prefix}arm_link1 在展开时做字符串替换,${} 里也可以写算式(如 ${wheel_radius/2 + 0.01},property 收编变量的完整流程即图 20-4)。忘写 xmlns:xacro,这些标签全部失效(05 节坑条)。
案例 B · include 总装 — car1.gazebo.xacro:把普通 URDF 直接拼进 Xacro(兼容性实锤)
<robot name="car1" xmlns:xacro="http://ros.org/wiki/xacro">
<!-- 命令行/launch 可传参数:$(arg prefix) 逐处取值 -->
<xacro:arg name="prefix" default=""/>
<!-- 兼容实锤:include 的目标是一份普通 .urdf 文件,原样引入、一行不改 -->
<xacro:include filename="$(find car1_description)/urdf/car1.urdf"/>
<!-- 再叠加物理、传动、硬件接口三份宏文件 -->
<xacro:include filename="$(find car1_description)/gazebo/car1.gazebo.xacro"/>
<xacro:include filename="$(find car1_description)/gazebo/car1.transmission.xacro"/>
<xacro:include filename="$(find car1_gazebo)/urdf/car1.gazebo.ros2_control.xacro"/>
<!-- 宏调用:把物理参数/传动/硬件刷到每个 link、joint 上 -->
<xacro:car1_gazebo prefix="$(arg prefix)"/>
<xacro:car1_transmission prefix="$(arg prefix)"/>
<xacro:car1_gazebo_ros2_control name="GazeboSimSystem"/>
…(其后是 Gazebo<plugin>、相机<sensor> 等原生标签,按原样保留到文件尾)…
</robot>
读法:总装文件 = URDF(子集原样合入)+ Xacro 宏调用 + 原生 Gazebo 标签混排;xacro 命令一跑,car1.urdf 的一千二百行原封并入、宏全部替换为普通标签,输出即标准 URDF。URDF 与 Xacro 的双向兼容在此文件里是实锤——加载方法就是 04 节步骤 ② 两段代码。
xmlns:xacro → 全部宏标签失效、${} 原样泄漏进输出;宏调用 name 撞车 → 展开后重复 link/joint 解析失败;宏定义必须先于调用且同在一个 <robot> 内。三条都能被"展开 → check_urdf"当场抓住(04 节代码逐段对应)。04实验步骤
car1.urdf 和它的 Xacro 加载起来用——全程代码说话:步骤 ① 体检 + launch 加载 car1.urdf,进 RViz2 拖滑块,亲眼比对"一直转动"与"有限转动";步骤 ② 用 xacro 展开组合文件,把展开文本注入 robot_description。先看框架图认清文件、节点与工具;命令逐段可复制,输出直接列在下方对照。步骤 ① 加载 car1.urdf:体检 → 注入 → 三节点
sudo apt install liburdfdom-tools ros-humble-xacro -y,并确认已 source 工作空间check_urdf …/car1.urdf → 对照下方输出 ① 的三行display.launch.py 的核心八行——读文件 → 注入参数 → 起三节点(见代码段 ②)ros2 launch car1_description display.launch.py → RViz2 整车出现,拖滑块见输出 ③# 输出 ①:三行 = car1.urdf 解析通过;根 link 是 base_footprint,挂着 1 个直接子件(base_link) $ check_urdf $(ros2 pkg prefix car1_description)/share/car1_description/urdf/car1.urdf robot name is: car1 ---------- Successfully Parsed XML --------------- root Link: base_footprint has 1 child(ren) # 代码段 ②:launch 怎么把文件送进节点(car1_description/launch/display.launch.py 核心 8 行) pkg_share = get_package_share_directory('car1_description') urdf_file = os.path.join(pkg_share, 'urdf', 'car1.urdf') with open(urdf_file, 'r') as f: robot_desc = f.read() # 文件全文 → 字符串 Node(package='robot_state_publisher', parameters=[{'robot_description': robot_desc}]) # 注入参数(缺这步 = 黑屏) Node(package='joint_state_publisher_gui', …) # 发 /joint_states,带滑块 Node(package='rviz2', arguments=['-d', rviz_config]) # 用预配 car1.rviz 画出来 # 输出 ③:一键拉起三节点(launch 已写好,无需改) $ ros2 launch car1_description display.launch.py # RViz2:Fixed Frame 选 base_footprint → 底盘+4轮+机械臂+夹爪整体出现 # 拖滑块 arm_joint1 → 转到 ±3.14 rad 就停 ……… revolute 有限转动(02 节例 6) # 拖滑块 wheel_lf_joint → 转圈没有尽头 …………… continuous 一直转动(02 节例 5)
| 联动检查 | 预期 | 不满足时 |
|---|---|---|
| Fixed Frame = base_footprint | 整车居中显示 | Global Status 红字、场景全空 |
RobotModel → Description Source = Topic /robot_description | 外形出现 | 参数未注入或未 source install |
| 拖 arm_joint1 滑块 | 臂转到 ±3.14 卡住 | joint type 被改成 fixed 或 GUI 没启动 |
| 拖 wheel_lf_joint 滑块 | 轮子连续转圈不停 | 轴 axis 丢失,或关节被加了 limit |
步骤 ② 加载 Xacro:展开得到 URDF,再走同一条注入路
# ① 命令行展开 + 体检(car1_gazebo 总装文件 = 03 节案例 B,内部 include 了 car1.urdf) $ sudo apt install ros-humble-xacro -y $ xacro $(ros2 pkg prefix car1_gazebo)/share/car1_gazebo/urdf/car1.gazebo.xacro \ > /tmp/car1_expanded.urdf $ check_urdf /tmp/car1_expanded.urdf robot name is: car1 ---------- Successfully Parsed XML --------------- root Link: base_footprint has 1 child(ren) # 展开件与原文件同样三行通过 = 宏、变量、include 已全部翻译成标准 URDF # ② 工程 launch 的真实做法(car1_gazebo/launch/gazebo_arm.launch.py 原代码) urdf_content = subprocess.check_output(['xacro', xacro_file]).decode('utf-8') Node(package='robot_state_publisher', parameters=[{'robot_description': urdf_content}]) # ③ 两条路线并排看——注入方式完全相同,唯一区别是文本从哪来 URDF 路线:open('car1.urdf').read() ──┐ Xacro 路线:xacro 总装.xacro 的展开输出文本 ──┼─→ robot_description ─→ 三节点 ─→ RViz2/Gazebo ┘ 下游感知不到差别 = 03 节"双向兼容"的落地
$(ros2 pkg prefix …) 换成绝对路径 ~/…/car1_description/urdf/car1.urdf 照样能跑;但 launch 里请始终用 get_package_share_directory()——install 之后 src/ 绝对路径必失效。② 展开得到的 /tmp/car1_expanded.urdf 可再喂 urdf_to_graphiz 出拓扑图,对照 02 节分类表逐关节核对 fixed / continuous / revolute 各在何处。| 加载环节 | 容易踩的坑 | 解法 |
|---|---|---|
| check_urdf 报错解析失败 | 文件截断 / 标签没闭合(XML 零容忍) | 看报错行号;urdf_to_graphiz 二分定位 |
| RViz 黑屏、无模型 | robot_description 未注入 / 未 source install | 对照本节代码段 ② 的 open().read() 四行 |
xacro 展开产物含 ${} | 变量未定义或 xmlns:xacro 缺失 | 回查 03 节案例 A:宏先于调用、根标签声明命名空间 |
| launch 找不到模型文件 | 硬编码 src/ 绝对路径,install 后失效 | 统一用 get_package_share_directory() |
| package:// 网格全丢(Gazebo 白模) | 仿真环境不解析包路径 | 照 gazebo_arm.launch.py:展开后 replace 成 file:// 绝对路径 |
检验工具三件套
# ① 语法+结构验证(步骤 ① 的原命令,汇总在此) $ check_urdf $(ros2 pkg prefix car1_description)/share/car1_description/urdf/car1.urdf robot name is: car1 ---------- Successfully Parsed XML --------------- root Link: base_footprint has 1 child(ren) # ② 生成 link-joint 拓扑图(需 liburdfdom-tools) $ sudo apt install liburdfdom-tools -y $ urdf_to_graphiz /tmp/car1_expanded.urdf # → 生成 PDF:base_footprint→base_link 分叉到 4 轮 / 激光 / IMU / 6 节臂 / 夹爪 # ③ Xacro 展开件复检(步骤 ② 的产物,两条命令即闭环) $ xacro $(ros2 pkg prefix car1_gazebo)/share/car1_gazebo/urdf/car1.gazebo.xacro \ > /tmp/car1_expanded.urdf $ check_urdf /tmp/car1_expanded.urdf # 同样三行通过 = 展开产物是合法标准 URDF(宏语法无误、include 拼接成功)
URDF Doctor 症状诊断
📸 关键操作与现象截图(3 图)



05注意事项与避坑清单
- origin 求值顺序:先 rpy 旋转、再 xyz 平移——顺序颠倒视觉全变;开 TF Display 查坐标轴方向。
- 惯性矩阵对称正定:ixx/iyy/izz>0 且满足三角不等式,非法值 = Gazebo 爆炸;初学 1e-3 占位。
- joint 类型不可混:轮子 continuous(无限转)、肘关节 revolute + limit;给轮子设 revolute 会限死角度。
- Xacro 声明命名空间:xmlns:xacro 缺失则所有宏标签失效。
- Fixed Frame 用根 link:必须是 URDF 中真实存在的 link 名(car1 用 base_footprint,通用写法 base_link),否则 RobotModel 拒绝渲染。
- 路径用包索引:get_package_share_directory() 取路径,绝不要硬编码 src/ 下的绝对路径。
- 三种验证手段分工:check_urdf 查语法结构、urdf_to_graphiz 查拓扑外观、RViz 查几何与坐标。
| 工具 | 作用 | 命令 |
|---|---|---|
check_urdf | 语法+结构验证 | check_urdf model.urdf |
urdf_to_graphiz | 生成链接拓扑图 PDF | urdf_to_graphiz model.urdf |
| Rviz2 RobotModel | 外观/碰撞可视化 | Visual / Collision Enabled 开关 |
| Rviz2 TF Display | 验证各坐标系位置 | 观察 RGB 轴箭头方向 |
xacro | 宏展开与表达式求值 | xacro model.xacro > out.urdf |
06本课总结与验证题
XML 描述几何结构、运动学约束与动力学参数,Rviz2/Gazebo/TF2/运动规划共享同一模型源。
visual/collision/inertial 按用途分离,各场景只声明所需;fixed/revolute/continuous/prismatic 按自由度选型,两种 origin 语义严格区分。
joint_state_publisher 给关节角,robot_state_publisher 用 URDF 解算 TF,Rviz2 依据 Fixed Frame 绘制——状态与模型解耦。
macro 模板复用、property/${} 收编尺寸与算式、include 把现有 URDF 拼进总装——展开即标准 URDF,与纯 URDF 双向兼容(见 03 节两案例、04 节两段加载代码)。
<collision> 整段删掉、其余不动:RViz2 与 Gazebo 里的表现分别如何?由此说明三属性为何必须分离。
参考答案与得分点
得分点:①RViz2 外观完全正常(只消费 visual)②Gazebo 中底盘无碰撞体——穿地、与他物互穿 ③分离是为了解耦三种消费方:渲染吃数万三角形、碰撞要实时求交、动力学要真实惯性——独立声明各取所需。(①②各 30%,③ 40%)wheel_lf_joint 的 type="continuous" 改成 "fixed",拖 joint_state_publisher_gui 滑块会看到什么?用哪一步能最快定位到这个改动?
参考答案与得分点
得分点:①左前轮纹丝不动(fixed 零自由度焊死),其余三轮照常转——四轮不对称 ②URDF Doctor"拖滑块轮子不动"处方:先查 joint type ③不可与 revolute 混:轮子要一直转(continuous 无 limit);revolute 有限转必带 limit lower/upper。(①②各 40%,③ 20%)xmlns:xacro="http://ros.org/wiki/xacro",再运行 xacro …/car1_gazebo/urdf/car1.gazebo.xacro,会发生什么?展开产物还能过 check_urdf 吗?