4、蓝牙协议栈基础:GAP、GATT、ATT 核心概念

做蓝牙网关开发,绕不开协议栈。很多新手一上来就扎进代码,结果被广播、连接、服务、特征值这些概念搞得晕头转向。我刚开始接触蓝牙时也踩过这个坑——调了三天连不上设备,最后发现是 GAP 角色没配对。

这一章,咱们就把 GAP、GATT、ATT 这三个核心层掰开揉碎。你理解透它们,后面写网关固件会顺手很多。

4.1 GAP——广播与连接的门面

GAP(Generic Access Profile),说白了就是控制设备怎么被发现、怎么建立连接的。它定义了四种角色:

角色 说明 典型设备
Broadcaster 只发广播,不连接 信标、温湿度传感器
Observer 只扫描广播,不连接 扫描仪、网关监听模式
Peripheral 可被连接,通常发广播 手环、智能锁
Central 主动扫描并发起连接 手机、蓝牙网关

网关通常扮演 Central 角色。它扫描周围的 Peripheral 设备,然后发起连接。但这里有个细节——广播包不是无限发的。BLE 广播有三个信道:37、38、39。设备在这三个信道上轮流发广播包,间隔可配。

广播间隔怎么设?

我个人的习惯是:

  • 需要快速发现的设备:设 20-50ms,但功耗高
  • 电池供电的传感器:设 500ms-1s,省电
  • 网关扫描窗口:至少覆盖两个广播间隔,否则容易丢包

举个例子。你有一个温湿度传感器,每 5 秒上报一次数据。广播间隔设 1 秒,网关扫描窗口设 2 秒。这样网关在 2 秒内至少能抓到一次广播,然后发起连接读取数据。

避坑指南

我曾经在一个项目中,把广播间隔设成了 10ms,结果网关扫描窗口只有 50ms。看起来没问题对吧?但实际测试发现,广播信道 37 和 38 上干扰严重,丢包率高达 30%。后来我把广播间隔调到 100ms,扫描窗口调到 150ms,问题才解决。记住:广播间隔不是越小越好,要考虑信道冲突。

4.2 GATT——服务与特征值的组织方式

连接建立之后,数据怎么交换?这就轮到 GATT(Generic Attribute Profile)上场了。

GATT 定义了一种树形结构:

  • Service(服务):一组相关功能的集合。比如「电池服务」、「设备信息服务」。
  • Characteristic(特征值):具体的数据点。比如「电池电量百分比」、「设备序列号」。
  • Descriptor(描述符):对特征值的补充说明。比如「客户端特征配置描述符(CCCD)」用于使能通知。

你想想看,这就像文件系统:Service 是文件夹,Characteristic 是文件,Descriptor 是文件的属性。

一个典型的 GATT 结构示例

设备
├── 通用访问服务 (0x1800)
│   └── 设备名称特征值 (0x2A00)
├── 电池服务 (0x180F)
│   └── 电池电量特征值 (0x2A19)
│       └── 客户端特征配置描述符 (0x2902)
└── 自定义数据服务 (自定义 UUID)
    ├── 温度特征值 (自定义 UUID)
    │   └── 客户端特征配置描述符 (0x2902)
    └── 湿度特征值 (自定义 UUID)
        └── 客户端特征配置描述符 (0x2902)

网关读取数据时,流程是这样的:

  1. 发现服务:遍历所有 Service
  2. 发现特征值:遍历每个 Service 下的 Characteristic
  3. 读取或订阅:读特征值,或者使能通知/指示

我记得有一次调试一个第三方传感器,它的温度特征值 UUID 写错了,我按标准 UUID 去读,死活读不到。后来用抓包工具一看,才发现它用了自定义 UUID。所以,拿到新设备时,先用手机 App 或抓包工具确认一下 GATT 结构,别盲目写代码。

4.3 ATT——属性协议,数据交换的底层机制

GATT 定义了「存什么」,ATT(Attribute Protocol)定义了「怎么存、怎么取」。

ATT 的核心是属性(Attribute)。每个属性有四个要素:

要素 说明 示例
句柄(Handle) 属性的唯一编号 0x0001, 0x0002
类型(UUID) 标识属性是什么 0x2A19(电池电量)
权限(Permissions) 可读、可写、需要加密等 读 + 通知
值(Value) 实际数据 0x64(100%)

ATT 定义了六种 PDU(协议数据单元)类型,但最常用的就三种:

  • Read Request / Response:网关主动读数据
  • Write Request / Response:网关写数据,需要确认
  • Handle Value Notification:设备主动推数据,不需要确认

注意:通知 vs 指示

通知(Notification)不需要应用层确认,速度快但不可靠。指示(Indication)需要设备确认,可靠但慢。网关采集高频数据时,我建议用通知;如果是关键控制指令,用指示更安全。

为什么会这样?因为 ATT 层在 L2CAP 之上,L2CAP 只保证包不丢,但不保证顺序和重传。通知模式下,如果链路层丢包,应用层是不知道的。指示模式则多了一层确认机制。

4.4 三者关系:一张图看懂

下面这张图展示了 GAP、GATT、ATT 在协议栈中的位置和关系:

蓝牙协议栈核心层关系图 应用层(Profiles & Applications) GAP 广播、扫描、连接管理 GATT 服务、特征值、描述符 ATT 属性读写、通知、指示 L2CAP Link Layer + PHY GAP 管连接前 GATT/ATT 管连接后

从这张图可以看得很清楚:

  • GAP 负责设备发现和连接建立,工作在连接之前
  • GATT 构建在 ATT 之上,定义了数据的组织方式
  • ATT 是真正的数据搬运工,负责属性的读写操作
  • 下层是 L2CAP 和 Link Layer,负责数据包的分段、重组和无线传输

4.5 网关开发中的实战要点

讲完理论,说点实际的。你在写网关固件时,这几个点要特别注意:

实战要点一:MTU 协商

ATT 层默认 MTU 是 23 字节,其中 3 字节是 ATT 头,实际数据只有 20 字节。如果你要传输超过 20 字节的数据(比如固件升级包),必须协商更大的 MTU。我建议在连接建立后立即发起 MTU 请求,一般能协商到 247 字节。

实战要点二:连接参数更新

GAP 层允许 Central 和 Peripheral 协商连接间隔、延迟和超时时间。网关作为 Central,可以根据业务场景动态调整:

  • 数据采集密集时:连接间隔设 7.5ms-15ms
  • 设备待机时:连接间隔设 100ms-500ms

我曾经在一个项目中,网关同时连接了 20 个传感器,连接间隔都设成 7.5ms,结果 L2CAP 层频繁丢包。后来把非活跃设备的连接间隔调到 200ms,问题才解决。

实战要点三:多连接管理

网关通常要同时管理多个 BLE 连接。每个连接都有独立的 GATT 上下文,包括服务发现结果、特征值句柄缓存、通知使能状态。我建议用连接句柄(Connection Handle)作为索引,维护一个连接状态表。否则,你很容易搞混哪个数据来自哪个设备。

4.6 小结

这一章我们聊了 GAP、GATT、ATT 三个核心层。GAP 管广播和连接,GATT 管数据怎么组织,ATT 管数据怎么传输。三者配合,构成了 BLE 通信的基础。

嗯,这里要注意:不要试图一次性理解所有细节。先跑通一个简单的数据读取流程,再慢慢深入。我在带团队时,总是让新人先写一个扫描+连接+读特征值的 demo,跑通了再讲原理。效果比先啃协议栈文档好得多。