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)
网关读取数据时,流程是这样的:
- 发现服务:遍历所有 Service
- 发现特征值:遍历每个 Service 下的 Characteristic
- 读取或订阅:读特征值,或者使能通知/指示
我记得有一次调试一个第三方传感器,它的温度特征值 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 在协议栈中的位置和关系:
从这张图可以看得很清楚:
- 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,跑通了再讲原理。效果比先啃协议栈文档好得多。