(1)实验平台:普中STM32F103朱雀、玄武开发板
这一章我们来学习下 STM32F1 的实时时钟(以下简称 RTC) , 利用 RTC 设计一个简单的电子时钟。 本章要实现的功能是: 通过串口打印出日期和时间, DS0指示灯闪烁提示系统运行。 学习本章可以参考《STM32F10x 中文参考手册》 -16实时时钟(RTC) 章节, 特别是寄存器介绍部分。 本章分为如下几部分内容:
[34.1 STM32F1RTC 介绍](#34.1 STM32F1RTC 介绍)
[34.1.1 RTC 简介](#34.1.1 RTC 简介)
[34.1.2 RTC 结构框图](#34.1.2 RTC 结构框图)
[34.2 RTC 配置步骤](#34.2 RTC 配置步骤)
[34.3 硬件设计](#34.3 硬件设计)
[34.4 软件设计](#34.4 软件设计)
[34.4.1 RTC 初始化函数](#34.4.1 RTC 初始化函数)
[34.4.2 RTC 设置日期时间函数](#34.4.2 RTC 设置日期时间函数)
[34.4.3 获取 RTC 时间函数](#34.4.3 获取 RTC 时间函数)
[34.4.4 RTC 中断服务函数](#34.4.4 RTC 中断服务函数)
[34.4.5 主函数](#34.4.5 主函数)
[34.5 实验现象](#34.5 实验现象)
34.1 STM32F1RTC 介绍
34.1.1 RTC 简介
STM32 的实时时钟(RTC) 是一个独立的定时器。 STM32 的 RTC 模块拥有一组连续计数的计数器, 在相应软件配置下, 可提供时钟日历的功能。 修改计数器的值可以重新设置系统当前的时间和日期。
RTC 模块和时钟配置是在后备区域, 无论器件状态如何(运行模式、 低功耗模式或处于复位状态) , 只要保证后备区域供电正常, RTC 便不会停止工作, 所以通常会在后备区域供电端加一个纽扣电池, 即使主电源停止供电, 后备电源也会启动供电, 从而保证 RTC 时钟不停的运行, 只有当主电源和后备纽扣电池都没有电的时, RTC 才停止工作。 RTC 中的数据都保存在属于 RTC 的备份域中, 若主电源 VDD 和 VBAT 都掉电, 那么备份域中保存的所有数据将丢失。 备份域除了 RTC 模块的寄存器, 还有 42 个 16 位的寄存器可以在 VDD 掉电的情况下保存用户程序的数据, 系统复位或电源复位时, 这些数据也不会被复位。 我们一般用 BKP 来存储 RTC 的校验值或者记录一些重要的数据, 相当于一个 EEPROM,不过这个 EEPROM 并不是真正的 EEPROM, 而是需要电池来维持它的数据。
从 RTC 的定时器特性来说, 它是一个 32 位的计数器, 只能向上计数。 它的时钟来源有三种, 分别为高速外部时钟的 128 分频( HSE/128) 、 低速内部时钟 LSI 以及低速外部时钟 LSE。 使用 HSE 分频时钟或 LSI 的话, 在主电源VDD 掉电的情况下, 这两个时钟来源都会受到影响, 因此没法保证 RTC 正常工作。 所以 RTC 一般使用低速外部时钟 LSE, 在设计中, 频率通常为实时时钟模块中常用的 32.768KHz, 这是因为 32768 = 2^15, 分频容易实现, 所以它被广泛应用到 RTC 模块。 在主电源 VDD 有效的情况下(待机), RTC 还可以配置闹钟事件使 STM32 退出待机模式。
34.1.2 RTC 结构框图
STM32F1 RTC 拥有这么多功能, 是由 RTC 内部结构决定。 要更好的理解 STM32F1 的 RTC, 就需要了解它内部的结构。 如下图所示: ( 大家也可以查看《STM32F10x 中文参考手册》 -16 实时时钟(RTC) 章节内容)

从图中可以看出, 浅灰色的部分都是属于备份域的, 在 VDD 掉电时可在VBAT 的驱动下继续运行。 这部分仅包括 RTC 的分频器, 计数器, 和闹钟控制器。若 VDD 电源有效, RTC 可以触发 RTC_Second(秒中断)、 RTC_Overflow(溢出事件)和 RTC_Alarm(闹钟中断)。 从上图可以分析到, 其中的定时器溢出事件无法被配置为中断。 若 STM32 原本处于待机状态, 可由闹钟事件或 WKUP 事件(外部唤醒事件, 属于 EXTI 模块, 不属于 RTC)使它退出待机模式。 闹钟事件是在计数器 RTC_CNT 的值等于闹钟寄存器 RTC_ALR 的值时触发的。
在备份域中所有寄存器都是 16 位的, RTC 控制相关的寄存器也不例外。它的计数器 RTC_CNT 的 32 位由 RTC_CNTL 和 RTC_CNTH 两个寄存器组成, 分别保存定时计数值的低 16 位和高 16 位。 在配置 RTC 模块的时钟时, 通常把输入的 32768Hz 的 RTCCLK 进行 32768 分频得到实际驱动计数器的时钟TR_CLK = RTCCLK/32768= 1 Hz, 计时周期为 1 秒, 计时器在 TR_CLK 的驱动下计数, 即每秒计数器 RTC_CNT 的值加 1。
由于备份域的存在, 使得 RTC 核具有了完全独立于 APB1 接口的特性, 也因此对 RTC 寄存器的访问要遵守一定的规则。
系统复位后, 默认禁止访问后备寄存器和 RTC, 防止对后备区域(BKP)的意外写操作。 执行以下操作使能对后备寄存器和 RTC 的访问:
①设置 RCC_APB1ENR 寄存器的 PWREN 和 BKPEN 位来使能电源和后备接口时钟。
②设置电源控制寄存器(PWR_CR) 的 DBP 位使能对后备寄存器和 RTC 的访问。
设置后备寄存器为可访问后, 在第一次通过 APB1 接口访问 RTC 时, 因为时钟频率的差异, 所以必须等待 APB1 与 RTC 外设同步, 确保被读取出来的 RTC寄存器值是正确的。 若在同步之后, 一直没有关闭 APB1 的 RTC 外设接口, 就不需要再次同步了。
如果内核要对 RTC 寄存器进行任何的写操作, 在内核发出写指令后, RTC模块在 3 个 RTCCLK 时钟之后, 才开始正式的写 RTC 寄存器操作。 由于 RTCCLK的频率比内核主频低得多, 所以每次操作后必须要检查 RTC 关闭操作标志位RTOFF, 当这个标志被置 1 时, 写操作才正式完成。
由于篇幅限制, 本章并没有对 RTC 相关寄存器进行介绍, 大家可以参考《STM32F10x 中文参考手册》 -16 实时时钟(RTC) 章节, 里面有详细的讲解。 如果看不懂的可以暂时放下, 因为我们使用的是库函数开发。
34.2 RTC 配置步骤
接下来我们介绍下如何使用库函数对 STM32F1 的 RTC 进行配置。 这个也是在编写程序中必须要了解的。 具体步骤如下:(RTC 相关库函数在 stm32f10x_rtc.c和 stm32f10x_rtc.h 文件中)
(1) 使能电源时钟和后备域时钟, 开启 RTC 后备寄存器写访问
要访问 RTC 和 RTC 备份区域就必须先使能电源及后备域时钟, 然后使能RTC 后备区域访问。 电源时钟使能, 通过 RCC_APB1ENR 寄存器来设置; RTC 及RTC 备份寄存器的写访问, 通过 PWR_CR 寄存器的 DBP 位设置。 调用库函数为:
cpp
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//打开电源时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);//打开 RTC 后备域时钟
PWR_BackupAccessCmd(ENABLE);//打开后备寄存器访问
(2) 复位备份区域, 开启外部低速振荡器
在取消备份区域写保护之后, 我们可以先对这个区域复位, 以清除前面的设置, 当然这个操作不要每次都执行, 因为备份区域的复位将导致之前存在的数据丢失, 所以要不要复位, 要视情况而定。 然后我们使能外部低速振荡器, 注意这里一般要先判断 RCC_BDCR 的 LSERDY 位来确定低速振荡器已经就绪了才开始下面的操作。 备份区域复位的库函数为:
cpp
BKP_DeInit(); //复位备份区域
开启外部低速振荡器的函数是:
cpp
RCC_LSEConfig(RCC_LSE_ON);//开启外部 32.768K RTC 时钟
(3) 选择 RTC 时钟, 并使能
选择 LSE 为 RTC 时钟源库函数是:
cpp
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择 LSE 作为 RTC 时钟
使能 RTC 时钟库函数是:
cpp
RCC_RTCCLKCmd(ENABLE);//使能 RTC 时钟
(4) 设置 RTC 的分频以及配置 RTC 时钟
在开启了 RTC 时钟之后, 我们要做的就是设置 RTC 时钟的分频数, 通过RTC_PRLH 和 RTC_PRLL 来设置, 然后等待 RTC 寄存器操作完成, 并同步之后,设置秒钟中断。 然后设置 RTC 的允许配置位( RTC_CRH 的 CNF 位) , 设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL 两个寄存器) 。
在进行 RTC 配置之前首先要打开允许配置位(CNF), 调用的库函数是:
cpp
RTC_EnterConfigMode();// 允许配置
在配置完成之后, 千万不要忘记更新配置同时退出配置模式, 调用的库函数是:
cpp
RTC_ExitConfigMode();//退出配置模式, 更新配置
设置 RTC 时钟分频数, 调用的库函数是:
cpp
void RTC_SetPrescaler(uint32_t PrescalerValue);
这个函数只有一个参数, 就是 RTC 时钟的分频数, 很好理解。
然后是设置秒中断允许, RTC 使能中断的函数是:
cpp
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
函数的第一个参数用来选择 RTC 的中断类型, 可通过库文件的头文件查看,第二个参数用于使能还是失能。 比如要使能 RTC 秒中断, 如下:
cpp
RTC_ITConfig(RTC_IT_SEC, ENABLE);
接下来便是设置时间了, 设置时间实际上就是设置 RTC 的计数值, 时间与计数值之间是需要换算的。 库函数中设置 RTC 计数值的方法是:
cpp
void RTC_SetCounter(uint32_t CounterValue);
(5) 更新配置, 设置 RTC 中断分组
在设置完时钟之后, 我们将配置更新同时退出配置模式, 这里还是通过RTC_CRH 的 CNF 来实现。 调用库函数的方法是:
cpp
RTC_ExitConfigMode();//退出配置模式, 更新配置
在退出配置模式更新配置之后我们在备份区域 BKP_DR1 中写入 0XA0A0 代表我们已经初始化过时钟了, 下次开机(或复位) 的时候, 先读取 BKP_DR1 的值, 然后判断是否是 0XA0A0 来决定是不是要配置。 接着我们配置 RTC 的秒钟中断, 并进行分组。
往备份区域写用户数据的函数是:
cpp
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
函数的第一个参数用来设置备份寄存器的标号, 这个在 rtc 库文件头文件内有定义, 第二个参数是我们往备份寄存器写入的数据。 比如我们向 BKP_DR1 中写入 0XA0A0。 函数如下:
cpp
BKP_WriteBackupRegister(BKP_DR1, 0XA0A0);
同样库函数还提供一个读取备份寄存器内容的函数, 如下:
cpp
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
函数参数作用和写备份寄存器是一样的功能, 这个很好理解。
使能中断后, 就需要设置 RTC 的中断优先级, 即调用 NVIC_Init 函数初始化,这个在前面很多章节中都介绍过, 这里不多说
(6) 编写 RTC 中断服务函数
前面步骤中我们配置好了 RTC 的秒中断, 所以我们还需要编写对应的中断服务函数。 RTC 中断服务函数名在 STM32F1 启动文件内可以查找到, RTC 中断函数名如下:
cpp
RTC_IRQHandler
因为 RTC 的中断类型有很多, 所以进入中断后, 我们需要在中断服务函数开头处通过读取 RTC 状态寄存器的值判断此次中断是哪种类型, 然后做出相应的控制。 库函数中用来读取 RTC 状态标志位的函数如下:
cpp
FlagStatus RTC_GetFlagStatus(uint32_t RTC_FLAG);
参数 RTC_FLAG 用来选择 RTC 状态标志, 参数选择如下:

秒中断标志参数为 RTC_IT_SEC。 在秒钟中断产生的时候, 读取当前的时间值。 在中断函数结束之前我们会清除下对应的中断标志。
清除 RTC 秒中断标志函数如下:
cpp
RTC_ClearITPendingBit(RTC_IT_SEC);
将以上几步全部配置好后, 我们就可以正常使用 RTC 中断来更新时间了。
34.3 硬件设计
本实验使用到硬件资源如下:
(1) DS0 指示灯
(2) 蜂鸣器
(3) 串口 1
(4) RTC
DS0 指示灯、 蜂鸣器、 串口 1 电路在前面章节都介绍过, 这里就不多说, 至于 RTC 它属于 STM32F1 芯片内部的资源, 只要通过软件配置好即可使用。 DS0 指示灯用来提示系统运行状态, 串口 1 将读取的 RTC 时间日期信息打印出来。
这里需要注意 RTC 不能断电, 否则时间数据将会丢失, 如果想让时间在断电后还可以继续走, 那么必须确保开发板上的纽扣电池有电。
34.4 软件设计
本章所要实现的功能是: 设置 RTC 时间日期初值, 在 RTC 秒中断内使用串口打印出 RTC 日期和时间, DS0 指示灯闪烁提示系统运行。 程序框架如下:
(1) 初始化 RTC, 设置 RTC 时间日期初值
(2) 开启 RTC 的秒中断, 编写 RTC 中断函数
(3) 在 RTC 中断内更新时间并打印输出
(4) 编写主函数
前面介绍 RTC 配置步骤时, 就已经讲解如何初始化 RTC。 下面我们打开"\4--实验程序\1--基础实验\26-RTC 实时时钟实验" 工程, 在 APP 工程组中可以看到添加了 rtc.c 文件(里面包含了 RTC 驱动程序) , 在 StdPeriph_Driver 工程组中添加了 stm32f10x_rtc.c、 stm32f10x_pwr.c 和 stm32f10x_bkp.c 库文件。 RTC操作的库函数都放在 stm32f10x_rtc.c、 stm32f10x_pwr.c 和 stm32f10x_bkp.c及 对 应 的 头 文 件 中 , 所 以 使 用 到 RTC 就 必 须 加 入 stm32f10x_rtc.c 、stm32f10x_pwr.c 和 stm32f10x_bkp.c 文件, 同时还要包含对应的头文件路径。
这里我们分析几个重要函数, 其他部分程序大家可以打开工程查看。
34.4.1 RTC 初始化函数
要使用 RTC, 我们必须先对它进行配置。 初始化代码如下:
cpp
static void RTC_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; //RTC全局中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级1位,从优先级3位
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //先占优先级0位,从优先级4位
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能该通道中断
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}
/*******************************************************************************
* 函 数 名 : RTC_Init
* 函数功能 : RTC初始化
* 输 入 : 无
* 输 出 : 0,初始化成功
1,LSE开启失败
*******************************************************************************/
u8 RTC_Init(void)
{
//检查是不是第一次配置时钟
u8 temp=0;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外设时钟
PWR_BackupAccessCmd(ENABLE); //使能后备寄存器访问
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA0A0) //从指定的后备寄存器中读出数据:读出了与写入的指定数据不相乎
{
BKP_DeInit(); //复位备份区域
RCC_LSEConfig(RCC_LSE_ON); //设置外部低速晶振(LSE),使用外设低速晶振
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250) //检查指定的RCC标志位设置与否,等待低速晶振就绪
{
temp++;
delay_ms(10);
}
if(temp>=250)return 1;//初始化时钟失败,晶振有问题
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置RTC时钟(RTCCLK),选择LSE作为RTC时钟
RCC_RTCCLKCmd(ENABLE); //使能RTC时钟
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
RTC_WaitForSynchro(); //等待RTC寄存器同步
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中断
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
RTC_EnterConfigMode();// 允许配置
RTC_SetPrescaler(32767); //设置RTC预分频的值
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
RTC_Set(2019,11,1,17,34,55); //设置时间
RTC_ExitConfigMode(); //退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0XA0A0); //向指定的后备寄存器中写入用户程序数据
}
else//系统继续计时
{
RTC_WaitForSynchro(); //等待最近一次对RTC寄存器的写操作完成
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中断
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
}
RTC_NVIC_Config();//RCT中断分组设置
RTC_Get();//更新时间
return 0; //ok
}
在 RTC_Init()函数中, 首先使能电源 PWR 和后备域时钟, 打开后备寄存器写访问, 因为 RTC 初始化和初值的设置只需执行一次即可, 第一次初始化 RTC,我们只需按照前面介绍的 RTC 配置步骤完成, 在设置 RTC 时间和日期初值时是通过 RTC_Set 函数完成, 其实里面还是调用 RTC_SetCounter 完成的, 这里单独写这个函数是为了方便设置 RTC 时间和日期。默认我们将 RTC 初值日期设置为 2019年 11 月 1 日, 初值时间设置为 17 点 34 分 55 秒。
设置好时间后我们调用函数 RTC_WriteBackupRegister 向 RTC 的 BKR 寄存器(地址 0) 写入标志字 0XA0A0, 用于标记时间已经被设置了。 这样, 再次发生复位的时候, 通过调用函数 RTC_ReadBackupRegister 判断 RTC 对应 BKR地址的值, 来决定是不是需要重新设置时间, 如果不需要设置, 则跳过时间设置,这样就不会重复设置时间, 使得我们设置的时间不会因复位或者断电而丢失。
写备份域寄存器函数 RTC_WriteBackupRegister 原形是:
cpp
void RTC_WriteBackupRegister(uint32_t RTC_BKP_DR, uint32_t Data);
参数 RTC_BKP_DR 可以选择是 RTC_BKP_DR1-RTC_BKP_DR42。
读取备份域寄存器函数 RTC_ReadBackupRegister 原形是:
cpp
uint32_t RTC_ReadBackupRegister(uint32_t RTC_BKP_DR);
参数 RTC_BKP_DR 可以选择是 RTC_BKP_DR1-RTC_BKP_DR42。
最后我们的 RTC 初始化函数 RTC_Init 带有一个返回值, 如果返回值为 1 表示 RTC 初始化失败, 否则成功。
34.4.2 RTC 设置日期时间函数
在 RTC 初始化函数内调用了一个 RTC_Set 函数设置日期和时间, 具体代码如下:
cpp
//判断是否是闰年函数
//月份 1 2 3 4 5 6 7 8 9 10 11 12
//闰年 31 29 31 30 31 30 31 31 30 31 30 31
//非闰年 31 28 31 30 31 30 31 31 30 31 30 31
//输入:年份
//输出:该年份是不是闰年.1,是.0,不是
u8 Is_Leap_Year(u16 year)
{
if(year%4==0) //必须能被4整除
{
if(year%100==0)
{
if(year%400==0)return 1;//如果以00结尾,还要能被400整除
else return 0;
}else return 1;
}else return 0;
}
//月份数据表
u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表
//平年的月份日期表
const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};
/*******************************************************************************
* 函 数 名 : RTC_Set
* 函数功能 : RTC设置日期时间函数(以1970年1月1日为基准,把输入的时钟转换为秒钟)
1970~2099年为合法年份
* 输 入 : syear:年 smon:月 sday:日
hour:时 min:分 sec:秒
* 输 出 : 0,成功
1,失败
*******************************************************************************/
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
u16 t;
u32 seccount=0;
if(syear<1970||syear>2099)return 1;
for(t=1970;t<syear;t++) //把所有年份的秒钟相加
{
if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
else seccount+=31536000; //平年的秒钟数
}
smon-=1;
for(t=0;t<smon;t++) //把前面月份的秒钟数相加
{
seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数
}
seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加
seccount+=(u32)hour*3600;//小时秒钟数
seccount+=(u32)min*60; //分钟秒钟数
seccount+=sec;//最后的秒钟加上去
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外设时钟
PWR_BackupAccessCmd(ENABLE); //使能RTC和后备寄存器访问
RTC_SetCounter(seccount); //设置RTC计数器的值
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
return 0;
}
该函数用于设置日期时间, 把我们输入的时间, 转换为以 1970 年 1 月 1日 0 时 0 分 0 秒当做起始时间的秒钟信号, 后续的计算都以这个时间为基准的, 由于 STM32 的秒钟计数器可以保存 136 年的秒钟数据, 这样我们可以计时到 2106 年。
rtc.c 文件内还有一个闹钟设置函数 RTC_Alarm_Set, 此函数与设置时间函数完全一样, 只是函数名不同而已。
34.4.3 获取 RTC 时间函数
我们设置好 RTC 的初始时间, 接着就需要获取 RTC 时间, 具体代码如下:
cpp
//得到当前的时间
//返回值:0,成功;其他:错误代码.
u8 RTC_Get(void)
{
static u16 daycnt=0;
u32 timecount=0;
u32 temp=0;
u16 temp1=0;
timecount=RTC_GetCounter();
temp=timecount/86400; //得到天数(秒钟数对应的)
if(daycnt!=temp)//超过一天了
{
daycnt=temp;
temp1=1970; //从1970年开始
while(temp>=365)
{
if(Is_Leap_Year(temp1))//是闰年
{
if(temp>=366)temp-=366;//闰年的秒钟数
else {temp1++;break;}
}
else temp-=365; //平年
temp1++;
}
calendar.w_year=temp1;//得到年份
temp1=0;
while(temp>=28)//超过了一个月
{
if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2月份
{
if(temp>=29)temp-=29;//闰年的秒钟数
else break;
}
else
{
if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
else break;
}
temp1++;
}
calendar.w_month=temp1+1; //得到月份
calendar.w_date=temp+1; //得到日期
}
temp=timecount%86400; //得到秒钟数
calendar.hour=temp/3600; //小时
calendar.min=(temp%3600)/60; //分钟
calendar.sec=(temp%3600)%60; //秒钟
calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);//获取星期
return 0;
}
//获得现在是星期几
//功能描述:输入公历日期得到星期(只允许1901-2099年)
//输入参数:公历年月日
//返回值:星期号
u8 RTC_Get_Week(u16 year,u8 month,u8 day)
{
u16 temp2;
u8 yearH,yearL;
yearH=year/100; yearL=year%100;
// 如果为21世纪,年份数加100
if (yearH>19)yearL+=100;
// 所过闰年数只算1900年之后的
temp2=yearL+yearL/4;
temp2=temp2%7;
temp2=temp2+day+table_week[month-1];
if (yearL%4==0&&month<3)temp2--;
return(temp2%7);
}
该函数其实就是将存储在秒钟寄存器 RTC->CNTH 和 RTC->CNTL 中的秒钟数据(通过函数 RTC_SetCounter 设置)转换为真正的时间和日期。 该代码还用到了一个 calendar 的结构体, calendar 是我们在 rtc.h 里面将要定义的一个时间结构体, 用来存放时钟的年月日时分秒等信息。 因为 STM32 的 RTC 只有秒钟计数器, 而年月日时分秒这些需要我们自己软件计算。 我们把计算好的值保存在calendar 里面, 方便其他程序调用。
34.4.4 RTC 中断服务函数
在 RTC 初始化的时候使能的是秒中断, 所以需要编写一个秒中断函数, 在中断函数中需要更新 RTC 时间并打印输出。 代码如下:
cpp
//RTC时钟中断
//每秒触发一次
//extern u16 tcnt;
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒钟中断
{
RTC_Get();//更新时间
printf("RTC Time:%d-%d-%d %d:%d:%d\r\n",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间
}
if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//闹钟中断
{
RTC_ClearITPendingBit(RTC_IT_ALR); //清闹钟中断
RTC_Get(); //更新时间
printf("Alarm Time:%d-%d-%d %d:%d:%d\r\n",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间
}
RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清闹钟中断
RTC_WaitForLastTask();
}
此函数很简单, 首先判断中断类型, 然后获取 RTC 时间并打印输出。
34.4.5 主函数
编写好 RTC 初始化、 时间设置获取函数及相应的中断函数后, 接下来就可以编写主函数了, 代码如下:
cpp
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "rtc.h"
/*******************************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
int main()
{
u8 i=0;
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组 分2组
LED_Init();
USART1_Init(115200);
RTC_Init();
while(1)
{
i++;
if(i%20==0)
{
LED1=!LED1;
}
delay_ms(10);
}
}
主函数实现的功能很简单, 首先调用之前编写好的硬件初始化函数, 包括SysTick 系统时钟, 中断分组, LED 初始化等。 然后调用我们前面编写的 RTC_Init函数, 初始化 RTC, 如果是第一次初始化 RTC 会进入 RTC 初值设置部分, 只要保证 RTC 后备域有电, 就不会重新给 RTC 赋初值。 每经过一秒就会触发 RTC 秒中断,获取 RTC 时间并打印输出。 同时 DS0 指示灯间隔 200ms 闪烁。
34.5 实验现象
将工程程序编译后下载到开发板内, 可以看到 DS0 指示灯不断闪烁, 表示程序正常运行。 每过 1 秒进入 RTC 秒中断, 串口打印输出 RTC 时间和日期。 如果想在串口调试助手上看到输出信息, 可以打开"\5--开发工具\4-常用辅助开发软件\串口调试助手\串口调试助手(丁丁) " 内串口调试助手, 实验现象如下: (前提一定要连接好线路, USB 线一端连接电脑, 另一端连接开发板"USB 转串口模块" 上的 USB 下载口, 并且在"USB 转 TTL&电源" 模块上 P4 端子短接片已插上)

实验说明: 如果需要重新修改 RTC 初值时间, 可以在 RTC 初始化函数RTC_Init 的 if(HAL_RTCEx_BKUPRead(&RTC_Handler,RTC_BKP_DR0)!=0XA0A0)语句中的"!" 改成"=" , 这样就可以进入 if 内的初始化语句, 从而修改初值时间, 修改完后要记得把符号改回来, 否则下次复位又得重新设置初值。
课后作业
(1) 增加按键调节时间、 闹钟设置等功能。 (温馨提示: 在本实验基础上增加按键控制程序, 通过按键改变变量值传递至时间设置及闹钟设置函数)