一、概述

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=8MHzN/(MP)=8MHz336/(82)=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 中,源码如下:

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):

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 */
    // 时钟开启失败
  }
}

单独说一下这段:

RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) -1) << 16) |
               (RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24);

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

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

#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 总线上的外设的时钟使能函数为例:

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,就用如下语句来使能其时钟:

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

其它总线类似:

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)

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

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);
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐