哈喽,各位嵌入式圈的 “小白” 们!今天咱们来唠一个让无数新手头疼的模块 ——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);
  }
}

五、避坑指南

  1. 忘记解除备份寄存器保护:一定要调用 PWR_BackupAccessCmd(ENABLE),否则写备份寄存器会直接死机!

  2. LSE 没等稳定就配置:别迷信 RCC_GetFlagStatus(RCC_FLAG_LSERDY),直接延时 100ms 最靠谱。

  3. 中断标志位没清干净:中断服务函数里必须同时清 RTC 标志位和 EXTI 标志位,少一个都会无限触发中断。

  4. 年份设置错误RTC_DateStruct.RTC_Year 是后两位(比如 2024 年要传 24),别传 2024!

  5. 分频计算错误:异步分频 + 1 乘以 同步分频 + 1 必须等于时钟源频率(LSE 是 32768),否则时间会变快 / 变慢。

六、扩展知识:RTC 还能这么玩!

  • 闰年自动处理:STM32 的 RTC 会自动判断闰年(2 月 29 天),不用手动计算。
  • 低功耗模式唤醒:单片机进入待机模式后,RTC 可以定时唤醒,大大降低功耗(电池设备必备)。
  • 双闹钟功能:部分 STM32 型号支持两个闹钟(Alarm A 和 Alarm B),可以设置两个不同时间。
  • 亚秒级计时:通过读取 RTC 的预分频计数器,可以实现毫秒级甚至微秒级计时(精度取决于时钟源)。

总结:从 “小白” 到 “RTC 大师” 就差这一步

        RTC  看似复杂,其实核心就是 “选对时钟源(LSE)+ 正确配置中断 + 处理标志位”。记住这篇文章的步骤,你也能轻松实现 “断电计时”“定时闹钟”“周期性唤醒” 等功能。

        最后送大家一句口诀:“LSE 优先,备份寄存器存标记,中断标志要清完,时间日期别填反”

        赶紧拿起开发板试试吧!如果成功了,欢迎在评论区报喜;如果踩坑了,也可以留言提问,我会一一解答~

        点赞收藏不迷路,下次调 RTC 直接抄作业! 🚀

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐