搞过步进电机控制的人,十个里有九个都经历过这种场景:上电后直接给恒定频率的脉冲串,电机“哐”地一下猛地启动,整台机器跟着抖动;快到目标位置时又直接掐断脉冲,平台靠惯性冲过头,来回找零位都找不准。这些问题十有八九出在加减速控制上。STM32平台上,S型与SpTA加减速控制算法就是解决这类问题的两套主流方案,这篇文章把两种算法从原理到实现完整拆一遍,包括公式来源、参数计算、STM32定时器配置、实测数据,以及调试现场踩过的坑。不管你是刚接手电机控制项目的新手,还是已经在为失步、抖动和噪声头疼的老工程师,读完都能直接参考落地。
1. 步进电机为什么必须做加减速控制
1.1 步进电机的速度本质上就是脉冲频率
步进电机不像伺服电机那样靠电压调速,它每一步的转角完全由脉冲个数决定。以最常见的1.8度步距角电机为例,转子转一圈需要200个整步脉冲,如果驱动器做了16细分,那么一个脉冲对应的机械角度就是 360 / (200 × 16) = 0.1125度。这个细分后的角度分辨率,决定了定位的最小单位。
所以控制步进电机转速,本质上就是控制脉冲发出的快慢。设脉冲频率为f(单位PPS,即 pulses per second),细分倍数为m,步距角为θ,那么转速 n = f × θ / (360 × m),单位是转/秒。举个例子,目标速度是300转/分钟,也就是5转/秒,用16细分时脉冲频率 f = 5 × 200 × 16 = 16000 PPS。到这里其实还没什么玄机,真正的难点在于:频率从一个值变到另一个值的“过程”怎么走。
1.2 阶跃启停的力学代价:失步与过冲
直接在静止状态把脉冲频率拉到16000 PPS,相当于告诉电机转子:你在零时刻必须从0速度瞬间跳到5转/秒。机械系统里没有东西能承受这种速度突变,因为速度阶跃意味着加速度为无穷大,对应的就是冲击力无穷大。实际表现就是:电机定子磁场在高速旋转,转子因为惯性跟不上磁场,两者之间的角度差超过了电机的保持力矩极限,转子被“拽丢”了一步,这就叫失步。失步一旦发生,位置就永久性偏了,后面走多少步都补不回来。
反过来,高速运行中突然停止,转子还会因为惯性继续向前冲,导致实际停止位置越过目标位置,也就是过冲。有些负载惯量大的平台,过冲量能达到几十个脉冲对应的小时,毫米级定位根本不用想。这些都是我在实际项目里见过的问题,不是理论上的极端情况,而是只要不加加减速就会出现。
1.3 加减速曲线的本质:让速度变化过程连续化
加减速控制的本质,是把“瞬间跳变”的阶跃速度曲线,替换成一条从起始速度平滑过渡到目标速度的可跟踪曲线。曲线越平滑,加速度变化越缓慢,电机对负载的冲击就越小,失步和过冲的概率也就越低。
这里引入一个关键概念:加加速度,也就是Jerk,定义为加速度对时间的导数,单位是PPS³(脉冲/秒³)。梯形加减速只控制了加速度大小,但加速度本身是跳变的;S型加减速控制的是加速度的变化率,让加速度也连续变化。SpTA算法则是这两者之间的实用折中方案。下面分别拆开讲。
2. S型加减速算法:把“平滑”上升到数学层面
2.1 四个运动学量:位置、速度、加速度、加加速度
描述一个轴的运动状态,常用到四个量:位置s、速度v、加速度a、加加速度J。它们之间是层层求导的关系:
- s(t) = ∫ v(t)dt
- v(t) = ∫ a(t)dt
- a(t) = ∫ J(t)dt
梯形加减速里,加速度在启动瞬间从0跳到最大值,结束时又从最大值直接跳回0,这种行为在频域上会激励起机械系统的固有谐振点。而S型曲线通过控制Jerk,让加速度以斜坡形式上升和下降,冲击被摊开到一段时间内,机械振动幅度明显减小。
2.2 七段式速度规划的完整过程
S型加减速的完整速度曲线分为七个阶段:
- 加加速段:加速度从0按固定Jerk上升到最大加速度amax,速度曲线呈开口向上的抛物线
- 匀加速段:加速度保持amax不变,速度线性上升
- 减加速段:加速度从amax按固定Jerk下降到0,速度曲线呈开口向下的抛物线
- 匀速段:速度保持vmax不变
- 加减速段:加速度从0反向增加到-amax,速度开始下降
- 匀减速段:加速度保持-amax,速度线性下降
- 减减速段:加速度从-amax回到0,速度平滑回到起始速度,完成停止
其中第1、3段合起来对应加速过程中的S形过度,第5、7段是减速过程的镜像。如果最大速度比较低,或者加减速距离不够长,某些阶段可能会被截断,比如匀加速段直接消失,这时候规划算法需要做“削顶”处理,用更复杂的判断来决定哪些阶段保留、哪些阶段合并。
2.3 一组真实参数的加速段计算
光说公式不好吸收,我拿一组实际调过的参数走一遍计算过程。假设起始速度v0=500 PPS,目标速度vmax=76800 PPS,最大加速度amax=50000 PPS²,加加速度J=1000000 PPS³。先算加速度从0升到amax需要的时间:t1 = amax / J = 0.05秒。
在这0.05秒内速度增量 Δv1 = 0.5 × J × t1² = 0.5 × 1000000 × 0.0025 = 1250 PPS。也就是说,加加速段结束时速度达到1750 PPS。减加速段是对称的,同样增加1250 PPS。那么匀加速段需要吃掉的速度增量是 Δv2 = 76800 - 500 - 2 × 1250 = 73800 PPS。
匀加速段时间 t2 = Δv2 / amax = 73800 / 50000 = 1.476秒。加加速段的位移 s1 = v0 × t1 + J × t1³ / 6 ≈ 25 + 20.83 ≈ 45.83个脉冲。匀加速段位移 s2 = 1750 × 1.476 + 0.5 × 50000 × 1.476² ≈ 2583 + 54464 ≈ 57047个脉冲。单边加速总位移大约是 45.83 × 2 + 57047 ≈ 57139个脉冲。
这组数至少有两点工程意义。第一,加速段主要距离是匀加速段贡献的,S形过渡段只占很小的位移比例,所以用S型并不会比梯形增加多少加速距离。第二,如果你规划的加速距离小于这个值,就必须降低vmax或amax,否则实际速度根本达不到规划值,最终步数也会对不上。
2.4 S型算法的工程代价
S型曲线平滑效果好,代价是规划逻辑复杂。七段状态机的边界条件多,速度高、距离短时还要处理阶段消失的情况。另一个问题是参数多且相互耦合,v0、vmax、amax、J四个参数只要有一个不合理,计算出来的时间甚至会出现负值。在STM32这种MCU上,如果用浮点数在线计算,中断里负担会比较重。所以很多实现选择离线规划、预生成频率表,这样运行时就只剩查表了。
3. SpTA加减速算法:S型与梯形思想的融合
3.1 为什么还会有人想换算法
S型效果好,但工程现场其实有几个被吐槽的点。第一,参数多,四套参数之间的约束关系一旦没理清,曲线形状很难预期;第二,七段状态机在中断里容易写错,阶段切换漏一个情况就会导致脉冲频率跳变;第三,很多应用并不需要全程Jerk受限,真正会让机器抖动的就是加速转匀速、匀速转减速这两个加速度突变点。
SpTA(S型与梯形加速度融合算法)就是在这种需求下流行起来的。它不是某一家厂商的专用算法,而是一类混合策略的统称。我实际用过两种实现变体,这里讲一个我认为最实用、也最容易落地的版本:以梯形规划为骨架,在两个关键拐角处用S形过渡段替代直线折角。
3.2 SpTA的思路:梯形骨架加S形圆角
纯梯形算法的速度曲线是一个等腰梯形,加速段和匀速段的交界处,加速度从amax瞬间变成0;匀速段和减速段的交界处,加速度从0瞬间变成-amax。这两个点就是冲击的主要来源。
SpTA的处理方式是先按传统梯形算出大致的时间和步数,然后在这两个拐角处插入一段S形过渡。过渡段的作用是把加速度的突变变成斜坡变化,斜坡的斜率由Jerk控制。整个运动过程变成这样:
- 启动段:速度从v0经过一个S形过渡平滑上升,加速度从0缓变到amax
- 线性加速段:速度以最大加速度amax匀加速上升
- 拐角过渡1:接近vmax时,加速度从amax经S形过渡缓降到0
- 匀速段:速度保持vmax
- 拐角过渡2:开始减速前,加速度从0经S形过渡缓变到-amax
- 线性减速段:速度以-amax匀减速下降
- 停止段:接近v0时,加速度从-amax经S形过渡缓降到0,速度平滑归位
相比纯S型七段式,SpTA把“线性段”和“过渡段”分开处理,每个阶段的功能单一清晰,规划器只需要判断当前处于过渡段还是线性段,状态切换逻辑大幅简化。
3.3 SpTA的三种运行状态切换
在实际代码里,SpTA的状态机可以精简为三个状态:ACCEL、CRUISE、DECEL。每个状态下又细分过渡段和线性段。我建议用两个标志位区分:phase_type表示当前是过渡还是线性,phase_dir表示加速还是减速。
这样做的最大好处是,加减速距离的计算非常直观。先用梯形公式预算总步数,再单独计算两个S形过渡段多消耗的步数,两项相加就是需要的总步数。如果给定步数比这个更短,可以直接减小amax或者提前进入减速,而不像纯S型七段式那样需要重新推导所有阶段的公式。
对于运动控制任务比较固定的设备,比如打印机、贴片机、桌面机械臂,这种“梯形骨架+S形圆角”的方案调整起来最快,现场改一个Jerk参数就能观察振动变化,不用同时调好几个耦合参数。
3.4 与S型、纯梯形的横向对比
我在同一个机械平台、同一组速度参数下,对比过三种算法的实际表现。平台是某自制三轴运动平台,X轴用1.8度步进电机、16细分、丝杆导程5mm,目标平移速度120mm/s,换算成脉冲频率大概是76800 PPS。
| 项目 | 纯梯形 | S型七段式 | SpTA混合 |
|---|---|---|---|
| 加速段总距离 | 约56600脉冲 | 约57139脉冲 | 约56900脉冲 |
| 加速度突变 | 有两次阶跃 | 无突变 | 无突变 |
| 启动/停止冲击感 | 明显,机身抖动 | 几乎无感 | 弱,轻微过渡感 |
| 参数数量 | 3个 | 5个 | 4个 |
| 状态机复杂度 | 低 | 高 | 中 |
| 现场调参难度 | 低 | 高 | 低 |
从数据可以看出,SpTA在平滑性上非常接近S型,但参数数量和调参难度只比梯形高一点。我的经验是,对大多数中小型设备来说,SpTA是性价比最高的选择。S型适合对振动和声音要求极高的场景,比如光学检测设备、精密医疗仪器;纯梯形则只在极低速、低负载场合才能勉强用。
4. STM32上的工程实现和实测调参
4.1 定时器与脉冲输出方案
STM32产生步进脉冲最常用的是定时器的PWM输出模式。选择定时器时,我一般优先看三点:是否有自动重装载预装载寄存器、是否支持更新事件产生DMA请求、是否有多通道输出比较。像STM32F103的TIM1、TIM2、TIM3、TIM4这些主流定时器都能满足,区别只是位数和高级定时器的额外功能。
以我常用的方案为例:TIM3作为脉冲发生器,PA6映射到TIM3_CH1,配置为PWM模式1,输出极性为高,占空比50%。速度调节通过修改自动重装载值ARR实现。定时器时钟设为72MHz,输出脉冲频率 f_pulse = 72000000 / (ARR + 1)。如果目标频率是76800 PPS,那么ARR = 72000000 / 76800 - 1 ≈ 936。这个数就是定频脉冲的周期基准。
这里有个容易踩的坑:修改ARR时,如果占空比寄存器CCR不跟着改,脉宽会变成固定值,在ARR变化的过程中占空比会飘。解决办法是每次更新ARR时,同时把CCR设为ARR的一半。另外,ARR寄存器必须开启预装载功能,让数值在更新事件到来时统一生效,否则边改边生效会在波形中间产生一个毛刺,这对步进驱动器来说就是一次误脉冲。
4.2 速度到ARR的转换与同步更新
一个实用的速度控制函数长这样:
void motor_set_speed_pulse(uint32_t freq_hz) { uint32_t arr = TIMER_CLOCK / freq_hz - 1; TIM_SetAutoreload(TIM3, arr); TIM_SetCompare1(TIM3, arr >> 1); // 保持50%占空比 }加减速规划器每一拍算出一个新的目标频率,然后调用这个函数更新ARR,电机速度就跟着曲线走了。真实项目里要注意的是:不要在中断里做除法。72MHz除以几百几千还好,但如果是动态规划、频率值每几百微秒变一次,中断里做浮点除法会占用大量时间,导致后续ARR更新不及时,实际速度曲线和规划曲线对不上。
正确的做法是在初始化阶段预计算频率表。以加速段为例,规划器生成一个uint32_t数组,每个元素对应当前这一拍应该输出的脉冲频率或ARR值,中断里只做“读取数组+更新寄存器”两步操作。我用过的最简单形式:
volatile uint32_t current_idx = 0; uint32_t freq_table[ACCEL_POINTS]; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); if (current_idx < ACCEL_POINTS) { motor_set_speed_pulse(freq_table[current_idx++]); } } }4.3 规划器与执行器:代码框架
把加减速控制拆成“规划器”和“执行器”两层,是个值得养成的习惯。规划器负责根据运动参数计算速度曲线,输出一张频率表或关键参数;执行器只负责按节奏读取规划结果并驱动定时器。规划器可以放在主循环里跑,执行器放在定时器中断里跑,两者通过结构体共享数据。
我常用的一种规划结果结构:
typedef struct { uint32_t v0; // 起始速度 PPS uint32_t vmax; // 目标速度 PPS uint32_t amax; // 最大加速度 PPS^2 uint32_t jerk; // 加加速度 PPS^3 uint32_t accel_steps; // 加速阶段总脉冲数 uint32_t decel_steps; // 减速阶段总脉冲数 uint32_t cruise_steps; // 匀速阶段总脉冲数 } motion_plan_t;执行状态机再根据这三个steps切换阶段。我习惯在每次规划完成后做一个校验:accel_steps + cruise_steps + decel_steps 必须等于目标脉冲数,如果不相等,说明某个阶段计算出的步数有偏差,直接改规划参数而不是硬凑。
4.4 定点运算:把计算拉回整数域
S型和SpTA的计算公式里涉及乘法、除法和开方,如果直接在MCU上用float,编译效率低是一方面,更重要的是不同编译器的浮点行为可能不一致,调起来很玄幻。我的做法是全流程定点化,思路如下:
- 速度单位直接用uint32_t,单位就是PPS,频率多少就存多少
- 时间单位用微秒或“定时器Tick数”表示
- 加速度和Jerk用“每Tick变化多少PPS”的定点数表示,或者放大一定倍数后用整数运算
- 每个积分周期做一次累加:v += a_step; a += jerk_step;
比如加速度50000 PPS²,如果控制周期是1ms,那么每个控制周期速度增量就是50 PPS,保持整数运算绰绰有余。Jerk 1000000 PPS³,在1ms周期里加速度增量为1000 PPS²,依然整数。这种离散化的近似在控制周期足够短时,和连续公式误差极小,完全够用。
4.5 三组曲线实测对比
同一台测试平台上,我分别烧入纯梯形、S型七段式、SpTA三套算法,用逻辑分析仪抓取脉冲波形,再用码盘编码器读实际位置误差。测下来的结果:
- 纯梯形:速度从0跳到76800 PPS的过程里,平台有明显的“咯噔”一声,编码器显示在启动瞬间有短暂失步趋势,虽然最终没丢步,但丝杆有明显弹性变形
- S型七段式:启动和停止几乎无声,编码器读数平滑,没有失步趋势,整个运动过程中加速度计的振动幅值大约是梯形的三分之一
- SpTA混合:启动和停止有轻微可察觉的过渡感,但振动幅值大约只有梯形的二分之一,位置误差全程小于1个脉冲当量
这三组数据对应到实际产品里,最直观的差别就是声音。用手机分贝计在电机旁测,梯形算法在拐点时能到75分贝以上,S型大约70分贝左右,SpTA比S型高一两分贝。对于办公设备来说,这个差距足够决定一台机器能不能通过验收。
4.6 常见问题与排查实录
调试过程中我整理了几个高频问题,对应的排查思路如下。
| 问题现象 | 可能原因 | 排查与解决方向 |
|---|---|---|
| 电机启动时“嗡嗡”响且不走 | 启动频率超过电机力矩特性允许范围 | 降低v0起始速度至电机可靠启动的范围内 |
| 运行到中段突然失步 | 加速度过大,力矩不足以支撑负载惯量 | 降低amax,或增大驱动电流、提高供电电压 |
| 停止位置每次都差一点 | 减速点计算滞后,减速距离不足 | 增加减速段步数,提前进入减速状态 |
| 电流波形正常但速度忽快忽慢 | 中断里计算耗时太长,ARR更新不及时 | 预计算频率表,中断只读取数据 |
| 波形在频率变化时出现毛刺 | ARR修改未同步到更新事件 | 开启预装载,检查更新事件配置 |
其中失步问题最容易被误判成“电机功率不够”。实际上很多失步不是力矩不够,而是加减速曲线太陡,瞬间需要的加速度力矩超过了电机的短时过载能力。先降低amax试一下,比换电机和加驱动电压快得多。
还有一个冷门的坑:定时器更新中断里如果还做了别的事,比如处理按键扫描或者AD采样,会直接影响脉冲间隔均匀性。我后来把所有不紧急的任务全部移出中断,中断里只做ARR更新和一个计数自增,脉冲抖动才算彻底消除。
5. 一些个人经验
5.1 算法选择不是越高级越好
做了几个项目之后,我的选型建议是:优先评估运动距离和速度变化频率,而不是盲目上最平滑的算法。距离短、启停频繁的点位运动,SpTA的优势最明显,因为它兼顾平滑和计算效率;长距离高速运输,S型和SpTA差距不大,选梯形加圆角也够;对噪声和残留振动要求苛刻的精密设备,才值得为S型付出更多计算和调参成本。
5.2 一个被很多人忽略的细节:规划步数与实际步数
S型七段式规划里,有一个很隐蔽的错误源:加速段和减速段的步数计算用四舍五入,累计误差可能吃掉最后几个脉冲。所有阶段步数都应该是整数,并且在规划完成后强制做一次“总步数等于三段步数之和”的校验。我吃过这个亏,当时定位总是差两三个脉冲,查了整整一天才发现是匀加速段位移计算时少算了一个周期。
5.3 扩展方向
加减速算法本身可以做得更“聪明”一些。比如配合编码器做闭环,实时检测实际位置和规划位置之间的偏差,一旦偏差超过阈值立即触发失步保护;也可以用前瞻控制,把连续多段运动的路径合并处理,避免在每个小线段之间重复加减速,这在雕刻机、3D打印这类G代码运动里尤其有效。如果用的是带DMA功能的定时器,还可以把频率表通过DMA自动搬运到ARR寄存器,彻底摆脱中断频率的限制。
我的实际体会是,加减速控制不是玄学,它就是运动控制里最基础的力学工程。S型和SpTA给了你两条抵达平滑运动的路,一条绕远但风景好,一条抄近但够实用。先把公式跑通,再上机听声音、看波形、读编码器,你的电机自然就不抖了。