![【LMP】规范精讲[3]: 一条链路的一生——建链、经营与散场](http://pic.xiahunao.cn/yaotu/【LMP】规范精讲[3]: 一条链路的一生——建链、经营与散场)
建链那 90 毫秒里发生了什么功率为什么像锯齿一样升降断连时 6×Tpoll 到底在等谁这篇把连接控制掰开揉碎讲清楚。前面两篇把 LMP 的信令骨架PDU 格式、事务与碰撞规则、特性交换过了一遍。那些是语法今天这篇讲的是语义——连接控制LMP 里条目最多的一个大家族从建链握手到断连收尾一条 ACL 链路要经历的所有管理动作都在这里面。目录一、开张90 毫秒的参数全家桶1.1 page 结束之后谁先开口1.2 建链后的参数下发二、经营运行态的八个子系统2.1 功率控制三条腿走路的闭环2.2 AFH跳频地图的动态改道2.3 信道分类情报上抛机制2.4 监督超时链路的死亡判决书2.5 CQDDR看路况换车型2.6 QoS轮询节奏的两副面孔2.7 多时隙包保守起步的授权制2.8 EDR 包型表换调制先封路2.9 封装 PDU大件物流的拆装规则2.10 Ping带防伪的心跳2.11 时钟调整微调表针的两种手法2.12 SAM错峰营业的时隙地图三、打烊两种告别方式3.1 Detach 的全套规矩3.2 空口现场三次告别四、换老板不关店角色切换五、代码时间5.1 解析 AFH 信道地图5.2 Detach 计时状态机六、收个尾七、测验我拿一份手头的空口抓包做伴读素材是一条真实链路的完整生命周期从 17:03:50 建链到 17:05:40 结束中间还夹了一次掉线重连和一次角色切换。先上全景图把这篇文章的骨架立起来一条链路的一生可以切成三段开张建链与参数初始化、经营运行态的各种闭环控制、打烊断连。外加一个特殊事件换老板角色切换。下面按这个顺序展开。一、开张90毫秒的参数全家桶1.1 page 结束之后谁先开口Baseband 的 page 过程建立的是物理层同步——两边对上了跳频序列和时钟。但 LM 层的建链是另一回事。规范在连接控制一开篇就画了那张经典流程图注意 page 框下面那行字允许立刻发起的 LMP 过程只有五个clock offset request、version、supported features、name request、detach。为什么偏偏是这五个看性质就明白了——clock offset 是物理层刚同步完顺手就能量出来的值version 和 features 是静态只读信息name 是缓存过的老数据detach 则是给对端随时保留的退出通道。全是轻量、无副作用、不需要 Host 参与的东西。而真正涉及上层的连接要等一条专门的 PDUWhen the paging device wishes to create a connection involving layers above LM, it sends an LMP_HOST_CONNECTION_REQ PDU. When the other side receives this message, the Host is informed about the incoming connection.这句话的关键在第二句。LM 层的链路通了不代表对端的 Host 知道这件事——HOST_CONNECTION_REQ 的本质作用是把对端的 Host 从床上叫起来。对端 Host 有权说不回一条 LMP_NOT_ACCEPTED发起方就要立刻走断连流程。协议设计里这种 Host 否决权的例子不多这里是第一个。抓包里的现场是这样的17:03:50.437Host Connection 被 Accepted36 毫秒后17:03:50.473双方完成 SETUP_COMPLETE。这两条 PDU 夹着配对认证的整个流程——本例里中间穿插了 SSP 配对、鉴权、加密启动全部在 2 秒内完成。SETUP_COMPLETE 的语义值得逐字看When a device is not going to initiate any more security procedures during connection establishment it sends an LMP_SETUP_COMPLETE PDU. When both devices have sent LMP_SETUP_COMPLETE PDUs the traffic can be transferred on the BR/EDR ACL logical transport.两个要点一是它只声明安全流程结束不是所有初始化结束二是必须双向都发业务数据才放行。这是个两阶段收口——单方面的开门不算开门。1.2 建链后的参数下发SETUP_COMPLETE 之后立刻就是一顿密集的参数下发。看空口从 17:03:50.434 到 17:03:50.511约 80 毫秒内发生了功率直接拉到最大、AFH 地图下发、信道分类上报开关打开、Max Slot 协商到 5 时隙、EDR 包型表启用 2/3Mbps、CQDDR 自适应速率开启、时钟偏移读取、监督超时设为 5 秒。这张截图值得逐行读因为它就是整篇文章的目录预告功率控制第一个动作是 Increase to maximum power——新链路信号未知先按最坏情况打满再靠闭环往下调。宁滥勿缺先通再优。SET_AFH 紧跟着下发裁剪地图本例 20 个信道——跳频改道这件事从建链第一毫秒就开始了。Channel Classification Request 打开了从设备的上报通道——地图不是拍脑袋定的是靠持续情报喂出来的。Max Slot Request 双方各发一次都要求 5 时隙——大包传输的授权过程。Packet Type Table 协商到 2/3Mbps——EDR 调制解调模式的选择。Auto Rate 和 Preferred Rate 一发一收——CQDDR 速率自适应的握手。Clock Offset Transaction——把对端时钟偏移读出来存着下次 page 能快几百毫秒。Supervision Timeout 5 秒——给这条链路预设了死亡判决时限。每一条后面都对应一节下面进入经营篇逐个展开。二、经营运行态的八个子系统2.1 功率控制三条腿走路的闭环蓝牙的功率控制容易被人小看以为是简单的升降。实际上规范里写了一套相当细的规则。先看两套机制的分界。legacy 功率控制用 LMP_INCR_POWER_REQ / LMP_DECR_POWER_REQenhanced 用 LMP_POWER_CONTROL_REQ / LMP_POWER_CONTROL_RES。切换规则只有一条After the Link Manager supported features responses have been processed, if both devices support enhanced power control then only enhanced power control shall be used. Otherwise, if either device supports only the legacy power control mechanism then only legacy power control shall be used.木桶效应——任一方不支持 enhanced整条链路就退回 legacy。而且切换点是特性交换完成那一刻之前甚至可以先用 legacy 发一条应急调整。抓包里 17:03:50.434 那条 Increase to maximum power 就是 enhanced 功控的 all the way to max在特性交换刚完成时就发出去了。enhanced 功控的三种请求档位one step up、one step down、all the way to max。应答方要在新的功率档上发送 LMP_POWER_CONTROL_RES——这一条是闭环的精髓应答报文本身就是新功率的第一块试金石发起方收到它的 RSSI 直接就是下一轮决策的输入。还有一个反直觉的单向锁定规则If the receiver of the LMP_POWER_CONTROL_REQ PDU has indicated that the output power levels for all of the supported modulation modes are at maximum the requesting device shall only request an increase again after having requested a decrease at least once.已经到顶了还反复请求升功率不行必须先请求一次降功率解锁。这是防抖设计——避免两个决策鲁棒的实现在极端场景下互相拉扯。SPEC 对功率档位本身还有一组约束属于射频侧的底层配套相邻档位步长最小 2dB 最大 8dB最低档应低于 -30dBm所有包类型的头部功率差不能超过 10dBClass 1 设备最大 100mW / 20dBm必须能把功率降回 4dBm 以下——不发功控消息的链路发射功率不得超过 Class 2 上限。这些数字在音频耳机这种 Class 2 设备上感知不强但在车载、工业这些 Class 1 场景里就是硬指标。看稳态下的真实波形17:04:49 降一步、17:04:50 升一步、17:04:52 升到顶应答 Max power、17:04:56 连降两步——典型的搜索-保持-再搜索循环。注意 17:04:49.597 那条降功率请求标着 No Response From Peripheral大约 1 秒后链路直接改发升功率请求并成功。空口上一条 LMP 请求丢了不代表链路有问题——基带重传和上层超时各管各的功率控制这种高频小事务有自己的容错节奏。上面是规范里的 legacy 功控序列图Sequence 3 和 4 是两条边界应答MAX_POWER 和 MIN_POWER。到顶了、到底了都要明说。2.2 AFH跳频地图的动态改道AFH 解决两个问题自己的链路被 ISM 频段里其他东西干扰时绕开坏信道同时自己也少去干扰别人。规范一句话交代了使用范围AFH shall only be used during Connection state——page、inquiry 阶段一律全 79 信道跳因为那时双方还没有协商的信道。核心 PDU 是 LMP_SET_AFH三个参数The LMP_SET_AFH PDU contains three parameters: AFH_Instant, AFH_Mode, and AFH_Channel_Map. The parameter, AFH_Instant, specifies the instant at which the hopset switch shall become effective. This is specified as a Bluetooth Clock value of the Centrals clock, that is available to both devices. The AFH instant is chosen by the Central and shall be an even value at least 6×Tpoll or 96 slots (whichever is greater) in the future. The AFH_Instant shall be within 12 hours of the current clock value.Instant 机制是整个 LMP 的一个通用设计模式所有涉及时序切换的操作AFH、时钟调整、角色切换、Sniff 进入退出都不用隐式生效而是约定一个未来的时钟刻度双方到点同时切。这个刻度用 Central 的时钟表示——因为它是整个 piconet 唯一的公共时间基准。为什么至少 6×Tpoll 或 96 slots给基带确认和调度留时间。SET_AFH 发出后要等两层确认Baseband ACK 确认从设备收到了instant 过了确认从设备切过去了。Central 在收到 ACK 且 instant 未到之前不许发新的 SET_AFH。上限 12 小时则是防整数溢出式的荒谬值。还有一条容易忽略的规矩使能/更新 AFH 前Central 要先暂停 ACL-U 逻辑链路上的业务——先封路再改道。收到 Baseband ACK 后恢复传输。空口很有意思。本例全程信道数稳定在 20但地图编码几乎每次都在变——0x1A67C5C、0x1A68E60、0x1A69C3E……这说明什么20 个信道的集合没大变但具体选哪 20 个一直在微调。每次从设备上报信道分类Central 都重新评估地图哪怕结论只是换掉一两个边缘信道。跳频改道不是一次性的大动作而是持续的小步快跑。另一个值得注意的细节嗅探窗口约 17:04:05 到 17:04:33内Central 照常三次微调地图但从设备的分类上报整整静默了 28 秒退出嗅探后 73 毫秒立即恢复。规范只要求 Hold 模式下从设备不执行分类上报功率都停了量了也白量嗅探下的静默是实现自己的节能策略——嗅探窗口外收音机根本不开自然没法量信道。2.3 信道分类情报上抛机制上一节说了 Central 会根据上报改地图这一节说上报本身。从设备把 79 个信道按相邻两个一组分成 40 对每对报告 good、bad 或 unknown 三种状态。对内两信道状态不一致时有明确的吸收规则good unknown 报 goodbad unknown 报 badgood bad 的处理由实现自定。为什么要成对省空口开销——40 对每组两比特比逐信道报告紧凑。上报的节奏由三个参数控制都装在 LMP_CHANNEL_CLASSIFICATION_REQ 里AFH_reporting_mode开关、AFH_min_interval最小上报间隔、AFH_max_interval最大上报间隔。这是标准的令牌桶式限速——太频繁浪费空口太稀疏地图过期两端给夹住。本例里从设备实际上报间隔约 1~3 秒属于相当积极的档位。地图更新间隔约 2~7 秒比上报周期长——Central 不是每份情报都采纳攒几次再改道避免地图抖动。有个架构性的认识值得强调上报的是从设备的局部视角裁决权在 Central。每台从设备只知道自己收发时经历的信道质量多个从设备的情报在 Central 汇总成一张对全 piconet 生效的地图。这是中心化决策——好处是一致性坏处是 Central 的地图算法成了单点做不好全 piconet 陪葬。2.4 监督超时链路的死亡判决书每个物理链路上都有一个监督计时器规格Part B 3.1说得直白Upon reception of a valid packet header with one of the Peripherals addresses on the physical link, the timer shall be reset. If at any time in Connection state, the timer reaches the supervisionTO value, the connection shall be considered disconnected.只要收到一个带着自己 LT_ADDR 的有效包头计时器就归零。注意是包头就行——哪怕载荷全错至少证明对端还活着、还在按跳频序列发包。计时器到点链路宣告死亡无论谁在什么状态。为什么需要这个看似粗暴的机制SPEC Part B 原文给了答案Since this can happen without any prior warning, it is important to monitor the link on both the Central and the Peripheral side to avoid possible collisions when the logical transport address is reassigned to another Peripheral.关键词是 LT_ADDR 复用。3 比特的 LT_ADDR 只有 7 个有效值piconet 里设备一多就得回收复用。如果旧链路没死透、新链路已经开始用同一个地址两个从设备会对同一个包头做出响应——空口直接乱套。监督超时就是给地址复用兜底的死亡证明到了时限没收到包就视为物理层已断地址可以放心交给下一家。LMP 层的 LMP_SUPERVISION_TIMEOUT 是单向下发的通知从设备无权拒绝。空口下发 8000 slots 5 秒。有意思的是主机侧的细节看 HCI 证据主机 写了一次 Write Link Supervision Timeout (5s)紧接着 又写一次第二次收到 Command Disallowed。为什么因为第一次已经生效了控制器拒绝重复配置。而重连后的 出现了 HCI Link Supervision Timeout Changed 事件——控制器先于主机指令改了超时值角色切换后新 Central 重发了全家桶见第四节主机随后补写确认。同一个 5 秒值三条消息三种触发路径控制器的自主性和主机的期望在这里碰了一下。监督超时的值怎么选太小——正常嗅探间隔里没包就误判死亡太大——真断链了要干等才知道。本例 5 秒对 500ms 嗅探间隔的链路是 10 倍冗余留足了重传和调度抖动的余量。2.5 CQDDR看路况换车型信道质量驱动的数据速率变化机制轻巧设备 A 发一条 LMP_AUTO_RATE 说我开通了此业务之后设备 B 随时可以用 LMP_PREFERRED_RATE 建议 A 用什么档位的包——是否带 2/3 FEC、几个时隙、BR 还是 EDR 速率。注意用词是建议may request / preferredA 可以不听但有一个硬约束Device A shall not send a packet that is larger than max slots even if the preferred size is greater than this value.CQDDR 只能建议换车型不能突破道路限宽——限宽由下一节的 Max Slots 说了算。本例建链时 AUTO_RATE 和 PREFERRED_RATEBRFEC、EDR3Mbps5时隙包都出现了说明这条链路全程开着速率自适应。Data_Rate 参数的设计有个小巧思一个字节里同时装了 BR 和 EDR 两套速率建议BR 模式看 bit 0~2、EDR 模式看 bit 3~6且规范明确要求支持 EDR 的设备两套建议必须同时有效。为什么因为链路随时可能在 BR 和 EDR 包型之间切配合下一节的 ptt 协商如果建议值只有一个当前模式的档位模式一换建议就失效了还得重发一次 PREFERRED_RATE。两套并存等于把换挡建议做成模式无关的静态配置。2.6 QoS轮询节奏的两副面孔Tpoll 的定义值得背下来Central 对某个特定从设备两次 ACL 发送之间的最大时间间隔。它是 piconet 调度的节拍器——每个从设备的 Tpoll 就是它分到的带宽和时延预算。QoS 有两副面孔规格里写得清楚The Central notifies the Peripheral of the new poll interval and NBC by sending the LMP_QUALITY_OF_SERVICE PDU. The Peripheral cannot reject the notification.通知LMP_QUALITY_OF_SERVICE不可拒绝——Central 是调度者它说了算。请求LMP_QUALITY_OF_SERVICE_REQ可以谈——双向都能发起Central 可以接也可以拒。NBC 是广播包重复次数只有 Central 发给从设备时才有意义从设备发的请求里这个字段直接被无视。Tpoll 的承诺在 Active 和 Sniff 模式下都要兑现但例外清单很长page/page scan/inquiry 撞车、本 piconet 或其他 piconet 的关键 LMP 序列、基带关键序列page 响应、建链后第一个 POLL 之前、角色切换。翻译一下——Tpoll 是尽力而为的软承诺调度冲突时优先保命不保点。2.7 多时隙包保守起步的授权制ACL 包可以占 1、3、5 个时隙。占的时隙多一次拉的数据多但代价是对端要陪听更久、piconet 里其他从设备被压得更狠。所以这是个需要授权的东西。两条 PDU 分两种语义LMP_MAX_SLOT 是我允许你用几时隙LMP_MAX_SLOT_REQ 是我请求用几时隙。一条给权限一条要权限。关键规则After a new connection (as a result of page or page scan), or after a role switch, the value shall be 1 slot.每次新建链路或角色切换后默认值强制回 1 时隙。为什么这么保守角色切换意味着 piconet 参数全变跳频序列跟着新 Central 的地址和时钟重算谁也不知道切换后空口质量如何——先用最小包探路确认没问题再逐步放开。本例建链时双方各发一条 MAX_SLOT_REQ5 slots都被接受注意是双方——A 想发大包要 B 同意B 想发大包也要 A 同意权限是对等的。HCI 侧能看到对应事件HCI Max Slots Change (Slots5)在空口协商完成后几毫秒内上报主机。2.8 EDR 包型表换调制先封路EDR 用 pttpacket type table选择 ACL 链路上允许的包和调制模式。协商协议本身简单一条 LMP_PACKET_TYPE_TABLE_REQ应答 ACCEPTED_EXT 或 NOT_ACCEPTED_EXT但流程有三板斧Before changing the packet type table, the initiator shall finalize the transmission of the current ACL packet with ACL-U information and shall stop ACL-U transmissions. It shall then send the LMP_PACKET_TYPE_TABLE_REQ PDU.先把手头带业务数据的 ACL 包发完、停发业务、再发协商——和 AFH 一样的先封路再改道。接受方也一样确认收完、停发、切换表、应答收到应答的 Baseband ACK 后才恢复传输。任何时刻空口上都不存在用不同调制解调的半切换状态。还有一个细节改 ACL 的 ptt 不影响同 LT_ADDR 上 SCO 链路的包型。语音链路的包型在 eSCO 建链时就锁死了不跟着数据链路的调制变。2.9 封装 PDU大件物流的拆装规则有些事务配对的公钥就是典型载荷超过 16 字节而单个 LMP 消息装不下。封装机制来了一个封装 PDU 1 个 header N 个 payload 分片每片 16 字节最后一片可以零填充。最有意思的是这份豁免清单——封装传输进行中以下九种 PDU 可以随时插进来而不算事务碰撞信道分类、功率升降请求、MAX/MIN POWER、DETACH、MAX_SLOT、PREFERRED_RATE、SET_AFH。想想为什么是这九个功率控制和信道分类是高频心跳断连是紧急出口MAX_SLOT 和速率建议是轻量配置——全是短平快、跟被封装的大事务没有任何状态耦合的 PDU。碰撞规则里规定的是不同事务不能并发这九个天生就是无状态的散兵游勇放行它们既不破坏封装事务的原子性又保住了链路管理的实时性。2.10 Ping带防伪的心跳Ping 是安全连接配套的特性双方都要支持 Ping feature。表面看是心跳检测——LMP_PING_REQ / LMP_PING_RES 一问一答。但规范里那段 Note 才是重点Since the LMP_PING_RES PDU contains a MIC and will update the packet counter, the sender of the LMP_PING_REQ PDU can verify that packets with earlier packet counter values have not been deliberately suppressed by an attacker.MIC 是消息完整性校验包计数器每发一个带 MIC 的包就加一。攻击者如果想吞掉之前某些包比如吞掉一条降权限的指令计数器就会出现空洞——ping 一下对端必须回带 MIC 的包计数器必须连续。这不是简单的心跳是带防重放、防抑制的活体检测。触发时机也讲究Host 设了一个 authenticated payload timeout超过这个时限没收到任何带 MIC 的包LM 就该主动 ping——而且是赶在超时前提前 ping给对端留出响应时间。2.11 时钟调整微调表针的两种手法piconet 时钟就是 Central 的表但有时候 Central 要把表调一下——典型场景是对齐外部时间基准比如音频广播系统里的多 piconet 同步。两种手法粗调Coarse Clock Adjustment一步到位拖拽Clock Dragging温水煮青蛙。这个 Instant 的约束比 AFH 松AFH 要 6×Tpoll 或 96 slots因为时钟调整不改跳频序列只是改表——不需要给跳频内核留切换时间。Spec Part B 里那张经典的正调整图Figure 8.14值得展开讲clk_adj_instant 设在 12 号槽clk_adj_slots6clk_adj_us400µs。表针往前拨 6×6254004150µs。新时钟域的第一个槽从 18 号开始但 400µs 的 offset 意味着这个槽已经过去了 400µs——只剩 225µs 的尾巴这个残缺的槽不可用。传输不能从槽中间开始所以新时钟域的第一个完整可用槽要等到 20 号Central 发送槽。负调整则相反第一个槽要在 instant 之后 400µs 才开始之前那 400µs 是上一个槽的延续同样不可用。两种方向都会浪费一个槽——调表是有成本的这也是为什么有 Clock Dragging 作为温和替代每次只挪 1µs 级别靠着 clk_adj_dragTO1 秒超时的分段机制慢慢拖到位不产生不可用槽。从设备也可以请求调表LMP_CLK_ADJ_REQ而且有个很优雅的参数设计——Clk_Adj_Period。原文例子Clk_Adj_Slots18、Clk_Adj_Period48意味着 18、66、114、162、210 个槽的调整对从设备等效Central 可以在这组值里挑一个跟其他从设备需求冲突最小的。多目标优化问题用一组周期解来松弛这个设计我很喜欢。还有一个错误码专门为拒绝但帮忙的场景准备0x40Coarse Clock Adjustment Rejected but Will Try to Adjust Using Clock Dragging——粗调我拒绝了但我会用拖拽帮你调到位。拒绝里带着承诺。最后是并行限制这是 instant 类操作的通用红线任何带着 instant 或时序控制标志的事务角色切换、AFH、SCO、eSCO、嗅探、嗅探子评级、Hold、加密暂停恢复进行中不许发起时钟调整时钟调整 instant 未到、拖拽超时未过、恢复模式未出也不许发起那些事务。否则报 Different Transaction Collision (0x2A)。所有改时序的操作必须严格串行——两件事同时改表表就乱了。2.12 SAM错峰营业的时隙地图SAMSlot Availability Mask是给共存场景准备的——设备里有 LTE、WiFi 等其他无线时某些时隙得让出去。机制是把时隙按周期分成可用/不可用两类双方按地图错峰收发。三步曲设计得很干净LMP_SAM_SET_TYPE0先定义 type0 子图一种特殊的外部不可用时隙模式必须先定义后引用——在 DEFINE_MAP 里用了没定义过的 type0直接报 0x41 错误。LMP_SAM_DEFINE_MAP定义完整地图子图数量 NSAM_SM、子图长度 TSAM_SM∈[2,56] 偶数、每子图 2 比特类型。NSAM_SM0 表示删除地图。SAM_Index 不能是 0xFF保留给禁用也不能是当前选中的图。同一 SAM_Index 重复定义时新参数直接覆盖旧的。地图里可以引用 type0 子图但前提是 SET_TYPE0 已经跑过——没定义就引用报 0x41。LMP_SAM_SWITCH切换到某张图或用 0xFF 禁用 SAM。DSAM 是锚点偏移SAM_Instant 是激活时刻——instant 机制第三次出现了。而且这条有个狠细节The new SAM slot map may be activated at SAM_Instant even if the Baseband acknowledgment is not received——不等确认到点就切。跳频序列是双方各自的本地计算切错了顶多对不上跳SAM 地图如果两边不同步一边以为让出去了一边还在发状态直接分裂。所以宁可单方面到点硬切也要保证 instant 的确定性。SET_TYPE0 里的 Update_Mode 参数有三个值语义分层清晰0 表示作废所有引用了 type0 子图的现有地图对应索引在重新定义前不可用1 表示新 type0 子图立即生效2 表示下一个子间隔开始生效。如果当前激活的地图里有 type0 子图Update_Mode 不许设 0否则收到 0x1E 拒绝——不给你作废正在使用的地图。SAM_SWITCH 里则有两个防御性设计SAM_Instant 用 28 位 Central 时钟的 bit 27:1 表示Timing_Control_Flags 专门对付时钟回绕问题——蓝牙时钟约 23.3 小时回绕一次instant 如果落在回绕点附近不提前约定初始化规则就会出现今天和明天两个候选刻度的歧义。与两个模式的关系规则角色切换瞬间强制禁用SAM两边都要失败则恢复切换前参数。角色切换本身 piconet 时钟都换了SAM 锚点全部失效重新配置。嗅探优先于 SAM如果嗅探节奏无法跟 SAM 对齐理想情况是嗅探锚点落在可用槽、嗅探周期是 TSAM 整数倍嗅探配置说了算——哪怕要在 SAM 标记不可用的槽上收发。退出嗅探后 SAM 地图恢复效力。这个优先级安排透露了价值排序电源管理嗅探的确定性 共存礼貌SAM。共存是锦上添花链路保活是底线。三、打烊两种告别方式链路的死亡有两种好聚好散的 Detach无声无息的监督超时。前者是协议告别后者是法定死亡。3.1 Detach 的全套规矩Detach 的 PDU 只有一个 Error_Code 参数但断链握手的规定是整个 Connection control 里最细的段落之一值得整段引用The initiating LM shall pause traffic on the ACL-U logical link. The initiating LM then queues the LMP_DETACH for transmission and it shall start a timer for 6×Tpoll slots where Tpoll is the poll interval for the connection. If the initiating LM receives the Baseband acknowledgment before the timer expires it starts a timer for 3×Tpoll slots. When this timer expires, and if the initiating LM is the Central, the LT_ADDR(s) may be re-used immediately. If the initial timer expires then the initiating LM drops the link and starts a timer for Tlinksupervisiontimeout slots after which the LT_ADDR(s) may be re-used if the initiating LM is the Central.拆解一下发起方的流程先停业务ACL-U 暂停——又是先封路。发 DETACH起 6×Tpoll 计时器等 Baseband ACK。收到 ACK → 再等 3×Tpoll → Central 可以立刻复用 LT_ADDR。没收到 ACK → 直接掉链等监督超时那么久 → 才能复用 LT_ADDR。接收方对称收到 DETACH 后 Central 等 6×Tpoll、从设备等 3×Tpoll然后拆链。如果 DETACH 永远没送到空口太差监督超时兜底判死。为什么发起方和接收方的等待时间不对称3 vs 6想一下各自在等什么。发起方收到 ACK 后还等 3×Tpoll是给接收方留出完成它那 3×Tpoll 等待的时间——保证我这边复用地址发新包时对面的旧状态已经清干净了。接收方收到 DETACH 就证明发起方至少发出来了但发起方可能还没收到 ACK——所以接收方多等一倍6×Tpoll覆盖发起方 6×Tpoll 的 ACK 等待窗口。每个等待时间都精确对应另一边的某个阶段像两把互相咬合的齿轮。中间还有一段 Hold/Sniff 的特殊规则If the connection is in Hold mode, the initiating LM shall wait for Hold mode to end before initiating the procedure defined above. If the connection is in Sniff mode, the initiating LM shall perform the procedure to exit Sniff mode before initiating the procedure defined above. If the procedure to exit Sniff mode does not complete within the LMP response timeout (30 seconds) the procedure defined above shall be initiated anyway.Hold 里的从设备收音机是关的发了也白发——等 Hold 结束再发。嗅探里的从设备只在嗅探窗口醒来——先退嗅探再发 Detach。但退嗅探本身要通信极端情况下通信不了怎么办30 秒硬上限到点强行发。断连是最高优先级的意图任何中间状态都不能无限期阻塞它。最后一条冷知识链路上有 SCO/eSCO 时收 DETACH 会隐式拆除所有同步链路发起方不用挨个发拆除指令。断 ACL 等于断全部——同步语音链路寄生在 ACL 的物理链路上皮之不存毛将焉附。3.2 空口现场三次告别抓包里 110 秒内出现了三次 Detach原因码全是 Remote User Terminated Connection对端用户主动断开第一次比本次建链还早 0.3 秒——上一条短命探测链路的收尾。上一条链路刚断新链路紧跟着就建了空口上两代链路的挥手几乎首尾相接。第二次由耳机侧发起结束了 68 秒的稳态运行。注意断开前 1 秒地图更新还在照常进行——链路完全健康断连是上层的决定不是射频的锅。这类证据在排查断连问题时特别有价值把断连前最后几条 PDU 拉出来看如果地图、功率、上报一切正常基本可以排除物理层问题。第三次结束重连后的链路。三次告别三种上下文同一套协议流程。四、换老板不关店角色切换角色切换严格说不在连接控制的条目里它在独立的小节但从抓包看它和连接控制的耦合太深——切换瞬间 AFH 地图、Max Slot、监督超时全部要重谈不讲它这篇就不完整。为什么需要角色切换Spec Part B 给了两个场景寻呼方天生是 Central要加入现有 piconet 就得让位piconet 里的从设备想另立山头当 Central也得切。本例抓包里的场景更日常——耳机发起重连寻呼方是耳机于是耳机先当了临时 Central然后把位置还给手机。流程上是两层切换的叠加TDD 切换收发时隙对调 piconet 切换整个微网的参数换到新 Central 的地址和时钟上。Part B 的描述非常精彩To begin the role switch, A and B shall perform a TDD switch using the former hopping scheme (still using the Bluetooth Device Address and clock of device B), so there is no piconet switch yet.先在旧跳频序列上对调收发时序——此时微网还是旧微网只是角色先换。然后新 Central A 用旧微网参数发 FHS 包里面装着新微网的地址、时钟、给 B 的新 LT_ADDRB 回 ID 包确认确认之后双方才一起切到新微网参数上。中间还挂着一个 newconnectionTO 计时器超时了就全部回滚——旧角色、旧参数、旧状态当无事发生。FHS 里的时钟信息只有 1.25ms 分辨率不够对齐槽边界所以切换前 A 要先发一条 LMP_SLOT_OFFSET——旧微网 Central 槽起点和新微网 Central 槽起点之间的微秒级偏移0~1249µs、1µs 分辨率。1µs 精度的对齐信息提前打好包切换后 B 用它定位相关窗口。空口里这个场景的时间线堪称教科书17:05:13.328耳机临时 Central下发 SET_AFHCh79——全 79 信道耳机当 Central 的这十几毫秒里地图是全开的。耳机对环境信道的掌握有限毕竟一直当从设备干脆全图跑等手机接手后再裁剪。17:05:13.597LMP Role Switch356µs 的 slot offset切换完成。17:05:13.920 起手机新 Central立刻重发全家桶——SET_AFH 裁回 20 信道、分类上报开关重开、Max Slot 重设、监督超时重设 5 秒。对照 HCI 日志还能看到一个细节重连后主机用的连接句柄是 0x0003——这是此前那条旧链路用过的句柄控制器复用了它。角色切换加句柄复用资源回收利用得干干净净。还有一条前面提过的规则在这里兑现SAM 在角色切换瞬间强制禁用SAM 和角色切换不能同时挂起。所有涉及时序的操作在切换面前让路。五、代码时间5.1 解析 AFH 信道地图SET_AFH 的地图是 10 字节 80 比特79 个信道每信道 1 比特最高位保留。写个解析器把地图转成信道列表顺便算信道数def parse_afh_channel_map(map_bytes: bytes) - list[int]: LMP_SET_AFH 信道地图 - 可用信道列表。 地图 10 字节 80 比特bit(n) 对应信道 nn0..78 比特为 1 表示信道可用。最高位bit79保留忽略。 if len(map_bytes) ! 10: raise ValueError(AFH map 应为 10 字节) channels [] value int.from_bytes(map_bytes, big) for ch in range(79): # 信道 0~78 if value (79 - ch) 1: # 从 MSB 起数 channels.append(ch) return channels # 空口抓包里的一条真实地图16 进制缩短形式还原成 10 字节 # Ch20 表示裁剪后剩 20 个信道 sample bytes.fromhex(0007A0000000000000) # 示意前几信道全开 chans parse_afh_channel_map(sample) print(f可用信道 {len(chans)} 个: {chans}) # 抓包里另一条Ch79 全图角色切换期间临时 Central 下发 full bytes.fromhex(FFFFFFFFFFFFFE0000) # 79 个 1 保留位 assert len(parse_afh_channel_map(full)) 79配合抓包数据可以做一致性校验——每条 SET_AFH 的地图解析出来的信道数应该和 PDU 里带的信道计数字段一致不一致就是抓包工具或解析器的 bug。5.2 Detach 计时状态机把 3.1 节的文字规则翻译成状态机C 风格伪码/* Detach 发起方状态机简化版 */ typedef enum { ST_PAUSE_ACL, /* 停 ACL-U排队 DETACH */ ST_WAIT_BB_ACK, /* 等 Baseband ACK6*Tpoll */ ST_LINGER, /* ACK 后逗留3*Tpoll期满可复用地址 */ ST_DROP_WAIT, /* 无 ACK 掉链等监督超时 */ ST_DONE } detach_state_t; void detach_fsm_tick(link_t *lk) { switch (lk-detach_state) { case ST_PAUSE_ACL: acl_u_pause(lk); lmp_send_detach(lk, lk-reason); timer_start(lk-t, TPOLL_SLOTS(lk) * 6); lk-detach_state ST_WAIT_BB_ACK; break; case ST_WAIT_BB_ACK: if (lk-bb_acked) { /* 收到 BB ACK */ timer_start(lk-t, TPOLL_SLOTS(lk) * 3); lk-detach_state ST_LINGER; } else if (timer_expired(lk-t)) { /* 6*Tpoll 无 ACK */ link_drop(lk); /* 直接掉链 */ timer_start(lk-t, lk-supervision_timeout); lk-detach_state ST_DROP_WAIT; } break; case ST_LINGER: if (timer_expired(lk-t)) { if (is_central(lk)) lt_addr_pool_recycle(lk-lt_addr); /* 立即复用 */ lk-detach_state ST_DONE; } break; case ST_DROP_WAIT: if (timer_expired(lk-t)) { if (is_central(lk)) lt_addr_pool_recycle(lk-lt_addr); /* 超时后复用 */ lk-detach_state ST_DONE; } break; default: break; } }注意 ST_LINGER 和 ST_DROP_WAIT 两个出口对 LT_ADDR 复用时机的区别——一个立即、一个等满监督超时。实现里把这个顺序写反就会出现前面说的地址复用碰撞。六、收个尾把整篇的内容压回一张图其实就是开头那张全景图的三列。写完这篇最大的感受是 LMP 连接控制里有几个反复出现的设计母题-Instant 机制——AFH、时钟调整、SAM、嗅探、角色切换所有改时序的操作都用未来时钟刻度约定生效点双方到点同切不存在半切换状态。-先封路再改道——SET_AFH、包型表切换、Detach动参数前一律先停业务确认后恢复。-保守起步——新链路 Max Slot 归 1、功率先拉满再往下调、临时 Central 发全图。未知环境下先按最坏情况兜底再逐步优化。-串行红线——所有带 instant 的事务严格互斥碰撞报 0x2A。时序状态只有一个改它的操作必须排队。理解了这几个母题再看 Connection control 的任何一个小节都是熟面孔。协议条文背后是有哲学的。七、测验Q1LMP_DETACH 发送后Central 什么时候可以复用该从设备的 LT_ADDR如果一直收不到 Baseband ACK 呢答分两条路径。正常路径发起方停 ACL-U 业务后发送 DETACH在 6×Tpoll 内收到 Baseband ACK再等 3×Tpoll这段逗留是给接收方完成它自己的等待流程留的时间期满后 Central 可立即复用 LT_ADDR。异常路径6×Tpoll 内没收到 ACK发起方直接放弃链路但要再等一个 link supervision timeout 的时长才能复用地址——因为对端可能根本没收到 DETACH它的旧链路状态要靠监督超时来判死提前复用地址会让新从设备和还活着的旧状态对同一个 LT_ADDR 的包头同时响应。接收方侧对称收到 DETACH 后 Central 等 6×Tpoll、从设备等 3×Tpoll 再拆链。核心思想LT_ADDR 复用必须以对端旧状态的确定性消亡为前提。Q2为什么 LMP_SET_AFH 的 AFH_Instant 要求至少是 6×Tpoll 或 96 slots 之后的未来时刻取大者而且要在 12 小时之内答下界是为了留足协商传播时间。SET_AFH 要经历两个阶段——Baseband ACK 确认从设备收到指令、instant 到达确认双方同时切换。instant 太近从设备可能还没来得及处理上一条指令尤其链路在嗅探或高负载时Tpoll 本身就大造成两边跳频序列不同步、链路直接断。所以取 6×Tpoll 和 96 slots 的较大值覆盖最坏调度延迟。上界 12 小时是防止 instant 无限远挂起——一个永远不到的 instant 会让链路卡在待切换状态同时任何其他带 instant 的操作都被 0x2A 碰撞错误阻塞。另外 Central 收到上一条 SET_AFH 的 ACK 且 instant 过去之前不允许发新的 SET_AFH保证同一时刻只有一个待生效地图。Q3legacy 功率控制和 enhanced 功率控制能否在同一条链路上混用切换时机是什么enhanced 模式下为什么规定应答方要在新的功率档上发送 LMP_POWER_CONTROL_RES答不能混用。特性交换响应处理完毕后若双方都支持 enhanced 则只用 enhanced任一方只支持 legacy 则整条链路退回 legacy——木桶效应向下兼容。切换时机点之前的过渡窗口里允许用 legacy 发应急调整。应答方在新功率档上发 RES 的原因功率控制是闭环发起方依据接收信号质量决策RES 本身就是新功率下的第一份数据样本发起方收到它的 RSSI 直接验证调整效果并作为下一轮决策输入——应答报文兼作校准信号闭环一步到位。另外 enhanced 功控还有单向锁定规则应答方报告所有调制模式都到最大或最小功率后发起方必须先请求一次反方向调整才能再次请求同方向——防止两个实现在边界值上互相拉扯。