基于STM32的步进电机搅拌机控制系统设计与实现

发布时间:2026/10/23 11:52:42

基于STM32的步进电机搅拌机控制系统设计与实现 单片机类课程设计和毕业设计中步进电机控制一直是一个经典又有代表性的选题既涉及 GPIO 输出、定时器中断、按键状态机又包含电机驱动电路和硬件接线能够把嵌入式开发的常用知识点串起来。本文以“基于 STM32 单片机步进电机控制搅拌机控制”为例从需求分析、器件选型、步进电机原理、驱动电路、软件架构到最终调试完整拆解一套可以移植的工程方案。无论是正在做课设、备战电子设计竞赛还是想系统学习 STM32 标准库开发这篇文章都值得收藏备用。1. 项目背景与功能概述1.1 为什么选择 STM32 步进电机方案搅拌机在食品加工、化工配料、实验室液体混合等场景中应用广泛。传统搅拌机多使用交流电机或直流电机转速控制依赖机械调速或简单的可控硅调压虽然成本低但很难做到精确的转速控制、正反转切换和定时停机。步进电机则是一种把电脉冲信号转换成角位移或线位移的执行元件。每收到一个脉冲电机就走一个固定的角度因此不需要编码器就能实现开环定位和速度控制。对于搅拌机这种“长时间低速搅拌、需要正反转、对转速稳定性有要求”的应用场景步进电机配合 STM32 单片机可以实现多档转速切换低速搅拌更平稳正转、反转自由切换使物料混合更均匀定时搅拌到时间自动停机实时显示搅拌时间和工作状态。STM32F103 系列主频最高 72MHz内置多个定时器GPIO 翻转速度快完全满足步进电机控制所需的脉冲序列输出要求。更重要的是STM32 标准外设库和 HAL 库资料丰富网上工程模板多适合刚入门单片机开发的同学快速上手。1.2 系统功能需求本项目参考常见课程设计要求把搅拌机控制器功能拆解为以下几项功能说明启动/停止控制按下启动键后电机旋转再次按下停止也可定时结束自动停止正反转切换按键切换电机旋转方向LCD 上显示当前方向转速档位设置支持低、中、高三档速度切换搅拌时间设置可设置搅拌时长单位为秒倒计时显示在 LCD 上实时状态显示LCD1602 显示当前时间、速度档位、方向、运行状态蜂鸣器提示定时结束或按键操作时发出短提示音1.3 系统设计任务拆分整个项目可以分为四个部分硬件电路STM32F103C8T6 最小系统、步进电机驱动模块、LCD1602 显示模块、按键模块、蜂鸣器模块、电源模块。步进电机驱动根据电机型号选择合适的驱动方式产生正确的脉冲时序。软件控制实现 GPIO 初始化、定时器中断、电机换相、按键扫描、LCD 显示和主流程状态切换。整机联调下载程序至开发板测试各按键功能和不同档位下的电机转速。2. 系统总体设计与硬件选型2.1 核心器件选型项目核心器件选型如下器件型号数量说明主控芯片STM32F103C8T61基于 ARM Cortex-M3 内核主频 72MHzFlash 64KBSRAM 20KB步进电机28BYJ-4815 线 4 相减速步进电机额定电压 5V自带 1:64 减速箱电机驱动板ULN20031达林顿晶体管阵列最大驱动电流约 500mA可直接驱动 28BYJ-48显示屏LCD1602116 字符 × 2 行可显示英文字符和数字按键轻触按键4用于启动/停止、正反转、时间、速度设置蜂鸣器有源蜂鸣器1用于提示音电源5V/2A 适配器或 USB 供电1给单片机、LCD 和电机驱动供电这里需要说明一下28BYJ-48 是一种 5 线 4 相步进电机内部带有减速齿轮组步距角经过减速后非常小输出扭矩大非常适合低速搅拌、云台、小家电等场景。搜索热词中提到的“两相4线混合式步进电机”通常指 42 步进电机、57 步进电机这类工业电机需要搭配 A4988、DRV8825 等驱动模块本文后面会单独介绍迁移方案。2.2 系统框图整个系统以 STM32F103C8T6 为中心外围电路包括电源电路、复位电路、晶振电路、程序下载电路和功能模块电路。功能模块中LCD1602 通过与 GPIO 并口连接按键通过 GPIO 上拉输入检测蜂鸣器通过 GPIO 推挽输出驱动步进电机则经 ULN2003 驱动板与 GPIO 相连。系统的工作流程是STM32 根据按键输入改变状态控制定时器产生脉冲节拍GPIO 按换相表输出电平ULN2003 放大驱动电流后步进电机按设定方向与速度旋转同时 LCD1602 实时刷新显示状态。2.3 STM32F103C8T6 最小系统组成STM32F103C8T6 要正常工作至少需要以下部分3.3V 电源供电与去耦电容8MHz 外部晶振或直接使用内部 HSI 时钟BOOT0 引脚配置通常接地从 Flash 启动NRST 复位电路SWD 下载接口使用 ST-Link 下载程序。如果为了进一步降低开发难度也可以直接购买市面上的“STM32F103C8T6 最小系统板”板上已经集成了 USB 转串口、LDO 稳压、LED 指示灯等配合 ST-Link 即可完成程序下载和调试。3. 步进电机工作原理与控制方式3.1 步进电机的结构步进电机的核心结构包括定子和转子。定子上有多组绕组转子上有永磁体或软铁齿。给不同绕组依次通电时电机内部会产生旋转磁场转子会向对应方向转动一个固定的角度这个角度称为“步距角”。以 28BYJ-48 为例它属于 4 相永磁式减速步进电机共有 5 根线其中一根是公共电源线另外四根分别连接四相绕组。电机内部自带行星齿轮减速箱减速比为 1:64也就是说电机转子每转 64 圈输出轴才转 1 圈。这样设计的好处是低速扭矩大非常适合直接驱动搅拌桨叶。3.2 步距角与拍数28BYJ-48 的技术参数中步距角标注为 5.625°/64。这里的 5.625° 是指电机转子本体每输入一个脉冲转过的角度但由于减速箱的存在输出轴实际仅转动 5.625°/64 ≈ 0.0879°。常用的驱动方式有两类四相四拍每次只让一相绕组通电按 A-B-C-D 顺序循环控制简单但振动和噪音稍大。四相八拍采用半步驱动通电顺序为 A-AB-B-BC-C-CD-D-DA每拍转过角度更小运行更平稳输出扭矩也更大。本项目使用四相八拍方式因为搅拌机负载波动相对较快平滑度优先。3.3 转速与脉冲频率的关系步进电机的转速取决于输入脉冲频率。对于 28BYJ-48 1:64 减速箱输出轴旋转一圈需要接收360° / 0.0879° ≈ 4096 个半步脉冲如果每毫秒发一个脉冲即脉冲频率为 1kHz则输出轴每秒旋转 1000 / 4096 ≈ 0.244 圈每分钟约 14.6 转。这个转速对低速搅拌来说已经比较合适。如果需要更快的搅拌速度可以缩短脉冲间隔但需要注意步进电机存在启动极限频率频率过高会丢步甚至不转驱动板和电源电流必须满足要求负载惯量过大时需要加减速曲线不能直接高速启动。3.4 两相4线混合式步进电机的区别搜索热词中频繁出现“两相4线混合式步进电机”这类电机常见型号有 42BYGH、57BYGH 等与 28BYJ-48 有很大区别对比项28BYJ-4842/57 两相混合式线数5 线 4 相4 线 2 相驱动方式ULN2003 直接驱动A4988/DRV8825 等驱动器步距角5.625°/64通常 1.8° 或 0.9°工作电压5V12V-24V 或更高定位扭矩较小较大如果项目要求更大的搅拌扭矩可以把电机换成 42 步进电机使用 A4988 驱动模块。A4988 采用 STEP/DIR 控制方式DIR 引脚决定方向STEP 引脚每收到一个上升沿电机走一步。与直接 GPIO 换相相比软件逻辑更简单且 A4988 支持 1/1、1/2、1/4、1/8、1/16 细分可以进一步降低振动和噪音。4. 硬件电路设计4.1 ULN2003 驱动电路ULN2003 是一个达林顿晶体管阵列内部集成了 7 个达林顿管每个管子输出端内置续流二极管非常适合驱动继电器和步进电机。与 28BYJ-48 的接线方式如下28BYJ-48 红色线接 5V 电源正极作为公共端橙色线接 ULN2003 的 IN1对应 OUT1黄色线接 ULN2003 的 IN2粉色线接 ULN2003 的 IN3蓝色线接 ULN2003 的 IN4。ULN2003 输入引脚 IN1-IN4 分别连接到 STM32 的 PB0-PB3。由于 ULN2003 是反相驱动器当 GPIO 输出高电平时对应绕组端被拉到低电平绕组才通电。因此控制时序是“高电平有效”。如果把 PB0-PB3 的电平按 8 拍换相表输出就可以驱动电机旋转// 四相八拍换相表高电平表示对应相导通 // 顺序A - AB - B - BC - C - CD - D - DA static const uint8_t step_table[8] { 0x08, 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09 };其中最低位对应 PB0橙线最高位对应 PB3蓝线。4.2 LCD1602 显示电路LCD1602 的工作电压为 5V而 STM32 的 GPIO 是 3.3V。虽然多数 LCD1602 模块在 3.3V 下也能显示但为了稳定这里采用 4 线驱动模式数据线和控制线全部接 STM32 的 GPIO。引脚分配如下LCD1602 引脚接 STM32 引脚说明RSPA0寄存器选择ENPA1使能信号D4PA2数据线D5PA3数据线D6PA4数据线D7PA5数据线VSSGND电源地VDD5V电源正V010K 电位器中点对比度调节如果 LCD1602 模块自带 IIC 转接板也可以使用 PCF8574 方案只需两根 IIC 线即可驱动能节省 GPIO。本文以 4 线并口驱动为例因为能顺便讲解 STM32 的 GPIO 初始化和时序操作。4.3 按键与蜂鸣器电路按键采用四个独立轻触按键分别连接到 PB12-PB15。因为 STM32 的 GPIO 内部自带上拉电阻按键另一端接 GND所以配置为“上拉输入”检测到低电平时表示按键按下。蜂鸣器使用有源蜂鸣器模块信号端接 PB6电源端接 3.3V 或 5V 视模块而定。PB6 配置为推挽输出输出高电平时蜂鸣器鸣叫。如果使用无源蜂鸣器需要额外用 PWM 产生固定频率方波才能发声。4.4 电源设计整个系统中STM32 最小系统需要 3.3VLCD1602 和 ULN2003 需要 5V28BYJ-48 电机也需要 5V 供电。推荐使用 5V/2A 适配器作为总电源5V 直接给 LCD 和电机驱动板供电再通过 AMS1117-3.3 稳压芯片给 STM32 供电。需要注意的是28BYJ-48 在启动和正反转切换瞬间电流较大如果使用 USB 口供电可能会因为压降过大导致单片机复位。建议把电机电源与单片机电源分开或者在电机驱动板电源输入端并联 100uF 以上的电解电容。5. 软件开发环境搭建5.1 开发工具准备本项目使用以下软件环境工具说明Keil MDK 5嵌入式 C 语言开发环境STM32F1 系列芯片包Keil 中安装对应 Device Family PackSTM32 标准外设库V3.5 版本也可使用 HAL 库本文以标准库为例ST-Link Utility用于程序下载或 Flash 擦除ST-Link 下载器SWD 接口下载程序如果电脑上原来安装过 Keil 用于 51 单片机开发需要特别注意Keil C51 和 Keil MDK 可以安装在同一目录下但安装顺序建议先装 C51再装 MDK或者分别安装在不同目录。编译 STM32 工程时确认工程使用的是 MDK 的 ARM 编译器而不是 C51 编译器。5.2 标准外设库工程模板STM32 标准外设库工程通常包含以下目录Project/ ├── User/ │ ├── main.c │ ├── stm32f10x_it.c │ ├── motor.c │ ├── motor.h │ ├── lcd1602.c │ └── lcd1602.h ├── Libraries/ │ ├── CMSIS/ │ └── STM32F10x_Std_Driver/ ├── Drivers/ │ └── ... └── Output/如果不想手动搭建也可以直接在网上寻找“STM32F103C8T6 标准库模板”下载然后把本文中的 motor.c、lcd1602.c 等文件复制进去替换 main.c 即可。5.3 芯片包安装在 Keil 中新建 STM32 工程时如果找不到 STM32F103C8说明没有安装对应的 Device Family Pack。可以在 Keil 的 Pack Installer 中搜索“STM32F1”选择适合的版本安装。如果官网下载速度慢也可以从国内镜像或网友分享的 Pack 包手动导入。6. 核心代码实现6.1 电机驱动模块 motor.h首先定义电机引脚和操作接口。为了让程序结构清晰我将电机控制独立成 motor 模块对外只暴露少量接口初始化、设置方向、设置速度、启动、停止、以及定时器中断服务函数。// 文件路径User/motor.h #ifndef __MOTOR_H #define __MOTOR_H #include stm32f10x.h #define MOTOR_PORT GPIOB #define MOTOR_PINS (GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3) typedef enum { MOTOR_STOP 0, MOTOR_CW, // 正转 MOTOR_CCW // 反转 } MotorDir; void Motor_GPIO_Init(void); void Motor_SetDir(MotorDir dir); void Motor_SetSpeed(uint8_t level); void Motor_Run(void); void Motor_Stop(void); void Motor_ISR_Handler(void); #endif6.2 电机驱动模块 motor.c下面是电机驱动的核心实现。我们使用 Timer2 定时产生中断每隔固定时间调用一次 Motor_ISR_Handler在中断中推进一步换相这样主循环不会被电机延时阻塞。// 文件路径User/motor.c #include motor.h // 四相八拍换相表 static const uint8_t step_table[8] { 0x08, 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09 }; static uint8_t step_index 0; static MotorDir motor_dir MOTOR_STOP; static uint8_t run_state 0; static uint8_t speed_level 2; static uint16_t step_period 8; // 每拍需要的定时器中断次数 static uint16_t tick_count 0; void Motor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin MOTOR_PINS; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(MOTOR_PORT, GPIO_InitStructure); Motor_Stop(); } void Motor_Stop(void) { GPIO_ResetBits(MOTOR_PORT, MOTOR_PINS); run_state 0; tick_count 0; step_index 0; } void Motor_SetDir(MotorDir dir) { if (dir MOTOR_CW || dir MOTOR_CCW) { motor_dir dir; } } void Motor_SetSpeed(uint8_t level) { if (level 1) { level 1; } if (level 3) { level 3; } speed_level level; // 假设定时器中断周期为 125us // 低档250us * 16 2ms 一拍 // 中档250us * 8 1ms 一拍 // 高档250us * 4 0.5ms 一拍 switch (speed_level) { case 1: step_period 16; break; case 2: step_period 8; break; case 3: step_period 4; break; default: step_period 8; break; } } void Motor_Run(void) { run_state 1; tick_count 0; } // 驱动一步根据方向更新换相表索引并输出电平 static void Motor_Step(void) { if (motor_dir MOTOR_CW) { step_index (step_index 1) 0x07; } else if (motor_dir MOTOR_CCW) { step_index (step_index 7) 0x07; } else { return; } uint16_t odr GPIOB-ODR ~(uint16_t)MOTOR_PINS; GPIOB-ODR odr | (uint16_t)(step_table[step_index] 0x0F); } // 由定时器中断周期性调用 void Motor_ISR_Handler(void) { if (!run_state) { return; } tick_count; if (tick_count step_period) { tick_count 0; Motor_Step(); } }代码中的 step_table 是最关键的部分。当我们把 PB0-PB3 按顺序输出 0x08、0x0C、0x04、0x06、0x02、0x03、0x01、0x09 时电机四相绕组会依次导通磁场旋转带动转子转动。反过来把换相索引递减电机就反转。6.3 LCD1602 显示模块LCD1602 的驱动代码比较固定。这里使用 4 线模式每次发送一个字节时先发高 4 位再发低 4 位。// 文件路径User/lcd1602.h #ifndef __LCD1602_H #define __LCD1602_H #include stm32f10x.h void LCD1602_Init(void); void LCD1602_Clear(void); void LCD1602_SetCursor(uint8_t row, uint8_t col); void LCD1602_ShowString(uint8_t row, uint8_t col, char *str); void LCD1602_ShowNumber(uint8_t row, uint8_t col, uint16_t num); #endif// 文件路径User/lcd1602.c #include lcd1602.h #include stdio.h #define LCD_RS_PORT GPIOA #define LCD_RS_PIN GPIO_Pin_0 #define LCD_EN_PORT GPIOA #define LCD_EN_PIN GPIO_Pin_1 #define LCD_D4_PORT GPIOA #define LCD_D4_PIN GPIO_Pin_2 #define LCD_D5_PORT GPIOA #define LCD_D5_PIN GPIO_Pin_3 #define LCD_D6_PORT GPIOA #define LCD_D6_PIN GPIO_Pin_4 #define LCD_D7_PORT GPIOA #define LCD_D7_PIN GPIO_Pin_5 #define LCD_DATA_PINS (GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5) static void LCD1602_Delay(uint32_t t) { while (t--) { __NOP(); } } static void LCD1602_SetRS(uint8_t level) { if (level) { GPIO_SetBits(LCD_RS_PORT, LCD_RS_PIN); } else { GPIO_ResetBits(LCD_RS_PORT, LCD_RS_PIN); } } static void LCD1602_SetEN(uint8_t level) { if (level) { GPIO_SetBits(LCD_EN_PORT, LCD_EN_PIN); } else { GPIO_ResetBits(LCD_EN_PORT, LCD_EN_PIN); } } // 将 nibble 低4位输出到 D4-D7 static void LCD1602_WriteNibble(uint8_t nibble) { uint16_t odr GPIOA-ODR ~(uint16_t)LCD_DATA_PINS; if (nibble 0x01) { odr | LCD_D4_PIN; } if (nibble 0x02) { odr | LCD_D5_PIN; } if (nibble 0x04) { odr | LCD_D6_PIN; } if (nibble 0x08) { odr | LCD_D7_PIN; } GPIOA-ODR odr; } static void LCD1602_WriteByte(uint8_t data) { LCD1602_WriteNibble(data 4); LCD1602_SetEN(1); LCD1602_Delay(20); LCD1602_SetEN(0); LCD1602_WriteNibble(data 0x0F); LCD1602_SetEN(1); LCD1602_Delay(20); LCD1602_SetEN(0); } static void LCD1602_WriteCmd(uint8_t cmd) { LCD1602_SetRS(0); LCD1602_WriteByte(cmd); LCD1602_Delay(100); } static void LCD1602_WriteData(uint8_t data) { LCD1602_SetRS(1); LCD1602_WriteByte(data); LCD1602_Delay(100); } void LCD1602_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin LCD_RS_PIN | LCD_EN_PIN | LCD_DATA_PINS; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); LCD1602_Delay(10000); // 初始化时序 LCD1602_WriteCmd(0x33); LCD1602_WriteCmd(0x32); LCD1602_WriteCmd(0x28); // 4位模式2行5x7点阵 LCD1602_WriteCmd(0x0C); // 显示开光标关 LCD1602_WriteCmd(0x06); // 写入后光标自动右移 LCD1602_WriteCmd(0x01); // 清屏 LCD1602_Delay(2000); } void LCD1602_Clear(void) { LCD1602_WriteCmd(0x01); LCD1602_Delay(2000); } void LCD1602_SetCursor(uint8_t row, uint8_t col) { uint8_t addr; if (row 0) { addr 0x00 col; } else { addr 0x40 col; } LCD1602_WriteCmd(0x80 | addr); } void LCD1602_ShowString(uint8_t row, uint8_t col, char *str) { LCD1602_SetCursor(row, col); while (*str) { LCD1602_WriteData(*str); str; } } void LCD1602_ShowNumber(uint8_t row, uint8_t col, uint16_t num) { char buf[8]; sprintf(buf, %d, num); LCD1602_ShowString(row, col, buf); }6.4 按键与蜂鸣器初始化按键和蜂鸣器代码比较简单。按键使用 PB12-PB15 上拉输入蜂鸣器使用 PB6 推挽输出。// 文件路径User/main.c 片段 #define KEY1_PIN GPIO_Pin_12 #define KEY2_PIN GPIO_Pin_13 #define KEY3_PIN GPIO_Pin_14 #define KEY4_PIN GPIO_Pin_15 #define KEY_PORT GPIOB #define BEEP_PIN GPIO_Pin_6 #define BEEP_PORT GPIOB static void Key_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin KEY1_PIN | KEY2_PIN | KEY3_PIN | KEY4_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(KEY_PORT, GPIO_InitStructure); } static void Beep_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin BEEP_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(BEEP_PORT, GPIO_InitStructure); GPIO_ResetBits(BEEP_PORT, BEEP_PIN); }6.5 定时器初始化Timer2 配置为 72MHz 时钟分频 72得到 1MHz 的计数频率自动重装载值为 125因此中断周期为 125us。在中断服务函数中先调用电机驱动函数再累加秒计时。// 文件路径User/main.c 片段 static void Timer2_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Prescaler 72 - 1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStructure.TIM_Period 125 - 1; TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_InitStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); } // 全局变量运行状态与时间 uint8_t run_flag 0; uint8_t dir_flag 1; // 1 正转0 反转 uint8_t speed_level 2; // 1 低2 中3 高 uint16_t set_time 30; // 设定搅拌时间 uint16_t remain_time 0; // 剩余时间 uint16_t second_counter 0; // 秒计时 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 电机换相 Motor_ISR_Handler(); // 秒计时 second_counter; if (second_counter 8000) { // 125us * 8000 1s second_counter 0; if (run_flag remain_time 0) { remain_time--; if (remain_time 0) { // 定时结束停止电机 Motor_Stop(); run_flag 0; Beep_Once(); } } } } }6.6 按键扫描与主函数按键扫描采用简单的轮询方式为了消除机械抖动检测到按下后延时一段时间再确认。这里按下低电平有效。// 文件路径User/main.c 片段 static void Key_Delay(void) { volatile uint32_t i; for (i 0; i 3000; i); } // 返回按键编号1-4无按键返回0 static uint8_t Scan_Key(void) { if (GPIO_ReadInputDataBit(KEY_PORT, KEY1_PIN) RESET) { Key_Delay(); if (GPIO_ReadInputDataBit(KEY_PORT, KEY1_PIN) RESET) { while (GPIO_ReadInputDataBit(KEY_PORT, KEY1_PIN) RESET); return 1; } } if (GPIO_ReadInputDataBit(KEY_PORT, KEY2_PIN) RESET) { Key_Delay(); if (GPIO_ReadInputDataBit(KEY_PORT, KEY2_PIN) RESET) { while (GPIO_ReadInputDataBit(KEY_PORT, KEY2_PIN) RESET); return 2; } } if (GPIO_ReadInputDataBit(KEY_PORT, KEY3_PIN) RESET) { Key_Delay(); if (GPIO_ReadInputDataBit(KEY_PORT, KEY3_PIN) RESET) { while (GPIO_ReadInputDataBit(KEY_PORT, KEY3_PIN) RESET); return 3; } } if (GPIO_ReadInputDataBit(KEY_PORT, KEY4_PIN) RESET) { Key_Delay(); if (GPIO_ReadInputDataBit(KEY_PORT, KEY4_PIN) RESET) { while (GPIO_ReadInputDataBit(KEY_PORT, KEY4_PIN) RESET); return 4; } } return 0; }按键功能定义如下KEY1启动/停止。空闲时按下启动运行中按下停止。KEY2切换正转/反转。KEY3设置搅拌时间每次增加 10 秒范围 10-90 秒超过 90 秒回到 10 秒。KEY4切换速度档位低/中/高循环。// 文件路径User/main.c 片段 static void Beep_Once(void) { GPIO_SetBits(BEEP_PORT, BEEP_PIN); Key_Delay(); Key_Delay(); GPIO_ResetBits(BEEP_PORT, BEEP_PIN); } static void Show_Status(void) { static uint8_t last_run 0xFF; static uint8_t last_speed 0xFF; static uint16_t last_time 0xFFFF; static uint8_t last_dir 0xFF; char str[20]; // 第一行运行状态和时间 if (run_flag ! last_run || remain_time ! last_time) { LCD1602_SetCursor(0, 0); LCD1602_ShowString(0, 0, State: ); if (run_flag) { LCD1602_ShowString(0, 7, RUN ); } else { LCD1602_ShowString(0, 7, STOP); } sprintf(str, T:%02d, remain_time); LCD1602_ShowString(0, 12, str); last_run run_flag; last_time remain_time; } // 第二行速度、方向、设定时间 if (speed_level ! last_speed || dir_flag ! last_dir || set_time ! last_time) { sprintf(str, Spd:%d Dir:%s, speed_level, dir_flag ? CW : CCW); LCD1602_ShowString(1, 0, str); sprintf(str, Set:%02d, set_time); LCD1602_ShowString(1, 12, str); last_speed speed_level; last_dir dir_flag; last_time set_time; } } int main(void) { uint8_t key; // 初始化外设 Motor_GPIO_Init(); LCD1602_Init(); Key_GPIO_Init(); Beep_GPIO_Init(); Timer2_Init(); Motor_SetDir(motor_dir MOTOR_CW ? MOTOR_CCW : MOTOR_CW); // 默认正转 Motor_SetDir(MOTOR_CW); Motor_SetSpeed(speed_level); LCD1602_ShowString(0, 0, Stir Control); LCD1602_ShowString(1, 0, System Ready); Key_Delay(); LCD1602_Clear(); while (1) { key Scan_Key(); switch (key) { case 1: // 启动/停止 if (!run_flag) { remain_time set_time; run_flag 1; Motor_SetDir(dir_flag ? MOTOR_CW : MOTOR_CCW); Motor_Run(); Beep_Once(); } else { Motor_Stop(); run_flag 0; Beep_Once(); } break; case 2: // 正反转切换 dir_flag !dir_flag; Motor_SetDir(dir_flag ? MOTOR_CW : MOTOR_CCW); Beep_Once(); break; case 3: // 时间设置 set_time 10; if (set_time 90) { set_time 10; } if (!run_flag) { remain_time set_time; } Beep_Once(); break; case 4: // 速度档位切换 speed_level; if (speed_level 3) { speed_level 1; } Motor_SetSpeed(speed_level); Beep_Once(); break; default: break; } // 定时结束自动停止后复位状态显示 Show_Status(); } }这段主程序的逻辑并不复杂核心点在于把“电机换相”放到定时器中断里执行主循环只处理按键和显示。这样做的好处是按键扫描和 LCD 刷新不会阻塞电机运转即使按键按下后有短暂消抖延时电机依然能保持稳定转动。6.7 代码编译下载在 Keil MDK 中完成编译后使用 ST-Link 下载器连接 SWD 接口。接线方式为SWDIO、SWCLK、GND、3V3 四根线。打开 ST-Link Utility 可以查看芯片 ID确认连接正常后再下载。如果下载失败可以检查BOOT0 是否接地接线是否过长目标板是否独立供电是否需要在 Keil 中把下载算法选择为 STM32F10x Med-density Flash。7. 运行与验证7.1 功能测试步骤硬件接线和程序下载完成后按以下顺序验证上电后LCD 第一行显示“Stir Control”第二行显示“System Ready”片刻后进入主界面。按下 KEY3 设置搅拌时间观察 LCD 第二行“Set:”后面数字按 10 秒递增。按下 KEY4 切换速度档位观察“Spd:”后面数字在 1、2、3 之间循环。按下 KEY2 切换方向观察“Dir:”在 CW正转和 CCW反转之间切换。按下 KEY1 启动电机在低速档位下可以听到步进电机内部齿轮转动的声音输出轴缓慢旋转。运行过程中观察倒计时到 0 秒时电机自动停止蜂鸣器短鸣一声。7.2 预期转速参考在本文参数下低、中、高三档对应的换相周期分别为 2ms、1ms、0.5ms。由于输出轴每圈需要约 4096 个半步脉冲对应转速约为低档1 / (0.002 × 4096) ≈ 0.122 圈/秒约 7.3 转/分钟中档1 / (0.001 × 4096) ≈ 0.244 圈/秒约 14.6 转/分钟高档1 / (0.0005 × 4096) ≈ 0.488 圈/秒约 29.3 转/分钟。如果觉得转速偏高或偏低可以调整 Timer2 的中断周期或者在 Motor_SetSpeed 中修改 step_period 的取值。例如想要更低速可以把高档换相周期改为 8对应约 7.3 转/分钟。8. 常见问题与排查思路8.1 电机不转问题现象常见原因解决思路电机不转但 LCD 正常电机电源未接检查 28BYJ-48 红色线是否接 5V 电源电机不转电机温热换相表顺序或 GPIO 引脚不对检查 PB0-PB3 与 ULN2003 IN1-IN4 的对应关系电机不转LCD 花屏单片机复位或电源不稳定用独立 5V 电源给电机供电并联大电容电机只抖动不旋转驱动电流不足或延时过短检查 5V 电源电流降低速度档位排查时先确认 uln2003 板的电源指示灯是否点亮再用万用表测量 28BYJ-48 的电源线是否为 5V。如果这些正常可以在主循环中手动调用 Motor_Step 并加长延时观察电机是否一步一步转动从而判断是换相表问题还是中断配置问题。8.2 步进电机过热28BYJ-48 长时间通电时四相绕组持续导通即使电机不转也会发热。这是正常现象但如果温度过高需要检查是否长时间在高速档空转换相表是否有错误导致绕组长期上电电源电压是否偏高是否缺少散热条件。在停止状态下程序调用 Motor_Stop 会把 PB0-PB3 全部拉低此时所有绕组断电电机不会发热。如果停止后电机仍然发烫可以检查 GPIO 输出电平是否正常。8.3 LCD1602 无显示或乱码问题现象常见原因解决思路LCD 无背光VSS/VDD 接反或背光跳线未接检查 5V 和 GND 接线LCD 有背光无字符对比度电压不对调节 V0 电位器LCD 显示乱码初始化时序不对或数据线接错检查 D4-D7 是否对应 PA2-PA5LCD 首次正常重启后乱码电源上电时序不稳定在初始化前增加延时LCD1602 初始化时需要注意发送 0x33、0x32 的等待时间要足够否则模块可能没有正确进入 4 位模式。在工程中如果发现第一次上电正常复位后乱码通常是因为延时不够可以增大 LCD1602_Delay 的时间。8.4 程序下载失败程序下载失败时优先检查 ST-Link 驱动是否安装。Windows 设备管理器中能看到 ST-Link 设备才说明连接正常。Keil 中也要在 Options for Target - Debug 中选择 ST-Link Debugger并点击 Settings 确认能识别到 SW Device。如果遇到“No target connected”报错可以把目标板断电后重新上电再尝试下载。8.5 按键失灵或误触发按键没有按下却触发通常是 GPIO 没有配置为内部上拉输入或者按键引脚接触不良。如果按键按下去没反应首先要确认按键另一端是否接地。由于 STM32 内部上拉电阻有限按键连接线较长时容易受干扰可以在按键两端并联一个 0.1uF 电容。9. 工程建议与扩展优化9.1 加入加减速曲线在搅拌机实际运行中如果负载惯量较大电机突然从静止切换到高速很容易出现“丢步”现象表现为电机发出异响输出轴不转或转速不均。解决办法是加入梯形加减速控制启动时从低速开始每若干步提高一次速度停止前再逐步降低速度。实现思路如下在定时器中断中维护一个 current_speed启动阶段按加速度表递增速度离目标转速越近增量越小停止时先减速到低速再停止。代码上可以在 Motor_ISR_Handler 里根据当前剩余步数或时间动态调整 step_period。9.2 使用 A4988 驱动两相4线混合式步进电机如果课程设计需要用到“两相4线混合式步进电机”可以按以下方案调整把 28BYJ-48 和 ULN2003 替换为 42 步进电机和 A4988 驱动模块A4988 的 DIR 引脚接 STM32 任意 GPIOSTEP 引脚接另一个 GPIO在定时器中断中不要调用换相表而是每收到一个脉冲将 STEP 引脚拉高再拉低形成一个上升沿A4988 的 MS1/MS2/MS3 引脚组合决定细分倍数。核心驱动代码比直接换相更简单但要注意 A4988 的供电电压与电机额定电压匹配并且 A4988 逻辑供电 VDD 接
延伸阅读

更多相关文章

2026/10/22 0:59:30

安卓模拟器安装APK全攻略:ADB命令、架构兼容与常见错误排查

很多第一次接触模拟器的人,都会有一个相似的困惑:别人在电脑上装个安卓模拟器,然后像装手机软件一样点两下就完事了。怎么轮到自己的时候,不是装完找不到图标,就是提示“安装失败”,要么就是模拟器本身启动…

2026/10/22 6:28:07

双生火焰能量解读:用情绪检测与认知失真实战走出断联期

最近收到一些正在经历断联或冷战的朋友反馈,说情绪像过山车一样:前一秒觉得自己已经彻底放下,后一秒又因为一个念头、一张旧照片瞬间被拽回原处。尤其在 8.12 这种能量节点前后,很多人会明显感到波动加剧,一边是情绪反…

2026/10/21 6:19:20

C++音视频流媒体开发实战:从FFmpeg到SRS的完整链路

这次直接聊一个很多开发者问过的问题:C 音视频流媒体开发到底该怎么学、怎么验证。网上零散资料很多,但大多数教程把 FFmpeg、H264、RTMP、RTSP、WebRTC 这些概念拆开讲,缺少一条能串起来的实战路径。这篇文章就把这套技术栈从原理到落地完整…

2026/10/23 21:16:51

MATLAB卡尔曼滤波实现飞行器轨迹预测与雷达目标跟踪实战

简介:卡尔曼滤波在含噪声传感器数据处理与运动轨迹预测中的应用非常广泛。该MATLAB项目聚焦飞行物体运动轨迹预测,面向数字信号处理、导航与控制方向的初学者及研究者,帮助理解线性动态系统建模、状态估计与滤波更新流程。压缩包共6个文件、1…

2026/10/23 21:16:51

HAWC2_Matlab_tools:风力机气弹仿真后处理工具集实战

简介:HAWC2_Matlab_tools 是一套面向风力机气动弹性仿真从业者与科研人员的 MATLAB 工具集合,主要用于 HAWC2 输入文件与计算结果的前处理和后处理。HAWC2 由丹麦技术大学(DTU)风能部门开发,在风力机载荷与气弹分析领域…

2026/10/23 21:16:51

LINGO建模实战:从集合语法到求解器选型与灵敏度分析

简介:LINGO(线性与非线性全局优化器)是一款功能强大的数学建模优化软件,本资源提供其破解安装版本,面向工程、经济、金融、运筹学等领域的研究者、工程师及建模竞赛参与者,用于求解线性、非线性与整数规划问…

2026/10/23 21:16:51

从零到实战:Linux内核系统学习路径与专栏总目录设计

1. 为什么我决定开一个Linux内核专栏说实话,每次有人问我“内核到底怎么学”,我都有点不知道从哪回答。不是没得说,而是能说的太多,反而不知道该先讲哪个。断断续续做了这么多年内核相关的工作,从最早被内核日志里那些…

2026/10/23 21:16:50

Marc多平面剖切技术在工程仿真中的应用与技巧

1. 多平面剖切技术概述在工程仿真领域,Marc作为一款专业的非线性有限元分析软件,其强大的后处理功能对于结果解读至关重要。多平面剖切技术允许工程师从任意角度对三维模型进行虚拟切割,观察内部应力、应变等物理量的分布情况。这项功能在压力…

2026/10/23 21:11:50

信号检测与估计核心概念解析:从理论到工程实践

简介:这份资料是电子科技大学《信号检测与估计理论与应用》关欣译教材的配套课后答案,面向通信工程、信号处理等专业的学生与自学者,帮助在噪声背景下理解信号检测与参数估计的核心方法,巩固课堂所学并排查知识盲点。压缩包共95个…

2026/10/22 11:29:28

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/23 0:02:26

Verilog实现FPGA密码锁:矩阵键盘与状态机设计实战

说实话,这几年接触了不少想入门FPGA的朋友,如果你问哪个项目能一口气把Verilog的核心基本功串起来,我通常会推荐电子密码锁。这个项目看着不起眼,却把矩阵键盘扫描、按键消抖、状态机设计、动态显示刷新、外设控制这些数字逻辑里的…

2026/10/23 0:02:26

数据服务监控运维实战:从指标体系到告警治理与故障排查

周末凌晨两点,手机震了。第一反应是“数据服务挂了”。打开电脑看了下,果然,某张核心报表的数据产出时间已经超过SLA两小时,下游三个业务方连着发来问询。这种场景,做过大数据运维的人都不会陌生。数据服务的监控与运维…

2026/10/23 0:02:26

FreeRTOS任务通知实战详解:内核原理、API与避坑指南

裸机编程切到RTOS之后,任务之间的通信和同步永远是躲不开的坎。前面聊过队列、信号量、互斥锁、事件组,这次把FreeRTOS任务通知(Task Notification)单独拎出来讲透。原因很简单:任务通知是FreeRTOS里最容易用错、也最容…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑