# 引言:低功耗与精度的博弈 在物联网节点、可穿戴设备等场景中,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 ```c 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 更简单)。 ```c 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 唤醒中断中,读取当前时间,计算实际间隔,调整下一次闹钟。 ```c 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 模式 ```c 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 秒以内。开发者可根据具体芯片和应用需求调整参数,实现功耗与精度的最佳平衡。