STM32 时钟树(基于 STM32F407)

目录


一、概述

STM32 内部也是由多种多样的电路模块组合在一起实现的。当一个电路越复杂,在达到正确的输出结果前,它可能因为延时会有一些短暂的中间状态,而这些中间状态有时会导致输出结果会有一个短暂的错误,这叫做电路中的"毛刺现象",如果电路需要运行得足够快,那么这些错误状态会被其它电路作为输入采样,最终形成一系列的系统错误。为了解决这个问题,在单片机系统中,设计时以时序电路控制替代纯粹的组合电路,在每一级输出结果前对各个信号进行采样,从而使得电路中某些信号即使出现延时也可以保证各个信号的同步,可以避免电路中发生的"毛刺现象",达到精确控制输出的效果。

由于时序电路的重要性,因此在 MCU 设计时就设计了专门用于控制时序的电路,在芯片设计中称为时钟树设计。由此设计出来的时钟,可以精确控制我们的单片机系统。对于STM32F4 系列的芯片,正常工作的主频可以达到 168Mhz,但并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及 RTC 只需要几十 Khz 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。

STM32 本身非常复杂,外设非常的多,为了保持低功耗工作,STM32 的主控默认不开启这些外设功能。用户可以根据自己的需要决定STM32 芯片要使用的功能,这个功能开关在 STM32 主控中也就是各个外设的时钟。

二、时钟树框图

下图选自 STM32F4xx 参考手册:

下面来详细讨论上图中红框中的内容。

1、时钟源

对于 STM32F4,输入时钟源主要包括 HSIHSELSILSE。其中,从时钟频率来分可以分为高速时钟源和低速时钟源,其中 HSIHSE 是高速时钟,LSILSE 是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 HSELSE 是外部时钟源;其他是内部时钟源,芯片上电即可产生,不需要借助外部电路。

  • 高速外部振荡器 HSE (High Speed External Clock signal):外接石英/陶瓷谐振器,频率为 4MHz~26MHz。HSE 也可以直接做为系统时钟或者 PLL 输入。
  • 低速外部振荡器 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、锁相环

锁相环是自动控制系统中常用的一个反馈电路,在 STM32 主控中,锁相环的作用主要有两个部分:输入时钟净化和倍频。前者是利用锁相环电路的反馈机制实现,后者我们用于使芯片在更高且频率稳定的时钟下工作。

如框图所示,STM32F4 有两个 PLL:

  1. 主 PLL(PLL)由 HSE 或者 HSI 提供时钟信号,并具有两个不同的输出时钟。
    • 第一个输出 PLLP 用于生成高速的系统时钟(最高 168MHz)
    • 第二个输出 PLLQ 用于生成 USB OTG FS 的时钟(48MHz),随机数发生器的时钟和 SDIO 时钟。
  2. 专用 PLL(PLLI2S)用于生成精确时钟,从而在 I2S 接口实现高品质音频性能。

这里我们着重看看主PLL时钟第一个高速时钟输出PLLP的计算方法。如图:

主 PLL 时钟的时钟源要先经过一个分频系数为 M 的分频器,然后经过倍频系数为 N 的倍频器出来之后的时候还需要经过一个分频系数为 P(第一个输出 PLLP)或者 Q(第二个输出 PLLQ)的分频器分频之后,最后才生成最终的主 PLL 时钟。

例如我们的外部晶振选择 8MHz。同时我们设置相应的分频器 M=8,倍频器倍频系数 N=336,分频器分频系数 P=2,那么主 PLL 生成的第一个输出高速时钟 PLLP 为:

P L L = 8 M H z ∗ N / ( M ∗ P ) = 8 M H z ∗ 336 / ( 8 ∗ 2 ) = 168 M H z PLL=8MHz* N/ (M*P)=8MHz* 336 /(8*2) = 168MHz PLL=8MHz∗N/(M∗P)=8MHz∗336/(8∗2)=168MHz

如果我们选择 HSE 为 PLL 时钟源,同时 SYSCLK 时钟源为 PLL,那么 SYSCLK 时钟为 168MHz。

3、系统时钟

STM32 的系统时钟 SYSCLK 为整个芯片提供了时序信号。

讲解 PLL 作为系统时钟时,讲到了如何把主频通过 PLL 设置为 168MHz。从上面的时钟树图可知,AHB、APB1、APB2、内核时钟等时钟通过系统时钟分频得到。根据得到的这个系统时钟,下面我们结合外设来看一看各个外设时钟源。

下面结合 STM32CubeMX 的时钟树来看:

可以看到,系统时钟输入源可选时钟信号有外部高速时钟 HSE(8M)、内部高速时钟 HSI(16M)和经过倍频的 PLL CLK(168M)。这里选择 PLL CLK 作为系统时钟,此时系统时钟的频率为 168MHz。

然后是 AHB 预分频器,其中可选择的分频系数为1,2,4,8,16,32,64,128,256,512,我们选择不分频,所以 AHB 总线时钟达到最大的 168MHz。

然后看由 AHB 总线时钟得到的时钟:

  1. 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。
  2. APB2 总线时钟,由 HCLK 经过标号 APB2 预分频器得到,分频因子可以选择1,2,4,8,16,这里我们选择的是 2 分频,所以APB2 总线时钟频率为 84M。与 APB2 高速总线连接的外设有定时器 1/8/9/10/11、SPI1、USART1 和 USART6、3 个 ADC 和 SDIO 接口。
  3. AHB 总线时钟 直接作为 GPIO(A\B\C\D\E\F\G\H\I)、以太网、DCMI、FSMC、AHB 总线、Cortex 内核、存储器和 DMA 的 HCLK 时钟,并作为 Cortex 内核自由运行时钟 FCLK。

4、时钟信号输出 MCO

MCO 时钟输出的作用是为外部器件提供时钟。STM32 允许通过设置,通过 MCO 引脚输出一个稳定的时钟信号。

从右向左依次为:

  • 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)进行配置。

对于不同的 MCO 引脚,必须将相应的 GPIO 端口在复用功能模式下进行设置。MCO 输出时钟不得超过 100 MHz(最大 I/O 速度)

三、时钟配置

STM32F407 默认的情况下(比如:串口 IAP 时或者是未初始化时钟时),使用的是内部 8M 的 HSI 作为时钟源,所以不需要外部晶振也可以下载和运行代码的。

下面就来讲解如何让 STM32F407 芯片在 168MHz 的频率下工作,168MHz 是官方推荐使用的最高的稳定时钟频率。

1、修改主频

1.1 配置 HSE_VALUE

在文件 stm32f4xx.h 有如下内容:

宏定义 HSE_VALUE 匹配我们实际硬件的高速晶振频率(我的板子是 8MHz),代码中通过使用宏定义的方式来选择 HSE_VALUE 的值是 25M 或者 8M。

或者直接在 Keil 中添加宏定义也可以:

1.2 调用 SystemInit 函数

STM32 芯片启动过程 一文中我提到过 SystemInit 函数。该函数定义在文件 system_stm32f4xx.c 中,源码如下:

c 复制代码
void SystemInit(void)
{
  /* FPU settings ------------------------------------------------------------*/
  #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
    SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));  /* set CP10 and CP11 Full Access */
  #endif
  /* Reset the RCC clock configuration to the default reset state ------------*/
  /* Set HSION bit */
  RCC->CR |= (uint32_t)0x00000001;  // HSI 振荡器打开

  /* Reset CFGR register */
  RCC->CFGR = 0x00000000;  

  /* Reset HSEON, CSSON and PLLON bits */
  RCC->CR &= (uint32_t)0xFEF6FFFF;  // 关闭PLL,关闭时钟监测器,关闭 HSE振荡器

  /* Reset PLLCFGR register */
  RCC->PLLCFGR = 0x24003010; 

  /* Reset HSEBYP bit */
  RCC->CR &= (uint32_t)0xFFFBFFFF;  // 不旁路 HSE 振荡器

  /* Disable all interrupts */
  RCC->CIR = 0x00000000;  // 关闭所有中断

#if defined(DATA_IN_ExtSRAM) || defined(DATA_IN_ExtSDRAM)
  SystemInit_ExtMemCtl(); 
#endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
         
  /* Configure the System clock source, PLL Multiplier and Divider factors, 
     AHB/APBx prescalers and Flash settings ----------------------------------*/
  SetSysClock();

  /* Configure the Vector Table location add offset address ------------------*/
#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif
}

最主要的工作都是在函数 SetSysClock 里进行的,它进行了系统时钟源配置和各个分频器的设置。精简后如下(STM32F40_41xxx):

c 复制代码
static void SetSysClock(void)
{
/******************************************************************************/
/*            PLL (clocked by HSE) used as System clock source                */
/******************************************************************************/
  __IO uint32_t StartUpCounter = 0, HSEStatus = 0;
  
  /* Enable HSE */
  RCC->CR |= ((uint32_t)RCC_CR_HSEON);  // 打开 HSE 振荡器
 
  /* Wait till HSE is ready and if Time out is reached exit */
  do
  {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;  // 等待 HSE 振荡器就绪
    StartUpCounter++;				      // 超时时间:0x05000
  } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));

  // 判断是否超时 
  if ((RCC->CR & RCC_CR_HSERDY) != RESET) // HSE 振荡器已就绪
  {
    HSEStatus = (uint32_t)0x01;
  }
  else  // HSE 振荡器未就绪
  {
    HSEStatus = (uint32_t)0x00;
  }

  if (HSEStatus == (uint32_t)0x01)
  {
    /* Select regulator voltage output Scale 1 mode */
    // 使能电源时钟
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;  
    PWR->CR |= PWR_CR_VOS;

    /* HCLK = SYSCLK / 1*/
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1;  // 不进行分频

    /* PCLK2 = HCLK / 2*/
    RCC->CFGR |= RCC_CFGR_PPRE2_DIV2;  // 设置AHB时钟 2分频,即 APB2=AHB/2
    
    /* PCLK1 = HCLK / 4*/
    RCC->CFGR |= RCC_CFGR_PPRE1_DIV4;  // 设置AHB时钟 4分频 APB1 = AHB/4
	
	/* Configure the main PLL */
	// 设置 PLL 分频器
    RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) -1) << 16) |
                   (RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24);

	/* Enable the main PLL */
    RCC->CR |= RCC_CR_PLLON;  // 开启 PLL

    /* Wait till the main PLL is ready */
    // 等待主PLL时钟就绪
    while((RCC->CR & RCC_CR_PLLRDY) == 0)
    {
    }

	/* Configure Flash prefetch, Instruction cache, Data cache and wait state */
    FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN |FLASH_ACR_DCEN |FLASH_ACR_LATENCY_5WS;

	/* Select the main PLL as system clock source */
	// 选择 PLL 作为系统时钟
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= RCC_CFGR_SW_PLL;

    /* Wait till the main PLL is used as system clock source */
    // 等待PLL时钟设置完成
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL);
    {
    }
  }
  else
  { /* If HSE fails to start-up, the application will have wrong clock
         configuration. User can add here some code to deal with this error */
    // 时钟开启失败
  }
}

单独说一下这段:

c 复制代码
RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) -1) << 16) |
               (RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24);

对于 RCC_PLLCFGR 寄存器,官方文档说明如下:

而代码中的各个宏的值如下:

c 复制代码
#define PLL_M 8
#define PLL_N 336
#define PLL_P 2
#define RCC_PLLCFGR_PLLSRC_HSE ((uint32_t)0x00400000)
#define PLL_Q 7

算出来这里的 RCC->PLLCFGR 的结果为: 07405408 0740 5408 07405408,设置结果为:

  • PLLQ = 3
  • 选择 HSE 振荡器时钟作为 PLL 和 PLLI2S 时钟输入
  • PLLP = 2
  • PLLN = 336
  • PLLM = 8

时钟配置相关的内容就告一段落了。

2、STM32F4 时钟使能和配置

在配置好时钟系统之后,如果我们要使用某些外设,例如 GPIO,ADC 等,我们还要使能这些外设时钟。这里需要注意,如果在使用外设之前没有使能外设时钟,这个外设是不可能正常运行的。STM32 的外设时钟使能是在 RCC 相关寄存器中配置的。

下面以 AHB1 总线上的外设的时钟使能函数为例:

c 复制代码
void RCC_AHB1PeriphClockCmd(uint32_t RCC_AHB1Periph, FunctionalState NewState)
{
  /* Check the parameters */
  assert_param(IS_RCC_AHB1_CLOCK_PERIPH(RCC_AHB1Periph));

  assert_param(IS_FUNCTIONAL_STATE(NewState));
  if (NewState != DISABLE)
  {
    RCC->AHB1ENR |= RCC_AHB1Periph;
  }
  else
  {
    RCC->AHB1ENR &= ~RCC_AHB1Periph;
  }
}

如果我们想用 GPIOA,就用如下语句来使能其时钟:

c 复制代码
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

其它总线类似:

c 复制代码
void RCC_AHB2PeriphClockCmd(uint32_t RCC_AHB2Periph, FunctionalState NewState);
void RCC_AHB3PeriphClockCmd(uint32_t RCC_AHB3Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)

时钟源使能函数共有六个:

c 复制代码
void RCC_HSICmd(FunctionalState NewState);
void RCC_LSICmd(FunctionalState NewState);
void RCC_PLLCmd(FunctionalState NewState);
void RCC_PLLI2SCmd(FunctionalState NewState);
void RCC_PLLSAICmd(FunctionalState NewState);
void RCC_RTCCLKCmd(FunctionalState NewState);
相关推荐
智商偏低5 小时前
单片机之helloworld
单片机·嵌入式硬件
青牛科技-Allen7 小时前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
森焱森9 小时前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
白鱼不小白9 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D9 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术12 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt13 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘13 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang13 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c
几个几个n15 小时前
STM32-第二节-GPIO输入(按键,传感器)
单片机·嵌入式硬件