GATT:BLE 应用开发的核心
1. GATT 到底是什么
GATT(Generic Attribute Profile)定义了如何用属性表示服务、特征值和描述符,并规定发现、读取、写入、通知、指示等操作。
GATT Server 内部维护一张属性表。表中的每个 Attribute 都具有:
- Handle:当前数据库中的 16 位索引,连接期间用于快速访问;
- Type:通常由 UUID 标识;
- Value:属性内容;
- Permissions:是否允许读取或写入,是否要求加密、认证或授权。
应用开发时通常使用 SDK 抽象出的 Service、Characteristic 和 Descriptor,不需要手工构造每一条底层 Attribute,但理解属性表有助于排查 Handle、权限和缓存问题。
2. Service、Characteristic、Descriptor
2.1 Service
Service 是一组相关功能的集合。标准服务具有 Bluetooth SIG 分配的 UUID,例如 Device Information Service;厂商业务通常定义 128 位自定义 UUID。
Primary Service 表示设备的主要功能,Secondary Service 通常被其他服务引用。绝大多数自定义产品服务使用 Primary Service。
2.2 Characteristic
Characteristic 是最常接触的业务数据单元。它包含一个值,以及描述这个值能如何被访问的 Properties。
2.3 Descriptor
Descriptor 用于补充描述特征值。其中最常见的是 CCCD(Client Characteristic Configuration Descriptor,UUID 0x2902),客户端通过写它来开启或关闭 Notify/Indicate。
常见误区
Characteristic 声明支持 Notify,并不代表连接后会自动上报。客户端必须先订阅;协议层实际表现通常是客户端写入 CCCD。不同连接的订阅状态应分别管理。
3. GATT 数据结构的真实样子
从应用层看,GATT 数据库是一棵 Service → Characteristic → Descriptor 的树;从 ATT 层看,它实际是一张按 Handle 排列的 Attribute 表。
一个 Characteristic 并不是属性表中的单独一行。它至少由 Characteristic Declaration 和 Characteristic Value 两个 Attribute 组成;如果支持 Notify 或 Indicate,后面还会有 CCCD。
下面是一张简化属性表:
| Handle | Attribute Type | Attribute Value | 含义 |
|---|---|---|---|
0x0020 | 0x2800 Primary Service | 自定义 Service UUID | 一个服务从这里开始 |
0x0021 | 0x2803 Characteristic Declaration | Properties + Value Handle 0x0022 + Characteristic UUID | 描述下一个特征值 |
0x0022 | 自定义 Characteristic UUID | 当前传感器数值 | 真正被 Read 或 Notify 的值 |
0x0023 | 0x2902 CCCD | 0x0000、0x0001 或 0x0002 | 当前客户端的订阅配置 |
0x0024 | 0x2803 Characteristic Declaration | Properties + Value Handle 0x0025 + Characteristic UUID | 下一个特征值声明 |
0x0025 | 自定义 Characteristic UUID | 当前配置值 | 真正被 Read/Write 的值 |
客户端执行服务发现,本质上就是通过 ATT 查询这张表:先找到 Service 的 Handle 范围,再找其中的 Characteristic Declaration,最后根据需要查找 Descriptor。之后的读写和通知使用发现到的 Value Handle。
3.1 CCCD 的值
CCCD 通常使用以下值:
| 值 | 含义 |
|---|---|
0x0000 | 关闭 Notify 和 Indicate |
0x0001 | 开启 Notify |
0x0002 | 开启 Indicate |
CCCD 表示客户端对当前连接的配置,不是设备全局开关。多连接设备需要分别保存各连接的订阅状态。
4. UUID 和 Handle 不要混用
UUID
UUID 表示“这是什么”。标准对象常用 16 位 UUID,自定义业务一般使用 128 位 UUID。
Handle
Handle 表示“它在这张属性表的哪个位置”。Handle 由 Server 的 GATT 数据库布局决定,不是跨固件版本稳定的业务标识。
因此 App 应通过 UUID 发现目标特征值,并把本次发现得到的对象或 Handle 交给系统 API。不要在协议文档里把某个测试时看到的 Handle 写死成永久接口。
5. Characteristic Properties 与 Permissions
Properties 描述支持哪些 GATT 操作,Permissions 决定执行操作需要什么访问条件。两者缺一不可。
| Property | 含义 | 常见用途 |
|---|---|---|
| Read | 客户端主动读取 | 版本、当前配置、静态信息 |
| Write | 写入并等待协议层响应 | 控制命令、重要配置 |
| Write Without Response | 写入时不等待 ATT 响应 | 可容忍丢失或自带确认的高频数据 |
| Notify | Server 主动发送,无 ATT 确认 | 传感数据、状态变化、数据流 |
| Indicate | Server 主动发送,有 ATT 确认 | 需要协议层确认的重要事件 |
典型错误是特征值只配置了 Write Without Response,而 App 调用 Write with Response;或只配置 Notify,却没有 CCCD。此时 API 往往报告 property not supported。
6. Read、Write、Notify、Indicate 如何选择
ATT 层有响应不等于业务执行成功。例如“开锁命令写入成功”只能说明 Server 接收了写请求,不能证明锁已经打开。重要命令应定义应用层响应:
App -> Device: SET_STATE, request_id=42, target=ON
Device -> App: RESPONSE, request_id=42, result=OK, actual=ON7. 一个实用的 GATT 设计示例
假设设计一台带传感器和 OTA 的设备:
标准服务
- Device Information Service:厂商、型号、序列号、硬件和固件版本;
- Battery Service:电量百分比(电池产品可考虑)。
自定义控制服务
| 特征值 | Properties | 方向 | 用途 |
|---|---|---|---|
| Command | Write | App → Device | 可靠下发命令 |
| Response | Notify | Device → App | 命令执行结果 |
| Telemetry | Notify | Device → App | 传感数据与异步事件 |
| Configuration | Read + Write | 双向 | 少量静态配置 |
自定义 OTA 服务
| 特征值 | Properties | 用途 |
|---|---|---|
| OTA Control | Write + Notify | 开始、结束、校验和结果 |
| OTA Data | Write Without Response | 固件数据块 |
把高频数据和低频控制拆开,可以分别设置队列、权限和流控策略。不要为了省 UUID 把所有数据都塞进一个特征值而不给包定义。
8. MTU 与单次有效载荷
ATT_MTU 表示 ATT PDU 的最大尺寸。对常见的 Write、Notify 等操作,应用单包可用数据长度通常可按下式估算:
单次特征值有效载荷 ≈ ATT_MTU - 3默认 ATT_MTU 常见为 23,对应 20 字节特征值数据。连接后双方可交换 MTU,但最终值受两端协议栈和系统限制,业务层不能假定协商一定达到期望值。
还要区分:
- ATT_MTU;
- Link Layer Data Length;
- PHY 速率;
- 应用层每包数据长度。
它们都会影响吞吐量,但不是同一个参数。即使 MTU 很大,SDK 缓冲区、连接间隔、每个连接事件允许的包数以及手机调度仍可能成为瓶颈。
9. 服务发现与 GATT 缓存
客户端连接后通常执行服务发现。手机系统可能缓存 GATT 数据库以加速重连。如果固件升级后修改了服务、特征值或 Handle 布局,旧缓存可能让 App 看到过时结构。
产品设计时应:
- 尽早稳定 GATT 接口;
- 正确实现 Generic Attribute Service 中的 Service Changed 机制及相关缓存能力;
- 在开发阶段遇到结构不一致时,尝试清除绑定、系统缓存或重新安装 App;
- 不要把“重启蓝牙”当作产品级缓存方案。
10. 回调里应该做什么
协议栈回调通常运行在受限制的线程或任务中。推荐只完成快速工作:
- 复制收到的数据;
- 校验基本长度;
- 投递到业务队列;
- 尽快返回。
不要在回调中长时间阻塞、写 Flash、执行复杂业务或等待另一个 BLE 操作。否则可能造成协议栈超时、队列拥堵甚至看门狗复位。
11. 本篇小结
GATT Server 对外呈现 Service、Characteristic 和 Descriptor,内部则是一张由 Handle 索引的 Attribute 表。UUID 定义对象身份,Handle 定位本次数据库中的属性;Properties 定义操作能力,Permissions 定义访问条件;Notify/Indicate 需要客户端通过 CCCD 订阅。理解这张表以后,服务发现、读写和通知就不再是几组孤立的 API。
