第一次接触轨迹规划的人,多半都会觉得梯形速度分布太“朴素”:不就是先加速、再匀速、最后减速吗?可真正要把这套东西写进运动控制器,让它在一毫秒的中断里稳定跑起来,同时处理好位移不够、方向反转、浮点漂移这些边界情况时,才会发现坑一点不少。这篇文章我就从工程实操的角度,把梯形速度分布的设计思路、公式推导、代码实现、参数选择和常见坑一次性讲清楚。如果你正在做步进或伺服控制,或者在学习机器人轨迹规划,这篇应该能帮你省掉不少绕弯的时间。
1. 先搞清楚梯形速度分布到底在解决什么问题
1.1 从“给个位置就跑”到“规划怎么跑”
做运动控制的时候,最朴素的做法是:上位机直接给一个目标位置,驱动器闭环去追。可是位置指令是阶跃给进去的话,控制器会在一瞬间输出一个很大的速度指令或力矩,机械结构在极短的时间里被推到完全没有准备的速度上。轻则设备振动大、定位超调,重则直接触发过流报警,甚至损伤丝杠和联轴器。所以,在真正下发位置指令之前,必须先生成一条平滑的目标位置—时间曲线,让执行器沿着这条曲线走。这个生成曲线的过程,就是轨迹规划。
梯形速度分布,是这中间用得最多的一种实现。它把整个运动过程切成三段:加速段、匀速段、减速段。位移足够长时,三段都会出现;位移很短时,速度还没爬到设定上限就要开始刹车,曲线退化成三角形。但不管是梯形还是三角形,本质都是同一个算法里的两个分支,只是峰值速度是否达到设定值有区别。
1.2 为什么梯形能成为默认选项
既然市面上还有S形速度曲线、多项式插值、回旋曲线这些“更高级”的方案,为什么多数工业控制器仍然默认梯形?
首先是计算量。梯形分布的每一段都是匀加速直线运动,只有一次乘法和一次加法,连开方都只需要在三角形分支里做一次。对实时中断里跑的运动控制来说,这是几乎可以忽略的开销。
其次是时间效率。在只约束最大速度和最大加速度的情况下,梯形分布是所有可行曲线里用时最短的那一类。这个结论很直观:加速段用最大加速度尽快爬到限速,匀速段以最高速度跑,减速段用最大减速度尽快停下。中间没有任何浪费。
第三是直观。三段式的参数非常容易调试,某个轴走得不顺,示波器抓一下速度曲线,一眼就能看出是加速太猛还是匀速段规划不合理。相比之下,高阶曲线一旦出问题,定位起来要绕好几个弯。
顺便说一句,很多初学者把梯形速度分布和S形速度曲线混在一起。这里先给个结论:梯形速度曲线在加速度上是不连续的,转折点处加速度突变,机械上表现为冲击;S形曲线通过限制加加速度,让加速度连续变化,平滑性更好,但计算复杂,行程时间也更长。后面我会专门讲升级思路。
2. 梯形速度分布的公式推导与关键判断
2.1 三段式的时间、速度、位移计算
先把符号统一一下。假设物体沿正方向运动,v0是起始速度,vmax是设定的最大速度,ve是终点速度,a是加速阶段加速度,d是减速阶段减速度的绝对值,s是总位移。为了通用,这里默认v0、vmax、ve都不小于0;反向运动时可以先取绝对值算轨迹,最后再按方向赋值,这个细节后面说。
加速段从v0匀加速到vmax,位移为:
s1 = (vmax² - v0²) / (2a)
t1 = (vmax - v0) / a
减速段从vmax匀减速到ve,位移为:
s3 = (vmax² - ve²) / (2d)
t3 = (vmax - ve) / d
这两个公式用初高中匀变速运动的平方差公式就能推出来:初速度和末速度的平方差,等于两倍加速度乘以位移。工程计算中直接用这个公式,比先算平均速度再乘时间要省事,也不容易算错。
剩下的中间段是匀速。已经知道总位移s,那匀速段位移就是s2 = s - s1 - s3,时间就是 t2 = s2 / vmax。
注意这里有一个隐藏陷阱:如果s2算出来是负数,说明“还没加速到vmax就必须开始刹车了”。这时候完整梯形不存在,算法会自动降级为三角形态。这个判断是实际写梯形规划时第一个要检查的地方,很多人忽略它,结果算出一堆负时间。
2.2 位移不够时,峰值速度怎么算
当s1 + s3 > s的时候,峰值速度达不到vmax。那实际能达到的峰值速度vpeak是多少?很多人会直接套用公式,但不理解推导过程,出了问题也不知道怎么排查。这里展开推一下。
设加速段从v0加速到vpeak,位移为:
s_acc = (vpeak² - v0²) / (2a)
减速段从vpeak减速到ve,位移为:
s_dec = (vpeak² - ve²) / (2d)
两段位移加起来等于总位移s:
(vpeak² - v0²) / (2a) + (vpeak² - ve²) / (2d) = s
整理之后得到:
vpeak² × (1/a + 1/d) / 2 = s + v0²/(2a) + ve²/(2d)
再把系数除过去,就得到一个比较对称的公式:
vpeak = sqrt( (2sa d + d v0² + a ve²) / (a + d) )
如果v0 = 0、ve = 0、a = d,公式会简化成经典的三角形峰值公式 vpeak = sqrt(a s)。比如加速度500mm/s²、位移10mm,峰值速度就是sqrt(5000)约70.7mm/s,这个结果很符合直觉:一半路程加速,一半路程减速,峰值正好卡在几何中点上。
2.3 判型完成后的完整时间求解流程
把上面的逻辑整理成流程,就是:
- 核对输入:保证0 ≤ v0 ≤ vmax,0 ≤ ve ≤ vmax。如果起点速度本来比限速高,或者终点速度要求比限速高,说明规划参数不合法,需要先额外处理一段“起点减速”或“终点加速”,否则会算出负数时间。
- 计算s1和s3。
- 若s - s1 - s3 ≥ 0,走完整梯形:t1、t3按上面公式,t2 = (s - s1 - s3) / vmax。
- 否则,走三角形:用vpeak公式算出实际峰值,然后 t1 = (vpeak - v0) / a,t3 = (vpeak - ve) / d,t2 = 0。
这个流程可以用一个结构体加一个函数封装,输入四个参数,输出三段的时间和总时长。代码写出来不到一百行,实用价值极高。
关于方向,还有一个特别实用的技巧:规划时统一把位移取绝对值,速度参数也取绝对值,计算出来的时间一定为正。实际控制时,把每段的目标速度和目标位置都乘以运动方向(1或-1)就可以。这样代码里不用为反向运动单独写一套逻辑。
3. 把算法落到代码与脉冲输出上
3.1 控制周期、数据类型与离散化思路
轨迹规划生成的速度曲线是连续的,但实际系统只能在固定控制周期中采样目标位置。常见周期是1ms、2ms、500us。计算模式有两种:一种是离线预计算全部目标点表,运动控制器按点表逐点下发;另一种是每个控制周期在线计算当前目标位置。
我推荐做在线规划。梯形分布的每周期计算量很小,不超过十次乘加,中低端MCU也能轻松完成。而且在线规划最大的好处是支持中途变速、中途改目标位置,这在调试阶段几乎是刚需。离线点表虽然看起来“实时性更好”,但改一个vmax就要重新生成整张表,内存和开发效率都不划算。
离散化要注意一个问题:浮点位置值最终要落到脉冲数或者寄存器值上,如果每个周期只取整数部分,累计误差会造成终点差几个脉冲。解决办法是用“绝对位置”做目标,每个周期直接计算当前时刻对应的绝对位置,再与实际位置做差输出,而不是每周期累加一个增量。前者误差不会漂移,后者会。
3.2 核心代码实现示例
用C语言风格写一个完整示例。结构体保存规划参数和结果,函数负责生成三段的时间和位移,另一个函数用于每个周期取出当前位置。
typedef struct { float v0; float vmax; float ve; float a; float d; float s; float v_peak; float t1, t2, t3, t_total; float dir; } TrapProfile; int trap_generate(TrapProfile *tp) { float s_abs = fabsf(tp->s); float s1 = (tp->vmax * tp->vmax - tp->v0 * tp->v0) / (2.0f * tp->a); float s3 = (tp->vmax * tp->vmax - tp->ve * tp->ve) / (2.0f * tp->d); if (s1 + s3 <= s_abs) { tp->v_peak = tp->vmax; tp->t1 = (tp->vmax - tp->v0) / tp->a; tp->t3 = (tp->vmax - tp->ve) / tp->d; float s2 = s_abs - s1 - s3; tp->t2 = s2 / tp->vmax; } else { float s = s_abs; tp->v_peak = sqrtf((2.0f * s * tp->a * tp->d + tp->d * tp->v0 * tp->v0 + tp->a * tp->ve * tp->ve) / (tp->a + tp->d)); tp->t1 = (tp->v_peak - tp->v0) / tp->a; tp->t3 = (tp->v_peak - tp->ve) / tp->d; tp->t2 = 0.0f; } tp->t_total = tp->t1 + tp->t2 + tp->t3; tp->dir = (tp->s >= 0.0f) ? 1.0f : -1.0f; return 0; } float trap_position_at(const TrapProfile *tp, float t) { float ret; if (t < tp->t1) { ret = tp->v0 * t + 0.5f * tp->a * t * t; } else if (t < tp->t1 + tp->t2) { float t_cruise = t - tp->t1; ret = tp->v0 * tp->t1 + 0.5f * tp->a * tp->t1 * tp->t1 + tp->v_peak * t_cruise; } else if (t <= tp->t_total) { float t_dec = t - tp->t1 - tp->t2; ret = tp->v0 * tp->t1 + 0.5f * tp->a * tp->t1 * tp->t1 + tp->v_peak * tp->t2 + tp->v_peak * t_dec - 0.5f * tp->d * t_dec * t_dec; } else { return tp->s; } return tp->dir * ret; }代码里有一个容易被忽略的点:匀速段的起始位置不是0,而是加速段结束时已经走过的位置。所以trap_position_at里先算出加速段的终点位置,再叠加匀速段位移。减速段同理,要先把前面两段的位移都加上。如果直接只用当前速度乘时间,位置就会对不上。
另外,减速段的公式是 v_peak * t_dec - 0.5 * d * t_dec²,而不是用“剩余距离”倒推。原因在于这段是从v_peak开始匀减速,位置随时间的变化就是初速度乘时间再减去刹车损失。理解了这个,后面想扩展S形曲线时,思路也能平移过去。
3.3 一个具体的数字算例
假设一个直线平台,丝杠导程10mm,最大速度100mm/s,加减速度都是500mm/s²,起始和终止速度都是0。
位移50mm时,s1 = 100²/(2×500) = 10mm,s3同样是10mm,匀速段位移30mm,三段时间分别0.2s、0.3s、0.2s,总时间0.7s。
位移10mm时,s1 + s3 = 20mm大于10mm,走三角形分布。vpeak = sqrt(2×500×500×10/(500+500)) ≈ 70.7mm/s,加速段和减速段时间各约0.141s,总时间约0.282s。
这个算例可以直观地说明一个道理:短距离运动时,速度根本来不及爬满,时间也不像线性想象中那么短。做设备节拍估算时,按最大速度直接除位移会严重低估时间,必须先算峰值速度。
4. 工程落地:参数选取与实战避坑
4.1 加减速分设,别把a和d写成一个常量
很多初版代码里a和d共用一个参数,因为“加减速感觉差不多”。但实际工程里,加速取决于驱动力、负载惯量和摩擦,减速取决于制动力、摩擦和负载的惯性冲量。尤其垂直轴,重力方向会明显改变加减速特性:上升减速时重力帮你刹,下降减速时重力反而推着你冲。统一设置的结果就是某个方向刹不住,定位过冲。
我的习惯是把a和d作为两个独立参数放进配置表,并且在测试时分别标定。先让设备空载跑一个阶跃,测出实际加速度能力,再带载验证,最后留出15%到20%的余量作为规划用的a和d。
4.2 浮点误差、取整丢步与终点修正
用float做轨迹计算,长时间运行会有累计误差。这里有一个关键原则:输出目标位置时,永远用“时间自变量直接算绝对位置”,不要用“上一周期位置增量累加”。前者误差是每周期独立的有界误差,后者会随着时间累积出漂移。
脉冲控制时,还要处理取整丢步。理想位置是小数,发给驱动器只能发整数脉冲,余数如果直接扔掉,长时间后会丢出好几个脉冲。解决办法是把余数保存到下一个周期,和下一周期的理想增量合并后再次取整,保证总脉冲数不丢。
位置闭环的控制周期也要稳定。如果控制周期有抖动,同样vmax下实际平均速度忽快忽慢,电机容易发出不均匀的啸叫。尽量用硬件定时中断,不要用操作系统软件延时去凑。
4.3 步进电机高频丢步与速度上限匹配
梯形规划算出来的vmax是理论值,不等于电机真的能跑那么快。步进电机的输出力矩随转速升高明显下降,超过力矩拐点后很容易丢步。我见过一个项目,理论速度能到300mm/s,实际跑到250mm/s就开始丢步,表现为位置偏了但不报警。
排查方法是做扫频测试:从低到高设定不同的匀速速度,让电机走固定距离,回零点看偏差。找到丢步临界速度,把规划vmax设为临界速度的80%左右。另外,加速段末端是高力矩需求区,如果加速过大,即使匀速段能跑,加速末也会丢步。这时要么降低a,要么提高驱动电流、换大一号电机。
5. 常见现象与排查速查表
5.1 速度曲线看不到匀速段
如果设了vmax=100mm/s,位移只有10mm,实际峰值只有70.7mm/s,自然没有匀速段。这不是算法bug,而是位移不够。先算s1 + s3,和总位移对比。输出速度峰值永远等于min(vmax, vpeak_triangle)。看懂这个关系,调试速度曲线时就不会对着示波器发愣了。
5.2 终点停住时过冲或抖动
常见的诱因有三个方向。一是减速段太短,d取值过大,机械惯性冲过头;二是终点速度ve不为0,切换去位置闭环的瞬间速度突变;三是位置环增益太高,整个系统柔性不足,停在目标点附近反复震荡。处理顺序是:先把d调小延长减速段,保证到达终点时速度为0,再检查位置环增益是否太高。如果还抖,就在终点附近人为增加一小段低速趋近过程,而不是直接砍到0速度。
5.3 运行时间比理论值明显长
把t1、t2、t3分别打印出来,看时间占比。总时间主要由t1 + t3决定,瓶颈就是加减速度,需要提高a/d能力;总时间主要由t2决定,瓶颈就是vmax,需要考虑提高限速或优化传动比。这个分析对设备节拍优化非常有用。
下面是实际项目中常见的现象速查表。
| 现象 | 可能原因 | 排查方式 | 解决方向 |
|---|---|---|---|
| 实际位移偏小 | 脉冲取整丢步 | 回零查偏差 | 余数累计补偿 |
| 高速抖动、异常噪声 | 控制周期不稳 | 示波器抓速度 | 改用硬件定时中断 |
| 末端过冲 | 减速段太短、ve不为0 | 看t3和终点速度 | 调小d,ve置0 |
| 只跑了三角形 | 位移不够 | 算s1+s3 | 降低vmax或接受短行程 |
| 丢步不报警 | 力矩曲线拐点 | 扫频测试 | 降vmax或增大电机 |
6. 下一步:从梯形走向更平滑的方案
6.1 梯形 vs S形:到底差在哪
梯形速度分布的核心短板是加速度不连续。在加速段转匀速段、匀速段转减速段这两个点,加速度瞬间跳变,机械结构会感受到一个冲击。低速低负载场景感觉不明显,高速高精密场景就会表现为振动和轨迹误差。
S形曲线通过限制加加速度(jerk)让加速度连续变化,冲击更小,轨迹更平滑,代价是计算复杂度上升,行程时间略长。很多高端数控系统和精密机械臂在关键路径上会选S形,但普通点位运动仍然大量使用梯形,因为效率和简单性更重要。
6.2 在梯形基础上做平滑过渡的三种做法
如果暂时不想全面替换成S形,可以留用梯形框架,在转折点处做平滑处理。
第一种是余弦过渡。把加速段末端的速度从匀加速模式换成 v(t) = vmax × (1 - cos(π t / T)) / 2 这类曲线,起点和终点的加速度都是0。计算量还行,但会改变位移公式,需要重新积分。
第二种是限制jerk的梯形修形。在梯形转折点前后各加一小段jerk受限的过渡区,让加速度线性变化。这样全程的jerk有界,但规划参数会多出几个。
第三种是直接用七段S形曲线。把加速段拆成加加速、匀加速、减加速,减速段同样拆成三段,中间保留匀速段。这是最彻底的做法,适合需要平滑走路径的场合,代码量大概比梯形多一倍,但原理仍然是基于梯形速度分布的骨架。
6.3 扩展到在线变速与多轴同步
梯形规划还有一个非常好的特性:天然支持在线变速。运动过程中如果需要改目标速度,把当前采样周期的时间、位置、速度保存下来,以当前位置为起点、当前速度为v0、剩余位移为总位移,重新调用一次trap_generate,就等于完成了曲线切换。这个思路在做视觉联动、人手示教时非常实用。
多轴同步的话,最粗暴的方式是先让每条轴分别生成梯形曲线,得到各自的总时间,再以最长总时间为基准,把其他轴的目标速度等比例调低,保证所有轴同时到达。这种同步方式简单可靠,但可能让某些轴明显变慢。更好的做法是在任务空间里做路径规划,再逆解到关节空间,在关节空间里每个轴按各自能力分配时间,属于另一个话题了。
如果你现在打算在自己控制器里试一遍梯形速度分布,我的建议是:先把a和d分开,再用绝对位置公式计算目标位置,最后把s1+s3这个判型逻辑放在最前面。这三个点想清楚,基本不会出大问题。至于什么时候换S形,我的经验是,先让梯形跑起来,用示波器看实际曲线,确认振动和过冲确实成为瓶颈,再去做平滑升级。轨迹规划这行,能把梯形吃透,后面进阶怎么走心里都有底。