前言

本篇基于 STM32F103C8T6 单片机,搭配 28BYJ‑48 四相五线步进电机、ULN2003 驱动板以及 OLED 显示屏,实现步进电机正转、反转、指定角度旋转,同时 OLED 实时显示电机方向、运行步数、步进间隔。 采用TIM3 定时器中断 + 八拍半步驱动方案,摒弃软件延时做节拍,电机运行节拍稳定,适合嵌入式入门学习步进电机开环控制。

视频讲解:STM32F103C8T6驱动28BYJ‑48步进电机(ULN2003+OLED 显示)完整教程https://www.bilibili.com/video/BV1JRbi6VEVe/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

核心逻辑:STM32 决定线圈通电顺序与速度;ULN2003 提供线圈驱动电流;步进电机将电脉冲转化为固定角度机械运动;OLED 做状态显示。

一、演示功能目标

  1. 控制 28BYJ‑48 步进电机正转、反转、停止;
  2. 使用定时器中断产生步进节拍,不阻塞主程序;
  3. 八拍半步驱动模式,输出轴做精细角度转动;
  4. OLED 屏幕实时展示旋转方向、步数、步进间隔;
  5. 可设置指定半步数旋转,也支持电机连续旋转。

整体工作流程: 主程序配置旋转方向、步数、步进间隔 → 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 电源地

接线关键点讲解

  1. 为什么必须共地 STM32 输出高低电平是参考 GND 电位,如果驱动板和单片机地不接在一起,ULN2003 无法识别输入高低电平,现象为电机抖动、不转、随机乱动。
  2. 电机建议独立 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 两大核心功能:

  1. 将单片机微弱逻辑信号放大为电机线圈驱动电流;
  2. 吸收线圈断电反向感应电动势,保护主控电路。

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: CLOCKWISESTEPS:4096INTERVAL:2000us。 电机开始顺时针旋转完整一圈,旋转结束,屏幕更新显示MOTOR:STOPCOILS:OFF,电机线圈断电停止 1 秒。 之后切换方向为逆时针,电机反向旋转完整一圈,再次停止 1 秒,程序循环重复演示。

6.2 演示结论

  1. 步进电机属于开环控制,只靠脉冲计数,负载过大、速度过快会发生失步;
  2. TIM 定时器中断输出节拍比软件延时更加稳定,不会被主程序代码干扰;
  3. 28BYJ‑48 齿轮箱存在机械误差,多次旋转之后会累积位置偏差。

七、常见故障排查

故障 1:电机只抖动,不旋转

  1. ULN2003 IN1‑IN4 四根控制线接线顺序错误;
  2. 电机插头松动接触不良;
  3. 5V 电源输出电流不足,带不动电机;
  4. 步进间隔设置太小,启动直接失步;
  5. 机械负载阻力过大。

解决建议:先把步进间隔调到 3000~5000μs,卸掉负载测试。

故障 2:ULN2003 板载指示灯会变化,电机不转

说明 STM32 信号已经送到驱动板,排查下面几点:

  1. ULN2003 驱动板有没有接电机专用 5V 电源;
  2. 电机插头有没有插牢;
  3. STM32 和 ULN2003 驱动板是否共地;
  4. 电机本体或者驱动板损坏。

故障 3:电机实际旋转方向和 OLED 显示方向相反

不属于硬件故障。受电机出厂线序、观察电机正反面影响。解决方案:直接交换程序中 CLOCKWISE 和 COUNTER_CLOCKWISE 的序列逻辑。

故障 4:电机转一圈不能回到原始起点

  1. 28BYJ‑48 实际减速比和标称 4096 半步存在偏差;
  2. 速度过快、负载大,发生失步;
  3. 电源电压不足;
  4. 齿轮箱机械间隙回程误差。

解决方案:降低转速,实测校准一圈实际步数;高精度场景增加原点传感器或者编码器闭环反馈。

故障 5:电机、ULN2003 驱动板发热

线圈通电会消耗电能发热;本程序停止时关闭线圈,降低发热。如果需要停机保持锁轴,要监控温度,不能长时间通电。

八、总结

STM32 依靠 TIM 定时器中断切换八拍半步励磁序列,ULN2003 将单片机逻辑电平转换成电机线圈驱动电流,28BYJ‑48 依靠减速齿轮,将半步脉冲运动转化为低速可控角度输出,OLED 完成状态信息展示。

本项目完整演示步进电机开环控制,理解八拍半步驱动、定时器中断节拍、驱动芯片作用,同时理解步进电机的短板:失步、齿轮机械误差,为后续学习闭环步进电机打下基础。

如果本篇对你有帮助,欢迎点赞收藏,有问题评论区交流!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐