STM32——Uinx时间戳+BKP+RTC实时时钟

目录

一、Uinx时间戳

1.1Uinx简介

1.2UTC/GMT

1.3时间戳转换

1.3.1主要数据类型

1.3.2主要函数

1.3.3C语言时间戳转换示例

1.3.4时间格式化说明符

1.3.5注意事项

二、BKP

2.1BKP简介

2.2BKP基本结构

三、RTC

3.1RTC简介

3.2RTC框图

3.3RTC基本结构

3.4RTC硬件电路

3.5RTC操作注意事项

3.6学习中的疑惑与解答:PWR和RTC、BKP之间有什么关系?

3.6.1核心关系一句话总结

[3.6.2. 三个模块各自的角色](#3.6.2. 三个模块各自的角色)

3.6.3.它们为什么会联系在一起?

3.6.4总结与类比

四、BKP读写代码编写

4.1读写BKP步骤

4.2BKP相关库函数与PWR与之对应的库函数

4.3读写BKP代码编写

4.3.1读写数组BKP

五、RTC代码编写

5.1RTC初始化

5.2RTC相关库函数

5.3RTC初始化代码编写

5.4显示时间戳代码

5.5读写设置的时间

5.6初始化RTC优化

5.7显示余数寄存器的数值


一、Uinx时间戳

1.1Uinx简介

•Unix 时间戳(Unix Timestamp)定义为从UTC/GMT(伦敦时间)的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒

•时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量

•世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间

1.2UTC/GMT

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

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

1.3时间戳转换

C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换

1.3.1主要数据类型

  • time_t :通常是一个整数类型,用于存储时间戳

  • struct tm:用于存储日期和时间成分的结构体

  • clock_t **:**用于测量处理器时间的类型

1.3.2主要函数

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

1.3.3C语言时间戳转换示例

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

int main() 
{
    printf("========== 时间戳转换示例 ==========\n\n");

    // 获取当前时间戳
    time_t current_time = time(NULL);
    printf("1. 当前时间戳: %ld\n", current_time);
    printf("   (从1970年1月1日00:00:00 UTC开始的秒数)\n\n");

    // 将时间戳转换为本地时间结构体
    struct tm* local_time = localtime(&current_time);
    printf("2. 本地时间分解:\n");
    printf("   年份: %d (从1900年开始计算,实际年份=%d)\n",local_time->tm_year, local_time->tm_year + 1900);
    printf("   月份: %d (0-11, 0=1月,实际月份=%d)\n",local_time->tm_mon, local_time->tm_mon + 1);
    printf("   日: %d\n", local_time->tm_mday);
    printf("   时: %d\n", local_time->tm_hour);
    printf("   分: %d\n", local_time->tm_min);
    printf("   秒: %d\n", local_time->tm_sec);
    printf("   星期: %d (0-6, 0=周日)\n", local_time->tm_wday);
    printf("   年中的第几天: %d\n\n", local_time->tm_yday);

    // 将时间戳转换为UTC时间结构体
    struct tm* utc_time = gmtime(&current_time);
    printf("3. UTC时间分解:\n");
    printf("   年份: %d (实际年份=%d)\n",utc_time->tm_year, utc_time->tm_year + 1900);
    printf("   月份: %d (实际月份=%d)\n",utc_time->tm_mon, utc_time->tm_mon + 1);
    printf("   日: %d\n", utc_time->tm_mday);
    printf("   时: %d\n", utc_time->tm_hour);
    printf("   分: %d\n", utc_time->tm_min);
    printf("   秒: %d\n\n", utc_time->tm_sec);


    // 使用strftime格式化时间输出
    char formatted_time[100];
    strftime(formatted_time, sizeof(formatted_time),"4. 格式化时间: %Y年%m月%d日 %H时%M分%S秒", local_time);
    printf("%s\n", formatted_time);

    strftime(formatted_time, sizeof(formatted_time),"5. 另一种格式: %A, %B %d, %Y %I:%M:%S %p", local_time);
    printf("%s\n\n", formatted_time);

    // 将struct tm转换回时间戳
    time_t converted_time = mktime(local_time);
    printf("6. 转换回的时间戳: %ld\n", converted_time);

    printf("========== 程序结束 ==========\n");

    return 0;
}

#char* ctime(const time_t*);这个函数VS显示函数不安全,我就没展示#

cpp 复制代码
// 将时间戳转换为可读字符串
    printf("可读时间字符串: %s\n", ctime(&current_time));

1.3.4时间格式化说明符

strftime函数中常用的格式说明符

  • %Y:4位数的年份

  • %y:2位数的年份

  • %m:月份(01-12)

  • %d:日(01-31)

  • %H:24小时制的小时(00-23)

  • %I:12小时制的小时(01-12)

  • %M:分钟(00-59)

  • %S:秒(00-59)

  • %p:AM/PM指示

  • %A:完整的星期名称

  • %B:完整的月份名称

1.3.5注意事项

  1. struct tm中的年份是从1900年开始计算的,所以需要加1900

  2. struct tm中的月份是从0开始计数的(0=1月,11=12月),所以要加1,显示目前月份

  3. mktime()函数会自动调整struct tm中的超出范围的值(如将60秒调整为下一分钟)

  4. 时区转换需要注意使用localtime()(本地时间)或gmtime()(UTC时间)

二、BKP

2.1BKP简介

•BKP(Backup Registers)备份寄存器

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

RTC和BKP共享同一个备用电源域。这意味着,只要你的板子上有后备电池,无论是RTC的计时,还是BKP中存储的数据,在主板断电后都不会丢失。

若VDD和VBAT都断电,则BKP内数据丢失(BKP本质是RAM存储器)

•TAMPER引脚(引脚2 默认复用功能 安全保障设计)产生的侵入事件将所有备份寄存器内容清除

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

•存储RTC时钟校准寄存器

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

2.2BKP基本结构

图中橙色部分为后备区域,BKP和RTC处于后备区域,其特性是:VDD主电源掉电时,后备区域仍可以由VBAT的备用电池供电;VDD主电源上电时,后备区域供电会由VBAT切换到VDD(节省电池电量)

BKP含有:数据寄存器、控制寄存器、状态寄存器、RTC校准寄存器

a) 数据寄存器(BKP_DRx - Data Register)

这是BKP的核心,用于存储用户数据。不同型号的STM32,数据寄存器的数量不同

  • 命名BKP_DR1, BKP_DR2, BKP_DR3, ... BKP_DRn

  • 宽度 :每个寄存器都是 16位 宽,可以存储2个字节,小/中容量有10个寄存器,大容量/互联型设备有42个数据寄存器。

  • 功能:你可以用它们存储任何你想在断电后保留的数据,例如:

    • RTC配置状态标志

    • 系统配置参数

    • 运行日志或错误代码

    • 校准值


b) 控制与状态寄存器(BKP_CR / BKP_CSR)

这些寄存器用于管理BKP模块本身的功能,例如:

  • TAMPER引脚检测 :许多STM32有一个防篡改(Tamper)引脚。当这个引脚上有指定电平跳变时,它可以自动擦除整个BKP区域的数据(出于安全考虑)。相关寄存器用于配置检测边沿和使能该功能。

  • RTC校准:有些寄存器位用于对RTC时钟进行精细的软件校准。

三、RTC

3.1RTC简介

•RTC(Real Time Clock)实时时钟

•RTC是一个独立的定时器,可为系统提供时钟和日历的功能

•RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时

•32位的可编程计数器,可对应Unix时间戳的秒计数器

•20位的可编程预分频器,可适配不同频率的输入时钟

•可选择三种RTC时钟源:

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

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

LSI振荡器时钟(40KHz)

3.2RTC框图

3.3RTC基本结构

①RTCCLK时钟来源,RCC内配置(数据选择器黄色部分,选择时钟来源)

时钟源选择器 (RTCSEL)

  • 框图入口。RTC可以选择三个时钟源之一:

    • LSE首选。外部低速晶振,精度高。

    • LSI:内部低速RC振荡器,成本低但精度较差,受温度影响大。

    • HSE/128:高速外部时钟分频后的信号,精度高但耗电,主电源掉电后无法使用。

  • 通过 RCC_BDCR 寄存器中的 RTCSEL 位进行选择。这个选择是一次性的,只有在备份域复位后才能重新设置。

②预分频器:余数寄存器------自减计数器,存储当前计数值;重装寄存器------计数目标,决定分频值,之后得到1Hz秒计数信号(配置重装寄存器选择分频系数)

预分频器 (Prescaler)

  • 作用 :将输入的高速时钟(如32.768kHz)分频,得到精确的1Hz(每秒一次)信号。

  • 组成 :通常由一个20位的异步预分频器 和一个7位的同步预分频器组成。异步分频器降低时钟频率以降低功耗,同步分频器保证与APB1时钟域的无风险交互。

  • 计算 :例如,使用LSE(32768 Hz)时,通常设置异步分频器为127,同步分频器为255,则最终频率为: 32768 / (127 + 1) / (255 + 1) = 1 Hz

③计数器:32位计数器------1s自增一次;32位闹钟值------设定闹钟(配置32位寄存器,进行日期时间的读写)

32位可编程计数器 (RTC_CNT)

  • 这是RTC的核心。它是一个由预分频器输出的1Hz信号驱动的32位向上计数器。

  • 它存储的值就是从参考起点开始所经过的秒数。如果我们把参考起点设置为Unix纪元(1970-01-01 00:00:00),那么这个计数器的值就是当前的Unix时间戳。

  • 软件可以直接读写这个计数器来设置或获取时间。

闹钟寄存器 (RTC_ALR)

  • 一个32位的比较寄存器。你可以设置一个想要的秒数目标值。

  • RTC_CNT 的值与RTC_ALR 的值匹配时,就会产生一个闹钟中断。你可以利用这个中断让单片机在特定时间执行某些任务(比如唤醒停止模式)。

④三个信号触发中断:秒信号、计数器溢出信号、闹钟信号,通过中断输出控制,进行中断使能

⑤使能的中断,通向NVIC,向CPU申请中断(需要中断,先允许中断,再配置NVIC,再写对应的中断函数)

控制与状态寄存器

  • 用于管理RTC的各种功能,如:

    • 使能寄存器 (RTC_CRH/CRL):允许配置和产生秒中断、闹钟中断等。

    • 状态寄存器:用于查看各种中断标志位。

3.4RTC硬件电路

a) 主电源 (VDD)

  • 作用:为STM32芯片的主核心(包括RTC模块的逻辑部分)供电。

  • 要求:当主电源存在时,RTC由VDD供电。

b) 备用电池 (VBAT)

  • 作用 :这是RTC的"生命线"。当主电源VDD断开时,自动切换由VBAT引脚上的电池为整个备份域(包括RTC计数器和BKP寄存器)供电,保持计时和数据不丢失。

  • 典型选择:一颗3V的纽扣电池(如CR2032)。其续航能力可达数年。

  • 电路设计 :通常会在VBAT路径上串联一个肖特基二极管 ,并在VDD路径上也串联一个二极管。这样可以实现电源自动切换:当VDD电压高于VBAT时,由VDD供电;当VDD掉电时,由VBAT供电,防止电流倒灌。或者直接连接一个1.8~3.6V的电池到VBAT,内有一个供电开关,在不同情况选择合适供电。

c) 低速外部晶振 (LSE)

  • 作用:为RTC提供高精度的时钟源。RTC的本质是一个计数器,它需要一個"心跳"来递增,LSE就是这个"心跳"。

  • 频率32.768 kHz 。这是一个标准频率,因为 32768 = 2^15,经过一个15位的分频器(2^15分频)后,正好得到 1Hz(每秒一次)的信号,非常适合驱动秒计数器。

  • 电路设计:需要连接一个32.768kHz的晶振(Xtal),并搭配两个负载电容(通常为5~15pF)。

3.5RTC操作注意事项

•执行以下操作将使能对BKP和RTC的访问:

设置RCC_APB1ENR(开启APB1外设的时钟)的PWREN和BKPEN,使能PWR和BKP时钟

设置PWR_CR的DBP,使能对BKP和RTC的访问(调用PWR库函数)

•若在读取RTC寄存器时,RTC的APB1 接口曾经处于禁止状态 ,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置 1。(调用等待同步函数)

  • 根本原因:时钟域不同。RTC核心由低速的LSE(32.768kHz)驱动,而CPU和其寄存器接口由高速的APB1总线时钟驱动。这两个时钟域是异步的。

  • 问题所在 :当APB1接口被禁用(例如系统刚复位、或从低功耗模式唤醒)后重新启用 时,RTC的日历寄存器组(RTC_CNT, RTC_ALR, RTC_PRL)需要从RTC的时钟域同步到APB1的时钟域。在这个同步过程完成之前,软件读取到的寄存器值可能是陈旧、不可靠的。

  • RSF位的作用RSF (Register Synchronized Flag) 位由硬件自动置1,标志着同步过程已完成。软件必须等待此位被置1后,才能安全地读取日历寄存器,以确保数据的正确性。

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

  • 保证写的原子性RTC_PRL(预分频器)、RTC_CNT(计数器)和RTC_ALR(闹钟值)这三个寄存器是密切相关的。为了防止在软件修改其中一个寄存器时,RTC核心正在更新另一个寄存器而导致数据不一致(例如,在修改秒计数器的高16位时,低16位可能因为秒信号到来而递增),STM32引入了"配置模式"的概念。

  • CNF位的作用 :将 CNF (Configuration Flag) 位置1,会使RTC核心暂停对日历寄存器的更新,并允许软件一次性、安全地写入多个相关寄存器。退出配置模式后,RTC核心会使用新值继续运行。

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

  • 根本原因:慢速外设。RTC的时钟源非常慢(RTCCLK~32kHz),而CPU的指令执行速度极快(PCLK1~72MHz)。向RTC寄存器写入一个数据需要数个RTC时钟周期才能完成。

  • 问题所在:如果软件在前一次写操作尚未被RTC硬件处理完成时,就立刻发起下一次写操作,会导致后一次写操作被忽略,或者造成寄存器内容错误。

  • RTOFF位的作用RTOFF (RTC Operation OFF) 位由硬件控制。当它为 0 时,表示RTC正在处理上一次的写操作,此时总线会被锁定,禁止新的写入。当它为 1 时,表示RTC空闲,允许新的写操作。

3.6学习中的疑惑与解答:PWR和RTC、BKP之间有什么关系?

3.6.1核心关系一句话总结

PWR(电源控制)是基础和保障,而RTC(实时时钟)和BKP(备份寄存器)是需要被保护的核心数据。PWR确保了即使在主电源(VDD)断开的情况下,也能通过备用电池(VBAT)为RTC和BKP所在的"备份域"持续供电,从而保住这些关键数据不丢失。


3.6.2. 三个模块各自的角色

a) PWR - 电源控制

  • 职责 :管理整个微控制器的电源,包括供电、功耗模式(如睡眠、停止、待机模式)以及电源域的划分。

  • 关键概念 :STM32内部有两个主要的电源域

    • VDD域:由主电源(VDD)供电。绝大部分外设(如GPIO、USART、SPI)和内核(CPU)、SRAM都在这个域。当主电源断开时,这个域的内容会丢失。

    • 备份域 :由一个单独的引脚VBAT供电。即使主电源VDD断开,只要VBAT引脚上接有电池(如3V纽扣电池),这个域就能继续工作。

b) BKP - 备份寄存器

  • 职责 :提供一小块在备份域中的特殊内存(通常是16-20个16位的寄存器)。

  • 关键特性只要备份域有电(无论是来自VDD还是VBAT),这些寄存器里的数据就不会丢失。因此,它们常用来存储系统配置、密码、运行次数等需要掉电保存的关键数据。

  • 访问控制 :为了防止意外写入,对BKP寄存器的访问受到PWR_CR 寄存器中的DBP 位控制。只有先使能DBP位,才能读写BKP寄存器。

c) RTC - 实时时钟

  • 职责:提供一个独立的计时器/日历功能。

  • 关键特性 :RTC的核心也是一个需要持续运行的部件。因此,它也被放在了备份域中。这样,无论主系统是否上电,只要VBAT有电,RTC就能一直"滴答滴答"地走时。

3.6.3.它们为什么会联系在一起?

因为RTC和BKP同属于"备份域"

PWR模块,是进入和管理这个"备份域"的"看门人"和"电源开关"

当你学习RTC和BKP时,你必然要操作备份域里的资源。而要安全地操作这些资源,就必须通过PWR模块来进行配置。这就是为什么在讲BKP/RTC的章节,会突然引入PWR。

它们的关系结构图如下:

从这个图可以看出:

  1. 物理供电:PWR模块负责选择是使用VDD还是VBAT为备份域供电。

  2. 逻辑访问 :PWR模块中的DBP位是读写BKP和RTC部分寄存器的"钥匙"。

3.6.4总结与类比

你可以把一个带STM32的设备想象成一个公司

  • BKP :是公司的保险柜,里面放着最重要的现金和合同(关键数据)。

  • RTC :是公司墙上那个永远不停的电子钟,即使公司下班断电了,它靠自己的电池也能走(备份域供电)。

  • PWR :是公司的物业和电力部门 。它管理着整个大楼的供电(主电源和备用发电机),并且掌握着打开保险柜房间的钥匙(DBP位)。

  • Tamper引脚 :是保险柜上的震动警报器 。一旦有人动保险柜(触发入侵),警报器就会通知电力部门(PWR),电力部门会启动应急 protocol(唤醒系统),并且警报器会指令保险柜自毁(擦除BKP数据)。

四、BKP读写代码编写

4.1读写BKP步骤

①BKP初始化:开启PWREN和BKPEN的时钟;使用PWR_CR的一个函数,使能BKP和RTC的访问
②写DR:BKP写函数
③读DR:BKP读函数

4.2BKP相关库函数与PWR与之对应的库函数

1.恢复缺省配置(手动清空BKP所有的数据寄存器)

void BKP_DeInit(void);


下面2个TAMPER函数是配置TAMPER侵入检测功能
2.配置TAMPER引脚的有效电平(高/低电平触发)

void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);
3.TAMPER使能(是否开启侵入检测功能:若需要此项功能,需要先配置有效电平在配置使能)

void BKP_TamperPinCmd(FunctionalState NewState);


4.中断配置:是否开启中断

void BKP_ITConfig(FunctionalState NewState);
5.时钟输出功能配置(可选择在RTC引脚输出时钟信号,输出RTC校准时钟、RTC闹钟脉冲、秒脉冲)

void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);
6.设置RTC校准值(写入RTC校准寄存器)

void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);


⭐下面2个是BKP的重点函数
7.写备份寄存器

void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
8.读备份寄存器

uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

下面4个是获取标志和清除标志的函数

FlagStatus BKP_GetFlagStatus(void);

void BKP_ClearFlag(void);

ITStatus BKP_GetITStatus(void);

void BKP_ClearITPendingBit(void);


PWR与BKP相关的库函数:
⭐1.备份寄存器访问使能,设置PWR_CR寄存器内的DBP位,使能对BKP和RTC的访问

void PWR_BackupAccessCmd(FunctionalState NewState);

4.3读写BKP代码编写

cpp 复制代码
	//①使能BKP和PWR
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	
	//②使能对BKP和RTC的访问
	PWR_BackupAccessCmd(ENABLE);
	
	BKP_WriteBackupRegister(BKP_DR1,0x1234);
	
	OLED_ShowHexNum(1,1,BKP_ReadBackupRegister(BKP_DR1),4);
	
	while(1)
	{

	}

4.3.1读写数组BKP

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "key.h"
uint16_t WriteARR[]={0x1234,0x5678};
uint16_t ReadARR[2];
uint8_t key;
void key_pro(void)
{
	key=key_init();
	if(key==1)
	{
		WriteARR[0]++;
		WriteARR[1]++;

		BKP_WriteBackupRegister(BKP_DR1,WriteARR[0]);
		BKP_WriteBackupRegister(BKP_DR2,WriteARR[1]);

		OLED_ShowHexNum(1,3,WriteARR[0],4);
		OLED_ShowHexNum(1,8,WriteARR[1],4);
	}
	
}

int main(void)
{	
    OLED_Init();
	key_scan();//初始化

	//①使能BKP和PWR
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	
	//②使能对BKP和RTC的访问
	PWR_BackupAccessCmd(ENABLE);
	
	
	while(1)
	{
		OLED_ShowString(1,1,"W:");
		OLED_ShowString(2,1,"R:");

		key_pro();
		ReadARR[0]=BKP_ReadBackupRegister(BKP_DR1);
	    ReadARR[1]=BKP_ReadBackupRegister(BKP_DR2);

	    OLED_ShowHexNum(2,3,ReadARR[0],4);
		OLED_ShowHexNum(2,8,ReadARR[1],4);

	}
}

五、RTC代码编写

5.1RTC初始化

RTC初始化流程:
①开启PWR和BKP的时钟
②使能BKP和RTC的访问
③启动RTC时钟,选择LSE作为系统时钟(LES需要手动开启,平时为了省电是关闭状态)(RCC内部)
④配置RTCCLK数据选择器,指定LSE作为RTCCLK(RCC模块内)
⑤调用等待函数:等待同步和等待上一次操作完成
⑥配置预分频器,给PRC重装寄存器一个合适的预分频值,以确保输出给寄存器的频率是1Hz
⑦配置CNT的值,给RTC初始时间
⑧(若需要配置闹钟值)
⑨(若需要中断,配置中断:NVIC)

5.2RTC相关库函数

RCC模块内有关RTC库函数:
⭐1.配置LSE外部低速时钟,启动LSE时钟

void RCC_LSEConfig(uint8_t RCC_LSE);
2.配置LSI内部低速时钟(若外部时钟不起振,可使用内部时钟进行验证)

void RCC_LSICmd(FunctionalState NewState);
⭐3.RTCCLK配置(选择RTCCLK的时钟源)

void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
⭐4.启动RTCCLK(调用配置RTCCLK函数之后,还需使能RTCCLK)

void RCC_RTCCLKCmd(FunctionalState NewState);
⭐5.获取标志位(调用启动时钟后,需等待标志位LSERDY=1,时钟才算启动完成并且工作稳定)

FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);

RTC时钟源初始化库函数使用:1→5→3→4
RTC库函数
1.配置中断输出

void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
2.进入配置模式(置CRL的CNF=1,进入配置模式后,才可读写一些寄存器)

void RTC_EnterConfigMode(void);
3.退出配置模式(CNF=0)

void RTC_ExitConfigMode(void);


4.获取CNT计数器的值(读取时钟)

uint32_t RTC_GetCounter(void);
5.写入CNT计数器的值(设置时间)

void RTC_SetCounter(uint32_t CounterValue);
⭐6.写入预分频器(值写入到预分频器的PRL重装寄存器,配置预分频器的预分频系数)

void RTC_SetPrescaler(uint32_t PrescalerValue);
7.写入闹钟值(配置闹钟)

void RTC_SetAlarm(uint32_t AlarmValue);
8.读取预分频器中的DIV余数寄存器(自减计数器------为了获得更细致的时间)

uint32_t RTC_GetDivider(void);


⭐9.等待上次操作完成(循环,等待RTOFF=1)

void RTC_WaitForLastTask(void);
⭐10.等待同步(清除RSF标志位,循环直到RSF=1)

void RTC_WaitForSynchro(void);

(读取RTC寄存器之前,时钟源选择完毕后,先等待同步10和等待上次操作完成9)

(对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行,即每次进行写操作后都加一个等待上次操作完成函数)


11.获取标志位,清楚标志位的函数

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);

5.3RTC初始化代码编写

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

void MyRTC_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
	
	PWR_BackupAccessCmd(ENABLE);
	
	RCC_LSEConfig(RCC_LSE_ON);//启动LSE振荡器
	/*  
	*     @arg RCC_LSE_OFF: LSE oscillator OFF         LSE振荡器关闭
  *     @arg RCC_LSE_ON: LSE oscillator ON           LSE振荡器开启
  *     @arg RCC_LSE_Bypass: LSE oscillator bypassed with external clock  LSE时钟旁路------不接晶振,直接从OSE32------IN引脚输入一个指定频率的信号
	*/
	while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET);//等待LSE启动完成
	
	
	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//选择RTCCLK时钟源
	RCC_RTCCLKCmd(ENABLE);

	RTC_WaitForSynchro();
	RTC_WaitForLastTask();//⑤调用等待函数:等待同步和等待上一次操作完成
	
	RTC_SetPrescaler(32768-1);//⑥配置预分频器,给PRC重装寄存器一个合适的预分频值,以确保输出给寄存器的频率是1Hz
	RTC_WaitForLastTask();//对RTC的任何一个写操作,必须在前一次写操作结束后进行
//必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
//写入预分频器也需要进入配置模式,但是代码不需要写,内部自带进入退出配置代码
	RTC_SetCounter(1672588795);//⑦配置CNT的值,给RTC初始时间,此为2023年,不初始也可以,默认从0开始,一开始是1970年
	RTC_WaitForLastTask();
	
}

5.4显示时间戳代码

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


int main(void)
{		
    OLED_Init();
	MyRTC_Init();
	while(1)
	{
		OLED_ShowNum(1, 1, RTC_GetCounter(), 10);	//显示32位的秒计数器
	}
}

5.5读写设置的时间

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Time.h"

//存放年月日时分秒
uint16_t MyRTC_Time[]={2024,8,26,10,9,30};

//设置时间
void MyRTC_SetTime(void)
{
	/*
	①将数组时间填充到struct tm 结构体内;
	②使用mktime函数得到秒数
	③将秒数写入RTC的CNT中
	*/
	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;//读取的是伦敦时间的时间戳,-8个小时,就是北京时间的时间戳
	
	RTC_SetCounter(time_cnt);
	RTC_WaitForLastTask();
}

//读取时间
void MyRTC_ReadTime(void)
{
	time_t time_cnt;
	struct tm time_date;
	
	time_cnt=RTC_GetCounter()+8*60*60;//此时计数是伦敦时间,+8个小时,就是北京时间的时间戳
	time_date=*localtime(&time_cnt);
	
	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;
}


void MyRTC_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	
	PWR_BackupAccessCmd(ENABLE);
	
	RCC_LSEConfig(RCC_LSE_ON);//启动LSE振荡器
	while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET);//等待LSE启动完成
	
	
	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//选择RTCCLK时钟源
	RCC_RTCCLKCmd(ENABLE);
	
	RTC_WaitForSynchro();
	RTC_WaitForLastTask();//⑤调用等待函数:等待同步和等待上一次操作完成
		
	RTC_SetPrescaler(32768-1);//⑥配置预分频器,给PRC重装寄存器一个合适的预分频值,以确保输出给寄存器的频率是1Hz
	RTC_WaitForLastTask();//对RTC的任何一个写操作,必须在前一次写操作结束后进行
		
	//必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
	//写入预分频器也需要进入配置模式,但是代码不需要写,内部自带进入退出配置代码
		
	RTC_SetCounter(1672588795);//⑦配置CNT的值,给RTC初始时间,此为2023年,不初始也可以,默认从0开始,一开始是1970年
	RTC_WaitForLastTask();	
	MyRTC_SetTime();
	
}
cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "My_RTC.h"

int main(void)
{	
	
    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 :");
	while(1)
	{
		MyRTC_ReadTime();
		//显示MyRTC_Time数组中的时间值
		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, 9, MyRTC_Time[4], 2);//分
		OLED_ShowNum(2, 12, MyRTC_Time[5], 2);//秒
		OLED_ShowNum(3, 6, RTC_GetCounter(), 10);	//显示32位的秒计数器
	}
}

5.6初始化RTC优化

防止电源断电备用电源不断电或者按复位键时,时间重置

简单来说,就是在BKP设置一个标志位

cpp 复制代码
void MyRTC_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	
	PWR_BackupAccessCmd(ENABLE);
	
	RCC_LSEConfig(RCC_LSE_ON);//启动LSE振荡器
	while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET);//等待LSE启动完成
	
	
	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//选择RTCCLK时钟源
	RCC_RTCCLKCmd(ENABLE);
	
	//第一次上电或者系统完全断电之后,BKP_DR1默认是0,if成立,执行初始化
	if(BKP_ReadBackupRegister(BKP_DR1)!=0xFFFF)
	{
		RTC_WaitForSynchro();
		RTC_WaitForLastTask();//⑤调用等待函数:等待同步和等待上一次操作完成
		
		RTC_SetPrescaler(32768-1);//⑥配置预分频器,给PRC重装寄存器一个合适的预分频值,以确保输出给寄存器的频率是1Hz
		RTC_WaitForLastTask();//对RTC的任何一个写操作,必须在前一次写操作结束后进行
		
		//必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
		//写入预分频器也需要进入配置模式,但是代码不需要写,内部自带进入退出配置代码
		
		RTC_SetCounter(1672588795);//⑦配置CNT的值,给RTC初始时间,此为2023年,不初始也可以,默认从0开始,一开始是1970年
		RTC_WaitForLastTask();	
		MyRTC_SetTime();
		
		BKP_WriteBackupRegister(BKP_DR1,0xFFFF);
	}
	else
	{
		RTC_WaitForSynchro();
		RTC_WaitForLastTask();//⑤调用等待函数:等待同步和等待上一次操作完成
	}
	
}

5.7显示余数寄存器的数值

#之前一直CNT数值不变,但是加上DIV后,数值就自增了,好奇怪,没理解#

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

int main(void)
{	
	
    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(4, 1, "DIV :");
	while(1)
	{
		MyRTC_ReadTime();
		//显示MyRTC_Time数组中的时间值
		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, 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);	//显示余数寄存器的数值
	}
}

DIV数值一直自减,数值范围时32767~0
DIV每自减一轮,CNT+1
因此可以对秒数进行更细致的划分,秒-分秒-厘秒-毫秒

若转换为毫秒,1秒=1000毫秒

将数值范围32767------0线性变换为0------999

OLED_ShowNum(4, 6, (32767-RTC_GetDivider())/32767.0*999, 10); //显示余数寄存器的数值