4、RTC日历配置(上):CubeMX图形化配置RTC、设置日期与时间、启用日历模式、理解RTC初始化结构体

各位同学,咱们今天来聊聊RTC的日历配置。说实话,RTC这个外设在低功耗项目里几乎是标配——你想想看,不管是智能手表、数据记录仪,还是远程传感器节点,哪个不需要知道当前是几点几分?我最早做低功耗项目时,第一版代码连RTC都没配,结果设备跑了一周,数据全对不上时间戳,那叫一个尴尬。

这一节我们先搞定RTC的图形化配置,把日历模式跑起来。我会带着你一步步走完CubeMX的设置,再深入看看背后的初始化结构体。嗯,咱们开始吧。

4.1 为什么选RTC日历模式?

STM32L4的RTC有两种工作模式:日历模式和二进制计数器模式。我个人习惯,只要项目里需要“人类可读”的时间(年/月/日/时/分/秒),就直接上日历模式。二进制计数器模式虽然省一点点功耗,但每次读时间都得自己换算,调试时特别容易出bug。

核心结论:日历模式下,RTC内部硬件自动处理月份天数、闰年等逻辑。你只需要设置一次初始时间,剩下的交给硬件。

我在项目中遇到过一件事:有个同事用二进制计数器模式做数据记录仪,结果2月29日那天,所有日志的时间戳都乱了。从那以后,我但凡涉及日期,一律用日历模式。

4.2 CubeMX图形化配置步骤

打开CubeMX,选好你的STM32L4型号(我用的是STM32L476RG做演示)。咱们一步步来:

  1. 使能RTC外设:在Pinout & Configuration面板里,找到RTC,勾选Activate。
  2. 选择时钟源:RTC的时钟源我建议选LSE(32.768kHz外部晶振)。为什么?因为LSE的精度比LSI(内部低速振荡器)高一个数量级。我踩过坑——用LSI做RTC时钟,一天能差出十几秒,做时间同步项目时根本没法用。
  3. 配置日历参数:在RTC Configuration窗口里,切换到Calendar标签页:
    • Year:设置当前年份(比如2025)
    • Month:月份(1-12)
    • Date:日期(1-31)
    • Hour:小时(0-23,建议用24小时制)
    • Minute:分钟(0-59)
    • Second:秒(0-59)
  4. 启用日历模式:确保Calendar Mode被勾选。这个选项默认就是开的,但最好检查一下。
  5. 配置预分频器:RTC内部有两个预分频器——Asynchronous Prescaler(异步,默认128)和Synchronous Prescaler(同步,默认256)。两者乘积要等于时钟源频率(32768 / 128 / 256 = 1Hz)。

小技巧:异步预分频器值设大一点可以省电,但同步预分频器值设大一点能提高时间精度。我一般保持默认128+256不动,除非有特殊功耗要求。

配置完成后,点击Generate Code,让CubeMX帮你生成初始化代码。嗯,这里要注意:生成的代码里,RTC的初始化是在MX_RTC_Init()函数里完成的。

4.3 理解RTC初始化结构体

打开生成的rtc.c文件,你会看到这样一个结构体:

RTC_HandleTypeDef hrtc;

static void MX_RTC_Init(void)
{
  hrtc.Instance = RTC;
  hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
  hrtc.Init.AsynchPrediv = RTC_ASYNCH_PREDIV;   // 默认128
  hrtc.Init.SynchPrediv = RTC_SYNCH_PREDIV;     // 默认256
  hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
  hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
  hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
  hrtc.Init.OutPutRemap = RTC_OUTPUT_REMAP_NONE;
  
  if (HAL_RTC_Init(&hrtc) != HAL_OK)
  {
    Error_Handler();
  }
}

咱们拆开来看每个成员:

结构体成员 可选值 说明
HourFormat RTC_HOURFORMAT_24 / RTC_HOURFORMAT_12 24小时制还是12小时制。我建议统一用24小时制,省得处理AM/PM的麻烦。
AsynchPrediv 1-0x7F(7位) 异步预分频器值,影响功耗和精度。
SynchPrediv 1-0x1FF(9位) 同步预分频器值,与异步预分频器共同决定1Hz基准。
OutPut RTC_OUTPUT_DISABLE / RTC_OUTPUT_ALARMA / RTC_OUTPUT_ALARMB / RTC_OUTPUT_WAKEUP RTC输出引脚功能,一般用不到,保持禁用即可。
OutPutPolarity RTC_OUTPUT_POLARITY_HIGH / RTC_OUTPUT_POLARITY_LOW 输出极性,默认高电平有效。
OutPutType RTC_OUTPUT_TYPE_OPENDRAIN / RTC_OUTPUT_TYPE_PUSHPULL 输出类型,开漏或推挽。
OutPutRemap RTC_OUTPUT_REMAP_NONE / RTC_OUTPUT_REMAP_POS1 输出引脚重映射,默认不重映射。

注意:预分频器的值一旦设定,在RTC运行期间就不能改了。如果你要改,必须先停掉RTC。我曾经在调试时直接改预分频器,结果RTC计数器直接乱掉,时间跳到1970年去了……

4.4 设置日期与时间:HAL_RTC_SetTime和HAL_RTC_SetDate

初始化完成后,我们需要设置具体的日期和时间。CubeMX生成的代码里,这部分是放在main()函数里的:

RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};

// 设置时间:14:30:00
sTime.Hours = 14;
sTime.Minutes = 30;
sTime.Seconds = 0;
sTime.TimeFormat = RTC_HOURFORMAT_24;
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;

if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
{
  Error_Handler();
}

// 设置日期:2025年3月15日
sDate.Year = 25;    // 注意:年份是相对于2000年的偏移,25表示2025年
sDate.Month = 3;
sDate.Date = 15;
sDate.WeekDay = 6;  // 星期六(1=周一,7=周日)

if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
{
  Error_Handler();
}

这里有几个坑,我帮你标出来:

  • 年份偏移:sDate.Year存的是相对于2000年的偏移。比如2025年,就填25。我第一次用的时候直接填了2025,结果RTC显示的是3925年……
  • 星期计算:sDate.WeekDay需要你自己算好。1代表周一,7代表周日。我一般用在线工具查一下当天是星期几。
  • 格式选择:RTC_FORMAT_BIN表示用二进制格式传参,RTC_FORMAT_BCD表示用BCD码。我个人习惯用BIN,因为直观,调试时一眼就能看出数值对不对。
  • 调用顺序:必须先调HAL_RTC_SetTime,再调HAL_RTC_SetDate。为什么?因为RTC内部的时间寄存器会触发日期更新。顺序反了,日期可能写不进去。

经验之谈:如果你在调试时发现时间写不进去,先检查一下RTC的写保护有没有解除。STM32L4的RTC在复位后默认是写保护的,需要先调用HAL_RTC_MspInit()里的__HAL_RTC_WRITEPROTECTION_DISABLE()宏。CubeMX生成的代码会自动处理,但如果你自己手写初始化,很容易漏掉这一步。

4.5 知识结构图:RTC日历配置流程

下面这张图帮你理清整个配置流程,从时钟源到最终读取时间:

RTC日历配置流程 1. 选择时钟源 2. 使能RTC外设 3. 配置预分频器 4. 启用日历模式 5. HAL_RTC_SetTime 6. HAL_RTC_SetDate 7. HAL_RTC_GetTime/Date 注意:SetTime必须在SetDate之前调用,否则日期可能写入失败 年份值 = 实际年份 - 2000(如2025年填25)

4.6 验证配置:读取当前时间

配置完成后,怎么确认RTC在正常工作?很简单,读一下时间看看:

RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;

// 读取时间
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
// 读取日期(必须在GetTime之后调用)
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

printf("当前时间: %02d:%02d:%02d\r\n", sTime.Hours, sTime.Minutes, sTime.Seconds);
printf("当前日期: 20%02d-%02d-%02d\r\n", sDate.Year, sDate.Month, sDate.Date);

重要:HAL_RTC_GetDate()必须在HAL_RTC_GetTime()之后调用。为什么?因为RTC内部的时间寄存器在读取时会锁存日期值。如果你先读日期再读时间,日期可能读到的是旧值。这个顺序问题我当年调试了整整一个下午才找到原因……