从 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 插件,四步走:
- 理约束:把装配体配合关系清干净,每个未来独立运动的 link 对应一个子装配——插件靠同心、重合这类约束推断关节轴线,约束一乱,link 划分跟着乱;
- 划 link:在插件界面点选零件归组,逐个指定关节的类型、轴线、限位和力矩上限;
- 算属性:插件调用 SolidWorks 的质量属性功能,按各零件的材料密度积分,把每个 link 的质量、质心、惯性张量自动写进 URDF;
- 导出:网格两套(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)。这条链路还差两块地基,正好是前两篇:
- ROS 2 工业级环境构建:Docker 容器化与 PREEMPT_RT 实时内核——仿真宿主与实时内核如何在一台工控机上共存;
- 多传感器硬件时间同步:触发电路与编码器接口——对拍用的真机数据,时间戳先得可信。
模型是数字孪生的肉,环境与同步是骨架。三部凑齐,仿真和真机才真正开始互相校准。
参考文献
- ROS Wiki. urdf/XML/link. http://wiki.ros.org/urdf/XML/link(link 与 inertial 标签的权威定义)
- SolidWorks. SOLIDWORKS2URDF Exporter. https://github.com/solidworks/SOLIDWORKS2URDF(装配体导出流程与质量属性计算)
- Open Robotics. Gazebo Classic 教程:Physics Properties. http://classic.gazebosim.org/tutorials(mu1/mu2、kp/kd 等接触参数)
- Craig, J. J. 《机器人学导论》(Introduction to Robotics: Mechanics and Control, 3rd ed.)第 6 章,Pearson, 2005.(惯性张量与牛顿-欧拉方程)
- ros-simulation. gazebo_ros_pkgs. https://github.com/ros-simulation/gazebo_ros_pkgs(ROS 2 传感器插件与 SDF 噪声标签)