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做演示)。咱们一步步来:
- 使能RTC外设:在Pinout & Configuration面板里,找到
RTC,勾选Activate。 - 选择时钟源:RTC的时钟源我建议选
LSE(32.768kHz外部晶振)。为什么?因为LSE的精度比LSI(内部低速振荡器)高一个数量级。我踩过坑——用LSI做RTC时钟,一天能差出十几秒,做时间同步项目时根本没法用。 - 配置日历参数:在
RTC Configuration窗口里,切换到Calendar标签页:Year:设置当前年份(比如2025)Month:月份(1-12)Date:日期(1-31)Hour:小时(0-23,建议用24小时制)Minute:分钟(0-59)Second:秒(0-59)
- 启用日历模式:确保
Calendar Mode被勾选。这个选项默认就是开的,但最好检查一下。 - 配置预分频器: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日历配置流程
下面这张图帮你理清整个配置流程,从时钟源到最终读取时间:
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内部的时间寄存器在读取时会锁存日期值。如果你先读日期再读时间,日期可能读到的是旧值。这个顺序问题我当年调试了整整一个下午才找到原因……