2. GATT协议详解:BLE GATT架构、Service/Characteristic/Descriptor层级关系、UUID定义
好,咱们进入第二章。上一章我们把UWB和BLE的物理层、协议栈捋了一遍,这一章要啃的是GATT——通用属性协议。说白了,它就是BLE设备之间“怎么交换数据”的那套规矩。
我刚开始做BLE项目时,总觉得GATT这玩意儿有点绕。Service、Characteristic、Descriptor,三层嵌套,还有UUID那一大串十六进制数……但后来我发现,只要理解了它的设计哲学,这东西其实特别优雅。你想想看,它本质上就是一个轻量级的客户端-服务器模型。
2.1 GATT架构概览:谁是谁的“服务员”?
在BLE的世界里,GATT定义了两种角色:GATT客户端和GATT服务器。
- GATT服务器:持有数据的一方。比如你的UWB定位基站,它把测距结果、设备状态这些信息组织好,等着别人来读。
- GATT客户端:发起请求的一方。比如你的手机App,它去读基站上的数据,或者写一些配置指令下去。
嗯,这里要注意:一个设备可以同时是客户端和服务器。比如我们的UWB标签,它既要把自己的位置数据暴露给手机(服务器角色),又可能要去读基站的配置信息(客户端角色)。
核心概念:GATT通信永远是客户端发起请求,服务器响应。服务器不会主动“推”数据给客户端,除非客户端先订阅了通知(Notification)或指示(Indication)。
我在项目中遇到过一个问题:有个同事把UWB标签设计成主动往手机发数据,结果发现手机根本收不到。原因就是他没有理解GATT的“请求-响应”模型。后来改成手机先订阅通知,标签再通过Notification推送数据,问题就解决了。
2.2 层级关系:Service、Characteristic、Descriptor
GATT的数据结构是分层的,像一棵树。我习惯把它想象成一个文件柜:
- Service(服务):相当于一个抽屉。每个抽屉代表一类功能,比如“电池服务”、“设备信息服务”、“UWB定位服务”。
- Characteristic(特征):相当于抽屉里的文件夹。每个文件夹里装着一份具体的数据,比如“电池电量百分比”、“设备序列号”、“UWB测距结果”。
- Descriptor(描述符):相当于文件夹上的标签。它描述了这份数据的属性,比如“这个数据支持通知吗?”、“这个数据的单位是米还是厘米?”。
说白了,就是Service包含Characteristic,Characteristic包含Descriptor。一个Service可以有多个Characteristic,一个Characteristic可以有多个Descriptor。
下面这张图,我画了GATT的层级关系,你一看就明白:
2.3 Characteristic:数据的“最小单元”
Characteristic是GATT里最核心的概念。它不仅仅是数据本身,还包含了属性(Properties)和权限(Permissions)。
属性决定了客户端能对这个Characteristic做什么:
| 属性 | 含义 | 典型用途 |
|---|---|---|
| Read | 客户端可以读取这个值 | 读取设备型号、固件版本 |
| Write | 客户端可以写入这个值 | 下发配置参数、启动测距 |
| Notify | 服务器主动推送值(无确认) | 实时推送UWB测距结果 |
| Indicate | 服务器主动推送值(有确认) | 推送关键事件,如设备故障 |
我的经验:Notify和Indicate的区别在于可靠性。Notify不要求客户端确认,速度快但可能丢包;Indicate要求客户端回复确认,可靠但慢半拍。对于UWB定位数据,我一般用Notify,因为测距频率高(比如10Hz),偶尔丢一帧不影响整体定位。但如果是“设备掉线报警”这种关键事件,我会用Indicate。
2.4 Descriptor:数据的“说明书”
Descriptor是附加在Characteristic上的元数据。最常见的Descriptor是客户端特征配置描述符(CCCD,Client Characteristic Configuration Descriptor)。
CCCD的作用是什么?说白了,它就是客户端用来“订阅”通知或指示的开关。客户端往CCCD里写0x0001,表示“我要收通知”;写0x0002,表示“我要收指示”;写0x0000,就是“我不收了”。
我曾经踩过一个坑:在调试UWB标签时,手机App一直收不到测距数据。我检查了Characteristic的属性,Notify是打开的。但就是收不到。后来发现,我忘了让客户端去写CCCD。没有这个订阅动作,服务器是不会主动推数据的。嗯,这个细节很容易被忽略。
2.5 UUID:全球唯一的“身份证号”
每个Service、Characteristic、Descriptor都需要一个UUID来标识。UUID是128位的,但为了节省BLE的传输带宽,蓝牙SIG定义了一组16位UUID作为“短ID”。
蓝牙SIG标准服务(如电池服务、设备信息服务)都使用16位UUID。比如:
| 名称 | 16位UUID | 完整128位UUID |
|---|---|---|
| 电池服务 | 0x180F | 0000180F-0000-1000-8000-00805F9B34FB |
| 设备信息服务 | 0x180A | 0000180A-0000-1000-8000-00805F9B34FB |
| 电池电量特征 | 0x2A19 | 00002A19-0000-1000-8000-00805F9B34FB |
对于自定义服务(比如我们的UWB定位服务),我们需要使用自定义128位UUID。格式是:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。
注意:自定义UUID不能与SIG标准UUID冲突。我习惯用在线UUID生成器生成一个随机UUID,然后固定下来。千万别随便写一个,万一跟别人的服务撞上了,设备之间会互相干扰。
2.6 实战:设计一个UWB定位服务的GATT Profile
好了,理论讲完了。咱们来点实际的。假设我们要设计一个UWB定位标签的GATT Profile,它需要暴露以下数据:
- 标签的硬件版本
- 当前的测距结果(单位:厘米)
- 测距启动/停止控制
我们可以这样设计:
| Service | Characteristic | 属性 | UUID |
|---|---|---|---|
| 设备信息服务 (0x180A) | 硬件版本 (0x2A27) | Read | 标准16位 |
| UWB定位服务 (自定义) | 测距结果 | Notify | 自定义128位 |
| 测距控制 | Write | 自定义128位 |
对应的代码实现(以Zephyr RTOS为例):
/* 自定义UUID定义 */
#define BT_UWB_SERVICE_UUID \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x9ABC, 0xDEF012345678))
#define BT_UWB_RANGE_CHAR_UUID \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x9ABC, 0xDEF012345679))
#define BT_UWB_CTRL_CHAR_UUID \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x9ABC, 0xDEF012345680))
/* 注册UWB定位服务 */
BT_GATT_SERVICE_DEFINE(uwb_svc,
BT_GATT_PRIMARY_SERVICE(BT_UWB_SERVICE_UUID),
BT_GATT_CHARACTERISTIC(BT_UWB_RANGE_CHAR_UUID,
BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_NONE,
NULL, NULL, NULL),
BT_GATT_CCC(&uwb_range_ccc_cfg),
BT_GATT_CHARACTERISTIC(BT_UWB_CTRL_CHAR_UUID,
BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE,
NULL, uwb_ctrl_write_cb, NULL),
);
你看,代码里定义了一个自定义Service,里面有两个Characteristic:一个用于Notify测距结果,一个用于Write控制指令。CCCD也注册了,这样客户端才能订阅通知。
我个人习惯在写GATT Profile时,先把表格画出来,再写代码。表格能帮你理清层级关系,避免漏掉某个Characteristic或Descriptor。
2.7 避坑指南
最后,分享几个我踩过的坑:
- MTU大小:BLE默认MTU是23字节,其中3字节是协议头,实际有效载荷只有20字节。如果你的UWB测距数据超过20字节(比如包含多个标签的距离),记得要协商更大的MTU,或者分包发送。
- Notification频率:不要以超过BLE连接间隔的频率发送Notification。我曾经试过10ms发一次,结果手机端疯狂丢包。后来改成50ms一次,稳得很。
- UUID大小写:自定义UUID在代码里一定要统一大小写。我见过有人在小写和大写之间混用,结果两个设备对不上,折腾了半天。
嗯,这一章的内容就到这里。GATT协议是BLE应用层的核心,理解了它,你就能自由地设计自己的数据交换协议了。