一、问题背景:LSE 停振为何是“隐形杀手”
STM32F4 的 RTC 通常依赖外部低速晶振(LSE,32.768kHz)提供高精度时钟源。但 LSE 对 PCB 布局、环境温度敏感,一旦停振,RTC 将失去计数基准。若未做处理,系统复位后 RTC 时间会回退至默认值,导致数据记录、通信协议时间戳错乱。
核心难点:
- LSE 停振是渐进过程,软件难以实时捕捉。
- 备份域(VBAT)供电时,RTC 寄存器内容保留,但时钟源失效后计数停止。
- 常规方案(如检测 RTC_ISR 的 LSERDY 位)存在响应延迟,且复位后无法区分“正常上电”与“异常恢复”。
二、无感恢复原理:备份域寄存器 + 双时钟源切换
2.1 备份域寄存器(BKP)的角色
STM32F4 的备份域包含 20 个 32 位数据寄存器(BKP_DR0~DR19),由 VBAT 供电,复位不影响其内容。我们利用这些寄存器实现:
- 状态标志:记录 LSE 是否曾停振。
- 时间快照:在停振瞬间保存 RTC 计数器的值(以 LSI 为基准)。
2.2 无感恢复流程
- 正常运行:RTC 使用 LSE,BKP_DR0 写入魔数(如 0xA5A5),BKP_DR1 存储当前时间戳。
- LSE 停振检测:通过 RTC_ISR 的 LSERDY 位或 LSE 旁路监测,触发中断。
- 紧急切换:在中断服务函数中,将 RTC 时钟源切换至 LSI(约 32kHz),并立即读取 RTC 计数器值存入 BKP_DR1。
- 系统复位:若发生复位,启动时检查 BKP_DR0 魔数。若存在且 RTC 时钟源为 LSI,则说明发生过停振,用 BKP_DR1 恢复时间,并重新校准 LSE。
关键点:切换时钟源时,RTC 计数器会清零,因此必须先保存旧值。
三、寄存器级配置步骤
3.1 使能备份域访问
// 使能 PWR 和 BKP 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
// 允许备份域写入
PWR_BackupAccessCmd(ENABLE);
3.2 初始化 RTC 并设置时钟源
void RTC_Init(void) {
// 选择 LSE 作为 RTC 时钟
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待就绪
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
// 配置 RTC 参数(24小时制,1秒中断等)
RTC_InitTypeDef rtcInit;
rtcInit.RTC_HourFormat = RTC_HourFormat_24;
rtcInit.RTC_AsynchPrediv = 0x7F;
rtcInit.RTC_SynchPrediv = 0xFF;
RTC_Init(&rtcInit);
// 写入魔数和时间快照
BKP_WriteBackupRegister(BKP_DR0, 0xA5A5);
BKP_WriteBackupRegister(BKP_DR1, RTC_GetCounter());
}
3.3 LSE 停振检测与切换
void LSE_Stop_Handler(void) {
// 检查 LSE 是否就绪
if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) {
// 保存当前时间(基于 LSE 的计数)
uint32_t timeBefore = RTC_GetCounter();
BKP_WriteBackupRegister(BKP_DR1, timeBefore);
// 切换至 LSI
RCC_LSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
RTC_WaitForSynchro();
// 重新配置预分频(LSI 频率约 32kHz,需调整)
RTC_InitTypeDef rtcInit;
rtcInit.RTC_HourFormat = RTC_HourFormat_24;
rtcInit.RTC_AsynchPrediv = 0x7F; // 根据实际 LSI 频率微调
rtcInit.RTC_SynchPrediv = 0xFF;
RTC_Init(&rtcInit);
// 恢复时间(从 BKP_DR1 读取,但注意切换后计数器已清零,需重新设置)
RTC_SetCounter(BKP_ReadBackupRegister(BKP_DR1));
// 设置标志位
BKP_WriteBackupRegister(BKP_DR2, 0x5A5A); // 表示已切换
}
}
3.4 复位后的恢复逻辑
void RTC_Recovery(void) {
if (BKP_ReadBackupRegister(BKP_DR0) == 0xA5A5) {
// 检查是否发生过切换
if (BKP_ReadBackupRegister(BKP_DR2) == 0x5A5A) {
// 从备份寄存器恢复时间
uint32_t savedTime = BKP_ReadBackupRegister(BKP_DR1);
// 当前时钟源可能是 LSI,先尝试恢复 LSE
RCC_LSEConfig(RCC_LSE_ON);
uint32_t wait = 0;
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET && wait < 1000) wait++;
if (wait < 1000) {
// LSE 恢复,切换回 LSE
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RTC_WaitForSynchro();
// 重新配置预分频
RTC_InitTypeDef rtcInit;
rtcInit.RTC_HourFormat = RTC_HourFormat_24;
rtcInit.RTC_AsynchPrediv = 0x7F;
rtcInit.RTC_SynchPrediv = 0xFF;
RTC_Init(&rtcInit);
// 设置时间(注意:切换时钟源会清零计数器,需重新设置)
RTC_SetCounter(savedTime);
// 清除切换标志
BKP_WriteBackupRegister(BKP_DR2, 0x0000);
} else {
// LSE 仍不可用,保持 LSI,但时间已恢复
RTC_SetCounter(savedTime);
}
}
}
}
四、完整代码示例(主函数集成)
int main(void) {
// 初始化时钟、GPIO 等
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
// 检查备份域标志,执行恢复
RTC_Recovery();
// 正常初始化 RTC(如果未初始化过)
if (BKP_ReadBackupRegister(BKP_DR0) != 0xA5A5) {
RTC_Init();
}
// 使能 LSE 停振检测中断(通过 EXTI 或轮询)
// 此处以轮询为例
while (1) {
if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) {
LSE_Stop_Handler();
}
// 其他任务
}
}
五、注意事项与工程实践
-
备份域写入保护:必须调用
PWR_BackupAccessCmd(ENABLE)才能写 BKP 寄存器,否则写操作无效。 - 时钟切换清零:RTC 时钟源切换会导致计数器清零,务必先保存旧值,切换后重新设置。
- LSI 精度:LSI 频率误差较大(±5%),若需高精度,可在恢复后定期校准或使用外部高精度时钟源。
- 中断优先级:LSE 停振检测中断应设为高优先级,避免被其他中断延迟。
- 魔数校验:建议使用 16 位魔数,并考虑 CRC 校验,防止数据损坏。
- 低功耗模式:在待机模式下,备份域仍供电,但 RTC 时钟源需保持有效,否则时间会停止。
- 调试技巧:通过串口打印 BKP 寄存器值,验证恢复逻辑是否按预期执行。
六、总结
本文提出的方案利用 STM32F4 备份域寄存器的非易失特性,实现了 LSE 停振后的时间无感恢复。核心在于:
- 停振时快速保存时间戳并切换至 LSI。
- 复位后通过魔数识别异常状态,恢复时间并尝试重新启用 LSE。
该策略适用于对时间连续性要求高的嵌入式系统,如智能电表、工业控制器。开发者可根据实际需求调整预分频值,并增加看门狗或错误日志功能,进一步提升鲁棒性。