STM32系统时钟初始化源码解析(SPL库,STM32F103ZE)


一、背景

开发环境

  • STM32F103ZET6

原理图

image-20241123160212385

HSE-高速外部晶振High Speed External oscillator

通过23、24号引脚接入8M的高速外部晶振。

HSI-低速外部晶振Low Speed External oscillator

通过GPIO引脚PC14和PC15的复用接入32.768kHz低速外部晶振。

以Cube的图形化配置为例说明

image-20241123160112667

MCU内置了两个RC震荡电路:

  • HSI RC,High Speed Internal RC,8M高速内部RC震荡电路
  • LSI RC,Low Speed Internal RC,40kHz低速内部RC震荡电路

为什么MCU内置了,还需要外接晶振

  • RC震荡电路容易受到其他电路干扰,导致信号不稳定,且精度远比晶振低。

  • 由于MCU的微小封装,无法内嵌较大的晶振

  • 将晶振的灵活选择权交给开发者

Cube配置解析

外接晶振通过引脚接入MCU,但由于MCU的主频较高,一般将HSE通过PLL(锁相环,用来倍频)提高频率再作为系统时钟SYSCLK的来源。

图中 Mux是多路选择器 Multiplex的缩写,可以看到我们的8M HSE经过了1分频(即不分频),连接到了PLL Source Mux,通过这个多路选择器,我们使用HSE而不是HSI作为PLL的时钟来源(HSE在精度和稳定性上要比HSI好很多)。

接着经过了PLLMul,即锁相环倍频(PLL Multiply),将HSE的频率放大了9倍得到PLLCLK(8M * 9 = 72M),并连接到了System Clock Mux(即系统时钟多路选择器),这里我们当然选择稳定、高精度、高频的PLLCLK作为系统时钟。

二、时钟初始化过程源码分析(SPL库)

上电/复位后会发生什么?

MCU启动的汇编文件 startup.s会被执行:

startup_stm32f10x_hd.s

Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0

执行SystemInit函数

SystemInit 函数的入口地址加载到寄存器R0,通过 BLX跳转到其对应的代码段执行该函数,其中就包括了系统时钟的初始化过程。

执行main函数

main 函数的入口地址加载到寄存器R0,通过 BX跳转到其对应的代码段执行该函数,执行用户代码。

该初始化顺序遵循ARM的CMSIS(Cortex Microcontroller Software Interface Standard)标准

SystemInit

STM32F107参考手册

启用HSI,先让MCU有心跳

system_stm32f10x.c中找到 SystemInit函数,条件编译显示灰色的部分可以忽略

/* Set HSION bit */
RCC->CR |= (uint32_t)0x00000001;

先开启HSI高速内部时钟(时钟相当于MCU心跳,先让MCU有心跳能够工作,后面再将时钟源通过多路选择器切换为我们配置的HSE),将寄存器的第0位设置为1:

image-20241123165840693

image-20241123165911410

复位时钟相关寄存器比特位

/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
RCC->CFGR &= (uint32_t)0xF8FF0000;

该操作会将CFGR(时钟配置)寄存器如下比特位清零:

image-20241123170556835

image-20241123170704055

选择HSI(上一步已经开启了)作为系统时钟,其他的(HPRE, PPRE1, PPRE2, ADCPRE and MCO)分析过程类似。

/* Reset HSEON, CSSON and PLLON bits */
RCC->CR &= (uint32_t)0xFEF6FFFF;

/* Reset HSEBYP bit */
RCC->CR &= (uint32_t)0xFFFBFFFF;

/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
RCC->CFGR &= (uint32_t)0xFF80FFFF;

同样的,也是将一些位清零,将系统时钟初始化为仅由HSI作为时钟源的初始状态

SetSysClock设置系统时钟

/* Enable HSE */    
RCC->CR |= ((uint32_t)RCC_CR_HSEON);
 
/* Wait till HSE is ready and if Time out is reached exit */
do
{
  HSEStatus = RCC->CR & RCC_CR_HSERDY;
  StartUpCounter++;  
} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));

这里开启了HSE外部高速时钟,并等待HSE的状态转变为READY(由硬件置位)。

系统时钟树

/* HCLK = SYSCLK */
RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
  
/* PCLK2 = HCLK */
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;

/* PCLK1 = HCLK */
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

将系统时钟作为AHB和APB2的时钟源(对应HCLK和PCLK2),将系统时钟2分频,作为APB1的时钟源(对应PCLK2)。

AHB挂载了Cortex核、DMA等主动单元

APB2挂载了GPIO、USRAT1、ADC等需要高速率时钟的单元

APB1则挂载了TIMER、I2C等不需要那么高频的单元

image-20241123172121666

PLL倍频

/*  PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
  • 将PLL时钟来源RCC_CFGR_PLLSRC、HSE进入PLL后的分频RCC_CFGR_PLLXTPRE、PLL倍频系数RCC_CFGR_PLLMULL清零;
  • 选择PLL时钟来源为HSE(RCC_CFGR_PLLSRC_HSE),倍频系数为9(RCC_CFGR_PLLMULL9)
/* Enable PLL */
    RCC->CR |= RCC_CR_PLLON;

    /* Wait till PLL is ready */
    while((RCC->CR & RCC_CR_PLLRDY) == 0)
    {
    }

启动PLL锁相环,并等待它稳定。

/* Select PLL as system clock source */
   RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
   RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;    

   /* Wait till PLL is used as system clock source */
   while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
   {
   }

通过系统时钟的多路选择器将PLL选择为系统时钟源,并等待此切换完成。


文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录