【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

1)实验平台:正点原子MiniPro H750开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-336836-1-1.html
4)对正点原子STM32感兴趣的同学可以加群讨论:879133275

第十一章 STM32时钟系统

STM32H7时钟系统的知识在《STM32H7xx参考手册_V7(英文版).pdf》第八章复位和时钟控制章节有较详细的讲解。这里我们对STM32H7的整体架构作一个简单的介绍,帮助大家更全面、系统地认识STM32H7系统的主控结构。了解时钟系统在整个STM32系统的贯穿和驱动作用,学会设置STM32的系统时钟。
本章将分为如下几个小节:
11.1 认识时钟树
11.2 如何修改主频

11.1 认识时钟树

数字电路的知识告诉我们:任意复杂的电路控制系统都可以经由门电路组成的组合电路实现。回顾第五章的知识点,我们知道STM32内部也是由多种多样的电路模块组合在一起实现的。当一个电路越复杂,在达到正确的输出结果前,它可能因为延时会有一些短暂的中间状态,而这些中间状态有时会导致输出结果会有一个短暂的错误,这叫做电路中的“毛刺现象”,如果电路需要运行得足够快,那么这些错误状态会被其它电路作为输入采样,最终形成一系列的系统错误。为了解决这个问题,在单片机系统中,设计时以时序电路控制替代纯粹的组合电路,在每一级输出结果前对各个信号进行采样,从而使得电路中某些信号即使出现延时也可以保证各个信号的同步,可以避免电路中发生的“毛刺现象”,达到精确控制输出的效果。
由于时序电路的重要性,因此在MCU设计时就设计了专门用于控制时序的电路,在芯片设计中称为时钟树设计。由此设计出来的时钟,可以精确控制我们的单片机系统,这也是我们这节要展开分析的时钟分析。为什么是时钟树而不是时钟呢?一个MCU越复杂,时钟系统也会相应地变得复杂,如STM32H7的时钟系统比较复杂,不像简单的51单片机一个系统时钟就可以解决一切。对于STM32H7系列的芯片,正常工作的主频可以达到480MHz,但并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及RTC只需要几十KHz的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的MCU一般都是采取多时钟源的方法来解决这些问题。
STM32内部非常复杂,外设也非常多,为了更低的功耗,STM32默认不开启这些外设功能。用户可以根据自己的需要决定STM32芯片要使用哪些外设,从而再开启该外设的时钟,否则该外设不工作。
下面来看一下STM32H7时钟系统图。STM32H7时钟系统图看起来有点多且复杂,但是分开几部分各个理解其实没有那么难了。
【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1_第1张图片

图11.1.1 STM32H7时钟系统图
11.1.1 时钟源
对于STM32H7有六个时钟源可供使用,可分为外部时钟源和内部时钟源。具体如图11.1.1所示,在STM32H7的时钟系统图里,我们把这六个时钟源标记成橘黄色。
(1)2个外部时钟源:
高速外部振荡器 (HSE)
支持 4 MHz 到 48 MHz 频率范围内的晶振。
低速外部振荡器 (LSE)
一般是32.768 kHz 晶振,可用于看门狗和RTC实时时钟。
2个外部时钟源都是芯片外部晶振产生的时钟频率,故而都有精度高的优点。
(2)4个内部时钟源:
高速内部振荡器 (HSI)
频率 64MHz 的高速RC振荡器,可用于系统时钟。
48 MHz RC振荡器 (HSI48)
频率48MHz,用于给特定的外设提供时钟,比如配合CRS可以作为USB的时钟源。
低功耗内部振荡器 (CSI)
频率约是 4MHz,主要用于低功耗。
低速内部振荡器 (LSI)
频率约32 kHz的低速RC 振荡器,可用于RTC实时时钟、独立看门狗和自动唤醒。
11.1.2 锁相环PLL
STM32H7时钟系统图中看到红色框的部分,可以知道STM32H7有三个锁相环,分别是:PLL1、PLL2、PLL3。而且它们的时钟源只有一个,一起共用,但是进入到锁相环的时钟源分别经过三个不同的预分频器,这样我们就可以让锁相环有不同的输入时钟频率。
下面看到STM32H7时钟系统图标号①②③这三个组成部分:
①锁相环时钟源选择器
锁相环时钟源有三个:his_ck、csi_ck、hse_ck。我们一般选择hse_ck,即来自外部高速晶振,正点原子MiniPRO STM32H750外部高速晶振为8MHZ。
②PLL1\PLL2\PLL3时钟源预分频器
DIVM1、DIVM2和DIVM3分别是PLL1、PLL2和PLL3输入时钟的预分频系数,取值范围都是:1~63,请根据实际需要设置。
③PLL1\PLL2\PLL3锁相环
PLL1、PLL2和PLL3锁相环都是一个输入,三个输出。his_ck、csi_ck、hse_ck三个时钟源中的一个经过DIVM预分频器处理作为锁相环的输入时钟源。PLL1三个输出分别是:pll1_p_ck、pll1_q_ck和pll1_r_ck,PLL2和PLL3同理。
下面以PLL1为例介绍一下锁相环内部框图,PLL2和PLL3同理。
DIVN1是PLL1中VCO的倍频系数,其取值范围是:4~512。
DIVP1是pll1_p_ck的预分频系数,取值范围是:2、4、6…128(必须是偶数)。
DIVQ1是pll1_q_ck的预分频系数,取值范围是:1~128。
DIVR1是pll1_r_ck的预分频系数,取值范围是:1~128。
FRACN1是PLL1中VCO的倍频系数的小数部分,它和DIVN1一起组成PLL1的倍频系数,但是我们一般情况下都是不需要用到小数倍频的,所以小数部分我们不讨论。
这里以pll1_p_ck为例,简单介绍下PLL输出频率的计算公式(时钟PLL输入频率为hse_ck):

假设外部晶振为8Mhz,需要得到480Mhz的pll1_p_ck频率,则可以设置:DIVM1=2,DIVN1=240,DIVP1=2即可。其他分频输出(pll1_q_ck/pll1_r_ck)计算方法与上式相似。我们把它们对应到STM32CubeMX工具的时钟系统图上理解就很直观了,如图11.1.2.1所示。
【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1_第2张图片

图11.1.2.1 STM32CubeMX对应的设置
关于时钟系统,大家如果看数据手册不太理解,结合STM32CubeMX的时钟系统配置界面理解会有很好的效果。
11.1.3 系统时钟SYSCLK
下面看到STM32H7时钟系统图标号④⑤两个组成部分:
④sys_ck时钟源选择器
sys_ck(即系统时钟)的时钟源有四个:hsi_ck、csi_ck、hse_ck和pll1_p_ck,系统默认是选择hsi_ck(64Mhz)作为时钟源的。当我们把PLL1配置好并且选择pll1_p_ck作为系统时钟的时钟源,则系统时钟可以得到480MHZ的时钟频率,从而得到最高性能。
⑤系统时钟生成和使能单元
SCGU(System Clock Generation Unit,系统时钟生成单元),用于将sys_ck分成各种时钟频率,供给CPU、SysTick,以及各条总线使用,从而有了许多的时钟名称。这里大家对以下的总线及其相对应的时钟名称有印象即可,比如:在AHB14总线上的时钟就是rcc_hclk14,在APB14总线上的时钟就是rcc_pclk14。这8条总线和对应的8个时钟在后面会经常提及。
SCEU(System Clock Eable Unit,系统时钟使能单元),用于使能各个外设、总线等时钟,是一个时钟开关。
接下来,我们介绍系统时钟生成单元图(非常重要),如图11.1.3.1所示;
【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1_第3张图片

图11.1.3.1 STM32H750系统时钟生成图
上图主要列出了STM32H750系统时钟的生成原理,包括CPU时钟、SysTick时钟、AXI时钟、AHB14总线和APB14总线时钟等,这些时钟对整个系统运行来说非常重要,所以该图必须要看懂。
图中,D1、D2和D3域是ST为了支持动态能效管理,所设计的3个独立的电源域,每个域都能独立开启/关闭。系统时钟由SCGU产生,然后经过SCEU做开关,最终输出到各个时钟域(D1、D2和D3),从而能够控制和访问各类外设,保证系统的正常运行。
我们挑选出8个重要的地方进行介绍(图11.1.3.1中标出的①~⑧):
① SCGU输入时钟(sys_ck),该时钟我们一般选择来自pll1_p_ck,频率为480Mz。
② sys_d1cpre_ck时钟的分频系数,取值范围为1~512,通过RCC_ D1CFGR寄存器的D1CPRE[3:0]位设置,我们一般设置为1分频,以得到最高的sys_d1cpre_ck频率,480Mhz。
③ CPU时钟(rcc_c_ck、rcc_fclk_c),CPU时钟是直接来自sys_d1cpre_ck,没有分频
器,频率为480Mhz。
④ SysTick时钟预分频器(可以选择1分频或者8分频),在例程中,我们选择1分频,因此SysTick的时钟源直接来自sys_d1cpre_ck,频率为480Mhz。
⑤ 系统时钟使能单元(SCEU),它能够对D1、D2和D3域内的所有外设时钟进行开/关控制,所以在使用外设的时候,必须设置SCEU,使能其时钟,否则外设无法使用。这里所谓的SCEU设置,实际上就是通过AHB1ENR之类的寄存器设置的。
⑥ D1域,是高性能处理域,包含CPU、SysTick、AXI、AHB3和APB3等部分。CPU可以从TCM和Cache中提取紧急的或优先级较高的用户程序,在480M的主频下执行,确保实现最快速响应。
⑦ D2域,外设接口域,包含大部分外设,包括:AHB1、AHB2、APB1和APB2等时钟部分。注意:定时器的时钟都是在D2域进行控制,而且当D2PPRE1或D2PPRE2的分频系数不为1的时候,定时器的时钟频率为rcc_pclk1或rcc_pclk2的2倍。
⑧ D3域,低功耗处理域,包括AHB4和APB4等时钟部分。此部分中的ADC可以在整个系统深度休眠时仍然进行数据处理。在电池驱动的情况下,D3可以保证在低功耗条件下仍然进行必要的数据处理工作。
我们可以把STM32H750系统时钟生成图对应到STM32CubeMX工具上理解,如图11.1.3.2所示。
【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1_第4张图片

图11.1.3.2 对应STM32CubeMX配置图
11.1.4 外设内核时钟
下面看到STM32H7时钟系统图标号⑥部分:
⑥外设内核时钟选择和使能单元
PKSU(Peripheral Kernel clock Selection Unit,外设内核时钟选择单元),用于选择部分外设的内核时钟源,STM32H7内部,很多外设都可以自由选择内核时钟来源,从而提高使用的灵活性。
PKEU(Peripheral Kernel clock Enable Unit,外设内核时钟使能单元),用于使能部分外设的内核时钟,从而控制外设内核时钟的开/关。
接下来我们看看外设时钟使能框图,如图11.1.4.1所示:
【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1_第5张图片

图11.1.4.1 STM32H750外设时钟使能框图
图中,PERx(perx)代表外设x(x是STM32H7内部的外设,比如USART、TIM、SPI等),通过此图,我们可以可以知道STM32H7内部外设时钟的由来。
我们挑选出8个重要的地方进行介绍(图11.1.4.1中标出的①~⑧):
①系统时钟生成单元(SCGU),此部分在图11.1.3.1我们进行了详细介绍,这里通过SCGU
产生了三大总线时钟:rcc_bus_d1_ck、rcc_bus_d2_ck、rcc_bus_d3_ck,这些时钟实际上是指rcc_hclk14、rcc_pclk14等时钟。
② 系统时钟使能单元(SCEU),此部分将SCGU生成的时钟进行使能/禁止操作,最终控
制是否输出时钟(rcc_perx_bus_ck)给外设。
③ 总线接口时钟控制逻辑(busif Control Logic),用于控制SCEU是否输出时钟给外
设,它有很多控制信号,其中我们常用的是PERxEN(图中红框框出部分),通过这个位的设置就可以控制具体外设的时钟开/关。
④ 外设总线时钟(rcc_perx_bus_ck),该时钟主要用于访问外设寄存器,对外设进行
设置。任何外设都必须使能该时钟才可以正常使用。
⑤ 外设内核时钟选择单元(PKSU,即:Peripheral Kernel clock Selection Unit),用于选择某个外设的内核时钟来源选择。
⑥ 外设内核时钟使能单元(PKEU,即:Peripheral Kernel clock Enable Unit),此部分将⑤处选择的外设内核时钟进行使能/禁止操作,最终控制是否输出内核时钟(rcc_perx_ker_ck)给外设。
⑦ 内核时钟控制逻辑(Kernel Control Logic),用于控制PKEU是否输出内核时钟给外设,它有很多控制信号,其中我们常用的是PERxEN(图中红圈圈出部分),通过这个位的设置就可以控制具体外设内核时钟的开/关。注意:③和⑦,都可以通过 PERxEN控制使能。
⑧ 外设内核时钟(rcc_perx_ker_ck),该时钟用于驱动外设产生时序,如波特率、时钟脉冲等。大部分外设都需要用到rcc_perx_ker_ck,比如串口、SPI、IIC、FMC、SAI、LTDC和CAN等,但是,并不是所有的外设都需要用内核时钟来产生时序,比如:OPAMP、DMA等。
因此,大部分外设的使用,需要同时用到外设总线时钟(rcc_perx_bus_ck)和外设内核时钟(rcc_perx_ker_ck),一般情况下,这两个时钟都是由PERxEN控制使能。至于哪些外设有内核时钟,请参考:《STM32H7xx参考手册_V7(英文版).pdf》352页,Table 59。
SCEU和PKEU,一个用于控制外设的访问时钟(访问寄存器),一个用于控制外设的内核时钟(生成控制时序,如波特率等)。但是,并不是所有的外设都需要用到PKEU,因为有些外设并不需要生成时序,没有所谓的外设内核时钟,比如DMA、OPAMP等,这些外设只需要在SCEU进行使能即可。
11.1.5 MCO时钟输出
MCO时钟输出,其作用是为外部器件提供时钟,STM32H750有两个MCO。下面看到STM32H7时钟系统图标号⑦⑧⑨三个部分:
⑦MCO1\MCO2时钟源选择器
MCO1(外部器件的输出时钟1)时钟源有五个:his_ck、lse_ck、hse_ck、pll1_q_ck和hsi48_ck。
MCO2(外部器件的输出时钟2)时钟源有六个:sys_ck、pll2_p_ck、hse_ck、pll1_p_ck、csi_ck和lsi_ck。
⑧MCO1\MCO2时钟分频器
MCO1PRE是MCO1的预分频器,取值范围:1到15。
MCO2PRE是MCO2的预分频器,取值范围:1到15。
⑨MCO1\MCO2时钟输出引脚
MCO1、MCO2两个时钟输出引脚给外部器件提供时钟源(分别由PA8和PC9复用功能实现),每个引脚可以选择一个时钟源,通过RCC时钟配置寄存器 (RCC_CFGR)进行配置。
关于STM32H7xx时钟的详细介绍,请参考《STM32H7xx参考手册_V7(英文版).pdf》第8.5节,有不明白的地方,可以对照手册仔细研究。具体哪个外设是连接在哪个时钟总线上,以及对应的时钟总线最高主频是多少,请参考STM32H750VBT6.pdf数据手册(路径:A盘7,硬件资料2,芯片资料 STM32H750VBT6.pdf)。
通过以上内容的介绍,我们知道STM32H750的时钟设计的比较复杂,各个时钟基本都是可控的,任何外设都有对应的时钟控制开关,这样的设计,对降低功耗是非常有用的,不用的外设不开启时钟,就可以大大降低其功耗。

11.2 如何修改主频

STM32H750默认的情况下(比如:串口IAP时或者是未初始化时钟时),使用的是内部64M的HSI作为时钟源,所以不需要外部晶振也可以下载和运行代码的。
下面我们来讲解如何让STM32H750芯片在480MHZ的频率下工作,这是官方推荐使用的最高的稳定时钟频率。正点原子的MiniPRO STM32H750开发板的外部高速晶振的频率是8MHZ,我们就是在这个晶振频率的基础上,通过各种倍频和分频得到480MHZ的系统工作频率。
11.2.1 STM32H7时钟系统配置
下面我们将分几步给大家讲解STM32H7时钟系统配置过程,这部分内容很重要,请大家认真阅读。
第1步:配置HSE_VALUE
讲解stm32h7xx_hal_conf.h文件的时候,我们知道需要宏定义HSE_VALUE匹配我们实际硬件的高速晶振频率(这里是8MHZ),代码如下:
#if !defined (HSE_VALUE)
#define HSE_VALUE ((uint32_t)8000000) /* 外部高速振荡器的值,单位HZ /
#endif /
HSE_VALUE */
第2步:调用SystemInit函数
我们介绍启动文件的时候就知道,在系统启动之后,程序会先执行SystemInit函数,进行系统一些初始化配置。启动代码调用SystemInit函数如下:

Reset_Handler    PROC
                 EXPORT  Reset_Handler                    [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

下面我们来看看system_stm32h7xx.c文件下定义的SystemInit程序,源码在140行到245行,简化函数如下。

void SystemInit (void)
{
  /* FPU 设置------------------------------------------------------------*/
  #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
    SCB->CPACR |= ((3UL << (10*2))|(3UL << (11*2))); /* 设置CP10和CP11为全访问 */
  #endif
  /* 复位RCC时钟配置为默认配置 */
  /* 打开HSION位 */
  RCC->CR |= RCC_CR_HSION;
  RCC->CFGR = 0x00000000;  /* 复位CFGR寄存器 */
  /* 复位 HSEON, CSSON , CSION,RC48ON, CSIKERON PLL1ON, PLL2ON and PLL3ON 位 */
  RCC->CR &= 0xEAF6ED7FU;
  RCC->D1CFGR = 0x00000000;      	/* 复位D1CFGR寄存器 */
  RCC->D2CFGR = 0x00000000;      	/* 复位D2CFGR寄存器 */
  RCC->D3CFGR = 0x00000000;      	/* 复位D3CFGR寄存器 */
  RCC->PLLCKSELR = 0x00000000;   	/* 复位PLLCKSELR寄存器 */
  RCC->PLLCFGR = 0x00000000;     	/* 复位PLLCFGR寄存器 */
  RCC->PLL1DIVR = 0x00000000;    	/* 复位PLL1DIVR寄存器 */
  RCC->PLL1FRACR = 0x00000000;   	/* 复位PLL1FRACR寄存器 */
  RCC->PLL2DIVR = 0x00000000;    	/* 复位PLL2DIVR寄存器 */
  RCC->PLL2FRACR = 0x00000000;   	/* 复位PLL2FRACR寄存器 */
  RCC->PLL3DIVR = 0x00000000;    	/* 复位PLL3DIVR寄存器 */
  RCC->PLL3FRACR = 0x00000000;   	/* 复位PLL3FRACR寄存器 */
  RCC->CR &= 0xFFFBFFFFU;         	/* 清除CR寄存器的HSEBYP位 */
  RCC->CIER = 0x00000000;         	/* 关闭所有的中断 */

  /* 双核CM7或单核线 */
  if((DBGMCU->IDCODE & 0xFFFF0000U) < 0x20000000U)
  {
    /* if stm32h7 revY*/
/* Change  the switch matrix read issuing capability 
to 1 for the AXI SRAM target (Target 7) */
    *((__IO uint32_t*)0x51008108) = 0x000000001U;
  }

  /* Configure the Vector Table location add offset address for cortex-M7 ---*/
#ifdef VECT_TAB_SRAM
  SCB->VTOR = D1_AXISRAM_BASE  | VECT_TAB_OFFSET; 
#else
  SCB->VTOR = FLASH_BANK1_BASE | VECT_TAB_OFFSET; 
#endif
}

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

  1. 设置FPU
  2. 复位RCC时钟配置为默认复位值(默认开启HSI)
  3. 配置中断向量表地址
    HAL库的SystemInit函数并没有像标准库的SystemInit函数一样进行时钟的初始化配置。HAL库的SystemInit函数除了打开HSI之外,没有任何时钟相关配置,所以使用HAL库,我们必须编写自己的时钟配置函数。
    第3步:在main函数里调用用户编写的时钟设置函数
    我们打开HAL库例程实验1跑马灯实验,看看我们在工程目录Drivers\SYSTEM分组下面定义的sys.c文件中的时钟设置函数sys_stm32_clock_init的内容:
/**
 * @brief       时钟设置函数
 * @param       plln: PLL1倍频系数(PLL倍频), 取值范围: 4~512.
 * @param       pllm: PLL1预分频系数(进PLL之前的分频), 取值范围: 2~63.
 * @param       pllp: PLL1的p分频系数(PLL之后的分频), 分频后作为系统时钟, 取值范围: 
*                       2~128.(且必 须是2的倍数)
 * @param       pllq: PLL1的q分频系数(PLL之后的分频), 取值范围: 1~128.
 * @note
 *
 *              Fvco: VCO频率
 *              Fsys: 系统时钟频率, 也是PLL1的p分频输出时钟频率
 *              Fq:   PLL1的q分频输出时钟频率
 *              Fs:   PLL输入时钟频率, 可以是HSI, CSI, HSE等.
 *              Fvco = Fs * (plln / pllm);
 *              Fsys = Fvco / pllp = Fs * (plln / (pllm * pllp));
 *              Fq   = Fvco / pllq = Fs * (plln / (pllm * pllq));
 *
 *              外部晶振为25M的时候, 推荐值:plln = 192, pllm = 5, pllp = 2, pllq = 4.
 *              外部晶振为 8M的时候, 推荐值:plln = 240, pllm = 2, pllp = 2, pllq = 4.
 *              得到:Fvco = 8 * (240 / 2) = 960Mhz
 *                   Fsys = pll1_p_ck = 960 / 2 = 480Mhz
 *                   Fq   = pll1_q_ck = 960 / 4 = 240Mhz
 *
 *              H750默认需要配置的频率如下:
 *              CPU频率(rcc_c_ck) = sys_d1cpre_ck = 480Mhz
 *              rcc_aclk = rcc_hclk3 = 240Mhz
 *              AHB1/2/3/4(rcc_hclk1/2/3/4) = 240Mhz
 *              APB1/2/3/4(rcc_pclk1/2/3/4) = 120Mhz
 *              pll2_p_ck = (8 / 8) * 440 / 2) = 220Mhz
 *              pll2_r_ck = FMC时钟频率 = ((8 / 8) * 440 / 2) = 220Mhz
 *
 * @retval    错误代码: 0, 成功; 1, 错误;
 */
uint8_t sys_stm32_clock_init(uint32_t plln, uint32_t pllm, 
uint32_t pllp, uint32_t pllq)
{
    HAL_StatusTypeDef ret = HAL_OK;
    RCC_ClkInitTypeDef rcc_clk_init_handle;
    RCC_OscInitTypeDef rcc_osc_init_handle;
    RCC_PeriphCLKInitTypeDef rcc_periph_clk_init_handle;

MODIFY_REG(PWR->CR3, PWR_CR3_SCUEN, 0); /*使能供电配置更新 */
/* VOS = 1, Scale1, 1.2V内核电压,FLASH访问可以得到最高性能 */
    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);  
    while ((PWR->D3CR & (PWR_D3CR_VOSRDY)) != PWR_D3CR_VOSRDY);  

/* 等待电压稳定 */

/* 使能HSE,并选择HSE作为PLL时钟源,配置PLL1,开启USB时钟 */
rcc_osc_init_handle.OscillatorType = RCC_OSCILLATORTYPE_HSE 
| RCC_OSCILLATORTYPE_HSI48;
    rcc_osc_init_handle.HSEState = RCC_HSE_ON;
    rcc_osc_init_handle.HSIState = RCC_HSI_OFF;
    rcc_osc_init_handle.CSIState = RCC_CSI_OFF;
    rcc_osc_init_handle.HSI48State = RCC_HSI48_ON;
    rcc_osc_init_handle.PLL.PLLState = RCC_PLL_ON;
    rcc_osc_init_handle.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    rcc_osc_init_handle.PLL.PLLN = plln;
    rcc_osc_init_handle.PLL.PLLM = pllm;
    rcc_osc_init_handle.PLL.PLLP = pllp;
    rcc_osc_init_handle.PLL.PLLQ = pllq;
    rcc_osc_init_handle.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
    rcc_osc_init_handle.PLL.PLLRGE = RCC_PLL1VCIRANGE_2;
    rcc_osc_init_handle.PLL.PLLFRACN = 0;
    ret=HAL_RCC_OscConfig(&rcc_osc_init_handle);
    if(ret != HAL_OK)
    {
        return 1;
    }

    /*
     *  选择PLL的输出作为系统时钟
     *  配置RCC_CLOCKTYPE_SYSCLK系统时钟,480M
     *  配置RCC_CLOCKTYPE_HCLK 时钟,240Mhz,对应AHB1,AHB2,AHB3和AHB4总线
     *  配置RCC_CLOCKTYPE_PCLK1时钟,120Mhz,对应APB1总线
     *  配置RCC_CLOCKTYPE_PCLK2时钟,120Mhz,对应APB2总线
     *  配置RCC_CLOCKTYPE_D1PCLK1时钟,120Mhz,对应APB3总线
     *  配置RCC_CLOCKTYPE_D3PCLK1时钟,120Mhz,对应APB4总线
     */
    
    rcc_clk_init_handle.ClockType = (RCC_CLOCKTYPE_SYSCLK \
                                  		 | RCC_CLOCKTYPE_HCLK \
                                  		 | RCC_CLOCKTYPE_PCLK1 \
                                 		 | RCC_CLOCKTYPE_PCLK2 \
                                  		 | RCC_CLOCKTYPE_D1PCLK1 \
                                  		 | RCC_CLOCKTYPE_D3PCLK1);

    rcc_clk_init_handle.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    rcc_clk_init_handle.SYSCLKDivider = RCC_SYSCLK_DIV1;
    rcc_clk_init_handle.AHBCLKDivider = RCC_HCLK_DIV2;
    rcc_clk_init_handle.APB1CLKDivider = RCC_APB1_DIV2; 
    rcc_clk_init_handle.APB2CLKDivider = RCC_APB2_DIV2; 
    rcc_clk_init_handle.APB3CLKDivider = RCC_APB3_DIV2;  
    rcc_clk_init_handle.APB4CLKDivider = RCC_APB4_DIV2; 
    ret = HAL_RCC_ClockConfig(&rcc_clk_init_handle, FLASH_LATENCY_4);
    if(ret != HAL_OK)
    {
        return 1;
    }

    /*
     *  配置PLL2的R分频输出, 为220Mhz
     *  配置FMC时钟源是PLL2R
     *  配置QSPI时钟源是PLL2R
     *  配置串口1 和 串口6 的时钟源来自: PCLK2 = 120Mhz
     *  配置串口2 / 3 / 4 / 5 / 7 / 8 的时钟源来自: PCLK1 = 120Mhz
     *  USB工作需要48MHz的时钟,可以由PLL1Q,PLL3Q和HSI48提供,这里配置时钟源是HSI48
     */
rcc_periph_clk_init_handle.PeriphClockSelection = RCC_PERIPHCLK_USART2 
| RCC_PERIPHCLK_USART1 | RCC_PERIPHCLK_USB 
| RCC_PERIPHCLK_QSPI | RCC_PERIPHCLK_FMC;
    rcc_periph_clk_init_handle.PLL2.PLL2M = 8;
    rcc_periph_clk_init_handle.PLL2.PLL2N = 440;
    rcc_periph_clk_init_handle.PLL2.PLL2P = 2;
    rcc_periph_clk_init_handle.PLL2.PLL2Q = 2;
    rcc_periph_clk_init_handle.PLL2.PLL2R = 2;
    rcc_periph_clk_init_handle.PLL2.PLL2RGE = RCC_PLL2VCIRANGE_0;
    rcc_periph_clk_init_handle.PLL2.PLL2VCOSEL = RCC_PLL2VCOWIDE;
    rcc_periph_clk_init_handle.PLL2.PLL2FRACN = 0;
    rcc_periph_clk_init_handle.FmcClockSelection = RCC_FMCCLKSOURCE_PLL2;
    rcc_periph_clk_init_handle.QspiClockSelection = RCC_QSPICLKSOURCE_PLL2;
rcc_periph_clk_init_handle.Usart234578ClockSelection 
= RCC_USART234578CLKSOURCE_D2PCLK1;
rcc_periph_clk_init_handle.Usart16ClockSelection 
= RCC_USART16CLKSOURCE_D2PCLK2;
    rcc_periph_clk_init_handle.UsbClockSelection = RCC_USBCLKSOURCE_HSI48;
    ret = HAL_RCCEx_PeriphCLKConfig(&rcc_periph_clk_init_handle);
    if(ret != HAL_OK)
    {
        return 1;
    }

    sys_qspi_enable_memmapmode(0);  /* 使能QSPI内存映射模式, FLASH容量为普通类型 */

    HAL_PWREx_EnableUSBVoltageDetector();   /* 使能USB电压电平检测器 */
    __HAL_RCC_CSI_ENABLE() ;                  	/* 使能CSI时钟 */
    __HAL_RCC_SYSCFG_CLK_ENABLE() ;         	/* 使能SYSCFG时钟 */
    HAL_EnableCompensationCell();           	/* 使能IO补偿单元 */
    return 0;
}

函数sys_stm32_clock_init就是用户的时钟系统配置函数,除了配置PLL相关参数确定SYSCLK值之外,还配置了AHB、APB1、APB2、APB3和APB4总线时钟的预分频系数,也就是确定了rcc_hclk14和rcc_pclk14等时钟频率。我们首先来看看使用HAL库配置STM32H7时钟系统的一般步骤:

  1. 设置调压器输出电压级别:调用函数__HAL_PWR_VOLTAGESCALING_CONFIG()。
  2. 配置时钟源相关参数:调用函数HAL_RCC_OscConfig()。
  3. 选择系统时钟SYSCLK的时钟来源以及配置AHB、APB1、APB2、APB3和APB4总线时钟的预分频系数,调用HAL_RCC_ClockConfig()函数。
  4. 配置PLL2/PLL3以及扩展外设的时钟,调用HAL_RCCEx _PeriphCLKConfig()函数。
  5. 使能其他一些时钟和IO补偿单元。
    下面我们详细讲解这个5个步骤。
    步骤1:调压器输出电压级别VOS,它是由PWR控制寄存器D3CR的位15:14来确定的:
    位15:14 VOS[1:0]
    00:保留(默认选择电压调节3)
    01:电压调节3(默认)
    10:电压调节2
    11:电压调节1
    电压调节级别数值越小工作频率越高,所以如果我们要配置H750的主频为480MHz,那么我们需要配置调压器输出电压调节级别VOS为级别1,详细的表格参考在步骤3会讲解。所以函数sys_stm32_clock_init中步骤1源码如下:
    /* VOS = 1, Scale1, 1.2V内核电压,FLASH访问可以得到最高性能 */
    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
    步骤2:配置时钟源和锁相环PPL1的相关参数,使能并选择HSE作为PLL时钟源,我们调用的函数为HAL_RCC_OscConfig(),该函数在HAL库头文件stm32h7xx_hal_rcc.h中声明,在文件stm32h7xx_hal_rcc.c中定义。首先我们来看看该函数声明:
HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);
该函数只有一个形参,就是RCC_OscInitTypeDef结构体类型指针。接下来我们看看RCC_OscInitTypeDef结构体的定义:
typedef struct
{
  uint32_t OscillatorType;         	/* 需要选择配置的振荡器类型 */
  uint32_t HSEState;                	/* HSE状态 */
  uint32_t LSEState;                	/* LSE状态 */
  uint32_t HSIState;                	/* HIS状态 */
  uint32_t HSICalibrationValue;   	/* HIS校准值 */
  uint32_t LSIState;                	/* LSI状态 */
  uint32_t HSI48State;              	/* HSI48的状态 */
  uint32_t CSIState;                	/* CSI状态 */
  uint32_t CSICalibrationValue;   	/* CSI校准值 */
  RCC_PLLInitTypeDef PLL;          	/* PLL配置 */
}RCC_OscInitTypeDef;

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

typedef struct
{
  uint32_t PLLState;     	/* PLL1状态 */
  uint32_t PLLSource;    	/* PLL1时钟源 */
  uint32_t PLLM;          	/* PLL1分频系数M */
  uint32_t PLLN;          	/* PLL1倍频系数N */                        
  uint32_t PLLP;          	/* PLL1分频系数P */
  uint32_t PLLQ;          	/* PLL1分频系数Q */
  uint32_t PLLR;          	/* PLL1分频系数R */
  uint32_t PLLRGE;        	/* PLL1时钟输入范围 */
  uint32_t PLLVCOSEL;    	/* PLL1时钟输出范围 */
  uint32_t PLLFRACN;     	/* PLL1 VCO乘数因子的小数部分 */
}RCC_PLLInitTypeDef;

从RCC_PLLInitTypeDef结构体的定义很容易看出该结构体主要用来设置PLL时钟源以及相关分频倍频参数。这个结构体定义的相关内容请结合图 11.1.1时钟树中红色框部分内容一起理解。
接下来看看系统时钟初始化函数sys_stm32_clock_init中的配置内容:
/* 使能HSE,并选择HSE作为PLL时钟源,配置PLL1,开启USB时钟 */

rcc_osc_init_handle.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_HSI48;
rcc_osc_init_handle.HSEState = RCC_HSE_ON;         	/* 打开HSE */
rcc_osc_init_handle.HSIState = RCC_HSI_OFF;        	/* 关闭HSI */
rcc_osc_init_handle.CSIState = RCC_CSI_OFF;        	/* 关闭CSI */
rcc_osc_init_handle.HSI48State = RCC_HSI48_ON;    	/* 打开HSI48 */
rcc_osc_init_handle.PLL.PLLState = RCC_PLL_ON;    	/* 打开PLL */
rcc_osc_init_handle.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* 设置PLL时钟源为HSE */
rcc_osc_init_handle.PLL.PLLN = plln;
rcc_osc_init_handle.PLL.PLLM = pllm;
rcc_osc_init_handle.PLL.PLLP = pllp;
rcc_osc_init_handle.PLL.PLLQ = pllq;
rcc_osc_init_handle.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
rcc_osc_init_handle.PLL.PLLRGE = RCC_PLL1VCIRANGE_2;
rcc_osc_init_handle.PLL.PLLFRACN = 0;
ret=HAL_RCC_OscConfig(&rcc_osc_init_handle);

通过函数的该段程序,我们开启了HSE时钟源,同时选择PLL时钟源为HSE,然后把sys_stm32_clock_init的4个形式参数直接设置作为PLL的参数N、M、P和Q的值,这样就达到了设置PLL时钟源相关参数的目的。另外我们还开启了HSI48时钟,为USB相关实验做准备。
设置好时钟源和PLL1的相关参数后,就完成了步骤2的内容。
步骤3:选择系统时钟SYSCLK的时钟来源以及配置AHB、APB1、APB2、APB3和APB4总线时钟的预分频系数,用函数HAL_RCC_ClockConfig(),声明如下:
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,
uint32_t FLatency);
该函数有两个形参,第一个形参RCC_ClkInitStruct是RCC_ClkInitTypeDef结构体类型指针变量,用于设置SYSCLK时钟源以及SYSCLK、AHB、APB1、APB2、APB3和APB4的分频系数。第二个形参FLatency用于设置FLASH延迟。
RCC_ClkInitTypeDef结构体类型定义比较简单,我们来看看其定义:

typedef struct
{
  uint32_t ClockType;             	/* 要配置的时钟 */
  uint32_t SYSCLKSource;          	/* 系统时钟源 */
  uint32_t SYSCLKDivider;         	/* SYSCLK分频系数 */
  uint32_t AHBCLKDivider;         	/* AHB分频系数 */
  uint32_t APB3CLKDivider;        	/* APB3分频系数 */
  uint32_t APB1CLKDivider;        	/* APB1分频系数 */
  uint32_t APB2CLKDivider;        	/* APB2分频系数 */
  uint32_t APB4CLKDivider;        	/* APB4分频系数 */
}RCC_ClkInitTypeDef;

我们在sys_stm32_clock_init函数中的实际应用配置内容如下:

/*
 *  选择PLL的输出作为系统时钟
 *  配置RCC_CLOCKTYPE_SYSCLK系统时钟,480M
 *  配置RCC_CLOCKTYPE_HCLK 时钟,240Mhz,对应AHB1,AHB2,AHB3和AHB4总线
 *  配置RCC_CLOCKTYPE_PCLK1时钟,120Mhz,对应APB1总线
 *  配置RCC_CLOCKTYPE_PCLK2时钟,120Mhz,对应APB2总线
 *  配置RCC_CLOCKTYPE_D1PCLK1时钟,120Mhz,对应APB3总线
 *  配置RCC_CLOCKTYPE_D3PCLK1时钟,120Mhz,对应APB4总线
 */

rcc_clk_init_handle.ClockType = (RCC_CLOCKTYPE_SYSCLK \
                              		  | RCC_CLOCKTYPE_HCLK \
                              		  | RCC_CLOCKTYPE_PCLK1 \
                              		  | RCC_CLOCKTYPE_PCLK2 \
                               		  | RCC_CLOCKTYPE_D1PCLK1 \
                             		  | RCC_CLOCKTYPE_D3PCLK1);

rcc_clk_init_handle.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
rcc_clk_init_handle.SYSCLKDivider = RCC_SYSCLK_DIV1;
rcc_clk_init_handle.AHBCLKDivider = RCC_HCLK_DIV2;
rcc_clk_init_handle.APB1CLKDivider = RCC_APB1_DIV2; 
rcc_clk_init_handle.APB2CLKDivider = RCC_APB2_DIV2; 
rcc_clk_init_handle.APB3CLKDivider = RCC_APB3_DIV2;  
rcc_clk_init_handle.APB4CLKDivider = RCC_APB4_DIV2; 
ret = HAL_RCC_ClockConfig(&rcc_clk_init_handle, FLASH_LATENCY_4);

sys_stm32_clock_init函数中的RCC_ClkInitTypeDef结构体配置内容:
第一个成员ClockType配置表示我们要配置到的时钟,把所以配置到的时钟的宏都赋值给该成员,这里我们包含了六个宏定义。
第二个成员SYSCLKSource配置选择系统时钟源为PLL。
第三个成员SYSCLKDivider配置SYSCLK分频系数为1。
第四个成员AHBCLKDivider配置AHB总线时钟预分频系数为2。
第五个成员APB1CLKDivider配置APB1总线时钟预分频系数为2。
第六个成员APB2CLKDivider配置APB2总线时钟预分频系数为2。
第七个成员APB3CLKDivider配置APB3总线时钟预分频系数为2。
第八个成员APB4CLKDivider配置APB4总线时钟预分频系数为2。
根据我们在mian函数中调用sys_stm32_clock_init(240, 2, 2, 4)时设置的形参数值,代入PLL输出频率的计算公式可得:
【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1_第6张图片

因为我们选择系统时钟源为PLL(pll1_p_ck),所以系统时钟SYSCLK=480MHz。再结合各个总线时钟的预分频系数,总结一下调用函数sys_stm32_clock_init(240, 2, 2, 4)之后,关键的时钟频率值如下:(HCLK代表AHB1~4总线上的时钟)
sys_ck(系统时钟) = 480MHz
sys_ck分频后的时钟(sys_d1cpre_ck) = 480MHz
AHB总线时钟(HCLK= sys_d1cpre_ck /2) = 240MHz
APB1总线时钟(rcc_pclk1=HCLK/2) = 120MHz
APB2总线时钟(rcc_pclk2=HCLK/2) = 120MHz
APB3总线时钟 (rcc_pclk3=HCLK/2) = 120MHz
APB4总线时钟 (rcc_pclk4=HCLK/2) = 120MHz
最后我们来看看函数HAL_RCC_ClockConfig第二个入口参数FLatency的含义,对于STM32H7系列,FLASH延迟配置参数值是通过下表来确定的:
【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1_第7张图片

表11.2.1.1 FLASH推荐的等待状态和编程延迟数
上表中的时钟频率为ACLK(即AXI Interface clock),ACLK=HCLK=240MHz(我们现在实际是240MHZ),上表中,当调压器设置为VOS1级别的时候(我们设置的就是VOS1级别),如果需要ACLK最高为225MHz,那么FLASH延迟等待周期要为3WS,也就是总共4个FLASH时钟周期才访问一次FLASH。但是我们的ACLK=HCLK=240MHz,这里最高只有225MHZ,这是因为之前的H7最高是400MHZ的,现在出了480MHZ,但是官方文档没有更新完全,所以我们先将就参考这个表。下面我们看看我们在sys_stm32_clock_init中调用函数HAL_RCC_ClockConfig的时候,第二个形参设置值:
ret = HAL_RCC_ClockConfig(&rcc_clk_init_handle, FLASH_LATENCY_4);
从上可以看出,我们设置值为FLASH_LATENCY_4,也就是每次访问FLASH需要延迟4WS,总共要5个FLASH周期。为什么选FLASH_LATENCY_4呢?因为ST官方例程使用的就是4个FLASH四个延迟周期,其实大于2个WS都可以(最大不能超过7个WS),延时越久越稳定,但是肯定影响性能。综合考虑性能和稳定性,我们也就选择4个FLASH四个延迟周期。这个值也是我们实践出来的,大家直接选用这个值就行。
最后,再给大家用STM32CubeMX工具的时钟配置界面,标记一下:sys_ck、sys_d1cpre_ck、rcc_hclk14以及rcc_pclk14对应的位置,方便大家更容易理解。如下图:
【正点原子STM32连载】第十一章 STM32时钟系统 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1_第8张图片

图11.2.1.1STM32CubeMX时钟配置界面标记图
由上图可知道:
rcc_hclk1~4分别对应的是AHB1,AHB2,AHB3和AHB4总线的时钟。
rcc_pclk1~4分别对应的是APB1,APB2,APB3和APB4总线的时钟。
步骤4:配置PLL2,DIVM2预分频系数为8,DIVN2倍频系数为440,PLL2的三个时钟输出对应的预分频系数DIVP2、DIVQ2和DIVR2都是2,即2分频,所以得到它们的时钟频率都是220MHZ。因为我们要用到PLL2R输出作为FMC和QSPI的时钟源,所以我们用PLL输出频率计算公式算出PLL2R的输出频率:

由上面的公式得:PLL2R输出频率为220MHZ,PLL2P和PLL2Q计算方法同理。
这里我们除了配置PLL2R的输出频率外,还配置了串口和USB的时钟源,都是为后续实验准备。
配置PLL2/PLL3以及扩展外设的时钟,我们要调用HAL_RCCEx _PeriphCLKConfig()函数,其声明如下:
HAL_StatusTypeDef HAL_RCCEx_PeriphCLKConfig(RCC_PeriphCLKInitTypeDef *PeriphClkInit);
该函数只有一个形参,就是RCC_PeriphCLKInitTypeDef结构体类型指针。该结构体类型涉及的内容很多,我们只要把用到的搞懂就好。其简化定义如下:

typedef struct
{
  uint32_t PeriphClockSelection;       	/* 要配置的扩展时钟 */
  RCC_PLL2InitTypeDef PLL2;             	/* PLL2时钟配置结构体 */
  RCC_PLL3InitTypeDef PLL3;             	/* PLL3时钟配置结构体 */
  uint32_t FmcClockSelection;           	/* FMC时钟源 */
  uint32_t QspiClockSelection;          	/* QSPI时钟源 */
  uint32_t Usart234578ClockSelection; 	/* USART2/3/4/5/7/8 时钟源 */
  uint32_t Usart16ClockSelection;      	/* USART1/6时钟源 */
  uint32_t UsbClockSelection;           	/* USB时钟源 */
}RCC_PeriphCLKInitTypeDef;

我们只把现在用到的定义列出来,其他以后用到再讲。PeriphClockSelection是需要配置的时钟,结构体变量PLL2 和PLL3就是对应的PLL2和PLL3的时钟配置,其他的变量都是对应的外设时钟源配置。我们要用到PLL2的DIVR2分频输出,所以看看RCC_PLL2InitTypeDef结构体的定义:

typedef struct
{
  uint32_t PLL2M;          	/* PLL2分频系数M */
  uint32_t PLL2N;          	/* PLL2倍频系数N */   
  uint32_t PLL2P;          	/* PLL2倍频系数P */
  uint32_t PLL2Q;          	/* PLL2倍频系数Q */
  uint32_t PLL2R;          	/* PLL2倍频系数R */
  uint32_t PLL2RGE;        	/* PLL2时钟输入范围 */
  uint32_t PLL2VCOSEL;    	/* PLL2时钟输出范围 */
  uint32_t PLL2FRACN;     	/* PLL2 VCO乘数因子的小数部分 */
}RCC_PLL2InitTypeDef;

该结构体跟我们前面讲过的RCC_PLLInitTypeDef结构体很像。RCC_PLL3InitTypeDef的也是差不多的,就不列出来了,现在也没用到。
步骤4,我们在sys_stm32_clock_init函数中的实际配置内容如下:
/*

  • 配置PLL2的R分频输出, 为220Mhz
  • 配置FMC时钟源是PLL2R
  • 配置QSPI时钟源是PLL2R
  • 配置串口1 和 串口6 的时钟源来自: PCLK2 = 120Mhz
  • 配置串口2 / 3 / 4 / 5 / 7 / 8 的时钟源来自: PCLK1 = 120Mhz
  • USB工作需要48MHz的时钟,可以由PLL1Q,PLL3Q和HSI48提供,这里配置时钟源是HSI48
 */
rcc_periph_clk_init_handle.PeriphClockSelection = RCC_PERIPHCLK_USART2 
| RCC_PERIPHCLK_USART1 | RCC_PERIPHCLK_USB 
| RCC_PERIPHCLK_QSPI | RCC_PERIPHCLK_FMC;
rcc_periph_clk_init_handle.PLL2.PLL2M = 8;
rcc_periph_clk_init_handle.PLL2.PLL2N = 440;
rcc_periph_clk_init_handle.PLL2.PLL2P = 2;
rcc_periph_clk_init_handle.PLL2.PLL2Q = 2;
rcc_periph_clk_init_handle.PLL2.PLL2R = 2;
rcc_periph_clk_init_handle.PLL2.PLL2RGE = RCC_PLL2VCIRANGE_0;
rcc_periph_clk_init_handle.PLL2.PLL2VCOSEL = RCC_PLL2VCOWIDE;
rcc_periph_clk_init_handle.PLL2.PLL2FRACN = 0;
rcc_periph_clk_init_handle.FmcClockSelection = RCC_FMCCLKSOURCE_PLL2;
rcc_periph_clk_init_handle.QspiClockSelection = RCC_QSPICLKSOURCE_PLL2;
rcc_periph_clk_init_handle.Usart234578ClockSelection = 
RCC_USART234578CLKSOURCE_D2PCLK1;
rcc_periph_clk_init_handle.Usart16ClockSelection = 
RCC_USART16CLKSOURCE_D2PCLK2;
rcc_periph_clk_init_handle.UsbClockSelection = RCC_USBCLKSOURCE_HSI48;
ret = HAL_RCCEx_PeriphCLKConfig(&rcc_periph_clk_init_handle);

经过以上的配置,得到pll2_p_ck、pll2_q_ck和pll2_r_ck这三个时钟的频率都为220MHz,配置外设FMC和QSPI的时钟源都是pll2_r_ck,串口1、6时钟源是rcc_pclk2,串口2、3、4、5、7、8时钟源是rcc_pclk1,USB时钟源是HSI48。
步骤5: 使能其他一些时钟和IO补偿单元,我们直接看代码:
sys_qspi_enable_memmapmode(0); /* 使能QSPI内存映射模式, FLASH容量为普通类型 */

HAL_PWREx_EnableUSBVoltageDetector();  	/* 使能USB电压电平检测器 */
__HAL_RCC_CSI_ENABLE() ;                  /* 使能CSI时钟 */
__HAL_RCC_SYSCFG_CLK_ENABLE() ;         	/* 使能SYSCFG时钟 */
HAL_EnableCompensationCell();           	/* 使能IO补偿单元 */

sys_qspi_enable_memmapmode函数我们在12.2.2小节再讲解。后面的函数就比较简单,都是一些使能函数。其中使能IO补偿单元功能,对于要求IO翻转速度较快的应用,一般都建议使能IO补偿单元功能。使能CSI时钟,为IO补偿单元提供时钟。
时钟系统配置相关知识就给大家讲解到这里。
11.2.2 STM32H7时钟使能和配置
上一节我们讲解了时钟系统配置步骤。在配置好时钟系统之后,如果我们要使用某些外设,例如GPIO,ADC等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前没有使能外设时钟,这个外设是不可能正常运行的。STM32的外设时钟使能是在RCC相关寄存器中配置的。因为RCC相关寄存器非常多,有兴趣的同学可以直接打开《STM32H7xx参考手册_V7(英文版).pdf》 8.7小节查看所有RCC相关寄存器的配置。接下来我们来讲解通过STM32H7 的HAL库使能外设时钟的方法。
在STM32H7的HAL库中,外设时钟使能操作都是在RCC相关固件库文件头文件stm32h7xx_hal_rcc.h定义的。大家打开stm32h7xx_hal_rcc.h头文件可以看到文件中除了少数几个函数声明之外大部分都是宏定义标识符。外设时钟使能在HAL库中都是通过宏定义标识符来实现的。首先,我们来看看GPIOA的外设时钟使能宏定义标识符:

#define __HAL_RCC_GPIOA_CLK_ENABLE()   do { \
                                 __IO uint32_t tmpreg; \
                                 SET_BIT(RCC->AHB4ENR, RCC_AHB4ENR_GPIOAEN);\
                                 /* Delay after an RCC peripheral clock enabling */ \
                              tmpreg = READ_BIT(RCC->AHB4ENR, RCC_AHB4ENR_GPIOAEN);\
                              UNUSED(tmpreg); \
                            } while(0)

这段代码主要是定义了一个宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE(),它的核心操作是通过下面这行代码实现的:
SET_BIT(RCC->AHB4ENR, RCC_AHB4ENR_GPIOAEN);
这行代码的作用是,设置寄存器RCC->AHB4ENR的相关位为1,至于是哪个位,是由宏定义标识符RCC_AHB4ENR_GPIOAEN的值决定的,而它的值为:

#define RCC_AHB4ENR_GPIOAEN_Pos               (0U)
#define RCC_AHB4ENR_GPIOAEN_Msk               (0x1UL << RCC_AHB4ENR_GPIOAEN_Pos)  
#define RCC_AHB4ENR_GPIOAEN                    RCC_AHB4ENR_GPIOAEN_Msk

上面三行代码很容易计算出来RCC_AHB4ENR_GPIOAEN= 0x00000001,因此上面代码的作用是设置寄存器RCC->AHB4ENR寄存器的最低位为1。我们可以从STM32H7的参考手册中搜索AHB4ENR寄存器定义,最低位的作用是用来使用GPIOA时钟。AHB4ENR寄存器的位0描述如下:
位0 GPIOAEN:GPIOA 外设时钟使能
由软件置1和清零
0:禁止IO端口A时钟(复位后的默认值)
1:使能IO端口A时钟
那么我们只需要在我们的用户程序中调用宏定义标识符就可以实现GPIOA时钟使能。使用方法为:

__HAL_RCC_GPIOA_CLK_ENABLE();   /* 使能GPIOA时钟 */ 

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

__HAL_RCC_DMA1_CLK_ENABLE();     	/* 使能DMA1时钟 */
__HAL_RCC_USART2_CLK_ENABLE();  	/* 使能串口2时钟 */ 
__HAL_RCC_TIM1_CLK_ENABLE();    	/* 使能TIM1时钟 */

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

#define __HAL_RCC_GPIOA_CLK_DISABLE()  (RCC->AHB4ENR) &= ~ (RCC_AHB4ENR_GPIOAEN)

同样,宏定义标识符__HAL_RCC_GPIOA_CLK_DISABLE()的作用是设置RCC->AHB4ENR寄存器的最低位为0,也就是禁止GPIOA时钟。具体使用方法我们这里就不做过多讲解,我们这里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:

__HAL_RCC_DMA1_CLK_DISABLE();    	/* 禁止DMA1时钟 */
__HAL_RCC_USART2_CLK_DISABLE(); 	/* 禁止串口2时钟 */
__HAL_RCC_TIM1_CLK_DISABLE();   	/* 禁止TIM1时钟 */
关于STM32H7的外设时钟使能和禁止方法我们就给大家讲解到这里。

你可能感兴趣的:(正点原子,stm32,单片机,arm)