前言

本篇基于 STM32F103C8T6 最小系统板,完成DHT11 温湿度采集、OLED 屏幕本地显示、HC‑05 蓝牙双向串口通信综合实验。单片机定时读取环境温湿度,一方面在 OLED 屏幕实时展示,另一方面通过 HC‑05 蓝牙模块把数据上传到手机;手机蓝牙串口 APP 发送指令123,单片机会识别命令并回复指定字符串,同时 OLED 提示收到蓝牙指令。

视频讲解:STM32驱动HC-05蓝牙+DHT11温湿度采集+OLED显示完整实验https://www.bilibili.com/video/BV1pjtw67ExE/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

HC‑05 可以简单理解成一条无线串口线:单片机串口发出去的数据,无线传到手机;手机发送的数据,又通过串口给到 STM32 单片机。

一、实验演示目标

整体业务流程:

  1. DHT11 传感器采集环境温度、湿度;
  2. STM32 读取 DHT11 数据,一份数据给到 OLED 屏幕做本地显示;
  3. 另一份数据通过 USART1 串口交给 HC‑05,无线发送至手机蓝牙串口 APP;
  4. 手机 APP 发送文本指令123,经过 HC‑05 转为串口电平给到 STM32;
  5. 单片机串口接收中断捕获字符,识别完整命令后,蓝牙回复I get it!Sir!,OLED 同步打印接收状态。

二、硬件清单与接线说明

硬件清单

  • STM32F103C8T6 最小系统板
  • HC‑05 蓝牙串口模块
  • DHT11 温湿度传感器模块
  • 0.96 寸 I2C OLED 显示屏
  • 杜邦线若干

引脚接线对照表

模块引脚 STM32F103C8T6 引脚 功能说明
HC‑05 VCC 5V 蓝牙模块供电
HC‑05 TXD PA10 HC‑05 发送,STM32 USART1 接收
HC‑05 RXD PA9 HC‑05 接收,STM32 USART1 发送
HC‑05 GND GND 共地
DHT11 VCC 5V 温湿度传感器供电
DHT11 DATA PA11 DHT11 单总线数据引脚
DHT11 GND GND 共地
OLED SCL PB8 软件 I2C 时钟
OLED SDA PB9 软件 I2C 数据

接线重点注意事项

  1. TX、RX 必须交叉相连 TX 是发送引脚,RX 是接收引脚。设备 A 的发送必须接设备 B 的接收。
  • STM32 PA9(TX) → HC‑05 RXD
  • STM32 PA10 (RX) ← HC‑05 TXD 如果接成 TX 接 TX、RX 接 RX,双方完全无法通信。
  1. 所有模块必须共地 串口识别高低电平依赖 GND 作为参考电位。哪怕供电、TX/RX 接线完全正确,如果不共地,会出现乱码、收不到数据的现象。

三、HC‑05 模块工作原理

HC‑05 是经典的主从一体蓝牙串口模块,工作在透明传输模式。 简单来说模块本身不会解析温湿度、不会识别我们自定义的123指令,它只做信号转换:

  1. STM32 串口输出的字节电平信号,HC‑05 转为经典蓝牙无线信号发给手机;
  2. 手机蓝牙发出的数据,HC‑05 转换成 UART 串口电平输出给单片机;
  3. 数据代表什么含义、什么指令,全部由单片机程序和手机 APP 双方约定。

注意:本实验的123应用层自定义指令,不是 HC‑05 模块的 AT 配置命令。HC‑05 的 AT 模式只用来修改模块名字、配对密码、波特率等硬件参数,本实验不进入 AT 模式。

四、USART1 串口通信配置

本实验使用 STM32 的 USART1 外设,串口参数配置为 9600 8N1

参数 配置值
外设 USART1
发送引脚 PA9
接收引脚 PA10
波特率 9600
数据位 8 位
校验位
停止位 1 位
硬件流控 关闭

UART 属于异步串口通信,没有额外时钟线,通信双方必须约定一模一样的波特率。 一帧完整字节包含:1 位起始位 + 8 位数据位 +1 位停止位,合计 10bit。波特率 9600 情况下,传输 1 字节大约 1.04ms。

如果 STM32、HC‑05、手机 APP 三者波特率不统一,直接出现乱码或者完全收不到数据。

五、业务逻辑讲解

5.1 温湿度上报流程

  1. 主程序每 2 秒钟读取一次 DHT11 传感器;
  2. 读取校验成功:OLED 打印温度湿度,程序拼接字符串,通过 USART1 发送给 HC‑05 上传手机;
  3. 读取失败:OLED 打印Sensor Error,蓝牙向手机上报DHT11 read error

手机收到正常数据格式示例:

Temp:25.0C,Humi:60.0%
  • Temp:温度标识;Humi:湿度标识;\r\n为换行符,让手机串口助手自动换行。

上报周期设置为 2 秒,降低手机端解析刷新压力;两行报文之间增加 50ms 延时间隔,减少字节流分包问题。

5.2 手机指令接收与应答逻辑

手机 APP 发送连续字符123

  1. HC‑05 把蓝牙数据转为串口电平送入 STM32 PA10;
  2. 每收到 1 个字节触发 USART1 接收中断;
  3. 中断服务函数把字节存入环形接收缓冲区,同时匹配目标命令;
  4. 程序检测完整匹配123,置位命令接收标志位;
  5. 主循环里面执行应答发送,回复字符串I get it!Sir!,OLED 屏幕更新提示BT RX: 123 OK

关键点:数据接收放在中断,回复发送放在主循环,不要在中断函数里面做耗时的串口发送操作。哪怕手机发送123附带回车换行,程序依旧可以识别命令。

5.3 接收中断 + 环形缓冲区作用

如果不使用中断,主循环就要一直等待蓝牙数据,DHT11 采集、OLED 刷新都会卡住。

  • 没有蓝牙数据的时候,CPU 正常跑主程序,采集传感器、刷新屏幕;
  • 手机发来一个字节,立刻触发中断,快速保存数据之后回到原来程序继续运行。

环形缓冲区大小设置 128 字节,拥有读指针、写指针,写满之后丢弃最早数据,优先保存最新收到字节。中断里面只做最简单的保存、状态标记,复杂业务全部交给主循环处理。

六、实物演示

实验现象

  1. 上电之后 OLED 屏幕显示标题、实时温度湿度,底部提示BT TX: every 2s,代表每 2 秒向手机上报温湿度;
  2. 手机蓝牙搜索 HC‑05,配对连接,打开蓝牙串口助手;手机端每隔 2 秒接收一行温湿度字符串;
  3. APP 文本模式发送123
  4. 手机收到回复I get it!Sir!,同时 OLED 底部刷新显示BT RX: 123 OK
  5. DHT11 接触不良或者接线错误时,OLED 显示Sensor Error,手机收到传感器读取错误提示。

演示操作步骤

  1. 按照接线表完成全部接线,重点确认全部模块 GND 共地;
  2. 编译下载工程到 STM32 开发板,上电;观察 OLED 屏幕输出;
  3. 手机打开蓝牙,搜索配对 HC‑05 模块,打开蓝牙串口 APP 建立连接;
  4. 观察手机周期性收到温湿度数据;
  5. APP 文本模式发送123,观察手机回复和 OLED 状态变化;
  6. 可选拓展:修改代码里面预设命令字符串,实现自定义指令控制。

七、源代码

这里包括 HC‑05 驱动以及 main.c 主函数。 

核心宏定义参考:

#define HC05_DEFAULT_BAUDRATE       9600u
#define HC05_TX_LINE_GAP_MS         50u
#define HC05_RX_BUFFER_SIZE         128u
#define HC05_APP_COMMAND            "123"

关键函数说明:

函数 功能
HC05_Init USART1 初始化,波特率 9600
HC05_SendByte 发送单个字节
HC05_SendString 发送字符串
HC05_SendLine 发送字符串附带回车换行
HC05_SendTemperatureHumidity 打包发送温湿度报文
USART1_IRQHandler 串口 1 接收中断服务函数

main.c代码

// Teaching demo: DHT11 + OLED + HC-05 on STM32F103C8T6.
// HC-05: TX->PA10, RX->PA9; DHT11 DATA->PA11; OLED SCL->PB8, SDA->PB9.
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "dht11.h"
#include "HC_05.h"

/* 每 2 秒采集并发送一次,避免手机 APP 接收过于频繁。 */
#define SENSOR_REFRESH_MS 2000

static void Show_DHT11_Data(u8 humiH,u8 humiL,u8 tempH,u8 tempL,u8 commandReceived);
static void Show_DHT11_Error(void);

int main(void)
{
	u8 humiH=0;
	u8 humiL=0;
	u8 tempH=0;
	u8 tempL=0;
	u8 commandReceived=0;

	delay_init();
	delay_ms(1000);
	NVIC_Configuration();
	HC05_Init();
	OLED_Init();
	OLED_ColorTurn(0);
	OLED_DisplayTurn(0);
	DHT11_Init();
	HC05_SendLine("STM32 DHT11 + HC-05 ready");
	HC05_SendLine("Send 123 to test Bluetooth RX");

	while(1)
	{
		commandReceived=HC05_TakeAppCommandReceived();
		if(commandReceived!=0)
		{
			HC05_SendLine("I get it!Sir!");
		}

		if(DHT11_Read_Data(&humiH,&humiL,&tempH,&tempL)==0)
		{
			Show_DHT11_Data(humiH,humiL,tempH,tempL,commandReceived);
			HC05_SendTemperatureHumidity(tempH,tempL,humiH,humiL);
		}
		else
		{
			Show_DHT11_Error();
			HC05_SendLine("DHT11 read error");
			DHT11_Init();
		}

		delay_ms(SENSOR_REFRESH_MS);
	}
}

static void Show_DHT11_Data(u8 humiH,u8 humiL,u8 tempH,u8 tempL,u8 commandReceived)
{
	OLED_Clear();
	OLED_ShowString(16,0,(u8 *)"DHT11 + HC05",16);

	OLED_ShowString(0,18,(u8 *)"Temp:",16);
	OLED_ShowNum(48,18,tempH,2,16);
	OLED_ShowString(64,18,(u8 *)".",16);
	OLED_ShowNum(72,18,tempL,1,16);
	OLED_ShowString(88,18,(u8 *)"C",16);

	OLED_ShowString(0,34,(u8 *)"Humi:",16);
	OLED_ShowNum(48,34,humiH,2,16);
	OLED_ShowString(64,34,(u8 *)".",16);
	OLED_ShowNum(72,34,humiL,1,16);
	OLED_ShowString(88,34,(u8 *)"%",16);

	if(commandReceived!=0)
	{
		OLED_ShowString(0,52,(u8 *)"BT RX: 123 OK",12);
	}
	else
	{
		OLED_ShowString(0,52,(u8 *)"BT TX: every 2s",12);
	}
	OLED_Refresh();
}

static void Show_DHT11_Error(void)
{
	OLED_Clear();
	OLED_ShowString(16,0,(u8 *)"DHT11 + HC05",16);
	OLED_ShowString(8,22,(u8 *)"Sensor Error",16);
	OLED_ShowString(0,46,(u8 *)"Check PA11/VCC",12);
	OLED_Refresh();
}

HC_05.c代码

#include "HC_05.h"
#include "stm32f10x.h"
#include "delay.h"

/*
 * 文件说明:HC-05 蓝牙串口模块驱动。
 *
 * 本驱动使用 USART1 完成蓝牙数据的收发:
 *   PA9  配置为复用推挽输出,连接 HC-05 的 RXD;
 *   PA10 配置为浮空输入,连接 HC-05 的 TXD。
 *
 * 接收数据由 USART1 中断写入环形缓冲区,同时在接收字节流中匹配
 * HC05_APP_COMMAND。主循环可读取命令标志,也可逐字节读取原始数据。
 */

/* 环形缓冲区至少需要两个位置,才能区分“空”和“满”两种状态。 */
#if (HC05_RX_BUFFER_SIZE < 2u)
#error "HC05_RX_BUFFER_SIZE must be at least 2"
#endif

/* 需要在手机接收数据中匹配的命令字符串,当前为“123”。 */
static const char hc05_app_command[] = HC05_APP_COMMAND;

/* 串口接收环形缓冲区及其读写位置。 */
static volatile uint8_t hc05_rx_buffer[HC05_RX_BUFFER_SIZE];
static volatile uint16_t hc05_rx_head = 0u;
static volatile uint16_t hc05_rx_tail = 0u;

/* 命令匹配位置及完整命令接收标志。 */
static volatile uint8_t hc05_command_index = 0u;
static volatile uint8_t hc05_command_received = 0u;

/*
 * 功能:计算环形缓冲区的下一个位置。
 * 参数:index 为当前位置。
 * 返回:下一个有效数组下标,到达末尾后自动回到 0。
 */
static uint16_t HC05_NextRxIndex(uint16_t index)
{
	index++;
	if(index >= HC05_RX_BUFFER_SIZE)
	{
		index = 0u;
	}
	return index;
}

/* 清空接收缓冲区位置、命令匹配位置和命令标志。 */
static void HC05_ResetReceiveState(void)
{
	hc05_rx_head = 0u;
	hc05_rx_tail = 0u;
	hc05_command_index = 0u;
	hc05_command_received = 0u;
}

/*
 * 功能:将中断收到的一个字节保存到环形缓冲区。
 * 说明:缓冲区已满时丢弃最早的一个字节,始终保留最新数据。
 */
static void HC05_StoreReceivedByte(uint8_t data)
{
	uint16_t next_head;

	next_head = HC05_NextRxIndex(hc05_rx_head);
	if(next_head == hc05_rx_tail)
	{
		/* 缓冲区已满,尾指针前移一格,丢弃最早的数据。 */
		hc05_rx_tail = HC05_NextRxIndex(hc05_rx_tail);
	}

	hc05_rx_buffer[hc05_rx_head] = data;
	hc05_rx_head = next_head;
}

/* 将 0~255 的无符号整数转换为十进制字符并通过蓝牙发送。 */
static void HC05_SendUInt8Decimal(uint8_t value)
{
	uint8_t hundreds;
	uint8_t tens;
	uint8_t ones;

	hundreds = (uint8_t)(value / 100u);
	tens = (uint8_t)((value / 10u) % 10u);
	ones = (uint8_t)(value % 10u);

	if(hundreds != 0u)
	{
		HC05_SendByte((uint8_t)('0' + hundreds));
	}
	if((hundreds != 0u) || (tens != 0u))
	{
		HC05_SendByte((uint8_t)('0' + tens));
	}
	HC05_SendByte((uint8_t)('0' + ones));
}

/* 使用默认波特率初始化 HC-05 串口。 */
uint8_t HC05_Init(void)
{
	return HC05_InitWithBaudRate(HC05_DEFAULT_BAUDRATE);
}

/*
 * 功能:使用指定波特率初始化 HC-05 通信接口。
 * 配置:USART1、PA9、PA10 和 USART1 接收中断。
 */
uint8_t HC05_InitWithBaudRate(uint32_t baudrate)
{
	GPIO_InitTypeDef gpio_init;
	USART_InitTypeDef usart_init;
	NVIC_InitTypeDef nvic_init;

	if(baudrate == 0u)
	{
		baudrate = HC05_DEFAULT_BAUDRATE;
	}

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
	                       RCC_APB2Periph_AFIO |
	                       RCC_APB2Periph_USART1, ENABLE);

	/* PA9:STM32 发送端,连接 HC-05 的 RXD。 */
	GPIO_StructInit(&gpio_init);
	gpio_init.GPIO_Pin = GPIO_Pin_9;
	gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
	gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_Init(GPIOA, &gpio_init);

	/* PA10:STM32 接收端,连接 HC-05 的 TXD。 */
	gpio_init.GPIO_Pin = GPIO_Pin_10;
	gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOA, &gpio_init);

	/* USART1 配置为指定波特率、8 位数据位、无校验、1 位停止位。 */
	USART_DeInit(USART1);
	USART_StructInit(&usart_init);
	usart_init.USART_BaudRate = baudrate;
	usart_init.USART_WordLength = USART_WordLength_8b;
	usart_init.USART_StopBits = USART_StopBits_1;
	usart_init.USART_Parity = USART_Parity_No;
	usart_init.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	usart_init.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	USART_Init(USART1, &usart_init);

	HC05_ResetReceiveState();

	/* 配置并使能 USART1 中断通道。 */
	nvic_init.NVIC_IRQChannel = USART1_IRQn;
	nvic_init.NVIC_IRQChannelPreemptionPriority = 3u;
	nvic_init.NVIC_IRQChannelSubPriority = 3u;
	nvic_init.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&nvic_init);

	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	USART_Cmd(USART1, ENABLE);

	return HC05_OK;
}

/* 等待发送数据寄存器为空,然后向 HC-05 发送一个字节。 */
void HC05_SendByte(uint8_t data)
{
	while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET)
	{
	}
	USART_SendData(USART1, data);
}

/* 连续发送一段二进制数据;空指针不会执行发送操作。 */
void HC05_SendBuffer(const uint8_t *buffer, uint16_t length)
{
	uint16_t index;

	if(buffer == 0)
	{
		return;
	}

	for(index = 0u; index < length; index++)
	{
		HC05_SendByte(buffer[index]);
	}
}

/* 发送以 '\0' 结束的字符串;空指针不会执行发送操作。 */
void HC05_SendString(const char *string)
{
	if(string == 0)
	{
		return;
	}

	while(*string != '\0')
	{
		HC05_SendByte((uint8_t)*string);
		string++;
	}
}

/*
 * 发送一行字符串,并在末尾自动追加回车和换行。
 * 行内字节连续发送;整行完全离开 USART1 后再执行行间延时,
 * 避免多行消息紧挨着到达手机 APP,给接收端留出处理时间。
 */
void HC05_SendLine(const char *string)
{
	HC05_SendString(string);
	HC05_SendString("\r\n");
	HC05_Flush();
	if(HC05_TX_LINE_GAP_MS > 0u)
	{
		delay_ms(HC05_TX_LINE_GAP_MS);
	}
}

/*
 * 将 DHT11 的温度和湿度组合为一行便于手机查看的文本。
 * 数据格式:Temp:温度.C,Humi:湿度.%
 */
void HC05_SendTemperatureHumidity(uint8_t temperature_integer,
                                  uint8_t temperature_decimal,
                                  uint8_t humidity_integer,
                                  uint8_t humidity_decimal)
{
	HC05_SendString("Temp:");
	HC05_SendUInt8Decimal(temperature_integer);
	HC05_SendByte((uint8_t)'.');
	HC05_SendUInt8Decimal(temperature_decimal);
	HC05_SendString("C,Humi:");
	HC05_SendUInt8Decimal(humidity_integer);
	HC05_SendByte((uint8_t)'.');
	HC05_SendUInt8Decimal(humidity_decimal);
	HC05_SendLine("%");
}

/*
 * 功能:处理 USART1 接收到的一个字节。
 * 步骤:先保存原始字节,再逐字节匹配 HC05_APP_COMMAND。
 * 完整收到“123”后置位命令标志,由主循环读取并回复手机。
 */
void HC05_HandleReceivedByte(uint8_t data)
{
	HC05_StoreReceivedByte(data);

	if(hc05_app_command[0] == '\0')
	{
		return;
	}

	if(data == (uint8_t)hc05_app_command[hc05_command_index])
	{
		/* 当前字节匹配,继续检查命令中的下一个字符。 */
		hc05_command_index++;
		if(hc05_app_command[hc05_command_index] == '\0')
		{
			/* 已连续收到完整命令,置位标志并准备匹配下一条命令。 */
			hc05_command_received = 1u;
			hc05_command_index = 0u;
		}
	}
	else if(data == (uint8_t)hc05_app_command[0])
	{
		/* 当前字节可作为一条新命令的第一个字符。 */
		hc05_command_index = 1u;
	}
	else
	{
		hc05_command_index = 0u;
	}
}

/*
 * 返回环形缓冲区中尚未读取的字节数。
 * 读取首尾指针期间暂时关闭接收中断,避免中断同时修改指针。
 */
uint16_t HC05_RxAvailable(void)
{
	uint16_t head;
	uint16_t tail;

	USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
	head = hc05_rx_head;
	tail = hc05_rx_tail;
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

	if(head >= tail)
	{
		return (uint16_t)(head - tail);
	}
	return (uint16_t)(HC05_RX_BUFFER_SIZE - tail + head);
}

/*
 * 从环形缓冲区读取一个字节。
 * 返回 1 表示读取成功,返回 0 表示参数无效或当前没有数据。
 */
uint8_t HC05_ReadRxByte(uint8_t *data)
{
	if(data == 0)
	{
		return 0u;
	}

	USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
	if(hc05_rx_head == hc05_rx_tail)
	{
		USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
		return 0u;
	}

	*data = hc05_rx_buffer[hc05_rx_tail];
	hc05_rx_tail = HC05_NextRxIndex(hc05_rx_tail);
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	return 1u;
}

/* 清空接收环形缓冲区,不影响已经识别到的命令标志。 */
void HC05_ClearRxBuffer(void)
{
	USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
	hc05_rx_tail = hc05_rx_head;
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}

/*
 * 读取并清除命令接收标志。
 * 该设计确保一条命令在主循环中只被处理一次。
 */
uint8_t HC05_TakeAppCommandReceived(void)
{
	uint8_t received;

	USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
	received = hc05_command_received;
	hc05_command_received = 0u;
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	return received;
}

/* 等待发送完成标志置位,确保最后一个字节已离开串口移位寄存器。 */
void HC05_Flush(void)
{
	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET)
	{
	}
}

/* USART1 接收中断服务函数。 */
void USART1_IRQHandler(void)
{
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
	{
		HC05_HandleReceivedByte((uint8_t)USART_ReceiveData(USART1));
	}

	/* 依次读取状态寄存器和数据寄存器可清除串口溢出错误。 */
	if(USART_GetFlagStatus(USART1, USART_FLAG_ORE) != RESET)
	{
		(void)USART_ReceiveData(USART1);
	}
}

HC_05.h代码

#ifndef __HC_05_H
#define __HC_05_H

#include <stdint.h>

/*
 * 文件说明:STM32F103C8T6 的 HC-05 蓝牙串口驱动头文件。
 *
 * 硬件连接:
 *   HC-05 RXD <- PA9 (USART1_TX,单片机发送端)
 *   HC-05 TXD -> PA10(USART1_RX,单片机接收端)
 *   HC-05 VCC -> 5V
 *   HC-05 GND -> GND
 *
 * 默认串口参数:9600 波特率、8 位数据位、无校验、1 位停止位、无硬件流控。
 */

/* 驱动函数执行成功时的返回值。 */
#define HC05_OK                     0u

/* HC-05 数据透传模式的默认通信波特率。 */
#define HC05_DEFAULT_BAUDRATE       9600u

/*
 * 每发送完一整行数据后的等待时间,单位为毫秒。
 * 行内字节保持连续发送,行与行之间留出处理时间,减轻手机 APP 接收压力。
 */
#define HC05_TX_LINE_GAP_MS         50u

/* 串口接收环形缓冲区大小,实际最多保存 127 个尚未读取的字节。 */
#define HC05_RX_BUFFER_SIZE         128u

/* 手机 APP 发送该命令后,主程序回复“I get it!Sir!”。 */
#define HC05_APP_COMMAND            "123"

/* 使用默认波特率初始化 PA9、PA10、USART1 和接收中断。 */
uint8_t HC05_Init(void);

/* 使用指定波特率初始化 HC-05;baudrate 为 0 时使用默认波特率。 */
uint8_t HC05_InitWithBaudRate(uint32_t baudrate);

/* 向 HC-05 发送一个字节。 */
void HC05_SendByte(uint8_t data);

/* 向 HC-05 连续发送指定长度的二进制数据。 */
void HC05_SendBuffer(const uint8_t *buffer, uint16_t length);

/* 向 HC-05 发送以 '\0' 结束的字符串。 */
void HC05_SendString(const char *string);

/* 发送字符串,追加“\r\n”,等待整行发送完成,然后执行行间延时。 */
void HC05_SendLine(const char *string);

/*
 * 发送 DHT11 温湿度数据。
 * 输出示例:Temp:25.0C,Humi:60.0%
 */
void HC05_SendTemperatureHumidity(uint8_t temperature_integer,
                                  uint8_t temperature_decimal,
                                  uint8_t humidity_integer,
                                  uint8_t humidity_decimal);

/* 等待 USART1 中最后一个字节发送完成。 */
void HC05_Flush(void);

/* 返回接收环形缓冲区中尚未读取的字节数。 */
uint16_t HC05_RxAvailable(void);

/* 从接收缓冲区读取一个字节;成功返回 1,无数据或参数无效时返回 0。 */
uint8_t HC05_ReadRxByte(uint8_t *data);

/* 丢弃接收缓冲区中所有尚未读取的数据。 */
void HC05_ClearRxBuffer(void);

/* 处理一个接收字节,由 USART1_IRQHandler 调用。 */
void HC05_HandleReceivedByte(uint8_t data);

/*
 * 查询并清除手机命令接收标志。
 * 完整收到一次 HC05_APP_COMMAND 后返回 1,否则返回 0。
 */
uint8_t HC05_TakeAppCommandReceived(void);

/* USART1 接收中断服务函数。 */
void USART1_IRQHandler(void);

#endif

八、常见问题与故障排查

1、手机搜索不到 HC‑05 蓝牙

  1. 确认 HC‑05 供电正常,模块电源指示灯点亮;
  2. 确认模块没有被别的手机提前连接占用;
  3. 确认 APP 支持经典蓝牙,不是 BLE 低功耗蓝牙。

2、蓝牙可以连接,但是手机收不到温湿度

  1. 检查 PA9 到 HC‑05 RXD 接线,是否松动、接反;
  2. 确认 STM32 和 HC‑05GND 共地;
  3. HC‑05 模块实际波特率确认是 9600;
  4. APP 已经成功建立蓝牙串口通道。

3、手机发送 123,没有收到回复

  1. 检查 HC‑05 TXD 接到 PA10,接线不能接反;
  2. APP 必须是文本 ASCII 发送模式,不要打开 16 进制发送;
  3. 工程不要重复定义 USART1 中断函数。

4、手机接收出现乱码

  1. STM32、HC‑05、手机 APP 三方波特率全部设置 9600;
  2. APP 使用文本显示,不要开启十六进制显示。

5、手机偶尔收到半行数据

串口蓝牙属于字节流通信,系统底层接收分包是随机的,一行报文可能被拆分成多次给到 APP。

APP 解析逻辑:持续累积接收字符,检测\r\n换行标记之后,再当做一条完整消息处理,不要把单次回调当成一帧完整数据。本实验使用 2 秒上报周期 + 行间隔延时用来缓解分包现象,但是不能彻底消除字节流分包。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐