STM32 接蓝牙模块这件事看着简单真做起来能踩的坑比想象中多。我前后在几个小项目里用过这套组合一个车载数据采集盒、一个手持温湿度记录仪、一个给产线做的参数配置手柄另外还帮人调过几个用 STM32 做主体的无线串口小板。场景不同但底层逻辑是同一套——STM32 通过串口跟蓝牙模块说话蓝牙模块再跟手机或另一块板子说话。这篇就把我从选型、接线、AT 指令、代码框架到排查问题的完整过程摊开讲内容偏向实战适合刚上手 STM32 串口和蓝牙透传的朋友也适合已经能点亮但总在“能连上、收不到数据”上打转的人对照着看。1. 先想清楚STM32 为什么要挂蓝牙模块1.1 三种常见方案别一上来就买错型号市面上能跟 STM32 搭配的蓝牙模块大致分三类选错一次后面代码白写一半。第一类是经典蓝牙串口透传模块常见型号 HC-05、HC-06、JDY-31 这类。它们的本质是把蓝牙 SPP 协议栈封装好对外只露一个 UART 口你发什么它就发什么对 STM32 来说就当普通串口用。HC-05 支持主从切换HC-06 一般只能做从机。优点是开发成本极低手机装个通用蓝牙串口工具就能收发缺点是功耗高iOS 端基本用不了苹果对 SPP 支持有限传输距离常规在 10 米左右。第二类是低功耗蓝牙模块常见的是基于 CC2541 或类似芯片的 AT-09、HM-10 兼容模块走 BLE 的 GATT 模型。它不能用“串口透传”的思路直接理解数据要通过服务和特征值读写Android 和 iOS 都能连功耗低适合电池供电的设备。代价是你得懂一点 GATT 的层级关系调试时也更容易卡在“连上了但不知道怎么发数据”。第三类是双模模块价格贵一些经典蓝牙和 BLE 都能跑适合产品要同时兼顾老设备和手机 App 的场景。我一般这样判断如果只是桌面上做个数据回传、手机是 Android、供电不缺直接上 HC-05 最省事如果设备要装电池跑几个月、或者要给 iOS 用老老实实选 BLE如果是要做产品且客户群体杂才考虑双模。类型典型型号协议手机兼容性功耗上手难度适合场景经典透传HC-05 / HC-06SPPAndroid 好iOS 差较高约 20~40mA低桌面调试、工控手持机低功耗AT-09 / HM-10 兼容BLE 4.0双端都好低连接态数 mA中电池设备、可穿戴双模双模串口模块SPP BLE双端都好中等中量产产品1.2 一根串口就够了吗硬件连接的最小集最简连接其实只要四根线模块 VCC 接 3.3V 或 5V看模块标注、GND 共地、模块 TXD 接 STM32 的 RX、模块 RXD 接 STM32 的 TX。注意是交叉接不是 TX 对 TX。我见过太多次有人把两根线并着插上去然后怀疑模块坏了。除了这四根还有两根值得接出来。一根是 STATE或叫 LINK、连接状态指示连接成功后输出高电平可以接到 STM32 的 GPIO 上用来判断“现在到底有没有人连我”比在代码里猜要可靠得多。另一根是 EN或 KEYHC-05 这类模块拉高这个脚再上电才能进 AT 模式把它接到一个 GPIO 上用软件控制进 AT 模式比每次手动拔插杜邦线舒服太多。供电这块要单独说。蓝牙模块在发射瞬间的电流会有脉冲尤其是经典蓝牙峰值能到 40mA 以上。如果你从 STM32 板子上那颗小封装 LDO 取电同时又带着屏幕、传感器很可能出现“一连上就复位”的现象。我的习惯是模块单独走一路电源或者在模块 VCC 旁边并一个 100μF 电解加 0.1μF 瓷片这两个电容不是摆设能救回很多莫名其妙的掉线。2. 工程环境与串口配置参数不是随便填的2.1 CubeMX 里的串口配置与波特率计算过程我用得最多的是 HAL 库加 CubeMX配置流程很固定打开 USART1模式选 Asynchronous波特率先填 96008 位数据位、1 位停止位、无校验。这个 9600 不是随便定的它是 HC-05、HC-06、AT-09 出厂默认的通信波特率第一次通信必须跟它对齐等 AT 指令改完再同步改代码。有人会问波特率到底怎么算出来的这里把过程写清楚方便你以后改频率时心里有数。以常见的 STM32F103 为例系统主频 72MHzUSART1 挂在 APB2 上也是 72MHz。波特率寄存器 USARTDIV 的计算公式是USARTDIV fCK / (16 × 波特率)代入 960072 000 000 / (16 × 9600) 468.75。整数部分是 468也就是 0x1D4写进 BRR 的高 12 位小数部分 0.75 乘以 16 得 12也就是 0xC写进低 4 位。合起来 BRR 0x1D4C。再算一次 11520072 000 000 / (16 × 115200) 39.0625整数 39 0x27小数 0.0625 × 16 1所以 BRR 0x271。算完后误差是 0.0625/39 ≈ 0.16%远小于串口能容忍的 2%~3%可以放心用。理解这个计算的意义在于如果你把主频改成 64MHz 却忘了重新生成代码串口就会开始出乱码而且这种乱码很规律——每个字节都错但每次错得一样特别好认。2.2 中断、DMA 还是轮询三种接收方式的取舍初学者最常用的是 HAL_UART_Receive 轮询简单直接但只要你主循环里有延时或者有屏幕刷新就会漏字节。我的建议是只要数据不是“发一条等一条”的问答式一律上中断或 DMA。中断方式适合数据量小、实时性要求高的场合比如每 100ms 来一帧十几字节的传感器数据。DMA 加空闲中断IDLE适合不定长数据蓝牙透传模块发过来的数据长度是完全不可控的用户手指按一下可能发 3 个字节也可能发 200 个字节这种情况用 IDLE 中断一次性把这一批数据搬完最省心。我的一般做法是底层用 DMA IDLE 把数据丢进环形缓冲区上层用状态机从缓冲区里找帧头。这样无论上层业务多忙底层都不会丢数据缓冲区大小给 512 或 1024 字节够应付绝大多数场景。注意DMA 接收要在启动前调用一次 HAL_UARTEx_ReceiveToIdle_DMA并且要在回调里重新启动否则只能收到第一批数据。这个坑我第一次用时卡了整整一个下午。3. AT 指令阶段把模块“驯服”成你要的样子3.1 进 AT 模式的两种姿势以及为什么总进不去HC-05 有两种进 AT 模式的方式。一种是“上电前按住 KEY 键再上电”此时模块进入 AT 模式指示灯慢闪大约 2 秒一次串口波特率固定为 38400注意是 38400 不是 9600这是最容易搞错的地方。另一种是模块已经在通信状态直接把 KEY 拉高也能进 AT 模式但有些批次的固件不认这种方式。HC-06 就更简单没有 KEY 脚只要没有配对连接直接发 AT 就是 AT 模式且波特率跟通信波特率一致。AT-09 这类 BLE 模块通常是上电后一段时间内有的要求 200ms 内发 AT 才响应过了这个窗口就进入广播状态不再理 AT。也有的模块需要把某个脚拉低。这块一定要先看模块背面的丝印或者随附的说明型号兼容不代表行为完全一致。我实测下来最稳的流程是给 EN/KEY 脚接一个 GPIO初始化时拉高延时 100ms 后再给模块上电或者模块一直上电靠 EN 脚控制然后用 38400 波特率发 “AT\r\n”如果回 “OK” 就通了。如果回的是乱码八成是波特率不对把 9600、38400、115200 三个值挨个试一遍一般就能对上。顺带说一个自检技巧在怀疑模块之前先把 STM32 的 TX 和 RX 短接自己发自己收能收回来说明 MCU 侧串口配置没问题问题一定在模块或接线上。这一招能省掉一半的排查时间。3.2 常用 AT 指令对照与实测记录下面这张表是我在几个项目里反复用到的指令每个都实际验证过直接抄去用没问题。指令作用返回备注AT握手测试OK必须先通这条ATNAMEMyDev改设备名OK手机搜索时显示的名字ATPSWD1234改配对密码OKHC-05 用引号包起来也行ATUART115200,0,0改通信波特率OK三个参数是波特率、停止位、校验ATROLE0设从机OK1 是主机2 是回环ATROLE1设主机OK主机可主动搜索从机ATCMODE1任意地址连接OK0 是绑定地址ATBINDxxxx绑定对端地址OK地址格式用逗号分隔ATADDR?查本机地址ADDR:xxxx主从配对时要用ATRESET重启模块OK改完参数建议重启关于改波特率有个细节值得说。执行 ATUART115200,0,0 后模块会立刻切换到新波特率如果你还在用 9600 发指令后面就全是乱码。所以改完波特率的第一件事是把串口助手或 MCU 的波特率同步改掉。我吃过一次亏改完以为模块坏了重新烧了固件才发现只是自己没切波特率。还有一点HC-05 做主机时要配对从机需要先 ATROLE1再 ATCMODE0 或 1然后用 ATBIND 绑定对方地址最后重启。如果对方是手机基本不用做主机让手机当主机去连模块更简单。提示AT 指令结尾必须带 \r\n只发 \r 有的固件不认。用串口助手时记得勾上“发送新行”。4. 代码实现从串口字节到可用数据4.1 环形缓冲区与 DMA 空闲中断接收底层我是这么写的先定义环形缓冲区结构再配好 DMA 接收。#define RB_SIZE 1024 typedef struct { uint8_t buf[RB_SIZE]; volatile uint16_t head; // 写入位置 volatile uint16_t tail; // 读出位置 volatile uint16_t overflow; } ring_t; static ring_t g_rb; static inline uint16_t rb_used(void) { return (g_rb.head - g_rb.tail) (RB_SIZE - 1); } void rb_put(const uint8_t *data, uint16_t len) { for (uint16_t i 0; i len; i) { uint16_t next (g_rb.head 1) (RB_SIZE - 1); if (next g_rb.tail) { // 满了丢弃并计数 g_rb.overflow; return; } g_rb.buf[g_rb.head] data[i]; g_rb.head next; } } int rb_get(uint8_t *out) { if (g_rb.head g_rb.tail) return 0; *out g_rb.buf[g_rb.tail]; g_rb.tail (g_rb.tail 1) (RB_SIZE - 1); return 1; }DMA 接收回调这么处理把收到的这一段整块塞进缓冲区然后立刻重启接收#define RX_DMA_BUF 256 static uint8_t g_dma_buf[RX_DMA_BUF]; void uart_start_rx(void) { HAL_UARTEx_ReceiveToIdle_DMA(huart1, g_dma_buf, RX_DMA_BUF); } void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t size) { if (huart-Instance USART1) { rb_put(g_dma_buf, size); HAL_UARTEx_ReceiveToIdle_DMA(huart1, g_dma_buf, RX_DMA_BUF); } }这里有个细节HAL_UARTEx_RxEventCallback 在 IDLE 和半满/全满时都会触发所以必须重新启动 DMA否则只收一次。另外不要在回调里做耗时操作塞进缓冲区就赶紧返回解析放到主循环里做。4.2 帧格式设计让数据自己说明边界蓝牙透传本身不保证任何帧边界你发 10 个字节对端可能分两次收到先 6 个再 4 个。所以业务层必须自己定协议。我的常用格式是帧头2字节 0xAA 0x55 长度1字节 命令1字节 载荷N字节 校验和1字节前面所有字节累加取低 8 位。解析用状态机逐字节喂进去只有校验通过才认为是一帧有效数据。这样即使中途丢了几个字节也能在下一个帧头处重新同步。typedef enum { S_H1, S_H2, S_LEN, S_CMD, S_DATA, S_SUM } pstate_t; static pstate_t st S_H1; static uint8_t len, cmd, idx, sum; static uint8_t payload[64]; void frame_parse_byte(uint8_t b) { switch (st) { case S_H1: if (b 0xAA) { st S_H2; sum b; } break; case S_H2: if (b 0x55) { st S_LEN; sum b; } else st S_H1; break; case S_LEN: len b; idx 0; sum b; st (len 0 len 64) ? S_CMD : S_H1; break; case S_CMD: cmd b; sum b; st (len 0) ? S_DATA : S_SUM; break; case S_DATA: payload[idx] b; sum b; if (idx len) st S_SUM; break; case S_SUM: st S_H1; if (b sum) { on_frame(cmd, payload, len); } break; default: st S_H1; break; } }写完这套之后你会在主循环里看到while (rb_get(b)) frame_parse_byte(b);一行搞定收数据。要注意 on_frame 里不要做阻塞操作比如里面有 HAL_Delay 就会把后面的帧堵住。4.3 发送端别让 printf 抢了串口的活一个高频问题调试时用 printf 打印日志结果发现蓝牙发不出去数据。原因是 printf 重定向到了同一个串口两边打架。解决办法是日志走另一路串口或者用 SEGGER RTT 之类不占串口的方式再或者干脆做一个带互斥的发送函数。发送时有个小技巧透传模块对连续大数据不敏感但对“两次发送间隔太短”比较敏感尤其是经典蓝牙一包数据理论上是 20 字节左右一个包太长会被自动分包。所以业务层的帧不要超过 200 字节超过了就自己拆。我一般控制在 64 字节以内无论是 HC-05 还是 BLE 模块都很稳。5. BLE 模块的额外功课GATT 到底是什么5.1 服务、特征值、UUID 的层级关系如果你用的是 AT-09 或者 HM-10 兼容模块通讯方式跟经典蓝牙完全不一样。它不是把串口数据原样广播出去而是把自己伪装成若干“服务”每个服务下面挂几个“特征值”手机写入特征值数据才真正到达模块的串口。可以这样理解模块是一栋楼服务是楼层特征值是房间号。手机要知道去几楼几号房间送东西这个“几楼几号”就是 UUID。常见的透传模块一般有两三个服务最有用的那个服务 UUID 通常是 0xFFE0其下的特征值 UUID 是 0xFFE1往 FFE1 写数据就相当于往模块的 RXD 发数据订阅 FFE1 的通知就能收到模块 TXD 发出来的数据。还有一点必须知道订阅通知不是连上就自动生效的。手机端要往一个叫 CCCD 的描述符UUID 0x2902里写 0x0001模块才会开始主动推送数据。很多“连上了但收不到”的情况都是漏了这一步。名称UUID方向说明服务0xFFE0-透传主服务特征值0xFFE1读写通知数据通道描述符 CCCD0x2902写写 0x0001 开启通知5.2 用手机验证链路的最短路径拿到 BLE 模块别急着写 STM32 代码先用手机把链路走通。步骤是打开任意通用 BLE 调试工具扫描找到模块名字默认可能是 AT-09 之类连接进去找 FFE0 服务点开 FFE1开启通知然后发送一串字符同时把模块的 TXD 接到 USB 转串口上看有没有收到。反过来也一样用串口助手从模块的 RXD 发数据手机端应该能收到通知。这一正一反走通了说明模块本身没问题再回去查 STM32 就快得多。注意BLE 模块的通信波特率和 AT 模式波特率往往是同一个改之前一定记下来。另外 BLE 连接后模块会停止广播手机再搜是搜不到的得先断开。6. 常见问题排查实录下面这张表是我这些年被问得最多的问题按“现象—可能原因—验证方法”整理照着试基本能定位。现象可能原因快速验证发 AT 没反应波特率不对 / 没进 AT 模式 / 没加 \r\n三个波特率轮着试确认 EN 脚电平AT 返回乱码主频与工程配置不一致检查 CubeMX 时钟树重算 BRR手机搜不到设备模块没供电 / 已连接别的主机 / 天线问题看 STATE 指示灯是否快闪能配对连不上密码错 / 模块设成了主机模式ATPSWD 检查确认 ROLE0连上但收不到数据没开通知 / 波特率不匹配 / 帧头错手机端先看原始字节数据偶尔丢包缓冲区太小 / 中断里做耗时操作加大 RB_SIZE检查回调一连就复位供电电流不足单独供电VCC 并 100μF距离只有一两米天线被金属遮挡 / 板子铺铜太近拉开距离远离电池和电机连上后声音卡顿2.4G 频段被 WiFi 干扰换信道避开路由器改完参数不生效没重启模块发 ATRESET 或断电重上关于干扰这块补充一点。蓝牙工作在 2.4GHz跟 WiFi、无线鼠标、USB3.0 接口都在同一个频段。我遇到过一次很奇怪的现象设备一插上 USB3.0 移动硬盘蓝牙就断拔掉就好。后来查出来是 USB3.0 的辐射干扰解决办法是把模块天线尽量远离 USB 口或者给模块加一小片屏蔽。这类问题看代码永远看不出来只能靠换位置试。另外一个高频坑是杜邦线。线太长超过 20cm、线太细、接触不良都会导致串口在 115200 下出现随机错误。调试阶段我建议先用短而粗的线等逻辑通了再考虑布线。7. 低功耗与稳定性的一些经验7.1 电池设备怎么省电如果是电池供电光把 STM32 进 STOP 模式没用蓝牙模块自己就在耗电。经典蓝牙模块连接态基本在 20mA 以上STOP 模式下整体功耗降不下来。所以电池设备我基本只用 BLE并且做这几件事一是把模块的广播间隔调长有的模块支持 AT 指令改广播周期从 100ms 改到 500ms平均电流能降一半。二是数据发完主动断开连接需要时再让手机重连别一直保持连接。三是 STM32 在等待期间进 STOP用模块的 STATE 引脚接 EXTI 唤醒因为 STOP 模式下串口时钟停了靠串口中断是唤不醒的这一点很多人会忽略。唤醒流程大致是配置 PA0 为 EXTI 上升沿触发进入 STOP 前把串口的 DMA 停掉HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI)被 STATE 引脚拉高唤醒后重新配置系统时钟STOP 唤醒后默认回到 HSI必须重新调 SystemClock_Config再重新初始化串口。7.2 PCB 上的几个小动作如果你已经走到画板子这一步天线部分值得多花点心思。模块如果带 PCB 天线天线正下方和正前方要完全净空不能铺铜不能走线最好伸出板边。我见过有人把模块贴着电池放结果距离从 10 米掉到 1 米。电源部分模块 VCC 到 GND 之间放 100μF 加 0.1μF位置尽量靠近模块引脚。串口线上串 22Ω 到 100Ω 的小电阻能抑制一点边沿过冲虽然多数时候用不上但板子便宜加上不亏。还有一个容易被忽略的调试口的 TX/RX 不要跟蓝牙模块的串口复用同一个 USART。我早期的板子为了省事把调试串口和蓝牙串口接在一起结果调试信息全被模块吃掉了还以为是模块在乱回数据。后来分开成两路世界清净了。8. 说说我踩过的两个具体坑第一个是“能收到但格式全乱”。当时用 HC-05 做从机STM32 发一串中文过去手机收到的全是问号。查了半天代码最后发现是编码问题——串口助手发的是 GBK手机端按 UTF-8 解析。这跟蓝牙本身没关系但表现为“蓝牙传数据有问题”很容易带偏方向。后来我养成了一个习惯调通链路时一律用 0x00 到 0xFF 的原始字节测试不用字符串这样能立刻区分是链路问题还是编码问题。第二个是“隔一段时间就断一次”。现象很规律大概每 30 秒断一次重连就好。排查了电源、天线、代码最后发现是模块进了某种省电休眠。解决办法是把模块的某个配置脚拉高或者在固件里关闭休眠。这件事给我的教训是模块说明书上那些看起来不重要的脚往往决定了长时间运行的稳定性别嫌麻烦能不悬空就不悬空。调试蓝牙这类东西我的经验是永远分层验证先确认 MCU 串口自环没问题再确认模块 AT 能通再确认手机能连上再确认单字节能收发最后才是你的业务协议。跳过任何一层后面的问题都会被放大成“玄学”。手边常备一个 USB 转串口和一根短杜邦线比看十篇文档都管用。