STM32F4 系列 LSE 停振后的无痕复位恢复:RTC 备份寄存器的妙用

引言

在嵌入式系统中,RTC(实时时钟)常依赖 LSE(32.768kHz 晶振)提供精准时基。然而,LSE 可能因晶振老化、温度漂移或 PCB 布局问题而停振。一旦停振,RTC 时间会停止,若系统复位,默认的 RTC 配置可能丢失,导致时间错乱。本文介绍一种利用 RTC 备份寄存器(BKP)在复位后检测停振状态并恢复时间的方法,实现“无痕”复位,即用户无感知,系统时间连续。

原理分析

LSE 停振检测机制

STM32F4 的 RTC 模块提供时钟安全系统(CSS)功能,可监测 LSE 振荡。当检测到 LSE 故障时,CSS 会触发中断(RTC_CSS_IRQn),并将 RTC 时钟源自动切换到 LSI(内部低速 RC)。但默认情况下,CSS 中断未启用,且复位后 RTC 配置可能被重置。

RTC 备份寄存器的作用

RTC 备份寄存器(BKP)是 20 个 32 位寄存器,位于备份域,由 VBAT 供电,在系统复位或电源掉电时保持数据。我们可以利用 BKP 存储关键标志和时间戳,实现状态持久化。

无痕恢复策略

  1. 检测停振:在正常运行中,通过 CSS 中断或轮询 RTC_ISR 的 LSECSSD 标志,检测 LSE 停振。
  2. 保存状态:一旦检测到停振,立即将当前时间(从 RTC 读取)和停振标志写入 BKP。
  3. 复位处理:在系统复位后,检查 BKP 中的停振标志。若存在,则说明之前发生过停振,此时可从 BKP 恢复时间,并重新初始化 RTC,同时清除标志。若不存在,则正常初始化。

此方法的关键在于 BKP 在复位后仍保留数据,且 RTC 备份域寄存器不受系统复位影响。

配置步骤

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

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

void BKP_Config(void) {
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_RCC_BKP_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess(); // 允许访问备份域
}

2. 配置 RTC 和 LSE

初始化 RTC,启用 LSE,并开启 CSS 中断(可选)。

void RTC_Init(void) {
    RTC_HandleTypeDef hrtc;
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 0x7F;
    hrtc.Init.SynchPrediv = 0xFF;
    hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
    hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
    hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
    HAL_RTC_Init(&hrtc);

    // 启用 LSE 和 CSS
    __HAL_RCC_LSE_CONFIG(RCC_LSE_ON);
    while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {} // 等待就绪

    // 启用 CSS 中断(可选)
    HAL_RTCEx_EnableLSECSS(&hrtc);
    HAL_NVIC_EnableIRQ(RTC_CSS_IRQn);
}

3. 检测停振并保存状态

在 CSS 中断服务函数中,读取当前时间并写入 BKP。

void RTC_CSS_IRQHandler(void) {
    if (__HAL_RTC_CSS_GET_FLAG(&hrtc, RTC_FLAG_LSECSSD)) {
        __HAL_RTC_CSS_CLEAR_FLAG(&hrtc, RTC_FLAG_LSECSSD);
        
        // 读取当前时间
        RTC_TimeTypeDef sTime;
        RTC_DateTypeDef sDate;
        HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
        
        // 将时间打包为 32 位(例如:年-月-日-时-分-秒)
        uint32_t timestamp = ((uint32_t)sDate.Year << 24) | ((uint32_t)sDate.Month << 20) |
                             ((uint32_t)sDate.Date << 15) | ((uint32_t)sTime.Hours << 10) |
                             ((uint32_t)sTime.Minutes << 4) | (sTime.Seconds / 2);
        
        // 写入 BKP 寄存器(例如 BKP_DR1 存时间,BKP_DR2 存标志)
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, timestamp);
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0xA5A5); // 停振标志
        
        // 可在此切换时钟源或做其他处理
    }
}

4. 复位后的恢复逻辑

在系统初始化时,检查 BKP 标志,若存在则恢复时间。

void RTC_RestoreAfterReset(void) {
    RTC_TimeTypeDef sTime;
    RTC_DateTypeDef sDate;
    
    uint32_t flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
    if (flag == 0xA5A5) {
        // 读取保存的时间戳
        uint32_t timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
        
        // 解析时间
        sDate.Year = (timestamp >> 24) & 0xFF;
        sDate.Month = (timestamp >> 20) & 0x0F;
        sDate.Date = (timestamp >> 15) & 0x1F;
        sTime.Hours = (timestamp >> 10) & 0x1F;
        sTime.Minutes = (timestamp >> 4) & 0x3F;
        sTime.Seconds = (timestamp & 0x0F) * 2;
        
        // 重新初始化 RTC(假设 LSE 已恢复或使用 LSI)
        RTC_Init(); // 重新配置 RTC
        HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
        
        // 清除标志
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0x0000);
        
        // 可选:记录恢复事件
        printf("RTC restored from backup\n");
    } else {
        // 正常初始化
        RTC_Init();
    }
}

完整代码示例

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

#include "stm32f4xx_hal.h"

RTC_HandleTypeDef hrtc;

void SystemClock_Config(void);
void BKP_Config(void);
void RTC_Init(void);
void RTC_RestoreAfterReset(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    BKP_Config();
    
    RTC_RestoreAfterReset(); // 检查并恢复
    
    while (1) {
        // 主循环
    }
}

void RTC_CSS_IRQHandler(void) {
    if (__HAL_RTC_CSS_GET_FLAG(&hrtc, RTC_FLAG_LSECSSD)) {
        __HAL_RTC_CSS_CLEAR_FLAG(&hrtc, RTC_FLAG_LSECSSD);
        
        RTC_TimeTypeDef sTime;
        RTC_DateTypeDef sDate;
        HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
        
        uint32_t timestamp = ((uint32_t)sDate.Year << 24) | ((uint32_t)sDate.Month << 20) |
                             ((uint32_t)sDate.Date << 15) | ((uint32_t)sTime.Hours << 10) |
                             ((uint32_t)sTime.Minutes << 4) | (sTime.Seconds / 2);
        
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, timestamp);
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0xA5A5);
    }
}

void BKP_Config(void) {
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_RCC_BKP_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();
}

void RTC_Init(void) {
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 0x7F;
    hrtc.Init.SynchPrediv = 0xFF;
    hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
    hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
    hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
    HAL_RTC_Init(&hrtc);

    __HAL_RCC_LSE_CONFIG(RCC_LSE_ON);
    while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {}

    HAL_RTCEx_EnableLSECSS(&hrtc);
    HAL_NVIC_EnableIRQ(RTC_CSS_IRQn);
}

void RTC_RestoreAfterReset(void) {
    uint32_t flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
    if (flag == 0xA5A5) {
        uint32_t timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
        
        RTC_TimeTypeDef sTime;
        RTC_DateTypeDef sDate;
        sDate.Year = (timestamp >> 24) & 0xFF;
        sDate.Month = (timestamp >> 20) & 0x0F;
        sDate.Date = (timestamp >> 15) & 0x1F;
        sTime.Hours = (timestamp >> 10) & 0x1F;
        sTime.Minutes = (timestamp >> 4) & 0x3F;
        sTime.Seconds = (timestamp & 0x0F) * 2;
        
        RTC_Init();
        HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
        
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0x0000);
    } else {
        RTC_Init();
    }
}

void SystemClock_Config(void) {
    // 标准时钟配置,省略
}

注意事项

  • 备份域电源:确保 VBAT 引脚连接到电池或超级电容,否则 BKP 数据在掉电后会丢失。
  • 时间戳精度:示例中秒数除以 2 是为了压缩到 4 位,实际可根据需求调整,但需保证精度。
  • CSS 中断优先级:建议设置为高优先级,确保及时响应。
  • LSE 恢复:若 LSE 停振后恢复,需重新初始化 RTC 并启用 CSS,否则可能再次停振。
  • 多次停振:若在恢复前再次停振,BKP 中的时间可能不是最新,可考虑增加时间戳更新频率。
  • 测试:在实际硬件上模拟 LSE 停振(如断开晶振),验证恢复逻辑。

结语

通过 RTC 备份寄存器,我们可以在 LSE 停振后保存关键时间信息,并在复位后无痕恢复,大大提高了系统的可靠性。此方法不仅适用于 STM32F4,也适用于其他具有备份寄存器的 STM32 系列。希望本文能帮助你在嵌入式开发中应对类似挑战。