HC-05蓝牙模块与STM32F103串口通信:从硬件连接到协议设计的完整指南
2026/7/30 2:02:38 网站建设 项目流程

1. 项目缘起:为什么选择HC-05与STM32F103这对经典组合?

如果你正在做一个需要无线数据传输的嵌入式小项目,比如遥控小车、无线传感器数据采集,或者想给一个旧设备加上蓝牙控制功能,那么“HC-05蓝牙模块 + STM32F103单片机”这个组合,大概率会出现在你的备选方案里。这几乎是电子爱好者和嵌入式初学者入门无线通信的“必修课”。我当年做第一个蓝牙遥控项目时,也毫不犹豫地选择了它。原因很简单:成本极低、资料海量、上手飞快

HC-05是一个经典的蓝牙2.0+EDR模块,支持SPP(串行端口协议),这意味着你可以把它完全当成一个“无线的串口”来用。单片机通过串口发送数据,HC-05负责把数据通过蓝牙发出去;另一端的手机或电脑收到数据,也如同从串口读取一样简单。这种“透传”模式,极大地降低了无线通信的开发门槛,你几乎不需要理解复杂的蓝牙协议栈,只需掌握串口通信即可。

而STM32F103,更是被称为“单片机界的Hello World”。它性能足够(72MHz主频),外设丰富(尤其是USART串口),生态成熟(标准库、HAL库、寄存器开发资料遍地都是),价格还非常亲民。更重要的是,它的USART功能稳定可靠,与HC-05的串口通信可以说是“天作之合”。

所以,这个项目的核心价值,不在于实现多么高深的技术,而在于提供一个完整、可靠、可复现的“交钥匙”方案。我将结合自己多次调试的经验,不仅给你可以编译下载的程序,更会拆解每一个配置细节背后的“为什么”,并分享那些数据手册和教程里不会写的“坑点”。比如,为什么我的HC-05和单片机连上了却没数据?为什么发送的数据会丢包或乱码?STM32的串口配置里哪些参数是“高压线”绝对不能动?这些才是让你项目一次成功的关键。

2. 硬件连接与核心原理:不仅仅是接上TX和RX

拿到HC-05模块和STM32最小系统板,第一步当然是连线。但这步如果只照着“TX接RX,RX接TX,VCC接3.3V,GND接GND”来接,后面很可能遇到各种灵异问题。我们必须理解每个引脚的角色和连接时的注意事项。

2.1 HC-05模块引脚详解与电源考量

一个典型的HC-05模块会有6个引脚(有些精简版只有4个):

  • VCC:电源正极。这是第一个关键点:HC-05模块有3.3V和5V两种版本。虽然绝大多数现在都是3.3V逻辑电平,但务必确认你的模块型号。如果是5V版本,接STM32的3.3V可能会工作不稳定甚至无法启动。稳妥起见,用万用表量一下模块VCC和GND之间的电压需求,或者查阅模块背面芯片型号。我们通常按3.3V来接。
  • GND:电源地。与STM32共地,这是通信的基础。
  • TXD:模块的发送引脚。记住:模块的TXD,要接单片机的RX(接收引脚)。因为模块要“发送”数据给单片机。
  • RXD:模块的接收引脚。接单片机的TX(发送引脚)。
  • STATE:状态引脚。蓝牙连接成功后,该引脚会输出高电平(通常为3.3V),未连接时为低电平。这个引脚非常有用,你可以把它接到STM32的一个GPIO上,通过读取其电平来判断蓝牙是否已连接,从而在程序里做出相应提示(比如点亮一个LED)。
  • EN/KEY:使能或配置引脚。在正常通信模式下,此引脚可以悬空或接高电平。当需要进入AT指令配置模式时,需在模块上电前将此引脚拉高(接3.3V)。这是配置模块名称、密码、主从模式的关键。

对于电源,另一个经验是:务必保证电源容量充足。HC-05在发射数据时瞬时电流可能达到几十毫安。如果使用STM32开发板上的3.3V LDO供电,且板上还有其他外设,可能会引起电压波动,导致蓝牙模块复位或单片机工作异常。如果条件允许,建议为蓝牙模块单独供电,或者使用一个容量更大的稳压电源。我曾遇到因为电源问题,导致蓝牙一传输数据就系统重启的情况,排查了很久。

2.2 STM32F103串口外设选型与配置核心

STM32F103有多个USART(通用同步异步收发器)。我们通常选择USART1,因为它的引脚(PA9为TX, PA10为RX)常常被引接到调试串口上,方便我们同时用串口助手监控。但如果你要用USART1下载程序(通过串口ISP),或者它被其他功能占用了,USART2(PA2-TX, PA3-RX)或USART3(PB10-TX, PB11-RX)也是绝佳选择。

配置串口,有以下几个核心参数,它们必须与HC-05模块的默认设置严格匹配:

  • 波特率:HC-05最常见的默认波特率是960038400。也有少数是115200。如果你不确认,一个笨办法但有效的方法是:用USB转TTL工具连接HC-05,用串口助手从9600开始逐个波特率试,发送“AT”(后面跟回车换行),看哪个波特率能返回“OK”。在我们的程序里,这个波特率必须配置正确。
  • 数据位:8位。这是最常用的。
  • 停止位:1位。
  • 校验位:无。
  • 流控制:无。这就是所谓的“三线制”接法(TX、RX、GND)。硬件流控制(RTS/CTS)在我们这种低速、小数据量的透传场景中一般不需要,不接即可。

在STM32的固件库(无论是标准库还是HAL库)中,配置这些参数就是填充一个初始化结构体。例如在HAL库中,你需要配置huart.Init.BaudRatehuart.Init.WordLengthhuart.Init.StopBitshuart.Init.Parityhuart.Init.HwFlowCtl等字段。一个常见的疏忽是停止位和校验位的组合。数据位8位+无校验+停止位1位,这是最常规的组合。如果设置错误,接收到的数据会是乱码。

3. 软件架构与代码逐行解析

这里我以STM32CubeMX生成HAL库代码为基础,进行解析和补充。使用CubeMX可以快速完成引脚和时钟的图形化配置,避免低级错误。

3.1 串口初始化与中断配置

首先,在CubeMX中使能一个USART(比如USART2),模式选择为“Asynchronous”(异步)。波特率等参数按上述设置(例如9600-8-N-1)。关键步骤在于开启中断:

  • 在NVIC Settings标签页,勾选USART2的全局中断。这样,当串口收到每一个字节的数据时,都会触发中断服务函数。 为什么用中断而不用轮询?因为轮询方式(不断去查询串口状态寄存器)会大量占用CPU时间,使程序无法处理其他任务。而中断方式只在数据到达时才处理,效率高得多,也是嵌入式系统的标准做法。

生成代码后,在main.c中,你会看到huart2的初始化代码。我们需要在初始化后立即启动接收中断:

// 在main函数初始化部分之后,while(1)循环之前 HAL_UART_Receive_IT(&huart2, &rx_buffer, 1);

这行代码的意思是:使能USART2的接收中断,并且指定一个缓冲区rx_buffer(通常是一个全局的uint8_t类型变量)来存放接收到的一个字节。每当收到一个字节,数据存入rx_buffer,并触发中断。

3.2 中断服务函数与数据帧解析

中断触发后,程序会跳转到stm32f1xx_it.c文件中的USART2_IRQHandler()函数,它内部会调用HAL库的通用处理函数HAL_UART_IRQHandler。这个函数最终会调用一个回调函数:HAL_UART_RxCpltCallback我们需要重写这个回调函数

main.c或者单独的通信模块文件中,添加以下函数:

// 定义接收缓冲区和一个索引 uint8_t uart_rx_buffer[256]; // 缓冲区 uint16_t uart_rx_index = 0; // 当前存储位置 uint8_t aRxBuffer; // 用于中断接收的单个字节变量 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // 判断是哪个串口触发的中断 // 1. 将收到的一个字节存入缓冲区 uart_rx_buffer[uart_rx_index++] = aRxBuffer; // 2. 简单帧判断:例如,以换行符 '\n' 作为一帧数据的结束 if (aRxBuffer == '\n' || uart_rx_index >= sizeof(uart_rx_buffer) - 1) { // 帧结束,处理数据 uart_rx_buffer[uart_rx_index] = '\0'; // 添加字符串结束符,方便打印 process_received_data(uart_rx_buffer, uart_rx_index); // 你的数据处理函数 uart_rx_index = 0; // 重置索引,准备接收下一帧 } // 3. 重新启动接收中断,等待下一个字节 HAL_UART_Receive_IT(&huart2, &aRxBuffer, 1); } }

这里是第一个容易出错的点:必须在回调函数末尾重新调用HAL_UART_Receive_IT,否则串口只会接收第一个字节,之后就不再触发中断了。这是HAL库的设计机制。

数据处理函数process_received_data的设计:这里根据你的应用来定。例如,如果手机发送的是文本指令“LED_ON”,你可以用strcmp比较;如果发送的是传感器控制参数“PWM,1280”,你可以用sscanf解析。务必保证解析逻辑健壮,能处理错误格式的数据。

3.3 数据发送函数封装

发送数据相对简单。HAL库提供了阻塞式发送HAL_UART_Transmit和非阻塞式(中断/DMA)发送HAL_UART_Transmit_IT/HAL_UART_Transmit_DMA

对于透传应用,数据量不大,使用阻塞式发送最简单:

void bluetooth_send_string(const char *str) { HAL_UART_Transmit(&huart2, (uint8_t*)str, strlen(str), 1000); // 超时时间1000ms }

但要注意,HAL_UART_Transmit会一直等待直到所有字节发送完毕或超时,在这期间CPU被阻塞。如果发送较长的数据,可能会影响其他实时任务。对于更复杂的系统,可以考虑使用中断或DMA方式发送。

一个实用的技巧是添加一个简单的数据打包协议。比如,在要发送的数据前加一个帧头(如0xAA,0xBB),后面加一个校验和(如所有字节的累加和取低8位)。在接收端,只有帧头正确且校验和通过的数据才被处理。这能极大地提高通信的可靠性,避免因干扰导致的错误动作。即使对于蓝牙透传,底层链路可能可靠,但应用层加一个简单校验依然是好习惯。

4. HC-05的AT指令配置实战与避坑指南

刚买来的HC-05模块,其默认设置(如波特率、名称、配对码)可能不符合你的要求。这就需要使用AT指令进行配置。这是一个独立的步骤,需要在配置模式下进行。

4.1 进入配置模式的“标准流程”与“玄学问题”

  1. 接线:将HC-05的VCC、GND、TXD、RXD与USB转TTL工具的对应引脚连接。关键:将HC-05的EN/KEY引脚接高电平(3.3V)
  2. 上电先不要给HC-05模块通电。打开电脑的串口助手软件(如XCOM, Putty),设置好波特率(尝试9600和38400),数据格式8-N-1。
  3. 通电并发送AT:给HC-05模块通电。此时模块上的LED会进入慢闪模式(大约2秒一闪),表示进入AT命令模式。在串口助手中发送字符串“AT”(注意,有些模块需要后面跟回车换行,即“AT\r\n”),如果返回“OK”,恭喜你,进入成功。

避坑指南

  • 无返回“OK”:首先检查接线,尤其是EN/KEY是否确实接到了高电平。其次,尝试所有常见的波特率(9600, 38400, 115200, 57600)。有些模块的配置模式波特率固定为38400,与通信波特率不同。
  • 依然无反应:有些模块需要非常精确的时序。尝试这个“玄学”步骤:在串口助手已打开、发送框已输入“AT\r\n”的情况下,先点击“发送”按钮,然后瞬间给HC-05模块通电。多试几次。我遇到过好几个模块,只有用这种方法才能进入AT模式。
  • 返回ERROR:可能是发送的指令格式不对。确保是“AT”加上回车换行(即“AT\r\n”)。

4.2 常用AT指令集与配置策略

进入AT模式后,你就可以发送指令了。每条指令后都要跟回车换行(\r\n)。常用指令如下:

指令功能示例(发送)示例回复说明
AT测试连接ATOK基础测试
AT+NAME?查询模块名称AT+NAME?+NAME:HC-05查询当前蓝牙名称
AT+NAME=设置模块名称AT+NAME=MyCarOK设置蓝牙名为“MyCar”
AT+PSWD?查询配对码AT+PSWD?+PSWD:1234查询当前PIN码
AT+PSWD=设置配对码AT+PSWD=0000OK设置PIN码为“0000”
AT+UART?查询串口参数AT+UART?+UART:9600,0,0格式:波特率,停止位,校验位
AT+UART=设置串口参数AT+UART=9600,0,0OK设置为9600,1停止位,无校验
AT+ROLE?查询主从角色AT+ROLE?+ROLE:00-从机,1-主机,2-回环
AT+ROLE=设置主从角色AT+ROLE=0OK设置为从机模式

配置策略建议

  1. 先查询,后设置:先用AT+UART?等指令查看当前设置,做到心中有数。
  2. 设置通信参数AT+UART=9600,0,0。这个参数必须与STM32程序中的串口初始化参数完全一致
  3. 设置名称和密码AT+NAME=YourDeviceNameAT+PSWD=1234。设置一个容易辨识的名称和简单的密码。
  4. 确认角色:我们通常将连接手机或电脑的模块设为从机(ROLE=0)。如果你需要两个HC-05模块互相通信,则需要配置一个为主机(ROLE=1),一个为从机。
  5. 重启生效:修改参数后,有些指令需要重启。最彻底的方法是断开电源,将EN/KEY引脚断开(或接低电平),重新上电。此时LED快闪(约1秒2次),表示进入可配对状态。

注意:AT指令必须在配置模式下进行。正常通信模式下发送AT指令是无效的。配置完成后,务必断开EN/KEY的高电平连接,让模块恢复正常通信模式。

5. 手机端连接与数据收发测试

硬件和单片机端准备好了,还需要一个“终端”来测试。手机是最方便的选择。

5.1 安卓手机APP选择与使用

在安卓应用商店搜索“蓝牙串口”,会有很多选择,例如“蓝牙串口”、“Serial Bluetooth Terminal”等。它们的功能大同小异:搜索蓝牙设备、配对连接、提供一个发送文本框和一个接收显示区域。

连接步骤

  1. 打开手机蓝牙设置,搜索设备。你应该能看到你刚才设置的设备名(如“MyCar”)。
  2. 点击配对,输入你设置的PIN码(如“1234”)。
  3. 打开蓝牙串口APP,在APP内选择已配对的设备“MyCar”进行连接。
  4. 连接成功后,HC-05模块上的LED会由快闪变为常亮(或慢速双闪,因版本而异)。

测试通信

  • 手机发,单片机收:在APP的发送框输入“LED_ON”并发送。在STM32程序中,你需要在process_received_data函数里解析这个字符串,并控制一个GPIO引脚输出高电平,点亮一个LED。同时,可以让单片机通过蓝牙回发一个“LED is ON”的确认信息。
  • 单片机发,手机收:你可以在STM32程序中定时(比如用HAL_Delay或定时器)通过bluetooth_send_string发送一段数据,如“Temperature:25.6C”。在手机APP的接收区应该能看到这些信息。

5.2 通信调试中的常见问题与排查链路

当通信不成功时,不要慌,按照以下链路层层排查,可以解决99%的问题:

  1. 电源与硬件连接排查

    • 用万用表测量HC-05的VCC和GND之间电压是否为稳定的3.3V?
    • TXD/RXD交叉连接是否正确?(模块TXD接单片机RXD)
    • 接线是否牢固?尝试按压接口或重新焊接。
  2. 蓝牙配对与连接状态排查

    • HC-05模块的LED指示灯状态是什么?快闪(等待配对)-> 常亮/慢双闪(已连接)是正常流程。如果一直快闪,检查手机是否已配对并连接。
    • 尝试用手机系统蓝牙设置直接连接,看是否能成功。排除APP本身的问题。
  3. 单片机串口基础功能排查

    • 这是最关键的一步:暂时抛开蓝牙模块,用USB转TTL工具直接连接STM32的USART2_TX(PA2)引脚到电脑。在单片机程序中,让串口定时发送一句“Hello UART”。用串口助手看能否收到。如果收不到,说明单片机串口初始化或发送代码有问题。如果收到了,证明单片机端是好的。
  4. 波特率与数据格式排查

    • 确认STM32程序中的波特率、数据位、停止位、校验位与HC-05模块的当前设置(用AT指令查询的)完全一致。一个9600一个115200是绝对不通的。
    • 在串口助手中,尝试不同的“结束符”设置。单片机发送时,是否在字符串末尾加了\r\n?手机APP发送时,是否勾选了“发送新行”?
  5. 软件逻辑与缓冲区排查

    • 单片机的中断回调函数里,是否重新启动了接收中断(HAL_UART_Receive_IT)?
    • 接收缓冲区是否溢出?定义的数组大小是否足够?
    • 数据处理函数process_received_data是否有死循环或阻塞操作?这会导致程序无法及时响应后续的中断。

我个人的经验是,准备一个“调试引脚”非常有用。在程序的关键位置(如进入串口中断、收到特定指令)控制一个LED闪烁或者翻转一个GPIO,再用示波器或逻辑分析仪查看,可以直观地了解程序的运行状态,比盲目猜测高效得多。

6. 项目进阶:从透传到简单协议设计

基本的透传实现后,我们可以让这个项目变得更“工程化”和“可靠”。

6.1 设计一个简单的应用层协议

原始字符串指令如“LED_ON”简单,但功能有限,容易出错。我们可以设计一个简单的二进制协议帧。 例如,定义一个帧结构:[帧头0xAA] [帧头0x55] [命令字] [数据长度N] [数据...] [校验和]

  • 帧头:两个固定字节,用于在数据流中识别一帧的开始。
  • 命令字:1个字节,表示指令类型(如0x01控制LED,0x02读取温度)。
  • 数据长度:1个字节,表示后面跟随的有效数据字节数。
  • 数据:N个字节的有效载荷。
  • 校验和:1个字节,可以是前面所有字节的累加和取低8位,或者异或和。

在STM32的中断接收回调中,我们需要实现一个状态机来解析这个协议:

  1. 状态0-寻找帧头1:判断收到的是否为0xAA,是则进入状态1。
  2. 状态1-寻找帧头2:判断下一个字节是否为0x55,是则进入状态2。
  3. 状态2-接收命令字:读取命令字。
  4. 状态3-接收数据长度:读取长度N。
  5. 状态4-接收N个数据字节:将后续N个字节存入数据缓冲区。
  6. 状态5-接收校验和:读取校验和字节。
  7. 验证与处理:计算前序所有字节的校验和,与收到的校验和比对。一致则调用对应的命令处理函数;不一致则丢弃该帧,状态机复位。

这种协议虽然比直接解析字符串复杂,但抗干扰能力强,可以传输任意二进制数据(比如浮点数、整数),扩展性非常好。

6.2 使用DMA释放CPU资源

当需要高速、连续地传输数据(比如传输摄像头图像、音频采样数据)时,频繁的串口中断会消耗大量CPU资源。此时,可以使用STM32的DMA(直接存储器访问)功能。

配置思路

  1. 在CubeMX中,为USART的RX和TX分别配置一条DMA通道,模式设为循环模式(Circular)或正常模式(Normal)。
  2. 对于接收,可以开启DMA循环模式,并指定一个较大的缓冲区(如1024字节)。DMA会在后台自动将串口接收到的数据搬运到这个缓冲区,完全不需要CPU干预。
  3. 程序只需要定期(比如用定时器)去检查DMA的写指针,就能知道收到了多少新数据,然后一次性取出处理。这避免了每个字节都进中断的开销。
  4. 对于发送,可以将要发送的数据块地址和长度配置给DMA,启动后DMA会自动发送,发送完成会产生中断通知CPU。

HAL库提供了HAL_UART_Receive_DMAHAL_UART_Transmit_DMA函数。使用DMA是提升串口吞吐性能的终极手段,但对于简单的遥控指令传输,中断方式已经绰绰有余。

6.3 低功耗与稳定性考量

如果你的设备是电池供电,功耗就变得重要。

  • HC-05的功耗:HC-05在工作时电流不小。如果设备需要待机,可以考虑在不需要通信时,通过一个MOS管控制其电源,彻底断电。或者研究HC-05的AT指令是否支持进入低功耗睡眠模式(不同固件版本支持度不同)。
  • STM32的低功耗模式:在等待指令时,STM32可以进入睡眠(Sleep)或停止(Stop)模式,通过串口中断或外部中断(连接HC-05的STATE引脚)来唤醒。这需要仔细配置中断和时钟,是另一个有趣的优化方向。

稳定性方面,除了前面提到的电源和协议校验,还可以在软件上增加“看门狗”(IWDG)。防止程序跑飞导致设备“变砖”。STM32的独立看门狗使用起来非常简单,在CubeMX中使能并设置超时时间,然后在主循环中定期“喂狗”即可。

7. 移植与适配:GD32、其他STM32型号与常见问题

你可能会问,我的板子是GD32F103,或者我用的STM32F103C8T6(小蓝板),程序能直接用吗?代码兼容性如何?

7.1 GD32F103与STM32F103的异同

GD32是国内厂商推出的与STM32F103高度兼容的芯片。就这个蓝牙透传项目而言,HAL库代码在大多数情况下是可以直接移植的。因为HAL库的API是统一的。你需要做的是:

  1. 在STM32CubeMX中,将芯片型号换为对应的GD32型号(如果CubeMX支持)。如果不支持,就选择引脚和资源最接近的STM32型号。
  2. 关键区别在于时钟系统。GD32的主频通常更高(如108MHz),且内部RC振荡器精度可能不同。如果你使用了精确的串口波特率(如115200),直接使用STM32的时钟配置可能会产生误差,导致通信乱码。建议在GD32上使用外部晶振,并在CubeMX中根据实际的主频重新计算和设置波特率。
  3. 编译时,需要将MDK或IDE中的设备库(Device)改为GD32的库文件。

简单说,对于USART、GPIO、DMA这些外设的基本操作,代码是通用的。需要关注的是时钟、中断向量表、启动文件这些底层差异。第一次移植时,建议先跑一个最简单的LED闪烁和串口打印程序,确认基础系统工作正常,再添加蓝牙功能。

7.2 资源受限型MCU(如STM32F103C8T6)的注意事项

经典的“小蓝板”C8T6,只有64KB Flash和20KB RAM。我们的程序需要精简:

  • 避免使用printf等大型库函数printf会占用大量Flash空间。直接使用HAL_UART_Transmit发送原始字符串或字节数组。
  • 合理规划缓冲区:不要定义过大的全局数组。接收缓冲区256字节可能就够了,如果协议简单,甚至可以缩减到64字节。
  • 优化process_received_data函数:避免在中断回调或数据处理函数中使用HAL_Delay、复杂的字符串操作(如sprintf)。
  • 注意代码优化等级:在Keil或IDE的编译选项中,可以选择“Optimize for size”(-Os),能有效减小代码体积。

只要注意这些点,在C8T6上实现蓝牙透传是完全没有问题的,Flash和RAM都足够用。

7.3 没有PWM波形输出?一个可能的相关排查点

在相关热词里看到了“stm32f103的pa8做pwm,没有pwm波形输出”。虽然这和蓝牙透传不直接相关,但作为STM32的常见问题,我可以提供一个排查思路,这体现了嵌入式开发中“模块化测试”的重要性。

如果你的项目里同时用到了蓝牙和PWM(比如用蓝牙控制电机速度),而PWM没有输出,请按以下步骤隔离测试:

  1. 注释掉所有蓝牙相关的初始化代码和中断代码,只保留最基础的时钟、GPIO和TIM(定时器)初始化代码,在main函数里直接写死一个占空比。用示波器或LED(如果频率低)看PA8是否有输出。如果没有,问题在PWM配置本身(时钟是否使能、GPIO是否复用正确、定时器配置、CCR寄存器值等)。
  2. 如果PWM单独工作正常,逐步添加蓝牙初始化代码。添加后PWM失效,可能是中断冲突。检查NVIC(嵌套向量中断控制器)中,USART中断和TIM中断的优先级。如果蓝牙串口中断优先级太高,且中断服务函数执行时间过长,可能会阻塞其他中断(包括PWM更新中断),导致外设行为异常。尝试调整中断优先级。
  3. 检查电源和地线。蓝牙模块工作时可能引起电源噪声,影响单片机内部模拟电路(虽然PWM是数字信号,但电源不稳会影响整个芯片)。确保电源退耦电容(0.1uF和10uF)紧靠芯片电源引脚放置。

嵌入式开发就是这样,各个功能模块看似独立,但在中断、电源、CPU时间这些共享资源上会相互影响。养成“分模块调试、逐步集成”的习惯,能帮你快速定位这类耦合性问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询