首页 > 其他分享 >十三、BKP备份寄存器&RTC实时时钟

十三、BKP备份寄存器&RTC实时时钟

时间:2024-03-03 21:45:06浏览次数:16  
标签:RTC void BKP 寄存器 RCC 时钟

十一、BKP备份寄存器&RTC实时时钟

Unix时间戳

  • Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒
  • 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
  • 世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间

image

UTC/GMT

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

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

C语言时间戳转换函数

常用函数

函数 作用
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*); 日期时间转换为字符串(自定义格式)

image

数据类型

  • time_t:该类型默认使用64位的int类型。也可以特别声明要使用32位int类型

    typedef __int64                   __time64_t;
    
    #ifndef _CRT_NO_TIME_T
        #ifdef _USE_32BIT_TIME_T
            typedef __time32_t time_t;
        #else
            typedef __time64_t time_t;
        #endif
    #endif
    
  • 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(0表示周天,1表示周1) */
       int tm_yday;        /* 一年中的第几天,范围从 0 到 365 */
       int tm_isdst;       /* 夏令时 */    
    };
    

time

time_t time(time_t*);

获取系统时钟

计算当前日历时间,并把它编码成 time_t 格式。

#include <stdio.h>
#include <time.h>

time_t t_cnt;

int main()
{
    // 第一种方式:将time函数的返回值赋值给time_t类型的变量,参数部分写NULL
	t_cnt = time(NULL);
	printf("%lld\n", t_cnt);

    // 第二种方式:将time_t类型的变量地址作为参数
	time(&t_cnt);
	printf("%lld\n", t_cnt);

	return 0;
}

/* 运行结果 */
1709193537
1709193537

gmtime和localtime

struct tm* gmtime(const time_t*);秒计数器转换为日期时间(格林尼治时间)

timer 的值被分解为 tm 结构,并用协调世界时(UTC)也被称为格林尼治标准时间(GMT)表示。

#include <stdio.h>
#include <time.h>

time_t t_cnt;
struct tm t_date;

int main()
{
	// 获取当前系统时间,秒计数器
	time(&t_cnt);
	printf("%lld\n", t_cnt);

	t_date = *localtime(&t_cnt);
	printf("年:%d\n", t_date.tm_year + 1900);
	printf("月:%d\n", t_date.tm_mon + 1);
	printf("日:%d\n", t_date.tm_mday);
	printf("时:%d\n", t_date.tm_hour);
	printf("分:%d\n", t_date.tm_min);
	printf("秒:%d\n", t_date.tm_sec);

	return 0;
}

/* 运行结果 */
1709193537
年:2024
月:2
日:29
时:15
分:58
秒:57

struct tm* localtime(const time_t*); 秒计数器转换为日期时间(当地时间)

timer 的值被分解为 tm 结构,并用本地时区表示。

mktime

time_t mktime(struct tm*); 日期时间转换为秒计数器(当地时间)

#include <stdio.h>
#include <time.h>

time_t t_cnt;
struct tm t_date;

int main()
{
	// 获取当前系统时间,编码为time_t格式 
	time(&t_cnt);
	// 将time_t的值被分解为 tm 结构,并用本地时区表示。 
	t_date = *localtime(&t_cnt);
	// 把结构转换为一个依据本地时区的 time_t 值。
	t_cnt = mktime(&t_date);
	
	printf("%lld\n", t_cnt);
	return 0;
}

/* 运行结果 */
1709193537

ctime和asctime

char* ctime(const time_t*); 秒计数器转换为字符串(默认格式)

char* asctime(const struct tm*); 日期时间转换为字符串(默认格式)

#include <stdio.h>
#include <time.h>

time_t t_cnt;
struct tm t_date;
char* t_str;

int main()
{
	// 获取当前系统时间,编码为time_t格式 
	time(&t_cnt);
	// 将time_t的值被分解为 tm 结构,并用本地时区表示。 
	t_date = *localtime(&t_cnt);
	
	// 将time_t时间格式转换为字符串(默认格式) 
	t_str = ctime(&t_cnt);
	printf(t_str);
	
	// 将tm结构体的时间格式转换为字符串(默认格式) 
	t_str = asctime(&t_date);
	printf(t_str);
	
	return 0;
}

/* 运行结果 */
Thu Feb 29 16:52:19 2024
Thu Feb 29 16:52:19 2024

strftime

size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)

根据 format 中定义的格式化规则,格式化结构 timeptr 表示的时间,并把它存储在 str 中。

参数

  • str -- 这是指向目标数组的指针,用来复制产生的 C 字符串。
  • maxsize -- 这是被复制到 str 的最大字符数。
  • format -- 这是 C 字符串,包含了普通字符和特殊格式说明符的任何组合。这些格式说明符由函数替换为表示 tm 中所指定时间的相对应值。格式说明符是:
说明符 替换为 实例
%a 缩写的星期几名称 Sun
%A 完整的星期几名称 Sunday
%b 缩写的月份名称 Mar
%B 完整的月份名称 March
%c 日期和时间表示法 Sun Aug 19 02:56:02 2012
%d 一月中的第几天(01-31) 19
%H 24 小时格式的小时(00-23) 14
%I 12 小时格式的小时(01-12) 05
%j 一年中的第几天(001-366) 231
%m 十进制数表示的月份(01-12) 08
%M 分(00-59) 55
%p AM 或 PM 名称 PM
%S 秒(00-61) 02
%U 一年中的第几周,以第一个星期日作为第一周的第一天(00-53) 33
%w 十进制数表示的星期几,星期日表示为 0(0-6) 4
%W 一年中的第几周,以第一个星期一作为第一周的第一天(00-53) 34
%x 日期表示法 08/19/12
%X 时间表示法 02:50:06
%y 年份,最后两个数字(00-99) 01
%Y 年份 2012
%Z 时区的名称或缩写 CDT
%% 一个 % 符号 %
  • timeptr -- 这是指向 tm 结构的指针

示例代码

#include <stdio.h>
#include <time.h>

time_t t_cnt;
struct tm t_date;

int main()
{
	// 获取当前系统时间,编码为time_t格式 
	time(&t_cnt);
	// 将time_t的值被分解为 tm 结构,并用本地时区表示。 
	t_date = *localtime(&t_cnt);
	
	// 定义字符串 
	char str[50];
	// 自定义时间字符串格式 
	strftime(str, 50, "%Y/%m/%d %H:%M:%S", &t_date);
	
	printf(str);
	
	return 0;
}

/* 运行结果 */
2024/02/29 17:05:41

BKP备份寄存器

BKP简介

  • BKP(Backup Registers)备份寄存器

  • BKP可用于存储用户应用程序数据。当VDD(2.03.6V)电源被切断,他们仍然由VBAT(1.83.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位

    VDD 就是系统的主电源,供电电压是 2.0~3.6V;VBAT(V Battery)就是备用电池电源。

    引脚定义表中标红色的部分就是供电引脚

    • VDD 和 VSS_1、2、3 是内部数字部分电路的供电;
    • VDDA 和 VSSA 是内部模拟部分电路的供电
    • 这四组以 VDD 开头的供电,都是系统的主电源,在正常使用 STM32 时,这四组供电全部都需要接到 3.3V 的电源上。
    • VBAT 就是备用电池供电引脚,如果要使用 STM32 内部的 BKP 和 RTC,这个引脚就必须接备用电池,用来维持 BKP 和 RTC,在 VDD 主电源掉电后的供电,这里备用电池只有一根正极的供电引脚,接电池时,电池正极接到 VBAT,电池负极和主电源的负极接在一起共地
  • TAMPER引脚产生的侵入事件将所有备份寄存器内容清除

    TEMPER 引脚是一个安全保障设计,可以使能 TAMPER 引脚的侵入检测功能。

    设计电路时,TAMPER 引脚可以先加一个默认的上拉或者下拉电阻,然后引一根线到你的设备外壳的防拆开关或触点,别人一拆开你的设备,触发开关,就会在 TAMPER 引脚产生上升沿或者下降沿,这样 STM32 就检测到侵入事件了,这时 BKP 的数据会自动清零,并且申请中断,你在中断里,还可以继续保护设备,比如清除其他存储器数据,然后设备锁死,这样来保障设备的安全;

    主电源断电后,侵入检测仍然有效,这样即使设备关机也能防拆

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

    外部用设备测量 RTC 校准时钟,可以对内部 RTC 微小的误差进行校准;

    闹钟脉冲或者秒脉冲可以输出出来,为别的设备提供这些信号

  • 存储RTC时钟校准寄存器

    这个可以配合上面这个校准时钟输出的功能,结合一些测量方法,可以对 RTC 进行校准

注意: 根据引脚定义图可知, PC13、TEMPER 和 RTC 这 3 个引脚共用一个端口,所以这 3 个功能,同一时间,只能使用一个。

用户数据存储容量:20字节(中容量和小容量)/ 84字节(大容量和互联型)

BKP基本结构

image

这个图中橙色部分,可以叫做后备区域。

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 的误差进行校准。

RTC实时时钟

RTC简介

  • RTC(Real Time Clock)实时时钟
  • RTC是一个独立的定时器,可为系统提供时钟和日历的功能
  • RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.03.6V)断电后可借助VBAT(1.83.6V)供电继续走时
  • 32位的可编程计数器,可对应Unix时间戳的秒计数器
  • 20位的可编程预分频器,可适配不同频率的输入时钟

可选择三种RTC时钟源:

RCC时钟树部分截图

image

  • HSE时钟除以128(通常为8MHz/128)

    OSC 引脚接的 HSE,外部高速晶振,这个晶振是主晶振,我们一般都用的 8 MHz,8 MHz 进来,通过 128 分频,可以产生 RTCCLK 信号。后续再通过 20 位的分频器,进行一个适当的分频,输出 1 Hz 的信号给计数器

  • LSE振荡器时钟(通常为32.768KHz)

    外部低速晶振,在 OSC32 这两个引脚,接上外部低速晶振,这个晶振产生的时钟,可以直接提供给 RTCCLK,是内部 RTC 的专用时钟。

    跟 RTC 有关的晶振,都是统一的数值,就是 32.768 KHz。

    32768,这是一个 2 的次方数,2^15 = 32768,所以 32.768 KHz,即 32768 Hz,经过一个 15 位分频器的自然溢出,就能很方便的得到 1 Hz 的频率。

  • LSI振荡器时钟(40KHz)

    内部低速 RC 振荡器。LSI,固定是 40 KHz,如果选择 LSI 当作 RTCCLK,后续再经过 40K 的分频,就能得到 1 Hz 的计数时钟了。

    当然内部的 RC 振荡器,一般精准度没有外部晶振高,所以 LSI 给 RTCCLK,可以当作一个备选方案,另外,LSI 还可以提供给看门狗

最常用的就是中间这一路,外部 32.768 KHz 的晶振,提供 RTCCLK 的时钟。

  1. 中间这一路,32.768 KHz 的晶振,本身就是专供 RTC 使用的,上下这两路,其实是有各自的任务。
  2. 只有中间这一路的时钟,可以通过 VBAT 备用电池供电。要想实现 RTC 主电源掉电继续走时的功能,必须得选择中间这一路的 RTC 专用时钟

RTC框图

image

  1. 分频和计数计时部分

    输入时钟是 RTCCLK:RTCCLK 的来源需要在 RCC 里进行配置,可以选择的选项是 3 种 RTC 时钟源,我们主要选择第二种

    RCT预分频器:RTCCLK 进来,需要首先经过 RTC 预分频器进行分频,这个分频器由两个寄存器组成。

    • 分频器其实就是一个计数器,计几个数溢出一次,那就是几分频。所以对于可编程的分频器来说,需要有两个寄存器,一个寄存器用来不断地计数;另一个寄存器,我们写入一个计数目标值,用来配置是几分频。

    • 上面这个是重装载寄存器 RTC_PRL,用来配置分频系数;

    • 下面这个 RTC_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_ALR,这个 ALR 也是一个 32 位的寄存器,和上面这个 CNT 是等宽的

    • 他的作用就是设置闹钟,我们可以在 ALR 写一个秒数,设定闹钟,当 CNT 的值跟 ALR 设定的闹钟值一样时,就代表闹钟响了,就会产生 RTC_Alarm 闹钟信号,通往右边的中断系统,在中断函数里,你可以执行相应的操作;

    • 这个闹钟还可以让 STM32 退出待机模式,为了节省电量,芯片处于待机模式。我们可以用 RTC 自带的闹钟功能,定一个闹钟,闹钟一响,芯片唤醒,完成指定的任务,完成后,继续待机。另外,这个闹钟值是一个定值,只能响一次,所以如果你想实现周期性的闹钟,那在每次闹钟响之后,都需要在重新设置一下下一个闹钟时间

  2. 中断部分了

    有 3 个信号可以触发中断

    • RTC_Second:秒中断,它的来源,就是 CNT 的输入时钟,如果开启这个中断,那么程序就会每秒进一次 RTC 中断;
    • RTC_Overflow:溢出中断,它的来源是 CNT 的右边,意思就是 CNT 的 32 位计数器计满溢出了会触发一次中断,所以这个中断一般不会触发,我们上一小节说过这个 CNT 定义是无符号数,到 2106 年才会溢出。
    • RTC_Alarm:闹钟中断,上面介绍过,当计数器和闹钟值相等时,触发中断,同时,闹钟信号可以把设备从待机模式中唤醒。

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

  3. APB1总线读写部分

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

  4. PWR部分

    退出待机模式,还有一个 WKUP 引脚。闹钟信号和 WKUP(Weak Up)引脚,都可以唤醒设备,WKUP 引脚可以看一下接线图,就是 PA0 的位置,它兼具唤醒的功能

RTC基本结构

image

  • 最左边是 RTCCLK 时钟来源,这一块需要在 RCC 里配置,3 个时钟,选择一个,当作 RTCCLK;

  • RTCCLK 通过预分频器,对时钟进行分频,余数寄存器是一个自减计数器,存储当前的计数值,重装寄存器是计数目标,决定分频值,分频之后,得到 1 Hz 的秒计数信号;通向 32 位计数器,1s 自增一次

  • 下面还有一个 32 位的闹钟值,可以设定闹钟,如果不需要闹钟的话,下面这块可以不用管;

  • 右边有 3 个信号可以触发中断,分别是秒信号、计数器溢出信号和闹钟信号,3 个信号先通过中断输出控制进行中断使能,使能的中断才能通向 NVIC,然后向 CPU 申请中断。

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

RTC外部硬件电路

为了配合 STM32 的 RTC,外部还是需要有一些电路的,在最小系统电路上,外部电路还要额外加两部分

image

  • 备用电池。首先,备用电池供电部分,这里给了两个参考电路:

    1. 第一个是简单连接,就是使用一个 3V 的电池,负极和系统共地,正极直接引到 STM32 的 VBAT 引脚,这样就行了,这个供电方案非常简单,参考来源是 STM32 的数据手册,在 5.1.6 供电方案这里,就给出了这个图,

    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 没电时,就是备用电池供电,这是根据参考手册,设计的推荐电路。

  • 外部低速晶振。这里 X1 是一个 32.768 KHz 的 RTC 晶振,这个晶振不分正负极,两端分别接在 OSC32 这两个引脚上,然后晶振两端,再分别接一个起振电容,到 GND。

    这个电路的设计参考来源还是 STM32 的数据手册,在 5.3.6 外部时钟源特性这里有参考电路,使用一个晶体/陶瓷谐振器产生的低速外部时钟,下面这里就是典型电路,晶振是 32.768 KHz,CL1 和 CL2 上面这里写了,对于 CL1 和 CL2,建议使用高质量的 5 pF~15 pF 之间的瓷介电容器

外部硬件

image

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

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

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 的访问

  2. 若在读取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 总线上,这样之后读取的值,就都是没问题的了。

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

    RTC 会有一个进入配置模式的标志位,把这一位置 1,才能设置时间。

    在我们调用写寄存器的库函数时,里面已经包含了进入配置模式的代码,无需我们再手动添加了

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

    这个操作也是调用一个等待的函数就行了,跟我们之前读写 Flash 芯片是类似的。

    因为这里的 PCLK1 和 RTCCLK 时钟频率不一样,你用 PCLK1 的频率写入之后,这个值还不能立刻更新到 RTC 的寄存器里。因为 RTC 寄存器是由 RTCCLK 驱动的,所以 PCLK1 写完之后,得等一下 RTCCLK 的时钟,RTCCLK 来一个上升沿,值更新到 RTC 寄存器里,整个写入过程才算结束了

BKP相关库函数

// BKP寄存器全部复位。可以用来手动清空BKP所有的数据寄存器
void BKP_DeInit(void);

// 配置TAMPER引脚的有效电平。是上升沿还是下降沿触发TAMPER的侵入检测
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);

// TARMPER侵入检测使能
void BKP_TamperPinCmd(FunctionalState NewState);

// BKP中断使能
void BKP_ITConfig(FunctionalState NewState);

// 时钟输出功能配置。可以输出RTC校准时钟、RTC闹钟或秒脉冲
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);

// 设置RTC校准值
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);

// 写指定的BKP数据寄存器
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);

// 读指定的BKP数据寄存器
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

// 获取BKP相关标志位
FlagStatus BKP_GetFlagStatus(void);

// 清除BKP相关标志位
void BKP_ClearFlag(void);

// 获取BKP相关的中断标志位
ITStatus BKP_GetITStatus(void);

// 清除BKP相关的中断标志位
void BKP_ClearITPendingBit(void);

RTC相关库函数

// RTC中断使能
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);

// RTC进入配置模式
void RTC_EnterConfigMode(void);

// RTC退出配置模式
void RTC_ExitConfigMode(void);

// 获取RTC_CNT寄存器的值。RTC_CNT用来存储时间戳
uint32_t  RTC_GetCounter(void);

// 设置RTC_CNT寄存器的值。RTC_CNT用来存储时间戳
void RTC_SetCounter(uint32_t CounterValue);

// 设置RTC_PRL寄存器的值。RTC_PRL存储对RTCCLK的预分频值,包含0
void RTC_SetPrescaler(uint32_t PrescalerValue);

// 设置闹钟。写RTC_ALR寄存器
void RTC_SetAlarm(uint32_t AlarmValue);

// 获取RTC_DIV寄存器的值。RTC_DIV是预分频的计数器,读取这个可以获取更精细的时间
uint32_t  RTC_GetDivider(void);

// 等待RTC寄存器最后一次写操作完成。每次写RTC的寄存器后都需要调用一下该函数,或者写寄存器前调用也可以。对应RTC操作注意事项的第四条
void RTC_WaitForLastTask(void);

// 等待RTC寄存器同步完成。在STM32每次上电后调用一下即可。对应RTC操作注意事项的第二条
void RTC_WaitForSynchro(void);

// 获取RTC相关标志位
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);

// 清除RTC相关标志位
void RTC_ClearFlag(uint16_t RTC_FLAG);

// 获取RTC相关的中断标志位
ITStatus RTC_GetITStatus(uint16_t RTC_IT);

// 清除RTC相关的中断标志位
void RTC_ClearITPendingBit(uint16_t RTC_IT);

RCC相关库函数

void RCC_DeInit(void);
void RCC_HSEConfig(uint32_t RCC_HSE);
ErrorStatus RCC_WaitForHSEStartUp(void);
void RCC_AdjustHSICalibrationValue(uint8_t HSICalibrationValue);
void RCC_HSICmd(FunctionalState NewState);
void RCC_PLLConfig(uint32_t RCC_PLLSource, uint32_t RCC_PLLMul);
void RCC_PLLCmd(FunctionalState NewState);

#if defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || defined (STM32F10X_HD_VL) || defined (STM32F10X_CL)
 void RCC_PREDIV1Config(uint32_t RCC_PREDIV1_Source, uint32_t RCC_PREDIV1_Div);
#endif

#ifdef  STM32F10X_CL
 void RCC_PREDIV2Config(uint32_t RCC_PREDIV2_Div);
 void RCC_PLL2Config(uint32_t RCC_PLL2Mul);
 void RCC_PLL2Cmd(FunctionalState NewState);
 void RCC_PLL3Config(uint32_t RCC_PLL3Mul);
 void RCC_PLL3Cmd(FunctionalState NewState);
#endif /* STM32F10X_CL */ 

void RCC_SYSCLKConfig(uint32_t RCC_SYSCLKSource);
uint8_t RCC_GetSYSCLKSource(void);
void RCC_HCLKConfig(uint32_t RCC_SYSCLK);
void RCC_PCLK1Config(uint32_t RCC_HCLK);
void RCC_PCLK2Config(uint32_t RCC_HCLK);
void RCC_ITConfig(uint8_t RCC_IT, FunctionalState NewState);

#ifndef STM32F10X_CL
 void RCC_USBCLKConfig(uint32_t RCC_USBCLKSource);
#else
 void RCC_OTGFSCLKConfig(uint32_t RCC_OTGFSCLKSource);
#endif /* STM32F10X_CL */ 

void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);

#ifdef STM32F10X_CL
 void RCC_I2S2CLKConfig(uint32_t RCC_I2S2CLKSource);                                  
 void RCC_I2S3CLKConfig(uint32_t RCC_I2S3CLKSource);
#endif /* STM32F10X_CL */ 

// 配置外部低速时钟LSE。需要等待RCC_FLAG_LSERDY标志位置1后才启动成功
void RCC_LSEConfig(uint8_t RCC_LSE);
// 启用或禁用内部低速时钟LSI。外部时钟LSE不起振时可以使用LSI当备选条件。40KHz
void RCC_LSICmd(FunctionalState NewState);
// 选择RTCCLK的时钟源
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
// 使能RTCCLK
void RCC_RTCCLKCmd(FunctionalState NewState);
void RCC_GetClocksFreq(RCC_ClocksTypeDef* RCC_Clocks);
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);

#ifdef STM32F10X_CL
void RCC_AHBPeriphResetCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
#endif /* STM32F10X_CL */ 

void RCC_APB2PeriphResetCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphResetCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_BackupResetCmd(FunctionalState NewState);
void RCC_ClockSecuritySystemCmd(FunctionalState NewState);
void RCC_MCOConfig(uint8_t RCC_MCO);
// 获取RCC相关标志位
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
// 清除RCC相关标志位
void RCC_ClearFlag(void);
// 获取RCC相关中断标志位
ITStatus RCC_GetITStatus(uint8_t RCC_IT);
// 清除RCC相关中断标志位
void RCC_ClearITPendingBit(uint8_t RCC_IT);

案例

读写BKP备份寄存器

接线图

image

使用到的函数

// 写指定的BKP数据寄存器
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);

// 读指定的BKP数据寄存器
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

// 启用或禁用对RTC和备份寄存器的访问。该函数在PWR.h中
void PWR_BackupAccessCmd(FunctionalState NewState);

示例代码

#include "stm32f10x.h"                  // Device header
#include "OLED.h"

int main()
{
	OLED_Init();
	
	// 开启BKP和PWR的时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	
	// 开启对RTC和备份寄存器的访问权限。该函数属于PWR
	PWR_BackupAccessCmd(ENABLE);
	
	// 写BKP备份寄存器DR1
	BKP_WriteBackupRegister(BKP_DR1, 0x7788);
	
	// 读BKP备份寄存器DR1,并用OLED显示出来
	OLED_ShowHexNum(1,1,BKP_ReadBackupRegister(BKP_DR1),4);
	while(1)
	{
		
	}
}

实时时钟

接线图

image

用到的库函数

// 获取RTC_CNT寄存器的值。RTC_CNT用来存储时间戳
uint32_t  RTC_GetCounter(void);

// 设置RTC_CNT寄存器的值。RTC_CNT用来存储时间戳
void RTC_SetCounter(uint32_t CounterValue);

// 设置RTC_PRL寄存器的值。RTC_PRL存储对RTCCLK的预分频值,包含0
void RTC_SetPrescaler(uint32_t PrescalerValue);

// 等待RTC寄存器最后一次写操作完成。每次写RTC的寄存器后都需要调用一下该函数,或者写寄存器前调用也可以。对应RTC操作注意事项的第四条
void RTC_WaitForLastTask(void);

// 等待RTC寄存器同步完成。在STM32每次上电后调用一下即可。对应RTC操作注意事项的第二条
void RTC_WaitForSynchro(void);

// 配置外部低速时钟LSE。需要等待RCC_FLAG_LSERDY标志位置1后才启动成功
void RCC_LSEConfig(uint8_t RCC_LSE);

// 选择RTCCLK的时钟源
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);

// 使能RTCCLK
void RCC_RTCCLKCmd(FunctionalState NewState);

// 获取RCC相关标志位
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);

示例代码

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include <time.h>

// 存放时间戳
time_t time_cnt;
// 时间结构体
struct tm time_date;
// 年月日字符串
char Year_To_Date[11];
// 时分秒字符串
char Hour_Min_Sec[9];

void RTC_Init()
{
	// 开启BKP和PWR的时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	
	// 开启RTC和备份寄存器的访问权限
	PWR_BackupAccessCmd(ENABLE);
	
	// 检测备份寄存器的值是否复位,如果没有复位说明备用电源有电,RTC还在走时,就不需要初始化RTC
	if(BKP_ReadBackupRegister(BKP_DR1) == 0x0000)
	{
		
		
		// 开启LSE外部低速时钟
		RCC_LSEConfig(RCC_LSE_ON);
		// 等待RCC_FLAG_LSERDY标志位置1,LSE才开启成功
		while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
		
		// RTCCLK选择LSE作为时钟源
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
		// 使能RTCCLK
		RCC_RTCCLKCmd(ENABLE);
		
		// 等待RTC寄存器同步完成
		RTC_WaitForSynchro();
		// 等待写操作完成
		RTC_WaitForLastTask();
		
		// 设置预分频值。LSE的时钟频率为32.768KHz,经过32768分频刚好为1Hz
		RTC_SetPrescaler(32768 - 1);
		// 等待写操作完成
		RTC_WaitForLastTask();
		
		// 写入时间戳。写RTC_CNT寄存器
		RTC_SetCounter(1672588795);		// 该时间戳对应北京时间2023-1-1 23:59:55
		// 等待写操作完成
		RTC_WaitForLastTask();
		
		// 随便给BKP的DR1备份寄存器一个值,用于检测主电源断电后再上电时备用电源是否掉电
		BKP_WriteBackupRegister(BKP_DR1, 0x7788);
	}
	// RTC不需要初始化,但是STM32上电后最好还是等待RTC寄存器同步
	else
	{
		// 等待RTC寄存器同步完成
		RTC_WaitForSynchro();
		// 等待写操作完成
		RTC_WaitForLastTask();
	}
}

// 获取当前年月日的时间字符串
void RTC_Get_YearToDay()
{
	// 读取RTC_CNT获取时间戳。因为STM32无法判断处于那个时区,所以localtime和gmtime的返回值都是伦敦时间,所以需要加上8小时的偏移
	time_cnt = RTC_GetCounter() + (8*60*60);
	// 将时间戳转化为结构体类型
	time_date = *localtime(&time_cnt);
	// 使用自定义显示时间格式的函数
	strftime(Year_To_Date, 11, "%Y/%m/%d", &time_date);
}

// 获取当前时分秒的时间字符串
void RTC_Get_HourMinSec()
{
	// 读取RTC_CNT获取时间戳。因为STM32无法判断处于那个时区,所以localtime和gmtime的返回值都是伦敦时间,所以需要加上8小时的偏移
	time_cnt = RTC_GetCounter() + (8*60*60);
	// 将时间戳转化为结构体类型
	time_date = *localtime(&time_cnt);
	// 使用自定义显示时间格式的函数
	strftime(Hour_Min_Sec, 9, "%H:%M:%S", &time_date);
}

int main()
{
	OLED_Init();
	RTC_Init();
	while(1)
	{
		RTC_Get_YearToDay();
		RTC_Get_HourMinSec();
		OLED_ShowString(1,1,Year_To_Date);
		OLED_ShowString(2,1,Hour_Min_Sec);
		OLED_ShowNum(3,1,RTC_GetCounter(),10);
	}
}

标签:RTC,void,BKP,寄存器,RCC,时钟
From: https://www.cnblogs.com/liuhousheng/p/18050777

相关文章

  • Modbus字节序说明-汇川PLC用littly endine byte swap【低位优先传输且反序】 解析寄存
    Modbus字节序说明-汇川PLC用littlyendinebyteswap解析寄存器最近做ModBusTCP方面的测试有点多,尽管对于ModBus协议算是比较了解了,也经常知道字节传输序列的不同对工程师带来了很多不必要的麻烦,这不是一个技术难题,仅仅只是过去各家各户开发遗留下来的标准统一问题,所以这里写下......
  • 基于STM32F407MAC与DP83848实现以太网通讯二(DP83848硬件配置以及寄存器)
    参考内容:DP83848数据表一、PHYDP83848功能模块图                     DP83848的硬件模块主要为:MII/RMII/SNI INTERFACES:用于与MAC数据传输的MII/RMII/SNI接口Transmit BLOCK:数据发送模块,将从外部MAC(例如STM32ETH外设的MAC)接收......
  • webrtc终极版(三)将官方的demo部署到自己的服务器中
    webrtc终极版(三)将官方的demo部署到自己的服务器中本节,我们详细介绍下,如何再本地搭建RTCMultiConnection服务目录webrtc终极版(三)将官方的demo部署到自己的服务器中前言一、安装步骤1.下载并解压文件2.使用npm安装总结前言webrtc终极版系列,再年前,写了前两篇,还剩下两篇没有写,......
  • RTC命令行操作
    RTCOperationDocumenthelpmetodoRTCoperationusehelpcommandtogetjazzhelpdocumentonwindowspowershellorcmdbatch.scmhelpscmhelp{OPERATION}scmhelpcreatescmhelp--allLoadOperationCreateStreamscmcreatestream-r{Link}-u{US......
  • webrtc终极版(二)搭建自己的iceserver服务,并用到RTCMultiConnection的demo中
    webrtc终极版(二)搭建自己的iceserver服务,并用到RTCMultiConnection的demo中目录webrtc终极版(二)搭建自己的iceserver服务,并用到RTCMultiConnection的demo中前言一、stunserver,turnserver,iceserver是什么?二、具体搭建步骤1.下载安装coturn2、处理证书问题3、处理各个ip以及端口的配......
  • webrtc终极版(题外话)辛苦写文章分享,竟然遇到喷子狂喷,写篇文章回怼下,顺便发表下面对喷子
    webrtc终极版(题外话)辛苦写文章分享,竟然遇到喷子狂喷,写篇文章回怼下,顺便发表下面对喷子的处理方式第一篇文章发过后,出人意料的是,收到了博客园某一位用户的狂喷【注:本系列文章会同步发布到csdn、博客园、稀土掘金等平台上】,如下图示图片可能不清楚,我再把这位喷子的原话粘贴下来:......
  • restartcodesys.timer
    restartcodesys.timer lckfb@linux:~$lckfb@linux:~$cat/usr/lib/systemd/system/restartcodesys.timer[Unit]Description=RestartCodesysTimer[Timer]OnUnitInactiveSec=110minOnBootSec=3minPersistent=true[Install]WantedBy=timers.targetlckfb@linu......
  • webrtc终极版(一)5分钟搭建多人实时会议系统webrtc
    webrtc终极版(一),支持https,在线部署【不是本地demo】,采用RTCMultiConnection官方demo,5分钟即可搭建成功@目录webrtc终极版(一),支持https,在线部署【不是本地demo】,采用RTCMultiConnection官方demo,5分钟即可搭建成功前言一、webrtc是什么?二、搭建demo步骤1.代码内容2.运行效果总结前......
  • 从yank put看vim寄存器
    寄存器在软件开发过程中,Ctrl-C和Ctrl-V是程序员的核心技能,这就不可避免的涉及到复制,粘贴,删除。在windows环境下,大家习惯了只有一个系统剪切板,复制之后直接粘贴还是比较方便。在vim环境下,有更多的寄存器可以选择:26个字母(大小写分别对应不同用途),还有0——9共10个数字对应的寄存器......
  • __iomem读写寄存器偏移是4 unsigned long 读写寄存器偏移是1
     __iomem读写寄存器偏移是4:   unsignedlong读写寄存器偏移是1: ......