从零实现BLE协议栈(5-1)漂移与补偿:晶振误差与WW
漂移与补偿:晶振误差与 Window Widening
前提知识
阅读本文需要具备以下基础:
- 理解锚点和调度器骨架(第 4-2、4-3 篇有详细讲解):本文不再解释锚点是什么或调度器如何运行,而是直接讨论为什么长时间运行后连接会失步,以及系统如何自我恢复。
- 理解 ppm 与 SCA 字段(第 3-1 篇有详细讲解):Window Widening 公式的核心输入是两端的 SCA 等级。
- 理解 RTC tick 精度机制(第 4-2 篇讲解了 ticks + remainder 的亚 tick 表示):漂移修正在 tick 和 picosecond 两个层次同时操作,需要理解这个双层表示的原因。
一、漂移的根源:晶振误差 (SCA)
到第 4-2 篇为止,我们已经建立了一个看似完整的画面:连接事件围绕锚点组织,锚点按 T_0 + n Interval 排布。
但在真实世界里,每块晶振都有自己的误差,用 SCA(Sleep Clock Accuracy) 量化,单位是 ppm(百万分之一)。比如 500 ppm 的意思是:理想经过 1 秒时,实际计时值可能快或慢约 500 µs。
CONNECT_IND 的 SCA 字段(3 bit)把 Master 的晶振精度分为 7 个等级:
| SCA 字段值 | Master 精度(ppm) |
|---|---|
| 0 | 251500 |
| 1 | 151250 |
| 2 | 101150 |
| 3 | 76100 |
| 4 | 5175 |
| 5 | 3150 |
| 6 | 2130 |
| 7 | 020 |
Demo 代码把这张表直接存放为数组,用于 WW 参数计算:
/* main.c */
const uint16_t sca_ppm_table[] = {500, 250, 150, 100, 75, 50, 30, 20};
二、漂移对锚点的影响
假设连接间隔是 100 ms,而某一方的时钟精度是 500 ppm,则单个周期内的潜在时间误差量级约为:
50 µs 意味着什么?意味着:如果 Slave 以为第 1 次事件应在某时刻开始,但 Master 的真实定时因为晶振误差已经早了或晚了 50 µs,那么 Slave 若只在理想锚点附近开一个很窄的接收窗,根本罩不住 Master 的包。
更麻烦的是,这种误差不是一次性的。若长时间没有成功对时,偏差会继续随时间累积,而不是自动归零。
所以工程上的关键,不是有没有锚点,而是当锚点开始偏离现实时,系统怎么不崩。这就引出了 Window Widening。
三、补偿机制:窗口拓宽 (Window Widening)

3.1 主动留出容错余量
Window Widening 是规范规定的容错机制:Slave 必须在预期锚点的前后各扩展一定时间,保持接收窗口开启,以便捕获可能偏移的 Master 包。
3.2 公式推导
扩展量根据双方的晶振精度和距上次成功同步的时间来计算:
单位:ppm µs = µs。
以具体数字为例:若双方 SCA 均为 500 ppm,距上次同步已过 1 s:
Slave 必须在预期锚点前 \(W_{widening}\) µs 开启接收,在预期锚点后 \(W_{widening}\) µs 才能关闭。
3.3 实际窗口宽度
其中 WinSize 是首次连接事件时由 CONNECT_IND 指定的接收窗口,后续正常事件中 WinSize 通常为 0。
3.4 代码实现与上限保护
/* conn.c: conn_loop() 中的 WW 参数初始化 */
uint16_t master_sca_ppm = sca_ppm_table[conn_params.sca & 0x07];
uint32_t combined_sca_ppm = (uint32_t)master_sca_ppm + LOCAL_SCA_PPM;
/* 每个 connInterval 带来的 WW 增量(向上取整) */
uint32_t ww_periodic_us = DIV_ROUND_UP(
combined_sca_ppm * conn_interval_us, 1000000);
/* WW 上限 = 半个 Interval - T_IFS(防止覆盖到下一个事件) */
uint32_t ww_max_us = (conn_interval_us >> 1) - T_IFS_US;
每次事件增加一份 WW,上限 ww_max_us 防止接收窗口延伸到下一个连接事件引发全身性时序混乱:
ww_event_us += ww_periodic_us;
if (ww_event_us > ww_max_us) {
ww_event_us = ww_max_us;
}
uint32_t hcto_add = ((EVENT_JITTER_US + EVENT_TICKER_RES_MARGIN_US +
ww_event_us) << 1) + win_size_event_us;
四、锚点重同步 (Re-sync)
Window Widening 是被动容错,锚点重同步(漂移校正) 则是主动纠偏每次成功收到 Master 的包后,用实际到达时刻来更新本端对锚点的预测。
4.1 原理
收到包时,TIMER0 硬件捕获会记录 EVENTS_ADDRESS(last_rx_aa_us)和 EVENTS_READY(last_rx_ready_us)。两者差值就是"从 RXEN 到 AA 匹配"的实际时间。完全无漂移的情况应等于:
两者之差即漂移量 \(drift = actual\_s2a - expected\_s2a\)。
drift > 0:Master 包晚了drift < 0:Master 包早了
4.2 到 RTC tick + 皮秒余量的修正
uint32_t actual_s2a = last_rx_aa_us - last_rx_ready_us;
int32_t drift_us = (int32_t)actual_s2a - (int32_t)(EVENT_JITTER_US + EVENT_TICKER_RES_MARGIN_US + ADDR_US_1M);
/* 把 drift_us 折算成皮秒并修正 remainder */
int64_t adj_ps = (int64_t)next_event_remainder_ps + (int64_t)drift_us * 1000000LL;
while (adj_ps >= (int64_t)ps_per_tick) {
adj_ps -= ps_per_tick;
next_event_rtc = (next_event_rtc + 1) & HAL_TICKER_CNTR_MASK;
}
while (adj_ps < 0) {
adj_ps += ps_per_tick;
next_event_rtc = (next_event_rtc - 1) & HAL_TICKER_CNTR_MASK;
}
next_event_remainder_ps = (uint32_t)adj_ps;
亚 tick 皮秒余量的作用:
RTC0 运行在 32768 Hz,每个 tick 约 30.52 µs。直接转为 tick 截断误差极大。用皮秒记录亚 tick 精度能把误差控制在纳秒级。每次连接事件后推进 interval_ticks 和 interval_remainder_ps,由于累计余量进位,锚点才不会因整数化单向漂移。
4.3 锚点更新与重置WW
新的 next_event_rtc 作为下一次锚点基准,并重置 WW 为 0:
last_rx_rtc = next_event_rtc;
anchor.last_rx_anchor_rtc = next_event_rtc;
/* 锚点修正后 WW 和 win_size 归零 */
ww_event_us = 0;
win_size_event_us = 0;
五、Supervision Timeout 与 Slave Latency
5.1 Supervision Timeout
如果连续丢包,WW 扩大,功耗上升。如果在超时时间内完全没收到 Master 的包,判定为断开:
uint32_t elapsed_us = HAL_TICKER_TICKS_TO_US((next_event_rtc - last_rx_rtc) & HAL_TICKER_CNTR_MASK);
if (elapsed_us >= supervision_timeout_us) {
break; /* 退出,返回广播态 */
}
5.2 Slave Latency 期间的处理的代价
Slave 跳过 N 个事件后醒来,距上次同步已过 \((N+1) \times Interval\),对应的 WW 增量是 N+1 倍。宽窗口代价可以用更低的 SCA 等级晶振来平衡换取省电。
本系列教程同款硬件:👇
芯片: nRF 52832 开发板
工具: nRF 52840 BLE Dongle 蓝牙嗅探器
工具: 逻辑分析仪
工具: BPA low energy 蓝牙分析仪
本文版权归作者:ixbwer所有,转载请注明原文链接:https://www.cnblogs.com/ixbwer/p/19810026,否则保留追究法律责任的权利。

浙公网安备 33010602011771号