问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

STM32的时钟系统详解

创作时间:
作者:
@小白创作中心

STM32的时钟系统详解

引用
1
来源
1.
https://frozencandles.fun/embedded/stm32/05-clock-system/

STM32的时钟系统是其核心组成部分之一,理解时钟系统的原理和配置方法对于开发人员来说至关重要。本文将详细介绍STM32的时钟结构、时钟源、倍频、分频等关键概念,并通过具体代码示例展示时钟树的应用,包括时钟初始化、系统定时器的使用以及时钟输出等功能。

STM32的时钟

STM32时钟结构

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

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

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

  • 时钟源

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

  1. 高速外部时钟(HSE):以外部晶振作为时钟源,晶振频率可取范围为4~16MHz。一般采用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总线时钟PCLK1。PCLK1属于低速总线时钟,最高为36MHz,挂载的都是低速外设
  • HCLK还会经过APB2预分频器分频得到APB2总线时钟PCLK2。PCLK2属于高速总线时钟,挂载的都是高速外设,包括所有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);
while(RCC_GetFlagStatus(RCC_FLAG_HSIRDY)==RESET);
  1. 配置PLL

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

RCC_PLLConfig(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);
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一次最多可以计数2^24个时钟脉冲。每接收到一个时钟脉冲,VAL的值就向下减1,直至减为0时触发系统定时器中断,可以在中断服务函数中处理定时任务。同时,硬件自动将重装载寄存器LOAD中保存的数据加载到VAL中,重新开始下一轮向下计数。

SysTick的时钟来源是AHB总线时钟HCLK或HCLK的8分频。一般设置HCLK和SYSCLK均为72MHz,那么在使用HCLK时,计完这2^24个数用时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->LOAD = SystemCoreClock / 1000;

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

要让定时器开始工作,需要通过置CTRL的ENABLE位实现,清零该位可以使定时器停止工作。判断定时器定时完毕的依据是检查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 */
}

关于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,观察时钟信号并判断系统时钟是否设置正确。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号