STM32之RTC实时时钟
RTC实时时钟简介:
STM32的RTC外设,实质是一个掉电后还继续运行的定时器,从定时器的角度来看,相对于通用定时器TIM外设,它的功能十分简单,只有计时功能(也可以触发中断)。但是从掉电还能继续运行来看,它是STM32中唯一一个具有这个功能功能的外设。(RTC外设的复杂之处不在于它的定时,而在于它掉电还可以继续运行的特性)
所谓掉电,是指电源Vpp断开的情况下,为了RTC外设掉电可以继续运行,必须给STM32芯片通过VBAT引脚街上锂电池。当主电源VDD有效时,由VDD给RTC外设供电。当VDD掉电后,由VBAT给RTC外设供电。无论由什么电源供电,RTC中的数据始终都保存在属于RTC的备份域中,如果主电源和VBA都掉电,那么备份域中保存的所有数据都将丢失。(备份域除了RTC模块的寄存器,还有42个16位的寄存器可以在VDD掉电的情况下保存用户程序的数序,系统复位或电源复位时,这些数据也不会被复位)。
从RTC的定时器特性来说,它是一个32位的计数器,只能向上计数。他使用的时钟源有三种,分别为:
1,高速外部时钟的128分频:HSE/128;
2,低速内部时钟LSI;
3,低速外部时钟LSE;
使用HSE分频时钟或者LSI的时候,在主电源VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证RTC正常工作。所以RTC一般都时钟低速外部时钟LSE,频率为实时时钟模块中常用的32.768KHz,因为32768 = 2^15,分频容易实现,所以被广泛应用到RTC模块。(在主电源VDD有效的情况下(待机),RTC还可以配置闹钟事件使STM32退出待机模式)。
RTC工作过程:
RTC架构:
图中浅灰色的部分都是属于备份域的,在VDD掉电时可在VBAT的驱动下继续运行。这部分仅包括RTC的分频器,计数器,和闹钟控制器。若VDD电源有效,RTC可以触发RTC_Second(秒中断)、RTC_Overflow(溢出事件)和RTC_Alarm(闹钟中断)。从结构图可以看到到,其中的定时器溢出事件无法被配置为中断。如果STM32原本处于待机状态,可由闹钟事件或WKUP事件(外部唤醒事件,属于EXTI模块,不属于RTC)使它退出待机模式。闹钟事件是在计数器RTC_CNT的值等于闹钟寄存器RTC_ALR的值时触发的。
因为RTC的寄存器是属于备份域,所以它的所有寄存器都是16位的。它的计数RTC_CNT的32位由RTC_CNTL和RTC_CNTH两个寄存器组成,分别保存计数值的低16位和高16位。在配置RTC模块的时钟时,把输入的32768Hz的RTCCLK进行32768分频得到实际驱动计数器的时钟TR_CLK = RTCCLK/37768 = 1Hz,计时周期为1秒,计时器在TR_CLK的驱动下计数,即每秒计数器RTC_CNT的值加1(常用)
由于备份域的存在,使得RTC核具有了完全独立于APB1接口的特性,也因此对RTC寄存器的访问要遵守一定的规则。
系统复位后,禁止访问后备寄存器和RCT,防止对后卫区域(BKP)的意外写操作。(执行以下操作使能对后备寄存器好RTC的访问):
1,设置RCC_APB1ENR寄存器的PWREN和BKPEN位来使能电源和后备接口时钟。
2,设置PWR_CR寄存器的DBP位使能对后备寄存器和RTC的访问。
设置为可访问后,在第一次通过APB1接口访问RTC时,必须等待APB1与RTC外设同步,确保被读取出来的RTC寄存器值是正确的,如果在同步之后,一直没有关闭APB1的RTC外设接口,就不需要再次同步了。
如果内核要对RTC寄存器进行任何的写操作,在内核发出写指令后,RTC模块在3个RTCCLK时钟之后,才开始正式的写RTC寄存器操作。我们知道RTCCLK的频率比内核主频低得多,所以必须要检查RTC关闭操作标志位RTOFF当这个标志被置1时,写操作才正式完成。
(以上操作在STM32库里面都有库函数,不需要具体的查阅寄存器~~~~)
UNIX时间戳:
假如从现在起,把计数器RTC_CNT的计数值置0,然后每秒加1,RTC_CNT什么时候会溢出? RTC_CNT是一个32位寄存器,可存储的最大值为(2^32-1),这样的话就是在2^32秒之后溢出,大概换算为:
Time = 2^32/365/24/60/60大约等于136年
假如某个时刻读取到计数器的数值为X = 60*60*24*2(2天),又知道计数器是在2016年1月1日的0时0分0秒置0的,那么根据计数器的这个相对时间数值,可以计算得到这个时刻是2016年1月3日的0时0分0秒了,而计数器会在(2016+136)年左右溢出。(如果我们穿越回到2016年1月1日,如果还在使用这个计数器提供事件的话就会出问题啦。).
定时器被置0的这个事件被称为计时元年,相对计时元年经过的秒数称为时间戳。
PS:
大多数操作系统都是利用时间戳和计时元年来计算当前时间的,而这个时间戳和计时元年大家都取了同一个标准——UNIX时间戳和UNIX计时元年.UNIX 计时元年被设置为格林威治时间1970年1月1日0时0分0秒,大概是为了纪念UNIX的诞生吧。而UNIX时间戳即为当前时间相对于UNIX计时元年经过的秒数。在这个计时系统中,使用的是有符号的32位整型变量来保存UNIX时间戳的,即实际可用计数位数比我们上面例子中的少了一位,少了这一位,UNIX 计时元年也相对提前了,这个计时方法在2038年1月19日03时14分07秒将会发生溢出。这个时间离我们并不远,UNIX时间戳被广泛应用到各种系统中,溢出可能会导致系统发生严重错误,差不多到这个时候,记得注意这个问题呀。
实例分析:
利用RTC提供北京时间:
RTC外设这个连续计数的计数器,在相应软件配置下,可提供时钟日历的功能,修改计数器的值则可以重新设置系统当前的时间和日期。而 由于它的时钟配置系统(RCC_BDCR 寄存器)是在备份域,在系统复位或从待机模式唤醒后RTC的设置和时间维持不变,利用它,可以实现实时时钟的功能。
main函数:
struct rtc_time systmtime;
int main(void)
{
/串口配置/
USART1_Config();
/配置RTC秒中断优先级/
NVIC_Configuration();
//RTC检测及配置
RTC_CheckAndConfig(&systmtime);
//刷新时间
Time_Show(&systmtime);
}
main函数流程:
1,用到了串口,配置好串口(代码和之前的例程一样);
2,配置RTC秒中断优先级,这里设置主优先级为1,次优先级为0(只用到一个RTC,中断随便写都可以)。(代码和之前的中断例程相似,只不过中断通道不一样,这里使用的中断通道是RTC_IRQn);
3,查看RTC外设是否在本次VDD上电前被配置过,如果没有被配置过,则需要输入当前时间,重新初始化RTC和配置时间;
4,配置好RTC后,根据秒中断设置的标志位,每隔1秒向终端更新一次;
事件管理结构体 rtc_time
struct rtc_time
{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
}
这个类型的结构体有时,分,秒,日,月,年及星期7个成员。当需要给RTC的计时器重新配置时间时(更改时间戳),肯定不会询问用户现在距离UNIX计时元年过了多少秒,而是向用户询问现在的公元纪年,以及所在时区的事件。根据RTC计时器向用户输出时间。
这就是 rtc_time 这个结构体的作用,配置RTC时,保存用户输入的时间,其它函数通过它求出UNIX时间戳,写入RTC,RTC正常运行后,需要输出时间时,其它函数通过RTC获取UNIX时间戳,转化成用友好的时间表示方式保存在这个结构体上。
PS:
起始在C语言标准库ANSI C中,也有类似的结构体所以 struct tm,位于标准的time.h文件中,转化函数是mktime()和localtime(),分别把tm结构体成员转化成时间戳和用时间戳转化成结构体成员。
检查RTC RTC_CheckAndConfig()
void RTC_CheckAndConfig(struct rtc_time *tm)
{
/检查备份寄存器BKP_DR1,内容不为0xA5A5,则需要重新配置时间并且询问用户调整时间/
if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
printf(“rnrn RTC not yet configured…。”);
/* RTC 配置 */
RTC_Configuration();
printf(“rnrn RTC configured…。”);
/* 用户输入时间*/
Time_Adjust(tm);
/再往备份寄存器BKP_DR1写入0xA5A5/
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
/启动无需设置新时钟/
else
{
/检查是否掉电重启/
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)
{
printf(“rnrn Power On Reset occurred…。”);
}
/检查是否Reset复位/
else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)
{
printf(“rnrn External Reset occurred…。”);
}
printf(“rn No need to configure RTC…。”);
/等待寄存器同步/
RTC_WaitForSynchro();
/允许RTC秒中断/
RTC_ITConfig(RTC_IT_SEC, ENABLE);
/等待上次RTC寄存器写操作完成/
RTC_WaitForLastTask();
}
/定义了时钟输出宏,则配置校正时钟输出到 PC13,用于RTC时钟频率的校准或调整时间补偿/
#ifdef RTCClockOutput_Enable
/使能PWR和BKP的时钟/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
/允许访问BKP备份域/
PWR_BackupAccessCmd(ENABLE);
/输出64分频时钟/
BKP_RTCOutputConfig(BKP_RTCOutputSource_CalibClock);
#endif
RCC_ClearFlag();
}
if语句调用BKP_ReadBackupRegister()读取RTC备份域寄存器里面的值,判断备份寄存器里面的是否正确,根据后面代码,如果配置成功,会向备份域寄存器写入数值0xA5A5.
(这个数值在VDD掉电后仍然会保存,如果VBAT也掉电,那么备份域,RTC所有寄存器将被复位,这时这个寄存器的值就不会等于0xA5A5了,RTC的计数器的值也是无效的。
简单的说,就是写入的这个数值用作标志RTC是否从未被配置或配置是否已经失效,然后写入任何数值到任何一个备份域寄存器,只要检查的时候与写入值匹配就行了)
RTC未被配置或者配置已经失效的情况:
1,如果RTC从未被配置或者配置已经失效(备份域寄存器写入值等于0xA5A5)这两种情况其中一种为真的话,则调用RTC_Configuration()来初始化RTC,配置RTC外设的控制参数,时钟分频等,并往电脑的超级终端打印出相应的调试信息;
2,初始化好RTC之后,调用函数 Time_Adjust() 让用户键入(通过超级终端输入)时间值;
3,输入时间值后,Time_Adjust() 函数把用户输入的北京时间转化为UNIX时间戳,并把这个UNIX时间戳写入到RTC外设的计数寄存器RTC_CNT.接着RTC外设在这个时间戳的基础上,每秒对RTC_CNT加1,RTC时钟就运行起来了,并且在VDD掉电还运行,以后需要知道时间就直接读取RTC的计时值,就可以计算出时间了;
4,设置好时间后,调用BKP_WriteBackupRegister()把0xA5A5这个值写入备份域寄存器,作为配置成功的标志;
确认RTC曾经被配置过的情况:
1,调用RCC_GetFlagStatus检测是上电复位还是按键复位,根据不同的复位情况在超级终端中打印出不同的调试信息(两种复位都不需要重新设置RTC里面的时间值);
2,调用RTC_WaitForSynchro等待APB1接口与RTC外设同步,上电后第一次通过APB1接口访问RTC时必须要等待同步;
3,同步完成后调用RTC_ITConfig()使能RTC外设的秒中断(使能RTC的秒中断是一个对RTC外设寄存器的写操作);
4,进行写操作以后,必须调用RTC_WaitForLastTask()来等待,确保写操作完成;
在下面有一个条件编译选项询问是否需要output RTCCLK/64 on Tamper pin,这是RTC的时钟输出配置,在rtc的头文件定义 RTCClockOutput_Enable这个宏,PC13引脚会输出RTCCLK的64分频时钟,主要是用于RTC时钟频率的校准或调整时间补偿。
(如果需要用到这个时钟信号的话,只需要在头文件定义RTCClockOutput_Enable这个宏就行了,不要定义为0值就行了~~~~)
初始化RTC RTC_Configuration():
void RTC_Configuration(void)
{
/使能PWR和BKP时钟/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
/对备份域进行软件复位/
PWR_BackupAccessCmd(ENABLE);
/对备份域进行软件复位/
BKP_DeInit();
/* 使能低速外部时钟 LSE */
RCC_LSEConfig(RCC_LSE_ON);
/* 等待LSE起振稳定 */
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)
{}
/* 选择LSE作为 RTC 外设的时钟*/
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
/* 使能RTC时钟 */
RCC_RTCCLKCmd(ENABLE);
/* 等待RTC寄存器与APB1同步*/
RTC_WaitForSynchro();
/* 等待对RTC的写操作完成*/
RTC_WaitForLastTask();
/* 使能RTC秒中断 */
RTC_ITConfig(RTC_IT_SEC, ENABLE);
/* 等待对RTC的写操作完成 */
RTC_WaitForLastTask();
/* 设置RT 时钟分频: 使RTC定时周期为1秒 */
RTC_SetPrescaler(32767);
/* RTC 周期 = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1) */
/等待对RTC的写操作完成 /
RTC_WaitForLastTask();
}
在这个初始化函数里,没有见到熟悉的初始化结构体,对RTC的每一个初始化参数都是使用相应的库函数来配置的.RTC作为备份域的一份子,在访问前首先要使能备份域、电源管理外设的时钟,设置备份域访问权限,作为定时器,初始化时必须要选择好时钟来源,时钟分频。
时间调节Time_Adjust():
void Time_Adjust(struct rtc_time *tm)
{
/* 等待前面可能的 RTC 写操作完成 */
RTC_WaitForLastTask();
/* 利用串口,在终端向用户询问当前北京时间(年月日时分秒),
写入到 rtc_time 型结构体 */
Time_Regulate(tm);
/* 计算输入的日期是星期几,把rtc_time型结构体填充完整 */
GregorianDay(tm);
/* 根据输入日期,计算出 UNIX 时间戳,修改当前 RTC 计数寄存器内容*/
RTC_SetCounter(mktimev(tm));
/* 等待 RTC 写操作完成 */
RTC_WaitForLastTask();
}
这里流程就是使用Time_Regulate()从终端获取当前北京时间,然后根据用户的输入,调用函数mktimev()根据用户输入的年,月,日,时,。分,秒数据,计算出相应的UNIX时间戳,最后调用库函数RTC_SetCounter()把这个UNIX时间戳写入到计数器RTC_CNT,RTC就正式运行了。
获取时间Time_Regulate():
void Time_Reglate(struct rtc_time *tm)
{
u32 Tmp_YY = 0xFF, Tmp_MM = 0xFF, Tmp_DD = 0xFF, Tmp_HH =0xFF, Tmp_MI = 0xFF, Tmp_SS = 0xFF;
printf(“rn==========Time Settings==================”);
printf(“rn 请输入年份(Please Set Years): 20”);
while (Tmp_YY == 0xFF)
{
Tmp_YY = USART_Scanf(99);
}
printf(“nr 年份被设置为: 20%0.2dnr”, Tmp_YY);
tm-》tm_year = Tmp_YY+2000;
Tmp_MM = 0xFF;
printf(“rn 请输入月份(Please Set Months): ”);
while (Tmp_MM == 0xFF)
{
Tmp_MM = USART_Scanf(12);
}
printf(“nr 月份被设置为: %dnr”, Tmp_MM);
tm-》tm_mon= Tmp_MM;
Tmp_DD = 0xFF;
printf(“rn 请输入日期(Please Set Dates): ”);
while (Tmp_DD == 0xFF)
{
Tmp_DD = USART_Scanf(31);
}
printf(“nr 日期被设置为: %dnr”, Tmp_DD);
tm-》tm_mday= Tmp_DD;
Tmp_HH = 0xFF;
printf(“rn 请输入时钟(Please Set Hours): ”);
while (Tmp_HH == 0xFF)
{
Tmp_HH = USART_Scanf(23);
}
printf(“nr 时钟被设置为: %dnr”, Tmp_HH );
tm-》tm_hour= Tmp_HH;
Tmp_MI = 0xFF;
printf(“rn 请输入分钟(Please Set Minutes): ”);
while (Tmp_MI == 0xFF)
{
Tmp_MI = USART_Scanf(59);
}
printf(“nr 分钟被设置为: %dnr”, Tmp_MI);
tm-》tm_min= Tmp_MI;
Tmp_SS = 0xFF;
printf(“rn 请输入秒钟(Please Set Seconds): ”);
while (Tmp_SS == 0xFF)
{
Tmp_SS = USART_Scanf(59);
}
printf(“nr 秒钟被设置为: %dnr”, Tmp_SS);
tm-》tm_sec= Tmp_SS;
}
这里就是在里面从终端获取用户输入的时间,要留意的是,从终端输入的ASCII码,而不是实际数值(在USART_Scanf里面做处理)
PS:这里补上USART_Scanf()的代码,之前串口篇的时候好像没有附上
static uint8_t USART_Scanf(uint32_t value)
{
uint32_t index = 0;
uint32_t tmp[2] = {0, 0};
while (index 《 2)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) ==RESET)
{}
tmp[index++] = (USART_ReceiveData(USART1));
/*数字0到9的ASCII码为0x30至0x39*/
if((tmp[index - 1] 《 0x30) || (tmp[index -1] 》 0x39))
{
printf(“nrPlease enter valid number between 0 and 9 --》: ”)
index--;
}
}
/* 计算输入字符的 ASCII 码转换为数字*/
index = (tmp[1] - 0x30) + ((tmp[0] - 0x30) * 10);
if (index 》 value)
{
printf(“nrPlease enter valid number between 0 and %d”, value);
return 0xFF;
}
return index;
}
STM32之RTC实时时钟
RTC实时时钟简介:
STM32的RTC外设,实质是一个掉电后还继续运行的定时器,从定时器的角度来看,相对于通用定时器TIM外设,它的功能十分简单,只有计时功能(也可以触发中断)。但是从掉电还能继续运行来看,它是STM32中唯一一个具有这个功能功能的外设。(RTC外设的复杂之处不在于它的定时,而在于它掉电还可以继续运行的特性)
所谓掉电,是指电源Vpp断开的情况下,为了RTC外设掉电可以继续运行,必须给STM32芯片通过VBAT引脚街上锂电池。当主电源VDD有效时,由VDD给RTC外设供电。当VDD掉电后,由VBAT给RTC外设供电。无论由什么电源供电,RTC中的数据始终都保存在属于RTC的备份域中,如果主电源和VBA都掉电,那么备份域中保存的所有数据都将丢失。(备份域除了RTC模块的寄存器,还有42个16位的寄存器可以在VDD掉电的情况下保存用户程序的数序,系统复位或电源复位时,这些数据也不会被复位)。
从RTC的定时器特性来说,它是一个32位的计数器,只能向上计数。他使用的时钟源有三种,分别为:
1,高速外部时钟的128分频:HSE/128;
2,低速内部时钟LSI;
3,低速外部时钟LSE;
使用HSE分频时钟或者LSI的时候,在主电源VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证RTC正常工作。所以RTC一般都时钟低速外部时钟LSE,频率为实时时钟模块中常用的32.768KHz,因为32768 = 2^15,分频容易实现,所以被广泛应用到RTC模块。(在主电源VDD有效的情况下(待机),RTC还可以配置闹钟事件使STM32退出待机模式)。
RTC工作过程:
RTC架构:
图中浅灰色的部分都是属于备份域的,在VDD掉电时可在VBAT的驱动下继续运行。这部分仅包括RTC的分频器,计数器,和闹钟控制器。若VDD电源有效,RTC可以触发RTC_Second(秒中断)、RTC_Overflow(溢出事件)和RTC_Alarm(闹钟中断)。从结构图可以看到到,其中的定时器溢出事件无法被配置为中断。如果STM32原本处于待机状态,可由闹钟事件或WKUP事件(外部唤醒事件,属于EXTI模块,不属于RTC)使它退出待机模式。闹钟事件是在计数器RTC_CNT的值等于闹钟寄存器RTC_ALR的值时触发的。
因为RTC的寄存器是属于备份域,所以它的所有寄存器都是16位的。它的计数RTC_CNT的32位由RTC_CNTL和RTC_CNTH两个寄存器组成,分别保存计数值的低16位和高16位。在配置RTC模块的时钟时,把输入的32768Hz的RTCCLK进行32768分频得到实际驱动计数器的时钟TR_CLK = RTCCLK/37768 = 1Hz,计时周期为1秒,计时器在TR_CLK的驱动下计数,即每秒计数器RTC_CNT的值加1(常用)
由于备份域的存在,使得RTC核具有了完全独立于APB1接口的特性,也因此对RTC寄存器的访问要遵守一定的规则。
系统复位后,禁止访问后备寄存器和RCT,防止对后卫区域(BKP)的意外写操作。(执行以下操作使能对后备寄存器好RTC的访问):
1,设置RCC_APB1ENR寄存器的PWREN和BKPEN位来使能电源和后备接口时钟。
2,设置PWR_CR寄存器的DBP位使能对后备寄存器和RTC的访问。
设置为可访问后,在第一次通过APB1接口访问RTC时,必须等待APB1与RTC外设同步,确保被读取出来的RTC寄存器值是正确的,如果在同步之后,一直没有关闭APB1的RTC外设接口,就不需要再次同步了。
如果内核要对RTC寄存器进行任何的写操作,在内核发出写指令后,RTC模块在3个RTCCLK时钟之后,才开始正式的写RTC寄存器操作。我们知道RTCCLK的频率比内核主频低得多,所以必须要检查RTC关闭操作标志位RTOFF当这个标志被置1时,写操作才正式完成。
(以上操作在STM32库里面都有库函数,不需要具体的查阅寄存器~~~~)
UNIX时间戳:
假如从现在起,把计数器RTC_CNT的计数值置0,然后每秒加1,RTC_CNT什么时候会溢出? RTC_CNT是一个32位寄存器,可存储的最大值为(2^32-1),这样的话就是在2^32秒之后溢出,大概换算为:
Time = 2^32/365/24/60/60大约等于136年
假如某个时刻读取到计数器的数值为X = 60*60*24*2(2天),又知道计数器是在2016年1月1日的0时0分0秒置0的,那么根据计数器的这个相对时间数值,可以计算得到这个时刻是2016年1月3日的0时0分0秒了,而计数器会在(2016+136)年左右溢出。(如果我们穿越回到2016年1月1日,如果还在使用这个计数器提供事件的话就会出问题啦。).
定时器被置0的这个事件被称为计时元年,相对计时元年经过的秒数称为时间戳。
PS:
大多数操作系统都是利用时间戳和计时元年来计算当前时间的,而这个时间戳和计时元年大家都取了同一个标准——UNIX时间戳和UNIX计时元年.UNIX 计时元年被设置为格林威治时间1970年1月1日0时0分0秒,大概是为了纪念UNIX的诞生吧。而UNIX时间戳即为当前时间相对于UNIX计时元年经过的秒数。在这个计时系统中,使用的是有符号的32位整型变量来保存UNIX时间戳的,即实际可用计数位数比我们上面例子中的少了一位,少了这一位,UNIX 计时元年也相对提前了,这个计时方法在2038年1月19日03时14分07秒将会发生溢出。这个时间离我们并不远,UNIX时间戳被广泛应用到各种系统中,溢出可能会导致系统发生严重错误,差不多到这个时候,记得注意这个问题呀。
实例分析:
利用RTC提供北京时间:
RTC外设这个连续计数的计数器,在相应软件配置下,可提供时钟日历的功能,修改计数器的值则可以重新设置系统当前的时间和日期。而 由于它的时钟配置系统(RCC_BDCR 寄存器)是在备份域,在系统复位或从待机模式唤醒后RTC的设置和时间维持不变,利用它,可以实现实时时钟的功能。
main函数:
struct rtc_time systmtime;
int main(void)
{
/串口配置/
USART1_Config();
/配置RTC秒中断优先级/
NVIC_Configuration();
//RTC检测及配置
RTC_CheckAndConfig(&systmtime);
//刷新时间
Time_Show(&systmtime);
}
main函数流程:
1,用到了串口,配置好串口(代码和之前的例程一样);
2,配置RTC秒中断优先级,这里设置主优先级为1,次优先级为0(只用到一个RTC,中断随便写都可以)。(代码和之前的中断例程相似,只不过中断通道不一样,这里使用的中断通道是RTC_IRQn);
3,查看RTC外设是否在本次VDD上电前被配置过,如果没有被配置过,则需要输入当前时间,重新初始化RTC和配置时间;
4,配置好RTC后,根据秒中断设置的标志位,每隔1秒向终端更新一次;
事件管理结构体 rtc_time
struct rtc_time
{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
}
这个类型的结构体有时,分,秒,日,月,年及星期7个成员。当需要给RTC的计时器重新配置时间时(更改时间戳),肯定不会询问用户现在距离UNIX计时元年过了多少秒,而是向用户询问现在的公元纪年,以及所在时区的事件。根据RTC计时器向用户输出时间。
这就是 rtc_time 这个结构体的作用,配置RTC时,保存用户输入的时间,其它函数通过它求出UNIX时间戳,写入RTC,RTC正常运行后,需要输出时间时,其它函数通过RTC获取UNIX时间戳,转化成用友好的时间表示方式保存在这个结构体上。
PS:
起始在C语言标准库ANSI C中,也有类似的结构体所以 struct tm,位于标准的time.h文件中,转化函数是mktime()和localtime(),分别把tm结构体成员转化成时间戳和用时间戳转化成结构体成员。
检查RTC RTC_CheckAndConfig()
void RTC_CheckAndConfig(struct rtc_time *tm)
{
/检查备份寄存器BKP_DR1,内容不为0xA5A5,则需要重新配置时间并且询问用户调整时间/
if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
printf(“rnrn RTC not yet configured…。”);
/* RTC 配置 */
RTC_Configuration();
printf(“rnrn RTC configured…。”);
/* 用户输入时间*/
Time_Adjust(tm);
/再往备份寄存器BKP_DR1写入0xA5A5/
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
/启动无需设置新时钟/
else
{
/检查是否掉电重启/
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)
{
printf(“rnrn Power On Reset occurred…。”);
}
/检查是否Reset复位/
else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)
{
printf(“rnrn External Reset occurred…。”);
}
printf(“rn No need to configure RTC…。”);
/等待寄存器同步/
RTC_WaitForSynchro();
/允许RTC秒中断/
RTC_ITConfig(RTC_IT_SEC, ENABLE);
/等待上次RTC寄存器写操作完成/
RTC_WaitForLastTask();
}
/定义了时钟输出宏,则配置校正时钟输出到 PC13,用于RTC时钟频率的校准或调整时间补偿/
#ifdef RTCClockOutput_Enable
/使能PWR和BKP的时钟/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
/允许访问BKP备份域/
PWR_BackupAccessCmd(ENABLE);
/输出64分频时钟/
BKP_RTCOutputConfig(BKP_RTCOutputSource_CalibClock);
#endif
RCC_ClearFlag();
}
if语句调用BKP_ReadBackupRegister()读取RTC备份域寄存器里面的值,判断备份寄存器里面的是否正确,根据后面代码,如果配置成功,会向备份域寄存器写入数值0xA5A5.
(这个数值在VDD掉电后仍然会保存,如果VBAT也掉电,那么备份域,RTC所有寄存器将被复位,这时这个寄存器的值就不会等于0xA5A5了,RTC的计数器的值也是无效的。
简单的说,就是写入的这个数值用作标志RTC是否从未被配置或配置是否已经失效,然后写入任何数值到任何一个备份域寄存器,只要检查的时候与写入值匹配就行了)
RTC未被配置或者配置已经失效的情况:
1,如果RTC从未被配置或者配置已经失效(备份域寄存器写入值等于0xA5A5)这两种情况其中一种为真的话,则调用RTC_Configuration()来初始化RTC,配置RTC外设的控制参数,时钟分频等,并往电脑的超级终端打印出相应的调试信息;
2,初始化好RTC之后,调用函数 Time_Adjust() 让用户键入(通过超级终端输入)时间值;
3,输入时间值后,Time_Adjust() 函数把用户输入的北京时间转化为UNIX时间戳,并把这个UNIX时间戳写入到RTC外设的计数寄存器RTC_CNT.接着RTC外设在这个时间戳的基础上,每秒对RTC_CNT加1,RTC时钟就运行起来了,并且在VDD掉电还运行,以后需要知道时间就直接读取RTC的计时值,就可以计算出时间了;
4,设置好时间后,调用BKP_WriteBackupRegister()把0xA5A5这个值写入备份域寄存器,作为配置成功的标志;
确认RTC曾经被配置过的情况:
1,调用RCC_GetFlagStatus检测是上电复位还是按键复位,根据不同的复位情况在超级终端中打印出不同的调试信息(两种复位都不需要重新设置RTC里面的时间值);
2,调用RTC_WaitForSynchro等待APB1接口与RTC外设同步,上电后第一次通过APB1接口访问RTC时必须要等待同步;
3,同步完成后调用RTC_ITConfig()使能RTC外设的秒中断(使能RTC的秒中断是一个对RTC外设寄存器的写操作);
4,进行写操作以后,必须调用RTC_WaitForLastTask()来等待,确保写操作完成;
在下面有一个条件编译选项询问是否需要output RTCCLK/64 on Tamper pin,这是RTC的时钟输出配置,在rtc的头文件定义 RTCClockOutput_Enable这个宏,PC13引脚会输出RTCCLK的64分频时钟,主要是用于RTC时钟频率的校准或调整时间补偿。
(如果需要用到这个时钟信号的话,只需要在头文件定义RTCClockOutput_Enable这个宏就行了,不要定义为0值就行了~~~~)
初始化RTC RTC_Configuration():
void RTC_Configuration(void)
{
/使能PWR和BKP时钟/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
/对备份域进行软件复位/
PWR_BackupAccessCmd(ENABLE);
/对备份域进行软件复位/
BKP_DeInit();
/* 使能低速外部时钟 LSE */
RCC_LSEConfig(RCC_LSE_ON);
/* 等待LSE起振稳定 */
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)
{}
/* 选择LSE作为 RTC 外设的时钟*/
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
/* 使能RTC时钟 */
RCC_RTCCLKCmd(ENABLE);
/* 等待RTC寄存器与APB1同步*/
RTC_WaitForSynchro();
/* 等待对RTC的写操作完成*/
RTC_WaitForLastTask();
/* 使能RTC秒中断 */
RTC_ITConfig(RTC_IT_SEC, ENABLE);
/* 等待对RTC的写操作完成 */
RTC_WaitForLastTask();
/* 设置RT 时钟分频: 使RTC定时周期为1秒 */
RTC_SetPrescaler(32767);
/* RTC 周期 = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1) */
/等待对RTC的写操作完成 /
RTC_WaitForLastTask();
}
在这个初始化函数里,没有见到熟悉的初始化结构体,对RTC的每一个初始化参数都是使用相应的库函数来配置的.RTC作为备份域的一份子,在访问前首先要使能备份域、电源管理外设的时钟,设置备份域访问权限,作为定时器,初始化时必须要选择好时钟来源,时钟分频。
时间调节Time_Adjust():
void Time_Adjust(struct rtc_time *tm)
{
/* 等待前面可能的 RTC 写操作完成 */
RTC_WaitForLastTask();
/* 利用串口,在终端向用户询问当前北京时间(年月日时分秒),
写入到 rtc_time 型结构体 */
Time_Regulate(tm);
/* 计算输入的日期是星期几,把rtc_time型结构体填充完整 */
GregorianDay(tm);
/* 根据输入日期,计算出 UNIX 时间戳,修改当前 RTC 计数寄存器内容*/
RTC_SetCounter(mktimev(tm));
/* 等待 RTC 写操作完成 */
RTC_WaitForLastTask();
}
这里流程就是使用Time_Regulate()从终端获取当前北京时间,然后根据用户的输入,调用函数mktimev()根据用户输入的年,月,日,时,。分,秒数据,计算出相应的UNIX时间戳,最后调用库函数RTC_SetCounter()把这个UNIX时间戳写入到计数器RTC_CNT,RTC就正式运行了。
获取时间Time_Regulate():
void Time_Reglate(struct rtc_time *tm)
{
u32 Tmp_YY = 0xFF, Tmp_MM = 0xFF, Tmp_DD = 0xFF, Tmp_HH =0xFF, Tmp_MI = 0xFF, Tmp_SS = 0xFF;
printf(“rn==========Time Settings==================”);
printf(“rn 请输入年份(Please Set Years): 20”);
while (Tmp_YY == 0xFF)
{
Tmp_YY = USART_Scanf(99);
}
printf(“nr 年份被设置为: 20%0.2dnr”, Tmp_YY);
tm-》tm_year = Tmp_YY+2000;
Tmp_MM = 0xFF;
printf(“rn 请输入月份(Please Set Months): ”);
while (Tmp_MM == 0xFF)
{
Tmp_MM = USART_Scanf(12);
}
printf(“nr 月份被设置为: %dnr”, Tmp_MM);
tm-》tm_mon= Tmp_MM;
Tmp_DD = 0xFF;
printf(“rn 请输入日期(Please Set Dates): ”);
while (Tmp_DD == 0xFF)
{
Tmp_DD = USART_Scanf(31);
}
printf(“nr 日期被设置为: %dnr”, Tmp_DD);
tm-》tm_mday= Tmp_DD;
Tmp_HH = 0xFF;
printf(“rn 请输入时钟(Please Set Hours): ”);
while (Tmp_HH == 0xFF)
{
Tmp_HH = USART_Scanf(23);
}
printf(“nr 时钟被设置为: %dnr”, Tmp_HH );
tm-》tm_hour= Tmp_HH;
Tmp_MI = 0xFF;
printf(“rn 请输入分钟(Please Set Minutes): ”);
while (Tmp_MI == 0xFF)
{
Tmp_MI = USART_Scanf(59);
}
printf(“nr 分钟被设置为: %dnr”, Tmp_MI);
tm-》tm_min= Tmp_MI;
Tmp_SS = 0xFF;
printf(“rn 请输入秒钟(Please Set Seconds): ”);
while (Tmp_SS == 0xFF)
{
Tmp_SS = USART_Scanf(59);
}
printf(“nr 秒钟被设置为: %dnr”, Tmp_SS);
tm-》tm_sec= Tmp_SS;
}
这里就是在里面从终端获取用户输入的时间,要留意的是,从终端输入的ASCII码,而不是实际数值(在USART_Scanf里面做处理)
PS:这里补上USART_Scanf()的代码,之前串口篇的时候好像没有附上
static uint8_t USART_Scanf(uint32_t value)
{
uint32_t index = 0;
uint32_t tmp[2] = {0, 0};
while (index 《 2)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) ==RESET)
{}
tmp[index++] = (USART_ReceiveData(USART1));
/*数字0到9的ASCII码为0x30至0x39*/
if((tmp[index - 1] 《 0x30) || (tmp[index -1] 》 0x39))
{
printf(“nrPlease enter valid number between 0 and 9 --》: ”)
index--;
}
}
/* 计算输入字符的 ASCII 码转换为数字*/
index = (tmp[1] - 0x30) + ((tmp[0] - 0x30) * 10);
if (index 》 value)
{
printf(“nrPlease enter valid number between 0 and %d”, value);
return 0xFF;
}
return index;
}
举报