STM32 时钟树(基于 STM32F407)

[toc]


一、概述

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

$$
PLL=8MHz* N/ (MP)=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 中,源码如下:

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
38
39
40
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):

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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 */
// 时钟开启失败
}
}

单独说一下这段:

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

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

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

1
2
3
4
5
#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 的结果为:$0740 5408$,设置结果为:

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

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

2、STM32F4 时钟使能和配置

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,就用如下语句来使能其时钟:

1
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

其它总线类似:

1
2
3
4
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)

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

1
2
3
4
5
6
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);

STM32 时钟树(基于 STM32F407)
http://example.com/2024/09/25/STM32-时钟树/
作者
Yu xin
发布于
2024年9月25日
许可协议