广东建设局网站首页,物流官网网站,河南省建筑工程网,龙岩网站建设套餐报价这一章节我们要讲的主要内容是 RTC 实时时钟#xff0c;对应手册#xff0c;是第 16 章的位置。
实时时钟这个东西#xff0c;本质上是一个定时器#xff0c;但是这个定时器#xff0c;是专门用来产生年月日时分秒#xff0c;这种日期和时间信息的。所以学会了 STM32 的…这一章节我们要讲的主要内容是 RTC 实时时钟对应手册是第 16 章的位置。
实时时钟这个东西本质上是一个定时器但是这个定时器是专门用来产生年月日时分秒这种日期和时间信息的。所以学会了 STM32 的 RTC你就可以在 STM32 内部拥有一个独立运行的钟表想要记录或读取日期和时间就可以通过操作 RTC 来实现。
那 RTC 这个外设呢比较特殊它和备份寄存器 BKP、电源控制 PWR 这两章的关联性比较强在 RTC 这一章BKP 和 PWR 也会经常来串门所以我们这章节就把 BKP 和 RTC 放在一起介绍这样整体思路会比较清晰PWR 电源控制我们下章节再介绍。
然后我们这一大章节分三小节来介绍。第一小节会单独介绍一下时间戳这个东西这也是个蛮有意思的知识点。想要使用这款 STM32 的 RTC学习时间戳的知识点还是非常必要的第二小节我们就学习 BKP 和 RTC 外设的结构最后第三小节就是写代码来完成程序现象了。这就是本章节的安排。
好那先看一下我们最终的程序现象本节一共有两个实例代码12-1 读写备份寄存器也就是读写 BKP12-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 就是 60s100s 就是 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 是什么东西为什么会有闰秒这个现象。
首先看一下 GMTGreenwich Mean Time格林尼治标准时间/格林威治标准时间/格林威治平均时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时以此确定计时标准。 格林尼治 是一个地名位于英国伦敦所以如果你对格林尼治这个名字不熟悉可以简单理解它就是伦敦标准时间。格林尼治这个地方有个天文台可以通过观察天上的太阳和星星来确定地球的自转和公转。 那可以看出这种计时方法非常符合我们的直觉一天的定义就是地球自转一周然后一天等分 24 小时再等分 60 分钟再等分 60 秒这样就能确定时间基础了。当然我这里是简单的理解具体过程也能会更复杂一些。 那 GMT 是以前全球计时的时间标准大家都遵循 GMT 的标准不同时区再加上对应的小时偏移这样全球各地的时间就能确定下来了。但为什么说 GMT 是以前的时间标准呢这是因为 GMT 有一个棘手的问题就是地球自转一周的时间其实是不固定的由于潮汐力地球活动等原因地球目前是越转越慢的。那你再根据一天的时间来定义时间基准这个时间基准就是在不断变化的。比如你把一天等分为 24 小时对应的秒数地球越转越慢那你定义 1s 的时间是不是也就越来越长啊一个不固定的时间基准对科学研究影响非常大。比如我们说光速是多少 m/s声速是多少 m/s前提是 1s 到底是多长必须是一个恒定不变的量所以说为了时间的定义更标准科学家又提出了新的计时系统叫做 UTC。
UTCUniversal 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 可以看成是一样的。像我们手机电脑的时间设置里可能就是说我们当前的北京时间是 GMT8 或者 UTC8这都是可以的这就说明我们使用的是东 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 种数据类型都是什么意思。
秒计数器数据类型它的数据类型名叫做 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 数据类型。可以用来存储时间戳中那个一直自增的秒数日期时间数据类型类型名是 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 库函数里的方式是一样的效果这个大家了解一下。
那这就是日期时间结构体的内容我们就清楚了。
字符串数据类型类型名是 char*就是 char 型数据的指针用来指向一个表示时间的字符串这个等会可以给大家演示。
好3 种数据类型我们就准备好了。接下来我们来使用函数尝试一下数据类型的转换可以看到这些函数中大量的出现了指针的操作不熟悉指针操作的话建议再看一下我空间里的指针教程要不然你不容易理解这些函数的用法。其实像这些官方的模块真的是遍地是指针自己写程序的话为了方便理解一般用指针还是比较少的但是耐不住别人都用指针所以指针大家还是要好好学一学的。 time_t time(time_t*); 作用是获取系统时钟。返回值是 time_t表示当前系统时钟的秒计数器值参数是 time_t*这是一个输出参数输出的内容和返回值是一样的所以这个函数可以通过返回值获取系统时钟也可以通过输出参数获取系统时钟。 这个函数在电脑里可以直接读取电脑的时间但是在 STM32 里是用不了的因为 STM32 是一个离线的裸机系统它也不知道现在是啥时间。 struct tm* gmtime(const time_t*); 将秒计数器的值转换为格林尼治日期时间也就是伦敦时间。参数是 const time_t*秒计数器指针类型是输入参数返回值 struct tm* 是日期时间结构体指针类型。 struct tm* localtime(const time_t*); 秒计数器转换当地时间这个函数和 gmtime 的使用方法是一样的只是 localtime 会根据时区自动添加小时的偏移。 time_t mktime(struct tm*); 就是上面两个函数的逆过程了它是将日期时间转换为秒计数器当然 mktime 传入的日期时间需要是当地的。参数是日期时间结构体指针类型返回值 time_t 是秒计数器类型。 另外再说明一下mktime 的参数前面并没有加 const实际上这个参数既是输入参数也是输出参数。它内部的工作过程是日期时间结构体里面由年月日时分秒星期等数据但是仅通过年月日时分秒就足以算出秒计数器了你填的星期参数实际上是不作为输入参数的相反这个函数在算出秒数的同时还会顺便算一下当前年月日是星期几然后回填到结构体里面的星期之中所以使用这个函数给定一个年月日我们可以很方便的计算对应的是星期几这个功能大家可以自己试一试我就不再演示了 实际上这个 time.h 里面重要的部分我们就已经讲完了也就是秒计数器和日期时间计算比较麻烦我们需要用这些现成的函数。
下面这三个函数实际上就是把时间转换为字符串表示这就比较简单了我们不用它的函数也能很方便的操作如果你需要用的话我们也演示一下使用方法。
char* ctime(const time_t*); 就是把秒计数器转换为 char* 格式的字符串使用默认的格式。char* asctime(const struct tm*); 就是把日期时间转换为字符串使用默认的格式。size_t strftime(char*, size_t, const char*, const struct tm*); 日期时间转换为字符串自定义格式这个函数就比较高级了它的作用和 asctime 是一样的但是可以自定格式。它总共有四个参数前面两个参数需要传入一个字符数组和数组长度第三个参数需要给定指定的格式字符串第四个参数把 time_date 传进去就行了。
#include stdio.h
#include time.htime_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 的部分首先看一下简介。
BKPBackup Registers备份寄存器/后备寄存器
BKP 用途可用于存储用户应用程序数据。 BKP 就是一些存储器可以储存自定义数据想存啥就存啥。 BKP 特性当VDD2.0~3.6V电源被切断他们仍然由VBAT1.8~3.6V维持供电。当系统在待机模式下被唤醒或系统复位或电源复位时他们也不会被复位。 这里的 VDD 就是系统的主电源供电电压是 2.0~3.6VVBATV 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 备用电源我们就清楚了。 VBAT 的作用就是当 VDD 断电时BKP 会切换到 VBAT 供电这样可以继续维持 BKP 里面的数据如果 VDD 断电VBAT 也没电呢那 BKP 里的数据就会清零因为 BKP 本质上是 RAM 存储器没有掉电不丢失的能力。然后后面一句的意思是待机唤醒或者复位时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 外设。还是先看一下简介。
RTCReal Time Clock实时时钟
在 STM32 中RTC 是一个独立的定时器可为系统提供时钟和日历的功能 RTC 实时时钟一般就是指提供年月日时分秒这种日期时间信息的计时装置。51 单片机的 DS1302 是外置的 RTC 芯片这个芯片可以独立计时我们需要设置时间或读取时间就通过通信协议向它发送或接收数据来完成那在我们 STM32 内部有这个 RTC 的外设所以 STM32 可以在内部直接实现 RTC 的功能这样就不用再外挂 RTC 芯片了当然 RTC 芯片所必要的元件比如备用电池、RTC 晶振这些东西就要接到 STM32 上了。 RTC 和时钟配置系统处于后备区域系统复位时数据不清零VDD2.0~3.6V断电后可借助VBAT1.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/128LSE 振荡器时钟通常为 32.768KHzLSI 振荡器时钟40KHz 这 3 个时钟可以选择其中一个接入到这里的 RTCCLK那这 3 个时钟都是什么意思呢我们可以看一下之前定时器这里讲过的 RCC 时钟树这个图就是整个芯片的时钟系统整个芯片可以有 4 个时钟源右下角写了HSE高速外部时钟信号HSI高速内部时钟信号LSI低速内部时钟信号LSE低速外部时钟信号。这些时钟字母你就记住H(High) 开头是高速L(Low) 开头是低速E(External) 结尾是外部I(Internal) 结尾是内部高速低速内部外部一组合就是 4 种情况。这里高速时钟一般供内部程序运行和主要外设使用低速时钟一般供 RTC、看门狗这些东西使用。 那对于我们本节的 RTC 呢我们可以看到下面有一个指向通往 RTC 的箭头就是 RTCCLKRTCCLK 有 3 个来源 第一个是 OSC 引脚接的 HSE外部高速晶振这个晶振是主晶振我们一般都用的 8 MHz8 MHz 进来通过 128 分频可以产生 RTCCLK 信号。为什么要先 128 分频呢这是因为这个 8 MHz 的主晶振太快了如果不提前分频直接给 RTCCLK后续即使再通过 RTC 的 20 位分频器也分不到 1 Hz 这么低的频率所以 8 MHz 提前先进行 128 分频后续 20 位的分频器再进行一个适当的分频就可以输出 1 Hz 的信号给计数器了这是第一路来源HSE 的时钟。然后中间这一路时钟来源是 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 的这是第二路。最后看第三路时钟源来自于 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 框图