教程 · 中级 · 40 分钟

运动曲线规划:梯形与 S 形加减速

为什么直接给位置指令会让电机丢步、机器撞击作响,梯形曲线如何解决这个问题,以及限制 jerk 的 S 形曲线要多花多少时间。

引言

告诉电机去某个位置,它会立刻试着到那里。不是“快点”——是立刻。这条指令里没有任何关于多快的信息,所以唯一可用的解释就是“越快越好”,而接下来发生什么由最先耗尽的那一样决定:转矩、限流、附着力,或者步进电机跟不上的能力。

运动曲线就是那份缺失的信息。它把“去 100 mm”变成一份时间表——从现在到到达之间的每一个瞬间轴该在哪——并交给控制器一个机器真能交付的速度下移动的目标。

这就是“一台机械臂到达了”和“一台机械臂到达并且不抖了”之间的区别。

裸位置指令实际上要求了什么

想象一个阶跃:t = 0 时目标从 0 mm 跳到 100 mm。

要精确跟随它,轴需要无限速度,而无限速度需要无限加速度,无限加速度需要无限力。它做不到,于是有东西要让步:

  • 步进电机被下发一个在那个速度下它的转矩根本产生不出的步进率,于是丢步——而它跑在开环下,自己根本不知道丢了。位置从此永久地错。为什么会这样,以及为什么永远是四步一次。
  • 带 PID 环的直流电机看到一个巨大的瞬时误差,输出饱和,积分项卷绕,然后超调。
  • 舵机一头撞到最大速率到达,整个机构还在动。

这些都不是调参问题。它们是同一个问题:指令在物理上不可能。

梯形曲线

解法是下发速度,并以电机能维持的速率去改变速度。以恒定加速度升到巡航速度,保持,再以同样的速率减速。把速度对时间作图,它就是一个梯形,名字就是这么来的。

三个数定义它:距离 d、极速 v_max、加速度 a。

t_accel = v_max / a                     time spent ramping
d_accel = v_max² / (2a)                 distance used by one ramp
t_total = v_max / a  +  d / v_max       whole move, if it reaches v_max

一次 100 mm 的移动,速度 100 mm/s,a = 500 mm/s²:每段斜坡用 0.2 s、走 10 mm,剩下 80 mm 巡航,合计 1.2 s。

下面是整件事写成时间的函数——在瞬间 t,你的控制环该追的位置:

// Target position t seconds into a trapezoidal move of `d` mm.
float profilePosition(float t, float d, float vMax, float a) {
  float tAcc = vMax / a;
  float dAcc = vMax * vMax / (2 * a);

  if (2 * dAcc > d) {                      // too short to reach vMax
    float tPeak = sqrtf(d / a);            // triangular profile
    if (t >= 2 * tPeak) return d;
    if (t < tPeak) return 0.5f * a * t * t;
    float left = 2 * tPeak - t;
    return d - 0.5f * a * left * left;
  }

  float tCruise = (d - 2 * dAcc) / vMax;
  float tTotal  = 2 * tAcc + tCruise;
  if (t >= tTotal) return d;
  if (t < tAcc)           return 0.5f * a * t * t;
  if (t < tAcc + tCruise) return dAcc + vMax * (t - tAcc);

  float left = tTotal - t;
  return d - 0.5f * a * left * left;
}

注意这个函数不是什么:它不是控制器。它生成一个设定点。你原本有的东西——步进调度器、PID 环、舵机写值——仍然负责跟随。曲线只改变它跟随的东西。

你要的移动不总是你得到的移动

那个 2 * dAcc > d 分支是人们最先遇到、也最先误诊的情况。

如果移动短于加速上去再减速下来所需的距离,轴永远到不了你设定的速度。它斜坡上升,撞到中点,立刻斜坡下降——那是三角形,不是梯形。

阈值是 d < v_max² / a。对上面那组数来说是 20 mm,所以任何短于 20 mm 的移动,无论你怎么设,速度都上不到 100 mm/s。

在同样 500 mm/s 平方和 100 mm/s 极速的曲线设置下三次移动的速度对时间曲线图。8 mm 的移动形成一个在 0.25 秒后峰值 63 mm/s 的三角形,始终碰不到虚线标出的设定极速。20 mm 的移动刚好在顶点达到 100 mm/s。60 mm 的移动形成带清晰巡航段的梯形,用时 0.80 秒。
20 mm 以下,这根轴是距离先于斜坡用完的。提高限速对这些移动毫无作用——只有提高加速度才有。 下载 SVG

这就是为什么在一台做大量短移动的机器上提高 v_max 常常测不出任何变化。这些移动从来就不是速度受限的,它们是加速度受限的。做致密填充的 3D 打印机就是日常例子。

拐角仍然不连续

梯形曲线解决了速度。看加速度,问题只是挪了个地方。

t = 0 时加速度从 0 瞬间跳到 500 mm/s²。斜坡顶端又瞬间掉回 0。每一个都是加速度上的一个阶跃——而加速度的变化率是 jerk,所以加速度的一个阶跃就是一次无限的 jerk 冲激。

无限也不是机器能产生的量。它产生的是别的东西:

  • 每段斜坡的起止处一声可听的敲击。
  • 振铃——皮带会拉伸、联轴器会上弦,而结构是有柔顺的,所以一次突变的作用力会激励它,之后它就振荡起来。在打印机上,这就是你在锐利特征旁边看到的鬼影。
  • 转矩尖峰,足以触发限流——而平均转矩那个数字说它们是够的。
同一次 100 mm 移动的梯形曲线与 S 形曲线的四幅堆叠时间曲线图。位置:两者都是平滑的 S 形到达 100 mm,S 形稍晚一点。速度:梯形是直斜坡加锐利拐角,S 形是圆角并晚 0.1 秒完成。加速度:梯形是在正负 500 之间瞬跳的方波,S 形是在两者之间斜坡的梯形。Jerk:S 形是有界方波,梯形曲线则在拐角处画成四个无界冲激箭头。
同一次移动在两种曲线下的样子。每一行都是上一行的导数,而那个不连续点并不会因为你求导就消失——它只是顺着这一摞往下挪,直到变成一次冲激。 下载 SVG

S 形曲线

S 形曲线同时限制 jerk 和加速度。它不再一步跨到满加速度,而是在一个 jerk 时间 t_j = a_max / j_max 内斜坡升上去,保持,再斜坡降下来。速度曲线于是获得了圆角——名字由此而来——而加速度本身变成了一个梯形。

一次完整的 S 形移动有七个阶段:jerk 上升、保持加速度、jerk 下降、巡航,以及镜像的减速停车。

代价:正好一个 t_j

这一部分值得记进脑子,因为它让这个取舍变得具体。

当两种曲线达到同样的 v_max 和同样的 a_max 时,S 形移动多花的时间正好是 t_j——一个 jerk 时间,不是两个,而且与移动多远无关:

trapezoidal   T = v_max/a + d/v_max
S-curve       T = v_max/a + d/v_max + t_j          where t_j = a_max / j_max

对上面那次移动:1.2 s 变成 1.3 s。这次移动上多付 8%,更长的移动上百分比更小,换来的是有界的 jerk。

之所以是一个而不是两个:S 形的加速段更长,所以在开始巡航前多走了一些距离,于是巡航段缩短,把大部分时间还了回来。只有 jerk 时间本身是不可回收的。

这里面藏着一个约束。只有当 v_max ≥ a_max² / j_max 时加速度才真的达到 a_max。低于这个值,这次移动连满加速度也到不了,你会得到一个三角形加速度曲线——同一个“距离不够”的故事,只是往上求导了一阶。

打印机固件里的“Jerk”不是 jerk

在动手调任何东西之前值得知道,因为这个词本身会主动误导你。

经典 Marlin 的 DEFAULT_XJERK不是加速度的导数。它是两次移动之间的交界处允许的最大瞬时速度变化,单位是 mm/s——一个让规划器不用减速到零就能过弯的过弯余量。有用,但是另一个量,名字和单位都是错的。

较新的 Marlin 改用 Junction Deviation,它把拐角建模成一段给定偏差(毫米)的圆弧。Klipper 用 square_corner_velocity——90° 弯以多快的速度通过。这三者都是在回答“我怎么才能不在每个拐角都停下来”,而没有一个是物理意义上的 jerk 限幅。

所以如果论坛帖子让你把 jerk 调低来止振,先确认说的是哪个量。降低交界速度能减小过弯冲击,但它并不限制单次移动内部的 da/dt。

你的 PID 可能没问题

如果你有一根闭环轴,每次移动都超调振荡,本能反应是把它调钝——降 Kp、加微分、去和超调搏斗。

先试着把设定点曲线化。

一个阶跃输入是在向 PID 控制器索要无限输出。它的误差项一开始就是整段移动的距离,微分项看到一个瞬时跳变,而积分项在输出饱和、轴无法响应时持续累积。把控制器调钝,是为了扛过那一个不可能的瞬间,却让它在所有地方都变得迟钝。

有了曲线,整个移动过程中误差一直很小,因为目标始终在轴真正能到的某个地方。那组对阶跃完全不可用的增益,对斜坡往往表现良好——而这时控制器只在纠正负载和摩擦,那正是它该干的活。你可以在 PID 模拟器里把阶跃设定点和渐变设定点对比着看。

在真实机器人上你会遇到它的地方

  • 步进电机。AccelStepper的 setMaxSpeed() 和 setAcceleration() 只给你梯形曲线,仅此而已。没有 S 形选项;要那个得自己生成设定点。
  • 业余级舵机。SG90有一个你既看不见也调不了的内部控制器,所以所谓曲线化,就是把指令角度改成一步步渐变,而不是一次调用写出目标——同一个想法从外面施加。
  • **带编码器的直流电机。**把位置设定点曲线化,让编码器的 PID 去跟随它。这是最见收益的组合。
  • **移动机器人。**同样的限制也适用于 cmd_vel 桥上的 linear.x:下发一个速度阶跃会让轮子打滑,从而污染里程计。给 twist 加限速。
  • **打印机和 CNC。**Marlin、Klipper 和 GRBL 都在规划带过弯规则的梯形移动。

出问题时

现象 通常原因
步进电机在负载下悄悄丢位 没有曲线——下发的速率超过起动转矩
机器在移动起止处敲击 梯形的 jerk 冲激;需要 S 形或更低的加速度
锐利特征旁振铃 加速度阶跃激起了结构共振
短移动无视速度设定 三角形曲线——加速度受限,不是速度受限
每次移动都超调振荡 阶跃设定点进了闭环;先曲线化再谈调参
提高加速度有用,然后突然没用 现在是转矩受限而不是曲线受限了
启动时里程计漂得厉害 速度阶跃导致轮子打滑;给指令限速

曲线化是大多数业余运动系统能拿到的最便宜的大幅改进,因为它只花一个函数、零硬件。机械臂模拟器是看它为什么在长连杆末端重要的好地方——底座上一个小的加速度阶跃,在夹爪端会变成看得见的晃动。在移动机器人上,同一条曲线正是让路径跟踪器能为比转弯半径更急的弯减速、而不是根本跟不上的原因。

探索关联图谱

本教程所属的搭建

包含本教程的项目和学习路线。

延伸阅读

参考资料