时钟配置
# 时钟树设置
# 时钟树
基于 F4 系列分析
这是 STM32 的时钟树:
STM32 的时钟来源主要有5个:HSI、HSE、LSI、LSE、PLL,它们究竟是什么请参考之前写过的这个,其中 PLL 分为主 PLL 与 专用 PLL,它也被列为高速时钟。
# 计算分析
# PLL分析
PLL 为锁相环倍频 输出。F4 系列有两个 PLL:
主 PLL(PLL)由 HSE 或者 HSI 提供时钟信号,并具有两个不同的输出时钟。
第一个输出 PLLP 用于生成高速的系统时钟(最高 168MHz)
第二个输出 PLLQ 用于生成 USB OTG FS 的时钟(48MHz),随机数发生器的时钟和 SDIO 时钟。
专用 PLL(PLLI2S)用于生成精确时钟,从而在 I2S 接口实现高品质音频性能
对于主 PLL 我们可以分析一下,时钟信号是如何通过此处进行倍频的:
时钟信号进入 PLL 之前首先经过一个分频系数为 M 的分频器,随后经过 N 倍的倍频器之后,再次经过三个系数为分别为 P、Q、R 的分频器之后,输出最终的 PLL 时钟信号(分别为 PLLP、PLLQ 信号)
例如:令 M = 2,N = 8,P = 4,输入时钟信号为 24MHz。则输出将为:
# 常用时钟
A~G 为我们常用的时钟,5 个主要的时钟来源为他们提供信号,其中:
A 为这里是看门狗时钟输入。看门狗时钟源只能是低速的 LSI 时钟。
B 为 RTC 时钟源,从图上可以看出,RTC 的时钟源可以选择 LSI,LSE,以及 HSE 分频后的时钟,HSE 分频系数为 2~31
C 为输出时钟 MCO1 和 MCO2。
- MCO1 是向芯片的 PA8 引脚输出时钟。它有四个时钟来源分别为:HSI, LSE, HSE 和 PLL 时钟。
- MCO2 是向芯片的 PC9 输出时钟,它同样有四个时钟来源分别为:输出时钟,它同样有四个时钟来源分别为:HSE, PLL,SYSCLK 以及以及 PLLI2S 时钟。MCO 时钟输出时钟频率最大不超过 100MHz
D 为系统时钟。从图中可以看出,SYSCLK 系统时钟来源有三个方面:HSI, HSE 和 PLL。在我们实际应用中,因为对时钟速度要求都比较高我们才会选用 STM32F4 这种级别的处理器,所以一般情况下,都是采用 PLL 作为 SYSCLK 时钟源。根据前面的计算公式,就可以算出系统的 SYSCLK 是多少。
E 中:这里我们指的是以太网 PTP 时钟,AHB 时钟,APB2 高速时钟,APB1 低速时钟。其中以太网 PTP 时钟是使用系统时钟。其中 。其中有很多 xCLK 容易让我们迷惑,下面就分清他们:
- HCLK:系统时钟 SYSCLK 经过 AHB 分频器(AHB Prescaler) 处理之后得到的时钟频率,不管是看上面的大图还是 CubeMX 上的时钟树图,都可以看到后面的 APBx 分频、直接输出到 Cortex 内核的定时器时钟频率都来源于这个 HCLK,其最大时钟为 168MHz
- PCLKx:在 HCLK 时钟频率输出之后,APBx 分频器(APBx Prescaler) 处理后得到的时钟频率为 PCLKx (x = 1 或 2),这个时钟频率将直接为挂载在 APBx 总线上的外设提供时钟。而再次经过一个 APBx 倍频系数后才为 APBx 总线上的时钟频率。其中 AHB , APB2 高速时钟最大频率为 84MHz,而 APB1 低速时钟最大频率为 42MHz
- FCLK:Cortex 自出运行时钟频率
- 乘以倍频系数:分频系数 1 或 8 可以得到 Cortex 系统定时器运行的频率,这个定时器也就是滴答定时器的其中一个时钟源,顺便提一下,在 HAL 中,系统自动生成的代码工程中,滴答定时器的中断为 1ms 一次。但是关于 HAL 对滴答定时器的 bug 可以参考这篇文章 (opens new window),滴答定时器的时钟源只是 HCLK 而已(好像是这样)
F 为 I2S 时钟源。从图中可以看出,I2S 的时钟源来源于 PLLI2S 或者映射到 I2S_CKIN 引脚的外部时钟。 I2S 出于音质的考虑,对时钟精度要求很高
G 为 STM32 内部以太网 MAC 时钟的来源。对于 MII 接口来说,必须向外部 PHY 芯片提供 25MHz 的时钟,这个时钟,可以由 PHY 芯片外接晶振,或者使用 STM32F4 的 MCO 输出来提供。然后,PHY 芯片再给 STM32F4 提供 ETH_MII_TX_CLK 和 ETH_MII_RX_CLK 时钟。对于 RMII 接口来说,外部必须提供 50MHz 的时钟驱动 PHY 和 STM32F4 的 ETH_RMII_REF_CLK,这个 50MHz 时钟可以来自 PHY、有源晶振或者 STM32F4 的 MCO。我们的开发板使用的是 RMII 接口,使用 PHY 芯片提供 50MHz 时钟驱动 STM32F4 的 ETH_RMII_REF_CLK。
# HAL库设置时钟
HAL 库中系统初始化并没有设置时钟的相关设置,所以我们必须要自行设置时钟。虽然可以通过 CubeMX 初始化时钟设置,但是知道这种方法也很重要。
# 一般流程
- 使能 PWR 时钟:调用函数
__HAL_RCC_PWR_CLK_ENABLE()
- 设置调压器输出电压级别 :调用函数
__HAL_PWR_VOLTAGESCALING_CONFIG()
- 选择是否开启 Over Driver 功能:调用函数
HAL_PWREx_EnableOverDrive()
- 配置时钟源相关参数:调用函数
HAL_RCC_OscConfig()
- 配置系统时钟源以及 AHB, APB1 和 APB2 的分频系数:调用函数
HAL_RCC_ClockConfig()
其中,4 和 5 为主要的步骤,而前三步如果不需要的话的不是必要的
# 配置时钟源
在工程中,我们配置的为使用的的时钟源,也就是 HSE、LSE、HSI 或者 LSI,是通过函数HAL_RCC_OscConfig()
实现的:
__weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);
// 参数结构体的定义
typedef struct {
// Step1 -------------------
uint32_t OscillatorType; //需要选择配置的振荡器类型
// Step2 -------------------
uint32_t HSEState; //HSE状态
uint32_t HSEPredivValue; //HSE预分频系数
uint32_t LSEState; //LSE状态
uint32_t HSIState; //HIS状态
uint32_t HSICalibrationValue; //HIS校准值
uint32_t LSIState; //LSI状态
// Step3 -------------------
RCC_PLLInitTypeDef PLL; //PLL配置
}RCC_OscInitTypeDef;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
其实对于此的初始化就是将参数填入这个结构体中即可,主要是三步,已在注释中写好:
选择振荡器类型:如后面要开启 HSE,则
OscillatorType
填入RCC_OSCILLATORTYPE_HSE
即可在上一步选择哪个振荡器就把相应的
xxxState
调成RCC_HSE_ON
即可,其他的振荡器语句相似调节时钟源的预分频系数,也就是下面图 HSE 前的
/1
这个系数,为 1 即设为RCC_HSE_PREDIV_DIV1
即可设置 PLL 参数,这个结构体成员也是一个结构体,该结构体主要用来设置 PLL 时钟源以及相关分频倍频参数。形象的讲就是 CubeMX 里面的这个部分:
这个结构体是这个样子的:
typedef struct { uint32_t PLLState; // PLL 状态 uint32_t PLLSource; // PLL 时钟源 // 在 F4 系列 ----------------------- uint32_t PLLM; // PLL 分频系数 M uint32_t PLLN; // PLL 倍频系数 N uint32_t PLLP; // PLL 分频系数 P uint32_t PLLQ; // PLL 分频系数 Q // F1 系列中只有 -------------------- uint32_t PLLMUL; // PLL 主分频系数 }RCC_PLLInitTypeDef; // 初始化函数 __weak HAL_RCC_OscConfig(RCC_PLLInitTypeDef *PLL);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15从总图中可以看出,PLL 输出到 SYSCLK 的时钟频率走的是 PLLP,所以这个参数一定要设置!而其余的参数设置则与上面几乎一致
- 打开 PLL 则将
PLLState
设置为RCC_PLL_ON
- 设置 PLL 来源,根据上一个结构体指定的源设置,若为 HSE,则为:
RCC_PLLSOURCE_HSE
- 设置各个分频系数与倍频系数
- 打开 PLL 则将
# 配置系统时钟
主要使用的是 HAL_RCC_ClockConfig()
这个函数,它有两个参数:
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency);
// 第一个参数结构体定义
typedef struct
{
uint32_t ClockType;
uint32_t SYSCLKSource;
uint32_t AHBCLKDivider;
uint32_t APB1CLKDivider;
uint32_t APB2CLKDivider;
} RCC_ClkInitTypeDef;
2
3
4
5
6
7
8
9
10
11
第一个结构体参数中:
首个成员
ClockType
指定了你接下来需要设定的参数的东西,一般我们会同时设定 SYSCLK、HCLK、PCLK1 与 PCLK2,可以这样写:RCC_ClkInitStructure.ClockType = (RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2);
1随后通过
SYSCLKSource
设置系统时钟的输入源,这里我们一般会选择刚刚设置过的 PLL,即将此成员设置为RCC_SYSCLKSOURCE_PLLCLK
下面的三个成员分别设置了 AHB、APB1、APB2 的分频系数,其中:
- 设置 AHB 时,由于其输入为系统时钟,所以格式为
RCC_SYSCLK_DIV1
,即设置 AHB 分频系数为1,改变时只需要改变后面的数字即可(但要在规定范围内) - 设置 APBx 时,其输入为 HCLK,所以格式为
PCC_HCLK_DIV2
,将 APBx 分频系数设为2
- 设置 AHB 时,由于其输入为系统时钟,所以格式为
而这个函数的第二个参数 FLatency
用于设置 FLASH 延迟
关于 FLASH 延迟周期,你目前只需要知道是由于 CPU 运行的速度要大于 FLASH 读写的速度,所以要设置 CPU 等待 FLASH。在时钟初始化前,FLASH 延迟周期默认是 0,即一个等待周期,这个时候需要将
LATENCY
也就是等待周期寄存器设为 5。但是在初始化时钟,将系统时钟频率设为 72 MHz 后,虽然 FLASH 需要 6 个 CPU 等待周期,但是由于 STM32F4 具有自适应实时存储器加速器(ART Accelerator)Accelerator),通过指令缓存存储器,预取指令,实现相当于 0 FLASH 等待的运行速度。关于自适应实时存储器加速器的详细介绍,可以参考《 STM32F4xx 中文参考手册》3.4.2节。但是一般也将这个值设为 2
对于流程的前三项,不需要 RTC 实时时钟以及唤醒等功能的话一般是不需要设置的。所以可以参考一下 CubeMX 自动生成的代码:
# 代码示例
# CubeMX
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; //时钟源为HSE
RCC_OscInitStruct.HSEState = RCC_HSE_ON; //打开HSE
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; //HSE预分频
RCC_OscInitStruct.HSIState = RCC_HSI_ON; //时钟源为HSI
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; //打开PLL
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; //PLL时钟源选择HSE
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; //主PLL倍频因子
if(HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) //初始化
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| 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_DIV2; //APB1分频系数为2
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; //APB2分频系数为1
if(HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) //同时设置FLASH延时周期为2WS,也就是3个CPU周期
{
Error_Handler();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
这段代码实现了这样的时钟配置:
sCubeMX 同时打开了 HSE 与 HSI,随后进行初始化
# 正点原子
这是正点原子的参考初始化时钟例程:
// 时钟系统配置函数
// PLL:选择的倍频数,RCC_PLL_MUL2~RCC_PLL_MUL16
// 返回值:0,成功;1,失败
void Stm32_Clock_Init(u32 PLL)
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStructure;
RCC_ClkInitTypeDef RCC_ClkInitStructure;
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开HSE
RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE预分频
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打开PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE; //PLL时钟源选择HSE
RCC_OscInitStructure.PLL.PLLMUL=PLL; //主PLL倍频因子
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
if(ret!=HAL_OK) while(1);
//选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK; //设置系统时钟时钟源为PLL
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1; //AHB分频系数为1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1分频系数为2
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2分频系数为1
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2); //同时设置FLASH延时周期为2WS,也就是3个CPU周期。
if(ret!=HAL_OK) while(1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
相较于 CubeMX 自动生成的程序,这里的 PLL 主倍频系数需要自己设定,其余并无差异。
无特殊需求的话,正常开发做到这些就足够了
# One More Thing
什么是时钟周期、CPU周期与指令周期?
我们从大到小说:
# 指令周期
我们知道一条指令的执行大致可以分为三个阶段:
- Fetch(取指),也就是从 PC 寄存器里找到对应的指令地址,根据指令地址从内存里把具体的指令,加载到指令寄存器中,然后把 PC 寄存器自增,好在未来执行下一条指令。
- Decode(译码),也就是根据指令寄存器里面的指令,解析成要进行什么样的操作,是 R、I、J 中的哪一种指令,具体要操作哪些寄存器、数据或者内存地址。
- Execute(执行指令),也就是实际运行对应的 R、I、J 这些特定的指令,进行算术逻辑操作、数据传输或者直接的地址跳转。
简单来讲,执行完这三个阶段所需要的时间就是指令周期
# 机器周期
机器周期又称为 CPU 周期。在 CPU 执行指令时,通常一个指令会由若干基本操作组成(我们一般称取指、读存储器或写存储器这些基本操作为基本操作),执行完一次基本操作所需要的时间称为机器周期。通常我们称内存中读取一个指令字所需要的最短时间为一个CPU周期
# 时钟周期
时钟周期就是时钟频率的倒数。前面提到指令中的基本操作,,而这些基本操作又是由许多CPU的基本操作组成的,执行一个这样的CPU的基本操作所需要的时间就恰好是一个时钟周期
所以总的来讲,三者的关系大概如图: