全部例程

RTC - 实时时钟

W55MH32

更新于 2026年8月13日

本篇总结

使用 RTC + LSE,实现实时时钟计时并串口打印时间,掉电由 VBAT 维持走时。

🔑 KEIL MDK、UART调试、RTC

本示例演示 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。

引脚功能方向模式
PA9USART1_TX开发板 → PC复用推挽输出 (AF_PP)
PA10USART1_RXPC → 开发板浮空输入 (IN_FLOATING)
PC14OSC32_INLSE 外部 32.768kHz 晶振输入
PC15OSC32_OUTLSE 外部 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.uvprojxF7 编译,确认 0 Error(s)。连接 DEBUG USB 口到 PC,F8 下载。下载成功后按 RESET 键启动新固件。

运行验证

打开 WIZ UartTool V1.0,选择开发板对应 COM 口,波特率 115200 / 8N1,打开串口。按 RESET 启动后:

WIZ UartTool 串口输出 — RTC 日历设置与走时测试
WIZ UartTool 串口输出 — RTC 日历设置与走时测试

首次启动(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


WIZ UartTool 串口输出 — RTC LSI 校准测试
WIZ UartTool 串口输出 — RTC LSI 校准测试(LsiFreq: 39625 Hz)

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() 等待完成

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板