4. ATT PDU 与数据包结构:吞吐优化的底层密码

各位做 BLE 开发的同行,咱们今天聊点实在的。ATT 层,说白了就是 GATT 的“快递员”——你定义的 Characteristic 怎么读、怎么写、怎么通知,全得靠 ATT PDU 来承载。很多开发者调吞吐调到头秃,其实根子就在 ATT PDU 的结构上没吃透。

我个人习惯,拿到一个新的 BLE 芯片,第一件事就是翻它的 ATT MTU 支持范围。为什么?因为 PDU 长度直接决定了你一次能塞多少数据。你想想看,如果 MTU 只有 23 字节,那有效载荷才 20 字节,传个 500 字节的传感器数据得拆成 25 包,这吞吐能高才怪。

4.1 ATT PDU 类型:你用的每一个操作,底层都在发什么?

ATT PDU 的类型其实不多,但每个都有它的脾气。我按使用频率排个序,咱们一个个看。

PDU 类型 Opcode 方向 是否需要响应 典型用途
Read Request / Response 0x0A / 0x0B Client → Server / Server → Client 是 读取特征值
Write Request / Response 0x12 / 0x13 Client → Server / Server → Client 是 可靠写入
Write Command 0x52 Client → Server 否 高速写入
Handle Value Notification 0x1B Server → Client 否 高速通知
Handle Value Indication 0x1D Server → Client 是(需确认) 可靠通知

看到没?Write Command 和 Notification 是唯二不需要响应的 PDU。这就是吞吐优化的命门所在。

核心结论:不需要响应的 PDU,没有协议层的往返延迟,理论上可以达到链路层的最大吞吐。需要响应的 PDU,每发一包就得等一包 ACK,吞吐直接腰斩甚至更惨。

4.2 PDU 长度限制:MTU 到底能开多大?

ATT PDU 的长度上限由 ATT MTU 决定。默认值是 23 字节,其中 3 字节是 Opcode + Handle 的开销,所以有效载荷只有 20 字节。

但注意,MTU 是可以协商的。通过 Exchange MTU Request/Response,双方可以约定一个更大的 MTU。我在项目中遇到过,有些芯片号称支持 512 字节 MTU,但实际测试时发现超过 247 字节就开始丢包。嗯,这里要注意:MTU 的上限受限于双方芯片的 LL 层 PDU 大小和 Controller 的 buffer 深度,不是你想开多大就开多大。

MTU 大小 ATT PDU 最大长度 有效载荷(Notification) 有效载荷(Write Command)
23(默认) 23 20 20
64 64 61 61
128 128 125 125
247 247 244 244
512 512 509 509

这里有个细节:Notification 和 Write Command 的 PDU 结构里,Opcode 占 1 字节,Handle 占 2 字节,所以有效载荷 = MTU - 3。Read/Write Request 因为还要带偏移量等参数,开销会更大一些。

4.3 如何利用 Write Command 和 Notification 提升吞吐?

好,重头戏来了。既然不需要响应的 PDU 是吞吐利器,那怎么用才能榨干带宽?

4.3.1 数据流方向决定选择

  • 手机 → 设备(下行):用 Write Command。手机端发 Write Command,设备端不需要回复,手机可以连续发多包。
  • 设备 → 手机(上行):用 Notification。设备端主动推送数据,手机端不需要确认,设备可以连续发多包。

我曾经在一个 OTA 升级项目里,把固件传输从 Write Request 改成 Write Command,配合 MTU 从 23 提升到 247,吞吐从 8 KB/s 直接飙到 58 KB/s。你想想看,差了 7 倍多!

实战技巧:使用 Write Command 时,一定要在应用层做流控。因为底层没有 ACK,发太快了设备 buffer 会溢出。我一般会在设备端暴露一个“剩余 buffer 大小”的 Characteristic,手机端每发 N 包就读一次,动态调整发送速率。

4.3.2 连续发送的“管道”效应

BLE 的链路层其实支持 LE Data Length Extension,可以把单个连接事件里的数据包长度扩展到 251 字节。配合 ATT 层的 Notification/Write Command,你可以在一个连接事件里塞多个 ATT PDU。

举个例子:

  • MTU = 247,Notification 有效载荷 = 244 字节
  • LL 层 PDU 最大长度 = 251 字节
  • 一个连接事件里可以发 4-6 个 LL PDU(取决于 Connection Interval 和芯片能力)
  • 那么一个连接事件就能传 244 × 4 = 976 字节,约 1 KB

如果 Connection Interval 是 7.5 ms,理论吞吐 = 1 KB / 7.5 ms ≈ 133 KB/s。当然,实际还要考虑调度开销、射频干扰、对端处理能力,但方向是对的。

4.3.3 避坑指南:Indication 的“伪可靠”陷阱

有些开发者觉得 Notification 不可靠,就改用 Indication。Indication 确实有确认机制,但它的确认是串行的——发一个 Indication,等一个 Confirm,才能发下一个。这导致吞吐极低,通常只有 Notification 的 1/5 到 1/10。

我曾经在一个医疗设备项目里,客户坚持要用 Indication 保证数据不丢。结果实测吞吐只有 3 KB/s,根本达不到 20 KB/s 的需求。后来我跟他解释:Indication 保证的是“单包可靠”,但牺牲了“整体吞吐”。最终方案是:用 Notification + 应用层 CRC 校验 + 重传机制,吞吐做到了 45 KB/s,数据完整性也满足要求。

警告:不要在生产代码里用 Indication 做大数据量传输。它只适合传输关键控制指令(比如“开始测量”、“停止测量”这类偶发消息)。数据流传输,请用 Notification + 应用层可靠性。

4.4 ATT PDU 结构全景图

下面这张图,我画了 ATT PDU 的完整结构,以及不同 PDU 类型在其中的位置。你可以把它当作调优时的“地图”。

ATT PDU 结构全景图 ATT PDU 结构全景图 通用 ATT PDU 结构 | Opcode (1 byte) | 参数 (Attribute Handle + 其他参数) | Attribute Value (可变长度) | Handle Value Notification Opcode: 0x1B | 0x1B | Handle (2B) | Value (MTU-3) | 无需响应,高速上行 Write Command Opcode: 0x52 | 0x52 | Handle (2B) | Value (MTU-3) | 无需响应,高速下行 Write Request Opcode: 0x12 | 0x12 | Handle (2B) | Value (MTU-3) | 需响应,可靠但慢 吞吐优化核心策略 上行用 Notification(绿色),下行用 Write Command(橙色) 避免使用 Write Request(红色)和 Indication 做大数据传输 MTU 协商 Exchange MTU Request → 双方取较小值 有效载荷计算 Payload = MTU - 3(Notification / Write Command)

4.5 实战:一个简单的吞吐测试代码

下面是我在项目中常用的一个吞吐测试片段,用 Notification 连续发送数据。注意看流控的实现。

// 设备端:使用 Notification 连续发送数据
// 假设 MTU 已经协商到 247

#define MTU_SIZE        247
#define PAYLOAD_SIZE    (MTU_SIZE - 3)  // 244 bytes
#define TX_BUFFER_SIZE  4096

static uint8_t tx_buffer[TX_BUFFER_SIZE];
static uint16_t tx_index = 0;
static uint8_t flow_ctrl_buf[2];  // 用于暴露剩余 buffer 大小

void send_data_via_notification(void)
{
    uint16_t bytes_to_send;
    uint16_t remaining = TX_BUFFER_SIZE - tx_index;
    
    // 检查对端 buffer 状态(应用层流控)
    uint16_t peer_buf_available = get_peer_buffer_available();
    if (peer_buf_available < PAYLOAD_SIZE) {
        // 对端 buffer 满了,等待
        return;
    }
    
    bytes_to_send = (remaining > PAYLOAD_SIZE) ? PAYLOAD_SIZE : remaining;
    
    // 发送 Notification
    att_notify(CHAR_HANDLE, &tx_buffer[tx_index], bytes_to_send);
    
    tx_index += bytes_to_send;
    
    // 更新本地剩余 buffer 大小
    flow_ctrl_buf[0] = (TX_BUFFER_SIZE - tx_index) & 0xFF;
    flow_ctrl_buf[1] = ((TX_BUFFER_SIZE - tx_index) >> 8) & 0xFF;
    att_write(FLOW_CTRL_CHAR_HANDLE, flow_ctrl_buf, 2);  // 通知对端
}

关键点:这个代码里,我每发一个 Notification,就更新一次“剩余 buffer 大小”的 Characteristic。手机端通过读这个值,就知道设备还能收多少数据。这叫“应用层背压流控”,比固定延时靠谱得多。

4.6 本章小结

ATT PDU 的结构其实不复杂,但选对类型、用对长度、做好流控,这三件事直接决定了你的 BLE 吞吐能跑多高。我个人建议,新项目起步就把 MTU 协商到 247,上行用 Notification,下行用 Write Command,然后加上应用层流控。这套组合拳打下来,吞吐基本不会太难看。