STM32F4 在 LSE 停振后的无感恢复:RTC 备份寄存器与时钟源无缝切换实战
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列中,LSE(低速外部晶振)停振会导致 RTC 走时失效,甚至影响系统低功耗模式。本文深入剖析 LSE 停振检测机制,并演示如何利用 RTC 备份寄存器保存关键上下文,在检测到故障后无缝切换至 LSI 时钟源,实现系统无感恢复。通过原理讲解、寄存器配置、完整代码示例及注意事项,帮助开发者构建高可靠的 RTC 应用。
# 引言
在嵌入式系统中,RTC(实时时钟)常依赖 LSE(32.768kHz 晶振)提供精准时基。然而,晶振可能因环境干扰、老化或焊接问题而停振。STM32F4 系列提供了 RTC 时钟源监控和备份寄存器,但默认固件库并未完整实现故障恢复流程。本文以 STM32F407 为例,讲解如何通过 RTC 备份寄存器保存系统状态,在 LSE 停振时自动切换至 LSI(内部低速 RC),并恢复时间计数,实现无感恢复。
# 原理剖析
## 1. LSE 停振检测机制
STM32F4 的 RTC 模块包含一个时钟源标志位 `RTC_ISR.RSF`(寄存器同步标志)和 `RTC_ISR.INITS`(初始化状态标志)。当 LSE 停振时,RTC 时钟域会失去时钟,导致寄存器无法更新。更直接的方法是使用 `RTC->ISR` 的 `LSRMF`(LSE 单调检测标志)或通过 `RTC_CR` 的 `LSCOEN` 使能 LSE 时钟输出,配合外部检测。但最可靠的方式是启用 RTC 的 `RTC_TAFCR` 寄存器中的 `TAMPFLT` 和 `TAMPxTRG` 边沿检测,或使用 `RTC_ISR` 的 `ALRAF`(闹钟标志)作为辅助。
实际工程中,常用方案是:
- 使能 RTC 闹钟中断,设置一个短周期(如 1 秒)闹钟,若闹钟未触发,则判定 LSE 停振。
- 或者利用 LSE 的旁路监测功能,但 STM32F4 没有专用停振中断,需轮询。
## 2. RTC 备份寄存器的作用
RTC 备份寄存器(BKPxR)在 VDD 供电时由 VBAT 供电,可保存用户数据。在 LSE 停振时,RTC 时间寄存器可能因时钟丢失而无法读取,但备份寄存器不受影响。因此,我们可以将当前时间戳、时钟源状态、校准值等关键信息存入备份寄存器,在恢复时读取并重建时间。
## 3. 时钟源无缝切换策略
STM32F4 的 RTC 时钟源可选 LSE、LSI 或 HSE 分频。当检测到 LSE 停振,应:
1. 保存当前时间到备份寄存器(若可能)。
2. 关闭 RTC 时钟源选择,切换至 LSI。
3. 重新初始化 RTC 并恢复时间(从备份寄存器读取)。
4. 更新系统状态标志,通知应用层。
由于 LSI 精度较低(约 ±5%),需在恢复后定期校准或提示用户。
# 硬件与软件准备
- 硬件:STM32F407 开发板,外部 32.768kHz 晶振,备用电池连接 VBAT。
- 软件:STM32CubeIDE 或 Keil,HAL 库或标准外设库均可。本文使用 HAL 库。
# 配置步骤
## 1. 初始化 RTC 与备份寄存器
首先,使能 PWR 和 RTC 时钟,配置 RTC 时钟源为 LSE,并使能备份域访问。
```c
void RTC_Init(void) {
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_RTC_ENABLE();
HAL_PWR_EnableBkUpAccess();
RCC_OscInitTypeDef oscInit = {0};
oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSE;
oscInit.LSEState = RCC_LSE_ON;
if (HAL_RCC_OscConfig(&oscInit) != HAL_OK) {
// 若 LSE 启动失败,立即切换 LSI
SwitchToLSI();
return;
}
RCC_PeriphCLKInitTypeDef periphClkInit = {0};
periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
HAL_RCCEx_PeriphCLKConfig(&periphClkInit);
// 使能 RTC
__HAL_RCC_RTC_ENABLE();
RTC_HandleTypeDef hrtc = {0};
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
HAL_RTC_Init(&hrtc);
// 写入初始时间(示例)
RTC_TimeTypeDef sTime = {0};
sTime.Hours = 12;
sTime.Minutes = 0;
sTime.Seconds = 0;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
RTC_DateTypeDef sDate = {0};
sDate.Year = 24;
sDate.Month = 1;
sDate.Date = 1;
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
}
```
## 2. 保存上下文到备份寄存器
在正常运行时,周期性将当前时间戳和状态存入备份寄存器。
```c
void SaveContextToBackup(void) {
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
// 将时间转换为 Unix 时间戳(简化示例,实际需考虑闰年等)
uint32_t timestamp = 0;
// ... 转换代码 ...
HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR0, timestamp);
HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR1, 0xA5A5); // 标记有效
}
```
## 3. 检测 LSE 停振并切换时钟源
使用轮询方式检测 LSE 是否停振。例如,每秒检查 RTC 是否还在走时(通过比较秒寄存器),或使用闹钟中断。以下示例使用闹钟中断:
```c
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) {
// 闹钟触发,说明 RTC 正常,更新备份寄存器
SaveContextToBackup();
// 重新设置下一次闹钟
SetAlarm(1); // 1 秒后
}
void CheckLSEAndSwitch(void) {
// 若闹钟未触发(通过标志位),则判定 LSE 停振
if (alarmMissed) {
// 保存最后已知时间
SaveContextToBackup();
// 切换至 LSI
SwitchToLSI();
}
}
void SwitchToLSI(void) {
// 1. 关闭 RTC 时钟源
__HAL_RCC_RTC_DISABLE();
HAL_PWR_DisableBkUpAccess();
// 2. 使能 LSI 并配置为 RTC 时钟
RCC_OscInitTypeDef oscInit = {0};
oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSI;
oscInit.LSIState = RCC_LSI_ON;
HAL_RCC_OscConfig(&oscInit);
RCC_PeriphCLKInitTypeDef periphClkInit = {0};
periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
HAL_RCCEx_PeriphCLKConfig(&periphClkInit);
// 3. 重新初始化 RTC
__HAL_RCC_RTC_ENABLE();
RTC_HandleTypeDef hrtc = {0};
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127; // 需根据 LSI 频率调整(LSI 约 32kHz)
hrtc.Init.SynchPrediv = 255;
HAL_RTC_Init(&hrtc);
// 4. 从备份寄存器恢复时间
uint32_t timestamp = HAL_RTCEx_BKUPRead(RTC, RTC_BKP_DR0);
if (timestamp != 0xFFFFFFFF) {
// 将时间戳转换回 RTC 时间并设置
// ...
}
// 5. 设置状态标志
HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR1, 0x5A5A); // 表示已切换
}
```
## 4. 完整代码示例(主循环)
```c
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
RTC_Init();
// 使能闹钟中断
SetAlarm(1);
HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
while (1) {
// 每 100ms 检查一次闹钟是否超时
if (alarmMissed) {
CheckLSEAndSwitch();
}
// 其他任务
}
}
void SetAlarm(uint32_t seconds) {
RTC_AlarmTypeDef sAlarm = {0};
sAlarm.AlarmTime.Hours = 0;
sAlarm.AlarmTime.Minutes = 0;
sAlarm.AlarmTime.Seconds = seconds;
sAlarm.AlarmMask = RTC_ALARMMASK_DATE_WEEKDAY;
HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
}
```
# 注意事项
- **备份寄存器有效性**:在读取备份寄存器前,务必检查标记值(如 0xA5A5),避免使用未初始化的数据。
- **LSI 精度问题**:LSI 频率受温度影响大,切换后 RTC 走时误差可能较大。建议在应用层增加校准机制,或定期与外部时间源同步。
- **预分频器调整**:LSE 为 32.768kHz,LSI 约为 32kHz,但实际频率可能偏差。需根据实际 LSI 频率重新计算 AsynchPrediv 和 SynchPrediv,否则 RTC 时间不准确。
- **电源管理**:在切换时钟源时,务必先关闭 RTC 时钟,再修改时钟源,避免产生毛刺。
- **中断优先级**:闹钟中断应设为较高优先级,确保及时响应。
- **测试方法**:实际测试时,可断开 LSE 晶振引脚模拟停振,观察系统是否自动切换。
# 总结
通过 RTC 备份寄存器保存上下文,结合闹钟中断检测 LSE 停振,并在检测到故障后无缝切换至 LSI,可以显著提高 STM32F4 系统的可靠性。该方法无需外部硬件,仅依赖片内资源,适用于对时间连续性要求较高的工业设备、智能仪表等场景。开发者可根据实际需求扩展,如增加多次重试、故障日志记录等功能。