基于STM32 CubeIDE的蓝牙透传手机控制LED灯实战解析
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 发送字符蓝牙模块通过串口转发给 STM32STM32 解析后翻转 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-31LED5mm 红色1也可以用板载 LED电阻220Ω1限流保护面包板830孔1免焊接杜邦线公对公/公对母若干若干连接模块USB-TTL 模块CH340/CP21021给蓝牙模块配 AT 指令用手机支持蓝牙 4.0 Android/iOS1安装串口助手的设备2.2 接线方式和引脚选择接线是整个项目最容易出问题的地方我画一张表格按我实测的顺序来蓝牙模块引脚STM32 引脚说明RXDPA9USART1_TXSTM32 发送给模块TXDPA10USART1_RX模块发送给 STM32VCC3.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 → 蓝牙模块 RXDUSB-TTL 的 RXD → 蓝牙模块 TXD共地电脑打开串口助手波特率设置为 38400HC-05 默认或 9600进入 AT 模式后发送AT命令如果返回OK说明通信正常。常用的指令有指令功能示例ATNAMEXX修改模块名称ATNAMELED_BoardATUART9600,0,0设置波特率 9600HC-05 专用ATROLE0设为从机模式手机主动连接ATPSWD1234设置配对密码HC-05 经典蓝牙ATRMA2清空配对列表连接失败时尝试如果你用的是 JDY-31指令有些差异。常见的是ATNAME、ATPASS波特率默认 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异步模式波特率设为9600Word Length 设为8 BitParity 设为NoneStop 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.hCubeIDE 默认工程一般没有显式 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 串口收发链路完整。第三步测试控制逻辑。把回显代码注释掉发送1LED 点亮发送0LED 熄灭发送2LED 翻转。到这里项目基本就通了。如果你想做得更完整一点可以在手机端把快捷按钮改成实际指令甚至做一个简单的控制面板界面。Serial Bluetooth Terminal 的设置里可以自定义按钮和样式比如设置一个“开灯”按钮绑定LED_ON字符串一个“关灯”按钮绑定LED_OFF。这样做的好处是即使你以后换手机、换个串口工具协议仍然是稳定的。4.3 指令协议设计建议既然标题是“手机控制 LED”我只用了最简单的字符指令但在实际项目里指令协议设计是很重要的一环。我给几个方向指令帧结构起始符 指令类型 数据 校验。比如$L01#其中$是帧头L表示 LED01是命令码#是帧尾。STM32 端解析到帧尾再执行动作能有效避免误触发。校验机制如果需要远程控制开关门锁这类设备校验码必不可少。可以用简单的异或和校验或者 CRC8。蓝牙链路是无线环境可能受到干扰一帧数据出错造成误动作后果可能很严重。反馈机制控制指令执行后STM32 可以回传一个状态帧给手机比如L_OK。这样手机端就能确认执行成功交互体验好很多。协议别一开始设计得太复杂能跑通链路再加。我见过有人上来就写了一个巨长的协议解析框架结果硬件链路没通排查问题的时候根本不知道是协议问题还是硬件问题心态直接崩了。5. 常见问题与排查技巧实录5.1 速查表现象、原因、解决办法下面这个表是我在这个项目里遇到的以及帮别人排查过的高频问题汇总现象可能原因排查方法手机搜不到蓝牙模块模块未进入透传模式重新上电检查 EN 引脚状态手机搜得到但连不上配对密码不对或模块被旧设备占用HC-05 发送ATPSWD1234或ATRMA2手机连上但发指令无反应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本身而是一条完整的嵌入式调试方法论硬件接线、串口链路验证、协议设计、异常排查。这套流程在以后做任何带无线通信的嵌入式项目时都能复用。最后分享一个小习惯每次改完代码烧录之前先检查一遍配置界面里的时钟树和外设初始化再检查接线最后再怀疑代码——按这个顺序排查能帮你省下大量的无效调试时间。