引言

STM32F4 系列的 RTC 通常依赖外部低速晶振(LSE,32.768kHz)以保持低功耗高精度计时。然而,LSE 在恶劣环境(如高低温冲击、振动)或晶振老化下可能停振,导致 RTC 时间停滞,进而影响系统日志、通信协议或定时任务。传统方案是直接切换至内部低速 RC(LSI),但 LSI 精度差(±5%),且切换过程可能造成时间跳变。本文提出一种“无痕热切换”策略:利用备份域寄存器保存时间上下文,在 LSE 停振瞬间无缝切换至 LSI,并通过软件补偿维持时间精度,实现系统无感知的时钟接管。

原理剖析

1. 备份域结构

STM32F4 的备份域(Backup Domain)包含:

  • RTC 寄存器:实时时钟核心,支持 LSE 或 LSI 作为时钟源。
  • 备份寄存器(BKP):20 个 32 位寄存器(BKP0R~BKP19R),在 VBAT 供电下可保持数据。
  • 备份域控制寄存器:控制 RTC 使能、时钟源选择及复位。

关键点:备份域独立于主电源,由 VBAT 引脚供电。当 VDD 掉电时,备份域仍可工作,但 LSE 和 LSI 均可能失效(取决于供电设计)。

2. LSE 停振检测

RTC 提供时钟安全系统(CSS)功能,专用于 LSE 监测。当 LSE 停振时,CSS 会触发 RTC_CSS 中断(或事件),并自动将 RTC 时钟源切换至 LSI。但默认切换后,RTC 计数会基于 LSI 继续运行,但时间精度下降,且无补偿机制。

3. 热切换策略

我们的策略分为三步:

  1. 预存时间戳:在系统正常运行时,周期性将当前时间(如 Unix 时间戳)写入备份寄存器(例如 BKP0R)。
  2. 检测与切换:使能 CSS 中断,在中断服务函数中记录切换时刻,并立即读取 RTC 当前计数(基于 LSI)作为基准。
  3. 软件补偿:切换后,利用备份寄存器中的时间戳和切换时刻的 RTC 计数,通过软件定时器(如 SysTick)周期性校准时间,补偿 LSI 的频率偏差。

配置步骤

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

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

// 允许访问备份域
PWR_BackupAccessCmd(ENABLE);

// 复位备份域(可选,首次配置时)
RCC_BackupResetCmd(ENABLE);
RCC_BackupResetCmd(DISABLE);

2. 配置 LSE 并启用 CSS

RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待就绪

// 选择 LSE 作为 RTC 时钟
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);

// 使能 RTC 时钟
RCC_RTCCLKCmd(ENABLE);

// 等待 RTC 寄存器同步
RTC_WaitForSynchro();

// 使能 CSS 中断(在 NVIC 中配置)
RTC_CSSConfig(ENABLE);

3. 初始化 RTC 并设置时间

RTC_InitTypeDef RTC_InitStruct;
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
RTC_InitStruct.RTC_AsynchPrediv = 0x7F;  // 异步分频 128
RTC_InitStruct.RTC_SynchPrediv = 0xFF;   // 同步分频 256,得到 1Hz
RTC_Init(RTC_InitStruct);

// 设置时间(示例: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);

// 保存初始时间戳到备份寄存器
uint32_t timestamp = get_unix_timestamp(); // 自定义函数
BKP_WriteBackupRegister(BKP_DR1, timestamp);

4. 编写 CSS 中断服务函数

void RTC_IRQHandler(void) {
    if (RTC_GetITStatus(RTC_IT_CSS) != RESET) {
        RTC_ClearITPendingBit(RTC_IT_CSS);
        
        // 记录切换时刻的 RTC 计数(基于 LSI)
        uint32_t switch_sec = RTC_GetCounter(); // 若使用日历模式,则读取时间并转换
        
        // 保存切换标志和时间到备份寄存器
        BKP_WriteBackupRegister(BKP_DR2, switch_sec);
        BKP_WriteBackupRegister(BKP_DR3, 0x5A5A); // 标志
        
        // 可选:切换至 LSI 并重新配置分频器(CSS 已自动切换,但需确认)
        // 通常 CSS 已自动切换,无需手动操作
        
        // 启动软件补偿定时器(如 SysTick)
        SysTick_Config(SystemCoreClock / 1000); // 1ms 中断
    }
}

5. 软件补偿实现

在 SysTick 中断中,每 1 秒(或 10 秒)校准一次时间:

volatile uint32_t tick_count = 0;
volatile uint32_t last_sec = 0;

void SysTick_Handler(void) {
    tick_count++;
    if (tick_count >= 1000) { // 1 秒
        tick_count = 0;
        // 读取当前 RTC 计数(基于 LSI)
        uint32_t current_sec = RTC_GetCounter();
        // 计算偏差:假设 LSI 实际频率为 f_lsi,理想为 32kHz,但实际可能偏差
        // 通过备份寄存器中的基准时间戳和切换时刻,计算补偿值
        uint32_t base_ts = BKP_ReadBackupRegister(BKP_DR1);
        uint32_t switch_sec = BKP_ReadBackupRegister(BKP_DR2);
        uint32_t elapsed = current_sec - switch_sec; // 经过的秒数(基于LSI)
        uint32_t real_elapsed = (tick_count / 1000) + (base_ts - switch_sec); // 简化,实际需用系统时钟
        // 这里仅演示,实际应使用系统时钟(如 TIM)来测量真实时间
        
        // 若偏差超过阈值,则调整 RTC 计数(需谨慎)
        // 更稳健的方法:使用 RTC 同步分频器校准,但需要重新初始化
    }
}

实际工程中,建议使用一个硬件定时器(如 TIM2)作为高精度时间基准,在切换后持续校准 RTC。

完整代码示例

以下为关键部分整合(省略初始化细节):

// main.c
int main(void) {
    // 系统时钟配置(略)
    
    // 使能备份域访问
    PWR_BackupAccessCmd(ENABLE);
    
    // 检查备份寄存器标志,判断是否发生过切换
    if (BKP_ReadBackupRegister(BKP_DR3) == 0x5A5A) {
        // 已切换至 LSI,执行恢复流程
        // 读取备份时间戳,并设置 RTC 时间
        uint32_t saved_ts = BKP_ReadBackupRegister(BKP_DR1);
        // 转换为 RTC 时间并设置(略)
    } else {
        // 正常启动,配置 LSE 和 RTC
        // ...(见上文配置步骤)
    }
    
    // 使能 CSS 中断
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = RTC_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
    
    while (1) {
        // 主循环
    }
}

注意事项

  • 备份域供电:确保 VBAT 引脚连接到电池或超级电容,否则掉电后备份寄存器数据丢失,热切换失效。
  • CSS 中断优先级:应设置为高优先级,确保及时响应。
  • LSI 校准:LSI 频率随温度变化,建议定期(如每小时)通过外部高精度时钟(如 GPS 或网络)校准一次。
  • 时间戳保存频率:不要过于频繁写入备份寄存器(寿命有限),建议每分钟或每分钟一次。
  • 切换后的 RTC 配置:CSS 自动切换后,RTC 的分频器仍按 LSE 参数配置,但 LSI 频率不同,会导致时间快慢。因此,切换后需重新配置分频器(例如,LSI 典型 32kHz,但实际可能 30-34kHz,需测量)。
  • 软件补偿的复杂度:若对时间精度要求极高(如 <1ppm),建议使用外部高精度时钟源,或采用双 RTC 方案。

总结

通过备份域寄存器和 CSS 中断,STM32F4 可以在 LSE 停振时无缝切换至 LSI,并利用预存时间戳和软件补偿保持时间连续性。该方案成本低、实现简单,适用于大多数工业场景。开发者应根据实际需求调整补偿策略,并充分测试 LSI 频率特性。