简介:这份资源是一套面向嵌入式初学者与进阶开发者的STM32步进电机控制项目例程,重点解决如何通过手机APP经蓝牙串口与STM32通讯、进而远程驱动步进电机的问题,适用于智能家居、自动化设备等需要精确位置控制的场景。压缩包为rar格式,共479个文件,约71.95MB,以c与h源码、o目标文件、crf交叉引用文件为主,另含少量chm文档、uvproj工程文件及map、lst等编译产物,覆盖主函数、蓝牙通信、步进电机驱动、定时器配置与命令解析等模块。目前已有838人学习下载。读者可从中获取完整的串口通讯协议栈实现、定时器PWM脉冲生成与中断控制思路、电机正反转与调速命令解析逻辑,以及可直接编译的Keil工程结构,便于快速上手STM32外设配置与无线控制方案。
1. 手机蓝牙控步进电机:这套 STM32 串口通讯例程到底能跑通什么
手上有个 42 步进电机、一块 STM32 最小系统板、一个 HC-05 蓝牙模块,想用手机 APP 发指令让电机正反转、调速、定步数——这个场景在工控 demo、小型自动化装置、教学实验里反复出现。但真正动手时,卡人的往往不是电机本身,而是三件事:串口中断怎么收不定长指令、定时器怎么输出稳定脉冲、手机端和 MCU 端的协议怎么对齐。这套例程就是冲着这三个问题来的,它把步进电机驱动、串口蓝牙通讯、定时器脉冲生成揉在一个工程里,手机 APP 通过蓝牙串口发字符串指令,STM32 解析后控制电机动作。适合刚接触 STM32 外设联动的新手照着复现,也适合熟手拿去当协议框架改。下面按“资源是什么 → 怎么用 → 坑在哪”推一遍。
2. 拆开工程看骨架:定时器脉冲、串口中断、电机驱动怎么串起来
2.1 为什么用定时器比较输出而不是软件延时
步进电机要转得稳,脉冲频率必须准。用HAL_Delay或for循环翻转 IO 的写法,一旦串口来数据、中断一打断,脉冲间隔就抖,电机立刻发出“咔咔”异响甚至丢步。常见做法是用一个通用定时器(比如 TIM2)配成 PWM 输出模式,或者用定时器更新中断里翻转 IO。前者硬件自动翻转,CPU 几乎不参与;后者灵活但中断频繁时占 CPU。这套例程走的是定时器中断翻转 IO 的路子,好处是脉冲数可控、方便计步,缺点是高频时中断压力大。
关键参数就两个:预分频器PSC和自动重装载值ARR。假设系统时钟 72MHz,想要 1kHz 脉冲:
// TIM2 初始化,72MHz 时钟,目标 1kHz 更新中断 htim2.Instance = TIM2; htim2.Init.Prescaler = 72 - 1; // 72 分频 -> 1MHz 计数频率 htim2.Init.Period = 1000 - 1; // 1000 个计数 -> 1kHz 中断 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(&htim2); HAL_TIM_Base_Start_IT(&htim2); // 开启更新中断PSC=71把 72MHz 降到 1MHz,ARR=999让计数器每 1000 个数溢出一次,溢出频率就是 1kHz。改速度就是改ARR:ARR越小频率越高、电机越快。注意ARR不能小于脉冲宽度要求,否则驱动器来不及响应。中断回调里翻转脉冲引脚并累加步数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1); // 脉冲引脚翻转 if (step_count > 0) { step_count--; if (step_count == 0) { HAL_TIM_Base_Stop_IT(&htim2); // 步数走完停脉冲 } } } }step_count是剩余步数,由串口指令写入。走完自动停定时器,避免电机一直转。方向引脚单独用普通 GPIO 控制,换向时先停脉冲再改方向,否则容易丢步。
2.2 串口中断收不定长指令:别用阻塞接收
手机 APP 发过来的指令长度不固定,比如SPD:500、MOV:200、DIR:CW,用HAL_UART_Receive阻塞收必须知道长度,不现实。正确姿势是开接收中断,每来一个字节进一次回调,攒够一帧再解析。常见做法是配一个接收缓冲区和索引:
uint8_t rx_buf[64]; uint8_t rx_idx = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { uint8_t ch = rx_buf[rx_idx]; if (ch == '\n' || rx_idx >= sizeof(rx_buf) - 1) { rx_buf[rx_idx] = '\0'; parse_command((char *)rx_buf); // 解析整帧 rx_idx = 0; } else { rx_idx++; } HAL_UART_Receive_IT(&huart1, &rx_buf[rx_idx], 1); // 重新挂接收 } }这里用换行符\n作为帧结束标志,手机端每条指令末尾加\n。rx_idx是写指针,收到结束符就把缓冲区当字符串解析,然后清零。注意每次回调末尾必须重新调用HAL_UART_Receive_IT,否则只收一个字节就停了——这是新手最常翻车的地方。缓冲区大小 64 够用,指令别超长,超了会截断。
2.3 指令解析:字符串比对还是状态机
指令格式简单时,strncmp比对前缀最直接:
void parse_command(char *cmd) { int val; if (strncmp(cmd, "SPD:", 4) == 0) { val = atoi(cmd + 4); if (val > 0 && val < 10000) { __HAL_TIM_SET_AUTORELOAD(&htim2, 1000000 / val - 1); // 改速度 } } else if (strncmp(cmd, "MOV:", 4) == 0) { step_count = atoi(cmd + 4); HAL_TIM_Base_Start_IT(&htim2); } else if (strncmp(cmd, "DIR:", 4) == 0) { if (cmd[4] == 'C') HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); else HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); } }SPD改ARR实现调速,MOV设步数并启动,DIR改方向。atoi把字符串转整数,注意它不报错,非法输入返回 0,所以外面加了范围判断。指令多了以后strncmp链会变长,可以换成查表或简单状态机,但 demo 阶段够用。手机端 APP 用现成的蓝牙串口助手即可,发送框里敲MOV:200\n就能让电机走 200 步。
3. 从零复现:接线、配置、烧录、联调四步走
3.1 硬件接线与供电边界
接线不复杂,但供电是重灾区。STM32 的 IO 是 3.3V 逻辑,HC-05 模块一般吃 3.6~6V,步进驱动器(如 TB6600、A4988)的信号端通常兼容 3.3V,但逻辑电平要确认。典型接法:
| 模块 | 引脚 | 接到 STM32 | 说明 |
|---|---|---|---|
| HC-05 | TXD | USART1_RX (PA10) | 模块发,MCU 收 |
| HC-05 | RXD | USART1_TX (PA9) | 模块收,MCU 发 |
| HC-05 | VCC/GND | 5V/GND | 供电独立或板载 5V |
| 驱动器 | PUL+ | PA1 | 脉冲信号 |
| 驱动器 | DIR+ | PA2 | 方向信号 |
| 驱动器 | ENA+ | PA3 | 使能,可悬空 |
注意:步进电机和驱动器必须单独供电,别从 STM32 的 5V 引脚拉电流带电机,否则一转动 MCU 就复位,这个现象十有八九是供电塌陷。
HC-05 的 RXD 如果模块是 5V 逻辑,直接接 3.3V 的 PA9 可能识别不稳,常见做法是加个电平转换或串 1k 电阻分压。TX 方向模块 3.3V 输出,MCU 能直接收,问题不大。
3.2 CubeMX 配置要点
用 CubeMX 生成工程能省掉时钟树和引脚初始化的麻烦。关键配置项:
- 系统时钟:外部晶振 8MHz,PLL 倍频到 72MHz。
- USART1:异步模式,波特率 9600(和 HC-05 默认一致),开启全局中断。
- TIM2:内部时钟,PSC 和 ARR 先随便填,代码里会动态改,开启更新中断。
- GPIO:PA1、PA2、PA3 配成推挽输出,初始低电平。
- NVIC:USART1 和 TIM2 中断优先级都使能,USART 优先级可以略高于 TIM。
生成代码后,在main里启动第一次接收:HAL_UART_Receive_IT(&huart1, &rx_buf[0], 1);,只调一次,后续在回调里续挂。忘了这句,串口永远收不到第一个字节。
3.3 手机端联调与指令验证
手机装个蓝牙串口 APP,配对 HC-05(默认密码 1234 或 0000),连上后打开发送框。先发DIR:CW\n,用万用表量 PA2 应该变高;再发MOV:200\n,电机走 200 步后停。如果电机不转但 PA1 有波形,查驱动器使能ENA和细分设置;如果 PA1 没波形,查定时器中断有没有进——可以在回调里翻转一个调试 IO 用示波器看。
调速验证:发SPD:500\n再MOV:1000\n,听电机声音是否变尖、转速变快。SPD值对应频率,500 就是 500Hz。注意别在电机运行中猛改ARR,容易失步,稳妥做法是停脉冲再改。
4. 避坑排查:这五个现象我踩过不止一次
4.1 电机只抖不转
现象:上电后电机发出嗡嗡声但不旋转。原因:脉冲频率太高超出驱动器响应,或者相序接错。解决:先把SPD降到 200 以下试,确认能转再往上加;相序按驱动器说明书对 A+ A- B+ B- 逐一核对,别凭颜色猜。
4.2 串口收到乱码
现象:手机发指令,MCU 解析出乱七八糟的值。原因:波特率不匹配,或者 HC-05 和 MCU 的 TX/RX 接反。解决:确认两边都是 9600,用示波器量 TX 线看波形周期;TX/RX 交叉接,模块 TX 接 MCU RX,别接成 TX 对 TX。
4.3 走几步就停或丢步
现象:MOV:1000只走了几百步就停。原因:step_count是uint16_t溢出,或者中断里被其他高优先级中断打断太久。解决:把step_count改成uint32_t;检查有没有其他中断里做长延时,把串口解析挪到主循环做,中断只负责收字节。
4.4 一开电机 MCU 就复位
现象:电机启动瞬间单片机重启。原因:电机和 MCU 共用电源,启动电流把电压拉垮。解决:电机驱动器单独供电,共地即可;电源输入端并个大电容(470uF 以上)缓冲。
4.5 蓝牙连上但发指令没反应
现象:APP 显示已连接,发数据 MCU 无动作。原因:HC-05 处于 AT 模式没退出,或者HAL_UART_Receive_IT没续挂。解决:模块上电时如果 EN 拉高会进 AT 模式,正常通讯要 EN 悬空或拉低;检查回调里有没有漏掉重新挂接收的那句。
5. 进阶:把定步数改成梯形加减速,顺便验证脉冲均匀性
定速跑在低速没问题,速度一高,启动瞬间电机直接失步——因为静止到高速没有加速过程。工业上常用梯形加减速:加速段频率线性上升,匀速段保持,减速段线性下降。实现思路是在定时器中断里动态改ARR,每走一步根据当前步数查表或算频率。
// 梯形加减速简化实现:accel_steps 加速步数,target_arr 目标 ARR void update_speed(uint32_t current_step, uint32_t total_steps) { uint32_t arr; if (current_step < accel_steps) { // 加速段:ARR 从大变小 arr = start_arr - (start_arr - target_arr) * current_step / accel_steps; } else if (current_step > total_steps - accel_steps) { // 减速段:ARR 从小变大 uint32_t dec_step = total_steps - current_step; arr = start_arr - (start_arr - target_arr) * dec_step / accel_steps; } else { arr = target_arr; // 匀速 } __HAL_TIM_SET_AUTORELOAD(&htim2, arr); }start_arr对应启动频率(比如 200Hz),target_arr对应目标频率,accel_steps是加速所需步数,一般取总步数的 1/5 到 1/3。每进一次中断调一次update_speed,ARR平滑变化,电机就不会“咯噔”一下。注意ARR改太小会让中断频率过高,72MHz 下ARR别低于 72,否则中断来不及处理。
验证脉冲均匀性有个土办法:把 PA1 接到逻辑分析仪或带频率计的示波器,发MOV:2000看频率曲线是不是梯形。没有仪器就用示波器看周期抖动,匀速段周期应该稳定。我一般还会在加速段故意把accel_steps设成 1,听电机是不是直接失步,以此确认加减速逻辑真的在起作用。
从那以后我每次调步进电机,都强制先低速验证脉冲和方向,再上加减速,最后才联蓝牙——顺序反了,排查起来就是一团乱麻。希望帮到你。
本文还有配套的精品资源,点击获取