Wiki:知识&笔记 Wiki:知识&笔记
首页
  • 学习笔记

    • 《JavaScript教程》笔记
  • 嵌入式

    • STM32
  • 技术文档
归档
GitHub (opens new window)
首页
  • 学习笔记

    • 《JavaScript教程》笔记
  • 嵌入式

    • STM32
  • 技术文档
归档
GitHub (opens new window)
  • 基础

  • 底层&寄存器

    • 寄存器操作基础
    • 时钟配置
      • 时钟树
      • 计算分析
        • PLL分析
        • 常用时钟
      • HAL库设置时钟
        • 一般流程
        • 配置时钟源
        • 配置系统时钟
      • 代码示例
        • CubeMX
        • 正点原子
      • One More Thing
        • 指令周期
        • 机器周期
        • 时钟周期
    • 滴答定时器
    • GPIO工作原理
    • GPIO位带操作
  • HAL库外设

  • STM32
  • 底层&寄存器
2020-11-17
时钟树
计算分析
PLL分析
常用时钟
HAL库设置时钟
一般流程
配置时钟源
配置系统时钟
代码示例
CubeMX
正点原子
One More Thing
指令周期
机器周期
时钟周期

时钟配置

# 时钟树设置

# 时钟树

基于 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 初始化时钟设置,但是知道这种方法也很重要。

# 一般流程

  1. 使能 PWR 时钟:调用函数 __HAL_RCC_PWR_CLK_ENABLE()
  2. 设置调压器输出电压级别 :调用函数 __HAL_PWR_VOLTAGESCALING_CONFIG()
  3. 选择是否开启 Over Driver 功能:调用函数 HAL_PWREx_EnableOverDrive()
  4. 配置时钟源相关参数:调用函数 HAL_RCC_OscConfig()
  5. 配置系统时钟源以及 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;
1
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
    • 设置各个分频系数与倍频系数

# 配置系统时钟

主要使用的是 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;
1
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

而这个函数的第二个参数 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();
    }
}
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
30
31
32
33
34
35
36
37

这段代码实现了这样的时钟配置:

s

CubeMX 同时打开了 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);
}
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周期与指令周期?

我们从大到小说:

# 指令周期

我们知道一条指令的执行大致可以分为三个阶段:

  1. Fetch(取指),也就是从 PC 寄存器里找到对应的指令地址,根据指令地址从内存里把具体的指令,加载到指令寄存器中,然后把 PC 寄存器自增,好在未来执行下一条指令。
  2. Decode(译码),也就是根据指令寄存器里面的指令,解析成要进行什么样的操作,是 R、I、J 中的哪一种指令,具体要操作哪些寄存器、数据或者内存地址。
  3. Execute(执行指令),也就是实际运行对应的 R、I、J 这些特定的指令,进行算术逻辑操作、数据传输或者直接的地址跳转。

简单来讲,执行完这三个阶段所需要的时间就是指令周期

# 机器周期

机器周期又称为 CPU 周期。在 CPU 执行指令时,通常一个指令会由若干基本操作组成(我们一般称取指、读存储器或写存储器这些基本操作为基本操作),执行完一次基本操作所需要的时间称为机器周期。通常我们称内存中读取一个指令字所需要的最短时间为一个CPU周期

# 时钟周期

时钟周期就是时钟频率的倒数。前面提到指令中的基本操作,,而这些基本操作又是由许多CPU的基本操作组成的,执行一个这样的CPU的基本操作所需要的时间就恰好是一个时钟周期

所以总的来讲,三者的关系大概如图:

编辑 (opens new window)
上次更新: 2020/11/18, 10:11:00
寄存器操作基础
滴答定时器

← 寄存器操作基础 滴答定时器→

Theme by Vdoing | Copyright © 2020-2025 Jack :) | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式