简介:一套基于STM32实现步进电机加减速控制的完整工程源码,面向嵌入式开发者和自动化设备设计人员,可帮助快速掌握脉冲生成、定时器/PWM配置及S型加减速策略等关键环节。压缩包共103个文件,以C源文件(28个)和头文件(27个)为核心,涵盖STM32标准外设库常用模块,另含Keil工程配置、链接脚本、编译脚本及调试辅助文件,整体大小仅1.36MB,结构紧凑适合直接导入学习。内容聚焦四相步进电机的相位驱动与脉冲时序控制,包括全步、半步、微步模式及加减速曲线实现方法,代码层次清晰,便于移植到不同型号的STM32平台。已有241人学习下载,适合正在做电机控制项目或入门STM32底层驱动开发的读者参考借鉴。
1. STM32步进电机加减速程序:先解决丢步,再谈转速
发一个恒定频率的脉冲串给步进电机驱动芯片,电压一拉高就丢步、急停就过冲,这不是芯片不行,而是脉冲频率没有做加减速规划。所谓加减速程序,本质是把目标转速翻译成一条可行的脉冲频率曲线,让电机从静止经过加速段到达最大速度,最后减速停下,整个过程转子始终跟得上定子磁场。这套程序在 3D 打印机、小型 CNC、云台和毕业设计里的出镜率极高,也是 STM32 控制步进电机最常见的实战场景。目标读者是接过步进电机却对速度规划没底的人,读完能把定时器、驱动芯片和速度曲线串成一套能跑的代码。
2. 步进电机驱动硬件选型与 STM32 引脚接线
标题里的 Driver 有两层意思:一层是物理上的驱动芯片,另一层是固件里负责输出脉冲的那段代码。先把前者定下来,后者才有讨论的意义。STM32 的 GPIO 本身驱动能力有限,直接推挽输出给电机绕组既不现实也伤引脚,因此驱动芯片是必经之路。选型、接线、细分设置三步走完,加减速程序才有硬件基础。
2.1 A4988、DRV8825 和 TB6600 该选谁
步进电机驱动芯片的选择直接决定接线方式和电流上限。A4988 适合 28/35 这样的小电机,额定电流约 1A,细分最高 1/16;DRV8825 电流做到 2.5A 左右,细分支持 1/32,驱动 42 步进电机是主力;TB6600 电流 4A 以上,适合 57 及更大的电机,但模块体积大,控制信号往往需要光耦隔离。桌面级项目和 3D 打印机里,DRV8825 配 42 电机是最高频的组合。
| 芯片 | 最大电流 | 最高细分 | 适用电机 | 控制信号电平 |
|---|---|---|---|---|
| A4988 | 2A | 1/16 | 28/35 | 3.3V/5V |
| DRV8825 | 2.5A | 1/32 | 42/57 | 3.3V/5V |
| TB6600 | 4.5A | 1/32 | 57/86 | 5V-24V 光耦输入 |
选型要留意:最大电流是芯片能力,不代表直接给满。42 步进电机的额定电流通常在 1.2-1.7A,DRV8825 的峰值电流约等于 VREF 电压乘 2,把 VREF 调到 0.6-0.8V 就够用,调太大电机会发烫且容易丢步。A4988 的电流换算公式不同,是 VREF 除以 8 倍采样电阻再乘系数,所以换芯片时必须重算电流,不能照搬电阻位置。
2.2 DRV8825 与 STM32 的最小接线
以 STM32F103C8T6 为例,最少占用 3 个 GPIO:STEP、DIR、ENABLE。STEP 引脚每个上升沿走一步,DIR 高低决定方向,ENABLE 低电平有效使能输出。把 DRV8825 的 VDD 接 3.3V,GND 与 STM32 共地,STEP 接 PA0,DIR 接 PA1,ENABLE 接 PA2,MODE0-2 按细分表接。
STEP <- PA0 DIR <- PA1 ENABLE <- PA2 MODE0/1/2 -> PB0/PB1/PB2 VMOT -> 12V 电源正极 GND -> 电源地,与 STM32 共地 A1/A2 -> 电机绕组 A(通常绿/黑,以丝印为准) B1/B2 -> 电机绕组 B(通常红/蓝) VREF -> 电位器中脚,用于设定电流A1/A2 和 B1/B2 两组绕组如果接反,电机会反转;同组两线接反,电机会抖动不转。线序判断有个土办法:把 A 相和 B 相分别开路,手动转电机轴,感觉有磁阻力变化的两个引脚基本就是同一绕组。
接线里最容易漏的是 42 步进电机驱动拓展板上的电源去耦电容。VMOT 引脚附近要放一个 100uF 电解电容,放在驱动芯片和电机之间。步进电机是感性负载,换向瞬间会产生反电动势尖峰,不加电容轻则出现莫名其妙的复位,重则击穿驱动芯片。
2.3 GPIO 初始化与细分配置
用 HAL 库写 GPIO 初始化,PA0-PA2 配置为推挽输出,PB0-PB2 同理。DIR 和 ENABLE 只需要高低电平,不需要 PWM,普通推挽足够。
#define STEP_PIN GPIO_PIN_0 #define DIR_PIN GPIO_PIN_1 #define ENABLE_PIN GPIO_PIN_2 #define MODE_PINS (GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2) void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // 上电先把电机输出失能,等系统就绪再拉低使能 HAL_GPIO_WritePin(GPIOA, ENABLE_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, MODE_PINS, GPIO_PIN_RESET); GPIO_InitStruct.Pin = STEP_PIN | DIR_PIN | ENABLE_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = MODE_PINS; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); }初始把 ENABLE 拉高失能,等系统上电稳定后再拉低使能,可以避免上电瞬间的误动作。MODE 全部拉低为全步进,这个配置调通之前最稳妥;细分调到 1/16 或 1/32 后,同样的脉冲频率下转速降为步距角除以细分值,低速振动小很多,加减速曲线也有更高的分辨率。细分设置的核心作用不是提高精度,而是让每一步走得更小、低速更平滑,速度规划的空间更大。
3. 加减速曲线原理:梯形、指数与 S 形怎么选
3.1 为什么不能开场就满速
步进电机的最大启动频率远低于最大运行频率。42 电机在 24V 供电下,直接启动一般只能到几百赫兹到 1kHz,但加速后能跑到几 kHz。原因是转子从静止到同步需要建立过程,脉冲频率一旦高于转子能追随的最大拉入频率,定子磁场已经换了位置,转子还停在原地,这就是丢步。丢步不报错、不反馈,位置误差直接累积。
减速也一样。如果从高速瞬间停发脉冲,转子惯性会冲过头,停在错误的平衡点或者开始振荡。加减速程序做的就是在加速段让脉冲频率从一个安全值平滑升到目标值,减速段再对称降回来,把每一步的误差控制在一个脉冲当量内。
3.2 梯形加减速的状态切换
梯形曲线是最容易实现的速度规划,把整个运动过程分成加速、匀速、减速三段。设起始频率 f_start、目标频率 f_target、加减速步数分别为 accel_steps 和 decel_steps、总步数 total_steps,则匀速段步数为 total_steps - accel_steps - decel_steps。
每产生一个脉冲,更新一次当前频率:加速段按频率增量 delta 增加,到达目标频率后进入匀速;剩余步数小于等于 decel_steps 时进入减速,按同样增量降低频率。delta 由加速度决定,加速度越大到达目标速度越快,但震动和丢步风险越高。
#define F_START 500u /* 起始频率 Hz */ #define F_TARGET 5000u /* 目标频率 Hz */ #define ACCEL_REV 200u /* 加速段脉冲数 */ float delta = (float)(F_TARGET - F_START) / ACCEL_REV;delta 是每步的频率增量。总步数太少时加速都加不满,程序里要做钳位判断:如果 total_steps 小于 accel_steps + decel_steps,就要按比例缩小 accel_steps 和 decel_steps,否则匀速段变成负数,减速永远进不去。
这种三段式规划的优点是计算量小、逻辑直白,在中断里做几次整数加减就行。缺点是拐点处加速度突变,高速运行时机械振动明显,对精度要求高的场景得换 S 形。
3.3 S 形曲线与查表法
S 形曲线让频率变化率连续,从起始频率过渡到目标频率的每个点都平滑,机械冲击小,噪声也低。工程上很少实时算三角函数,而是预先算一张归一化频率表,运行时只是查表和插值。以 512 点为例,先把归一化位置 x = i/512 代入 S 形函数得到 0~1 之间的系数,再映射回频率区间。
#define S_CURVE_POINTS 512u uint16_t s_curve_tbl[S_CURVE_POINTS]; void gen_s_curve(uint16_t f_start, uint16_t f_end) { for (uint16_t i = 0; i < S_CURVE_POINTS; i++) { float x = (float)i / (S_CURVE_POINTS - 1.0f); /* 归一化 S 形:x 在 0~0.5 斜率渐增,0.5~1 斜率渐减 */ float factor = 0.5f * (1.0f - cosf(x * 3.14159265f)); s_curve_tbl[i] = (uint16_t)(f_start + (f_end - f_start) * factor); } }cosf 在桌面编译器上是小事,但在单片机上每次启动算 512 次三角函数要消耗几百毫秒,体验很差。常见做法是 boot 阶段算一次存进 RAM,或者在 PC 端生成一张表烧进 Flash,运行时直接读。如果曲线中点附近有异响,把点数加到 1024 通常是有效的。
查表法里频率随脉冲序号变化,不是随时间变化。两种实现看着差不多,实际差别很大:按时间变化需要累计时间变量,每个中断里做除法;按脉冲序号变化只需要一个下标,中断里下标加一,查表取频率值,计算量小一个量级。
3.4 加减速参数怎么定
起始频率不要取 0。频率为 0 意味着周期无限大,定时器永远到不了更新事件。工程上起始频率取 80-200Hz 最常见。目标频率看应用:3D 打印机一般 3-8kHz,CNC 雕刻金属件 2-5kHz,云台和闭环步进几百 Hz 到 1kHz。加减速步数按负载定:空载 400 步内完成很轻松,带负载建议加到 1000-2000 步。
判断参数是否合适的土办法:让电机跑一段完整行程,手放在电机外壳上感受震动,震动大就降低加速步数或目标频率;能听到明显的哒哒失步声,就提高起始频率或降低加速度。参数调整一次只改一个变量,改完跑同一段轨迹对比,不要同时动两处。
4. 定时器中断驱动的加减速核心代码
4.1 用 TIM3 输出可变频率脉冲
STM32 控制步进电机最标准的做法是用定时器产生脉冲,而不是用 HAL_Delay 或者软延时翻转 GPIO。软延时的致命伤是阻塞:中断一旦被别的任务抢占,脉冲频率就抖动,加减速曲线直接变形。正确思路是开一个通用定时器,每次更新事件后修改 ARR,把频率计算和脉冲输出在时间上错开。
void MX_TIM3_Init(uint16_t arr, uint16_t psc) { __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance = TIM3; htim3.Init.Prescaler = psc; /* 建议 71,72MHz/72=1MHz */ htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = arr; /* 初始 ARR */ htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim3); __HAL_TIM_ENABLE_IT(&htim3, TIM_IT_UPDATE); HAL_NVIC_SetPriority(TIM3_IRQn, 2, 0); HAL_NVIC_EnableIRQ(TIM3_IRQn); }PSC=71 把 72MHz 降为 1MHz,ARR 的数值就是脉冲周期微秒数。ARR=1000 对应 1kHz,ARR=200 对应 5kHz。修改 ARR 就修改了脉冲频率,加减速程序的核心操作就在这一行。这里没有用 PWM 输出通道,而是直接靠更新事件驱动,逻辑更简单,也不涉及比较寄存器同步问题。
4.2 频率递推与脉冲计数
频率递推的逻辑放到中断里。用一个结构体存电机运行状态:
typedef struct { volatile uint32_t speed_hz; /* 当前脉冲频率 */ volatile uint32_t accel_delta; /* 每脉冲频率增量 */ volatile uint32_t total_steps; /* 总步数 */ volatile uint32_t step_index; /* 已走步数 */ volatile uint8_t phase; /* 0=空闲 1=加速 2=匀速 3=减速 */ } step_motor_t;phase 对应加减速曲线的状态。中断里先判断剩余步数,再根据当前 phase 求出新频率:
void update_motor_speed(step_motor_t *m) { switch (m->phase) { case 1: /* 加速 */ m->speed_hz += m->accel_delta; if (m->speed_hz >= TARGET_FREQ_HZ) { m->speed_hz = TARGET_FREQ_HZ; m->phase = 2; } break; case 3: /* 减速 */ if (m->speed_hz <= START_FREQ_HZ) { m->speed_hz = START_FREQ_HZ; m->phase = 0; } else { m->speed_hz -= m->accel_delta; } break; default: break; } /* 新 ARR = 定时器计数频率 / 目标脉冲频率 */ __HAL_TIM_SET_AUTORELOAD(&htim3, TIMER_TICK_HZ / m->speed_hz); }注意 ARR 的修改不是瞬时的,当前周期计数到一半改 ARR,只会影响下一个周期,也就是频率调整最多滞后一个脉冲周期。几百 Hz 起始频率时这个滞后可以忽略,但目标频率到几十 kHz 时,一个周期只有几十微秒,ARR 取整误差会被放大。我一般把定时器计数频率从 1MHz 提到 2MHz,牺牲一点最大周期换更细的频率分辨率。
4.3 完整代码框架
把上面几段串起来,一个可直接编译的最小框架:
volatile uint32_t current_freq; volatile uint32_t remaining_steps; volatile uint8_t run_state; /* 0=空闲 1=加速 2=匀速 3=减速 */ void TIM3_IRQHandler(void) { __HAL_TIM_CLEAR_FLAG(&htim3, TIM_FLAG_UPDATE); if (run_state == 0) { return; } if (remaining_steps == 0) { run_state = 0; HAL_GPIO_WritePin(GPIOA, ENABLE_PIN, GPIO_PIN_SET); return; } remaining_steps--; switch (run_state) { case 1: current_freq += FREQ_STEP; if (current_freq >= TARGET_FREQ) { current_freq = TARGET_FREQ; run_state = 2; } break; case 3: if (current_freq <= START_FREQ) { current_freq = START_FREQ; run_state = 0; } else { current_freq -= FREQ_STEP; } break; default: break; } __HAL_TIM_SET_AUTORELOAD(&htim3, TIMER_TICK_HZ / current_freq); }FREQ_STEP 由 (TARGET_FREQ - START_FREQ) / accel_steps 算出。剩余步数的判断必须在切换状态之前做,否则减速段起始条件会晚一个脉冲,表现在机械上就是过冲半个齿的距离。匀速段进减速的切换逻辑在简化版里没写全,实际项目要把减速段提前量算进去:当 remaining_steps <= decel_steps 且 phase == 2 时,立即切到减速。
提示:中断里尽量只做加减法和查表,不要做浮点除法。浮点运算在 F103 上要几十个周期,会拉长中断执行时间,影响脉冲均勻性。
4.4 细分和电机转速的换算
电机实际转速不是脉冲频率,而是 (脉冲频率 / 细分数) × 步距角 / 360。比如 42 电机 1.8°,1/16 细分下每个脉冲走 0.1125°,5kHz 脉冲频率对应转速约 93.75rpm:
| 目标转速(rpm) | 1/16 细分时 STEP 频率(Hz) | ARR(定时器1MHz) |
|---|---|---|
| 60 | 3200 | 312 |
| 120 | 6400 | 156 |
| 180 | 9600 | 104 |
上表只对 1.8° 步距角的 42 电机成立。换成 0.9° 的电机,同样转速下脉冲频率要乘 2。实际项目把步距角和细分值写死在换算函数里,加减速参数直接以 rpm 为单位,比口算脉冲频率靠谱得多。
5. 调参技巧:从波形到参数的验证方法
5.1 用逻辑分析仪验证加减速曲线
没有示波器时逻辑分析仪足够用。把探头夹在 STEP 引脚,抓一段启动到停止的波形,用软件的频率测量功能看脉冲间隔。理想波形是间隔先逐渐缩短、稳定一段时间、再逐渐变长,变化率连续。如果看到脉冲间隔呈锯齿抖动,说明中断里频率更新和输出不同步,多半是 ARR 修改时机不对,或者浮点运算拖慢了中断。
5.2 三个先调的参数
第一个是起始频率,从 150Hz 起步,每次加 10Hz,直到电机能平稳起步;第二个是加速度,也就是加速段脉冲数,空载可按 1000 rps² 起步,带负载降到 300-500 rps²;第三个是目标频率对应的最大转速,以电机额定转速的 80% 为上限,超上限容易出现抓不住的现象,丢步很难排查。调整顺序不能乱:先定起止频率,再调加速度,任何一步出现丢步就回头降目标频率。
5.3 结合编码器做简易闭环
如果项目定位更高,给电机轴装编码器组成简易闭环。STM32 的定时器编码器接口模式直接读 CNT 寄存器就是位置,加减速规划和 PID 放在一起用:PID 输出的是目标速度增量,加减速曲线负责限幅和滤波。用 HAL 库做这一步不需要额外驱动,配置 TIM 的 Encoder Mode,把 CNT 周期设成编码器最值的两倍以上,否则溢出方向会误判。做到这一步,加减速程序就从开环速度规划变成了带转速反馈的伺服雏形,也是后面做多轴联动的基础。
本文还有配套的精品资源,点击获取