1. GATT 基础概念回顾:Service、Characteristic、UUID、Profile 的关系与区别

做蓝牙联调,尤其是 GATT 这一层,很多人一上来就对着 log 发懵。什么 Service 句柄、Characteristic 属性、UUID 不匹配……说白了,就是这几个基础概念没理清。我刚开始做 BLE 项目时也踩过这个坑,今天咱们就把它们彻底掰扯清楚。

1.1 从 Profile 说起——它其实是个“协议篮子”

Profile 不是代码里的一个结构体,它是一份规范文档。比如心率 Profile(HRS)、电池服务 Profile(BAS),它们规定了设备要暴露哪些 Service、这些 Service 里有什么 Characteristic、数据怎么传。

你可以把 Profile 想象成一个“协议篮子”:

  • Profile:装了一堆 Service 的篮子
  • Service:篮子里的小格子,每个格子代表一类功能
  • Characteristic:格子里的小抽屉,抽屉里放着具体数据
  • UUID:每个格子、每个抽屉上的标签

嗯,这个比喻我用了好多年,带新人时屡试不爽。

一句话总结:Profile 是“文档”,Service 是“功能模块”,Characteristic 是“数据点”,UUID 是“身份证”。

1.2 Service——功能模块的“容器”

Service 是一个逻辑分组。比如一个心率计设备,它会有个“心率服务”(Heart Rate Service),里面放心率值、传感器位置、控制点等数据。

每个 Service 都有一个 UUID 标识。标准服务用 16-bit UUID(比如 0x180D 就是心率服务),自定义服务用 128-bit UUID。

我记得有一次联调,对方设备死活连不上我的 Service。查了半天,发现他们把 Service UUID 写成了 0x180D,但实际用的是自定义心率服务,UUID 应该是 128-bit 的。这种低级错误,log 里根本看不出来,只能对着 spec 一行行对。

避坑指南:联调前先确认双方用的是标准 UUID 还是自定义 UUID。标准 UUID 在 Bluetooth SIG 官网上可查,自定义 UUID 必须双方约定好,不能随便编。

1.3 Characteristic——真正装数据的地方

Characteristic 是 GATT 通信的核心。它包含三个部分:

  • 声明(Declaration):固定格式,告诉对端这个 Characteristic 的属性、值句柄、UUID
  • 值(Value):实际数据,比如心率值 72 bpm
  • 描述符(Descriptors):可选,比如客户端特征配置描述符(CCCD),用来使能通知或指示

我见过不少新手把 Characteristic 的 Value 和 Descriptor 搞混。举个例子:你想让设备主动发数据,需要配 CCCD(0x2902),但有人直接在 Value 里写了个 0x0001,结果对端根本收不到通知。为什么?因为 CCCD 是 Descriptor,不是 Value 的一部分。

注意:Characteristic 的 Value 长度有限制(通常 20 字节,取决于 MTU)。超过这个长度,要么拆包,要么用长读/长写。别指望一个 Characteristic 能塞下整张图片。

1.4 UUID——蓝牙世界的“身份证号”

UUID 是 128-bit 的唯一标识符。标准服务/特征用 16-bit 或 32-bit 的“缩写”,但底层还是 128-bit。比如:

类型 示例 UUID 说明
标准 Service 0x180D 心率服务
标准 Characteristic 0x2A37 心率测量值
自定义 Service 0000FFF0-0000-1000-8000-00805F9B34FB 厂商自定义
标准 Descriptor 0x2902 客户端特征配置

联调时最常见的坑就是 UUID 不匹配。你写了个 0xFFF1,对方以为是 0xFFF2,结果 Attribute 找不到,直接报错。我曾经花了一下午排查,最后发现是文档里 UUID 写错了一位。

我的习惯:联调前先把双方用的 UUID 列表打印出来,逐条核对。别信文档,信代码。代码里写的是什么,就是什么。

1.5 它们的关系——一张图说清楚

下面这张图展示了 Profile、Service、Characteristic、UUID 的层级关系。你想想看,一个设备可以有多个 Profile,每个 Profile 包含多个 Service,每个 Service 包含多个 Characteristic,每个 Characteristic 有自己的 UUID。

GATT 层级结构图 Profile(协议篮子) 例如:心率 Profile (HRS)、电池 Profile (BAS) Service(功能模块) UUID: 0x180D(心率服务) 包含多个 Characteristic Characteristic(数据点) UUID: 0x2A37(心率测量值) 声明 (Declaration) 值 (Value) 描述符 (Descriptors) 每个节点都有唯一的 UUID 标识,标准 UUID 为 16-bit,自定义为 128-bit

1.6 联调时最容易犯的错

根据我这些年的经验,GATT 联调出问题,十有八九是下面这几个原因:

  1. UUID 写错——大小写、字节序、位数,任何一个不对都连不上
  2. Service/Characteristic 顺序不对——有些协议栈按句柄顺序访问,顺序乱了就找不到
  3. 属性权限没配好——比如只配了 Read 没配 Notify,结果对端收不到数据
  4. CCCD 没使能——想收通知,但没写 0x0001 到 CCCD
  5. MTU 没协商——默认 23 字节,扣掉 3 字节头,实际只能传 20 字节

我曾经踩过的坑:有一次联调心率设备,对方说收不到数据。我查了三天,最后发现是 CCCD 的句柄写错了。代码里硬编码了 0x0012,但实际 Service 顺序调整后,句柄变成了 0x0015。从那以后,我再也不硬编码句柄了,全部动态获取。

1.7 小结

GATT 的基础概念其实不复杂,但细节决定成败。Profile 是文档,Service 是容器,Characteristic 是数据点,UUID 是身份证。联调前先把这些理清楚,能省下 80% 的排查时间。