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


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

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

1.1 案例目标

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

1.2 日期结构体 RTC_DateTypeDef

cpp 复制代码
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;

两个关键约定:

  1. Year 是 0~99,起始年份是 2000 年------所以 .Year = 35 表示 2035 年;
  2. Month 必须用 BCD 码设置------注意十月、十一月、十二月:
cpp 复制代码
/* 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 内部:

cpp 复制代码
/* Change the current date */
hrtc->DateToUpdate.Year  = sDate->Year;
hrtc->DateToUpdate.Month = sDate->Month;
hrtc->DateToUpdate.Date  = sDate->Date;

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

而 RTC_HandleTypeDef 里的这个字段:

cpp 复制代码
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 控制只设一次)

cpp 复制代码
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:获取时间日期并格式化打印

cpp 复制代码
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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
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:读时间戳转回日期时间

cpp 复制代码
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);
}

运行效果:

cpp 复制代码
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 准备工作

  1. 创建新项目;
  2. 重定位 printf、连接串口;
  3. 启用 PF8 引脚:配置为推挽输出、输出高电平(点亮板载 LED0,注意这是低电平点亮还是高电平点亮以板子为准------课件配图 GPIO output level 设为 High,中断回调里写 GPIO_PIN_RESET 点亮,即 PF8 低电平点亮);
  4. 配置 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

cpp 复制代码
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 触发)

cpp 复制代码
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 通信。


四、本篇小结

cpp 复制代码
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。


相关推荐
zincsweet1 小时前
Linux网络编程入门(二):从 UDP 回显服务器到多人聊天室,三个项目的渐进式实践
linux·服务器·网络
深圳老胡1 小时前
STM32C5 应用阶段开发难点解析:从选型到量产的实战经验
笔记·stm32·单片机·代码规范
李永奉1 小时前
杰理可视化SDK开发-【BUG】AC7006F8芯片开启一拖二和双麦通话语音算法后,设置触摸音在唤醒语音助手和通话时触摸音没作用问题
单片机·嵌入式硬件·mcu·物联网·电脑·bug
贵州诚鑫致达科技有限公司1 小时前
接入汇聚都是环怎么防环路?华为SEP加RRPP混合环
服务器·华为·php
学逆向的1 小时前
C 悬空指针
c语言·开发语言·网络安全
syagain_zsx1 小时前
BRpc 搭建简单 rpc 服务器
服务器·c++·网络协议·rpc·protobuf·库
云飞云共享云桌面1 小时前
一台SolidWorks设计服务器多人共享落地方案和配置
运维·服务器·3d·电脑·制造
Pointer Pursuit1 小时前
进程优先级
linux·运维·服务器
Cry丶11 小时前
ubuntu安装vsftpd
服务器·网络·ubuntu