第九章 STM32MP1时钟系统1 z( _: l3 n# h( z4 ^/ m7 n
如果让你用一句话来形容时钟,你会怎么说?+ T1 B; |! R3 N; b1 E
A同学说:“时钟是单片机的心脏”;0 s W7 d3 d+ {( m3 e2 s
B同学说:“军训方队训练时的121”;
; k- U% c9 u8 R4 \' R- `& ]1 EC同学说:“时钟是单片机的能量”;8 O! u6 U$ t: }9 F1 f9 L$ w3 t( h
D同学说:“早上的闹铃”;
# z" b: P4 {' U( V' \…
* X. I. U: W. i2 s5 V可见,时钟是多么的重要。6 m" R: `) \- B: G/ N8 W
本章节我们来了解STM32MP1的时钟系统,并分析HAL库中和时钟相关的API函数,然后通过STM32CubeMX插件生成时钟初始化代码,通过分析工程代码去学习STM32MP1的系统时钟初始化步骤。+ N7 U3 D7 k: P$ h3 U. f$ |/ V
本章将分为如下几个小节:
- D8 k/ S* `5 F- ^, O7 T9.1、认识时钟树;
) g6 k, K8 x/ \5 f; U* S4 W; J9.2、RCC寄存器介绍;- I5 D z! x4 {( i; S' i S
9.3、时钟相关API函数;, k) O: S7 W- E% i7 E
9.4、配置系统时钟实验;/ O, |: M( r7 b, h7 H5 F
9.5、实验代码分析;
( _. L' U: \5 v. U9 i( i& A4 K/ N" M( o( I* ~4 K* Q2 W, [( O$ g8 l2 w
9.1 认识时钟树6 h L+ U; u ~
时钟是单片机运行的基础,相当于心跳,没有时钟,单片机就无法工作。为什么这么说呢?我们知道,寄存器是CPU内部的元件,由锁存器或触发器构成,触发器的置1、清0和置数的功能是靠其外部提供的时钟脉冲来产生跳变的,如果没有这个脉冲信号,电路就没有数字跳变,也就没有时序,单片机也就没有工作。时钟就像是人的心脏,这个恰当的比喻把时钟的重要性概括的淋漓尽致。" S% N4 s0 K- u3 E) Q& ]6 ~
听说过时钟,但时钟树又是啥?前面我们说单片机的运行需要时钟脉冲来驱动,这个脉冲由源始晶体振荡器提供时钟输入,经过各级电路的处理,最终传输给各个设备,设备获得一定时钟频率以后开始工作。时钟的这种传输过程就好比一棵大树的养分供给给其它分支,树上的主干和树枝就好比时钟的总线,树叶就像是外设,时钟就是通过总线传输给设备的。# `, o1 U, a, i7 w' s% ]; c
9.1.1时钟源9 [: V! d2 u! `1 T7 N! W2 @
51单片机的一个系统时钟就可以解决一切,但STM32不一样,STM32有5个输入时钟源(Input Clock)可以用,分为2个外部时钟和3个内部时钟。4 S6 t3 `1 I5 v* V9 x
下面我们看看这几种时钟源。
# z8 X2 B/ ?2 N t0 M(1)2个外部时钟源:
4 B# @4 C- D* d( B8 L! r高速外部振荡器 (HSE)- U- z( ?2 |& Z8 y$ k7 @
可通过外接有源或者无源晶振驱动,支持 4 MHz 到 48 MHz 频率范围内的晶振。如果要使用外部晶振,在STM32CubeIDE上配置的时候,请根据板子上外部焊接的晶振频率来配置,正点原子STM32MP157开发板的核心板上外接了24MHz有源晶振。# R1 h6 {% f1 C/ h( A% F! r
低速外部振荡器 (LSE)
; i, y. Z( r! B可通过外接晶振驱动,固件库中默认配置32.768 kHz(见7.4.1小节)。正点原子STM32MP157开发板的核心板上外接了32.768KHz无源晶振,主要用于驱动RTC 实时时钟和唤醒功能。
" @. o W- ~' K3 Q) q% A4 [. Y* v(2)3个内部时钟源:* }# S/ R' p3 h6 p K1 c# Y7 p
高速内部振荡器 (HSI)9 J' S' D" h9 h
频率约为8、16、32、64 MHz,在固件库中默认配置为64 MHz。HSI比HSE具有更快的启动时间,大约几微秒。, l) p% L" }+ w6 @9 s
低速内部振荡器 (LSI)
. D. M3 ?6 ?. m$ P- h频率约为32 kHz,实际值可能会因为电压和温度而变化。主要用于看门狗。
& k$ n8 \' e* e4 W5 Y* z+ Z$ X低功耗内部振荡器 (CSI)' k) P; ^7 |- T% X/ U
频率约是为4MHz,主要用于低功耗。) v3 V# f' E: D' p- M
以上2个外部时钟源都是由芯片外部晶振产生时钟频率,其精度较高,而内部的时钟源可能因为受到温度的变化,频率发生变化,也就是存在“温飘”的情况,即使经过校准以后,精度也比外部晶振低,对于日常普通的短时间计时,使用内部时钟相差不大,对于较长时间计时就不建议使用内部时钟了。使用内部时钟可以降低成本,例如在一些白色家电中,一些家电产品为了降低成本,把电路板上外接的晶振去掉,程序改用内部时钟来工作。 Q4 w* [. b1 L4 S
大家可能会问,为什么STM32 要有多个时钟源呢?* v+ Z. i' J* G7 P4 l2 }
我们知道,在同一个电路中,外设开的越多以及频率越高,功耗就越大,同时抗电磁干扰能力也会越弱。在实际的使用中我们并不是所有外设都会用到,有时候只是用到其中的某几个功能,如果打开用不到的外设,功耗会变大,而对于用到的外设,有的外设可能用不到系统时钟那么高的频率,例如看门狗或者RTC只需要几十KHz的时钟就可以工作了。ST在设计芯片的时候也是考虑到了这一点,对复杂的MCU采用多时钟源的方法来解决,并用时钟树来管理,将所有的外设时钟都关掉,用到什么外设,就使能对应的外设时钟即可。下面,我们来看看STM32MP1的时钟树。
) P" g1 _! M7 Z9.1.2时钟树0 |: u5 |! k6 q
$ T! p" M$ x) F; e. U+ ~+ T
% a8 d' l. ~4 R s% y' A! M0 G- Y N0 G/ i2 }
图9.1.2. 1时钟树框图, Z# D6 t: z' s+ N* S6 t U! ?
注:
% T. d5 h7 ~: |# P3 x6 c' h①图中的 表示动态时钟选择器,图中的D符号表示动态选择开关,即时钟选择器允许即时重新配置选择输入的时钟源,两个输入之间的过渡无干扰。仅当目标时钟源准备就绪(启动延迟或PLL锁定后时钟稳定)时,才从一个时钟源切换到另一时钟源。如果选择了尚未准备好的时钟源,则在时钟源准备就绪时进行切换。
$ I& }* K @7 Q3 b. I( t& G②图中的部分时钟命名:' ]3 f' A3 P5 M* u) U
" \" b' W6 A: @. B+ p I6 f
# A, ~, P) k" R; A( d
8 f- B/ o6 t$ p1 Z* ~4 H" |
表9.1.2. 1时钟命名9 r/ `0 q' q+ c5 f% @: P4 t
3 C; L L) G0 o$ f# A" {" \3 D
外部时钟输入
6 N: A' K1 X4 \# q% Y图中①处是外部时钟输入引脚,当使用HSE时,HSE在OSC_IN引脚接收外部时钟源,当使用LSE时,LSE在OSC32_IN引脚接收外部时钟源。0 {9 z% f/ G, f/ S ^! s
2 ^- _0 V" i) g
5 s. T( T _( y" o* X8 w
! D0 p6 B3 @/ \9 o: r8 ]
表9.1.2. 2外部时钟输入引脚( T5 H9 x* r9 K8 }
2. MCO时钟输出
4 I0 G+ H5 ~' G2 ]$ x0 X②处是MCO微控制器时钟输出(microcontroller clock output)部分。
# u; n; P7 ?; Z/ g. [6 bMCO1和MCO2是时钟输出,通过芯片引脚可以给外部的芯片提供时钟,可以节省晶振,节约成本。MCO1和MCO2时钟输出通过配置RCC_MCO1CFGR寄存器来实现,具体配置可以参考手册详细说明。
. |" h/ Q# x5 }1 L0 V0 a时钟输出 Pin name1 n/ ]+ A! G7 q1 d: Q: n
, u2 ~5 m( r* |+ T& I( v5 R
: C; V+ E* h2 U- r) Z4 ~4 d% e; m
: R: q6 A; e1 E- y/ r1 S+ Z0 Z. {
表9.1.2. 3 MCO时钟输出引脚. c2 z: D0 a. a& @
MCO1SEL和MCO2SEL是时钟源选择器,分别选择MCO1和MCO2的时钟来源;
8 ^0 H$ s! `9 \& |+ h# jMCO1DIV和MCO2DIV是时钟分频器(也叫预分频器),取值范围是1~16;% O8 {6 H9 K1 V' d
MCO1时钟源来自hse_ck、lse_ck、his_ck、lsi_ck和csi_ck,用于输出HSI,HSE,CSI,LSI或LSE时钟。% C: K# p: v! n: Y& ]1 a$ X2 R
MCO2时钟源来自mpuss_ck、axiss_ck、mcuss_ck、pll4_p_ck、hse_ck和hsi_ck,用于输出MPUSS、AXISS、MCUSS、PLL4(PLL4_P)、HSE或HSI时钟。
& N$ }% i3 ^5 q+ ?7 \! t. J# _) [. z! ^4 G) e; J. {
3. 锁相环PLL
: x5 i6 v' y1 }4 t+ |③处是和锁相环有关部分。& B' Z' C# N1 g( L {4 e& e
PLL(Phase Locked Loop) 锁相环路,其作用是使外部的输入信号与内部的振荡信号的相位同步,在STM32中主要用于分频和倍频。STM32MP1有4个锁相环,分别为PLL1、PLL2、PLL3和PLL4:
8 E: L( l1 ]6 rPLL1,用于向MPU子系统提供时钟;
& J R8 ^9 d9 G3 a& g* ?% h/ a8 DPLL2,用于向AXI子系统、DDR和GPU提供时钟;# t! ], }" P" o. L
PLL3,用于向MCU子系统提供时钟并为外设生成内核时钟;
* m) A2 m- S. yPLL4,用于生成外设的内核时钟。
* k! ]/ W6 L3 p; F; m W; `9 _2 E0 {④是PLL时钟源选择器。( R6 Z: w1 o" p5 g8 k+ X4 u
锁相环输入时钟源有三个:his_ck、hse_ck 和csi_ck,分别对应HSI、HSE和CSI的时钟。我们可以配置某个锁相环选择对应的时钟源。5 N, ^" E1 \5 G: O0 l
⑤是PLL时钟源分频器7 I0 h% t" {' U
DIVM1、DIVM2和DIVM3和DIVM4分别是PLL1、PLL2、PLL3和PLL4的分频器,取值范围都是1~64,表示将输入时钟源进行分频。* N1 ~6 h: s( r* p
⑥是锁相环6 d# B, X _5 h! i
其中PLL1是一个输入一个输出,PLL2、PLL3和PLL4是一个输入三个输出。) j+ |, ]* e, S& {2 m* \ k
我们以PLL3为例子分析一下锁相环的内部结构:/ c% D. c$ j4 N! Z* r: y
VCO表示输出频率;+ o" b, t) Q$ D' R. \
DIVN表示VCO的倍数;
9 F% Q. }1 e5 J' k2 QDIVP、DIVQ和DIVR表示预分频器;6 _/ O- y( T" o1 k
SSCG扩频时钟发生器,可以减少EMI峰值的数量,一般不用设置;- k6 G3 ~( D8 I' }
FRACV是分数倍频系数,它和DIVN一起组成PLL的倍频系数,当FRACV等于0的时候表示在整数模式下,大于0的时候表示在小数(或者说分数)模式下。# L1 M8 ?! c4 t9 h7 P E) |0 D
PLL输出的频率有两种计算方法,我们以PLL3输出pll3_p_ck为例,其它锁相环的输出计算方法类似,时钟源默认使用HSI,即64MHz。$ Z3 ?& w, c+ Z: h5 [5 i0 L" r
在整数模式下使用PLL:9 ]# Y' F1 `* A5 b" a0 V) z1 \
: U9 l6 R) l) }4 \8 }
在小数模式下使用PLL:
/ W; q: r1 S; k2 ^1 ~9 [注:
8 U# w2 i; I4 |) U手册中公式的(DIVN + 1)其实就相当于我们上面公式中的DIVN,因为STM32CubeMX插件中是这样计算的,我们的公式中就跟STM32CubeMX插件的计算方式一致。, j" V4 Q9 s% t
" f0 c( m' V2 i# ^ F, P
, b( X- R& {. {6 o8 ~0 h% {9 o$ ~4 X4 [) m! Z. \
图9.1.2. 2参考手册上的公式, Z' m; ` {4 C0 t+ ]: u
例如,在整数模式下(FRACV为0),当设置分频器DIVM3为4,DIVN为26,预分频器DIVP为2时,计算出pll3_p_ck为208MHz。如果再设置FRACV的值为10,根据上述计算出pll3_p_ck约为208.009766MHz。在STM32CubeMX插件上,我们手动配置这几个参数,结果和我们计算的结果一致,如下图所示:, e3 L+ p" q" o0 ~1 W2 Y, m
( h, N k% _3 f# A
) N3 p# X2 z# |7 Z2 j# v1 h- o5 [# u9 s
图9.1.2. 3 整数模式
9 |8 d" F5 W7 E g3 M' R% \/ q3 a/ P. a
' ?5 a7 ^8 r+ ]! c' ^. {: b, |' _8 M6 ?( E5 b: H" B k
图9.1.2. 4分数模式, r/ x; ^' m6 Z, q; S# r
4. 子系统时钟
6 K' R, u3 d0 E P⑦处是子系统时钟,RCC处理三个子系统时钟:mpuss_ck、axiss_ck和mcuss_ck。其中mpuss_ck给MPU提供时钟,axiss_ck给AXI互联矩阵提供时钟,mcuss_ck给MCU提供时钟。上电复位后,所有的PLL和CSI以及HSE关闭,默认将HSI选择为整个系统的主时钟,因此,子系统时钟默认来自HSI。当系统运行时,用户可以选择子系统的时钟源。
& w& }/ t0 L, R7 w. q4 h我们来查看子系统部分的详细框图:
3 O8 Z* ^, @! }/ \- x7 R) ~1 v4 } M6 E7 z# t; X
; R* W& T7 p" `& g3 i7 G' E/ z
7 D* ^8 u! b* ^7 {4 D+ \7 Q5 ?图9.1.2. 5子系统框图
6 L, _3 `! T* ~/ B, tmpuss_ck的频率最大可以是650MHz或者800MHz,这个根据所用的器件型号决定,STM32MP157A的MOU主频为650MHz,STM32MP157D的MPU主频为800MHz。 axiss_ck时钟最大是266 MHz,其经过AXIDIV分频器分频后分流为几路,给总线提供时钟。mcuss_ck时钟最大为209 MHz,经过MCUDIV分频器分频以后也是分成了几路。
3 b- K7 c& @2 p7 |# W" y/ ?9 \. cpclk4和pclk5最大为133 MHz,pclk1、pclk2、pclk3最大为104.5 MHz。" t7 }# E$ E) U& j7 h
子系统时钟汇总如下表格所示:
3 B! _9 I) u; q7 N( _; o& d- m# y% W* J$ @8 ]
8 t. r7 f) x+ i+ Q0 B$ n! W% K: o; I6 i$ Z$ e: d( w
表9.1.2. 4子系统时钟信息表
' u q E9 j2 E: z+ v5. CPU、AXI总线矩阵和外设时钟
# f$ u3 S& V$ o) h在STM32CubeMX插件上,我们可以看更具体的时钟树框图,如下图:
; i: u* ` U. }; D. p3 n6 _A处是时钟源,可选外部时钟或者内部时钟,要用外部时钟的话,必选先使能外部时钟;# b5 O. c! L2 g" N
B处是锁相环PLL;
9 L, L( B/ k$ Y* K. X( ?C处是内部时钟输出MCO1和MCO2;
; A0 ^' {) B; Q# V+ Q黄色区域是子系统时钟;: N$ {) e4 p2 V$ I( L; ^
紫色区域是专门给CA7外设提供时钟的总线时钟,分别是AXI、AHB5、AHB6、APB4、APB5,;
3 b' N6 u3 e& K蓝色区域是给MPU和MCU提供时钟,其中,MPU时钟最大是650或800MHz,MCU时钟最大是209MHz,MCU有一个内核外设Systick,频率最大也是209MHz;
' Z" q# g; X+ j0 \- `+ B绿色区域中AHB1-4、APB1-3 是提供给 CM4 外设使用的;
4 H( ]3 q% b) S0 |9 ?, T红色区域是内核外设时钟具体配置部分,默认没有使能,是灰色,只要在 STM32CubeMX 使能了对应的外设,对应外设的时钟就会由灰色变成蓝色。# Q) r0 Y! _- B
注:
: i# L6 G. v2 m! g: |0 \RCC为整个电路提供时钟,为了避免造成误解,和参考手册一致,使用以下术语:
' m: p L6 w3 P; H# Y9 ~7 K•外设时钟:0 U5 X% Q6 u2 d7 t# R
外设时钟是RCC提供给外设的时钟,可以为外围设备提供两种时钟:
) h: W- V4 N- \8 Q–总线接口时钟
( b6 y/ f; U% Q- M7 ]–内核时钟
( p$ f) G4 V0 ]( l8 _5 S. o这里的外设有分普通的外设和内核外设。外设将从RCC接收一个或多个总线接口时钟,该时钟通常是AHB,APB或AXI总线接口时钟,具体取决于外围设备连接的总线。! N+ }& q9 D7 E& |* a, b# M# B
某些外设仅需要总线接口时钟(例如GPIOx、ETZPC、EXTI 、IPCC 、WWDG1、DMAx …)。
7 G$ C5 A; j" i- ]) c一些外设可能还需要专用时钟来处理接口功能, 该时钟称为内核时钟。例如,诸如SAI、ETH、FMC、GPU等这些的外设需要生成特定且准确的主时钟频率,这需要专用的内核时钟频率。
! q6 d9 B B* X* ~" k•CPU时钟
' c! x) D" H: Z) c1 J3 n9 lCPU时钟是提供给CPU的时钟mpuss_ck。MCU时钟是mpuss_ck。, ^4 j8 T) n! Q% a$ l( _6 o& ?
•总线矩阵时钟6 X+ S% o! P- h8 ^' N9 p; ]
总线矩阵时钟是提供给不同桥(APB,AHB或AXI)的时钟。2 F2 O1 a. n: @: y2 t* b
图中,表示APB总线的分频器,里边的数值表示分频因子。表示是时钟源选择器。4 p4 p* H! |4 ~2 {+ |+ I; P0 ]
STM32CubeMX插件上的时钟树比较形象,很好理解,图中的一些符号可能和我们参考手册上看到的不一样,不过根据字面上的意思可以知道大概对应了哪些设备。我们可以通过STM32CubeMX插件来配置时钟,然后自动生成时钟的初始化代码。 y9 s! D6 z4 N" z+ x
, b2 P, f0 U. t, D. O# Z
0 k6 x$ P% m! H8 B0 ~% N
3 i3 E: F w& W* X$ o
图9.1.2. 6 STM32CubeMX时钟树
0 |5 M9 Q/ p: n( X. ^对于内核外设时钟,我们以SPI为例子来说明一下:& t+ N/ j. k5 m" }
: }0 U9 h1 h$ ]9 g
/ d" `* j; q) q, D% ~- v& D N
3 _& }( } @+ q9 C/ u g图9.1.2. 7 SPI时钟框图% ~" J% F+ Q. C) h5 A7 y; a
PKCS (Peripheral Kernel Clock Selection)是外设内核时钟选择单元,用于选择某个外设的内核时钟来源。PCKE(Peripheral ClocKs Enable)是外设时钟使能单元,用于是否启用时钟输出到外设中。" ^; u4 o0 K& w
如上图,SPI6的内核时钟源来自于APB5总线(pclk5)、PLL(PLL3和PLL4)、HSI、CSI和HSE中的某一个,具体是哪一个由PKCS决定。其中,HSI和CSI用于低功耗中,PLL4在需要时具有更高的灵活性,例如,它允许通过PLL3更改MCU总线的频率,而不会影响某些串行接口的速度。& ]+ g" s! C0 u! ]- v) r, J b
SPI6的PCKE有两个时钟部分,一个是来自于APB5总线接口的spi_pclk,一个是来自于内核时钟spi_ker_ck。PCKE决定是否启用SPI6的时钟,其它内核外设也是通过PCKE来开启的。由此可见,各个外设时钟基本都是可控的,不用的外设,就关闭对应的时钟,用到的外设,就根据分频器来配置,选择一个适合频率的时钟源,然后再开启外设时钟,这样的设计大大降低了功耗。
( b, M6 P' h2 `, N; n4 P. o$ U1 p$ O根据STM32CubeMX插件上的时钟树,ST给出了一张总线框图,如下图,从图中可以看出哪些外设挂在哪根总线上,以及总线的最大时钟频率是多少等信息。
" l+ t2 I! L \( q" V0 }3 t% F W下图中,AXI互联矩阵主要用于Cortex-A7 CPU子系统和高带宽的设备(USBH、ETH、SDMMC1/2、MDMA、GPU和 LTDC等)。MLAHB高性能总线主要用于Cortex-M4子系统和相关的设备(GPIO、ADC、SRAM、OTG、DMA1/2和SDMMC3等)。APB低速总线主要用于低带宽的周边外设(如UART、USB、SPI、I2C等)之间的连接,也称之为外围总线。
- u( d& S& v6 v5 e, [! F: D/ ]- @
, {1 Z# x5 }. k# o' A: e4 `
2 d' U7 S7 q0 {; Q3 D
O, j! d+ U: {. s! \& b图9.1.2. 8 STM32MP157A/D系统总架构图, `* x1 v) j9 v% c9 @/ j4 G
6. 时钟安全系统(CSS)
- ?% t+ {. K d1 \7 |& ^9 t如果启用外部时钟HSE和LSE的话,硬件将启用HSE或者LSE上的CSS。我们在上面的框图以及STM32CubeMX插件上的时钟树均可以看到CSS模块。如下图,右边的是框图中的部分截图,右边的是STM32CubeMX插件上具体的截图。图中配置HSE为PLL3的时钟来源,在并将pll3_p_ck(PLL3P)作为MCU子系统的时钟源,可以看到Enable CSS模块自动变为深蓝色,表示使能了CSS。而没有使用的LSE,对应的Enable CSS模块颜色为灰色。5 e7 W3 Q5 v1 G/ M8 z. z7 ?7 ^4 `
' o& m4 l$ S* e% }0 W5 }! \5 W+ H4 _
) N8 l( o, a1 C5 y5 @5 A; s
: n: f1 ^* ?& [9 M; d
图9.1.2. 9外部时钟CSS
$ _4 X8 ^0 G2 }* O: Z [RCC具有时钟安全系统(CSS),允许应用程序检测外部时钟(HSE、LSE)是否由于意外原因而停止翻转。
+ L8 q/ }5 i1 f/ X( V8 R4 J如果使用的是HSE,在检测到故障的情况下,CSS将生成系统重置,并保护BKPSRAM的敏感数据。时钟安全系统可以由应用软件通过HSECSSON位激活,(但无法通过软件清除HSECSSON位),即使将HSEON设置为0,也可以激活HSECSSON位。 当启用并准备好HSE并且HSECSSON设置为“ 1”时,硬件将启用HSE上的CSS。当禁用HSE时, HSE上的CSS会被禁用。- w/ s2 v8 g j9 f2 X4 z H
如果使用的是LSE,如果检测到故障,CSS将生成故障事件,禁用提供给RTC的时钟并保护BKPSRAM的敏感数据。也可以通过应用程序将RCC_BDCR寄存器的LSECSSON位置1来激活LSE上的时钟安全系统(CSS),但必须按照以下顺序在LSE上激活CSS:9 E& e! m+ }# m
①将LSEON设置为’1’,并等待LSERDY =‘1’;; \2 y0 g) K" d
②通过RTCSRC字段选择LSE时钟;
, w7 r6 z3 W2 \. ?③设置LSECSSON 到“ 1”。
( b" m {" F2 F1 x% X) u关于CSS,我们这里不做深入研究,感兴趣的可以查阅参考手册以及ST官网相关介绍。
2 ]# C# [' G2 J3 ?9.2 RCC寄存器介绍
6 s# Z: Z; g, V2 g9 u* f3 W: J下面我们来看看RCC相关的寄存器。RCC寄存器分为RCC TrustZone相关寄存器,RCC振荡器时钟相关寄存器,HSI、CSI、MCO1和MCO2、AXI、AHB、APB、PLL、MPU和MCU相关寄存器,还有外设的内核时钟选择寄存器。这么多寄存器,我们要配置哪个寄存器就查看对应寄存器即可,这里就不一一介绍所有的寄存器了。而且HAL库已经将这些寄存器封装好了,调用HAL库即可控制这些寄存器。# X4 F9 c9 H8 F
9.2.1 寄存器分类介绍# `) p7 Z6 l) Q% l; `# k. G, h! a
2 G; b/ z2 k6 A# b* j) s6 m
RCC振荡器时钟相关寄存器
) P/ f9 B" ]3 W- q! A$ \+ HRCC振荡器时钟寄存器有RCC_OCENSETR、RCC_OCENCLRR、RCC_OCRDYR。; I8 t0 ?! y6 L
(1)RCC振荡器时钟使能设置寄存器(RCC_OCENSETR)/ f8 s# H v. o0 s& ~, B
4 U x/ I2 K1 T1 I1 Q0 F
$ D: y# M" S- V2 M
0 s# v1 `, s( k, P) L- C$ `图9.2.1.1 RCC_OCENSETR寄存器) ?9 ?6 [3 h6 b( a. v
RCC_OCENSETR时钟用于使能(HSE、HSI、CSI)时钟,向该寄存器写入0无效,将对应的位置1则开启对应的时钟。如果TZEN = MCKPROT =‘1’,则只能在安全模式下修改此寄存器。在时钟恢复序列期间,不允许对该寄存器进行写访问。
! D. R9 B* c, f2 D% Y/ }该寄存器配置也比较简单,以HSE为例,如要开启HSE,则先将第8位HSEON置1来开启HSE时钟,再根据是否使用旁路模式来配置第10位HSEBYP,如果使用安全模式,则配置HSECSSON。
) V% n- f, }% u2 R(2)RCC振荡器时钟使能清除寄存器(RCC_OCENCLRR)" T" u# t$ N2 O9 F6 g4 Z
( b2 G2 B6 `3 V8 U- ^0 `
) ]' l1 K" a3 z
# p6 o. c- W/ S3 l" L图9.2.1.2 RCC_OCENCLRR寄存器4 F* ?5 V' U. ?( A9 O/ U0 A2 `
该寄存器用于清除寄存器对应的位,对位写0无效操作,写1表示清除设置的位,也就是关闭对应的时钟。如果TZEN = 1,则只能在安全模式下修改此寄存器。 在时钟恢复序列期间,不允许对该寄存器进行写访问。
3 B W8 _2 e! N$ S. B$ [1 t例如,要关闭HSE,则将HSEON位写1即可。其它寄存器操作也类似。
; a+ Q% z+ m5 P9 S7 L7 a(3)RCC振荡器时钟就绪寄存器(RCC_OCRDYR)" i: D5 I- k* @5 F! I( U
) p9 G. E- J! [; ^
" W# c, B" U' ^! i; @2 r) O+ j) p
图9.2.1.3 RCC_OCRDYR寄存器# V) n. C6 F8 @) X, _+ x
该寄存器是一个只读访问寄存器,它包含振荡器的状态标志,写该寄存器的话是没有效果的。如果读取某位为0的话,表示该时钟未准备就绪,如果读取某位为1的话,表示该时钟已经准备就绪。例如,如果读取HSERDY为0,表示HSE时钟未准备就绪,如果为1,表示HSE时钟已经准备就绪。" F8 W K" U3 ]; W' x- g0 F5 y& m
2. MCO1和MCO2配置寄存器' U" }6 t! i5 R
MCO1和MO2用于配置MCO1和MCO2输出,两个寄存器的配置类似,我们以MCO1为例:; C) Q. j# R" u0 d/ X6 E8 O
9 D7 F- R6 f' }, o) X
& v( e; s! ?1 D; @* V& ^5 I
; ]& b( b1 G' S! t图9.2.1.4 RCC_MCO1CFGR寄存器
* k3 H8 T" n. G5 gMCO1SEL[2:0]:为MCO1时钟输出选择位,由软件设置和清除。时钟源选择可能会在MCO1上产生毛刺,所以建议仅在复位后配置这些位,然后再使能外部振荡器和PLL。将该位:: Y: M+ j- u" x C0 C
0x0:选择HSI时钟(hsi_ck)作为输出;
" x U" Z. L% N7 J) O0x1:选择HSE时钟(hse_ck)作为输出;
+ V7 K9 T9 N% B& C$ |1 [3 j2 m1 T# t& [0x2:选择CSI时钟(csi_ck)作为输出;
5 Z0 L! i3 J5 l7 ^* Q6 W& |0x3:选择LSI时钟(lsi_ck)作为输出;
0 Y# A3 B" z+ e0 j7 q0x4:选择LSE时钟(lse_ck)作为输出;
0 Y# C4 C% B5 c4 Y; H) ~4 QMCO1DIV[3:0]位用于配置MCO1的预分频器,对该位配置:
' m6 Q' X% ^* J- s0x0:旁路模式;! v" l; g- E; U) _( k
0x1:分频值为2;- r' \+ v" D$ ?, v8 k0 P. ]/ K- U
0x2:分频值为3;- o% o9 B4 f# z4 ~3 ?2 Z3 o2 t# k
…
, Q) T0 P" w( r" K b& n0xF:分频值为16.
1 l- M2 g, Z+ N: t* Z$ [& bMCO1ON位用于控制MCO1输出,将该位写0则关闭MCO1输出,写1则开启MCO1输出。
0 H# v4 g: s5 P5 o3. MPU和MCU时钟源选择寄存器, n* `( @ F& j y# z
STM32MP157有两个A7内核和一个M4内核,RCC_MPCKSELR和RCC_MSSCKSELR分别用于选择MPU和MCU的时钟源。我们以MCU为例:7 p, L% p5 e: u: H; m c4 l
(1)RCC MCU子系统时钟选择寄存器(RCC_MSSCKSELR)
# L2 W# K; Y8 m% z% g' U9 W: m2 Z& _9 S: T/ w6 _; I$ d
5 G! C- E) _, h5 R9 t
3 H- K% W6 Y/ N2 e! v) X图9.2.1.5 RCC_MSSCKSELR寄存器7 j5 W- d1 ^" @ i& |7 z
MCUSSRC[1:0]位用于选择MCU子系统的时钟(MCUSS)来源,通过配置该位可以选择MCUSS可以来自:
) M/ x2 @- P6 m0x0: 配置MCUSS时钟源来自HSI时钟(hsi_ck);& E& ^5 I5 ^* J: c
0x1: 配置MCUSS时钟源来自HSE (hse_ck);4 O; l' D/ u! t- Q- M1 m" j
0x2:配置MCUSS时钟源来自CSI (csi_ck);
5 o4 V# r( t) t0x3:配置MCUSS时钟来自PLL3 (pll3_p_ck); u3 i4 E" b: Z
MCUSSRCRDY属于只读位,读取该位为1,表示MCUSS已经准备好,可以切换到MCUSSRC配置的时钟;读取该位为0表示MCUSS未准备好。6 A9 g9 K3 N/ m2 H3 y7 b
5 Z5 j# K" h( |$ V: h通过软件置1和清除,以配置MCO1的预分频器。 修改该预分频器可能会在MCO1上产生毛刺。 强烈建议仅在复位后才更改该预分频器,然后再使能外部振荡器和PLL。9 M1 f) r% A7 ]" P
5 o! C4 k# Z. G$ Y有关详细信息,请参见第10.4.4节:时钟输出生成(MCO1和MCO2)。7 H% H, K2 T6 J# P ^7 H+ Z
(2)RCC MCU时钟分频器寄存器(RCC_MCUDIVR)
6 f V3 ]2 e! ?0 J
/ [8 }. M9 [9 S. a/ \9 r* h- W
) M- [' ^! M' X6 f
; r# i1 w' Y+ V) E
图9.2.1.6 RCC_MCUDIVR寄存器+ w7 Z' W! `) F+ Z; D
MCUDIV[3:0]用于配置MCU时钟的分频值,配置该位:
4 h( V7 w" Y+ `$ d9 @/ Y0x0:MCU子系统时钟(也可以写为MCUSS或者mcuss_ck)不分频;. ^9 H- r9 S$ d- V
0x1:2分频;
# n0 s, n/ g1 p! U0x2:4分频;- x8 y9 F) J) P- |% p9 R
0x3:8分频;
: w' s' \/ S. ]' X…
5 a: V! E+ N/ Y0x8:256分频。
0 V- a$ ?" C* ^7 Z: J其它配置:512分频
. E8 Z3 s& i+ `2 ~% QMCUDIVRDY属于只读位,用于指示是否考虑了新的分频系数,该位:* w$ R# [" z' T, w2 [
0:尚未考虑新的除法系数;
8 X3 `& _5 b& n+ a7 f- y2 V4 e0 Y1:考虑新的除法系数。(重置后的默认值)
8 O+ Y x6 s( w6 K! L4 _: G9 A6 p, v4. HSI/CSI时钟配置寄存器8 q6 \* @. a6 ]) z
HSI和CSI时钟配置寄存器由RCC_HSICFGR和RCC_CSICFGR来控制,用于配置HSI和CSI的时钟源选择和分频器分频值等。( E$ e+ f; d9 Q6 L0 z; R8 q
(1)RCC HSI配置寄存器(RCC_HSICFGR)
: j# c6 b& K: H1 i# Z ~- Q. D; W" L6 h) J/ N
. g! n3 s Z# C0 F; Y& P; _
& D/ H+ C& j- i, Z$ ] ?图9.2.1.7 RCC_HSICFGR寄存器
+ f! o' m: |$ FHSIDIV[1:0]用于配置HSI的时钟分频值:
( L4 [4 r' h6 x0x0:配置HSI为1分频,hsi_ck (hsi_ker_ck) = 64 MHz;
* q8 L. |6 M6 [# r$ B7 \6 L8 ~8 ^0x1:配置HSI为2分频,hsi_ck (hsi_ker_ck) = 32 MHz;
3 f( N" Z' ~! E8 Q7 T. {1 N. I) R, q% K0x2:配置HSI为4分频,hsi_ck (hsi_ker_ck) = 16 MHz;
8 @; M) }0 s* f- D7 ^: l0x3:配置HSI为8分频,hsi_ck (hsi_ker_ck) = 8 MHz;' g/ Z. d M; N4 U3 [- b3 [6 f7 N
HSICAL[11:0]和HSITRIM[6:0]:用于配置时钟校准调整值,这两位我们可以不用管。5 i+ t) `# B0 T. F/ M
5. AXI/PLL1/PLL2/PLL3/PLL4时钟选择
- Q6 J! M( e* J1 _7 [6 z% z( `4 _AXI/PLL1/PLL2/PLL3/PLL4都有对应的时钟选择寄存器,用于选择其时钟源。
! h6 {( f/ u; I& U; wAXI子系统的时钟可以选择HSI、HSE、PLL2;! `; o0 v2 i( a: t
PLL1和PLL2时钟可以来自HSI、HSE;
" A! D' q* O. |% G2 ]$ \$ mPLL3时钟可以来自HSI、HSE、CSI;) k+ g- B# Y/ D4 ~9 ]) ?# e! {" K
PLL4时钟可以来自HSI、HSE、CSI和I2S_CKIN。
, A) A) @; c- q* }8 g以上相关时钟源选择配置寄存器的配置也比较简单,我们就不一一举例子了,如需要,大家按照要求来配置即可。1 M+ Z* D) ^3 M6 d3 k+ j5 l$ I% C
5. APB、AXI分频寄存器, {3 P4 p; k, M+ z z
APB1APB5以及AXI都有时钟分频器,可以设置时钟分频值。AXI时钟分频器可以配置分频值为1、2、3和4。APB1APB5分频器的分频值可以配置为2、4、8和16。相关寄存器的配置也比较简单,我们这里就不一一介绍了。, e2 p# c* F. R3 ? U
6. PLL控制和配置寄存器
) w& J* f0 I* M2 n5 q8 G6 wPLL1~PLL4有专门的控制寄存器和配置寄存器,我们这里以PLL1为例进行介绍。! k7 k( {$ }0 K3 u) D
(1)RCC PLL1控制寄存器(RCC_PLL1CR). N0 X. q6 X1 C# i5 @
3 P0 Y# `- ?' q* N1 O, u
8 }' x: O# c! ^
" r( L( F. ~' j! u* h2 b! {) L' y
图9.2.1.8 RCC_PLL1CR寄存器
) V& h6 o8 X$ j/ d' _0 @PLLON位用于使能PLL1,将该位置1表示使能PLL1,复位表示将PLL1关闭;0 C$ z& {3 q1 i
PLL1RDY表示PLL1时钟就绪标志位,属于只读位。该位为1表示PLL1处于锁的状态,为0表示PLL1处于未锁的状态;
- i4 l1 \3 B) `5 D; K+ vDIVPEN表示PLL1 DIVP分频器输出使能位,通过软件置1和复位,以使能PLL1的pll1_p_ck输出。该位为0,表示pll1_p_ck输出被禁用,为1,表示pll1_p_ck输出启用。为了节省功耗,当不需要pll1_p_ck时,必须将DIVPEN和DIVP设置为’0’。# a/ o& G; a* b( R2 c L/ W
DIVQEN是PLL1 DIVQ分频器输出使能位,该位为0,表示pll1_q_ck输出被禁用;该位为1,表示设置pll1_q_ck输出已启用。
- R" y) E/ F' T' l4 K- FDIVREN表示PLL1 DIVR分频器输出使能位,该位为0,表示pll1_r_ck输出被禁用;该位为1,表示设置pll1_r_ck输出启用。
! u& o! w% @" e4 _2 w(2)PLL1配置寄存器(RCC_PLL1CFGR1和RCC_PLL1CFGR2)- [4 j$ l5 j% ?0 C+ R, _( w. b0 V
PLL1配置寄存器有2个,其它PLL配置寄存器也是有2个。PLL1配置寄存器1(RCC_PLL1CFGR1)如下:
% ]) K8 q0 r Z( k& O3 j
4 c" T' M6 Q7 R( r! i( ^
8 i* p4 k7 v! |# \1 K, w. u+ b0 b. |
图9.2.1.9 RCC_PLL1CFGR1寄存器. }, Y) j. T* C: ~$ g {0 ?- x3 r
DIVN[8:0]位用于配置PLL1 VCO的倍数,将该位配置为:
8 f7 v( T! B" Q0x18:分频比为25;$ f, H3 D- b0 B; q( C4 e
0x19:分频比为 26;! T* m$ E7 b! h$ B0 f ^' K
…
8 z3 M5 L( _ \$ k J( z6 Y0x31: 分频比为50 (default after reset);
8 `: a5 K: U& Q…" L% x9 l1 e- s9 o* Q" ]9 ]
0x63: 分频比为100。
' `5 j( Y4 J( w4 P. n3 T- l& vPLL1配置寄存器1(RCC_PLL1CFGR2)如下:. _0 O3 ^' i& k" F% j
6 m3 t( B g1 o6 W. ^" F$ T* U# n
3 H" S$ P7 a# d8 {2 }+ v2 }* @) E2 n- T& l
图9.2.1.10 RCC_PLL1CFGR2寄存器
/ y' {; q$ w: y6 e7 J& }DIVP[6:0]位、DIVQ[6:0]位和DIVR[6:0]位分别用于配置PLL1 DIVP、PLL1 DIVQ和PLL1 DIVR的分频系数,要配置为什么样的分频值,根据手册来配置即可。! H5 y- F7 i8 H6 t3 m/ C* q
7. PLL小数寄存器
/ I w9 \: l- M8 ]0 V* _+ c我们前面也说过,锁相环PLL的 FRACV是分数倍频系数,它和DIVN一起组成PLL的倍频系数,当FRACV等于0的时候表示在整数模式下,大于0的时候表示在小数(或者说分数)模式下。每个PLL都有其配置小数寄存器。我们以PLL1为例子。RCC PLL1小数寄存器(RCC_PLL1FRACR)如下:3 y& |( G) w) t7 z
6 X8 X7 o3 L+ o L, \- l
4 m' q/ O5 g/ L2 T: L3 N$ O o' H7 ^- K$ `$ r
图9.2.1.11 RCC_PLL1FRACR寄存器
# C" a. h4 N& `7 VFRACLE是PLL1分数使能位,该位必须先设置为0然后设置为1,这样,通过从0到“1”的转换将FRACV的内容传输到调制器中。
% w0 y" q4 }6 F3 G7 ` y0 PFRACV[12:0]是PLL1的乘法因子的小数部分。FRACV可介于0和213-1之间。* F" a% q! R* k/ r8 `$ S
9.2.2 外设时钟使能和关闭6 B6 m g. P3 ~! a+ `
我们要注意,STM32MP1外设的时钟在上电复位以后默认处于关闭状态,即这些外设都不可用,这么做的目的就是为了降低功耗,如果我们要使用某个外设,必须要先打开外设的时钟才可以对外设进行操作。% g' R8 Y5 E5 ^. r! a7 [0 G8 J6 I
在RCC外设相关寄存器中,我们会看到有两种寄存器,一个是以“SETR”结尾的寄存器,一个是以“CLRR”结尾的寄存器,前者是使能寄存器,后者是关闭寄存器。
3 `5 ^8 X, n1 Z) `* r例如,APB1总线挂载的设备有窗口看门狗(WWDG)、I2C1~ I2C3、USART2、UART8和TIM2~TIM7等等外设,如果要使用WWDG,就必须先开启WWDG的时钟。对于MCU,APB1外设的时钟由RCC_MC_APB1ENSETR寄存器来开启,其第28位是WWDG时钟使能位,对该位:
' B# a4 }/ }! P; n, q- j% h写’0’表示无效操作,读取该位,读到’0’则表示该外设时钟已经被关闭;
4 }: }, L5 ]. a, q& r写’1’表示开启外设时钟,读取该位,读取到’1’意味着外设时钟已经开启了。
8 ^+ C+ n; K% C1 d# ?
% N' Y) X3 e3 y! L+ w% K
, W8 E c* y7 Y4 j) e4 g- o, @' K6 i& h- @* F9 E6 J
图9.2.2.1 MC_APB1ENSETR寄存器
1 y+ f* ~' V; @- r* H1 [另外一个寄存器RCC_MP_APB1ENCLRR用于关闭APB1外设的时钟,我们看看此寄存器:. {5 J0 H/ d2 i! [5 I O/ W
( ]# b- N9 l5 @6 b; X! f! d
1 r- i5 q- ]. {# H
+ e+ l1 c- K9 _, B1 ]' P
图9.2.2.2 MP_APB1ENCLRR寄存器( w6 x( k( U% {
可以发现,并没有看到控制WWDG的时钟位,第28位是保留的,而其它外设,例如I2C1~ I2C3、USART2、UART8等对应的位有看到,对这些位:
* D% ?$ P/ {) i6 }写’0’表示无效操作,读取到’0’表示已经关闭外设的时钟了;; _" q6 r2 L; l
写’1’表示关闭外设时钟,读取到’1’表示已经开启外设时钟了;7 `8 o' s7 B: b1 `! X
为什么窗口看门狗(WWDG)在RCC_MC_APB1ENCLRR寄存器中没有对应的时钟操作位呢?因为WWDG这个外设比较特殊,只要开启窗口看门狗以后就不能将其关闭了,必须等到系统重置以后才可以禁用窗口看门狗,而其它外设就不一样了,其它外设可以通过对时钟操作位写1来将其时钟关闭。
8 [2 i$ n4 K$ T, n在HAL库的stm32mp1xx_hal_rcc.h头文件中有所有外设时钟开启或者关闭宏定义:8 |9 x1 {; ^8 U, [4 M1 V" [
1 J- E, f R7 h, I- u
- X% E6 H$ v; J; Z/ C' N* D* i* R8 U5 C/ L6 c) ]% ]& @* \
图9.2.2.3 外设时钟使能/禁用宏定义
- l2 e: q1 m( [ m: b* ]* z; f例如,WWDG部分:7 X# N: ^% m7 `$ ~& e, Q1 ~, E2 p
1 #define __HAL_RCC_WWDG1_CLK_ENABLE() (RCC->MC_APB1ENSETR = \ RCC_MC_APB1ENSETR_WWDG1EN)* b; T7 k! y4 z% f2 @
2 #define __HAL_RCC_WWDG1_CLK_DISABLE() (RCC->MC_APB1ENCLRR = \ RCC_MC_APB1ENCLRR_WWDG1EN)4 ]. V. z. s5 n H# W# ` w% y% `
RCC_MC_APB1ENSETR_WWDG1EN和RCC_MC_APB1ENCLRR_WWDG1EN在stm32mp157axx_cm4.h头文件中有定义:
- ]3 \+ @6 W2 k#define RCC_MC_APB1ENSETR_WWDG1EN B(28)
0 T3 t8 {- X G8 W2 k#define RCC_MC_APB1ENCLRR_WWDG1EN B(28)
: o$ w. r1 M; b" w" uB(28)则表示将第28位置1,所以(RCC->MC_APB1ENSETR = RCC_MC_APB1ENSETR_WWDG1EN)就表示将RCC_MC_APB1ENSETR寄存器的第28位写1,也就是开启WWDG这个外设的时钟。在代码中,要开启WWDG的时钟的话,直接调用__HAL_RCC_WWDG1_CLK_ENABLE()这个宏就可以实现,其它的所有外设的时钟开启都可以类推。
. N7 O9 G& Y( e' H3 }同理,(RCC->MC_APB1ENCLRR = RCC_MC_APB1ENCLRR_WWDG1EN)表示关闭WWDG的时钟,不过因为WWDG特殊,系统运行的时候是不能将WWDG时钟关闭的,所以寄存器RCC_MC_APB1ENCLRR并没有针对WWDG的操作位,所以调用__HAL_RCC_WWDG1_CLK_DISABLE()这个宏是没什么实际意义的,不过对于其它一般的外设,例如I2C3、USART2、UART8等,调用对应的宏就可以将外设时钟关闭了。
. t$ d7 t6 s% B2 c/ s& P又例如GPIOA~GPIOK是挂在AHB4总线上的,在HAL库的stm32mp1xx_hal_rcc.h头文件中同样有GPIO时钟开启和关闭宏定义,操作方法也和上面的外设类似。7 o+ ]. Y- s% r
$ G* F: d! a8 g
0 t2 h$ I! Z, B- @$ S- @
& Y" O- @6 M" A( h4 ~3 q图9.2.2.4 外设时钟使能/禁用宏定义8 s& o2 r$ I1 G/ r0 n1 f
事实上,用STM32CubeMX插件生成的初始化代码中,也就是通过调用这些宏来开启外设时钟的。$ x6 j; ^, M. t% Y" j% |- \
9.3 时钟相关API函数% S4 ?# g$ X% K% E$ Q! S2 ]
头文件:stm32mp1xx_hal_rcc.h。
! C. V5 V* n+ Q; f. W) bstm32mp1xx_hal_rcc.c是RCC相关的HAL模块驱动程序文件,主要管理复位、初始化、取消初始化和外设时钟控制的功能。
! |2 S K# m3 A3 J复位后,设备从内部高速时钟(HSI 64MHz)运行,并且所有外设均关闭,但内部SRAM1,SRAM2,SRAM3和PWR除外。高速(AHB)和低速(APB)总线上没有预分频,这些总线上映射的所有外设均以HSI速度运行。
4 C6 n2 K: q2 N& D3 K c, j- H设备复位后,如果用户需要更高的频率/性能,需要进行如下操作:
6 g% F5 k( `0 \①选择用于驱动MPU、AXI和MCU子系统的时钟源;
: R8 ~8 a5 w; [, t8 j! b②配置AHB和APB总线分频器;) F* \3 X5 E, _ ?) B" S
③对于那些不是从总线获得时钟的内核外设需要配置内核时钟源;
0 `6 z: B+ J6 `; N5 f④启用要使用的外设的时钟。
' ^' J m4 k2 @; F/ j) `不管使用哪个时钟源,都是通过软件对相关寄存器的操作来实现的,我们直接通过HAL库的API函数来实现控制寄存器操作,下面,我们来分析一下stm32mp1xx_hal_rcc.c文件中的API函数。7 M" t1 s* e! m* c
9.3.1 函数HAL_RCC_DeInit
; a: c% r3 R/ I, H●函数功能:主要就是将RCC时钟配置为默认重置状态,主要做了以下操作:/ Q% }* L. d$ Q
①将HSION位置1,将HSI用作系统时钟源;
% t' L% Z/ o; w2 e. y②将MCO1、MCO2HSE、PLL1、PLL2、PLL3和PLL4关闭;
) v3 r$ j. X, \" d- y p③AHB,APB总线预分频器设置为1;- W* V/ j, T$ {
④禁止所有中断(从CSTOP中断允许唤醒除外)。, G! e! I# _3 g- n5 W* @
●函数返回值:
, J+ T: A( g: G% a2 l% k4 m5 m枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)! R! y$ U* \3 [ t# X" n0 Y4 i9 |
●注意:此功能不会修改外设、LSI、 LSE 和 RTC时钟,不会更改HSECSS(HSE时钟安全系统HSE Clock security system)和LSECSS(LSE Clock security system)。* {0 F- L1 v* L4 I1 ^4 A% J8 M% E3 e9 ^' ?
函数HAL_RCC_DeInit的部分代码如下,因为代码很多,我们省略掉了部分代码。
4 X& r: U0 a8 ~. n- ~, f" w$ @. L, p- G
- 1 HAL_StatusTypeDef HAL_RCC_DeInit(void)
5 ]% Y4 `- O5 ~. f8 S - 2 {& w$ D$ a( X# ]
- 3 uint32_t tickstart;0 j/ @* y* i2 ]) ?6 z) e: |, R
- 4
# G7 u$ b6 I3 o" y6 s% x4 L J - 5 /* 将HSION位置1,使能HSI振荡器 */
2 Y' n/ r& v3 z U& \6 v - 6 SET_BIT(RCC->OCENSETR, RCC_OCENSETR_HSION);
: m6 S0 p1 P4 z( ~( W v' a - 7 6 V( D) M* H# V. f; P
- 8 /* 获取全局变量uwTick当前计算值 */
8 s+ T+ U4 n2 m2 ^" m g; B( z* P - 9 tickstart = HAL_GetTick();$ \8 T* [% c) G8 H6 H, |
- 10
, q7 J& Z, c: m6 ^! w8 W - 11 /* 等待HSI准备就绪 */
; @+ Y7 g% e: k1 B) I% z' F9 d - 12 while ((RCC->OCRDYR & RCC_OCRDYR_HSIRDY) == 0U). ~7 p0 l5 H) ~5 U$ u5 u
- 13 { K/ L$ ~( i L6 w8 D$ W% h( L* n
- 14 if ((HAL_GetTick() - tickstart) > HSI_TIMEOUT_VALUE) T/ [ P$ U! L8 a1 Z
- 15 {
4 J" G( s1 q( V! e# } p - 16 return HAL_TIMEOUT;4 {; \. S/ m$ o% o3 A
- 17 }7 W0 n' O) w* i" o* d* t
- 18 }7 v$ R5 c" x- ?$ Y' q' e% t$ z
- 19
. k- ]( r# x/ H! f$ f3 p% ~: i3 A - 20 /*$ p& U& [# h9 _& Q
- 21 *省略掉代码......
4 n( U4 b, T) Y' Y - 22 *关闭MCO1、MCO2HSE、PLL1、PLL2、PLL3和PLL4' k& m; t3 J- X: U3 h: J2 \
- 23 */+ x& K, R2 m- {2 T
- 24
9 ]1 g. t% ~8 Y4 d% g3 c/ L: J - 25 /* 等待HSIDIV分频器就绪*/
0 Y7 Z7 L+ N, O- S - 26 while ((RCC->OCRDYR & RCC_OCRDYR_HSIDIVRDY) == 0U)5 y7 ]5 l8 V# F) N7 @( o* j! o
- 27 {7 [# f" P! F4 Q: w, C, A
- 28 if ((HAL_GetTick() - tickstart) > HSI_TIMEOUT_VALUE)
+ F+ ~( h8 B% z2 {+ v5 i3 c7 z8 e - 29 {
( k' b. \5 Y9 t2 v& v - 30 return HAL_TIMEOUT;
" @9 Z4 u" W% E - 31 }$ j1 D2 b5 h |/ ]. [: ?- L0 c
- 32 }4 o2 C, N e2 Z% x
- 33 /******省略掉代码******/
3 p7 o' L9 J9 {3 F+ B6 U - 34 /* 更新SystemCoreClock全局变量 */
) Z. m! h& A" L& L/ b - 35 SystemCoreClock = HSI_VALUE;' B% ^( x1 ?& B. a, g, P3 C
- 364 H: L) g8 j' M8 _
- 37 /* 调整Systick中断时间 */0 a I6 h( c9 `$ q' @
- 38 if (HAL_InitTick(uwTickPrio) != HAL_OK)
6 @# y; z, {& P - 39 {' i+ M% C. ?9 T3 G2 M5 P
- 40 return HAL_ERROR;
8 D ?- K7 Q) O' ~$ c7 J - 41 }
: b1 M& u9 a$ [8 H1 m' B - 428 m3 P, i1 _0 [# @+ ?, b; [
- 43 return HAL_OK;
( s X& r$ w- i/ s8 ? - 44 }
复制代码 7 \. i C) C4 |: D5 d0 {
我们简单分析这部分代码:
* R3 |+ r1 y2 f1 N7 j1 r第6行,通过SET_BIT置位操作,将RCC_OCENSETR寄存器的HSEON位置1,表示使能HSI振荡器。, k+ d' M b/ O7 m" N' ?
第9到,先获取全局变量uwTick当前计算值。& m( `1 ~3 A- i
第12行,等待HSI是否已经就绪。
+ _8 L$ U( c4 u4 U- @$ r( U, o* `, w, Q% m: n: |' T
注:RCC_OCRDYR是振荡器时钟就绪寄存器7 o* P6 V/ O- ?0 I4 p* \8 {- t+ T
①第0位HSIRDY是HSI时钟就绪标志,由硬件置1,以指示HSI振荡器稳定,如果此位为0,表示HSI时钟未准备好,如果此位为1,表示HSI时钟已准备好。
3 I: w0 e4 j/ G' I/ a7 k②第2位HSIDIVRDY是分频器就绪标志位,由硬件置位和复位。如果此位为0,表示新的分频比尚未传播到hsi_ck(hsi_ker_ck)(复位后默认),如果此位为1,表示hsi_ck(hsi_ker_ck)时钟频率反映了新的HSIDIV值,即分频器已经准备好。5 u* q; l( I6 ]0 w. p4 G
③第4位CSIRDY是CSI时钟就绪标志,由硬件置1,以指示CSI振荡器稳定。如果此位为0,表示CSI时钟未准备好(复位后的默认值),如果此位为1,表示CSI时钟已准备好。
& F8 H, f; I( W3 m7 Y4 l- K④第8位HSERDY表示HSE时钟就绪标志由硬件置1,指示HSE振荡器稳定。如果此位为0,表示HSE时钟未准备好(复位后的默认值),如果此位为1,HSE时钟已准备好。
, m, e$ A! R$ K* c# G) E( z" u1 j. Q* o% u9 T6 N' Y+ j! U
) c: Y3 `/ ^; P. P t& {8 D ]" _" R' C) r6 ~# Q
图9.3.1. 1 RCC_OCRDYR寄存器; h$ g( v, v+ m+ J0 U4 W2 f- C j7 ^
代码中,RCC_OCRDYR_HSIRDY值为0x00000001。如果(RCC->OCRDYR & RCC_OCRDYR_HSIRDY) == 0U,表示HSI振荡器未稳定,程序会卡在while循环中。如果两次取得的全局变量uwTick的差值大于HSI_TIMEOUT_VALUE(100ms),表示HSI超时,程序返回HAL_TIMEOUT标志并退出while循环。$ M- U+ I! }+ Y* T7 Y
第26到第32行,表示等待HSIDIV分频器就绪。
+ t8 i) P: P8 }+ r第35行,更新SystemCoreClock全局变量的值为HSI_VALUE。SystemCoreClock变量用于存储系统时钟频率,可以用来设置SysTick定时器或配置其他参数,每次时钟变化时,都应该更新它,这样也是为了保证SystemCoreClock的准确性。我们在第6.3.4小节system_stm32mp1xx.c文件中有分析过。- N8 |8 s% I4 V
第38到第41行,调用HAL_InitTick函数更新Systick中断时间(1ms),因为很多地方要用systick作为时基源,所以时钟源变化后,要将其更新。如果HAL_InitTick函数没有运行成功,程序将返回HAL_ERROR。HAL_InitTick函数我们在7.4.2小节的stm32mp1xx_hal.c文件中有分析过。
+ O ], ?6 T3 f t# Y9.3.2 函数HAL_RCC_OscConfig8 L6 `/ v5 p: `# S4 R5 U4 E* f" q
●函数功能:主要就是配置 HSE、HSI、LSI、LSE 和 PLL(PLL1、PLL2、PLL3和PLL4)。
( r1 v- d" w6 X% n, ^8 r- d+ t●函数返回值:
6 K9 q' V, C% F0 R" V枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)
. \, S. z4 L0 c8 P' a; T8 D$ C Z; ^. z注意:当PLL用作系统时钟时,PLL不会被禁用。! n4 K) \% X! G# N
函数部分代码如下:
0 V* y' L2 ]2 ^
+ m: u% q! o# N3 C) N- 1 __weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef \ *RCC_OscInitStruct)
# h- j- c2 O* Z& R - 2 {
. @. r: v& [) Y. d/ P7 D - 3 uint32_t tickstart;9 Q( L3 w8 q' y& s6 E4 r' \" G$ a
- 4 HAL_StatusTypeDef result = HAL_OK; M) D \. V- Q; c' t$ S
- 5 + [7 E" F( f0 M0 p0 D) D" _
- 6 /* 检查是否是空指针 */) Y* s9 A; m9 h
- 7 if (RCC_OscInitStruct == NULL)
' Z+ A2 u: ^" R7 t - 8 {
* `; d) K- m5 g* I# P8 x& v9 ~4 J - 9 return HAL_ERROR;
+ g2 b: k6 Y! H9 e [ - 10 }$ Y; |* \- C! v: p
- 110 _; F$ W1 o- v& T
- 12 /* 使用断言检查参数 */
: K$ j# b+ {: H' m7 F- f( f - 13 assert_param(IS_RCC_OSCILLATORTYPE(RCC_OscInitStruct->OscillatorType));
& y9 @$ V% u& v( Y- Q - 14
+ e+ `$ N7 _ B% }* \0 a5 g - 15 /******省略HSE、HSI、CSI、LSI、LSE、PLL配置代码 ******/
' w' q/ z6 ^8 S - 16 return HAL_OK;
" b J& D: M. G* b4 b+ D* P - 17 }5 V1 K [' K# i: H) a1 H
- 18
复制代码
" R4 O7 T2 C. _! Zweak表示函数是弱定义,用户可以在其它地方重新定义一个同名的函数。函数参数是RCC_OscInitTypeDef 类型结构体变量,主要对RCC内部/外部振荡器(HSE,HSI,CSI,LSE和LSI)配置的结构定义,RCC_OscInitStruct是指向RCC_OscInitTypeDef结构的指针,RCC_OscInitTypeDef 类型结构体声明如下,通过指针可以操作结构体中的成员变量。例如,如果要选择HSE为振荡器:0 X/ q0 w" z+ ~, w0 s+ \4 u
①设置OscillatorType的值为RCC_OSCILLATORTYPE_HSE;4 C. P& U/ f6 I# h3 E& ?) N
②然后设置HSEState的值为RCC_HSE_ON开启HSE;( a: p, g" w! p( X6 c
③如果用到PLL,则配置对应PLL的参数。; |) i9 K, i9 Q$ O- z) i7 F* b( U4 e
对于其它的时钟源(HSI、LSI、LSE和CSI)配置方法类似。0 v* [2 q2 Y& z; |: M- \9 S, S7 l
: U; A# ^" j8 _: j- [- stm32mp1xx_hal_rcc.h文件代码+ r3 `+ Y: Z; _) l* f4 ]- |
- typedef struct4 v! d2 x" C, ^9 p5 q8 e
- {
' a/ X- `3 A* Y. W: k% u$ ] - uint32_t OscillatorType; /* 要配置的振荡器 */ ; ], E- B; o" a) P( a
- uint32_t HSEState; /* HSE的新状态 */
, Q k2 y4 \8 v* b - uint32_t LSEState; /* LSE的新状态 */ & v" S5 ?# X2 @2 ^' d. z3 R
- uint32_t HSIState; /* HSI的新状态 */
& ?/ S2 n `1 l- H - uint32_t HSICalibrationValue; /* 校准调整值 */ & e$ t) t2 Q4 @& b, x
- uint32_t HSIDivValue; /* HSI的分频系数 */ 0 t8 r9 n8 M$ g+ S8 O
- uint32_t LSIState; /* LSI的新状态 */
& ^+ n$ B2 F9 I6 i+ O - uint32_t CSIState; /* CSI的新状态 */ 5 m, c8 M' ]- |& `+ A& c! d
- uint32_t CSICalibrationValue; /* 校准调整值 */ . w U* i! z; {
- RCC_PLLInitTypeDef PLL; /* PLL1结构参数: */
, j+ r4 G0 k' K. B5 N) z - RCC_PLLInitTypeDef PLL2; /* PLL2结构参数 */
: L7 \8 K. m7 d+ d# @. @4 m - RCC_PLLInitTypeDef PLL3; /* PLL3结构参数 */
+ }* U) T' n; O6 ^+ h - RCC_PLLInitTypeDef PLL4; /* PLL4结构参数 */ " }; h3 E( ~4 e$ V8 v
- } RCC_OscInitTypeDef;
- [/ M) u3 n: C: p" p* D$ I( [
复制代码 $ U# V9 D8 m# ~8 P6 G
我们查看PLL的RCC_PLLInitTypeDef结构参数有哪些,如下,此结构体定义了锁相环PLL中用到的DIVM、DIVP、DIVQ、DIVR、FRACV等参数变量,通过给这些结构体成员赋值即可配置PLL的时钟。
H0 L1 c; W, ^0 q4 M% J+ E6 [+ s- ~ W
( @5 e1 Y$ c5 s5 s, f* ]# ?6 O- stm32mp1xx_hal_rcc.h文件代码. T! p% E0 z5 v; @+ I& Z3 X
- typedef struct
' I8 ^+ C9 m. I, e' V2 Q - {
# n( K n; M/ h& c1 l, { - uint32_t PLLState; /* PLL的新状态 */
7 Z/ R1 ~9 Y7 Z# Y - uint32_t PLLSource; /* PLL输入时钟源 */
2 Y% q( A5 x& B9 H+ n - uint32_t PLLM; /* PLL VCO输入时钟的分频系数DIVM */ 3 @5 v9 K3 k0 S& F% E
- uint32_t PLLN; /* PLL VCO输出时钟的倍数DIVN */ ; I: }+ o1 v& {; h7 v& Q* C
- uint32_t PLLP; /* 分频因子DIVP */ 5 J$ u, G9 k+ p7 ?
- uint32_t PLLQ; /* 分频因子DIVQ */ ) v. z/ D. w5 h* Z2 k+ o w. d$ M
- uint32_t PLLR; /* 分频因子DIVR */
; J, V7 b* w7 X+ V; c - uint32_t PLLRGE; /* PLL3和PLL4的PLL输入频率范围 */
$ Z7 n6 ~# j v$ _ - uint32_t PLLFRACV; /* PLL1 VCO乘数的小数部分FRACV */ - e; H; V$ _7 u" f6 ^$ N' l. Y
- uint32_t PLLMODE; /* 使用PLL模式 */
5 J$ @- g3 c: G; G: G+ n9 G - uint32_t MOD_PER; /* 调制周期调整 */ ( c7 o" u5 ~- ]0 u& T: q. m. S% H
- uint32_t RPDFN_DIS; /* 抖动的RPDF噪音控制 */
+ B" ^1 \7 S* R - uint32_t TPDFN_DIS; /* 抖动的TPDF噪声控制 */
. E8 H( E0 m' F - uint32_t SSCG_MODE; /* 扩频时钟发生器模式 */ + W6 h5 h+ V8 u% U$ [& b8 l
- uint32_t INC_STEP; /* 调制深度调整*/ 3 ?% K2 w& d. M2 N" t/ q# A
- } RCC_PLLInitTypeDef;
复制代码 % p! ]3 ^# X- \ ^5 d
在HSE、HSI、CSI、LSI、LSE、PLL配置代码部分,我们主要分析HSE、HSI和PLL配置部分。7 y/ a: b. U3 t
+ R9 X4 d* }6 n; P$ h6 VHSE配置部分. T# T! Z0 D4 P: J" C
- 1 if (((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_HSE) \, K' q) F2 b4 I5 S' s7 u
- == RCC_OSCILLATORTYPE_HSE)
9 g& z' z4 \$ s% w3 l+ w9 a# Q* U- ?/ g - 2 {0 i. x( D/ C. t
- 3 /* 使用断言检查检查参数 */
$ _$ H X+ Z/ R$ H - 4 assert_param(IS_RCC_HSE(RCC_OscInitStruct->HSEState));
3 [1 e' S9 d, k1 Y - 5 /* 当HSE在系统中的某处使用时,将不会被禁用 */8 t8 |( c& U; n6 P$ a* Q7 ]
- 6 if (IS_HSE_IN_USE())4 m$ T K, @. A5 p2 H
- 7 {
% }8 c" P7 c6 c/ i1 ]6 c) M - 8 if ((__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) != RESET) && \ (RCC_OscInitStruct->HSEState != RCC_HSE_ON))
4 H% l5 W t9 y) @6 m$ A9 ~ - 9 {
4 X5 m6 J% {8 h9 U- G, d2 H - 10 return HAL_ERROR;
( X6 a: p/ q( b) ~$ l - 11 }
+ ?7 C' o$ C7 N, o - 12 }
* @6 c3 R$ V- I' v3 Z" ?$ r - 13 else
8 `. F2 c! j3 h0 i9 r$ J - 14 {
; @% {- `( t$ m. U - 15 /* 配置HSE振荡器 */' Q" X: m9 V5 @9 J9 b
- 16 result = HAL_RCC_HSEConfig(RCC_OscInitStruct->HSEState);5 a4 ~9 i b9 ^! {" j
- 17 if (result != HAL_OK)
x0 l0 {6 M2 r, f6 d f# E' l5 Q& W - 18 {
# Z8 P7 v. C/ L' M, w1 N" p - 19 return result;9 U# P6 z" y7 i0 p/ A. z* K
- 20 }* T7 W; p8 P8 |5 Q0 s
- 21 }; D8 W! u6 R- j3 I3 O6 G. h, L
- 22 }
复制代码
: L* k$ }! m; A Y2 Z第1行,通过RCC_OscInitStruct指针判断振荡器是否选中了HSE,如果选中了HSE,将进行后续的配置操作。
: |, {5 a5 T3 g* C: ?' C4 W第4行,使用断言检查HSE参数的状态设置是否正确。
( {3 V/ M; C0 N# `. ~& I0 D第6到第12行,判断HSE是否有在使用中,如果有在使用,则不会被禁用。 (__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) != RESET)用于判断RCC_OCRDYR振荡器时钟就绪寄存器中的HSERDY位是否为0,为0的话表示HSE时钟未准备好(复位后的默认值也是0)。
6 r0 y. ~ w7 w5 z(RCC_OscInitStruct->HSEState != RCC_HSE_ON)表示HSE的状态没有被开启。+ h1 z* C; ^5 s6 c& s1 r
如果HSERDY位为0,且HSE的状态没有开启,那么HSE没有被使用,程序返回HAL_ERROR。是否已经开启HSE,这取决于用户,所以程序要通过判断HSERDY位以及用户的设置状态来判断到底HSE用还是没用。
( O, y; ]7 {* [& `) V第16行,调用HAL_RCC_HSEConfig函数完成HSE振荡器的初始化。
. _7 w0 f8 e$ u' g- e5 m7 u& u程序中主要实现操作3个寄存器,一个是上面提到的RCC_OCRDYR,另外两个是RCC_OCENCLRR核RCC_OCENSETR寄存器。 C, ~: f+ c. R, R
* s. r) Q, v, ~注:
8 _6 b1 m: o: `RCC_OCENCLRR寄存器用于控制振荡器,向该寄存器写入0无效,写入1将清除相应的位。
9 B$ ]0 E v3 w. S* D4 ?5 g, D第0位,HSION:写1表示清除HSION位,禁用HSI。0 s t( T _9 m5 V
第4位,CSION:写1表示清除CSION位,禁用CSI。2 `5 {# B% m+ D) y2 E7 V
第7位,DIGBYP:当连接到OSC_IN的外部时钟为低摆幅信号时,软件将其清零。写1表示清除DIGBYP位(模拟旁路),禁用 R: k$ e C, ~! b- h
第8位,HSEON:写1表示清除HSEON位,禁用HSE。* u$ \" ?+ L( D4 f' J) M' y; ]
RCC_OCENSETR寄存器的功能和RCC_OCENCLRR寄存器的功能相反,RCC_OCENSETR寄存器表示开启对应的功能。8 z! _8 J, t3 o0 A" N. v; V8 a
$ q7 j1 a. t8 Y; d; e
, y0 P0 \) b% c) _
% ^7 \$ {/ y+ l+ [图9.3.2. 1 RCC_OCENCLRR寄存器' e; ]. Y c2 b2 c; D1 C9 l
代码中附上了详细的注释,通过注释可以知道程序的实现过程:
/ b" g7 b/ S( n" C3 o% H5 B①在配置HSE前,先关闭HSE,如果已经使用了HSE,应先切换成别的时钟源,然后再关闭HSE,最后才可以配置HSE。
5 [6 o/ E& Z1 I7 S2 U②程序中通过将RCC振荡器时钟使能清除寄存器(RCC_OCENCLRR)的HSEON位写入“ 1”来禁用HSE。+ f% H. Y2 w9 ~3 E- q+ P
③通过验证RCC_OCRDYR的HSERDY位是否设置为’0’,检查HSE是否被禁用。
+ H) Q1 ^6 a8 T4 B+ ~$ v8 G/ o④通过将RCC振荡器时钟使能清除寄存器(RCC_OCENCLRR)的HSEBYP位写入’1’,禁用HSE旁路模式。2 v$ P1 m2 m1 W9 @' ?/ l/ {
⑤通过将RCC振荡器时钟使能设置寄存器(RCC_OCENSETR)的HSEON位写入“ 1”来再次使能HSE2 F& y& W7 w/ H
⑥检查RCC_OCRDYR的HSERDY位是否设置为“ 1”,然后准备使用HSE。7 }6 `. |4 ?% }9 E3 r, p$ f
8 r( M, q& Z& G, I# q( h
- 1 /**
1 S3 g; h# K+ m+ w5 s* Z3 u( k. C2 k4 ^ - 2 *@brief 根据指定的参数初始化RCC HSE振荡器。- N: Y( i) ^4 l9 x6 i9 f
- 3 * @note 在更改HSE配置之前,请注意不要将HSE振荡器用作时钟,也就是要先关闭HSE,& v! w S4 a1 T
- 4 * 如果使用了HSE,则必须选择另一个源时钟,然后更改HSE状态(例如:禁用它)。 $ f- h" g: ^! |8 R2 Q( x( _
- 5 *@note 进入STOP和STANDBY模式时,HSE由硬件停止。4 K0 r# Z; B1 X: B& z
- 6 *@note 此功能会重置CSSON位,因此,如果先前启用了时钟安全系统(CSS),
8 A# }. G* N# j( F# D! t - 7 则必须在调用此功能后再次启用CCS。
' k7 M, s, x6 j) a - 8 *@param 状态包含RCC HSE振荡器的配置。
w7 n0 F) E% W& \, [& ` - 9 * 此参数可以是下列值之一:
$ B2 S7 H3 H4 I8 ~ - 10 * @arg RCC_HSE_OFF:" x0 A$ Y; H& O+ u$ V! w9 Y3 ]
- 11 * 关闭HSE振荡器* }, E7 _7 k! a. y* Z
- 12 * @arg RCC_HSE_ON:
" ^% |- ^: x! ], T0 s% M* ~ - 13 * 打开HSE振荡器
: P7 C; M0 R8 [# ^1 ] - 14 * @arg RCC_HSE_BYPASS:
) x# v) W* ~# a. `4 M2 f- g - 15 * 使用提供给OSC_IN的低摆幅模拟信号(旁路时钟),
; u Q* V% _: y# e1 F, i6 |8 P; ? - 16 HSE振荡器被外部时钟旁路
' U6 ?6 B1 g4 B: u( h - 17 * @arg RCC_HSE_BYPASS_DIG:5 M8 S4 u5 a+ `9 a& f* L
- 18 * 使用提供给OSC_IN的全摆幅数字信号(数字旁路时钟),
! J8 _/ E6 {- Q* W& d! O - 19 HSE振荡器被外部时钟旁路
+ ?$ I& t3 W3 B. p8 |" {1 m - 20 * @retval HAL status0 |( U: h, K' [8 O! z) { P' F
- 21 */0 W( I3 e9 p3 j( c. _- Q
- 22 HAL_StatusTypeDef HAL_RCC_HSEConfig(uint32_t State)
: J$ ?* V) v1 m: ^5 D0 T: F; r6 k - 23 {9 W% _( j& L7 } B
- 24 uint32_t tickstart;
& y% Z3 _+ Y ] - 25
& D$ b2 i3 L# `/ Y4 X' C/ s - 26 /* 检查参数 */
) C& n) {) n, a$ ?; B. l5 g - 27 assert_param(IS_RCC_HSE(State));
- D6 {0 W& L( V( U - 28
. N& o6 q/ G; X. t7 H! H8 ]0 Q - 29 /* 在配置HSE之前禁用HSEON */. x5 X) E. W# a' ]+ d0 J8 s# X1 W. [
- 30 WRITE_REG(RCC->OCENCLRR, RCC_OCENCLRR_HSEON);) w; c, O/ _1 p& \
- 31 ! E* h' ]1 k& G" G9 U5 _
- 32 /* 获取全局变量uwTick当前计算值 */! y+ r3 J+ O) B2 p! o# R
- 33 tickstart = HAL_GetTick();
0 f3 |: u( R* Y: M6 d' i2 [; r$ X9 H - 34 3 x1 k; I* d8 q/ g! M" N
- 35 /* 等待直到禁用HSE */
) u" I, P7 g6 M6 N) X - 36 while (__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) != RESET)/ a# a4 E( B* ^" q
- 37 {: w: h4 F. E& {1 S) t D
- 38 if ((HAL_GetTick() - tickstart) > HSE_TIMEOUT_VALUE)& @, a* J+ e' ?! f4 Q3 @1 M# Y
- 39 {
& E8 [8 |3 @8 S7 [% @" J; a5 G - 40 return HAL_TIMEOUT;
3 G8 C5 |" t$ k - 41 }
7 L$ d8 ~$ A3 y# |' T5 J5 A5 p - 42 }
: n( h# Q ]4 O: z" ~6 w - 43
9 @: _! |: L( ?7 L( l! Z - 44 /* 清除剩余的位 */
! k) q4 G9 S# b3 h! e- ^- v - 45 WRITE_REG(RCC->OCENCLRR, (RCC_OCENCLRR_DIGBYP | \ RCC_OCENSETR_HSEBYP));3 i! t1 q- }7 Y9 e6 a5 r. Q, Y0 i
- 46 + r9 L: r. d' s/ V
- 47 /* 如果需要,启用HSE */% W! L( Y6 d& e7 t; P" Q% `2 Q; T# f
- 48 if (State != RCC_HSE_OFF)& A0 W8 N/ ]- q0 \7 d/ [9 t. Z' |9 z
- 49 {
* t. m- \/ } l - 50 /* 如果设置的是旁路时钟,则启用旁路模式 */7 R5 k9 u: O R, y" K3 m
- 51 if (State == RCC_HSE_BYPASS)
5 z) D6 O7 |; M) f# ~; C* K - 52 {/ [& N j- L$ U, B4 m# z% i) O
- 53 SET_BIT(RCC->OCENSETR, RCC_OCENSETR_HSEBYP);
; M/ N* f2 i0 @( b3 e( M - 54 }. w2 d8 ]+ t) r B" {0 L' U
- 55 /* 如果设置的是数字旁路时钟,则启用数字旁路模式 */
5 u; X7 W0 _; ^# Z% ~+ Z/ `4 G - 56 else if (State == RCC_HSE_BYPASS_DIG); ~$ ^" @5 U e6 X" t
- 57 {
( E7 E8 K& Y- p, ]) D - 58 SET_BIT(RCC->OCENSETR, RCC_OCENCLRR_DIGBYP);
- ^% k# y4 M7 R - 59 SET_BIT(RCC->OCENSETR, RCC_OCENSETR_HSEBYP);2 [. b& P8 p+ U) V" z
- 60 }
3 a& ~9 t+ f9 U& M1 B* N( K: J# u$ s - 61 . y+ O! s1 R; o7 t5 \: c
- 62 /* 启用HSE */
% [! z7 H' _! N/ C* l z* t - 63 SET_BIT(RCC->OCENSETR, RCC_OCENSETR_HSEON);
+ C6 g9 ^, A( _7 T1 @ - 64
# H+ |5 c0 C% R' Y6 f - 65 /* 获取全局变量uwTick当前计算值 */7 j1 Q! s( Q( S* l
- 66 tickstart = HAL_GetTick();0 b4 ?/ M- w/ m( X4 I+ h1 I- o
- 67
4 C+ p* R7 |5 ]" {0 g5 d% d$ W+ t - 68 /* 等待HSE准备就绪 */
4 H c5 M, E; W& }6 Q {7 G - 69 while (__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) == RESET)
( v0 C. j& k: d! O# X - 70 {$ z1 f7 S% q3 v; B
- 71 if ((HAL_GetTick() - tickstart) > HSE_TIMEOUT_VALUE)
4 N$ [9 R5 o: l D: L - 72 {
6 T/ s6 G! C1 c5 Y2 J- Q: m/ T - 73 return HAL_TIMEOUT;
9 M% w4 j0 W5 W - 74 }5 E; t: ^. g8 `
- 75 } y4 ] [( d) X4 `" L+ V# i( o- _
- 76 }
0 b+ ]/ u d" L# L% n% e - 77
+ U7 R+ H, ?$ X/ S) B. @ - 78 return HAL_OK;4 e9 Z- L+ J7 g& d0 }! x
- 79 }
5 G% R, c6 q8 y$ {; o
复制代码 5 G4 C+ f1 j" @/ y
HSI配置部分
5 w$ \+ D9 s7 ?3 {+ Z0 `3 X关于HSI的配置部分,有以下两点:
& I4 i4 p1 t# `2 f) K2 g( D' v①RCC_OCRDYR寄存器的HSIRDY标志指示HSI是否稳定。 启动时,在硬件将HSIRDY位置1之前,不会释放HSI输出时钟。应用程序还可以通过位于RCC振荡器时钟使能设置寄存器(RCC_OCENSETR)和RCC振荡器时钟使能清除寄存器(RCC_OCENCLRR)中的HSION位来控制HSI激活。
) f# D+ ]% M7 [②HSI的预分频器由位于HSI配置寄存器RCC_HSICFGR中的HSIDIV位控制。RCC振荡器时钟就绪寄存器RCC_OCRDYR可以使用标志HSIDIVRDY位来检查硬件何时考虑新的分频比。HSIDIV值可以即时更改,并且HSI将考虑新的分频比,但是,必须考虑:. l3 T" y( F3 @5 ^9 S3 ~3 b% H" E
如果HSI当前用作PLL的参考时钟,则不允许更改HSIDIV。% r; C! T+ _. B* W
如果当前将HSI时钟用作某些外围设备的内核时钟,则应用程序必须确保HSI频率变化不会干扰外围设备。2 M7 ^5 @7 ]9 U( W! R
HSI配置部分的代码框架如下,代码比较多,这里省略了代码。
$ V8 k0 O+ ~% h; C. y- 1 if (((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_HSI) \- \) ` t* \% p% c# V: Y
- == RCC_OSCILLATORTYPE_HSI)
1 v& W/ A: C: ? - 2 {* k- V1 ~1 q7 e1 U! j
- 3 /* 使用断言检查检查参数是否符合范围*/
7 Q7 b! {$ }" y3 _: V - 4 /****** 省略部分代码 ******/
9 j6 P' }9 J1 L9 n' ?$ ^5 K* a - 5 /*
0 s" H ~" ~( O$ L8 h7 J - 6 * 当HSI被使用时(在AXI、MPU、MCU和PLL1~pLL4使用),它不会被禁用,允许校准。
$ H2 d0 r! P$ e9 O7 v - 7 * 1、如果HSI当前用作PLL的参考时钟,则不允许更改HSIDIV的置。
" L s% r2 c9 h) h% {( E - 8 * 2、如果HSI没有被当做PLL的参考时钟,则更新HSIDIV值,; w, s* X% o3 y" y9 a# k
- 9 * 同时,更新SystemCoreClock全局变量。. e: c. i4 g7 k$ {
- 10 */
( P/ h. t* \8 F9 j - 11 if (IS_HSI_IN_USE())
) J8 v* R2 v& H4 ?3 w3 d4 f' y - 12 {6 u7 x* D; B* ^' \# R' @& y
- 13 /****** 省略部分代码 ******/! S4 ?$ N- w* }0 C
- 14 }
7 d! M* I8 N$ J" k - 15 /* 8 {0 Y! Z) V) c) e t; t/ W" m" [) S
- 16 * 如果HSI没有在AXI、MPU、MCU和PLL1~pLL4使用:( ^( I5 L4 Q9 P! u% m* q$ h+ R
- 17 * 1、确定HSI没有被使用,如果检查到HSIState为开启状态(一般是用户开启),先启$ w/ o/ M4 L" A$ R4 i7 v
- 18 * 用HSI,等待HSI准备就绪后,更新HSIDIV值,然后整内部高速振荡器(HSI)校准值。
, x: K6 V! C! I/ p) Y. H' L+ X+ { - 19 *
& \* w- C6 E# \ - 20 * 2、如果如果检查到HSIState为关闭状态(一般是用户关闭),则禁用HSI。
: A3 z7 z$ Z8 f - 21 */
# C7 _) ]7 o2 y8 l6 V, Q! g# [0 z - 22 else
- P* y: J: L! x2 } r - 23 {' I# h8 W1 i0 h R% c
- 24 /****** 省略部分代码 ******/& Y6 l, [' n" l5 j. S
- 25 }( U2 i+ k& |3 A/ f4 [, [+ f! o0 i
- 26 }
复制代码
/ l2 P9 H9 w' w* z3 G HPLL配置部分6 N) _! ?" L) H9 m
在启用PLL之前,必须先完成以下PLL配置:
/ o) f/ E, @- B4 B; y选择PLL时钟输入(HSI或CSI或HSE): U5 T+ G$ O( R/ Z$ B- Z7 U
PLL时钟频率输入范围(配置DIVM参数)7 _& n& Y6 P5 ~8 e
一旦启用了PLL,DIVMx,DIVNx,DIVPx,DIVQx和DIVRx这些参数就无法更改如果用户想要更改PLL参数,则他必须禁用相关的PLL(PLLxON = 0),并等待PLLxRDY标志为0才可以(这里的x等于1~4)。
" }/ X j. A0 k配置PLL部分主要是通过调用RCC_PLL1_Config、RCC_PLL2_Config、RCC_PLL3_Config、RCC_PLL4_Config函数来实现,函数的参数RCC_OscInitStruct用于指定配置哪一个PLL。通过对PLL的RCC_PLLInitTypeDef结构体成员赋值可以配置PLL中用到的DIVM、DIVP、DIVQ、DIVR、FRACV等参数。
6 b. [+ N5 Z3 A0 Y7 }. A3 I- 1 /* 配置PLL1 */, c b3 o% _0 j$ p' P& K7 z! W3 x
- 2 result = RCC_PLL1_Config(&(RCC_OscInitStruct->PLL));
- _/ \) W8 n) P, G9 @* z8 `* U- _3 B - 3 if (result != HAL_OK)3 t9 f+ a9 z& i; v' a. p" s$ R
- 4 {9 Z1 z$ y% _/ c; W, w5 `
- 5 return result;
9 A+ J( B% z4 l; B/ F0 | - 6 }8 x7 X, ^ R- p$ e, {
- 7
a; `2 Q/ P! I; L5 Q9 H- \9 _ - 8 /* 配置PLL2 */
: A$ e. ~, ]' a, b' ? - 9 result = RCCEx_PLL2_Config(&(RCC_OscInitStruct->PLL2));
; r6 Q" o; u, R$ ?( c2 S4 x% V9 p - 10 if (result != HAL_OK)
, h7 s+ W5 R9 |2 m, x% s9 y - 11 {: \; f" N( @ L4 G
- 12 return result;
6 `4 O- J) X! ]0 H - 13 }
7 r, E) V0 P4 e0 w - 14; N' ~) s& w5 m" I( T: V
- 15 /* 配置PLL3 */$ }6 L' w- j6 g5 e2 D% ^4 W4 S
- 16 result = RCCEx_PLL3_Config(&(RCC_OscInitStruct->PLL3));5 }% D( ^/ J+ D7 @ A: I7 O K
- 17 if (result != HAL_OK)# p; b( c% X. |4 N
- 18 {
5 }) @# C8 m6 E/ C - 19 return result;- Q% ^& I5 I* K
- 20 }
; b7 p1 J8 A" m1 [7 B- K7 d - 21
/ ~+ l: R8 z- Y - 22 /* 配置PLL4 */+ A1 n3 O* v; h3 n8 Q
- 23 result = RCCEx_PLL4_Config(&(RCC_OscInitStruct->PLL4));
. @. G4 F1 Q6 v0 Q2 I - 24 if (result != HAL_OK)
8 Y' I" F! x% S8 O8 E - 25 {
6 ^) @4 f- \3 F7 e, H) K - 26 return result;
. n- L0 ]1 q7 O! F) y6 I! y9 s - 27 }
复制代码
% [6 M4 Y1 \1 O9.3.3 函数HAL_RCC_ClockConfig% }9 | m6 T1 A5 `# _. I- J. L
●函数功能:根据RCC_ClkInitStruct中指定的参数初始化MPU,AXI,AHB和APB总线时钟。) U6 y0 x6 f2 k+ O- H: A
●函数返回值:% J- c. e2 V5 Z! _
枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)" A! q) M# R; X* k* {- f) F
注意:
6 Q3 B) u4 u. E/ D' u①系统从上电复位、停机或者待机唤醒以后,或者在HSE发生故障的情况下,则直接或间接将HSI直接或间接用作系统时钟。9 V/ _6 x+ y' G
②仅当目标时钟源准备就绪(启动延迟或PLL锁定后时钟稳定)时,才会从一个时钟源切换到另一时钟源。如果选择了尚未准备好的时钟源,则当时钟源准备就绪时将进行切换。
; K( ?. u1 R. @, u+ U③根据设备电压范围,软件必须正确设置HPRE [3:0]位以确保HCLK不超过最大允许频率。
5 e4 T' P/ v% Q5 |5 q9 G: C& D- ?
8 `$ |$ Q0 q I1 B6 f) U- 1 HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef \ *RCC_ClkInitStruct)
& G3 s: u3 E6 j. `" Q$ ? - 2 {' a2 Q% `% I: a
- 3
+ S% l' M# w: [8 o: p3 b- t - 4 HAL_StatusTypeDef status = HAL_OK;/ d! u& r0 J, S7 F" y
- 5 uint32_t tickstart;0 y% b0 N/ v( L7 G* g: D0 Z8 n
- 6 1 O+ {) t" c% K
- 7 /* 检查是否是检查空指针 */
) F& |0 s4 j* W. z' V - 8 if (RCC_ClkInitStruct == NULL)
1 E( c0 o3 @8 [+ Y7 H' n - 9 {! W5 J% B" X1 _9 \: Y
- 10 return HAL_ERROR;
% `% Z1 w+ c9 l" c: { - 11 }
) F- R- |, A9 f7 t$ b* F5 ] - 12
1 R8 V2 T" Z' B - 13 assert_param(IS_RCC_CLOCKTYPETYPE(RCC_ClkInitStruct->ClockType));8 w* w- v" m' H3 K
- 14 /******省略 MPU、AXISS模块配置代码******/9 O0 Q8 g8 k4 Y9 z2 @! T$ v
- 15 /******省略 APB1~APB5总线的分频器APB1DIV~APBDIV5的代码******/
& x6 Z* G; b; E* A5 f* c - 16
5 S. L! ?$ o* ^ - 17 return HAL_OK;
1 B0 W7 m- f. |) r" _0 O - 18 }
复制代码
8 A4 r O, { Q* l/ T参数RCC_ClkInitStruct是指向RCC_ClkInitTypeDef结构的指针,我们查看此结构体。如下,此结构体主要用于设置时钟源以及MPU、AXI子系统、MCU子系统和APB1~APB5的分频系数。
! Y, P0 ~# K- K) \6 R
% A) ~' G! B- J- C: Y# Istm32mp1xx_hal_rcc.h文件代码; g5 h$ q- e; a8 T# f. E1 T
4 d6 i5 {* e" A( @% w# i
- typedef struct
3 Z, U# d+ U+ O7 t6 k& q3 D - {
$ z" b% S p3 y# P! t1 e- \ y - uint32_t ClockType; /* 时钟源选择 /
. i) E3 B7 k8 I9 B/ q - RCC_MPUInitTypeDef MPUInit; / MPU结构参数(时钟源和分频数) /
" g/ `0 I5 B, Y% n6 e - RCC_AXISSInitTypeDef AXISSInit; / AXI结构参数(时钟源和分频数)/' U. R# K+ i* l; _- I% i
- RCC_MCUInitTypeDef MCUInit; / APB4分频数 /% b+ @3 I4 M$ ], m, e( w, m l6 A% |1 A
- uint32_t APB4_Div; / APB4分频数 /
. y4 ^ f: _/ c; s* n - uint32_t APB5_Div; / APB5分频数 /
+ m% R& P2 D. U- O" I - uint32_t APB1_Div; / APB1分频数 /0 s: q7 O. G+ T( v3 S
- uint32_t APB2_Div; / APB2分频数 /% W* u4 }0 i+ x
- uint32_t APB3_Div; / APB3分频数 */
( x# g X, B5 n" X - } RCC_ClkInitTypeDef;
复制代码
5 J: J) a5 K* c+ V6 ? `9.3.4 函数HAL_RCC_GetSystemCoreClockFreq5 ]! v$ G& D: f) e* V( g* i
●函数功能:根据所选的时钟源以及预定义的常量来返回系统核心频率。 |/ s. o; T, K; ^+ h" D
●函数返回值:2 a/ W" k2 h4 ^* b2 G/ u8 s& u2 B' G) B
MCU或者MPU时钟频率。9 i5 s" M) e' m
注意:
/ D( f- `: c' l% E! Q①我们说的系统时钟频率,其实就是MCU或者MPU的时钟频率。0 N' A9 T w2 d9 y
②每次MCU/MPU更改时,必须调用此函数以更新正确的值,否则,基于此功能的任何配置都将不正确。我们前面说的SystemCoreClock全局变量就是通过此函数来获取最新的系统时钟频率的:
% o& j2 H) ~9 G- B1 \8 s' D, ESystemCoreClock = HAL_RCC_GetSystemCoreClockFreq();
7 ?. g, [) J# W( w1 w6 W我们后面的实验会通过HAL_RCC_GetSystemCoreClockFreq函数来获取系统时钟频率。,函数如下。函数中,通过宏定义CORE_CA7来选择MPU或者MCU的时钟,其中HAL_RCC_GetMPUSSFreq函数返回MPU的时钟,HAL_RCC_GetMCUFreq函数返回MCU的时钟。4 w( S: G {6 u% K$ y0 O$ O& _. b' B
& M; [% @9 u& D, I# y! {! D' A- 1 uint32_t HAL_RCC_GetSystemCoreClockFreq(void)
; _/ L, p: k7 n( g& E0 ^2 b% n - 2 {: z& z- Q6 B% R8 X( G
- 3 #ifdef CORE_CA7: c5 V6 x/ H6 d! ~! f1 |. K& H( T
- 4 return HAL_RCC_GetMPUSSFreq();/ n! p( K4 Z8 ~
- 5 #else /* CORE_CM4 */
4 o6 C/ y% u' M/ g% e - 6 return HAL_RCC_GetMCUFreq();' }; u& V4 c. B! v6 E$ g) q1 U
- 7 #endif
3 W# F- Z, h/ ^- L2 i1 ~( i - 8 }
复制代码 $ Y: E% B6 a3 o% l' n7 s- B* p
9.3.5 HAL_RCCEx_PeriphCLKConfig; P, R7 X6 y3 F4 `
●函数功能:根据RCC_PeriphCLKInitTypeDef中的指定参数初始化RCC外设时钟。, A1 U6 D7 U, \
●函数返回值:
' E3 p6 P* ?9 w: W1 n9 R" E枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)2 e3 @- F( W; f- d
可以通过此函数可以指定配置哪个外设以及外设的时钟源是哪个,后面的外设实验中,我们会接触此函数。该函数声明如下:. w! r3 n! {/ q% b# d9 ^* X; `
k/ H( G0 ?9 \4 Z3 B" ?6 D3 t7 f% \- HAL_StatusTypeDef HAL_RCCEx_PeriphCLKConfig(RCC_PeriphCLKInitTypeDef! p4 d# D/ n) u
- *PeriphClkInit)* f' I9 l. V' B. R2 m+ P
- PeriphClkInit是指向包含字段PeriphClockSelection的RCC_PeriphCLKInitTypeDef结构的指针,该字段可以指定是哪个外设。例如,RCC_PERIPHCLK_UART24指定USART2和UART4,RCC_PERIPHCLK_I2C12指定I2C1和I2C2。
9 n' E; I5 z. n& @$ D- [0 s( ? - typedef struct; J3 C+ L! N! A" C& A7 _, `, g- v9 X$ U" n
- {3 x) A c+ B) R' ~, `9 R/ c
- uint64_t PeriphClockSelection; /* 外设时钟源选择 */
7 U6 G! m3 w5 i5 O9 A% u - RCC_PLLInitTypeDef PLL2; /* PLL2结构参数 */ 9 e$ z3 W. U; S: d6 c D$ E
- RCC_PLLInitTypeDef PLL3; /* PLL3结构参数 */
" @( B" M0 l( Y% W& n7 H - RCC_PLLInitTypeDef PLL4; /* PLL4结构参数 */+ \! m4 \* K$ U7 e) {
- /* 指定I2C1/2时钟源,该参数可以是RCC_PERIPHCLK_I2C12 */' K! x# n3 u7 A. r q. j: d& `) `
- uint32_t I2c12ClockSelection;
) {% B9 ]! C( b# \% E* C6 C5 Q4 U - /********* 省略部分代码 *********/$ {! [- b; P, u
- /* 指定UART2/4时钟源,该参数可以是RCCEx_UART24_Clock_Source */: u7 q- V" e6 Z! \& Q3 H" }
- uint32_t Uart24ClockSelection;
8 R3 K6 _- G5 [' m0 u" t - /* 指定UART3/5时钟源,该参数可以是RCCEx_UART35_Clock_Source */
0 J5 @9 {3 i3 W: A - uint32_t Uart35ClockSelection; ( Z5 s" p/ d0 K; s! S0 A
- /* 指定UART6时钟源,该参数可以是RCCEx_USART6_Clock_Source */
0 o9 N+ b4 F" l/ ]' |! ^ - uint32_t Usart6ClockSelection;
1 |/ y5 F! Q" w" ]( Y - /* 指定UART7/8时钟源,该参数可以是RCCEx_UART78_Clock_Source */
/ [) @# m; y& Q7 X; R# ^ - uint32_t Uart78ClockSelection; 7 I z8 O- W$ B& j; B0 z6 i2 O5 D
- /* 指定RNG1时钟源,该参数可以是RCCEx_RNG1_Clock_Source */ / O2 U7 X- }, P) h
- uint32_t Rng1ClockSelection;
H+ _" i; j' z$ a# ^2 i - /* 指定RTC时钟源,该参数可以是RCC_RTC_Clock_Source */ " S9 _- K/ b6 J
- uint32_t RTCClockSelection;
) D+ S4 l+ J j8 c5 Q. A - /********* 省略部分代码 *********/
9 e& i( z) h7 s - /* 指定TIM2时钟源,该参数可以是RCCEx_TIMG2_Prescaler_Selection */ ( V# {4 P, A7 Z( l$ }2 J. [
- uint32_t TIMG2PresSelection;
$ v, X m( G# O! l - } RCC_PeriphCLKInitTypeDef;' q6 W2 c, x+ R1 {
- RCC_PLLInitTypeDef结构体我们前面已经分析过,其声明了锁相环PLL中用到的DIVM、DIVP、DIVQ、DIVR、FRACV等参数变量。
# b2 j6 N1 E7 {% [
复制代码
% y! G# ]# [: n: f; q1 }2 E9.3.6 其它函数- Q. e" }0 |% |: u: s
; x+ Q# j3 s4 R7 j' v( v8 L$ `. V# s
; o* m3 H5 n9 M( D% [ j
8 Y( y- j- a/ c/ N表9.3.5. 1其它API函数9 W+ K7 p0 x& ?
9.4 配置系统时钟实验
+ I' P# V# n4 G8 U# F# \% s根据前面的分析以及ST官方给的总线框图,MCU时钟最大是209MHz,MCU有一个内核外设Systick,频率最大也是209MHz。下面我们分别使用HSE、HSI时钟源,通过配置时钟树来实现设置MCU最大的频率值,并通过程序读取MCU的时钟频率值和验证Systick的时钟频率。
d/ Y$ Q# H( j* }3 g; W9.4.1 使用HSE$ I6 H' A" {8 G3 F
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\2-1 MCU_HSE。& L: d) k% }) n% i8 x
' [7 g) D( o6 p; i4 f: O$ ?' _: e
硬件原理设计$ x* c+ _7 V; N( ?4 {# ]- _
正点原子STM32MP157开发板的核心板上外接了24MHz有源晶振,所以HSE为24MHz。
+ A1 s( q( i1 W, W/ l5 X; `1 _0 _8 K s/ l% Q9 r
, P2 d1 Q6 t w2 j. ^+ ]6 q+ a2 l. y' F
7 e1 l: B( l- S( ]- ~图9.4.1. 1 晶振原理图截图
5 i6 {- |& s+ D2. 软件配置
7 q! e1 @! J0 D" Z我们新建一个工程MCU_HSE,然后在STM32CubeMX插件上开启HSE,注意,要用外部时钟源HSE和LSE的话,一定要先将他们开启。如下图所示,会有4个选项:% z5 t( H5 l4 R- \( Q7 |
①Disable 关闭外部时钟6 O5 @; ^& u4 I9 ^& L
②BYPASS Clock Source 旁路时钟
; A8 H: Z" D' e H* s C③DIGBYPASS Clock Source 数字时钟(数字信号提供时钟): P/ D4 A7 C o2 I$ N
④Crystal/Ceramic Resonator 晶体/陶瓷谐振器
$ @% W6 X- [: L9 F* B0 e* {# R( D7 |5 _8 V/ `
6 K# ^# q' |+ d6 Z. d! n ?) z
& l% |3 q' k# M: n6 j( `
图9.4.1. 2 HSE时钟模式选择$ W; Z& v! j6 q3 H' w5 Q! Y/ w
旁路时钟是指使用外部晶振时,不需要芯片内部时钟驱动组件,直接从外部导入时钟信号,就好比芯片内部的驱动组件被旁路了,这种模式的话,一般接有源晶振。6 t C, Q$ Q( F# O' J3 u! @
晶体/陶瓷谐振器选项是指,使用外部晶振时,时钟源是由外部晶振体与MCU内部时钟驱动电路共同配合形成,有一定的启动时间,精度较高,可以接无源晶振或者有源晶振。
: G" E. _# b: h& H( ^7 E1 Z$ t无源晶振自身是无法起振的,如果使用的是无源晶振的话,需要芯片上电跑起来以后,芯片内部的供电电路给无源晶振供电以后才可以起振。如果配置成BYPASS Clock Source模式的话,无源晶振就没法工作了,所以必须配置成Crystal/Ceramic Resonator模式。
8 e9 y8 S3 L1 c- l( \如果使用有源晶振,只要外部电路一上电,有源晶振就起振了,所以可以选BYPASS Clock Source或者Crystal/Ceramic Resonator模式都是可以的。
3 Y' v- V' P9 w6 B' `如下图,将HSE配置成旁路时钟和晶体/陶瓷谐振器的差别,旁路时钟是单向信号,晶体/陶瓷谐振器是双向信号。- |. w7 J# f0 s% z" s
2 w- f! n% |3 A/ H- N
; L; N8 n& [" N, N+ H- j/ N9 g. Z# ]$ B: T" \4 }
图9.4.1. 3 旁路时钟! w2 F! P1 A( U0 m, c% N
! a; L: Q$ J# W& m5 I9 P9 ~6 T
/ G# d& H3 H5 a+ }/ v/ v: s
( Q5 J* { z; I8 I3 e3 c图9.4.1. 4晶体/陶瓷谐振器
# D4 h# _6 W; k2 _; V/ d我们这里就选Crystal/Ceramic Resonator模式(选BYPASS Clock Source模式也可以):
) g' }7 j1 x( O/ A2 X" |2 k7 M! m# ?4 ^& W4 L, m8 l
) Q# N* z8 I3 @* {1 z2 Y( h
2 r7 x* w3 W0 s. _3 k图9.4.1. 5选Crystal/Ceramic Resonator模式
( S7 i: m E# D- i2 w在Clock Configuration处选择HSE时钟作为时钟源。由24MHz倍频到209MHz的话,需要通过PLL3配置分频系数和倍频系数来实现。具体配置步骤为:2 K5 }+ t- l1 n8 R7 \0 V; O
①选择HSE作为PLL3的时钟源;
H$ |: W( E* o5 T②选择PLL3P(也就是pll3_p_ck)作为mcuss_ck的输入时钟源;
: b8 x E5 @+ M( r" n' x, L1 w③手动输入209并按下回车,最大也只能输入209了,如果输入大于209并回车以后,系统系会频率超出范围。$ H' B. N% {& b7 f7 W, h
④输入209以后,STM32CubeMX会动态计算PLL3的倍频和分频系数,其中: }. |* P0 {, ]% s0 P
DIVM3=2,DIVN3=52,fracv3=2048,DIVP3=31 f' y8 X. U& N0 O6 q: P, P% U: ]
我们将上面的参数代入前面的计算公式中,计算pll3_p_ck结果也刚好是209MHz。
! K9 w+ K% t1 s7 h; G
7 s2 P% @+ q8 f. i5 J
: W* o% a0 z- P0 b& `/ Z, D' C
3 a- @2 R" l* Q5 _6 D4 u
图9.4.1. 6 手动输入参数
' I+ o7 u4 \' G6 Y⑤STM32CubeMX动态计算MCU、AHB1~ AHB4以及APB1~ APB3桥接器和外设时钟。我们看到PCLK1~PCLK3后面显示红色,红色表示数值超出范围,并且窗口有一个叉提示配置异常:! e% U5 d* _8 {. N; {# T/ t
) Z1 T6 Q4 w; h
8 D% a, Y# L) Q9 i1 L# c6 a
: s0 c5 E7 m+ P2 w5 ^) L v9 S& Z图9.4.1. 7配置异常提示$ v% y$ O; {6 u0 l
根据前面9.1.2小节有提到,pclk1、pclk2、pclk3最大为104.5 MHz,所以我们要修改这部分的参数。APB1DIV~APB3DIV参数可选1/2/4/8/16,我们随便选其中一个,下图选参数2,刚好满足最大频率为104.5 MHz:2 z4 G3 k& z( `6 @9 p* w/ }
1 Q5 n) N- I! ]4 j/ ~/ y" A
: }3 ?! j0 H) w6 V
0 A+ a7 V0 y) I0 ~& r7 F9 w
图9.4.1. 8修改APB1DIV~APB3DIV
0 n: M8 d! e9 b$ f6 a查看上述配置,MCU和Systick的时钟频率均为209MHz,是否真的是这样呢?我们后面会通过HAL库函数读取它们的值。* Z$ c& W: {5 c+ a8 q
按下“Ctrl+S”保存配置,系统生成初始化代码。这里提一下,系统时钟(MCU)的代码默认是在main.c文件中的,所以在Project Manager窗口的Code Generator处勾选“Generate peripheral initialization as a pair of’.c/.h’ files per peripheral”以后是不会专门生成时钟代码的.c和.h文件的,这点我们在第4.1.2小节说过。所以这里就不选了:
, u& l" K" T: [5 x
5 J/ ?* u0 @" A; N
, w: d6 O8 k- w" ]! z t: {- I! H. m8 X* z4 b7 K( B5 ~4 L
图9.4.1. 9 Project Manager窗口配置( g: e0 _9 l0 @
系统生成工程如下:
6 w/ H9 r% d9 a$ k/ l4 u! F: I, i: D3 h6 a
3 ^( }; Z+ K5 |# ~4 M0 t2 k" ^7 @. i& E/ x9 M* J b
图9.4.1. 10生成MCU_HSE工程5 k6 |5 U7 K/ \
3. 添加用户代码
9 L; J/ Y. n6 J我们在main.c文件中添加如下代码:9 ^# d; P8 a+ [8 K% H8 W5 X
: R: b0 J% X8 P: d7 G- main.c文件代码1 w6 O; D o0 {& ]2 k
- static uint32_t mcu_freq = 0;
1 F2 W+ j7 r4 _3 W" b - static uint32_t Systick_freq = 0;! q; A6 W+ P# c" Q+ v8 w
- mcu_freq=HAL_RCC_GetSystemCoreClockFreq(); /* 获取MCU的时钟频率 */
/ Y! A( I. G% o) ]: J' n' I, k - Systick_freq=SysTick->LOAD; /* 获取Systick的LOAD寄存器的值 */
复制代码
& C* X& Y. | ~; \8 m' B( C* D编译、调试
/ I! f, E# U; G4 P7 s0 a3 S4 x8 G+ k保存修改,编译工程无报错,先在while循环处添加一个断点,然后按照第4.1.6小节连接好开发板和ST-Link,进入Debug模式。
& `7 y' @9 O7 }4 a8 \$ f/ W进入Debug模式以后,程序停在HAL_Init函数处,此时变量的值为01 A' Q8 N) ]9 E/ Y8 h p7 A: c- E U
! ?4 d( V9 S0 m* V
; U) o, B) T7 l
: q8 i7 @; ^1 p6 x5 x0 l4 }图9.4.1. 11进入Debug模式
- A6 A* L8 X! Q: Q4 z点击运行按钮,程序运行到断点处停止,观察此时mcu_freq的值为209000128,Systick_freq的值为208999。
; y9 w3 q5 y9 }# A. \3 Q5 K
$ X. Q6 N2 I3 x, P: w
- y$ m" E2 {0 k
6 ~& d4 I' S4 j3 n
图9.4.1. 12 运行调试
; @7 C) M/ E# D" }5. 实验结果分析# T% ?' ~6 t2 H% L( a D9 J! s
根据上面的调试结果,我们看到mcu_freq的值为209000128,约等于209MHz,说明此时的系统时钟(MCU时钟)是209MHz,与前面的配置一致。
$ M' G! C3 t6 y( [Systick_freq的值为208999,约等于209000,此值我们是读取SysTick定时器中的LOAD寄存器的值。我们根据此结果逆推一下,SysTick每次计209000下就发生中断,如果要确保每1ms的中断周期的话,那么频率必须为209MHz。
# s# J3 Z5 k1 S: y$ k0 l5 z! Z. O" }注:
+ I: [3 R+ s" `. g( X o. aSysTick一个24位向下递减计数器,启动后,LOAD寄存器的值赋给VAL 寄存器,VAL寄存器递减,当递减到0的时候,会产生一次中断,然后再从LOAD寄存器取值,然后再从所得值开始递减,递减到0的时候又产生一次中断,如此反复,从而实现计时。我们在前面的7.4.2小节有讲过。
2 X5 ?+ Y6 I2 t, U6 x8 X前面我们提到,SystemCoreClock全局变量也是通过HAL_RCC_GetSystemCoreClockFreq()函数来获取最新的系统时钟频率的,那么他的值应该也是209000128。我们在STM32CubeIDE上查看此变量的值:9 X- n2 r3 X6 Y3 ^& c+ \1 a
& e! H9 a6 Y2 H. h1 a; w) m
& h8 V# U3 l: a4 m( i
* T* t# w0 y w图9.4.1. 13 打开表达式窗口
1 d0 }6 y# v. `如下图,添加要观察的SystemCoreClock全局变量,其值为209000128。4 {! K1 ?6 Z+ c4 @ r* D4 q
9 N( H% a O# F u, N4 O
; ]2 ?+ A, ^ V, z2 |
4 z. T. u) q$ E" f' ~( |3 `8 W
图9.4.1. 14观察值( c! N/ s9 @6 [
9.4.2 使用HSI
( A0 M) i o7 @2 q8 ?- l$ _& j本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\2-2 MCU_HSI。/ g6 w4 Q0 i; M: |
新建一个工程,工程名字为MCU_HSI,进入时钟配置窗口以后,mcuss_ck默认是64MHz,我们手动输入209然后按下回车键,STM32CubeMX会自动计算各个参数,如下图所示:! @! s, q/ ~; [% S3 Z
6 J. V' X5 c l) G- a+ e& M) W6 Z
- Y! \; v" r2 m+ m( A0 n% b
/ o, U4 o. z( Q/ x3 Y图9.4.1. 15手动配置参数
+ f: M- V( W0 a$ n( ]% X3 [. D红色部分提示pclk1、pclk2和pclk3的频率数值超出范围,我们调整一下,将分频值由1改为2:
l& u8 g1 A. j N6 u
! ]1 v3 N" m5 {0 D
6 l7 k. f N& {! h- Y% C! }9 J; k! `/ N
图9.4.1. 16手动调整参数
8 }- V8 m% E3 a- A. k7 E. k配置好以后,我们看到各个参数:
, D- ]- R- A0 H# Q' k①默认选择HSI作为PLL3的时钟源;3 d9 @9 u; `6 r$ u, ^
②默认选择PLL3P(也就是pll3_p_ck)作为mcuss_ck的输入时钟源;# v6 u; |. V3 v! a+ j4 P+ [
④输入209以后,STM32CubeMX会动态计算PLL3的倍频和分频系数,其中:
! _) b% M- K* {# ?* iDIVM3=4,DIVN3=26,fracv3=1024,DIVP3=2
/ y& _- Z) f: T# F3 s1 U8 o我们将上面的参数代入前面的计算公式中,计算pll3_p_ck结果也刚好是209MHz。2 p t2 L3 Z u7 ]4 M+ w$ |& P
% |8 N e1 D( k! Q% {
按下“Ctrl+S”保存配置,STM32CubeIDE自动生成工程:- Q0 B$ Q2 U4 c; M
我们在main.c文件中添加如下代码:
2 a! u$ i5 X9 V+ j% i. V5 {
* V6 v; H. G! I- main.c文件代码: r! g3 K4 m& ]3 U: h% H/ _$ G- e) ^/ i
- static uint32_t mcu_freq = 0;
& S0 P. U: U- w! _ - static uint32_t Systick_freq = 0;, m& E8 u: J2 n: P- k
- mcu_freq=HAL_RCC_GetSystemCoreClockFreq();/* 获取MCU的时钟频率 */+ T+ n. |! H: \! y, ?
- Systick_freq=SysTick->LOAD; /* 获取Systick的LOAD寄存器的值 */
复制代码 $ B# j# o. r3 h0 k
7 T# d) ^' F$ v- M
( w/ n) }; H; F* h' N0 h& N3 `图9.4.1. 17添加的代码
0 s7 F% K- f3 f7 y0 ^6 t保存修改,编译工程无报错,先在while循环处添加一个断点,然后按照第4.1.6小节连接好开发板和ST-Link,进入Debug模式。注意不要选错前面的MCU_HSE的工程,可以在Debug配置界面处确认一下:- {, [- G0 m2 ?" ?6 x
% F* y6 `: l- j0 Q5 ]3 M, O1 H- S* w
) k8 B% q: Q1 p P$ O* Q" a
; w% o+ M: K7 t* i; {
图9.4.1. 18确认调试的工程
. b& W3 ]8 P2 `进入Debug模式以后,程序停在HAL_Init函数处,此时变量的值为0。
4 U: y9 V# `# f: a* j3 B$ W6 B3 J# w0 Y0 x
4 i% `2 _, {- ]8 F1 `/ H/ z; ^$ N8 u1 v( k
图9.4.1. 19进入Debug模式) ]1 L3 ~* u1 g/ k6 n9 D3 `
点击运行按钮,程序运行到断点处停止,观察此时mcu_freq的值为209000128,Systick_freq的值为208999。实验结果和上面MCU_HSE工程结果一样。实验分析请查看前面的MCU_HSE工程部分。
9 ^" w* c) r4 l2 |5 d/ k+ s/ m0 ?7 e( E" C. ~
5 F2 W+ [. s# M4 ?
; L* I& t7 W0 K7 ?# z2 s
图9.4.1. 20运行调试
( @& E( c1 t& Z( V3 R0 n: p9.5 实验代码分析
$ d( b3 {! R8 Z7 j: I! t实验虽然验证完毕了,虽然是STM32CubeIDE自动生成的初始化代码,但我们还是有必要分析一下工程代码,这对HAL库的理解还是很有好处的。我们以MCU_HSE工程为例子,分析一下代码的实现过程。
' x7 m Q; r# W9.5.1 配置HSE相关宏定义9 g1 \0 i. E9 p5 b# ^4 {7 Y
此部分主要是在stm32mp1xx_hal_conf.h文件中完成的,在第7.4.1小节有分析过此文件。
. W. X7 N8 |* r# |7 ^2 F. T9 {3 i' J5 F/ q
选择HSE相关模块( E3 x7 X G B5 f) o5 i9 S/ g
在stm32mp1xx_hal_conf.h文件中使能HAL驱动程序中要使用的模块的列表,实验中用到了RCC:6 G' p& ~! v, j6 e5 x5 c2 l8 S
- stm32mp1xx_hal_conf.h文件代码
: O: N" S& ~5 i2 y F, l - #define HAL_MODULE_ENABLED
! u( J( t5 k( l$ g: g7 D - /*#define HAL_HASH_MODULE_ENABLED */
- W8 }4 k% n8 C# U9 S& L1 R - #define HAL_HSEM_MODULE_ENABLED0 Z7 Z2 [: F7 B: E1 w0 Y- a
- /*#define HAL_WWDG_MODULE_ENABLED */2 d. h; J# C6 ~+ A- x
- #define HAL_GPIO_MODULE_ENABLED
; S1 T2 }% N+ g1 A - #define HAL_EXTI_MODULE_ENABLED& A8 V% i6 w% K
- #define HAL_DMA_MODULE_ENABLED
$ C% v- |5 U: y3 s" L: |9 V" T: M+ \ - #define HAL_MDMA_MODULE_ENABLED
e( c H; s3 N+ W7 Z - #define HAL_RCC_MODULE_ENABLED
% {5 `: U% c7 X* c+ ~- o - #define HAL_PWR_MODULE_ENABLED
: N3 C2 U- r, |% ]6 E. C4 b) T V - #define HAL_CORTEX_MODULE_ENABLED
复制代码
* P/ R# C2 |* a, P包含HSE相关模块的头文件
) E) Q' y8 d3 v% y* I& p/ V- stm32mp1xx_hal_conf.h文件代码- p6 x; ^8 q9 H2 F3 K
- #ifdef HAL_RCC_MODULE_ENABLED4 R' ^7 D! S2 V- i. w% F" V( C9 _
- #include "stm32mp1xx_hal_rcc.h"0 a* P5 @$ z# z* }
- #endif /* HAL_RCC_MODULE_ENABLED */' z1 w z8 Z9 w2 A% d: U& a% t8 ~
+ _+ f& B7 g# ^1 l) G) c3 l% d- #ifdef HAL_EXTI_MODULE_ENABLED
5 h; I2 G3 o7 r! x - #include "stm32mp1xx_hal_exti.h"
: S& u6 q- }3 u" _4 r+ L7 H) f - #endif /* HAL_EXTI_MODULE_ENABLED */
5 n" ^8 k( R9 N4 _( p - 0 }# z$ \' S9 Y- ]- o' C$ J
- #ifdef HAL_GPIO_MODULE_ENABLED: D- I9 r: |4 X1 G
- #include "stm32mp1xx_hal_gpio.h"
6 K/ _( J9 J: y3 @% K - #endif /* HAL_GPIO_MODULE_ENABLED */4 w5 W7 B( v g; e/ S8 c8 @
- + @, U' ]7 z, l
- #ifdef HAL_HSEM_MODULE_ENABLED8 P* F' O! h. ]! [9 `+ e
- #include "stm32mp1xx_hal_hsem.h". V/ S+ x6 U* C5 ^# m0 {; ?7 e
- #endif /* HAL_HSEM_MODULE_ENABLED */7 _2 A2 H3 r6 V1 c4 n
- /****** 省略部分代码 *******/
复制代码 $ l$ K- u/ r/ R; J! ~
配置HSE_VALUE5 t8 v6 d+ f1 y$ } u
宏定义HSE_VALUE匹配我们实际硬件的高速晶振频率(这里是24MHz),代码如下:" k& r* e7 r. M8 P- \( i3 ^
- stm32mp1xx_hal_conf.h文件代码
: e- X! t8 S# k$ W - #if !defined (HSE_VALUE)( N, ` n* w; c+ h' h5 _
- #define HSE_VALUE (24000000U)
f! K7 a. b1 g; F; J; r1 ^7 D - #endif
复制代码
0 p0 P% E X+ s+ z9 G9 h1 {9.5.2 系统时钟设置函数
8 e) u4 o+ S/ N: v* ]% [! n' C8 m% p1 r9 H
函数代码实现% y* q8 _6 q: T$ ?, t
gpio.c文件代码很简单,主要就是开启GPIOH时钟,因为HSE的两个时钟引脚挂在GPIOH上:# A0 b6 o, c3 b& a6 f5 [
- #include "gpio.h"+ n S7 _ i% [4 V- h6 M. E
- # ^1 l' I; k4 R
- void MX_GPIO_Init(void)
" [/ n/ q: L2 T) ?* n$ u3 I0 O - {0 Q3 t3 w& v2 K" |* T" M$ p$ P' M
- /* 开启GPIOH时钟,因为HSE的两个引脚PH0-OSC_IN和PH1-OSC_OUT挂在GPIOH上 */7 f' H3 n( m9 r$ [4 l5 `: p
- __HAL_RCC_GPIOH_CLK_ENABLE();
3 U. e- h5 c- ^0 F6 G% f8 A3 U - }
复制代码 / w+ R. I, V8 P# l2 f/ |
下面,我们重点来看看main.c文件中系统时钟初始化代码。如下图是系统时钟设置函数SystemClock_Config的代码。代码主要分成三部分:
9 M; V1 g0 l5 T, s q( v% F①第一部分,是定义并初始化RCC_OscInitStruct、RCC_ClkInitStruct结构体变量,初始值默认为0,即将HSE、HSI、LSE、LSE、CSI和PLL1~PLL4均关闭;
9 ^ Y0 p _) u: i②第二部分,通过RCC_OscInitStruct结构体变量开启时钟源HSI、HSE、LSI和PLL3,并设置PLL3的分频和倍频参数,从而使pll3_p_ck输出频率为209MHz。. o( e: Q! v3 h8 x. K
③第三部分,通过RCC_ClkInitStruct结构体变量完成AXI、MCU和APB1~APB5的配置,实现AXI、MCU、APB1~APB3和APB4~APB5时钟频率分别为64MHz、209MHz、64MHz和104.5MHz。+ w5 `6 E" W6 D( O5 j. n- T
main.c文件代码
( y8 y+ L! G. M3 C. ^9 K! O6 o- 1 /**
& U8 S, c% [' M6 S# L- Z: e1 X - 2 * @brief M4主频时钟设置函数,也就是设置PLL3
7 ~3 J5 i: T/ v0 K' S c+ c+ \! T - 3 * @ note plln: PLL3倍频系数(PLL倍频), 取值范围: 25~200." @" W. u. ]) C0 [+ \
- 4 * pllm: PLL3预分频系数(进PLL之前的分频), 取值范围: 1~64.4 f1 [/ n, J9 X, z( K
- 5 * pllp: PLL3的p分频系数(PLL之后的分频), 分频后作为系统时钟, 取 值范围: 1~128.(且必须是2的倍数)
; p9 j' O) q( Q( Z0 M - 6 * pllq: PLL3的q分频系数(PLL之后的分频), 取值范围: 1~128.
* v# v! W+ A/ d; Z# H - 7 *
. b/ f, H% j. ^1 l - 8 * MP157使用HSE时钟的时候,默认最高配置为:9 ]' M" L# }$ I( d& u! |2 u7 b
- 9 * CPU频率(mcu_ck) = MLHCLK = PLL3P / 1 = 209Mhz
8 C. D3 ~- w5 R* b4 p A - 10 * hclk = MLHCLK = 209Mhz: e' D3 D- C6 ^5 Y8 F n
- 11 * AHB1/2/3/4 = hclk = 209Mhz
, l6 E/ t- v) G9 {* o& O - 12 * APB1/2/3 = MLHCLK / 2 = 104.5Mhz' H8 u+ X2 p7 t$ R
- 13 * @retval 无;9 x l5 p8 f2 V/ ^4 t0 B( U
- 14 */6 n# x& d# `# Y3 {( w
- 15 void SystemClock_Config(void); e3 k& A" v' L( T# m2 U2 k
- 16 {; I; h. T6 T" {8 u1 s- q
- 17 /* 定义RCC_OscInitStruct、RCC_ClkInitStruct结构体变量,并初始化为0 */$ G6 P2 F/ e; [' b* M
- 18 RCC_OscInitTypeDef RCC_OscInitStruct = {0};
* A4 C9 A' Q4 P5 w3 w" Y% e - 19 RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};# u5 a$ C$ g% H( w! k3 W
- 20
8 V- \6 M5 }# u. u: R6 b& E - 21 /*给RCC_OscInitTypeDef结构中的成员变量赋值完成初始化RCC振荡器 */: L* `2 }$ d/ O9 L! D
- 22 RCC_OscInitStruct.OscillatorType = \ RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_LSI
( A; C; a" k% d0 C; f7 B - 23 |RCC_OSCILLATORTYPE_HSE;
( T3 t( F4 `+ h0 x( I& K8 X' g2 i0 p - 24 RCC_OscInitStruct.HSEState = RCC_HSE_ON; /* 打开HSE */% }" p- ^6 ?" Q5 n6 n6 v
- 25 RCC_OscInitStruct.HSIState = RCC_HSI_ON; /* 打开HSI */: e# l: ^3 Z6 s/ e& b- f" _
- 26 RCC_OscInitStruct.HSICalibrationValue = 16; /* 校准HSI值 */
) ^2 {' A. X2 V$ S - 27 RCC_OscInitStruct.HSIDivValue = RCC_HSI_DIV1; /* 设置HSI分频值为1 */0 P/ ]0 @8 t+ y( t( c
- 28
' H' s* T M' J" ~' h& `& y - 29 RCC_OscInitStruct.LSIState = RCC_LSI_ON; /* 开启LSI */) w" r" C* I7 ]% h8 u3 ^) g0 ^
- 30 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; /* 没有PLL的状态 */
4 V, {* B8 o' Z! ?* S4 G7 X+ W - 31 RCC_OscInitStruct.PLL2.PLLState = RCC_PLL_NONE; /* 没有PLL2的状态 */9 q3 Q1 z: {+ A/ }
- 32 RCC_OscInitStruct.PLL3.PLLState = RCC_PLL_ON; /* 开启PLL3 */7 o3 `7 `7 n1 I0 o/ ~1 j! n
- 33 /* PLL3输入时钟源为HSE */
: S( a9 P5 j: ?! d6 c - 34 RCC_OscInitStruct.PLL3.PLLSource = RCC_PLL3SOURCE_HSE;
: o J' L* l" x# G( J0 z - 35 /*
; H2 D# k# z, {: x: {4 { - 36 * 配置锁相环PLL3的分频和倍频参数,也就是: F0 \( b1 n8 l4 v: y
- 37 * DIVM3=2,DIVN3=52,DIVP3=3,DIVQ3=2,DIVR3=2,FRACV=2048& V: w6 c) Q/ `# j* b8 L
- 38 * 则PLL3的pll3_p_ck输出频率为:2 O9 \/ R8 w: Z; A# G
- 39 * pll3_p_ck==209MHz
- D! F& @1 v3 V - 40 */
% b; V0 Z8 G: F: n/ ]' p - 41 RCC_OscInitStruct.PLL3.PLLM = 2; /* DIVM3=2 */8 t; X: [3 f, |" P
- 42 RCC_OscInitStruct.PLL3.PLLN = 52; /* DIVN3=52 */
: ~( |: R, U1 @- ]% L9 x - 43 RCC_OscInitStruct.PLL3.PLLP = 3; /* DIVP3=3 */
/ M. e* {: O. G4 f3 l - 44 RCC_OscInitStruct.PLL3.PLLQ = 2; /* DIVQ3=2 */
3 T% K# u6 d5 O - 45 RCC_OscInitStruct.PLL3.PLLR = 2; /* DIVR3=2 */
! ~& b+ U. u, T7 u9 f( ~ - 46 RCC_OscInitStruct.PLL3.PLLRGE = RCC_PLL3IFRANGE_1;
1 y4 o& m! w; u - 47 RCC_OscInitStruct.PLL3.PLLFRACV = 2048; /* FRACV=2048 */
3 M4 @% _( d' J+ h - 48 RCC_OscInitStruct.PLL3.PLLMODE = RCC_PLL_FRACTIONAL;/* 分数模式 */' q7 ~7 d2 T o, ?
- 49 RCC_OscInitStruct.PLL4.PLLState = RCC_PLL_NONE; /* PLL4没有状态 */4 p; ]3 Q! s" j( a- s- {1 n, U
- 50
6 E7 T; w9 g" h2 j0 u$ ?: A9 c8 O - 51 /* 调用的HAL_RCC_OscConfig函数用于判断 HSE、HSI、LSI、LSE 和
- q0 ^+ a! }6 `/ E - 52 * PLL(PLL1、PLL2、PLL3和PLL4)是否配置完成,配置完成则返回HAL_OK。1 t' p4 P( b9 j# |4 ~, Q8 y+ F1 G; T
- 53 * 如果没有配置完成,发生错误的话就会进入Error_Handler函数(空循环)。
+ F& V. A% J2 r* y6 v5 [& a - 54 */5 A0 n4 |: Y# V& A) s
- 55 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
7 F/ M% b0 _8 l7 Q G - 56 {
! \5 z! g& c# k5 k - 57 Error_Handler();
+ S% V7 B' c2 O! n. S3 i - 58 }$ V( Y& p- }' D/ s d8 k& V5 e
- 59 /*1 \, Q' A5 J2 T
- 60 * 给RCC_ClkInitStruct结构体成员赋值来配置RCC时钟,也就是:
) s W+ [# _1 H) O- K. m - 61 * 配置AXI的时钟源和分频器分频值(也就是配置ACLK);" }4 O( K# ]: t$ D9 Y
- 62 * 配置MCU的时钟源和分频器分频值;
; x% F2 i0 h; {. \: k" E; B - 63 * 配置APB1~APB5的分频值(也就是配置PCLK1~5)。
* S* s, v* F/ h% f: Y U - 64 */1 M t" Y% R, G; w6 H, T* I! |/ L
- 65 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_ACLK
# P( [' e4 H9 _$ ?+ G7 h - 66 |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK28 g) Q# ^( ~; f
- 67 |RCC_CLOCKTYPE_PCLK3|RCC_CLOCKTYPE_PCLK4/ t% T& @7 ~4 o+ G, O( L" {" {
- 68 |RCC_CLOCKTYPE_PCLK5;" p6 D, q5 X0 U: @% y: Z' j% b# ]
- 69 /* 配置AXI时钟源为HSI */ " L5 D- o( Q7 N% G/ W& w
- 70 RCC_ClkInitStruct.AXISSInit.AXI_Clock = RCC_AXISSOURCE_HSI;2 X( R3 ]% E7 t( @2 w( X D9 \# y
- 71 /* 配置AXI分频器为1分频=ACLK=64MHz */ X) c; F! z/ V
- 72 RCC_ClkInitStruct.AXISSInit.AXI_Div = RCC_AXI_DIV1;
8 g8 ~, D* g+ O. l - 73 /* 配置MCU时钟源来自PLL3=209MHz */) S3 p; G6 i6 m# S2 s
- 74 RCC_ClkInitStruct.MCUInit.MCU_Clock = RCC_MCUSSOURCE_PLL3;7 a- W2 f1 c6 ?, N- T3 V/ W
- 75 /* 配置MCU分频器为1分频=MCU=209MHz */
5 Y7 H8 v* ~% c - 76 RCC_ClkInitStruct.MCUInit.MCU_Div = RCC_MCU_DIV1;
5 o2 d' @+ T' l! [) ]# s/ G Z - 77 /* 配置APB4分频器为1分频=PCLK4=64MHz*/
* ? C, m4 f9 i. f& O9 o - 78 RCC_ClkInitStruct.APB4_Div = RCC_APB4_DIV1;( i A! W) W* J) z! b
- 79 /* 配置APB5分频器为2分频=PCLK5=64MHz */
7 K( e `& I7 b* I( ^ - 80 RCC_ClkInitStruct.APB5_Div = RCC_APB5_DIV1;" \: q! d% M1 s! G
- 81 /* 配置APB1分频器为2分频=PCLK1=104.5MHz */
( A* ]9 A; i! M7 r0 n, a2 [3 ] - 82 RCC_ClkInitStruct.APB1_Div = RCC_APB1_DIV2;* u5 ~1 Z. N$ {9 Q4 B+ d
- 83 /* 配置APB2分频器为2分频=PCLK2=104.5MHz */
9 ]" j6 @$ {/ X) {0 p K - 84 RCC_ClkInitStruct.APB2_Div = RCC_APB2_DIV2;0 _2 L% _# H9 K/ t
- 85 /* 配置APB3分频器为2分频=PCLK3=104.5MHz */8 }) R n- R) \5 j: t. C: E# J. K5 p
- 86 RCC_ClkInitStruct.APB3_Div = RCC_APB3_DIV2;
' a4 `4 f# l+ v# g3 I - 87 /*9 h9 S/ c/ x* n
- 88 * 调用HAL_RCC_ClockConfig函数,根据RCC_ClkInitStruct中
0 [& M' R n" B+ ?6 W - 89 * 指定的参数初始化MPU,AXI,AHB和APB总线时钟,如果初始化
7 w: t5 f9 _/ U6 D' g - 90 * 不成功,则进入Error_Handler空循环函数。* h' G! W# _3 X
- 91 */
% t$ @& t: s! M6 E; z - 92 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct) != HAL_OK)
9 f! y* }5 |' t6 v - 93 {
; \ {1 R6 U' ?. s% C - 94 Error_Handler();
4 W* X K+ l: d9 U- M) _& P W - 95 }
* P* |1 P) v9 I9 Y - 96 /* 设置RTC时钟的HSE分频因子 */3 S6 t3 p& X5 r8 _
- 97 __HAL_RCC_RTC_HSEDIV(1);8 r2 @3 L8 h% a2 ^6 o" }" N, [
- 98 }
复制代码 3 x$ Y6 I7 F2 E8 _/ Z! M: u" w( l
函数代码分析
" Q7 Y5 h7 t9 \/ R9 E; G; U下面,我们来分析SystemClock_Config函数的代码实现过程。
5 t, |2 w8 U7 P5 U; D第18行,定义RCC_OscInitTypeDef类型结构体变量RCC_OscInitStruct,并初始化为0,表示将HSE、HSI、LSE、LSE、CSI和PLL1~PLL4均关闭。1 o4 c, s) W G2 h( \) Q
以上结构体在stm32mp1xx_hal_rcc.h文件中有定义,我们在前面也有详细介绍过,为了大家不总是跳跃看代码,这里还是贴出代码了,如下。( T. @, u+ N+ V- H
RCC_OscInitTypeDef主要对RCC内部/外部振荡器(HSE,HSI,CSI,LSE和LSI)配置的结构定义,主要是定义要配置的振荡器、时钟源的状态、校准值和PLL的参数。通过给结构体成员赋值即可开或者关对应的时钟,并可以配置PLL的参数。9 h* U5 B, B& E
- stm32mp1xx_hal_rcc.h文件代码! \1 p1 \1 |" i$ a4 u
- typedef struct: k6 c6 r6 k4 Y; k7 U
- {
$ s3 c* E0 A7 _% d4 o$ K# a - uint32_t OscillatorType; /* 要配置的振荡器 */
0 c5 f$ ^! y4 ]1 p8 }* p6 Z - uint32_t HSEState; /* HSE的新状态 */
) Z6 ?2 q* K; \1 p" n# u5 x - uint32_t LSEState; /* LSE的新状态 */
7 n$ Z/ R3 Z1 I - uint32_t HSIState; /* HSI的新状态 */ + g2 C( X: k2 ?) l$ b9 M# s
- uint32_t HSICalibrationValue; /* 校准调整值 */ * w! x7 `5 f/ o% I/ ]* U$ Z. k
- uint32_t HSIDivValue; /* HSI的分频系数 */
" R9 H2 O' D) z& M% | - uint32_t LSIState; /* LSI的新状态 */
8 g9 E9 D6 ?9 H1 A/ C" ] - uint32_t CSIState; /* CSI的新状态 */
; @) x0 m% |; r& p9 E7 W - uint32_t CSICalibrationValue; /* 校准调整值 */ # D. Y I8 H: m- {
- RCC_PLLInitTypeDef PLL; /* PLL1结构参数 */ . K% u! |# r$ ]7 @$ V) i
- RCC_PLLInitTypeDef PLL2; /* PLL2结构参数 */ - l! i! R. n M" _) ^
- RCC_PLLInitTypeDef PLL3; /* PLL3结构参数 */ ) f1 E* s6 D5 X% b! w
- RCC_PLLInitTypeDef PLL4; /* PLL4结构参数 */ . O1 _+ T9 d& e# y# C
- } RCC_OscInitTypeDef;
复制代码
8 ~ p# ]8 Q% j, O$ X4 R我们查看PLL的结构参数有哪些,如下,此结构体定义了锁相环PLL中用到的DIVM、DIVP、DIVQ、DIVR、FRACV等参数变量,通过给这些结构体成员赋值即可配置PLL的时钟。
. I: k1 k- L/ u+ K9 [* D
; h* l) W7 q) _- stm32mp1xx_hal_rcc.h文件代码
! b) `2 o. V. R! T
; {# _, v, n' _- typedef struct
& @' I5 Q K4 B- j - {$ y1 U! T# j# O$ r
- uint32_t PLLState; /* PLL的新状态 /2 j+ i0 x- S" H- h2 U# n0 n" {! G
- uint32_t PLLSource; / PLL输入时钟源 /
+ ]0 l5 u7 d! k& l0 n: J - uint32_t PLLM; / PLL VCO输入时钟的分频系数DIVM /
1 |9 \) K3 Z2 R/ E( n& s3 c% q - uint32_t PLLN; / PLL VCO输出时钟的倍数DIVN /+ t3 l j, z E. {' D5 {
- uint32_t PLLP; / 分频因子DIVP /
% b1 I. ?9 I2 a h+ A7 _, K5 u - uint32_t PLLQ; / 分频因子DIVQ /
6 x7 l& R" q- b% z: e' F7 q - uint32_t PLLR; / 分频因子DIVR /" _: _0 `* j! Y3 G2 |+ Y! M
- uint32_t PLLRGE; / PLL3和PLL4的PLL输入频率范围 /: a3 t; |$ G+ U! E
- uint32_t PLLFRACV; / PLL VCO乘数的小数部分FRACV /
# q% v( a* y) x' p - uint32_t PLLMODE; / 使用PLL模式 /
5 K4 ]* w+ O1 _" ~5 w9 k - uint32_t MOD_PER; / 调制周期调整 /; @+ g" j! y% S. E; H
- uint32_t RPDFN_DIS; / 抖动的RPDF噪音控制 /
4 `" y+ ^7 t) o" h( b( D# a - uint32_t TPDFN_DIS; / 抖动的TPDF噪声控制 /# f5 j. a! K2 z% e; E7 K$ K
- uint32_t SSCG_MODE; / 扩频时钟发生器模式 /
: q* ^; t1 j! O7 u# y7 e - uint32_t INC_STEP; / 调制深度调整*/
6 y5 M* Q! Q0 q - } RCC_PLLInitTypeDef;$ Q' H0 }3 o# M
- 第19行,定义RCC_ClkInitTypeDef类型结构体变量,并初始化为0,即AXI子系统、MCU子系统时钟源默认来自HSI,且AXI时钟和MCU时钟以及APB1~APB5时钟的分频器的分频值为0(没有分频)。
* a, e9 ~0 Y0 t3 f1 I" l% D0 ^ - RCC_ClkInitStruct此结构体我们在前面也有讲解过。RCC_ClkInitTypeDef结构体定义如下,主要是定义AXI和MPU的时钟源以及分频系数,还有APB1~APB5的分频系数,通过给结构体成员赋值即可实现对MPU、AXI和APB进行分频。# Y: _: B* e0 u
- stm32mp1xx_hal_rcc.h文件代码
) _) J% b0 y3 f9 c, Q+ H1 k, U8 { - - m, H% T: Z, L
- typedef struct
; j7 e2 `' S7 L# s0 b0 z - {, S- U# C! R* e* h7 A
- uint32_t ClockType; /* 时钟源选择 /( v8 O9 |" A: r0 f1 {* j& a* I
- RCC_MPUInitTypeDef MPUInit; / MPU结构参数(时钟源和分频数) /
) @2 V. l/ f/ }: Z( B. r, P# ^ - RCC_AXISSInitTypeDef AXISSInit; / AXI结构参数(时钟源和分频数)/1 X& ~0 v: V& c
- RCC_MCUInitTypeDef MCUInit; / APB4分频数 /
* X8 K/ S9 B1 s% C0 S - uint32_t APB4_Div; / APB4分频数 /, C2 S$ r" y% Z+ m2 b" R
- uint32_t APB5_Div; / APB5分频数 /( L2 V) u; I z) [
- uint32_t APB1_Div; / APB1分频数 /
/ H3 o! \2 ^8 I, p - uint32_t APB2_Div; / APB2分频数 /0 I3 @ u* U4 i6 B# @2 y, I0 Q
- uint32_t APB3_Div; / APB3分频数 */
8 _& B& @( B8 e - } RCC_ClkInitTypeDef;
复制代码
3 A' }: ?# G7 k; K; |( L4 ?分析完了结构体,整个系统时钟初始化就大概明白了,需要哪个时钟源,要用那个PLL以及PLL的各个参数,要对那根总线进行分频以及分频系数是多少等等,我们都可以通过给结构体成员赋值来实现。SystemClock_Config函数其实也是这么做的,我们接着分析此函数剩下的代码。1 t5 p# n& @0 w' _) g) b0 q
第21到第34行,给RCC_OscInitTypeDef结构中的成员变量赋值完成初始化RCC振荡器,开启了HSE,配置PLL3为209MHz。, `. W0 h6 d4 U
第55到58行,调用HAL_RCC_OscConfig函数真正完成HSE、HSI、LSI 和 PLL3的配置。" Q" C: _# p/ b3 G6 l& m5 \
第65到第86行,给RCC_ClkInitStruct结构体成员赋值,完成AXI、MCU和APB的时钟分配。其中:
2 w) y2 c. E/ j" B5 @AXI时钟源来自HSI,经过1分频后最终频率为64MHz;
, ]0 v7 c- y' B W* d0 S2 AMCU时钟源来自PLL3,经过1分频后最终频率为209MHz;
0 R* J. [+ Q! A$ Q9 d, u确定了AXI的时钟,那么APB4和APB5的时钟源也就确定了。APB4和APB5经过1分频后最终频率为64MHz;: D: e& h: m! ~* G5 A
确定了MCU的时钟,那么APB1APB5的时钟源也就确定了。APB1APB2经过1分频后最终时钟频率为104.5MHz。
F1 ^. l7 L9 y _+ e: T) J' B可能大家会问,为啥没看到AHB1AHB6相关的代码呢?这个系统就默认配置好了,因为他们不需要经过分频器。AHB1AHB4的时钟直接通过mcuss_ck获得,为64MHz。AHB5~AHB6的时钟直接通过axiss_ck获得,为209MHz。
* _. o. D3 h" C1 Q9.5.3 main函数调用
" \) e% R. @3 i* d X% e在main函数中调用系统时钟配置函数的代码:5 C* y+ b" R5 z
0 Y0 Y6 A' G0 v3 e# D6 ?( K- if(IS_ENGINEERING_BOOT_MODE()) e; Q8 ?/ H% x* W4 A
- {
+ [4 Q# A. I0 \ - SystemClock_Config();& s! h/ T9 O. `4 c5 ?
- }
复制代码 ( l: E0 ^, r( L! }
其中,先判断IS_ENGINEERING_BOOT_MODE()函数的返回值,如果为TRUE 则初始化系统时钟,如果为FALSE,条件不成立,if语句不执行。我们来看看IS_ENGINEERING_BOOT_MODE函数:
3 F2 H) G, J5 g: L# `6 Y+ h M2 F% Q ]0 C% D" ^% g
- stm32mp157dxx_cm4.h文件代码
9 f' v9 l& Z* q8 j L7 _7 ] - #define IS_ENGINEERING_BOOT_MODE() (((SYSCFG->BOOTR) & \ (SYSCFG_BOOTR_BOOT2|SYSCFG_BOOTR_BOOT1|SYSCFG_BOOTR_BOOT0)) == \ (SYSCFG_BOOTR_BOOT2))
复制代码 函数IS_ENGINEERING_BOOT_MODE通过判断引脚BOOT2 是否为1,为1的话表示从MCU启动(ST官方叫做工程启动模式),然后调用IS_ENGINEERING_BOOT_MODE函数来初始化系统时钟。9 P2 U+ X8 O: _* l7 X% B/ i
大家可以分析一下第二个工程MCU_HSI的系统时钟代码,对比看看它们的区别。4 _4 k2 i) m5 u% j$ U& g7 Y9 d
关于外设时钟初始化部分,下面的章节小结有总,我们也会在对应的外设实验章节进行讲解。
+ N& u. l+ b( k2 B, P/ a( |7 B% g- z6 w, D9 E
9.5.4 HAL_Delay的计时' b' \+ c+ N- U6 S( y
关于HAL_Delay是怎么实现计时的,我们在前面第7.4.2小节有分析过,关于Systick我们后面会有专门的实验章节做讲解。默认情况下,STM32CubeMX使用Systick作为时基给其它程序提供计时,例如HAL_Delay延时函数,以及串口程序中的Timeout 超时机制等等。/ h$ |1 T' F' x. v
, W0 z3 {8 L9 ?# J2 O; `
4 \ j' |; z* E- i# t" m
6 v' a% B4 o5 B, i' m! a. X) E图9.5.2. 1 STM32CubeMX插件默认配置) t. @6 L8 e- |% t& c
————————————————; D- ?: R! H/ {( |
版权声明:正点原子' J8 o7 \/ M7 u. e! p: R" P
. F0 a( Q9 S' F0 }, q7 ~; @/ v8 [4 X6 y i" Z
|