RTC - 实时时钟
更新于 2026年8月13日
本篇总结
使用 RTC + LSE,实现实时时钟计时并串口打印时间,掉电由 VBAT 维持走时。
本示例演示 W55MH32L 的 RTC 外设实现实时时钟日历功能。使用板载外部 32.768kHz 晶振(LSE)作为时钟源,经 32768 分频得到精确 1Hz 秒脉冲。程序通过备份寄存器 BKP_DR1 标志判断是否首次上电:首次则串口交互设置时间,之后每秒打印一次当前时间;非首次则直接恢复走时。
关键整改点:原例程使用 LSI(~40kHz)作为 RTC 时钟源,预分频仍按 32.768kHz 计算,导致每秒偏快约 22%。本次整改改为 LSE 外部晶振,获得精确 1Hz。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口交互与日志观察工具:点击下载
硬件连接
RTC 走时依赖外部 32.768kHz 晶振(Y4,PC14/PC15)和 VBAT 备用电池。USART1 的 TX(PA9) / RX(PA10) 通过板载 USB 转串口芯片连接到 DEBUG USB 口,用于时间设置交互和走时打印。仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC。
| 引脚 | 功能 | 方向 | 模式 |
|---|---|---|---|
| PA9 | USART1_TX | 开发板 → PC | 复用推挽输出 (AF_PP) |
| PA10 | USART1_RX | PC → 开发板 | 浮空输入 (IN_FLOATING) |
| PC14 | OSC32_IN | — | LSE 外部 32.768kHz 晶振输入 |
| PC15 | OSC32_OUT | — | LSE 外部 32.768kHz 晶振输出 |
| VBAT | 备份域电源 | — | 接 3V 纽扣电池,VDD 掉电时维持走时 |
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\RTC\,核心文件如下:
RTC/
├── USER/
│ ├── main.c ← RTC初始化 + 时间设置/显示 + printf 重定向
│ ├── w55mh32_conf.h ← 外设头文件集中包含
│ ├── w55mh32_it.c/h ← 中断服务函数(含 RTC_IRQHandler)
│ ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│ └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
└── delay/
└── delay.c/h ← SysTick 延时函数
步骤1:头文件与全局变量
文件:USER/main.c — 包含标准库与外设头文件,声明各功能函数,定义秒中断标志位 TimeDisplay。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "delay.h"
#include "w55mh32.h"
USART_TypeDef *USART_TEST = USART1;
void UART_Configuration(uint32_t bound);
void NVIC_Configuration(void);
void RCC_ClkConfiguration(void);
void RTC_Configuration(void);
void Time_Adjust(void);
void Time_Show(void);
__IO uint32_t TimeDisplay = 0;
TimeDisplay 是秒中断与主循环通信的标志位:RTC 每秒中断将其置 1,主循环检测到后打印时间并清零。
步骤2:系统时钟配置
函数:RCC_ClkConfiguration() — HSE 8MHz × PLL9 = 72MHz,同时开启 LSE 外部 32.768kHz 晶振供 RTC 使用。
void RCC_ClkConfiguration(void)
{
RCC_DeInit();
RCC_HSEConfig(RCC_HSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET);
RCC_PLLCmd(DISABLE);
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
RCC_PLLCmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
RCC_HCLKConfig(RCC_SYSCLK_Div1);
RCC_PCLK1Config(RCC_HCLK_Div2);
RCC_PCLK2Config(RCC_HCLK_Div1);
/* 同时开启 LSE,供 RTC 使用 */
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
RCC_HSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET);
}系统时钟:HSE 8MHz 作为 PLL 输入,9 倍频得到 SYSCLK = 72MHz。HCLK = 72MHz(不分频),PCLK1 = 36MHz(2 分频),PCLK2 = 72MHz(不分频)。同时开启 LSE 等待起振,作为 RTC 时钟源。
如果你想自定义
- 改用其他主频 → 修改
RCC_PLLMul_x,注意 PCLK1 不超过 36MHz - LSE 起振失败 → 检查 PC14/PC15 焊接和 32.768kHz 晶振,起振时间可达数秒,
while等待不可省略
步骤3:RTC 配置(关键整改点)
函数:RTC_Configuration() — 使能备份域访问 → 选 LSE 为 RTC 时钟源 → 等待同步 → 使能秒中断 → 预分频 32767 得 1Hz。
void RTC_Configuration(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
BKP_DeInit();
/* 使用板载 LSE 外部 32.768kHz 晶振(Y4, PC14/PC15)作为 RTC 时钟源,分频 32768 = 1Hz */
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)
{
}
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_ITConfig(RTC_IT_SEC, ENABLE);
RTC_WaitForLastTask();
RTC_SetPrescaler(32767); /* RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1) */
RTC_WaitForLastTask();
}整改说明:LSI → LSE
- 原代码:
RCC_LSICmd(ENABLE)+ 等待RCC_FLAG_LSIRDY+RCC_RTCCLKSource_LSI(使用 ~40kHz 内部低速时钟) - 整改后:
RCC_LSEConfig(RCC_LSE_ON)+ 等待RCC_FLAG_LSERDY+RCC_RTCCLKSource_LSE(使用外部 32.768kHz 晶振) - 原因:LSI 实际频率 ~40kHz,但预分频按 32767 计算(假设 32.768kHz),导致实际秒脉冲 ≈ 40000/32768 ≈ 1.22Hz,每秒偏快约 22%。改用 LSE 后 32768/(32767+1) = 1Hz 精确无误。
关键三步:使能 PWR/BKP 时钟并开备份域访问(默认禁止)→ 选 LSE 并等待起振 → 配置秒中断与预分频。每次写 RTC 寄存器后都要 RTC_WaitForLastTask() 等待完成。
步骤4:NVIC 中断配置
函数:NVIC_Configuration() — 设置优先级分组,使能 RTC 全局中断。
void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* Configure one bit for preemption priority */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
/* Enable the RTC Interrupt */
NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
优先级分组 NVIC_PriorityGroup_1(1 位抢占 + 3 位子优先级),RTC 中断抢占优先级 1、子优先级 0。
步骤5:串口时间设置
函数:Time_Regulate() / USART_Scanf() / Time_Adjust() — 首次上电时通过串口让用户依次输入时分秒,写入 RTC 计数器。
uint8_t USART_Scanf(uint32_t value)
{
uint32_t index = 0;
uint32_t tmp[2] = {0, 0};
while (index < 2)
{
while (USART_GetFlagStatus(USART_TEST, USART_FLAG_RXNE) == RESET)
{
}
tmp[index++] = (USART_ReceiveData(USART_TEST));
if ((tmp[index - 1] < 0x30) || (tmp[index - 1] > 0x39))
{
printf("\n\rPlease enter valid number between 0 and 9");
index--;
}
}
index = (tmp[1] - 0x30) + ((tmp[0] - 0x30) * 10);
if (index > value)
{
printf("\n\rPlease enter valid number between 0 and %d", value);
return 0xFF;
}
return index;
}
uint32_t Time_Regulate(void)
{
uint32_t Tmp_HH = 0xFF, Tmp_MM = 0xFF, Tmp_SS = 0xFF;
printf("\r\n==============Time Settings=====================================");
printf("\r\n Please Set Hours");
while (Tmp_HH == 0xFF)
{
Tmp_HH = USART_Scanf(23);
}
printf(": %d", Tmp_HH);
printf("\r\n Please Set Minutes");
while (Tmp_MM == 0xFF)
{
Tmp_MM = USART_Scanf(59);
}
printf(": %d", Tmp_MM);
printf("\r\n Please Set Seconds");
while (Tmp_SS == 0xFF)
{
Tmp_SS = USART_Scanf(59);
}
printf(": %d", Tmp_SS);
return ((Tmp_HH * 3600 + Tmp_MM * 60 + Tmp_SS));
}
void Time_Adjust(void)
{
RTC_WaitForLastTask();
RTC_SetCounter(Time_Regulate());
RTC_WaitForLastTask();
}
USART_Scanf() 从串口读两个数字字符组成两位整数,超范围返回 0xFF 强制重输。Time_Regulate() 依次询问时分秒并换算成总秒数。Time_Adjust() 将秒数写入 RTC 计数器作为起始时间戳。
输入格式注意
设置小时 14 时,需在串口工具发送区输入 两位数字 14(不是 0x14),USART_Scanf 按字符 '1' '4' 解析。单数字需补零,如 08。
步骤6:主函数逻辑
函数:main() — 初始化 → 检查 BKP_DR1 标志 → 首次则配置 RTC 并设时间,否则直接恢复走时 → 进入 Time_Show 循环。
int main(void)
{
RCC_ClocksTypeDef clocks;
delay_init();
RCC_ClkConfiguration();
UART_Configuration(115200);
printf("RTC Calendar Test (LSE 32.768kHz, 1Hz).\n");
RCC_GetClocksFreq(&clocks);
printf("\n");
printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
(float)clocks.SYSCLK_Frequency / 1000000, (float)clocks.HCLK_Frequency / 1000000,
(float)clocks.PCLK1_Frequency / 1000000, (float)clocks.PCLK2_Frequency / 1000000, (float)clocks.ADCCLK_Frequency / 1000000);
NVIC_Configuration();
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
printf("\rRTC not yet configured....\n");
RTC_Configuration();
printf("RTC configured....\n");
Time_Adjust();
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)
{
printf("Power On Reset occurred....\n");
}
else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)
{
printf("External Reset occurred....\n");
}
printf("No need to configure RTC....\n");
RTC_WaitForSynchro();
RTC_ITConfig(RTC_IT_SEC, ENABLE);
RTC_WaitForLastTask();
}
RCC_ClearFlag();
Time_Show();
while (1);
}
首次上电(BKP_DR1 != 0xA5A5):配置 RTC → 串口设时间 → 写标志。非首次:根据复位类型打印提示(上电复位 / 外部复位)→ 同步 RTC → 使能秒中断恢复走时。备份寄存器在 VDD 掉电后由 VBAT 维持,确保标志不丢。
步骤7:时间显示与秒中断
函数:Time_Display() / Time_Show()(main.c)+ RTC_IRQHandler()(w55mh32_it.c)— 秒中断置位标志,主循环打印时间。
/* ---- main.c ---- */
void Time_Display(uint32_t TimeVar)
{
uint32_t THH = 0, TMM = 0, TSS = 0;
if (RTC_GetCounter() == 0x0001517F)
{
RTC_SetCounter(0x0);
RTC_WaitForLastTask();
}
THH = TimeVar / 3600;
TMM = (TimeVar % 3600) / 60;
TSS = (TimeVar % 3600) % 60;
printf("Time: %0.2d:%0.2d:%0.2d\n", THH, TMM, TSS);
}
void Time_Show(void)
{
printf("\n\r");
while (1)
{
if (TimeDisplay == 1)
{
Time_Display(RTC_GetCounter());
TimeDisplay = 0;
}
}
}
/* ---- w55mh32_it.c ---- */
extern __IO uint32_t TimeDisplay;
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
{
/* Clear the RTC Second interrupt */
RTC_ClearITPendingBit(RTC_IT_SEC);
/* Enable time update */
TimeDisplay = 1;
/* Wait until last write operation on RTC registers has finished */
RTC_WaitForLastTask();
}
}
秒中断每秒触发一次:清除 SEC 挂起位 → 置 TimeDisplay=1 → 等写完成。主循环检测到标志后读取 RTC 计数器换算成 HH:MM:SS 打印。0x0001517F = 86399 为 24 小时满秒数,到点自动归零实现日循环。
步骤8:printf 重定向
重写 fputc() 将 printf 输出重定向到 USART1,并自动在 \n 前补 \r 确保串口工具正确换行。
int SER_PutChar(int ch)
{
while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
USART_SendData(USART_TEST, (uint8_t)ch);
return ch;
}
int fputc(int c, FILE *f)
{
/* Place your implementation of fputc here */
/* e.g. write a character to the USART */
if (c == '\n')
{
SER_PutChar('\r');
}
return (SER_PutChar(c));
}
SER_PutChar() 阻塞式发送单字符,等待 TC 标志。fputc() 遇 \n 自动补 \r,确保 WIZ UartTool 等终端正确换行。
编译与下载
Keil MDK 中打开 RTC.uvprojx,F7 编译,确认 0 Error(s)。连接 DEBUG USB 口到 PC,F8 下载。下载成功后按 RESET 键启动新固件。
运行验证
打开 WIZ UartTool V1.0,选择开发板对应 COM 口,波特率 115200 / 8N1,打开串口。按 RESET 启动后:
首次启动(BKP_DR1 != 0xA5A5):程序检测到 RTC 未配置,自动进入时间设置模式,依次提示输入小时、分钟、秒。输入完毕后写入 RTC 计数器,并在备份寄存器写入 0xA5A5 标志。此后每秒打印一次当前时间。
后续启动(BKP_DR1 == 0xA5A5):跳过时间设置,直接同步 RTC 并使能秒中断,恢复走时。
预期串口输出(首次设置时间为 14:08:36 后):
RTC Calendar Test (LSE 32.768kHz, 1Hz).
SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 36.0Mhz
RTC not yet configured....
RTC configured....
==============Time Settings=====================================
Please Set Hours: 14
Please Set Minutes: 8
Please Set Seconds: 36
Time: 14:08:36
Time: 14:08:37
Time: 14:08:38
...
每秒稳定打印一行时间,间隔严格为 1 秒(LSE 32.768kHz / 32768 = 1Hz)。对比原版 LSI 方案(约 0.82 秒/次),计时精度显著提升。拔掉 VBAT 电池后再上电,时间会丢失并重新进入设置流程;保留 VBAT 的情况下拔插 USB 电源,时间不中断。
常见问题
Q: 时间走时明显偏快(每秒约快 22%)
原 LSI 方案遗留问题。LSI 实际频率 ~40kHz,但预分频按 32767 计算(假设 32.768kHz),导致秒脉冲 ≈ 1.22Hz。确认 RTC_Configuration 中已改用 RCC_LSEConfig + RCC_RTCCLKSource_LSE。
Q: 程序卡在 RCC_FLAG_LSERDY 等待处不动
LSE 起振失败。检查 PC14/PC15 焊接、32.768kHz 晶振 Y4 是否完好、负载电容是否匹配。LSE 起振时间较长(可达 1~2 秒),while 等待本身是正常的,但若超过数秒仍未就绪则是硬件问题。
Q: 拔插 USB 电源后时间丢失,每次都进入设置流程
VBAT 未接备用电池或电池电量耗尽。备份寄存器 BKP_DR1 与 RTC 计数器都依赖 VBAT 维持。确认 VBAT 引脚接了 3V 纽扣电池(CR2032 等),且电池正极朝向正确。
Q: 串口设置时间时输入无效或被拒
USART_Scanf 要求输入两位 ASCII 数字字符(0x30~0x39)。设置 8 分时需输入 08 而非 8。串口工具发送区确认未勾选「HEX 发送」,应按文本模式发送。
Q: 串口无输出或乱码
先看串口打印的时钟频率是否正常(SYSCLK 应为 72.0Mhz)。若频率异常,检查 system_w55mh32.c / RCC_ClkConfiguration 中 HSE_VALUE 是否与实际晶振一致。再确认 COM 口号正确、波特率 115200,下载后需按 RESET 键启动。
Q: 修改 RTC 寄存器后立即读取数值未更新
RTC 时钟(32.768kHz)远低于 APB 主频,写操作需 3 个 RTCCLK 周期才生效。每次写 RTC 寄存器(RTC_SetCounter / RTC_SetPrescaler 等)后必须调用 RTC_WaitForLastTask() 等待 RTOFF 标志置位,否则后续操作可能被丢弃。
总结
本文基于 W55MH32L 的 RTC 外设实现了实时时钟日历功能:
- 时钟源整改:由 LSI(~40kHz)改为 LSE 外部 32.768kHz 晶振,预分频 32767 得到精确 1Hz 秒脉冲,解决原例程走时偏快 22% 的问题
- 备份域机制:通过
BKP_DR1标志位区分首次上电与后续启动,首次串口交互设时间,之后直接恢复走时;VBAT 电池维持 VDD 掉电后的走时与标志 - 秒中断驱动:RTC 每秒中断置
TimeDisplay标志,主循环换算 HH:MM:SS 打印,24 小时(0x0001517F 秒)自动归零循环 - 关键细节:访问备份域前需
PWR_BackupAccessCmd(ENABLE);每次写 RTC 寄存器后必须RTC_WaitForLastTask()等待完成