03. STM32的时钟系统
一、STM32F4的时钟树
其中,A 部分表示 输入时钟源,可分为 外部时钟源 和 内部时钟源;B 为 锁相环 “PLL”;C 为 系统时钟源选择器,此项决定了 MCU 的系统主时钟 “SYSCLK” 的大小;AHB 预分频器将 SYSCLK 分频或不分频后分发给其它外设进行处理,包括到 D 部分的 Cortex-M 内核系统的时钟 和使能单元。E 为 定时器以及其它外设的时钟源 APB1/APB2。F 是 STM32 的 MCO 时钟输出功能。
二、时钟源
对于 STM32F4,输入时钟源(Input Clock)主要包括 HSI
,HSE
,LSI
,LSE
。其中,从时钟频率来分可以分为 高速时钟源 和 低速时钟源,其中 HSI、HSE 是 高速时钟,LSI 和 LSE 是 低速时钟。从来源可分为 外部时钟源 和 内部时钟源,外部时钟源 就是从 外部通过接晶振 的方式获取时钟源,其中 HSE 和 LSE 是 外部时钟源;HSI 和 LSI 是内部时钟源,芯片上电即可产生,不需要借助外部电路。
- 高速外部振荡器 HSE(High Speed External Clock signal)
- 外接石英/陶瓷谐振器,频率为 4MHz~26MHz。开发板使用的是 8MHz。
- 低速外部振荡器 LSE(Low Speed External Clock signal)
- 外接 32.768kHz 石英晶体,主要作用于 RTC 的时钟源。
- 高速内部振荡器 HSI(High Speed Internal Clock signal)
- 由内部 RC 振荡器产生,频率为 16MHz。
- 低速内部振荡器 LSI(Low Speed Internal Clock signal)
- 由内部 RC 振荡器产生,频率为 32kHz,可作为独立看门狗的时钟源。
芯片上电时默认由内部的 HSI 时钟启动,如果用户进行了硬件和软件的配置,芯片才会根据用户配置调试尝试切换到对应的外部时钟源。
2 个外部时钟源都是芯片外部晶振产生的时钟频率,故而都有精度高的优点。
三、锁相环PLL
锁相环是自动控制系统中常用的一个反馈电路,在 STM32 主控中,锁相环的作用主要有两个部分:输入时钟净化 和 倍频。前者是利用锁相环电路的反馈机制实现,后者我们用于使芯片在更高且频率稳定的时钟下工作。
在 STM32 中,锁相环的输出也可以作为芯片系统的时钟源。根据图 11.1.1 的时钟结构,使用锁相环时只需要进行三个部分的配置。为了方便查看,截取了使用 PLL 作为系统时钟源的配置部分,如图 11.1.2.1 所示。
【1】、PLL Source Mux:PLL 时钟源选择器
图中标号 ① 表示的是 PLL 时钟源的选择器,由寄存器 RCC_PLLCFGR 的 bit22 位进行控制,关于该寄存器的描述可参考《STM32F4xx 参考手册_V4(中文版).pdf》第 116 页,如下图所示:
寄存器 RCC_PLLCFGR 的 bit22 位有两种可选择的输入源:一个是内部时钟 HSI 信号,另一个是外部时钟 HSE 信号。
【2】、PLLM:HSE 分频器作为 PLL 输入 (HSE divider for PLL entry)
标号 ② 的地方,主 PLL 输入时钟的分频系数,并把它的控制功能放在 RCC_PLLCFGR 寄存器中。
【3】、PLLMUL:PLL 倍频系数 (PLL multiplication factor)
图中 ③ 所表示的配置锁相环倍频系数,同样地可以查到在 STM32F4 系列中,ST 设置它的有效倍频范围为 2~16 倍。结合时钟树,要实现 168MHz 的主频率,我们通过选择 HSE 分频作为 PLL 输入的时钟信号,输入 8Mhz,8 分频,即 1MHz,通过标号 ③ 选择倍频因子,我们选择 336 倍频,这样可以得到时钟信号为 1*336=336MHz,然后经过 2 分频,得到 168MHz。
四、系统时钟SYSCLK
STM32 的系统时钟 SYSCLK 为整个芯片提供了时序信号。对于相同的稳定运行的电路,时钟频率越高,指令的执行速度越快,单位时间能处理的功能越多。STM32 的系统时钟是可配置的,在 STM32F4 系列中,它可以为 HSI、PLLCLK、HSE 中的一个,通过 CFGR 的位 SW[1:0] 设置。
标号 ④ 为系统时钟输入源选择,可选时钟信号有外部高速时钟 HSE(8M)、内部高速时钟 HSI(16M)和经过倍频的 PLL CLK(168M)。这里我们选择 PLL CLK 作为系统时钟,此时系统时钟的频率为 168MHz。系统时钟来到标号 ⑤ 的 AHB 预分频器,其中可选择的分频系数为 1,2,4,8,16,32,64,128,256,512,我们选择不分频,所以 AHB 总线时钟达到最大的 168MHz。
APB1 总线时钟,由 HCLK 经过标号 ⑥ 的低速 APB1 预分频器得到,分频因子可以选择 1,2,4,8,16,这里我们选择的是 4 分频,所以 APB1 总线时钟为 42M。由于 APB1 是低速总线时钟,APB1 总线最高频率为 42MHz,片上低速的外设就挂载在该总线上,例如有看门狗定时器、定时器 2/3/4/5/6/7、RTC 时钟、USART2/3/4/5、SPI2(I2S2)与 SPI3(I2S3)、I2C1~3、CAN 和 2 个 DAC。
APB2 总线时钟,由 HCLK 经过标号 ⑦ 的高速 APB2 预分频器得到,分频因子可以选择 1,2,4,8,16,这里我们选择的是 2 分频,所以 APB2 总线时钟频率为 84M。与 APB2 高速总线连接的外设有定时器 1/8/9/10/11、SPI1、USART1 和 USART6、3 个 ADC 和 SDIO 接口。
其中标号 ⑧ 决定了定时器时钟频率,该位由硬件自动设置,分为两种情况:
- 如果 APB 预分频器为 1,定时器时钟频率等于 APB 域的频率
- 否则,等于 APB 域的频率的两倍(×2)
此外,AHB 总线时钟直接作为 GPIO(A\B\C\D\E\F\G\H\I)、以太网、DCMI、FSMC、AHB总线、Cortex 内核、存储器和 DMA 的 HCLK 时钟,并作为 Cortex 内核自由运行时钟 FCLK。
标号 ⑨ 是 RTC 时钟,其时钟源有三个途径:HSE/x(x = 2~31)、LSE 或 LSI。
五、时钟信号输出MCO
标号 ⑩ 是 MCO 时钟输出,其作用是为外部器件提供时钟。STM32 允许通过设置,通过 MCO 引脚输出一个稳定的时钟信号。可以从上图看到 STM32F4 时钟系统图标号 ⑩、⑪、⑫ 三个部分:
-
⑫MCO1\MCO2 时钟源选择器
- MCO1(外部器件的输出时钟 1)时钟源有四个:LSE、HSE、HSI 和 PLLCLK。
- MCO2(外部器件的输出时钟 2)时钟源有四个:SYSCLK、PLLI2SCLK、HSE 和 PLLCLK。
-
⑪MCO1\MCO2 时钟分频器
- MCO1 和 MCO2 的预分频器,取值范围均为:1 到 5。
-
⑩MCO1\MCO2 时钟输出引脚
- MCO1、MCO2 两个时钟输出引脚给外部器件提供时钟源(分别由 PA8 和 PC9 复用功能实现),每个引脚可以选择一个时钟源,通过 RCC 时钟配置寄存器(RCC_CFGR)进行配置。
六、修改时钟主频
STM32F407 默认的情况下,使用的是内部 8M 的 HSI 作为时钟源,所以不需要外部晶振也可以下载和运行代码的。STM32F407 芯片在 168MHz 的频率下工作,168MHz 是官方推荐使用的最高的稳定时钟频率。而开发板的外部高速晶振的频率就是 8MHz,我们就是在这个晶振频率的基础上,通过各种倍频和分频得到 168MHz 的系统工作频率。
【1】、配置 HSE_VALUE
stm32f4xx_hal_conf.h 文件 84 行左右,有个 HSE_VALUE 参数,这个参数表示我们的外部高速晶振的频率。这个参数请务必根据我们板子外部焊接的晶振频率来修改,官方默认是 25M。这里,我们使用的开发板,晶振频率为 8MHz。
#if !defined (HSE_VALUE)
/* 外部高速振荡器的值,单位 HZ */
#define HSE_VALUE ((uint32_t)8000000)
#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_stm32f4xx.c 文件下定义的 SystemInit() 程序,源码在 168 行到 183 行左右。
void SystemInit(void)
{
/* FPU settings ------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
/* set CP10 and CP11 Full Access */
SCB->CPACR |= ((3UL << 10 * 2) | (3UL << 11 * 2));
#endif
#if defined(DATA_IN_ExtSRAM) || defined(DATA_IN_ExtSDRAM)
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
/* Configure the Vector Table location -------------------------------------*/
#if defined(USER_VECT_TAB_ADDRESS)
/* Vector Table Relocation in Internal SRAM */
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
#endif /* USER_VECT_TAB_ADDRESS */
}
从上面代码可以看出,SystemInit() 函数主要做了如下两个方面工作:
- 外部存储器配置
- 中断向量表地址配置
【3】、在 main() 函数里调用用户编写的时钟设置函数
我们可以调用函数 HAL_RCC_OscConfig()
来配置 时钟源相关参数,它的函数说明如下:
HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);
该函数只有一个形参,就是结构体 RCC_OscInitTypeDef 类型指针。它的定义如下:
typedef struct
{
uint32_t OscillatorType; // 需要配置的振荡器
uint32_t HSEState; // HSE状态
uint32_t LSEState; // LSE状态
uint32_t HSIState; // HSI状态
uint32_t HSICalibrationValue; // HSI校准值,范围0x00~0x1F
uint32_t LSIState; // LSI状态
RCC_PLLInitTypeDef PLL; // PLL初始化结构体
}RCC_OscInitTypeDef;
RCC_OscInitTypeDef 这个结构体还有一个很重要的成员变量是 PLL,它是结构体RCC_PLLInitTypeDef 类型。它的作用是配置 PLL 相关参数,它的定义如下:
typedef struct
{
uint32_t PLLState; // PLL状态
uint32_t PLLSource; // PLL时钟源
uint32_t PLLM; // PLL分频系数M
uint32_t PLLN; // PLL倍频系数N
uint32_t PLLP; // PLL分频系数P
uint32_t PLLQ; // PLL分频系数Q
}RCC_PLLInitTypeDef;
然后,我们可以调用函数 HAL_RCC_ClockConfig()
配置系统时钟源,以及 SYSCLK、AHB、APB1 和 APB2 相关参数,它的函数说明如下:
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency);
该函数有两个形参,第一个形参是 RCC_ClkInitTypeDef 结构体类型指针变量,用于设置 SYSCLK 时钟源以及 SYSCLK、AHB、APB1 和 APB2 的分频系数。
RCC_ClkInitTypeDef 结构体类型定义如下:
typedef struct
{
uint32_t ClockType; // 要配置的时钟(SYSCLK/HCLK/PCLK1/PCLK2)
uint32_t SYSCLKSource; // 系统时钟源
uint32_t AHBCLKDivider; // AHB时钟预分频系数
uint32_t APB1CLKDivider; // APB1时钟预分频系数
uint32_t APB2CLKDivider; // APB2时钟预分频系数
}RCC_ClkInitTypeDef;
第二个参数 FLatency 用于设置 FLASH 延迟,为了使 FLASH 读写正确(因为 168Mhz 的时钟比 Flash 的操作速度 24Mhz 要快得多,操作速度不匹配容易导致 Flash 操作失败),所以需要设置延时时间。对于 STM32F4 系列,FLASH 延迟配置参数值是通过下表来确定的:
七、自定义时钟配置函数
首先配置了时钟源相关参数,我们开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE,然后把system_stm32_clock_init() 函数的形参直接设置作为 PLL 的参数的值,这样就达到了设置 PLL 时钟源相关参数的目的。
设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率,然后我们要配置系统时钟源,以及 SYSCLK、AHB、APB1 和 APB2 相关参数。
根据我们在 main() 函数中调用 System_Clock_Init(8, 336, 2, 7)
时设置的形参数值,我们可以计算出,PLL 时钟为 PLLCLK = HSE * 336 / 8 / 2 = 168MHz。同时我们选择系统时钟源为 PLL,所以系统时钟 SYSCLK=168MHz。AHB 分频系数为 1,故频率为 HCLK=SYSCLK/1=168MHz 。 APB1 分频系数为 4,故其频率为 PCLK1=HCLK/4=42MHz。APB2 分频系数为 2,故其频率为 PCLK2=HCLK/2=84MHz。
/**
* @brief 时钟设置函数
*
* @param pllm: 主PLL和音频PLL预分频系数(进PLL之前的分频), 取值范围:2 ~ 63
* @param plln: 主PLL倍频系数(PLL倍频), 取值范围: 64 ~ 432
* @param pllp: 主PLL的p分频系数(PLL之后的分频),分频后作为系统时钟,取值范围: [2, 4, 6, 8](仅限这4个值)
* @param pllq: 主PLL的q分频系数(PLL之后的分频),取值范围: 2 ~ 15
* @return uint8_t 错误代码: 0,成功;1,错误;
*
* @note
* 外部晶振为 8M的时候, 推荐值: pllm = 8, plln = 336, pllp = 2, pllq = 7.
* F(vco): F(hse) / pllm * plln = 8 / 8 * 336 = 336Mhz
* F(PLL48CK) = F(vco) / pllq = F(hse) / pllm * plln / pllq = 336 / 7 = 48Mhz
* F(SYSCLK) = F(PLLCLK) = F(vco) / pllp = F(hse) / pllm * plln / pllp = 336 / 2 = 168Mhz
* F(AHB1/2/3) = F(SYSCLK) = 168Mhz
* F(HCLK) = F(AHB1/2/3) = 168Mhz
* F(APB1) = pll_p_ck / 4 = 42Mhz
* F(APB2) = pll_p_ck / 2 = 84Mhz
*/
uint8_t System_Clock_Init(uint32_t pllm, uint32_t plln, uint32_t pllp, uint32_t pllq)
{
HAL_StatusTypeDef result = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE(); // 使能PWR时钟
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); // 设置调压器输出电压级别,以便在器件未以最大频率工作
// 使能HSE,并选择HSE作为PLL时钟源,配置PLL1,开启USB时钟
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 时钟源为HSE
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 打开HSE
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // PLL时钟源选择HSE
RCC_OscInitStruct.PLL.PLLM = pllm;
RCC_OscInitStruct.PLL.PLLN = plln;
RCC_OscInitStruct.PLL.PLLP = pllp;
RCC_OscInitStruct.PLL.PLLQ = pllq;
result = HAL_RCC_OscConfig(&RCC_OscInitStruct); // 初始化RCC
if(result != HAL_OK)
{
// 时钟初始化失败,可以在这里加入自己的处理
return 1;
}
// 选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2
RCC_ClkInitStruct.ClockType = ( RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 设置系统时钟时钟源为PLL
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB分频系数为1
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; // APB1分频系数为4
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; // APB2分频系数为2
result = HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5); // 同时设置FLASH延时周期为5WS,也就是6个CPU周期
if(result != HAL_OK)
{
// 时钟初始化失败,可以在这里加入自己的处理
return 1;
}
// STM32F405x/407x/415x/417x Z版本的器件支持预取功能
if (HAL_GetREVID() == 0x1001)
{
__HAL_FLASH_PREFETCH_BUFFER_ENABLE(); // 使能flash预取
}
return 0;
}