Unix时间戳
- 什么是Unix时间戳?
①Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒.
②时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量.
③世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间.
GMT/UTC
- 什么是GMT?
GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准. - 什么是UTC?
UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致.
具体时区划分见该视频:时区划分详解
时间戳转换
- C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换,如图:

- 各种时间转换函数关系图:

BKP(备份寄存器)
- 什么是BKP?
①BKP(Backup Registers)备份寄存器.
②BKP可用于存储用户应用程序数据。当VDD(2.0~3.6V) 电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位.
③TAMPER引脚产生的侵入事件将所有备份寄存器内容清除.
④RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲.
⑤存储RTC时钟校准寄存器.
⑥用户数据存储容量:20字节(中容量和小容量)/ 84字节(大容量和互联型).
⑦STM32属于中容量设备,故只有20字节的存储容量可以使用,也只有10个寄存器可以使用(大容量设备有45个寄存器可以使用). - BKP基本结构如图:

RTC(实时时钟)
- 什么是RTC?
①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) - RTC基本结构如图:

- RTC操作注意事项:
①执行以下操作将使能对BKP和RTC的访问:设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟,设置PWR_CR的DBP,使能对BKP和RTC的访问.
②若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1.
③必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器.
④对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器.
RTC与BKP之间的联系
- 共享电源域
①RTC和BKP都属于备份域(Backup Domain).
②由VBAT引脚或VDD供电,即使主电源断开,数据也不会丢失.
③需要使能PWR(电源控制)和BKP时钟才能访问. - 硬件依赖关系
// 必须使能这些时钟才能使用RTC和BKP
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE); // 允许访问备份域
- 应用场景:
VBAT电池
↓
┌─────────────┐
│ 备份域 │ ← 保持RTC时间和BKP数据
│ • RTC计数器│
│ • BKP寄存器│
└─────────────┘
- 总结:
RTC提供时间功能,BKP提供数据存储功能,两者共同构成STM32的备份域系统,确保关键数据(时间信息和配置标志)在断电情况下不会丢失.
读写备份寄存器&实时时钟
读写备份寄存器
- 接线图如下:

- 由于配置代码比较少,所以就不再封装,只在main.c文件中进行初始化和调用
main.c代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
uint16_t ArrayWrite[] = {0x1234,0x5678};
uint16_t ArrayRead[2];
uint8_t KeyNum;
int main(void)
{
OLED_Init();
Key_Init();
OLED_ShowString(1,1,"W:");
OLED_ShowString(2,1,"R:");
//初始化
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
PWR_BackupAccessCmd(ENABLE);
while (1)
{
KeyNum = Key_GetNum();
//KeyNum == 1:按键按下
if(KeyNum == 1)
{
BKP_WriteBackupRegister(BKP_DR1,ArrayWrite[0] ++);
BKP_WriteBackupRegister(BKP_DR2,ArrayWrite[1] ++);
OLED_ShowHexNum(1,3,BKP_ReadBackupRegister(BKP_DR1),4);
OLED_ShowHexNum(1,8,BKP_ReadBackupRegister(BKP_DR2),4);
}
OLED_ShowHexNum(2,3,BKP_ReadBackupRegister(BKP_DR1),4);
OLED_ShowHexNum(2,8,BKP_ReadBackupRegister(BKP_DR2),4);
}
}
值得注意的是:
①备份寄存器BKP和电源控制的PWR是连接在总线APB1的设备.
②PWR_BackupAccessCmd(ENABLE);这句代码是开启对备份寄存器访问的权限(Backup缩写为BKP).
③BKP寄存器与传统的STM32寄存器不同,它是16位的.
实时时钟
- 接线图如下:

- 由于RTC使用的STM32板子上自带的晶振,电路是内部的,不涉及外部设备,所以在System文件夹下新建MyRTC.c文件和MyRTC.h文件.
MyRTC.c代码:
#include "stm32f10x.h" // Device header
#include <time.h>
//定义时间的数组,按照年月日时分秒的的顺序排列:2025-11-19 17:27:55
uint16_t MyRTC_Time[] = {2025,11,19,17,27,55};
void MyRTC_SetTime(void);
void MyRTC_Init(void)
{
//初始化
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
PWR_BackupAccessCmd(ENABLE);
if(BKP_ReadBackupRegister(BKP_DR1) != 0XA5A5)
{
//开启LSE作为时钟来源
RCC_LSEConfig(RCC_LSE_ON);
//等待LSE晶振起振完成
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
//选择时钟源(LSE,LSI,HSI)
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
//上电
RCC_RTCCLKCmd(ENABLE);
//读同步:此函数等待硬件完成同步
RTC_WaitForSynchro();
//写同步:必须等待前一次写操作真正完成后,才能发起下一次写操作,否则会失败
RTC_WaitForLastTask();
//LSE的时钟频率为32768HZ,要想得到RTC需要的1HZ要对其进行32768次分频
RTC_SetPrescaler(32768 - 1);
//每次写操作后都要调用该函数
RTC_WaitForLastTask();
MyRTC_SetTime();
BKP_WriteBackupRegister(BKP_DR1,0XA5A5);
}
else
{
//读同步:此函数等待硬件完成同步
RTC_WaitForSynchro();
//写同步:必须等待前一次写操作真正完成后,才能发起下一次写操作,否则会失败
RTC_WaitForLastTask();
}
}
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];
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;
//将读出的秒数写到RTC寄存器中
RTC_SetCounter(time_cnt);
//每次写操作后都要调用该函数
RTC_WaitForLastTask();
}
void MYRTC_ReadTime(void)
{
time_t time_cnt;
struct tm time_date;
//读取RTC中设置的秒
time_cnt = RTC_GetCounter() + 8*60*60;
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;
}
值得注意的是:
①在初始化函数中有if语句,其作用是为了防止在按复位按钮或者拔掉STM32电源重新插上(没有拔掉BKP的电源),OLED的时间要恢复为原始的值重新计时,当按复位按钮或重新插上STM32电源时,要重新执行初始化函数,当执行到判断语句BKP_ReadBackupRegister(BKP_DR1) != 0XA5A5,会判断BKP的寄存器1中值是否等于0xA5A5,如果STM32是直接断掉所有电源(包括备用电源)然后重新插上重新启动的话,该寄存器1的值会被清零,所以不等于0xA5A5,会执行if内部的代码,重新从设置的初始时间计时,否则会跳出if语句从断电以前的值自增.
②那么为什么if语句的内部要指定DR1寄存器和值0xA5A5呢?
答:事实上,STM32属于中容量设备,备用寄存器一共有10个,指定这其中的任何寄存器都行,因为在拔掉STM32的所有电源线(包括备用电源线)重新插上后,所有的备用寄存器都会清零,这也就说明指定的值除了0x0000以外的任何值也都行.
③在读数据和写数据的函数中 time_date.tm_year加减1900的操作,这是因为在C语言的time.h头文件中,tm结构体的tm_year成员表示的是"自1900年以来的年数".
④在读数据函数有time_cnt = mktime(&time_date) - 86060;,在写数据函数中有time_cnt = RTC_GetCounter() + 86060;,这是因为mktime(&time_date)函数将设置好的年月日转换的时区是伦敦的时区,而不是北京的东八区,这就要我们在在读函数中再加上8小时偏移,写函数减去8小时的偏移量.
⑤在读函数中,对月的操作time_date.tm_mon 的加减1原理与年类似,月的起始是从0,到11结束,加上1符合人类日常习惯.
- 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
值得注意的是:
①对于数组的.h文件声明即使extern关键字依旧可以被外部文件调用.
main.c代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.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();
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,5,RTC_GetCounter(),10);
OLED_ShowNum(4,5,RTC_GetDivider(),10);
}
}
846

被折叠的 条评论
为什么被折叠?



