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


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

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 字节),可在系统复位/待机/主电源断电时保持内容。

数据存储(写入 BKP 寄存器)

// 写 BKP 前必须使能对备份域的写访问
// PWR_CR 的 DBP 位控制备份域的写保护
void BKP_Write(uint16_t reg_index, uint16_t data)
{
    // 1. 使能对备份寄存器和 RTC 的访问
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;      // 开启 PWR 时钟
    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;      // 开启 BKP 时钟

    // 2. 解锁备份域写保护
    PWR->CR |= PWR_CR_DBP;                   // DBP=1: 允许写备份域

    // 3. 写入备份寄存器(BKP_DR1~BKP_DR42)
    // BKP 寄存器地址偏移: 0x04 + (reg_index-1) * 4
    BKP->DR[reg_index - 1] = data;           // 写入

    // 4. 重新加锁(可选的,安全考虑)
    // PWR->CR &= ~PWR_CR_DBP;
}

uint16_t BKP_Read(uint16_t reg_index)
{
    // 读取不需要 DBP 解锁
    return BKP->DR[reg_index - 1];
}

防侵入功能(TAMPER)

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

// 配置 TAMPER 引脚(PC13)为防侵入模式
BKP->CR |= BKP_CR_TPE;        // TPE=1: 使能防侵入
// 当 PC13 检测到上升沿时 → 自动清除 BKP 寄存器 → 产生中断

RTC 实时时钟

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

RTC 功能框图

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

RTC 时钟源选择

RTC 有三个时钟源可选:

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

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

RTC 预分频器

RTC 预分频器由两部分组成:

// 异步预分频(7 位,最大 128 分频)
// 同步预分频(15 位,最大 32768 分频)
// 总预分频 = (ASYNC_PREDIV + 1) × (SYNC_PREDIV + 1)

// LSE = 32.768KHz 时:
RTC->PRLH = 0x0000;            // 异步预分频高(很少用)
RTC->PRLL = 127;               // 异步 = 127+1 = 128
RTC->DIVH = 0x0000;            // 同步预分频高(很少用)
RTC->DIVL = 255;               // 同步 = 255+1 = 256
// 总 = 128 × 256 = 32768 → 1Hz 基准时钟

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 读写流程

// RTC 写入流程(RTC 寄存器属于备份域,写入需特殊时序)
void RTC_SetCounter(uint32_t count)
{
    // 1. 电源和时钟使能
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
    PWR->CR |= PWR_CR_DBP;                // 允许写备份域

    // 2. RTC 配置模式
    RTC->CRL |= RTC_CRL_CNF;              // CNF=1: 进入配置模式

    // 3. 写入新值
    RTC->CNTH = count >> 16;              // 高 16 位
    RTC->CNTL = count & 0xFFFF;           // 低 16 位

    // 4. 退出配置模式
    RTC->CRL &= ~RTC_CRL_CNF;             // CNF=0

    // 5. 等待写入完成
    while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {}  // RTOFF=1 才完成
}

uint32_t RTC_GetCounter(void)
{
    // 读取 RTC 不必进配置模式,但需要等待同步
    // 等待 RSF 标志
    RTC->CRL |= RTC_CRL_RSF;
    while ((RTC->CRL & RTC_CRL_RSF) == 0) {}

    // 读取计数器
    uint16_t high = RTC->CNTH;
    uint16_t low  = RTC->CNTL;

    // 如果高 16 位在读低 16 位时改变了(进位),重新读
    if (high != RTC->CNTH)
    {
        high = RTC->CNTH;
        low  = RTC->CNTL;
    }

    return ((uint32_t)high << 16) | low;
}

Unix 时间戳转换

RTC 计数器存储的是从指定基准时间(如 1970-01-01)开始的秒数(Unix 时间戳)。

// Unix 时间戳 → 年月日时分秒(标准库 time.h 处理)
#include <time.h>

time_t raw_time = RTC_GetCounter();     // 读取 RTC 秒数
struct tm *info = localtime(&raw_time);  // 转换为日期时间

printf("%04d-%02d-%02d %02d:%02d:%02d\r\n",
    info->tm_year + 1900,                // 年份
    info->tm_mon + 1,                    // 月份 (0~11)
    info->tm_mday,                       // 日
    info->tm_hour,                       // 时
    info->tm_min,                        // 分
    info->tm_sec);                       // 秒

// 反向:年月日→Unix 时间戳
// 使用 mktime() 函数

实验:BKP 数据存储 + RTC 日历

项目路径stm32/21_bkp_test_register ~ stm32/26_rtc_calendar_hal

// RTC + BKP 初始化
void RTC_BKP_Init(void)
{
    // 1. 开启 PWR 和 BKP 时钟
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
    PWR->CR |= PWR_CR_DBP;                 // 解锁备份域

    // 2. 如果 LSE 未启动,初始化 RTC
    if ((RCC->BDCR & RCC_BDCR_RTCEN) == 0)
    {
        // 复位备份域
        RCC->BDCR |= RCC_BDCR_BDRST;
        RCC->BDCR &= ~RCC_BDCR_BDRST;

        // 配置 LSE 并等待就绪
        RCC->BDCR |= RCC_BDCR_LSEON;
        while (!(RCC->BDCR & RCC_BDCR_LSERDY));

        // 选择 LSE 为 RTC 时钟源
        RCC->BDCR |= RCC_BDCR_RTCSEL_0;    // RTCSEL=01: LSE

        // 配置 RTC 预分频器
        RTC->CRL |= RTC_CRL_CNF;            // 进入配置
        RTC->PRLL = 32768 - 1;              // 32.768KHz → 1Hz
        RTC->CRL &= ~RTC_CRL_CNF;           // 退出配置
        while (!(RTC->CRL & RTC_CRL_RTOFF));

        // 使能 RTC
        RCC->BDCR |= RCC_BDCR_RTCEN;

        // 设置初始时间(示例:2026-01-01 00:00:00)
        RTC_SetCounter(1767225600);          // Unix 时间戳
    }
}

HAL 库版 RTC/BKP

HAL 库使用 RTC_HandleTypeDef 管理 RTC,提供 SetTime/GetTime/SetAlarm 等高级 API。

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

// HAL 库设置时间(使用 RTC_TimeTypeDef / RTC_DateTypeDef 结构体)
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};

sTime.Hours = 12;
sTime.Minutes = 0;
sTime.Seconds = 0;
sTime.TimeFormat = RTC_HOURFORMAT_24;  // 24 小时制
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);   // 设置时间

sDate.Year = 26;    // 2026 年
sDate.Month = 7;
sDate.Date = 15;
sDate.WeekDay = RTC_WEEKDAY_WEDNESDAY;
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);   // 设置日期

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

printf("%04d-%02d-%02d %02d:%02d:%02d\r\n",
       2000 + sDate.Year, sDate.Month, sDate.Date,
       sTime.Hours, sTime.Minutes, sTime.Seconds);

// HAL 库读 BKP 寄存器
uint16_t data = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);

// HAL 库写 BKP 寄存器
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_SetCounter(秒数) HAL_RTC_SetTime() + SetDate()
读取时间 RTC_GetCounter() → Unix 转换 HAL_RTC_GetTime() + GetDate()
配置 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. Unix 时间戳时区问题 → localtime() 使用系统时区,跨时区应用用 gmtime()