3. GATT 服务与特征结构归纳总结
GATT 服务和特征,是 BLE 开发里最基础也最关键的概念。说白了,你只要在跟蓝牙设备打交道,就离不开这两个东西。我记得刚接触 BLE 那会儿,对着 Core Spec 看了半天,脑子里全是「服务到底是个啥?特征又是个啥?」。后来做项目做多了,才慢慢理清楚——它俩其实就是一个树形结构,把设备的能力一层层暴露给外面。
这一章我们就系统地把这个结构拆开来看。我会结合自己做过的项目经验,帮你把每个零部件都捋清楚。
3.1 服务(Service)的定义与组成
服务是什么?它就是一个逻辑功能的集合。比如你做一个心率手环,会有「心率服务」、「电池服务」、「设备信息服务」——每个服务负责一块独立的功能。
每个服务用 UUID 来标识。标准服务用 16 位 UUID(比如心率服务的 UUID 是 0x180D),自定义服务就用 128 位 UUID。我早期有个项目,团队里有人直接把标准服务的 UUID 写错了两位,结果手机端死活连不上——嗯,从那以后我每次写 UUID 都会反复核对。
服务内部有两个核心组成部分:
- Include(包含引用):一个服务可以引用另一个服务。说白了就是「我这个服务里面包含了另一个服务的全部功能」。这种场景在复合设备里很常见。
- Characteristic(特征):服务的主体内容。服务可以包含零个或多个特征,数据交互最终都在特征上完成。
一句话总结: 服务 = UUID + Include 列表 + 特征集合。它是 BLE 数据组织的「文件夹」,特征才是真正的「文件」。
3.2 特征(Characteristic)的三要素
特征是 GATT 通信的真正载体。任何一个特征,都由三个部分组成:声明(Declaration)、值(Value)、描述符(Descriptors)。我习惯把它们叫做「特征三要素」。
3.2.1 特征声明与属性
特征声明本身也是一个属性,它的值包含了特征的 属性(Properties)、值的句柄以及特征的 UUID。属性是一个位掩码字段,决定了这个特征能干什么。
常用的属性值组合如下:
| 属性 | 值(十六进制) | 说明 | 典型场景 |
|---|---|---|---|
| Read | 0x02 | 客户端可以读取特征值 | 设备信息、状态查询 |
| Write | 0x08 | 客户端可以写入特征值 | 配置参数、控制指令 |
| Write Without Response | 0x04 | 写入无需应答,效率更高 | 大数据传输、实时控制 |
| Notify | 0x10 | 服务端主动推送,无需确认 | 实时数据上报(心率、电量) |
| Indicate | 0x20 | 服务端主动推送,需要确认 | 重要事件、可靠通知 |
我个人的习惯: 如果数据丢一两个没关系,用 Notify;如果每条都必须到达,用 Indicate。Notify 效率高,Indicate 可靠——鱼和熊掌的问题。
3.2.2 特征值
特征值是实际数据的存放位置。它的长度可以是固定的,也可以是可变的。比如一个电池电量特征,值长度就是 1 字节;而一个设备名称特征,值长度就可以变化。
特征值的格式由 UUID 和上层协议定义。标准特征有固定的格式规范(比如心率测量特征的格式在 HRS 规范里定义得很清楚),自定义特征就得你自己定规矩了。
// 一个典型特征声明的 ATT 数据流(举例)
// 特征声明属性句柄 0x0003,值指向句柄 0x0004
// Properties: Read + Notify (0x12)
// Value UUID: 0x2A37 (心率测量)
0x0003: 12 04 00 37 2A
// └─ 属性: 0x12 (Read | Notify)
// └─ 值句柄: 0x0004
// └─ UUID: 0x2A37
3.3 特征描述符(Descriptors)的作用
描述符是特征的「额外说明」。它不是必须的,但很多时候没有它功能就转不起来。最典型的就是 CCCD(Client Characteristic Configuration Descriptor,0x2902)。
我记得第一次做 BLE 项目时,配好服务和特征后发现手机收不到通知。查了两天资料,最后发现是忘了配 CCCD。你想想看,通知的开关就藏在这个描述符里——客户端要先往 CCCD 写 0x0001 才能开启通知。这是一个容易踩坑的点。
常见的描述符:
- CCCD (0x2902):控制 Notify/Indicate 的开关。客户端往它里面写值来订阅或取消订阅。
- CUD (0x2901):用户描述。一个 UTF-8 字符串,给特征起个友好的名字,方便调试。
- Format (0x2904):描述特征值的数据格式、单位、指数等。数值类特征建议配上。
避坑指南: 如果你的特征启用了 Notify 或 Indicate 属性,必须同时包含 CCCD 描述符。否则客户端无法订阅通知,功能就是废的。我曾经在一个量产项目中漏掉这个,烧了 500 片 PCB 才发现……嗯,不提了。
3.4 整体结构一览
把上面讲的串起来,GATT 的结构就是一棵倒着的树。我画了一张图,把核心节点和它们的关系展示出来,你看一眼就明白了:
从这个图里你能看到,整个 GATT 结构其实就四层。每一层有明确的职责,上一层通过「包含」或「引用」来组织下一层。我个人觉得,把这张图记在脑子里,BLE 开发的骨架就搭起来了。
3.5 避坑指南
做 GATT 服务定义时,有几个地方特别容易出问题。我这些年踩过的坑,给你整理一下:
- UUID 冲突: 标准服务一定要用蓝牙 SIG 定义的 UUID,别自己编一个 16 位的撞上了。自定义服务老老实实用 128 位 UUID。
- 属性与描述符不匹配: 启用了 Notify 就必须配 CCCD,这个前面说过了。另外,如果特征可读,记得在属性里加上 Read 标志,不然客户端读不到数据。
- 特征值长度不确定: 变长特征要处理好长度协商和溢出保护。我曾经遇到过一个设备,特征值最大长度声明为 20 字节,但实际数据有 30 字节——写进去就崩。
- 多服务之间的 Include 循环: 服务 A 包含服务 B,服务 B 又包含服务 A——这种死循环在规范里是禁止的,但复杂系统中容易不小心搞出来。
一个小建议: 在项目初期先用纸笔画清楚服务树。把每个服务的 UUID、包含关系、特征列表和属性都列出来。这一步花不了半小时,但能省掉后期大量的调试时间。我每个项目都这么做。
好了,第三章先到这儿。GATT 服务和特征的结构是整个 BLE 通信的地基,把这一层吃透了,后面讲读写流程和依赖注入边界时,你就能很自然地理解「边界在哪里」了。