
做BLE开发这几年我越来越觉得链路层这件事光靠读应用层API和协议栈日志永远只能看到一半。另一半藏在你必须用空口日志才能看到的细节里连接请求里到底装了哪些参数、第一个跳频落在哪个信道、重传发生在哪一帧、连接失败到底是哪一方没有应答。这篇文章我拿一份实际抓包数据把BLE Link Layer Connection建立连接的完整过程拆开揉碎来讲从ADV_IND广播包一直说到CONNECT_REQ里的每一个字节再讲到连接建立后的链路层交互最后分享几个我用空口日志定位线上问题的真实案例。适合正在用SDK调BLE却总被连接参数、连接失败折磨的开发者也适合那些在协议文档里看得昏昏欲睡、想从波形角度理解BLE的入门者。1. 用好空口日志协议栈调试的最大误区与破局点1.1 为什么协议栈日志看不到真相很多朋友一开始调试BLE习惯性打开串口看LOG发现协议栈打印了一堆“connect success”或者“disconnected, reason 0x08”然后就开始猜是不是信号不好是不是干扰是不是兼容性问题我也经历过这个阶段坦白讲猜中率极低。原因很简单协议栈对上层暴露的接口是高度封装过的。你调用一条连接API底层状态机怎么在35ms内完成跳频、参数锁定、定时器初始化、信道重映射这些细节完全不会出现在应用日志里。出问题的时候应用层只能看到失败结果看不到失败发生在哪一步。空口日志解决的就是这个“看不见”的问题。它用一个额外的接收机在物理层监听两个BLE设备之间真实传输的射频数据包把所有在空气里飞过的PDU记录下来。这些PDU本身就携带了链路层状态机的全部证据不需要任何一方设备开口就能还原整个过程。1.2 空口日志到底能看到什么对于连接建立这个主题空口日志里值得关注的信息可以分成三类PDU类型与内容广播信道上的ADV_IND、SCAN_REQ、SCAN_RSP以及连接请求CONNECT_REQ里面每个字段都能逐字节看到。时序关系广播间隔到底多少、扫描请求和广播包之间的T_IFS是否规范、从收到广播到发出连接请求经过多少时间、第一个连接事件在什么时刻开始。射频侧信息包发在哪个信道、RSSI强弱、有没有CRC错误、有没有重传。这些信息组合在一起基本就能还原出BLE连接建立的全过程。你会发现很多应用层“玄学”问题其实是链路层的策略或参数问题比如广播间隔太大导致被扫描到之前就切走了或者连接请求发出的信道与设备实际监听的信道不一致。2. 从头搭建一套空口抓包环境2.1 硬件和软件选型从入门到半专业说实话现在做BLE空口分析的门槛比几年前低很多。我自己的经验是不需要一上来就上商用协议分析仪也不需要学院派那种实验台一套USB嗅探棒配合开源的协议分析平台已经能覆盖绝大多数连接建立和链路层Debug场景。硬件方面我用的是基于某款低功耗射频收发芯片的USB嗅探棒插上电脑后会被识别成一个网络接口设备。它在2.4GHz频段上可以连续扫描全部40个BLE信道包括37/38/39三个广播信道和0-36号数据信道。对于只看连接过程的场景这个能力完全够用。软件方面我用的是开源网络协议分析平台配合对应插件抓包文件格式是PCAP字段解析非常直观。它能把BLE的广播PDU、控制PDU、数据PDU按协议树展开鼠标点一下某个字节就能看到对应含义。如果你有商用的协议分析仪功能会更强比如能解析BQB认证需要的所有测试项但对日常排查来说性价比不划算。2.2 抓包前必须做好的三件事第一件事是确认信道覆盖。确保嗅探器设置为全信道监听不要用某个单信道模式。之前我不止一次遇到“怎么抓不到连接请求”的情况最后发现是嗅探器只锁在37信道上而两个设备跳到了38信道去建立连接。第二件事是设置过滤条件。BLE空中环境很嘈杂周围可能有十几个设备在广播不加过滤的话PCAP文件里全是噪音。我用的是设备MAC地址过滤直接在协议分析软件里填上目标设备的地址注意区分公共地址和随机地址配置前先确认。也可以按广播名称过滤比如只显示设备名为某个Demo设备的广播包。第三件事是物理摆放。嗅探器的天线不要贴着设备也不要隔着一堵墙距离半米到一米效果最好。天线方向和设备保持正对中间少放金属物体。不然会看到大量CRC错误或者漏包分析起来很痛苦。抓包动作本身很简单让嗅探器开始监听再让中心设备手机或者其他主设备去连接那个从设备等连接稳定后停止抓包保存PCAP文件。这个过程建议反复抓几组带异常的一起保存方便事后对比。3. 空口日志中连接建立全流程逐段拆解3.1 广播阶段ADV_IND包怎么看连接建立的第一步一定是广播者Peripheral把存在感打出去。在空口日志里你会看到广播者在三个广播信道37/38/39上周期性地发送ADV_IND包。这个包的结构看着简单里面信息量却很大。ADV_IND属于广播信道PDUPDU头里有四个要命的bitTxAdd表示发送者地址类型0代表公共地址1代表随机地址后面跟着的AdvA就是发送者对自己的完整MAC地址。Payload部分接着是AdvData里面是一个个AD Structure每个AD Structure就是“长度类型数据”的架子。最常见的几个AD类型包括Flags指示设备支持的具体模式、Complete Local Name设备名称、Tx Power Level发射功率。实际抓包里我经常直接看三个指标。一个是广播间隔两个ADV_IND包之间的时间差规范要求是advInterval加上0到10ms的伪随机延迟。默认配置往往是100ms如果设置了20ms空口日志上会看到非常密集的广播包。第二个是指定的广播信道。大多数设备默认在37/38/39三个信道上轮流广播如果设备只配了其中一个信道抓包时另外两个信道会非常安静。第三个就是RSSI。同一个设备在37信道上的RSSI和39信道上的RSSI经常差出10dBm以上。如果某个信道衰减特别厉害扫描端可能在某个时间段内听不到广播表现就是连接慢、不稳定。3.2 扫描阶段SCAN_REQ与SCAN_RSP的互动扫描者Scanner收到ADV_IND之后可以选择只读广播内容也可以主动再问一句“你还有没有什么别的没拿出来”。这句“问话”就是SCAN_REQ。SCAN_REQ的PDU头只有两个重要字段ScannerA是扫描者的地址AdvA是广播者的地址。它本质上是扫描者在广播信道上直接对广播者说我在这个信道上听到你了把你的扫描响应数据给我。广播者收到SCAN_REQ后会在同一个广播信道上、在T_IFS150微秒之后回一个SCAN_RSP。空口日志里最值得关注的是这组互动有没有发生、发生在哪个信道。如果扫描设备根本没发SCAN_REQ可能是广播状态不对或者上层压根没触发主动扫描。如果发了SCAN_REQ但没收到SCAN_RSP就要怀疑广播者是否在正确信道、是否有响应能力。这里有个细节很多人会忽略SCAN_REQ本身也是一个空口数据包它也会被嗅探器抓到。所以完全可以通过日志判断扫描行为到底是应用主动发起的还是协议栈自动做的。这种信息在你排查“手机为什么没发现设备”的时候特别有用。3.3 连接请求CONNECT_REQ里藏着全套连接参数当扫描者决定进一步建立连接它就要发送连接请求。这里先说明一个容易混淆的命名点在不同版本协议或不同分析软件里连接请求PDU可能叫CONNECT_REQ也可能叫CONNECT_IND规范演进过程中有过调整本质上都是同一个包。为统一下文就叫它CONNECT_REQ。CONNECT_REQ是在广播信道上发送的发完之后两个设备会约定好跳到一个数据信道开始真正的连接数据交互。这个包的长度比广播包大不少结构是InitA加AdvA再加上22个字节的LLData。这22个字节几乎全是决定连接行为的关键参数我把它们拆开来看LLData字段长度单位含义与作用Access Address (AA)4字节-连接专用的接入地址后续所有数据信道PDU的第一个字段都是它用于识别这个连接CRCInit3字节-数据信道CRC计算的初始值链路层用它来校验每个包的完整性WinSize1字节1.25ms第一个连接事件的可接收时间窗口大小WinOffset2字节1.25ms从CONNECT_REQ结束到第一个连接事件开始之间的偏移时间Interval2字节1.25ms连接间隔相邻两个连接事件起始时刻的周期Latency2字节连接事件个数从设备可跳过的最大连接事件数用于省电Timeout2字节10ms链路超时超过这个时间没收到有效包就判定连接失联Channel Map (ChM)5字节位图哪37个数据信道可用每位对应一个信道Hop5bit-跳频增量必须是1到16之间的奇数SCA3bitppm区间主设备的睡眠时钟精度影响事件窗口的补偿策略这些参数之间还有强约束关系。最关键的公式是Timeout必须大于约两个连接事件周期规范里要求 Timeout (1 Latency) * 2 * Interval。也就是说如果连接间隔是30ms从机延迟是4那超时至少要大于300ms。很多连接不稳定的问题追根到底就是参数配错导致两边都在同一个时刻判定对方失联。WinOffset和WinSize决定了连接建立后“第一步”怎么迈出去。主设备会在WinOffset这个时间点开始在WinSize这个窗口里向从设备发起第一个连接事件。空口日志里你能清楚看到CONNECT_REQ的结束时刻然后在约WinOffset之后数据信道上第一次出现带AA的数据包。有一个实际经验不要把WinSize设得太小。很多芯片对WinOffset和WinSize的处理会有几毫秒误差如果窗口太小从设备还没来得及把射频切到目标信道就已经错过监听窗口连接就会失败。我们一般会把WinSize放到20ms以上给双方都留出从容余量。4. 连接建立后的链路层交互验证4.1 第一个连接事件与跳频序列连接请求发出后两边并不是“直接连着”就完事而是进入“定期叫醒、同步对表”的节奏。这个节奏的骨架是连接事件Connection Event每个连接事件发生在某个数据信道上事件内双方可以交换一个或多个数据包事件结束则一起睡到下一个连接事件。空口日志最妙的地方在于可以直接看到跳频序列。主设备按照 ChM和Hop 算出下一个信道先由当前信道号加Hop得到目标信道号如果这个信道在ChM位图中被禁用就通过重映射表顺延到下一个可用信道。比如当前信道是5Hop是3目标就是8如果8被屏蔽就跳到9或者10具体要看可用信道映射。抓包时你会看到一个个连接事件按固定间隔出现信道号在每个事件之间按规律变化。如果某一次连接事件延迟了日志上会出现一个比正常间隔更长的空隙这通常是射频干扰导致收包失败、进入了重传或者退避流程。这是非常好的排查线索远胜过看协议栈“connection supervision timeout”的报错码。4.2 链路层控制PDU交换连接建立完成后两个设备会先交换一批链路层控制PDU来完成能力协商和参数确认空口日志里最常见的是三类LL_VERSION_IND两边互相通报协议版本号和子版本用于兼容性确认。LL_FEATURE_REQ / LL_FEATURE_RSP双方宣告自己支持哪些扩展特性比如LE数据包长度扩展、2M PHY等。如果某个设备不支持某个特性协商结果会在响应包里体现。LL_CONNECTION_UPDATE_IND主设备发起的连接参数更新请求包含新的Interval、Latency、Timeout相当于给连接“重新签约”。空口日志里看到这些包的时候往往连接已经进入稳定态。这时候建议把注意力放在“有没有重传”和“有没有跟预期匹配的控制包”上。比如如果手机端请求了连接参数更新但从设备没有回ACK包那一定是参数不满足约束条件或者根本没收到。这个判断在代码日志里通常看不清晰在空口里却一目了然。5. 用空口日志定位三个真实连接问题5.1 设备一直在广播但手机就是连不上有一次我调试一个原型机从设备在广播手机端扫描列表里能稳定看到设备名但一发起连接就失败。应用日志只反复打“connection fail”。我上了嗅探器盯了十几分钟发现了一个奇怪的现象手机发出了SCAN_REQ从设备也回了SCAN_RSP但迟迟没有CONNECT_REQ。后来把抓包时间轴拉宽发现了一个之前完全没想到的问题手机的扫描窗口和广播者的广播信道总是错开。从设备把广播配置成了只在37信道上发而且广播间隔很短手机在38信道上扫描的时间占比较高。虽然偶尔能在37信道上遇到广播包并完成扫描但当手机真正切到发起连接时信道上刚好没有广播包可触发连接请求。把广播策略改成三信道轮流广播后连接立刻顺畅了。这个过程如果没有空口日志只改参数瞎试效率会低很多。5.2 连接建立成功后立即断开另一个案例是两设备能连上但不到300ms就断开。应用日志显示“connection terminated by remote”。抓包发现CONNECT_REQ之后数据信道上也出现了第一个连接事件数据包说明双方确实切到了数据信道。但紧接着只过了两个连接事件就出现了LL_TERMINATE_IND错误码是0x08也就是连接超时。问题出在两个地方从设备没有在规定的连接事件里回应主设备的包同时连接间隔和超时参数过于激进。抓包里的时间戳显示主设备第一个连接事件发出后从设备延迟了约200ms才回应而超时参数只配了300ms余量太小。查下来发现从设备芯片进入了一个低功耗模式射频唤醒时间超出预期。最后在广播阶段就把连接间隔拉大到30ms、超时设成1秒问题消失。空口日志在这里扮演的角色是直接锁定了“事件延迟”和“超时参数”这两个变量的因果链。5.3 连接参数更新总是失败再分享一个参数协商的问题。设备连接后主设备发起了LL_CONNECTION_UPDATE_IND请求把连接间隔从20ms改到50ms。空口日志里能看到主设备反复发这个控制包但从设备始终没有ACK。用空口抓包和协议文档一对比就发现原因主设备请求的新间隔对应的超时参数没有满足“Timeout (1 Latency) * 2 * Interval”这个约束从设备直接认为这个连接参数非法选择不响应。修改了参数组合后协商一次成功。所以说参数协商类问题千万别光看代码返回码把请求包里的字段拆出来算一算问题原因往往就在字段组合上。6. 常见问题与排查技巧速查最后把用空口日志排查BLE连接问题时最常见的情况整理成一张表方便遇到问题直接对号入座。现象空口日志表现可能原因解决方向抓不到CONNECT_REQ只有ADV_IND和SCAN_RSP没有CONNECT_REQ嗅探器信道不匹配、发起连接的操作没触发、目标设备不在监听状态检查全信道监听确认发起端的确执行了连接动作核对过滤条件CONNECT_REQ后无数据CONNECT_REQ出现但数据信道没有第一事件从设备没在WinSize窗口内完成同步或参数冲突放大WinOffset与WinSize检查连接参数约束连接后立刻断开LL_TERMINATE_IND携带0x08超时错误某一方唤醒太慢连接参数过于激进增加连接间隔和超时排查低功耗模式唤醒时间广播看得见但连不上广播间隔正常但扫描窗口错开广播信道太少、广播间隔与扫描窗口频段不匹配开启三信道广播调整广播间隔增加广播事件数参数更新总失败主设备反复发LL_CONNECTION_UPDATE_IND无ACK新参数违反协议约束或从设备不支持某些特性检查超时/间隔/延迟组合确认特性协商结果排障中还有几个也算压箱底的小技巧。一是抓包时直接把目标设备MAC地址加入过滤能过滤掉99%的无关广播速度提升极大。二是看时间列时要把标尺切到微秒级因为T_IFS这类关键时序只有150微秒毫秒级根本看不出问题。三是判断重传不要纯看个数要看包序号和CRC错误标志有些协议分析软件会把CRC错误和重传包用不同颜色标记出来一眼就能瞄到。四是抓完包一定要保存原始文件不要只截图很多问题要离线放大时间轴才看得清。还有一点从实践里悟出来的经验空口日志是“事实层”它告诉你在空气里到底飞了什么包但它不会直接告诉你“原因”。原因要靠你把包内容和双方的代码逻辑、参数配置组合起来分析。所以调试BLE连接问题时最好的姿势是左手代码日志、右手空口日志两边对照着看单看任何一边都很容易误判。再往深处说空口日志这个技能一旦掌握再去读规范文档里那些图和工作流程都会有豁然开朗的感觉因为你已经见过每一帧在真实世界里长什么样。最后建议下一个项目调试连接问题时试着至少在前期投入半小时做一轮空口抓包记录建立基线数据之后再出问题就有参照物可比了。