后备寄存器-BKP
寄存器实现
HAL实现
源码分析
RTC实时时钟
原理分析
框图
后备域重置
RTC时钟源
示例代码
// 配置RTC内部时钟源为LSE
static void rtc_clksource_config(void) {
// 开启后后备域写保护
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
PWR->CR |= PWR_CR_DBP;
if (BKP->DR2) {
LOG_DEBUG("识别到BKP-DR2标识,跳过RTC时钟源的配置")
return;
}
// 软件重置后备域(重置后才能配置RTC时钟源)
RCC->BDCR |= RCC_BDCR_BDRST;
RCC->BDCR &= ~RCC_BDCR_BDRST;
// 使能LSE
RCC->BDCR |= RCC_BDCR_LSEON;
// 选择LSE作为RTC时钟源
RCC->BDCR |= RCC_BDCR_RTCSEL_LSE;
// 使用后备寄存器DR2标识RTC时钟源已经配置过
BKP->DR2 = 1;
LOG_DEBUG("RTC时钟源已配置位LSE")
}
配置RTC寄存器流程
硬件置位标志位 & 写操作同步
读寄存器同步
- RTC 核心时钟和 APB1 接口时钟是独立的。
- 软件通过 APB1 接口访问 RTC 数据时,实际上读取的是同步后的副本。
- 当 APB1 接口重新启用后,第一次读取 RTC 数据可能出错,因为寄存器副本还未同步完成。
- 软件需要在读取 RTC 数据之前,等待寄存器同步完成(通过检测
RSF
标志)
案例-使用Unix时间戳作为RTC计数值
后备域寄存器访问使能
void BKP_EnableAccess(void) {
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
// 开启后后备域写保护
PWR->CR |= PWR_CR_DBP;
}
RTC时钟源配置(LSE)
/**
* @brief 配置 RTC 的时钟源为 LSE。
* @details 如果后备域已经配置,则直接返回。
* 否则,软件复位后备域,启用 LSE 时钟,并将其配置为 RTC 时钟源。
*/
void RTC_ConfigLSE(void) {
if (!BKP->DR1) {
// 软件重置后备域(重置后才能重新选择RTC时钟源)
RCC->BDCR |= RCC_BDCR_BDRST;
RCC->BDCR &= ~RCC_BDCR_BDRST;
// 使能LSE
RCC->BDCR |= RCC_BDCR_LSEON;
// 选择LSE作为RTC时钟源
RCC->BDCR |= RCC_BDCR_RTCSEL_LSE;
// 设置后备寄存器DR,指示RTC时钟源已经配置过
BKP->DR1 = 1;
LOG_DEBUG("RTC时钟源已配置为LSE")
} else {
LOG_DEBUG("识别到BKP-DR标识,跳过RTC时钟源的配置")
}
}
RTC初始化(写预分频装载值)
void RTC_Init() {
BKP_EnableAccess();
RTC_ConfigLSE();
/* 时钟配置 */
// 等待LSE就绪
while ((RCC->BDCR & RCC_BDCR_LSERDY) == 0) {
}
// 使能RTC-APB1接口时钟
RCC->BDCR |= RCC_BDCR_RTCEN;
/* 参考手册RTC寄存器配置流程 */
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
// 进入RTC配置模式
RTC->CRL |= RTC_CRL_CNF;
// 配置RTC预分频装载值,例如32768(7FFF),分频后得到TR_CLK=1Hz
RTC->PRLH = 0;
RTC->PRLL = 0x7FFF & RTC_PRLL_PRL;
// 退出RTC配置模式
RTC->CRL &= ~RTC_CRL_CNF;
// 退出RTC配置模式后,写操作才会被执行
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
}
设置Unix时间戳(写计数值)
void RTC_SetUnixSecond(uint32_t unixSecond) {
/* 参考手册RTC寄存器配置流程 */
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
// 进入RTC配置模式
RTC->CRL |= RTC_CRL_CNF;
// 配置RTC计数值,例如当前unix时间戳(秒)1734524925
// 等待秒标志位置位后才能配置RTC计数值
while ((RTC->CRL & RTC_CRL_SECF) == 0) {
}
RTC->CNTH = unixSecond >> 16;
RTC->CNTL = unixSecond;
// 退出RTC配置模式
RTC->CRL &= ~RTC_CRL_CNF;
// 退出RTC配置模式后,写操作才会被执行
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
}
RTC 的计数器和分频器在每一秒钟的末尾更新,并通过 SECF
标志告知硬件和软件:
- 当前秒钟周期已经结束,计数器即将被更新到下一秒。
- 在
SECF
置位时,RTC 内部时钟稳定且处于受控状态,允许执行对 RTC 计数器的更新操作。
因此,等待 SECF
被置位确保了:
- RTC 当前的计时周期已经稳定。
- 分频器的状态是同步的(与 1Hz 的时钟频率对齐)。
获取Unix时间戳(读计数值)
uint32_t RTC_GetUnixSecond() {
// 等待寄存器同步
while (!(RTC->CRL & RTC_CRL_RSF)) {
}
// 清除RSF
RTC->CRL &= ~RTC_CRL_RSF;
// 读取计数器中的值
return RTC->CNTH << 16 | RTC->CNTL;
}
测试掉电后RTC仍在运行
先运行一次,设置时间戳;然后注释时间戳的设置,测试重启、断电重新上电后,后备域仍在运行。
int main() {
uart_init();
RTC_Init();
//RTC_SetUnixSecond(1734524925);
LOG_DEBUG("hello world")
while (1) {
LOG_DEBUG("unix sencod = %d", RTC_GetUnixSecond())
delay_ms(1000);
}
}
时间戳解析
cmd获取时间戳方法:
powershell -Command "[int][double]::Parse((Get-Date -UFormat %s))"
时间戳解析:
void RTC_GetCalendar(CalendarTypeDef *calendarBuf) {
// 等待寄存器同步
while (!(RTC->CRL & RTC_CRL_RSF)) {
}
// 清除RSF
RTC->CRL &= ~RTC_CRL_RSF;
// 读取计数器中的值
uint32_t second = RTC->CNTH << 16 | RTC->CNTL;
// 解析时间戳
struct tm *time = localtime(&second);
calendarBuf->year = time->tm_year + 1900;
calendarBuf->month = time->tm_mon + 1;
calendarBuf->day = time->tm_mday;
calendarBuf->hour = time->tm_hour;
calendarBuf->minute = time->tm_min;
calendarBuf->second = time->tm_sec;
}
测试:
int main() {
uart_init();
RTC_Init();
//RTC_SetUnixSecond(1734606383);
LOG_DEBUG("hello world")
CalendarTypeDef calendar;
while (1) {
RTC_GetCalendar(&calendar);
LOG_DEBUG("%04d-%02d-%02d %02d:%02d:%02d", calendar.year,
calendar.month, calendar.day, calendar.hour, calendar.minute,
calendar.second);
delay_ms(1000);
}
}
优化-时间戳初始化
void RTC_SetUnixSecond(uint32_t unixSecond) {
if (BKP->DR3) {
LOG_DEBUG("已初始化过时间戳")
return;
}
BKP->DR3 = 1;
LOG_DEBUG("开始初始化时间戳")
/* 参考手册RTC寄存器配置流程 */
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
// 进入RTC配置模式
RTC->CRL |= RTC_CRL_CNF;
// 配置RTC计数值,例如当前unix时间戳(秒)1734524925
// 等待秒标志位置位后才能配置RTC计数值
while ((RTC->CRL & RTC_CRL_SECF) == 0) {
}
RTC->CNTH = unixSecond >> 16;
RTC->CNTL = unixSecond;
// 退出RTC配置模式
RTC->CRL &= ~RTC_CRL_CNF;
// 退出RTC配置模式后,写操作才会被执行
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
}
int main() {
uart_init();
RTC_Init();
RTC_SetUnixSecond(1734606383);
LOG_DEBUG("hello world")
CalendarTypeDef calendar;
while (1) {
RTC_GetCalendar(&calendar);
LOG_DEBUG("%04d-%02d-%02d %02d:%02d:%02d", calendar.year,
calendar.month, calendar.day, calendar.hour, calendar.minute,
calendar.second);
delay_ms(1000);
}
}
完整代码
#include "delay.h"
#include "key.h"
#include "led.h"
#include "logger.h"
#include "stdio.h"
#include "stm32f10x.h"
#include "string.h"
#include "time.h"
#include "uart.h"
typedef struct {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t weekday;
} CalendarTypeDef;
void uart1_received_callback(uint8_t buf[], uint8_t size) {}
void BKP_EnableAccess(void) {
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
// 开启后后备域写保护
PWR->CR |= PWR_CR_DBP;
}
/**
* @brief 配置 RTC 的时钟源为 LSE。
* @details 如果后备域已经配置,则直接返回。
* 否则,软件复位后备域,启用 LSE 时钟,并将其配置为 RTC 时钟源。
*/
void RTC_ConfigLSE(void) {
if (!BKP->DR1) {
// 软件重置后备域(重置后才能重新选择RTC时钟源)
RCC->BDCR |= RCC_BDCR_BDRST;
RCC->BDCR &= ~RCC_BDCR_BDRST;
// 使能LSE
RCC->BDCR |= RCC_BDCR_LSEON;
// 选择LSE作为RTC时钟源
RCC->BDCR |= RCC_BDCR_RTCSEL_LSE;
// 设置后备寄存器DR,指示RTC时钟源已经配置过
BKP->DR1 = 1;
LOG_DEBUG("RTC时钟源已配置为LSE")
} else {
LOG_DEBUG("识别到BKP-DR标识,跳过RTC时钟源的配置")
}
}
void RTC_SetUnixSecond(uint32_t unixSecond) {
if (BKP->DR3) {
LOG_DEBUG("已初始化过时间戳")
return;
}
BKP->DR3 = 1;
LOG_DEBUG("开始初始化时间戳")
/* 参考手册RTC寄存器配置流程 */
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
// 进入RTC配置模式
RTC->CRL |= RTC_CRL_CNF;
// 配置RTC计数值,例如当前unix时间戳(秒)1734524925
// 等待秒标志位置位后才能配置RTC计数值
while ((RTC->CRL & RTC_CRL_SECF) == 0) {
}
RTC->CNTH = unixSecond >> 16;
RTC->CNTL = unixSecond;
// 退出RTC配置模式
RTC->CRL &= ~RTC_CRL_CNF;
// 退出RTC配置模式后,写操作才会被执行
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
}
void RTC_GetCalendar(CalendarTypeDef *calendarBuf) {
// 等待寄存器同步
while (!(RTC->CRL & RTC_CRL_RSF)) {
}
// 清除RSF
RTC->CRL &= ~RTC_CRL_RSF;
// 读取计数器中的值
uint32_t second = RTC->CNTH << 16 | RTC->CNTL;
// 解析时间戳
struct tm *time = localtime(&second);
calendarBuf->year = time->tm_year + 1900;
calendarBuf->month = time->tm_mon + 1;
calendarBuf->day = time->tm_mday;
calendarBuf->hour = time->tm_hour;
calendarBuf->minute = time->tm_min;
calendarBuf->second = time->tm_sec;
}
void RTC_Init() {
BKP_EnableAccess();
RTC_ConfigLSE();
/* 时钟配置 */
// 等待LSE就绪
while ((RCC->BDCR & RCC_BDCR_LSERDY) == 0) {
}
// 使能RTC-APB1接口时钟
RCC->BDCR |= RCC_BDCR_RTCEN;
/* 参考手册RTC寄存器配置流程 */
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
// 进入RTC配置模式
RTC->CRL |= RTC_CRL_CNF;
// 配置RTC预分频装载值,例如32768(7FFF),分频后得到TR_CLK=1Hz
RTC->PRLH = 0;
RTC->PRLL = 0x7FFF & RTC_PRLL_PRL;
// 退出RTC配置模式
RTC->CRL &= ~RTC_CRL_CNF;
// 退出RTC配置模式后,写操作才会被执行
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {
}
}
int main() {
uart_init();
RTC_Init();
RTC_SetUnixSecond(1734606383);
LOG_DEBUG("hello world")
CalendarTypeDef calendar;
while (1) {
RTC_GetCalendar(&calendar);
LOG_DEBUG("%04d-%02d-%02d %02d:%02d:%02d", calendar.year,
calendar.month, calendar.day, calendar.hour, calendar.minute,
calendar.second);
delay_ms(1000);
}
}