2. MTU协商机制:从定义到实战优化

MTU这个东西,说白了就是蓝牙通信中一次能搬运的数据包大小。很多初学者上来就问我:“为什么我的BLE设备传数据这么慢?”我通常会反问一句:“你检查过MTU吗?”

嗯,这一章我们就来彻底搞懂MTU——它是什么、怎么协商、如何影响吞吐量,以及我这些年积累的优化经验。

2.1 MTU定义:不是越大越好

MTU全称是Maximum Transmission Unit,最大传输单元。在BLE GATT协议里,它指的是ATT层一次能发送或接收的最大数据包长度。

注意,这里说的是ATT层,不是L2CAP层,更不是物理层。默认情况下,BLE的MTU是23字节。为什么是23?因为ATT默认PDU最大长度就是23,其中包含3字节的ATT操作头,真正能放用户数据的只有20字节。

关键概念区分:

  • ATT MTU:应用层一次能传输的最大数据包长度(我们通常说的MTU)
  • L2CAP MTU:L2CAP层能承载的最大SDU长度,通常比ATT MTU大4字节(L2CAP头)
  • 有效载荷:去掉协议头后,真正能放用户数据的字节数

我见过不少开发者把MTU设得特别大,觉得越大越好。其实不然。MTU大小受限于几个因素:

  • 芯片硬件限制:有些低端蓝牙芯片的缓冲区就那么大,你设个512字节它根本装不下
  • 链路质量:信号差的时候,大包更容易丢,重传更频繁,反而更慢
  • 功耗考量:大包意味着射频开启时间更长,功耗自然更高

我的经验:在项目初期,先查芯片手册确认最大支持MTU。我踩过这个坑——某国产芯片标称支持247字节,实际跑到200以上就开始丢包。后来发现是内部DMA缓冲区配置问题。

2.2 MTU交换流程:三步搞定

MTU协商不是自动的,需要客户端主动发起。流程其实很简单,就三步:

  1. 客户端发送MTU请求:包含客户端支持的最大接收MTU
  2. 服务端响应MTU响应:包含服务端支持的最大接收MTU
  3. 双方取最小值:协商后的MTU = min(客户端MTU, 服务端MTU)

来看一个实际的代码示例,我用的是Zephyr RTOS:

// 客户端发起MTU协商
static void mtu_exchange(struct bt_conn *conn)
{
    int err;
    
    // 发起MTU请求,声明客户端支持512字节
    err = bt_gatt_exchange_mtu(conn, 512);
    if (err) {
        printk("MTU exchange failed (err %d)\n", err);
        return;
    }
    
    printk("MTU exchange initiated\n");
}

// MTU协商完成回调
static void mtu_updated(struct bt_conn *conn, uint16_t mtu)
{
    printk("Negotiated MTU: %d\n", mtu);
    // 这里可以开始大数据传输
}

服务端那边呢?其实不需要主动做什么,只要在初始化时设置好支持的最大MTU就行:

// 服务端配置(通常在初始化时设置)
static struct bt_gatt_attr attrs[] = {
    BT_GATT_PRIMARY_SERVICE(&my_service),
    BT_GATT_CHARACTERISTIC(&my_char, BT_GATT_CHRC_NOTIFY),
    BT_GATT_CCC(my_ccc_cfg_changed, NULL),
};

// 服务端最大接收MTU在Kconfig中配置
// CONFIG_BT_L2CAP_TX_MTU=512
// CONFIG_BT_L2CAP_RX_MTU=512

注意:MTU协商必须在任何数据收发之前完成。我曾经在一个项目中,先发了通知数据才去协商MTU,结果数据被截断,排查了半天才发现是顺序问题。

为了让你更直观地理解,我画了一张流程图:

MTU协商流程图 GATT客户端 GATT服务端 ① MTU请求(客户端支持512) ② MTU响应(服务端支持256) 协商结果:MTU = min(512, 256) = 256 关键点: • 协商是单向的:只能由客户端发起 • 协商结果 = min(客户端MTU, 服务端MTU) • 协商完成后,双方都可以用这个MTU发送数据 • 协商只进行一次,除非重新连接

2.3 MTU对吞吐量的影响:数字会说话

MTU对吞吐量的影响,说白了就是“一次能搬多少砖”。我们来做一道简单的算术题:

假设连接间隔是30ms,每个连接事件能发4个包(这个取决于链路层调度)。

MTU大小 有效载荷/包 包数/秒 理论吞吐量 实际吞吐量(约)
23(默认) 20字节 133 2.66 KB/s 1.8-2.2 KB/s
100 97字节 133 12.9 KB/s 9-11 KB/s
247(经典) 244字节 133 32.5 KB/s 22-28 KB/s
512 509字节 133 67.8 KB/s 45-55 KB/s

看到没?从默认的23字节提升到512字节,吞吐量提升了将近25倍。这就是为什么我总说:“优化GATT性能,第一步就是看MTU。”

但注意,实际吞吐量永远达不到理论值。为什么?因为:

  • 协议开销:每个包都有L2CAP和ATT头
  • 调度延迟:连接事件之间有空隙
  • 重传:丢包了要重传,尤其是大包更容易丢
  • CPU处理能力:有些MCU处理大包的速度跟不上

实测数据:我在一个项目中用nRF52840测试,MTU从23提升到247后,OTA升级时间从8分钟缩短到45秒。客户当场就竖了大拇指。

2.4 如何优化MTU值:实战经验

优化MTU不是简单地把值设大就完事了。我总结了一套“三步走”策略:

第一步:摸清硬件底牌

先搞清楚你的芯片和协议栈支持的最大MTU是多少。不同芯片差异很大:

芯片/协议栈 最大MTU 备注
nRF52832 (SoftDevice) 247 经典限制,很多项目用这个
nRF52840 (SoftDevice) 247 同上,但实际可以更高(需修改配置)
nRF5340 (Zephyr) 512 新架构,支持更大MTU
ESP32 (NimBLE) 512 配置灵活,但要注意内存
DA1469x 512 Dialog的芯片,性能不错

第二步:根据场景选值

不同应用场景,最优MTU不一样:

  • 传感器数据上报(小数据量):MTU 100-150就够了,没必要太大
  • OTA升级(大数据量):能设多大设多大,我一般用247或512
  • 音频传输(实时性要求高):MTU 150-200,太大反而增加延迟
  • 低功耗场景:MTU 100左右,平衡速度和功耗

第三步:动态协商策略

我个人习惯的做法是:不写死MTU,而是让客户端根据实际情况动态协商。

// 动态MTU协商策略
static uint16_t get_optimal_mtu(struct bt_conn *conn)
{
    // 先获取连接参数
    struct bt_conn_info info;
    bt_conn_get_info(conn, &info);
    
    // 根据连接间隔调整MTU
    uint16_t interval = info.le.interval; // 单位:1.25ms
    uint16_t mtu;
    
    if (interval < 12) { // 连接间隔 < 15ms
        mtu = 512;       // 快速连接,用大MTU
    } else if (interval < 24) { // 15-30ms
        mtu = 247;       // 中等速度
    } else {
        mtu = 100;       // 慢速连接,用小MTU保稳定
    }
    
    return mtu;
}

// 使用时
uint16_t mtu = get_optimal_mtu(conn);
bt_gatt_exchange_mtu(conn, mtu);

避坑指南:我曾经在一个项目中,把MTU设成512,结果在信号弱的地方频繁断连。后来加了动态降级策略——如果连续3次写操作失败,自动把MTU降到247。问题就解决了。

优化总结

最后,我把MTU优化的核心要点列出来,方便你对照检查:

  1. 协商时机:连接建立后立即协商,不要等要发数据了才想起来
  2. 值的选择:不是越大越好,要结合硬件、场景、链路质量
  3. 动态调整:根据连接参数和链路质量动态调整MTU
  4. 内存规划:大MTU意味着大缓冲区,提前算好内存够不够
  5. 测试验证:在不同信号强度下测试,确保稳定性