33. RTC实时时钟

一、RTC时钟简介

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

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

二、RTC框图

RTC框图

①、时钟源

  STM32F407 的 RTC 时钟源(RTC_CLK)通过时钟控制器,可以从 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 时钟频率的计算公式如下:

\[F_{ck\_apre} = \frac{F_{rtc\_clk}}{PREDIV_A + 1} \]

  当 RTC_SSR 寄存器递减到 0 的时候,会使用 PREDIV_S 的值重新装载 PREDIV_S。而 PREDIV_S 一般为 255,这样,我们得到亚秒时间的精度是:1/256 秒,即 3.9ms 左右,有了这个亚秒寄存器 RTC_SSR,就可以得到更加精确的时间数据。

  经过 15 位同步预分频器出来的时钟 ck_spre 可以用于更新日历,也可以用作 16 位唤醒自动重载定时器的时基,ck_apre 时钟频率的计算公式如下:

\[F_{ck\_spre} = \frac{F_{rtc\_clk}}{(PREDIV_S + 1) * (PERDIV_A + 1)} \]

  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 的 \(F_{ck\_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。

三、RTC常用寄存器

3.1、RTC时间寄存器

RTC时间寄存器

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

3.2、RTC日期寄存器

RTC日期寄存器

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

3.3、RTC控制寄存器

RTC控制寄存器

  位 14 WUTIE 位是唤醒定时器中断使能位,设置为 1,开启。位 13 ALRAIE 位是闹钟 A 中断使能位,设置为 1,开启。位 10 WUTE 位和 位 8 ALRAE 位分别是唤醒定时器和闹钟 A 使能位,同样都设置为 1,开启。位 6 FMT 位为小时格式选择位,设置为 0,选择 24 小时制。WUCKSEL[2:0] 位,用于唤醒时钟选择。

3.4、RTC初始化和状态寄存器

RTC初始化和状态寄存器

  该寄存器中,位 10 WUTF 位、位 9 ALRBF 位和位 8 ALRAF 位,分别是唤醒定时器、闹钟 B 和闹钟 A 的中断标志位,当对应事件产生时,这些标志位被置 1,如果设置了中断,则会进入中断服务函数,这些位通过软件写 0 清除。

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

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

  位 5 RSF 位为寄存器同步标志,仅在该位为 1 时,表示日历影子寄存器已同步,可以正确读取 RTC_TR/RTC_TR 寄存器的值了。

  位 2 WUTWF 位、位 1 ALRBWF 位和位 0 ALRAWF 位分别是唤醒定时器、闹钟 B 和闹钟 A 的写标志,只有在这些位为 1 的时候,才可以更新对应的内容。比如:要设置闹钟 A 的 ALRMAR 和 ALRMASSR,则必须先等待 ALRAWF 为 1,才可以设置。

3.5、RTC预分频器寄存器

RTC预分频器寄存器

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

3.6、RTC唤醒定时器寄存器

RTC唤醒定时器寄存器

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

3.7、RTC闹钟A寄存器

RTC闹钟A寄存器

  该寄存器用于设置闹铃 A,当 WDSEL 选择 1 时,使用星期制闹铃。该寄存器的配置,必须等待 RTC_ISR 的 ALRAWF 为 1 才可以进行。

3.8、RTC闹钟B寄存器

RTC闹钟B寄存器

3.9、RTC写保护寄存器

RTC写保护寄存器

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

3.10、RTC亚秒寄存器

RTC亚秒寄存器

  该寄存器可用于获取更加精确的 RTC 时间。

3.11、RTC备份寄存器

RTC备份寄存器

  该寄存器组总共有 32 个,每个寄存器是 32 位的,可以存储 128 个字节的用户数据,这些寄存器在备份域中实现,可在 VDD 电源关闭时通过 VBAT 保持上电状态。备份寄存器不会在系统复位或电源复位时复位,也不会在 MCU 从待机模式唤醒时复位。

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

  • 电源控制寄存器(PWR_CR)的 DBP 位来使能 RTC 及 RTC 备份寄存器的访问。
  • 往 RTC_WPR 写入 0xCA、0x53 解锁序列(先写 0xCA,再写 0x53)。

我们可以用 BKP 来存储一些重要的数据,相当于一个 EEPROM,不过这个 EEPROM 并不是真正的 EEPROM,而是需要电池来维持它的数据。

四、RTC配置步骤

4.1、使能电源时钟并使能备份域访问

  我们要访问 RTC 和 RTC 备份区域就必须先使能电源时钟,然后使能 RTC 即后备区域访问。电源时钟使能,通过 RCC_APB1ENR 寄存器来设置;RTC 及 RTC 备份寄存器的写访问,通过 PWR_CR 寄存器的 DBP 位设置。

  使能电源时钟:

#define __HAL_RCC_PWR_CLK_ENABLE()     do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)

  使能备份域访问:

void HAL_PWR_EnableBkUpAccess(void);

4.2、选择RTC时钟源并使能

  开启 LSE 外部低速振荡器:

HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef  *RCC_OscInitStruct);

  该函数只有一个形参,就是结构体 RCC_OscInitTypeDef 类型指针。它的定义如下:

typedef struct
{
    uint32_t OscillatorType;        // 需要配置的振荡器
    uint32_t HSEState;              // HSE状态
    uint32_t LSEState;              // LSE状态
    uint32_t HSIState;              // HSI状态
    uint32_t HSICalibrationValue;   // HSI校准值,范围0x00~0x1F
    uint32_t LSIState;              // LSI状态
    RCC_PLLInitTypeDef PLL;         // PLL初始化结构体
}RCC_OscInitTypeDef;

  使能 RTC:

#define __HAL_RCC_RTC_ENABLE() (*(__IO uint32_t *) RCC_BDCR_RTCEN_BB = ENABLE)

4.3、配置RTC工作参数

  HAL_RTC_Init() 函数为 RTC 的初始化函数,其声明如下:

HAL_StatusTypeDef HAL_RTC_Init(RTC_HandleTypeDef *hrtc);

  形参 hrtcRTC_HandleTypeDef 结构体类型指针变量,其定义如下:

typedef struct
{
  RTC_TypeDef *Instance;            // 寄存器基地址
  RTC_InitTypeDef  Init;            // RTC 配置结构体
  HAL_LockTypeDef Lock;             // RTC锁定对象
  __IO HAL_RTCStateTypeDef State;   // RTC设备访问状态
} RTC_HandleTypeDef;

  Instance指向 RTC 寄存器基地址,可选值如下:

#define RTC                 ((RTC_TypeDef *) RTC_BASE)

  Init:是真正的 RTC 初始化结构体,其结构体类型 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;

  HourFormat 用来 设置小时格式,可以是 12 小时制或者 24 小时制,这两个选项的宏定义分别为 RTC_HOURFORMAT_12 和 RTC_HOURFORMAT_24。

#define RTC_HOURFORMAT_24              0x00000000U
#define RTC_HOURFORMAT_12              RTC_CR_FMT

  AsynchPrediv 用来 设置 RTC 的异步预分频系数,也就是设置 RTC_PRER 寄存器的PREDIV_A 相关位,因为异步预分频系数是 7 位,所以最大值为 0x7F,不能超过这个值。

  SynchPrediv 用来 设置RTC的同步预分频系数,也就是设置 RTC_PRER 寄存器的 PREDIV_S 相关位,因为同步预分频系数也是 15 位,所以最大值为 0x7FFF,不能超过这个值。

  OutPut 用来选择要连接到 RTC_ALARM 输出的标志,可选值如下:

#define RTC_OUTPUT_DISABLE             0x00000000U           // 禁止输出
#define RTC_OUTPUT_ALARMA              RTC_CR_OSEL_0         // 使能闹钟A输出
#define RTC_OUTPUT_ALARMB              RTC_CR_OSEL_1         // 使能闹钟B输出
#define RTC_OUTPUT_WAKEUP              RTC_CR_OSEL           // 使能唤醒输出

  OutPutPolarity 用来 设置 RTC_ALARM 的输出极性,与 Output 成员变量配合使用,可选值如下:

#define RTC_OUTPUT_POLARITY_HIGH       0x00000000U
#define RTC_OUTPUT_POLARITY_LOW        RTC_CR_POL

  OutPutType 用来 设置 RTC_ALARM 的输出类型 为开漏输出(RTC_OUTPUT_TYPE_OPENDRAIN)还是推挽输出(RTC_OUTPUT_TYPE_PUSHPULL),与成员变量 OutPut 和 OutPutPolarity 配合使用。

#define RTC_OUTPUT_TYPE_OPENDRAIN      0x00000000U
#define RTC_OUTPUT_TYPE_PUSHPULL       RTC_TAFCR_ALARMOUTTYPE

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

typedef enum 
{
    HAL_OK = 0x00U,             // 成功
    HAL_ERROR = 0x01U,          // 错误
    HAL_BUSY = 0x02U,           // 忙碌
    HAL_TIMEOUT = 0x03U         // 超时
} HAL_StatusTypeDef;

4.4、设置RTC的日期和时间

4.4.1、设置日期

  HAL_RTC_SetDate() 函数用于设置 RTC 的日期,即设置日期寄存器 RTC_DR 的相关位的值。

HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);

  形参 hrtcRTC_HandleTypeDef 结构体类型指针变量,即 RTC 的句柄。

  形参 Format 用来 设置输入的时间格式 为 BIN 格式还是 BCD 格式,可选值如下:

#define RTC_FORMAT_BIN                  0x00000000U
#define RTC_FORMAT_BCD                  0x00000001U

  形参 sDataRTC_DateTypeDef 结构体类型指针变量,定义如下:

typedef struct
{
    uint8_t WeekDay;    // 星期
    uint8_t Month;      // 月份
    uint8_t Date;       // 日期
    uint8_t Year;       // 年
} RTC_DateTypeDef;

4.4.2、设置时间

  HAL_RTC_SetTime() 函数用于设置 RTC 的时间,即设置时间寄存器 RTC_TR 的相关位的值。

HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);

  形参 hrtcRTC_HandleTypeDef 结构体类型指针变量,即 RTC 的句柄。

  形参 sTimeRTC_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;

  形参 Format 用来 设置输入的时间格式 为 BIN 格式还是 BCD 格式,可选值为 RTC_FORMAT_BIN 或者 RTC_FORMAT_BCD

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

4.5、获取RTC当前日期和时间

4.5.1、获取日期

HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);

  形参 hrtcRTC_HandleTypeDef 结构体类型指针变量,即 RTC 的句柄。

  形参 sDataRTC_DateTypeDef 结构体类型指针变量。

  形参 Format 用来 设置输入的时间格式 为 BIN 格式还是 BCD 格式,可选值为 RTC_FORMAT_BIN 或者 RTC_FORMAT_BCD

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

4.5.2、获取时间

  HAL_RTC_GetTime() 函数用于获取当前 RTC 时间,即读时间寄存器 RTC_TR 的相关位的值。

HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);

  形参 hrtcRTC_HandleTypeDef 结构体类型指针变量,即 RTC 的句柄。

  形参 sTimeRTC_TimeTypeDef 结构体类型指针变量。

  形参 Format 用来 设置输入的时间格式 为 BIN 格式还是 BCD 格式,可选值为 RTC_FORMAT_BIN 或者 RTC_FORMAT_BCD

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

五、RTC闹钟配置

  RTC 闹钟的配置在 RTC 已经配置好相关参数后的基础上进行配置。

5.1、配置闹钟参数并开启中断

HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format);

  形参 hrtcRTC_HandleTypeDef 结构体类型指针变量,即 RTC 的句柄。

  形参 sAlarm 是 RTC_AlarmTypeDef 闹钟结构体变量,它的定义如下:

typedef struct
{
    RTC_TimeTypeDef AlarmTime;          // 设定RTC时间寄存器的
    uint32_t AlarmMask;                 // RTC闹钟掩码字段选择
    uint32_t AlarmSubSecondMask;        // RTC闹钟掩码字段选择
    uint32_t AlarmDateWeekDaySel;       // 闹钟的日期/星期选择
    uint8_t AlarmDateWeekDay;           // 指定闹钟的日期/星期
    uint32_t Alarm;                     // RTC闹钟选择
} RTC_AlarmTypeDef;

  形参 Format 用来 设置输入的时间格式 为 BIN 格式还是 BCD 格式,可选值为 RTC_FORMAT_BIN 或者 RTC_FORMAT_BCD

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

5.2、使能闹钟中断

5.2.1、设置中断优先级分组

  HAL_NVIC_SetPriorityGrouping() 函数是设置中断优先级分组函数。其声明如下:

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

  其中,参数 PriorityGroup中断优先级分组号,可以选择范围如下:

#define NVIC_PRIORITYGROUP_0         0x00000007U /*!< 0 bits for pre-emption priority
                                                      4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1         0x00000006U /*!< 1 bits for pre-emption priority
                                                      3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2         0x00000005U /*!< 2 bits for pre-emption priority
                                                      2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3         0x00000004U /*!< 3 bits for pre-emption priority
                                                      1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4         0x00000003U /*!< 4 bits for pre-emption priority
                                                      0 bits for subpriority */

  这个函数在一个工程里基本只调用一次,而且是在程序 HAL 库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。如果调用了多次,则以最后一次为准。

5.2.2、设置中断优先级

  HAL_NVIC_SetPriority() 函数是设置中断优先级函数。其声明如下:

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);

  其中,参数 IRQn中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。

typedef enum
{
  RTC_Alarm_IRQn              = 41,     /*!< RTC Alarm (A and B) through EXTI Line Interrupt                   */
} IRQn_Type;

  参数 PreemptPriority抢占优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。

  参数 SubPriority响应优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。

5.2.3、使能中断

  HAL_NVIC_EnableIRQ() 函数是中断使能函数。其声明如下:

void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);

  其中,参数 IRQn中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。

5.3、编写中断服务函数

  每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU 将找不到中断服务函数)。中断服务函数接口厂家已经在 startup_stm32f407xx.s 中写好了。

void RTC_Alarm_IRQHandler(void);

  HAL 库为了用户使用方便,提供了一个中断通用入口函数 HAL_UART_IRQHandler()

void HAL_RTC_AlarmIRQHandler(RTC_HandleTypeDef *hrtc);

  该函数又会调用 HAL_RTC_AlarmAEventCallback() 回调函数:

void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)

六、RTC周期性唤醒配置

  RTC 周期性唤醒的配置也是在 RTC 已经配置好相关参数后的基础上进行配置。

6.1、清除RTC WKUP标志位

#define __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(__HANDLE__, __FLAG__)            ((__HANDLE__)->Instance->ISR) = (~((__FLAG__) | RTC_ISR_INIT)|((__HANDLE__)->Instance->ISR & RTC_ISR_INIT))

6.2、设置闹钟参数并开启中断

HAL_StatusTypeDef HAL_RTCEx_SetWakeUpTimer_IT(RTC_HandleTypeDef *hrtc, uint32_t WakeUpCounter, uint32_t WakeUpClock);

6.3、使能闹钟中断

6.3.1、设置中断优先级分组

  HAL_NVIC_SetPriorityGrouping() 函数是设置中断优先级分组函数。其声明如下:

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

  其中,参数 PriorityGroup中断优先级分组号,可以选择范围如下:

#define NVIC_PRIORITYGROUP_0         0x00000007U /*!< 0 bits for pre-emption priority
                                                      4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1         0x00000006U /*!< 1 bits for pre-emption priority
                                                      3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2         0x00000005U /*!< 2 bits for pre-emption priority
                                                      2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3         0x00000004U /*!< 3 bits for pre-emption priority
                                                      1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4         0x00000003U /*!< 4 bits for pre-emption priority
                                                      0 bits for subpriority */

  这个函数在一个工程里基本只调用一次,而且是在程序 HAL 库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。如果调用了多次,则以最后一次为准。

6.3.2、设置中断优先级

  HAL_NVIC_SetPriority() 函数是设置中断优先级函数。其声明如下:

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);

  其中,参数 IRQn中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。

typedef enum
{
  RTC_WKUP_IRQn               = 3,      /*!< RTC Wakeup interrupt through the EXTI line                        */
} IRQn_Type;

  参数 PreemptPriority抢占优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。

  参数 SubPriority响应优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。

6.3.3、使能中断

  HAL_NVIC_EnableIRQ() 函数是中断使能函数。其声明如下:

void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);

  其中,参数 IRQn中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。

6.4、编写中断服务函数

  每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU 将找不到中断服务函数)。中断服务函数接口厂家已经在 startup_stm32f407xx.s 中写好了。

void RTC_WKUP_IRQHandler(void);

  HAL 库为了用户使用方便,提供了一个中断通用入口函数 HAL_UART_IRQHandler()

void HAL_RTCEx_WakeUpTimerIRQHandler(RTC_HandleTypeDef *hrtc);

  该函数又会调用 HAL_RTCEx_WakeUpTimerEventCallback() 回调函数:

void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)

七、程序源码

typedef struct Date
{
    uint16_t year;
    uint8_t month;
    uint8_t day;
    uint8_t weekDay;
} Date;

typedef struct Time
{
    uint8_t hour;
    uint8_t minute;
    uint8_t second;
} Time;

typedef struct DateTime
{
    Date date;
    Time time;
} DateTime;

  RTC 初始化函数:

RTC_HandleTypeDef g_rtc_handle;

/**
 * @brief RTC初始化函数
 * 
 * @param dateTime DateTime,日期时间结构体
 */
void RTC_Init(DateTime dateTime)
{
    uint16_t flag = 0;

    g_rtc_handle.Instance = RTC;                                                // RTC寄存器基地址
    g_rtc_handle.Init.HourFormat = RTC_HOURFORMAT_24;                           // 小时格式
    g_rtc_handle.Init.AsynchPrediv = 0x7F;                                      // 异步预分频系数
    g_rtc_handle.Init.SynchPrediv = 0xFF;                                       // 同步预分频系数
    g_rtc_handle.Init.OutPut = RTC_OUTPUT_DISABLE;                              // 选择连接到RTC_ALARM输出的标志
    g_rtc_handle.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;                // 设置RTC_ALARM的输出极性
    g_rtc_handle.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;                   // 设置RTC_ALARM的输出类型为开漏输出还是推挽输出
    HAL_RTC_Init(&g_rtc_handle);


    flag = HAL_RTCEx_BKUPRead(&g_rtc_handle, 0);                                // 读取后备域寄存器0的值
    if (flag != 0x5050)                                                         // 之前未初始化过, 重新配置
    {
        RTC_SetDateTime(dateTime);                                              // 设置RTC时间
        HAL_RTCEx_BKUPWrite(&g_rtc_handle, 0, 0x5050);                          // 写入0x5050到后备域寄存器0
    }
}

  RTC 底层初始化回调函数:

/**
 * @brief RTC底层初始化回调函数
 * 
 * @param hrtc RTC的句柄
 */
void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_PeriphCLKInitTypeDef RCC_PeriphCLKInitStruct = {0};

    __HAL_RCC_PWR_CLK_ENABLE();                                                 // 使能PWR时钟
    HAL_PWR_EnableBkUpAccess();                                                 // 使能备份域访问
  
    __HAL_RCC_RTC_ENABLE();                                                     // 使能RTC

    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE;                  // 选择时钟源
    RCC_OscInitStruct.LSEState = RCC_LSE_ON;                                    // 开启LSE
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;                              // 不配置PLL
    HAL_RCC_OscConfig(&RCC_OscInitStruct);

    RCC_PeriphCLKInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;           // 选择RTC时钟
    RCC_PeriphCLKInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;           // RTC时钟源来自LSE
    HAL_RCCEx_PeriphCLKConfig(&RCC_PeriphCLKInitStruct);
}

  RTC 设置时间函数:

/**
 * @brief RTC设置时间
 * 
 * @param time Time,时间的结构体
 */
void RTC_SetTime(Time time)
{
    RTC_TimeTypeDef RTC_TimeStruct = {0};

    RTC_TimeStruct.Hours = time.hour;                                           // 时
    RTC_TimeStruct.Minutes = time.minute;                                       // 分
    RTC_TimeStruct.Seconds = time.second;                                       // 秒
    RTC_TimeStruct.TimeFormat = RTC_HOURFORMAT_24;                              // 24小时制
    RTC_TimeStruct.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;                    // 不使用夏令时
    RTC_TimeStruct.StoreOperation = RTC_STOREOPERATION_RESET;                   // RTC不支持自动重置
    HAL_RTC_SetTime(&g_rtc_handle, &RTC_TimeStruct, RTC_FORMAT_BIN); 
}

  RTC 设置日期函数:

/**
 * @brief RTC设置日期函数
 * 
 * @param date Date,日期的结构体
 */
void RTC_SetDate(Date date)
{
    RTC_DateTypeDef RTC_DateStruct = {0};

    RTC_DateStruct.Year = date.year - 2000;                                     // 年
    RTC_DateStruct.Month = date.month;                                          // 月
    RTC_DateStruct.Date = date.day;                                             // 日
    RTC_DateStruct.WeekDay = date.weekDay;                                      // 周
    HAL_RTC_SetDate(&g_rtc_handle, &RTC_DateStruct, RTC_FORMAT_BIN);
}

  RTC 设置日期时间函数:

/**
 * @brief RTC设置日期时间函数
 * 
 * @param dateTime DateTime,日期时间的结构体
 */
void RTC_SetDateTime(DateTime dateTime)
{
    RTC_SetTime(dateTime.time);
    RTC_SetDate(dateTime.date);
}

  RTC 设置闹钟函数:

/**
 * @brief RTC设置闹钟函数
 * 
 * @param dateTime 
 */
void RTC_SetAlarm(DateTime dateTime)
{
    RTC_AlarmTypeDef RTC_AlarmStruct = {0};

    RTC_AlarmStruct.AlarmTime.Hours = dateTime.time.hour;                       // 时
    RTC_AlarmStruct.AlarmTime.Minutes = dateTime.time.minute;                   // 分
    RTC_AlarmStruct.AlarmTime.Seconds = dateTime.time.second;                   // 秒
    RTC_AlarmStruct.AlarmTime.SubSeconds = 0;                                   // 亚秒
    RTC_AlarmStruct.AlarmTime.TimeFormat = RTC_HOURFORMAT_24;                   // 24小时制

    RTC_AlarmStruct.AlarmDateWeekDay = dateTime.date.weekDay;                   // 星期

    RTC_AlarmStruct.AlarmMask = RTC_ALARMMASK_NONE;                             // 精确匹配星期,时分秒
    RTC_AlarmStruct.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_NONE;
    RTC_AlarmStruct.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_WEEKDAY;      // 按星期

    RTC_AlarmStruct.Alarm = RTC_ALARM_A;                                        // 闹钟A
    HAL_RTC_SetAlarm_IT(&g_rtc_handle, &RTC_AlarmStruct, RTC_FORMAT_BIN);
  
    HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
}

  RTC 闹钟中断服务函数:

/**
 * @brief RTC闹钟中断服务函数
 * 
 */
void RTC_Alarm_IRQHandler(void)
{
    if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF) != 0U)
    {
        HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_RESET);

        __HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF);
    }
}

  RTC 周期性唤醒函数:

/**
 * @brief RTC周期性唤醒函数
 * 
 * @param clock RTC时钟频率,可选值:
 *                              RTC_WAKEUPCLOCK_RTCCLK_DIV1x ,x可取[2, 4, 8, 16]
 *                              RTC_WAKEUPCLOCK_CK_SPRE_16BITS
 *                              RTC_WAKEUPCLOCK_CK_SPRE_16BITS
 * @param count 自动重装载值,减到0,产生中断
 */
void RTC_SetWakeUp(uint8_t clock, uint16_t count)
{ 
    __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_WUTF);             // 清除RTC WAKE UP的标志

    HAL_RTCEx_SetWakeUpTimer_IT(&g_rtc_handle, count, clock);                   // 设置重装载值和时钟

    HAL_NVIC_SetPriority(RTC_WKUP_IRQn, 3, 0);
    HAL_NVIC_EnableIRQ(RTC_WKUP_IRQn);
}

  RTC 周期性唤醒服务函数:

/**
 * @brief RTC周期性唤醒服务函数
 * 
 */
void RTC_WKUP_IRQHandler(void)
{
    if (__HAL_RTC_WAKEUPTIMER_GET_FLAG(&g_rtc_handle, RTC_FLAG_WUTF) != 0U)
    {
        HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
        /* Clear the Wakeup timer interrupt pending bit */
        __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_WUTF);
    }

    /* Clear the EXTI's line Flag for RTC WakeUpTimer */
    __HAL_RTC_WAKEUPTIMER_EXTI_CLEAR_FLAG();

    /* Change RTC state */
    g_rtc_handle.State = HAL_RTC_STATE_READY;
}

  main() 函数:

int main(void)
{
    DateTime dateTime = {{2024, 3, 18, 1}, {15, 30, 30}};
    RTC_DateTypeDef RTC_DateStruct = {0};
    RTC_TimeTypeDef RTC_TimeStruct = {0};

    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

    UART_Init(&g_usart1_handle, USART1, 115200);
    RTC_Init(dateTime);

    LED_Init();

    dateTime.time.hour = 16;
    dateTime.time.minute = 30;
    dateTime.time.second = 30;
    RTC_SetAlarm(dateTime);

    RTC_SetWakeUp(RTC_WAKEUPCLOCK_CK_SPRE_16BITS, 0);

    while (1)
    {
        HAL_RTC_GetTime(&g_rtc_handle, &RTC_TimeStruct, RTC_FORMAT_BIN);
        HAL_RTC_GetDate(&g_rtc_handle, &RTC_DateStruct, RTC_FORMAT_BIN);
  
        printf("Date: 20%02d-%02d-%02d ", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date);
        printf("Time: %2d:%02d:%02d\n", RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes, RTC_TimeStruct.Seconds);
        HAL_Delay(1000);
    }
  
    return 0;
}

当我们使用 HAL 库提供的函数获取日期和时间时,要先获取时间,再获取日期(一定要获取日期),不然 RTC 时钟可能不会跑。

posted @ 2023-12-24 21:12  星光樱梦  阅读(18)  评论(0编辑  收藏  举报