搞机械臂的人应该都有这种经历:明明关节角度看起来正常,末端却跑到不知道什么地方去了。大部分问题出在建模上,更准确地说,出在坐标系没摆明白。做机械臂开发,无论是做仿真、跑运动规划,还是写重力补偿,正运动学都是一切的地基。而 MDH(Modified Denavit-Hartenberg)建模,就是把这个地基打牢的主流方法。这篇文章不绕弯子,直接从坐标系建立讲到正运动学矩阵计算,全程手把手,让你看完就能在自己的机械臂上把 DH 参数表填出来,再用代码算出末端位姿。不管是刚入门的学生,还是做毕设、做 ROS 开发、搞开源机械臂的工程师,这套流程都适用。
1. 机械臂为什么要建模:MDH到底是什么
1.1 运动学建模解决什么问题
机械臂本质上是一串通过关节连接起来的刚体连杆。你给每个关节一个角度,末端工具会到达某个位置、呈现出某个姿态。这个过程要算出来,就需要一套数学规则,把“关节角度”映射成“末端位姿”。正向计算叫做正运动学,反向求解叫逆运动学。不管是哪个方向,前提都是先把机械臂“翻译”成一种机械臂能理解的语言——DH 参数表。可以说,DH 建模是机械臂运动学的通用语言,就像地图上的经纬度一样,把空间里的每个连杆、每个关节都标得清清楚楚。
很多人觉得建模不就是画几个坐标系、填几个参数吗?实战中你会发现,坐标系摆错一点点,算出来的末端位置可能差出整条手臂的长度。而且系统一大,机械臂加上移动底盘、夹爪、传感器,坐标系一多,混乱就来了。MDH 建模的价值,就是用一套严格、统一的规则,把“坐标系应该放在哪、参数应该怎么填”变成可以照着执行的流程。它不解决运动学计算本身的所有问题,但它能保证你从源头不出错。
1.2 SDH 与 MDH:两套“指路规则”的区别
在机器人学教材里,DH 建模其实有两个流派:标准 DH(Standard DH,常简称 SDH)和改进 DH(Modified DH,也就是 MDH)。两个流派都能描述机械臂,但它们描述连杆的方式不一样,坐标系放置的位置也不一样,参数表填出来自然不同。很多新手把两套混在一起用,最后算出来的矩阵五花八门。
简单理解:SDH 把每个坐标系放在连杆的“起点”关节上,描述连杆时先沿 Z 轴转、再沿 Z 轴平移,然后沿 X 轴平移、绕 X 轴旋转。MDH 则把坐标系固连在连杆的“远端”关节上,变换顺序换成了先绕 X 轴旋转、沿 X 轴平移,再绕 Z 轴旋转、沿 Z 轴平移。这个差别看起来只是步骤顺序变了,实际上影响很大:在 MDH 里,连杆自身的几何属性(a 和 α)和关节的运动属性(d 和 θ)分得很清晰,每一根连杆的尺寸是独立描述的,建模树形结构、并联结构、腿部结构这类复杂机构时不容易乱。我自己的体会是,MDH 对新手更友好,因为它的参数定义更贴近“这个连杆本身的形状是什么样的”,而不是“这个关节相对于上一个关节怎么运动”。
1.3 为什么我推荐新手直接学 MDH
如果你去翻 ROS、MoveIt、URDF 相关的工程代码,会发现很多工具链在做 URDF 和 DH 参数互转时,MDH 的兼容性更好。URDF 中每个 joint 的 origin 描述的是 child joint 相对于 parent joint 的位姿,它的参考关系更接近 MDH 的“当前坐标系往下一个关节的远端走”这种思路。所以从实际工程出发,我推荐新手直接学 MDH,然后用 MDH 贯穿整个项目:建模仿真、逆解、轨迹规划、手眼标定,全都用同一套参数,这样能减少很多因为换约定导致的坑。
当然,如果你要复现某篇论文里的结果,而论文用的是 SDH,那就必须严格按论文的参数来。用之前一定先确认清楚,这是建模之前的第一件事。
2. 坐标系建立的完整流程
2.1 建模前需要准备什么
在动笔之前,先把机械臂的几何图纸准备好。无论是 CAD 模型里的尺寸,还是实物上用游标卡尺量出来的数据,你至少需要知道每个关节轴之间的相对位置关系、相邻关节轴之间的夹角,以及各个连杆的长度。没有图纸直接凭感觉填参数,后面百分之百要返工。
我的习惯是先画一张简化的机械臂骨架图:把每个关节轴画成一条直线,把每个连杆画成连接相邻轴的线段。图中要标清楚基座在哪、哪个是关节 1、哪个是关节 2,一直到末端工具。骨架图不要求精度,但一定要把关节轴的方向和位置关系表达清楚,它是后续建立坐标系和填参数的地图。如果是六轴机械臂,建议按照从基座到末端的顺序,把每个关节编号写明白:基座是连杆 0,第一个活动关节是关节 1,末端工具坐标系通常放在连杆 n 的末端。
2.2 Z 轴和 X 轴的确定规则
MDH 的坐标系建立规则可以归纳成几句话。对每个关节 i,先把 Z 轴定下来:旋转关节的 Z 轴沿着旋转轴的方向,移动关节的 Z 轴沿着移动方向。这是最基础的规则。
定完 Z 轴之后,X 轴的方向由相邻两个关节轴决定:X_i 是 Z_{i-1} 和 Z_i 的公垂线方向,更严谨地说,X_i 的方向等于 Z_{i-1} 叉乘 Z_i 得到的方向。这里有个很容易忽略的细节:叉乘的次序不能反,一反对应的是相反方向,后面填 θ_i 的时候就会差 180°。如果相邻两个 Z 轴是相交的,X_i 就取同时垂直于这两个 Z 轴的方向;如果两个 Z 轴平行,公垂线不唯一,这时可以按简化原则选择一组方便计算的方向,通常选在让后续参数更整齐的位置。
Y 轴不需要单独定义,用右手定则:Y_i = Z_i × X_i 自动补全。到这里每个关节的坐标系就成型了。
2.3 坐标系原点怎么放:MDH 的“远端固定”原则
这是新手最容易迷惑的地方。SDH 和 MDH 最核心的差别之一,就是坐标系放在连杆的哪一端。MDH 中,坐标系 {i} 固定在连杆 i 的远端,也就是关节 i+1 所在的位置,这意味着坐标系 {i} 会跟着连杆 i 一起运动。而坐标系 {i-1} 则固定在连杆 i-1 的远端,也就是关节 i 的位置。
拿手臂来打比方:大臂和小臂之间是肘关节。假如大臂是连杆 2,小臂是连杆 3,那么在 MDH 里,大臂坐标系放在肘关节处,小臂坐标系放在手腕关节处。这样每个坐标系描述的都恰好是“这个连杆末端的信息”,变换到下一个连杆时,逻辑就非常顺。很多网上的资料不强调这一点,导致很多人把坐标系画在关节的驱动端,填参数时 a、d 全部对不上。记住这个“远端固定”原则,MDH 就算理解了一半。
3. 手把手填写 MDH 参数表
3.1 四个参数的定义与正负号约定
MDH 的每个相邻连杆变换,用四个参数来描述,它们分别是 α_{i-1}、a_{i-1}、d_i、θ_i。
第一个参数 α_{i-1} 是连杆扭转角,表示绕 X_{i-1} 轴,从 Z_{i-1} 转到 Z_i 所需转过的角度。第二个参数 a_{i-1} 是连杆长度,表示沿 X_{i-1} 轴方向,从 Z_{i-1} 与 X_{i-1} 的交点(也就是坐标系 {i-1} 的原点)到 Z_i 与 X_{i-1} 的交点之间的距离。这两个参数描述的是连杆 i-1 本身的几何属性。第三个参数 d_i 是关节偏距,表示沿 Z_i 轴方向,从 X_{i-1} 与 Z_i 的交点到 X_i 与 Z_i 的交点之间的距离。第四个参数 θ_i 是关节角,表示绕 Z_i 轴,从 X_{i-1} 方向转到 X_i 方向的角度。d_i 和 θ_i 描述的是关节 i 的运动属性,旋转关节的 θ_i 是变量,移动关节的 d_i 是变量。
正负号一律按照右手定则判断:绕 X 轴旋转时,从 Y 轴转向 Z 轴为正;绕 Z 轴旋转时,从 X 轴转向 Y 轴为正。如果方向搞反,后面矩阵里会出现明显的符号错误。我的建议是每一行参数都配上“前一个坐标系的 X 轴方向”和“当前坐标系的 Z 轴方向”对照图,检查正负号就不容易出错。
3.2 从图纸到 DH 表的实操步骤
拿到机械臂图纸后,我一般按下面的顺序填表。第一步,把所有关节轴画出来,标好 Z 轴方向。第二步,对每对相邻关节轴,找出公垂线,确定 X_i 的方向和坐标系 {i} 的原点位置。第三步,逐行量取或计算四个参数:先用三角函数算 α_{i-1},再量 a_{i-1},再沿 Z_i 量 d_i,最后根据两个 X 轴的夹角判断 θ_i。第四步,处理变量:如果是旋转关节,θ_i 这一行填入初始偏置值,变量部分在后面计算时再叠加。
一个比较实用的技巧是,在建坐标系时就把 X_i 的方向选得让 θ_i 的初始值为 0 或 90° 这类容易检查的整数。虽然理论上 X_i 的方向有一定自由度,选得好能让 DH 表看起来清爽很多。因为 X_i 有正反两个方向可选,所以同样一个机械臂,可能摆出两套不同的 MDH 参数,它们描述的是同一个物理结构,但参数值不一样。遇到这种情况别慌,只要矩阵计算结果一致,就说明建模是对的。
3.3 六轴机械臂的完整示例表
下面是一张六轴垂直关节机械臂的示例 MDH 参数表,单位是毫米和度。这张表不是某一款公开机械臂的官方参数,是我根据常见六轴构型整理的演示数据,重点在于展示填表的思路和后续计算流程,实际工程中请用你自己的机械臂图纸数据替换。
| 关节 i | α_{i-1} (deg) | a_{i-1} (mm) | d_i (mm) | θ_i 初始偏置 (deg) |
|---|---|---|---|---|
| 1 | 0 | 0 | 153 | 0 |
| 2 | -90 | 31 | 0 | -90 |
| 3 | 0 | 246 | 0 | 0 |
| 4 | 0 | 182 | 0 | 0 |
| 5 | 90 | 0 | 126 | 0 |
| 6 | -90 | 0 | 91 | 0 |
这张表里,关节 2 的 θ_2 初始偏置是 -90°,这很常见,因为这种垂直关节构型的第二关节在零位时,大臂通常是竖直向上的,为了让 DH 表在零位时末端位置更直观,需要加一个初始偏置。第四列 d_i 在一些行是 0,说明相邻坐标系的原点在 Z_i 方向上没有偏移。每一行的 α 和 a 描述的是上一个连杆的形状,比如第二行的 α=-90° 就表示关节 1 的 Z 轴和关节 2 的 Z 轴之间的夹角是 90 度,方向是负的。
4. 正运动学矩阵计算:从单杆变换到末端位姿
4.1 齐次变换矩阵与“左乘还是右乘”的直觉
正运动学的核心工具是齐次变换矩阵,一个 4x4 的矩阵把旋转和平移打包到一起,形式如下:
旋转部分是一个 3x3 的旋转矩阵,描述了坐标系的姿态偏移;平移部分是一个 3x1 的向量,描述了坐标原点的位置。很多人在矩阵相乘时纠结顺序,其实可以这么理解:从基座到末端,每一步变换都相当于“在当前坐标系下先发生了一次位置变化,然后又发生了一次姿态旋转”,整体结果就是把前一段的累积位姿带到新的位置和朝向上。
类比成走路:你先向前走 1 米,再向右转 90°,然后向前走 0.8 米,最终位置取决于每一步的方向和距离。矩阵乘法就是把这些“方向 + 距离”逐步叠加。顺序错了,相当于先走 0.8 米再转 90 度,到达的位置完全不同。所以从基座到末端,矩阵只能从左往右依次相乘:0T_n = 0T_1 × 1T_2 × … × n-1T_n。这个顺序在代码里就体现为循环里连续做矩阵乘法,千万不能反过来排。
4.2 MDH 相邻变换矩阵公式与含义
MDH 的相邻连杆变换矩阵 i-1T_i 对应四条变换:沿 X_{i-1} 平移 a_{i-1},绕 X_{i-1} 旋转 α_{i-1},绕 Z_i 旋转 θ_i,沿 Z_i 平移 d_i。写成矩阵形式就是:
i-1T_i = RotX(α_{i-1}) × TransX(a_{i-1}) × RotZ(θ_i) × TransZ(d_i)
展开后得到下面的 4x4 矩阵:
| cosθ | -sinθ | 0 | a |
|---|---|---|---|
| sinθ·cosα | cosθ·cosα | -sinα | -sinα·d |
| sinθ·sinα | cosθ·sinα | cosα | cosα·d |
| 0 | 0 | 0 | 1 |
其中 α、a、d、θ 就是 DH 表里的四个参数。这个矩阵的第四列前三个元素就是坐标系 {i} 原点在坐标系 {i-1} 中的位置,上面三行三列则是姿态。这里有个容易混淆的点:d 在矩阵里同时影响第四列的 Y 和 Z 分量,乘上的是 sinα 和 cosα,说明当两轴之间存在扭转角时,沿 Z 轴的平移会被投影到前一个坐标系的 Y 和 Z 方向上。这也是为什么填表时 α 的正负直接影响位置结果。
4.3 Python 实现末端位姿计算
理论讲完,直接上代码。下面是一个用 Python 和 NumPy 实现的 MDH 正运动学计算函数,代码量很短,核心就是挨个生成相邻变换矩阵,然后连乘。
import numpy as np def mdh_transform(alpha_rad, a, d, theta_rad): """ 根据 MDH 参数生成相邻连杆变换矩阵 i-1T_i 参数单位:alpha_rad 和 theta_rad 为弧度,a 和 d 为毫米 """ ct = np.cos(theta_rad) st = np.sin(theta_rad) ca = np.cos(alpha_rad) sa = np.sin(alpha_rad) return np.array([ [ct, -st, 0, a], [st * ca, ct * ca, -sa, -sa * d], [st * sa, ct * sa, ca, ca * d], [0, 0, 0, 1] ]) def forward_kinematics_mdh(dh_params): """ dh_params: 列表,每个元素是 [alpha_deg, a, d, theta_deg] 返回基座到末端的齐次变换矩阵 0T_n """ T = np.eye(4) for alpha_deg, a, d, theta_deg in dh_params: alpha_rad = np.deg2rad(alpha_deg) theta_rad = np.deg2rad(theta_deg) Ti = mdh_transform(alpha_rad, a, d, theta_rad) T = T @ Ti return T # 示例:上一节六轴机械臂的 MDH 表,关节变量全设为 0 dh_params = [ [0, 0, 153, 0], [-90, 31, 0, -90], [0, 246, 0, 0], [0, 182, 0, 0], [90, 0, 126, 0], [-90, 0, 91, 0], ] T_end = forward_kinematics_mdh(dh_params) print("末端位置 (mm):", T_end[:3, 3]) print("末端姿态矩阵:\n", T_end[:3, :3])运行这段代码,得到的就是在零位状态下,末端工具坐标系相对基座坐标系的位姿。第四列前三个元素是 X、Y、Z 坐标,直接拿来和 CAD 模型上的零位坐标做对比,如果对得上,说明 DH 表和工作流程基本没问题。如果这里的运算结果和图纸对不上,不要怀疑矩阵公式,第一时间检查 DH 表和坐标方向即可。
5. 常见问题排查与建模校验实操
5.1 四个高频翻车点
我用 MDH 建模这几年来,见过最多的问题集中在四个地方。第一,SDH 和 MDH 混用。明明题目要求用 MDH,参数表却按照 SDH 的思路填,导致 a、d 和 α 的位置完全错位。解决办法只有一个:动手前先明确约定,并把这个约定写到项目文档的第一行。第二,坐标系“近端”和“远端”搞混。MDH 的坐标系固定在连杆远端,如果你在建模时把坐标系放到了靠近驱动电机的一侧,后面所有参数都会跟着偏移。第三,X 轴方向反了。X_i 是 Z_{i-1} × Z_i 的方向,一旦叉乘顺序写反,相当于坐标系转了 180°,θ_i 就必须加或减 180° 才能补偿,很难看出来。第四,初始偏置没加。像六轴机械臂的关节 2,很多时候零位对应的不是 0°,而是 -90° 或者其他角度,忘加偏置就会导致输出出现一个固定的角度差。
下面把这个几个问题整理成速查表,排查时直接对照。
| 症状 | 可能原因 | 修复思路 |
|---|---|---|
| 末端位置整体反向或差一个固定偏置 | X 轴方向定义反了,或者 θ 偏置忘加 | 检查 Z_{i-1} × Z_i 方向,确认 θ 初始偏置 |
| X、Y、Z 多处对不上 | SDH 和 MDH 混用 | 统一到 MDH 坐标系放置规则 |
| 角度零位对,但位置差一个连杆长度 | a 和 α 填错行 | 核对 a_{i-1} 描述的是连杆 i-1 的几何 |
| 只有单个关节运动时末端轨迹不对 | 该关节的 d 或 θ 定义反了 | 检查该关节 Z 轴方向和变量类型 |
5.2 用零位坐标验证 DH 表
整套建模流程走完后,第一件事就是做零位验证。具体操作是:把机械臂所有关节都转到一个初始零位,通常在机械臂的说明书里会定义零位,这时读取一张标准姿态下的末端坐标;在 CAD 模型里也量出同一个末端坐标;然后把关节变量全部设成 0,用正运动学代码算出末端坐标。三个数一对比,如果偏差在几毫米以内,说明 DH 表基本正确。如果偏差很大,就用上面的速查表逐项排查。
这里有一个很值得养成的习惯:验证时不要只测一个点,要把机械臂转到几个不同姿态分别对比。比如第一组零位,第二组让关节 2 转 90°,第三组让关节 5 转 180°,这样每个关节的旋转轴方向、旋转方向和 DH 表里是否一致都能验证到。只测一个点的结果往往因为偶然因素掩盖了某些参数错误。
5.3 DH 参数不准:偏差校准的思路
建模做完之后,你可能会发现一个问题:机械臂的实机位置和模型算出来的位置总有一个小偏差。这个偏差可能来自机械臂本身的加工装配误差、关节零点标定不准,或者减速器回差。热词里提到的“机械臂偏差”,指的就是这一类问题。当正运动学的计算流程确认无误后,就要考虑把 DH 参数当成待优化变量来校准。
校准的思路很直接:机械臂末端装上测量设备,比如激光跟踪仪、动捕系统,或者简单的千分表加上标准块,让机械臂运动到很多组不同的关节角度,记录下机械臂模型算出的末端位置和实际测量位置。以两者之间的误差最小化为目标,对 DH 参数进行优化求解。这里可以用最小二乘法或者遗传算法,优化变量一般是 α、a、d、θ 的偏置值。注意每次只修改某个关节的参数,会让它的影响体现在所有后续的变换矩阵里,所以最好对整个系统做一次性优化,而不是逐个人工调整。这一步做完,末端的绝对定位精度通常能提升不少,尤其在机械臂抓取、手眼标定这类场景里,效果非常明显。
6. 从建模到应用的几条经验
6.1 模型文件、仿真和实物的对照
建模不是算完正运动学矩阵就结束了,后面还有一连串工作要用到这些参数。比如在 ROS 里做机械臂仿真,URDF 文件里的 joint origin 和 DH 参数之间存在对应关系;用 MoveIt 做运动规划,底层的运动学插件也要用到 DH 参数;做手眼标定,标定板的位姿计算一样离不开坐标变换。我习惯把“MDH 表 + 正运动学代码 + URDF 文件”三样东西放在同一个项目目录下,并写上注释说明它们之间的对应关系。每次更新机械臂结构,三样东西同步修改,避免出现仿真里能规划、实机上却乱跑的情况。
有一个实用经验:在 URDF 中把各个 link 的坐标系画出来,和 MDH 坐标系放在一起对比。URDF 里每个 joint 的 origin 表示 child link 坐标系相对 parent link 坐标系的位姿,这个关系有时和 MDH 表的列并不完全一致,需要把 MDH 参数转成坐标变换再写入 URDF。很多工具可以自动转,但手动转换时一定要画图,不要只靠脑子想。
6.2 给新手的 3 条建议
第一,动手建模前先把 SDH 和 MDH 的定义写清楚,贴在自己工位面前。这不是浪费时间,而是避免陷入“参数填好了但矩阵算出来不对”的泥潭。第二,所有角度统一用角度制记录、弧度制计算,代码里用deg2rad转换,注释里写清楚单位。这个细节能省掉很多调试时间。第三,先做 2R 平面机械臂的小例子,再用自己的六轴机械臂做完整流程。很多人在六轴上反复调不通,其实是因为对基本流程不够熟。拿一个两连杆平面臂走通全流程之后,六轴只是多几行参数而已,并没有本质区别。
我在实际项目中最大的体会是:MDH 建模本身不难,难的是每一步都严格按照约定执行。坐标系画对了,参数填对了,正运动学矩阵就是在几分钟内能算完的事情。把这一层彻底打通,后面无论是做逆解、轨迹规划,还是抓取、强化学习,都会轻松很多。最后再分享一个小技巧:把 DH 表保存成 CSV 文件,代码里直接读取,这样每次改参数不用动代码,校准参数时也方便来回试。机械臂上的事,能做到“参数驱动”就不要“改代码驱动”,这是减少事故的好方法。