很多人觉得蓝牙连接就是把两个设备拉到一起,点一下配对就完事。但真在项目里调试过经典蓝牙(BR/EDR)连接问题的人都清楚,从上层调用Create_Connection到空中链路真正建立,中间隔着一整套层层转译的握手:Host 下发 HCI 命令,Controller 执行寻呼,基带完成跳频同步,LMP 协议在两边控制器之间交换连接请求和特性列表——任何一个环节没有对齐,你看到的就是一个莫名其妙的连接失败,或者"能搜到但连不上"这种让人抓狂的现象。
这篇内容适合三类人:做蓝牙驱动和固件开发的工程师、在安卓/iOS 上层做蓝牙应用但需要定位底层问题的开发者,以及测试人员。我会把经典蓝牙从 HCI 命令下发到 LMP 协议交换的完整连接流程拆开讲,包括关键参数、命令格式、状态变化,以及我在实际调板中踩过的坑。你不需要一次性记住所有细节,但至少你下次再看到Page Timeout或者LMP timeout的时候,能立刻反应过来问题出在第几层。
1. 一条连接请求的完整旅程:从Host到空中的分层转译
1.1 协议栈里谁在说话:Host、Controller与HCI的边界
经典蓝牙协议栈在物理实体上分成两大部分:Host 和 Controller。Host 跑在应用处理器侧,负责的逻辑层、L2CAP、SDP、GAP、各类 Profile 都在这一侧;Controller 则通常是一个独立的蓝牙芯片或者 SoC 内部的协议处理单元,负责射频收发、基带控制、跳频、链路管理和大部分的底层时序。
这两个部分之间靠 HCI(Host Controller Interface)通信。HCI 的定位是一组标准化的命令、事件和数据通道:Host 往 Controller 下发命令,Controller 把结果以事件的形式上报。而 LMP(Link Manager Protocol)则完全跑在 Controller 内部,是两台设备的 Controller 之间通过空中报文直接对话的协议。你把它理解成两个一线工程师在你没听见的地方悄悄把活儿干完了,最后只跟你汇报一句"已经连接上了"。
为了把这个分层关系说清楚,我给你打个比方。Host 是产品经理,Controller 是一线施工队,HCI 是这两个角色之间的工单系统。产品经理给施工队下工单"去把对面那栋楼的 A 房间连接上",施工队接到工单后开始干活。施工队和对面楼的施工队之间怎么沟通,用的是什么暗号、怎么对齐频率、怎么确认对方身份,这些是 LMP 的职责。施工队忙活完之后在工单系统里回一句"已连接",这就是 HCI 的Connection_Complete事件。
理解这个边界对排查问题非常关键。很多人在上层看到连接失败,第一反应是怀疑协议栈代码,但实际上问题出在 Controller 内部的 LMP 交互;反之,如果 HCI 命令压根没下发成功,那就要先看 Host 的调度和资源管理。
1.2 连接建立到底要经过哪几站:发现、寻呼、连接确认、特性交换
一次完整的经典蓝牙连接流程,按我习惯画的时序图来看,大致是下面这几站:
- 发现(Inquiry):这是可选步骤。如果你已经知道对端设备的 BD_ADDR,可以直接跳过这步去发起连接。Inquiry 的目的只是"探测周围有哪些蓝牙设备、每个设备是什么地址、什么设备类型"。
- 寻呼(Page):发起方在特定的跳频序列上持续发送设备的访问码,目标是让目标设备在它的扫描窗口里"听到"这个敲门声,并回复确认。这一站做的事情是建立物理层的联系,也就是两个射频前端之间完成了频率上的同步。
- 连接确认(LMP Connection Request):物理链路同步之后,双方的 Link Manager 开始交换连接建立相关消息,确认连接参数和双方能力。这一步在大多数实现中就是
LMP_host_connection_req和LMP_accepted的往来。 - 特性交换(Feature Exchange):双方互相告知自己支持哪些特性,比如是否支持 EDR、是否支持 3 槽包、是否支持 AFH。这个信息决定了后续数据通路上能用什么样的封包格式和速率。
- 上报完成(Connection Complete):Controller 内部处理完后,通过 HCI 事件通知 Host,"链路已经好了,给你一个 Connection Handle,以后传数据就用这个句柄"。
这里必须强调一下,"连接"和"配对"是两个完全不同的过程。连接(Connection)指的是物理链路和逻辑传输的建立,也就是上面这五站;而配对(Pairing)是在链路建立之后,通过 LMP 层的鉴权交互生成和交换 Link Key 的过程,它决定的是以后能不能加密通信、能不能自动鉴权。很多人概念混淆,导致调试连接问题时思路跑偏——连接都还没建立,就一上来在应用层找配对失败的原因,那肯定要浪费大量时间。
2. 连接建立前的寻呼与扫描:跳频同步背后的时序博弈
2.1 page scan与page:一个等,一个敲门
寻呼阶段是整个连接流程里最基础也最容易出问题的环节。发起方的 Controller 会在page hopping sequence(寻呼跳频序列)上周期性发送 ID 包。这个包非常轻量,里面不携带完整的蓝牙地址,只携带一个由目标设备 BD_ADDR 低位生成的接入码(DAC)。而目标设备这边,如果它处于可被发现且可被连接的状态,它的 Controller 会在page scan窗口里周期性醒来,使用自己的page scan hopping sequence监听空中有没有人在喊自己。
这两条跳频序列是不同的,想对上需要一点运气和时序技巧。发送方会快速地在一系列频点上轮流发送 ID 包,并且每发送一轮会调整一次时序相位;接收方在扫描窗口内也按自己的节奏在频点上跳。相当于两个人在一个不断变化的频道表上找对方,一个人喊得足够频繁,另一个人监听窗口足够长,才能碰上。
这里有几个关键参数,直接影响连接速度和成功率:
- Page Scan Interval:扫描方每隔多久醒来一次,默认是 1.28 秒。
- Page Scan Window:每次醒来监听多长时间,默认是 11.25 毫秒。
- Page Timeout:发起方愿意花多长时间反复发送寻呼请求,超过这个时间还没建立连接,就给 Host 回一个失败事件。
算一下你就能理解为什么连接有时候快有时候慢:如果页面扫描间隔是 1.28 秒,而扫描窗口只有 11.25 毫秒,那么设备真正在监听的时间占比不到 1%。发起方平均要花几百毫秒才能撞上扫描方醒着的窗口,如果双方时钟漂移比较大,甚至要更久。所以很多低功耗设计会把扫描窗口调大来提升连接的响应速度,代价是功耗增加。
2.2 从ID包到FHS包:寻呼完成的三个回合
寻呼不是一个包就解决的,它分三个回合:
- 发起方发送 ID 包(携带目标的 DAC),等待目标回应。
- 目标在自己的扫描窗口收到 ID 包后,返回一个相同的 ID 包作为确认。
- 发起方收到确认后,紧接着发送 FHS 包(Frequency Hop Synchronization packet),这个包里面装着发起方的 BD_ADDR、当前蓝牙时钟、BCH 校验等信息。目标再回一个 ID 包确认收到 FHS。
这三步完成之后,两边才算建立了一个物理层面的连接,双方的收发频率会切换到连接态的跳频序列上继续同步。目标设备在确认收到 FHS 之后,会把发起方当作 Master,自己作为 Slave,进入连接态。
这里有个值得注意的细节:FHS 包是明文发送的,里面包含了发起方的地址和时钟信息。所以在这一阶段,空中是没有任何加密保护的。这也意味着任何人都能通过抓包看到你在和谁建立连接。真正安全的加密,要等连接完成后的配对鉴权阶段才启动。
2.3 HCI层看到的寻呼过程:Create_Connection与Connection_Request
站在 HCI 层看寻呼过程,发起方和接收方看到的视角完全不同。
如果你是发起方(Central),Host 会下发HCI_Create_Connection命令,把这个命令交给 Controller,Controller 收到后开始在物理层执行上面说的寻呼流程。接收方这边,Controller 通过HCI_Connection_Request事件通知本机的 Host:"有人要连我,这是对方的地址和设备类型,要不要接受?"Host 需要回复HCI_Accept_Connection_Request或者HCI_Reject_Connection_Request。
需要注意的是,很多实际设备在角色配置上会和这个默认逻辑有出入。比如手机做主机去连耳机,耳机侧会弹出一个HCI_Connection_Request事件,而协议栈的上层策略(比如耳机的系统策略)决定要不要接受这个连接。这也是为什么有些设备在连接时会有意识地去过滤某些地址的设备,它就是在HCI_Connection_Request事件里做的拦截。
3. HCI命令的生命周期:从opcode编码到Connection Complete事件
3.1 命令包格式与OGF/OCF:从字节流到语义
HCI 命令在 Host 和 Controller 之间传输时,是严格按照字节流格式组织的。一个完整的 HCI 命令包,由三部分组成:
| 字段 | 长度 | 说明 |
|---|---|---|
| Opcode | 2 字节 | 高 6 bit 是 OGF(Opcode Group Field),低 10 bit 是 OCF(Opcode Command Field) |
| 参数总长度 | 1 字节 | 后面跟着的参数区总字节数 |
| 参数区 | 不定长 | 具体命令的各个参数 |
OGF 用来标识命令属于哪个功能组,比如 0x01 是链路控制命令组(Link Control),0x03 是主机流控命令组(Host Flow Control),0x04 是控制器参数命令组等。同一个组内的每条命令用 OCF 区分。
举个例子,HCI_Create_Connection的 OGF 是 0x01,OCF 是 0x0005,那它的 opcode 算出来就是 0x0005 | (0x01 << 10)。参数区里面依次填充对端 BD_ADDR、允许的包类型、页面扫描重复模式、时钟偏移、是否允许角色切换等信息。代码里写的话看起来像这样:
// 示意:构造 Create_Connection 命令参数 hci_cmd_create_connection cmd; cmd.bd_addr = target_dev_addr; // 目标设备地址 6 字节 cmd.packet_type = 0xCC18; // 允许的包类型:DM1/DH1/DM3/DH3/DM5/DH5 cmd.page_scan_rep_mode = 0x00; // R0,连续扫描 cmd.reserved = 0x00; cmd.clock_offset = 0x0000; // 未知时钟偏移时为 0 cmd.allow_role_switch = 0x01; // 允许角色切换有一个很容易踩的坑是:很多人以为 HCI 命令是"发出去一个请求,等一个响应",但实际上大部分 HCI 命令是异步的。Controller 收到命令后,先回一个Command Status事件表示"命令我收到了正在处理",等真正处理完成后再回一个对应的事件。像HCI_Create_Connection返回的是Command Status事件(0x0F),而最终的成功与否要看HCI_Connection_Complete事件(0x03)。如果你在代码里用同步等待Command Complete的方式来处理连接命令,那肯定会卡死直到超时。
3.2 从Create_Connection到Connection_Complete:一条命令的完整生命周期
一条连接命令的完整生命周期,按 HCI 事件流来梳理是这样:
Host Controller | HCI_Create_Connection | |--------------------------->| | Command Status (0x0F) | |<---------------------------| | | (开始 page 寻呼) | | (LMP 连接请求与特性交换) | HCI_Connection_Complete | |<---------------------------| | Status=0x00, Handle=0x000B|HCI_Connection_Complete事件里最重要的几个字段是:Status(0 表示成功)、Connection_Handle(后续 ACL 数据传输用的句柄)、BD_ADDR(对端地址)、Link_Type(这里是 ACL 链路)、Encryption_Mode(加密状态,通常是 0 表示未加密)。
抓 HCI 日志时,判断一次连接是否正常,最简单的办法就是看这一串事件序列。如果Command Status事件的状态值不为 0,那就是命令格式有问题;如果Connection_Complete的Status = 0x04,说明Page Timeout了,寻呼阶段没找到目标设备。
3.3 容易被忽略的事件时序:为什么要在Command Status之后等待Connection_Complete
我见过很多新人在做蓝牙上层应用时,在调用Create_Connection之后立刻开始做下一步操作,结果返回的是Command Status而不是连接结果。这里的问题在于,Command Status只表示 Controller 接受了命令,不代表物理链路已经建立。
如果你的系统是多任务环境,正确的做法是建立一个命令事务状态机:下发命令后进入"等待完成"状态,把Command Status事件里的状态先记录一下,然后等待对应的完成事件。对连接命令来说就是等HCI_Connection_Complete,对断开连接命令来说就是等HCI_Disconnection_Complete。在实际的蓝牙协议栈里,这些事件都由一个统一的事件分发循环分发到各个等待队列。代码层面不复杂,但架构上一定要把"命令下发"和"事件等待"解耦,否则一个慢速的物理连接流程可能把整个主机协议栈的消息循环拖死。
抓 HCI 日志时,我建议你把Command Status和后续完成事件的时间戳差值也记下来。这个差值通常就是寻呼消耗的时间。如果两次事件之间超过了 Page Timeout 设置值,基本可以断定是空中环境或者对端扫描参数的问题,不用再在 HCI 层反复调参了。
4. LMP层的外交谈判:连接请求、特性交换与角色博弈
4.1 LMP PDU长什么样:空中协议和HCI命令的区别
LMP 是 Controller 和 Controller 之间的空中协议,它不经过 HCI,也不通过 L2CAP。在蓝牙基带层,LMP 报文被封装在 ACL 或者 SCO 的逻辑传输里面,但它的 Payload 头部有自己的格式。一个 LMP PDU 大体上由 Transaction ID、Opcode 和 Payload 组成。Transaction ID 用来区分这个 PDU 是请求、响应还是无确认的通知;Opcode 表示消息类型,比如连接请求、接受、拒绝、特性请求等。
这里需要注意一个协议栈实现上的要点:LMP 消息的长度非常短,通常只有几个字节,因为它的设计目标就是在两个 Link Manager 之间做轻量级的控制面通信,把所有链路管理决策下沉到 Controller。Host 对此一无所知,除非你用专门的蓝牙协议分析仪去抓空中的报文,不然你看不到这些你不在场时发生的对话。
4.2 连接建立阶段的典型LMP对答序列
以一台手机连接一台耳机的场景为例,物理寻呼完成后,LMP 层会紧跟着发生这样一串消息:
- LMP_host_connection_req:发起方的 Link Manager 发出连接请求,其中带有对端可以接受的参数范围。
- LMP_accepted:接收方回复"我接受",双方进入连接状态。
- LMP_features_req:发起方询问"你支持哪些特性?"
- LMP_features_res:接收方返回自己的特性掩码,内容包括是否支持 EDR、是否支持 3 槽包、是否支持 AFH、是否支持安全简单配对等。
- 可选地,双方再交换
LMP_version_req和LMP_version_res,确认各自支持的蓝牙规范版本。
这些交互整体上发生在HCI_Connection_Complete事件上报给 Host 之前。也就是说,当你在 HCI 日志里看到Connection_Complete成功时,LMP 层最核心的特性交换已经完成了,Controller 已经知道双方能不能用 EDR、能不能用高速包、能不能做角色切换。
为什么LMP_features_req/res必须在连接初期完成?因为后续很多 LMP 流程(比如加密协商、角色切换、AFH 重配置)都依赖双方是否支持对应的扩展特性。如果跳过特性交换直接做加密协商,可能会出现一方支持安全连接而另一方完全不认识这条消息的尴尬情况。我在调试某些老款蓝牙芯片时,还遇到过因为固件特性掩码配置错误,导致双方协商的结果是只能用 Basic Rate 通讯,速度直接掉到 720kbps 以下,上层却完全看不到任何报错——这种问题只能靠空中抓包才能发现。
4.3 Role Switch:谁当主谁当从,LMP怎么拍板
正常情况下,发起寻呼的一方就是 Master,被寻呼的一方是 Slave,这个角色分配在物理层寻呼完成时就已经确定了。但很多场景下,这个默认角色并不是双方系统想要的。
举个例子,手机 A 连接手机 B 的时候,如果 A 发起寻呼,那 A 是 Master。但 B 上的某个应用可能需要自己作为 Master 来承担后续的 piconet 调度(比如两个设备在同一个微微网里要继续拓展连接别的外设),于是 B 会发起LMP_switch_req请求角色互换。A 的 Link Manager 如果接受,就回复LMP_switch_cfm或者相应消息,两边再执行一次 TDD 切换协调,完成主从角色的对调。
角色切换在空中的开销并不大,但在实现上却容易翻车。我之前测试时遇到过一批固件,在角色切换完成后会出现短暂的 LMP 重传风暴,导致上层 L2CAP 连接直接断开。排查了几天才发现是固件在role switch后没有正确重置跳频同步状态。所以如果你在调试应用时发现连接不稳定,而且日志里出现过switch role相关事件,先怀疑固件的角色切换实现,而不是上层应用。
4.4 AFH与跳频:连接态之后的另一个"隐形"LMP流程
AFH(Adaptive Frequency Hopping,自适应跳频)是用来避开拥挤频段的机制。它把 79 个跳频信道划分为可用信道和不可用信道,由 Master 决定信道映射并通过 LMP 消息通知 Slave。这通常发生在连接建立后的一段时间内,因为系统需要一段时间的信道质量统计。
AFH 和连接建立看似无关,但它对连接稳定性的影响非常大。如果两个设备之间的跳频信道数太少(比如在 2.4G Wi-Fi 密集的环境里),蓝牙的吞吐量和延迟都会明显恶化。我在一个 IoT 项目里遇到过一个问题:设备连上后每隔几分钟就断一次,抓包分析发现是对端设备周围 Wi-Fi 信道干扰太严重,AFH 把大量信道标记为不可用,链路基于剩余少量信道的又连续丢包,最终触发了链路监督超时。这个问题的解法不是改蓝牙连接参数,而是在应用层加入了信道管理策略。
5. 连接完成后那些看不见的连锁反应:L2CAP、SDP与加密协商
5.1 L2CAP与SDP为什么紧随其后
当HCI_Connection_Complete事件上报给 Host 之后,物理链路和 LMP 层的事算是稿一段落了。但上层用户感知的"连接成功"通常还要等 L2CAP 通道建立和 SDP 查询完成。L2CAP 是逻辑链路控制和适配协议,它负责把上层的应用数据分帧并通过 ACL 链路发送;SDP 是服务发现协议,用来查询对方支持哪些 Profile 服务,比如 HFP、A2DP、SPP 等。
实际场景里,手机连耳机时,手机在收到物理连接完成后会立刻建立 L2CAP 连接并发送 SDP 查询,确认耳机支持 A2DP、HFP、AVRCP 中的哪些服务。这个 SDP 查询的响应时间,直接影响用户感知的"连接成功"速度。如果耳机的 SDP 响应慢,手机上可能就会卡在"正在连接"界面很久。
这里很多人有个误区:认为物理层连接成功就等于 Profile 连接成功。实际上 A2DP 的媒体通道建立、HFP 的语音通道建立都是 L2CAP 之上单独的通道建立过程,任何一个环节失败,用户看到的效果都是"蓝牙连上了但没声音"或者"蓝牙连上了但电话接不起来"。
5.2 鉴权与加密的LMP协商:没有想象中那么自动
物理连接完成之后,如果双方之前已经有了 Link Key(比如历史配对过),那么连接后会启动鉴权过程。Link Manager 之间会通过LMP_au_rand和LMP_sres交换随机数和签名响应来互相验证对方的 Link Key 是否正确。验证通过后,再通过LMP_encryption_mode_req协商是否开启链路层加密。
这个过程的触发策略由 Host 的 GAP 层决定,Controller 本身不会自动加密。如果 Host 配置的 Security Mode 是"不要求加密",那即便 Link Key 已经存在,LMP 层也不会发起加密。这在开发阶段很方便,但商业化产品里如果漏配了安全模式,用户的数据就会以明文在空中传输,用抓包工具一眼就能看到。
5.3 连接超时与故障恢复机制
链路建立成功之后,并不是就一直保持存在的。蓝牙协议定义了两种关键的监督机制:link supervision timeout(链路监督超时)和page timeout(寻呼超时,这是寻呼阶段用的)。一旦连续一段时间收不到对方的基带包,Controller 就会认为链路已经丢失,通过HCI_Disconnection_Complete事件通知 Host,同时释放连接句柄。
在调试低功耗 IoT 设备时,我发现很多人把page timeout和link supervision timeout混为一谈。前者是寻呼阶段用多长时间找设备,后者是连接建立后允许多久收不到包就判定链路断开。这两个参数分别在 HCI 层和 LM 层的配置里设置,如果上层或者工具误改了,会出现"连接几分钟就掉"这种看起来像硬件问题的现象。
6. 连接失败的排查路径:HCI日志的分层归因与实战案例
6.1 只用HCI日志怎么定位问题域
如果你手头暂时没有蓝牙协议分析仪,HCI 日志依然是定位连接问题最直接的手段。抓 HCI 日志常用的工具包括btmon、hcidump,以及各家芯片厂商的 vendor 工具。拿到 HCI 日志之后,先看事件序列,再看状态码。
连接阶段最常见的事件序列和问题归因可以用下面这个表来总结:
| 现象 | 关键事件 | 状态码 | 问题层 |
|---|---|---|---|
| 发起连接后无反应 | Command Status 正常,但没有 Connection Complete | 无 | 大概率 Controller 内部卡在寻呼 |
| 寻呼超时 | Connection Complete | 0x04 Page Timeout | 对端不在可连接状态或信号太弱 |
| 连接被拒绝 | Connection Complete | 0x05 Authentication Rejected | 对端的安全策略拒绝了你 |
| 连接建立后立刻断开 | Disconnection Complete | 0x08 Connection Timeout | 链路监督超时,空中干扰或对端掉电 |
| 连接建立但无法建立 L2CAP | 无有效 ACL 数据上报 | L2CAP 错误 | 对端 Host 协议栈卡死或资源不足 |
这些状态码是Status字段里常见的值,完整的错误码表在蓝牙核心规范 Vol 2 Part D 里都有。养成先看状态码、再看事件序列的习惯,能帮你把问题域缩小到具体层级。
6.2 分层排查白板思路:HCI→LMP→RF三层各看什么
对蓝牙连接问题做归因时,我习惯在脑子里画一个三层模型:最上层是 HCI 命令和事件层,中间是 LMP 报文交换层,最底层是射频信号质量层。
- HCI 层看什么:命令是否下发成功、事件是否超时、状态码是否正常。这一层能确认的是"Host 和 Controller 之间的协作有没有问题"。
- LMP 层看什么:双方 Controller 之间是否完成了连接请求和特性交换,是否有重传风暴、超时重发。这一层必须有协议分析仪才能看到。它能确认"两个 Controller 的 LM 之间有没有谈拢"。
- RF 层看什么:RSSI 是否正常、丢包率是否高、CRC 错误是否频繁。这一层能确认信号质量是否有物理干扰。
很多连接问题在 HCI 层看就是"Page Timeout",但根本原因可能是射频信号太弱,也可能是对端固件在扫描态和连接态之间切换出现 bug。如果你只有 HCI 日志,能做的只是在 Host 侧优化,但如果有个分析仪,你就能区分出到底是寻呼没被收到,还是收到了但没回复——这两者的解决方向完全不同。
6.3 实战案例:一个典型的"能搜到但连不上"问题
我在一个智能硬件项目里碰到过一次非常典型的"能搜到但连不上"问题。设备端用的是某国产蓝牙 SoC,手机端能发现设备,但每次点击连接都会在几秒后失败。
HCI 日志显示,手机侧正常下发了HCI_Create_Connection,但等到了Page Timeout。也就是说,设备端根本没有回应寻呼请求。可问题在于,设备端明明处于可连接状态,因为我们能在手机上看得到它。
后来我们用协议分析仪抓无线包,发现了真相:设备端的 Controller 在周期性退出 page scan 进入 inquiry scan 时,固件内部有一个超过 10ms 的中断处理黑洞,导致它恰好在这段时间里错过了手机发来的 ID 包。因为手机发送 ID 包是有时序窗口的,如果设备在关键时段内没有开启接收窗口,这次寻呼就失败了。手机端重试几次失败后,就会提示连接失败。
这个问题的修复方式并不复杂——在固件的中断优先级和处理时间上做了优化,确保 page scan 窗口不会被其他中断长时间占用。但如果没有协议分析仪,我们可能还要在 HCI 参数、射频功率甚至天线匹配上浪费好几个星期。
6.4 我踩过的坑与调试建议
最后说几个我个人的经验,不一定会在标准文档里写,但对实际调试很有帮助。
第一个,别忽略 clock offset 的作用。HCI_Create_Connection命令里有个Clock_Offset参数,如果你在连接之前是通过Inquiry发现对方的,那么Inquiry结果里就有一个时钟信息可以用来估算对方的时钟相位,填进Clock_Offset可以让寻呼方更快地跳到正确的跳频相位。很多上层代码直接用 0,导致每次连接都要多花几百毫秒在跳频相位对齐上。虽然不至于连不上,但体验差别挺明显。
第二个,Allow_Role_Switch这个参数不要随便填 0 或 1。有些设备为了稳定,会直接把允许角色切换关掉,但这样在与另一台同样关掉角色切换的主机设备互连时,可能因为双方都想当 Master 而导致连接直接失败。比较好的做法是只在确实不需要角色切换的链路上关掉它,其余场景保持默认。
第三个,建议有条件的话团队里准备一台蓝牙协议分析仪。虽然 HCI 日志能解决 70% 的问题,但剩下 30% 的 LMP、AFH、跳频同步类问题,没有空中数据包基本只能靠猜。一台 Frontline 或者 Ellisys 虽然不便宜,但你算一下几个工程师耗在定位问题上的工时,这笔钱其实很快就能回本。
第四个,抓 HCI 日志的时候记得把hcidump或者btmon的-t时间戳选项打开。很多偶现问题需要对比多次复现之间的时间间隔差异,没有时间戳你会丢失非常关键的信息。
从实际使用体验来说,我认为把连接流程理解为一次多层协作的握手序列,比记住任何单独一个命令参数都重要。你只要牢记每一层各自负责什么、事件从哪里来、消息往哪里去,绝大多数连接问题都能在半小时内定位到具体层级。之后不管是调参数、改固件还是换天线,方向都不会跑偏。