欢迎光临
我们一直在努力

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 <stdio.h>
    #include <stdarg.h>

    /************************* 全局变量定义 *************************/
    #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 字符串首地址(以'\\0'结尾)
    * @retval 无
    * @note 逐字节发送字符串,直到遇到结束符'\\0'
    */

    void Serial_SPP_SendString(char *String)
    {
    uint8_t i = 0;
    while (String[i] != '\\0')
    {
    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 文本模式:包头@ → 数据 → 包尾\\r\\n;
    * HEX模式:包头0xA5 → 数据 → 包尾0x5A;
    * 接收完成后置Serial_SPP_RxFlag=1,缓冲区自动添加结束符(文本加'\\0',HEX无)
    */

    void USART1_IRQHandler(void)
    {
    // 状态机状态(文本:0=等@,1=收数据,2=等\\n;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:接收数据,等待\\r
    if (RxData == '\\r')
    {
    RxState = 2; // 切换到等待\\n状态
    }
    else
    {
    // 数据存入缓冲区(防止越界)
    if (pRxPacket < SERIAL_SPP_RX_BUF_LEN – 1)
    {
    Serial_SPP_RxPacket[pRxPacket] = RxData;
    pRxPacket++;
    }
    }
    break;

    case 2: // 状态2:等待\\n,完成数据包接收
    if (RxData == '\\n')
    {
    RxState = 0; // 重置状态机
    Serial_SPP_RxPacket[pRxPacket] = '\\0'; // 添加字符串结束符
    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 // 启用文本数据包(@开头,\\r\\n结尾)
    #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\\r\\n)
    #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_HEADER0XA5
    #define PACKET_TRAILER0X5A
    // 各数据类型的数量(支持自定义)
    #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内容~~

    感谢大伙观看,别忘了三连支持一下

    大伙也可以关注一下我的其它专栏,同样精彩喔~

    大伙也可以添加下面的微信来交流

    赞(0)
    未经允许不得转载:171主机测评 » stm32教程:蓝牙模块通信
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址