【正点原子MP157连载】第九章 STM32MP1时钟系统-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614
在这里插入图片描述

第九章 STM32MP1时钟系统

如果让你用一句话来形容时钟,你会怎么说?
A同学说:“时钟是单片机的心脏”;
B同学说:“军训方队训练时的121”;
C同学说:“时钟是单片机的能量”;
D同学说:“早上的闹铃”;

可见,时钟是多么的重要。
本章节我们来了解STM32MP1的时钟系统,并分析HAL库中和时钟相关的API函数,然后通过STM32CubeMX插件生成时钟初始化代码,通过分析工程代码去学习STM32MP1的系统时钟初始化步骤。
本章将分为如下几个小节:
9.1、认识时钟树;
9.2、RCC寄存器介绍;
9.3、时钟相关API函数;
9.4、配置系统时钟实验;
9.5、实验代码分析;

9.1 认识时钟树
时钟是单片机运行的基础,相当于心跳,没有时钟,单片机就无法工作。为什么这么说呢?我们知道,寄存器是CPU内部的元件,由锁存器或触发器构成,触发器的置1、清0和置数的功能是靠其外部提供的时钟脉冲来产生跳变的,如果没有这个脉冲信号,电路就没有数字跳变,也就没有时序,单片机也就没有工作。时钟就像是人的心脏,这个恰当的比喻把时钟的重要性概括的淋漓尽致。
听说过时钟,但时钟树又是啥?前面我们说单片机的运行需要时钟脉冲来驱动,这个脉冲由源始晶体振荡器提供时钟输入,经过各级电路的处理,最终传输给各个设备,设备获得一定时钟频率以后开始工作。时钟的这种传输过程就好比一棵大树的养分供给给其它分支,树上的主干和树枝就好比时钟的总线,树叶就像是外设,时钟就是通过总线传输给设备的。
9.1.1时钟源
51单片机的一个系统时钟就可以解决一切,但STM32不一样,STM32有5个输入时钟源(Input Clock)可以用,分为2个外部时钟和3个内部时钟。
下面我们看看这几种时钟源。
(1)2个外部时钟源:
高速外部振荡器 (HSE)
可通过外接有源或者无源晶振驱动,支持 4 MHz 到 48 MHz 频率范围内的晶振。如果要使用外部晶振,在STM32CubeIDE上配置的时候,请根据板子上外部焊接的晶振频率来配置,正点原子STM32MP157开发板的核心板上外接了24MHz有源晶振。
低速外部振荡器 (LSE)
可通过外接晶振驱动,固件库中默认配置32.768 kHz(见7.4.1小节)。正点原子STM32MP157开发板的核心板上外接了32.768KHz无源晶振,主要用于驱动RTC 实时时钟和唤醒功能。
(2)3个内部时钟源:
高速内部振荡器 (HSI)
频率约为8、16、32、64 MHz,在固件库中默认配置为64 MHz。HSI比HSE具有更快的启动时间,大约几微秒。
低速内部振荡器 (LSI)
频率约为32 kHz,实际值可能会因为电压和温度而变化。主要用于看门狗。
低功耗内部振荡器 (CSI)
频率约是为4MHz,主要用于低功耗。
以上2个外部时钟源都是由芯片外部晶振产生时钟频率,其精度较高,而内部的时钟源可能因为受到温度的变化,频率发生变化,也就是存在“温飘”的情况,即使经过校准以后,精度也比外部晶振低,对于日常普通的短时间计时,使用内部时钟相差不大,对于较长时间计时就不建议使用内部时钟了。使用内部时钟可以降低成本,例如在一些白色家电中,一些家电产品为了降低成本,把电路板上外接的晶振去掉,程序改用内部时钟来工作。
大家可能会问,为什么STM32 要有多个时钟源呢?
我们知道,在同一个电路中,外设开的越多以及频率越高,功耗就越大,同时抗电磁干扰能力也会越弱。在实际的使用中我们并不是所有外设都会用到,有时候只是用到其中的某几个功能,如果打开用不到的外设,功耗会变大,而对于用到的外设,有的外设可能用不到系统时钟那么高的频率,例如看门狗或者RTC只需要几十KHz的时钟就可以工作了。ST在设计芯片的时候也是考虑到了这一点,对复杂的MCU采用多时钟源的方法来解决,并用时钟树来管理,将所有的外设时钟都关掉,用到什么外设,就使能对应的外设时钟即可。下面,我们来看看STM32MP1的时钟树。
9.1.2时钟树
在这里插入图片描述

图9.1.2. 1时钟树框图
注:
①图中的 表示动态时钟选择器,图中的D符号表示动态选择开关,即时钟选择器允许即时重新配置选择输入的时钟源,两个输入之间的过渡无干扰。仅当目标时钟源准备就绪(启动延迟或PLL锁定后时钟稳定)时,才从一个时钟源切换到另一时钟源。如果选择了尚未准备好的时钟源,则在时钟源准备就绪时进行切换。
②图中的部分时钟命名:
在这里插入图片描述

表9.1.2. 1时钟命名

  1. 外部时钟输入
    图中①处是外部时钟输入引脚,当使用HSE时,HSE在OSC_IN引脚接收外部时钟源,当使用LSE时,LSE在OSC32_IN引脚接收外部时钟源。
    在这里插入图片描述

表9.1.2. 2外部时钟输入引脚
2. MCO时钟输出
②处是MCO微控制器时钟输出(microcontroller clock output)部分。
MCO1和MCO2是时钟输出,通过芯片引脚可以给外部的芯片提供时钟,可以节省晶振,节约成本。MCO1和MCO2时钟输出通过配置RCC_MCO1CFGR寄存器来实现,具体配置可以参考手册详细说明。
时钟输出 Pin name
在这里插入图片描述

表9.1.2. 3 MCO时钟输出引脚
MCO1SEL和MCO2SEL是时钟源选择器,分别选择MCO1和MCO2的时钟来源;
MCO1DIV和MCO2DIV是时钟分频器(也叫预分频器),取值范围是1~16;
MCO1时钟源来自hse_ck、lse_ck、his_ck、lsi_ck和csi_ck,用于输出HSI,HSE,CSI,LSI或LSE时钟。
MCO2时钟源来自mpuss_ck、axiss_ck、mcuss_ck、pll4_p_ck、hse_ck和hsi_ck,用于输出MPUSS、AXISS、MCUSS、PLL4(PLL4_P)、HSE或HSI时钟。
3. 锁相环PLL
③处是和锁相环有关部分。
PLL(Phase Locked Loop) 锁相环路,其作用是使外部的输入信号与内部的振荡信号的相位同步,在STM32中主要用于分频和倍频。STM32MP1有4个锁相环,分别为PLL1、PLL2、PLL3和PLL4:
PLL1,用于向MPU子系统提供时钟;
PLL2,用于向AXI子系统、DDR和GPU提供时钟;
PLL3,用于向MCU子系统提供时钟并为外设生成内核时钟;
PLL4,用于生成外设的内核时钟。
④是PLL时钟源选择器。
锁相环输入时钟源有三个:his_ck、hse_ck 和csi_ck,分别对应HSI、HSE和CSI的时钟。我们可以配置某个锁相环选择对应的时钟源。
⑤是PLL时钟源分频器
DIVM1、DIVM2和DIVM3和DIVM4分别是PLL1、PLL2、PLL3和PLL4的分频器,取值范围都是1~64,表示将输入时钟源进行分频。
⑥是锁相环
其中PLL1是一个输入一个输出,PLL2、PLL3和PLL4是一个输入三个输出。
我们以PLL3为例子分析一下锁相环的内部结构:
VCO表示输出频率;
DIVN表示VCO的倍数;
DIVP、DIVQ和DIVR表示预分频器;
SSCG扩频时钟发生器,可以减少EMI峰值的数量,一般不用设置;
FRACV是分数倍频系数,它和DIVN一起组成PLL的倍频系数,当FRACV等于0的时候表示在整数模式下,大于0的时候表示在小数(或者说分数)模式下。
PLL输出的频率有两种计算方法,我们以PLL3输出pll3_p_ck为例,其它锁相环的输出计算方法类似,时钟源默认使用HSI,即64MHz。
在整数模式下使用PLL:

在小数模式下使用PLL:

注:
手册中公式的(DIVN + 1)其实就相当于我们上面公式中的DIVN,因为STM32CubeMX插件中是这样计算的,我们的公式中就跟STM32CubeMX插件的计算方式一致。
在这里插入图片描述

图9.1.2. 2参考手册上的公式
例如,在整数模式下(FRACV为0),当设置分频器DIVM3为4,DIVN为26,预分频器DIVP为2时,计算出pll3_p_ck为208MHz。如果再设置FRACV的值为10,根据上述计算出pll3_p_ck约为208.009766MHz。在STM32CubeMX插件上,我们手动配置这几个参数,结果和我们计算的结果一致,如下图所示:
在这里插入图片描述

图9.1.2. 3 整数模式
在这里插入图片描述

图9.1.2. 4分数模式
4. 子系统时钟
⑦处是子系统时钟,RCC处理三个子系统时钟:mpuss_ck、axiss_ck和mcuss_ck。其中mpuss_ck给MPU提供时钟,axiss_ck给AXI互联矩阵提供时钟,mcuss_ck给MCU提供时钟。上电复位后,所有的PLL和CSI以及HSE关闭,默认将HSI选择为整个系统的主时钟,因此,子系统时钟默认来自HSI。当系统运行时,用户可以选择子系统的时钟源。
我们来查看子系统部分的详细框图:
在这里插入图片描述

图9.1.2. 5子系统框图
mpuss_ck的频率最大可以是650MHz或者800MHz,这个根据所用的器件型号决定,STM32MP157A的MOU主频为650MHz,STM32MP157D的MPU主频为800MHz。 axiss_ck时钟最大是266 MHz,其经过AXIDIV分频器分频后分流为几路,给总线提供时钟。mcuss_ck时钟最大为209 MHz,经过MCUDIV分频器分频以后也是分成了几路。
pclk4和pclk5最大为133 MHz,pclk1、pclk2、pclk3最大为104.5 MHz。
子系统时钟汇总如下表格所示:
在这里插入图片描述

表9.1.2. 4子系统时钟信息表
5. CPU、AXI总线矩阵和外设时钟
在STM32CubeMX插件上,我们可以看更具体的时钟树框图,如下图:
A处是时钟源,可选外部时钟或者内部时钟,要用外部时钟的话,必选先使能外部时钟;
B处是锁相环PLL;
C处是内部时钟输出MCO1和MCO2;
黄色区域是子系统时钟;
紫色区域是专门给CA7外设提供时钟的总线时钟,分别是AXI、AHB5、AHB6、APB4、APB5,;
蓝色区域是给MPU和MCU提供时钟,其中,MPU时钟最大是650或800MHz,MCU时钟最大是209MHz,MCU有一个内核外设Systick,频率最大也是209MHz;
绿色区域中AHB1-4、APB1-3 是提供给 CM4 外设使用的;
红色区域是内核外设时钟具体配置部分,默认没有使能,是灰色,只要在 STM32CubeMX 使能了对应的外设,对应外设的时钟就会由灰色变成蓝色。
注:
RCC为整个电路提供时钟,为了避免造成误解,和参考手册一致,使用以下术语:
•外设时钟:
外设时钟是RCC提供给外设的时钟,可以为外围设备提供两种时钟:
–总线接口时钟
–内核时钟
这里的外设有分普通的外设和内核外设。外设将从RCC接收一个或多个总线接口时钟,该时钟通常是AHB,APB或AXI总线接口时钟,具体取决于外围设备连接的总线。
某些外设仅需要总线接口时钟(例如GPIOx、ETZPC、EXTI 、IPCC 、WWDG1、DMAx …)。
一些外设可能还需要专用时钟来处理接口功能, 该时钟称为内核时钟。例如,诸如SAI、ETH、FMC、GPU等这些的外设需要生成特定且准确的主时钟频率,这需要专用的内核时钟频率。
•CPU时钟
CPU时钟是提供给CPU的时钟mpuss_ck。MCU时钟是mpuss_ck。
•总线矩阵时钟
总线矩阵时钟是提供给不同桥(APB,AHB或AXI)的时钟。
图中,表示APB总线的分频器,里边的数值表示分频因子。表示是时钟源选择器。
STM32CubeMX插件上的时钟树比较形象,很好理解,图中的一些符号可能和我们参考手册上看到的不一样,不过根据字面上的意思可以知道大概对应了哪些设备。我们可以通过STM32CubeMX插件来配置时钟,然后自动生成时钟的初始化代码。
在这里插入图片描述

图9.1.2. 6 STM32CubeMX时钟树
对于内核外设时钟,我们以SPI为例子来说明一下:
在这里插入图片描述

图9.1.2. 7 SPI时钟框图
PKCS (Peripheral Kernel Clock Selection)是外设内核时钟选择单元,用于选择某个外设的内核时钟来源。PCKE(Peripheral ClocKs Enable)是外设时钟使能单元,用于是否启用时钟输出到外设中。
如上图,SPI6的内核时钟源来自于APB5总线(pclk5)、PLL(PLL3和PLL4)、HSI、CSI和HSE中的某一个,具体是哪一个由PKCS决定。其中,HSI和CSI用于低功耗中,PLL4在需要时具有更高的灵活性,例如,它允许通过PLL3更改MCU总线的频率,而不会影响某些串行接口的速度。
SPI6的PCKE有两个时钟部分,一个是来自于APB5总线接口的spi_pclk,一个是来自于内核时钟spi_ker_ck。PCKE决定是否启用SPI6的时钟,其它内核外设也是通过PCKE来开启的。由此可见,各个外设时钟基本都是可控的,不用的外设,就关闭对应的时钟,用到的外设,就根据分频器来配置,选择一个适合频率的时钟源,然后再开启外设时钟,这样的设计大大降低了功耗。
根据STM32CubeMX插件上的时钟树,ST给出了一张总线框图,如下图,从图中可以看出哪些外设挂在哪根总线上,以及总线的最大时钟频率是多少等信息。
下图中,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等)之间的连接,也称之为外围总线。
在这里插入图片描述

图9.1.2. 8 STM32MP157A/D系统总架构图
6. 时钟安全系统(CSS)
如果启用外部时钟HSE和LSE的话,硬件将启用HSE或者LSE上的CSS。我们在上面的框图以及STM32CubeMX插件上的时钟树均可以看到CSS模块。如下图,右边的是框图中的部分截图,右边的是STM32CubeMX插件上具体的截图。图中配置HSE为PLL3的时钟来源,在并将pll3_p_ck(PLL3P)作为MCU子系统的时钟源,可以看到Enable CSS模块自动变为深蓝色,表示使能了CSS。而没有使用的LSE,对应的Enable CSS模块颜色为灰色。
在这里插入图片描述

图9.1.2. 9外部时钟CSS
RCC具有时钟安全系统(CSS),允许应用程序检测外部时钟(HSE、LSE)是否由于意外原因而停止翻转。
如果使用的是HSE,在检测到故障的情况下,CSS将生成系统重置,并保护BKPSRAM的敏感数据。时钟安全系统可以由应用软件通过HSECSSON位激活,(但无法通过软件清除HSECSSON位),即使将HSEON设置为0,也可以激活HSECSSON位。 当启用并准备好HSE并且HSECSSON设置为“ 1”时,硬件将启用HSE上的CSS。当禁用HSE时, HSE上的CSS会被禁用。
如果使用的是LSE,如果检测到故障,CSS将生成故障事件,禁用提供给RTC的时钟并保护BKPSRAM的敏感数据。也可以通过应用程序将RCC_BDCR寄存器的LSECSSON位置1来激活LSE上的时钟安全系统(CSS),但必须按照以下顺序在LSE上激活CSS:
①将LSEON设置为’1’,并等待LSERDY =‘1’;
②通过RTCSRC字段选择LSE时钟;
③设置LSECSSON 到“ 1”。
关于CSS,我们这里不做深入研究,感兴趣的可以查阅参考手册以及ST官网相关介绍。
9.2 RCC寄存器介绍
下面我们来看看RCC相关的寄存器。RCC寄存器分为RCC TrustZone相关寄存器,RCC振荡器时钟相关寄存器,HSI、CSI、MCO1和MCO2、AXI、AHB、APB、PLL、MPU和MCU相关寄存器,还有外设的内核时钟选择寄存器。这么多寄存器,我们要配置哪个寄存器就查看对应寄存器即可,这里就不一一介绍所有的寄存器了。而且HAL库已经将这些寄存器封装好了,调用HAL库即可控制这些寄存器。
9.2.1 寄存器分类介绍

  1. RCC振荡器时钟相关寄存器
    RCC振荡器时钟寄存器有RCC_OCENSETR、RCC_OCENCLRR、RCC_OCRDYR。
    (1)RCC振荡器时钟使能设置寄存器(RCC_OCENSETR)
    在这里插入图片描述

图9.2.1.1 RCC_OCENSETR寄存器
RCC_OCENSETR时钟用于使能(HSE、HSI、CSI)时钟,向该寄存器写入0无效,将对应的位置1则开启对应的时钟。如果TZEN = MCKPROT =‘1’,则只能在安全模式下修改此寄存器。在时钟恢复序列期间,不允许对该寄存器进行写访问。
该寄存器配置也比较简单,以HSE为例,如要开启HSE,则先将第8位HSEON置1来开启HSE时钟,再根据是否使用旁路模式来配置第10位HSEBYP,如果使用安全模式,则配置HSECSSON。
(2)RCC振荡器时钟使能清除寄存器(RCC_OCENCLRR)
在这里插入图片描述

图9.2.1.2 RCC_OCENCLRR寄存器
该寄存器用于清除寄存器对应的位,对位写0无效操作,写1表示清除设置的位,也就是关闭对应的时钟。如果TZEN = 1,则只能在安全模式下修改此寄存器。 在时钟恢复序列期间,不允许对该寄存器进行写访问。
例如,要关闭HSE,则将HSEON位写1即可。其它寄存器操作也类似。
(3)RCC振荡器时钟就绪寄存器(RCC_OCRDYR)
在这里插入图片描述

图9.2.1.3 RCC_OCRDYR寄存器
该寄存器是一个只读访问寄存器,它包含振荡器的状态标志,写该寄存器的话是没有效果的。如果读取某位为0的话,表示该时钟未准备就绪,如果读取某位为1的话,表示该时钟已经准备就绪。例如,如果读取HSERDY为0,表示HSE时钟未准备就绪,如果为1,表示HSE时钟已经准备就绪。
2. MCO1和MCO2配置寄存器
MCO1和MO2用于配置MCO1和MCO2输出,两个寄存器的配置类似,我们以MCO1为例:
在这里插入图片描述

图9.2.1.4 RCC_MCO1CFGR寄存器
MCO1SEL[2:0]:为MCO1时钟输出选择位,由软件设置和清除。时钟源选择可能会在MCO1上产生毛刺,所以建议仅在复位后配置这些位,然后再使能外部振荡器和PLL。将该位:
0x0:选择HSI时钟(hsi_ck)作为输出;
0x1:选择HSE时钟(hse_ck)作为输出;
0x2:选择CSI时钟(csi_ck)作为输出;
0x3:选择LSI时钟(lsi_ck)作为输出;
0x4:选择LSE时钟(lse_ck)作为输出;
MCO1DIV[3:0]位用于配置MCO1的预分频器,对该位配置:
0x0:旁路模式;
0x1:分频值为2;
0x2:分频值为3;

0xF:分频值为16.
MCO1ON位用于控制MCO1输出,将该位写0则关闭MCO1输出,写1则开启MCO1输出。
3. MPU和MCU时钟源选择寄存器
STM32MP157有两个A7内核和一个M4内核,RCC_MPCKSELR和RCC_MSSCKSELR分别用于选择MPU和MCU的时钟源。我们以MCU为例:
(1)RCC MCU子系统时钟选择寄存器(RCC_MSSCKSELR)
在这里插入图片描述

图9.2.1.5 RCC_MSSCKSELR寄存器
MCUSSRC[1:0]位用于选择MCU子系统的时钟(MCUSS)来源,通过配置该位可以选择MCUSS可以来自:
0x0: 配置MCUSS时钟源来自HSI时钟(hsi_ck);
0x1: 配置MCUSS时钟源来自HSE (hse_ck);
0x2:配置MCUSS时钟源来自CSI (csi_ck);
0x3:配置MCUSS时钟来自PLL3 (pll3_p_ck)
MCUSSRCRDY属于只读位,读取该位为1,表示MCUSS已经准备好,可以切换到MCUSSRC配置的时钟;读取该位为0表示MCUSS未准备好。

通过软件置1和清除,以配置MCO1的预分频器。 修改该预分频器可能会在MCO1上产生毛刺。 强烈建议仅在复位后才更改该预分频器,然后再使能外部振荡器和PLL。

有关详细信息,请参见第10.4.4节:时钟输出生成(MCO1和MCO2)。
(2)RCC MCU时钟分频器寄存器(RCC_MCUDIVR)
在这里插入图片描述

图9.2.1.6 RCC_MCUDIVR寄存器
MCUDIV[3:0]用于配置MCU时钟的分频值,配置该位:
0x0:MCU子系统时钟(也可以写为MCUSS或者mcuss_ck)不分频;
0x1:2分频;
0x2:4分频;
0x3:8分频;

0x8:256分频。
其它配置:512分频
MCUDIVRDY属于只读位,用于指示是否考虑了新的分频系数,该位:
0:尚未考虑新的除法系数;
1:考虑新的除法系数。(重置后的默认值)
4. HSI/CSI时钟配置寄存器
HSI和CSI时钟配置寄存器由RCC_HSICFGR和RCC_CSICFGR来控制,用于配置HSI和CSI的时钟源选择和分频器分频值等。
(1)RCC HSI配置寄存器(RCC_HSICFGR)
在这里插入图片描述

图9.2.1.7 RCC_HSICFGR寄存器
HSIDIV[1:0]用于配置HSI的时钟分频值:
0x0:配置HSI为1分频,hsi_ck (hsi_ker_ck) = 64 MHz;
0x1:配置HSI为2分频,hsi_ck (hsi_ker_ck) = 32 MHz;
0x2:配置HSI为4分频,hsi_ck (hsi_ker_ck) = 16 MHz;
0x3:配置HSI为8分频,hsi_ck (hsi_ker_ck) = 8 MHz;
HSICAL[11:0]和HSITRIM[6:0]:用于配置时钟校准调整值,这两位我们可以不用管。
5. AXI/PLL1/PLL2/PLL3/PLL4时钟选择
AXI/PLL1/PLL2/PLL3/PLL4都有对应的时钟选择寄存器,用于选择其时钟源。
AXI子系统的时钟可以选择HSI、HSE、PLL2;
PLL1和PLL2时钟可以来自HSI、HSE;
PLL3时钟可以来自HSI、HSE、CSI;
PLL4时钟可以来自HSI、HSE、CSI和I2S_CKIN。
以上相关时钟源选择配置寄存器的配置也比较简单,我们就不一一举例子了,如需要,大家按照要求来配置即可。
5. APB、AXI分频寄存器
APB1APB5以及AXI都有时钟分频器,可以设置时钟分频值。AXI时钟分频器可以配置分频值为1、2、3和4。APB1APB5分频器的分频值可以配置为2、4、8和16。相关寄存器的配置也比较简单,我们这里就不一一介绍了。
6. PLL控制和配置寄存器
PLL1~PLL4有专门的控制寄存器和配置寄存器,我们这里以PLL1为例进行介绍。
(1)RCC PLL1控制寄存器(RCC_PLL1CR)
在这里插入图片描述

图9.2.1.8 RCC_PLL1CR寄存器
PLLON位用于使能PLL1,将该位置1表示使能PLL1,复位表示将PLL1关闭;
PLL1RDY表示PLL1时钟就绪标志位,属于只读位。该位为1表示PLL1处于锁的状态,为0表示PLL1处于未锁的状态;
DIVPEN表示PLL1 DIVP分频器输出使能位,通过软件置1和复位,以使能PLL1的pll1_p_ck输出。该位为0,表示pll1_p_ck输出被禁用,为1,表示pll1_p_ck输出启用。为了节省功耗,当不需要pll1_p_ck时,必须将DIVPEN和DIVP设置为’0’。
DIVQEN是PLL1 DIVQ分频器输出使能位,该位为0,表示pll1_q_ck输出被禁用;该位为1,表示设置pll1_q_ck输出已启用。
DIVREN表示PLL1 DIVR分频器输出使能位,该位为0,表示pll1_r_ck输出被禁用;该位为1,表示设置pll1_r_ck输出启用。
(2)PLL1配置寄存器(RCC_PLL1CFGR1和RCC_PLL1CFGR2)
PLL1配置寄存器有2个,其它PLL配置寄存器也是有2个。PLL1配置寄存器1(RCC_PLL1CFGR1)如下:
在这里插入图片描述

图9.2.1.9 RCC_PLL1CFGR1寄存器
DIVN[8:0]位用于配置PLL1 VCO的倍数,将该位配置为:
0x18:分频比为25;
0x19:分频比为 26;

0x31: 分频比为50 (default after reset);

0x63: 分频比为100。
PLL1配置寄存器1(RCC_PLL1CFGR2)如下:
在这里插入图片描述

图9.2.1.10 RCC_PLL1CFGR2寄存器
DIVP[6:0]位、DIVQ[6:0]位和DIVR[6:0]位分别用于配置PLL1 DIVP、PLL1 DIVQ和PLL1 DIVR的分频系数,要配置为什么样的分频值,根据手册来配置即可。
7. PLL小数寄存器
我们前面也说过,锁相环PLL的 FRACV是分数倍频系数,它和DIVN一起组成PLL的倍频系数,当FRACV等于0的时候表示在整数模式下,大于0的时候表示在小数(或者说分数)模式下。每个PLL都有其配置小数寄存器。我们以PLL1为例子。RCC PLL1小数寄存器(RCC_PLL1FRACR)如下:
在这里插入图片描述

图9.2.1.11 RCC_PLL1FRACR寄存器
FRACLE是PLL1分数使能位,该位必须先设置为0然后设置为1,这样,通过从0到“1”的转换将FRACV的内容传输到调制器中。
FRACV[12:0]是PLL1的乘法因子的小数部分。FRACV可介于0和213-1之间。
9.2.2 外设时钟使能和关闭
我们要注意,STM32MP1外设的时钟在上电复位以后默认处于关闭状态,即这些外设都不可用,这么做的目的就是为了降低功耗,如果我们要使用某个外设,必须要先打开外设的时钟才可以对外设进行操作。
在RCC外设相关寄存器中,我们会看到有两种寄存器,一个是以“SETR”结尾的寄存器,一个是以“CLRR”结尾的寄存器,前者是使能寄存器,后者是关闭寄存器。
例如,APB1总线挂载的设备有窗口看门狗(WWDG)、I2C1~ I2C3、USART2、UART8和TIM2~TIM7等等外设,如果要使用WWDG,就必须先开启WWDG的时钟。对于MCU,APB1外设的时钟由RCC_MC_APB1ENSETR寄存器来开启,其第28位是WWDG时钟使能位,对该位:
写’0’表示无效操作,读取该位,读到’0’则表示该外设时钟已经被关闭;
写’1’表示开启外设时钟,读取该位,读取到’1’意味着外设时钟已经开启了。
在这里插入图片描述

图9.2.2.1 MC_APB1ENSETR寄存器
另外一个寄存器RCC_MP_APB1ENCLRR用于关闭APB1外设的时钟,我们看看此寄存器:
在这里插入图片描述

图9.2.2.2 MP_APB1ENCLRR寄存器
可以发现,并没有看到控制WWDG的时钟位,第28位是保留的,而其它外设,例如I2C1~ I2C3、USART2、UART8等对应的位有看到,对这些位:
写’0’表示无效操作,读取到’0’表示已经关闭外设的时钟了;
写’1’表示关闭外设时钟,读取到’1’表示已经开启外设时钟了;
为什么窗口看门狗(WWDG)在RCC_MC_APB1ENCLRR寄存器中没有对应的时钟操作位呢?因为WWDG这个外设比较特殊,只要开启窗口看门狗以后就不能将其关闭了,必须等到系统重置以后才可以禁用窗口看门狗,而其它外设就不一样了,其它外设可以通过对时钟操作位写1来将其时钟关闭。
在HAL库的stm32mp1xx_hal_rcc.h头文件中有所有外设时钟开启或者关闭宏定义:
在这里插入图片描述

图9.2.2.3 外设时钟使能/禁用宏定义
例如,WWDG部分:
1 #define __HAL_RCC_WWDG1_CLK_ENABLE() (RCC->MC_APB1ENSETR = \ RCC_MC_APB1ENSETR_WWDG1EN)
2 #define __HAL_RCC_WWDG1_CLK_DISABLE() (RCC->MC_APB1ENCLRR = \ RCC_MC_APB1ENCLRR_WWDG1EN)
RCC_MC_APB1ENSETR_WWDG1EN和RCC_MC_APB1ENCLRR_WWDG1EN在stm32mp157axx_cm4.h头文件中有定义:
#define RCC_MC_APB1ENSETR_WWDG1EN B(28)
#define RCC_MC_APB1ENCLRR_WWDG1EN B(28)
B(28)则表示将第28位置1,所以(RCC->MC_APB1ENSETR = RCC_MC_APB1ENSETR_WWDG1EN)就表示将RCC_MC_APB1ENSETR寄存器的第28位写1,也就是开启WWDG这个外设的时钟。在代码中,要开启WWDG的时钟的话,直接调用__HAL_RCC_WWDG1_CLK_ENABLE()这个宏就可以实现,其它的所有外设的时钟开启都可以类推。
同理,(RCC->MC_APB1ENCLRR = RCC_MC_APB1ENCLRR_WWDG1EN)表示关闭WWDG的时钟,不过因为WWDG特殊,系统运行的时候是不能将WWDG时钟关闭的,所以寄存器RCC_MC_APB1ENCLRR并没有针对WWDG的操作位,所以调用__HAL_RCC_WWDG1_CLK_DISABLE()这个宏是没什么实际意义的,不过对于其它一般的外设,例如I2C3、USART2、UART8等,调用对应的宏就可以将外设时钟关闭了。
又例如GPIOA~GPIOK是挂在AHB4总线上的,在HAL库的stm32mp1xx_hal_rcc.h头文件中同样有GPIO时钟开启和关闭宏定义,操作方法也和上面的外设类似。
在这里插入图片描述

图9.2.2.4 外设时钟使能/禁用宏定义
事实上,用STM32CubeMX插件生成的初始化代码中,也就是通过调用这些宏来开启外设时钟的。
9.3 时钟相关API函数
头文件:stm32mp1xx_hal_rcc.h。
stm32mp1xx_hal_rcc.c是RCC相关的HAL模块驱动程序文件,主要管理复位、初始化、取消初始化和外设时钟控制的功能。
复位后,设备从内部高速时钟(HSI 64MHz)运行,并且所有外设均关闭,但内部SRAM1,SRAM2,SRAM3和PWR除外。高速(AHB)和低速(APB)总线上没有预分频,这些总线上映射的所有外设均以HSI速度运行。
设备复位后,如果用户需要更高的频率/性能,需要进行如下操作:
①选择用于驱动MPU、AXI和MCU子系统的时钟源;
②配置AHB和APB总线分频器;
③对于那些不是从总线获得时钟的内核外设需要配置内核时钟源;
④启用要使用的外设的时钟。
不管使用哪个时钟源,都是通过软件对相关寄存器的操作来实现的,我们直接通过HAL库的API函数来实现控制寄存器操作,下面,我们来分析一下stm32mp1xx_hal_rcc.c文件中的API函数。
9.3.1 函数HAL_RCC_DeInit
●函数功能:主要就是将RCC时钟配置为默认重置状态,主要做了以下操作:
①将HSION位置1,将HSI用作系统时钟源;
②将MCO1、MCO2HSE、PLL1、PLL2、PLL3和PLL4关闭;
③AHB,APB总线预分频器设置为1;
④禁止所有中断(从CSTOP中断允许唤醒除外)。
●函数返回值:
枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)
●注意:此功能不会修改外设、LSI、 LSE 和 RTC时钟,不会更改HSECSS(HSE时钟安全系统HSE Clock security system)和LSECSS(LSE Clock security system)。
函数HAL_RCC_DeInit的部分代码如下,因为代码很多,我们省略掉了部分代码。

1   HAL_StatusTypeDef HAL_RCC_DeInit(void)
2   {
3     uint32_t tickstart;
4 
5     /* 将HSION位置1,使能HSI振荡器 */
6     SET_BIT(RCC->OCENSETR, RCC_OCENSETR_HSION);
7 
8     /* 获取全局变量uwTick当前计算值 */
9     tickstart = HAL_GetTick();
10
11    /* 等待HSI准备就绪 */
12    while ((RCC->OCRDYR & RCC_OCRDYR_HSIRDY) == 0U)
13    {
14      if ((HAL_GetTick() - tickstart) > HSI_TIMEOUT_VALUE)
15      {
16        return HAL_TIMEOUT;
17      }
18    }
19    
20  /*
21  *省略掉代码......
22  *关闭MCO1、MCO2HSE、PLL1、PLL2、PLL3和PLL4
23  */
24   
25    /* 等待HSIDIV分频器就绪*/
26    while ((RCC->OCRDYR & RCC_OCRDYR_HSIDIVRDY) == 0U)
27    {
28      if ((HAL_GetTick() - tickstart) > HSI_TIMEOUT_VALUE)
29      {
30        return HAL_TIMEOUT;
31      }
32    }
33  /******省略掉代码******/
34    /* 更新SystemCoreClock全局变量 */
35    SystemCoreClock = HSI_VALUE;
36
37    /* 调整Systick中断时间 */
38    if (HAL_InitTick(uwTickPrio) != HAL_OK)
39    {
40      return HAL_ERROR;
41    }
42
43    return HAL_OK;
44  }
我们简单分析这部分代码:
第6行,通过SET_BIT置位操作,将RCC_OCENSETR寄存器的HSEON位置1,表示使能HSI振荡器。
第9到,先获取全局变量uwTick当前计算值。
第12行,等待HSI是否已经就绪。

注:RCC_OCRDYR是振荡器时钟就绪寄存器
①第0位HSIRDY是HSI时钟就绪标志,由硬件置1,以指示HSI振荡器稳定,如果此位为0,表示HSI时钟未准备好,如果此位为1,表示HSI时钟已准备好。
②第2位HSIDIVRDY是分频器就绪标志位,由硬件置位和复位。如果此位为0,表示新的分频比尚未传播到hsi_ck(hsi_ker_ck)(复位后默认),如果此位为1,表示hsi_ck(hsi_ker_ck)时钟频率反映了新的HSIDIV值,即分频器已经准备好。
③第4位CSIRDY是CSI时钟就绪标志,由硬件置1,以指示CSI振荡器稳定。如果此位为0,表示CSI时钟未准备好(复位后的默认值),如果此位为1,表示CSI时钟已准备好。
④第8位HSERDY表示HSE时钟就绪标志由硬件置1,指示HSE振荡器稳定。如果此位为0,表示HSE时钟未准备好(复位后的默认值),如果此位为1,HSE时钟已准备好。
在这里插入图片描述

图9.3.1. 1 RCC_OCRDYR寄存器
代码中,RCC_OCRDYR_HSIRDY值为0x00000001。如果(RCC->OCRDYR & RCC_OCRDYR_HSIRDY) == 0U,表示HSI振荡器未稳定,程序会卡在while循环中。如果两次取得的全局变量uwTick的差值大于HSI_TIMEOUT_VALUE(100ms),表示HSI超时,程序返回HAL_TIMEOUT标志并退出while循环。
第26到第32行,表示等待HSIDIV分频器就绪。
第35行,更新SystemCoreClock全局变量的值为HSI_VALUE。SystemCoreClock变量用于存储系统时钟频率,可以用来设置SysTick定时器或配置其他参数,每次时钟变化时,都应该更新它,这样也是为了保证SystemCoreClock的准确性。我们在第6.3.4小节system_stm32mp1xx.c文件中有分析过。
第38到第41行,调用HAL_InitTick函数更新Systick中断时间(1ms),因为很多地方要用systick作为时基源,所以时钟源变化后,要将其更新。如果HAL_InitTick函数没有运行成功,程序将返回HAL_ERROR。HAL_InitTick函数我们在7.4.2小节的stm32mp1xx_hal.c文件中有分析过。
9.3.2 函数HAL_RCC_OscConfig
●函数功能:主要就是配置 HSE、HSI、LSI、LSE 和 PLL(PLL1、PLL2、PLL3和PLL4)。
●函数返回值:
枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)
注意:当PLL用作系统时钟时,PLL不会被禁用。
函数部分代码如下:

1   __weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef \  																*RCC_OscInitStruct)
2   {
3     uint32_t tickstart;
4     HAL_StatusTypeDef result = HAL_OK;
5 
6     /* 检查是否是空指针 */
7     if (RCC_OscInitStruct == NULL)
8     {
9       return HAL_ERROR;
10    }
11
12    /* 使用断言检查参数 */
13    assert_param(IS_RCC_OSCILLATORTYPE(RCC_OscInitStruct->OscillatorType));
14  
15    /******省略HSE、HSI、CSI、LSI、LSE、PLL配置代码 ******/ 
16    return HAL_OK;
17  }
18
weak表示函数是弱定义,用户可以在其它地方重新定义一个同名的函数。函数参数是RCC_OscInitTypeDef 类型结构体变量,主要对RCC内部/外部振荡器(HSE,HSI,CSI,LSE和LSI)配置的结构定义,RCC_OscInitStruct是指向RCC_OscInitTypeDef结构的指针,RCC_OscInitTypeDef 类型结构体声明如下,通过指针可以操作结构体中的成员变量。例如,如果要选择HSE为振荡器:
①设置OscillatorType的值为RCC_OSCILLATORTYPE_HSE;
②然后设置HSEState的值为RCC_HSE_ON开启HSE;
③如果用到PLL,则配置对应PLL的参数。
对于其它的时钟源(HSI、LSI、LSE和CSI)配置方法类似。
stm32mp1xx_hal_rcc.h文件代码
    typedef struct
    {
      uint32_t OscillatorType; 				/* 要配置的振荡器 */                                                     
      uint32_t HSEState;   					/* HSE的新状态 */                                                                   
      uint32_t LSEState; 						/* LSE的新状态 */                                                                       
      uint32_t HSIState;   					/* HSI的新状态 */                                                                   
      uint32_t HSICalibrationValue; 			/* 校准调整值 */                                      
      uint32_t HSIDivValue;  					/* HSI的分频系数 */                                             
      uint32_t LSIState;  					/* LSI的新状态 */                                                 
      uint32_t CSIState;  					/* CSI的新状态 */                                                 
      uint32_t CSICalibrationValue; 			/* 校准调整值 */                                       
      RCC_PLLInitTypeDef PLL; 				/* PLL1结构参数: */      
      RCC_PLLInitTypeDef PLL2;				/* PLL2结构参数 */     
      RCC_PLLInitTypeDef PLL3; 				/* PLL3结构参数 */     
      RCC_PLLInitTypeDef PLL4; 				/* PLL4结构参数 */                                                  
    } RCC_OscInitTypeDef;
我们查看PLL的RCC_PLLInitTypeDef结构参数有哪些,如下,此结构体定义了锁相环PLL中用到的DIVM、DIVP、DIVQ、DIVR、FRACV等参数变量,通过给这些结构体成员赋值即可配置PLL的时钟。
stm32mp1xx_hal_rcc.h文件代码
        typedef struct
        {
          uint32_t PLLState;   		/* PLL的新状态 */                                              
          uint32_t PLLSource;  		/* PLL输入时钟源 */                            
          uint32_t PLLM;       		/* PLL VCO输入时钟的分频系数DIVM */                           
          uint32_t PLLN;       		/* PLL VCO输出时钟的倍数DIVN */                            
          uint32_t PLLP;       		/* 分频因子DIVP */                          
          uint32_t PLLQ;       		/* 分频因子DIVQ */                           
          uint32_t PLLR;       		/* 分频因子DIVR */                           
          uint32_t PLLRGE;     		/* PLL3和PLL4的PLL输入频率范围 */                          
          uint32_t PLLFRACV;   		/* PLL1 VCO乘数的小数部分FRACV */                          
          uint32_t PLLMODE;   			/* 使用PLL模式 */                            
          uint32_t MOD_PER;   			/* 调制周期调整 */                          
          uint32_t RPDFN_DIS;  		/* 抖动的RPDF噪音控制 */                            
          uint32_t TPDFN_DIS;  		/* 抖动的TPDF噪声控制 */                          
          uint32_t SSCG_MODE;  		/* 扩频时钟发生器模式 */                     
          uint32_t INC_STEP;   		/* 调制深度调整*/                           
        } RCC_PLLInitTypeDef;
在HSE、HSI、CSI、LSI、LSE、PLL配置代码部分,我们主要分析HSE、HSI和PLL配置部分。
  1. HSE配置部分
1   if (((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_HSE) \
												== RCC_OSCILLATORTYPE_HSE)
2     {
3       /* 使用断言检查检查参数 */
4       assert_param(IS_RCC_HSE(RCC_OscInitStruct->HSEState));
5       /* 当HSE在系统中的某处使用时,将不会被禁用 */
6       if (IS_HSE_IN_USE())
7       {
8         if ((__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) != RESET) && \ 									(RCC_OscInitStruct->HSEState != RCC_HSE_ON))
9         {
10          return HAL_ERROR;
11        }
12      }
13      else
14      {
15        /* 配置HSE振荡器 */
16        result = HAL_RCC_HSEConfig(RCC_OscInitStruct->HSEState);
17        if (result != HAL_OK)
18        {
19          return result;
20        }
21      }
22    }
第1行,通过RCC_OscInitStruct指针判断振荡器是否选中了HSE,如果选中了HSE,将进行后续的配置操作。
第4行,使用断言检查HSE参数的状态设置是否正确。
第6到第12行,判断HSE是否有在使用中,如果有在使用,则不会被禁用。	(__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) != RESET)用于判断RCC_OCRDYR振荡器时钟就绪寄存器中的HSERDY位是否为0,为0的话表示HSE时钟未准备好(复位后的默认值也是0)。
(RCC_OscInitStruct->HSEState != RCC_HSE_ON)表示HSE的状态没有被开启。
如果HSERDY位为0,且HSE的状态没有开启,那么HSE没有被使用,程序返回HAL_ERROR。是否已经开启HSE,这取决于用户,所以程序要通过判断HSERDY位以及用户的设置状态来判断到底HSE用还是没用。
第16行,调用HAL_RCC_HSEConfig函数完成HSE振荡器的初始化。
程序中主要实现操作3个寄存器,一个是上面提到的RCC_OCRDYR,另外两个是RCC_OCENCLRR核RCC_OCENSETR寄存器。

注:
RCC_OCENCLRR寄存器用于控制振荡器,向该寄存器写入0无效,写入1将清除相应的位。
第0位,HSION:写1表示清除HSION位,禁用HSI。
第4位,CSION:写1表示清除CSION位,禁用CSI。
第7位,DIGBYP:当连接到OSC_IN的外部时​​钟为低摆幅信号时,软件将其清零。写1表示清除DIGBYP位(模拟旁路),禁用
第8位,HSEON:写1表示清除HSEON位,禁用HSE。
RCC_OCENSETR寄存器的功能和RCC_OCENCLRR寄存器的功能相反,RCC_OCENSETR寄存器表示开启对应的功能。
在这里插入图片描述

图9.3.2. 1 RCC_OCENCLRR寄存器
代码中附上了详细的注释,通过注释可以知道程序的实现过程:
①在配置HSE前,先关闭HSE,如果已经使用了HSE,应先切换成别的时钟源,然后再关闭HSE,最后才可以配置HSE。
②程序中通过将RCC振荡器时钟使能清除寄存器(RCC_OCENCLRR)的HSEON位写入“ 1”来禁用HSE。
③通过验证RCC_OCRDYR的HSERDY位是否设置为’0’,检查HSE是否被禁用。
④通过将RCC振荡器时钟使能清除寄存器(RCC_OCENCLRR)的HSEBYP位写入’1’,禁用HSE旁路模式。
⑤通过将RCC振荡器时钟使能设置寄存器(RCC_OCENSETR)的HSEON位写入“ 1”来再次使能HSE
⑥检查RCC_OCRDYR的HSERDY位是否设置为“ 1”,然后准备使用HSE。

1   /**
2    *@brief 根据指定的参数初始化RCC HSE振荡器。
3    * @note  在更改HSE配置之前,请注意不要将HSE振荡器用作时钟,也就是要先关闭HSE,
4    *         如果使用了HSE,则必须选择另一个源时钟,然后更改HSE状态(例如:禁用它)。       
5    *@note   进入STOP和STANDBY模式时,HSE由硬件停止。
6    *@note   此功能会重置CSSON位,因此,如果先前启用了时钟安全系统(CSS),
7              则必须在调用此功能后再次启用CCS。
8    *@param  状态包含RCC HSE振荡器的配置。
9    *         此参数可以是下列值之一:
10   *         @arg RCC_HSE_OFF:
11   *         关闭HSE振荡器
12   *         @arg RCC_HSE_ON: 
13   *         打开HSE振荡器
14   *         @arg RCC_HSE_BYPASS:
15   *         使用提供给OSC_IN的低摆幅模拟信号(旁路时钟),
16             HSE振荡器被外部时钟旁路
17   *         @arg RCC_HSE_BYPASS_DIG:
18   *         使用提供给OSC_IN的全摆幅数字信号(数字旁路时钟),
19             HSE振荡器被外部时钟旁路
20   * @retval HAL status
21   */
22  HAL_StatusTypeDef HAL_RCC_HSEConfig(uint32_t State)
23  {
24    uint32_t tickstart;
25    
26    /* 检查参数 */
27    assert_param(IS_RCC_HSE(State));
28 
29    /* 在配置HSE之前禁用HSEON */
30    WRITE_REG(RCC->OCENCLRR, RCC_OCENCLRR_HSEON);
31 
32    /* 获取全局变量uwTick当前计算值 */
33    tickstart = HAL_GetTick();
34 
35    /* 等待直到禁用HSE  */
36    while (__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) != RESET)
37    {
38      if ((HAL_GetTick() - tickstart) > HSE_TIMEOUT_VALUE)
39      {
40        return HAL_TIMEOUT;
41      }
42    }
43 
44    /* 清除剩余的位 */
45    WRITE_REG(RCC->OCENCLRR, (RCC_OCENCLRR_DIGBYP | \ 	RCC_OCENSETR_HSEBYP));
46 
47    /* 如果需要,启用HSE */
48    if (State != RCC_HSE_OFF)
49    {
50      /* 如果设置的是旁路时钟,则启用旁路模式 */
51      if (State == RCC_HSE_BYPASS)
52      {
53        SET_BIT(RCC->OCENSETR, RCC_OCENSETR_HSEBYP);
54      }
55      /* 如果设置的是数字旁路时钟,则启用数字旁路模式 */
56      else if (State == RCC_HSE_BYPASS_DIG)
57      {
58        SET_BIT(RCC->OCENSETR, RCC_OCENCLRR_DIGBYP);
59        SET_BIT(RCC->OCENSETR, RCC_OCENSETR_HSEBYP);
60      }
61 
62      /* 启用HSE */
63      SET_BIT(RCC->OCENSETR, RCC_OCENSETR_HSEON);
64 
65      /* 获取全局变量uwTick当前计算值 */
66      tickstart = HAL_GetTick();
67 
68      /* 等待HSE准备就绪  */
69      while (__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) == RESET)
70      {
71        if ((HAL_GetTick() - tickstart) > HSE_TIMEOUT_VALUE)
72        {
73          return HAL_TIMEOUT;
74        }
75      }
76    }
77 
78    return HAL_OK;
79  }
  1. HSI配置部分
    关于HSI的配置部分,有以下两点:
    ①RCC_OCRDYR寄存器的HSIRDY标志指示HSI是否稳定。 启动时,在硬件将HSIRDY位置1之前,不会释放HSI输出时钟。应用程序还可以通过位于RCC振荡器时钟使能设置寄存器(RCC_OCENSETR)和RCC振荡器时钟使能清除寄存器(RCC_OCENCLRR)中的HSION位来控制HSI激活。
    ②HSI的预分频器由位于HSI配置寄存器RCC_HSICFGR中的HSIDIV位控制。RCC振荡器时钟就绪寄存器RCC_OCRDYR可以使用标志HSIDIVRDY位来检查硬件何时考虑新的分频比。HSIDIV值可以即时更改,并且HSI将考虑新的分频比,但是,必须考虑:
    如果HSI当前用作PLL的参考时钟,则不允许更改HSIDIV。
    如果当前将HSI时钟用作某些外围设备的内核时钟,则应用程序必须确保HSI频率变化不会干扰外围设备。
    HSI配置部分的代码框架如下,代码比较多,这里省略了代码。
1   if (((RCC_OscInitStruct->OscillatorType) & RCC_OSCILLATORTYPE_HSI) \
 												== RCC_OSCILLATORTYPE_HSI)
2   {
3      /* 使用断言检查检查参数是否符合范围*/
4      /****** 省略部分代码 ******/
5   /* 
6    * 当HSI被使用时(在AXI、MPU、MCU和PLL1~pLL4使用),它不会被禁用,允许校准。
7    * 1、如果HSI当前用作PLL的参考时钟,则不允许更改HSIDIV的置。
8    * 2、如果HSI没有被当做PLL的参考时钟,则更新HSIDIV值,
9    * 同时,更新SystemCoreClock全局变量。
10   */
11     if (IS_HSI_IN_USE())
12      {
13        /****** 省略部分代码 ******/
14      }
15  /* 
16   * 如果HSI没有在AXI、MPU、MCU和PLL1~pLL4使用:
17   * 1、确定HSI没有被使用,如果检查到HSIState为开启状态(一般是用户开启),先启
18   * 用HSI,等待HSI准备就绪后,更新HSIDIV值,然后整内部高速振荡器(HSI)校准值。
19   * 
20   * 2、如果如果检查到HSIState为关闭状态(一般是用户关闭),则禁用HSI。
21   */
22      else
23      {
24        /****** 省略部分代码 ******/
25      }
26  }
  1. PLL配置部分
    在启用PLL之前,必须先完成以下PLL配置:
    选择PLL时钟输入(HSI或CSI或HSE)
    PLL时钟频率输入范围(配置DIVM参数)
    一旦启用了PLL,DIVMx,DIVNx,DIVPx,DIVQx和DIVRx这些参数就无法更改如果用户想要更改PLL参数,则他必须禁用相关的PLL(PLLxON = 0),并等待PLLxRDY标志为0才可以(这里的x等于1~4)。
    配置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等参数。
1   /* 配置PLL1 */
2   result = RCC_PLL1_Config(&(RCC_OscInitStruct->PLL));
3   if (result != HAL_OK)
4   {
5   return result;
6   }
7 
8   /* 配置PLL2 */
9   result = RCCEx_PLL2_Config(&(RCC_OscInitStruct->PLL2));
10  if (result != HAL_OK)
11  {
12  return result;
13  }
14
15  /* 配置PLL3 */
16  result = RCCEx_PLL3_Config(&(RCC_OscInitStruct->PLL3));
17  if (result != HAL_OK)
18  {
19  return result;
20  }
21
22  /* 配置PLL4 */
23  result = RCCEx_PLL4_Config(&(RCC_OscInitStruct->PLL4));
24  if (result != HAL_OK)
25  {
26  return result;
27  }

9.3.3 函数HAL_RCC_ClockConfig
●函数功能:根据RCC_ClkInitStruct中指定的参数初始化MPU,AXI,AHB和APB总线时钟。
●函数返回值:
枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)
注意:
①系统从上电复位、停机或者待机唤醒以后,或者在HSE发生故障的情况下,则直接或间接将HSI直接或间接用作系统时钟。
②仅当目标时钟源准备就绪(启动延迟或PLL锁定后时钟稳定)时,才会从一个时钟源切换到另一时钟源。如果选择了尚未准备好的时钟源,则当时钟源准备就绪时将进行切换。
③根据设备电压范围,软件必须正确设置HPRE [3:0]位以确保HCLK不超过最大允许频率。

1   HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef \ 	*RCC_ClkInitStruct)
2   {
3 
4     HAL_StatusTypeDef status = HAL_OK;
5     uint32_t tickstart;
6 
7     /* 检查是否是检查空指针 */
8     if (RCC_ClkInitStruct == NULL)
9     {
10      return HAL_ERROR;
11    }
12
13    assert_param(IS_RCC_CLOCKTYPETYPE(RCC_ClkInitStruct->ClockType));
14    /******省略 MPU、AXISS模块配置代码******/
15    /******省略 APB1~APB5总线的分频器APB1DIV~APBDIV5的代码******/
16    
17    return HAL_OK;
18  }
参数RCC_ClkInitStruct是指向RCC_ClkInitTypeDef结构的指针,我们查看此结构体。如下,此结构体主要用于设置时钟源以及MPU、AXI子系统、MCU子系统和APB1~APB5的分频系数。

stm32mp1xx_hal_rcc.h文件代码

typedef struct

{
uint32_t ClockType; /* 时钟源选择 /
RCC_MPUInitTypeDef MPUInit; /
MPU结构参数(时钟源和分频数) /
RCC_AXISSInitTypeDef AXISSInit; /
AXI结构参数(时钟源和分频数)/
RCC_MCUInitTypeDef MCUInit; /
APB4分频数 /
uint32_t APB4_Div; /
APB4分频数 /
uint32_t APB5_Div; /
APB5分频数 /
uint32_t APB1_Div; /
APB1分频数 /
uint32_t APB2_Div; /
APB2分频数 /
uint32_t APB3_Div; /
APB3分频数 */
} RCC_ClkInitTypeDef;

9.3.4 函数HAL_RCC_GetSystemCoreClockFreq
●函数功能:根据所选的时钟源以及预定义的常量来返回系统核心频率。
●函数返回值:
MCU或者MPU时钟频率。
注意:
①我们说的系统时钟频率,其实就是MCU或者MPU的时钟频率。
②每次MCU/MPU更改时,必须调用此函数以更新正确的值,否则,基于此功能的任何配置都将不正确。我们前面说的SystemCoreClock全局变量就是通过此函数来获取最新的系统时钟频率的:
SystemCoreClock = HAL_RCC_GetSystemCoreClockFreq();
我们后面的实验会通过HAL_RCC_GetSystemCoreClockFreq函数来获取系统时钟频率。,函数如下。函数中,通过宏定义CORE_CA7来选择MPU或者MCU的时钟,其中HAL_RCC_GetMPUSSFreq函数返回MPU的时钟,HAL_RCC_GetMCUFreq函数返回MCU的时钟。

1     uint32_t HAL_RCC_GetSystemCoreClockFreq(void)
2     {
3     #ifdef CORE_CA7
4       return HAL_RCC_GetMPUSSFreq();
5     #else /* CORE_CM4 */
6       return HAL_RCC_GetMCUFreq();
7     #endif
8     }

9.3.5 HAL_RCCEx_PeriphCLKConfig
●函数功能:根据RCC_PeriphCLKInitTypeDef中的指定参数初始化RCC外设时钟。
●函数返回值:
枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)
可以通过此函数可以指定配置哪个外设以及外设的时钟源是哪个,后面的外设实验中,我们会接触此函数。该函数声明如下:

HAL_StatusTypeDef HAL_RCCEx_PeriphCLKConfig(RCC_PeriphCLKInitTypeDef
                                            *PeriphClkInit)
	PeriphClkInit是指向包含字段PeriphClockSelection的RCC_PeriphCLKInitTypeDef结构的指针,该字段可以指定是哪个外设。例如,RCC_PERIPHCLK_UART24指定USART2和UART4,RCC_PERIPHCLK_I2C12指定I2C1和I2C2。
typedef struct
{
  uint64_t PeriphClockSelection;   /* 外设时钟源选择 */
  RCC_PLLInitTypeDef PLL2;          /* PLL2结构参数 */      
  RCC_PLLInitTypeDef PLL3;          /* PLL3结构参数 */
  RCC_PLLInitTypeDef PLL4;          /* PLL4结构参数 */
  /* 指定I2C1/2时钟源,该参数可以是RCC_PERIPHCLK_I2C12 */
  uint32_t I2c12ClockSelection;    
/********* 省略部分代码 *********/
/* 指定UART2/4时钟源,该参数可以是RCCEx_UART24_Clock_Source */
  uint32_t Uart24ClockSelection;  
/* 指定UART3/5时钟源,该参数可以是RCCEx_UART35_Clock_Source */   
  uint32_t Uart35ClockSelection;  
/* 指定UART6时钟源,该参数可以是RCCEx_USART6_Clock_Source */   
  uint32_t Usart6ClockSelection; 
/* 指定UART7/8时钟源,该参数可以是RCCEx_UART78_Clock_Source */     
  uint32_t Uart78ClockSelection;   
 /* 指定RNG1时钟源,该参数可以是RCCEx_RNG1_Clock_Source */     
  uint32_t Rng1ClockSelection; 
/* 指定RTC时钟源,该参数可以是RCC_RTC_Clock_Source  */      
  uint32_t RTCClockSelection;      
/********* 省略部分代码 *********/
/* 指定TIM2时钟源,该参数可以是RCCEx_TIMG2_Prescaler_Selection */ 
  uint32_t TIMG2PresSelection;        
} RCC_PeriphCLKInitTypeDef;
	RCC_PLLInitTypeDef结构体我们前面已经分析过,其声明了锁相环PLL中用到的DIVM、DIVP、DIVQ、DIVR、FRACV等参数变量。

9.3.6 其它函数
在这里插入图片描述

表9.3.5. 1其它API函数
9.4 配置系统时钟实验
根据前面的分析以及ST官方给的总线框图,MCU时钟最大是209MHz,MCU有一个内核外设Systick,频率最大也是209MHz。下面我们分别使用HSE、HSI时钟源,通过配置时钟树来实现设置MCU最大的频率值,并通过程序读取MCU的时钟频率值和验证Systick的时钟频率。
9.4.1 使用HSE
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\2-1 MCU_HSE。

  1. 硬件原理设计
    正点原子STM32MP157开发板的核心板上外接了24MHz有源晶振,所以HSE为24MHz。
    在这里插入图片描述

图9.4.1. 1 晶振原理图截图
2. 软件配置
我们新建一个工程MCU_HSE,然后在STM32CubeMX插件上开启HSE,注意,要用外部时钟源HSE和LSE的话,一定要先将他们开启。如下图所示,会有4个选项:
①Disable 关闭外部时钟
②BYPASS Clock Source 旁路时钟
③DIGBYPASS Clock Source 数字时钟(数字信号提供时钟)
④Crystal/Ceramic Resonator 晶体/陶瓷谐振器
在这里插入图片描述

图9.4.1. 2 HSE时钟模式选择
旁路时钟是指使用外部晶振时,不需要芯片内部时钟驱动组件,直接从外部导入时钟信号,就好比芯片内部的驱动组件被旁路了,这种模式的话,一般接有源晶振。
晶体/陶瓷谐振器选项是指,使用外部晶振时,时钟源是由外部晶振体与MCU内部时钟驱动电路共同配合形成,有一定的启动时间,精度较高,可以接无源晶振或者有源晶振。
无源晶振自身是无法起振的,如果使用的是无源晶振的话,需要芯片上电跑起来以后,芯片内部的供电电路给无源晶振供电以后才可以起振。如果配置成BYPASS Clock Source模式的话,无源晶振就没法工作了,所以必须配置成Crystal/Ceramic Resonator模式。
如果使用有源晶振,只要外部电路一上电,有源晶振就起振了,所以可以选BYPASS Clock Source或者Crystal/Ceramic Resonator模式都是可以的。
如下图,将HSE配置成旁路时钟和晶体/陶瓷谐振器的差别,旁路时钟是单向信号,晶体/陶瓷谐振器是双向信号。
在这里插入图片描述

图9.4.1. 3 旁路时钟

在这里插入图片描述

图9.4.1. 4晶体/陶瓷谐振器
我们这里就选Crystal/Ceramic Resonator模式(选BYPASS Clock Source模式也可以):
在这里插入图片描述

图9.4.1. 5选Crystal/Ceramic Resonator模式
在Clock Configuration处选择HSE时钟作为时钟源。由24MHz倍频到209MHz的话,需要通过PLL3配置分频系数和倍频系数来实现。具体配置步骤为:
①选择HSE作为PLL3的时钟源;
②选择PLL3P(也就是pll3_p_ck)作为mcuss_ck的输入时钟源;
③手动输入209并按下回车,最大也只能输入209了,如果输入大于209并回车以后,系统系会频率超出范围。
④输入209以后,STM32CubeMX会动态计算PLL3的倍频和分频系数,其中:
DIVM3=2,DIVN3=52,fracv3=2048,DIVP3=3
我们将上面的参数代入前面的计算公式中,计算pll3_p_ck结果也刚好是209MHz。

在这里插入图片描述

图9.4.1. 6 手动输入参数
⑤STM32CubeMX动态计算MCU、AHB1~ AHB4以及APB1~ APB3桥接器和外设时钟。我们看到PCLK1~PCLK3后面显示红色,红色表示数值超出范围,并且窗口有一个叉提示配置异常:
在这里插入图片描述

图9.4.1. 7配置异常提示
根据前面9.1.2小节有提到,pclk1、pclk2、pclk3最大为104.5 MHz,所以我们要修改这部分的参数。APB1DIV~APB3DIV参数可选1/2/4/8/16,我们随便选其中一个,下图选参数2,刚好满足最大频率为104.5 MHz:
在这里插入图片描述

图9.4.1. 8修改APB1DIV~APB3DIV
查看上述配置,MCU和Systick的时钟频率均为209MHz,是否真的是这样呢?我们后面会通过HAL库函数读取它们的值。
按下“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小节说过。所以这里就不选了:
在这里插入图片描述

图9.4.1. 9 Project Manager窗口配置
系统生成工程如下:
在这里插入图片描述

图9.4.1. 10生成MCU_HSE工程
3. 添加用户代码
我们在main.c文件中添加如下代码:

main.c文件代码
    static uint32_t mcu_freq = 0;
    static uint32_t Systick_freq = 0;
    mcu_freq=HAL_RCC_GetSystemCoreClockFreq();		/* 获取MCU的时钟频率  */
    Systick_freq=SysTick->LOAD; 			/* 获取Systick的LOAD寄存器的值  */
  1. 编译、调试
    保存修改,编译工程无报错,先在while循环处添加一个断点,然后按照第4.1.6小节连接好开发板和ST-Link,进入Debug模式。
    进入Debug模式以后,程序停在HAL_Init函数处,此时变量的值为0
    在这里插入图片描述

图9.4.1. 11进入Debug模式
点击运行按钮,程序运行到断点处停止,观察此时mcu_freq的值为209000128,Systick_freq的值为208999。
在这里插入图片描述

图9.4.1. 12 运行调试
5. 实验结果分析
根据上面的调试结果,我们看到mcu_freq的值为209000128,约等于209MHz,说明此时的系统时钟(MCU时钟)是209MHz,与前面的配置一致。
Systick_freq的值为208999,约等于209000,此值我们是读取SysTick定时器中的LOAD寄存器的值。我们根据此结果逆推一下,SysTick每次计209000下就发生中断,如果要确保每1ms的中断周期的话,那么频率必须为209MHz。
注:
SysTick一个24位向下递减计数器,启动后,LOAD寄存器的值赋给VAL 寄存器,VAL寄存器递减,当递减到0的时候,会产生一次中断,然后再从LOAD寄存器取值,然后再从所得值开始递减,递减到0的时候又产生一次中断,如此反复,从而实现计时。我们在前面的7.4.2小节有讲过。
前面我们提到,SystemCoreClock全局变量也是通过HAL_RCC_GetSystemCoreClockFreq()函数来获取最新的系统时钟频率的,那么他的值应该也是209000128。我们在STM32CubeIDE上查看此变量的值:
在这里插入图片描述

图9.4.1. 13 打开表达式窗口
如下图,添加要观察的SystemCoreClock全局变量,其值为209000128。
在这里插入图片描述

图9.4.1. 14观察值
9.4.2 使用HSI
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\2-2 MCU_HSI。
新建一个工程,工程名字为MCU_HSI,进入时钟配置窗口以后,mcuss_ck默认是64MHz,我们手动输入209然后按下回车键,STM32CubeMX会自动计算各个参数,如下图所示:
在这里插入图片描述

图9.4.1. 15手动配置参数
红色部分提示pclk1、pclk2和pclk3的频率数值超出范围,我们调整一下,将分频值由1改为2:
在这里插入图片描述

图9.4.1. 16手动调整参数
配置好以后,我们看到各个参数:
①默认选择HSI作为PLL3的时钟源;
②默认选择PLL3P(也就是pll3_p_ck)作为mcuss_ck的输入时钟源;
④输入209以后,STM32CubeMX会动态计算PLL3的倍频和分频系数,其中:
DIVM3=4,DIVN3=26,fracv3=1024,DIVP3=2
我们将上面的参数代入前面的计算公式中,计算pll3_p_ck结果也刚好是209MHz。

按下“Ctrl+S”保存配置,STM32CubeIDE自动生成工程:
我们在main.c文件中添加如下代码:
main.c文件代码
    static uint32_t mcu_freq = 0;
    static uint32_t Systick_freq = 0;
    mcu_freq=HAL_RCC_GetSystemCoreClockFreq();/* 获取MCU的时钟频率  */
    Systick_freq=SysTick->LOAD; 			/* 获取Systick的LOAD寄存器的值  */

在这里插入图片描述

图9.4.1. 17添加的代码
保存修改,编译工程无报错,先在while循环处添加一个断点,然后按照第4.1.6小节连接好开发板和ST-Link,进入Debug模式。注意不要选错前面的MCU_HSE的工程,可以在Debug配置界面处确认一下:
在这里插入图片描述

图9.4.1. 18确认调试的工程
进入Debug模式以后,程序停在HAL_Init函数处,此时变量的值为0。
在这里插入图片描述

图9.4.1. 19进入Debug模式
点击运行按钮,程序运行到断点处停止,观察此时mcu_freq的值为209000128,Systick_freq的值为208999。实验结果和上面MCU_HSE工程结果一样。实验分析请查看前面的MCU_HSE工程部分。
在这里插入图片描述

图9.4.1. 20运行调试
9.5 实验代码分析
实验虽然验证完毕了,虽然是STM32CubeIDE自动生成的初始化代码,但我们还是有必要分析一下工程代码,这对HAL库的理解还是很有好处的。我们以MCU_HSE工程为例子,分析一下代码的实现过程。
9.5.1 配置HSE相关宏定义
此部分主要是在stm32mp1xx_hal_conf.h文件中完成的,在第7.4.1小节有分析过此文件。

  1. 选择HSE相关模块
    在stm32mp1xx_hal_conf.h文件中使能HAL驱动程序中要使用的模块的列表,实验中用到了RCC:
stm32mp1xx_hal_conf.h文件代码
	#define HAL_MODULE_ENABLED
    /*#define HAL_HASH_MODULE_ENABLED   */
    #define HAL_HSEM_MODULE_ENABLED
    /*#define HAL_WWDG_MODULE_ENABLED   */
    #define HAL_GPIO_MODULE_ENABLED
    #define HAL_EXTI_MODULE_ENABLED
    #define HAL_DMA_MODULE_ENABLED
    #define HAL_MDMA_MODULE_ENABLED
    #define HAL_RCC_MODULE_ENABLED
    #define HAL_PWR_MODULE_ENABLED
    #define HAL_CORTEX_MODULE_ENABLED
  1. 包含HSE相关模块的头文件
stm32mp1xx_hal_conf.h文件代码
    #ifdef HAL_RCC_MODULE_ENABLED
     #include "stm32mp1xx_hal_rcc.h"
    #endif /* HAL_RCC_MODULE_ENABLED */

    #ifdef HAL_EXTI_MODULE_ENABLED
     #include "stm32mp1xx_hal_exti.h"
    #endif /* HAL_EXTI_MODULE_ENABLED */

    #ifdef HAL_GPIO_MODULE_ENABLED
     #include "stm32mp1xx_hal_gpio.h"
    #endif /* HAL_GPIO_MODULE_ENABLED */

    #ifdef HAL_HSEM_MODULE_ENABLED
     #include "stm32mp1xx_hal_hsem.h"
    #endif /* HAL_HSEM_MODULE_ENABLED */
	/****** 省略部分代码 *******/
  1. 配置HSE_VALUE
    宏定义HSE_VALUE匹配我们实际硬件的高速晶振频率(这里是24MHz),代码如下:
stm32mp1xx_hal_conf.h文件代码
#if !defined  (HSE_VALUE)
    #define HSE_VALUE    (24000000U) 
    #endif 

9.5.2 系统时钟设置函数

  1. 函数代码实现
    gpio.c文件代码很简单,主要就是开启GPIOH时钟,因为HSE的两个时钟引脚挂在GPIOH上:
#include "gpio.h"

void MX_GPIO_Init(void)
{
  /* 开启GPIOH时钟,因为HSE的两个引脚PH0-OSC_IN和PH1-OSC_OUT挂在GPIOH上 */
  __HAL_RCC_GPIOH_CLK_ENABLE();
}
下面,我们重点来看看main.c文件中系统时钟初始化代码。如下图是系统时钟设置函数SystemClock_Config的代码。代码主要分成三部分:
①第一部分,是定义并初始化RCC_OscInitStruct、RCC_ClkInitStruct结构体变量,初始值默认为0,即将HSE、HSI、LSE、LSE、CSI和PLL1~PLL4均关闭;
②第二部分,通过RCC_OscInitStruct结构体变量开启时钟源HSI、HSE、LSI和PLL3,并设置PLL3的分频和倍频参数,从而使pll3_p_ck输出频率为209MHz。
③第三部分,通过RCC_ClkInitStruct结构体变量完成AXI、MCU和APB1~APB5的配置,实现AXI、MCU、APB1~APB3和APB4~APB5时钟频率分别为64MHz、209MHz、64MHz和104.5MHz。
main.c文件代码
1   /**
2    * @brief     M4主频时钟设置函数,也就是设置PLL3
3    * @ note     plln: PLL3倍频系数(PLL倍频), 取值范围: 25~200.
4    *             pllm: PLL3预分频系数(进PLL之前的分频), 取值范围: 1~64.
5    *             pllp: PLL3的p分频系数(PLL之后的分频), 分频后作为系统时钟, 取					值范围: 1~128.(且必须是2的倍数)
6    *             pllq: PLL3的q分频系数(PLL之后的分频), 取值范围: 1~128.
7    * 
8    *              MP157使用HSE时钟的时候,默认最高配置为:
9    *              CPU频率(mcu_ck) = MLHCLK = PLL3P / 1 = 209Mhz
10   *              hclk = MLHCLK = 209Mhz
11   *              AHB1/2/3/4 = hclk = 209Mhz
12   *              APB1/2/3 = MLHCLK / 2 = 104.5Mhz
13   * @retval      无;
14   */
15  void SystemClock_Config(void)
16  {
17  /* 定义RCC_OscInitStruct、RCC_ClkInitStruct结构体变量,并初始化为0 */
18    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
19    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
20
21  /*给RCC_OscInitTypeDef结构中的成员变量赋值完成初始化RCC振荡器 */
22    RCC_OscInitStruct.OscillatorType = \ 				RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_LSI
23                                |RCC_OSCILLATORTYPE_HSE;
24    RCC_OscInitStruct.HSEState = RCC_HSE_ON;  		/* 打开HSE */
25    RCC_OscInitStruct.HSIState = RCC_HSI_ON;  		/* 打开HSI */
26    RCC_OscInitStruct.HSICalibrationValue = 16;		/* 校准HSI值 */
27    RCC_OscInitStruct.HSIDivValue = RCC_HSI_DIV1; /* 设置HSI分频值为1 */
28    
29    RCC_OscInitStruct.LSIState = RCC_LSI_ON;  		/* 开启LSI */
30    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;	/* 没有PLL的状态 */
31    RCC_OscInitStruct.PLL2.PLLState = RCC_PLL_NONE;	/* 没有PLL2的状态 */
32    RCC_OscInitStruct.PLL3.PLLState = RCC_PLL_ON;	/* 开启PLL3 */
33  /* PLL3输入时钟源为HSE */ 
34    RCC_OscInitStruct.PLL3.PLLSource = RCC_PLL3SOURCE_HSE;
35  /*
36  * 配置锁相环PLL3的分频和倍频参数,也就是:
37  * DIVM3=2,DIVN3=52,DIVP3=3,DIVQ3=2,DIVR3=2,FRACV=2048
38  * 则PLL3的pll3_p_ck输出频率为:
39  * pll3_p_ck==209MHz
40  */
41    RCC_OscInitStruct.PLL3.PLLM = 2;			/* DIVM3=2 */
42    RCC_OscInitStruct.PLL3.PLLN = 52;			/* DIVN3=52 */
43    RCC_OscInitStruct.PLL3.PLLP = 3;			/* DIVP3=3 */
44    RCC_OscInitStruct.PLL3.PLLQ = 2;			/* DIVQ3=2 */
45    RCC_OscInitStruct.PLL3.PLLR = 2;			/* DIVR3=2 */
46    RCC_OscInitStruct.PLL3.PLLRGE = RCC_PLL3IFRANGE_1;
47    RCC_OscInitStruct.PLL3.PLLFRACV = 2048;	/* FRACV=2048 */
48    RCC_OscInitStruct.PLL3.PLLMODE = RCC_PLL_FRACTIONAL;/* 分数模式 */
49    RCC_OscInitStruct.PLL4.PLLState = RCC_PLL_NONE;	/* PLL4没有状态 */
50    
51   /* 调用的HAL_RCC_OscConfig函数用于判断 HSE、HSI、LSI、LSE 和
52    * PLL(PLL1、PLL2、PLL3和PLL4)是否配置完成,配置完成则返回HAL_OK。
53    * 如果没有配置完成,发生错误的话就会进入Error_Handler函数(空循环)。
54    */
55    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
56    {
57      Error_Handler();
58    }
59  /*
60   * 给RCC_ClkInitStruct结构体成员赋值来配置RCC时钟,也就是:
61   * 配置AXI的时钟源和分频器分频值(也就是配置ACLK);
62   * 配置MCU的时钟源和分频器分频值;
63   * 配置APB1~APB5的分频值(也就是配置PCLK1~5)。
64   */
65    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_ACLK
66                                |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2
67                                |RCC_CLOCKTYPE_PCLK3|RCC_CLOCKTYPE_PCLK4
68                                |RCC_CLOCKTYPE_PCLK5;
69  /* 配置AXI时钟源为HSI */                            
70    RCC_ClkInitStruct.AXISSInit.AXI_Clock = RCC_AXISSOURCE_HSI;
71  /* 配置AXI分频器为1分频=ACLK=64MHz */   
72    RCC_ClkInitStruct.AXISSInit.AXI_Div = RCC_AXI_DIV1;
73  /* 配置MCU时钟源来自PLL3=209MHz */
74    RCC_ClkInitStruct.MCUInit.MCU_Clock = RCC_MCUSSOURCE_PLL3;
75  /* 配置MCU分频器为1分频=MCU=209MHz */
76    RCC_ClkInitStruct.MCUInit.MCU_Div = RCC_MCU_DIV1;
77  /* 配置APB4分频器为1分频=PCLK4=64MHz*/
78    RCC_ClkInitStruct.APB4_Div = RCC_APB4_DIV1;
79  /* 配置APB5分频器为2分频=PCLK5=64MHz */
80    RCC_ClkInitStruct.APB5_Div = RCC_APB5_DIV1;
81  /* 配置APB1分频器为2分频=PCLK1=104.5MHz */
82    RCC_ClkInitStruct.APB1_Div = RCC_APB1_DIV2;
83  /* 配置APB2分频器为2分频=PCLK2=104.5MHz */
84    RCC_ClkInitStruct.APB2_Div = RCC_APB2_DIV2;
85  /* 配置APB3分频器为2分频=PCLK3=104.5MHz */
86    RCC_ClkInitStruct.APB3_Div = RCC_APB3_DIV2;
87  /*
88  * 调用HAL_RCC_ClockConfig函数,根据RCC_ClkInitStruct中
89  * 指定的参数初始化MPU,AXI,AHB和APB总线时钟,如果初始化
90  * 不成功,则进入Error_Handler空循环函数。
91  */
92    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct) != HAL_OK)
93    {
94      Error_Handler();
95    }
96    /* 设置RTC时钟的HSE分频因子 */
97    __HAL_RCC_RTC_HSEDIV(1);
98  }
  1. 函数代码分析
    下面,我们来分析SystemClock_Config函数的代码实现过程。
    第18行,定义RCC_OscInitTypeDef类型结构体变量RCC_OscInitStruct,并初始化为0,表示将HSE、HSI、LSE、LSE、CSI和PLL1~PLL4均关闭。
    以上结构体在stm32mp1xx_hal_rcc.h文件中有定义,我们在前面也有详细介绍过,为了大家不总是跳跃看代码,这里还是贴出代码了,如下。
    RCC_OscInitTypeDef主要对RCC内部/外部振荡器(HSE,HSI,CSI,LSE和LSI)配置的结构定义,主要是定义要配置的振荡器、时钟源的状态、校准值和PLL的参数。通过给结构体成员赋值即可开或者关对应的时钟,并可以配置PLL的参数。
stm32mp1xx_hal_rcc.h文件代码
    typedef struct
    {
      uint32_t OscillatorType; 			/* 要配置的振荡器 */                                                     
      uint32_t HSEState;   				/* HSE的新状态 */                                                                   
      uint32_t LSEState; 					/* LSE的新状态 */                                                                       
      uint32_t HSIState;   				/* HSI的新状态 */                                                                   
      uint32_t HSICalibrationValue; 		/* 校准调整值 */                                      
      uint32_t HSIDivValue;  				/* HSI的分频系数 */                                             
      uint32_t LSIState;  				/* LSI的新状态 */                                                 
      uint32_t CSIState;  				/* CSI的新状态 */                                                 
      uint32_t CSICalibrationValue; 		/* 校准调整值 */                                       
      RCC_PLLInitTypeDef PLL; 			/* PLL1结构参数 */      
      RCC_PLLInitTypeDef PLL2;			/* PLL2结构参数 */     
      RCC_PLLInitTypeDef PLL3; 			/* PLL3结构参数 */     
      RCC_PLLInitTypeDef PLL4; 			/* PLL4结构参数 */                                                  
    } RCC_OscInitTypeDef;
我们查看PLL的结构参数有哪些,如下,此结构体定义了锁相环PLL中用到的DIVM、DIVP、DIVQ、DIVR、FRACV等参数变量,通过给这些结构体成员赋值即可配置PLL的时钟。

stm32mp1xx_hal_rcc.h文件代码

typedef struct
{
uint32_t PLLState; /* PLL的新状态 /
uint32_t PLLSource; /
PLL输入时钟源 /
uint32_t PLLM; /
PLL VCO输入时钟的分频系数DIVM /
uint32_t PLLN; /
PLL VCO输出时钟的倍数DIVN /
uint32_t PLLP; /
分频因子DIVP /
uint32_t PLLQ; /
分频因子DIVQ /
uint32_t PLLR; /
分频因子DIVR /
uint32_t PLLRGE; /
PLL3和PLL4的PLL输入频率范围 /
uint32_t PLLFRACV; /
PLL VCO乘数的小数部分FRACV /
uint32_t PLLMODE; /
使用PLL模式 /
uint32_t MOD_PER; /
调制周期调整 /
uint32_t RPDFN_DIS; /
抖动的RPDF噪音控制 /
uint32_t TPDFN_DIS; /
抖动的TPDF噪声控制 /
uint32_t SSCG_MODE; /
扩频时钟发生器模式 /
uint32_t INC_STEP; /
调制深度调整*/
} RCC_PLLInitTypeDef;
第19行,定义RCC_ClkInitTypeDef类型结构体变量,并初始化为0,即AXI子系统、MCU子系统时钟源默认来自HSI,且AXI时钟和MCU时钟以及APB1~APB5时钟的分频器的分频值为0(没有分频)。
RCC_ClkInitStruct此结构体我们在前面也有讲解过。RCC_ClkInitTypeDef结构体定义如下,主要是定义AXI和MPU的时钟源以及分频系数,还有APB1~APB5的分频系数,通过给结构体成员赋值即可实现对MPU、AXI和APB进行分频。
stm32mp1xx_hal_rcc.h文件代码

typedef struct
{
uint32_t ClockType; /* 时钟源选择 /
RCC_MPUInitTypeDef MPUInit; /
MPU结构参数(时钟源和分频数) /
RCC_AXISSInitTypeDef AXISSInit; /
AXI结构参数(时钟源和分频数)/
RCC_MCUInitTypeDef MCUInit; /
APB4分频数 /
uint32_t APB4_Div; /
APB4分频数 /
uint32_t APB5_Div; /
APB5分频数 /
uint32_t APB1_Div; /
APB1分频数 /
uint32_t APB2_Div; /
APB2分频数 /
uint32_t APB3_Div; /
APB3分频数 */
} RCC_ClkInitTypeDef;
分析完了结构体,整个系统时钟初始化就大概明白了,需要哪个时钟源,要用那个PLL以及PLL的各个参数,要对那根总线进行分频以及分频系数是多少等等,我们都可以通过给结构体成员赋值来实现。SystemClock_Config函数其实也是这么做的,我们接着分析此函数剩下的代码。
第21到第34行,给RCC_OscInitTypeDef结构中的成员变量赋值完成初始化RCC振荡器,开启了HSE,配置PLL3为209MHz。
第55到58行,调用HAL_RCC_OscConfig函数真正完成HSE、HSI、LSI 和 PLL3的配置。
第65到第86行,给RCC_ClkInitStruct结构体成员赋值,完成AXI、MCU和APB的时钟分配。其中:
AXI时钟源来自HSI,经过1分频后最终频率为64MHz;
MCU时钟源来自PLL3,经过1分频后最终频率为209MHz;
确定了AXI的时钟,那么APB4和APB5的时钟源也就确定了。APB4和APB5经过1分频后最终频率为64MHz;
确定了MCU的时钟,那么APB1APB5的时钟源也就确定了。APB1APB2经过1分频后最终时钟频率为104.5MHz。
可能大家会问,为啥没看到AHB1AHB6相关的代码呢?这个系统就默认配置好了,因为他们不需要经过分频器。AHB1AHB4的时钟直接通过mcuss_ck获得,为64MHz。AHB5~AHB6的时钟直接通过axiss_ck获得,为209MHz。
9.5.3 main函数调用
在main函数中调用系统时钟配置函数的代码:

 if(IS_ENGINEERING_BOOT_MODE())
      {
        SystemClock_Config();
      }
其中,先判断IS_ENGINEERING_BOOT_MODE()函数的返回值,如果为TRUE 则初始化系统时钟,如果为FALSE,条件不成立,if语句不执行。我们来看看IS_ENGINEERING_BOOT_MODE函数:
stm32mp157dxx_cm4.h文件代码
  #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函数来初始化系统时钟。
大家可以分析一下第二个工程MCU_HSI的系统时钟代码,对比看看它们的区别。
关于外设时钟初始化部分,下面的章节小结有总,我们也会在对应的外设实验章节进行讲解。

9.5.4 HAL_Delay的计时
关于HAL_Delay是怎么实现计时的,我们在前面第7.4.2小节有分析过,关于Systick我们后面会有专门的实验章节做讲解。默认情况下,STM32CubeMX使用Systick作为时基给其它程序提供计时,例如HAL_Delay延时函数,以及串口程序中的Timeout 超时机制等等。
在这里插入图片描述

图9.5.2. 1 STM32CubeMX插件默认配置

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值