【STM32】RTC实时时钟
【STM32】RTC实时时钟
- 一、Unix时间戳
- 1.1 什么是时间戳
- 1.2 时间戳转换
- 1.3 C标准库<time.h>
- (1) time_t time(time_t*)
- (2) struct tm* gmtime(const time_t*)
- (3) struct tm* localtime(const time_t*)
- (4) time_t mktime(struct tm*)
- (5) char* ctime(const time_t*)
- (6) char* asctime(const struct tm*)
- (7) size_t strftime(char*, size_t, const char*, const struct tm*)
- 二、BKP备份寄存器
- 2.1 BKP简介
- 2.2 BKP基本结构
- 三、RTC实时时钟
- 3.1 RTC简介
- 3.2 RTC框图
- 3.3 RTC基本结构
- 3.4 硬件电路
- 3.5 RTC 操作注意事项
- 四、其他
一、Unix时间戳
1.1 什么是时间戳
- Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒
- 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
- 世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间
时间戳是标识某一时刻的数值或字符串,通常以“从固定基准时间(如1970-01-01 00:00:00 UTC)起的秒数”或“人类可读的日期时间文本”形式存在,用于记录、同步或验证时间先后。
ps(了解):
GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准
UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致
1.2 时间戳转换
C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换
1.3 C标准库<time.h>
以下是time.h
头文件中各个时间戳转换相关函数的使用示例及详细解释:
(1) time_t time(time_t*)
该函数用于获取系统当前的时间,返回值是从1970年1月1日00:00:00 UTC 到当前时间所经过的秒数。
#include <stdio.h>
#include <time.h>int main() {time_t current_time;// 获取当前时间current_time = time(NULL); printf("从1970年1月1日00:00:00 UTC到现在经过的秒数为: %ld\n", (long)current_time);return 0;
}
解释:
time(NULL)
表示直接获取当前时间戳,参数传入NULL
,表示不需要将时间戳存储到传入的指针指向的位置。函数返回值赋给current_time
,然后通过printf
打印出来。
(2) struct tm* gmtime(const time_t*)
将time_t
类型表示的秒计数器转换为格林尼治时间的struct tm
结构体形式。
struct tm {int tm_sec; /* 秒,范围从 0 到 59 */int tm_min; /* 分,范围从 0 到 59 */int tm_hour; /* 小时,范围从 0 到 23 */int tm_mday; /* 一月中的第几天,范围从 1 到 31 */int tm_mon; /* 月份,范围从 0 到 11 */int tm_year; /* 自 1900 起的年数 */int tm_wday; /* 一周中的第几天,范围从 0 到 6 */int tm_yday; /* 一年中的第几天,范围从 0 到 365 */int tm_isdst; /* 夏令时 */
};
#include <stdio.h>
#include <time.h>int main() {time_t current_time = time(NULL);struct tm *gmt_time = gmtime(¤t_time);printf("格林尼治时间:%d-%02d-%02d %02d:%02d:%02d\n", gmt_time->tm_year + 1900, gmt_time->tm_mon + 1, gmt_time->tm_mday,gmt_time->tm_hour, gmt_time->tm_min, gmt_time->tm_sec);return 0;
}
解释:
- 先通过
time
函数获取当前时间戳current_time
。 - 然后将
current_time
的地址传入gmtime
函数,gmtime
函数会将秒数转换为格林尼治时间的struct tm
结构体,并返回指向该结构体的指针,赋值给gmt_time
。 struct tm
结构体中,tm_year
是从1900年开始的年份偏移,tm_mon
是从0开始的月份偏移,所以在打印时需要进行相应的处理。
(3) struct tm* localtime(const time_t*)
将time_t
类型表示的秒计数器转换为本地时间的struct tm
结构体形式。
#include <stdio.h>
#include <time.h>int main() {time_t current_time = time(NULL);struct tm *local_time = localtime(¤t_time);printf("本地时间:%d-%02d-%02d %02d:%02d:%02d\n", local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, local_time->tm_min, local_time->tm_sec);return 0;
}
解释:
与gmtime
函数类似,不同之处在于localtime
会根据系统设置的时区信息,将秒数转换为本地时间对应的struct tm
结构体 。
(4) time_t mktime(struct tm*)
将struct tm
结构体表示的日期时间转换为time_t
类型的秒计数器(以本地时间为准)。
#include <stdio.h>
#include <time.h>int main() {struct tm custom_time = {.tm_year = 2024 - 1900, .tm_mon = 10 - 1, .tm_mday = 1, .tm_hour = 12, .tm_min = 30, .tm_sec = 0};time_t seconds = mktime(&custom_time);printf("自定义时间对应的秒数为: %ld\n", (long)seconds);return 0;
}
解释:
- 先定义一个
struct tm
结构体custom_time
,并设置好各个成员变量,注意年份和月份的偏移。 - 然后将
custom_time
的地址传入mktime
函数,mktime
函数会根据本地时区,将结构体表示的时间转换为从1970年1月1日00:00:00 UTC 开始的秒数,返回值赋给seconds
并打印。
(5) char* ctime(const time_t*)
将time_t
类型表示的秒计数器转换为默认格式的字符串。
#include <stdio.h>
#include <time.h>int main() {time_t current_time = time(NULL);char *time_str = ctime(¤t_time);printf("当前时间字符串: %s", time_str);return 0;
}
解释:
- 先获取当前时间戳
current_time
。 - 再将
current_time
的地址传入ctime
函数,ctime
函数会将秒数转换为默认格式的字符串,例如Wed Oct 18 15:30:00 2023\n
,并返回指向该字符串的指针,最后打印输出。
(6) char* asctime(const struct tm*)
将struct tm
结构体表示的日期时间转换为默认格式的字符串。
#include <stdio.h>
#include <time.h>int main() {time_t current_time = time(NULL);struct tm *local_time = localtime(¤t_time);char *time_str = asctime(local_time);printf("本地时间字符串: %s", time_str);return 0;
}
解释:
- 先获取当前时间戳并转换为本地时间的
struct tm
结构体。 - 然后将
local_time
指针传入asctime
函数,asctime
函数会将struct tm
结构体表示的时间转换为默认格式的字符串 ,并返回指向该字符串的指针,最后打印出来。
(7) size_t strftime(char*, size_t, const char*, const struct tm*)
将struct tm
结构体表示的日期时间按照自定义格式转换为字符串。
#include <stdio.h>
#include <time.h>int main() {time_t current_time = time(NULL);struct tm *local_time = localtime(¤t_time);char buffer[100];strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", local_time);printf("自定义格式的本地时间字符串: %s\n", buffer);return 0;
}
解释:
- 同样先获取当前时间戳并转换为本地时间的
struct tm
结构体。 - 定义一个字符数组
buffer
用于存储转换后的字符串。 strftime
函数的第一个参数是目标字符串缓冲区指针,第二个参数是缓冲区的大小,第三个参数是自定义的格式字符串,%Y
表示四位数的年份,%m
表示两位数的月份,%d
表示两位数的日期,%H
表示小时(24小时制),%M
表示分钟,%S
表示秒,第四个参数是指向struct tm
结构体的指针。函数会按照指定格式将时间转换为字符串存入buffer
,最后打印输出。
二、BKP备份寄存器
2.1 BKP简介
- BKP(Backup Registers)备份寄存器
- BKP 可用于存储用户应用程序数据。当 VDD(2.0~3.6V)电源被切断,他们仍然由 VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位
- TAMPER引脚产生的侵入事件将所有备份寄存器内容清除
- RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
- 存储RTC时钟校准寄存器
- 用户数据存储容量:
20字节(中容量和小容量)/ 84字节(大容量和互联型)
2.2 BKP基本结构
- VBAT(备用电池输入):当主电源(VDD)掉电时,VBAT 为备份域供电,确保备份寄存器(DR)、RTC 等模块在主电源中断后仍能工作或保存数据(实现 “掉电不丢失”)。
- TAMPER(入侵检测输入):外部入侵检测信号输入,可触发 “入侵事件”(如检测到外部引脚被非法触碰),进而触发备份域的中断或数据保护逻辑。
- RTC(时钟输出 / 输入):与实时时钟(RTC)模块交互,既接收 RTC 的时钟信号用于时间同步,也可将 RTC 的时钟(如秒脉冲、闹钟脉冲)输出,为外部设备提供时间基准。
- 控制寄存器:配置备份域的工作模式(如入侵检测的触发条件、时钟输出的使能等)。
- 状态寄存器:反馈备份域的运行状态(如是否检测到入侵、VBAT 供电是否正常等)。
- RTC 时钟校准寄存器:用于微调 RTC 的时钟精度,补偿晶体振荡的误差,让 RTC 计时更准确。
三、RTC实时时钟
3.1 RTC简介
- RTC(Real Time Clock)实时时钟
- RTC是一个独立的定时器,可为系统提供时钟和日历的功能
- RTC 和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助 VBAT(1.8~3.6V)供电继续走时
- 32位的可编程计数器,可对应Unix时间戳的秒计数器
- 20位的可编程预分频器,可适配不同频率的输入时钟
- 可选择三种RTC时钟源:
HSE时钟除以128(通常为8MHz/128)
LSE振荡器时钟(通常为32.768KHz)
LSI振荡器时钟(40KHz)
3.2 RTC框图
1. RTCCLK
:RTC 的输入时钟源(通常来自外部 32.768kHz 晶振、内部低速时钟 LSI,或其他高速时钟分频后得到),是 RTC 计时的 “基准脉冲”。
2. RTC 预分频器(分频产生 “秒脉冲”)
预分频器由 RTC_PRL(预分频装载寄存器) 和 RTC_DIV(预分频余数寄存器) 组成,作用是将 RTCCLK 分频为1Hz 的秒脉冲(实现 “秒计数”):
- RTC_PRL:存储预分频系数(比如 32.768kHz 时钟时,PRL 通常设为 32767,对应分频比 32768)。
- RTC_DIV:实时递减计数器,从 RTC_PRL 的值开始自减,当减到 0 时,产生一个 “溢出脉冲”(即TR_CLK),同时自动重新加载 RTC_PRL 的值,循环往复。
- 上升沿触发:RTC_DIV 的递减由 RTCCLK 的上升沿驱动,保证分频节奏与时钟源同步。
3. 32 位可编程计数器与闹钟(计时核心)
- RTC_CNT(32 位计数器):
接收预分频器产生的 TR_CLK(1Hz 脉冲),每收到一个脉冲就加 1,因此 RTC_CNT 的值代表 “从基准时间(如 1970-01-01 00:00:00)起经过的秒数”。
标注 “待机时维持供电”:待机模式下,RTC_CNT 仍靠备份域供电,计时不中断(这是 RTC 掉电仍能计时的关键)。 - RTC_ALR(闹钟寄存器):
存储用户设置的 “闹钟时间”(对应 RTC_CNT 的某个计数值)。
内部比较器 “=”:实时比较 RTC_CNT 与 RTC_ALR 的值,当两者相等时,产生 RTC_Alarm 信号(触发闹钟)。
4. 中断与待机唤醒控制
RTC_Second
:秒脉冲,每秒产生一次,用于秒级计时或同步。RTC_Overflow
:计数器溢出,32位计数器计满后触发(因周期极长,实际少用)。RTC_Alarm
:闹钟匹配,当RTC计数值与预设闹钟值一致时触发,用于定时唤醒或事件触发。- 待机唤醒(WKP_STDBY):
当 MCU 处于待机模式时,RTC_Alarm 信号可通过 WKP_STDBY 逻辑,直接触发 “退出待机模式”(无需 NVIC 参与,因为待机时 NVIC 断电)。
WKUP pin:外部唤醒引脚,也可与 RTC_Alarm 一起,通过或门 WKP_STDBY 触发待机唤醒。
3.3 RTC基本结构
整体逻辑总结:
RTC 工作流程:选择时钟源 → 预分频得到 1Hz 秒脉冲 → CNT 秒计数 → ALR 闹钟匹配 → 事件触发中断 / 唤醒。这套架构让 STM32 既能实现精准的长期计时,又能通过闹钟功能实现 “定时任务、低功耗唤醒”,是物联网、工业控制等场景中 “时间同步、定时触发” 的核心。
3.4 硬件电路
3.5 RTC 操作注意事项
要详细分析 STM32 RTC 操作及关联的标准库函数,需结合 STM32 标准外设库(StdPeriph) 的 API 与 RTC 硬件特性展开,以下分“使能访问、同步等待、配置模式、写操作原子性”四部分拆解:
一、使能 BKP 和 RTC 访问(时钟与权限解锁)
- 硬件逻辑
RTC 属于备份域(BKP),需先使能 PWR 时钟(电源管理)和 BKP 时钟,再解锁备份域访问权限。
标准库函数
// 1. 使能 PWR 和 BKP 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);// 2. 使能对 BKP 和 RTC 的访问(设置 PWR_CR 的 DBP 位)
PWR_BackupAccessCmd(ENABLE);
RCC_APB1PeriphClockCmd
:属于 RCC(复位与时钟控制) 模块的函数,用于控制 APB1 总线上外设的时钟使能/禁用。这里同时使能PWR
(电源管理)和BKP
(备份域)的时钟。PWR_BackupAccessCmd
:属于 PWR(电源) 模块的函数,用于控制“备份域访问权限”。置位后,才能对 BKP 和 RTC 的寄存器进行读写。
二、读取 RTC 寄存器前的同步等待(RSF 标志)
- 硬件逻辑
RTC 内部时钟(RTCCLK)与 APB1 总线时钟不同步,若 APB1 接口曾被禁止,读取 RTC 寄存器前需等待RTC_CRL
的RSF
位(寄存器同步标志)置 1,确保数据有效。
标准库函数
// 等待 RTC_CRL 的 RSF 位被硬件置 1(同步完成)
RTC_WaitForSynchro(); //等待同步
三、进入 RTC 配置模式(CNF 位控制)
- 硬件逻辑
要修改RTC_PRL
(预分频)、RTC_CNT
(计数器)、RTC_ALR
(闹钟)等核心寄存器,必须先将RTC_CRL
的CNF
位置 1,使 RTC 进入配置模式。
标准库函数
// 进入 RTC 配置模式(设置 RTC_CRL 的 CNF 位)
RTC_EnterConfigMode();
RTC_EnterConfigMode
:属于 RTC 模块的函数,内部操作是置位RTC_CRL
的CNF
位,使 RTC 进入配置模式,允许对核心寄存器写入。- 这一步通常不需要自己调用,在标准库函数中
RTC_SetPrescaler
RTC_SetCounter
RTC_SetAlarm
RTC_SetTime()
这几个函数内部通过 RTC_EnterConfigMode() 和 RTC_ExitConfigMode(),间接完成了 “设置 CNF 位进入配置模式 → 写寄存器 → 清除 CNF 位退出配置模式” 的流程
四、RTC 写操作的原子性等待(RTOFF 标志)
- 硬件逻辑
RTC 写操作是“原子操作”(需完整执行),两次写操作之间必须等待前一次完成。通过查询RTC_CRL
的RTOFF
位(寄存器操作完成标志),仅当RTOFF=1
时,才能发起新的写操作。
标准库函数
// 等待 RTOFF 位为 1(前一次写操作完成)
RTC_WaitForLastTask(); //等待上一次操作完成
四、其他
BKP和RTC的关系
- 供电方面:BKP和RTC都位于后备区域,当主电源VDD被切断后,它们都由VBAT(备用电池电源)维持供电,确保在主电源掉电时能继续工作或保存数据。
- 功能协作方面
数据存储:RTC在运行过程中需要存储一些数据,如校准值、时间计数等,这些数据可存储在BKP中。因为BKP具有在主电源断电、系统复位或电源复位时数据不丢失的特性,能为RTC数据提供稳定的存储环境。
时钟输出:BKP可配置为输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲,为外部设备提供时间基准信号。- 配置方面:对BKP和RTC的访问使能需要先设置RCC_APB1ENR寄存器使能PWR和BKP时钟,再设置PWR_CR寄存器的DBP位,才能对它们进行访问和操作。
时钟输出解释:外部设备(比如另一个单片机、计数器、示波器等)就可以通过这个引脚,获取 RTC 的时间基准。举例来说,用示波器接这个引脚,就能看到每秒一次的脉冲,直观确认 RTC 是否在正常计时;或者让另一个设备通过这个脉冲同步自己的动作,实现多设备的时间对齐。
简单说,就是 BKP 帮 RTC 把内部的时间信号 “传出去”,给外部设备当参考。
写BPK_DR1时,写完一个8位,指针会自增到下一位吗?
在 STM32 中,BKP_DR(备份数据寄存器) 是 8 位寄存器,每个寄存器(如 BKP_DR1、BKP_DR2……)独立存储 8 位数据,且没有自动递增的指针。
当你写入 BKP_DR1 时,数据仅存储在 DR1 中;若要写入下一个寄存器(如 DR2),必须显式指定地址(如通过寄存器地址直接访问 DR2),不会因为写了 DR1 就自动切换到 DR2。
那如果我4位先写到DR1,再写一个4位到DR1,会覆盖吗?
会覆盖。
BKP_DR 寄存器是 8 位寄存器,每次写入操作都会覆盖整个寄存器的 8 位数据。即使你分两次写入 4 位数据(比如第一次写低 4 位,第二次写高 4 位),第二次写入时,新的 4 位数据会直接覆盖寄存器中原有的 8 位内容(包括之前的低 4 位),而不是 “拼接” 或 “补充”。
区分RCC与RTC
RCC 是复位与时钟控制,为 STM32 各模块提供时钟并管理复位 。它有 HSI、HSE、LSI、LSE 等多种时钟源可选,能灵活配置各外设时钟,关乎系统性能与功耗。
RTC 即实时时钟,用于提供精确时间日期 。靠 LSE 等时钟源驱动,掉电后靠 VBAT 维持计时,常用于定时任务、时间戳记录。
打个比方:如果把芯片比作一个工厂,RCC 就是 “电力和时间调度中心”—— 决定用哪个 “发电机”(时钟源)供电,怎么分配电力(时钟)给各个车间(外设),以及在机器出问题时如何重启(复位)。而 RTC 更像工厂里的 “精密闹钟”,专门负责时间计数,它的时钟通常是由 RCC 从低速时钟源(比如 LSE 晶振)分配过来的。
有关【STM32】RTC实时时钟 就到这,希望对你有所帮助,感谢观看!
码文不易,留个赞再走吧~