江协科技STM32 12-2 BKP备份寄存器RTC实时时钟
这一节我们要讲的主要内容是RTC实时时钟,实时时钟本质上是一个定时器,但是这个定时器是专门用来产生年月日时分秒,这种日期和时间信息的。所以学会了STM32的RTC就可以在STM32内部拥有一个独立运行的钟表。想要记录或读取日期和时间,就可以通过操作RTC来实现。
RTC这个外设比较特殊,它和备份寄存器(BKP)、电源控制(PWR)这两章关联性比较强,因此在RTC这一章中,就把BKP和RTC放在一起讲。
我们首先就先介绍备份寄存器(BKP),其实备份寄存器(BKP)和SPI章节中学过的Flash存储器类似,都是用来存储数据的,只是Flash的数据是真正的掉电不丢失;而BKP的数据是需要VBAT引脚接上备用电池来维持的,只要VBAT有电池供电,即使STM32主电源断电,BKP的值也可以维持原状。
首先我们来看一下时间戳的知识点。Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始到现在所经过的秒数,不考虑闰秒。在计算机的底层我们使用秒计数器来计时,需要给人类观看时,就转换为年月日时分秒这样的格式。时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量,计算机为了存储这样一个永不进位的秒数,这个数据变量类型还是要定义大一些,这个变量类型在不同系统中定义是不一样的。我们本节STM32中的RTC,其核心的计时部分是一个32位的可编程计数器,说明我们这款STM32,它的时间戳是32位的数据类型。世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间。
GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准。
UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致
接下来我们学习时间戳中秒计数器和日期时间如何进行相互转换,这时候我们需要用到time.h模块,C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换,在time.h里主要有表格内的这些主要函数
时间戳转换关系如下图所示
接下来我们学习BKP和RTC的外设部分,我们首先学习BKP的相关知识点。BKP全称Backup Registers,翻译过来就是备份寄存器,BKP的用途就是可用于存储用户应用程序数据。其特性就是当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。如果VDD断电,VBAT也没电,那BKP里的数据就会清零,因为BKP本质上是RAM存储器,没有掉电不丢失的能力。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位;TAMPER引脚产生的侵入事件将所有备份寄存器内容清除,TEMPER是一个接到STM32外部的引脚,其位置就是VBAT旁边的2号引脚,其与PC13、RTC共用,这个TAMPER引脚是一个安全保障设计,比如如果做一个安全系数非常高的设备,设备需要有防拆功能,BKP里也存储了一些敏感数据,那就可以使能这个TEMPER引脚的侵入检测功能;设计者把下面两个RTC功能也放在了BKP中: 1.引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲,RTC的引脚也是在PC13这个位置 2.存储RTC时钟校准寄存器。最后看一下BKP中,用户数据的存储容量,在中容量和小容量设备里,BKP是20个字节,在大容量和互联型设备里,BKP是84个字节。可以看出来BKP的容量其实非常小,一般只能用来存储少量的参数。BKP的简介我们就介绍到这里。
下面看一下BKP的基本结构,图中橙色部分我们可以叫做后备区域,BKP处于后备区域,但后备区域不只有BKP,还有RTC的相关电路。STM32后备区域的特性就是当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电,当VDD主电源上电时,后备区域供电会由VBAT切换到VDD,也就是主电源有电时,VBAT不会用到,这样可以节省电池电量。BKP是位于后备区域的,BKP里主要有数据寄存器、控制寄存器、状态寄存器和RTC时钟校准寄存器。其中数据寄存器是主要部分,用来存储数据,每个数据寄存器都是16位的,也就是一个数据寄存器可以存2个字节,对于中容量和小容量的设备,里面有DR1、DR2一直到DR10,总共10个数据寄存器,那一个寄存器两个字节,所以容量是20个字节。对于大容量和互联型设备,里面除了DR1到DR10还有DR11、DR12一直到DR42,总共42个数据寄存器,容量是84个字节。侵入检测可以从PC13位置的TAMPER引脚引入一个检测信号,当TAMPER产生上升沿或者下降沿时,清除BKP所有的内容,以保证安全,时钟输出可以把RTC的相关时钟从PC13位置的RTC引脚输出出去,供外部使用。其中,输出校准时钟时,再配合校准寄存器,可以对RTC的误差进行校准。以上就是BKP外设的结构和功能。
接下来我们就继续学习以下RTC外设,RTC英文全称 Real Time Clock 中文翻译为实时时钟,在STM32中,RTC是一个独立的定时器,可为系统提供时钟和日历的功能,RTC实时时钟,一般就指提供年月日时分秒这种日期时间信息的计时装置;RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时;其内部设有32位的可编程计数器,可对应Unix时间戳的秒计数器。在读取时间时,我们先得到这个计数器中的秒数,然后使用time.h模块里的localtime函数就能立刻知道年月日时分秒的信息了,在写入时间时,我们先填充年月日时分秒信息到struct tm结构体,然后用mktime函数得到秒数,再写入到这个32位计数器即可。RTC外设中配有20位的可编程预分频器,可适配不同频率的输入时钟,由于32位的秒计数器显然1秒要自增一次,所以驱动计数器的时钟,需要是一个1Hz的信号,但是实际提供给RTC模块的时钟,也就是RTCCLK一般频率都比较高。所以显然我们需要在RTCCLK和计数器时钟输入之间加入一个分频器,给RTCCLK降一降频率,保证分频器输出给计数器的频率为1Hz。那为了适配各种频率的RTCCLK,就在其中间加入了一个20位的分频器,可以选择对输入时钟进行1-2^20这么大范围的分频,这样就可以适配不同频率的输入时钟,这就是这个可编程分频器的作用;可选择三种RTC时钟源:1. HSE时钟除以128(通常为8MHz/128) 2. LSE振荡器时钟(通常为32.768KHz) 3. LSI振荡器时钟(40KHz) 这三个时钟可以选择其中一个介入到RTCCLK。
在时钟树中,高速时钟一般供内部程序运行和主要外设使用;低速时钟一般供RTC、看门狗这些东西使用,红色所圈出来的地方最右侧箭头通往RTC,就是RTCCLK,RTCCLK有三个来源,第一个是OSC引脚接的HSE,外部高速晶振,这个晶振是主晶振,一般用的8MHz,8MHz进来,通过128分频,可以产生RTCCLK信号,128分频的原因是8MHz的主晶振太快了,如果不提前分频,直接给RTCCLK,后续即使再通过RTC的20位分频器也分不到1Hz这么低的频率。中间这一路的时钟来源是LSE,外部低速晶振,我们在OSC32这两个引脚接上外部低速晶振,这个晶振产生的时钟可以直接提供给RTCCLK,这个OSC32的晶振是内部RTC的专用时钟,这个晶振的值也不是随便选的,通常跟RTC有关的晶振,都是统一的数值,就是32.768KHz,选择这个数值的原因是32KHz这个值附近的频率是这个晶振工艺比较合适的频率,另一方面是32768是一个2的次方数,2^15 = 32768 ,所以32.768KHz即32768Hz,经过一个15位分频器的自然溢出,就能很方便地得到1Hz的频率。自然溢出的意思就是设计一个15位的计数器,这个计数器不用设置计数目标,直接从0计到最大值,就是计到32767,计满后自然溢出,这个溢出信号就是1Hz。所以,目前在RTC电路中,基本都是清一色的32.768KHz的晶振。最后第三路时钟源来自LSI,内部低速RC振荡器,LSI固定是40kHz,如果选择LSI当作RTCCLK,后续再经过40K的分频,就能得到1Hz的计数时钟了。当然内部的RC振荡器一般精准度没有外部晶振高,所以LSI给RTCCLK可以当作一个备选方案。另外LSI还可以提供给看门狗,这个之后我们讲看门狗的时候再说。
这三路时钟中我们最常用的就是中间这一路外部32.768KHz的晶振,提供RTCCLK的时钟。不仅因为中间这一路32.768KHz的晶振本身就是专供RTC使用的,其余的时钟其实各自都有各自的主要任务,另外一个更重要的原因就是只有中间这一路的时钟可以通过VBAT备用电池供电,上下两路时钟,在主电源断电后,是停止运行的。所以要想实现RTC主电源掉电继续走时的功能,必须选择中间这一路的RTC专用时钟。
接下来我们看一下RTC的框图,看一下RTC外设具体是怎么设计的。先整体上划分一下,左边的一块是核心的分频和计数计时部分,右边这一块是中断输出使能和NVIC部分,最上面一块是APB1总线读写部分,最下面一块是和PWR关联的部分,意思就是RTC的闹钟可以唤醒设备,推出待机模式。在图中,有灰色填充的部分都处于后备区域,这些电路在主电源掉电后,可以使用备用电池维持工作,其他未被填充的部分就是待机时不供电,有关睡眠、停机、待机这些低功耗相关的内容,我们下节学PWR的时候再细讲。
我们依次详细看一下。首先看分频和计数计时部分,这一块的输入时钟是RTCCLK,RTCCLK的来源需要在RCC里进行配置。因为可选的三路时钟频率各不相同,而且都远大于我们所需要的1Hz的秒计数频率,所以RTCCLK进来,需要首先经过RTC预分频器进行分频,这个分频器由两个寄存器组成,上面这个是重装载寄存器RTC_PRL,下面这个RTC_DIV,手册里叫做余数寄存器,但实际上这一块跟我们之前定时器时基单元里的计数器CNT和重装值ARR是一样的作用。分频器其实就是一个计数器,计几个数溢出一次,那就是几分频,所以对于可编程的分频器来说,需要有两个计数器,RTC_DIV寄存器用来不断地计数,另一个RTC_PRL寄存器,我们写入一个计数目标值,用来配置是几分频。那么PRL中就是计数目标,我们写入6,那就是7分频,写入9,那就是10分频;下面这个DIV,就是每来一个时钟计一个数的用途了,当然这个DIV计数器是一个自减计数器,每来一个输入时钟,DIV的值自减一次,自减到0时,再来一个输入时钟,DIV输出一个脉冲,产生溢出信号,同时DIV从PRL获取重装值,回到重装值继续自减。分频输出后的时钟频率是1Hz,提供给后续的秒计数器。然后看一下计数计时部分,32位可编程计数器RTC_CNT就是计时最核心的部分,我们可以把这个计数器看作是Unix时间戳的秒计数器,这样借助time.h的函数就可以很方便地得到年月日时分秒了,在其下面还设计有一个闹钟寄存器RTC_ALR,这个ALR也是一个32位的寄存器,和上面这个CNT是等宽的,它的作用顾名思义就是设置闹钟,我们可以在ALR写一个秒数,设定闹钟,当CNT的值跟ALR设定的闹钟值一样时,这时就会产生RTC_Alarm闹钟信号,通往右边的中断系统,在中断函数里,你可以执行相应的操作,同时这个闹钟还兼具一个功能,就是下面这里的闹钟信号可以让STM32退出待机模式。这个功能就可以对应一些用途,比如你设计一个数据采集设备,需要在环节非常恶劣的地方工作,比如海底、高原、深井这些地方,然后要求是每天中午12点采集一次环节数据,其他时间为了节省电量,避免频繁换电池,芯片都必须处于待机模式,这样的话我们就可以用这个RTC自带的闹钟功能。另外这个闹钟值是一个定值,只能响一次,所以你想实现周期性的闹钟,那在每次闹钟响之后,都需要再重新设置一下下一个闹钟时间。继续往右看就是中断部分了,在左边这里有三个信号可以触发中断,第一个是RTC_Second,秒中断,它的来源就是CNT的输入时钟,如果开启这个中断,那么程序就会每秒进一次RTC中断;第二个是RTC_Overflow,溢出中断,它的来源是CNT的右边,意思就是CNT的32位计数器计满溢出了会触发一次中断,所以这个中断一般不会触发;第三个RTC_Alarm,闹钟中断,刚才说过,当计数器和闹钟值相等时,触发中断,同时,闹钟信号可以把设备从待机模式唤醒。中断信号到右边这里就是中断标志位和中断输出控制,F结尾的是对应的中断标志位,IE结尾的是中断使能,最后三个信号通过一个或门汇聚到NVIC中断控制器。最上面这部分APB1总线和APB1接口就是我们程序读写寄存器的地方了,读写寄存器可以通过APB1总线来完成,另外也可以看出RTC是APB1总线上的设备。最后,最下面这一块,推出待机模式还有一个WKUP引脚,闹钟信号和WKUP引脚都可以唤醒设备。到这里这个RTC外设框图就已经全部了解清楚了。
接下来看一下下面的基本结构图,再总结一下以上内容,最左边是RTCCLK时钟来源,这一块需要在RCC里配置,3个时钟,选择一个,当作RTCCLK,之后RTCCLK先通过预分频器,对时钟进行分频,余数寄存器是一个自减寄存器,存储当前的计数值,重装寄存器是计数目标,决定分频值。分频之后得到1Hz的秒计数信号,通向32位计数器,1s自增一次,下面还有一个32位的闹钟值可以设定闹钟,右边有三个信号可以触发中断,分别是秒信号、计数器溢出信号和闹钟信号,三个信号先通过中断输出控制进行中断使能,使能的中断才能通向NVIC,然后向CPU申请中断。在程序中,我们配置这个数据选择器,可以选择时钟来源;配置重装寄存器,可以选择分频系数;配置32位计数器,可以进行日期时间的读写,需要闹钟的话,配置32位闹钟值即可;需要中断的话,先允许中断,再配置NVIC,最后写对应的中断函数即可,这就是RTC外设的主要内容。
最后,我们再看一些这个RTC的一些操作注意事项
1.执行以下操作将使能对BKP和RTC的访问:设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟;设置PWR_CR的DBP,使能对BKP和RTC的访问。正常的外设开启了时钟就能使用了,但是BKP和RTC这两个外设开启稍微复杂些,首先要设置RCC_APB1ENR,这个实际上就是开启APB1外设的时钟,要同时开启PWR和BKP的时钟,对于RTC来说,并没有单独开启时钟的选项。然后我们还要设置PWR_CR的DBP位,来使能对BKP和RTC的访问
2. 若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1。这一步对应代码里的一个库函数,就是RTC等待同步,一般在刚上电的时候调用一下这个函数就行了。
3. 必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。就是RTC会有一个进入配置模式的标志位,把这一位置1,才能设置时间,其实这个操作在库函数中,每个写寄存器的函数都会自动加上这个操作,所以就不用再单独调用代码进入配置模式了。
4. 对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器
2和3的注意事项都是因为读写数据时使用的APB1总线是在PCLK1的时钟频率下运行的,但是RTC外设内部的工作时钟是RTCCLK,PCLK1的频率远大于RTCCLK,因此任何读写操作都需要等待一会。
下面我们进入到代码编写的部分。
首先介绍一下备份寄存器BKP的库函数,其中,BKP_DeInit函数用于恢复缺省配置在BKP的外设中是有一个用途的,就是手动清空BKP所有的数据寄存器,因为如果有备用电池的话,BKP的数据主电源掉电不清零、上电复位也不清零,它就没清零的时候,如果我们确实想要清零,就可以使用这个函数,这样所有BKP的数据都会变0;BKP_TamperPinLevelConfig和BKP_TamperPinCmd用于配置TAMPER侵入检测功能,前者可以配置TAMPER引脚的有效电平,就是高电平触发还是低电平触发,后者就是配置是否开启侵入检测功能,如果需要侵入检测的话,那就先配置TAMPER有效电平,再使能侵入检测功能就行了;BKP_ITConfig,中断配置,就是配置是否开启中断;BKP_RTCOutputConfig这是配置时钟输出功能,可以选择在RTC引脚上输出时钟信号,输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲,该配置需要通过 BKP 模块的寄存器来实现,因此函数被归在了BKP的库文件之中;BKP_SetRTCCalibrationValue,用于设置RTC校准值,其实就是写入RTC校准寄存器,校准值的设置也需要写入 BKP 模块的相关寄存器,因此也被归在了BKP的库文件之中。以上这些函数就是我们在上面说的BKP附加的小功能。之后的这几个函数才是经常使用的:BKP_WriteBackupRegister,写备份寄存器,其第一个参数指定要写在哪个DR里,第二个参数填你要写入的数据;BKP_ReadBackupRegister,读备份寄存器。参数指定要读哪个DR,返回值就是DR里存的值。 此外,我们还需要特别关注PWR库函数中的PWR_BackupAccessCmd函数,即备份寄存器访问使能,该函数中的内容就是设置PWR_CR寄存器里的DBP位,我们来调用这个函数满足使用RTC和BKP外设时的注意事项1。
下面对于RTC实时时钟编程,我们总结其初始化步骤如下:
1.开启PWR时钟和BKP时钟,使能BKP和RTC的访问
2.启动RTC的时钟,我们计划使用LES作为系统时钟,所以使用RCC模块里的函数,开启LSE的时钟
3.配置RTCCLK这个数据选择器,指定LSE为RTCCLK,这一步的函数也是在RCC模块里的
4.调用注意事项中提到的等待函数,分别为等待同步和等待上一次操作完成
5.配置预分频器,给PRL重装寄存器一个合适的分频值,以确保输出给计数器的频率是1Hz
6.配置CNT的值,给这个RTC一个初始时间
如果需要闹钟的话,可以配置闹钟值;需要中断的话可以配置中断部分
因为RTC比较简单,所以库函数并没有使用结构体来配置,RTC也没有RTC_Cmd这样的函数,开启时钟就能自动运行了,不需要最后再启动一下的。
在RCC库函数中,存在着一些和RTC时钟相关的函数。其中RCC_LSEConfig用于配置LSE外部低速时钟,启动LSE时钟就调用这个函数;RCC_LSICmd函数用于配置LSI内部低速时钟,如果出现了外部时钟不起振的情况,也可以使用这个内部时钟来进行实验;RCC_RTCCLKConfig,RTCCLK配置,这个函数用来选择RTCCLK的时钟源,实际上就是配置简化结构图中的数据选择器;RCC_RTCCLKCmd,启动RTCCLK,在调用上一个函数选择时钟之后,还需要调用一下这个Cmd函数,使能一下;另外还需要用到RCC_GetFlagStatus函数获取标志位,因为LSE时钟不是你让它启动它就能立刻启动的,调用启动时钟的函数之后,我们还需要等待一下标志位,等RCC的标志位LSERDY置1之后,这个时钟才算启动完成,工作稳定。有关RCC时钟部分的函数就这么多。
接下来我们继续看RTC库函数中的函数:RTC_ITConfig用于配置中断输出;RTC_EnterConfigMode,进入配置模式,就是置CRL的CNF为1,进入配置模式,其对应注意事项中的第三条;RTC_ExitConfigMode,退出配置模式,就是把CNF位清零;RTC_GetCounter,获取CNT计数器的值,显然,读取时钟就靠这个函数;RTC_SetCounter,写入CNT计数器的值,显然,设置时间,就靠这个函数;RTC_SetPrescaler,写入预分频器,这个值会写入到预分频器的PRL重装寄存器中,用来配置预分频器的分频系数;RTC_SetAlarm,写入闹钟值;RTC_GetDivider,读取预分频器中的DIV余数寄存器,余数寄存器是一个自减寄存器,获取余数寄存器的值,一般是为了得到更细致的时间,因为CNT计数间隔最短就是1s,如果需要更细致的时间,比如分秒、厘秒、毫秒,那就得靠这个DIV余数寄存器来实现;RTC_WaitForLastTask,等待上次操作完成,对应注意事项中的第四条,等待前一次写操作结束;RTC_WaitForSynchro,等待同步,对应注意事项中的第二条。
在RTC显示实时时钟代码中,我如何保证在系统复位后,保证时间信息仍然不被重置呢?我可以借助BKP中存储内容在单片机供电断开时仍然能靠电池保存的特性,在初始化代码中加入判断:如果BKP中任一一个数据寄存器中内容不等于约定好的数字,说明现在是单片机电源掉电、电池电源掉电之后重新启动,因此需要进行初始化,并且在BKP对应寄存器中写入该数据;如果其中的内容等于数字了,说明电池并未掉电,则跳过初始化。