RTC和BKP:为STM32添加持久化时钟与关键数据存储


后备寄存器-BKP

image-20241218165141262

寄存器实现

image-20241218170348501

HAL实现

image-20241218172338725

image-20241218172438717

源码分析

image-20241218172533254

image-20241218172604416

RTC实时时钟

image-20241218203341971

原理分析

框图

image-20241218183552806

后备域重置

image-20241218192521184

image-20241218195313254

RTC时钟源

image-20241218200313953

示例代码

// 配置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寄存器流程

image-20241218191047351

image-20241218202928641

硬件置位标志位 & 写操作同步

image-20241218191656801

image-20241218191156436

读寄存器同步

  • RTC 核心时钟和 APB1 接口时钟是独立的。
  • 软件通过 APB1 接口访问 RTC 数据时,实际上读取的是同步后的副本。
  • 当 APB1 接口重新启用后,第一次读取 RTC 数据可能出错,因为寄存器副本还未同步完成。
  • 软件需要在读取 RTC 数据之前,等待寄存器同步完成(通过检测 RSF 标志)

image-20241218221117011

image-20241218221222165

案例-使用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仍在运行

先运行一次,设置时间戳;然后注释时间戳的设置,测试重启、断电重新上电后,后备域仍在运行。

image-20241218223711578

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);
    }
}

image-20241219111723558

完整代码

#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);
    }
}

HAL库实现

image-20241219115411267

image-20241219120116173

文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录