STM32单片机学习(36) —— RTC

文章目录

RTC简介

实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作将使能对后备寄存器和RTC的访问:

  • 设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
  • 设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。

主要特性

在分析主要特性之前我们要先回顾以下,时钟树,只看这下半部分就可以

可以看到RTC可由HSE、LSE和LSI三个时钟源提供时钟。显然LSE只为RTC提供时钟,所以LSE是我们的首选,如果LSE不起振,那么可以选择LSI或HSE,当然在这两者之间肯定也是优先选择LSI。

在简介中我们得知,RTC是一个独立计数器,并且可以提供时钟日历的功能。在这里我们需要先了解一个概念------Unix时间戳

Unix时间戳

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

  • 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
  • 世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当

下面是RTC的内部框图

由此可以看到,中间部分,RTC内部只有一个32位的计数器,没有其他寄存器,意味着日期时间是我们同各国时间戳进行转化。所以他只能存储一个时间戳。那么在这里我们就需要考虑预分频器了。

我们使用RTC_CNT计时,那么肯定就是需要一秒钟加1,这里就需要1HZ的频率。所以在这里我们分频系数要足够大。如果使用LSE作为时钟源,那么我们就要分频系数为32768 - 1。如果是HSE提供的好就需要更高的预分频系数。

所以分频系数最高为 2 20 2^{20} 220。

  • 2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟频率必须小于PCLK1时钟频率的四分之一以上。这个是厂商的设计,不需要我们关心。但是也容易理解,因为两个时钟源的频率不一致,如果CPU要访问RTC的话,就需要跨时域进行访问,那么要保证这两部分的同步问题,才能正常访问,所以在手册中有了硬性的规定)

  • 2个独立的复位类型

    • APB1接口由系统复位
    • RTC核心(预分频器、闹钟、计数器、和分频器)只能由后备域复位(具体看参考手册RCC章节)
  • 3个专门的可屏蔽中断

    • 闹钟中断------用来产生一个软件可编程的闹钟

    • 秒中断------用来产生一个可编程的周期性中断信号(最长可达1s)

    • 溢出中断,指内部可编程计数器溢出并回转为0的状态

      前两个中断可以很好的帮助我们完成一些定时任务,比如每隔一段时间进行一次传感器数据采集,或者通过对时间戳和日期时间进行相互转换,可以设置更长时间的定时任务。但是在在这里需要声明一下,闹钟只会执行一次,如果需要例如一天执行一次的话,那就需要在处理完当次的中断后,再设置一个闹钟中断。

      关于溢出中断,我们基本不需要操心,基本上不会出现,计数器满至少距离我们还很遥远,如果是有符号数,好像是在2038年会出现溢出问题,到时候一些老旧设备可能会因为这个出现一系列问题。但是在MCU的场景下存储的是无符号,可能还有个几十年上百年的时间吧。

复位过程

除了RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器外,所有的系统寄存器都由系统复位或电源复位进行异步复位。RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器仅能通过备份域复位信号复位

读RTC寄存器

RTC核完全独立于RTC APB1接口。

软件通过APB1接口访问RTC的预分频值、计数器值和闹钟值。但是,相关的可读寄存器只在与RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。RTC标志也是如此的。这意味着,如果APB1接口曾经被关闭,而读操作又是在刚刚重新开启APB1之后,则在第一次的内部寄存器更新之前,从APB1上读出的RTC寄存器数值可能被破坏了(通常读到0)。下述几种情况下能够发生这种情形:

  • 发生系统复位或电源复位
  • 系统刚从待机模式唤醒
  • 系统刚从停机模式唤醒

所有以上情况中,APB1接口被禁止时(复位、无时钟或断电)RTC核仍保持运行状态。因此,若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置'1'。

在这里我不需要我们手动操作寄存器,SPL库中提供了相关函数,包括手册中提到的一些其他寄存器操作问题,也都给我们封装好了函数,我们只需要调用即可,这部分的操作还是比较简单的。

配置RTC寄存器

必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。另外,对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是'1'时,才可以写入RTC寄存器。

配置过程:

  1. 查询RTOFF位,直到RTOFF的值变为'1'

  2. 置CNF值为1,进入配置模式

  3. 对一个或多个RTC寄存器进行写操作

  4. 清除CNF标志位,退出配置模式

  5. 查询RTOFF,直至RTOFF位变为'1'以确认写操作已经完成。

仅当CNF标志位被清除时,写操作才能进行,这个过程至少需要3个RTCCLK周期。

实验:输出日期和时间

我们已经了解了RTC内部存储了一个时间戳,并且在学习C语言的时候,了解过一个头文件<time.h>。这个头文件中实际上就是通过时间戳进行日期转换的

c 复制代码
#include <time.h>

int main(){
    time_t t = time(NULL);
}

相信在学习C语言阶段,如果使用随机数的话,需要一个种子,一般都是通过time(NULL)来完成的。这个函数实际上就是一个时间戳。当然在该库中存在如何将时间戳转换成时间日期的。所以我们可以读取RTC的时间戳,然后通过time.h 中的相关函数输出一个时间戳。当然我们只做简单的演示,具体time.h中都有什么样的玩法,还有待大家去发掘,网上搜一搜或查一下creference手册就清晰了。

代码

这里就只写RTC的代码了,其他的我们在之前的文章中都是经常用到的。

c 复制代码
// RTC.h
#ifndef __RTC_H
#define __RTC_H
#include "stm32f10x.h"
void init_RTC(void);

#endif
c 复制代码
// RTC.c
#include "RTC.h"


void init_RTC(void){
	// 开启时钟(这三步是手册中明确规定的)
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
	PWR_BackupAccessCmd(ENABLE);
	// 开启LSE时钟
	RCC_LSEConfig(RCC_LSE_ON);
	// 等待时钟开启
	while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
	// RTC时钟源选择
	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
	// 使能
	RCC_RTCCLKCmd(ENABLE);
	// 等待同步
	RTC_WaitForSynchro();
	// 等待上次写完成
	RTC_WaitForLastTask();
	// 配置预分频器
	RTC_SetPrescaler(32768 - 1);
	// 等待写完成
	RTC_WaitForLastTask();
	// 写入时间戳
	RTC_SetCounter(1767196790);
	RTC_WaitForLastTask();
}
c 复制代码
// main.c
#include "stm32f10x.h"
#include "OLED.h"
#include "Delay.h"
#include "../RTC.h"
#include "USART1.h"
#include <time.h>
#include <string.h>
#include <stdio.h>

char buffer[24];
typedef struct TD{
	uint8_t mon;
	uint8_t day;
	char time[10];
	uint16_t year;
	//char week[8];
}td;

int main(){
	init_USART1();
	init_RTC();
	time_t tim;
	while(1){
		tim = RTC_GetCounter() + 8 * 60 * 60; // 默认是伦敦时间,转换成北京时间要加8小时
		USART1_SendStr(asctime(localtime(&tim)));
		Delay_S(1);
	}
}

主程序代码逻辑并不复杂。里边可能出现了一些其他的声明等,这部分是我之前用OLED屏幕显示设置的。在gitee仓库的代码中保留了OLED显示的逻辑。因为在博客中截屏更方便,所以我改用了USART,所以对于那些变量忽视即可。

运行结果

补充

有我们的程序现象可知,我们需要自己传入一个时间戳,程序才能帮我们解析出时间。所以在我们写程序的角度来讲,基本上不可能写出来搞好与现实时间完全对应的程序。在这个小实验中我们只是学习演示出来就可以了。如果在我们做项目的时候,我们需要一个较为精确的时间怎么办呢。我的建议时,加一个时间校准的机制,可以手动传输,也可以通过其他网络设备定时发送校验。

相关推荐
森利威尔电子-1 小时前
森利威尔 SL3042 | 9V-120V 宽压输入 1.25-50V 可调输出 峰值 10A 电源芯片
单片机·嵌入式硬件·电源芯片·降压恒压芯片
Kobebryant-Manba1 小时前
学习语言模型
人工智能·学习·语言模型
憧憬成为web高手3 小时前
[HITCON 2017]SSRFme
学习
妖精的羽翼3 小时前
AI + 前端、可视化 & 大屏
学习
xuhaoyu_cpp_java10 小时前
项目学习(三)分页查询
java·经验分享·笔记·学习
Szime11 小时前
高速 ADC 国产替代选型:通信、雷达、仪器仪表项目要看哪些参数?
单片机·嵌入式硬件·fpga开发
小宋加油啊12 小时前
机械臂抓取物体 PVN3D算法调研学习
学习·算法·3d
灯琰112 小时前
# STM32L051K6U6 IAP Bootloader 开发踩坑实录
stm32
Xzh042313 小时前
AI Agent 学习路线(Java 后端方向)
java·人工智能·学习