专栏:STM32F103入门教程
环境:Keil‑MDK + STM32F103 标准库3.5版本
一、RTC
RTC(Real‑Time Clock,实时时钟),专门用来记录年、月、日、时、分、秒、星期的外设,核心目标:主 MCU 断电之后,时间继续跑。。
主要功能
日历功能:提供秒、分、时、日、月、年、星期
闹钟中断:可设置一个或多个闹钟,到时触发中断/唤醒
周期性唤醒:定时唤醒 MCU(如每秒、每分钟)
时间戳:记录特定事件发生的精确时间
校准功能:补偿晶振频率偏差,提高走时精度
STM32F103 RTC 相关
STM32F103 的RTC是后备域的实时时钟外设,本质是一个32位秒计数器,没有硬件日历,年月日星期全部软件换算;主电源掉电可由VBAT电池继续运行。
寄存器基地址:0x40002800
1. 时钟源(3选1)
- LSE:外部32.768kHz晶振(推荐)
外接32.768K晶振,搭配负载电容,精度高;后备电池供电下也可以运行。 - LSI:内部RC振荡器 ~40kHz
内部RC,不需要外部元件;温漂大,误差很大,不适合做准确计时。 - 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_CNT | 32位计数器,保存总秒数;从0开始累加,这就是RTC的“时间” |
| RTC_PRL | 预分频寄存器,把RTC时钟分频得到1Hz秒脉冲;LSE(32768K)时PRL=32767 |
| RTC_ALR | 32位闹钟寄存器;当RTC_CNT == RTC_ALR,触发闹钟中断 |
| RTC_CRH | 中断使能:秒中断、闹钟中断、溢出中断 |
| RTC_CRL | 状态位:RSF寄存器同步标志位、OWE、ALRF等 |
✨重点:STM32F1 RTC没有硬件年月日寄存器!
RTC_CNT只是“总秒数”,需要软件写算法把总秒数转换成:年‑月‑日‑时‑分‑秒‑星期。
和STM32F4/H7不一样,F4有硬件日历。
4. 中断
- 闹钟中断 ALR:CNT计数等于ALR,产生中断,可以唤醒STOP待机模式。
- 秒中断 SEC:每1秒产生一次中断。
- 溢出中断 OVF:32位计数器溢出(要很多年才溢出)。
中断属于后备域,NVIC依然需要配置。
5. 后备寄存器 BKP_DR1 ~ BKP_DR10
属于后备域,VBAT供电保持。共10个16bit寄存器,掉电不丢失,可以存少量用户参数。
不是真正EEPROM,只是SRAM;拿掉VBAT电池数据就丢失。
6. 标准初始化流程要点(F1特有)
- 开启电源后备域访问:
PWR->CR |= PWR_CR_DBP;必须解锁后备域,否则写不进RTC/BKP - 选择RTC时钟源(LSE),等待LSE稳定
- 等待RTC寄存器同步 RSF=1
- 如果需要配置RTC,先进入RTC配置模式 CNF=1
- 设置PRL预分频,设置CNT初值,设置ALR闹钟值
- 退出配置模式 CNF=0,等待RSF同步完成
每一次修改RTC寄存器,都要走这套流程,否则写不生效。
7. 常见问题&坑
- ❗忘记打开
PWR_CR_DBP,后备域锁死,RTC/BKP寄存器写无效。 - LSE晶振不起振:负载电容不匹配、晶振虚焊;此时RTC不走。
- 写RTC寄存器没有等待RSF同步,写入不生效。
- LSI做时钟源:时间越走偏差越大,不适合产品。
- VBAT无电池:断电时间丢失,上电需要重新设置时间。
- 后备寄存器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

5959

被折叠的 条评论
为什么被折叠?



