天天看点

STM32H7 时钟系统配置

在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系统一些初始化配置。那么我们先来看看 SystemInit 程序:

void SystemInit (void)
{
//如果需要 FPU 的话就使能 FPU,设置 CP10 和 CP11 为全访问
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));
#endif
/* 复位 RCC 时钟配置为默认配置-----------*/
RCC->CR |= RCC_CR_HSION; //打开 HSION 位
RCC->CFGR = 0x00000000; //复位 CFGR 寄存器
RCC->CR &= (uint32_t)0xEAF6ED7F;//清除 HSEON,CSSON,CSION,RC48ON,
//CSIKERON,PLL1ON,PLL2ON 和 PLL3ON 位
RCC->D1CFGR=0x00000000;  //复位 D1CFGR 寄存器
RCC->D2CFGR=0x00000000;  //复位 D2CFGR 寄存器
RCC->D3CFGR = 0x00000000; //复位 D3CFGR 寄存器
RCC->PLLCKSELR = 0x00000000;  //复位 PLLCKSELR 寄存器
RCC->PLLCFGR = 0x00000000; //复位 PLLCFGR 寄存器
RCC->PLL1DIVR = 0x00000000; //复位 PLL1DIVR 寄存器
RCC->PLL1FRACR = 0x00000000;  //复位 PLL1FRACR 寄存器
RCC->PLL2DIVR = 0x00000000; //复位 PLL2DIVR 寄存器
RCC->PLL2FRACR = 0x00000000;  //复位 PLL2FRACR 寄存器
RCC->PLL3DIVR = 0x00000000; //复位 PLL3DIVR 寄存器
RCC->PLL3FRACR = 0x00000000;  //复位 PLL3FRACR 寄存器
RCC->CR &= (uint32_t)0xFFFBFFFF; //清除 CR 寄存器的 HSEBYP 位
RCC->CIER = 0x00000000; //关闭所有的中断
*((__IO uint32_t*)0x51008108) = 0x00000001;
//如果要使用外部 RAM 的话就初始化外部 RAM
#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM)
SystemInit_ExtMemCtl();
#endif
/* 配置中断向量表地址=基地址+偏移地址 ------------------*/
#ifdef VECT_TAB_SRAM
SCB->VTOR = D1_AXISRAM_BASE | VECT_TAB_OFFSET;
#else
SCB->VTOR = FLASH_BANK1_BASE | VECT_TAB_OFFSET;
#endif
}
           

从上面代码可以看出,SystemInit 主要做了如下三个方面工作:

  1. FPU 设置
  2. 复位 RCC 时钟配置为默认复位值(默认开启 HSI)
  3. 中断向量表地址配置

    HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。

    HAL 库的 SystemInit 函数除了打开 HSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们必须编写自己的时钟配置函数。首先我们打开工程模板看看我们在工程 SYSTEM 分组下面定义的 sys.c 文件中的时钟初始化函数 Stm32_Clock_Init 的内容:

//时钟设置函数
//Fvco=Fs*(plln/pllm);
//Fsys=Fvco/pllp=Fs*(plln/(pllm*pllp));
//Fq=Fvco/pllq=Fs*(plln/(pllm*pllq));
//Fvco:VCO 频率
//Fsys:系统时钟频率,也是 PLL1 的 p 分频输出时钟频率
//Fq:PLL1 的 q 分频输出时钟频率
//Fs:PLL 输入时钟频率,可以是 HSI,CSI,HSE 等.
//plln:PLL1 倍频系数(PLL 倍频),取值范围:4~512.
//pllm:PLL1 预分频系数(进 PLL 之前的分频),取值范围:2~63.
//pllp:PLL1 的 p 分频系数(PLL 之后的分频),分频后作为系统时钟,取值范围:2~128
//pllq:PLL1 的 q 分频系数(PLL 之后的分频),取值范围:1~128.
//CPU 频率(rcc_c_ck)=sys_d1cpre_ck=400Mhz
//rcc_aclk=rcc_hclk3=200Mhz
//AHB1/2/3/4(rcc_hclk1/2/3/4)=200Mhz
//APB1/2/3/4(rcc_pclk1/2/3/4)=100Mhz
//FMC 时钟频率=pll2_r_ck=((25/25)*512/2)=256Mhz
//外部晶振为 25M 的时候,推荐值:plln=160,pllm=5,pllp=2,pllq=4.
//得到:Fvco=25*(160/5)=800Mhz
// Fsys=800/2=400Mhz
// Fq=800/2=400Mhz
//返回值:0,成功;1,失败。
void Stm32_Clock_Init(u32 plln,u32 pllm,u32 pllp,u32 pllq)
{
HAL_StatusTypeDef ret=HAL_OK;
RCC_ClkInitTypeDef RCC_ClkInitStruct;
RCC_OscInitTypeDef RCC_OscInitStruct;
MODIFY_REG(PWR->CR3,PWR_CR3_SCUEN, 0);
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE
_SCALE1);
while ((PWR->D3CR & (PWR_D3CR_VOSRDY)) != PWR_D3CR_VOSRDY) {}
//配置 PLL1
RCC_OscInitStruct.OscillatorType=RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState=RCC_HSE_ON;
RCC_OscInitStruct.HSIState=RCC_HSI_OFF;
RCC_OscInitStruct.CSIState=RCC_CSI_OFF;
RCC_OscInitStruct.PLL.PLLState=RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource=RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLN=plln;
RCC_OscInitStruct.PLL.PLLM=pllm;
RCC_OscInitStruct.PLL.PLLP=pllp;
RCC_OscInitStruct.PLL.PLLQ=pllq;
RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1VCIRANGE_2;
ret=HAL_RCC_OscConfig(&RCC_OscInitStruct);
if(ret!=HAL_OK) while(1);
//配置其他的 PLL
RCC_ClkInitStruct.ClockType=(RCC_CLOCKTYPE_SYSCLK|\
RCC_CLOCKTYPE_HCLK |\
RCC_CLOCKTYPE_D1PCLK1 |\
RCC_CLOCKTYPE_PCLK1 |\
RCC_CLOCKTYPE_PCLK2 |\
RCC_CLOCKTYPE_D3PCLK1);
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2;
RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2;
RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2;
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4);
if(ret!=HAL_OK) while(1);
//使能其他一些时钟
__HAL_RCC_CSI_ENABLE() ; //使能 CSI 时钟 
__HAL_RCC_SYSCFG_CLK_ENABLE() ; //使能 SYSCFG 时钟
HAL_EnableCompensationCell(); //使能 IO 补偿单元
}
           

从函数注释可知,函数 Stm32_Clock_Init 的作用是进行时钟系统配置,除了配置 PLL 相关参数确定 SYSCLK 值之外,还配置了 AHB,APB1、APB2、APB3 和 APB4 的分频系数,也就是确定了 HCLK,PCLK1、PCLK2 、PCLK3 和 PCLK4 的时钟值。我们首先来看看使用 HAL 库配置 STM32H7 时钟系统的一般步骤:

  1. 设置调压器输出电压级别:调用函数__HAL_PWR_VOLTAGESCALING_CONFIG()。
  2. 配置时钟源相关参数:调用函数 HAL_RCC_OscConfig()。
  3. 配置系统时钟源以及 SYSCLK、AHB,APB1、APB2、APB3 和 APB4 的分频系数:调用函数HAL_RCC_ClockConfig()。
  4. 使能其他一些时钟和 IO 补偿单元。

    步骤 1 后面在讲解,先来看一下步骤 2 和步骤 3,

    对于步骤 2,使用 HAL 来配置时钟源相关参数,我们调用的函数为 HAL_RCC_OscConfig(),该函数在 HAL 库关键头文件

    stm32h7xx_hal_rcc.h 中声明,在文件 stm32h7xx_hal_rcc.c 中定义。首先我们来看看该函数声明:__weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);

    该函数只有一个入口参数,就是结构体 RCC_OscInitTypeDef 类型指针。接下来我们看看结构体 RCC_OscInitTypeDef 的定义:

typedef struct
{
uint32_t OscillatorType; //需要选择配置的振荡器类型
uint32_t HSEState; //HSE 状态
uint32_t LSEState; //LSE 状态
uint32_t HSIState; //HIS 状态
uint32_t HSICalibrationValue; //HIS 校准值
uint32_t LSIState; //LSI 状态
uint32_t HSI48State //HSI48 的状态
uint32_t CSIState;  //CSI 状态
uint32_t CSICalibrationValue; //CSI 校准值
RCC_PLLInitTypeDef PLL; //PLL 配置
}RCC_OscInitTypeDef;
           

对于这个结构体,前面几个参数主要是用来选择配置的振荡器类型。比如我们要开启 HSE,那么我们会设置 OscillatorType 的值为 RCC_OSCILLATORTYPE_HSE,然后设置 HSEState 的值为 RCC_HSE_ON 开启 HSE。对于其他时钟源 HSI,LSI 和 LSE,配置方法类似。这个结构体还有一个很重要的成员变量是 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
uint32_t PLLR; //PLL 分频系数 R
uint32_t PLLRGE;  //PLL1 时钟输入范围
uint32_t PLLVCOSEL;  //PLL1 时钟输出范围
uint32_t PLLFRACN; //PLL1 VCO 乘数因子的小数部分
}RCC_PLLInitTypeDef;
           

从 RCC_PLLInitTypeDef;结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及

相关分频倍频参数。

这个结构体的定义我们就不做过多讲解,接下来我们看看我们的时钟初始化函数

Stm32_Clock_Init 中的配置内容:

RCC_OscInitStruct.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStruct.HSEState=RCC_HSE_ON; //打开 HSE
RCC_OscInitStruct.HSIState=RCC_HSI_OFF; //关闭 HSI
RCC_OscInitStruct.CSIState=RCC_CSI_OFF;  //关闭 CSI
RCC_OscInitStruct.PLL.PLLState=RCC_PLL_ON; //打开 PLL
RCC_OscInitStruct.PLL.PLLSource=RCC_PLLSOURCE_HSE;  //PLL 时钟源为 HSE
RCC_OscInitStruct.PLL.PLLN=plln;
RCC_OscInitStruct.PLL.PLLM=pllm;
RCC_OscInitStruct.PLL.PLLP=pllp;
RCC_OscInitStruct.PLL.PLLQ=pllq;
RCC_OscInitStruct.PLL.PLLVCOSEL=RCC_PLL1VCOWIDE;
RCC_OscInitStruct.PLL.PLLRGE=RCC_PLL1VCIRANGE_2;
ret=HAL_RCC_OscConfig(&RCC_OscInitStruct);
           

通过该段函数,我们开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE,然后把Stm32_Clock_Init 的 4 个入口参数直接设置作为 PLL 的参数 N,M,P 和 Q 的值,这样就达到了设

置 PLL 时钟源相关参数的目的。设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率,接下来我们就需要设置系统时钟,以及 SYSCLK、AHB,APB1、APB2、APB3 和 APB4 相关参数,也就是我们前面提到的步骤 3。接下来我们来看看步骤 3 中提到的HAL_RCC_ClockConfig()函数,声明如下:

该函数有两个入口参数,第一个入口参数 RCC_ClkInitStruct 是结构体 RCC_ClkInitTypeDef指针类型,用来设置 SYSCLK 时钟源以及 SYSCLK、AHB,APB1、APB2、APB3 和 APB4 的分频系数。第二个入口参数 FLatency 用来设置 FLASH 延迟,这个参数我们放在后面跟步骤 1 一起讲解。

RCC_ClkInitTypeDef 结构体类型定义非常简单,这里我们就不列出来,我们来看看Stm32_Clock_Init 函数中的配置内容:

//选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1、PCLK2、PCLK3 和 PCLK4
RCC_ClkInitStruct.ClockType=(RCC_CLOCKTYPE_SYSCLK|\
RCC_CLOCKTYPE_HCLK |\
RCC_CLOCKTYPE_D1PCLK1 |\
RCC_CLOCKTYPE_PCLK1 |\
RCC_CLOCKTYPE_PCLK2 |\
RCC_CLOCKTYPE_D3PCLK1);
RCC_ClkInitStruct.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK; //系统时钟源 PLL
RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1; //SYSCLK 分频系数 1
RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV2;  //AHB 分频系数 2
RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2; //APB1 分频系数 2
RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2; //APB2 分频系数 2
RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2; //APB3 分频系数 2
RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2; //APB4 分频系数 2
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4);
           

第一个参数 ClockType 配置说明我们要配置的是 SYSCLK,HCLK、D1PCLK1(PCLK3)、

PCLK1、PCLK2 和 D3PCLK1(PCLK4)六个时钟。

第二个参数 SYSCLKSource 配置选择系统时钟源为 PLL。

第三个参数 SYSCLKDivider 配置 SYSCLK 分频系数为 1。

第四个参数 AHBCLKDivider 配置 AHB 分频系数为 2。

第五个参数 APB1CLKDivider 配置 APB1 分频系数为 2。

第六个参数 APB2CLKDivider 配置 APB2 分频系数为 2。

第七个参数 APB3CLKDivider 配置 APB3 分频系数为 2。

第八个参数 APB4CLKDivider 配置 APB4 分频系数为 2。

根据我们在主函数中调用 Stm32_Clock_Init(160,5,2,4)时候设置的入口参数值,我们可以计

算出,PLL 时钟为 PLLCLK=HSEN/(MP)=25MHz160/(52)=400MHz,同时我们选择系统

时钟源为 PLL,所以系统时钟 SYSCLK=400MHz。AHB 分频系数为 2,故其频率为

HCLK=SYSCLK/2=200MHz。APB1 分频系数为 2,故其频率为 PCLK1=HCLK/2=100MHz。APB2

分频系数为 2,故其频率为 PCLK2=HCLK/2=200/2=100MHz,APB3 分频系数为 2,故其频率

PCLK3=HCLK/2=200/2=100MHz,APB4 的分频系数为 4,故其频率 PLCK4=HCLK/2=200/2=

100MHz 最后我们总结一下通过调用函数 Stm32_Clock_Init(160,5,2,4)之后的关键时钟频率值:

SYSCLK(系统时钟) =400MHz
PLL 主时钟 =400MHz
AHB 总线时钟(HCLK=SYSCLK/2)  =200MHz
APB1 总线时钟(PCLK1=HCLK/2) =100MHz
APB2 总线时钟(PCLK2=HCLK/2) =100MHz
APB3 总线时钟 (PCLK3=HCLK/2) =100MHz
APB4 总线时钟 (PCLK4=HCLK/2) =100MHz
           

最后我们来看看步骤 1 和步骤 3 中函数HAL_RCC_ClockConfig 第二个入口参数 FLatency的含义。这里我们不想讲解得太复杂,大家只需要知道调压器输出电压级别 VOS 和 FLASH 的延迟 Latency 三个参数,在我们芯片电源电压和 HCLK 固定之后,他们三个参数也是固定的。

首先我们来看看调压器输出电压级别 VOS,它是由 PWR 控制寄存器 D3CR 的位 15:14 来确定的:

位 15:14 VOS[1:0]
00:保留(默认模式 3 选中)
01:级别 3:
10:级别 2:
11:级别 1:
           

级别数值越小工作频率越高,所以如果我们要配置 H7 的主频为 400MHz,那么我们必须配置调压器输出电压级别 VOS 为级别 1。所以函数 Stm32_Clock_Init 中步骤 1 源码如下:

//步骤 1,设置调压器输出电压级别 1
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
           

配置好调压器输出电压级别 VOS 之后,如果需要 H7 主频要达到 400MHz,还需要配置FLASH 延迟 Latency。对于 STM32H7 系列,FLASH 延迟配置参数值是通过下表来确定的:

STM32H7 时钟系统配置

上表中的时钟频率为 ACLK,ACLK=HCLK=200MHz,从上表可以看出,当调压器设置为VOS1 级别的时候(我们设置的就是 VOS1 级别),如果需要 ACLK 为 210MHz,那么等待周期要为 2WS,也就是 3 个 CPU 周期。下面我们看看我们在 Stm32_Clock_Init 中调用函数HAL_RCC_ClockConfig 的时候,第二个入口参数设置值:

从上可以看出,我们设置值为 FLASH_LATENCY_4,也就是 4WS,5 个 CPU 周期,为什么不是 2 个 WS 呢?因为 ST 官方例程使用的就是 4 个 WS,其实大于 2 个 WS 都可以(最大不能超过 7 个 WS),延时越久越稳定,但是肯定影响性能,后续我们会用大型例程测试 2 个 WS是否稳定,如果稳定的话就会改为 2 个。时钟系统配置相关知识就给大家讲解到这里。