BKP 备份寄存器 RTC 实时时钟-stm32入门

2023-12-21 01:30

本文主要是介绍BKP 备份寄存器 RTC 实时时钟-stm32入门,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这一章节我们要讲的主要内容是 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++ 这个软件里,一一调用这些函数,来给大家演示它们的用法。

所以我们本小节的任务有两个。

  1. 了解时间戳,它到底是什么东西。
  2. 会使用 C 语言 time.h 里面的这些函数进行时间戳各种形式数据的转换。

那本小节的内容,其实是计算机领域的一个通用知识点,不特别应用在 STM32 中,所以学完本小节,你之后在其他地方,说不定也能用得到。好,那我们来看一下

1.1 Unix 时间戳简介

Unix 时间戳最早是在 Unix 系统使用的,所以叫 Unix 时间戳。之后很多由 Unix 演变而来的系统,也都继承了 Unix 时间戳的规定。目前 Linux、Windows、安卓这些系统它们底层的计时系统,都是使用的 Unix 时间戳。所以在我们现在计算机世界的底层,Unix 时间戳还是在扮演着重要的角色的。

  1. 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 点,这个我们也应该清楚是怎么回事,这就是这个时间戳在线工具。大家写代码的时候,可以参考这个工具来进行验证,这个了解一下。

好,时间戳的基础知识我们就了解这么多。

1.2 UTC/GMT

这里主要就是两个科普的内容,我们来了解一下 GMT、UTC 是什么东西,为什么会有闰秒这个现象。

首先看一下 GMT(Greenwich Mean Time,格林尼治标准时间/格林威治标准时间/格林威治平均时间)是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准。

格林尼治 是一个地名,位于英国伦敦,所以如果你对格林尼治这个名字不熟悉,可以简单理解,它就是伦敦标准时间。格林尼治这个地方,有个天文台,可以通过观察天上的太阳和星星,来确定地球的自转和公转。
那可以看出,这种计时方法非常符合我们的直觉,一天的定义,就是地球自转一周,然后一天等分 24 小时,再等分 60 分钟,再等分 60 秒,这样就能确定时间基础了。当然我这里是简单的理解,具体过程也能会更复杂一些。

那 GMT 是以前全球计时的时间标准,大家都遵循 GMT 的标准,不同时区再加上对应的小时偏移,这样全球各地的时间就能确定下来了。但为什么说 GMT 是以前的时间标准呢?这是因为 GMT 有一个棘手的问题,就是地球自转一周的时间,其实是不固定的,由于潮汐力,地球活动等原因,地球目前是越转越慢的。那你再根据一天的时间来定义时间基准,这个时间基准就是在不断变化的。比如你把一天等分为 24 小时对应的秒数,地球越转越慢,那你定义 1s 的时间,是不是也就越来越长啊,一个不固定的时间基准,对科学研究影响非常大。比如我们说光速是多少 m/s,声速是多少 m/s,前提是 1s 到底是多长,必须是一个恒定不变的量,所以说,为了时间的定义更标准,科学家又提出了新的计时系统,叫做 UTC。

UTC(Universal Time Coordinated,协调世界时)是一种以原子钟为基础的时间计量系统。它规定 铯133 原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致

原子钟是当前计时最精确的装置,上千万年才误差 1 s,所以使用原子钟提供的时间,具有定义明确,恒定不变这些好的特征。也就是使用原子钟计时,1s 到底是多长,我们就可以定死了。那最初,我们确定的这个参数,它定义 1s 的时长,是和 1970 年的 GMT 保持一致的。那现在问题又来了,我们以一个恒定不变的秒来计时,但是地球自转越来越慢,这样记下去,计时的一天和自转的一天就会出现偏差,时间长一些,可能中午 12 点,太阳就不是最高的位置。或者时间再长一些,计时的白天黑夜就会和现实的白天黑夜颠倒,这是我们不能忍受的,虽然说地球自转变慢的过程非常缓慢,误差大到白天黑夜颠倒,那得很久很久了,但是科学家对精度的极致追求,不能容忍哪怕 1s 的偏差,所以在原子钟计时系统的基础上,我们得加入闰秒的机制,来消除计时一天和地球自转一周的误差。闰秒的操作流程就是当原子钟计时一天的时间,与地球自转一周的时间相差超过 0.9s 时,UTC 会执行闰秒来保证其计时与地球自转的协调一致。所谓闰秒,就是计时标准是恒定不变的,但是地球越转越慢,误差超过 0.9s 时,我的计时系统就多走一秒,来等一下地球的自转,比如上一次闰秒的时刻是北京时间 2017 年 1 月 1 日 7 时 59 分 59 秒,在下一秒时,时钟会出现 7 时 59 分 60 秒,一分钟总共是 61 秒,这就是闰秒的操作。恒定的时间标准加上闰秒机制的设计,就能保证 UTC 既满足科学研究的需要,又满足人类生活的需要,这就是协调世界时的设计思路。

UTC 是现行的时间标准,它比 GMT 更加严谨,但是闰秒机制的设计,可能也会造成一些程序 bug,所以大家要有这个准备,就是一分钟可能会出现 61s 的情况。那在平时的生活中,大多不会追求极致的严谨,所以这时 GMT 和 UTC 可以看成是一样的。像我们手机电脑的时间设置里,可能就是说,我们当前的北京时间是 GMT+8 或者 UTC+8,这都是可以的,这就说明我们使用的是东 8 区的时间。好,这就是 UTC 和 GMT 的介绍还有闰秒机制产生的原因了,那再看时间戳的定义,UTC/GMT 的 1970 年 1 月 1 日 0 时 0 分 0 秒实际上就是格林尼治的当地时间,也就是伦敦时间,不考虑闰秒,说明目前这个时间戳对闰秒没有适应性,每次产生闰秒时,时间戳的时间和国家授时中心的标准时间就会产生 1s 的偏差,这个了解一下。

那时间戳的基础知识,大家就清楚了,接下来就是实践部分。

1.3 时间戳转换

我们来学习,时间戳中秒计数器和日期时间,如何进行相互转换。这时我们需要用到 time.h 模块。

C 语言的 time.h 模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换,使用还是非常方便的,直接调函数,填参数,就行了。

在 time.h 里,主要有以下这么多函数:

并不是全部,还有两个不太重要的函数没列出来。

函数作用
time_t time(time_t*);获取系统时钟
struct tm* gmtime(const time_t*);秒计数器转换为日期时间(格林尼治时间)
struct tm* localtime(const time_t*);秒计数器转换为日期时间(当地时间)
time_t mktime(struct tm*);日期时间转换为秒计数器(当地时间)
char* ctime(const time_t*);秒计数器转换为字符串(默认格式)
char* asctime(const struct tm*);日期时间转换为字符串(默认格式)
size_t strftime(char*, size_t, const char*, const struct tm*);日期时间转换为字符串(自定义格式)

如何去学习这些函数呢?其实网上也有很多的教程,大家自学的时候,都可以去网上搜索相关资源。重要的函数,我给大家演示一下,这些函数中,数标记的三个最为重要。其中, gmtime 就是秒计数器转换为 GMT,格林尼治日期时间的函数;localtime 就是秒计数器转换为当地日期时间的函数,就是在 gmtime 的基础上加一个时区偏移,所以这两个函数,是非常相似的;mktime 就是日期时间转换为秒计数器的函数,这个就只有当地的时间。有了这 3 个函数,我们就可以进行时间戳的转换了。

在这里插入图片描述

这个图就清晰地显示了每个函数的作用。就是在各种数据类型之间进行转换,为了明白函数的用途,我们首先得清楚,这 3 种数据类型都是什么意思。

  1. 秒计数器数据类型,它的数据类型名,叫做 time_t。time_t 其实是一个 typedef 重命名的类型,如果不是特别声明(define(_USE_32BIT_TIME_T)),我们要用 32 位的秒计数器类型,那么默认情况下,time_t,就是 __time64_t,然后 __time64_t 实际上就是 __int64,所以 time_t,实际上就是 int64 类型,是一个 64 位有符号的整型数据。所以可以看出,使用的是 64 位的秒计数器,不用担心溢出问题,这就是 time_t 数据类型。可以用来存储时间戳中那个一直自增的秒数
  2. 日期时间数据类型,类型名是 struct tm,这两个词组合在一起,代表一个结构体类型名。这个 tm 结构体,我们也可以在 time.h 里找到定义,它是一个封装的结构体类型,结构体的成员有
struct tm
{int tm_sec;//秒,取值范围 0~59int tm_min;//分钟,取值范围 0~59int tm_hour;//小时,取值范围 0~23int tm_mday;//一个月的几号,取值范围 1~31int tm_mon;//从 1 月开始的第几个月,取值范围 0~11,如果是 1 月,它的值是 0;一直到 12 月值是 11,所以这个参数值 + 1 才是我们所说的月份int tm_year;//从 1900 年的第几年,所以这个参数值加上 1900,才是我们所说的年份。另外注意,这个年份的偏移是 1900,我们时间戳的起点是 1970,这两个年份不一样,注意一下,所以这个参数最小值,就应该是 70。int tm_wday;//从周末开始的星期几,取值范围 0~6。0 表示周末,1 表示周一,2 表示周二,一直到 6,表示周六int tm_yday;//从 1 月 1 号开始的第几天,取值范围 0~365,这个参数我们平常用的不多int tm_isdst;//是否使用夏令时,+1 表示使用夏令时,0 表示不使用夏令时,-1 表示不知道。
};

夏令时这个东西,欧美地区的大部分国家,还有其它地区的少部分国家,都还在使用,我国最初也使用了一段时间,但是现在我国已经不用夏令时了,所以我们对夏令时这个东西可能比较陌生。夏令时简单来说,就是为了鼓励大家夏天的时候早睡早起、节约用电而设计的,感兴趣的话,大家自己再研究,这里就不给大家详细介绍了。

好,这个日期时间结构体,我们就了解了。它里面就是这样一个个,表示年月日时分秒,星期等内容的数据。当然这个结构体的定义,在形式上和我们 STM32 库函数里的方法有所区别,我们 STM32 中,使用的是 typedef struct {} 新名字; 这样的形式定义的,这里没有使用 typedef,而是在花括号前给结构体起了一个名字,叫 tm,这样在使用的时候,数据类型名,就是两个词 struct tm,然后跟着的是变量名,这样的方式也是可以的,和我们 STM32 库函数里的方式是一样的效果,这个大家了解一下。

那这就是日期时间结构体的内容,我们就清楚了。

  1. 字符串数据类型,类型名是 char*,就是 char 型数据的指针,用来指向一个表示时间的字符串,这个等会可以给大家演示。

好,3 种数据类型,我们就准备好了。接下来我们来使用函数,尝试一下数据类型的转换,可以看到,这些函数中,大量的出现了指针的操作,不熟悉指针操作的话,建议再看一下我空间里的指针教程,要不然你不容易理解这些函数的用法。其实像这些官方的模块,真的是遍地是指针,自己写程序的话,为了方便理解,一般用指针还是比较少的,但是耐不住别人都用指针,所以指针,大家还是要好好学一学的。

  1. time_t time(time_t*); 作用是获取系统时钟。返回值是 time_t,表示当前系统时钟的秒计数器值;参数是 time_t*,这是一个输出参数,输出的内容和返回值是一样的,所以这个函数可以通过返回值获取系统时钟,也可以通过输出参数获取系统时钟。
    这个函数,在电脑里,可以直接读取电脑的时间,但是在 STM32 里是用不了的,因为 STM32 是一个离线的裸机系统,它也不知道现在是啥时间。

  2. struct tm* gmtime(const time_t*); 将秒计数器的值转换为格林尼治日期时间,也就是伦敦时间。参数是 const time_t*,秒计数器指针类型,是输入参数;返回值 struct tm* 是日期时间结构体指针类型。

  3. struct tm* localtime(const time_t*); 秒计数器转换当地时间,这个函数和 gmtime 的使用方法是一样的,只是 localtime 会根据时区自动添加小时的偏移。

  4. time_t mktime(struct tm*); 就是上面两个函数的逆过程了,它是将日期时间转换为秒计数器,当然 mktime 传入的日期时间,需要是当地的。参数是日期时间结构体指针类型;返回值 time_t 是秒计数器类型。

另外再说明一下,mktime 的参数前面并没有加 const,实际上这个参数既是输入参数,也是输出参数。它内部的工作过程是:日期时间结构体,里面由年月日时分秒星期等数据,但是仅通过年月日时分秒,就足以算出秒计数器了,你填的星期参数,实际上是不作为输入参数的;相反,这个函数在算出秒数的同时,还会顺便算一下当前年月日是星期几,然后回填到结构体里面的星期之中,所以使用这个函数,给定一个年月日,我们可以很方便的计算对应的是星期几,这个功能大家可以自己试一试,我就不再演示了

实际上这个 time.h 里面重要的部分,我们就已经讲完了,也就是秒计数器和日期时间计算比较麻烦,我们需要用这些现成的函数。

下面这三个函数,实际上就是把时间转换为字符串表示,这就比较简单了,我们不用它的函数,也能很方便的操作,如果你需要用的话,我们也演示一下使用方法。

  1. char* ctime(const time_t*); 就是把秒计数器转换为 char* 格式的字符串,使用默认的格式。
  2. char* asctime(const struct tm*); 就是把日期时间转换为字符串,使用默认的格式。
  3. size_t strftime(char*, size_t, const char*, const struct tm*); 日期时间转换为字符串(自定义格式),这个函数就比较高级了,它的作用和 asctime 是一样的,但是可以自定格式。它总共有四个参数,前面两个参数,需要传入一个字符数组和数组长度,第三个参数,需要给定指定的格式字符串,第四个参数,把 time_date 传进去就行了。
#include <stdio.h>
#include <time.h>time_t time_cnt;
struct tm time_date;
char* time_str;int main(void) {
//1.//time_cnt = time(NULL);			//1. 参数不需要的话可以给 NULL,这样就得到了时间(当前时间戳的秒数)//time(&time_cnt);					//2. 用输出参数来获取,这两条语句的效果是一样的。time_cnt = 1672588795;				//3. 另外我们可以手动给它一个数值printf("%d\n", time_cnt);//2.//gmtime(&time_cnt);				//根据传进去的数值,函数内部就会经过一通计算,返回值就是日期时间了time_date = *gmtime(&time_cnt);		//1. 结构体变量互相赋值//struct tmj* ptime_date;//ptime_date = gmtime(&time_cnt);	//2. 结构体指针互相赋值printf("%d\n", time_date.tm_year + 1900);//从 1900 年经过的年数printf("%d\n", time_date.tm_mon + 1);//从 1 月经过的月数printf("%d\n", time_date.tm_mday);printf("%d\n", time_date.tm_hour);printf("%d\n", time_date.tm_min);printf("%d\n", time_date.tm_sec);printf("%d\n", time_date.tm_wday);//3.time_date = *localtime(&time_cnt);		//这个函数内部会根据当前电脑的设置,自动判断我们处于哪个时区,然后把时间添加时区偏移后,输出出来//localtime 函数判断我们在东 8 区,就自动把时间加了 8 个小时输出,显示是 23 点,这与给定的北京时间是一致的。printf("%d\n", time_date.tm_year + 1900);printf("%d\n", time_date.tm_mon + 1);printf("%d\n", time_date.tm_mday);printf("%d\n", time_date.tm_hour);printf("%d\n", time_date.tm_min);printf("%d\n", time_date.tm_sec);printf("%d\n", time_date.tm_wday);//4.time_cnt= mktime(&time_date);		//这个函数就会经过一通计算,给我们返回对应的秒计数器的值printf("%d\n", time_cnt);//可以发现最终的秒数和最初的秒数是一样的,这说明 mktime 给它传入当地时间,是正确的,不是依据伦敦时间来进行的//5.time_str = ctime(&time_cnt);//返回值是 char* 的字符串printf(time_str);//西方国家的格式习惯,我们中国,一般不用这么奇怪的格式,所以,这个函数我们用的不多time_str = asctime(&time_date);//实际上是同样的效果,只是它的参数不一样而已printf(time_str);//最终这两个函数运行的效果是完全一样的,这两个函数,了解即可char t[50];strftime(t, 50, "%H-%M-%S", &time_date);//第三个参数可以参考函数定义中的格式定义表,实际上这个就类似于 printf 第一个参数的格式字符串。左边是占位符格式,右边是解释和实例,比如我们想写小时,就是 %H,分钟,就是 %M,秒,就是 %S,其他这些格式大家都可以一一尝试。在程序中第三个参数给个字符串。//%什么 是占位符,打印时会替换为后面时间的具体值,其他的符号,会保留原始内容。打印的字符串,通过前两个参数到指定一个数组里。这就是这个函数的作用。printf(t);//可以看到,它就按照我们指定的格式来打印字符串了。return 0;
}

好,到这里,我们这个 time.h 的部分重要函数,就讲完了,然后剩下,time.h 里还有几个函数没讲到。大家可以在函数库里自行学习,主要就是这个 clock 函数,可以用来计算程序执行了多长时间,然后 difftime,可以计算两个时间之间的差值。其他的函数,好像都提到过,当然最重要的函数,还是 localtime 和 mktime 这两个,这是整个 time.h 里最复杂的函数,也是我们 STM32 的 RTC 程序会用到的,所以这两个重点掌握,其他的了解即可。

那本小节的两个任务,我们就完成了,一个是了解 Unix 时间戳,另一个是会进行时间戳不同数据类型的转换,这就是本小节的内容。

2. BKP 和 RTC 的外设部分

当然我们本节的重点是 RTC,所以 BKP 这部分内容比较少,要求也不高。大家知道 BKP 是什么,然后会读写这些数据寄存器就行了。之后 RTC 的部分呢,就需要我们重点掌握了。这个等会再细讲。

2.1 BKP 简介

那我们先看 BKP 的部分,首先看一下简介。

BKP(Backup Registers)备份寄存器/后备寄存器

BKP 用途:可用于存储用户应用程序数据。

BKP 就是一些存储器,可以储存自定义数据,想存啥就存啥。

BKP 特性:当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。

这里的 VDD 就是系统的主电源,供电电压是 2.0~3.6V;VBAT(V Battery)就是备用电池电源,供电电压是 1.8~3.6V。可以看一下引脚定义表中标红色的部分就是供电引脚,下面这三组,VDD 和 VSS_1、2、3 是内部数字部分电路的供电;上面这一组 VDDA 和 VSSA 是内部模拟部分电路的供电,那这四组以 VDD 开头的供电,都是系统的主电源,在正常使用 STM32 时,这四组供电全部都需要接到 3.3V 的电源上。最后,上面这还有一个引脚,VBAT 这就是备用电池供电引脚,如果要使用 STM32 内部的 BKP 和 RTC,这个引脚就必须接备用电池,用来维持 BKP 和 RTC,在 VDD 主电源掉电后的供电,当然这里备用电池只有一根正极的供电引脚,接电池时,电池正极接到 VBAT,电池负极和主电源的负极接在一起,共地,就行了。
然后看一下我们最小系统板的原理图,这里可以看到 VBAT 引脚,直接通过排针引出来了,这个引脚,就位于我们板子右上角的地方,引脚标号是 VB,或者 VBAT;另外这里可以看出,如果不接电池的话,VBAT 引脚是悬空的,当然,STM32 参考手册里建议的是,如果没有外部电池,建议 VBAT 引脚接到 VDD,就是 VBAT 和 主电源接到一起,并且再连接一个 100nF 的滤波电容,这是手册里的建议,大家要是自己设计电路的话,可以注意一下这个问题。
好,那这样,这个 VDD 主电源和 VBAT 备用电源,我们就清楚了。

  1. VBAT 的作用就是当 VDD 断电时,BKP 会切换到 VBAT 供电,这样可以继续维持 BKP 里面的数据,如果 VDD 断电,VBAT 也没电呢,那 BKP 里的数据就会清零,因为 BKP 本质上是 RAM 存储器,没有掉电不丢失的能力。
  2. 然后后面一句的意思是待机唤醒或者复位时,BKP 的数据保持原样,这个特性是显然要有的。要不然,你说你 VDD 掉电保持数据,结果 VDD 一上电复位,你数据也跟着清除了,那掉电保持,就没有意义了。

这就是 BKP 存储器的特性。

BKP 的几个额外的功能:(这些功能大家了解即可,我们本节暂时不涉及)

  • TAMPER 引脚产生的侵入事件将所有备份寄存器内容清除

TAMPER 是一个接到 STM32 外部的引脚,它的位置可以参考一下引脚定义表,这里可以看到 PC13-TAMPER-RTC 也就是 PC13、TEMPER、RTC 这 3 个功能共用一个引脚,引脚位置,就是 VBAT 旁边的 2 号引脚。这个 TEMPER 引脚是一个安全保障设计,比如如果你做一个安全系数非常高的设备,设备需要有防拆功能,然后 BKP 里也存储了一些敏感数据,这些数据不能被别人窃取或者篡改,那你就可以使能这个 TAMPER 引脚的侵入检测功能。设计电路时,TAMPER 引脚可以先加一个默认的上拉或者下拉电阻,然后引一根线到你的设备外壳的防拆开关或触点,别人一拆开你的设备,触发开关,就会在 TAMPER 引脚产生上升沿或者下降沿,这样 STM32 就检测到侵入事件了,这时 BKP 的数据会自动清零,并且申请中断,你在中断里,还可以继续保护设备,比如清除其他存储器数据,然后设备锁死,这样来保障设备的安全;另外主电源断电后,侵入检测仍然有效,这样即使设备关机,也能防拆,这就是 TAMPER 侵入检测的功能,大家了解一下。

  • RTC 引脚输出 RTC 校准时钟、RTC 闹钟脉冲或者秒脉冲

RTC 引脚刚才看过了,也是在 PC13 这个位置,这就是 RTC 时钟输出的功能,RTC 的校准时钟,闹钟或者秒脉冲的信号,可以通过 RTC 引脚输出。其中,外部用设备测量 RTC 校准时钟,可以对内部 RTC 微小的误差进行校准;然后,闹钟脉冲或者秒脉冲可以输出出来,为别的设备提供这些信号,这是 RTC 时钟输出的功能。因为 PC13、TEMPER 和 RTC 这 3 个引脚共用一个端口,所以这 3 个功能,同一时间,只能使用一个。

  • 存储RTC时钟校准寄存器

这个可以配合上面这个校准时钟输出的功能,结合一些测量方法,可以对 RTC 进行校准。那这两个功能,实际上就是 RTC 的配置,我觉得放在 RTC 那个外设的地方应该比较合适。当然 RTC 和 BKP 关联程序比较高,设计者目前就是把这两个 RTC 的功能放在 BKP 里了,这个大家知道一下。

那 BKP 的介绍和基本功能,即使上面这些。最后看一下,BKP 中,用户数据的存储容量:

  • 在中容量和小容量设备里,BKP 是 20 个字节。
  • 在大容量和互联型设备里,BKP 是 84 个字节。

我们使用的 C8T6 是中容量设备,BKP 就是 20 个字节。所以可以看出,BKP 的容量其实非常小,一般只能用来存储少量的参数,那这就是 BKP 的简介,我们就介绍到这里。

下面看一下 BKP 的基本结构。

在这里插入图片描述
这个图中橙色部分,我们可以叫做后备区域。BKP 处于后备区域,但后备区域不只有 BKP,还有 RTC 的相关电路也位于后备区域,STM32 后备区域的特性就是当 VDD 主电源掉电时,后备区域仍然可以由 VBAT 的备用电池供电,当 VDD 主电源上电时,后备区域供电会由 VBAT 切换到 VDD,也就是主电源有电时,VBAT 不会用到,这样可以节省电池电量。然后 BKP 是位于后备区域的,BKP 里主要有数据寄存器、控制寄存器、状态寄存器和 RTC 时钟校准寄存器这些东西,其中数据寄存器是主要部分,用来存储数据的,每个数据寄存器都是 16 位的,也就是,一个数据寄存器可以存 2 个字节,那对于中容量和小容量的设备,里面有 DR1、DR2、一直到 DR10 总共 10 个数据寄存器,那一个寄存器存两个字节,所以容量是 20 个字节,就是上面说的 20 字节。
然后对于大容量和互联型设备,里面除了 DR1 到 DR10 还有 DR11、DR12、一直到 DR42,总共 42 个数据寄存器,容量是 84 个字节,就是上面说的 84 字节。然后,BKP 还有几个功能,就是左边这里的侵入检测,可以从 PC13 位置的 TAMPER 引脚引入一个检测信号,当 TAMPER 产生上升沿或者下降沿时,清除 BKP 所有的内容,以保证安全;时钟输出,可以把 RTC 的相关时钟,从 PC13 位置的 RTC 引脚输出出去,供外部使用,其中,输出校准时钟时,再配合校准寄存器,可以对 RTC 的误差进行校准。

好,以上这些就是 BKP 这个外设的结构和功能。内容总体来说也不是很多,大家了解一下。

2.2 RTC 简介

那接下来,我们就继续来学习这个 RTC 外设。还是先看一下简介。

RTC(Real Time Clock)实时时钟

在 STM32 中,RTC 是一个独立的定时器,可为系统提供时钟和日历的功能

RTC 实时时钟,一般就是指提供年月日时分秒这种日期时间信息的计时装置。51 单片机的 DS1302 是外置的 RTC 芯片,这个芯片可以独立计时,我们需要设置时间或读取时间,就通过通信协议向它发送或接收数据来完成;那在我们 STM32 内部,有这个 RTC 的外设,所以 STM32 可以在内部直接实现 RTC 的功能,这样就不用再外挂 RTC 芯片了,当然 RTC 芯片所必要的元件,比如备用电池、RTC 晶振这些东西就要接到 STM32 上了。

RTC 和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时。

这个特性就和之前的 BKP 是一样的了。为了保持时钟能一直连续运行不出错,在主电源断电后,RTC 走时肯定不能停下来,在系统复位时,RTC 时间值肯定也不能复位,那为了实现这些功能,VBAT 接上备用电池就是必须的了。主电源断电后,VBAT 的电池可以继续维持 BKP 和 RTC 的运行。

32位的可编程计数器,可对应Unix时间戳的秒计数器

这一点可以对照 RTC 框图来理解,可以看到,这里负责计时的装置只有一个 32 位的秒计数器。如果你没学过我们上一小节讲的 Unix 时间戳,可能就会非常疑惑了,你想,这不是一个实时时钟外设么?那年呢,月呢,日呢,小时呢,分钟呢,之前学习 DS1302 的时候,那里面可是有一堆寄存器的,什么年月日时分秒,各种日期时间的信息,都一目了然,写入对应寄存器就是修改时间,读取对应寄存器就是获取时间。然后到这里,你咋就只给我一个秒呢?这让我怎么用,初学者看到这,可能会有这个疑惑。另外整个手册里,也都没有提到时间戳这个东西,所以如果你不了解时间戳相关的操作,那确实不太好用这个 RTC。但是,我们经过上一小节的学习,应该一眼就能看明白了这个是什么意思。
显然,这个 32 位可编程计数器,就对应的是时间戳里的秒计数器。在读取时间时,我们先得到这个秒数,然后使用 time.h 模块里的 localtime 函数,就能立刻知道年月日时分秒的信息了;在写入时间时,我们先填充年月日时分秒信息到 struct tm 结构体,然后用 mktime 函数,得到秒数,再写入到这个 32 位计数器即可。
这样,操作这个秒计数器的思路是不是就很清晰了,那得益于时间戳的设计,这个硬件电路就得到了极大的简化。你看要想实现年月日时分秒的计时,我们只需要一个 32 位的秒计数器即可,什么年月日,小时分钟的寄存器,都不需要再设计了,硬件也不再需要考虑大月小月、平年闰年这些特殊情况的,直接一个秒,一直加就行了,这无疑极大的简化了硬件电路的设计。那当然,硬件简化了,压力就来到了软件这边,我们每次读取和写入秒计数器时,都要进行时间戳的转换,这需要消耗一定的软件计算资源,这就是这个 32 位可编程计数器的设计。

20位的可编程预分频器,可适配不同频率的输入时钟

这可以继续对照 RTC 框图来理解。这里 32 位的计数器,显然 1s 要自增一次,所以这个地方,驱动计数器的时钟,需要是一个 1 Hz 的信号;但是实际提供给 RTC 模块的时钟,也就是这里的 RTCCLK,一般频率都比较高。所以显然,我们需要在这之间加一个分配器,给 RTCCLK 降一降频率,保证分频器输出给计数器的频率为 1 Hz,这样计时才是正确的,对吧。
那为了适配各种频率的 RTCCLK 呢,这里就加了一个 20 位的分频器,可以选择对输入时钟进行 1~220 这么大范围的分频,这样就可以适配不同频率的输入时钟,这就是这个可编程分频器的作用。

可选择三种RTC时钟源:

  • HSE 时钟除以 128(通常为 8MHz/128)
  • LSE 振荡器时钟(通常为 32.768KHz)
  • LSI 振荡器时钟(40KHz)

这 3 个时钟,可以选择其中一个,接入到这里的 RTCCLK,那这 3 个时钟都是什么意思呢?我们可以看一下之前定时器这里讲过的 RCC 时钟树,这个图就是整个芯片的时钟系统,整个芯片可以有 4 个时钟源,右下角写了,HSE,高速外部时钟信号;HSI,高速内部时钟信号;LSI,低速内部时钟信号;LSE,低速外部时钟信号。这些时钟字母,你就记住,H(High) 开头是高速,L(Low) 开头是低速,E(External) 结尾是外部,I(Internal) 结尾是内部,高速低速,内部外部一组合,就是 4 种情况。这里高速时钟,一般供内部程序运行和主要外设使用;低速时钟,一般供 RTC、看门狗这些东西使用。
那对于我们本节的 RTC 呢,我们可以看到下面有一个指向通往 RTC 的箭头,就是 RTCCLK,RTCCLK 有 3 个来源:

  1. 第一个是 OSC 引脚接的 HSE,外部高速晶振,这个晶振是主晶振,我们一般都用的 8 MHz,8 MHz 进来,通过 128 分频,可以产生 RTCCLK 信号。为什么要先 128 分频呢?这是因为这个 8 MHz 的主晶振太快了,如果不提前分频,直接给 RTCCLK,后续即使再通过 RTC 的 20 位分频器,也分不到 1 Hz 这么低的频率,所以 8 MHz 提前先进行 128 分频,后续 20 位的分频器,再进行一个适当的分频,就可以输出 1 Hz 的信号给计数器了,这是第一路来源,HSE 的时钟。
  2. 然后中间这一路,时钟来源是 LSE,外部低速晶振,我们在 OSC32 这两个引脚,接上外部低速晶振,这个晶振产生的时钟,可以直接提供给 RTCCLK,这个 OSC32 的晶振,是内部 RTC 的专用时钟。这个晶振的值,也不是随便选的,通过跟 RTC 有关的晶振,都是统一的数值,就是 32.768 KHz。为什么选择这个数值呢?一方面是,32 KHz 这个值附近的频率,是这个晶振工艺比较合适的频率,你要说非要做一个 1 Hz 的晶振,那可能是做不出来,或者做出来了,但体积很大,性能很差;另一方面是,32768,这是一个 2 的次方数,215 = 32768,所以 32.768 KHz,即 32768 Hz,经过一个 15 位分频器的自然溢出,就能很方便的得到 1 Hz 的频率。自然溢出的意思就是设计一个 15 位的计数器,这个计数器不用设置计数目标,直接从 0 计到最大值,就是计到 32767,计满后自然溢出,这个溢出信号就是 1 Hz,自然溢出的好处,就是不用再额外设计一个计数目标了,也不用比较计数器是不是计到目标值了,这样可以简化电路设计。所以目前在 RTC 电路中,基本都是清一色的 32.768 KHz 的晶振,你只要看到 32.768 KHz 的晶振,它八成就是提供给 RTC 的,这是第二路。
  3. 最后看第三路时钟源,来自于 LSI,内部低速 RC 振荡器。LSI,固定是 40 KHz,如果选择 LSI 当作 RTCCLK,后续再经过 40K 的分频,就能得到 1 Hz 的计数时钟了。当然内部的 RC 振荡器,一般精准度没有外部晶振高,所以 LSI 给 RTCCLK,可以当作一个备选方案,另外,LSI 还可以提供给看门狗,这个了解一下,之后我们介绍看门狗的时候再说。

那这 3 个时钟源呢,我们最常用的就是中间这一路,外部 32.768 KHz 的晶振,提供 RTCCLK 的时钟。
第一个原因就是,中间这一路,32.768 KHz 的晶振,本身就是专供 RTC 使用的,上下这两路,其实是有各自的任务。上面这一路,主要作为系统主时钟;下面这一路,主要作为看门狗时钟。它们只是顺带作为备选当作 RTC 的时钟,这么不专心的时钟,我们自然很少用它了。
另外一个更重要的原因就是,只有中间这一路的时钟,可以通过 VBAT 备用电池供电。上下两路时钟,在主电源断电后,是停止运行的,所以要想实现 RTC 主电源掉电继续走时的功能,必须得选择中间这一路的 RTC 专用时钟。如果选择的是上下两路时钟,主电源断电后,时钟就暂停了,这显然会导致走时出错。
所以这 3 路时钟,我们主要选择中间这一路,上下两路在特殊情况下,可以作为备选方案。这就是这 3 路时钟的介绍和选择问题。

这个 RTC 的简介我们就介绍完了。接下来我们来看一下这个 RTC 的框图。

2.3 RTC 框图

看一下这个 RTC 外设,具体是怎么设计的
在这里插入图片描述
先整体上划分一下,左边这一块是核心的、分频和计数计时部分,右边这一块是中断输出使能和 NVIC 部分;上面这一块是 APB1 总线读写部分;下面这一块是和 PWR 关联的部分,意思就是 RTC 的闹钟可以唤醒时钟,退出待机模式。然后在图中,我们看到有灰色填充的部分,都处于后备区域,这些电路在主电源掉电后,可以使用备用电池维持工作,另外这里还写了,这些模块在待机时都会继续维持供电,其他未被填充的部分,就是待机时不供电。有关睡眠、停机、待机、这些低功耗相关的内容,我们下节学 PWR 的时候再细讲,这里先了解一下就行。

然后我们依次详细看一下。

  1. 首先,看分频和计数计时部分,这一块的输入时钟是 RTCCLK。这个刚才说过,RTCCLK 的来源需要在 RCC 里进行配置,可以选择的选项是 3 种 RTC 时钟源,我们主要选择第二种,那因为这 3 种时钟频率各不相同,而且都远大于我们所需要的 1 Hz 的秒计数频率。所以 RTCCLK 进来,需要首先经过 RTC 预分频器进行分频,这个分频器由两个寄存器组成。上面这个是重装载寄存器 RTC_PRL;下面这个 RTC_DIV,手册里叫做余数寄存器,但实际上,这一块跟我们之前定时器时基单元里的计数器 CNT 和重装值 ARR,是一样的作用,可能是右边已经有一个计数器 CNT 了,所以这个名字就比较奇怪,叫做余数寄存器,但实际上,它还是计数器的作用。分频器其实就是一个计数器,计几个数溢出一次,那就是几分频,对吧。所以对于可编程的分频器来说,需要有两个寄存器,一个寄存器用来不断地计数;另一个寄存器,我们写入一个计数目标值,用来配置是几分频。那在这里,上面这个 PRL ,就是计数目标,我们写入 6,那就是 7 分频;写入 9,那就是 10 分频,因为计数值包含了 0,所以重装值写入几,就是 几+1 分频,这个和定时器那里是一样的。然后下面这个 DIV,就是每来一个时钟计一个数的用途了,当然这个 DIV 计数器,是一个自减计数器,每来一个输入时钟,DIV 的值自减一次,自减到 0 时,再来一个输入时钟,DIV 输出一个脉冲,产生溢出信号,同时 DIV 从 PRL 获取重装值,回到重装值继续自减。举个例子,比如 RTCCLK 输入时钟是 32.768 KHz,即 32768 Hz,为了分频之后得到 1 Hz,PRL 就要给 32767,这个数值是始终不变的,DIV 可以保持初始值为 0,那在第一个输入时钟到来时,DIV 就立刻溢出,产生溢出信号给后续电路,同时,DIV 变为重装值 32767;然后第二个输入时钟,DIV 自减变为 72766;第三个时钟,DIV 变为 32765,之后一直这样,来一个输入时钟自减一次,直到变为 0;然后再来一个输入时钟,就会产生一个溢出信号,同时 DIV 回到 32767,依次往复循环。这样的话,也就是每来 32768 个输入脉冲,计数器溢出一次,产生一个输出脉冲,这就是 32768 分频了,分频输出后的时钟频率是 1 Hz,提供给后续的秒计数器。
    好,然后看一下计数计时部分,这一块就比较简单了,32 位可编程计数器 RTC_CNT,就是计时最核心的部分。我们可以把这个计数器看作是 Unix 时间戳的秒计数器,这样借用 time.h 的函数,就可以很方便的得到年月日时分秒了。然后在下面这里,这个 RTC 还设计的有一个闹钟寄存器 RTC_ALR,这个 ALR 也是一个 32 位的寄存器,和上面这个 CNT 是等宽的,它的作用,顾名思义,就是设置闹钟,我们可以在 ALR 写一个秒数,设定闹钟,当 CNT 的值跟 ALR 设定的闹钟值一样时,也就是图中画的等号,如果它俩值相等,就代表闹钟响了,对吧。这时就会产生 RTC_Alarm 闹钟信号,通往右边的中断系统,在中断函数里,你可以执行相应的操作;同时,这个闹钟还兼具一个功能,就是图中下面这里的,闹钟信号可以让 STM32 退出待机模式,这个功能就可以对应一些用途。比如你设计一个数据采集设备,需要在环境非常恶劣的地方工作,比如海底、高原、深井这些地方,然后要求是,每天中午 12 点采集一次环境数据,其他时间,为了节省电量,避免频繁换电池,芯片都必须处于待机模式。这样的话,我们就可以用这个 RTC 自带的闹钟功能,定一个中午 12 点的闹钟,闹钟一响,芯片唤醒,采集数据,完成后,继续待机,这样是不是就可以完成这个任务了。另外,这个闹钟值是一个定值,只能响一次,所以如果你想实现周期性的闹钟,那在每次闹钟响之后,都需要在重新设置一下下一个闹钟时间,这就是这个闹钟和闹钟唤醒的一个用途。

  2. 那继续往右看,这就是中断部分了。在左边这里,有 3 个信号可以触发中断,第一个是 RTC_Second,秒中断,它的来源,就是 CNT 的输入时钟,如果开启这个中断,那么程序就会每秒进一次 RTC 中断;第二个是 RTC_Overflow,溢出中断,它的来源是 CNT 的右边,意思就是 CNT 的 32 位计数器计满溢出了会触发一次中断,所以这个中断一般不会触发,我们上一小节说过这个 CNT 定义是无符号数,到 2106 年才会溢出。所以这个中断,在 2106 年会触发一次,如果你想程序更完善一些,可以开启这个中断,到 2106 年,计数器溢出,为了避免不必要的错误,你可以让芯片罢工,然后提示当前设备过老,请及时更换。当然在 2106 年之后,这个 STM32 的 RTC 就不太好用了,到时候获取可以通过打补丁的方式继续运行,或者直接淘汰 32 位的时间戳,这个问题就留给后人解决吧;然后继续看,下面第三个 RTC_Alarm,闹钟中断,刚才说过,当计数器和闹钟值相等时,触发中断,同时,闹钟信号可以把设备从待机模式中唤醒。好,这就是这 3 个中断信号。
    中断信号到右边这里,这一块就是中断标志位和中断输出控制,这些 F(Flag) 结尾的是对应的中断标志位,IE(Interrupt Enable) 结尾的是中断使能,最后 3 个信号通过一个或门汇聚到 NVIC 中断控制器,中间这个地方是不是漏画了一根线,中间这个应该也是要通过或门的。好,这就是右边的中断部分。

  3. 然后上面这部分,APB1 总线和 APB1 接口,就是我们程序读写寄存器的地方了,读写寄存器,可以通过 APB1 总线来完成,另外也可以看出,RTC 是 APB1 总线上的设备。

  4. 最后下面这一块,退出待机模式,还有一个 WKUP 引脚。闹钟信号和 WKUP(Weak Up)引脚,都可以唤醒设备,WKUP 引脚可以看一下接线图,就是 PA0 的位置,它兼具唤醒的功能,这个我们下一节再学习。

好,到这里,我们这个 RTC 外设框图,就已经全部了解清楚了。

接下来看一下我这里给的基本结构,再总结一下以上内容吧。
在这里插入图片描述
RTC 的核心部分如图所示:最左边是 RTCCLK 时钟来源,这一块需要在 RCC 里配置,3 个时钟,选择一个,当作 RTCCLK;之后,RTCCLK 先通过预分频器,对时钟进行分频,余数寄存器是一个自减计数器,存储当前的计数值,重装寄存器是计数目标,决定分频值,分频之后,得到 1 Hz 的秒计数信号;通向 32 位计数器,1s 自增一次,下面还有一个 32 位的闹钟值,可以设定闹钟,如果不需要闹钟的话,下面这块可以不用管;然后右边有 3 个信号可以触发中断,分别是秒信号、计数器溢出信号和闹钟信号,3 个信号先通过中断输出控制进行中断使能,使能的中断才能通向 NVIC,然后向 CPU 申请中断。

在程序中,我们配置这个数据选择器,可以选择时钟来源;配置重装寄存器,可以选择分频系数;配置 32 位计数器,可以进行日期时间的读写;需要闹钟的话,配置 32 位闹钟值即可;需要中断的话,先允许中断,再配置 NVIC,最后写对应的中断函数即可,这就是 RTC 外设的主要内容。我们就介绍到这里。

2.4 硬件电路

那 STM32 内部的设计清楚了,外部的一些电路也还是需要的。为了配合 STM32 的 RTC,外部还是需要有一些电路的,在最小系统电路上,外部电路还要额外加两部分。
在这里插入图片描述

  • 第一部分就是备用电池。首先,备用电池供电部分,这里给了两个参考电路:
    1. 第一个是简单连接,就是使用一个 3V 的电池,负极和系统共地,正极直接引到 STM32 的 VBAT 引脚,这样就行了,这个供电方案非常简单,参考来源是 STM32 的数据手册,在 5.1.6 供电方案这里,就给出了这个图,图上画的就是直接接一个 1.8~3.6V 的电池到 VBAT 就行了,另外也可以看到,在内部是有一个供电开关的,当 VDD 有电时,开关拨到下面,后备电路由 VDD 供电;当 VDD 没电时,开关拨到上面,后备电路由 VBAT 供电。然后 VBAT 供电的设备,在这里写了,VBAT 供电的后备电路,有 32 KHz 的振荡器,RTC,唤醒电路和后备寄存器。那这就是根据数据手册里,设计的 VBAT 供电方案,这个设计非常简单,一般来说也没问题。
    2. 然后这里还给了第二种方案是推荐连接,这种连接方法是电池,通过二极管 D1,向 VBAT 供电,另外主电源的 3.3V,也通过二极管 D2,向 VBAT 供电,最后,VBAT 再加一个 0.1 μF 的电源滤波电容,这个供电方案的参考来源是 STM32 的参考手册。在这个 4.1.2 电池备份区域这一节,有这些描述,大家可以都看看。其中有几个建议,一个是,在手册描述的一些特殊情况下,电流可能通过 VDD 和 VBAT 之间的内部二极管注入到 VBAT,如果与 VBAT 连接的电源或者电池不能承受这样的注入电流,强烈建议在外部 VBAT 和电源之间连接一个低压降二极管;另一个是,如果在应用中没有外部电池,建议 VBAT 在外部连接到 VDD 并连接一个 100 nF 的陶瓷滤波电容。所以综合这两条建议,我们可以设计出右边的推荐连接,电池和主电源都加一个二极管,防止电流倒灌,VBAT 加一个 0.1 μF 的电源滤波电容,0.1 μF 就是 100 nF。如果没有备用电池,就是 3.3V 的主电源供电,如果接了备用电池,3.3V 没电时,就是备用电池供电,这是根据参考手册,设计的推荐电路。

如果你只是进行实验,那使用左边的简单连接就行了;如果你要画板子设计产品,那还是推荐使用右边的连接,这样更保险。

好,这是 VBAT 供电部分。

  • 第二部分就是外部低速晶振。看一下右边的外部低速晶振部分,这就是一个典型的晶振电路了。这里 X1 是一个 32.768 KHz 的 RTC 晶振,这个晶振不分正负极,两端分别接在 OSC32 这两个引脚上,然后晶振两端,再分别接一个起振电容,到 GND。这个电路的设计参考来源还是 STM32 的数据手册,在 5.3.6 外部时钟源特性这里有参考电路,使用一个晶体/陶瓷谐振器产生的低速外部时钟,下面这里就是典型电路,晶振是 32.768 KHz,CL1 和 CL2 上面这里写了,对于 CL1 和 CL2,建议使用高质量的 5 pF~15 pF 之间的瓷介电容器,所以对应硬件电路的设计,大家还是得多看看手册。手册看多了,自然就会了。所以在这里给出的晶振电路是这样的,起振电容给的是 10 pF,这就是这两块电路。

最后看一下下面的图片。
在这里插入图片描述
这个备用电池,我们一般可以选择这样的 3V 的纽扣电池,型号是 CR2032,这是一个非常常用的纽扣电池型号。另外注意,这个纽扣电池印字的这一面是正极,这也有个正号标注,另一面,比较小的那个电极是负极,这个别搞错了。
然后 32.768 KHz 的晶振,我们可以选择这样的一个金属壳柱状体的晶振,这个晶振也是比较常见,大家拆开钟表、电子表,基本上都能找到这样一个元件,这就是 32.768 KHz 的晶振,晶振的全称是石英晶体振荡器,所以我们常说的石英钟,名称就来源于这样一个元件。

然后右边这个是我们的最小系统板,这个板子自带的有 RTC 晶振电路,这里这个黑色的元件,写的有 32.768 K,这个也是一种样式的 RTC 晶振。然后旁边这个金属壳柱状体,是 8 MHz 的外部高速晶振,不过我们这个板子,没有自带备用电池,VBAT 引脚,直接通过右上角这个端口引出来了,如果需要备用电池的话,可以接在这里,或者直接把 3.3V 的主电源接在这里,也能实现功能。

好,以上就是这个 RTC 的硬件电路部分。

2.5 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 的访问,这个调用 PWR 的库函数,开启一下就行。
所以总结一下就是,如果你要使用 BKP 或者 RTC,都要先执行这两步:第一步,开启 PWR 和 BKP 时钟;第二步,使用 PWR,使能 BKP 和 RTC 的访问。这个我们在初始化的时候,需要注意一下,按照这个流程来就行了。

  1. 若在读取 RTC 寄存器时,RTC 的 APB1 接口曾经处于禁止状态,则软件首先必须等待 RTC_CRL 寄存器中的 RSF 位(寄存器同步标志)被硬件置 1

这一步对于代码里的一个库函数,就是 RTC 等待同步,一般在刚上电的时候调用一下这个函数就行了。为什么要有这一步呢?可以看一下 RTC 框图,在左上边会有两个时钟,PCLK1 和 RTCCLK,PCLK1 在主电源掉电时会停止,所以为了保证 RTC 主电源掉电正常工作,RTC 里的寄存器都是在 RTCCLK 的同步下变更的,当我们用 PCLK1 驱动的总线去读取 RTCCLK 驱动的寄存器时,就会有个时钟不同步的问题。RTC 寄存器,只有在 RTCCLK 的上升沿更新,但是 PCLK1 的频率 36 MHz,远大于 RTCCLK 的频率 32 KHz,如果我们在 APB1 刚开启时,就立刻读取 RTC 寄存器,有可能 RTC 寄存器还没有更新到 APB1 总线上,这样我们读取到的值,就是错误的,通常来说就是读取到 0。所以这就要求,我们在 APB1 总线刚开启时,要等一下 RTCCLK,只要 RTCCLK 来一个上升沿,RTC 把它的寄存器的值同步到 APB1 总线上,这样之后读取的值,就都是没问题的了。这是设计细节的一个问题,当然我们其实也不用管那么多的,只需要在初始化时,调用一个等待同步的函数就行了。

  1. 必须设置 RTC_CRL 寄存器中的 CNF 位,使 RTC 进入配置模式后,才能写入 RTC_PRL、RTC_CNT、RTC_ALR 寄存器

就是 RTC 会有一个进入配置模式的标志位,把这一位置 1,才能设置时间。其实这个操作在库函数中,每个写寄存器的函数,它都自动帮我们加上了这个操作,所以我们就不用再单独调用代码,进入配置模式了。

  1. 对 RTC 任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询 RTC_CR 寄存器中的 RTOFF 状态位,判断 RTC 寄存器是否处于更新中。仅当 RTOFF 状态位是 1 时,才可以写入 RTC 寄存器。

这个操作也是调用一个等待的函数就行了,跟我们之前读写 Flash 芯片是类似的。就是写入之前,要等待一下,如果上一次的写入还没完成,你就别急着写下一次了;或者说,每次写入之后,你要等待 RTOFF 为 1,只有 RTOFF 为 1 了,才表示写入完成。为什么要用这个操作呢?其实还是因为这里的 PCLK1 和 RTCCLK 时钟频率不一样,你用 PCLK1 的频率写入之后,这个值还不能立刻更新到 RTC 的寄存器里。因为 RTC 寄存器是由 RTCCLK 驱动的,所以 PCLK1 写完之后,得等一下 RTCCLK 的时钟,RTCCLK 来一个上升沿,值更新到 RTC 寄存器里,整个写入过程才算结束了,这个操作了解一下。在代码里,也就是调用一个等待函数的事。

那这些,就是操作这个 RTC 的一些注意事项,其实都是一些细节问题,这个我们写代码的时候会再给大家提一下的,现在大家先知道有这么个事就行了。

好,那有关 BKP 和 RTC 的知识点,就介绍这么多,最后还是照例看一下手册。

本节涉及手册的内容,主要是第 5 章,备份寄存器 BKP 和第 16 章,实时时钟 RTC。
先看一下 BKP 的内容。手册里对 BKP 的介绍,还是非常少的,总共就是一页多点,后面就是寄存器描述了,这么点内容,大家可以全都在仔细看一看的。

其中上面这一段是 BKP 的简介,最后这几句就是注意事项的第一条:初始化第一步,要先开启 PWR 和 BKP 的时钟,再使能对 BKP 和 RTC 的访问。

下面这些是 BKP 的特性和侵入检测的具体描述,大家可以再看看。

之后,RTC 校准,借助 RTC 校准时钟输出和校准寄存器的功能,可以对 RTC 进行校准,关于 RTC 校准和如何提高精度,请再查看这个文档。

接着下面就是寄存器描述了。首先,备份数据寄存器,1 到 10,总共 10 个,每个数据寄存器都是 16 位的,可以在这里存储自己的数据;然后,RTC 时钟校准寄存器,里面可以配置输出时钟的选择和校准值;下面是控制寄存器,主要用来配置侵入检测的功能,侵入信号的电平极性,和是否开启侵入检测功能;之后控制/状态寄存器,里面是一些标志位等。最后就是存储器映像的总表,里面 DR1 一直到 DR10,还是在一起的,中间是一些控制寄存器等,最后,额外的还有 DR11 一直到 DR42,后面这一块是大容量和互联型设备才有的。

那这些就是 BKP 的内容。BKP 还是比较简单的,也没有很多东西。

那我们再看一下 RTC,前面是一些简介。

这篇关于BKP 备份寄存器 RTC 实时时钟-stm32入门的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/518211

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始,按照书籍的划分,第10章开始就进入保护模式(Protected Mode)部分了,感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断(Interrupt)的设计,中断的几种方式:外部硬件中断、内部中断和软中断。通过中断做了一个会走的时钟和屏幕上输入字符的程序。 我自己理解中断的一些作用: 为了更好的利用处理器的性能。协同快速和慢速设备一起工作

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

poj 2104 and hdu 2665 划分树模板入门题

题意: 给一个数组n(1e5)个数,给一个范围(fr, to, k),求这个范围中第k大的数。 解析: 划分树入门。 bing神的模板。 坑爹的地方是把-l 看成了-1........ 一直re。 代码: poj 2104: #include <iostream>#include <cstdio>#include <cstdlib>#include <al

MySQL-CRUD入门1

文章目录 认识配置文件client节点mysql节点mysqld节点 数据的添加(Create)添加一行数据添加多行数据两种添加数据的效率对比 数据的查询(Retrieve)全列查询指定列查询查询中带有表达式关于字面量关于as重命名 临时表引入distinct去重order by 排序关于NULL 认识配置文件 在我们的MySQL服务安装好了之后, 会有一个配置文件, 也就

【STM32】SPI通信-软件与硬件读写SPI

SPI通信-软件与硬件读写SPI 软件SPI一、SPI通信协议1、SPI通信2、硬件电路3、移位示意图4、SPI时序基本单元(1)开始通信和结束通信(2)模式0---用的最多(3)模式1(4)模式2(5)模式3 5、SPI时序(1)写使能(2)指定地址写(3)指定地址读 二、W25Q64模块介绍1、W25Q64简介2、硬件电路3、W25Q64框图4、Flash操作注意事项软件SPI读写W2

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显