OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 3279|回复: 1

《STM32F407 探索者开发指南》第二十七章 RTC实时时钟实验

[复制链接]

1140

主题

1152

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4895
金钱
4895
注册时间
2019-5-8
在线时间
1248 小时
发表于 2023-7-26 14:18:52 | 显示全部楼层 |阅读模式
第二十七章 RTC实时时钟实验
1)实验平台:正点原子探索者STM32F407开发板

2) 章节摘自【正点原子】STM32F407开发指南 V1.1

3)购买链接:https://detail.tmall.com/item.htm?id=609294673401

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)STM32技术交流QQ群:151941872

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

本章,我们将介绍STM32F407的内部实时时钟(RTC)。我们将使用LCD模块来显示日期和时间,实现一个简单的实时时钟,并可以设置闹铃,另外还将介绍BKP的使用。
本章分为如下几个小节:
27.1 RTC时钟简介
27.2 硬件设计
27.3 程序设计
27.4 下载验证

27.1 RTC时钟简介
STM32F407的实时时钟(RTC)是一个独立的定时器。STM32的RTC模块拥有一组连续计数的计数器,在相对应的软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统的当前时间和日期。

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

27.1.1 RTC框图
下面先来学习RTC框图,通过学习RTC框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。RTC的框图,如图27.1.1所示:                                 
image001.png
图27.1.1 RTC框图

我们把RTC框图分成以下几个部分讲解:

①     时钟源
STM32F407的RTC时钟源(RTCCLK)通过时钟控制器,可以从LSE时钟、LSI时钟以及HSE时钟三者中选择其一(通过设置RCC_BDCR寄存器选择)。一般我们选择LSE,即外部32.768KHz晶振作为时钟源(RTCCLK)。外部晶振具有精度高的优点。LSI是STM32芯片内部的低速RC 振荡器,频率约32 KHz,缺点是精度较低,所以一般不建议使用。比如当没有外部低速晶振(32.768KHz)的时候,分频后的HSE可以作为备选使用的时钟源。

②     预分频器
预分配器(RTC_PRER)分为2个部分:一个通过RTC_PRER寄存器的PREDIV_A位配置的7位异步预分频器。另一个通过RTC_PRER寄存器的PREDIV_S位配置的15位同步预分频器。
经过7位异步预分频器出来的时钟ck_apre可作为RTC_SSR亚秒递减计数器(RTC_SSR)的时钟,ck_apre时钟频率的计算公式如下:
image003.png
当RTC_SSR寄存器递减到0的时候,会使用PREDIV_S的值重新装载PREDIV_S。而PREDIV_S一般为255,这样,我们得到亚秒时间的精度是:1/256秒,即3.9ms左右,有了这个亚秒寄存器RTC_SSR,就可以得到更加精确的时间数据。
经过15位同步预分频器出来的时钟ck_spre可以用于更新日历,也可以用作16位唤醒自动重载定时器的时基,ck_apre时钟频率的计算公式如下:
image005.png
PREDIV_A和PREDIV_S分别为RTC的异步和同步分频器,使用两个预分频器时,我们推荐设置7位异步预分频器(PREDIV_A)的值较大,以最大程度降低功耗。例如:本实验我们的外部低速晶振的频率32.768KHz经过7位异步预分频器后,再经过15位同步预分频器,要得到1Hz频率的时钟用于更新日历。通过计算知道,32.768KHz的时钟要经过32768分频,才能得到1Hz的ck_spre。于是我们只需要设置:PREDIV_A=0X7F,即128分频;PREDIV_S=0XFF,即256分频,即可得到1Hz的Fck_spre,PREDIV_A的值我们也是往尽量大的原则,以最大程度降低功耗。

③     时间和日期相关寄存器
该部分包括三个影子寄存器,RTC_SSR(亚秒)、RTC_TR(时间)、RTC_DR(日期)。实时时钟一般表示为:时/分/秒/亚秒。RTC_TR寄存器用于存储时/分/秒时间数据,可读可写(即可设置或者获取时间)。RTC_DR寄存器用于存储日期数据,包括年/月/日/星期,可读可写(即可设置或者获取日期)。RTC_SSR寄存器用于存储亚秒级的时间,这样我们可以获取更加精确的时间数据。
这三个影子寄存器可以通过与 PCLK1(APB1 时钟)同步的影子寄存器来访问,这些时间和日期寄存器也可以直接访问,这样可避免等待同步的持续时间。
每隔2个RTCCLK周期,当前日历值便会复制到影子寄存器,并置位RTC_ISR寄存器的RSF位。我们可以读取RTC_TR和RTC_DR来得到当前时间和日期信息,不过需要注意的是:时间和日期都是以BCD码的格式存储的,读出来要转换一下,才可以得到十进制的数据。

④     可编程闹钟
STM32F407提供两个可编程闹钟:闹钟A(ALARM_A)和闹钟B(ALARM_B)。通过RTC_CR寄存器的ALRAE和ALRBE位置1来使能闹钟。当亚秒、秒、分、小时、日期分别与闹钟寄存器RTC_ALRMASSR/RTC_ALRMAR和RTC_ALRMBSSR/RTC_ALRMBR中的值匹配时,则可以产生闹钟(需要适当配置)。本章我们将利用闹钟A产生闹铃,即设置RTC_ALRMASSR和RTC_ALRMAR即可。

⑤     周期性自动唤醒
STM32F407的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来使能周期性唤醒中断时,可以使STM32退出低功耗模式。系统复位以及低功耗模式(睡眠、停机和待机)对唤醒定时器没有任何影响,它仍然可以正常工作,故唤醒定时器,可以用于周期性唤醒STM32。

27.1.2RTC寄存器
接下来,我们介绍本实验我们要用到的RTC寄存器。

l  RTC时间寄存器(RTC_TR
RTC时间寄存器描述如图27.1.2.1所示:   
image007.png
图27.1.2.1 RTC_TR寄存器

该寄存器是RTC的时间寄存器,可读可写,对该寄存器写,可以设置时间,对该寄存器读,可以获取当前的时间,此外该寄存器受到寄存器写保护,通过RTC写保护寄存器(RTC_WPR)设置,后面会讲解到RTC_WPR寄存器。需要注意的是:本寄存器存储的数据都是BCD格式的,读取之后需要进行转换,方可得到十进制的时分秒等数据。

l  RTC日期寄存器(RTC_DR
RTC日期寄存器描述如图27.1.2.2所示:     
image009.png
图27.1.2.2 RTC_DR寄存器

该寄存器是RTC的日期寄存器,可读可写,对该寄存器写,可以设置日期,对该寄存器读,可以获取当前的日期,同样该寄存器也受到寄存器写保护,存储的数据也都是BCD格式的。

l  RTC控制寄存器(RTC_CR
RTC控制寄存器描述如图27.1.2.3所示:     
image011.png
图27.1.2.3 RTC_CR寄存器

该寄存器重点介绍几个要用到的位:WUTIE是唤醒定时器中断使能位,ALRAIE是闹钟A中断使能位,本章用到这两个使能位,都设置为1即可。WUTE和ALRAE分别是唤醒定时器和闹钟A使能位,同样都设置为1,开启。FMT为小时格式选择位,我们设置为0,选择24小时制。WUCKSEL[2:0],用于唤醒时钟选择,这个前面已经有介绍了,我们这里就不多说了。

l  RTC亚秒寄存器(RTC_SSR
RTC亚秒寄存器描述如图27.1.2.4所示:     
image013.png
图27.1.2.4 RTC_SSR寄存器

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

l  RTC初始化和状态寄存器(RTC_ISR
RTC初始化和状态寄存器描述如图27.1.2.5所示:     
image015.png
图27.1.2.5 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,才可以设置。

l  RTC预分频寄存器(RTC_PRER
RTC预分频寄存器描述如图27.1.2.6所示:     
image017.png
图27.1.2.6 RTC_PRER寄存器

该寄存器用于RTC的分频,我们在之前也有讲过,这里就不多说了。该寄存器的配置,必须在初始化模式(INITF=1)下,才可以进行。

l  RTC唤醒寄存器(RTC_WUTR
RTC唤醒寄存器描述如图27.1.2.7所示:     
image019.png
图27.1.2.7 RTC_WUTR寄存器

该寄存器用于设置自动唤醒重装载值,可用于设置唤醒周期。该寄存器的配置,必须等待RTC_ISR的WUTWF为1才可以进行。

l  RTC闹钟A寄存器(RTC_ALRMAR
RTC闹钟A寄存器描述如图27.1.2.8所示:     
image021.png
图27.1.2.8 RTC_ALRMAR寄存器

该寄存器用于设置闹铃A,当WDSEL选择1时,使用星期制闹铃,本章我们选择星期制闹铃。该寄存器的配置,必须等待RTC_ISR的ALRAWF为1才可以进行。另外,还有RTC_ALRMASSR寄存器,该寄存器我们这里就不再介绍了,大家参考手册。

l  RTC写保护寄存器(RTC_WPR
RTC写保护寄存器:RTC_WPR,该寄存器比较简单,低八位有效。上电后,所有RTC寄存器都受到写保护(RTC_ISR[13:8]、RTC_TAFCR和RTC_BKPxR除外),必须依次写入:0xCA、0x53两关键字到RTC_WPR寄存器,才可以解锁。写一个错误的关键字将再次激活RTC的寄存器写保护。

l  RTC备份寄存器(RTC_BKPxR
RTC备份寄存器描述如图27.1.2.9所示:     
image023.png
图27.1.2.9 RTC_BKPxR寄存器

该寄存器组总共有32个,每个寄存器是32位的,可以存储128个字节的用户数据,这些寄存器在备份域中实现,可在VDD电源关闭时通过VBAT保持上电状态。备份寄存器不会在系统复位或电源复位时复位,也不会在MCU从待机模式唤醒时复位。
复位后,对RTC和RTC备份寄存器的写访问被禁止,执行以下操作可以使能RTC及RTC备份寄存器的写访问:
1)电源控制寄存器(PWR_CR)的DBP位来使能RTC及RTC备份寄存器的访问。
2)往RTC_WPR写入0xCA、0x53解锁序列(先写0xCA,再写0x53)。
我们可以用BKP来存储一些重要的数据,相当于一个EEPROM,不过这个EEPROM并不是真正的EEPROM,而是需要电池来维持它的数据。

l  备份区域控制(RCC_BDCR
备份区域控制寄存器描述如图27.1.2.10所示:     
image025.png
图27.1.2.10 RCC_BDCR寄存器

RTC的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在RTC操作之前先要通过这个寄存器选择RTC的时钟源,然后才能开始其他的操作。

27.2 硬件设计
1. 例程功能
本实验通过LCD显示RTC时间,并可以通过usmart设置RTC时间,从而调节时间,或设置RTC闹钟,还可以写入或者读取RTC后备区域SRAM。LED0闪烁,提示程序运行。

2. 硬件资源
1)LED灯
LED0 – PF9
LED1 – PF10
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)RTC(实时时钟)
4)正点原子 2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)

3. 原理图
RTC属于STM32F407内部资源,通过软件设置好就可以了。不过RTC不能断电,否则数据就丢失了,我们如果想让时间在断电后还可以继续走,那么必须确保开发板的电池有电。

27.3 程序设计
27.3.1 RTC的HAL库驱动
RTC在HAL库中的驱动代码在stm32f4xx_hal_rtc.c文件(及其头文件)中。下面介绍几个重要的RTC函数,其他没有介绍的请看源码。

1. HAL_RTC_Init函数
RTC的初始化函数,其声明如下:
  1. HAL_StatusTypeDefHAL_RTC_Init(RTC_HandleTypeDef *hrtc);
复制代码
l  函数描述:
用于初始化RTC。

l  函数形参:
形参1是RTC_HandleTypeDef结构体类型指针变量,其定义如下:
  1. typedef struct
  2. {
  3.   RTC_TypeDef                     *Instance;      /* 寄存器基地址 */
  4. RTC_InitTypeDef                 Init;           /* RTC配置结构体 */
  5. HAL_LockTypeDef                 Lock;            /* RTC锁定对象 */
  6.   __IOHAL_RTCStateTypeDef      State;           /* RTC设备访问状态 */
  7. }RTC_HandleTypeDef;
复制代码
1)Instance:指向RTC寄存器基地址。
2)Init:是真正的RTC初始化结构体,其结构体类型RTC_InitTypeDef定义如下:
  1. typedef struct
  2. {
  3.   uint32_t HourFormat;           /* 小时格式 */
  4.   uint32_t AsynchPrediv;        /* 异步预分频系数 */
  5.   uint32_t SynchPrediv;          /* 同步预分频系数 */  
  6.   uint32_t OutPut;                /* 选择连接到RTC_ALARM输出的标志 */   
  7.   uint32_t OutPutPolarity;      /* 设置RTC_ALARM的输出极性 */
  8.   uint32_t OutPutType;           /* 设置RTC_ALARM的输出类型为开漏输出还是推挽输出 */  
  9. }RTC_InitTypeDef;
复制代码
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配合使用。
3)Lock:用于配置锁状态。
4)State:RTC设备访问状态。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

2. HAL_RTC_SetTime函数
HAL_RTC_SetTime是设置RTC的时间函数。其声明如下:
  1. HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc,
  2. RTC_TimeTypeDef *sTime, uint32_t Format);
复制代码
l  函数描述:
该函数用于设置RTC的时间,即设置时间寄存器RTC_TR的相关位的值。
  1. typedef struct
  2. {
  3.   uint8_t Hours;
  4.   uint8_t Minutes;
  5.   uint8_t Seconds;
  6.   uint8_t TimeFormat;
  7.   uint32_t SubSeconds;
  8.   uint32_t SecondFraction;
  9.   uint32_t DayLightSaving;
  10.   uint32_t StoreOperation;
  11. }RTC_TimeTypeDef;
复制代码
l  函数形参:
形参1是RTC_HandleTypeDef结构体类型指针变量,即RTC的句柄。
形参2是RTC_TimeTypeDef结构体类型指针变量,定义如下:

前面四个成员变量就比较好理解了,分别用来设置RTC时间参数的小时,分钟,秒钟,以及AM/PM符号,大家参考前面讲解的RTC_TR的位描述即可。SubSeconds用来读取保存亚秒寄存器 RTC_SSR的值,SecondFraction用来读取保存同步预分频系数的值,也就是RTC_PRER的位0~14,DayLightSaving用来设置日历时间增加1小时,减少1小时,还是不变。StoreOperation用户可对此变量设置以记录是否已对夏令时进行更改。
形参3是uint32_t类型变量,用来设置输入的时间格式为BIN格式还是BCD格式,可选值为RTC_FORMAT_BIN或者RTC_FORMAT_BCD。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

3. HAL_RTC_SetDate函数
HAL_RTC_SetDate是设置RTC的日期函数。其声明如下:
  1. HAL_StatusTypeDefHAL_RTC_SetDate(RTC_HandleTypeDef *hrtc,
  2. RTC_DateTypeDef *sDate, uint32_t Format);
复制代码
l  函数描述:
该函数用于设置RTC的日期,即设置日期寄存器RTC_DR的相关位的值。

l  函数形参:
形参1是RTC_HandleTypeDef结构体类型指针变量,即RTC的句柄。
形参2是RTC_DateTypeDef结构体类型指针变量,定义如下:
  1. typedef struct
  2. {
  3.   uint8_t WeekDay;   /* 星期 */
  4.   uint8_t Month;     /* 月份 */
  5.   uint8_t Date;      /* 日期 */
  6.   uint8_t Year;      /* 年份 */
  7. }RTC_DateTypeDef;
复制代码
结构体一共四个成员变量,这四个成员变量分别对应星期、月份、日期和年份,对应的是RTC_DR寄存器。
形参3是uint32_t类型变量,用来设置输入的时间格式为BIN格式还是BCD格式,可选值为RTC_FORMAT_BIN或者RTC_FORMAT_BCD。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

4. HAL_RTC_GetTime函数
HAL_RTC_GetTime是获取当前RTC时间函数。其声明如下:
  1. HAL_StatusTypeDefHAL_RTC_GetTime(RTC_HandleTypeDef *hrtc,
  2. RTC_TimeTypeDef*sTime, uint32_t Format);
复制代码
l  函数描述:
该函数用于获取当前RTC时间,即读时间寄存器RTC_TR的相关位的值。

l  函数形参:
形参1是RTC_HandleTypeDef结构体类型指针变量,即RTC的句柄。
形参2是RTC_TimeTypeDef结构体类型指针变量,对应的是RTC_TR寄存器。
形参3是uint32_t类型变量,用来设置获取的时间格式为BIN格式还是BCD格式,可选值为RTC_FORMAT_BIN或者RTC_FORMAT_BCD。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

5. HAL_RTC_SetDate函数
HAL_RTC_SetDate是获取当前RTC日期函数。其声明如下:
  1. HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef*hrtc,
  2. RTC_DateTypeDef *sDate, uint32_t Format);
复制代码
RTC配置步骤
1)使能电源时钟,并使能RTCRTC后备寄存器写访问。
我们要访问RTC和RTC备份区域就必须先使能电源时钟,然后使能RTC即后备区域访问。电源时钟使能,通过RCC_APB1ENR寄存器来设置;RTC及RTC备份寄存器的写访问,通过PWR_CR寄存器的DBP位设置。HAL库设置方法为:
  1. __HAL_RCC_PWR_CLK_ENABLE();  /* 使能电源时钟PWR */
  2. __HAL_RCC_BKP_CLK_ENABLE();  /* 使能备份时钟 */
  3. 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的时间
调用HAL_RTC_SetTime函数设置RTC时间,该函数实际设置时间寄存器RTC_TR的相关位的值。

5)设置RTC的日期
调用HAL_RTC_SetDate函数设置RTC的日期,该函数实际设置日期寄存器RTC_DR的相关位的值。

6)获取RTC当前日期和时间
调用HAL_RTC_GetTime函数获取当前RTC时间,该函数实际读取RTC_TR寄存器,然后将值存放到相应的结构体中。
调用HAL_RTC_GetDate函数获取当前RTC日期,该函数实际读取RTC_DR寄存器,然后将值存放到相应的结构体中。
通过以上6个步骤,我们就完成了对RTC的配置,RTC即可正常工作,而且这些操作不是每次上电都必须执行的,可以视情况而定。当然,我们还可以唤醒中断、闹钟等,这些将在后面介绍。

27.3.2程序流程图
QQ截图20230725152633.png
图27.3.2.1 RTC实时时钟实验程序流程图

27.3.3 程序解析
1. RTC驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。RTC驱动源码包括两个文件:rtc.c和rtc.h。

限于篇幅,rtc.c中的代码,我们不全部贴出了,只针对几个重要的函数进行介绍。

rtc.h头文件只有函数的声明,下面我们直接介绍rtc.c的程序,先看RTC的初始化函数,其定义如下:
  1. /**
  2. * @brief      RTC初始化
  3. * @note         默认尝试使用LSE,当LSE启动失败后,切换为LSI.
  4. *               通过BKP寄存器0的值,可以判断RTC使用的是LSE/LSI:
  5. *               当BKP0==0x5050时,使用的是LSE
  6. *               当BKP0==0x5051时,使用的是LSI
  7. *               注意:切换LSI/LSE将导致时间/日期丢失,切换后需重新设置.
  8. *
  9. * @param       无
  10. * @retval     0,成功
  11. *               1,进入初始化模式失败
  12. */
  13. uint8_t rtc_init(void)
  14. {
  15.     uint16_t bkpflag = 0;
  16.     g_rtc_handle.Instance = RTC;
  17.     g_rtc_handle.Init.HourFormat = RTC_HOURFORMAT_24;  /* RTC设置为24小时格式 */
  18.     g_rtc_handle.Init.AsynchPrediv = 0x7F; /* RTC异步分频系数(1~0x7F) */
  19.     g_rtc_handle.Init.SynchPrediv = 0xFF;   /* RTC同步分频系数(0~0x7FFF) */
  20.     g_rtc_handle.Init.OutPut = RTC_OUTPUT_DISABLE;
  21.     g_rtc_handle.Init.OutPutPolarity =RTC_OUTPUT_POLARITY_HIGH;
  22.     g_rtc_handle.Init.OutPutType =RTC_OUTPUT_TYPE_OPENDRAIN;
  23.     /* 检查是不是第一次配置时钟 */
  24.     bkpflag = rtc_read_bkr(RTC_BKP_DR0);    /* 读取BKP0的值 */
  25.     if (HAL_RTC_Init(&g_rtc_handle) != HAL_OK)
  26.     {
  27.         return 1;
  28.     }
  29.     if ((bkpflag != 0x5050) && (bkpflag != 0x5051))  /* 之前未初始化过, 重新配置 */
  30.     {
  31.         rtc_set_time(6, 59, 56,RTC_HOURFORMAT12_AM);/* 设置时间, 根据实际时间修改 */
  32.         rtc_set_date(20, 4, 22, 3);    /* 设置日期 */
  33.     }
  34.     return 0;
  35. }
复制代码
该函数用来初始化RTC配置以及日期和时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位都不会再进行时间设置了(前提是备份电池有电)。在第一次配置的时候,我们是按照上面介绍的RTC初始化步骤调用函数HAL_RTC_Init来实现的。

我们通过读取BKP寄存器0的值来判断是否需要进行时间的设置,对BKP寄存器0的写操作是在HAL_RTC_MspInit回调函数中实现,下面会讲。第一次未对RTC进行初始化BKP寄存器0的值非0x5050非0x5051,当进行RTC初始化时,BKP寄存器0的值就是0x5050或0x5051,所以以上代码操作确保时间只会设置一次,复位时不会重新设置时间。电池正常供电时,我们设置的时间不会因复位或者断电而丢失。

读取后备寄存器的函数其实还是调用HAL库提供的函数接口,写后备寄存器函数同样也是。这两个函数如下:
  1. uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister);
  2. voidHAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister,
  3. uint32_t Data);
复制代码
这两个函数的使用方法就非常简单,分别用来读和写BKR寄存器的值。这里我们只是略微点到为止,详看例程源码。

接下来,我们用HAL_RTC_MspInit函数来编写RTC时钟配置等代码,其定义如下:
  1. /**
  2. *@brief       RTC底层驱动,时钟配置
  3. *@param       hrtc : RTC句柄
  4. *@note        此函数会被HAL_RTC_Init()调用
  5. *@retval      无
  6. */
  7. voidHAL_RTC_MspInit(RTC_HandleTypeDef* hrtc)
  8. {
  9.     uint16_t retry = 200;
  10.    RCC_OscInitTypeDef rcc_osc_init_handle;
  11.    RCC_PeriphCLKInitTypeDef rcc_periphclk_init_handle;
  12.    __HAL_RCC_RTC_ENABLE();     /* RTC使能 */
  13.    HAL_PWR_EnableBkUpAccess(); /* 取消备份区域写保护 */
  14.    __HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟PWR */
  15.    __HAL_RCC_RTC_ENABLE();     /* RTC使能 */
  16.     /* 使用寄存器的方式去检测LSE是否可以正常工作 */
  17.     RCC->BDCR |= 1 << 0;         /* 尝试开启LSE */
  18.     while (retry && ((RCC->BDCR & 0x02) == 0))  /* 等待LSE准备好 */
  19.     {
  20.        retry--;
  21.        delay_ms(5);
  22.     }
  23.    
  24.     if (retry == 0)     /* LSE起振失败 使用LSI */
  25.     {
  26.        rcc_osc_init_handle.OscillatorType = RCC_OSCILLATORTYPE_LSI;
  27.        /* 选择要配置的振荡器 */
  28.        rcc_osc_init_handle.PLL.PLLState = RCC_PLL_NONE;    /* PLL不配置 */
  29.        rcc_osc_init_handle.LSIState = RCC_LSI_ON;           /* LSI状态:开启 */
  30.        HAL_RCC_OscConfig(&rcc_osc_init_handle);/* 配置设置的rcc_oscinitstruct */
  31.        rcc_periphclk_init_handle.PeriphClockSelection = RCC_PERIPHCLK_RTC;
  32.        /* 选择要配置外设 RTC */
  33.        rcc_periphclk_init_handle.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
  34.        /* RTC时钟源选择LSI */
  35.        HAL_RCCEx_PeriphCLKConfig(&rcc_periphclk_init_handle);
  36.        /* 配置设置的rcc_periphclkinitstruct */
  37.         rtc_write_bkr(0, 0x5051);
  38.     }
  39.     else
  40.     {
  41.        rcc_osc_init_handle.OscillatorType = RCC_OSCILLATORTYPE_LSE;
  42.         /* 选择要配置的振荡器 */
  43.        rcc_osc_init_handle.PLL.PLLState = RCC_PLL_NONE;  /* PLL不配置 */
  44.        rcc_osc_init_handle.LSEState = RCC_LSE_ON;         /* LSE状态:开启 */
  45.        HAL_RCC_OscConfig(&rcc_osc_init_handle); /* 配置设置的rcc_oscinitstruct */
  46.        rcc_periphclk_init_handle.PeriphClockSelection = RCC_PERIPHCLK_RTC;
  47.        /* 选择要配置外设RTC */
  48.        rcc_periphclk_init_handle.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
  49.        /* RTC时钟源选择LSE */
  50.        HAL_RCCEx_PeriphCLKConfig(&rcc_periphclk_init_handle);
  51.         /* 配置设置的rcc_periphclkinitstruct */
  52.        rtc_write_bkr(0, 0x5050);
  53.     }
  54.    
  55. }
复制代码
接着,我们介绍一下rtc_set_alarma函数,其定义如下:
  1. /**
  2. *@breif       设置闹钟时间(按星期闹铃,24小时制)
  3. *@param       week          :星期几(1~7)
  4. *@param       hour,min,sec : 小时,分钟,秒钟
  5. *@retval      无
  6. */
  7. voidrtc_set_alarma(uint8_t week, uint8_t hour, uint8_t min, uint8_t sec)
  8. {
  9.    RTC_AlarmTypeDef rtc_alarm_handle;
  10.    
  11.    rtc_alarm_handle.AlarmTime.Hours = hour;    /* 小时 */
  12.    rtc_alarm_handle.AlarmTime.Minutes = min;  /* 分钟 */
  13.    rtc_alarm_handle.AlarmTime.Seconds = sec;  /* 秒 */
  14.    rtc_alarm_handle.AlarmTime.SubSeconds = 0;
  15.    rtc_alarm_handle.AlarmTime.TimeFormat = RTC_HOURFORMAT12_AM;
  16.    
  17.    rtc_alarm_handle.AlarmMask = RTC_ALARMMASK_NONE;    /* 精确匹配星期,时分秒 */
  18.    rtc_alarm_handle.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_NONE;
  19.     /*按星期 */
  20.     rtc_alarm_handle.AlarmDateWeekDaySel= RTC_ALARMDATEWEEKDAYSEL_WEEKDAY;
  21.    rtc_alarm_handle.AlarmDateWeekDay = week;  /* 星期 */
  22.    rtc_alarm_handle.Alarm = RTC_ALARM_A;       /* 闹钟A */
  23.    HAL_RTC_SetAlarm_IT(&g_rtc_handle, &rtc_alarm_handle, RTC_FORMAT_BIN);
  24.    
  25.    HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 1, 2); /* 抢占优先级1, 子优先级2 */
  26.    HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
  27. }
复制代码
该函数用于设置闹钟A,也就是设置ALRMAR和ALRMASSR寄存器的值,来设置闹钟时间,这里使用HAL库的HAL_RTC_SetAlarm_IT函数来初始化闹钟相关寄存器,前面已经介绍过这个函数,请回顾。

调用函数rtc_set_wakeup设置闹钟A的参数之后,最后,开启闹钟A中断(连接在外部中断线17),并设置中断分组。当RTC的时间和闹钟A设置的时间完全匹配时,将产生闹钟中断。

接着,我们介绍一下rtc_set_wakeup函数,该函数代码如下:
  1. /**
  2. *@breif       周期性唤醒定时器设置
  3. *@param       wksel
  4. *  @arg      RTC_WAKEUPCLOCK_RTCCLK_DIV16       ((uint32_t)0x00000000)
  5. *  @arg       RTC_WAKEUPCLOCK_RTCCLK_DIV8         ((uint32_t)0x00000001)
  6. *  @arg      RTC_WAKEUPCLOCK_RTCCLK_DIV4        ((uint32_t)0x00000002)
  7. *  @arg      RTC_WAKEUPCLOCK_RTCCLK_DIV2        ((uint32_t)0x00000003)
  8. *  @arg      RTC_WAKEUPCLOCK_CK_SPRE_16BITS     ((uint32_t)0x00000004)
  9. *  @arg      RTC_WAKEUPCLOCK_CK_SPRE_17BITS     ((uint32_t)0x00000006)
  10. *@note       000,RTC/16;001,RTC/8;010,RTC/4;011,RTC/2;
  11. *@note        注意:RTC就是RTC的时钟频率,即RTCCLK!
  12. *@param       cnt: 自动重装载值.减到0,产生中断.
  13. *@retval      无
  14. */
  15. voidrtc_set_wakeup(uint8_t wksel, uint16_t cnt)
  16. {
  17.     /* 清除RTC WAKE UP的标志 */
  18.    __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_WUTF);  
  19.    HAL_RTCEx_SetWakeUpTimer_IT(&g_rtc_handle, cnt, wksel);/* 设置重装载值和时钟 */
  20.    HAL_NVIC_SetPriority(RTC_WKUP_IRQn, 2, 2);  /* 抢占优先级2,子优先级2 */
  21.    HAL_NVIC_EnableIRQ(RTC_WKUP_IRQn);
  22. }
复制代码
该函数用于设置RTC周期性唤醒定时器,实现周期性唤醒中断,连接在外部中断线22。该函数调用的是HAL库函数HAL_RTCEx_SetWakeUpTimer_IT实现的。

有了中断设置函数,就必定有中断服务函数,同时因为HAL库会开放中断处理回调函数,接下来看这两个中断的中断服务函数和中断处理回调函数,代码如下:
  1. /**
  2. *@breif       RTC闹钟中断服务函数
  3. *@param       无
  4. *@retval      无
  5. */
  6. voidRTC_Alarm_IRQHandler(void)
  7. {
  8.    HAL_RTC_AlarmIRQHandler(&g_rtc_handle);
  9. }
  10. /**
  11. *@breif       RTC闹钟A中断处理回调函数
  12. *@param       hrtc : RTC句柄
  13. *@retval      无
  14. */
  15. voidHAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
  16. {
  17.    printf("ALARMA!\r\n");
  18. }
  19. /**
  20. *@breif       RTC WAKE UP中断服务函数
  21. *@param       无
  22. *@retval      无
  23. */
  24. voidRTC_WKUP_IRQHandler(void)
  25. {
  26.    HAL_RTCEx_WakeUpTimerIRQHandler(&g_rtc_handle);
  27. }
  28. /**
  29. *@breif       RTC WAKE UP中断处理处理回调函数
  30. *@param       hrtc : RTC句柄
  31. *@retval      无
  32. */
  33. voidHAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
  34. {
  35.    LED1_TOGGLE();
  36. }
复制代码
其中,RTC_Alarm_IRQHandler函数用于闹钟中断,其中断控制逻辑写在中断回调函数HAL_RTC_AlarmAEventCallback中,每当闹钟A闹铃时,会从串口打印一个字符串“ALARM A!”。RTC_WKUP_IRQHandler函数用于RTC自动唤醒定时器中断,其中断控制逻辑写在中断回调函数HAL_RTCEx_WakeUpTimerEventCallback中,可以通过观察LED1的状态来查看RTC自动唤醒中断的情况。

rtc.c的其他程序,这里就不再介绍了,请大家直接看源码。

2. main.c代码
在main.c里面编写如下代码:
  1. int main(void)
  2. {
  3.     uint8_t hour, min, sec, ampm;
  4.     uint8_t year, month, date, week;
  5.     uint8_t tbuf[40];
  6.     uint8_t t=0;
  7.    HAL_Init();                                  /* 初始化HAL库 */
  8.    sys_stm32_clock_init(336, 8, 2, 7);     /* 设置时钟, 168Mhz */
  9.    delay_init(168);                            /* 延时初始化 */
  10.    usart_init(115200);                        /* 串口初始化为115200 */
  11.    usmart_dev.init(84);                       /* 初始化USMART */
  12.    led_init();                                  /* 初始化LED */
  13.    lcd_init();                                  /* 初始化LCD */
  14.     rtc_init();                                  /* 初始化RTC */
  15.     /* 配置WAKE UP中断,1秒钟中断一次 */
  16.    rtc_set_wakeup(RTC_WAKEUPCLOCK_CK_SPRE_16BITS, 0);
  17.    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
  18.    lcd_show_string(30, 70, 200, 16, 16, "RTCTEST", RED);
  19.    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
  20.     while (1)
  21.     {
  22.        t++;
  23.        if ((t % 10) == 0)  /* 每100ms更新一次显示数据 */
  24.        {
  25.            rtc_get_time(&hour, &min, &sec, &m);
  26.            sprintf((char *)tbuf, "Time:%02d:%02d:%02d", hour, min, sec);
  27.            lcd_show_string(30, 140, 210, 16, 16, (char*)tbuf, RED);
  28.            rtc_get_date(&year, &month, &date, &week);
  29.            sprintf((char *)tbuf, "Date:20%02d-%02d-%02d", year, month, date);
  30.            lcd_show_string(30, 160, 210, 16, 16, (char*)tbuf, RED);
  31.            sprintf((char *)tbuf, "Week:%d", week);
  32.            lcd_show_string(30, 180, 210, 16, 16, (char*)tbuf, RED);
  33.        }
  34.        if ((t % 20) == 0)
  35.        {
  36.            LED0_TOGGLE();  /* 每200ms,翻转一次LED0 */
  37.        }        
  38.        delay_ms(10);
  39.     }
  40. }
复制代码
我们在无限循环中每100ms读取RTC的时间和日期(一次),并显示在LCD上面。每200ms,翻转一次LED0。

为方便RTC相关函数的调用验证,在usmart_config.c里面,修改了usmart_nametab如下:
  1. /* 函数名列表初始化(用户自己添加)
  2. * 用户直接在这里输入要执行的函数名及其查找串
  3. */
  4. struct _m_usmart_nametab usmart_nametab[] =
  5. {
  6. #if USMART_USE_WRFUNS == 1      /* 如果使能了读写操作 */
  7.     (void *)read_addr, "uint32_tread_addr(uint32_t addr)",
  8.     (void *)write_addr, "voidwrite_addr(uint32_t addr, uint32_t val)",
  9. #endif
  10.     (void *)delay_ms, "voiddelay_ms(uint16_t nms)",
  11.     (void *)delay_us, "voiddelay_us(uint32_t nus)",
  12.         
  13.     (void *)rtc_read_bkr, "uint32_trtc_read_bkr(uint32_t bkrx)",
  14.     (void *)rtc_write_bkr, "voidrtc_write_bkr(uint32_t bkrx, uint32_t data)",
  15. (void *)rtc_set_time, "uint8_trtc_set_time(uint8_t hour, uint8_t min,
  16. uint8_t sec,uint8_t ampm)",
  17. (void *)rtc_set_date, "uint8_trtc_set_date(uint8_t year, uint8_t month,
  18. uint8_t date,uint8_t week)",
  19.     (void *)rtc_set_wakeup, "voidrtc_set_wakeup(uint8_t wksel, uint16_t cnt)",
  20. (void *)rtc_get_week, "uint8_trtc_get_week(uint16_t year, uint8_t month,
  21. uint8_tday)",
  22. (void *)rtc_set_alarma, "voidrtc_set_alarma(uint8_t week, uint8_t hour,
  23. uint8_t min,uint8_t sec)",
  24. };
复制代码
将RTC的一些相关函数加入了usmart,这样通过串口就可以直接设置RTC时间、日期、闹钟A和周期性唤醒等操作。至此,RTC实时时钟的软件设计就完成了,接下来就让我们来检验一下,我们的程序是否正确。

27.4下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。然后,可以看到LCD开始显示时间,实际显示效果如图27.4.1所示:
image029.png
图27.4.1 RTC实验测试图

如果时间不正确,可以利用上一章介绍的usmart工具,通过串口来设置;并且可以设置闹钟时间等,如图27.4.2所示:
image031.png
图27.4.2 通过USMART设置时间并测试闹钟

按照图中编号6、7、8顺序,设置闹钟A、设置日期、设置时间。然后等待我们设置的时间到来后,串口打印ALARM A!这个字符串,证明我们的闹钟A程序正常运行了!
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

0

主题

1

帖子

0

精华

新手入门

积分
2
金钱
2
注册时间
2023-8-7
在线时间
0 小时
发表于 2023-8-7 09:17:22 | 显示全部楼层
本帖最后由 juanyong0518 于 2023-8-7 09:58 编辑

您好 原子哥,我买的F407的最小系统板,用的这份实例代码,写入代码后,初始化时一直是超时退出,仿真发现是 允许初始化标志位 一直不置位,这是啥原因


这里一直不置位,最后超时退出,是开发板坏了吗
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-2-24 08:12

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表