十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32步进电机搅拌机控制:从驱动原理到完整代码实现

STM32步进电机搅拌机控制:从驱动原理到完整代码实现 做搅拌机、混料机这类小型设备控制时很多工程师第一反应是“让电机转起来就行”。实际做下去才发现“能转”和“可控”之间隔着一段不小的距离转速一调就抖动方向切完就丢步按键按下去程序直接卡死。这些问题不是单片机性能不够而是驱动方式和软件结构出了问题。这篇文章以“基于STM32单片机步进电机控制搅拌机控制”为主线从步进电机选型讲到完整的搅拌机控制代码实现。读完你不仅能把一台搅拌机样机跑起来还能理解电机控制项目的一套通用组织方式用定时器产生步进节拍用状态机管理运行逻辑把硬件驱动和交互逻辑拆开写。这套方法可以直接迁移到舵机、传送带、数控滑台等其他位置控制场景。先给结论STM32 步进电机非常适合中小型搅拌设备其核心优势是转速稳定、启停准确、控制成本低不需要像伺服系统那样配备昂贵的编码器和驱动器。项目成败的关键不是单片机选型而是脉冲节拍的生成方式和设备状态机的设计。1. 这篇文章真正要解决的问题1.1 步进电机搅拌机控制的应用场景步进电机在搅拌类设备中很常见典型场景包括实验室小型搅拌器要求转速稳定、能正反转交替完成试剂混合。家用食品搅拌机演示样机要求定时搅拌、多档速度切换。化工/涂料小型混料机要求低速大扭矩且能在任意位置准确停止。学生竞赛/课程设计要求使用 STM32 完成电机调速、按键控制和状态显示。这类设备对电机控制的要求和普通直流电机有明显区别。直流电机调速简单但无法精确控制转过的角度带载后转速会下降步进电机是开环位置控制器件脉冲个数决定转角脉冲频率决定转速在普通搅拌负载下几乎不会丢步。1.2 很多新手踩过的坑和步进电机打过交道的人基本都经历过下面几个问题问题根因后续影响用HAL_Delay控制步进节拍延时期间 CPU 被完全占用按键扫描失效、显示刷新卡顿切换方向后脉冲序列不连续相位状态没有按方向反向推进电机抖动、丢步严重时堵转停止时只停定时器不断电电机线圈仍处于通电锁止状态电机发热驱动板发热所有逻辑堆在 while 循环里状态分支混乱速度档位难维护现场调试困难代码不可移植这篇文章会把上面这些坑挨个拆开。你要得到的不是一个“转就行”的演示程序而是一个可以继续扩展的搅拌机控制框架。1.3 什么样的读者适合读正在做 STM32 课程设计、毕业设计主题是步进电机或搅拌机控制。刚接触步进电机想弄懂四相八拍、定时器调速、正反转控制的完整原理。已经在用延时方式驱动电机但发现程序响应慢、难扩展想改成定时器中断驱动。想了解 A4988、ULN2003 等驱动方案的选型和接线。如果你只是想要一段“看起来能转”的代码网上有很多。但这篇文章的目标是让你理解代码为什么这么写以及出问题时怎么定位。2. 步进电机与搅拌机控制的核心原理2.1 步进电机为什么能精确转动步进电机内部有多个绕组线圈按顺序给不同线圈通电转子就会跟着磁场方向转过一个固定角度这个角度叫步距角。以常见的 28BYJ-48 步进电机为例它的内部步距角是 5.625°经过约 1:64 的减速齿轮后输出轴每步实际转动约 0.0879°也就是转一圈大约需要 4096 个脉冲。这个特性非常关键只要精确控制脉冲个数就能精确控制输出轴转过的角度。在搅拌机控制中即使不需要精确定位步进电机也能保证转过的圈数和预设值一致这对配方混料类设备很重要。2.2 整步、半步与细分两相步进电机最基本的驱动方式是四相四拍也就是每次只给一相通电循环 A → B → C → D。这种方式一圈的步数少但转矩波动大。实际工程更多使用四相八拍也就是上表中的状态序列状态编号ABCD0100011100201003011040010500116000171001八拍方式的半步模式相当于把每一步拆成两次通电状态电机运行的平稳性明显好于整步模式。搅拌机这种需要连续旋转且对抖动敏感的设备更适合用八拍方式。如果是 42 步进电机搭配 A4988 驱动板则可以通过 MS1/MS2/MS3 引脚设置细分倍数比如 1、1/2、1/4、1/8、1/16。细分不是把电机做得更精密而是让每个脉冲对应的角度变小从而减小振动和噪声。2.3 搅拌机控制真正的难点搅拌机控制真正的难点不在于让电机转起来而在于处理几种常见工况低速启动搅拌物料黏稠时电机启动扭矩不足需要从较低频率启动再逐步提速。正反转切换正转切反转时如果脉冲序列没有连续变化电机会抖动甚至丢步。急停响应设备出现卡料时必须立刻停止电机并断开线圈通电避免发热烧毁。速度档位切换切换速度时应该平滑调整脉冲周期避免瞬间跳变影响搅拌效果。本文的代码框架会围绕这几个工况设计。3. 系统选型与硬件准备3.1 两种驱动方案对比根据项目需求和预算步进电机驱动有两种主流方案对比项方案一28BYJ-48 ULN2003方案二42/57 步进电机 A4988优点便宜、接线简单、5V 供电扭矩大、细分可调、噪声低缺点扭矩小、噪声大、不适合重负载需要 12V 或 24V 电源接线稍复杂适合场景课程设计、小功率搅拌演示实际小型设备、带负载搅拌控制方式GPIO 直接输出八拍序列STEP/DIR/EN 脉冲控制转一圈步数约 4096 步内部约1:64减速整步200步细分后按倍数增加如果只是入门验证方案一成本最低本文核心代码按方案一实现。如果要做接近实际工程的产品建议把代码迁移到 A4988 方案后面我也会解释迁移思路。3.2 硬件清单本文演示使用如下硬件STM32F103C8T6 最小系统板28BYJ-48 步进电机ULN2003 步进电机驱动板LCD1602 液晶显示屏4线模式3 个独立按键启动/停止、速度切换、方向切换1 个限位开关或急停开关蜂鸣器模块ST-Link 下载器和若干杜邦线3.3 接线说明将步进电机驱动板的 IN1~IN4 连接到 STM32 的 PA0~PA3公共端接 5V。驱动板供电建议单独接 5V 电源不要直接从 STM32 板载 5V 取大电流否则 USB 供电容易掉电复位。模块引脚STM32 引脚ULN2003 IN1PA0ULN2003 IN2PA1ULN2003 IN3PA2ULN2003 IN4PA3按键 KEY1PB0按键 KEY2PB1按键 KEY3PB2限位/急停PB3LCD1602 RS/RW/ENPD0/PD1/PD2LCD1602 D4~D7PD4~PD7蜂鸣器PB10接线时要注意独立按键一端接 STM32 的 GPIO另一端接 GNDGPIO 内部开启上拉按下时读到低电平。这是最常用的按键接法。4. 工程初始化与基础配置4.1 使用 STM32CubeMX 创建工程推荐使用 STM32CubeMX 生成初始化代码能减少底层寄存器配置错误。创建工程时选择芯片 STM32F103C8T6时钟配置为外部晶振 8MHz系统主频 72MHz。如果找不到芯片请先安装对应的 STM32F1 系列支持包。时钟树配置要点HSE 选择 Crystal/Ceramic Resonator。PLL 倍频到 72MHz。APB1 总线时钟设为 36MHz定时器时钟为 72MHz。4.2 引脚分配在 CubeMX 的 Pinout 视图里按下表分配引脚引脚模式说明PA0 ~ PA3GPIO Output步进电机四相控制PB0 ~ PB2GPIO Input按键输入上拉PB3GPIO Input限位/急停输入下拉PA9USART1_TX调试串口PA10USART1_RX调试串口PD0 ~ PD2GPIO OutputLCD1602 控制引脚PD4 ~ PD7GPIO OutputLCD1602 数据引脚PC13GPIO Output运行状态 LEDPB10GPIO Output蜂鸣器4.3 定时器配置本文核心调速方案是使用定时器中断每一次中断输出一个步进节拍。通过修改定时器的自动重装载值ARR来改变中断频率从而改变电机转速。在 CubeMX 中启用 TIM3Clock Source 选择 Internal Clock。Prescaler 设为 71那么计数时钟为 72MHz / 72 1MHz即 1 微秒计数一次。Counter Period 先设为 999对应 1 毫秒中断一次之后在代码中动态修改。生成的定时器初始化代码大致如下/* 文件路径Core/Src/tim.c */ static void MX_TIM3_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; htim3.Instance TIM3; htim3.Init.Prescaler 71; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 999; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(htim3) ! HAL_OK) { Error_Handler(); } // 其余时钟源和主输出配置... }4.4 生成工程并整理目录生成工程后建议在项目里新建App目录放入step_motor.c、lcd1602.c等业务模块文件。这样做的目的是把 STM32 底层初始化代码和业务逻辑分开后续迁移到其他芯片时只需要改底层引脚配置不需要重写控制逻辑。5. 步进电机驱动代码完整实现5.1 电机驱动模块头文件首先定义电机驱动的对外接口。后续控制逻辑只调用这些接口不直接操作 GPIO这样代码层级清晰。/* 文件路径App/step_motor.h */ #ifndef __STEP_MOTOR_H #define __STEP_MOTOR_H #include main.h /* 电机四相引脚定义按 CubeMX 实际引脚修改 */ #define MOTOR_IN1_PORT GPIOA #define MOTOR_IN1_PIN GPIO_PIN_0 #define MOTOR_IN2_PORT GPIOA #define MOTOR_IN2_PIN GPIO_PIN_1 #define MOTOR_IN3_PORT GPIOA #define MOTOR_IN3_PIN GPIO_PIN_2 #define MOTOR_IN4_PORT GPIOA #define MOTOR_IN4_PIN GPIO_PIN_3 /* 速度档位对应定时器装载值数值越小脉冲周期越短转速越快 */ #define SPEED_SLOW_ARR 2000 #define SPEED_MEDIUM_ARR 1000 #define SPEED_FAST_ARR 400 typedef enum { MOTOR_STOP 0, MOTOR_FORWARD, MOTOR_BACKWARD } MotorDir; void StepMotor_Init(void); void StepMotor_SetSpeed(uint32_t arrValue); void StepMotor_SetDir(MotorDir dir); void StepMotor_Start(void); void StepMotor_Stop(void); void StepMotor_Tick(void); #endif5.2 电机驱动模块源文件在step_motor.c里实现四相八拍驱动。核心变量stepIndex表示当前相位状态motorDir表示方向motorSpeed保存当前定时器装载值。/* 文件路径App/step_motor.c */ #include step_motor.h /* 四相八拍控制序列 */ static const uint8_t stepSeq[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 stepIndex 0; static volatile MotorDir motorDir MOTOR_STOP; static volatile uint32_t motorSpeed SPEED_MEDIUM_ARR; void StepMotor_Init(void) { HAL_GPIO_WritePin(MOTOR_IN1_PORT, MOTOR_IN1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN2_PORT, MOTOR_IN2_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN3_PORT, MOTOR_IN3_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN4_PORT, MOTOR_IN4_PIN, GPIO_PIN_RESET); stepIndex 0; } void StepMotor_SetSpeed(uint32_t arrValue) { if (arrValue 10) { arrValue 10; /* 避免ARR过小导致中断频率过高 */ } motorSpeed arrValue; __HAL_TIM_SET_AUTORELOAD(htim3, arrValue - 1); } void StepMotor_SetDir(MotorDir dir) { motorDir dir; } void StepMotor_Start(void) { __HAL_TIM_SET_COUNTER(htim3, 0); HAL_TIM_Base_Start_IT(htim3); } void StepMotor_Stop(void) { HAL_TIM_Base_Stop_IT(htim3); /* 停止后必须断开所有线圈否则电机保持在锁止状态会发热 */ HAL_GPIO_WritePin(MOTOR_IN1_PORT, MOTOR_IN1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN2_PORT, MOTOR_IN2_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN3_PORT, MOTOR_IN3_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN4_PORT, MOTOR_IN4_PIN, GPIO_PIN_RESET); } /* 定时器中断中调用每调用一次就走一个步进节拍 */ void StepMotor_Tick(void) { if (motorDir MOTOR_FORWARD) { stepIndex (stepIndex 1) % 8; } else if (motorDir MOTOR_BACKWARD) { /* 减1并取模8相当于反向推进 */ stepIndex (stepIndex 7) % 8; } else { return; } HAL_GPIO_WritePin(MOTOR_IN1_PORT, MOTOR_IN1_PIN, stepSeq[stepIndex][0] ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN2_PORT, MOTOR_IN2_PIN, stepSeq[stepIndex][1] ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN3_PORT, MOTOR_IN3_PIN, stepSeq[stepIndex][2] ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN4_PORT, MOTOR_IN4_PIN, stepSeq[stepIndex][3] ? GPIO_PIN_SET : GPIO_PIN_RESET); }这里最容易理解错的地方是反转的实现。很多人会单独写一套反转序列其实不需要只要让数组下标反向推进即可。(stepIndex 7) % 8等于加 7 减 1正好实现反向走一步。5.3 定时器中断回调在main.c中重写定时器中断回调函数。当 TIM3 向上计数溢出时会触发中断并调用HAL_TIM_IRQHandler最终进入这个回调/* 文件路径Core/Src/main.c */ /* USER CODE BEGIN 0 */ extern TIM_HandleTypeDef htim3; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { StepMotor_Tick(); } } /* USER CODE END 0 */建议不要直接改stm32f1xx_it.c里的中断处理代码而是让 CubeMX 生成的TIM3_IRQHandler调用HAL_TIM_IRQHandler然后由 HAL 库回调到main.c。这样工程升级、重新生成代码时改动最少。5.4 速度控制原理设定时器预分频为 71则计数时钟为 1MHz。如果 ARR 为 1000则定时器每 1000 微秒产生一次中断步进电机每 1000 微秒走一步。对 28BYJ-48 来说走完一圈需要 4096 步所以周期约 4.096 秒转速约为每分钟 14.6 转。从这个换算可以看出ARR 越小脉冲频率越高转速越快。ARR 越大转速越低但扭矩越难保证。搅拌机启动时建议从较大 ARR 开始再逐渐减小实现软启动。6. 搅拌机控制逻辑与按键交互6.1 控制状态机搅拌机的运行状态可以抽象成三个状态停止、正转、反转。状态之间的切换由按键触发异常触发则直接进入停止。/* 文件路径App/mixer_control.c */ #include step_motor.h #include mixer_control.h typedef enum { STATE_STOP 0, STATE_RUNNING } RunState; static RunState runState STATE_STOP; static MotorDir curDir MOTOR_FORWARD; static uint8_t speedLevel 1; /* 0低 1中 2高 */ static const uint32_t speedTable[3] { SPEED_SLOW_ARR, SPEED_MEDIUM_ARR, SPEED_FAST_ARR }; void Mixer_Init(void) { StepMotor_Init(); StepMotor_SetSpeed(speedTable[speedLevel]); StepMotor_SetDir(curDir); runState STATE_STOP; } void Mixer_ToggleRun(void) { if (runState STATE_STOP) { StepMotor_SetDir(curDir); StepMotor_Start(); runState STATE_RUNNING; } else { StepMotor_Stop(); runState STATE_STOP; } } void Mixer_NextSpeed(void) { speedLevel (speedLevel 1) % 3; StepMotor_SetSpeed(speedTable[speedLevel]); } void Mixer_SwitchDir(void) { if (curDir MOTOR_FORWARD) { curDir MOTOR_BACKWARD; } else { curDir MOTOR_FORWARD; } if (runState STATE_RUNNING) { StepMotor_SetDir(curDir); } } void Mixer_EmergencyStop(void) { StepMotor_Stop(); runState STATE_STOP; } uint8_t Mixer_IsRunning(void) { return (runState STATE_RUNNING) ? 1 : 0; } MotorDir Mixer_GetDir(void) { return curDir; } uint8_t Mixer_GetSpeedLevel(void) { return speedLevel; }这套状态机的好处是业务代码只关心“启动”“停止”“切速”“换向”四个动作不关心电机底层怎么输出脉冲。即使以后换成 A4988 方案也只需要改step_motor.c的内部实现。6.2 按键扫描与消抖按键扫描放在主循环中使用延时消抖。对于演示项目这是最简单可靠的方式但要注意按键等待释放的while循环不要设计成长时间卡死。更完善的方案是使用定时器周期性扫描按键这里先给出常规写法/* 文件路径Core/Src/main.c */ /* USER CODE BEGIN 4 */ void Key_Scan(void) { /* KEY1启动/停止 */ if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) GPIO_PIN_RESET) { HAL_Delay(20); if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) GPIO_PIN_RESET) { Mixer_ToggleRun(); while (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) GPIO_PIN_RESET) { } } } /* KEY2速度档位切换 */ if (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) GPIO_PIN_RESET) { HAL_Delay(20); if (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) GPIO_PIN_RESET) { Mixer_NextSpeed(); while (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) GPIO_PIN_RESET) { } } } /* KEY3方向切换 */ if (HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin) GPIO_PIN_RESET) { HAL_Delay(20); if (HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin) GPIO_PIN_RESET) { Mixer_SwitchDir(); while (HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin) GPIO_PIN_RESET) { } } } }6.3 LCD1602 显示与蜂鸣器提示LCD1602 按 4 线模式接入RS、RW、EN 分别控制命令/数据选择、读写选择、使能信号。数据引脚只用 D4~D7。下面给出精简的四线驱动代码/* 文件路径App/lcd1602.c */ #include lcd1602.h static void LCD_Delay(void) { for (volatile uint32_t i 0; i 100; i) { __NOP(); } } static void LCD_WriteNibble(uint8_t nibble) { HAL_GPIO_WritePin(LCD_D_PORT, LCD_D4_PIN, (nibble 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D_PORT, LCD_D5_PIN, (nibble 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D_PORT, LCD_D6_PIN, (nibble 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_D_PORT, LCD_D7_PIN, (nibble 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_EN_PORT, LCD_EN_PIN, GPIO_PIN_SET); LCD_Delay(); HAL_GPIO_WritePin(LCD_EN_PORT, LCD_EN_PIN, GPIO_PIN_RESET); LCD_Delay(); } static void LCD_WriteCmd(uint8_t cmd) { HAL_GPIO_WritePin(LCD_RS_PORT, LCD_RS_PIN, GPIO_PIN_RESET); LCD_WriteNibble(cmd 4); LCD_WriteNibble(cmd 0x0F); } static void LCD_WriteData(uint8_t data) { HAL_GPIO_WritePin(LCD_RS_PORT, LCD_RS_PIN, GPIO_PIN_SET); LCD_WriteNibble(data 4); LCD_WriteNibble(data 0x0F); } void LCD_Init(void) { HAL_Delay(50); LCD_WriteNibble(0x03); LCD_Delay(); LCD_WriteNibble(0x03); LCD_Delay(); LCD_WriteNibble(0x03); LCD_Delay(); LCD_WriteNibble(0x02); LCD_WriteCmd(0x28); LCD_WriteCmd(0x0C); LCD_WriteCmd(0x01); HAL_Delay(5); LCD_WriteCmd(0x06); } void LCD_Clear(void) { LCD_WriteCmd(0x01); HAL_Delay(5); } void LCD_SetCursor(uint8_t row, uint8_t col) { uint8_t addr (row 0) ? (0x80 col) : (0xC0 col); LCD_WriteCmd(addr); } void LCD_WriteString(char *str) { while (*str) { LCD_WriteData((uint8_t)(*str)); str; } }显示状态更新函数可以放在主循环中每隔一段时间刷新一次避免频繁写屏影响 LCD 寿命void LCD_ShowMixerStatus(void) { char buffer[17]; LCD_SetCursor(0, 0); if (Mixer_IsRunning()) { LCD_WriteString(Mix: RUNNING); } else { LCD_WriteString(Mix: STOP ); } LCD_SetCursor(0, 8); if (Mixer_GetDir() MOTOR_FORWARD) { LCD_WriteString(FWD); } else { LCD_WriteString(REV); } LCD_SetCursor(1, 0); sprintf(buffer, Speed: %d, Mixer_GetSpeedLevel() 1); LCD_WriteString(buffer); }6.4 急停保护逻辑限位开关或急停开关接 PB3设置为下拉输入触发时输入高电平。在主循环中定时检测一次一旦触发立刻停止电机并关闭线圈void Safety_Check(void) { if (HAL_GPIO_ReadPin(LIMIT_GPIO_Port, LIMIT_Pin) GPIO_PIN_SET) { Mixer_EmergencyStop(); HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_SET); HAL_Delay(200); HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_RESET); } }急停检测不要放在定时器中断里做因为中断函数要尽量短。放在主循环足够响应一般的机械急停需求如果需要毫秒级响应可以用外部中断 EXTI。7. 运行验证与调试方法7.1 编译与下载使用 Keil MDK 打开生成的工程选择 ST-Link 下载器在 Options for Target 里配置 Debug 为 ST-Link Debugger然后点击编译下载。确认下载成功后给电机驱动板接上独立 5V 电源再给 STM32 上电。蜂鸣器响一声说明初始化完成按键切换到启动后电机应该开始旋转。7.2 现象验证建议按以下顺序验证功能按下 KEY1电机开始旋转。按下 KEY2转速明显变化
返回列表