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);

 }
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐