• STM32实战总结:HAL之RTC


    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。

    为什么其晶振频率是 32.768Khz 呢?因为2的15次方就是32768。在分频15次后就是1Hz,周期为1s。同时这个参数也是工程师总结的,时钟最为准确。

    (在主电源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

    生成代码即可。

    这里不需要中断,包括秒中断、溢出中断、闹钟中断。

    其他场景根据需要自行选择。

    关于日期无法更新的问题

    F1的RTC中,日期是无法在断电时保持更新的,因为没有日期寄存器。

    那么,日期是咋来的?

    而且,我在RTC框图中就看到了计数器,那么,年月日时分秒到底是咋来的?

    难道要获取计数值后自己硬算吗?

    通过查找资料,关于F1的RTC时间实现,进行如下记录。

    首先,了解时间戳的概念。

    其他领域不太清楚,在计算机领域,时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。貌似还有其他的标准起始点,但不管是哪种标准起始点,只要有个标准起始点即可。

    通常,计算特定的时间就是根据时间戳来转换的,比如,当时间戳是7200,表示自格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在已经过了7200秒,那么此时的时间就是北京时间1970年01月01日10时00分00秒。其他更多的时间戳换算同理。如果需要格式化则处理对应的格式即可。

    在STM32中,只有一个32位的计数器,又因为预分频后的周期通常都是1秒,所以计数器里的数,实际上就是一种时间戳。

    不像更高级的STM32单片机,比如STM32F4就有专用的时间寄存器和日期寄存器。

    而F1系列只有一个计数器。

    按照我的理解,通过时间戳直接计算出年月日和时分秒应该是可以的,不过应该是过程非常复杂,里面涉及到各种闰月闰年,日期28/29等,所以,HAL库中(不知道标准库是不是)是将日历和时间分开来实现的。

    首先要知道,计数器里存的是时间戳。在HAL库中,只将时间部分的时间戳存在了计数器寄存器中,而日历部分的时间处理是存在了一个结构体中。

    STM32CubeMX生成的HAL库中RTC函数中,HAL_RTC_SetDate 日期设置函数只是将日期保存至hrtc结构体变量中,HAL_RTC_GetDate 日期获取函数也是直接从hrt结构体变量中获取日期数据。这种情况下,系统重启后数据丢失,日期会被重置。

    STM32CubeMX生成的HAL库中RTC函数中,HAL_RTC_SetTime 时间设置函数是将时间转换为时间戳保存到计数器中,HAL_RTC_GetTime 时间获取函数则是将计数器的数值转换为时间。计数器在断电后能够依靠外部电池供电继续计时,所以掉电后,时间依然能够正常走时。但是,在时间戳与时间的换算处理中,会自动将时间戳转换为24H内的时间,所以并不能记录所走时长的天数。比如当时间计数值超过了一天,软件处理中会将继续增长的计数值换算成从0开始的时间。

    那么,怎么实现日期的更新呢?

    对于实现RTC掉电复位后更新日期现在使用比较广的是如下两种方式。

    第一种:

    使用备份寄存器,把日期信息存储在备份寄存器中,因为RTC模块属于后备区域,所以备份寄存器中的信息在复位后也是维持不变的。在下次上电后读取寄存器数据就可以知道日期了。但这个方式也有一种缺陷,日期虽然写在了备份寄存器中,但是这个部分没有计数,寄存器内的日期也无法进行更新。

    第二种:

    就是将日期和时间一起换算为时间戳保存在计数器中。

    此时,需要自己写函数来实现日期和时间的设置以及读取,无法再使用HAL库自带的函数。实现过程较为复杂。此处不赘述,需要可自行查阅相关资料。

    有些集成的时间芯片,可以直接通过接口读年月日时分秒,那是因为芯片内部已经把计数器的计数值进行了处理。并将年月日的值分别存放在了特定的存储单元,这时直接读取就可以了。 就比如开头链接中讲的DS1302,通过SPI接口去读芯片中的相应寄存器,直接得到年月日时分秒。

    相关函数

    1. /* RTC Time and Date functions ************************************************/
    2. /** @addtogroup RTC_Exported_Functions_Group2
    3. * @{
    4. */
    5. HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
    6. HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
    7. HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);
    8. HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);
    9. /**
    10. * @}
    11. */
    12. /* RTC Alarm functions ********************************************************/
    13. /** @addtogroup RTC_Exported_Functions_Group3
    14. * @{
    15. */
    16. HAL_StatusTypeDef HAL_RTC_SetAlarm(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format);
    17. HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format);
    18. HAL_StatusTypeDef HAL_RTC_DeactivateAlarm(RTC_HandleTypeDef *hrtc, uint32_t Alarm);
    19. HAL_StatusTypeDef HAL_RTC_GetAlarm(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Alarm, uint32_t Format);
    20. void HAL_RTC_AlarmIRQHandler(RTC_HandleTypeDef *hrtc);
    21. HAL_StatusTypeDef HAL_RTC_PollForAlarmAEvent(RTC_HandleTypeDef *hrtc, uint32_t Timeout);
    22. void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc);
    23. /**
    24. * @}
    25. */

    数据格式通常有BCD码和二进制码格式,可根据需要选择。

    另外,还有可能用到备份区域的读写函数,因为备份区域断电后数据依然保存,所以可以将某些关键信息(比如日历)存入。

    1. /** @addtogroup RTCEx_Exported_Functions_Group3
    2. * @{
    3. */
    4. void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data);
    5. uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister);

    里面的参数啥意思?

    第二个参数:

    第三个参数:要存入的数据

    关键代码

    1. /* Includes ------------------------------------------------------------------*/
    2. #include "MyApplication.h"
    3. /* Private define-------------------------------------------------------------*/
    4. /* Private variables----------------------------------------------------------*/
    5. RTC_TimeTypeDef RTC_TimeStruct_CurrentValue; //RTC当前时间
    6. RTC_DateTypeDef RTC_DateStruct_CurrentValue; //RTC当前日期
    7. uint8_t *Week_Str[7] =
    8. {
    9. (uint8_t*)"日",
    10. (uint8_t*)"一",
    11. (uint8_t*)"二",
    12. (uint8_t*)"三",
    13. (uint8_t*)"四",
    14. (uint8_t*)"五",
    15. (uint8_t*)"六"
    16. };
    17. /* Private function prototypes------------------------------------------------*/
    18. static void Calendar_Set(void); //设置日历
    19. static void Calendar_Get(void); //获取日历
    20. static void Calendar_Show(void); //显示日历
    21. static uint8_t Input_RTC_SetValue(uint8_t); //输入RTC设置值
    22. static void RTC_Time_Set(void); //RTC时间设置
    23. static void RTC_Date_Set(void); //RTC日期设置
    24. /* Public variables-----------------------------------------------------------*/
    25. MyRTC_t MyRTC =
    26. {
    27. TRUE,
    28. &RTC_TimeStruct_CurrentValue,
    29. &RTC_DateStruct_CurrentValue,
    30. Calendar_Set,
    31. Calendar_Get,
    32. Calendar_Show
    33. };
    34. /*
    35. * @name Calendar_Set
    36. * @brief 设置日历
    37. * @param None
    38. * @retval None
    39. */
    40. static void Calendar_Set()
    41. {
    42. //上电复位时,读取RTC备份寄存器1的数据,如果为0x1688,则不需要通过串口重新设置日期与时间
    43. if(HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1) != 0x1688)
    44. {
    45. printf("^_^^_^开始设置RTC的日期与时间^_^^_^\r\n\r\n");
    46. RTC_Date_Set(); //设置日期
    47. RTC_Time_Set(); //设置时间
    48. HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR1,0x1688);
    49. }
    50. else
    51. {
    52. printf("^_^^_^RTC的日期与时间已设置!^_^^_^\r\n\r\n");
    53. printf("重新设置的方法如下:\r\n");
    54. printf("方法一:长按触摸按键2s以上;\r\n");
    55. printf("方法二:系统断电,同时拔掉RTC电池。\r\n\r\n");
    56. }
    57. }
    58. /*
    59. * @name Calendar_Get
    60. * @brief 显示日历
    61. * @param None
    62. * @retval None
    63. */
    64. static void Calendar_Get()
    65. {
    66. //获取当前时间
    67. HAL_RTC_GetTime(&hrtc,MyRTC.pRTC_TimeStruct,RTC_FORMAT_BIN);
    68. //获取当前日期
    69. HAL_RTC_GetDate(&hrtc,MyRTC.pRTC_DateStruct,RTC_FORMAT_BIN);
    70. }
    71. /*
    72. * @name Calendar_Show
    73. * @brief 显示日历
    74. * @param None
    75. * @retval None
    76. */
    77. static void Calendar_Show()
    78. {
    79. //串口打印日期
    80. printf("当前时间为: %02u年%02d月%02d日(星期%s) ", 2000+MyRTC.pRTC_DateStruct->Year,MyRTC.pRTC_DateStruct->Month,MyRTC.pRTC_DateStruct->Date,Week_Str[MyRTC.pRTC_DateStruct->WeekDay]);
    81. //串口打印时间
    82. printf("%02u:%02u:%02u\r\n",MyRTC.pRTC_TimeStruct->Hours,MyRTC.pRTC_TimeStruct->Minutes,MyRTC.pRTC_TimeStruct->Seconds);
    83. //数码管显示时间
    84. Display.Disp_HEX(Disp_NUM_6,MyRTC.pRTC_TimeStruct->Hours/10,Disp_DP_OFF);
    85. Display.Disp_HEX(Disp_NUM_5,MyRTC.pRTC_TimeStruct->Hours%10,Disp_DP_ON);
    86. Display.Disp_HEX(Disp_NUM_4,MyRTC.pRTC_TimeStruct->Minutes/10,Disp_DP_OFF);
    87. Display.Disp_HEX(Disp_NUM_3,MyRTC.pRTC_TimeStruct->Minutes%10,Disp_DP_ON);
    88. Display.Disp_HEX(Disp_NUM_2,MyRTC.pRTC_TimeStruct->Seconds/10,Disp_DP_OFF);
    89. Display.Disp_HEX(Disp_NUM_1,MyRTC.pRTC_TimeStruct->Seconds%10,Disp_DP_OFF);
    90. }
    91. /*
    92. * @name Input_RTC_SetValue
    93. * @brief 输入RTC设置值
    94. * @param MAX_Value -> 输入最大值
    95. * @retval SetValue -> 返回输入字符对应的数值
    96. */
    97. static uint8_t Input_RTC_SetValue(uint8_t MAX_Value)
    98. {
    99. uint8_t SetValue = 0; //返回值
    100. uint8_t Value_Arr[2] = {0};
    101. uint8_t Index = 0;
    102. //以等待方式从串口接收2个有效字符
    103. while(Index < 2)
    104. {
    105. //等待接收串口数据
    106. Value_Arr[Index++] = getchar();
    107. //校验字符有效性
    108. if((Value_Arr[Index -1] < '0') || (Value_Arr[Index -1] > '9'))
    109. {
    110. printf("请输入 0 到 9 之间的数字 -->:\n");
    111. Index--;
    112. }
    113. }
    114. //接收到的2个字符转化为数值
    115. SetValue = (Value_Arr[0] - '0')*10 + (Value_Arr[1] - '0');
    116. //校验数值有效行
    117. if(SetValue > MAX_Value)
    118. {
    119. printf("请输入 0 到 %d 之间的数字\n", MAX_Value);
    120. SetValue = 0xFF; //SetValue设置为无效数据
    121. }
    122. //返回数据
    123. return SetValue;
    124. }
    125. /*
    126. * @name RTC_Date_Set
    127. * @brief RTC日期设置
    128. * @param None
    129. * @retval None
    130. */
    131. static void RTC_Date_Set()
    132. {
    133. RTC_DateTypeDef RTC_DateStruct_SetValue;
    134. uint8_t SetValue;
    135. printf("=========================日期设置==================\n");
    136. printf("请输入年份(00-99): 20\n");
    137. SetValue = 0xFF;
    138. while (SetValue == 0xFF)
    139. {
    140. SetValue = Input_RTC_SetValue(99);
    141. }
    142. printf("年份被设置为: 20%02u\n", SetValue);
    143. RTC_DateStruct_SetValue.Year = SetValue;
    144. printf("请输入月份(01-12): \n");
    145. SetValue = 0xFF;
    146. while (SetValue == 0xFF)
    147. {
    148. SetValue = Input_RTC_SetValue(12);
    149. if(SetValue == 0x00)
    150. {
    151. printf("月份不能设置为0,请重新输入月份:\r\n");
    152. SetValue = 0xFF;
    153. }
    154. }
    155. printf("月份被设置为: %02u\n", SetValue);
    156. RTC_DateStruct_SetValue.Month = SetValue;
    157. printf("请输入日期(01-31): \n");
    158. SetValue = 0xFF;
    159. while (SetValue == 0xFF)
    160. {
    161. SetValue = Input_RTC_SetValue(31);
    162. if(SetValue == 0x00)
    163. {
    164. printf("日期不能设置为0,请重新输入日期:\r\n");
    165. SetValue = 0xFF;
    166. }
    167. }
    168. printf("日期被设置为: %02u\r\n", SetValue);
    169. RTC_DateStruct_SetValue.Date = SetValue;
    170. //设置日期
    171. HAL_RTC_SetDate(&hrtc,&RTC_DateStruct_SetValue,RTC_FORMAT_BIN);
    172. }
    173. /*
    174. * @name RTC_Time_Set
    175. * @brief RTC时间设置
    176. * @param None
    177. * @retval None
    178. */
    179. static void RTC_Time_Set()
    180. {
    181. RTC_TimeTypeDef RTC_TimeStruct_SetValue;
    182. uint8_t SetValue;
    183. printf("=========================时间设置==================\n");
    184. printf("请输入时钟(00-23): \n");
    185. SetValue = 0xFF;
    186. while (SetValue == 0xFF)
    187. {
    188. SetValue = Input_RTC_SetValue(23);
    189. }
    190. printf("时钟被设置为: %02u\n", SetValue);
    191. RTC_TimeStruct_SetValue.Hours = SetValue;
    192. printf("请输入分钟(00-59): \n");
    193. SetValue = 0xFF;
    194. while (SetValue == 0xFF)
    195. {
    196. SetValue = Input_RTC_SetValue(59);
    197. }
    198. printf("分钟被设置为: %02u\n", SetValue);
    199. RTC_TimeStruct_SetValue.Minutes = SetValue;
    200. printf("请输入秒钟(00-59): \n");
    201. SetValue = 0xFF;
    202. while (SetValue == 0xFF)
    203. {
    204. SetValue = Input_RTC_SetValue(59);
    205. }
    206. printf("秒钟被设置为: %02u\n", SetValue);
    207. RTC_TimeStruct_SetValue.Seconds = SetValue;
    208. //设置时间
    209. HAL_RTC_SetTime(&hrtc,&RTC_TimeStruct_SetValue,RTC_FORMAT_BIN);
    210. }
    211. /********************************************************
    212. End Of File
    213. ********************************************************/

    更直观的代码

    1. void User_Init(void)
    2. {
    3. RTC_TimeTypeDef startTime = {0};
    4. startTime.Hours = 0x21U;
    5. startTime.Minutes = 0x0U;
    6. startTime.Seconds = 0x0U;
    7. HAL_RTC_SetTime(&hrtc, &startTime, RTC_FORMAT_BCD);
    8. }
    9. void Co_Run(void)
    10. {
    11. RTC_TimeTypeDef realTime = {0};
    12. HAL_RTC_GetTime(&hrtc, &realTime, RTC_FORMAT_BCD);
    13. printf("%x:%x:%x",realTime.Hours, realTime.Minutes, realTime.Seconds);
    14. printf("\r\n");
    15. HAL_Delay(1000);
    16. }

    注意,如果输入是BCD码,就要直接输入BCD码;

    输出的时候,直接以十六进制输出,得到的就是BCD码结果。

    关于BCD码,具体见:

    51单片机内部外设:实时时钟(SPI)_spi时钟_路溪非溪的博客-CSDN博客

  • 相关阅读:
    一步步带你设计MySQL索引数据结构
    Servlet小结
    为什么AirtestIDE的selenium Window突然无法检索控件了?
    【Rust—LeetCode题解】1656. 设计有序流
    Ansible stat模块 stat模块 – 检索文件或文件系统状态
    在两个排序数组中找到上中位数
    python深拷贝和浅拷贝
    若依前后端分离版开源项目学习
    JavaScript防抖和节流(从认识到理解到手写)
    【5】MySQL数据库备份-XtraBackup - 全量备份
  • 原文地址:https://blog.csdn.net/qq_28576837/article/details/128023759