做机械臂开发的人,十有八九第一次算正运动学时都被坐标系绕晕过。我头一回照着论文里的DH表建模,对着一个六轴机械臂建了两天,末端位置算出来差出十万八千里,最后发现是Z轴方向画反了。今天这篇是机械臂速成系列的第八篇,专门讲标准DH法实战:从连杆坐标系的建立,到运动学方程的组装,一步一步带你把正运动学跑通。这套方法无论你是用AR3、UR5e、松灵Piper这类现成机械臂,还是自己3D打印一个六轴来玩,都是绕不开的第一道坎。文章不会有太复杂的数学推导,但该有的细节和坑一个不少,适合刚入门、想搞懂运动学方程到底怎么来的同学,也适合已经会用现成库但没自己手推过一遍的老哥们。
1. 标准DH法到底在算什么?先把思路捋直
1.1 一个让你事半功倍的视角:连杆不是关节,坐标系才是本体
我先说一个大家容易犯的错误:学DH法的时候,满脑子都是“我这根杆多长、那个关节能转多少度”,这是站在机械结构的角度看问题。但DH法真正的视角是“坐标系”。每个连杆都绑一个坐标系,然后通过坐标系之间的相对变换来描述运动。
什么是“绑一个坐标系”?你可以把每个连杆想象成一块底板,底板上画了三个互相垂直的箭头,代表X轴、Y轴、Z轴。机械臂动起来的时候,每个连杆的姿态变了,底板上箭头指向也跟着变。运动学要回答的问题是:已知每个关节转了多少度,末端那块底板上的箭头相对于基座底板指向哪、原点在哪。
所以机械臂正运动学本质上是一条“坐标系链”:基座坐标系{0}到连杆1坐标系{1},{1}到{2},一直传到最后末端坐标系{n}。每个相邻变换只依赖四个参数,也就是一张DH参数表就能把一个六轴机械臂的全部几何关系描述完。我以前总觉得连杆参数表已经够清楚了,后来发现这是错觉。比如一根斜着的连杆,你光知道长度和角度,很难直观推出它在三维空间里的位姿,但放到坐标系变换里,只要按规则建系、填参数、矩阵连乘,答案就很稳定。
这种视角的优势是“模块化”。不管机械臂是四轴、六轴还是七轴,代码里都是同一个变换函数,只是参数表长度不同。当年我自己手写正运动学时,靠的就是这个思路,先把一根连杆的变换矩阵写对,后面全是体力活。
1.2 标准DH参数表:四个参数说清所有几何关系
标准DH法给每个相邻坐标系之间的变换准备了四个参数,分别是θ_i、d_i、a_i、α_i。我习惯用“谁和谁的关系”来记,而不是死背定义。以从坐标系{i-1}到坐标系{i}的变换为例:
- θ_i:绕Z_{i-1}轴,从X_{i-1}转到X_i的角度,通常是关节变量。
- d_i:沿Z_{i-1}轴,从X_{i-1}到X_i的距离,也就是两条X轴在Z_{i-1}方向上隔了多远。
- a_i:沿X_i轴,从Z_{i-1}到Z_i的距离,通常就是连杆长度。
- α_i:绕X_i轴,从Z_{i-1}转到Z_i的角度,通常反映相邻两个关节轴线的扭转角。
这四个参数里,θ_i和d_i描述的是“绕Z轴转了多少、沿Z轴平移多少”,a_i和α_i描述的是“下一个坐标系的X轴方向和旋转”。你可以把一根连杆想象成一根弯管,两端各有一个轴承座,a_i是弯管两头之间的直线距离,α_i是两端轴承孔中心线之间的夹角,d_i则是前一个关节轴方向上的错位量。
为什么需要四个参数而不是两个?因为三维空间里两个坐标系之间的相对关系一共有六个自由度,但DH法通过精心选择坐标系的建立方式,把其中两个自由度“藏”进了坐标系定义规则里。剩下的四个参数足以完整描述相邻连杆关系,这也是DH法能被广泛使用的根本原因。
1.3 标准DH和改进DH:别等建完坐标系才发现表错了
这里必须给新手提个醒。你搜DH法,会看到标准DH(也叫经典DH)和改进DH(也叫Craig DH)两种,长得像但坐标系约定不一样。我们这次实战用的是标准DH,也就是变换顺序为“绕Z旋转 → 沿Z平移 → 沿X平移 → 绕X旋转”。改进DH的顺序则是“沿X平移 → 绕X旋转 → 沿Z平移 → 绕Z旋转”。
为什么会有这种区别?因为坐标系放的位置不一样。标准DH把坐标系{i-1}的Z轴放在第i个关节轴线上,坐标系{i}的Z轴放在第i+1个关节轴线上;改进DH把坐标系{i}的Z轴放在第i个关节轴线上,相当于坐标系整体“向后错了一位”。这就导致同一个机械臂,用标准DH和用改进DH填出来的参数表可能完全不同。
| 对比项 | 标准DH(本文用) | 改进DH / Craig DH |
|---|---|---|
| 坐标系放置 | 坐标系{i-1}的Z轴沿关节i轴线 | 坐标系{i}的Z轴沿关节i轴线 |
| 变换顺序 | Rot(z,θ) → Trans(z,d) → Trans(x,a) → Rot(x,α) | Trans(x,a) → Rot(x,α) → Trans(z,d) → Rot(z,θ) |
| 常用下标 | 第i行写θ_i, d_i, a_i, α_i | 第i行写θ_i, d_i, a_{i-1}, α_{i-1} |
| 常见场景 | 经典教材、传统工业机器人资料 | Craig《机器人学导论》、ROS里不少URDF模型 |
如果你只是照着别人的机器人抄DH表,一定先确认对方用的是哪一种。我在实际项目里踩过一次坑:从某个开源六轴机械臂工程里拿到一张看起来很像样的DH表,直接用标准DH矩阵去套,结果末端旋转方向怎么调都不对,后来发现那份代码用的是改进DH。所以不是“哪个DH是对的”,而是“你自己的程序按哪种DH写的,参数表就要匹配哪种”。
2. 动手建立连杆坐标系:六轴机械臂一步一步来
2.1 Z轴的摆放原则
建坐标系的第一步,是先把每个关节的Z轴方向定下来。对于第i个旋转关节,Z_{i-1}轴就沿着第i个关节的旋转轴方向。这里的方向可以自己选,但选完就不要再改,不然后面所有参数都会跟着乱。
基座坐标系{0}的Z0,一般让机器人“立起来”的方向为正,也就是第一个关节竖着的转轴方向向上。比如一台典型的六轴工业机械臂,底座关节绕竖直方向旋转,那Z0就竖直向上。后面每个关节轴方向,要根据机械结构实际姿态来画。画图时我的习惯是把机械臂侧视图先画出来,标好每一个关节轴线,再在这个图上加坐标系,这样不容易漏。
有一个常见问题:末端坐标系{n}的Z轴怎么放?标准DH里,Z_n理论上可以任意选择,因为末端之后没有下一个关节了。实际中我建议先把最后一个关节的轴线方向作为Z_{n-1},然后末端坐标系Z_n沿工具方向(比如法兰中心向外),这样后续加装夹爪、视觉相机时可以直接用工具坐标系,思维负担小一点。
2.2 X轴和原点的确定
定好Z轴后,X轴按公垂线规则来:X_i沿Z_{i-1}与Z_i的公垂线方向,从Z_{i-1}指向Z_i。所谓公垂线,就是一条同时垂直两条异面直线的线段,它的方向就是两条Z轴之间最短距离的方向。这个方向定了,X_i的方向也就定了。
如果Z_{i-1}和Z_i是相交的,公垂线长度为0,X_i方向通常取与两者都垂直的方向,让X_i、Z_{i-1}、Z_i满足右手定则。如果Z_{i-1}和Z_i是平行的,公垂线有无数条,你可以任选一条;但选定之后,往往会影响相邻的d_i或a_i数值,所以最好选一条看起来最“自然”的,例如过前一个坐标系原点的那条公垂线,这样很多d_i会变成0,计算省事。
原点位置也顺带确定了:坐标系{i}的原点就是X_i轴与Z_i轴的交点。注意,不是X_i和Z_{i-1}的交点,而是X_i这条公垂线和Z_i的交点。这个细节我当年搞反过,导致原点位置全错了。如果你发现某一行参数填完,相邻变换矩阵里的平移向量和几何结构怎么也对不上,大概率就是原点取错地方了。
2.3 实例:一个6R垂直关节型机械臂的完整建模
为了实战,我建一个典型6R垂直关节型机械臂的结构:底座旋转,然后大臂俯仰、肘部俯仰,再往后是腕部旋转、腕部俯仰、末端旋转。这种构型很常见,很多六轴教学机械臂和工业机械臂都是这个路子。
先按前面的规则把Z轴方向捋一遍:Z0竖直向上,Z1水平(关节2轴线),Z2水平且与Z1平行(关节3轴线),Z3为腕部关节4轴线,Z4为关节5轴线,Z5为关节6轴线。按标准DH规则建立坐标系,并做一定的零位约定后,得到的DH参数表如下:
| i | θ_i(变量) | d_i(m) | a_i(m) | α_i(°) |
|---|---|---|---|---|
| 1 | θ1 | 0.1 | 0 | 90 |
| 2 | θ2 | 0 | 0.4 | 0 |
| 3 | θ3 | 0 | 0.35 | 0 |
| 4 | θ4 | 0.1 | 0 | 90 |
| 5 | θ5 | 0 | 0 | -90 |
| 6 | θ6 | 0.1 | 0 | 0 |
我来逐个解释参数对应的结构含义:d1=0.1是基座到肩关节的高度;a2=0.4是大臂长度;a3=0.35是小臂长度;d4=0.1是腕部到肘部的偏置距离;d6=0.1是末端法兰中心到工具坐标系原点的距离。α1=90°表示Z0到Z1轴线相互垂直;α2和α3为0,说明关节2、3轴线平行;α4=90°和α5=-90°描述腕部两个轴线的扭转关系。
这里要说明一下:我给的这组参数是简化的理想模型,不一定对应某台真实机械臂的完整CAD模型。真实机械臂的DH表往往还会有许多微小偏置,比如关节2和3之间的坐标系原点不一定刚好在关节轴心上,但这些偏置在运动学原理上完全一样,只是参数稍多一点。学习阶段先用简化模型把流程跑通,后面再填真实参数就顺了。
2.4 新手最容易犯的三个错误
第一个错误是平行关节的X轴方向乱取。像关节2和关节3,Z1∥Z2,公垂线不唯一。如果你把X2的方向取反,对应a2的符号也会变,同时相邻行的d和θ都可能受影响。只改一半,坐标就会翻转。我的建议是画好图后,每个X轴方向都用叉积验一遍:X_i方向必须垂直于Z_{i-1}和Z_i,而且从Z_{i-1}指向Z_i,别凭感觉。
第二个错误是Z轴方向正反搞错。Z轴方向反了,α_i的正负号就会差180°,矩阵模型会有非常明显的错误。比如某个关节明明应该顺时针转,末端却往逆时针跑。遇到这种问题,不要先怀疑代码,先回头检查Z轴画的方向。
第三个错误是基座坐标系和末端坐标系没有约定好。基座坐标系{0}的方向会影响最终读到的末端位置;末端坐标系{n}的方向会影响姿态矩阵怎么解读。尤其是后面要加装夹爪、相机,如果没分清楚“法兰坐标系”和“工具坐标系”,手眼标定的时候会被绕到怀疑人生。建议在项目一开始就定义一个统一约定,比如基座坐标系X指向前方,Z向上,末端工具坐标系Z沿工具伸出方向。
3. 从坐标系到运动学方程:相邻变换矩阵的推导
3.1 四次基本变换的顺序为什么不能乱
标准DH的相邻连杆变换矩阵T_i,可以拆成四个基本变换:
T_i = Rot(z, θ_i) * Trans(z, d_i) * Trans(x, a_i) * Rot(x, α_i)
这四个变换是“在变换后的当前坐标系下”依次执行的:先绕当前Z轴旋转θ,再沿当前Z轴平移d,再沿当前X轴平移a,最后绕当前X轴旋转α。因为矩阵乘法是右乘,所以每次新变换都作用在已经变换完的坐标系上。
我说一个你们肯定遇到过的类比:先向右转90度再往前走两步,和先往前走两步再向右转90度,结果完全不一样。DH变换顺序同理。你要是把Trans(x,a)和Rot(z,θ)调换位置,得到的矩阵几乎肯定不对。所以我写代码时从不用记忆的矩阵公式,而是每次都用四个基础矩阵相乘来生成,一是能减少公式记忆错误,二是以后换成改进DH也容易改。
3.2 标准DH矩阵长什么样
把上面四个基本变换乘起来,就得到标准DH的4×4齐次变换矩阵:
[ \begin{bmatrix} \cos\theta_i & -\sin\theta_i\cos\alpha_i & \sin\theta_i\sin\alpha_i & a_i\cos\theta_i \ \sin\theta_i & \cos\theta_i\cos\alpha_i & -\cos\theta_i\sin\alpha_i & a_i\sin\theta_i \ 0 & \sin\alpha_i & \cos\alpha_i & d_i \ 0 & 0 & 0 & 1 \end{bmatrix} ]
这个矩阵看着复杂,其实结构很清晰:左上3×3部分是旋转矩阵,右上3×1部分是平移向量,最后一行固定为[0,0,0,1]。旋转矩阵描述坐标系{i}相对{i-1}的三个轴方向,平移向量描述坐标系{i}原点在{i-1}里的位置。当α_i=90°时,矩阵里很多项会变成0,计算量会小很多,这也是为什么很多机械臂关节垂直相交时DH表看起来很“清爽”。
我建议初学者不要直接背这个公式,而是按一下步骤自己推一遍:写出Rot(z,θ)、Trans(z,d)、Trans(x,a)、Rot(x,α)四个4×4矩阵,然后依次相乘。推过一次之后,你就能理解矩阵里每一项是怎么来的,比如第一列其实就是X_i在X_{i-1}、Y_{i-1}、Z_{i-1}三个方向上的投影。
3.3 六轴正运动学方程怎么组装
六轴机械臂的正运动学就是六个相邻变换矩阵连乘:
T_06 = T_1 * T_2 * T_3 * T_4 * T_5 * T_6
这个T_06就是从基座坐标系{0}到末端坐标系{6}的总变换矩阵。T_06左上3×3是末端姿态矩阵,右上3×1是末端原点在基座坐标系下的位置。因为每个T_i里只有θ_i是变量,其余参数是常数,所以T_06本质上是六个关节角的函数。而这个“函数”长什么样,就是机械臂的运动学方程。
在实际工程里,我们很少真的把T_06完全展开成一大堆sin、cos的多项式,那样又长又容易抄错。通常直接让程序做矩阵连乘,需要某个位置的表达式时再单独提取。但你要理解展开式的来历,后面学逆运动学、求雅可比矩阵、做轨迹规划时,很多推导都要用到“每个T_i对θ_i的偏导”这类概念。这也是我一直强调要手推一遍正运动学的原因,不是为了考试,是为了让后面的路好走。
4. 用Python把正运动学“跑”起来
4.1 手写numpy版本的DH变换
工具我推荐Python配numpy。先写一个标准DH变换函数,输入当前关节角θ、d、a、α,返回4×4矩阵。这里有一个细节:DH参数表里的角度习惯用度,但numpy的三角函数默认用弧度,所以函数入口先把度和弧度转换统一。
import numpy as np def dh_transform(theta, d, a, alpha): """标准DH相邻连杆变换矩阵,角度单位:度""" theta = np.deg2rad(theta) alpha = np.deg2rad(alpha) ct, st = np.cos(theta), np.sin(theta) ca, sa = np.cos(alpha), np.sin(alpha) return np.array([ [ct, -st * ca, st * sa, a * ct], [st, ct * ca, -ct * sa, a * st], [0, sa, ca, d], [0, 0, 0, 1] ])这个函数写的顺序和标准DH公式完全一致,核心就是那三个cos、sin计算。写完这个函数后,无论是四轴还是六轴,正运动学就只是循环调用它。
主程序里先定义DH参数表,然后按顺序乘起来。我用前面那张机械臂参数表:
if __name__ == "__main__": # 参数表:theta_i, d_i, a_i, alpha_i params = [ (0, 0.1, 0, 90), (0, 0.0, 0.4, 0), (0, 0.0, 0.35, 0), (0, 0.1, 0, 90), (0, 0.0, 0, -90), (0, 0.1, 0, 0), ] q = [0, 0, 0, 0, 0, 0] # 六关节角度,单位度 T = np.eye(4) for i in range(6): theta_i, d_i, a_i, alpha_i = params[i] T = T @ dh_transform(theta_i + q[i], d_i, a_i, alpha_i) print("末端位置:", T[:3, 3]) print("姿态矩阵:\n", T[:3, :3])注意,这里的theta_i + q[i]是为了处理“零点偏移”。params里第一列是DH表里的θ_i常量偏移,如果机器人在零位时关节角不是0,你把这个偏移写进去,再在当前读数上叠加q[i]。我给的示例里偏移都是0,所以直接用q[i]也没问题。
4.2 代入真实参数并验证结果
运行上面这段代码,会得到类似这样的输出:
末端位置: [0.75 -0.2 0.1] 姿态矩阵: [[1. 0. 0.] [0. 0. -1.] [0. 1. 0.]]这个结果对应什么几何含义?所有关节角为0时,大臂和小臂沿X方向伸直,末端位置在X方向0.75米处,Y方向有-0.2米的偏置,Z方向高度0.1米。姿态矩阵不是单位阵,而是绕X轴转了90度的效果,这是因为DH表里α1和α4这些扭转角导致腕部坐标系发生了翻转。
看到姿态矩阵不是单位阵,很多初学同学会慌,觉得是不是哪里错了。其实只要对照坐标系定义能解释通,就是对的。最笨也最有效的验证方法,是把某个关节角度改成90°,观察末端位置是否真的绕对应轴线转了90°。比如把q设为[90,0,0,0,0,0],如果模型正确,末端位置应该沿基座Z0轴旋转90°,X方向的位置跑到Y方向去。如果符号反了,结果会跑到负方向,这时候就要回去查α或θ的符号。
4.3 怎么用末端位姿反过来检查DH表
正运动学写完后,如何确认自己建的DH表和真实机械臂一致?我一般用三个方法。
第一个是量零位坐标。把机械臂手动或通过示教器移到零位(所有关节角为0),用尺子量末端法兰中心在基座坐标系下的坐标,再和代码输出对比。如果误差在几毫米内,说明DH参数基本正确。这个方法最直接,但前提是你能准确找到基座坐标系原点,通常我会在基座中心贴一个标记。
第二个是仿真对照。很多机械臂发布过URDF模型,在Gazebo或PyBullet里可以把URDF的正运动学结果当成“标准答案”。你把自己手写的DH正运动学结果和仿真里读到的末端位姿对比,不一致就说明坐标系某处定义有出入。我当年调自己的六轴时,就是用PyBullet逐关节对了一遍,才发现关节4的α符号写反了。
第三个是运动轨迹一致性。让机械臂只动某个关节,比如只动关节2,观察末端的运动轨迹在基座坐标系里是不是一段圆弧。如果轨迹不是圆弧,说明这个关节对应的a或α参数很可能错了。这个方法在机械臂还没完全装好、只能手动掰关节时也特别好用。
5. 常见问题排查与实战心得
5.1 位置错得离谱?先查这四处
正运动学写出来后,如果末端位置完全对不上,不要急着改代码,先按下面这张表排查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 位置完全不对,姿态矩阵也乱 | 变换顺序用错,或矩阵元素抄错 | 先固定θ=0、a=1、d=0、α=0,看矩阵是不是沿X轴平移1 |
| 某个关节转动后方向反了 | Z轴方向取反,或α正负号不对 | 在图上标出Z和X轴,用右手定则复核 |
| 平行关节之间位置对但夹角不对 | X轴取法导致a和d分配不同 | 重选公垂线方向,同步检查相邻两行参数 |
| 角度值看着对但结果是乱码 | 角度/弧度混用 | 代码里统一用弧度,或确保传入deg前先转换 |
排查时我的习惯是先构造最简单的关节值,比如让所有θ=0,手算一下末端位置大概在哪里,再用代码算一遍。如果这段对不上,问题基本出在DH表或矩阵公式上;如果这一段能对上,但动某个关节后不对,问题大概率出在那根杆对应的坐标系方向上。
5.2 奇异点、关节限位和零点偏移对运动学方程的影响
正运动学本身不受关节限位影响,你给任何角度都能算出一个位姿,哪怕这个位姿在实际机械臂上根本到不了。所以不要用“正运动学算出来的位置能不能到达”来判断模型对不对。能不能到,是你后面要做轨迹规划和碰撞检测的事。
真正影响正运动学工程精度的是零点偏移。大多数机械臂用编码器测关节角,但编码器读数0不一定是DH表里的θ=0。如果机器人示教器显示“当前角度为0”时,其实大臂已经有一个初始倾角,这时候直接拿显示角度代入DH表,算出来的末端位置肯定偏。所以做样机时一定要做零点标定,把每个关节在机械零位时的编码器读数记录下来,作为偏移量。
另外,腕部奇异点值得提一句。很多六轴机械臂在关节5角度接近0度时,逆运动学解会出现数值不稳定,正运动学本身不会爆炸,但会暴露在雅可比矩阵条件数上。这虽然不是DH法的锅,但如果你发现某些位姿下手算的逆解跳变很大,多半是奇异点问题,不是模型错误。
5.3 从正运动学走向逆运动学:下一步怎么走
正运动学通了,下一步就是逆运动学:给一个末端目标位姿,反解六个关节角。逆运动学有解析法和数值法两条路。解析法快而且精确,但需要针对具体机械臂结构推公式,不是每种构型都有解析解。数值法通用,常见的是基于雅可比矩阵的迭代法、阻尼最小二乘法,还有优化法。
我个人的建议是:如果只是做应用,先用现成的库,比如Python的Robotics Toolbox、PyBullet、MoveIt里的KDL或Trac-IK,把逆解跑通,再去研究原理。如果是要做深入研究,或者想搞懂机械臂控制底层逻辑,回头再把数值逆解自己写一遍,写的时候你会对雅可比矩阵的几何意义有更深理解。
很多初学者容易陷入一个误区,觉得正运动学太简单,逆运动学才厉害,于是跳过正运动学疯狂研究逆解。实际上逆运动学所有计算都建立在正运动学模型之上,你正运动学里一个符号错误,逆解可能在某些姿态下看起来正确,在某些姿态下彻底发散。我做传统六轴机械臂项目时,花在标定DH参数和验证正运动学上的时间,比写逆解代码的时间多得多,但最后调试反而更顺利。
我自己现在写机械臂代码,还是会把DH表从零手写一遍,哪怕后面用现成库,也要拿手写结果和库结果对一下,确认模型没被某个隐晦的默认约定坑到。标准DH法看起来只是一张表和几个矩阵,但它决定了你后续所有运动学、动力学、轨迹规划的地基。如果你能亲手从连杆坐标系一步步推到运动学方程,再花半小时把numpy版本的正运动学跑通,后面遇到再复杂的机械臂,心里都会很有底。