3、BLE蓝牙技术基础:BLE与经典蓝牙的区别、BLE协议栈(GAP/GATT)、广播与连接、服务与特征值概念
在Arduino教育原型项目中,当推理结果需要从开发板发送到手机App或上位机时,BLE(Bluetooth Low Energy,低功耗蓝牙)往往是比经典蓝牙更合适的选择。BLE并非经典蓝牙的简单升级,而是一套面向低功耗、小数据量、间歇性传输场景重新设计的无线通信体系。理解BLE与经典蓝牙的本质差异、协议栈分层、广播与连接机制以及服务与特征值模型,是正确设计Arduino BLE应用的前提。本章将从这四个维度展开,帮助你在实际项目中做出合理的技术选型与代码设计。
3.1 BLE与经典蓝牙的核心差异
核心原理:两种不同的设计哲学
经典蓝牙(BR/EDR,Basic Rate/Enhanced Data Rate)和BLE虽然共享"蓝牙"这一名称,但它们在设计目标上截然不同。经典蓝牙面向持续、高速率的数据流,例如无线耳机音频、文件传输;而BLE面向低功耗、小数据包、非连续传输的物联网场景。BLE通过三种关键机制实现低功耗:极短的射频开启时间(一次传输仅需数毫秒)、深度休眠模式(连接间隙可进入睡眠)、非对称设计(从设备可以比主设备更简单、更省电)。
在Arduino项目中,这种差异直接体现在硬件选型上。例如,HC-05/HC-06模块基于经典蓝牙,适合与手机配对后做串口透传;而HM-10、CC2541、nRF52832等模块基于BLE,适合传感器数据上报、控制指令下发等场景。如果项目需要持续传输音频或大量日志,经典蓝牙更合适;如果项目由电池供电、每次只发送几十字节的推理结果(如"检测到障碍物"或"温度25.3°C"),BLE是更优选择。
对比表:经典蓝牙 vs BLE
| 对比维度 | 经典蓝牙 (BR/EDR) | BLE (低功耗蓝牙) |
|---|---|---|
| 功耗 | 约1W,持续连接 | 0.01~0.5W,休眠唤醒 |
| 峰值速率 | 1~3 Mbps | 125 kbps ~ 2 Mbps |
| 连接建立延迟 | 约100ms | 约3~10ms |
| 数据包大小 | 最大约1MB(可拆包) | ATT层默认23字节(可协商至244字节) |
| 拓扑结构 | 点对点、微微网(最多7个从设备) | 广播、连接、mesh网络 |
| 典型应用 | 音频耳机、文件传输、车载免提 | 传感器、信标、智能家居、穿戴设备 |
| Arduino常用模块 | HC-05、HC-06 | HM-10、CC2541、nRF52832 |
判断方法:如何选择BLE还是经典蓝牙
- 看数据量:单次传输超过几百字节且持续不断,选经典蓝牙;每次几十字节且间隔数秒以上,选BLE。
- 看供电方式:USB供电或插座供电,两者皆可;纽扣电池或锂电池供电,优先BLE。
- 看连接延迟要求:需要秒级甚至毫秒级快速连接,BLE明显优于经典蓝牙。
- 看手机兼容性:iOS和Android对BLE支持非常完善,而经典蓝牙在iOS上受限较多。
3.2 BLE协议栈:GAP与GATT的分工
核心原理:协议栈的分层协作
BLE协议栈从下到上分为物理层(PHY)、链路层(LL)、主机控制器接口(HCI)、L2CAP、属性协议(ATT)、通用访问规范(GAP)和通用属性规范(GATT)。对于Arduino开发者而言,最需要关注的是GAP和GATT这两层,因为它们直接决定了设备如何被发现、如何连接以及数据如何组织。
GAP(Generic Access Profile)负责设备的广播、扫描和连接建立。它定义了设备角色:广播者(Broadcaster)、观察者(Observer)、外围设备(Peripheral)和中心设备(Central)。在Arduino场景中,Arduino通常作为外围设备(Peripheral)广播自己的存在,手机作为中心设备(Central)扫描并连接。
GATT(Generic Attribute Profile)负责连接建立后的数据交互。它基于ATT协议,将数据组织为"服务(Service)"和"特征值(Characteristic)"的层级结构。GATT定义了两种角色:GATT服务器(提供数据)和GATT客户端(读取/写入数据)。Arduino通常作为GATT服务器,手机作为GATT客户端。
GAP与GATT的分工对比
| 层次 | 职责 | 关键操作 | Arduino中的体现 |
|---|---|---|---|
| GAP | 设备发现、连接建立 | 广播、扫描、发起连接 | BLEPeripheral.begin()、setLocalName() |
| GATT | 连接后的数据交互 | 服务发现、读写特征值、通知 | addService()、addCharacteristic()、setValue() |
案例拆解:Arduino BLE外设的GAP/GATT配置
假设我们要用Arduino Nano 33 BLE Sense采集环境温度,并通过BLE发送到手机。在Arduino代码中,GAP层的配置包括设置设备名称和广播参数,GATT层的配置包括创建服务和特征值:
// 引入BLE库
#include <ArduinoBLE.h>
// 创建自定义服务(UUID需唯一)
BLEService envService("181A"); // 环境监测服务
// 创建特征值:温度,支持读取和通知
BLEFloatCharacteristic tempChar("2A6E", BLERead | BLENotify);
void setup() {
// GAP层配置
BLE.begin();
BLE.setLocalName("ArduinoTemp"); // 广播名称
BLE.setAdvertisedService(envService); // 广播服务UUID
// GATT层配置
BLE.setAdvertisedService(envService);
envService.addCharacteristic(tempChar);
BLE.addService(envService);
// 开始广播
BLE.advertise();
}
void loop() {
// 等待手机连接
BLEDevice central = BLE.central();
if (central) {
while (central.connected()) {
float temp = readTemperature(); // 读取传感器
tempChar.writeValue(temp); // 更新特征值
delay(1000);
}
}
}
这段代码中,BLE.setLocalName()和BLE.advertise()属于GAP层操作,而addService()、addCharacteristic()、writeValue()属于GATT层操作。理解这两层的分工,有助于排查连接问题和数据交互问题。
3.3 广播与连接:两种工作模式
核心原理:广播是"喊话",连接是"对话"
BLE设备有两种基本工作模式:广播模式和连接模式。广播模式下,外围设备周期性发送包含设备名称、服务UUID、厂商自定义数据等信息的广播包,任何扫描设备都能收到,但广播包最大仅31字节(扩展广播可达255字节)。连接模式下,中心设备与外围设备建立一对一的逻辑链路,双方可以双向收发数据,数据量更大、更可靠。
广播模式适合单向、无需确认的数据传输,例如信标(Beacon)推送、设备发现。连接模式适合双向交互,例如手机向Arduino发送控制指令,Arduino返回传感器数据。在Arduino项目中,广播模式常用于设备配对前的发现阶段,连接模式用于实际的数据交换。
广播与连接的工作流程
- 广播阶段:外围设备以固定间隔(如100ms)发送广播包,包含设备地址、设备名称、服务UUID等。
- 扫描阶段:中心设备持续扫描,收到广播包后解析设备信息。
- 连接发起:中心设备向目标外围设备发送连接请求,包含连接间隔、从设备延迟等参数。
- 连接建立:双方切换到连接状态,按照协商的连接间隔定期唤醒交换数据。
- 数据交互:通过GATT进行读写、通知等操作。
- 断开连接:任一方主动断开或超时断开,设备回到广播或休眠状态。
BLE.setConnectionInterval()调整。对于推理结果上报,建议设置100ms~500ms的间隔,兼顾实时性和功耗。
案例拆解:广播与连接的排障场景
某学生在项目中遇到"手机扫描不到Arduino设备"的问题。排查过程如下:
- 初始现象:手机App使用nRF Connect扫描,列表为空。
- 检查广播参数:确认
BLE.advertise()已调用,但发现广播间隔设置过短(10ms),导致广播包冲突。 - 检查设备名称:设备名称超过20字节,部分手机无法正确解析。
- 检查电源:Arduino使用USB供电,但BLE模块需要3.3V,电压不稳导致射频异常。
- 解决措施:将广播间隔调整为100ms,缩短设备名称为"ArduinoTemp",改用稳压电源供电。
- 结果对比:调整后手机立即扫描到设备,连接成功率从30%提升至95%。
3.4 服务与特征值:GATT的数据组织模型
核心原理:UUID、服务、特征值的层级关系
GATT将数据组织为三级结构:服务(Service)、特征值(Characteristic)和描述符(Descriptor)。每个服务包含一个或多个特征值,每个特征值包含一个值(Value)和若干属性(Properties),如读、写、通知、指示等。每个服务和特征值都有一个唯一的UUID(Universally Unique Identifier)来标识。
UUID分为两种:16位标准UUID(由蓝牙SIG定义,如温度服务0x181A、电池服务0x180F)和128位自定义UUID(用于厂商自定义服务)。在Arduino项目中,如果使用标准服务,可以直接使用16位UUID;如果自定义数据格式,需要生成128位UUID。
服务与特征值的结构示例
| 层级 | 示例 | 说明 |
|---|---|---|
| 服务 | 环境监测服务 (0x181A) | 包含温度、湿度、气压等特征值 |
| 特征值 | 温度特征值 (0x2A6E) | 数据类型为Float,支持读取和通知 |
| 描述符 | 客户端特征配置描述符 (0x2902) | 用于启用/禁用通知功能 |
特征值的属性与操作
- 读(Read):客户端主动读取特征值,适合低频数据获取。
- 写(Write):客户端向特征值写入数据,适合下发控制指令。
- 通知(Notify):服务器主动推送数据,客户端无需请求,适合实时数据上报。
- 指示(Indicate):类似通知,但客户端需要确认,可靠性更高。
案例拆解:推理结果通过BLE通知发送
假设Arduino运行一个简单的图像分类模型,推理结果为"cat"或"dog"。我们可以设计一个自定义服务,包含一个字符串特征值用于发送结果:
// 自定义128位UUID(示例)
const char* serviceUUID = "12345678-1234-5678-1234-56789abcdef0";
const char* resultCharUUID = "12345678-1234-5678-1234-56789abcdef1";
BLEService inferenceService(serviceUUID);
BLECharacteristic resultChar(resultCharUUID, BLERead | BLENotify, 20);
void setup() {
BLE.begin();
BLE.setLocalName("AIInference");
BLE.setAdvertisedService(inferenceService);
inferenceService.addCharacteristic(resultChar);
BLE.addService(inferenceService);
BLE.advertise();
}
void loop() {
BLEDevice central = BLE.central();
if (central) {
while (central.connected()) {
String result = runInference(); // 执行推理
resultChar.writeValue(result.c_str(), result.length());
delay(500); // 每500ms发送一次
}
}
}
在这个案例中,手机App作为GATT客户端,订阅resultChar的通知。当Arduino写入新值时,手机会自动收到推送,无需轮询。这种模式非常适合推理结果上报,既省电又实时。
常见误区与总结复盘
常见误区2:忽略特征值的最大长度限制。BLE 4.0/4.1的ATT默认MTU为23字节,扣除协议头后有效载荷仅20字节。如果需要发送更长的数据,必须协商MTU或分包发送。
常见误区3:混淆GAP广播和GATT通知。广播是连接前所有设备都能收到的,通知是连接后只有配对设备能收到的。