STM32F4 系列 LSE 停振后的无痕复位恢复:RTC 备份寄存器的妙用
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列开发中,LSE(低速外部晶振)停振是常见故障,尤其在恶劣环境或晶振匹配不佳时。停振会导致 RTC 时间丢失,甚至系统复位后无法恢复正确时间。本文深入探讨如何利用 RTC 备份寄存器在复位后快速检测 LSE 停振状态,并实现无痕恢复,确保系统时间连续性和数据完整性。通过原理分析、配置步骤和完整代码示例,帮助开发者构建高可靠的嵌入式时钟系统。
# 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 时钟,并允许访问备份域。
```c
void BKP_Config(void) {
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 允许访问备份域
}
```
### 2. 配置 RTC 和 LSE
初始化 RTC,启用 LSE,并开启 CSS 中断(可选)。
```c
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。
```c
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 标志,若存在则恢复时间。
```c
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();
}
}
```
## 完整代码示例
以下是一个整合的示例,包含初始化、中断处理和恢复逻辑。
```c
#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 系列。希望本文能帮助你在嵌入式开发中应对类似挑战。