STM32 进阶封神之路(十八):RTC 实战全攻略 —— 时间设置 + 秒中断 + 串口更新 + 闹钟功能(库函数 + 代码落地)

0 阅读16分钟

STM32 进阶封神之路(十八):RTC 实战全攻略 —— 时间设置 + 秒中断 + 串口更新 + 闹钟功能(库函数 + 代码落地)

上一篇我们吃透了 RTC 的底层原理、时钟源选型和寄存器配置,这一篇聚焦实战落地 —— 基于 STM32F103,从时间初始化、秒中断实时刷新,到串口指令更新时间、闹钟功能实现,再到备份寄存器数据存储,手把手带你搭建完整的 RTC 实时时钟系统,所有代码基于提供的rtc.h核心函数扩展,可直接编译运行!

本文结合实战场景,覆盖 “时间设置→实时显示→动态更新→定时告警” 全流程,同时解析代码中的关键细节和避坑要点,让你不仅 “会配置 RTC”,更能 “灵活应用 RTC” 到实际项目中!

一、实战准备:硬件环境与核心代码说明

1. 硬件环境要求

  • 主控:STM32F103C8T6 最小系统板;
  • 时钟源:外接 32.768KHz LSE 晶振(含 12.5pF 负载电容);
  • 备用电源:VBAT 引脚接 CR2032 纽扣电池(确保掉电时间不丢失);
  • 调试工具:USB-TTL 模块(CH340G),用于串口打印时间和发送更新指令;
  • 辅助硬件:LED(用于闹钟功能指示)。

2. 核心代码依赖说明

本文基于提供的rtc.h代码扩展,核心函数分工如下:

表格

函数名称核心功能调用场景
RTC_Config初始化 RTC(解锁备份域、配置 LSE、使能秒中断)系统启动时初始化
Time_Adjuststruct tm结构体时间转换为时间戳,写入 RTC 计数器初始时间设置、串口更新时间
RTC_IRQHandlerRTC 中断服务函数,触发秒中断标志位每秒自动触发
RTC_Handle解析串口指令,更新 RTC 时间接收电脑串口指令时
Time_Get读取 RTC 计数器值,转换为时间结构体并打印秒中断触发后,实时显示时间

3. 关键数据结构与全局变量解析

c

运行

#include "rtc.h"
#include "stm32f10x.h"
#include <stdio.h>
#include <time.h>

// 秒中断标志位(1=需更新时间)
uint8_t RTC_Second_FLAG=0; 
// 待设置时间结构体(初始时间:2026-03-18 17:33:00)
struct tm Set_Time= {0,33,17,18,3-1,2026-1900}; 
// 待设置时间戳(总秒数)
uint32_t Set_Time_Cnt; 
// 当前时间戳
uint32_t Now_Time_Cnt; 
// 当前时间结构体指针(指向Set_Time,复用内存)
struct tm *Now_Time=&Set_Time; 

// 串口接收相关全局变量(用于接收时间更新指令)
#define U1_REC_BUFF_LEN 32
uint8_t U1_Rec_Buff[U1_REC_BUFF_LEN]; // 串口接收缓冲区
uint16_t U1_Rec_Cnt = 0; // 接收字节数
uint8_t U1_Rec_Idle = 0; // 接收完成标志位
  • struct tm结构体:标准 C 库时间结构体,存储年、月、日、时、分、秒,注意:

    • tm_sec:秒(0~59);
    • tm_min:分(0~59);
    • tm_hour:时(0~23);
    • tm_mday:日(1~31);
    • tm_mon:月(0~11,需 + 1 转换为实际月份);
    • tm_year:年(从 1900 年开始的偏移量,需 + 1900 转换为实际年份)。

二、实战 1:RTC 初始化与初始时间设置

核心目标:系统启动时初始化 RTC,设置默认时间(2026-03-18 17:33:00),并通过串口打印实时时间。

1. 串口初始化(配合 RTC 数据打印)

RTC 的时间显示和指令接收依赖串口,需先初始化 USART1(115200bps 8N1):

c

运行

// USART1初始化:115200bps,8N1,中断接收
void UART1_Config(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    // 使能GPIOA和USART1时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);

    // 配置PA9(TX)为复用推挽输出
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 配置PA10(RX)为浮空输入
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 配置USART1参数
    USART_InitStruct.USART_BaudRate = 115200;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART1, &USART_InitStruct);

    // 使能USART1接收中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    // 配置NVIC中断优先级
    NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 使能USART1
    USART_Cmd(USART1, ENABLE);
}

// 串口发送字节
void USART1_SendByte(uint8_t data) {
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, data);
}

// printf重定向
int fputc(int ch, FILE *f) {
    USART1_SendByte((uint8_t)ch);
    return ch;
}

// 串口接收中断服务函数(存储接收数据)
void US_t rx_data;
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        rx_data = (uint8_t)USART_ReceiveData(USART1);
        if(U1_Rec_Cnt < U1_REC_BUFF_LEN) {
            U1_Rec_Buff[U1_Rec_Cnt++] = rx_data;
            // 检测帧尾(假设指令帧为8字节,接收满8字节触发处理)
            if(U1_Rec_Cnt == 8) {
                U1_Rec_Idle = 1; // 标记接收完成
            }
        } else {
            U1_Rec_Cnt = 0; // 缓冲区溢出,重置
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

// 清空串口接收缓冲区
void UART1_Clear_R_Buff(void) {
    memset(U1_Rec_Buff, 0, sizeof(U1_Rec_Buff));
    U1_Rec_Cnt = 0;
    U1_Rec_Idle = 0;
}

2. 主函数初始化与时间打印

c

运行

int main(void) {
    // 初始化串口1(用于打印和指令接收)
    UART1_Config();
    // 初始化RTC(LSE时钟源,秒中断使能)
    RTC_Config();

    printf("RTC实时时钟系统初始化成功!\r\n");
    printf("初始时间:2026-03-18 17:33:00\r\n");
    printf("支持功能:\r\n");
    printf("1. 每秒自动打印当前时间\r\n");
    printf("2. 串口发送指令更新时间(帧格式:0x5A 0x06 YY MM DD HH MM 校验和)\r\n");
    printf("=======================================\r\n\r\n");

    while(1) {
        // 处理串口时间更新指令
        RTC_Handle();
        // 秒中断触发后,打印当前时间
        Time_Get();
    }
}

3. 运行效果

系统启动后,串口助手(115200bps 8N1)接收如下信息,每秒刷新一次时间:

plaintext

RTC实时时钟系统初始化成功!
初始时间:2026-03-18 17:33:00
支持功能:
1. 每秒自动打印当前时间
2. 串口发送指令更新时间(帧格式:0x5A 0x06 YY MM DD HH MM 校验和)
=======================================

 当前时间2026-03-18 17:33:00
 当前时间2026-03-18 17:33:01
 当前时间2026-03-18 17:33:02

三、实战 2:串口指令更新 RTC 时间(动态校准)

核心目标:通过电脑串口发送指定格式的指令,动态更新 RTC 时间(解决 LSE 晶振长期计时误差问题)。

1. 串口指令格式说明(关键!)

提供的RTC_Handle函数定义了固定指令帧格式,需严格遵循:

表格

字节序号字节内容说明示例值(更新为 2026-03-19 09:45:00)
00x5A帧头(固定)0x5A
10x06数据长度(后续 5 个数据字节 + 1 个校验和字节?不,实际数据长度为 5)0x06
2YY年份(2000+YY)0x26(2026 年)
3MM月份(1~12)0x03(3 月)
4DD日期(1~31)0x13(19 日)
5HH小时(0~23)0x09(9 时)
6MM分钟(0~59)0x2D(45 分)
7校验和前 7 字节累加和的低 8 位0x5A+0x06+0x26+0x03+0x13+0x09+0x2D=0xCC

2. 指令发送与解析逻辑

(1)指令发送步骤
  1. 打开串口助手,选择 “十六进制发送”;
  2. 输入指令帧:5A 06 26 03 13 09 2D CC(对应 2026-03-19 09:45:00);
  3. 点击发送,STM32 接收后解析指令,更新 RTC 时间。
(2)RTC_Handle函数解析

函数核心逻辑:校验帧格式→计算校验和→解析时间字段→更新 RTC 计数器:

c

运行

void RTC_Handle(void) {
    if(U1_Rec_Idle == 1) {
        U1_Rec_Idle = 0;
        // 1. 验证帧格式:帧头0x5A + 长度06 + 总长度8字节
        if(U1_Rec_Cnt == 8 && U1_Rec_Buff[0] == 0x5A && U1_Rec_Buff[1] == 0x06) {
            // 2. 计算校验和(从第0字节到第6字节累加,取低8位)
            uint8_t check_sum = 0;
            for(uint8_t i = 0; i < 7; i++) {
                check_sum += U1_Rec_Buff[i];
            }

            // 3. 校验和验证
            if(check_sum == U1_Rec_Buff[7]) {
                // 4. 解析时间字段(转换为struct tm格式)
                uint8_t year = U1_Rec_Buff[2];   // 年份=2000+26=2026
                uint8_t month = U1_Rec_Buff[3];  // 月份=3(直接使用,后续-1转换为0~11)
                uint8_t day = U1_Rec_Buff[4];    // 日期=19
                uint8_t hour = U1_Rec_Buff[5];   // 小时=9
                uint8_t min = U1_Rec_Buff[6];    // 分钟=45

                // 5. 更新时间结构体(适配struct tm格式)
                Set_Time.tm_sec = 0;              // 秒默认设为0
                Set_Time.tm_min = min;
                Set_Time.tm_hour = hour;
                Set_Time.tm_mday = day;
                Set_Time.tm_mon = month - 1;     // 月份转换为0~11(3→2)
                Set_Time.tm_year = (2000 + year) - 1900; // 2026-1900=126

                // 6. 将更新后的时间写入RTC计数器
                Time_Adjust();
                // 7. 串口打印更新结果
                printf("RTC时间更新成功:%04d-%02d-%02d %02d:%02d:00\r\n",
                       2000+year, month, day, hour, min);
            } else {
                printf("RTC更新失败:校验和错误(接收:0x%02X,计算:0x%02X)\r\n",
                       U1_Rec_Buff[7], check_sum);
            }
        } else {
            printf("RTC更新失败:帧格式错误(长度:%d,帧头:0x%02X)\r\n",
                   U1_Rec_Cnt, U1_Rec_Buff[0]);
        }
        // 清空缓冲区,准备接收下一帧
        UART1_Clear_R_Buff();
    }
}

3. 运行效果

发送指令后,串口助手接收如下信息,时间同步更新:

plaintext

RTC时间更新成功:2026-03-19 09:45:00
 当前时间2026-03-19 09:45:00
 当前时间2026-03-19 09:45:01

四、实战 3:RTC 闹钟功能实现(定时告警)

核心目标:配置 RTC 闹钟时间,当达到指定时间时触发闹钟中断,控制 LED 闪烁告警。

1. 硬件连接

  • LED 正极→PB0(推挽输出)→1KΩ 限流电阻→GND;
  • 核心逻辑:闹钟中断触发后,翻转 PB0 电平,实现 LED 闪烁。

2. 闹钟功能代码扩展

(1)GPIO 初始化(LED 控制)

c

运行

// LED GPIO初始化(PB0)
void LED_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;

    // 使能GPIOB时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    // 配置PB0为推挽输出
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 初始状态:LED熄灭
    GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
(2)闹钟配置函数(添加到rtc.h

c

运行

/**
  * @brief  配置RTC闹钟时间
  * @param  alarm_time:闹钟时间结构体(年、月、日、时、分、秒)
  * @retval 无
  */
void RTC_SetAlarm(struct tm alarm_time) {
    uint32_t alarm_cnt;

    // 1. 等待RTC写操作完成
    RTC_WaitForLastTask();

    // 2. 将闹钟时间转换为时间戳
    alarm_cnt = mktime(&alarm_time);

    // 3. 设置RTC闹钟寄存器(ALRH/ALRL)
    RTC->ALRL = (uint16_t)alarm_cnt;          // 闹钟计数器低16位
    RTC->ALRH = (uint16_t)(alarm_cnt >> 16);  // 闹钟计数器高16位

    // 4. 使能RTC闹钟中断
    RTC_ITConfig(RTC_IT_ALR, ENABLE);

    // 5. 等待配置生效
    RTC_WaitForLastTask();

    // 打印闹钟配置结果
    printf("闹钟配置成功:%04d-%02d-%02d %02d:%02d:%02d\r\n",
           alarm_time.tm_year+1900, alarm_time.tm_mon+1, alarm_time.tm_mday,
           alarm_time.tm_hour, alarm_time.tm_min, alarm_time.tm_sec);
}
(3)修改中断服务函数,处理闹钟中断

c

运行

void RTC_IRQHandler(void) {
    // 1. 秒中断处理
    if (RTC_GetITStatus(RTC_IT_SEC) != RESET) {
        RTC_Second_FLAG=1;
        RTC_ClearITPendingBit(RTC_IT_SEC);
        RTC_WaitForLastTask();
    }

    // 2. 闹钟中断处理
    if (RTC_GetITStatus(RTC_IT_ALR) != RESET) {
        GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0)));
        printf("闹钟触发!当前时间:%04d-%02d-%02d %02d:%02d:%02d\r\n",
               Now_Time->tm_year+1900, Now_Time->tm_mon+1, Now_Time->tm_mday,
               Now_Time->tm_hour, Now_Time->tm_min, Now_Time->tm_sec);
        RTC_ClearITPendingBit(RTC_IT_ALR);
        RTC_WaitForLastTask();
    }
}
(4)主函数中配置闹钟

c

运行

int main(void) {
    // 初始化LED(闹钟指示)
    LED_Init();
    // 初始化串口    RTC_Config();

    // 配置闹钟时间:2026-03-19 09:46:00
    struct tm alarm_time = {0, 46, 9, 19, 3-1, 2026-1900};
    RTC_SetAlarm(alarm_time);

    // 后续主循环(同前序代码)
    printf("RTC实时时钟系统初始化成功!\r\n");
    while(1) {
        RTC_Handle();
        Time_Get();
    }
}

3. 运行效果

  • 闹钟时间到达时,LED 开始闪烁(每秒翻转一次);
  • 串口打印告警信息:

plaintext

闹钟触发!当前时间:2026-03-19 09:46:00
 当前时间2026-03-19 09:46:00
闹钟触发!当前时间:2026-03-19 09:46:01
 当前时间2026-03-19 09:46:01

五、实战 4:备份寄存器(BKP)数据存储(掉电不丢失)

核心目标:使用 RTC 备份域的 BKP 寄存器存储用户数据(如闹钟开关状态),主电源关闭后,备用电源持续供电,数据不丢失。

1. BKP 数据存储函数(添加到rtc.h

c

运行

/**
  * @brief  向BKP寄存器写入数据(掉电不丢失)
  * @param  dr:备份寄存器编号(1~10)
  * @param  data:待存储数据(16位)
  * @retval 无
  */
void BKP_WriteData(uint8_t dr, uint16_t data) {
    // 1. 等待RTC写操作完成
    RTC_WaitForLastTask();
    // 2. 写入备份寄存器(BKP_DR1~BKP_DR10)
    switch(dr) {
        case 1: BKP->DR1 = data; break;
        case 2: BKP->DR2 = data; break;
        case 3: BKP->DR3 = data; break;
        case 4: BKP->DR4 = data; break;
        case 5: BKP->DR5 = data; break;
        case 6: BKP->DR6 = data; break;
        case 7: BKP->DR7 = data; break;
        case 8: BKP->DR8 = data; break;
        case 9: BKP->DR9 = data; break;
        case 10: BKP->DR10 = data; break;
        default: break;
    }
    // 3. 等待操作完成
    RTC_WaitForLastTask();
}

/**
  * @brief  从BKP寄存器读取数据
  * @param  dr:备份寄存器编号(1~10)
  * @retval 存储的数据(16位)
  */
uint16_t BKP_ReadData(uint8_t dr) {
    // 等待RTC同步完成
    RTC_WaitForSynchro();
    // 读取对应寄存器
    switch(dr) {
        case 1: return BKP->DR1;
        case 2: return BKP->DR2;
        case 3: return BKP->DR3;
        case 4: return BKP->DR4;
        case 5: return BKP->DR5;
        case 6: return BKP->DR6;
        case 7: return BKP->DR7;
        case 8: return BKP->DR8;
        case 9: return BKP->DR9;
        case 10: return BKP->DR10;
        default: return 0;
    }
}

2. 实战应用:存储闹钟开关状态

c

运行

int main(void) {
    uint16_t alarm_en;

    LED_Init();
    UART1_Config();
    RTC_Config();

    // 从BKP_DR1读取闹钟开关状态(1=开启,0=关闭)
    alarm_en = BKP_ReadData(1);
    if(alarm_en == 1) {
        // 之前开启过闹钟,恢复配置
        struct tm alarm_time = {0, 46, 9, 19, 3-1, 2026-1900};
        RTC_SetAlarm(alarm_time);
    } else {
        // 首次启动,默认开启闹钟并存储状态
        struct tm alarm_time = {0, 46, 9, 19, 3-1, 2026-1900};
        RTC_SetAlarm(alarm_time);
        BKP_WriteData(1, 1); // 存储开启状态到BKP_DR1
        printf("首次启动,默认开启闹钟并存储状态\r\n");
    }

    while(1) {
        RTC_Handle();
        Time_Get();
    }
}

3. 验证效果

  • 首次启动:开启闹钟,存储状态到 BKP_DR1;
  • 断开主电源,等待 10 秒后重新上电;
  • 系统重启后,自动从 BKP_DR1 读取状态,恢复闹钟配置,无需重新设置。

六、RTC 实战避坑指南(10 + 高频错误)

1. RTC 初始化失败,时间不更新

  • 原因 1:未解除备份域写保护→无法修改 RTC 寄存器;解决:确保初始化流程中调用PWR_BackupAccessCmd(ENABLE)
  • 原因 2:LSE 晶振未稳定→直接配置 RTC 时钟源;解决:必须等待RCC_GetFlagStatus(RCC_FLAG_LSERDY) == SET后再继续;
  • 原因 3:预分频器配置错误→未分频为 1Hz;解决:LSE=32.768KHz 时,预分频器值必须为 32767(RTC_SetPrescaler(32767))。

2. 串口更新时间失败

  • 原因 1:指令帧格式错误→帧头、长度或校验和不匹配;解决:严格按0x5A 0x06 YY MM DD HH MM 校验和格式发送,确保校验和正确;
  • 原因 2:struct tm格式转换错误→月份或年份未调整;解决:月份需 - 1(0~11),年份需转换为 1900 年偏移量(2026→126)。

3. 闹钟不触发

  • 原因 1:未使能闹钟中断→RTC_ITConfig(RTC_IT_ALR, ENABLE)未调用;解决:配置闹钟后,必须使能闹钟中断;
  • 原因 2:闹钟时间戳计算错误→mktime函数参数错误;解决:确保struct tm结构体的各字段在合法范围(如小时 023,日期 131);
  • 原因 3:NVIC 未配置闹钟中断优先级→中断无法响应;解决:在RTC_Config中添加闹钟中断的 NVIC 配置(类似秒中断)。

4. 掉电后时间丢失

  • 原因 1:未接备用电源(VBAT)→主电源关闭后 RTC 断电;解决:VBAT 引脚接 CR2032 纽扣电池,确保电压在 2.0V~3.6V;
  • 原因 2:备份域复位→误操作RCC_BDCR的 BDRST 位;解决:初始化时仅复位一次,后续避免调用BKP_DeInit()

5. 时间走时不准

  • 原因 1:LSE 晶振负载电容不匹配→晶振频率偏移;解决:使用 12.5pF~20pF 的负载电容,严格按晶振 datasheet 配置;
  • 原因 2:未定期校准→长期使用后晶振误差累积;解决:通过串口指令定期更新时间,或使用 BKP 存储校准参数。

七、总结:RTC 实战核心要点与进阶方向

1. 核心要点回顾

  • RTC 实战核心流程:初始化(解锁备份域 + LSE 配置)→时间设置(初始 / 串口更新)→实时显示(秒中断)→功能扩展(闹钟 + BKP);
  • 关键函数:Time_Adjust(时间戳写入)、RTC_IRQHandler(中断处理)、RTC_Handle(串口更新)、BKP_WriteData(掉电存储);
  • 避坑核心:备份域解锁、LSE 稳定等待、struct tm格式转换、中断与 NVIC 配置。

2. 进阶学习方向

  • 低功耗唤醒:RTC 闹钟中断唤醒 STM32 深度睡眠模式,降低功耗;
  • 时间校准:通过网络(如 NTP)或 GPS 自动校准 RTC 时间,消除晶振误差;
  • 多闹钟配置:扩展支持多个闹钟时间,实现复杂定时任务;
  • 日志记录:结合 SD 卡,使用 RTC 时间戳记录设备事件日志(如传感器采集时间);
  • 时区支持:添加时区转换功能,适配不同地区时间显示。

掌握 RTC 实战后,你已具备嵌入式设备 “时间管理” 的核心能力,可应用于智能手表、数据记录仪、定时控制器等场景。下一篇我们将学习 SPI 通信,实现 STM32 与 W25Q64 闪存芯片的数据存储,进一步拓展嵌入式系统的存储能力!