正点原子mini板原理图_「正点原子STM32Mini板资料连载」第十八章 RTC 实时时钟实验...

1)实验平台:正点原子STM32mini 开发板
2)摘自《正点原子STM32 不完全手册(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

0f4370141caae48abe1a759714a329e7.png

第十八章 RTC 实时时钟实验

前面我们介绍了两款液晶模块,这一章我们将介绍 STM32 的内部实时时钟(RTC)。在本

章中,我们将使用 ALIENTEK 2.8 寸 TFTLCD 模块来显示日期和时间,实现一个简单的时钟。

另外,本章将顺带向大家介绍 BKP 的使用。本章分为如下几个部分:

18.1 STM32 RTC 时钟简介

18.2 硬件设计

18.3 软件设计

18.4 下载验证

18.1 STM32 RTC 时钟简介

STM32 的实时时钟(RTC)是一个独立的定时器。STM32 的 RTC 模块拥有一组连续计数

的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当

前的时间和日期。

RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式

唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC,

以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP)

写保护。

RTC 的简化框图,如图 18.1.1 所示:

c2ecd081d3b94948aa44beaea23daff2.png

图 18.1.1 RTC 框图

RTC 由两个主要部分组成(参见图 18.1.1),第一部分(APB1 接口)用来和 APB1 总线相连。

此单元还包含一组 16 位寄存器,可通过 APB1 总线对其进行读写操作。APB1 接口由 APB1 总

线时钟驱动,用来与 APB1 总线连接。

另一部分(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。

接下来,我们介绍一下 RTC 相关的几个寄存器。首先要介绍的是 RTC 的控制寄存器,RTC

总共有 2 个控制寄存器 RTC_CRH 和 RTC_CRL,两个都是 16 位的。RTC_CRH 的各位描如图

18.1.2 所示:

632e900c82711c81b38573e7530f4e69.png

图 18.1.2 RTC_CRH 寄存器各位描述

该寄存器用来控制中断的,我们本章将要用到秒钟中断,所以在该寄存器必须设置最低位

为 1,以允许秒钟中断。我们再看看 RTC_CRL 寄存器。该寄存器各位描述如图 18.1.3 所示:

cd51a83e211829d097b46cf3c79d3ba7.png

图 18.1.3 RTC_CRL 寄存器各位描述

本章我们用到的是该寄存器的 0、3~5 这几个位,第 0 位是秒钟标志位,我们在进入闹钟

中断的时候,通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零(写

0)。第 3 位为寄存器同步标志位,我们在修改控制寄存器 RTC_CRH/CRL 之前,必须先判断该

位,是否已经同步了,如果没有则等待同步,在没同步的情况下修改 RTC_CRH/CRL 的值是不

行的。第 4 位为配置标位,在软件修改 RTC_CNT/RTC_ALR/RTC_PRL 的值的时候,必须先软

件置位该位,以允许进入配置模式。第 5 位为 RTC 操作位,该位由硬件操作,软件只读。通过

该位可以判断上次对 RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才

能开始下一次操作。

第二个要介绍的寄存器是 RTC 预分频装载寄存器,也有 2 个寄存器组成,RTC_PRLH 和

RTC_PRLL。这两个寄存器用来配置 RTC 时钟的分频数的,比如我们使用外部 32.768K 的晶振

作为时钟的输入频率,那么我们要设置这两个寄存器的值为 32767,以得到一秒钟的计数频率。

RTC_PRLH 的各位描述如图 18.1.4 所示:

c8b4a76fa269bac2ba3ed95af2fbf2f7.png

图 18.1.4 RTC_PRLH 寄存器各位描述

从图 18.1.4 可以看出,RTC_PRLH 只有低四位有效,用来存储 PRL 的 19~16 位。而 PRL

的前 16 位,存放在 RTC_PRLL 里面,寄存器 RTC_PRLL 的各位描述如图 18.1.5 所示:

c6d0c804fe8ecbd076cafe5d6206e3bb.png

图 18.1.5 RTC_PRLL 寄存器各位描述

在介绍完这两个寄存器之后,我们介绍 RTC 预分频器余数寄存器,该寄存器也有 2 个寄存

器组成 RTC_DIVH 和 RTC_DIVL,这两个寄存器的作用就是用来获得比秒钟更为准确的时钟,

比如可以得到 0.1 秒,或者 0.01 秒等。该寄存器的值自减的,用于保存还需要多少时钟周期获

得一个秒信号。在一次秒钟更新后,由硬件重新装载。这两个寄存器和 RTC 预分频装载寄存器

的各位是一样的,这里我们就不列出来了。

接着要介绍的是 RTC 最重要的寄存器,RTC 计数器寄存器 RTC_CNT。该寄存器由 2 个 16

位的寄存器组成 RTC_CNTH 和 RTC_CNTL,总共 32 位,用来记录秒钟值(一般情况下)。此

两个计数器也比较简单,我们也不多说了。注意一点,在修改这个寄存器的时候要先进入配置

模式。

最后我们介绍 RTC 部分的最后一个寄存器,RTC 闹钟寄存器,该寄存器也是由 2 个 16 为

的寄存器组成 RTC_ALRH 和 RTC_ALRL。总共也是 32 位,用来标记闹钟产生的时间(以秒为

单位),如果 RTC_CNT 的值与 RTC_ALR 的值相等,并使能了中断的话,会产生一个闹钟中断。

该寄存器的修改也要进入配置模式才能进行。

因为我们使用到备份寄存器来存储 RTC 的相关信息(我们这里主要用来标记时钟是否已经

经过了配置),我们这里顺便介绍一下 STM32 的备份寄存器。

备份寄存器是 42 个 16 位的寄存器(Mini 开发板就是大容量的),可用来存储 84 个字节的

用户应用程序数据。他们处在备份域里,当 VDD 电源被切断,他们仍然由 VBAT 维持供电。

即使系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。

此外,BKP 控制寄存器用来管理侵入检测和 RTC 校准功能,这里我们不作介绍。

复位后,对备份寄存器和 RTC 的访问被禁止,并且备份域被保护以防止可能存在的意外的

写操作。执行以下操作可以使能对备份寄存器和 RTC 的访问:

1)通过设置寄存器 RCC_APB1ENR 的 PWREN 和 BKPEN 位来打开电源和后备接口的时

2)电源控制寄存器(PWR_CR)的 DBP 位来使能对后备寄存器和 RTC 的访问。

我们一般用 BKP 来存储 RTC 的校验值或者记录一些重要的数据,相当于一个 EEPROM,

不过这个 EEPROM 并不是真正的 EEPROM,而是需要电池来维持它的数据。关于 BKP 的详细

介绍请看《STM32 参考手册》的第 47 页,5.1 一节。

最后,我们还要介绍一下备份区域控制寄存器RCC_BDCR。该寄存器的个位描述如图18.1.6

所示:

03612a38830edc437857ddcdfb086657.png

图 18.1.6 RCC_ BDCR 寄存器各位描述

RTC 的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在 RTC 操作之前

先要通过这个寄存器选择 RTC 的时钟源,然后才能开始其他的操作。

寄存器介绍就给大家介绍到这里了,我们下面来看看要经过哪几个步骤的配置才能使 RTC

正常工作。RTC 正常工作的一般配置步骤如下:接下来我们来看看通过 HAL 库配置 RTC 一般

配置步骤。RTC 相关的 HAL 库文件为 stm32f1xx_hal_rtc.c 以及头文件 stm32f1xx_hal_rtc.h 中:

1)使能电源时钟和备份区域时钟。

前面已经介绍了,我们要访问 RTC 和备份区域就必须先使能电源时钟和备份区域时钟。这

个通过 RCC_APB1ENR 寄存器来设置。RTC 及 RTC 备份寄存器的写访问,通过 PWR_CR 寄存

器的 DBP 位设置。HAL 库设置方法为:

__HAL_RCC_PWR_CLK_ENABLE();//使能电源时钟 PWR

HAL_PWR_EnableBkUpAccess();//取消备份区域写保护

2)开启外部低速振荡器 LSE,选择 RTC 时钟,并使能。

配置开启 LSE 的函数为 HAL_RCC_OscConfig,使用方法为:

RCC_OscInitStruct.OscillatorType=RCC_OSCILLATORTYPE_LSE;//LSE 配置

RCC_OscInitStruct.PLL.PLLState=RCC_PLL_NONE;

RCC_OscInitStruct.LSEState=RCC_LSE_ON; //RTC 使用 LSE

HAL_RCC_OscConfig(&RCC_OscInitStruct);

选择 RTC 时钟源为函数为 HAL_RCCEx_PeriphCLKConfig,使用方法为:

PeriphClkInitStruct.PeriphClockSelection=RCC_PERIPHCLK_RTC;//外设为 RTC

PeriphClkInitStruct.RTCClockSelection=RCC_RTCCLKSOURCE_LSE;//RTC 时钟源为 LSE

HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct);

使能 RTC 时钟方法为:

__HAL_RCC_RTC_ENABLE();//RTC 时钟使能

3)初始化 RTC,设置 RTC 的分频,以及配置 RTC 参数。

在 HAL 中,初始化 RTC 是通过函数 HAL_RTC_Init 实现的,该函数声明为:

HAL_StatusTypeDef HAL_RTC_Init(RTC_HandleTypeDef *hrtc);

同样按照以前的方式,我们来看看 RTC 初始化参数结构体 RTC_HandleTypeDef 定义:

typedef struct

{

RTC_TypeDef *Instance;

RTC_InitTypeDef

Init;

RTC_DateTypeDef DateToUpdate;

HAL_LockTypeDef

Lock;

__IO HAL_RTCStateTypeDef

State;

}RTC_HandleTypeDef;

这里我们着重讲解成员变量 Init 含义,因为它是真正的 RTC 初始化变量,它是

RTC_InitTypeDef 结构体类型,结构体 RTC_InitTypeDef 定义为:

typedef struct

{

uint32_t AsynchPrediv;

uint32_t OutPut;

}RTC_InitTypeDef;

AsynchPrediv 用来设置 RTC 的异步预分频系数,也就是设置 RTC_PRER 寄存器的

PREDIV_A 相关位,因为异步预分频系数是 7 位,所以最大值为 0x7F,不能超过这个值。

OutPut 用来选择要连接到 RTC_ALARM 输出的标志,取值为:RTC_OUTPUT_DISABLE

(禁止输出),RTC_OUTPUT_ALARMA(使能闹钟 A 输出),RTC_OUTPUT_ALARMB(使能

闹钟 B 输出)和 RTC_OUTPUT_WAKEUP(使能唤醒输出)。

接下来我们看看 RTC 初始化的一般格式:

RTC_Handler.Instance=RTC;

RTC_Handler.Init.AsynchPrediv=32767;

HAL_RTC_Init(&RTC_Handler);

同样,HAL 库也提供了 RTC 初始化 MSP 函数。函数声明为:

void HAL_RTC_MspInit(RTC_HandleTypeDef* hrtc);

该函数内部一般存放时钟使能,时钟源选择等操作程序。

4)设置 RTC 的时间。

HAL 库中,设置 RTC 时间的函数为:

HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc,

RTC_TimeTypeDef *sTime, uint32_t Format);

实际上,根据我们前面寄存器的讲解,RTC_SetTime 函数是用来设置时间寄存器 RTC_TR

的相关位的值。

RTC_SetTime 函数的第三个参数 Format,用来设置输入的时间格式为 BIN 格式还是 BCD 格

式,可选值为 RTC_FORMAT_BIN 和 RTC_FORMAT_BCD。

我们接下来看看第二个初始化参数结构体 RTC_TimeTypeDef 的定义:

typedef struct

{

uint8_t Hours;

uint8_t Minutes;

uint8_t Seconds;

}RTC_TimeTypeDef;

这三个成员变量就比较好理解了,分别用来设置 RTC 时间参数的小时,分钟,秒钟,大家

参考前面讲解的 RTC_TR 的位描述即可。HAL_RTC_SetTime 函数参考实例如下:

RTC_TimeTypeDef RTC_TimeStructure;

RTC_TimeStructure.Hours=hour;

RTC_TimeStructure.Minutes=min;

RTC_TimeStructure.Seconds=sec;

HAL_RTC_SetTime(&RTC_Handler,&RTC_TimeStructure,RTC_FORMAT_BIN);

5)设置 RTC 的日期。

设置 RTC 的日期函数为:

HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc,

RTC_DateTypeDef *sDate, uint32_t Format);

实际上,根据我们前面寄存器的讲解,HAL_RTC_SetDate 设置日期函数是用来设置日期寄

存器 RTC_DR 的相关位的值。

该 函 数 有 三 个 入 口 参 数 , 我 们 着 重 讲 解 第 二 个 入 口 参 数 sData , 它 是 结 构 体

RTC_DateTypeDef 指针类型变量,结构体 RTC_DateTypeDef 定义如下:

typedef struct

{

uint8_t WeekDay;

//星期几

uint8_t Month;

//月份

uint8_t Date;

//日期

uint8_t Year;

//年份

}RTC_DateTypeDef;

结构体一共四个成员变量,这四个成员变量非常好理解,对应的是 RTC_DR 寄存器相关设

置位,这里我们就不做过多讲解。

6) 获取 RTC 当前日期和时间。

获取当前 RTC 时间的函数为:

HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc,

RTC_TimeTypeDef *sTime, uint32_t Format);

获取当前 RTC 日期的函数为:

HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc,

RTC_DateTypeDef *sDate, uint32_t Format);

这两个函数非常简单,实际就是读取RTC_TR寄存器和RTC_DR寄存器的时间和日期的值,

然后将值存放到相应的结构体中。

通过以上 6 个步骤,我们就完成了对 RTC 的配置,RTC 即可正常工作,而且这些操作不

是每次上电都必须执行的,可以视情况而定。当然,我们还需要设置时间、日期、唤醒中断、

闹钟等,这些将在后面介绍。

18.2 硬件设计

本实验用到的硬件资源有:

1) 指示灯 DS0

2) 串口

3) TFTLCD 模块

4) RTC

前面 3 个都介绍过了,而 RTC 属于 STM32 内部资源,其配置也是通过软件设置好就可以

了。不过 RTC 不能断电,否则数据就丢失了,我们如果想让时间在断电后还可以继续走,那么

必须确保开发板的电池有电(ALIENTEK MiniSTM32 开发板标配是有电池的)。

18.3 软件设计

打开上一章的工程,首先在 HARDWARE 文件夹下新建一个 RTC 的文件夹。然后打开 USER

文件夹下的工程,新建一个 rtc.c 的文件和 rtc.h 的头文件,保存在 RTC 文件夹下,并将 RTC 文

件夹加入头文件包含路径。

由于篇幅所限,rtc.c 中的代码,我们不全部贴出了,这里针对几个重要的函数,进行简要

说明,首先是 RTC_Init,其代码如下:

//实时时钟配置

//初始化 RTC 时钟,同时检测时钟是否工作正常

//BKP->DR1 用于保存是否第一次配置的设置

//返回 0:正常

//其他:错误代码

u8 RTC_Init(void)

{

RTC_Handler.Instance=RTC;

RTC_Handler.Init.AsynchPrediv=32767;

//时钟周期设置(有待观察,看是否跑慢了?)理论值:32767

if(HAL_RTC_Init(&RTC_Handler)!=HAL_OK) return 1;

if(HAL_RTCEx_BKUPRead(&RTC_Handler,RTC_BKP_DR1)!=0X5050)

//是否第一次配置

{

RTC_Set(2019,11,26,18,8,0);//设置日期和时间,2019 年 11 月 27 日,18 点 8 分 0 秒

HAL_RTCEx_BKUPWrite(&RTC_Handler,RTC_BKP_DR1,0X5050);

//标记已经初始化过了

printf("FIRST TIME");

}

__HAL_RTC_ALARM_ENABLE_IT(&RTC_Handler,RTC_IT_SEC); //允许秒中断

__HAL_RTC_ALARM_ENABLE_IT(&RTC_Handler,RTC_IT_ALRA); //允许闹钟中断

HAL_NVIC_SetPriority(RTC_IRQn,0x01,0x02);

//抢占优先级 1,子优先级 2

HAL_NVIC_EnableIRQ(RTC_IRQn);

RTC_Get();//更新时间

return 0; //ok

}

该函数用来初始化 RTC 时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位

都不会再进行时间设置了(前提是备份电池有电),在第一次配置的时候,我们是按照上面介绍

的 RTC 初始化步骤来做的,这里就不在多说了。这里设置时间和日期,是通过 RTC_Set 函数

来实现的,该函数将在后续进行介绍。这里默认将时间设置为 2019 年 11 月 27 日,18 点 08 分

0 秒。在设置好时间之后,我们向 RTC 的 BKR 寄存器(地址 0)写入标志字 0X5050,用于标

记时间已经被设置了。这样,再次发生复位的时候,该函数通过判断 RTC 对应 BKR 的值,来

决定是不是需要重新设置时间,如果不需要设置,则跳过时间设置,这样不会重复设置时间,

使得我们设置的时间不会因复位或者断电而丢失。

该函数还有返回值,返回值代表此次操作的成功与否,如果返回 0,则代表初始化 RTC 成

功,如果返回值非零则代表错误代码了。

这里我们来看看读备份区域和写备份区域寄存器的两个函数为:

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_Init,我们来介绍一下 RTC_Set 函数,代码如下:

//设置时钟

//把输入的时钟转换为秒钟

//以 1970 年 1 月 1 日为基准

//1970~2099 年为合法年份

//返回值: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};

//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

//把所有年份的秒钟相加

{

if(Is_Leap_Year(t))seccount+=31622400; //闰年的秒钟数

else seccount+=31536000;

//平年的秒钟数

}

smon-=1;

for(t=0;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->APB1ENR|=1<<28;

//使能电源时钟

RCC->APB1ENR|=1<<27;

//使能备份时钟

PWR->CR|=1<<8;

//取消备份区写保护

//上面三步是必须的!

RTC->CRL|=1<<4;

//允许配置

RTC->CNTL=seccount&0xffff;

RTC->CNTH=seccount>>16;

RTC->CRL&=~(1<<4);

//配置更新

while(!(RTC->CRL&(1<<5)));

//等待 RTC 寄存器操作完成

RTC_Get();

//设置完之后更新一下数据

return 0;

}

该函数用于设置时间,把我们输入的时间,转换为以 1970 年 1 月 1 日 0 时 0 分 0 秒当做起

始时间的秒钟信号,后续的计算都以这个时间为基准的,由于 STM32 的秒钟计数器可以保存

136 年的秒钟数据,这样我们可以计时到 2106 年。

接着,我们介绍 RTC_Alarm_Set 函数,该函数用于设置闹钟时间,同 RTC_Set 函数几乎一

模一样,主要区别,就是将:RTC->CNTL 和 RTC->CNTH 换成了 RTC->ALRL 和 RTC->ALRH,

用于设置闹钟时间,具体代码请参考本例程源码。

接着,我们介绍一下 RTC_Get 函数,该函数用于获取时间和日期等数据,其代码如下:

//得到当前的时间,结果保存在 calendar 结构体里面

//返回值:0,成功;其他:错误代码.

u8 RTC_Get(void)

{

static u16 daycnt=0;

u32 timecount=0;

u32 temp=0;

u16 temp1=0;

timecount=RTC->CNTH;

//得到计数器中的值(秒钟数)

timecount<<=16;

timecount+=RTC->CNTL;

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 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;

}

函数其实就是将存储在秒钟寄存器 RTC->CNTH 和 RTC->CNTL 中的秒钟数据转换为真正

的时间和日期。该代码还用到了一个 calendar 的结构体,calendar 是我们在 rtc.h 里面将要定义

的一个时间结构体,用来存放时钟的年月日时分秒等信息。因为 STM32 的 RTC 只有秒钟计数

器,而年月日,时分秒这些需要我们自己软件计算。我们把计算好的值保存在 calendar 里面,

方便其他程序调用。

最后,我们介绍一下秒钟中断服务函数,该函数代码如下:

//RTC 时钟中断

//每秒触发一次

void RTC_IRQHandler(void)

{

if(__HAL_RTC_ALARM_GET_FLAG(&RTC_Handler,RTC_FLAG_SEC)!=RESET)

//秒中断

{

__HAL_RTC_ALARM_CLEAR_FLAG(&RTC_Handler,RTC_FLAG_SEC);

//清除秒中断

RTC_Get();

//更新时间

LED1=!LED1;

//LED1 翻转

}

if(__HAL_RTC_ALARM_GET_FLAG(&RTC_Handler,RTC_FLAG_SEC)!=RESET)

//闹钟中断

{

__HAL_RTC_ALARM_CLEAR_FLAG(&RTC_Handler,RTC_FLAG_ALRAF);

//清除闹钟中断

RTC_Get();

//更新时间

printf("ALARM A!");

}

__HAL_RTC_ALARM_CLEAR_FLAG(&RTC_Handler,RTC_FLAG_OW);// 清 除 溢 出

}

此部分代码比较简单,主要做了 2 个处理,通过 RTC->CRL 的不同位来判断发生的是何种

中断,如果是秒钟中断,则执行一次时间的计算,获得最新时间,结果保存在 calendar 结构体

里面,因此,我们可以在 calendar 里面读到最新的时间、日期等信息。如果是闹钟中断,则更

新时间后,将当前的闹铃时间通过 printf 打印出来,可以在串口调试助手看到当前的闹铃情况。

我们看看 main 函数源码如下:

int main(void)

{

u8 t;

HAL_Init();

//初始化 HAL 库

Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M

delay_init(72);

//初始化延时函数

uart_init(115200);

//初始化串口

LED_Init();

//初始化 LED

LCD_Init();

//初始化 LCD FSMC 接口

usmart_dev.init(84);

//初始化 USMART

RTC_Init();

//初始化 RTC

POINT_COLOR=RED;

//设置字体为红色

LCD_ShowString(30,50,200,16,16,"Mini STM32");

LCD_ShowString(30,70,200,16,16,"RTC TEST");

LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");

LCD_ShowString(30,110,200,16,16,"2019/11/15");

while(RTC_Init())

//RTC 初始化 ,一定要初始化成功

{

LCD_ShowString(30,130,200,16,16,"RTC ERROR! ");

delay_ms(800);

LCD_ShowString(30,130,200,16,16,"RTC Trying...");

}

//显示时间

POINT_COLOR=BLUE;//设置字体为蓝色

LCD_ShowString(30,130,200,16,16," - - ");

LCD_ShowString(30,166,200,16,16," : : ");

while(1)

{

if(t!=calendar.sec)

{

t=calendar.sec;

LCD_ShowNum(30,130,calendar.w_year,4,16);

LCD_ShowNum(70,130,calendar.w_month,2,16);

LCD_ShowNum(94,130,calendar.w_date,2,16);

switch(calendar.week)

{

case 0:

LCD_ShowString(30,148,200,16,16,"Sunday ");

break;

case 1:

LCD_ShowString(30,148,200,16,16,"Monday ");

break;

case 2:

LCD_ShowString(30,148,200,16,16,"Tuesday ");

break;

case 3:

LCD_ShowString(30,148,200,16,16,"Wednesday");

break;

case 4:

LCD_ShowString(30,148,200,16,16,"Thursday ");

break;

case 5:

LCD_ShowString(30,148,200,16,16,"Friday ");

break;

case 6:

LCD_ShowString(30,148,200,16,16,"Saturday ");

break;

}

LCD_ShowNum(30,166,calendar.hour,2,16);

LCD_ShowNum(54,166,calendar.min,2,16);

LCD_ShowNum(78,166,calendar.sec,2,16);

LED0=!LED0;

}

delay_ms(10);

};

}

这部分代码,也比较简单,注意,我们通过

为了方便设置时间,我们在 usmart_config.c 里面,修改 usmart_nametab 如下:

struct _m_usmart_nametab usmart_nametab[]=

{

#if USMART_USE_WRFUNS==1 //如果使能了读写操作

(void*)read_addr,"u32 read_addr(u32 addr)",

(void*)write_addr,"void write_addr(u32 addr,u32 val)",

#endif

(void*)delay_ms,"void delay_ms(u16 nms)",

(void*)delay_us,"void delay_us(u32 nus)",

(void*)RTC_Set,"u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) ",

(void*)RTC_Alarm_Set,"u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,

u8 sec)",

};

将 RTC 的一些相关函数加入了 usmart,这样通过串口就可以直接设置 RTC 的时间和闹钟

了。

至此,RTC 实时时钟的软件设计就完成了,接下来就让我们来检验一下,我们的程序是否

正确了。

18.4 下载验证

将程序下载到 MiniSTM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时

可以看到 TFTLCD 模块开始显示时间,实际显示效果如图 18.4.1 所示:

bff5bf00a89c5e71e8fd3569205690f1.png

图 18.4.1 RTC 实验测试图

如果时间不正确,大家可以用上一章介绍的方法,通过串口调用 RTC_Set 来设置一下当前

时间,如图 18.4.2 所示:

63fed0c8c5dbadf2b836bb5d7ef59c00.png

图 18.4.2 通过 USMART 设置 RTC 时间

上图中,我们通过 usmart 设置时间为:2015 年 8 月 11 日,22 点 21 分 53 秒,执行完以后,

可以在 LCD 上面看到时间变成了我们所设置的时间。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值