从零实现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,则单个周期内的潜在时间误差量级约为:

\[100\,\text{ms} \times 500 \times 10^{-6} = 50\,\mu s \]

50 µs 意味着什么?意味着:如果 Slave 以为第 1 次事件应在某时刻开始,但 Master 的真实定时因为晶振误差已经早了或晚了 50 µs,那么 Slave 若只在理想锚点附近开一个很窄的接收窗,根本罩不住 Master 的包。
更麻烦的是,这种误差不是一次性的。若长时间没有成功对时,偏差会继续随时间累积,而不是自动归零。

所以工程上的关键,不是有没有锚点,而是当锚点开始偏离现实时,系统怎么不崩。这就引出了 Window Widening。


三、补偿机制:窗口拓宽 (Window Widening)

image

3.1 主动留出容错余量

Window Widening 是规范规定的容错机制:Slave 必须在预期锚点的前后各扩展一定时间,保持接收窗口开启,以便捕获可能偏移的 Master 包。

3.2 公式推导

扩展量根据双方的晶振精度和距上次成功同步的时间来计算:

\[W_{widening} = (SCA_{Master} + SCA_{Slave}) \times T_{since\_last\_sync} \]

单位:ppm µs = µs。
以具体数字为例:若双方 SCA 均为 500 ppm,距上次同步已过 1 s:

\[W_{widening} = (500 + 500) \times 10^{-6} \times 1{,}000{,}000\,\mu\text{s} = 1000\,\mu\text{s} \]

Slave 必须在预期锚点 \(W_{widening}\) µs 开启接收,在预期锚点 \(W_{widening}\) µs 才能关闭。

3.3 实际窗口宽度

\[W_{total} = 2 \times W_{widening} + WinSize \]

其中 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_ADDRESSlast_rx_aa_us)和 EVENTS_READYlast_rx_ready_us)。两者差值就是"从 RXEN 到 AA 匹配"的实际时间。完全无漂移的情况应等于:

\[expected\_s2a = EVENT\_JITTER + TICKER\_RES\_MARGIN + T_{addr\_1M} \]

两者之差即漂移量 \(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_ticksinterval_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 蓝牙分析仪

posted @ 2026-04-02 10:02  ixbwer  阅读(78)  评论(0)    收藏  举报