6轴机械臂正逆运动学解析:从DH建模到球形腕解耦求解
2026/9/15 17:30:26 网站建设 项目流程

做机器人控制这几年,绕不开的一个槛就是6轴机械臂正逆运动学。同事常说“正解谁都会写,逆解才是劝退大师”,话糙理不糙。如果你手头的机械臂是拟人臂加球形腕的结构——肩部两个轴、肘部一个轴、腕部三个轴交于一点——那恭喜你,逆解可以写出解析解,不需要靠数值迭代硬啃。这篇文章就用我自己调过的一台6轴臂举例,从DH建模开始,把正运动学和逆运动学拆开讲透,代码全部给出,最后再聊几个踩过的坑。

这事适合谁看?做毕设的学生、刚入手机械臂想搞懂底层算法的工程师、或者被ROS里IK库黑盒折磨的人。你不需要多深的基础,但至少要知道坐标变换和矩阵乘法是怎么回事。读完你不仅能自己写出FK/IK,还能知道为什么球形腕这么设计,以及怎么判断你的解靠不靠谱。

1. 构型拆解:为什么是拟人臂+球形腕

1.1 这种构型到底长什么样

先说拟人臂。从基座向上数,第一个关节通常是绕竖直轴转的“腰”,第二个和第三个关节负责让大臂、小臂在一个平面内俯仰,类似人的肩膀和肘。再往上就是手腕部分:三个旋转轴交于一个固定点,那个点叫腕心(Wrist Center)。这种设计在工业机器人和协作臂里占了绝大多数,像常见的UR、FANUC、KUKA等6轴臂基本都是这个套路。

为什么大家都这么做?因为球形腕最核心的价值是“解耦”:末端姿态由后三个关节决定,末端位置主要由前三个关节决定。虽然实际求解时还牵扯到d4这类偏置,但腕心分离后,6自由度问题可以拆成“3自由度定位”加“3自由度定向”,每个部分都有明确的几何意义。要是腕部三个轴不交于一点,逆运动学就只能走纯数值迭代,又慢又不稳定,实时控制里很难用。

1.2 DH建模前的三个约定

每个写DH的人都会告诉你:先把约定定清楚。经典DH和改进DH的矩阵写法不一样,同一个机器人标出来的参数也可能差一个负号。我下面用的是经典DH,就是Craig那套教材里的写法:变换矩阵下标从左往右乘,坐标系 i-1 到坐标系 i 的变换写成:

A_i = RotZ(theta_i) * TransZ(d_i) * TransX(a_i) * RotX(alpha_i)

三个约定我固定在代码里:

  1. 关节角统一用弧度制,所有角度计算都在弧度下进行。
  2. 基座坐标系固定在腰关节正下方,z轴竖直向上。
  3. 腕心坐标用工具坐标系原点减去d6 * R[:,2]来求,这里的R[:,2]是末端姿态矩阵的第三列,也就是末端z轴在世界系下的方向向量。

这三点看着简单,但很多人后面 IK 对不上,十有八九是在这里松了口。

1.3 我用的DH参数表

我用一台拿来做轨迹规划的6轴臂举例,参数按照常见拟人臂来设,单位是米:

id_ia_ialpha_i对应含义
10.200pi/2底座到腰关节高度
200.400大臂长度
300.350小臂长度
40.100pi/2腕心在肘关节轴方向偏置
500-pi/2球形腕过渡
60.0800工具法兰到腕心距离

注意第四行的d4=0.10,这个偏置让腕心不坐在小臂延长线上,而是往关节轴方向歪了一点。没有这个偏置,正逆解会简单很多,但真实机械臂为了给电机和减速器腾地方,基本都有这个小偏移,所以我们必须把它算进去。

2. 正运动学:矩阵乘法练手

2.1 DH变换矩阵长什么样

经典DH的单个变换矩阵长这样:

[ cos(theta) -sin(theta)*cos(alpha) sin(theta)*sin(alpha) a*cos(theta) ] [ sin(theta) cos(theta)*cos(alpha) -cos(theta)*sin(alpha) a*sin(theta) ] [ 0 sin(alpha) cos(alpha) d ] [ 0 0 0 1 ]

每个关节一个矩阵,把六个矩阵从左到右乘起来,就得到从基座到末端的齐次变换矩阵T06。这个T06就是正运动学的输出,里面既有末端位置,也有末端姿态。

2.2 代码实现

我用Python加NumPy实现,代码结构保持简单直接:

import numpy as np from math import pi, sin, cos def dh_mat(theta, d, a, alpha): st, ct = sin(theta), cos(theta) sa, ca = sin(alpha), cos(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参数表,每行是 (d, a, alpha) robot = [ (0.20, 0.00, pi/2), (0.00, 0.40, 0.0), (0.00, 0.35, 0.0), (0.10, 0.00, pi/2), (0.00, 0.00, -pi/2), (0.08, 0.00, 0.0), ] def fk(q): T = np.eye(4) for (d, a, alpha), theta in zip(robot, q): T = T @ dh_mat(theta, d, a, alpha) return T # 测试一组关节角 q_test = [0.0, pi/4, -pi/6, 0.0, pi/3, 0.0] T_end = fk(q_test) print(T_end)

跑出来的T_end就是一个4x4矩阵,前三行三列是姿态,最后一列前三行是末端位置。有了这个函数,后面逆运动学验证就有了基准。

2.3 正解结果为什么值得先跑通

我见过不少人一上来就闷头写逆解,结果算出来的关节角送到示教器上完全乱跑。原因很简单:你的DH参数、坐标轴方向、d/a符号可能从一开始就是错的。正运动学是唯一能和物理实体直接对照的东西,所以我的习惯是先把正解跑通,拿一个已知关节角把末端位置量出来,对比一下再继续。坐标系这种东西,错一个负号,后头全白干。

3. 逆运动学:解析解(腕心分离法)

3.1 核心思想:把6自由度拆成位置控制+姿态控制

逆运动学就是已知末端位姿,反推六个关节角。常规解法很多,但球形腕结构给了我们一条捷径:先把腕心坐标算出来,用前三个关节去找腕心,再用后三个关节去配姿态。

已知目标末端齐次变换矩阵T_target,其中旋转矩阵记为R,位置向量记为p。腕心的位置是:

p_w = p - d6 * R[:,2]

为什么要减?因为第六个关节沿着末端z轴方向平移了d6,减掉这一段,剩下就是腕心的位置。这个p_w只跟前三个关节有关,和后三个关节无关。

3.2 前三关节:肩关节与肘关节求解

先求theta1。因为第四轴的d4偏置,腕心并不在肩关节旋转出来的平面里,所以有两个解:

rho = sqrt(px^2 + py^2) theta1 = atan2(py, px) + s1 * atan2(d4, sqrt(rho^2 - d4^2))

其中s1+1-1,表示肩关节的左右两个解。如果rho^2 - d4^2小于零,说明这个目标点在腕心可达范围之外,直接返回无解。

得到theta1之后,把腕心投影到手臂平面。手臂平面内的水平距离x是:

x = sqrt(rho^2 - d4^2) y = pz - d1

这其实就是把三维问题规约成了经典的二连杆平面机械臂问题。大臂长度a2,小臂长度a3,腕心相对肩关节的水平距离是x,垂直距离是y。于是:

cos_theta3 = (x^2 + y^2 - a2^2 - a3^2) / (2 * a2 * a3) theta3 = s2 * acos(cos_theta3)

s2+1-1,对应肘部上/下两个解。接着求theta2

theta2 = atan2(y, x) - atan2(a3 * sin(theta3), a2 + a3 * cos(theta3))

这套公式对同样的构型是通用的,唯一要注意的是a2a3用正值,如果换了厂家参数,特别是出现负号时,要先确认自己的DH表到底把长度定义在哪个方向。

3.3 后三关节:球形腕姿态求解

前三个关节定下来后,算出手腕坐标系相对基座的旋转矩阵R03,然后得到腕部相对旋转矩阵:

R36 = R03.T @ R

因为球形腕的三个轴相交于腕心,而这个构型下R36正好可以写成:

R36 = Rz(theta4) * Ry(theta5) * Rz(theta6)

这是一个标准的ZYZ欧拉角问题。提取时看矩阵元素:

r13 = R36[0,2] r23 = R36[1,2] r33 = R36[2,2] r31 = R36[2,0] r32 = R36[2,1] theta5 = atan2(sqrt(r13^2 + r23^2), r33) theta4 = atan2(r23, r13) theta6 = atan2(r32, -r31)

如果sqrt(r13^2 + r23^2)接近零,说明腕部处于奇异位形,theta4theta6不再是独立解,这时候可以固定theta4=0,把theta6直接设为atan2(r21, r11)

3.4 代码实现(完整可运行)

把上面的推导写成代码:

def wrap_to_pi(angle): return (angle + pi) % (2*pi) - pi def ik(T_target): # 参数 d1, a2, a3, d4, d6 = 0.20, 0.40, 0.35, 0.10, 0.08 R = T_target[:3, :3] p = T_target[:3, 3] # 1. 腕心坐标 p_w = p - d6 * R[:, 2] px, py, pz = p_w rho = np.hypot(px, py) if rho**2 - d4**2 < 0: return [] solutions = [] # 2. 肩关节两个解 theta1_candidates = [ np.arctan2(py, px) + np.arctan2(d4, np.sqrt(rho**2 - d4**2)), np.arctan2(py, px) - np.arctan2(d4, np.sqrt(rho**2 - d4**2)), ] for theta1 in theta1_candidates: x = np.sqrt(rho**2 - d4**2) y = pz - d1 r2 = x*x + y*y cos_theta3 = (r2 - a2*a2 - a3*a3) / (2*a2*a3) if cos_theta3 < -1 or cos_theta3 > 1: continue # 3. 肘关节上下两个解 for theta3 in [np.arccos(cos_theta3), -np.arccos(cos_theta3)]: theta2 = np.arctan2(y, x) - np.arctan2(a3*np.sin(theta3), a2 + a3*np.cos(theta3)) # 4. 求 R03 T03 = fk([theta1, theta2, theta3]) R03 = T03[:3, :3] R36 = R03.T @ R r13, r23 = R36[0,2], R36[1,2] r33 = R36[2,2] s5 = np.sqrt(r13*r13 + r23*r23) if s5 < 1e-6: # 腕部奇异,theta4 和 theta6 不独立 theta4 = 0.0 theta6 = np.arctan2(R36[1,0], R36[0,0]) solutions.append([theta1, theta2, theta3, theta4, 0.0, theta6]) continue theta4 = np.arctan2(r23, r13) theta5 = np.arctan2(s5, r33) theta6 = np.arctan2(R36[2,1], -R36[2,0]) # 第一组腕部解 solutions.append([theta1, theta2, theta3, theta4, theta5, theta6]) # 腕部翻转解 solutions.append([ theta1, theta2, theta3, theta4 + pi, -theta5, theta6 - pi, ]) # 所有角度归一化到 [-pi, pi] solutions = [ [wrap_to_pi(a) for a in sol] for sol in solutions ] return solutions

到这里,一组目标位姿最多会出来8组解:肩关节2个、肘关节2个、腕关节2个。控制上选哪组,要看当前关节角、避碰条件和奇异距离。

4. 多解、奇异与工作空间

4.1 8组解怎么选

8组解不是每个都能用。最常用的选择策略是“离当前关节角最近”:把每一组解和当前关节角做差,取加权平方和最小的一组。权重大的关节优先少动,比如腰关节和肩关节动起来对整个臂影响最大,权重就设高一点。

也可以用“连续性优先”:上一时刻用的解,这一时刻优先选离它最近的解,避免关节角跳变。这个在轨迹跟踪里特别重要,不然末端看着平滑,关节角却会突然转个大圈。

4.2 奇异点是怎么回事

奇异点不是控制逻辑能绕过的,是机械结构本身带来的。这个构型下主要有三种:

奇异类型触发条件典型现象
肩部奇异腕心投影半径接近d4腰关节快速转动,末端几乎不动
肘部奇异cos_theta3 = ±1,大小臂完全伸直或折叠肘关节需求速度爆炸
腕部奇异theta5接近0或pi4轴和6轴高速反向补偿

解析IK里遇到奇异,代码不会崩,但关节角可能乱跳。如果你要做轨迹插补,一般要把奇异点附近的解剔除,或者对速度层做奇异鲁棒处理。

4.3 工作空间判断

逆解返回空数组,不代表代码有bug,很可能是目标点在可达空间之外。最简单的手算是判断:

  1. rho小于d4,无解。
  2. cos_theta3超出了[-1,1],无解。

进一步想看机械臂末端整体能覆盖多大范围,就随机采样N组关节角,跑正解,把末端位置画成点云。这个方法直观又不会被公式绕晕。我习惯先在仿真里扫一遍,记住工作空间的边界形状,后面调IK时心里有底。

5. 常见问题与坑

5.1 角度单位、绕序害死人

弧度制还是角度制,这个说出来都觉得低级,但我真见过有人在代码里混着用,结果前三个关节正常,后三个关节乱飞。建议代码里所有输入输出都统一弧度,只有打印调试信息时才转成角度给人看。

DH矩阵里的绕序也要固定死。经典DH是RotZ * TransZ * TransX * RotX,你换成改进DH,矩阵里sincos的位置会变,参数意义也会变。网上的代码满天飞,抄之前先看清楚它用的是哪种DH。

5.2 末端位置对的,姿态却错

这种情况多半是p_w求错了。腕心偏移方向是沿末端z轴,也就是R[:,2]。如果你用了末端x轴,或者减了d6但符号反了,位置解还能碰巧对,姿态解就会不对。

排查办法很简单:随机给一组关节角q,跑正解得到T,再喂给IK,看输出里有没有一组和原始q等价。如果没有,要么DH参数不对,要么p_w的公式不对。这个闭环测试建议写成一个单元测试函数,以后改参数不用胆战心惊。

5.3 解析解和数值解怎么选

解析解快、确定、可分支选择,但只对特定构型成立。数值解(比如LM迭代、伪逆法)对任意构型都适用,但需要一个好的初始值,否则容易陷在局部最优里。实际工程中,我经常先用解析解算出一个初值,再丢给数值解做精细调整。纯用数值解做轨迹跟踪时,每帧都要把上一帧的解当作初始值,才不容易跳。

5.4 一段完整的自检代码

最后分享一个我留在项目里的自检函数。随机生成100组关节角,正解得到目标位姿,再用IK反解,检查有没有哪个解能和原关节角对上:

def check_ik(): ok_cnt = 0 for _ in range(100): q = np.random.uniform(-pi, pi, 6) T = fk(q) sols = ik(T) found = False for s in sols: diff = np.array(s) - np.array(q) diff = np.abs(np.arctan2(np.sin(diff), np.cos(diff))) if np.all(diff < 1e-6): found = True break ok_cnt += int(found) print(f"通过率: {ok_cnt}/100")

我自己的经验是,只要DH参数和p_w公式没错,通过率应该是100%。如果通过率不够,先别急着调IK公式,回头把每一组DH变换单独打印出来和最开始的推导对比一遍。

6. 个人体会与后续扩展

写这套代码最深的体会是:逆运动学不是一个公式就能糊弄过去的事情,它跟你前面怎么建坐标、怎么定义参数完全绑定。你从网上找一份UR的IK代码,跑在另一台参数完全不同的臂上,大概率是废的。真正值得花时间的,是把自己这台机器的DH参数吃透,然后基于腕心分离的思路自己推一遍。推通之后你会发现,所谓解析解不过就是把几何关系用代码还原出来。

后面还可以继续做几件事:一是加入雅可比矩阵,算关节速度和工作空间奇异度;二是把IK选解和轨迹插补结合,做平滑过渡;三是在仿真里拖一个目标点,实时看关节角是不是按直觉在变化。做到这一步,你手里这台6轴臂基本就摸透了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询