--- 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`** ```c #ifndef __BKP_H #define __BKP_H #include "stm32f10x.h" void BKP_Init(void); #endif ``` **文件:`stm32/21_bkp_test_register/Hardware/BKP/bkp.c`** ```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`** ```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`** ```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 寄存器内容(防止篡改)。 ```c 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 位有效)。 ```c // 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`** ```c #ifndef __RTC_H #define __RTC_H #include "stm32f10x.h" #include 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`** ```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`** ```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`** ```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`** ```c #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`**(初始化和闹钟设置) ```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`** ```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`** ```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 速查 ```c // 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` | CubeMX 自动处理 | | 写 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