URDF 机器人建模完全指南

ROS
9.5k 词

照着模板拼 URDF 只能得到”能显示”的模型;工程上你要的是”仿真行为可信“的模型。本文把建模背后被跳过的数学补上:joint origin 的 rpy 到底是什么旋转顺序 → 惯量张量怎么算、Fusion360 的数值为什么是错的(单位陷阱)→ transmission 减速比的力矩/速度换算 → xacro 参数化几何推导 → Gazebo 摩擦与阻尼调参 → 验证工具链。最后是 RoboMaster 云台/麦轮车这类实战模型的建模要点。

一、坐标变换:URDF 就是一棵 TF 树

树形结构的本质

URDF 里每个 <joint> 声明”child link 坐标系在 parent link 坐标系中的位姿“:

1
2
3
4
5
6
<joint name="yaw_joint" type="revolute">
<origin xyz="0 0 0.12" rpy="0 0 0"/> <!-- child 系原点在 parent 系中的位置 -->
<parent link="base_link"/>
<child link="gimbal_yaw"/>
<axis xyz="0 0 1"/> <!-- 旋转轴,在 PARENT 坐标系中定义 -->
</joint>

整个机器人 = 一棵有向树:每个 link 只能有一个父 joint,不能有环。TF 树就是这棵树运行时发布的结果——rviz 里看到的坐标系就是逐个 <origin> 连乘出来的。关节的旋转只改变 child 系的姿态(绕 parent 系中定义的 axis 旋转),位置不受影响。

rpy 的旋转顺序:为什么是 Rz·Ry·Rx

URDF 的 rpy="roll pitch yaw" 约定为:先绕 X 转 roll,再绕新 Y 转 pitch,最后绕新 Z 转 yaw(内旋),等价于固定轴外旋:

$$R = R_z(\text{yaw})\cdot R_y(\text{pitch})\cdot R_x(\text{roll})$$

注意矩阵乘法的顺序不能换——rpy="0 0 90°" 和 rpy="90° 0 0" 给出的姿态完全不同。KDL/ROS 内部全部用这个约定,写 URDF 和读 TF 时保持同一约定,否则旋转对不上。

万向锁:当 pitch = ±90° 时,roll 和 yaw 的旋转轴重合,丢失一个自由度,欧拉角表示退化(同一姿态有无穷多种 rpy 表示)。URDF 里一般碰不到(关节轴多是单轴),但从 rpy 转四元数、做插值时一定绕开 ±90°。

rpy → 四元数(Rz·Ry·Rx 约定):

$$w = c_y c_p c_r + s_y s_p s_r, \quad x = c_y c_p s_r - s_y s_p c_r$$
$$y = c_y s_p c_r + s_y c_p s_r, \quad z = s_y c_p c_r - c_y s_p s_r$$

($c_\cdot=\cos(\cdot/2), s_\cdot=\sin(\cdot/2)$)。四元数的好处是无万向锁、插值平滑,TF 内部就是用四元数存储姿态的。

传感器坐标系:optical frame 的坑

相机、雷达的坐标系约定和机器人不同(相机 z 轴朝前、y 轴朝下)。URDF 里要加一个 fixed joint 把传感器转到 optical 系,例如相机(绕 X 转 -90°):

1
2
3
4
5
<joint name="camera_optical_joint" type="fixed">
<origin xyz="0 0 0" rpy="-1.5708 0 -1.5708"/>
<parent link="camera_link"/>
<child link="camera_optical_frame"/>
</joint>

忘了这个转换,点云/图像在 rviz 里就是”歪的”,SLAM 建图直接失败。

二、惯量:仿真可信度的根基

基本形状的惯量张量(推导结论)

惯性张量是 3×3 对称矩阵,对角线是绕各轴的转动惯量。常见形状(质量 m 均匀分布):

形状 轴向惯量
长方体 $a\times b\times c$ $I_{xx}=\frac{1}{12}m(b^2+c^2)$,$I_{yy}=\frac{1}{12}m(a^2+c^2)$,$I_{zz}=\frac{1}{12}m(a^2+b^2)$
圆柱(半径 r、长 l) $I_{zz}=\frac{1}{2}mr^2$(绕轴线),$I_{xx}=I_{yy}=\frac{1}{12}m(3r^2+l^2)$
球体 $I=\frac{2}{5}mr^2$
实心长方体绕质心 同上表,非对角项为 0

以圆柱绕轴线的 $\frac{1}{2}mr^2$ 为例,积分思路:把圆柱切成半径 $\rho$ 的圆环,每环质量 $dm = \frac{m}{\pi r^2 l}\cdot 2\pi\rho,d\rho,l$,则

$$I_{zz} = \int_0^r \rho^2 dm = \frac{2m}{r^2}\int_0^r \rho^3 d\rho = \frac{1}{2}mr^2$$

平行轴定理:质心不在原点时

CAD 导出的惯性通常是”相对于质心“的,而 URDF 的 <inertial><origin> 可以指定质心位置。若质心相对 link 原点偏移 $d$,绕 link 原点的惯量要加一项:

$$I = I_c + md^2$$

算例:一根长 0.4m、质量 2kg 的均匀连杆,质心在 link 原点(中点)时 $I_{zz}=\frac{1}{12}ml^2 = \frac{1}{12}\times2\times0.16 = 0.0267,\text{kg·m}^2$;若把 <inertial><origin> 设在端点(偏移 0.2m),则 $I_{zz} = 0.0267 + 2\times0.04 = 0.1067,\text{kg·m}^2$——差了 4 倍。写错质心位置,仿真里的摆动频率、稳定性全部失真。

Fusion360 单位陷阱:×10⁻⁹ 是怎么来的

Fusion360 导出的惯性默认单位是 g·mm²(克·平方毫米),URDF 要求 kg·m²:

$$1,\text{g·mm}^2 = 10^{-3},\text{kg} \times (10^{-3},\text{m})^2 = 10^{-9},\text{kg·m}^2$$

所以所有数值 ×10⁻⁹:原笔记里的 689.2 → 6.892E-07,验证:$689.2\times10^{-9} = 6.892\times10^{-7}$ ✓。忘了换算 = 惯量小了 9 个数量级 = Gazebo 里模型轻如纸、一碰就飞。

惯量错误的表现与自检

症状 原因
模型静止时自己缓慢旋转/漂移 惯量张量非对角项乱填或质心严重偏离
控制增益调大就发散、仿真震荡 惯量比真实小几个数量级(单位没换算)
倒下/碰撞后疯狂抖动 惯量过小 + 碰撞摩擦参数不匹配

自检方法:check_urdf 只查语法,查不了物理;拿已知质量的手动算一遍(如电机转子:M3508 转子约 100g、半径 ~2cm,$I \approx \frac{1}{2}\times0.1\times0.02^2 = 2\times10^{-5},\text{kg·m}^2$),和模型里的数量级对比。注意 URDF 的 <inertial> 缺失时 Gazebo 会用默认密度从几何体估算,算出来的质量与真实材料差几个数量级——每个 link 都要手填惯量,别指望自动。

三、Joint 与 Transmission:驱动链路

joint 类型与 limit 的物理含义

type 自由度 说明
fixed 0 刚性连接(传感器、支架)
revolute 1 有限角度旋转,必须给 limit
continuous 1 无限旋转(轮子)
prismatic 1 直线滑动
floating 6 自由漂浮(无人机、机械臂末端调试)

<limit> 里的 effort 是关节最大力矩(N·m),velocity 是最大角速度(rad/s)——Gazebo 的关节执行器会按这两个值饱和。**effort 不设或设得极大,Gazebo 里就是”大力出奇迹”**,模型会显得异常”暴躁”。

transmission:减速比 N:1 的力矩/速度换算

1
2
3
4
5
6
7
8
9
10
<transmission name="wheel_trans">
<type>transmission_interface/SimpleTransmission</type>
<joint name="wheel_joint">
<hardwareInterface>EffortJointInterface</hardwareInterface>
</joint>
<actuator name="wheel_motor">
<mechanicalReduction>50</mechanicalReduction>
<hardwareInterface>EffortJointInterface</hardwareInterface>
</actuator>
</transmission>

mechanicalReduction = 50 表示电机转 50 圈,关节转 1 圈(减速比 50:1)。物理后果:

$$\tau_{joint} = N\cdot\tau_{motor} = 50\tau_{motor}, \qquad \omega_{joint} = \omega_{motor}/N$$

所以减速箱是”力矩放大器 + 速度衰减器“:M3508 配减速箱后关节侧力矩翻几十倍,但关节最高转速也相应除以减速比——云台/底盘选型时,先定关节需要的峰值力矩和最大转速,再反推电机侧需求,两个约束都要满足。

控制器与硬件接口对应(ros_control):

接口 控制器 说明
EffortJointInterface joint_effort_controller 力矩/电流环
VelocityJointInterface joint_velocity_controller 速度环
PositionJointInterface joint_position_controller 位置环
Joint State joint_state_controller 状态发布

ROS2 里 transmission 写法不变,但接口描述移入 <ros2_control> 标签:

1
2
3
4
5
6
7
8
<ros2_control name="RobotSystem" type="system">
<hardware><plugin>gazebo_ros2_control/GazeboSystem</plugin></hardware>
<joint name="wheel_joint">
<command_interface name="velocity"/>
<state_interface name="velocity"/>
<state_interface name="position"/>
</joint>
</ros2_control>

四、xacro:把几何变成公式

参数化:改一个数,全车跟着变

四轮小车最典型的做法:轮距、轴距、车体尺寸定义为 property,轮子位置用公式算而不是硬编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<xacro:property name="base_length" value="0.4"/>
<xacro:property name="base_width" value="0.25"/>
<xacro:property name="wheel_radius" value="0.05"/>
<xacro:property name="wheel_width" value="0.02"/>

<xacro:macro name="wheel" params="prefix side">
<link name="${prefix}_wheel">
<visual>
<geometry><cylinder radius="${wheel_radius}" length="${wheel_width}"/></geometry>
<origin xyz="0 0 0" rpy="${pi/2} 0 0"/> <!-- 轮轴沿 y 方向 -->
</visual>
<inertial>
<mass value="0.5"/>
<inertia ixx="${0.5*0.5*wheel_radius**2}" ixy="0" ixz="0"
iyy="${0.5*0.5*wheel_radius**2}" iyz="0"
izz="${0.5*0.5*wheel_width**2}"/>
</inertial>
</link>
<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel"/>
<origin xyz="${side*base_length/2} ${base_width/2} ${-wheel_radius}"
rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
</xacro:macro>

<xacro:wheel prefix="front_left" side="-1"/>
<xacro:wheel prefix="front_right" side="1"/>

${} 里支持完整 Python 表达式(**、pi、三角函数)。轮子 y 坐标 = ±轮距/2、z 坐标 = -轮半径——这些”几何关系”写进公式后,改一个 wheel_radius,轮子位置、惯量自动全对,不可能出现”轮子悬空”这类低级错误。

组织多文件

1
2
<xacro:include filename="$(find my_robot)/urdf/common.xacro"/>   <!-- 公共 property/宏 -->
<xacro:include filename="$(find my_robot)/urdf/sensors.xacro"/> <!-- 传感器 -->

宏定义文件里只放 <xacro:macro>,实例化放到主文件——否则重复 include 会重复定义。

五、碰撞与 Gazebo 调参

为什么 STL 碰撞会卡:GJK 的成本

碰撞检测里,球/盒/圆柱是解析几何,一算就出结果;三角网格(STL)要走 GJK 等迭代算法,每帧每个接触点都要迭代,面数越多越慢。规则:

  1. visual 用 STL(好看),collision 用基本体包裹(快)——尺寸略大于真实轮廓(安全边距 1~2mm)
  2. 必须用网格时,用凸包(convex hull)代替原始网格:凸包是 GJK 的最爱,几百个面的凸包比几万个面的凹网格快一个数量级
  3. 单个 STL 控制在 2MB 以内(fusion2urdf 导出的网格记得简化)

摩擦与阻尼:模型为什么打滑/塌陷/乱晃

现象 参数
轮子原地打滑、爬不上坡 <gazebo reference="wheel_link"><mu1>1.0</mu1><mu2>1.0</mu2></gazebo>(mu1/mu2 是两方向摩擦系数,橡胶对地面 0.8~1.2)
关节松垮、模型软绵绵 <gazebo reference="joint_name"><jointDamping>0.1</jointDamping></gazebo>(关节阻尼,模拟轴承/减速器摩擦)
静止时抖个不停 接触刚度 kp/kd 默认值偏”硬”,或惯量过小;先查惯量再调 kp

注意 Gazebo 的关节是”理想执行器+阻尼”模型,没有真实电机的电流环/速度环特性。仿真里调好的 PID 增益直接搬到真机往往太激进——真机有电流饱和、反电动势、机械谐振。反过来,真机参数进仿真也要打折。仿真验证的是运动学/算法逻辑,不是控制参数的绝对值。

六、验证工具链

1
2
3
4
5
6
7
8
9
10
# 1. 语法与树结构
check_urdf my_robot.urdf
urdf_to_graphiz my_robot.urdf && dot -Tpng my_robot.gv -o tree.png

# 2. xacro 展开检查(别拿 xacro 文件直接喂 check_urdf)
xacro my_robot.urdf.xacro > /tmp/my_robot.urdf

# 3. 运动学手动验证:RViz + 关节滑块
roslaunch urdf_tutorial display.launch model:=/tmp/my_robot.urdf
# 拖动滑块,看 child 是否绕 axis 正确旋转、是否有链接错位

常见错误信息解读:

报错 原因 解法
root link ... has an inertia ... KDL does not support KDL 不允许根 link 带惯量(融合建图等工具链要求) 加一个空 dummy link 做根,fixed joint 接 base_link
No link found for ... joint 的 parent/child 拼写不一致 检查 name 严格匹配(区分大小写)
模型在 RViz 里灰白无颜色 缺 joint_state_publisher 或 visual 没给 material 装 ros-noetic-joint-state-publisher,给 <material> 设颜色
Gazebo 里模型爆炸/穿透地面 惯量单位没换算 / 碰撞几何与 visual 不重合 检查 ×10⁻⁹、collision origin
spawner 找不到 controller_manager launch 里用了旧式 spawner 用 controller_manager 的 spawner 节点(或 ROS2 的 spawner 可执行文件)

七、实战:云台与麦轮车建模要点

  1. base_link 是质心:base_link 的 <inertial><origin> 放整车(不含轮子)质心,joint origin 从质心出发——写反了整车的动态就全错
  2. 云台两轴:yaw 关节轴沿 z(axis="0 0 1"),pitch 关节轴沿 y(axis="0 1 0"),且 pitch 的 parent 是 yaw 的 child——树的层级就是机械的层级
  3. 麦轮/全向轮:轮子绕自身轴 continuous 旋转 + 辊子被动轴,碰撞用圆柱近似即可(辊子细节留给 visual)
  4. fusion2urdf 规范:零部件名只用字母数字、单个实体、刚体/旋转/滑动联接、子-父顺序、必须有一个 base_link、参数精确定位不用鼠标拖
  5. STL 缩放:米制模型 scale="0.001 0.001 0.001"(毫米转米),毫米制不用——导出前确认单位,错了整个模型尺寸差 1000 倍
  6. 惯性换算:Fusion360 惯性值全部 ×10⁻⁹,选”相对于质心”的数值

八、踩坑清单(实战总结)

  1. rpy 顺序记反:URDF 是 Rz·Ry·Rx,写 rpy="0 0 90" 和 rpy="90 0 0" 是两种姿态;pitch 接近 ±90° 时小心万向锁
  2. 惯量单位不换算:g·mm² 当 kg·m² 用,模型轻 9 个数量级,Gazebo 直接放飞
  3. 质心偏移忘用平行轴定理:惯量差 4 倍,摆动频率全错
  4. collision 直接用 STL:仿真卡顿,用凸包/基本体包裹
  5. effort/velocity 不设:关节”力大无穷”,控制对比失真
  6. 传感器光学坐标系:不加 fixed 转换,点云歪的、SLAM 建图失败
  7. xacro 硬编码尺寸:改参数要改十几个地方,用 property + ${} 公式
  8. 惯量缺失靠 Gazebo 自动估算:默认密度与真实材料差几个数量级
  9. 仿真 PID 直接上真机:Gazebo 关节没有电流环/谐振,参数要打折重调
  10. 树结构成环:URDF 必须是树,一个 link 只能有一个父 joint