开发环境:
MDK:Keil 5.30
开发板:GD32F207I-EVAL
MCU:GD32F207IK
16.1 RTC工作原理
16.1.1 RTC简介
GD32 的 RTC 外设,实质是一个掉电后还继续运行的定时器。从定时器的角度来说,相对于通用定时器 TIMER 外设,它十分简单,只有很纯粹的计时功能(当然,可以触发中断);但从掉电还继续运行的角度来说,它却是 GD32中唯一一个具有如此强大功能的外设。所以 RTC 外设的复杂之处并不在于它的定时功能,而在于它掉电还继续运行的特性。
以上所说的掉电,是指主电源 VDD断开的情况,为了 RTC 外设掉电继续运行,必须给GD32芯片通过 VBAT引脚接上锂电池。当主电源 VDD有效时,由 VDD给 RTC 外设供电。当 VDD掉电后,由 VBAT给 RTC 外设供电。但无论由什么电源供电,RTC 中的数据都保存在属于 RTC 的备份域中,若主电源 VDD和 VBAT都掉电,那么备份域中保存的所有数据将丢失。备份域除了 RTC 模块的寄存器,还有 42 个 16 位的寄存器可以在 VDD掉电的情况下保存用户程序的数据,系统复位或电源复位时,这些数据也不会被复位。
从 RTC 的定时器特性来说,它是一个 32 位的计数器,只能向上计数。它使用的时钟源有三种,分别为高速外部时钟的 128 分频:HXTAL/128;低速内部时钟IRC40K;使 HXTAL分频时钟或IRC40K的话,在主电源 VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证 RTC 正常工作。因此 RTC 一般使用低速外部时钟LXTAL,频率为实时时钟模块中常用的 32.768KHz,这是因为 32768 = 215,分频容易实现,所以它被广泛应用到 RTC 模块。在主电源 VDD有效的情况下(待机),RTC 还可以配置闹钟事件使 GD32退出待机模式。
RTC模块在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。RTC模块和时钟配置系统处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。
16.1.2主要特性
可编程的预分频系数:分频系数最高为2^20
32位的可编程计数器,可用于较长时间段的测量。
2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟的频率必须小于PCLK1时钟频率的四分之一以上)。
可以选择以下三种RTC的时钟源:
A) HXTAL 时钟除以 128
B) LXTAL 振荡电路时钟
C) IRC40K 振荡电路时钟
2个独立的复位类型:
A) APB1接口由系统复位;
B) RTC核心(预分频器、闹钟、计数器和分频只能由后备域复位
3个专门的可屏蔽中断:
A) 闹钟中断,用来产生一个软件可编程的闹钟中断。
B) 秒中断,用来产生一个可编程的周期性中断信号 (最长可达1秒)。
C) 溢出中断,指示内部可编程计数器溢出并回转为的状态。
16.1.3 RTC架构
RTC的架构如下图所示。
RTC 由两个主要部分组成, 第一部分(APB1 接口)用来和 APB1 总线相连。此单元还包含一组 16 位寄存器,可通过 APB1 总线对其进行读写操作。 APB1 接口由 APB1 总线时钟驱动,用来与 APB1 总线连接。
另一部分(RTC 核心)由一组可编程计数器组成,RTC内核包含两个主要模块。一个是RTC预分频模块,用来产生RTC时间基准时钟SC_CLK。RTC预分频模块包含一个20位可编程分频器(RTC预分频器) ,该分频器可以通过对RTC时钟源分频产生SC_CLK。如果对RTC_INTEN寄存器中的秒中断标志位被使能, RTC会在每个SC_CLK上升沿产生一个秒中断。 另外一个模块是一个32 位可编程计数器,其数值可以被初始化为当前系统时间。如果对RTC_INTEN 寄存器的闹钟中断标志位被使能, RTC会在系统时间等于闹钟时间(存储于RTC_ALRMH/L 寄存器)时产生一个闹钟中断。
16.2 RTC寄存器分析
16.2.1 RTC寄存器描述
RTC 总共有 2 个控制寄存器RTC_INTEN和 RTC_CTL。
RTC_INTEN寄存器用来控制中断的,我们本章将要用到秒钟中断,所以在该寄存器必须设置最低位为 1,以允许秒钟中断。
RTC_CTL的第 0 位是秒钟标志位,我们在进入闹钟中断的时候,通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零(写0)。第 3 位为寄存器同步标志位,我们在修改控制寄存器之前,必须先判断该位,是否已经同步了,如果没有则等待同步,在没同步的情况下修改RTC_INTEN/RTC_CTL的值是不行的。第4位为配置标位,在软件修改 RTC_CNTx/RTC_ALRMx/RTC_PSCx的值的时候,必须先软件置位该位,以允许进入配置模式。第 5 位为 RTC 操作位,该位由硬件操作,软件只读。通过该位可以判断上次对 RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才能开始下一次操作。
【注意】
任何标志位都将保持挂起状态,直到适当的RTC_CTL请求位被软件复位,表示所请求的中断已经被接受。
在复位时禁止所有中断,无挂起的中断请求,可以对RTC寄存器进行写操作。
当APB1时钟不运行时,SCIF、ALRMIF、OVIF和RSYNF位不被更新。
SCIF、ALRMIF、OVIF和RSYNF位只能由硬件置位,由软件来清零。
若ALRMIF =1且ALRMIE =1,则允许产生RTC全局中断。如果在EXTI控制器中允许产生EXTI线 17中断,则允许产生RTC全局中断和RTC闹钟中断。
若ALRMIF =1,如果在EXTI控制器中设置了EXTI线 17的中断模式,则允许产生RTC闹钟中断;如果在EXTI控制器中设置了EXTI线 17的事件模式,则这条线上会产生一个脉冲(不会产生RTC闹钟中断)。
RTC 预分频装载寄存器,也有 2 个寄存器组成,RTC_PSCH和RTC_PSCL。这两个寄存器用来配置 RTC 时钟的分频数的,比如我们使用外部 32.768K 的晶振作为时钟的输入频率,那么我们要设置这两个寄存器的值为 32767,以得到一秒钟的计数频率。RTC_PSCH的各位描述如下图所示。
从上图可以看出,RTC_PSCH只有低四位有效,用来存储PSC的 19~16 位。而PSC的前 16 位,存放在RTC_PSCL里面,寄存器RTC_PSCL的各位描述如下图所示。
【注】如果输入时钟频率是32.768kHz(RTCCLK),这个寄存器中写入7FFFh可获得周期为1秒钟的信号。
RTC 预分频器寄存器也有 2 个寄存器组成 RTC_DIVH 和 RTC_DIVL,这两个寄存器的作用就是用来获得比秒钟更为准确的时钟,比如可以得到 0.1 秒,或者 0.01 秒等。该寄存器的值自减的,用于保存还需要多少时钟周期获得一个秒信号。在一次秒钟更新后,由硬件重新装载。这两个寄存器和 RTC 预分频装载寄存器的各位是一样的,这里我们就不列出来了。
接着要介绍的是 RTC 最重要的寄存器, RTC 计数器寄存器 RTC_CNT。该寄存器由 2 个 16位的寄存器组成 RTC_CNTH 和 RTC_CNTL,总共 32 位,用来记录秒钟值(一般情况下)。此两个计数器也比较简单,我们也不多说了。注意一点,在修改这个寄存器的时候要先进入配置模式。
最后我们介绍 RTC 部分的最后一个寄存器, RTC 闹钟寄存器,该寄存器也是由 2 个 16 为的寄存器组成 RTC_ALRH 和 RTC_ALRL。总共也是 32 位,用来标记闹钟产生的时间(以秒为单位),如果 RTC_CNT 的值与 RTC_ALR 的值相等,并使能了中断的话,会产生一个闹钟中断。该寄存器的修改也要进入配置模式才能进行。
因为我们使用到备份寄存器来存储 RTC 的相关信息(我们这里主要用来标记时钟是否已经经过了配置)。
16.2.2读RTC寄存器
RTC完全独立于RTC APB1接口。
软件通过APB1接口访问RTC的预分频值、计数器值和闹钟值。但是,相关的可读寄存器只在与RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。RTC标志也是如此的。
这意味着,如果APB1接口曾经被关闭,而读操作又是在刚刚重新开启APB1之后,则在第一次的内部寄存器更新之前,从APB1上读出的RTC寄存器数值可能被破坏了(通常读到0) 。下述几种情况下能够发生这种情形:
发生系统复位或电源复位
系统刚从待机模式唤醒
系统刚从停机模式唤醒
所有以上情况中,APB1接口被禁止时(复位、无时钟或断电)RTC核仍保持运行状态。
因此,若在读取RTC寄存器时,RTC 的APB1 接口曾经处于禁止状态,则软件首先必须等待RTC_CTL寄存器中的RSYNF位(寄存器同步标志)被硬件置’1’。
16.2.3配置RTC寄存器
必须设置RTC_CTL寄存器中的CMF位,使 RTC进入配置模式后,才能写入 RTC_PSC、RTC_CNT、RTC_ALRM寄存器。
另外,对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CTL寄存器中的LWOFF状态位,判断RTC寄存器是否处于更新中。仅当LWOFF状态位是’1’时,才可以写入RTC寄存器。
配置过程:
1.查询LWOFF位,直到LWOFF的值变为’1’
2.置CMF值为1,进入配置模式
3.对一个或多个RTC寄存器进行写操作
4.清除CMF标志位,退出配置模式
5.查询LWOFF,直至LWOFF位变为’1’ 以确认写操作已经完成。
6.仅当CMF标志位被清除时,写操作才能进行,这个过程至少需要3个RTCCLK周期。
16.3 RTC具体代码实现
RTC 正常工作的一般配置步骤如下:
1)使能电源时钟和备份区域时钟。
前面已经介绍了,我们要访问 RTC 和备份区域就必须先使能电源时钟和备份区域时钟。
rcu_periph_clock_enable(RCU_BKPI);
rcu_periph_clock_enable(RCU_PMU);
2)取消备份区写保护。
要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能),否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经配置过了,这样避免每次复位之后重新配置时钟。 取消备份区域写保护的库函数实现方法是:
pmu_backup_write_enable(); //使能 RTC 和后备寄存器访问
3)复位备份区域,开启外部低速振荡器。
在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY位来确定低速振荡器已经就绪了才开始下面的操作。
备份区域复位的函数是:
bkp_deinit();//复位备份区域
开启外部低速振荡器的函数是:
rcu_osci_on(RCU_LXTAL);// 开启外部低速振荡器
4)选择 RTC 时钟,并使能。
库函数中,选择 RTC 时钟的函数是:
rcu_rtc_clock_config(RCU_RTCSRC_LXTAL); //选择LXTAL作为 RTC 时钟
对于 RTC 时钟的选择使能 RTC 时钟的函数是:
rcu_periph_clock_enable(RCU_RTC); //使能 RTC 时钟
5)设置 RTC 的分频,以及配置 RTC 时钟。
在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PSCH 和RTC_PSCL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置RTC 的允许配置位( RTC_CTL的 CMF 位),设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL两个寄存器)。
下面我们一一这些步骤用到的库函数:
在进行 RTC 配置之前首先要打开允许配置位(CMF),库函数是:
rtc_configuration_mode_enter ();/// 允许配置
在配置完成之后,千万别忘记更新配置同时退出配置模式,函数是:
rtc_configuration_mode_exit ();//退出配置模式,更新配置
设置 RTC 时钟分频数, 库函数是:
void rtc_prescaler_set(uint32_t psc)
这个函数只有一个入口参数,就是 RTC 时钟的分频数,很好理解。
然后是设置秒中断允许,RTC 使能中断的函数是:
void rtc_interrupt_enable(uint32_t interrupt)
这个函数的第一个参数是设置秒中断类型,这些通过宏定义定义的。 对于使能秒中断方法是:
rtc_interrupt_enable(RTC_INT_SECOND); //使能 RTC 秒中断
下一步便是设置时间了,设置时间实际上就是设置 RTC 的计数值,时间与计数值之间是需要换算的,当然也可先不设置。库函数中设置 RTC 计数值的方法是:
void rtc_counter_set(uint32_t cnt)
通过这个函数直接设置 RTC 计数值。
6)更新配置,设置 RTC 中断分组。
在设置完时钟之后,我们将配置更新同时退出配置模式,这里还是通过 RTC_CTL的CMF来实现。库函数的方法是:
rtc_configuration_mode_exit ();//退出配置模式,更新配置
在退出配置模式更新配置之后我们在备份区域 BKP_DATA0中写入 0xA5A5代表我们已经初始化过时钟了,下次开机(或复位)的时候,先读取 BKP_DATA0的值,然后判断是否是 0xA5A5来决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。
往备份区域写用户数据的函数是:
void bkp_data_write(bkp_data_register_enum register_number, uint16_t data)
这个函数的第一个参数就是寄存器的标号了,这个是通过宏定义定义的。 比如我们要往BKP_DATA0 写入 0xA5A5,方法是:
bkp_data_write(BKP_DATA_0, 0xA5A5);
同时,有写便有读,读取备份区域指定寄存器的用户数据的函数是:
uint16_t bkp_data_read(bkp_data_register_enum register_number)
这个函数就很好理解了,这里不做过多讲解。
设置中断分组的方法之前已经详细讲解过,这里不做重复讲解。
7)编写中断服务函数。
最后,我们要编写中断服务函数,在秒钟中断产生的时候,读取当前的时间值。
/**
* @brief This function handles RTC exception.
* @param None
* @retval None
*/
void RTC_IRQHandler(void)
{
if(rtc_flag_get(RTC_FLAG_SECOND)!=RESET)//读取中断标志
{
rtc_flag_clear(RTC_FLAG_SECOND);//清楚中断标志
tim_bz=1;//秒中断标志
}
}
完成的配如下:
/**
* @brief RTC配置
* @param None
* @retval None
*/
void rtc_configuration(void)
{
/* enable PMU and BKPI clocks 使能电源时钟和备份区域时钟*/
rcu_periph_clock_enable(RCU_BKPI);
rcu_periph_clock_enable(RCU_PMU);
/* allow access to BKP domain 允许访问BKP区域*/
pmu_backup_write_enable();
//复位备份区域,开启外部低速振荡器
/* reset backup domain */
bkp_deinit();
/* enable LXTAL 使能外部低速晶振 32.768K */
rcu_osci_on(RCU_LXTAL);
/* wait till LXTAL is ready */
rcu_osci_stab_wait(RCU_LXTAL);
//选择 RTC 时钟,并使能
/* select RCU_LXTAL as RTC clock source */
rcu_rtc_clock_config(RCU_RTCSRC_LXTAL);
/* enable RTC Clock 使能RTC时钟 */
rcu_periph_clock_enable(RCU_RTC);
rtc_configuration_mode_enter();
/* wait for RTC registers synchronization */
rtc_register_sync_wait();
/* wait until last write operation on RTC registers has finished 等待写RTC寄存器完成*/
rtc_lwoff_wait();
/* enable the RTC second interrupt 使能RTC秒中断*/
rtc_interrupt_enable(RTC_INT_SECOND);
/* wait until last write operation on RTC registers has finished 等待写RTC寄存器完成*/
rtc_lwoff_wait();
/* set RTC prescaler: set RTC period to 1s 设置预分频*/
rtc_prescaler_set(32767);
/* wait until last write operation on RTC registers has finished 等待写RTC寄存器完成*/
rtc_lwoff_wait();
nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);
nvic_irq_enable(RTC_IRQn, 1, 0);
}
/**
* @brief RTC时钟初始化
* @param None
* @retval None
*/
void clock_init(void)
{
if(0xA5A5 != bkp_data_read(BKP_DATA_0))
{
//第一次运行 初始化设置
rtc_configuration();//RTC初始化
/* wait until last write operation on RTC registers has finished */
rtc_lwoff_wait();
/* change the current time */
rtc_counter_set(0);
/* wait until last write operation on RTC registers has finished */
rtc_lwoff_wait();
rtc_lwoff_wait();//等待写RTC寄存器完成
rtc_lwoff_wait();//等待写RTC寄存器完成
bkp_data_write(BKP_DATA_0, 0xA5A5);//写配置标志
}
else
{
/* check if the power on reset flag is set */
if(rcu_flag_get(RCU_FLAG_PORRST) != RESET)
{
printf("\r\n\n Power On Reset occurred....");
}
else if(rcu_flag_get(RCU_FLAG_SWRST) != RESET)
{
/* check if the pin reset flag is set */
printf("\r\n\n External Reset occurred....");
}
printf("\r\n No need to configure RTC....");
rtc_register_sync_wait();//等待RTC寄存器同步
rtc_interrupt_enable(RTC_INT_SECOND);//使能RTC秒中断
rtc_lwoff_wait();//等待写RTC寄存器完成
}
rtc_configuration_mode_exit();//退出配置模式, 更新配置
rcu_all_reset_flag_clear();//清除复位标志;
}
主函数代码如下:
/*
brief main function
param[in] none
param[out] none
retval none
*/
int main(void)
{
uint32_t TimeData=0,hh=0,mm=0,ss=0;
//usart init 115200 8-N-1
com_init(COM1);
rtc_configuration();
clock_init();
while(1)
{
if(tim_bz==1)
{
tim_bz=0;
TimeData=rtc_counter_get();
hh= TimeData/3600;
mm = (TimeData%3600)/60;
ss = TimeData%60;
printf("Time: %0.2d:%0.2d:%0.2d\r\n",hh,mm,ss);
}
}
}
16.4实验现象
打开串口助手,打印信息如下:
这里没有设置初始时间,时分秒是从0开始的。
欢迎访问我的网站
BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎
资源获取方式
1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[GD32开发实战指南]获取资料提取码