引言

在工业控制、物联网设备等场景中,STM32F4 的 RTC(实时时钟)常依赖 LSE(32.768kHz 外部晶振)提供精准时基。然而,晶振可能因振动、温度或老化而停振,导致日历走时失效。传统方案是直接切换到 LSI(内部低速 RC 振荡器),但 LSI 精度差(典型 ±1.5%),日误差可达数分钟。本文提出一种基于备份域寄存器的补偿策略:检测 LSE 停振后,自动切换至 LSI,并利用备份域寄存器存储补偿参数,结合软件算法修正日历漂移,实现无外部晶振下的高精度计时。

原理剖析

1. 备份域与 RTC 的关系

STM32F4 的备份域(Backup Domain)包含 RTC、备份寄存器(BKPx)和备份 SRAM,由 VBAT 引脚供电。当主电源 VDD 掉电时,备份域仍可工作,确保 RTC 和关键数据不丢失。备份寄存器(如 BKP0R~BKP19R)是 32 位通用寄存器,可在复位或掉电后保持内容,非常适合存储校准参数。

2. LSE 停振检测机制

RTC 的时钟源可选 LSE、LSI 或 HSE 分频。当选择 LSE 时,可通过 RTC_ISR 寄存器的 LSERDY 位监测晶振状态。若 LSERDY 变为 0,表示 LSE 停振。但硬件不会自动切换时钟源,需要软件干预。

3. 补偿策略设计

  • 检测阶段:周期性检查 LSERDY,若发现停振,立即记录当前时间戳。
  • 切换阶段:将 RTC 时钟源切换至 LSI,并重新初始化 RTC(注意切换会复位 RTC 计数器)。
  • 补偿阶段:利用备份域寄存器存储 LSI 的校准系数(如每 10 秒误差毫秒数),在每次 RTC 中断时累加误差并调整秒计数。

配置步骤

1. 使能备份域和 PWR 时钟

__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 允许访问备份域

2. 检测 LSE 状态并切换时钟源

void RTC_CheckLSEAndSwitch(void) {
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {
        // LSE 停振,切换到 LSI
        RCC_LSICmd(ENABLE);
        while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET);
        
        // 重新配置 RTC 时钟源为 LSI
        RTC_HandleTypeDef hrtc;
        hrtc.Instance = RTC;
        hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
        hrtc.Init.AsynchPrediv = 127;  // 异步分频
        hrtc.Init.SynchPrediv = 255;   // 同步分频,得到 1Hz
        hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
        HAL_RTC_Init(&hrtc);
        
        // 存储切换标志到备份寄存器
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0xCAFE); // 自定义标志
    }
}

3. 实现补偿算法

在 RTC 秒中断中,读取当前时间并应用补偿:

void HAL_RTCEx_RTCEventCallback(RTC_HandleTypeDef *hrtc) {
    static uint32_t tick_count = 0;
    static int32_t error_ms = 0;
    
    // 从备份寄存器读取校准参数(例如每 10 秒误差 +50ms)
    int32_t calib_ms = (int32_t)HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR1);
    
    tick_count++;
    if (tick_count >= 10) { // 每 10 秒调整一次
        tick_count = 0;
        error_ms += calib_ms;
        if (error_ms >= 1000) {
            // 误差累积超过 1 秒,增加 1 秒
            RTC_TimeTypeDef sTime;
            HAL_RTC_GetTime(hrtc, &sTime, RTC_FORMAT_BIN);
            sTime.Seconds++;
            if (sTime.Seconds >= 60) {
                sTime.Seconds = 0;
                sTime.Minutes++;
                if (sTime.Minutes >= 60) {
                    sTime.Minutes = 0;
                    sTime.Hours++;
                    if (sTime.Hours >= 24) sTime.Hours = 0;
                }
            }
            HAL_RTC_SetTime(hrtc, &sTime, RTC_FORMAT_BIN);
            error_ms -= 1000;
        }
    }
}

4. 校准参数获取

在系统正常使用 LSE 时,可周期性测量 LSI 的实际频率,计算误差并写入备份寄存器:

void CalibrateLSI(void) {
    // 使用定时器测量 LSI 频率(例如通过 MCO 输出或比较)
    // 假设测得 LSI 实际频率为 32768 * (1 + 0.015) = 33259 Hz
    // 则每 10 秒误差 = (33259 - 32768) * 10 / 32768 ≈ 150ms
    int32_t calib_ms = 150; // 正数表示走快
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, (uint32_t)calib_ms);
}

完整代码示例

以下是一个集成检测、切换和补偿的简化示例(基于 HAL 库):

#include "stm32f4xx_hal.h"

RTC_HandleTypeDef hrtc;

void SystemClock_Config(void);
void RTC_Init(void);
void RTC_CheckAndCompensate(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    RTC_Init();
    
    while (1) {
        RTC_CheckAndCompensate();
        HAL_Delay(1000); // 每秒检查一次
    }
}

void RTC_Init(void) {
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_RCC_BKP_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();
    
    // 尝试启用 LSE
    RCC_OscInitTypeDef oscInit = {0};
    oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSE;
    oscInit.LSEState = RCC_LSE_ON;
    if (HAL_RCC_OscConfig(&oscInit) != HAL_OK) {
        // LSE 启动失败,直接使用 LSI
        RCC_LSICmd(ENABLE);
        while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET);
        HAL_RCCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0xBAD); // 标记使用 LSI
    }
    
    // 配置 RTC
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 127;
    hrtc.Init.SynchPrediv = 255;
    hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
    HAL_RTC_Init(&hrtc);
    
    // 使能秒中断
    HAL_RTCEx_SetSecond_IT(&hrtc);
    HAL_NVIC_EnableIRQ(RTC_IRQn);
}

void RTC_CheckAndCompensate(void) {
    // 检查备份寄存器标志
    uint32_t flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
    if (flag == 0xCAFE) {
        // 已切换到 LSI,执行补偿
        // 补偿逻辑在回调中处理
    } else if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {
        // LSE 停振,切换并存储标志
        RCC_LSICmd(ENABLE);
        while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET);
        // 重新初始化 RTC(会复位计数器,需保存时间)
        RTC_TimeTypeDef sTime;
        HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        HAL_RTC_DeInit(&hrtc);
        HAL_RTC_Init(&hrtc);
        HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0xCAFE);
        // 设置校准参数(示例值)
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 150); // 每10秒快150ms
    }
}

void RTC_IRQHandler(void) {
    HAL_RTCEx_RTCIRQHandler(&hrtc);
}

void HAL_RTCEx_RTCEventCallback(RTC_HandleTypeDef *hrtc) {
    // 补偿逻辑(见上文)
}

注意事项

  • 时钟切换会复位 RTC:切换 LSE 到 LSI 时,RTC 计数器会清零,务必先保存当前时间再重新设置。
  • 备份寄存器容量有限:STM32F4 有 20 个备份寄存器(BKP0R~BKP19R),每个 32 位,合理规划存储内容。
  • LSI 温度漂移:LSI 频率受温度影响大,建议在系统运行时定期重新校准,更新备份寄存器中的补偿值。
  • 中断优先级:RTC 秒中断应设为较高优先级,避免补偿逻辑被延迟。
  • 低功耗模式:在待机模式下,备份域仍供电,但 LSI 可能关闭,需在进入低功耗前确保 RTC 时钟源稳定。

总结

通过备份域寄存器,我们不仅能在 LSE 停振后无缝切换到 LSI,还能存储校准参数,实现软件补偿。这种方法无需额外硬件,成本低且灵活性高。在实际项目中,建议结合系统 RTC 唤醒功能和定期校准机制,进一步提升时间精度。希望本文能为你的嵌入式设计提供有力参考。