ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

《现代机器人学》课后习题解答:自由度计算与运动学自检指南

《现代机器人学》课后习题解答:自由度计算与运动学自检指南

简介:这份《现代机器人学(Modern Robotics: Mechanics, Planning, and Control)》课后习题解答面向机器人专业学生、课程自学者及需要补强理论基础的工程师,围绕机械、规划与控制三条主线提供对照学习材料。压缩包仅含1个PDF文件,大小约13.97MB,按章节组织第2至13章习题解答,便于按课程进度检索。内容覆盖自由度计算、空间定位、约束条件、关节运动分析等典型题目,例如多点定位系统的自由度求和与空间拓扑、上肢七自由度机构(肩部球关节、肘部、腕部及前臂旋转)及固定手掌位置方向后的约束分析;后续章节还延伸到动力学、路径规划与控制理论。已有1247人学习,适合在课程作业、考前复习或机器人设计入门时核对思路、理解推导过程,并借由完整章节答案加深对核心概念的掌握。

1. 习题解答不是对答案:先从第 2 章的自由度清点说起

翻到 Exercise 2.5,题目给定 6 个连杆加机架、5 个 R 关节和 2 个 S 关节,答案直接给出 dof = 5。如果只把这个数字抄进作业本,这份解答的价值就丢掉了一大半。真正卡人的地方在于:6(N−1−J)+Σfi 里的 N 到底含不含机架,S 关节凭什么记 3 个自由度,并联支链上的关节要不要重复计入。这份《现代机器人学》课后习题解答覆盖第 2 章到第 13 章,成稿日期标注为 2017 年 9 月 9 日,第 2 章配置空间与自由度部分写得最细,正好当成机器人学入门期的公式校准器用:先自己清点,再拿答案对中间步骤,而不是对最后那个结果。适合正在啃这本书的自学者、带课程设计的工科生,以及需要回查 Grübler 公式含义的工程师。

2. 配置空间与自由度:把「几个自由度」拆成可数的三张表

自由度算错,九成不是公式记错,而是清点环节出了问题。这一章先把配置空间的拓扑写法立住,再落到 N、J、Σfi 三个量的清点含义,最后给出平面版和空间版公式的切换判据。三步走完,第 2 章那二十来道机构题基本能自己复算一遍。

2.1 配置空间的笛卡尔积:拓扑写法为什么先于数字

Exercise 2.1 是个很好的引子。在 n 维空间里依次放置点:第一个点有 n 个自由度,第二个点被第一个点约束掉一个自由度,剩 n−1 个,第三个剩 n−2 个,累加得到 n(n+1)/2。答案还多写了一句拓扑结构:R^n × S^{n−1} × S^{n−2} × … × S^1。很多人只抄了前半句的求和,把后半句当装饰,其实后半句才是能拿去写代码的部分。

原因很直接:笛卡尔积里每一个因子代表一组互相独立的坐标,因子的维数加起来就是总自由度;而因子是 R 还是 S,决定了这组坐标要不要处理周期性。R 是无界实数轴,S 是圆周,角度过 2π 要绕回来。做数值优化、做轨迹插值时,一个 S 因子忘了取模,代价函数就会出现跳变,求解器在边界附近反复横跳——这种问题靠看答案的数字是永远看不出来的。

再看 Exercise 2.6(a):轮式移动底座加 6R 机械臂,底座按滚动硬币处理,配置空间是 R^2 × T^2,6R 臂是 T^6,合起来 R^2 × T^8。这里 T^2 和 T^6 都是环面,维数分别是 2 和 6,相加得 8,和用 Grübler 公式算出来的结果对得上。所以一个很实用的习惯是:每算完一个机构的自由度,顺手把配置空间的笛卡尔积写出来,两边的维数必须一致。不一致就说明关节类型、连杆数或者拓扑至少有一处清点错了,这时候再回去查,比对着答案硬凑快得多。

2.2 连杆数 N、关节数 J、关节自由度之和 Σfi 的清点含义

三个量的含义必须统一,否则同一道题两个人能算出三个答案。

N 是连杆总数,包含机架。机架算 1 个连杆,不管它由几块板焊成;但如果有两个互不连接的地面固定点,那就要按两个机架处理,很多并联机构的答案在这里翻车。J 是关节总数,一条边对应一个关节,一个关节连接两个连杆,串联链上是 n 个连杆 n−1 个关节,闭链要额外加一个。Σfi 是所有关节自由度数的总和,注意是「和」不是「种类数」:6 个 S 关节是 6×3=18,不是 6。

关节类型到 fi 的映射,第 2 章反复用到,整理成一张表随时查:

关节代号名称自由度 fi典型出现位置
R转动副1绝大多数串联臂
P移动副1升降轴、平行四边形机构
H螺旋副1丝杠传动
C圆柱副2R 与 P 同轴组合
U万向节2并联平台支链
S球副3Stewart 平台两端
E平面副3少见,平面内两移动一转动

清点流程建议固定成四步,走顺了以后基本不会漏:第一步画拓扑图,连杆画成节点,关节画成边;第二步给节点编号,机架固定编号 0;第三步给每条边标注关节类型;第四步分别数节点数、边数、把每条边的 fi 加起来。第 2 章那些看着复杂的图,按这四步拆完都是一张稀疏图,数字自然就出来了。

举 Exercise 2.11(a) 做对照:5 个连杆加机架,N=6;6 个 U 关节,J=6;Σfi = 6×2 = 12。代入空间版公式 6(N−1−J)+Σfi = 6×(6−1−6)+12 = 6。这里如果 U 关节按 1 个自由度记,结果会变成 0,机构被判成不能动,和图上明显能动的结构直接矛盾。关节自由度记错是最常见的一类错误,而且错了以后结果往往是个「看起来还挺合理」的小整数,不容易察觉。

2.3 平面版与空间版公式的切换判据

两个版本的公式形式一样,系数不同:

  • 平面机构:dof = 3(N − 1 − J) + Σfi
  • 空间机构:dof = 6(N − 1 − J) + Σfi

选错版本,结果能差出一倍还多。Exercise 2.16 就是典型:四个 R 关节构成的空间四连杆,所有转动轴线交于一点,所有连杆被约束在以该点为球心的球面上运动。用空间版算:N=4、J=4、Σfi=4,得 6×(4−1−4)+4 = −2,看起来机构根本不能动;用平面版(更准确地说是二维版)算:3×(4−1−4)+4 = 1,与实际吻合。

判据我一般压成三条,按顺序过:

  1. 所有转动轴线互相平行、所有移动副方向共面——平面机构,用 3 系数版本。
  2. 所有转动轴线交于一点,或所有构件被约束在同一个球面/曲面上——用二维版本,本质是把运动子空间的维数当系数。
  3. 其余情况——空间机构,用 6 系数版本。

第三条之所以放最后,是因为前两条是「降维」,而降维的依据是几何约束,不是关节类型。这个判断顺序不能反:先去数关节,再看几何,很容易把球面四连杆当成普通空间机构,然后得到一个负的自由度,接着开始怀疑自己数错了连杆。

3. Grübler 公式落地:一个能复算第 2 章全部机构的脚本

公式本身只有一行,难的是把机构可靠地翻译成三个数。这一章先把关节等效替换的规则讲清楚,再用 Python 把计算过程封装成可断言的函数,最后拿第 2 章若干小题做一轮实算核对。

3.1 关节类型到自由度的映射与等效替换

等效替换的规则只有一条:替换前后,机构各连杆之间的相对运动不能变。满足这条规则的前提下,你可以把 R+P 同轴组合看成一个 C 关节,把平行四边形支链看成一个等效的 6 自由度「关节」,把叉形关节看成一个 C。但替换之后,N、J、Σfi 三个量必须同时更新,只改一个必然出错。

Exercise 2.9 给了正反两个方向的对照。合并算法:7 个 R、1 个 P、2 个 C 关节,N=8、J=10、Σfi = 7×1 + 1×1 + 2×2 = 12,平面公式得 3×(8−1−10)+12 = 3。不合并的算法:把每个 C 拆回 R+P,N 变成 14、J 变成 18、Σfi 变成 18,平面公式得 3×(14−1−18)+18 = 3。两个结果一致,说明替换是合法的。

这个「双算」动作值得养成习惯:凡是做了一次等效替换,就用未替换的原始清点再算一遍,两个结果对上才算过关。对不上,要么替换不合法,要么某个量漏更新了。代价是多写两行,收益是省掉一次返工。

还有一个容易忽略的点:等效替换会改变公式的适用范围。Exercise 2.12(d) 里,4-RFRRPR 支链中的 F 是四杆平行四边形,整条支链被折叠成一个 6 自由度关节处理,这时 N 只剩「动平台 + 机架」两个,J=4,Σfi=24,空间公式得 6×(2−1−4)+24 = 6。如果坚持把平行四边形展开成一根根连杆去数,数字会大很多,但结果应该仍然收敛到 6——展开后算不出 6,说明某个中间连杆被重复计入了。

3.2 Python 实现:dof 计算器与批量断言

把清点结果直接写成代码里的数据结构,比在纸上画图可靠。下面这个脚本不长,但把第 2 章的主要题型都覆盖了。

# grubler.py # 用法:把机构清点成 (连杆数, 关节类型列表, 版本),直接拿返回值对答案 JOINT_DOF = { "R": 1, "P": 1, "H": 1, # 单自由度关节 "C": 2, "U": 2, # 双自由度关节 "S": 3, "E": 3, # 三自由度关节 } def grubler(n_links, joints, space="spatial", verbose=False): """ n_links : 连杆总数,含机架 joints : 关节类型字符串列表,如 ["R","R","S","S"] space : "spatial" 用 6 系数,"planar" 用 3 系数 """ N = n_links J = len(joints) sum_f = sum(JOINT_DOF[j] for j in joints) base = 6 if space == "spatial" else 3 dof = base * (N - 1 - J) + sum_f if verbose: print(f"N={N} J={J} sum_f={sum_f} base={base} -> dof={dof}") return dof def check(name, n_links, joints, space, expect): """断言封装:实算值与答案不一致时直接报出中间量""" got = grubler(n_links, joints, space, verbose=True) flag = "OK " if got == expect else "FAIL" print(f"[{flag}] {name}: got={got} expect={expect}\n") return got == expect if __name__ == "__main__": # Exercise 2.5:5R + 2S,空间 check("Ex2.5", 7, list("RRRRR") + ["S", "S"], "spatial", 5) # Exercise 2.11(a):6 个 U 关节,空间 check("Ex2.11a", 6, ["U"] * 6, "spatial", 6) # Exercise 2.9(a):合并成 C 关节后,平面 check("Ex2.9a-merged", 8, list("RRRRRRR") + ["P"] + ["C", "C"], "planar", 3) # Exercise 2.16(a):球面四连杆,必须用二维版本 check("Ex2.16a-planar", 4, ["R"] * 4, "planar", 1) check("Ex2.16a-spatial", 4, ["R"] * 4, "spatial", -2)

逻辑说明:grubler只做三件事——算关节数、算关节自由度之和、按版本选系数。把这三个量与公式拆开,是为了出错时能一眼看出是哪一项不对。check里加verbose=True是血泪经验:批量跑十几道题时,如果只打印 PASS/FAIL,出了问题还得回去手动重算;把 N、J、Σfi 一起打出来,失败的那一行立刻能定位到清点环节。

参数说明:n_links一定要含机架,这是最容易漏的一项;joints是逐个关节列出来的列表而不是计数器,这样能强制你按拓扑图走一遍;space用字符串而不是布尔值,是为了后面扩展到球面版本时不用改签名。

最后两行是刻意写的:Ex2.16(a) 用平面版本得 1、用空间版本得 −2,同一个机构两个结果。把这两个都写成断言,等于把「版本选错」这个坑固化成了可执行的检查。

3.3 实算记录:第 2 章若干小题的逐步核对

把脚本跑一遍,再把几个有代表性的小题列出来对中间量,比只看最终数字有用得多。

题目NJΣfi版本dof备注
Ex2.577(5R+2S)11空间5基础串联+闭链混合
Ex2.7(a)89(3R+6S)21空间9多臂协同抓取
Ex2.9(a)810(7R+1P+2C)12平面3合并算法
Ex2.11(a)66(6U)12空间6U 记 2 自由度
Ex2.13812(12S)36空间66-SPS 平台
Ex2.15(a)66(6R)6平面3正方形机构,平面版
Ex2.16(a)44(4R)4二维1球面四连杆,空间版给 −2

表里 Ex2.15(a) 和 Ex2.16(a) 要特别留意:前者五个正方形连成的机构,平面版给 3,答案认可;但如果按空间版算,得到 0,机构被判成结构。Ex2.15(b) 的答案里明确写了这一点——Grübler 公式无法区分「恰好 0 自由度」和「因为有相等边长的隐含约束而实际能动」这两种情况。这类过约束机构是公式的固有盲区,不是清点错误。

Ex2.7(a) 值得一提:6 个连杆加一个被抓的物体、再加机架,N=8;3 个 R 加 6 个 S,J=9;Σfi = 3+18 = 21;空间公式得 6×(8−1−9)+21 = 9。答案 9 的意思是这套抓取系统的总自由度是 9,但物体本身的位姿只占 6 个,剩下 3 个是内部自由度——这个区分在后续章节讲抓取矩阵的时候还会回来,如果当时没看懂 9 是怎么来的,后面会一直卡。

4. 从答案到推导:刚体运动与运动学部分的对照方法

第 2 章之后,答案的形式从「一个整数」变成了「一个矩阵或一组表达式」,对答案的方式也得换。这一章给一套自检顺序:先验旋转矩阵,再验正运动学,最后看逆解的多解性和奇异位形。

4.1 旋转矩阵与指数坐标的自检清单

答案里出现 R 的时候,先别急着往下看推导,花三秒钟验三个条件:R^T R 是不是单位阵、det R 是不是 +1、三个列向量是不是构成右手系。三条全过,这个 R 才可能是对的;只验第一条不够,因为 det = −1 的反射矩阵也满足正交。

import numpy as np def rot_check(R, tol=1e-9): """返回 (正交误差, 行列式),两个值都应落在容差内""" R = np.asarray(R, dtype=float) ortho_err = np.linalg.norm(R.T @ R - np.eye(3)) det = np.linalg.det(R) return ortho_err, det # 例:绕 z 轴转 30 度 th = np.deg2rad(30) Rz = np.array([[np.cos(th), -np.sin(th), 0], [np.sin(th), np.cos(th), 0], [0, 0, 1]]) print(rot_check(Rz)) # 正交误差 ~1e-16,det = 1.0

参数说明:tol取 1e-9 是给手算结果留的余量,如果 R 是从答案里手抄进来的、有效位只有三四位,误差会到 1e-6 量级,这时候要放宽到 1e-6,否则会把对的答案判成错的。行列式那项不要用== 1直接比,浮点算出来的 0.9999999 会判失败。

指数坐标是下一个关口。角速度向量 ω 只有在模长为 1 的时候才能直接套 Rodrigues 公式,很多答案里给出的轴向量没有归一化,直接代入会得到一个「差不多但不对」的旋转矩阵。ω 的模长接近 0 时是纯平移,要单独走平移分支,不能套旋转公式——这是指数积实现里最常见的一个分支遗漏。

def exp_so3(w, th): """w: 转轴(内部会归一化); th: 转角(rad)""" w = np.asarray(w, dtype=float) n = np.linalg.norm(w) if n < 1e-12: # 零轴退化为纯平移,由调用方处理 return np.eye(3) w = w / n K = np.array([[0, -w[2], w[1]], [w[2], 0, -w[0]], [-w[1], w[0], 0]]) return np.eye(3) + np.sin(th) * K + (1 - np.cos(th)) * (K @ K)

4.2 正运动学 PoE 与雅可比的对照顺序

指数积形式的正运动学是 T(θ) = e^[S1]θ1 · e^[S2]θ2 · … · e^[Sn]θn · M。对着答案检查的时候,顺序不能乱:先确认 M 是零位位形,再确认每个螺旋轴是在基座系还是末端系下表达的,最后才逐项相乘。

M 错了,后面全错,而且错得很隐蔽——乘出来的矩阵仍然是个合法位姿,只是零位不对。零位核对的办法很简单:把所有 θ 置 0,T 必须等于 M。这一步用一行代码就能验,但跳过它的人不在少数。

螺旋轴的表达系搞混是第二个高频错误。用基座系表达时,所有 [Si] 左乘;用末端系表达时,所有 [Si] 右乘。答案里如果混用了两种表达却写出了同一个 T,说明至少有一处轴写错了,只是碰巧抵消。

def fk_poe(M, S_list, thetas): """M: 4x4 零位位形; S_list: 螺旋轴列表(基座系); thetas: 关节角列表""" T = np.eye(4) for S, th in zip(S_list, thetas): T = T @ exp_se3(S, th) # 左乘,基座系表达 return T @ M # 零位自检:theta 全 0 时必须返回 M T0 = fk_poe(M, S_list, [0] * len(S_list)) assert np.allclose(T0, M), "零位位形不对,先查 M"

雅可比矩阵的核对更依赖于几何直觉:把每个关节的瞬时运动贡献列出来,转动副的线速度部分是 ω × r,移动副只有线速度部分。对着答案逐列比,比整体比矩阵容易定位。奇异位形则是看 J 的秩,或者更稳一点看最小奇异值——用一个固定的阈值判秩,在接近奇异的位置会给出抖动结果。

def jacobian_singular(J, tol=1e-6): """返回 (是否奇异, 奇异值序列);最小奇异值小于 tol 即视为奇异""" s = np.linalg.svd(J, compute_uv=False) return s[-1] < tol, s

4.3 逆解多解性与奇异位形的判断

逆运动学的答案往往是一组解而不是一个解,判断自己漏没漏解,有两个抓手。

第一个是腕部球形结构的解析解组数。腕部三个轴交于一点时,位置和姿态可以解耦,姿态部分通常给出若干组解析解,组数由反三角函数的象限决定。答案只列了一组,不代表只有一组;做轨迹规划时选错分支,机械臂会在两个构型之间突然翻转。

第二个是奇异位形处的解退化。雅可比秩亏时,逆解要么无解,要么无穷多解,数值迭代会发散或者停在原地。判断方法就是上一节的奇异值检查,把这个检查插到逆解的每一次迭代之前,比事后debug省事。

数值逆解常用阻尼最小二乘,核心是把 (J^T J)^(−1) 换成 (J^T J + λ^2 I)^(−1),λ 在接近奇异时自动放大,牺牲一点精度换稳定性:

def ik_dls(J, e, lam=1e-3): """一步阻尼最小二乘更新:dq = J^T (J J^T + lam^2 I)^-1 e""" JJt = J @ J.T + (lam ** 2) * np.eye(J.shape[0]) return J.T @ np.linalg.solve(JJt, e)

参数说明:lam太小起不到阻尼作用,太大收敛慢,1e-3 是个常用起点,接近奇异时可以按最小奇异值动态调整。e是当前位姿误差,旋量形式要统一成 6 维,别一半用角度一半用弧度。

5. 避坑与排查:五类出错场景与定位手法

第 2 章的答案写得足够细,细到能反推出每一步的清点过程。但正因为细,抄错一步就会引发连锁反应。下面五类是实际复算时最常遇到的。

第一类:拓扑版本选错,机构被判成不能动。

现象:算出来的自由度是负数,比如球面四连杆用空间公式得 −2,但图上机构明显能动。

原因:所有转动轴线交于一点,构件被约束在一个二维球面上运动,可用的运动子空间只有 2 维,用 6 系数版本相当于凭空多扣了维度。

解决:上手先看几何,轴线平行走平面版,轴线共点走二维版,都不满足才用空间版。把这条判据写成注释贴在脚本开头,比记在脑子里可靠。

第二类:关节等效替换后只更新了一个量。

现象:把 R+P 合并成 C 之后,J 从 18 变成 10,但 N 忘了从 14 改成 8,算出来的自由度和答案差 18。

原因:等效替换是把「几根连杆加几个关节」整体折叠成一个关节,N 和 J 同时减少,Σfi 也变。只改其中一个,公式里的 (N−1−J) 那项就完全错了。

解决:每次替换后强制执行双算——替换版和原始版各算一遍,两个结果必须相等。Ex2.9 的答案本身就是这么写的,照着做就行。

第三类:过约束机构,公式给 0 但实际能动。

现象:五个同尺寸正方形连成的机构,空间版公式给出 0,按定义应该是个结构,但实物能折叠。

原因:Grübler 公式只数连杆和关节,看不到「所有正方形边长相等」这种几何约束带来的额外自由度。Ex2.15(b) 的答案里明确写了公式无法区分这类情况。

解决:公式结果 ≤ 0 但直觉上机构能动时,优先怀疑过约束,去查有没有等长、共线、平行、共面这类隐含条件。这类机构的自由度要靠瞬时运动分析或者螺旋理论另算,不能硬套公式。

第四类:把末端约束数当成了机构自由度。

现象:看到「手掌固定在空间中」,就直接写 6 个约束、6 自由度,答案却是 1。

原因:Ex2.2(b) 的推理是:手臂原本 7 个自由度,固定手掌施加 6 个约束,剩 1 个——那 1 个是肘尖在圆弧上滑动的自由度,不是机构的总自由度。

解决:约束数和自由度是两回事。看到「固定末端」先问一句:这是在求剩余自由度,还是在求机构本身的设计自由度?答案里的每一句「因此」都值得停一下,确认它推的是哪个量。

第五类:U 关节按 1 记、S 关节按 2 记。

现象:Ex2.11(a) 六个 U 关节,Σfi 记成 6,空间公式得 0,答案给的是 6。

原因:U 关节是两个转动副正交组合,fi=2;S 关节是三个转动副交于一点,fi=3。记成 1 和 2 是最顺手的错误,因为关节「个数」和「自由度数」在中文里都是「个」。

解决:清点时不要数关节个数,直接把每个关节的 fi 写出来再求和,写完回头看一眼总和是不是等于关节数乘平均自由度。这一步只多花十几秒,能挡掉相当一部分错误。

6. 进阶:把答案库变成参数扫描与自检夹具

第 2 章有几道题的价值不在答案本身,而在结论的普适性。Ex2.8 就是典型:n 条开链腿抓一个物体,每条腿 α 个自由度,代入空间公式得到 dof = 6+(α−6)n,结论是这个表达式恒等于 6,与腿的数量无关。这种「公式结果与某个参数无关」的结论,最适合用符号计算验一遍,因为它把「n 取多少」这个变量彻底消掉了。

import sympy as sp n, alpha = sp.symbols('n alpha', positive=True) # N=2(物体 + 机架),J=n,sum_f = alpha*n dof = 6 * (2 - 1 - n) + alpha * n print(sp.expand(dof)) # 6 + n*(alpha - 6) print(sp.solve(sp.Eq(dof, 6), alpha)) # [6] # 扫描:腿数从 2 到 8,看自由度是否恒为 6 for k in range(2, 9): for a in (6, 7): val = dof.subs({n: k, alpha: a}) print(f"legs={k} alpha={a} -> dof={val}")

跑出来的表会告诉你两件事:α=6 时不管几条腿都是 6,这就是 Ex2.8 的结论;α=7 时自由度随腿数线性增长,每多一条腿多一个内部自由度。后一种情况在公式上仍然成立,但物理意义变了——多出来的不是物体位姿的自由度,而是冗余驱动带来的内力分配自由度,做控制分配的时候要单独处理。

这层区分很容易被跳过。Ex2.7(b) 里 n 条 7 自由度开链臂抓物体,公式给 n+6,答案也是这么写的,但真正有价值的信息是那个 6 之外的 n:物体只占 6 个,剩下 n 个是臂与臂之间的内部运动。如果一开始就把 n+6 当成「物体的自由度」,后面看抓取矩阵的时候会完全对不上。

再往前推一步,可以把第 2 章的答案批量转成回归测试。做法是把每题的三元组和期望值写成数据表,用一个测试函数逐条跑,改脚本的时候先跑一遍,全绿了再往下改公式。这样第 3 章以后的推导如果引用了某个自由度结论,能立刻确认它是从哪道题来的、当时的清点标准是什么。答案库本身是静态的,配上这么一层断言之后才变成能反复使用的自检夹具。

最后留一个我自己的习惯。每次翻到一道自由度题,不管答案多简单,我都会先在纸上走一遍「画图 → 编号 → 标关节 → 求和」四步,把 N、J、Σfi 三个数写在题目旁边,再去对答案。对上了就把这三个数抄到笔记里,对不上就把中间过程也写下来。从那以后我每次带人复算这本书,都强制走一遍这四步再允许翻答案,因为跳过它省下的五分钟,通常会在后面某个并联机构上以两小时的形式还回来。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表