第11章 RTC实时时钟
实时时钟本质上是一个定时器,但是这个定时器是专门用来产生年月日时分秒,这种日期和时间信息的。学会了RTC实时时钟,就可以在STM32内部拥有一个独立运行的钟表。想要记录或读取日期和时间,就可以通过操作RTC来实现。RTC这个外设比较特殊,它和备份寄存器BKP、电源控制PWR、这两章关联性比较强,在RTC这一章,BKP和PWR会经常来串门。
11.1 Unix时间戳
11.1.1 Unix时间戳
Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒;
时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量;
世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间。
11.1.2 UTC/GMT
GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准;
GMT是以前的时间标准,这是因为GMT有一个棘手的问题,就是地球自转一周的时间,其实不是固定的,由于潮汐力、地球活动等原因,地球目前是越来越慢的,这时再根据一天的时间来定义时间基准,这个时间基准就是在不断变化的。
UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致.
11.1.3 时间戳转换
C 标准库 – | 菜鸟教程 (runoob.com)https://www.runoob.com/cprogramming/c-standard-library-time-h.html
C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换。
函数 | 作用 |
time_t time(time_t*); | 获取系统时钟 |
struct tm* gmtime(const time_t*); | 秒计数器转换为日期时间(格林尼治时间) |
struct tm* localtime(const time_t*); | 秒计数器转换为日期时间(当地时间) |
time_t mktime(struct tm*); | 日期时间转换为秒计数器(当地时间) |
char* ctime(const time_t*); | 秒计数器转换为字符串(默认格式) |
char* asctime(const struct tm*); | 日期时间转换为字符串(默认格式) |
size_t strftime(char*, size_t, const char*, const struct tm*); | 日期时间转换为字符串(自定义格式) |
11.2 BKP备份寄存器和RTC实时时钟
11.2.1 BKP备份寄存器
11.2.1.1 BKP简介
BKP(Backup Registers)备份寄存器;
BKP可用于存储用户应用程序数据。当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位;
VBAT(V Battery):备用电池电源;
上图中VSS_i和VDD_i(i=1~3)是数字部分电路的供电, VSSA和VDDA是内部模拟部分电路的供电。这四组以VDD开头的供电,都是系统的主电源。在正常使用STM32时,这四组供电,全部都需要接到3.3V的电源上。
VBAT:就是备用电池供电引脚,如果要使用STM32内部的BKP和RTC实时时钟,这个引脚就必须接备用电池,用来维持BKP和RTC,在VDD主电源掉电后的供电。这里备用电池只有一根正极的供电引脚,接电池时,电池正极接到VBAT,电池负极和主电源的负极接在一起,共地就行了。如果VDD断电,VBAT也没电,那BKP里的数据就会清零,因为BKP本质上就是RAM存储器,没有掉电不丢失的能力。
TAMPER引脚产生的侵入事件将所有备份寄存器内容清除;
TAMPER是一个接到STM32外部的引脚,是一个安全保障设计,比如做一个安全系数非常高的设备,设备需要有防拆功能,然后BKP里也存储了一些敏感数据,这些数据不能被别人窃取或篡改,那就可以使用这个TAMPER引脚的侵入检测功能。设计电路时,TAMPER引脚可以先加一个默认的上拉或下拉电阻,然后引一根线,到设备外壳的防拆开关或触点,别人一拆开设备,触发开关,就会在TAMPER引脚产生上升沿或者下降沿,这样STM32就检测到侵入事件了,这时BKP的数据就会自动清零,并且申请中断,在中断里,还可以继续保护设备,比如清除其它存储器数据,然后设备锁死,这样来保证设备的安全。另外,主电源断电后,侵入检测任然有效,这样即使设备关机,也能防拆,这就是TAMPER侵入检测的功能。
RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲;
存储RTC时钟校准寄存器;
用户数据存储容量:
20字节(中容量和小容量)/ 84字节(大容量和互联型)。
我们使用的芯片BKP是20字节,BKP一般只能用来存储少量的参数。
11.2.1.2 BKP基本结构
图中橙色部分我们可以叫做后备区域, BKP处于后备区域,但后备区域不止有BKP、还有RTC的相关电路,也位于后备区域,STM32后备区域的特性就是,当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电,当VDD主电源上电时,后备区域供电会由VBAT切换到VDD,也即是主电源有电时,VBAT不会用到,这样可以节省电池电量。BKP是位于后备区域的,BKP主要有数据寄存器、控制寄存器、状态寄存器和RTC实时时钟校准寄存器这些东西。其中数据寄存器是主要部分,用来存储数据的,每个数据寄存器都是16位的,也就是,一个数据寄存器可以存2个字节,那对于中容量和小容量的设备,里面有DR1、DR2,一直到DR10,总共10个数据寄存器。那一个寄存器存2个字节,所以容量是20个字节。对于大容量和互联型设备,里面除了DR1到DR10,还有DR11到DR12,一直到DR42,总共42个数据寄存器,容量是84个字节。
侵入检测,可以从PC13位置的TAMPER引脚引入一个检测信号,当TAMPER产生上升沿或下降沿时,清除BKP所有的内容,以保证安全。
时钟输出可以把RTC的相关时钟,从PC13位置的RTC引脚输出出去,供外部使用,其中输出校准时钟时,再配合校准寄存器,可以对RTC的误差进行校准。
11.2.2 RTC实时时钟
11.2.2.1 RTC简介
RTC(Real Time Clock)实时时钟;
RTC是一个独立的定时器,可为系统提供时钟和日历的功能;
RTC实时时钟,一般就指提供年月日时分秒这种日期时间信息的计时装置。在51单片机时有DS1302这个芯片,DS1302是外置的RTC芯片,这个芯片可以独立计时,我们需要设置时间或读取时间,就通过通信协议向它发送或接收数据来完成。在我们STM32内部,有这个RTC的外设,所以STM32可以在内部直接实现RTC的功能,这样就不用外挂RTC芯片了,当然RTC芯片所必要的元件,比如备用电池、RTC晶振这些东西就要接到STM32上了。
RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时;
为了保持时钟能一直连续运行不出错,在主电源断电后,RTC走时肯定不能停下来,在系统复位时,RTC时间值肯定也不能复位。为了实现这些功能,VBAT接上备用电池就是必须的了。主电源断电后,VBAT的电池可以继续维持BKP和RTC的运行。
32位的可编程计数器,可对应Unix时间戳的秒计数器;
这个32位可编程计数器就是对应的是时间戳里的秒计数器,在读取时间时,我们先得到这个秒数,然后使用time.h模块里的localtime函数,就能立刻知道年月日时分秒的信息了。在写入时间时,我们先填充年月日时分秒信息到struct tm结构体,然后用mktime函数,得到秒数,再先写入到32位计数器即可。这样,操作这个秒计数器的思路就不是很清晰了。得益于时间戳的设计,这个硬件电路就得到了极大的简化,要想实现年月日时分秒的计时,只需要一个32位的秒计数器即可。
20位的可编程预分频器,可适配不同频率的输入时钟;
32位的秒计数器,1秒需要自增一次,所以这个地方驱动计数器的时钟,需要是一个1Hz的信号,但是实际提供给RTC模块的时钟,也就是RTCCLK,一般频率都比较高,所以,显然,我们需要在这之间,加一个分频器,给RTCCLK降一下频率,保证分频器输出给计数器的频率位1Hz。为了适配各种频率的RTCCLK,这里就加了一个20位的分频器,可以选择对输入时钟进行1~2^20这么大范围的分频,这样就可以适配不同频率的输入时钟。
可选择三种RTC时钟源:
HSE时钟除以128(通常为8MHz/128)
LSE振荡器时钟(通常为32.768KHz)
LSI振荡器时钟(40KHz)
这三个时钟可以选择其中一个接入到RTCCLK。
高速时钟,一般供内部程序运行和主要外设使用, 低速时钟,一般供RTC、看门狗这些东西使用。我们最常用的是LSE OSC这一路外部32.768KHz的晶振,提供RTCCLK的时钟。第一个原因就是,中间这一路32.768KHz的晶振,本身就是专供RTC使用的,上面这两路其实是有各自的任务,上面这一路,主要作为系统主时钟,下面这一路主要作为看门狗时钟。它们只是顺带备选当作RTC的时钟。另外是只有这一路的时钟,可以通过VBAT备用电池供电,上下两路时钟,在主电源断电后,是停止运行的。所以要想实现RTC主电源掉电继续走时的功能,必须得选择中间这一路的RTC专用时钟。如果选择上下两路时钟,主电源断电后,时=时钟就暂停了,这样显然会出错。
11.2.2.2 RTC框图
11.2.2.3 RTC基本结构
最左边是RTCCLK时钟来源,这一块需要在RCC里配置, 3个时钟,选择一个当作RTCCLK,之后RTCCLK通过预分频器,对时钟进行分频。余数寄存器是一个自减计数器,存储当前的计数值,重装寄存器是计数目标,决定分频值,分频之后得到1Hz的秒计数信号,通向32位计数器,1秒自增1次,下面还有一个32位的闹钟值,可以设定闹钟,如果不需要的话,下面可以不用管。右边有三个信号可以触发中断,分别是秒信号,计数器溢出信号和闹钟信号,三个信号先通过中断输出控制,进行中断使能,使能的中断才能通向NVIC,然后向CPU申请中断。在程序中,我们可以配置数据选择器选择时钟来源,配置重装寄存器,可以选择分频系数,配置32位计数器,可以进行日期时间的读写。需要闹钟的话,配置32位闹钟值即可,需要中断的话,先允许中断,再配置NVIC。最后写对应的中断函数即可。
11.2.2.4 硬件电路
在最小电路上,外部电路还要再额外加两部分,第一部分就是备用电池供电, 第二部分就是外部低速晶振。
11.2.2.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寄存器。
11.3 读写备份寄存器
11.3.1 硬件电路
11.3.2 软件部分
(1)复制《OLED显示屏》并改为《读写备份寄存器》
(2)BKP库函数
void BKP_DeInit(void); //恢复缺省配置,手动清空BKP所有的数据寄存器
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel); //用于配置TAMPER侵入检测功能,高电平还是低电平触发
void BKP_TamperPinCmd(FunctionalState NewState); //是否开启侵入检测功能
void BKP_ITConfig(FunctionalState NewState); //中断配置,是否开启中断
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource); //时钟输出功能配置,可以选择在RTC引脚上输出时钟信号,输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue); //设置RTC校准值,其实就是写入RTC校准寄存器
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data); //写备份寄存器,第一参数指定卸载哪个DR里,第二个参数指定要写入的数据
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR); //读备份寄存器
FlagStatus BKP_GetFlagStatus(void);
void BKP_ClearFlag(void);
ITStatus BKP_GetITStatus(void);
void BKP_ClearITPendingBit(void);
(3) PWR库函数
void PWR_DeInit(void);
void PWR_BackupAccessCmd(FunctionalState NewState); // 备份寄存器访问使能
void PWR_PVDCmd(FunctionalState NewState);
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);
void PWR_WakeUpPinCmd(FunctionalState NewState);
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry);
void PWR_EnterSTANDBYMode(void);
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);
void PWR_ClearFlag(uint32_t PWR_FLAG);
(4)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Key.h"
uint8_t KeyNum;
uint16_t ArrayWrite[] = {0x1234,0x5678};
uint16_t ArrayRead[2];
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Key_Init();
OLED_ShowString(1,1,"W:");
OLED_ShowString(2,1,"R:");
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); //使能PWR时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE); //使能BKP时钟
PWR_BackupAccessCmd(ENABLE); //使能对BKP和RTC的访问
// BKP_WriteBackupRegister(BKP_DR1,0x1234);
// OLED_ShowHexNum(1,1,BKP_ReadBackupRegister(BKP_DR1),4);
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1)
{
ArrayWrite[0]++;
ArrayWrite[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);
}
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);
}
}
11.4 实时时钟
11.4.1 硬件电路
11.4.2 软件部分
(1)复制复制《OLED显示屏》并改为《实时时钟》
(2)添加驱动文件
(3)使用的RCC库函数
void RCC_LSEConfig(uint8_t RCC_LSE); //配置LSE外部低速时钟
void RCC_LSICmd(FunctionalState NewState); //配置LSI内部低速时钟
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource); //选择RTCCLK的时钟源
void RCC_RTCCLKCmd(FunctionalState NewState); //启动RTCCLK
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG); //获取标志位,LSE时钟不是说让它启动就能立刻启动,还需要1等待标志位LSEREADY置1
(4)RTC库函数
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState); //配置中断输出
void RTC_EnterConfigMode(void); //进入配置模式,置CRL的CNF位为1,进入配置模式
void RTC_ExitConfigMode(void); //退出配置模式,把CNF位清零
uint32_t RTC_GetCounter(void); //获取CNT计数器的值,读取时钟,就靠这个函数
void RTC_SetCounter(uint32_t CounterValue); //写入CNT计数器的值,设置时间,就靠这个函数
void RTC_SetPrescaler(uint32_t PrescalerValue); //写入预分频器,这个值会写入的预分频器的PRL重装寄存器中,用来配置预分频器的分频系数
void RTC_SetAlarm(uint32_t AlarmValue); //写入闹钟值
uint32_t RTC_GetDivider(void); //读取预分频器中的DIV余数寄存器,余数寄存器是一个自减计数器,一般是为了得到更细致的时间
void RTC_WaitForLastTask(void); //等待上次操作完成
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);
(5)MyRTC.c
#include "stm32f10x.h" // Device header
#include <time.h>
void MyRTC_SetTime(void);
uint16_t MyRTC_Time[]= {2024,8,15,11,13,55}; //十进制前面千万不要补0,因为8进制以0开头
/*RTC初始化函数*/
void MyRTC_Init(void)
{
/*第1步:开启PWR和BKP的时钟,使能BKP和RTC的访问*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
PWR_BackupAccessCmd(ENABLE);
if(BKP_ReadBackupRegister(BKP_DR1 != 0xA5A5))
{
/*第2步:开启LSE时钟,并等待LSE时钟启动完成*/
RCC_LSEConfig(RCC_LSE_ON); //启动LSE晶振
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET); //等待LSE状态标志位置1
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK时钟源
RCC_RTCCLKCmd(ENABLE); //使能时钟
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
RTC_SetPrescaler(32768-1); //配置分频系数得到1Hz
RTC_WaitForLastTask(); //等待上一次操作完成
// RTC_SetCounter(1723690874);
MyRTC_SetTime();
/*有的板子RTC晶振起振不了,RTC晶振不起振,就会卡死在RCC_GetFlagStatus(RCC_FLAG_LSERDY)这里
,这时侯只能备选LSI,进行以下修改*/
// RCC_LSICmd(ENABLE); //启动LSI晶振
// while(RCC_GetFlagStatus(RCC_FLAG_LSIRDY)!=SET); //等待LSI状态标志位置1
// RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); //选择RTCCLK时钟源
// RCC_RTCCLKCmd(ENABLE); //使能时钟
//
// RTC_WaitForSynchro(); //等待同步
// RTC_WaitForLastTask(); //等待上一次操作完成
//
// RTC_SetPrescaler(40000-1); //配置分频系数得到1Hz
// RTC_WaitForLastTask(); //等待上一次操作完成
// RTC_SetCounter(1723690874);
BKP_WriteBackupRegister(BKP_DR1,0xA5A5);
}
else
{
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
}
}
/*将时间转换位Unix时间戳形式函数*/
void MyRTC_SetTime(void)
{
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)
{
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;
}
(6)MyRTC.h
#ifndef __MYRTC_
#define __MYRTC_
extern uint16_t MyRTC_Time[];
void MyRTC_Init(void);
void MyRTC_SetTime(void);
void MyRTC_ReadTime(void);
#endif
(7)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "MyRTC.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
MyRTC_Init();
OLED_ShowString(1,1,"Date: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,6,RTC_GetCounter(),10);
OLED_ShowNum(2,12,MyRTC_Time[5],2);
OLED_ShowNum(3,6,RTC_GetCounter(),10);
OLED_ShowNum(4,6,(32767-RTC_GetDivider())/32767.0*999,10);
}
}