4. 链路层核心机制:设备状态机与连接管理
链路层(Link Layer)是蓝牙协议栈里最贴近硬件的部分。说白了,它负责两台设备之间怎么发现对方、怎么建立连接、怎么收发数据。我刚开始接触蓝牙开发时,觉得链路层就是个状态机来回跳,后来踩了几个坑才明白——不理解状态机,你连广播包都抓不明白。
这一章我们重点拆解五个核心概念:设备状态机、广播信道与数据信道、连接事件与连接间隔、白名单与过滤策略。嗯,每个都是实战中绕不开的硬骨头。
4.1 设备状态机:五种状态的切换逻辑
蓝牙链路层定义了五种设备状态:就绪(Standby)、广播(Advertising)、扫描(Scanning)、发起(Initiating)、连接(Connection)。设备在同一时刻只能处于其中一种状态。
我习惯把这五种状态画成一个五角星,每个角代表一种状态,箭头代表切换条件。你想想看,设备上电后默认进入 Standby,然后根据上层指令跳转到其他状态。
核心原则:设备不能从一种「活动状态」直接跳到另一种「活动状态」,必须先回到 Standby 再切换。比如从广播状态不能直接进入扫描状态,必须经过 Standby。
状态切换速查表
| 当前状态 | 目标状态 | 触发条件 |
|---|---|---|
| Standby | Advertising | 主机下发 LE_Set_Advertising_Enable |
| Standby | Scanning | 主机下发 LE_Set_Scan_Enable |
| Standby | Initiating | 主机下发 LE_Create_Connection |
| Advertising | Connection | 收到连接请求并完成握手 |
| Scanning | Connection | 收到可连接广播并发起连接 |
| Initiating | Connection | 收到目标设备的广播并完成连接 |
| 任何状态 | Standby | 主机下发断开或超时 |
我在项目中遇到过一个问题:设备在广播状态下收到扫描请求,按理说应该回复扫描响应,但有些芯片实现会把设备自动切到扫描状态——这是错误的。记住,广播状态下的设备只能回复扫描响应,不能切换状态。
避坑指南:我曾经在调试一款低功耗蓝牙门锁时,发现门锁广播一段时间后自动停止。排查了半天,原来是广播超时参数设得太短。BLE 规范规定广播事件间隔加上随机延迟不能超过 10.24 秒,但很多 SDK 默认值只有 30 秒的广播时长限制。如果你需要长时间广播,记得检查 Advertising_Duration 参数。
4.2 广播信道与数据信道
蓝牙 4.0 之后把 2.4GHz ISM 频段划分成 40 个信道,每个信道 2MHz。其中 3 个广播信道(37、38、39)用于设备发现和连接建立,37 个数据信道(0-36)用于连接后的数据传输。
为什么是 3 个广播信道?说白了就是为了避开 Wi-Fi 的 1、6、11 信道。37 信道在 2402MHz,38 在 2426MHz,39 在 2480MHz,刚好卡在 Wi-Fi 信道的间隙里。这个设计很巧妙,我当年第一次看到这个频段规划时,不得不佩服蓝牙 SIG 的工程师。
广播信道 vs 数据信道
| 特性 | 广播信道 | 数据信道 |
|---|---|---|
| 数量 | 3 个(37/38/39) | 37 个(0-36) |
| 用途 | 广播、扫描、连接请求 | 连接后的数据收发 |
| 跳频 | 固定频率,不跳频 | 自适应跳频(AFH) |
| 数据包长度 | 最大 47 字节(含报头) | 最大 255 字节(含报头) |
| 功耗 | 较高(持续发送) | 较低(按连接事件收发) |
广播信道上的数据包格式很简单:前导码(1 字节)+ 访问地址(4 字节)+ PDU + CRC(3 字节)。访问地址固定为 0x8E89BED6,这是蓝牙规范写死的。数据信道上的访问地址则是在连接建立时由链路层随机生成,每个连接都不一样。
实战技巧:如果你用抓包工具(比如 Ellisys 或 Nordic 的 nRF Sniffer)分析蓝牙空中包,看到访问地址是 0x8E89BED6,那一定是广播包。数据包的访问地址是随机生成的,你可以用它来区分不同连接。
4.3 连接事件与连接间隔
连接建立后,主设备和从设备按照固定的时间间隔进行数据交换。这个时间间隔就叫 连接间隔(Connection Interval),单位是 1.25ms。规范允许的范围是 7.5ms(6 个单元)到 4s(3200 个单元)。
每个连接事件(Connection Event)包含一次主设备发送和一次从设备回复。主设备在连接事件开始时发送第一个数据包,从设备收到后回复。如果双方还有数据要发,可以继续在当前连接事件内交换,直到双方都没有数据或达到最大传输次数。
我习惯把连接事件想象成「定时约会」:主设备在每个连接间隔开始时主动找从设备聊天,聊完就各自休息,等下一个时间点再聊。
连接间隔的选择策略
- 短间隔(7.5ms - 30ms):低延迟,适合音频、实时控制。但功耗高,因为设备频繁唤醒。
- 中间隔(30ms - 100ms):平衡方案,适合数据采集、传感器上报。大部分 IoT 设备用这个范围。
- 长间隔(100ms - 4s):超低功耗,适合周期性上报的传感器(如温湿度计)。但延迟大。
我在做一款智能手环时,需要同时保持手机连接和心率数据采集。连接间隔设成 30ms,结果手环电池撑不到一天。后来我把连接间隔调到 100ms,心率数据用缓存批量上报,续航直接翻了三倍。嗯,这里要注意:连接间隔不是越小越好,要根据实际业务场景权衡。
连接参数更新流程:从设备可以请求更新连接参数(包括连接间隔、延迟、超时时间)。主设备收到请求后可以选择接受或拒绝。我建议从设备不要在连接刚建立时就请求更新,等 5-10 秒后再发,成功率更高。
4.4 白名单与过滤策略
白名单(White List)是链路层提供的一种硬件级过滤机制。你可以把允许连接的设备地址写入白名单,链路层在收到广播或扫描请求时自动比对地址,匹配的才放行,不匹配的直接丢弃。
为什么要用白名单?说白了就是为了省电。如果没有白名单,设备收到每个广播包都要上报给主机处理,主机频繁唤醒,功耗直线上升。白名单在链路层就完成了过滤,主机可以继续睡大觉。
白名单的三种过滤策略
| 策略 | 描述 | 适用场景 |
|---|---|---|
| 仅扫描过滤 | 只过滤扫描请求和扫描响应 | 设备只扫描,不广播 |
| 仅广播过滤 | 只过滤连接请求 | 设备只广播,不接受陌生连接 |
| 扫描+广播过滤 | 同时过滤扫描和连接请求 | 双向安全过滤,最常用 |
白名单存储在链路层控制器内部,容量有限。一般芯片支持 8-32 个地址。我建议不要塞满,留几个空位给动态添加的设备。
避坑指南:我曾经在一个网关项目里用了白名单过滤,结果发现某些手机连不上。排查后发现是手机使用了随机地址(RPA),每次连接地址都不一样。白名单里存的是静态地址,自然匹配不上。解决方案是使用「解析列表(Resolving List)」配合白名单,让链路层能解析可解析私有地址。
4.5 链路层状态机全景图
下面这张 SVG 图把链路层状态机、信道切换、连接事件串在一起。我建议你把它存下来,调试时对照着看。
4.6 实战:状态机调试技巧
调试链路层状态机是蓝牙开发中最容易出问题的环节。我总结了几条经验:
- 用日志打印状态切换:在每次状态切换时打印当前状态和触发事件。我习惯用
LL_STATE_CHANGE回调函数来捕获。 - 抓空口包确认:不要只看代码逻辑,用抓包工具看实际空口包。有时候芯片实现和规范有差异。
- 注意状态超时:广播超时、连接超时、扫描超时,每个超时参数都要仔细配置。默认值不一定适合你的场景。
- 白名单测试:先不加白名单跑通基本流程,再加白名单验证过滤逻辑。一步到位容易出问题。
调试小工具:我常用 Nordic 的 nRF Connect 手机 App 配合开发板调试。手机端可以模拟主设备,开发板做从设备,两边同时抓日志,对比状态机是否一致。这个方法帮我解决过至少 5 个连接失败的问题。