stm32-时间戳、BKP、RTC
Unix时间戳
简介

从伦敦时间1970年1月1日0点开始计时,只计秒数,不会进位成分钟什么的,到图中的2023年伦敦时间已经计时了16亿多秒。这样的计时方式对人类来说不友好不直观,但对于计算机是非常方便的,只需要计秒这一个变量,不需要年月日时分这些变量;硬件电路设计也因为只需要计秒数而变得简单;但这样需要转换为实时时间的时候也会多使用一些资源。
秒数要转换成年月日时分秒的实时时间较为麻烦,要考虑平年闰年大月小月,但好在步骤都是固定的。
时间戳当然不能够一直计数下去,时间戳存储在32位的秒计数器中,上限就是2^32-1,差不多可以计数到2108年,然后就会计数溢出了;64位秒计数器更久,可以计三千亿年了。
时间戳是以伦敦时间为准,其他时区的实时时间也只需要加上时间偏移就好了,如图中的北京。
GMT、UTC

GMT是以地球自转来计时的,但现在地球因为潮汐力等原因自转越来越慢,所以在GMT中1秒的时间长度会变化,是不稳定的,所以现在都使用UTC了。
UTC规定了1秒的长度,基本是固定不变的,但现实中的实时时间应与地球自转同步,所以当UTC的时间比GMT时间快超过0.9秒即接近一秒时,UTC就会多走一秒,这一分钟就有61秒,相当于等待GMT时间,这一秒就是闰秒了。
C语言相关函数


BKP备份寄存器
简介

VDD主电源供电时,BKP由主电源供电;VDD被切断时,BKP由备用电源VBAT供电;当二者都被切断BKP数据清零,因为BKP本质是RAM存储器,掉电会丢失。TAMPER引脚侵入事件清除BKP数据可以用于保护数据防止被盗。
结构

BKP重要的寄存器存在于后备区域,即图中橙色部分,凡是VDD切断后可以使用VBAT供电的都在后备区域,包括RTC。
RTC
简介



既然是实时时钟,那么就要一秒自增一次,频率应为1HZ,所以对于各种频率的外部时钟,应该先进行分频,并且外部时钟频率一般比较高,所以就设置了可1~2^20分频的预分频器。
对于高速外部时钟HSE,可以将自身8MHZ进行128分频后输入RTC的RTCCLK引脚,之后RTC内部再进行1~2^20预分频。先进行128分频是因为HSE频率很高,只使用RTC内部预分频是达不到需要的1HZ的;对于低速外部时钟LSE,频率是32.768KHZ即32768HZ,32768本身就是2的次方数,所以使用RTC的2^15预分频就能很方便得到1HZ。LSE是专门为RTC设计的;对于低速内部时钟LSI,频率40KHZ也可以作为RTC的外部时钟源。
三种时钟源中,HSE是系统时钟,LSI是看门狗时钟,它们的主要任务都不是给RTC提供时钟,并且它们只能用VDD供电;而LSE,是专门给RTC使用的,而且可以在VDD切断后使用VBAT供电。所以LSE是最佳选择。
结构

左边的部分是计时部分,选择的外部时钟首先进行预分频:RTC预分频器里其实也是一个定时器,RTC_PRL中写入需要的分频值,RTC_DIV余数寄存器相当于定时器的CNT,外部时钟每来一个输入脉冲,这个DIV就自减一次,减到0之后再来一个输入脉冲,就会给右边的计数器一个输出脉冲,计数器里面就自增一次,同时左边的CNT数值回到分频值。
例如,使用LSE时钟,输入频率是32768HZ,设置PRL=32767,因为包含0所以分频值=32767+1=32768,DIV初始值0。LSE输入一个脉冲,DIV自减,这时会给右边计数器一个输出脉冲,计数器+1,DIV的值回到32767,LSE继续给脉冲,DIV自减,给32768个脉冲后DIV从0又自减,再给计数器一个输出脉冲,计数器+1,DIV回到32767。相当于原本的32768HZ一秒可以产生32768个脉冲,通过左边定时器的控制使得它一秒产生的32768个脉冲才输出一个脉冲,将频率降低了32768倍即1HZ。
右边的计数器就是和时间戳有关的,可以一直计数到2^32-1之后溢出。RTC_ALR是闹钟寄存器,设置一个值,当计数器的值等于它时就可以产生中断,也可以唤醒系统退出待机模式,但是值是固定的。
右边的部分是中断,有三种信号可以产生中断,一是闹钟寄存器,二是计数器溢出,三是自增一次,即过去一秒可以进入一次中断。其中只有使能的中断才可以通向NVIC控制。
下面部分和PWR电源控制有关,WKUP引脚也可以使系统退出待机模式。
图中灰色部分都位于后备区域,可以用VBAT供电,待机时也能维持供电;其余区域待机时就断电了。RTC时钟源在APB1总线上选择。
流程

外部电路

VBAT供电电路有两种方案,简单连接就是备用电池直接连接VBAT引脚;但推荐连接是VDD和VBAT供电时都加一个低压降二极管,防止VDD的电流倒灌到备用电源,同时加一个滤波电容。
LSE电路使用晶振和两个起振电容连接,通过两个引脚和核心板连接。
注意事项

要使用BKP和RTC,需要先开启APB1总线上PWR和BKP的时钟,再设置PWR寄存器的DBP位,这样才能使能BKP和RTC。
由结构图可以看出里面有两种时钟,APB1总线使用的PCLK1时钟和RTC使用的时钟,它们的频率是不一样的,所以想通过APB1总线去访问RTC就需要等待RTC的寄存器同步到APB1总线,不然一开始就去访问,数据就是错的。即访问RTC前要等待RTC中的寄存器同步标志位RSF置1。
想要对RTC进行写入操作,要先设置CNF位。
对RTC前一次写入结束之后才能开始下一次写入,所以要等待RTOFF标志位置1才能继续写入,其实还是时钟不同步的问题,数据写入RTC需要时间。
读写BKP代码
实现按一下按键就改写一次BKP寄存器里的值并读出。
主函数
#include "stm32f10x.h"// Device header
#include "Delay.h"
#include "OLED.h"
#include "key.h"
uint16_t num,writeArray[2]={0x1234,0x2345};
uint16_t receArray[2];
int main()
{
OLED_Init();
key_init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);//开启时钟
PWR_BackupAccessCmd(ENABLE);//设置DBP位,使能访问
while(1)
{
num=key_num();
BKP_WriteBackupRegister(BKP_DR1,writeArray[0]);
BKP_WriteBackupRegister(BKP_DR2,writeArray[1]);
OLED_ShowHexNum(1,1,BKP_ReadBackupRegister(BKP_DR1),4);
OLED_ShowHexNum(2,1,BKP_ReadBackupRegister(BKP_DR2),4);
if(num==1)
{
writeArray[0]++;
writeArray[1]++;
}
}
}
RTC读写时间代码
MYRTC.c文件
#include "stm32f10x.h" // Device header
#include <time.h>
uint16_t MYRTC_time[]={2023,1,1,23,59,55};//年月日时分秒
//初始化RTC,先开PWR和BKP时钟,设置PWR的DBP位。然后打开LSE,选择LSE,使能LSE。接着等待同步,配置分频值和计数值
void MYRTC_init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);//开启时钟
PWR_BackupAccessCmd(ENABLE);//设置DBP位,使能访问
//没有彻底断电过,RTC的值就不应该重置,所以需要一个if判断决定有没有彻底断电过,可以通过判断BKP寄存器的值有没有重置来判断
if(BKP_ReadBackupRegister(BKP_DR1)!=0xa5a5)//如果有彻底断电,再次上电BKP重置后寄存器值=0,RTC需要重置
{
//使用LSE,为例省电LSE默认是关闭的,需要先打开
RCC_LSEConfig(RCC_LSE_ON);
//观察标志位LSERDY来得知LSE开启成功没有
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY)==RESET);
//再选择LSE作为时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
//使能时钟
RCC_RTCCLKCmd(ENABLE);
//等待寄存器同步、等待上一次写入完成
RTC_WaitForSynchro();
RTC_WaitForLastTask();
//配置分频器、等待写入完成
RTC_SetPrescaler(32768-1);//需要先设置CNF位才能对定时器的寄存器进行写入,但在代码中对这些寄存器进行写入操作的函数里都已经设置了CNF位了
RTC_WaitForLastTask();
//设置计数器初始CNT,等待写入完成
RTC_SetCounter(1672588795);//计数器的值CNT会从这里开始一秒自增,如果不设置CNT就是0开始,即1970年1月1日0点
RTC_WaitForLastTask();
BKP_WriteBackupRegister(BKP_DR1,0xa5a5);//改写BKP寄存器值,用于下次断电判断
}
else//如果没有彻底断电过,RTC值不能重置
{
RTC_WaitForSynchro();
RTC_WaitForLastTask();
}
}
void MYRTC_settime(void)//数组写入年月日时分秒,函数转换为CNT的值
{
time_t time_cnt;//time_t是unsigned int
struct tm time_date;//结构体tm中的变量名称叫time_date,结构体成员是年月日时分秒
time_date.tm_year =MYRTC_time[0]-1900;//结构体年份范围是在1900年基础上加的,所以这里数组减去1900
time_date.tm_mon =MYRTC_time[1]-1;//结构体月的范围是0~11,所以这里减一
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);//mktime函数变量是结构体地址,返回值是CNT
RTC_SetCounter(time_cnt);//将RTC的CNT设置为mktime函数计算好的CNT值
RTC_WaitForLastTask();//等待写入完成
}
void MYRTC_gettime(void)//读取CNT,函数转化为年月日时分秒
{
time_t time_cnt;
struct tm time_date;
time_cnt=RTC_GetCounter();//获取RTC的CNT值,起始值是初始化函数里设定的CNT值
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;
}
MYRTC.h文件
#ifndef __MYRTC_H_
#define __MYRTC_H_
extern uint16_t MYRTC_time[];
void MYRTC_init(void);
void MYRTC_settime(void);
void MYRTC_gettime(void);
#endif
主函数
#include "stm32f10x.h"// Device header
#include "Delay.h"
#include "OLED.h"
#include "MYRTC.h"
int main()
{
OLED_Init();
MYRTC_init();
OLED_ShowString(1,1,"xxxx-xx-xx");
OLED_ShowString(2,1,"xx:xx:xx");
OLED_ShowString(3,1,"CNT:");
while(1)
{
MYRTC_gettime();
OLED_ShowNum(1,1,MYRTC_time[0],4);
OLED_ShowNum(1,6,MYRTC_time[1],2);
OLED_ShowNum(1,9,MYRTC_time[2],2);
OLED_ShowNum(2,1,MYRTC_time[3],2);
OLED_ShowNum(2,4,MYRTC_time[4],2);
OLED_ShowNum(2,7,MYRTC_time[5],2);
OLED_ShowNum(3,5,RTC_GetCounter(),10);
}
}
更多推荐


所有评论(0)