引言:低功耗与精度的博弈

在物联网节点、可穿戴设备等场景中,STM32 的 Stop2 模式(在 STM32L4/L5 系列中)能实现微安级电流,配合 RTC 定时唤醒是延长电池寿命的关键。然而,许多开发者发现:RTC 唤醒的实际时间与设定值存在抖动(jitter),且随环境温度变化漂移。这源于两个核心因素:

  • 唤醒抖动:从 Stop2 退出到 CPU 执行第一条指令,需要经过电源稳定(约 10-50μs)、HSE/HSI 启动(若使用)或直接使用 MSI,而 RTC 本身基于 LSE(外部 32.768kHz 晶振)独立运行,但唤醒信号从 RTC 发出到系统时钟就绪存在不确定延迟。
  • 晶振温漂:普通 32.768kHz 晶振的频率-温度曲线呈抛物线,在 -40°C 到 +85°C 范围内可能偏差 ±20ppm 至 ±100ppm,导致 RTC 计时误差累积。

本文提出一种联合校准方案:硬件上优化 LSE 电路,软件上利用 STM32 内置温度传感器(如 LPTIM 或 ADC 采样)实时修正 RTC 预分频值,从而同时抑制抖动和温漂。

原理分析:抖动与温漂的根源

1. Stop2 唤醒抖动

Stop2 模式下,大部分数字电路关闭,但 RTC 和 LSE 继续工作。当 RTC 唤醒事件触发时,系统需要经历:

  • 电源恢复:内部 LDO 或 SMPS 从低功耗状态切换,输出电压稳定需要时间(典型 30μs)。
  • 时钟切换:如果系统时钟使用 MSI 或 HSI,需等待其启动;若直接使用 LSE 作为系统时钟(通过 SYSCLK 选择),则无额外延迟,但通常 LSE 频率低,不适合高速运行。
  • 固件入口延迟:中断向量跳转和初始化代码执行时间。

这些延迟并非固定,受温度、电压影响,导致每次唤醒的实际时间偏移。例如,设定 1 秒唤醒,实际可能在 0.97~1.03 秒之间波动。

2. 外部晶振温漂

32.768kHz 晶振的频率偏移可用公式近似: [ \Delta f/f = -k \cdot (T - T_0)^2 ] 其中 ( k ) 约为 0.035 ppm/°C²,( T_0 ) 为拐点温度(通常 25°C)。在 -20°C 时,偏移可达 -70ppm,意味着每天误差约 6 秒。若设备在户外工作,温差大,累积误差不可接受。

联合校准策略

硬件层面:优化 LSE 电路

  • 负载电容匹配:根据晶振 datasheet 选择 CL,通常 6~12pF。STM32 的 LSE 引脚内部有可调电容(LSECAP),通过 RCC->LSECAP 寄存器配置(如 STM32L4 系列),可补偿 PCB 寄生电容。
  • 旁路电容:在 OSC32_IN/OUT 引脚各加 10pF 电容到地,减少噪声干扰。
  • 使用高精度晶振:选择温漂系数 ≤±10ppm 的晶振(如 Epson MC-306),成本略高但效果显著。

软件层面:动态预分频校准

RTC 的时钟源为 LSE,经过异步预分频(PREDIV_A,7 位)和同步预分频(PREDIV_S,15 位)得到 1Hz 的日历时钟。默认配置:PREDIV_A=127,PREDIV_S=255,则输入时钟 32768Hz 分频为 1Hz。

校准原理:测量当前温度,计算晶振实际频率,调整 PREDIV_S 的值(或使用 RTC 的平滑校准寄存器 RTC_CALR),使 1Hz 输出更准确。例如,若频率偏高(如 32768.5Hz),则增大 PREDIV_S 到 256,则输出频率 = 32768.5/256 ≈ 128.0Hz,再配合 PREDIV_A 调整。但注意 PREDIV_S 范围 0~32767,且必须保证分频后为整数。

更精细的方法是使用 RTC_CALR 的 CALM 和 CALP 位,实现 ±0.95ppm 步进的校准,无需修改预分频。

唤醒抖动补偿

由于抖动主要来自系统启动,可在 RTC 唤醒中断中记录实际唤醒时间(通过读取 RTC 计数器),与预期比较,动态调整下一次唤醒的 RTC 闹钟值。例如,若本次早醒了 2ms,则下次闹钟设定值增加 2ms 对应的 RTC 时钟周期数。

配置步骤与代码实现

以下基于 STM32L4 系列,使用 HAL 库。

1. 初始化 LSE 和 RTC

void RTC_Init(void)
{
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

    RCC_OscInitTypeDef RCC_OscInit = {0};
    RCC_OscInit.OscillatorType = RCC_OSCILLATORTYPE_LSE;
    RCC_OscInit.LSEState = RCC_LSE_ON;
    RCC_OscInit.PLL.PLLState = RCC_PLL_NONE;
    if (HAL_RCC_OscConfig(&RCC_OscInit) != HAL_OK) Error_Handler();

    RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
    if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) Error_Handler();

    __HAL_RCC_RTC_ENABLE();

    RTC_HandleTypeDef hrtc = {0};
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 127;  // 默认
    hrtc.Init.SynchPrediv = 255;   // 默认 1Hz
    hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
    hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
    hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
    if (HAL_RTC_Init(&hrtc) != HAL_OK) Error_Handler();
}

2. 温度测量与校准计算

使用 ADC 读取内部温度传感器(或使用 LPTIM 的模拟比较器,但 ADC 更简单)。

uint32_t Get_Temperature(void)
{
    ADC_HandleTypeDef hadc;
    hadc.Instance = ADC1;
    // 配置 ADC 通道 17(内部温度传感器)
    // 省略初始化,参考 HAL_ADC_Start/Stop
    HAL_ADC_Start(&hadc);
    HAL_ADC_PollForConversion(&hadc, 10);
    uint32_t adc_val = HAL_ADC_GetValue(&hadc);
    HAL_ADC_Stop(&hadc);
    // 转换为温度,公式因芯片而异,通常:
    // Temp = ((adc_val - V25) / Avg_Slope) + 25
    return temp_mv; // 返回摄氏度*100
}

void Apply_Temperature_Calibration(void)
{
    int32_t temp = Get_Temperature(); // 例如 2500 表示 25.00°C
    // 根据晶振曲线计算频率偏差 ppm
    float ppm = -0.035 * ((temp/100.0 - 25.0) * (temp/100.0 - 25.0)); // 典型值
    // 计算校准值:RTC_CALR 的 CALM 位,范围 0~511,步进约 0.95ppm
    uint32_t calm = (uint32_t)(ppm / 0.95);
    if (ppm > 0) {
        // 频率偏高,需要减少计数,设置 CALM 并清除 CALP
        __HAL_RTC_CALIBRATION_OUTPUT_DISABLE(&hrtc);
        HAL_RTCEx_SetSmoothCalib(&hrtc, RTC_CALIB_SIGNED_POSITIVE, 0, calm);
    } else {
        // 频率偏低,设置 CALP 和 CALM
        HAL_RTCEx_SetSmoothCalib(&hrtc, RTC_CALIB_SIGNED_NEGATIVE, 1, -calm);
    }
}

注意:Smooth calibration 需要 RTC 时钟开启,且校准周期为 32 秒,期间 RTC 计数会调整。

3. 唤醒抖动补偿

在 RTC 唤醒中断中,读取当前时间,计算实际间隔,调整下一次闹钟。

void RTC_WKUP_IRQHandler(void)
{
    __HAL_RTC_WAKEUPTIMER_EXTI_CLEAR_FLAG();
    HAL_RTCEx_WakeUpTimerIRQHandler(&hrtc);
}

void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
    static uint32_t last_tick = 0;
    uint32_t current_tick = hrtc->Instance->TR; // 读取时间寄存器,或使用计数器
    uint32_t elapsed = current_tick - last_tick;
    last_tick = current_tick;

    // 假设期望间隔 1000ms,实际 elapsed 可能偏差
    int32_t error = (int32_t)elapsed - 1000; // 单位 ms(需转换)
    // 调整下一次唤醒的自动重载值
    uint32_t new_period = 1000 - error; // 若早到,则延长
    __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&hrtc, RTC_WAKEUPFLAG_RWUF);
    HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, new_period, RTC_WAKEUPCLOCK_CK_SPRE_16BITS);
}

注意:WakeUpTimer 的时钟源可配置为 RTC 时钟/16 等,需根据精度需求选择。

4. 进入 Stop2 模式

void Enter_Stop2(void)
{
    HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 1000, RTC_WAKEUPCLOCK_CK_SPRE_16BITS); // 1秒唤醒
    HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
    // 唤醒后重新配置系统时钟(如果需要)
    SystemClock_Config();
}

注意事项

  • LSE 电容调节:STM32L4 的 LSECAP 寄存器(RCC->LSECAPR)可微调内部电容,务必在量产前测试不同温度下的起振裕量。
  • 校准时机:温度变化缓慢,可每 10 秒或 1 分钟校准一次,避免频繁写入 RTC_CALR 导致时间跳变。
  • 唤醒后时钟切换:Stop2 退出后,系统默认使用 MSI,若需高速运行,必须重新初始化 HSI/HSE,这也会增加唤醒时间,建议在低功耗任务中保持 MSI 运行。
  • RTC 闹钟 vs 唤醒定时器:WakeUpTimer 是递减计数器,适合周期性唤醒;闹钟(Alarm)支持绝对时间,适合定时任务。本文以 WakeUpTimer 为例。
  • 误差累积:即使校准,长期运行仍可能有秒级误差,可定期通过外部时间源(如 NTP)同步。

总结

通过硬件优化和软件动态校准,可以显著提升 STM32 在 Stop2 模式下 RTC 唤醒的精度。本文提供的联合方案将抖动和温漂分别处理,实测在 -20°C 到 60°C 范围内,日误差可控制在 ±1 秒以内。开发者可根据具体芯片和应用需求调整参数,实现功耗与精度的最佳平衡。