
RTC实时时钟简介
RTC外设(Real Time Clock)实质是一个掉电后还继续运行的定时器。从定时器的角度,相对于通用定时器Timer外设,它十分简单,只有很纯粹的计时和触发中断的功能,但具备掉电还能继续运行的特殊功能,可以应用在特定场景。这里所说的掉电是指主电源VDD断开的情况,因此为了RTC外设掉电继续运行,必须接上锂电池通过VBAT引脚供电。当主电源VDD有效时,由VDD给RTC外设供电;而当VDD掉电后,由VBAT给RTC外设供电。但无论由什么电源供电,RTC中的数据都保存在属于RTC的备份域中,若主电源VDD和VBAT都掉电,那么备份域中保存的所有数据将丢失。
从RTC的定时器特性来说,它是一个32位的计数器,只能向上计数。在相应软件配置下,可提供时钟日历的功能,修改计数器的值可以重新设置系统当前的时间和日期。它使用的时钟源有三种,分别为高速外部时钟的128分频(HSE/128)、低速内部时钟LSI以及低速外部时钟LSE。在主电源VDD掉电的情况下,HSE和LSI这两个时钟来源都会受到影响,没法保证RTC正常工作,因此RTC一般使用低速外部时钟LSE供电。在主电源VDD有效的情况下(待机),RTC还可以配置闹钟事件使CKS32退出待机模式。
RTC框图结果分析
RTC由两个主要部分组成,参见下图。第一部分(背景灰色区域)用来和APB1总线相连,属于备份域,在VDD掉电时可在VBAT的驱动下继续运行。这部分仅包括RTC的分频器,计数器和闹钟控制器。若VDD电源有效,RTC可以触发RTC_Second(秒中断)、RTC_Overflow(溢出事件)和RTC_Alarm(闹钟中断)。此单元还包含一组16位寄存器,可通过APB1总线对其进行读写操作。APB1接口由APB1总线时钟驱动,用来与APB1总线连接。
另一部分(RTC核心)由一组可编程计数器组成,分成两个主要模块。第一个模块是RTC的预分频模块,它可编程产生最长为1秒的RTC时间基准TR_CLK。RTC的预分频模块包含了一个20位的可编程分频器(RTC 预分频器)。如果在RTC_CR寄存器中设置了相应的允许位,则在每个TR_CLK周期中RTC产生一个中断(秒中断)。第二个模块是一个32位的可编程计数器,可被初始化为当前的系统时间,按秒钟计算,可以记录4294967296秒,约合136年左右,作为一般应用已经足够。RTC还有一个闹钟寄存器RTC_ALR,用于产生闹钟。系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时间相比较,如果RTC_CR控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断。
图1 简化的RTC框图
由于备份域的存在,使得RTC内核具有了完全独立于APB1接口的特性,也因此对RTC寄存器的访问要遵守一定的规则。系统复位后,默认禁止访问后备寄存器和RTC,防止对后备区域BKP的意外写操作。需要执行以下操作使能才可以对后备寄存器和RTC的访问:(1)设置RCC_APB1ENR寄存器的PWREN和BKPEN位来使能电源和后备接口时钟。(2)设置PWR_CR寄存器的DBP位使能对后备寄存器和RTC的访问。设置后备寄存器为可访问后,在第一次通过APB1接口访问RTC时,因为时钟频率的差异,所以必须等待APB1与RTC外设同步,确保被读取出来的RTC寄存器值是正确的。如果内核要对RTC寄存器进行任何的写操作,在内核发出写指令后,RTC模块在3个RTCCLK时钟之后才开始正式的写RTC寄存器操作。由于RTCCLK的频率比内核主频低得多,所以每次操作后必须要检查RTC关闭操作标志位RTOFF,当这个标志被置1时,写操作才正式完成。当然,以上的操作都具有库函数来快速实现。
RTC控制相关库函数
标准库对RTC控制提供了完善的函数,使用它们可以方便地进行控制,本小节对这些内容进行讲解。RTC相关的库函数在文件cks32f10x_rtc.c和cks32f10x_rtc.h文件中。
1、等待时钟同步和操作完成
RTC区域的时钟比APB时钟慢,访问前需要进行时钟同步,只要调用库函数 RTC_WaitForSynchro即可,而如果修改了RTC的寄存器,又需要调用RTC_WaitForLastTask函数确保数据已写入。这两个库函数主要通过while循环检测RTC控制寄存器的RSF和RTOFF位实现等待功能。
/** * @brief 等待RTC寄存器与APB时钟同步 (RTC_CNT, RTC_ALR and RTC_PRL) * @note 在APB时钟复位或停止后,在对RTC寄存器的任何操作前,必须调用本函数 * @param None * @retval None */ void RTC_WaitForSynchro(void) { RTC->CRL &= (uint16_t)~RTC_FLAG_RSF; // 清除RSF寄存器位 while ((RTC->CRL & RTC_FLAG_RSF) == (uint16_t)RESET); //等待RSF寄存器位为SET } /** * @brief 等待上一次对RTC寄存器的操作完成 * @note 修改RTC寄存器后,必须调用本函数 * @param None * @retval None */ void RTC_WaitForLastTask(void) { while ((RTC->CRL & RTC_FLAG_RTOFF) == (uint16_t)RESET) ; //等待至 RTOFF 寄存器位为SET }
2、使能备份域及RTC访问
默认情况下RTC 所属的备份域禁止访问,可用库函数PWR_BackupAccessCmd使能访问。
/**
* @brief 使能对RTC和Backup寄存器的访问
* @param ENABLE 或 DISABLE
* @retval None
*/
void PWR_BackupAccessCmd(FunctionalState NewState)
{
*(__IO uint32_t *) CR_DBP_BB = (uint32_t)NewState;
}
该函数通过PWR_CR寄存器的DBP位使能访问,使能后才可以访问RTC相关的寄存器,然而若希望修改RTC的寄存器,还需要进一步调用RTC_EnterConfigMode使能RTC控制寄存器的CNF位使能寄存器配置。
/** * @brief 进入RTC配置模式 * @param None * @retval None */ void RTC_EnterConfigMode(void) { RTC->CRL |= RTC_CRL_CNF; //设置CNF位进入配置模式 }
3、设置RTC时钟分频
选择RTC使用的时钟后,可以使用库函数RTC_SetPrescaler进行分频,把函数参数PrescalerValue写入到RTC的PRLH和PRLL寄存器,一般会把RTC时钟分频得到1Hz时钟。
/** * @brief 设置RTC分频配置 * @param PrescalerValue:RTC分频值 * @retval None */ void RTC_SetPrescaler(uint32_t PrescalerValue) { RTC_EnterConfigMode(); RTC->PRLH = (PrescalerValue & PRLH_MSB_MASK) >> 16; //设置RTC分频值的高八位 RTC->PRLL = (PrescalerValue & RTC_LSB_MASK); //设置RTC分频值的低八位 RTC_ExitConfigMode(); }
4、设置RTC计数器
RTC外设中最重要的就是计数器以及闹钟寄存器了,它们可以使用RTC_SetCounter、RTC_GetCounter以及RTC_SetAlarm库函数操作。利用RTC_SetCounter可以向RTC的计数器写入新数值,通常这些数值被设置为时间戳以更新时间。RTC_GetCounter函数则用于在RTC正常运行时获取当前计数器的值以获取当前时间。RTC_SetAlarm函数用于配置闹钟时间,当计数器的值与闹钟寄存器的值相等时,可产生闹钟事件或中断,该事件可以把睡眠、停止和待机模式的芯片唤醒。
/** * @brief 设置RTC计数器的值 * @param CounterValue:要设置的RTC计数值 * @retval None */ void RTC_SetCounter(uint32_t CounterValue) { RTC_EnterConfigMode(); RTC->CNTH = CounterValue >> 16; //设置RTC计数值的高八位 RTC->CNTL = (CounterValue & RTC_LSB_MASK); //设置RTC计数值的低八位 RTC_ExitConfigMode(); } /** * @brief 获取RTC计数器的值 * @param None * @retval 返回RTC计数器的值 */ uint32_t RTC_GetCounter(void) { uint16_t tmp = 0; tmp = RTC->CNTL; return (((uint32_t)RTC->CNTH << 16 ) | tmp) ; } /** * @brief 设置RTC闹钟的值 * @param AlarmValue:要设置的RTC闹钟值 * @retval None */ void RTC_SetAlarm(uint32_t AlarmValue) { RTC_EnterConfigMode(); RTC->ALRH = AlarmValue >> 16; //设置RTC闹钟的高八位 RTC->ALRL = (AlarmValue & RTC_LSB_MASK); //设置RTC闹钟的低八位 RTC_ExitConfigMode(); }
UNIX时间戳
在使用RTC外设前,还需要引入UNIX时间戳的概念。如果从现在起,把计数器RTC_CNT的计数值置0,然后每秒加1,RTC_CNT什么时候会溢出呢?由于RTC_CNT是32位寄存器,可存储的最大值为(232-1),这样计时的话,在232秒后溢出,N=232/365/24/60/60≈136年,即它将在今后136年时溢出。
假如某个时刻读取到计数器的数值为X=60*60*24*2,即两天时间的秒数,而假设又知道计数器是在2011年1月1日的0时0分0秒置0的,那么就可以根据计数器的这个相对时间数值,计算得这个X时刻是2011年1月3日的0时0分0秒了。而计数器则会在(2011+136)年左右溢出,也就是说到了(2011+136)年时,如果我们还在使用这个计数器提供时间的话就会出现问题。在这个例子中,定时器被置0的这个时间被称为计时元年,相对计时元年经过的秒数称为时间戳,也就是计数器中的值。
大多数操作系统都是利用时间戳和计时元年来计算当前时间的,而这个时间戳和计时元年大家都取了同一个标准——UNIX时间戳和UNIX计时元年。UNIX计时元年被设置为格林威治时间1970年1月1日0时0分0秒,大概是为了纪念UNIX的诞生的时代吧,而UNIX时间戳即为当前时间相对于UNIX计时元年经过的秒数。因为UNIX时间戳主要用来表示当前时间或者和电脑有关的日志时间(如文件创立时间,log发生时间等),考虑到所有电脑文件不可能在1970年前创立,所以用UNIX时间戳很少用来表示1970前的时间。
在这个计时系统中,使用的是有符号的32位整型变量来保存UNIX时间戳的,即实际可用计数位数比我们上面例子中的少了一位,少了这一位,UNIX 计时元年也相对提前了一半,这个计时方法在2038年1月19日03时14分07秒将会发生溢出,这个时间离我们并不远,在设计预期寿命较长的设备需要注意。
小结
本章内容介绍了CKS32F107系列RTC实时时钟外设的硬件结构和工作原理,并结合相关寄存器讲解了与RTC控制相关的外设库函数使用方法,最后介绍了UNIX时间戳的概念。从上述内容可知,RTC外设是个连续计数的计数器,利用它提供的时间戳,可通过程序转换输出实时时钟和日历的功能,修改计数器的值则可以重新设置系统当前的时间和日期。由于它的时钟配置系统(RCC_BDCR寄存器)是在备份域,在系统复位或从待机模式唤醒后RTC的设置维持不变,而且使用备份域电源可以让RTC计时器在主电源关掉的情况下仍然运行,保证时间的正确。有了这些基础,下一节将详细介绍如何利用RTC的计时功能实现一个简单的万年历效果。
来源:中科芯MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。