12. RTC实时时钟

一、Unix 时间戳

简单来说,就是从 1970 年 1 月 1 日 0 时 0 分 0 秒 开始经过的时间的总秒数。

在 C 语言中,有很多可以帮我们获取时间戳以及转换的函数,他们存储在头文件 <time.h> 中。
首先我们要认识结构体 rm

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(注意:0代表1月,11代表12月)
    int tm_year;  // 年:从1900年开始的年数(比如2026年就是126)
    int tm_wday;  // 星期:0~6(0代表周日,1代表周一)
    int tm_yday;  // 一年中的第几天:0~365
    int tm_isdst; // 夏令时标志
};

相关函数定义如下表所示:

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

当然,在 STM32 中,我们无法使用 time() 函数读取时间戳,需要从单片机的内部时钟读取。

二、RTC 实时时钟

RTC(Real Time Clock),就是 STM32 内部自带的独立记时模块,通电后可以自动计时,并且在掉电时,能够使用备用电源进行运转。RTC 可通过低速晶振作为外部时钟,这个在单片机内部已经有了,记得开启就好。

关键寄存器

RTC 内部有 4 个关键寄存器:

  1. RTC_CNT 计数寄存器
    32 位寄存器,秒计数器,直接对应 Unix 时间戳
    读 = 当前时间戳,写 = 设置初始时间。

  2. RTC_PRL 预分频寄存器
    配置分频值,固定写入 32767,得到 1 秒计时。

  3. RTC_CR 控制寄存器
    开启中断、闹钟、秒触发等功能。

  4. RTC_FLAG 状态标志位
    同步标志:等待时钟稳定
    操作完成标志:等待寄存器写入完毕

实现代码

初始化:

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) // 如果是第一次上电
	{
		RCC_LSEConfig(RCC_LSE_ON); // 开启低速晶振
		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); // 等待晶振起振稳定
		
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 选择LSE低速晶振作为RTC的时钟源
		RCC_RTCCLKCmd(ENABLE); // 使能RTC
		
		RTC_WaitForSynchro(); // 等待RTC时钟同步完成
		RTC_WaitForLastTask(); // 等待操作完成
		
		RTC_SetPrescaler(32768 - 1); // 预分频器(32768Hz/32768=1Hz,每秒加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; // 年份-1900
	time_date.tm_mon = MyRTC_Time[1] - 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; // mktime转化为时间戳,再减去北京时间(UTC+8)
	
	RTC_SetCounter(time_cnt); // 写入RTC
	RTC_WaitForLastTask();
}

读取时间:

void MyRTC_ReadTime(void)
{
	time_t time_cnt;
	struct tm time_date;
	
	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;
}

三、BKP 备份寄存器

BKP(Backup Registers)就是在 RTC 旁边的“小U盘”,能储存的数据量很少,因此只用来储存关键数据。与 RTC 一样,掉电时能够使用备用电源运行,因此同样有掉电不丢失的性质。
在 STM32F103C8T6 中,有有 42 个 16 位的备份寄存器,编号从 BKP_DR1 到 BKP_DR42。

存哪些数据

  1. 存 W25Q64 的写指针
    每次往 W25Q64 里存完东西,就把对应地址写进 BKP 里。这样下次再打开时直接读取 BKP 中的地址,就能定位到该数据在 W25Q64 中的位置。

  2. 存系统配置参数
    比如传感器的采样间隔、加速度计和陀螺仪的量程、报警阈值等。这样每次上电后就不需要重新设置了。

  3. 存上次掉电和上电的时间戳
    这样就能记录中间断电了多久。

  4. 标记是否是第一次上电
    第一次上电的时候,BKP 里的所有寄存器都是 0xFFFF。我们可以在 BKP 里存一个 "魔术字",比如 0x1234。上电后检查这个魔术字,如果是 0xFFFF,说明是第一次上电,需要初始化所有参数。如果是 0x1234,说明不是第一次上电,直接从 BKP 里加载参数。

如何读写

想要往 BKP 里写东西,首先要开启 PWR 和 BKP 的时钟,并进行使能:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);

PWR_BackupAccessCmd(ENABLE);

读写代码如下:

BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]); // 写入
ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1); // 读取

四、程序示例

例1 读写备份寄存器

接线图:12-1 读写备份寄存器

点击查看代码

main.c

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

uint8_t KeyNum;

uint16_t ArrayWrite[] = {0x1234, 0x5678};
uint16_t ArrayRead[2];

int main(void)
{
	OLED_Init();
	Key_Init();
	
	OLED_ShowString(1, 1, "W:");
	OLED_ShowString(2, 1, "R:");
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); // 开时钟
	
	PWR_BackupAccessCmd(ENABLE); // 开启之后才能往里面写数据
	
	while (1)
	{
		KeyNum = Key_GetNum();
		
		if (KeyNum == 1)
		{
			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);
		}
		
		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);
	}
}

例2

接线图:12-2 实时时钟

点击查看代码

MyRTC.c

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

uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 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) // 如果是第一次上电
	{
		RCC_LSEConfig(RCC_LSE_ON); // 开启低速晶振
		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); // 等待晶振起振稳定
		
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 选择LSE低速晶振作为RTC的时钟源
		RCC_RTCCLKCmd(ENABLE); // 使能RTC
		
		RTC_WaitForSynchro(); // 等待RTC时钟同步完成
		RTC_WaitForLastTask(); // 等待操作完成
		
		RTC_SetPrescaler(32768 - 1); // 预分频器(32768Hz/32768=1Hz,每秒加1)
		RTC_WaitForLastTask(); // 等待操作完成
		
		MyRTC_SetTime(); // 设置初始时间
		
		BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 写入魔术字
	}
	else // 不是第一次上电,不用初始化了
	{
		RTC_WaitForSynchro(); 
		RTC_WaitForLastTask();
	}
}

//如果LSE无法起振导致程序卡死在初始化函数中
//可将初始化函数替换为下述代码,使用LSI当作RTCCLK
//LSI无法由备用电源供电,故主电源掉电时,RTC走时会暂停
/* 
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)
	{
		RCC_LSICmd(ENABLE);
		while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
		
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
		RCC_RTCCLKCmd(ENABLE);
		
		RTC_WaitForSynchro();
		RTC_WaitForLastTask();
		
		RTC_SetPrescaler(40000 - 1);
		RTC_WaitForLastTask();
		
		MyRTC_SetTime();
		
		BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
	}
	else
	{
		RCC_LSICmd(ENABLE);
		while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
		
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
		RCC_RTCCLKCmd(ENABLE);
		
		RTC_WaitForSynchro();
		RTC_WaitForLastTask();
	}
}*/

void MyRTC_SetTime(void)
{
	time_t time_cnt;
	struct tm time_date;
	
	time_date.tm_year = MyRTC_Time[0] - 1900; // 年份-1900
	time_date.tm_mon = MyRTC_Time[1] - 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; // mktime转化为时间戳,再减去北京时间(UTC+8)
	
	RTC_SetCounter(time_cnt); // 写入RTC
	RTC_WaitForLastTask();
}

void MyRTC_ReadTime(void)
{
	time_t time_cnt;
	struct tm time_date;
	
	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;
}

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, 6, RTC_GetCounter(), 10);
		OLED_ShowNum(4, 6, RTC_GetDivider(), 10);
	}
}

posted @ 2026-05-24 15:43  Steven24  阅读(15)  评论(0)    收藏  举报