1)实验平台:正点原子APM32E103最小系统板
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban
本章介绍APM32E103实时时钟(RTC)的使用,实时时钟能为系统提供一个准确的时间,即时系统复位或主电源断电,RTC依然能够运行,因此RTC也经常用于各种低功耗场景。通过本章的学习,读者将学习到RTC的使用。
本章分为如下几个小节:
27.1 硬件设计
27.2 程序设计
27.3 下载验证
27.1 硬件设计
27.1.1 例程功能
#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”的字符串提示。