3、GATT操作流程:Service Discovery、Characteristic Read/Write、Notification与Indication机制、MTU协商
好,咱们进入GATT操作的核心环节。说白了,GATT就是蓝牙设备之间怎么“聊天”的规矩。你连上设备后,第一件事不是直接读数据,而是先搞清楚对方“长什么样”——这就是Service Discovery。
3.1 Service Discovery——先摸清对方底细
我刚开始做蓝牙项目时,犯过一个低级错误:连上设备后直接去读某个Characteristic的UUID,结果一直返回错误。后来才意识到,我连这个服务存不存在都没确认。嗯,这里要注意:GATT通信的第一步,永远是发现服务。
Service Discovery说白了就是主机(手机/模组)向从机(传感器/外设)发一个请求:“你有哪些Service?每个Service里有哪些Characteristic?”从机收到后,会把整个属性表一层层返回。
核心流程:
- 主机发送
Exchange MTU Request(可选,但建议先做) - 主机发送
Discover All Primary Services请求 - 从机返回Service列表(UUID + 句柄范围)
- 主机针对每个Service,发送
Discover All Characteristics请求 - 从机返回Characteristic列表(UUID + 属性 + 值句柄)
- 主机可选发送
Discover All Characteristic Descriptors获取CCCD等描述符
我记得有一次调试一个心率模组,Service Discovery返回了三个Service,但其中一个Service的UUID是自定义的。我查了半天文档才发现,那个是厂商私有的OTA升级服务。所以,不要假设设备只有标准服务。
我的习惯:在联调阶段,我会先用手机上的nRF Connect或LightBlue做一次完整的Service Discovery,截图保存。这样写代码时,UUID和句柄范围一目了然,省得反复查文档。
3.2 Characteristic Read/Write——读写操作的门道
Service Discovery做完后,你就知道每个Characteristic的句柄了。接下来就是读写操作。但这里有个坑:不是所有Characteristic都能读,也不是所有都能写。每个Characteristic的属性字段(Properties)决定了你能对它做什么。
| 属性位 | 含义 | 说明 |
|---|---|---|
| 0x02 | Read | 允许主机读取值 |
| 0x04 | Write Without Response | 主机写入,从机不回复确认 |
| 0x08 | Write | 主机写入,从机回复确认 |
| 0x10 | Notify | 从机主动通知,主机不确认 |
| 0x20 | Indicate | 从机主动指示,主机必须确认 |
读操作很简单:主机发 Read Request,从机回 Read Response。但写操作分两种,我重点说一下。
Write Request vs Write Command
- Write Request(带响应):主机发数据,从机收到后必须回复
Write Response。适合写配置参数这种需要确认的场景。缺点是慢,一来一回。 - Write Command(无响应):主机发数据,从机不回复。适合写大量数据(比如OTA固件包)。快,但不可靠——你根本不知道从机收到没有。
我曾经踩过的坑:有一次写一个温湿度传感器的配置参数,用了Write Command。结果参数写进去了,但传感器没重启,导致配置没生效。后来改成Write Request,等收到响应后再发重启指令,问题解决。所以,关键配置一定要用Write Request。
3.3 Notification与Indication——从机主动上报的两种姿势
读和写都是主机主动发起的。但很多场景下,我们希望从机主动上报数据——比如心率传感器每秒钟发一次心率值。这时候就要用到Notification和Indication。
两者的区别,说白了就是:Notification不确认,Indication要确认。
| 特性 | Notification | Indication |
|---|---|---|
| 主机确认 | 不需要 | 必须回复确认 |
| 传输速度 | 快(无确认开销) | 慢(每次都要等确认) |
| 可靠性 | 低(可能丢包) | 高(确认机制保证) |
| 典型应用 | 高频传感器数据 | 关键事件/报警 |
| CCCD配置 | 0x0001 | 0x0002 |
使用Notification/Indication前,主机必须先使能对应的CCCD(Client Characteristic Configuration Descriptor)。CCCD是一个描述符,句柄通常在Characteristic值句柄+1的位置。主机往CCCD写0x0001开启Notification,写0x0002开启Indication,写0x0000关闭。
使能Notification的代码片段(以ESP32为例):
// 假设已找到目标Characteristic的CCCD句柄
uint16_t cccd_handle = char_handle + 1;
uint8_t enable_notify[] = {0x01, 0x00}; // 小端模式
esp_ble_gattc_write_char(
gattc_if,
conn_id,
cccd_handle,
sizeof(enable_notify),
enable_notify,
ESP_GATT_WRITE_TYPE_REQ
);
我个人习惯是:高频数据用Notification,关键事件用Indication。比如一个加速度传感器,每秒输出100组数据,用Notification;但电池电量低于10%的报警,用Indication,确保主机一定收到。
3.4 MTU协商——别让数据包卡在门口
MTU(Maximum Transmission Unit)决定了GATT一次能传多少字节。默认MTU是23字节,其中3字节是协议头(操作码+句柄),所以实际有效载荷只有20字节。你想想看,如果一次要传一个100字节的固件包,得拆成5次发,效率极低。
MTU协商的过程很简单:
- 主机发
Exchange MTU Request,带上自己支持的MTU大小(比如247) - 从机回
Exchange MTU Response,带上自己支持的MTU大小(比如512) - 双方取较小值作为本次连接的MTU
举个例子:主机说“我能支持247”,从机说“我能支持512”,那最终MTU就是247。如果主机说247,从机说128,那最终就是128。
我的建议:MTU协商要在Service Discovery之前做。因为MTU变大后,Service Discovery返回的数据可以一次性打包,减少交互次数。我一般在连接建立后的第一个操作就是发MTU Request。
这里有个细节:MTU协商不是必须的。如果你只传几个字节的控制指令,默认23字节完全够用。但如果你要传传感器波形数据或OTA固件,一定要做MTU协商。我记得有一次联调一个音频模组,没做MTU协商,每次只能传20字节,一个音频帧要拆成十几包,延迟高得离谱。后来把MTU提到512,一包搞定。
3.5 整体流程梳理
把上面这些串起来,一个完整的GATT操作流程是这样的:
嗯,这张图把整个流程串起来了。你实际开发时,按这个顺序走,基本不会出大问题。但要注意:每一步都要检查返回值。我见过太多人写完代码不检查GATT状态码,结果设备没连上都不知道。
避坑指南:我曾经在一个项目中,MTU协商成功后,Service Discovery返回的数据量突然变大,导致接收缓冲区溢出。后来发现是MTU变大后,从机把多个服务打包在一个ATT PDU里返回,而我的缓冲区还是按默认23字节分配的。所以,MTU协商后,记得同步更新接收缓冲区大小。