STM32F103C8T6驱动28BYJ‑48步进电机(ULN2003+OLED 显示)完整教程
前言
本篇基于 STM32F103C8T6 单片机,搭配 28BYJ‑48 四相五线步进电机、ULN2003 驱动板以及 OLED 显示屏,实现步进电机正转、反转、指定角度旋转,同时 OLED 实时显示电机方向、运行步数、步进间隔。 采用TIM3 定时器中断 + 八拍半步驱动方案,摒弃软件延时做节拍,电机运行节拍稳定,适合嵌入式入门学习步进电机开环控制。
核心逻辑:STM32 决定线圈通电顺序与速度;ULN2003 提供线圈驱动电流;步进电机将电脉冲转化为固定角度机械运动;OLED 做状态显示。
一、演示功能目标
- 控制 28BYJ‑48 步进电机正转、反转、停止;
- 使用定时器中断产生步进节拍,不阻塞主程序;
- 八拍半步驱动模式,输出轴做精细角度转动;
- OLED 屏幕实时展示旋转方向、步数、步进间隔;
- 可设置指定半步数旋转,也支持电机连续旋转。

整体工作流程: 主程序配置旋转方向、步数、步进间隔 → TIM3 定时中断触发 → 中断内部切换八拍半步励磁序列 → STM32 的 PA2/PA5/PA4/PA3 输出四路电平信号 → ULN2003 放大电流驱动电机线圈 → 电机转子按固定角度转动 → OLED 刷新显示运行状态。
二、硬件清单
| 器件 | 说明 |
|---|---|
| STM32F103C8T6 最小系统板 | 主控单片机 |
| 28BYJ‑48 步进电机 | 带减速箱四相五线步进电机 |
| ULN2003 驱动模块 | 步进电机电流放大驱动板 |
| 0.96 寸 I2C‑OLED 显示屏 | 状态显示 |
| 外部 5V 电源 | 给步进电机驱动板供电 |
| 杜邦线若干 | 接线使用 |
三、硬件接线说明
⚠️重要提醒:电机电源和 STM32 必须共地;不要用 STM32 的 GPIO 直接驱动电机线圈。
| 模块引脚 | STM32F103C8T6 引脚 | 功能说明 |
|---|---|---|
| ULN2003 IN1 | PA2 | 控制电机第 1 相线圈 |
| ULN2003 IN2 | PA5 | 控制电机第 2 相线圈 |
| ULN2003 IN3 | PA4 | 控制电机第 3 相线圈 |
| ULN2003 IN4 | PA3 | 控制电机第 4 相线圈 |
| ULN2003 VCC/+ | 外部 5V 电源正极 | 电机线圈供电 |
| ULN2003 GND/- | 外部 5V 电源负极 + STM32 GND | 共地 |
| OLED SCL | PB8 | 软件 I2C 时钟 |
| OLED SDA | PB9 | 软件 I2C 数据 |
| OLED VCC | 3.3V/5V | 按 OLED 模块标识选择供电电压 |
| OLED GND | STM32 GND | OLED 电源地 |
接线关键点讲解
- 为什么必须共地 STM32 输出高低电平是参考 GND 电位,如果驱动板和单片机地不接在一起,ULN2003 无法识别输入高低电平,现象为电机抖动、不转、随机乱动。
- 电机建议独立 5V 供电 步进电机线圈工作电流远大于单片机 GPIO 输出能力;线圈通断会带来电源扰动。独立 5V 供电可以避免 STM32 意外复位、OLED 屏幕闪烁。独立供电不等于电气隔离,电源负极一定要和单片机 GND 相连。
四、原理讲解
4.1 28BYJ‑48 步进电机原理
28BYJ‑48 是带减速齿轮箱的四相五线永磁式步进电机。内部有 4 组定子线圈 + 永磁转子。公共电源线接 5V 正极,剩下四根相线由 ULN2003 拉到地实现线圈通电。
线圈依次通电产生移动磁场,永磁转子跟随磁场一步步转动,再经过内部减速齿轮,输出轴实现低速大扭矩输出。
步进电机对比普通直流电机区别:
| 对比项 | 步进电机 | 普通直流电机 |
|---|---|---|
| 控制方式 | 按顺序切换多相线圈通电 | 直接施加电压驱动 |
| 位置控制 | 脉冲计数估算位置(开环) | 一般需要编码器反馈位置 |
| 速度控制 | 改变步进脉冲频率 | 调节电压或者 PWM 占空比 |
| 低速性能 | 适合低速分步运动 | 低速控制难度高 |
| 堵转现象 | 容易失步,线圈发热 | 电流急剧增大,轴停止转动 |
4.2 ULN2003 驱动芯片作用
STM32 GPIO 只能输出小电流逻辑电平,不足以驱动电机线圈。ULN2003 内部集成多路达林顿管,充当低端开关。
简单工作过程:STM32 引脚输出高电平 → ULN2003 内部达林顿管导通 → 5V 电流流过电机线圈,经过 ULN2003 流回 GND,线圈产生磁场;输入低电平,管子截止,线圈断电。
电机线圈是感性负载,断电瞬间会产生反向高压,ULN2003 自带续流保护通路,吸收反向电压,保护单片机电路。
ULN2003 两大核心功能:
- 将单片机微弱逻辑信号放大为电机线圈驱动电流;
- 吸收线圈断电反向感应电动势,保护主控电路。
4.3 八拍半步驱动模式
本工程使用八拍半步驱动,单相通电和两相同时通电交替切换,转子每次只走半个基础步距,运转更平稳。
| 拍序 | IN1 | IN2 | IN3 | IN4 | 程序十六进制值 | 通电状态 |
|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 0 | 0x01 | 第 1 相线圈通电 |
| 2 | 1 | 1 | 0 | 0 | 0x03 | 1、2 相线圈同时通电 |
| 3 | 0 | 1 | 0 | 0 | 0x02 | 第 2 相线圈通电 |
| 4 | 0 | 1 | 1 | 0 | 0x06 | 2、3 相线圈同时通电 |
| 5 | 0 | 0 | 1 | 0 | 0x04 | 第 3 相线圈通电 |
| 6 | 0 | 0 | 1 | 1 | 0x0C | 3、4 相线圈同时通电 |
| 7 | 0 | 0 | 0 | 1 | 0x08 | 第 4 相线圈通电 |
| 8 | 1 | 0 | 0 | 1 | 0x09 | 4、1 相线圈同时通电 |
1 代表 ULN2003 输入高电平,对应线圈通电;0 代表线圈断电。
- 正向旋转:拍序按 1→2→3→4→5→6→7→8 循环;
- 反向旋转:拍序按 8→7→6→5→4→3→2→1 循环;
- 停止切换拍序,关闭全部线圈,电机停止,无保持力矩。
注意:实物电机看到的旋转方向会受接线顺序、观察电机正反面影响,如果方向相反,程序内部调换正反转定义即可。
4.4 步数、角度、减速比关系
工程标称输出轴完整一圈需要 4096 个半步
- 单半步输出轴角度 = 360° / 4096 ≈0.0879°
- 需要的半步数 = 目标角度 ÷360° ×4096
目标角度 对应半步数 45° 512 90° 1024 180° 2048 360° 4096
注意:4096 是教学标称值,市面上 28BYJ‑48 齿轮减速比存在个体差异,长时间运行会累积角度误差。高精度定位需要原点传感器或者编码器做闭环。
4.5 TIM3 定时器中断控制速度
本项目不用软件延时,依靠 TIM3 定时器中断生成稳定步进节拍。STM32F103 系统时钟 72M,TIM3 时钟源 72MHz。
定时器预分频设置 72‑1,计数频率 72M/72=1MHz,计数一次耗时 1μs。 例如步进间隔设置 2000μs,自动重装载值 ARR=1999,每 2ms 触发一次中断,在中断中执行一次半步。
转速简单举例: 半步间隔 2000μs →半步频率 500 次每秒;输出轴转速≈7.32 转每分钟;电机转一圈耗时约 8.192s。
关键知识点:程序设置的参数是步进时间间隔,间隔越小,脉冲频率越高,电机转速越快;间隔越大转速越慢。 不能设置间隔太小,启动过快电机磁场切换太快,转子跟不上就会发生失步。
| 步进间隔 | 半步频率 | 标称输出轴转速 |
|---|---|---|
| 500μs | 2000 半步 / 秒 | 约 29.30rpm |
| 1000μs | 1000 半步 / 秒 | 约 14.65rpm |
| 2000μs | 500 半步 / 秒 | 约 7.32rpm |
| 3000μs | 约 333 半步 / 秒 | 约 4.88rpm |
| 5000μs | 200 半步 / 秒 | 约 2.93rpm |
4.6 软件整体运行逻辑
上电执行初始化:GPIO 初始化、OLED 初始化、TIM3 定时器初始化;配置步进间隔、旋转方向。 主程序调用步进运行函数启动电机,真正线圈换相操作放在 TIM3 中断服务函数后台执行,主程序可以做别的任务。
中断内部逻辑:判断电机是否正在运行,如果未运行直接退出;判断设定步数是否执行完成,完成则关闭定时器,切断线圈;未完成,按照正 / 反方向取下一拍励磁码输出到 GPIO,剩余步数自减。
OLED 只是显示单片机下发命令,无法检测电机真实物理位置,电机失步之后屏幕显示不会自动修正。
五、源代码相关
下面提供工程文件说明,完整源码放在文末下载,核心接口展示如下。
工程文件说明
| 文件 | 关键函数 / 数组 | 功能 |
|---|---|---|
| STEPPERMOTOR.c | g_half_step_sequence[] | 八拍半步励磁表数组 |
| StepperMotor_Init() | GPIO、TIM3、中断初始化 | |
| StepperMotor_SetSpeed() | 设置步进间隔,参数限幅保护 | |
| StepperMotor_SetDirection() | 设置电机正反转 | |
| StepperMotor_Step() | 运行指定半步数 | |
| StepperMotor_RotateContinuous() | 电机连续旋转 | |
| StepperMotor_Stop() | 停止电机,关闭线圈输出 | |
| TIM3_IRQHandler() | 定时器中断,切换励磁拍、计数步数 | |
| main.c | Motor_RunOneRevolution() | 电机旋转一圈演示函数 |
| Motor_ShowRunning() | OLED 刷新状态显示函数 |
main.c代码
/* 28BYJ-48 stepper motor and OLED teaching demonstration. */
#include "delay.h"
#include "oled.h"
#include "STEPPERMOTOR.h"
#define MOTOR_DEMO_INTERVAL_US 2000U
#define MOTOR_DEMO_PAUSE_MS 1000U
static void Motor_ShowRunning(MotorDirection direction);
static void Motor_ShowStopped(void);
static void Motor_RunOneRevolution(MotorDirection direction);
int main(void)
{
delay_init();
OLED_Init();
OLED_ColorTurn(0U);
OLED_DisplayTurn(0U);
StepperMotor_Init();
StepperMotor_SetSpeed(MOTOR_DEMO_INTERVAL_US);
while(1)
{
Motor_RunOneRevolution(CLOCKWISE);
Motor_ShowStopped();
delay_ms(MOTOR_DEMO_PAUSE_MS);
Motor_RunOneRevolution(COUNTER_CLOCKWISE);
Motor_ShowStopped();
delay_ms(MOTOR_DEMO_PAUSE_MS);
}
}
static void Motor_RunOneRevolution(MotorDirection direction)
{
StepperMotor_SetDirection(direction);
Motor_ShowRunning(direction);
StepperMotor_Step(STEPPERMOTOR_STEPS_PER_REVOLUTION);
while(StepperMotor_IsRunning() != 0U)
{
/* TIM3 interrupt performs the half-steps in the background. */
}
}
static void Motor_ShowRunning(MotorDirection direction)
{
OLED_Clear();
OLED_ShowString(0U, 0U, (u8 *)"28BYJ-48 + ULN", 16U);
if(direction == CLOCKWISE)
{
OLED_ShowString(0U, 18U, (u8 *)"DIR: CLOCKWISE", 12U);
}
else
{
OLED_ShowString(0U, 18U, (u8 *)"DIR: COUNTER-CW", 12U);
}
OLED_ShowString(0U, 34U, (u8 *)"STEPS: 4096", 12U);
OLED_ShowString(0U, 50U, (u8 *)"INTERVAL: 2000us", 12U);
OLED_Refresh();
}
static void Motor_ShowStopped(void)
{
OLED_Clear();
OLED_ShowString(0U, 0U, (u8 *)"28BYJ-48 + ULN", 16U);
OLED_ShowString(0U, 20U, (u8 *)"MOTOR: STOP", 16U);
OLED_ShowString(0U, 40U, (u8 *)"COILS: OFF", 16U);
OLED_Refresh();
}
STEPPERMOTOR.c代码
#include "STEPPERMOTOR.h"
#include "misc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_tim.h"
/*
* 文件名称:STEPPERMOTOR.c
* 功能说明:使用 TIM3 定时中断控制 28BYJ-48 步进电机。
*
* 下表为八拍半步励磁序列。每个数值的位 0~位 3 分别对应
* ULN2003 的 IN1~IN4。半步驱动在单线圈与双线圈励磁之间交替,
* 与四拍驱动相比,运行更加平稳且角度分辨率更高。
*/
static const uint8_t g_half_step_sequence[8] =
{
0x01U, /* IN1 */
0x03U, /* IN1 + IN2 */
0x02U, /* IN2 */
0x06U, /* IN2 + IN3 */
0x04U, /* IN3 */
0x0CU, /* IN3 + IN4 */
0x08U, /* IN4 */
0x09U /* IN4 + IN1 */
};
/* 以下变量会被主程序和 TIM3 中断共同访问,因此使用 volatile 修饰。 */
static volatile uint16_t g_step_interval_us = STEPPERMOTOR_DEFAULT_INTERVAL_US; /* 步进间隔 */
static volatile MotorDirection g_direction = CLOCKWISE; /* 旋转方向 */
static volatile uint32_t g_steps_remaining = 0UL; /* 剩余半步数 */
static volatile uint8_t g_current_phase = 0U; /* 当前励磁拍 */
static volatile uint8_t g_is_running = 0U; /* 运行标志 */
static volatile uint8_t g_is_continuous = 0U; /* 连续旋转标志 */
static void StepperMotor_WritePhase(uint8_t phase);
static void StepperMotor_StopFromInterrupt(void);
/*
* 函数功能:初始化步进电机驱动。
* 说明:PA2、PA5、PA4、PA3 配置为推挽输出;TIM3 配置为 1 MHz
* 计数频率,即计数器每增加 1 所经过的时间为 1 微秒。
*/
void StepperMotor_Init(void)
{
GPIO_InitTypeDef gpio_init;
TIM_TimeBaseInitTypeDef timer_init;
NVIC_InitTypeDef nvic_init;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
gpio_init.GPIO_Pin = STEPPERMOTOR_ALL_PINS;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(STEPPERMOTOR_GPIO_PORT, &gpio_init);
GPIO_ResetBits(STEPPERMOTOR_GPIO_PORT, STEPPERMOTOR_ALL_PINS);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_DeInit(TIM3);
/* APB1 定时器时钟为 72 MHz,经过 72 分频后得到 1 MHz 时基。 */
timer_init.TIM_Prescaler = 72U - 1U;
timer_init.TIM_CounterMode = TIM_CounterMode_Up;
timer_init.TIM_Period = STEPPERMOTOR_DEFAULT_INTERVAL_US - 1U;
timer_init.TIM_ClockDivision = TIM_CKD_DIV1;
timer_init.TIM_RepetitionCounter = 0U;
TIM_TimeBaseInit(TIM3, &timer_init);
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
nvic_init.NVIC_IRQChannel = TIM3_IRQn;
nvic_init.NVIC_IRQChannelPreemptionPriority = 1U;
nvic_init.NVIC_IRQChannelSubPriority = 0U;
nvic_init.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_init);
g_step_interval_us = STEPPERMOTOR_DEFAULT_INTERVAL_US;
g_direction = CLOCKWISE;
g_steps_remaining = 0UL;
g_current_phase = 0U;
g_is_continuous = 0U;
g_is_running = 0U;
TIM_Cmd(TIM3, DISABLE);
}
/*
* 函数功能:设置步进时间间隔。
* 参数说明:interval_us 为相邻两拍之间的时间,单位为微秒。
* 注意事项:参数会被限制在 500~5000 微秒范围内。
*/
void StepperMotor_SetSpeed(uint16_t interval_us)
{
uint8_t was_running;
if(interval_us < STEPPERMOTOR_MIN_INTERVAL_US)
{
interval_us = STEPPERMOTOR_MIN_INTERVAL_US;
}
else if(interval_us > STEPPERMOTOR_MAX_INTERVAL_US)
{
interval_us = STEPPERMOTOR_MAX_INTERVAL_US;
}
was_running = g_is_running;
g_is_running = 0U;
TIM_Cmd(TIM3, DISABLE);
g_step_interval_us = interval_us;
TIM_SetAutoreload(TIM3, (uint16_t)(interval_us - 1U));
TIM_SetCounter(TIM3, 0U);
TIM_GenerateEvent(TIM3, TIM_EventSource_Update);
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
if(was_running != 0U)
{
g_is_running = 1U;
TIM_Cmd(TIM3, ENABLE);
}
}
/*
* 函数功能:设置电机旋转方向。
* 参数说明:CLOCKWISE 为顺时针,COUNTER_CLOCKWISE 为逆时针。
*/
void StepperMotor_SetDirection(MotorDirection direction)
{
if((direction == CLOCKWISE) || (direction == COUNTER_CLOCKWISE))
{
g_direction = direction;
}
}
/*
* 函数功能:启动定步数运行。
* 参数说明:steps 表示需要执行的半步数;传入 0 时直接停止电机。
*/
void StepperMotor_Step(uint32_t steps)
{
if(steps == 0UL)
{
StepperMotor_Stop();
return;
}
/* 先停止定时器并清除旧中断,防止旧中断占用新命令的步数。 */
g_is_running = 0U;
TIM_Cmd(TIM3, DISABLE);
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
TIM_SetCounter(TIM3, 0U);
g_steps_remaining = steps;
g_is_continuous = 0U;
g_is_running = 1U;
TIM_Cmd(TIM3, ENABLE);
}
/* 启动连续旋转模式,步数不受限制。 */
void StepperMotor_RotateContinuous(void)
{
g_is_running = 0U;
TIM_Cmd(TIM3, DISABLE);
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
TIM_SetCounter(TIM3, 0U);
g_steps_remaining = 0UL;
g_is_continuous = 1U;
g_is_running = 1U;
TIM_Cmd(TIM3, ENABLE);
}
/* 停止电机,同时关闭四路输出以降低线圈发热和功耗。 */
void StepperMotor_Stop(void)
{
/* 先清除运行标志,避免待处理的中断继续输出一个半步。 */
g_is_running = 0U;
TIM_Cmd(TIM3, DISABLE);
g_is_continuous = 0U;
g_steps_remaining = 0UL;
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
GPIO_ResetBits(STEPPERMOTOR_GPIO_PORT, STEPPERMOTOR_ALL_PINS);
}
/* 返回电机当前运行状态。 */
uint8_t StepperMotor_IsRunning(void)
{
return g_is_running;
}
/* 返回定步数模式下尚未执行的半步数。 */
uint32_t StepperMotor_GetRemainingSteps(void)
{
return g_steps_remaining;
}
/* 返回当前步进时间间隔,单位为微秒。 */
uint16_t StepperMotor_GetSpeed(void)
{
return g_step_interval_us;
}
/* 返回当前设置的旋转方向。 */
MotorDirection StepperMotor_GetDirection(void)
{
return g_direction;
}
/*
* TIM3 更新中断服务函数。
* 每进入一次中断,按照设定方向切换到下一拍并更新 ULN2003 输入。
*/
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
if(g_is_running == 0U)
{
return;
}
/* 最后一拍保持完整的步进间隔后再断电,确保转子有足够时间到位。 */
if((g_is_continuous == 0U) && (g_steps_remaining == 0UL))
{
StepperMotor_StopFromInterrupt();
return;
}
if(g_direction == CLOCKWISE)
{
g_current_phase = (uint8_t)((g_current_phase + 1U) & 0x07U);
}
else
{
g_current_phase = (uint8_t)((g_current_phase - 1U) & 0x07U);
}
StepperMotor_WritePhase(g_current_phase);
if(g_is_continuous == 0U)
{
g_steps_remaining--;
}
}
}
/*
* 根据八拍序号设置四路 GPIO 电平。
* 使用 BSRR 寄存器一次完成置位和复位,避免逐个切换引脚产生错误拍型。
*/
static void StepperMotor_WritePhase(uint8_t phase)
{
uint8_t pattern;
uint16_t pins_to_set;
uint16_t pins_to_reset;
pattern = g_half_step_sequence[phase & 0x07U];
pins_to_set = 0U;
if((pattern & 0x01U) != 0U)
{
pins_to_set |= STEPPERMOTOR_IN1_PIN;
}
if((pattern & 0x02U) != 0U)
{
pins_to_set |= STEPPERMOTOR_IN2_PIN;
}
if((pattern & 0x04U) != 0U)
{
pins_to_set |= STEPPERMOTOR_IN3_PIN;
}
if((pattern & 0x08U) != 0U)
{
pins_to_set |= STEPPERMOTOR_IN4_PIN;
}
pins_to_reset = (uint16_t)(STEPPERMOTOR_ALL_PINS & (uint16_t)(~pins_to_set));
STEPPERMOTOR_GPIO_PORT->BSRR = ((uint32_t)pins_to_reset << 16U) | pins_to_set;
}
/* 在 TIM3 中断内部结束定步数运行。 */
static void StepperMotor_StopFromInterrupt(void)
{
g_is_running = 0U;
g_is_continuous = 0U;
TIM_Cmd(TIM3, DISABLE);
GPIO_ResetBits(STEPPERMOTOR_GPIO_PORT, STEPPERMOTOR_ALL_PINS);
}
STEPPERMOTOR.h代码
#ifndef __STEPPERMOTOR_H
#define __STEPPERMOTOR_H
#include "stm32f10x.h"
/*
* 文件名称:STEPPERMOTOR.h
* 功能说明:28BYJ-48 步进电机驱动接口定义。
* 驱动方式:STM32F103C8T6 通过 ULN2003 驱动,采用八拍半步方式运行。
*/
/* ULN2003 输入端与 STM32F103C8T6 的引脚连接。 */
#define STEPPERMOTOR_GPIO_PORT GPIOA
#define STEPPERMOTOR_IN1_PIN GPIO_Pin_2 /* ULN2003 IN1 -> PA2 */
#define STEPPERMOTOR_IN2_PIN GPIO_Pin_5 /* ULN2003 IN2 -> PA5 */
#define STEPPERMOTOR_IN3_PIN GPIO_Pin_4 /* ULN2003 IN3 -> PA4 */
#define STEPPERMOTOR_IN4_PIN GPIO_Pin_3 /* ULN2003 IN4 -> PA3 */
#define STEPPERMOTOR_ALL_PINS (STEPPERMOTOR_IN1_PIN | \
STEPPERMOTOR_IN2_PIN | \
STEPPERMOTOR_IN3_PIN | \
STEPPERMOTOR_IN4_PIN)
/*
* 相邻两拍之间的时间间隔,单位为微秒。
* 间隔越小,电机转速越快;间隔过小可能导致电机失步。
*/
#define STEPPERMOTOR_MIN_INTERVAL_US 500U
#define STEPPERMOTOR_MAX_INTERVAL_US 5000U
#define STEPPERMOTOR_DEFAULT_INTERVAL_US 2000U
/* 28BYJ-48 输出轴旋转一圈通常按 4096 个半步计算。 */
#define STEPPERMOTOR_STEPS_PER_REVOLUTION 4096UL
/* 步进电机旋转方向。 */
typedef enum
{
CLOCKWISE = 0, /* 顺时针旋转 */
COUNTER_CLOCKWISE = 1 /* 逆时针旋转 */
} MotorDirection;
/* 初始化电机控制 GPIO、TIM3 和 TIM3 中断。 */
void StepperMotor_Init(void);
/* 设置相邻两拍之间的时间间隔,单位为微秒。 */
void StepperMotor_SetSpeed(uint16_t interval_us);
/* 设置旋转方向。 */
void StepperMotor_SetDirection(MotorDirection direction);
/* 按当前方向运行指定的半步数。 */
void StepperMotor_Step(uint32_t steps);
/* 按当前方向连续旋转,直到调用 StepperMotor_Stop。 */
void StepperMotor_RotateContinuous(void);
/* 停止电机并关闭全部线圈,防止线圈持续发热。 */
void StepperMotor_Stop(void);
/* 查询电机是否正在运行:1 表示运行,0 表示停止。 */
uint8_t StepperMotor_IsRunning(void);
/* 获取定步数运行模式下尚未执行的半步数。 */
uint32_t StepperMotor_GetRemainingSteps(void);
/* 获取当前相邻两拍之间的时间间隔,单位为微秒。 */
uint16_t StepperMotor_GetSpeed(void);
/* 获取当前设置的旋转方向。 */
MotorDirection StepperMotor_GetDirection(void);
#endif /* __STEPPERMOTOR_H */
核心调用示例
1、指定方向、速度,旋转指定步数
//设置顺时针
StepperMotor_SetDirection(CLOCKWISE);
//设置步进间隔2000微秒
StepperMotor_SetSpeed(2000U);
//运行4096个半步,输出轴一圈
StepperMotor_Step(4096UL);
//等待电机运行完成
while(StepperMotor_IsRunning() != 0U)
{
}
2、电机连续旋转
StepperMotor_SetDirection(COUNTER_CLOCKWISE);
StepperMotor_SetSpeed(3000U);
StepperMotor_RotateContinuous();
3、停止电机
StepperMotor_Stop();
补充:当前 Stop 函数直接关闭所有线圈,功耗低,但是停机没有锁轴保持力矩;如果需要停机锁死,就不关闭线圈,维持当前励磁拍,注意长时间通电电机发热。
六、实物演示章节
6.1 演示现象
上电之后 OLED 屏幕点亮,第一行打印标题28BYJ‑48 + ULN,随后显示DIR: CLOCKWISE,STEPS:4096,INTERVAL:2000us。 电机开始顺时针旋转完整一圈,旋转结束,屏幕更新显示MOTOR:STOP,COILS:OFF,电机线圈断电停止 1 秒。 之后切换方向为逆时针,电机反向旋转完整一圈,再次停止 1 秒,程序循环重复演示。
6.2 演示结论
- 步进电机属于开环控制,只靠脉冲计数,负载过大、速度过快会发生失步;
- TIM 定时器中断输出节拍比软件延时更加稳定,不会被主程序代码干扰;
- 28BYJ‑48 齿轮箱存在机械误差,多次旋转之后会累积位置偏差。
七、常见故障排查
故障 1:电机只抖动,不旋转
- ULN2003 IN1‑IN4 四根控制线接线顺序错误;
- 电机插头松动接触不良;
- 5V 电源输出电流不足,带不动电机;
- 步进间隔设置太小,启动直接失步;
- 机械负载阻力过大。
解决建议:先把步进间隔调到 3000~5000μs,卸掉负载测试。
故障 2:ULN2003 板载指示灯会变化,电机不转
说明 STM32 信号已经送到驱动板,排查下面几点:
- ULN2003 驱动板有没有接电机专用 5V 电源;
- 电机插头有没有插牢;
- STM32 和 ULN2003 驱动板是否共地;
- 电机本体或者驱动板损坏。
故障 3:电机实际旋转方向和 OLED 显示方向相反
不属于硬件故障。受电机出厂线序、观察电机正反面影响。解决方案:直接交换程序中 CLOCKWISE 和 COUNTER_CLOCKWISE 的序列逻辑。
故障 4:电机转一圈不能回到原始起点
- 28BYJ‑48 实际减速比和标称 4096 半步存在偏差;
- 速度过快、负载大,发生失步;
- 电源电压不足;
- 齿轮箱机械间隙回程误差。
解决方案:降低转速,实测校准一圈实际步数;高精度场景增加原点传感器或者编码器闭环反馈。
故障 5:电机、ULN2003 驱动板发热
线圈通电会消耗电能发热;本程序停止时关闭线圈,降低发热。如果需要停机保持锁轴,要监控温度,不能长时间通电。
八、总结
STM32 依靠 TIM 定时器中断切换八拍半步励磁序列,ULN2003 将单片机逻辑电平转换成电机线圈驱动电流,28BYJ‑48 依靠减速齿轮,将半步脉冲运动转化为低速可控角度输出,OLED 完成状态信息展示。
本项目完整演示步进电机开环控制,理解八拍半步驱动、定时器中断节拍、驱动芯片作用,同时理解步进电机的短板:失步、齿轮机械误差,为后续学习闭环步进电机打下基础。
如果本篇对你有帮助,欢迎点赞收藏,有问题评论区交流!
更多推荐



所有评论(0)