简介:面向STM32F4系列单片机开发者的步进电机驱动工程包,特别适用于实现步进电机的梯形加减速运动与精确定位控制。包内基于STM32F407,集成四相八拍驱动、定时器PWM脉冲序列生成、加减速算法及PID速度优化思路,可在Keil或STM32CubeIDE中直接修改外设配置并移植到F4系列其他型号。压缩包共242个文件,包含124个h头文件和110个c源文件,另含uvprojx工程文件、uvoptx选项配置、hex固件及启动文件等,整体仅1.64MB,以STM32 HAL库驱动和应用代码为主,便于对照工程理解GPIO与定时器设置。目前已有474人学习下载。文件结构突出stepper_motor_driver模块与中断服务例程,配合HAL库参考代码,能帮助嵌入式开发者快速掌握步进电机梯形加减速的参数调整方法和实际调试技巧。
1. 梯形加减速为什么比直接发脉冲更适合 STM32F407 步进控制
直接给步进电机发固定频率脉冲很稳,但只要频率稍高,电机就会在启动瞬间丢步,停止瞬间还会冲过头。STM32F407 的主频和定时器资源完全够用,难的不是产生脉冲,而是“什么时候把脉冲频率调上去,什么时候调下来”。梯形加减速把速度曲线拆成加速、匀速、减速三段,让脉冲频率逐步变化,驱动力矩才有余量。这篇文章按我实际调试 F407 时会用的思路讲:先算梯形参数,再用定时器 PWM 实现,最后给驱动板和示波器验证的完整流程。适合用标准库或 HAL 库写运动控制的开发者,也适合刚把电机驱动板接好但不清楚加速度怎么设的人。
2. 梯形参数计算:加速步数、ARR 周期与 STM32F407 定时器配置
梯形曲线不是一个固定的速度值,而是一组约束关系。只有把总步数、最高速度和加速度三者换算成加减速步数,才能在定时器中断里准确切换阶段。
2.1 梯形速度剖面的三个关键参数
第一个参数是启动速度v_start,一般取一个较低值,比如 100 step/s。直接给零速定时器周期是无穷大,没法写进 ARR,所以实际工程里不会从 0 开始加速。第二个参数是最大速度v_max,也就是匀速阶段的速度。第三个参数是加速度a,单位是step/s^2,它决定电机多长时间能冲到最高速。
从速度曲线上看,加速阶段速度从v_start线性爬到v_max,匀速阶段保持v_max,减速阶段再线性回落到v_start。整个曲线在速度时间图上就是一个梯形;如果总步数太少,匀速阶段被压缩为 0,曲线退化成三角形。
在计算加速步数时,不能用速度变化量直接除以步数,而要按公式折算位移。加速度为a时,从v_start加速到v_max需要走过的步数约等于:
sa = (v_max^2 - v_start^2) / (2 * a)我把这个公式放进规划函数里,输入需要的约束参数,直接得到三段步数。
2.2 计算加减速步数的 C 函数
下面是一个很常见的梯形规划函数,我习惯用一个结构体保存运动参数。用浮点计算是为了在处理小数步数时不容易累积误差。
typedef struct { float v_start; // 启动速度,step/s float v_max; // 最大速度,step/s float accel; // 加速度,step/s^2 uint32_t total_steps; uint32_t accel_steps; uint32_t const_steps; uint32_t decel_steps; } trapezoid_t; void calc_trapezoid(trapezoid_t *t) { float a = t->accel; float v0 = t->v_start; float v1 = t->v_max; uint32_t sa = (uint32_t)((v1 * v1 - v0 * v0) / (2.0f * a)); uint32_t sd = sa; // 正常梯形:足以完成加速和减速 if (sa + sd <= t->total_steps) { t->accel_steps = sa; t->decel_steps = sd; t->const_steps = t->total_steps - sa - sd; return; } // 三角曲线:加速步数太多,先算实际能达到的峰值速度 float vp = sqrtf(v0 * v0 + a * (float)t->total_steps); sa = (uint32_t)((vp * vp - v0 * v0) / (2.0f * a)); if (sa > t->total_steps) { sa = t->total_steps / 2; } t->accel_steps = sa; t->decel_steps = t->total_steps - sa; t->const_steps = 0; t->v_max = vp; }这段代码做了两件事:先按理想梯形分配三段步数,如果加速段和减速段之和超过总步数,就重新计算一个更低的峰值速度vp,并把匀速段清零。实际使用中,如果const_steps为 0,说明运动距离短,电机只会经历加速和减速,不会冲到设定最高速。此时把v_max覆写为vp,后续中断里用到的速度都从规划结果里取,避免速度跳变。
2.3 把速度转换成 F407 定时器 ARR 周期
STM32F407 的定时器时钟来自 APB1。系统主频 168MHz 时,APB1 定时器时钟通常被配置为 84MHz。为了让速度到 ARR 的换算简单,我会把预分频器设为 84 减 1,让定时器计数频率变成 1MHz。这样每个脉冲间隔的微秒数就直接等于 ARR 值,不再需要额外换算。
uint32_t speed_to_arr(float speed) { // 定时器计数频率为 1MHz return (uint32_t)(1000000.0f / speed); }举例来说,电机速度为 1000 step/s,每个脉冲间隔是 1ms,ARR 就是 1000。如果速度是 5000 step/s,ARR 就是 200。TIM2 是 32 位定时器,ARR 最大可以到 2^32 减 1,所以低速度下也不怕溢出。
同样的逻辑也要用在占空比上。步进驱动器识别的是脉冲边沿,不一定要求严格 50% 占空比,但固定半周期更方便观察频率。每次更新 ARR 后,把比较寄存器 CCR 设为 ARR 的一半即可。下面这张表是我常用的初调参数范围,细分越高,最大脉冲频率可以给得越高。
| 参数 | 典型取值 | 说明 |
|---|---|---|
| v_start | 50~300 step/s | 启动速度不能太高,否则启动即丢步 |
| v_max | 1000~5000 step/s | 取决于细分和电机,见第 4 章 |
| accel | 3000~15000 step/s² | 空载可取上限,带载降到一半以下 |
| 定时器计数频率 | 1MHz | 预分频 84-1,ARR 直接等于微秒数 |
3. STM32F407 HAL 库实现梯形加减速的状态机
有了规划函数,下一步就是让定时器按规划结果输出频率不断变化的 PWM 脉冲。我采用的做法是:TIM2 PWM 输出 STEP 脉冲,更新中断里修改 ARR。每产生一个脉冲,中断就根据当前阶段计算下一个脉冲间隔。
3.1 初始化 GPIO 与 TIM2 PWM
STEP 引脚用 PA0,也就是 TIM2_CH1 的复用输出;DIR 和 EN 用 PB1、PB2 作为普通 GPIO。这是最简单的接线方式,适合先用杜邦线验证。
static TIM_HandleTypeDef htim2; void Step_GPIO_Init(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; // PA0 复用为 TIM2_CH1,输出 STEP 脉冲 GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLDOWN; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF1_TIM2; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // PB1 DIR,PB2 EN,普通推挽输出 GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_2; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); // EN 低电平使能 }PA0 的复用功能号是GPIO_AF1_TIM2,不同引脚差别很大,比如 PA1 也是 TIM2_CH2,但 AF 编号相同。在 CubeMX 里双击引脚确认复用编号是最稳妥的方式。
定时器初始化里,预分频设为 84 减 1,初始 ARR 随便设一个值,启动运动后会立刻被梯形规划覆盖。
void Step_PWM_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance = TIM2; htim2.Init.Prescaler = 84 - 1; // 84MHz / 84 = 1MHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 1000; // 初始 1kHz,后续动态修改 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim2); TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; // 占空比 50% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); }注意HAL_TIM_PWM_Start_IT打开的不只是 PWM 输出,还包括更新中断。启动前先调用__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE)也可以做到同样效果,但使用HAL_TIM_PWM_Start_IT更省事。
3.2 梯形运动启动函数
启动函数需要完成三件事:设置方向、计算梯形步数、把 ARR 设为启动速度对应的周期,然后启动 PWM。下面的代码里用一个全局trapezoid_t保存规划结果,供中断回调使用。
trapezoid_t trap; volatile uint32_t pulse_index; volatile uint32_t total_steps; volatile uint32_t accel_steps; volatile uint32_t const_steps; volatile uint32_t decel_steps; volatile float v0; volatile float vmax; void Motor_Move(uint32_t steps, float v_max, float accel, uint8_t dir) { if (steps == 0) return; // 设置方向与使能 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, dir ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); // 规划梯形曲线 trap.total_steps = steps; trap.v_start = 100.0f; trap.v_max = v_max; trap.accel = accel; calc_trapezoid(&trap); pulse_index = 0; total_steps = trap.total_steps; accel_steps = trap.accel_steps; const_steps = trap.const_steps; decel_steps = trap.decel_steps; v0 = trap.v_start; vmax = trap.v_max; // 写入第一个脉冲周期 uint32_t first_arr = speed_to_arr(v0); if (first_arr < 2) first_arr = 2; __HAL_TIM_SET_AUTORELOAD(&htim2, first_arr); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, first_arr >> 1); HAL_TIM_PWM_Start_IT(&htim2, TIM_CHANNEL_1); }这里的v_start固定写成 100,也可以改成函数参数。启动速度主要是避开零速除零,同时给驱动器一个可靠的起始脉冲宽度。若电机带负载启动困难,把v_start降到 50,甚至改成v_start = v_start * 0.7f再启动。
3.3 在脉冲完成回调里更新下一拍周期
梯形控制的实时更新发生在 PWM 更新中断中。HAL 库的 PWM 更新中断会进入HAL_TIM_PWM_PulseFinishedCallback,我在这个回调里递增脉冲计数,并根据当前所在阶段更新 ARR。
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance != TIM2) return; pulse_index++; if (pulse_index >= total_steps) { HAL_TIM_PWM_Stop_IT(&htim2, TIM_CHANNEL_1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); // 失能驱动器 return; } float speed; if (pulse_index < accel_steps) { // 加速阶段:速度线性上升 speed = v0 + (vmax - v0) * ((float)pulse_index / (float)accel_steps); } else if (pulse_index < accel_steps + const_steps) { // 匀速阶段:维持最大速度 speed = vmax; } else { // 减速阶段:速度线性下降 uint32_t idx = pulse_index - accel_steps - const_steps; float ratio = (float)(idx + 1) / (float)decel_steps; speed = vmax - (vmax - v0) * ratio; } if (speed < v0) speed = v0; uint32_t arr = speed_to_arr(speed); if (arr < 2) arr = 2; // 防止周期过小,超出定时器最小分频能力 __HAL_TIM_SET_AUTORELOAD(&htim2, arr); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, arr >> 1); }整个状态机是隐式的,用一个pulse_index和三个步数阈值判断当前阶段。加速阶段结束时自动进入匀速,匀速结束时自动进入减速,不需要额外的 state 变量。
这个实现有几个需要注意的点。第一,速度是线性增加,但 ARR 是速度倒数,所以更严格的梯形计算里 ARR 不是线性变化;把速度按比例缩放已经是常见且够用的做法,追求更高精度时可以引入 S 曲线,详见第 5 章的验证思路。第二,更新中断频率就是脉冲频率,速度越高中断越频繁。F407 主频高,跑到 10kHz 脉冲还能接受,但中断内的浮点除法会带来抖动,后续可以用 DMA 把 ARR 表直接写入定时器,让 CPU 从中断里解脱。
4. 步进电机驱动板接线与 F407 参数调优
梯形曲线算得再准,驱动板接线错误或细分设置不当也会导致失步。这里以最常见的 A4988 和 DRV8825 为例,讲清接线和参数调整。
4.1 A4988 / DRV8825 细分设置与 STEP、DIR 接线
A4988 正面有三组细分跳线 MS1、MS2、MS3,短接为 1,断开为 0。下面只列出最常用的几个档位:
| 细分 | MS1 | MS2 | MS3 | 200 步电机每圈脉冲 |
|---|---|---|---|---|
| 全步 | 0 | 0 | 0 | 200 |
| 1/4 | 0 | 1 | 0 | 800 |
| 1/8 | 1 | 1 | 0 | 1600 |
| 1/16 | 1 | 1 | 1 | 3200 |
DRV8825 的跳线脚叫 MODE0/MODE1/MODE2,逻辑类似,但 1/8 细分是 100,1/16 是 010,不要照搬 A4988 的表格。接线时,STM32 的 PA0 STEP 接驱动板 STEP,PB1 接 DIR,PB2 接 EN。EN 一般低电平有效,不要接反。
很多 42 步进驱动板支持 3.3V 逻辑电平,F407 的 GPIO 可以直接驱动。如果驱动器要求 5V 逻辑,就在 STEP 和 DIR 线上加 1k 电阻串联或光耦隔离。三轴方案里常见做法是给每轴分配一个定时器通道,比如 TIM2、TIM3、TIM9 分别控制 X/Y/Z,中断合并到一个回调里处理,规则和单轴完全一样。
4.2 加速度和最大脉冲频率先按经验值
理论加速度可以通过“最大失步力矩”换算,但实际工程里我一般是空载实测调出来的。下面这组数据适合 42 步进电机在 24V 供电下使用:
| 细分 | v_max 经验值 | accel 经验值 | 适用场景 |
|---|---|---|---|
| 1/8 | 800 ~ 2000 | 5000 ~ 12000 | 轻载调试 |
| 1/16 | 1600 ~ 5000 | 8000 ~ 20000 | 常用掉电自锁不强的场景 |
| 1/16 | 超过 6000 | 视负载调低 | 需要高速但允许加速稍慢 |
电机在加速阶段丢步,表现为到达位置后误差固定;在减速阶段丢步,则表现为“过冲”或“刹车不住”。把accel调小一倍再测,如果故障消失,说明问题是加速度过大而不是最大速度过大。
4.3 三个容易导致失步的 F407 参数
第一个是占空比。有人会把 CCR 设置成 ARR/2,但更新ARR后忘了同步CCR,导致半周期不对称。第二个是脉冲最小宽度。A4988 要求 STEP 高电平至少 1μs,如果计数频率太高而 ARR 太低,单脉冲宽度会被压到几百 ns,驱动器识别不到。我使用 1MHz 计数频率,速度到 10k 时 ARR 为 100,半周期 50μs,余量很足。第三个是方向切换时序。F407 控制 DIR 后,至少等一个 STEP 脉冲周期再开始发脉冲;方向还没稳定就发脉冲,第一拍方向可能被驱动器忽略。
5. 用 F407 输入捕获回看梯形曲线并加动态减速保护
梯形运动调好后,最大的问题不是速度不够,而是“我以为它是梯形,实际是不是梯形”。F407 自带 DWT 周期计数器,不需要额外连线,就能粗略测量 STEP 脉冲间隔,从而还原速度曲线。
5.1 借 DWT 计数器测 STEP 间隔
启用 DWT 很简单,在main里调用下面的初始化函数,然后让PulseFinishedCallback在每次进入时记录一次时间戳。
void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; DWT->CYCCNT = 0; } volatile uint32_t last_cycle; volatile float measured_speed[256]; volatile uint8_t measured_count; void Speed_Record(void) { uint32_t now = DWT->CYCCNT; uint32_t diff = now - last_cycle; // CYCCNT 溢出后按 32 位差值处理 last_cycle = now; float interval_s = (float)diff / 168000000.0f; if (measured_count < 256) { measured_speed[measured_count++] = 1.0f / interval_s; } }然后在HAL_TIM_PWM_PulseFinishedCallback第一行调用Speed_Record(),运动结束后用串口把measured_speed数组打印出来。理想情况是前半段先逐渐升高,中间平稳,后半段逐渐降低。如果加速段出现平台或突然凹下去,说明中断被优先级更高的代码抢占,导致 ARR 更新不及时。注意这里测量的时间差包含中断入口和退出的开销,但频率不高时误差可以忽略。
5.2 动态减速距离保护
即使提前计算了减速步数,实际加速度受负载影响,可能和设定值不一致。更稳妥的做法是在匀速阶段不断检查剩余距离,当剩余步数小于当前速度对应的刹车距离时,立刻切入减速。
// 在 PulseFinishedCallback 的匀速分支里加入判断 if (pulse_index >= accel_steps && pulse_index < accel_steps + const_steps) { float brake_steps = (vmax * vmax - v0 * v0) / (2.0f * trap.accel); uint32_t remaining = total_steps - pulse_index; if (remaining <= (uint32_t)(brake_steps + 2)) { // 截断匀速段,立即进入减速 const_steps = pulse_index - accel_steps; } }这个技巧让梯形曲线从“静态规划”变成了“动态修正”,即使负载导致实际加速度小于设定值,也能保证没有提前冲过目标位置。代价是减速阶段的起点会比理论值稍早,但不会影响最终停止精度。
调完这一项后,STEP 波形在示波器上就是一条先密后疏再变密的脉冲串,加速段和减速段完全对称。若不对称,优先检查驱动板细分设置是否和代码里的v_max匹配。
本文还有配套的精品资源,点击获取