文章目录
RTC简介
实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作将使能对后备寄存器和RTC的访问:
- 设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
- 设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。
主要特性
在分析主要特性之前我们要先回顾以下,时钟树,只看这下半部分就可以

可以看到RTC可由HSE、LSE和LSI三个时钟源提供时钟。显然LSE只为RTC提供时钟,所以LSE是我们的首选,如果LSE不起振,那么可以选择LSI或HSE,当然在这两者之间肯定也是优先选择LSI。
在简介中我们得知,RTC是一个独立计数器,并且可以提供时钟日历的功能。在这里我们需要先了解一个概念------Unix时间戳
Unix时间戳
Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒
- 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
- 世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当
下面是RTC的内部框图

由此可以看到,中间部分,RTC内部只有一个32位的计数器,没有其他寄存器,意味着日期时间是我们同各国时间戳进行转化。所以他只能存储一个时间戳。那么在这里我们就需要考虑预分频器了。
我们使用RTC_CNT计时,那么肯定就是需要一秒钟加1,这里就需要1HZ的频率。所以在这里我们分频系数要足够大。如果使用LSE作为时钟源,那么我们就要分频系数为32768 - 1。如果是HSE提供的好就需要更高的预分频系数。
所以分频系数最高为 2 20 2^{20} 220。
-
2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟频率必须小于PCLK1时钟频率的四分之一以上。这个是厂商的设计,不需要我们关心。但是也容易理解,因为两个时钟源的频率不一致,如果CPU要访问RTC的话,就需要跨时域进行访问,那么要保证这两部分的同步问题,才能正常访问,所以在手册中有了硬性的规定)
-
2个独立的复位类型
- APB1接口由系统复位
- RTC核心(预分频器、闹钟、计数器、和分频器)只能由后备域复位(具体看参考手册RCC章节)
-
3个专门的可屏蔽中断
-
闹钟中断------用来产生一个软件可编程的闹钟
-
秒中断------用来产生一个可编程的周期性中断信号(最长可达1s)
-
溢出中断,指内部可编程计数器溢出并回转为0的状态
前两个中断可以很好的帮助我们完成一些定时任务,比如每隔一段时间进行一次传感器数据采集,或者通过对时间戳和日期时间进行相互转换,可以设置更长时间的定时任务。但是在在这里需要声明一下,闹钟只会执行一次,如果需要例如一天执行一次的话,那就需要在处理完当次的中断后,再设置一个闹钟中断。
关于溢出中断,我们基本不需要操心,基本上不会出现,计数器满至少距离我们还很遥远,如果是有符号数,好像是在2038年会出现溢出问题,到时候一些老旧设备可能会因为这个出现一系列问题。但是在MCU的场景下存储的是无符号,可能还有个几十年上百年的时间吧。
-
复位过程
除了RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器外,所有的系统寄存器都由系统复位或电源复位进行异步复位。RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器仅能通过备份域复位信号复位
读RTC寄存器
RTC核完全独立于RTC APB1接口。
软件通过APB1接口访问RTC的预分频值、计数器值和闹钟值。但是,相关的可读寄存器只在与RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。RTC标志也是如此的。这意味着,如果APB1接口曾经被关闭,而读操作又是在刚刚重新开启APB1之后,则在第一次的内部寄存器更新之前,从APB1上读出的RTC寄存器数值可能被破坏了(通常读到0)。下述几种情况下能够发生这种情形:
- 发生系统复位或电源复位
- 系统刚从待机模式唤醒
- 系统刚从停机模式唤醒
所有以上情况中,APB1接口被禁止时(复位、无时钟或断电)RTC核仍保持运行状态。因此,若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置'1'。
在这里我不需要我们手动操作寄存器,SPL库中提供了相关函数,包括手册中提到的一些其他寄存器操作问题,也都给我们封装好了函数,我们只需要调用即可,这部分的操作还是比较简单的。
配置RTC寄存器
必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。另外,对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是'1'时,才可以写入RTC寄存器。
配置过程:
-
查询RTOFF位,直到RTOFF的值变为'1'
-
置CNF值为1,进入配置模式
-
对一个或多个RTC寄存器进行写操作
-
清除CNF标志位,退出配置模式
-
查询RTOFF,直至RTOFF位变为'1'以确认写操作已经完成。
仅当CNF标志位被清除时,写操作才能进行,这个过程至少需要3个RTCCLK周期。
实验:输出日期和时间
我们已经了解了RTC内部存储了一个时间戳,并且在学习C语言的时候,了解过一个头文件<time.h>。这个头文件中实际上就是通过时间戳进行日期转换的
c
#include <time.h>
int main(){
time_t t = time(NULL);
}
相信在学习C语言阶段,如果使用随机数的话,需要一个种子,一般都是通过time(NULL)来完成的。这个函数实际上就是一个时间戳。当然在该库中存在如何将时间戳转换成时间日期的。所以我们可以读取RTC的时间戳,然后通过time.h 中的相关函数输出一个时间戳。当然我们只做简单的演示,具体time.h中都有什么样的玩法,还有待大家去发掘,网上搜一搜或查一下creference手册就清晰了。
代码
这里就只写RTC的代码了,其他的我们在之前的文章中都是经常用到的。
c
// RTC.h
#ifndef __RTC_H
#define __RTC_H
#include "stm32f10x.h"
void init_RTC(void);
#endif
c
// RTC.c
#include "RTC.h"
void init_RTC(void){
// 开启时钟(这三步是手册中明确规定的)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
// 开启LSE时钟
RCC_LSEConfig(RCC_LSE_ON);
// 等待时钟开启
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
// RTC时钟源选择
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
// 使能
RCC_RTCCLKCmd(ENABLE);
// 等待同步
RTC_WaitForSynchro();
// 等待上次写完成
RTC_WaitForLastTask();
// 配置预分频器
RTC_SetPrescaler(32768 - 1);
// 等待写完成
RTC_WaitForLastTask();
// 写入时间戳
RTC_SetCounter(1767196790);
RTC_WaitForLastTask();
}
c
// main.c
#include "stm32f10x.h"
#include "OLED.h"
#include "Delay.h"
#include "../RTC.h"
#include "USART1.h"
#include <time.h>
#include <string.h>
#include <stdio.h>
char buffer[24];
typedef struct TD{
uint8_t mon;
uint8_t day;
char time[10];
uint16_t year;
//char week[8];
}td;
int main(){
init_USART1();
init_RTC();
time_t tim;
while(1){
tim = RTC_GetCounter() + 8 * 60 * 60; // 默认是伦敦时间,转换成北京时间要加8小时
USART1_SendStr(asctime(localtime(&tim)));
Delay_S(1);
}
}
主程序代码逻辑并不复杂。里边可能出现了一些其他的声明等,这部分是我之前用OLED屏幕显示设置的。在gitee仓库的代码中保留了OLED显示的逻辑。因为在博客中截屏更方便,所以我改用了USART,所以对于那些变量忽视即可。
运行结果

补充
有我们的程序现象可知,我们需要自己传入一个时间戳,程序才能帮我们解析出时间。所以在我们写程序的角度来讲,基本上不可能写出来搞好与现实时间完全对应的程序。在这个小实验中我们只是学习演示出来就可以了。如果在我们做项目的时候,我们需要一个较为精确的时间怎么办呢。我的建议时,加一个时间校准的机制,可以手动传输,也可以通过其他网络设备定时发送校验。
