4. 链路层(LL)核心机制:设备角色与状态机
链路层,也就是 Link Layer,简称 LL。它是 BLE 协议栈里最核心的一层,直接跑在硬件之上。说白了,所有设备之间的数据收发,最终都要通过 LL 来调度。我刚开始接触 BLE 时,总觉得 Host 层(GATT、GAP)才是重点,后来被一个连接断连的问题折磨了两天,才发现问题出在 LL 的状态机跳转上。嗯,从那以后我再也不敢小看这一层了。
今天咱们就聊聊 LL 的四个设备角色、五个状态机,以及空中接口的数据包长什么样。这些都是你写 BLE 固件时必须刻在脑子里的东西。
4.1 设备角色:你是广播者还是观察者?
BLE 定义了四种设备角色。你想想看,一个蓝牙设备在通信前,总得先搞清楚自己是“说话”还是“听话”,是“发起连接”还是“等待连接”。
| 角色 | 英文 | 核心行为 | 典型设备 |
|---|---|---|---|
| 广播者 | Broadcaster | 周期性发送广播包,不接收连接 | 信标、温度传感器 |
| 观察者 | Observer | 扫描广播包,不发起连接 | 蓝牙扫描仪、手机 App 扫描 |
| 外设 | Peripheral | 发送可连接广播,等待 Central 连接 | 心率带、智能手环 |
| 中心 | Central | 扫描并主动发起连接 | 手机、平板、网关 |
关键点:Broadcaster 和 Observer 之间不建立连接,数据通过广播通道单向传递。Peripheral 和 Central 之间会建立连接,实现双向数据交换。
我在项目中遇到过一种情况:客户要求一个传感器只发数据,不需要被连接。很多人直接配成了 Peripheral 角色,结果手机一扫描就能连上去,功耗还高。其实用 Broadcaster 就够了,省电又安全。
4.2 链路层状态机:五个状态的跳转逻辑
LL 的状态机是 BLE 通信的“交通规则”。设备在任何时刻只能处于以下五种状态之一:
- 待机(Standby)—— 初始状态,不发送也不接收任何数据。
- 广播(Advertising)—— 设备在广播信道上发送广播包。
- 扫描(Scanning)—— 设备监听广播信道,接收广播包。
- 发起(Initiating)—— 设备主动向指定广播者发起连接请求。
- 连接(Connection)—— 设备已建立连接,在数据信道上通信。
状态之间的跳转有严格规则。举个例子:从 Standby 可以直接跳到 Advertising,但你不能从 Scanning 直接跳到 Advertising,必须先回到 Standby。
我的习惯:调试时我会先确认设备当前处于哪个状态。用逻辑分析仪抓一下广播事件,看看有没有周期性广播包。如果没有,大概率是卡在 Standby 了,检查一下广播参数配置。
下面这张图展示了状态机的完整跳转关系:
我曾经踩过的坑:有一次设备在连接状态下异常掉线,我以为是信号问题。后来抓包发现,设备在 Connection 状态下收到了一个非预期的广播包,LL 层直接跳回了 Standby。原因是广播通道和连接通道的时序冲突,没有做好隔离。记住:连接状态下,设备只在数据通道上通信,不应该再监听广播通道。
4.3 空中接口数据包格式
BLE 在空中传输的数据包,结构非常紧凑。每个包由几个固定字段组成,我习惯把它拆成三部分来看:
- 前导码(Preamble):1 字节,用于接收端同步时钟。1M PHY 下固定为 0xAA 或 0x55。
- 访问地址(Access Address):4 字节。广播通道固定为 0x8E89BED6,数据通道使用连接期间分配的随机地址。
- 协议数据单元(PDU):2~257 字节,包含实际数据。
- CRC:3 字节,校验 PDU 的完整性。
PDU 内部又分为头部、载荷和可能存在的 MIC(消息完整性校验)。MIC 只在加密连接中出现,长度为 4 字节。
重点记忆:广播通道的访问地址是固定的 0x8E89BED6。你抓包时看到这个地址,就知道这是广播包。数据通道的访问地址是连接建立时由 Initiator 随机生成的,每个连接都不一样。
下面是一个典型的广播通道数据包结构:
| 前导码(1B) | 访问地址(4B) | PDU 头部(2B) | PDU 载荷(0-31B) | CRC(3B) |
| 0xAA | 0x8E89BED6 | 0x0040 | 广播数据... | 0x123456 |
PDU 头部虽然只有 2 字节,但信息量很大:
| 位域 | 长度 | 说明 |
|---|---|---|
| PDU 类型 | 4 bit | 0x00=ADV_IND, 0x01=ADV_DIRECT_IND, 0x02=ADV_NONCONN_IND, 0x03=SCAN_REQ, 0x04=SCAN_RSP, 0x05=CONNECT_REQ |
| RFU | 2 bit | 保留位,必须为 0 |
| ChSel | 1 bit | 信道选择算法指示 |
| TxAdd | 1 bit | 发送地址类型:0=公共地址,1=随机地址 |
| RxAdd | 1 bit | 接收地址类型 |
| 长度 | 6 bit | PDU 载荷的字节数(0~37) |
| RFU | 2 bit | 保留位 |
调试技巧:我习惯用 Wireshark 抓 BLE 空口包。打开后直接看 PDU 类型字段。如果看到大量 ADV_NONCONN_IND,说明设备是 Broadcaster 角色,不打算建立连接。如果看到 CONNECT_REQ,说明有人正在发起连接。这些信息对排查连接失败问题特别有用。
4.4 小结
链路层的核心就三件事:角色决定行为,状态机控制跳转,数据包格式决定怎么收发。你写代码时,脑子里要时刻清楚当前设备是什么角色、处于什么状态、发出去的包长什么样。
我个人习惯在初始化代码里显式设置角色和状态,而不是依赖默认值。比如用 Nordic 的 SDK,我会在 ble_gap_adv_start() 之前先确认广播类型是 BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE 还是 BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE。这个细节决定了设备是 Broadcaster 还是 Peripheral。