简介:这份资源面向使用STM32单片机驱动28BYJ-48步进电机的初学者与嵌入式开发者,提供一套经过实测可用的控制代码,重点解决步进电机角度调节与正反转控制的问题,适合课程设计、小型机电项目或电机驱动入门练习。压缩包共120个文件,约2.39MB,以31个c源文件与32个h头文件为核心,涵盖定时器、GPIO、时钟、串口等外设驱动,另含Keil工程文件、hex烧录文件、map映射文件及少量说明文本,工程结构完整,可直接编译与下载验证。目前已有4316人学习下载,具备一定的参考基础。读者可从中获得可运行的控制程序框架、角度与方向调节的实现思路,以及基于STM32标准外设库的工程组织方式,便于在此基础上修改参数、移植到同类电机控制场景,减少从零搭建的调试成本。
1. 从一颗“抖一下”的电机说起:STM32 驱动 28BYJ-48 到底能干什么
手头有一块 STM32 最小系统板,一个五线四相的 28BYJ-48 步进电机,外加一块 ULN2003 驱动板,很多人第一次上电就翻车:电机不转、只抖、发热、丢步,甚至把驱动板烧了。这个项目要解决的就是把这三样东西连起来,让电机按你给的步数、方向、速度稳定转起来。它适合正在做课设、做小型自动化装置、做云台或阀门控制的开发者,也适合刚接触 STM32 定时器和 GPIO 的入门者。整套代码不依赖复杂外设,核心就是 GPIO 输出时序加定时器节拍,跑通之后你能清楚看到“脉冲怎么变成角度”这条链路。
2. 硬件链路与工程结构:从引脚到相序的完整映射
2.1 28BYJ-48 与 ULN2003 的电气关系
28BYJ-48 是四相永磁式步进电机,内部四组线圈共用一路电源,常见接法是五线制:一根公共端接 VCC,另外四根分别对应 A、B、C、D 四相。STM32 的 GPIO 输出电流只有毫安级,直接驱动线圈必烧,所以中间必须加 ULN2003 这类达林顿阵列做电流放大。ULN2003 的 IN1~IN4 接 STM32 的四个 GPIO,OUT1~OUT4 接电机四相,COM 端接电机公共端。
这里有个血泪经验:ULN2003 模块上的电源跳线帽一定要确认。很多模块默认把电机电源和逻辑电源短接,如果你用 5V 给电机供电、STM32 用 3.3V,跳线帽不拆就可能把 3.3V 网络倒灌。常见做法是电机侧单独供 5V,逻辑侧由 STM32 板供电,两边只共地。
| 信号 | STM32 引脚(示例) | ULN2003 | 电机线 |
|---|---|---|---|
| A 相 | PA0 | IN1 | 蓝 |
| B 相 | PA1 | IN2 | 粉 |
| C 相 | PA2 | IN3 | 黄 |
| D 相 | PA3 | IN4 | 橙 |
| 公共端 | — | COM | 红 |
引脚可以换,但四根必须落在同一个 GPIO 端口上,后面用位操作一次性输出四相状态会方便很多。
2.2 工程目录与模块划分
一个能二次开发的工程,不建议把所有东西塞进 main.c。我一般按下面这样拆:
step_motor_28byj48/ ├── Core/ │ ├── Inc/ │ │ ├── motor.h // 电机对外接口 │ │ └── main.h │ └── Src/ │ ├── motor.c // 相序表、步进、方向、速度 │ ├── main.c // 初始化与业务调用 │ └── stm32f1xx_it.c // 定时器中断入口 ├── Drivers/ // HAL 库 └── MDK-ARM/ 或 STM32CubeIDE/motor.c只暴露四个函数:初始化、设置速度、走指定步数、停止。业务层不关心相序表长什么样,这样以后换电机型号只改驱动层。定时器选一个基本定时器(如 TIM3),配置成 1ms 中断,在中断里做节拍计数,主循环只负责下发命令,避免在中断里做长逻辑。
3. 核心代码拆解:相序表、定时器节拍与步进函数
3.1 四相八拍相序表
28BYJ-48 常见驱动方式有单四拍、双四拍和八拍。八拍步距角更小、运行更平滑,代价是同样转角需要两倍步数。相序表本质是一个数组,每个元素对应四相 GPIO 的高低电平组合。
/* motor.h */ #ifndef MOTOR_H #define MOTOR_H #include "main.h" void Motor_Init(void); void Motor_SetSpeed(uint16_t step_interval_ms); void Motor_Step(int32_t steps); void Motor_Stop(void); #endif/* motor.c 关键片段 */ #include "motor.h" /* 四相八拍相序:A-AB-B-BC-C-CD-D-DA */ static const uint8_t phase_table[8] = { 0x01, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x09 }; static volatile uint8_t phase_index = 0; static volatile uint16_t step_interval = 2; /* 单位 ms */ static volatile int32_t remain_steps = 0; static volatile int8_t direction = 1; static void Motor_OutputPhase(uint8_t phase) { /* 假设四相接在 PA0~PA3,低四位直接映射 */ GPIOA->ODR = (GPIOA->ODR & 0xFFF0) | (phase & 0x0F); }phase_table里每个字节的低四位就是 A、B、C、D 四相电平,0x03表示 A、B 同时通电。Motor_OutputPhase用读改写的方式只动低四位,不会影响 PA4 以上引脚。这里用寄存器操作而不是 HAL 的HAL_GPIO_WritePin四次,是因为在 1ms 节拍里四次函数调用的开销虽然不大,但寄存器写法更直观,也方便你后面改成整端口输出。
3.2 定时器中断里的步进节拍
步进电机的转速由“每秒多少步”决定,而每一步的持续时间由定时器中断控制。把走步逻辑放在中断里,主循环就不会因为延时函数阻塞。
/* stm32f1xx_it.c 中 TIM3 中断回调 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint16_t tick = 0; if (htim->Instance != TIM3) return; if (remain_steps == 0) { Motor_OutputPhase(0x00); /* 断电,防止发热 */ return; } tick++; if (tick < step_interval) return; tick = 0; phase_index = (phase_index + (direction > 0 ? 1 : 7)) & 0x07; Motor_OutputPhase(phase_table[phase_index]); if (remain_steps > 0) remain_steps--; else if (remain_steps < 0) remain_steps++; }step_interval是每步之间的毫秒数,值越小转得越快。direction为 1 时索引加一,为 -1 时加七再对 8 取模,等价于减一,这样不用处理负数取模。remain_steps用有符号数,正数正转、负数反转,走到 0 自动断电。注意中断里只做计数和输出,不做浮点运算,也不调用HAL_Delay。
3.3 对外接口与步数换算
28BYJ-48 的减速比常见是 1:64,内部转子步距角 5.625°/64,八拍模式下转一圈大约 4096 步。这个数字因批次有差异,实际调试时以实测为准。
void Motor_SetSpeed(uint16_t step_interval_ms) { if (step_interval_ms < 1) step_interval_ms = 1; step_interval = step_interval_ms; } void Motor_Step(int32_t steps) { direction = (steps >= 0) ? 1 : -1; remain_steps = (steps >= 0) ? steps : -steps; } void Motor_Stop(void) { remain_steps = 0; Motor_OutputPhase(0x00); }Motor_Step只设置方向和剩余步数,真正的输出在中断里完成,所以它是非阻塞的。调用Motor_Step(4096)大约转一圈,调用Motor_Step(-2048)反转半圈。如果你要按角度控制,封装一个Motor_StepAngle(float deg),内部用deg / 360.0f * 4096换算成步数即可,但换算结果要四舍五入成整数。
4. 跑起来:初始化顺序、编译与上电验证
4.1 GPIO 与定时器初始化
用 CubeMX 生成工程时,把 PA0~PA3 配成推挽输出、低速即可,初始电平低。TIM3 的预分频和重装载值要算到 1ms:假设系统时钟 72MHz,预分频 71 得到 1MHz 计数频率,重装载 999 就是 1ms 中断。
void Motor_Init(void) { GPIO_InitTypeDef gpio = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); Motor_OutputPhase(0x00); HAL_TIM_Base_Start_IT(&htim3); }初始化顺序很重要:先配 GPIO 并确保输出全低,再启动定时器中断。如果反过来,定时器先跑起来而 GPIO 还没配好,中断里写GPIOA->ODR可能作用在默认状态上,电机可能瞬间抖一下。
4.2 编译与上电检查清单
编译前确认三点:motor.c已加入工程、stm32f1xx_it.c里的回调没有被重复定义、TIM3 中断优先级不要高于系统关键中断。烧录后按下面顺序验证:
- 上电先不接电机,用万用表测 ULN2003 的 IN1~IN4,调用
Motor_Step(8)应能看到四相依次变化。 - 接上电机,调用
Motor_Step(4096),观察是否连续转动而不是原地抖动。 - 把
step_interval从 2 改到 1,听声音是否变尖锐、是否丢步。
提示:第一次上电建议串一个限流电阻或先用小电流电源,确认相序正确再全速跑。
5. 避坑与排查:电机不转、发热、丢步的现场记录
5.1 电机只抖不转
现象是通电后转子左右晃动但不连续旋转。原因通常是相序表顺序错了,或者四根线接错。解决方法是把phase_table改成单四拍0x01,0x02,0x04,0x08先验证每相单独通电能吸住转子,再换回八拍表。另一个常见原因是公共端没接 VCC,四相形不成回路。
5.2 电机发热严重甚至烫手
现象是静止时电机也温热。原因是走完步数后没有断电,某一相长期通电。解决方法是remain_steps到 0 时调用Motor_OutputPhase(0x00),并且不要在Motor_Stop之外的地方让相序停在非零状态。ULN2003 本身也有压降,长时间堵转会热,属于正常范围,但电机烫手就不正常。
5.3 高速时丢步或反转
现象是step_interval调到 1ms 以下时电机啸叫但不转。原因是 28BYJ-48 是减速电机,响应速度有限,节拍太快线圈来不及建立磁场。解决方法是把最小间隔限制在 1~2ms,需要更快转速就换电机或提高供电电压,而不是继续压节拍。另外中断里如果有其他耗时操作,也会导致节拍不均。
5.4 编译通过但电机完全没反应
现象是程序跑起来了,串口有打印,电机不动。先查 TIM3 中断是否真的进入,可以在回调里翻转一个测试引脚用示波器看。再查Motor_Init是否在HAL_Init之后调用,以及HAL_TIM_Base_Start_IT的定时器句柄是否和 CubeMX 生成的一致。还有一种情况是 GPIO 端口时钟没开,写 ODR 无效。
5.5 换板子后引脚映射错乱
现象是同一份代码在另一块板上相序全乱。原因是Motor_OutputPhase里写死了 PA0~PA3,换板后引脚变了。解决方法是把端口和引脚号抽成宏,或者改成用HAL_GPIO_WritePin逐脚输出,牺牲一点效率换可移植性。
6. 二次开发:把步进控制封装成可复用的运动层
跑通单电机之后,真正有价值的是把它变成能复用的模块。我的习惯是把“电机”和“运动”分开:motor.c只管相序和节拍,上层再包一个motion.c管加减速和角度换算。下面是一个简单的梯形加减速思路,不追求复杂曲线,但能明显减少启停丢步。
/* motion.c 片段:按剩余步数动态调整节拍 */ void Motion_MoveSteps(int32_t steps, uint16_t v_start, uint16_t v_max) { int32_t abs_steps = (steps >= 0) ? steps : -steps; uint16_t interval = v_start; Motor_Step(steps); while (abs_steps > 0) { /* 前 1/4 加速,后 1/4 减速,中间匀速 */ if (abs_steps > (steps * 3 / 4)) { if (interval > v_max) interval--; } else if (abs_steps < (steps / 4)) { if (interval < v_start) interval++; } Motor_SetSpeed(interval); HAL_Delay(5); abs_steps -= 8; /* 粗略估算,实际按中断计数 */ } }这段代码是示意性的,真实项目里应该用中断里的步数计数做状态机,而不是在主循环里HAL_Delay轮询。改造时的验证方法是:先用示波器看一路 GPIO 的脉冲间隔是否按预期变化,再听电机声音是否由低沉平滑过渡到高频,最后用标记点测实际转角是否和理论步数一致。
另一个实用改造是加限位开关。把限位接成外部中断,触发时调用Motor_Stop,这样回零动作就不会撞坏机构。还有一点,如果你要做多电机,别共用一个定时器中断里的全局phase_index,每个电机一套状态结构体,定时器只负责发节拍标志。
从那以后我每次接新电机,都强制先用单四拍逐相验证接线,再上八拍跑连续转动,最后才调速度曲线。这个顺序看起来慢,但能省掉大量“到底是线错还是代码错”的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取