简介:这是一份STM32控制步进电机的完整工程源码,面向嵌入式初学者和需要做蓝牙控制项目的开发者,适合有一定单片机基础、希望实践电机驱动与无线通信的人群。项目以手机APP通过蓝牙串口与STM32通讯,实现正转、反转、调速等电机动作,涉及串口协议解析、定时器PWM输出和步进电机驱动策略,代码按功能拆分为蓝牙、命令解析、定时器、电机驱动等模块。资料包共479个文件,以c/h源文件、crf/d/o编译中间文件、uvproj工程文件为主体,另含chm帮助文档与备份配置,整体约71.95MB,目录分类清晰。目前已有836人学习。开发者可从中获取各模块完整代码,理解HAL库外设初始化与中断响应流程,也能参考工程结构快速移植到其他STM32项目中,对毕设、竞赛和产品原型开发都有参考价值。 做嵌入式这几年,STM32控制步进电机加上手机APP通过串口蓝牙下发指令这个组合,我前前后后做了不下三版。每次重做都有新收获,也踩过不少坑——驱动板烧了、电机丢步、蓝牙连上了但指令发不过去,这些都遇到过。这篇文章就把我反复验证过的一套方案完整拆开讲一遍:从硬件选型、电路接线,到STM32固件的指令解析和加减速控制,再到手机APP开发,最后是联调排错。内容偏实战,正在做毕设、准备电赛,或者想给手头设备加个蓝牙遥控功能的朋友,可以直接照着干。
1. 项目整体设计:先想清楚再动手
1.1 核心需求拆解
先把项目功能拆开看。手机APP通过蓝牙发出指令,蓝牙模块以串口透传的方式把数据交给STM32,STM32解析指令后根据预设的逻辑去驱动步进电机完成正转、反转、加减速、停止等动作。这个链路听起来不复杂,但每一环都有讲究:
- 指令层:手机APP发什么格式的数据?STM32怎么判断一条指令什么时候开始、什么时候结束、内容是否合法?
- 传输层:蓝牙模块选透传模式还是AT命令模式?波特率怎么匹配?数据会不会丢?
- 执行层:步进电机怎么转、转速怎么调、怎么避免启动瞬间丢步?
- 交互层:APP界面怎么让操作直观,既能让新手点按钮就转,又能让进阶用户调速度、调步数?
这些需求直接影响后面每一步的设计。很多人一上来就写代码,结果APP、固件、硬件各自为政,联调时一堆问题,根本不知道是谁的锅。先把协议定清楚,是这类项目少走弯路的唯一捷径。
1.2 核心器件与选型思路
主控我选的是STM32F103C8T6。这是最经典的“蓝色药丸”板,主频72MHz,Flash 64KB,RAM 20KB,做这个小项目绰绰有余。关键是资料多到泛滥,标准库和HAL库的例程都随手可得,遇到问题一搜就有答案。你要用F407或者G431也完全可以,逻辑是一样的。
电机我用的是42步进电机(42BYGH系列),配上A4988驱动板。A4988是市面上最常见的步进电机驱动芯片,板载斩波恒流驱动,通过STEP、DIR两个引脚就能控制,占用的MCU资源极少。供电范围8V-35V,峰值电流2A,驱动42电机刚好。
蓝牙模块选了HC-05。它支持主从一体、串口透传,默认波特率9600,配置成从机模式后,手机端配对密码是1234或0000,连上就能直接收发串口数据,非常省心。如果只想做单向控制,HC-06更便宜;想走低功耗蓝牙就换JDY-23或者CC2541,但透传逻辑是一样的。
1.3 两种驱动方案对比
很多新手会纠结:用ULN2003+28BYJ-48五线四相电机不更便宜吗?确实便宜,一套下来不到十块钱,但它有两个硬伤:扭矩小、转速上去之后丢步严重,而且ULN2003是达林顿管驱动,根本不适合高速脉冲。我第一版图便宜用了这套,结果带个稍微有点阻力的负载就原地踏步。
我的建议是直接上A4988(或DRV8825) + 42电机方案。A4988几个关键特点:
- 细分可调:通过MS1、MS2、MS3三个引脚组合实现1、1/2、1/4、1/8、1/16细分。细分越高,电机运行越平滑、噪音越小,但同样步数下转速会变慢。
- 电流可调:板上有个电位器,调节Vref电压来控制输出电流,避免电机过热或力矩不足。
- 自动电流衰减:电机停止时自动降低电流,防止发热,这点对长时间运行很重要。
2. 硬件连接与电路设计细节
2.1 A4988驱动电路接线
A4988典型接线如下,这是我最常用的一套引脚分配:
| A4988引脚 | 接STM32/电源 | 说明 |
|---|---|---|
| VDD | 5V | 逻辑电源,接STM32板载5V |
| GND | GND | 与STM32共地 |
| STEP | PA0 | 脉冲输入,每来一个上升沿走一步 |
| DIR | PA1 | 方向控制,高电平正转,低电平反转 |
| MS1/MS2/MS3 | PB0/PB1/PB2 或直接接VCC | 细分设置 |
| VMOT | 12V | 电机电源正极 |
| GND | 12V电源地 | 注意与逻辑地共地 |
| 1A/1B/2A/2B | 电机四线 | 接42电机 |
这里有几个必须注意的细节:
VMOT电源千万不要接到5V上。A4988的VMOT是电机主供电,至少要9V以上才能发挥42电机的正常扭矩,5V只够逻辑电路工作。我第一版图省事直接用USB的5V,电机稍微带载就转不动。
EN(使能)引脚默认悬空是低电平有效,即默认使能。如果电机在静止时嗡嗡响或发热严重,可以把EN接高电平禁用输出,或者直接不理它——反正静止时A4988会自动降低电流。
电流限制怎么调:用万用表测电位器上的Vref电压,然后按公式I = Vref / (8 × Rs)计算。A4988模块上的采样电阻Rs通常是0.05Ω(也有0.1Ω的),42电机额定电流1.5A左右,把Vref调到0.6V-1V之间比较合适。注意:调节时必须断电操作,边通电边拧电位器,手一抖短路就是一颗芯片。
细分设置:如果只是做简单正反转,把MS1、MS2、MS3全部接地(1细分)即可。想要平滑运行,建议MS1接高、MS2/MS3接地,也就是1/2细分。细分越高,相同转速下需要的脉冲频率越高,STM32的定时器完全扛得住,但没必要盲目拉高,1/2或1/4细分在扭矩和噪音之间平衡得最好。
2.2 HC-05蓝牙模块接线
HC-05是串口透传模块,接线极度简单:
| HC-05引脚 | 接STM32/电源 | 说明 |
|---|---|---|
| VCC | 5V/3.3V | 供电,注意模块版本 |
| GND | GND | 共地 |
| TXD | PA3(USART2_RX) | 蓝牙发数据给STM32 |
| RXD | PA2(USART2_TX) | STM32发数据给蓝牙 |
| STATE | 悬空 | 状态指示,可接LED |
| EN | 悬空或接3.3V | 拉高进入AT指令模式 |
这里最容易犯的错就是TXD和RXD接反。很多新手看到“TXD”就接到STM32的TX引脚上,结果数据全乱。记住一条铁律:收发交叉——蓝牙的TXD接STM32的RXD,蓝牙的RXD接STM32的TXD。
HC-05默认波特率9600。如果你想让通讯更快,可以用AT指令改成38400或115200,但必须保证STM32串口初始化的波特率和它一致,否则收到的全是乱码。我在这个项目里用9600,控制指令长度很短,这个速率完全够用,而且低速传输更稳。
HC-05固定从机配置流程(如果模块状态未知):
- 按住模块上的按键再上电,进入AT模式(指示灯慢闪)
- 用USB转TTL连接模块,发送AT测试,返回OK
- 发送
AT+ROLE=0设为从机 - 发送
AT+UART=9600,0,0固定波特率 - 发送
AT+PSWD=1234设置配对密码 - 重新上电,退出AT模式
2.3 电源系统设计
这个项目的电源设计如果偷懒,后面联调会很痛苦。我的建议是分两路:
- 逻辑电源:STM32板载的USB 5V或者外部5V稳压供电,给MCU、HC-05、A4988的VDD用
- 电机电源:独立的12V 2A适配器,只给A4988的VMOT供电
两路电源的GND必须连在一起,否则就会出现串口数据时好时坏、电机乱转的诡异问题。原因很简单:不共地的话,两个电路的电平基准不同,TTL电平无法正确判读。
另外在VMOT和GND之间并一个100μF的电解电容,能有效吸收电机启停时的反向尖峰电压。这个电容我一开始没加,电机启动瞬间干扰直接导致STM32重启,加了之后问题消失。
3. 固件开发:指令解析、步进控制与加减速
3.1 通讯协议设计
STM32和APP之间的通讯协议是整个项目的灵魂。协议定得好,联调半小时搞定;协议定得烂,两边代码改来改去折腾一整天。
我用的是最简单的帧格式:
$ID,CMD,P1,P2#$起始符:一帧数据的开始ID设备地址:预留扩展位,多设备组网时用,单设备固定01CMD命令字:两个字符的ASCII码P1/P2参数:逗号分隔的数字,具体含义由CMD决定#结束符:一帧数据的结束
实际指令表设计如下:
| 指令 | 含义 | 示例 |
|---|---|---|
$01,MV,2000,500# | 正转2000步,速度500步/秒 | 带动滑块前进 |
$01,MV,-1000,400# | 反转1000步,速度400步/秒 | 负号代表反向 |
$01,ST# | 紧急停止 | 立即停止,不减速 |
$01,SP,800# | 修改运行速度为800步/秒 | 改变后续运动速度 |
$01,HO# | 回原点 | 可以配合限位开关 |
为什么要用$和#做边界?因为蓝牙串口是字节流,没有明确的“帧”概念,数据可能粘包、半包甚至乱序。有了起始符和结束符,STM32端就能用状态机精确切分每一帧。
3.2 串口接收状态机实现
串口接收我用的是USART2 + 接收中断的方式(USART1留作调试打印)。代码逻辑很清晰:
uint8_t rx_buffer[64]; uint8_t rx_index = 0; uint8_t frame_ready = 0; typedef enum { WAIT_START, // 等待'$' WAIT_DATA, // 接收数据 } RX_STATE; RX_STATE rx_state = WAIT_START; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE)) { uint8_t ch = USART_ReceiveData(USART2); switch (rx_state) { case WAIT_START: if (ch == '$') { rx_index = 0; rx_state = WAIT_DATA; } break; case WAIT_DATA: if (ch == '#') { rx_buffer[rx_index] = '\0'; frame_ready = 1; // 通知主循环处理 rx_state = WAIT_START; } else if (rx_index < sizeof(rx_buffer) - 1) { rx_buffer[rx_index++] = ch; } else { rx_index = 0; // 缓冲区溢出,重新接收 rx_state = WAIT_START; } break; } } }主循环里轮询frame_ready标志,解析字符串、执行电机控制动作。注意一点:不要在中断里做复杂的解析动作,中断服务函数越短越好,否则会阻塞其他中断,造成系统响应延迟。解析和电机控制一律放主循环处理。
解析字符串时有个小技巧:用sscanf可以一行搞定:
int id, steps, speed; char cmd[8]; if (strncmp(rx_buffer, "01,MV,", 6) == 0) { sscanf(rx_buffer + 6, "%d,%d", &steps, &speed); motor_run(steps, speed); // 传入方向由steps符号决定 }这种写法简单粗暴但非常有效,解析几字节的指令一点压力都没有。
3.3 步进电机控制:PWM脉冲与梯形加减速
步进电机的本质是:给STEP引脚一个脉冲,电机转一步。所以控制步进电机就是控制脉冲的频率和个数。
最常用的实现方式是用定时器输出PWM。以TIM3为例,用通道输出PWM到PA0(STEP引脚),通过调整PWM频率来控制速度,通过控制PWM脉冲个数来控制转过的角度(步数)。
速度和频率的换算关系:电机步距角1.8°,在1细分模式下转一圈需要360/1.8=200个脉冲。如果电机以1转/秒运行,PWM频率就是200Hz。如果想跑10转/秒,PWM频率就要2000Hz。实测42电机在12V供电下,2000Hz-4000Hz是稳定运行的区间,再高就要考虑加减速了。
这里必须强调加减速的原因:步进电机有个“牵出转矩”特性,启动瞬间如果频率太高,转子来不及响应驱动脉冲,就会丢步甚至完全堵转。我测试中发现直接从静止给2000Hz脉冲,电机经常会咔咔响几下然后不动。解决办法是启动时从低频率开始,逐步加速到目标频率,这就是梯形加减速算法。
简单实用的实现思路是用定时器的Pulse+Direction模式不行的话,就自己控制定时器中断的频率:
// 使用TIM2输出PWM驱动STEP,ARPE预装载 // 启动时:先就把ARR设大(频率低),每隔N个脉冲减小ARR(提高频率) // 停止前:反过来增大ARR(降低频率)完整加减速可以做个查表,把速度曲线离散成128个档位,比如从200Hz逐步升到2000Hz,每步只提升十几Hz。这样代码简单,不涉及浮点运算,F103的Cortex-M3内核毫无压力。
const uint16_t speed_table[128] = { 200, 210, 221, 233, ... // 实际值可根据电机特性调整 }; void motor_run(int32_t steps, uint32_t target_speed) { // 运动前计算加速段/匀速段/减速段的步数分配 // 假设加速段和减速段各占10%-20% uint32_t accel_steps = steps / 8; // 加速步数 uint32_t decel_steps = steps / 8; // 减速步数 uint32_t run_steps = steps - accel_steps - decel_steps; // 加速段逐步提高PWM频率 // 匀速段保持恒定频率 // 减速段逐步降低PWM频率 }梯形加减速的原理图和实现代码,我在实际项目里用的是分段判断:步数计数器小于加速段时递增速度,大于总步数减减速段时递减速度,其他时间保持匀速。这个逻辑非常直观,调参也容易。
4. 手机APP开发:3小时做出控制界面
4.1 开发方式选型
手机APP这块,关键看你有没有安卓开发基础。我推荐三条路:
| 方式 | 难度 | 优点 | 缺点 |
|---|---|---|---|
| 现成串口助手APP | 零 | 立刻能用,支持自定义发送 | 界面固定,不够“产品化” |
| MIT App Inventor | 低 | 图形化拖拽,适合快速验证 | 打包安装略繁琐,底层不可控 |
| Android Studio原生 | 中高 | 界面自由,蓝牙控制更稳 | 开发周期长,蓝牙权限处理麻烦 |
如果项目是毕设或者比赛演示,我建议先下载现成蓝牙串口助手把硬件链路调通,再用App Inventor做定制界面。这样即使App开发遇到问题,也不至于卡住整个项目的进度。
4.2 App Inventor实现要点
App Inventor的蓝牙支持内置了蓝牙客户端组件,操作很直观:
- 在组件面板拖入BluetoothClient、ListPicker(设备选择)、Button(方向控制按钮)、TextBox(参数输入)、Slider(速度调节)
- 在ListPicker的选择事件中调用蓝牙设备的地址列表,让用户选择一个HC-05
- 调用
BluetoothClient.Connect(address)连接 - 在按钮点击事件里,用
BluetoothClient.SendText("$01,MV,2000,500#")发送指令
逻辑块设计里有个坑:所有字符串拼接注意别用中文标点,协议里明确要求逗号和$#必须是半角字符,否则STM32端状态机解析不出来。
界面设计上,我给按钮做了几个档位:
- 「正转」固定发送
$01,MV,2000,800# - 「反转」固定发送
$01,MV,-2000,800# - 「停止」发送
$01,ST# - 速度滑块动态生成
$01,SP,数值#
这样设计的好处是:操作简单直观,演示的时候不用解释太多参数含义,点按钮就能看到效果。
4.3 Android Studio 原生开发要点
如果坚持用原生开发,有几个关键点要注意:
蓝牙权限:Android 6.0以上需要动态申请定位权限(蓝牙扫描需要),Android 12以上还需要BLUETOOTH_SCAN和BLUETOOTH_CONNECT运行时权限。权限申请不完整,蓝牙根本扫不到设备。
UUID选择:HC-05的串口服务UUID是标准的00001101-0000-1000-8000-00805F9B34FB,socket连接时直接用这个UUID。很多人用了别的UUID导致连接失败。
线程处理:蓝牙读写必须在子线程完成,不能在UI线程直接操作蓝牙Socket,否则会报NetworkOnMainThreadException或者卡死界面。用Handler或者协程处理回调。
回读数据:如果要实现“电机状态回传”,需要在子线程里循环InputStream.read()读数据。注意字节转字符串时的编码,UTF-8和GBK混用会出问题。
5. 联调全流程与踩坑实录
5.1 联调第一步:先别碰蓝牙
我的联调顺序是先串口后蓝牙,先电脑后手机:
- STM32用USB转TTL直接连电脑串口助手(比如XCOM、sscom),发送
$01,MV,2000,500#,观察电机是否按预期转动 - 串口助手发送
$01,MV,-2000,500#,确认方向正确 - 发
$01,SP,800#,观察转速变化 - 以上全部通过后,把USB转TTL换成HC-05蓝牙模块
- 电脑蓝牙配对HC-05,用蓝牙虚拟串口(COM口)再重复第1-3步
- 最后才上手手机APP
为什么要坚持这个顺序?因为每一步只引入一个新变量。如果用手机直接联调,出了问题可能是手机App的bug、蓝牙传输的干扰、协议解析的漏洞,也可能是电机控制的逻辑错误,多个变量搅在一起完全没法排查。
5.2 高频问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 电机完全不动,但模块有供电 | A4988电流限制没调,电机没力矩 | 断电调Vref,确保在0.6V以上 |
| 电机咔咔响但不动 | 启动频率太高,或细分设置和程序不一致 | 降低启动速度,检查MS引脚电平 |
| A4988发烫严重 | VMOT电压过低,或电流限制调太高 | 检查电源,降低Vref |
| 电机转向相反 | 电机接线相位反了 | 交换A+和A-或B+和B-任意一组 |
| 手机搜不到HC-05 | 蓝牙模块状态异常 | 重新上电,确认指示灯慢闪 |
| 能搜到但连不上 | 密码错误或模块处于AT模式 | 恢复默认密码1234或0000 |
| 连接成功,数据发不过去 | 波特率不匹配或TXD/RXD接反 | 检查STM32串口初始化,核对交叉接线 |
| 串口数据大量乱码 | 波特率不一致,或共地问题 | 统一波特率,检查GND连接 |
| 电机一启动STM32就重启 | 电源干扰 | VMOT两端加100μF电容,逻辑电源和电机电源分开 |
| 电机停止瞬间位置偏移 | 没做减速段,惯性导致丢步 | 加入梯形加减速,减速段拉长 |
| 烧录报error: no stm32 target found | ST-Link引脚冲突或接线错误 | 检查SWDIO/SWCLK接线,确保没把PA13/PA14复用成普通IO |
5.3 几个容易忽略但致命的坑
第一个坑:蓝牙模块的数据回传方向。HM-10这类透传模块是全双工的,但很多新手只关注“APP发数据给STM32”,忽略了“STM32回数据给APP”。如果APP端不主动去读返回数据,即使STM32发了状态回传,APP也收不到。我一开始用App Inventor做测试时,忘记监听蓝牙的接收事件,电机转没转只能靠眼睛看。
第二个坑:HC-05的AT模式误触发。如果模块的EN引脚悬空还好,但有些模块上电瞬间如果检测到KEY引脚为高,就会进入AT模式,这时候手机连不上、透传功能失效。排查时先观察LED闪烁频率——慢闪(大概2秒一次)是AT模式,快闪才是透传模式。
第三个坑:串口空闲中断的误用。我最初想用空闲中断来判断一帧数据接收完了,但蓝牙传输是“不定长字节流”,不同字符之间的间隔并不固定,空闲中断常常把一帧数据切成两半。所以最后老老实实回到“状态机+起始/结束符”方案,稳得一批。
第四个坑:A4988逻辑地和电机地分开接。有些教程会说“隔离”,但实际做的时候如果VDD的GND和VMOT的GND不共地,STEP信号的参考电平就不一致,电机要么不动要么乱走。
6. 最后扩展:这个项目还能怎么玩
基础功能跑通之后,我建议你往这几个方向扩展,都在现有硬件基础上小改即可:
- 加限位开关:两个GPIO接微动开关,实现回原点逻辑,配合
HO指令可以做位置归零 - 加OLED显示屏:I2C接口的0.96寸OLED,实时显示当前速度和位置,演示效果瞬间提升一个档次
- 加传感器联动:比如加个温湿度传感器,当温度超过阈值时自动旋转电机开窗,这就是一个完整的智能家居demo
- 换步进电机+编码器:升级成闭环驱动,通过编码器反馈实际位置,丢步后自动补偿。STM32F103的资源也够用,只是代码复杂度更高
我个人做完这个项目的最大体会是:先定协议后写代码这句话真的是无数个晚上的教训换来的。无论是自己做还是带人做,前期花半小时把通讯格式定清楚,后期少踩一半的坑。另外就是联调时耐心,一次只引入一个变量,问题就好定位。
最后分享一个实操中很实用的细节:调试时在STM32端加一个串口回显功能——收到什么指令,原样回发给手机APP。这样当你怀疑是蓝牙传输问题还是解析问题时,看手机端收到的是不是原指令就一目了然了。这个功能几行代码就能实现,但排查问题时能帮你节省大量时间。
本文还有配套的精品资源,点击获取