引言
在嵌入式系统中,LSE(32.768kHz晶振)通常为RTC提供精准时钟源。然而,晶振老化、焊接问题或电磁干扰可能导致LSE停振,而系统可能仍以LSI(内部低速RC)继续运行,导致时间漂移或唤醒异常。传统故障记录依赖外部存储或中断,但会侵入主流程。本文介绍一种利用RTC备份域寄存器(BKP)的无侵入式方案:在LSE停振时,通过RTC的时钟安全系统(CSS)或轮询检测,将故障状态和上下文写入备份寄存器,系统重启后仍可读取,实现故障追溯。
原理剖析
1. RTC备份域寄存器(BKP)
STM32F4的RTC备份域包含20个32位备份寄存器(BKP0R-BKP19R),位于VDD备份供电区域。这些寄存器在系统复位、电源跌落时(若VBAT供电)仍保持内容,非常适合存储故障标志、时间戳或关键数据。访问BKP需先使能PWR和备份域时钟,并解除写保护。
2. LSE停振检测机制
- 时钟安全系统(CSS):RTC的CSS功能可监测LSE,一旦检测到停振,自动切换时钟源并触发RTC_Alarm或RTC_WakeUp中断。但CSS仅适用于LSE作为RTC时钟源时,且需配置相关中断。
- 轮询法:通过读取RTC_ISR的LSERDY位(或备份域控制寄存器RCC_BDCR的LSERDY),定期检查LSE状态。此方法简单,但需占用CPU时间。
本文采用轮询法,结合BKP实现无侵入记录,避免中断处理对主流程的干扰。
3. 无侵入式设计
无侵入意味着不修改现有业务代码,仅通过独立模块(如定时器或低优先级任务)周期检查LSE状态,一旦发现停振,立即将故障信息写入BKP,并置位标志。系统重启后,Bootloader或主程序可读取BKP,判断是否发生过LSE故障,并执行相应恢复策略(如切换时钟源、校准时间)。
硬件与软件准备
- 硬件:STM32F4系列开发板(如STM32F407),外部32.768kHz晶振,VBAT接3V电池(可选)。
- 软件:STM32CubeIDE或Keil,HAL库或标准外设库。本文以HAL库为例。
配置步骤
1. 使能备份域时钟和访问
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 解除备份域写保护
2. 配置RTC时钟源(LSE)
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
// 错误处理
}
3. 初始化RTC(可选,若需时间戳)
RTC_HandleTypeDef hrtc;
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
HAL_RTC_Init(&hrtc);
4. 编写LSE检测函数
uint8_t Check_LSE_Status(void) {
// 读取RCC_BDCR的LSERDY位
if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY)) {
return 1; // LSE正常
} else {
return 0; // LSE停振
}
}
5. 故障记录函数
void Record_Fault_To_BKP(uint32_t fault_code) {
// 写入备份寄存器0:故障代码
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, fault_code);
// 写入备份寄存器1:当前系统时间(若RTC运行)
RTC_TimeTypeDef sTime;
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
uint32_t timestamp = (sTime.Hours << 16) | (sTime.Minutes << 8) | sTime.Seconds;
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, timestamp);
// 写入备份寄存器2:累计故障次数(自增)
uint32_t count = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) + 1;
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, count);
}
6. 主循环中的轮询(低优先级)
void LSE_Monitor_Task(void) {
static uint32_t last_check = 0;
if (HAL_GetTick() - last_check > 1000) { // 每秒检查一次
last_check = HAL_GetTick();
if (!Check_LSE_Status()) {
Record_Fault_To_BKP(0xA5A5); // 自定义故障码
// 可选:切换时钟源到LSI或执行其他恢复
}
}
}
7. 系统启动时读取故障记录
void Check_Fault_At_Boot(void) {
uint32_t fault_code = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
uint32_t count = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
if (fault_code == 0xA5A5) {
// 发生过失振,执行恢复策略
printf("LSE fault detected! Count: %lu\n", count);
// 清除故障标志(可选)
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0);
}
}
完整代码示例
以下是一个完整的模块化示例,包含初始化、检测和记录功能。
#include "stm32f4xx_hal.h"
RTC_HandleTypeDef hrtc;
void BKP_Init(void) {
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
}
void RTC_Init(void) {
// 使能LSE
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
// 初始化RTC
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
if (HAL_RTC_Init(&hrtc) != HAL_OK) {
Error_Handler();
}
}
uint8_t Is_LSE_Running(void) {
return __HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY);
}
void Record_Fault(uint32_t code) {
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, code);
RTC_TimeTypeDef sTime;
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
uint32_t ts = (sTime.Hours << 16) | (sTime.Minutes << 8) | sTime.Seconds;
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, ts);
uint32_t cnt = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) + 1;
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, cnt);
}
void LSE_Monitor(void) {
static uint32_t last = 0;
if (HAL_GetTick() - last >= 1000) {
last = HAL_GetTick();
if (!Is_LSE_Running()) {
Record_Fault(0xA5A5);
// 可在此添加切换时钟源逻辑
}
}
}
int main(void) {
HAL_Init();
BKP_Init();
RTC_Init();
// 启动时检查故障
uint32_t code = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
if (code == 0xA5A5) {
// 处理故障
}
while (1) {
LSE_Monitor();
// 其他任务
}
}
注意事项
-
备份域写保护:必须调用
HAL_PWR_EnableBkUpAccess(),否则写入无效。 - VBAT供电:若系统断电,备份寄存器内容依赖VBAT引脚供电。若VBAT未接电池,断电后数据丢失,但系统复位(非断电)时仍保留。
- 时钟源切换:检测到LSE停振后,建议将RTC时钟源切换到LSI,并重新校准时间,避免时间错误累积。
- 轮询频率:根据应用需求调整检查间隔,过频会增加功耗,过疏可能错过瞬时故障。
- 故障码设计:使用特定模式(如0xA5A5)作为有效标志,避免误判。
- 多寄存器原子性:写入多个BKP寄存器时,若中途断电,可能部分写入。可先写一个“写入完成”标志寄存器,最后置位,读取时检查该标志。
总结
通过RTC备份域寄存器,我们实现了一种轻量级、无侵入的LSE故障记录方案。该方法不依赖额外硬件,不打断主流程,且能在系统重启后保留故障信息,极大方便了现场调试和远程诊断。开发者可根据实际需求扩展,例如记录更多上下文(电压、温度)或结合看门狗实现自动恢复。希望本文能为您提供有价值的参考。