同一份打流代码,BLE 上的吞吐可以从 20 Kbps 做到 1296 Kbps——前后相差 65 倍。决定权不在代码,在三个开关。三个都拨到位,BLE 能追平经典蓝牙 SPP;只要有一个没开,数字直接砍到零头。这篇只讲这三个开关,以及怎么一步步把它拨到位。
一、为什么 BLE 也要做吞吐测试
BLE 大部分时间传的是命令和状态这类小数据,极限吞吐用得少。真正吃吞吐上限的是三件事:OTA 固件升级、传图片、传音频。一次 OTA 传 500KB 镜像,700 Kbps 和 20 Kbps 的差别就是 6 秒和 3 分钟——用户握着手机等进度条的时候,这个差别非常具体。
但还有一半价值在测试评估侧:BLE 上没有标准的吞吐测试工具。原因是 iperf 这类打流本质上是上层业务测试,不依赖某个特定底层协议,但它需要一个标准化的承载通道才可能成为通用工具。WiFi 下有 UDP/TCP,经典蓝牙有 SPP,而 BLE 连接建立后必须先自己创建 GATT 通道才能收发数据——标准协议里并没有规范一个专门用于打流的 GATT 服务,每个厂家都得自建 GATT、自定打流协议。
承载通道本身没标准化,通用工具自然无从谈起。行业现状也印证了这一点:大部分厂家根本没做过 BLE 饱和打流,即便做,也多是拿手机和设备对传——手机侧协议栈自带的调度和缓冲限制,让链路根本打不满。测到的是「能跑通」,不是「链路极限」。所以大部分产品对自己的 BLE 真实极限吞吐是不清楚的。
打流工具只能自研,机制参考 WiFi 的 iperf:自建 GATT 通道,一端发送、一端按秒统计,跑满链路看真实上限。跑出来的数字还顺带回答了另一个问题——BLE 协议栈的成熟度怎么样。满带打流能把调度、缓冲、连接稳定性层面的毛病全暴露出来,「命令能通」是看不出来的。
二、应用层先把该做的做满:244 字节的算术
打流代码本身很简单。先说一个方向上的细节:打流要选「不需要协议栈回复」的 GATT 接口。主机方向用 Write-No-Response(写完不等 ACK,栈才能连发);从机方向用 notify(主动上报,同样免响应)。如果主机用带响应的 Write、从机用 indication,每包都要等一次协议栈回执,吞吐直接被 ACK 往返拖垮。
发送线程(主机方向,write no rsp)就是一个不带任何延时的忙发循环:
static uint32_t iperf_write_task(void *arg)
{
ble_iperf_data_t pkt = {
.head = 0xAA55,
.length = 240,
.data = {0}};
write.handle = conn->write_no_rsp_handle;
write.len = sizeof(pkt); /* 2 + 2 + 240 = 244 */
while (running) {
ret = gatt_write_no_rsp(&write);
if (ret != BLE_OK && ret != BLE_BUSY) break;
}
}
每包 244 字节整包交栈,返回 BUSY 当没发生继续塞,其余错误退出。没有 sleep、没有节流——什么时候等,交给协议栈的流控反馈,应用层不瞎掺和。
while(1) 死循环,优先级定错了两头出问题——高于底层收发接口,会抢占协议栈线程,底层来不及收发,吞吐反而掉甚至丢包;太低,又会被统计、打印这类耗时线程压住,发送空转。合理位置是夹在中间:略低于底层收发、高于统计打印。这一条不看代码、只看配置表,跟主频一样属于「白送的吞吐」。关键是 244 这个数怎么来的。三个参数咬合在一起:
MTU 协商到 247
→ ATT PDU 数据区 = 247 - 3(ATT 头) = 244 字节
iperf 包 = head(2) + length(2) + data(240) = 244 字节
→ 恰好填满一个 ATT PDU,无分片、无 padding
每包如果超过 MTU-3,一包会被拆成多个 ATT 传输,开销翻倍;小于则有 padding 浪费。MTU 247、数据 240、包头 4,加起来正好压线,这是一包一个 ATT PDU 的最优解。
不过吞吐公式的大头不在这,在连接事件:
吞吐 ≈ 单个连接事件能传的 LL 字节数 ÷ 连接间隔
测试固定连接间隔 100ms,每秒只有 10 个连接事件。如果每个事件只能发 1 个 27 字节的 LL 包(DLE 未开),算出来才 2 Kbps 量级。要拉满,必须让每个连接事件内连续塞多个 LL 包——靠两个底层机制:
- DLE(Data Length Extension):把单个 LL PDU 从默认 27 字节扩到 251 字节,发送时间片扩到 2120μs。
- more data bit(MD 位):链路层发完一包,如果上层还有数据待发,就把包头的 MD 位置 1,连接事件继续拉数据,直到时间片用完。100ms 这种稀疏间隔下,吞吐天花板完全由「单个连接事件能塞下多少字节」决定,而塞不塞得满,就看 MD 这一位。
应用层配置清单:MTU 247、DLE 开满(251 字节 / 2120μs)、每包 244 对齐 MTU、写用 Write-No-Response 免 ACK、发送循环零节流。该做的全做了,数字是多少?
三、开关一:PHY,1M → 2M
实测第一轮,1M PHY:
0-1 sec avg tp 743 Kbps, rssi -23
1-2 sec avg tp 743 Kbps, rssi -24
2-3 sec avg tp 743 Kbps, rssi -22
3-4 sec avg tp 743 Kbps, rssi -22
4-5 sec avg tp 743 Kbps, rssi -22
743 Kbps,逐秒恒定,RSSI 很近、链路很干净——这不是「有问题」,这就是 1M PHY 的满带。BLE 默认跑 LE 1M PHY,标称 1 Mbit/s 原始码率,但前导、访问地址、包头、CRC 这些固定开销,加上连接事件时间片的空转,应用层载荷封顶就在 700–750 Kbps 这档。应用层配得再满也破不掉物理层的顶。
要突破,只有把 PHY 从 1M 协商到 2M(LE 2M PHY,BLE 5 特性,需双方都支持)。这是吞吐能否翻倍的先决条件,也是最容易被忽略的一项——很多团队调参数调半天,从来没确认过自己实际跑在几 M 的 PHY 上。
协商到 2M 之后:
10-11 sec avg tp 1296 Kbps, rssi -24
11-12 sec avg tp 1296 Kbps, rssi -24
12-13 sec avg tp 1296 Kbps, rssi -23
13-14 sec avg tp 1296 Kbps, rssi -22
四、开关二:CPU 主频
同一颗 CPU,有两种主频模式:默认低功耗跑 32 MHz,不休眠可跑 128 MHz。同一份打流代码,两种主频下:
| CPU 主频 | 吞吐 | 说明 |
|---|---|---|
| 32 MHz | ~200 Kbps | 连接事件时间片内处理不过来 |
| 128 MHz | ~500 Kbps | 2.5 倍差距 |
满带打流时协议栈收发、LL 包处理、数据搬运全是 CPU 活,主频不够,连接事件的时间片内处理不过来,链路再好也喂不满。打流前先确认 CPU 跑在哪个档——这一条不看代码、只看配置,却经常被漏掉。
五、开关三:串口打印的实现方式(最狠)
这条最狠。调试打流时顺手加了几条打印,吞吐直接从 ~700 Kbps 掉到 ~20 Kbps,35 倍。
罪魁是打印的实现方式:这个平台的 printf 是「关全局中断 + 忙等发完一整行」的阻塞式。发一行打印的功夫,整个系统是冻结的——协议栈、定时器、调度全部停摆。打流热路径上每打一次,链路就断一截。
- 少打:逐包打印全删,收敛到每秒一条统计(观测密度的下限);
- 快打:串口波特率能上 2M 就上 2M,缩短阻塞时长;
- 换实现:确认平台的打印是不是「关中断 + 忙等」型——若是,热路径上要么换非阻塞异步打印(入队后台发),要么一根都不碰。
第 3 条最容易被漏:大家习惯性以为「加条打印没关系」,但在这种阻塞实现上,打印不是观测,是把吸管插进吞吐里。
六、三个开关拨完的完整链路
| 状态 | 吞吐 | 卡在哪 |
|---|---|---|
| 阻塞打印 + 32MHz + 1M PHY | ~20 Kbps | 每行打印冻结整个系统 |
| 32MHz + 1M PHY(打印收敛后) | ~200 Kbps | CPU 处理不过来连接事件 |
| 128MHz + 1M PHY | ~743 Kbps | 1M PHY 物理上限 |
| 128MHz + 2M PHY | ~1296 Kbps | 链路满带,与 BT SPP 持平 |
这三个开关没有一个是「BLE 知识」:PHY 是配置意识、主频是系统意识、打印是工程习惯。全是普通工程问题,只是都长在性能路径上。
七、排障速查:对着症状表查
| 症状 | 最可能的位置 | 先查什么 |
|---|---|---|
| 吞吐离谱地低(几十 Kbps 级) | 打印实现 | 是否「关中断+忙等」阻塞串口、逐包打印 |
| 吞吐卡在 200 Kbps 档上不去 | CPU 配置 | 实际主频档位 |
| 吞吐卡在 700–750 Kbps 恒定 | PHY | 当前连接实际 PHY 是 1M 还是 2M、对端是否支持 2M |
| 吞吐离 251 字节包模型算的数差好几倍 | 链路层配置 | DLE 是否真开到 251、MD 是否在单事件连发 |
八、验收清单
BLE 吞吐性能要立得住,测试前过一遍:
- 链路侧:当前连接实际 PHY(要实测确认,不是「应该协商到了」)、DLE 已扩到 251 且对端同步开启、MTU 247 双端一致、连接间隔确认是预期值(看连接更新回读值)。
- 系统侧:CPU 主频档位确认、打流线程优先级夹在「低于底层收发、高于统计打印」中间。
- 观测侧:打印收敛到每秒一条、波特率 2M、确认打印实现非阻塞型。
- 数据侧:逐秒统计窗口的均值 + 方差,多轮取稳态,RSSI 仅作参考(以综测仪为准)。
这些开关都拨到位之后,BLE 和 BT SPP 的满带吞吐就在同一量级了。下次再有人说「BLE 跑不快」,先问他:PHY 跑在几 M?CPU 在哪个档?打印是不是阻塞的?
本文由「嵌入式菜鸟日记」原稿优化改写,保留全部技术结论与实测数据。
