做运动控制的工程师,十有八九都被加减速曲线折磨过。梯形加减速虽然简单,但启停瞬间加速度突变带来的冲击和振动,在高速高精场景下根本没法用。这时候就得请出S形曲线速度规划了。S形曲线,顾名思义,速度变化不是一条直线,而是带拐弯的平滑曲线,它的核心作用就是把加加速度(jerk)限制住,让整个运动过程既快又稳。
“5段式”是S形曲线的一种经典实用形式。很多人一听S形曲线就想到7段式,觉得复杂难啃。实际上,5段式在大多数点到点定位场景里已经足够好用,而且参数少、调起来快、计算量小,特别适合资源受限的嵌入式平台或者对实时性要求很高的上位机控制。这篇文章不绕弯子,直接讲5段式的数学原理、参数计算、C语言实现和工程里最容易踩的坑。
1. 内容整体设计与思路拆解
1.1 为什么需要S形曲线,5段式又是什么形态
先理清一个基本概念:运动控制里的“规划”,本质上是在回答一个问题——给定起点速度、终点速度、位移和约束条件(最大速度、最大加速度、最大加加速度),在这段路程里,速度随时间怎么变化。
梯形规划的思路是“匀加速-匀速-匀减速”,加速度是方波,在启动和结束瞬间加速度从0突变到设定值,产生无限大的加加速度。实际机械系统里,这意味着电机会瞬间发力又瞬间卸力,表现在设备上就是抖动、异响,精度也会受影响。S形曲线把加速度的变化也变成了斜坡,加加速度被限制在一个有限值,机械冲击大幅降低。
7段式S形曲线包含加加速、匀加速、减加速、匀速、加减速、匀减速、减减速7个阶段。但工程里有一个很常见的情况:加速段和减速段的最大加速度根本到不了设定值,因为路程太短,速度还没爬上去就得开始刹车。这时候,7段式里“匀加速”和“匀减速”这两段就不存在了,只剩下加加速、减加速、匀速(可能没有)、加减速、减减速,一共5段,这就是5段式名字的由来。
1.2 5段式相比7段式与梯形规划的核心优势
5段式既保留了S形曲线平滑性的“灵魂”,又砍掉了7段式中相对冗余的参数计算。在实际项目中,它的价值体现在这三方面:
第一,冲击控制。加速度的变化率被限制住,设备启停瞬间的“闯动感”明显下降,尤其适合负载较大或机构刚性较弱的场合。第二,计算效率高。5段式的时间计算、位置和速度插值公式比7段式简洁很多,没有复杂的条件分支套分支,在低主频MCU上也能轻松跑起来。第三,参数标定简单。需要调的参数只有最大速度、最大加速度、最大加加速度,每个参数物理意义明确,现场调试时很容易找到规律。
有人会问,既然7段式更通用,为什么不直接用7段式?答案是:如果目标场景经常是短距离点位运动,5段式已经覆盖了90%的情况,而7段式的大量代码分支反而容易引入边界条件bug。稍后我还会讲如何从5段式扩展成7段式,其实就多一步判断。
2. 5段式数学原理与参数推导
2.1 五个阶段的物理方程拆解
为了方便叙述,定义以下参数:
- v0:起始速度
- v1:加速段结束时的速度,通常等于最大速度vmax
- v2:减速段开始时的速度,通常也等于vmax
- v3:结束速度
- amax:最大加速度
- jmax:最大加加速度(jerk)
- t1:加加速段时间
- t2:减加速段时间
- t3:匀速段时间
- t4:加减速段时间
- t5:减减速段时间
在5段式里,加速段由“加加速”和“减加速”两段组成。加加速阶段的加速度从0线性增加到amax,减加速阶段的加速度从amax线性减少到0。减速段同理,不过方向相反。
下面是核心的运动学方程。加加速段(0到t1):
- 加加速度:j(t) = jmax
- 加速度:a(t) = jmax * t
- 速度:v(t) = v0 + 0.5 * jmax * t²
- 位移:s(t) = v0 * t + (jmax * t³) / 6
减加速段(t1到t1+t2):
- 加加速度:j(t) = -jmax
- 加速度:a(t) = amax - jmax * (t - t1)
- 速度:v(t) = v1 - 0.5 * jmax * (t - t1)²
注意,减速段的方程和加速段是对称的,只需要把加加速度取反、起始速度改成v2、加速度改为减速方向即可。这个对称性在实现时很关键,后面代码里我会直接利用它来减少代码量。
2.2 三类约束:速度、加速度、加加速度
任何运动规划都必须满足三个物理约束:
- 速度约束:任何时刻速度不能超过电机或机械机构允许的最大速度vmax。
- 加速度约束:加速度不能超过amax,否则电机会过流或机构会打滑。
- 加加速度约束:加加速度不能超过jmax,这是保证机械寿命和运动平稳性的核心。
5段式的“灵魂”就是让加加速度在每一段都是常数,要么是+jmax,要么是-jmax,要么是0。这样一来,加速度曲线就是梯形,速度曲线就是平滑的S形。如果梯形规划的加速度是矩形,S形规划的加速度就是梯形,这个关系很好记。
这里有一个关键点:实际规划时,如果路径总长度不够,你会发现无论怎么算,加减速都到不了vmax,甚至到不了amax。这时候必须降低目标速度或有效加速度。这个“降级”逻辑是规划器最容易出错的地方,后面我会详细讲。
2.3 各段时间和位移的计算公式推导
先看最理想的情况:总位移足够长,速度能到达vmax,加速度也能到达amax。此时:
加加速段的时间t1完全由amax和jmax决定:
t1 = amax / jmax
减加速段时间t2通常和t1相等,因为加速度从0升到amax需要的时间,从amax降到0也需要同样的时间。5段式的一个典型特征就是t1 = t2,t4 = t5。
加速段总位移:
s_acc = v0 * (t1 + t2) + 0.5 * jmax * t1² * (t1 + t2) - (jmax * t1³) / 6
这个公式看着复杂,实际因为t1 = t2 = amax / jmax,可以化简为:
s_acc = v0 * 2 * t1 + amax * t1²
同理,减速段从v2减速到v3,在对称情况下:
s_dec = v2 * 2 * t1 - amax * t1²
如果加速段能达到vmax,那么:
v1 = v0 + amax * t1
整个运动过程的总位移为:
s_total = s_acc + vmax * t3 + s_dec
通过这个公式可以反推出匀速段时间t3:
t3 = (s_total - s_acc - s_dec) / vmax
如果t3 > 0,说明速度确实能到达vmax,5段式中包含匀速段。如果t3 <= 0,则说明路程太短,到不了vmax,需要调整策略。
3. 参数计算与核心代码实现
3.1 根据目标位移反推可达的最大速度
实际工程中,我们拿到的目标往往是一段位移,而不是直接给一段速度曲线。所以规划的第一步是“试算”:先假设速度能够到达vmax,算出加速段位移和减速段位移,再看中间剩下的路程够不够跑匀速段。
如果不够,就要降低最大速度vmax。具体降多少?可以通过解一元二次方程来算。假设加速段和减速段的参数相同,引入降速后的目标速度v_target:
加速段位移 = 0.5 * v_target² / amax 的“非对称修正版”
这里有个更直观的推导思路:速度从0加速到v_target,如果按梯形加速度曲线来走,因为加加速度有限,实际等效于先花t_ramp = amax / jmax时间把加速度升到amax,然后再花同样时间把加速度降到0。从0加速到v_target的位移,等于匀加速度为amax、时间为2 * t_ramp的位移,再减去首尾两段”拐角“的位移差。实际代码里,我更喜欢直接按二次方程来解:
- 若v_target < amax² / jmax,说明加速度根本到不了amax,此时位移公式要用到三次项。
- 若v_target >= amax² / jmax,加速度能到amax,位移公式退化为二次。
代码里需要区分这两种情况,否则算出来的时间会偏大或偏小。这是一个非常隐蔽的坑,后面会专门讲。
3.2 核心C语言实现:参数结构与主流程
我平时在MCU上用的是一套很精简的结构体实现,先定义运动规划参数和运动状态:
typedef struct { float v0; // 起始速度 float v3; // 结束速度 float vmax; // 最大速度 float amax; // 最大加速度 float jmax; // 最大加加速度 float s_total; // 总位移 } S_PlanParam; typedef struct { float t1, t2, t3, t4, t5; // 各段时间 float v1, v2; // 各段拐点速度 float s1, s2, s3, s4, s5; // 各段位移 uint8_t segment; // 当前段号 float segment_time; // 当前段内已运行时间 } S_PlanResult;核心规划函数只负责计算各段时间,不涉及周期插值,这样便于在非实时环境下预先算好参数:
uint8_t S_Curve_Plan(S_PlanParam *param, S_PlanResult *result) { float t_acc, t_dec, s_acc, s_dec; float v_peak; float v_target = param->vmax; // 计算加速段时间(假定可以到amax) t_acc = param->amax / param->jmax; // 加速段位移(v0=0, 对称S形) s_acc = v_target * t_acc; // 这里是一个近似,实际需根据v0修正 // 减速段对称,s_dec = s_acc(v3=0) s_dec = s_acc; // 如果总位移不足以完成完整加减速,则降低峰值速度 if (s_acc + s_dec > param->s_total) { // 解方程:总位移 = 2 * (v_peak * t_acc) -> v_peak = s_total / (2 * t_acc) v_peak = param->s_total / (2.0f * t_acc); if (v_peak < 0.0f) v_peak = 0.0f; // 检查是否到amax,如果v_peak不超过amax*t_acc,则需要走“三角形”加速度 if (v_peak <= param->amax * t_acc / 2.0f) { // 简单三角形加速度S形处理 t_acc = sqrtf(v_peak / param->jmax); s_acc = 2.0f * v_peak * t_acc / 3.0f; s_dec = s_acc; v_target = v_peak; } else { v_target = v_peak; s_acc = v_target * t_acc; s_dec = s_acc; } } result->t1 = t_acc; result->t2 = t_acc; result->t4 = t_acc; result->t5 = t_acc; result->v1 = v_target; result->v2 = v_target; // 计算匀速段时间 float s_remain = param->s_total - s_acc - s_dec; if (s_remain > 0.0f) { result->t3 = s_remain / v_target; } else { result->t3 = 0.0f; } return 1; }你没看错,我在上面代码注释里写了“这是一个近似”。这是因为实际推导中,如果起始速度v0不为0,加速段位移公式还需要加上v0 * 2 * t_acc这项。如果你做的是绝对定位且v0通常为0,那上面的实现问题不大。但如果v0不为0或者需要做连续轨迹衔接,建议用完整推导的二次项公式。
3.3 每个阶段的位置与速度插值公式
规划器算完各段时间后,运动控制周期里要按周期T反复调用插值函数。每一段有独立的计算公式,但可以统一为“根据当前段的时间t,计算本周期指令位置和速度”。这里给出加速段的插值公式:
加加速段(0 <= t < t1):
float jerk = jmax; float a = jerk * t; float v = v0 + 0.5f * jerk * t * t; float s = v0 * t + (jerk * t * t * t) / 6.0f;减加速段(0 <= t < t2,局部时间):
float a = amax - jmax * t; float v = v1 - 0.5f * jmax * t * t; float s = v1 * t - (jmax * t * t * t) / 6.0f;匀速段直接按vmax跑:
float v = vmax; float s = vmax * t;减速段与加速段对称,只需要把符号反过来。实际代码中,我会写一个“低通滤波”式的积分器来交替累积位置,也可以直接利用每段的初始位置+位移增量来给定位置指令。使用累计积分的好处是:即使你的浮点运算有微小误差,位置指令也不会漂移。
4. 实操环节:参数怎么选、如何调通第一版
4.1 机械约束到规划参数的映射方法
参数不是拍脑袋定的,它们来自机械系统的物理极限和工艺需求。我自己做设备调试时,一般按这个顺序来确定参数:
最大速度vmax先看电机额定转速和减速比,算出执行端的最大线速度,再留20%-30%的余量。如果你选的伺服驱动器有速度前馈,这个vmax还要考虑前馈饱和的情况。
最大加速度amax要看电机的峰值扭矩和负载转动惯量。公式很简单:amax = (T_peak - T_friction) / J_total,如果负载惯量较大,这个值通常远低于电机空载能力,千万别按电机的空载参数写死。
最大加加速度jmax是最难定的参数,因为它直接影响机械振动。一般先按amax的10到20倍来设置,比如amax = 1000 mm/s²,jmax就设10000~20000 mm/s³。然后在实际设备上试跑,听声音、看振动,慢慢往下调。jmax设得越大,曲线越接近梯形,冲击越大;设得越小,运动越柔和,但总时间会变长。
4.2 从零调通一个5段式规划的步骤演示
我拿一个具体的数控定位平台来演示。假设平台行程100mm,要求定位时间尽量短,允许最大速度100mm/s,最大加速度500mm/s²,jmax先设2000mm/s³,启停速度为0。
先算t_acc = amax / jmax = 500 / 2000 = 0.25s。
加速段位移s_acc = v * t_acc = 100 * 0.25 = 25mm。减速段对称,也是25mm。
总位移100mm,加减速占50mm,剩余匀速段位移50mm,匀速段时间t3 = 50 / 100 = 0.5s。
整个运动时间 = t_acc * 4 + t3 = 1.5s。
这个结果合理吗?我们用另一种思路校验:纯梯形加减速的话,加减速各需要0.2s,加速段位移10mm,整个时间 = 0.2 * 2 + (100 - 20)/100 = 1.2s。S形比梯形慢了0.3s,这是平滑性的代价,符合预期。
接着我把jmax调成10000mm/s³,重新算:t_acc = 500 / 10000 = 0.05s,加速段位移= 100 * 0.05 = 5mm,总时间 = 0.05*4 + (100 - 10)/100 = 1.1s。你看,jmax调大,运动时间就接近梯形了。靠这个逻辑,可以轻松地在“平滑性”和“效率”之间做权衡。
现场调参技巧:如果设备抖动厉害,先把jmax降到amax的5~8倍,往往能立竿见影。如果抖动还在,再降。但jmax过低,你会发现加减速过程很“拖沓”,看起来像没力气一样。这个度需要根据负载惯量比来摸索,没有万能值。
4.3 规划结果仿真与曲线可视化验证
调完参数,最好先在离线环境仿真一条曲线,确认没有超限。我常用的方法是在电脑上跑同样的算法,然后导出速度、加速度、加加速度数据,用Python画一下曲线。
下面是一段简单的Python验证脚本:
import numpy as np import matplotlib.pyplot as plt v0, vmax, amax, jmax = 0, 100, 500, 2000 t_acc = amax / jmax s_acc = vmax * t_acc s_total = 100 t3 = (s_total - 2 * s_acc) / vmax t_list = [] v_list = [] a_list = [] # 加加速段 for i in range(100): t = i * t_acc / 100 t_list.append(t) v_list.append(0.5 * jmax * t * t) a_list.append(jmax * t) # 减加速段 for i in range(100): t = i * t_acc / 100 t_list.append(t_acc + t) v_list.append(vmax - 0.5 * jmax * t * t) a_list.append(amax - jmax * t) # 匀速段 for i in range(100): t = i * t3 / 100 t_list.append(2 * t_acc + t) v_list.append(vmax) a_list.append(0) fig, ax1 = plt.subplots() ax1.plot(t_list, v_list, 'b-', label='velocity') ax2 = ax1.twinx() ax2.plot(t_list, a_list, 'r--', label='acceleration') plt.show()仿真时重点看三件事:速度曲线有没有突变尖点;加速度曲线有没有超出amax的尖峰;加加速度是否保持恒定。如果三条曲线都正常,再上设备实测。这样能省掉大量现场试错时间。
5. 常见问题与排查技巧实录
5.1 到位时间比预期长很多,问题出在哪
这个现象十有八九是参数降级逻辑写错了。我见过最典型的案例:总位移刚好比加减速位移多一点点,比如总位移100mm,加减速位移总共98mm,匀速段只剩2mm,匀速段时间0.02s。但代码判断条件写了if (s_remain > 0.1f),结果t3直接置0,导致实际最高速度到不了vmax,整体时间反而拉长。
另一种情况是速度-加速度边界没区分好。当目标速度vmax较小,比如小于amax²/jmax时,加速度根本没有机会到达amax,此时如果还按“加速度先升到amax再降下来”去计算时间和位移,会得到偏大的位移,规划出来的速度曲线会在中间出现平台,看起来像匀速,实际是计算失真。正确做法是区分“能到amax”和“不能到amax”两种模式,分别套用二次和三次公式。
排查方法很简单:打印规划器算出来的各段时间,看看t1、t2、t4、t5是否在合理范围。如果t2远大于t1,说明你走了莫名其妙的非对称分支,大概率是公式套错了。
5.2 速度曲线在拐点处有尖角,不够平滑
这通常是因为加加速段和减加速段的jmax符号没有正确切换。在代码里,如果加加速段用的是jerk = jmax,减加速段应该用jerk = -jmax,但如果写成了jerk = -amax / t_acc这种“等效jerk”,一旦amax或t_acc发生了浮点取整,误差就会在拐点处放大,速度曲线出现肉眼可见的折角。
另一个常见原因是浮点精度。实时系统里,我们用float存时间,如果t_acc特别小(比如1ms级别),float的精度勉强够,但如果你在32位MCU上跑,编译器开了FPU还是会出现微小的累积误差。我一般用double存储中间计算结果,只在最终插值指令输出时转成float。
5.3 到位后位置有跳动,如何消除
位置跳动几乎都是因为S形曲线收尾时的速度没归零。5段式的最后一段“减减速”理论上应该把速度降到v_end,但如果你的减速段参数计算用了近似公式,最后一点残余速度没有完全抵消,就会表现为到位后电机还在微动,然后被伺服的位置环拽回去,产生一次可见的抖动。
解决方法是:规划结束时强制位置等于目标位置,速度等于v_end。在插值函数的末尾加一个截断逻辑:
if (t >= total_time) { pos = target_pos; vel = target_vel; acc = 0; }绝对不要靠积分自然收敛,因为浮点累积误差一定会让位置偏那么一丝丝。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 到位时间比预期长 | 匀速段判断阈值不当 | 打印t3,确认是否被截断 |
| 速度曲线有尖角 | jerk符号切换错误 | 检查各段的jerk赋值 |
| 到位位置跳动 | 末尾未强制归位 | 增加规划结束截断逻辑 |
| 加速度超限 | 参数降级逻辑未覆盖短行程 | 增加“到不了amax”的分支 |
| 低速度下曲线变形 | 误用二次公式 | 判断vmax与amax²/jmax关系 |
6. 从5段式扩展:什么时候需要7段式
5段式再往上一步就是完整的7段式。区别只在于,7段式允许加速度在加减速过程中保持一段时间,所以速度曲线中间会出现一段“匀加速”和“匀减速”,适用于长行程、低速、大加减速能力的场景。
实际判断标准很简单:如果5段式的速度曲线在达到vmax后立刻就要减速,中间没有匀速段,而且加速度达到了amax,那么改成7段式可以让加速度保持一段时间,总时间更短。但代价是参数计算更复杂,要判断匀加速段是否存在。
从代码角度,5段式升级到7段式,只需要在现有结构体里增加t2和t4(匀加速、匀减速时间),同时把加速段的位移公式换成:
s_acc = v0 * (t1 + t2) + 0.5 * jmax * t1² * (t1 + t2) - (jmax * t1³)/6
我建议你先跑通5段式,再把7段式当成一个“锦上添花”的扩展。直接一上来就写7段式,很容易被各种边界情况绕晕。
前面debug无数次以后,我最大的体会是:S形曲线规划的难点不在公式本身,而在参数降级和边界条件。很多代码跑起来看着对,一到短行程就露馅。所以写完规划器,第一件事不是接电机,而是把行程从100mm逐步缩到0.1mm,观察规划出来的v-t曲线是不是符合直觉。这一轮测试过了,再上设备基本就能安稳。如果你正在被加减速曲线折磨,希望这篇能帮你省下几个星期的调试时间。