UNIX时间戳
·UNIX时间戳最早是在UNIX系统使用的,所以叫做UNIX时间戳,之后很多由UNIX演变而来的系统也继承了UNIX时间戳的规定,目前linux,windows,安卓这些操作系统的底层计时系统都是用UNIX时间戳
·时间戳这个计时系统和我们常用的年月日时分秒的计时系统具有较大的差别。
年月日时分秒计时系统是每60秒进位1次,记为一分钟,每60分钟进位1次记为1小时,之后便是日月年
时间戳计时系统定义1970年1月1日0点整为0秒,之后只用最基本的秒计时,永不进位,无论数有多大都不进位。
这样有很多好处:
第一,简化了硬件电路,在设计RTC硬件电路的时候直接用一个很大的寄存器即可,无需考虑其他的寄存器
第二,在进行一些时间间隔的计算时非常方便,秒数相见换成时间很方便
第三,存储方便,存储秒数一个很大的变量即可,存储年月日时分秒则需要很多变量
坏处:比较占用软件资源,每次进行转换的时候软件需要进行复杂的计算
·时间戳存储在一个秒计数器中,计算机为了存储这一个永不进位的秒数,定义的变量类型通常会较大。变量类型在不同的系统中是不一样的。在早起的UNIX系统中,这个秒数大多是用32位有符号的整形变量进行存储,32位有符号数所能表示的最大数是 / 2 - 1,这个数是21亿多,有溢出的风险,根据计算,32位的有符号时间戳会在2038年的1月19号溢出,到时候采用32位有符号数存储时间戳的设备,计时系统会因数据溢出而出错,会导致很多不健全的计算机程序崩溃。
随着操作系统和设备的更新换代,目前基本使用64位的数据存储时间戳。
stm32核心的计时部分是一个32位的可编程计数器,说明这款stm32的时间戳是32位的数据类型,但是是无符号的32位,最大是 - 1,大概到2106年才会溢出
·地球上不一样的经度时间是不一样的,穿过英国伦敦的经线是本初子午线,这个位置的时间是一个时间标准,时间戳的1970年1月1日0时0分0秒也是指伦敦时间的0时0分0秒,其他地方分为24个时区,每偏差一个时区时间需要加或减一个小时
对于不同地区是共用同一个时间戳的秒计数器,在伦敦是0,北京也是0,根据不同时区添加小时的偏移即可,伦敦是0点的时候,北京处于东八区就是8点
图中的箭头代表的是一个时间轴,在时间轴上定义一个起点,时间戳从起点开始计时,起点是人为规定的,对于起点之前的时间,时间戳是无法表示的
UTC/GMT
·格林尼治是一个地名,位于英国伦敦,也就是伦敦的标准时间。在格林尼治有个天文台,可以观察天上太阳和星星以确定太阳的自转和公转,将地球自转一周的时间间隔分为24小时。
GMT是以前的计时时间,因为存在一个棘手的问题,因为地球自转一周的时间是不固定的,由于潮汐力等因素导致地球处于越转越慢的状态,根据这个时间基准定义新的时间,那么这个时间基准是不断变化的,地球越转越慢,那么定义的1秒的时间也就越来越长
·为了时间的定义更标准,提出了新的计时系统,叫做UTC协调世界时。在原子钟计时系统的基础上加入了润秒的机制,来消除计时一天和自转一周的误差。所谓润秒就是计时标准是恒定不变的,但是地球越转越慢,当误差超过0.9秒的时候,计时系统就多走一秒等一下地球的自转。
润秒的机制可能会造成一些程序的bug,可能会出现一分钟61秒的情况。平时不追求严谨的话使用GMT或UTC都可以
时间戳转换
·time_t time(time_t*):
作用是获取系统时钟,返回值是time_t,表示当前系统时钟的秒计数期值,参数time_t*,是一个输出参数,输出的内容和返回值是一样的。这个函数可以通过返回值获取系统时钟,也可以通过输出参数获取。
这个函数在电脑中可以直接读取电脑的时间,在STM32中无法使用,因为STM32是一个离线的裸机系统,无法获取目前的时间状态,
代码(使用DEV C++进行编写)
#include <stdio.h>
#include <time.h>
time_t time_cnt;//秒计数器 time_t是一个typedef重命名的类型 如果不是特别声明要用32位的秒计数器类型
//那么默认状况下就是__time64_t 然后__time_t实际上是int64 是一个有符号的整型数据 无需担心溢出问题 可以用于存储时间戳中一直自增的秒数
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名
char *time_str;
int main()
{
time_cnt = time(NULL);//参数不需要的话给null即可
printf("%d\n",time_cnt);
return 0;
}
编译烧录后在中断可以看到这样的输出
将终端的数值复制到网络上的时间戳在线转换工具中,进行转换,验证了数据是准确的
将代码
time_cnt = time(NULL);
进行注释,替换成
time(&time_cnt);
也可以进行正常的显示当前的时间值
·struct tm* gmtime(const time_t):
将秒计数器的值转换为格林尼治,也就是伦敦时间。
参数是const time_t*,秒计数器指针类型,是输入参数,返回值struct tm*是日期时间结构体指针类型
gmtime()的参数是time_t*,需要将地址传进去,返回值是struct tm*
如果直接time_date = gmtime(&time_cnt)是指针跨级赋值,等号左边是一个变量,右边是一个地址,解决方法有两个:
第一:time_date = *gmtime(&time_cnt),在函数的右边返回值加上*取内容,等号左右两边都是变量,结构体变量互相赋值是ok的
第二:定义变量的时候struct tm *time_date定义为指针类型,那么time_date = gmtime(&time_cnt)的等号左右都是指针,结构体指针之间相互赋值
代码(使用DEV C++进行编写)
#include <stdio.h>
#include <time.h>
time_t time_cnt;//秒计数器 time_t是一个typedef重命名的类型 如果不是特别声明要用32位的秒计数器类型
//那么默认状况下就是__time64_t 然后__time_t实际上是int64 是一个有符号的整型数据 无需担心溢出问题 可以用于存储时间戳中一直自增的秒数
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名
char *time_str;
int main()
{
//time_cnt = time(NULL);//参数不需要的话给null即可
//time(&time_cnt);
time_cnt = 1713960225;
printf("%d\n",time_cnt);
time_date = *gmtime(&time_cnt);
printf("%d\n",time_date.tm_year + 1900);//年份偏移
printf("%d\n",time_date.tm_mon+1);//月份偏移
printf("%d\n",time_date.tm_mday);
printf("%d\n",time_date.tm_hour+8);//北京时间东八区
printf("%d\n",time_date.tm_min);
printf("%d\n",time_date.tm_sec);
printf("%d\n",time_date.tm_wday);
return 0;
}
·struct tm* localtime(const time_t*):
转换当地时间,函数的使用方法和gmtime是一样的,作用也是一样的,不过localtime会根据时区自动添加小时的偏移,程序只需将gmtime的名字替换成localtime即可,这个函数会自动根据当前电脑的设置判断所处的时区,将时间添加时区偏移之后输出
·time_t mktime(struct tm*):
将日期时间转换为秒计数器,mktime传入的日期时间需要是当地的,maketime就是前边两个转换的逆过程。
代码(使用DEV C++进行编写)
编译烧录后,第一行是给定的时间秒数,中间是转换成当地的时间,最后一行是根据日期时间转换回的秒数,最终的秒数和最初的秒数是一样的。
将localtime改成gmtime,最终的秒数就和最初的秒数不一样,说明mktime不是根据伦敦时间进行的
mktime的参数前边是没有const,这个参数既是输入参数也是输出参数。内部的工作过程是这样的:日期时间结构体,里面有年月日时分秒星期等数据,但是仅通过年月日时分秒就足以计算出秒计数器,里边的星期参数实际上是不作为输入参数的;但是相反的是,函数在算出秒数的同时还会顺便计算当前年月日是星期几,回填到结构体中的星期之中。使用这个函数可以很方便的计算对应的是星期几
#include <stdio.h>
#include <time.h>
time_t time_cnt;//秒计数器 time_t是一个typedef重命名的类型 如果不是特别声明要用32位的秒计数器类型
//那么默认状况下就是__time64_t 然后__time_t实际上是int64 是一个有符号的整型数据 无需担心溢出问题 可以用于存储时间戳中一直自增的秒数
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名
char *time_str;
int main()
{
//time_cnt = time(NULL);//参数不需要的话给null即可
//time(&time_cnt);
time_cnt = 1713960225;
printf("%d\n",time_cnt);
time_date = *localtime(&time_cnt);
printf("%d\n",time_date.tm_year + 1900);
printf("%d\n",time_date.tm_mon+1);
printf("%d\n",time_date.tm_mday);
printf("%d\n",time_date.tm_hour);
printf("%d\n",time_date.tm_min);
printf("%d\n",time_date.tm_sec);
printf("%d\n",time_date.tm_wday);
time_cnt = mktime(&time_date); //参数是日期时间结构体指针 可以传入日期时间结构体的地址 返回秒计数器的值
printf("%d\n",time_cnt);
return 0;
}
char* ctime(const time_t*):
将秒计时器转换成字符串表示,使用默认的格式,相当于转换成char* 格式的字符串
char* asctime(const struct tm*):
将日期转换为字符串,使用默认格式。相当于转换成char* 格式的字符串
代码:两个函数卸载一块进行展示
#include <stdio.h>
#include <time.h>
time_t time_cnt;//秒计数器 time_t是一个typedef重命名的类型 如果不是特别声明要用32位的秒计数器类型
//那么默认状况下就是__time64_t 然后__time_t实际上是int64 是一个有符号的整型数据 无需担心溢出问题 可以用于存储时间戳中一直自增的秒数
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名
char *time_str;
int main()
{
//time_cnt = time(NULL);//参数不需要的话给null即可
//time(&time_cnt);
time_cnt = 1713960225;
printf("%d\n",time_cnt);
time_date = *localtime(&time_cnt);
printf("%d\n",time_date.tm_year + 1900);
printf("%d\n",time_date.tm_mon+1);
printf("%d\n",time_date.tm_mday);
printf("%d\n",time_date.tm_hour+8);
printf("%d\n",time_date.tm_min);
printf("%d\n",time_date.tm_sec);
printf("%d\n",time_date.tm_wday);
time_cnt = mktime(&time_date); //参数是日期时间结构体指针 可以传入日期时间结构体的地址 返回秒计数器的值
printf("%d\n",time_cnt);
time_str = ctime (&time_cnt);
printf(time_str);
time_str = asctime (&time_date);
printf(time_str);
return 0;
}
编译运行后显示的为下图所示(最后两行)
size_t strftime(char*,size_t,const char*,const struct tm*):
这个函数作用同上边两个函数一样,但是区别是可以自定义格式。输入下列代码接在前边的代码后边,
char t[50];
strftime(t,50,"%H-%M-%S",&time_date);//前两个参数需要传入一个字符数组和数组长度 第三个参数需要给定指定的格式字符串 第四个参数把time_date传进去即可
printf(t);
其中关于第三个参数的格式,可以参考菜鸟教程中的文件,
编译烧录后得到结果在最后一行
菜鸟教程
clock_t clock(void)函数可以用于计算程序执行的时长,
double difftime(time_t time1,time_t2 time2)可以计算两个时间之间的差值
最重要且最复杂的是其中的localtime 和mktime函数,在STM32的RTC程序会使用到,需要重点掌握
各个函数的关系和作用
秒计数器数据类型time_t:
秒计数器,time_t是一个typedef重命名的类型 如果不是特别声明要用32位的秒计数器类型,那么默认状况下就是__time64_t ,__time_t实际上是int64,是一个有符号的整型数据,无需担心溢出问题,可以用于存储时间戳中一直自增的秒数
日期时间数据类型struct tm:
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名.
关于struct tm里边的变量定义如下
struct tm {
int tm_sec; //表示秒 取值范围0-59
int tm_min; //表示分钟 取值范围0-59
int tm_hour; //表示午夜开始的小时 取值范围0-23
int tm_mday; //表示一个月的几号 取值范围0-31
int tm_mon; //表示1月开始的第几个月 取值范围0-11 1月是0 12月是11 参数值需要加1才是所需月份
int tm_year; //表示从1900年的第几年 所以参数需要加上1900才是年份 时间戳起点是1970 参数最小值是70
int tm_wday; //表示周末开始的星期几 0表示周日 1表示周一 直到6表示周六
int tm_yday; //表示从1月1号开始的第几天 取值范围0-365
int tm_isdst; //是否使用夏令时 +1表示使用 0表示不用 -1表示不知道
};
字符串数据类型char*
char型数据的指针,用于表示一个指向时间的字符串
需要在ST_Link上引出一个3.3V电源接到stm32的VBAT引脚上,用于模拟电池的电源,一般情况下VBAT是电池供电口,需要接备用电池
BKP寄存器和Flash存储器类似,都是用于存储数据,不过Flash的数据是掉电不丢失,而BKP的数据是需要通过VBAT引脚上的备用电池来维持的。只要VBAT有电池供电,即使STM32主电源断电,BKP的值仍然可以维持原状,并且在按下系统复位键之后,BKP的数据并不会产生复位
如果把VBAT的电池断电,再次拔掉主电源上电,BKP的数据被清零,因为BKP的本质并不完全掉电不丢失,数据需要VBAT引脚提供备用电源维持。如果stm32接了备用电池,那么BKP可以完成主电源掉电时保存少量数据的任务。
实时时钟
第一行显示日期,给的是一个测试时间;第二行是时间;第三行是时间戳的秒计数器;第四行是RTC预分频器的计数值
作为实时时钟,按下复位键的时候不能复位,时间会继续运行。实时时钟在系统主电源断电之后需要继续运行,就像手机关机后时钟需继续运行,否则造成时间错误。在VBAT接上了电源再断开系统主电源,可以看到时间数据不会丢失,并且在断电期间RTC会继续走时不会暂停,是借助BKP实现的,RTC与BKP关联度较高