欢迎光临
我们一直在努力

【STM32】RTC 完结篇:日期操作、闹钟中断与附录(时间戳 / 闹钟 / NTP)


   日期操作、闹钟中断与附录(时间戳 / 闹钟 / NTP) 

一、RTC 日期操作:简易日历

1.1 案例目标

  • 程序启动时设置一个初始时刻(含年月日、时分秒);
  • 使用 RTC_CNT 保存初始时刻,随时间流逝自动更新;
  • 通过串口每秒输出格式化时间;
  • 断电后时间更新能继续正确进行。

1.2 日期结构体 RTC_DateTypeDef

typedef struct
{
uint8_t WeekDay; /*!< 星期几 (HAL_RTC_SetDate 不需要此参数),可选值参考 @ref RTC_WeekDay_Definitions */
uint8_t Month; /*!< 月份 (BCD格式),可选值参考 @ref RTC_Month_Date_Definitions */
uint8_t Date; /*!< 日期 (1~31) */
uint8_t Year; /*!< 年份 (0~99, 对应 2000~2099 年) */
} RTC_DateTypeDef;

两个关键约定:

  • Year 是 0~99,起始年份是 2000 年——所以 .Year = 35 表示 2035 年;
  • Month 必须用 BCD 码设置——注意十月、十一月、十二月:
  • /* Coded in BCD format */
    #define RTC_MONTH_JANUARY ((uint8_t)0x01)
    #define RTC_MONTH_FEBRUARY ((uint8_t)0x02)
    #define RTC_MONTH_MARCH ((uint8_t)0x03)
    #define RTC_MONTH_APRIL ((uint8_t)0x04)
    #define RTC_MONTH_MAY ((uint8_t)0x05)
    #define RTC_MONTH_JUNE ((uint8_t)0x06)
    #define RTC_MONTH_JULY ((uint8_t)0x07)
    #define RTC_MONTH_AUGUST ((uint8_t)0x08)
    #define RTC_MONTH_SEPTEMBER ((uint8_t)0x09)
    #define RTC_MONTH_OCTOBER ((uint8_t)0x10) // 十月 = 0x10!
    #define RTC_MONTH_NOVEMBER ((uint8_t)0x11) // 十一月 = 0x11!
    #define RTC_MONTH_DECEMBER ((uint8_t)0x12) // 十二月 = 0x12!

            十月不能用 0x0A(那是非法 BCD),必须用 0x10——BCD 的"每个十进制位独立 4 位"规则,1 和 0 各占 4 位。

    1.3 设置/获取日期函数签名

    HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc,
    RTC_DateTypeDef *sDate,
    uint32_t Format);
    HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc,
    RTC_DateTypeDef *sDate,
    uint32_t Format);

    参数含义
    hrtc RTC 句柄
    sDate 日期结构体(SetDate 输入、GetDate 输出)
    Format RTC_FORMAT_BIN / RTC_FORMAT_BCD
    返回值 HAL_StatusTypeDef

    ⚠️ 最大的坑(F1 专属):

            STM32F1 系列没有"RTC 日期寄存器"——HAL_RTC_SetDate 只是把日期写入内存中的结构体,并没有真正写入 RTC 相关寄存器。

            复位开发板,日期数据就丢失!

            而 STM32F0/F4 系列有独立的日期寄存器,不存在这个问题。

    源码印证——跳进 HAL_RTC_SetDate 内部:

    /* Change the current date */
    hrtc->DateToUpdate.Year = sDate->Year;
    hrtc->DateToUpdate.Month = sDate->Month;
    hrtc->DateToUpdate.Date = sDate->Date;

            它只是把年月日写进了 hrtc->DateToUpdate(内存结构体)。

    而 RTC_HandleTypeDef 里的这个字段:

    typedef struct
    {
    RTC_TypeDef *Instance; /*!< RTC寄存器基地址 */
    RTC_InitTypeDef Init; /*!< RTC所需参数配置 */
    RTC_DateTypeDef DateToUpdate; /*!< 用户设置的当前日期,会自动更新 */
    HAL_LockTypeDef Lock; /*!< RTC锁定对象(用于互斥访问) */
    __IO HAL_RTCStateTypeDef State; /*!< RTC通信状态 */
    …
    } RTC_HandleTypeDef;

    注意区分:

            普通寄存器(如 GPIO 的 ODR)复位也会丢数据;

            RTC 系列寄存器处于备份域,靠备份电源(VBAT)供电才具备"复位不丢"的能力。

            日期没进备份域,只进了 RAM,所以复位必丢。

    1.4 代码版本 1:使用 HAL_RTC_SetDate

    ① DateTimeInit:设置初始时间日期(BKP 控制只设一次)

    void DateTimeInit(void)
    {
    RTC_TimeTypeDef sTime = {
    .Hours = 8, .Minutes = 0, .Seconds = 0
    };
    RTC_DateTypeDef sData = {
    .Year = 35, // 2035 年
    .Month = 7, // 7 月
    .Date = 15 // 15 日
    };

    uint16_t magic_number = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
    if (magic_number != RTC_MAGIC_NUMBER)
    {
    HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    HAL_RTC_SetDate(&hrtc, &sData, RTC_FORMAT_BIN);
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, RTC_MAGIC_NUMBER);
    }
    }

    ② GetDateTime:获取时间日期并格式化打印

    void GetDateTime(void)
    {
    RTC_TimeTypeDef sTime = {0};
    RTC_DateTypeDef sData = {0};

    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    HAL_RTC_GetDate(&hrtc, &sData, RTC_FORMAT_BIN);

    printf("%02d-%02d-%02d %02d:%02d:%02d\\n",
    sData.Year, sData.Month, sData.Date,
    sTime.Hours, sTime.Minutes, sTime.Seconds);
    }

    main 里 while(1) 调用 GetDateTime + HAL_Delay(1000)。

    运行现象(第一次上电正常):

    2035-07-15 08:00:00
    2035-07-15 08:00:01
    …

    ⚠️ 一按复位键,日期就出问题:

    复位前:2035-07-15 08:00:22 → 08:00:23 → …(正常)
    复位后:2035-07-15 08:00:26 → … → 2000-01-01 08:00:26 ← 日期被清空了!

    原因分析:

       time 还是正确的(存在备份域 RTC_CNT),主要是 Date 不对了(存在内存结构体,复位即丢)。

    而且注意:

            其实保存在内存里的日期只是一个"起始日期",后续 HAL_RTC_GetDate 会在底层自动计算累计过了多少秒,再更新日期数据。

    解决思路两条:

    方案评价
    用后备寄存器保存年月日 资源占用多、太麻烦,不推荐
    不再用 SetDate/GetDate,改用 UNIX 时间戳:基于 RTC_CNT 直接保存时间戳,通过时间戳转"年月日时分秒" 采用(F1 上工程实现有局限,教学使用)

    1.5 代码版本 2:UNIX 时间戳方案

    1.5.1 什么是 UNIX 时间戳

    计算机保存时间,通常不按"年月日时分秒"字符串存,而是用一个整数:

            以 1970 年 1 月 1 日 0 时 0 分 0 秒为基准,计算当前时刻与基准时刻的秒数之差,称为 UNIX 时间戳。

    当前日期时间 = 基准时刻(1970-01-01 00:00:00) + 偏移量 N(秒)

    • 时间是线性递增的 → 时间戳也是线性增长的数字;
    • 现在距 1970 年已 50 多年,时间戳已经是个很大的数字(例:2038-01-01 20:14:49 → 2145960889 秒);
    • 常见在线工具:时间戳(Unix timestamp)转换工具 – 在线工具

    溢出问题:

    类型位宽溢出年份
    标准 UNIX 时间戳(有符号 32 位) 31 位有效 2038 年
    RTC_CNT(无符号 32 位) 32 位 2106 年

    工程哲学:

            时间须存储方能使用,存储必占有限位宽,位宽必致数值上限,上限必生溢出。

            溢出既不可免,则时间设置便非技术难题,而是工程选择:使溢出时点晚于产品设计寿命即可。

    为什么选 1970 年 1 月 1 日?

            UNIX 操作系统研发于 20 世纪 60~70 年代,开发 UNIX 的大佬正是 C 语言之父丹尼斯·里奇和肯·汤姆逊(C 语言其实是他们开发 UNIX 时"顺便"设计的),他们选了 1970-01-01 作为基准——如今已成为计算机"数字纪元"的起点。

    1.5.2 时间与数字

    如果日期/时间被转化为数字 → 日期时间就能被保存 → 被 RTC 维护
    时间是线性递增的 → 数字也递增 → 这个数字就是时间戳

    1.5.3 代码设计

    第一步:修改 HAL 源码(真实工程禁止)——参考"RTC_CNT 操作"章节,把 RTC_ReadTimeCounter / RTC_WriteTimeCounter 的 static 去掉,并在 main.c 上方声明。

    第二步:AI 生成时间戳转换代码(课件演示了 Qoder 智能体工作台,AI 擅长生成这种固定规则的复杂逻辑;AI 生成的代码每次可能不同):

    TimeStamp.h

    #ifndef __BSP_TIMESTAMP_H
    #define __BSP_TIMESTAMP_H

    #include <stdint.h>
    #include <stdbool.h>
    #include <stddef.h>

    // 日期时间结构体
    typedef struct {
    uint16_t year; // 年份 (1970-2100)
    uint8_t month; // 月份 (1-12)
    uint8_t day; // 日 (1-31)
    uint8_t hour; // 时 (0-23)
    uint8_t minute; // 分 (0-59)
    uint8_t second; // 秒 (0-59)
    uint8_t weekday; // 星期 (0-6,0=周日)
    } DateTime_t;

    /**
    * @brief 时间戳转日期时间
    * @param timestamp: Unix时间戳 (从1970-01-01 00:00:00开始的秒数)
    * @param dt: 输出日期时间结构体
    * @retval true: 转换成功,false: 转换失败
    */
    bool Timestamp_To_DateTime(uint32_t timestamp, DateTime_t *dt);

    /**
    * @brief 日期时间转时间戳
    * @param dt: 日期时间结构体
    * @param timestamp: 输出Unix时间戳
    * @retval true: 转换成功, false: 转换失败
    */
    bool DateTime_To_Timestamp(const DateTime_t *dt, uint32_t *timestamp);

    /**
    * @brief 判断闰年
    * @param year: 年份
    * @retval true: 闰年, false: 平年
    */
    bool Is_Leap_Year(uint16_t year);

    /**
    * @brief 获取某月的天数
    * @param year: 年份
    * @param month: 月份 (1-12)
    * @retval 该月的天数
    */
    uint8_t Get_Days_In_Month(uint16_t year, uint8_t month);

    /**
    * @brief 验证日期时间是否合法
    * @param dt: 日期时间结构体
    * @retval true: 合法, false: 不合法
    */
    bool Is_Valid_DateTime(const DateTime_t *dt);

    #endif /* __BSP_TIMESTAMP_H */

    TimeStamp.c

    #include "BSP_Timestamp.h"

    // 每个月的天数(平年)
    static const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    // 判断闰年
    bool Is_Leap_Year(uint16_t year)
    {
    return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
    }

    // 获取某月的天数
    uint8_t Get_Days_In_Month(uint16_t year, uint8_t month)
    {
    if (month < 1 || month > 12) return 0;
    if (month == 2 && Is_Leap_Year(year)) return 29;
    return days_in_month[month – 1];
    }

    // 验证日期时间是否合法
    bool Is_Valid_DateTime(const DateTime_t *dt)
    {
    if (dt == NULL) return false;
    if (dt->year < 1970 || dt->year > 2100) return false;
    if (dt->month < 1 || dt->month > 12) return false;
    if (dt->day < 1 || dt->day > Get_Days_In_Month(dt->year, dt->month)) return false;
    if (dt->hour > 23) return false;
    if (dt->minute > 59) return false;
    if (dt->second > 59) return false;
    if (dt->weekday > 6) return false;
    return true;
    }

    // 时间戳转日期时间
    bool Timestamp_To_DateTime(uint32_t timestamp, DateTime_t *dt)
    {
    if (dt == NULL) return false;

    // 计算秒数
    uint32_t seconds = timestamp;

    // 计算星期 (1970-01-01 是星期四)
    dt->weekday = (4 + seconds / 86400) % 7;

    // 计算天数
    uint32_t days = seconds / 86400;
    seconds %= 86400;

    // 计算时分秒
    dt->hour = seconds / 3600;
    seconds %= 3600;
    dt->minute = seconds / 60;
    dt->second = seconds % 60;

    // 计算年份
    uint16_t year = 1970;
    while (days >= 365)
    {
    if (Is_Leap_Year(year))
    {
    if (days >= 366) { days -= 366; year++; }
    else break;
    }
    else
    {
    days -= 365;
    year++;
    }
    }
    dt->year = year;

    // 计算月份
    uint8_t month = 1;
    while (month <= 12)
    {
    uint8_t days_in_this_month = Get_Days_In_Month(year, month);
    if (days < days_in_this_month) break;
    days -= days_in_this_month;
    month++;
    }
    dt->month = month;

    // 计算日期
    dt->day = days + 1;
    return true;
    }

    // 日期时间转时间戳
    bool DateTime_To_Timestamp(const DateTime_t *dt, uint32_t *timestamp)
    {
    if (dt == NULL || timestamp == NULL) return false;
    if (!Is_Valid_DateTime(dt)) return false;

    uint32_t days = 0;

    // 计算从1970年到目标年份的天数
    for (uint16_t year = 1970; year < dt->year; year++)
    {
    days += Is_Leap_Year(year) ? 366 : 365;
    }

    // 计算从1月到目标月份的天数
    for (uint8_t month = 1; month < dt->month; month++)
    {
    days += Get_Days_In_Month(dt->year, month);
    }

    // 加上日期
    days += dt->day – 1;

    // 转换为秒
    *timestamp = days * 86400 + dt->hour * 3600 + dt->minute * 60 + dt->second;
    return true;
    }

    1.5.4 编写调用逻辑

    ① DateTimeInit:把"年月日"转成时间戳写入 RTC_CNT

    void DateTimeInit(void)
    {
    DateTime_t dt = {
    .year = 2035,
    .month = 7,
    .day = 15,
    .hour = 8,
    .minute = 0,
    .second = 0
    };

    uint16_t magic_number = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
    if(magic_number != RTC_MAGIC_NUMBER)
    {
    uint32_t timestamp = 0;
    DateTime_To_Timestamp(&dt, &timestamp);
    printf("timestamp: %u\\n", timestamp);
    RTC_WriteTimeCounter(&hrtc, timestamp); // 秒数存进备份域
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, RTC_MAGIC_NUMBER); // 写魔数
    }
    }

    ② GetDateTime:读时间戳转回日期时间

    void GetDateTime(void)
    {
    DateTime_t dt;
    uint32_t timestamp = RTC_ReadTimeCounter(&hrtc); // 读秒数(备份域,不丢)
    Timestamp_To_DateTime(timestamp, &dt); // 秒数 → 年月日时分秒

    printf("%04d-%02d-%02d %02d:%02d:%02d\\n",
    dt.year, dt.month, dt.day,
    dt.hour, dt.minute, dt.second);
    }

    运行效果:

    RTC Test Example:
    timestamp: 2068099200 ← 2035-07-15 08:00:00 对应的时间戳
    2035-07-15 08:00:00
    2035-07-15 08:00:01
    …(正常走)
    断电…(等一会)…重新上电
    2035-07-15 08:00:18 ← 断电期间时间继续计时!
    2035-07-15 08:00:19

    结论:开发板复位时间日期也能正确显示;断电后时间继续计时(VBAT 供电)。

    细节:如果不成功,扣一下电池——因为我们用备份寄存器标记"首次启动",扣电池让后备域彻底掉电、清掉魔数,新代码才会执行初始化。

    1.6 一句总结:F1 内置 RTC 的定位

            STM32F103ZET6 已问世 15 年多,内部 RTC 功能并不完善——从它只提供 RTC_CNT、没有日期类寄存器就能看出来;且 HAL 库没有直接暴露设置 RTC_CNT 的函数,所以这部分代码只做课堂练习。实际工程中会用更完善的 MCU,或外接独立的 RTC 模块来完成计时。


    二、RTC 闹钟操作

    2.1 案例目标

    • 使用 RTC_CNT 设置系统时间(不考虑日期);
    • 使用 RTC_ALR 设置闹钟时间;
    • 当系统时间到达闹钟时间时,点亮板载 LED0。

    2.2 准备工作

  • 创建新项目;
  • 重定位 printf、连接串口;
  • 启用 PF8 引脚:配置为推挽输出、输出高电平(点亮板载 LED0,注意这是低电平点亮还是高电平点亮以板子为准——课件配图 GPIO output level 设为 High,中断回调里写 GPIO_PIN_RESET 点亮,即 PF8 低电平点亮);
  • 配置 RTC 闹钟中断:勾选 RTC alarm interrupt through EXTI line 17。
  • RTC 的两个中断,别搞混:

    中断触发事件处理函数中断号用途
    RTC global interrupt RTC_Second、RTC_Overflow RTC_IRQHandler 3 秒中断/溢出中断
    RTC alarm interrupt through EXTI line 17 只 RTC_Alarm RTC_Alarm_IRQHandler 41 闹钟中断,能唤醒深度睡眠

            一般需要秒中断用 RTC global interrupt;需要闹钟中断 + 唤醒深度睡眠用 EXTI line 17。RTC_Second/RTC_Overflow 和 RTC_Alarm 在软件上是分开处理的。

    2.3 闹钟关键函数

    ① 闹钟结构体 RTC_AlarmTypeDef

    typedef struct {
    RTC_TimeTypeDef AlarmTime; /*!< RTC闹钟时间成员 */
    uint32_t Alarm; /*!< 闹钟ID (STM32F1仅支持1个闹钟), 参数取值参考 @ref RTC_Alarm */
    } RTC_AlarmTypeDef;

    typedef struct
    {
    uint8_t Hours; /*!< 小时, 范围 0-23 */
    uint8_t Minutes; /*!< 分钟, 范围 0-59 */
    uint8_t Seconds; /*!< 秒, 范围 0-59 */
    } RTC_TimeTypeDef;

    Alarm ID 是什么?

             HAL 库为不同型号 MCU 设计,有的 MCU 有多个闹钟,需要区分设置的是哪一个。STM32F103 系列只有唯一取值 RTC_ALARM_A(0U),所以设置好之后不用关心。

    ② 设置闹钟(带中断)

    HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc,
    RTC_AlarmTypeDef *sAlarm,
    uint32_t Format);

    ⚠️ 注意:

        HAL_RTC_SetTime() 必须先调用,再使能闹钟功能。

            还有 HAL_RTC_SetAlarm(不带 _IT)也能设置闹钟,但时间到了不会触发中断,需要额外写代码(比如循环读闹钟寄存器),所以优先用 HAL_RTC_SetAlarm_IT。

    ③ 获取闹钟

    HAL_StatusTypeDef HAL_RTC_GetAlarm(RTC_HandleTypeDef *hrtc,
    RTC_AlarmTypeDef *sAlarm,
    uint32_t Alarm, // RTC_ALARM_A
    uint32_t Format);

    2.4 编写代码

    ① My_Init:设置初始时间 + 设置闹钟(12:00:10 触发)

    void My_Init() {
    // 1. 在 RTC 中设置时间
    RTC_TimeTypeDef sTime = {0};
    sTime.Hours = 12;
    sTime.Minutes = 0;
    sTime.Seconds = 0;
    if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) {
    Error_Handler();
    }

    // 2. 设置RTC闹钟
    RTC_AlarmTypeDef sAlarm = {0};
    sAlarm.AlarmTime.Hours = 12;
    sAlarm.AlarmTime.Minutes = 0;
    sAlarm.AlarmTime.Seconds = 10; // 设置闹钟在10秒后响

    // 重要,启动RTC闹钟中断
    if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN) != HAL_OK) {
    Error_Handler();
    }
    printf("初始化完成!\\n");
    }

    调用:/* USER CODE BEGIN 2 */ My_Init(); /* USER CODE END 2 */

    ② GetTimeInfo / GetAlarmInfo:读取当前时间和闹钟

    void GetTimeInfo() {
    RTC_TimeTypeDef sTime = {0};
    if (HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) {
    Error_Handler();
    }
    printf("Current Time: %02d:%02d:%02d\\n", sTime.Hours, sTime.Minutes, sTime.Seconds);
    }

    void GetAlarmInfo() {
    RTC_AlarmTypeDef sAlarm = {0};
    if (HAL_RTC_GetAlarm(&hrtc, &sAlarm, RTC_ALARM_A, RTC_FORMAT_BIN) != HAL_OK) {
    Error_Handler();
    }
    printf("Alarm Time: %02d:%02d:%02d\\n", sAlarm.AlarmTime.Hours,
    sAlarm.AlarmTime.Minutes, sAlarm.AlarmTime.Seconds);
    }

    ③ main 主循环:1 秒间隔打印 + 检测闹钟标志

    /* USER CODE BEGIN PV */
    volatile int alarm_flag = 0; // 中断里置位,主循环里查询
    /* USER CODE END PV */

    int main(void)
    {
    …
    BSP_Usart_Init(&huart1);
    HAL_Delay(1000);
    printf("RTC Test Example:\\n");
    My_Init();

    while (1)
    {
    GetTimeInfo();
    GetAlarmInfo();

    if(alarm_flag)
    {
    alarm_flag = 0;
    printf("alarm interrupt trigger!\\n");
    }
    HAL_Delay(1000);
    }
    }

    ④ 中断回调:闹钟到达 → 置标志 + 点亮 LED0

    void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
    {
    if(hrtc == &hrtc)
    {
    alarm_flag = 1; // 通知主循环
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_RESET); // 点亮 LED0
    }
    }

    运行效果:

    RTC Test Example:
    初始化完成
    Current Time: 12:00:00
    Alarm Time: 12:00:10
    Current Time: 12:00:01
    Alarm Time: 12:00:10
    …
    Current Time: 12:00:10 ← 时间到达闹钟时间
    Alarm Time: 12:00:10
    alarm interrupt trigger! ← 闹钟中断触发!

    现象:10 秒之后,时间到达闹钟时间,产生闹钟中断,并点亮了 LED0。

    2.5 顺手验证秒中断

    HAL_StatusTypeDef HAL_RTCEx_SetSecond_IT(RTC_HandleTypeDef *hrtc);

            启动 RTC 秒中断(对应 RTC global interrupt,处理函数 RTC_IRQHandler,中断号 3)。

    测试代码现场写:

            使能秒中断后,每 1 秒触发一次中断,可在 RTC_IRQHandler 回调里翻转 LED 或打印。


    三、附录:时钟树、LSE 位置、石英钟历史、时间从哪里来

    3.1 时钟树第二部分:树形类比

    把时钟树想象成一棵树:

    树叶
    ┌──────────┴──────────┐
    RTC 独立看门狗(IWDG)
    RTCCLK IWDGCLK
    └──────────┬──────────┘
    选择器(树干)
    ┌──────────┴──────────┐
    /128 分频 直连
    │ │
    HSE(8MHz) LSI(40kHz) LSE(32.768kHz)
    (树根,三个时钟源,都是真硬件)

    • 树根:HSE(8MHz 外部)、LSE(32.768kHz 外部)、LSI(40kHz 内部)三个时钟源;
    • 树干:选择器 + /128 分频;
    • 树枝:RTCCLK(给 RTC)、IWDGCLK(给独立看门狗)——只有两根树枝;
    • 树叶:RTC 和 IWDG 两个外设——只有两片树叶。

    关键规则:

            APB1 预分频系数≠1 时定时器时钟频率不变;

            APB1 最大 36MHz、APB2 最大 72MHz、HCLK 最大 72MHz(老知识复习)。

    3.2 LSE 在什么位置:它是真硬件

    之前说的"时钟源",在 MCU 或板子上是真真实实的硬件——灵犀板上:

    • 32.768kHz 晶振:型号 Q13FC13500004_C32346(配 20pF 电容,接 PC14-OSC32_IN / PC15-OSC32_OUT);
    • 8MHz 晶振:型号 X50328MSB2GI(配 20pF 电容,接 OSC_IN/OSC_OUT)。

    这就是为什么 LSE 断电也能走:晶振是无源谐振器,对应振荡电路在 MCU 内部(类似后备域),LSE 的振荡电路可以在 VBAT 下工作。

    3.3 石英钟历史:32.768kHz 是怎么成为标准的

    (课件为 AI 生成内容,总结如下)

    阶段时间事件
    早期探索 1928 年 美国贝尔电话实验室展示世界上第一台石英钟,证明石英晶体高精度计时的可行性
    先驱探索 1960 年代 瑞士、日本研究机构开发石英音叉电子手表原型
    业界认可 1969 年 日本精工(Seiko)推出世界上第一款商用石英腕表,开启石英表时代
    标准确立 随后 芝柏(Girard-Perregaux)等品牌机芯明确采用 32,768Hz——能精准分频出 1Hz 秒信号、功耗和体积达到完美平衡,成为行业事实标准
    低成本量产 20 世纪 70 年代 尤尔根·斯托特(Juergen H. Staudte)发明光刻工艺批量生产微型音叉晶振,大规模低成本普及

    生态推测:

            32.768kHz 在石英手表领域优先使用 → 形成产业链 → 造出来的晶振大多是 32.768kHz → 单片机振荡器选型时直接拿来用。

            32.768kHz = 2^15 Hz,正好方便分频成 1Hz 秒脉冲。

    3.4 时间从哪里来:NTP 协议

    MCU 的初始时间更多是从网络中同步来的:

    • 一般走 4G 基站或以太网,通过 NTP 协议获取时间,然后进行时间校正;
    • NTP(Network Time Protocol,网络时间协议):互联网上同步计算机系统时间的标准协议,解决不同设备时间不一致的核心问题;
    • 可以想象成一个遍布全球、极为精确的"网络报时系统":
      • 开发板 MCU → Wi-Fi 模块(STA,SSID/密码)或以太网口(RJ45 + PHY)→ 路由器/AP(DHCP/NAT)→ 互联网(NTP/UDP)→ NTP 服务器(公共或局域网,默认 UDP 123 端口)→ 返回 UTC 时间戳 → 时间同步送给 MCU 程序。

    注意:

            STM32F103 中用内置 RTC 的很少,一般都有独立 RTC 模块做精准计时。

            我们学内置 RTC,是为了理解什么是 RTC、RTC 用来干什么,学完遇到外置的也更好理解。

    常见外置 RTC 芯片/模块:DS1302(RTC 芯片)、DS3231(RTC 模块,精度高带温补)、PCF8563(RTC 芯片/模块)——一般通过 IIC 通信。


    四、本篇小结

    1. F1 无日期寄存器:HAL_RTC_SetDate 只写内存 DateToUpdate → 复位日期丢(2000-01-01)
    F0/F4 有独立日期寄存器,无此问题
    2. 方案二选一:备份寄存器存年月日(不推荐) vs UNIX 时间戳(采用)
    3. 时间戳 = 1970-01-01 00:00:00 起的秒数;有符号32位2038溢出,RTC_CNT无符号可到2106
    溢出是工程选择:让溢出时点晚于产品设计寿命即可
    4. DateTime_t 双向换算:闰年/月天数/合法性校验 + 秒→日期、日期→秒
    5. 闹钟:RTC_AlarmTypeDef(AlarmTime + RTC_ALARM_A) → HAL_RTC_SetAlarm_IT(带中断)
    → CNT==ALR → EXTI17 → HAL_RTC_AlarmAEventCallback → 置标志/点灯
    6. 两个中断:RTC global(秒/溢出, IRQn=3, RTC_IRQHandler);EXTI17(闹钟, IRQn=41, RTC_Alarm_IRQHandler, 可唤醒睡眠)
    7. 附录:时钟树树形类比、LSE 是真晶振、32.768kHz 石英表历史、NTP 网络校时、外置 RTC 芯片


    面试题

    一、日期操作

    Q1. RTC_DateTypeDef 结构体有哪些成员?Year 的取值范围和含义?

    A:

            WeekDay(星期,SetDate 不需要)、Month(月份,BCD 格式)、Date(日期 131)、Year(年份 099,对应 2000~2099 年,.Year = 35 表示 2035 年)。


    Q2. Month 为什么用 BCD 码?十月的宏定义为什么是 0x10 而不是 0x0A?

    A:

            HAL 要求 Month 用 BCD 码设置(RTC_MONTH_OCTOBER = 0x10)。

            因为 BCD 码每个十进制位独立占 4 位:10 = "1"和"0" → 0001 0000 = 0x10;0x0A 是非法 BCD(1010 不在 0~9 范围内)。


    Q3. 为什么 STM32F1 的日期复位后会丢?

    A:

            F1 没有日期寄存器,HAL_RTC_SetDate 只是把年月日写进内存中的 hrtc->DateToUpdate 结构体(源码:hrtc->DateToUpdate.Year = sDate->Year;),复位后 RAM 清空,日期变回 2000-01-01;而 RTC_CNT 在备份域(VBAT 供电),时间秒数不丢。F0/F4 有独立日期寄存器,无此问题。


    Q4. 日期丢失问题的两种解决方案?选哪种?

    A:

            ① 用后备寄存器保存年月日——资源多、麻烦,不推荐;

            ② 放弃 SetDate/GetDate,改用 UNIX 时间戳——把日期折算成秒数存进 RTC_CNT(备份域),需要时再转回年月日,采用。


    Q5. 什么是 UNIX 时间戳?

    A:

            以 1970 年 1 月 1 日 0 时 0 分 0 秒为基准,当前时刻与基准时刻的秒数之差,用一个整数表示时间。时间是线性递增的,时间戳也是线性增长的数字:当前时间 = 基准时刻 + 偏移量 N。


    Q6. 为什么标准 UNIX 时间戳 2038 年溢出,而 RTC_CNT 能到 2106 年?

    A:

            标准 UNIX 时间戳用有符号 32 位整数(31 位有效),2^31 秒 ≈ 2038 年;

            RTC_CNT 是无符号 32 位整数(32 位有效),2^32 秒 ≈ 2106 年。


    Q7. 怎么看待时间戳溢出问题?

    A:

            时间须存储必占有限位宽,位宽必致上限,上限必生溢出。

            溢出不可免,所以时间设置是工程选择:让溢出时点晚于产品设计寿命即可,不必恐慌。


    Q8. 为什么选 1970 年 1 月 1 日作为时间基准?

    A:

            UNIX 操作系统研发于 20 世纪 60~70 年代,开发者(C 语言之父丹尼斯·里奇和肯·汤姆逊,C 语言是开发 UNIX 时"顺便"设计的)选择了 1970-01-01 作为基准,成为计算机"数字纪元"的起点。


    Q9. DateTime_To_Timestamp 的算法步骤?

    A:

            ① 校验合法性(Is_Valid_DateTime);

            ② 累加 1970 到目标年份前一年的天数(闰年 366 否则 365);

            ③ 累加 1 月到目标月份前一月的天数(Get_Days_In_Month);

            ④ 加上 day-1;⑤ 总天数 ×86400 + 时×3600 + 分×60 + 秒。


    Q10. Timestamp_To_DateTime 怎么算星期几?

    A:

            1970-01-01 是星期四,所以 weekday = (4 + seconds/86400) % 7(0=周日)。


    Q11. 闰年判断规则?

    A:

      (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)

            能被 4 整除且不能被 100 整除,或能被 400 整除。

    二、闹钟操作

    Q12. RTC_AlarmTypeDef 结构体是什么?Alarm 字段干什么用?

    A:

            包含 AlarmTime(RTC_TimeTypeDef,闹钟具体时间)和 Alarm(闹钟 ID)。

            HAL 库为多闹钟 MCU 设计,需区分设置的是哪个闹钟;

            STM32F103 只有 RTC_ALARM_A(0U),所以设完不用关心。


    Q13. HAL_RTC_SetAlarm 和 HAL_RTC_SetAlarm_IT 的区别?

    A:

            都能设置闹钟;

            SetAlarm 时间到了不触发中断,需要额外写代码(如循环读闹钟寄存器);        

            SetAlarm_IT 时间到了自动触发 RTC_Alarm 中断。

            优先用 _IT 版本。注意必须先调用 HAL_RTC_SetTime 再使能闹钟。


    Q14. RTC 的两个中断分别是什么?处理函数和中断号?

    A:

            ① RTC global interrupt:RTC_Second/RTC_Overflow 触发,处理函数 RTC_IRQHandler,中断号 3;

            ② RTC alarm interrupt through EXTI line 17:只 RTC_Alarm 触发,处理函数 RTC_Alarm_IRQHandler,中断号 41,能唤醒深度睡眠。

            两者在软件上分开处理。


    Q15. 闹钟到点的完整链路?

    A:

            RTC_CNT 每秒 +1 → 与 RTC_ALR 相等 → 硬件产生闹钟事件 → EXTI line 17 触发中断 → RTC_Alarm_IRQHandler → HAL 回调 HAL_RTC_AlarmAEventCallback → 置 alarm_flag、点亮 LED0。


    Q16. 为什么主循环里要用 volatile 修饰 alarm_flag?

    A:

            alarm_flag 在中断回调里修改、主循环里读取,volatile 防止编译器把它优化到寄存器里而读不到中断的最新值,保证每次循环都从内存重新读取。


    Q17. 秒中断用哪个函数使能?

    A:

      HAL_RTCEx_SetSecond_IT(&hrtc),对应 RTC global interrupt,每秒触发一次,处理函数 RTC_IRQHandler。


    赞(0)
    未经允许不得转载:171主机测评 » 【STM32】RTC 完结篇:日期操作、闹钟中断与附录(时间戳 / 闹钟 / NTP)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址