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 通信的“交通规则”。设备在任何时刻只能处于以下五种状态之一:

  1. 待机(Standby)—— 初始状态,不发送也不接收任何数据。
  2. 广播(Advertising)—— 设备在广播信道上发送广播包。
  3. 扫描(Scanning)—— 设备监听广播信道,接收广播包。
  4. 发起(Initiating)—— 设备主动向指定广播者发起连接请求。
  5. 连接(Connection)—— 设备已建立连接,在数据信道上通信。

状态之间的跳转有严格规则。举个例子:从 Standby 可以直接跳到 Advertising,但你不能从 Scanning 直接跳到 Advertising,必须先回到 Standby。

我的习惯:调试时我会先确认设备当前处于哪个状态。用逻辑分析仪抓一下广播事件,看看有没有周期性广播包。如果没有,大概率是卡在 Standby 了,检查一下广播参数配置。

下面这张图展示了状态机的完整跳转关系:

BLE 链路层状态机跳转图 BLE 链路层状态机跳转图 待机 (Standby) 广播 (Advertising) 扫描 (Scanning) 发起 (Initiating) 连接 (Connection) 开始广播 开始扫描 发起连接 停止广播 停止扫描 连接失败 收到连接请求 连接成功 断开连接 说明:实线箭头表示主动跳转,虚线箭头表示被动跳转(停止/失败/断开) 所有状态都必须经过 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。