前面几篇一直围着安全和加密转,从钥匙生成讲到加密启停。这次换个视角,看一段所有连接都绕不开、却最容易被忽略的对话:链路刚建好那一瞬间,两个链路管理器之间到底聊了什么。
目录
先上空口log。这是一次全新连接建立后 400ms 内的空口记录:特性交换、扩展特性两页、版本交换、时钟偏移,一整套摸底动作行云流水。对着这张图往下读,你会发现所谓信息类请求,本质上是主设备在连上之后连问对方五个问题:你的表准吗、咱俩钟差多少、你什么版本、你会哪些招、你叫什么名字。

一、第一问:你的表准吗
蓝牙设备各揣一块表,精度却天差地别。手机用的是温补晶振,耳机为了省成本省电往往只给个普通晶振,两者的长期漂移能差一个数量级。中心设备如果不问清楚,就得按最坏的表来设计后续流程。于是有了时钟精度请求,规范写得很直白:
If timing accuracy information has not been received from the remote device, the worst-case values (drift = 250 ppm and jitter = 10 µs) shall be used.
拿到对端答复就用真实值,拿不到就一律按漂移 250ppm、抖动 10µs 的最坏情况处理。这两个默认值不是随便拍的,后面所有涉及定时预算的流程,比如监督超时、嗅探间隔的余量设计,都建立在最坏假设之上。规范还专门强调,报上来的精度必须是固定值,问一百次答案要一样,而且不能掺入微微网时钟调整带来的变化------设备体质数据,不是实时数据。

应答方带着 Drift 和 Jitter 两个参数回话,如果不支持就回一条 NOT_ACCEPTED,问的人自己退回最坏假设。整个过程就一来一回,简单到容易让人低估它的价值。
真正的价值在用途。规范说这个信息用于缩短微微网物理信道再同步的扫描窗口。展开说:从设备如果连续 200ms 没收到中心设备的包,就算失步,得重新搜。重新搜的时候不能只开 20µs 的窄窗了,因为不知道中心设备的包到底偏到哪里去了,窗口要放宽到 Xµs。窗口开多大,就取决于双方时钟的漂移和抖动有多少。基带部分给了具体的放宽策略:

窗宽超过 1250µs 就不能每跳都开,否则相邻窗口重叠,改为每 4 跳开一次、窗宽最大 2500µs,或者每 6 跳开一次、窗宽最大 3750µs。而且只搜接收频点,哪怕窗口延伸到本该属于发送时隙的时间段。规范还建议中心设备在从设备再同步期间只发单槽包,帮它尽快锁回来。下面这张 Hold 模式恢复的接收时序图,画的就是那个以估计的中心设备发送时刻为中心、宽 Xµs 的搜索窗:

一句话总结这一问:知道你的表多不准,我才知道窗口要开多宽。省下的是扫描时的射频接收功耗,牺牲的是一次毫不起眼的信息交换。
二、第二问:咱俩的钟差多少
第二个问题与第一个问题是一对搭档。时钟精度描述的是表走得准不准,时钟偏移描述的是两块表此刻差多少。定义只有一个公式:
(CLKN16-2 Peripheral - CLKN16-2 Central) mod 215
取从设备本地时钟与中心设备本地时钟去掉低两位后的差值,对 2 的 15 次方取模,存成 15 位偏移。中心设备在寻呼成功之后的任何时刻都可以要这个值,连接建立前、建立中、建立后都行,PDU 表里也明确标了这是强制项。

它存在的唯一意义是加速下一次寻呼。下次再呼同一个设备,中心设备用本地时钟加上存好的偏移,就能估出对方当前大致醒在哪个时刻、守在哪个频点,寻呼消息精准打过去,不用漫天撒网。

回看首连空口log,连接建立约 390ms 后中心设备做了一次时钟偏移事务,拿到 0x5644------这个数就是给下次重连准备的存档。

更妙的呼应在主机侧。重连抓包里,主机下发 Remote Name Request 读名命令,参数里赫然带着一份时钟偏移提示。读名命令自带寻呼过程,控制器拿着这份提示去呼对端,正是上次连接时存下的偏移在发挥余热。链路层的功课,最终服务的是主机层一次普通的查询。

三、第三问:你是什么版本
第三个问题交换的是身份三元组:版本号、厂商标识、子版本。表结构强制,来回复各一次:

三参数各有讲究。Version 指明设备支持到哪一代 LMP 规范,编号维护在 Assigned Numbers 文档里,5.3 对应 0x0C。Company_Identifier 是厂商代码,做过独立 LM 实现的公司都有自己的编号。Subversion 由厂商自行管理,规范建议每一版 RF/BB/LM 实现都给一个唯一子版本号,同一版本同一厂商之下,子版本号随每次新发布单调递增。两个参数若取 0xFFFF,都表示无有效值。

最容易踩的坑,规范用两条注脚专门堵上了。
第一条:版本号不代表特性全集,设备报了 5.3 不等于 5.3 的所有特性它都会,能不能用某个特性要看第四问的位图。
第二条:版本号大不代表规格更高。为什么会有这种反常识的注脚?因为版本交换压根没有协商机制,原文写得干脆:
There is no ability to negotiate the version of the LMP. The sequence below is only used to exchange the parameters.
它只是亮身份证,不是讨价还价。两边各报各的,谁也不迁就谁,之后所有配合全靠第四问的特性位图裁决。打个比方,版本号是你毕业的届别,特性位图才是你的成绩单------届别新不代表科科都修。

空口里这一问还有个耐人寻味的细节:中心设备在连接建立约 320ms 时发起版本交换,记录显示未获对端即时应答;紧接着对端自己主动发起了一次版本交换,双方各自拿到对方的 5.3;约 70ms 后中心设备又成功跑了一遍完整交换。两秒后对端还单独再问了一次。这背后是常见的工程实现:双方都会主动发起版本交换做兜底,谁先收到请求谁应答,谁被晾了谁重试,最终状态收敛于彼此知情。规范没规定这些细节,留给实现自由发挥。
四、第四问:你会哪些招
第四问是整个摸底流程的核心,也是信息量最大的一问。开局一来一回:

LMP_FEATURES_REQ 带着发起方自己的 64 位特性位图,LMP_FEATURES_RES 带着应答方的,一轮交换双方互相摸清对方页 0 的底细。但 64 位很快不够用了,规范于是设计了扩展机制:扩展特性按页组织,每页 64 位,页号 0 到 255,页 0 就是普通特性掩码。LMP_FEATURES_REQ_EXT 多了三个参数,请求哪个页、发起方自己的最大有效页号、发起方自己的该页位图;应答方镜像回复。规范对边界情况交代得非常干净:
If a device does not support any page number it shall return an Extended_Features parameter with every bit set to 0.
没有的页回全零,同时附上自己的最大有效页号。若对端压根不支持扩展请求,则它所有扩展页一律按全零假设。配合 Max_Supported_Page 参数,请求方知道什么时候该收手------问到对端声明的最大页号就可以停了。

看log。首连里页 0 特性交换之后紧跟两个扩展页:页 1 是主机侧能力(安全简单配对主机、LE 主机、安全连接主机),页 2 是控制器侧能力(安全连接控制器、Ping)。

注意一个细节:发起方页 1 报了三个位,应答方只回了两个位------对端的安全连接主机位是空的。前几篇分析配对降级时反复提到的那个缺位,根源就在这里被坐实。特性位图是唯一的裁决依据,位图上没有的,版本号说得再好听也不能用。这也是为什么规范在版本交换那条注脚里反复强调要看位图。

顺带一提,页 0 位图里有一位专门指示是否支持扩展特性请求,这就是规范说的 Support for the extended features mask is indicated by the presence of the appropriate bit in the LMP features mask。想跟人聊扩展页,先看人家页 0 里有没有按下这个同意键。
五、第五问:你叫什么名字
第五问最有人情味,问的是设备名。规范定义名字最长 248 字节,UTF-8 编码,具体类型是 utf8s{248z}。这个类型值得展开:定长 248 字节,实际字符串不足时第一个空闲字节填零,z 后缀表示剩余所有字节也必须为零。规范给了一个漂亮的例子,字符串 Café 编码成 utf8s{8z} 是 43 61 66 C3 A9 00 00 00,前五个字节是四个字符的 UTF-8 编码,后面全部补零。

248 字节的名字没法塞进一个 PDU,于是走分片。每个分片包带 14 字节名字片段,配合 Name_Offset 和 Name_Length 两个参数完成拼图。重组规则规范给得像数学定义:
Name_Fragment(N) = name(N + Name_Offset), if (N + Name_Offset) \< Name_Length
片段里第 N 个字节对应完整名字的第 N 加偏移字节,超界的字节一律取零。首次请求偏移必为 0,之后请求方按收到的进度递增偏移,循环往复直到收齐。分片走的是 DM1 包,这也是为什么抓包里读长名字会看到一串 NAME_REQ 和 NAME_RES 成对出现。参数表把三个字段的身份写得清清楚楚,片段正是 utf8s{14z} 类型:

六、重连时,五问重来
信息类请求有个隐含的脾气:不跨连接保留。重连log里,链路建立后 130ms 内,版本、页 0 特性、扩展页 1、页 2,整套摸底原样再来一遍,双方各自发起、互为应答。规范并未禁止缓存这些信息,但常规实现都选择每次重连重新确认------反正开销只有几条 PDU,换来的是对端状态永远新鲜,固件升级后能力变化也能第一时间感知。

主机侧的对照同样工整:连接建立前主机用 Remote Name Request 自带寻呼读名,连接建立后 Read Remote Version Information、Read Remote Supported Features、Read Remote Extended Features 页 1 页 2 鱼贯而出。主机侧的每次读取,控制器侧几乎都能对应到一段刚跑完的 LMP 交换------主机问的是结果,链路管理器早把作业做完了。

七、代码示意
两个片段还原关键计算。第一个是时钟偏移的应用场景,把存档的偏移加回本地时钟做寻呼预估:
/* 用上次连接存的 15 位偏移, 预估对端当前时钟 CLKE
* offset 即 (CLKN16-2_P - CLKN16-2_C) mod 2^15 的快照 */
uint32_t estimate_peer_clock(uint32_t clkn, uint16_t offset)
{
/* 偏移基于去掉低两位的时钟值, 恢复时左移两位 */
return (clkn + ((uint32_t)offset << 2)) & 0x0FFFFFFFu;
}
第二个是名字分片的重组,严格按规范公式处理越界补零:
#define NAME_FRAG_LEN 14 /* 每个分片 14 字节, utf8s{14z} */
/* 返回 1 表示名字已收齐, 0 表示还差片段, -1 表示参数非法 */
int name_reassemble(uint8_t *buf, uint8_t cap, uint8_t off,
uint8_t total, const uint8_t *frag)
{
if (off >= cap || total > cap)
return -1;
for (int n = 0; n < NAME_FRAG_LEN; n++) {
if (off + n < total) /* 有效字节: name(n + off) */
buf[off + n] = frag[n];
else if (off + n < cap) /* 越界字节: 按公式取 0 */
buf[off + n] = 0;
}
return (off + NAME_FRAG_LEN >= total) ? 1 : 0;
}
八、收个尾
五连问串起来看,是一条完整的逻辑链:问表准不准,是为了算同步窗口省功耗;问钟差多少,是为了下次寻呼快人一步;问版本和特性,是为了摸清能力边界再谈配合;问名字,是给上面的人一个交代。没有一个问题是白问的,没有一次交换是走过场的。下次抓包看到连接建立后那一小串 PDU,别再一划而过------那是两个设备在正式共事前,认真交换过的底细。
九、测验
问题一:LMP 版本交换中,对端报告 Version 为 5.3,能否据此断定它支持安全连接配对?
参考答案:
不能。版本号只声明设备遵循到哪一代 LMP 规范,不等于该版本所有特性都实现。规范明确注记版本值不代表支持对应版本的特性全集,特性支持与否的唯一裁决依据是特性交换得到的位图------安全连接主机能力要看页 1 的 Secure Connections Host 位,控制器能力要看页 2 的相应位。另外版本号偏大也未必代表规格更高,版本交换无任何协商语义,纯粹是参数亮牌。排查兼容性问题时应始终以特性位图为准。
问题二:时钟偏移为什么要对 2 的 15 次方取模?这个偏移什么时候会失效?
参考答案:
偏移定义基于双方本地时钟去掉低两位后的 15 位字段,即以时隙为单位的时钟差,天然落在 15 位空间内,所以取模 2 的 15 次方做回绕处理。它服务的对象是下次寻呼时的时钟预估,让中心设备把寻呼消息打到对端最可能醒着的时刻与频点上。失效场景主要有两个:一是两次连接间隔太久,两块表的漂移累积使得存档偏移过期,预估相位偏离实际,寻呼退回普通流程;二是期间任一设备经历过时钟调整或角色变化,旧偏移作废。这也是实现里普遍选择每次重连重新做全套信息交换的原因之一。
问题三:抓包看到对端扩展特性页 1 全部为零,能否断定它不支持扩展特性请求机制?
参考答案:
不能直接断定,全零有两种可能。一种是对端确实支持扩展特性请求机制,但它的页 1 没有任何有效位,按规范要求回全零并附上自己的最大有效页号;另一种是对端压根不支持扩展特性请求,此时规范规定请求方应把它所有扩展页一律按全零假设。区分二者的线索有两个:看 Max_Supported_Page 参数的值,为 0 表示只有页 0;看对方是否回了合法的 LMP_FEATURES_RES_EXT 结构。这也是为什么规范把扩展特性请求设计成带页号和最大页号的镜像应答,而不是简单的有没有------把机制支持与能力有无拆开表达,才不会误判。