首页 » 软件开发 » 「正点原子NANO STM32开发板资料连载」第十九章 RTC 时钟实验(寄存器设置时钟函数时间)

「正点原子NANO STM32开发板资料连载」第十九章 RTC 时钟实验(寄存器设置时钟函数时间)

乖囧猫 2024-07-23 18:59:44 软件开发 0

扫一扫用手机浏览

文章目录 [+]

第十九章 RTC 实时时钟实验

前面我们介绍了两款液晶模块,这一章我们将介绍 STM32F4 的内部实时时钟(RTC)。
在本章中,我们将使用数码管来显示日期和时间,实现一个简单的时钟,并可以设置闹铃。
另外,本章将顺带向大家介绍 BKP 的使用。
本章分为如下几个部分:

19.1 STM32F4 RTC 时钟简介

「正点原子NANO STM32开发板资料连载」第十九章 RTC 时钟实验(寄存器设置时钟函数时间) 「正点原子NANO STM32开发板资料连载」第十九章 RTC 时钟实验(寄存器设置时钟函数时间) 软件开发
(图片来自网络侵删)

19.2 硬件设计

19.3 软件设计

「正点原子NANO STM32开发板资料连载」第十九章 RTC 时钟实验(寄存器设置时钟函数时间) 「正点原子NANO STM32开发板资料连载」第十九章 RTC 时钟实验(寄存器设置时钟函数时间) 软件开发
(图片来自网络侵删)

19.4 下载验证

19.1 STM32F4 RTC 时钟简介

STM32F4 的实时时钟(RTC)相对于 STM32F1 来说,改进了不少,带了日历功能了,STM32F4 的 RTC,是一个独立的 BCD 定时器/计数器。
RTC 提供一个日历时钟(包含年月日时分秒信息)、两个可编程闹钟(ALARM A 和 ALARM B)中断,以及一个具有中断功能的周期性可编程唤醒标志。
RTC 还包含用于管理低功耗模 式的自动唤醒单元。
两个 32 位寄存器(TR 和 DR)包含二进码十进数格式 (BCD) 的秒、分钟、小时(12 或24 小时制)、星期、日期、月份和年份。
此外,还可提供二进制格式的亚秒值。

STM32F4 的 RTC 可以自动将月份的天数补偿为 28、29(闰年)、30 和 31 天。
并且还可以进行夏令时补偿。

RTC 模块和时钟配置是在后备区域,即在系统复位或从待机模式唤醒后 RTC 的设置和时间维持不变,只要后备区域供电正常,那么 RTC 将可以一直运行。
但是在系统复位后,会自动禁止访问后备寄存器和 RTC,以防止对后备区域(BKP)的意外写操作。
所以在要设置时间之前,先要取消备份区域(BKP)写保护。
RTC 的简化框图,如图 19.1.1 所示:

图 19.1.1 RTC 框图

本章我们用到 RTC 时钟和日历,并且用到闹钟功能。
接下来简单介绍下 STM32F4 RTC 时

钟的使用。

1,时钟和分频

首先,我们看 STM32F4 的 RTC 时钟分频。
STM32F4 的 RTC 时钟源(RTCCLK)通过时钟控

制器,可以从 LSE 时钟、LSI 时钟以及 HSE 时钟三者中选择(通过 RCC_BDCR 寄存器选择)。

一般我们选择 LSE,即外部 32.768Khz 晶振作为时钟源(RTCCLK),而 RTC 时钟核心,要求提

供 1Hz 的时钟,所以,我们要设置 RTC 的可编程预分配器。
STM32F4 的可编程预分配器

(RTC_PRER)分为 2 个部分:

1, 一个通过 RTC_PRER 寄存器的 PREDIV_A 位配置的 7 位异步预分频器。

2, 一个通过 RTC_PRER 寄存器的 PREDIV_S 位配置的 15 位同步预分频器。

图 19.1.1 中,ck_spre 的时钟可由如下计算公式计算:

Fck_spre=Frtcclk/[(PREDIV_S+1)( PREDIV_A+1)]

其中,Fck_spre 即可用于更新日历时间等信息。
PREDIV_A 和 PREDIV_S 为 RTC 的异步

和同步分频器。
且推荐设置 7 位异步预分频器(PREDIV_A)的值较大,以最大程度降低功耗。

要设置为 32768 分频,我们只需要设置:PREDIV_A=0X7F,即 128 分频;PREDIV_S=0XFF,

即 256 分频,即可得到 1Hz 的 Fck_spre。

另外,图 20.1.1 中,ck_apre 可作为 RTC 亚秒递减计数器(RTC_SSR)的时钟,Fck_apre

的计算公式如下:

Fck_apre=Frtcclk/( PREDIV_A+1)

当 RTC_SSR 寄存器递减到 0 的时候,会使用 PREDIV_S 的值重新装载 PREDIV_S。

PREDIV_S 一般为 255,这样,我们得到亚秒时间的精度是:1/256 秒,即 3.9ms 左右,有了这

个亚秒寄存器 RTC_SSR,就可以得到更加精确的时间数据。

2,日历时间(RTC_TR)和日期(RTC_DR)寄存器

STM32F4 的 RTC 日历时间(RTC_TR)和日期(RTC_DR)寄存器,用于存储时间和日期

(也可以用于设置时间和日期),可以通过与 PCLK1(APB1 时钟)同步的影子寄存器来访问,

这些时间和日期寄存器也可以直接访问,这样可避免等待同步的持续时间。

每隔 2 个 RTCCLK 周期,当前日历值便会复制到影子寄存器,并置位 RTC_ISR 寄存器的 RSF

位。
我们可以读取 RTC_TR 和 RTC_DR 来得到当前时间和日期信息,不过需要注意的是:时间

和日期都是以 BCD 码的格式存储的,读出来要转换一下,才可以得到十进制的数据。

3,可编程闹钟

STM32F4 提供两个可编程闹钟:闹钟 A(ALARM_A)和闹钟 B(ALARM_B)。
通过 RTC_CR

寄存器的 ALRAE 和 ALRBE 位置 1 来使能可编程闹钟功能。
当日历的亚秒、秒、分、小时、

日期分别与闹钟寄存器 RTC_ALRMASSR/RTC_ALRMAR 和 RTC_ALRMBSSR/RTC_ALRMBR

中的值匹配时,则可以产生闹钟(需要适当配置)。
本章我们将利用闹钟 A 产生闹铃,即设置

RTC_ALRMASSR 和 RTC_ALRMAR 即可。

4,周期性自动唤醒

STM32F4 的 RTC 不带秒钟中断了,但是多了一个周期性自动唤醒功能。
周期性唤醒功能,

由一个 16 位可编程自动重载递减计数器(RTC_WUTR)生成,可用于周期性中断/唤醒。

我们可以通过 RTC_CR 寄存器中的 WUTE 位设置使能此唤醒功能。

唤醒定时器的时钟输入可以是:2、4、8 或 16 分频的 RTC 时钟(RTCCLK),也可以是 ck_spre

时钟(一般为 1Hz)。

当选择 RTCCLK(假定 LSE 是:32.768 kHz)作为输入时钟时,可配置的唤醒中断周期介于

122us(因为 RTCCLK/2 时,RTC_WUTR 不能设置为 0)和 32 s 之间,分辨率最低为:61us。

当选择 ck_spre(1Hz)作为输入时钟时,可得到的唤醒时间为 1s 到 36h 左右,分辨率为 1

秒。
并且这个 1s~36h 的可编程时间范围分为两部分:

当 WUCKSEL[2:1]=10 时为:1s 到 18h。

当 WUCKSEL[2:1]=11 时约为:18h 到 36h。

在后一种情况下,会将 2^16 添加到 16 位计数器当前值(即扩展到 17 位,相当于最高位用

WUCKSEL [1]代替)。

初始化完成后,定时器开始递减计数。
在低功耗模式下使能唤醒功能时,递减计数保持有

效。
此外,当计数器计数到 0 时,RTC_ISR 寄存器的 WUTF 标志会置 1,并且唤醒寄存器会使

用其重载值(RTC_WUTR 寄存器值)动重载,之后必须用软件清零 WUTF 标志。

通过将 RTC_CR 寄存器中的 WUTIE 位置 1 来使能周期性唤醒中断时,可以使 STM32F4

退出低功耗模式。
系统复位以及低功耗模式(睡眠、停机和待机)对唤醒定时器没有任何影响,

它仍然可以正常工作,故唤醒定时器,可以用于周期性唤醒 STM32F4。

接下来,我们看看本章我们要用到的 RTC 部分寄存器,首先是 RTC 时间寄存器:RTC_TR,

该寄存器各位描述如图 19.1.2 所示:

图 19.1.2 RTC_TR 寄存器各位描述

这个寄存器比较简单,注意数据保存时 BCD 格式的,读取之后需要稍加转换,才是十进

制的时分秒等数据,在初始化模式下,对该寄存器进行写操作,可以设置时间。

然后看 RTC 日期寄存器:RTC_DR,该寄存器各位描述如图 19.1.3 所示:

图 19.1.3 RTC_DR 寄存器各位描述

同样,该寄存器的的数据采用 BCD 码格式(如不熟悉 BCD,百度即可),其他的就比较

简单了。
同样,在初始化模式下,对该寄存器进行写操作,可以设置日期。

接下来,看 RTC 亚秒寄存器:RTC_SSR,该寄存器各位描述如图:19.1.4 所示:

图 19.1.4 RTC_SSR 寄存器各位描述

该寄存器可用于获取更加精确的 RTC 时间。
不过,在本章没有用到,如果需要精确时间的

地方,大家可以使用该寄存器。

接下来看 RTC 控制寄存器:RTC_CR,该寄存器各位描述如图 19.1.5 所示:

图 19.1.5 RTC_CR 寄存器各位描述

该寄存器我们不详细介绍每个位了,重点介绍几个要用到的:WUTIE,ALRAIE 是唤醒定

时器中断和闹钟 A 中断使能位,本章要用到,设置为 1 即可。
WUTE 和 ALRAE,则是唤醒定

时器和闹钟 A 定时器使能位,同样设置为 1,开启。
FMT 为小时格式选择位,我们设置为 0,

选择 24 小时制。
最后 WUCKSEL[2:0],用于唤醒时钟选择,这个前面已经有介绍了,我们这

里就不多说了,RTC_CR 寄存器的详细介绍,请看《STM32F411xC/E 参考手册》第 17.6.3 节。

接下来看 RTC 初始化和状态寄存器:RTC_ISR,该寄存器各位描述如图 19.1.6 所示:

图 19.1.6 RTC_ISR 寄存器各位描述

该寄存器中,WUTF、ALRBF 和 ALRAF,分别是唤醒定时器闹钟 B 和闹钟 A 的中断标志

位,当对应事件产生时,这些标志位被置 1,如果设置了中断,则会进入中断服务函数,这些

位通过软件写 0 清除;

INIT 为初始化模式控制位,要初始化 RTC 时,必须先设置 INIT=1;

INITF为初始化标志位,当设置 INIT 为 1 以后,要等待 INITF 为 1,才可以更新时间、日期和预分频

寄存器等;RSF 位为寄存器同步标志,仅在该位为 1 时,表示日历影子寄存器已同步,可以正

确读取 RTC_TR/RTC_TR 寄存器的值了;WUTWF、ALRBWF 和 ALRAWF 分别是唤醒定时器、

闹钟 B 和闹钟 A 的写标志,只有在这些位为 1 的时候,才可以更新对应的内容,比如:要设置

闹钟 A 的 ALRMAR 和 ALRMASSR,则必须先等待 ALRAWF 为 1,才可以设置。

接下来看 RTC 预分频寄存器:RTC_PRER,该寄存器各位描述如图 19.1.7 所示:

图 19.1.7 RTC_PRER 寄存器各位描述

该寄存器用于 RTC 的分频,我们在之前也有讲过,这里就不多说了。
该寄存器的配置,必

须在初始化模式(INITF=1)下,才可以进行。

接下来看 RTC 唤醒定时器寄存器:RTC_WUTR,该寄存器各位描述如图 19.1.8 所示:

图 19.1.8 RTC_WUTR 寄存器各位描述

该寄存器用于设置自动唤醒重装载值,可用于设置唤醒周期。
该寄存器的配置,必须等待

RTC_ISR 的 WUTWF 为 1 才可以进行。

接下来看 RTC 闹钟 A 器寄存器:RTC_ALRMAR,该寄存器各位描述如图 19.1.9 所示:

图 19.1.9 RTC_ ALRMAR 寄存器各位描述

该寄存器用于设置闹铃 A,当 WDSEL 选择 1 时,使用星期制闹铃,本章我们选择星期制

闹铃。
该寄存器的配置,必须等待 RTC_ISR 的 ALRAWF 为 1 才可以进行。
另外,还有

RTC_ALRMASSR 寄存器,该寄存器我们这里就不再介绍了,大家参考《STM32F4xx 中文数据

手册》第 23.6.19 节。

接下来看 RTC 写保护寄存器:RTC_WPR,该寄存器比较简单,低八位有效。
上电后,所

有 RTC 寄存器都受到写保护(RTC_ISR[13:8]、RTC_TAFCR 和 RTC_BKPxR 除外),必须依

次写入:0XCA、0X53 两关键字到 RTC_WPR 寄存器,才可以解锁。
写一个错误的关键字将再

次激活 RTC 的寄存器写保护。

接下来,我们介绍下 RTC 备份寄存器:RTC_BKPxR,该寄存器组总共有 20 个,每个寄

存器是 32 位的,可以存储 80 个字节的用户数据,这些寄存器在备份域中实现,可在 VDD 电

源关闭时通过 VBAT 保持上电状态。
备份寄存器不会在系统复位或电源复位时复位,也不会在

MCU 从待机模式唤醒时复位。

复位后,对 RTC 和 RTC 备份寄存器的写访问被禁止,执行以下操作可以使能对 RTC 及RTC 备份寄存器的写访问:

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

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

我们可以用 BKP 来存储一些重要的数据,相当于一个 EEPROM,不过这个 EEPROM 并不

是真正的 EEPROM,而是需要电池来维持它的数据。

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

所示:

图 19.1.10 RCC_ BDCR 寄存器各位描述

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

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

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

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

文件为 stm32f4xx_hal_rtc.c 以及头文件 stm32f4xx_hal_rtc.h 中:

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

前面已经介绍了,我们要访问 RTC 和备份区域就必须先使能电源时钟,然后使能 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;

HAL_LockTypeDef

Lock;

__IO HAL_RTCStateTypeDef

State;

}RTC_HandleTypeDef;

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

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

typedef struct

{

uint32_t HourFormat;

//小时格式

uint32_t AsynchPrediv;

//异步预分频系数

uint32_t SynchPrediv;

//同步预分频系数

uint32_t OutPut;

//选择连接到 RTC_ALARM 输出的标志

uint32_t OutPutPolarity; //设置 RTC_ALARM 的输出极性

uint32_t OutPutType;

//设置 RTC_ALARM 的输出类型为开漏输出还是推挽输出

}RTC_InitTypeDef;

该结构体有 6 个成员变量。

成 员 变 量 HourFormat 用 来 设 置小 时 格 式 , 为 12 小 时 制 或者 24 小 时 制 ,取 值 为

RTC_HOURFORMAT_12 或者 RTC_HOURFORMAT_24。

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

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

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

相关位,因为同步预分频系数也是 15 位,所以最大值为 0x7FFF,不能超过这个值。

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

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

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

OutPutPolarity 用来设置 RTC_ALARM 的输出极性,与 Output 成员变量配合使用,取值为

RTC_OUTPUT_POLARITY_HIGH(高电平)或 RTC_OUTPUT_POLARITY_LOW(低电平)。

OutPutType 用 来 设 置 RTC_ALARM 的 输 出 类 型 为 开 漏 输 出

(RTC_OUTPUT_TYPE_OPENDRAIN)还是推挽输出(RTC_OUTPUT_TYPE_PUSHPULL),

与成员变量 OutPut 和 OutPutPolarity 配合使用。

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

RTC_Handler.Instance=RTC;

RTC_Handler.Init.HourFormat=RTC_HOURFORMAT_24;//RTC 设置为 24 小时格式

RTC_Handler.Init.AsynchPrediv=0X7F;

//RTC 异步分频系数(1~0X7F)

RTC_Handler.Init.SynchPrediv=0XFF;

//RTC 同步分频系数(0~7FFF)

RTC_Handler.Init.OutPut=RTC_OUTPUT_DISABLE;

RTC_Handler.Init.OutPutPolarity=RTC_OUTPUT_POLARITY_HIGH;

RTC_Handler.Init.OutPutType=RTC_OUTPUT_TYPE_OPENDRAIN;

HAL_RTC_Init(&RTC_Handler);

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

void HAL_RTC_MspInit(RTC_HandleTypeDef hrtc);

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

4) 设置 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;

uint8_t TimeFormat;

uint32_t SubSeconds;

uint32_t SecondFraction;

uint32_t DayLightSaving;

uint32_t StoreOperation;

}RTC_TimeTypeDef;

前面四个成员变量比较好理解了,分别用来设置 RTC 时间参数的小时,分钟,秒钟,以及

AM/PM 符号,大家参考前面讲解的 RTC_TR 的位描述即可。
SubSeconds 用来读取保存亚秒寄

存器 RTC_SSE 的值,SecondFraction 用来读取保存同步预分频系数的值,也就是 RTC_PRER

的位 0~14,DayLightSaving 用来设置日历时间增加 1 小时,减少 1 小时,还是不变。
StoreOperation

用户对此变量设置以记录是否已对夏令时进行更改。
HAL_RTC_SetTime 函数参考实例如下:

RTC_TimeTypeDef RTC_TimeStructure;

RTC_TimeStructure.Hours=1;

RTC_TimeStructure.Minutes=1;

RTC_TimeStructure.Seconds=1;

RTC_TimeStructure.TimeFormat= RTC_HOURFORMAT12_PM;

RTC_TimeStructure.DayLightSaving=RTC_DAYLIGHTSAVING_NONE;

RTC_TimeStructure.StoreOperation=RTC_STOREOPERATION_RESET;

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 的相关位的值。

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

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 即可正常工作,而且这些操作不

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

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

19.2 硬件设计

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

1) 指示灯 DS0

2) 串口

3) 数码管

4) RTC

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

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

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

19.3 软件设计

同样,打开我们光盘的 RTC 时钟实验,可以看到,我们的工程中加入了 rtc.c 源文件和 rtc.h

头文件,同时,引入了 stm32f4xx_hal_rtc.c 和 stm32f4xx_hal_rtc_ex.c 库文件。

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

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

//RTC 初始化

//返回值:0,初始化成功;

//

2,进入初始化模式失败;

u8 RTC_Init(void)

{

RTC_Handler.Instance=RTC;

RTC_Handler.Init.HourFormat=RTC_HOURFORMAT_24;//RTC 设置为 24 小时格式

RTC_Handler.Init.AsynchPrediv=0X7F;

//RTC 异步分频系数(1~0X7F)

RTC_Handler.Init.SynchPrediv=0XFF;

//RTC 同步分频系数(0~7FFF)

RTC_Handler.Init.OutPut=RTC_OUTPUT_DISABLE;

RTC_Handler.Init.OutPutPolarity=RTC_OUTPUT_POLARITY_HIGH;

RTC_Handler.Init.OutPutType=RTC_OUTPUT_TYPE_OPENDRAIN;

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

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

//是否第一次配置

{

RTC_Set_Time(23,59,56,RTC_HOURFORMAT12_PM);

//设置时间 ,根据实际时间修改

RTC_Set_Date(15,12,27,7);

//设置日期

HAL_RTCEx_BKUPWrite(&RTC_Handler,RTC_BKP_DR0,0X5050);

//标记已经初始化过了

}

return 0;

}

该函数用来初始化 RTC 配置以及日期时钟,但是只在第一次的时候设置时间,以后如果重

新上电/复位都不会再进行时间设置了(前提是备份电池有电),在第一次配置的时候,我们是

按照上面介绍的 RTC 初始化步骤调用函数(HAL_RTC_Init)来实现的,这里这里就不在多说

了。

这里我们设置时间和日期,分别是通过 RTC_Set_Time 和 RTC_Set_Data 函数来实现的,这

两个函数实际就是调用库函数里面的 HAL_RTC_SetTime 函数和 HAL_RTC_SetData 函数来实

现,这里我们之所以要写两个这样的函数,目的是为了我们的 USMART 来调用,方便直接通

过 USMART 来设置时间和日期。

这里默认将时间设置为 15 年 12 月 27 日星期天,23 点 59 分 56 秒。
在设置好时间之后,

我们调用函数 HAL_RTCEx_BKUPWrite 向 RTC 的 BKP 寄存器(地址 0)写入标志字 0X5050,

用 于 标 记 时 间 已 经 被 设 置 了 。
这 样 , 再 次 发 生 复 位 的 时 候 , 该 函 数 通 过 调 用 函 数

HAL_RTCEx_BKUPRead 判断 RTC 对应 BKR 地址的值,来决定是不是需要重新设置时间,如

果不需要设置,则调过时间设置,这样不会重复设置时间,使得我们设置的时间不会因复位或

者断电而丢失。

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

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_AlarmA 函数,该函数代码如下:

//设置闹钟时间(按星期闹铃,24 小时制)

//week:星期几(1~7) @ref RTC_WeekDay_Definitions

//hour,min,sec:小时,分钟,秒钟

void RTC_Set_AlarmA(u8 week,u8 hour,u8 min,u8 sec)

{

RTC_AlarmTypeDef RTC_AlarmSturuct;

RTC_AlarmSturuct.AlarmTime.Hours=hour;

//小时

RTC_AlarmSturuct.AlarmTime.Minutes=min;

//分钟

RTC_AlarmSturuct.AlarmTime.Seconds=sec;

//秒

RTC_AlarmSturuct.AlarmTime.SubSeconds=0;

RTC_AlarmSturuct.AlarmTime.TimeFormat=RTC_HOURFORMAT12_AM;

RTC_AlarmSturuct.AlarmMask=RTC_ALARMMASK_NONE;//精确匹配星期,时分秒

RTC_AlarmSturuct.AlarmSubSecondMask=RTC_ALARMSUBSECONDMASK_NONE;

RTC_AlarmSturuct.AlarmDateWeekDaySel=

RTC_ALARMDATEWEEKDAYSEL_WEEKDAY;//按星期

RTC_AlarmSturuct.AlarmDateWeekDay=week; //星期

RTC_AlarmSturuct.Alarm=RTC_ALARM_A; //闹钟 A

HAL_RTC_SetAlarm_IT(&RTC_Handler,&RTC_AlarmSturuct,RTC_FORMAT_BIN);

HAL_NVIC_SetPriority(RTC_Alarm_IRQn,0x01,0x02); //抢占优先级 1,子优先级 2

HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);

}

该函数用于设置闹钟 A,也就是设置 ALRMAR 和 ALRMASSR 寄存器的值,来设置闹钟

时间,这里 HAL 库中用来设置闹钟并开启中断的函数为:

HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef hrtc,

RTC_AlarmTypeDef sAlarm, uint32_t Format);

第三个参数 RTC_Format 用来设置格式,这里前面我们讲解过,就不做过多讲解。

接下来我们着重看看第二个参数 sAlarm,该入口参数是 RTC_AlarmTypeDef 结构体指针类

型,结构体定义如下:

typedef struct

{

RTC_TimeTypeDef AlarmTime;

uint32_t AlarmMask;

uint32_t AlarmSubSecondMask;

uint32_t AlarmDateWeekDaySel;

uint8_t AlarmDateWeekDay;

uint32_t Alarm;

}RTC_AlarmTypeDef;

该 结 构 体 有 6 个 成 员 变 量 , 第 一 个 成 员 变 量 AlarmTime 用 来 设 置 闹 钟 时 间 , 是

RTC_TimeTypeDef 结构体类型,该结构体前面我们已经讲解过各个成员变量含义,这里我们就

不做过多讲解。

AlarmMask 用来设置闹钟时间掩码,也就是在我们第一个参数设置的时间中(包括后面参

数 RTC_AlarmDatteWeekDay 设置的星期几/哪一天),那些是无关的。
比如我们设置闹钟时间

为每天的 10 点 10 分 10 秒,那么我们可以选择值 RTC_AlarmMask_DateWeekDay,也就是我们

不关系是星期几/每月哪一天。
这里我们选择为 RTC_AlarmMask_None,也就是精确匹配时间,

所有的时分秒以及星期几/(或者每月哪一天)都要精确匹配。

AlarmSubSecondMask 和 AlarmMask 作用类似,只不过该变量是用来设置亚秒的。

AlarmDateWeekDaySel 用 来 选 择 是 闹 钟 是 按 日 期 还 是 按 星 期 。
比 如 我 们 选 择

RTC_AlarmDateWeekDaySel_WeekDay 那 么 闹 钟 就 是 按 星 期 。
如 果 我 们 选 择

RTC_AlarmDataWeekDaySel_Date 那么闹钟就是按日期。
这与后面第四个参数是有关联的,我

们在后面第四个参数讲解。

AlarmDateWeekDay 用 来 设 置 闹 钟 的 日 期 或 者 星 期 几 , 比 如 我 们 第 三 个 参 数

RTC_AlarmWeekDaySel 设置了值为 RTC_AlarmDateWeekDaySel_WeekDay,也就是按星期,那

么 参 数 RTC_AlarmDateWeekDay 的 数 值 范 围 就 为 星 期 一 ~ 星 期 天 , 也 就 是

RTC_Weekday_Monday~RTC_Weekday_Sunday。
如果第三个参数 RTC_AlarmDateWeekDaySel

设置值为 RTC_AlarmDateWeekDaySel_Date,那么它的 取值范围就为日期值,0~31。

Alarm 用来设置闹钟 A 还是闹钟 B,这个很好理解。

调用函数 RTC_SetAlarm 设置闹钟 A 的参数之后,最后,开启闹钟 A 中断(连接在外部中

断线 17),并设置中断分组。
当 RTC 的时间和闹钟 A 设置的时间完全匹配时,将产生闹钟中

断。

接着,我们介绍一下 RTC_Set_WakeUp 函数,该函数代码如下:

void RTC_Set_WakeUp(u32 wksel,u16 cnt)

{

__HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&RTC_Handler,

RTC_FLAG_WUTF);//清除 RTC WAKE UP 的标志

HAL_RTCEx_SetWakeUpTimer_IT(&RTC_Handler,cnt,wksel); //设置重装载值和时钟

HAL_NVIC_SetPriority(RTC_WKUP_IRQn,0x02,0x02); //抢占优先级 1,子优先级 2

HAL_NVIC_EnableIRQ(RTC_WKUP_IRQn);

}

该函数用于设置 RTC 周期性唤醒定时器,实现周期性唤醒中断,连接外部中断线 22。

函数调用的是 HAL 库函数 HAL_RTCEx_SetWakeUpTimer_IT 实现的,该函数使用方法比较简

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

有了中断设置函数,就必定有中断服务函数,同时因为 HAL 库会开放中断处理回调函数,

接下来看这两个中断的中断服务函数和中断处理回调函数,代码如下:

//RTC 闹钟中断服务函数

void RTC_Alarm_IRQHandler(void)

{

HAL_RTC_AlarmIRQHandler(&RTC_Handler);

}

//RTC WAKE UP 中断服务函数

void RTC_WKUP_IRQHandler(void)

{

HAL_RTCEx_WakeUpTimerIRQHandler(&RTC_Handler);

}

//RTC 闹钟 A 中断处理回调函数

void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef hrtc)

{

printf("ALARM A!\r\n");

}

//RTC WAKE UP 中断处理回调函数

void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef hrtc)

{

LED1=!LED1;

}

其中,RTC_Alarm_IRQHandler 函数用于闹钟中断,其中断控制逻辑写在中断回调函数

HAL_RTC_AlarmAEventCallback 中 , 每 当 闹 钟 A 闹 铃 是 , 会 从 串 口 打 印 一 个 字 符 串

“ALARMA!
”。
RTC_WKUP_IRQHandler 函数用于 RTC 自动唤醒定时器中断,其中断控制

逻辑写在中断回调函数 HAL_RTCEx_WakeUpTimerEventCallback 中,可以通过观察 LED1 的状

态来查看 RTC 自动唤醒中断的情况。

rtc.c 的其他程序,这里就不再介绍了,请大家直接看光盘的源码。
rtc.h 头文件中主要是一

些函数声明,我们就不多说了,有些函数在这里没有介绍,请大家参考本例程源码。

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

// 共阴数字数组

// 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F, .,全灭

u8 smg_num[]={0xfc,0x60,0xda,0xf2,0x66,0xb6,0xbe,0xe0,0xfe,0xf6,

0xee,0x3e,0x9c,0x7a,0x9e,0x8e,0x01,0x00};

RTC_TimeTypeDef RTC_TimeStruct;

RTC_DateTypeDef RTC_DateStruct;

int main(void)

{

HAL_Init();

//初始化 HAL 库

Stm32_Clock_Init(96,4,2,4);

//设置时钟,96Mhz

delay_init(96);

//初始化延时函数

uart_init(115200);

//初始化串口 115200

LED_Init();

//初始化 LED

LED_SMG_Init();

//数码管初始化

TIM3_Init(20-1,9600-1);

//数码管 2ms 定时显示

usmart_dev.init(96);

//初始化 USMART

while(RTC_Init()) //RTC 初始化,一定要初始化成功

{

printf("RTC ERROR!\r\n");

delay_ms(800);

printf("RTC Trying...\r\n");

}

RTC_Set_WakeUp(RTC_WAKEUPCLOCK_CK_SPRE_16BITS,0);

//配置 WAKE UP 中断,1 秒钟中断一次

while(1)

{

}

}

u8 smg_wei=0;//数码管位选

u8 num=0;//数码管数值

u8 time=0;//时间值

//回调函数,定时器中断服务函数调用

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef htim)

{

if(htim==(&TIM3_Handler))

{

HAL_RTC_GetTime(&RTC_Handler,&RTC_TimeStruct,RTC_FORMAT_BIN);

HAL_RTC_GetDate(&RTC_Handler,&RTC_DateStruct,RTC_FORMAT_BIN);

switch(smg_wei)

{

case 0: num = smg_num[RTC_TimeStruct.Hours/10]; break;//时

case 1: num = smg_num[RTC_TimeStruct.Hours%10]; break;

case 2:

case 5: num = 0x02; break;

case 3: num = smg_num[RTC_TimeStruct.Minutes/10]; break; //分

case 4: num = smg_num[RTC_TimeStruct.Minutes%10]; break;

case 6: num = smg_num[RTC_TimeStruct.Seconds/10]; break; //秒

case 7: num = smg_num[RTC_TimeStruct.Seconds%10]; break;

}

if(time!=RTC_TimeStruct.Seconds)//LED0 每秒闪烁

{

time=RTC_TimeStruct.Seconds;

LED0=!LED0;

}

LED_Write_Data(num,smg_wei);//写数据到数码管

LED_Refresh();//更新显示

smg_wei++;

if(smg_wei==8) smg_wei=0;

}

}

这 main.c 文件中,在包含了 rtc.h 之后,先对用到的外设初始化,由于数码管是使用动态扫

描显示的,我们开启了定时器 3 以 2ms 中断周期动态刷新显示,在定时器 3 中断更新时间的显

示,同时我们设置了 LED0 每 1 秒钟闪烁一次,用来提示程序已经开始跑了。
由于数码管的显

示位数限制,这里我们只显示时分秒。

为了方便设置时间,我们在 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_Time,"u8 RTC_Set_Time(u8 hour,u8 min,u8 sec,u8 ampm)",

(void)RTC_Set_Date,"u8 RTC_Set_Date(u8 year,u8 month,u8 date,u8 week)",

(void)RTC_Set_AlarmA,"void RTC_Set_AlarmA(u8 week,u8 hour,u8 min,u8 sec)",

(void)RTC_Set_WakeUp,"void RTC_Set_WakeUp(u8 wksel,u16 cnt)",

};

将 RTC_Set 加入了 usmart,这样通过串口就可以直接设置 RTC 时间了。

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

正确了。

19.4 下载验证

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

同时可以看到数码管开始显示时间,实际显示效果如图 19.4.1 所示:

图 19.4.1 RTC 实验效果图

如果时间和日期不正确,可以利用上一章介绍的 usmart 工具,通过串口来设置,并且可以

设置闹钟时间等,如图 19.4.2 所示:

图 19.4.2 通过 USMART 设置时间和日期并测试闹钟 A

可以看到,设置闹钟 A 后,串口返回了 ALARM A!字符串,说明我们的闹钟 A 代码正常

运行了!

标签:

相关文章