什么是RTC设备?
RTC设备即real time clock的缩写,是一种掉电也能继续计时的计时器。虽然它只有简单的计时和触发中断的功能,但它掉电也能继续运行则让它的价值瞬间上升了无数倍。
CUBEmx配置
RTC设备因为其独特的运行方式(即掉电依旧运行)导致它不能使用HSE或者HSI进行分频,否则资源消耗太大,小小的纽扣电池根本吃不消。所以我们可以选择单片机内部的LSI或者使用外部晶振LSE,推荐使用外部晶振LSE,因为单片机内部的LSI容易受到电压以及温度的影响导致精度不足。
以上为基本的一些设置。
以上分别是设置中断和设置系统日期。
因为接下来的例程会使用的串口和led灯所以要在cube中配置一下。
HAL库中有关RTC设备的API讲解
在编程之前我们需要先了解一下我们一会用到的api:
/*设置系统时间*/ HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format) /*读取系统时间*/ HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format) /*设置系统日期*/ HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format) /*读取系统日期*/ HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format) /*启动报警功能*/ HAL_StatusTypeDef HAL_RTC_SetAlarm(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format) /*设置报警中断*/ HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format) /*报警时间回调函数*/ __weak void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) /*写入后备储存器*/ void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data) /*读取后备储存器*/ uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister
代码实战
接下来的代码会演示如何实时显示时间,设置rtc报警中断并启用中断,以及解决rtc掉电重启时间会重置的问题。
cubemx会自动帮我们生成初始化代码:
#include "rtc.h" /* USER CODE BEGIN 0 */ /* USER CODE END 0 */ RTC_HandleTypeDef hrtc; /* RTC 初始化函数 */ void MX_RTC_Init(void) { RTC_TimeTypeDef sTime = {0}; RTC_DateTypeDef DateToUpdate = {0}; /** 初始化RTC */ hrtc.Instance = RTC; hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND; hrtc.Init.OutPut = RTC_OUTPUTSOURCE_NONE; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN Check_RTC_BKUP */ /* USER CODE END Check_RTC_BKUP */ /** 设置RTC系统时间以及日期 */ sTime.Hours = 19; sTime.Minutes = 29; sTime.Seconds = 0; if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } DateToUpdate.WeekDay = RTC_WEEKDAY_SUNDAY; DateToUpdate.Month = RTC_MONTH_APRIL; DateToUpdate.Date = 4; DateToUpdate.Year = 20; if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } } void HAL_RTC_MspInit(RTC_HandleTypeDef* rtcHandle) { if(rtcHandle->Instance==RTC) { /* USER CODE BEGIN RTC_MspInit 0 */ /* USER CODE END RTC_MspInit 0 */ HAL_PWR_EnableBkUpAccess(); /* Enable BKP CLK enable for backup registers */ __HAL_RCC_BKP_CLK_ENABLE(); /* RTC clock enable */ __HAL_RCC_RTC_ENABLE(); /* RTC interrupt Init */ HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0); HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn); /* USER CODE BEGIN RTC_MspInit 1 */ /* USER CODE END RTC_MspInit 1 */ } } void HAL_RTC_MspDeInit(RTC_HandleTypeDef* rtcHandle) { if(rtcHandle->Instance==RTC) { /* USER CODE BEGIN RTC_MspDeInit 0 */ /* USER CODE END RTC_MspDeInit 0 */ /* Peripheral clock disable */ __HAL_RCC_RTC_DISABLE(); /* RTC interrupt Deinit */ HAL_NVIC_DisableIRQ(RTC_IRQn); HAL_NVIC_DisableIRQ(RTC_Alarm_IRQn); /* USER CODE BEGIN RTC_MspDeInit 1 */ /* USER CODE END RTC_MspDeInit 1 */ } } /* USER CODE BEGIN 1 */ /* USER CODE END 1 */
我已经把原本的英文注释翻译成了中文注释,简单的来说上面这些代码就是完成了RTC设备的初始化以及设置系统时间,并且初始化了中断。
接下来完成通过串口实时显示时间的功能:
首先为了方便接下来的使用我们需要把下面这个结构体变量转换为全局变量
RTC_TimeTypeDef sTime = {0};
#include "rtc.h" /* USER CODE BEGIN 0 */ RTC_TimeTypeDef sTime = {0}; /* USER CODE END 0 */ RTC_HandleTypeDef hrtc;
然后点开我们的main函数,在while循环中读取当前时间,并且每秒通过串口传输到电脑上
#include "main.h" #include "rtc.h" #include "usart.h" #include "gpio.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "stdio.h" /* USER CODE END Includes */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ int fputc(int ch,FILE *f){ uint8_t temp[1]={ch}; HAL_UART_Transmit(&huart1,temp,1,2); return ch; } /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ extern RTC_TimeTypeDef sTime; /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_RTC_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); printf("%02d:",sTime.Hours); printf("%02d:",sTime.Minutes); printf("%02d\n",sTime.Seconds); HAL_Delay(1000); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
为了方便我们打印时间,我们改写了printf函数。
实现RTC报警中断功能:
首先在全局变量中定义结构体变量。
/* USER CODE BEGIN 0 */ RTC_TimeTypeDef sTime = {0}; RTC_AlarmTypeDef sAlarm ; /* USER CODE END 0 */
让我们来看一下RTC_AlarmTypeDef这个结构体里面都有些什么?
在这里我们又看到了一个结构体名称,有没有觉得这个名称在哪见过?
没错!在最开始初始化RTC系统时间时我们有见到过它
继续查看这个结构体的内容发现:
原来设置报警的时间和设置系统的时间所用的数据结构是一样的。
搞清楚了报警结构体后设置报警中断就很容易了:
RTC_AlarmTypeDef sAlarm ; void sAlarm_Config(int hours,int minutes,int seconds){ /*填写报警结构体变量*/ sAlarm.Alarm=RTC_ALARM_A; sAlarm.AlarmTime.Hours=hours; sAlarm.AlarmTime.Minutes=minutes; sAlarm.AlarmTime.Seconds=seconds; HAL_RTC_SetAlarm(&hrtc,&sAlarm, RTC_FORMAT_BIN); //开启中断功能 HAL_RTC_SetAlarm_IT(&hrtc,&sAlarm, RTC_FORMAT_BIN); //设置中断 }
然后写一下回调函数:
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc){ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); }
接下来去main函数中调用一下:
#include "main.h" #include "rtc.h" #include "usart.h" #include "gpio.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "stdio.h" /* USER CODE END Includes */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ int fputc(int ch,FILE *f){ uint8_t temp[1]={ch}; HAL_UART_Transmit(&huart1,temp,1,2); return ch; } void sAlarm_Config(int hours,int minutes,int seconds) //声明中断函数 /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ extern RTC_TimeTypeDef sTime; /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_RTC_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ void sAlarm_Config(19,30,0); //在1分钟后发送中断 /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); printf("%02d:",sTime.Hours); printf("%02d:",sTime.Minutes); printf("%02d\n",sTime.Seconds); HAL_Delay(1000); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE
当然,如果我们要完成的是在5小时候后发送中断我们可以把中断函数改写成这样:
void sAlarm_Config2(int hours,int minutes,int seconds){ HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); //获取设置中断时的时间 sAlarm.Alarm=RTC_ALARM_A; sAlarm.AlarmTime.Hours=hours+sTime.Hours; sAlarm.AlarmTime.Minutes=minutes+sTime.Minutes; sAlarm.AlarmTime.Seconds=seconds+sTime.Seconds; HAL_RTC_SetAlarm(&hrtc,&sAlarm, RTC_FORMAT_BIN); HAL_RTC_SetAlarm_IT(&hrtc,&sAlarm, RTC_FORMAT_BIN); }
在这里要注意一下,因为我们设置的是多少时间后进行中断,那么我们就要把当前时间加上多少我们要求的时间,所以要先获取一下当前时间是多少,如果不获取一下,结构体里面的变量就都是0。具体请看以下代码:
我们从HAL_RTC_GetTime()这个函数内容可以看出sTime结构体是通过RTC_ReadTimeCounter()这个函数读取了当前RTC设备下的RTC_CNT寄存器中的值。如果不读取的话,这个结构体里面的值就为0。
另外,我在看操作手册时看到了以下这段话:
我最开始以为就是操作手册中的这个原因,但是我分析一下后发现,应该不是这个原因,因为在这里应该是结构体中的变量没有被赋值导致的数据错误,而不是硬件上的问题,虽然结果都是0。如果有人发现我的分析是错误的话,非常欢迎在下面评论中指出我的错误。
好了言归正传,接下来我们还要解决掉电后,日期被重置的问题。
首先在创建如下函数:
void user_CheckRtcBkup(){ HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 1); }
然后在RTC初始化函数中加入这句:
/* USER CODE BEGIN Check_RTC_BKUP */ if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1)==1){ return; } /* USER CODE END Check_RTC_BKUP */
然后在main函数中调用一下:
int main(void) { /* USER CODE BEGIN 1 */ extern RTC_TimeTypeDef sTime; /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_RTC_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ user_CheckRtcBkup(); sAlarm_Config2(0,0,10); user_sendmessage(); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); printf("%02d:",sTime.Hours); printf("%02d:",sTime.Minutes); printf("%02d\n",sTime.Seconds); HAL_Delay(1000); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
这样我们就在初始化好RTC设备后,在后备寄存器中写入一个值1,这个后备寄存器在断电重启后值依旧不会被改变,所以在断电重启后再一次初始化时,RTC初始化函数中就不会把最开始设置的系统时间赋给sTime这个结构体。
*额外功能
当然我们在真实的开发中回调函数不大可能就单单只亮个灯,就比如我要在12小时后开启电磁阀,首先我要输出一个高电平(假设),然后在几秒钟后我们还要让电磁阀关闭,那我们相对的是不是还要输出一个低电平。以下代码完成了这部分的功能:
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *nhrtc){ if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0)==GPIO_PIN_SET){ HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); //翻转led灯的电平 sAlarm.AlarmTime.Hours=sTime.Hours; sAlarm.AlarmTime.Minutes=sTime.Minutes; sAlarm.AlarmTime.Seconds=sTime.Seconds+5; //设置报警时间为5秒后 HAL_RTC_SetAlarm_IT(&hrtc,&sAlarm,RTC_FORMAT_BIN); return; //这个return很关键,在设置完中断后要马上退出回调函数,否则led灯会在下个if中再次翻转 } if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0)==GPIO_PIN_RESET){ HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); return; } }
当然我这里还是用led灯来演示了,毕竟C语言入门hello world,单片机入门点亮led灯。具体思路就是首先检测电平,如果为高(我这块开发板低电平点亮led灯),就代表led灯是暗着的,那我们就翻转电平,并且设置中断时间为5秒后。设置完后直接退出当前回调函数,5秒后中断触发,这时电平由于是低电平那么我们就再次翻转一下,但是不再设置下一次中断。