news 2026/10/29 14:13:33

STM32蓝牙模块串口透传实战:选型、AT指令、DMA接收与排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32蓝牙模块串口透传实战:选型、AT指令、DMA接收与排错

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(或 KEY),HC-05 这类模块拉高这个脚再上电才能进 AT 模式,把它接到一个 GPIO 上,用软件控制进 AT 模式,比每次手动拔插杜邦线舒服太多。

供电这块要单独说。蓝牙模块在发射瞬间的电流会有脉冲,尤其是经典蓝牙,峰值能到 40mA 以上。如果你从 STM32 板子上那颗小封装 LDO 取电,同时又带着屏幕、传感器,很可能出现“一连上就复位”的现象。我的习惯是模块单独走一路电源,或者在模块 VCC 旁边并一个 100μF 电解加 0.1μF 瓷片,这两个电容不是摆设,能救回很多莫名其妙的掉线。

2. 工程环境与串口配置,参数不是随便填的

2.1 CubeMX 里的串口配置与波特率计算过程

我用得最多的是 HAL 库加 CubeMX,配置流程很固定:打开 USART1,模式选 Asynchronous,波特率先填 9600,8 位数据位、1 位停止位、无校验。这个 9600 不是随便定的,它是 HC-05、HC-06、AT-09 出厂默认的通信波特率,第一次通信必须跟它对齐,等 AT 指令改完再同步改代码。

有人会问波特率到底怎么算出来的,这里把过程写清楚,方便你以后改频率时心里有数。以常见的 STM32F103 为例,系统主频 72MHz,USART1 挂在 APB2 上也是 72MHz。波特率寄存器 USARTDIV 的计算公式是:

USARTDIV = fCK / (16 × 波特率)

代入 9600:72 000 000 / (16 × 9600) = 468.75。整数部分是 468,也就是 0x1D4,写进 BRR 的高 12 位;小数部分 0.75 乘以 16 得 12,也就是 0xC,写进低 4 位。合起来 BRR = 0x1D4C。

再算一次 115200:72 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必须先通这条
AT+NAME=MyDev改设备名OK手机搜索时显示的名字
AT+PSWD=1234改配对密码OKHC-05 用引号包起来也行
AT+UART=115200,0,0改通信波特率OK三个参数是波特率、停止位、校验
AT+ROLE=0设从机OK1 是主机,2 是回环
AT+ROLE=1设主机OK主机可主动搜索从机
AT+CMODE=1任意地址连接OK0 是绑定地址
AT+BIND=xxxx绑定对端地址OK地址格式用逗号分隔
AT+ADDR?查本机地址+ADDR:xxxx主从配对时要用
AT+RESET重启模块OK改完参数建议重启

关于改波特率,有个细节值得说。执行 AT+UART=115200,0,0 后模块会立刻切换到新波特率,如果你还在用 9600 发指令,后面就全是乱码。所以改完波特率的第一件事是把串口助手或 MCU 的波特率同步改掉。我吃过一次亏,改完以为模块坏了,重新烧了固件才发现只是自己没切波特率。

还有一点,HC-05 做主机时要配对从机,需要先 AT+ROLE=1,再 AT+CMODE=0 或 1,然后用 AT+BIND 绑定对方地址,最后重启。如果对方是手机,基本不用做主机,让手机当主机去连模块更简单。

提示: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 指示灯是否快闪
能配对连不上密码错 / 模块设成了主机模式AT+PSWD 检查,确认 ROLE=0
连上但收不到数据没开通知 / 波特率不匹配 / 帧头错手机端先看原始字节
数据偶尔丢包缓冲区太小 / 中断里做耗时操作加大 RB_SIZE,检查回调
一连就复位供电电流不足单独供电,VCC 并 100μF
距离只有一两米天线被金属遮挡 / 板子铺铜太近拉开距离,远离电池和电机
连上后声音卡顿2.4G 频段被 WiFi 干扰换信道,避开路由器
改完参数不生效没重启模块发 AT+RESET 或断电重上

关于干扰这块补充一点。蓝牙工作在 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 转串口和一根短杜邦线,比看十篇文档都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 14:10:50

CEC2013测试集复现:input常数读取、评估器实现与对比实验避坑

简介&#xff1a;这份CEC2013测试集面向进化计算、群体智能与连续优化方向的研究生及算法工程师&#xff0c;用于在统一基准上复现、比较和验证优化算法的收敛精度、稳定性与鲁棒性。压缩包共32个文件&#xff0c;以28个txt输入数据与说明文本为主体&#xff0c;辅以2个m脚本、…

作者头像 李华
网站建设 2026/10/29 14:08:58

硒代蛋氨酸与亚硒酸盐:代谢机制、形态分析与营养评价

硒这个元素挺有意思&#xff0c;在元素周期表上排第34位&#xff0c;紧挨着砷和溴&#xff0c;单质形态下毒性不小&#xff0c;但偏偏在生物体内以特定有机形态存在时&#xff0c;又成了不可或缺的微量元素。我第一次系统接触硒的生化代谢&#xff0c;是因为一个做植物富硒研究…

作者头像 李华
网站建设 2026/10/29 14:08:00

EDCA架构与QAS测试框架:事件驱动的高效自动化测试实践

1. EDCA OS体系与QAS实验框架概述在分布式计算和自动化测试领域&#xff0c;EDCA&#xff08;Event-Driven Component Architecture&#xff09;操作系统体系正逐渐成为构建高响应性系统的首选架构。这个体系最显著的特点是采用事件驱动模型作为核心通信机制&#xff0c;所有组…

作者头像 李华
网站建设 2026/10/29 14:07:49

网桥与交换机:核心区别与组网选型指南

1. 网桥与交换机的本质区别当我们在办公室或家里连接多台设备上网时&#xff0c;经常会听到"交换机"这个词&#xff0c;而"网桥"则相对陌生。实际上&#xff0c;这两者都是网络互联设备&#xff0c;但工作层级和应用场景有着根本差异。网桥&#xff08;Bri…

作者头像 李华
网站建设 2026/10/29 14:07:21

营销技能全解析:从用户洞察到数据复盘的实战方法论

做营销这些年&#xff0c;我越来越觉得“marketingskills”这个词被低估了。外行以为营销就是写文案、做海报、发朋友圈&#xff1b;刚入行一两年的朋友觉得营销是投广告、做活动、追热点&#xff1b;但真正在一线扛过KPI、操盘过完整项目的人会告诉你&#xff0c;营销技能本质…

作者头像 李华