一、读写备份寄存器
接线图

初始化
使能 PWR 和 BKP 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
使能对备份寄存器的写访问
PWR_BackupAccessCmd(ENABLE);
写入备份寄存器
BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]);
读取备份寄存器
BKP_ReadBackupRegister(BKP_DR1);
完整代码
程序最开始,是初始化和两个用于指示的字符串显示。之后三句是BKP的初始化,然后主循环里循环执行的是,先获取键码,如果按键按下,就变换一下测试数据,把数据分别写入到BKP的DR1和DR2中,再在OLED显示写入的数据。在按键没有按下时,也不断读取BKP的DR1和DR2,刷新显示到OLED上。
#include "stm32f10x.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);
}
}
二、RTC实时时钟
uint16_t MyRTC_Time[] = {2026,3,15,13,46,00};
写入RTC
把自定义的时间数组转换成标准时间戳,写入 RTC 硬件计数器,完成 RTC 时间配置。
struct tm 标准时间结构体

time_t / mktime()
-
time_t:存储Unix 时间戳(从 1970 年 1 月 1 日 0 点开始的总秒数) -
mktime(&time_date):把tm结构体转换成时间戳,是标准库函数
硬件操作逻辑
-
RTC_SetCounter(time_cnt):STM32 的 RTC 本质是一个秒计数器,写入时间戳后,RTC 会每秒自动加 1,实现计时 -
RTC_WaitForLastTask():RTC 寄存器写入需要时间,必须等待操作完成,否则会出现时间设置失败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); RTC_SetCounter(time_cnt); RTC_WaitForLastTask();}
初始化
上电后只初始化一次 RTC,断电后时间不丢失,下次上电直接读取已保存的时间。
1. 为什么要这么初始化
STM32 的RTC + BKP 备份区域 有电池供电(VBAT),主电源断电后,RTC 时间和备份寄存器数据不会丢失。
所以代码做了关键判断:
- 第一次上电:初始化 RTC、设置时间、写入标志
0xA5A5 - 后续重启:检测到
0xA5A5,直接用之前的时间,不重新初始化
2. 关键硬件知识点
- LSE 32.768kHz 晶振RTC 专用低速晶振,功耗极低,电池供电也能运行
- 分频值 32768-1 32768Hz ÷ 32768 = 1Hz → 每秒 RTC 计数器 + 1,精准计时
- BKP 备份寄存器 10 个独立寄存器,断电不丢失,用来存
0xA5A5标志位 - PWR 电源控制负责开启备份区域写权限,不开启无法配置 RTC
3. 必须等待的函数(防止配置失败)
RTC_WaitForSynchro():等待 RTC 时钟同步RTC_WaitForLastTask():等待寄存器写入完成RTC 是低速外设,操作后必须等待,否则会配置失效
4.完整运行流程
- 开启 PWR、BKP 时钟,解除备份区写保护
- 读取 BKP_DR1 判断是否第一次初始化
- 第一次初始化
- 开启 32.768kHz 晶振
- 选择 RTC 时钟源
- 设置分频实现 1 秒计时
- 调用
MyRTC_SetTime()设置初始时间 - 写入
0xA5A5标志
- 非第一次初始化
只做等待同步,直接使用之前保存的时间
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);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_SetPrescaler(32768-1);
RTC_WaitForLastTask();
MyRTC_SetTime();
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else //RTC不是第一次配置
{
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
}
}
读取RTC
从硬件 RTC 中读取时间戳,转换成我们能看懂的年月日时分秒,存到数组里。
1. 执行流程
- 读取硬件 :
RTC_GetCounter()→ 获取 RTC 里的时间戳秒数 - 时间转换 :
localtime()→ 把秒数转成tm结构体时间 - 格式还原 :把
tm结构体 → 还原成你的MyRTC_Time数组
2. 关键函数:localtime()
- 作用:Unix 时间戳 → 年月日时分秒
- 输入:
time_t类型的秒数指针 - 输出:
struct tm结构体指针 - 必须包含头文件:
<time.h>
3. 为什么要 +1900、+1?
和写时间时刚好相反:
-
设置时:
实际年 - 1900→ 存入 tm -
读取时:
tm年 + 1900→ 变回实际年 -
设置时:
实际月 - 1→ 存入 tm -
读取时:
tm月 + 1→ 变回实际月void MyRTC_ReadTime(void)
{
time_t time_cnt;
struct tm time_date;time_cnt = RTC_GetCounter(); 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;}
注:
这里如果使用gmtime()会导致数据出错,可能是库函数或者这个编译器有问题。使用localtime()时偶尔出现不加8的现象,解决方法是固定东八区时区.
// 固定东八区时区,让 localtime 永远稳定 +8小时
extern char **environ;
char *__env[] = {"TZ=UTC-8", NULL};
char **environ = __env;
调用
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:");
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);
}
}