简介:本资源是一套基于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型)或五段(非对称),工程中常用三段式简化模型:
- 加加速段(Jerk-up):加速度a从0线性增至a_max,持续时间t1;
- 匀加速度段(Constant-accel):a=a_max恒定,持续时间t2;
- 减加速段(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):
- RCC使能:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3, ENABLE); - GPIO初始化:
GPIO_PinAFConfig(GPIOA, GPIO_PinSource0, GPIO_AF_TIM2);(PA0复用为TIM2_CH1) - 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过大,导致电机转子无法跟随电磁场变化。修正步骤:
- 固定t1=3000μs,将j_q15从Q15(0.5)逐步降至Q15(0.1):每降一档,运行相同距离,听啸叫是否减弱。若降至0.1仍啸叫,说明t1过小,进入第2步;
- 将t1从3000μs增至5000μs,j_q15恢复Q15(0.3):延长加加速时间,降低瞬时jerk冲击;
- 实测负载惯量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,但换来的是参数可量化、问题可复现——这对运动控制项目而言,是性价比最高的调试投入。
本文还有配套的精品资源,点击获取