小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
既然STM32F103单片机内部可以存储时间,那么通过这个功能,就可以做一个万年历出来。实现过程如下:
typedef struct
{
vu8 hour;
vu8 min;
vu8 sec;
//公历日月年周
vu16 w_year;
vu16 w_month;
vu8 w_date;
vu8 week;
} _calendar_obj;
复制代码
首先定义一个结构体来存储时、分、秒、年、月、日、星期这些数据。
//判断是否是闰年函数
//输入:年份
//输出:该年份是不是闰年.1,是.0,不是
u8 IS_Leap_Year( u16 year )
{
//能被4或者400整除,并且不能被100整除
if( ( ( year % 4 == 0 ) || ( year % 400 == 0 ) ) && ( year % 100 != 0 ) )
return 1;
else
return 0;
}
//设置时钟
//把输入的时钟转换为秒钟
//以1970年1月1日为基准
//1970~2099年为合法年份
//返回值:0,成功;其他:错误代码.
//月份数据表
u8 const table_week[12] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5}; //月修正数据表
//平年的月份日期表
const u8 mon_table[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
u8 RTC_Set( u16 syear, u8 smon, u8 sday, u8 hour, u8 min, u8 sec )
{
u16 t;
u32 seccount = 0;
if( syear < 1970 || syear > 2099 )
return 1;
for( t = 1970; t < syear; t++ ) //所有年份的秒钟相加
{
if( IS_Leap_Year( t ) )
seccount += 31622400; //闰年秒钟数
else
seccount += 31536000; //平年秒钟数
}
smon -= 1;
for( t = 0; t < smon; t++ ) //把前面月份的秒钟数相加
{
seccount += ( u32 )mon_table[t] * 86400;
if( IS_Leap_Year( syear ) && t == 1 ) //闰年2月份增加一天的秒钟数
seccount += 86400;
}
seccount += ( u32 )( sday - 1 ) * 86400; //把前面天数的秒钟数相加
seccount += ( u32 )hour * 3600; //加小时秒钟数
seccount += ( u32 )min * 60; //加分钟秒钟数
seccount += sec; //加上最后的秒数
RCC_APB1PeriphClockCmd( RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE );
PWR_BackupAccessCmd( ENABLE );
RTC_SetCounter( seccount ); //设置RTC计数器的值
RTC_WaitForLastTask();
return 0;
}
//初始化闹钟
//以1970年1月1日为基准
//1970~2099年为合法年份
//syear,smon,sday,hour,min,sec:闹钟的年月日时分秒
//返回值:0,成功;其他:错误代码.
u8 RTC_Alarm_Set( u16 syear, u8 smon, u8 sday, u8 hour, u8 min, u8 sec )
{
u16 t;
u32 seccount = 0;
if( syear < 1970 || syear > 2099 )
return 1;
for( t = 1970; t < syear; t++ ) //所有年份的秒钟相加
{
if( IS_Leap_Year( t ) )
seccount += 31622400; //闰年秒钟数
else
seccount += 31536000; //平年秒钟数
}
smon -= 1;
for( t = 0; t < smon; t++ ) //把前面月份的秒钟数相加
{
seccount += ( u32 )mon_table[t] * 86400;
if( IS_Leap_Year( syear ) && t == 1 ) //闰年2月份增加一天的秒钟数
seccount += 86400;
}
seccount += ( u32 )( sday - 1 ) * 86400; //把前面天数的秒钟数相加
seccount += ( u32 )hour * 3600; //加小时秒钟数
seccount += ( u32 )min * 60; //加分钟秒钟数
seccount += sec; //加上最后的秒数
RCC_APB1PeriphClockCmd( RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE );
PWR_BackupAccessCmd( ENABLE );
RTC_SetAlarm( seccount );
RTC_WaitForLastTask();
return 0;
}
//得到当前的时间
//返回值:0,成功;其他:错误代码.
u8 RTC_Get( void )
{
static u16 daycnt = 0;
u32 timecount = 0;
u32 temp = 0;
u16 temp1 = 0;
timecount = RTC_GetCounter();
temp = timecount / 86400; //得到天数 小于1天时,秒小于86400
if( daycnt != temp )
{
daycnt = temp;
temp1 = 1970;
while( temp > 365 ) //超过1年
{
if( IS_Leap_Year( temp1 ) ) //闰年
{
if( temp > 366 )
{
temp -= 366;
}
else
{
temp1++;
break;
}
}
else //平年
{
temp -= 365;
}
temp1++;
}
calendar.w_year = temp1; //得到年份
temp1 = 0;
while( temp >= 28 ) //超过1月
{
if( IS_Leap_Year( calendar.w_year ) && temp1 == 1 )
{
if( temp >= 29 )
temp -= 29;
else
break;
}
else
{
if( temp >= mon_table[temp1] )
temp -= mon_table[temp1];
else
break;
}
temp1++;
}
calendar.w_month = temp1 + 1; //月
calendar.w_date = temp + 1; //日
}
temp = timecount % 86400;
calendar.hour = temp / 3600;
calendar.min = ( temp % 3600 ) / 60;
calendar.sec = ( temp % 3600 ) % 60;
calendar.week = RTC_Get_Week( calendar.w_year, calendar.w_month, calendar.w_date );
return 0;
}
//获得现在是星期几
//功能描述:输入公历日期得到星期(只允许1901-2099年)
//输入参数:公历年月日
//返回值:星期号
u8 RTC_Get_Week( u16 year, u8 month, u8 day )
{
u16 tem;
u8 yearH, yearL;
yearH = year / 100;
yearL = year % 100;
if( yearH > 19 )
yearL += 100;
tem = yearL + yearL / 4;
tem = tem % 7;
tem = tem + day + table_week[month - 1];
if( yearL % 4 == 0 && month < 3 )
tem--;
return tem % 7;
}
复制代码
接下来编写时间设置和闹钟设置相关函数,因为在RTC中存储的时间是以秒为单位的,所以在读取时间和设置时间的时候,都需要和秒进行转换。而存储的秒数不是从设置时间的那一刻开始的,而是从1970年01月01日00时00分00秒开始的。为什么是这个时间呢?这就要从计算机的诞生说起了。
时间戳的诞生
1969年8月,贝尔实验室的程序员肯汤普逊利用妻儿离开一个月的机会,开始着手创造一个全新的革命性的操作系统,他使用B编译语言在老旧的PDP-7机器上开发出了Unix的一个版本。
随后,汤普逊和同事丹尼斯里奇改进了B语言,开发出了C语言,重写了Unix,新版于1971年发布。
在Unix被发明出来之后,需要在Unix上表示时间,就需要想办法定义一个能表示一份数据在某个特定时间之前已经存在的、完整的、可验证的数据来表示时间。
于是,Unix时间戳被定义出来,即通过当前时间和一个"纪元时间"进行对比,其间相差的秒数作为时间戳。
为了让Unix时间戳表示时间这种方式用的尽可能久,最初就把Unix诞生的时间1971-1-1定义成"纪元时间"。
不是1970年吗?怎么变成了1971年,那是因为时间戳还被修改过。
时间戳修改
除了开始时间是1971-1-1而不是1970-1-1外,最初的时间戳也不是每增加1秒时间戳就变动一次,而是每1/60秒都会改变一次时间戳。
另外,Unix是在1971年发明出来的,当时的计算机系统是32位,如果用32表示有整数,那么最大值是2147483647(2^31-1)。
那么,简单做一个数学计算,如果用当时的时间戳计算方式来表示时间的话,Unix时间戳最多可以使用4294967296/(606024)/60 = 828.5天(一天有606024秒,每1/60秒会占用一个时间戳)。
想象一下,设计出一个计算机系统,他的时间只能表示 828.5天,是不是很难让人接受,但是最初的Unix确实是这样的。
后来,Unix的开发者们也渐渐意识到这样不是长久之计,于是开始做出改变。
最开始,他们将每1/60秒改变一次时间戳修改成每1秒改变一次时间戳。这样时间戳可以表示的时间就又放大了60倍。这时候有828.5*60/365 = 136年。
这时候,一方面136年已经足够久了,纪元时间稍微向前调一下影响也不大。另外一方面为了方便记忆和使用。
于是就把纪元时间从1971-01-01调整到1970-01-01了。 于是,随着后面各种开发语言的诞生,就都沿袭了1970-1-1这个设定。
所以,通常我们说的时间戳,就是指格林威治时间(GMT)1970年01月01日00时00分00秒起至现在的总秒数。
当设置时间或者读取时间时,都需要将时间先换算为从1970年1月1日0时0分0秒到当前的时间的秒数,所以这里需要一个专门的函数来进行转换,在转换的过程中记得要判断闰年。
需要设置时间和闹钟使直接通过下面两个函数设置就行,参数依次为年、月、日、时、分、秒。
RTC_Set( 2021, 10, 1, 08, 00, 00 ); //设置时间
RTC_Alarm_Set(2021, 10, 7, 08, 00, 00 );
复制代码
当需要读取时间时调用 RTC_Get()函数就行。
RTC_Get();
复制代码
这个函数会将读取到的时间信息存储到结构体中,直接读取结构体变量就可以读取到当前时间了。