嵌入式——RTC实时时钟
Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间BKP(Backup Registers)备份寄存器BKP可用于存储用户应用程序数据。当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT
实时时钟本质是一个定时器,专门用来产生年月日时分秒,这种日期和时间信息
一、Unix时间戳
1.Unix时间戳简介
Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒
时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间

2.UTC/CMT
GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准。
UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致。
3.时间戳转换
C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换。

我们来使用DEVC+来举例使用该函数。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
time_t time_cnt;
struct tm time_date;
char *time_str;
int main(void) {
// 返回当前时间 或者写 time(&time_cnt)
time_cnt = time(NULL);
printf("现在时间的计数器为%d\n", time_cnt);
// 秒计数器转换为伦敦时间函数
time_cnt = 1672588795;
printf("%d\n", time_cnt);
time_date = *gmtime(&time_cnt);
printf("伦敦%d年 ", time_date.tm_year + 1900); // 年
printf("%d月 ", time_date.tm_mon + 1); // 月
printf("%d日 ", time_date.tm_mday); // 日
printf("%d时 ", time_date.tm_hour); // 时
printf("%d分 ", time_date.tm_min); // 分
printf("%d秒 ", time_date.tm_sec); // 秒
printf("星期%d\n", time_date.tm_wday); // 星期
// 秒计数器转换为当地时间函数
time_date = *localtime(&time_cnt);
printf("当地时间%d年 ", time_date.tm_year + 1900); // 年
printf("%d月 ", time_date.tm_mon + 1); // 月
printf("%d日 ", time_date.tm_mday); // 日
printf("%d时 ", time_date.tm_hour); // 时
printf("%d分 ", time_date.tm_min); // 分
printf("%d秒 ", time_date.tm_sec); // 秒
printf("星期%d\n", time_date.tm_wday); // 星期
// 将当地时间转换为秒计数器时间
time_cnt = mktime(&time_date);
printf("当地时间2023年1月1日23时59分55秒星期0的计数器为:%d\n", time_cnt);
// 秒计数器转换为字符串
time_str = ctime(&time_cnt);
printf(time_str);
// 日期时间转换为字符串
time_str = asctime(&time_date);
printf(time_str);
char t[50];
strftime(t, 50, "%H-%M-%S", &time_date);
printf(t);
return 0;
}
打印结果
现在时间的计数器为1754480604
1672588795
伦敦2023年 1月 1日 15时 59分 55秒 星期0
当地时间2023年 1月 1日 23时 59分 55秒 星期0
当地时间2023年1月1日23时59分55秒星期0的计数器为:1672588795
Sun Jan 01 23:59:55 2023
Sun Jan 01 23:59:55 2023
23-59-55
转换状态图如图所示

二、BKP
1.BKP简介
BKP(Backup Registers)备份寄存器
BKP可用于存储用户应用程序数据。当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位
TAMPER引脚产生的侵入事件将所有备份寄存器内容清除
RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
存储RTC时钟校准寄存器
用户数据存储容量:
20字节(中容量和小容量)/ 84字节(大容量和互联型)
2.BKP基本结构

橙色部分为后备区域,BKP处于后备区域,后备区域还有RTC的相关电路,STM32设备后备区域的特点是,当VDD主电源掉电时,后备区域依然可以由VBAT的备用电池供电,当VDD主电源上电时,后备区域供电会由VBAT切换为VDD。
BKP中主要有数据寄存器,控制寄存器,状态寄存器,RTC时钟校准校准寄存器,数据寄存器用来存储数据,每个数据寄存器都是16位的,也就是一个数据寄存器可以存2个字节。
BKP的功能:侵入检测,可以从PC13位置的TAMPER引脚引入一个检测信号,当TAMPER产生上升沿或下降沿时,清除BKP的内容,以保证安全。时钟输出可以把RTC相关的时钟从PC13位置的RTC引脚输出出去,供外部使用,其中输出校准时钟时,配合校准寄存器,可以对RTC的误差进行校准。
三、RTC
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)
整个芯片可以有4个时钟源:
HSE——高速外部时钟信号
HSI——高速内部时钟信号
LSI——低速内部时钟信号
LSE——低速外部时钟信号
高速时钟:一般供内部程序运行和主要外设使用
低速时钟:一般供RTC看门狗使用
2.RTC框图

左边阴影部分是核心的分频和计数计时部分,右边是中断输出使能和NVIC部分,上面是APB1总线读写部分,下面阴影是和PWR关联的部分,意思就是RTC的闹钟可以唤醒设备,退出待机模式。图中的全部阴影部分都处于后备区域,这些电路在主电源掉电后,可以使用备用电池维持工作,这些模块在待机时都会继续维持供电,其他区域待机时不会供电。
首先分频和计数计时部分,这一块的输入时钟是RTCCLK,RTCCLK的来源需要在RCC里进行配置,使用的是LSE振荡器时钟(通常为32.768KHz),由于计数器需要的是1Hz的频率,所以RTCCLK进来首先需要经过RTC预分频器分频,这个分频器由两个寄存器组成,上面为重装寄存器RTC_PRL——写入几就是几+1分频,下面的RTC_DIV是余数寄存器,但它还是计数器的作用,它是一个自减计数器,每来一个输入时钟,DIV值自减一次,自减到0时,DIV输出一个脉冲,产生溢出信号,同时DIV从PRL获取重装值,回到重装值继续自减,右边是计数器,可以看作是时间戳的秒计数器,然后下面有一个等宽的闹钟,这个闹钟是一个定值,当检测到闹钟值与上面的计数器值相等时,闹钟就会响,触发中断,也可以让芯片退出待机模式。
右边就是中断部分,有三个信号可以触发中断,第一个时RTC_Second秒中断,它的来源就是CNT的输入时钟,如果开启这个中断,那么程序就会每秒进一次RTC中断,第二个是RTC_Overflow,溢出中断,当32位计数器计满溢出,会触发一次中断,第三个RTC_Alarm,闹钟中断。
上面APB1总线和APB1接口就是程序读写寄存器的部分,读写寄存器可以通过APB1总线来完成,另外也可以看出 RTC是APB1总线上的设备。
3.RTC基本结构

左边是RTCCLK时钟来源,这一块需要在RCC中配置,三个时钟选择一个当作RTCCLK,之后RTCCLK通过预分频器,对时钟进行分频,余数计数器是一个自减计数器,存储当前的计数值,重装计数器是计数目标,决定分频值,分频之后得到1HZ的秒计数信号,通向32位计数器,一秒自增一次,下面还有一个32位的闹钟值,可以设定闹钟,然后右边由三个信号可以触发中断,分别是秒信号,计数器溢出信号和闹钟信号,三个信号先通过中断输出控制进行中断使能,使能的中断才能通向NVIC然后向CPU申请中断。
4.硬件电路

5.RTC操作注意事项
执行以下操作将使能对BKP和RTC的访问:
设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟
设置PWR_CR的DBP,使能对BKP和RTC的访问
若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1
必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器。
六、RCC库函数
void RCC_LSEConfig(uint8_t RCC_LSE);
作用:配置LSE外部低速时钟,启动LSE使用该函数
void RCC_LSICmd(FunctionalState NewState);
作用:配置LSI内部低速时钟
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
作用:RTCCLK配置,选择RTCCLK的时钟源(配置数据选择器)
void RCC_RTCCLKCmd(FunctionalState NewState);
作用:启动RTCCLK
七、RTC库函数
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
作用:配置中断输出
void RTC_EnterConfigMode(void);
作用:进入配置模式,设置RTC_CRL寄存器中的CNF位为1
void RTC_ExitConfigMode(void);
作用:退出配置模式,CNF位清零
uint32_t RTC_GetCounter(void);
作用:获取CNT计数器的值
void RTC_SetCounter(uint32_t CounterValue);
作用:写入计数器的值
void RTC_SetPrescaler(uint32_t PrescalerValue);
作用:写入预分频器,写入到预分频器的PRL重装寄存器中,配置分频系数
void RTC_SetAlarm(uint32_t AlarmValue);
作用:写入闹钟值
uint32_t RTC_GetDivider(void);
作用:读取预分频器中的DIV余数寄存器
void RTC_WaitForLastTask(void);
作用:等待上次操作完成,等待RTOFF状态位是1
void RTC_WaitForSynchro(void);
作用:等待同步,等待RSF置1
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
void RTC_ClearFlag(uint16_t RTC_FLAG);
ITStatus RTC_GetITStatus(uint16_t RTC_IT);
void RTC_ClearITPendingBit(uint16_t RTC_IT);
八、代码
使用BKP或RTC都要先执行:
①开启PWR和BKP的时钟、
②使用PWR,使能BKP和RTC的访问
1.代码1——读写备份寄存器
步骤:①初始化②写DR③读DR
BKP的初始化步骤:
①开启PWR和BKP的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
②使用PWR,使能BKP和RTC的访问
PWR_BackupAccessCmd(ENABLE);
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
uint16_t ArrayWrite[2] = {0x1234, 0x5678};
uint16_t ArrayRead[2];
uint8_t KeyNum;
int main(void)
{
OLED_Init();
Key_Init();
OLED_ShowString(1, 1, "W:");
OLED_ShowString(2, 1, "R:");
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]);
BKP_WriteBackupRegister(BKP_DR2, ArrayWrite[1]);
OLED_ShowHexNum(1, 3, ArrayWrite[0], 4);
OLED_ShowHexNum(1, 8, ArrayWrite[1], 4);
ArrayWrite[0] ++;
ArrayWrite[1] ++;
}
ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1);
ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2);
OLED_ShowHexNum(2, 3, ArrayRead[0] , 4);
OLED_ShowHexNum(2, 8, ArrayRead[1] , 4);
}
}
实验现象:按下按键,将数组中的数值写入备份寄存器,每按下一次,数值加1,当断开单片机供电,但是不断开VBAT,在单片机再次上电时,依然显示存入的数据。当断开VBAT,后备寄存器数据丢失。
2.代码2——实时时钟
RTC初始化流程:
①开启PWR和BKP的时钟,使用PWR,使能BKP和RTC的访问
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
②启动RTC时钟(使用LSE)
RCC_LSEConfig(RCC_LSE_ON);
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
③配置RTCCLK数据选择器,指定RTCCLK
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
④等待RTC_CRL寄存器中的RSF位,查询RTC_CR寄存器中的RTOFF状态位
RTC_WaitForSynchro();
RTC_WaitForLastTask();
⑤配置预分频器
RTC_SetPrescaler(32768 - 1);
RTC_WaitForLastTask();
⑥配置CNT,给RTC一个初始时间
RTC_SetCounter(1672588795);
RTC_WaitForLastTask();
接下来写两个函数一个设置时间,一个读取时间
读取时间就是读取CNT的值然后转为年月日时分秒放在一个全局数组中
void MyRTC_ReadTime(void) // 将CNT秒数转换为数组的时间
{
time_t time_cnt;
struct tm time_date;
time_cnt = RTC_GetCounter() + 8 * 60 * 60;
time_date = *localtime(&time_cnt);
MyRTC_Time[0] = time_date.tm_year + 1900;
MyRTC_Time[1] = time_date.tm_mon + 1;
MyRTC_Time[2] = time_date.tm_mday;
MyRTC_Time[3] = time_date.tm_hour;
MyRTC_Time[4] = time_date.tm_min;
MyRTC_Time[5] = time_date.tm_sec;
}
设置时间就是把全局数组的年月日时分秒转换为秒数,再写入到CNT
void MyRTC_SetTime(void) // 将数组的时间转换为CNT秒数
{
time_t time_cnt;
struct tm time_date;
time_date.tm_year = MyRTC_Time[0] - 1900;
time_date.tm_mon = MyRTC_Time[1] - 1;
time_date.tm_mday = MyRTC_Time[2];
time_date.tm_hour = MyRTC_Time[3];
time_date.tm_min = MyRTC_Time[4];
time_date.tm_sec = MyRTC_Time[5];
time_cnt = mktime(&time_date) - 8 * 60 * 60;
RTC_SetCounter(time_cnt);
RTC_WaitForLastTask();
}
MyRTC.c模块
#include "stm32f10x.h" // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2025, 8, 5, 0, 43, 55};
void MyRTC_SetTime(void);
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
RCC_LSEConfig(RCC_LSE_ON);
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_SetPrescaler(32768 - 1);
RTC_WaitForLastTask();
MyRTC_SetTime();
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
RTC_WaitForSynchro();
RTC_WaitForLastTask();
}
}
void MyRTC_SetTime(void) // 将数组的时间转换为CNT秒数
{
time_t time_cnt;
struct tm time_date;
time_date.tm_year = MyRTC_Time[0] - 1900;
time_date.tm_mon = MyRTC_Time[1] - 1;
time_date.tm_mday = MyRTC_Time[2];
time_date.tm_hour = MyRTC_Time[3];
time_date.tm_min = MyRTC_Time[4];
time_date.tm_sec = MyRTC_Time[5];
time_cnt = mktime(&time_date) - 8 * 60 * 60;
RTC_SetCounter(time_cnt);
RTC_WaitForLastTask();
}
void MyRTC_ReadTime(void) // 将CNT秒数转换为数组的时间
{
time_t time_cnt;
struct tm time_date;
time_cnt = RTC_GetCounter() + 8 * 60 * 60;
time_date = *localtime(&time_cnt);
MyRTC_Time[0] = time_date.tm_year + 1900;
MyRTC_Time[1] = time_date.tm_mon + 1;
MyRTC_Time[2] = time_date.tm_mday;
MyRTC_Time[3] = time_date.tm_hour;
MyRTC_Time[4] = time_date.tm_min;
MyRTC_Time[5] = time_date.tm_sec;
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"
int main(void)
{
OLED_Init();
MyRTC_Init();
OLED_ShowString(1, 1, "Data:XXXX-XX-XX");
OLED_ShowString(2, 1, "Time:XX:XX:XX");
OLED_ShowString(3, 1, "CNT:");
OLED_ShowString(4, 1, "DIV:");
while (1)
{
MyRTC_ReadTime();
OLED_ShowNum(1, 6, MyRTC_Time[0], 4);
OLED_ShowNum(1, 11, MyRTC_Time[1], 2);
OLED_ShowNum(1, 14, MyRTC_Time[2], 2);
OLED_ShowNum(2, 6, MyRTC_Time[3], 2);
OLED_ShowNum(2, 9, MyRTC_Time[4], 2);
OLED_ShowNum(2, 12, MyRTC_Time[5], 2);
OLED_ShowNum(3, 5, RTC_GetCounter(), 10);
OLED_ShowNum(4, 5, RTC_GetDivider(), 10);
}
}
程序现象,OLED第一行显示数组中的年月日,第二行显示时分秒,第三行显示CNT的值,第四行显示分频值。分频值不断自减,当减为0,CNT的值+1,同时第二行的时间的秒也+1,依次运行。
九、总结
STM32实时时钟(RTC)是集成在STM32微控制器中的独立计时模块,旨在提供高精度、低功耗的时间跟踪功能,即便在主电源关闭或系统进入低功耗模式时仍能持续运行。它通常由外部低速振荡器(LSE,多为32.768kHz晶体)或内部低速振荡器(LSI)提供时钟源,其中LSE因频率稳定度更高而成为主流选择,可确保时间计数的准确性。RTC的核心功能包括秒、分、时、日、星期、月、年等日历信息的实时更新与存储,支持闰年自动修正,能适应不同月份天数的变化,满足长期计时需求。此外,它还具备丰富的中断和事件功能,例如可配置闹钟中断(支持按特定时间点或周期性触发)、周期性中断(如每秒、每分、每时等固定间隔触发)以及溢出中断(当计数器达到最大值时触发),这些功能使得RTC在定时唤醒低功耗系统、触发特定任务执行、记录事件时间戳等场景中发挥关键作用。在硬件设计上,RTC通常拥有独立的电源域,可通过备用电源(如纽扣电池)供电,当主电源掉电时,备用电源能维持RTC的正常运行,避免时间信息丢失,确保系统恢复供电后时间的连续性。同时,STM32的RTC还提供了时间校准机制,允许用户通过软件微调时钟频率,补偿振荡器本身的误差或温度变化带来的漂移,进一步提升计时精度。在实际应用中,RTC广泛用于需要时间戳记录的设备(如数据记录仪、传感器节点)、定时唤醒的低功耗系统(如物联网终端)以及需要实时时钟显示的仪器(如医疗设备、工业控制器)等领域,是STM32微控制器中实现时间管理的核心模块。
更多推荐



所有评论(0)