【正点原子STM32连载】第27章:APM32E103最小系统板使用指南中的RTC实验

1)实验平台:正点原子APM32E103最小系统板
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban

第二十七章 RTC实验

本章介绍APM32E103实时时钟(RTC)的使用,实时时钟能为系统提供一个准确的时间,即时系统复位或主电源断电,RTC依然能够运行,因此RTC也经常用于各种低功耗场景。通过本章的学习,读者将学习到RTC的使用。
本章分为如下几个小节:
27.1 硬件设计
27.2 程序设计
27.3 下载验证

27.1 硬件设计
27.1.1 例程功能

  1. LED1每秒翻转一次,表示每秒产生一次RTC唤醒中断
  2. 通过LCD实时显示RTC时间,并可通过USMART设置RTC时间等
  3. LED0闪烁,指示程序正在运行
    27.1.2 硬件资源
  4. LED
    LED0 – PB5
  5. USART1(PA9、PA10连接至板载USB转串口芯片上)
  6. 正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
  7. RTC
    27.1.3 原理图
    本章实验使用的RTC为APM32E103的片上资源,因此没有相应的连接原理图。
    27.2 程序设计
    27.2.1 Geehy标准库的PMU驱动
    本章实验要使用到RTC,因此需要对RTC及其相关的寄存器进行配置,但是为了防止误操作,系统复位后备份区域(指RTC、备份寄存器)是被禁止写访问的,备份区域的写访问是由电源管理单元(PMU)进行配置的,具体的配置步骤如下:
    ①:使能写备份区域
    在Geehy标准库中对应的驱动函数如下:
    ①:使能写备份区域
    该函数用于使能写备份区域,其函数原型如下所示:
    void PMU_EnableBackupAccess(void);
    该函数的形参描述,如下表所示:
    形参 描述
    无 无
    表27.2.1.1 函数PMU_EnableBackupAccess()形参描述
    该函数的返回值描述,如下表所示:
    返回值 描述
    无 无
    表27.2.1.2 函数PMU_EnableBackupAccess()返回值描述
    该函数的使用示例,如下所示:
#include " apm32e10x.h"
#include "apm32e10x _pmu.h"

void example_fun(void)
{
    /* 使能写备份区域 */
    PMU_EnableBackupAccess();
}

27.2.2 Geehy标准库的RCM驱动
本章实验使用了RTC,因此需要配置RTC的时钟,RTC的时钟可以来自LSE、LSI或HSE的分频,以上均由RCM进行管理,其具体的配置步骤如下:
①:优先使能LSE
②:若LSE无法就绪,则使能LSI
③:配置RTC的时钟源为LSE或LSI
在Geehy标准库中对应的驱动函数如下:
①:使能LSE
该函数用于配置LSE,其函数原型如下所示:
void RCM_ConfigLSE(RCM_LSE_T state);
该函数的形参描述,如下表所示:
形参 描述
state LSE的配置状态
例如:RCM_LSE_CLOSE、RCM_LSE_OPEN等(在apm32e10x_rcm.h文件中有定义)
表27.2.2.1 函数RCM_ConfigLSE()形参描述
该函数的返回值描述,如下表所示:
返回值 描述
无 无
表27.2.2.2 函数RCM_ConfigLSE()返回值描述
该函数的使用示例,如下所示:

#include "apm32e10x.h"
#include "apm32e10x_rcm.h"

void example_fun(void)
{
    /* 使能LSE */
    RCM_ConfigLSE(RCM_LSE_OPEN);
    
    /* 禁止LSE */
    RCM_ConfigLSE(RCM_LSE_CLOSE);
}

②:使能LSI
该函数用于使能LSI,其函数原型如下所示:
void RCM_EnableLSI(void);
该函数的形参描述,如下表所示:
形参 描述
无 无
表27.2.2.3 函数RCM_EnableLSI()形参描述
该函数的返回值描述,如下表所示:
返回值 描述
无 无
表27.2.2.4 函数RCM_EnableLSI()返回值描述
该函数的使用示例,如下所示:

#include "apm32e10x.h"
#include "apm32e10x_rcm.h"

void example_fun(void)
{
    RCM_EnableLSI(); /* 使能LSI */
}

③:配置RTC的时钟源
该函数用于配置RTC的时钟源,其函数原型如下所示:
void RCM_ConfigRTCCLK(RCM_RTCCLK_T rtcClkSelect);
该函数的形参描述,如下表所示:
形参 描述
rtcClkSelect RTC的时钟源选择
例如:RCM_RTCCLK_LSE、RCM_RTCCLK_LSI等(在apm32e10x_rcm.h文件中有定义)
表27.2.2.5 函数RCM_ConfigRTCCLK()形参描述
该函数的返回值描述,如下表所示:
返回值 描述
无 无
表27.2.2.6 函数RCM_ConfigRTCCLK()返回值描述
该函数的使用示例,如下所示:

#include "apm32e10x.h"
#include "apm32e10x_rcm.h"

void example_fun(void)
{
    /* 配置RTC的时钟源为LSE */
    RCM_ConfigRTCCLK(RCM_RTCCLK_LSE);
}

27.2.3 Geehy标准库的RTC驱动
本章实验使用了RTC,RTC最基本的操作就是设置和获取时间和日期,同时还需要读写RTC备份寄存器保存是否已经在初始化过程中设置过时间的标志,其具体的步骤如下:
①:初始化配置RTC
②:读取RTC备份寄存器判断是否进行设置过时间
③:若未设置过时间,则设置RTC的时间
④:若未设置过时间,则设置RTC的日期
⑤:将设置过时间的标志写入RTC备份寄存器
⑥:读取RTC的时间
⑦:读取RTC的日期
在Geehy标准库中对应的驱动函数如下:
①:读取RTC备份寄存器
该函数用于读取RTC备份寄存器,其函数原型如下所示:
uint16_t BAKPR_ReadBackupRegister(BAKPR_DATA_T bakrData);
该函数的形参描述,如下表所示:
形参 描述
bakrData RTC指定数据备份寄存器
例如:BAKPR_DATA1、BAKPR_DATA2等(在apm32e10x_rtc.h文件中有定义)
表27.2.3.3 函数BAKPR_ReadBackupRegister ()形参描述
该函数的返回值描述,如下表所示:
返回值 描述
uint16_t类型数据 RTC备份寄存器值
表27.2.3.4 函数BAKPR_ReadBackupRegister ()返回值描述
该函数的使用示例,如下所示:

#include "apm32e10x.h"
#include "apm32e10x_rtc.h"

void example_fun(void)
{
    uint32_t rtc_backup_data1;
    
    /* 读取RTC备份寄存器1 */
    rtc_backup_data1 = BAKPR_ReadBackupRegister (BAKPR_DATA1);
    
    /* Do something. */
}

②:写入RTC备份寄存器
该函数用于写入RTC备份寄存器,其函数原型如下所示:
void BAKPR_ConfigBackupRegister(BAKPR_DATA_T bakrData, uint16_t data);
该函数的形参描述,如下表所示:
形参 描述
bakrData RTC指定数据备份寄存器
例如:BAKPR_DATA1、BAKPR_DATA2等(在apm32e10x_rtc.h文件中有定义)
data RTC备份寄存器值
表27.2.3.9 函数BAKPR_ConfigBackupRegister ()形参描述
该函数的返回值描述,如下表所示:
返回值 描述
无 无
表27.2.3.10 函数BAKPR_ConfigBackupRegister ()返回值描述
该函数的使用示例,如下所示:

#include "apm32e10x.h"
#include "apm32e10x_rtc.h"

void example_fun(void)
{
    /* 往RTC备份寄存器0写入0x50505050 */
    BAKPR_ConfigBackupRegister(BAKPR_DATA1, 0x50505050);
}

③:使能RTC指定中断
该函数用于使能RTC的指定中断,其函数原型如下所示:
void RTC_EnableInterrupt(uint32_t interrupt);
该函数的形参描述,如下表所示:
形参 描述
interrupt 指定的RTC中断
例 如:RTC_INT_OVR、RTC_INT_ALR等(在apm32e10x_rtc.h文件中有定义)
表27.2.3.19 函数RTC_EnableInterrupt()形参描述
该函数的返回值描述,如下表所示:
返回值 描述
无 无
表27.2.3.20 函数RTC_EnableInterrupt()返回值描述
该函数的使用示例,如下所示:

#include "apm32e10x.h"
#include "apm32e10x_rtc.h"

void example_fun(void)
{
    /* 使能RTC唤醒中断 */
    RTC_EnableInterrupt(RTC_INT_OVR);
}

④:使能RTC中断
请见第12.2.3小节中配置中断的相关内容。
本实验同时也使能了RTC的闹钟功能,闹钟功能可以在RTC时间到达设定值时触发中断,其具体的使用步骤如下:
①:配置RTC闹钟
②:配置RTC闹钟对应的EINT线
③:使能RTC闹钟中断
④:使能RTC闹钟中断,并配置其相关的中断优先级
⑤:使能RTC闹钟
在Geehy标准库中对应的驱动函数如下:
①:配置RTC闹钟
该函数用于配置RTC的闹钟,其函数原型如下所示:
void RTC_ConfigAlarm(uint32_t value);
该函数的形参描述,如下表所示:
形参 描述
value RTC闹钟数值
表27.2.3.23 函数RTC_ConfigAlarm()形参描述
该函数的返回值描述,如下表所示:
返回值 描述
无 无
表27.2.3.24 函数RTC_ConfigAlarm()返回值描述
该函数使用数值变量传入RTC闹钟的配置参数,并未用到具体的结构体。

#include "apm32e10x.h"
#include "apm32e10x_rtc.h"

void example_fun(uint8_t hour, uint8_t min, uint8_t sec)
{
uint32_t seccount = 0;

    seccount += (date - 1) * 86400;
    seccount += hour * 3600;
    seccount += min * 60;
    seccount += sec;

    RTC_ConfigAlarm(seccount);
}

②:使能RTC指定中断
请见第27.2.3小节中使能RTC指定中断的内容。
③:使能RTC中断
请见第12.2.3小节中配置中断的相关内容。
27.2.4 RTC驱动
本章实验的RTC驱动主要负责向应用层提供RTC的初始化和配置自动唤醒及闹钟的函数。本章实验中,RTC的驱动代码包括rtc.c和rtc.h两个文件。
RTC驱动中,RTC的初始化函数,如下所示:

/**
 * @brief       初始化RTC
 * @param       无
 * @retval      初始化结果
 * @arg         0: 初始化成功
 * @arg         1: 初始化失败
 */
uint8_t rtc_init(void)
{
    uint16_t bkpflag;
    uint16_t retry;
    
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_PMU);    /* 使能PMU时钟 */
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_BAKR);   /* 使能BAKPR时钟 */
    PMU_EnableBackupAccess();                          /* 使能访问备份寄存器 */
    bkpflag = rtc_read_bkr(BAKPR_DATA1);               /* 读取备份寄存器1的值 */
    
    retry = 0;
    RCM_ConfigLSE(RCM_LSE_OPEN);                       /* 尝试使能LSE */
    
    while ((retry < 200) && (RCM->BDCTRL_B.LSERDYFLG != SET))
    {
        retry++;
        delay_ms(5);
    }
    
    if (RCM->BDCTRL_B.LSERDYFLG != SET)                /* LSE无法就绪,改用LSI */
    {
        RCM_ConfigLSE(RCM_LSE_CLOSE);                  /* 禁用LSE */
        RCM_EnableLSI();                               /* 使能LSI */
        while (RCM->CSTS_B.LSIRDYFLG != SET)           /* 等待LSI就绪 */
        {
            delay_ms(5);
        }
        RCM_ConfigRTCCLK(RCM_RTCCLK_LSI);              /* 选择RTC的时钟源为LSI */
        rtc_write_bkr(BAKPR_DATA1, 0x5051);
    }
    else                                               /* LSE已就绪 */
    {
        RCM_ConfigRTCCLK(RCM_RTCCLK_LSE);              /* 选择RTC的时钟源为LSE */
        rtc_write_bkr(BAKPR_DATA1, 0x5050);
    }
    
    RCM_EnableRTCCLK();                                /* 使能RTC时钟 */
    RTC_WaitForSynchro();                              /* 等待RTC寄存器同步 */
    RTC_WaitForLastTask();                             /* 等待RTC操作完成 */
    
    RTC_ConfigPrescaler(32767);                        /* 配置RTC预分频寄存器 */
    RTC_WaitForLastTask();
   
    if ((bkpflag != 0x5050) && (bkpflag != 0x5051))    /* 之前从未配置过 */
    {
        rtc_set(22, 10, 22, 8, 8, 8);                  /* 设置RTC时间 */
    }
    
    RTC_EnableInterrupt(RTC_INT_SEC);                  /* 使能RTC秒中断 */
    RTC_WaitForLastTask();
    NVIC_EnableIRQRequest(RTC_IRQn, 2, 0);
    
    return 0;
}

从上面的代码中可以看出,RTC的初始化函数中,优先尝试使用LSE作为RTC的时钟源,若LSE无法就绪,则使用LSI作为RTC的时钟源。配置好RTC的时钟源后,配置RTC为24小时制,并配置RTC的预分频寄存器的数值为32767。最后根据RTC备份寄存器中的标志,决定是否配置RTC的时间,若之前已经配置过,则不会再次配置。
RTC驱动中设置、获取RTC时间、日期的三个函数,如下所示:
/

**
 * @brief       设置RTC时间
 * @param       year : 年
 * @param       month: 月
 * @param       date : 日
 * @param       hour : 小时
 * @param       min  : 分钟
 * @param       sec  : 秒钟
 * @retval      设置结果
 * @arg         0: 设置成功
 * @arg         1: 设置失败
 */
uint8_t rtc_set(uint8_t year, uint8_t month,
                uint8_t date, uint8_t hour, 
                uint8_t min,  uint8_t sec)
{
    uint16_t year_4;
    uint16_t index;
    uint32_t seccount = 0;
    
    year_4 = year + 2000;
    if ((year_4 < 1970) || (year_4 > 2099))
    {
        return 1;
    }
    
    for (index=1970; index<year_4; index++)
    {
        if (is_leap_year(index))
        {
            seccount += 31622400;
        }
        else
        {
            seccount += 31536000;
        }
    }
    
    month--;
    for (index=0; index<month; index++)
    {
        seccount += (uint32_t)month_table[index] * 86400;
        if (is_leap_year(year_4) && (index == 1))
        {
            seccount += 86400;
        }
    }
    
    seccount += (uint32_t)(date - 1) * 86400;
    seccount += (uint32_t)hour * 3600;
    seccount += (uint32_t)min * 60;
    seccount += sec;
    
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_PMU);     /* 使能PMU时钟 */
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_BAKR);    /* 使能BAKPR时钟 */
    PMU_EnableBackupAccess();                           /* 使能访问备份寄存器 */
    
    RTC_ConfigCounter(seccount);                        /* 设置RTC计数值 */
    RTC_WaitForLastTask();
    
    return 0;
}

/**
 * @brief       获取RTC时间
 * @param       year : 年
 * @param       month: 月
 * @param       date : 日
 * @param       week : 星期
 * @param       hour : 小时
 * @param       min  : 分钟
 * @param       sec  : 秒钟
 * @retval      无
 */
void rtc_get(uint8_t *year, uint8_t *month,
             uint8_t *date, uint8_t *week,
             uint8_t *hour, uint8_t *min,
             uint8_t *sec)

{
    uint32_t seccount;
    uint16_t daycount;
    static uint16_t last_daycount = 0;
    uint16_t index;
    
    /* 读取RTC计数值 */
    seccount = RTC_ReadCounter();
    daycount = seccount / 86400;
    if (last_daycount != daycount)
    {
        last_daycount = daycount;
        
        index = 1970;
        while (daycount >= 365)
        {
            if (is_leap_year(index))
            {
                if (daycount >= 366)
                {
                    daycount -= 366;
                }
                else
                {
                    index++;
                    break;
                }
            }
            else
            {
                daycount -= 365;
            }
            index++;
        }
        *year = index - 2000;
        
        index=0;
        while (daycount >= 28)
        {
            if (is_leap_year(*year + 2000) && (index == 1))
            {
                if (daycount >= 29)
                {
                    daycount -= 29;
                }
                else
                {
                    break;
                }
            }
            else
            {
                if (daycount >= month_table[index])
                {
                    daycount -= month_table[index];
                }
                else
                {
                    break;
                }
            }
            index++;
        }
        *month = index + 1;
        *date = daycount + 1;
    }
    
    daycount = seccount % 86400;
    *hour = daycount / 3600;
    *min = (daycount % 3600) / 60;
    *sec = (daycount % 3600) % 60;
    *week = rtc_get_week(*year + 2000, *month, *date);
}

/**
 * @brief       通过日期计算星期
 * @param       year : 年
 * @param       month: 月
 * @param       date : 日
 * @retval      星期
 */
uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t date)
{
    uint8_t year_l;
    uint8_t year_h;
    uint8_t week;
    
    year_h = year / 100;
    year_l = year % 100;
    if (year_h > 19)
    {
        year_l += 100;
    }
    
    week = (((year_l + (year_l >> 2)) % 7) + date + week_table[month - 1] - ((((year_l % 4) == 0) && (month < 3)) ? 1 : 0)) % 7;
    
    return week;
}

以上三个获取、设置RTC时间、日期的函数,均是对Geehy标准库中RTC驱动的简单封装。
RTC驱动中,配置RTC唤醒中断及其对应的中断回调函数,如下所示:

/**
 * @breif       RTC中断服务函数
 * @param       无
 * @retval      无
 */
void RTC_IRQHandler(void)
{
    if (RTC_ReadIntFlag(RTC_INT_SEC) == SET)    /* 判断秒中断标志 */
    {
        LED1_TOGGLE();
        RTC_ClearIntFlag(RTC_INT_SEC);          /* 清除秒中断标志 */
    }
    
    if (RTC_ReadIntFlag(RTC_INT_ALR) == SET)    /* 判断闹钟中断标志 */
    {
        printf("ALARM!\r\n");
        RTC_ClearIntFlag(RTC_INT_ALR);          /* 清除闹钟中断标志 */
    }
    
    RTC_WaitForLastTask();
}

从上面的代码中可以看出,在RTC的秒中断中翻转了LED1的亮灭状态,并清除了秒中断标志,因此RTC时间将会周期性地发生,也就能看到LED1周期性地改变状态。同时,在进行判断秒中断标志时,也对闹钟中断标志位进行了判断。在该过程中,我们通过串口打印出的“ALARM”字样提示得知单片机正在响应该中断。
RTC驱动中,配置RTC闹钟中断及其对应的中断回调函数,如下所示:

/**
 * @breif       设置RTC闹钟时间
 * @param       year : 年
 * @param       month: 月
 * @param       date : 日
 * @param       hour : 小时
 * @param       min  : 分钟
 * @param       sec  : 秒钟
 * @retval      无
 */
void rtc_set_alarm(uint8_t year, uint8_t month,
                   uint8_t date, uint8_t hour,
                   uint8_t min,  uint8_t sec)

{
    uint16_t year_4;
    uint16_t index;
    uint32_t seccount = 0;
    
    year_4 = year + 2000;
    if ((year_4 < 1970) || (year_4 > 2099))
    {
        return;
    }
    
    for (index=1970; index<year_4; index++)
    {
        if (is_leap_year(index))
        {
            seccount += 31622400;
        }
        else
        {
            seccount += 31536000;
        }
    }
    
    month--;
    for (index=0; index<month; index++)
    {
        seccount += month_table[index] * 86400;
        if (is_leap_year(year_4) && (index == 1))
        {
            seccount += 86400;
        }
    }
    
    seccount += (date - 1) * 86400;
    seccount += hour * 3600;
    seccount += min * 60;
    seccount += sec;
    
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_PMU);     /* 使能PMU时钟 */
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_BAKR);    /* 使能BAKPR时钟 */
    PMU_EnableBackupAccess();                           /* 使能访问备份寄存器 */
    
    RTC_ConfigAlarm(seccount);                          /* 设置RTC闹钟 */
    RTC_WaitForLastTask();
}
从上面的代码中可以看到,通过传入的年,月,日,时,分,秒等参数对闹钟进行设置,而这些参数已经在RTC初始化函数中已经设置完成,用户只需调根据自己的需求进行函数调用即可,用户同时也可以在RTC初始化函数中对以上参数进行自定义设置。在这里便不做过多阐述。
27.2.5 实验应用代码
本章实验的应用代码,如下所示:
int main(void)
{
    uint8_t year;
    uint8_t month;
    uint8_t date;
    uint8_t week;
    uint8_t hour;
    uint8_t min;
    uint8_t sec;
    uint8_t tbuf[40];
    uint8_t t = 0;
    
    NVIC_ConfigPriorityGroup(NVIC_PRIORITY_GROUP_4);  /* 设置中断优先级分组为组4 */
    sys_apm32_clock_init(15);                         /* 配置系统时钟 */
    delay_init(120);                                  /* 初始化延时功能 */
    usart_init(115200);                               /* 初始化串口 */
    usmart_dev.init(120);                             /* 初始化USMART */
    led_init();                                       /* 初始化LED */
    lcd_init();                                       /* 初始化LCD */
    rtc_init();                                       /* 初始化RTC */
    
    lcd_show_string(30, 50, 200, 16, 16, "APM32", RED);
    lcd_show_string(30, 70, 200, 16, 16, "RTC TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    
    while (1)
    {
        t++;
        if ((t % 10) == 0)                            /* 每100毫秒更新一次数据 */
        {
            rtc_get(&year, &month, &date, &week, &hour, &min, &sec);
            sprintf((char *)tbuf, "Time:%02d:%02d:%02d", hour, min, sec);
            lcd_show_string(30, 130, 210, 16, 16, (char *)tbuf, RED);
            sprintf((char *)tbuf, "Date:20%02d-%02d-%02d", year, month, date);
            lcd_show_string(30, 150, 210, 16, 16, (char *)tbuf, RED);
            sprintf((char *)tbuf, "Week:%d", week);
            lcd_show_string(30, 170, 210, 16, 16, (char *)tbuf, RED);
        }
        if ((t % 20) == 0)
        {
            LED0_TOGGLE();
        }
        
        delay_ms(10);
    }
}

从上面的代码中可以看到,在初始化完RTC后便每间隔100毫秒获取一次RTC的时间和日期,并在LCD上进行显示。
本实验同时也使用的USMART调试组件,并在usart_config.c文件中添加了RTC驱动中相关的函数,以便调试。
27.3 下载验证
在完成编译和烧录操作后,可以看到LCD上实时地显示着RTC的时间,并且可以看到LED1在RTC周期性唤醒的驱动下以0.5Hz的频率闪烁着,此时可以通过串口调试助手调用USMART调试组件的rtc_set_alarma()函数来设置RTC的闹钟,当通过LCD观察到RTC的时间达到设置的闹钟时间后,可以看到串口调试助手上打印了“ALARM A!\r\n”的字符串提示。

物联沃分享整理
物联沃-IOTWORD物联网 » 【正点原子STM32连载】第27章:APM32E103最小系统板使用指南中的RTC实验

发表评论