1. GATT协议基础回顾:从BLE协议栈看GATT层的位置与作用
做物联网无线开发的朋友,对BLE肯定不陌生。但很多人一上来就扎进GATT的细节里,结果越学越乱。我个人习惯是,先退一步,看看整个协议栈的架构。你想想看,GATT在BLE协议栈里到底扮演什么角色?
说白了,GATT(Generic Attribute Profile)是建立在ATT(Attribute Protocol)之上的一个高层框架。它定义了数据如何组织、如何交互。没有GATT,你的BLE设备就是一盘散沙——数据能发出去,但接收方根本不知道这些数据是什么意思。
核心观点:GATT是BLE应用层的"语法规则"。它规定了数据必须以Service、Characteristic、Descriptor的层级结构来组织。违反这个规则,你的设备就无法与标准BLE终端(如手机)正常通信。
1.1 BLE协议栈中GATT的位置
先看一张图,你就明白了。
从这张图可以清楚看到,GATT层紧挨着应用层。它不直接管无线收发,那是Link Layer和PHY的事。GATT只做一件事:把应用层的数据请求,翻译成ATT能理解的属性操作。
我记得刚做BLE项目时,有个同事直接在应用层拼ATT指令,结果代码乱成一锅粥。后来我建议他用GATT框架重写,结构清晰多了,调试也方便。嗯,这里要注意:不要绕过GATT直接操作ATT,除非你很清楚自己在做什么。
1.2 GATT角色:Server与Client
GATT定义了两种角色:Server和Client。这个模型很简单,但很多人搞反了。
| 角色 | 职责 | 典型设备 | 数据流向 |
|---|---|---|---|
| GATT Server | 存储数据(属性),响应读写请求,主动通知 | 传感器、智能灯、心率带 | 数据从Server流向Client |
| GATT Client | 发起读写请求,接收通知/指示 | 手机App、网关、树莓派 | 数据从Client流向Server(请求) |
举个例子:你用一个智能手环测心率。手环是GATT Server,它存储心率数据。手机App是GATT Client,它发起"读取心率"的请求。手环收到请求后,把心率值返回给手机。
避坑指南:我曾经在一个项目中,把网关配成了GATT Server,传感器配成了Client。结果传感器不断发起连接请求,网关被动响应,功耗高得离谱。后来才意识到,传感器应该做Server,网关做Client才对。记住:数据提供方是Server,数据消费方是Client。
1.3 Profile层级结构:Service、Characteristic、Descriptor
这是GATT最核心的部分。很多初学者看到Service、Characteristic、Descriptor就头大。我换个说法,你一听就懂。
想象一个文件柜:
- Service = 一个抽屉(比如"心率服务")
- Characteristic = 抽屉里的文件夹(比如"心率测量值")
- Descriptor = 文件夹上的标签(比如"单位是bpm"、"支持通知")
每个Service包含一个或多个Characteristic。每个Characteristic可以包含零个或多个Descriptor。这就是GATT的层级结构。
1.4 深入理解三个核心概念
Service(服务)
Service是一组相关功能的集合。每个Service有一个唯一的UUID标识。蓝牙SIG定义了很多标准Service,比如:
- 0x180D — 心率服务(Heart Rate Service)
- 0x180F — 电池服务(Battery Service)
- 0x181A — 环境感知服务(Environmental Sensing Service)
你也可以自定义Service,用128位UUID。我在项目中经常用自定义Service来封装私有协议,比如OTA升级服务、设备配置服务。
Characteristic(特征)
Characteristic是实际数据的载体。它包含三个部分:
- 声明(Declaration) — 固定格式,包含属性、句柄、UUID
- 值(Value) — 实际数据,比如心率值87bpm
- 描述符(Descriptors) — 附加信息,比如单位、配置
Characteristic还有一组属性(Properties),决定了它能做什么:
| 属性 | 含义 | 典型用途 |
|---|---|---|
| Read | Client可以读取该值 | 设备信息、固件版本 |
| Write | Client可以写入该值 | 控制指令、配置参数 |
| Notify | Server主动推送值(无确认) | 实时传感器数据 |
| Indicate | Server主动推送值(有确认) | 重要事件、告警 |
注意:Notify和Indicate的区别在于可靠性。Notify不要求Client确认,速度快但可能丢包。Indicate要求Client回复确认,可靠但慢。我曾经在一个心率监测项目中,用了Indicate,结果手机端处理不过来,导致数据堆积。后来改成Notify,配合应用层校验,问题就解决了。选哪个,要看你的业务场景。
Descriptor(描述符)
Descriptor提供Characteristic的额外信息。最常见的Descriptor是CCCD(Client Characteristic Configuration Descriptor),UUID为0x2902。它用来使能或禁用Notify/Indicate。
举个例子:
// 伪代码:Client使能心率通知
// 1. 发现心率服务的CCCD描述符
// 2. 写入0x0001使能通知
// 3. 之后Server就会主动推送心率数据
// CCCD值含义:
// 0x0000 — 禁用通知和指示
// 0x0001 — 使能通知
// 0x0002 — 使能指示
// 0x0003 — 同时使能通知和指示
其他常见Descriptor还有:
- 0x2901 — Characteristic User Description(用户描述,比如"心率值")
- 0x2904 — Characteristic Presentation Format(数据格式,比如uint16、浮点数)
1.5 实战中的GATT设计原则
说了这么多理论,来点实际的。我在做GATT服务设计时,有几个原则一直遵守:
- Service粒度要适中 — 一个Service不要塞太多Characteristic。我见过有人把20多个Characteristic塞进一个Service,调试时找属性找到崩溃。一般一个Service放3-5个Characteristic比较合理。
- 合理使用Notify — 高频数据用Notify,低频数据用Read。比如加速度计数据(100Hz)用Notify,设备温度(1Hz)用Read。
- UUID命名规范 — 自定义UUID要有规律。我习惯用固定前缀+序号,比如
AAAA-0001、AAAA-0002,方便代码生成和文档管理。 - 预留扩展位 — 在Service里留几个保留Characteristic,万一后期要加功能,不用重新设计整个Profile。
总结一下:GATT是BLE应用层的骨架。理解Server/Client角色,掌握Service-Characteristic-Descriptor的层级结构,是做好BLE开发的基础。下一节我们会深入GATT的通信流程,看看数据到底是怎么在Server和Client之间流转的。