STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟A/B事件和备份寄存器
读者可访问 GitHub - lc-guo/STM32CubeMX-Series-Tutorial 获取原始工程代码
1、准备材料
STM32CubeMX软件(Version 6.10.0)
keil µVision5 IDE(MDK-Arm)
2、实验目标
使用STM32CubeMX软件配置STM32F407开发板实现RTC周期唤醒、闹钟A/B事件功能,具体为在周期唤醒时利用串口输出当前RTC记录时间,当闹钟A/B事件发生时利用串口输出闹钟A/B事件发生提示
3、实验流程
3.0、前提知识
RTC的时钟可以由外部低速时钟LSE、外部高速时钟HSE经过2-31分频和内部RC振荡LSI三种时钟来源提供,但是一般我们都选择使用32.768kHz的LSE作为RTC的时钟源,因为32.768kHz的时钟频率可以经过128次分频,然后再经过256次分频得到一个较为精确的1Hz信号,此信号1s脉动一次,可以方便的用于更新日历,如下图所示 (注释1)
另外RTC还有两个可编程的闹钟A/B,如果设置了闹钟A/B的时间,则闹钟A/B设定时间会和当前日历时间对比,如果时间相等,会产生ALRA/BF事件
周期唤醒可以使用RTC内部一个16位唤醒自动重载寄存器来实现,周期唤醒的时钟信号可以来自于更新日历的1Hz(ck_spre)信号,也可以使用RTC时钟的2/4/8/16分频后的时钟,设置该自动重载寄存器的值,根据时钟频率向上计数,当计数溢出时发生周期唤醒事件
闹钟A/B,周期唤醒产生的 ALRAF、 ALRBF和WUTF事件均可以输出到复用引脚RTC_AF1(PC13)
STM32F407的RTC还有20个32位的备份寄存器,其名字从RTC_BKP_DR0到RTC_BKP_DR19,定义在stm32f4xx_hal_rtc_ex.h文件中,RTC和备份寄存器均由单片机的备用电源VBAT提供,主电源VDD/VDDA断开不影响备份寄存器内容存储及RTC的正常运行
3.1、CubeMX相关配置
3.1.0、工程基本配置
打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示
开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示
详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立”
3.1.1 、时钟树配置
本文实验中RTC时钟信号源选择为外部32.768kHz的低速时钟LSE,首先需要在Pinout & Configuration页面左边System Core/RCC中将原来Disable状态的Low Speed Clock(LSE)选择为Crystal/Ceramic Resonator,表示外部低速时钟LSE由32.768kHz的晶振提供,如下图所示
然后还是在这个页面,在Timers/RTC中单击Activate Clock Source,激活时钟源之后才可以对Clock Configuration页面的时钟修改,如下图所示
最后在Clock Configuration页面将输出到RTC时钟的时钟源选择为LSE,此时就已经配置好了RTC的输入时钟为32.768kHz的LSE,如下图所示
3.1.2、外设参数配置
本实验需要需要初始化USART1作为信息输出渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”
单击Pinout & Configuration页面左边Timers/RTC,在该页面中间单击Activate Calendar激活日历
这里Alarm A、Alarm B和WakeUp均有Disable、Internal Alarm/WakeUp 和 Routed to AF1三个选项,分别表示不使用、单纯内部使用和输出到复用引脚AF1(PC13)
注意由于AF1只有一个所以一旦某一个选择输出到了复用引脚AF1,其他便不可以设置,具体配置如下图所示
然后对启用的日历、Alarm A、Alarm B和WakeUp参数做不同的配置,这里比较通俗易懂,具体配置请看下图
3.1.3 、外设中断配置
在Pinout & Configuration页面左边System Core/NVIC中勾选闹钟A/B中断及周期唤醒中断,然后选择合适的中断优先级即可,另外串口中断可以不打开,本节实验输出采用阻塞传输数据的方式输出RTC时间
3.2、生成代码
3.2.0、配置Project Manager页面
单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示
详细Project Manager配置内容读者可以阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节
3.2.1、外设初始化函数调用流程
主函数中调用MX_RTC_Init()函数对RTC基本参数及日历时间、日历日期、闹钟A定时时间、闹钟B定时时间和周期唤醒等参数初始化/使能
在初始化RTC的函数HAL_RTC_Init()中调用了HAL_RTC_MspInit()函数完成了对RTC时钟使能,NVIC使能,NVIC优先级设置
如下图所示为上述的函数调用流程
3.2.2、外设中断函数调用流程
在stm32f4xx_it.c文件中新增了周期唤醒中断服务函数RTC_WKUP_IRQHandler()
在该RTC_WKUP_IRQHandler()函数中调用了HAL_RTCEx_WakeUpTimerIRQHandler()函数处理周期回调事件
最终调用了虚函数HAL_RTCEx_WakeUpTimerEventCallback(),该函数需要用户重新实现
如下图所示为周期唤醒中断函数调用流程
同时在stm32f4xx_it.c文件中新增了RTC闹钟A/B事件中断服务函数RTC_Alarm_IRQHandler()
在该RTC_Alarm_IRQHandler()函数中调用了HAL_RTC_AlarmIRQHandler()函数处理闹钟A/B事件
最后在该函数中调用了虚函数HAL_RTC_AlarmAEventCallback()处理闹钟A事件,调用虚函数HAL_RTCEx_AlarmBEventCallback()处理闹钟B事件
如下图所示为RTC闹钟A/B事件中断函数调用流程
3.2.3、添加其他必要代码
重新实现周期唤醒中断回调函数HAL_RTCEx_WakeUpTimerEventCallback()在rtc.c中,具体实现代码如下图所示
源代码如下
/*周期唤醒回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
if(HAL_RTC_GetTime(hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
{
HAL_RTC_GetDate(hrtc, &sDate, RTC_FORMAT_BIN);
char str[22];
sprintf(str,"RTC Time= %2d:%2d:%2d\r\n",sTime.Hours,sTime.Minutes,sTime.Seconds);
printf("%s", str);
}
HAL_GPIO_TogglePin(RED_LED_GPIO_Port,RED_LED_Pin);
}
重新实现闹钟A/B事件中断回调函数HAL_RTC_AlarmAEventCallback()和HAL_RTCEx_AlarmBEventCallback()在rtc.c中,具体代码如下所示
源代码如下
/*闹钟A事件回调函数*/
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
char infoA[]="Alarm A(xx:xx:15) trigger: \r\n";
printf("%s", infoA);
HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port,GREEN_LED_Pin);
}
/*闹钟B事件回调函数*/
void HAL_RTCEx_AlarmBEventCallback(RTC_HandleTypeDef *hrtc)
{
char infoB[]="Alarm B(xx:0:30) trigger: \r\n";
printf("%s", infoB);
HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port,GREEN_LED_Pin);
}
此时的代码可以正常运行,但存在一个问题,复位后重新执行RTC初始化函数会对RTC时间强制初始化为0:0:0,日期也会强制初始化,而我们想要设定的是当我们需要其初始化时就初始化,当一次初始化完毕之后,我不希望每次单片机复位时重新初始化
因此我们可以通过上述介绍的备份寄存器修复此BUG,我们在RTC通用初始化结束之后,RTC日期和时间初始化之前处,添加判断RTC备份寄存器是否已被写入1来决定是否需要初始化时间和日期,如果已被写入1,则表示之前已完成日期和时间初始化,不需要再次重新初始化,因此启动周期唤醒后直接退出函数,如下图代码所示
源代码如下
//读取备份寄存R0
uint32_t iniRTC=HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
//非零
if((iniRTC & 0x01))
{
//使能周期唤醒
if(HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 0, RTC_WAKEUPCLOCK_CK_SPRE_16BITS) != HAL_OK)
Error_Handler();
//提前退出函数,不初始化时间和日期
return;
}
何时改变/写入RTC备份寄存器中的值呢?
这里笔者使用按键来控制,当按下WK_UP按键时,就翻转备份寄存器RTC_BKP_DR0中存储的值,也就是说按下一次WK_UP按键,备份寄存器RTC_BKP_DR0中的值会在0/1之间改变,如下图所示为主循环中的按键扫描程序
源代码如下
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == 1)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == 1)
{
uint32_t iniRTC = HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR0);
iniRTC = !iniRTC;
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR0, iniRTC);
printf("Write RTC_BKP_DR0 %d\r\n", iniRTC);
while(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin));
}
}
4、常用函数
/*RTC周期回调中断服务函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
/*RTC闹钟A中断服务函数*/
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
/*RTC闹钟B中断服务函数*/
void HAL_RTCEx_AlarmBEventCallback(RTC_HandleTypeDef *hrtc)
/*查询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备份寄存器的值*/
uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister)
/*写RTC备份寄存器的值*/
void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data)
5、烧录验证
烧录程序,通过串口助手观察串口输出信息,每隔1秒,串口助手收到开发板传来的RTC时间信息,并且红色LED每一秒状态翻转一次
当时间到达0:0:15时,闹钟A触发,此时绿色LED灯状态翻转被点亮
随着时间继续流逝,当时间到达0:0:30时,闹钟B触发,此时此时绿色LED灯状态翻转被熄灭
此后每分钟的第15秒闹钟A会触发一次,每小时的0分30秒闹钟B会触发一次,具体串口输出信息如下图所示
按下WK_UP按键可以翻转备份寄存器RTC_BKP_DR0内存储的值,当备份寄存器RTC_BKP_DR0的值为1时,复位之后RTC的时间不会重置为0;
而当备份寄存器RTC_BKP_DR0的值为0时,复位之后RTC的时间会被重新初始化为0:0:0,串口输出信息如下图所示
为什么上图中RTC时间为0:0:15时刻的闹钟A没有响应?
这是因为备份寄存器RTC_BKP_DR0的值为1时,我们在MX_RTC_Init初始化函数中初始化完毕RTC之后直接启动了周期唤醒然后整个函数就退出了,并没有对RTC的闹钟A/B进行初始化,如果你想兼顾两者功能,也可以编写程序不直接退出,而是绕过RTC时间和日期赋初值的代码,然后执行RTC的闹钟A/B的初始化
6、注释详解
注释1:图片来源STM32F4xx中文参考手册 RM0090