STM32的时钟系统

STM32的时钟

STM32时钟结构

STM32 芯片为了实现低功耗,设计了一个功能完善但却非常复杂的时钟系统。普通的单片机一般只要配置好 GPIO 的寄存器就可以使用了,但 STM32 还有一个步骤,就是开启并调整外设时钟。

下图展示了STM32完整的时钟结构:

它可以大致简化为如下结构:

该图说明了 STM32 的时钟走向,从图的左边开始,从时钟源一步步分配到外设时钟。

  • 时钟源

以上结构图最左侧的方框代表 4 个时钟源:

  1. 高速外部时钟(HSE):以外部晶振作为时钟源,晶振频率可取范围为 4~16 MHz。一般采用 8MHz 的晶振
  2. 高速内部时钟(HSI):由内部 RC 振荡器产生,频率为 8MHz
  3. 低速外部时钟(LSE):以外部晶振作为时钟源,主要提供给实时时钟模块,一般采用 32.768kHz 的晶振
  4. 低速内部时钟(LSI):由内部 RC 振荡器产生,也主要提供给实时时钟模块,频率大约为 40kHz

从时钟频率来说,以上时钟源分为高速时钟和低速时钟,高速时钟是提供给芯片主体的主时钟,而低速时钟只是提供给芯片中的 RTC(实时时钟)及独立看门狗使用。

从芯片角度来说,以上时钟源分为内部时钟与外部时钟源,内部时钟是由芯片内部 RC 振荡器产生的,起振较快,所以时钟在芯片刚上电的时候,默认使用内部高速时钟。而外部时钟信号是由外部的晶振输入的,在精度和稳定性上都有很大优势,所以上电之后再通过软件配置,转而采用外部时钟信号。

  • 倍频

STM32F1 的高速时钟最多也只支持 16MHz 的晶振,为了获得更高的主频,PLL(Phase-locked loops, 相位锁定环)在 STM32 的时钟结构中起着非常关键的作用。PLL 的原理比较复杂,它主要通过锁定输入信号的相位来调整输出信号的频率,实现频率的倍频、分频或相位调整。通过 PLL,可以将基础的时钟源(如 HSI 或 HSE)的频率倍数放大,达到 MCU 运行所需的更高或特定的频率。

之所以不直接外界 72MHz 的晶振,主要考虑的是电磁兼容性,太高的振荡频率容易受到外界环境的干扰,会给制作电路板带来一定的难度。除此之外,PLL 也便于控制系统的频率,开发者可以根据具体的应用需求选择合适的频率倍数,既可以主动减少倍数来减慢系统的处理速度,从而减少功耗;也可以通过增加倍数来进一步提升处理速度,这就是所谓超频的原理。通过 PLL 改变主频远比更换晶振来的容易。

PLL 倍频系数可以 是 2 到 16 的整数,同样可以通过程序配置。STM32F1 系列稳定运行的最大频率推荐值是 72MHz ,因此在使用 8MHz 晶振的情况下,PLL 可以设置为 9 倍频。

PLL 的输出时钟频率一般记为 PLLCLK

  • 分频

不论采用的是内部时钟还是外部时钟,以及是否经过倍频,输入的时钟最终会得到 STM32 的系统时钟,供内核和各种外设使用。系统时钟一般记为 SYSCLK ,在 system_stm32f10x.c 中定义了一个全局变量 SystemCoreClock ,可以在程序中直接得到系统时钟的频率。

不过,系统时钟一般不直接作为外设的时钟,这是因为 STM32 既有高速外设也有低速外设,不同外设对系统功率的要求都不一样。例如,GPIO 可能需要较高的工作频率,这样对外输出信号的速度才能比较高;而一些信号采集外设的频率不能太高,否则采集结果的准确度不能得到保证。

因此,STM32 将不同速度的外设挂载到不同总线上,通过总线前的分频器统一调整输入的时钟频率。关键的分频点有 3 个:

  • 系统时钟 SYSCLK 经过 AHB 预分频器分频之后得到 AHB 总线时钟 HCLK 。内核和大部分外设的时钟都基于 HCLK 得到
  • AHB 总线时钟 HCLK 再次经过 APB1 预分频器分频得到 APB1 总线时钟 PCLK1PCLK1 属于低速总线时钟,最高为 36MHz ,挂载的都是低速外设
  • HCLK 还会经过 APB2 预分频器分频得到 APB2 总线时钟 PCLK2PCLK2 属于高速总线时钟,挂载的都是高速外设,包括所有 GPIO 外设和 USART1

有些外设在 APB1/2 总线时钟前还会继续分频,这部分时钟的问题等到相关的外设章节再做介绍。

  • 一系列开关

每个外设都配备了时钟开关,当不使用外设时,可以主动将外设时钟关闭,从而降低功耗。所以,在使用外设(操作外设对应的寄存器)前,注意开启外设的时钟,防止某些操作没有效果。

  • 其它部分

在时钟的框图中还有一个结构 CSS ,全称时钟安全系统(Clock Security System),主要用于外部晶振出现问题时触发中断,用户可以编写用于处理错误的中断服务函数,确保单片机可以正常工作。

时钟树的应用

接下来通过几个示例介绍时钟树与应用的关系。以下只展示了程序的核心代码,完整的代码可以在 GitHub 仓库中获取。

时钟的初始化过程

在 STM32F1 系列的启动文件中,初始化了一些关键的寄存器和中断向量后,便调用了一个函数 SystemInit() 。在标准外设库的 system_stm32f10x.c 文件中,已经写好了这个函数的模板,这个函数的主要用途就是设置系统时钟,包括选择时钟源、设置 PLL 倍数、配置各个总线时钟分频等。接下来通过分析这个函数的执行过程,来说明 STM32F1 的时钟初始化过程。如果需要其它的时钟配置,可以在这个函数的基础上修改。

SystemInit() 函数包含了许多寄存器操作,但它们的目的只是将 RCC 外设复位为默认状态,这些寄存器操作与标准外设库的函数 RCC_DeInit() 作用是一样的。在 SystemInit() 函数内,调用了一个函数 SetSysClock() ,这个函数又根据预定义宏的不同调用了设置不同系统时钟频率的函数。如果系统时钟采用的是 72MHz 的时钟,那么调用的是 SetSysClockTo72()

SetSysClockTo72() 内,系统时钟的初始化和配置也是直接通过操作寄存器的方式完成的。为了方便说明执行的操作,并编写自己的代码修改时钟配置,以下采用标准外设库封装后的形式。

SetSysClockTo72() 执行了以下配置:

  1. 选择时钟源

刚上电时,系统所使用的时钟是 HSI ,为了换用 HSE ,需要通过以下函数启用:

RCC_HSEConfig(RCC_HSE_ON);

然后需要等待 HSE 准备就绪。底层代码中已经处理了循环和超时检测的问题,只需要判断最后的结果并自定义错误处理方式即可。

if (RCC_WaitForHSEStartUp() == SUCCESS) {
    // ...
}
else {
    while (1); // ...
}

如果程序中已经启用了 HSE ,那么换回 HSI 可以通过以下程序实现:

RCC_HSEConfig(RCC_HSE_OFF);
RCC_HSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET);
  1. 配置 PLL

然后,程序来到了配置 PLL 的相关部分,主要包括将 HSE 作为 PLL 输入,以及设置 PLL 的倍率。如果需要调整系统总体的运行频率,可以通过修改 PLL 倍率的方法实现:

RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
RCC_PLLCmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
  1. 调整系统时钟源

系统时钟 SYSCLK 可以是 HSI、HSE 或 PLL 中的一个,一般选择 PLL 作为系统时钟:

RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
while (RCC_GetSYSCLKSource() != 0x08);
  1. 设置总线分频

系统时钟主要流入 3 个区域:AHB 、APB1 和 APB2 总线,可以通过设置不同的分频值来调整它们的运行频率:

RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB 时钟 = 系统时钟
RCC_PCLK1Config(RCC_HCLK_Div2);  // APB1 时钟 = AHB 时钟 / 2
RCC_PCLK2Config(RCC_HCLK_Div1);  // APB2 时钟 = AHB 时钟

在系统时钟是 72MHz 的情况下,PCLK1 为 36MHz ,PCLK2 为 72MHz 。

实际上 SystemInit() 会更复杂一些,它除了时钟的配置外,还包含了 Flash 驱动和中断的一些设置。

使用系统定时器

系统定时器的概念

系统定时器(SysTick timer, STK)是属于 Cortex-M3 内核中的一个结构,内嵌在 NVIC 中,因此所有基于 Cortex-M3 内核的单片机都具有该结构,使得所有采用该内核的单片机都有一个统一的定时器。

系统定时器一般用于操作系统中,提供了系统级的定时和延时功能,维持操作系统正常运转。这样操作系统也便于在各种采用 Cortex-M3 内核的单片机之间移植。

和 SysTick 相关的寄存器很少,只有 4 个,外加 core_cm3.c/h 文件并没有对 SysTick 有过多封装,所以有必要了解一下 SysTick 的寄存器操作。

SysTick 主要结构包括一个 24bit 有效的当前计数值寄存器 VAL 中,因此 SysTick 一次最多可以计数 224 个时钟脉冲。每接收到一个时钟脉冲,VAL 的值就向下减 1 ,直至减为 0 时触发系统定时器中断,可以在中断服务函数中处理定时任务。同时,硬件自动将重装载寄存器 LOAD 中保存的数据加载到 VAL 中,重新开始下一轮向下计数。

SysTick 的时钟来源是 AHB 总线时钟 HCLKHCLK 的 8 分频。一般设置 HCLKSYSCLK 均为 72MHz ,那么在使用 HCLK 时,计完这 224 个数用时 0.233 秒。在 8 分频的情况下,最大计时时间可以翻 8 倍。

SysTick 在复位后,默认会使用 8 分频后的时钟。如果不想分频,可以通过以下函数改变系统定时器的时钟:

  • void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);

SysTick 的控制和状态寄存器 CTRL 用于控制 SysTick 定时器的启用、中断使能、计数器的清零等。

接下来通过程序介绍系统定时器的使用。

使用系统定时器精确延时

通过系统定时器的原理,可以编写一个函数实现精确时间的延时任务,具体思路为:为 SysTick 设置一个定时间隔,当 SysTick 的计数器往下递减到 0 的时候,CTRL 寄存器的位 COUNTGLAG 会置 1 ,通过不断空等直到该位置 1 ,即可使用定时器实现精确的延时任务。

根据上文对 SysTick 的介绍,定时器计数一次的时间为 1 / SystemCoreClock ,因此到达中断所需的时间为 LOAD / SystemCoreClock 秒。不管系统时钟频率是多少,只需要设置 LOAD 为 SystemCoreClock / scale ,就可以实现 1 / scale 秒的计时。例如,如果要实现 1ms 的延时,那么可以通过如下形式初始化 SysTick :

SysTick->VAL = 0x00;
SysTick->LOAD = SystemCoreClock / 1000;

这里要注意装载值的范围,LOAD 为 24 位寄存器,最大装载值约为 16.7M ,一般情况下是低于 SystemCoreClock 的。如果要实现较长时间的延时,应该通过多次循环来实现。

要让定时器开始工作,需要通过置 CTRLENABLE 位实现,清零该位可以使定时器停止工作。判断定时器定时完毕的依据是检查 COUNTFLAG 位是否置 1 ,如果为 1 说明定时结束;且读取该位的值可自动清 0 。因此,延时的核心代码为:

SET_BIT(SysTick->CTRL, SysTick_CTRL_ENABLE_Msk);
for (uint32_t i = 0; i < ms; i++)
    while (READ_BIT(SysTick->CTRL, SysTick_CTRL_COUNTFLAG_Msk) != Bit_SET);
CLEAR_BIT(SysTick->CTRL, SysTick_CTRL_ENABLE_Msk);

使用系统定时器执行定时任务

SysTick 定时器能产生中断,在定时结束后,会发生 SysTick_IRQn 中断,进入 SysTick_Handler() 中断函数处理。因此可以通过使定时器定时产生中断,实现每隔固定时间执行一段程序的目的。使用定时器执行定时任务是一种非阻塞式的方式,在定时过程中可以执行别的程序。如果采用软件延时,那么在延时时间内程序只能干等。

可以通过以下标准外设库的函数向系统定时器载入重装载寄存器的计时值,并启用中断功能:(这个函数由 core_cm3.h 提供)

  • static inline uint32_t SysTick_Config(uint32_t ticks);

例如,以下配置可以使系统定时器每隔 0.1s 产生一次中断:

if (SysTick_Config(SystemCoreClock / 10)) {
    /* error handling */
    while (1);
}
关于SysTick的中断优先级

SysTick 属于内核外设,跟普通外设的中断优先级有些区别。在 Cortex-M3 中,内核外设的中断优先级由内核 SCB 外设的寄存器 SHPRx 配置,SHPRx 的每个字节控制着一个内核外设的中断优先级的配置。不过在 STM32F1 中,只用到了高四位,所以内核外设的中断优先级可编程为 0~15 共 16 个优先级,数值越小,优先级越高。

外部中断的优先级由 NVIC 中的 IPx 寄存器控制,并且在 STM32F1 中,这些寄存器也是以字节起效的,并且也只使用高 4 位表达优先级。这样内核外设的优先级就和普通外设的优先级一样,可以在 NVIC 内经由优先级分组,并比较抢占优先级和子优先级。

不管是内核中断的优先级还是外设中断的优先级,当它们的软件优先级一样时,就比较他们在中断向量表中的硬件编号,编号越小则优先级越高。在启动文件的默认设置下,内核中断在中断向量表的编号均小于外设中断。

然后就可以编写中断服务函数。SysTick 没有中断标志位的概念,只要简单地执行定时任务即可:

void SysTick_Handler(void) {
    LED_Toggle(LED_B);
}

时钟输出

STM32 允许使用引脚向外界输出时钟信号,这就是它的微控制器时钟输出(Microcontroller clock output, MCO)功能。

在 STM32F1 系列中,PA8 可以复用为 MCO 引脚,对外提供时钟输出。为了使用 MCO 功能,需要先初始化 PA8 为复用推挽输出模式:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure = {
    .GPIO_Mode = GPIO_Mode_AF_PP,
    .GPIO_Speed = GPIO_Speed_50MHz,
    .GPIO_Pin = GPIO_Pin_8
};
GPIO_Init(GPIOA, &GPIO_InitStructure);

MCO 时钟可以被设置为以下四个时钟信号之一:

  1. SYSCLK
  2. HSI
  3. HSE
  4. PLL 时钟

程序中可以通过以下函数设置 MCO 的输出时钟:

  • void RCC_MCOConfig(uint8_t RCC_MCO);

此时,PA8 就开始输出时钟信号。可以通过示波器监测 PA8 ,观察时钟信号并判断系统时钟是否设置正确。

京ICP备2021034974号
contact me by hello@frozencandles.fun