引言

在工业控制、物联网设备等嵌入式场景中,RTC(实时时钟)通常依赖 LSE(32.768kHz 晶振)提供精准时基。然而,晶振老化、环境干扰或 PCB 布局不当可能导致 LSE 停振,此时 RTC 将退化为 LSI(约 32kHz,精度差),时间误差迅速累积。更棘手的是,若系统未检测到停振,RTC 时间会静默漂移,导致日志时间戳、定时任务错乱。本文以 STM32F4 系列为例,介绍如何利用 RTC 备份域寄存器实现停振检测、时间无痕恢复及补偿,确保系统时间连续可靠。

1. LSE 停振检测原理

1.1 硬件机制

STM32F4 的 RTC 时钟源可选 LSE、LSI 或 HSE 分频。当 LSE 停振时,RTC 不会自动切换时钟源,但硬件会通过 RTC_ISR 寄存器的 LSERDY 位(LSE 就绪标志)反映状态。此外,RTC_ISR 的 RTC_ISR[13:8] 保留位 中,RTC_ISR[13](LSRMF)可配置为 LSE 监测模式,用于检测 LSE 失效。更可靠的方式是使用 RTC 校准寄存器(RTC_CALR)CALPCALW8 位组合,配合 RTC_ISRCALF 标志,实现周期性校准检测。

1.2 软件检测策略

推荐采用 双保险检测

  • 轮询 LSERDY:周期性读取 RTC->ISR & RTC_ISR_LSERDY,若为 0 则判定 LSE 停振。
  • RTC 校准检测:利用 RTC 的 488ppm 校准窗口,通过比较 RTC 计数与外部参考(如系统定时器)的偏差,间接判断 LSE 是否失效。

2. 备份域寄存器的作用

RTC 备份域(Backup Domain)包含 20 个 32 位备份寄存器(BKPxR),位于 VBAT 供电区域,可在主电源断电时保持数据。利用这些寄存器,我们可以在 LSE 停振前保存关键时间信息,并在恢复后用于时间补偿。

核心思路:

  • 保存停振时刻:检测到 LSE 停振时,立即将当前 RTC 时间(基于 LSI 的近似值)写入备份寄存器。
  • 记录停振时长:利用备份寄存器存储一个单调递增的计数器(由 LSI 驱动),用于估算停振持续时间。
  • 恢复时补偿:当 LSE 恢复后,读取备份寄存器中的停振时刻和时长,计算出补偿值,并写入 RTC 时间寄存器。

3. 实现步骤

3.1 硬件配置

  • 确保 VBAT 引脚连接电池或超级电容,以维持备份域供电。
  • 使能 PWR 和 RTC 时钟,并允许访问备份域。
// 使能 PWR 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
// 允许访问备份域
PWR_BackupAccessCmd(ENABLE);
// 使能 RTC 时钟(若未使能)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_RTC, ENABLE);

3.2 初始化 RTC 与备份寄存器

// 初始化 RTC,选择 LSE 作为时钟源
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待 LSE 就绪
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();

// 配置 RTC 预分频(32768 / 128 = 256 Hz)
RTC_InitTypeDef RTC_InitStruct;
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
RTC_InitStruct.RTC_AsynchPrediv = 127;
RTC_InitStruct.RTC_SynchPrediv = 255;
RTC_Init(&RTC_InitStruct);

// 写入备份寄存器初始值(例如,0 表示无停振记录)
RTC_WriteBackupRegister(RTC_BKP_DR0, 0);
RTC_WriteBackupRegister(RTC_BKP_DR1, 0);

3.3 停振检测与记录

在系统主循环或定时中断中,周期性检查 LSE 状态。

void Check_LSE_Status(void) {
    if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) {
        // LSE 停振!
        // 1. 记录当前 RTC 时间(基于 LSI 的粗略值)到备份寄存器
        uint32_t current_time = RTC_GetCounter(); // 假设 RTC 计数器为秒
        RTC_WriteBackupRegister(RTC_BKP_DR0, current_time);
        
        // 2. 启动一个基于 LSI 的单调计数器(例如,使用定时器或 SysTick)
        // 这里简化:使用一个全局变量,由 LSI 驱动的定时器中断递增
        // 并在备份寄存器中存储计数器的初始值
        RTC_WriteBackupRegister(RTC_BKP_DR1, 0); // 清零停振时长计数
        
        // 3. 切换 RTC 时钟源到 LSI(可选,但建议保持 RTC 运行)
        // 注意:切换时钟源需谨慎,可参考 RM0090 的步骤
        // 此处省略,实际应用中可配置 RTC 时钟源切换
        
        lse_stopped = 1; // 设置标志位
    } else {
        if (lse_stopped) {
            // LSE 恢复!
            // 1. 读取停振时刻和停振时长
            uint32_t stop_time = RTC_ReadBackupRegister(RTC_BKP_DR0);
            uint32_t stop_duration = RTC_ReadBackupRegister(RTC_BKP_DR1);
            
            // 2. 计算补偿后的时间(假设停振期间 RTC 继续以 LSI 运行,但误差较大)
            // 这里需要根据实际 LSI 频率校准,假设 LSI 为 32kHz,误差约 5%
            uint32_t compensated_time = stop_time + (uint32_t)(stop_duration * 1.05); // 粗略补偿
            
            // 3. 设置 RTC 时间
            RTC_SetCounter(compensated_time);
            
            // 4. 清除备份寄存器记录
            RTC_WriteBackupRegister(RTC_BKP_DR0, 0);
            RTC_WriteBackupRegister(RTC_BKP_DR1, 0);
            
            lse_stopped = 0;
        }
    }
}

3.4 时间补偿细节

实际补偿需考虑 LSI 的精度。LSI 典型频率为 32kHz,但误差可达 ±5%。为提高补偿精度,可在系统正常时测量 LSI 的实际频率(通过比较 LSE 和 LSI 的计数),并存储校准系数。

// 校准 LSI:在 LSE 正常时,测量 1 秒内 LSI 的计数
void Calibrate_LSI(void) {
    // 启动 LSI 并等待稳定
    RCC_LSICmd(ENABLE);
    while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
    
    // 使用定时器测量 1 秒内 LSI 脉冲数(此处省略具体实现)
    // 假设测得 lsi_count_per_sec
    uint32_t lsi_count = 32000; // 示例值
    RTC_WriteBackupRegister(RTC_BKP_DR2, lsi_count); // 保存校准值
}

在补偿时,使用校准值计算实际停振时长:

uint32_t lsi_cal = RTC_ReadBackupRegister(RTC_BKP_DR2);
if (lsi_cal != 0) {
    // 假设停振时长计数器以 LSI 时钟递增,则实际秒数 = counter / lsi_cal
    uint32_t actual_seconds = stop_duration / lsi_cal;
    compensated_time = stop_time + actual_seconds;
} else {
    // 无校准值,使用默认估算
    compensated_time = stop_time + (uint32_t)(stop_duration / 32000);
}

4. 完整代码示例

以下是一个简化的完整示例,展示核心逻辑(省略具体外设初始化细节)。

// 全局变量
volatile uint8_t lse_stopped = 0;
volatile uint32_t lsi_counter = 0; // 由 LSI 驱动的定时器中断递增

// 定时器中断服务函数(假设使用 TIM2,时钟源为 LSI)
void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update)) {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        lsi_counter++;
        // 将计数器值写入备份寄存器,以便恢复时读取
        RTC_WriteBackupRegister(RTC_BKP_DR1, lsi_counter);
    }
}

// 主循环中的检测函数
void LSE_Monitor(void) {
    if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) {
        if (!lse_stopped) {
            // 首次检测到停振
            lse_stopped = 1;
            // 保存当前 RTC 时间(基于 LSI 的粗略值)
            uint32_t current_time = RTC_GetCounter();
            RTC_WriteBackupRegister(RTC_BKP_DR0, current_time);
            // 清零 LSI 计数器
            lsi_counter = 0;
            RTC_WriteBackupRegister(RTC_BKP_DR1, 0);
            // 启动 LSI 定时器(此处省略配置)
            // 注意:RTC 时钟源切换需谨慎,建议保持 RTC 运行在 LSI 上
        }
    } else {
        if (lse_stopped) {
            // LSE 恢复
            uint32_t stop_time = RTC_ReadBackupRegister(RTC_BKP_DR0);
            uint32_t stop_duration = lsi_counter; // 或从备份寄存器读取
            
            // 补偿计算(使用校准值)
            uint32_t lsi_cal = RTC_ReadBackupRegister(RTC_BKP_DR2);
            uint32_t actual_seconds;
            if (lsi_cal) {
                actual_seconds = stop_duration / lsi_cal;
            } else {
                actual_seconds = stop_duration / 32000; // 默认 32kHz
            }
            
            // 设置新时间
            RTC_SetCounter(stop_time + actual_seconds);
            
            // 清除标志和备份寄存器
            lse_stopped = 0;
            RTC_WriteBackupRegister(RTC_BKP_DR0, 0);
            RTC_WriteBackupRegister(RTC_BKP_DR1, 0);
        }
    }
}

int main(void) {
    // 初始化系统时钟、GPIO、RTC、备份寄存器等(省略)
    // 启动 LSI 定时器(配置 TIM2 时钟源为 LSI,并开启中断)
    
    while (1) {
        LSE_Monitor();
        // 其他任务
    }
}

5. 注意事项

  • 备份域访问权限:在写入备份寄存器前,必须使能 PWR 时钟并调用 PWR_BackupAccessCmd(ENABLE),否则写操作无效。
  • RTC 时钟源切换:当 LSE 停振时,RTC 不会自动切换到 LSI,需要软件干预。切换时钟源需遵循参考手册的步骤,否则可能导致 RTC 配置丢失。建议在检测到停振后,先停止 RTC 计数,切换时钟源,再重新启动。
  • LSI 校准:LSI 精度较差,建议在系统正常时定期校准,并将校准值存入备份寄存器。
  • 备份寄存器数量:STM32F4 有 20 个备份寄存器,可根据需求分配,但注意 VBAT 供电有限,避免频繁写入。
  • 时间戳连续性:在停振期间,RTC 基于 LSI 运行,时间误差较大,但通过补偿可保证恢复后的时间连续性,避免系统时间跳变。
  • 测试验证:实际应用中,可人为断开 LSE 模拟停振,验证恢复逻辑和补偿精度。

结语

通过 RTC 备份域寄存器,我们可以在 LSE 停振时保存关键时间信息,并在恢复后实现无痕补偿。该方法不仅提高了系统的鲁棒性,还避免了因时间错误导致的连锁故障。开发者可根据实际需求扩展,例如增加看门狗或报警机制,进一步提升可靠性。希望本文能为你的嵌入式项目提供有价值的参考。