4. 时钟与定时器管理:蓝牙协议栈对系统时钟的依赖,如何实现低功耗定时器

做蓝牙助听器,时钟就是命根子。

我刚开始接触蓝牙协议栈移植时,总觉得时钟管理不就是配个晶振、设个分频嘛,有什么难的?直到第一次调通BLE连接,发现设备每隔几秒就断连一次,抓包一看——时钟漂移了。嗯,从那以后我再也不敢小看时钟管理了。

4.1 蓝牙协议栈为什么离不开精确时钟?

说白了,蓝牙通信就是一场「约定好的时间游戏」。

主设备和从设备之间,所有数据包的发送和接收,都基于一个共同的时钟基准。蓝牙规范里有个核心概念叫 蓝牙时钟(Bluetooth Clock),它由 312.5 μs 的 tick 驱动。每个数据包的发送时机、跳频序列的切换、连接事件的调度,全依赖这个时钟。

我举个例子你就明白了:

  • 连接事件(Connection Event)的起始时间,由主设备的蓝牙时钟决定
  • 从设备必须在精确的时间窗口内醒来接收数据
  • 如果时钟偏差超过 ±20 ppm,连接就会不稳定

你想想看,助听器这种设备,用户戴着它走在街上,突然蓝牙断了,声音卡顿——这体验得多糟糕。

关键参数:蓝牙协议栈要求系统时钟精度至少达到 ±20 ppm,对于低功耗蓝牙(BLE),连接间隔越短,对时钟精度的要求越高。

4.2 系统时钟的三种典型来源

在实际项目中,我遇到过三种主流的时钟方案。每种都有它的脾气。

时钟源 精度 功耗 典型场景
外部晶振(32.768 kHz) ±10 ~ ±20 ppm 低(~1 μA) 睡眠模式、低功耗运行
外部晶振(16/32 MHz) ±10 ~ ±30 ppm 高(~5 mA) 活动模式、数据收发
内部RC振荡器 ±1% ~ ±3% 极低(~0.5 μA) 深度睡眠、粗略计时

我个人习惯,在助听器这种对功耗极其敏感的设备上,主时钟用外部32.768 kHz晶振,配合内部RC做唤醒定时器。为什么?因为32.768 kHz晶振的功耗只有1 μA左右,精度却能达到 ±20 ppm,刚好满足BLE连接的要求。

小技巧:如果你用的SoC内部有32 kHz RC振荡器,别直接用它做蓝牙时钟。我曾经试过,结果连接间隔一拉长到100 ms以上,设备就开始丢包。内部RC的温度漂移太大了,从0°C到50°C能漂出几百ppm。

4.3 低功耗定时器的实现思路

助听器要省电,核心思路就一条:能睡就睡,醒了赶紧干活,干完继续睡

蓝牙协议栈里,低功耗定时器主要干三件事:

  1. 唤醒调度:在下一个连接事件到来前,把CPU从睡眠中唤醒
  2. 超时管理:比如连接超时、扫描超时、广播超时
  3. 延迟补偿:补偿从唤醒到真正开始收发之间的时间差

我分享一个我在项目中用过的实现框架:

// 低功耗定时器驱动接口(伪代码)
typedef struct {
    uint32_t    target_tick;    // 目标唤醒时刻(基于蓝牙时钟)
    uint32_t    current_tick;   // 当前蓝牙时钟值
    uint32_t    wakeup_latency; // 唤醒延迟(从定时器触发到CPU就绪)
    void        (*callback)(void);
} lp_timer_t;

// 设置下一个唤醒时间
void lp_timer_set(lp_timer_t *timer, uint32_t delay_ms) {
    // 1. 读取当前蓝牙时钟
    timer->current_tick = bt_clock_get();
    
    // 2. 计算目标唤醒时刻(考虑唤醒延迟)
    timer->target_tick = timer->current_tick 
                        + ms_to_ticks(delay_ms) 
                        - ms_to_ticks(timer->wakeup_latency);
    
    // 3. 配置硬件定时器(使用32.768 kHz晶振)
    hw_timer_compare_set(timer->target_tick);
    
    // 4. 进入睡眠模式
    enter_sleep_mode(SLEEP_MODE_DEEP);
}

// 定时器中断服务函数
void lp_timer_isr(void) {
    // 1. 清除中断标志
    hw_timer_clear_irq();
    
    // 2. 调用回调(通常是蓝牙协议栈的调度函数)
    if (timer.callback) {
        timer.callback();
    }
    
    // 3. 重新计算下一个唤醒点
    lp_timer_schedule_next();
}

这里有个坑,我踩过好几次——唤醒延迟。从定时器触发中断,到CPU真正开始执行代码,中间有几十微秒的延迟。如果你不补偿这个延迟,每次唤醒都会晚一点点,累积起来连接就断了。

注意:唤醒延迟不是固定的!它受温度、电压、晶振起振时间影响。我建议你在初始化时做一次校准,把实际测得的延迟写入寄存器。我在一个项目中,因为没做校准,结果从 -10°C 到 60°C 延迟差了将近 200 μs,连接直接崩了。

4.4 时钟同步与漂移补偿

蓝牙协议栈内部有一套时钟同步机制。主设备在每个连接事件里会发送一个包,从设备根据这个包的时间戳来调整自己的本地时钟。

具体来说,从设备会计算:

// 时钟漂移补偿计算
int32_t drift = (expected_arrival_time - actual_arrival_time);
if (abs(drift) > DRIFT_THRESHOLD) {
    // 调整本地时钟的 tick 周期
    clock_adjust(drift);
    // 更新睡眠定时器的目标时间
    lp_timer_resync();
}

我见过一些工程师,为了省事,直接把漂移补偿关了。结果呢?设备在口袋里放一会儿,拿出来就断连了。因为体温导致晶振频率变化,漂移越来越大。

所以我的建议是:漂移补偿一定要开,而且补偿周期不能太长。BLE规范建议每个连接事件都做一次补偿,但考虑到功耗,我一般每 4~8 个连接事件做一次。这样既省电,又不会让漂移累积太多。

4.5 实战中的低功耗定时器配置

最后,我总结一下在助听器项目中,低功耗定时器的配置要点:

  • 时钟源选择:32.768 kHz 外部晶振 + 内部 RC 做备份
  • 定时器分辨率:至少 312.5 μs(蓝牙 tick 单位),建议做到 62.5 μs
  • 唤醒延迟校准:上电后做一次,运行时每 10 秒校准一次
  • 睡眠模式:用深度睡眠(保留 RAM),不要用掉电模式
  • 中断优先级:定时器中断优先级要高于蓝牙协议栈的其他中断

一句话总结:时钟是蓝牙的骨架,定时器是蓝牙的脉搏。把这两样管好了,你的助听器蓝牙协议栈移植就成功了一半。

下一章,我会讲蓝牙协议栈的内存管理——这又是一个容易翻车的地方。到时候见。