引言

在工业控制、物联网设备等嵌入式场景中,RTC(实时时钟)的稳定性至关重要。STM32F4 系列常使用 LSE(低速外部晶振,32.768kHz)作为 RTC 时钟源,但 LSE 可能因环境干扰、晶振老化或 PCB 设计问题而停振。一旦停振,RTC 时间会停止,若未妥善处理,系统重启后可能显示错误时间,甚至导致数据记录错乱。

传统方案是检测到停振后直接复位系统或重新初始化 RTC,但这会造成服务中断。本文将介绍一种更优雅的方案:利用 RTC 备份域寄存器(BKP)保存关键状态,在 LSE 恢复后无感切换,确保时间连续性和系统稳定性。

原理剖析

1. RTC 备份域寄存器(BKP)

STM32F4 的 RTC 模块包含 20 个 32 位备份寄存器(BKP0~BKP19),位于备份域中。备份域由 VBAT 引脚供电,当主电源 VDD 掉电时,只要 VBAT 有电,BKP 寄存器内容就不会丢失。此外,备份域还包含 RTC 时钟、校准寄存器等。

关键特性:

  • 备份域复位源独立:仅当备份域复位(如 POR/PDR 或 BDRST 位)时才会清零。
  • 访问保护:通过 PWR_CR 的 DBP 位解锁,防止意外写入。
  • 可存储任意数据:适合保存 RTC 校准值、时间戳或状态标志。

2. LSE 停振检测机制

STM32F4 的 RTC 提供时钟安全系统(CSS)功能,专门用于监测 LSE。当 LSE 停振时,CSS 会触发一个不可屏蔽中断(NMI),并自动切换到 LSI(内部低速时钟)作为 RTC 时钟源。但 LSI 精度较低(典型 ±5%),长时间使用会导致时间漂移。

我们的策略:

  • 在 CSS 中断中,记录当前 RTC 时间到 BKP 寄存器,并设置停振标志。
  • 尝试重新启动 LSE(通过 RCC->BDCR 控制)。
  • 若 LSE 恢复,则基于 BKP 中保存的时间进行补偿,并清除标志。
  • 若 LSE 长时间无法恢复,则继续使用 LSI,但定期更新 BKP 中的时间,确保数据不丢失。

配置步骤

1. 使能备份域访问和时钟

首先,需要使能 PWR 和 RTC 时钟,并解锁备份域访问。

// 使能 PWR 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
// 使能备份域访问
PWR_BackupAccessCmd(ENABLE);
// 使能 LSE 和 RTC 时钟(若未使能)
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);

2. 初始化 RTC 并配置 CSS

初始化 RTC 为独立模式,并开启 CSS 中断。

// 初始化 RTC(以 1Hz 为例)
RTC_InitTypeDef RTC_InitStruct;
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
RTC_InitStruct.RTC_AsynchPrediv = 0x7F;
RTC_InitStruct.RTC_SynchPrediv = 0xFF;
RTC_Init(&RTC_InitStruct);

// 使能 CSS 中断(NMI)
RCC_ITConfig(RCC_IT_LSECSS, ENABLE);
NVIC_EnableIRQ(RTC_WKUP_IRQn); // 注意:CSS 中断实际映射到 NMI,但需使能相关 NVIC 通道?
// 实际上,CSS 中断是 NMI,无需 NVIC 配置,但需在 NMI_Handler 中处理。

注意:CSS 中断会触发 NMI,因此需要在 NMI_Handler 中处理。

3. 编写 CSS 中断处理函数

在 NMI 处理函数中,检测 CSS 标志,保存时间到 BKP,并尝试恢复 LSE。

void NMI_Handler(void) {
    if (RCC_GetITStatus(RCC_IT_LSECSS) != RESET) {
        // 清除 CSS 中断标志
        RCC_ClearITPendingBit(RCC_IT_LSECSS);
        
        // 读取当前 RTC 时间(此时时钟源已自动切换到 LSI)
        RTC_TimeTypeDef time;
        RTC_GetTime(RTC_Format_BIN, &time);
        
        // 保存到 BKP 寄存器(例如 BKP0 存秒数,BKP1 存标志)
        uint32_t seconds = (time.RTC_Hours * 3600) + (time.RTC_Minutes * 60) + time.RTC_Seconds;
        BKP_WriteBackupRegister(BKP_DR0, seconds);
        BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 标志:停振发生
        
        // 尝试重新启动 LSE(先关闭再开启)
        RCC_LSEConfig(RCC_LSE_OFF);
        RCC_LSEConfig(RCC_LSE_ON);
        // 等待 LSE 就绪,但不要阻塞过久,可设置超时
        uint32_t timeout = 0xFFFF;
        while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout-- > 0));
        
        if (timeout > 0) {
            // LSE 恢复,RTC 会自动切换回 LSE?实际上需要手动切换时钟源?
            // 注意:RTC 时钟源切换需要重新配置 RTC 时钟,但这里我们保留 LSI 继续运行,稍后补偿。
            // 更稳妥:记录恢复时间,在下次系统启动时补偿。
            BKP_WriteBackupRegister(BKP_DR2, 1); // 标记 LSE 已恢复
        } else {
            BKP_WriteBackupRegister(BKP_DR2, 0); // 标记 LSE 未恢复
        }
    }
}

4. 系统启动时的恢复逻辑

在系统初始化时,检查 BKP 中的标志,若存在停振记录,则进行时间补偿。

void RTC_Recovery(void) {
    // 读取 BKP 标志
    uint32_t flag = BKP_ReadBackupRegister(BKP_DR1);
    if (flag == 0xA5A5) {
        // 有停振记录
        uint32_t saved_seconds = BKP_ReadBackupRegister(BKP_DR0);
        uint32_t lse_restored = BKP_ReadBackupRegister(BKP_DR2);
        
        // 获取当前 RTC 时间(可能基于 LSI 或 LSE)
        RTC_TimeTypeDef time;
        RTC_GetTime(RTC_Format_BIN, &time);
        uint32_t current_seconds = (time.RTC_Hours * 3600) + (time.RTC_Minutes * 60) + time.RTC_Seconds;
        
        // 计算漂移:假设 LSI 漂移率为 5%,则实际时间 = 当前时间 - 漂移量
        // 这里简化处理:若 LSE 已恢复,则直接使用 LSE 时间,但需补偿停振期间的时间差
        // 更精确:记录停振持续时间,但需额外硬件支持。此处示例仅演示原理。
        
        if (lse_restored) {
            // LSE 已恢复,但 RTC 可能仍使用 LSI,需重新配置时钟源
            // 重新初始化 RTC 时钟为 LSE,并设置时间(基于保存的时间 + 停振时长)
            // 停振时长 = 当前时间 - 保存时间(但当前时间可能已漂移)
            // 实际应用中,可结合外部时间源(如 GPS)校准。
            // 示例:直接使用保存的时间,忽略漂移(简单场景)
            RTC_SetTime(RTC_Format_BIN, &time); // 保持当前时间,但后续会切换时钟源
            // 切换时钟源:先关闭 RTC 时钟,再重新配置
            RCC_RTCCLKCmd(DISABLE);
            RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
            RCC_RTCCLKCmd(ENABLE);
            // 等待 RTC 就绪
            RTC_WaitForSynchro();
        } else {
            // LSE 未恢复,继续使用 LSI,但更新 BKP 中的时间,以便下次恢复
            BKP_WriteBackupRegister(BKP_DR0, current_seconds);
        }
        
        // 清除标志
        BKP_WriteBackupRegister(BKP_DR1, 0);
    }
}

完整代码示例

以下是一个简化的完整示例,包含初始化、CSS 处理和恢复逻辑。

// main.c
#include "stm32f4xx.h"

void RTC_Init(void);
void RTC_Recovery(void);
void NMI_Handler(void);

int main(void) {
    // 初始化系统时钟等
    SystemInit();
    
    // 使能备份域访问
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    PWR_BackupAccessCmd(ENABLE);
    
    // 检查并恢复 RTC
    RTC_Recovery();
    
    // 初始化 RTC(若未初始化)
    if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) {
        RTC_Init();
    }
    
    while (1) {
        // 主循环
    }
}

void RTC_Init(void) {
    // 使能 LSE
    RCC_LSEConfig(RCC_LSE_ON);
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
    
    // 配置 RTC 时钟源
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    RCC_RTCCLKCmd(ENABLE);
    
    // 等待 RTC 同步
    RTC_WaitForSynchro();
    
    // 配置 RTC 参数
    RTC_InitTypeDef RTC_InitStruct;
    RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
    RTC_InitStruct.RTC_AsynchPrediv = 0x7F;
    RTC_InitStruct.RTC_SynchPrediv = 0xFF;
    RTC_Init(&RTC_InitStruct);
    
    // 设置初始时间(例如 2024-01-01 00:00:00)
    RTC_TimeTypeDef time;
    time.RTC_H12 = RTC_H12_AM;
    time.RTC_Hours = 0;
    time.RTC_Minutes = 0;
    time.RTC_Seconds = 0;
    RTC_SetTime(RTC_Format_BIN, &time);
    
    // 使能 CSS 中断
    RCC_ITConfig(RCC_IT_LSECSS, ENABLE);
}

void RTC_Recovery(void) {
    uint32_t flag = BKP_ReadBackupRegister(BKP_DR1);
    if (flag == 0xA5A5) {
        uint32_t saved_seconds = BKP_ReadBackupRegister(BKP_DR0);
        uint32_t lse_restored = BKP_ReadBackupRegister(BKP_DR2);
        
        RTC_TimeTypeDef time;
        RTC_GetTime(RTC_Format_BIN, &time);
        uint32_t current_seconds = (time.RTC_Hours * 3600) + (time.RTC_Minutes * 60) + time.RTC_Seconds;
        
        if (lse_restored) {
            // LSE 已恢复,切换时钟源并保持时间
            RCC_RTCCLKCmd(DISABLE);
            RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
            RCC_RTCCLKCmd(ENABLE);
            RTC_WaitForSynchro();
            // 可选:根据 saved_seconds 和 current_seconds 补偿时间
            // 这里简化,直接使用当前时间
        } else {
            // LSE 未恢复,更新保存的时间
            BKP_WriteBackupRegister(BKP_DR0, current_seconds);
        }
        
        // 清除标志
        BKP_WriteBackupRegister(BKP_DR1, 0);
    }
}

void NMI_Handler(void) {
    if (RCC_GetITStatus(RCC_IT_LSECSS) != RESET) {
        RCC_ClearITPendingBit(RCC_IT_LSECSS);
        
        // 保存时间
        RTC_TimeTypeDef time;
        RTC_GetTime(RTC_Format_BIN, &time);
        uint32_t seconds = (time.RTC_Hours * 3600) + (time.RTC_Minutes * 60) + time.RTC_Seconds;
        BKP_WriteBackupRegister(BKP_DR0, seconds);
        BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
        
        // 尝试恢复 LSE
        RCC_LSEConfig(RCC_LSE_OFF);
        RCC_LSEConfig(RCC_LSE_ON);
        uint32_t timeout = 0xFFFF;
        while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout-- > 0));
        
        if (timeout > 0) {
            BKP_WriteBackupRegister(BKP_DR2, 1);
        } else {
            BKP_WriteBackupRegister(BKP_DR2, 0);
        }
    }
}

注意事项

  • 备份域电源:确保 VBAT 引脚连接电池或超级电容,否则 BKP 数据在掉电后会丢失。
  • CSS 中断处理:CSS 中断是 NMI,不可屏蔽,处理函数必须简洁快速,避免长时间阻塞。
  • 时钟切换:在切换 RTC 时钟源时,必须先关闭 RTC 时钟,否则可能导致配置错误。
  • 时间补偿:示例中未实现精确补偿,实际应用中可结合外部时间源(如 GPS、NTP)或使用 LSI 的校准功能。
  • BKP 寄存器数量:STM32F4 有 20 个 BKP 寄存器,足够存储多个状态,但注意不要溢出。
  • 测试:建议在开发板上模拟 LSE 停振(如断开晶振),验证恢复逻辑。

总结

通过 RTC 备份域寄存器,我们可以在 LSE 停振时保存关键时间信息,并在恢复后无感切换,避免系统重启或时间丢失。该方法充分利用了 STM32F4 的硬件特性,提高了系统的鲁棒性。在实际项目中,还需根据具体需求完善时间补偿算法,以达到更高精度。希望本文能为你提供有价值的参考。