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。
1.6 联调时最容易犯的错
根据我这些年的经验,GATT 联调出问题,十有八九是下面这几个原因:
- UUID 写错——大小写、字节序、位数,任何一个不对都连不上
- Service/Characteristic 顺序不对——有些协议栈按句柄顺序访问,顺序乱了就找不到
- 属性权限没配好——比如只配了 Read 没配 Notify,结果对端收不到数据
- CCCD 没使能——想收通知,但没写 0x0001 到 CCCD
- MTU 没协商——默认 23 字节,扣掉 3 字节头,实际只能传 20 字节
我曾经踩过的坑:有一次联调心率设备,对方说收不到数据。我查了三天,最后发现是 CCCD 的句柄写错了。代码里硬编码了 0x0012,但实际 Service 顺序调整后,句柄变成了 0x0015。从那以后,我再也不硬编码句柄了,全部动态获取。
1.7 小结
GATT 的基础概念其实不复杂,但细节决定成败。Profile 是文档,Service 是容器,Characteristic 是数据点,UUID 是身份证。联调前先把这些理清楚,能省下 80% 的排查时间。