引言

在工业控制、物联网设备等嵌入式应用中,RTC(实时时钟)是记录时间戳的关键外设。STM32F4 系列通常使用 32.768kHz 的 LSE 晶振作为 RTC 的低速时钟源,但 LSE 可能因温度漂移、焊接不良或电磁干扰而停振。一旦 LSE 停止,RTC 将无法计时,系统时间归零或停滞,造成严重后果。本文提出一种基于 RTC 备份寄存器的纠偏策略:在 LSE 停振时,利用备份寄存器保存关键时间信息,并在恢复后自动校正日历,实现无外部晶振的可靠运行。

原理剖析

1. RTC 与备份寄存器架构

STM32F4 的 RTC 模块包含一个 20 位的预分频器、32 位可编程计数器(用于秒计数)以及一组 20 字节的备份寄存器(BKP Registers)。这些寄存器位于 VDD 供电域,当主电源掉电时,可由 VBAT 引脚供电,保持数据不丢失。备份寄存器的主要用途是存储系统配置或关键数据,例如 RTC 校准值、唤醒标志等。

2. LSE 停振检测机制

RTC 时钟源可选 LSE、LSI 或 HSE 分频。LSE 停振时,RTC 的时钟输入丢失,但 RTC 本身不会自动切换时钟源。通过检测 RTC_ISR 寄存器的 LSERDY 位(LSE 就绪标志)可以判断 LSE 状态。当 LSERDY=0 时,表示 LSE 未就绪或停振。此时,RTC 计数器停止更新,但备份寄存器仍可读写。

3. 纠偏策略设计

核心思路:

  • 正常运行时,定期将当前时间(如秒计数)和日期信息写入备份寄存器,作为“时间快照”。
  • 检测到 LSE 停振时,立即停止依赖 RTC 的时间更新,并记录停振时刻的备份值。
  • 在 LSE 恢复后,读取备份寄存器中的时间快照,结合停振持续时间(通过系统定时器或外部事件估算),重新初始化 RTC 计数器,实现日历纠偏。

配置步骤

1. 使能 RTC 与备份寄存器时钟

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

// 使能 PWR 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
// 允许访问备份域
PWR_BackupAccessCmd(ENABLE);
// 使能 RTC 时钟(这里使用 LSE,但需先使能 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 同步

2. 初始化 RTC 并设置时间

配置 RTC 预分频器(LSE 为 32768Hz,分频系数为 32767),并设置初始时间。

RTC_InitTypeDef RTC_InitStruct;
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
RTC_InitStruct.RTC_AsynchPrediv = 0x7F;   // 异步分频 128
RTC_InitStruct.RTC_SynchPrediv = 0xFF;    // 同步分频 256,实际分频为 32768/(128*256)=1Hz
RTC_Init(&RTC_InitStruct);

// 设置时间:2024年1月1日 12:00:00
RTC_TimeTypeDef time = {0};
time.RTC_Hours = 12;
time.RTC_Minutes = 0;
time.RTC_Seconds = 0;
RTC_SetTime(RTC_Format_BIN, &time);

RTC_DateTypeDef date = {0};
date.RTC_Year = 24; // 2024年
date.RTC_Month = 1;
date.RTC_Date = 1;
date.RTC_WeekDay = 1;
RTC_SetDate(RTC_Format_BIN, &date);

3. 定期保存时间快照到备份寄存器

在应用主循环或定时器中,每隔一段时间(如 1 秒)将当前时间写入备份寄存器。

void SaveTimeSnapshot(void) {
    RTC_TimeTypeDef time;
    RTC_DateTypeDef date;
    RTC_GetTime(RTC_Format_BIN, &time);
    RTC_GetDate(RTC_Format_BIN, &date);
    
    // 将时间信息打包存储(示例:使用 4 个备份寄存器)
    uint32_t snapshot = (time.RTC_Hours << 16) | (time.RTC_Minutes << 8) | time.RTC_Seconds;
    RTC_WriteBackupRegister(RTC_BKP_DR0, snapshot);
    snapshot = (date.RTC_Year << 16) | (date.RTC_Month << 8) | date.RTC_Date;
    RTC_WriteBackupRegister(RTC_BKP_DR1, snapshot);
    // 记录写入时的系统 tick(用于计算停振时长)
    RTC_WriteBackupRegister(RTC_BKP_DR2, (uint32_t)GetSystemTick());
}

4. 检测 LSE 停振并处理

在主循环中轮询 LSE 状态,一旦发现停振,立即停止时间更新,并记录当前系统 tick。

void CheckLSEStatus(void) {
    if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) {
        // LSE 停振
        lse_stopped = 1;
        stop_tick = GetSystemTick();
        // 可在此关闭 RTC 中断或相关任务
    } else {
        if (lse_stopped) {
            // LSE 恢复,执行纠偏
            CorrectRTC();
            lse_stopped = 0;
        }
    }
}

5. 纠偏函数实现

读取备份寄存器中的快照,计算停振时长(基于系统 tick 差值),然后重新设置 RTC 时间。

void CorrectRTC(void) {
    // 读取快照
    uint32_t time_snap = RTC_ReadBackupRegister(RTC_BKP_DR0);
    uint32_t date_snap = RTC_ReadBackupRegister(RTC_BKP_DR1);
    uint32_t saved_tick = RTC_ReadBackupRegister(RTC_BKP_DR2);
    
    uint32_t elapsed = GetSystemTick() - saved_tick; // 停振时长(假设 tick 为毫秒)
    
    // 解析时间
    RTC_TimeTypeDef time;
    time.RTC_Hours = (time_snap >> 16) & 0xFF;
    time.RTC_Minutes = (time_snap >> 8) & 0xFF;
    time.RTC_Seconds = time_snap & 0xFF;
    
    RTC_DateTypeDef date;
    date.RTC_Year = (date_snap >> 16) & 0xFF;
    date.RTC_Month = (date_snap >> 8) & 0xFF;
    date.RTC_Date = date_snap & 0xFF;
    
    // 将停振时长加到时间上(简化处理,需考虑进位)
    uint32_t total_seconds = time.RTC_Hours * 3600 + time.RTC_Minutes * 60 + time.RTC_Seconds;
    total_seconds += elapsed / 1000; // 假设 tick 为毫秒
    
    // 转换为时分秒
    time.RTC_Hours = total_seconds / 3600;
    time.RTC_Minutes = (total_seconds % 3600) / 60;
    time.RTC_Seconds = total_seconds % 60;
    
    // 设置新时间(日期进位可忽略或另行处理)
    RTC_SetTime(RTC_Format_BIN, &time);
    RTC_SetDate(RTC_Format_BIN, &date);
    
    // 重新保存快照
    SaveTimeSnapshot();
}

完整代码示例

以下是一个简化的综合示例,展示主循环中的调用逻辑。

// 全局变量
volatile uint8_t lse_stopped = 0;
uint32_t stop_tick = 0;

// 假设系统 tick 为 1ms
uint32_t GetSystemTick(void) {
    return HAL_GetTick(); // 使用 HAL 库
}

int main(void) {
    HAL_Init();
    // 初始化时钟、GPIO 等
    
    // 初始化 RTC(如上)
    RTC_Init();
    
    // 初始保存快照
    SaveTimeSnapshot();
    
    while (1) {
        CheckLSEStatus();
        
        // 正常运行时每秒保存一次快照
        if (!lse_stopped) {
            static uint32_t last_save = 0;
            if (GetSystemTick() - last_save >= 1000) {
                SaveTimeSnapshot();
                last_save = GetSystemTick();
            }
        }
        
        // 其他任务
    }
}

注意事项

  • 备份寄存器数量有限:STM32F4 有 20 个备份寄存器,每个 32 位,需合理规划存储内容。示例中使用了 3 个,可根据需要扩展。
  • 时间进位处理:纠偏时,如果停振时间较长,可能导致日期进位(如跨天、跨月),示例中未处理,实际应用需根据日期算法完善。
  • 系统 tick 的可靠性:停振期间,系统 tick 必须持续运行(如由 HSI 或 PLL 驱动),否则无法计算停振时长。若系统掉电,则备份寄存器仍可保存数据,但恢复后需人工干预。
  • LSE 恢复检测:LSE 恢复可能需要一定时间,建议在检测到 LSERDY 置位后,延时一段时间再执行纠偏,确保时钟稳定。
  • 备份域复位:如果备份域被复位(如 VBAT 掉电),备份寄存器内容丢失,需重新初始化 RTC。
  • 功耗考虑:备份寄存器在 VBAT 供电下会消耗少量电流,设计低功耗产品时需注意。

总结

通过 RTC 备份寄存器保存时间快照,并在 LSE 停振后利用系统 tick 计算停振时长,可以实现无外部晶振的日历纠偏。该方法简单有效,适用于对时间精度要求不苛刻但需保证连续性的场景。开发者可根据实际需求扩展,例如增加日期进位算法、使用外部中断检测等。希望本文能帮助你在嵌入式项目中构建更健壮的 RTC 系统。