4. RTC初始化与配置:寄存器级配置(HAL库与LL库两种方式)
RTC的初始化,说白了就是让这个“万年历”模块跑起来,并且把时间日期设对。很多初学者觉得RTC配置很复杂,其实拆开来看,就那么几步:使能电源、选择时钟源、配置预分频器、设置时间日期。今天我就带大家用HAL库和LL库两种方式,把RTC的寄存器级配置彻底搞明白。
4.1 RTC初始化流程概览
在动手写代码之前,我们先理清RTC初始化的完整流程。我习惯把它分成四个阶段:
- 电源与备份域使能——RTC和备份寄存器都在VBAT供电域,必须先使能PWR和BKP时钟,并允许访问备份域。
- 时钟源选择与使能——RTC时钟可以来自LSE、LSI或HSE分频。绝大多数低功耗项目用LSE(32.768kHz),因为精度最高。
- 预分频器配置——把32.768kHz分频到1Hz,这是RTC计时的基础。
- 时间日期设置——写入初始的时、分、秒、年、月、日。
下面这张图把整个流程串起来了,大家可以对照着看。
4.2 HAL库方式:RTC初始化与配置
HAL库把RTC的初始化封装成了几个结构体和函数。我个人觉得HAL库的代码可读性很好,适合项目初期快速验证。但要注意,HAL库的RTC初始化函数内部会做很多状态检查,如果时钟源没稳定,它会一直等。
4.2.1 时钟与备份域使能
这一步是基础,很多新手容易忘。我曾经帮一个同事排查RTC不走的故障,查了半天发现他忘了使能PWR时钟。嗯,这种低级错误其实挺常见的。
// 使能PWR和BKP时钟
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
// 允许访问备份域
HAL_PWR_EnableBkUpAccess();
4.2.2 配置LSE时钟源
LSE是外部低速晶振,32.768kHz。我建议在PCB布局时把LSE晶振尽量靠近MCU,走线要短,否则容易起振失败。
// 使能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();
}
// 选择LSE作为RTC时钟源
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0};
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;
PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) {
Error_Handler();
}
// 使能RTC时钟
__HAL_RCC_RTC_ENABLE();
4.2.3 初始化RTC并设置时间日期
这里用到了HAL_RTC_Init()和HAL_RTC_SetTime()、HAL_RTC_SetDate()。注意,设置时间日期前要等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;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
if (HAL_RTC_Init(&hrtc) != HAL_OK) {
Error_Handler();
}
// 设置时间:14:30:00
RTC_TimeTypeDef sTime = {0};
sTime.Hours = 14;
sTime.Minutes = 30;
sTime.Seconds = 0;
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;
if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) {
Error_Handler();
}
// 设置日期:2025年4月15日
RTC_DateTypeDef sDate = {0};
sDate.WeekDay = RTC_WEEKDAY_TUESDAY;
sDate.Month = RTC_MONTH_APRIL;
sDate.Date = 15;
sDate.Year = 25; // 2025年
if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK) {
Error_Handler();
}
💡 预分频器计算小技巧
异步预分频 + 1 乘以 同步预分频 + 1 必须等于 RTC时钟频率。
对于32.768kHz LSE: (127+1) × (255+1) = 128 × 256 = 32768 ✅
如果你用LSI(约32kHz),需要调整这两个值,保证乘积接近时钟频率。
异步预分频 + 1 乘以 同步预分频 + 1 必须等于 RTC时钟频率。
对于32.768kHz LSE: (127+1) × (255+1) = 128 × 256 = 32768 ✅
如果你用LSI(约32kHz),需要调整这两个值,保证乘积接近时钟频率。
4.3 LL库方式:更贴近寄存器的操作
LL库的代码更精简,执行效率更高。我一般在产品量产固件里用LL库,因为代码体积小,而且没有HAL库那些超时等待。但LL库需要你对寄存器更熟悉——说白了,你得知道自己在写什么。
4.3.1 时钟与备份域使能(LL库)
// 使能PWR和BKP时钟
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_BKP);
// 允许访问备份域
LL_PWR_EnableBkUpAccess();
4.3.2 配置LSE并选择RTC时钟源
// 使能LSE并等待稳定
LL_RCC_LSE_SetDriveCapability(LL_RCC_LSEDRIVE_MEDIUM);
LL_RCC_LSE_Enable();
while (!LL_RCC_LSE_IsReady()) {
// 等待LSE稳定,超时处理建议加在这里
}
// 选择LSE作为RTC时钟源
LL_RCC_SetRTCClockSource(LL_RCC_RTC_CLKSOURCE_LSE);
// 使能RTC时钟
LL_RCC_EnableRTC();
4.3.3 配置预分频器(LL库)
LL库没有像HAL库那样的Init结构体,你需要直接操作寄存器。这里我直接写寄存器,但为了可读性,LL库提供了宏定义。
// 等待RTC寄存器同步
LL_RTC_DisableWriteProtection(RTC);
LL_RTC_EnterInitMode(RTC);
// 配置异步预分频和同步预分频
LL_RTC_SetAsynchPrescaler(RTC, 127); // 异步预分频值
LL_RTC_SetSynchPrescaler(RTC, 255); // 同步预分频值
// 退出初始化模式
LL_RTC_ExitInitMode(RTC);
LL_RTC_EnableWriteProtection(RTC);
4.3.4 设置时间日期(LL库)
// 设置时间:14:30:00
LL_RTC_DisableWriteProtection(RTC);
LL_RTC_EnterInitMode(RTC);
LL_RTC_TIME_SetHour(RTC, 14);
LL_RTC_TIME_SetMinute(RTC, 30);
LL_RTC_TIME_SetSecond(RTC, 0);
// 设置日期:2025年4月15日
LL_RTC_DATE_SetYear(RTC, 25);
LL_RTC_DATE_SetMonth(RTC, LL_RTC_DATE_MONTH_APRIL);
LL_RTC_DATE_SetDay(RTC, 15);
LL_RTC_DATE_SetWeekDay(RTC, LL_RTC_DATE_WEEKDAY_TUESDAY);
LL_RTC_ExitInitMode(RTC);
LL_RTC_EnableWriteProtection(RTC);
⚠ 重要:写保护与初始化模式
操作RTC寄存器前,必须先关闭写保护(LL_RTC_DisableWriteProtection),然后进入初始化模式(LL_RTC_EnterInitMode)。退出时顺序相反。我曾经见过有人只关了写保护没进初始化模式,结果时间写不进去,排查了半天。
操作RTC寄存器前,必须先关闭写保护(LL_RTC_DisableWriteProtection),然后进入初始化模式(LL_RTC_EnterInitMode)。退出时顺序相反。我曾经见过有人只关了写保护没进初始化模式,结果时间写不进去,排查了半天。
4.4 HAL库 vs LL库:关键差异对比
为了让大家更直观地选择,我把两种方式的差异整理成了表格。
| 对比项 | HAL库 | LL库 |
|---|---|---|
| 代码量 | 较多,结构体+函数调用 | 较少,直接寄存器操作 |
| 可读性 | 高,适合初学者 | 中等,需要了解寄存器 |
| 执行效率 | 较低,有状态检查 | 高,无额外开销 |
| 代码体积 | 较大 | 小 |
| 超时处理 | 内部有超时机制 | 需要自己加超时 |
| 适用场景 | 原型验证、快速开发 | 量产固件、资源受限 |
4.5 避坑指南:RTC配置常见问题
做RTC配置这么多年,我踩过不少坑。下面这几个问题,几乎每个项目都会遇到。
- LSE不起振——检查晶振负载电容是否匹配,PCB走线是否过长。我遇到过因为晶振旁边走了一根高频信号线导致LSE起振失败的案例。
- 时间写不进去——多半是没关写保护,或者没进入初始化模式。用HAL库时注意HAL_RTC_SetTime()必须在HAL_RTC_Init()之后调用。
- 复位后时间丢失——检查VBAT引脚是否接电池或电容。如果VBAT悬空,掉电后RTC数据全丢。
- 预分频器配置错误导致时间不准——记住公式:异步预分频 × 同步预分频 = RTC时钟频率。32.768kHz LSE对应127和255。
🔑 核心要点
RTC配置的本质就是:
1. 打开备份域访问权限
2. 选择并稳定时钟源
3. 配置预分频器得到1Hz
4. 写入初始时间日期
无论用HAL库还是LL库,底层寄存器操作都是一样的。理解了原理,用什么库都只是语法差异。
RTC配置的本质就是:
1. 打开备份域访问权限
2. 选择并稳定时钟源
3. 配置预分频器得到1Hz
4. 写入初始时间日期
无论用HAL库还是LL库,底层寄存器操作都是一样的。理解了原理,用什么库都只是语法差异。