简介DH2URDF是一款面向机器人开发者与ROS用户的实用工具核心功能是将Denavit-Hartenberg参数表自动转换为URDF统一机器人描述格式并在对应坐标系上生成圆柱体轴模型帮助用户快速搭建完整的机器人描述文件。压缩包共9个文件大小约138KB包含4个JavaScript脚本、1个HTML演示页面、1个URDF示例、1个litcoffee源码说明及文档等其中脚本覆盖数据转换、数学运算、模板渲染与XML美化等功能结构清晰便于直接部署或二次开发。目前已有1049人学习下载。通过内置的转换脚本与blank_robot.urdf基础模板配合可交互的网页演示用户可以在浏览器中查看生成的URDF结构并调节各关节值直观理解DH参数与URDF的对应关系。该工具适合需要从DH表出发构建机器人模型的初学者也方便ROS开发者快速生成初始URDF骨架后再细化碰撞与惯性参数整体实用性和可读性都很强。1. DH表到URDF绕不开的坐标系转换手里有一张写好的DH参数表想把它变成URDF这事听起来就是“照着填数据”实际操作却经常让人怀疑人生。DH表是一套极简的运动学速记URDF想要的是另一套坐标系描述中间隔着旋转顺序、单位、关节类型好几个坑。我见过不少开发者包括当年的我兴冲冲把DH表的四个参数抄进URDF结果模型加载出来link乱飞、关节反转、甚至整个机械臂折成直角。这篇笔记用JavaScript写一个DH2URDF转换脚本说清楚DH参数到底落在URDF的哪个字段踩坑点在哪怎么验证结果没做错。适合写机器人驱动、搭仿真模型、或者被运动学正解搞到头秃的开发者。2. DH参数与URDF的映射逻辑先搞清楚四个量去哪了2.1 标准DH与改进DH先分清你手里是哪张表DH表不是只有一种。最常见的是标准DHSDH和改进DHMDH两套约定它们对“坐标系挂在哪个关节上”的定义不一样。SDH把坐标系i固定在link i的远端而MDH把坐标系i固定在link i的近端。同一个机械臂用两套约定写出来的参数完全不一样转URDF的结果也不一样。我自己的习惯是拿到表先问一句你这张表是哪本书、哪个讲义来的如果对方说不上来就看参数特征。SDH的变换顺序通常是先从上一坐标系沿x轴平移a、绕x轴转alpha再沿当前z轴平移d、绕z轴转thetaMDH则是先绕z轴转theta、沿z轴平移d再沿x轴平移a、绕x轴转alpha。这个顺序直接决定了你把参数填进URDF之前构造变换矩阵时谁乘谁在前。很多转换工具默认只支持SDH你拿一张MDH表硬套结果就是模型在仿真里反着转。所以代码里我一般会把变换顺序做成可配置的至少留一个参数说明当前用的是哪种约定。不要觉得这是小事我见过有人在MDH表上按SDH顺序算折腾一晚上最后只是把乘的顺序换了一下就对了。2.2 URDF的joint origin与axistheta为什么不写进originURDF里一个link的位姿是靠joint来表达的joint下面有origin和axis两个关键字段。origin是child link坐标系在parent link坐标系里的固定变换用xyz和rpy描述axis是关节的运动轴用[x,y,z]描述。这是理解DH转URDF的核心DH里的a、alpha、d是固定几何量而theta是关节变量对旋转关节来说。很多人把theta也写进origin的rpy这是不对的。如果theta是变量URDF里的joint本身会动你再把theta写死在origin里等于动了两次。正确的做法是把theta0时的固定姿态写进origin关节运动交给axis和限位去表达。如果你的DH表里theta给的不是0比如机械臂有一个初始关节角度那就把这个初始角度当作零位偏置转成rpy的一部分写进origin。还有一个细节URDF里origin的rpy是绕固定轴的旋转也就是说先绕x轴转roll再绕y轴转pitch最后绕z轴转yaw。DH变换里绕x轴和绕z轴的两次旋转是复合关系不能直接把alpha填进roll、把theta填进yaw了事。必须先把旋转矩阵算出来再从矩阵里拆出rpy。2.3 从DH变换矩阵到xyz与rpy一个手算例子之前的思路是把DH的四个参数构造成齐次变换矩阵提取平移向量做xyz提取旋转子矩阵拆rpy。拿SDH的标准顺序举例从父坐标系到子坐标系的变换矩阵是T RotX(alpha) * TransX(a) * RotZ(theta) * TransZ(d)展开后平移部分的三维向量是x a y -d * sin(theta alpha) z d * cos(theta alpha)等等这个公式有前提——它依赖教科书里“alpha、a、 d、theta”各自的正方向定义。不同教材对alpha的符号约定有细微差异直接背公式容易翻车。我个人的习惯是不背这个展开式而是直接用矩阵乘法代码算谁乘谁按你手里那本教材的约定来。矩阵是黑匣子但至少它不会记错符号。手算验证的时候拿单link去算假设alpha0、a0.5米、d0.2米、theta0rad代入矩阵得到xyz[0.5, 0, 0.2]旋转矩阵是单位矩阵rpy[0,0,0]。如果算出来的y方向有值说明你的alpha或者d的符号反了。这个手算例子是我每次写完转换脚本之后必跑的测试用例成本一分钟能拦住后面一整晚的调试。3. 用JavaScript实现DH2URDF核心类与转换脚本3.1 先把DH表建模成JavaScript数据结构写转换脚本之前先把DH表的结构定义清楚。我习惯把一行DH参数映射成一个对象字段名直接用a、alpha、d、theta外加几个URDF需要的元信息joint名、parent link名、child link名、关节类型、限位。// 一行DH参数 URDF关节元信息 const dhRow { a: 0.5, // 连杆长度单位米 alpha: Math.PI / 2, // 连杆扭转单位弧度 d: 0.2, // 连杆偏距单位米 theta: 0, // 关节角零位值单位弧度 jointType: revolute, // 关节类型revolute / continuous / fixed / prismatic jointName: joint2, parentLink: link1, childLink: link2, limitLower: -Math.PI, limitUpper: Math.PI, axis: [0, 0, 1] // 旋转轴DH约定下通常绕z轴 };逻辑说明alpha和d是固定量theta是变量但在DH表里通常给一个初始值转换时把它当作零位姿态。a的单位必须统一成米URDF不认毫米。轴的方向默认是[0,0,1]因为标准DH约定旋转绕当前z轴。如果你手里的DH表用的是改进DH这个axis可能需要调整。参数说明jointType为“prismatic”时DH里的d是变量、theta是常数转换逻辑要反过来处理axis仍为[0,0,1]但运动是平移。这一步容易搞混建议在代码里分开处理平移关节和旋转关节。3.2 旋转矩阵转RPY先算矩阵再拆角URDF写rpy时用的是固定轴X-Y-Z顺序也就是先绕固定x轴转、再绕固定y轴转、最后绕固定z轴转。不要试图用欧拉角直接拼正确做法是把旋转矩阵算出来然后用下面的函数拆成rpy。function rpyFromMatrix(R) { // R 是 3x3 旋转矩阵按行存储 const pitch Math.atan2(-R[2][0], Math.sqrt(R[0][0] ** 2 R[1][0] ** 2)); if (Math.abs(pitch) Math.PI / 2 - 1e-6) { // 万向锁直接取 roll0yaw 从矩阵里算 const yaw Math.atan2(-R[0][1], R[1][1]); return [0, pitch, yaw]; } const roll Math.atan2(R[2][1], R[2][2]); const yaw Math.atan2(R[1][0], R[0][0]); return [roll, pitch, yaw]; }逻辑说明先算pitch因为固定轴RPY顺序下pitch等于绕y轴的转角可以用R[2][0]直接求。再判断是不是接近90度接近时会丢一个自由度这是欧拉角的通病。最后分别用atan2提取roll和yaw。注意JavaScript里Math.atan2(y, x)的第一个参数是y。参数说明这个函数不关心DH定义只关心URDF的rpy约定。任何来源的旋转矩阵只要你想写进URDF都可以走这个函数。我在转换脚本里所有关节的rpy都经过它不在任何地方手动拼角度字符串。格式化输出时记得把弧度转成小数URDF可以直接写小数但保留六位小数比较稳妥。3.3 完整转换脚本DH表输入URDF字符串输出下面把完整流程串起来。输入是一个DH表数组输出是一段URDF的link和joint定义字符串。这里用齐次矩阵乘法构造变换避免手推公式出错。function rotX(a) { return [[1,0,0,0],[0,Math.cos(a),-Math.sin(a),0],[0,Math.sin(a),Math.cos(a),0],[0,0,0,1]]; } function rotZ(t) { return [[Math.cos(t),-Math.sin(t),0,0],[Math.sin(t),Math.cos(t),0,0],[0,0,1,0],[0,0,0,1]]; } function transX(x) { return [[1,0,0,x],[0,1,0,0],[0,0,1,0],[0,0,0,1]]; } function transZ(z) { return [[1,0,0,0],[0,1,0,0],[0,0,1,z],[0,0,0,1]]; } function matMul(A, B) { const C Array.from({length: 4}, () new Array(4).fill(0)); for (let i 0; i 4; i) for (let j 0; j 4; j) for (let k 0; k 4; k) C[i][j] A[i][k] * B[k][j]; return C; } function extractPose(T) { const xyz [T[0][3], T[1][3], T[2][3]]; const R [T[0].slice(0,3), T[1].slice(0,3), T[2].slice(0,3)]; const rpy rpyFromMatrix(R); return { xyz, rpy }; } function dhRowToUrdf(row, prevT) { // 按SDH顺序构造变换RotX(alpha) → TransX(a) → RotZ(theta) → TransZ(d) let T matMul(rotX(row.alpha), transX(row.a)); T matMul(T, rotZ(row.theta)); T matMul(T, transZ(row.d)); const { xyz, rpy } extractPose(matMul(prevT, T)); // 拼出 joint 的 origin 字符串 const originStr xyz${xyz.map(n n.toFixed(6)).join( )} rpy${rpy.map(n n.toFixed(6)).join( )}; return { T, originStr }; }逻辑说明prevT是父坐标系累积变换。对每一个joint先计算当前DH段相对父坐标系的变换T再乘上prevT得到全局位姿这样URDF里每个joint的origin都直接写成相对父link的位姿。注意由于URDF天然是树状结构这里的prevT其实只需要父link自己的origin不需要从基座一直累积。但累积计算有一个额外的好处能顺便输出每个link的全局位姿用来对运动学正解做校验。参数说明打印时用toFixed(6)避免浮点误差带来的一长串小数。如果你的DH表用了MDH顺序把dhRowToUrdf里四行matMul的调用顺序换成RotZ(theta) → TransZ(d) → TransX(a) → RotX(alpha)即可。这个脚本只生成joint片段link的视觉和碰撞模型建议单独写不要从DH表自动生成。4. 避坑DH表转URDF时最容易翻车的细节4.1 方向反了alpha正负号与右手定则现象生成的URDF在仿真里加载成功但转动第一个关节后面的link朝相反方向偏转或者明明该往上摆的连杆往下沉。原因DH表里的alpha是绕x轴的旋转正方向必须按右手定则判定。画图时很容易把“从y轴转向z轴”的方向看反。另外不少教材的DH参数截图里alpha标的是锐角没有标注正负号抄表时直接把负号丢了。解决转换脚本跑完之后拿单link做手算验证。alpha0、a0.5、d0.2时xyz必然是[0.5, 0, 0.2]。如果y方向不是0先检查alpha再检查d的符号。从那以后我每次转换都先跑这段单link用例。4.2 角度乱了URDF的rpy是固定轴旋转现象URDF里写rpy0 1.5707 0模型加载出来link的姿态跟预期完全对不上旋转轴明显不对。原因rpyFromMatrix拆出来的roll/pitch/yaw是固定轴X-Y-Z外旋的表示不是数学课上学的那种欧拉角内旋。直接用DH里alpha当roll、theta当yaw拼进rpy等于把两次旋转的复合写成了两次独立旋转结果必然错。解决所有rpy一律从旋转矩阵拆禁止手工拼接。我在rpyFromMatrix函数里加了万向锁分支避免pitch接近正负90度时出现NaN。如果拆出来的rpy数值特别怪异先打印旋转矩阵看看正交性是否成立。4.3 单位炸了毫米当成米现象URDF加载后整个模型尺度大得离谱或者碰撞检测时零件之间隔着一大截空气。原因机械臂厂家给的DH表最常见单位是毫米而URDF里长度单位强制要求米。有人把a500直接填进去模型瞬间变成500米高。解决转换脚本入口处统一做单位换算所有数值乘0.001。我给dhRow加了一个units字段默认m填mm就自动换算。URDF里不要出现单位后缀xyz的值就是纯小数。4.4 关节瘫了joint类型和限位没配对现象模型加载正常但某个关节在仿真里完全转不动或者转起来停不下来。原因把本应是revolute的关节写成了fixed或者写了revolute但limit里面lower和upper一致等于焊死又或者用了continuous类型的关节仿真里PID控制时它一直朝一个方向转下去。解决DH表里theta是变量就是revolutetheta固定就是fixed。连续旋转关节只适合理论上无限旋转的轴大多数工业机械臂不是应该用revolute加限位。限位的上下值先用DH表里的机械限位没有就按正负pi。4.5 父子反了模型折叠和坐标系倒挂现象URDF看起来每个joint都对但整体结构像折纸一样叠在一起link之间互相穿透。原因URDF里joint的parent必须靠近基座child必须远离基座。DH表描述的是坐标系i到坐标系i1的变换有人把parent和child写反了变换方向反了模型自然折叠。解决生成joint时强制约定parentLink是当前行对应的父linkchildLink是下一行对应的子link。六轴机械臂从基座往上数joint1的parent是base_link。检查方法把每个joint的origin打印出来xyz的z值应该随着往末端走逐渐增大如果出现负增长大概率父子关系反了。5. 进阶让转换脚本真正落地到你的机器人工作流5.1 批量生成多关节机械臂的URDF单个joint的转换跑通后把整个DH表循环一遍就能生成完整机械臂。这里有一个通用做法把link的视觉模型和碰撞模型单独维护不要跟DH表混在一起。function dhTableToUrdf(dhTable) { let prevT [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]; let out ; for (const row of dhTable) { const { T, originStr } dhRowToUrdf(row, prevT); out joint name${row.jointName} type${row.jointType}\n; out origin ${originStr}/\n; out parent link${row.parentLink}/\n; out child link${row.childLink}/\n; out axis xyz${row.axis.join( )}/\n; if (row.jointType revolute || row.jointType prismatic) { out limit lower${row.limitLower} upper${row.limitUpper} effort10 velocity1/\n; } out /joint\n; prevT T; } return out; }逻辑说明代码里prevT每循环一次就更新这样即便DH表不是严格的链式结构只要你能保证遍历顺序是从基座到末端输出就正确。对于树状结构的机器人也就是一个父link下面挂多个子link的情况不能简单用这个循环需要先对DH表做拓扑排序。5.2 转换后的验证三步走脚本写完别急着拿去用先跑三遍验证。第一步是手算单link的xyz和rpy比对代码输出。第二步是把生成的URDF加载进仿真器逐个关节手动转动确认每个link的运动方向跟直觉一致。第三步是拿运动学正解对一下把每个关节设成特定角度用正解算末端位置再跟你在仿真里读到的末端位姿对比误差应该在毫米以内。这三步看起来废话实际救了我很多次。有一次第二步就发现第三个关节方向反了回去查表发现alpha少了个负号还有一次第三步发现末端坐标差了好几毫米最后定位到是在拼接URDF时把两个link的origin重复写了一遍。从那以后我每次转换DH表都强制自己按这三步走一遍尤其是第一步单link手算成本不到一分钟却能挡住绝大多数低级错误。希望帮到你也欢迎你带着项目里具体的DH表来交流。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?