HC-05蓝牙模块与STM32F103串口通信:从硬件连接到协议设计的完整指南

HC-05蓝牙模块与STM32F103串口通信:从硬件连接到协议设计的完整指南 1. 项目缘起为什么选择HC-05与STM32F103这对经典组合如果你正在做一个需要无线数据传输的嵌入式小项目比如遥控小车、无线传感器数据采集或者想给一个旧设备加上蓝牙控制功能那么“HC-05蓝牙模块 STM32F103单片机”这个组合大概率会出现在你的备选方案里。这几乎是电子爱好者和嵌入式初学者入门无线通信的“必修课”。我当年做第一个蓝牙遥控项目时也毫不犹豫地选择了它。原因很简单成本极低、资料海量、上手飞快。HC-05是一个经典的蓝牙2.0EDR模块支持SPP串行端口协议这意味着你可以把它完全当成一个“无线的串口”来用。单片机通过串口发送数据HC-05负责把数据通过蓝牙发出去另一端的手机或电脑收到数据也如同从串口读取一样简单。这种“透传”模式极大地降低了无线通信的开发门槛你几乎不需要理解复杂的蓝牙协议栈只需掌握串口通信即可。而STM32F103更是被称为“单片机界的Hello World”。它性能足够72MHz主频外设丰富尤其是USART串口生态成熟标准库、HAL库、寄存器开发资料遍地都是价格还非常亲民。更重要的是它的USART功能稳定可靠与HC-05的串口通信可以说是“天作之合”。所以这个项目的核心价值不在于实现多么高深的技术而在于提供一个完整、可靠、可复现的“交钥匙”方案。我将结合自己多次调试的经验不仅给你可以编译下载的程序更会拆解每一个配置细节背后的“为什么”并分享那些数据手册和教程里不会写的“坑点”。比如为什么我的HC-05和单片机连上了却没数据为什么发送的数据会丢包或乱码STM32的串口配置里哪些参数是“高压线”绝对不能动这些才是让你项目一次成功的关键。2. 硬件连接与核心原理不仅仅是接上TX和RX拿到HC-05模块和STM32最小系统板第一步当然是连线。但这步如果只照着“TX接RXRX接TXVCC接3.3VGND接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或者它被其他功能占用了USART2PA2-TX, PA3-RX或USART3PB10-TX, PB11-RX也是绝佳选择。配置串口有以下几个核心参数它们必须与HC-05模块的默认设置严格匹配波特率HC-05最常见的默认波特率是9600和38400。也有少数是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方式发送。一个实用的技巧是添加一个简单的数据打包协议。比如在要发送的数据前加一个帧头如0xAA0xBB后面加一个校验和如所有字节的累加和取低8位。在接收端只有帧头正确且校验和通过的数据才被处理。这能极大地提高通信的可靠性避免因干扰导致的错误动作。即使对于蓝牙透传底层链路可能可靠但应用层加一个简单校验依然是好习惯。4. HC-05的AT指令配置实战与避坑指南刚买来的HC-05模块其默认设置如波特率、名称、配对码可能不符合你的要求。这就需要使用AT指令进行配置。这是一个独立的步骤需要在配置模式下进行。4.1 进入配置模式的“标准流程”与“玄学问题”接线将HC-05的VCC、GND、TXD、RXD与USB转TTL工具的对应引脚连接。关键将HC-05的EN/KEY引脚接高电平3.3V。上电先不要给HC-05模块通电。打开电脑的串口助手软件如XCOM Putty设置好波特率尝试9600和38400数据格式8-N-1。通电并发送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基础测试ATNAME?查询模块名称ATNAME?NAME:HC-05查询当前蓝牙名称ATNAME设置模块名称ATNAMEMyCarOK设置蓝牙名为“MyCar”ATPSWD?查询配对码ATPSWD?PSWD:1234查询当前PIN码ATPSWD设置配对码ATPSWD0000OK设置PIN码为“0000”ATUART?查询串口参数ATUART?UART:9600,0,0格式波特率停止位校验位ATUART设置串口参数ATUART9600,0,0OK设置为96001停止位无校验ATROLE?查询主从角色ATROLE?ROLE:00-从机1-主机2-回环ATROLE设置主从角色ATROLE0OK设置为从机模式配置策略建议先查询后设置先用ATUART?等指令查看当前设置做到心中有数。设置通信参数ATUART9600,0,0。这个参数必须与STM32程序中的串口初始化参数完全一致。设置名称和密码ATNAMEYourDeviceName和ATPSWD1234。设置一个容易辨识的名称和简单的密码。确认角色我们通常将连接手机或电脑的模块设为从机ROLE0。如果你需要两个HC-05模块互相通信则需要配置一个为主机ROLE1一个为从机。重启生效修改参数后有些指令需要重启。最彻底的方法是断开电源将EN/KEY引脚断开或接低电平重新上电。此时LED快闪约1秒2次表示进入可配对状态。注意AT指令必须在配置模式下进行。正常通信模式下发送AT指令是无效的。配置完成后务必断开EN/KEY的高电平连接让模块恢复正常通信模式。5. 手机端连接与数据收发测试硬件和单片机端准备好了还需要一个“终端”来测试。手机是最方便的选择。5.1 安卓手机APP选择与使用在安卓应用商店搜索“蓝牙串口”会有很多选择例如“蓝牙串口”、“Serial Bluetooth Terminal”等。它们的功能大同小异搜索蓝牙设备、配对连接、提供一个发送文本框和一个接收显示区域。连接步骤打开手机蓝牙设置搜索设备。你应该能看到你刚才设置的设备名如“MyCar”。点击配对输入你设置的PIN码如“1234”。打开蓝牙串口APP在APP内选择已配对的设备“MyCar”进行连接。连接成功后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%的问题电源与硬件连接排查用万用表测量HC-05的VCC和GND之间电压是否为稳定的3.3VTXD/RXD交叉连接是否正确模块TXD接单片机RXD接线是否牢固尝试按压接口或重新焊接。蓝牙配对与连接状态排查HC-05模块的LED指示灯状态是什么快闪等待配对- 常亮/慢双闪已连接是正常流程。如果一直快闪检查手机是否已配对并连接。尝试用手机系统蓝牙设置直接连接看是否能成功。排除APP本身的问题。单片机串口基础功能排查这是最关键的一步暂时抛开蓝牙模块用USB转TTL工具直接连接STM32的USART2_TXPA2引脚到电脑。在单片机程序中让串口定时发送一句“Hello UART”。用串口助手看能否收到。如果收不到说明单片机串口初始化或发送代码有问题。如果收到了证明单片机端是好的。波特率与数据格式排查确认STM32程序中的波特率、数据位、停止位、校验位与HC-05模块的当前设置用AT指令查询的完全一致。一个9600一个115200是绝对不通的。在串口助手中尝试不同的“结束符”设置。单片机发送时是否在字符串末尾加了\r\n手机APP发送时是否勾选了“发送新行”软件逻辑与缓冲区排查单片机的中断回调函数里是否重新启动了接收中断HAL_UART_Receive_IT接收缓冲区是否溢出定义的数组大小是否足够数据处理函数process_received_data是否有死循环或阻塞操作这会导致程序无法及时响应后续的中断。我个人的经验是准备一个“调试引脚”非常有用。在程序的关键位置如进入串口中断、收到特定指令控制一个LED闪烁或者翻转一个GPIO再用示波器或逻辑分析仪查看可以直观地了解程序的运行状态比盲目猜测高效得多。6. 项目进阶从透传到简单协议设计基本的透传实现后我们可以让这个项目变得更“工程化”和“可靠”。6.1 设计一个简单的应用层协议原始字符串指令如“LED_ON”简单但功能有限容易出错。我们可以设计一个简单的二进制协议帧。 例如定义一个帧结构[帧头0xAA] [帧头0x55] [命令字] [数据长度N] [数据...] [校验和]帧头两个固定字节用于在数据流中识别一帧的开始。命令字1个字节表示指令类型如0x01控制LED0x02读取温度。数据长度1个字节表示后面跟随的有效数据字节数。数据N个字节的有效载荷。校验和1个字节可以是前面所有字节的累加和取低8位或者异或和。在STM32的中断接收回调中我们需要实现一个状态机来解析这个协议状态0-寻找帧头1判断收到的是否为0xAA是则进入状态1。状态1-寻找帧头2判断下一个字节是否为0x55是则进入状态2。状态2-接收命令字读取命令字。状态3-接收数据长度读取长度N。状态4-接收N个数据字节将后续N个字节存入数据缓冲区。状态5-接收校验和读取校验和字节。验证与处理计算前序所有字节的校验和与收到的校验和比对。一致则调用对应的命令处理函数不一致则丢弃该帧状态机复位。这种协议虽然比直接解析字符串复杂但抗干扰能力强可以传输任意二进制数据比如浮点数、整数扩展性非常好。6.2 使用DMA释放CPU资源当需要高速、连续地传输数据比如传输摄像头图像、音频采样数据时频繁的串口中断会消耗大量CPU资源。此时可以使用STM32的DMA直接存储器访问功能。配置思路在CubeMX中为USART的RX和TX分别配置一条DMA通道模式设为循环模式Circular或正常模式Normal。对于接收可以开启DMA循环模式并指定一个较大的缓冲区如1024字节。DMA会在后台自动将串口接收到的数据搬运到这个缓冲区完全不需要CPU干预。程序只需要定期比如用定时器去检查DMA的写指针就能知道收到了多少新数据然后一次性取出处理。这避免了每个字节都进中断的开销。对于发送可以将要发送的数据块地址和长度配置给DMA启动后DMA会自动发送发送完成会产生中断通知CPU。HAL库提供了HAL_UART_Receive_DMA和HAL_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是统一的。你需要做的是在STM32CubeMX中将芯片型号换为对应的GD32型号如果CubeMX支持。如果不支持就选择引脚和资源最接近的STM32型号。关键区别在于时钟系统。GD32的主频通常更高如108MHz且内部RC振荡器精度可能不同。如果你使用了精确的串口波特率如115200直接使用STM32的时钟配置可能会产生误差导致通信乱码。建议在GD32上使用外部晶振并在CubeMX中根据实际的主频重新计算和设置波特率。编译时需要将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没有输出请按以下步骤隔离测试注释掉所有蓝牙相关的初始化代码和中断代码只保留最基础的时钟、GPIO和TIM定时器初始化代码在main函数里直接写死一个占空比。用示波器或LED如果频率低看PA8是否有输出。如果没有问题在PWM配置本身时钟是否使能、GPIO是否复用正确、定时器配置、CCR寄存器值等。如果PWM单独工作正常逐步添加蓝牙初始化代码。添加后PWM失效可能是中断冲突。检查NVIC嵌套向量中断控制器中USART中断和TIM中断的优先级。如果蓝牙串口中断优先级太高且中断服务函数执行时间过长可能会阻塞其他中断包括PWM更新中断导致外设行为异常。尝试调整中断优先级。检查电源和地线。蓝牙模块工作时可能引起电源噪声影响单片机内部模拟电路虽然PWM是数字信号但电源不稳会影响整个芯片。确保电源退耦电容0.1uF和10uF紧靠芯片电源引脚放置。嵌入式开发就是这样各个功能模块看似独立但在中断、电源、CPU时间这些共享资源上会相互影响。养成“分模块调试、逐步集成”的习惯能帮你快速定位这类耦合性问题。