基于七次B样条的机械臂轨迹规划:八个路径点与速度加速度约束实现
2026/9/9 2:38:25 网站建设 项目流程

做机械臂轨迹规划这些年,我踩过不少坑,也攒了不少好用的套路。今天想重点聊一个我自己在项目里反复用过的方案:用 MATLAB 做七次 B 样条轨迹规划,输入八个路径点,同时把速度和加速度约束都带上,让机械臂在关节空间也能走出平顺、可执行、不抖动的曲线。不少同行一上来就用三次样条,但真遇到高速重载或者高精度场景,三次在加速度连续性上常常不够看;而七次 B 样条的优势就是能让加速度甚至加加速度都保持连续,运动冲击小得多。这篇东西不是教科书,是我实际调程序和跑真机的经验总结,适合正在做机械臂轨迹规划、运动控制,或者准备写相关毕业设计的同学参考。

整套方案的核心逻辑并不复杂:八个路径点是机械臂必须经过的关节位置,我们在这八个点之间构造一条七次 B 样条曲线,曲线自然通过所有路径点,然后通过差分得到速度和加速度曲线,最后在满足约束的前提下优化每个路径段的时间分配。下面我就把这个过程拆开,从原理讲到代码,再讲到调试时的坑。

1. 项目定位与整体设计思路

1.1 先搞清楚需求:为什么是七次,为什么是八个点

很多初学者拿到一个轨迹规划任务,第一反应是“用多项式插值每个路径段”。比如五个点,就分成四段,每段用三次多项式去插值,末端位置和速度连续。这个思路简单,但有个很明显的问题:段与段之间加速度会突变。机械臂在加速突变的位置会产生冲击,轻则轨迹抖动,重则激发结构共振,真机跑起来噪声大、寿命短。七次 B 样条能把整个轨迹表达成一条整体曲线,位置、速度、加速度、甚至加加速度都能做到连续,运动过程一气呵成。

为什么固定用八个点?很直接的理由:七次 B 样条在夹持节点向量下需要八组控制系数,八个路径点正好构成“每段一个点”的最小完整结构。实际工艺里,机械臂从抓取到放置,中间往往有四五个中间示教点,加起来正好就是六到十个点,八个是非常典型的规模。标题里写八个点,本质上是把“路径点数量”固定下来,方便对照验证,你拿到代码后改成十二个、二十个也同样可行。

有一点必须提醒:八控制点 + 七次 + 夹持节点向量,在数学上会退化为一条 Bézier 曲线。这意味着整条曲线是全局的,移动任何一个路径点,整条轨迹都会受影响。这不算 bug,但你要心里有数。如果项目更看重局部修改的灵活性,建议改用节点插入或多段七次 B 样条拼接,不过那套复杂度会高很多,后面我也会展开说区别。

1.2 整体技术路线:从路径点到约束优化

拿到底层需求后,我习惯先把技术路线画成下面几步,后面所有代码都围绕这条主线写:

  1. 输入阶段:给定八个关节位置路径点,例如某个轴的角度序列q0, q1, ..., q7,以及每个轴的速度上限v_max和加速度上限a_max
  2. 时间参数化:为每个路径点分配时间节点t0, t1, ..., t7。最朴素的是等间隔分布,但等间隔往往不是最优的,甚至会触发速度约束。因此更常见的是把时间间隔Δt1, Δt2, ..., Δt7当作优化变量。
  3. B 样条反算:在给定节点向量和时间点后,反算七次 B 样条的控制点,使曲线精确穿过八个路径点。
  4. 导数计算:利用 B 样条导数还是 B 样条的性质,对控制点做差分得到速度曲线控制点、加速度曲线控制点。
  5. 约束检查:B 样条曲线永远落在控制点构成的凸包内,因此只要速度控制点的幅值不超过限速,整个速度曲线就不会超限。加速度同理。
  6. 优化迭代:如果超限,就调整时间节点向量,让机械臂在速度允许的区间多花时间,在运动平缓的区间少花时间,直到约束全部满足,同时总时间尽量短。

这六步就是整个方案的骨架。标题里如果写的是“基于 NS 系列优化算法”,那通常就是指第 6 步用NSGA-II 或类似进化算法去搜索最优时间向量。本质上,这个问题的目标函数和约束条件都清晰,普通fmincon也能解,但 NSGA-II 这类多目标算法的好处是可以同时优化两个甚至多个指标,比如总时间和冲击最大值,最后从 Pareto 前沿里挑一组折中解。这点后面我用一节专门讲。

2. 七次 B 样条的核心原理

2.1 B 样条基函数与节点向量

B 样条曲线的基本形式是:

C(t) = Σ_{i=0}^{n} N_{i,p}(t) * P_i

其中P_i是控制点,N_{i,p}(t)是 p 次 B 样条基函数。基函数由节点向量U = [u0, u1, ..., um]决定,递推公式就是标准的 Cox-de Boor 公式:

  • p=0 时,若t落在第 i 个节点区间内则基函数为 1,否则为 0;
  • p>0 时,由两个低一次基函数线性组合而成。

第一次看这个公式的人容易懵,但可以把它理解成“局部加权平均”:每个控制点只对附近一段区间有影响,离得远的区间完全不受影响。这个局部支撑性质正是 B 样条比普通多项式插值更灵活的原因。

节点向量的选择非常关键,常见有两种:

  • 均匀节点向量:节点等距分布,简单,但曲线在端点附近容易振荡。
  • 夹持节点向量(clamped):两端节点各重复p+1次,曲线强制穿过首尾控制点,这是轨迹规划里最常用的形式。

在我们这个场景中,时间t的跨度可以设成[0, T],两端各重复 8 次,节点向量就长这样:

U = [0, 0, 0, 0, 0, 0, 0, 0, t8, ..., T, T, T, T, T, T, T, T]

注意,如果八个路径点的时间节点t0=0t7=T都正好落在内部节点值上,那么曲线会精确通过这些路点。这一点在反算控制点时是核心约束。

2.2 为什么选七次:连续性是一件奢侈的事

B 样条有个很重要的性质:p 次 B 样条在节点不重复的情况下具有 C^{p-1} 连续。七次就是 C6 连续,也就是说位置、速度、加速度、加加速度、甚至更高阶导数全部连续。

很多场景用三次 B 样条就够了,C2 连续意味着加速度连续,但加加速度仍有跳变。真机对加加速度非常敏感,尤其在高速搬运、精密装配这类场景中,加加速度突变会产生瞬间的惯性冲击。七次 B 样条把这些高阶量都磨平了,代价是计算量略大,但现代控制器完全扛得住。我自己在半导体设备和轻型机械臂上都用过七次方案,效果是末端轨迹细腻,电机没有尖锐异响

七次还有个好处:可以自由控制速度和加速度的端点值。比如要求起止点速度为零、加速度也为零,七次样条有足够的自由度去满足这些端点条件。三次样条只能满足位置和速度,加速度端点往往没法精确指定。

2.3 反算控制点与八个数据点的关系

直接拿八个路径点当控制点,曲线并不会经过这些点,而是把控制多边形“包围”在内部。想让曲线精确穿过八个点,需要解一个线性方程组:

A * P = Q

其中A的每个元素是基函数在路径点对应时间处的值,Q是八个路径点坐标。只要矩阵可逆,解出来的控制点P就能保证曲线经过所有路点。

这一步在 MATLAB 里有多种实现方式。如果你装了 Curve Fitting Toolbox,可以直接用spapi(k, t, Q)生成样条,底层逻辑就是反算;如果不依赖工具箱,就自己用基函数循环填矩阵,再左除求解。我更推荐自己写一遍这个过程,里面涉及的矩阵构造和索引问题,能帮人把 B 样条吃透。

这里要再次强调退化问题:八个数据点对应八个控制点,在夹持节点向量下没有多余节点来“切分段”,所以曲线退化成一条 Bézier 曲线。它依然保留高次连续的特性,但失去了 B 样条的局部支撑性。这意味着调整任意一个路点,所有控制点都会跟着变。如果项目需求是“改一个点只影响局部轨迹”,那就要把路点数量增加或者采用节点插入技术,让控制点数量大于路点数量,再用最小二乘逼近。实操时先确认需求再选方案,别稀里糊涂上七次。

3. 速度与加速度约束的实现

3.1 导数曲线的控制点递推

B 样条最漂亮的性质之一,是导数依然是 B 样条,且次数降低一次。对位置控制点做差分,就能得到速度曲线的控制点:

V_i = p * (P_{i+1} - P_i) / (u_{i+p+1} - u_{i+1})

用这组新控制点、以及去掉首尾节点后的新节点向量,就能构造出速度曲线。继续对速度控制点做同样操作,又得到加速度控制点。也就是说,不需要在密集时间点上求数值差分,光是处理控制点,就能从源头上获得整个曲线族的表达。

这里有一个很实用的推断:B 样条曲线永远落在控制点构成的凸包内。如果所有速度控制点分量的绝对值都小于v_max,那么整条速度曲线的绝对值也不会超过v_max。这个性质非常强,它把“整条曲线上所有时刻都满足约束”这个无穷维问题,转化为“有限个控制点满足约束”的简单问题。加速度约束同理。这也是为什么我敢在真机上用这套方案——约束验证是数学上可保证的,不是靠密集采样碰运气。

3.2 约束怎么加:凸包性、离散检查与优化框架

日常工程中,约束验证有三种级别:

  • 控制点级约束:利用凸包性,直接检查每一阶导数控制点的分量绝对值。这是最严格也最省事的做法,适合理论分析和离线规划。
  • 离散点检查:把曲线按很小的时间步长采样成千上万个点,逐点核对速度加速度。这个办法实现简单,能直观看到超限位置,但只能作为验证手段,不能完全证明约束恒成立。我在调试时用它来定位问题区间。
  • 优化框架内的约束:把控制点约束写进优化算法的约束条件或罚函数,让优化器自动寻找满足约束的时间向量。这就是 NSGA-II 或fmincon干的事。

我最常用的组合是:优化器和优化器内部用控制点约束,出结果后单独写一个高分辨率采样函数做最终验证。两者都对上了,才算过关。

3.3 时间映射与节点调整

轨迹规划中,路径和时间是两码事。B 样条曲线本质上是参数曲线C(t),其中t可以直接取成时间。那么速度就是dC/dt,加速度是d²C/dt²,非常直观。

给定八个路点后,每个路点对应一个时间戳,这些时间戳同时就是节点向量里的内部节点值。时间戳之间的间隔Δt_i直接决定了这一段的速度水平:Δt_i越小,意味着机械臂在单位时间内走过更大的角度,速度自然越大。既然约束速度加速度,本质上就是寻找一组合适的时间间隔,让机械臂在需要快速通过的地方快速通过,在需要精确到位的地方慢下来

优化变量通常是七个时间间隔Δt1...Δt7,加上起点时间固定为 0,总时间就是所有间隔之和。目标函数通常是总时间最短,或者总时间与冲击峰值的加权和。约束条件就是速度控制点和加速度控制点的幅值界限。这个优化问题规模不大,变量只有七个,非常适合拿 NSGA-II 这类进化算法来试。

4. MATLAB 程序框架与关键实现

4.1 程序总体结构

我在实际工程里的 MATLAB 程序大概分成这五个文件:

  • main_traj_plan.m:主脚本,定义路径点、约束、调用优化并绘图。
  • BasisFun.m:递归计算 B 样条基函数。
  • ComputeControlPoints.m:反算控制点。
  • DeriveControlPoints.m:计算导数控制点。
  • EvalBSpline.m:根据控制点和节点向量求任意时间点的位置、速度、加速度。

主脚本的逻辑就是前面那句话:读入八个点 → 初始化时间向量 → 反算控制点 → 求导 → 检查约束 → 不满足就优化迭代。下面我逐段拆关键代码。

4.2 基函数与节点向量构造

节点向量按夹持方式构造。假设八个路点对应的时间参数是t(1)t(8),两端各加 8 个重复节点,内部节点通常直接用路点时间值:

% p = 7, nControl = 8, nKnot = 16 knots = zeros(1, 16); knots(1:8) = t(1); knots(9:16) = t(8); knots(9:14) = t(2:7); % 视情况调整,保证非递减

不过需要注意,只给八个路点八个内部位置时,为了满足节点向量长度,这套写法和标准的平均节点法略有不同。标准做法更稳妥的是用augknt或手动构造内部节点,例如:

% 八个点,七次,控制点数等于数据点数,节点向量两端重复 8 次 knots = [repmat(t(1),1,8), t(2:7), repmat(t(8),1,8)];

这样可以保证节点向量长度为 8 + 6 + 8 = 22,反而比刚才的 16 多。这里就有一个很关键的数学细节:控制点数、数据点数、节点数三者必须满足m = n + p + 1。当你只有 8 个数据点且要求完全插值时,控制点数也是 8,那么节点总数必须是 16,于是中间没有任何内部节点,退化为 Bézier。如果你把内部节点加进去,控制点数就要相应增加,插值问题变成欠定或超定,需要用最小二乘。因此代码里到底用哪种,取决于你要“精确插值”还是“逼近”。我在实际项目里为了保留局部性,常常在八个路点之间再插入中间节点,然后用最小二乘逼近,路径点误差控制在 1e-4 度量级,换取局部可控性。你如果只是跑通流程,退化为 Bézier 反而最简单,不会出矩阵奇异问题。

基函数递归写起来也很直接:

function N = BasisFun(i, p, t, knots) if p == 0 if t >= knots(i+1) && t < knots(i+2) N = 1; else N = 0; end return; end left = 0; right = 0; denomL = knots(i+p+1) - knots(i+1); if denomL > 0 left = (t - knots(i+1)) / denomL * BasisFun(i, p-1, t, knots); end denomR = knots(i+p+2) - knots(i+2); if denomR > 0 right = (knots(i+p+2) - t) / denomR * BasisFun(i+1, p-1, t, knots); end N = left + right; end

递归版本适合学原理,项目里跑大量采样点会慢,建议改成循环版本,或者直接用 MATLAB 的bsplvb等底层函数。

4.3 反算控制点

有了基函数,反算就顺理成章:

A = zeros(8,8); for j = 1:8 for i = 1:8 A(j,i) = BasisFun(i, 7, t(j), knots); end end P = A \ Q; % Q 是 8 个路径点组成的矩阵,每列一个维度

这个P就是位置曲线的控制点矩阵。注意时间点t(j)最好选择节点向量中的有效区间,避免取到末端重复节点的边界处,否则矩阵可能奇异。我一开始写这步时,习惯把t取成linspace(0,T,8),后来发现等间隔在某些路点分布极不均匀时会出问题,于是改成按路点之间的弦长比例分配时间,矩阵条件数明显改善。

4.4 速度加速度计算与约束检查

导数控制点计算封装成函数:

function dP = DeriveControlPoints(P, p, knots) n = size(P,2) - 1; dP = zeros(size(P,1), n); for i = 1:n denom = knots(i+p+2) - knots(i+1+1); % 对应零基公式分母 u_{i+p+1} - u_{i+1} if denom > 0 dP(:,i) = p * (P(:,i+1) - P(:,i)) / denom; end end end

有了这个函数,速度控制点和加速度控制点就是连续调用两次:

V = DeriveControlPoints(P, 7, knots); kV = knots(2:end-1); % 导数曲线的节点向量 A = DeriveControlPoints(V, 6, kV); kA = kV(2:end-1);

约束检查则直接看控制点的绝对值:

if max(abs(V(:))) > v_max disp('速度超限'); end if max(abs(A(:))) > a_max disp('加速度超限'); end

这里我特别强调一句:由于凸包性质,控制点不超限则整条曲线不超限。所以优化时完全可以用这个判据作为硬约束,不需要在每轮迭代里密集采样判断,计算量小很多。

4.5 基于 NSGA-II 的时间节点优化思路

如果七个时间间隔交给 NSGA-II 优化,先用冒号创建种群个体向量:

population(i).dt = [dt1, dt2, dt3, dt4, dt5, dt6, dt7];

每个个体对应一套时间分配。把dt累积成时间戳:

t = cumsum([0, pop.dt]);

构造节点向量,反算控制点,计算速度加速度控制点,然后计算两个目标:

  • 目标一:总时间T_sum = sum(pop.dt),希望它尽量小;
  • 目标二:速度或加速度的富余量,比如max(abs(V(:))) / v_max,希望它尽量低。

约束则直接设为速度控制点绝对值不超过v_max、加速度控制点绝对值不超过a_max,违反约束的个体在非支配排序时直接淘汰或罚掉。NSGA-II 跑几十代后,会得到一条 Pareto 前沿,你可以从中挑一个总时间较短、又留有一定余量的解。

实际项目里,时间间隔不能设得太小,否则机器人运动学规划没问题,动力学却会陷入极限。我通常会给dt设置下界,比如 0.05 秒或 0.1 秒,这个值取决于伺服周期和机械臂的响应速度。

5. 仿真结果与分析

5.1 一组典型数据的输出曲线

我拿一组典型关节角数据做示例(单位:度):

q = [0, 10, 20, 18, 15, 22, 30, 25]

假设速度上限v_max = 20 deg/s,加速度上限a_max = 50 deg/s²。先按等间隔时间 1 秒安排八个点,跑一下程序,位置曲线看起来挺平滑,但速度峰值很轻松就超过 30 deg/s,直接触发超限。随后我把时间间隔交给 NSGA-II 优化,目标是最小总时间,约束为速度 20、加速度 50,最终得到总时间约 10.4 秒,速度峰值 19.7,加速度峰值 48.2,都在限制内。

绘制曲线时,我用linspace(0, T, 500)采样并调用EvalBSpline输出位置、速度、加速度曲线。图上最直观的现象是:优化后的速度曲线在路点附近快速爬升,中段平稳,接近末端时再来一个平缓减速,完全符合人工规划直觉。

5.2 约束验证:不能只靠肉眼

看曲线觉得没问题,只是第一步。我会额外写一个验证脚本,用更高分辨率采样,比如 5000 个点,逐点检查速度加速度是否超限。由于优化内部已经检查了控制点,理论上不会有超限点,但采样验证能捕捉到数值计算误差以及我代码里的索引 bug。

有一次,我的速度曲线端点因为节点向量边界条件没处理好,出现了一个无限大的尖峰,而控制点检查居然没发现,就是因为导数控制点在边界处分母接近零。改成高分辨率采样后,那个尖峰立刻暴露出来。所以我的建议是:控制点约束用于优化,采样验证用于交付前复查,两者缺一不可。

6. 调试经验与常见问题

6.1 常见问题速查表

我把这套程序运行中容易踩的坑整理成一张表,遇到问题时直接对照排查:

问题现象可能原因解决办法
矩阵奇异,反算控制点报错时间戳与节点向量不匹配,或路径点重合检查时间戳单调递增;去掉重复路径点;改用最小二乘逼近
曲线在端点附近严重振荡节点向量未夹持,或时间间隔分配极不均匀两端补足 p+1 个重复节点;按弦长比例分配时间间隔
速度曲线端点出现大尖峰导数控制点在边界处分母接近零检查节点向量去首尾后的长度;给端点时间加上微小偏移
总时间太长约束过严,或时间下界设得太大适当放宽加速度限制;减小时间间隔下界
优化结果局部最优用 fmincon 时初始点选得不好换多起点,或改用 NSGA-II 类全局搜索
轨迹在某一维度突变相邻路点变化率过大,时间分配不足针对该维度单独增加该段时间间隔
MATLAB 未安装 Curve Fitting Toolbox代码里用了 spapi、fnder 等函数改写自实现版本,或先确认工具箱环境

6.2 独家避坑技巧

七次 B 样条这个“高次”属性,在数据点分布不均匀时很容易把曲线拉出过冲。八个点角度变化幅度差异很大的话,比如前两点只差 2 度,后两点差 30 度,高次曲线的振荡会很吓人。我的处理办法是:对待优化的时间向量做归一化处理,让每段时间占比与对应路径段的角度变化量大致呈正比,再扔给优化器微调。这样既保证优化空间,又避免初始解过于离谱。

另一个技巧是端点条件要显式处理。机械臂从静止开始、到静止结束,是常见需求。在反算控制点前,我会额外加入虚拟点或直接指定一二阶导数控制点满足边界条件。如果你用spapi这类现成函数,可以直接指定边界导数,但用自己写的反算函数时,需要手动扩展方程组,把首尾速度、加速度约束当作附加行加进去。这一步写起来稍微麻烦,但能避免规划出的轨迹在起点和终点产生明显的速度跳变。

还有一个非常容易被忽略的细节:B 样条的参数 t 不能直接用优化器给的任意时间戳,必须确保节点向量严格非递减。如果 NSGA-II 变异算子生成了乱序的时间向量,节点向量里会出现负数区间,基函数计算全部出错。我在写约束函数时,第一件事就是if any(diff(t) <= 0), 直接返回一个大惩罚值,从源头挡住非法个体。

最后再说说“基于 NS”的优化。标题如果指的是 NSGA-II,那么我要提醒:NSGA-II 适合做多目标权衡,但如果你只关心“总时间最短且不超限”,用fminconga反而更快。我在项目里通常先用 NSGA-II 跑一遍,看总时间和速度富余量的趋势关系,选定一组比较稳的参数后,再用fmincon在该点附近做局部精调。两层结合,既保证全局搜索能力,又保证最终解光滑可用。

我做这套方案最大的体会是:高次 B 样条本身不复杂,复杂的是约束条件怎么合理表达、时间节点怎么高效优化。把 B 样条的凸包性质用好,约束检查就从“逐点采样”变成了“查控制点”,整个优化过程轻一个量级。如果你也正在做机械臂轨迹规划,不妨先从八个点的七次 B 样条入手,跑通这整条链路之后,再往动力学约束、避障、时间最优这些方向扩展。这条路我走过,确实能省不少时间。

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

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

立即咨询