一、问题背景:LSE 停振为何是“隐形杀手”

STM32F4 的 RTC 通常依赖外部低速晶振(LSE,32.768kHz)提供高精度时钟源。但 LSE 对 PCB 布局、环境温度敏感,一旦停振,RTC 将失去计数基准。若未做处理,系统复位后 RTC 时间会回退至默认值,导致数据记录、通信协议时间戳错乱。

核心难点

  • LSE 停振是渐进过程,软件难以实时捕捉。
  • 备份域(VBAT)供电时,RTC 寄存器内容保留,但时钟源失效后计数停止。
  • 常规方案(如检测 RTC_ISR 的 LSERDY 位)存在响应延迟,且复位后无法区分“正常上电”与“异常恢复”。

二、无感恢复原理:备份域寄存器 + 双时钟源切换

2.1 备份域寄存器(BKP)的角色

STM32F4 的备份域包含 20 个 32 位数据寄存器(BKP_DR0~DR19),由 VBAT 供电,复位不影响其内容。我们利用这些寄存器实现:

  • 状态标志:记录 LSE 是否曾停振。
  • 时间快照:在停振瞬间保存 RTC 计数器的值(以 LSI 为基准)。

2.2 无感恢复流程

  1. 正常运行:RTC 使用 LSE,BKP_DR0 写入魔数(如 0xA5A5),BKP_DR1 存储当前时间戳。
  2. LSE 停振检测:通过 RTC_ISR 的 LSERDY 位或 LSE 旁路监测,触发中断。
  3. 紧急切换:在中断服务函数中,将 RTC 时钟源切换至 LSI(约 32kHz),并立即读取 RTC 计数器值存入 BKP_DR1。
  4. 系统复位:若发生复位,启动时检查 BKP_DR0 魔数。若存在且 RTC 时钟源为 LSI,则说明发生过停振,用 BKP_DR1 恢复时间,并重新校准 LSE。

关键点:切换时钟源时,RTC 计数器会清零,因此必须先保存旧值。

三、寄存器级配置步骤

3.1 使能备份域访问

// 使能 PWR 和 BKP 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
// 允许备份域写入
PWR_BackupAccessCmd(ENABLE);

3.2 初始化 RTC 并设置时钟源

void RTC_Init(void) {
    // 选择 LSE 作为 RTC 时钟
    RCC_LSEConfig(RCC_LSE_ON);
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待就绪
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    RCC_RTCCLKCmd(ENABLE);
    RTC_WaitForSynchro();
    // 配置 RTC 参数(24小时制,1秒中断等)
    RTC_InitTypeDef rtcInit;
    rtcInit.RTC_HourFormat = RTC_HourFormat_24;
    rtcInit.RTC_AsynchPrediv = 0x7F;
    rtcInit.RTC_SynchPrediv = 0xFF;
    RTC_Init(&rtcInit);
    // 写入魔数和时间快照
    BKP_WriteBackupRegister(BKP_DR0, 0xA5A5);
    BKP_WriteBackupRegister(BKP_DR1, RTC_GetCounter());
}

3.3 LSE 停振检测与切换

void LSE_Stop_Handler(void) {
    // 检查 LSE 是否就绪
    if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) {
        // 保存当前时间(基于 LSE 的计数)
        uint32_t timeBefore = RTC_GetCounter();
        BKP_WriteBackupRegister(BKP_DR1, timeBefore);
        
        // 切换至 LSI
        RCC_LSICmd(ENABLE);
        while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
        RTC_WaitForSynchro();
        // 重新配置预分频(LSI 频率约 32kHz,需调整)
        RTC_InitTypeDef rtcInit;
        rtcInit.RTC_HourFormat = RTC_HourFormat_24;
        rtcInit.RTC_AsynchPrediv = 0x7F; // 根据实际 LSI 频率微调
        rtcInit.RTC_SynchPrediv = 0xFF;
        RTC_Init(&rtcInit);
        // 恢复时间(从 BKP_DR1 读取,但注意切换后计数器已清零,需重新设置)
        RTC_SetCounter(BKP_ReadBackupRegister(BKP_DR1));
        // 设置标志位
        BKP_WriteBackupRegister(BKP_DR2, 0x5A5A); // 表示已切换
    }
}

3.4 复位后的恢复逻辑

void RTC_Recovery(void) {
    if (BKP_ReadBackupRegister(BKP_DR0) == 0xA5A5) {
        // 检查是否发生过切换
        if (BKP_ReadBackupRegister(BKP_DR2) == 0x5A5A) {
            // 从备份寄存器恢复时间
            uint32_t savedTime = BKP_ReadBackupRegister(BKP_DR1);
            // 当前时钟源可能是 LSI,先尝试恢复 LSE
            RCC_LSEConfig(RCC_LSE_ON);
            uint32_t wait = 0;
            while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET && wait < 1000) wait++;
            if (wait < 1000) {
                // LSE 恢复,切换回 LSE
                RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
                RTC_WaitForSynchro();
                // 重新配置预分频
                RTC_InitTypeDef rtcInit;
                rtcInit.RTC_HourFormat = RTC_HourFormat_24;
                rtcInit.RTC_AsynchPrediv = 0x7F;
                rtcInit.RTC_SynchPrediv = 0xFF;
                RTC_Init(&rtcInit);
                // 设置时间(注意:切换时钟源会清零计数器,需重新设置)
                RTC_SetCounter(savedTime);
                // 清除切换标志
                BKP_WriteBackupRegister(BKP_DR2, 0x0000);
            } else {
                // LSE 仍不可用,保持 LSI,但时间已恢复
                RTC_SetCounter(savedTime);
            }
        }
    }
}

四、完整代码示例(主函数集成)

int main(void) {
    // 初始化时钟、GPIO 等
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
    PWR_BackupAccessCmd(ENABLE);
    
    // 检查备份域标志,执行恢复
    RTC_Recovery();
    
    // 正常初始化 RTC(如果未初始化过)
    if (BKP_ReadBackupRegister(BKP_DR0) != 0xA5A5) {
        RTC_Init();
    }
    
    // 使能 LSE 停振检测中断(通过 EXTI 或轮询)
    // 此处以轮询为例
    while (1) {
        if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) {
            LSE_Stop_Handler();
        }
        // 其他任务
    }
}

五、注意事项与工程实践

  • 备份域写入保护:必须调用 PWR_BackupAccessCmd(ENABLE) 才能写 BKP 寄存器,否则写操作无效。
  • 时钟切换清零:RTC 时钟源切换会导致计数器清零,务必先保存旧值,切换后重新设置。
  • LSI 精度:LSI 频率误差较大(±5%),若需高精度,可在恢复后定期校准或使用外部高精度时钟源。
  • 中断优先级:LSE 停振检测中断应设为高优先级,避免被其他中断延迟。
  • 魔数校验:建议使用 16 位魔数,并考虑 CRC 校验,防止数据损坏。
  • 低功耗模式:在待机模式下,备份域仍供电,但 RTC 时钟源需保持有效,否则时间会停止。
  • 调试技巧:通过串口打印 BKP 寄存器值,验证恢复逻辑是否按预期执行。

六、总结

本文提出的方案利用 STM32F4 备份域寄存器的非易失特性,实现了 LSE 停振后的时间无感恢复。核心在于:

  1. 停振时快速保存时间戳并切换至 LSI。
  2. 复位后通过魔数识别异常状态,恢复时间并尝试重新启用 LSE。

该策略适用于对时间连续性要求高的嵌入式系统,如智能电表、工业控制器。开发者可根据实际需求调整预分频值,并增加看门狗或错误日志功能,进一步提升鲁棒性。