☰
基于STM32 CubeIDE的蓝牙透传手机控制LED灯实战解析
2026/10/21 17:02:39 网站建设 项目流程

STM32 CubeIDE 使用蓝牙模块实现手机控制LED灯

做嵌入式的朋友应该都有过这样的经历:调通一块板子、点亮一颗灯,明明是很简单的事情,但总有那么一瞬间会觉得"要是能躺着用手机控制就好了"。这个项目就是干这个的——用 STM32 的 CubeIDE 开发环境,接一个蓝牙透传模块,让手机通过蓝牙给开发板发指令,从而控制一颗或者多颗 LED 灯的开关。

我不会上来就甩一堆原理图,然后让你拿面包板自己猜。这篇文章会把从选型、接线、CubeIDE 图形化配置、代码编写到最终的手机联调全部拆开讲到,还会把我自己踩过的坑一并交代清楚。适合刚接触 STM32、正准备从寄存器开发转到 HAL 库开发,或者想把手头开发板"无线化"的同学们参考。项目本身不难,用到的核心知识点就三块:STM32CubeMX 的引脚配置、HAL 库的串口中断接收、蓝牙模块的透传原理。换个场景,这套思路稍微改一改,就能变成无线温度采集、远程报警、智能开关的雏形,所以值得花点时间认真做一遍。

1. 整体设计与思路拆解

1.1 为什么选 STM32 + CubeIDE 这个组合

先说选型逻辑。市面上的方案其实挺多。用 ESP32 或者 Arduino 加蓝牙模块也能实现,甚至有那种直接带蓝牙 SoC 的模组,手机连上就能控制。那我为什么还用 STM32 加外挂蓝牙模块?

核心原因是这套组合的"可迁移性"最强。STM32 是国内工业控制和物联网开发的主战场,学会用 CubeIDE 做图形化配置、用 HAL 库写外设驱动,这套技能基本可以平移到后续大部分项目上。ESP32 和 Arduino 也很好,但你多半只是调库,底层的东西接触得少;而用 STM32 挂一个串口蓝牙透传模块,你就必须把 UART 中断、DMA、GPIO 输出模式这些基本功搞明白,之后做 LoRa、GPS、4G 模块,都是同一套套路。

再说 CubeIDE。它和老的 Keil + 标准外设库路线最大的区别是:图形化配置和代码生成。你只需要在界面上点一点,选择用哪个引脚、配成什么功能、时钟频率多少,它就能自动生成一套初始化代码。相比对着数据手册逐行写寄存器,效率和正确率都高很多,尤其适合刚入门的人。CubeIDE 也能看到代码生成之后帮你整理好的外设句柄,比如UART_HandleTypeDef huart2;这种结构,配合 HAL 库的函数,逻辑非常清晰。

1.2 蓝牙模块的选型考量:HC-05 和 JDY-31 的取舍

蓝牙模块的选择我单独拿出来说。市面上最常见的两种透传模块是 HC-05 和 JDY-31(或者叫 JDY-30、CC2541 方案)。

HC-05 是经典款,主从一体,支持 AT 指令配置,默认波特率 9600 或者 38400(不同商家出货设置不一样,坑点之一)。它工作电压 3.6V 到 6V,逻辑电平是 3.3V,和 STM32 的 IO 电平可以直连。JDY-31 则更便宜,体积更小,同样支持 AT 指令,BLE 协议栈用的是 4.0,手机连接更方便,功耗也低一些。

我个人的建议是:如果只是做手机控制,首选 JDY-31。因为现在手机蓝牙都已经到 5.0 了,JDY-31 走的是 BLE 协议,手机连接时就识别成一个低功耗蓝牙设备,不需要像 HC-05 那样走经典蓝牙配对流程,配对密码这种麻烦事也少很多。但如果你的开发板模块是从旧货堆里翻出来的 HC-05,也完全没问题,文章里的代码和接线逻辑通用,只要注意波特率和供电就行。

1.3 系统整体架构:一条数据链路串起来

整个系统的数据流向用一句话概括:手机 APP 发送字符,蓝牙模块通过串口转发给 STM32,STM32 解析后翻转 GPIO,最终控制 LED 亮灭。

拆解一下这条链路上的每一个环节:

  • 手机端:安装一个蓝牙串口助手(后面我会推荐),通过蓝牙连接模块。用户在界面上输入1、0或者别的自定义字符,点击发送。
  • 蓝牙模块端:它处于"透传模式",收到什么就通过串口发什么,不做任何数据处理。这是一层透明的管道,不需要额外编程,前提是模块本身的串口参数(波特率、数据位、停止位)和 STM32 保持一致。
  • STM32 端:通过 UART 接收中断拿到字符,简单的状态机判断指令内容,然后调用HAL_GPIO_WritePin()控制 LED 引脚电平。
  • 执行机构端:一颗 5mm 红色 LED 串联一个 220Ω 限流电阻,接到某个 GPIO 引脚,低电平点亮(或高电平点亮,取决于你的接线)。

整体逻辑就这么多,难点其实不在逻辑,而在工程细节:UART 中断回调怎么写、波特率怎么对不上、模块怎么进 AT 模式、为什么收不到数据。下面这部分才是整篇文章的重点。

2. 硬件准备与接线要点

2.1 完整材料清单

这个项目的硬件成本非常低,如果你手头已经有一套 STM32 最小系统板,基本就是加一个蓝牙模块的事情。清单如下:

器件型号/参数数量备注
STM32 开发板STM32F103C8T6 最小系统板1板载 LED 也可用
蓝牙模块HC-05 或 JDY-311推荐 JDY-31
LED5mm 红色1也可以用板载 LED
电阻220Ω1限流保护
面包板830孔1免焊接
杜邦线公对公/公对母若干若干连接模块
USB-TTL 模块CH340/CP21021给蓝牙模块配 AT 指令用
手机支持蓝牙 4.0+ Android/iOS1安装串口助手的设备

2.2 接线方式和引脚选择

接线是整个项目最容易出问题的地方,我画一张表格,按我实测的顺序来:

蓝牙模块引脚STM32 引脚说明
RXDPA9(USART1_TX)STM32 发送给模块
TXDPA10(USART1_RX)模块发送给 STM32
VCC3.3V 或 5V取决于模块
GNDGND共地必须接
LED 阳极PA5GPIO 输出,高电平点亮
LED 阴极GND串联电阻后接地

这里有个关键点:TXD 和 RXD 必须交叉连接。STM32 的 PA9 是发送引脚,要接蓝牙模块的 RXD(接收引脚),PA10 是 STM32 的接收引脚,要接蓝牙模块的 TXD。很多新手第一次接反,结果手机收不到任何反馈,或者发送没反应,然后变成查半天代码,最后发现线接错了。

供电方面也要留意。HC-05 的 VCC 官方标称 3.6V 到 6V,但很多 HC-05 的板子是带稳压的,接 5V 没问题。JDY-31 是低功耗 BLE 模块,直接接 3.3V 就行。为了稳妥,刚开始调的时候,全部用 3.3V 供电。如果发现模块不工作,再用 5V 去试,毕竟每个厂家的板子细节不一样。

LED 这边,我用的 PA5 是因为 STM32F103C8T6 的最小系统板上板载 LED 就挂在 PC13 或者 PA1 上(不同厂家不一样),如果外接 LED,选哪个空闲 GPIO 都行。需要注意的是输出模式要配成推挽输出,若是开漏输出,高电平是拉不起来的。

2.3 蓝牙模块的 AT 模式操作

蓝牙模块在出厂状态下大多可以直接连,但你最好还是进一次 AT 模式确认波特率和从机模式,否则后续联调大概率翻车。

进入 AT 模式的方法:先按住蓝牙模块上的按键(或者将 EN/KEY 引脚拉高),再给模块上电。这样模块进入 AT 指令模式。如果你用的 USB-TTL 模块连接蓝牙模块,接线方式为:

  • USB-TTL 的 TXD → 蓝牙模块 RXD
  • USB-TTL 的 RXD → 蓝牙模块 TXD
  • 共地
  • 电脑打开串口助手,波特率设置为 38400(HC-05 默认)或 9600

进入 AT 模式后,发送AT命令,如果返回OK,说明通信正常。常用的指令有:

指令功能示例
AT+NAME=XX修改模块名称AT+NAME=LED_Board
AT+UART=9600,0,0设置波特率 9600HC-05 专用
AT+ROLE=0设为从机模式手机主动连接
AT+PSWD=1234设置配对密码HC-05 经典蓝牙
AT+RMA2清空配对列表连接失败时尝试

如果你用的是 JDY-31,指令有些差异。常见的是AT+NAME、AT+PASS,波特率默认 9600。JDY-31 不需要区分主从,因为它只有从机模式。配好之后给模块重新上电,让它退出 AT 模式,进入透传模式,手机就能搜到它了。

我个人反复遇到过一个问题:HC-05 在 AT 模式用 38400 波特率通信正常,但是退出 AT 模式后,如果代码里还是按 9600 去初始化 STM32 的 UART,那肯定收不到数据。所以务必在电脑上把串口参数和你接下来写在 CubeIDE 里的参数对齐。

3. CubeIDE 项目配置与代码实现

3.1 新建项目:芯片选型与时钟树配置

打开 STM32CubeIDE,点击 File → New → STM32 Project。在芯片选择界面搜索STM32F103C8T6(如果你用的是别的板子如 H743、L476,一样的操作,只是引脚编号不同)。选中后项目命名随意,我这里就叫Bluetooth_LED_Control。

项目创建之后,CubeIDE 会打开一个图形化配置界面,这就是 STM32CubeMX 的核心区域。左侧是引脚排列图,右边可以配置外设和时钟。这里不用动时钟树,默认的 8MHz 外部晶振或者内部 RC 都可以跑起来。但如果板子没有外部晶振,记得把 RCC 的 HSE 设成Bypass Clock,或者干脆用 HSI 内部时钟,否则系统时钟会出错,后面串口波特率就会偏。

我对时钟树多说一句:串口波特率是由外设时钟分频得到的,如果时钟源不对,你配置的 9600 波特率实际跑出来是错的,这就是为什么有的人模块和代码都设置成 9600 却仍然乱码或丢数据的隐藏原因。开局先把 System Clock 这块理顺,后面能少踩很多坑。

3.2 UART 与 GPIO 的具体配置

在界面里找到Connectivity→USART1或者USART2。我用的是 USART1,因为 PA9/PA10 正好在板子边缘,方便插杜邦线。

在 USART1 的配置页面里:

  • Mode 选Asynchronous(异步模式)
  • 波特率设为9600
  • Word Length 设为8 Bit
  • Parity 设为None
  • Stop Bits 设为1
  • 其他默认

接着配置 GPIO。在引脚排列图中找到 PA9 和 PA10,它们会被自动分配为 USART1_TX 和 USART1_RX。通常 GPIO 模式会默认设置为复用推挽输出和浮空输入,这个不用改。

再配置 LED 引脚 PA5。点击 PA5 引脚,选择GPIO_Output。在 GPIO 设置栏里:

  • GPIO output level 设为 Low(初始熄灭)
  • GPIO mode 设为 Output Push Pull(推挽输出)
  • Maximum output speed 设为 Low,控制 LED 完全够用
  • User Label 改成LED,这样生成的代码里引脚宏定义好认

配置完之后,点击右上角的Generate Code。CubeIDE 会自动生成一个包含main.c、usart.c、gpio.c的工程骨架。接下来就是写逻辑。

3.3 串口接收中断的初始化与回调函数

很多人第一次用 HAL 库写串口接收,容易犯的错是只初始化了 UART 但忘了启动接收中断。UART 初始化代码是 CubeIDE 自动生成的,但它只负责"外设初始化",不会主动去接收数据。你需要手动在main()里调用一次:

HAL_UART_Receive_IT(&huart1, &rx_data, 1);

这行代码的意思是:让串口在后台接收 1 个字节,接收完成之后触发中断回调。rx_data是一个全局变量,用来存接收到的数据。

中断处理逻辑写在stm32f1xx_it.c里的USART1_IRQHandler会由 CubeIDE 自动生成并调用 HAL 库的中断处理函数,你不需要改。真正写代码的地方是弱函数HAL_UART_RxCpltCallback,推荐放在main.c底部:

uint8_t rx_data = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { if (rx_data == '1') { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } else if (rx_data == '0') { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } else if (rx_data == '2') { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } HAL_UART_Receive_IT(&huart1, &rx_data, 1); } }

这段代码逻辑很直白,但有两个细节必须强调。

第一,回调函数结束后必须重新调用HAL_UART_Receive_IT。HAL 库的接收中断是一次性的,接收完一个字节之后如果不重新开启,下一次收数据永远不会触发回调。很多人的现象是"第一次发送成功,第二次没反应",就是漏了这行。

第二,我在回调里判断的是字符'1'和'0',而不是整型1和0。手机蓝牙串口助手发送的是 ASCII 码,你可以理解为它把1按键转换成了十六进制0x31,所以代码里面必须用单引号包裹。如果你发的是十六进制01,那判断条件就要改成rx_data == 0x01。这个要提前想好,协议一旦定下来就统一,我吃过亏:发十六进制和发字符串混着用,结果调了两个小时。

3.4 优化:增加指令校验和状态机

如果只是控制一颗 LED,上面那段代码完全够用。但既然写都写了,我建议多加一点东西,让这个控制协议更有扩展性,以后接其他设备不用大改。

比如用状态机处理多字节指令。手机端发LED_ON和LED_OFF这种字符串指令,比单个字符更直观,也不太怕误触。实现方式是在回调里把收到的字节累计到缓冲区:

#define CMD_BUFFER_SIZE 16 uint8_t rx_buf[CMD_BUFFER_SIZE]; uint8_t rx_index = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { if (rx_index < CMD_BUFFER_SIZE - 1) { rx_buf[rx_index++] = rx_data; if (rx_data == '\n' || rx_data == '\r') { rx_buf[rx_index] = '\0'; rx_index = 0; if (strncmp((char *)rx_buf, "LED_ON", 6) == 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } else if (strncmp((char *)rx_buf, "LED_OFF", 7) == 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } } } HAL_UART_Receive_IT(&huart1, &rx_data, 1); } }

这样做的额外好处是,手机端发送时勾选"发送新行"(即自动追加\r\n),就能形成一帧完整的指令,不会出现数据粘连。状态机也好扩展,以后加一个 PWM 亮度调节指令,只需在判断分支里加个else if,控制逻辑不会被撑爆。

3.5 编译烧录与调试技巧

代码写完直接点锤子图标编译。如果编译报错,八成是变量没声明或者头文件缺失。strncmp需要包含<string.h>,CubeIDE 默认工程一般没有显式 include,手动加一行:

#include <string.h>

烧录用 ST-Link 或者串口 ISP 都可以。如果是 ST-Link,直接点 Configuration Manager 里添加调试器,然后按 Debug 按钮。烧录之后板子会进入调试暂停状态,按一下 Resume 按钮让它全速运行。

在调通之前,建议先用 USB-TTL 连接 STM32 的另一个串口,或者逻辑分析仪看 STM32 有没有正确输出。如果没有工具,也可以先做个简单的自测:把HAL_UART_Transmit(&huart1, "OK\r\n", 5, 100);写到 main 函数里,每次按键发送一个字符,看看手机端能不能正常收到。这一步能快速把"蓝牙链路通不通"和"STM32 逻辑对不对"分开排查。

4. 手机端控制与联调

4.1 选择手机蓝牙调试工具

手机端选择很关键。Android 首推Serial Bluetooth Terminal,它可以自定义快捷发送按钮,支持十六进制和字符串发送。iOS 端选择相对少一点,可以试试LightBlue或者BLE Terminal。注意 iOS 对蓝牙串口透传的支持比较挑模块,JDY-31 这种 BLE 模块在 iOS 下兼容性通常比 HC-05 好很多,这是我实测下来的经验。

安装好助手后,手机蓝牙打开,搜索附近设备。你会看到一个类似HC-05或者JDY-31的名字(如果你改过 AT 指令,就是你自定义的名字)。点击配对。如果提示输入 PIN 码,一般是 1234 或者 0000。连接成功后,在串口助手底部输入框输入字符1,点击发送。

4.2 联调步骤:从小白测试到自定义控制面板

不要一上来就急着发指令。我的调试顺序是:

第一步,空测蓝牙链路。发送一个AT或者随便一个字符,观察蓝牙模块上的指示灯有没有快速闪烁,这代表它收到了数据。如果你的模块在透传模式下不支持 AT,那这一步跳过。

第二步,测试 STM32 是否收到数据。在回调函数里面加个回显逻辑:

HAL_UART_Transmit(&huart1, (uint8_t *)&rx_data, 1, 100);

这样手机发送1,马上收到1回显,说明 STM32 串口收发链路完整。

第三步,测试控制逻辑。把回显代码注释掉,发送1,LED 点亮;发送0,LED 熄灭;发送2,LED 翻转。到这里,项目基本就通了。

如果你想做得更完整一点,可以在手机端把快捷按钮改成实际指令,甚至做一个简单的控制面板界面。Serial Bluetooth Terminal 的设置里可以自定义按钮和样式,比如设置一个“开灯”按钮绑定LED_ON字符串,一个“关灯”按钮绑定LED_OFF。这样做的好处是即使你以后换手机、换个串口工具,协议仍然是稳定的。

4.3 指令协议设计建议

既然标题是“手机控制 LED”,我只用了最简单的字符指令,但在实际项目里,指令协议设计是很重要的一环。我给几个方向:

  • 指令帧结构:起始符 + 指令类型 + 数据 + 校验。比如$L01#,其中$是帧头,L表示 LED,01是命令码,#是帧尾。STM32 端解析到帧尾再执行动作,能有效避免误触发。
  • 校验机制:如果需要远程控制开关门锁这类设备,校验码必不可少。可以用简单的异或和校验,或者 CRC8。蓝牙链路是无线环境,可能受到干扰,一帧数据出错造成误动作,后果可能很严重。
  • 反馈机制:控制指令执行后,STM32 可以回传一个状态帧给手机,比如L_OK。这样手机端就能确认执行成功,交互体验好很多。

协议别一开始设计得太复杂,能跑通链路再加。我见过有人上来就写了一个巨长的协议解析框架,结果硬件链路没通,排查问题的时候根本不知道是协议问题还是硬件问题,心态直接崩了。

5. 常见问题与排查技巧实录

5.1 速查表:现象、原因、解决办法

下面这个表是我在这个项目里遇到的以及帮别人排查过的高频问题汇总:

现象可能原因排查方法
手机搜不到蓝牙模块模块未进入透传模式重新上电,检查 EN 引脚状态
手机搜得到但连不上配对密码不对,或模块被旧设备占用HC-05 发送AT+PSWD=1234,或AT+RMA2
手机连上但发指令无反应STM32 UART 波特率与模块不一致确认两边波特率均为 9600 或 38400
第一个指令有效,后续无效HAL_UART_Receive_IT未重新开启回调末尾重新调用接收函数
串口数据乱码波特率不匹配,或时钟配置错误检查 System Clock 频率和 UART 参数
LED 一直点亮不受控GPIO 输出模式配置错误或接线短路排查 PA5 是否配置为推挽输出
模块发热严重供电电压过高或接反立刻断电检查 VCC/GND
距离短、不稳定天线被遮挡,或模块干扰天线露出,远离电源走线

这里面最常见的是第二个和第三个问题。蓝牙模块调试就是这样,链路没有回显信息,出错只能一层层排除。我的习惯是先测蓝牙模块本身,再测 STM32 和模块之间的串口,最后才测逻辑代码,不要跳步骤。

5.2 实测心得:电源和地线是隐形坑

最后补充一个看起来不值一提但实际害人不浅的点:电源回路和共地。

如果 STM32 开发板是 USB 供电,蓝牙模块从开发板的 3.3V 引脚取电,那它们之间是共地的,没问题。但如果你用独立的 USB-TTL 给蓝牙模块供电,而 STM32 又用另一路适配器供电,两者之间没有物理上的地线连接,那么串口的电平参考地都没对齐,数据肯定是废的。一定要用一根杜邦线把两个系统的 GND 连起来。

另外,蓝牙模块在工作时瞬间电流能到 30mA 左右,虽然不大,但劣质面包板的内阻有时候会出怪问题,LED 亮度闪烁、模块偶发断连。排查时可以用万用表量一下模块供电引脚,看电压是否稳定在 3.3V 附近。如果电压偏低,换一根更短的杜邦线,或者直接在开发板的 3.3V 输出引脚并联一个 100μF 电容,实测能改善不少。

5.3 从 LED 控制到完整智能设备的改进方向

项目做完之后别停,这套代码骨架的复用价值很高。控制 LED 只是验证链路,你把它换成继电器模块,就能控制台灯、水泵、电磁锁;把 LED 换成电机驱动芯片的使能引脚,就能做一个无线遥控小车。我自己做的时候,后来把这块扩展成了一个房间灯光的无线开关,手机发TOP_ON开启吸顶灯,发TOP_OFF关闭,配合一个继电器模块只需要改两行代码。

如果想让体验更接近"物联网产品",还可以加一个 STM32 端的回传逻辑,把当前 LED 状态上报,这样手机打开 APP 就能同步显示灯的状态,而不是盲控制。代码实现也不算复杂,在蓝牙串口助手上收到LED_ON这个消息,手机端写个正则匹配就能重新显示开关状态。

做这个项目最大的收获其实不是"手机控制 LED"本身,而是一条完整的嵌入式调试方法论:硬件接线、串口链路验证、协议设计、异常排查。这套流程在以后做任何带无线通信的嵌入式项目时都能复用。最后分享一个小习惯:每次改完代码烧录之前,先检查一遍配置界面里的时钟树和外设初始化,再检查接线,最后再怀疑代码——按这个顺序排查,能帮你省下大量的无效调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询