两年前我在一个小型运动平台上调试步进电机当时用的还是梯形加减速速度曲线写起来简单但电机一到中速段就开始“嗡嗡”叫带着联轴器一起抖严重的时候直接丢步加工出来的零件全是残次品。后来我把加减速算法从梯形换成S型启动和停止的冲击明显小了一截丢步问题也基本消失。这篇文章就围绕STM32上的S型加减速把原理、参数设计、源码实现以及调试中的坑一次性讲清楚适合正在做步进电机控制、想优化启停体验的嵌入式开发者参考。1. S型加减速解决的是哪个层面的问题1.1 梯形加减速的两个硬伤先说说为什么梯形的方案不够用。梯形加减速的数学模型非常简单速度随时间线性上升加速度恒定。写成公式就是 v v0 a·t加速阶段速度直线上升减速阶段直线下降。这个模型在低速场合完全够用代码写起来也很顺手但它在真实机械系统里有两处硬伤。硬伤一是加速度在瞬间发生阶跃。启动瞬间速度开始增长加速度从0直接跳到设定值a到了匀速段加速度又突然从a变成0减速起始和结束同样有两个加速度突变。这种突变产生的“冲击”在物理上对应无穷大的加加速度也就是jerk。机械系统最怕这种东西联轴器间隙被反复撞击导轨产生弹性形变整个平台的固有频率被激励起来表现出来就是整机抖动和低频噪声。硬伤二是步进电机本身的开环特性。步进电机没有编码器反馈一旦丢步控制器无从感知。电机的输出力矩会随着转速升高而下降也就是矩频特性曲线如果加速段初期的加速度拉得太猛转矩需求超过电机当前转速下能提供的最大力矩就会失步。梯形加速的启动阶段加速度最大恰好踩在这个软肋上。所以问题并不是梯形加减速“数学上错”而是它把机械冲击和力矩需求集中在几个时间点上对开环步进系统非常不友好。1.2 为什么步进电机开环控制必须“温柔”启动步进电机开环控制想要跑得稳核心逻辑就是“让速度变化尽可能平滑”。你可以想象自己开一台手动挡车如果起步时离合器瞬间全部松开、油门一脚踩到底乘客肯定会顿挫老司机的做法是先缓加油、等转速起来后再逐渐加深油门整个加速过程是渐变而非突变。S型加减速做的事情本质上就是模拟老司机开车的这种“温柔”。它的速度曲线不是直线而是一条先缓后急、再缓的S形曲线。加速过程中加速度从0开始逐渐增大达到峰值后再逐渐回落直到匀速段变成0。减速时完全对称。这样一来jerk是有限值机械系统受到的冲击被分散到整个加速区间。对步进电机来说这样做还有一个额外好处启动初期的加速度为零电机不会在起步瞬间被要求输出很大力矩而是随着速度上升在力矩能力相对充足的区间逐步释放加速度。对带负载的平台、丝杆、同步带结构这种启动方式的成功率明显更高。2. S曲线的数学原理与参数设计2.1 低频振动的根源加加速度突变要让加减速变平滑就要引入一个比加速度更底层的物理量加加速度也就是 jerk定义为加速度对时间的导数。梯形加减速的速度曲线是直线加速度曲线是矩形脉冲jerk在两个直角边处是无穷大的冲激信号。这个无穷大的冲击力就是共振和振动的根源。S型加减速的思路很直白让加速度不是阶跃变化而是连续地上升和下降。这样jerk就从“无穷大冲激”变成“有限值曲线”。工程上并不要求jerk完全连续只要把加速度变化率限制在可接受的范围内机械冲击就已经大幅下降。经典的7段S曲线把加速度过程拆成加加速、匀加速、减加速、匀速、加减速、匀减速、减减速七个阶段每段的jerk是常数。这种实现精度高但参数计算和状态切换都比较繁琐在步进电机这种本身不是超高精度的场合多数时候用不到那么复杂。2.2 用余弦函数构造S型速度曲线工程上我更推荐用余弦函数去构造S型速度曲线公式简洁、边界条件自然效果也足够平滑。加速段的速度公式写出来是这个样子v(t) v0 (vmax - v0) × (1 - cos(π·t / T_acc)) / 2这里v0是起始速度vmax是目标最高速度T_acc是加速段时间。当t0时1 - cos(0) 0速度等于v0当tT_acc时cos(π) -11 - (-1) 2整体等于(vmax - v0)速度正好到达vmax。这个形式妙在边界处的加速度自然为零。对时间求导a(t) (vmax - v0) × π / (2·T_acc) × sin(π·t / T_acc)可以看到sin值在t0和tT_acc时都等于0意味着启动和到达最大速度这两个瞬间加速度都是0没有突变。加速度的峰值在T_acc的一半处出现大小为Amax (vmax - v0) × π / (2·T_acc)再求一次导就能得到jerkj(t) (vmax - v0) × π² / (2·T_acc²) × cos(π·t / T_acc)jerk的峰值在起点和终点出现也是有限值。这就是S型曲线的本质特性速度连续加速度连续且首尾加速度为零。这里有一个容易误解的点我必须单独说一下如果T_acc和梯形加减速的加速时间一样S型曲线的加速度峰值反而比梯形还要高大约57%。因为梯形加速度均值是(vmax - v0)/T_acc而S型峰值约等于1.57倍的平均值。S型曲线的优势在于“没有突变”而不是“降低峰值”。所以设计参数时一定要以允许的最大加速度Amax为约束而不是套用梯形方案的时间。2.3 步进脉冲间隔表怎么生成步进电机每一步对应一个固定的角度增量比如1.8度步距角配合8细分后每一步是0.225度。速度控制最终转换成脉冲频率控制而S型规划给出的是速度-时间曲线需要把连续曲线离散成“每一步脉冲的周期”。连续速度曲线下每走一步所需的时间是变化的最直接的做法是“时间戳法”。原理是让仿真时间以很小的步长推进每推进一个微小时间片就累加一次位置增量 v(t)·dt当累计位移正好跨过某一步的整数倍时记录下当前时刻t_k。这一步的实际周期就是 t_{k1} - t_k。我在PC上用Python生成间隔表再把结果导出为C数组整个流程大概是这样的import math TIMER_CLK 72_000_000 # STM32定时器时钟 V0 400 # 起始速度 step/s VMAX 5000 # 目标速度 step/s T_ACC 0.15 # 加速时间 s ACC_STEPS 800 # 加速段最大步数 DT 0.00001 # 仿真步长 10us times [] pos 0.0 k 1 t 0.0 while k ACC_STEPS and t T_ACC: ratio t / T_ACC v V0 (VMAX - V0) * (1.0 - math.cos(math.pi * ratio)) / 2.0 pos_new pos v * DT if pos_new k: # 线性插值求出更精确的跨越时刻 t_cross t - DT (k - pos) / (pos_new - pos 1e-12) * DT times.append(t_cross) k 1 pos pos_new t DT periods [] for i in range(len(times) - 1): dt_k times[i 1] - times[i] arr int(round(dt_k * TIMER_CLK)) - 1 if arr 2: arr 2 periods.append(arr) print(const uint32_t speed_table[] {) for i, val in enumerate(periods): print(f {val},) print(};) print(f// accel_steps {len(periods)})生成的periods数组里每个元素就是对应那个脉冲周期在72MHz计数频率下需要计数的ARR值。之所以要减1是因为定时器的计数器从0数到ARR一共是ARR1个时钟周期。上电后MCU里不需要再做浮点计算直接把这张表放进FLASH就能用。3. STM32端的脉冲产生方案与状态机3.1 三种脉冲产生方案为什么我推荐PWM在STM32上给步进驱动器输出脉冲主流有三种做法。第一种是定时器中断里翻转GPIO。代码简单但每产生一个方波周期要进两次中断频率稍高时CPU负载明显而且如果中断被其他任务打断脉冲间隔会变得不均匀。第二种是定时器PWM硬件输出。让定时器工作在PWM模式输出引脚自动产生方波占空比固定为50%。速度变化只需要在比较中断里更新ARR和CCR值硬件自动按新频率输出CPU负担小脉冲均匀度高。这也是我推荐的方式。第三种是用DMA搬运一串预先设置好的ARR列表几乎不占CPU但每个运动过程都要准备一整张脉冲表长距离运动时内存开销很大一般用在有大量重复轨迹的场合。对大多数步进应用来说PWM方案是性价比最高的。我用的是TIM3的通道1做PWM输出同时使能比较中断。每来一个完整的PWM周期系统进一次中断在中断里根据速度表计算下一步的ARR和CCR然后写进定时器寄存器。3.2 运动状态机与步数规划一个完整的S型加减速运动包含加速、匀速、减速三个阶段为了把逻辑组织清楚我建议用一个简单的状态机不要把所有逻辑都堆在中断里。状态枚举如下typedef enum { STEPPER_IDLE 0, STEPPER_ACCEL, STEPPER_CRUISE, STEPPER_DECEL, STEPPER_FINISH } StepperState;运动开始时先做一次规划根据总步数total_steps、加速段步数accel_steps、减速段步数decel_steps算出有没有匀速段。如果总步数足够中间就有一块匀速段如果路程很短可能加速还没到最高速度就要开始减速这时匀速段步数为0。控制逻辑很简单脉冲计数器step_done记录已经发出的完整脉冲个数每个阶段用不同的速度表来源。加速阶段从头往尾查加速表匀速阶段用恒定的巡航ARR减速阶段把加速表反过来查。这样一张加速表既承担了加速又通过反序复用来承担减速代码量最少。启动运动前设置好方向引脚DIR把第一个脉冲的ARR和CCR写入定时器然后启动PWM输出。3.3 中断回调里的源码实现规划函数大概长这样void stepper_start(uint32_t total_steps, uint8_t dir) { if (total_steps 0) return; stepper.total_steps total_steps; stepper.step_done 0; stepper.state STEPPER_ACCEL; HAL_GPIO_WritePin(DIR_PORT, DIR_PIN, dir ? GPIO_PIN_SET : GPIO_PIN_RESET); if (accel_steps decel_steps total_steps) { stepper.cruise_steps total_steps - accel_steps - decel_steps; } else { stepper.cruise_steps 0; } __HAL_TIM_SET_AUTORELOAD(htim3, speed_table[0]); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, speed_table[0] 1); HAL_TIM_PWM_Start_IT(htim3, TIM_CHANNEL_1); }重点是中断回调这是整个S型加减速的核心void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance ! TIM3) return; stepper.step_done; if (stepper.step_done stepper.total_steps) { HAL_TIM_PWM_Stop_IT(htim3, TIM_CHANNEL_1); stepper.state STEPPER_FINISH; return; } uint32_t next stepper.step_done; uint32_t arr 0; if (next accel_steps) { stepper.state STEPPER_ACCEL; arr speed_table[next]; } else if (next accel_steps stepper.cruise_steps) { stepper.state STEPPER_CRUISE; arr cruise_arr; } else if (next stepper.total_steps) { stepper.state STEPPER_DECEL; uint32_t dec_i next - accel_steps - stepper.cruise_steps; arr speed_table[accel_steps - 1 - dec_i]; } else { return; } __HAL_TIM_SET_AUTORELOAD(htim3, arr); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, arr 1); }这里step_done的含义是“已经完成的完整脉冲数”。每次进入回调说明硬件刚刚又输出完一个完整的PWM周期所以先自增。然后以自增后的值作为“下一个待输出脉冲的索引”去速度表里查对应的ARR。第一次进中断时step_done变成1下一脉冲用的是speed_table[1]而speed_table[0]在启动时就已经写进定时器了两个逻辑完全对得上。减速段的反查要特别留意。假设加速表一共有800个元素匀速段结束之后next等于accel_steps cruise_steps那么dec_i等于0取的是speed_table[799]。next每加1arr就往加速表的头方向走一步正好形成对称的减速S曲线。4. 工程落地短行程处理与参数调优4.1 短行程下到不了最大速度怎么办最容易被忽略的情况是“短行程”。比如总步数只有300而加速段规划需要500步减速段又要500步显然来不及加速到最高速度就已经要刹车了。如果在代码里硬套长行程逻辑速度曲线会失真甚至出现还没加速就突然减速的怪异行为。处理思路是动态收缩速度顶点。运动总步数N固定后先判断一下梯形近似的临界条件如果按规划的最高速度vmax来计算加速距离加上减速距离已经超过N那就说明根本到不了vmax需要重新算一个“实际能达到的最高速度”。工程上不用做太精确的推导做一个比例收缩就能用。把总步数N拆成两部分加速段和减速段各占一部分然后根据N重新生成一张“短行程专用速度表”。在PC端生成表的时候就判断一下如果加速表长度加减速表长度大于总步数就减小vmax重新生成。还有一种更省事的处理方式对短行程直接用对称S型也就是加速占用一半步数减速占用另一半整个运动就是一张“从小到大再从大到小”的速度表。这种情况下没有匀速段电机刚加速到位就开始减速体验上依然是平滑的。代价是高速度来不及充分体现但对短距离定位来说这是必然的物理限制。4.2 减速点和恒速段怎么算有了加速表之后减速点不用单独算一个“位置”纯粹靠步数索引就够了。我在规划函数里把total_steps、accel_steps、cruise_steps存进结构体中断回调里通过比较next与各阶段步数边界天然就知道当前处于加速、匀速还是减速。有个细节需要注意减速段的反查表是speed_table[accel_steps - 1 - dec_i]其中dec_i从0递增到decel_steps-1。那么减速段使用的最后一个值应该是dec_i等于decel_steps-1时对应的speed_table[accel_steps - decel_steps]。如果accel_steps大于decel_steps数组访问没问题如果accel_steps小于decel_steps那说明这张表设计有问题减速段没法完全覆盖。我一般强制要求减速段步数和加速段步数相等都是accel_steps这样反查一定安全。匀速段的cruise_arr也不是随便取。它对应的速度是vmax所以arr值可以直接从速度表最后一个元素近似得到。我在PC端生成表时就额外输出一个cruise_arr#define CRUISE_ARR ((uint32_t)(TIMER_CLK / VMAX) - 1)如果加速表最后一个arr和这个值差得比较多说明加速段结束时的速度收敛得还不够可以把T_ACC调大一点让速度曲线更平滑地靠近vmax。4.3 调参顺序与经验参数表我调试时不会一上来就改运动曲线而是先按下面这个顺序把基础参数定下来。第一步确定起始速度v0。v0不能设成0因为速度为0意味着脉冲周期无穷大第一步永远等不完。实际工程中v0一般取300到800 step/s保证电机在启动频率内能可靠建立力矩。起步太慢会拖慢整个节拍起步太快又容易在第一步就丢步所以要结合负载试。第二步确定最高速度vmax也就是目标脉冲频率。对大多数42步进电机配细分驱动器5000到10000 step/s是一个常见区间超过这个区间必须检查电机发热和力矩。第三步根据允许的最大加速度Amax反推T_acc。根据公式 Amax (vmax - v0) × π / (2·T_acc) 可以得到T_acc (vmax - v0) × π / (2·Amax)如果对Amax没有概念可以先取一个经验值比如5000到20000 step/s²再根据抖动和丢步情况增减。下面这张表给出一组可以参考的初始参数参数推荐范围说明v0300 ~ 800 step/s启动频率需低于电机不失步频率vmax3000 ~ 10000 step/s高速段受力矩和驱动器限制T_acc0.1 ~ 0.3 s加速时间影响加速度峰值Amax5000 ~ 20000 step/s²峰值加速度决定T_acc细分8 ~ 32细分越高单步越平滑但最高频率受限调参的时候有个很实用的建议先用一个大点的T_acc比如0.3秒确保启动不丢步然后逐步减小T_acc每次减小20%左右同时听电机的声音。一旦开始出现“嘶嘶”或“咔咔”的异常声就退回到上一次的参数再加10%余量作为最终值。5. 调试实录与避坑指南5.1 电机明明在转声音却很难听这个问题我遇到过很多次第一反应往往是加减速曲线不对但检查完发现S型参数本身没问题。后来用示波器抓了STEP引脚的波形才意识到真正的问题是脉冲频率在某些点发生了跳变。跳变的来源有两个。第一个是速度表的量化误差如果相邻两项ARR的差值不是平滑递减而是忽大忽小电机听起来就会有沙哑感。解决办法是在PC端生成表时增加仿真步数比如把DT从10us缩小到2us让每步周期变化更平滑另一个办法是在速度表中做一下滑动平均滤波但注意不要过度滤波以免破坏首尾的加速度零边界。第二个来源是巡航段和加速表最后一个值衔接不上。如果cruise_arr与speed_table[accel_steps-1]差得太多速度会有一个小台阶听感上像“咯噔”一下。让加速表最后一个值尽量接近匀速ARR或者直接让巡航段从加速表过渡而不是立刻换到常量。5.2 突然丢步先从启动频率查S型加减速加持下还是丢步我排查顺序永远是先看v0再看Amax最后看vmax。v0太高是最常见的元凶。有人觉得S型曲线很平滑就把起始速度调到2000 step/s结果起步瞬间力矩根本不够第一步就丢了。起步频率一定要按实际负载测空载可以800带了一根丝杆和滑台之后可能要降到300甚至更低。Amax太高是第二个原因。S型只是让加速度平滑上升峰值依然存在。如果峰值加速度超过了电机在对应速度下的力矩能力到了加速中段一样丢步。用手捏住滑台感受一下启动力矩如果觉得推背感太强大概率就是Amax偏大。最后才是vmax。步进电机高频力矩下降很快vmax太高时电机转矩不足即使加减速曲线再好也没用。确认办法把vmax逐步降下来如果降到某个值以后丢步现象消失说明是高速力矩不够。5.3 中断里别做多余的事PWM回调里的代码要尽量精简原则是除了更新ARR、CCR和简单计数之外不要再做任何耗时操作。我有一次为了调试方便直接在中断里调用HAL_GPIO_TogglePin去翻转LED结果输出脉冲的周期被拉长了差不多一倍速度曲线完全失真。更危险的操作是中断里调用printf、做浮点除法、或者执行HAL_Delay。这类操作会让中断服务时间超过脉冲周期导致脉冲丢失而且很难查。调试信息通过串口发送时应该先写到一个环形缓冲区在主循环里统一发送。如果发现中断处理时间真的紧张可以进一步优化把速度表从FLASH拷贝到RAM减少FLASH读取等待ARR和CCR的更新直接操作寄存器不经过HAL封装。实测下来72MHz主频下处理一次回调只用几十个时钟周期就够了。5.4 用示波器验证S曲线是否生效代码写完了怎么确认S曲线真的一步步在按预期执行我推荐用示波器抓STEP引脚并把时基调到能看多个脉冲的位置观察脉冲之间的间隔。加速阶段示波器上应该看到脉冲越来越密也就是频率越来越高。减速阶段则反过来脉冲越来越稀疏。如果只看波形觉得不明显可以把示波器切换到频率测量模式或者用逻辑分析仪记录每个脉冲的时间戳导到电脑里画成速度曲线。这个办法最能直接反映加减速的真实形态比耳朵听可靠得多。还有一个点是在视察脉冲间隔的同时结合DIR引脚的逻辑判断运动方向防止减速段反查表的索引方向搞反。方向反了的表现是加速段正常但到了后程频率不降反升和预期正好相反。我在调试阶段习惯把v0设低、T_acc设大让S曲线特性在示波器上看得非常明显确认基础逻辑无误后再逐步把参数逼近实际工况。这样一次调通的概率最高也少走很多弯路。
阅读完成 · 觉得有帮助?