跳到主要内容

【RTC】实时时钟 & 掉电走时

下载例程代码下载代码

注意

请一定按照 例程使用方法🔗 导入例程,否则下载的可能不是例程而是其他工程。

RTC 简介

RTC(Real Time Clock),即实时时钟,类似于钟表一般,能够持续记录时间,为程序提供精确的日期和时间信息,即使在断电期间也能确保准确运行。

原理和特点

  • 在STM32中,存在两个时钟源:高速时钟(8 MHz)和低速时钟(32.768 kHz)。高速时钟用于驱动CPU、外设和定时器等核心组件,而低速时钟则负责管理看门狗和RTC等功能。
  • RTC依赖低速时钟运行。
  • RTC模块内部包含了一个独立的32位寄存器来保存当前的时间戳信息。
  • 低速时钟以极低的功耗运行,即使在断电情况下,通过备用电源(如纽扣电池),RTC也能持续运行以确保时间准确性。

RTC的一般使用方法

  • 在CubeMX中找到Timers -> RTC,勾选Activate Clock Source,即可激活RTC时钟功能。
  • 即使学习板断电,RTC依然能够持续记录时间。
  • HAL库的RTC驱动未实现日期的断电走时功能,即断电后时间可以继续走时,但日期会重置。
  • keysking提供了RTC库,可以实现断电走时功能,具体代码见下文。
  • 需要获取当前日期和时间时,只需调用相应函数即可实现。

如何使用例程

下载程序,并连接硬件,即可看到效果

硬件连接

  • 使用配套TYPE-C数据线,将学习板连接到计算机
连接usb线

程序效果

串口输出信息

例程讲解

下面介绍了如何自己实现该例程的功能

1、工程配置

  • 开启外部晶振:在Pinout&Configuration -> System Core -> RCC 页面,将 High Speed Clock (HSE) 以及 Low Speed Clock (LSE) 都配置为 Crystal/Ceramic Resonator
配置��时钟源
  • 配置主时钟频率:在Clock Configuration 页面,将PLL Source 选择为 HSE,将System Clock Mux 选择为 PLLCLK,然后在HCLK (MHz) 输入72并回车,将HCLK频率配置为 72 MHz
时钟配置
  • 配置RTC时钟频率:在Clock Configuration 页面,将RTC时钟源选择为 LSE
配置RTC时钟源
  • 激活RTC:在Pinout&Configuration -> Timers -> RTC -> Mode,勾选 Activate Clock Source、Activate Calendar,以启用RTC时钟并激活日历功能。仅开启RTC时钟将仅记录时间,而不包括日期信息。
RTC配置
  • 打开串口2外设:Pinout&Configuration -> Connectivity -> USART2,将Mode选择为Asynchronous

2、代码

在工程的Core/Inc文件夹上右键,选择New -> File,创建kk_rtc.h文件,将以下代码粘贴到kk_rtc.h文件中

#ifndef INC_KK_RTC_H_
#define INC_KK_RTC_H_
#include "stm32f1xx_hal.h"
#include "rtc.h"
#include "time.h"

HAL_StatusTypeDef KK_RTC_SetTime(struct tm *time);
struct tm *KK_RTC_GetTime();
void KK_RTC_Init();

#endif /* INC_KK_RTC_H_ */

在工程的Core/Src文件夹上右键,选择New -> File,创建kk_rtc.c文件,将以下代码粘贴到kk_rtc.c文件中

#include "kk_rtc.h"

// RTC已经被初始化的值 记录在RTC_BKP_DR1中
#define RTC_INIT_FLAG 0x2333

/**
* @brief 进入RTC初始化模式
* @param hrtc 指向包含RTC配置信息的RTC_HandleTypeDef结构体的指针
* @retval HAL status
*/
static HAL_StatusTypeDef RTC_EnterInitMode(RTC_HandleTypeDef *hrtc)
{
uint32_t tickstart = 0U;

tickstart = HAL_GetTick();
/* 等待RTC处于INIT状态,如果到达Time out 则退出 */
while ((hrtc->Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET)
{
if ((HAL_GetTick() - tickstart) > RTC_TIMEOUT_VALUE)
{
return HAL_TIMEOUT;
}
}

/* 禁用RTC寄存器的写保护 */
__HAL_RTC_WRITEPROTECTION_DISABLE(hrtc);


return HAL_OK;
}

/**
* @brief 退出RTC初始化模式
* @param hrtc 指向包含RTC配置信息的RTC_HandleTypeDef结构体的指针
* @retval HAL status
*/
static HAL_StatusTypeDef RTC_ExitInitMode(RTC_HandleTypeDef *hrtc)
{
uint32_t tickstart = 0U;

/* 禁用RTC寄存器的写保护。 */
__HAL_RTC_WRITEPROTECTION_ENABLE(hrtc);

tickstart = HAL_GetTick();
/* 等到RTC处于INIT状态,如果到达Time out 则退出 */
while ((hrtc->Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET)
{
if ((HAL_GetTick() - tickstart) > RTC_TIMEOUT_VALUE)
{
return HAL_TIMEOUT;
}
}

return HAL_OK;
}

/**
* @brief 写入RTC_CNT寄存器中的时间计数器。
* @param hrtc 指向包含RTC配置信息的RTC_HandleTypeDef结构体的指针。
* @param TimeCounter: 写入RTC_CNT寄存器的计数器
* @retval HAL status
*/
static HAL_StatusTypeDef RTC_WriteTimeCounter(RTC_HandleTypeDef *hrtc, uint32_t TimeCounter)
{
HAL_StatusTypeDef status = HAL_OK;

/* 进入RTC初始化模式 */
if (RTC_EnterInitMode(hrtc) != HAL_OK)
{
status = HAL_ERROR;
}
else
{
/* 设置RTC计数器高位寄存器 */
WRITE_REG(hrtc->Instance->CNTH, (TimeCounter >> 16U));
/* 设置RTC计数器低位寄存器 */
WRITE_REG(hrtc->Instance->CNTL, (TimeCounter & RTC_CNTL_RTC_CNT));

/* 退出RTC初始化模式 */
if (RTC_ExitInitMode(hrtc) != HAL_OK)
{
status = HAL_ERROR;
}
}

return status;
}


/**
* @brief 读取RTC_CNT寄存器中的时间计数器。
* @param hrtc 指向包含RTC配置信息的RTC_HandleTypeDef结构体的指针。
* @retval 时间计数器
*/
static uint32_t RTC_ReadTimeCounter(RTC_HandleTypeDef *hrtc)
{
uint16_t high1 = 0U, high2 = 0U, low = 0U;
uint32_t timecounter = 0U;

high1 = READ_REG(hrtc->Instance->CNTH & RTC_CNTH_RTC_CNT);
low = READ_REG(hrtc->Instance->CNTL & RTC_CNTL_RTC_CNT);
high2 = READ_REG(hrtc->Instance->CNTH & RTC_CNTH_RTC_CNT);

if (high1 != high2)
{
/* 当读取CNTL和CNTH寄存器期间计数器溢出时, 重新读取CNTL寄存器然后返回计数器值 */
timecounter = (((uint32_t) high2 << 16U) | READ_REG(hrtc->Instance->CNTL & RTC_CNTL_RTC_CNT));
}
else
{
/* 当读取CNTL和CNTH寄存器期间没有计数器溢出, 计数器值等于第一次读取的CNTL和CNTH值 */
timecounter = (((uint32_t) high1 << 16U) | low);
}

return timecounter;
}

/**
* @brief 设置RTC时间
* @param time 时间
* @retval HAL status
*/
HAL_StatusTypeDef KK_RTC_SetTime(struct tm *time){
uint32_t unixTime = mktime(time);
return RTC_WriteTimeCounter(&hrtc, unixTime);
}

/**
* @brief 获取RTC时间
* @retval 时间
*/
struct tm *KK_RTC_GetTime() {
time_t unixTime = RTC_ReadTimeCounter(&hrtc);
return gmtime(&unixTime);
}

void KK_RTC_Init(){
uint32_t initFlag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
if(initFlag == RTC_INIT_FLAG) return;
if (HAL_RTC_Init(&hrtc) != HAL_OK){
Error_Handler();
}
struct tm time = {
.tm_year = 2025 - 1900,
.tm_mon = 1 - 1,
.tm_mday = 1,
.tm_hour = 23,
.tm_min = 59,
.tm_sec = 55,
};
KK_RTC_SetTime(&time);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, RTC_INIT_FLAG);

}


找到MX_RTC_Init的定义,在此文件中引用#include "kk_rtc.h",并在MX_RTC_Init函数的USER CODE RTC_Init 0注释对中调用KK_RTC_Init()函数 并且通过return 绕过MX_RTC_Init函数后面生成的代码

	hrtc.Instance = RTC;
hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM;
KK_RTC_Init();
return;

在main函数的while循环中获取并通过串口输出当前时间

  now = KK_RTC_GetTime();
sprintf(message, "%d-%d-%d %02d:%02d:%02d", now->tm_year + 1900,now->tm_mon + 1,now->tm_mday,
now->tm_hour,now->tm_min,now->tm_sec);
HAL_UART_Transmit(&huart2, (uint8_t*)message, strlen(message), HAL_MAX_DELAY);
HAL_Delay(1000);
RTC主代码