ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32步进电机加减速算法:S型与SpTA实战解析

STM32步进电机加减速算法:S型与SpTA实战解析

用STM32驱动步进电机这几年,我踩过最大的坑就是“电机一加负载就失步”。明明空载时跑到几千转都欢快,挂上负载直接闷住,位置全乱,现场调试被整套设备等着,那种压力谁懂。后来我把精力集中在加减速算法上,才真正把步进系统调稳。目前工程里用得最多的两条路线,一个是正统的S型加减速,另一个是我自己改进过的工程版本——SpTA(Speed Profile with Time-Adaptive adjustment,时间自适应速度规划)。这篇针对STM32平台,把这两条算法的原理、离散化思路、中断实现和实测参数完整拆开讲一遍。如果你正在被步进电机抖动、丢步、到位过冲折磨,这篇应该能帮你省下不少调试时间;刚入门的朋友也能从原理部分理解为什么电机不是“越猛越好”。

1. 为什么步进电机加减速是绕不开的坎

1.1 矩频特性与“丢步”是怎么发生的

步进电机和伺服电机最不一样的地方,就是它没有“反馈闭环”来兜底。脉冲发过来了,定子绕组换相,转子按理说就跟着转一步;但实际上能不能跟上,完全看电机在当前转速下还能不能输出足够转矩。这就引出了步进电机最要命的“矩频特性”:转速越高,绕组反向电动势越大,有效驱动电流越小,输出转矩就越低。低速时转矩很足,高速时转矩掉得厉害。

如果启动瞬间就把脉冲频率拉到很高,等于让电机在低转矩区起步。轻载可能勉强转,负载一重,电磁转矩小于负载阻力矩,转子就原地不动,或者抖几下,然后彻底丢步。这种丢步在开环控制里最恐怖——没有编码器回读,你根本不知道位置已经错了,等发现时产品已经报废。而加减速控制的作用,本质就是让脉冲频率从低速慢慢爬上去,在低速高转矩区“助跑”,再过渡到高速低转矩区,避免直接顶着低转矩区猛冲。

1.2 加减速的本质:让脉冲频率变成一条平滑曲线

很多初学者以为只要设置一个“目标速度”就行,这在步进电机里是行不通的。电机实际转速由单位时间收到的脉冲数决定,也叫脉冲频率。你改变定时器装载值,就是在改变脉冲频率。启动瞬间从0直接跳到目标频率,脉冲间隔瞬间从无限长变成很短,转子的角加速度在数学上就是无穷大——物理上当然做不到,于是电机便“挣扎”一下,轻则振动,重则堵转。

所以加减速算法要做的事情非常具体:给每个脉冲重算下一步的脉冲间隔(或者等价地,定时器装载值),让频率沿一条设计好的曲线上升和下降。梯形加减速是直线上升,S型加减速是让速度曲线呈“S”形,而SpTA则是我针对STM32这种资源有限的MCU做的折衷方案:用分段线性的方式逼近S型曲线,让加速度变化可控但计算量很小。三条路线的差异,其实全在这条频率曲线的形状上。

2. S型加减速的数学骨架与离散化实现

2.1 七段式S曲线到底“软”在哪里

S型加减速之所以比梯形平滑,核心在于它引入了“加加速度(jerk)”这个概念。梯形加减速的加速度是阶跃的,启动瞬间从0跳到最大加速度,结束瞬间又从最大加速度跳回0,机械冲击很大;而S型曲线把加速度的“突变”也做成了渐变,加速度的变化率——加加速度——是有限值,整个运动过程的力学冲击就要柔和得多。做个类比:开车从静止到巡航,如果你是直接一脚地板油,人会被猛甩一下;如果你先轻踩油门让车慢慢走起来,再逐渐加深,这就是“限制加加速度”的S型风格。

严格来说,完整的S型加减速是七段式:加加速度增大的加速段、匀加速段、加加速度减小的加速段,然后是匀速段,再是反向的三种减速段。如果把速度写成时间的函数,可以总结成下表:

阶段编号运动特征加速度状态速度曲线形状
1加加速从0线性增大到a_max向上凹
2匀加速保持a_max不变直线上升
3减加速从a_max线性减小到0向上凸
4匀速0水平
5加减速从0反向线性增大到-a_max向上凸
6匀减速保持-a_max不变直线下降
7减减速从-a_max线性减小到0向上凹

实现七段式的精确计算在STM32上非常费劲,每一段的速度公式都不同,还牵涉到判断各段是否存在的边界条件。工程上我做了一个简化:用“半正弦升速模型”来近似S形,加速阶段速度比例按 0.5(1-cos(πt/Ta)) 变化。这个模型不是严格的七段多项式,但没有jerk突变,平滑度在绝大多数步进应用里已经够用,而且实现极其友好。

2.2 从连续速度到定时器装载值:关键换算

不管速度曲线多漂亮,最后要落到具体寄存器上。STM32产生脉冲流最常见的方式是让定时器工作在PWM模式,把某个输出通道配成脉冲输出口。此时定时器计数频率 fclk 由时钟和预分频决定,脉冲输出频率就等于:

f_pulse = fclk / (ARR + 1)

所以你要输出某个速度对应的脉冲频率 f_cur,定时器的自动重装载值就是:

ARR = fclk / f_cur - 1

运动控制里常说的“转速”,要和脉冲频率换算。比如某电机转一圈需要200个整步步距,驱动器细分设为16,那么电机转一圈需要 200×16=3200 个脉冲。目标转速300rpm,对应每秒转数5,脉冲频率就是 3200×5=16000 pps。你在中断里要做的核心事情就是:根据当前加减速曲线上的速度点,算出f_cur,然后更新ARR。

这里要特别注意一个细节:ARR在更新中断里被修改后,影子寄存器会在下一次更新事件才装载,所以实际生效会延迟一个脉冲周期。这个延迟在低速时无所谓,但高速时一个脉冲周期只有几十微秒,如果中断响应不及时,相邻两个脉冲间隔就会抖动,电机运行不平稳。这也是为什么中断里代码必须短、必须快。

2.3 MCU友好的实现:查表替代实时三角函数

直接在线算cos函数是STM32F1这种无FPU芯片的大忌。我在早期版本里试过在中断里用浮点算余弦,结果脉冲间隔肉眼可见地抖动,电机高速时一顿一顿,逻辑分析仪看波形全是毛刺。后来改成查表,问题立刻消失。

S型加速阶段可以预先生成一张归一化速度比例表。最简单的方法:把0到π/2的余弦值量化成uint16_t数组,中断里按当前步数索引比例,再反推速度。类似下面这样:

// 加速阶段归一化速度比例表,长度可配,按需要生成 // s_table[i] = (uint16_t)(0.5 * (1.0 - cos(3.1415926f * i / (TABLE_LEN - 1))) * 65535.0f) static const uint16_t s_table[TABLE_LEN] = { 0, 123, 492, 1105, 1960, ... }; // 中断里定点化更新速度 void s_curve_update_speed(s_motion_t *m) { uint32_t idx; if (m->phase == PHASE_ACCEL) { idx = (uint32_t)m->acc_step * (TABLE_LEN - 1) / m->acc_total_steps; if (idx >= TABLE_LEN) idx = TABLE_LEN - 1; // 定点乘:把速度范围按65536分成 uint32_t range = m->vmax - m->vmin; uint32_t speed = m->vmin + ((uint32_t)s_table[idx] * range >> 16); m->cur_speed = speed; // 换算定时器装载值 TIMx->ARR = TIM_CLK / speed - 1; m->acc_step++; } // 其他阶段类似 }

这套定点化的思路,核心是把浮点比例的0~1映射到0~65535的整数,再通过移位完成乘法,中断里的计算量只有几次整数乘除和比较。在实际项目里,我用主频72MHz的F103跑16000pps的脉冲输出,中断占用时间微乎其微,曲线平滑度明显改善。

3. SpTA:一种面向实时调整的S型工程近似

3.1 什么时候S型不够用

S型加减速好归好,但有两个痛点没法回避。第一是中断计算负担相对高,哪怕用了查表,每个脉冲都要查表、乘除、判断状态,频率跑到几十kHz时CPU资源吃紧。第二个更麻烦:S型曲线的七段边界是按照预先规划好的加速段长度、匀速段长度、减速段长度来切的,一旦现场负载变化导致实际速度偏离规划,或者运动过程中收到新的目标位置指令,这套规划很难实时修正,硬改就会造成脉冲间隔跳变,电机直接来一次“抽搐”。

实际产线做多工位定位时,经常遇到这种情况:上一个物料刚走完,还没来得及按原计划减速,就马上要往下一个位置跑。固定S型曲线在这种场景下不够灵活。我需要的是一套能按“剩余步数”实时调整减速点、但又不想花大代价去精确算jerk的方法,SpTA就是在这样的需求下被我提出来并不断打磨的。

3.2 SpTA算法拆解:分段、线性逼近与时间自适应

SpTA的名字是我在工程笔记里的叫法,全称是Speed Profile with Time-Adaptive adjustment,行业里并没有统一命名,你可以把它理解成一种“分段梯形逼近S型”的工程算法。核心思想分三层:

第一层是分段。把整个加速或减速过程按时间片(或者按步数块)切成有限段,我一般切8到16段就够了。每一段内部,加速度被看作是恒定值,那么段内速度随脉冲数线性变化。因为每段很短,速度变化范围被限制在很小的区间,整体上看速度曲线还是一根平滑上升的S形,加速度的突变被拆成了很多小台阶,机械冲击远小于梯形。

第二层是边界速度连续。前一段末尾的脉冲频率,必须等于后一段开头的脉冲频率。虽然段与段之间加速度会跳一下,但速度不会跳,从运动学上看这保证了位置、速度的连续性,不会出现“突然加速”的顿挫。

第三层是时间自适应。段长度不是写死的,而是在运动过程中根据剩余步数实时重规划。每次执行完一段后,系统检查当前速度、剩余距离,判断“再往前走还刹不刹得住”。如果剩余步数已经接近理论刹车距离,就提前缩短后续段、平滑进入减速过程;如果发现剩余还很充裕,就维持当前加速趋势。这就是SpTA里“自适应”的真正含义——它本质上做的是动态减速点重规划。

3.3 三张表看懂梯形、S型、SpTA差异

很多同行问我一上来应该选那种算法,我一般直接甩一张对比表,然后让对方按自己的工况挑:

项目梯形加减速S型加减速SpTA(本文变体)
加速度曲线阶跃突变连续变化分段阶跃,段内恒定
加加速度不可控,理论无穷大有限可控段间有界可控
中断计算量很小较大(需查表/浮点)较小(每段一次规划)
实时重规划能力弱中强(按时间片重规划)
速度曲线平滑度一般优秀接近S型
典型场景低速简单点位高速高精度定位频繁变速、实时短距离定位

梯形是最原始、代码最简单的方案,但启动和停止瞬间的冲击是真疼,负载稍大就容易丢步。S型适合那些要求高速、高平滑、机械结构精密的场景,代价是调参和CPU开销。SpTA的定位是我给“既要平滑又要实时修改”的场景做的不错折衷,它的曲线平滑度比不上严格S型,但已经能解决绝大多数抖动问题,同时让代码结构简单到可以在中断里放心跑。

3.4 SpTA的编码骨架与重规划流程

SpTA的数据结构可以这样设计,每个时间片段保存起始速度、结束速度和段内剩余步数:

#define SPTA_SEG_NUM 12 typedef struct { uint32_t steps_limit; // 本段内需要执行的步数 uint32_t steps_done; // 本段内已执行步数 uint32_t speed_start; // 段首脉冲频率 pps uint32_t speed_end; // 段末脉冲频率 pps uint32_t step_span; // 每调整一次速度间隔多少步 uint32_t speed_step; // 每次调整的速度增量 pps } spta_seg_t; typedef struct { uint8_t phase; // 0停止, 1加速, 2匀速, 3减速, 4到位 uint8_t seg_idx; // 当前段索引 uint32_t steps_total; // 总步数 uint32_t steps_done; // 已执行步数 uint32_t cur_speed; // 当前脉冲频率 pps uint32_t min_speed; // 最低运行频率 uint32_t max_speed; // 目标速度 pps spta_seg_t seg[SPTA_SEG_NUM]; } spta_motion_t;

在每次更新中断里,只需要推进步数、判断当前段是否结束,再计算下一个ARR:

void spta_irq_handler(spta_motion_t *m) { // 清中断标志 // 每累计一个脉冲 m->steps_done++; spta_seg_t *sg = &m->seg[m->seg_idx]; // 不是每个脉冲都改速度,而是每隔若干步调整一次,进一步降低计算量 if ((sg->steps_done % sg->step_span) == 0) { if (m->cur_speed < sg->speed_end) { m->cur_speed += sg->speed_step; } else if (m->cur_speed > sg->speed_end) { m->cur_speed -= sg->speed_step; } } sg->steps_done++; if (sg->steps_done >= sg->steps_limit) { // 本段结束,切下一段并触发重规划 if (m->seg_idx + 1 < SPTA_SEG_NUM) { m->seg_idx++; spta_replan_decel(m); // 根据剩余距离调整后续段 } else { m->phase = 0; m->cur_speed = m->min_speed; } } TIMx->ARR = TIM_CLK / m->cur_speed - 1; }

重规划减速点的函数是SpTA的灵魂,关键是估算刹车距离并预留余量:

static void spta_replan_decel(spta_motion_t *m) { uint32_t remain = m->steps_total - m->steps_done; uint32_t brake_steps = estimate_brake_steps(m->cur_speed, m->min_speed); // 预留10~20步安全余量,防止机械惯量导致刹车距离估算偏短 if (remain <= brake_steps + 20) { m->phase = 3; build_decel_segments(m, m->cur_speed, m->min_speed, remain); } }

这里的estimate_brake_steps用梯形刹车模型粗略估算:已知当前速度v_cur、最低速度v_min、最大减速度a_dec,理论上减速步数约为 (v_cur^2 - v_min^2) / (2 * a_dec)(换算成脉冲数注意单位和时钟频率)。虽然估算值不精确,但因为有安全余量,配合最后一段低速爬行,位置精度足够。

4. STM32上的引擎细节:脉冲链路与中断优化

4.1 用定时器PWM当脉冲源,让硬件替CPU打工

步进电机驱动器的“方向+脉冲”接口本质上要的就是TTL脉冲串。早期我在F103上用过GPIO翻转方式来产生脉冲:进入中断,翻转引脚,算好下一次翻转的时间,再设置下一个定时器比较值。每产生一个脉冲就要进一次中断,高速时CPU几乎被占满,而且GPIO翻转的延迟受中断优先级影响很大,脉冲抖动明显。

后来我全部改成定时器PWM模式。把某个定时器通道配置为PWM输出,输出引脚直接接驱动器的PUL端口。让计数器自由计数,PWM输出按ARR值自动翻转,硬件自己就把脉冲流发出来了,CPU只需要在更新中断里修改ARR,为下一步输出做准备。这样脉冲波形的上升下降沿由定时器硬件生成,抖动很小,CPU也被解放出来做运动规划、通信这些更重要的任务。

4.2 中断里只做“一次加法一次查表”

定时器更新中断的频率等于当前脉冲频率,如果跑到16000pps甚至更高,意味着每秒钟要进16000次中断。中断里放太多浮点计算、多次乘除,中断的响应时间会漂移,最终表现为相邻脉冲间隔不稳定。

我这里给出一套工程铁律:中断里只做必要的最小操作,能查表就查表,能用定点就用定点,能提前算好就提前算好。比如ARR的换算,做一次TIM_CLK / speed整数除法可以接受,但如果每个脉冲都做,而且还有大量其他代码,就容易被更高优先级中断打断,产生毛刺。

一个更极端的优化是预生成ARR表:把加减速全程每个脉冲对应的ARR值预先算好,存在数组里,中断里只做数组索引和寄存器赋值。对于固定距离的往复运动,我试过用这种方案把中断开销压到最低。缺点是不灵活,一旦中途要改目标位置,整个表就得重算。实际项目我通常用SpTA,因为它按时间片重规划,原本就不是每个脉冲都要重算,压力天然小。

4.3 运动状态机与减速点动态重规划

加减速算法只是运动控制的一部分,真正跑起来还需要一套状态机把“加速、匀速、减速、到位”这几个状态管起来。我的习惯是定义五种状态:静止、加速、匀速、减速、爬行。其中爬行状态非常关键:高速到达目标点附近后,不直接刹停,而是先以一个很小的速度(比如几百pps)走最后几十步,再完全停住,这样结构性余量很小,位置却更准,还能减少到位后的机械弹跳。

状态机内部还要处理一些细节。比如收到新位置指令时,如果当前还在加速,不能立刻跳到新曲线,否则速度会突变。正确做法是调用重规划函数,把当前速度和剩余步数作为新曲线的初始条件,重新生成后续段。这也是SpTA的优势,因为段结构天然支持“从当前段开始重来”,不像S型曲线那样整条曲线推倒重来。

5. 实测波形与参数整定的那些坑

5.1 逻辑分析仪下的脉冲间隔曲线

调加减速算法时,最直观的检查工具不是示波器,而是逻辑分析仪。抓一下驱动器PUL引脚,测量相邻脉冲上升沿的时间间隔,就能看到速度曲线长什么样。下面是我在某次测57步进电机、16细分、目标速度16000pps时抓到的典型数据,加速度阶段末段的部分脉冲间隔数值:

算法加速末段连续脉冲间隔序列(单位微秒)
梯形2500, 62.5, 62.5, 62.5, 62.5, 62.5
S型166, 155, 142, 130, 118, 107, 96, 86, 78, 71, 66, 64, 62.5
SpTA166, 150, 135, 110, 95, 80, 72, 65, 62.5, 62.5

梯形的问题一眼就能看出来:脉冲间隔从2500微秒直接跳到62.5微秒,中间没有任何过渡,机械冲击就在这一刻发生。S型的间隔是连续递减的,非常顺滑。SpTA的间隔曲线呈现一种“段内小台阶下落”的形态——整体平滑但带着细碎的阶梯感。只要段数够多、每段速度跨度控制得当,这种阶梯在实际运行时完全感觉不到。

5.2 参数整定经验:启动频率、加速度、jerk、分段数

我踩过最深的坑是启动频率设成0。理论上从0开始最平滑,但实践里从0启动意味着电机要长时间停留在极低转速区,而那个区域电机的输出力矩反而并不理想,更麻烦的是转动惯量会让转子在极低频率下产生爬行,启动不仅慢还容易抖动。现在我一般把最低运行频率设在200到500pps之间,利用低速高转矩区快速脱离。

加速度上限的整定要结合负载和矩频曲线,没有万能值。初值估算时可以用“电机峰值转矩60%作为动态余量”的办法,然后再在实测时缓慢增大,直到出现轻微顿挫就退回0.8倍。jerk相关参数在S型算法里影响“柔软程度”,我一般取加速度变化率的4到8倍每秒;SpTA里没有显式jerk参数,但分段数要够,我建议至少8段,太少段间加速度跳跃太大,会重新出现顿挫,太多又失去低开销优势。常用的16段是个不错的中间值。

5.3 常见故障排查表

参数整定和算法选型的问题,最终都会表现为某种可观察的故障。下面是我调试中整理出的一份快速排查表,遇到类似现象可以先对着查:

现象大概率原因处理方向
电机高速啸叫刺耳加速度过大或段间速度跳变太大降低加速度/增加SpTA分段数
启动瞬间“咔哒”一声后停转启动频率高于突跳频率降低最低运行频率,加长启动段
到位后过冲明显减速点太晚或减速度不足增大安全余量,提前减速点
低速时电机明显抖动最低频率过低或细分不够提高最低运行频率,增加细分
运行中位置轻微偏移加减速期间发生丢步降低目标速度,检查驱动电流
脉冲波形占空比异常定时器配置错误或ARR更新冲突检查PWM模式和影子寄存器设置

5.4 开环系统的步数守恒与位置同步

最后必须强调一个开环步进系统里容易忽略、却极其要命的原则:步数守恒。开环控制没有编码器,系统默认“发多少脉冲,就走多少步”,所以任何加减速重规划都不能改变最终发出去的总脉冲数。SpTA在动态调整减速段时,新生成的段内步数总和必须严格等于剩余步数,否则机器每走一个来回,位置就会偏几个脉冲,日积月累就是肉眼可见的精度丢失。

我的解决方案是在重规划函数里始终以剩余步数为约束条件:无论怎么重组加减速段,都先统计总步数,如果新计划总步数不等于剩余步数,就微调匀速段或者最后爬行段的步数来补齐。这点看似简单,但关乎整个设备在连续运行几百次后的定位精度,千万别图省事跳过去。

一个实操技巧:我通常把所有加减速逻辑封装成同一套接口,S型和SpTA可以按项目一键切换。负载不重、点位运动频繁的设备,用SpTA能把动态响应做得很灵活;末端要求准停、启停不频繁且结构精密的设备,老老实实切回S型。启动频率、最大加速度、分段数这些参数尽量做成可调变量,现场调试时一个一个试,电机最终转得顺不顺,逻辑分析仪上那一串脉冲间隔曲线会告诉你一切。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进