引言

在工业控制、物联网设备等场景中,RTC(实时时钟)是系统时间基准的核心。STM32F4 系列通常采用 32.768kHz 的 LSE 晶振来驱动 RTC,以获得高精度。然而,LSE 可能因晶振老化、外部干扰或焊接问题而停振。一旦停振,RTC 将失去时钟源,导致时间停滞,若未处理,复位后时间将重置为默认值,造成严重后果。

本文提出一种基于备份域寄存器的“无感恢复”方案:当检测到 LSE 停振时,自动切换至内部 LSI(约 32kHz)继续计数,同时将当前时间与补偿偏移量保存至备份寄存器;待 LSE 恢复后,利用保存的参数重新校准时间,实现系统无感知的时间连续性。

原理剖析

1. RTC 时钟源与备份域

STM32F4 的 RTC 支持三种时钟源:LSE、LSI、HSE(分频)。其中,LSE 和 LSI 均位于备份域(VBAT 供电区域),即使主电源断电,只要 VBAT 有电,RTC 仍可运行。备份域包含 RTC 寄存器、备份寄存器(BKP)以及相关的控制逻辑。

  • LSE:外部低速晶振,频率 32.768kHz,精度高(典型 ±20ppm)。
  • LSI:内部低速 RC 振荡器,频率约 32kHz,精度低(典型 ±5%),但无需外部元件。

2. LSE 停振检测

STM32F4 的 RTC 提供时钟安全系统(CSS)功能,专门用于监测 LSE。当 LSE 停振时,CSS 会产生一个中断(RTC_CSS_IRQn),通知软件立即处理。在中断服务函数中,我们可以读取 RTC->ISR 的 CSSF 标志位确认事件。

3. 无感恢复策略

核心思想:

  • 停振期间:切换到 LSI 作为 RTC 时钟源,保证计数不中断。
  • 时间补偿:由于 LSI 频率偏差大,在切换时记录当前时间戳(基于 LSE),并计算一个补偿系数(例如,每秒钟 LSI 计数与理想 32768 的差值)。
  • 恢复后校准:当 LSE 恢复(通过定时检测或外部事件),利用备份寄存器中的时间戳和补偿系数,重新计算实际时间,并切换回 LSE。

配置步骤

1. 使能备份域访问

// 使能 PWR 和 RCC 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_BKPSRAM, ENABLE); // 若使用备份SRAM

// 使能备份域写访问
PWR_BackupAccessCmd(ENABLE);

2. 配置 RTC 时钟源与中断

// 选择 LSE 作为 RTC 时钟源
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待就绪

RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);

// 使能 CSS 中断
RTC_CSSCmd(ENABLE);
RTC_ITConfig(RTC_IT_CSS, ENABLE);

// 配置 NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = RTC_CSS_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

3. 初始化 RTC 时间

RTC_InitTypeDef RTC_InitStructure;
RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24;
RTC_InitStructure.RTC_AsynchPrediv = 0x7F;  // 异步分频 127
RTC_InitStructure.RTC_SynchPrediv = 0xFF;   // 同步分频 255
RTC_Init(&RTC_InitStructure);

// 设置时间(例如 2025-01-01 00:00:00)
RTC_TimeTypeDef time = {0};
time.RTC_Hours = 0;
time.RTC_Minutes = 0;
time.RTC_Seconds = 0;
RTC_SetTime(RTC_Format_BIN, &time);

4. 备份寄存器读写函数

// 写备份寄存器(32位)
void BKP_WriteReg(uint32_t reg, uint32_t value) {
    PWR_BackupAccessCmd(ENABLE);
    BKP_WriteBackupRegister(reg, value);
    PWR_BackupAccessCmd(DISABLE);
}

// 读备份寄存器
uint32_t BKP_ReadReg(uint32_t reg) {
    return BKP_ReadBackupRegister(reg);
}

完整代码示例

// 全局变量
volatile uint8_t lse_failure = 0;
volatile uint32_t lse_stop_tick = 0;  // 停振时的 RTC 计数(基于 LSE)
volatile uint32_t lsi_compensation = 0; // 补偿系数

// RTC CSS 中断服务函数
void RTC_CSS_IRQHandler(void) {
    if (RTC_GetITStatus(RTC_IT_CSS) != RESET) {
        RTC_ClearITPendingBit(RTC_IT_CSS);
        
        // 1. 记录当前时间(基于 LSE 的计数)
        lse_stop_tick = RTC_GetCounter();
        
        // 2. 切换到 LSI 时钟源
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
        
        // 3. 等待 LSI 就绪并重新初始化 RTC 分频(LSI 频率约 32kHz)
        while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
        RTC_InitTypeDef RTC_InitStructure;
        RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24;
        RTC_InitStructure.RTC_AsynchPrediv = 0x7F;
        RTC_InitStructure.RTC_SynchPrediv = 0xFF;
        RTC_Init(&RTC_InitStructure);
        
        // 4. 设置当前时间为停振时刻(保持连续)
        RTC_SetCounter(lse_stop_tick);
        
        // 5. 计算补偿系数:理想情况下 1 秒 = 32768 个 LSI 周期,但实际可能偏差
        //    这里假设 LSI 实际频率为 32000 Hz,则补偿系数 = 32768 - 32000 = 768
        //    实际应用中可通过测量 LSI 频率获得,此处示例固定值
        lsi_compensation = 768; // 每秒钟需要补偿的计数
        
        // 6. 保存到备份寄存器
        BKP_WriteReg(BKP_DR1, lse_stop_tick);
        BKP_WriteReg(BKP_DR2, lsi_compensation);
        
        lse_failure = 1;
    }
}

// 主循环中定期检查 LSE 是否恢复(例如每 10ms 检查一次)
void Check_LSE_Recovery(void) {
    if (lse_failure) {
        // 尝试重新使能 LSE
        RCC_LSEConfig(RCC_LSE_ON);
        uint32_t timeout = 0xFFFF;
        while (timeout-- && RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
        
        if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) {
            // LSE 恢复,进行时间补偿
            uint32_t current_lsi_tick = RTC_GetCounter();
            uint32_t elapsed_ticks = current_lsi_tick - lse_stop_tick;
            uint32_t compensated_ticks = elapsed_ticks + (elapsed_ticks * lsi_compensation) / 32768;
            
            // 切换回 LSE 时钟源
            RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
            while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
            
            // 重新初始化 RTC 分频(LSE 频率 32768)
            RTC_InitTypeDef RTC_InitStructure;
            RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24;
            RTC_InitStructure.RTC_AsynchPrediv = 0x7F;
            RTC_InitStructure.RTC_SynchPrediv = 0xFF;
            RTC_Init(&RTC_InitStructure);
            
            // 设置补偿后的时间
            RTC_SetCounter(lse_stop_tick + compensated_ticks);
            
            // 清除标志
            lse_failure = 0;
            BKP_WriteReg(BKP_DR1, 0); // 清除备份寄存器
            BKP_WriteReg(BKP_DR2, 0);
        }
    }
}

int main(void) {
    // 系统初始化...
    // 读取备份寄存器,判断是否有未完成的补偿
    uint32_t saved_tick = BKP_ReadReg(BKP_DR1);
    uint32_t saved_comp = BKP_ReadReg(BKP_DR2);
    if (saved_tick != 0 && saved_comp != 0) {
        // 上次异常掉电,恢复时进行补偿
        lse_stop_tick = saved_tick;
        lsi_compensation = saved_comp;
        lse_failure = 1;
        // 强制切换到 LSI 并继续,等待 LSE 恢复
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
        while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
        RTC_InitTypeDef RTC_InitStructure;
        RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24;
        RTC_InitStructure.RTC_AsynchPrediv = 0x7F;
        RTC_InitStructure.RTC_SynchPrediv = 0xFF;
        RTC_Init(&RTC_InitStructure);
        RTC_SetCounter(saved_tick);
    }
    
    while (1) {
        Check_LSE_Recovery();
        // 其他任务
    }
}

注意事项

  • 备份域电源:确保 VBAT 引脚连接到电池或大电容,否则备份域寄存器在掉电后丢失,无法实现恢复。
  • LSI 频率校准:补偿系数需根据实际 LSI 频率测量,可在系统启动时通过定时器测量 LSI 周期,动态计算,提高精度。
  • 中断优先级:CSS 中断应设置为高优先级,确保及时响应,避免时间误差积累。
  • 时钟切换时机:切换时钟源时,RTC 会短暂停止,需在切换前记录当前计数,切换后恢复,以保证连续性。
  • 备份寄存器数量:STM32F4 有 20 个 32 位备份寄存器,可存储更多状态,如时间戳、补偿系数、校验和等。
  • 测试建议:在实际硬件上模拟 LSE 停振(如断开晶振),验证恢复逻辑,并观察时间误差。

结语

通过上述方案,STM32F4 在 LSE 停振时能够无缝切换至 LSI,并利用备份域寄存器保存关键参数,实现时间补偿与无感恢复。该方法不仅提高了系统可靠性,也为其他类似时钟问题提供了参考思路。开发者可根据实际需求调整补偿算法,例如使用卡尔曼滤波或温度补偿,进一步提升精度。