HC-05是一款广泛应用于嵌入式系统的蓝牙串口通信模块,支持主从模式自由切换,可通过UART接口与MCU进行通信。
在使用前,建议先通过USB转TTL模块将HC-05连接至PC,发送测试指令以验证模块的接收响应功能是否正常。
目前uart1和uart6均已测试可用,性能无差异。基于此,我选择使用uart6进行通信,同时保留uart2作为日志输出接口。
方案设计创建两个独立任务:发送任务负责向蓝牙传输数据,接收任务处理来自蓝牙的响应数据。考虑到数据接收可能存在延迟,采用串口中断机制进行数据接收,并通过设置标志位在接收任务中进行后续处理。
为提高多任务处理效率,可采用队列机制实现数据发送:先将数据存入队列,再由发送任务从队列中获取消息并转发至蓝牙设备。
1,初始化串口配置,与debug串口不同的是多了一个使能接收中断。
c
void UART_BT_Init(uint32_t buad)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
//时钟开启
RCC_AHB1PeriphClockCmd(UARTx_BT_GPIO_CLK, ENABLE);
RCC_APB2PeriphClockCmd(UARTx_BT_RCC_CLK, ENABLE);
//串口1对应引脚复用映射
GPIO_PinAFConfig(UARTx_BT_TX_GPIO_PORT, UARTx_BT_TX_PIN_SOURCE, UARTx_BT_TX_AF); //GPIOC6复用为USART6
GPIO_PinAFConfig(UARTx_BT_RX_GPIO_PORT, UARTx_BT_RX_PIN_SOURCE, UARTx_BT_RX_AF); //GPIOC7复用为USART6
/* Reset GPIO init structure parameters values */
GPIO_InitStruct.GPIO_Pin = UARTx_BT_TX_PIN | UARTx_BT_RX_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(UARTx_BT_TX_GPIO_PORT, &GPIO_InitStruct);
/* USART_InitStruct members default value */
USART_InitStruct.USART_BaudRate = buad;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No ;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_Init(UARTx_BT, &USART_InitStruct);
// 使能接收中断 (RXNE)
USART_ITConfig(UARTx_BT, USART_IT_RXNE, ENABLE);
//使能
USART_Cmd(UARTx_BT, ENABLE);
}
2,串口中断,目前数据量不大就没有设置很大的缓冲区。
c
void USART6_IRQHandler(void)
{
if (USART_GetITStatus(UARTx_BT, USART_IT_RXNE) != RESET)
{
if (USARTx_BT_Rx_Index < 256)
{
USARTx_BT_Rx_Data[USARTx_BT_Rx_Index++] = (uint8_t)USART_ReceiveData(UARTx_BT);
}
else
{
// 缓冲区满,丢弃数据并重置索引
(void)USART_ReceiveData(UARTx_BT);
USARTx_BT_Rx_Index = 0; // 重置避免溢出
}
}
}
3,接收任务,通过在一定时间内检测缓冲区数据长度是否稳定来判断数据接收是否完成。
c
void BT_RxTask(void *pvParameters)
{
uint16_t len;
uint8_t ret = 0;
uint16_t last_index = 0;
uint32_t last_recv_time = 0;
while (1)
{
if (USARTx_BT_Rx_Index > 0)
{
if (last_index != USARTx_BT_Rx_Index) {
// 有新的数据到达,更新时间戳
last_index = USARTx_BT_Rx_Index;
last_recv_time = xTaskGetTickCount();
}
// 检查是否超过10ms没有新数据
else if ((xTaskGetTickCount() - last_recv_time) > pdMS_TO_TICKS(50) && last_index == USARTx_BT_Rx_Index)
{
// 超时,认为接收完成
taskENTER_CRITICAL();
len = USARTx_BT_Rx_Index;
USARTx_BT_Rx_Index = 0;
taskEXIT_CRITICAL();
last_index = 0;
// printfHex(USARTx_BT_Rx_Data, len);
printf("receiving data: %.*s\r\n", len, USARTx_BT_Rx_Data);
}
}
else {
last_index = 0; // 重置
}
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms轮询
}
}
4,发送处理流程如下:当调用发送接口时,系统会先将数据存入发送队列,随后由发送任务从队列中提取数据,并通过串口向蓝牙设备进行传输。
发送接口:
c
uint16_t USARTx_BT_Send(uint8_t *data, uint8_t length)
{
USARTx_BT_TxPacket_t packet;
// 检查队列是否已创建
if (USARTx_BT_TxQueue == NULL)
{
printf("Queue not initialized!\r\n");
return USART_BT_QUEUE_CREATION_FAILED;
}
// printf("Queueing data for transmission: %.*s\r\n", length, data);
packet.length = length;
if (packet.length > 255)
return USART_BT_SEND_LEN_ERROR;
memcpy(packet.buffer, data, packet.length);
BaseType_t ret = xQueueSend(USARTx_BT_TxQueue, &packet, pdMS_TO_TICKS(100));
if (ret != pdTRUE)
{
printf("Queue send failed!\r\n");
return USART_BT_QUEUE_SEND_FAILED;
}
return 0;
}
发送任务:
c
void BT_TxTask(void *pvParameters)
{
USARTx_BT_TxPacket_t packet;
USARTx_BT_TxQueue = xQueueCreate(10, sizeof(USARTx_BT_TxPacket_t));
// 添加队列创建失败的检查
if (USARTx_BT_TxQueue == NULL)
{
printf("USARTx_BT_TxQueue creation failed!\r\n");
while(1); // 队列创建失败,停机(喂狗任务也无法执行,会复位)
}
printf("USARTx_BT_TxQueue created successfully.\r\n");
while(1)
{
if (xQueueReceive(USARTx_BT_TxQueue, &packet, portMAX_DELAY) == pdTRUE)
{
// printf("Sending data: %.*s\r\n", packet.length, packet.buffer);
for (int i = 0; i < packet.length; i++)
{
USART_SendData(UARTx_BT, packet.buffer[i]);
while (USART_GetFlagStatus(UARTx_BT, USART_FLAG_TXE) == RESET);
}
}
}
}
建议先发送"AT\r\n"测试通讯连接是否正常。为便于后续手机连接,新增了初始化任务,用于发送AT指令完成以下操作:查询设备版本信息、配置设备名称、启用从模式等参数设置。该初始化任务执行完毕后会自动销毁。
c
uint8_t HC05_Send_AT_Command(const char *command)
{
return USARTx_BT_Send((uint8_t *)command, strlen(command));
}
uint8_t HC_05_Init(void)
{
// 发送AT命令测试蓝牙模块是否正常工作
HC05_Send_AT_Command("AT\r\n");
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保模块有足够时间响应
// 获取蓝牙模块版本信息
HC05_Send_AT_Command("AT+VERSION?\r\n");
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保模块有足够时间响应
// 设置蓝牙模块名称
HC05_Send_AT_Command("AT+NAME=STM32_BT\r\n");
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保模块有足够时间响应
// 查询蓝牙模块名称
HC05_Send_AT_Command("AT+NAME?\r\n");
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保模块有足够时间响应
// 查询蓝牙模块密码
HC05_Send_AT_Command("AT+PSWD?\r\n");
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保模块有足够时间响应
// // 设置蓝牙模块密码
// HC05_Send_AT_Command("AT+PSWD=3214\r\n");
// vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保模块有足够时间响应
// 设置蓝牙模块为从模式
HC05_Send_AT_Command("AT+ROLE=0\r\n");
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保模块有足够时间响应
// 查询蓝牙模块地址
HC05_Send_AT_Command("AT+ADDR?\r\n");
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保模块有足够时间响应
return 0; // 返回0表示初始化成功
}
正常初始化的log为:
c
Serial communication is normal
Free heap before any task: 0 bytes
After Key_Task: 20112 bytes
After Feeddog_Task: 19880 bytes
After BT_TxTask: 15680 bytes
After BT_RxTask: 11480 bytes
After Bluetooth_Init_Task: 9328 bytes
Bluetooth initialization task started
USARTx_BT_TxQueue created successfully.
Starting Bluetooth AT commands...
receiving data: OK
receiving data: +VERSION:hc05V2.3_le OK
receiving data: OK
receiving data: +NAME:STM32_BT
OK
receiving data: +PSWD:NO KEY OK
receiving data: OK
receiving data: +ADDR=12:64:99:BE:EC:97 OK
Bluetooth initialization completed, deleting this task
当使用手机蓝牙调试应用(已放仓库/相关文件)搜索蓝牙设备时,设备名称可能显示为"STM32_BT",但连接成功后,在通信界面显示的可能是"HC-05"。
在数据传输过程中,由于编码格式差异,接收端可能会出现乱码现象。此时建议改用十六进制(hex)格式打印数据,以确保数据正确显示。
发送:

接收:

可以自定义通信协议,其格式为:帧头 + 命令码 + 数据长度 + 数据内容 + 校验和 + 帧尾。通过校验帧头、帧尾与校验和可确保数据包的完整性,而不同的命令码则用于区分具体功能。
c
#define BT_PROTOCOL_HEADER 0xA5 // 帧头
#define BT_PROTOCOL_TAIL 0x5A // 帧尾
/* 数据包结构体 ------------------------------------------------------------*/
typedef struct {
uint8_t header; // 帧头 (0xA5)
uint8_t cmd; // 命令码
uint8_t length; // 数据长度 (0-256)
uint8_t data[256]; // 数据内容
uint8_t checksum; // 校验和
uint8_t tail; // 帧尾 (0x5A)
} BT_Protocol_Packet_t;
/* 命令码定义 --------------------------------------------------------------*/
typedef enum {
CMD_NONE = 0x00, // 无操作
CMD_LED_TOGGLE = 0x01, // LED翻转控制(无参数)
CMD_LED_ON = 0x02, // LED开启(带参数:0x01)
CMD_LED_OFF = 0x03, // LED关闭(带参数:0x00)
CMD_GET_TEMP_HUMI = 0x10, // 获取温湿度数据
CMD_GET_VOLTAGE = 0x11, // 获取电压
CMD_GET_CURRENT = 0x12, // 获取电流
CMD_GET_POWER = 0x13, // 获取功率
CMD_GET_ALL_DATA = 0x14, // 获取所有数据
CMD_DEVICE_STATUS = 0x20, // 查询设备状态
CMD_DEVICE_RESET = 0x21, // 设备复位
CMD_ECHO_TEST = 0x30, // 回环测试
} Protocol_Cmd_t;
收到手机下发的数据,进行数据包解析
c
uint8_t BT_Protocol_ParsePacket(uint8_t *buffer, uint16_t length, BT_Protocol_Packet_t *packet)
{
uint16_t i = 0;
uint8_t calc_checksum;
// 查找帧头
while (i < length && buffer[i] != BT_PROTOCOL_HEADER)
{
i++;
}
if (i > length - 5) // 至少需要 header(1)+cmd(1)+len(1)+checksum(1)+tail(1)=5字节
{
return 1; // 未找到有效帧头或数据不足
}
// 提取数据包
packet->header = buffer[i++];
packet->cmd = buffer[i++];
packet->length = buffer[i++];
// 检查长度是否合理
if (packet->length > 250 || i + packet->length + 2 > length)
{
return 2; // 长度不合理
}
// 复制数据
memcpy(packet->data, &buffer[i], packet->length);
i += packet->length;
packet->checksum = buffer[i++];
packet->tail = buffer[i++];
// 验证帧尾
if (packet->tail != BT_PROTOCOL_TAIL)
{
return 3;
}
// 验证校验和
calc_checksum = packet->cmd ^ packet->length;
for (uint8_t j = 0; j < packet->length; j++)
{
calc_checksum ^= packet->data[j];
}
if (calc_checksum != packet->checksum)
{
return 4; // 校验失败
}
return 0; // 解析成功
}
解析完,提取命令码实现不同的功能
c
void BT_Protocol_ProcessCommand(BT_Protocol_Packet_t *packet)
{
uint8_t response_data[10];
uint8_t response_len = 0;
switch (packet->cmd)
{
case CMD_LED_TOGGLE:
// LED翻转
LED_Toggle();
BT_Protocol_SendResponse(packet->cmd, RSP_OK, NULL, 0);
// printf("LED toggled via Bluetooth\r\n");
break;
case CMD_LED_ON:
// LED开启
if (packet->length >= 1 && packet->data[0] == 0x01) {
LED(1);
BT_Protocol_SendResponse(packet->cmd, RSP_OK, NULL, 0);
// printf("LED turned ON via Bluetooth\r\n");
} else {
BT_Protocol_SendResponse(packet->cmd, RSP_INVALID_DATA, NULL, 0);
}
break;
case CMD_LED_OFF:
// LED关闭
if (packet->length >= 1 && packet->data[0] == 0x00) {
LED(0);
BT_Protocol_SendResponse(packet->cmd, RSP_OK, NULL, 0);
// printf("LED turned OFF via Bluetooth\r\n");
} else {
BT_Protocol_SendResponse(packet->cmd, RSP_INVALID_DATA, NULL, 0);
}
break;
case CMD_GET_TEMP_HUMI:
// 获取温湿度数据(示例数据)
response_data[0] = DHT11_data.temp_int; // 温度
response_data[1] = DHT11_data.humi_int; // 湿度
response_len = 2;
BT_Protocol_SendResponse(packet->cmd, RSP_OK, response_data, response_len);
// printf("Sent temperature and humidity data\r\n");
break;
case CMD_ECHO_TEST:
// 回环测试
response_len = packet->length;
memcpy(response_data, packet->data, packet->length);
BT_Protocol_SendResponse(packet->cmd, RSP_OK, response_data, response_len);
// printf("Echo test: sent back %d bytes\r\n", response_len);
break;
default:
// 未知命令
BT_Protocol_SendResponse(packet->cmd, RSP_UNKNOWN_CMD, NULL, 0);
printf("Unknown command: 0x%02X\r\n", packet->cmd);
break;
}
}
执行完功能要返回状态给手机
c
void BT_Protocol_SendResponse(uint8_t cmd, uint8_t result, uint8_t *data, uint8_t data_len)
{
uint8_t buffer[300] = {0};
uint16_t index = 0;
uint8_t checksum = 0;
buffer[index++] = BT_PROTOCOL_HEADER; // 帧头
buffer[index++] = cmd; // 响应命令码(最高位置1)
buffer[index++] = data_len + 1; // 长度(响应码 + 数据)
buffer[index++] = result; // 响应码
// 复制数据
if (data_len > 0 && data != NULL) {
memcpy(&buffer[index], data, data_len);
index += data_len;
}
// 计算校验和
checksum = (cmd) ^ (data_len + 1) ^ result;
for (uint8_t i = 0; i < data_len; i++) {
checksum ^= data[i];
}
buffer[index++] = checksum; // 校验和
buffer[index++] = BT_PROTOCOL_TAIL; // 帧尾
// 通过蓝牙发送
USARTx_BT_Send(buffer, index);
}
接下来可以在手里蓝牙调试里面根据这个协议来编辑两个按钮,用于实现LED翻转,获取温湿度。

