1. GATT 协议栈与特征模型
GATT(Generic Attribute Profile)是 BLE 协议栈中位于 L2CAP 之上的服务层协议,负责定义数据如何被组织、暴露和操作。理解 GATT 的角色结构、服务树、特征声明及句柄映射是进行特征操作的前提,也是依赖边界控制的基础。
1.1 GATT 角色
GATT 定义了两种通信角色:
| 角色 | 职责 | 典型设备 |
|---|---|---|
| GATT 客户端 (Client) | 发起操作请求,读取/写入/订阅特征 | 手机、平板、PC |
| GATT 服务器 (Server) | 暴露服务与特征,响应客户端请求 | 传感器、穿戴设备、外设 |
注意:一个设备可以同时作为 Client 和 Server(如网关设备),但操作开始时必须明确当前连接中的角色。
1.2 服务树与特征声明
GATT 服务器中的数据按层次组织为 服务(Service) → 特征(Characteristic) → 描述符(Descriptor)。每个对象由 UUID 标识,并分配一个 16 位句柄(Handle)作为唯一索引。
- Service:一组相关特征的集合。必须唯一 UUID(16/32/128位)。
- Characteristic:包含一个值声明(Value Declaration)和多个描述符。特征声明本身也是一个属性(Attribute)。
- Descriptor:描述特征属性的辅助数据,如客户端特征配置描述符(CCCD)用于使能通知/指示。
| 属性类型 | UUID(16位) | 说明 |
|---|---|---|
| Primary Service | 0x2800 | 声明一个主服务,属性值包含服务 UUID |
| Secondary Service | 0x2801 | 声明一个次服务(较少使用) |
| Characteristic Declaration | 0x2803 | 声明一个特征,属性值包含特征属性、值句柄、特征 UUID |
| Characteristic Value | 自定义 | 存储特征的当前数值 |
| Characteristic Descriptor | 0x2900 ~ 0x2904 | CCCD(0x2902)、扩展属性等 |
句柄映射:每个属性在服务器中按顺序分配递增的句柄。客户端通过句柄直接寻址属性。例如服务声明句柄 0x0001,其内部特征声明句柄 0x0002,特征值句柄 0x0003,CCCD 句柄 0x0004。
1.3 典型服务树结构
以下是一个 BLE 电池服务(Battery Service)的实例:
Handle Type Value
0x0010 Primary Service (0x2800) -> UUID = 0x180F (Battery Service)
0x0011 Characteristic Decl. (0x2803) -> Properties=Read+Notify, Handle=0x0012, UUID=0x2A19
0x0012 Battery Level (0x2A19) -> 0x64 (100%)
0x0013 Client Char. Config (0x2902) -> 0x0000 (未使能通知)
1.4 属性类型与访问模型
每个属性由以下四元组定义:
- 句柄:16位地址,唯一标识属性
- 类型:UUID,区分服务声明、特征声明、值、描述符等
- 权限:读/写/认证/授权/加密
- 值:字节数组,长度可变
GATT 操作(Read/Write/Notify/Indicate)直接作用于属性的值。依赖边界控制的核心在于特征声明的属性值(Properties)字段决定了客户端能否执行特定操作。
常见错误:特征声明属性字节写错会导致客户端无法发现特征,或通知无法触发。务必检查属性字段:Broadcast=0x01, Read=0x02, WriteNoRsp=0x04, Write=0x08, Notify=0x10, Indicate=0x20。
1.5 GATT 依赖边界与句柄映射图
1.6 实现流程:构建基础服务
以下为使用 ESP32 的 NimBLE 主机栈创建 GATT 服务的核心步骤:
// 1. 定义服务 UUID
static const ble_uuid128_t svc_uuid = BLE_UUID128_INIT(0x00,0x11,0x22,... );
// 2. 定义特征声明结构
static const struct ble_gatt_chr_def characteristic_defs[] = {
{
.uuid = &char_uuid.u,
.access_cb = char_access_callback,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
.val_handle = &char_val_handle,
},
{ 0 } // 结束标记
};
// 3. 定义服务
static const struct ble_gatt_svc_def services[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = &svc_uuid.u,
.characteristics = characteristic_defs,
},
{ 0 }
};
// 4. 注册服务
ble_gatts_add_svcs(services);
调试技巧:使用 nRF Connect 或 LightBlue 扫描设备,查看服务树是否完整,句柄是否连续,特征属性值是否正确。
1.7 性能与稳定性优化
- 服务树深度不宜过深(建议 ≤ 3 层),避免句柄碎片。
- 特征声明属性字节应合理设置,避免不必要的 Notify 权限导致广播包拥塞。
- 句柄映射在设备重启后应保持稳定(推荐静态分配句柄)。
- 每添加一个特征,句柄空间消耗约 2~3 个属性。合理规划总属性数 ≤ 200 个。
1.8 常见错误与排查
| 错误现象 | 根因 | 解决 |
|---|---|---|
| 客户端无法发现服务 | 服务声明 UUID 类型错误或句柄重复 | 检查服务注册 API 的 UUID 格式和句柄分配 |
| 特征值无法读取 | 特征声明属性未包含 Read 标志 | 设置 BLE_GATT_CHR_F_READ |
| 通知无响应 | CCCD 句柄不存在或未使能 | 添加 CCCD 描述符(0x2902)并在应用层处理配置写入 |
句柄映射检查:用 nRF Sniffer 抓包,确认 ATT Read By Type Request 返回的句柄值是否与代码中分配的句柄一致。若不一致,检查服务添加顺序或动态注册顺序。