4、GATT数据流与缓冲区:ATT协议、MTU大小、发送缓冲区与接收缓冲区的工作机制

好,咱们今天聊点实在的。GATT 服务跑起来之后,数据到底是怎么从 A 点流到 B 点的?很多开发者把注意力全放在 Service 和 Characteristic 的定义上,觉得数据流是蓝牙协议栈自动搞定的事。嗯,我以前也这么想,直到有一次在量产前发现数据老是丢包,才老老实实把 ATT 层扒了个底朝天。

说白了,GATT 是应用层的“规矩”,而真正负责搬砖的是 ATT(Attribute Protocol)。你写的所有读、写、通知、指示,最终都要通过 ATT 协议封装成 PDU(Protocol Data Unit),然后塞进 L2CAP 的通道里发出去。这中间任何一个缓冲区的尺寸没算对,数据保真就是一句空话。

4.1 ATT 协议:GATT 的“搬运工”

ATT 协议定义了两种角色:Client 和 Server。GATT Server 持有属性数据,GATT Client 发起请求。我习惯把 ATT 层想象成一个“邮局”——你写好信(属性操作),邮局负责打包、贴邮票、投递,对方收到后再回执。

ATT 的 PDU 格式非常简洁:

+----------+----------+------------------+
| Opcode   | 参数长度  | 参数(可变)      |
| (1 byte) | (1 byte) | (0-MTU-2 bytes)  |
+----------+----------+------------------+

Opcode 决定了这次操作是读请求、写请求、通知还是指示。参数长度字段告诉接收方“后面跟了多少字节”。这里有个关键点:ATT PDU 的最大长度受 MTU 限制。

核心原则:ATT 层一次只能发送一个 PDU,且 PDU 长度不能超过当前连接协商的 MTU 值。超过 MTU 的数据必须由应用层自行分片,ATT 本身不提供自动分片重组。

我在一个健康手环项目里就踩过这个坑。当时心率数据包超过了默认 MTU(23 字节),我以为协议栈会自动分包,结果对端只收到了前 23 个字节,后面的全丢了。从那以后,我养成了一个习惯:所有超过 MTU-3 的数据,一律在应用层手动分片。

4.2 MTU 大小:数据通道的“管径”

MTU(Maximum Transmission Unit)决定了 ATT PDU 的最大长度。默认值是 23 字节,其中 3 字节被 Opcode 和参数长度字段占用,留给实际数据的只有 20 字节。你想想看,20 字节能传什么?一个浮点数加一个时间戳就满了。

好在 BLE 4.2 引入了 MTU 扩展机制,双方可以通过 MTU Exchange Request/Response 协商一个更大的 MTU。协商过程很简单:

  1. Client 发送 MTU Request,带上自己支持的 MTU 值
  2. Server 回复 MTU Response,带上自己支持的 MTU 值
  3. 双方取 较小值 作为本次连接的 MTU

举个例子:

设备 请求的 MTU 实际协商结果
手机(Client) 247 min(247, 512) = 247
手环(Server) 512

我的建议:不要在连接建立后立刻做 MTU Exchange。等双方都初始化完毕,再发起协商。我曾经见过某些手机在连接瞬间还没准备好,MTU Request 被直接丢弃,导致后续所有大包通信都失败。

MTU 越大,单次能传输的数据就越多,传输效率也越高。但也不是越大越好——MTU 每增加 1 字节,L2CAP 层的缓冲区就要多占 1 字节。对于 RAM 受限的嵌入式设备,MTU 设到 512 可能就把宝贵的 SRAM 吃掉了大半。

4.3 发送缓冲区:数据出口的“蓄水池”

发送缓冲区(TX Buffer)是协议栈为每个连接维护的一块内存区域。应用层调用 gatt_write() 或 gatt_notify() 时,数据并不会立刻发到空中,而是先拷贝到 TX Buffer 里,等待链路层调度发送。

TX Buffer 的工作机制可以用三个字概括:排队、拷贝、确认。

  • 排队:多个应用层数据包按顺序进入 TX Buffer
  • 拷贝:协议栈将数据从应用层内存拷贝到 TX Buffer
  • 确认:收到对端 ACK 后,协议栈才释放 TX Buffer 中的对应数据

这里有个容易忽略的细节:通知(Notification)不要求应用层确认,但协议栈内部仍然需要链路层的 ACK。如果链路层 ACK 迟迟不来,TX Buffer 就会被占满,新的数据包无法入队,表现为“写操作返回失败”或“通知发送超时”。

我曾经遇到过一个案例:设备在休眠唤醒后,TX Buffer 里残留了休眠前未发送完的数据。唤醒后应用层立刻发送新数据,结果新老数据在缓冲区里混在一起,对端收到后解析出错。解决方案是:唤醒后先清空 TX Buffer,再重新开始数据发送。

4.4 接收缓冲区:数据入口的“漏斗”

接收缓冲区(RX Buffer)是协议栈为接收数据准备的临时存储区。对端发来的 ATT PDU 先进入 RX Buffer,协议栈解析后,再通过回调函数通知应用层。

RX Buffer 的大小直接影响数据接收的可靠性。如果 RX Buffer 太小,而对方连续发送多个大包,就会发生缓冲区溢出——后面的数据直接被丢弃,连个通知都没有。

我一般这样计算 RX Buffer 的最小尺寸:

RX_Buffer_Size = MTU × (TX_Pending_Count + 1)

其中 TX_Pending_Count 是对方可能同时发起的未确认请求数。对于通知(Notification),这个值通常是 1~3;对于指示(Indication),因为需要等待应用层确认,这个值就是 1。

经验值:对于大多数 BLE 应用,RX Buffer 设为 MTU × 3 就足够安全了。如果 RAM 实在紧张,至少保证 MTU × 2,否则在高数据速率下很容易丢包。

4.5 数据流全景图

为了让你更直观地理解整个数据流,我画了一张图。这张图展示了从应用层到物理层的完整路径,以及缓冲区在其中的位置。

GATT 数据流与缓冲区全景图 应用层 (GATT Profile) 发送缓冲区 (TX Buffer) ATT 协议层 (PDU 封装) L2CAP 层 (分段/重组) 链路层 (LL) + 物理层 接收缓冲区 (RX Buffer) 回调通知 MTU 决定 ATT PDU 最大长度;TX Buffer 排队等待发送;RX Buffer 防止接收溢出

从这张图可以清楚看到:数据从应用层出发,经过 TX Buffer 排队,再到 ATT 层封装成 PDU,然后经过 L2CAP 分段(如果 PDU 超过 L2CAP MTU),最后通过链路层发送出去。接收端则走相反的路径:链路层收到数据,L2CAP 重组,ATT 解析,RX Buffer 暂存,最后通过回调交给应用层。

4.6 缓冲区配置的实战建议

说了这么多理论,最后给几条我在项目里验证过的配置建议:

场景 推荐 MTU TX Buffer RX Buffer
低功耗传感器(每秒发一次数据) 23(默认) MTU × 2 MTU × 2
健康手环(心率+血氧+步数) 128 MTU × 3 MTU × 3
OTA 固件升级 512 MTU × 4 MTU × 4
音频/大数据流 512 MTU × 6 MTU × 6

一个小技巧:在调试阶段,把 TX Buffer 和 RX Buffer 的当前使用量通过日志打印出来。如果发现 TX Buffer 经常接近满值,说明发送速率超过了链路层的处理能力,要么降低发送频率,要么增大 MTU。如果 RX Buffer 溢出,说明接收端处理速度跟不上,需要优化回调函数的执行时间。