51单片机蓝牙温度控制系统:从DS18B20到手机APP的实现
2026/9/12 3:45:12 网站建设 项目流程

简介:一套基于51单片机的温度控制与蓝牙控制系统全套资料,面向电子类学生、嵌入式初学者以及课程设计、电子竞赛人群。内容覆盖Proteus仿真、单片机源代码、硬件原理图、实物制作流程和配套手机APP,从项目搭建到整机调试形成完整闭环。系统具备环境温度实时检测与液晶显示功能,手机通过蓝牙模块连接后可同步查看温度数据;用户既能用板上按键,也能在APP端设置温度报警上、下限,断电后系统自动保存设置,重启无需重新配置。当温度超出上下限时,对应指示灯与继电器联动,可方便地外接散热风扇或加热片,适合作为恒温控制、智能家居类项目的参考方案。压缩包约46MB,已有221人学习下载;资料内含可直接编译的Keil源码、Proteus仿真工程、原理图及实物制作流程说明,手机APP源码也一并提供,便于二次开发与快速复用。

1. 51单片机温度控制蓝牙系统,为什么值得从头做一遍

课程设计和工程实训里,"基于51单片机的温度控制蓝牙系统"是出现频率最高的题目之一。它把 DS18B20 测温、51 单片机控制、HC-05 蓝牙透传、手机 APP、继电器驱动加热器串成一条完整的链路,做一遍等于把嵌入式产品从采集到控制的常见环节全走了一遍。

有人一上来就换 STM32 或 ESP32,但 51 在本题里依然最稳:Proteus 仿真资料最全、单总线时序好调、硬件成本几十元,而且用普通 I/O 口模拟单总线协议的方式,比直接调库更能讲清底层原理,面试时也更容易被认可。

下面按原理图、Keil C51 源代码、Proteus 仿真、手机 APP、实物制作流程的顺序推进,把每一段最容易翻车的地方单独讲透。这套内容适合正在做课设、想攒完整项目经验、或者准备从仿真跨到实物的读者,全程不依赖任何特定的开发板,焊万能板也能跑。

2. 51单片机硬件设计与原理图:DS18B20、HC-05 和继电器驱动怎么接

原理图决定后面代码能省多少事。这套系统的硬件可以拆成四块:测温电路、单片机最小系统、蓝牙接口电路、继电器驱动电路。只要这四块的电平关系和供电关系不出错,代码基本不用改硬件。

2.1 测温传感器选型对比与 DS18B20 单总线电路

选传感器是第一个决策点。市面上 51 课程设计最常见的三种测温方案,差异非常明显:

对比项DS18B20LM35NTC 热敏电阻
输出信号数字单总线模拟电压阻值变化
是否需要 ADC不需要需要,51 要外扩 ADC需要 ADC 加查表
精度±0.5℃(-10~+85℃)典型 ±0.5℃1%~5%,依赖标定
测温范围-55~+125℃-55~+150℃-40~+150℃
接线复杂度3 根,可寄生供电3 根2 根

结论很直接:LM35 和 NTC 都要额外处理模拟量,而 AT89C52、STC89C52RC 这类 51 单片机没有内置 ADC,外扩 ADC0809 会让原理图和代码都多出一大块。DS18B20 是数字输出,51 只靠一根普通 I/O 口线就能模拟时序读温度,课程设计里选它几乎不需要犹豫。

DS18B20 的典型接法:VCC 接 5V(数据手册允许 3.0~5.5V),GND 接地,DQ 数据脚接 51 的 P3.7,并在 DQ 与 VCC 之间接一个 4.7kΩ 上拉电阻。上拉电阻的取值不是随便填的:单总线空闲时靠上拉保持高电平,从机应答和写 0 时要把总线拉低,4.7kΩ 是手册推荐值,换成 10kΩ 会让边沿变缓,远距离或快速连续读写时容易出错;换成 1kΩ 则拉低瞬间灌电流偏大。

这里有一个实物上极其常见的坑:DS18B20 的 DQ 线虚焊或者忘了加上拉电阻,程序照样跑,但读回来的温度会固定停在 85.0℃。85℃ 不是室温,而是 DS18B20 上电后暂存器的默认值,说明主机根本没读到传感器数据。仿真阶段不会暴露这个问题,因为 Proteus 模型的总线状态理想化,所以画原理图时要把上拉电阻当成必画元件,而不是可选项。

// 用串口调试助手给 HC-05 发 AT 指令时的典型配置流程 AT+NAME=TempCtrl // 改模块名,注意 AT 指令必须回车结尾 AT+UART=9600,0,0 // 设波特率 9600,无校验,1 个停止位 AT+ROLE=0 // 设为从机模式,手机主动连它 AT+PSWD=1234 // 配对密码,默认就是 1234

这段 AT 指令在接实物前的模块配置阶段用得上。把 HC-05 的 KEY 引脚接高电平再上电,模块就进入 AT 指令模式,此时蓝牙不可见、只走串口,需要用 USB-TTL 转接板连到电脑来配置;配置完把 KEY 拉低重新上电,模块才回到透传模式。

2.2 HC-05 蓝牙模块与 51 单片机最小系统接线细节

HC-05 是最常用的蓝牙转串口模块,六针排针里真正必接的只有四根:VCC、GND、TXD、RXD,另外 STATE 可以做连接指示灯,KEY 用来切换 AT 模式。它与 51 的连接关系如下:

HC-05 引脚接到 51 哪里说明
VCC公共 5V板载 3.3V LDO,峰值电流约 40mA
GND系统 GND必须与 51 共地,否则收不到数据
TXDP3.0 / RXD3.3V 高电平,对 51 的 TTL 输入足够
RXDP3.1 / TXD 分压后接入5V 不能直进,需要 1k 串 2k 分压
KEY拨码开关到 VCC上电前接高进入 AT 模式
STATEP1.1 或 LED 串 1k配对成功后输出高电平

电平匹配是 HC-05 接线的核心。HC-05 是 3.3V 逻辑模块,51 的 TXD 在高电平时输出接近 5V,直接灌进 HC-05 的 RXD 长期使用会损伤模块,常见做法是用两个电阻分压:1kΩ 串在信号路径上,2kΩ 从 HC-05 的 RXD 下拉到地,分压后高电平约为 5×2/(1+2)≈3.33V。反过来,HC-05 的 TXD 输出 3.3V 高电平,而 51 的串口输入按 TTL 电平判定,高电平阈值只要 2.0V 就能识别,所以这一路可以直连,不需要电平转换。

KEY 引脚容易被忽略。很多人在实物阶段手机搜不到模块,查到最后是 KEY 悬空导致模块一直在 AT 模式或者状态不确定。KEY 在正常透传模式下应该保持低电平,建议原理图上用下拉电阻到地,再用拨码开关或跳线帽选择是否接 VCC,这样调试 AT 模式时拨一下就能切换,不用反复拔线。

51 最小系统本身没什么悬念:AT89C52 或 STC89C52RC,11.0592MHz 晶振加两个 30pF 负载电容,复位电路用 10μF 电解电容和 10kΩ 电阻,EA 引脚接 VCC,P0 口外接 10kΩ 排阻上拉。晶振频率必须选 11.0592MHz,而不是常见的 12MHz,原因后面讲串口波特率时会算给你看。

2.3 继电器驱动、电源去耦与完整元件清单

继电器的驱动电路是原理图里第二个高频翻车点。51 的 P1、P3 口高电平输出能力很弱,直接驱动继电器线圈根本吸合不了,而且继电器线圈是感性负载,关断瞬间会产生反向电动势,轻则引起单片机复位,重则打坏 I/O 口。常规做法是用一只 NPN 三极管做开关:基极串 1kΩ 电阻接 P1.0,发射极接地,集电极接继电器线圈的一端,线圈另一端接 5V,同时在线圈两端反向并联一只 1N4007 续流二极管。三极管选 S8050 或 2N2222 都够用,因为 5V 继电器线圈电流一般在 30~70mA。

电源去耦决定了这套系统能不能长时间运行。继电器吸合瞬间电流变化很大,如果 51 和继电器共享一条细走线,会在 VCC 上拉出明显毛刺,轻则蓝牙断连,重则单片机复位。原理图上要给每颗芯片的 VCC 和 GND 之间加 104 去耦电容,电源入口处并一只 100μF 电解电容,继电器单独从电源根部取电,不要和 HC-05 共用一根飞线。

完整元件清单如下,照着备料基本不会缺东西:

元件规格数量用途
AT89C52 或 STC89C52RCDIP401主控
DS18B20TO-92 封装1数字测温
HC-05 蓝牙模块板载 3.3V LDO1串口透传
晶振11.0592MHz1串口波特率基准
瓷片电容30pF2晶振负载
电解电容10μF/16V1复位电路
电阻4.7kΩ1DS18B20 上拉
排阻10kΩ 9 脚1P0 口上拉
三极管S80501继电器驱动
二极管1N40071续流
继电器5V SPDT1控制加热器或风扇
电阻1kΩ、2kΩ 若干4~5 只分压、基极限流、LED 限流

原理图工具用立创 EDA 或者 Altium Designer 都行,51 项目规模小,立创 EDA 免费版足够,画完还能直接导出 BOM 表去下单。画图时要特别注意三条地线:USB 电源地、继电器功率地、HC-05 信号地最终要在电源根部单点汇合,这样能明显减少继电器开关对数字电路的干扰。

3. Keil C51 源代码:DS18B20 时序、串口中断与继电器控制怎么写

代码整体拆成三个独立模块:DS18B20 驱动、串口收发与指令解析、温度阈值控制。51 没有硬件单总线外设,DS18B20 的时序全部要靠 I/O 口模拟,这是整套代码里最需要耐心的部分。Keil 里别忘了在 Options for Target 的 Output 选项卡勾选 Create HEX File,否则 Proteus 没东西可加载。

3.1 DS18B20 复位、写时序、读时序的 C 语言实现

单总线协议只有一根数据线,所有通信都由主机发起。一个完整操作周期分三步:主机发复位脉冲,从机回存在脉冲;主机发 ROM 指令,这里只用 0xCC 跳过 ROM;主机发功能指令,0x44 启动温度转换,0xBE 读取暂存器。复位和读写都有严格的时序窗口,代码里用延时函数和nop() 微调。

#include <reg52.h> #include <intrins.h> sbit DQ = P3^7; // 单总线数据线 void delay_us(unsigned int us) { while (us--) { _nop_(); _nop_(); // 12MHz 12T 模式下约 1~2us } } // 复位:拉低 480us 以上,释放后读存在脉冲 bit ds18b20_reset(void) { bit present = 0; DQ = 0; delay_us(500); // 主机拉低 DQ = 1; // 释放总线 delay_us(60); // 等从机应答窗口 present = DQ; // 读到 0 说明 DS18B20 在线上 delay_us(400); // 补足复位时间 return present; } // 写一个字节,低位在前,每个位时隙约 60us void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; // 产生写时隙 _nop_(); DQ = dat & 0x01; // 写 0 则继续拉低,写 1 则释放 delay_us(60); // 保持到整个时隙结束 DQ = 1; _nop_(); dat >>= 1; } } // 读一个字节,读时隙内 15us 采样 unsigned char ds18b20_read_byte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; DQ = 0; // 启动读时隙 _nop_(); DQ = 1; // 释放总线 _nop_(); if (DQ) dat |= 0x80; // 采样点 delay_us(60); } return dat; }

写时序与读时序很容易混淆。写 0 和写 1 的区别在于释放总线的时机:写 0 要一直拉低满 60us,写 1 则在 1us 后释放,靠上拉电阻把总线拉高。读时序里主机只负责启动时隙,从机在 15us 内把总线拉低表示读 0,所以主机必须在启动读时隙后立刻采样,晚于 15us 就会读错。这两个函数的基础时序全部来自芯片数据手册的时隙图,手工测量的宽容度其实比想象中大,但每个位时隙最少 60us、两个时隙之间要有 1us 以上恢复时间这两个底线不能破。

下面是把原始 16 位数据转换成温度值的主函数:

// 读取暂存器,返回 16 位原始温度,实际温度 = raw * 0.0625 unsigned int ds18b20_get_temp(void) { unsigned char tl, th; unsigned int raw; ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳过 ROM,总线上只有一个设备 ds18b20_write_byte(0x44); // 启动温度转换 delay_ms(800); // 12 位分辨率最长转换时间 750ms ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器,从第 0 字节开始 tl = ds18b20_read_byte(); // 温度低字节 th = ds18b20_read_byte(); // 温度高字节 raw = ((unsigned int)th << 8) | tl; return raw; }

这里有一个必须说清楚的细节:很多人用while(!DQ);等待转换完成,但这条语句在快速单片机上会失效。原因在于 0x44 命令的最后一个写时隙结束时总线是释放状态,DS18B20 需要几微秒时间处理命令并开始拉低总线,这期间 DQ 处于高电平,while(!DQ)会直接跳过等待,读到上一次的旧温度。换成固定 750ms 延时最省心,代价是每次测温阻塞约 0.8 秒,主循环里每 1 秒采一次温就完全够用。4 位小数的计算很简单:16 位 raw 值按 0.0625℃/位换算,unsigned int 完全可以容纳 -55℃ 到 +125℃ 对应的原始值范围。

3.2 滞回控制:温度阈值比较与继电器输出代码

温度控制的执行逻辑有两种主流写法:简单的阈值比较和带滞回的比较。如果只设一个阈值,比如 30℃,温度在 29.9℃ 和 30.1℃ 之间波动时,继电器会在几秒内反复吸合断开,触点打火、寿命急剧下降。这里用上限 30℃、下限 25℃ 的滞回区间来避免这个问题。

#define TEMP_UPPER 30 // 上限阈值,单位 ℃ #define TEMP_LOWER 25 // 下限阈值,单位 ℃ sbit RELAY = P1^0; // 继电器控制脚 bit relay_state = 0; // 当前继电器状态,1 表示吸合 // 滞回控制:加热到上限断开,冷却到下限重启 void temp_control(unsigned int raw) { unsigned char temp_now; temp_now = (unsigned char)(raw >> 4); // 取整数部分,忽略小数 if (temp_now >= TEMP_UPPER) { RELAY = 0; // 停止加热 relay_state = 0; } else if (temp_now <= TEMP_LOWER) { RELAY = 1; // 开始加热 relay_state = 1; } // 温度落在 25~30 之间时保持现状,什么都不做 }

raw >> 4是把 12 位整数位取出来的惯用做法,因为 raw 乘以 0.0625 后,高 4 位加半个字节就是整数部分,直接右移四位成本最低。25℃ 到 30℃ 这 5℃ 就是滞回区间,也叫回差。控制继电器这类执行器时回差越大,继电器动作频率越低,但温度波动也越大,5℃ 对加热灯、保温箱这类大惯性对象是合理的起步值,想要更精细可以缩到 2℃。

如果控温精度要求再高,常见做法是把继电器控制换成 PWM 调功,即在 30 秒周期里按比例调整加热时间,这时候滞回控制就不够看了,需要改成增量式 PID。PID 在 51 上也能跑,但代码量和调参工作量不是一个量级,课程设计和大多数恒温场景用滞回加合适回差已经完全够用。

3.3 串口初始化和蓝牙指令解析:帧格式与解析状态机

串口是 51 和 HC-05 之间的唯一通道,波特率必须和模块一致。HC-05 出厂默认 9600、8 位数据、无校验、1 个停止位,所以串口初始化也按这个配置来:

// 串口初始化:模式 1,允许接收,波特率 9600 void uart_init(void) { SCON = 0x50; // SM0=0 SM1=1 模式1,REN=1 允许接收 TMOD &= 0x0F; // 只改定时器 1,不动定时器 0 TMOD |= 0x20; // T1 工作方式 2,8 位自动重装 TH1 = 0xFD; // 重装值,9600 波特率对应 0xFD TL1 = 0xFD; TR1 = 1; // 启动定时器 1 ES = 1; // 开串口中断 EA = 1; // 开总中断 } void uart_send_byte(unsigned char dat) { SBUF = dat; while (!TI); // 等待发送完成 TI = 0; }

TH1 为什么是 0xFD,这就是前面强调 11.0592MHz 晶振的原因。51 工作在 12T 模式下,定时器 1 方式 2 的波特率计算公式是:波特率 = 晶振频率 / (12 × 32 × (256 - TH1))。把 11.0592MHz 和 TH1=0xFD 代入,分母是 12×32×3=1152,11059200/1152 正好等于 9600,误差为零。如果换成 12MHz 晶振,算出来是 10416,和 9600 差了约 8.5%,串口打印出来的就是乱码。

帧格式可以自己定义,但一定要有帧头、长度或数据区、校验和三个要素。我给这套系统定的协议如下:

方向帧内容含义
手机 → 单片机AA 01 00 01查询当前温度,校验=0x01+0x00
手机 → 单片机AA 02 1E 20设置上限阈值为 30(0x1E),校验=0x02+0x1E
手机 → 单片机AA 03 01 04手动吸合继电器,校验=0x03+0x01
单片机 → 手机AA 81 TH TL SUM温度上报,TH/TL 为 16 位 raw 值的高低字节

校验和取功能码加数据区的低字节,接收方重新计算后不一致就整帧丢弃。串口接收用中断实现,解析逻辑写成简单的状态机:

// 串口中断:先支持字符调试命令,再解析 4 字节二进制帧 void uart_isr(void) interrupt 4 { unsigned char ch, sum; static unsigned char buf[4]; static unsigned char idx = 0; if (TI) { TI = 0; return; } // 发送完成标志 if (!RI) return; RI = 0; ch = SBUF; // 调试用字符命令:Proteus 虚拟终端里直接敲 if (ch == 'T' || ch == 't') { uart_send_temp_report(); return; } if (ch == '1') { RELAY = 1; relay_state = 1; return; } if (ch == '0') { RELAY = 0; relay_state = 0; return; } // 二进制帧解析 if (idx == 0 && ch != 0xAA) return; // 未同步帧头,丢弃 buf[idx++] = ch; if (idx == 4) { sum = (buf[1] + buf[2]) & 0xFF; // 重算校验和 if (sum == buf[3]) { switch (buf[1]) { case 0x01: uart_send_temp_report(); break; // 查询温度 case 0x02: TEMP_UPPER = buf[2]; break; // 更新上限 case 0x03: RELAY = buf[2] & 0x01; relay_state = (buf[2] != 0); break; } } idx = 0; // 一帧处理完,回到等帧头状态 } }

字符命令T10是为 Proteus 仿真阶段准备的,毕竟虚拟终端里直接敲字符最方便;二进制帧是手机 APP 实际走的协议,仿真时可以用串口助手的 HEX 发送来验证。解析状态机没有用复杂的有限状态机结构,而是靠idx变量和静态数组完成,4 字节固定帧长让代码量控制在 30 行以内,逻辑也一目了然。要注意的是,串口中断里调用了uart_send_byte,这个函数内部while(!TI)会阻塞,如果主程序同时也在发数据可能产生竞态,这个规模的项目里风险可接受,但如果你追求严谨,可以改成查询方式发送。

温度上报函数把 raw 值拆成高低字节,加上功能码 0x81 和校验和一起发出:

// 上报温度:AA 81 TH TL SUM void uart_send_temp_report(void) { unsigned int raw; unsigned char sum; raw = ds18b20_get_temp(); uart_send_byte(0xAA); uart_send_byte(0x81); uart_send_byte((unsigned char)(raw >> 8)); uart_send_byte((unsigned char)raw); sum = (0x81 + (raw >> 8) + raw) & 0xFF; uart_send_byte(sum); }

校验和的计算注意一点:(unsigned char)raw取低字节后参与加法时,先把 raw 强制转换再相加,避免 16 位和 8 位运算混在一起产生歧义。这个函数在主循环里被调用,查询命令来自手机时才会真正触发一次完整测温,所以虽然测温阻塞 800ms,也不会影响用户操作节奏。

4. Proteus 仿真 51 单片机蓝牙系统的搭建与手机 APP 协议联调

Proteus 仿真的价值在于:在焊板子之前,就能验证 DS18B20 时序、串口收发、继电器逻辑这三块代码是否自洽。很多 51 课程设计失败都发生在"代码能编译、上板就死机",而仿真能把问题提前拦截在原理图和时序层面。

4.1 Proteus 元件清单与最小仿真电路搭建步骤

Proteus 里新建工程后,依次从元件库搜索并放置这些元件:AT89C52、DS18B20、CRYSTAL、两个 30pF 电容、10μF 电解电容、10kΩ 电阻、4.7kΩ 电阻、LED 若干、绿色电源端子,以及 Virtual Terminal。部分元件搜索不到时注意分类,DS18B20 在元件库里的关键词就是 DS18B20,Virtual Terminal 在左侧工具栏的 Virtual Instruments 面板里,图标是一个带波形的小显示器。

搭建步骤按下面的顺序操作比较稳妥:

  1. 放置 AT89C52,双击设置 Clock Frequency 为 11.0592MHz,这个值和晶振必须一致。
  2. 最小系统:X1、X2 接晶振和两个 30pF 电容到地;RST 接 10μF 电解电容到 VCC、10kΩ 电阻到 GND;EA 接 VCC。
  3. DS18B20 的 DQ 接 P3.7,外部 4.7kΩ 上拉到 VCC。
  4. P1.0 接一个 LED 串 1kΩ 电阻到 VCC,用 LED 代替继电器线圈的吸合状态,仿真里没必要放真实继电器模型。
  5. Virtual Terminal 的 RX 引脚接 AT89C52 的 TXD(P3.1),TX 引脚接 RXD(P3.0),双击把 Baud Rate 属性设为 9600。
  6. 在 Keil 里编译生成 HEX 文件后,双击 AT89C52,在 Program File 里选择该 HEX 文件,点击运行。

仿真运行后,在 Virtual Terminal 窗口里敲一个T,单片机应当立刻回一串温度帧;再敲1,P1.0 的 LED 点亮。这个最小闭环验证通过,说明 DS18B20 驱动、串口初始化、中断解析三条链路全部正常,可以进入温度和蓝牙的联调。

4.2 在 Proteus 里模拟手机蓝牙指令的两种做法

Proteus 元件库没有 HC-05 官方模型,教学中最常用的替代方案是 Virtual Terminal 和 COMPIM 虚拟串口,两者适用场景不同。

方法一,Virtual Terminal 直接模拟手机。前面代码里预留的T10字符命令就是为这一阶段准备的,运行时点开 Virtual Terminal 窗口,直接用键盘敲字符即可。这个方式适合验证控制逻辑,但因为 Virtual Terminal 收发的本质是 ASCII 字符,要向单片机发送AA 01 00 01这种二进制帧并不直观。

方法二,COMPIM 加 PC 串口助手。从元件库搜索 COMPIM 拖入画布,设置 COM 端口号、波特率 9600、数据位 8、停止位 1、无校验,把 COMPIM 的 RXD、TXD 分别接到单片机的 TXD、RXD。电脑上需要先装虚拟串口软件(比如 Virtual Serial Port Driver)创建一对互联的 COM 口,比如 COM5 和 COM6,Proteus 里 COMPIM 选 COM5,电脑串口助手选 COM6,这样模拟的是手机端通过蓝牙串口发数据。串口助手里勾选 HEX 发送,输入AA 01 00 01,单片机收到后回传AA 81 xx xx xx,整条协议链路就和实物完全一致。

仿真时改环境温度也有固定套路:暂停仿真,双击 DS18B20 元件,在属性面板里把 Temperature(部分版本显示为 Data)改成目标值,比如 40,继续运行,单片机上报的温度会随之变化。如果当前用的是 12 位分辨率模式,注意改完温度后要留给它 750ms 的转换时间,连续快速改值时偶尔读到上一次的缓存值属于正常现象。

4.3 手机 APP 的蓝牙连接、温度上报与指令发送

手机 APP 的选型决定开发效率。最快的路径是 MIT App Inventor 2,浏览器打开在线开发环境,用积木块拖拽出界面,蓝牙部分使用 BluetoothClient 组件,手机装上配套的 MIT AI2 Companion 就能直接调试,不需要安装 Android Studio,适合以功能演示为主的课程设计。如果对原生开发更熟悉,Android Studio 里走 Bluetooth SPP 流程,核心连接代码如下:

private BluetoothSocket connectBluetooth(String mac, OutputStream out) throws Exception { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter == null || !adapter.isEnabled()) { throw new Exception("蓝牙未开启"); } BluetoothDevice device = adapter.getRemoteDevice(mac); // SPP 服务的标准 UUID,HC-05 全系支持 UUID spp = UUID.fromString( "00001101-0000-1000-8000-00805F9B34FB"); BluetoothSocket socket = device.createRfcommSocketToServiceRecord(spp); socket.connect(); // 阻塞,放到子线程执行 out = socket.getOutputStream(); return socket; }

这段代码的关键是 UUID00001101-0000-1000-8000-00805F9B34FB,这是蓝牙串口服务 SPP 的固定标识,HC-05 这类模块都实现这个服务,换别的 UUID 会连接失败。socket.connect()是阻塞调用,必须在子线程里执行,否则 Android 会报 NetworkOnMainThreadException,这一点新手很容易忽略。

连接成功后发送温度查询指令,字节数组按协议组帧:

byte[] queryFrame = new byte[] { (byte) 0xAA, 0x01, 0x00, 0x01 // 校验 = 0x01 + 0x00 }; outputStream.write(queryFrame); // 阻塞读取响应:AA 81 TH TL SUM,共 5 字节 byte[] resp = new byte[5]; inputStream.read(resp); // 实际应用需要先读到 0xAA 再读 4 字节 int tempRaw = ((resp[2] & 0xFF) << 8) | (resp[3] & 0xFF); float tempC = tempRaw * 0.0625f; // 换算成摄氏度

注意resp数据里的高低字节顺序要和 C 代码里uart_send_temp_report保持一致:先高字节后低字节。resp[2]resp[3]分别对应 TH 和 TL,这个顺序错了温度会差出几百倍。APP 界面至少包含四个元素:设备 MAC 地址输入框、连接按钮、温度显示 TextView、继电器开关按钮,有条件再加一个阈值设置输入框发AA 02 xx xx帧,就形成了完整的远程控制闭环。

蓝牙权限适配也要提前做,Android 6 到 11 需要 BLUETOOTH、BLUETOOTH_ADMIN 和定位权限,Android 12 以上则改用 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT,只用目标 SDK 31 以下的老写法会在新手机上直接崩溃。

5. 实物制作流程与蓝牙、温度跳变的排错技巧

从仿真跨到实物,最大的变量从"逻辑"变成"电气"。焊接顺序、上电检查、模块配置这三个环节按顺序做好,大部分问题都能在上电前被消灭。实物制作不要按原理图从头焊到尾,而是按功能块逐级点亮。

5.1 焊接顺序与上电前检查

常见做法是先焊电源部分:USB 供电就直连 5V,9V 电源则先接 LM7805 或 AMS1117-5.0 降压,上电后用万用表确认输出稳定在 5V,再焊最小系统。晶振起振可以用示波器看,没有示波器就烧一个 LED 闪烁程序验证最小系统;接着单独写一个只读温度的程序,串口打印确认 DS18B20 能读数,再接继电器、最后接 HC-05,这样每一步的故障边界都很清晰。

上电前用万用表蜂鸣档测一遍 VCC 和 GND 之间有没有短路,重点检查电解电容正负极、二极管方向、三极管三个脚的顺序。HC-05 建议最后接,因为电平接错最容易烧的就是它。模块供电要从电源根部拉线,不要在继电器附近取电,否则继电器吸合瞬间的压降会让蓝牙模块直接掉线。

5.2 HC-05 蓝牙模块连接不上、温度乱跳的定位表

实物阶段最常见的几个问题,按现象归类对号入座:

现象可能原因排查顺序
手机搜不到 HC-05模块没进入可配对状态、供电不足量 VCC、看 STATE 脚 LED 是否慢闪
搜得到但连不上密码错误、已被其他设备占用删除手机旧配对,PIN 输入 1234
连上但收不到数据波特率不匹配、RXD/TXD 接反确认模块 9600,确认交叉连接
温度固定 85℃DQ 线悬空、上拉电阻缺失检查 DQ 焊接,补 4.7kΩ 上拉
温度上下乱跳线路过长、供电干扰线长控制在 20cm 内,加去耦电容
继电器吸合时单片机复位电源塌陷继电器线圈两端并 1N4007,加大电容

每一条都对应实际项目的失败经历。比如温度固定 85℃,仿真里永远不会出现,因为 Proteus 的 DS18B20 模型总线状态理想;但实物上 DQ 线虚焊或上拉电阻漏焊,DS18B20 上电后暂存器就是 85℃ 这个默认值,程序读回来自然不会变。串口乱码则要先看波特率公式里的晶振,再考虑 HC-05 是否被改成过别的波特率,用 AT+UART 查一下最直接。

5.3 用滑动平均给 DS18B20 去跳变

DS18B20 在小数位上跳 0.25℃ 是正常的,但在继电器、加热器共板的场景里,电源波动会让读值偶尔跳个 1~2℃,这时候滤波比换传感器更实际。滑动平均是性价比最高的做法,代码量小,效果立竿见影:

#define SAMPLE_N 16 // 窗口大小,2 的幂次方便除法优化 static unsigned int sample_buf[SAMPLE_N]; static unsigned char sample_idx = 0; static unsigned int sample_sum = 0; // 返回滑动平均后的原始温度值 unsigned int filtered_temp_raw(void) { unsigned int raw = ds18b20_get_temp(); sample_sum -= sample_buf[sample_idx]; // 先减旧值 sample_buf[sample_idx] = raw; sample_sum += raw; // 再加新值 sample_idx = (sample_idx + 1) % SAMPLE_N; return sample_sum / SAMPLE_N; }

窗口用 16 时,sample_sum 的最大值是 16 个 125℃ 对应的原始值,约 16×2000=32000,还没超出 unsigned int 的 65535 上限,但窗口加到 32 就接近边界了,到时候要么把求和变量改成 unsigned long,要么窗口保持 16。滑动平均的本质是把瞬时毛刺摊薄到整个窗口里,毛刺对输出的影响变成原来的 1/16,代价是温度变化响应慢了约 16 秒,对继电器控温这种大惯性对象完全划算。

最后提醒插座里的一个细节:滑动平均窗口里每次调用ds18b20_get_temp()都会阻塞 750ms,主循环的周期要按 采样时间加滤波时间合计来设计,把采样周期定在 1 秒、窗口取 16,蓝牙上报和控温两头都兼顾,这也是我在实物上最终确定的参数。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询