1. GATT 基础回顾:协议栈架构与角色模型

做 BLE 开发的朋友,GATT 应该是打交道最多的一个层。我刚开始接触时,总觉得它就是个「属性读写器」,后来踩了几个坑才明白——GATT 的设计哲学直接决定了你产品的功耗、吞吐量和兼容性。

这一章我们快速过一遍基础。老手可以当复习,新手建议逐段看,后面优化章节会反复引用这些概念。

1.1 GATT 在蓝牙协议栈中的位置

先看一张整体架构图。GATT 跑在 ATT(Attribute Protocol)之上,而 ATT 又依赖 L2CAP 提供的数据通道。说白了,GATT 就是给 ATT 的「属性」赋予了业务含义。

关键认知: GATT 不是传输层,它是「数据组织层」。它不关心数据怎么分包、怎么重传,它只关心「这个数据属于哪个服务、哪个特征」。
蓝牙低功耗协议栈 — GATT 位置 Application (Profiles) GATT (Generic Attribute Profile) ATT (Attribute Protocol) L2CAP Link Layer + PHY 说明: • GATT 定义数据的「组织方式」——服务、特征、描述符的层级结构 • ATT 定义数据的「读写方式」——请求、响应、通知、指示等操作 • 上层 Profile 通过 GATT 接口访问底层属性,不直接操作 ATT PDU

这张图我建议你存下来。后面讲 MTU 优化、PDU 合并、通知批量发送时,你都得回来想清楚——你改的是哪一层?

1.2 Server / Client 角色模型

GATT 的角色划分很简单:

  • GATT Server — 持有属性数据的一方。通常是传感器、外设。
  • GATT Client — 发起读写请求的一方。通常是手机、网关。
我的经验: 很多人以为「Server 就是从机,Client 就是主机」。其实不对。一个设备可以同时是 Server 和 Client。比如手机连了手表,手机是 Client;但手机又把数据转发给云,手机对云来说又成了 Server。角色是按「谁持有数据」划分的,不是按连接角色。

举个例子。你做一个心率胸带:

  • 胸带是 GATT Server — 它存着心率值、电池电量、设备信息。
  • 手机 App 是 GATT Client — 它发请求读心率,或者订阅心率通知。

但胸带也可能需要读手机的当前时间。这时候胸带就临时充当 Client,手机变成 Server。嗯,这种场景在「时间同步」服务里很常见。

1.3 Profile / Service / Characteristic 层级关系

这是 GATT 最核心的数据组织方式。我习惯把它比作「文件系统」:

层级 类比 说明 UUID 示例
Profile 应用程序 一个或多个 Service 的集合,定义完整用例 心率 Profile (0x180D)
Service 文件夹 一组相关功能的集合,包含多个 Characteristic 电池服务 (0x180F)
Characteristic 文件 一个具体的数据点,包含值、属性、描述符 电池电量 (0x2A19)
Descriptor 文件属性 描述 Characteristic 的配置或元数据 CCCD (0x2902)
注意: Profile 在协议栈里不是一个实体。它只是规范文档里的概念。真正在 ATT 层注册的是 Service、Characteristic 和 Descriptor。我见过有人试图在代码里创建一个「Profile 对象」,结果发现 SDK 根本不支持——因为 Profile 不存在于属性表中。

再画一张层级关系图,帮你建立直观印象:

GATT 层级结构 — Profile / Service / Characteristic 心率 Profile (Heart Rate Profile) 心率服务 (0x180D) 电池服务 (0x180F) 心率测量 (0x2A37) 体感位置 (0x2A38) 控制点 (0x2A39) CCCD (0x2902) 报告参考 (0x2908) CCCD (0x2902) 层级规则: • 一个 Profile 包含 1 个或多个 Service • 一个 Service 包含 1 个或多个 Characteristic • 一个 Characteristic 包含值、属性、0 个或多个 Descriptor

1.4 Characteristic 的四个核心要素

每个 Characteristic 由四部分组成。我建议你把它刻在脑子里:

  1. 声明(Declaration) — 一个 ATT 属性,告诉 Client「这个 Characteristic 的 UUID、值句柄、属性是什么」。
  2. 值(Value) — 实际数据存放的地方。比如心率值 72 bpm。
  3. 属性(Properties) — 定义这个值支持哪些操作:读、写、通知、指示等。
  4. 描述符(Descriptors) — 附加信息,比如 CCCD(Client Characteristic Configuration Descriptor)控制通知开关。
避坑指南: 我曾经在一个产品里把 Characteristic 的属性设成了「可读 + 可通知」,但忘了加 CCCD。结果手机端死活收不到通知。查了两天才发现——没有 CCCD,Client 没法使能通知。这个坑我建议你记下来。

1.5 属性表与句柄

GATT Server 内部维护一张「属性表」。每个属性有一个唯一的句柄(Handle),从 0x0001 开始递增。Client 通过句柄来定位属性。

举个例子,一个简单的心率服务属性表长这样:

句柄 类型 (UUID) 值 权限
0x0001 Primary Service (0x2800) 0x180D 只读
0x0002 Characteristic (0x2803) 0x10 | 0x0003 | 0x2A37 只读
0x0003 Heart Rate Measurement (0x2A37) [0x00, 0x48] 可读 / 通知
0x0004 CCCD (0x2902) 0x0000 可读 / 可写

注意看句柄 0x0002 的 Characteristic 声明里,包含了值句柄 0x0003 和 UUID 0x2A37。这就是 Client 发现服务的依据——它先读声明,拿到值句柄,再去读实际数据。

我的习惯: 设计服务时,我会先画一张属性表草图,标清楚每个句柄的用途。这样写代码时不容易漏掉 CCCD,也方便后面做 MTU 优化时计算属性总长度。

1.6 本章小结

这一章我们覆盖了三个核心概念:

  • GATT 在协议栈中的位置 — 它跑在 ATT 之上,是数据组织层。
  • Server/Client 角色 — 谁持有数据谁是 Server,角色可以切换。
  • Profile/Service/Characteristic 层级 — 像文件系统一样组织数据。