ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蓝牙SPP吞吐从83Kbps到1.2Mbps:三个隐蔽瓶颈的定位与修复实录

蓝牙SPP吞吐从83Kbps到1.2Mbps:三个隐蔽瓶颈的定位与修复实录 上一篇讲了打流工具的设计——分层、双向角色模型、990B 包型。这一篇讲它投入使用后暴露的三个真实瓶颈也就是工具自己身上的三处病。产品要验证 BT 侧的速率协商和协议栈稳定性用打流工具跑了一轮每秒吞吐稳定在 83 Kbps 左右。经典蓝牙 EDR 的空口理论速率是 2.1 MbpsSPP/RFCOMM 层层开销打对折也不该只有零头。而且这个数字稳得可疑——不是抖来抖去的那种慢是逐秒打印出来都一模一样的那种慢。恒定说明瓶颈不是射频环境、不是对端而是代码里有一只手在掐着节拍器。三轮修复后83 → 804 → 1208 Kbps15 倍。三个瓶颈一个比一个隐蔽最后一个甚至不是蓝牙的问题。测试环境2 块相同开发板同固件各自经串口连 PCCLI 下发打流命令串口波特率 1M/2M。数据均为逐秒实测各取连续 5-6 秒稳态窗口。阶段修复项逐秒实测Kbps均值1修复前81 / 83 / 83 / 83 / 83 / 81~822去固定延时 修统计截断805 / 765 / 781 / 804 / 867~8043打印收敛1167 / 1113 / 1237 / 1330 / 1191~1208先看数据本身阶段 1 恒定阶段 2/3 波动。恒定的慢是人为节流的指纹——确定性的延时产生确定性的速率射频差、干扰这类真实瓶颈反而会抖。反过来波动回归说明节流解除、数字开始反映真实链路状态credit 水位、缓冲深浅。打流看到稳定得反常的低先怀疑自己的代码别先怀疑天线。坑一发送循环里的固定延时定位第一个看的自然是发送路径。发送循环长这样while (running) { ret spp_port_write(data_pkt); /* 990B 数据包 */ if (ret ! BT_OK) break; sys_sleep_ms(10); /* ← 包间固定延时 10ms */ }每包睡 10ms一包 990B算下来正好 80 Kbps 上下——数字对上了。当初加这个延时意图大概是别把底层压垮但它和流控是两回事底层缓存满了会通过写失败反馈根本不需要任务侧主动让路。这个延时不是流控是节流。而确定性节流产生确定性速率恰好还干扰定位——数字看着稳定容易被当成链路正常、速率就这样。修法满速 pump把什么时候等交给协议栈RFCOMM 自带 credit 流控对端收不过来时本端写会失败——这是忙不是错。所以修法分两段/* 任务侧满速 pump零延时 */ while (running) { ret spp_port_write(data_pkt); if (ret ! BT_OK ret ! BT_BUSY) { /* 非 BUSY 的才是真失败 */ log_error(write fail); break; } /* BUSY 落到循环里当没发生继续 pump */ } /* 适配层内部spp_port_write 里 */ if (流控导致写失败) { /* credit 耗尽 / 发送队列满 */ sys_sleep_ms(5); /* 退避后返回 BT_BUSY */ return BT_BUSY; } return BT_ERR; /* NULL 参数等才是真失败 */退避放在适配层而不是任务里是因为适配层离协议栈最近由它判定这不是失败是流控任务侧和其它所有调用方都不用各自猜。语义上任务只需认识三态BT_OK继续、BT_BUSY也继续、其它退出。5ms 怎么来的退避要等的是对端补回 credit 的 RFCOMM 往返——对端读走上层缓冲、归还 credit、credit 帧经无线回来量级约 1~10ms。取 1ms 偏紧对端 credit 还没归就急着重试连续打转白烧 CPU5ms 落在量级内。这是本平台调的经验值照抄没意义按各自平台实测调整。修完这轮数字该起飞了吧跑出来 180 Kbps 左右——有提升但远低于预期。这才引出第二个坑。坑二统计把 990B 记成 222B读数低估 4.46 倍180 Kbps 不对劲链路不可能这么慢。沿着数据流向逐层核对每个包到底按多长计入在统计入口发现static uint32_t iperf_show(uint16_t conn, const uint8_t *data, uint8_t len) /* ↑ uint8_t */统计函数的长度参数声明成了uint8_t。990 传进去回绕截成 222990 mod 256 222。每包少记 768B统计低估系数 990/222 ≈4.46 倍——真实链路早就在 ~800 Kbps 跑了仪表盘上只显示零头。/* 修复len 与适配层 length 同宽 */ static uint32_t iperf_show(uint16_t conn, const uint8_t *data, uint32_t len)修完统计逐秒数据立刻变成 765–867 的波动区间均值 ~804 Kbps——从恒定变成波动数字开始呼吸说明看到的是真实链路了。这类 bug 的恶心之处在于它不崩溃、不报错只是让仪表说谎。复盘时间线也很有意思坑一修完后实际链路已经 ~800 Kbps但统计截断把它显示成 180——如果当时信了仪表坑一的修复效果会被误判为只提升了 2 倍。性能问题排查先确认你的测量工具本身是对的否则优化会算错账。顺带协议里所有跨层长度参数后来统一排查了一遍——写路径的length本来就是uint32_t没问题只有统计入口这一处窄了。分层接口的参数宽度要跟数据包真实上限对齐这类检查建议在 code review 里专门过一次。坑三逐包 debug 打印串口 IO 吃掉三分之一吞吐统计修对后 804 Kbps离原厂宣称的 ~1200 Kbps 还差三分之一。继续找这次不在蓝牙路径上——在打印。打流代码里残留着逐包 debug 打印。满速下每秒 800~900 包这是每秒上千次串口输出。串口是阻塞性高频 IO每次输出都占 CPU、拖发送节奏——990B 的包好处理一次 printf 的开销比它还贵。修法全删逐包打印收敛为每秒一条统计就是本文开头那些[iperf tx]: 0-1 sec avg tp ...。两点展开串口波特率是吞吐链路的一环。低波特率115200 这档下逐包打印会把链路直接拖死——很多人复现不出高吞吐最后发现是打印没关。我们测试固定 1M/2M 波特率就是为了让串口 IO 不成为干扰项。数字死活上不去的时候先把打印关了再怀疑别的。打流期间的打印本身就是个干扰实验。每秒一条统计是观测密度的下限再低你看不到问题再高你就改变了被测系统。修完这轮1167 / 1113 / 1237 / 1330 / 1191均值~1208 Kbps达到原厂宣称水平。吞吐波动幅度比第二轮更大——串口 IO 释放后链路真正跑满数字跟着 credit 节奏起伏。附统计周期稳定的隐藏条件每秒统计靠软定时器触发这里的设计篇里讲过结论回调只做两件轻量事耗时活挪去 workqueue补充一下当时的实测背景原始实现把统计、读 RSSI、上报、重武装全串行塞在定时器回调里而软定时器回调跑在系统共享的软定时器线程上干重活会拖长它的关断窗口统计周期自己先漂了。这也是为什么验收清单里专门有一条每秒统计周期稳定。另说一句日志里rssi:-21这类数值是统计上报帧里带过来的信号强度只作参考展示——本版栈的 RSSI 接口在打流场景下取值不一定准定位链路问题时别把它当唯一依据以频谱仪/综合测试仪实测为准。补一个坑外坑打流线程的优先级要夹在中间三个坑修完数字达标了但还有一条实机调过的约束需要补在这里打流相关线程的优先级不能随便给。原则一句话打流是给底层数据处理打工的同时又需要比非实时的东西跑得快所以它的优先级要夹在中间不能高于底层数据处理线程协议栈收发/调度。pump 线程满速循环是全系统最能抢的执行流——如果它的优先级压过协议栈的数据处理线程pump 会把 CPU 抢走底层数据消化不掉写失败连环出现吞吐反而被自己掐死。测试工具的任务是喂饱底层不是抢赢底层。不能低于串口打印、日志等非实时线程。反过来如果打流/统计线程优先级太低被打印、后台任务长期压制发送节奏被拖乱统计窗口也跟着漂移——测出来的又是被调度干扰过的假数字。协议栈数据收发/调度线程 ← 天花板打流不能压过它 ↑ 打流 pump / 统计线程 ← 夹在这一层 ↑ 串口打印 / 日志 / 非实时任务 ← 地板打流不能被它们压住原理上就一句话测试线程要贴着被测系统的节奏走——比被测对象高你改变了被测系统比周边杂事低你测到的是杂事的干扰。具体数值按各自平台/RTOS 的实际优先级表填栈大小、优先级统一用宏管理调的时候一处改全局生效。三个坑的共性环节原问题修复贡献发送节奏包间固定延时 10ms满速 pump 适配层流控退避解除人为节流统计正确性uint8_t len截 990B 为 222B改uint32_t对齐读数低估 4.46× → 如实CPU/IO逐包打印千次/秒收敛为每秒一条串口 IO 释放效果链83 → 804 → 1208 Kbps15 倍。复盘下来三条经验恒定的慢优先怀疑代码确定性节流产生确定性速率真实射频瓶颈会抖先修仪表再修机器统计 bug 在场时第一轮修复的真实收益你算不清性能路径上没有无害的打印每个逐包打印都是插在吞吐上的吸管回头看三个瓶颈没有一个是蓝牙知识问题——延时是编码习惯、截断是类型宽度、打印是调试习惯全是普通工程问题只是恰好都长在性能路径上。吞吐上不去的时候别急着怀疑协议栈和射频对着这张表先自查一遍你看到的症状最可能的位置先查什么吞吐恒定地低逐秒数字几乎一样发送循环里的人为节流包间固定延时 / sleep吞吐忽高忽低、量级正常真实链路状态credit 水位、射频环境、对端消费速度吞吐数字离谱地小差好几倍统计路径长度参数位宽uint8_t 回绕、统计口径吞吐比预期差一个固定比例打印 / 日志 IO逐包 debug 打印、串口波特率尾声两个收尾时的实机教训主流程通了收尾阶段还有两个坑都实机踩过值得记录。STOP 时刻意不清 role防 ping-pong先说下停止是怎么走的。发起方CLIENT想停的时候并不是自己单方面算数就完了它发一个 STOP 帧过去对端收到后要回一个 echo——CLIENT 是在等到这个 echo 回来后才算正式结束。这时它会算好整场平均吞吐、把自己的 role 清掉、之后收到什么都不再回声。问题就出在清 role这个动作的时机上。直觉上我都要停了先把状态复位了再等 echo不是很合理吗实测恰恰相反——stop 发出去以后role 得留着等 echo 回来再清。因为 echo 回来的那一下代码要靠 role 判断这是我发起的测试的回音如果提前把 role 清了这帧 echo 会被当成对端发来的新命令走 SERVER 分支又回一个 echo对端收到再回……两边就这么一来一回回不完最后停不下来平均吞吐也算不出来。而测试结束后万一有残留没清干净的 role也不用心慌断链回调会兜底清一次链路都断了不可能再有 echo 来。一句话总结收尾的时候看起来该顺手清掉的状态要等流程真正走完再清。清理的时机本身是个设计决定不是顺手的事。STOP 小包发不出去还有个更实际的麻烦满速打流时链路上全是 990B 的大包在跑这时去插一个 8 字节的 STOP 小包要么被 pump 挤掉要么正赶上流控失败——命令发不出去想停停不下来。修法很直接先把自己这边的 pump 停掉让链路空出来再发 STOP失败了就重试几次。先停再发用几次重试换一次送达实机验证通过。这是打流系列的排障篇。工具本身的设计——分层、双向角色模型、990B 包型的由来——在另一篇里展开。两篇合起来就是一个测试工具也值得详细设计的完整论证。系列文章 - 第 1 篇蓝牙SPP打流工具设计详解分层架构、双向角色模型与990B包型 - 本文第 2 篇蓝牙SPP吞吐从83Kbps到1.2Mbps三个隐蔽瓶颈的定位与修复实录相关标签蓝牙SPPRFCOMM吞吐优化性能排查嵌入式
返回列表