STOpen之RTC-片内RTC的可靠初始化及时间转换算法

本章我们将从硬件和软件,应用几个方面来详细的讲解ST32F103的RTC实时时钟的配置方法,编程方法,以及设计注意事项。
首先我们看看RTC的框图如下,它除了RTC实时时钟以外,还具有报警功能,报警功能的主要作用就是用来把系统从深度睡眠状态唤醒,从而可以以极低功耗的模式运行系统功能,其唤醒作用和通过外部引脚WKUP唤醒一样。
本章我们主要讲解RTC的使用,报警功能另外章节再详细剖析。
在这里插入图片描述
要使RTC能按照我们预期的方式正常运行,我们先看看其硬件组成:

  1. 电源

RTC部分的电源在系统VDD供电的时候,通过一个内部开关会切换到VDD来供电,减少对Vbat引脚外部电池电源的消耗。
当系统VDD断电或者掉电以后,该开关会自动切换到Vbat引脚供电,从而以极低的运行功耗维持RTC部分实时时钟相关功能的运行。
电源路径如下图所示:
在这里插入图片描述
红色是VDD存在的时候的供电路径,蓝色是VDD消失后的供电路径。
在3.3V供电的时候,RTC区域只需要消耗1.4uA的功耗,所以系统可以以非常低的电流消耗维持一个长时间的RTC功能应用,从而实现我们的RTC时钟功能。
在这里插入图片描述
我们在Vbat引脚可以增加一个外部电池,在系统掉电后来维持RTC部分的电源供给,其电路如下图所示:
在这里插入图片描述

  1. 时钟

要让RTC驱动起来,少不了提供一个精确的时钟提供者。
RTC部分可以有如下图所示的三条时钟路径,如果我们是用来做精确的计时作用,就需要使用外部32768Hz的晶体来提供精确的时钟源,其他两个时钟源不能满足RTC计时的准确性。
因为32768Hz的时钟通过2的15分频后可以得到准确的1Hz的时钟源,其他两路都无法提供如此精确的时钟。
时钟的精度取决于外部晶体的精度,如果你对时钟的要求比较高,需要每一个产品在生产的时候进行校正,匹配晶体的电容,提高晶体的频率输出精度,一般精度大约在10-30s/月。要提高精度有两个办法,一是每一片校正的时候,还可以利用片内的校正寄存器进行校正,可以将精度提高到5-10s/月。另外一个办法就是如果你的产品是联网工作的,通过网络进行校正。
在这里插入图片描述

  1. 初始化

ST32F103的时钟在第一次上电的时候我们需要进行初始化,如果备份电源存在的话,以后主电源(一般3.3V)再次上电,就不需要初始化了,否则时钟就乱了,不能继续计时,而是变成每一次都被初始化为一个固定的值了,达不到实时时钟的目的。
那么如何来判断我们是第一次上电还是第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("晶体没有起振\r\n");
		}
		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....\r\n");
        RtcNVICConfig();
    } 
    PWR_BackupAccessCmd(DISABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, DISABLE);
}

以后使用的时候我们就可以通过两个接口来访问RTC部分,提供标准输出:

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*************************************/
  1. 时间格式化和转换

该系列芯片的时钟计时功能,只是提供了一个寄存器对时钟源进行计数,并没有转换为标准的时间格式,所以我们需要对其进行转换,方便应用程序使用。
转换分为两部分,一个是将寄存器的数值(秒)转换为年月日时分秒的格式,另外一个就是将年月日时分秒转换为秒为单位的数值存储进去。
在进行计时的时候,我们需要选择一个基准时间,也就是寄存器的数值,究竟代表什么时间?比如经过一段时间的运行,寄存器读回来的数值是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;
}

以上的方法适用于任何其他类似架构的芯片的时间处理,有些芯片提供了年月日时分秒的寄存器,那就不需要进行后面的转换工作了,简化了我们的编程工作。
原创文章,欢迎转载,请注明来源,未经书面允许,请勿用于商业用途。
在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值