本章我们将从硬件和软件,应用几个方面来详细的讲解ST32F103的RTC实时时钟的配置方法,编程方法,以及设计注意事项。 RTC部分的电源在系统VDD供电的时候,通过一个内部开关会切换到VDD来供电,减少对Vbat引脚外部电池电源的消耗。 要让RTC驱动起来,少不了提供一个精确的时钟提供者。 ST32F103的时钟在第一次上电的时候我们需要进行初始化,如果备份电源存在的话,以后主电源(一般3.3V)再次上电,就不需要初始化了,否则时钟就乱了,不能继续计时,而是变成每一次都被初始化为一个固定的值了,达不到实时时钟的目的。 以后使用的时候我们就可以通过两个接口来访问RTC部分,提供标准输出: 头文件声明: 该系列芯片的时钟计时功能,只是提供了一个寄存器对时钟源进行计数,并没有转换为标准的时间格式,所以我们需要对其进行转换,方便应用程序使用。 以上的方法适用于任何其他类似架构的芯片的时间处理,有些芯片提供了年月日时分秒的寄存器,那就不需要进行后面的转换工作了,简化了我们的编程工作。
首先我们看看RTC的框图如下,它除了RTC实时时钟以外,还具有报警功能,报警功能的主要作用就是用来把系统从深度睡眠状态唤醒,从而可以以极低功耗的模式运行系统功能,其唤醒作用和通过外部引脚WKUP唤醒一样。
本章我们主要讲解RTC的使用,报警功能另外章节再详细剖析。
要使RTC能按照我们预期的方式正常运行,我们先看看其硬件组成:
当系统VDD断电或者掉电以后,该开关会自动切换到Vbat引脚供电,从而以极低的运行功耗维持RTC部分实时时钟相关功能的运行。
电源路径如下图所示:
红色是VDD存在的时候的供电路径,蓝色是VDD消失后的供电路径。
在3.3V供电的时候,RTC区域只需要消耗1.4uA的功耗,所以系统可以以非常低的电流消耗维持一个长时间的RTC功能应用,从而实现我们的RTC时钟功能。
我们在Vbat引脚可以增加一个外部电池,在系统掉电后来维持RTC部分的电源供给,其电路如下图所示:
RTC部分可以有如下图所示的三条时钟路径,如果我们是用来做精确的计时作用,就需要使用外部32768Hz的晶体来提供精确的时钟源,其他两个时钟源不能满足RTC计时的准确性。
因为32768Hz的时钟通过2的15分频后可以得到准确的1Hz的时钟源,其他两路都无法提供如此精确的时钟。
时钟的精度取决于外部晶体的精度,如果你对时钟的要求比较高,需要每一个产品在生产的时候进行校正,匹配晶体的电容,提高晶体的频率输出精度,一般精度大约在10-30s/月。要提高精度有两个办法,一是每一片校正的时候,还可以利用片内的校正寄存器进行校正,可以将精度提高到5-10s/月。另外一个办法就是如果你的产品是联网工作的,通过网络进行校正。
那么如何来判断我们是第一次上电还是第2次以后的上电呢?这里我们使用了一个技巧,利用芯片内部提供的备份域的寄存器在主电源掉电后也能通过Vbat维持的特性来做一个标志,从而进行判断,得知是那一次上电。
芯片的备份域提供了42个寄存器给我们使用,我们只需要使用2个寄存器来做这个标志就足够了。为什么不是一个寄存器而是两个呢?为了防止错误,我们相当于买了个双保险,只有两个寄存器都正确的时候才可以确定是已经初始化过RTC了,从而提高了代码的强壮性。
初始化流程说明:
使能备份域和RTC电源部分的时钟
使能备份域读写允许功能
读两个备份寄存器,根据标志判断是否需要初始化RTC,如果需要初始化,就进行下面的初始化流程,否则直接disable掉备份域的读写就退出了。
第一次初始化RTC需要先打开LSE时钟(32768HZ),并且等待其正常起振工作。在这里如果晶体起振失败,我们可以以声音或者LED闪烁的方式作出提示。
起振正常以后,我们配置RTC的时钟源为LSE,并且enable,然后等待它完成。为什么呢?因为RTC部分的工作时钟这时候变成了32K,而我们主系统时钟很高,一般是72MHz,所以快的要等待慢的完成,才能同步,并且读/写到正确的内容,后面所有对RTC部分的访问都要遵循这个原则。
然后我们设置正确的分频系数,得到1s的触发中断,驱动RTC部分计时。有人会问,我可不可以设置其他的分频系数呢?答案是肯定的。比如你可以设置0.5s就产生一个计数时钟,那么在你读取计时寄存器的数值后,需要做一个除以2的动作,才是1s的时间单位,这样一来,是不是多此一举了呢?
最后我们再设置一个初始时间进去,作为计时的开始。准确的起始时间以后还需要通过其他工具通过通讯接口来初始化,或者按键菜单进行调整,才能正确的和当前时间同步。
最后我们写入标志位,确认RTC已经初始化,下次再次上电就不需要再初始化了。
详细代码如下:void RtcInit(void) { UINT32 StartUpCounter = 0,LSEStatus; RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能power和备份区域 PWR_BackupAccessCmd(ENABLE); //允许访问备份区域 //使用两个备份区域的寄存器作为rtc是否已经被初始化过的标记 if ((BKP_ReadBackupRegister(BKP_DR1) != RTC_FALG1) || (BKP_ReadBackupRegister(BKP_DR2) != RTC_FALG2)) { BKP_DeInit(); RCC_LSEConfig(RCC_LSE_ON); StartUpCounter = 0; do { LSEStatus = RCC_GetFlagStatus(RCC_FLAG_LSERDY); StartUpCounter++; } while((LSEStatus == RESET) && (StartUpCounter < LSE_STARTUP_TIMEOUT));//Wait LSE is ready if(LSEStatus == RESET) {//晶振启动失败,可以在这里闪烁一个led,鸣叫蜂鸣器等提示 DebugPrintf("晶体没有起振rn"); } else { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); //读操作前等待APB1总线同步 RTC_WaitForLastTask(); //等待写寄存器完成 RtcNVICConfig(); RTC_SetPrescaler(32767); //RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1) RTC_WaitForLastTask(); { DATETIME datetime; datetime.year = 2020; //写一个错误的时间(比当前时间旧的时间即可),以便掉电后检测到时间错误 datetime.month = 5; datetime.day = 1; datetime.week = 5; //2020.5.1=星期五 datetime.hour = 10; datetime.minute = 30; datetime.second = 0; RTC_SetCounter(RtcToSecond(&datetime)); RTC_WaitForLastTask(); } BKP_WriteBackupRegister(BKP_DR1, RTC_FALG1); BKP_WriteBackupRegister(BKP_DR2, RTC_FALG2); //写入初始化成功标志,下次从新上电就不需要再次初始化了 } } else { DebugPrintf("RTC configured....rn"); RtcNVICConfig(); } PWR_BackupAccessCmd(DISABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, DISABLE); }
void GetRtc(DATETIME *dt) { UINT32 sec; RTC_WaitForSynchro(); sec = RTC_GetCounter(); SecondToRtc(dt,sec); dt->week = GetWeekDay(dt); //计算出来星期 } void SetRtc(DATETIME *dt) { RCC_Reset_Backup(); RtcInit(); //时间错误后,再次设置的时候初始化一下,不然有个别芯片存储不了新的时间。算是芯片的一个bug RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); RTC_WaitForSynchro(); RTC_SetCounter(RtcToSecond(dt)); RTC_WaitForLastTask(); PWR_BackupAccessCmd(DISABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, DISABLE); }
/*********************************************************************** [File] rtc.h [version] 1.0.0 [author] huangbin [date] 2020.05.09 [email] huangembed@163.com [blog] https://blog.csdn.net/huangbinvip [wechat Account] huangbinmbed ------------------------------------------------------------------------ COPYRIGHT 2020 SHENZHEN ZHONG EMBENDED CO.,LTD. ************************************************************************/ #ifndef __RTC_H__ #define __RTC_H__ #include "datatype.h" #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ typedef struct tagDateTime{ UINT16 year; // current year(2000-2100) UINT8 month; // month (1-12) UINT8 day; // day of the month(1-31) UINT8 hour; // hours(0-23) UINT8 minute; // minutes(0-59) UINT8 second; // seconds(0-59) UINT8 week; // week(1-7,7=sunday) }DATETIME; /*************************************************************************** 函数名称: RtcInit 功能描述: 初始化实时时钟 输入参数: 无 输出参数: 无 使用注意: ***************************************************************************/ SYS_EXTERN void RtcInit(void); /*************************************************************************** 函数名称: GetRtc 功能描述: 获取实时时钟 输入参数: 无 输出参数: dt:保存日期和时间 使用注意: ***************************************************************************/ SYS_EXTERN void GetRtc(DATETIME *dt); /*************************************************************************** 函数名称: SetRtc 功能描述: 设置实时时钟 输入参数: dt:要设置的日期和时间 输出参数: 无 使用注意: ***************************************************************************/ SYS_EXTERN void SetRtc(DATETIME *dt); #ifdef __cplusplus } #endif /* __cplusplus */ #endif /************************END OF FILE*************************************/
转换分为两部分,一个是将寄存器的数值(秒)转换为年月日时分秒的格式,另外一个就是将年月日时分秒转换为秒为单位的数值存储进去。
在进行计时的时候,我们需要选择一个基准时间,也就是寄存器的数值,究竟代表什么时间?比如经过一段时间的运行,寄存器读回来的数值是500,那么它究竟代表多少时间呢?答案是多少都可以,取决于我们的定义。
一般我们选择2000年1月1日 00:00:00作为起始时间比较方便,也就是寄存器的值为1的时候代表2000年1月1日 00:00:00,如此一来,500就代表2000年1月1日 00:08:20。芯片的计数器是32位的,如果1s计数加1,能提供大约136年的时间记录,所以我们不用担心它会溢出。
下面是一个简化后的高效的转换方法,在2000-2100年能够正确运行。//将时间转换距2000.1.1 0:00:00的秒数 //返回转换结果的数值 UINT8 const DayOfMonthList[] = {31,28,31,30,31,30,31,31,30,31,30,31}; UINT32 RtcToSecond(DATETIME *pDt) { UINT32 i; UINT32 TotalDay; UINT32 TotalSec; TotalDay = pDt->year - 2000; if( TotalDay ) //不是2000年 TotalDay = TotalDay * 365 + (TotalDay + 3) / 4; //计算当前年距离起始年的天数和经过了多少个闰年 for(i = 0; i < (UINT32)pDt->month - 1;i++) //再计算余下的月份里有多少天 { TotalDay += DayOfMonthList[i]; } if((((pDt->year) % 4) == 0) && (pDt->month > 2)) //闰年(2000-2100年,简化,可以认为就是每隔4年一个闰年),并且当前月份大于2月 TotalDay++; TotalDay += pDt->day - 1; //计算出来已经过去的天数 TotalSec = (UINT32)TotalDay * 3600L * 24L; //过去的天数有多少秒 TotalSec += (UINT32)pDt->hour * 3600L +(UINT32) pDt->minute * 60L + (UINT32)pDt->second; return TotalSec; } //将秒数转换为时间 UINT16 const MonthDayofYear[12] = {31,31+28,31+28+31,31+28+31+30, 120+31,120+31+30,120+31+30+31,120+31+30+31+31, 243+30,243+30+31,243+30+31+30,243+30+31+30+31, }; UINT16 const MonthDayofleapYear[12] = {31,31+29,31+29+31,31+29+31+30, 121+31,121+31+30,121+31+30+31,121+31+30+31+31, 244+30,244+30+31,244+30+31+30,244+30+31+30+31, }; void SecondToRtc(DATETIME *pDt,UINT32 second) { UINT32 year,day,remain; UINT32 leap,i; UINT16 const *pTable; day = second / (3600L*24L) + 1; //多少天(是从1.1日起,不是0) year = day / 365; remain = day % 365; leap = (year + 3) / 4; //2000以来过了多少个闰年(不包含当前年) pDt->year = year + 2000; if(remain <= leap) //剩余天数不够补闰年 { pDt->year -= 1; if((pDt->year % 4) == 0) //闰年 remain = remain + 366 - leap; else remain = remain + 365 - leap; } else { remain -= leap; //已经过去的闰年,不包含本年 } if((pDt->year % 4) == 0) //闰年 pTable = MonthDayofleapYear; else pTable = MonthDayofYear; for(i = 0; i < 12;i++) { if(remain <= pTable[i]) break; } pDt->month = i + 1; if(i) pDt->day = remain - pTable[i - 1]; else pDt->day = remain; remain = second % (3600L*24); //剩余一天的秒数 pDt->hour = (UINT8)(remain / 3600L); remain = remain % 3600L; pDt->minute = remain / 60; pDt->second = remain % 60; } //基姆拉尔森计算公式: W= (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400) mod 7 // 在公式中d表示日期中的日数,m表示月份数,y表示年数。注意:在公式中有个与其他公式不同的地方: // 把一月和二月看成是上一年的十三月和十四月,例:如果是2004-1-10则换算成:2003-13-10来代入公式计算 //w=0-6(星期一-星期天) //根据年月日求出来星期几 UINT8 GetWeekDay(DATETIME *pDt) { UINT32 y,m,d; SINT32 week; y = pDt->year; m = pDt->month; d = pDt->day; if (m < 3) { m += 12; y -= 1; } week=(d+2*m+3*(m+1)/5+y+y/4-y/100+y/400)%7; week += 1; //我们定义的week=1-7,1=monday return week; }
原创文章,欢迎转载,请注明来源,未经书面允许,请勿用于商业用途。
本网页所有视频内容由 imoviebox边看边下-网页视频下载, iurlBox网页地址收藏管理器 下载并得到。
ImovieBox网页视频下载器 下载地址: ImovieBox网页视频下载器-最新版本下载
本文章由: imapbox邮箱云存储,邮箱网盘,ImageBox 图片批量下载器,网页图片批量下载专家,网页图片批量下载器,获取到文章图片,imoviebox网页视频批量下载器,下载视频内容,为您提供.
阅读和此文章类似的: 全球云计算