1. GATT协议基础:GATT与GAP的关系、GATT角色与Attribute Protocol

各位同学,咱们今天正式进入GATT的世界。说实话,我刚接触蓝牙协议栈那会儿,最头疼的就是分不清GATT和GAP到底谁管谁。后来被坑了几次才明白——GAP管的是“能不能连”,GATT管的是“连上以后能干啥”。

打个比方:GAP就像门卫,负责发现设备、建立连接、管理安全。GATT呢,就像进了门以后的服务员,告诉你“这里有温度计、有开关、有电池信息,你想看哪个?”

嗯,这个比喻我用了好多年,每次带新人我都这么讲。今天咱们就把GATT的底裤扒干净。

1.1 GATT与GAP:分工明确的兄弟俩

先看一张图,把这两个协议栈层的关系理清楚:

蓝牙协议栈中 GAP 与 GATT 的分工 GAP 层(通用访问规范) 管“能不能连” 设备发现(Advertising / Scanning) 连接建立与参数协商 安全管理(配对 / 绑定 / 加密) 角色定义(Central / Peripheral) 连接间隔 / 延迟 / 超时管理 MTU 大小协商(部分) GATT 层(通用属性规范) 管“连上以后能干啥” Service / Characteristic 定义 属性协议(Attribute Protocol) 数据读写(Read / Write / Notify) Server / Client 角色交互 属性分组与 UUID 管理 CCC(客户端特征配置) 依赖 GAP 建立连接后,GATT 才能开始数据交互。两者缺一不可。

你看,GAP在底层把连接通道建好,GATT在上层用这个通道做数据交换。我见过不少新手在调试时发现GATT读写失败,结果查了半天是GAP的连接参数没配好——连接间隔设得太长,导致数据交互超时。所以这两个层虽然分工不同,但调试时得一起看。

核心记忆点:

  • GAP = 能不能连、怎么连、安全不安全
  • GATT = 连上以后能读什么、写什么、收什么通知
  • GATT 依赖 GAP 建立的连接,但 GAP 不关心 GATT 的数据内容

1.2 GATT 角色:Server 与 Client

GATT定义了两个角色,说白了就是“谁提供数据,谁消费数据”。

角色 职责 典型设备 我踩过的坑
GATT Server 存储属性数据,响应读写请求,主动发送通知 传感器、心率带、温控器 Server 端忘记配置 CCC,导致 Client 收不到 Notify
GATT Client 发现服务、读写属性、订阅通知 手机 App、网关、中心节点 Client 端没有等 Service Discovery 完成就发读写,直接失败

这里有个容易混淆的点:GAP的角色(Central/Peripheral)和GATT的角色(Server/Client)是正交的。什么意思?

  • 一个 Peripheral 设备可以是 GATT Server(最常见,比如蓝牙温度计)
  • 一个 Peripheral 设备也可以是 GATT Client(比如一个蓝牙遥控器去读电视的状态)
  • 一个设备可以同时是 Server 和 Client(比如手机既是 Server 提供电量服务,又是 Client 读手环数据)

我记得有一次做智能门锁项目,门锁作为 Peripheral + GATT Server,手机作为 Central + GATT Client。一切正常。后来客户要求门锁也能读取云端配置——好嘛,门锁得同时当 Client 去连手机。这时候角色就乱了,一个连接里只能有一个 Server 和一个 Client,所以得建两个连接。嗯,这个坑我替你们踩过了。

我的调试习惯:在代码里用宏定义明确标注当前设备在 GAP 和 GATT 中的角色,比如:

#define GAP_ROLE      PERIPHERAL
#define GATT_ROLE     SERVER

这样看代码时一目了然,不会搞混。

1.3 Attribute Protocol:GATT 的基石

GATT 不是凭空造出来的,它建立在 Attribute Protocol(ATT) 之上。说白了,ATT 定义了“怎么读写一个个属性”,GATT 定义了“这些属性怎么组织成服务”。

每个属性(Attribute)由四个要素组成:

要素 说明 我见过的翻车现场
Attribute Handle 属性的唯一编号,16位,0x0001~0xFFFF Handle 重复导致服务发现失败,芯片直接复位
Attribute Type (UUID) 标识属性是什么,16位/128位 UUID UUID 写错一个字节,手机 App 识别不出服务
Attribute Permissions 读/写/加密/认证权限 权限设成“仅加密可读”,但连接没加密,Client 读回来全是 0
Attribute Value 实际数据,最长 512 字节(单包) Value 超过 MTU 没做分包,直接丢包

ATT 的通信模式很简单:Client 发请求,Server 回响应。但也有例外——Server 可以主动发 Notification 或 Indication,不需要 Client 先问。这个机制在低功耗场景下特别有用,传感器数据变了直接推,不用轮询。

注意:ATT 的 MTU(最大传输单元)默认只有 23 字节,其中 3 字节是 ATT 头部,实际数据只有 20 字节。如果你要传超过 20 字节的数据,要么协商更大的 MTU(最高 512 字节),要么做分包。我曾经有个项目没做 MTU 协商,直接传 100 字节,结果 Client 端只收到前 20 个字节,后面全丢了。查了两天才发现是 MTU 的问题。

1.4 属性分组:从 Attribute 到 Service

GATT 在 ATT 的基础上做了分组。单个 Attribute 太零散了,GATT 把它们组织成三级结构:

GATT 属性分组结构 Service(服务) 例如:电池服务 (0x180F)、心率服务 (0x180D) 包含一个或多个 Characteristic Characteristic(特征) 例如:电池电量百分比、心率测量值 包含:声明 + Value + 可选的 Descriptor Descriptor(描述符) 例如:CCC (0x2902) 控制 Notify 开关、用户描述 (0x2901)

这个结构你想想看,是不是很像文件系统?Service 是文件夹,Characteristic 是文件,Descriptor 是文件的属性。手机 App 连上设备后,第一步就是遍历这个树形结构——也就是 Service Discovery。

我个人习惯在开发阶段把 Service 和 Characteristic 的 UUID 打印出来,对照蓝牙规范手册检查。有一次我发现心率服务的 UUID 写成了 0x180F(电池服务),结果手机 App 怎么都读不到心率数据。嗯,这种低级错误,犯过一次就长记性了。

GATT 分组的好处:

  • 可发现性:Client 不需要提前知道设备有什么数据,连上后遍历就行
  • 可扩展性:新增一个 Service 不影响已有的 Service
  • 标准化:蓝牙 SIG 定义了大量标准 Service,不同厂家的设备可以互操作

1.5 本章小结

咱们这一章把 GATT 的骨架搭起来了:

  • GAP vs GATT:一个管连接,一个管数据,别搞混
  • Server vs Client:谁提供数据,谁消费数据,角色可以叠加但一个连接里只能各有一个
  • Attribute Protocol:Handle + UUID + 权限 + Value,四个要素缺一不可
  • 属性分组:Service → Characteristic → Descriptor,树形结构方便发现和管理