stm32教程:蓝牙模块通信
蓝牙模块是STM32入门阶段最常用的无线通信模块之一,无论是做遥控小车、数据采集上传,还是简单的人机交互,都能用到。我们从基础理论到实操落地,一步步搞定STM32F103C8T6与蓝牙模块的通信,重点实现“手机发数据,STM32收数据并回传”的核心功能,今天一篇文章带大伙全部搞定。

文章目录
- 一、蓝牙分类:BLE与SPP,到底该选哪一个?
- 1. 核心定义
- 2. 区别与联系
- 二、手机蓝牙软件
- 1. 软件下载与安装
- 2. 软件核心界面与功能
- (1)蓝牙连接区
- (2)数据收发区:分为“接收区”和“发送区”,是我们后续实操的核心:
- (3)专业调试 和 按钮控制
- 设置(注意)
- 3. 其他常用软件补充
- 三、蓝牙AT指令:配置蓝牙模块的“钥匙”
- 1. AT指令基础认知
- 2. 常见蓝牙模块(HC-05)进入AT模式的方法
- 3. 常用AT指令(HC-05通用,必记)
- 4. AT指令配置实操注意事项
- 四、让蓝牙模块和STM32F103C8T6互相通信
- 1. 所需硬件准备
- 2. 硬件接线
- 3. 软件准备
- 4. 核心步骤拆解(一步一实操)
- 步骤1:配置蓝牙模块(复用第二部分的AT指令)
- 步骤2:STM32硬件接线
- 步骤3:Keil5代码编写与下载
- Serial_SPP.c
- Serial_SPP.h
- main.c
- 步骤4:测试蓝牙模块与STM32的通信
- 文本模式收发演示
- 五、实战:手机蓝牙发送数据包,STM32接收并回传
- 1. 数据包格式定义
- 2. STM32代码优化(实现数据包接收与回传)
- 3. 核心代码片段
- DataPacket_SPP.h
- DataPacket_SPP.c
- 4. 实战测试步骤
- 5. 常见问题排查
- 六、尾声
- 感谢大伙观看,别忘了三连支持一下
- 大伙也可以关注一下我的其它专栏,同样精彩喔~
- 大伙也可以添加下面的微信来交流
在开始实操前,我们先理清几个核心概念,避免后续踩坑。
一、蓝牙分类:BLE与SPP,到底该选哪一个?
我们在STM32开发中常用的蓝牙模块,本质上分为两类:SPP蓝牙(经典蓝牙)和BLE蓝牙(低功耗蓝牙)。很多新手刚开始会纠结“选哪种”,其实核心看你的应用场景,先搞懂两者的区别和联系,再选择就很简单了。
说明:下面的讲解是针对蓝牙的底层实现,两者对于与stm32通信的使用来说是一样的,大伙不要担心。
这里的讲解主要含义在于让大伙明白两种蓝牙的实现完全不同,对于手机APP解析蓝牙传输过来的信号不同,也就是说有些软件支持SPP,有些支持BLE,也有两种都支持的,这部分的讲解可以看第二部分。
1. 核心定义
• SPP蓝牙:全称Serial Port Profile(串口透传协议),属于经典蓝牙(Bluetooth Classic),我们常说的HC-05、HC-06模块,就属于SPP蓝牙。它的核心作用是“模拟有线串口”,把蓝牙通信转换成我们熟悉的UART串口通信,不需要复杂的协议解析。
• BLE蓝牙:全称Bluetooth Low Energy(低功耗蓝牙),属于蓝牙4.0及以上版本的协议,常用模块有CC2541、ESP32自带的BLE功能。它的核心优势是“低功耗”,适合电池供电的设备(比如智能手环、传感器节点),但协议相对复杂,需要解析GATT、UUID等。
2. 区别与联系
先上核心区别表格,清晰明了:
| 对比维度 | SPP蓝牙(经典蓝牙) | BLE蓝牙(低功耗蓝牙) |
|---|---|---|
| 功耗 | 较高,适合外接电源设备 | 极低,适合电池供电设备 |
| 通信速率 | 较高(约1Mbps),可传输大数据量 | 较低(约100kbps),适合小数据量传输 |
| 协议复杂度 | 极低,串口透传,无需解析复杂协议 | 较高,需解析GATT服务、UUID、特征值 |
| 上手难度 | 新手友好,1-2小时可实现通信 | 有一定门槛,需掌握BLE协议基础 |
| 常用模块 | HC-05、HC-06(性价比高,最常用) | CC2541、ESP32-BLE、nRF52832 |
| 应用场景 | 遥控小车、串口调试、数据实时传输 | 智能穿戴、传感器采集、低功耗设备 |
补充说明:两者都属于蓝牙技术,本质都是实现无线数据传输,支持手机、电脑等设备的连接;部分高端模块支持“双模”(同时支持SPP和BLE),兼顾实用性和低功耗,但价格稍高,新手入门不推荐,先吃透一种即可。
二、手机蓝牙软件
实现手机与STM32蓝牙通信,需要一款手机端的蓝牙调试软件——核心作用是“连接蓝牙模块、发送数据、接收STM32回传的数据”。市面上有很多蓝牙调试软件,比如Serial Bluetooth Terminal、蓝牙调试器、BLE调试助手等。
额外说明,目前 IOS端 没有支持SPP协议的APP,小程序也没有支持SPP协议的APP
今天重点讲解“蓝牙调试器”(安卓端,新手最友好,支持SPP和BLE,且数据包格式可自定义,贴合本文后续实操)。
1. 软件下载与安装
• 下载渠道:应用商店搜索“蓝牙调试器”(图标多为蓝色串口样式,开发者多为“慧净科技”或类似,任意一款均可,功能大同小异),免费下载,无需付费。软件也会在文末的网盘中提供。
• 兼容性:支持安卓4.4及以上版本,几乎所有安卓手机都能使用;苹果手机可搜索“Bluetooth Serial”,功能类似。
2. 软件核心界面与功能
打开软件后,界面分为 4 个核心部分:
(1)蓝牙连接区
打开手机蓝牙后,软件会自动搜索周围的蓝牙设备,找到我们配置好的蓝牙模块(比如我们设置的“BunnySPP”),点击即可连接。连接成功后会在左上角显示连接的蓝牙名称,如果这个软件不支持的蓝牙的协议,你就会搜不到,这时候就需要换一个软件或者换一个蓝牙。

注意:连接前需确保手机蓝牙已打开;如果连接失败,检查配对密码是否正确、模块是否正常供电、手机与模块距离是否过远(建议1米内)。
(2)数据收发区:分为“接收区”和“发送区”,是我们后续实操的核心:
• 接收区:显示STM32通过蓝牙回传的数据,支持显示十六进制(Hex)和字符串(ASCII)格式,后续我们会用到Hex格式(更规范,避免中文乱码)。
• 发送区:我们在这里输入要发送的数据包,支持手动输入、自动发送,同样支持Hex和ASCII格式,数据包格式后续会详细说明。

(3)专业调试 和 按钮控制
专业调试:这个模式可以设置多个组件,然后将所有信息组成一个数据包,然后发送出去,这个我们后面详细讲解一下。
按钮控制:这个我们这期不讲。


设置(注意)
这里有一个非常重要的地方需要大伙注意:
换行符替换,一定要和自己USART上通信设置的一样,否则会收不到信息,我一开始被这里困住了 T_T

3. 其他常用软件补充
除了蓝牙调试器,还有2款软件推荐,适合不同场景:
• Serial Bluetooth Terminal:界面更简洁,支持自定义波特率、数据包格式,适合习惯简约风格的小伙伴,操作和蓝牙调试器类似。
• BLE调试助手:专门用于BLE蓝牙模块的调试,支持解析GATT服务、UUID。
三、蓝牙AT指令:配置蓝牙模块的“钥匙”
无论你用哪种蓝牙模块,要让它正常工作,都需要先通过AT指令进行配置——比如设置蓝牙名称、波特率、配对密码、角色(主从机)等。很多新手对AT指令感到陌生,其实它非常简单,本质就是“指令+响应”的交互方式,和我们用串口发送指令控制STM32的逻辑一致。
1. AT指令基础认知
• 定义:AT指令(Attention Command)是一种用于控制调制解调器、蓝牙模块等通信设备的标准化指令集,蓝牙模块沿用了这一指令集,用于配置模块参数。
• 核心逻辑:我们通过串口(UART)向蓝牙模块发送AT指令,模块执行指令后,会通过串口返回响应信息(比如“OK”表示指令执行成功,“ERROR”表示指令错误)。
• 关键前提:配置AT指令时,蓝牙模块必须处于“AT模式”(而非通信模式),不同模块进入AT模式的方式不同,这是新手最容易踩的坑!
2. 常见蓝牙模块(HC-05)进入AT模式的方法
HC-05模块进入AT模式有两种方式,推荐第一种(简单易操作):
方式1:断电状态下,按住模块上的KEY键(或EN键),再给模块通电,此时模块上的LED灯“慢闪”(约1秒闪1次),表示成功进入AT模式。
方式2:通过AT指令切换(需先正常通信),发送“AT+RESET”指令重启模块,重启时自动进入AT模式,但这种方式需要先知道模块的默认参数,新手不推荐。
注意:HC-06模块没有KEY键,默认就是AT模式(通电即可配置),但HC-06只能作为从机,无法设置为主机,而HC-05可以设置为主机或从机,灵活性更高,新手推荐用HC-05。
3. 常用AT指令(HC-05通用,必记)
HC-05默认参数(未配置前):波特率9600、数据位8、停止位1、校验位无、配对密码1234、角色从机、蓝牙名称“HC-05”。
以下是新手必备的AT指令,建议收藏,直接复用(指令不区分大小写,末尾需加“回车换行”,即CR+LF,否则模块无法识别):
| AT指令 | 指令功能 | 响应信息 | 补充说明 |
|---|---|---|---|
| AT | 测试指令(判断模块是否正常响应) | OK | 最基础的指令,用于验证AT模式是否生效 |
| AT+NAMExxx | 设置蓝牙名称(xxx为自定义名称) | OK+NAME:xxx | 例:AT+NAMESTM32_BLE,设置名称为STM32_BLE |
| AT+BAUDx | 设置波特率(x为对应参数) | OK+BAUD:x | x对应值:1=1200,2=2400,3=4800,4=9600(默认),5=19200 |
| AT+PINxxxx | 设置配对密码(xxxx为4位数字) | OK+PIN:xxxx | 例:AT+PIN6666,设置密码为6666,建议设简单好记的 |
| AT+ROLEx | 设置角色(主从机) | OK+ROLE:x | x=0:从机(默认,手机主动连接模块);x=1:主机(模块主动连接其他蓝牙设备) |
| AT+RESET | 重启模块(使配置生效) | OK+RESET | 所有配置修改后,必须发送此指令,否则配置不生效 |
| AT+VERSION | 查询模块固件版本 | OK+VERSION:xxxx | 用于判断模块是否为HC-05,避免用错指令 |

4. AT指令配置实操注意事项
-
串口参数必须与蓝牙模块默认参数一致(默认9600,8N1),否则无法发送指令;如果修改了波特率,后续配置需切换对应波特率。
-
指令末尾必须加“回车换行”(CR+LF),很多新手忘记加,导致模块无响应——串口调试助手(如SSCOM)中,勾选“发送新行”即可。
-
配置完成后,发送“AT+RESET”重启模块,重启后模块退出AT模式,进入通信模式(LED灯快闪,约0.5秒闪1次)。
-
如果模块无响应,检查:KEY键是否按住、接线是否正确(TX/RX交叉连接)、模块供电是否稳定(HC-05建议3.3V供电,避免5V烧模块)。
四、让蓝牙模块和STM32F103C8T6互相通信
前面我们搞定了理论、指令和工具,现在进入最核心的部分——将蓝牙模块与STM32F103C8T6连接,完成硬件接线、软件配置,让蓝牙模块能正常与STM32通信。这部分步骤必须严格按照顺序操作,新手建议一步一检查,避免接线错误烧模块或STM32。
1. 所需硬件准备
• 主控板:STM32F103C8T6最小系统板。
• 蓝牙模块:HC-05模块(SPP蓝牙,带KEY键,方便进入AT模式),建议搭配杜邦线。
• 供电设备:USB-TTL模块(用于给STM32下载程序、给蓝牙模块供电),或独立3.3V电源。
• 其他:杜邦线(4-6根)、电脑(安装Keil5、USB-TTL驱动)、安卓手机(安装蓝牙调试器)。
2. 硬件接线
核心原则:蓝牙模块的TX接STM32的RX,蓝牙模块的RX接STM32的TX(交叉连接),供电必须为3.3V(HC-05不支持5V,接5V会烧模块),GND共地。与USART是一样的。
我们选用STM32F103C8T6的USART1(串口1)作为与蓝牙模块的通信串口。
STM32F103C8T6 USART1引脚定义:
• USART1_TX:PA9(发送引脚,STM32向蓝牙模块发送数据)
• USART1_RX:PA10(接收引脚,STM32接收蓝牙模块的数据)
接线对应关系(HC-05 → STM32F103C8T6):
| HC-05模块引脚 | STM32F103C8T6引脚 | 备注 |
|---|---|---|
| VCC | 3.3V | 必须3.3V,禁止接5V! |
| GND | GND | 共地,否则通信异常 |
| TXD | PA10(USART1_RX) | 交叉连接,蓝牙发送 → STM32接收 |
| RXD | PA9(USART1_TX) | 交叉连接,STM32发送 → 蓝牙接收 |
| KEY | 无需连接(或接3.3V,用于固定AT模式) | 新手可不用接,配置AT模式时手动按KEY键即可 |
接线检查要点:
-
确认VCC接3.3V,GND共地,TX/RX交叉连接,没有接反。
-
蓝牙模块和STM32的供电要稳定,USB-TTL模块的5V接口可给STM32供电,3.3V接口给蓝牙模块供电。
-
接线完成后,先通电检查:STM32最小系统板的电源灯亮,蓝牙模块的LED灯快闪(通信模式),说明接线正常。
3. 软件准备
软件核心需求:配置STM32的USART1(串口1),实现“接收蓝牙模块发送的数据 → 原样回传蓝牙模块”(后续会优化为自定义数据包格式),波特率与蓝牙模块一致(默认9600)。
前提:电脑已安装Keil5(支持STM32F103系列)、STM32F103固件库(或使用HAL库,本文用标准固件库)、USB-TTL驱动(确保能识别STM32)。
4. 核心步骤拆解(一步一实操)
步骤1:配置蓝牙模块(复用第二部分的AT指令)
-
USB-TTL模块连接蓝牙模块(VCC→3.3V,GND→GND,TX→蓝牙RX,RX→蓝牙TX),按住蓝牙模块KEY键,给USB-TTL通电,进入AT模式(LED慢闪)。
-
打开串口调试助手(如SSCOM),配置串口参数:波特率9600,数据位8,停止位1,校验位无,勾选“发送新行”。
-
发送AT指令配置模块:
-
AT(测试响应,返回OK)
-
AT+NAMEBunnySPP(设置名称,方便手机识别)
-
AT+PIN1234(设置密码,简单易记)
-
AT+BAUD4(设置波特率9600,对应x=4)
-
AT+ROLE0(设置为从机,手机主动连接)
-
AT+RESET(重启模块,配置生效)
- 重启后,蓝牙模块LED快闪,退出AT模式,配置完成,断开USB-TTL与蓝牙模块的连接。
步骤2:STM32硬件接线
将蓝牙模块与STM32F103C8T6按前面的接线表连接,确保接线正确,然后用USB-TTL模块给STM32供电(USB-TTL的5V接STM32的5V,GND接GND)。
步骤3:Keil5代码编写与下载
这里和之前的USART的代码一致,就不做讲解,如果不是很清楚可以跳转看看那篇文章:https://blog.csdn.net/sikimayi/article/details/147893516。
下面的代码做了一些修改,大伙可以使用新的。
说一下修改的地方:
因为收发HEX和文本的包头包尾不同,我做了一些修改,只需要修改一下参数,就能够切换两种收发模式。详细用法可以看一下代码中的注释。
Serial_SPP.c
#include "Serial_SPP.h"
#include
#include
/************************* 全局变量定义 *************************/
#ifdef SERIAL_SPP_USE_TEXT_PACKET
char Serial_SPP_RxPacket[SERIAL_SPP_RX_BUF_LEN]; // 文本数据包缓冲区
#endif
#ifdef SERIAL_SPP_USE_HEX_PACKET
uint8_t Serial_SPP_RxPacket[SERIAL_SPP_RX_BUF_LEN]; // HEX数据包缓冲区
#endif
uint8_t Serial_SPP_RxFlag = 0; // 接收标志位(初始为0)
/**
* @brief 串口初始化函数(USART1)
* @param 无
* @retval 无
* @note 配置参数:9600波特率、8位数据位、1位停止位、无校验、收发使能;
* PA9=复用推挽输出(TX),PA10=上拉输入(RX);开启接收中断,NVIC分组2
*/
void Serial_SPP_Init(void)
{
// 1. 初始化GPIO结构体
GPIO_InitTypeDef GPIO_InitStruct;
// 2. 初始化USART结构体
USART_InitTypeDef USART_InitStruct;
// 3. 初始化NVIC结构体
NVIC_InitTypeDef NVIC_InitStruct;
// 步骤1:使能时钟(GPIOA + USART1)
RCC_APB2PeriphClockCmd(SERIAL_SPP_GPIO_CLK | SERIAL_SPP_USART_CLK, ENABLE);
// 步骤2:配置TX引脚(PA9)- 复用推挽输出
GPIO_InitStruct.GPIO_Pin = SERIAL_SPP_TX_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SERIAL_SPP_GPIO_PORT, &GPIO_InitStruct);
// 步骤3:配置RX引脚(PA10)- 上拉输入
GPIO_InitStruct.GPIO_Pin = SERIAL_SPP_RX_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(SERIAL_SPP_GPIO_PORT, &GPIO_InitStruct);
// 步骤4:配置USART核心参数
USART_InitStruct.USART_BaudRate = 9600; // 波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // 收发双模式
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 1位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 8位数据位
USART_Init(SERIAL_SPP_USART, &USART_InitStruct);
// 步骤5:开启串口接收中断(RXNE)
USART_ITConfig(SERIAL_SPP_USART, USART_IT_RXNE, ENABLE);
// 步骤6:配置NVIC中断分组(分组2:2位抢占优先级 + 2位响应优先级)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 步骤7:配置USART1中断的NVIC参数
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; // 选择USART1中断线
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能中断线
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; // 响应优先级1
NVIC_Init(&NVIC_InitStruct);
// 步骤8:使能USART1外设
USART_Cmd(SERIAL_SPP_USART, ENABLE);
}
/**
* @brief 串口发送一个字节
* @param Byte 要发送的单字节数据(0~255)
* @retval 无
* @note 阻塞式发送,等待发送寄存器为空后再写入
*/
void Serial_SPP_SendByte(uint8_t Byte)
{
USART_SendData(SERIAL_SPP_USART, Byte); // 写入数据到发送寄存器
// 等待发送完成(TXE标志位为1表示发送寄存器空)
while (USART_GetFlagStatus(SERIAL_SPP_USART, USART_FLAG_TXE) == RESET);
}
/**
* @brief 串口发送数组
* @param Array 数组首地址
* @param Length 数组长度(字节数)
* @retval 无
* @note 逐字节发送数组内容,阻塞式
*/
void Serial_SPP_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i++)
{
Serial_SPP_SendByte(Array[i]); // 逐字节发送
}
}
/**
* @brief 串口发送字符串
* @param String 字符串首地址(以' '结尾)
* @retval 无
* @note 逐字节发送字符串,直到遇到结束符' '
*/
void Serial_SPP_SendString(char *String)
{
uint8_t i = 0;
while (String[i] != ' ')
{
Serial_SPP_SendByte(String[i]); // 逐字节发送
i++;
}
}
/**
* @brief 内部辅助函数:计算X的Y次方
* @param X 底数
* @param Y 指数(非负整数)
* @retval X^Y的计算结果
* @note 仅用于Serial_SPP_SendNumber函数内部
*/
static uint32_t Serial_SPP_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
/**
* @brief 串口发送数字(指定显示长度)
* @param Number 要发送的数字(0~4294967295)
* @param Length 数字显示长度(1~10)
* @retval 无
* @note 按指定长度补前导0,例如Number=123、Length=5则发送"00123"
*/
void Serial_SPP_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
// 逐位提取数字并转换为ASCII码发送
Serial_SPP_SendByte(Number / Serial_SPP_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief 串口格式化输出函数
* @param format 格式化字符串
* @param ... 可变参数列表
* @retval 无
* @note 基于vsprintf实现,支持%d/%s/%c等printf常用格式符
*/
void Serial_SPP_Printf(char *format, ...)
{
char String[SERIAL_SPP_RX_BUF_LEN]; // 临时缓冲区
va_list arg; // 可变参数列表
va_start(arg, format); // 初始化可变参数
vsprintf(String, format, arg); // 格式化字符串到缓冲区
va_end(arg); // 结束可变参数处理
Serial_SPP_SendString(String); // 发送格式化后的字符串
}
/**
* @brief USART1中断服务函数(处理接收数据包)
* @param 无
* @retval 无
* @note 文本模式:包头@ → 数据 → 包尾
;
* HEX模式:包头0xA5 → 数据 → 包尾0x5A;
* 接收完成后置Serial_SPP_RxFlag=1,缓冲区自动添加结束符(文本加' ',HEX无)
*/
void USART1_IRQHandler(void)
{
// 状态机状态(文本:0=等@,1=收数据,2=等
;HEX:0=等0xA5,1=收数据,2=等0x5A)
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0; // 接收缓冲区指针
uint8_t RxData = 0; // 临时接收字节
// 检查是否为接收中断(RXNE)
if (USART_GetITStatus(SERIAL_SPP_USART, USART_IT_RXNE) == SET)
{
RxData = USART_ReceiveData(SERIAL_SPP_USART); // 读取接收字节
#ifdef SERIAL_SPP_USE_TEXT_PACKET
// ========== 文本数据包解析逻辑 ==========
switch (RxState)
{
case 0: // 状态0:等待包头@
if (RxData == '@' && Serial_SPP_RxFlag == 0)
{
RxState = 1; // 切换到接收数据状态
pRxPacket = 0; // 重置缓冲区指针
}
break;
case 1: // 状态1:接收数据,等待
if (RxData == '
')
{
RxState = 2; // 切换到等待
状态
}
else
{
// 数据存入缓冲区(防止越界)
if (pRxPacket < SERIAL_SPP_RX_BUF_LEN - 1)
{
Serial_SPP_RxPacket[pRxPacket] = RxData;
pRxPacket++;
}
}
break;
case 2: // 状态2:等待
,完成数据包接收
if (RxData == '
')
{
RxState = 0; // 重置状态机
Serial_SPP_RxPacket[pRxPacket] = ' '; // 添加字符串结束符
Serial_SPP_RxFlag = 1; // 置接收完成标志
}
break;
default:
RxState = 0; // 异常状态重置
break;
}
#endif
#ifdef SERIAL_SPP_USE_HEX_PACKET
// ========== HEX数据包解析逻辑 ==========
switch (RxState)
{
case 0: // 状态0:等待包头0xA5
if (RxData == 0xA5 && Serial_SPP_RxFlag == 0)
{
RxState = 1; // 切换到接收数据状态
pRxPacket = 0; // 重置缓冲区指针
}
break;
case 1: // 状态1:接收数据,等待包尾0x5A
if (RxData == 0x5A)
{
RxState = 0; // 重置状态机
Serial_SPP_RxFlag = 1; // 置接收完成标志
}
else
{
// 数据存入缓冲区(防止越界)
if (pRxPacket < SERIAL_SPP_RX_BUF_LEN - 1)
{
Serial_SPP_RxPacket[pRxPacket] = RxData;
pRxPacket++;
}
}
break;
default:
RxState = 0; // 异常状态重置
break;
}
#endif
USART_ClearITPendingBit(SERIAL_SPP_USART, USART_IT_RXNE); // 清除中断标志
}
}
Serial_SPP.h
#ifndef __SERIAL_SPP_H
#define __SERIAL_SPP_H
// 引入STM32核心头文件
#include "stm32f10x.h"
/************************* 数据包类型选择(二选一) *************************/
// #define SERIAL_SPP_USE_TEXT_PACKET // 启用文本数据包(@开头,
结尾)
#define SERIAL_SPP_USE_HEX_PACKET // 启用HEX数据包(0xA5开头,0x5A结尾)
/************************* 串口外设宏定义 *************************/
#define SERIAL_SPP_USART USART1 // 串口外设选择USART1
#define SERIAL_SPP_USART_CLK RCC_APB2Periph_USART1 // 串口时钟使能位
#define SERIAL_SPP_GPIO_PORT GPIOA // 串口引脚所在GPIO口
#define SERIAL_SPP_TX_PIN GPIO_Pin_9 // 串口发送引脚PA9
#define SERIAL_SPP_RX_PIN GPIO_Pin_10 // 串口接收引脚PA10
#define SERIAL_SPP_GPIO_CLK RCC_APB2Periph_GPIOA // GPIO口时钟使能位
#define SERIAL_SPP_RX_BUF_LEN 100 // 接收数据包缓冲区长度
/************************* 全局变量声明 *************************/
#ifdef SERIAL_SPP_USE_TEXT_PACKET
extern char Serial_SPP_RxPacket[SERIAL_SPP_RX_BUF_LEN]; // 文本数据包缓冲区(格式@MSG
)
#endif
#ifdef SERIAL_SPP_USE_HEX_PACKET
extern uint8_t Serial_SPP_RxPacket[SERIAL_SPP_RX_BUF_LEN]; // HEX数据包缓冲区(格式0xA5+数据+0x5A)
#endif
extern uint8_t Serial_SPP_RxFlag; // 接收数据包完成标志位(1=完成)
/************************* 串口核心函数声明 *************************/
void Serial_SPP_Init(void); // 串口初始化(9600波特率,8N1,接收中断)
void Serial_SPP_SendByte(uint8_t Byte); // 串口发送一个字节
void Serial_SPP_SendArray(uint8_t *Array, uint16_t Length); // 串口发送数组
void Serial_SPP_SendString(char *String); // 串口发送字符串
void Serial_SPP_SendNumber(uint32_t Number, uint8_t Length); // 串口发送数字(指定长度)
void Serial_SPP_Printf(char *format, ...); // 串口格式化输出
#endif
main.c
#include "stm32f10x.h" // Device header
#include "string.h"
#include "Delay.h"
#include "LED.h"
#include "OLED.h"
#include "Timer.h"
#include "Serial_SPP.h"
#include "DataPacket_SPP.h"
DataPacket_TypeDef packet;
int main(void)
{
/*模块初始化*/
LED_Init();
OLED_Init(); //OLED初始化
Serial_SPP_Init(); //串口初始化
/*显示静态字符串*/
OLED_ShowString(0, 0, "RxPacket", OLED_8X16);
OLED_Update();
while (1)
{
if (Serial_SPP_RxFlag == 1) //如果接收到数据包
{
OLED_Printf(0, 16, OLED_8X16, Serial_SPP_RxPacket);
OLED_Update();
Serial_SPP_Printf(Serial_SPP_RxPacket);
Serial_SPP_RxFlag = 0;
}
}
}
步骤4:测试蓝牙模块与STM32的通信
-
确保STM32和蓝牙模块正常供电,蓝牙模块LED快闪(通信模式)。
-
手机打开蓝牙,打开蓝牙调试器,搜索“Bunny_SPP”,点击连接,输入配对密码1234,连接成功(软件提示连接成功,界面变绿)。
-
此时,我们已经完成了蓝牙模块在STM32上的基础运行配置,下一步就是实现“手机发数据,STM32收数据并回传”。
文本模式收发演示
大伙可以看一下下面的演示视频:
蓝牙演示
五、实战:手机蓝牙发送数据包,STM32接收并回传
前面的步骤都是铺垫,这一部分是本文的核心实战内容——我们将定义标准化的数据包格式(参考蓝牙调试器的规范),实现“手机发送一个数据包 → STM32接收数据包 → STM32回传一个相同格式的数据包 → 手机接收回传数据”,完成闭环通信,为后续更复杂的应用(如遥控、数据采集)打下基础。
1. 数据包格式定义
蓝牙调试器的数据包格式,常用的有两种:字符串格式和十六进制(Hex)格式。考虑到后续拓展(如传输传感器数据、控制指令),我们选用Hex格式(避免中文乱码,且便于解析多字节数据),数据包格式定义如下(新手可直接复用,也可自定义):
标准数据包格式:
帧头1(0xA5) + BOOL类型长度 + BYTE类型长度 + SHORT类型长度 + INT类型长度 + FLOAT类型长度 + 校验位 + 帧尾(0x5A)
举个栗子:0xA5 0x01 0x01 0x5A(只有一位BOOL类型,BOOL为01)
各字段说明:
• 帧头1(0xA5):用于STM32识别数据包的起始位置,避免误判(只有检测到0xA5,才认为是有效数据包)。
• 数据长度:BOOL类型长度 + BYTE类型长度 + SHORT类型长度 + INT类型长度 + FLOAT类型长度。每个类型的数量都可以自己定义。
• 校验位:用于校验数据包是否完整(原数据包中数据和的后八位)。
• 帧尾(0x5A):用于STM32识别数据包的结束位置,检测到0x5A,代表一个完整的数据包接收完成。
2. STM32代码优化(实现数据包接收与回传)
前面的代码只是实现了“串口接收数据并原样回传”,现在需要优化代码,实现“数据包解析 + 按格式回传”,核心逻辑如下:
-
定义数据包接收缓冲区(数组),用于存储蓝牙模块发送的数据包。
-
通过串口接收中断,逐字节接收数据,判断是否收到帧头(0xA5),如果收到,继续接收后续字节,直到收到帧尾(0x5A)。
-
接收完成后,判断数据包格式是否正确,如果正确,将该数据包原样回传蓝牙模块;如果不正确,丢弃该数据包,等待下一个数据包。
3. 核心代码片段
这里只放拆包,装包的代码
DataPacket_SPP.h
#ifndef DATAPACKET_SPP_H
#define DATAPACKET_SPP_H
#include "stm32f10x.h"
// -------------------------- 配置宏(stm32接收) --------------------------
// 各数据类型的数量(支持自定义)
#define BOOL_IN_COUNT 2 // bool数量(1字节最多存8个)
#define BYTE_IN_COUNT 1 // byte数量
#define SHORT_IN_COUNT 1 // short数量
#define INT_IN_COUNT 1 // int数量
#define FLOAT_IN_COUNT 1 // float数量
// 自动计算字段长度
#define BOOL_IN_FIELD_LEN ((BOOL_IN_COUNT + 7) / 8) // bool字段总字节数(向上取整)
#define BYTE_IN_FIELD_LEN (BYTE_IN_COUNT * 1) // byte字段总字节数
#define SHORT_IN_FIELD_LEN (SHORT_IN_COUNT * 2) // short字段总字节数
#define INT_IN_FIELD_LEN (INT_IN_COUNT * 4) // int字段总字节数
#define FLOAT_IN_FIELD_LEN (FLOAT_IN_COUNT * 4) // float字段总字节数
// 数据结构
typedef struct {
uint8_t bools[BOOL_IN_COUNT + 1]; // 解析后的bool值
uint8_t bytes[BYTE_IN_COUNT + 1]; // 解析后的byte值
int16_t shorts[SHORT_IN_COUNT + 1]; // 解析后的short值
int32_t ints[INT_IN_COUNT + 1]; // 解析后的int值
float floats[FLOAT_IN_COUNT + 1]; // 解析后的float值
} DataPacket_IN_TypeDef;
// -------------------------- 配置宏(stm32发送) --------------------------
// 包头包尾
#define PACKET_HEADER 0XA5
#define PACKET_TRAILER 0X5A
// 各数据类型的数量(支持自定义)
#define BOOL_OUT_COUNT 1 // bool数量(1字节最多存8个)
#define BYTE_OUT_COUNT 1 // byte数量
#define SHORT_OUT_COUNT 1 // short数量
#define INT_OUT_COUNT 1 // int数量
#define FLOAT_OUT_COUNT 1 // float数量
// 自动计算字段长度
#define BOOL_OUT_FIELD_LEN ((BOOL_OUT_COUNT + 7) / 8) // bool字段总字节数(向上取整)
#define BYTE_OUT_FIELD_LEN (BYTE_OUT_COUNT * 1) // byte字段总字节数
#define SHORT_OUT_FIELD_LEN (SHORT_OUT_COUNT * 2) // short字段总字节数
#define INT_OUT_FIELD_LEN (INT_OUT_COUNT * 4) // int字段总字节数
#define FLOAT_OUT_FIELD_LEN (FLOAT_OUT_COUNT * 4) // float字段总字节数
// 自动计算总长度
#define PACKET_OUT_TOTAL_FIELD_LEN (1 + BOOL_OUT_FIELD_LEN + BYTE_OUT_FIELD_LEN + SHORT_OUT_FIELD_LEN + INT_OUT_FIELD_LEN + FLOAT_OUT_FIELD_LEN + 2)
// 数据结构
typedef struct {
uint8_t bools[BOOL_OUT_COUNT + 1]; // 解析后的bool值
uint8_t bytes[BYTE_OUT_COUNT + 1]; // 解析后的byte值
int16_t shorts[SHORT_OUT_COUNT + 1]; // 解析后的short值
int32_t ints[INT_OUT_COUNT + 1]; // 解析后的int值
float floats[FLOAT_OUT_COUNT + 1]; // 解析后的float值
} DataPacket_OUT_TypeDef;
// -------------------------- 函数声明 --------------------------
DataPacket_IN_TypeDef DataPacket_Parse(uint8_t *buf);
void DataPacket_Pack(DataPacket_OUT_TypeDef packet, uint8_t* buf);
#endif
DataPacket_SPP.c
#include "DataPacket_SPP.h"
/**
* @brief 解析HEX数据包
* @retval
*/
DataPacket_IN_TypeDef DataPacket_Parse(uint8_t *buf)
{
DataPacket_IN_TypeDef packet;
// 1. 逐字段解析
uint16_t offset = 0;
// 解析bool(按位存储,bit0对应第一个bool)
if (BOOL_IN_COUNT)
{
uint8_t boolByte = buf[offset++];
for (uint8_t i = 0; i < BOOL_IN_COUNT; i++) {
packet.bools[i] = (boolByte >> i) & 0x01;
}
}
// 解析byte(直接读取字节)
for (uint8_t i = 0; i < BYTE_IN_COUNT; i++) {
packet.bytes[i] = buf[offset++];
}
// 解析short(大端转小端)
for (uint8_t i = 0; i < SHORT_IN_COUNT; i++) {
packet.shorts[i] = (int16_t)((buf[offset]) | buf[offset+1] << 8);
offset += 2;
}
// 解析int(大端转小端)
for (uint8_t i = 0; i < INT_IN_COUNT; i++) {
packet.ints[i] = (int32_t)((buf[offset]) |
(buf[offset+1] << 8) |
(buf[offset+2] << 16) |
buf[offset+3] << 24);
offset += 4;
}
// 解析float(大端转小端后强制转换)
for (uint8_t i = 0; i < FLOAT_IN_COUNT; i++) {
uint32_t floatRaw = (uint32_t)((buf[offset]) |
(buf[offset+1] << 8) |
(buf[offset+2] << 16) |
buf[offset+3] << 24);
packet.floats[i] = *(float *)&floatRaw;
offset += 4;
}
return packet;
}
/**
* @brief 将DataPacket_TypeDef结构体数据打包成协议格式的字节数组
* @param packet:输入的待打包数据结构体
* @param buf:输出的打包后字节数组(需提前分配至少PACKET_TOTAL_LEN长度的内存)
* @retval 成功返回true,失败返回false(buf为空/长度不足等)
*/
void DataPacket_Pack(DataPacket_OUT_TypeDef packet, uint8_t* buf)
{
uint16_t offset = 0;
// 写入包头(0xA5)
buf[offset++] = PACKET_HEADER;
// 打包bool类型(按位存储:bit0对应第一个bool,bit1对应第二个...1字节存8个)
if (BOOL_OUT_COUNT)
{
uint8_t boolByte = 0;
for (uint8_t i = 0; i < BOOL_OUT_COUNT; i++) {
// 只取bool值的0/1(防止传入非0/1值)
if (packet.bools[i] != 0) {
boolByte |= (1 << i);
}
}
buf[offset++] = boolByte;
}
// 打包byte类型(直接逐字节写入)
for (uint8_t i = 0; i < BYTE_OUT_COUNT; i++) {
buf[offset++] = packet.bytes[i];
}
// 打包short类型(大端序:高字节在前,低字节在后)
for (uint8_t i = 0; i < SHORT_OUT_COUNT; i++) {
buf[offset++] = packet.shorts[i] & 0xFF; // 高8位
buf[offset++] = (packet.shorts[i] >> 8) & 0xFF; // 低8位
}
// 打包int类型(大端序)
for (uint8_t i = 0; i < INT_OUT_COUNT; i++) {
buf[offset++] = packet.ints[i] & 0xFF; // 最高8位
buf[offset++] = (packet.ints[i] >> 8) & 0xFF; // 次高8位
buf[offset++] = (packet.ints[i] >> 16) & 0xFF; // 次低8位
buf[offset++] = (packet.ints[i] >> 24) & 0xFF; // 最低8位
}
// 打包float类型(先转成uint32_t,再按大端序写入)
for (uint8_t i = 0; i < FLOAT_OUT_COUNT; i++) {
// 强制类型转换:将float的二进制表示转为uint32_t
uint32_t floatRaw = *(uint32_t *)&(packet.floats[i]);
buf[offset++] = floatRaw & 0xFF; // 最低8位
buf[offset++] = (floatRaw >> 8) & 0xFF; // 次低8位
buf[offset++] = (floatRaw >> 16) & 0xFF; // 次高8位
buf[offset++] = (floatRaw >> 24) & 0xFF; // 最高8位
}
// 计算并写入校验和(所有字节之和的后8位:包头后 → 校验和前的所有字节)
uint8_t checksum = 0;
// 校验范围:buf[1] 到 buf[offset-1](包头后,校验和前)
for (uint16_t i = 1; i < offset; i++) {
checksum += buf[i];
}
// 取后8位
checksum = checksum & 0xFF;
buf[offset++] = checksum;
// 写入包尾(0x5A)
buf[offset++] = PACKET_TRAILER;
}
4. 实战测试步骤
蓝牙视频2

5. 常见问题排查
- 手机能连接蓝牙,但发送数据后,STM32不回传:
-
检查接线:TX/RX是否交叉连接,GND是否共地,蓝牙模块是否为3.3V供电。
-
检查波特率:STM32串口1的波特率是否与蓝牙模块一致(默认9600)。
-
检查数据包格式:是否为Hex格式,帧头、帧尾、校验位是否正确。
- 手机无法连接蓝牙模块:
-
检查蓝牙模块是否进入通信模式(LED快闪),是否已配置为从机(AT+ROLE0)。
-
检查配对密码是否正确(本文设置为1234),手机与模块距离是否过远。
- 接收的数据乱码:
-
波特率不匹配,重新配置蓝牙模块和STM32的波特率,确保一致。
-
供电不稳定,更换USB-TTL模块或电源,确保3.3V供电稳定。
六、尾声
下面先给大伙提供一手网盘链接:
链接:https://pan.quark.cn/s/9a1a0e262477
提取码:nFam
本文作为STM32F103C8T6蓝牙模块基础教学,从理论到实操,完整讲解了“蓝牙分类→AT指令→手机软件→STM32配置→实战通信”的全流程。
如果大家在实操过程中遇到问题,欢迎在评论区留言,我会逐一回复;如果需要完整的Keil工程文件(包含所有代码、工程配置),也可以留言说明,后续会整理分享。
喜欢本文的小伙伴,记得点赞、收藏、关注,后续持续更新STM32内容~~









