4. 链路层状态机与包类型
链路层是 BLE 协议栈里最核心的一层。说白了,它决定了设备什么时候发、什么时候收、发什么格式的数据。我刚开始接触 BLE 时,总觉得这层就是个状态切换,后来被一个广播超时问题折腾了两天,才意识到状态机的细节有多重要。
这一章,我们就来拆解链路层的五种状态,以及它们对应的包类型。你把这些搞清楚了,抓包分析时就能一眼看出设备在干什么。
4.1 链路层五种状态
BLE 链路层定义了五种基本状态。设备在同一时刻只能处于其中一种状态。它们之间的切换,构成了所有 BLE 通信的基础。
核心要点:链路层状态机是 BLE 通信的骨架。抓包时,你看到的每一个包,都对应着设备在某个状态下的行为。
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 状态切换与包类型的对应关系
理解了状态和包类型,我们来看看它们在实际通信中是怎么配合的。我画了一张简化的流程图,帮你理清思路。
你看,广播状态下只发广播包,扫描状态下只收广播包,发起状态下发 CONNECT_REQ,连接状态下才交换数据包和控制包。每种状态对应的包类型是固定的,不会乱。
注意:抓包时,如果你在广播信道上看到了 LL Data PDU,那一定是抓错了。数据包只在连接事件的数据信道上出现。我刚开始抓包时,就犯过这个错误,对着广播信道上的噪声分析半天,后来才发现信道没切对。
4.5 实战:抓包识别状态与包类型
最后,分享一个我常用的抓包分析方法。当你拿到一段抓包日志时,可以按以下步骤快速定位设备状态:
- 看信道:包在 37/38/39 上?那就是广播信道,设备处于 Advertising、Scanning 或 Initiating 状态。
- 看 PDU 类型:广播包看 PDU Type 字段,数据包看 LLID 字段。
- 看设备地址:广播包里有发送者和接收者地址。如果接收者是全零(公共地址),那就是广播状态。如果接收者是特定设备,那就是定向广播或发起状态。
- 看连接事件:如果包在数据信道上,并且有 Access Address(不是 0x8E89BED6),那就是连接状态。
我的习惯:我会在 Wireshark 里设置过滤条件,比如 btle.advertising_header.pdu_type == 0 只看 ADV_IND 包。这样能快速过滤出感兴趣的包,省去翻找的时间。