蓝牙低功耗(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 管"说话语速多快"。三者都要大,吞吐才高。
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,只是很多协议栈出厂默认不开满。
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 必须同步调大,否则只是把"大块头"切碎了慢慢发。
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 速率在空中发送
链条上最窄的那一环,就是你的实际瓶颈:
理论峰值吞吐可粗略估算。以 1M PHY 为例:
于是 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 还需对端及时处理避免流控丢包。
MTU 不是自动变大的,必须由一端发起 Exchange MTU:
exchangeMtu(maxMtu) 之类接口;BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST(nRF)/ BT_GATTS_EVT_EXCHANGE_MTU(Zephyr)里回应自己支持的最大 MTU;各平台提示:
sdk_config.h 的 NRF_SDH_BLE_GATT_MAX_MTU_SIZE(默认 23,建议 ≥ 247)。CONFIG_BT_L2CAP_RX_MTU / CONFIG_BT_BUF_ACL_RX_SIZE 等配合调大。esp_ble_gatt_set_local_mtu()。BluetoothGatt.requestMtu(int) 传入想要的值(常见 247 或 517),以实际回调的 MTU 为准。peripheral.maximumWriteValueLength / 实际 Exchange 结果为准,调试时以抓包或回调数值为准,不要假设固定值。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 调了也白搭。
2M / Coded 通过 LE Set PHY 协商(nRF sd_ble_gap_phy_update、Zephyr bt_conn_le_phy_update)。注意:
LE Read Local Supported Features 里声明支持 2M / Coded;即便三件套拉满,连接间隔(Connection Interval)过长、每事件包数受限,吞吐也上不去:
同样是传数据,GATT 操作类型对吞吐影响很大:
做 OTA、批量上报,优先用 Notify 或 Write Without Response,把"请求—响应"的等待降到最低。
LE Set Data Length 到 251。TX_COMPLETE 事件再发下一批)。requestMtu 到 517,iOS 以实际协商为准;调试时用抓包(nRF Sniffer / Wireshark)确认最终生效 MTU,别凭代码假设。BLE 的吞吐不是单一参数,而是 MTU(一次说多少)× DLE(信封多大)× PHY(语速多快)× 连接参数(多久说一次) 的乘积效应;默认三件套都在最低档,必须四者协同调优才能逼近 1.36 Mbps 的实测峰值。调优口诀:Exchange MTU 拉到 247 以上 → Set Data Length 到 251 → 近距用 2M → 短连接间隔多包 / 事件 → 大数据走 Notification,同时给足协议栈缓冲与背压,否则就是"配置满了、实际照样卡"。