4. 链路层状态机与包类型

链路层是 BLE 协议栈里最核心的一层。说白了,它决定了设备什么时候发、什么时候收、发什么格式的数据。我刚开始接触 BLE 时,总觉得这层就是个状态切换,后来被一个广播超时问题折腾了两天,才意识到状态机的细节有多重要。

这一章,我们就来拆解链路层的五种状态,以及它们对应的包类型。你把这些搞清楚了,抓包分析时就能一眼看出设备在干什么。

4.1 链路层五种状态

BLE 链路层定义了五种基本状态。设备在同一时刻只能处于其中一种状态。它们之间的切换,构成了所有 BLE 通信的基础。

核心要点:链路层状态机是 BLE 通信的骨架。抓包时,你看到的每一个包,都对应着设备在某个状态下的行为。

BLE 链路层状态机 Standby Advertising Scanning Initiating Connection 断开连接 空闲 广播 扫描 发起 连接

4.1.1 Standby(空闲状态)

这是设备的初始状态,也是唯一一个不发送也不接收任何无线包的状态。设备上电后,默认就待在这里。从 Standby 可以跳转到 Advertising、Scanning 或 Initiating。反过来,其他状态结束后也会回到 Standby。

4.1.2 Advertising(广播状态)

设备在广播信道(37/38/39)上发送广播包,告诉周围设备“我在这里”。广播者可以是 Beacon、传感器,也可以是等待被连接的外设。

我的经验:调试 Beacon 产品时,我习惯先抓广播包确认广播间隔和 TX Power 是否正确。有一次发现广播间隔被误配成 20ms,导致功耗飙升。抓包一看,问题一目了然。

4.1.3 Scanning(扫描状态)

扫描设备在广播信道上监听广播包。它不主动发数据,只负责接收。扫描分为被动扫描和主动扫描两种。主动扫描会额外发送扫描请求,获取更多信息。

4.1.4 Initiating(发起状态)

这个状态很特殊。它和 Scanning 类似,也是在广播信道上监听。但区别在于:Initiating 只监听特定设备的广播包,一旦收到,立即发送连接请求,建立连接。

注意:Initiating 状态是单向的。它只能从 Standby 进入,成功建立连接后进入 Connection 状态,失败则回到 Standby。我曾经见过一个 bug,发起设备在收到广播包后没有及时发送 CONNECT_REQ,导致连接超时。抓包分析发现,是协议栈处理延迟造成的。

4.1.5 Connection(连接状态)

连接建立后,主设备和从设备都进入 Connection 状态。此时,它们按照预定义的连接事件(Connection Event)在数据信道上通信。连接状态可以回到 Standby,但必须通过断开连接操作。

4.2 广播包类型

广播包是 BLE 通信的起点。链路层定义了四种广播包类型,每种都有特定的用途。抓包时,通过包类型字段就能判断设备的行为意图。

包类型 PDU 类型值 可连接 可扫描 典型用途
ADV_IND 0000 是 是 通用广播,可被连接或扫描
ADV_DIRECT_IND 0001 是 否 定向广播,快速重连
ADV_NONCONN_IND 0010 否 否 不可连接广播,Beacon 常用
ADV_SCAN_IND 0110 否 是 可扫描广播,响应扫描请求

4.2.1 ADV_IND(通用广播包)

这是最常见的广播包。它允许其他设备扫描它,也允许其他设备发起连接。我抓包时,90% 以上的广播包都是这个类型。它的广播间隔通常为 100ms 到 1.28s,具体取决于应用场景。

4.2.2 ADV_DIRECT_IND(定向广播包)

这种包只发给一个特定的设备。它包含目标设备的 MAC 地址,其他设备收到后会直接忽略。定向广播的间隔很短(通常 3.75ms),用于快速重连。但它的功耗也高,不适合长时间使用。

避坑指南:我曾经在开发一个耳机配对功能时,用了定向广播做快速重连。结果发现,如果目标设备不在范围内,广播者会一直以 3.75ms 间隔狂发包,电池很快就耗光了。后来我加了一个超时机制,3 秒内连不上就切回 ADV_IND。

4.2.3 ADV_NONCONN_IND(不可连接广播包)

这种包只广播,不接受连接请求,也不响应扫描请求。Beacon 设备(如 iBeacon、Eddystone)用的就是它。它的好处是功耗低,适合需要长时间广播的场景。

4.2.4 ADV_SCAN_IND(可扫描广播包)

这种包允许扫描设备发送扫描请求,但它不接受连接请求。它常用于广播一些额外信息,比如设备名称或服务 UUID。扫描设备收到广播后,可以主动请求更多数据。

4.3 数据包类型

连接建立后,主从设备之间交换的就是数据包。数据包分为两类:LL Data PDU 和 LL Control PDU。抓包时,通过 LLID 字段可以区分它们。

LLID 值 PDU 类型 说明
01 LL Data PDU(续传) 包含 L2CAP 数据,可能分段
10 LL Data PDU(起始) 包含 L2CAP 数据,起始或完整包
11 LL Control PDU 链路层控制命令

4.3.1 LL Data PDU

这是承载用户数据的包。它包含 L2CAP 层的数据,也就是应用层真正关心的内容。LL Data PDU 有两种子类型:

  • 起始包(LLID=10):表示一个 L2CAP 数据包的开始,或者是一个完整的短包。
  • 续传包(LLID=01):表示后续的分段数据。如果 L2CAP 包太大,会被拆成多个 LL Data PDU 发送。

4.3.2 LL Control PDU

这是链路层的控制命令,用于管理连接本身。常见的控制命令包括:

  • LL_CONNECTION_UPDATE_REQ:更新连接参数(间隔、延迟、超时)。
  • LL_CHANNEL_MAP_REQ:更新信道映射。
  • LL_TERMINATE_IND:断开连接。
  • LL_PAUSE_ENC_REQ / LL_PAUSE_ENC_RSP:暂停加密。

抓包技巧:当你看到 LL Control PDU 时,说明链路层正在做“家务活”。比如,连接参数更新失败时,你会看到大量的 LL_CONNECTION_UPDATE_REQ 重传。这时候,问题往往出在主设备不支持从设备请求的参数上。

4.4 状态切换与包类型的对应关系

理解了状态和包类型,我们来看看它们在实际通信中是怎么配合的。我画了一张简化的流程图,帮你理清思路。

状态与包类型对应关系 Advertising 发送广播包 Scanning 接收广播包 Initiating 发送 CONNECT_REQ Connection 交换 LL Data PDU / LL Control PDU 广播包:ADV_IND / ADV_DIRECT_IND / ADV_NONCONN_IND / ADV_SCAN_IND

你看,广播状态下只发广播包,扫描状态下只收广播包,发起状态下发 CONNECT_REQ,连接状态下才交换数据包和控制包。每种状态对应的包类型是固定的,不会乱。

注意:抓包时,如果你在广播信道上看到了 LL Data PDU,那一定是抓错了。数据包只在连接事件的数据信道上出现。我刚开始抓包时,就犯过这个错误,对着广播信道上的噪声分析半天,后来才发现信道没切对。

4.5 实战:抓包识别状态与包类型

最后,分享一个我常用的抓包分析方法。当你拿到一段抓包日志时,可以按以下步骤快速定位设备状态:

  1. 看信道:包在 37/38/39 上?那就是广播信道,设备处于 Advertising、Scanning 或 Initiating 状态。
  2. 看 PDU 类型:广播包看 PDU Type 字段,数据包看 LLID 字段。
  3. 看设备地址:广播包里有发送者和接收者地址。如果接收者是全零(公共地址),那就是广播状态。如果接收者是特定设备,那就是定向广播或发起状态。
  4. 看连接事件:如果包在数据信道上,并且有 Access Address(不是 0x8E89BED6),那就是连接状态。

我的习惯:我会在 Wireshark 里设置过滤条件,比如 btle.advertising_header.pdu_type == 0 只看 ADV_IND 包。这样能快速过滤出感兴趣的包,省去翻找的时间。