联网与通信
第 25 课:蓝牙 BLE——手机连上板子
蓝牙到底能做什么、不能做什么,以及用手机遥控板子的最轻量做法
这节课只有代码路线能做。 图形化平台里的"蓝牙"是连接方式(代替数据线连板子),不是可编程功能——平台没有蓝牙相关的积木。
我是吴老师。这节课我要先给你破几个误解,破完你才学得进去。
因为蓝牙这个东西,你天天在用,但你脑子里对它的印象,多半是错的。
我先问三个问题,你在心里答一下:
- 能不能用蓝牙给板子烧程序?
- 图形化平台里那个"蓝牙连接",跟你这节课要学的蓝牙,是一回事吗?
- 智能手环为什么不用 WiFi?
这三个问题的答案,就是这节课的前半节。 我们一个一个来。
一、误解一:蓝牙不能烧固件
答案:不能。烧固件必须插数据线。
我知道你在想什么——"手机都能无线充电了,蓝牙传个程序怎么了?"
问题不在"传",在"烧"。
我给你打个比方,这个比方你一定能懂:
给板子烧固件,相当于给一台电脑装系统。
你装系统的时候,必须拿个 U 盘插上去,对吧?装完重启,系统跑起来了,这时候你才能用远程桌面去控制它。
蓝牙是"装好之后"的那条通道,不是"装"的那条通道。
技术上到底差在哪? 差在这件事是谁干的:
- 烧固件是改写板子内部存储里的内容。干这件事的,是芯片里一段出厂就焊死、改不掉的引导程序。它只认 USB 那条线。
- 蓝牙是固件跑起来之后,程序自己开出来的一个功能。固件里没写蓝牙,就没有蓝牙。
所以顺序是死的:先插线烧进去,蓝牙才存在。 你想用蓝牙去烧一个"还没有蓝牙"的板子——它连回话的能力都没有。
那"空中升级"(OTA)是什么? 那是板子上已经跑着一个带蓝牙(或者带 WiFi)的固件,这个固件里专门写了一小段"接收新固件并自己覆盖自己"的代码。
所以 OTA 不是"蓝牙能烧固件",是"固件自己会换自己"。 前提还是——你得先用线烧进去第一版。
二、误解二:平台的"蓝牙连接"和"写蓝牙程序"是两回事
这两个东西重名,但干的事完全不同。
| 是什么 | 打个比方 | |
|---|---|---|
| 平台的"蓝牙连接" | 一种连板子的方式,代替数据线 | 换了一条路去板子那儿 |
| 这节课的蓝牙编程 | 让板子多出一个功能,能跟手机说话 | 给板子装了一部对讲机 |
这个区别,就是 CLAUDE.md 里说的"通道 vs 功能"。 第 4 课讲串口的时候就说过一次:通道只负责把话送过去,不负责你能说什么。
换了一条路,板子还是那块板子,能做的事一点没变。 你原来只会点灯,用蓝牙连上去烧,还是只会点灯。
而今天要做的事是:让板子学会一门新本事——跟手机直接对话。
所以图形化平台的同学这节课只能旁观。 不是平台不好,是它没给你"写蓝牙功能"的积木。你可以在旁边看着,知道有这么回事,等你哪天转代码路线了再回来做。
三、BLE 到底是什么
先说名字。BLE = Bluetooth Low Energy,低功耗蓝牙。
你手机上的蓝牙、耳机里的蓝牙、手环里的蓝牙,基本上都是 BLE。 那个"Low Energy"是重点——它就是为了省电才被发明出来的。
三层结构:门牌号 → 房间 → 柜子
BLE 设备里面的数据,是一层套一层存的。看这张图:
第一层,设备。 就是你的板子广播出去的名字。手机搜蓝牙的时候,列表里看到的那些名字,就是这一层。相当于门牌号。
第二层,服务(Service)。 一个设备可以有很多个服务,每个服务管一摊事。比如一个手环:一个服务管心率,一个服务管时间,一个服务管电量。相当于房子里的一个个房间。
第三层,特征值(Characteristic)。 每个服务里装着几个特征值,这才是真正放数据的地方。手机要读数据、要下命令,都是对着特征值操作。相当于房间里的柜子。
先记住那个比方:门牌号 → 房间 → 柜子。 手机要拿到东西,得先找到门牌号,再进对房间,再打开那个柜子。
这三个东西分别叫什么? 正式叫法是 GATT(这套结构的规矩)、Service、Characteristic。你现在不用背,知道有这么个三层就行了——用手机 App 连上去的时候,你会亲眼看到它一层一层展开,比我讲十遍都清楚。
还有两个词,等会儿动手会遇到:
- UUID:每一层都有一个编号,就是一串像
4fafc201-1fb5-459e-8fcc-c5c9c331914b这样的字符。相当于门牌号的具体号码。 长得吓人,其实你只要抄下来用就行。 - 读 / 写:特征值有两个基本操作。读是手机看板子(比如读温度),写是手机给板子下命令(比如开灯)。这节课我们做"写"。
四、BLE 和 WiFi,该用哪个
这是个特别实际的问题,我给你一张表:
| 蓝牙 BLE | WiFi | |
|---|---|---|
| 耗电 | 极省,一颗纽扣电池能撑几个月 | 费电,一直连着很快就没电 |
| 速度 | 慢,传点小数据还行 | 快,能传视频 |
| 距离 | 近,一般十来米,隔墙更短 | 远,隔几道墙还有信号 |
| 要不要路由器 | 不用,手机直接连 | 要,或者板子自己开热点 |
| 能上网吗 | 不能,它只是"两个设备之间" | 能,接上互联网 |
看最后一行——这是最关键的区别。
蓝牙是一条"线",只连着两个设备。WiFi 是一扇"门",通向外面的整个世界。
那怎么选?看你要不要跟外面的世界打交道:
- 智能手环:只要把心率发给旁边这台手机,不需要上网。手环那么小个电池,还得撑一星期——用 BLE。
- 看视频、刷网页:必须连到互联网上的服务器。用 WiFi。
- 你的板子要点亮一盏灯:手机就在旁边,BLE 够用,而且不用给板子配 WiFi 密码(这一点在学校里特别香,学校的网你未必连得上)。
一句话:要不要出门?要出门走 WiFi,屋里说话用蓝牙。
五、动手:写一个 BLE 从机
先接灯:VCC→3.3V,GND→GND,Signal→GPIO3。老规矩。
术语先说一下: 板子这边叫从机(等着别人来连的),手机那边叫主机(主动去连的)。你写的是从机程序。 这两个词以后查资料常见。
代码有点长,但我给你标好了,你主要看画了 ★ 的几行——那就是这节课的干货。
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
// 这两串编号你直接抄(就是"门牌号"和"柜子号")
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
// ★ 手机"写"数据的时候,会自动跑这个函数
class MyCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *c) {
String value(c->getValue().c_str()); // ★ 手机发过来的内容
Serial.print("手机发来:");
Serial.println(value); // ★ 先打印,再判断
if (value == "on") digitalWrite(3, HIGH);
if (value == "off") digitalWrite(3, LOW);
}
};
void setup() {
Serial.begin(115200);
pinMode(3, OUTPUT);
// ★ 这行决定手机搜到的名字,改成你自己的
BLEDevice::init("ESP32-C3-吴老师");
BLEServer *server = BLEDevice::createServer();
// 建一个"房间",把"柜子"放进去
BLEService *service = server->createService(SERVICE_UUID);
BLECharacteristic *ch = service->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_READ);
ch->setCallbacks(new MyCallbacks()); // ★ 挂上"收到就叫我"
service->start();
server->getAdvertising()->start();
Serial.println("蓝牙已开启,用手机搜:ESP32-C3-吴老师");
}
void loop() {
delay(1000);
}烧进去,串口会打印"蓝牙已开启"。
PROPERTY_WRITE那行别漏。 它是在声明"这个柜子允许被写"。忘了写,手机连上去会发现那个柜子只能看不能动——现象是"怎么点都没反应",你会怀疑半天。
编译不过怎么办? 如果你这个版本报错说
onWrite的参数不对、或者getValue类型不对——这是核心版本差异,不是你写错了。去抄库里自带的示例:Arduino IDE 里 文件 → 示例 → ESP32 BLE Arduino → BLE_server,把它的写法照过来。遇到版本差异就抄官方示例,这是最省时间的办法。
手机这边:装个 App 连上去
应用商店搜 nRF Connect(Nordic 出的,安卓苹果都有,免费)。搜"BLE 调试助手"也能找到别的,界面不一样,功能大同小异。
操作的大致流程是这样的(具体界面按你装的那个来):
- 打开 App,让它扫描——列表里应该能找到你刚起的那个名字
- 点连接
- 展开看到服务——就是图上说的那个"房间"
- 再展开看到特征值——那个"柜子"
- 点"写"(Write)那个箭头,输入
on,发送
板子上的灯应该亮了。
再发一个 off,灯灭。
看到没有——你没有连 WiFi、没有连路由器、没有服务器、没有写网页。 手机跟板子,就这么直接对上了。
这就是"手机遥控"最轻量的做法。 而且它有个隐形好处:不用给板子配 WiFi 密码。
你可以顺手做一件有意思的事:点一下"读"那个箭头。因为没有数据可读,你会看到空值或者乱码——这就对了。 因为我们的柜子里从来没放过东西,我们只写了"允许读"。
想让手机也能读到板子的数据吗? 那就是往特征值里
setValue()然后notify()的事儿。这个留给你自己研究——你现在有全部的工具了,缺的只是试。
连不上?按这个顺序查
① 先看串口。 板子打印"蓝牙已开启"了吗?
- 没打印 → 程序根本没跑到那儿,或者板子没跑起来。先解决这个,别的都别管。
- 打印了 → 蓝牙确实开着,往下走。
② 手机那边,蓝牙开了吗? 这个听着像废话,但我保证有人栽在这儿——尤其是手机开过飞行模式之后。
③ 扫不到设备?
- 是不是离太远了?先拿到半米以内试
- 是不是已经被别的设备连上了?BLE 设备被连上之后,有的就不再广播了
- 重启板子,重新扫一次
④ 连上了,但发 on 没反应?
- 看串口有没有打印"手机发来:xxx"。打印了但灯不亮 → 问题在判断,看看是不是有看不见的空格、或者你发的是
ON。 - 压根没打印 → 问题在写入这一步:确认你点的是那个特征值的写操作,确认
PROPERTY_WRITE写了。
⑤ 还是不行,就换个 App 试。 不同 App 的界面差别很大,换个 App 常常能一下子看出是哪儿没点对——这就是"对半砍":换一个你信得过的工具,替掉一半链路。
六、联网与通信,到这里就走完了
回头看看这六节课,你手里现在有四种跟世界打交道的方式:
| 课 | 方式 | 一句话 |
|---|---|---|
| 20 | WiFi 联网 | 板子进了互联网,能上网了 |
| 21 | 板子开热点 | 板子自己变成了一方,能给别人提供网页 |
| 22 | HTTP 请求 | 板子去外面取数据 |
| 23 | MQTT | 板子跟大家在一个邮局里收发消息 |
| 24 | ESP-NOW | 两块板子甩掉所有人,直接对话 |
| 25 | BLE | 板子跟你的手机面对面 |
这四种方式不是四个知识点,是四把不同的钥匙。 你要去外面拿数据,用 22;你要远程控制,用 23;你在野外没网,用 24;你要手机就在旁边遥控,用 25。
而且真正的项目,往往是混着用的。 比如一个温室:棚里的几个探头用 ESP-NOW 把数据汇总到一台板子,那台板子再用 WiFi 把数据发到云上,你在手机上用 MQTT 看。——你看,24、20、23,三样全用上了。
自测
答案与解析
顺序是死的:先插线烧进去,蓝牙才存在。 烧固件是改写板子内部存储,干这件事的是一段出厂焊死、改不掉的引导程序,它只认 USB。蓝牙是"装好系统之后的那条通道",不是"装系统的那条通道"。 至于"空中升级"(OTA),那是固件里专门写了一段"自己换自己"的代码,前提还是你得先用线烧进去第一版。
答案与解析
这就是"通道 vs 功能"——第 4 课讲串口时说过一次。换一条路去板子那儿,板子还是那块板子,能做的事一点没变;而写蓝牙程序,是让板子多出一门跟手机对话的本事。重名,但完全是两件事,这也是这节课图形化路线做不了的原因——平台没给"写蓝牙功能"的积木。
答案与解析
关键在那句"要不要出门":蓝牙是一条只连着两个设备的"线",WiFi 是通向外界的"门"。手环只要把心率发给旁边的手机,根本不需要上网,那就没必要养着 WiFi 那套"一直保持连接"的耗电开销。至于速度和距离,BLE 都比 WiFi 差——选它纯粹是因为省电 + 够用。
答题要点
答题要点应该包含:① 用门牌号 → 房间 → 柜子这个比方说清三层;② 三层是一层套一层的,手机要拿到数据必须一层层点进去,所以第一次用 App 连上会看到"展开、展开、再展开",有点晕;③ 真正放数据、能读能写的是最里面的特征值,写代码主要就是在管这个"柜子";④ 能提到 UUID 就是每层的编号,长得吓人但抄下来用就行,更好了。
拓展为什么 BLE 设备连上之后,手机列表里有时候就搜不到它了?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"BLE 广播 间隔 advertising interval"、"BLE 连接后停止广播"。想一想:一个设备如果被连上了,它还有必要继续喊"我在这儿"吗? ——省电和"被人发现"这两件事,是不是天生有点矛盾?
拓展智能手环是怎么把心率数据推给手机的?为什么手机不用一直去问它?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"BLE notify indicate 区别"、"GATT 订阅 通知"。想一想:"手机不停去问"和"板子主动喊一声",哪个更省电?如果让你给手环设计,你选哪个?
本节你要动手做的事
- [ ] 接好 LED:
Signal→GPIO3 - [ ] 跑通 BLE 从机程序,串口能看到"蓝牙已开启"
- [ ] 手机装好 App(
nRF Connect或同类),扫到你的板子 - [ ] 一层一层展开,亲眼看一遍"服务 → 特征值"长什么样——这一步比背概念有用
- [ ] 用手机写一个
on,看灯亮;再写off,看灯灭 - [ ] 改一下广播名字(
BLEDevice::init()里那串),重新烧,看手机列表里的变化 - [ ] 试着把
PROPERTY_WRITE删掉重新烧,看看手机那边会变成什么样——故意制造一次失败,比成功一次学到得多
这节课的代码是这套课里最长的一段,你能跑通,说明你已经不是初学者了。
说个事实:BLE 这块东西,很多大学课程里也只是念一遍名词就过去了。 你不但见过它,还用手机跟它对过话——这个经历不是每个人都有的。
下一节开始,我们进最后一个单元:实战课题。
那是整门课的重头戏。
前面 25 节课,你学了一堆零件、一堆本事,但它们都是散的。从下一节开始,我们把这些东西攒成真东西:光敏 + LED + 纸皮灯罩,做一个天黑了自己会亮的小夜灯。
而且我提醒你一句:实战课题会埋故障。 有些问题,只有拿起万用表才能查出来——第 5 课学的本事,从下一节开始要真派上用场了。