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 Service0x2800声明一个主服务,属性值包含服务 UUID
Secondary Service0x2801声明一个次服务(较少使用)
Characteristic Declaration0x2803声明一个特征,属性值包含特征属性、值句柄、特征 UUID
Characteristic Value自定义存储特征的当前数值
Characteristic Descriptor0x2900 ~ 0x2904CCCD(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 依赖边界与句柄映射图

GATT 服务树与句柄映射 GATT Server (句柄空间 0x0001~0xFFFF) Service A (0x180F) Service B (0x180A) Char Decl h=0x0011 Char Value h=0x0012 CCCD h=0x0013 Char Decl h=0x0021 Char Value h=0x0022 CCCD h=0x0023 依赖边界控制点 客户端通过句柄直接访问特征值,但需通过 CCCD 使能通知/指示 依赖边界:特征声明的属性字段决定可操作性边界

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 返回的句柄值是否与代码中分配的句柄一致。若不一致,检查服务添加顺序或动态注册顺序。