作者:junziyang


(注:如非特別声明,以下笔记内容均针对stm32F103ZET6而言。不同型号,细节可能存在差别)

5.1 RTC简介

RTC全称为Real-Time Clock,即实时时钟,是芯片内部一个独立的计时器。类似于电脑上的时钟,在合适软件的支持下,可以为嵌入式系统提供时间、日历和闹钟等功能。例如,显示x年x月x日xx时xx分xx秒,利用闹钟中断定时唤醒等。RTC位于备份区,即使系统断电关机,仍可由电池供电,维持RTC的计时。

RTC的计数器为32bit,可由LSI/LSE或HSE的128分频来提供时钟。如果以秒为单位,理论上来说单向最大计数时长可达136年。RTC可以产生3个中断,即闹钟中断,秒中断和溢出中断。RTC闹钟与外部中断线EXTI 17相连,且在NVIC中有专门的闹钟中断向量RTC_Alarm_RIQn(编号41)与之对应。通过闹钟中断,可以对程序运行进行日程和时间管理,比如节假日自动关机等。

5.2 RTC寄存器复位值表

stm32死区怎么配置(自学STM32-05)(1)

图1 RTC寄存器地址映射与复位值表

任何外设的功能配置都是通过配置相关寄存器实现的,RTC也不例外。为便于接下来RTC原理和功能的学习,可以先浏览一下RTC寄存器的地址映射与复位值表,如图5.1所示(黄色背景的标志位仅可由硬件设置)。RTC共占用10个32bit寄存器,两两配对,分为5组:

  1. CRH/CRL为控制寄存器,分管中断使能和状态位;
  2. DIVH/DIVL为预分频寄存器,用来对时钟源RTCCLK进行分频,得到RTC的基础时钟TR_CLK(通常为1Hz);
  3. PRLH/PRLL为预分频重载寄存器,用来存储预分频系数,每个TR_CLK周期向分频计数器重装载;
  4. CNTH/CNTL为计数寄存器,从初始值开始在TR_CLK控制下递增;
  5. ALRH/ALRL为闹钟寄存器,用来设置闹钟,当CNT寄存器与ALR寄存器中的值相等时,产生闹钟事件,如果CRH寄存器在的ALRIE=1,则会产生闹钟中断。
5.3 RTC的功能与原理5.3.1 结构框图

stm32死区怎么配置(自学STM32-05)(2)

图2. RTC原理框图

RTC的原理框图如图2所示。主要有两个单元构成:APB1接口(橙色部分)和RTC核心(绿色部分)。APB1接口与一组16bit的RTC的寄存器相连(见图1),共享APB1总线的时钟(PCLK1),这样通过APB1总线可以对这些寄存器进行读写操作从而实现对寄存器的配置。APB1接口在待机状态是不供电的。

RTC核心单元又可分为两个模块:RTC预分频器和计数器。左侧的RTC prescaler主要是对RTCCLK进行分频,以产生RTC的基础时钟TR_CLK。其中包含两组功能寄存器(参考图2),RTC_DIV(共32bit)用来对RTCLK分频,而RTC_PRL(共20bit),用来存储预分频系数,每个TR_CLK周期向分频计数器装载1次。右侧的模块中是一个计数器,也包括2组功能寄存器,RTC_CNT(共32bit)用来计↑,RTC_ALR(共32bit)用来设置闹钟。通常将RTC_CNT初始化为当前时间(对表),然后它会按TR_CLK时钟递增计数。如果不初始化,默认的时间起点是1969年12月31日23:59:58(STM32F103ZET6实测,可以用c语言的time和localtime函数获取该时间)。RTC核心部分在备份区中,在待机模式甚至主电源断开情况下该区域都是有供电的(如果安装了电池)。因此RTC初始化以后,只要不主动关闭,或电池耗尽,可以持续工作。

在待机模式下APB1接口以及右侧的RTC_CR寄存器是不供电的。RTC的3个中断中,溢出中断(RTC_Overflow)和秒中断(RTC_Second)在待机模式下无效。而闹钟中断(RTC_Alarm)有独立的中断向量,在待机状态下可以触发中断并将MCU从待机状态唤醒。

5.3.2 RTC设置

系统复位或电源复位后,除RTC_PRL/DIV/CNT/ALR不会复位外,RTC_CR寄存器与备份区外的所有寄存器一起被复位。这几个受保护的RTC寄存器,只有在对备份区进行专门复位(RCC_APB1RSTR寄存器BKPRST位置1)时才会被清除。

与其他外设一样,RTC的设置也是通过配置相关寄存器实现的。但由于这些寄存器位于“保护区”,读写操作略有不同。

1. 读取RTC寄存器

RTC核心与RTC APB1接口是完全独立的。RTC寄存器的更新受控于自己内部的时钟,在TR-CLK的每个上升沿被更新,即使外部时钟关闭的情况下,也是如此。而软件读取RTC的可读寄存器需要通过APB1接口。如果APB1的时钟被关闭,再次重新开启后,RTC的时钟要与之重新同步。如果APB1时钟重新开启后马上就去读RTC寄存器(例如更新时间显示),由于时钟不同步,第一次读取的数据有可能是不正确的(通常会读到0)。例如,系统复位或电源复位后,或者MCU刚从停止模式(Stop mode)或待机模式(Standby mode)被唤醒后,马上去读RTC寄存器就有可能会出现这种情况。

为了避免这种情况,在APB1接口重启后,第一次读RTC寄存器前,要等待硬件将RTC_CRL中的RSF(Register Synchronized Flag)位置1。RSF=1表明,TR_CLK与PCLK1同步完成。

普通睡眠模式不会受此影响,因为这种情况下APB1接口时钟不会关闭。

2. 配置RTC寄存器

RTC位于备份区,系统复位后备份区默认是写保护的。如果需要设置或修改RTC,需要执行如下操作:

1)置RCC_APB1ENR寄存器的PWREN=1和BKPEN=1,开启电源接口和备份区接口的时钟。

2)置电源控制寄存器PWR_CR中的DBP=1,使能对备份区和RTC的访问。

RTC寄存器必须在前一次写入结束后才能进行下一次写入。执行完上述操作,放开写入权限后,还必须确认没有正在写入RTC寄存器的操作才可以执行新的写入。确认的方法是:读取RTOFF(RTC operation OFF),写操作结束后,硬件会将该位置1。

由于RTC_PRL/CNT/ALR寄存器是在TR_CLK同步下不断更新的,要写入这些寄存器,必须先主动停止这些寄存器的自动更新。方法是:在确认RTOFF=1的情况下,将CNF(Configuration Flag)位置1。可以认为CNF=1后,这些寄存器被暂时与TR_CLK断开,进入配置模式(Configuration mode)。配置完毕再置CNF=0,退出配置模式,配置才会生效。为了保险期间,最后可以再确认写一下RTOFF=1。概况起来,写入这些寄存器的步骤如下:

1)查询并等待RTOFF=1;

2)设置CNF=1;

3)写入RTC寄存器;

4)清除CNF,即写入CNF=0;

5)查询并等待RTOFF=1。

由于内外时钟的不同频率,CNF的写入至少需要等待3个RTCCLK周期。

3. RTC的标志位

RTC相关状态标志位由RTC_CRL寄存器管理,除了前面提到过的RTOFF(写入结束)、CNF(配置模式)和RSF(寄存器同步)外,还有SECF(秒标志)、OWF(溢出标志)和ALRF(闹钟标志)3个标志位:

SECF(Second Flag)置位发生在RTC计数器更新前,提前量为1个RTCCLK周期,计数器在每个TR_CLK周期都会更新。因TR_CLK通常设为1秒,所以称为秒标志。

OWF(Overflow Flag)置位发生在计数器数器溢出前,提前量为1个RTCCLK周期。RTC的计数器是32bit递增的,达到最大数后会自动回到0,这一事件称为计数器溢出。

ALRF(Alarm Flag)置位发生在计数器数值达到ALR 1前,提前量为1个RTCCLK。图3所示为PR=0003(即TR_CLK为RTCCLK4分频),ALR=0004(即第5秒)时,RTC时钟、秒时钟、计数器以及ALRF置位的时序关系。OWF的置位情况与此类似,只是发生在计数器溢出前。

stm32死区怎么配置(自学STM32-05)(3)

图3. ALRF与计数器、RTC时钟以及秒时钟的时序关系

5.3.3 RTC寄存器

RTC共有10个寄存器,按功能分为5组,可以按半字或字访问。

1. RTC_CRH/L

RTC_CRH和RTC_CRL为RTC控制寄存器(RTC Control Register High/Low )。二者均占32bit,但大部分为预留空间。

RTC_CRH用来管理RTC相关的中断,只有3个功能位:

RTC_CRL用来管理RTC相关的一些标志位。共有6个功能位:

说明:

2. RTC_PRLH/L

RTC的本地时钟TR_CLK是通过对RTCCLK进行分频得到的。分频器的工作原理是,通过一个计数器,每隔N个输入时钟脉冲,产生1个输出时钟脉冲,即实现了对时钟源的N分频。分频器中用一个寄存器来存储分频系数,每次计数器计数归零,自动将分频系数寄存器中的值重新装载到计数器,开始下一个周期的计数。

RTC_PRLH/L为RTC预分频装载寄存器(RTC Prescaler Load Register High/Low),是用来存储分频系数的,每个RTCCLK周期递减,归零时产生TR_CLK时钟脉冲。在该脉冲触发下,RTC_PRL将其中存储的分频系数重新装载到计数器,开始下一个TR_CLK周期的计数(参见图3)。

RTC_PRL寄存器由两个32位寄存器组成。RTC_PRLH仅使用低4位,存储PRL[19-16]。RTC_PRLL使用低16位,存储PRL[15-0]。RTCCLK的频率一般为32.768kHz(215),将RPLL设为0x7FFF,即可得到1s的周期。设置PRLH主要是为了应对更高的RTC时钟频率,例如HSE的128分频为562.5kHz,得到一秒则需要用到PRLH(0x89544)。

TR_CLK的与RTCCLK的关系为:

stm32死区怎么配置(自学STM32-05)(4)

该寄存器受RTOFF位的保护。

3. RTC_DIVH/L

RTC_DIVH/L为RTC预分频器计数寄存器(RTC Prescaler Divider Register High/Low),用来存储预分频计数器当前值。有了该寄存器,可以在不干扰计数器工作的情况下,通过获取当前计数值实现更精确的时间度量。例如,TR_CLK通常为1秒,为32768个RTCCLK周期,通过读取RTC_DIV中的值,可以实现比秒更短的时间分辨,比如秒表上的1/100s。

此寄存器为只读。RTC_PRL或CNT寄存器修改后,硬件会重新装载该寄存器的值。

4. RTC_CNTH/L

RTC_CNTH/L是RTC计数寄存器(RTC Counter Register High/Low),是用来计数的。由两个32位寄存器组成,每个寄存器仅用低16位,每个TR_CLK周期数值递增1。可连续计数范围0-232(约136年)。该寄存器受RTOFF位的保护。

5. RTC_ALRH/L

RTC_ALRH/L为RTC闹钟寄存器(RTC Alarm Register High/Low),用来设置闹钟。当此寄存器中的值与CNT寄存器中的值相等时,会设置ALRF位,产生闹钟中断/事件。

该寄存器受RTOFF位的保护。

5.4 HAL库RTC函数5.4.1 RTC配置步骤

在HAL库中,RTC的API在STM32f1xx_hal_rtc.h中声明,在stm32f1xx_hal_rtc.c中定义。配置RTC基本步骤如下:

  1. 使能RTC区访问:RCC_APB1ENR中的PWREN=1和BKPEN=1,PWR_CR中的DBP=1;
  2. 准备配置RTC:清除RSF并待其硬件复位,确保寄存器已同步;每次写入前都要确认RTOFF=1,即没有进行中的写入;
  3. 配置RTC预分频系数。
  4. 设置日期和时间:将日期和时间折算为秒,写入RTC_CNT寄存器。CNT寄存器仅是一个计数器,时间和日期需要在程序中约定一个起点,然后根据计数器的值进行折算。时间起点的约定可以是任意的。计数寄存器中并不会存储年月日等信息,只是相对于参考时间起点的计数次数(秒数),具体的日期和时间需要在程序中折算。
  5. 设置闹钟:写入RTC_ALRH/L寄存器。
  6. 使能所需中断。
5.4.2 RTC初始化/复位函数5.4.3 事件和日期设置函数5.4.4 闹钟设置函数5.4.5 状态控制函数5.5 BKP寄存器5.5.1 BKP简介

备份区(BacKuP domain)中除了RTC,还有一组寄存器,用来备份应用程序中的用户数据。由于关机或待机后备份区可以由电池供电,因此把一些重要数据存储到备份区的寄存器中,MCU复位或唤醒后可以读取这些数据,执行一些初始化的工作。例如,MCU从停机被唤醒后,默认会使用HSI时钟,往往会造成一些问题。可以设置一个睡眠方式标志,停机前写入备份区寄存器,待被重新激活后,可以读取这个数据,判断是否从停机状态恢复,以便重新初始化时钟。(好像读PWR_CR的PDDS位也可以)。

5.5.2 BKP寄存器

STM32系列大容量产品在备份区设置了42个16bit的寄存器(32bit寄存器仅开放低16位),可以存储84个字节的数据。由于备份区在复位后默认是写保护的,与前述设置RTC一样,必须先去掉写保护(RCC_APB1ENR:PWREN=1,BKPEN=1;PWR_CR:DBP=1)才可以写入BKP寄存器。

除了42个数据寄存器,BKP还有3个专用寄存器,来管理RTC的校准、防侵入引脚(TAMPER)的设置,以及BKP状态和中断。这个专用寄存器的地址映射即复位值如图4所示。

stm32死区怎么配置(自学STM32-05)(5)

图4. BKP寄存器地址映射与复位值表

1. BKP_RTCCR寄存器

BKP_RTCCR是RTC校准寄存器(RCC Clock Calibration Register ),用来管理RTC脉冲输出和校正。该寄存器共有4个功能位:

2. BKP_CR寄存器

BKP_CR是备份区控制寄存器(Backup control register),用来管理防侵入Tamper引脚。该寄存器只有两个功能位:

3. BKP_CSR寄存器

BKP_CSR是备份区控制/状态寄存器(Backup control/status register),用来管理TAMPER引脚上的中断/事件及标志位。该寄存器共5个功能位:

5.5.3 HAL库BKP函数

不同型号的MCU,BKP不尽相同。因此HAL库将相关函数纳入扩展API。在stm32f1xx_hal_rtc_ex.h中声明,在stm32f1xx_hal_rtc_ex.c中定义。

1. TAMPER管理函数

2. 秒中断/事件管理函数

3.扩展控制函数

6. 示例-半点报时闹钟

/* Includes ------------------------------------------------------------------*/ #include "rtc.h" /* USER CODE BEGIN 0 */ #include "time.h" /* USER CODE END 0 */ RTC_HandleTypeDef hrtc; /* RTC init function */ void MX_RTC_Init(void){ /* USER CODE BEGIN RTC_Init 0 */ RTC_AlarmTypeDef sAlarm = {0}; /* USER CODE END RTC_Init 0 */ RTC_TimeTypeDef sTime = {0}; RTC_DateTypeDef DateToUpdate = {0}; /* USER CODE BEGIN RTC_Init 1 */ /* USER CODE END RTC_Init 1 */ /** Initialize RTC Only */ hrtc.Instance = RTC; hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND; hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN Check_RTC_BKUP */ /* USER CODE END Check_RTC_BKUP */ /** Initialize RTC and set the Time and Date */ sTime.Hours = 10; sTime.Minutes = 0; sTime.Seconds = 0; if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } DateToUpdate.WeekDay = RTC_WEEKDAY_FRIDAY; DateToUpdate.Month = RTC_MONTH_OCTOBER; DateToUpdate.Date = 1; DateToUpdate.Year = 21; if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN RTC_Init 2 */ sTime.Hours = 1; sTime.Minutes = 0; sAlarm.AlarmTime = sTime; HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN); //Alarm in 30min later /* USER CODE END RTC_Init 2 */ } 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, 2, 1); HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn); /* USER CODE BEGIN RTC_MspInit 1 */ /* USER CODE END RTC_MspInit 1 */ } } /*---------------------------------------------------------------------- * * Alarm Event Callback * -----------------------------------------------------------------------*/ void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc){ uint8_t n=0u; HQ_RTC_UpdateAlarm(hrtc,30); printf("Current Time is:"); HQ_RTC_DisplayTime(); if((PWR->CSR&(0x1<<1))!=0){ //Wakeup from Stop mode SystemClock_Config(); //Restore system clock } for (n=0;n<4;n ) { //Ring the bell BEEP = !BEEP; delay_ms(300); } } //-----UpdateAlarm to ring bell after INTERVAL minutes ----------------- void HQ_RTC_UpdateAlarm(RTC_HandleTypeDef *hrtc,uint8_t interval){ uint16_t high1 = 0U, low = 0U; uint32_t AlarmCounter = 0U,tickstart = 0U; // 01. RTC_ReadAlarmCounter high1 = READ_REG(hrtc->Instance->ALRH & RTC_CNTH_RTC_CNT); low = READ_REG(hrtc->Instance->ALRL & RTC_CNTL_RTC_CNT); AlarmCounter = (((uint32_t) high1 << 16U) | low); // 02. Update the value AlarmCounter = ((uint32_t) interval)*60U; // 03. RTC_WriteAlarmCounter while ((hrtc->Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET);//Wait till RTC is in INIT state __HAL_RTC_WRITEPROTECTION_DISABLE(hrtc);/* Disable the write protection for RTC registers */ WRITE_REG(hrtc->Instance->ALRH, (AlarmCounter >> 16U));/* Set RTC COUNTER MSB word */ WRITE_REG(hrtc->Instance->ALRL, (AlarmCounter & RTC_ALRL_RTC_ALR));/* Set RTC COUNTER LSB word */ __HAL_RTC_WRITEPROTECTION_ENABLE(hrtc);/* Enable the write protection for RTC registers */ tickstart = HAL_GetTick(); while ((hrtc->Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET){ //Wait till RTC is in INIT state if ((HAL_GetTick() - tickstart) > RTC_TIMEOUT_VALUE){ Error_Handler(); } } } // ------ DisplayTime ------------------------------------------------ void HQ_RTC_DisplayTime(){ RTC_TimeTypeDef sTime = {0}; RTC_DateTypeDef sDate = {0}; HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); printf("20d-d-d d:d:d \r\n", sDate.Year,sDate.Month,sDate.Date, sTime.Hours,sTime.Minutes,sTime.Seconds); }


学习心得:

嵌入式系统开发过程中最繁琐的部分可能就是配置寄存器,先后顺序、延时、相互间的逻辑联系.....非常容易出错。库函数对常用的寄存器进行了功能性封装。基于库函数开发可以事半功倍,而且HAL库函数自带超时和容错机制,不容易出错。学习过程中可以读一下库函数,看一下其中是如何操作寄存器的。对学习大有裨益。还可以先基于库函数实现所需的功能,然后按其中的思路自己再用配置寄存器的方法实现一遍。调试的过程中可以学到很多细节的东西。毕竟配置寄存器是与硬件对话的更直接途径。

,