STM32 进阶封神之路(十八):RTC 实战全攻略 —— 时间设置 + 秒中断 + 串口更新 + 闹钟功能(库函数 + 代码落地)
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_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_FLAG0; // 待设置时间结构体初始时间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_TimeSet_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~59tm_min分0~59tm_hour时0~23tm_mday日1~31tm_mon月0~11需 1 转换为实际月份tm_year年从 1900 年开始的偏移量需 1900 转换为实际年份。二、实战 1RTC 初始化与初始时间设置核心目标系统启动时初始化 RTC设置默认时间2026-03-18 17:33:00并通过串口打印实时时间。1. 串口初始化配合 RTC 数据打印RTC 的时间显示和指令接收依赖串口需先初始化 USART1115200bps 8N1c运行// USART1初始化115200bps8N1中断接收 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); // 配置PA9TX为复用推挽输出 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); // 配置PA10RX为浮空输入 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(); // 初始化RTCLSE时钟源秒中断使能 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接收如下信息每秒刷新一次时间plaintextRTC实时时钟系统初始化成功 初始时间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:0000x5A帧头固定0x5A10x06数据长度后续 5 个数据字节 1 个校验和字节不实际数据长度为 50x062YY年份2000YY0x262026 年3MM月份1~120x033 月4DD日期1~310x1319 日5HH小时0~230x099 时6MM分钟0~590x2D45 分7校验和前 7 字节累加和的低 8 位0x5A0x060x260x030x130x090x2D0xCC2. 指令发送与解析逻辑1指令发送步骤打开串口助手选择 “十六进制发送”输入指令帧5A 06 26 03 13 09 2D CC对应 2026-03-19 09:45:00点击发送STM32 接收后解析指令更新 RTC 时间。2RTC_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]; // 年份2000262026 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~113→2 Set_Time.tm_year (2000 year) - 1900; // 2026-1900126 // 6. 将更新后的时间写入RTC计数器 Time_Adjust(); // 7. 串口打印更新结果 printf(RTC时间更新成功%04d-%02d-%02d %02d:%02d:00\r\n, 2000year, 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. 运行效果发送指令后串口助手接收如下信息时间同步更新plaintextRTC时间更新成功2026-03-19 09:45:00 当前时间2026-03-19 09:45:00 当前时间2026-03-19 09:45:01四、实战 3RTC 闹钟功能实现定时告警核心目标配置 RTC 闹钟时间当达到指定时间时触发闹钟中断控制 LED 闪烁告警。1. 硬件连接LED 正极→PB0推挽输出→1KΩ 限流电阻→GND核心逻辑闹钟中断触发后翻转 PB0 电平实现 LED 闪烁。2. 闹钟功能代码扩展1GPIO 初始化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.hc运行/** * 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_year1900, alarm_time.tm_mon1, 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_FLAG1; 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_year1900, Now_Time-tm_mon1, 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.hc运行/** * 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)原因 2LSE 晶振未稳定→直接配置 RTC 时钟源解决必须等待RCC_GetFlagStatus(RCC_FLAG_LSERDY) SET后再继续原因 3预分频器配置错误→未分频为 1Hz解决LSE32.768KHz 时预分频器值必须为 32767RTC_SetPrescaler(32767)。2. 串口更新时间失败原因 1指令帧格式错误→帧头、长度或校验和不匹配解决严格按0x5A 0x06 YY MM DD HH MM 校验和格式发送确保校验和正确原因 2struct tm格式转换错误→月份或年份未调整解决月份需 - 10~11年份需转换为 1900 年偏移量2026→126。3. 闹钟不触发原因 1未使能闹钟中断→RTC_ITConfig(RTC_IT_ALR, ENABLE)未调用解决配置闹钟后必须使能闹钟中断原因 2闹钟时间戳计算错误→mktime函数参数错误解决确保struct tm结构体的各字段在合法范围如小时 0~23日期 1~31原因 3NVIC 未配置闹钟中断优先级→中断无法响应解决在RTC_Config中添加闹钟中断的 NVIC 配置类似秒中断。4. 掉电后时间丢失原因 1未接备用电源VBAT→主电源关闭后 RTC 断电解决VBAT 引脚接 CR2032 纽扣电池确保电压在 2.0V~3.6V原因 2备份域复位→误操作RCC_BDCR的 BDRST 位解决初始化时仅复位一次后续避免调用BKP_DeInit()。5. 时间走时不准原因 1LSE 晶振负载电容不匹配→晶振频率偏移解决使用 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 闪存芯片的数据存储进一步拓展嵌入式系统的存储能力