• 《嵌入式 – GD32开发实战指南》第16章 RTC


    开发环境:
    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);
    
    • 1
    • 2

    2)取消备份区写保护。

    要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能),否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经配置过了,这样避免每次复位之后重新配置时钟。 取消备份区域写保护的库函数实现方法是:

    pmu_backup_write_enable(); //使能 RTC 和后备寄存器访问
    
    • 1

    3)复位备份区域,开启外部低速振荡器。

    在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY位来确定低速振荡器已经就绪了才开始下面的操作。

    备份区域复位的函数是:

    bkp_deinit();//复位备份区域
    开启外部低速振荡器的函数是:
    rcu_osci_on(RCU_LXTAL);// 开启外部低速振荡器
    
    • 1
    • 2
    • 3

    4)选择 RTC 时钟,并使能。
    库函数中,选择 RTC 时钟的函数是:

    rcu_rtc_clock_config(RCU_RTCSRC_LXTAL); //选择LXTAL作为 RTC 时钟
    
    • 1

    对于 RTC 时钟的选择使能 RTC 时钟的函数是:

    rcu_periph_clock_enable(RCU_RTC); //使能 RTC 时钟
    
    • 1

    5)设置 RTC 的分频,以及配置 RTC 时钟。

    在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PSCH 和RTC_PSCL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置RTC 的允许配置位( RTC_CTL的 CMF 位),设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL两个寄存器)。

    下面我们一一这些步骤用到的库函数:

    在进行 RTC 配置之前首先要打开允许配置位(CMF),库函数是:

    rtc_configuration_mode_enter ();/// 允许配置
    
    • 1

    在配置完成之后,千万别忘记更新配置同时退出配置模式,函数是:

    rtc_configuration_mode_exit ();//退出配置模式,更新配置
    
    • 1

    设置 RTC 时钟分频数, 库函数是:

    void rtc_prescaler_set(uint32_t psc)
    
    • 1

    这个函数只有一个入口参数,就是 RTC 时钟的分频数,很好理解。
    然后是设置秒中断允许,RTC 使能中断的函数是:

    void rtc_interrupt_enable(uint32_t interrupt)
    
    • 1

    这个函数的第一个参数是设置秒中断类型,这些通过宏定义定义的。 对于使能秒中断方法是:

    rtc_interrupt_enable(RTC_INT_SECOND); //使能 RTC 秒中断
    
    • 1

    下一步便是设置时间了,设置时间实际上就是设置 RTC 的计数值,时间与计数值之间是需要换算的,当然也可先不设置。库函数中设置 RTC 计数值的方法是:

    void rtc_counter_set(uint32_t cnt)
    
    • 1

    通过这个函数直接设置 RTC 计数值。

    6)更新配置,设置 RTC 中断分组。

    在设置完时钟之后,我们将配置更新同时退出配置模式,这里还是通过 RTC_CTL的CMF来实现。库函数的方法是:

    rtc_configuration_mode_exit ();//退出配置模式,更新配置
    
    • 1

    在退出配置模式更新配置之后我们在备份区域 BKP_DATA0中写入 0xA5A5代表我们已经初始化过时钟了,下次开机(或复位)的时候,先读取 BKP_DATA0的值,然后判断是否是 0xA5A5来决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。

    往备份区域写用户数据的函数是:

    void bkp_data_write(bkp_data_register_enum register_number, uint16_t data)
    
    • 1

    这个函数的第一个参数就是寄存器的标号了,这个是通过宏定义定义的。 比如我们要往BKP_DATA0 写入 0xA5A5,方法是:

    bkp_data_write(BKP_DATA_0, 0xA5A5);
    
    • 1

    同时,有写便有读,读取备份区域指定寄存器的用户数据的函数是:

    uint16_t bkp_data_read(bkp_data_register_enum register_number)
    
    • 1

    这个函数就很好理解了,这里不做过多讲解。

    设置中断分组的方法之前已经详细讲解过,这里不做重复讲解。

    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;//秒中断标志
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    完成的配如下:

    /**
      * @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();//清除复位标志;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    主函数代码如下:

    /*
        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);  
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    16.4实验现象

    打开串口助手,打印信息如下:

    在这里插入图片描述

    这里没有设置初始时间,时分秒是从0开始的。



    欢迎访问我的网站

    BruceOu的哔哩哔哩
    BruceOu的主页
    BruceOu的博客
    BruceOu的CSDN博客
    BruceOu的简书
    BruceOu的知乎


    资源获取方式

    1.关注公众号[嵌入式实验楼]
    2.在公众号回复关键词[GD32开发实战指南]获取资料提取码

  • 相关阅读:
    go: play with so source code. Go源代码编译和调试
    重要文件怎么加密?文件加密软件哪个好用?
    分享:STC-51激光雕刻机项目(免费完整资料)
    第七章 用户和组管理
    Python潮流周刊#2:Rust 让 Python 再次伟大
    HTTP的理解和各个版本的介绍优化
    2022年第十三届蓝桥杯软件类决赛C/C++大学B组题解
    经过一个多月的等待我有幸成为Spring相关项目的Contributor
    写JDBC遇到的问题
    扫拖一体洗地机哪个品牌好?家用智能洗地机推荐
  • 原文地址:https://blog.csdn.net/u013162035/article/details/126358693