STM32标准库——(18)Unix时间戳、BKP备份寄存器、RTC实时时钟

目录

1.Unix时间戳

1.1 简介

1.2 UTC/GMT

1.3 时间戳转换

1.4 C标准库 

1.4.1 time.h 中定义的变量类型

1.4.2 tm结构定义

1.4.3 相关示例

2.BKP备份寄存器

2.1 简介

2.2 基本结构

3.RTC实时时钟

3.1 简介

3.2 框图

3.3 基本结构

3.4 硬件电路

3.5 操作注意事项

4.读写备份寄存器B

4.1 相关API

4.1.1 BKP_ReadBackupRegister

4.1.2 BKP_WriteBackupRegister

4.1.3 PWR_BackupAccessCmd

4.2 接线图

4.3 相关代码

5.读写RTC实时时钟

5.1 相关API

5.1.1 RCC_LSEConfig

5.1.2 RCC_GetFlagStatus

5.1.3 RCC_RTCCLKConfig

 5.1.4 RCC_RTCCLKCmd

5.1.5 RTC_WaitForSynchro

5.1.6 RTC_WaitForLastTask

5.1.7 RTC_SetPrescaler

5.1.8 RTC_SetCounter

5.1.9 RTC_GetCounter

5.2 接线图

5.3 相关代码

5.3.1 MyRTC.c

5.3.2 MyRTC.h

5.3.3 main.c


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 中定义的变量类型
序号变量 & 描述
1size_t 是无符号整数类型,它是 sizeof 关键字的结果。
2clock_t 这是一个适合存储处理器时间的类型。
3time_t is 这是一个适合存储日历时间类型。
4struct 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 = 1709649653
      Date = *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 框图

  1. 首先看分频和计数器计数部分,这一块的输入时钟是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获取重装值,回到重装值继续自减

  2. 然后看一下计数计时部分,32位可编程计数器,RTC_CNT就是计时最核心的部分,我们可以把这个计数器看作是Unix时间戳的秒计数器,这样借助time.h的函数,就可以很方便地得到年月日时分秒了,然后在下面这里,这个RTC还设计的有一个闹钟寄存器RTC_ALR,这个ALR也是一个32位的寄存器,和上面这个cnt是等宽的,它的作用顾名思义就是设置闹钟,我们可以在ALR写一个秒数设定闹钟,当cnt的值跟ALR设定的闹钟值一样时,也是这里画了等号啊,如果他俩值相等就代表闹钟响了,这时就会产生RTC_Alarm闹钟信号,通往右边的中断系统,在中断函数里你可以执行相应的操作,同时这个闹钟还兼具一个功能,就下面这里的闹钟信号,可以让STM32退出待机模式,这个就可以对应一些用途,比如你设计一个数据采集设备,需要在环境非常恶劣的地方工作,比如海底高原深井这些地方,然后要求是每天中午12点采集一次环境数据,其他时间为了节省电量避免频繁换电池,芯片都必须处于待机模式,这样的话我们就可以用这个rtc自带的闹钟功能,定一个中午12点的闹钟,闹钟一响芯片唤醒采集数据完成后继续待机,另外这个闹钟值是一个定值,只能响一次,所以如果你想实现周期性的闹钟,大家每次闹钟响之后都需要再重新设置一下下一个闹钟时间,就是这个闹钟和闹钟唤醒的一个用途。

  3. 那继续往右看,这是中段部分的,在左边这里有三个信号可以触发中断,第一个是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 操作注意事项

  1. 正常的外设第一步开启时钟就能用了,但是BKP和RTC这两个外设,开启稍微复杂一些,如果你要使用BKP或者RTC,都要先执行这两步,第一步开启PWR和BKP的时钟,第二步使用PWR使能BKP和RTC的访问
  2. 若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1,这一步对应代码里的一个库函数就是RTC等待同步,一般在刚上电的时候调用一下这个函数就行了,为什么要有这一步呢,可以看看上面的框图,在这里会有两个时钟PCLK1和RTCCLKPCLK1在主电源掉电时会停止,所以为了保证RTC主电源掉电正常工作,RTC里的寄存器,都是在RTCCLK的同步下变更的当我们用PCLK驱动的总线去读取RTCCLK驱动的寄存器时,就会有个时钟不同步的问题,RTC寄存器只有在RTCCLK的上升沿更新,但是PCLK1的频率36MHz远大于RTCCLK的频率32KHz如果我们在APB1刚开启时,就立刻读取RTC寄存器,有可能RTC寄存器还没有更新到APB1总线上,这样我们读到的值就是错误的,通常来说就读取到0,所以这就要求我们在APB1总线刚开机时,要等一下RTCCLK,只要RTCCLK来一个上升沿,rtc把它的寄存器的值同步到APB1总线上,这样之后读取的值就都是没问题的了,这是设计细节的一个问题,当然我们其实也不用管那么多了,只需要在初始化时调用一个等待同步的函数就行了。

  3. 必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器,这一条其实比较简单,就是RTC会有一个进入配置模式的标志位把这一位置1才能设置时间,其实这个操作在库函数中,每个写寄存器的函数,它都自动帮我们加上了这个操作,所以我们就不用再单独调用函数进入配置模式了。

  4. 对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来源为LSE
		RCC_RTCCLKCmd(ENABLE);								//RTCCLK使能
		
		RTC_WaitForSynchro();								//等待同步
		RTC_WaitForLastTask();								//等待上一次操作完成
		
		RTC_SetPrescaler(32768 - 1);						//设置RTC预分频器,预分频后的计数频率为1Hz
		RTC_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_H

extern 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);	//显示余数寄存器
	}
}
  • 23
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值