41. RTC——实时时钟

41.1. RTC简介

RA6M5的RTC(Real Time Clock)外设,实质是一个掉电后还继续运行的定时器。从定时器的角度来说,相对于GPT外设,要简单很多 ,只有计时和触发中断以及输入捕获的功能。但从掉电还继续运行的角度来说,它却是RA6M5中唯一一个具有如此强大功能的外设。 所以RTC外设的特别之处并不在于它的定时功能,而在于它掉电还继续运行的特性。

以上所说的掉电,是指主电源VDD断开的情况,为了RTC外设掉电继续运行,必须通过VBAT引脚接上纽扣电池给RA6M5的RTC供电。 当主电源VDD有效时,由VDD给RTC外设供电; 而当VDD掉电后,由VBAT给RTC外设供电,继续日历计数器的计时,除此之外RTC的其他功能都无法使用。 若VDD和VBAT都掉电,日历计数器的计时会丢失。

RA6M5的实时时钟(RTC)有两种计数模式,日历计数模式和二进制计数模式,通过切换寄存器设置使用。 对于日历计数模式,RTC具有从2000年到2099年的100年日历,并自动调整闰年的日期。 对于二进制计数模式,RTC计数秒,并保留信息作为串行值。 二进制计数模式可用于公历(西历)以外的日历。

子时钟振荡器或LOCO可以选择作为时间计数器的计数源。RTC使用128Hz的时钟,通过将计数源除以预分频器的值获得: 年、月、日、星期、上午/下午。 (12/24 小时模式)、时、分、秒或32位二进制按1/128秒计数。

41.1.1. RTC 特性

RTC 特性:

  • 计数模式:日历计数模式和二进制计数模式。

  • 时钟源:子时钟或LOCO。

  • 日历计数模式:年,月,日,星期,小时,分钟,秒计数。

  • 二进制计数模式:32位计二进制计数。

  • 闹钟中断:在日历计数模式下,可以与年,月,日,星期,小时,分钟和秒进行比较。在二进制计数模式下则与32位2进制计数器进行对比。

  • 周期性中断:可以选择2秒,1秒,1/2秒,1/4秒,1/8秒,1/16秒,1/32秒,1/64秒、1/128秒或1/256秒作为中断周期。

  • 进位中断:当从64HZ计数器到二进制计数器的进位时和当改变64Hz计数器和R64同时读取CNT寄存器时进行中断。

  • 输入捕获:当检测到捕获时间输入引脚的电平发生跳变时(上升沿或者下降沿时),可以进行输入捕获。 该输入捕获可以用日历计数或者二进制计数。电平跳变时可以产生中断。与GPT相同的是,该输入捕获也能使用噪声滤波器。

  • 事件关联:周期性输出事件。

  • TrustZone过滤器:可以设置安全属性。

41.2. RTC的结构框图

图

下面我们就来一一介绍这些功能。

41.2.1. RTC引脚

  • XCIN,XCOUT:连接到32.768kHz的晶振。

  • RTCOUT:输出1Hz或64Hz的方波,无法在待机模式下使用。

  • RTCICn(n = 0,1,2):输入捕获引脚。

41.2.2. 时钟分频

对时钟源Sub-Clock或者LOCO进行分频,输出128Hz的时钟脉冲,可在VBAT供电下使用。

41.2.3. 日历计数器/二进制计数器

这个计数器可以在VBAT供电下使用,由以下寄存器(同时也是计数器)构成:

  • R64CNT:R64CNT是一个8位的计数器,由128Hz的时钟脉冲驱动,实际只使用[6:0]位,故其计数27即128次, 也就是1秒,就会产生一次进位,并驱动RSECCNT/BCNT计数器+1。

  • RSECCNT:用于统计R64CNT每秒产生的进位信号,表示“秒”,设置范围为0-59,如果设置其他值,会导致RTC工作异常。

  • RMINCNT:用于统计RSECCNT每分钟产生的进位信号,表示“分”,设置范围为0-59,如果设置其他值,会导致RTC工作异常。

  • RHRCNT:用于统计RMINCNT每小时产生的进位信号,表示“时”,当RTC设置为24小时制,则设置范围时0-23。 当RTC设置为12小时制,则设置范围时0-11。如果设置其他值,会导致RTC工作异常。

  • RDAYCNT:用于统计RHRCNT的每天时产生进位信号,表示“日”,计数范围取决于月份以及这一年是否为闰年。设置范围为1-31,如果设置其他值,会导致RTC工作异常。

  • RWKCNT:用于统计RHRCNT的每天产生的进位信号,表示“星期”,计数范围为0-6,如果设置其他值,会导致RTC工作异常。

  • RMONCNT:用于统计RDAYCNT每个月产生的进位信号,表示“月”,计数范围为1-12,如果设置其他值,会导致RTC工作异常。

  • RYRCNT:用于统计RMONCNT每个月产生的进位信号,表示“年”,计数范围为0-99,如果设置其他值,会导致RTC工作异常。

在二进制模式下,RSECCNT、RMINCNT、RHRCNT、RWKCNT共同构成BCNT(Binary Counter),是一个32位,向上递增的计数器。通过统计R64CNT的进位次数进行计数。

41.2.4. 闹钟功能

在日历计数模式下,闹钟可以按年、月、日、周、时、分、秒或它们的任意组合。在闹钟设置涉及的寄存器的ENB位(都是最高位)写1, 并在低位设置闹钟时间。将0写入不涉及闹钟设置的寄存器的ENB位。例如,设置闹钟为每天的8点30分, 则在RMINAR的最高位写入1,同时低位为30。RHRAR最高位写入1,同时低位写8。其他寄存器则在最高位写0。

在二进制计数模式下,闹钟可以以32位任意组合,在BCNTnAER中,将需要使用的位写1,不需要使用的写0。并在BCNTnAR中设置闹钟时间 例如,设置当BCNTn的[3:0]为1010的时候产生闹钟中断,则在BCNTnAR的[3:0]写入1010,且在BCNTnAER的[3:0]都写入1, BCNTnART的其他位写0。

需要注意的是,在日历计数模式下,闹钟寄存器的比较值使用BCD码。

41.3. 实验1:用RTC提供日历时间

RTC最基础的功能便是日历计数,本次实验将使用RTC的日历计数并在周期中断中,将日历时钟按“年-月-日-时:分:秒”的格式打印出来。

41.3.1. 硬件设计

图

本开发板中提供了一个钮扣电池插槽,可以接入型号为CR1220的钮扣电池,提供3V供电。

41.3.2. 软件设计

41.3.2.1. 新建工程

由于本实验需要用到LED,也会用到串口打印提示信息, 因此我们在前面串口通信章节的“实验1:UART收发回显”例程的基础上修改程序。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “39_RTC_Date”, 最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “39_RTC_Date”, 并进入该文件夹里面双击 Keil 工程文件,打开该工程。

工程新建好之后,在工程根目录的 “src” 文件夹下面新建 “rtc” 文件夹, 再进入 “rtc” 文件夹里面新建源文件和头文件:“bsp_rtc.c” 和 “bsp_rtc.h”。 工程文件结构如下。

文件结构
39_RTC_Date
├─ ......
└─ src
   ├─ led
   │  ├─ bsp_led.c
   │  └─ bsp_led.h
   ├─ debug_uart
   │  ├─ bsp_debug_uart.c
   │  └─ bsp_debug_uart.h
   ├─ rtc
   │  ├─ bsp_rtc.c
   │  └─ bsp_rtc.h
   └─ hal_entry.c

41.3.2.2. FSP配置

LED与串口的配置读者可以翻看本教程第10章和第19章,这里不再赘述。

双击 configuration.xml 打开配置界面:

然后点开“Pins”->“Peripherals”->“Timers:RTC”->“RTC0”来配置SCI模块: 将“Operation Mode”配置为“Enabled”

图

点击配置窗口底部的“Stack”,如图步骤加入RTC模块。

图

点击刚刚加入的窗口,在左下角的“属性”窗口中配置中断,时钟源,模块名字等属性。由于只有Sub-Clock能使用纽扣电池供电, 故时钟源建议选择Sub-Clock。

图

最后点右上角的“Generate Project Content”按钮,让软件自动生成配置代码。

RTC属性描述

属性

描述

Parameter Cheacking

参数检查,如果设置为Enabled则会检查RTC设置的日历时间是否合法,并通

过年月日自动计算星期。

Set Source Clock in Open

若设置为Enabled,则在函数“R_RTC_Open”中初始化时钟源,若设置为

Disabled,必须调用R_RTC_ClockSourceSet来设置RTC时钟源。

Name

模块名字,可以根据个人需要取名。

Clock Source

设置时钟源,可以选择LOCO和Sub-Clock。

Frequency Comparision Value(LOCO)

使用LOCO作为时钟源时的分频值,取值范围是7-511,默认为255,

则此时输出给日历计数器/二进制计数器的时钟脉冲为

LOCO/(255+1) = 32768/256 = 128Hz

Automatic Adjustment Mode

自动校正模式,用于校正Sub-Clock震荡过快或过慢引起的误差

(例如芯片老化,环境温度过低或过高),默认为Enable。

Automatic Adjustment Period

自动校正的时间,可选择10秒,1分钟或NONE

Adjustment Type(Plus-Minus)

自动校正类型,可以选择None,Addtion或Substraction。选择Addtion则是在

每次自动校正的时候在分频器中加上Error Adjustment Value。

选择Substraction则是在每次自动校正的时候在分频器中减去Error Adjustment Value。

Error Adjustment Value

在触发自动校正时候分频器减去或增加的值。

Callback

RTC中断回调函数的名称,根据读者需求进行设置即可。

Alarm Interrupt Priority

选择闹钟中断的优先级,如果选择Disabled则不使能闹钟中断。

Period Interrupt Priority

选择周期性中断的优先级,如果选择Disabled则不使能周期性中断。

Carry Interrupt Priority

选择进位中断的优先级。

RTCOUT

输出引脚选择,可输出1Hz或64Hz的方波,无法在待机模式下使用。

RTCICn

输入捕获引脚选择。

需要注意的是,FSP库并未对RTC输入和输出引脚提供操作函数,故读者如果需要使用这两个功能的话,则要直接调用RTC寄存器, 由于这不是RTC的重点功能,因此这里不进行详细阐述,读者可以在《RA6M5 Group User’s Manual》的23章查阅。

41.3.2.3. 宏定义

代码清单39-1:宏定义
/**********日期宏定义**********/
#define RTC_YEAR_SET 2008       //年
#define RTC_MON_SET 8           //月
#define RTC_MDAY_SET 8          //日
/*通过蔡勒公式计算星期,1~6代表周一到周六,0代表周日*/
#define RTC_WDAY_SET (RTC_YEAR_SET-2000 \
                  + ((RTC_YEAR_SET-2000)/4) \
                  - 35 + (26*(RTC_MON_SET+1))/10 \
                  + RTC_MDAY_SET -1 )%7

/**********时间宏定义**********/
#define RTC_HOUR_SET 0          //时
#define RTC_SEC_SET 0           //秒
#define RTC_MIN_SET 0           //分

这里的星期使用蔡勒公式进行计算,读者在设置日期的时候可以直接使用这个宏,只需修改自己想设置的日期即可。 若RTC的 “Parameter Cheacking” 为 “Enabled”,则星期会在函数 “R_RTC_CalendarTimeSet()” 中被自动设置,同样使用的是蔡勒公式。

41.3.2.4. 初始化RTC

代码清单39-2:初始化RTC
void RTC_Init(void)
{
   //初始化时设定的时间
   rtc_time_t set_time =
   { .tm_sec = RTC_SEC_SET,  //秒
     .tm_min = RTC_MIN_SET,  //分
     .tm_hour = RTC_HOUR_SET,  //小时
     .tm_mday = RTC_MDAY_SET,  //日(一个月中)
     .tm_wday = RTC_WDAY_SET,   //星期
     .tm_mon = RTC_MON_SET - 1,   //月份,0~11代表11~12月
     .tm_year = RTC_YEAR_SET-1900, //年份(如今年是2008,则这里输入2008-1900=108)
   };
   /*打开RTC模块*/
   R_RTC_Open (rtc.p_ctrl, rtc.p_cfg);

   /*时钟源设置,如果在FSP Configuration设置"Set Source Clock in Open"为"enabled",那这一步可以被跳过*/
   R_RTC_ClockSourceSet (rtc.p_ctrl);

   /*若RTC时钟已经使用纽扣电池工作了一段时间,则可以使用这个函数获取当前日历并设置当前时间*/
   //R_RTC_CalendarTimeGet(rtc.p_ctrl,&set_time);

   /*这个函数至少调用一次以启动RTC*/
   R_RTC_CalendarTimeSet (rtc.p_ctrl, &set_time); //设置当前时间

   /*设置周期中断的周期为1秒*/
   R_RTC_PeriodicIrqRateSet (rtc.p_ctrl, RTC_PERIODIC_IRQ_SELECT_1_SECOND);
}

41.3.2.5. RTC中断回调函数

代码清单39-3:RTC中断回调函数
void rtc_callback(rtc_callback_args_t *p_args)
{
      static rtc_time_t get_time;
      switch (p_args->event)
      {
         /*若是周期中断,则发送日期给串口并切换LED电平*/
         case RTC_EVENT_PERIODIC_IRQ:
            LED1_TOGGLE; //反转LED

            /*获取当前时间*/
            R_RTC_CalendarTimeGet (rtc.p_ctrl, &get_time);

            /*打印当前时间*/
            printf ("\r\n%d-%d-%d-%d:%d:%d\r\n", get_time.tm_year + 1900, get_time.tm_mon + 1, get_time.tm_mday,
                     get_time.tm_hour, get_time.tm_min, get_time.tm_sec);
            break;
         default:
            break;
      }
}

需要注意的是,由于日历时间是在RTC的中断回调函数中打印的,而printf函数需要使用串口中断,故打印使用的串口的中断优先级必须要高于RTC, 这样串口在打印的时候才能抢占RTC中断,避免出现堵塞现象,导致程序无法正常运行。

41.3.2.6. hal_entry函数

代码清单39-4:hal_entry函数
void hal_entry(void)
{
   /* TODO: add your own code here */
   R_BSP_PinAccessEnable(); //启用对PFS寄存器的访问,因为后面写IO口都用BSP内联函数
   Debug_UART_Init(); //初始化调试串口
   RTC_Init();  //初始化RTC

   while(1){}
#if BSP_TZ_SECURE_BUILD
   /* Enter non-secure code */
   R_BSP_NonSecureEnter();
#endif
}

41.3.2.7. 下载验证

保证开发板相关硬件连接正确,用Type-C线连接开发板“USB TO UART”接口跟电脑。本次实验需要使用到串口调试助手, 配置好串口参数并打开串口后, 如图,在调试助手的接收框中每隔一秒接收一次当前RTC日历时间,同时可以观察到开发板的LED灯在以1秒的频率闪烁。

图

41.4. 实验2:RTC闹钟

闹钟也是RTC比较重要的一个功能,可以用于低功耗模式下的唤醒,本实验使用日历闹钟, 在本实验中使用蜂鸣器作为声源,让开发板每隔1分钟发出一次声响来模拟闹钟功能。

41.4.1. 硬件设计

与实验1相同基本,但是需要使用蜂鸣器,当P605引脚为高电平时,三极管导通,蜂鸣器便会响起。

图

41.4.2. 软件设计

41.4.2.1. 新建工程

由于本实验与实验1基本相同,所以在这里我们直接使用实验1的代码来继续编写。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程模板 “39_RTC_Date”, 然后将工程文件夹重命名为 “39_RTC_Alarm”, 最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程模板 “39_RTC_Date”, 然后将工程文件夹重命名为 “39_RTC_Alarm”, 并进入该文件夹里面双击 Keil 工程文件,打开该工程。

41.4.2.2. FSP配置

在原有的基础上,配置Alarm Interrupt Priority不为Disabled且优先级低于串口中断即可。

图

41.4.2.3. 闹钟初始化

代码清单39-5:闹钟初始化
/**
* @brief 初始化RTC闹钟
* @param 无
* @retval 无
*/
void RTC_Alarm_Init(void)
{
    rtc_alarm_time_t alarm_time =
    {
        .time = {
                .tm_sec = RTC_ALARM_SEC_SET,  //秒
                .tm_min = RTC_ALARM_MIN_SET,  //分
                .tm_hour = RTC_ALARM_HOUR_SET,  //小时
                .tm_mday = RTC_ALARM_MDAY_SET,  //日(一个月中)
                .tm_wday = RTC_ALARM_WDAY_SET,   //星期
                .tm_mon = RTC_ALARM_MON_SET,   //月份
                .tm_year = RTC_ALARM_YEAR_SET-1900, //年份
        },
        .dayofweek_match = RTC_ALARM_WDAY_ENABLE,
        .hour_match = RTC_ALARM_HOUR_ENABLE,
        .mday_match = RTC_ALARM_MDAY_ENABLE,
        .min_match = RTC_ALARM_MIN_ENABLE,
        .mon_match = RTC_ALARM_MON_ENABLE,
        .sec_match = RTC_ALARM_SEC_ENABLE,
        .year_match = RTC_ALARM_YEAR_ENABLE,
    };
    R_RTC_CalendarAlarmSet(rtc.p_ctrl, &alarm_time);
}

41.4.2.4. RTC初始化

代码清单39-6:RTC初始化
/**
* @brief 初始化RTC
* @retval 无
* @param 无
*/
void RTC_Init(void)
{
    //初始化时设定的时间
    rtc_time_t set_time =
    {
      .tm_sec = RTC_SEC_SET,  //秒
      .tm_min = RTC_MIN_SET,  //分
      .tm_hour = RTC_HOUR_SET,  //小时
      .tm_mday = RTC_MDAY_SET,  //日(一个月中)
      .tm_wday = RTC_WDAY_SET,   //星期
      .tm_mon = RTC_MON_SET - 1,   //月份,0~11代表11~12月
      .tm_year = RTC_YEAR_SET-1900, //年份(如今年是2008,则这里输入2008-1900=108)
    };
    /*打开RTC模块*/
    R_RTC_Open (rtc.p_ctrl, rtc.p_cfg);

    /*时钟源设置,如果在FSP Configuration设置"Set Source Clock in Open"为"enabled",那这一步可以被跳过*/
    R_RTC_ClockSourceSet (rtc.p_ctrl);

    /*若RTC时钟已经使用纽扣电池工作了一段时间,则可以使用这个函数获取当前日历并设置当前时间*/
    //R_RTC_CalendarTimeGet(rtc.p_ctrl,&set_time);

    /*这个函数至少调用一次以启动RTC*/
    R_RTC_CalendarTimeSet (rtc.p_ctrl, &set_time); //设置当前时间

    RTC_Alarm_Init(); //初始化闹钟中断
}

41.4.2.5. 初始化使用的宏定义

代码清单39-7:初始化使用的宏定义
/**************RTC设置时间宏定义**************/

/**********日期宏定义**********/
#define RTC_YEAR_SET 2008       //年
#define RTC_MON_SET 8           //月
#define RTC_MDAY_SET 8          //日
/*通过蔡勒公式计算星期,1~6代表周一到周六,0代表周日*/
#define RTC_WDAY_SET (RTC_YEAR_SET-2000 \
                    + ((RTC_YEAR_SET-2000)/4) \
                    - 35 + (26*(RTC_MON_SET+1))/10 \
                    + RTC_MDAY_SET -1 )%7

/**********时间宏定义**********/
#define RTC_SEC_SET 0           //秒
#define RTC_MIN_SET 0           //分
#define RTC_HOUR_SET 0          //时

/**************闹钟宏定义**************/

/**********日期宏定义**********/
#define RTC_ALARM_YEAR_SET 2008       //年
#define RTC_ALARM_MON_SET 8           //月
#define RTC_ALARM_MDAY_SET 8          //日
/*通过蔡勒公式计算星期*/
#define RTC_ALARM_WDAY_SET (RTC_YEAR_SET-2000 \
                    + ((RTC_YEAR_SET-2000)/4) \
                    - 35 + (26*(RTC_MON_SET+1))/10 \
                    + RTC_MDAY_SET -1 )%7

/**********时间宏定义**********/
#define RTC_ALARM_SEC_SET 15          //秒
#define RTC_ALARM_MIN_SET 0           //分
#define RTC_ALARM_HOUR_SET 0          //时

/***使能闹钟比较***/
#define RTC_ALARM_YEAR_ENABLE 0  //年
#define RTC_ALARM_MON_ENABLE 0   //月
#define RTC_ALARM_MDAY_ENABLE 0  //日
#define RTC_ALARM_WDAY_ENABLE 0     //星期

#define RTC_ALARM_SEC_ENABLE 1          //秒
#define RTC_ALARM_MIN_ENABLE 0           //分
#define RTC_ALARM_HOUR_ENABLE 0          //时

通过宏定义将闹钟时间定为每分钟的第15秒,想要更改配置的时候只要修改相应的宏定义就可以了。

41.4.2.6. RTC中断回调函数

代码清单39-8:RTC中断回调函数
void rtc_callback(rtc_callback_args_t *p_args)
{
    static rtc_time_t get_time;
    switch (p_args->event)
    {
        case RTC_EVENT_PERIODIC_IRQ:
            break;
        /*若是闹钟中断,则发送日期给串口并切换LED电平并让蜂鸣器叫一声*/
        case RTC_EVENT_ALARM_IRQ:
            /*获取当前时间*/
            R_RTC_CalendarTimeGet (rtc.p_ctrl, &get_time);

            /*打印当前时间*/
            printf ("\r\n%d-%d-%d-%d:%d:%d\r\n", get_time.tm_year + 1900, get_time.tm_mon + 1, get_time.tm_mday,
                                get_time.tm_hour, get_time.tm_min, get_time.tm_sec);

            LED1_TOGGLE; //反转LED1

            Buzzer_sout(); //蜂鸣器叫一声

            break;
        default:
            break;
    }
}

41.4.2.7. hal_entry函数

代码清单39-9:hal_entry函数
void hal_entry(void)
{
    /* TODO: add your own code here */
    R_BSP_PinAccessEnable(); //启用对PFS寄存器的访问,因为后面写IO口都用BSP内联函数
    Debug_UART4_Init(); //初始化调试串口
    RTC_Init();  //初始化RTC
    RTC_Alarm_Init(); //初始化闹钟

    while(1){}

#if BSP_TZ_SECURE_BUILD
    /* Enter non-secure code */
    R_BSP_NonSecureEnter();
#endif
}

41.4.3. 下载验证

保证开发板相关硬件连接正确,用Type-C线连接开发板“USB TO UART”接口跟电脑。本次实验需要使用到串口调试助手, 配置好串口参数并打开串口后,可以观察到开发板的LED灯每分钟变化一次亮灭状态, 并且蜂鸣器会响一声,串口调试助手接收到闹钟响起时的RTC日历时间。

../../_images/download_2.png