蓝牙模块使用
051.6.7 蓝牙模块使用
/*
* 整体目的:复用上一节UART程序,通过JDY-31蓝牙串口模块与手机通信并控制开发板外设。
* 整体设计:主循环把测试文本经P3.1/TXD交给蓝牙模块发送到手机;手机发来的命令经模块TXD
* 进入P3.0/RXD,UART接收中断读取SBUF,再用switch控制LED、蜂鸣器和继电器。
* 接线依据:模块VCC按本课程模块要求接5V、GND接开发板GND,模块TXD接单片机RXD,
* 模块RXD接单片机TXD;收发线必须交叉,公共地为双方电平提供同一参考。
* 命令前提:手机蓝牙助手要选择HEX发送;发送01才能匹配case 0x1,文本字符'1'是0x31。
* 硬件依据:四个LED、蜂鸣器驱动和实验所用继电器模块均按低电平有效方式控制。
*/
#include "reg52.h" /* 引入8052特殊功能寄存器声明,使程序能使用P1、SCON、SBUF、RI等名称。 */
#include <intrins.h> /* 引入Keil C51内部函数声明,软件延时需要调用_nop_()。 */
typedef unsigned char uchar; /* 为8位无符号字符型建立简写,用于UART字节和接收变量。 */
typedef unsigned int uint; /* 为C51的16位无符号整型建立简写,用于循环和毫秒参数。 */
uint count = 0; /* 定义a~z发送循环使用的全局计数器,取值范围为0~25。 */
uchar recv; /* 定义8位全局接收变量,在中断中保存手机经蓝牙发来的一个命令字节。 */
sbit LED1 = P1^0; /* 将LED1绑定到P1.0;输出0时通过灌电流点亮第一只LED。 */
sbit LED2 = P1^1; /* 将LED2绑定到P1.1;输出0时点亮第二只LED。 */
sbit LED3 = P1^2; /* 将LED3绑定到P1.2;输出0时点亮第三只LED。 */
sbit LED4 = P1^3; /* 将LED4绑定到P1.3;输出0时点亮第四只LED。 */
sbit BEEP = P1^6; /* 将蜂鸣器控制端绑定到P1.6;输出0使PNP驱动管导通并发声。 */
sbit JDQ1 = P2^0; /* 将继电器模块IN绑定到P2.0;课程所用模块以低电平触发吸合。 */
/*
* 函数目的:执行由xms指定的毫秒级软件忙等待,降低主循环向手机重复发送文本的频率。
* 时间前提:计数值按11.0592MHz和当前Keil C51指令耗时生成;时钟或优化变化会影响精度。
*/
void Delay_xms(uint xms) /* 定义软件延时函数,xms表示计划等待的毫秒数。 */
{ /* 延时函数体开始,忙等期间CPU不能处理其他前台任务。 */
uchar data i, j; /* 定义两个8位内层计数器,并放入片内直接寻址data RAM。 */
while (xms) /* 每完成一轮约1ms等待,就把剩余毫秒数减1。 */
{ /* 一毫秒延时循环开始。 */
_nop_(); /* 执行空操作指令,用于补齐延时生成器计算的时间。 */
i = 2; /* 初始化外层8位计数器。 */
j = 199; /* 初始化内层计数器,其递减指令构成主要延时。 */
do /* 使用后判断循环,保证循环体至少执行一次。 */
{ /* do循环体开始,此处只消耗CPU时间。 */
while (--j); /* 先递减j,非0就继续空转;分号表示循环体为空。 */
} while (--i); /* 一组j循环结束后递减i,非0时继续下一组。 */
xms--; /* 一轮约1ms结束,将剩余等待次数减1。 */
} /* xms归零后退出延时循环。 */
} /* 延时函数结束并返回调用处。 */
/* 函数目的:用TI查询方式可靠发送一个字节,防止连续写SBUF造成数据丢失。 */
void UART_Send_Byte(uchar send_byte) /* send_byte保存本次要发送到蓝牙模块的8位数据。 */
{ /* 单字节发送流程开始。 */
SBUF = send_byte; /* 写发送SBUF,UART从P3.1/TXD启动一帧输出。 */
while (TI == 0); /* TI为0时原地等待;末尾分号表示空循环。 */
TI = 0; /* 硬件置位TI后由软件清0,为下一帧做准备。 */
} /* 当前字节已发送到规定时刻,函数返回。 */
/* 函数目的:遍历以'\0'结尾的C字符串,并复用单字节函数发送到手机。 */
void UART_Send_Str(uchar *send_str) /* send_str接收待发送字符串的首地址。 */
{ /* 字符串发送函数体开始。 */
while (*send_str != '\0') /* 当前字符不是NUL结束标志时继续循环。 */
{ /* 当前正文字符的发送分支开始。 */
UART_Send_Byte(*send_str++); /* 发送当前字符,再将指针移动到下一个字符。 */
} /* 遇到'\0'时退出,不把结束标志发送出去。 */
} /* 整个字符串发送完成并返回。 */
/* 函数目的:初始化UART和波特率时基,循环向手机发测试内容并等待蓝牙命令中断。 */
void main() /* 定义Keil C51程序入口。 */
{ /* 主函数体开始。 */
SCON = 0x50; /* 选择UART方式1、允许接收,并将TI和RI初始化为0。 */
PCON &= 0x7F; /* 清SMOD选择波特率不加倍,同时保留PCON其他位。 */
TMOD &= 0x0F; /* 清除定时器1高4位配置,保留定时器0低4位。 */
TMOD |= 0x20; /* 设置定时器1为内部定时、模式2自动重装。 */
TH1 = 0xFD; /* 保存自动重装值253,用于产生9600波特时基。 */
TL1 = 0xFD; /* 设置第一次计数初值,使首轮周期与后续一致。 */
TR1 = 1; /* 启动定时器1,开始给UART提供波特率时钟。 */
ES = 1; /* 打开串口中断分开关,使蓝牙接收完成的RI可请求中断。 */
EA = 1; /* 打开CPU总中断门控,使已允许的串口请求能被响应。 */
while (1) /* 无限循环,持续发送测试内容并接受中断打断。 */
{ /* 一轮发往手机的测试数据开始。 */
for (count = 0; count < 26; count++) /* count取0~25,依次产生26个小写字母。 */
{ /* 当前字母发送循环体开始。 */
UART_Send_Byte('a' + count); /* 生成'a'~'z'并经TXD、蓝牙模块逐字节发给手机。 */
} /* 26个字母发送完毕。 */
UART_Send_Byte('\n'); /* 发送LF换行符,使手机接收区换到下一行。 */
UART_Send_Str("i am wfeng!\r\n"); /* 发送测试字符串和CR+LF行结束序列。 */
Delay_xms(1000); /* 忙等待约1秒,降低整段测试文本的重复频率。 */
} /* 一轮结束后回到while顶部继续发送。 */
} /* 主函数语法边界;无限循环使其不会正常返回。 */
/*
* 函数目的:响应手机经蓝牙模块送来的串口字节,并映射为九种外设控制命令。
* 接收路径:手机蓝牙→JDY-31 TXD→P3.0/RXD→接收SBUF;一帧完成后硬件置RI。
* 执行原则:中断内只进行快速寄存器赋值,不加入延时、配对或其他耗时操作。
*/
void UART_Routine(void) interrupt 4 /* 声明串口中断服务函数,对应向量地址0x0023。 */
{ /* 中断处理开始,先判断本次是否包含接收完成事件。 */
if (1 == RI) /* RI为1说明UART已接收完手机发来的一个数据帧。 */
{ /* 接收命令处理分支开始。 */
RI = 0; /* 按配套程序顺序由软件清RI,为接收下一帧做准备。 */
recv = SBUF; /* 读取接收SBUF,把手机命令字节转存到recv。 */
switch (recv) /* 根据收到的HEX数值选择相应的外设动作。 */
{ /* switch分支表开始。 */
case 0x1: /* 手机发送HEX 01时进入LED1分支。 */
LED1 = 0; /* 拉低P1.0,点亮低电平有效的LED1。 */
break; /* 结束本分支,防止继续执行下一case。 */
case 0x2: /* 手机发送HEX 02时进入LED2分支。 */
LED2 = 0; /* 拉低P1.1,点亮LED2。 */
break; /* 完成本命令后退出switch。 */
case 0x3: /* 手机发送HEX 03时进入LED3分支。 */
LED3 = 0; /* 拉低P1.2,点亮LED3。 */
break; /* 完成本命令后退出switch。 */
case 0x4: /* 手机发送HEX 04时进入LED4分支。 */
LED4 = 0; /* 拉低P1.3,点亮LED4。 */
break; /* 完成本命令后退出switch。 */
case 0x5: /* 手机发送HEX 05时执行四灯全灭。 */
P1 |= 0x0F; /* 把P1低4位置1并保留高4位,使LED1~LED4全部熄灭。 */
break; /* 完成全灭动作后退出switch。 */
case 0x6: /* 手机发送HEX 06时执行蜂鸣器开启。 */
BEEP = 0; /* 拉低P1.6使PNP驱动管导通,蜂鸣器得电发声。 */
break; /* 完成蜂鸣器开启后退出switch。 */
case 0x7: /* 手机发送HEX 07时执行蜂鸣器关闭。 */
BEEP = 1; /* 拉高P1.6使PNP驱动管截止,蜂鸣器停止。 */
break; /* 完成蜂鸣器关闭后退出switch。 */
case 0x8: /* 手机发送HEX 08时执行继电器吸合。 */
JDQ1 = 0; /* P2.0向低电平触发模块输出有效信号,使继电器吸合。 */
break; /* 完成吸合命令后退出switch。 */
case 0x9: /* 手机发送HEX 09时执行继电器释放。 */
JDQ1 = 1; /* P2.0撤销低电平触发,使课程所用继电器模块释放。 */
break; /* 完成释放命令后退出switch。 */
default: /* 收到01~09之外的数据时进入默认分支。 */
break; /* 不改变外设状态,直接退出switch。 */
} /* 命令分支选择结束。 */
} /* 接收事件处理结束;RI为0时不会误读旧数据。 */
} /* 串口中断服务结束并返回先前程序。 */
一、蓝牙模块使用
本实验使用JDY-31 SPP蓝牙串口模块,把手机与STC89C52之间的无线通信转换成普通UART收发。单片机程序沿用串口接收实验,不需要因为传输介质从USB数据线变成蓝牙而改变SCON、SBUF、TI、RI等串口逻辑。
整体通信链路如下:
手机蓝牙助手
│ Bluetooth Classic SPP无线链路
▼
JDY-31蓝牙协议与串口转换
│ TTL电平UART:TXD、RXD
▼
STC89C52:P3.0/RXD、P3.1/TXD
│
├─ 接收命令01~09:控制LED、蜂鸣器、继电器
└─ 主动发送a~z和字符串:显示在手机蓝牙助手中
“透明传输”表示模块尽量保持用户数据字节不变:手机发出的0x01经过蓝牙链路后,从模块TXD输出为UART字节0x01;单片机发出的ASCII字符串也会被模块转发给手机。模块负责蓝牙协议和无线收发,单片机仍只面对UART字节流。
1.1 接线方式
视频使用的是带底板、排针和稳压电路的JDY-31模块,板背面标注Power: 3.6V~6V和LEVEL: 3.3V。基础接线如下:
| JDY-31底板引脚 | 连接位置 | 作用 |
|---|---|---|
VCC | 开发板+5V | 给模块底板供电 |
GND | 开发板GND | 电源回路和UART共同参考地 |
TXD | STC89C52的P3.0/RXD | 模块向单片机发送数据 |
RXD | STC89C52的P3.1/TXD | 模块接收单片机发送的数据 |
STATE | 基础实验可不接 | 指示蓝牙连接状态 |
EN | 基础实验按模块默认状态使用 | 模块使能或相关控制引脚,具体以底板说明为准 |
JDY-31底板 STC89C52开发板
VCC ───────────────────────────────→ +5V
GND ───────────────────────────────→ GND
TXD ───────────────────────────────→ P3.0/RXD
RXD ←─────────────────────────────── P3.1/TXD
数据方向:发送端TXD必须连接到对方接收端RXD
为什么TX和RX必须交叉连接
TXD是本设备的数据输出,RXD是本设备的数据输入。两个输出端接在一起会争用线路,两个输入端接在一起又没有任何设备驱动信号;正确关系只能是“一端发送→另一端接收”。
模块发送:JDY-31 TXD ─→ MCU RXD
模块接收:JDY-31 RXD ←─ MCU TXD
如果只需要手机控制单片机,可以只连接模块TXD→MCU RXD;但本实验还要让手机看到单片机主动发送的字符串,所以两个方向都要连接。
为什么必须共地
UART的高、低电平都是相对于GND测量的。没有公共地时,即使TXD导线已经连接,接收方也无法获得稳定的电压参考:
发送端输出电压 = TXD相对于发送端GND的电压
接收端判断电平 = RXD相对于接收端GND的电压
两端GND连接后,才建立共同的0V参考
供电电压与UART信号电平必须分开理解
视频中的JDY-31带底板模块应把VCC接5V,因为底板输入范围为3.6~6V,5V是推荐值。直接把这块底板接到3.3V,可能因板载稳压器压差而导致内部电压不足、启动不稳定。
但“VCC接5V”不等于“RXD/TXD也是5V逻辑”。该底板标注的UART逻辑电平是3.3V:
电源侧:VCC输入5V ─→ 底板稳压电路 ─→ 给JDY-31核心供电
信号侧:TXD/RXD仍按照约3.3V逻辑工作
还要区分两种产品形态:
| 产品形态 | 典型VCC范围 | 说明 |
|---|---|---|
| JDY-31裸SMD核心模块 | 1.8~3.6V,推荐3.3V | 不能直接把5V接到核心VCC |
| 视频中的带底板模块 | 3.6~6V,推荐5V | 底板带供电处理,VCC可接开发板5V |
STC89C52工作在5V时,P3.1/TXD输出高电平可能接近5V,而JDY-31的RXD属于3.3V逻辑输入。视频所用成品底板直接连接能够完成实验,但换用其他批次或裸模块时,不能默认RXD耐受5V;应核对底板是否带电平保护,必要时在MCU TXD→模块RXD方向增加电阻分压或电平转换器。
典型电阻分压可以这样理解:
MCU TXD 5V ── R上 ──┬──→ JDY-31 RXD约3.3V
│
R下
│
GND
V_RXD = 5V × R下 / (R上 + R下)
例如取R上=1kΩ、R下=2kΩ,理想输出约为:
$ V_{RXD}=5\times\frac{2}{1+2}\approx3.33V $
反方向的JDY-31 TXD→MCU RXD输出约3.3V,是否能被5V单片机稳定识别为高电平,要检查单片机输入高电平门限。当前开发板实验可以正常通信,但不能把这一结果无条件套用到其他芯片。
接线和上电前应再次确认模块背面丝印。外形相似的HC-05、HC-06、JDY-30、JDY-31以及裸模块可能具有不同供电范围和引脚顺序,不能只按模块颜色或排针数量判断。
1.2 实验演示
实验包含手机、JDY-31、STC89C52开发板、板载LED与蜂鸣器,以及外接继电器模块。代码仍使用上一讲的命令表:
| 手机以HEX发送 | 单片机收到的字节 | 执行动作 |
|---|---|---|
01 | 0x01 | 点亮LED1 |
02 | 0x02 | 点亮LED2 |
03 | 0x03 | 点亮LED3 |
04 | 0x04 | 点亮LED4 |
05 | 0x05 | 熄灭全部LED |
06 | 0x06 | 打开蜂鸣器 |
07 | 0x07 | 关闭蜂鸣器 |
08 | 0x08 | 继电器吸合 |
09 | 0x09 | 继电器释放 |
视频中主要演示了01、02、03、05、06、07、08、09,04同样由代码支持。这里控制的是蜂鸣器,不是风扇;继电器只是一个电控机械开关,是否接风扇或其他负载取决于继电器触点侧的实际接线。
手机还会收到单片机主循环主动发送的字母和字符串:
单片机UART发送 ─→ JDY-31 RXD ─→ 蓝牙无线链路 ─→ 手机APP显示
而手机按钮命令走反方向:
手机APP发送 ─→ 蓝牙无线链路 ─→ JDY-31 TXD ─→ 单片机RXD与串口中断
这说明通信是双向的。蓝牙模块并不理解“开灯”“蜂鸣器”这些业务含义,它只转发01~09等字节;真正把命令解释成硬件动作的是单片机中的switch(recv)。
自定义按钮
蓝牙助手通常允许长按按钮并配置按钮名称与发送内容。例如建立“全部关灯”按钮:
按钮名称:全部关灯
发送模式:HEX
发送内容:05
点击按钮后,APP发送一个字节0x05,而不是ASCII字符'5'对应的0x35。其他按钮可以依照命令表分别配置01~09。
如果希望APP使用普通文本发送数字,则代码中的分支应匹配case '1'到case '9',或者先把ASCII数字转换成数值;发送端格式与接收端协议必须一致。
1.3 思维导图
JDY-31属于Bluetooth Classic SPP串口透传模块。出厂常见名称为JDY-31-SPP,默认配对密码为1234,默认UART波特率为9600;名称、密码或波特率若已通过AT命令调整,应以模块当前配置为准。
Android获取与安装蓝牙助手
课程资料中提供了安卓蓝牙助手APK,也可以在可信应用商店搜索支持SPP和HEX收发的蓝牙串口工具。选择软件时重点确认:
| 功能 | 用途 |
|---|---|
| 支持Bluetooth Classic SPP | 能连接JDY-31,而不只是BLE设备 |
| 支持HEX发送 | 可直接发送01~09命令字节 |
| 支持接收显示 | 查看单片机返回的字符串 |
| 支持自定义按钮 | 把常用命令做成“开灯、关灯”等按钮 |
从课程资料安装APK时,可以通过USB、可信网盘或系统文件管理器把文件传入手机,再按Android提示临时允许对应来源安装未知应用。安装完成后应关闭该权限。聊天软件或特定浏览器只是文件转交方式,不是安装APK的必需条件;不要安装来源不明或被二次修改的APK。
iPhone兼容性说明
JDY-31使用经典蓝牙SPP。普通iOS应用不能像Android应用那样随意访问未经过相应认证的经典蓝牙串口配件,因此即使App Store中能搜到“蓝牙串口”软件,也不代表它一定能连接JDY-31。使用iPhone时应先确认软件明确支持该模块;如果需要通用iOS连接,通常应选择BLE串口模块并使用支持其BLE服务的应用。
SPP与BLE不能混淆
| 项目 | JDY-31 SPP | 常见BLE串口模块 |
|---|---|---|
| 蓝牙类型 | Bluetooth Classic | Bluetooth Low Energy |
| 数据抽象 | 串口配置文件SPP | GATT服务与特征值 |
| Android支持 | 通常较直接 | 需要BLE应用支持相应服务 |
| iOS通用应用支持 | 受经典蓝牙配件机制限制 | 通常可通过Core Bluetooth访问 |
手机能在系统设置中看到模块,只表示发现或配对层面可见,不等于任意串口App都能建立数据通道。
1.4 软件使用方式
配对和连接步骤
- 按接线表连接模块并给开发板上电,确认模块状态灯工作。
- 在Android手机的蓝牙设置中查找
JDY-31-SPP或实际广播名称。 - 发起配对,未改过配置时输入默认PIN码
1234。 - 打开支持SPP的蓝牙助手,选择已配对的JDY-31并建立连接。
- 设置与单片机一致的串口参数。本实验使用9600、8N1。
- 打开发送选项,勾选HEX发送;显示时间、显示发送内容可按调试需要开启。
- 发送
01~09并观察硬件动作,同时查看单片机主动发回的文本。
模块状态灯的具体闪烁方式会因底板版本而异,通常未连接时闪烁,建立蓝牙连接后节奏改变或常亮。应以模块实物说明为准,不要只凭LED颜色判断串口数据一定已经收发成功。
为什么不勾选HEX就没有动作
APP在文本模式中发送字符1时,线路上的字节是ASCII码0x31;代码判断的是数值0x01:
HEX模式输入01 ─→ 发送0x01 ─→ 匹配 case 0x1 ─→ LED1点亮
文本模式输入1 ─→ 发送0x31 ─→ 不匹配任何case ─→ default,无动作
所以必须把APP设置与代码协议对应起来。“显示HEX”和“发送HEX”也是两个不同选项:前者只改变接收区怎样显示,后者才决定实际发出的字节内容。
为什么蓝牙模块会影响串口烧录
STC89C52的程序下载也使用P3.0/RXD和P3.1/TXD。开发板上的CH340 USB转串口芯片已经连接到这组引脚,再外接JDY-31后,相当于两个串口设备同时挂在同一组线上:
┌← CH340 TXD(下载工具向MCU发送)
MCU P3.0/RXD ←─────────┤
└← JDY-31 TXD(蓝牙模块向MCU发送)
┌→ CH340 RXD(电脑接收MCU数据)
MCU P3.1/TXD ──────────┤
└→ JDY-31 RXD(蓝牙模块接收MCU数据)
一个TXD同时驱动两个RXD通常只是增加输入负载;真正危险的是两个TXD并到MCU的同一个RXD上。当CH340和JDY-31输出不同电平时,会发生信号争用,既可能破坏下载数据,也可能形成不必要的对拉电流。
CH340 TXD输出1 ─┐
├─ 同一节点发生冲突 ─→ MCU收到错误波形,下载失败
JDY-31 TXD输出0 ─┘
烧录失败或无法握手时,最稳妥的处理是断电后拔下蓝牙模块的VCC、TXD、RXD,至少应断开模块TXD→P3.0/RXD这条可能主动驱动的线路;烧录完成后再断电恢复接线。不要在开发板和模块通电时随意插拔,避免短路或接错引脚。
若要长期保留两套串口设备,可以增加跳帽、模拟开关或三态缓冲器,在“CH340下载”和“蓝牙通信”之间明确选择;也可以把蓝牙模块接到其他GPIO并使用软件串口,但51单片机的软件串口会占用CPU时间,对波特率和中断时序要求更高。
排查蓝牙通信时,可以按以下顺序检查:
模块供电是否正确
↓
TXD/RXD是否交叉、GND是否共地
↓
模块名称、PIN和SPP连接是否成功
↓
双方波特率是否都是9600、帧格式是否8N1
↓
APP是否真的启用了“发送HEX”
↓
发送01~09时,单片机RI是否触发、switch是否匹配
↓
下载失败时,断开蓝牙模块后重新烧录
掌握这条排查链后,就能把问题区分为供电、接线、电平、蓝牙连接、串口参数、数据格式和程序解析七个层次,而不是只反复点击“连接蓝牙”。