同一份打流代码,BLE 上的吞吐可以从 20 Kbps 做到 1296 Kbps——前后相差 65 倍。决定权不在代码,在三个开关。三个都拨到位,BLE 能追平经典蓝牙 SPP;只要有一个没开,数字直接砍到零头。这篇只讲这三个开关,以及怎么一步步把它拨到位。

一句话结论:「BLE 天生慢」是个误会——它只是默认配置低。PHY、CPU 主频、打印实现,这三处拨对,BLE 和 BT 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 这档。应用层配得再满也破不掉物理层的顶。

怎么判断「恒定」是物理上限还是代码坑?看数字靠不靠近理论上限——SPP 篇里「逐秒恒定的低」是代码节流的指纹,这次恒定的 743 是物理上限的指纹。同样恒定,一个在代码里,一个在空口上。

要突破,只有把 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
1296 Kbps,1.75 倍,基本追平了 BT SPP 优化后的 1208 Kbps。经典蓝牙和 BLE,维度对齐打满之后是一个量级的。

四、开关二:CPU 主频

同一颗 CPU,有两种主频模式:默认低功耗跑 32 MHz,不休眠可跑 128 MHz。同一份打流代码,两种主频下:

CPU 主频吞吐说明
32 MHz~200 Kbps连接事件时间片内处理不过来
128 MHz~500 Kbps2.5 倍差距

满带打流时协议栈收发、LL 包处理、数据搬运全是 CPU 活,主频不够,连接事件的时间片内处理不过来,链路再好也喂不满。打流前先确认 CPU 跑在哪个档——这一条不看代码、只看配置,却经常被漏掉。

五、开关三:串口打印的实现方式(最狠)

这条最狠。调试打流时顺手加了几条打印,吞吐直接从 ~700 Kbps 掉到 ~20 Kbps,35 倍。

罪魁是打印的实现方式:这个平台的 printf 是「关全局中断 + 忙等发完一整行」的阻塞式。发一行打印的功夫,整个系统是冻结的——协议栈、定时器、调度全部停摆。打流热路径上每打一次,链路就断一截。

关于打印的三个层次对策:
  1. 少打:逐包打印全删,收敛到每秒一条统计(观测密度的下限);
  2. 快打:串口波特率能上 2M 就上 2M,缩短阻塞时长;
  3. 换实现:确认平台的打印是不是「关中断 + 忙等」型——若是,热路径上要么换非阻塞异步打印(入队后台发),要么一根都不碰。

第 3 条最容易被漏:大家习惯性以为「加条打印没关系」,但在这种阻塞实现上,打印不是观测,是把吸管插进吞吐里。

六、三个开关拨完的完整链路

状态吞吐卡在哪
阻塞打印 + 32MHz + 1M PHY~20 Kbps每行打印冻结整个系统
32MHz + 1M PHY(打印收敛后)~200 KbpsCPU 处理不过来连接事件
128MHz + 1M PHY~743 Kbps1M 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 在哪个档?打印是不是阻塞的?

本文由「嵌入式菜鸟日记」原稿优化改写,保留全部技术结论与实测数据。