一、背景
开发环境
- STM32F103ZET6
原理图
HSE-高速外部晶振High Speed External oscillator
通过23、24号引脚接入8M的高速外部晶振。
HSI-低速外部晶振Low Speed External oscillator
通过GPIO引脚PC14和PC15的复用接入32.768kHz低速外部晶振。
以Cube的图形化配置为例说明
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
启用HSI,先让MCU有心跳
在system_stm32f10x.c
中找到 SystemInit
函数,条件编译显示灰色的部分可以忽略
/* Set HSION bit */
RCC->CR |= (uint32_t)0x00000001;
先开启HSI高速内部时钟(时钟相当于MCU心跳,先让MCU有心跳能够工作,后面再将时钟源通过多路选择器切换为我们配置的HSE),将寄存器的第0位设置为1:
复位时钟相关寄存器比特位
/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
RCC->CFGR &= (uint32_t)0xF8FF0000;
该操作会将CFGR(时钟配置)寄存器如下比特位清零:
选择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等不需要那么高频的单元
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选择为系统时钟源,并等待此切换完成。