RTC实时时钟实验
4 u/ z& I1 E0 M; P本章,我们将介绍STM32H750的内部实时时钟(RTC)。我们将使用LCD模块来显示日期和时间,实现一个简单的实时时钟,并可以设置闹铃,另外还将介绍BKP的使用。
' ?- o4 a9 y' Y, X
6 @! ^$ x+ Y8 `5 y" s6 { j27.1 RTC时钟简介
& z/ n) b$ c* `# w2 o% p% r9 B6 _; fSTM32H750的实时时钟(RTC)相对于STM32F1来说,改进了不少,带了日历功能了。RTC是一个独立的 BCD 定时器/计数器。RTC 提供一个日历时钟(包含年月日时分秒信息)、两个可编程闹钟(ALARM A和ALARM B)中断,以及一个具有中断功能的周期性可编程唤醒标志。RTC 还包含用于管理低功耗模式的自动唤醒单元。
0 o7 z9 R5 m& z S* M两个32位寄存器(TR和DR)包含二进码十进数格式 (BCD) 的秒、分钟、小时(12或24小时制)、星期、日期、月份和年份。此外,还可提供二进制格式的亚秒值。% a) g2 z! X( G7 V8 H
STM32H750的RTC可以自动将月份的天数补偿为 28、29(闰年)、30 和 31 天。并且还可以进行夏令时补偿。
1 b3 h( W/ m- ?/ y5 P) v \RTC模块和时钟配置是在后备区域,即在系统复位或从待机模式唤醒后RTC的设置和时间维持不变,只要后备区域供电正常,那么RTC将可以一直运行。但是在系统复位后,会自动禁止访问后备寄存器和RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP)写保护。: A3 N9 p. u i6 B. m
27.1.1 RTC框图
- H" k; ]3 m% ]* p下面先来学习RTC框图,通过学习RTC框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。RTC的框图,如图27.1.1所示:" ?0 n, n6 l2 M, |0 u- p; L
+ ]5 X; f3 b& ?' B) D) m8 C, D& a- Y$ s
# i! p( y+ L+ h) R/ K- s6 b8 J
图27.1.1 RTC框图
$ { r/ Z v' b我们把RTC框图分成以下几个部分讲解:
6 S4 @5 k: O7 ~/ f, s①时钟源
% K. j1 z2 O9 H, s: Z. uSTM32H750的RTC时钟源(RTCCLK)通过时钟控制器,可以从LSE时钟、LSI时钟以及HSE时钟三者中选择其一(通过设置RCC_BDCR寄存器选择)。一般我们选择LSE,即外部32.768Khz晶振作为时钟源(RTCCLK)。外部晶振具有精度高的优点。LSI是STM32芯片内部的低速RC 振荡器,频率约32 kHz,缺点是精度较低,可能会有温漂,所以一般不建议使用。比如当没有外部低速晶振(32.768Khz)的时候,分频后的HSE可以作为备选使用的时钟源。
+ {/ ]7 |7 N$ ~8 d* |②预分频器5 N9 k2 p7 Q9 c, `
预分配器(RTC_PRER)分为2个部分:一个通过RTC_PRER寄存器的PREDIV_A位配置的7位异步预分频器。另一个通过RTC_PRER寄存器的PREDIV_S位配置的15位同步预分频器。" [' ~# t: b" _) t$ B$ U: G
经过7位异步预分频器出来的时钟ck_apre可作为RTC_SSR亚秒递减计数器(RTC_SSR)的时钟,ck_apre时钟频率的计算公式如下:' o' F$ F- q6 {, g9 s1 {' w
: v( X% F6 \: `4 u当RTC_SSR寄存器递减到0的时候,会使用PREDIV_S的值重新装载PREDIV_S。而PREDIV_S一般为255,这样,我们得到亚秒时间的精度是:1/256秒,即3.9ms左右,有了这个亚秒寄存器RTC_SSR,就可以得到更加精确的时间数据。# s$ d, L; J, Z0 |3 L
经过15位同步预分频器出来的时钟ck_spre可以用于更新日历,也可以用作16位唤醒自动重载定时器的时基,ck_apre时钟频率的计算公式如下:
: [6 d; N0 Z7 h) Z& C3 r- b1 a$ ~) q* j8 |8 q
PREDIV_A和PREDIV_S分别为RTC的异步和同步分频器,使用两个预分频器时,我们推荐设置7位异步预分频器(PREDIV_A)的值较大,以最大程度降低功耗。例如:本实验我们的外部低速晶振的频率32.768KHZ经过7位异步预分频器后,再经过15位同步预分频器,要得到1HZ频率的时钟用于更新日历。通过计算知道,32.768KHZ的时钟要经过32768分频,才能得到1Hz的ck_spre。于是我们只需要设置:PREDIV_A=0X7F,即128分频;PREDIV_S=0XFF,即256分频,即可得到1Hz的Fck_spre,PREDIV_A的值我们也是往尽量大的原则,以最大程度降低功耗。7 j2 a' T$ C% v0 b
③时间和日期相关寄存器
% m# i( Y/ Q: P2 z% }- c" C& a该部分包括三个影子寄存器,RTC_SSR(亚秒)、RTC_TR(时间)、RTC_DR(日期)。实时时钟一般表示为:时/分/秒/亚秒。RTC_TR寄存器用于存储时/分/秒时间数据,可读可写(即可设置或者获取时间)。RTC_DR寄存器用于存储日期数据,包括年/月/日/星期,可读可写(即可设置或者获取日期)。RTC_SSR寄存器用于存储亚秒级的时间,这样我们可以获取更加精确的时间数据。- n4 R) \9 d, z! Z I
这三个影子寄存器可以通过与 PCLK4(APB4 时钟)同步的影子寄存器来访问,这些时间和日期寄存器也可以直接访问,这样可避免等待同步的持续时间。2 }5 a9 s O0 p" M g8 {7 Z( A9 }
每隔2个RTCCLK周期,当前日历值便会复制到影子寄存器,并置位RTC_ISR寄存器的RSF位。我们可以读取RTC_TR和RTC_DR来得到当前时间和日期信息,不过需要注意的是:时间和日期都是以BCD码的格式存储的,读出来要转换一下,才可以得到十进制的数据。* N& p4 C4 q9 q# U
④可编程闹钟
+ E% [$ p1 F+ | p" ?STM32H750提供两个可编程闹钟:闹钟A(ALARM_A)和闹钟B(ALARM_B)。通过RTC_CR寄存器的ALRAE和ALRBE位置1来使能闹钟。当亚秒、秒、分、小时、日期分别与闹钟寄存器RTC_ALRMASSR/RTC_ALRMAR和RTC_ALRMBSSR/RTC_ALRMBR中的值匹配时,则可以产生闹钟(需要适当配置)。本章我们将利用闹钟A产生闹铃,即设置RTC_ALRMASSR和RTC_ALRMAR即可。
. r7 a9 r2 ^# ]* x" H: S' s; z⑤周期性自动唤醒
' d/ s( T3 ?; a- W; f @STM32H750的RTC不带秒钟中断了,但是多了一个周期性自动唤醒功能。周期性唤醒功能,由一个16位可编程自动重载递减计数器(RTC_WUTR)生成,可用于周期性中断/唤醒。
; n3 I F% m1 P我们可以通过RTC_CR寄存器中的WUTE位设置使能此唤醒功能。& h" }0 W0 k6 Q" F! D% w
唤醒定时器的时钟输入可以是:2、4、8或16分频的RTC时钟(RTCCLK),也可以是ck_spre时钟(一般为1Hz)。
; p, O! y9 H v/ i5 S) K6 k& `& q当选择RTCCLK(假定LSE是:32.768 kHz)作为输入时钟时,可配置的唤醒中断周期介于 122us(因为RTCCLK/2时,RTC_WUTR不能设置为0)和32 s之间,分辨率最低为:61us。% {5 v: d+ t" e$ {
当选择ck_spre(1Hz)作为输入时钟时,可得到的唤醒时间为 1s到36h左右,分辨率为1 秒。并且这个1s~36h的可编程时间范围分为两部分:$ e9 J5 F' }% D0 i
当WUCKSEL[2:1]=10时为:1s到18h。% A H8 {: N6 C7 x& e; |
当WUCKSEL[2:1]=11时约为:18h到36h。! j% I& A1 n5 t/ T
在后一种情况下,会将2^16添加到16位计数器当前值(即扩展到17位,相当于最高位用WUCKSEL [1]代替)。) J# n; Q3 V1 B
初始化完成后,定时器开始递减计数。在低功耗模式下使能唤醒功能时,递减计数保持有效。此外,当计数器计数到0时,RTC_ISR寄存器的WUTF标志会置1,并且唤醒寄存器会使用其重载值(RTC_WUTR寄存器值)动重载,之后必须用软件清零WUTF标志。
) W" e& F/ U* d5 u通过将 RTC_CR寄存器中的WUTIE位置1来使能周期性唤醒中断时,可以使STM32H750退出低功耗模式。系统复位以及低功耗模式(睡眠、停机和待机)对唤醒定时器没有任何影响,它仍然可以正常工作,故唤醒定时器,可以用于周期性唤醒STM32H750。: g& _3 n9 {! N3 x8 C0 n
6 e k/ j! h% h n3 ]! G- U27.1.2 RTC寄存器
) F) f! E: a4 A2 }# T. ~接下来,我们介绍本实验我们要用到的RTC寄存器。
! C! U! c2 F: x, K2 [& cRTC时间寄存器(RTC_TR)1 u l4 ?! d* J, C- ?; l$ i
RTC时间寄存器描述如图27.1.2.1所示:7 V2 k0 d* m8 V1 I: v6 N. c
9 U9 J3 w: H! D& J4 T5 {
: d9 G2 C1 \ O
0 H9 H% {, C5 _! m) }; {
图27.1.2.1 RTC_TR寄存器
& O( g& ^! L- ^# ]+ Y该寄存器是RTC的时间寄存器,可读可写,对该寄存器写,可以设置时间,对该寄存器读,可以获取当前的时间,此外该寄存器受到寄存器写保护,通过RTC写保护寄存器(RTC_WPR)设置,后面会讲解到RTC_WPR寄存器。需要注意的是:本寄存器存储的数据都是BCD格式的,读取之后需要进行转换,方可得到十进制的时分秒等数据。* z0 w8 C9 u+ C
RTC日期寄存器(RTC_DR)
8 @& I# F2 _* C4 y9 e$ f0 s3 ~RTC日期寄存器描述如图27.1.2.2所示:2 m. E7 |: r# W8 U$ q# T. d
* @' e/ v% A6 T) q/ l" Y+ {4 d6 X
) O7 o" w& O# m, X" I- W; z# N6 ?5 z/ b
图27.1.2.2 RTC_DR寄存器% I2 s; s$ f, E- m1 g
该寄存器是RTC的日期寄存器,可读可写,对该寄存器写,可以设置日期,对该寄存器读,可以获取当前的日期,同样该寄存器也受到寄存器写保护,存储的数据也都是BCD格式的。- u3 j8 T. a; K/ B5 Y
RTC亚秒寄存器(RTC_SSR)
) c1 q- ~5 H5 aRTC亚秒寄存器描述如图27.1.2.3所示:
- P# ]% f( S4 U* h+ ~8 w' h
3 w& _9 T$ o. F8 i" Y1 I
/ S' |/ @5 W2 @
# i( a' G) h; x/ k
图27.1.2.3 RTC_SSR寄存器
% n4 C. F& m5 m0 x/ [7 H' F9 j该寄存器可用于获取更加精确的RTC时间。不过,在本章没有用到,如果需要精确时间的地方,大家可以使用该寄存器。
; H2 E: L/ ^: B4 ` iRTC控制寄存器(RTC_CR)- r! c3 ?2 @& F4 \( a3 h; z4 S
RTC控制寄存器描述如图27.1.2.4所示:
5 H' U" H; [$ n5 F5 z2 L; ^: l: o$ V( C$ i2 E. E8 Y8 X
' w' \# F! J2 q+ `4 C
8 j. j4 p# c( ]- `9 V' A; L% @
图27.1.2.4 RTC_CR寄存器3 I( H, N2 ^& Q8 H: R) C
该寄存器重点介绍几个要用到的位:WUTIE是唤醒定时器中断使能位,ALRAIE是闹钟A中断使能位,本章用到这两个使能位,都设置为1即可。WUTE和ALRAE分别是唤醒定时器和闹钟A使能位,同样都设置为1,开启。FMT为小时格式选择位,我们设置为0,选择24小时制。WUCKSEL[2:0],用于唤醒时钟选择,这个前面已经有介绍了,我们这里就不多说了。7 e' B3 U ?# W1 y
RTC初始化和状态寄存器(RTC_ISR)+ |& L2 V- }3 S c* ^0 D ]9 j
RTC初始化和状态寄存器描述如图27.1.2.5所示:
9 J# B+ R% H+ U+ v0 z& T- m# J& M j8 J
) ^- d* g% l. \- Y$ k
/ c; T7 ]. C! S& l! U& _2 r3 m% K& ?图27.1.2.5 RTC_ISR寄存器
' G5 \# O1 R5 U7 m该寄存器中,WUTF、ALRBF和ALRAF,分别是唤醒定时器、闹钟B和闹钟A的中断标志位,当对应事件产生时,这些标志位被置1,如果设置了中断,则会进入中断服务函数,这些位通过软件写0清除。
9 z2 j* D: v! n. J k9 SINIT为初始化模式控制位,要初始化RTC时,必须先设置INIT=1。! I3 E7 |) }% K
INITF为初始化标志位,当设置INIT为1以后,要等待INITF为1,才可以更新时间、日期和预分频寄存器等。. R4 X, r* P4 y' U3 }. m
RSF位为寄存器同步标志,仅在该位为1时,表示日历影子寄存器已同步,可以正确读取RTC_TR/RTC_TR寄存器的值了。
" k& p5 f$ I" d: e' rWUTWF、ALRBWF和ALRAWF分别是唤醒定时器、闹钟B和闹钟A的写标志,只有在这些位为1的时候,才可以更新对应的内容。比如:要设置闹钟A的ALRMAR和ALRMASSR,则必须先等待ALRAWF为1,才可以设置。
9 \( E/ l, n6 O; URTC预分频寄存器(RTC_PRER)
4 z: H$ f0 t* @- `9 IRTC预分频寄存器描述如图27.1.2.6所示:$ g! d" u/ }+ t! N x! y5 r7 n# I" W! S
9 D, h: Z: |- Z. C# H+ b7 ` @
1 B" n$ l9 G5 ]0 K" G( c/ R# H
( {# A) a$ v: U ]* T
图27.1.2.6 RTC_PRER寄存器8 n9 P( Y' L7 T9 z Q8 E2 ?
该寄存器用于RTC的分频,我们在之前也有讲过,这里就不多说了。该寄存器的配置,必须在初始化模式(INITF=1)下,才可以进行。! [0 W+ ~8 c0 x
RTC唤醒寄存器(RTC_WUTR)
; X5 m M% e% U" |8 ~$ {RTC唤醒寄存器描述如图27.1.2.7所示:
1 t2 k! n2 N# q, X
7 S7 K* P$ d7 s6 D/ \" D$ H" o
0 m' S8 z- m# y0 B0 F7 d; z% l
, N( h9 ~6 k3 s3 g图27.1.2.7 RTC_WUTR寄存器1 z8 _7 e2 w' a
该寄存器用于设置自动唤醒重装载值,可用于设置唤醒周期。该寄存器的配置,必须等待RTC_ISR的WUTWF为1才可以进行。$ s. X7 L: O' a6 l
RTC闹钟A寄存器(RTC_ALRMAR)
, K. c/ E c2 i; U3 m$ a( f3 |RTC闹钟A寄存器描述如图27.1.2.8所示:2 K: u3 z4 _5 z# N) {) e3 l; e
9 o+ Y# O+ _% m/ o p
c6 S" J" k. l; h2 {9 R# ~& Z V
. e( f8 y( {5 n8 |8 w图27.1.2.8 RTC_WUTR寄存器( Y0 @7 [( A) U6 j
该寄存器用于设置闹铃A,当WDSEL选择1时,使用星期制闹铃,本章我们选择星期制闹铃。该寄存器的配置,必须等待RTC_ISR的ALRAWF为1才可以进行。另外,还有RTC_ALRMASSR寄存器,该寄存器我们这里就不再介绍了,大家参考手册。! @9 m0 G; Z5 G4 Q+ F
RTC写保护寄存器(RTC_WPR)
9 n4 G! |2 g; r' y, P# sRTC写保护寄存器:RTC_WPR,该寄存器比较简单,低八位有效。上电后,所有RTC寄存器都受到写保护(RTC_ISR[13:8]、RTC_TAFCR和RTC_BKPxR除外),必须依次写入:0XCA、0X53两关键字到RTC_WPR寄存器,才可以解锁。写一个错误的关键字将再次激活RTC的寄存器写保护。9 k+ Z0 g( J/ K8 [
RTC备份寄存器(RTC_BKPxR); m& {. P) s X: U
RTC备份寄存器描述如图27.1.2.9所示:
% b5 D$ O* N* E% i+ p" u
2 {- S6 U- s% h8 h A, k
3 e/ n. z5 f* B O9 x- y
2 {! c. p$ W% O: K/ v3 l7 |
图27.1.2.9 RTC_BKPxR寄存器
9 M! r2 g& k3 i( `( w! v1 ~该寄存器组总共有32个,每个寄存器是32位的,可以存储128个字节的用户数据,这些寄存器在备份域中实现,可在VDD电源关闭时通过VBAT保持上电状态。备份寄存器不会在系统复位或电源复位时复位,也不会在MCU从待机模式唤醒时复位。
* G3 v; O0 y+ z5 Q9 E) l* g' |3 e复位后,对RTC和RTC备份寄存器的写访问被禁止,执行以下操作可以使能对RTC及RTC备份寄存器的写访问:. C: k. `- n- p3 I
1)电源控制寄存器(PWR_CR)的DBP位来使能对RTC及RTC备份寄存器的访问。8 r& L' h( s1 N7 m
2)往RTC_WPR写入0XCA、0X53解锁序列(先写0XCA,再写0X53)。
5 M- b9 x5 ?7 [5 B我们可以用BKP来存储一些重要的数据,相当于一个EEPROM,不过这个EEPROM并不是真正的EEPROM,而是需要电池来维持它的数据。
' s" E# ?% H8 r! k; u5 m6 }备份区域控制(RCC_BDCR)# g9 ^" d3 w' E5 E* n% J) U: N+ T
备份区域控制寄存器描述如图27.1.2.10所示:/ w/ G" H' j! _( X2 T
# o- f1 V$ z+ i8 u2 E1 L, ~& s
T4 |0 x1 t: G: A1 r8 t$ `
- i- n5 t, d( C- I& X/ b图27.1.2.10 RCC_BDCR寄存器 ^( @7 Y; p4 A6 X& ~9 R4 N
RTC的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在RTC操作之前先要通过这个寄存器选择RTC的时钟源,然后才能开始其他的操作。
& X4 o4 k% x. D9 z$ Q/ R' I' }5 y4 L! s8 ?+ X4 K
27.2 硬件设计% q" J! h7 t7 F
1.例程功能
1 c4 k9 S# q$ B9 z+ [7 X4 ?本实验通过LCD显示RTC时间,并可以通过usmart设置RTC时间,从而调节时间,或设置RTC闹钟,还可以写入或者读取RTC后备区域SRAM。LED1每两秒闪烁一次,表示进入WAKE UP中断。LED0闪烁,提示程序运行。4 ]/ t; m/ E, p" \
2.硬件资源7 Q; J K |+ _/ x/ m; n
1)RGB灯" p" A( c. W, U
RED : LED0 - PB4
0 k3 f7 h' G ^) Q+ XGREEN : LED1 - PE6- d- _. z% ^- Y! |5 |6 t
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)+ [2 ~3 J. t0 }* H" ?
3)RTC(实时时钟)
. P+ O6 O# [7 f1 U8 U- V4)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
# f7 B3 M! j$ k; _ r+ D% `1 e8 U( P3.原理图0 y6 s" h7 v) d9 H! E" c* Y+ J D; G8 b
RTC属于STM32H750内部资源,通过软件设置好就可以了。不过RTC不能断电,否则数据就丢失了,我们如果想让时间在断电后还可以继续走,那么必须确保开发板的电池有电。 G, s- }/ v' d! S
. f" V+ C& P: I' D
27.3 程序设计5 M; r9 Z4 e" V4 Z- @
27.3.1 RTC的HAL库驱动
6 Q$ D+ [. K8 a; e& iRTC在HAL库中的驱动代码在stm32h7xx_hal_rtc.c文件(及其头文件)中。下面介绍几个重要的RTC函数,其他没有介绍的请看源码。
5 ?% Z. k. F* q8 b2 `' w2 i4.HAL_RTC_Init函数
! @/ N! b. {3 X/ i |5 KRTC的初始化函数,其声明如下:
- T8 R% { H1 q# B mHAL_StatusTypeDef HAL_RTC_Init(RTC_HandleTypeDef *hrtc);+ ^ M, h9 d6 b- }- O g
函数描述:, U; ~4 t* [8 _' k7 ]/ g" V
用于初始化RTC。3 B o9 \( |7 n" O* u. r
函数形参:
) `0 F- [0 C9 G3 [% o7 X形参1是RTC_HandleTypeDef结构体类型指针变量,其定义如下:" q4 q: j; j* ^. C; n" a0 z
- typedef struct
, x. J+ E3 f/ B9 ?8 H - {
: h; g$ g( o$ k - RTC_TypeDef *Instance; /* 寄存器基地址 */
. U5 J2 C* D+ {, C2 v# k - RTC_InitTypeDef Init; /* RTC配置结构体 */
; H8 v) K a5 G$ V, q5 g% u* o) k - HAL_LockTypeDef Lock; /* RTC锁定对象 */3 n9 C% ~/ Z9 @& v5 @
- __IO HAL_RTCStateTypeDef State; /* RTC设备访问状态 */
& K+ [6 y% u. A/ F - }RTC_HandleTypeDef;# O9 F8 n& s9 f+ z
- 1)Instance:指向RTC寄存器基地址。8 a/ |7 M, X- I# }$ E; b
- 2)Init:是真正的RTC初始化结构体,其结构体类型RTC_InitTypeDef定义如下:
7 ^( ?7 D% `8 L - typedef struct/ J% }9 l2 l, s0 c
- {
/ H7 Y; }* j/ e4 P; a - uint32_t HourFormat; /* 小时格式 */' n0 j* H: x6 L2 X, R6 ]6 J
- uint32_t AsynchPrediv; /* 异步预分频系数 */+ u# I# n9 Y9 ?, h0 E
- uint32_t SynchPrediv; /* 同步预分频系数 */
+ S5 E7 s; b0 a2 z( _" s$ Q6 c - uint32_t OutPut; /* 选择连接到RTC_ALARM输出的标志 */ ) t( B$ J: u" G1 b* {) x
- uint32_t OutPutRemap; /* 指定RTC输出的映射 */
" l; n4 }* R% e& M( h7 v/ V& G: v - uint32_t OutPutPolarity; /* 设置RTC_ALARM的输出极性 */4 p0 K+ w5 O0 T# r% X
- uint32_t OutPutType; /* 设置RTC_ALARM的输出类型为开漏输出还是推挽输出 */
% o- p1 `9 w8 J, L - }RTC_InitTypeDef;
复制代码
! x e& K, i& F6 A2 [2 ]HourFormat用来设置小时格式,可以是12小时制或者24小时制,这两个选项的宏定义分别为RTC_HOURFORMAT_12和RTC_HOURFORMAT_24。
# i) A- U n6 J% [! [& J6 _) F. pAsynchPrediv用来设置RTC的异步预分频系数,也就是设置RTC_PRER寄存器的PREDIV_A相关位,因为异步预分频系数是7位,所以最大值为0x7F,不能超过这个值。
8 [( Q( P! C( pSynchPrediv用来设置RTC的同步预分频系数,也就是设置RTC_PRER寄存器的PREDIV_S相关位,因为同步预分频系数也是15位,所以最大值为0x7FFF,不能超过这个值。
) P* [2 e% I2 t7 P- ~& V8 y' I$ pOutPut用来选择要连接到RTC_ALARM输出的标志,取值为:RTC_OUTPUT_DISABLE(禁止输出),RTC_OUTPUT_ALARMA(使能闹钟A输出),RTC_OUTPUT_ALARMB(使能闹钟B输出)和RTC_OUTPUT_WAKEUP(使能唤醒输出)。
) C+ M( i( i- g: G: {. S8 \OutPutRemap用于设置指定RTC输出的映射,即配置RTC_OR寄存器。( h) l. { {) Y$ S( l
OutPutPolarity用来设置RTC_ALARM的输出极性,与Output成员变量配合使用,取值为RTC_OUTPUT_POLARITY_HIGH(高电平)或RTC_OUTPUT_POLARITY_LOW(低电平)。
/ [$ }, ~ N! E% @6 aOutPutType用来设置RTC_ALARM的输出类型为开漏输出(RTC_OUTPUT_TYPE_ OPENDRAIN)还是推挽输出(RTC_OUTPUT_TYPE_PUSHPULL),与成员变量OutPut和OutPutPolarity配合使用。
" L+ F4 Q1 P% {3)Lock:用于配置锁状态。
0 a, ~ f! Y* T$ K" v4)State:RTC设备访问状态。0 V8 L$ l R2 I9 C3 u# M" v3 ?; |( q
函数返回值:
4 m9 G# M X9 b# m- {4 RHAL_StatusTypeDef枚举类型的值。
: w* C0 ?7 j) q) `0 ~' h2 `9 z; \2. HAL_RTC_SetTime函数# g8 @: N" j$ G9 ?* y
HAL_RTC_SetTime是设置RTC的时间函数。其声明如下:
- ^5 Q& H3 J' c8 \9 nHAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc,8 T% G. L3 S2 Z! [- l5 d
RTC_TimeTypeDef *sTime, uint32_t Format); f; Y& W" V- w) b6 C* g
函数描述:8 P: D0 h- ~/ \# W( l
该函数用于设置RTC的时间,即设置时间寄存器RTC_TR的相关位的值。
5 ?5 N5 l" B1 }' X/ b% Y$ J函数形参:
9 _: L- M$ N6 J" o1 E% d形参1是RTC_HandleTypeDef结构体类型指针变量,即RTC的句柄。8 i" l2 @0 P8 E+ K/ K' a) G
形参2是RTC_TimeTypeDef结构体类型指针变量,定义如下:0 ~: x4 U+ [( s+ t
3 }7 ?$ \! L; `* F
- typedef struct/ C5 T. [! K% R. y9 U8 C6 q/ b9 X
- {
* S2 L" V9 ?* ` l* _/ j, u - uint8_t Hours;
/ p) ^2 q( R# |( X - uint8_t Minutes;. e, C1 d' Y1 ^/ O6 `. I" v
- uint8_t Seconds;, Q9 l# q/ d, ]5 H, `+ s9 {6 p, b
- uint8_t TimeFormat;5 r9 o. A* t+ S& D+ X. H
- uint32_t SubSeconds;2 o) H' Z2 \1 I+ N
- uint32_t SecondFraction;
8 b ^ H) O7 W) C5 B7 [ - uint32_t DayLightSaving;
2 G$ X. y( d$ h& U% x6 { - uint32_t StoreOperation;
2 _) x( G1 ?2 f9 G- q4 }8 X - }RTC_TimeTypeDef;
复制代码 + t/ ^- n& e, V$ ^
前面四个成员变量就比较好理解了,分别用来设置RTC时间参数的小时,分钟,秒钟,以及AM/PM符号,大家参考前面讲解的RTC_TR的位描述即可。SubSeconds用来读取保存亚秒寄存器 RTC_SSR的值,SecondFraction用来读取保存同步预分频系数 的值,也就是RTC_PRER的位0~14,DayLightSaving用来设置日历时间增加1小时,减少1小时,还是不变。StoreOperation用户可对此变量设置以记录是否已对夏令时进行更改。
+ q' A3 K( m# m" D: C/ b形参3是uint32_t类型变量,用来设置输入的时间格式为BIN格式还是BCD格式,可选值为RTC_FORMAT_BIN或者RTC_FORMAT_BCD。1 d: s5 J! }3 w3 W6 V% p
函数返回值:
* C4 d2 K/ z6 n" nHAL_StatusTypeDef枚举类型的值。
$ e- \# P( A0 U' f8 v/ e" Y3. HAL_RTC_SetDate函数$ M- o: k. ~/ X4 ?% Y
HAL_RTC_SetDate是设置RTC的日期函数。其声明如下:
8 L; i6 T3 d) L! uHAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc,. D; n+ G. ~# z. i
RTC_DateTypeDef *sDate, uint32_t Format);8 B1 |& k: j W! k8 t$ D+ j
函数描述:0 v& W- j: [ _2 V1 S
该函数用于设置RTC的日期,即设置日期寄存器RTC_DR的相关位的值。, o3 q/ \+ d) A: o7 q
函数形参:
0 Z/ O# l% c' U4 q- w形参1是RTC_HandleTypeDef结构体类型指针变量,即RTC的句柄。0 ?- ]4 y. E, W1 \2 T6 P7 a6 _' c
形参2是RTC_DateTypeDef结构体类型指针变量,定义如下:
$ P& L! @2 }! b8 X) ^1 E
" j# J& ]1 U3 u3 M* j5 U. J- typedef struct
+ t. T& K1 |9 c- L( S2 l, b - {
9 ]0 a0 }6 `1 Y! i( e$ N - uint8_t WeekDay; /* 星期 */
0 i0 F. _. }7 g: K3 R - uint8_t Month; /* 月份 */, o* h: Q6 ]6 @/ ?; x, ]/ q7 q
- uint8_t Date; /* 日期 */
" A7 f7 N0 K3 E+ Q - uint8_t Year; /* 年份 */
/ [9 W! c' m: M( _$ f; A - }RTC_DateTypeDef;
复制代码 7 F" Y6 z& V* z3 a& q; n+ ^5 P1 [ z
结构体一共四个成员变量,这四个成员变量分别对应星期、月份、日期和年份,对应的是RTC_DR寄存器。
/ S1 O; S) R( A, K7 s+ l" b) b形参3是uint32_t类型变量,用来设置输入的时间格式为BIN格式还是BCD格式,可选值为RTC_FORMAT_BIN或者RTC_FORMAT_BCD。
. _6 `5 b3 G h$ I( a" F* | h函数返回值:
7 \7 h) d+ m, c) XHAL_StatusTypeDef枚举类型的值。4 h( k+ \. p5 N8 d, m
4. HAL_RTC_GetTime函数
* g C( n6 q$ ? U, A7 p) wHAL_RTC_GetTime是获取当前RTC时间函数。其声明如下:4 ~8 y* t; Z- _+ k0 ~; B
HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc,
) S" E$ u+ m' e5 r+ ~RTC_TimeTypeDef *sTime, uint32_t Format);
1 u$ ^( X7 i3 v函数描述:+ L% w) R+ f. o
该函数用于获取当前RTC时间,即读时间寄存器RTC_TR的相关位的值。2 Q' u( M/ |! D- W; p5 `5 U
函数形参:
; i2 A2 U; _( ?2 f4 E& a形参1是RTC_HandleTypeDef结构体类型指针变量,即RTC的句柄。
Z* L: N# y* H* K形参2是RTC_TimeTypeDef结构体类型指针变量,对应的是RTC_TR寄存器。
" [% O6 `8 k& y4 r3 i0 w形参3是uint32_t类型变量,用来设置获取的时间格式为BIN格式还是BCD格式,可选值为RTC_FORMAT_BIN或者RTC_FORMAT_BCD。
3 u' { s8 w& ^函数返回值:
. @) C& B* K* S0 T% \$ jHAL_StatusTypeDef枚举类型的值。; f9 M, x: f8 B; y
5. HAL_RTC_SetDate函数
' y" t. {* r: I% C1 Z% R; eHAL_RTC_SetDate是获取当前RTC日期函数。其声明如下:
$ w3 K; o6 g- ~, B$ i4 Y+ IHAL_StatusTypeDef HAL_RTC_SetDate (RTC_HandleTypeDef *hrtc,
% b! Q2 q% q! t- f! M4 ARTC_DateTypeDef *sDate, uint32_t Format);; j6 _# n/ C3 W ~8 f- S ?4 `
函数描述:
8 t$ d* N, G$ V' X! ]' F该函数用于获取当前RTC日期,即读时间寄存器RTC_DR的相关位的值。/ d/ {( I4 @% H, o
函数形参:4 m+ e9 J; R2 K
形参1是RTC_HandleTypeDef结构体类型指针变量,即RTC的句柄。5 B9 O7 k# x9 y2 o' L9 T- f' M6 e
形参2是RTC_DateTypeDef结构体类型指针变量,对应的是RTC_DR寄存器。
! v* n3 U, C! F8 c形参3是uint32_t类型变量,用来设置获取的时间格式为BIN格式还是BCD格式,可选值为RTC_FORMAT_BIN或者RTC_FORMAT_BCD。
+ F4 h" R: V- F9 L& a6 O函数返回值:, E5 d" J( h0 {
HAL_StatusTypeDef枚举类型的值。' c$ H% H6 x% m' }1 Z' w
6. HAL_RTC_SetAlarm_IT函数% u* {! U8 G$ b) F X- ~* i: y6 U
HAL_RTC_SetAlarm_IT是设置闹钟并开启闹钟中断的函数。其声明如下:( g C; _: h s2 m) Q4 ?* z6 u. Q
HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc,4 u+ l. K& J! C. C" c3 r+ J$ l
RTC_AlarmTypeDef *sAlarm, uint32_t Format);& g7 _9 F: _7 v* a
函数描述:
, L& ?9 F9 M; A+ G# z% _该函数用于设置闹钟并开启闹钟中断。
9 h) o& A! t2 j函数形参:- u2 {# E2 P* a8 i' J7 ~, W+ S; ^
形参1是RTC_HandleTypeDef结构体类型指针变量,即RTC的句柄。& ~* S, C. c. I
形参2是RTC_AlarmTypeDef结构体类型指针变量。+ K2 d' `* |3 ~9 F& ]9 T. i6 w/ M
形参3是uint32_t类型变量,用来设置获取的时间格式为BIN格式还是BCD格式,可选值为RTC_FORMAT_BIN或者RTC_FORMAT_BCD。& {- e- b, ~# q l' n
重点介绍RTC_AlarmTypeDef 结构体指针类型,结构体定义如下:
/ ^7 `# u) n! I
$ X0 @( A0 G. P/ ~+ r s- typedef struct
k* K+ `5 w* E) ] - {" f; k- O4 t+ y, G2 b' r( w
- RTC_TimeTypeDef AlarmTime; & H/ n h- f: `: X i& x
- uint32_t AlarmMask; 2 k5 q: I/ e7 v" I; Y& y
- uint32_t AlarmSubSecondMask;5 U5 a% |5 b% W7 j/ f% K1 D8 Y
- uint32_t AlarmDateWeekDaySel;
' W! s& G6 [+ P/ `" {' Y1 N. W- n3 L! m - uint8_t AlarmDateWeekDay;
+ O: Q) J# ?' u: N+ {2 i - uint32_t Alarm; * z: K# k K/ n. t: |) H
- }RTC_AlarmTypeDef;
复制代码 3 w1 y( I) g# N3 A8 X5 \: U
AlarmTime用来设置闹钟时间,是RTC_TimeTypeDef结构体类型,该结构体前面我们已经讲解过各个成员变量含义。
$ A# \$ }$ R+ cAlarmMask用来设置闹钟时间掩码,也就是在我们第一个参数设置的时间中(包括后面参数RTC_AlarmDateWeekDay设置的星期几/哪一天),哪些是无关的。 比如我们设置闹钟时间为每天的10点10分10秒,那么我们可以选择值RTC_AlarmMask_DateWeekDay,也就是我们不关心是星期几/每月哪一天。这里我们选择为RTC_AlarmMask_None,也就是精确匹配时间,所有的时分秒以及星期几/(或者每月哪一天)都要精确匹配。
$ O0 P. F k5 O8 m+ ?& ^AlarmSubSecondMask和AlarmMask作用类似,只不过该变量是用来设置亚秒的。8 u; F- O! o8 K/ ^! G+ F
AlarmDateWeekDaySel用来选择是闹钟是按日期还是按星期。比如我们选择RTC_AlarmDateWeekDaySel_WeekDay那么闹钟就是按星期。如果我们选择RTC_AlarmDateWeekDaySel_Date那么闹钟就是按日期。这与后面第四个参数是有关联的,我们在后面第四个参数讲解。- h0 y- {, A* \' x, v
AlarmDateWeekDay用来设置闹钟的日期或者星期几。比如我们第三个参数RTC_AlarmDateWeekDaySel设置了值为RTC_AlarmDateWeekDaySel_WeekDay,也就是按星期,那么参数RTC_AlarmDateWeekDay的取值范围就为星期一到星期天,也就是RTC_Weekday_Monday到RTC_Weekday_Sunday。如果第三个参数RTC_AlarmDateWeekDaySel设置值为RTC_AlarmDateWeekDaySel_Date,那么它的取值范围就为日期值,0~31。
4 m. R2 p# U3 [5 a6 T1 p# s, KAlarm用来设置是闹钟A还是闹钟B,这个很好理解。
$ h1 I5 ?0 B2 i9 O% [( `5 _函数返回值:2 V- q1 C5 P v& X; e
HAL_StatusTypeDef枚举类型的值。
/ d- H, D2 D% L' _# fRTC配置步骤
2 E0 N; i) o! `% ?) i+ m1)使能RTC时钟,并使能RTC及RTC后备寄存器写访问0 _. i. X2 d, S0 _ I' C$ L& v
复位后,后备区域存在写保护,RTC属于后备区域,所以我们需要取消后备区域写保护。同时,我们需要使能RTC时钟,通过RCC_APB4ENR寄存器中的RTCAPBEN位设置。而取消后备区域写保护则是通过PWR_CR1寄存器中的DBP位去设置。HAL库设置方法为:( Q Q7 e- Y8 G3 K: M* h
__HAL_RCC_RTC_CLK_ENABLE(); /* 使能RTC时钟 /
2 ` g! R: o2 l" M0 gHAL_PWR_EnableBkUpAccess(); / 取消备份区域写保护 */7 X; ~9 R3 `* T0 M! d) Q
2)开启外部低速振荡器LSE,选择RTC时钟,并使能
S0 K& L* n9 t2 T调用HAL_RCC_OscConfig函数配置开启LSE。
, w7 o) }' N0 O2 S# V$ M" I调用HAL_RCCEx_PeriphCLKConfig函数选择RTC时钟源。0 Q4 J2 d1 p4 a) @+ v; J5 }, }8 m. S
使能RTC函数为:__HAL_RCC_RTC_ENABLE。0 K l0 ~, Q9 Z; j1 { O. O$ `8 L
3)初始化RTC,设置RTC的分频,以及配置RTC参数0 G) ?) [ H) _2 D0 p
在HAL中,通过HAL_RTC_Init函数配置RTC分频系数,以及RTC的工作参数。0 {7 ]- x) P$ b7 \* q
注意:该函数会调用:HAL_RTC_MspInit函数来完成对RTC的底层初始化,包括:RTC时钟使能、时钟源选择等。0 f7 a9 d) N6 s( \9 K
4)设置RTC的时间$ ^$ F8 Y1 r( _! I$ F
调用HAL_RTC_SetTime函数设置RTC时间,该函数实际设置时间寄存器RTC_TR的相关位的值。# V# D @# R) A% V6 P2 M2 }
5)设置RTC的日期
; a- f' ~9 D. i. O) d+ y) j调用HAL_RTC_SetDate函数设置RTC的日期,该函数实际设置日期寄存器RTC_DR的相关位的值。) _1 J |7 ^0 l" D
6)获取RTC当前日期和时间8 e9 i1 \& l! f
调用HAL_RTC_GetTime函数获取当前RTC时间,该函数实际读取RTC_TR寄存器,然后将值存放到相应的结构体中。
; y G J; d. W! W调用HAL_RTC_GetDate函数获取当前RTC日期,该函数实际读取RTC_DR寄存器,然后将值存放到相应的结构体中。7 W. A3 S- ~2 C+ n
通过以上6个步骤,我们就完成了对RTC的配置,RTC即可正常工作,而且这些操作不是每次上电都必须执行的,可以视情况而定。当然,我们还可以唤醒中断、闹钟等,这些将在后面介绍。7 B+ S* r; f ^& n' {+ D
9 t- O1 a* `) G% y! C G27.3.2 程序流程图& F! `# S9 [1 b. y1 q
8 {8 g% n. ^& Q6 D7 {) r
6 m( f o3 u( d1 u& q! |3 N: n# C5 `
% j! X* [+ _7 e G5 M
图27.3.2.1 RTC实时时钟实验程序流程图
3 |7 T! K9 ?% m) k# ]' R8 d# W
* M: w1 G( t+ C, n. {1 i- G27.3.3 程序解析
( [! e3 E$ v4 u) p1 r8 T1.RTC驱动代码0 {; M) q* g2 U
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。RTC驱动源码包括两个文件:rtc.c和rtc.h。
' F! z# e" v9 `) d8 z7 ]1 @0 _由于篇幅所限,rtc.c中的代码,我们不全部贴出了,只针对几个重要的函数进行介绍。
5 X1 Z m9 ?+ t: f" h# a' D# U/ nrtc.h头文件只有函数的声明,下面我们直接介绍rtc.c的程序,先看RTC的初始化函数,其定义如下:
2 U% J: e7 F' C9 v$ E- G3 {9 ]- /**
6 n! p! u3 a$ s; E# B; E2 q: B - * @brief RTC初始化: ^- [0 F8 X7 }' Q _
- * @note
& l! K0 y D' Q% o6 R: S2 E2 n% y' s - * 默认尝试使用LSE,当LSE启动失败后,切换为LSI.: w I `. @& T: I9 J
- * 通过BKP寄存器0的值,可以判断RTC使用的是LSE/LSI:
" q8 Y' ~* r1 I3 t( Q* ` - * 当BKP0==0X5050时,使用的是LSE
$ F2 B0 Z3 L% {/ E7 a4 z$ E - * 当BKP0==0X5051时,使用的是LSI2 W1 V8 W% _, d4 q: M& C6 t' _
- * 注意:切换LSI/LSE将导致时间/日期丢失,切换后需重新设置.
- {. } I2 P+ _ - *
; y* @6 R* j& X" l& }- y8 w! `* | - * @param 无& r- s+ H: c* E5 s# ^- K) o. K
- * @retval 0,成功
+ |4 p! h" A4 L2 [0 H0 g5 t) a - * 1,进入初始化模式失败
" v5 L0 l) j0 k0 h9 w) c2 _$ u - */
+ r% n% Y0 j4 b0 s/ Q9 a - uint8_t rtc_init(void)' I* ^, n& C. _9 d5 S- k' W
- {
* M7 O: F! E& z( {# h# c - /* 检查是不是第一次配置时钟 */; c+ ~7 z1 A: v( t! z, J$ t
- uint16_t bkpflag = 0;3 n6 c, l3 ~( X! H b! e
- : ]9 Y5 a1 e6 H: b- Z
- g_rtc_handle.Instance = RTC;
" N1 D' D0 Y4 l* f e+ { - g_rtc_handle.Init.HourFormat = RTC_HOURFORMAT_24; /* RTC设置为24小时格式 */) ?9 C8 W) p9 u3 L, E* [
- g_rtc_handle.Init.AsynchPrediv = 0X7F; /* RTC异步分频系数(1~0X7F) */* H1 J' _0 Z/ L( a* U& q8 w/ e* M
- g_rtc_handle.Init.SynchPrediv = 0XFF; /* RTC同步分频系数(0~7FFF) */
8 J- v# q1 Y4 r - g_rtc_handle.Init.OutPut = RTC_OUTPUT_DISABLE;
~. g0 |: I q+ s' v - g_rtc_handle.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
. z0 N# w. b: y! H2 j q" D - g_rtc_handle.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
; ^3 r& G8 U* o# W/ l& Z* o
/ D0 F: t: i, |$ s+ s4 A- bkpflag = rtc_read_bkr(0); /* 读取BKP0的值 */ X! ~: C, S. e) u
( v8 n! s: c: p: h! B* `- if (HAL_RTC_Init(&g_rtc_handle) != HAL_OK)
5 p" ^- J: m2 I - {8 J7 T! n& ~: P0 E+ K3 S$ D
- return 1;
5 [3 D! \/ r" b8 S - }1 M0 k, l9 S* @& x$ j9 q8 U! X7 P) z
4 U m* I& L7 _$ O- if ((bkpflag != 0X5050) && (bkpflag != 0x5051)) /* 之前未初始化过 重新配置 */
. O, z) d) }. W- R7 s - {5 G" h7 J. G! P9 r
- rtc_set_time(23, 59, 56, RTC_HOURFORMAT12_AM); /* 设置时间*/
+ q' e& Z7 J4 d# [( v - rtc_set_date(20, 1, 13, 7); /* 设置日期 */
5 \# o2 q4 G) t0 x7 P! V. W* u$ Y - }
}1 R) W: i: ] - + l# Z4 b% ~( Z, T7 f" K7 p/ t
- return 0;
- V# x( u4 v2 K. m2 U - }
复制代码 ; _" C: Y: g2 g0 V
该函数用来初始化RTC配置以及日期和时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位都不会再进行时间设置了(前提是备份电池有电)。在第一次配置的时候,我们是按照上面介绍的RTC初始化步骤调用函数HAL_RTC_Init来实现的。( P$ P7 X8 K. C6 I: p0 G: x) P
我们通过读取BKP寄存器0的值来判断是否需要进行时间的设置,对BKP寄存器0的写操作是在HAL_RTC_MspInit回调函数中实现,下面会讲。第一次未对RTC进行初始化BKP寄存器0的值非0x5050非0x5051,当进行RTC初始化时,BKP寄存器0的值就是0x5050或0x5051,所以以上代码操作确保时间只会设置一次,复位时不会重新设置时间。电池正常供电时,我们设置的时间不会因复位或者断电而丢失。
0 A3 p5 J0 _1 z读取后备寄存器的函数其实还是调用HAL库提供的函数接口,写后备寄存器函数同样也是。这两个函数如下:
; _' t) I! h4 }0 s" f! Suint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister);+ R+ G+ h0 ^1 }3 R7 S- b6 M# F
void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister,, |% @8 |6 W1 H e4 H6 o
uint32_t Data);
3 Q! r+ y2 ?0 X这两个函数的使用方法就非常简单,分别用来读和写BKR寄存器的值。这里我们只是略微点到为止,详看例程源码。) f$ D- d, ~: \7 Q
这里设置时间和日期,分别是通过rtc_set_time和rtc_set_date函数来实现的,这两个函数实际就是调用库函数里面的HAL_RTC_SetTime函数和HAL_RTC_SetDate函数来实现,这里我们之所以要写两个这样的函数,目的是为了我们的USMART来调用,方便直接通过USMART来设置时间和日期。rtc_set_time和rtc_set_date实现十分简单,这里讲解了,详看源码。
7 w. |* l5 F' R; l' A接下来,我们用HAL_RTC_MspInit函数来编写RTC时钟配置等代码,其定义如下:
: k) j3 {+ u3 q( g3 {, f% w. H8 x' s# z
- void HAL_RTC_MspInit(RTC_HandleTypeDef* hrtc)5 q! {# T% ~) v7 w8 B! J! Q4 @
- {1 V% r; s4 ` {$ p/ T1 K
- uint16_t retry = 200;
- o" O1 Y2 t4 D, m. ^7 z6 J6 i - # b) P# w3 `: }9 K, x6 p
- RCC_OscInitTypeDef rcc_osc_init_handle;+ r& a/ k3 ?2 e% u, O
- RCC_PeriphCLKInitTypeDef rcc_periphclk_init_handle;
# U. X- _- }; t3 T! O: q
, F4 Q: {9 j* R% l- __HAL_RCC_RTC_CLK_ENABLE(); /* 使能RTC时钟 */6 k8 ~; c9 H" @1 S! z
- HAL_PWR_EnableBkUpAccess(); /* 取消备份区域写保护 */
0 c! ~+ Y( G& L% } - __HAL_RCC_RTC_ENABLE(); /* 使能RTC */
* X4 k' E% A+ F9 R: `; i
+ i- j; u1 j; b0 ]! e: V' B- /* 使用寄存器的方式去检测LSE是否可以正常工作 */; W6 i' h* ?) S0 h9 R* ]
- RCC->BDCR |= 1 << 0; /* 开启外部低速振荡器LSE */) e' Q3 ?, b- g) K
! \( F t+ A1 J- while (retry && ((RCC->BDCR & 0X02) == 0)) /* 等待LSE准备好 */
& o; b" ~7 s! ~* I( |! B; C2 Q- Y. O - {' c! n6 l; A6 t7 G. |7 G9 r. D* s
- retry--;
i! G# b$ O+ L( p8 e - delay_ms(5);
0 c5 b) b: T5 x3 s - }
) E2 L3 U8 g `# @- y - " C/ o/ D, E" ~' j6 w- u0 S$ A$ R
- if (retry == 0) /* LSE起振失败,使用LSI */
( m* n8 d i: s. p% K7 j: w+ E - { /* 选择要配置的振荡器 */ d7 O: k3 U$ L% Y4 ~
- rcc_osc_init_handle.OscillatorType = RCC_OSCILLATORTYPE_LSI; ( G9 v2 c+ e) p9 O
- rcc_osc_init_handle.LSEState = RCC_LSI_ON; /* LSI状态:开启 */
. l: Z# Y+ x7 i$ C1 T5 n - rcc_osc_init_handle.PLL.PLLState = RCC_PLL_NONE; /* PLL无配置 */7 L! G! A6 a* ]( o/ s
- HAL_RCC_OscConfig(&rcc_osc_init_handle);
4 b9 w) H7 S' i0 V3 N
1 P8 U' A* s6 l& Y5 y- /* 选择要配置的外设 RTC */9 I9 g$ S9 {- M: \" H. P; Y0 J
- rcc_periphclk_init_handle.PeriphClockSelection = RCC_PERIPHCLK_RTC;
1 B- z! s: I, R( i% n - /* RTC时钟源选择 LSI */& h) @" `3 b( q }. K4 o' [. l/ p i
- rcc_periphclk_init_handle.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; ! l `8 e5 Y4 a9 a
- HAL_RCCEx_PeriphCLKConfig(&rcc_periphclk_init_handle);
$ g# I6 R& U2 Z$ j7 }2 C - rtc_write_bkr(0, 0X5051);4 N2 v! x, b; g4 b
- }$ ^, X M/ `$ k4 @
- else6 |6 P6 o6 t) x) ~9 v$ |1 G
- {- i- q/ `9 \" W: C. u( l! ]
- rcc_osc_init_handle.OscillatorType = RCC_OSCILLATORTYPE_LSE;
7 Z- t( B" @9 v4 e - rcc_osc_init_handle.PLL.PLLState = RCC_PLL_NONE; /* PLL不配置 */
4 @* W6 t- M# c& U; t0 |4 c7 U9 r5 @ - rcc_osc_init_handle.LSEState = RCC_LSE_ON; /* LSE状态:开启 */
1 B% o1 b# \$ |; k* ^ - HAL_RCC_OscConfig(&rcc_osc_init_handle); 2 d" x' r" J/ \9 J2 S
5 L* V& N* l, e- Q* i/ I2 P& k6 ?- rcc_periphclk_init_handle.PeriphClockSelection = RCC_PERIPHCLK_RTC; . T0 _5 m3 ^* }7 F: R+ ~3 Y
- rcc_periphclk_init_handle.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; 1 S- ]8 V$ c4 i( _7 e
- HAL_RCCEx_PeriphCLKConfig(&rcc_periphclk_init_handle); 6 a# c D' k( B" k s( [
- rtc_write_bkr(0, 0X5050);/ r! W; [" O0 Q+ y4 X9 `
- }
! ~$ Y% L( j# q: W7 y - }4 `* k# g" V1 B& ~0 P
- 接着,我们介绍一下rtc_set_alarma函数,其定义如下:
' j5 l8 x% }; @$ [ - /**) j, O0 T+ ]: k' q1 p; x
- * @breif 设置闹钟时间(按星期闹铃,24小时制)
6 `" k' @9 o. Q$ ~ - * @param week : 星期几(1~7) P( J' s& |+ V8 k
- * @param hour,min,sec: 小时,分钟,秒钟
$ L7 g- p' \* @ - * @retval 无- {" A' o+ ], O7 X
- */
3 E- h6 }7 D, L; [: i' t - void rtc_set_alarma(uint8_t week, uint8_t hour, uint8_t min, uint8_t sec)6 w( ]3 h- P: `
- { 0 V( }2 |0 M6 q
- RTC_AlarmTypeDef rtc_alarm_handle;
3 W& T0 L7 }: l: i: K; Q! i' Z+ k
" T7 V8 i2 A/ L9 F9 a- rtc_alarm_handle.AlarmTime.Hours = hour; /* 小时 */: H' L) h9 y/ u* v3 G
- rtc_alarm_handle.AlarmTime.Minutes = min; /* 分钟 */1 o+ \, S" N' m& j! U
- rtc_alarm_handle.AlarmTime.Seconds = sec; /* 秒 */% J$ _7 ~0 M4 q$ Z5 I; c5 |# i
- rtc_alarm_handle.AlarmTime.SubSeconds = 0;
6 p& m; h, v- m - rtc_alarm_handle.AlarmTime.TimeFormat = RTC_HOURFORMAT12_AM;
* h x4 I' a8 u; q
" k0 }8 h2 _! C P) X) `- rtc_alarm_handle.AlarmMask = RTC_ALARMMASK_NONE; /* 精确匹配星期,时分秒 */9 a" s' B4 K4 b- M! n
- rtc_alarm_handle.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_NONE;4 ~/ h6 E+ y7 l8 }3 w4 s) \
- /* 按星期 */) F; f& V0 j, e4 x# P4 b/ G: \. S
- rtc_alarm_handle.AlarmDateWeekDaySel=RTC_ALARMDATEWEEKDAYSEL_WEEKDAY;
0 v8 h$ ]# l7 u2 o, B9 N! v - rtc_alarm_handle.AlarmDateWeekDay = week; /* 星期 */( P6 Q. v( p: d1 O6 r+ j3 f* @6 f
- rtc_alarm_handle.Alarm = RTC_ALARM_A; /* 闹钟A */
1 ]1 l9 V- Y; `9 @& b$ X - HAL_RTC_SetAlarm_IT(&g_rtc_handle, &rtc_alarm_handle, RTC_FORMAT_BIN);7 c) T: y6 l+ g) e
2 |/ U2 b% X( @( Z: Y" C- HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 1, 2); /* 抢占优先级1,子优先级2 */- M, v0 J8 ~$ F2 b2 Z7 W$ A; G
- HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
' s/ i! n6 N& K6 l. m) | - }
复制代码 : u% Y9 V7 I1 I: O
该函数用于设置闹钟A,也就是设置ALRMAR和ALRMASSR寄存器的值,来设置闹钟时间,这里使用HAL库的HAL_RTC_SetAlarm_IT函数来初始化闹钟相关寄存器,前面已经介绍过这个函数,请回顾。
# m4 o/ B ^% C B( P2 i4 s: @0 ^6 Z2 C6 {( J; h3 r6 t
调用函数rtc_set_wakeup设置闹钟A的参数之后,最后,开启闹钟A中断(连接在外部中断线17),并设置中断分组。当RTC的时间和闹钟A设置的时间完全匹配时,将产生闹钟中断。! T8 y; {9 P: C- g) R/ M
接着,我们介绍一下rtc_set_wakeup函数,该函数代码如下:; t* \$ ~2 c# {4 k8 G" U
6 v" R6 ?4 m) a' v; m+ F; O7 ]- /**
: g% T* k1 h5 y0 T - * @breif 周期性唤醒定时器设置
/ k) R) ^- `9 p* v; ?- p - * @param wksel
. l. A) V" O# @. H - * @arg RTC_WAKEUPCLOCK_RTCCLK_DIV16 ((uint32_t)0x00000000)
2 v$ t; v% @0 ?4 h" _0 w' I! J - * @arg RTC_WAKEUPCLOCK_RTCCLK_DIV8 ((uint32_t)0x00000001)4 T# {3 R$ z& @
- * @arg RTC_WAKEUPCLOCK_RTCCLK_DIV4 ((uint32_t)0x00000002)
8 C* }& p" Z+ \9 n - * @arg RTC_WAKEUPCLOCK_RTCCLK_DIV2 ((uint32_t)0x00000003)
- a0 z% I7 n% F - * @arg RTC_WAKEUPCLOCK_CK_SPRE_16BITS ((uint32_t)0x00000004): l5 c! g+ @" S w D$ u3 ~
- * @arg RTC_WAKEUPCLOCK_CK_SPRE_17BITS ((uint32_t)0x00000006)& T; t; p9 Z7 N1 v& S' z- c
- * @note 000,RTC/16;001,RTC/8;010,RTC/4;011,RTC/2;% I/ u, P% q4 e
- * @note 注意:RTC就是RTC的时钟频率,即RTCCLK!1 \* w0 y' \3 v: M- l- X
- * @param cnt: 自动重装载值.减到0,产生中断.4 n2 h( h- W3 [! v! M! ]5 V
- * @retval 无: P, x6 |4 a# P) n( [3 q8 _3 r
- */
; ^* X4 c6 Z: }% g6 K - void rtc_set_wakeup(uint8_t wksel, uint16_t cnt)0 d+ K; S% e; I1 Q& W% f& D$ F
- { $ o# x# ~0 i+ l* E
- /* 清除RTC WAKE UP的标志 */4 ^- S0 p0 ^- c6 s$ u
- __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_WUTF);
2 K3 O/ `$ r; ^ - HAL_RTCEx_SetWakeUpTimer_IT(&g_rtc_handle, cnt, wksel);/* 设置重装载值和时钟 */
8 J% m3 _" \8 E) t! e1 E; E$ P0 t2 N - HAL_NVIC_SetPriority(RTC_WKUP_IRQn, 2, 2); /* 抢占优先级2,子优先级2 */9 w3 A$ h5 |! k- J
- HAL_NVIC_EnableIRQ(RTC_WKUP_IRQn);1 c2 o9 B0 I6 G. p
- }
" Y) R/ J/ Q0 Z) g( i9 s - 该函数用于设置RTC周期性唤醒定时器,实现周期性唤醒中断,连接在外部中断线19。该函数调用的是HAL库函数HAL_RTCEx_SetWakeUpTimer_IT实现的。; N+ j! K, q5 u4 A+ t; B
- 有了中断设置函数,就必定有中断服务函数,同时HAL库会开放中断处理回调函数。接下来看这两个中断的中断服务函数和中断处理回调函数,代码如下:) t& J _& f8 s# S. J! x3 ^
- /**
: D$ j+ K7 Q& ^" a; A0 C - * @breif RTC闹钟中断服务函数- }0 @& }- l S+ B+ y4 j( Y3 Y0 N& f
- * @param 无
' O* u# v. O) P" u - * @retval 无
! [" F+ m- c) g6 k - */
5 E4 I1 l4 j& Y/ {$ |: e4 N - void RTC_Alarm_IRQHandler(void)
# H0 ]1 N4 O: ~* \8 v' F' K7 a" g* j! H - {
- s0 O3 D, e. ?8 |+ M" [ - HAL_RTC_AlarmIRQHandler(&g_rtc_handle);6 ?8 Y% H' }2 v- X* q8 ~2 I
- }6 h# u& C- Z4 ~, L2 O/ T" M! A1 F
& h% s7 W P5 ~* y+ k& d- /**
; G0 |/ @" J6 _% a$ j - * @breif RTC闹钟A中断处理回调函数
6 p3 Y- q% E9 C$ b - * @param hrtc:RTC句柄
2 Y6 U+ A5 f' R, N+ u6 @ - * @retval 无' Z8 I+ d* ]: Z7 P6 k+ `
- */
9 ~: ?( D- U8 s* Z - void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
9 `3 y2 I8 x- A) ]6 d - {" a: T4 C+ N; V0 z3 p3 E
- printf("ALARM A!\r\n");6 i p5 F5 F+ I- G
- }) y# ]* }8 U0 U
- q. x8 I0 j8 J+ U' c- /**
( S% |+ G$ L3 s$ k6 S - * @breif RTC WAKE UP中断服务函数9 g. }0 Y9 [4 N. Q, W; Y1 p
- * @param 无$ ?" y3 K3 a" K
- * @retval 无
`# Y8 e. k! j' B+ J, ~( F - */7 D# x! d7 v7 K: C: h3 K
- void RTC_WKUP_IRQHandler(void)% T: R; a+ G+ X0 h X7 C" j, h
- {" V" r C: W! H$ s. i0 K7 R
- HAL_RTCEx_WakeUpTimerIRQHandler(&g_rtc_handle);
W! w* r7 {( x - }
0 p: c8 D+ x, T5 J# y. Q& M - $ X7 F9 D% |6 Z
- /**
# k; q2 V, R, W - * @breif RTC WAKE UP中断处理处理回调函数0 W b3 q8 L5 C2 c
- * @param hrtc:RTC句柄
# d1 w5 p6 H" d9 ^" h+ f+ I - * @retval 无
$ x8 ~9 ?4 d* Y% ^6 d I9 ]' d - */
3 A1 S" w* {0 K) d: q# B% {; z8 ^ - void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
! t$ U' P1 e7 a% C - {
+ [$ ?1 w1 Y9 q6 J - LED1_TOGGLE();3 W. r/ j: }7 U- [7 K0 U/ R
- }
3 Q2 Z5 a+ z. G+ X - 其中,RTC_Alarm_IRQHandler函数用于闹钟中断,其中断控制逻辑写在中断回调函数HAL_RTC_AlarmAEventCallback中,每当闹钟A闹铃时,会从串口打印一个字符串“ALARM A!”。RTC_WKUP_IRQHandler函数用于RTC自动唤醒定时器中断, 其中断控制逻辑写在中断回调函数HAL_RTCEx_WakeUpTimerEventCallback中,可以通过观察LED1的状态来查看RTC自动唤醒中断的情况。
1 {( t3 B8 j4 _" N - rtc.c的其他程序,这里就不再介绍了,请大家直接看源码。( C3 ^0 z# `4 z6 N+ `/ l% A
- 2. main.c代码
: E [9 b- y2 m, w& i7 ]9 [ - 在main.c里面编写如下代码: 2 T, X4 W3 u6 P2 U
- int main(void)
" b) x, F7 v: D! E4 [( D# \ - {
7 U. ^! s; x* N' U. n - uint8_t hour, min, sec, ampm;
& ^: h4 I @2 m9 U8 k - uint8_t year, month, date, week;- B9 L& ~. Z0 Z' M6 t
- uint8_t tbuf[40];! e7 [0 [: U0 O2 K% @
- uint8_t t = 0;
- B6 F6 P. V! _0 z. N7 P - sys_cache_enable(); /* 打开L1-Cache */' W2 n# s- ?2 Q5 t3 F. [
- HAL_Init(); /* 初始化HAL库 */
8 o. }3 K. s3 _! ~ - sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */
" ~# o$ n( s; T( Z; k8 ]$ w+ } - delay_init(480); /* 延时初始化 */
+ H" p$ D$ e9 }4 U- T+ P4 s - usart_init(115200); /* 串口初始化为115200 */
7 [2 z3 w" s ~( P8 g% r; r9 m - usmart_dev.init(240); /* 初始化USMART */ r. O l; E6 a/ {2 G/ I1 T
- led_init(); /* 初始化LED */
4 ~7 Q5 @' e; {' o" A - mpu_memory_protection(); /* 保护相关存储区域 */
/ ?4 }7 v; F C/ Y( c: T F - lcd_init(); /* 初始化LCD */
7 ?+ Y5 m5 F- n9 `" n4 V- m" u. t - rtc_init(); /* 初始化RTC */) U: ^; B( |" b3 [
- /* 配置WAKE UP中断,1秒钟中断一次 */
2 ]1 J; P1 O) J! c: A - rtc_set_wakeup(RTC_WAKEUPCLOCK_CK_SPRE_16BITS, 0);
7 x& e$ k6 s- M - lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
* N" Q% o# W- J& o: Y0 G4 G - lcd_show_string(30, 70, 200, 16, 16, "RTC TEST", RED);
1 N) E% D9 m: T - lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);9 E. } b+ U0 v) ^
- while (1)/ o6 r2 _; t4 R) C, q
- {& K, f$ S X+ @' ~9 L
- t++;
3 _9 }5 i2 i" w" Q - if ((t % 10) == 0) /* 每100ms更新一次显示数据 */
" U' N2 [. }* F - {; n7 @+ _: O# S: q
- rtc_get_time(&hour, &min, &sec, &m);- h' r/ [; P+ T# t3 ~" U$ n
- sprintf((char *)tbuf, "Time:%02d:%02d:%02d", hour, min, sec);
; c6 ^9 ~, X; P m4 [. Q# a: b9 r - lcd_show_string(30, 140, 210, 16, 16, (char*)tbuf, RED);
X' z, }+ b# D$ g% w1 l( f - rtc_get_date(&year, &month, &date, &week);
7 S" w& y5 }0 c8 a - sprintf((char *)tbuf, "Date:20%02d-%02d-%02d", year, month, date);1 e) P. W/ Q3 W; s' {' g
- lcd_show_string(30, 160, 210, 16, 16, (char*)tbuf, RED);6 Z# I! y, I8 ~: v. Z9 |/ u
- sprintf((char *)tbuf, "Week:%d", week);
3 v; |. k; d2 r3 E% G; I4 W _ - lcd_show_string(30, 180, 210, 16, 16, (char*)tbuf, RED);' v/ D2 `. S7 N# ]6 U
- }
: A+ Y) }5 g8 z' h - if ((t % 20) == 0)8 y. p n W6 x E$ V
- {5 R2 d6 w: e0 y! I/ B
- LED0_TOGGLE(); /* 每200ms,翻转一次LED0 */# h; D s9 x( U
- } " Y& w& `. D& p
- delay_ms(10);$ P; X: x- C, h/ T) N
- }
$ ?$ a# M& `! X, y$ J4 W. Q - }
9 S' C: a& @5 x+ ]7 ~6 z
复制代码
( H6 R, q1 |3 n! F0 f/ a' w% A9 H我们通过RTC_Set_WakeUp(RTC_WAKEUPCLOCK_CK_SPRE_16BITS,0)设置RTC周期性自动唤醒周期为1秒钟。然后,在无限循环中每100ms读取RTC的时间和日期(一次),并显示在LCD上面。每200ms,翻转一次LED0。/ \ T! W( B0 y
为方便RTC相关函数的调用验证,在usmart_config.c里面,修改了usmart_nametab如下:" Q; A. F5 x4 r; E1 M
/* 函数名列表初始化(用户自己添加)
0 k/ v$ S1 V7 l/ k% H2 a4 p5 e% T% y1 r1 M r: x8 r
用户直接在这里输入要执行的函数名及其查找串% @( k4 [2 u" u$ W
- */
3 e% z! E9 s( z1 [ - struct _m_usmart_nametab usmart_nametab[] =
: t+ M) ~6 e8 x' V" c) \ - {$ i# K( L. L; e% R& `
- #if USMART_USE_WRFUNS == 1 /* 如果使能了读写操作 */' ?. X" q/ J1 f
- (void *)read_addr, "uint32_t read_addr(uint32_t addr)",
; Q2 U. N, z/ W8 H1 ?+ m - (void *)write_addr, "void write_addr(uint32_t addr,uint32_t val)",3 e2 t+ L. R! H) f
- #endif/ s/ }7 _/ `7 G( C: i# v
- (void *)delay_ms, "void delay_ms(uint16_t nms)",( ~( Q+ M7 P6 w, D& w
- (void *)delay_us, "void delay_us(uint32_t nus)",
7 ?/ }$ n8 a* f+ @8 B
5 B: d& `$ L; a# e' k2 F4 Q9 b- (void *)rtc_read_bkr, "uint32_t rtc_read_bkr(uint32_t bkrx)",) X2 V y9 ^8 z+ w) c/ c
- (void *)rtc_write_bkr, "void rtc_write_bkr(uint32_t bkrx, uint32_t data)",, C: [' y+ ~0 s# F( o2 M
- (void *)rtc_set_time, "uint8_t rtc_set_time(uint8_t hour, uint8_t min, * ^! f1 w+ ~1 A" [$ b4 {
- uint8_t sec, uint8_t ampm)",
% y) X% ~/ y/ \4 A - (void *)rtc_set_date, "uint8_t rtc_set_date(uint8_t year, uint8_t month,
$ k& R$ t2 m3 i1 Z0 {' `( N7 q - uint8_t date, uint8_t week)"," D$ ~& _7 y( G: c4 v- m
- 7 l$ f0 @$ P H" v. e
- (void *)rtc_set_wakeup, "void rtc_set_wakeup(uint8_t wksel, uint16_t cnt)",
, g8 x; s! S* ^1 e5 ? - (void *)rtc_get_week, "uint8_t rtc_get_week(uint16_t year, uint8_t month,
5 N3 u, K4 Q1 ` - uint8_t day)",
2 R1 ~6 Q: I! y' A2 H3 s - (void *)rtc_set_alarma, "void rtc_set_alarma(uint8_t week, uint8_t hour, X, k5 o! e, F2 H7 s2 T( w
- uint8_t min, uint8_t sec)",
$ u: b. f/ L9 w( E( h4 Q! f - };
复制代码
/ c0 ~- m' ~# D6 Q2 F将RTC的一些相关函数加入了usmart,这样通过串口就可以直接设置RTC时间、日期、闹钟A和周期性唤醒等操作。 至此,RTC实时时钟的软件设计就完成了,接下来就让我们来检验一下,我们的程序是否正确。
; A, c |* P. x- r4 I27.4 下载验证
' M/ S& P' |1 C将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了,同时LED1每两秒闪烁一次,说明周期性唤醒中断工作正常。然后,可以看到LCD模块开始显示时间,实际显示效果如图27.4.1所示:& B: Y5 t; w& D* I4 S( a( z
' X6 M! w- B3 Y" V, t, B' i1 t, `
# A8 g* k# w8 w5 o& ]! ^3 x
8 d: J3 a& n# Z, E8 c: @
图27.4.1 RTC实验测试图4 s4 o/ U, w6 u" U$ b% M& y7 p) \8 p
如果时间和日期不正确,可以利用上一章介绍的usmart工具,通过串口来设置,并且可以设置闹钟时间等,如图27.4.2所示:
; A! V4 p; T# t, g1 Z1 {9 w2 A! [1 `9 \% F
5 E% e" l4 R/ Z1 R/ L
( y5 x8 u) H1 x/ Z" W# q图27.4.2 通过USMART设置时间和日期并测试闹钟A& `: A( u% O" r* R
按照图中编号6、7、8顺序,设置闹钟A、设置日期、设置时间。然后等待我们设置的时间到来后,串口打印ALARM A!这个字符串,证明我们的闹钟A程序正常运行了!" C- i, O* N; W# q1 P" a. ~. G
————————————————
, P& |& Z* A% l4 r, T! W! N版权声明:正点原子
* h' z7 q0 U4 \+ [3 y" n$ e: e( V1 G7 `
; s7 Y' M! |4 ^ K1 g4 i
|