这一章节我们要讲的主要内容是 RTC 实时时钟,对应手册,是第 16 章的位置。
实时时钟这个东西,本质上是一个定时器,但是这个定时器,是专门用来产生年月日时分秒,这种日期和时间信息的。所以学会了 STM32 的 RTC,你就可以在 STM32 内部拥有一个独立运行的钟表,想要记录或读取日期和时间,就可以通过操作 RTC 来实现。
那 RTC 这个外设呢,比较特殊,它和备份寄存器 BKP、电源控制 PWR 这两章的关联性比较强,在 RTC 这一章,BKP 和 PWR 也会经常来串门,所以我们这章节,就把 BKP 和 RTC 放在一起介绍,这样整体思路会比较清晰,PWR 电源控制,我们下章节再介绍。
然后,我们这一大章节,分三小节来介绍。第一小节,会单独介绍一下时间戳这个东西,这也是个蛮有意思的知识点。想要使用这款 STM32 的 RTC,学习时间戳的知识点还是非常必要的;第二小节,我们就学习 BKP 和 RTC 外设的结构;最后,第三小节,就是写代码,来完成程序现象了。这就是本章节的安排。
好,那先看一下我们最终的程序现象,本节一共有两个实例代码,12-1 读写备份寄存器,也就是读写 BKP;12-2 实时时钟,就是 OLED 显示年月日时分秒了。
先看一下第一个代码,这里,我们要在 STLINK 上再引出一根 3.3V 的电源,接到 VBAT 引脚,这根线就模拟一个电池的电源。一般情况下,VBAT 是电池供电口,需要接备用电池,但是我们目前套件里没有电池,所以就直接引出一根 3.3V 电源线来,也是一样的效果。那看一下显示屏,这个程序的目的是,在 BKP 备份寄存器写入两个数据,然后再把它们读出来,显示一下,目前 W 是写的内容,我们还没有写入数据,R 是读的内容,默认读出来都是 0。然后,我们可以按一下按钮,这时就在 2 个备份寄存器中,分别写入了 1234 5678,之后,读出来,也是 1234 5678,写入和读出是一样的,没问题。那继续按按键,我们会改变数据,再写入进去,下面读出来,和写入一样,都没问题。其实 BKP 备份寄存器和上一节学的 Flash 存储器类似,都是用来存储数据的,只是 Flash 的数据是真正的掉电不丢失,而 BKP 的数据,是需要 VBAT 引脚接上备用电池来维持的,只要 VBAT 有电池供电,即使 STM32 主电源断电,BKP 的值也可以维持原状。
那我们试一下,拔掉 STM32 板子最下面这个主电源的正极引脚,现在 STM32 断电,但是 VBAT 有电,可以维持 BKP 的数据,再次上电后,在没有写数据的情况下,直接读出 BKP,它的数据和断电之前是一样的,这说明 BKP 的数据在主电源断电后,得到了保持,并且在系统复位后,可以按下复位键,BKP 的数据也不会复位,那如果我们把 VBAT 的电池断电,再次拔掉主电源,重新上电,BKP 的数据就清零了,因为 BKP 本质上,并不能完全掉电不丢失,它的数据,需要 VBAT 引脚提供备用电池来维持,这就是 BKP 备份寄存器的特性。如果你的 STM32 接了备用电池,那 BKP 可以完成一些主电源掉电时,保存少量数据的任务,这就是第一个代码的现象。
其实备份寄存器和 VBAT 引脚的存在,更多的是为了服务 RTC 的,所以我们接着看第二个代码,实时时钟。这就是实时时钟的现象,第一行是日期,目前是给的一个测试时间,2023 年 1 月 2 日,第二行是时间,目前是 0 时 0 分 xx 秒,第三行是时间戳的秒计数器,目前是 16 亿多,这个什么意思,等会儿就来学习。第四行是 RTC 预分频器的计数值,这个先看一下就行,用途我们写代码的时候再研究,这就是我们这个实时时钟的显示。
当然实时时钟,光有显示还不够,为了保证时间不出错,他还要有其他特性。首先是复位,既然你在计时,总不能每次复位都重新设置时间吧,我们按下复位键,可以看到,时间会继续运行,不会复位。然后,实时时钟,在系统主电源断电后,它还需要继续运行,就像我们手机一样,关机后,里面的时钟还必须要继续走,要不然时间就错了,是吧,所以只要在 VBAT 接上了备用电源,我们再断开系统主电源,然后插上,可以看到时间数据不会丢失,并且,在主电源断开的时间里,RTC 会继续走时,不会因为主电源断电而暂停,这就是 RTC 实时时钟的程序现象。可以发现,RTC 这个复位和主电源掉电后,数据不丢失,就是借用 BKP 来实现的,所以 RTC 和 BKP 关联程度是比较高的,这就是实时时钟的程序现象。
另外在这里,还要提几个在测试程序的时候,遇到的硬件 bug。
首先是,有的芯片,我给主电源断电后,VBAT 的电源还会给微弱地整个系统供电,这导致我主电源拔掉后,电源指示灯和 OLED 屏幕还会微弱的亮着,这是一个问题,当然这个问题其实也不影响最终的实验现象。
然后是还有的芯片,在进行 RTC 实验时,会出现 RTC 晶振不起振的情况,这会导致程序卡死在等待晶振起振的地方,这个问题还没找到完美的解决方法。但是在学习过程中,也是可以有一些替代方法可以使用的,所以这些问题先给大家提个醒,替代方法,我们后续写代码的时候再说。
好,那程序现象我们就看到这里。
1. Unix 时间戳
在这一小节,我将会介绍,时间戳是什么东西,为什么要使用时间戳来计时。然后 UTC 和 GMT 是什么东西,这一块就是一些科普性质的知识点。然后就是时间戳里的秒计数器和日期时间数据如何互相转换,这涉及到 C 语言中的 time.h 这个官方函数库。这里我会在 DevC++ 这个软件里,一一调用这些函数,来给大家演示它们的用法。
所以我们本小节的任务有两个。
- 了解时间戳,它到底是什么东西。
- 会使用 C 语言 time.h 里面的这些函数进行时间戳各种形式数据的转换。
那本小节的内容,其实是计算机领域的一个通用知识点,不特别应用在 STM32 中,所以学完本小节,你之后在其他地方,说不定也能用得到。好,那我们来看一下
1.1 Unix 时间戳简介
Unix 时间戳最早是在 Unix 系统使用的,所以叫 Unix 时间戳。之后很多由 Unix 演变而来的系统,也都继承了 Unix 时间戳的规定。目前 Linux、Windows、安卓这些系统它们底层的计时系统,都是使用的 Unix 时间戳。所以在我们现在计算机世界的底层,Unix 时间戳还是在扮演着重要的角色的。
- Unix 时间戳(Unix Timestamp),它的定义是从 UTC/GMT 的 1970 年 1 月 1 日 0 时 0 分 0 秒开始所经过的秒数,不考虑闰秒。
这里大家可能有些疑问:
第一,UTC/GMT,这个是什么东西。
第二,闰年、闰月,这些我们听得比较多,但是这个闰秒,是个什么东西呢。
这两个知识点,我们等会儿再介绍。
现在这句话,我们简单理解一下,意思就是,时间戳是一个计数器数值,这个数值表示的是一个从 1970 年 1 月 1 日 0 时 0 分 0 秒开始,到现在,总共所经过的秒数,所以时间戳这个计时系统,和我们常用的年月日时分秒这个计时系统有很大差别。年月日时分秒计时系统是每 60 秒,进位一次,记为 1 分钟,每 60 分钟进位 一次,记为 1 小时,之后继续进位,就是日、月、年了。而时间戳计时系统就比较简单粗暴了,它定义 1970 年 1 月 1 日 0 时整为 0 秒,之后,就只用最基本的秒来计时,永不进位,60s 就是 60s,100s 就是 100s,一千秒、一万秒、一亿秒,无论这个数有多大,我都不进位,始终都只用秒来计时。所以从 1970 年计到现在,这个时间戳的秒数已经非常大了,目前这个秒数,已经来到了 16 亿这个数量级了。对于人类来说,这个 16 亿秒,肯定是又难记又难理解;但是对于计算机来说,一个永不进位的秒,无论是存储,还是计算,都是非常方便的。所以时间戳在计算机程序的底层,应用非常广泛,时间戳的秒计数器和日期时间,可以互相转换,在计算器的底层,我们使用秒计数器来计时,需要给人类观看时,我们就转换为年月日时分秒这样的格式就行了。
那使用这样一个很大的秒数来表示时间,有很多好处。
第一,就是简化硬件电路,我们在设计 RTC 硬件电路的时候,直接弄一个很大的秒寄存器就行了,不需要再考虑什么年月日寄存器、进位,大月小月、平年闰年这些东西了。对于硬件电路设计来说,是非常友好的。
第二,就是在进行一些时间间隔的计算时,非常方便。比如 1 月 1 号 8 点到 3 月 1 号 18 点之间间隔了多少小时啊?这个如果用年月日时分秒来计算的话,需要考虑的东西就比较多了。但如果用秒计数器来算的话,我们只需要把两个时刻的秒数相减,再除一个小时的秒数,就可以很快计算两个时刻的间隔了。
第三,就是存储方便,存储秒数,一个比较大的变量就行了,存储年月日时分秒的话,就得很多变量了。
那当然使用秒计数器来表示时间,也有坏处。
就是比较占用软件资源,在每次进行秒计数器和日期时间转换时,软件都要进行一通比较复杂的计算,这会占用一些软件资源,那这就是使用时间戳的一些好处和坏处。
时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
那计算机为了存储这样一个永不进位的秒数,这个数据变量类型还是要定义大一些,对吧,这个变量类型,在不同系统中,定义是不一样的。在早期的 Unix 系统中,这个秒数大多是用 32 位有符号的整形变量来存储的。32 位有符号数,所能表示的最大数字是 232/2 - 1 = 21 亿多,这其实是有溢出风险的,因为目前到 2023 年,时间戳已经计到 16 亿了,再过一些年,32 位有符号数,就存不下这么大的数字了。那根据计算,32 位有符号数的时间戳会在 2038 年的 1 月 19 号溢出,到时候,采用 32 位有符号数存储时间戳的设备,计时系统就会因为数据溢出而出错,这可能会导致很多不健全的计算机程序崩溃,这就是 2038 年危机,大家感兴趣的话可以网上搜一搜。那当然,随着操作系统和设备的更新换代,目前的手机电脑等设备,基本上都已经采用 64 位的数据来存储时间戳了,64 位的时间戳,能存储的时间范围非常非常的大,总之,对于人类来说,完全可以高枕无忧了。最后我们本节 STM32 中的 RTC,可以看一下手册,可以看到,它核心的计时部分是一个 32 位的可编程计数器,这说明我们这款 STM32,它的时间戳是 32 位的数据类型,32 位的时间戳,这表示我们这个 STM32 也会在 2038 年出现 bug 吗?实际上并不会,因为根据研究,这个时间戳在 STM32 程序中定义的其实是无符号的 32 位,无符号 32 位最大数值是 232 - 1,计算一下,要到 2106 年才会溢出,虽然不是高枕无忧,但是有生之年,八成是不用担心。好,这就是时间戳的存储格式和溢出风险的分析。
世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间
我们知道,地球上不同经度,它的时间是不一样的,穿过英国伦敦的经线,我们把它叫做本初子午线,这个位置的时间是一个时间标准。我们时间戳所说的 1970 年 1 月 1 日 0 时 0 分 0 秒,也是指的伦敦时间的 0 时 0 分 0 秒。那其他地方呢,可以分为 24 个时区,每偏差一个时区,时间就要加或减一个小时,我们处理不同时区的方式是所有时区共用一个时间戳的秒计数器,也就是在伦敦秒计数器是 0,在北京也是 0,然后根据不同时区,我们再添加小时的偏移即可。比如秒计数器的 0 对应伦敦时间的 0 点,那中国,使用北京时间,处于东 8 区的位置,对应北京的时间,就是 8 点。这就是时间戳对不同时区的处理方式。
那最后看一下下面这个图,总结一下上面的知识点。
图中这个箭头,代表的是一个时间轴。在这个时间轴上,我们要定义一个起点,时间戳从这个起点开始计时,这个起点是人为规定的,当时的设计者选择了伦敦时间的 1970 年 1 月 1 日 0 点。
对于 1970 年之前的时间,时间戳是无法表示的,那时间戳有两种表现形式。
一种是它的基本形式,也就是永不进位的秒计数器,从 0 开始,一直往后,每过 1s,加一个数;
另一种就是秒计数器经过计算,翻译出来的日期和时间了,比如 0s,对应伦敦时间 1970 年 1 月 1 日 0 点,然后秒计数器一直计啊计,比如计到这个 10 亿秒的时候,就对应伦敦时间 2001 年 9 月 9 日 1 时 46 分 40 秒。
那我咋知道 10 亿秒对应这个日期的时间呢?这背后要经过一些比较复杂的计算。比如先算一年有多少秒,得到现在是哪一年,然后再算一天有多少秒,得到现在是一年的第几天,然后再计算现在是几月几号,最后再计算是几时几分几秒。这里面还需要考虑大月小月、平年闰年这些特殊情况。
所以可以想到,这个计算是非常麻烦的,但是好在,这个计算步骤是固定的。而且,C 语言官方已经帮我们把程序写好了,这就是我们等会要学的 time.h 这个模块。这里面就有现成的,秒计数器转换日期时间,日期时间转换秒计数器这些函数。所以这里,我们只要会调用 time.h 的函数,就可以知道这些秒计数器和日期时间的对应关系了。至于计算步骤,我们不用过多了解,感兴趣的话可以自行研究。那有了 time.h 里的函数,这个秒计数器的计算,就非常简单了。比如 1672588795 这个秒数调用函数一计算对应的伦敦时间就是 2023 年 1 月 1 日 15 点 59 分 55 秒,那最后一行,在伦敦时间的基础上,得到北京时间,就比较简单了,每个秒计数器对应的伦敦时间,再加上 8 个小时就是对应的北京时间,这就是这个 Unix 时间戳整个的设计思路。
最后可以给大家推荐一个网站工具,比如在百度直接搜索 Unix 时间戳,然后就可以看到很多时间戳在线转换工具,我们打开网站,里面就有别人做好的转换工具。比如显示的是现在这个时刻对应的秒计数器,就是这么多秒;然后时间戳,就是秒计数器,你输入多少秒,点转换,它就能告诉你,对应的北京时间是多少;然后你输入一个日期时间,点转换,它就能告诉你对应的秒计数器是多少。当然这里好像只能转换北京时间,比如给个 0s,因为是北京时间,它对应的就是 8 点,这个我们也应该清楚是怎么回事,这就是这个时间戳在线工具。大家写代码的时候,可以参考这个工具来进行验证,这个了解一下。
好,时间戳的基础知识我们就了解这么多。