目录
一、汇承HC-05蓝牙模块介绍
1.蓝牙简介
HC-05 蓝牙串口通信模块,是基于 Bluetooth Specification V2.0 带 EDR 蓝牙协议的数传模块。无线工作频段为 2.4GHz ISM,调制方式是 GFSK。模块最大发射功率为 4dBm,接收灵敏度-85dBm,板载 PCB 天线,可以实现 10 米距离通信。
模块采用邮票孔封装方式,模块大小 27mm×13mm×2mm,方便客户嵌入应用系统之内,自带 LED 灯,可直观判断蓝牙的连接状态。
模块采用 CSR 的 BC417 芯片,支持 AT 指令,用户可根据需要更改角色(主、从模式)以及串口波特率、设备名称等参数,使用灵活。
2.基本参数
3.工作原理
如上图所示,HC-05 模块用于代替全双工通信时的物理连线。左边的设备向模块发送串口数据,模块的 RXD 端口收到串口数据后,自动将数据以无线电波的方式发送到空中。右边的模块能自动接收到,并从 TXD 还原最初左边设备所发的串口数据。从右到左也是一样的。
4.与各个设备连接
(1)与MCU之间的连接
①:模块与供电系统为 3.3V 的 MCU 连接时,串口交叉连接即可(模块的 RX 接 MCU 的 TX、模块的 TX 接 MCU的 RX)
②:模块与供电系统为 5V 的 MCU 连接时,可在模块的 RX 端串接一个 220R~1K 电阻再接 MCU 的 TX,模块的TX 直接接 MCU 的 RX,无需串接电阻。(注:请先确认所使用的 MCU 把 3.0V 或以上电压认定为高电平,否则需加上 3.3V/5V 电平转换电路)
注:模块的电源为 3.3V,不能接 5V, 5V 的电源必须通过 LDO 降压到 3.3V 后再给模块供电我是直接连接开发板5V,暂时没出现什么问题
(2)与手机之间的连接
HC-05 可以与安卓手机自带蓝牙连接,通讯测试可以使用安卓串口助手软件,可在汇承官网下载
广州汇承信息科技有限公司https://www.hc01.com/downloads
(3)与模块之间的连接
设置一个为主机,一个为从机,配对码一致(默认均为 1234),波特率一致,上电即可自动连接。
HC-05 支持一对一连接。
在连接模式 CMODE 为 0 时,主机第一次连接后,会自动记忆配对对象,如需连接其他模块, 必须先清除配对记忆。在连接模式 CMODE 为 1 时,主机则不受绑定指令设置地址的约束,可以与其他从机模块连接。
5.模块原理图
6.常用AT指令
7.HC-T串口助手测试
初学者套餐里带有HC-T,插上通电就可以简单测试AT指令。
二、开发板
使用的两块开发板分别为野火STM32F103RCT6MIN开发板、野火STM32F103ZET6核心板双USB款开发板分别作为蓝牙主机和从机
野火STM32F103ZET6
STM32F103RCT6MIN
三、代码部分
1.获取电脑系统时间
软件使用VS2022
#define _CRT_SECURE_NO_WARNINGS 1
#include <windows.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#define MAX_PORTS 32
typedef struct {
char name[64];
} PortInfo;
void getCurrentTimeString(char* buffer, int len) {
time_t now = time(NULL);
struct tm* t = localtime(&now);
strftime(buffer, len, "T%Y-%m-%d %H:%M:%S\n", t);//获取计算机时间
}
int tryOpenPort(const char* portName) {
HANDLE h = CreateFileA(portName, GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);
if (h == INVALID_HANDLE_VALUE) return 0;
CloseHandle(h);
return 1;
}
HANDLE openSerialPort(const char* portName) {
HANDLE hSerial = CreateFileA(portName, GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);
if (hSerial == INVALID_HANDLE_VALUE) return NULL;
DCB dcb = { 0 }; // 创建 DCB 结构体并清零
dcb.DCBlength = sizeof(dcb); // 设置 DCB 结构体长度
GetCommState(hSerial, &dcb); // 获取当前串口配置
dcb.BaudRate = CBR_115200; // 设置波特率为 115200
dcb.ByteSize = 8; // 数据位 8 位
dcb.StopBits = ONESTOPBIT; // 停止位 1 位
dcb.Parity = NOPARITY; // 无奇偶校验位
SetCommState(hSerial, &dcb); // 应用新的串口配置
COMMTIMEOUTS timeouts = { 0 };
timeouts.WriteTotalTimeoutConstant = 50;
SetCommTimeouts(hSerial, &timeouts);
return hSerial;
}
int listAvailablePorts(PortInfo* ports) {
int count = 0;
for (int i = 1; i <= 30; i++) {
char name[64];
sprintf(name, "\\\\.\\COM%d", i);
if (tryOpenPort(name)) {
strcpy(ports[count].name, name);
count++;
}
}
return count;
}
int main() {
PortInfo ports[MAX_PORTS];
int portCount = listAvailablePorts(ports);
if (portCount == 0) {
printf("未发现任何可用串口。\n");
return 1;
}
printf(" 发现以下串口:\n");
for (int i = 0; i < portCount; i++) {
printf(" [%d] %s\n", i + 1, ports[i].name);
}
int choice = 0;
printf("请选择串口编号(1-%d):", portCount);
scanf("%d", &choice);
if (choice < 1 || choice > portCount) {
printf("输入编号无效。\n");
return 1;
}
const char* selectedPort = ports[choice - 1].name;
printf("选择串口:%s\n", selectedPort);
HANDLE hSerial = NULL;
while (1) {
if (!hSerial || hSerial == INVALID_HANDLE_VALUE) {
hSerial = openSerialPort(selectedPort);
if (!hSerial) {
printf("串口打开失败,5秒后重试...\n");
Sleep(5000);
continue;
}
printf("串口连接成功,开始发送时间。\n");
}
char timeStr[64];
getCurrentTimeString(timeStr, sizeof(timeStr));
DWORD bytesWritten;
BOOL success = WriteFile(hSerial, timeStr, strlen(timeStr), &bytesWritten, NULL);
if (!success || bytesWritten == 0) {
printf("写入失败,串口可能已断开。\n");
CloseHandle(hSerial);
hSerial = NULL;
continue;
}
printf("已发送:%s", timeStr);
Sleep(1000);
}
if (hSerial) CloseHandle(hSerial);
return 0;
}
运行代码现象如下,先选择要使用的串口,我这个电脑不使用单片机时为COM1,使用单片机时为COM3,传输时间需要使用作为主机的串口。
2.蓝牙主机程序
void CheckConnect_LinkHC05_Test(void)
{
static uint8_t last_connected = 0; // 记录上一次是否连接
if( (last_connected==1) && ( !IS_HC05_CONNECTED() ) )
{
HC05_INFO("蓝牙连接已断开\r\n");
last_connected=0; // 重置状态
}
if( !IS_HC05_CONNECTED() ) // 判断蓝牙是否未连接,IS_HC05_CONNECTED 是用户自定义的状态检测函数
{
Usart_SendString(USART1, "蓝牙尚未连接\r\n"); // 通过串口提示未连接
if(hc05_role == 1) // 如果当前蓝牙模块为主机模式
{
HC05_INFO("正在扫描蓝牙设备..."); // 打印扫描提示信息
/* 搜索蓝牙模块,并尝试连接 */
if( linkHC05() == 0 ) // linkHC05 函数负责扫描并连接设备,返回0表示连接成功
{
while( !IS_HC05_CONNECTED() ); // 等待连接状态变为已连接(INT引脚拉低等方式)
}
}
else // 当前为从机模式
{
HC05_INFO("请搜索连接蓝牙..."); // 提示等待主机连接
HC05_Send_CMD("AT+INQ\r\n", 1); // 发送 AT 命令让 HC-05 进入查询状态,便于被搜索
}
}
else // 蓝牙已连接
{
if(last_connected == 0) // 只在首次连接时打印一次信息
{
HC05_INFO("主机蓝牙已连接上从机\r\n"); // 显示连接成功信息
// HC05_INFO("发送指令1获取蓝牙工作状态\r\n"); // 提示用户可发送数据
// sprintf(sendData, "<%s> 蓝牙已连接从机,发送指令1获取蓝牙工作状态\r\n", hc05_name); // 格式化字符串,包含设备名
// HC05_SendString(sendData); // 通过蓝牙发送数据给对方
last_connected = 1; // 设置标志位,防止重复提示
}
// 如果收到了串口时间字符串,就转发给蓝牙从机
if (USART_RX_FLAG)
{
USART_RX_FLAG = 0; // 清除标志位
if(USART_RX_BUF[0] == 'T'&& strlen(USART_RX_BUF)>10)
{
HC05_INFO("转发时间字符串至从机:\r\n");
// HC05_INFO(USART_RX_BUF[20]); // 调试打印
HC05_SendString(USART_RX_BUF); // 通过蓝牙发送时间字符串
HC05_INFO("时间已通过蓝牙发送\r\n");
}
else
{
HC05_INFO("接收到非法数据,已丢弃\r\n");
}
}
}
}
/**
* @brief 检查蓝牙接收缓冲区、处理接收数据
* @param 无
* @retval 无
*/
void CheckRecvBltBuff_Test(void)
{
char* redata;
uint16_t len;
if( IS_HC05_CONNECTED())
{
uint16_t linelen;
redata = get_rebuff(&len);
linelen = get_line(linebuff, redata,len);
if(linelen < 200 && linelen !=0)
{
if(strcmp(redata, "1") == 0) {
LED1_ON;
HC05_SendString("LED1_ON_OK\r\n");
// HC05_Send_CMD("AT+STATE?\r\n",1); //返回值为connected,表示是连接状态
}
else if(strcmp(redata,"master") == 0)
{
HC05_Send_CMD("AT+DISC\r\n",1);
Delay_ms(5000);
Switch_HC05Mode_Test();
LED1_ON;
HC05_SendString("LED1_OFF_OK\r\n");
}
else
{
/*这里只演示显示单行的数据,如果想显示完整的数据,可直接使用redata数组*/
HC05_INFO("Receive:\r\n%s",linebuff);
}
/*处理数据后,清空接收蓝牙模块数据的缓冲区*/
clean_rebuff();
}
}
else
{
// Usart_SendString( USART1, "\r\n蓝牙未连接。接收到蓝牙返回数据:\r\n" );
// redata = get_rebuff(&len);
// Usart_SendString( USART1, (uint8_t *)redata );
// Usart_SendString( USART1, "\r\n\r\n" );
}
}
代码从野火蓝牙综合示例程序移植而来,修改了部分程序,上述程序是其中最主要的两段程序,以此作为主机来发送从系统获取到的时间
3.蓝牙从机程序
蓝牙从机部分驱动程序
/**
* @brief 检查蓝牙连接
* 作为主机时,搜索蓝牙并连接名字含有“HC05”的蓝牙模块
* 作为从机时,连接后通过蓝牙模块发送字符串
* @param 无
* @retval 无
*/
void CheckConnect_LinkHC05_Test(void)
{
static uint8_t last_connected = 0; // 记录上一次是否连接
if( (last_connected==1) && ( !IS_HC05_CONNECTED() ) )
{
HC05_INFO("蓝牙连接已断开\r\n");
last_connected=0; // 重置状态
}
if( !IS_HC05_CONNECTED() ) // 判断蓝牙是否未连接,IS_HC05_CONNECTED 是用户自定义的状态检测函数
{
Usart_SendString(USART1, "蓝牙尚未连接\r\n"); // 通过串口提示未连接
if(hc05_role == 1) // 如果当前蓝牙模块为主机模式
{
HC05_INFO("正在扫描蓝牙设备..."); // 打印扫描提示信息
/* 搜索蓝牙模块,并尝试连接 */
if( linkHC05() == 0 ) // linkHC05 函数负责扫描并连接设备,返回0表示连接成功
{
while( !IS_HC05_CONNECTED() ); // 等待连接状态变为已连接(INT引脚拉低等方式)
}
}
else // 当前为从机模式
{
HC05_INFO("请搜索连接蓝牙..."); // 提示等待主机连接
HC05_Send_CMD("AT+INQ\r\n", 1); // 发送 AT 命令让 HC-05 进入查询状态,便于被搜索
}
}
else // 蓝牙已连接
{
if(last_connected == 0) // 只在首次连接时打印一次信息
{
HC05_INFO("从机蓝牙已连接上主机\r\n"); // 显示连接成功信息
// HC05_INFO("手机发送指令1\r\n"); // 提示用户可发送数据
// sprintf(sendData, "<%s> 蓝牙从机已连接,发送指令1获取蓝牙工作状态\r\n", hc05_name); // 格式化字符串,包含设备名
// HC05_SendString(sendData); // 通过蓝牙发送数据给对方
last_connected = 1; // 设置标志位,防止重复提示
}
}
}
/**
* @brief 检查蓝牙接收缓冲区、处理接收数据
* @param 无
* @retval 无
*/
void CheckRecvBltBuff_Test(void)
{
char* redata;
uint16_t len;
char buff[256] = "/";
char filename[64]; // 存储接收的文件名
if( IS_HC05_CONNECTED())
{
uint16_t linelen;
redata = get_rebuff(&len);
linelen = get_line(linebuff, redata,len);
if(linelen < 200 && linelen !=0)
{
if(strcmp(redata, "1") == 0) {
LED1_ON;
HC05_SendString("LED1_ON_OK\r\n");
}
else if(strcmp(redata,"2") == 0)
{
LED1_OFF;
HC05_SendString("LED1_OFF_OK\r\n");
}
else
{
/*这里只演示显示单行的数据,如果想显示完整的数据,可直接使用redata数组*/
HC05_INFO("Receive:\r\n%s",linebuff);
}
/*处理数据后,清空接收蓝牙模块数据的缓冲区*/
clean_rebuff();
}
}
else
{
// Usart_SendString( USART1, "\r\n蓝牙未连接。接收到蓝牙返回数据:\r\n" );
// redata = get_rebuff(&len);
// Usart_SendString( USART1, (uint8_t *)redata );
// Usart_SendString( USART1, "\r\n\r\n" );
}
}
蓝牙从机主程序main.c
#include "stm32f10x.h"
#include <string.h>
#include <stdlib.h>
#include "bsp_spi_sdcard.h"
#include "ff.h"
#include "./delay/delay.h"
#include "./sys/sys.h"
#include "./DS3231/DS3231.h"
#include "./MyI2C/MyI2C.h"
#include "./Timer/Timer.h"
#include "./led/bsp_led.h"
#include "./FAT/FAT.h"
#include "./OLED/OLED.h"
#include "./data_change/data_change.h"
#include "./Serial/Serial.h"
#include "./Key/bsp_key.h"
#include "./XYCheck/XYCheck.h"
#include "./HC05/HC05.h"
#include "./HC05/HC05_USART.h"
#include "./dwt_delay/core_delay.h"
#include "./systick/bsp_SysTick.h"
#include "./lcd/bsp_ili9341_lcd.h"
uint8_t DataBit;
uint8_t data =0x00;
uint8_t RxData;
extern uint16_t lcdid;
char time_str[20];
char date_str[20];
extern int hc05_inquery_connect; //每3s检查蓝牙是否连接
extern int hc05_check_recvbuff; //每500ms检查蓝牙是否连接
void oled_show(void);
void Display_Time_Date(void);
char timeStr[32];
void Display_DS3231_Time(void);
void parse_and_set_time(char *str);
int main(void)
{
Timer_Init();
SysTick_Init(); /* SysTick 10ms中断初始化 */
MyI2C1_Init();
DS3231_Init(); //DS3231初始化
LED_GPIO_Config();
Serial_Init();
OLED_Init();
Key_GPIO_Config(); /* 按键端口初始化 */
Hc05_USART_Config();
CPU_TS_TmrInit(); /* 延时函数初始化 */
// ILI9341_Init (); //屏幕初始化
// Set_DS3231_Time(25,4,2,12,00,00,3);//设置时间为2024年10月29日15时00分00秒星期2,下载一次之后,需要注释掉重新下载
// Get_DS3231_Time(); //获取DS3231的时间
if(HC05_Init() ==0 )
{
HC05_INFO("HC05模块检测正常");
}
else
{
HC05_ERROR("HC05模块不正常,请重新测试");
while(1);
}
/*复位、恢复默认状态*/
HC05_Send_CMD("AT+RESET\r\n",1); //复位指令发送完成之后,需要一定时间HC05才会接受下一条指令
HC05_Send_CMD("AT+ORGL\r\n",1);
HC05_Send_CMD("AT+VERSION?\r\n",1);
HC05_Send_CMD("AT+ADDR?\r\n",1);
HC05_Send_CMD("AT+UART?\r\n",1);
HC05_Send_CMD("AT+CMODE?\r\n",1);
HC05_Send_CMD("AT+STATE?\r\n",1);
// HC05_Send_CMD("AT+ROLE=1\r\n",1);
HC05_Send_CMD("AT+PSWD=9632\r\n",1);
/*初始化SPP规范*/
HC05_Send_CMD("AT+INIT\r\n",1);
HC05_Send_CMD("AT+CLASS=0\r\n",1);
HC05_Send_CMD("AT+INQM=1,9,48\r\n",1);
/*设置模块名字*/
sprintf(hc05_nameCMD,"AT+NAME=%s\r\n",hc05_name);
HC05_Send_CMD(hc05_nameCMD,1);
HC05_INFO("本模块名字为:%s ,模块已准备就绪。",hc05_name);
while(1)
{
if(1 == hc05_inquery_connect)
{
hc05_inquery_connect = 0; //清零标志位
CheckConnect_LinkHC05_Test();
}
//连接后每个一段时间检查接受缓冲区
if(1 == hc05_check_recvbuff)
{
hc05_check_recvbuff = 0; // 清零标志位
CheckRecvBltBuff_Test();
}
if (USART_RX_FLAG)
{
USART_RX_FLAG = 0; // 清除标志位
// 判断是否为设置时间指令
if (USART_RX_BUF[0] == 'T')
{
int year, month, day, hour, min, sec;
int parsed = sscanf(USART_RX_BUF, "T%4d-%2d-%2d %2d:%2d:%2d",
&year, &month, &day, &hour, &min, &sec);
if (parsed == 6) // 确保成功解析了6个字段
{
Set_DS3231_Time(year % 100, month, day, hour, min, sec, 0); // 设置时间
HC05_INFO("SetTime_OK\r\n");
// HC05_SendString("SetTime_OK\r\n");
HC05_INFO("已设置时间为:\r\n");
// HC05_INFO(USART_RX_BUF);
HC05_SendString(USART_RX_BUF);
Display_DS3231_Time();
Delay_ms(980); // 每秒刷新一次
}
else
{
HC05_SendString("SetTime_ERROR\r\n");
HC05_INFO("时间格式错误,未设置\r\n");
}
}
}
}
}
void Display_DS3231_Time(void)
{
Get_DS3231_Time(); // 更新 calendar 结构体
sprintf(timeStr, "%02d-%02d-%02d", calendar.w_year+2000, calendar.w_month, calendar.w_date);
OLED_ShowString(1, 3, timeStr);
sprintf(timeStr, "%02d:%02d:%02d", calendar.hour, calendar.min, calendar.sec);
OLED_ShowString(3, 3, timeStr);
}
四、实验现象
1.获取时间程序现象
2.蓝牙主机
蓝牙助手连接蓝牙主机,发送master之后,然后再断开蓝牙,或者按键KEY1,切换主从模式,上电默认为从模式。串口助手显示连接上从机之后,需要断开串口,获取时间的程序和串口助手所使用的串口为一个串口号,不能同时使用
3.从机接收时间现象
蓝牙从机显示接收到了来自主机时间,接收到时间同时也会设置到DS3231模块,后续也可以用OLED屏显示,上述程序暂时没有加入。