2、BLE基础概念:蓝牙低功耗技术原理、GAP与GATT协议、广播与连接模式
在ESP32上开发BLE家居设备,首先需要理解BLE(Bluetooth Low Energy,蓝牙低功耗)的底层逻辑。BLE并非传统蓝牙的简单升级,而是为物联网场景重新设计的一套协议栈。它牺牲了连续数据传输速率,换取了极低的峰值功耗、毫秒级的连接建立时间和灵活的角色切换能力。本章将围绕BLE的物理层与协议栈分层、GAP(Generic Access Profile,通用访问规范)与GATT(Generic Attribute Profile,通用属性规范)两大核心协议,以及广播与连接两种工作模式展开,帮助你在ESP32上建立正确的BLE开发心智模型。
上图展示了BLE协议栈的完整分层关系。物理层负责2.4GHz频段的射频收发,链路层管理信道选择、重传和加密。在链路层之上,GAP和GATT是两个平行的协议,分别解决“设备如何被发现和连接”与“连接后如何组织数据”的问题。对于ESP32开发者而言,理解这两个协议的区别至关重要:GAP处理的是设备间的“外交关系”,而GATT处理的是“商务合同”。
2.1 蓝牙低功耗技术原理:从物理层到协议栈
BLE之所以能实现低功耗,核心在于其“短突发”工作方式。传统蓝牙BR/EDR在连接后持续占用射频资源,而BLE在空闲时让射频完全关闭,仅在需要发送或接收数据时快速唤醒。这种机制使得BLE的峰值电流虽然可达15mA左右,但平均电流可以低至微安级别。
BLE在2.4GHz频段划分了40个信道,每个信道带宽2MHz。其中37个信道用于数据传输,3个专用广播信道(37、38、39)用于设备发现和连接建立。这种设计避免了与Wi-Fi的2.4GHz信道重叠干扰,同时广播信道分布在频段的不同位置,提高了抗干扰能力。
esp_ble_gap_set_prefer_conn_params() 调整连接参数,但物理层速率由芯片硬件决定。
BLE协议栈从下到上依次为:物理层(PHY)、链路层(LL)、主机控制接口(HCI)、逻辑链路控制与适配协议(L2CAP)、属性协议(ATT)、安全管理(SM)、GAP和GATT。在ESP32中,这些层大部分由芯片固件实现,开发者通过ESP-IDF提供的API操作GAP和GATT层即可,无需直接操作链路层。
2.2 GAP协议:设备发现与连接管理
GAP定义了BLE设备如何广播自身信息、如何被其他设备发现、以及如何建立连接。它规定了四种角色:Broadcaster(仅广播)、Observer(仅扫描)、Peripheral(从设备,可被连接)、Central(主设备,发起连接)。在智能家居场景中,ESP32通常作为Peripheral,手机App作为Central。
GAP的核心机制是广播数据包。广播包由两部分组成:广播数据(Advertising Data)和扫描响应数据(Scan Response Data)。广播数据是必须的,最多31字节;扫描响应数据是可选的,也最多31字节。当Observer收到广播包后,可以主动发送扫描请求,Peripheral则回复扫描响应数据。
| 广播数据类型 | AD Type 值 | 用途 | 示例 |
|---|---|---|---|
| Flags | 0x01 | 声明设备支持的能力(LE、BR/EDR等) | 0x06 表示支持LE且不支持BR/EDR |
| Complete Local Name | 0x09 | 设备完整名称 | "ESP32_Light_01" |
| Service UUIDs | 0x03/0x05 | 声明设备提供的服务UUID | 0xFFF0(自定义服务) |
| Manufacturer Specific Data | 0xFF | 厂商自定义数据 | 设备状态、电量等 |
| Tx Power Level | 0x0A | 广播时的发射功率 | 0dBm、-4dBm等 |
在ESP32上配置广播数据时,需要将多个AD结构(Type + Length + Data)拼接成一个完整的广播包。例如,要广播设备名称和自定义服务UUID,可以这样构造:
// ESP32 广播数据配置示例
uint8_t adv_data[] = {
0x02, 0x01, 0x06, // Flags: LE General Discoverable
0x03, 0x03, 0xF0, 0xFF, // Complete List of 16-bit Service UUIDs: 0xFFF0
0x0F, 0x09, 'E', 'S', 'P', '3', '2', '_', 'L', 'i', 'g', 'h', 't', '_', '0', '1' // Name
};
esp_ble_gap_config_adv_data(sizeof(adv_data), adv_data);
GAP还定义了连接参数,包括连接间隔(Connection Interval)、从设备延迟(Slave Latency)和超时时间(Supervision Timeout)。连接间隔决定了Central和Peripheral之间的数据交换频率,间隔越短,实时性越高,但功耗也越大。从设备延迟允许Peripheral跳过若干次连接事件而不必监听,进一步降低功耗。
esp_ble_gap_update_conn_params() 动态调整。
2.3 GATT协议:数据组织与交互
GATT建立在ATT协议之上,定义了连接建立后数据的结构化组织方式。GATT的核心概念是Service(服务)和Characteristic(特征)。一个Service包含多个Characteristic,每个Characteristic包含一个Value和若干Descriptor(描述符)。所有数据通过UUID(Universally Unique Identifier)进行标识。
GATT定义了两种角色:GATT Server(提供数据)和GATT Client(消费数据)。在ESP32智能家居设备中,ESP32通常作为GATT Server,手机App作为GATT Client。Server维护一个属性表,Client通过读写属性来获取或设置数据。
Characteristic支持四种操作:Read(读)、Write(写)、Notify(通知)和Indicate(指示)。Notify和Indicate的区别在于:Notify不要求Client确认,数据可能丢失;Indicate要求Client发送确认,可靠性更高但多一次交互。对于状态同步场景,Notify通常足够;对于关键控制指令,建议使用Indicate或Write with Response。
// ESP32 GATT 服务定义示例(简化)
static esp_gatt_srvc_id_t service_id = {
.id.uuid = {.len = ESP_UUID_LEN_16, .uuid = {.uuid16 = 0xFFF0}},
.is_primary = true
};
// 特征:开关状态(可读、可写、可通知)
static esp_gatt_char_prop_t char_prop = ESP_GATT_CHAR_PROP_BIT_READ |
ESP_GATT_CHAR_PROP_BIT_WRITE |
ESP_GATT_CHAR_PROP_BIT_NOTIFY;
esp_ble_gatts_create_attr_tab(gatts_attr_tab, gatts_if, 3, 0);
GATT的Profile结构可以类比为文件系统:Service是文件夹,Characteristic是文件,Descriptor是文件的属性标签。这种分层设计使得不同厂商的设备可以互相理解数据含义,只要它们遵循相同的UUID定义。
2.4 广播与连接模式:两种工作方式的选择
广播模式(无连接)和连接模式(面向连接)各有适用场景。广播模式适合单向、低频、低功耗的数据传输,例如智能门锁的电量广播、温湿度传感器的周期性上报。连接模式适合双向、高频、需要可靠传输的场景,例如智能灯的控制与状态反馈。
广播模式的优势在于:无需配对、功耗极低、支持一对多接收。但缺点是:数据长度受限(31字节)、无确认机制、无法保证数据到达。连接模式则提供可靠传输、支持大数据量、支持加密和配对,但需要额外的连接维护开销。
在ESP32上,你可以通过 esp_ble_gap_start_advertising() 启动广播,通过 esp_ble_gap_connect() 发起连接。设备可以同时处于广播和连接状态,但需要注意广播和连接会共享射频资源,可能影响性能。
2.5 实战场景:智能灯泡的BLE控制与状态同步
假设我们要设计一款ESP32智能灯泡,要求支持手机App控制开关、调节亮度,并实时同步灯泡状态。我们选择ESP32作为Peripheral和GATT Server,手机App作为Central和GATT Client。
初始目标与约束:灯泡需要支持开关和亮度调节,状态变化需要在100ms内同步到手机。同时,灯泡在待机时功耗要尽可能低,目标平均电流小于1mA。
关键设计决策:
- GAP角色:ESP32作为Peripheral,广播间隔设为100ms,广播数据包含设备名称和自定义服务UUID(0xFFF0)。
- GATT服务设计:定义两个Characteristic:开关状态(0xFFF1,可读可写可通知)和亮度值(0xFFF2,可读可写可通知)。
- 连接参数:连接间隔设为30ms,从设备延迟设为0,超时时间设为2s。这样保证控制指令的实时性。
- 功耗优化:当灯泡处于关闭状态且无操作时,通过
esp_ble_gap_update_conn_params()将连接间隔调整为200ms,降低监听功耗。
实现过程:手机App扫描到广播后发起连接。连接建立后,App读取当前开关状态和亮度值。当用户在App上滑动亮度条时,App通过Write操作发送亮度值,ESP32收到后控制PWM输出,并通过Notify将新亮度值广播给App(实际上是通过连接发送通知)。当用户关闭灯泡时,ESP32更新状态并发送Notify,App收到后更新UI。
结果与复盘:实测控制延迟约50ms,满足100ms要求。待机时平均电流约0.8mA(连接间隔200ms),符合低功耗目标。但发现一个问题:当手机App在后台运行时,系统可能挂起BLE连接,导致状态不同步。解决方案是App在进入后台前主动断开连接,ESP32检测到连接断开后恢复快速广播,等待下次连接。
2.6 常见误区与总结
在BLE开发中,以下误区需要特别注意:
- 误区一:认为广播数据越多越好。广播数据过长会降低广播成功率,且Observer需要更多时间解析。建议广播数据控制在20字节以内,剩余空间留给扫描响应。
- 误区二:忽略MTU(Maximum Transmission Unit)大小。默认MTU为23字节,其中ATT头占3字节,实际数据仅20字节。如果传输大块数据,需要协商更大的MTU(ESP32支持最大517字节)。
- 误区三:混淆Notify和Indicate。Notify不保证送达,适合状态同步;Indicate保证送达,适合控制指令。在信号不稳定的环境中,Indicate更可靠。
- 误区四:不处理配对和绑定。对于智能家居设备,建议启用配对和加密,防止未授权设备连接。ESP32支持LE Secure Connections,使用ECDH密钥交换。