从零实现BLE协议栈(7-1)Slave Latency:低功耗的权衡艺术
Slave Latency:低功耗的权衡艺术
- 理解连接事件的周期性调度(第 4-2 篇有详细讲解):Slave 默认每隔 connInterval 唤醒一次射频,监听 Master 的包,回复 ACK 或数据,然后关闭射频等待下一次唤醒。本文的全部讨论都建立在这个周期性循环之上。
- 理解 Window Widening 的含义(第 5-2 篇有详细讲解):Slave 必须比预计的锚点更早打开 RX 窗口,以应对时钟漂移。WW 的大小直接影响 Slave Latency 跳过事件后重新监听时的功耗和成功率。
- 理解 L2CAP 发送队列(第 6-2 篇有详细讲解):Slave Latency 的一个核心约束是“有数据要发就不能跳过”,这里的“有数据”指的就是 L2CAP TX 队列非空。
不需要了解 LL Control Procedure 或 Instant 机制。本文只讨论纯粹的 Slave Latency 行为。
一、为什么要有 Slave Latency
一个典型的 BLE 连接场景是智能手表和手机配对。连接建立后,手表每隔 30 ms(connInterval = 24)就要唤醒一次射频.整个过程中,射频从唤醒到关闭大约 300~500 µs,但唤醒本身——从深度睡眠到 PLL 锁相——消耗的电流远超空闲时的待机功耗。对一块 CR2032 纽扣电池而言,每 30 ms 唤醒一次射频将在几周内耗尽电量。
问题在于:绝大多数连接事件里,Master 发来的是空包(没有新数据),Slave 回的也是空包。两端只是在互相确认"对方还活着"——纯粹的心跳维持,没有有效载荷。
如果 Slave 能够在没有数据要发的时候跳过一些连接事件不唤醒,功耗问题就迎刃而解了。这就是 Slave Latency(connSlaveLatency) 的设计初衷。
BLE Core Spec Vol 6, Part B, 4.5.2 定义了 Slave Latency 的行为:
The slave may transmit during any connection event that it listens or sends in. A slave does not need to listen during every connection event. The slave shall listen to at least every connSlaveLatency connection events even if it has no data to send.
翻译成操作规则:
- Slave 最多可以连续跳过
connSlaveLatency个连接事件 - 跳过
connSlaveLatency个之后,必须在下一个事件唤醒监听 - 如果 Slave 有数据要发(TX 队列非空),不得跳过,必须监听
- 跳过事件期间,信道选择算法仍需正常推进——下次唤醒时使用的信道编号必须和 Master 一致
connSlaveLatency 在连接建立时由 CONNECT_IND 的 Latency 字段给出,单位是"连接事件个数"。例如 Latency = 4 表示 Slave 最多跳过 4 个事件,即每 5 个事件至少醒一次。
这意味着 Slave 的有效唤醒周期从 connInterval 变成了 connInterval × (connSlaveLatency + 1)。例如:
| connInterval | connSlaveLatency | 有效唤醒周期 | 射频唤醒频率 |
|---|---|---|---|
| 30 ms | 0 | 30 ms | 每秒 33 次 |
| 30 ms | 4 | 150 ms | 每秒 6.7 次 |
| 30 ms | 9 | 300 ms | 每秒 3.3 次 |
从每秒 33 次降到每秒 3.3 次,射频功耗降低约 90%。
二、代码实现
实现 Slave Latency 最直观的误区是“直接让 CPU 睡到下次唤醒”。这会导致信道丢失和时钟脱节。跳过事件 \(\neq\) 什么都不做。
3.1 跳过判断函数
判断是否可以跳过当前连接事件的函数非常短,但每一个条件都至关重要:
static bool can_skip_event(void)
{
/* 已经用完 latency 配额 — 必须唤醒 */
if (lat.latency_used >= lat.latency_max) {
return false;
}
/* 有数据要发 — 必须唤醒 */
if (data_tx_q.count > 0 || tx_pdu_pending) {
return false;
}
return true;
}
3.2 主循环中的跳过逻辑
在连接态主循环中,跳过和监听的分支:
while (!conn_terminated) {
bool skip_this_event = false;
if (lat.latency_max > 0 && can_skip_event()) {
skip_this_event = true;
lat.latency_used++;
lat.events_skipped++;
/* ★ 跳过时: 仍需推进信道选择! */
(void)chan_sel_1();
conn_event_counter++;
} else {
lat.latency_used = 0;
lat.events_listened++;
}
if (!skip_this_event) {
/* 正常监听: 打开射频、收包、发包 */
ww_event_us += ww_periodic_us;
/* ... conn_event() ... */
} else {
/* ★ 跳过时: WW 仍需累积 */
ww_event_us += ww_periodic_us;
}
/* 锚点递推 (无论是否跳过) */
next_event_rtc = (next_event_rtc + interval_ticks) &
HAL_TICKER_CNTR_MASK;
next_event_remainder_ps += interval_remainder_ps;
/* ... */
}
有三个关键操作在跳过时必须执行:
chan_sel_1():推进信道选择算法,确保下次唤醒时使用正确的信道ww_event_us += ww_periodic_us:WW 按事件数线性累积- 锚点递推:
next_event_rtc += interval_ticks,确保时间线正确
跳过事件时不执行的是 conn_event() 函数——即不打开射频、不进行 TX/RX 交换。CPU 在推进完状态后,直接进入下一个循环迭代。
3.3 Window Widening 的累积效应
这里需要特别强调 WW 的累积问题。假设 ww_periodic_us = 5 µs(每个连接事件的漂移上限),latency_max = 4:
- 跳过事件 #1:ww = 5 µs(不监听)
- 跳过事件 #2:ww = 10 µs(不监听)
- 跳过事件 #3:ww = 15 µs(不监听)
- 跳过事件 #4:ww = 20 µs(不监听)
- 监听事件 #5:ww = 25 µs → 打开射频,RX 窗口需要
HCTO = ... + 2 × 25 µs + ...
如果不累积 WW,第 5 个事件使用的 WW 只有 5 µs,远不够覆盖 5 个 interval 的总漂移。结果就是 RX 窗口太窄,Master 的包到达时 RX 已经关闭——连接失败。
WW 的累积直到成功收到 Master 的包后才重置为 0。这正是 conn_event() 中 rx_ok 分支执行 ww_event_us = 0 的含义:成功同步一次,漂移归零,重新开始累积。
三、Supervision Timeout 的安全约束
Slave 不能无限跳过事件。BLE 规范要求:
这个约束保证即使 Slave 用满了 latency 配额,它在 Supervision Timeout 到期之前仍有足够多的连接事件来重新同步。如果不满足这个约束,可能出现以下场景:
- Slave 跳过了
connSlaveLatency个事件 - 第
connSlaveLatency + 1事件唤醒后,RX 超时(干扰或时钟漂移过大) - Slave 尝试在后续事件重试,但 Supervision Timeout 已经到期
- 连接被断开
在我们的实现中,即使 Slave 跳过了事件,supervision_timeout_us 的检查仍在每次非跳过事件的 conn_event() 失败分支中执行——一旦从上次成功 RX 算起超过了 supervisionTimeout,立即终止连接。
本系列教程同款硬件:👇
芯片: nRF 52832 开发板
工具: nRF 52840 BLE Dongle 蓝牙嗅探器
工具: 逻辑分析仪
工具: BPA low energy 蓝牙分析仪
本文版权归作者:ixbwer所有,转载请注明原文链接:https://www.cnblogs.com/ixbwer/p/19810111,否则保留追究法律责任的权利。

浙公网安备 33010602011771号