0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何利用RTC外设实现万年历功能

中科芯MCU 来源:中科芯MCU 2025-02-18 16:56 次阅读

万年历实验

本小节讲解的是如何利用RTC外设实现万年历功能,本实验工程与RTC底层驱动相关的文件为bsp_rtc.c/h,在底层驱动之上我们添加了bsp_calendar.c/h和bsp_date.c/h文件,用于万年历的计算。

程序设计要点

(1)初始化RTC外设;

(2)设置时间以及添加配置标志;

(3)获取当前时间;

代码分析

1、RTC实验配置相关宏定义

在这个RTC实验中的bsp_rtc.h文件中添加了一些宏定义用于切换工程的配置。本实验默认使用LSI内部时钟,使用内部时钟时,即使安装了钮扣电池,主电源掉电后时间是不会继续走的,只会保留上次断电的时间。若要持续运行,需要切换成使用LSE外部时钟。

#define RTC_CLOCK_SOURCE_LSI //使用LS内部时钟

#define RTC_BKP_DRX BKP_DR1

#define RTC_BKP_DATA 0xA5A5//写入到备份寄存器的数据宏定义

#define TIME_ZOOM (8*60*60)//北京时间的时区秒数差

RTC_BKP_DRX和RTC_BKP_DATA:这两个宏用于在备份域寄存器设置RTC已配置标志,本实验中初始化RTC后,向备份域寄存器写入一个数字,若下次芯片上电检测到该标志,说明RTC之前已经配置好时间,所以不应该再设置RTC,而如果备份域电源也掉电,备份域内记录的该标志也会丢失,所以芯片上电后需要重新设置时间。这两个宏的值中,BKP_DR1是备份域的其中一个寄存器,而0xA5A5则是随意选择的数字,只要写入和检测一致即可。

TIME_ZOOM:这个宏用于设置时区的秒数偏移,例如北京时间为(GMT+8)时区,即相对于格林威治时间(GMT)早8个小时,此处使用的宏值即为8个小时的秒数(8*60*60),若使用其它时区,修改该宏即可。

2、初始化RTC

在本工程中,我们编写了RTC_Configuration函数对RTC进行初始化。这个初始化的流程如下:使用RCC_APB1PeriphClockCmd使能PWR和BKP区域(即备份域)的时钟系统,使用PWR_BackupAccessCmd设置允许对BKP区域的访问,使能LSI时钟,选择LSI作为RTC的时钟源并使能RTC时钟,利用库函数RTC_WaitForSynchro对备份域和APB进行同步,用RTC_ITConfig使能秒中断,使用RTC_SetPrescaler分频配置把RTC时钟频率设置为1Hz,那么RTC每个时钟周期都会产生一次中断。经过这样的配置后,RTC每秒产生一次中断事件,实验中在中断设置标志位以便更新时间。

/*

* 函数名:RTC_Configuration

* 描述 :配置RTC

* 输入 :无

* 输出 :无

*/

void RTC_Configuration(void)

{

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//使能PWR和Backup时钟

PWR_BackupAccessCmd(ENABLE);//允许访问 Backup 区域

BKP_DeInit();//复位 Backup 区域

RCC_LSICmd(ENABLE);//使能 LSI

while(RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); //等待 LSI 准备好

RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);//选择 LSI 作为 RTC 时钟源

RCC_RTCCLKCmd(ENABLE);//使能 RTC 时钟

RTC_WaitForSynchro();//因为RTC时钟是低速的,内环时钟是高速的,所以要等待 RTC 寄存器同步

RTC_WaitForLastTask();//确保上一次 RTC 的操作完成

RTC_ITConfig(RTC_IT_SEC, ENABLE);//使能 RTC 秒中断

RTC_WaitForLastTask();//确保上一次 RTC 的操作完成

/* 设置 RTC 分频: 使 RTC 周期为1s ,LSI约为40KHz */

RTC_SetPrescaler(40000-1); //RTC period = RTCCLK/RTC_PR = (40 KHz)/(40000-1+1) = 1HZ

RTC_WaitForLastTask();//确保上一次 RTC 的操作完成

}

3、时间管理结构体

RTC初始化完成后可以直接往它的计数器写入时间戳,但是时间戳对用户不友好,不方便配置和显示时间,在本工程中,使用bsp_date.h文件的rtc_time结构体来管理时间。这个类型的结构体具有时、分、秒、日、月、年及星期这7个成员。当需要给RTC的计时器重新配置或显示时间时,使用这种容易接受的时间表示方式。

struct rtc_time {

int tm_sec;

int tm_min;

int tm_hour;

int tm_mday;

int tm_mon;

int tm_year;

int tm_wday;

};

在配置RTC时,使用这种类型的变量保存用户输入的时间,然后利用函数由该时间求出对应的UNIX时间戳,写入RTC的计数器;RTC正常运行后,需要输出时间时,利用函数通过RTC的计数器获取UNIX时间戳,转化成这种友好的时间表示方式保存到变量输出。

4、时间格式转换

在本实验中,tm格式转时间戳使用mktimev函数,时间戳转tm格式使用to_tm函数,这两个函数都定义在bsp_date.c文件中。关于日期计算的细节此处不作详细分析,其原理是以1970年1月1日0时0分0秒为计时基点,对日期和以秒数表示时间戳进行互相转化,转化重点在于闰年的计算。这两个函数都是以格林威治时间(GMT)时区来计算的,在调用这些函数时我们会对输入参数加入时区偏移的运算,进行调整。

/* Converts Gregorian date to seconds since 1970-01-01 0000.

* Assumes input in normal date format, i.e. 1980-12-31 2359

* => year=1980, mon=12, day=31, hour=23, min=59, sec=59.

*/

u32 mktimev(struct rtc_time *tm)

{

if (0 >= (int) (tm->tm_mon -= 2)) { /* 1..12 -> 11,12,1..10 */

tm->tm_mon += 12; /* Puts Feb last since it has leap day */

tm->tm_year -= 1;

}

return (((

(u32) (tm->tm_year/4 - tm->tm_year/100 + tm->tm_year/400 + 367*tm->tm_mon/12 + tm->tm_mday) +

tm->tm_year*365 - 719499

)*24 + tm->tm_hour /* now have hours */

)*60 + tm->tm_min /* now have minutes */

)*60 + tm->tm_sec; /* finally seconds */

}

void to_tm(u32 tim, struct rtc_time * tm)

{

register u32 i;

register long hms, day;

day = tim / SECDAY; //有多少天

hms = tim % SECDAY; //今天的时间,单位s

tm->tm_hour = hms / 3600;//时

tm->tm_min = (hms % 3600) / 60;//分

tm->tm_sec = (hms % 3600) % 60;//秒

/*算出当前年份,起始的计数年份为1970年*/

for (i = STARTOFTIME; day >= days_in_year(i); i++) {

day -= days_in_year(i);

}

tm->tm_year = i;

/*计算当前的月份*/

if (leapyear(tm->tm_year)) {

days_in_month(FEBRUARY) = 29;

}

for (i = 1; day >= days_in_month(i); i++) {

day -= days_in_month(i);

}

days_in_month(FEBRUARY) = 28;

tm->tm_mon = i;

tm->tm_mday = day + 1;//计算当前日期

GregorianDay(tm); //计算当前是星期几

}

5、配置时间

有了以上的准备,接下来学习一下Time_Adjust函数。Time_Adjust函数用于配置时间,它先调用前面的RTC_Configuration初始化RTC,接着调用库函数RTC_SetCounter向RTC计数器写入要设置时间的时间戳值,而时间戳的值则使用mktimev函数通过输入参数tm来计算,计算后还与宏TIME_ZOOM运算,计算时区偏移值。此处的输入参数tm是北京时间,所以“mktimev(tm)- TIME_ZOOM”计算后写入到RTC计数器的是格林威治时区的标准UNIX时间戳。

/*

* 函数名:Time_Adjust

* 描述 :时间调节

* 输入 :用于读取RTC时间的结构体指针(北京时间)

* 输出 :无

*/

void Time_Adjust(struct rtc_time *tm)

{

RTC_Configuration();//RTC 配置

RTC_WaitForLastTask(); //等待确保上一次操作完成

GregorianDay(tm); //计算星期

RTC_SetCounter(mktimev(tm)-TIME_ZOOM); //由日期计算时间戳并写入到RTC计数寄存器

RTC_WaitForLastTask(); //等待确保上一次操作完成

}

6、检查并配置RTC

上面的Time_Adjust函数直接把参数写入到RTC中修改配置,但在芯片每次上电时,并不希望每次都修改系统时间,所以我们增加了RTC_CheckAndConfig函数用于检查是否需要向RTC写入新的配置。

/*

* 函数名:RTC_CheckAndConfig

* 描述 :检查并配置RTC

* 输入 :用于读取RTC时间的结构体指针

* 输出 :无

*/

void RTC_CheckAndConfig(struct rtc_time *tm)

{

/*在启动时检查备份寄存器BKP_DR1,如果内容不是0xA5A5,则需重新配置时间并询问用户调整时间*/

if (BKP_ReadBackupRegister(RTC_BKP_DRX) != RTC_BKP_DATA)

{

Time_Adjust(tm);//使用tm的时间配置RTC寄存器

BKP_WriteBackupRegister(RTC_BKP_DRX, RTC_BKP_DATA);//向BKP_DR1寄存器写入标志

}

else

{

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//使能时钟

PWR_BackupAccessCmd(ENABLE);//允许访问 Backup 区域

#ifdef RTC_CLOCK_SOURCE_LSI// LSE启动无需设置新时钟

RCC_LSICmd(ENABLE);//使能 LSI

while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); //等待 LSI 准备好

#endif

if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)//检查是否掉电重启

{

printf(" Power On Reset occurred....");

}

else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)//检查是否Reset复位

{

printf(" External Reset occurred....");

}

printf(" No need to configure RTC....");

RTC_WaitForSynchro();//等待寄存器同步

RTC_ITConfig(RTC_IT_SEC, ENABLE);//允许RTC秒中断

RTC_WaitForLastTask();//等待上次RTC寄存器写操作完成

}

RCC_ClearFlag();//清除复位标志 flags

}

在本函数中,会检测备份域寄存器RTC_BKP_DRX内的值是否等于RTC_BKP_DATA而分成两个分支。若不等,说明之前没有配置RTC所以直接调用Time_Adjust函数初始化RTC并写入时间戳进行计时,配置完成后向备份域寄存器RTC_BKP_DRX写入值RTC_BKP_DATA作为标志,这样该标志就可以指示RTC的配置情况了,因为备份域不掉电时,RTC和该寄存器的值都会保存完好,而如果备份域掉电,那么RTC配置和该标志都会一同丢失。

若本函数的标志判断相等,进入else分支,不再调用Time_Adjust函数初始化RTC,而只是使用RTC_WaitForSynchro和RTC_ITConfig同步RTC域和APB以及使能中断,以便获取时间。如果使用的是LSI时钟,还需要使能LSI时钟,RTC才会正常运行,这是因为当主电源掉电和备份域的情况下LSI会关闭,而LSE则会正常运行,驱动RTC计时。

7、转换并输出时间

RTC正常运行后,可以使用Time_Display函数转换时间格式并输出到串口。

/*

* 函数名:Time_Display

* 描述 :显示当前时间值

* 输入 :-TimeVar RTC计数值,单位为 s

* 输出 :无

*/

void Time_Display(uint32_t TimeVar,struct rtc_time *tm)

{

static uint32_t FirstDisplay = 1;

uint32_t BJ_TimeVar;

uint8_t str[200]; //字符串暂存

BJ_TimeVar =TimeVar + TIME_ZOOM; //把标准时间转换为北京时间

to_tm(BJ_TimeVar, tm);//把定时器的值转换为北京时间

if((!tm->tm_hour && !tm->tm_min && !tm->tm_sec) || (FirstDisplay))

{

GetChinaCalendar((u16)tm->tm_year, (u8)tm->tm_mon, (u8)tm->tm_mday, str);

printf(" 今天新历:%0.2d%0.2d,%0.2d,%0.2d", str[0], str[1], str[2], str[3]);

GetChinaCalendarStr((u16)tm->tm_year,(u8)tm->tm_mon,(u8)tm->tm_mday,str);

printf(" 今天农历:%s ", str);

if(GetJieQiStr((u16)tm->tm_year, (u8)tm->tm_mon, (u8)tm->tm_mday, str))

{

printf(" 今天农历:%s ", str);

}

FirstDisplay = 0;

}

/* 输出时间戳,公历时间 */

printf(" UNIX时间戳 = %d 当前时间为: %d年(%s年) %d月 %d日 (星期%s) %0.2d:%0.2d:%0.2d ",TimeVar,

tm->tm_year, zodiac_sign[(tm->tm_year-3)%12], tm->tm_mon, tm->tm_mday,

WEEK_STR[tm->tm_wday], tm->tm_hour, tm->tm_min, tm->tm_sec);}

本函数的核心部分已加粗显示,主要是使用to_tm把时间戳转换成日常生活中使用的时间格式,to_tm以BJ_TimeVar作为输入参数,而BJ_TimeVar对时间戳变量Time_Var进行了时区偏移,也就是说调用Time_Display函数时,以RTC计数器的值作为TimeVar作为输入参数即可,最终会输出北京时间。利用to_tm转换格式后,调用bsp_calendar.c文件中的日历计算函数,求出星期、农历、生肖等内容,然后使用串口显示出来。

8、中断服务函数

一般来说,上面的Time_Display时间显示每秒中更新一次,而根据前面的配置,RTC每秒会进入一次中断,本实验中的RTC中断服务函数如下。RTC的秒中断服务函数只是简单地对全局变量TimeDisplay置1,在main函数的while循环中会检测这个标志,当标志为1时,就调用Time_Display函数显示一次时间,达到每秒钟更新当前时间的效果。

void RTC_IRQHandler(void)

{

if (RTC_GetITStatus(RTC_IT_SEC) != RESET)

{

RTC_ClearITPendingBit(RTC_IT_SEC); //清中断标志

TimeDisplay = 1; //置位秒显示更新任务标志

RTC_WaitForLastTask(); //等待RTC操作完成

}

}

9、main函数

main函数的流程非常清晰,初始化了按键、串口等外设后,调用RTC_CheckAndConfig函数初始化RTC,若RTC是第一次初始化,就使用变量systmtime中的默认时间配置,若之前已配置好RTC,那么RTC_CheckAndConfig函数仅同步时钟系统,便于获取实时时间。在 while循环里检查中断设置的TimeDisplay是否置1,若置1了就调用Time_Display函数,它的输入参数是库函数RTC_GetCounter的返回值,也就是RTC计数器里的时间戳,Time_Display函数把该时间戳转化成北京时间显示到串口上。

/**

* @brief 主函数

* @param

* @retval 无

*/

int main()

{

USART_Config();

Key_GPIO_Config();

RTC_NVIC_Config();/* 配置RTC秒中断优先级 */

RTC_CheckAndConfig(&systmtime);

while (1)

{

if (TimeDisplay == 1)//每过1s 更新一次时间

{

Time_Display( RTC_GetCounter(),&systmtime); //当前时间

TimeDisplay = 0;

}

//按下按键,通过串口修改时间

if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )

{

struct rtc_time set_time;

Time_Regulate_Get(&set_time);//使用串口接收设置的时间,输入数字时注意末尾要加回车

Time_Adjust(&set_time);//用接收到的时间设置RTC

BKP_WriteBackupRegister(RTC_BKP_DRX, RTC_BKP_DATA);//向备份寄存器写入标志

}

}

}

main函数中当检测到开发板上的KEY1被按下时,会调用Time_Regulate_Get函数通过串口获取配置时间,然后把获取得的时间输入到Time_Adjust函数把该时间写入到RTC计数器中,更新配置。Time_Regulate_Get函数的本质是利用重定向到串口的C标准数据流输入函数scanf获取用户输入,若获取得的数据符合范围,则赋值到tm结构体中,在main函数中再调用Time_Adjust函数把tm存储的时间写入到RTC计数器中。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 寄存器
    +关注

    关注

    31

    文章

    5394

    浏览量

    122199
  • 开发板
    +关注

    关注

    25

    文章

    5264

    浏览量

    99787
  • 万年历
    +关注

    关注

    3

    文章

    189

    浏览量

    24132
  • RTC
    RTC
    +关注

    关注

    2

    文章

    597

    浏览量

    67677

原文标题:MCU微课堂|CKS32F107xx RTC(二)

文章出处:【微信号:中科芯MCU,微信公众号:中科芯MCU】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    相关推荐

    电子万年历设计

    电子万年历设计
    发表于 08-20 22:46

    万年历

    如何在万年历加上闹钟
    发表于 09-24 15:29

    万年历电路图

    万年历电路图万年历电路图
    发表于 08-05 14:59

    万年历

    万年历万年历万年历万年历
    发表于 03-20 21:08

    万年历 仿真

    游戏 万年历
    发表于 07-08 11:19

    怎么用FPGA实现万年历的设计

    问各位大神们怎样用FPGA实现数码万年历的设计
    发表于 07-03 08:31

    STM32RTC万年历制作本设计

    写博客,请多多关照本设计是用STM32F103c8t6制作的简单万年历后续功能会添加,也请广大网友给本设计出出主意,若有错误或更好的方法,请多多指正,虚心受教,谢谢首先是配置RTC时钟用库函数...
    发表于 08-12 06:26

    如何利用C51单片机实现万年历设计?

    如何利用C51单片机实现万年历设计?
    发表于 11-08 06:39

    基于单片机的液晶多功能万年历设计资料分享

    单片机实训之万年历(具有时分秒,日期,星期调校功能什么是万年历????年历是中国古代传说中最古老的一部太阳历。万年历是记录一定时间范围内(比
    发表于 11-10 08:04

    stm32如何实现秒表及万年历的设计?

    stm32如何实现秒表及万年历的设计?
    发表于 12-15 07:06

    如何利用STM32与3264点阵屏实现功能万年历的设计

    基于STM32与3264点阵屏的多功能万年历一、前因一次偶然的机会,笔者得到了一块二手的3264双色点阵屏,一番把玩过后发现这个屏幕的显示效果还是很棒的,就萌生了一个用这块屏diy的想法,思来想去
    发表于 01-07 07:13

    基于FPGA的多功能电子万年历

    基于FPGA的多功能电子万年历,毕业论文
    发表于 10-29 17:19 22次下载

    万年历

    电子万年历,可以运行的哦,单片机相关知识。
    发表于 05-17 11:09 16次下载

    万年历

    基于C51单片机的万年历
    发表于 12-17 20:48 119次下载

    万年历protues仿真 实时时钟仿真 12864万年历仿真 5

    万年历protues仿真 实时时钟仿真 12864万年历仿真 51万年历设计
    发表于 01-14 22:32 175次下载