1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html
第十一章 STM32时钟配置
MCU都是基于时序控制的一个系统。这一讲将结合《STM32F10xxx参考手册_V10(中文版).pdf》和《Cortex-M3权威指南》的知识,对STM32F1的整体架构作一个简单的介绍,帮助大家更全面、系统地认识STM32F1系统的主控结构。了解时钟系统在整个STM32系统的贯穿和驱动作用,学会设置STM32的系统时钟。
本章将分为如下几个小节:
11.1 认识时钟树
11.2 如何修改主频
11.1 认识时钟树
数字电路的知识告诉我们:任意复杂的电路控制系统都可以经由门电路组成的组合电路实现。回顾《第五章 STM32基础知识入门》的知识点,我们知道STM32内部也是由多种多样的电路模块组合在一起实现的。当一个电路越复杂,在达到正确的输出结果前,它可能因为延时会有一些短暂的中间状态,而这些中间状态有时会导致输出结果会有一个短暂的错误,这叫做电路中的“毛刺现象”,如果电路需要运行得足够快,那么这些错误状态会被其它电路作为输入采样,最终形成一系列的系统错误。为了解决这个问题,在单片机系统中,设计时以时序电路控制替代纯粹的组合电路,在每一级输出结果前对各个信号进行采样,从而使得电路中某些信号即使出现延时也可以保证各个信号的同步,可以避免电路中发生的“毛刺现象”,达到精确控制输出的效果。
由于时序电路的重要性,因此在MCU设计时就设计了专门用于控制时序的电路,在芯片设计中称为时钟树设计。由此设计出来的时钟,可以精确控制我们的单片机系统,这也是我们这节要展开分析的时钟分析。为什么是时钟树而不是时钟呢?一个MCU越复杂,时钟系统也会相应地变得复杂,如STM32F1的时钟系统比较复杂,不像简单的51单片机一个系统时钟就可以解决一切。对于STM32F1系列的芯片,正常工作的主频可以达到72Mhz,但并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及RTC只需要几十kHZ的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的MCU一般都是采取多时钟源的方法来解决这些问题。
STM32本身非常复杂,外设非常的多,为了保持低功耗工作,STM32的主控默认不开启这些外设功能。用户可以根据自己的需要决定STM32芯片要使用的功能,这个功能开关在STM32主控中也就是各个外设的时钟。
图11.1.1 STM32F1时钟系统图
如图11.1.1为一个简化的STM32F1时钟系统。图中已经把我们主要关注几处标注出来。A部分表示其它电路需要的输入源时钟信号;B为一个特殊的振荡电路“PLL”,由几个部分构成;C为我们重点需要关注的MCU内的主时钟“SYSCLK”;AHB预分频器将SYSCLK分频或不分频后分发给其它外设进行处理,包括到F部分的Cortex-M内核系统的时钟。D、E部分别为定时器等外设的时钟源APB1/APB2。G是STM32的时钟输出功能,其它部分等我们学习到再详细探讨。接下来我们来详细了解这些部分的功能。
11.1.1 时钟源
对于STM32F1,输入时钟源(Input Clock)主要包括HSI,HSE,LSI,LSE。其中,从时钟频率来分可以分为高速时钟源和低速时钟源,其中HSI、HSE高速时钟,LSI和LSE是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中HSE和LSE是外部时钟源;其他是内部时钟源,芯片上电即可产生,不需要借助外部电路。下面我们看看STM32的时钟源。
(1)2个外部时钟源:
高速外部振荡器HSE (High Speed External Clock signal)
外接石英/陶瓷谐振器,频率为4MHz~16MHz。本开发板使用的是8MHz。
低速外部振荡器LSE (Low Speed External Clock signal)
外接32.768kHz石英晶体,主要作用于RTC的时钟源。
(2)2个内部时钟源:
高速内部振荡器HSI(High Speed Internal Clock signal)
由内部RC 振荡器产生,频率为 8MHz。
低速内部振荡器LSI(Low Speed Internal Clock signal)
由内部RC振荡器产生,频率为40kHz,可作为独立看门狗的时钟源。
芯片上电时默认由内部的HSI时钟启动,如果用户进行了硬件和软件的配置,芯片才会根据用户配置调试尝试切换到对应的外部时钟源,所以同时了解这几个时钟源信号还是很有必要的。如何设置时钟的方法我们会在后文提到。
11.1.2 锁相环PLL
锁相环是自动控制系统中常用的一个反馈电路,在STM32主控中,锁相环的作用主要有两个部分:输入时钟净化和倍频。前者是利用锁相环电路的反馈机制实现,后者我们用于使芯片在更高且频率稳定的时钟下工作。
在STM32中,锁相环的输出也可以作为芯片系统的时钟源。根据图11.1.1的时钟结构,使用锁相环时只需要进行三个部分的配置。为了方便查看,截取了使用PLL作为系统时钟源的配置部分,如图11.1.2.1所示。
图11.1.2.1 PLL时钟配置图
图11.1.2.1借用了在CubeMX下用锁相环配置72MHz时钟的一个示例:
PLLXTPRE:HSE分频器作为PLL输入 (HSE divider for PLL entry)
即图11.1.2.1在标注为①的地方,它专门用于HSE,ST设计它有两种方式,并把它的控制功能放在RCC_CFGR寄存器中,我们引用如图11.1.2.2。
图11.1.2.2 PLLXTPRE设置选项值
从F103参考手册可知它的值有两个:一是2分频,另一种是1分频(不分频)。
经过HSE分频器处理后的输出振荡时钟信号比直接输入的时钟信号更稳定。
PLLSRC:PLL输入时钟源 (PLL entry clock source)
图中②表示的是PLL时钟源的选择器,同样的,参考F103参考手册:
图11.1.2.3 PLLSRC锁相环时钟源选择
它有两种可选择的输入源:设计为HSI的二分频时钟,另一个是A处的PLLXTPRE处理后的HSE信号。
PLLMUL:PLL倍频系数 (PLL multiplication factor)
图中③所表示的配置锁相环倍频系数,同样地可以查到在STM32F1系列中,ST设置它的有效倍频范围为2~16倍。
结合图11.1.2.1,要实现72MHz的主频率,我们通过选择HSE不分频作为PLL输入的时钟信号,即输入8Mhz,通过标号③选择倍频因子,可选择2-16倍频,我们选择9倍频,这样可以得到时钟信号为89=72MHz。
11.1.3 系统时钟SYSCLK
STM32的系统时钟SYSCLK为整个芯片提供了时序信号。我们已经大致知道STM32主控是时序电路链接起来的。对于相同的稳定运行的电路,时钟频率越高,指令的执行速度越快,单位时间能处理的功能越多。STM32的系统时钟是可配置的,在STM32F1系列中,它可以为HSI、PLLCLK、HSE中的一个,通过CFGR的位SW[1:0]设置。
讲解PLL作为系统时钟时,根据我们开发板的资源,可以把主频通过PLL设置为72MHz。仍使用PLL作为系统时钟源,如果使用HSI/2,那么可以得到最高主频8MHz/216=64MHz。从上面的图11.2.1时钟树图可知,AHB、APB1、APB2、内核时钟等时钟通过系统时钟分频得到。根据得到的这个系统时钟,下面我们结合外设来看一看各个外设时钟源。
图11.2.3.1 STM32F103系统时钟生成图
大家看图11.2.3.1 STM32F103系统时钟,标号C为系统时钟输入选择,可选时钟信号有外部高速时钟HSE(8M)、内部高速时钟HSI(8M)和经过倍频的PLL CLK(72M),选择PLL CLK作为系统时钟,此时系统时钟的频率为72MHz。系统时钟来到标号D的AHB预分频器,其中可选择的分频系数为1,2,4,8,16,32,64,128,256,我们选择不分频,所以AHB总线时钟达到最大的72MHz。
下面介绍一下由AHB总线时钟得到的时钟:
APB1总线时钟,由HCLK经过标号E的低速APB1预分频器得到,分频因子可以选择1,2,4,8,16,这里我们选择的是2分频,所以APB1总线时钟为36M。由于APB1是低速总线时钟,所以APB1总线最高频率为36MHz,片上低速的外设就挂载在该总线上,例如有看门狗定时器、定时器2/3/4/5/6/7、RTC时钟、USART2/3/4/5、SPI2(I2S2)与SPI3(I2S3)、I2C1与I2C2、CAN、USB设备和2个DAC。
APB2总线时钟,由HCLK经过标号F的高速APB2预分频器得到,分频因子可以选择1,2,4,8,16,这里我们选择的是1即不分频,所以APB2总线时钟频率为72M。与APB2高速总线链接的外设有外部中断与唤醒控制、7个通用目的输入/输出口(PA、PB、PC、PD、PE、PF和PG)、定时器1、定时器8、SPI1、USART1、3个ADC和内部温度传感器。其中标号G是ADC的预分频器在后面ADC实验中详细说明。
此外,AHB总线时钟直接作为SDIO、FSMC、AHB总线、Cortex内核、存储器和DMA的HCLK时钟,并作为Cortex内核自由运行时钟FCLK。
图11.2.3.2 USB、RTC、MCO相关时钟
标号H是USBCLK,是一个通用串行接口时钟,时钟来源于PLLCLK。STM32F103内置全速功能的USB外设,其串行接口引擎需要一个频率为48MHz的时钟源。该时钟源只能从 PLL输出端获取,可以选择为1.5分频或者1分频,也就是,当需要使用 USB模块时,PLL 必须使能,并且时钟频率配置为48MHz或72MHz。
标号I是MCO输出内部时钟,STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出,可以选择为PLL输出的 2 分频、HSI、HSE、或者系统时钟。这个时钟可以用来给外部其他系统提供时钟源。
标号J是RTC定时器,其时钟源为HSE/128、LSE或LSI。
11.1.4 时钟信号输出MCO
STM32允许通过设置,通过MCO引脚输出一个稳定的时钟信号。在图11.1.1中标注为“G”的部分。以下四个时钟信号可被选作MCO时钟:
● SYSCLK
● HSI
● HSE
● 除2的PLL时钟
时钟的选择由时钟配置寄存器(RCC_CFGR)中的MCO[2:0]位控制。
我们可以通过MCO引脚来输出时钟信号,测试输出时钟的频率,或作为其它需要时钟信号的外部电路的时钟。
11.2 如何修改主频
STM32F103默认的情况下(比如:串口IAP时或者是未初始化时钟时),使用的是内部8M的HSI作为时钟源,所以不需要外部晶振也可以下载和运行代码的。
下面我们来讲解如何让STM32F103芯片在72MHZ的频率下工作,72MHz是官方推荐使用的最高的稳定时钟频率。而正点原子的STM32F103战舰开发板的外部高速晶振的频率就是8MHz,我们就是在这个晶振频率的基础上,通过各种倍频和分频得到72MHZ的系统工作频率。
11.2.1 STM32F1时钟系统配置
下面我们将分几步给大家讲解STM32F1时钟系统配置过程,这部分内容很重要,请大家认真阅读。
第1步:配置HSE_VALUE
讲解STM32F1xx_hal_conf.h文件的时候,我们知道需要宏定义HSE_VALUE匹配我们实际硬件的高速晶振频率(这里是8MHZ),代码中通过使用宏定义的方式来选择HSE_VALUE的值是25M或者8M,这里我们不去定义USE_STM3210C_EVAL这个宏或者全局变量即可,选择定义HSE_VALUE的值为8M。代码如下:
#if !defined (HSE_VALUE)
#if defined(USE_STM3210C_EVAL)
#define HSE_VALUE 25000000U /*!< Value of the External oscillator in Hz */
#else
#define HSE_VALUE 8000000U /*!< Value of the External oscillator in Hz */
#endif
#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_stm32f1xx.c文件下定义的SystemInit程序,源码在176行到188行,简化函数如下。
void SystemInit (void)
{
#if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) || defined(STM32F103xE) || defined(STM32F103xG)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* 配置扩展SRAM */
#endif
/* 配置中断向量表 */
#if defined(USER_VECT_TAB_ADDRESS)
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#endif /* USER_VECT_TAB_ADDRESS */
}
从上面代码可以看出,SystemInit主要做了如下两个方面工作:
- 外部存储器配置
- 中断向量表地址配置
然而我们的代码中实际并没有定义DATA_IN_ExtSRAM和USER_VECT_TAB_ADDRESS这两个宏,实际上SystemInit对于正点原子的例程并没有起作用,但我们保留了这个接口。从而避免了去修改启动文件。另外,是可以把一些重要的初始化放到SystemInit这里,在main函数运行前就把重要的一些初始化配置好(如ST这里是在运行main函数前先把外部的SRAM初始化),这个我们一般用不到,直接到main函数中处理即可,但也有厂商(如RT-Thread)就采取了这样的做法,使得main函数更加简单,但对于初学者,我们暂时不建议这种用法。
HAL库的SystemInit函数并没有任何时钟相关配置,所以后续的初始化步骤,我们还必须编写自己的时钟配置函数。
第3步:在main函数里调用用户编写的时钟设置函数
我们打开HAL库例程实验1跑马灯实验,看看我们在工程目录Drivers\SYSTEM分组下面定义的sys.c文件中的时钟设置函数sys_stm32_clock_init的内容:
/**
* @brief 系统时钟初始化函数
* @param plln: PLL倍频系数(PLL倍频), 取值范围: 2~16
中断向量表位置在启动时已经在SystemInit()中初始化
* @retval 无
*/
void sys_stm32_clock_init(uint32_t plln)
{
HAL_StatusTypeDef ret = HAL_ERROR;
RCC_OscInitTypeDef rcc_osc_init = {0};
RCC_ClkInitTypeDef rcc_clk_init = {0};
rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; /* 选择要配置HSE */
rcc_osc_init.HSEState = RCC_HSE_ON; /* 打开HSE */
rcc_osc_init.HSEPredivValue = RCC_HSE_PREDIV_DIV1; /* HSE预分频系数 */
rcc_osc_init.PLL.PLLState = RCC_PLL_ON; /* 打开PLL */
rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* PLL时钟源选择HSE */
rcc_osc_init.PLL.PLLMUL = plln; /* PLL倍频系数 */
ret = HAL_RCC_OscConfig(&rcc_osc_init); /* 初始化 */
if (ret != HAL_OK)
{
while (1); /* 时钟初始化失败后,程序将可能无法正常执行,可以在这里加入自己的处理 */
}
/* 选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2*/
rcc_clk_init.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;/* 设置系统时钟来自PLL */
rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; /* AHB分频系数为1 */
rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV2; /* APB1分频系数为2 */
rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; /* APB2分频系数为1 */
/* 同时设置FLASH延时周期为2WS,也就是3个CPU周期。 */
ret = HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_2);
if (ret != HAL_OK)
{
while (1); /* 时钟初始化失败后,程序将可能无法正常执行,可以在这里加入自己的处理 */
}
}
函数sys_stm32_clock_init就是用户的时钟系统配置函数,除了配置PLL相关参数确定SYSCLK值之外,还配置了AHB、APB1和APB2的分频系数,也就是确定了HCLK,PCLK1和PCLK2的时钟值。
我们首先来看看使用HAL库配置STM32F1时钟系统的一般步骤:
- 配置时钟源相关参数:调用函数HAL_RCC_OscConfig()。
- 配置系统时钟源以及SYSCLK、AHB、APB1和APB2的分频系数:调用函数HAL_RCC_ClockConfig()。
下面我们详细讲解这个2个步骤。
步骤1:配置时钟源相关参数,使能并选择HSE作为PLL时钟源,配置PLL1,我们调用的函数为HAL_RCC_OscConfig(),该函数在HAL库头文件STM32F1xx_hal_rcc.h中声明,在文件STM32F1xx_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 HSEPredivValue; /* HSE预分频值 */
uint32_t LSEState; /* LSE状态 */
uint32_t HSIState; /* HIS状态 */
uint32_t HSICalibrationValue; /* HIS校准值 */
uint32_t LSIState; /* LSI状态 */
RCC_PLLInitTypeDef PLL; /* PLL配置 */
}RCC_OscInitTypeDef;
该结构体前面几个参数主要是用来选择配置的振荡器类型。比如我们要开启HSE,那么我们会设置OscillatorType的值为RCC_OSCILLATORTYPE_HSE,然后设置HSEState的值为RCC_HSE_ON开启HSE。对于其他时钟源:HIS、LSI、LSE,配置方法类似。
RCC_OscInitTypeDef这个结构体还有一个很重要的成员变量是PLL,它是结构体RCC_PLLInitTypeDef类型。它的作用是配置PLL相关参数,我们来看看它的定义:
typedef struct
{
uint32_t PLLState; /* PLL状态 */
uint32_t PLLSource; /* PLL时钟源 */
uint32_t PLLMUL; /* PLL倍频系数M */
}RCC_PLLInitTypeDef;
从RCC_PLLInitTypeDef;结构体的定义很容易看出该结构体主要用来设置PLL时钟源以及相关分频倍频参数。这个结构体的定义的相关内容请结合时钟树中红色框的内容一起理解。
接下来我们看看我们的时钟初始化函数sys_stm32_clock_init中的配置内容:
/* 使能HSE,并选择HSE作为PLL时钟源,配置PLLMUL */
RCC_OscInitTypeDef rcc_osc_init = {0};
rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; /* 使能HSE */
rcc_osc_init.HSEState = RCC_HSE_ON; /* 打开HSE */
rcc_osc_init.HSEPredivValue = RCC_HSE_PREDIV_DIV1; /* HSE预分频 */
rcc_osc_init.PLL.PLLState = RCC_PLL_ON; /* 打开PLL */
rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* PLL时钟源为HSE */
rcc_osc_init.PLL.PLLMUL = plln; /* 主PLL倍频因子 */
ret = HAL_RCC_OscConfig(&rcc_osc_init); /* 初始化 */
通过函数的该段程序,我们开启了HSE时钟源,同时选择PLL时钟源为HSE,然后把sys_stm32_clock_init的形参直接设置作为PLL的参数M的值,这样就达到了设置PLL时钟源相关参数的目的。
设置好PLL时钟源参数之后,也就是确定了PLL的时钟频率,然后到我们的步骤2。
步骤2:配置系统时钟源,以及SYSCLK、AHB、APB1和APB2相关参数,用函数HAL_RCC_ClockConfig(),声明如下:
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,
uint32_t FLatency);
该函数有两个形参,第一个形参RCC_ClkInitStruct是结构体RCC_ClkInitTypeDef类型指针变量,用于设置SYSCLK时钟源以及SYSCLK、AHB、APB1和APB2的分频系数。第二个形参FLatency用于设置FLASH延迟。
RCC_ClkInitTypeDef结构体类型定义比较简单,我们来看看其定义:
typedef struct
{
uint32_t ClockType; /* 要配置的时钟 */
uint32_t SYSCLKSource; /* 系统时钟源 */
uint32_t AHBCLKDivider; /* AHB分频系数 */
uint32_t APB1CLKDivider; /* APB1分频系数 */
uint32_t APB2CLKDivider; /* APB2分频系数 */
}RCC_ClkInitTypeDef;
我们在sys_stm32_clock_init函数中的实际应用配置内容如下:
/****************** 具体配置*************************/
/*选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2*/
/*设置系统时钟时钟源为PLL*/
/*AHB分频系数为1*/
/*APB1分频系数为2*/
/*APB2分频系数为1*/
/*同时设置FLASH延时周期为2WS,也就是3个CPU周期。*/
/***************************************************/
rcc_clk_init.ClockType = (RCC_CLOCKTYPE_SYSCLK |
RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_PCLK1 |
RCC_CLOCKTYPE_PCLK2);
rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1;
rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV2;
rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1;
ret = HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_2);
sys_stm32_clock_init函数中的RCC_ClkInitTypeDef结构体配置内容:
第一个参数ClockType配置表示我们要配置的是SYSCLK、HCLK、PCLK1和PCLK四个时钟。
第二个参数SYSCLKSource配置选择系统时钟源为PLL。
第三个参数AHBCLKDivider配置AHB分频系数为1。
第四个参数APB1CLKDivider配置APB1分频系数为2。
第五个参数APB2CLKDivider配置APB2分频系数为1。
根据我们在mian函数中调用sys_stm32_clock_init(RCC_PLL_MUL9)时设置的形参数值,我们可以计算出,PLL时钟为PLLCLK = HSE * 9 = 8MHz * 9 = 72MHz。
同时我们选择系统时钟源为PLL,所以系统时钟SYSCLK=72MHz。AHB分频系数为1,故频率为HCLK=SYSCLK/1=72MHz。APB1分频系数为2,故其频率为PCLK1=HCLK/2=36MHz。APB2分频系数为1,故其频率为PCLK2=HCLK/1=72MHz。我们总结一下通过调用函数sys_stm32_clock_init(RCC_PLL_MUL9)之后的关键时钟频率值:
SYSCLK(系统时钟) =72MHz
PLL主时钟 =72MHz
AHB总线时钟(HCLK=SYSCLK/1) =72MHz
APB1总线时钟(PCLK1=HCLK/2) =36MHz
APB2总线时钟(PCLK2=HCLK/1) =72MHz
最后我们来看看函数HAL_RCC_ClockConfig 第二个入口参数FLatency的含义,为了使FLASH读写正确(因为72Mhz的时钟比Flash的操作速度24Mhz要快得多,操作速度不匹配容易导致Flash操作失败),所以需要设置延时时间。对于STM32F1系列,FLASH延迟配置参数值是通过下表11.2.1.1来确定的,具体可以参考《STM32F10xxx闪存编程参考手册》3寄存器说明/3.1闪存访问控制寄存器。
表11.2.1.1 FLASH推荐的等待状态和编程延迟数
从上可以看出,我们设置值为FLASH_LATENCY_2,也就是2WS,也就是3个CPU周期,为什么呢?因为经过上面的配置之后,系统时钟频率达到了最高的72MHz,对应的就是两个等待状态,所以选择FLASH_LATENCY_2。
时钟系统配置相关知识就给大家讲解到这里。
11.2.2 STM32F1时钟使能和配置
上一节我们讲解了时钟系统配置步骤。在配置好时钟系统之后,如果我们要使用某些外设,例如GPIO,ADC等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前没有使能外设时钟,这个外设是不可能正常运行的。STM32的外设时钟使能是在RCC相关寄存器中配置的。因为RCC相关寄存器非常多,有兴趣的同学可以直接打开《STM32F10xxx参考手册_V10(中文版).pdf》6.3小节查看所有RCC相关寄存器的配置。接下来我们来讲解通过STM32F1的HAL库使能外设时钟的方法。
在STM32F1的HAL库中,外设时钟使能操作都是在RCC相关固件库文件头文件STM32F1xx_hal_rcc.h定义的。大家打开STM32F1xx_hal_rcc.h头文件可以看到文件中除了少数几个函数声明之外大部分都是宏定义标识符。外设时钟使能在HAL库中都是通过宏定义标识符来实现的。首先,我们来看看GPIOA的外设时钟使能宏定义标识符:
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
UNUSED(tmpreg); \
} while(0U)
这段代码主要是定义了一个宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE(),它的核心操作是通过下面这行代码实现的:
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
这行代码的作用是,设置寄存器RCC->APB2ENR的相关位为1,至于是哪个位,是由宏定义标识符RCC_APB2ENR_IOPAEN的值决定的,而它的值为:
#define RCC_APB2ENR_IOPAEN_Pos (0U)
#define RCC_APB2ENR_IOPAEN_Msk (0x1UL << RCC_APB2ENR_IOPAEN_Pos)
#define RCC_APB2ENR_IOPAEN RCC_APB2ENR_IOPAEN_Msk
上面三行代码很容易计算出来RCC_APB2ENR_IOPAEN= (0x00000001<<2),因此上面代码的作用是设置寄存器RCC->APB2ENR寄存器的位2为1。我们可以从STM32F1的参考手册中搜索APB2ENR寄存器定义,位2的作用是用来使用GPIOA时钟。APB2ENR寄存器的位2描述如下:
位0 IOPAEN:IO端A时钟使能(I/O port A clock enable)
由软件置‘1’或清‘0’
0:IO端口A时钟关闭
1:IO端口A时钟开启
那么我们只需要在我们的用户程序中调用宏定义标识符就可以实现GPIOA时钟使能。使用方法为:
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能GPIOA时钟 */
对于其他外设,同样都是在STM32F1xx_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->APB2ENR) &= ~ (RCC_APB2ENR_GPIOAEN)
同样,宏定义标识符__HAL_RCC_GPIOA_CLK_DISABLE()的作用是设置RCC->APB2ENR寄存器的位2为0,也就是禁止GPIOA时钟。具体使用方法我们这里就不做过多讲解,我们这里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:
__HAL_RCC_DMA1_CLK_DISABLE(); /* 禁止DMA1时钟 */
__HAL_RCC_USART2_CLK_DISABLE(); /* 禁止串口2时钟 */
__HAL_RCC_TIM1_CLK_DISABLE(); /* 禁止TIM1时钟 */
关于STM32F1的外设时钟使能和禁止方法我们就给大家讲解到这里。