发布于 

从 CAD 到数字孪生:URDF 建模与惯性参数校准

每台在 Gazebo 里搭起来的机器人,迟早要犯这三连:整机站立时慢悠悠往下瘫(软绵绵)、关节悬停时自己哆嗦(乱抖动)、轻轻碰一下桌腿就弹射起飞(一碰就飞)。绝大多数人的第一反应是调物理引擎——求解器迭代数、步长、收敛容差挨个试,一圈下来病一分没好。

因为病根不在引擎,在 URDF 里那几个从 CAD 抄错的数字:质量、质心、惯性张量、摩擦。仿真器有句格言:垃圾进,垃圾出——物理引擎只保证把牛顿定律解得足够快,不负责把你拍脑袋填的 mass = 1 变成真的。

这篇当一次会诊,先给一张分诊单,然后每章治一个数:

症状 病根 章节
整机发软、缓缓下沉 质量/惯量不对 二、三
关节悬停时哆嗦 damping 留空为 0 四
一碰就飞、微扰爆炸 惯性全填 1、接触参数没配 三、四
原地转向一颠一颠、里程漂移 轮地摩擦用默认 1.0 四
仿真满分、上真机翻车 传感器零噪声零偏 五

数字全部对上之后,"数字孪生"四个字才敢往外说。

一、URDF/XACRO:先分清皮和骨头

URDF 里一个 <link> 由三段组成:<visual> 是皮,只在 RViz 里渲染,物理引擎看都不看;<collision> 是碰撞体;<inertial> 才是骨头——动力学方程里真正出场的,只有质量、质心、惯性张量三件套。<joint> 决定骨头之间怎么连、怎么动。

很多人建模的顺序是反的:visual 的网格和配色调一晚上,inertial 的六个数字随手全填 1。仿真里受力的是 inertial 不是 visual——皮再漂亮,骨头错了照样抖。一根 Ø30×300 mm 实心铝杆的合格写法:

<link name="lower_arm">
  <visual>
    <geometry>
      <mesh filename="package://robot/meshes/lower_arm.stl"/>
    </geometry>
    <material name="alu_silver"/>
  </visual>
  <collision>
    <geometry>
      <mesh filename="package://robot/meshes/lower_arm_simple.stl"/>
    </geometry>
  </collision>
  <inertial>
    <!-- 质心:杆件均匀,正好在中点 0.15 m 处 -->
    <origin xyz="0 0 0.15" rpy="0 0 0"/>
    <mass value="0.573"/>
    <!-- 6061 铝实心杆:Ixx=Iyy=m(3r²+L²)/12,Izz=mr²/2 -->
    <inertia ixx="0.0043" ixy="0" ixz="0"
             iyy="0.0043" iyz="0"
             izz="0.000064"/>
  </inertial>
</link>

三个字段各自的含义:mass 是质量(kg);origin xyz 是质心在 link 坐标系里的位置——是质心,不是几何中心,带电机端盖的杆两者能差几十毫米;inertia 六个属性是惯性张量的独立分量(第三章展开)。先留两个直觉:izz 比 ixx 小近 70 倍——杆绕自己长轴转很"轻",绕横轴摆很"重";visual 网格几十万面没关系,collision 网格要 Decimate 到千面级,否则物理引擎每步的碰撞检测慢十倍。

URDF 写到第二个机器人,你就会发现自己在复制粘贴、全文搜索替换数字。XACRO 用属性和宏把参数收拢:密度只定义一次,尺寸改一处、全机联动:

<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="six_axis">
  <!-- 材料只定义一次:6061 铝,单位 kg/m³;换碳管只改这一行 -->
  <xacro:property name="density" value="2700.0"/>

  <!-- 一行声明一根杆;宏内部按密度与尺寸
       自动展开质量、质心、惯量六个数 -->
  <xacro:solid_rod name="lower_arm" length="0.3" radius="0.015"/>
  <xacro:solid_rod name="upper_arm" length="0.25" radius="0.013"/>
</robot>

solid_rod 宏内部,mass 与六个惯性分量由表达式按密度、长度、半径实时展开——上面那组实心杆公式直接写进模板,几何一改数字自动跟。以后把铝杆换成碳纤维管,改 density 一个属性,整机质量属性全部重算。参数单一来源,是模型能长期维护的前提。

二、从 CAD 导出:让软件替你算

规则几何体才配套公式,真实零件全是异形:端盖、轴承座、减重孔、走线槽。正路是把 CAD 装配体直接转成 URDF,让 CAD 内核做积分。SolidWorks 的标准工具是 SW2URDF 插件,四步走:

  1. 理约束:把装配体配合关系清干净,每个未来独立运动的 link 对应一个子装配——插件靠同心、重合这类约束推断关节轴线,约束一乱,link 划分跟着乱;
  2. 划 link:在插件界面点选零件归组,逐个指定关节的类型、轴线、限位和力矩上限;
  3. 算属性:插件调用 SolidWorks 的质量属性功能,按各零件的材料密度积分,把每个 link 的质量、质心、惯性张量自动写进 URDF;
  4. 导出:网格两套(visual 高精度、collision 简化)连同 ROS 包结构一次生成。

Fusion 360 那条路结构相同:f3602urdf 脚本加 STL 导出,同样是"装配约束 → link 划分 → 质量属性计算"三板斧,只是自动化程度低一档。

**但有个坑能一口气坑掉 60% 的质量:零件材料没指派。**SolidWorks 的新零件可能压根不带材料,质量属性直接算不出数;而一些导出链路在材料缺失时按 1 g/cm³——水的密度——兜底。铝的密度是 2.7 g/cm³,同一根杆质量差 2.7 倍,惯量跟着错 2.7 倍。导出前逐个零件过一遍材料树,这一步比后面所有校准都便宜。

单位也要留神:SolidWorks 建模常用毫米,URDF 只认米。确认插件版本做了换算,导出后抽一个零件手验一遍质量,五分钟买个安心。

三、惯性张量:杆件里藏着的飞轮

惯性张量是"转动版的质量":质量描述平动惰性,张量描述绕各轴转动的惰性。对角线 Ixx Iyy Izz 是绕三根坐标轴的转动惯量,叉项 Ixy Ixz Iyz 描述质量分布不对称带来的轴间耦合。量级用 I = m × r² 估算:1 kg 质量集中在 0.1 m 半径处,I ≈ 0.01 kg·m²——机械臂杆件的数字绝大多数落在 1e-4 到 1e-2 之间。

最经典的错误:六个数全填 1。1 kg·m² 什么概念?中型机械臂肘关节的实测惯量也就 0.1–0.5 kg·m²,给一条小舵机手臂填 1,等于每个关节焊了一只磨床飞轮。症状很典型:动作像泡在蜜里、PID 增益加到烧也不够硬、力矩限幅一触发整机被顶飞。

张量对几何形状还极其敏感。同样 0.3 m 长、Ø30 mm、0.57 kg:

  • 实心铝杆绕杆轴:izz = ½ × m × r² ≈ 6.4e-5 kg·m²,质量填满圆心;
  • 同外径薄壁管(壁减薄、材料补足同质量):izz = m × r² ≈ 1.3e-4 kg·m²,质量全贴外圆——同样重量,翻倍。

CAD 里一个 link 常由多个零件拼成:杆身、端部电机、减速器座。SW2URDF 按组自动合成;若要手算合并,每个零件都得先过平行轴定理 I' = I + m·d² 搬到共同参考点再相加——这就是别手算、用 CAD 的原因。

正确的数字从 SolidWorks 质量属性面板直接抄:评估 → 质量属性,把"输出坐标系"设为 link 坐标系,面板给的值与 URDF 字段一一对应:

SolidWorks 质量属性面板 URDF 对应字段
质量 <mass value>
重心坐标(相对输出坐标系) <origin xyz>
惯性张量(输出坐标系,含叉项) 六个 i** 属性

面板会同时给两套张量:"主轴张量"(已对角化、叉项为零)和"输出坐标系张量"。URDF 要的是后者——抄成前者,不对称杆件的叉项信息全丢,机器人转到特定姿态会莫名加速。另外 URDF 的惯性必须绕质心表达,若零件相对 link 系有安装转角,确认导出器做了坐标变换。

四、摩擦与阻尼:给运动一点手感

前三个数管"动起来多难",下一类病是"停不下来"。URDF joint 的 <dynamics> 只有两个参数,却最常被整个留空:

<joint name="elbow_joint" type="revolute">
  <parent link="upper_arm"/>
  <child link="lower_arm"/>
  <axis xyz="0 1 0"/>
  <limit lower="-2.3" upper="2.3" effort="30" velocity="2.5"/>
  <!-- damping:粘性阻尼 N·m·s/rad;friction:库仑摩擦 N·m -->
  <dynamics damping="0.08" friction="0.4"/>
</joint>

damping 是粘性阻尼,阻力矩与角速度成正比;friction 是库仑摩擦,方向恒与运动相反的恒定阻力矩。两者为 0 的症状:重力补偿一开,关节受一点扰动就永不停振——真机减速器里的摩擦其实是朋友,它天然替你镇定系统。数量级参考:谐波减速器关节 damping 0.01–0.1、friction 0.1–1,精确值留给第六章实测。顺带一句:<limit> 里的 effort 是控制器力矩限幅,随手填 0 等于把关节直接锁死。

轮子与地面之间的摩擦,URDF 本身管不着(它不定义接触),要靠 <gazebo> 扩展标签:

<gazebo reference="wheel_left_link">
  <mu1>0.6</mu1>              <!-- 切向摩擦系数 1 -->
  <mu2>0.6</mu2>              <!-- 切向摩擦系数 2 -->
  <kp>1000000</kp>            <!-- 接触刚度:轮胎压地多"硬" -->
  <kd>1.0</kd>                <!-- 接触阻尼 -->
  <minDepth>0.001</minDepth>  <!-- 允许最小穿透,抑制高频抖动 -->
</gazebo>

mu1 mu2 是接触点两个正交切向的摩擦系数,kp kd 决定接触软硬。地面摩擦设 1.0 是个流行病:地面默认 1.0、轮子也 1.0,看着"安全",但仿真接触没有胎面形变——差速机器人原地转向时轮子被迫侧向滑动,求解器在静摩擦与动摩擦之间高频切换,机器人一颠一颠往前挪,而里程计以为轮子在匀速滚动,里程悄悄漂移。按真实轮材查表设置:橡胶轮对环氧地坪约 0.5–0.7,别信默认值。

五、传感器噪声:仿真 100 分,真机 60 分

物理参数修完,机器人"看着正常"了,还差最后一块隐形差距:仿真里的传感器不说谎。仿真编码器每一拍都是精确角度,IMU 零偏是完美的 0,相机画面没有噪点。控制器、EKF、LQR 在这种无菌数据上跑 100 分,搬到真机直面噪声与零偏,常常只剩 60 分——差距不在算法,在训练环境太干净。

解法是把噪声建进传感器模型。Gazebo 的 IMU 插件原生支持高斯噪声与零偏:

<gazebo reference="imu_link">
  <sensor name="imu_sensor" type="imu">
    <always_on>true</always_on>
    <update_rate>200</update_rate>
    <imu>
      <angular_velocity>
        <x><noise type="gaussian"><mean>0.0</mean><stddev>0.0002</stddev></noise></x>
        <y><noise type="gaussian"><mean>0.0</mean><stddev>0.0002</stddev></noise></y>
        <!-- bias_mean:上电就存在、EKF 要收敛几十秒的零偏 -->
        <z><noise type="gaussian"><mean>0.0</mean><stddev>0.0002</stddev>
            <bias_mean>0.0004</bias_mean><bias_stddev>0.00002</bias_stddev></noise></z>
      </angular_velocity>
      <linear_acceleration>
        <x><noise type="gaussian"><mean>0.0</mean><stddev>0.015</stddev></noise></x>
        <y><noise type="gaussian"><mean>0.0</mean><stddev>0.015</stddev></noise></y>
        <z><noise type="gaussian"><mean>0.0</mean><stddev>0.015</stddev></noise></z>
      </linear_acceleration>
    </imu>
    <plugin name="imu_plg" filename="libgazebo_ros_imu_sensor.so">
      <ros><namespace>/robot</namespace></ros>
      <frame_name>imu_link</frame_name>
    </plugin>
  </sensor>
</gazebo>

参数从传感器手册抄:角速度与比力的 stddev 按噪声密度乘采样带宽换算;bias_mean 是零偏均值——真机上 EKF 开机要收敛几十秒的那个量,仿真里不建它,你的标定与收敛流程就从未被考验过。

编码器那边,Gazebo 关节状态插件本身不带噪声,量化在接收端补一个十几行的小节点最诚实:

#!/usr/bin/env python3
# 编码器量化:把理想的关节角离散成 14 bit 编码器真正会报告的值
import rclpy
from sensor_msgs.msg import JointState

BITS, TAU = 14, 2 * 3.14159265358

def main():
    rclpy.init()
    node = rclpy.create_node("encoder_quantizer")
    pub = node.create_publisher(JointState, "joint_states_quantized", 10)

    def cb(msg: JointState):
        msg.position = [round(p / TAU * 2**BITS) / 2**BITS * TAU
                        for p in msg.position]
        pub.publish(msg)

    node.create_subscription(JointState, "joint_states_raw", cb, 10)
    rclpy.spin(node)

if __name__ == "__main__":
    main()

14 bit 对应 2π/16384 ≈ 3.8e-4 rad 的角分辨率,再叠上轮系背隙与小高斯抖动,你的里程计融合算法才算见过世面。

六、校准闭环:让数字对上实物

到这里都是"CAD 往仿真搬",但 CAD 自己也会骗人:线束、螺钉、轴承没画进去,密度抄的是标称值,齿轮箱油干脆没算。最后一环是实测对拍:

  • 质量:拆下来上电子秤,分总成称重,和 CAD 明细对账;
  • 质心:两点称重法——整机前后端分别架两个秤,力矩平衡解出质心位置;
  • 惯量:复摆法。杆件水平吊起当摆,测小幅摆动周期 T,绕悬挂点 I = m·g·d·T²/(4π²)(d 为悬挂点到质心距离),再按平行轴定理减 m·d²,得到绕质心的值;
  • 摩擦:恒速电流法。关节以两档低速匀速转动,稳态电流 × 转矩常数 = 库仑摩擦;两档力矩差 ÷ 速度差 = 粘性系数。
参数 实测方法 常见坑
质量 电子秤分总成称重 CAD 漏画线束、螺钉、齿轮油
质心 两点称重力矩平衡 电池实际装配位和 CAD 相反
惯量 复摆周期法 吊具自身惯量忘了扣除
摩擦 恒速电流法 低温下油脂粘度近乎翻倍

然后是迭代:仿真和真机跑同一条轨迹,把关节电流曲线叠在一起对拍——启动段差一截,惯量偏小;稳态段整体偏移,摩擦错了;停机后还在晃,阻尼没建。改一轮跑一轮,把电流误差从 ±30% 压进 ±10%,模型才配叫数字孪生。

七、模型准了之后:通向 HIL

参数可信之后,才敢把真控制器接进仿真:控制板跑真实代码和实时栈,被控对象是校准过的模型——这就是硬件在环(HIL)。这条链路还差两块地基,正好是前两篇:

模型是数字孪生的肉,环境与同步是骨架。三部凑齐,仿真和真机才真正开始互相校准。

参考文献

  1. ROS Wiki. urdf/XML/link. http://wiki.ros.org/urdf/XML/link(link 与 inertial 标签的权威定义)
  2. SolidWorks. SOLIDWORKS2URDF Exporter. https://github.com/solidworks/SOLIDWORKS2URDF(装配体导出流程与质量属性计算)
  3. Open Robotics. Gazebo Classic 教程:Physics Properties. http://classic.gazebosim.org/tutorials(mu1/mu2、kp/kd 等接触参数)
  4. Craig, J. J. 《机器人学导论》(Introduction to Robotics: Mechanics and Control, 3rd ed.)第 6 章,Pearson, 2005.(惯性张量与牛顿-欧拉方程)
  5. ros-simulation. gazebo_ros_pkgs. https://github.com/ros-simulation/gazebo_ros_pkgs(ROS 2 传感器插件与 SDF 噪声标签)