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 个关键寄存器:
-
RTC_CNT 计数寄存器
32 位寄存器,秒计数器,直接对应 Unix 时间戳
读 = 当前时间戳,写 = 设置初始时间。 -
RTC_PRL 预分频寄存器
配置分频值,固定写入 32767,得到 1 秒计时。 -
RTC_CR 控制寄存器
开启中断、闹钟、秒触发等功能。 -
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。
存哪些数据
-
存 W25Q64 的写指针
每次往 W25Q64 里存完东西,就把对应地址写进 BKP 里。这样下次再打开时直接读取 BKP 中的地址,就能定位到该数据在 W25Q64 中的位置。 -
存系统配置参数
比如传感器的采样间隔、加速度计和陀螺仪的量程、报警阈值等。这样每次上电后就不需要重新设置了。 -
存上次掉电和上电的时间戳
这样就能记录中间断电了多久。 -
标记是否是第一次上电
第一次上电的时候,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 读写备份寄存器
接线图:
点击查看代码
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
接线图:
点击查看代码
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);
}
}

浙公网安备 33010602011771号