STM32快速回顾系列(九):RTC时钟模块详解

文章目录

  • 前言
  • 一、RTC是什么?RTC的工作原理?
  • 二、库函数以及示例
  • 1.标准库函数
  • 2.示例代码
  • 总结

  • 前言

    STM32 的实时时钟(RTC)是一个独立的定时器。 STM32 的 RTC 模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。

    RTC 模块和时钟配置系统 (RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC,以防止对后备区域 (BKP) 的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP)写保护。

    unix时间戳: Unix 时间戳是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。
    STM32相较于51的外接时钟寄存器DS1302,STM32内部计时是没有年月日,时分秒寄存器的。STM32是利用单独的秒寄存器,以1970年1月1日为0,进行每秒自增的定时器计数。

    当前为1723138563秒,转换后再加上1970年就是当前时间
    闰秒:由于地球自转的影响,每天有一定的计数误差,闰秒规定,当误差大于0.9秒。当前计数分钟会出现61秒,然后下一分钟再从0开始计数。
    这样减少了硬件的负担,但是增加软件转换的麻烦。于是STM32定义了time.h 头文件。专门用于转换时间。

    (BKP)备份寄存器是42个16位的寄存器(中容量,小容量为10个16位的寄存器,20字节),可用来存储84个字节的用户应用程序数据。他们处在备份域里,当VDD电源被切断,他们仍然由VBAT维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。 此外,BKP控制寄存器用来管理侵入检测和RTC校准功能。 复位后,对备份寄存器和RTC的访问被禁止,并且备份域被保护以防止可能存在的意外的写操作。

    一、RTC是什么?RTC的工作原理?

    RTC(Real Time Clock):实时时钟,是指可以像时钟一様输出实际时间的电子设备,一般会是集成电路,因此也称为时钟芯片。总之,RTC只是个能靠电池维持运行的32位定时器,并不像实时时钟芯片,读出来就是年月日。RTC就只一个定时器而已,掉电之后所有信息都会丢失,因此我们需要找一个地方来存储这些信息,于是就找到了备份寄存器(BKP)。因为它掉电后仍然可以通过纽扣电池供电,所以能时刻保存这些数据。 STM32 的实时时钟(RTC)是一个独立的定时器。 STM32 的 RTC 模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。

    TIPS:RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP)写保护


    下图中RTC数据有三条,RTC_Second(秒中断)、RTC_Overflow(溢出事件)和RTC_Alarm(闹钟中断),以及最下方的WKUP(唤醒,常用于PA0引脚),用来进行中断控制,增加了闹钟和休眠唤醒等功能。

    RTC_PRL:计数目标(写入n,就是n+1分频);

    RTC_DIV:自减计数器,每来一个输入时钟,DIV的值自减一次,减到0后再来一个时钟,从PRL获取到重装值继续自减;

    RTC_CNT:Unix时间戳的秒计数器;

    RTC_ALR:闹钟寄存器, 当CNT=ALR时会产生RTC_Alarm闹钟信号,就能进入右边的中断系统;同时也可以让STM32退出待机模式;

    中断:RTC_Second,秒中断:开启后每秒进一次RTC中断;RTC_Overflow,溢出中断:CNT溢出;RTC_Alarm,闹钟中断:CNT=ALR时会触发中断,同时也可以让STM32退出待机模式;

    图中浅灰色的部分都是属于备份域的,在VDD掉电时可在VBAT的驱动下继续运行.这部分仅包括RTC的分频器,计数器,和闹钟控制器.若VDD电源有效,RTC可以触发RTC_Second(秒中断)、RTC_Overflow(溢出事件)和RTC_Alarm(闹钟中断).从结构图可以看到到,其中的定时器溢出事件无法被配置为中断.如果STM32原本处于待机状态,可由闹钟事件或WKUP事件(外部唤醒事件,属于EXTI模块,不属于RTC)使它退出待机模式.闹钟事件是在计数器RTC_CNT的值等于闹钟寄存器RTC_ALR的值时触发的.

    因为RTC的寄存器是属于备份域,所以它的所有寄存器都是16位的.它的计数RTC_CNT的32位由RTC_CNTL和RTC_CNTH两个寄存器组成,分别保存计数值的低16位和高16位.在配置RTC模块的时钟时,把输入的32768Hz的RTCCLK进行32768分频得到实际驱动计数器的时钟TR_CLK = RTCCLK/37768 = 1Hz,计时周期为1秒,计时器在TR_CLK的驱动下计数,即每秒计数器RTC_CNT的值加1(常用)

    RTC只是一个时钟,但与RTC相连的有两个系统时钟,一个是APB1接口的PCLK1另一个是RTC时钟。这样,RTC功能也就分为两个部分:第一部分,APB1接口部分,与APB1总线相连,MCU也就是通过这条总线对其进行读写操作。另一部分,RTC核,由一系列可编程计数器组成,这部分又再细分为两个组件:预分频模块与32位可编程计数器。预分频模块用来产生最长为1秒的RTC时间基准,而32位的可编程的计数器可被初始化为当前的系统时间。

    电路图简化:

    流程(会按照流程写出代码即可):
    1. 使能电源时钟和备份区域时钟
    2. 取消备份区写保护
    3. 复位备份区域,开启外部低速振荡器
    4. 选择 RTC 时钟,并使能
    5. 设置 RTC 的分频,以及配置 RTC 时钟
    6. 更新配置,设置 RTC 中断分组
    7. 编写中断服务函数

    RTC 相关的库函数在文件 stm32f10x_rtc.c 和 stm32f10x_rtc.h 文件中, BKP 相关的库函数在
    文件 stm32f10x_bkp.c 和文件 stm32f10x_bkp.h 文件中

    二、库函数以及示例

    1.标准库函数

    1、RTC时钟源和时钟操作函数;
    void RCC_RTCCLKConfig(uint32_t CLKSource);//时钟源选择;
    void RCC_RTCCLKCmd(FunctionalState NewState);//时钟使能;
    2、RTC初始化函数
    ErrorStatus RTC_Init(RTC_InitTypeDef* RTC_InitStruct);
    trypedef struct
    {
    uint32_t RTC_HourFormat;//小时格式:24/12
    uint32_t RTC_AsynchPrediv;//异步分频系数
    uint32_t RTC_SynchPrediv;//同步分频系数;
    }RTC_InitTypeDef;
    3、日历配置相关函数
    ErrorStatus RTC_SetTime(uint32_t RTC_Format,RTC_TimeTypeDef* RTC_TimeStruct);数值当前时间
    void RTC_GetTime(uint32_t RTC_Format,RTC_TimeTypeDef* RTC_TimeStruct);
    ErrorStatus RTC_SetDate(uint32_t RTC_Format,RTC_Dae TypeDef* RTC_DataStruct);
    void RTC_GetDate(uint32_t RTC_Format,RTC_Date TypeDef* RTC_DateStruct);
    uint32_t RTC_GetSubSecond(void);
    4、RTC闹钟相关函数
    ErrorStatus RTC_AlarmCmd(uint32_t RTC_Alarm,FunctionalState NewState);
    void RTC_SetAlarm();
    void RTC_GetAlarm();
    void RTC_AlarmSubSecondConfig();
    uint32_t RTC_GetAlarmSubSecond(uint32_t RTC_Alarm);
    5、RTC周期唤醒相关函数:
    void RTC_WakeUpClockConfig();
    void RTC_SetWakeUpCounter();
    uint32_t RTC_GetWakeUpCounter(void);
    RTC_WakeUpCmd(DISABLE);//关闭WAKEUP
    6、RTC中断配置以及状态相关函数
    void RTC_ITConfig();
    FlagStatus RTC_GetFlgStatus(uint32_t RTC_FLAG);
    void RTC_ClearFlag(uint32_t RTC_FLAG);
    ITStatus RTC_GetITStatus(uint32_t RTC_IT);
    void RTC_ClearITPendingBit();
    7、RTC相关约束函数
    void RTC_WriteProtectionCmd();//取消写保护
    ErrorStatus RTC_EnterInitNode();//进入配hi模式,RTC_ISR_INITF位设置位1
    void RTC_ExitInitMode(void);//退出初始化模式
    8、其他函数
    uint32_t RTC_ReadBackupRegister();
    void RTC_WriteBackupRegister();
    void RTC_ITConfig();

    RTC_ITConfig 使能或者失能指定的 RTC 中断
    RTC_EnterConfigMode 进入 RTC 配置模式
    RTC_ExitConfigMode 退出 RTC 配置模式
    RTC_GetCounter 获取 RTC 计数器的值
    RTC_SetCounter 设置 RTC 计数器的值
    RTC_SetPrescaler 设置 RTC 预分频的值
    RTC_SetAlarm 设置 RTC 闹钟的值
    RTC_GetDivider 获取 RTC 预分频分频因子的值
    RTC_WaitForLastTask 等待最近一次对 RTC 寄存器的写操作完成
    RTC_WaitForSynchro 等待 RTC 寄存器(RTC_CNT, RTC_ALR and RTC_PRL)与 RTC 的 APB 时钟同步
    RTC_GetFlagStatus 检查指定的 RTC 标志位设置与否
    RTC_ClearFlag 清除 RTC 的待处理标志位
    RTC_GetITStatus 检查指定的 RTC 中断发生与否
    RTC_ClearITPendingBit 清除 RTC 的中断待处理位

    1. 使能电源时钟和备份区域时钟
      前面已经介绍了,我们要访问 RTC 和备份区域就必须先使能电源时钟和备份区域时钟。
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);

    2. 取消备份区写保护
      要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能),否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经配置过了,这样避免每次复位之后重新配置时钟。 取消备份区域写保护的库函数实现方法是:
      PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问

    3. 复位备份区域,开启外部低速振荡器
      在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个
      操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY位来确定低速振荡器已经就绪了才开始下面的操作。
      BKP_DeInit();//复位备份区域
      RCC_LSEConfig(RCC_LSE_ON);// 开启外部低速振荡器

    4.选择 RTC 时钟,并使能。
    这里我们将通过 RCC_BDCR 的 RTCSEL 来选择选择外部 LSI 作为 RTC 的时钟。然后通过RTCEN 位使能 RTC 时钟。
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择 LSE 作为 RTC 时钟

    对于 RTC 时钟的选择,还有 RCC_RTCCLKSource_LSI 和 RCC_RTCCLKSource_HSE_Div128两个,顾名思义,前者为 LSI,后者为 HSE 的 128 分频,这在时钟系统章节有讲解过。使能 RTC 时钟的函数是:
    RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟

    5.设置 RTC 的分频,以及配置 RTC 时钟。
    在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PRLH 和RTC_PRLL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置RTC 的允许配置位(RTC_CRH 的 CNF 位), 设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL两个寄存器)。 下面我们一一这些步骤用到的库函数:在进行 RTC 配置之前首先要打开允许配置位(CNF),库函数是:
    RTC_EnterConfigMode();/// 允许配置
    在配置完成之后,千万别忘记更新配置同时退出配置模式,函数是:

    RTC_ExitConfigMode();//退出配置模式, 更新配置

    设置 RTC 时钟分频数, 库函数是:
    void RTC_SetPrescaler(uint32_t PrescalerValue);
    这个函数只有一个入口参数,就是 RTC 时钟的分频数,很好理解。
    然后是设置秒中断允许, RTC 使能中断的函数是:
    void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);

    这个函数的第一个参数是设置秒中断类型,这些通过宏定义定义的。 对于使能秒中断方法是:
    RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断
    下一步便是设置时间了,设置时间实际上就是设置 RTC 的计数值,时间与计数值之间是需要换算的。库函数中设置 RTC 计数值的方法是:
    void RTC_SetCounter(uint32_t CounterValue)//最后在配置完成之后

    6.更新配置,设置 RTC 中断分组。
    在设置完时钟之后,我们将配置更新同时退出配置模式,这里还是通过 RTC_CRH 的 CNF
    来实现。
    RTC_ExitConfigMode();//退出配置模式,更新配置
    在退出配置模式更新配置之后我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化过时钟了,下次开机(或复位)的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。

    往备份区域写用户数据的函数是:
    void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);

    这个函数的第一个参数就是寄存器的标号了,这个是通过宏定义定义的。 比如我们要往
    BKP_DR1 写入 0x5050,方法是:
    BKP_WriteBackupRegister(BKP_DR1, 0X5050);

    同时,有写便有读,读取备份区域指定寄存器的用户数据的函数是
    uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

    1. 编写中断服务函数
      我们要编写中断服务函数,在秒钟中断产生的时候,读取当前的时间值,并显示到
      oled 模块上。

    2.示例代码

    代码如下(示例):

    //初始化配置
    void MyRTC_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);		//开启PWR的时钟
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);		//开启BKP的时钟
    	
    	/*备份寄存器访问使能*/
    	PWR_BackupAccessCmd(ENABLE);				      //使用PWR开启对备份寄存器的访问
    	
        //通过写入备份寄存器的标志位,判断RTC是否是第一次配置
    	if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
    	{
    		RCC_LSEConfig(RCC_LSE_ON);							//开启LSE时钟
    		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);	//等待LSE准备就绪
    		
            
    		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);				//选择RTCCLK来源为LSE
    		RCC_RTCCLKCmd(ENABLE);								//RTCCLK使能
    		
    		RTC_WaitForSynchro();								//等待同步
    		RTC_WaitForLastTask();								//等待上一次操作完成
    		
    		RTC_SetPrescaler(32768 - 1);	//设置RTC预分频器,预分频后的计数频率为1Hz
    		RTC_WaitForLastTask();			//等待上一次操作完成
    		
    		MyRTC_SetTime();	//设置时间,调用此函数,全局数组里时间值刷新到RTC硬件电路
    		
            //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置
    		BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
    	}
    	else													//RTC不是第一次配置
    	{
    		RTC_WaitForSynchro();								//等待同步
    		RTC_WaitForLastTask();								//等待上一次操作完成
    	}
    }
     
    //设置时间
    uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55};
    void MyRTC_SetTime(void)
    {
    	time_t time_cnt;		//定义秒计数器数据类型
    	struct tm time_date;	//定义日期时间数据类型
    	
    	time_date.tm_year = MyRTC_Time[0] - 1900;		//将数组的时间赋值给日期时间结构体
    	time_date.tm_mon = MyRTC_Time[1] - 1;
    	time_date.tm_mday = MyRTC_Time[2];
    	time_date.tm_hour = MyRTC_Time[3];
    	time_date.tm_min = MyRTC_Time[4];
    	time_date.tm_sec = MyRTC_Time[5];
    	
    	time_cnt = mktime(&time_date) - 8 * 60 * 60;	//调用mktime函数,将日期时间转换为秒计数器格式
    													//- 8 * 60 * 60为东八区的时区调整
    	
    	RTC_SetCounter(time_cnt);						//将秒计数器写入到RTC的CNT中
    	RTC_WaitForLastTask();							//等待上一次操作完成
    }
    
    		  
    
    

    江科大STM32例程

    #include "stm32f10x.h"                  // Device header
    #include <time.h>
    
    uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55};
    
    void MyRTC_SetTime(void);
    
    void MyRTC_Init(void)
    {
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
    	
    	PWR_BackupAccessCmd(ENABLE);
    	
    	if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
    	{
    		BKP_DeInit();
    	
    		RCC_LSEConfig(RCC_LSE_ON);
    		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
    		
    		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    		RCC_RTCCLKCmd(ENABLE);
    		
    		RTC_WaitForSynchro();
    		RTC_WaitForLastTask();
    		
    		RTC_SetPrescaler(32767);
    		RTC_WaitForLastTask();
    		
    		MyRTC_SetTime();
    		
    		BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
    	}
    	else
    	{
    		RTC_WaitForSynchro();
    		RTC_WaitForLastTask();
    	}
    }
    
    /*
    void MyRTC_Init(void)
    {
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
    	
    	PWR_BackupAccessCmd(ENABLE);
    	
    	if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
    	{
    		BKP_DeInit();
    	
    		RCC_LSICmd(ENABLE);
    		while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
    		
    		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
    		RCC_RTCCLKCmd(ENABLE);
    		
    		RTC_WaitForSynchro();
    		RTC_WaitForLastTask();
    		
    		RTC_SetPrescaler(40000 - 1);
    		RTC_WaitForLastTask();
    		
    		MyRTC_SetTime();
    		
    		BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
    	}
    	else
    	{
    		RCC_LSICmd(ENABLE);
    		while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
    		
    		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
    		RCC_RTCCLKCmd(ENABLE);
    		
    		RTC_WaitForSynchro();
    		RTC_WaitForLastTask();
    	}
    }*/
    
    void MyRTC_SetTime(void)
    {
    	time_t time_cnt;
    	struct tm time_date;
    	
    	time_date.tm_year = MyRTC_Time[0] - 1900;
    	time_date.tm_mon = MyRTC_Time[1] - 1;
    	time_date.tm_mday = MyRTC_Time[2];
    	time_date.tm_hour = MyRTC_Time[3];
    	time_date.tm_min = MyRTC_Time[4];
    	time_date.tm_sec = MyRTC_Time[5];
    	
    	time_cnt = mktime(&time_date) - 8 * 60 * 60;
    	
    	RTC_SetCounter(time_cnt);
    	RTC_WaitForLastTask();
    }
    
    void MyRTC_ReadTime(void)
    {
    	time_t time_cnt;
    	struct tm time_date;
    	
    	time_cnt = RTC_GetCounter() + 8 * 60 * 60;
    	
    	time_date = *localtime(&time_cnt);
    	
    	MyRTC_Time[0] = time_date.tm_year + 1900;
    	MyRTC_Time[1] = time_date.tm_mon + 1;
    	MyRTC_Time[2] = time_date.tm_mday;
    	MyRTC_Time[3] = time_date.tm_hour;
    	MyRTC_Time[4] = time_date.tm_min;
    	MyRTC_Time[5] = time_date.tm_sec;
    }
    
    

    总结

    RTC的工作流程通常分为初始化和运行两个阶段。在初始化阶段,我们需要配置RTC的时钟源、预分频器、日历寄存器等参数,并启用相应的中断或事件。在运行阶段,RTC会根据预设的时钟源和分频器递增计数器,同时更新日历信息,处理闹钟事件等。通过合理配置和管理,我们可以充分发挥RTC模块的功能和性能。

    作者:典则

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32快速回顾系列(九):RTC时钟模块详解

    发表回复