19-BKP备份寄存器与RTC实时时钟.md 16 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(扩展篇)V1.0.0 — BKP/RTC章节 + 配套代码21~26" author: "尚硅谷研究院" date: 2026-07-16

created: 2026-07-15

BKP备份寄存器与RTC实时时钟

用生活理解:BKP 就像一块随身带的防水笔记本——即使主人在游泳池里(主电源断电),笔记本也不会泡坏(VBAT 电池供电)。RTC 就像手表上的秒针——一直在走(32.768KHz 晶振驱动),永远不会停。两个配合使用,可以做到断电后时间不丢失、数据不丢失。


备份域(Backup Domain)

备份域是 STM32 中一个独立的供电区域,由 VBAT 引脚供电。

VDD(3.3V) ────┐
               ├──→ 选择器 ──→ 备份域
VBAT(电池) ────┘
  • VDD 正常时:备份域由 VDD 供电
  • VDD 断电时:备份域自动切换到 VBAT(外接电池)
  • LSE 晶振(32.768KHz)也属于备份域——RTC 靠它走时

参考:参考手册 §6(BKP 寄存器)、§17(RTC 寄存器) 参考:原理图 VBAT 电路

备份域包含的内容

模块 说明
BKP 寄存器 42 个 16 位寄存器(共 84 字节)
RTC 实时时钟(日历/闹钟)
LSE 振荡器 32.768KHz 晶振
PC13~PC15 备份域 I/O(由 BKP 控制)

BKP 备份寄存器

BKP(Backup)寄存器共 42 个 16 位寄存器(总容量 84 字节),可在系统复位/待机/主电源断电时保持内容。

实验 1:寄存器版 BKP

项目路径stm32/21_bkp_test_register

文件:stm32/21_bkp_test_register/Hardware/BKP/bkp.h

#ifndef __BKP_H
#define __BKP_H

#include "stm32f10x.h"

void BKP_Init(void);

#endif

文件:stm32/21_bkp_test_register/Hardware/BKP/bkp.c

#include "bkp.h"

void BKP_Init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;    // 开启 PWR 时钟
    PWR->CR |= PWR_CR_DBP;                 // 解锁备份域写保护
    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;    // 开启 BKP 时钟
}

文件:stm32/21_bkp_test_register/User/main.c

#include "usart.h"
#include "delay.h"
#include "key.h"
#include "bkp.h"

int main(void)
{
    USART_Init();
    KEY_Init();
    BKP_Init();

    printf("尚硅谷备份寄存器实验...\n");

    // BKP->DR1 = 9999;   // 向备份寄存器写入数据

    while (1) { }
}

实验 2:HAL 库版 BKP

项目路径stm32/22_bkp_test_hal

文件:stm32/22_bkp_test_hal/Core/Src/main.c

#include "main.h"
#include "rtc.h"
#include "usart.h"
#include "gpio.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_RTC_Init();
    MX_USART1_UART_Init();

    printf("尚硅谷备份寄存器实验...\n");

    // HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 6666);   // HAL 库写 BKP

    while (1) { }
}

// 按键中断读取 BKP 数据
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == KEY_Pin)
    {
        HAL_Delay(100);
        if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET)
        {
            printf("DR1 = %d\n", HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1));
        }
    }
}

防侵入功能(TAMPER)

BKP 支持防侵入检测——当 TAMPER 引脚被触发时,自动清除所有 BKP 寄存器内容(防止篡改)。

BKP->CR |= BKP_CR_TPE;    // TPE=1: 使能防侵入
// 当 PC13 检测到上升沿时 → 自动清除 BKP 寄存器 → 产生中断

RTC 实时时钟

RTC(Real-Time Clock)提供精确的日历/时钟功能,具有闹钟中断、秒中断等功能。

RTC 功能框图

时钟源选择 → RTC 预分频器 → 32位计数器 → 比较器 → 闹钟中断
                ↓                              ↓
              1Hz 基准时钟                  秒中断

RTC 时钟源选择

时钟源 频率 精度 特点
LSE 32.768KHz 推荐,需外部晶振
LSI ~40KHz 差(±10%) 不需要外部元件
HSE 8MHz(/128=62.5KHz) 需要外部晶振

推荐 LSE:32.768KHz 经过 32768 分频可精确获得 1Hz,最适合 RTC。

RTC 预分频器

RTC 预分频器由 20 位组成(7 位异步 + 13 位同步),STM32F1 的 RTC 只使用一个 16 位 PRL 寄存器(低 13 位有效)。

// LSE = 32.768KHz 时,PRL = 32767 → 1Hz
RTC->PRLH = 0;
RTC->PRLL = 0x7fff;        // 32767

RTC 寄存器

寄存器 说明
RTC_CRH 控制寄存器高(OWIE 溢出中断、ALRIE 闹钟中断、SECIE 秒中断)
RTC_CRL 控制寄存器低(CNF 配置模式、RSF 寄存器同步、SECF/RTOFF)
RTC_PRL 预分频装载寄存器(RTC_PRLL 低 16 位)
RTC_DIV 余数寄存器(当前分频计数值)
RTC_CNT 32 位计数器(秒数)
RTC_ALR 闹钟寄存器(与 CNT 匹配时触发闹钟中断)

注意:RTC 寄存器通过 APB1 接口访问,但 APB1 频率可能低于 RTC 频率,因此读取前需等待 RSF 标志同步。


RTC 实验

实验 3:寄存器版 RTC 日历

项目路径stm32/25_rtc_calendar_register

文件:stm32/25_rtc_calendar_register/Hardware/RTC/rtc.h

#ifndef __RTC_H
#define __RTC_H

#include "stm32f10x.h"
#include <time.h>

typedef struct
{
    uint16_t year;
    uint8_t month;
    uint8_t day;
    uint8_t hour;
    uint8_t minute;
    uint8_t second;
} DateTime;

void RTC_Init(void);
void RTC_SetAlarm(uint32_t s);
void RTC_SetTimestamp(uint32_t ts);
void RTC_GetDateTime(DateTime * dateTime);

#endif

文件:stm32/25_rtc_calendar_register/Hardware/RTC/rtc.c

#include "rtc.h"

void RTC_Init(void)
{
    // 1. 开启 PWR 时钟 + 解锁备份域
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    PWR->CR |= PWR_CR_DBP;

    // 2. 使能 LSE → 等待就绪 → 选择 RTC 时钟源
    RCC->BDCR |= RCC_BDCR_RTCEN;
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) { }
    RCC->BDCR &= ~RCC_BDCR_RTCSEL;
    RCC->BDCR |= RCC_BDCR_RTCSEL_0;

    // 3. 等待 RTOFF → 进入配置模式 → 设 PRL=32767 → 退出配置
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
    RTC->CRL |= RTC_CRL_CNF;
    RTC->PRLH = 0;
    RTC->PRLL = 0x7fff;
    RTC->CRL &= ~RTC_CRL_CNF;
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
}

void RTC_SetAlarm(uint32_t s)
{
    RTC->CRL &= ~RTC_CRL_ALRF;
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
    RTC->CRL |= RTC_CRL_CNF;
    RTC->CNTH = 0;
    RTC->CNTL = 0;
    s -= 1;
    RTC->ALRH = (s >> 16) & 0xffff;
    RTC->ALRL = (s >> 0) & 0xffff;
    RTC->CRL &= ~RTC_CRL_CNF;
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
}

void RTC_SetTimestamp(uint32_t ts)
{
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
    RTC->CRL |= RTC_CRL_CNF;
    RTC->CNTH = (ts >> 16) & 0xffff;
    RTC->CNTL = (ts >> 0) & 0xffff;
    RTC->CRL &= ~RTC_CRL_CNF;
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
}

void RTC_GetDateTime(DateTime *dateTime)
{
    while (!(RTC->CRL & RTC_CRL_RSF)) { }

    uint32_t second = RTC->CNTH << 16 | RTC->CNTL;
    struct tm* ptm = localtime(&second);

    dateTime->year   = ptm->tm_year + 1900;
    dateTime->month  = ptm->tm_mon + 1;
    dateTime->day    = ptm->tm_mday;
    dateTime->hour   = ptm->tm_hour;
    dateTime->minute = ptm->tm_min;
    dateTime->second = ptm->tm_sec;
}

文件:stm32/25_rtc_calendar_register/User/main.c

#include "usart.h"
#include "delay.h"
#include "rtc.h"

int main(void)
{
    USART_Init();
    RTC_Init();

    printf("尚硅谷RTC实验:RTC实时时钟...\n");

    // RTC_SetTimestamp(1736160789);   // 设置一次初始时间戳

    DateTime dateTime;

    while (1)
    {
        RTC_GetDateTime(&dateTime);
        printf("%04d年%02d月%02d日 %02d:%02d:%02d\n",
            dateTime.year, dateTime.month, dateTime.day,
            dateTime.hour, dateTime.minute, dateTime.second);
        Delay_ms(1000);
    }
}

实验 4:HAL 库版 RTC 日历

项目路径stm32/26_rtc_calendar_hal

文件:stm32/26_rtc_calendar_hal/Core/Src/main.c

#include "main.h"
#include "rtc.h"
#include "usart.h"
#include "gpio.h"

void read_stored_date(RTC_HandleTypeDef * hrtc);
void write_stored_date(RTC_HandleTypeDef * hrtc);

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_RTC_Init();
    MX_USART1_UART_Init();

    printf("尚硅谷RTC实验:RTC实时时钟...\n");

    RTC_DateTypeDef date;
    RTC_TimeTypeDef time;

    while (1)
    {
        // 从 BKP 寄存器恢复保存的日期
        read_stored_date(&hrtc);

        HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BIN);
        HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN);

        // 将当前日期写入 BKP 寄存器
        write_stored_date(&hrtc);

        printf("20%02d年%02d月%02d日 %02d:%02d:%02d\n",
            date.Year, date.Month, date.Date,
            time.Hours, time.Minutes, time.Seconds);

        HAL_Delay(1000);
    }
}

void read_stored_date(RTC_HandleTypeDef * hrtc)
{
    if (HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR1) == 0)
        return;

    hrtc->DateToUpdate.Year    = HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR2);
    hrtc->DateToUpdate.Month   = HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR3);
    hrtc->DateToUpdate.Date    = HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR4);
    hrtc->DateToUpdate.WeekDay = HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR5);
}

void write_stored_date(RTC_HandleTypeDef * hrtc)
{
    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, 1);
    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR2, hrtc->DateToUpdate.Year);
    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR3, hrtc->DateToUpdate.Month);
    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR4, hrtc->DateToUpdate.Date);
    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR5, hrtc->DateToUpdate.WeekDay);
}

实验 5:寄存器版 RTC 闹钟唤醒待机

项目路径stm32/23_rtc_alarm_standby_register

文件:stm32/23_rtc_alarm_standby_register/Hardware/RTC/rtc.h

#ifndef __RTC_H
#define __RTC_H

#include "stm32f10x.h"

void RTC_Init(void);
void RTC_SetAlarm(uint32_t s);

#endif

文件:stm32/23_rtc_alarm_standby_register/Hardware/RTC/rtc.c(初始化和闹钟设置)

#include "rtc.h"

void RTC_Init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    PWR->CR |= PWR_CR_DBP;

    // 复位备份域寄存器(与日历版不同:此处主动复位)
    RCC->BDCR |= RCC_BDCR_BDRST;
    RCC->BDCR &= ~RCC_BDCR_BDRST;

    RCC->BDCR |= RCC_BDCR_RTCEN;
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) { }
    RCC->BDCR &= ~RCC_BDCR_RTCSEL;
    RCC->BDCR |= RCC_BDCR_RTCSEL_0;

    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
    RTC->CRL |= RTC_CRL_CNF;
    RTC->PRLH = 0;
    RTC->PRLL = 0x7fff;
    RTC->CRL &= ~RTC_CRL_CNF;
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
}

void RTC_SetAlarm(uint32_t s)
{
    RTC->CRL &= ~RTC_CRL_ALRF;
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
    RTC->CRL |= RTC_CRL_CNF;
    RTC->CNTH = 0;
    RTC->CNTL = 0;
    s -= 1;
    RTC->ALRH = (s >> 16) & 0xffff;
    RTC->ALRL = (s >> 0) & 0xffff;
    RTC->CRL &= ~RTC_CRL_CNF;
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
}

文件:stm32/23_rtc_alarm_standby_register/User/main.c

#include "usart.h"
#include "delay.h"
#include "led.h"
#include "rtc.h"

void enter_standby_mode(void);

int main(void)
{
    USART_Init();
    LED_Init();
    RTC_Init();

    if (PWR->CSR & PWR_CSR_SBF)
    {
        printf("从待机模式唤醒:\n");
        PWR->CR |= PWR_CR_CSBF;
    }
    if (PWR->CSR & PWR_CSR_WUF)
    {
        printf("产生了唤醒事件:\n");
        PWR->CR |= PWR_CR_CWUF;
    }

    printf("尚硅谷RTC实验:闹钟唤醒待机模式...\n");

    LED_On(LED_2);
    Delay_s(2);

    while (1)
    {
        printf("正常运行执行完毕,3s后进入待机模式...\n");
        Delay_s(3);
        printf("进入待机模式,5s后闹钟唤醒...\n");
        Delay_ms(1);

        RTC_SetAlarm(5);   // 设置 5 秒后闹钟
        enter_standby_mode();

        printf("从待机模式唤醒...\n");
        Delay_s(2);
    }
}

void enter_standby_mode(void)
{
    SCB->SCR |= SCB_SCR_SLEEPDEEP;
    PWR->CR |= PWR_CR_PDDS;       // 待机模式
    __WFI();
}

实验 6:HAL 库版 RTC 闹钟唤醒待机

项目路径stm32/24_rtc_alarm_standby_hal

文件:stm32/24_rtc_alarm_standby_hal/Core/Src/main.c

#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "rtc.h"

uint8_t ch;

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    MX_RTC_Init();

    if (__HAL_PWR_GET_FLAG(PWR_FLAG_SB))
    {
        printf("从待机模式唤醒:\n");
        __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);
    }
    if (__HAL_PWR_GET_FLAG(PWR_FLAG_WU))
    {
        printf("产生了唤醒事件:\n");
        __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
    }

    printf("尚硅谷低功耗实验:待机模式...\n");

    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
    HAL_Delay(2000);

    while (1)
    {
        printf("正常运行执行完毕,3s后进入待机模式...\n");
        HAL_Delay(3000);
        printf("进入待机模式,等待5s后RTC闹钟唤醒...\n");

        // 使用自定义 RTC 闹钟函数(与寄存器版共用逻辑)
        RTC_SetAlarm(5);

        HAL_PWR_EnterSTANDBYMode();

        printf("从待机模式唤醒...\n");
        HAL_Delay(2000);
    }
}

HAL 库版 RTC/BKP 速查

// CubeMX 生成 RTC 配置:LSE 时钟源,启用日历
RTC_HandleTypeDef hrtc;

RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};

sTime.Hours = 12;
sTime.Minutes = 0;
sTime.Seconds = 0;
sTime.TimeFormat = RTC_HOURFORMAT_24;
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);

sDate.Year = 26;
sDate.Month = 7;
sDate.Date = 15;
sDate.WeekDay = RTC_WEEKDAY_WEDNESDAY;
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);    // 必须先读时间再读日期!
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

uint16_t data = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x55AA);

核心速查表

操作 寄存器版 HAL 库
解锁备份域 `PWR->CR = PWR_CR_DBP`
写 BKP 寄存器 BKP->DR[i] = data HAL_RTCEx_BKUPWrite()
读 BKP 寄存器 data = BKP->DR[i] HAL_RTCEx_BKUPRead()
设置时间 RTC_SetTimestamp(UNIX时间戳) HAL_RTC_SetTime() + SetDate()
读取时间 RTC_GetDateTime(&dt) → DateTime 结构体 HAL_RTC_GetTime() + GetDate()
设置闹钟 RTC_SetAlarm(s) → CNT=0, ALR=s-1 自定义 RTC_SetAlarm()
配置 LSE RCC->BDCR 逐位操作 CubeMX 自动配置
写注意事项 需 CNF 模式 + 等待 RTOFF HAL 库自动处理

常见问题与避坑

  1. BKP 写入不成功 → 检查 PWR_CR_DBP 是否置 1(必须解锁备份域)
  2. RTC 不走时 → 检查 LSE 是否起振(LSERDY 标志)、RTCEN 是否使能
  3. RTC 读值不对 → 读取前等待 RSF 同步(APB1 比 RTC 慢时)
  4. VDD 断电 RTC 停走 → 检查 VBAT 引脚是否接电池或 VDD
  5. 写 RTC 后系统挂死 → RTC 写入需等待 RTOFF=1(未完成前不可再次写入)
  6. HAL 读 RTC 顺序 → 必须先读 GetTime() 再读 GetDate(),否则日期可能不刷新
  7. 闹钟唤醒待机无需使能 EWUP → RTC 闹钟可以直接从待机模式唤醒,不需要配置 PA0 WKUP