RTC理论

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

154.8.1 RTC 理论

/* 课程工程:CubeIDE/14 RTC。
 * 该工程选择 LSE 作为 RTC 时钟源,初始化为 2023-10-31 周二 12:00:00,
 * 再经 USART1(PA9/PA10,115200 bit/s)每秒打印一次。 */

RTC_TimeTypeDef RTC_Time;
RTC_DateTypeDef RTC_Date;
uint8_t RTC_Buff[200];

/* rtc.c 中的关键初始化。RTC_AUTO_1_SECOND 会按 RTCCLK 自动计算预分频值。 */
void MX_RTC_Init(void)
{
    RTC_TimeTypeDef sTime = {0};
    RTC_DateTypeDef DateToUpdate = {0};

    hrtc.Instance = RTC;
    hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
    hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM;
    if (HAL_RTC_Init(&hrtc) != HAL_OK) {
        Error_Handler();
    }

    sTime.Hours = 0x12;
    sTime.Minutes = 0x00;
    sTime.Seconds = 0x00;
    if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD) != HAL_OK) {
        Error_Handler();
    }

    DateToUpdate.WeekDay = RTC_WEEKDAY_TUESDAY;
    DateToUpdate.Month = RTC_MONTH_OCTOBER;
    DateToUpdate.Date = 0x31;
    DateToUpdate.Year = 0x23;
    if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BCD) != HAL_OK) {
        Error_Handler();
    }
}

/* main.c 中的读取和串口输出循环。 */
while (1)
{
    HAL_RTC_GetDate(&hrtc, &RTC_Date, RTC_FORMAT_BIN);
    sprintf((char *)RTC_Buff, "Time:%d-%d-%d-%d\r\n",
            RTC_Date.Year, RTC_Date.Month, RTC_Date.Date, RTC_Date.WeekDay);
    HAL_UART_Transmit(&huart1, RTC_Buff, strlen((char *)RTC_Buff), 1000);

    HAL_RTC_GetTime(&hrtc, &RTC_Time, RTC_FORMAT_BIN);
    sprintf((char *)RTC_Buff, "Time:%02d:%02d:%02d\r\n",
            RTC_Time.Hours, RTC_Time.Minutes, RTC_Time.Seconds);
    HAL_UART_Transmit(&huart1, RTC_Buff, strlen((char *)RTC_Buff), 1000);
    HAL_Delay(1000);
}

本节对应 CubeIDE/14 RTC。上面的代码按工程原样摘录并仅作排版;工程原文件把日期行误标为 Time:,且以 sprintf 写入 200 字节缓冲区。固定的短格式在本例中不会越界,但实际项目宜使用 snprintf 并检查返回值。

一、实时时钟RTC

1.1 实验演示

  • **RTC(Real-Time Clock,实时时钟)**用于持续计量时间。视频中的显示界面包含日期、星期和时分秒;拔掉开发板主电源后,屏幕和串口都会停止,但只要备份域仍有电且 RTC 时钟未停,RTC 的计数可以继续。

  • 这不是“单片机没有电仍在运行”。STM32F103 的主电源是 VDDVDD 缺失时,芯片的内核、SRAM 和普通外设不工作。只有由 VBAT 供电的备份域可以保留:RTC、备份寄存器和 LSE 振荡器。数据手册规定 VBAT 的工作范围为 1.8--3.6 V。

  • 视频开发板使用纽扣电池为 VBAT 提供后备电源。装电池前应确认电池座和板上丝印;常见电池座要求带 + 标记的一面朝上,但应以本板电池座的正负极标识为准,不能反接。

  • 本板有两套外部时钟:课程中所说的 X2 为主时钟用的 HSE(工程配置为外部 8 MHz),X1 为低速外部晶体 LSE(32.768 kHz)。工程的 .ioc 将 LSE 分配到 PC14-OSC32_INPC15-OSC32_OUT,将 HSE 分配到 OSC_IN/OSC_OUT

  • 32.768 kHz = 32768 Hz = 2^15 Hz。选择它的直接好处是,用预分频器装入 32768 - 1 = 32767 即可得到 1 Hz:

    f_1Hz = f_RTCCLK / (PRL + 1) = 32768 / (32767 + 1) = 1 Hz
    

    原笔记“8 MHz 不是 2 的整数次幂,不能精确分频得到 1 Hz”是错误的:8 MHz 也能用可编程分频器除以 8,000,000 得到 1 Hz。RTC 通常选择 32.768 kHz,是因为该频率是行业常用的低功耗时钟频率,晶体频率稳定,并且分频值小、便于得到秒基准;不是因为其他频率绝对无法产生 1 Hz。

  • 对 STM32F103,RTC 时钟源可选 LSE、LSI 或 HSE/128。只有 LSE 被选作 RTCCLK 且 VBAT 存在时,官方参考手册才保证 VDD 断开后 RTC 继续工作;LSI/HSE 不在备份域中,不能据此承诺掉电走时。

1.2 理论知识

  • 备份供电的实际路径是:VDD 存在时,芯片内部电源开关从 VDD 给备份域供电;VDD 掉电后,开关切换到 VBAT。因此,纽扣电池不是给整个开发板供电,而是仅维持低功耗的备份域。
  • LSE 晶体接在 PC14/PC15 附近,晶体与芯片内部振荡放大器、负载电容共同组成振荡回路。CubeMX 中应选择 Crystal/Ceramic Resonator,表示板上接的是无源晶体;若外部已经提供了 32.768 kHz 方波,才应使用旁路(bypass)模式,二者不能混用。
  • LSE 的优势是精度和低功耗,并不意味着“绝对准确”。晶体标称频率仍会随初始频偏、温度和老化变化;长期计时要求较高时,需要校准、温补,或通过网络/GNSS 等外部时间源校时。
  • LSI 是片内低速 RC。参考手册给出的典型频率约为 40 kHz,范围可在 30--60 kHz,未校准时不适合作为高精度钟表基准;它仍可用于成本敏感、精度要求较低的 RTC/自动唤醒场景。HSI 是高速 RC,主要服务系统时钟,也不是本工程的 RTC 源。
  • **F1 特有的限制:**硬件 RTC 的核心是 32 位秒计数器、预分频器和闹钟,并没有后代 STM32 常见的硬件日历寄存器。此工程所用 F1 HAL 将年月日存放在 RTC_HandleTypeDef.DateToUpdate(RAM)中,并在读取时跨过 24 小时时由软件递增日期。复位、停止或待机后,这份软件日期会丢失;若要保存完整日历,应用必须自行写入 BKP 备份寄存器或外部非易失存储。
  • 因而,“RTC 掉电仍走时”与“复位后仍显示正确的日期时间”是两个问题:前者依赖 LSE+VBAT 保持 RTC 计数;后者还要求启动代码不要无条件覆盖计数,并恢复软件日期状态。

1.3 建工程

1.3.1 RCC配置
  • 在 CubeMX 的 RCC 中将 HSE 设为 Crystal/Ceramic Resonator,对应本板主晶体;将 LSE 同样设为 Crystal/Ceramic Resonator,对应 32.768 kHz 晶体。工程生成结果为 HSE 经 PLL x9 得到 72 MHz 系统时钟,LSE 独立为 RTC 提供 32.768 kHz,不参与 CPU 的 72 MHz 主时钟链。
  • 先在 Timers -> RTC 激活 RTC,时钟树中的 RTC 时钟源才可选。此工程在时钟配置中选择 LSE;生成的 SystemClock_Config() 对应 RCC_RTCCLKSOURCE_LSE
  • 对 LSE 而言,启动后必须等待 LSERDY,时钟才会释放。CubeMX/HAL 会处理正常的配置流程;硬件调试中若 LSE 不起振,应首先核查晶体、负载电容、焊接和是否误选旁路模式,而不是把 PC14/PC15 当普通 GPIO 驱动。
  • RTC 时钟源一旦选择,除非进行备份域复位,否则不能改为另一源。备份域复位会清除 RTC 和备份寄存器,所以变更时钟源是会丢失后备时间/标志的操作。
  • LSE 分频的配置关系为 PRL = RTCCLK / 目标频率 - 1。本工程的 RTC_AUTO_1_SECOND 根据 32,768 Hz 自动写入 PRL=32767,形成 1 秒节拍。
1.3.2 RTC配置
  • CubeMX 中激活 RTC 并勾选 Calendar 后,可以在图形界面填入初始年、月、日、星期、时、分、秒;工程生成的初始值为 2023-10-31(Tuesday)12:00:00,使用 BCD 形式写入:0x23 表示十进制 23,0x31 表示十进制 31,而非十六进制数值 35 或 49。
  • Calendar 选项仅帮助生成 HAL 的日期时间初始化代码,不会让 F1 产生硬件日历。HAL_RTC_SetDate() 在 F1 上把日期保存在 HAL 句柄的 RAM 成员中;这是本例最容易被忽略的限制。
  • 当前 MX_RTC_Init() 每次 MCU 启动均执行 HAL_RTC_SetTime()HAL_RTC_SetDate(),会把计数和软件日期重新写成固定值。因此它是“RTC 读写与串口验证”示例,不是可掉电恢复的成品时钟
  • 要实现实际的首次初始化逻辑,应使用 BKP 数据寄存器存放一个魔数:魔数不存在时设置初始时间、日期并写入魔数;魔数存在时只初始化访问通道、读取现有 RTC 计数并从备份区恢复日期。还要设计“电池耗尽或备份域复位”后的重新校时入口。该流程是对本示例的必要工程化补充,不是原工程已经实现的功能。
1.3.3 RTC代码走势分析
  • MX_RTC_Init() 依次填充 hrtc.Instance、预分频和输出选择,调用 HAL_RTC_Init(),再调用 HAL_RTC_SetTime()HAL_RTC_SetDate()。F1 HAL 的 HAL_RTC_Init() 会依据 RTC_AUTO_1_SECOND 计算 PRL = RTCCLK - 1;LSE 为 32,768 Hz 时就是 32767。
  • 不应把“MSP init 加各寄存器”误写为某个固定 API 名。HAL_RTC_MspInit() 是 HAL 调用的底层初始化回调;本工程在其中打开备份域写访问、BKP 时钟和 RTC 时钟:HAL_PWR_EnableBkUpAccess()__HAL_RCC_BKP_CLK_ENABLE()__HAL_RCC_RTC_ENABLE()
  • HAL_RTC_SetTime() 把时分秒换算为当天已过秒数并写入 RTC 计数器;HAL_RTC_GetTime() 再由计数器换算出时分秒。HAL_RTC_SetDate()/GetDate() 则处理 HAL 的软件日期。阅读 stm32f1xx_hal_rtc.c 时应带着这一硬件/软件边界理解,而不只是记函数名。
  • 当前循环先读日期、再读时间;在恰好跨越午夜的极小窗口,日期与时间可能来自相邻两天。普通演示可接受;对日志、计费等需要一致快照的场景,应检测跨日并重读,或设计带时间戳的原子化读取策略。
1.3.4 RTC常用接口
1.3.4.1 例题: 设置时间和日期
  • F1 HAL 的主要接口是 HAL_RTC_SetTime()HAL_RTC_GetTime()HAL_RTC_SetDate()HAL_RTC_GetDate();实际名称带 HAL_ 前缀,原笔记中的 rtc_set_time 等不是本工程可直接调用的函数名。
  • 时间使用 RTC_TimeTypeDef,成员为 HoursMinutesSeconds;日期使用 RTC_DateTypeDef,成员为 YearMonthDateWeekDayYear 在此 HAL 中是两位年份(0--99),应用层若显示完整年份可自行加上世纪,例如本例显示为 20%02d
  • HAL_RTC_SetTime(&hrtc, &sTime, format)HAL_RTC_SetDate(&hrtc, &sDate, format) 的第一个参数是 RTC 句柄地址,第二个参数是结构体地址,第三个参数规定传入结构体的编码。传入 BCD 时应填写 0x120x31 这类压缩十进制;传入二进制时应填写 1231
  • WeekDay 不应依赖人工填错。在本工程的 F1 HAL 中,HAL_RTC_SetDate() 会根据年月日重新计算星期,并将调用者给入的星期值忽略。因此日期正确比手填星期更关键。
1.3.4.2 例题: 设置日期和时间
  • 本小节原题与视频内容不一致:视频此处实际演示的是获取日期和时间。读取时使用 HAL_RTC_GetTime(&hrtc, &RTC_Time, RTC_FORMAT_BIN)HAL_RTC_GetDate(&hrtc, &RTC_Date, RTC_FORMAT_BIN),获取结果保存在调用者提供的结构体中。
  • 设置与获取不可混淆:Set... 会改写 RTC 的当前状态,Get... 只读取状态。调试时若每次循环都调用 Set...,时间将永远从固定初值重新开始。
1.3.4.3 例题: 获取时间和日期
  • RTC_FORMAT_BIN 表示让 HAL 返回通常的二进制数,适合直接用 %d%02d 格式化;RTC_FORMAT_BCD 则返回压缩 BCD。BCD 的一个字节中高/低半字节各表示一位十进制数字,例如 0x45 表示十进制 45,不是十进制 69,也不是“十六进制保存的时间”。
  • 必须让结构体类型与函数匹配:读取时间用 RTC_TimeTypeDef,读取日期用 RTC_DateTypeDef。函数的第二个参数均为结构体指针,所以局部变量要传 &rtc_time&rtc_date
  • F1 HAL 会在 HAL_RTC_GetTime()HAL_RTC_GetDate() 发现累计超过一天时更新其 RAM 内日期。若应用长时间不读取,下一次读取时仍会按已过去的天数更新;但发生复位后,RAM 日期信息不在,不能借此恢复真实日期。
1.3.4.4 例题: UART传输函数
  • 本工程调用 HAL_UART_Transmit(&huart1, RTC_Buff, strlen((char *)RTC_Buff), 1000)。参数依次是 UART 句柄、待发数据地址、字节数和超时(ms)。huart1 定义在 usart.c,需通过 #include "usart.h" 声明后使用。
  • 源码配置的是 USART1:PA9 为 TX、PA10 为 RX,参数为 115200 bit/s、8 数据位、1 停止位、无校验、无硬件流控。串口助手也必须匹配这些参数,且 TX/RX 要交叉连接并共地。
  • HAL_UART_Transmit() 是阻塞式发送。当前一行几十字节、每秒发送一次,开销很小;若改为高频长报文,应考虑超时处理、DMA/中断发送和缓冲区所有权,而不能无限期阻塞在轮询中。
1.3.4.5 例题: 时间打印
  • sprintf 将数字格式化为以 \0 结尾的 C 字符串,strlen 取得实际待发长度。因此不能把缓冲区容量 200 直接作为发送长度,否则会把未初始化的尾部字节也传出。
  • 本工程已经包含 <string.h> 以声明 strlen。建议把 sprintf 改为 snprintf((char *)RTC_Buff, sizeof RTC_Buff, ...),防止格式或数据扩展后覆盖缓冲区。
  • HAL_Delay(1000) 使循环约每秒执行一次;它不是严格的整秒对齐机制,因为两次 UART 发送和格式化也会耗时。若需要秒边沿对齐,应使用 RTC 秒中断、闹钟或比较计数值,而不是仅靠循环延时。
  • 输出中使用 %02d 能补零,例如 12:03:07,比 %d:%d:%d 更符合时间格式。字符串末尾使用 \r\n,可兼容大多数串口终端的换行显示。
1.3.4.6 例题: 日期打印
  • 可用 "Date:20%02d-%02d-%02d, weekday:%d\r\n" 输出本例的日期;其中 Year=23 表示 2023 的后两位,世纪前缀是应用约定,不是 RTC 自动保存的世纪信息。
  • F1 HAL 的星期常量为 RTC_WEEKDAY_SUNDAY=0RTC_WEEKDAY_MONDAY=1RTC_WEEKDAY_TUESDAY=2,依次到星期六 6。应先验证日期与日历一致,再把数值映射为“周日”至“周六”等显示文本。
  • 日期正确保存需要与掉电方案一并设计:仅靠 RTC_DateTypeDef 变量不具备非易失性;使用 BKP 寄存器时还要考虑其容量、版本/校验和、后备电池失效检测,以及用户重新校时后的更新。

依据:课程配套工程 CubeIDE/14 RTCSTM32F103ZET6.pdf 的 VBAT、PC14/PC15 与 LSE 电气特性;STM32中文参考手册.pdf 第 4、5、6、16 章的备份域、BKP、RTC 时钟源说明;工程随附 stm32f1xx_hal_rtc.c 对 F1 HAL 软件日期的实现说明。