你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】STM32L4时钟系统(转载)

[复制链接]
STMCU小助手 发布时间:2022-1-2 09:00

1. STM32L4 时钟树概述

时钟系统是 CPU 的脉搏,就像人的心跳一样。

STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率。比如看门狗以及 RTC 只需要几十k 的时钟即可。

同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。

首先让我们来看看 STM32L4 的时钟系统图:

在 STM32L475 中,有 6 个重要的时钟源,为 HSI、 HSE、 LSI、 LSE、 MSI、 PLL。

其中PLL 实际是分为三个时钟源,分别为主 PLL 和、PLLISAI1 和 PLLSAI2。

从时钟频率来分可以分为高速时钟源和低速时钟源;

在这 6个中 :

HSI, HSE, MSI 以及 PLL 是高速时钟,

LSI 和 LSE 是低速时钟。

从来源可分为外部时钟源和内部时钟源,

外部时钟源就是从外部通过接晶振的方式获取时钟源,

其中HSE 和 LSE 是外部时钟源,

其他的是内部时钟源。

以我目前的方案来看,OSC_32KHz_IN外接32.768KHZ晶振,高速时钟源采用内部时钟源;

讲解顺序是按图中红圈标示的顺序:

① LSI 是低速内部时钟, RC 振荡器,频率为 32kHz 左右。供独立看门狗、 RTC 和 LCD

使用。

② LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。

③ HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz-48MHz。我们的开发板接的是 8MHz 的晶振。 HSE 也可以直接做为系统时钟或者 PLL 输入。

④ HSI 是高速内部时钟, RC 振荡器, 频率为 16MHz。可以直接作为系统时钟或者用作PLL 输入。

⑤ MSI 时钟信号由内部 RC 振荡器产生。其频率范围可通过时钟控制寄存器(RCC_CR)中的 MSIRANGE[3:0]位进行调整。

⑥ PLL 为锁相环倍频输出。 STM32L4 有三个 PLL:

1) 主 PLL(PLL)可由 HSE、HSI 或者 MSI 提供时钟信号,并具有三个不同的输出时钟。

第一个输出 PLLR, 用于生成高速的系统时钟(SYSTEM,最高 80MHz)

第二个输出 PLLQ,可为 USB、 RNG 和 SDMMC 提供时钟源

第三个输出 PLLP,可用于 SAI1 和 SAI2 时钟

2) PLLSAI1 用于生成精确时钟,同样具有三个不同的输出时钟。

第一个输出 PLLSAI1P,可用于 SAI1 和 SAI2 时钟

第二个输出 PLLSAI1Q, 可为 USB、 RNG 和 SDMMC 提供时钟源。

第三个输出 PLLSAI1R,可为 ADC 提供时钟

3) PLLSAI2 用于生成精确时钟,具有两个不同的输出时钟。

第一个输出 PLLSAI2P,可用于 SAI1 和 SAI2 时钟

第二个输出 PLLSAI2R,可为 ADC 提供时钟

这里我们重点看看主 PLL 时钟第一个高速时钟输出 PLLR 的计算方法。下图为主PLL的时钟图。

从图中可以看出:

主 PLL 的时钟源要先经过一个分频系数为 M 的分频器,然后经过倍频系数为 N 的倍频器,出来之后还需要经过分频系数为 R(输出 PLLR 时钟)、或者 P(输出 PLLP时钟)、或者 Q(输出 PLLQ 时钟),最后才生成最终的主 PLL 时钟。

我目前方案上的系统时钟设置如下:

  1. /* Activate PLL with HSI as source to obtain 80 MHz fHCLK */% i$ d) P' u) H2 X5 z; W  L
  2.   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;4 V: S7 _( Z9 ^# y' t( r& n
  3.   RCC_OscInitStruct.HSIState = RCC_HSI_ON;
    / D. Z# }* u9 Z( ~- |- r* z
  4.   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    . @3 Y. k  x  w+ c5 g' Q/ j. V
  5.   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;" `+ ^: W. h4 P! I" o
  6.   RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
    , [5 q9 Y6 k$ n2 l& c2 n
  7.   RCC_OscInitStruct.PLL.PLLM = 2;# d. C- b1 s$ l! F/ t" Y% N
  8.   RCC_OscInitStruct.PLL.PLLN = 20;3 ]6 V8 m  ]4 Y! S% f* n  X
  9.   RCC_OscInitStruct.PLL.PLLP = 7;" S4 R. ~2 d+ @6 G2 b
  10.   RCC_OscInitStruct.PLL.PLLQ = 4;& H2 n! c7 i/ o$ k: J# p! f  {: l# d
  11.   RCC_OscInitStruct.PLL.PLLR = 2;
    4 E/ F  y6 F# ^2 P# [" H- @3 U4 x, g
  12.   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
复制代码

从代码看,我们的PLL时钟源为HSI(16M),同时我们设置相应的分频器 M=2,倍频器倍频系数N=20,分频器分频系数 R=2,那么主 PLL 生成的第一个输出高速时钟 PLLR 为:

  1. PLLR=16MHz*N/(M*R)=16MHz*20/(2*2)=80MHz
复制代码
" ?, ]5 p8 a- N+ P1 g0 c# ^

查看上述的时钟树,PLLR输出到SYSCLK,因此SYSCLK也为80MHZ;

图 中,我们挑选出 9 个重要的地方进行介绍(图中标出的 A~I)。

A. 这里是看门狗时钟。从图中可以看出看门狗时钟源只能是低速的 LSI 时钟。

B. 这里是 RTC 与 LCD 时钟源,从图中可以 RTC 与 LCD 时钟源可以选择 LSI、 LSE 以及 HSE 分频后的时钟, HSE 分频系数为 2~31。

C. 这里是 STM32L475 输出时钟 MCO。 MCO 是向芯片 PA8 引脚输出时钟。 它有七个时钟来源分别为: LSE、 LSI、 HSE、 SYSCLK、 MSI、 HSI 和 PLL 时钟。

D. 这里是系统时钟。从图中可以看出, SYSCLK 系统时钟来源有四个方面: HSI、HSE、 MSI 和 PLL。

E. 这里指的是 PWR 时钟、 AHB 时钟、 APB1 时钟和 APB2 时钟。 这些时钟都是来源于SYSCLK 系统时钟。其中 AHB、APB1 和 APB2 时钟都是经过 SYSCLK 时钟分频得来,并且这三个时钟最大频率为 80MHz。

F. 这里是 48MHz 时钟,主要用于 USB、 RNG、 SDMMC 时钟。这里的时钟源来自三个方面: MSI、 PLLQ 和 PLLSAI1Q。

G. 这里是 ADC 的时钟,这里的时钟源来自三个方面:SYSCLK、PLLSAI1R 和 PLLSAI2R。

H. 这里是 SAI1 的时钟,这里的时钟源来自四个方面: PLLP、 PLLSAI1P、 PLLSAI2P 和SAI1_EXTCLK。

I. 这里是 SAI2 的时钟,这里的时钟源来自四个方面: PLLP、 PLLSAI2P、 PLLSAI2P 和SAI2_EXTCLK。

这里还需要说明一下,Cortex 系统定时器 Systick 的时钟源可以是 AHB 时钟 HCLK 或 HCLK的 8 分频。

在以上的时钟输出中,有很多是带使能控制的,例如 AHB 总线时钟、内核时钟、各种APB1 外设、 APB2 外设等等。当需要使用某模块时,一定要先使能对应的时钟。

2. STM32L4 时钟系统配置

上面对 STM32L4 时钟树进行了详细讲解,接下来来讲解通过 STM32L4 的HAL 库进行 STM32L4 时钟系统配置步骤。

实际上, ①STM32L4 的时钟系统配置也可以通过图形化配置工具 STM32CubeMX 来配置生成,②这里讲解初始化代码,是为了让大家STM32L4时钟系统有更加清晰的理解。

前面我们讲解过,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系统一些初始化配置。那么我们先来看看 SystemInit 程序:

  1. void SystemInit (void)- I' d. Z& d$ _9 a7 T
  2. {, S' B7 K7 e4 |0 O9 P1 y% M- _
  3. //如果需要 FPU 的话就使能 FPU,设置 CP10 和 CP11 为全访问
    % I- `- _0 A5 e/ A- R3 q
  4. #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)+ B5 o* m2 w9 D: t0 a
  5. SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));- z3 o! @+ X. f% M/ a7 u) d' x
  6. #endif# H6 ], p; K* B' x& Y
  7. /* 复位 RCC 时钟配置为默认配置-----------*/: V0 |. ?# o9 T' R( q2 f! M' n
  8. RCC->CR |= RCC_CR_MSION; //打开 MSION 位' W; a. ^2 h: i, J' a) s, n
  9. RCC->CFGR = 0x00000000; //复位 CFGR 寄存器
    1 m6 M6 u: J  W- u; b% W7 I, T
  10. RCC->CR &= (uint32_t)0xEAF6FFFF;//清除 HSEON,CSSON,HSION ,PLLON 位' S. `  Y5 j/ m5 b4 N/ v
  11. RCC->PLLCFGR = 0x00001000; //复位 PLLCFGR 寄存器; Y2 r8 c6 s8 L6 L  G/ {' J
  12. RCC->CR &= (uint32_t)0xFFFBFFFF;//复位 HSEBYP 位( y0 w  V3 i7 j8 W
  13. RCC->CIER = 0x00000000; //关闭所有的中断3 A* d0 \+ v) \' d
  14. /* 配置中断向量表地址=基地址+偏移地址 ------------------*/  Z+ t  B" u  p: z0 Z
  15. #ifdef VECT_TAB_SRAM' n  U# n& w' u4 M1 H8 k6 B+ T2 p' c
  16. SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
    * O8 s+ [% s8 m+ e0 a
  17. #else
    8 c- W8 n4 O( c4 ?& R+ z. e
  18. SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;1 o' D/ R7 Q- Q% k6 i  {$ s
  19. #endif/ w, X5 N1 \9 z2 e
  20. }
复制代码
$ c4 N: Q3 G. {; \0 L& n. _

从上面代码可以看出, SystemInit 主要做了如下三个方面工作:

1) FPU 设置

2) 复位 RCC 时钟配置为默认复位值(默认开启 MSI)

3) 中断向量表地址配置

HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。

HAL 库的 SystemInit 函数除了打开 MSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们必须编写自己的时钟配置函数。

时钟初始化函数 StartUpClock_Config 的内容:

  1. void StartUpClock_Config(void) {: `' I# F2 H! [' \
  2.   RCC_ClkInitTypeDef RCC_ClkInitStruct;
    ) g- R4 x4 E) f- q8 ~6 p
  3.   RCC_OscInitTypeDef RCC_OscInitStruct;" X+ g( A7 k- A9 \* I% w

  4. % Q% b- m( x8 |2 g: ~
  5.   /* Activate PLL with HSI as source to obtain 80 MHz fHCLK */
    # U' [1 w* W; Z: E5 U( W3 I+ o
  6.   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;9 L/ f8 e0 f7 g, O" Q9 C
  7.   RCC_OscInitStruct.HSIState = RCC_HSI_ON;' v* [* \$ S3 l3 ^" K, N  k# G
  8.   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    ' i0 _6 \7 [: O3 F
  9.   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
    8 l. b7 F2 e# K/ N9 u/ |' e4 @
  10.   RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;0 g' E$ g& H4 K& _& B
  11.   RCC_OscInitStruct.PLL.PLLM = 2;
    0 a9 @" v1 I' q, ^
  12.   RCC_OscInitStruct.PLL.PLLN = 20;$ V6 N9 r1 p! N; T
  13.   RCC_OscInitStruct.PLL.PLLP = 7;, R1 ~9 y$ g" T/ d
  14.   RCC_OscInitStruct.PLL.PLLQ = 4;
    % c- A2 A$ z$ r
  15.   RCC_OscInitStruct.PLL.PLLR = 2;
    8 H  }4 Y( Z% z% d7 J* O# W, ]
  16.   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
    4 e+ h# V0 H3 ]
  17. #ifdef STL_VERBOSE_POR
    2 j4 g4 F, k6 V; h
  18.     printf("PLL clock config failure\n\r");
    / Q! ?1 Y/ V7 i: w6 A5 g
  19. #endif /* STL_VERBOSE_POR */2 A# z0 h8 L( e2 `& x7 _8 ~
  20.     FailSafePOR();
    % }, o- o9 U6 {( B0 o
  21.   }- v' f' {5 j6 ?5 @+ U7 t
  22.   /* Select PLL as system clock source and configure the HCLK, PCLK1 and PCLK2
      r  J: w$ O$ S! n+ b) |- j
  23.    * clocks dividers */
    ; ^- v0 b7 ^$ y4 U* j
  24.   RCC_ClkInitStruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
    $ T! _- j  c4 T  n  _6 ^/ j
  25.                                  RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);' W; Y' q- ]: B' |3 g
  26.   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    ) Z7 P  @) {8 U/ M: D
  27.   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;' w, ^5 b2 `* }( X( z% [
  28.   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;: g& k7 d# E( x' Q2 H6 k3 A
  29.   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;1 ]. B) K& {/ u5 o1 ?/ i  i6 L
  30.   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) {
    * p5 s6 p2 ^8 w
  31. #ifdef STL_VERBOSE_POR
    , s3 H5 G1 Z& u+ W0 R' M
  32.     printf("PLL clock switch failure\n\r");1 \9 R0 S- F8 [! m
  33. #endif /* STL_VERBOSE_POR *// }) f' A: J& j) ]& D
  34.     FailSafePOR();
    " b! }' N' N& i& {
  35.   }
    . W- N8 o+ q9 G0 m( e; i
  36. }
复制代码

5 b* `3 Z# x# H7 w/ p

从函数注释可知,函数 StartUpClock_Config 的作用是进行时钟系统配置,除了配置 PLL 相关参数确定 SYSCLK 值之外,还配置了 AHB,APB1 和 APB2 的分频系数,也就是确定了 HCLK,PCLK1 和 PCLK2 时钟值。

步骤 1 后面在讲解,先来看一下步骤 2 和步骤 3

对于步骤 2,使用 HAL 来配置时钟源相关参数,我们调用的函数为HAL_RCC_OscConfig(),该函数在 HAL 库关键头文件stm32l4xx_hal_rcc.h 中声明,在文件 stm32l4xx_hal_rcc.c 中定义。首先我们来看看该函数声明:

  1. HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct)
复制代码
( u- q; s% z4 B' z- x1 ~  m7 j7 Z

该函数只有一个入口参数,就是结构体 RCC_OscInitTypeDef 类型指针。接下来我们看看结构体 RCC_OscInitTypeDef 的定义:

  1. typedef struct
    ' P( i4 A, {+ `- `. }# Q5 @
  2. {
    - Y1 v7 J# N  W) o2 {: u
  3. uint32_t OscillatorType; //需要选择配置的振荡器类型
    8 ?$ W, g. u) G- I# G3 R
  4. uint32_t HSEState; //HSE 状态
    * |' [3 [; L( R4 z3 o" n# v* j- h
  5. uint32_t LSEState; //LSE 状态  [. r* l  I' e
  6. uint32_t HSIState; //HIS 状态
    7 Y+ ~+ K) q- ~. m; a; m) F
  7. uint32_t HSICalibrationValue; //HIS 校准值
    , X- T2 I, S5 {8 \" i! q
  8. uint32_t LSIState; //LSI 状态
    % j: h- Q: E' V
  9. uint32_t MSIState //MSI 的状态
    6 R% Y0 J( f, _+ x  v
  10. uint32_t MSICalibrationValue; //MSI 校准值
    ; q0 c$ G* ]7 b8 g
  11. uint32_t MSIClockRange; //MSI 时钟范围
    " D0 n3 ~8 {% n8 {/ C" t; b
  12. uint32_t HSI48State; //HSI48 状态
    0 a7 {" b- x+ k" P: H- T2 E- O
  13. RCC_PLLInitTypeDef PLL; //PLL 配置
    ; X6 |( i3 ]7 n' e* N, e+ U3 A
  14. }RCC_OscInitTypeDef;
复制代码

对于这个结构体,前面几个参数主要是用来选择配置的振荡器类型。比如我们要开启 HSE,那么我们会设置 OscillatorType 的值为 RCC_OSCILLATORTYPE_HSE,然后设置 HSEState 的值为 RCC_HSE_ON 开启 HSE。对于其他时钟源 HSI,LSI 和 LSE,配置方法类似。这个结构体还有一个很重要的成员变量是 PLL,它是结构体 RCC_PLLInitTypeDef 类型。它的作用是配置 PLL相关参数,我们来看看它的定义:

  1. typedef struct
    * o3 C. N  @; L8 h% }
  2. {5 L5 S1 w8 J: \" |2 e$ A+ s
  3. uint32_t PLLState; //PLL 状态5 F) n: \* q% V1 o* r! @
  4. uint32_t PLLSource; //PLL 时钟源+ _6 ?/ _0 r) y# b$ ]9 q% T" h( K
  5. uint32_t PLLM; //PLL 分频系数 M. w2 E) q. {4 f) N$ o: a
  6. uint32_t PLLN; //PLL 倍频系数 N  [) @3 F- w8 m7 A2 [2 _) S* d
  7. uint32_t PLLP; //PLL 分频系数 P0 s% d* P: Y/ ?
  8. uint32_t PLLQ; //PLL 分频系数 Q
    6 s5 Y( Q6 c  l* y8 x# P
  9. uint32_t PLLR; //PLL 分频系数 R
    9 f, r" r- F5 x6 ]) N% d9 E4 W
  10. }RCC_PLLInitTypeDef;V
复制代码

从 RCC_PLLInitTypeDef;结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及相关分频倍频参数。接下来我们看看我们的时钟初始化函数SystemClock_Config 中的配置内容:

  1. RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE) N3 Y+ Y& z% r
  2. RCC_OscInitStruct.HSEState = RCC_HSE_ON; //打开 HSE( }! M" I! m: _: ?# d
  3. RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; //打开 PLL" r( i/ O1 ^3 O( _6 F+ g( s; R( }
  4. RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; //PLL 时钟源为 HSE: L9 g+ e. h' K# V* o" K( J
  5. RCC_OscInitStruct.PLL.PLLM = 1;
    ( D" p1 H% N, E" W
  6. RCC_OscInitStruct.PLL.PLLN = 20;& f! c3 j3 G; J' Y5 H) @
  7. RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;  c* v* i. w/ q* |; U1 Y* a
  8. RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
    2 R/ I1 J" l0 _' F
  9. RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;5 x. j0 F" T" D! O
  10. ret = HAL_RCC_OscConfig(&RCC_OscInitStruct);
复制代码

通过该段函数,我们开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE, 同时也配置了 PLL的参数 M,N,M,P 和 Q 的值,这样就达到了设置 PLL 时钟源相关参数的目的。设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率。

接下来我们就需要设置系统时钟,以及SYSCLK、AHB, APB1 和 APB2 相关参数,也就是我们前面提到的步骤 3接下来我们来看看步骤 3 中提到的 HAL_RCC_ClockConfig()函数,声明如下:

  1. HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,
    7 _8 l0 ]3 ?+ K4 O  S
  2. uint32_t FLatency);
复制代码

该函数有两个入口参数,第一个入口参数 RCC_ClkInitStruct 是结构体 RCC_ClkInitTypeDef指针类型,用来设置 SYSCLK 时钟源以及 SYSCLK、 AHB, APB1 和 APB2 的分频系数。第二个入口参数 FLatency 用来设置 FLASH 延迟,这个参数我们放在后面跟步骤 4 一起讲解。

RCC_ClkInitTypeDef 结构体类型定义非常简单,就不列出来了,

我们来看看SystemClock_Config 函数中的配置内容:

  1. *选中 PLL 作为系统时钟源并且配置 SYSCLK、 PCLK1、 PCLK2*/
    " R: ~5 [, Y" W" v( ]4 R
  2. RCC_ClkInitStruct.ClockType=RCC_CLOCKTYPE_HCLK |( c% Q0 ?9 C( O4 E; Z" B# I
  3. RCC_CLOCKTYPE_SYSCLK |
    " a' {5 g$ M6 Z4 `& O7 @
  4. RCC_CLOCKTYPE_PCLK1 |" n; S6 o# O9 S- X8 Y. Z4 n5 }
  5. RCC_CLOCKTYPE_PCLK2;# b0 V* G2 D+ U7 Q, w
  6. RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;  V! c3 B1 ^9 A
  7. RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    2 Z4 F" ~; g( n! h' H/ _
  8. RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;6 U( i: r0 u. h% r0 S
  9. RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    1 z3 p/ N( o3 @! h: x- l
  10. ret = HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4);
复制代码

" l# C8 d8 S. }% O5 S; @/ M

第一个参数 ClockType 配置说明我们要配置的是 SYSCLK, HCLK、 PCLK2、 PCLK1 四个时钟。

第二个参数 SYSCLKSource 配置选择系统时钟源为 PLL。

第三个参数 AHBCLKDivider 配置 AHB 分频系数为 1。

第四个参数 APB1CLKDivider 配置 APB1 分频系数为 1。

第五个参数 APB2CLKDivider 配置 APB2 分频系数为 1。

根据函数初始化参数值,我们可以计算出, PLL 时钟为 PLLCLK=HSE*N/(M*R)=8MHz*20/(1*2)=80MHz,同时我们选择系统时钟源为 PLL,所以系统时钟SYSCLK=80MHz。

AHB 分频系数为 1,故其频率为 HCLK=SYSCLK/1=80MHz。

APB1 分频系数为 1,故其频率为PCLK1=HCLK/1=80MHz。

APB2 分频系数为 1,故其频率为 PCLK2=HCLK/1=80MHz。

最后我们总结一下通过调用函数 SystemClock_Config 之后的关键时钟频率值:

  1. SYSCLK(系统时钟) =80MHz3 ?1 b1 ?* V2 }4 \; W
  2. PLL 主时钟 =80MHz) @2 }# g+ o+ i) t$ w
  3. AHB 总线时钟(HCLK=SYSCLK/1) =80MHz2 G: f# }* l' O2 x/ p
  4. APB1 总线时钟(PCLK1=HCLK/1) =80MHz) a; t; s2 X: D  X+ p
  5. APB2 总线时钟(PCLK2=HCLK/1) =80MHz
复制代码
8 ?" Q  ^" |. T8 S

我们来看看步骤 1 和步骤 4 中函数 HAL_RCC_ClockConfig 第二个入口参数 FLatency的含义。这里我们不想讲解得太复杂,大家只需要知道调压器输出电压级别 VOS 和 FLASH 的延迟 Latency 两个参数,在我们芯片电源电压和 HCLK 固定之后,他们两个参数也是固定的。

首先我们来看看调压器输出电压级别 VOS,它是由 PWR 控制寄存器 CR1 的位 10:9 来确定的

  1. 位 15:14 VOS[1:0]
    ' B2 C2 k8 T. w
  2. 00: Cannot be written
    6 j' k* G% I5 [. F* G* B
  3. 01:Range 1
      q! C( F1 G6 W, |2 k
  4. 10:Range 2
    * z) s( h: j5 O7 Y
  5. 11: Cannot be written
复制代码

4 G0 N/ j. N! p1 B- p$ \

级别数值越小工作频率越高, 所以如果我们要配置 L4 的主频为 80MHz, 那么我们必须配置调压器输出电压级别 VOS 为级别 1。所以函数 SystemClock_Config 中步骤 4 源码如下:

  1. //步骤 1, 设置调压器输出电压级别 1( v* k, K4 c/ N* E6 }- @
  2. HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1);
复制代码

配置好调压器输出电压级别 VOS 之后,如果需要 L4 主频要达到 80MHz,还需要配置FLASH 延迟 Latency。对于 STM32L4 系列, FLASH 延迟配置参数值是通过下表来确定的:

从上表可以看出,在 Vcore Range 1 时如果 HCLK 为 80Mhz,那么等待周期要 4WS 也就是 5 个 CPU 周期。下面我们看看我们在 SystemClock_Config 中调用函数 HAL_RCC_ClockConfig的时候,第二个入口参数设置值:

ret=HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4);

从上可以看出,我们设置值为 FLASH_LATENCY_4,也就是 4WS, 5 个 CPU 周期。

9 X4 n2 L" o7 p4 P

3. STM32L4 时钟使能和配置

上面讲解了时钟系统配置步骤。

在配置好时钟系统之后,如果我们要使用某些外设,例如 GPIO, ADC 等,我们还要使能这些外设时钟。

如果在使用外设之前没有使能外设时钟,这个外设是不可能正常运行的。

STM32 的外设时钟使能是在 RCC 相关寄存器中配置的。 RCC 相关寄存器非常多,可以通过《STM32L4XX 编程参考手册》 6.4 小节查看所有 RCC 相关寄存器的配置。

接下来讲解通过 STM32L4 的HAL 库使能外设时钟的方法。

在 STM32L4 的 HAL 库中,外设时钟使能操作都是在 RCC 相关固件库文件头文件stm32l4xx_hal_rcc.h 定义的。打开 stm32l4xx_hal_rcc.h 头文件可以看到文件中除了少数几个函数声明之外大部分都是宏定义标识符。外设时钟使能在 HAL 库中都是通过宏定义标识符来实现的。首先,来看看 GPIOA 的外设时钟使能宏定义标识符:

  1. #define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
    8 X4 {  @/ k- J
  2. __IO uint32_t tmpreg; \" A, v# v; j1 T4 [4 x" u4 l7 \
  3. SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN); \1 d) X$ q) }9 M1 Q5 i2 r8 e( E& N" y6 `
  4. tmpreg = READ_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN); \
      W0 @1 q% F- i0 r/ ?
  5. UNUSED(tmpreg); \2 u! Z6 c# V4 _5 [# F
  6. } while(0)
复制代码

这 几 行 代 码 非 常 简 单 , 主 要 是 定 义 了 一 个 宏 定 义 标 识 符__HAL_RCC_GPIOA_CLK_ENABLE(),它的核心操作是通过下面这行代码实现的:

  1. SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);
复制代码
7 D% F* i. V! V1 d6 F: Y

这行代码的作用是,设置寄存器 RCC->AHB2ENR 的相关位为 1,至于是哪个位,是由宏定义标识符 RCC_AHB2ENR_GPIOAEN 的值决定的,而它的值为:

  1. #define RCC_AHB2ENR_GPIOAEN_Pos (0U). n- J1 A4 y0 U9 Y4 Z+ o( m" V
  2. #define RCC_AHB2ENR_GPIOAEN_Msk (0x1UL << RCC_AHB2ENR_GPIOAEN_Pos)% \- W+ I0 N- ]8 X4 ~2 A" i7 y
  3. #define RCC_AHB2ENR_GPIOAEN RCC_AHB2ENR_GPIOAEN_Msk
复制代码

上面三行代码很容易计算出来 RCC_AHB2ENR_GPIOAEN= 0x00000001, 因此上面代码的作用是设置寄存器 RCC->AHB2ENR 寄存器的最低位为 1。我们可以从 STM32L4 的参考手册中搜索 AHB2ENR 寄存器定义,最低位的作用是用来使用 GPIOA 时钟。

AHB2ENR(不是AHB2ENR_GPIOAEN的位0) 寄存器的位 0描述为:

  1. Bit 0 GPIOAEN: IO port A clock enable //GPIOA 时钟使能
    & b  u. c" c; h9 p  N  s
  2. Set and cleared by software. //由软件置 1 和清零/ ~0 P: N3 S; L9 U  E
  3. 0: IO port A clock disabled //禁止 GPIOA 时钟0 x& B, m5 O( C1 ]$ F0 q- B6 k
  4. 1: IO port A clock enabled //使能 GPIOA 时钟
复制代码

那 么 我 们 只 需 要 在 我 们 的 用 户 程 序 中 调 用 宏 定 义 标 识 符__HAL_RCC_GPIOA_CLK_ENABLE()就可以实现 GPIOA 时钟使能。使用方法为:

  1. __HAL_RCC_GPIOA_CLK_ENABLE();<i>//使能 GPIOA 时钟</i>
复制代码
% ~* R1 ~: i7 a' P0 `

对于其他外设,同样都是在 stm32l4xx_hal_rcc.h 头文件中定义,大家只需要找到相关宏定义标识符即可,这里列出几个常用使能外设时钟的宏定义标识符使用方法:

  1. __HAL_RCC_DMA1_CLK_ENABLE();//使能 DMA1 时钟
      v, n+ ?; T" f6 c. C4 D  j: T, F9 L
  2. __HAL_RCC_USART2_CLK_ENABLE(); //使能串口 2 时钟. v% ~9 y) [2 l4 U* @$ T- a4 O" r
  3. __HAL_RCC_TIM1_CLK_ENABLE(); //使能 TIM1 时钟
复制代码

我们使用外设的时候需要使能外设时钟,如果我们不需要使用某个外设,同样我们可以禁止某个外设时钟。禁止外设时钟使用方法和使能外设时钟非常类似,同样是头文件中定义的宏定义标识符。我们同样以 GPIOA 为例,宏定义标识符为:

  1. #define __HAL_RCC_GPIOA_CLK_DISABLE()( K5 d6 w& z& V; [
  2. CLEAR_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN)
复制代码

同 样 , 宏 定 义 标 识 符 __HAL_RCC_GPIOA_CLK_DISABLE() 的 作 用 是 设 置RCC->AHB2ENR 寄存器的最低位为 0,也就是禁止 GPIOA 时钟。

我们列出几个常用的禁止外设时钟的宏定义标识符使用方法:

  1. __HAL_RCC_DMA1_CLK_DISABLE(); //禁止 DMA1 时钟. P2 D  }/ t0 H" J
  2. __HAL_RCC_USART2_CLK_DISABLE();//禁止串口 2 时钟
    ) n8 j4 e+ a- g6 p+ _- [) l  K
  3. __HAL_RCC_TIM1_CLK_DISABLE(); //禁止 TIM1 时钟
复制代码
3 ], }7 f* Y' n/ u. @
  |. n) |5 T" k! q5 U% p% h( \7 y
收藏 评论0 发布时间:2022-1-2 09:00

举报

0个回答
关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版