重要的内容写在前面:
视频链接:[12-1] Unix时间戳_哔哩哔哩_bilibili
1、Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒。
(1)时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量。
(2)世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间。
2、GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统,它将地球自转一周的时间间隔等分为24小时,以此确定计时标准。
3、UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致。
4、C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间(是个结构体,其中的参数为年月日时分秒以及星期几等)和字符串之间的转换。
函数 |
作用 |
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*); |
日期时间转换为字符串(自定义格式) |
5、BKP(Backup Registers)备份寄存器:
(1)BKP可用于存储用户应用程序数据,当VDD(2.0~3.6V)电源被切断,它们仍然由VBAT(1.8~3.6V)维持供电,当系统在待机模式下被唤醒、或系统复位、或电源复位时,它们也不会被复位。
(2)TAMPER引脚产生的侵入事件会将所有备份寄存器的内容清除,同时还会申请中断,在中断函数中可以继续清除其它存储器的数据以及锁死设备,可用于预防数据被窃取等恶性事件。
(3)RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲。
(4)存储RTC时钟校准寄存器。
(5)用户数据存储容量:20字节(中容量和小容量)/ 84字节(大容量和互联型)。
6、BKP基本结构:
7、RTC(Real Time Clock)实时时钟:
(1)RTC是一个独立的定时器,可为系统提供时钟和日历的功能。
(2)RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时。
(3)32位的可编程计数器,可对应Unix时间戳的秒计数器。
(4)20位的可编程预分频器,可适配不同频率的输入时钟。
(5)可选择三种RTC时钟源:
①HSE时钟除以128(通常为8MHz/128)
②LSE振荡器时钟(通常为32.768KHz,一般都选用该时钟作为RTCCLK)
③LSI振荡器时钟(40KHz)
8、RTC框图:
(1)后备区域的电路在主电源掉电后可以使用备用电池维持工作。
(2)RTCCLK是经过选择器输入RTC的时钟,一般选择LSE振荡器时钟,进入RTC的时钟首先由预分频器进行分频。
(3)RTC的预分频器由两个寄存器组成,RTC_PRL是重装载寄存器(决定分频系数),RTC_DIV是余数寄存器(本质上是一个自减计数器),DIV负则对RTCCLK的时钟脉冲进行计数,当自减为0时会输出一个时钟脉冲到TR_CLK上,同时PRL将重装载值写进DIV中,以此往复达到分频的效果。
(4)RTC_CNT就是秒计数器,TR_CLK每有一个时钟脉冲,计数器的值+1,理论上TE_CLK应该配置为每秒产生一个时钟脉冲。
(5)RTC_ALR是闹钟寄存器,它与RTC_CNT等长,用于设置闹钟,用户可以在ALR中写一个时间戳,当ALR与CNT的值相等时会产生RTC_Alarm信号通往右侧的中断系统,同时还可以使STM32退出待机模式。(RTC_ALR的值不会自己改变,需要用户设定)
(6)RTC_Second是秒中断信号,每有一个时钟脉冲就会产生一个秒中断信号;RTC_Overflow是溢出中断信号,不过对于RTC_CNT而言,它存储的是32位的无符号数,到2016年才会发生计数溢出。(SECF、OWF、ALRF是中断标志位,SECIE、OWIE、ALRIE是中断使能位)
(7)读写RTC中的寄存器需要通过APB1总线(RTC是APB1总线上的设备)。
9、硬件电路:
10、BKP和RTC操作的注意事项:
(1)执行以下操作将使能对BKP和RTC的访问:
①设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟。
②设置PWR_CR的DBP,使能对BKP和RTC的访问。(使用PWR_BackupAccessCmd函数)
(2)若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1。
(3)必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。
(4)对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器。
11、读写备份寄存器:
(1)按照下图所示接好电路,并将OLED显示屏的项目文件夹复制一份作为模板使用。
(2)在stm32f10x_bkp.h文件中有BKP相关的函数:
[1]BKP_DeInit函数:恢复缺省配置,可以用于清空BKP的所有数据。
[2]BKP_TamperPinLevelConfig函数:配置TAMPER是高电平触发还是低电平触发(侵入检测功能)。
[3]BKP_TamperPinCmd函数:选择是否开启侵入检测功能。
[4]BKP_ITConfig函数:选择是否开启中断。
[5]BKP_RTCOutputConfig函数:配置时钟输出功能,选择输出的时钟源。
[6]BKP_SetRTCCalibrationValue函数:设置RTC校准值。
[7]BKP_WriteBackupRegister函数:写备份寄存器。
[8]BKP_ReadBackupRegister函数:读备份寄存器。
[9]BKP_GetFlagStatus函数:获取标志位。
[10]BKP_ClearFlag函数:清除标志位。
[11]BKP_GetITStatus函数:在中断函数中获取标志位。
[12]BKP_ClearITPendingBit函数:在中断函数中清除标志位。
(3)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中并进行调试(主要是开发板掉电后VBAT引脚通电与不通电的区别,观察两种情况下BKP中的数据是否会被重置)。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Key.h"
uint16_t ArrayWrite[] = {0x1234, 0x5678};
uint16_t ArrayRead[2];
uint8_t KeyNum;
int main()
{
OLED_Init();
Key_Init();
OLED_ShowString(1, 1, "W:");
OLED_ShowString(2, 1, "R:");
//使能PWR和BKP时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
//使能对BKP和RTC的访问
PWR_BackupAccessCmd(ENABLE);
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1) //按下按键1,ArrayWrite中的数据自增并写入BKP
{
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);
}
//读取BKP中的数据
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);
}
}
12、实时时钟:
(1)按照下图所示接好电路,并将OLED显示屏的项目文件夹复制一份作为模板使用。
(2)在stm32f10x_rcc.h文件中有几个本例相关的函数。
[1]RCC_LSEConfig函数:配置LES外部低速时钟。
[2]RCC_LSICmd函数:配置LSI内部低速时钟。
[3]RCC_RTCCLKConfig函数:配置RTCCLK的数据选择器,选择RTCCLK的时钟源。
[4]RCC_RTCCLKCmd函数:允许RTCCLK选择的时钟进入RTC。
[5]RCC_GetFlagStatus函数:获取标志位。
[6]RCC_ClearFlag函数:清除标志位。
(3)在stm32f10x_rtc.h文件中有RTC模块相关的函数。
[1]RTC_ITConfig函数:配置中断输出。
[2]RTC_EnterConfigMode函数:RTC进入配置模式(置CRL寄存器的CNF位为1)。
[3]RTC_ExitConfigMode函数:RTC退出配置模式(置CRL寄存器的CNF位01)。
[4]RTC_GetCounter函数:读CNT计数器(获取时间)。
[5]RTC_SetCounter函数:写CNT计数器(设置时间)。
[6]RTC_SetPrescaler函数:写预分频器的PRL重装寄存器,也就是设置分频系数。
[7]RTC_SetAlarm函数:写闹钟寄存器。
[8]RTC_GetDivider函数:读预分频器的DIV余数寄存器。
[9]RTC_WaitForLastTask函数:等待上次操作完成(等待RTOFF=1)。
[10]RTC_WaitForSynchro函数:等待同步(等待REF=1)。
[11]RTC_GetFlagStatus函数:获取标志位。
[12]RTC_ClearFlag函数:清除标志位。
[13]RTC_GetITStatus函数:在中断函数中获取标志位。
[14]RTC_ClearITPendingBit函数:在中断函数中清除标志位。
(3)在项目的System组中添加MyRTC.h文件和MyRTC.c文件用于封装RTC模块的代码。
①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
②MyRTC.c文件:
#include "stm32f10x.h" // Device header
#include
uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55}; //2023年1月1日23:59:55
void MyRTC_SetTime(void);
void MyRTC_Init(void)
{
//使能PWR和BKP时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
//使能对BKP和RTC的访问
PWR_BackupAccessCmd(ENABLE);
if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) //RTC只需初始化一遍即可(关机后再开机,计时不会被重置)
{
//开启LSE的时钟
RCC_LSEConfig(RCC_LSE_ON);
//等待LES时钟开启
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
//配置RTCCLK的数据选择器,指定LSE为时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
//允许RTCCLK选择的时钟进入RTC
RCC_RTCCLKCmd(ENABLE);
//等待同步,等待前一次写操作结束
RTC_WaitForLastTask();
RTC_WaitForSynchro();
//配置预分频器,输出1Hz的时钟(RTC_SetPrescaler函数中有使RTC进入配置模式的过程)
RTC_SetPrescaler(32768 - 1); //32.768KHz / 32768 = 1Hz
RTC_WaitForLastTask(); //等待前一次写操作结束
//给RTC一个初始时间
MyRTC_SetTime();
//第一次初始化RTC时给BKP写值,如果程序复位,初始化函数会再执行一遍
//而程序复位时BKP不会被重置,可以依据BKP中的值是否为A5A5判断是否需要初始化RTC
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
RTC_WaitForLastTask();
RTC_WaitForSynchro();
}
}
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;
//日期时间结构体转换为伦敦时间的时间戳,再调整为北京时间的时间戳
//(这步只是为了CNT使用得到北京时间的时间戳,即使使用伦敦的时间戳也不影响最终结果)
RTC_SetCounter(time_cnt); //写CNT(设置时间)
RTC_WaitForLastTask(); //等待前一次写操作结束
}
void MyRTC_ReadTime(void)
{
time_t time_cnt; //秒计数器数据类型
struct tm time_date; //日期时间数据类型
time_cnt = RTC_GetCounter() + 8 * 60 * 60;
//当前时间戳是北京时间的时间戳,进行转换时需要换回伦敦时间的时间戳
//(这步只是为了CNT使用北京时间的时间戳,即使使用伦敦的时间戳也不影响最终结果)
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;
}
(3)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中并进行调试(主要是开发板掉电后VBAT引脚通电与不通电的区别,观察两种情况下RTC的计时是否被重置)。
#include "stm32f10x.h" // Device headerCmd
#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(4, 1, "DIV:");
while(1)
{
MyRTC_ReadTime();
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);
OLED_ShowNum(4,6,RTC_GetDivider(),10);
}
}