资讯动态

STM32步进电机S型加减速:速度规划原理与源码实现

发布时间:2026/10/18 7:11:46 来源:尧图企业网站定制
1. 从一次撞机事故说起梯形加减速解决不了的问题1.1 那次“咔咔咔”的声音和一顿一顿的走位STM32步进电机S型加减速程序源码与分析这类话题我其实是被一次事故“逼”着开始研究的。早年间帮一个朋友调一台小型贴标机构的Y轴电机带着同步带和滑块跑空机测试时一切正常一装上标签卷和导向机构问题全出来了启动瞬间整个横梁“咔”一声像是在硬生生撞什么东西匀速跑动时能明显感觉到滑块一顿一顿不是流畅地走而是一格一格地蹦更麻烦的是停止时总要有几毫米的过冲把标签贴歪。那时候第一反应是机械装配有问题调了张紧轮、换了同步带甚至换过电机折腾了一周也没解决。后来才意识到问题根本不在机械而在速度规划。步进电机和伺服电机不一样它不靠闭环反馈知道当前位置而是严格按收到的脉冲个数和频率转。脉冲频率就是转速频率的每次突变对电机来说相当于瞬间变速。机械系统里有惯量滚珠丝杠、同步带、滑块、负载这些都有质量速度突变时惯性会让转子跟不上脉冲指令轻则顿挫抖动重则失步丢步。那次事故就是典型的频率突变导致的失步——电机实际走的步数和控制器发出的指令步数对不上后面的每一次定位都会偏移。也正是这次事故让我把注意力从机械装配转移到了控制程序本身开始认真研究S型加减速的实现。1.2 为什么梯形加减速救不了场绝大多数步进电机程序里速度规划用的是梯形加减速启动后以一个固定的加速度升速到达设定速度后匀速接近目标位置前再以固定加速度减速。它的优点是计算简单、占用内存少、容易理解很多开源项目和开发板例程都这么写。但在要求稍微高一点的场合梯形曲线的短板非常明显——加速度在两个拐点上发生了跳变。跳变有多严重你在开车时如果一脚踩死刹车车里的人会往前栽如果缓缓踩下去人不会太难受。梯形加减速就是在“启动瞬间加速度从0直接跳到某个值减速瞬间又从某个值直接跳回0”等于把一脚油门和一脚刹车的冲击全部集中在几个毫秒里释放。步进电机转子本来是带惯性的突然要求它从静止直接进入一个高加速度状态它不可能瞬时响应于是出现了“咔咔咔”的机械冲击和丢步。要量化这个问题需要引入一个概念加加速度jerk也就是加速度的变化率。用公式解释一下速度v对时间t求导是加速度aa dv/dt加速度对时间求导是加加速度jj da/dt。梯形加减速中加速度只有一个固定阶跃值拐点处加速度瞬间改变斜率不存在这意味着j是无穷大S型加减速的核心就是让加速度不是阶跃突变而是以某个有限斜率连续上升、再连续下降让j控制在机械可承受的范围内。所以S型加减速本质上回答了一个梯形回答不了的问题如何在速度从0到最大、再从最大到0的过程中不让加速度出现突变。这看起来只是多了一个jerk参数但整套数学构造和代码实现都比梯形复杂一个量级。不过对于带负载、高速度、大行程的三维平台、贴片机、雕刻机、机械臂来说这笔复杂度的投入完全值得。2. S型曲线的数学构造与嵌入式落地2.1 先从物理直觉写jerk、加速度、速度三者的关系在做程序之前先把物理模型理清楚。想象一辆车从静止起步到一百码再刹停最舒服的驾驶方式不是一脚油门踩到底而是慢慢把油门踩深加速度逐渐变大维持一段深踩油门加速度恒定再慢慢松油门让加速度逐渐归零。这个“油门深度”的变化过程就是加加速、匀加速、减加速。S型加减速曲线的名字就看速度曲线在时间轴上是不是呈现出“S”字形两头平缓、中间陡峭。用数学语言描述设加加速度为j加速度为a速度为v位移为s三者对时间的关系是a(t) a0 j·t加速度随时间线性变化v(t) v0 ∫a(t)dt速度是加速度的积分s(t) s0 ∫v(t)dt位移是速度的积分。j为常数时加速度曲线是直线速度曲线是抛物线拼接位移曲线是三次多项式拼接这也是很多人把S型加减速称为“三次S曲线”的来历。嵌入式程序里我们不关心连续解只关心离散的脉冲间隔但物理关系必须保留脉冲间隔反映了瞬时速度加速度和jerk则以时间和距离的形式决定速度曲线形状。这种“受约束的积分关系”就是S型规划的全部秘密。2.2 七段式S型速度规划每一段干什么、边界条件怎么算实际工程中最常用的是七段式S型曲线。名字里的“七段”指加速段和减速段各被拆成三段加上中间的匀速段。加速侧的三段是加加速段jerk为正加速度从0线性上升到最大加速度匀加速段加速度保持最大减加速段jerk为负加速度从最大线性下降到0。减速侧与加速侧对称加减速段、匀减速段、减减速段。中间如果行程够长还会夹一个匀速段。这七段关系列成一张表段号段名加速度a速度变化趋势说明1加加速段0 → a_max缓慢上升冲击主要控制在这一段2匀加速段a_max线性上升速度变化率最大3减加速段a_max → 0上升放缓为进入匀速做准备4匀速段0恒定如果没有这段速度为三角形曲线5加减速段0 → -a_max下降放缓减速开始6匀减速段-a_max线性下降速度快速下降7减减速段-a_max → 0下降放缓平稳停住关键的计算在三个边界条件上目标速度vmax、最大加速度amax、加加速度jmax。给定这三个值加速段需要的最小步数和最小时间就能算出来。以加速侧为例。设加加速段时间为t1匀加速段时间为t2减加速段时间也等于t1。速度要达到vmax必须满足vmax - v0 jmax·t1² amax·t2这里v0是起始速度一般取0或低频启转速度amax jmax·t1。从第二个式子得到 t1 amax / jmax。如果vmax足够大t2 (vmax - v0) / amax - t1如果算出来的t2小于0说明在达到vmax之前加速度就必须开始下降这时不存在匀加速段速度曲线退化成“三角形”加速只有加加速和减加速峰值速度也到不了vmax只能达到一个中间值。这个判断非常重要因为程序跑起来以后不可能动态算浮点积分必须在运动开始前就把各段步数规划好。规划步数的方法是把每段时间上的速度积分换算成脉冲步数比如加加速段的步数大约是 v0·t1 amax·t1²/6这是速度线性上升时曲线下的面积匀加速段的步数是 (v0 amax·t1)·t2 amax·t2²/2以此类推。2.3 为什么我最终选择位置域查表而不是中断里跑公式七段式越算越复杂很多人会想直接在定时器中断里实时用公式算当前速度不就行了理论可行但嵌入式平台上有两个现实问题。第一个问题是浮点运算耗时。STM32虽然有浮点单元但不是所有系列都带即使带中断里做exp、除法、乘法在高速脉冲每秒钟几万步下会挤占大量CPU时间导致中断嵌套、脉冲丢失。第二个问题是误差累积。时间域的实时积分每一轮都会引入累积误差行程越长误差越大最终体现在终点位置不准上。我后来把方案改成了位置域查表让速度成为“已完成步数”的函数而不是“当前时间”的函数。加速段一共要走N_acc步那么第i步完成时的速度可以预先算成一张表。运动过程中中断每产生一个脉冲只做一件事——步数加一然后查表得到该位置对应的速度换算成定时器重装值更新ARR。整个过程没有浮点指数运算也不是每周期都从头积分最终速度只跟位置有关和程序跑了多久无关。查表还有另一个好处用来拟合的曲线可以非常自由。你可以用标准七段式的解析积分也可以直接用sigmoid函数去拟合一条速度—位置曲线甚至可以把别人导出的速度曲线直接采样成表。步进电机控制只关心“位置和速度的对应关系”不关心你这条曲线到底是用哪个公式画出来的。这给调试带来了极大的灵活性——改曲线形状只需要重新生成表不需要动中断处理函数。3. STM32驱动侧架构脉冲、中断与速度更新3.1 三种产生脉冲的方式PWM硬件、定时器翻转、软件翻转S型加减速调度好之后关键是如何让STM32按计算出来的速度产生脉冲。常见方案有三种各有取舍我整理成表方式实现原理优点缺点适用场景定时器PWM输出用定时器输出比较通道产生PWM波形CPU占用低频率稳定方向引脚另用GPIO控制每次频率改变需要更新ARR更新时机要处理好大多数工程项目的首选定时器中断翻转GPIO每次更新中断里翻转一次电平灵活便于插入状态判断中断频率高CPU占用大低速场合或原型验证软件延时驱动用DWT/延时循环翻转GPIO不占用定时器延时期间CPU被阻塞不推荐用于正式产品我的做法是定时器PWM输出模式脉冲频率由定时器的自动重装值ARR决定。假设定时器时钟是1MHz想要输出频率为FHz的脉冲ARR就是1000000 / F。这个公式很朴素但它是驱动侧一切逻辑的出发点S型加减速计算出“当前速度”驱动侧只把它翻译成“定时器ARR值”。3.2 定时器中断里那点事ARR更新的时序与量化误差使用PWM硬件时中断处理函数里的逻辑很简单定时器溢出/比较事件触发中断在中断里步数加一根据加减速阶段查表得到速度更新ARR让下一次脉冲周期的频率变成新的值。代码核心是这样的void TMR_UP_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE); motion.completed_steps; // 步数1 if (motion.state STATE_ACCEL) { if (motion.completed_steps motion.accel_steps) { motion.state STATE_RUN; motion.speed motion.max_speed; } else { motion.speed lookupspeed(motion.acc_table, motion.completed_steps, motion.table_len); } } else if (motion.state STATE_DECEL) { uint32_t dec_done motion.completed_steps - motion.accel_steps - motion.cruise_steps; if (dec_done motion.decel_steps) { motion.state STATE_STOP; motion.speed 0; } else { motion.speed lookupspeed(motion.dec_table, dec_done, motion.table_len); } } // 1MHz定时器时钟下的ARR计算 if (motion.speed 0.001f) { __HAL_TIM_SET_AUTORELOAD(htim, (uint32_t)(1000000UL / motion.speed)); } } }这里有一个工程细节很容易被忽略ARR更新后定时器要等到下一个计数周期结束才会按新值重装所以实际产生的脉冲周期变化会比计算点晚一个周期。也就是说我们查表得到“第i步结束后的目标速度”但实际生效的是“第i1个脉冲的周期”。这个相位差在半闭环步进控制里影响不大因为加减速过程是连续的晚一拍不会改变整体曲线走向。另一个细节是整数转换带来的量化误差。1000000 / speed的结果是整数四舍五入或者截断都会让每个脉冲周期有微小偏差。如果每个周期都向下取整几百步以后偏差可能累积成明显的频率偏低。解决的办法是在代码里加一个小数余量用定点数表示当前速度每次更新ARR时把余数保留下来累加到下一个周期。我在实际项目里直接用浮点speed变量配合ARM的FPU做精度够用如果目标芯片不带FPU建议用32位定点数把速度放大1000倍存储计算ARR时再除回来。3.3 与步进驱动器配合的接口考量STM32输出的通常是脉冲和方向两个逻辑信号而不是电机绕组的直接功率。实际驱动由外部的细分步进驱动器完成STM32通过“脉冲方向”接口跟它对接。这里有几个容易踩的坑脉冲频率上限取决于驱动器的最大输入频率。一般驱动器支持200kHz以上但细分倍数越高同样的机械转速对应的脉冲频率越高方向信号切换要提前于脉冲信号。否则第一个脉冲到来时方向还没稳定电机可能往反方向转一下稳妥做法是方向切换后延时至少5微秒如果驱动器支持光耦隔离输入最好用可以屏蔽干扰但要注意脉冲高电平持续时间不能太短否则光耦来不及响应。细分设置直接决定“步”的机械含义。例如一个1.8度步距角的电机8细分下每脉冲就是0.225度一秒钟12000步等价于电机每秒转1.5圈。做S型加减速规划时所有速度、加速度、加加速度参数都以“步/秒”和“步/秒²”为单位细分改变后这些参数要重新换算这一点很多人在换驱动器或改细分的时侯容易忘。4. 加减速核心源码逐行剖析4.1 结构体与初始化把运动参数一次性算成步数我给运动控制专门建了一个结构体把所有运行参数集中管理这样不同轴之间复制起来也很容易typedef struct { uint32_t total_steps; // 目标总步数含符号可表示方向 uint32_t accel_steps; // 加速段步数 uint32_t cruise_steps; // 匀速段步数 uint32_t decel_steps; // 减速段步数 float max_speed; // 目标峰值速度步/秒 float start_speed; // 启动速度步/秒 float max_accel; // 最大加速度步/秒^2 float max_jerk; // 最大加加速度步/秒^3 float speed; // 当前运行速度 uint8_t state; // 加速/匀速/减速/停止 uint32_t completed_steps; // 已完成步数 uint16_t *acc_table; // 加速段查表指针 uint16_t *dec_table; // 减速段查表指针 uint16_t table_len; // 表长 } SCurve_t;初始化函数要做的事情是先判断这是一个短行程速度达不到max_speed还是长行程再分配加速、匀速、减速段步数。短行程的判断就是前面说的加速段位移 减速段位移大于总步数这时要降低目标速度重新计算三个段的步数。这个逻辑是整个程序里最容易出错的部分。我的经验是用“距离优先”而不是“速度优先”不管目标速度设多高先算最多能跑多远如果距离不够就降低峰值速度到能匹配的最大值而不是让中间出现一段没有任何减速缓冲的高速运动。4.2 查表插值函数与Timer ARR计算查表函数不长但性能关键uint16_t lookupspeed(const uint16_t *table, uint32_t pos, uint16_t len) { if (len 1) return table[0]; // 把已完成步数映射到[0, len-1]区间 float f (float)pos * (len - 1) / (float)lookup_denominator; uint16_t idx (uint16_t)f; float frac f - idx; if (idx len - 1) return table[len - 1]; uint16_t v0 table[idx]; uint16_t v1 table[idx 1]; return (uint16_t)(v0 (v1 - v0) * frac); }这里表里存的是“速度 × 1000”的定点值线性插值可以减少量化台阶。为什么要插值因为一张表如果直接存N_acc步的速度值行程一变表就要重做而固定表长比如256点配合线性插值可以在不同行程之间复用同一张归一化曲线只是位置映射的缩放系数不同。归一化S型曲线的生成可以用sigmoid拟合也可以直接用七段式积分。我用sigmoid做了个对比版将位置归一化到0到1速度归一化到0到1用 s(x) 1 / (1 exp(-k·(x - 0.5))) 来拟合再裁剪首尾让曲线上起终点速度严格等于0和1。曲线形状完全由k控制k小了曲线更软、加减速更平缓k大了更接近直线冲击变大。在初始化时把这条归一化曲线算成表运行时按实际段步数缩放映射即可void generate_scurve_table(uint16_t *table, uint16_t len, float steepness) { float s0 1.0f / (1.0f expf(steepness * 0.5f)); float s1 1.0f / (1.0f expf(-steepness * 0.5f)); for (int i 0; i len; i) { float x (float)i / (len - 1); float val 1.0f / (1.0f expf(-steepness * (x - 0.5f))); val (val - s0) / (s1 - s0); // 裁剪到[0,1] table[i] (uint16_t)(val * 1000.0f); } }注意这里生成的只是速度曲线的“归一化形状”真正的速度还要乘上目标峰值速度。我把乘法和定时器基准频率结合在中断里一起算完省掉一次乘除。4.3 状态机加速、匀速、减速、停止的切换逻辑整个加减速运行过程可以抽象成一个小状态机四种状态分别是STATE_ACCEL加速中查加速表更新速度STATE_RUN匀速中速度固定为目标峰值速度STATE_DECEL减速中查减速表更新速度STATE_STOP运动结束关闭脉冲输出。它在中断里的切换逻辑就是我前面代码片段里写的那个样子。加速段步数跑完进匀速段匀速段步数跑完进减速段减速段步数跑完进停止。这里有一个必须注意的细节到达停止状态时电机可能因为机械惯性还停不稳或者最后一步脉冲频率很低导致转子欠冲。我的处理是保留一个“停步稳速”的小功能减速段的最后若干步速度不是直接到0而是维持一个很小的启动速度走固定几步后再停相当于把脉冲频率降到一个低于“共振低限”的数值再切断输出。电机就不会出现低频振动和最后一步的啪一声。4.4 边界条件加速距离不够时怎么办前面提到短行程问题这里展开说。假设运动参数是vmax10000步/秒amax12000步/秒²jmax120000步/秒³算出来的加速段需要大约416步减速段也是对称为416步。如果目标总步数只有500步那么416 416 832步大于500显然不可能达到10000步/秒中间必然有一段在高速区直接减速。对于这种情况我的初始化函数会重算一个满足总距离的峰值速度。具体做法是用二分法搜索v_peak让“加速段步数 减速段步数”最接近总步数然后重新生成加速表和减速表。二分法迭代大概十多次初始化时执行不影响实时中断。如果不处理这个边界会发生极其危险的现象电机加速到一半程序发现总步数已经用了一半立刻进入减速状态但因为峰值速度太高、减速距离不够最后几百步相当于高速急刹车失步、堵转、机械冲击全来了。5. 实测调参与排坑记录5.1 同一条S曲线不同负载的差异参数怎么定调S型加减速核心参数就是三个最大加速度amax、最大加加速度jmax、峰值速度vmax。记住一个规律负载惯量越大amax和jmax就得越小。加速度决定电机的电磁转矩需求jerk决定机械系统的抖动程度。我的起步参考值是这样定的先把jmax设得很大比如amax的10倍此时S曲线接近梯形如果机器没有明显冲击逐步减小jmax直到机械共振消失再单独调amax看电机在低速高速切换时是否有失步趋势有的话减小amaxvmax通常由工艺产能决定能不改就不改。负载类型建议amax步/秒²建议jmax步/秒³空载小滑块轻同步带50000 ~ 80000amax × 5 ~ 8中等负载约2kg20000 ~ 40000amax × 3 ~ 5重负载龙门轴5000 ~ 15000amax × 2 ~ 4这些值只能作为起点。真正的调参手段是看执行机构的振动声音和温升电机闷响说明加速度偏大皮带抖说明jerk偏大碰一下机构明显抖动就说明S曲线不够软。5.2 三个高频坑第一个坑是减速点算错导致高速直接刹车。这个前面已经说了根源是短行程边界没处理。我见过不止一个项目运动距离一改电机就撞限位后来干脆在初始化里输出三段的步数日志每次启动打印一次这样一眼就能看出减速段是否合理。第二个坑是中断里做浮点公式运算。早期我试过在中断里动态计算七段式积分公式代码很简洁但实际跑起来低速时还没事高速时中断处理时间越来越长偶尔会出现脉冲间隔不均电机声音都变了。换成查表后中断里只剩一个整数查表和一次整数除法CPU占用率降了一个量级。第三个坑是低频共振。步进电机在几十到几百赫兹区间存在明显的低频共振如果S曲线从0开始它的起始段必然经过这个区间电机会在刚启动时抖动一下。我的方案是设置一个start_speed一般取500到1000步/秒让电机从有意义的低速直接进入S曲线绕开最低的共振区间。代价是启动瞬间有一个小的速度阶跃但500步/秒的阶跃冲击远小于1500步/秒以上基本可以接受。5.3 与梯形加减速的实测对比用前面提到的那台贴标机构做了一组对比负载约2kg目标速度9000步/秒行程2000步。项目梯形加减速S型加减速最大加速度40000 步/秒²40000 步/秒²加加速度无穷大阶跃150000 步/秒³机械噪音启动有明显“咔”声无明显冲击声停机过冲约3~5mm小于1mm连续往返100次丢步数偶发约2~4步0步整段运动时间约0.38秒约0.42秒时间上慢了0.04秒但换来了几乎零丢步和更低的机械冲击。对于贴标、定位这类以一致性为命脉的场合这0.04秒完全值得。参数整定上还有一个小建议先用一个客户能接受的低速度跑通整个流程确认S曲线形状合适后再逐步提高速度上限。很多人一上来就按理论峰值速度跑结果机械共振点恰好落在加速中段排查起来非常痛苦。关于S型加减速我自己一直保留着一个习惯在任何一版源码里都预留一个“曲线形状参数”哪怕一开始只做最简的梯形也把jerk这个变量留在初始化结构体里。因为只要运动控制项目跑到后期总会面对更重的负载、更高的速度要求到那时候再回过头改架构远比一开始就留好扩展点麻烦得多。如果你正在做STM32步进电机方案建议先把七段式的段分配逻辑和位置域查表搞清楚再谈其他花哨的速度拟合方法——把地基打牢后面换任何曲线都是一张表的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑