1.Unix时间戳
1.1 简介
32位有符号数所能表示的最大数字是2^32/2-1这个数是21亿多,这其实是有溢出风险的,因为目前到2023年时间戳已经计到16亿了,32位有符号数的时间戳会在2038年的1月19号溢出,64位的时间戳能存储的时间范围非常非常的大,看下手册STM32它核心的计时部分,是一个32位的可编程计数器,这说明我们这款stm32 ,它的时间戳是32位的数据类型,这表示我们这个stm32也会在2038年出现bug吗,实际上并不会啊,因为根据我的研究,这个时间戳在stm32程序中定义的其实是无符号的,要到2106年才会溢出。
1.2 UTC/GMT
1.3 时间戳转换
C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换
1.4 C标准库<Time.h>
1.4.1 time.h 中定义的变量类型
序号 | 变量 & 描述 |
1 | size_t 是无符号整数类型,它是 sizeof 关键字的结果。 |
2 | clock_t 这是一个适合存储处理器时间的类型。 |
3 | time_t is 这是一个适合存储日历时间类型。 |
4 | struct tm 这是一个用来保存时间和日期的结构。 |
1.4.2 tm结构定义
struct tm {int tm_sec; /* 秒,范围从 0 到 59 */int tm_min; /* 分,范围从 0 到 59 */int tm_hour; /* 小时,范围从 0 到 23 */int tm_mday; /* 一月中的第几天,范围从 1 到 31 */int tm_mon; /* 月,范围从 0 到 11 */int tm_year; /* 自 1900 年起的年数 */int tm_wday; /* 一周中的第几天,范围从 0 到 6 */int tm_yday; /* 一年中的第几天,范围从 0 到 365 */int tm_isdst; /* 夏令时 */
};
1.4.3 相关示例
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// struct tm {
// int tm_sec; /* 秒,范围从0 到59 */
// int tm_min; /* 分,范围从0 到59 */
// int tm_hour; /* 小时,范围从0 到23 */
// int tm_mday; /* 一月中的第几天,范围从1 到31 */
// int tm_mon; /* 月,范围从0 到11 */
// int tm_year; /* 自1900 年起的年数 */
// int tm_wday; /* 一周中的第几天,范围从0 到6 */
// int tm_yday; /* 一年中的第几天,范围从0 到365 */
// int tm_isdst; /* 夏令时 */
// };
int main(int argc, char const *argv[])
{time_t Time; // 时间戳struct tm_Date;//日期存放结构体char *time_str;//日期字符串// Time = time(NULL);time(&Time);printf("Time = %ld\n", Time); // Time = 1709649653Date = *localtime(&Time);//显示当地时间printf("%d\n", Date.tm_year + 1900);//加1900是因为这里tm_year是从1900后开始记起printf("%d\n", Date.tm_mon + 1);//加1是因为这里tm_mon是从0开始记起printf("%d\n", Date.tm_mday);printf("%d\n", Date.tm_hour);printf("%d\n", Date.tm_min);printf("%d\n", Date.tm_sec);printf("%d\n", Date.tm_wday);Time = mktime(&tm_Data); //将日期时间重新转化为秒计时器printf("%d\n",Time); time_str = ctime(&Time);//将秒计数器转化为字符串printf(time_str);//这里字符串的名字就是首地址tine_str = asctime(&tm_Data);//将日期转化为字符串printf(time_str);char t[50];strftime(t,50,"$H-%M-%S",tm_Data);//前两个参数 传入字符数组和数组长度 第三个参数给定指定的格式字符串 第四个参数为日期结构体 %H是小时 %M是分钟 %S是秒 将字符串格式对应日期结构体中的参数写入字符数组中 再将字符数组打印出来printf(t);return 0;
}
现象如下:表示2024.3.5 22:40:53 星期二
1709649653
2024
3
5
22
40
53
2
1709649653
Tue Mar 05 22:40:53 2024
Tue Mar 05 22:40:53 2024
22-40-53
2.BKP备份寄存器
2.1 简介
- TAMPER引脚产生的侵入事件将所有备份寄存器内容清除,TAMPER是一个接到stm32外部的引脚,它的位置可以看一下引脚定义,这个TAMPER是一个安全保障设计,比如如果你做一个安全系数非常高的设备,设备需要有防拆功能,然后bkp里也存储了一些敏感数据,这些数据不能被别人窃取或者篡改,那你就可以使用这个TAMPER引脚的侵入检测功能,设计电路时,TAMPER引脚可以先加一个默认的上拉或者下拉电阻,然后引一根线到你的设备外壳的防拆开关或触点,别人一拆开你的设备触发开关,就会在TAMPER引脚产生上升沿或者下降沿,这样STM32就检测到侵入事件了,这时BKP的数据会自动清零,并且申请中断,你在中断里还可以继续保护设备,比如清除其他存储器数据,然后设备锁死,这样来保障设备的安全,另外主电源断电后,侵入检测仍然有效,这样即使设备关机也能防拆。
-
RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲,RTC引脚刚才看过了,也是在PC13这个位置,这就是RTC时钟输出的功能,RTC的校准时钟,闹钟或者秒脉冲的信号,可以通过RTC引脚输出,其中外部用设备测量RTC校准时钟,可以对内部RTC微小的误差进行校准,然后闹钟脉冲或者秒脉冲可以输出出来,为别的设备提供这些信号,这是RTC时钟输出的功能,因为PC13、temple和RTC这三个引脚共用一个端口,所以这三个功能同一时间只能使用一个。
-
BKP中用户数据的存储容量,在中容量和小容量设备里,BKP是20个字节,在大容量和互联型设备里,BKP是84个字节,我们使用的c8t6是中容量设备,所以可以看出BKP的容量其实非常小,一般只能用来存储少量的参数。
2.2 基本结构
- 看一下BKP的基本结构,这个图中橙色部分我们可以叫做后备区域,BKP处于后备区域,但后备区域不只有BKP,还有RTC的相关电路也位于后备区域,STM32后备区域的特性就是当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电,当VDD主电源上电时,后备区域供电会切换到VDD,主电源有电时VBAT不会用的,这样可以节省电池电量,然后BKP是位于后备区域的,BKP里主要有数据寄存器、控制寄存器、状态寄存器和RTC时钟校准寄存器这些东西,其中数据寄存器是主要部分,用来存储数据的,每个数据寄存器都是16位的,也就是一个数据寄存器可以存两个字节,那对于中容量和小容量的设备,里面有dr1、dr2一直到dr10总共十个数据寄存器,那一个寄存器存两个字节,所以容量是20个字节,再往后最多能有42个数据寄存器,容量就是84个字节,对应大容量和互联型。
-
BKP还有几个功能,就下面这里的侵入检测,可以从PC13位置的TAMPER引脚引入一个检测信号,当TAMPER产生上升沿或者下降沿时,清除BKP所有的内容以保证安全,时钟输出,可以把RTC的相关时钟,从pc13位置的RTC引脚输出数据供外部使用,其中输出较准时钟时,再配合这个校准寄存器,可以对RTC的误差进行校准。
3.RTC实时时钟
3.1 简介
记住H开头是高速,L开头是低速,E结尾是外部,I结尾是内部,这里高速时钟一般供内部程序运行和主要外设使用,低速时钟一般供RTC看门狗这些东西使用,那对于我们本节的RTC呢,我们可以看到这一块,这里指向的箭头通往RTC,就是RTCCLK,RTCCLK有三个来源,第一个是OSC引脚接的HSE外部高速晶振,这个晶振是主晶振,我们一般都用的8MHZ,通过128分频可以产生RTCCLK信号,为什么要先128分频,这是因为这个8MHz的晶振太快了,如果不提前分频,直接给RTCCLK,后续即使再通过RTC的20位分频器,也分不到1Hz这么低的频率。然后中间这一路时钟来源是LSE,外部低速晶振,我们在oc32这两个引脚,接上外部低速晶振,这个晶振产生的时钟,可以直接提供给RTCCLK,这个osc32的晶振,是内部RTC的专用时钟,这个晶振的值也不是随便选的,通常跟RTC有关的晶振都是统一的数值,就是32.768KHz,为什么选择这个数值呢,一方面是32.768KHz这个值附近的频率,是晶振工艺比较合适的频率,你要说非要做一个1Hz的晶振,那可能是做不出来或者做出来了但体积很大性能很差,另一方面是32768这是一个二的次方数,2的15次方等于32768,所以32.768KHz,经过一个15位分频器的自然溢出,就能很方便地得到1Hz的频率,自然溢出的意思就是设计一个15位的计数器,这个计数器不用设置计数目标,直接从0计到最大值,就计得32767,计满后自然溢出,这个溢出信号就是1Hz,自然溢出的好处就是不用再额外设计一个计数目标了,也不用比较计数是不是计到目标值,这样可以简化电路设计,所以目前在RTC电路中,基本都是清一色的32.768KHz的晶振,你只要看到32.768KHz的晶振八成就是提供给rtc的,这是第二路,最后看第三路时钟源,来自于LSI,内部低速rc振荡器,LSI固定是40KHz,如果选择LSI当做RTCCLK,后续再经过40k的分频,就能得到1Hz的计数时钟了,当然内部的RC振荡器,一般精度没有外部晶振高,所以LSI给RTCCLK,可以当做一个备选方案,另外LSI还可以提供给看门狗,我们最常用的就是中间这一路,外部32.768KHz的晶振,提供RTCCLK的时钟,第一个原因就是中间这一路,32.768KHz的晶振本身就是专供rtc使用的,上下这两路其实是有各自的任务,上面这一路主要作为系统主时钟,下面这一路主要作为看门狗时钟,他们只是顺带可以备选当做rdc的时钟,另外一个更重要的原因,只有中间这一路的时钟可以通过VBAT备用电池供电,上下两路时钟在主电源断电后是停止运行的,所以要想实现rtc主电源掉电继续走时的功能,必须得选择中间这一路的rtc专用时钟,如果选择的是上下两路时钟,主电源断电后时钟就暂停了,这显然会导致走时出错。
3.2 框图
-
首先看分频和计数器计数部分,这一块的输入时钟是RTCCLK,这刚才说过RTCCLK的来源需要在RCC里进行配置,可以选择的选项是这三个,我们主要选择中间一路,那因为这三路时钟频率各不相同,而且都远大于我们所需要的1Hz的秒计数频率,所以RTCCLK进来,首先需要经过RTC预分频器进行分频,这个分频器由两个计算器组成,上面这个是重装载寄存器RTC_PRL,下面这个RTC_DIV手册里叫做余数寄存器,但实际上这一块跟我们之前定时器时机单元里的计数器CNT和重装值ARR是一样的,可能是右边已经有一个计数器cnt了,所以这个名字就比较奇怪,叫做余数寄存器,但实际上它还是计数器的作用,分频器其实就是一个计数器,记几个数溢出一次就几分频,所以对于可编程的分频器来说,需要有两个寄存器,一个寄存器用来不断的计数(RTC_DIV),另一个寄存器我们写入一个计数目标值(RTC_PRL),用来配置是几分频,那在这里上面这个PRL就是计数目标,我们写入六那就是七分频,写九那就是十分频,因为计数指包含了零,然后下面这个DIV就是每来一个时钟计一个数的用途了,当然这个DIV计数器是一个自减计数器,每来一个输入时钟,DIV的值自减一次,自减到0时再来一个输入时钟,DIV输出一个脉冲产生溢出信号,同时DIV从PRL获取重装值,回到重装值继续自减。
-
然后看一下计数计时部分,32位可编程计数器,RTC_CNT就是计时最核心的部分,我们可以把这个计数器看作是Unix时间戳的秒计数器,这样借助time.h的函数,就可以很方便地得到年月日时分秒了,然后在下面这里,这个RTC还设计的有一个闹钟寄存器RTC_ALR,这个ALR也是一个32位的寄存器,和上面这个cnt是等宽的,它的作用顾名思义就是设置闹钟,我们可以在ALR写一个秒数设定闹钟,当cnt的值跟ALR设定的闹钟值一样时,也是这里画了等号啊,如果他俩值相等就代表闹钟响了,这时就会产生RTC_Alarm闹钟信号,通往右边的中断系统,在中断函数里你可以执行相应的操作,同时这个闹钟还兼具一个功能,就下面这里的闹钟信号,可以让STM32退出待机模式,这个就可以对应一些用途,比如你设计一个数据采集设备,需要在环境非常恶劣的地方工作,比如海底高原深井这些地方,然后要求是每天中午12点采集一次环境数据,其他时间为了节省电量避免频繁换电池,芯片都必须处于待机模式,这样的话我们就可以用这个rtc自带的闹钟功能,定一个中午12点的闹钟,闹钟一响芯片唤醒采集数据完成后继续待机,另外这个闹钟值是一个定值,只能响一次,所以如果你想实现周期性的闹钟,大家每次闹钟响之后都需要再重新设置一下下一个闹钟时间,就是这个闹钟和闹钟唤醒的一个用途。
-
那继续往右看,这是中段部分的,在左边这里有三个信号可以触发中断,第一个是RTC_Second秒中断,它的来源就是cnt的输入时钟,如果开启这个中断,那么程序就会每秒进一次rtc中断,第二个是RTC_Overflow溢出中断,它的来源是cnt的右边,意思就是cnt的32位计数器计满溢出来了,会触发一次中断,所以这个中段一般不会触发,我们上一节说过,这个cnt定义的是无符号数,到2106年才会溢出,所以这个中段在2106年会触发一次,如果你想程序更完善一些,可以开启这个中段,到2106年就是一溢出,为了避免不必要的错误,你可以让芯片罢工,然后提示当前设备过老,请及时更换,但在2106年之后这个stm32的rtc就不太好用了,到时候或许可以通过打补丁的方式继续运行,或者直接淘汰32位的时间戳,这个问题就留给后人解决吧。来继续看下面第三个RTC_Alarm闹钟中断,刚才说过,当计数器和闹钟值相等时触发中断,同时闹钟信号可以把设备从待机模式唤醒,这是这三个中断信号,中断信号到右边这里,这一块就是中断标志位和中段输出控制,这些f结尾的是对应的中断标志位,ie结尾(Interrupt ENABLE)的是中断使能,最后三个信号通过一个或门汇聚到NVIC中断控制器,这个地方是不是漏画了一根线,中间这个应该也是要通过或门的,好这是右边的中断部分,然后上面这部分APB1总线和APB1接口,就是我们程序读写寄存器的地方,读写计算器可以通过APB1总线来完成,另外也可以看出,RTC是APB1总线上的设备,最后下面这一块退出待机模式,还有一个wake up引脚,闹钟信号和wake up引脚都可以唤醒设备,wake up引脚可以看一下接线图,就这里PA0的位置它兼具唤醒的功能,这个我们下一节再学习。
3.3 基本结构
rtc的核心部分如图所示,最左边是RTCCLK时钟来源,这块需要在RCC里配置,三个时钟选择一个当做RTCCLK,之后RTCCLK先通过预分频器对时钟进行分频,余数寄存器是一个自减计数器,存储当前的计数值,重装寄存器是计数目标,决定分频值,分频之后得到1Hz的秒计数信号通向32位计数器,一秒自增一次,下面还有个32位的闹钟值,可以设定闹钟,如果不需要闹钟的话,下面这一块可以不用管,然后右边有三个信号可以触发中断,分别是秒信号、计数器溢出信号和闹钟信号,三个信号先通过中断输出控制进行中断使能,使能的中断才能通向NVIC,然后向cpu申请中断,在程序中我们配置这个数据选择器,可以选择时钟来源,配置重装寄存器可以选择分频系数,配置32位计数器可以进行日期时间的读写,需要闹钟的话,配置32位闹钟值即可,需要中断的话,先允许中断,再配置nvic,最后写对应的中断函数即可,这是RTC外设的主要内容。
3.4 硬件电路
备用电池供电部分,我这里给了两个参考电路,第一个是简单连接,就使用一个3v的电池,负极和系统共地,正极直接接到stm32 的VBAT引脚,这样就行了,这个供电方案非常简单,参考来源是stm32的数据手册,在5.1.6供电方案这里就给出来这个图,图上画的就是直接建一个1.8~3.6v的电池到VBAT就行了,另外也可以看到,在内部是有一个供电开关的,当vdd有电时,开关拨到下面,后备电路由vdd供电,当vdd没电时,开关拨到上面,后备电路由VBAT供电,然后vbat供电的设备,在这里写了,vbat供电的后备电路有32KHz振荡器、rtc、唤醒电路和后备寄存器,那这就是根据数据手册里设计的VBAT供电方案,这个设计非常简单一般来说也没问题,然后我这里还给了第二种方案是推荐连接,这种连接方法是电池通过二极管D1向VBAT供电,另外主电源的3.3V,也通过二极管D2向VBAT供电,最后VBAT再加一个0.1uf的电源滤波电容,这个供电方案的参考来源是stm32的参考手册,在这个4.1.2电池备份区域这一节有这样描述,大家可以都看看,其中手册里有几个建议,一个是在这些这些情况下,电流可能通过vdd和vbat之间的内部二极管注入到vbat,如果与vbat连接的电源或者电池,不能承受这样的注入电流,强烈建议在外部,vbat和电源之间连接一个低压降的二极管,另一个是如果在应用中没有外部电池,建议vbat在外部连接到vdd,并连接一个100nf的陶瓷滤波电容,所以综合这两条建议,我们可以设计出右边的推荐连接,电池和主电源都加一个二极管,防止电流倒灌,vbat加一个0.1uf的电源滤波电容,0.1uf就是100nf,如果没有备用电池,就3.3V的主电源供电,如果接了备用电池,3.3v没电时,就是备用电池供电,这是根据参考手册设计的推荐电路,如果你只是进行实验,那使用左边的简单连接就行了,如果你要画板子设计产品,那还是推荐使用右边的连接,这样更保险,这是vbat供电部分。
3.5 操作注意事项
- 正常的外设第一步开启时钟就能用了,但是BKP和RTC这两个外设,开启稍微复杂一些,如果你要使用BKP或者RTC,都要先执行这两步,第一步开启PWR和BKP的时钟,第二步使用PWR使能BKP和RTC的访问。
-
若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1,这一步对应代码里的一个库函数就是RTC等待同步,一般在刚上电的时候调用一下这个函数就行了,为什么要有这一步呢,可以看看上面的框图,在这里会有两个时钟PCLK1和RTCCLK,PCLK1在主电源掉电时会停止,所以为了保证RTC主电源掉电正常工作,RTC里的寄存器,都是在RTCCLK的同步下变更的,当我们用PCLK驱动的总线去读取RTCCLK驱动的寄存器时,就会有个时钟不同步的问题,RTC寄存器只有在RTCCLK的上升沿更新,但是PCLK1的频率36MHz远大于RTCCLK的频率32KHz,如果我们在APB1刚开启时,就立刻读取RTC寄存器,有可能RTC寄存器还没有更新到APB1总线上,这样我们读到的值就是错误的,通常来说就读取到0,所以这就要求我们在APB1总线刚开机时,要等一下RTCCLK,只要RTCCLK来一个上升沿,rtc把它的寄存器的值同步到APB1总线上,这样之后读取的值就都是没问题的了,这是设计细节的一个问题,当然我们其实也不用管那么多了,只需要在初始化时调用一个等待同步的函数就行了。
-
必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器,这一条其实比较简单,就是RTC会有一个进入配置模式的标志位,把这一位置1才能设置时间,其实这个操作在库函数中,每个写寄存器的函数,它都自动帮我们加上了这个操作,所以我们就不用再单独调用函数进入配置模式了。
-
对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器,这个操作也是调用一个等待的函数就行了,跟我们之前读写flash芯片是类似的,就写入之前要等待一下,如果上一次的写入还没完成,你就别急着写下一次了,或者说每次写入之后,你要等待RTOFF为1,只有RTOFF为1才表示写完成,为什么要有这个操作呢,其实还是因为这里的PCLK1和RTCCLKk时钟频率不一样,你用PCLK1的频率写入之后,这个值还不能立刻更新到RTC的寄存器里,因为RTC寄存器是由RTCCLK驱动的,所以PCLK1写完之后,得等一下RTCCLK的时钟,RTCCLK来个上升沿,值更新到RTC寄存器里,整个写作过程才算结束了,这个操作了解一下,在代码里也就是调用一个等待函数的事。
4.读写备份寄存器B
4.1 相关API
4.1.1 BKP_ReadBackupRegister
/*** @brief Reads data from the specified Data Backup Register.* @param BKP_DR: specifies the Data Backup Register.* This parameter can be BKP_DRx where x:[1, 42]* @retval The content of the specified Data Backup Register*/
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
功能:从指定的后备寄存器中读出数据
参数:BKP_DR:数据后备寄存器
返回值:指定的后备寄存器中的数据
4.1.2 BKP_WriteBackupRegister
/*** @brief Writes user data to the specified Data Backup Register.* @param BKP_DR: specifies the Data Backup Register.* This parameter can be BKP_DRx where x:[1, 42]* @param Data: data to write* @retval None*/
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
功能:向指定的后备寄存器中写入用户程序数据
参数:BKP_DR:数据后备寄存器Data:待写入的数据
返回值:无
4.1.3 PWR_BackupAccessCmd
/*** @brief Enables or disables access to the RTC and backup registers.* @param NewState: new state of the access to the RTC and backup registers.* This parameter can be: ENABLE or DISABLE.* @retval None*/
void PWR_BackupAccessCmd(FunctionalState NewState);
功能:使能或者失能 RTC 和后备寄存器访问
参数:NewState: RTC 和后备寄存器访问的新状态
返回值:无
4.2 接线图
4.3 相关代码
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"uint8_t KeyNum; //定义用于接收按键键码的变量uint16_t ArrayWrite[] = {0x1234, 0x5678}; //定义要写入数据的测试数组
uint16_t ArrayRead[2]; //定义要读取数据的测试数组int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化Key_Init(); //按键初始化/*显示静态字符串*/OLED_ShowString(1, 1, "W:");OLED_ShowString(2, 1, "R:");/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟/*备份寄存器访问使能*/PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问while (1){KeyNum = Key_GetNum(); //获取按键键码if (KeyNum == 1) //按键1按下{ BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]); //写入测试数据到备份寄存器BKP_WriteBackupRegister(BKP_DR2, ArrayWrite[1]);OLED_ShowHexNum(1, 3, ArrayWrite[0], 4); //显示写入的测试数据OLED_ShowHexNum(1, 8, ArrayWrite[1], 4);ArrayWrite[0] ++; //测试数据自增ArrayWrite[1] ++;}ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1); //读取备份寄存器的数据ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2);OLED_ShowHexNum(2, 3, ArrayRead[0], 4); //显示读取的备份寄存器数据OLED_ShowHexNum(2, 8, ArrayRead[1], 4);}
}
现象:按下按键OLED显示数组数据 再次按下数据自增 同时拔掉电源 下次通电后还是最后一次显示R的数据 但是拔掉备用电源 数据会消失
5.读写RTC实时时钟
5.1 相关API
5.1.1 RCC_LSEConfig
/*** @brief Configures the External Low Speed oscillator (LSE).* @param RCC_LSE: specifies the new state of the LSE.* This parameter can be one of the following values:* @arg RCC_LSE_OFF: LSE oscillator OFF* @arg RCC_LSE_ON: LSE oscillator ON* @arg RCC_LSE_Bypass: LSE oscillator bypassed with external clock* @retval None*/
void RCC_LSEConfig(uint8_t RCC_LSE);
功能:设置外部低速晶振(LSE)
参数:RCC_LSE: LSE 的新状态
返回值:无
5.1.2 RCC_GetFlagStatus
/*** @brief Checks whether the specified RCC flag is set or not.* @param RCC_FLAG: specifies the flag to check.* * For @b STM32_Connectivity_line_devices, this parameter can be one of the* following values:* @arg RCC_FLAG_HSIRDY: HSI oscillator clock ready* @arg RCC_FLAG_HSERDY: HSE oscillator clock ready* @arg RCC_FLAG_PLLRDY: PLL clock ready* @arg RCC_FLAG_PLL2RDY: PLL2 clock ready * @arg RCC_FLAG_PLL3RDY: PLL3 clock ready * @arg RCC_FLAG_LSERDY: LSE oscillator clock ready* @arg RCC_FLAG_LSIRDY: LSI oscillator clock ready* @arg RCC_FLAG_PINRST: Pin reset* @arg RCC_FLAG_PORRST: POR/PDR reset* @arg RCC_FLAG_SFTRST: Software reset* @arg RCC_FLAG_IWDGRST: Independent Watchdog reset* @arg RCC_FLAG_WWDGRST: Window Watchdog reset* @arg RCC_FLAG_LPWRRST: Low Power reset* * For @b other_STM32_devices, this parameter can be one of the following values: * @arg RCC_FLAG_HSIRDY: HSI oscillator clock ready* @arg RCC_FLAG_HSERDY: HSE oscillator clock ready* @arg RCC_FLAG_PLLRDY: PLL clock ready* @arg RCC_FLAG_LSERDY: LSE oscillator clock ready* @arg RCC_FLAG_LSIRDY: LSI oscillator clock ready* @arg RCC_FLAG_PINRST: Pin reset* @arg RCC_FLAG_PORRST: POR/PDR reset* @arg RCC_FLAG_SFTRST: Software reset* @arg RCC_FLAG_IWDGRST: Independent Watchdog reset* @arg RCC_FLAG_WWDGRST: Window Watchdog reset* @arg RCC_FLAG_LPWRRST: Low Power reset* * @retval The new state of RCC_FLAG (SET or RESET).*/
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
功能:检查指定的 RCC 标志位设置与否
参数:RCC_FLAG:待检查的 RCC 标志位
返回值:RCC_FLAG 的新状态(SET 或者 RESET)
5.1.3 RCC_RTCCLKConfig
/*** @brief Configures the RTC clock (RTCCLK).* @note Once the RTC clock is selected it can't be changed unless the Backup domain is reset.* @param RCC_RTCCLKSource: specifies the RTC clock source.* This parameter can be one of the following values:* @arg RCC_RTCCLKSource_LSE: LSE selected as RTC clock* @arg RCC_RTCCLKSource_LSI: LSI selected as RTC clock* @arg RCC_RTCCLKSource_HSE_Div128: HSE clock divided by 128 selected as RTC clock* @retval None*/
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
功能:设置 RTC 时钟(RTCCLK)
参数:RCC_RTCCLKSource: 定义 RTCCLK
返回值:无
5.1.4 RCC_RTCCLKCmd
/*** @brief Enables or disables the RTC clock.* @note This function must be used only after the RTC clock was selected using the RCC_RTCCLKConfig function.* @param NewState: new state of the RTC clock. This parameter can be: ENABLE or DISABLE.* @retval None*/
void RCC_RTCCLKCmd(FunctionalState NewState);
功能:使能或者失能 RTC 时钟
参数:NewState:RTC 时钟的新状态, 这个参数可以取:ENABLE 或者 DISABLE
返回值:无
5.1.5 RTC_WaitForSynchro
/*** @brief Waits until the RTC registers (RTC_CNT, RTC_ALR and RTC_PRL)* are synchronized with RTC APB clock.* @note This function must be called before any read operation after an APB reset* or an APB clock stop.* @param None* @retval None*/
void RTC_WaitForSynchro(void);
功能:等待最近一次对 RTC 寄存器的写操作完成
参数:无
返回值:无
5.1.6 RTC_WaitForLastTask
/*** @brief Waits until last write operation on RTC registers has finished.* @note This function must be called before any write to RTC registers.* @param None* @retval None*/
void RTC_WaitForLastTask(void);
功能:等待最近一次对 RTC 寄存器的写操作完成
参数:无
返回值:无
5.1.7 RTC_SetPrescaler
/*** @brief Sets the RTC prescaler value.* @param PrescalerValue: RTC prescaler new value.* @retval None*/
void RTC_SetPrescaler(uint32_t PrescalerValue);
功能:设置 RTC 预分频的值
参数:PrescalerValue:新的 RTC 预分频值
返回值:无
5.1.8 RTC_SetCounter
/*** @brief Sets the RTC counter value.* @param CounterValue: RTC counter new value.* @retval None*/
void RTC_SetCounter(uint32_t CounterValue);
功能:设置 RTC 计数器的值
参数:CounterValue:新的 RTC 计数器值
返回值:无
5.1.9 RTC_GetCounter
/*** @brief Gets the RTC counter value.* @param None* @retval RTC counter value.*/
uint32_t RTC_GetCounter(void);
功能:获取 RTC 计数器的值
参数:无
返回值:RTC 计数器的值
5.2 接线图
5.3 相关代码
5.3.1 MyRTC.c
#include "stm32f10x.h" // Device header
#include <time.h>uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55}; //定义全局的时间数组,数组内容分别为年、月、日、时、分、秒void MyRTC_SetTime(void); //函数声明/*** 函 数:RTC初始化* 参 数:无* 返 回 值:无*/
void MyRTC_Init(void)
{/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟/*备份寄存器访问使能*/PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) //通过写入备份寄存器的标志位,判断RTC是否是第一次配置//if成立则执行第一次的RTC配置{RCC_LSEConfig(RCC_LSE_ON); //开启LSE时钟while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); //等待LSE准备就绪RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK来源为LSERCC_RTCCLKCmd(ENABLE); //RTCCLK使能RTC_WaitForSynchro(); //等待同步RTC_WaitForLastTask(); //等待上一次操作完成RTC_SetPrescaler(32768 - 1); //设置RTC预分频器,预分频后的计数频率为1HzRTC_WaitForLastTask(); //等待上一次操作完成MyRTC_SetTime(); //设置时间,调用此函数,全局数组里时间值刷新到RTC硬件电路BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置}else //RTC不是第一次配置{RTC_WaitForSynchro(); //等待同步RTC_WaitForLastTask(); //等待上一次操作完成}
}//如果LSE无法起振导致程序卡死在初始化函数中
//可将初始化函数替换为下述代码,使用LSI当作RTCCLK
//LSI无法由备用电源供电,故主电源掉电时,RTC走时会暂停
/*
void MyRTC_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);PWR_BackupAccessCmd(ENABLE);if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5){RCC_LSICmd(ENABLE);while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);RCC_RTCCLKCmd(ENABLE);RTC_WaitForSynchro();RTC_WaitForLastTask();RTC_SetPrescaler(40000 - 1);RTC_WaitForLastTask();MyRTC_SetTime();BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);}else{RCC_LSICmd(ENABLE); //即使不是第一次配置,也需要再次开启LSI时钟while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);RCC_RTCCLKCmd(ENABLE);RTC_WaitForSynchro();RTC_WaitForLastTask();}
}*//*** 函 数:RTC设置时间* 参 数:无* 返 回 值:无* 说 明:调用此函数后,全局数组里时间值将刷新到RTC硬件电路*/
void MyRTC_SetTime(void)
{time_t time_cnt; //定义秒计数器数据类型struct tm time_date; //定义日期时间数据类型time_date.tm_year = MyRTC_Time[0] - 1900; //将数组的时间赋值给日期时间结构体time_date.tm_mon = MyRTC_Time[1] - 1;time_date.tm_mday = MyRTC_Time[2];time_date.tm_hour = MyRTC_Time[3];time_date.tm_min = MyRTC_Time[4];time_date.tm_sec = MyRTC_Time[5];time_cnt = mktime(&time_date) - 8 * 60 * 60; //调用mktime函数,将日期时间转换为秒计数器格式//- 8 * 60 * 60为东八区的时区调整RTC_SetCounter(time_cnt); //将秒计数器写入到RTC的CNT中RTC_WaitForLastTask(); //等待上一次操作完成
}/*** 函 数:RTC读取时间* 参 数:无* 返 回 值:无* 说 明:调用此函数后,RTC硬件电路里时间值将刷新到全局数组*/
void MyRTC_ReadTime(void)
{time_t time_cnt; //定义秒计数器数据类型struct tm time_date; //定义日期时间数据类型time_cnt = RTC_GetCounter() + 8 * 60 * 60; //读取RTC的CNT,获取当前的秒计数器//+ 8 * 60 * 60为东八区的时区调整time_date = *localtime(&time_cnt); //使用localtime函数,将秒计数器转换为日期时间格式MyRTC_Time[0] = time_date.tm_year + 1900; //将日期时间结构体赋值给数组的时间MyRTC_Time[1] = time_date.tm_mon + 1;MyRTC_Time[2] = time_date.tm_mday;MyRTC_Time[3] = time_date.tm_hour;MyRTC_Time[4] = time_date.tm_min;MyRTC_Time[5] = time_date.tm_sec;
}
5.3.2 MyRTC.h
#ifndef __MYRTC_H
#define __MYRTC_Hextern uint16_t MyRTC_Time[];void MyRTC_Init(void);
void MyRTC_SetTime(void);
void MyRTC_ReadTime(void);#endif
5.3.3 main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化MyRTC_Init(); //RTC初始化/*显示静态字符串*/OLED_ShowString(1, 1, "Date:XXXX-XX-XX");OLED_ShowString(2, 1, "Time:XX:XX:XX");OLED_ShowString(3, 1, "CNT :");OLED_ShowString(4, 1, "DIV :");while (1){MyRTC_ReadTime(); //RTC读取时间,最新的时间存储到MyRTC_Time数组中OLED_ShowNum(1, 6, MyRTC_Time[0], 4); //显示MyRTC_Time数组中的时间值,年OLED_ShowNum(1, 11, MyRTC_Time[1], 2); //月OLED_ShowNum(1, 14, MyRTC_Time[2], 2); //日OLED_ShowNum(2, 6, MyRTC_Time[3], 2); //时OLED_ShowNum(2, 9, MyRTC_Time[4], 2); //分OLED_ShowNum(2, 12, MyRTC_Time[5], 2); //秒OLED_ShowNum(3, 6, RTC_GetCounter(), 10); //显示32位的秒计数器OLED_ShowNum(4, 6, RTC_GetDivider(), 10); //显示余数寄存器}
}