简介:基于STM32的步进电机加减速程序是一份嵌入式电机控制学习资料,面向STM32开发者、自动化及机器人方向的初学者与进阶者,解决步进电机平滑加减速与实时控制问题。压缩包共221个文件,约6.2MB,内有不少C源文件、头文件、汇编启动文件、工程配置文件、编译中间文件及调试记录文档,目录结构完整,可对照学习STM32CubeMX初始化、定时器脉冲输出、加减速算法与中断服务等关键环节。已有3150人学习下载。资料围绕单轴电机控制项目展开,涵盖梯形/S曲线加减速实现、步进驱动芯片控制、PWM脉冲调节及串口调试技巧,并附有编译生成的hex、map、axf等文件,便于直接烧录验证与实际排错。对于希望深入理解STM32定时器与电机控制结合的开发者,这是一份可直接参考的工程范例,也适合作为课程设计或入门项目的起步模板。
1. 为什么步进电机会丢步,加减速到底在解决什么问题
先说个很多新手容易踩的现象:用STM32直接给脉冲引脚一个固定频率,步进电机确实能转,但频率调高到某个值之后,电机会突然发出“嗡嗡”的异响,然后停在原地不动了——这就是经典的失步(丢步)。更诡异的是,有时候电机转是转了,但停下来会往前“冲”一小段,位置对不上。这两个问题,本质上都不是电机坏了,而是你从头到尾就没做加减速。
要理解加减速的必要性,得先搞清楚步进电机的转动模型。步进电机内部是一组定子绕组和永磁转子,你每发一个脉冲,控制器就切换一次绕组通电状态,转子就往前“咬合”一步。转速的本质是脉冲频率,位置的本质是脉冲个数。问题在于,转子是有惯量的,绕组产生的电磁转矩在低速时足够拉动转子,但转速越高,反电动势越大、绕组感抗越强,实际转矩会显著下降——这就是步进电机的矩频特性曲线,几乎所有驱动器的数据手册里都有这条曲线。
回到丢步的场景:电机静止时,转子停在某个稳定平衡位置。如果你一瞬间把脉冲频率拉到很高,第一个脉冲来了,转子还没来得及完成一步的角位移,第二个脉冲又来了。转子跟不上指令,最终累积偏差越来越大,就是丢步。反过来,高速运行中突然把频率降为0,转子因为惯性还会继续转,绕组却已经切换到制动状态,于是发生过冲。丢步和过冲,就是没有加减速的必然结果。
所以加减速的本质是什么?是让脉冲频率按一个合理的斜率从低到高爬升,再按合理的斜率从高到低下降。加速度不能超过电机在当前频率下的最大可跟踪能力,否则照样丢步。它就像开车:你不可能从静止直接一脚踩到120km/h,也不可能从120km/h瞬间刹停,必须有一个过渡过程。步进电机控制里,这个“过渡过程”就是加减速曲线。
实际工程中,加减速还有另一层作用:抑制机械振动。步进电机在中低频段存在明显的共振区域,通常是几十到几百赫兹之间,取决于电机型号和负载。快速跨越共振区可以显著减少噪音和振动,这也是很多高速应用必须做加减速而不是恒定高速运行的原因之一。
另外要给小白提个醒:加减速不解决“扭矩根本不够”的问题。如果总负载已经超过电机最大静转矩,加减速做得再漂亮电机也转不动。加减速解决的是“扭矩跟得上但转子惯量跟不上”的动态问题,这一点要先想清楚,别程序写完发现电机带不动然后怀疑算法写错了。
2. 梯形与S形:两种主流加减速算法的本质差异与选型
加减速的工程实现,归根结底就是设计一条“频率-时间曲线”。业界用得最多的就两套:梯形加减速和S形加减速。它们没有本质的谁优谁劣,只有适不适合当前场景。
梯形加减速,曲线是一条三段折线:加速段、匀速段、减速段。加速段里频率线性上升,也就是说恒定加速度。它的数学模型最简单,只有一个加速度常量,代码里用一个累加器就能实现,很多开源的步进库和GRBL固件底层用的都是梯形规划。它的优缺点都很鲜明:简单、CPU开销低、加减速过程快,但在加速段起点和终点,加速度有突变,对应到机械上就是冲击感比较明显,高速运动时会有顿挫和震动,对运动平稳性要求高的场合不够用。
S形加减速,曲线是一条S形弧线。它在加速段起始和结束位置做了平滑过渡,加速度本身是渐变的,而不是恒定的。工程上常用三段S形或者七段S形来近似。七段S形是教科书里最完整的形态:加加速度段(jerk in)、匀加速段、减加速度段、匀速段、加减速度段、匀减速段、减减速度段。每一段的加速度都是连续变化的,位移、速度、加速度三条曲线全部连续,机械冲击最小。
两者怎么选?我的经验很简单:
- 定位精度要求高、启停频繁、机械刚性不足的场合,比如3D打印机、激光雕刻机的某些轴,建议用S形,平稳性对打印质量和机械寿命影响很大。
- 追求速度极限、负载轻、结构刚度好的场合,比如高速贴片机的送料轴、轻载传送带,用梯形就够了,加减速时间短,能更快到达目标速度。
- 如果不确定,先上梯形跑通,再评估振动是否超标,根据实测数据决定要不要升级S形,不要一上来就搞复杂的规划而忽略了基本功能验证。
一个容易被忽视的选型维度是系统的处理器算力和中断负载。梯形算法一个定时器中断里维护几个变量就行,S形要多维护好几段的状态机和曲线映射表。STM32F103这类M3内核跑72MHz其实两个都扛得住,但如果你用的是F0系列、主频只有48MHz甚至8MHz的,而且中断里还在做别的事(比如实时闭环、通信处理),那S形每个脉冲中断的大量浮点和查表运算就可能拖慢主循环。这个时候优先考虑梯形,或者把S形表做成查表法,牺牲内存换时间。
对了,还有一类是“指数加减速”,很多老式的开环步进驱动板会用RC电路做硬件指数加速,但说实话精度很差,现在自己做控制器基本都是梯形或S形二选一。下面我先把梯形算法的实现细节讲透,再说怎么在它基础上改造成S形。
3. STM32定时器动态调速:核心实现思路与开箱代码
步进电机加减速的程序怎么落地到STM32上?市面上的主流做法有两种,我先说各自的适用场景,再给出完整可用的代码。
第一种方案:用定时器产生固定频率PWM,加速度放在一个单独的高频定时器中断里去做,每次中断调整PWM频率。这种方案会用到两个定时器,逻辑上绕了一层,但优点是PWM生成和调速逻辑解耦,波形更稳定。第二种方案:直接用定时器输出PWM,并且让PWM频率本身在中断里动态改变,每个脉冲周期都重新装载定时器的自动重载值ARR。这种方案只用一个定时器,逻辑更紧凑,是大多数开源方案采用的做法,我下面详细讲这种。
代码基于STM32标准库,选TIM3作为脉冲发生器,配置成PWM输出模式,输出比较通道接到步进驱动器的脉冲输入端。核心思路是:每个PWM周期结束(更新事件)进入中断,在中断服务函数里根据当前所处状态(加速/匀速/减速)计算下一步的频率,换算成ARR值写回定时器,同时维护步数计数器。
先看定时器初始化,主频72MHz,预分频器设成1,计数时钟就是36MHz:
void TIM3_Init(uint32_t initial_freq) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 1000000 / initial_freq; TIM_TimeBaseStructure.TIM_Prescaler = 1; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = (1000000 / initial_freq) / 2; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInit(TIM3, &TIM_OCInitStructure); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); TIM_Cmd(TIM3, ENABLE); }代码里最关键的一行是TIM_Period = 1000000 / initial_freq。这里要注意:定时器计数时钟是36MHz,所以1秒产生36,000,000个计数,要让定时器产生频率为f的输出,ARR就得是36000000 / f。我上面的代码预分频设的是1,实际计数时钟就是 72MHz / (1+1) = 36MHz,所以ARR = 36000000 / f。初值配置里频率单位是Hz,这个换算关系在任何动态调速的代码里都会反复出现,建议单独写一个宏或函数,别到处硬编码。
紧接着是动态调速的核心逻辑,我把它放在更新中断服务函数里:
volatile int32_t step_count = 0; // 已发送脉冲数 volatile int32_t target_steps = 0; // 目标总步数 volatile uint32_t accel_steps = 0; // 加速段步数 volatile uint32_t decel_steps = 0; // 减速段步数 volatile uint32_t run_steps = 0; // 匀速段步数 volatile uint32_t current_freq = 0; // 当前脉冲频率 volatile uint32_t start_freq = 500; // 起始频率,单位Hz volatile uint32_t max_freq = 20000; // 最大频率 volatile uint32_t accel_rate = 500; // 每步频率增量 volatile uint8_t motion_state = 0; // 0停止 1加速 2匀速 3减速 4结束 void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); if (motion_state == 0) return; step_count++; if (motion_state == 1) { current_freq += accel_rate; if (step_count >= accel_steps) { motion_state = 2; current_freq = max_freq; } } else if (motion_state == 2) { if (step_count >= accel_steps + run_steps) { motion_state = 3; } } else if (motion_state == 3) { if (current_freq > start_freq) { current_freq -= accel_rate; if (current_freq < start_freq) current_freq = start_freq; } if (step_count >= target_steps) { motion_state = 4; TIM_Cmd(TIM3, DISABLE); return; } } TIM_SetAutoreload(TIM3, 36000000UL / current_freq); TIM_SetCompare1(TIM3, (36000000UL / current_freq) / 2); } }上面这个中断函数把梯形加减速的三个阶段都包含了。注意几个细节:
第一,加速和减速用的是同一个accel_rate变量,这样对称性最好,实际运动过程中加速段和减速段的步数天然一致,不会出现跑完加速段却发现减速距离不够的尴尬情况。如果非要不对称升降速,可以分开定义两个变量,但从工程角度看,除非特殊需求,对称升降速是最省心的选择。
第二,start_freq不能设成0。ARR=36000000/0在数学上无意义,在代码里会溢出得到一个很大的ARR值,定时器周期变成几秒,电机实际处于停转状态。一般起步频率选200到1000Hz比较合理,太小了浪费时间,太大了启动瞬间可能丢步,需要根据电机的启动频率实测。这个“启动频率”也是步进电机手册里的一个关键参数,指电机不经过加速直接启动所能承受的最大脉冲频率,通常都不高。
第三,这个中断服务函数每产生一个脉冲就进一次中断。如果最大频率是40kHz,意味着中断频率也是40kHz,CPU在中断里的时间随着频率升高而占比变大,主循环容易被饿死。所以这里的accel_rate和定时器配置要提前算好,在满足加减速精度的前提下尽量降低最大频率,别把CPU耗在无意义的高速中断上。一般步进电机的实际工作频率很少超过30kHz,对STM32F103来说这个负载还能接受。
加速段步数accel_steps和减速段步数decel_steps怎么算?这是梯形加减速规划的核心,用公式算一下。设加速段步数为 n,起始频率 f0,最大频率 fmax,每步频率增量为 a(也就是accel_rate),那么加速段步数:
n = (fmax - f0) / a这个公式反过来也能用:已知目标步数和最大频率,反推加速度,保证运动过程中有足够距离完成加速和减速。总步数必须满足:
target_steps >= 2 * n如果总步数小于两倍的加速步数,说明行程太短,电机刚加速完还没来得及匀速就得开始减速,这时候必须降低最大频率或降低加速度,否则峰值频率达不到设定值就会丢步。我在实际调的时候遇到过这个坑,后面有一小节专门说。
梯形规划在启动一个运动任务前要做的事很简单:
void Motion_Start(int32_t steps, uint32_t freq_max, uint32_t freq_start, uint32_t rate) { uint32_t accel_n = (freq_max - freq_start) / rate; if ((uint32_t)(steps > 0 ? steps : -steps) < 2 * accel_n) { // 行程太短,自动降低最大频率 freq_max = freq_start + rate * ((uint32_t)(steps > 0 ? steps : -steps) / 2); accel_n = (freq_max - freq_start) / rate; } target_steps = (steps > 0) ? steps : -steps; accel_steps = accel_n; run_steps = target_steps - 2 * accel_n; decel_steps = accel_n; current_freq = freq_start; start_freq = freq_start; max_freq = freq_max; accel_rate = rate; step_count = 0; motion_state = 1; TIM_SetAutoreload(TIM3, 36000000UL / current_freq); TIM_SetCompare1(TIM3, (36000000UL / current_freq) / 2); TIM_Cmd(TIM3, ENABLE); }这个函数就是梯形加减速的“规划层”,它只算三个阶段的步数,然后启动定时器,剩下的交给中断里的“执行层”去完成。规划层和执行层分离是我强烈推荐的编程风格,后续想升级S形曲线,只需要改规划层的计算公式和中断里的调整策略,接口不用动。
再说位置控制方向的问题。上面代码只管发脉冲,方向信号DIR需要单独用一个普通IO口控制,方向切换必须在电机完全停止后执行,否则运动中途突然反转会造成剧烈的机械冲击,严重时直接损坏丝杠和电机。所以Motion_Start里应该先判断目标位置和当前位置的方向,设置好DIR引脚电平,再启动定时器。
if (steps < 0) { DIR_PIN_HIGH; } else { DIR_PIN_LOW; }还有一个细节:中断里定时器停止用的是TIM_Cmd(TIM3, DISABLE),停止前最好手动把输出引脚拉到一个确定电平,因为某些驱动器对于脉冲输入的下降沿敏感,如果不确定电平可能造成误动作。我自己习惯在停止前把PWM占空比固定为50%且频率保持原状,这样波形不会出现半个脉冲的毛刺。
4. 从梯形到S形:改造思路与抖动问题处理
梯形加减速虽然简单好用,但有些场景确实不够用。我最早在做一个桌面级雕刻机改造的时候,Z轴用的是梯形加减速,动作时能明显听到电机“咯噔咯噔”的顿挫声,空跑测试时龙门架轻微晃动。后来换成S形加减速,问题明显改善。这个改造并不复杂,核心就一句话:把“恒定加速度”改成“加速度从0渐变再归0”。
S形的完整实现有很多种,最严谨的是七段式,但我在实际项目里用的是简化版三段式S形,已经能满足绝大多数场合的平稳性需求。思路是:把原加速段的步数再分成三段——凸加速段、线性加速段、凹加速段。凸加速段里加速度从0逐渐增大,中间的线性加速段保持恒定加速度,凹加速段里加速度逐渐减小到0。这样速度曲线的拐弯处被磨圆了,机械冲击就小了。
具体到定时器中断里的代码,梯形加减速是每个脉冲把频率加上一个固定值,S形则是这个固定值本身也在动态变化。可以引入一个“加加速度”的概念,每次中断里先更新加速度,再用当前加速度去更新频率:
volatile int32_t jerk = 0; // 加加速度,每步的加速度增量 volatile int32_t accel = 0; // 当前加速度,每步的频率增量 volatile int32_t accel_target = 0; // 目标加速度(线性段用) void TIM3_IRQHandler_S(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); if (motion_state == 0) return; step_count++; if (motion_state == 1) { // 凸加速段 accel += jerk; if (accel >= accel_target) { accel = accel_target; motion_state = 2; // 进入线性加速 } } else if (motion_state == 2) { // 线性加速段 // accel保持不变 if (step_count >= accel_convex_steps + accel_linear_steps) { motion_state = 3; // 凹加速段 } } else if (motion_state == 3) { // 凹加速段 accel -= jerk; if (accel <= 0) { accel = 0; motion_state = 4; // 匀速 } } // ... 其他状态类似 current_freq += accel; TIM_SetAutoreload(TIM3, 36000000UL / current_freq); TIM_SetCompare1(TIM3, (36000000UL / current_freq) / 2); } }这段代码只是示意,实际项目里还要考虑每段的步数边界、最大频率限制、减速段的镜像处理。但核心思想很清楚:加加速度jerk控制了加速度变化的快慢,jerk越大越接近梯形,jerk越小曲线越平滑。具体jerk取值没有公式可以直接算出最优解,我的经验是先按“加速段总步数的20%为凸段、60%为线性段、20%为凹段”的比例分配,再根据实际振动情况调整。
S形加减速一个常见的坑是:频率变化到极低速时,由于频率增量被加速度控制得很小,每个脉冲间隔的“时长变化”可能突然跳变,导致转速曲线出现肉眼可见的抖动。这个问题在低频段特别明显,因为低频段ARR值很大,ARR微小的变化就能引起频率很大的相对波动。解决办法是:在低速段不要用S形,先用固定的小加速度,等频率超过某阈值(比如1kHz)再切换到S形规划。这相当于把S形的起点放在低速段之上,既保证了启动平顺,又避免了低频抖动。
5. 实测调参记录:加速度上限、细分、共振区怎么避
程序写完只是第一步,真正让电机跑起来不丢步、不振动、不发热,得靠实测调参。这一节我把自己踩过的坑和调参经验整理成几个可操作的原则。
关于加速度上限的判定方法。理论上加速度上限由电机的最大转矩和负载惯量决定,但实际项目里没人去精确算转动惯量,一般用经验法:设定一个加速度,让电机从静止加速到最大频率,观察是否丢步。如果丢步,把加速度减半再试。反复二分直到找到一个临界值,然后留出30%的余量作为最终值。以42步进电机常见的配置为例,不带负载时加速度设到30000步/s²问题不大,带个几公斤的丝杠负载就得降到8000以下,差距非常大。
关于细分的设定。驱动器上的细分倍数直接影响脉冲频率和步距角。细分8细分意味着电机每步被分成8个微步,同样转速下需要的脉冲频率是整步模式的8倍。细分越高,低速平稳性越好,但脉冲频率需求也越高,对MCU的中断负载越大。我一般这么选择:需要低速运行平稳的场合用16细分,追求高速的场合用8细分,4细分现在很少用了。细分本身不影响加减速算法的结构,只影响频率和步数之间的换算关系。
关于共振区的判断与跨越。步进电机在某个脉冲频率范围内会产生共振,典型表现是噪音突然变大、振动剧烈。这个共振区间和电机本身的固有频率有关,通常在100到400Hz之间。最简单的方法是在调试时把速度从低往高扫一遍,留意哪个频率段噪音最大。知道共振区间之后,要么让加减速曲线在这个区间内快速通过(比如适当增大加速度),要么干脆把运行频率区间避开共振区,另选一个工作速度。加减速本质上是让电机快速穿过共振区而不是停留在里面,这也是前面说的“加减速抑制振动”的工程来源。
关于最长运行时间的发热问题。步进电机长时间满速运行发热非常明显,尤其是堵转或负载偏大的情况下,电机外壳温度能到七八十度。加减速程序本身不解决发热问题,但要留好温度保护逻辑:超过设定温度主动停机,别等烧了线圈再排查。我在自己的项目里加了一个简单的NTC测温方案,贴在电机外壳上,ADC采样,超温就停脉冲并报警。
关于启动频率的实测。前面提到启动频率不能设成0,具体值怎么定?把目标频率设成你想要的值,加速度设成0,直接给脉冲,看电机能不能转起来。如果转不起来,把频率降下来再试,找到临界值。这个临界值的60%左右就作为加减速程序里的起始频率。我的经验是:42步进电机空载,起始频率300到600Hz比较稳;28步进电机因为转子小、惯量低,可以到800Hz甚至更高。
6. 写完程序之后必须做的硬件检查
加减速程序写得再好,硬件接线不对也是白搭。很多“程序没问题但电机不转”的案例,最后查出来都是硬件问题。这一节补充几个我在调试过程中踩出经验的检查点,写给第一次做这个项目的朋友参考。
方向信号和脉冲信号的接线。5V逻辑的驱动器接STM32的3.3V引脚,电平匹配一般没问题,但最好在脉冲引脚和DIR引脚上串一个100到330欧的限流电阻。市面上有些驱动器的光耦输入电路没有内置限流电阻,直接接高电平可能电流过大。反过来,如果驱动器要求24V逻辑电平,3.3V的TTL信号驱动不了,必须加电平转换电路,这一条最容易忽略。
共阳还是共阴接法。各类驱动器的PUL+、PUL-、DIR+、DIR-的接法五花八门,一定要先看手册,搞清楚驱动器是共阳还是共阴。我自己就试过把共阳接成共阴,结果电机纹丝不动,排查了半天才发现是接法问题。共阳是把正极统一接+5V,控制信号接负极;共阴是负极统一接GND,控制信号接正极。STM32的输出只能拉低或拉高,选对应的接法才能让光耦正确导通。
电源容量。步进电机启动瞬间的电流冲击很大,加减速做得再好也改变不了这一点。选电源的时候要看电机的额定相电流和驱动器的推荐电压,最低容量的经验公式是电机相电流 × 供电电压 × 1.5。比如42电机设定1.5A相电流,用24V供电,推荐电源功率至少54W,实际选60W或100W更稳。
接地和抗干扰。步进电机本身是很大的电磁干扰源,尤其PWM快速切换绕组的瞬间,会产生强烈的电压尖峰。STM32的3.3V系统如果和电机驱动共用电源,很容易被干扰导致复位或死机。我的做法是:控制板的电源和驱动器电源严格分开,两者只在一点共地。信号线用双绞线,脉冲线和方向线分开走,别和电机动力线绑在一起。还有一个容易忽略的是在驱动器脉冲输入端子并联一个几百皮法的电容,可以吸收高频毛刺,防止误触发。
转轴的机械锁紧。步进电机输出轴和联轴器之间的连接如果松动,加减速运行几次之后位置偏差就会累积得很明显。检查时用手晃动联轴器两侧,如果有间隙就拧紧螺丝。这个问题看着低级,但在实际故障案例里的发生率不低,尤其电机高速振动一段时间后,螺丝松脱很常见。
做完这些硬件检查,再配合前面的加减速程序,电机的运动控制框架基本就完整了。沿着这个思路继续往下走,还能加协议层、做成多轴联动、加上位机指令解析、甚至替换成闭环步进电机来进一步提高定位精度,但那是另一个阶段的话题了。
最后分享一个小经验:调加减速时别把示波器只挂在PWM输出脚上看波形,真正实用的是在电机的A/B相串电阻采样电流波形,电流波形的变化能直观反映电机有没有走到位的趋势。加速段电流平滑上升、匀速段电流稳定、减速段电流波动减小,这样的三段特征出现,说明加减速参数基本调对了。如果加速段电流出现明显的失步特征——电流突然跌到零再重新建立——不用看别的,先减小加速度。这套观察方法在我后来的每个步进项目里都用得上,比纯看代码逻辑直观得多。
本文还有配套的精品资源,点击获取