stm32f103c8t6学习笔记(学习B站up江科大自化协)-UNIX时间戳、BKP&RTC

UNIX时间戳

·UNIX时间戳最早是在UNIX系统使用的,所以叫做UNIX时间戳,之后很多由UNIX演变而来的系统也继承了UNIX时间戳的规定,目前linux,windows,安卓这些操作系统的底层计时系统都是用UNIX时间戳

·时间戳这个计时系统和我们常用的年月日时分秒的计时系统具有较大的差别。

年月日时分秒计时系统是每60秒进位1次,记为一分钟,每60分钟进位1次记为1小时,之后便是日月年

时间戳计时系统定义1970年1月1日0点整为0秒,之后只用最基本的秒计时,永不进位,无论数有多大都不进位。

这样有很多好处:

第一,简化了硬件电路,在设计RTC硬件电路的时候直接用一个很大的寄存器即可,无需考虑其他的寄存器

第二,在进行一些时间间隔的计算时非常方便,秒数相见换成时间很方便

第三,存储方便,存储秒数一个很大的变量即可,存储年月日时分秒则需要很多变量

坏处:比较占用软件资源,每次进行转换的时候软件需要进行复杂的计算

·时间戳存储在一个秒计数器中,计算机为了存储这一个永不进位的秒数,定义的变量类型通常会较大。变量类型在不同的系统中是不一样的。在早起的UNIX系统中,这个秒数大多是用32位有符号的整形变量进行存储,32位有符号数所能表示的最大数是 / 2 - 1,这个数是21亿多,有溢出的风险,根据计算,32位的有符号时间戳会在2038年的1月19号溢出,到时候采用32位有符号数存储时间戳的设备,计时系统会因数据溢出而出错,会导致很多不健全的计算机程序崩溃。

随着操作系统和设备的更新换代,目前基本使用64位的数据存储时间戳。

stm32核心的计时部分是一个32位的可编程计数器,说明这款stm32的时间戳是32位的数据类型,但是是无符号的32位,最大是 - 1,大概到2106年才会溢出

·地球上不一样的经度时间是不一样的,穿过英国伦敦的经线是本初子午线,这个位置的时间是一个时间标准,时间戳的1970年1月1日0时0分0秒也是指伦敦时间的0时0分0秒,其他地方分为24个时区,每偏差一个时区时间需要加或减一个小时

对于不同地区是共用同一个时间戳的秒计数器,在伦敦是0,北京也是0,根据不同时区添加小时的偏移即可,伦敦是0点的时候,北京处于东八区就是8点

图中的箭头代表的是一个时间轴,在时间轴上定义一个起点,时间戳从起点开始计时,起点是人为规定的,对于起点之前的时间,时间戳是无法表示的

UTC/GMT

·格林尼治是一个地名,位于英国伦敦,也就是伦敦的标准时间。在格林尼治有个天文台,可以观察天上太阳和星星以确定太阳的自转和公转,将地球自转一周的时间间隔分为24小时。

GMT是以前的计时时间,因为存在一个棘手的问题,因为地球自转一周的时间是不固定的,由于潮汐力等因素导致地球处于越转越慢的状态,根据这个时间基准定义新的时间,那么这个时间基准是不断变化的,地球越转越慢,那么定义的1秒的时间也就越来越长

·为了时间的定义更标准,提出了新的计时系统,叫做UTC协调世界时。在原子钟计时系统的基础上加入了润秒的机制,来消除计时一天和自转一周的误差。所谓润秒就是计时标准是恒定不变的,但是地球越转越慢,当误差超过0.9秒的时候,计时系统就多走一秒等一下地球的自转。

润秒的机制可能会造成一些程序的bug,可能会出现一分钟61秒的情况。平时不追求严谨的话使用GMT或UTC都可以

时间戳转换

·time_t time(time_t*):

作用是获取系统时钟,返回值是time_t,表示当前系统时钟的秒计数期值,参数time_t*,是一个输出参数,输出的内容和返回值是一样的。这个函数可以通过返回值获取系统时钟,也可以通过输出参数获取。

这个函数在电脑中可以直接读取电脑的时间,在STM32中无法使用,因为STM32是一个离线的裸机系统,无法获取目前的时间状态,

代码(使用DEV C++进行编写)
cpp 复制代码
#include <stdio.h>
#include <time.h>

time_t time_cnt;//秒计数器	time_t是一个typedef重命名的类型  如果不是特别声明要用32位的秒计数器类型 
//那么默认状况下就是__time64_t  然后__time_t实际上是int64 是一个有符号的整型数据 无需担心溢出问题 可以用于存储时间戳中一直自增的秒数 
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名 
char *time_str; 

int main()
{
	time_cnt = time(NULL);//参数不需要的话给null即可 
	printf("%d\n",time_cnt);
	return 0;
}

编译烧录后在中断可以看到这样的输出

将终端的数值复制到网络上的时间戳在线转换工具中,进行转换,验证了数据是准确的

将代码

cpp 复制代码
time_cnt = time(NULL);

进行注释,替换成

cpp 复制代码
time(&time_cnt);

也可以进行正常的显示当前的时间值

·struct tm* gmtime(const time_t):

将秒计数器的值转换为格林尼治,也就是伦敦时间。

参数是const time_t*,秒计数器指针类型,是输入参数,返回值struct tm*是日期时间结构体指针类型

gmtime()的参数是time_t*,需要将地址传进去,返回值是struct tm*

如果直接time_date = gmtime(&time_cnt)是指针跨级赋值,等号左边是一个变量,右边是一个地址,解决方法有两个:

第一:time_date = *gmtime(&time_cnt),在函数的右边返回值加上*取内容,等号左右两边都是变量,结构体变量互相赋值是ok的

第二:定义变量的时候struct tm *time_date定义为指针类型,那么time_date = gmtime(&time_cnt)的等号左右都是指针,结构体指针之间相互赋值

代码(使用DEV C++进行编写)
cpp 复制代码
#include <stdio.h>
#include <time.h>

time_t time_cnt;//秒计数器	time_t是一个typedef重命名的类型  如果不是特别声明要用32位的秒计数器类型 
//那么默认状况下就是__time64_t  然后__time_t实际上是int64 是一个有符号的整型数据 无需担心溢出问题 可以用于存储时间戳中一直自增的秒数 
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名 
char *time_str; 

int main()
{
	//time_cnt = time(NULL);//参数不需要的话给null即可 
	//time(&time_cnt);
	time_cnt = 1713960225;
	printf("%d\n",time_cnt);
	
	time_date = *gmtime(&time_cnt);
	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+8);//北京时间东八区
	printf("%d\n",time_date.tm_min);
	printf("%d\n",time_date.tm_sec);
	printf("%d\n",time_date.tm_wday);

	return 0;
}

·struct tm* localtime(const time_t*):

转换当地时间,函数的使用方法和gmtime是一样的,作用也是一样的,不过localtime会根据时区自动添加小时的偏移,程序只需将gmtime的名字替换成localtime即可,这个函数会自动根据当前电脑的设置判断所处的时区,将时间添加时区偏移之后输出

·time_t mktime(struct tm*):

将日期时间转换为秒计数器,mktime传入的日期时间需要是当地的,maketime就是前边两个转换的逆过程。

代码(使用DEV C++进行编写)

编译烧录后,第一行是给定的时间秒数,中间是转换成当地的时间,最后一行是根据日期时间转换回的秒数,最终的秒数和最初的秒数是一样的。

将localtime改成gmtime,最终的秒数就和最初的秒数不一样,说明mktime不是根据伦敦时间进行的

mktime的参数前边是没有const,这个参数既是输入参数也是输出参数。内部的工作过程是这样的:日期时间结构体,里面有年月日时分秒星期等数据,但是仅通过年月日时分秒就足以计算出秒计数器,里边的星期参数实际上是不作为输入参数的;但是相反的是,函数在算出秒数的同时还会顺便计算当前年月日是星期几,回填到结构体中的星期之中。使用这个函数可以很方便的计算对应的是星期几

cpp 复制代码
#include <stdio.h>
#include <time.h>

time_t time_cnt;//秒计数器	time_t是一个typedef重命名的类型  如果不是特别声明要用32位的秒计数器类型 
//那么默认状况下就是__time64_t  然后__time_t实际上是int64 是一个有符号的整型数据 无需担心溢出问题 可以用于存储时间戳中一直自增的秒数 
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名 
char *time_str; 

int main()
{
	//time_cnt = time(NULL);//参数不需要的话给null即可 
	//time(&time_cnt);
	time_cnt = 1713960225;
	printf("%d\n",time_cnt);
	
	time_date = *localtime(&time_cnt);
	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);

	time_cnt = mktime(&time_date); //参数是日期时间结构体指针 可以传入日期时间结构体的地址 返回秒计数器的值 
	printf("%d\n",time_cnt);
	return 0;
}

char* ctime(const time_t*):

将秒计时器转换成字符串表示,使用默认的格式,相当于转换成char* 格式的字符串

char* asctime(const struct tm*):

将日期转换为字符串,使用默认格式。相当于转换成char* 格式的字符串

代码:两个函数卸载一块进行展示
cpp 复制代码
#include <stdio.h>
#include <time.h>

time_t time_cnt;//秒计数器	time_t是一个typedef重命名的类型  如果不是特别声明要用32位的秒计数器类型 
//那么默认状况下就是__time64_t  然后__time_t实际上是int64 是一个有符号的整型数据 无需担心溢出问题 可以用于存储时间戳中一直自增的秒数 
struct tm time_date;//tm是结构体类型名,time_date是结构体变量名 
char *time_str; 

int main()
{
	//time_cnt = time(NULL);//参数不需要的话给null即可 
	//time(&time_cnt);
	time_cnt = 1713960225;
	printf("%d\n",time_cnt);
	
	time_date = *localtime(&time_cnt);
	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+8);
	printf("%d\n",time_date.tm_min);
	printf("%d\n",time_date.tm_sec);
	printf("%d\n",time_date.tm_wday);

	time_cnt = mktime(&time_date); //参数是日期时间结构体指针 可以传入日期时间结构体的地址 返回秒计数器的值 
	printf("%d\n",time_cnt);
		
	time_str = ctime (&time_cnt);
	printf(time_str);
	
	time_str = asctime (&time_date);
	printf(time_str);
	
	return 0;
}

编译运行后显示的为下图所示(最后两行)

size_t strftime(char*,size_t,const char*,const struct tm*):

这个函数作用同上边两个函数一样,但是区别是可以自定义格式。输入下列代码接在前边的代码后边,

cpp 复制代码
char t[50]; 
strftime(t,50,"%H-%M-%S",&time_date);//前两个参数需要传入一个字符数组和数组长度 第三个参数需要给定指定的格式字符串 第四个参数把time_date传进去即可 
printf(t);

其中关于第三个参数的格式,可以参考菜鸟教程中的文件,

编译烧录后得到结果在最后一行

菜鸟教程

clock_t clock(void)函数可以用于计算程序执行的时长,

double difftime(time_t time1,time_t2 time2)可以计算两个时间之间的差值

最重要且最复杂的是其中的localtime 和mktime函数,在STM32的RTC程序会使用到,需要重点掌握

各个函数的关系和作用

秒计数器数据类型time_t:

秒计数器,time_t是一个typedef重命名的类型 如果不是特别声明要用32位的秒计数器类型,那么默认状况下就是__time64_t ,__time_t实际上是int64,是一个有符号的整型数据,无需担心溢出问题,可以用于存储时间戳中一直自增的秒数

日期时间数据类型struct tm:

struct tm time_date;//tm是结构体类型名,time_date是结构体变量名.

关于struct tm里边的变量定义如下

cpp 复制代码
  struct tm {
    int tm_sec;	//表示秒 取值范围0-59 
    int tm_min;	//表示分钟 取值范围0-59 
    int tm_hour; //表示午夜开始的小时 取值范围0-23 
    int tm_mday; //表示一个月的几号 取值范围0-31 
    int tm_mon;  //表示1月开始的第几个月 取值范围0-11 1月是0 12月是11 参数值需要加1才是所需月份 
    int tm_year; //表示从1900年的第几年 所以参数需要加上1900才是年份 时间戳起点是1970 参数最小值是70 
    int tm_wday; //表示周末开始的星期几  0表示周日  1表示周一  直到6表示周六 
    int tm_yday; //表示从1月1号开始的第几天 取值范围0-365 
    int tm_isdst; //是否使用夏令时 +1表示使用 0表示不用 -1表示不知道 
  };
字符串数据类型char*

char型数据的指针,用于表示一个指向时间的字符串

BKP简介

·需要在ST_Link上引出一个3.3V电源接到stm32的VBAT引脚上,用于模拟电池的电源,一般情况下VBAT是电池供电口,需要接备用电池。STM32有四组以VDD开头的供电都是系统的主电源,正常使用STM32时这四组电源供电都需要接到3.3V的电源上。如果要使用内部的BKP和RTC,VBAT引脚就需要接上备用电池,用于维持主电源掉点后给BKP和RTC的供电,电池的负极和主电源的负极共地即可。如果没有外部电池,建议将VBAT引脚接到VDD,同时连接100nf的滤波电容

BKP寄存器和Flash存储器类似,都是用于存储数据,不过Flash的数据是掉电不丢失,而BKP的数据是需要通过VBAT引脚上的备用电池来维持的。只要VBAT有电池供电,即使STM32主电源断电,BKP的值仍然可以维持原状,并且在按下系统复位键之后,BKP的数据并不会产生复位

如果把VBAT的电池断电,再次拔掉主电源上电,BKP的数据被清零,因为BKP的本质并不完全掉电不丢失,数据需要VBAT引脚提供备用电源维持。如果stm32接了备用电池,那么BKP可以完成主电源掉电时保存少量数据的任务。

·TAMPER引脚产生的侵入事件将所有备份寄存器内容清除,TAMPER是一个接到STM32外部的引脚,位置上PC13、TAMPER、RTC共用同一个引脚,在VBAT旁边的2号引脚。在同一时间只能使用一个功能

比如说制作了一个安全系数非常高的设备,需要有防拆功能,BKP中存储敏感数据,要防止被窃取或篡改,可以使用TAMPER引脚的侵入检测功能,设计时可以在TAMPER引脚加上一个上拉或下拉电阻,然后引一根线到设备外壳的防拆触点,一旦设备被拆开,就会在TAMPER引脚产生上升沿或下降沿,此时检测到侵入时间,BKP数据会清零,并申请中断,可以在中断里进行清除其他存储器数据、设备锁死等操作继续保护设备。主电源断电后侵入检测仍然有效,即使设备关机也能防拆

·RTC的校准时钟、闹钟或者秒脉冲信号可以通过RTC引脚输出,其中外部用设备测量RTC校准时钟可以对内部RTC微小的误差进行校准。闹钟脉冲、秒脉冲可以输出为别的设备提供信号,

BKP基本结构

·图中橙色部分可以叫做后备区域,BKP处于后备区域,但是后备区域不只有BKP,RTC的相关电路也位于后备区域。当VDD主电源掉电的时候,后备区域仍然可以有VBAT的电池供电,当VDD主电源上电的时候,后备供电区域会由VBAT切换到VDD,节省电池点亮

·数据寄存器是主要部分,用于存储数据,每个数据寄存器都是16位的,一个 寄存器可以存2个字节。对于中小容量的设备有DR1到DR10共10个数据寄存器,容量是20个字节。对于大容量和互联型设备是DR1到DR42,总共42个寄存器,容量84个字节。

RTC简介

·RTC实时时钟一般是指提供年月日时分秒这种日期信息的计时装置。

·掉电可借助VBAT供电继续走时,这个特性和前边BKP是一样的,为了保持时钟能一直连续运行不出错,主电源断电后RTC走时不能停,系统复位时RTC时间值也不能复位

·20位的可编程预分配器,适配不同频率的输入时钟,32位的秒计数器显然1秒要自增1次,所以驱动计数器的时钟需要是一个1Hz的信号,但是实际提供给RTC模块的时钟,也就是前边的RTCCLK一般频率比较高,所以需要加上一个分频器,将RTCCLK的频率降下来保证输出为1Hz,为了适配各种频率,分频器为20位的,可以选择对输入时钟进行1-范围的分频

·HSE、LSE、LSI可以选择其中的一个接入到RTCCLK,接下来看看时钟树部分的内容

·整个芯片有四个时钟源,H(HIGH)开头是高速,L(LOW)开头是低速,E(External)结尾是外部,I(Internal是内部),高速低速内部外部一组合就是右下角四种情况

高速时钟一般供内部程序运行使用和主要外设使用,低速时钟一般供RTC和看门狗等使用,

图中箭头指向RTC的部分就是RTCCLK,RTCCLK有三个来源:

·第一个是OSC引脚接的HSE外部高速晶振,这个晶振用的是主晶振,一般是8MHz,通过128分频可以产生RTCCLK信号。进行128分频是因为8MHz的主晶振太快,如果不分频直接提供给RTCCLK,后续即使再通过RTC的20位分频器也无法达到1Hz的频率

·(最常用)中间的一路的时钟来源是LSE低速外部晶振,在OSC32引脚接上外部低速晶振,这个晶振产生的时钟可以直接提供给RTCCLK,OSC32是内部RTC专用的时钟。通常与RTC有关的晶振都是统一数值32.768KHz,因为32KHz附近的频率是晶振工艺比较合适的频率,并且32768是2的15次方,所以32768HZ经过一个15位分频器的自然溢出,能很方便的得到1HZ的频率。

自然溢出就是设计一个15位的计数器,无需设置计数目标,从0记到32767,计满后自动溢出的信号就是1Hz,简化电路设计。在RTC电路中基本上是32.768KHz的晶振

·第三路时钟源来自于LSI内部低速RC振荡器,LSI固定是40KHz,如果选择LSI当做RTCCLK,后续经过40K的时钟分频就能得到1Hz的计数时钟,内部的RC振荡器一般精准度没有外部的晶振高,所以LSI给RTCCLK可以当做一个备选方案

·上述的三个时钟源中,第一个主要作为系统主时钟,下面一路主要做看门狗时钟,备选作为RTC的时钟。只有中间一路的时钟可以通过VBAT备用电池供电,上下两路时钟在主电源断电之后是停止运行的,要想实现RTC主电源掉电继续运行的功能,必须选择中间这路RTC专用时钟

RTC框图

·左边是核心的分频和计数计时部分;右边是中断输出使能和NVIC部分;上边是APB1总线的读写部分;下边是和PWR关联的部分,意思是RTC的闹钟可以唤醒设备退出待机模式。

·图中灰色填充部分都处于后备区域,这些电路在主电源掉电之后可以用备用电池维持工作,这些模块在待机时都为继续供电,未被填充的部分待机不供电

分频和计数计时部分:

输入时钟是RTCCLK,RTCCLK的来源是在RCC里进行配置,可选项有三个,但是由于频率各不相同且都远大于1Hz的秒技术频率,所以RTCCLK进来后需要先经过RTC预分频器进行分频。分频器由两个寄存器组成,上边的是重装载寄存器RTC_PRL,下边的是RTC_DIV手册叫做余数寄存器,但是实际上和之前定时器时基单元里的计数器CNT和重装值ARR一样,是计数器的作用。

分频器实际上就是计数器,计几个数溢出一次就是几分频,对于可编程的分频器来说需要有两个寄存器,一个寄存器用来不断的计数,另一个寄存器用来写入一个计数目标值,用于配置是几分频。

上边的PRL是计数目标,写入6就是7分频,写入9就是10分频,因为计数值包含了0,重装值写入n就是n+1分频,下边的DIV就是每来一个时钟记一个数,DIV计数器是一个自减计数器,每来一个时钟DIV的值自减一次,自减到0的时候,再来一个输入时钟,DIV输出一个脉冲产生溢出信号,同时DIV从PRL获取重装值,回到重装值继续自减。

举个例子,RTCCLK为36728Hz时,为了分频后得到1Hz,PRL就要给32727,这个数值是始终不变的,DIV可以保持初始值为0,第一个输入时钟到来时,DIV立刻溢出产生溢出信号给后续电路,同时DIV变为重装值32767,第二个时钟DIV自减变为32766,第三个时钟DIV变为32765,之后来一个时钟自减一次直到为0,再来一个输入时钟会产生一个溢出信号,同时DIV回到32767,这样的话每来32768个输入脉冲,计数器溢出一次,产生一个输出脉冲,分频后输出的频率是1Hz,提供给后边的秒计数器

32位可编程计数器RTC_CNT,就是计时最核心的部分,可以把这个计数器看成是UNIX时间戳的秒计数器,借助time.h函数可方便的获得年月日时分秒,这个RTC还设计有闹钟寄存器RTC_ALR,这个ALR也是32位的寄存器,和CNT等宽,作用就是设置闹钟,可以在ALR写一个秒数设定闹钟,当CNT和ALR设定的闹钟一样时,代表闹钟响,这时会产生一个RTC_Alarm信号通向右边的中断系统,在中断函数里可以执行相应的操作。闹钟兼具一个功能,闹钟信号可以让STM32退出待机模式,比如设计一个数据采集设备需要再环境非常恶劣的地方工作,要求是每天中午12点采集一次数据,其他时间为了节省电量芯片需处于待机模式,此时就可以使用RTC自带的闹钟功能,时间到闹钟响,采集数据完成继续待机。闹钟值是一个定值,只能响一次,如果想实现周期性的闹钟,在每次闹钟响之后都需要重新设置下一个闹钟时间

中断输出使能和NVIC部分:

在右边的中断系统中可以注意到有3个信号可以出发中断:

第一个是RTC_Second,秒中断,来源是CNT的输入时钟,如果开启这个中断,那么程序就会每秒进一次RTC中断,

第二个是RTC_Overflow溢出中断,来源是CNT的右边,意思是CNT的计数器计满溢出后会触发一次中断,这个中断一般不会触发,因为CNT定义的是无符号数,到2106年才会溢出

第三个RTC_Alarm闹钟中断,当计数器和闹钟值相等时触发闹钟中断,同时可以将设备从待机模式唤醒

中断信号到右边的地方,那一块是中断标志位和中断输出控制,F(Flag)结尾的是对应中断标志位,IE(Interrupt Enable)结尾的是中断使能最后三个信号通过一个或门汇聚到NVIC中断控制器

APB1总线的读写部分:

APB总线和APB1接口是程序读写寄存器的地方,读写寄存器可以通过APB1总线完成,也可以看出RTC是APB1总线上的设备

PWR关联的部分:

下边的退出待机模式,还有一个WKUP(wake up)引脚,闹钟信号和WKUP引脚都可以唤醒设备,接在PA0的引脚上

RTC基本结构

RTC的核心部分如图所示,最左边是RTCCLK时钟来源,需要在RCC里边配置,3个时钟选择一个当做RTCCLK,之后先通过预分频器对时钟进行分频;余数寄存器是一个自减计数器,存储当前的计数值,重装计数器是计数目标,决定分频值,分频之后得到1Hz的秒计数信号,通向32位计数器,1秒自增一次,下边有一个32位的闹钟值可以设定闹钟;右边有三个信号可触发中断,分别是秒信号,计数器溢出信号和闹钟信号,3个信号线通过中断输出控制,进中断使能,使能的中断才能通向NVIC,想CPU申请中断;在程序中配置数据选择器可以选择时钟来源,配置重装寄存器可以选择分频系数,配置32位计数器可以进行日期时间的读写,需要中断的话先允许中断在配置NVIC,最后写对应中断函数即可

硬件电路

为了配合STM32的RTC,外部需要一些电路。在最小系统上外部电路还要额外加两部分,第一部分是备用电池,第二部分是外部低速晶振。

简单连接

第一个简单电路参考的是数据手册的这个部分

直接接入一个1.8-3.6V的电池到VBAT即可。在内部有一个供电开关,当VDD有电时开关拨到下面,后备电路由VDD供电,当VDD没电时开关拨到上面,后备电路由VBAT供电。VBAT供电的设备有32KHz振荡器、RTC、唤醒电路和后备寄存器

推荐连接

电池通过二极管D1向VBAT供电,另外主电源的3.3V也通过二极管D2向VBAT供电,最后VBAT再加一个0.1uF的电源滤波电容。设计方案参考自手册的电池备份区域

电池和主电源都加上一个二极管,防止电流绕管,VBAT加一个100nf的滤波电容

外部低速晶振

如图是一个典型的晶振电路,X12是一个32.768KHz的低速晶振,没有正负极之分,两端分别接在OSC32的引脚上,晶振两端分别接一个起振电容到GND,手册参考电路图如下

对于CL1和CL2建议使用高质量的5pF-15pF之间的瓷介电容器

RTC操作注意事项

·如果要使用BKP或RTC,都需要先开启PWR和BKP的时钟,然后使用PWR,使能BKP和RTC的访问。PWR和BKP的时钟需要同时开启,对于RTC没有单独开启时钟的选项

·第二条等待寄存器同步,可以参考前边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总线上,后续读写的值都是没问题的。只需要在初始化的时候调用一个等待同步的函数即可

·第三条设置CNF位,RTC会有一个进入配置模式的标志位,把这一位置1才能设置时间。这个操作在库函数中,每个写寄存器的函数都自动加上了这个操作,无需再次手动调用函数进入配置模式

·第四条注意事项,只需要调用一个等待函数即可,类似于读写Flash芯片,写入之前需要先等待,如果上一次的写入还没完成,不能写入下一次。每次写入之后要等待RTOFF为1,只有RTOFF为1才代表写入完成。

这个操作的原因是PLCK和RTCCLK的时钟频率不一样,用PCLK的频率写入之后,这个值还不能立刻更新到RTC的寄存器里,因为RTC寄存器时由RTCCLK驱动的,PCLK写完之后需要等待RTCCLK的时钟,RTCCLK来一个上升沿,值更新到RTC寄存器里,才完成整个写入过程。

功能实现部分

读写BKP

接线图

程序中会用到的相关函数

cpp 复制代码
//恢复缺省配置 用于手动清空BKP所有的数据寄存器(也可以采用断开备用电源的方法)
void BKP_DeInit(void);
//用于配置TAMPER侵入检测功能 配置引脚的有效电平是高电平触发or低电平触发
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);
//是否开启侵入检测功能  如果需要侵入检测的话先配置有效电平再使能侵入检测即可
void BKP_TamperPinCmd(FunctionalState NewState);
//是否开启中断
void BKP_ITConfig(FunctionalState NewState);
//配置时钟输出功能 可选择在RTC引脚上输出时钟信号 输出RTC校准时钟、RTC闹钟脉冲或秒脉冲
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);
//设置RTC校准值  其实是写入RTC校准寄存器
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);
//写备份寄存器 第一个参数指定写入的DR 第二个参数是写入的数据
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
//读备份寄存器 参数指定读哪个DR 返回值是DR里的值
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
FlagStatus BKP_GetFlagStatus(void);
void BKP_ClearFlag(void);
ITStatus BKP_GetITStatus(void);
void BKP_ClearITPendingBit(void);

//备份寄存器访问使能 设置PWR_CR寄存器里的DBP位
void PWR_BackupAccessCmd(FunctionalState NewState);

由于代码较为简单,只需卸载main中即可

cpp 复制代码
#include "stm32f10x.h"                 
#include "Delay.h"
#include "OLED.h"


int main()
{

	OLED_Init();
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	
	PWR_BackupAccessCmd(ENABLE);
	//小容量的只有1-10 大容量1-42
	BKP_WriteBackupRegister(BKP_DR1,0x1234);
	OLED_ShowHexNum(1,1, BKP_ReadBackupRegister(BKP_DR1),4);
	while(1)
	{

	}
}

烧录后上电发现OLED屏幕显示的是1234,尝试按下复位键发现,数据并不会清零。验证了复位不清零BKP数据

cpp 复制代码
BKP_WriteBackupRegister(BKP_DR1,0x1234);

将上边这行代码进行注释,之后保持VBAT的电源,断开STM32的主电源,之后重新上电,发现显示的仍然是1234,验证了BKP主掉电保存数据。

将主电源和VBAT都断电,重新上电后数据0000,证明了VBAT掉电清零数据

按键改变写入BKP的值并读出

代码
cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"

uint8_t KEY_Num;

uint16_t ArrayWrite[]={0x1234,0x5678};
uint16_t ArrayRead[2];

int main()
{

	OLED_Init();
	GPIO_Key_Init();
	OLED_ShowString(1,1,"W:");
	OLED_ShowString(2,1,"R:");
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	
	PWR_BackupAccessCmd(ENABLE);

	while(1)
	{
		KEY_Num = Key_GetNum();
		if(KEY_Num == 1)
		{
			ArrayWrite[0] ++;
			ArrayWrite[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);
		}
		
		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屏幕上,并进行读取。起初上电的时候可以发现W:后面为空,R:为0000 0000这是因为还没开始写入。按下按键后发现变换的数据是1235 5679,与我们写入的1234 5678不同,这是因为写入之前执行了++的操作;多次按下按键改变写入的值,发现写入和读取都是一样的。

断开主电源的供电,重新上电,发现上电后没有写入,所以W:后面为空,R:后边为上次掉电前的数据

RTC实时时钟

第一行显示日期,给的是一个测试时间;第二行是时间;第三行是时间戳的秒计数器;第四行是RTC预分频器的计数值

作为实时时钟,按下复位键的时候不能复位,时间会继续运行。实时时钟在系统主电源断电之后需要继续运行,就像手机关机后时钟需继续运行,否则造成时间错误。在VBAT接上了电源再断开系统主电源,可以看到时间数据不会丢失,并且在断电期间RTC会继续走时不会暂停,是借助BKP实现的,RTC与BKP关联度较高

相关函数

cpp 复制代码
//配置LSE外部低速时钟
void RCC_LSEConfig(uint8_t RCC_LSE);
//配置LSI内部低速时钟
void RCC_LSICmd(FunctionalState NewState);
//RTCCLK配置 用于选择RTCCLK的时钟源 
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
//启动RTCCLK 在调用上边的选择时钟函数之后 需要调用Cmd进行使能
void RCC_RTCCLKCmd(FunctionalState NewState);


//获取标志位	调用启动时钟的函数之后需要等待标志位置1 时钟才算启动完成工作稳定
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);

//配置中断输出
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
//进入配置模式  CNF位置1后才能写入寄存器
void RTC_EnterConfigMode(void);
//退出配置模式
void RTC_ExitConfigMode(void);
//获取CNT计数器的值	可读取时钟
uint32_t  RTC_GetCounter(void);
//写入CNT计数器的值 可设置时间
void RTC_SetCounter(uint32_t CounterValue);
//写入预分频器  会写入预分频器的PRL重装寄存器中 用于配置预分频器的分频系数
void RTC_SetPrescaler(uint32_t PrescalerValue);
//写入闹钟值	配置闹钟
void RTC_SetAlarm(uint32_t AlarmValue);
//读取预分频器中的DIV余数寄存器 是一个自减计数器 为了得到更细致的时间 
uint32_t  RTC_GetDivider(void);
//等待同步	等待RSF置1
void RTC_WaitForLastTask(void);

//下边四个均为与标志位相关的函数
void RTC_WaitForSynchro(void);
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
void RTC_ClearFlag(uint16_t RTC_FLAG);
ITStatus RTC_GetITStatus(uint16_t RTC_IT);
void RTC_ClearITPendingBit(uint16_t RTC_IT);

main.c部分代码

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"

int main()
{
	OLED_Init();
	MyRTC_Init();
	
	while(1)
	{
		OLED_ShowNum(1,1,RTC_GetCounter(),10);
	}
}

MyRTC.c部分的代码

cpp 复制代码
#include "stm32f10x.h"


void MyRTC_Init(void)
{
	//1.开启PWR和BKP的时钟 使能BKP和RTC的访问
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	
	PWR_BackupAccessCmd(ENABLE);
	
	//2.开启LSE时钟 并等待LSE时钟启动完成
	//共3个参数 LSE_OFF表示LSE振荡器关闭 LSE_ON表示打开
	//LSE_Bypass 表示时钟旁路 意思是不接晶振 直接从OSC32_IN这个引脚输入一个指定频率的信号当做时钟源
	RCC_LSEConfig(RCC_LSE_ON);
	//等待LSE启动完成
	while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
	
	//选择RTCCLK时钟源
	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
	//使能时钟
	RCC_RTCCLKCmd(ENABLE);
	
	//等待同步	函数内部自带while等待 调用即可实现等待效果
	RTC_WaitForSynchro();
	//等待上一次写入操作完成
	RTC_WaitForLastTask();
	
	//要将LSE的32.768KHz频率分频为1分频,其中 减1 是因为计数值包含0
	RTC_SetPrescaler(32768 - 1);//自带进入和退出配置模式的代码  	
	RTC_WaitForLastTask();
	
	//给一个32位的秒计数器
	RTC_SetCounter(1672588795);
	RTC_WaitForLastTask();
	
}

编译烧录后程序运行的效果是显示时间戳,并按每秒增加1的频率实时更新

时间戳、日期、CNT、DIV显示

MyRTC.c部分代码

cpp 复制代码
#include "stm32f10x.h"
#include <time.h>

uint16_t MyRTC_Time[] = {2023,1,1,23,59,55};//2023超过了uint8_t数据的范围 所以定义成16位

//将数组的时间刷新到RTC外设里 调用读取函数 将RTC外设的时间刷新到这个数组里
//值得注意的是:如果是个位数 前面不要补0 因为在C语言中以0开头的是8进制 虽然8进制的1和10进制的1一样
//但是如果写09的时候会报错,因为在八进制中数字9是无效的 所以在十进制的前边不可以随意补0

void MyRTC_Init(void)
{
	//1.开启PWR和BKP的时钟 使能BKP和RTC的访问
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	
	PWR_BackupAccessCmd(ENABLE);
	
	if(BKP_ReadBackupRegister(BKP_DR1) != 0xa5a5)//第一次上电时 或者系统完全断电之后 BKP_DR1默认是0
	{	//if成立执行初始化 初始化之后置BKP标志位为a5a5 下次读到这个数据说明备用电池没断电
		
		
		//2.开启LSE时钟 并等待LSE时钟启动完成
		//共3个参数 LSE_OFF表示LSE振荡器关闭 LSE_ON表示打开
		//LSE_Bypass 表示时钟旁路 意思是不接晶振 直接从OSC32_IN这个引脚输入一个指定频率的信号当做时钟源
		RCC_LSEConfig(RCC_LSE_ON);
		//RCC_LSICmd(ENABLE);//启动LSI
		//等待LSE启动完成
		while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
		//while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);//等待LSI
		
		
		//选择RTCCLK时钟源
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
		//RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);//时钟修改为LSI
		//使能时钟
		RCC_RTCCLKCmd(ENABLE);
		
		//等待同步	函数内部自带while等待 调用即可实现等待效果
		RTC_WaitForSynchro();
		//等待上一次写入操作完成
		RTC_WaitForLastTask();
		
		//要将LSE的32.768KHz频率分频为1分频,其中 减1 是因为计数值包含0
		RTC_SetPrescaler(32768 - 1);//自带进入和退出配置模式的代码
		//RTC_SetPrescaler(40000 - 1);//LSI时钟源40KHz
		RTC_WaitForLastTask();
		
		//给一个32位的秒计数器
		RTC_SetCounter(1672588795);
		RTC_WaitForLastTask();
	
		BKP_WriteBackupRegister(BKP_DR1,0xa5a5);
	}
	else//如果已经初始化过了 则无需初始化 但是最好执行等待的函数防止意外
	{
		RTC_WaitForSynchro();
		RTC_WaitForLastTask();
	}
	

	
}

void MyRTC_SetTime(void)
{
	time_t time_cnt;
	struct tm time_date;
	
	//1.把数组指定的时间填充到struct tm结构体里
	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_sec  = MyRTC_Time[4];
	time_date.tm_min  = MyRTC_Time[5];
	//2.使用mktime函数得到秒数
	time_cnt = mktime(&time_date);
	//3.将秒数写入到RTC的CNT中
	RTC_SetCounter(time_cnt);
	RTC_WaitForLastTask();
}

void MyRTC_ReadTime(void)
{
	time_t time_cnt;
	struct tm time_date;
	
	//1.RTC_GetCounter读取CNT秒数
	time_cnt = RTC_GetCounter();
	//2.使用localtime函数得到日期时间
	time_date = *localtime(&time_cnt);	//返回值是struct tm* 所以需要先取内容
	//3.将time_date的日期时间转移到数组里 操作和上边相反即可
	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_sec ;
	MyRTC_Time[5] = time_date.tm_min ;
}

mian.c部分代码

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"

int main()
{
	OLED_Init();
	MyRTC_Init();
	
	OLED_ShowString(1,1,"Date:xxxx-xx-xx");
	OLED_ShowString(2,1,"Time:xx:xx:xx");
	OLED_ShowString(3,1,"CNT:");
	OLED_ShowString(3,1,"DIV:");
	while(1)
	{
		MyRTC_ReadTime();//调用之后CNT对应的时间值就会刷新到数组里
		//读取全局数组进行显示即可
		
		OLED_ShowNum(1, 6,MyRTC_Time[0],4);
		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,11,MyRTC_Time[4],2);
		OLED_ShowNum(2,14,MyRTC_Time[5],2);
		
		OLED_ShowNum(3,6,RTC_GetCounter(),10);
		OLED_ShowNum(3,6,(32767 - RTC_GetDivider()) / 32767.0 * 999,10);
		//转换为毫秒 先用32767减去DIV,这样范围就是0-32767,
		//除以36727乘以999进行缩放 其中为了防止丢失小数 加上.0变为浮点运算
		//这样显示的倒数就从32767自减变成了999自减
	}
}
相关推荐
CCSBRIDGE2 小时前
Magento2项目部署笔记
笔记
爱吃西瓜的小菜鸡2 小时前
【C语言】判断回文
c语言·学习·算法
不过四级不改名6773 小时前
蓝桥杯嵌入式备赛教程(1、led,2、lcd,3、key)
stm32·嵌入式硬件·蓝桥杯
小A1593 小时前
STM32完全学习——SPI接口的FLASH(DMA模式)
stm32·嵌入式硬件·学习
亦枫Leonlew3 小时前
微积分复习笔记 Calculus Volume 2 - 5.1 Sequences
笔记·数学·微积分
岁岁岁平安3 小时前
spring学习(spring-DI(字符串或对象引用注入、集合注入)(XML配置))
java·学习·spring·依赖注入·集合注入·基本数据类型注入·引用数据类型注入
武昌库里写JAVA3 小时前
Java成长之路(一)--SpringBoot基础学习--SpringBoot代码测试
java·开发语言·spring boot·学习·课程设计
qq_589568103 小时前
数据可视化echarts学习笔记
学习·信息可视化·echarts
爱码小白4 小时前
网络编程(王铭东老师)笔记
服务器·网络·笔记
LuH11244 小时前
【论文阅读笔记】Learning to sample
论文阅读·笔记·图形渲染·点云