RTC基础知识参考:
51单片机内部外设:实时时钟(SPI)_路溪非溪的博客-CSDN博客
STM32中的RTC
51单片机通常是外置的RTC芯片如DS1302,那么STM32的RTC是什么情况呢?
STM32芯片自带RTC,因此不须像其他MCU需外接RTC模块。
先读一读单片机的数据手册。
实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。
RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。 系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操 作。执行以下操作将使能对后备寄存器和RTC的访问:
● 设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
● 设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。
主要特性:
● 可编程的预分频系数:分频系数高为220。
● 32位的可编程计数器,可用于较长时间段的测量。
● 2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟的频率必须小于PCLK1时钟 频率的四分之一以上)。
● 可以选择以下三种RTC的时钟源:
● HSE时钟除以128;
● LSE振荡器时钟;
● LSI振荡器时钟● 2个独立的复位类型:
● APB1接口由系统复位;
● RTC核心(预分频器、闹钟、计数器和分频器)只能由后备域复位● 3个专门的可屏蔽中断:
● 1.闹钟中断,用来产生一个软件可编程的闹钟中断。● 2.秒中断,用来产生一个可编程的周期性中断信号(长可达1秒)。
● 3.溢出中断,指示内部可编程计数器溢出并回转为0的状态。
RTC时钟源:
三种不同的时钟源可被用来驱动系统时钟(SYSCLK):● HSI振荡器时钟
● HSE振荡器时钟
● PLL时钟这些设备有以下2种二级时钟源:
● 40kHz低速内部RC,可以用于驱动独立看门狗和通过程序选择驱动RTC。 RTC用于从停机/待机模式下自动唤醒系统。
● 32.768kHz低速外部晶体也可用来通过程序选择驱动RTC(RTCCLK)。RTC原理框图
灰色区域为待机时维持供电区域。
APB1 接口:用来和 APB1 总线相连。
此单元还包含一组 16 位寄存器,可通过 APB1 总线对其进行读写操作。APB1 接口由 APB1 总线时钟驱动,用来与 APB1 总线连接。通过APB1接口可以访问RTC的相关寄存器(预分频值,计数器值,闹钟值)。
RTC 核心接口:由一组可编程计数器组成,分成两个主要模块 。
第一个模块是个RTC预分频器。
第二个模块是一个 32 位的可编程计数器 (RTC_CNT)可被初始化为当前的系统时间。
一个 32 位的时钟计数器,按秒钟计算,可以记录4294967296秒,约合136年左右,作为一般应用,这已经是足够了的。
RTC具体流程:
RTCCLK经过RTC_DIV预分频,RTC_PRL设置预分频系数,然后得到TR_CLK时钟信号,我们一般设置其周期为1s,RTC_CNT计数器计数,假如1970设置为时间起点为0s,通过当前时间的秒数计算得到当前的时间。RTC_ALR是设置闹钟时间,RTC_CNT计数到RTC_ALR就会产生计数中断。RTC_Second为秒中断,用于刷新时间。
RTC_Overflow是溢出中断。
RTC Alarm 控制开关机。断电后这三个中断不起作用。
RTC时钟选择
使用HSE分频时钟或者LSI的时候,在主电源VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证RTC正常工作。所以RTC一般都时钟低速外部时钟LSE,频率为实时时钟模块中常用的32.768KHz,因为32768 = 2^15,分频容易实现,所以被广泛应用到RTC模块。(在主电源VDD有效的情况下(待机),RTC还可以配置闹钟事件使STM32退出待机模式)RTC设备因为其独特的运行方式(即掉电依旧运行)使用HSE分频时钟或者LSI的时候,在主电源VDD掉电的情况下,这两个时钟来源都会受到影响,资源消耗太大,小小的纽扣电池根本吃不消。没法保证RTC正常工作.所以RTC一般都时钟低速外部时钟LSE。
RTC复位过程
除了RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器外,所有的系统寄存器都由系统复位或电源复位进行异步复位。
RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器仅能通过备份域复位信号复位。系统复位后,禁止访问后备寄存器和RCT,防止对后卫区域(BKP)的意外写操作。RTC中断
秒中断:
这里时钟自带一个秒中断,每当计数加一的时候就会触发一次秒中断。注意,这里所说的秒中断并非一定是一秒的时间,它是由RTC时钟源和分频值决定的“秒”的时间,当然也是可以做到1秒钟中断一次。我们通过往秒中断里写更新时间的函数来达到时间同步的效果。闹钟中断:
闹钟中断就是设置一个预设定的值,计数每自加多少次触发一次闹钟中断。另外要注意的是,RTC的后备区域是没有日期寄存器的。所以,在断电重启后,日期不会更新,只有时间是最新的。那么,怎么能获取到最新的日期呢?
第一种办法是使用更高级的单片机,比如F4系列,就自带了日期寄存器。
另外一种办法就是通过计数器来硬算。计算量很大。
实际中可根据情况来选择最适合的方式。
硬件电路
RTC时钟:
后备电池模块
MX配置
开启RTC
开启LSE
生成代码即可。
关键代码
/* Includes ------------------------------------------------------------------*/ #include "MyApplication.h" /* Private define-------------------------------------------------------------*/ /* Private variables----------------------------------------------------------*/ RTC_TimeTypeDef RTC_TimeStruct_CurrentValue; //RTC当前时间 RTC_DateTypeDef RTC_DateStruct_CurrentValue; //RTC当前日期 uint8_t *Week_Str[7] = { (uint8_t*)"日", (uint8_t*)"一", (uint8_t*)"二", (uint8_t*)"三", (uint8_t*)"四", (uint8_t*)"五", (uint8_t*)"六" }; /* Private function prototypes------------------------------------------------*/ static void Calendar_Set(void); //设置日历 static void Calendar_Get(void); //获取日历 static void Calendar_Show(void); //显示日历 static uint8_t Input_RTC_SetValue(uint8_t); //输入RTC设置值 static void RTC_Time_Set(void); //RTC时间设置 static void RTC_Date_Set(void); //RTC日期设置 /* Public variables-----------------------------------------------------------*/ MyRTC_t MyRTC = { TRUE, &RTC_TimeStruct_CurrentValue, &RTC_DateStruct_CurrentValue, Calendar_Set, Calendar_Get, Calendar_Show }; /* * @name Calendar_Set * @brief 设置日历 * @param None * @retval None */ static void Calendar_Set() { //上电复位时,读取RTC备份寄存器1的数据,如果为0x1688,则不需要通过串口重新设置日期与时间 if(HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1) != 0x1688) { printf("^_^^_^开始设置RTC的日期与时间^_^^_^\r\n\r\n"); RTC_Date_Set(); //设置日期 RTC_Time_Set(); //设置时间 HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR1,0x1688); } else { printf("^_^^_^RTC的日期与时间已设置!^_^^_^\r\n\r\n"); printf("重新设置的方法如下:\r\n"); printf("方法一:长按触摸按键2s以上;\r\n"); printf("方法二:系统断电,同时拔掉RTC电池。\r\n\r\n"); } } /* * @name Calendar_Get * @brief 显示日历 * @param None * @retval None */ static void Calendar_Get() { //获取当前时间 HAL_RTC_GetTime(&hrtc,MyRTC.pRTC_TimeStruct,RTC_FORMAT_BIN); //获取当前日期 HAL_RTC_GetDate(&hrtc,MyRTC.pRTC_DateStruct,RTC_FORMAT_BIN); } /* * @name Calendar_Show * @brief 显示日历 * @param None * @retval None */ static void Calendar_Show() { //串口打印日期 printf("当前时间为: %02u年%02d月%02d日(星期%s) ", 2000+MyRTC.pRTC_DateStruct->Year,MyRTC.pRTC_DateStruct->Month,MyRTC.pRTC_DateStruct->Date,Week_Str[MyRTC.pRTC_DateStruct->WeekDay]); //串口打印时间 printf("%02u:%02u:%02u\r\n",MyRTC.pRTC_TimeStruct->Hours,MyRTC.pRTC_TimeStruct->Minutes,MyRTC.pRTC_TimeStruct->Seconds); //数码管显示时间 Display.Disp_HEX(Disp_NUM_6,MyRTC.pRTC_TimeStruct->Hours/10,Disp_DP_OFF); Display.Disp_HEX(Disp_NUM_5,MyRTC.pRTC_TimeStruct->Hours%10,Disp_DP_ON); Display.Disp_HEX(Disp_NUM_4,MyRTC.pRTC_TimeStruct->Minutes/10,Disp_DP_OFF); Display.Disp_HEX(Disp_NUM_3,MyRTC.pRTC_TimeStruct->Minutes%10,Disp_DP_ON); Display.Disp_HEX(Disp_NUM_2,MyRTC.pRTC_TimeStruct->Seconds/10,Disp_DP_OFF); Display.Disp_HEX(Disp_NUM_1,MyRTC.pRTC_TimeStruct->Seconds%10,Disp_DP_OFF); } /* * @name Input_RTC_SetValue * @brief 输入RTC设置值 * @param MAX_Value -> 输入最大值 * @retval SetValue -> 返回输入字符对应的数值 */ static uint8_t Input_RTC_SetValue(uint8_t MAX_Value) { uint8_t SetValue = 0; //返回值 uint8_t Value_Arr[2] = {0}; uint8_t Index = 0; //以等待方式从串口接收2个有效字符 while(Index < 2) { //等待接收串口数据 Value_Arr[Index++] = getchar(); //校验字符有效性 if((Value_Arr[Index -1] < '0') || (Value_Arr[Index -1] > '9')) { printf("请输入 0 到 9 之间的数字 -->:\n"); Index--; } } //接收到的2个字符转化为数值 SetValue = (Value_Arr[0] - '0')*10 + (Value_Arr[1] - '0'); //校验数值有效行 if(SetValue > MAX_Value) { printf("请输入 0 到 %d 之间的数字\n", MAX_Value); SetValue = 0xFF; //SetValue设置为无效数据 } //返回数据 return SetValue; } /* * @name RTC_Date_Set * @brief RTC日期设置 * @param None * @retval None */ static void RTC_Date_Set() { RTC_DateTypeDef RTC_DateStruct_SetValue; uint8_t SetValue; printf("=========================日期设置==================\n"); printf("请输入年份(00-99): 20\n"); SetValue = 0xFF; while (SetValue == 0xFF) { SetValue = Input_RTC_SetValue(99); } printf("年份被设置为: 20%02u\n", SetValue); RTC_DateStruct_SetValue.Year = SetValue; printf("请输入月份(01-12): \n"); SetValue = 0xFF; while (SetValue == 0xFF) { SetValue = Input_RTC_SetValue(12); if(SetValue == 0x00) { printf("月份不能设置为0,请重新输入月份:\r\n"); SetValue = 0xFF; } } printf("月份被设置为: %02u\n", SetValue); RTC_DateStruct_SetValue.Month = SetValue; printf("请输入日期(01-31): \n"); SetValue = 0xFF; while (SetValue == 0xFF) { SetValue = Input_RTC_SetValue(31); if(SetValue == 0x00) { printf("日期不能设置为0,请重新输入日期:\r\n"); SetValue = 0xFF; } } printf("日期被设置为: %02u\r\n", SetValue); RTC_DateStruct_SetValue.Date = SetValue; //设置日期 HAL_RTC_SetDate(&hrtc,&RTC_DateStruct_SetValue,RTC_FORMAT_BIN); } /* * @name RTC_Time_Set * @brief RTC时间设置 * @param None * @retval None */ static void RTC_Time_Set() { RTC_TimeTypeDef RTC_TimeStruct_SetValue; uint8_t SetValue; printf("=========================时间设置==================\n"); printf("请输入时钟(00-23): \n"); SetValue = 0xFF; while (SetValue == 0xFF) { SetValue = Input_RTC_SetValue(23); } printf("时钟被设置为: %02u\n", SetValue); RTC_TimeStruct_SetValue.Hours = SetValue; printf("请输入分钟(00-59): \n"); SetValue = 0xFF; while (SetValue == 0xFF) { SetValue = Input_RTC_SetValue(59); } printf("分钟被设置为: %02u\n", SetValue); RTC_TimeStruct_SetValue.Minutes = SetValue; printf("请输入秒钟(00-59): \n"); SetValue = 0xFF; while (SetValue == 0xFF) { SetValue = Input_RTC_SetValue(59); } printf("秒钟被设置为: %02u\n", SetValue); RTC_TimeStruct_SetValue.Seconds = SetValue; //设置时间 HAL_RTC_SetTime(&hrtc,&RTC_TimeStruct_SetValue,RTC_FORMAT_BIN); } /******************************************************** End Of File ********************************************************/