零基础STM32入门教程--15-RTC 实时时钟

专栏:STM32F103入门教程
环境:Keil‑MDK + STM32F103 标准库3.5版本

一、RTC

RTC(Real‑Time Clock,实时时钟),专门用来记录年、月、日、时、分、秒、星期的外设,核心目标:主 MCU 断电之后,时间继续跑。

主要功能
日历功能:提供秒、分、时、日、月、年、星期
闹钟中断:可设置一个或多个闹钟,到时触发中断/唤醒
周期性唤醒:定时唤醒 MCU(如每秒、每分钟)
时间戳:记录特定事件发生的精确时间
校准功能:补偿晶振频率偏差,提高走时精度

STM32F103 RTC 相关

STM32F103 的RTC是后备域的实时时钟外设,本质是一个32位秒计数器没有硬件日历,年月日星期全部软件换算;主电源掉电可由VBAT电池继续运行。
寄存器基地址:0x40002800

1. 时钟源(3选1)
  1. LSE:外部32.768kHz晶振(推荐)
    外接32.768K晶振,搭配负载电容,精度高;后备电池供电下也可以运行。
  2. LSI:内部RC振荡器 ~40kHz
    内部RC,不需要外部元件;温漂大,误差很大,不适合做准确计时
  3. HSE/128:主时钟分频作为RTC时钟
    主电源断电就停止,不能掉电走时,几乎不用

工业产品一律使用 LSE 32.768K。

2. 供电 — VBAT后备域
  • 当主电源VDD掉电,芯片切换到 VBAT引脚供电
  • VBAT供电下,只有:RTC外设 + 后备寄存器保持工作,其余外设全部复位。
  • VBAT可以接纽扣电池,也可以接外部3V后备电源。
  • ⚠️ 如果VBAT悬空,断电RTC时间丢失,上电时间随机。

注意:F103上电,检测LSE是否起振;晶振不起振RTC就不走。

3. RTC核心寄存器

F1的RTC有同步机制:写寄存器不能直接写,要等待同步标志位,这是F1最大坑。

寄存器作用
RTC_CNT32位计数器,保存总秒数;从0开始累加,这就是RTC的“时间”
RTC_PRL预分频寄存器,把RTC时钟分频得到1Hz秒脉冲;LSE(32768K)时PRL=32767
RTC_ALR32位闹钟寄存器;当RTC_CNT == RTC_ALR,触发闹钟中断
RTC_CRH中断使能:秒中断、闹钟中断、溢出中断
RTC_CRL状态位:RSF寄存器同步标志位、OWE、ALRF等

✨重点:STM32F1 RTC没有硬件年月日寄存器!
RTC_CNT只是“总秒数”,需要软件写算法把总秒数转换成:年‑月‑日‑时‑分‑秒‑星期。
和STM32F4/H7不一样,F4有硬件日历。

4. 中断
  1. 闹钟中断 ALR:CNT计数等于ALR,产生中断,可以唤醒STOP待机模式。
  2. 秒中断 SEC:每1秒产生一次中断。
  3. 溢出中断 OVF:32位计数器溢出(要很多年才溢出)。

中断属于后备域,NVIC依然需要配置。

5. 后备寄存器 BKP_DR1 ~ BKP_DR10

属于后备域,VBAT供电保持。共10个16bit寄存器,掉电不丢失,可以存少量用户参数
不是真正EEPROM,只是SRAM;拿掉VBAT电池数据就丢失。

6. 标准初始化流程要点(F1特有)
  1. 开启电源后备域访问:PWR->CR |= PWR_CR_DBP必须解锁后备域,否则写不进RTC/BKP
  2. 选择RTC时钟源(LSE),等待LSE稳定
  3. 等待RTC寄存器同步 RSF=1
  4. 如果需要配置RTC,先进入RTC配置模式 CNF=1
  5. 设置PRL预分频,设置CNT初值,设置ALR闹钟值
  6. 退出配置模式 CNF=0,等待RSF同步完成

每一次修改RTC寄存器,都要走这套流程,否则写不生效。

7. 常见问题&坑
  1. ❗忘记打开 PWR_CR_DBP,后备域锁死,RTC/BKP寄存器写无效。
  2. LSE晶振不起振:负载电容不匹配、晶振虚焊;此时RTC不走。
  3. 写RTC寄存器没有等待RSF同步,写入不生效。
  4. LSI做时钟源:时间越走偏差越大,不适合产品。
  5. VBAT无电池:断电时间丢失,上电需要重新设置时间。
  6. 后备寄存器BKP:只是SRAM,电池拿掉全部清零,不能当EEPROM。

二、STM32F103 RTC 库函数相关

1. 等待同步类(F1最重要!写RTC前必须等待)

void RTC_WaitForLastTask(void);
// 等待对RTC寄存器的上一次写操作完成。修改CNT/PRL/ALR之后必须调用。
void RTC_WaitForSynchro(void);
// 等待RTC寄存器与APB1同步(RSF位);后备域上电后必须调用,否则读寄存器错误。

2. 设置、获取计数器(RTC_CNT,32位秒计数器)

void RTC_SetCounter(uint32_t CounterValue);
// 设置RTC_CNT计数器,总秒数。调用前:进入配置模式、等待同步。
		 
uint32_t RTC_GetCounter(void);
// 读取RTC_CNT,获取当前总秒数

3. 设置预分频 PRL

void RTC_SetPrescaler(uint32_t PrescalerValue);
// 设置预分频器;LSE=32768kHz时,设置 32767,输出1Hz秒脉冲。

4. 闹钟 ALR 设置

void RTC_SetAlarm(uint32_t AlarmValue);
// 设置闹钟值;当RTC_CNT == AlarmValue 触发闹钟中断

5. 配置模式开关(写RTC寄存器必须开启CNF位)

void RTC_EnterConfigMode(void);  // 进入配置模式 CNF=1,允许修改RTC寄存器
void RTC_ExitConfigMode(void);   // 退出配置模式 CNF=0

6. 中断使能/关闭

void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
/*
RTC_IT_SEC  秒中断
RTC_IT_ALR  闹钟中断
RTC_IT_OVF  溢出中断
*/

7. 获取中断标志位、清除标志

FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
// RTC_FLAG_SEC、RTC_FLAG_ALR、RTC_FLAG_OVF、RTC_FLAG_RSF

void RTC_ClearFlag(uint16_t RTC_FLAG);
void RTC_ClearITPendingBit(uint16_t RTC_IT);

8. 获取DIV(预分频计数器,调试看RTC时钟是否运行)

uint32_t RTC_GetDivider(void);
// 返回RTC_DIV寄存器,可以判断RTC时钟是否在跑。

9、配套 PWR、BKP 函数(RTC初始化必用)

RTC属于后备域,必须解锁后备域保护。

// PWR 电源后备域解锁
void PWR_BackupAccessCmd(FunctionalState NewState);
// PWR_BackupAccessCmd(ENABLE); // DBP置1,允许访问BKP、RTC寄存器

// BKP:复位后备域(会清空RTC时间、BKP寄存器)
void BKP_DeInit(void);
// BKP后备寄存器读写
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

RCC:RTC时钟源选择

void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
// RCC_RTCCLKSource_LSE  //外部32.768K晶振(推荐)
// RCC_RTCCLKSource_LSI  //内部40K RC
// RCC_RTCCLKSource_HSE_Div128

void RCC_RTCCLKCmd(FunctionalState NewState); //使能RTC时钟

三、工程与程序编写

3.1工程复制

复制ADC 工程文件夹为RTC文件夹,ADC.uvprojx复制为RTC.uvprojx。

3.2 添加驱动文件

Hardware文件夹下新建Rtc文件夹,添加rtc.h,rtc.c。

rtc.h

#ifndef __RTC_H
#define __RTC_H

#include "stm32f10x.h"

// 时间结构体
typedef struct
{
    uint16_t year;
    uint8_t  month;
    uint8_t  day;
    uint8_t  week;
    uint8_t  hour;
    uint8_t  min;
    uint8_t  sec;
} rtc_time_type_def;

extern rtc_time_type_def g_rtc_time;


uint8_t rtc_init(void);
void    rtc_set_date_time(rtc_time_type_def *time);
void    rtc_get_date_time(rtc_time_type_def *time);

// 秒计数器 <--> 日历时间转换
uint32_t rtc_date_time_to_sec(rtc_time_type_def *time);
void     rtc_sec_to_date_time(uint32_t sec_cnt, rtc_time_type_def *time);

/* 每秒一次秒中断 */
void rtc_second_irq_init(void);


#endif

rtc.c

#include "rtc.h"

rtc_time_type_def g_rtc_time;

// 平年每个月天数
static const uint8_t month_days[] = {31,28,31,30,31,30,31,31,30,31,30,31};

/**
 * @brief  判断是否闰年
 * @param  year: 公历年份
 * @retval 1闰年 0平年
 */
static uint8_t rtc_is_leap_year(uint16_t year)
{
    if((year%4==0 && year%100!=0) || (year%400==0))
        return 1;
    return 0;
}

/**
 * @brief rtc初始化,LSE 32.768K晶振作为时钟源
 * @retval 0:成功 1:LSE启动失败
 */
uint8_t rtc_init(void)
{
    uint32_t t = 0;
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
    PWR_BackupAccessCmd(ENABLE);    //解锁后备域

    // 判断LSE是否已经开启,RTC是否已经配置
    if(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET)
    {
        // LSE已经运行,直接同步RTC
        RTC_WaitForSynchro();
        return 0;
    }

    BKP_DeInit();   //复位后备域,RTC、BKP寄存器清零

    //开启LSE外部32.768K
    RCC_LSEConfig(RCC_LSE_ON);
    
    while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)==RESET)
    {
        t++;
        if(t>0x100000)
        {
            return 1; // LSE晶振不起振
        }
    }

    //选择RTC时钟源 LSE
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    RCC_RTCCLKCmd(ENABLE);

    RTC_WaitForSynchro();

    RTC_EnterConfigMode();
    RTC_SetPrescaler(32767); //32768分频得到1Hz
    RTC_SetCounter(0);
    RTC_ExitConfigMode();

    RTC_WaitForLastTask();
    return 0;
}

/**
 * @brief 初始化RTC秒中断,1秒触发一次
 * @note 调用此函数前必须先调用 rtc_init()
 */
void rtc_second_irq_init(void)
{
    NVIC_InitTypeDef nvic_init_struct;

    RTC_WaitForSynchro();

    // 使能RTC秒中断
    RTC_ITConfig(RTC_IT_SEC, ENABLE);

    // NVIC配置 RTC_IRQn
    nvic_init_struct.NVIC_IRQChannel = RTC_IRQn;
    nvic_init_struct.NVIC_IRQChannelPreemptionPriority = 1;
    nvic_init_struct.NVIC_IRQChannelSubPriority = 0;
    nvic_init_struct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvic_init_struct);
}

/**
 * @brief 设置RTC时间,把日历时间转为总秒数写入RTC_CNT
 * @param time:时间指针
 */
void rtc_set_date_time(rtc_time_type_def *time)
{
    uint32_t sec_val = rtc_date_time_to_sec(time);

    RTC_EnterConfigMode();
    RTC_SetCounter(sec_val);
    RTC_ExitConfigMode();

    RTC_WaitForLastTask();
}

/**
 * @brief 获取当前RTC日历时间
 * @param time:输出时间结构体指针
 */
void rtc_get_date_time(rtc_time_type_def *time)
{
    uint32_t sec_val = RTC_GetCounter();
    rtc_sec_to_date_time(sec_val, time);
}


/**
 * @brief 日历时间转换为总秒数(基准:2000?01?01 00:00:00)
 * @param time:输入时间
 * @retval 从基准开始累计的秒数
 */
uint32_t rtc_date_time_to_sec(rtc_time_type_def *time)
{
    uint32_t sec = 0;
    uint16_t y;
    uint8_t m;

    //累加整年秒数
    for(y = 2000; y < time->year; y++)
    {
        if(rtc_is_leap_year(y))
            sec += 366 * 86400UL;
        else
            sec += 365 * 86400UL;
    }

    //累加整月
    for(m = 1; m < time->month; m++)
    {
        sec += month_days[m-1] * 86400UL;
        if(m == 2 && rtc_is_leap_year(time->year))
        {
            sec += 86400UL;
        }
    }

    //日时分秒
    sec += (time->day - 1) * 86400UL;
    sec += time->hour * 3600UL;
    sec += time->min * 60UL;
    sec += time->sec;

    return sec;
}

/**
 * @brief 总秒数还原成日历时间(基准2000?01?01 00:00:00)
 * @param sec_cnt RTC_CNT总秒数
 * @param time 输出时间结构体
 */
void rtc_sec_to_date_time(uint32_t sec_cnt, rtc_time_type_def *time)
{
    uint32_t day_cnt;
    uint32_t day_sec;
    uint16_t y = 2000;
    uint8_t m;

    day_cnt = sec_cnt / 86400UL;        //总天数
    day_sec = sec_cnt % 86400UL;        //当天剩余秒数

    //时分秒
    time->hour = day_sec / 3600UL;
    time->min  = (day_sec % 3600UL)/60UL;
    time->sec  = day_sec % 60UL;

    //计算年
    while(1)
    {
        uint16_t y_days = rtc_is_leap_year(y)?366:365;
        if(day_cnt >= y_days)
        {
            day_cnt -= y_days;
            y++;
        }else{
            break;
        }
    }
    time->year = y;

    //计算月
    for(m = 1; m <=12; m++)
    {
        uint8_t md = month_days[m-1];
        if(m == 2 && rtc_is_leap_year(y))
            md +=1;

        if(day_cnt >= md)
        {
            day_cnt -= md;
        }else{
            break;
        }
    }
    time->month = m;
    time->day = day_cnt + 1;

    //星期简单计算:2000?01?01是星期六
    time->week = (6 + sec_cnt / 86400UL) % 7;
}

/**
 * @brief RTC中断服务函数
 */
void RTC_IRQHandler(void)
{
	if(RTC_GetITStatus(RTC_IT_SEC) != RESET)
    {
        RTC_ClearITPendingBit(RTC_IT_SEC);
        // 执行用户回调,每秒进来一次
		rtc_sec_to_date_time(RTC_GetCounter(),&g_rtc_time);
		
    }
}

main.c

#include "stm32f10x.h"  
#include "led.h"
#include "delay.h"
#include "key.h"
#include "usart.h"
#include "exti.h"
#include "time.h"
#include <stdio.h>
#include "i2c_sim.h"
#include "at24c02.h"
#include "spi.h"
#include "w25qxx.h"
#include "adc.h"
#include "rtc.h"


int main(void)
{		
	rtc_time_type_def t;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);// 设置中断优先级分组4
	led_init();
	delay_init();
	usart1_init(115200);	
    rtc_init();
	
	//设置初始时间 2026/09/03 12:00:00
    t.year  = 2026;
    t.month = 9;
    t.day   = 3;
    t.hour  = 12;
    t.min   = 0;
    t.sec   = 0;
    rtc_set_date_time(&t);
	rtc_second_irq_init();  //开启每秒中断

	while(1)
	{		
		delay_ms(1000);		
		printf("%d-%d-%d %d : %d : %d  \r\n",
			g_rtc_time.year,
			g_rtc_time.month,
			g_rtc_time.day,
			g_rtc_time.hour,
			g_rtc_time.min,
			g_rtc_time.sec);
	}
	
}


/*
2026-9-3 12 : 0 : 0  
2026-9-3 12 : 0 : 1  
2026-9-3 12 : 0 : 2  
2026-9-3 12 : 0 : 3  
2026-9-3 12 : 0 : 4  
2026-9-3 12 : 0 : 5  
2026-9-3 12 : 0 : 6  
2026-9-3 12 : 0 : 7  
2026-9-3 12 : 0 : 8  
2026-9-3 12 : 0 : 9  
2026-9-3 12 : 0 : 10  
2026-9-3 12 : 0 : 11  
2026-9-3 12 : 0 : 12  
2026-9-3 12 : 0 : 13  

*/

3.3 添加库文件,工程文件,和文件包含路径

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.4 编译烧录

在这里插入图片描述
在这里插入图片描述

烧录程序后,每秒会打印前的时间

资料下载
参考资料网盘 :
链接 : https://pan.baidu.com/s/1BrH0JbUY6z7j7gAihTa4Dw
提取码: qnws

本节课完整工程例程网盘:
链接: https://pan.baidu.com/s/1rl6EIqi9m6kQ1JFjuSqINg
提取码: bumc

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oshan2012

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值