【STM32 + CubeMX 教程】RTC 实时时钟 之 日历 -- F407篇

本篇使用 STM32F407VE,实现 RTC 实时时钟的日历功能(包含断电续存、串口更新时间)。

目录

[一、RTC 实时时钟 核心要点](#一、RTC 实时时钟 核心要点)

[二、RTC 硬件电路:外部晶振 + 备用电池](#二、RTC 硬件电路:外部晶振 + 备用电池)

[三、工程准备:CubeMX / Keil 基础工程搭建](#三、工程准备:CubeMX / Keil 基础工程搭建)

[四、CubeMX 配置 RTC 模块](#四、CubeMX 配置 RTC 模块)

[五、读取 RTC 日历数据](#五、读取 RTC 日历数据)

六、用备份寄存器实现断电续存

[七、更新 RTC 时间 / 日期](#七、更新 RTC 时间 / 日期)


一、RTC 实时时钟 核心要点

RTC(Real-Time Clock,实时时钟)是 STM32 芯片内置的独立低功耗定时器模块,核心特性是:主电源(VDD)断电后可由备用电池(VBAT)持续供电,能长期精准保持日期、时间数据不丢失。

STM32F407 的 RTC 核心功能:

  1. 基础日历时钟(记录年 / 月 / 日 / 时 / 分 / 秒 / 星期,如传感器数据采集时间戳)
  2. 两路可编程闹钟:支持按时间点 / 时间间隔触发闹钟中断(如每天 8 点触发提醒)
  3. 一路可编程唤醒:休眠模式下定时唤醒芯片执行任务(低功耗场景核心功能)

⚠️ 踩坑提醒:

  • 读取顺序:必须先读时间,后读日期(违反顺序会导致数据错乱)
  • 写入顺序:必须先写时间,后写日期(违反顺序会导致数据错乱)

二、RTC 硬件电路:外部晶振 + 备用电池

STM32 RTC 功能的稳定实现,必须依赖外部硬件电路!核心包含两部分:外部低速晶振(LSE)电路VBAT 备用电池电路

原理图设计,参考下图:

硬件实物,参考下图:

1. 外部低速晶振电路 LSE (RTC的时钟源)

STM32F407 的 RTC 支持 3 种时钟源,实际项目中 99% 选用 LSE。各特性对比如下:

时钟源 频率 优点 缺点 适用场景
LSI 约 32kHz 芯片内部集成,省成本 精度低 仅临时测试
LSE 32.768kHz 精度高、功耗低 需外部晶振+电池电路 实际项目首选
HSE 如 25MHz 精度极高 功耗高、断电后无法工作 持续主供电场景

LSE 电路设计要点(新手必看):

器件 规格要求 & 注意事项
32.768kHz 晶振 必须用**无源晶振;**有源晶振需额外供电,不适合 RTC 低功耗场景
匹配电容 2 个等值电容,参考容值 12pF; 电容过大 / 过小会导致晶振起振失败; 电容偏小时RTC 走时偏快,偏大时走时偏慢; 正常误差约 1.5 秒 / 天,误差过大可微调电容值;

2. 备用电池电路 (VBAT 引脚)

STM32 的 VBAT 引脚是 RTC、备份域的专属供电引脚:

  • 主电源VDD正常时:由 VDD 给 RTC / 备份寄存器供电;
  • 主电源断电后:自动切换为备用电池供电,保证时间 / 备份数据不丢失。

VBAT 电路设计要点:

器件 规格要求 & 注意事项
电池座 常用 CR1220;空间充足选 CR2032(容量约 220mAh,可供电 RTC 数月至数年)
纽扣电池 配套电池座选型;必须使用 3V 规格(禁用 1.5V/5V 电池,会损坏芯片);
去耦电容 100nF陶瓷电容,尽量靠近 VBAT 引脚焊接,滤除供电杂波;
BAT54C 隔离 VDD 和备用电池,防止电源倒灌,保证供电切换可靠;

⚠️ 提醒 1 : VBAT电路仅为RTC、备份寄存器供电,不是为整个芯片供电

⚠️ **提醒 2 :**长期供电场景可替换为「锂电池 + 充放电管理电路」,续航和稳定性优于纽扣电池。


三、工程准备:CubeMX / Keil 基础工程搭建

本节仅聚焦 RTC 功能相关的基础准备,确保后续能通过串口打印 / 更新 RTC 日历数据。

步骤 3-1:创建基础工程

基于已有的 STM32F407 CubeMX 工程添加 RTC 功能;你可以使用自己的 CubeMX 工程添加。

若需新建工程,可参考:

步骤 3-2:配置 UART1 收发 & 实现 printf 重定向

调试时需要通过 printf 打印 RTC 数据,同时接收串口助手的数据以更新时间。

因此,工程需要实现 UART1 通信,和printf 重定向到UART1:

⚠️ **提醒 :**可以用自己熟悉的串口收发方案,只需保证能与串口助手双向通信即可,无需照搬特定方法。

步骤 3-3:工程验证(必做)

完成 printf 重定向后,添加测试代码并烧录:

cpp 复制代码
printf( "Hello World! RTC 测试 \r\n" ); 

打开串口助手(关闭 16 进制显示),确认能正常接收字符串,验证:

  • 系统时钟配置正确;
  • printf 重定向是否成功(核心,保证后续 RTC 数据能正常打印)

四、CubeMX 配置 RTC 模块

步骤 4-1:启用外部低速晶振(LSE)

具体操作:

  • 在 CubeMX 的【RCC】页面中,LSE选择:Crystal/Ceramic Resonator (晶振)

步骤 4-2:选择 RTC 时钟源

具体操作:

  • 在【时钟树】中, RTC 时钟源选择: LSE (32.768KHz)

⚠️ 提醒 :

LSE 固定频率为 32.768kHz,后续预分频参数计算完全依赖此值;而HSE、LSI 频率不同,切勿选错!

步骤 4-3:启用 RTC 并配置预分频参数

具体操作:

  1. 打开【TIM】组,进入【RTC】页面

  2. 勾选 Activate Clock Source ,使能 RTC 时钟

  3. 勾选 Activate Calendar,启用 日历设置

  4. 其它参数:默认;(不要改动,后面将用代码进行日历数据更新)

⚠️ 参数解释:

如果上一步选择 LSE (固定为32.768KHz), 这里参数已默认配置好了,无需手动设置:

  • 小时格式选择: 24 Hours(24 小时制,符合常规习惯);
  • 异步预分频值 (Asynchronous Prescaler) : 127,即 128-1。值越大,RTC 功耗越低;
  • 同步预分频值 (Synchronous Prescaler):255,即 256-1。值越大,计时精度越高

核心计算公式:

LSE 时钟频率 ÷ 异步预分频值 ÷ 同步预分频值 = 32768Hz ÷ 128 ÷ 256 = 1Hz

即 RTC 每秒计数 1 次。

当使用 HSE、LSI时,需要手动配置上述两个预分频值,使计时实现常用的1Hz:

步骤 4-4:生成初始化代码

具体操作:

  • 点击 右上角 Generate Code 按钮,CubeMX 自动将 RTC 配置代码添加到工程中。

至此,RTC 的底层初始化配置已完成,接下来可进入应用层代码编写(如时间读写、闹钟配置等)。


五、读取 RTC 日历数据

步骤 5-1:日历读取 & 打印代码

具体操作:

  • while(1) 循环中添加以下代码,实现【每 1 秒读取一次 RTC 数据并打印】。
cpp 复制代码
        /** 每隔1000ms,读取一次日历数据,并打印 **/
        static uint32_t lastTime_1000ms = 0;                                             // 定义静态变量,用于记录上次执行这个任务的时间点(单位:毫秒)
        if (HAL_GetTick() - lastTime_1000ms >= 1000)                                     // 检查当前时间距离上次执行是否已经过了1000ms; HAL_GetTick()是获取启动后经过的毫秒数;
        {
            RTC_TimeTypeDef time = { 0 };                                                // 时间 时-分-秒
            RTC_DateTypeDef data = { 0 };                                                // 日期 年-月-日
            // 重点:必须先读时间、后读日期 否则数据可能错乱
            HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN);                               // 读取时间数据; 必须先读时间后读日期; 写操作时,也必须先写时间后写日期,否则数据可能错乱
            HAL_RTC_GetDate(&hrtc, &data, RTC_FORMAT_BIN);                               // 读取日期数据; 必须先读时间后读日期; 写操作时,也必须先写时间后写日期,否则数据可能错乱
            printf("%04d年%02d月%02d日  ", 2000 + data.Year, data.Month, data.Date);     // 打印 日期
            printf("%02d:%02d:%02d  ", time.Hours, time.Minutes, time.Seconds);          // 打印 时间
            switch (data.WeekDay)                                                        // 打印 星期
            {
                case 1:
                    printf("星期一");  break;
                case 2:
                    printf("星期二");  break;
                case 3:
                    printf("星期三");  break;
                case 4:
                    printf("星期四");  break;
                case 5:
                    printf("星期五");  break;
                case 6:
                    printf("星期六");  break;
                case 7:
                    printf("星期日");  break;
                default:
                    printf("星期错误"); break;
            }
            printf("  \r\n");                                                            // 打印 换行
            // 记录任务时间戳
            lastTime_1000ms = HAL_GetTick();                                             // 更新时间戳:将当前时间设为新基准,确保下一次周期准确从此时开始计算
        }

步骤 5-2:编译烧录 & 验证 (发现断电不续存问题)

完成代码编写后,编译工程并烧录到开发板:

  1. 打开串口助手(波特率、校验位等与 UART1 配置一致);
  2. 可以看到,串口助手会每秒打印一次 RTC日历数据;
  3. 先运行一会,如几十秒
  4. 断电、重新上电;通过串口助手发现,日历时间被重置了,没有续存!

⚠️ 时间被重置的原因:

MX_RTC_Init() 函数里,每次上电会执行「RTC 初始化 + 写入 CubeMX 预设的默认时间」,导致程序每次重新运行都被重置时间。下文解决这个事件!


六、用备份寄存器实现断电续存

VBAT 引脚的备用电池可同时为 RTC备份寄存器供电,因此可通过备份寄存器记录「RTC 是否已初始化」,避免程序重新运行时再次写入默认时间。

这一节,通过备份寄存器的 RTC_BKP_DR1 存储一个标志值,判断 RTC 是否已配置过日历数据:

  • 若备份寄存器 RTC_BKP_DR1 值 == 0x01,表示已设置过日历,跳过函数下方的日历设置
  • 若备份寄存器 RTC_BKP_DR1 值 ≠ 0x01,表示第一次运行,需要设置一次日历数据

**⚠️ 提示 1:**F407共有20个备份寄存器(0~19); 只要VBAT引脚上保持有3V供电,它们就有效;

**⚠️ 提示 2:**其它存储方式:使用芯片内部Flash、24C02等均可,但备份寄存器更适合。

步骤 6-1:修改 MX_RTC_Init () 函数

具体操作:

  • 打开 rtc.c 的 MX_RTC_Init() 函数,
  • 在 /* USER CODE BEGIN Check_RTC_BKUP */ 区域添加:
cpp 复制代码
    // 1.读取备份寄存器1的值,如果等于1,就跳过下面的设置,退出函数
    if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) == 0x01)
    {
        return;
    }

    // 2.写入备份寄存器1的值,标记已设置过日历数据
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x01);

添加完成后,位置如下图:

步骤 6-2:编译烧录 & 验证

编译工程,并烧录:

  1. 打开串口助手,先运行几十秒;
  2. 断电、重新上电、通过串口助手观察,
  3. 打印的时间,已能断电续存,实现了断电不丢失。

运行效果如下图:


七、更新 RTC 时间 / 日期

实际项目中,RTC 时间更新的方式有很多:

  • 触摸屏 / 按键:读取用户输入的时间;
  • GPS 同步:解析 GPS 模块输出的时间戳,再校准 RTC;
  • 网络同步:通过 NTP 协议从服务器获取标准时间,更新 RTC。

本节以串口助手为例,实现通过发送固定格式字符串更新 RTC 时间。

核心规则设计思路:

  • 串口助手发送固定格式的字符串(无空格、无换行、字段完整),以便于用代码解释
  • 范样:2026年02月04日19时11分30秒星期三
  • 格式拆解:YYYY年MM月DD日HH时MM分SS秒星期X(X 为 "一 / 二 / 三 / 四 / 五 / 六 / 日")

步骤 7-1:编写字符串解析函数 RTC_ParseTimeString()

具体操作:

  • 位置,在 main.c 的 /* USER CODE BEGIN 0 */ 区域内
  • 编写下面的 RTC_ParseTimeString() 函数:
cpp 复制代码
#include <string.h>
#include <stdio.h>

/******************************************************************************
 * 函  数: RTC_ParseTimeString
 * 功  能: 解析固定格式的时间字符串,提取有效数据并更新RTC时间
 * 格  式: "YYYY年MM月DD日HH时MM分SS秒星期X"(如:2026年02月04日19时11分30秒星期三)
 * 约  定: 1. 字符串无空格、无换行,字段顺序/单位不可修改;
 *          2. 年4位、月/日/时/分/秒各2位,星期为中文单字(一/二/.../日);
 *          3. 星期映射:一=1、二=2、三=3、四=4、五=5、六=6、日=7;
 * 参  数: const char* timeStr  待解析的时间字符串(串口接收的原始数据)
 * 返回值: 无
 * 异常处理:空指针/格式错误/星期非法 → 触发提示并退出,不执行更新
 ******************************************************************************/
void RTC_ParseTimeString(const char* timeStr)
{
    // 1. 空指针校验,避免程序崩溃
    if (timeStr == NULL)
    {
        printf("解释失败:输入字符串为空! \r\n");      // 空字符串直接退出
        return;
    }
    // 2. 定义变量存储解析结果
    uint16_t year = 0;        // 年(4位,如2026)
    uint8_t month = 0;        // 月(2位,01-12)
    uint8_t date = 0;         // 日(2位,01-31)
    uint8_t hours = 0;        // 时(2位,00-23)
    uint8_t minutes = 0;      // 分(2位,00-59)
    uint8_t seconds = 0;      // 秒(2位,00-59)
    uint8_t weekday = 0;      // 星期(1-7,对应一-日)
    char weekStr[2] = {0};    // 存储星期中文单字
    // 3. 按固定格式解析字符串
    // %4hu:匹配4位无符号短整型(对应uint16_t)
    // %2hhu:匹配2位无符号字符(对应uint8_t)
    // %2s:匹配星期中文单字(适配GBK/UTF-8编码)
    int parseRet = sscanf(timeStr, "%4hu年%2hhu月%2hhu日%2hhu时%2hhu分%2hhu秒星期%2s",
                          &year, &month, &date, &hours, &minutes, &seconds, weekStr);
    // 4. 解析结果校验(正常应提取7个字段,否则格式不匹配)
    if (parseRet != 7)
    {
        printf("解释失败:字符串格式错误! 请按示例格式发送:\n");  // 格式不匹配,进入错误处理
        printf("2026年03月05日15时45分00秒星期四 \n\n");           // 字符串格式,必须严格匹配
        return;
    }
    // 5. 中文星期转换为数字(匹配RTC_SetNewData参数)
    if (strcmp(weekStr, "一") == 0)         weekday = 1;           // 中文"一"对应RTC星期1(周一)
    else if (strcmp(weekStr, "二") == 0)    weekday = 2;           // 中文"二"对应RTC星期2(周二)
    else if (strcmp(weekStr, "三") == 0)    weekday = 3;           // 中文"三"对应RTC星期3(周三)
    else if (strcmp(weekStr, "四") == 0)    weekday = 4;           // 中文"四"对应RTC星期4(周四)
    else if (strcmp(weekStr, "五") == 0)    weekday = 5;           // 中文"五"对应RTC星期5(周五)
    else if (strcmp(weekStr, "六") == 0)    weekday = 6;           // 中文"六"对应RTC星期6(周六)
    else if (strcmp(weekStr, "日") == 0)    weekday = 7;           // 中文"日"对应RTC星期7(周日)
    else                                                           // 未知星期字符,解析失败
    {
        printf("\n解释失败,星期格式错误!\r\n ");
        return;
    }
    // 6. 解析成功,调用函数更新RTC时间
    RTC_SetNewData(year, month, date, hours, minutes, seconds, weekday);
}

步骤 7-2:编写 RTC_SetNewData() 函数 (写入日历数据)

具体操作:

  • 位置,在刚才的 RTC_ParseTimeString() 函数上方,
  • 编写下面的 RTC_SetNewData() 函数 :
cpp 复制代码
/******************************************************************************
 * 函  数: RTC_SetNewData
 * 功  能: 设置RTC的时间和日期(严格遵循"先时间后日期"的操作顺序)
 * 说  明: 1. 操作前需解锁后备域;
 *          2. CubeMX默认以2000年为RTC年份基准,需做偏移处理;
 *          3. 中国不实行夏令时,相关参数固定为NONE;
 * 参  数: uint16_t  year    实际年份(2000-2127,超出范围则报错)
 *          uint8_t   month   月份(1-12)
 *          uint8_t   date    日期(1-31)
 *          uint8_t   hours   小时(0-23,24小时制)
 *          uint8_t   minutes 分钟(0-59)
 *          uint8_t   seconds 秒数(0-59)
 *          uint8_t   weekday 星期(1=周一,2=周二,...,7=周日)
 * 返回值: 无
 ******************************************************************************/
void RTC_SetNewData(uint16_t year, uint8_t month, uint8_t date, uint8_t hours, uint8_t minutes, uint8_t seconds, uint8_t weekday)
{
    /* 1.定义RTC时间、日期结构体 */
    RTC_TimeTypeDef sTime = {0};                                     // 时间结构体 (时/分/秒)
    RTC_DateTypeDef sDate = {0};                                     // 日期结构体 (年/月/日/星期)
    /* 2.检查参数合法性 */
    if ((year < 2000) || (year > 2127) || (month < 1) || (month > 12) || (date < 1) || (date > 31) ||
            (hours > 23) || (minutes > 59) || (seconds > 59) || (weekday < 1) || (weekday > 7))
    {
        printf("日历配置函数:参数非法,退出本次设置。\r\n");        // 打印 非法参数
        return;                                                      // 非法参数直接退出,不执行后续配置
    }
    /* 3.使能时钟、解锁后备域 */
//    __HAL_RCC_PWR_CLK_ENABLE();                                      // 使能PWR时钟;
//   HAL_PWR_EnableBkUpAccess();                                      // 解锁后备域访问; F4默认锁定;
    /* 4.配置时间; RTC强制要求:先写时间,后写日期 */
    sTime.Hours = hours;                                             // 时; 注意小时的格式,检查MX_RTC_Init()函数里的HourFormat参数,有24小时、12小时两种格式
    sTime.Minutes = minutes;                                         // 分
    sTime.Seconds = seconds;                                         // 秒
    sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;                  // 关闭夏令时。中国不实行夏令时,因此固定设置为NONE
    sTime.StoreOperation = RTC_STOREOPERATION_RESET;                 // 夏令时配套参数,固定为RESET
    if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)    // 设置时间; RTC要求:先设置时间(必须顺序)
    {
        printf("RTC时间写入失败! \n");
        return;                                                      // 直接退出,不执行后续配置
    }
    /* 5.配置日期; 必须在时间写入后执行 */
    sDate.WeekDay = weekday;                                         // 星期:1-一、2-二、3-三、4-四、5-五、6-六、7-日
    sDate.Month = month;                                             // 月
    sDate.Date = date;                                               // 日
    sDate.Year = year - 2000;                                        // 年份偏移; CubeMX默认2000为基准, 需减去2000
    if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)    // RTC要求:后设置日期(必须顺序)
    {
        printf("RTC日期写入失败! \n");
        return;                                                      // 直接退出,不执行后续配置
    }
    /* 6.写入标记值到备份寄存器,标记已配置过日历 */
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x01);                   // 备份寄存器1写入标志值。用于下次上电时判断是否已设置日历; 网上很多示例会用类似0x5A5A的值,原理是一样的,但0x5A5A容易被误解有特殊作用
    /* 7.锁定后备域、关闭PWR时钟 */
//    HAL_PWR_DisableBkUpAccess();                                     // 操作完成后锁定后备域,防止误操作
//    __HAL_RCC_PWR_CLK_DISABLE();                                     // 关闭PWR时钟,节省功耗
    /* 8.打印配置成功提示 */
    printf("\nRTC日历数据写入成功!\r\n");                           // 打印 提示
}

步骤 7-3:配置串口接收 & 触发解析

具体操作:

  • 位置,在 main() 函数的 while(1) 循环中,
  • 添加编写下面的串口接收处理、调用字符串解释。
cpp 复制代码
        if (UART1_GetRxNum())                                                            // 判断USART1是否获取到新一帧数据:函数返回的接收字节数,大于0,即为收到新一帧数据
        {
            // 获取数据
            uint8_t *rxData = UART1_GetRxData();                                         // 获取数据的地址
            uint16_t rxNum = UART1_GetRxNum();                                           // 获取数据的字节数
            // 把收到的数据,printf到串口助手观察
            printf("\r<<<<< USART1 收到一帧数据 \r");                                    // 提示
            printf("字节数:%d \r", rxNum);                                              // 显示字节数
            printf("ASCII : %s\r", (char *)rxData);                                      // 显示数据,以ASCII方式显示,即以字符串的方式显示
            printf("\r\r");                                                              // 显示换行
            RTC_ParseTimeString((const char*) rxData);                                   // 解释日历时间,并更新保存
            UART1_ClearRx();                                                             // 重要:清0接收标志,即清0接收到的字节数; 每次处理完成数据,就要调用这个函数清0
        }

编写完成后,位置参考如下图:

注意 & 优化:

上面的UART1接收处理,你可以替换成自己熟悉的方法、方案。

步骤 7-4:编译烧录 & 验证

具体操作:

  1. 烧录后,串口助手将每秒打印当前时间;
  2. 发送固定格式字符串,如: 2026年03月10日18时57分30秒星期二
  3. 串口提示:RTC日历数据写入成功!
  4. 断电重启,时间仍为更新后的数值,验证断电续存。

至此,RTC 初始化、日历读取和写入、日历更新,已全部实现。


相关推荐
LCG元2 小时前
振动能量采集:STM32U5从振动启动,能量管理完整方案
stm32·单片机·嵌入式硬件
_探索_11 小时前
STM32U5F7VJT6Q (Cortex-M33, 160MHz) TouchGFX统计MCU占用率和FPS
stm32·单片机·嵌入式硬件
猫猫的小茶馆11 小时前
【Linux 驱动开发】Linux 内核启动过程详解
linux·c语言·arm开发·驱动开发·stm32·单片机·mcu
辰哥单片机设计11 小时前
STM32太阳能光伏板
stm32·单片机·嵌入式硬件
炸膛坦客11 小时前
Cortex-M3-STM32F1 开发:(五十四)CAN(车企会用),难但很重要
stm32·单片机·嵌入式硬件
猫猫的小茶馆12 小时前
【Linux 驱动开发】STM32MP1 + GT911 触摸显示系统开发笔记
linux·arm开发·驱动开发·stm32·单片机·嵌入式硬件·mcu
雾削木13 小时前
STM32 & ESP32 嵌入式学习路线
stm32·单片机·学习
LCG元13 小时前
十年电池寿命:STM32L4低功耗模式+RTC定时唤醒,传感器节点方案
stm32·嵌入式硬件·实时音视频
芦苇电子13 小时前
【STM32】STM32开发详解 : 寄存器、标准库与HAL库三种开发方式深度解析及初学者建议
stm32·单片机·嵌入式硬件