1. 为什么电机控制绕不开“速度曲线”这道坎
做运动控制、机器人、CNC、自动化设备的朋友,对“速度曲线规划”这个词肯定不陌生。简单说,它解决的是这样一个问题:电机从一个位置A运动到位置B,中间这段路怎么走,速度怎么变,才能在“快、准、稳”三者之间找到平衡点。
如果不做任何规划,直接给电机发一个阶跃速度指令,比如让它瞬间从0转到3000转,电机会怎样?轻则丢步、过冲,重则机构振动、撞击限位,甚至烧毁驱动器。因为机械系统有惯性,电气系统有响应延迟,物理世界不允许速度突变。这就好比开车,你不可能一脚把车速从0踩到120,轮胎会打滑,人也受不了那个加速度。
梯形速度曲线是最基础、最常用的一种速度规划方案。它的速度-时间图像像一个等腰梯形:加速段、匀速段、减速段。名字很直白,形状也直观,但真正把它用明白,里面涉及的参数计算、边界判断、离散化实现、加速度不连续带来的冲击问题,每一个细节都值得抠一抠。
这篇文章我会把梯形速度曲线从原理到代码实现、从参数计算到实际调参经验,完整拆开讲一遍。适合刚入门运动控制的工程师,也适合已经写了几年规划代码但没仔细想过“为什么这么写”的老手。
2. 先说清楚:梯形速度曲线到底在优化什么
2.1 物理约束下的“最快路径”
任何运动控制系统,都要先回答一个数学问题:在加速度、减速度、最大速度都受限的条件下,从起点到终点,怎么走时间最短?
梯形速度曲线就是这个问题的最优解。为什么?因为在匀加速、匀速、匀减速这三种运动模式下,时间最短的路径只能由它们组合而成。这个结论可以从最优控制理论里的“Bang-Bang控制”推导出来:当加速度受限时,最优策略就是要么以最大加速度加速,要么以最大减速度减速,要么匀速,不存在“慢慢加速”这种中间状态。
把边界条件列一下:
- 最大速度 ( v_{max} ):由电机额定转速、机械结构承受能力决定
- 最大加速度 ( a_{acc} ):由电机扭矩、负载惯量决定
- 最大减速度 ( a_{dec} ):通常不等于加速度,因为制动能力和加速能力不一定一样,尤其是竖直轴要考虑重力
这三组参数就是整个规划的“天花板”。梯形曲线做的事情,就是在这些约束内,找一个时间最优的运动方案。
2.2 三个关键参数:距离、速度、加减速度
实际工程中,我们通常已知的是:
- 起始位置 ( q_0 )
- 目标位置 ( q_1 )
- 规划周期 ( T_s )(比如1ms、125us)
然后我们要根据用户设定的 ( v_{max} )、( a_{acc} )、( a_{dec} ) 来生成一条从 ( q_0 ) 到 ( q_1 ) 的轨迹。
这里有个工程上必须注意的点:用户设定的 ( v_{max} ) 不一定是实际能达到的速度。如果两点之间的距离很短,可能刚加速到一半就要开始减速了,根本到不了 ( v_{max} )。这种时候梯形退化成了三角形——只有加速段和减速段,没有匀速段。代码里必须要处理这种情况,否则计算出来的匀速段时间是负数,就出bug了。
判断条件很简单:按照设定的加速度从0加速到 ( v_{max} ) 再减速到0,所需要的距离是:
[ s_{need} = \frac{v_{max}^2}{2a_{acc}} + \frac{v_{max}^2}{2a_{dec}} ]
如果 ( s_{need} ) 大于实际位移距离 ( s = |q_1 - q_0| ),说明跑不满最大速度,此时实际峰值速度要按下面的公式反算:
[ v_{peak} = \sqrt{\frac{2s \cdot a_{acc} \cdot a_{dec}}{a_{acc} + a_{dec}}} ]
这个公式是梯形规划里最容易漏掉的地方。很多初写代码的人只算了正常梯形的情况,没考虑短距离,结果距离一短匀速段时间算成负数,轨迹形态直接崩了。
3. 梯形规划的完整推导:从公式到代码
3.1 分情况讨论:完整梯形和退化三角形
把情况分清楚,代码就好写了。
情况A:能跑满最大速度(完整梯形)
此时轨迹分成三个阶段:
- 加速段:从0到 ( v_{max} ),耗时 ( t_{acc} = v_{max} / a_{acc} ),位移 ( s_{acc} = v_{max}^2 / (2a_{acc}) )
- 匀速段:以 ( v_{max} ) 运行,耗时 ( t_{c} = (s - s_{acc} - s_{dec}) / v_{max} )
- 减速段:从 ( v_{max} ) 到0,耗时 ( t_{dec} = v_{max} / a_{dec} ),位移 ( s_{dec} = v_{max}^2 / (2a_{dec}) )
总时间:
[ t_{total} = t_{acc} + t_c + t_{dec} ]
情况B:跑不满最大速度(三角形)
此时只有两个阶段:
- 加速段:从0到 ( v_{peak} ),耗时 ( t_{acc} = v_{peak} / a_{acc} )
- 减速段:从 ( v_{peak} ) 到0,耗时 ( t_{dec} = v_{peak} / a_{dec} )
没有匀速段。( v_{peak} ) 用前面那个公式反算。
3.2 规划的核心:时间轴上的三个关键节点
一旦知道 ( v_{peak} )、( t_{acc} )、( t_{dec} )、( t_{total} ),整条轨迹就完全确定了。剩下的事情就是把它离散化。
假设当前时刻为 ( t ),位置为 ( q(t) ),速度为 ( v(t) ),那么:
- 如果 ( t < t_{acc} ):处于加速段
[ v(t) = a_{acc} \cdot t ]
[ q(t) = q_0 + \frac{1}{2} a_{acc} t^2 ]
- 如果 ( t_{acc} \le t < t_{acc} + t_c ):处于匀速段
[ v(t) = v_{peak} ]
[ q(t) = q_0 + s_{acc} + v_{peak} (t - t_{acc}) ]
- 如果 ( t \ge t_{acc} + t_c ):处于减速段
[ v(t) = v_{peak} - a_{dec} (t - t_{acc} - t_c) ]
[ q(t) = q_0 + s_{acc} + v_{peak} \cdot t_c + v_{peak}(t - t_{acc} - t_c) - \frac{1}{2} a_{dec} (t - t_{acc} - t_c)^2 ]
代码写出来是这个样子:
import math class TrapezoidProfile: def __init__(self, q0, q1, v_max, a_acc, a_dec, ts): self.q0 = q0 self.q1 = q1 self.v_max = v_max self.a_acc = a_acc self.a_dec = a_dec self.ts = ts self.s = q1 - q0 self.sign = 1.0 if self.s >= 0 else -1.0 self.s = abs(self.s) # 计算跑满最大速度所需距离 s_need = (v_max * v_max) / (2.0 * a_acc) + (v_max * v_max) / (2.0 * a_dec) if self.s >= s_need: self.v_peak = v_max self.t_acc = v_max / a_acc self.t_dec = v_max / a_dec self.s_acc = v_max * v_max / (2.0 * a_acc) self.s_dec = v_max * v_max / (2.0 * a_dec) self.t_c = (self.s - self.s_acc - self.s_dec) / v_max else: # 三角形:跑不满最大速度 self.v_peak = math.sqrt(2.0 * self.s * a_acc * a_dec / (a_acc + a_dec)) self.t_acc = self.v_peak / a_acc self.t_dec = self.v_peak / a_dec self.s_acc = self.v_peak * self.v_peak / (2.0 * a_acc) self.s_dec = self.v_peak * self.v_peak / (2.0 * a_dec) self.t_c = 0.0 self.t_total = self.t_acc + self.t_c + self.t_dec self.current_t = 0.0 def reset(self): self.current_t = 0.0 def step(self): t = self.current_t self.current_t += self.ts if t < 0: q = self.q0 v = 0.0 elif t < self.t_acc: q = self.q0 + self.sign * (0.5 * self.a_acc * t * t) v = self.sign * self.a_acc * t elif t < self.t_acc + self.t_c: q = self.q0 + self.sign * (self.s_acc + self.v_peak * (t - self.t_acc)) v = self.sign * self.v_peak elif t < self.t_total: dt = t - self.t_acc - self.t_c q = self.q0 + self.sign * (self.s_acc + self.v_peak * self.t_c + self.v_peak * dt - 0.5 * self.a_dec * dt * dt) v = self.sign * (self.v_peak - self.a_dec * dt) else: q = self.q1 v = 0.0 return q, v3.3 为什么在轨迹末端要做“位置闭合”
上面的代码里,最后一个 ( else ) 分支直接把位置拉到了 ( q_1 ),速度置0。这个设计有什么讲究?
理论上,只要前面的浮点计算都精确,减速段结束时刻的位置就是 ( q_1 )。但实际上,浮点数有误差,采样周期 ( T_s ) 也不一定能量除总时间,最后一个周期的时间步长往往不是整数倍。这会导致一个结果:程序运行到最后,位置停在 ( q_1 ) 附近,但差了一点点,速度也不完全是0。
如果不做闭合,控制器就会拿着这个“差一点”的位置误差继续修正,产生末端抖动。尤其是点位运动场景,每一次都停在 ( q_1 \pm \epsilon ) 的位置,累计误差就会显现出来。
所以在工程实现里,必须把“轨迹结束”做一次显式处理:
- 当前时间超过 ( t_{total} ),位置强制等于 ( q_1 )
- 速度强制为0
- 规划器状态置为“完成”,后续不再输出新的运动指令
这段逻辑看起来简单,但少了它,系统稳定性会差一个档次。
4. 梯形曲线的“硬伤”:加速度突变
4.1 为什么梯形曲线会有冲击
梯形曲线虽然实现简单,但它有一个天然的缺陷:加速度在四个时刻点会发生跳变。
具体是这四个点:
- ( t=0 ) 时刻,加速度从0突变到 ( +a_{acc} )
- ( t=t_{acc} ) 时刻,加速度从 ( +a_{acc} ) 突变到0
- ( t=t_{acc}+t_c ) 时刻,加速度从0突变到 ( -a_{dec} )
- ( t=t_{total} ) 时刻,加速度从 ( -a_{dec} ) 突变到0
加速度突变意味着什么?意味着力的突变。力的突变意味着机械结构会受到冲击,也就是所谓的“柔性冲击”。
实际设备上这是什么表现?
- 龙门架式的设备启停瞬间会“咯噔”一下
- 皮带传动机构会有明显的顿挫感
- 高速轻载场景下,末端会振动,甚至激起机械共振
- 减速机齿轮磨损会加快
拿一根悬臂梁来说,你用手快速推一下,它会晃动。电机动起来也一样,加速度突变相当于给机械结构一个脉冲激励,结构会以自己的固有频率振动起来,然后慢慢衰减。这就是为什么有些设备低速走没问题,一高速走就抖。
4.2 S型曲线为什么能解决这个问题
S型速度曲线的思路是:不让加速度突变,而是让加速度自身也平滑地变化。也就是说,引入第三个约束——加加速度(jerk),也就是加速度的变化率,单位是 ( m/s^3 )。
S型曲线在梯形曲线的转折点处,用一段斜坡把加速度的突变“抹平”。这样施加在机械结构上的力,变化是连续的,冲击自然就小了。
但S型曲线也不是没有代价:
- 规划算法复杂得多,需要解七段分段方程
- 计算量大,对控制周期短的场合(比如125us)增加负担
- 参数多了,调起来更麻烦
所以实际工程里,很多场合仍然优先用梯形曲线。比如:
- 点对点定位要求不高、速度不快的场景
- 低速重载,系统刚度大、冲击不明显的场景
- 计算资源有限的嵌入式MCU
- 对实时性要求极高、规划周期极短的场合
一句话:不是所有场景都值得上S型,梯形曲线用对了地方,依然是最经济的选择。这个选择逻辑,后面在实战部分我会再展开讲。
5. 工程实战:梯形规划在MCU上的完整实现思路
5.1 离散时间下的位置更新:别忽略“步长”的影响
在MCU上做梯形规划,和Python里边仿真边看曲线完全是两种体验。嵌入式环境里,我们通常用一个定时器中断,按固定的控制周期执行规划器。每个周期做三件事:
- 读取当前规划时间
- 调用规划器计算当前位置和速度
- 把位置/速度指令发给位置环或速度环
代码结构大概是这样:
typedef struct { float q0; float q1; float v_max; float a_acc; float a_dec; float ts; float v_peak; float t_acc; float t_dec; float t_c; float t_total; float s_acc; float s_dec; float t_current; float sign; uint8_t is_finished; } TrapezoidProfile; void trapezoid_init(TrapezoidProfile* prof, float q0, float q1, float v_max, float a_acc, float a_dec, float ts) { prof->q0 = q0; prof->q1 = q1; prof->v_max = v_max; prof->a_acc = a_acc; prof->a_dec = a_dec; prof->ts = ts; prof->t_current = 0.0f; prof->is_finished = 0; float s = q1 - q0; prof->sign = (s >= 0.0f) ? 1.0f : -1.0f; s = fabsf(s); float s_need = (v_max * v_max) / (2.0f * a_acc) + (v_max * v_max) / (2.0f * a_dec); if (s >= s_need) { prof->v_peak = v_max; prof->t_acc = v_max / a_acc; prof->t_dec = v_max / a_dec; prof->s_acc = v_max * v_max / (2.0f * a_acc); prof->s_dec = v_max * v_max / (2.0f * a_dec); prof->t_c = (s - prof->s_acc - prof->s_dec) / v_max; } else { prof->v_peak = sqrtf(2.0f * s * a_acc * a_dec / (a_acc + a_dec)); prof->t_acc = prof->v_peak / a_acc; prof->t_dec = prof->v_peak / a_dec; prof->s_acc = prof->v_peak * prof->v_peak / (2.0f * a_acc); prof->s_dec = prof->v_peak * prof->v_peak / (2.0f * a_dec); prof->t_c = 0.0f; } prof->t_total = prof->t_acc + prof->t_c + prof->t_dec; } void trapezoid_plan(TrapezoidProfile* prof, float* pos_cmd, float* vel_cmd) { if (prof->is_finished) { *pos_cmd = prof->q1; *vel_cmd = 0.0f; return; } float t = prof->t_current; prof->t_current += prof->ts; float q = 0.0f, v = 0.0f; if (t < prof->t_acc) { q = 0.5f * prof->a_acc * t * t; v = prof->a_acc * t; } else if (t < prof->t_acc + prof->t_c) { q = prof->s_acc + prof->v_peak * (t - prof->t_acc); v = prof->v_peak; } else if (t < prof->t_total) { float dt = t - prof->t_acc - prof->t_c; q = prof->s_acc + prof->v_peak * prof->t_c + prof->v_peak * dt - 0.5f * prof->a_dec * dt * dt; v = prof->v_peak - prof->a_dec * dt; } else { q = prof->s_acc + prof->v_peak * prof->t_c + prof->s_dec; v = 0.0f; prof->is_finished = 1; } *pos_cmd = prof->q0 + prof->sign * q; *vel_cmd = prof->sign * v; }这里有几个工程细节值得拿出来单独讲。
第一个细节:单位换算。很多入行不久的同学,把位置单位搞混。电机的位置有几种表示方式:脉冲数、编码器计数、转数、毫米、弧度。规划器内部最好统一用一种单位,推荐用“用户单位”做规划,在跟驱动器交互时才做换算。比如你上位机用mm做单位,那么:
- 下发位置指令时要转换成脉冲数:( pulses = mm \times \text{pulses_per_mm} )
- 读反馈位置时要反向换算
- 速度和加速度的单位也随之成套转换
单位不统一,是运动控制领域最常见、也最隐蔽的bug来源。因为数值上可能差了几百倍,但程序不报错,只在运行效果上表现出诡异的行为——比如走位忽快忽慢、距离越长误差越大。
第二个细节:浮点运算与定点优化。很多MCU没有硬件浮点单元(FPU),比如一些低成本的Cortex-M0芯片。这时候直接拿float做规划,每一次乘除都要调用软件浮点库,性能开销不小。
解决方案有两种:
- 把加速度、速度、位置全部换成Q格式定点数(比如Q24、Q16),用整数运算模拟小数
- 加高主频或换带FPU的芯片
我的经验是:如果控制周期在1ms以上,用整数运算完全够用;如果控制周期在125us左右,建议直接上带FPU的MCU,省下的开发调试时间远比省几块钱芯片成本值。
5.2 在线修改目标位置:梯形曲线规划的“中途换点”
实际设备调试过程中,经常遇到需要在运动过程中修改目标位置的情况。典型场景:
- 视觉定位系统发现目标偏移,要求机械手中途转向新的目标点
- 安全光幕触发,机器需要立即停下或临时更改目标
- 流水线上游来料位置变化,夹具需要动态调整到位点
最简单的做法是:先停到当前位置,再用新的目标重新规划。这个方案稳妥,但损失效率。更高级的做法是“在线重规划”:从当前时刻的速度 ( v_{now} )、位置 ( q_{now} ) 出发,以它们作为新的初始条件,重新规划到新的目标点。
在线重规划的数学本质,是要处理“初速度不为0”的梯形规划。这就涉及到梯形规划的另一种通用形式——带初速度的版本。
假设当前速度为 ( v_0 ),目标速度为 ( v_1=0 ),距离剩余 ( s_{rem} = q_{new} - q_{now} )。如果要先加速到 ( v_{max} ) 再减速,那么所需距离是:
[ s_{need} = \frac{v_{max}^2 - v_0^2}{2a_{acc}} + \frac{v_{max}^2}{2a_{dec}} ]
如果 ( s_{rem} < s_{need} ),说明到不了 ( v_{max} ),需要计算一个转折速度 ( v_{turn} ),它满足:
[ s_{rem} = \frac{v_{turn}^2 - v_0^2}{2a_{acc}} + \frac{v_{turn}^2}{2a_{dec}} ]
解这个方程:
[ v_{turn} = \sqrt{\frac{2a_{acc}a_{dec}s_{rem} + a_{dec}v_0^2}{a_{acc} + a_{dec}}} ]
这个公式在在线重规划里非常关键。它要同时满足两个条件:既能从 ( v_0 ) 加速到 ( v_{turn} )(或直接减速),又能从 ( v_{turn} ) 减速到0,且总距离刚好等于 ( s_{rem} )。
还要注意方向问题:如果 ( v_0 ) 的方向和 ( s_{rem} ) 的方向相反,说明电机动着往错误的方向跑,此时要先减速到0,再反向加速。这相当于把轨迹拆成两段:先走一个“减速停”轨迹,再走一个“从0开始”的梯形轨迹。工程上一般会直接设一个标志位,让逻辑变成“先停稳再动”,避免处理复杂的反向重规划。
5.3 抖动抑制:一个简单但有效的“末端整形”
梯形曲线的末端有个物理现象:减速到0的瞬间,如果指令速度恰好为0,但实际机械末端还在因为惯性微幅振荡,那么位置环会一直试图修正,表现为末端“嗡嗡”抖动。
处理方法之一,是在轨迹末端做“软着陆”。具体做法:在距离目标点还剩最后一段距离 ( s_{land} ) 时,不再按梯形曲线减速,而是切换成低通滤波后的位置指令,或者改用指数衰减速度曲线:
[ v(t) = v_{enter} \cdot e^{-(t-t_{enter})/\tau} ]
这里的 ( v_{enter} ) 是进入软着陆段时的速度,( \tau ) 是时间常数,一般取10~30ms,需要根据机构刚性实测调整。软着陆的意义在于:速度连续衰减且衰减速率逐渐减小,不会像梯形曲线那样在结束点产生加速度阶跃。
这个方法不改变梯形规划的框架,只在末端加了一个整形环节,实现成本很低,但对末端振动的抑制效果非常明显。我在两个项目上用过这个方案:
- 一台高速贴片机的Z轴,贴装前末段改用软着陆后,贴装压力波动从原来的15%降到了5%以内
- 一台桌面型三轴点胶机,XY轴改成软着陆后,点胶轨迹起点和终点的顿挫痕迹明显消失
值得说明的是,软着陆会略微增加总运动时间,但通常在几十毫秒以内,对节拍影响不大。如果你做的是高速高精度设备,这个时间成本换来的稳定性提升完全值得。
6. 调参实战:加速度、速度到底怎么给
6.1 先算负载惯量,再定加速度
很多新手拿到一台设备,第一件事就是拍脑袋设加速度,设大了电机报警过流,设小了节拍拖慢,反复试错浪费时间。实际上,加速度的选择有迹可循。
核心依据是牛顿第二定律的旋转形式:
[ T_{motor} = J_{total} \cdot \alpha + T_{load} ]
其中,( J_{total} ) 是折算到电机轴上的总惯量(电机转子惯量+负载折算惯量),( \alpha ) 是角加速度,( T_{load} ) 是负载力矩(摩擦力矩、重力矩等)。
如果电机额定扭矩是 ( T_{rated} ),那么允许的最大角加速度不能超过:
[ \alpha_{max} = \frac{T_{rated} - T_{load}}{J_{total}} ]
这个 ( \alpha_{max} ) 换算成直线加速度,还要乘上丝杠导程或齿轮减速比。换句话说,加速度的上限由“电机能输出的净力矩”除以“系统总惯量”决定。
我见过不少现场问题,都是给加速度太猛,驱动器一直工作在过流保护边缘,偶尔报一次过流报警,线体就停一次。这种问题不是梯形规划本身的错,而是参数没匹配上物理系统。
建议调参步骤:
- 先测系统总惯量(可以用加减速法:给定恒定力矩,测角加速度,反推惯量)
- 算出一个理论最大加速度
- 取理论值的50%~70%作为规划加速度,留出扭矩裕量
- 用示波器或驱动器自带监控观察实际电流,如果电流接近额定值,说明加速度给大了
6.2 梯形规划里,加速度和减速要不要分开设置
答案是要,而且强烈建议分开。
原因很简单:加速时,电机输出正向力矩,与运动方向相同;减速时,电机需要输出反向力矩。如果机械系统存在摩擦、重力等恒定的负载力矩(这在竖直轴里特别明显),那么:
- 加速时实际需要的力矩是 ( T_{acc} = J\alpha + T_{friction} + T_{gravity} )
- 减速时实际需要的力矩是 ( T_{dec} = J\alpha_{dec} - T_{friction} - T_{gravity} )
如果设同样的加减速度,加速段可能已经接近电机峰值扭矩,减速段则比较轻松。反过来,在竖直下行的场景里,重力会帮助加速,但减速时电机要额外抵抗重力,减速难度反而更大。
所以稳妥的做法是:先按相同值调试,如果某一段出现电流过大或振动明显,就把对应段的加/减速度调小。很多运动控制器,包括一些国产点位控制卡,都支持分开设定加减速度,就是这个原因。
6.3 参数整定的“黄金法则”:先小后大,先短后长
实际操作中,参数整定有个很实用的顺序法则。
第一步:用很小的速度(比如最大速度的10%)和很小的加速度(比如理论最大值的20%),跑一遍完整的点到点运动。确认方向正确、没有异响、没有任何报警。
第二步:逐步提高最大速度,观察电机电流、运行声音和停止时的过冲量。正常情况,速度提高后只应该看到运动时间缩短,不应该看到过冲增大——如果过冲增大,说明加速度跟不上,加减速段实际偏短,需要同时增大加速度。
第三步:逐步增大加速度,观察启动瞬间和停止瞬间的机械振动。一旦发现“咯噔”声或末端余振,就回调一点,找到临界值再留20%~30%裕量。
这个法则能让你在半小时内完成一台新设备的梯形规划参数确认,而不是靠猜。
7. 梯形曲线的一种实用变体:非对称加减速
7.1 为什么有时候必须用非对称
前面提到,竖直轴场景里,重力的影响导致加减速能力不对称。再举一个常见例子:皮带传输的载物台,带负载前进时摩擦大,返回空载时摩擦小。如果加减速度参数共用一套,要么前进段力矩不够,要么返回段振动明显。
解决办法是:定义两组参数,一组用于“正方向加速/反方向减速”,另一组用于“正方向减速/反方向加速”。形状上,这就变成一个非对称的梯形——左边的斜率和右边的斜率不一样。
实现上,这改动不大,只是把原来的 ( a_{acc} ) 和 ( a_{dec} ) 分别替换成两个方向的对应值。但要注意,边界判断公式也要跟着换:
正方向运动(( q_1 > q_0 )):
- 加速段用 ( a_{acc_fwd} )
- 减速段用 ( a_{dec_fwd} )
反方向运动(( q_1 < q_0 )):
- 加速段用 ( a_{acc_rev} )
- 减速段用 ( a_{dec_rev} )
这个变量对应关系极易搞混。我的建议是在命名上就区分清楚:
float a_acc_fwd; // 正向运动的加速度 float a_dec_fwd; // 正向运动的减速度 float a_acc_rev; // 反向运动的加速度(数值为正,方向由sign处理) float a_dec_rev; // 反向运动的减速度(数值为正,方向由sign处理)7.2 实现差异:两套判断逻辑
非对称的实现,变化集中在初始化阶段的位置判断。核心是先把运动方向定下来,再选对应的加减速度组合重新计算轨迹参数。其他阶段的计算逻辑完全一样,因为公式里用的是已经选好的 ( a_{acc} ) 和 ( a_{dec} )。
这个变体工程量不大,但对特定设备的运行效果提升明显。尤其是竖直轴,如果不用非对称参数,要么往上走加速度不足显得“肉”,要么往下走减速刹不住产生过冲。搞过Z轴的朋友应该深有体会。
8. 如果梯形曲线已经无法满足:何时换S型曲线
8.1 梯形和S型的适用边界
很多工程师会有个误区:S型曲线一定比梯形好。实际不是这样。
梯形曲线的优势是:计算简单、响应快、总时间最短。在相同 ( v_{max} )、( a_{max} ) 约束下,任何平滑程度更高的曲线(比如S型)总时间都会更长,因为它们要用一部分加速度能力去平滑过渡。
什么时候必须用S型?
- 末端定位精度要求极高,且机械结构阻尼小的场景(比如半导体贴片设备)
- 负载是易碎品或液体,冲击会导致损坏或飞溅的场景
- 丝杠、导轨间隙较大,加速度突变会产生反弹的场景
- 速度高且负载惯量大,加速度突变会激发结构共振的场景
什么时候用梯形就够了?
- 定位精度在0.1mm级别即可的设备
- 机构刚性好,比如重型铸铁底座的工作台
- 运动速度低,加速度本身设置得不大
- 控制周期短、运算资源有限,S型可能拖累实时性
说句实在话,很多中小型自动化设备,梯形曲线调好了完全够用。与其急着上S型,不如把梯形曲线的末端整形、加减速分段、方向区分这些细节做到位。实际效果差距并没有想象那么大。
8.2 从梯形平滑改造:五次多项式过渡
如果你实在不想整体迁移到S型,又需要降低冲击,有一个折中方案:用五次多项式对梯形曲线的转折点做局部过渡。
思路是这样的:在速度转折点附近取一个时间窗口 ( [t_1, t_2] ),窗口内用五次多项式连接转折前后的速度曲线。五次多项式的好处是:两端的位置、速度、加速度都可以精确匹配,也就是说连接处不会有跳变。
但这个方法实现起来有个麻烦:需要知道前后曲线的精确表达式,还要在过渡窗口内解五次多项式的系数。窗口长度选多少、在哪个位置切,都要调试。代码复杂度其实比直接上S型低不了多少。所以我的建议是:如果只是轻微改善冲击,可以用;如果目标是把振动明显降下来,还是正经用S型。
9. 复盘:梯形速度曲线做得好不好,看这几个细节
写了这么多,最后把梯形速度曲线规划中最容易出问题的点汇总一下。
第一,边界条件是否全覆盖。距离短到跑不满 ( v_{max} ) 时,是否走了三角形分支?初始和结束速度是否都为0?加减速度是否按方向分开设置?这些边界条件没处理干净,程序在极端工况下就会表现异常。
第二,单位制是否统一。位置、速度、加速度、时间四者的单位一定要成套。最稳妥的做法是全程用标准单位(m、m/s、m/s²、s)做规划,只在接口层做换算。千万别在规划器内部混用mm和脉冲。
第三,末端是否做了位置闭合。轨迹结束时,位置、速度必须精确设定为目标值和0,不能依赖浮点误差自然收敛。
第四,采样步长是否合理。采样周期 ( T_s ) 必须远小于轨迹总时间 ( t_{total} ),一般要求 ( t_{total} / T_s > 50 ),否则离散化误差会很明显。如果你发现轨迹走完位置偏差较大,先看一下是不是 ( T_s ) 太粗糙,而不是急着怀疑算法本身。
第五,有没有给振动留裕量。理论算出来的最大加速度,装到设备上至少要打七折。机械系统的实际刚度、间隙、阻尼,在线性仿真里都算不出来,现场实测才是准的。
梯形速度曲线是一把趁手的工具,但不是万能钥匙。搞懂它的原理,知道每个参数的作用以及参数变化对轨迹形态的影响,再结合现场实测慢慢调,你基本就能驾驭大多数点位运动控制场景。等哪天遇到梯形曲线确实解决不了的问题,再上S型也不迟。
最后再分享一个小技巧:调试阶段,把规划器的位置指令、实际反馈位置、速度指令三条曲线同时录下来观察。如果位置指令和反馈位置贴合良好,说明规划器工作正常,问题大概率在机构或者伺服参数;如果连指令本身就不平滑,那就先回头检查规划器的参数和逻辑,别急着动伺服增益。这个排查顺序,能帮你少走不少弯路。