3. 链路控制层:状态机、寻呼与查询扫描、连接建立过程
好,咱们今天聊聊链路控制层。这一层,说白了就是蓝牙协议栈里最底层的“交通警察”。它管着设备怎么被发现、怎么建立连接、怎么维持连接。我当年刚接触蓝牙时,觉得这层就是个状态机来回跳,没什么难的。结果第一次调通时,发现设备死活连不上,折腾了两天才发现是寻呼扫描的参数配错了。嗯,从那以后我再也不敢小看这一层了。
3.1 链路控制层状态机
链路控制层(Link Controller)的核心,就是一套有限状态机。它定义了蓝牙设备在通信过程中的各种状态,以及状态之间的转换条件。我个人习惯把这套状态机分成三大类:待机状态、连接状态和中间状态。
咱们先看一张简化的状态转换图(我手绘的,凑合看):
Standby
|
+----+----+
| |
Inquiry Page
| |
Inquiry Page
Scan Scan
| |
+----+----+
|
Master
Response
|
+--+--+
| |
Slave Master
Response Response
| |
+--+--+
|
Connected
你想想看,一个蓝牙设备刚上电时,默认就在 Standby(待机) 状态。这时候它啥也不干,就等着被唤醒。然后根据上层指令,它可以进入 Inquiry(查询) 或 Page(寻呼) 状态,或者反过来进入 Inquiry Scan(查询扫描) 或 Page Scan(寻呼扫描) 状态。
这里有个关键点:Inquiry 和 Page 是主动方,Inquiry Scan 和 Page Scan 是被动方。主动方负责“喊”,被动方负责“听”。
重要概念:蓝牙设备在连接建立前,角色是不对称的。主动发起连接的一方叫 Master(主设备),被动接受连接的一方叫 Slave(从设备)。但连接建立后,角色可以互换,这叫 角色切换(Role Switch)。
3.2 寻呼与查询扫描
咱们先讲 Inquiry(查询)。这个状态是用来发现周围有哪些蓝牙设备的。主动方会发送一个 ID 包,里面包含一个特定的 查询访问码(IAC)。被动方如果处于 Inquiry Scan 状态,就会收到这个包,然后回复一个 FHS 包,告诉主动方自己的蓝牙地址和时钟信息。
我记得有一次,客户反馈说他们的 Soundbar 搜索不到手机。我查了半天,发现是 Inquiry Scan 的窗口开得太小了。默认是 11.25ms,但客户为了省电,改成了 2.5ms。结果手机发 Inquiry 时,Soundbar 刚好在休眠,根本听不到。嗯,这就是典型的“喊的人嗓子都哑了,听的人却在打瞌睡”。
再来看 Page(寻呼)。这个状态是用来建立连接的。主动方已经知道对方的蓝牙地址了(通过 Inquiry 或者事先配置),所以它发送的 ID 包 里包含的是对方的 设备访问码(DAC)。被动方在 Page Scan 状态下收到这个包后,会回复一个相同的 ID 包,表示“我听到了”。
寻呼过程其实是个“握手”过程,分三步:
- 主动方发 ID 包:包含对方的 DAC,持续一段时间。
- 被动方回 ID 包:在 Page Scan 窗口内收到后,立即回复。
- 主动方发 FHS 包:收到回复后,主动方发送自己的 FHS 包,包含时钟和地址信息。被动方收到后,回复一个 ID 包确认。
这里有个坑:Page Scan 的间隔和窗口。如果窗口太小,或者间隔太大,主动方可能很久都扫不到被动方。我曾经遇到过一个问题,两个设备明明就在 1 米内,但连接建立时间长达 5 秒。后来发现是 Page Scan 的间隔设成了 2.56 秒,而主动方的寻呼重试间隔是 1.28 秒。你想想看,主动方每 1.28 秒喊一次,被动方每 2.56 秒才听一次,刚好完美错过。这概率,啧啧。
避坑指南:我曾经在调试 Soundbar 时,发现它和手机连接总是超时。后来用抓包工具一看,原来是 Soundbar 的 Page Scan 窗口设成了 10ms,而手机那边用的是标准 11.25ms 的寻呼间隔。10ms 的窗口太小,手机发的 ID 包经常落在窗口外。建议 Page Scan 窗口至少设为 11.25ms,间隔不要超过 1.28 秒。
3.3 连接建立过程
连接建立,说白了就是 Page 成功后的后续步骤。当主动方和被动方完成了上述的“握手”后,它们就进入了 Connected(已连接) 状态。但这时候,连接还没完全建立好,还需要做一些“家务活”。
连接建立过程大致分这么几步:
- 寻呼成功:双方交换了 FHS 包,知道了彼此的时钟和地址。
- 主从角色确定:默认情况下,主动方是 Master,被动方是 Slave。但可以后续切换。
- 连接参数协商:Master 会发送 LMP 包 来协商连接参数,比如跳频序列、连接间隔、超时时间等。
- 建立 ACL 链路:这是异步无连接链路,用于传输数据。
- 可选:建立 SCO/eSCO 链路:如果是要传音频,还需要建立同步面向连接链路。
我建议你在调试时,重点关注 连接间隔(Connection Interval) 和 从设备延迟(Slave Latency)。这两个参数直接影响功耗和延迟。比如 Soundbar 这种需要低延迟音频的设备,连接间隔通常设得比较小,比如 20ms 到 50ms。而像蓝牙耳机这种对功耗敏感的,可能会设到 100ms 以上。
咱们看一个典型的连接参数表:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 连接间隔 | 20ms - 100ms | Master 和 Slave 之间的通信周期 |
| 从设备延迟 | 0 - 500 | Slave 可以跳过多少个连接事件 |
| 超时时间 | 2s - 20s | 超过这个时间没收到包,就认为连接断了 |
| 跳频序列 | 79 个频点 | 基于 Master 的时钟和地址生成 |
这里有个细节:跳频序列 是基于 Master 的蓝牙时钟和地址生成的。所以连接建立后,Slave 必须把自己的时钟同步到 Master 的时钟上。这就是为什么 FHS 包里要包含时钟信息。
注意事项:连接建立后,如果 Slave 的时钟漂移太大,可能会导致跳频不同步,进而丢包。我曾经在 Soundbar 上遇到过一个问题,播放音频时偶尔出现“咔咔”声。后来发现是 Slave 的晶振精度不够,时钟漂移超过了 ±20ppm。换了个高精度晶振后,问题就解决了。所以,硬件设计时,晶振的精度一定要选好。
3.4 状态机中的常见问题
最后,我总结几个我在项目中遇到过的状态机相关的问题,希望能帮你避坑:
- 状态机卡死:有时候设备会卡在某个中间状态,比如 Page 状态一直出不去。这通常是因为超时参数没设好,或者收到了异常的包。我建议在代码里加个看门狗定时器,超时后强制回到 Standby。
- 角色切换失败:连接建立后,如果 Master 和 Slave 需要互换角色,可能会失败。这通常是因为双方的角色切换能力不一致。我建议在连接建立前,先通过 LMP 包协商好角色。
- 查询和寻呼冲突:如果设备同时处于 Inquiry Scan 和 Page Scan 状态,可能会发生冲突。因为这两个状态用的都是同一个射频窗口。我建议在实现时,把这两个窗口错开,或者用优先级机制。
嗯,链路控制层的内容就讲到这里。说白了,它就是蓝牙通信的“地基”。地基没打好,上面的协议栈再牛也白搭。下一章咱们会讲 基带层,看看数据是怎么在物理层上传输的。到时候再聊。