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 服务与特征结构层次图 GATT Profile Service UUID + 包含关系 Include 引用其他 Service Characteristic 声明 + 值 + 描述符 Properties 读/写/通知/指示 Value 实际数据内容 Descriptors CCCD / CUD 等 说明:Service 是逻辑功能的集合,Characteristic 是数据交互的载体。 Include 实现了服务之间的引用,Descriptors 为特征提供配置和描述信息。

从这个图里你能看到,整个 GATT 结构其实就四层。每一层有明确的职责,上一层通过「包含」或「引用」来组织下一层。我个人觉得,把这张图记在脑子里,BLE 开发的骨架就搭起来了。

3.5 避坑指南

做 GATT 服务定义时,有几个地方特别容易出问题。我这些年踩过的坑,给你整理一下:

  • UUID 冲突: 标准服务一定要用蓝牙 SIG 定义的 UUID,别自己编一个 16 位的撞上了。自定义服务老老实实用 128 位 UUID。
  • 属性与描述符不匹配: 启用了 Notify 就必须配 CCCD,这个前面说过了。另外,如果特征可读,记得在属性里加上 Read 标志,不然客户端读不到数据。
  • 特征值长度不确定: 变长特征要处理好长度协商和溢出保护。我曾经遇到过一个设备,特征值最大长度声明为 20 字节,但实际数据有 30 字节——写进去就崩。
  • 多服务之间的 Include 循环: 服务 A 包含服务 B,服务 B 又包含服务 A——这种死循环在规范里是禁止的,但复杂系统中容易不小心搞出来。

一个小建议: 在项目初期先用纸笔画清楚服务树。把每个服务的 UUID、包含关系、特征列表和属性都列出来。这一步花不了半小时,但能省掉后期大量的调试时间。我每个项目都这么做。

好了,第三章先到这儿。GATT 服务和特征的结构是整个 BLE 通信的地基,把这一层吃透了,后面讲读写流程和依赖注入边界时,你就能很自然地理解「边界在哪里」了。