4. 备份域寄存器与RTC核心寄存器:BKP域的作用,RTC_TR、RTC_DR、RTC_SSR、RTC_PRER等寄存器详解

好,咱们这一章聊点硬核的——备份域和RTC的核心寄存器。说实话,我刚接触STM32L4的时候,也被备份域这个概念绕晕过。它到底备了个啥?跟RTC又是什么关系?

别急,咱们一层层剥开。

4.1 备份域(BKP Domain)到底是什么?

先看一张图,帮你建立整体印象:

备份域(BKP Domain)结构图 VDD 主电源域 主电源供电时工作 VBAT 备份域 主电源掉电时由VBAT供电 CPU / 外设 SRAM / Flash RTC 寄存器 20字节BKP寄存器 RTC 计数器 RTC 预分频器 32.768kHz晶振 VBAT供电切换 APB总线 关键点: • 主电源掉电时,备份域由VBAT引脚(纽扣电池)独立供电 • RTC计数器、预分频器、备份寄存器都在备份域内,掉电不丢失

说白了,备份域就是一块「永不掉电」的小天地。主电源断了,它靠VBAT引脚上的纽扣电池继续工作。RTC的计数器、预分频器、还有那20字节的备份寄存器,全待在里面。

核心结论:备份域 = RTC核心逻辑 + 备份寄存器 + 独立供电。只要VBAT有电,时间就不会停,数据就不会丢。

4.2 备份域寄存器(BKP Registers)

STM32L4的备份域里,除了RTC之外,还有一组BKP寄存器。L4系列提供了20字节的备份寄存器(5个32位寄存器)。

我个人的习惯是,把这20字节当成「系统黑匣子」。比如记录系统复位次数、保存最后一次休眠前的状态标志、存个校准参数什么的。主电源掉电再恢复,读出来还在。

实用技巧:调试低功耗问题时,我经常在进入Stop模式前写一个特定值到BKP寄存器,醒来后检查这个值是否被意外篡改。如果变了,说明备份域复位过——VBAT可能没接好。

4.3 RTC核心寄存器详解

好,重头戏来了。RTC有四个寄存器你必须搞明白:RTC_TR、RTC_DR、RTC_SSR、RTC_PRER。它们决定了RTC怎么计时、精度多高。

4.3.1 RTC_TR —— 时间寄存器

这个寄存器存的是「当前时间」——时、分、秒。格式是BCD码,不是二进制。

举个例子:下午3点25分08秒,在RTC_TR里存的是 0x152508。注意看,15是时、25是分、08是秒,每位都是BCD。

// 读取当前时间(HAL库方式)
RTC_TimeTypeDef sTime = {0};
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BCD);
// sTime.Hours   = 0x15
// sTime.Minutes = 0x25
// sTime.Seconds = 0x08

注意:读取RTC_TR之前,必须先检查RTC_ISR寄存器中的RSF位(寄存器同步标志)。RTC的时钟域和APB时钟域不同步,直接读可能读到错误值。HAL库内部做了这件事,但如果你直接操作寄存器,别忘了这一步。

4.3.2 RTC_DR —— 日期寄存器

存年、月、日、星期。同样是BCD格式。

位域 名称 说明
[31:24] YT 年的十位(BCD)
[23:20] YU 年的个位(BCD)
[19:16] WDU 星期几(1=周一,7=周日)
[15:12] MT 月的十位(BCD)
[11:8] MU 月的个位(BCD)
[7:4] DT 日的十位(BCD)
[3:0] DU 日的个位(BCD)

比如2024年3月15日,存的是 0x240315。年份只存后两位,世纪由RTC_CR寄存器的BCDU位决定。

4.3.3 RTC_SSR —— 亚秒寄存器

这个寄存器是L4系列RTC的亮点。它存的是「亚秒值」,也就是比秒更精细的时间。

RTC内部有一个亚秒计数器,从 PREDIV_S 的值开始递减,每来一个异步时钟脉冲减1,减到0时秒进位。RTC_SSR读到的就是当前计数器的值。

举个例子:如果 PREDIV_S = 255,那么SSR的值从255递减到0,精度就是 1/256 秒 ≈ 3.9毫秒。

// 读取亚秒值
uint32_t subsecond = hrtc.Instance->SSR;
// 结合PREDIV_S计算实际亚秒时间
float subsecond_ms = (float)(PREDIV_S - subsecond) / (PREDIV_S + 1) * 1000.0f;

实战价值:我在做一个高精度数据采集项目时,用SSR实现了微秒级的时间戳对齐。每次外部中断触发,同时捕获SSR值,精度比单纯靠软件延时高了一个数量级。

4.3.4 RTC_PRER —— 预分频寄存器

这个寄存器决定了RTC的计时精度。它分两部分:

  • PREDIV_A(异步预分频):7位,取值范围0~127。推荐值127。
  • PREDIV_S(同步预分频):15位,取值范围0~32767。推荐值255。

计算公式:

RTC时钟频率 = LSE频率 / (PREDIV_A + 1) / (PREDIV_S + 1)

默认配置:32768 / (127 + 1) / (255 + 1) = 32768 / 128 / 256 = 1 Hz

也就是说,默认配置下RTC每秒走一格。如果你需要更高的分辨率,可以调整PREDIV_S的值。

PREDIV_A PREDIV_S 异步时钟频率 同步时钟频率 SSR精度
127 255 256 Hz 1 Hz 3.9 ms
127 127 256 Hz 2 Hz 1.95 ms
63 511 512 Hz 1 Hz 1.95 ms
0 32767 32768 Hz 1 Hz 30.5 μs

我曾经踩过的坑:为了追求高精度,我把PREDIV_A设成0,PREDIV_S设成32767。结果功耗直接飙了3倍!因为异步预分频器工作在32kHz,功耗比256Hz高得多。低功耗项目里,建议保持PREDIV_A=127,只调PREDIV_S。

4.4 寄存器写入保护机制

RTC的寄存器不是你想写就能写的。ST设计了写保护,防止程序跑飞时把时间改乱。

写入流程:

  1. 往RTC_WPR寄存器写 0xCA
  2. 再写 0x53
  3. 然后才能修改RTC_TR、RTC_DR等寄存器
  4. 修改完成后,RTC自动重新进入写保护状态
// 手动操作示例(HAL库内部就是这么做的)
RTC->WPR = 0xCA;
RTC->WPR = 0x53;
// 现在可以写RTC_TR、RTC_DR了
RTC->TR = 0x152508;  // 设置15:25:08
// 写完后自动锁住

调试技巧:如果你发现RTC时间写不进去,先检查是不是忘了解锁。我见过有人把解锁代码放在初始化里,但后面每次写时间都忘了再解锁——结果时间永远停在初始值。

4.5 初始化配置实战

最后,给出一段完整的RTC初始化代码骨架,把上面讲的寄存器都用上:

void MX_RTC_Init(void)
{
  RTC_TimeTypeDef sTime = {0};
  RTC_DateTypeDef sDate = {0};

  // 1. 使能RTC时钟访问
  HAL_RTC_Init(&hrtc);

  // 2. 配置预分频器(PRER)
  //    异步127,同步255 -> 1Hz
  hrtc.Instance->PRER = (127 << 16) | 255;

  // 3. 设置初始时间(BCD格式)
  sTime.Hours = 0x09;
  sTime.Minutes = 0x30;
  sTime.Seconds = 0x00;
  sTime.TimeFormat = RTC_HOURFORMAT_24;
  HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD);

  // 4. 设置初始日期
  sDate.Year = 0x24;    // 2024年
  sDate.Month = 0x03;   // 3月
  sDate.Date = 0x15;    // 15日
  sDate.WeekDay = 5;    // 周五
  HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BCD);

  // 5. 使能RTC
  HAL_RTC_Enable(&hrtc);
}

嗯,到这里,备份域和RTC核心寄存器的内容就差不多了。你想想看,有了这些基础,后面咱们聊RTC的闹钟、唤醒、校准功能时,就不会觉得寄存器陌生了。