简介:本资源是面向嵌入式初学者与STM32实践者的步进电机控制完整工程,聚焦于使用STM32F103C8T6微控制器驱动常见5线四相步进电机28BYJ-48,解决精确定位、启停控制与方向调节等典型运动控制问题,适用于智能小车、3D打印平台、自动化演示装置等教学与原型开发场景。压缩包共261个文件,含114个头文件(.h)定义外设与电机接口、69个源文件(.c)实现HAL库驱动、定时器PWM调速、ULN2003电平转换及正反转步序逻辑,另有编译中间文件(.o/.d/.crf)与可执行镜像(.hex/.axf),整体7.04MB,结构规范,适配Keil MDK开发环境。已有2515人学习下载,资源包含完整的STM32 HAL库工程框架,覆盖I2C、UART、SPI、TIM、ADC等多外设初始化代码,便于读者理解电机控制与系统协同逻辑,并可快速迁移至同类电机或升级为细分驱动方案。 最近翻硬盘又看到那份《STM32控制步进电机28BYJ-48.zip》,早几年第一次调28BYJ-48步进电机时,我在接线、相位顺序和电源上花了不少冤枉时间。这次重新整理了一遍,把工程里能用的代码、硬件接线和踩坑记录一次性写出来。大部分新手拿到STM32想做的第一件“会动”的事,就是驱动一个步进电机。28BYJ-48恰好是最容易买到、资料最多、也最能逼你搞懂原理的型号:它便宜、自带减速箱、5V就能跑,配上ULN2003驱动板,几乎成了各类DIY项目的标配。文章适合刚接触STM32、想让电机转起来但不知道从哪下手的读者,也适合想弄明白“为什么这么接、为什么这么写”的人。搞清楚这个小电机,后面再玩两相步进电机、伺服电机就顺了。
1. 项目整体方案拆解:为什么是“STM32 + 28BYJ-48 + ULN2003”这套组合
1.1 先认识一下28BYJ-48
28BYJ-48是一只永磁式步进电机,5根线、内部4个绕组、自带一级齿轮减速箱。型号里的“28”指的是电机外壳直径约28mm,BYJ常被理解为永磁步进带减速,48这个数字在不同资料里有不同说法,不用太纠结。真正关心的是铭牌上那几个关键参数:额定5V、步距角5.625°、减速比1/64。很多人第一次看到这组数字会懵,我拆开讲一次。
这里说的5.625°是内部电机转子每一步转过的角度,是在“四相八拍”驱动方式下得到的。八拍就是按 A→AB→B→BC→C→CD→D→DA 的顺序给绕组通电,每一步让转子走半个整步步距。因为有1/64的齿轮减速,输出轴实际每一步只转 5.625 / 64 ≈ 0.0879°。算一下:360 / 0.0879 ≈ 4096,也就是让输出轴转一整圈,需要精确给出4096个步进脉冲。这个数字后面写代码随时要用。
说句实在话,28BYJ-48的运动性能没有多强。减速之后输出转速低,扭矩也有限,空载稳定运行大概在每分钟15转上下,带负载时还要再慢一点。它适合做什么?智能窗帘、摄像头云台、自动浇花器的阀门、小型转台、广告展示架,这些“要动但不用快”的场景是它的主场。相比之下,3D打印机里常用的42步进电机是两相4线,走的是A4988或DRV8825驱动方案,和28BYJ-48不是一回事,别混着用。
1.2 ULN2003驱动板在这里面到底干了什么
STM32的GPIO引脚输出高电平是3.3V,最大输出电流通常就几毫安,而28BYJ-48每个绕组的直流电阻只有几十欧姆,5V供电时单相电流就要一百毫安左右,四个绕组同时换相时尖峰更明显。直接用GPIO去拉,轻则转不动,重则把单片机引脚烧了。
ULN2003模块的作用就是解决“小信号驱动大负载”的问题。它内部是7路达林顿晶体管阵列,每一路都工作在集电极开路状态,本质是一个反相器:输入高电平时,输出端导通到地;输入低电平时,输出端断开。电机绕组一端接5V电源正极,另一端接ULN2003输出,当STM32给对应输入引脚一个高电平,输出端导通,这条绕组回路就通电了。
有人会问,STM32的3.3V高电平过去,ULN2003能不能识别?可以。ULN2003的高电平输入门槛大概在2V左右,3.3V完全够。驱动板上的VCC和GND是用来给板子内部逻辑和电机公共端供电的,注意需要5V,不是3.3V。这个模块还有一个隐藏好处:内部每个通道输出端都带有续流二极管,电机绕组是感性负载,断电瞬间会产生反向电动势,没有续流二极管很容易打坏驱动管,ULN2003把这些都集成好了,这也是它适合驱动这种小步进电机的原因。
1.3 为什么不是L298N、不是A4988
很多新手手边可能会翻出一块L298N模块,想着“反正都是电机驱动,应该能用”。L298N确实能驱动步进电机,但它是为有刷直流电机设计的双H桥芯片,体积大、压降大、还需要额外的逻辑电源,驱动28BYJ-48属于大材小用。最尴尬的是,L298N供电电压高一些效果才好,而28BYJ-48常见的5V版本压降一扣,实际加在绕组上的电压不足,扭矩反而更差。
A4988和DRV8825这类模块是给两相4线混合式步进电机设计的,它们内部有微步细分、电流调节、STEP/DIR控制接口,概念和5线4相永磁电机完全不匹配。28BYJ-48没有A相B相这种差分结构,它是4个绕组共用一个公共端,硬接A4988很容易出问题。
ULN2003方案最大的优势就是“匹配”:电机买回来自带驱动板,五根线一插就完事。控制方式也直观,4个GPIO按顺序换相即可,不需要PWM,不需要电流调节,非常适合学习和做小型项目。
2. 硬件连接:接线、电源、共地,一个都不能少
2.1 常见引脚分配与完整接线表
我这次用的主控是经典的STM32F103C8T6蓝色开发板,也就是大家口中的“STM32最小系统板”。引脚分配上故意避开了一些特殊功能引脚,选了PB0、PB1、PB10、PB11这四个普通IO口来连接ULN2003的IN1~IN4。这样做的原因是STM32F103的PB3、PB4、PA15默认作为JTAG调试引脚使用,如果新手对这些引脚不熟悉,配置不好会得到一个“不受控”的输出,后面容易怀疑人生。
ULN2003驱动板一端是4个输入端子和GND/VCC端子,另一端是5个电机接线柱。正常接线表如下:
| 驱动板端子 | 连接目标 |
|---|---|
| IN1 | STM32 PB0 |
| IN2 | STM32 PB1 |
| IN3 | STM32 PB10 |
| IN4 | STM32 PB11 |
| VCC | 外部5V电源正极 |
| GND | STM32的GND(必须共地) |
| 电机端子 | 28BYJ-48排线插上,公共端对公共端 |
28BYJ-48的排线通常是5根,常见颜色是红、蓝、粉、黄、橙,红色是公共端,接驱动板的电源正极位置,其余四色按丝印顺序插入。这个顺序在不同批次电机上可能存在差异,所以我把代码引脚做成了数组映射,如果转不动或方向反了,直接调换数组里的引脚顺序就能解决,不用重新接线。
2.2 5V电源和“共地”问题
这个坑我必须单独拎出来说。很多新手第一版接线喜欢直接从STM32开发板的3.3V引脚给ULN2003模块供电,结果电机要么没劲,要么转动几下之后单片机就整体复位了。原因是开发板上的稳压芯片一般只能输出几百毫安,28BYJ-48启动和换相时瞬时电流很容易超过这个值,电压一跌,主控先崩了。
28BYJ-48在5V供电下单相绕组电流大约100mA,四相轮流通电时总电流可达三四百毫安,建议准备一个能输出500mA以上、最好1A的独立5V电源。可以用USB电源、手机充电器改造的电源模块,或者降压模块从更高电压降到5V。电源和STM32开发板之间一定要共地,也就是把5V电源的GND和STM32的GND接到一起。如果不共地,GPIO输出的3.3V高电平和ULN2003模块没有共同的参考电位,信号乱飞,电机可能完全不动或者随机乱跳。
2.3 3.3V的STM32能不能直接驱动
能,但要注意配置。GPIO要设置成推挽输出模式(GPIO_MODE_OUTPUT_PP),而不是开漏模式。开漏模式下高电平是靠外部上拉电阻实现的,如果没有上拉,引脚输出高电平时会变成高阻态,ULN2003输入拉不起来,电机就罢工了。
另外,ULN2003的输入端和STM32之间要不要串电阻?我的建议是,驱动板输入端如果已经带了保护电阻就不需要再加,没有的话串一个几百欧到1k欧的电阻也不会影响高电平识别,还能避免接线失误带来的风险。不过实际项目中,大多数廉价ULN2003模块输入端已经有串联电阻,直接把杜邦线怼上去就能工作。
3. 核心代码实现:从点亮一个IO到稳定转动
3.1 相位表和GPIO的映射关系
步进电机控制的核心就是“按顺序给不同绕组通电”。我们要维护一张相位表,表中每一行表示某一时刻四个GPIO的电平状态。以八拍半步方式为例:
static const uint8_t phase_table[8][4] = { {1, 0, 0, 0}, // A相导通 {1, 1, 0, 0}, // A、B相导通 {0, 1, 0, 0}, // B相导通 {0, 1, 1, 0}, // B、C相导通 {0, 0, 1, 0}, // C相导通 {0, 0, 1, 1}, // C、D相导通 {0, 0, 0, 1}, // D相导通 {1, 0, 0, 1}, // D、A相导通 };第1行到第8行依次执行一遍,输出轴正好走一个“内部步距角”,也就是5.625°。执行4096次,输出轴转一圈。为什么用八拍而不是直接四拍 A→B→C→D?四拍也能转,但每一步角度大一倍,转子运动更生硬,扭矩波动也大。八拍半步方式相当于在每两个整步之间插入了一个“两相同时导通”的状态,让转子过渡更平滑,噪音更小,定位更细腻。这个细节很值得体会,它是步进电机控制里非常基础的设计思想。
GPIO的映射我用数组管理,这样调整线序时不用改函数逻辑,只需改数组内容:
#define M_IN1_PORT GPIOB #define M_IN1_PIN GPIO_PIN_0 #define M_IN2_PORT GPIOB #define M_IN2_PIN GPIO_PIN_1 #define M_IN3_PORT GPIOB #define M_IN3_PIN GPIO_PIN_10 #define M_IN4_PORT GPIOB #define M_IN4_PIN GPIO_PIN_113.2 用定时器中断产生精确的步进节拍
控制步进电机,最关键的是“每一拍之间的时间间隔要准确”。网上很多入门教程直接用延时函数:
while (1) { set_phase(phase_index++); delay_ms(2); }这个写法在电机单独转、没有任何其他任务时勉强能用,一旦你要同时处理按键、串口、显示,延时函数就会卡住整个流程,而且CPU大部分时间都浪费在空等上。更麻烦的是,如果中断里不小心调用了带延时的函数,系统直接卡死。
我推荐用定时器中断实现:定时器以固定周期触发中断,每次中断里切换一拍,主程序只负责下发“走多少步、正转还是反转”的命令。这样电机运转不再占用主循环,后续想同时处理其他外设,代码也好写得多。
以STM32F103的TIM3为例,芯片主频72MHz,定时器挂在APB1总线上,经过倍频后也是72MHz。设置预分频器PSC为71,得到一个1MHz的计数时钟,再设置自动重装值ARR为999,定时器更新周期就是 1000 / 1000000 = 1ms。也就是说,每1ms发生一次更新中断,电机每1ms走一拍,输出轴转速约14.65 RPM。
3.3 支持速度、方向、步数的电机模块代码
我把电机控制封装成了一个小模块,包含初始化、启动、停止、周期回调四个部分。放在工程里直接调用即可。
// motor.h #ifndef __MOTOR_H #define __MOTOR_H #include "stm32f1xx_hal.h" #define M_IN1_PORT GPIOB #define M_IN1_PIN GPIO_PIN_0 #define M_IN2_PORT GPIOB #define M_IN2_PIN GPIO_PIN_1 #define M_IN3_PORT GPIOB #define M_IN3_PIN GPIO_PIN_10 #define M_IN4_PORT GPIOB #define M_IN4_PIN GPIO_PIN_11 void Motor_Init(void); void Motor_Run(int32_t steps); void Motor_Stop(void); void Motor_PeriodElapsedCallback(void); #endif// motor.c #include "motor.h" static const uint8_t phase_table[8][4] = { {1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 1}, {0, 0, 0, 1}, {1, 0, 0, 1}, }; static volatile uint8_t phase_index = 0; static volatile int32_t remain_steps = 0; static volatile int8_t motor_dir = 1; static void set_phase(uint8_t idx) { HAL_GPIO_WritePin(M_IN1_PORT, M_IN1_PIN, phase_table[idx][0] ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(M_IN2_PORT, M_IN2_PIN, phase_table[idx][1] ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(M_IN3_PORT, M_IN3_PIN, phase_table[idx][2] ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(M_IN4_PORT, M_IN4_PIN, phase_table[idx][3] ? GPIO_PIN_SET : GPIO_PIN_RESET); } void Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; TIM_HandleTypeDef htim3 = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin = M_IN1_PIN | M_IN2_PIN | M_IN3_PIN | M_IN4_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance = TIM3; htim3.Init.Prescaler = 71; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 999; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim3); HAL_TIM_Base_Start_IT(&htim3); HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM3_IRQn); } void Motor_Run(int32_t steps) { if (steps == 0) return; motor_dir = (steps > 0) ? 1 : -1; remain_steps = (steps > 0) ? steps : -steps; } void Motor_Stop(void) { remain_steps = 0; HAL_GPIO_WritePin(M_IN1_PORT, M_IN1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(M_IN2_PORT, M_IN2_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(M_IN3_PORT, M_IN3_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(M_IN4_PORT, M_IN4_PIN, GPIO_PIN_RESET); } void Motor_PeriodElapsedCallback(void) { if (remain_steps == 0) { return; } if (motor_dir > 0) { phase_index = (phase_index + 1) & 0x07; } else { phase_index = (phase_index + 7) & 0x07; } set_phase(phase_index); remain_steps--; if (remain_steps == 0) { HAL_GPIO_WritePin(M_IN1_PORT, M_IN1_PIN, GPIO_PIN_RESET); HAL_GPIO_Write <p> <a href="https://download.csdn.net/download/weixin_43469377/37960535" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>