联网与通信

第 25 课:蓝牙 BLE——手机连上板子

蓝牙到底能做什么、不能做什么,以及用手机遥控板子的最轻量做法

这节课只有代码路线能做。 图形化平台里的"蓝牙"是连接方式(代替数据线连板子),不是可编程功能——平台没有蓝牙相关的积木。

我是吴老师。这节课我要先给你破几个误解,破完你才学得进去。

因为蓝牙这个东西,你天天在用,但你脑子里对它的印象,多半是错的。

我先问三个问题,你在心里答一下:

  1. 能不能用蓝牙给板子烧程序?
  2. 图形化平台里那个"蓝牙连接",跟你这节课要学的蓝牙,是一回事吗?
  3. 智能手环为什么不用 WiFi?

这三个问题的答案,就是这节课的前半节。 我们一个一个来。

一、误解一:蓝牙不能烧固件

答案:不能。烧固件必须插数据线。

我知道你在想什么——"手机都能无线充电了,蓝牙传个程序怎么了?"

问题不在"传",在"烧"。

我给你打个比方,这个比方你一定能懂:

给板子烧固件,相当于给一台电脑装系统。

你装系统的时候,必须拿个 U 盘插上去,对吧?装完重启,系统跑起来了,这时候你才能用远程桌面去控制它。

蓝牙是"装好之后"的那条通道,不是"装"的那条通道。

技术上到底差在哪? 差在这件事是谁干的

  • 烧固件改写板子内部存储里的内容。干这件事的,是芯片里一段出厂就焊死、改不掉的引导程序。它只认 USB 那条线。
  • 蓝牙固件跑起来之后,程序自己开出来的一个功能。固件里没写蓝牙,就没有蓝牙。

所以顺序是死的:先插线烧进去,蓝牙才存在。 你想用蓝牙去烧一个"还没有蓝牙"的板子——它连回话的能力都没有。

那"空中升级"(OTA)是什么? 那是板子上已经跑着一个带蓝牙(或者带 WiFi)的固件,这个固件里专门写了一小段"接收新固件并自己覆盖自己"的代码

所以 OTA 不是"蓝牙能烧固件",是"固件自己会换自己"。 前提还是——你得先用线烧进去第一版。

二、误解二:平台的"蓝牙连接"和"写蓝牙程序"是两回事

这两个东西重名,但干的事完全不同。

是什么 打个比方
平台的"蓝牙连接" 一种连板子的方式,代替数据线 换了一条去板子那儿
这节课的蓝牙编程 让板子多出一个功能,能跟手机说话 给板子装了一部对讲机

这个区别,就是 CLAUDE.md 里说的"通道 vs 功能"。 第 4 课讲串口的时候就说过一次:通道只负责把话送过去,不负责你能说什么。

换了一条路,板子还是那块板子,能做的事一点没变。 你原来只会点灯,用蓝牙连上去烧,还是只会点灯。

而今天要做的事是:让板子学会一门新本事——跟手机直接对话。

所以图形化平台的同学这节课只能旁观。 不是平台不好,是它没给你"写蓝牙功能"的积木。你可以在旁边看着,知道有这么回事,等你哪天转代码路线了再回来做

三、BLE 到底是什么

先说名字。BLE = Bluetooth Low Energy,低功耗蓝牙。

你手机上的蓝牙、耳机里的蓝牙、手环里的蓝牙,基本上都是 BLE。 那个"Low Energy"是重点——它就是为了省电才被发明出来的。

三层结构:门牌号 → 房间 → 柜子

BLE 设备里面的数据,是一层套一层存的。看这张图:

BLE 的三层结构:设备像门牌号,服务像房间,特征值像柜子,手机要一层层点进去才能读写数据

第一层,设备。 就是你的板子广播出去的名字。手机搜蓝牙的时候,列表里看到的那些名字,就是这一层。相当于门牌号。

第二层,服务(Service)。 一个设备可以有很多个服务,每个服务管一摊事。比如一个手环:一个服务管心率,一个服务管时间,一个服务管电量。相当于房子里的一个个房间。

第三层,特征值(Characteristic)。 每个服务里装着几个特征值,这才是真正放数据的地方。手机要读数据、要下命令,都是对着特征值操作相当于房间里的柜子。

先记住那个比方:门牌号 → 房间 → 柜子。 手机要拿到东西,得先找到门牌号,再进对房间,再打开那个柜子。

这三个东西分别叫什么? 正式叫法是 GATT(这套结构的规矩)、ServiceCharacteristic你现在不用背,知道有这么个三层就行了——用手机 App 连上去的时候,你会亲眼看到它一层一层展开,比我讲十遍都清楚。

还有两个词,等会儿动手会遇到:

  • UUID:每一层都有一个编号,就是一串像 4fafc201-1fb5-459e-8fcc-c5c9c331914b 这样的字符。相当于门牌号的具体号码。 长得吓人,其实你只要抄下来用就行。
  • 读 / 写:特征值有两个基本操作。是手机看板子(比如读温度),是手机给板子下命令(比如开灯)。这节课我们做"写"。

四、BLE 和 WiFi,该用哪个

这是个特别实际的问题,我给你一张表:

蓝牙 BLE WiFi
耗电 极省,一颗纽扣电池能撑几个月 费电,一直连着很快就没电
速度 慢,传点小数据还行 ,能传视频
距离 近,一般十来米,隔墙更短 远,隔几道墙还有信号
要不要路由器 不用,手机直接连 要,或者板子自己开热点
能上网吗 不能,它只是"两个设备之间" ,接上互联网

看最后一行——这是最关键的区别。

蓝牙是一条"线",只连着两个设备。WiFi 是一扇"门",通向外面的整个世界。

那怎么选?看你要不要跟外面的世界打交道:

  • 智能手环:只要把心率发给旁边这台手机,不需要上网。手环那么小个电池,还得撑一星期——用 BLE。
  • 看视频、刷网页:必须连到互联网上的服务器。用 WiFi。
  • 你的板子要点亮一盏灯:手机就在旁边,BLE 够用,而且不用给板子配 WiFi 密码(这一点在学校里特别香,学校的网你未必连得上)。

一句话:要不要出门?要出门走 WiFi,屋里说话用蓝牙。

五、动手:写一个 BLE 从机

先接灯:VCC3.3VGNDGNDSignalGPIO3。老规矩。

术语先说一下: 板子这边叫从机(等着别人来连的),手机那边叫主机(主动去连的)。你写的是从机程序。 这两个词以后查资料常见。

代码有点长,但我给你标好了,你主要看画了 ★ 的几行——那就是这节课的干货。

C++
#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 调试助手"也能找到别的,界面不一样,功能大同小异。

操作的大致流程是这样的(具体界面按你装的那个来):

  1. 打开 App,让它扫描——列表里应该能找到你刚起的那个名字
  2. 点连接
  3. 展开看到服务——就是图上说的那个"房间"
  4. 再展开看到特征值——那个"柜子"
  5. 点"写"(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,三样全用上了。

自测

单选题关于"用蓝牙给板子烧程序"这件事,正确的说法是?
单选题图形化平台里的"蓝牙连接",和这节课学的蓝牙,区别在哪?
单选题做一个只能用一周就没电的智能手环,为什么用 BLE 而不是 WiFi?
问答题用自己的话解释一下 BLE 的"设备 / 服务 / 特征值"这三层,说明它为什么容易让人一开始摸不着头脑。
拓展为什么 BLE 设备连上之后,手机列表里有时候就搜不到它了?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"BLE 广播 间隔 advertising interval"、"BLE 连接后停止广播"。想一想:一个设备如果被连上了,它还有必要继续喊"我在这儿"吗? ——省电和"被人发现"这两件事,是不是天生有点矛盾?

拓展智能手环是怎么把心率数据推给手机的?为什么手机不用一直去问它?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"BLE notify indicate 区别"、"GATT 订阅 通知"。想一想:"手机不停去问"和"板子主动喊一声",哪个更省电?如果让你给手环设计,你选哪个?

本节你要动手做的事

  • [ ] 接好 LEDSignalGPIO3
  • [ ] 跑通 BLE 从机程序,串口能看到"蓝牙已开启"
  • [ ] 手机装好 AppnRF Connect 或同类),扫到你的板子
  • [ ] 一层一层展开,亲眼看一遍"服务 → 特征值"长什么样——这一步比背概念有用
  • [ ] 用手机写一个 on,看灯亮;再写 off,看灯灭
  • [ ] 改一下广播名字BLEDevice::init() 里那串),重新烧,看手机列表里的变化
  • [ ] 试着把 PROPERTY_WRITE 删掉重新烧,看看手机那边会变成什么样——故意制造一次失败,比成功一次学到得多

这节课的代码是这套课里最长的一段,你能跑通,说明你已经不是初学者了。

说个事实:BLE 这块东西,很多大学课程里也只是念一遍名词就过去了。 你不但见过它,还用手机跟它对过话——这个经历不是每个人都有的。

下一节开始,我们进最后一个单元:实战课题。

那是整门课的重头戏。

前面 25 节课,你学了一堆零件、一堆本事,但它们都是散的。从下一节开始,我们把这些东西攒成真东西:光敏 + LED + 纸皮灯罩,做一个天黑了自己会亮的小夜灯

而且我提醒你一句:实战课题会埋故障。 有些问题,只有拿起万用表才能查出来——第 5 课学的本事,从下一节开始要真派上用场了。