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_Adjust | 将struct tm结构体时间转换为时间戳,写入 RTC 计数器 | 初始时间设置、串口更新时间 |
RTC_IRQHandler | RTC 中断服务函数,触发秒中断标志位 | 每秒自动触发 |
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) |
|---|---|---|---|
| 0 | 0x5A | 帧头(固定) | 0x5A |
| 1 | 0x06 | 数据长度(后续 5 个数据字节 + 1 个校验和字节?不,实际数据长度为 5) | 0x06 |
| 2 | YY | 年份(2000+YY) | 0x26(2026 年) |
| 3 | MM | 月份(1~12) | 0x03(3 月) |
| 4 | DD | 日期(1~31) | 0x13(19 日) |
| 5 | HH | 小时(0~23) | 0x09(9 时) |
| 6 | MM | 分钟(0~59) | 0x2D(45 分) |
| 7 | 校验和 | 前 7 字节累加和的低 8 位 | 0x5A+0x06+0x26+0x03+0x13+0x09+0x2D=0xCC |
2. 指令发送与解析逻辑
(1)指令发送步骤
- 打开串口助手,选择 “十六进制发送”;
- 输入指令帧:
5A 06 26 03 13 09 2D CC(对应 2026-03-19 09:45:00); - 点击发送,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 闪存芯片的数据存储,进一步拓展嵌入式系统的存储能力!