1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html
本章,我们将介绍STM32F103的内部实时时钟(RTC)。我们将使用LCD模块来显示日期和时间,实现一个简单的实时时钟,并可以设置闹铃,另外还将介绍BKP的使用。
本章分为如下几个小节:
27.1 RTC时钟简介
27.2 硬件设计
27.3 程序设计
27.4 下载验证
STM32F103的实时时钟(RTC)是一个独立的定时器。STM32的RTC模块拥有一组连续计数的计数器,在相对应的软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统的当前时间和日期。
RTC模块和时钟配置系统(RCC_BDCR寄存器)是在后备区域,即在系统复位或从待机模式唤醒后RTC的设置和时间维持不变,只要后备区域供电正常,那么RTC将可以一直运行。但是在系统复位后,会自动禁止访问后备寄存器和RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前,先要取消备份区域(BKP)写保护。
27.1.1 RTC框图
下面先来学习RTC框图,通过学习RTC框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。RTC的框图,如图27.1.1所示:
图27.1.1 RTC框图
我们在讲解RTC架构之前,说明一下框图中浅灰色的部分,他们是属于备份域的,在VDD掉电时可在VBAT的驱动下继续工作,这部分包括RTC的分频器,计数器以及闹钟控制器。在寄存器部分才展开解释一下备用域。下面把RTC框图分成以下2个部分讲解:
① APB1接口: 用来和APB1总线相连。通过APB1总线可以访问RTC相关的寄存器,对其进行读写操作。
② RTC核心: 由一组可编程计数器组成,主要分成两个模块。第一个模块是RTC的预分频模块,它可编程产生1秒的RTC时间基准TR_CLK。RTC的预分频模块包括了一个20位的可编程分频器(RTC预分频器)。如果在RTC_CR寄存器中设置相对应的允许位,则在每个TR_CLK周期中RTC产生一个中断(秒中断)。第二个模块是一个32位的可编程计数器,可被初始化为当前的系统时间,一个32位的时钟计数器,按秒钟计算,可以记录4294967296 秒,约合136年左右,作为一般应用足够了。
RTC还有一个闹钟寄存器RTC_ALR,用于产生闹钟。系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时间相比较,如果RTC_CR控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断。
由于备份域的存在,所以RTC内核可以完全独立于RTC APB1接口。而软件是通过 APB1 接口访问RTC的预分频值、计数器值和闹钟值的。但是相关可读寄存器只在 RTC APB1 时钟进行重新同步的 RTC 时钟的上升沿被更新,RTC 标志也是如此。这就意味着,如果 APB1 接口刚刚被开启之后,在第一次的内部寄存器更新之前,从 APB1 上读取的 RTC 寄存器值可能被破坏了(通常读到 0)。因此,若在读取 RTC 寄存器曾经被禁止的 RTC APB1 接口,软件首先必须等待 RTC_CRL 寄存器的 RSF 位(寄存器同步标志位,bit3)被硬件置 1。
27.1.2 RTC寄存器
接下来,我们介绍本实验我们要用到的RTC寄存器。
RTC控制寄存器(RTC_CRH/CRL)
RTC控制寄存器共有两个控制寄存器RTC_CRH和RTC_CRL,两个都是16位的。
RTC控制寄存器高位RTC_CRH,描述如图27.1.2.1所示:
图27.1.2.1 RTC_CRH寄存器
该寄存器是RTC控制寄存器高位,本章将用到秒钟中断,所以在该寄存器必须设置最低位为1,以允许秒钟中断。
RTC控制寄存器低位RTC_CRL,描述如图27.1.2.2所示:
图27.1.2.2 RTC_CRL寄存器
该寄存器是RTC控制寄存器低位,本章我们用到的是该寄存器的0,3~5这几个位,第0位是秒钟标志位,我们在进入闹钟中断的时候,通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零(写零)。第3位为寄存器同步标志位,我们在修改控制寄存器RTC_CRH/RTC_CRL之前,必须先判断该位,是否已经同步了,如果没有则需要等待同步,在没同步的情况下修改RTC_CRH/RTC_CRL的值是不行的。第4位为配置标志位,在软件修改RTC_CNT/RTC_PRL的值的时候,必须先软件置位该位,以允许进入配置模式。第5位为RTC操作位,该位由硬件操作,软件只读。通过该位可以判断上次对 RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才能开始下一次操作。
RTC预分频装载寄存器(RTC_PRLH/RTC_PRLL)
RTC预分频装载寄存器也是有两个寄存器组成,RTC_PRLH和RTC_PRLL。这两个寄存器用来配置RTC时钟的分频数的,比如我们使用外部32.768K的晶振作为时钟的输入频率,那么我们要设置这两个寄存器的值为32767,得到一秒钟的计数频率。
RTC预分频装载寄存器高位描述如图27.1.2.3所示:
图27.1.2.3 RTC_PRLH寄存器
该寄存器是RTC预分频装载寄存器高位,低四位有效用来存放PRL的19~16位。关于PRL的其余位存放在RTC_PRLL寄存器。
RTC预分频装载寄存器低位描述如图27.1.2.4所示:
图27.1.2.4 RTC_PRLL寄存器
该寄存器是RTC预分频装载寄存器低位,存放RTC预分频装载值低位。如果输入时钟是32.768kHz,这个预分频寄存器中写入0x7FFF可获得周期1秒钟的信号。
RTC预分频余数寄存器(RTC_DIVH/RTC_DIVL)
RTC预分频余数寄存器是用来获得比秒钟更加准确的时钟,比如可以得到0.1秒,甚至0.01秒等,该寄存器的值自减的,用于保存还需要多少时钟周期获得一个秒信号。在一次秒钟更新后,由硬件重新装载。这两个寄存器和 RTC 预分频装载寄存器的各位是一样的,这里我们就不列出来了。
RTC计数器寄存器(RTC_CNTH/RTC_CNTL)
RTC计数器寄存器RTC_CNT,由2个16位寄存器组成RTC_CNTH和RTC_CNTL,总共32位,用来记录秒钟值。注意的是,修改这两个寄存器的时候要先进入配置模式。RTC_CNT描述如图27.1.2.5所示:
图27.1.2.5 RTC_CNT寄存器
RTC闹钟寄存器(RTC_ALRH/RTC_ALRL)
RTC闹钟寄存器,该寄存器也是由2个16位的寄存器组成RTC_ALRH和RTC_ALRL。总共32位,用来标记闹钟产生的时间(以秒为单元)。对于STM32F1系列的芯片来说,RTC外设没有专门的年用日寄存器来分别存放这些信息,全部日期信息以秒的格式存储在这两个寄存器中,后面编程时会对时间进行特殊处理。如果RTC_CNT的值与RTC_ALR的值相等,并使能了中断的话,会产生一个闹钟中断。注意:该寄存器的修改也是要进入配置模式才能进行。
RTC闹钟寄存器描述如图27.1.2.6所示:
图27.1.2.5 RTC_ALR寄存器
备份数据寄存器(BKP_DRx)
备份数据寄存器描述如图27.1.2.6所示。
图27.1.2.6 BKP_DRx寄存器
该寄存器是一个16位寄存器,可以用来存储用户数据,可在VDD电源关闭时,通过VBAT保持上电状态。备份数据寄存器不会在系统复位、电源复位、从待机模式唤醒时复位。
那么在MCU复位后,对RTC和备份数据寄存器的写访问就被禁止,需要执行一下操作才可以对RTC及备份数据寄存器进行写访问:
1)通过设置寄存器RCC_APB1ENR的PWREN和BKPEN位来打开电源和后备接口时钟
2)电源控制寄存器(PWR_CR)的DBP位来使能对后备寄存器和RTC访问
备份区域控制寄存器(RCC_BDCR)
备份区域控制寄存器描述如图25.1.2.7所示。
图25.1.2.7 RCC_BDCR寄存器
RTC的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在RTC操作之前先要通过这个寄存器选择RTC的时钟源,然后才能开始其他的操作。
27.2 硬件设计
typedef struct
{
RTC_TypeDef *Instance; /* 寄存器基地址 */
RTC_InitTypeDef Init; /* RTC配置结构体 */
RTC_DateTypeDef DataToUpdata; /* RTC 日期结构体 */
HAL_LockTypeDef Lock; /* RTC锁定对象 */
__IO HAL_RTCStateTypeDef State; /* RTC设备访问状态 */
}RTC_HandleTypeDef;
1)Instance:指向RTC寄存器基地址。
2)Init:是真正的RTC初始化结构体,其结构体类型RTC_InitTypeDef定义如下:
typedef struct
{
uint32_t AsynchPrediv; /* 异步预分频系数 */
uint32_t OutPut; /* 选择连接到RTC_ALARM输出的标志 */
}RTC_InitTypeDef;
AsynchPrediv用来设置RTC的异步预分频系数,也就是设置两个预分频重载寄存器的相关位,因为异步预分频系数是19位,所以最大值为0x7FFFF,不能超过这个值。
OutPut用来选择RTC输出到Tamper引脚的信号,取值为:RTC_OUTPUTSOURCE_NONE(没有输出),RTC_OUTPUTSOURCE_CALIBCLOCK(RTC时钟经过64分频输出到TAMPER),RTC_OUTPUTSOURCE_ALARM(闹钟脉冲信号输出)和RTC_ OUTPUTSOURCE_SECOND(秒脉冲信号输出)。本实验选择没有输出,不配置即默认值,RTC_OUTPUTSOURCE_NONE。
3)DataToUpdata:日期结构体。
4)Lock:用于配置锁状态。
5)State:RTC设备访问状态。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
注意:实验中没有使用HAL库自带的设置RTC时间的函数HAL_RTC_SetTime、设置RTC日期的函数HAL_RTC_SetDate、获取当前RTC日期的函数HAL_RTC_GetTime。原因在于版本的HAL库函数不满足我们同时更新年月日时分秒的要求,且在实测中发现写时间会覆盖日期,写日期亦然,所以我们直接通过操作寄存器的方式去编写功能更加全面的函数。
RTC配置步骤
1)使能电源时钟,并使能RTC及RTC后备寄存器写访问。
我们要访问RTC和RTC备份区域就必须先使能电源时钟,然后使能RTC即后备区域访问。电源时钟使能,通过RCC_APB1ENR寄存器来设置;RTC及RTC备份寄存器的写访问,通过PWR_CR寄存器的DBP位设置。HAL库设置方法为:
__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟PWR /
__HAL_RCC_BKP_CLK_ENABLE(); / 使能备份时钟 /
HAL_PWR_EnableBkUpAccess(); / 取消备份区域写保护 */
2)开启外部低速振荡器LSE,选择RTC时钟,并使能。
调用HAL_RCC_OscConfig函数配置开启LSE。
调用HAL_RCCEx_PeriphCLKConfig函数选择RTC时钟源。
使能RTC时钟函数为__HAL_RCC_RTC_ENABLE。
3)初始化RTC,设置RTC的分频,以及配置RTC参数。
在HAL中,通过函数HAL_RTC_Init函数配置RTC分频系数,以及RTC的工作参数。
注意:该函数会调用HAL_RTC_MspInit函数来完成对RTC的底层初始化,包括:RTC时钟使能,时钟源选择等。
4)设置RTC的日期和时间。
根据我们前面的说明,我们使用操作寄存器的方式重新定义了设置RTC日期和时间的函数rtc_set_time,使用该函数就可以设置年月日时分秒。
5)获取RTC当前日期和时间。
同样的,获取RTC当前日期和时间的函数rtc_get_time,我们也是直接重新定义。该函数不直接返回时间,而是把时间保存在我们定义的时间结构体里。
通过以上5个步骤,我们就完成了对RTC的配置,RTC即可正常工作,这些操作不是每次上电都必须执行的,视情况而定。我们还可以设置闹钟,这些将在后面介绍。
27.3.2 程序流程图
图27.3.2.1 RTC实时时钟实验程序流程图
27.3.3 程序解析
/**
* @brief RTC初始化
* @note 默认尝试使用LSE,当LSE启动失败后,切换为LSI.
* 通过BKP寄存器0的值,可以判断RTC使用的是LSE/LSI:
* 当BKP0==0X5050时,使用的是LSE
* 当BKP0==0X5051时,使用的是LSI
* 注意:切换LSI/LSE将导致时间/日期丢失,切换后需重新设置.
*
* @param 无
* @retval 0,成功
* 1,进入初始化模式失败
*/
uint8_t rtc_init(void)
{
/* 检查是不是第一次配置时钟 */
uint16_t bkpflag = 0;
__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟 */
__HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份时钟 */
HAL_PWR_EnableBkUpAccess(); /* 取消备份区写保护 */
bkpflag = rtc_read_bkr(0); /* 读取BKP0的值 */
g_rtc_handle.Instance = RTC;
/*时钟周期设置,理论值:32767, 这里也可以用 RTC_AUTO_1_SECOND */
g_rtc_handle.Init.AsynchPrediv = 32767;
if (HAL_RTC_Init(&g_rtc_handle) != HAL_OK)
{
return 1;
}
/* 之前未初始化过,重新配置 */
if ((bkpflag != 0x5050) && (bkpflag != 0x5051))
{
rtc_set_time(2020, 4, 26, 9, 22, 35); /* 设置时间 */
}
__HAL_RTC_ALARM_ENABLE_IT(&g_rtc_handle, RTC_IT_SEC); /* 允许秒中断 */
HAL_NVIC_SetPriority(RTC_IRQn, 0x2, 0); /* 优先级设置* /
HAL_NVIC_EnableIRQ(RTC_IRQn); /* 使能RTC中断通道 */
rtc_get_time(); /* 更新时间 */
return 0;
}
该函数用来初始化RTC配置以及日期和时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位都不会再进行时间设置了(前提是备份电池有电)。在第一次配置的时候,我们是按照上面介绍的RTC初始化步骤调用函数HAL_RTC_Init来实现的。
我们通过读取BKP寄存器0的值来判断是否需要进行时间的设置,对BKP寄存器0的写操作是在HAL_RTC_MspInit回调函数中实现,下面会讲。第一次未对RTC进行初始化BKP寄存器0的值非0x5050非0x5051,当进行RTC初始化时,BKP寄存器0的值就是0x5050或0x5051,所以以上代码操作确保时间只会设置一次,复位时不会重新设置时间。电池正常供电时,我们设置的时间不会因复位或者断电而丢失。
读取后备寄存器的函数其实还是调用HAL库提供的函数接口,写后备寄存器函数同样也是。这两个函数如下:
uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister);
void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister,
uint32_t Data);
这两个函数的使用方法就非常简单,分别用来读和写BKR寄存器的值。这里我们只是略微点到为止,详看例程源码。
这里设置时间和日期,是通过rtc_set_time函数来实现的,我们之所以不是用HAL库自带的设置时间和日期的函数,在前面已经提到了这个原因,就不用多说了。那么rtc_set_time是我们直接操作寄存器,同时,它也可以为我们的USMART所调用,十分方便我们调试时候使用。
接下来,我们用HAL_RTC_MspInit函数来编写RTC时钟配置等代码,其定义如下:
void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
{
uint16_t retry = 200;
__HAL_RCC_RTC_ENABLE(); /* RTC时钟使能 */
RCC_OscInitTypeDef rcc_oscinitstruct;
RCC_PeriphCLKInitTypeDef rcc_periphclkinitstruct;
/* 使用寄存器的方式去检测LSE是否可以正常工作 */
RCC->BDCR |= 1 << 0; /* 开启外部低速振荡器LSE */
while (retry && ((RCC->BDCR & 0X02) == 0)) /* 等待LSE准备好 */
{
retry--;
delay_ms(5);
}
if (retry == 0) /* LSE起振失败 使用LSI */
{ /* 选择要配置的振荡器 */
rcc_oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_LSI;
rcc_oscinitstruct.LSEState = RCC_LSI_ON; /* LSI状态:开启 */
rcc_oscinitstruct.PLL.PLLState = RCC_PLL_NONE; /* PLL无配置 */
HAL_RCC_OscConfig(&rcc_oscinitstruct);
/* 选择要配置的外设 RTC */
rcc_periphclkinitstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;
/* RTC时钟源选择 LSI */
rcc_periphclkinitstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
HAL_RCCEx_PeriphCLKConfig(&rcc_periphclkinitstruct);
rtc_write_bkr(0, 0X5051);
}
else
{
rcc_oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_LSE ;
rcc_oscinitstruct.LSEState = RCC_LSE_ON; /* LSE状态:开启 */
rcc_oscinitstruct.PLL.PLLState = RCC_PLL_NONE; /* PLL不配置 */
rcc_periphclkinitstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;
rcc_periphclkinitstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
HAL_RCCEx_PeriphCLKConfig(&rcc_periphclkinitstruct);
rtc_write_bkr(0, 0X5050);
}
}
介绍完RTC初始化相关函数后,我们来介绍一下rtc_set_time函数,代码如下:
/**
* @brief 设置时间, 包括年月日时分秒
* @note 以1970年1月1日为基准, 往后累加时间
* 合法年份范围为: 1970 ~ 2105年
HAL默认为年份起点为2000年
* @param syear : 年份
* @param smon : 月份
* @param sday : 日期
* @param hour : 小时
* @param min : 分钟
* @param sec : 秒钟
* @retval 0, 成功; 1, 失败;
*/
uint8_t rtc_set_time(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{
uint32_t seccount = 0;
/* 将年月日时分秒转换成总秒钟数 */
seccount = rtc_date2sec(syear, smon, sday, hour, min, sec);
__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟 */
__HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份域时钟 */
HAL_PWR_EnableBkUpAccess(); /* 取消备份域写保护 */
/* 上面三步是必须的! */
RTC->CRL |= 1 << 4; /* 进入配置模式 */
RTC->CNTL = seccount & 0xffff;
RTC->CNTH = seccount >> 16;
RTC->CRL &= ~(1 << 4); /* 退出配置模式 */
/* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */
while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF));
return 0;
}
该函数用于设置时间,把我们输入的时间,转换为以1970年1月1日0时0分0秒做起始时间的秒钟信号,后续的计算都以这个时间为基准,由于STM32的秒钟计数器可以保存136年的秒钟数据,这样我们就可以计时到2106年。
接着,我们介绍rtc_set_alarma函数,该函数用于设置闹钟时间,同rtc_set_time函数几乎一模一样,主要区别:就是将RTCCNTL和RTCCNTH换成了RTCALRL和RTCALRH,用于设置闹钟时间。RTC其实是有闹钟中断的,我们这里并没有用到,本实验用到了秒中断,所以在秒中断里顺带处理闹钟中断的事情。具体代码请参考本例程源码。
特别提醒:假如只是使用HAL库的__HAL_RTC_ALARM_ENABLE_IT函数来使能闹钟中断,但是没有设置闹钟相关的NVIC和EXTI,实际上不会产生闹钟中断,只会产生闹钟标志(RTC->CRL的ALRL置位)。可以通过读取闹钟标志来判断是否发生闹钟事件。
接着,我们介绍一下rtc_get_time函数,其定义如下:
/**
* @brief 得到当前的时间
* @note 该函数不直接返回时间, 时间数据保存在calendar结构体里面
* @param 无
* @retval 无
*/
void rtc_get_time(void)
{
static uint16_t daycnt = 0;
uint32_t seccount = 0;
uint32_t temp = 0;
uint16_t temp1 = 0;
/* 平年的月份日期表 */
const uint8_t month_table[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
seccount = RTC->CNTH; /* 得到计数器中的值(秒钟数) */
seccount <<= 16;
seccount += RTC->CNTL;
temp = seccount / 86400; /* 得到天数(秒钟数对应的) */
if (daycnt != temp) /* 超过一天了 */
{
daycnt = temp;
temp1 = 1970; /* 从1970年开始 */
while (temp >= 365)
{
if (rtc_is_leap_year(temp1)) /* 是闰年 */
{
if (temp >= 366)
{
temp -= 366; /* 闰年的秒钟数 */
}
else
{
break;
}
}
else
{
temp -= 365; /* 平年 */
}
temp1++;
}
calendar.year = temp1; /* 得到年份 */
temp1 = 0;
while (temp >= 28) /* 超过了一个月 */
{
/* 当年是不是闰年/2月份 */
if (rtc_is_leap_year(calendar.year) && temp1 == 1)
{
if (temp >= 29)
{
temp -= 29; /* 闰年的秒钟数 */
}
else
{
break;
}
}
else
{
if (temp >= month_table[temp1])
{
temp -= month_table[temp1]; /* 平年 */
}
else
{
break;
}
}
temp1++;
}
calendar.month = temp1 + 1; /* 得到月份 */
calendar.date = temp + 1; /* 得到日期 */
}
temp = seccount % 86400; /* 得到秒钟数 */
calendar.hour = temp / 3600; /* 小时 */
calendar.min = (temp % 3600) / 60; /* 分钟 */
calendar.sec = (temp % 3600) % 60; /* 秒钟 */
/* 获取星期 */
calendar.week = rtc_get_week(calendar.year, calendar.month, calendar.date);
}
该函数其实就是将存储在秒钟寄存器RTC->CNTL和RTC->CNTH中的秒钟数据转换为真正的时间和日期。该代码还用到了一个calendar的结构体,calendar是我们在rtc.h里面将要定义的一个时间结构体,用来存放时钟的年月日时分秒等信息。因为STM32的RTC只有秒钟计数器,而年月日,时分秒则需要我们自己软件计算。我们把计算好的值保存在calendar里面,方便其他函数调用。
接着,我们介绍一下使用多次数最多的函数rtc_date2sec,该函数代码如下:
/**
* @brief 将年月日时分秒转换成秒钟数
* @note 以1970年1月1日为基准, 1970年1月1日, 0时0分0秒, 表示第0秒钟
* 最大表示到2105年, 因为uint32_t最大表示136年的秒钟数(不包括闰年)!
* 本代码参考只linux mktime函数, 原理说明见此贴:
* http://www.openedv.com/thread-63389-1-1.html
* @param syear : 年份
* @param smon : 月份
* @param sday : 日期
* @param hour : 小时
* @param min : 分钟
* @param sec : 秒钟
* @retval 转换后的秒钟数
*/
static long rtc_date2sec(uint16_t syear, uint8_t smon, uint8_t sday,
uint8_t hour, uint8_t min, uint8_t sec)
{
uint32_t Y, M, D, X, T;
signed char monx = smon; /* 将月份转换成带符号的值, 方便后面运算 */
if (0 >= (monx -= 2)) /* 1..12 -> 11,12,1..10 */
{
monx += 12; /* Puts Feb last since it has leap day */
syear -= 1;
}
/* 公元元年1到现在的闰年数 */
Y = (syear - 1) * 365 + syear / 4 - syear / 100 + syear / 400;
M = 367 * monx / 12 - 30 + 59;
D = sday - 1;
X = Y + M + D - 719162; /* 减去公元元年到1970年的天数 */
T = ((X * 24 + hour) * 60 + min) * 60 + sec; /* 总秒钟数 */
return T;
}
该函数参考了linux的mktime函数,用于将年月日时分秒转化成秒钟数,进而被其他函数使用,例如rtc_set_time和rtc_set_alarm,那两个函数的形参是需要使用rtc_date2sec函数获取秒钟数,进而操作寄存器的方法把总秒数写入特定的寄存器完成相对应的功能。
前面介绍的函数rtc_init中,存在RTC中断使能操作,那么这里必定是有中断服务函数,接下来看中断服务函数,代码如下:
/**
* @brief RTC时钟中断
* @note 秒钟中断服务函数,顺带处理闹钟标志
* 根据RTC_CRL寄存器的 SECF 和 ALRF 位区分是哪个中断
* @param 无
* @retval 无
*/
void RTC_IRQHandler(void)
{
if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle,RTC_FLAG_SEC) != RESET)/*秒中断*/
{
rtc_get_time(); /* 更新时间 */
__HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_SEC); /* 清除秒中断 */
//printf("sec:%d\r\n", calendar.sec); /* 打印秒钟 */
}
/* 顺带处理闹钟标志 */
if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF) != RESET)
{
__HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF); /*清除闹钟标志*/
printf("Alarm Time:%d-%d-%d %d:%d:%d\n", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);
}
__HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_OW); /* 清除溢出中断标志 */
/* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */
while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF));
}
RTC_IRQHandle中断服务函数用于RTC秒中断的, 由于在rtc_init中已经配置好了时钟周期为1秒,所以每一秒都会跳进RTC中断服务函数中。在函数中,判断秒中断是否触发,由于每一次都是秒中断触发,所以可以先更新时间,然后把printf的注释去掉看一下效果,是不是每一秒打印一下。接着判断闹钟标志是否置位,这个闹钟标志跟我们的rtc_set_alarm函数有关,假设时间到了闹钟设置的时间,就会跳进该秒中断中顺带处理闹钟标志,执行函数体的指令。执行完上述的任务之后,需要在最后清除溢出中断标志。
rtc.c的其他程序,这里就不再介绍了,请大家直接看源码。
2. main.c代码
在main.c里面编写如下代码:
/* 定义字符数组用于显示周 */
char* weekdays[]={"Sunday","Monday","Tuesday","Wednesday",
"Thursday","Friday","Saterday"};
int main(void)
{
uint8_t tbuf[40];
uint8_t t = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
usmart_dev.init(72); /* 初始化USMART */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
rtc_init(); /* 初始化RTC */
rtc_set_alarm(2020, 4, 26, 9, 23, 45); /* 设置一次闹钟 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", 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) /* 每100ms更新一次显示数据 */
{
rtc_get_time();
sprintf((char *)tbuf, "Time:%02d:%02d:%02d", calendar.hour,
calendar.min, calendar.sec);
lcd_show_string(30, 120, 210, 16, 16, (char *)tbuf, RED);
sprintf((char *)tbuf, "Date:%04d-%02d-%02d", calendar.year,
calendar.month, calendar.date);
lcd_show_string(30, 140, 210, 16, 16, (char *)tbuf, RED);
sprintf((char *)tbuf, "Week:%s", weekdays[calendar.week]);
lcd_show_string(30, 160, 210, 16, 16, (char *)tbuf, RED);
}
if ((t % 20) == 0)
{
LED0_TOGGLE(); /* 每200ms,翻转一次LED0 */
}
delay_ms(10);
}
}
我们在无限循环中每100ms读取RTC的时间和日期(一次),并显示在LCD上面。每200ms,翻转一次LED0。
为方便RTC相关函数的调用验证,在usmart_config.c里面,修改了usmart_nametab如下:
/* 函数名列表初始化(用户自己添加)
* 用户直接在这里输入要执行的函数名及其查找串
*/
struct _m_usmart_nametab usmart_nametab[] =
{
#if USMART_USE_WRFUNS == 1 /* 如果使能了读写操作 */
(void *)read_addr, "uint32_t read_addr(uint32_t addr)",
(void *)write_addr, "void write_addr(uint32_t addr,uint32_t val)",
#endif
(void *)delay_ms, "void delay_ms(uint16_t nms)",
(void *)delay_us, "void delay_us(uint32_t nus)",
(void *)rtc_read_bkr, "uint16_t rtc_read_bkr(uint32_t bkrx)",
(void *)rtc_write_bkr, "void rtc_write_bkr(uint32_t bkrx, uint16_t data)",
(void *)rtc_get_week, "uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t day)",
(void *)rtc_set_time, "uint8_t rtc_set_time(uint16_t syear, uint8_t smon,
uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)",
(void *)rtc_set_alarm, "uint8_t rtc_set_alarm(uint16_t syear, uint8_t smon,
uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)",
};
将RTC的一些相关函数加入了usmart,这样通过串口就可以直接设置RTC时间、闹钟。 至此,RTC的软件设计就完成了,接下来就让我们来检验一下,程序是否正确。
27.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。然后,可以看到LCD开始显示时间,实际显示效果如图27.4.1所示:
在这里插入图片描述
图27.4.1 RTC实验测试图
如果时间不正确,可以利用上一章介绍的usmart工具,通过串口来设置,并且可以设置闹钟时间等,如图27.4.2所示:
图27.4.2 通过USMART设置时间并测试闹钟
按照图中编号1、2顺序,设置闹钟、设置时间。然后等待我们设置的时间到来后,串口打印Alarm Time:2020-9-15 12:30:45这个字符串,证明我们的闹钟程序正常运行了!