本章我们将从硬件和软件,应用几个方面来详细的讲解ST32F103的RTC实时时钟的配置方法,编程方法,以及设计注意事项。
首先我们看看RTC的框图如下,它除了RTC实时时钟以外,还具有报警功能,报警功能的主要作用就是用来把系统从深度睡眠状态唤醒,从而可以以极低功耗的模式运行系统功能,其唤醒作用和通过外部引脚WKUP唤醒一样。
本章我们主要讲解RTC的使用,报警功能另外章节再详细剖析。
要使RTC能按照我们预期的方式正常运行,我们先看看其硬件组成:
- 电源
RTC部分的电源在系统VDD供电的时候,通过一个内部开关会切换到VDD来供电,减少对Vbat引脚外部电池电源的消耗。
当系统VDD断电或者掉电以后,该开关会自动切换到Vbat引脚供电,从而以极低的运行功耗维持RTC部分实时时钟相关功能的运行。
电源路径如下图所示:
红色是VDD存在的时候的供电路径,蓝色是VDD消失后的供电路径。
在3.3V供电的时候,RTC区域只需要消耗1.4uA的功耗,所以系统可以以非常低的电流消耗维持一个长时间的RTC功能应用,从而实现我们的RTC时钟功能。
我们在Vbat引脚可以增加一个外部电池,在系统掉电后来维持RTC部分的电源供给,其电路如下图所示:
- 时钟
要让RTC驱动起来,少不了提供一个精确的时钟提供者。
RTC部分可以有如下图所示的三条时钟路径,如果我们是用来做精确的计时作用,就需要使用外部32768Hz的晶体来提供精确的时钟源,其他两个时钟源不能满足RTC计时的准确性。
因为32768Hz的时钟通过2的15分频后可以得到准确的1Hz的时钟源,其他两路都无法提供如此精确的时钟。
时钟的精度取决于外部晶体的精度,如果你对时钟的要求比较高,需要每一个产品在生产的时候进行校正,匹配晶体的电容,提高晶体的频率输出精度,一般精度大约在10-30s/月。要提高精度有两个办法,一是每一片校正的时候,还可以利用片内的校正寄存器进行校正,可以将精度提高到5-10s/月。另外一个办法就是如果你的产品是联网工作的,通过网络进行校正。
- 初始化
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);
}
头文件声明:
#ifndef __RTC_H__
#define __RTC_H__
#include "datatype.h"
#ifdef __cplusplus
extern "C" {
#endif
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;
SYS_EXTERN void RtcInit(void);
SYS_EXTERN void GetRtc(DATETIME *dt);
SYS_EXTERN void SetRtc(DATETIME *dt);
#ifdef __cplusplus
}
#endif
#endif
- 时间格式化和转换
该系列芯片的时钟计时功能,只是提供了一个寄存器对时钟源进行计数,并没有转换为标准的时间格式,所以我们需要对其进行转换,方便应用程序使用。
转换分为两部分,一个是将寄存器的数值(秒)转换为年月日时分秒的格式,另外一个就是将年月日时分秒转换为秒为单位的数值存储进去。
在进行计时的时候,我们需要选择一个基准时间,也就是寄存器的数值,究竟代表什么时间?比如经过一段时间的运行,寄存器读回来的数值是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;
}
以上的方法适用于任何其他类似架构的芯片的时间处理,有些芯片提供了年月日时分秒的寄存器,那就不需要进行后面的转换工作了,简化了我们的编程工作。
原创文章,欢迎转载,请注明来源,未经书面允许,请勿用于商业用途。