性能调优

BLE 吞吐量调优:MTU、PHY 与 DLE 如何协同榨干带宽

发布于 2026-07-21 · 来自「嵌入式江湖」技术专栏

蓝牙低功耗(BLE)标称"低功耗",但很多人忽略了一点:在需要传大量数据时(固件 OTA、传感器批量上报、设备贴近手机传图/传文件),BLE 的吞吐其实可以做到 1 Mbps 以上——前提是 MTU、PHY、DLE 三件套同时调对。这三样分别卡在协议栈的不同层,只调其中一个几乎没用。本文把三层机制、吞吐计算模型、协商方法和实战坑一次讲透。

阅读前提:假设你已经了解 GATT / ATT 基本概念与连接事件(Connection Event)。如果还不清楚广播与连接,建议先看专栏里的《广播与扫描》和 GATT 基础文章。

一、先厘清三层:它们卡在不同地方

很多人把"MTU""PHY""DLE"混为一谈,其实它们在协议栈里各管一段:

概念 所在层 它决定什么 默认值 上限
ATT_MTU GATT / ATT 层 一次 ATT 操作(读 / 写 / Notify)能搬多少字节 23 字节 517 字节(规范上限)
DLE(Data Length Extension) 链路层(LL) 一帧空中包(LL Data PDU)的 payload 多大 27 字节 251 字节
PHY 物理层 每秒传多少符号、传多远 1M 2M / Coded(S2 / S8)

一句人话:MTU 管"一次对话说多少",DLE 管"一个信封装多少",PHY 管"说话语速多快"。三者都要大,吞吐才高。

1.1 ATT_MTU:一次 ATT 操作的上限

ATT_MTU 是 Attribute Protocol 的最大传输单元。默认 23 字节,其中 3 字节是 ATT 头(操作码 1 + 属性句柄 2),所以一次 Write / Notify 实际只能带 20 字节 净数据。想多带?必须通过 Exchange MTU 协商增大。

规范规定 ATT_MTU 的硬上限是 517 字节(Bluetooth Core v4.2 起)。也就是说,协商到顶时,一次 Notification / Write Without Response 最多能装 517 - 3 = 514 字节 净数据。

注意:你常看到芯片 SDK 把"最大 MTU"设为 247(如 Nordic 的 NRF_SDH_BLE_GATT_MAX_MTU_SIZE 默认 247),这是工程上 251 字节 DLE 与 L2CAP 分段对齐的常用平衡值,并不是规范上限。规范允许到 517,只是很多协议栈出厂默认不开满。

1.2 DLE:链路层信封变大

BLE 4.2 引入 Data Length Extension。在此之前,一帧 LL Data PDU 的 payload 固定最多 27 字节(所以即便你 MTU=23,链路层还有富余;但一旦 MTU 变大,27 字节的信封就成了瓶颈)。DLE 把单帧 payload 上限提到 251 字节

关键点:MTU 可以大于 DLE。当 ATT_MTU=247 但 DLE=27 时,一个 ATT 包会被链路层切成近 10 个 27 字节小帧发送,每帧都带一遍 LL 头加帧间间隔,效率极低。所以 MTU 大了,DLE 必须同步调大,否则只是把"大块头"切碎了慢慢发。

1.3 PHY:语速

  • 1M PHY:1 Msym/s,未编码,最通用,是速率基准。
  • 2M PHY(BLE 5.0):符号率翻倍到 2 Msym/s,吞吐近乎翻倍,但传输距离变短、抗干扰略降。
  • Coded PHY(S2 / S8)(BLE 5.0):用前向纠错把每符号扩成 2 或 8 位,S2 有效速率约 500 kbps、S8 约 125 kbps,但距离和穿墙能力大幅提升。适合"远但慢"的场景。

2M 和 Coded 都要求通信双方都声明支持对应 PHY 特性,并通过 LE Set PHY 协商切换。

二、三者如何协同:一个被忽略的真相

很多工程师的误区是"我把 MTU 调到 247 就快了"。错。真实吞吐由三者共同决定,且受连接参数制约。

设想一次数据传输:

应用层一包数据(比如 500 字节)
  -> 按 ATT_MTU 切成若干 ATT 包(若 MTU=247,约 3 个 ATT 包,每包净约 244)
    -> 每个 ATT 包按 DLE 装进 LL 帧(若 DLE=251,一个 ATT 包基本一帧;若 DLE=27,要切 9 帧)
      -> 每帧按 PHY 速率在空中发送

链条上最窄的那一环,就是你的实际瓶颈:

  • MTU=23(默认)→ 每帧只装 20 字节净数据,251 字节的 DLE 信封里大量空闲和头,浪费到极点
  • MTU=247 + DLE=27 → 大 ATT 包被切碎成 9 个 27 字节小帧,帧间间隔 T_IFS(150μs)反复插入,效率骤降
  • MTU=247 + DLE=251 + 2M PHY → 一个 ATT 包基本一帧发完,语速翻倍,这才是高吞吐组合。

三、吞吐量到底能到多少:计算模型

理论峰值吞吐可粗略估算。以 1M PHY 为例:

  • 一个 LL Data PDU(251 payload)空中占用 ≈ 前导(1)+ 接入地址(4)+ LL 头(2)+ 长度(1)+ 载荷(251)+ CRC(3)+ 帧间(0.15ms T_IFS)≈ 262 字节 / 1 Mbps + 150 μs ≈ 2096 + 150 = 约 2246 μs
  • 每帧净数据:251(DLE)− 4(L2CAP 头)− 3(ATT 头)= 244 字节 / 帧
  • 若连接间隔(Connection Interval)7.5 ms,且一个连接事件内连续发多包(无重发接入地址,仅靠 T_IFS 衔接),7.5 ms 内约可发 3 个完整帧。

于是 1M + DLE251 + MTU247 的理论速率 ≈ (244 × 3 × 1000 / 7.5)/ 1000 ≈ 约 0.78 Mbps(典型实测峰值,Nordic 等芯片实测与此一致)。

切换到 2M PHY,语速翻倍,理论峰值约 1.36 Mbps(常见实测)。这就是 BLE 5 的数据速率提升来源。

常见配置对照(实测峰值,仅供参考,具体取决于芯片 / 协议栈 / 连接间隔):

PHY DLE MTU 实测峰值 说明
1M 27(默认) 23(默认) 约 0.27 Mbps 量级 出厂默认,最慢
1M 251 247 约 0.78 Mbps 经典高吞吐组合
2M 251 247 约 1.36 Mbps BLE 5 最佳吞吐
Coded S8 251 247 约 0.1 Mbps 量级 远距优先,速率让位
1M 251 23 很低 MTU 没调,信封白大

提醒:以上是理想峰值(无重传、无加密开销、连接事件塞满、Slave Latency 为 0)。真实应用要打折扣:加密连接(LE Secure Connections)会降低速率,协议栈缓冲和 CPU 处理也会成为新瓶颈,长包 Notify 还需对端及时处理避免流控丢包。

四、怎么把三件套都调起来

4.1 MTU 协商(GATT 层)

MTU 不是自动变大的,必须由一端发起 Exchange MTU

  • 中心(手机 / 主机)调用 exchangeMtu(maxMtu) 之类接口;
  • 从机在 BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST(nRF)/ BT_GATTS_EVT_EXCHANGE_MTU(Zephyr)里回应自己支持的最大 MTU;
  • 最终生效值 = min(中心声明,从机声明),取两端较小者。所以从机侧也必须在 SDK 里把最大 MTU 配置够大,否则中心再大也无效。

各平台提示:

  • nRF SDK:改 sdk_config.hNRF_SDH_BLE_GATT_MAX_MTU_SIZE(默认 23,建议 ≥ 247)。
  • ZephyrCONFIG_BT_L2CAP_RX_MTU / CONFIG_BT_BUF_ACL_RX_SIZE 等配合调大。
  • ESP-IDFesp_ble_gatt_set_local_mtu()
  • AndroidBluetoothGatt.requestMtu(int) 传入想要的值(常见 247 或 517),以实际回调的 MTU 为准。
  • iOS:CoreBluetooth 会自动协商,最终 MTU 以 peripheral.maximumWriteValueLength / 实际 Exchange 结果为准,调试时以抓包或回调数值为准,不要假设固定值。

4.2 DLE 协商(链路层)

DLE 通过 HCI 命令 LE Set Data Length 请求(主机侧 sd_ble_gap_data_length_update / Zephyr bt_gap_le_set_data_len)。两端各自声明自己支持的 max_tx_octets / max_tx_time,协议栈取 min。

别忘了:很多芯片默认 DLE 仍是 27,必须主动调用上述接口把发送 / 接收的 octets 提到 251,否则前面 MTU 调了也白搭。

4.3 PHY 切换(物理层)

2M / Coded 通过 LE Set PHY 协商(nRF sd_ble_gap_phy_update、Zephyr bt_conn_le_phy_update)。注意:

  • 双方都要在 LE Read Local Supported Features 里声明支持 2M / Coded;
  • 2M 速率快但距离短,做高吞吐短距(如 OTA 升级、设备贴近手机)很合适;
  • Coded 用于远距离回传,速率牺牲大,别指望高吞吐。

4.4 连接参数:吞吐的第四个旋钮

即便三件套拉满,连接间隔(Connection Interval)过长、每事件包数受限,吞吐也上不去:

  • 缩短 Connection Interval(如 7.5 ms 最小)可增加单位时间内的连接事件数;
  • 增大每个连接事件内可发的包数 / 事件长度(Event Length / max packets per event):让一个事件里连续发多包而非只发一包;
  • 代价是功耗上升、从机更难睡眠。吞吐与功耗天然对立,按场景取舍。

五、选对 GATT 操作:吞吐差异巨大

同样是传数据,GATT 操作类型对吞吐影响很大:

  • Notification / Write Without Response:无确认、无往返等待,可以一个连接事件内连续猛发,是大数据量的首选。
  • Indication:需要对端确认(Confirmation),每发一包等确认,吞吐受限。
  • Read / Write Request:每笔都有 Request / Response 往返,最慢,适合小控制指令。

做 OTA、批量上报,优先用 Notify 或 Write Without Response,把"请求—响应"的等待降到最低。

六、实战坑提醒(10 条)

  1. 出厂默认就是最慢的:MTU=23、DLE=27、1M PHY,三件套全在最低档。不主动调,永远跑不满。
  2. MTU 取两端最小:中心请求 517,从机只配了 23,最终就是 23。从机侧最大 MTU 必须一起调。
  3. DLE 不调,MTU 白调:251 字节的 ATT 包被切碎成 27 字节帧,效率暴跌。务必 LE Set Data Length 到 251。
  4. 2M PHY 距离短:高吞吐组合(2M + DLE251 + MTU247)适合贴近场景;隔墙或远距会掉速甚至断连,必要时回退 1M。
  5. 连接间隔是隐藏瓶颈:MTU / PHY / DLE 都满,但 CI=100ms,单位时间事件少,吞吐照样上不去。短 CI 加多包 / 事件才有效。
  6. 加密连接降速:LE Secure Connections 加密有计算与协议开销,安全连接下峰值低于明文,预留余量。
  7. 协议栈缓冲与 CPU:从机 MCU 太弱、缓冲太小,Notify 发太快会被流控丢包;发送端要做背压(等 TX_COMPLETE 事件再发下一批)。
  8. 长包 Notify 对端要跟得上:手机端处理 Notify 有延迟,狂发会触发对端 L2CAP 流控 / 缓冲满,导致丢包或断连。分批加限速。
  9. 平台 MTU 上限差异:Android 可 requestMtu 到 517,iOS 以实际协商为准;调试时用抓包(nRF Sniffer / Wireshark)确认最终生效 MTU,别凭代码假设。
  10. Coded PHY 别用来追吞吐:S8 速率仅约 125 kbps 量级,它是"远"不是"快";要快用 2M,要远用 Coded,二者难兼得。

七、一句话总结

BLE 的吞吐不是单一参数,而是 MTU(一次说多少)× DLE(信封多大)× PHY(语速多快)× 连接参数(多久说一次) 的乘积效应;默认三件套都在最低档,必须四者协同调优才能逼近 1.36 Mbps 的实测峰值。调优口诀:Exchange MTU 拉到 247 以上 → Set Data Length 到 251 → 近距用 2M → 短连接间隔多包 / 事件 → 大数据走 Notification,同时给足协议栈缓冲与背压,否则就是"配置满了、实际照样卡"。

更多 BLE / 鸿蒙星闪 / 芯片选型 / MCU 实战,持续更新