一、前言
1.1 技术背景
蓝牙技术作为短距离无线通信的主流方案,在物联网、智能家居、工业控制等领域应用广泛。HC-05是一款经典的蓝牙串口透传模块,支持蓝牙2.0+EDR协议,具有成本低、配置简单、稳定性好等特点,非常适合与STM32等嵌入式平台配合使用。
在嵌入式开发中,通过蓝牙实现设备与手机的无线通信,可以大大简化人机交互界面设计,无需复杂的屏幕和按键,仅通过手机APP即可完成参数配置、数据监控、远程控制等功能。
1.2 本文目标
通过本教程,你将学会:
- HC-05蓝牙模块的工作原理和配置方法
- STM32F103与HC-05的硬件连接设计
- 使用USART实现串口通信
- 编写手机APP进行蓝牙数据收发
- 实现完整的双向通信系统
适合读者:
- 有一定STM32基础的嵌入式开发者
- 希望学习无线通信的物联网爱好者
- 需要实现手机与单片机通信的项目开发者
1.3 技术栈
| 组件 | 型号/版本 | 说明 |
|---|---|---|
| 主控芯片 | STM32F103C8T6 | ARM Cortex-M3内核 |
| 蓝牙模块 | HC-05 | 蓝牙2.0+EDR串口模块 |
| 开发环境 | Keil MDK 5 | 嵌入式开发IDE |
| 串口工具 | XCOM V2.0 | 串口调试助手 |
| 手机APP | 蓝牙串口助手 | 安卓蓝牙调试工具 |
二、环境准备
2.1 硬件准备
核心硬件清单:
- STM32F103C8T6最小系统板 × 1
- HC-05蓝牙模块 × 1
- USB转TTL模块(CH340/CP2102)× 1
- 杜邦线若干
- 安卓手机 × 1
硬件连接示意图:
STM32F103C8T6 HC-05蓝牙模块
┌─────────────┐ ┌─────────────┐
│ │ │ │
│ PA9 ────┼────────┼────> RX │
│ PA10 <───┼────────┼──── TX │
│ 3.3V ────┼────────┼──── VCC │
│ GND ────┼────────┼──── GND │
│ │ │ │
└─────────────┘ └─────────────┘
引脚定义:
- PA9 (USART1_TX) → HC-05 RX
- PA10 (USART1_RX) → HC-05 TX
- 3.3V → HC-05 VCC
- GND → HC-05 GND
⚠️ 注意: HC-05的RX引脚需要连接STM32的TX,TX连接RX,即交叉连接。
2.2 软件环境
Keil MDK 5 安装配置:
- 下载并安装 Keil MDK-ARM 5.38
- 安装 STM32F1xx 系列 Device Family Pack
- 配置 ST-Link 调试器驱动
串口调试工具:
- 推荐:XCOM V2.0、SSCOM、Putty
- 波特率设置:9600(HC-05默认)
手机APP:
- 蓝牙串口助手(各大应用商店可下载)
- 或自行开发APP(可选)
三、HC-05模块详解
3.1 HC-05模块介绍
HC-05是一款主从一体蓝牙串口模块,主要特性:
- 蓝牙协议:Bluetooth V2.0+EDR
- 工作频率:2.4GHz ISM频段
- 通信距离:10米(空旷环境)
- 串口波特率:4800~1382400bps(默认9600)
- 工作电压:3.3V~5V(推荐3.3V)
- 配对密码:1234(默认)
引脚定义:
| 引脚 | 名称 | 功能说明 |
|---|---|---|
| 1 | VCC | 电源正极(3.3V~5V) |
| 2 | GND | 电源地 |
| 3 | TXD | 串口发送(TTL电平) |
| 4 | RXD | 串口接收(TTL电平) |
| 5 | STATE | 连接状态指示(未连接低电平,连接高电平) |
| 6 | EN | 使能引脚(高电平进入AT模式) |
3.2 工作模式
HC-05有两种工作模式:
1. 通信模式(默认)
- 模块上电后自动进入
- LED快速闪烁(约2Hz)
- 等待蓝牙设备连接
- 连接成功后LED常亮
2. AT命令模式
- 用于配置模块参数
- 上电前将EN引脚接高电平
- LED慢速闪烁(约1Hz)
- 波特率固定为38400
3.3 常用AT指令
进入AT模式后,可通过串口发送以下指令:
| 指令 | 功能 | 返回 |
|---|---|---|
| AT | 测试通信 | OK |
| AT+NAME=XXX | 设置蓝牙名称 | OK |
| AT+PIN=1234 | 设置配对密码 | OK |
| AT+BAUD=4 | 设置波特率9600 | OK9600 |
| AT+ROLE=0 | 设置为从机模式 | OK |
| AT+ADDR? | 查询模块地址 | +ADDR:xxxx |
波特率设置对照表:
| 参数 | 波特率 |
|---|---|
| 1 | 1200 |
| 2 | 2400 |
| 3 | 4800 |
| 4 | 9600 |
| 5 | 19200 |
| 6 | 38400 |
| 7 | 57600 |
| 8 | 115200 |
四、STM32程序设计
4.1 工程创建与配置
在Keil MDK中创建新工程:
- 选择器件:STM32F103C8
- 添加启动文件:startup_stm32f10x_md.s
- 配置CMSIS和StdPeriph库
时钟配置:
- 系统时钟:72MHz(使用外部8MHz晶振)
- APB2总线时钟:72MHz
- USART1时钟:72MHz
4.2 USART1初始化
📄 创建文件:
usart1.c
/**
* @file usart1.c
* @brief USART1驱动程序 - 用于与HC-05蓝牙模块通信
*
* 功能:
* - 初始化USART1,配置波特率9600
* - 实现数据发送和接收
* - 支持中断接收模式
* - 提供环形缓冲区管理
*/
#include "usart1.h"
#include "stm32f10x.h"
#include <string.h>
#include <stdio.h>
/* 环形缓冲区大小 */
#define RX_BUFFER_SIZE 256
#define TX_BUFFER_SIZE 256
/* 接收缓冲区 */
static volatile uint8_t rx_buffer[RX_BUFFER_SIZE];
static volatile uint16_t rx_write_index = 0;
static volatile uint16_t rx_read_index = 0;
/* 发送缓冲区 */
static volatile uint8_t tx_buffer[TX_BUFFER_SIZE];
static volatile uint16_t tx_write_index = 0;
static volatile uint16_t tx_read_index = 0;
/* 接收完成标志 */
static volatile uint8_t rx_complete_flag = 0;
/**
* @brief 初始化USART1
*
* 配置参数:
* - 波特率:9600 bps(与HC-05默认波特率匹配)
* - 数据位:8位
* - 停止位:1位
* - 校验位:无
* - 流控制:无
* - 工作模式:全双工
*/
void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
/* 1. 使能时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
/* 2. 配置GPIO引脚 */
// PA9 - USART1_TX (复用推挽输出)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10 - USART1_RX (浮空输入)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
/* 3. 配置USART参数 */
USART_InitStruct.USART_BaudRate = 9600; // 波特率9600
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 8位数据
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 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(USART1, &USART_InitStruct);
/* 4. 配置中断 */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断
USART_ITConfig(USART1, USART_IT_TXE, ENABLE); // 使能发送中断
/* 5. 配置NVIC */
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
/* 6. 使能USART */
USART_Cmd(USART1, ENABLE);
/* 7. 初始化缓冲区 */
rx_write_index = 0;
rx_read_index = 0;
tx_write_index = 0;
tx_read_index = 0;
rx_complete_flag = 0;
}
/**
* @brief 发送单个字符
* @param ch 要发送的字符
*/
void USART1_SendChar(uint8_t ch)
{
// 等待发送缓冲区为空
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, ch);
}
/**
* @brief 发送字符串
* @param str 要发送的字符串
*/
void USART1_SendString(const char *str)
{
while (*str) {
USART1_SendChar(*str++);
}
}
/**
* @brief 发送数据缓冲区
* @param data 数据指针
* @param len 数据长度
*/
void USART1_SendData(const uint8_t *data, uint16_t len)
{
for (uint16_t i = 0; i < len; i++) {
USART1_SendChar(data[i]);
}
}
/**
* @brief 检查接收缓冲区是否有数据
* @return 可读数据长度
*/
uint16_t USART1_GetRxDataLength(void)
{
if (rx_write_index >= rx_read_index) {
return rx_write_index - rx_read_index;
} else {
return RX_BUFFER_SIZE - rx_read_index + rx_write_index;
}
}
/**
* @brief 从接收缓冲区读取数据
* @param data 数据存储缓冲区
* @param len 要读取的最大长度
* @return 实际读取的字节数
*/
uint16_t USART1_ReadData(uint8_t *data, uint16_t len)
{
uint16_t available = USART1_GetRxDataLength();
uint16_t to_read = (len < available) ? len : available;
for (uint16_t i = 0; i < to_read; i++) {
data[i] = rx_buffer[rx_read_index];
rx_read_index = (rx_read_index + 1) % RX_BUFFER_SIZE;
}
return to_read;
}
/**
* @brief 读取一行数据(以\n或\r\n结尾)
* @param line 行缓冲区
* @param max_len 最大长度
* @return 实际读取长度,0表示无完整行
*/
uint16_t USART1_ReadLine(char *line, uint16_t max_len)
{
uint16_t available = USART1_GetRxDataLength();
uint16_t i = 0;
uint16_t temp_read_index = rx_read_index;
// 查找换行符
for (i = 0; i < available && i < max_len - 1; i++) {
char ch = rx_buffer[temp_read_index];
temp_read_index = (temp_read_index + 1) % RX_BUFFER_SIZE;
if (ch == '\n') {
// 找到换行符,读取整行
uint16_t line_len = i + 1;
USART1_ReadData((uint8_t *)line, line_len);
line[line_len] = '\0';
// 处理\r\n情况
if (line_len > 1 && line[line_len - 2] == '\r') {
line[line_len - 2] = '\0';
} else {
line[line_len - 1] = '\0';
}
return strlen(line);
}
}
return 0; // 未找到完整行
}
/**
* @brief 清空接收缓冲区
*/
void USART1_ClearRxBuffer(void)
{
rx_write_index = 0;
rx_read_index = 0;
}
/**
* @brief USART1中断服务程序
*
* 处理接收和发送中断:
* - 接收中断:将数据存入环形缓冲区
* - 发送中断:从发送缓冲区取出数据发送
*/
void USART1_IRQHandler(void)
{
// 接收中断处理
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
uint8_t data = USART_ReceiveData(USART1);
// 存入环形缓冲区
uint16_t next_index = (rx_write_index + 1) % RX_BUFFER_SIZE;
if (next_index != rx_read_index) { // 缓冲区未满
rx_buffer[rx_write_index] = data;
rx_write_index = next_index;
}
// 检查是否为命令结束符
if (data == '\n' || data == '\r') {
rx_complete_flag = 1;
}
}
// 发送中断处理(可选,用于中断发送模式)
if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET) {
if (tx_read_index != tx_write_index) {
USART_SendData(USART1, tx_buffer[tx_read_index]);
tx_read_index = (tx_read_index + 1) % TX_BUFFER_SIZE;
} else {
USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
}
}
}
/**
* @brief 重定向printf到USART1
* @note 需要在编译选项中勾选"Use MicroLIB"
*/
int fputc(int ch, FILE *f)
{
USART1_SendChar((uint8_t)ch);
return ch;
}
📄 创建文件:
usart1.h
/**
* @file usart1.h
* @brief USART1驱动头文件
*/
#ifndef __USART1_H
#define __USART1_H
#include <stdint.h>
/* 函数声明 */
void USART1_Init(void);
void USART1_SendChar(uint8_t ch);
void USART1_SendString(const char *str);
void USART1_SendData(const uint8_t *data, uint16_t len);
uint16_t USART1_GetRxDataLength(void);
uint16_t USART1_ReadData(uint8_t *data, uint16_t len);
uint16_t USART1_ReadLine(char *line, uint16_t max_len);
void USART1_ClearRxBuffer(void);
#endif /* __USART1_H */
4.3 蓝牙通信协议设计
设计简单的文本协议,便于手机APP解析:
命令格式:
命令格式:<命令字>:<参数>\r\n
示例:
LED:ON\r\n - 打开LED
LED:OFF\r\n - 关闭LED
PWM:50\r\n - 设置PWM占空比50%
READ:ADC\r\n - 读取ADC值
响应格式:
响应格式:<状态>:<数据>\r\n
示例:
OK:LED_ON\r\n - LED已打开
OK:PWM_SET_50\r\n - PWM设置成功
DATA:ADC_2048\r\n - ADC值为2048
ERR:UNKNOWN_CMD\r\n - 未知命令
📄 创建文件:
bluetooth_protocol.c
/**
* @file bluetooth_protocol.c
* @brief 蓝牙通信协议处理
*
* 实现命令解析和响应生成
*/
#include "bluetooth_protocol.h"
#include "usart1.h"
#include "led_control.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
/* 命令缓冲区 */
static char cmd_buffer[CMD_BUFFER_SIZE];
/**
* @brief 初始化蓝牙协议处理
*/
void BT_Protocol_Init(void)
{
memset(cmd_buffer, 0, sizeof(cmd_buffer));
}
/**
* @brief 解析并执行命令
* @param cmd 命令字符串
* @return 执行结果
*/
static BT_Status ExecuteCommand(const char *cmd)
{
char cmd_type[16] = {0};
char cmd_param[32] = {0};
// 解析命令格式:TYPE:PARAM
const char *delimiter = strchr(cmd, ':');
if (delimiter == NULL) {
return BT_ERR_INVALID_FORMAT;
}
// 提取命令类型
size_t type_len = delimiter - cmd;
if (type_len >= sizeof(cmd_type)) {
return BT_ERR_INVALID_FORMAT;
}
strncpy(cmd_type, cmd, type_len);
cmd_type[type_len] = '\0';
// 提取参数
strncpy(cmd_param, delimiter + 1, sizeof(cmd_param) - 1);
// 执行对应命令
if (strcasecmp(cmd_type, "LED") == 0) {
return HandleLedCommand(cmd_param);
}
else if (strcasecmp(cmd_type, "PWM") == 0) {
return HandlePwmCommand(cmd_param);
}
else if (strcasecmp(cmd_type, "READ") == 0) {
return HandleReadCommand(cmd_param);
}
else if (strcasecmp(cmd_type, "ECHO") == 0) {
return HandleEchoCommand(cmd_param);
}
else {
return BT_ERR_UNKNOWN_CMD;
}
}
/**
* @brief 处理LED命令
*/
static BT_Status HandleLedCommand(const char *param)
{
if (strcasecmp(param, "ON") == 0) {
LED_On();
BT_SendResponse(BT_OK, "LED_ON");
return BT_OK;
}
else if (strcasecmp(param, "OFF") == 0) {
LED_Off();
BT_SendResponse(BT_OK, "LED_OFF");
return BT_OK;
}
else if (strcasecmp(param, "TOGGLE") == 0) {
LED_Toggle();
BT_SendResponse(BT_OK, "LED_TOGGLE");
return BT_OK;
}
else if (strncasecmp(param, "BLINK", 5) == 0) {
// 解析闪烁频率 LED:BLINK_500
int period = atoi(param + 6);
if (period > 0) {
LED_SetBlink(period);
char resp[32];
snprintf(resp, sizeof(resp), "LED_BLINK_%d", period);
BT_SendResponse(BT_OK, resp);
return BT_OK;
}
}
return BT_ERR_INVALID_PARAM;
}
/**
* @brief 处理PWM命令
*/
static BT_Status HandlePwmCommand(const char *param)
{
int duty = atoi(param);
if (duty < 0 || duty > 100) {
return BT_ERR_INVALID_PARAM;
}
PWM_SetDutyCycle(duty);
char resp[32];
snprintf(resp, sizeof(resp), "PWM_SET_%d", duty);
BT_SendResponse(BT_OK, resp);
return BT_OK;
}
/**
* @brief 处理读取命令
*/
static BT_Status HandleReadCommand(const char *param)
{
if (strcasecmp(param, "ADC") == 0) {
uint16_t adc_value = ADC_ReadChannel(0);
char resp[32];
snprintf(resp, sizeof(resp), "ADC_%d", adc_value);
BT_SendResponse(BT_DATA, resp);
return BT_OK;
}
else if (strcasecmp(param, "TEMP") == 0) {
float temperature = ReadTemperature();
char resp[32];
snprintf(resp, sizeof(resp), "TEMP_%.1f", temperature);
BT_SendResponse(BT_DATA, resp);
return BT_OK;
}
else if (strcasecmp(param, "STATUS") == 0) {
char resp[64];
snprintf(resp, sizeof(resp), "LED_%s_PWM_%d",
LED_GetState() ? "ON" : "OFF",
PWM_GetDutyCycle());
BT_SendResponse(BT_DATA, resp);
return BT_OK;
}
return BT_ERR_INVALID_PARAM;
}
/**
* @brief 处理回显命令(测试用)
*/
static BT_Status HandleEchoCommand(const char *param)
{
BT_SendResponse(BT_OK, param);
return BT_OK;
}
/**
* @brief 发送响应
* @param status 状态码
* @param data 数据内容
*/
void BT_SendResponse(BT_Status status, const char *data)
{
char response[128];
const char *status_str;
switch (status) {
case BT_OK:
status_str = "OK";
break;
case BT_DATA:
status_str = "DATA";
break;
case BT_ERR_UNKNOWN_CMD:
status_str = "ERR:UNKNOWN_CMD";
break;
case BT_ERR_INVALID_PARAM:
status_str = "ERR:INVALID_PARAM";
break;
case BT_ERR_INVALID_FORMAT:
status_str = "ERR:INVALID_FORMAT";
break;
default:
status_str = "ERR:UNKNOWN";
break;
}
if (data != NULL && strlen(data) > 0) {
snprintf(response, sizeof(response), "%s:%s\r\n", status_str, data);
} else {
snprintf(response, sizeof(response), "%s\r\n", status_str);
}
USART1_SendString(response);
}
/**
* @brief 协议处理主循环
* @note 在主循环中调用
*/
void BT_Protocol_Process(void)
{
char line[CMD_BUFFER_SIZE];
// 读取完整命令行
if (USART1_ReadLine(line, sizeof(line)) > 0) {
// 执行命令
BT_Status status = ExecuteCommand(line);
// 如果执行失败,发送错误响应
if (status != BT_OK && status != BT_DATA) {
BT_SendResponse(status, NULL);
}
}
}
📄 创建文件:
bluetooth_protocol.h
/**
* @file bluetooth_protocol.h
* @brief 蓝牙通信协议头文件
*/
#ifndef __BLUETOOTH_PROTOCOL_H
#define __BLUETOOTH_PROTOCOL_H
#include <stdint.h>
/* 缓冲区大小 */
#define CMD_BUFFER_SIZE 128
/* 状态码 */
typedef enum {
BT_OK = 0, // 执行成功
BT_DATA, // 数据响应
BT_ERR_UNKNOWN_CMD, // 未知命令
BT_ERR_INVALID_PARAM, // 参数错误
BT_ERR_INVALID_FORMAT // 格式错误
} BT_Status;
/* 函数声明 */
void BT_Protocol_Init(void);
void BT_Protocol_Process(void);
void BT_SendResponse(BT_Status status, const char *data);
/* 命令处理函数(内部使用) */
static BT_Status ExecuteCommand(const char *cmd);
static BT_Status HandleLedCommand(const char *param);
static BT_Status HandlePwmCommand(const char *param);
static BT_Status HandleReadCommand(const char *param);
static BT_Status HandleEchoCommand(const char *param);
#endif /* __BLUETOOTH_PROTOCOL_H */
4.4 LED和PWM控制
📄 创建文件:
led_control.c
/**
* @file led_control.c
* @brief LED和PWM控制驱动
*/
#include "led_control.h"
#include "stm32f10x.h"
#include "stm32f10x_tim.h"
static volatile uint8_t led_state = 0;
static volatile uint16_t pwm_duty = 0;
static volatile uint16_t blink_period = 0;
static volatile uint32_t blink_counter = 0;
/**
* @brief 初始化LED控制
* 使用PA0控制LED,TIM2_CH1输出PWM
*/
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;
/* 使能时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
/* 配置PA0为复用推挽输出(TIM2_CH1) */
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
/* 配置TIM2 */
// 72MHz / 72 / 1000 = 1kHz PWM频率
TIM_TimeBaseStruct.TIM_Period = 999; // 自动重装载值
TIM_TimeBaseStruct.TIM_Prescaler = 71; // 预分频器
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);
/* 配置PWM模式 */
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 0; // 初始占空比0
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStruct);
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM2, ENABLE);
/* 启动TIM2 */
TIM_Cmd(TIM2, ENABLE);
led_state = 0;
pwm_duty = 0;
blink_period = 0;
}
void LED_On(void)
{
blink_period = 0; // 停止闪烁
PWM_SetDutyCycle(100);
led_state = 1;
}
void LED_Off(void)
{
blink_period = 0; // 停止闪烁
PWM_SetDutyCycle(0);
led_state = 0;
}
void LED_Toggle(void)
{
if (led_state) {
LED_Off();
} else {
LED_On();
}
}
void LED_SetBlink(uint16_t period_ms)
{
blink_period = period_ms;
blink_counter = 0;
}
void PWM_SetDutyCycle(uint16_t duty)
{
if (duty > 100) duty = 100;
pwm_duty = duty;
uint16_t pulse = (duty * 1000) / 100; // 转换为TIM脉冲值
TIM_SetCompare1(TIM2, pulse);
led_state = (duty > 0) ? 1 : 0;
}
uint16_t PWM_GetDutyCycle(void)
{
return pwm_duty;
}
uint8_t LED_GetState(void)
{
return led_state;
}
/**
* @brief LED闪烁处理(在1ms定时器中调用)
*/
void LED_Blink_Handler(void)
{
if (blink_period == 0) return;
blink_counter++;
if (blink_counter >= blink_period) {
blink_counter = 0;
LED_Toggle();
}
}
📄 创建文件:
led_control.h
/**
* @file led_control.h
* @brief LED控制头文件
*/
#ifndef __LED_CONTROL_H
#define __LED_CONTROL_H
#include <stdint.h>
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);
void LED_SetBlink(uint16_t period_ms);
void LED_Blink_Handler(void);
void PWM_SetDutyCycle(uint16_t duty);
uint16_t PWM_GetDutyCycle(void);
uint8_t LED_GetState(void);
#endif /* __LED_CONTROL_H */
4.5 主程序
📄 创建文件:
main.c
/**
* @file main.c
* @brief 蓝牙通信主程序
*
* 功能:
* - 初始化系统时钟和外设
* - 建立蓝牙通信连接
* - 处理蓝牙命令
* - 支持LED控制、PWM调节、数据读取
*/
#include "stm32f10x.h"
#include "usart1.h"
#include "bluetooth_protocol.h"
#include "led_control.h"
#include <stdio.h>
/* 系统滴答定时器计数 */
static volatile uint32_t sys_tick_count = 0;
/**
* @brief 系统时钟配置
* 配置为72MHz,使用外部8MHz晶振
*/
void SystemClock_Config(void)
{
// 已在启动文件中配置,此处可添加额外配置
}
/**
* @brief 滴答定时器初始化
* 用于产生1ms时基
*/
void SysTick_Init(void)
{
// 配置SysTick为1ms中断
// 72MHz / 8 = 9MHz,9000个计数 = 1ms
SysTick_Config(SystemCoreClock / 1000);
}
/**
* @brief 滴答定时器中断服务程序
*/
void SysTick_Handler(void)
{
sys_tick_count++;
// 每1ms调用LED闪烁处理
LED_Blink_Handler();
}
/**
* @brief 获取系统运行时间(毫秒)
*/
uint32_t GetSysTick(void)
{
return sys_tick_count;
}
/**
* @brief 延时函数(毫秒)
*/
void Delay_ms(uint32_t ms)
{
uint32_t start = GetSysTick();
while ((GetSysTick() - start) < ms);
}
/**
* @brief 主函数
*/
int main(void)
{
/* 初始化系统 */
SystemClock_Config();
SysTick_Init();
/* 初始化外设 */
LED_Init();
USART1_Init();
BT_Protocol_Init();
/* 发送启动消息 */
Delay_ms(100); // 等待HC-05启动
USART1_SendString("\r\n");
USART1_SendString("================================\r\n");
USART1_SendString(" STM32 Bluetooth Control System\r\n");
USART1_SendString(" Version 1.0\r\n");
USART1_SendString("================================\r\n");
USART1_SendString("Ready for connection...\r\n");
/* 主循环 */
while (1) {
/* 处理蓝牙协议 */
BT_Protocol_Process();
/* 可添加其他后台任务 */
// 例如:传感器数据采集、状态上报等
}
}
五、手机APP配置
5.1 推荐APP
安卓平台:
- 蓝牙串口助手(推荐)
- 蓝牙调试器
- Serial Bluetooth Terminal
iOS平台:
- Bluetooth Serial
- LightBlue
5.2 连接步骤
-
开启手机蓝牙
- 进入手机设置 → 蓝牙
- 打开蓝牙开关
-
搜索设备
- 在可用设备列表中找到HC-05
- 默认名称通常为"HC-05"或自定义名称
-
配对连接
- 点击HC-05进行配对
- 输入配对密码:1234(默认)
- 等待连接成功
-
打开串口助手APP
- 选择已配对的HC-05设备
- 设置波特率:9600
- 点击连接
5.3 测试命令
连接成功后,在APP中发送以下命令测试:
测试命令:
ECHO:Hello\r\n → 应收到:OK:Hello
LED:ON\r\n → 应收到:OK:LED_ON,LED灯亮
LED:OFF\r\n → 应收到:OK:LED_OFF,LED灯灭
LED:TOGGLE\r\n → 应收到:OK:LED_TOGGLE,LED状态翻转
PWM:50\r\n → 应收到:OK:PWM_SET_50,LED亮度50%
READ:STATUS\r\n → 应收到:DATA:LED_X_PWM_X
六、系统测试
6.1 硬件测试
测试步骤:
-
电源测试
- 使用万用表测量3.3V和GND之间电压
- 正常范围:3.2V~3.4V
-
串口通信测试
- 使用USB转TTL连接HC-05
- 进入AT模式,发送AT指令测试
- 应返回OK
-
蓝牙连接测试
- 手机搜索并配对HC-05
- 观察HC-05状态LED
- 未连接:快速闪烁
- 已连接:常亮
6.2 软件测试
功能测试清单:
| 测试项 | 测试方法 | 预期结果 |
|---|---|---|
| 串口初始化 | 上电观察 | 无错误,正常启动 |
| 命令解析 | 发送ECHO:test | 返回OK:test |
| LED控制 | 发送LED:ON/OFF | LED相应亮灭 |
| PWM调节 | 发送PWM:0~100 | LED亮度渐变 |
| 状态读取 | 发送READ:STATUS | 返回当前状态 |
| 错误处理 | 发送无效命令 | 返回错误信息 |
6.3 调试技巧
使用串口助手调试:
- 连接USB转TTL到STM32的USART1
- 打开XCOM串口助手
- 设置波特率9600,数据位8,停止位1
- 观察收发数据
常见问题排查:
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 无响应 | 波特率不匹配 | 检查HC-05波特率设置 |
| 乱码 | 接线错误 | 检查TX/RX是否交叉连接 |
| 无法配对 | 密码错误 | 使用默认密码1234 |
| 连接不稳定 | 电源不足 | 确保供电稳定,增加滤波电容 |
七、故障排查与问题解决
7.1 通信故障
问题1:手机无法搜索到HC-05
现象:
- 手机蓝牙搜索不到设备
- HC-05 LED正常闪烁
原因分析:
- HC-05未进入可发现模式
- 蓝牙名称被修改
- 模块故障
解决方案:
-
检查模块状态
- 确认HC-05上电正常(LED闪烁)
- 测量VCC电压是否在3.3V~5V范围
-
恢复默认设置
进入AT模式: 1. 将EN引脚接高电平 2. 重新上电 3. 发送:AT+ORGL\r\n (恢复出厂设置) 4. 发送:AT+NAME=HC-05\r\n (设置名称) 5. 发送:AT+PIN=1234\r\n (设置密码) -
验证修复
- 重新上电进入通信模式
- 手机再次搜索
问题2:连接后无数据传输
现象:
- 手机显示已连接
- 发送命令无响应
原因分析:
- 波特率不匹配
- 接线错误
- STM32程序未运行
解决方案:
-
检查波特率设置
// 确认USART1初始化波特率 USART_InitStruct.USART_BaudRate = 9600; // 必须与HC-05一致 -
验证接线
STM32 PA9(TX) → HC-05 RX STM32 PA10(RX) → HC-05 TX -
使用串口助手测试
- 连接USB转TTL到HC-05
- 手机连接HC-05
- 在手机和电脑串口助手之间发送数据
- 验证HC-05透传功能正常
7.2 程序故障
问题3:程序卡死或异常复位
现象:
- 程序运行一段时间后停止
- 看门狗复位
原因分析:
- 中断处理不当
- 缓冲区溢出
- 堆栈溢出
解决方案:
-
优化中断处理
// 中断服务程序应尽量简短 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE)) { uint8_t data = USART_ReceiveData(USART1); // 仅做数据存储,不处理逻辑 if ((rx_write_index + 1) % RX_BUFFER_SIZE != rx_read_index) { rx_buffer[rx_write_index] = data; rx_write_index = (rx_write_index + 1) % RX_BUFFER_SIZE; } } } -
添加看门狗保护
// 初始化独立看门狗 void IWDG_Init(void) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_64); // 64分频 IWDG_SetReload(625); // 约1秒超时 IWDG_ReloadCounter(); IWDG_Enable(); } // 主循环中喂狗 while (1) { BT_Protocol_Process(); IWDG_ReloadCounter(); // 喂狗 }
7.3 性能优化
优化1:提高通信稳定性
// 添加数据校验
void BT_SendPacket(const uint8_t *data, uint16_t len)
{
uint8_t checksum = 0;
// 发送帧头
USART1_SendChar(0xAA);
USART1_SendChar(0x55);
// 发送长度
USART1_SendChar((len >> 8) & 0xFF);
USART1_SendChar(len & 0xFF);
// 发送数据并计算校验
for (uint16_t i = 0; i < len; i++) {
USART1_SendChar(data[i]);
checksum ^= data[i];
}
// 发送校验
USART1_SendChar(checksum);
}
优化2:降低功耗
// 使用低功耗模式
void EnterSleepMode(void)
{
// 等待当前传输完成
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
// 进入睡眠模式
__WFI();
}
// 使用中断唤醒
void USART1_IRQHandler(void)
{
// 接收中断唤醒CPU
if (USART_GetITStatus(USART1, USART_IT_RXNE)) {
// 处理数据...
}
}
八、扩展应用
8.1 添加传感器数据采集
// 读取温度传感器
float ReadTemperature(void)
{
// 假设使用DS18B20
// 实现温度读取代码
return temperature;
}
// 在命令处理中添加
if (strcasecmp(param, "TEMP") == 0) {
float temp = ReadTemperature();
char resp[32];
snprintf(resp, sizeof(resp), "TEMP_%.1f", temp);
BT_SendResponse(BT_DATA, resp);
return BT_OK;
}
8.2 实现数据上报
// 定时上报数据
void DataReport_Task(void)
{
static uint32_t last_report_time = 0;
uint32_t current_time = GetSysTick();
// 每5秒上报一次
if (current_time - last_report_time >= 5000) {
last_report_time = current_time;
char report[128];
snprintf(report, sizeof(report),
"REPORT:TIME_%lu,TEMP_%.1f,ADC_%d\r\n",
current_time / 1000,
ReadTemperature(),
ADC_ReadChannel(0));
USART1_SendString(report);
}
}
九、总结
9.1 核心知识点回顾
通过本教程,我们学习了:
-
HC-05蓝牙模块工作原理
- 主从一体工作模式
- AT指令配置方法
- 串口透传机制
-
STM32 USART通信
- USART初始化配置
- 中断接收和发送
- 环形缓冲区管理
-
蓝牙通信协议设计
- 文本协议格式
- 命令解析和响应
- 错误处理机制
-
手机APP交互
- 蓝牙设备配对
- 串口调试工具使用
- 双向数据传输
9.2 扩展学习方向
- 蓝牙BLE:学习低功耗蓝牙技术
- WiFi通信:ESP8266/ESP32模块
- 自定义APP开发:使用Android Studio开发专属APP
- 云平台接入:将数据上传至阿里云/腾讯云
9.3 学习资源
官方文档:
开源项目:
附录
A. 物料清单(BOM)
| 序号 | 名称 | 型号 | 数量 | 备注 |
|---|---|---|---|---|
| 1 | 单片机 | STM32F103C8T6 | 1 | 最小系统板 |
| 2 | 蓝牙模块 | HC-05 | 1 | 带底板 |
| 3 | USB转TTL | CH340 | 1 | 调试使用 |
| 4 | LED | 3mm红发 | 1 | 状态指示 |
| 5 | 电阻 | 1kΩ | 1 | LED限流 |
| 6 | 杜邦线 | 母对母 | 若干 | 连接线 |
1万+

被折叠的 条评论
为什么被折叠?



