【保姆级教程】STM32 RTC 实时时钟从入门到放弃?不!看完这篇直接上手!
文章摘要: 本文讲解STM32的RTC实时时钟模块,适合嵌入式新手学习。RTC相当于单片机的"电子表";,断电后仍能计时,常用于记录数据、定时任务等场景。重点分析了三种时钟源(LSE、LSI、HSE)的优缺点,推荐使用LSE晶振保证精度。通过代码示例详细演示了RTC初始化、时间设置、闹钟和唤醒中断配置的全流程,并总结了常见避坑点(如备份寄存器保护、中断标志清除等)。最后还扩展了RTC的闰年处理、低
哈喽,各位嵌入式圈的 “小白” 们!今天咱们来唠一个让无数新手头疼的模块 ——RTC 实时时钟。你是不是也曾对着 datasheet 里的 “LSE”“LSI” 一脸懵?是不是配置完代码发现时钟跑着跑着就 “放飞自我” 了?别慌,这篇文章带你从 “啥是 RTC” 到 “亲手写代码实现闹钟”,全程大白话 + 搞笑吐槽,保证严谨又好懂!
一、先搞懂:RTC 到底是个啥?
想象一下:你家的电子表,就算拔掉电源(换成纽扣电池),时间也不会 “失忆”—— 这就是 RTC 的核心作用!
RTC(Real Time Clock)= 实时时钟,它就像单片机里的 “小闹钟”:
- 能独立计时(秒、分、时、日、月、年)
- 断电后靠纽扣电池续命(所以掉电不丢时间)
- 还能触发闹钟、定时唤醒单片机(省电神器!)
为啥需要 RTC?这些场景离不了!
- 嵌入式系统:比如温湿度记录仪,得精确记录 “xx 时间温度是 xx”
- 智能家居:智能灯定时开关、电饭煲预约煮饭,全靠它记时
- 工业控制:生产线的定时巡检、设备维护提醒
- 仪器仪表:示波器的时间戳、万用表的测量记录
- 闹钟 / 定时器:从手环的起床提醒到物联网设备的周期性上报
二、RTC 的 “心脏”:时钟源怎么选?
RTC 能跑起来,全靠稳定的时钟源。就像人需要稳定的心跳,RTC 的时钟源要是 “跳得忽快忽慢”,时间就不准了。STM32 给了 3 种选择,咱们一个个扒:

1. LSE:外部低速时钟
- 来源:外接 32.768kHz 的晶振(就是电路板上那个小圆柱,长得像纽扣电池的 “邻居”)
- 优点:精度高!32.768kHz 刚好是 2¹⁵,分频后完美得到 1 秒(32768 = 2¹⁵,分频后 1Hz)
- 缺点:需要外接元件(不过大部分开发板都焊好了)
吐槽:这玩意儿就像 “专业手表机芯”,走时准,但得额外装个 “零件”。
2. LSI:内部低速时钟
- 来源:STM32 内部的 RC 振荡器(没有外部元件,全靠芯片自己振)
- 优点:省事儿!不用接外部晶振,电路简单
- 缺点:精度差到离谱!温度一变就 “发疯”(夏天快 2 分钟,冬天慢 3 分钟是常事)
吐槽:这就是个 “地摊电子表”,便宜但不靠谱,适合对时间精度没要求的场景(比如单纯唤醒单片机)。
3. HSE-RTC:高速外部时钟分频
- 来源:把外部高速时钟(HSE,比如 8MHz)分频后给 RTC 用
- 优点:精度还行(毕竟 HSE 本身精度不错)
- 缺点:HSE 耗电比 LSE 大,而且分频电路复杂
吐槽:相当于用 “跑车发动机” 改造成 “钟表齿轮”,性能过剩还费电,除非特殊需求否则不用。
总结:99% 的场景选LSE!精度够、耗电低,完美适配 RTC 需求。
三、RTC 的 “充电宝”:电源从哪来?
RTC 之所以断电还能计时,全靠 “双电源” 设计:
- 正常供电:单片机工作时,用 VDD 主电源
- 掉电供电:主电源断开后,自动切换到VBAT 引脚(接纽扣电池,一般是 CR2032)
就像手机没电了自动切换到备用电池,保证时间 “不断更”。

四、手把手写代码:从初始化到闹钟响
终于到了最激动人心的环节!咱们以 STM32F103 为例,一步步实现 RTC 功能。代码会加超详细注释,保证小白也能看懂。
核心思路:先判断是否需要重新配置
RTC 最烦的是 “每次上电都要重设时间”。解决办法是:用备份寄存器存个 “标记”,如果标记没变,说明纽扣电池没断电,时间还在跑,不用重设!
步骤 1:初始化框架(先看整体流程)
void RTC_Init(void) {
// 读取备份寄存器的“标记”,判断是否需要重配置
if (RTC_ReadBackupRegister(RTC_BKP_DR0) != 0x1234) {
// 标记不对:说明第一次上电或纽扣电池没电了,需要重新配置
RTC_Config(); // 配置RTC时钟源、分频器
RTC_SetTime(23, 59, 55, RTC_H12_PM); // 设置初始时间(23:59:55)
RTC_SetDate(12, 31, 2023, 7); // 设置初始日期(2023-12-31 周日)
RTC_WriteBackupRegister(RTC_BKP_DR0, 0x1234); // 写入标记
} else {
// 标记正确:时间一直在跑,只需同步寄存器
RTC_WaitForSynchro(); // 等待RTC寄存器同步
}
// 配置闹钟和唤醒功能(不管是否重配置,都要开中断)
RTC_Alarm_Init();
RTC_WakeUp_Init();
}
步骤 2:详细配置 RTC(RTC_Config 函数)
这里重点讲LSE 配置(推荐用法):
void RTC_Config(void) {
// 1. 开电源时钟,允许访问备份寄存器
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE); // 解除备份寄存器保护(关键!不然写不了)
// 2. 配置LSE时钟源
RCC_LSEConfig(RCC_LSE_ON); // 开启外部低速晶振
// 等待LSE稳定(别用标志位,直接延时最靠谱,老司机都这么干)
delay_ms(100);
// 3. 选择RTC时钟源为LSE
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE); // 开启RTC时钟
// 4. 等待RTC寄存器同步(必须等,不然读不到正确值)
RTC_WaitForSynchro();
// 5. 配置RTC分频器(32768Hz分频到1Hz,刚好1秒)
RTC_InitTypeDef RTC_InitStruct;
RTC_InitStruct.RTC_AsynchPrediv = 0x7F; // 异步分频:127
RTC_InitStruct.RTC_SynchPrediv = 0xFF; // 同步分频:255
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; // 24小时制
RTC_Init(&RTC_InitStruct);
}
关键知识点:
- 分频计算:
(0x7F + 1) * (0xFF + 1) = 128 * 256 = 32768,刚好把 32768Hz 分成 1Hz(1 秒)。 - 为什么用延时等 LSE?因为 LSE 启动慢,标志位有时候不准,直接等 100ms 最稳。
步骤 3:设置时间和日期
// 设置时间(时、分、秒、上午/下午)
void RTC_SetTime(uint8_t Hour, uint8_t Minute, uint8_t Second, uint8_t AM_PM) {
RTC_TimeTypeDef RTC_TimeStruct;
RTC_TimeStruct.RTC_Hours = Hour;
RTC_TimeStruct.RTC_Minutes = Minute;
RTC_TimeStruct.RTC_Seconds = Second;
RTC_TimeStruct.RTC_H12 = AM_PM; // 选RTC_H12_AM(上午)或PM(下午)
RTC_SetTime(RTC_Format_BIN, &RTC_TimeStruct); // BIN表示十进制(不是二进制!)
}
// 设置日期(月、日、年、星期)
void RTC_SetDate(uint8_t Month, uint8_t Date, uint8_t Year, uint8_t WeekDay) {
RTC_DateTypeDef RTC_DateStruct;
RTC_DateStruct.RTC_Month = Month;
RTC_DateStruct.RTC_Date = Date;
RTC_DateStruct.RTC_Year = Year; // 注意:这里是后两位(比如23表示2023)
RTC_DateStruct.RTC_WeekDay = WeekDay; // 1=周一,7=周日
RTC_SetDate(RTC_Format_BIN, &RTC_DateStruct);
}
坑点提醒:
- 年份只存后两位(00-99),所以 2023 年要传 23。
- 星期几是 “1 = 周一,7 = 周日”,别搞反了!
步骤 4:配置闹钟(到点提醒)
比如设置 “每天 8:00:00” 响铃:
void RTC_Alarm_Init(void) {
// 1. 配置闹钟时间
RTC_AlarmTypeDef RTC_AlarmStruct;
RTC_AlarmStruct.RTC_AlarmTime.RTC_Hours = 8;
RTC_AlarmStruct.RTC_AlarmTime.RTC_Minutes = 0;
RTC_AlarmStruct.RTC_AlarmTime.RTC_Seconds = 0;
RTC_AlarmStruct.RTC_AlarmTime.RTC_H12 = RTC_H12_AM;
RTC_AlarmStruct.RTC_AlarmDateWeekDay = 1; // 日期(如果RTC_AlarmDateWeekDaySel是日期)
RTC_AlarmStruct.RTC_AlarmDateWeekDaySel = RTC_AlarmDateWeekDaySel_Date; // 按日期触发
RTC_AlarmStruct.RTC_AlarmMask = RTC_AlarmMask_None; // 不屏蔽任何字段(严格匹配时分秒)
RTC_SetAlarm(RTC_Format_BIN, RTC_Alarm_A, &RTC_AlarmStruct);
// 2. 配置中断(EXTI_17线专门接RTC闹钟)
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line17;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 3. 配置NVIC中断优先级
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = RTCAlarm_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 4. 使能闹钟中断
RTC_ITConfig(RTC_IT_ALRA, ENABLE);
RTC_AlarmCmd(RTC_Alarm_A, ENABLE);
}
// 闹钟中断服务函数
void RTCAlarm_IRQHandler(void) {
if (RTC_GetITStatus(RTC_IT_ALRA) != RESET) {
// 闹钟响了!这里可以加代码(比如开灯、发串口消息)
printf("起床啦!太阳晒屁股了!\r\n");
// 清除标志位(必须做,不然会一直触发中断)
RTC_ClearITPendingBit(RTC_IT_ALRA);
EXTI_ClearITPendingBit(EXTI_Line17);
}
}
步骤 5:配置唤醒功能(周期性触发)
比如每 1 秒唤醒一次,用来更新时间显示:
void RTC_WakeUp_Init(void) {
// 1. 先关闭唤醒功能(配置前必须关)
RTC_WakeUpCmd(DISABLE);
// 2. 配置EXTI_22线(专门接RTC唤醒)
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line22;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 3. 配置NVIC
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = RTC_WKUP_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 4. 配置唤醒时钟源(选LSE分频后的1Hz,即1秒)
RTC_WakeUpClockConfig(RTC_WakeUpClock_CK_SPRE_16bits); // 1Hz时钟
RTC_SetWakeUpCounter(0); // 计数0,即1秒触发一次
// 5. 使能唤醒中断
RTC_ITConfig(RTC_IT_WUT, ENABLE);
RTC_WakeUpCmd(ENABLE);
}
// 唤醒中断服务函数(每1秒触发一次)
void RTC_WKUP_IRQHandler(void) {
if (RTC_GetITStatus(RTC_IT_WUT) != RESET) {
// 读取当前时间并显示
RTC_TimeTypeDef time;
RTC_DateTypeDef date;
RTC_GetTime(RTC_Format_BIN, &time);
RTC_GetDate(RTC_Format_BIN, &date);
printf("20%02d-%02d-%02d %02d:%02d:%02d\r\n",
date.RTC_Year, date.RTC_Month, date.RTC_Date,
time.RTC_Hours, time.RTC_Minutes, time.RTC_Seconds);
// 清除标志位
RTC_ClearITPendingBit(RTC_IT_WUT);
EXTI_ClearITPendingBit(EXTI_Line22);
}
}
五、避坑指南
-
忘记解除备份寄存器保护:一定要调用
PWR_BackupAccessCmd(ENABLE),否则写备份寄存器会直接死机! -
LSE 没等稳定就配置:别迷信
RCC_GetFlagStatus(RCC_FLAG_LSERDY),直接延时 100ms 最靠谱。 -
中断标志位没清干净:中断服务函数里必须同时清 RTC 标志位和 EXTI 标志位,少一个都会无限触发中断。
-
年份设置错误:
RTC_DateStruct.RTC_Year是后两位(比如 2024 年要传 24),别传 2024! -
分频计算错误:异步分频 + 1 乘以 同步分频 + 1 必须等于时钟源频率(LSE 是 32768),否则时间会变快 / 变慢。
六、扩展知识:RTC 还能这么玩!
- 闰年自动处理:STM32 的 RTC 会自动判断闰年(2 月 29 天),不用手动计算。
- 低功耗模式唤醒:单片机进入待机模式后,RTC 可以定时唤醒,大大降低功耗(电池设备必备)。
- 双闹钟功能:部分 STM32 型号支持两个闹钟(Alarm A 和 Alarm B),可以设置两个不同时间。
- 亚秒级计时:通过读取 RTC 的预分频计数器,可以实现毫秒级甚至微秒级计时(精度取决于时钟源)。
总结:从 “小白” 到 “RTC 大师” 就差这一步
RTC 看似复杂,其实核心就是 “选对时钟源(LSE)+ 正确配置中断 + 处理标志位”。记住这篇文章的步骤,你也能轻松实现 “断电计时”“定时闹钟”“周期性唤醒” 等功能。
最后送大家一句口诀:“LSE 优先,备份寄存器存标记,中断标志要清完,时间日期别填反”
赶紧拿起开发板试试吧!如果成功了,欢迎在评论区报喜;如果踩坑了,也可以留言提问,我会一一解答~
点赞收藏不迷路,下次调 RTC 直接抄作业! 🚀
更多推荐



所有评论(0)