STM32F103实现步进电机S型加减速的硬件级方案
2026/9/13 16:37:40 网站建设 项目流程

简介:本资源是一套基于STM32F103微控制器的步进电机S曲线加减速控制完整工程,面向嵌入式初学者、电机控制实践者及自动化项目开发者,解决步进电机启停冲击大、运行不平稳等实际问题。压缩包共149个文件,含30个C源码与29个头文件(实现定时器配置、PWM生成、S形速度规划等核心逻辑),18个.o和.d编译中间文件,以及uvproj/uvopt等Keil工程配置文件,整体大小1.85MB,结构完整,开箱即可编译调试。已有650人学习下载,资源附带详尽算法说明与硬件接口设计思路,涵盖GPIO驱动、中断调度、细分控制模式(全步/半步/微步)及S曲线参数(tacc/tdec)计算方法,代码模块清晰、注释充分,特别适合深入理解电机运动控制底层实现与工程落地。

1. STM32F103控制步进电机时,为什么“匀速走半秒就抖,加速到一半就失步”?这不是驱动板问题,而是加减速算法没落地

很多用STM32F103做运动控制的工程师,第一次把42步进电机接上DRV8825或A4988驱动板后,会发现:空载下用固定频率的GPIO翻转或TIM输出PWM脉冲,电机能稳稳转;但只要一加速度——哪怕只是从0加速到1000pps、持续200ms——立刻出现啸叫、丢步甚至堵转。更困惑的是,换更高供电电压、更大电流驱动板,问题依旧。根本原因不在硬件选型,而在于STM32F103资源有限条件下,S型加减速算法未被正确裁剪、定时器中断未与脉冲生成严格解耦、关键参数未按电机-负载惯量实测标定。这不是“加个延时for循环就能解决”的小问题,而是涉及定时器级联配置、中断优先级抢占、脉冲计数器溢出防护、以及加速度斜率与步进电机反电动势特性的硬匹配。本文面向已能用标准外设库(v3.5.0)点亮LED、配置基本TIM输出的开发者,不讲PID闭环或EtherCAT总线,只聚焦在单片机裸机环境下,如何用最小代码量、最稳中断逻辑、最易调参方式,让F103真正驱动步进电机走出平滑S曲线。你不需要HAL库、不需要RTOS,甚至不需要CubeMX——但必须理解TIM的更新事件与脉冲边沿的时序咬合点。

2. 用TIM2+TIM3级联实现S型加减速脉冲发生器:为什么必须放弃“软件延时+for循环”方案

2.1 传统软件延时方案为何必然失败:从CPU周期到电机响应延迟的硬约束

初学者常写如下代码:

void step_move(uint32_t steps, uint16_t start_freq, uint16_t max_freq, uint16_t acc_step) { uint16_t freq = start_freq; for(uint32_t i = 0; i < steps; i++) { GPIO_SetBits(GPIOA, GPIO_Pin_0); // 发一个脉冲 delay_us(1000000 / freq); // 频率倒数即周期 GPIO_ResetBits(GPIOA, GPIO_Pin_0); if(freq < max_freq && i < acc_step) freq += 10; // 线性加速 } }

这段代码在Keil中编译后,delay_us()实际消耗约12~15个CPU周期(72MHz主频下≈167ns/周期),但函数调用开销、循环判断、变量自增等额外指令使实际最小延时下限卡在1.8μs左右,对应最高脉冲频率仅555kHz——看似够用。然而问题在于:

  • delay_us()是阻塞式,期间无法响应任何中断,包括编码器反馈、急停信号;
  • freq每次自增10Hz,加速度恒定,生成的是梯形加减速曲线,而非S型;
  • 更致命的是,当freq接近10kHz时,1000000/freq计算结果为100,但delay_us(100)实际执行时间因编译器优化等级不同,在-15%~+25%间漂移,导致脉冲间隔抖动超10μs——这已超过42步进电机典型保持转矩响应时间(8~12μs),直接引发失步。

提示:STM32F103的72MHz主频不是用来“算除法”的,而是用来“精确触发硬件事件”的。所有脉冲边沿必须由硬件定时器直接控制,软件只负责更新下一个周期值。

2.2 TIM2+TIM3级联方案:用硬件自动完成S曲线微分积分

S型加减速本质是对速度曲线进行两次积分:加速度a(t)→速度v(t)→位移s(t)。在单片机上,我们不直接解微分方程,而是用查表+插值法生成预计算的“脉冲间隔数组”,再用定时器按序读取。但F103 RAM仅20KB,存不下万级点数组。因此采用双定时器级联架构

  • TIM3作为主定时器:工作在向上计数模式,ARR=65535,时钟源为72MHz经PSC=71分频 → 基础计数频率1MHz(1μs/计数)。其更新事件(UEV)每65.536ms触发一次,用于驱动加速度斜率更新;
  • TIM2作为脉冲发生器:工作在PWM模式,CH1输出脉冲高电平,CH2输出方向电平。其ARR由TIM3的捕获比较寄存器CCR1动态写入——即TIM3每产生一次UEV,就计算下一个脉冲周期T,并写入TIM2->ARR,TIM2立即按新周期重装计数器并输出脉冲。

该架构将“计算下一个周期”和“输出当前脉冲”完全解耦:TIM3在UEV中断中执行S曲线算法(含浮点运算),耗时可长达50μs;TIM2则始终以硬件精度输出脉冲,零软件干预。实测在72MHz下,TIM2脉冲抖动<±1个系统时钟周期(13.9ns),远低于步进电机敏感阈值。

2.3 S曲线算法核心:三段式分段函数与F103定点化实现

S型加减速分为七段(对称S型)或五段(非对称),工程中常用三段式简化模型

  1. 加加速段(Jerk-up):加速度a从0线性增至a_max,持续时间t1;
  2. 匀加速度段(Constant-accel):a=a_max恒定,持续时间t2;
  3. 减加速段(Jerk-down):a从a_max线性减至0,持续时间t1。

对应速度v(t)为三次多项式:
v(t) = v0 + j·t²/2 - j·t³/(6·t1)(0≤t≤t1)
其中j为加加速度(Jerk),决定S曲线陡峭度。

为适配F103无FPU特性,全部转为Q15定点运算(16位整数,小数点后15位):

  • 定义宏:#define Q15(x) ((int16_t)((x)*32768.0f))
  • 将j、t1、t2等参数存为Q15格式;
  • 关键计算j * t² / 2改为:(int32_t)j * t * t >> 15(先扩为32位防溢出,再右移15位还原Q15)
// S曲线速度计算(Q15定点版,t为当前计数值,单位:μs) int16_t calc_s_curve_speed_q15(uint32_t t, int16_t j_q15, uint16_t t1_us, uint16_t t2_us) { if (t <= t1_us) { // 加加速段 int32_t t_q15 = Q15((float)t); int32_t t_sq = (int32_t)t_q15 * t_q15; // t² in Q30 int32_t term1 = (int32_t)j_q15 * t_sq >> 15; // j*t²/2 in Q15 int32_t term2 = (int32_t)j_q15 * t_q15 * t_q15 / (6 * t1_us); // j*t³/(6*t1) return (int16_t)(term1 - term2); } else if (t <= t1_us + t2_us) { // 匀加速度段 return Q15((float)(t1_us * t1_us * j_q15 / 2 / 32768)); // v_max in Q15 } else { // 减加速段(对称) uint32_t t_prime = t - t1_us - t2_us; return calc_s_curve_speed_q15(t_prime, j_q15, t1_us, 0); } }

注意:t1_us建议取值范围2000~5000μs(2~5ms),过小则S形不明显,过大则启动缓慢;j_q15初始可设为Q15(0.5),即加加速度0.5 rad/s³,后续根据电机啸叫程度调整。

3. 在STM32F103标准外设库v3.50下配置双定时器:从RCC使能到中断优先级实战

3.1 硬件资源分配与引脚映射:为什么必须用TIM2_CH1+TIM3_UP组合

F103系列定时器资源有限,需规避已占用外设:

  • TIM1通常被高级控制占用(如电机FOC);
  • TIM4常被SysTick或FreeRTOS抢占;
  • TIM5无输入捕获功能,无法接收TIM3 UEV信号。

故选定:

  • TIM2:使用APB1总线,时钟72MHz,CH1(PA0)输出脉冲,CH2(PA1)输出方向电平;
  • TIM3:同属APB1,时钟72MHz,仅启用更新中断(UEV),不接任何GPIO。

关键配置顺序(按标准外设库v3.50):

  1. RCC使能:RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3, ENABLE);
  2. GPIO初始化:GPIO_PinAFConfig(GPIOA, GPIO_PinSource0, GPIO_AF_TIM2);(PA0复用为TIM2_CH1)
  3. TIM2基础配置:
TIM_TimeBaseInitTypeDef TIM2_TimeBaseStructure; TIM2_TimeBaseStructure.TIM_Period = 1000; // 初始ARR=1000,对应1kHz脉冲 TIM2_TimeBaseStructure.TIM_Prescaler = 71; // PSC=71 → 1MHz计数频率 TIM2_TimeBaseStructure.TIM_ClockDivision = 0; TIM2_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM2_TimeBaseStructure); // PWM输出配置(CH1脉冲,CH2方向) TIM_OCInitTypeDef TIM2_OCInitStructure; TIM2_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM2_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM2_OCInitStructure.TIM_Pulse = 500; // 初始占空比50% TIM2_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &TIM2_OCInitStructure); TIM_OC2Init(TIM2, &TIM2_OCInitStructure); // CH2同理,但脉冲宽度=0(低电平有效方向)

3.2 TIM3更新中断服务程序:S曲线计算与TIM2_ARR动态写入

TIM3中断服务函数是整个加减速引擎的核心,必须满足:

  • 执行时间稳定(实测<45μs);
  • 无浮点运算;
  • 严格保护共享变量(如当前步数、目标步数);
volatile uint32_t g_current_step = 0; volatile uint32_t g_target_step = 0; volatile uint16_t g_tim2_arr_next = 1000; // 下一个TIM2->ARR值 void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 1. 计算当前速度对应脉冲周期T = 1000000 / v (单位:μs) int16_t v_q15 = calc_s_curve_speed_q15(g_current_step, J_Q15, T1_US, T2_US); uint32_t v_real = (uint32_t)(v_q15 * 1000.0f / 32768.0f); // 转回整数Hz if (v_real == 0) v_real = 1; // 防除零 uint16_t T_us = 1000000 / v_real; // 当前脉冲周期(μs) // 2. 写入TIM2->ARR,触发硬件重载 if (T_us > 100 && T_us < 65535) { // 限定安全范围 g_tim2_arr_next = (uint16_t)T_us; TIM_SetAutoreload(TIM2, g_tim2_arr_next); } // 3. 步数递增(注意:此处g_current_step为计时步数,非电机物理步数) g_current_step++; // 4. 到达目标步数则关闭TIM3中断 if (g_current_step >= g_target_step) { TIM_ITConfig(TIM3, TIM_IT_Update, DISABLE); TIM_Cmd(TIM2, DISABLE); // 停止脉冲 } } }

提示:g_current_step在此处是时间离散化步数,每进入一次TIM3中断代表经过1个时间单位(由TIM3 ARR决定),而非电机转动一步。若需精确控制物理步数,需在TIM2的更新中断中另设计数器。

3.3 中断优先级配置:确保TIM2脉冲不被TIM3计算打断

F103中断嵌套规则:数值越小,优先级越高。必须保证TIM2的更新中断(UEV)能打断TIM3的UEV中断:

NVIC_InitTypeDef NVIC_InitStructure; // 先配置TIM2:最高优先级 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级0 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 再配置TIM3:次高优先级 NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_Init(&NVIC_InitStructure);

此配置下,当TIM2正在输出脉冲边沿时,TIM3的S曲线计算中断会被挂起,待TIM2 UEV处理完后再执行,彻底消除脉冲抖动。

4. 参数整定与失步诊断:用示波器抓3个关键波形定位真实瓶颈

4.1 必测三路信号及合格判据(附实测截图描述)

仅靠肉眼观察电机是否抖动远远不够。必须用示波器同时捕获:

信号位置探头连接合格判据失效表现
PA0(TIM2_CH1)探头接地夹接GND,尖端接PA0脉冲边沿陡峭(上升/下降时间<100ns),周期T随时间平滑变化,无跳变或平台出现周期突变(如T从1000μs跳至500μs),说明ARR写入异常
PA1(TIM2_CH2)同上方向电平在运动开始前稳定建立,切换时无毛刺,且与PA0脉冲严格同步(方向变前至少20μs)方向电平在脉冲边沿附近翻转,导致驱动芯片误判
驱动板ENABLE引脚接DRV8825的EN引脚使能信号在TIM2启动前100μs拉低,停止后100μs拉高,全程无振荡ENABLE信号有高频振荡(>100kHz),说明电源退耦不足

注意:测试时务必断开电机绕组,仅测控制信号。若PA0波形合格但电机仍失步,则问题必在驱动板电流设置或电机相序。

4.2 惯量匹配调试法:从“电机啸叫”到“静音加速”的3步参数修正

S曲线失步常表现为高频啸叫(>5kHz),本质是加加速度j过大,导致电机转子无法跟随电磁场变化。修正步骤:

  1. 固定t1=3000μs,将j_q15从Q15(0.5)逐步降至Q15(0.1):每降一档,运行相同距离,听啸叫是否减弱。若降至0.1仍啸叫,说明t1过小,进入第2步;
  2. 将t1从3000μs增至5000μs,j_q15恢复Q15(0.3):延长加加速时间,降低瞬时jerk冲击;
  3. 实测负载惯量J_L:用已知扭矩T的弹簧秤拉动电机轴,测角加速度α,按J_L = T/α计算。将J_L代入公式j_max = T_max / J_L(T_max为电机保持转矩),反推理论j上限。

例如:42步进电机保持转矩0.4N·m,实测J_L=2.5e-5 kg·m²,则j_max ≈ 0.4 / 2.5e-5 = 16000 rad/s³,对应Q15值为Q15(16000) = 16000*32768 = 524288000,远超16位范围——说明必须用t1延长法,而非硬提j。

4.3 常见失步场景与代码级修复对照表

现象根本原因代码修复位置验证方法
启动瞬间丢步方向电平未在第一个脉冲前建立检查TIM2_OC2Init()TIM_Pulse是否设为0,且在TIM_Cmd(TIM2, ENABLE)前执行GPIO_ResetBits(GPIOA, GPIO_Pin_1)示波器看PA1是否在PA0第一个上升沿前≥20μs稳定为低
中速段规律性抖动TIM2 ARR更新时机与计数器重载不同步TIM3_IRQHandler中,将TIM_SetAutoreload()改为TIM_SetCompare1()并启用影子寄存器:TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable)抓PA0波形,看周期变化是否发生在同一计数值点
减速段堵转减速段jerk-down斜率过缓,导致反电动势过高将减加速段时间t1'设为t1的0.7倍(非对称S型),并在calc_s_curve_speed_q15()中单独实现修改t1_us传参,观察减速段电机是否仍有余力

5. 进阶技巧:用TIM2编码器接口实现闭环位置校验,不增加硬件成本

5.1 复用TIM2通道为编码器输入:将PA0/PA1从输出切换为输入

F103的TIM2支持编码器接口模式,可直接将原脉冲/方向引脚复用为正交解码输入。无需额外IO或芯片:

// 关闭TIM2输出功能 TIM_Cmd(TIM2, DISABLE); TIM_DeInit(TIM2); // 重配置为编码器模式 TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM2, 0); // 清零计数器 TIM_Cmd(TIM2, ENABLE);

此时PA0(TI1)和PA1(TI2)自动切换为编码器A/B相信号输入,TIM2_CNT寄存器实时累加正交脉冲数。关键优势:

  • 解码完全硬件化,CPU零开销;
  • 计数范围0~65535(16位),覆盖绝大多数行程;
  • 可与前述S曲线驱动并存:启动时用S曲线开环驱动,运行中用TIM2_CNT校验实际位置。

5.2 位置偏差实时补偿:在TIM3中断中注入微调脉冲

当检测到|g_current_step - TIM2->CNT| > 3(3个脉冲误差)时,不重启整个S曲线,而是在下次TIM2更新中断中插入补偿:

// 在TIM2_IRQHandler中(需使能TIM2更新中断) void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); int16_t err = g_current_step - TIM2->CNT; if (abs(err) > 3) { // 插入1个补偿脉冲:强制TIM2再计数1次 TIM_SetCounter(TIM2, TIM2->CNT + 1); } } }

此法避免了重新计算S曲线的耗时,且因补偿量小(≤3脉冲),不会破坏S形平滑性。实测在1000mm行程中,位置累积误差从开环的±12步降至±1步以内。

5.3 用串口打印关键状态:一行代码开启调试之门

main()中添加:

printf("S-Curve: t1=%dus, j=%.2f, v_max=%dHz\r\n", T1_US, (float)J_Q15/32768.0f, V_MAX_HZ);

配合USB转TTL模块,用XCOM等串口工具实时查看参数。当发现v_max远低于预期时,立即检查:

  • 是否TIM2时钟未使能(RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE)遗漏);
  • 是否TIM_SetAutoreload()写入值超出65535(需加if(T_us<65535)保护)。

提示:printf重定向到串口会占用约1.2KB Flash,但换来的是参数可量化、问题可复现——这对运动控制项目而言,是性价比最高的调试投入。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询