STM32F103 RTC配置
本节我们将介绍RTC
,实际上有关RTC
我们在学习S3C2440
的时候已经详细介绍过《Mini2440
裸机开发之RTC
》。
一、RTC
RTC
,英文全称Real Time Clock
,中文就是实时时钟,是一个可以为系统提供精确的时间基准的元器件,RTC
一般采用精度较高的晶振作为时钟源,有些RTC
为了在主电源掉电时还可以工作,需要外加电池供电。
1.1 概述
在断电情况下RTC
仍可以独立运行,只要芯片的外加电池一直供电,RTC
上的时间会一直走。
RTC
本质上是个独立的定时器。RTC
模块拥有一个连续计数的计数器。从定时器的角度来看,相对于通用定时器TIM
外设,它的功能十分简单,只有计时功能(也可以触发中断),但其高级之处也就在于掉电之后还可以正常运行。
两个32
位寄存器包含二进码十进数格式 (BCD
) 的秒、分钟、小时(12
或24
小时制)、星期几、日期、月份和年份。此外,还可提供二进制格式的亚秒值。系统可以自动将月份的天数补偿为28
、29
(闰年)、30
和31
天。
上电复位后,所有RTC
寄存器都会受到保护,以防止可能的非正常写访问。
无论器件状态如何(运行模式、低功耗模式或处于复位状态),只要电源电压保持在工作范围内,RTC
不会停止工作。
1.2 特性
-
可编程的预分频系数:分频系数高为\(2^{20}\);
-
32
位的可编程计数器,可用于较长时间段的测量; -
2
个分离的时钟:用于APB1
接口的PCLK1
和RTC
时钟(RTC
时钟的频率必须小于PCLK1
时钟频率的四分之一以上); -
可以选择以下三种
RTC
的时钟源;HSE
时钟除以128
;LSE
振荡器时钟;LSI
振荡器时钟;
-
2
个独立的复位类型:APB1
接口由系统复位;RTC
核心(预分频器、闹钟、计数器和分频器)只能由后备域复位;
-
3
个专门的可屏蔽中断;- 闹钟中断,用来产生一个软件可编程的闹钟中断;
- 秒中断,用来产生一个可编程的周期性中断信号(长可达1秒);
- 溢出中断,指示内部可编程计数器溢出并回转为
0
的状态;
1.3 RTC
方框图

RTC
由两个主要部分组成。
- 第一部分(
APB1
接口):用来和APB1
总线相连,此单元还包含一组16
位寄存器,可通过APB1
总线对其进行读写操作。APB1
接口由APB1
总线时钟驱动,用来与APB1
总线连接; - 另一部分(
RTC
核心):由一组可编程计数器组成,分成两个主要模块;- 第一个模块是
RTC
的预分频模块(左下1):它可编程产生最长为1
秒的RTC
时间基准TR_CLK
。RTC
的预分频模块包含了一个20
位的可编程分频器(RTC
预分频器)。如果在RTC_CR
寄存器中设置了相应的允许位,则在每个实时时钟(RTC
)TR_CLK
周期中RTC
产生一个中断(秒中断); - 第二个模块是一个
32
位的可编程计数器(左下2):可被初始化为当前的系统时间;系统时间按TR_CLK
周期累加并与存储在RTC_ALR
寄存器中的可编程时间相比较,如果RTC_CR
控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断。
- 第一个模块是
1.3.1 RTCCLK
选择
我们在《STM32F103
系统时钟配置》中介绍了STM32
的时钟框图;

RTCCLK
可以选择以下三种作为时钟源;
HSE
时钟除以128
;LSE
振荡器时钟;LSI
振荡器时钟;
使用HSE
分频时钟或者LSI
的时候,在主电源VDD
掉电的情况下,这两个时钟来源都会受到影响,因此没法保证RTC
正常工作,所以RTC
一般都时钟低速外部时钟LSE
,频率为实时时钟模块中常用的32.768KHz
。
因为32768 = 2^15
,分频容易实现,所以被广泛应用到RTC
模块。在主电源VDD
有效的情况下(待机),RTC
还可以配置闹钟事件使STM32
退出待机模式。
1.3.2 具体流程
RTCCLK
经过RTC_DIV
预分频,RTC_PRL
设置预分频系数,然后得到TR_CLK
时钟信号,我们一般设置其周期为1s
,RTC_CNT
计数器计数,假如1970
设置为时间起点为0s
,通过当前时间的秒数计算得到当前的时间。
RTC_ALR
是设置闹钟时间,RTC_CNT
计数到RTC_ALR
就会产生计数中断;
RTC_Second
为秒中断,用于刷新时间;RTC_Overflow
是溢出中断;RTC Alarm
控制开关机。
1.3.3 RTC
复位过程
除了RTC_PRL
、RTC_ALR
、RTC_CNT
和RTC_DIV
寄存器外,所有的系统寄存器都由系统复位或电源复位进行异步复位。
RTC_PRL
、RTC_ALR
、RTC_CNT
和RTC_DIV
寄存器仅能通过备份域复位信号复位。
系统复位后,禁止访问后备寄存器和RCT
,防止对后卫区域(BKP
)的意外写操作。
1.3.4 读RTC
寄存器
RTC
内核完全独立于APB1
接口,软件通过APB1
接口对RTC
相关寄存器访问。但是相关寄存器只在RTC APB1
时钟进行重新同步的RTC
时钟的上升沿被更新。所以软件必须先等待寄存器同步标志位(RTC_CRL
的RSF
位)被硬件置1
才读。
1.3.5 配置RTC
寄存器
必须设置RTC_CRL
寄存器中的CNF
位,使RTC
进入配置模式后,才能写入RTC_PRL
、RTC_CNT
、RTC_ALR
寄存器。
另外,对RTC
任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR
寄存器中的RTOFF
状态位,判断RTC
寄存器是否处于更新中。仅当RTOFF
状态位是1
时,才可以写入RTC
寄存器。
二、RTC
相关寄存器
RTC
相关的寄存器主要有:
RTC
控制寄存器:RTC_CRH
,RTC_CRL
;RTC
预分频装载寄存器:RTC_PRLH
,RTC_PRLL
;RTC
预分频余数寄存器:RTC_DIVH
,RTC_DIVL
;RTC
计数器寄存器:RTC_CNTH
,RTC_CNTL
;RTC
闹钟寄存器:RTC_ALRH
,RTC_ALRL
。
2.1 RTC
控制寄存器
2.1.1 RTC_CRH

作用:配置3个专门的可屏蔽中断(溢出中断、闹钟中断、秒中断)使能。
注意:系统复位后所有的中断被屏蔽,因此可通过写RTC
寄存器来确保在初始化后没有挂起的中断请求。当外设正在完成前一次写操作时(标志位RTOFF=0
),不能对RTC_CRH
寄存器进行写操作。
2.1.2 RTC_CRL


一般用到该寄存器的3
,4
,5
位:
- 位
3
(RSF
):寄存器同步标志位,我们在修改控制寄存器RTC_CRH
/RTC_CRL
之前,必须先判断该位,是否已经同步了,如果没有则等待同步; - 位
4
(CNF
):配置标位,在软件修改RTC_CNT
/RTC_ALR
/RTC_PRL
的值的时候,必须先软件置位该位,以允许进入配置模式; - 位
5
(RTOFF
):RTC
操作位,该位由硬件操作,软件只读。通过该位可以判断上次对RTC
寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才能开始下一次,也就是判断RTOFF
位是否置位。
3
个位总结如下:
(1) 修改RTC_CRH
/RTC_CRL
寄存器,必须先判断RSF
位,确定已经同步;
(2) 修改RTC_CNT
/RTC_ALR
/RTC_PRL
的时候,必须先配置CNF
位进入配置模式,修改完之后,设置CNF
位为0
退出配置模式。
(3) 同时在对RTC
相关寄存器写操作之前,必须先判断RTOFF
位,确定上次对RTC
寄存器的操作是已经完成。
2.2 RTC
预分频装载寄存器
预分频装载寄存器用来保存RTC
预分频器的周期计数值。在修改这个寄存器的时候要先进入配置模式,即设置RTC_CRL
寄存器中的CNF
位,使RTC
进入配置模式。
我们使用外部32.768kHz
的晶振作为RTC
时钟的输入频率,那么就要设置这两个寄存器的值为32767
(7FFF
)以得到1s
的计数频率。
2.2.1 RTC_PRLH

2.2.2 RTC_PRLL

2.3 RTC
预分频余数寄存器
预分频余数寄存器用于获取比秒钟更准确的时钟。该寄存器的值是自减的,用于保存还需要多少时钟周期获取的一个秒信号。在一次秒钟更新后,由硬件重新加载,这两个寄存器和RTC_PRL
的各位是一样的。
在TR_CLK
的每个周期里,RTC
预分频器中计数器的值都会被重新设置为RTC_PRL
寄存器的值。用户可通过读取RTC_DIV
寄存器,以获得预分频计数器的当前值,而不停止分频计数器的工作,从而获得精确的时间测量。
此寄存器是只读寄存器,其值在RTC_PRL
或RTC_CNT
寄存器中的值发生改变后,由硬件重新装载。
2.3.1 RTC_DIVH

2.3.2 RTC_DIVL

2.4 RTC
计数器寄存器
存放计数器内的计数值,也就是用来记录时钟时间。
该寄存器由2
个16
位的寄存器组成RTC_CNTH
和RTC_CNTL
,总共32
位,当进行读操作时,直接返回计数器内的计数值(系统时间)。
在修改这个寄存器的时候要先进入配置模式,即设置RTC_CRL
寄存器中的CNF
位,使RTC
进入配置模式。
2.4.1 RTC_CNTH

2.4.2 RTC_CNTL

2.5 RTC
闹钟寄存器
当可编程计数器RTC_CNT
的值与RTC_ALR
中的32
位值相等时,即触发一个闹钟事件,并且产生RTC
闹钟中断。
在修改这个寄存器的时候要先进入配置模式,即设置RTC_CRL
寄存器中的CNF
位,使RTC
进入配置模式。
2.5.1 RTC_ALRH

2.5.2 RTC_ALRL

2.6 BKP
备份寄存器
备份寄存器是42
个16
位的寄存器。可用来存储84
个字节数据。
它们处在备份区域,当VDD
电源切断,仍然由VBAT
维持供电。
当系统在待机模式下被唤醒,或者系统复位或者电源复位,它们也不会复位。执行以下操作将使能对后备寄存器和RTC
访问:
- 设置寄存器
RCC_APB1ENR
的PWREN
和BKPEN
位,使能电源和后备时钟; - 设置寄存器
PWR_CR
的DBP
位,使能对RTC
和后备寄存器的访问;
2.6.1 备份数据寄存器(BKP_DRx
)
一般用BKP
来存储RTC
的校验值或者记录一些重要的数据;

2.6.2 备份域控制寄存器 (RCC_BDCR
)
备份域控制寄存器中(RCC_BDCR
)的LSEON
、LSEBYP
、RTCSEL
和RTCEN
位处于备份域。
因此,这些位在复位后处于写保护状态,只有在电源控制寄存器(PWR_CR
)中的DBP
位置1
后才能对这些位进行改动。

三、RTC
源码
3.1 RTC
初始化步骤
RTC
配置流程如下:
(1) 时钟使能以及取消写保护;
-
使能电源时钟和备份区域时钟:通过配置
RCC_APB1ENR
的位28
和位27
;-
位
28
(PWREN
):电源接口时钟使能; -
位
27
(BKPEN
):备份接口时钟使能;
-
-
取消备份区写保护:通过配置
PWR_CR
的DBP
位(位8
)为1
来使能对后备寄存器和RTC
的访问;
(2) RTC
时钟配置 (复位,取消复位,时钟源选择,使能)->该模块是在备份区域;
-
备份区域软复位,复位结束;
- 通过配置
RCC_BDCR
寄存器位16
为1
备份区域软复位; - 通过配置
RCC_BDCR
寄存器位16
为0
备份区域软复位结束;
- 通过配置
-
开启
LSE
;-
通过配置
RCC_BDCR
寄存器位0
使能外部低速振荡器; -
通过读取
RCC_BDCR
寄存器位1
等待外部低速振荡器就绪;
-
-
选择
RTC
时钟,并使能:通过配置RCC_BDCR
的位[9:8
]和位15
;-
位[
9:8
]:RTC
是时钟源选择,配置为01
选择LSE
作为RTC
时钟; -
位
15
:RTC
时钟使能;
-
(3) 等待上一次RTC
写操作完成:判断上一次RTC
操作是否完成,通过等待RTC_CRL
位5
(RTOFF=1
)实现;
(4) 等待RTC
寄存器同步:判断RTC
寄存器是否同步,通过等待RTC_CRL
位3
(RSF=1
)实现;
(5) 如果使能时钟,可以配置RTC_CRH
寄存器,比如;
- 位
0
:允许秒中断; - 位
1
:允许闹钟中断; - 位
2
:允许溢出中断;
(6) 进入配置模式:允许配置,通过RTC_CRL
位4
(CNF=1
)实现;
(7) 配置RTC_CNT
,PRC_PRL
,PRC_ARL
;
- 配置预分频装载寄存器
RTC_PRL
为32767
; - 配置计数器寄存器
RTC_CNT
初始化时间; - 配置闹钟寄存器
RTC_ALR
;
(8) 更新RTC
:设置RTC_CRL
位4
(CNF=0
)退出配置模式;
(9) 等待RTC
更新完成,通过等待RTC_CRL
位5
(RTOFF=0
)实现;
(10) 设置初始化完成标志;
- 向
BKP_DR1
写入0x5050
作为已经初始化过时钟的标志;下次开机或复位的时候先读取BKP_DR1
,判断是否是0x5050
来决定是不是要配置;
(11) 设置NVIC
;
- 参考《
STM32F103
嵌套向量中断控制器》:设置中断优先级分组、设置响应优先级和抢断优先级、使能相应中断位;
(12) 中断处理函数;
- 设置中断服务函数(包括清除中断标志)。
3.2 源码实现
3.2.1 RTC_TypeDef
RTC
寄存器结构RTC_TypeDef
,在文件stm32f10x_map.h
中定义如下:
/*------------------------ Real-Time Clock -----------------------------------*/
typedef struct
{
vu16 CRH; // 控制寄存器高位 ;
u16 RESERVED0; //
vu16 CRL; // 控制寄存器低位 ;
u16 RESERVED1; //
vu16 PRLH; // 预分频装载寄存器高位 ;
u16 RESERVED2; //
vu16 PRLL; // 预分频装载寄存器低位 ;
u16 RESERVED3; //
vu16 DIVH; // 预分频分频因子寄存器高位 ;
u16 RESERVED4; //
vu16 DIVL; // 预分频分频因子寄存器低位 ;
u16 RESERVED5; //
vu16 CNTH; // 计数器寄存器高位 ;
u16 RESERVED6; //
vu16 CNTL; // 计数器寄存器低位 ;
u16 RESERVED7; //
vu16 ALRH; // 闹钟寄存器高位 ;
u16 RESERVED8; //
vu16 ALRL; // 闹钟寄存器低位 ;
u16 RESERVED9; //
} RTC_TypeDef;
#ifdef _RTC
#define RTC ((RTC_TypeDef *) RTC_BASE)
#endif /*_RTC */
在前面我们已经对RTC_TypeDef
结构体中定义的大部分寄存器进行了详细的介绍,那么我们如何编码去初始化这些寄存器呢?
3.2.2 RTC_Init
RTC
初始化函数RTC_Init
定义如下:
/**********************************************************************************
*
* 初始化步骤
* 1.时钟使能 取消写保护
* 2.RTC时钟配置 (复位,取消复位,时钟源选择,使能)->该模块是在备份区域
* 3.等待上一次RTC写操作完成
* 4.等待RTC寄存器同步
* 5.中断使能
* 6.进入配置模式
* 7.配置RTC_CNT,PRC_PRL,PRC_ARL
* 8.更新RTC
* 9.等待RTC更新完成
* 开启成功返回0,否则返回1
备份域控制寄存器中(RCC_BDCR)的LSEON、LSEBYP、RTCSEL和RTCEN位处于备份域。
由此,这些位在复位后被写保护,只有在电源控制寄存器(PWR_CR)中的DBP位置1之后才
能对这些位进行改动
*
***********************************************************************************/
u8 RTC_Init(void)
{
u8 temp=0;
if(BKP->DR1!=0X5050) //第一次初始化RTC
{
RCC->APB1ENR |=1<<28; //POWER时钟使能
RCC->APB1ENR |=1<<27; //BKPEN时钟使能
PWR->CR |=1<<8; //取消写保护
RCC->BDCR |=1<<16; //备份区域软复位
RCC->BDCR &=~(1<<16); //复位结束
RCC->BDCR |=1<<0; //开启LSE
while((!(RCC->BDCR&0X02))&&temp<250) //等待LSE就绪
{
temp++;
delay_ms(10);
}
if(temp>=250)
{
return 1; //初始化失败
}
RCC->BDCR |=1<<8; //LSE作为RTC时钟 (默认是无时钟的)
RCC->BDCR |=1<<15; //RTC时钟使能
while(!(RTC->CRL&(1<<5))); //等待上一次RTC写操作完成
while(!(RTC->CRL&(1<<3)))
printf("tring.......\n"); //等待RTC寄存器同步
RTC->CRH |=1; //开启秒钟中断
RTC->CRL |=1<<4; //CNF=1 进入配置模式
RTC->PRLL =32767;
RTC->PRLH =0;
RTC_Set(2015,12,3,13,25,10); //初始化时间
RTC->CRL &=~(1<<4); //CNF=0 更新RTC
while(!(RTC->CRL&(1<<5))); //等待RTC写操作完成
BKP->DR1=0X5050; //作为初始化完成的标志
}
else
{
while(!(RTC->CRL&(1<<3))); //等待RTC寄存器同步
RTC->CRH |=0X01; //开启秒钟中断 CR寄存器使用的是APB1时钟而不是RTCCLK
while(!(RTC->CRL&(1<<5))); //等待上一次RTC写操作完成
}
return 0;
}
3.2.3 RTC_Set
RTC
时间设定函数RTC_Set
定义如下:
u8 MonthTable[12]={31,28,31,30,31,30,31,31,30,31,30,31}; //存放每个月份的天数
//***************************闰年判定********************************************
u8 IsLeapYear(u16 year)
{
if(((year%4==0)&&(year%100!=0))||(year%400==0))
{
return 1;
}
else
{
return 0;
}
}
/********************************************************************************
*
* Description:RTC时间设定
* 以2000年1月1日为基址 星期六
*
**********************************************************************************/
void RTC_Set(u16 year,u8 month,u8 day,u8 hour,u8 min,u8 sec)
{
u16 t=0; //存储年份
u32 seccount; //存储秒钟数
for(t=2000;t<year;t++)
{
if(IsLeapYear(t))
seccount +=31622400; //闰年
else
seccount +=31536000; //平年
}
for(t=0;t<(month-1);t++)
{
seccount +=(u32)MonthTable[t]*86400;
if(IsLeapYear(year)&&(t==1))
seccount +=86400; //闰年二月29天
}
seccount +=(u32)(day-1)*86400; //天
seccount +=(u32)(hour)*3600; //时
seccount +=(u32)(min)*60; //分
seccount +=(u32)(sec); //秒
RCC->APB1ENR |=1<<28; //POWER时钟使能
RCC->APB1ENR |=1<<27; //BKPEN时钟使能 RTC模块和始终配置系统都在后备区域
PWR->CR |=1<<8; //取消写保护
RTC->CRL |=1<<4; //进入配置模式
RTC->CNTL =seccount&0xFFFF;
RTC->CNTH =seccount>>16;
RTC->CRL &=~(1<<4); //更新
while(!(RTC->CRL&(1<<5))); //等待配置完成
}
3.2.4 RTC_Get
RTC
时间获取函数RTC_Get
定义如下:
/********************************************************************************
*
* Description:RTC时间获取
* 以2000年1月1日为基址 星期六
*
**********************************************************************************/
void RTC_Get(void)
{
u32 seccount;
u16 daycount;
u16 temp=2000;
while(!(RTC->CRL&(1<<3))); //等待RTC寄存器已经同步
seccount =RTC->CNTH;
seccount <<=16;
seccount +=RTC->CNTL; //获取此时秒钟数
daycount =seccount/86400; //获取天数
if(daycount>=1) //大于1天
{
temp=2000;
while(daycount>=365) //闰年
{
if(IsLeapYear(temp))
{
if(daycount>=366)
daycount -=366;
else
break;
}
else
{
daycount -=365; //平年
}
temp++;
}
cale.year =temp; //获取年份
temp =0;
while(daycount>=28)
{
if((IsLeapYear(cale.year))&&(temp==1)) //闰年二月
{
if(daycount>=29)
daycount -=29;
else
break;
}
else
{
if(daycount>=MonthTable[temp])
daycount -=MonthTable[temp];
else
break;
}
temp++;
}
cale.month =temp+1; //得到月份
cale.day =daycount+1; //得到日期
}
temp =seccount%86400; //得到剩余秒钟数
cale.hour =temp/3600; //得到小时
cale.min =(temp%3600)/60; //得到分
cale.sec =(temp%3600)%60; //得到秒
temp =daycount%7;
switch(temp)
{
case 0:
cale.week = Sunday;
break;
case 1:
cale.week = Monday;
break;
case 2:
cale.week = Tuesday;
break;
case 3:
cale.week = Wednesday;
break;
case 4:
cale.week = Thursday;
break;
case 5:
cale.week = Friday;
break;
default:
cale.week = Saturday;
}
}
3.2.5 RTC_IRQHandler
RTC
中断处理函数RTC_IRQHandler
定义如下:
/*******************************************************************************
* Function Name : RTC_IRQHandler
* Description : This function handles RTC global interrupt request.
* Input : None
* Output : None
* Return : None
若ALRF=1且ALRIE=1,则允许产生RTC全局中断。如果在EXTI控制器中允许产生EXTI线 17
中断,则允许产生RTC全局中断和RTC闹钟中断。
*
*******************************************************************************/
void RTC_IRQHandler(void)
{
if(RTC->CRL&0X01) //秒中断
{
RTC_Get(); //获取当前时间
printf("当前时间为%d-%d-%d %d:%d",cale.year,cale.month,cale.day,cale.hour,cale.min);
switch(cale.week)
{
case 0:
printf("\tSunday\n");
break;
case 1:
printf("\tMonday\n");
break;
case 2:
printf("\tTuesday\n");
break;
case 3:
printf("\tWednesday\n");
break;
case 4:
printf("\tThursday\n");
break;
case 5:
printf("\tFriday\n");
break;
default:
printf("\tSaturday\n");
}
RTC->CRL &=~0X01; //清中断
}
if(RTC->CRL&0X02) //闹铃中断
{
RTC->CRL &=~(0X01<<1); //清中断
}
if(RTC->CRL&0X04) //溢出中断
{
RTC->CRL &=~(0X01<<2); //清中断 =
}
while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成
}
3.3 实现功能
这里我们实现一个很简单的功能,设置当前时间,每隔1s
中断输出一次当前时间。
3.3.1 main
函数实现
int main()
{
u32 temp=0;
STM32_Clock_Init(9); //系统时钟初始化
STM32_NVIC_Init(2,USART1_IRQn,0,1); //串口中断优先级初始化,其中包括中断使能
usart_init(USART_1,115200); //串口1初始化,波特率115200 映射到PA9 PA10
STM32_NVIC_Init(2,RTC_IRQn,0,1); //RTC中断优先级初始化,其中包括中断使能
while(RTC_Init()); //RTC初始化
// LED1初始化
gpio_init(PA8,GPO_SpeedMax_50,HIGH); //PA8接入LED1
while(1)
{
RTC_Get();
}
}
3.3.2 测试
编译程序并下载测试:

注意:如果串口输出中文为乱码,点击左上角【设置】-> 【选项】-> 【字符集编码】-> 【ANSI
(GBK
)】.
四、源码下载
源码下载路径:stm32f103
。
参考文章