在开发 iOS 蓝牙通信(CoreBluetooth)时,我遇到一个比较有意思的问题:

同一个 BLE 设备,不同 iPhone 连接后,maximumWriteValueLengthForType: 返回的最大写入长度居然不同。

更奇怪的是:

  • HCI 日志显示 MTU 协商完全一样

  • 但 iOS API 返回的最大写入长度却不同

本文记录完整的 实验现象 + 协议分析 + iOS 行为解释


一、实验现象

测试设备:

  • BLE 模块

  • iPhone X

  • iPhone 12

连接建立后,通过 HCI log 观察 MTU 协商:

Exchange MTU Request: 527
Exchange MTU Response: 517

根据 Bluetooth 规范:

Effective MTU = min(527, 517)

因此:

ATT MTU = 517

iOS API 获取最大写入长度

使用 CoreBluetooth API:

NSUInteger len =
[peripheral maximumWriteValueLengthForType:type];

分别测试:

  • CBCharacteristicWriteWithResponse

  • CBCharacteristicWriteWithoutResponse

得到结果:

设备 协商MTU WriteWithResponse WriteWithoutResponse
iPhone X 517 512 514
iPhone 12 517 512 512

于是产生两个疑问:

1️⃣ MTU 协商并没有区分 write type,为什么 API 要区分?
2️⃣ 协议最大 payload 是 514,为什么 API 返回 512

要回答这两个问题,需要先理解 BLE 数据在协议栈中的传递过程。


二、BLE 协议栈数据路径

BLE 数据从应用层到无线空口,会经过多个协议层:

App

CoreBluetooth

ATT (Attribute Protocol)

L2CAP

Link Layer

Air

MTU 只发生在 ATT 层


三、ATT MTU 的含义

BLE 的 MTU 是 ATT(Attribute Protocol)层的最大 PDU 长度

MTU 协商流程:

Client → Exchange MTU Request
Server → Exchange MTU Response

最终:

Effective MTU = min(Client MTU, Server MTU)

本例:

min(527, 517) = 517

因此:

ATT MTU = 517


四、ATT 写入数据结构

ATT Write PDU:

Opcode (1 byte)
Attribute Handle (2 bytes)
Attribute Value (N bytes)

因此最大 Value:

ATT payload = MTU - 3

所以:

517 - 3 = 514

也就是说:

ATT协议允许最大写入 = 514 bytes


五、ATT → L2CAP 封装(517 → 521)

ATT 数据不会直接发送到无线空口,而是会封装到 L2CAP 层

ATT 运行在:

L2CAP fixed channel (CID = 0x0004)

L2CAP PDU 结构:

Length (2 bytes)
CID (2 bytes)
Payload (ATT PDU)

因此 L2CAP header 为:

4 bytes

所以:

ATT PDU = 517
L2CAP PDU = 517 + 4
                     = 521

即:

ATT 517

L2CAP 521


六、Link Layer payload 的大小限制

L2CAP 数据还不会直接发送到无线空口,还需要经过 Link Layer

BLE 的无线空口包结构:

Preamble
Access Address
LL Header
LL Payload
CRC

关键限制在:

LL Payload


BLE4.0 / BLE4.1

早期 BLE 版本:

LL payload 最大 = 27 bytes

因此如果 ATT payload 很大,就必须分片发送。

例如:

ATT payload = 514

需要:

514 / 27 ≈ 20 packets

吞吐量非常低。


BLE4.2(Data Length Extension)

BLE 4.2 引入了 Data Length Extension (DLE),大幅提高吞吐能力。

最大:

LL payload = 251 bytes

因此:

ATT 517
→ L2CAP 521
→ LL fragmentation

521 字节会被拆成:

251 + 251 + 19

因此:

MTU = ATT层限制
空口包 = Link Layer限制

两者不是同一个概念。


七、Data Length Extension 协商

在 BLE4.2 之后,连接建立后还会发生一次 Link Layer 数据长度协商

LL_LENGTH_REQ
LL_LENGTH_RSP

协商内容:

MaxTxOctets
MaxRxOctets
MaxTxTime
MaxRxTime

如果协商成功:

MaxTxOctets = 251

如果设备不支持 DLE:

MaxTxOctets = 27

因此:

最终 LL payload 取决于双方能力


八、为什么很多时候甚至不到 251

实际开发中,经常会发现 LL payload 并没有达到 251,原因可能有多个。

1 控制器限制

有些 BLE 芯片:

MaxTxOctets = 185

这时分片可能变为:

185 + 185 + ...


2 Connection Event 时间限制

BLE 发送发生在 connection event 中。

如果一个 event 时间不够:

剩余分片 → 下一个 event

抓包时可能看到:

packet1
packet2
(下一次 event)
packet3


3 控制器 buffer 限制

控制器内部可能限制:

同时发送 packet 数量

因此:

实际吞吐 < 理论吞吐


九、为什么 iOS API 要区分 Write Type

CoreBluetooth API:

maximumWriteValueLengthForType:

参数:

  • CBCharacteristicWriteWithResponse

  • CBCharacteristicWriteWithoutResponse

原因是 ATT 协议中存在两种写入方式:

写类型 ATT Opcode
Write Request 0x12
Write Command 0x52

区别:

类型 是否需要响应
WithResponse 需要 Response
WithoutResponse 不需要 Response

因此 iOS 需要分别计算最大写入长度。


十、512 是从哪里来的?

理论最大 payload:

ATT payload = 517 - 3 = 514

但 API 返回:

WriteWithResponse = 512

说明:

iOS 在 ATT payload 之上增加了一层限制

可以理解为:

AllowedWriteSize =
min(ATT_payload, iOS_internal_limit)

其中:

iOS_internal_limit ≈ 512

因此:

min(514, 512) = 512


十一、不同 iPhone 的行为差异

实验结果:

设备 WithResponse WithoutResponse
iPhone X 512 514
iPhone 12 512 512

说明:

  • WriteWithResponse 一直被限制为 512

  • WriteWithoutResponse 在某些系统版本允许 MTU-3

也就是说:

旧系统:

WithoutResponse = MTU - 3

新系统:

WithoutResponse = min(MTU - 3, 512)

这属于 CoreBluetooth 实现策略变化,不是 BLE 协议变化。


十二、为什么 HCI log 看不出这个区别

HCI log 只能看到:

ATT 协议行为

例如:

Exchange MTU
Write Command
Write Request

但:

maximumWriteValueLengthForType

属于:

CoreBluetooth Framework

层级关系:

App

CoreBluetooth

ATT

L2CAP

Link Layer

因此:

HCI log 无法看到 iOS API 的内部限制


十三、iOS BLE 开发的最佳实践

不要自己计算:

mtu - 3

正确方式:

NSUInteger maxLen =
[peripheral maximumWriteValueLengthForType:type];

然后按照该长度进行分包。

原因是:

  • 不同 iOS 版本行为不同

  • 不同设备行为不同


十四、总结

实验结论:

项目 数值
协商 MTU 517
ATT 最大 payload 514
L2CAP PDU 521
LL payload 最大 251
iOS WithResponse 512
iOS WithoutResponse 512 / 514

关键结论:

1️⃣ BLE 协议允许最大 ATT payload = MTU - 3

2️⃣ ATT 数据会封装到 L2CAP,因此 517 → 521

3️⃣ L2CAP 数据在 Link Layer 会再次分片

4️⃣ iOS CoreBluetooth 对写入长度进行了额外限制

5️⃣ maximumWriteValueLengthForType 返回的是系统允许写入的最大长度,而不是 MTU


如果你在开发 BLE SDK / OTA / 高吞吐通信,理解这些细节非常重要,否则很容易出现:

  • 数据分包错误

  • 写入失败

  • 不同 iPhone 行为不一致

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐