一、RTC 简介

RTC 的全称是 Real Time Clock,即实时时钟。

从本质上看,RTC 是一个用于记录时间的计数器。它通常以秒为单位计数,可以在系统运行过程中提供当前时间,也可以在 MCU 掉电后依靠后备电源继续运行。

1. RTC 的作用

RTC 常用于这些场景:

  • 记录当前时间。

  • 生成定时唤醒事件。

  • 给日志、故障记录、采样数据添加时间戳。

  • 在系统掉电后继续保持时间。

  • 配合后备寄存器保存少量关键数据。

2. RTC 的主要特性

STM32F1 RTC 具有以下特点:

特性 说明
本质 以固定频率计数的计数器
常用计数频率 1 Hz,也就是 1 秒计数一次
低功耗 可在低功耗场景下继续工作
掉电运行 配合 VBAT 和外部电池可在主电源掉电后继续计时
后备域 RTC 和后备寄存器位于后备域中
独立复位 RTC 核心不受普通系统复位影响

STM32F1 的 RTC 和后续系列差异较大。F1 的 RTC 更像是一个“秒计数器”,它没有直接提供年、月、日、时、分、秒等日历寄存器。实际使用时,通常通过软件算法把秒计数转换为日历时间。

二、F1 RTC 框图理解

STM32F1 RTC 的核心可以简化为:

RTC时钟源 -> RTC预分频器 -> 32位可编程计数器 -> 秒计数/闹钟/中断

F1 RTC 主要由以下部分组成:

模块 作用
RTC 时钟源 为 RTC 提供基础时钟
RTC 预分频器 将输入时钟分频到目标计数频率
32 位计数器 保存当前 RTC 计数值
RTC 控制寄存器 配置中断、状态和访问控制
APB1 接口 CPU 通过 APB1 总线访问 RTC 寄存器

如果把 RTC 配置为 1 Hz 计数,那么 32 位计数器的值就可以理解为从某个起始时间开始累计的总秒数。

三、RTC 时钟源

STM32F1 RTC 可选择多个时钟源。

时钟源 说明 特点
LSE 外部 32.768 kHz 低速晶振 精度高,稳定,推荐使用
LSI 内部低速 RC,常按 40 kHz 估算 不需要外部晶振,但精度较低,有温漂
HSE/128 外部高速晶振经过 128 分频 使用较少,依赖主时钟相关配置

实际项目中,RTC 通常优先选择 LSE。原因是 32.768 kHz 晶振专门用于实时时钟,容易分频得到 1 Hz,而且稳定性比 LSI 更好。

1. 为什么推荐 LSE

LSE 的典型频率为:

32768 Hz

这个频率可以方便地分频为 1 Hz:

32768 / 32768 = 1 Hz

也就是说,配置合适的预分频值后,RTC 计数器可以做到每秒加 1。

2. LSI 的问题

LSI 是芯片内部 RC 时钟,优点是不需要外部晶振,硬件更简单。

但它的缺点也很明显:

  • 频率精度低。

  • 容易受温度、电压影响。

  • 长时间计时误差较大。

如果只是粗略计时或低成本方案,可以使用 LSI;如果需要较准确的时钟,应使用 LSE。

四、后备域与掉电保持

RTC 的一个重要特点是可以在主电源掉电后继续工作。

STM32F1 中,RTC 和后备寄存器通过一个供电开关供电:

VDD 有效 -> 使用 VDD 供电
VDD 失效 -> 使用 VBAT 引脚供电

只要 VBAT 上接有电池或备用电源,RTC 就可以在主系统掉电后继续计时。

1. 后备寄存器

后备寄存器可用于保存少量用户数据。它们和 RTC 同属于后备域,不会因为普通系统复位或电源复位而立即丢失。

常见用途:

  • 标记 RTC 是否已经初始化。

  • 保存设备运行状态。

  • 保存少量配置参数。

  • 保存异常复位标志。

2. 两类复位

F1 RTC 相关复位可以分为两类:

类型 说明
APB1 接口复位 由系统复位影响
RTC 核心复位 由后备域复位影响

这意味着普通系统复位后,RTC 的核心计数值仍可能保持不变。初始化 RTC 时通常需要通过后备寄存器判断是否已经配置过 RTC,避免每次复位都重新设置时间。

五、F1 RTC 关键寄存器

STM32F1 的 RTC 常见寄存器包括 RTC_PRLRTC_CNTRTC_DIVRTC_CRLRTC_ALR 等。

1. RTC_PRL 预分频装载寄存器

RTC_PRL 用于设置 RTC 的预分频值。

当使用 LSE = 32768 Hz,并希望 RTC 计数器每秒增加 1 次时,可将预分频装载值设置为:

RTC_PRL = 32768 - 1 = 32767

因为计数器通常按 PRL + 1 的方式分频。

2. RTC_CNT 计数器寄存器

RTC_CNT 是 32 位可编程计数器,用于保存当前 RTC 计数值。

如果 RTC 配置为 1 Hz,RTC_CNT 就可以保存从设定起点开始经过的总秒数。

例如:

RTC_CNT = 0       -> 起始时刻
RTC_CNT = 3600    -> 起始时刻后 1 小时
RTC_CNT = 86400   -> 起始时刻后 1 天

STM32F1 没有直接的年月日时分秒寄存器,因此需要把 RTC_CNT 与日历算法结合起来使用。

3. RTC_DIV 分频余数寄存器

RTC_DIV 用于读取当前预分频器的计数状态。

实际应用中,普通日历功能不一定需要直接使用 RTC_DIV,但它可以帮助理解 RTC 内部预分频和秒计数之间的关系。

4. RTC_CRL 控制低寄存器

RTC_CRL 是 F1 RTC 配置中非常关键的寄存器。

常见状态位包括:

作用
RTOFF RTC 操作完成标志
CNF 配置模式控制位
RSF 寄存器同步标志
OWF 溢出标志
ALRF 闹钟标志
SECF 秒标志

配置 RTC 寄存器前,需要等待 RTOFF = 1,然后置位 CNF 进入配置模式。配置完成后清除 CNF,再等待 RTOFF = 1 表示写操作完成。

5. RTC_ALR 闹钟寄存器

RTC_ALR 用于设置闹钟值。

RTC_CNT 的值与 RTC_ALR 匹配时,可以产生闹钟事件。F1 的闹钟机制同样基于计数器比较,而不是直接基于年月日时分秒字段。

六、F1 RTC 基本配置流程

STM32F1 RTC 的初始化通常按以下步骤进行。

1. 使能 PWR 和 BKP 时钟

访问 RTC 和后备寄存器前,需要先使能电源控制和备份域相关时钟:

使能 PWR 时钟
使能 BKP 时钟

HAL 宏通常包括:

__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();

2. 使能后备域访问

默认情况下,后备域受写保护。配置 RTC 前需要先打开访问权限:

HAL_PWR_EnableBkUpAccess();

这一步非常重要。如果没有使能后备域访问,后续 RTC 时钟源、预分频器等配置可能无法生效。

3. 配置 RTC 时钟源

常见做法是开启 LSE,并选择 LSE 作为 RTC 时钟源:

开启 LSE
等待 LSE 就绪
选择 RTC 时钟源为 LSE
使能 RTC 时钟

相关寄存器主要是 RCC_BDCR

如果使用 HAL 库,常见函数包括:

HAL_RCC_OscConfig();
HAL_RCCEx_PeriphCLKConfig();
__HAL_RCC_RTC_ENABLE();

4. 进入 RTC 配置模式

进入配置模式前,需要等待上一次 RTC 写操作完成:

等待 RTOFF = 1
设置 CNF = 1

进入配置模式后,才能修改 RTC_PRLRTC_CNTRTC_ALR 等寄存器。

5. 设置预分频值和计数值

初始化时一般先设置 RTC 预分频值,使 RTC 计数频率变为 1 Hz。

使用 LSE 时:

RTC_PRL = 32767

计数值 RTC_CNT 可根据具体需求设置。如果是第一次初始化 RTC,需要把当前日期时间转换为总秒数后写入 RTC_CNT

6. 退出配置模式

配置完成后:

清除 CNF
等待 RTOFF = 1

等待 RTOFF 的目的是确认 RTC 寄存器写入已经完成。

七、F1 RTC 与 HAL 库的关系

在 STM32F1 中,RTC 常常需要结合 HAL 库和寄存器操作来实现完整日历功能。

常见 HAL 函数或宏包括:

函数或宏 作用
HAL_RTC_Init() 初始化 RTC 基本参数
HAL_RTC_MspInit() RTC 底层初始化回调
HAL_RCC_OscConfig() 配置 LSE/LSI 等振荡器
HAL_RCCEx_PeriphCLKConfig() 配置 RTC 外设时钟源
HAL_PWR_EnableBkUpAccess() 使能后备域访问
__HAL_RCC_PWR_CLK_ENABLE() 使能 PWR 时钟
__HAL_RCC_BKP_CLK_ENABLE() 使能 BKP 时钟
__HAL_RCC_RTC_ENABLE() 使能 RTC

需要注意,F1 的日历设置和读取通常不是直接依赖 HAL_RTC_SetTime()HAL_RTC_GetTime() 这类日期时间接口,而是通过操作 RTC 秒计数器,再用软件算法实现 rtc_set_time()rtc_get_time()

八、F1 时间设置与读取

STM32F1 RTC 的关键思想是:

设置时间:日历时间 -> 总秒数 -> 写入 RTC_CNT
读取时间:读取 RTC_CNT -> 总秒数 -> 转换为日历时间

1. 定义日历结构体

工程中通常定义一个日历结构体,用于保存当前时间信息:

typedef struct
{
    uint16_t year;
    uint8_t month;
    uint8_t date;
    uint8_t hour;
    uint8_t min;
    uint8_t sec;
    uint8_t week;
} rtc_calendar_t;

然后定义一个全局变量保存当前日历:

rtc_calendar_t calendar;

2. 判断闰年

计算日期时必须判断闰年。常用规则:

能被 4 整除且不能被 100 整除 -> 闰年
能被 400 整除 -> 闰年
其他情况 -> 平年

函数形式可以设计为:

uint8_t rtc_is_leap_year(uint16_t year);

3. 日历时间转总秒数

设置 RTC 时间时,需要把年月日时分秒转换为总秒数。

函数形式可以设计为:

long rtc_date2sec(uint16_t year,
                  uint8_t month,
                  uint8_t date,
                  uint8_t hour,
                  uint8_t min,
                  uint8_t sec);

转换思路:

  1. 累加从起始年份到目标年份前一年的总天数。

  2. 根据平年/闰年累加目标年份中目标月份之前的总天数。

  3. 加上当前月份已经过去的天数。

  4. 将天数转换为秒数。

  5. 加上时、分、秒对应的秒数。

4. 总秒数转日历时间

读取 RTC 时间时,需要把 RTC_CNT 中的总秒数转换为年月日时分秒。

函数形式可以设计为:

void rtc_get_time(void);

转换思路:

  1. 读取 RTC 当前总秒数。

  2. 计算已经过去的总天数和当天剩余秒数。

  3. 根据年天数逐年扣减,得到年份。

  4. 根据月份天数逐月扣减,得到月份和日期。

  5. 根据当天剩余秒数计算时、分、秒。

  6. 计算星期并更新全局日历结构体。

5. 计算星期

星期通常可以通过某个基准日期推算。

函数形式可以设计为:

uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t date);

计算星期时要明确返回值定义,例如:

0 -> 星期日
1 -> 星期一
...
6 -> 星期六

如果项目中 UI 或串口输出使用 1 ~ 7 表示星期,也要统一转换,避免显示混乱。

九、RTC 初始化状态判断

RTC 的初始化不应该每次上电或复位都重新执行完整时间设置,否则会把已经保持的时间覆盖掉。

常见做法是使用后备寄存器保存一个标志值:

后备寄存器标志有效 -> RTC 已经初始化过,只同步寄存器并读取时间
后备寄存器标志无效 -> 第一次初始化 RTC,配置时钟源、预分频器并设置初始时间

这种方式可以区分普通系统复位和 RTC 第一次初始化。

需要重新初始化 RTC 的典型情况包括:

  • 后备域被复位。

  • VBAT 电池断电。

  • LSE 启动失败后切换时钟源。

  • 用户主动重新设置时间。

Logo

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

更多推荐