STM32学习笔记(3)——时钟系统
一、STM32时钟系统
1. STM32时钟系统框图
STM32的时钟系统非常强大,但也非常复杂。下面为时钟树:
下面分别介绍图中的各个元素 (STM32中文参考手册6.2节时钟):
(1)最左边
最左边的OSC_IN
和OSC_OUT
是两个引脚,默认是外部晶振引脚。(我们的板子接了8MHz的晶振,数据手册表示可以接4——16MHz的晶振)
MCO(microcontroller clock output)是输出内部时钟,该功能能将STM32内部的时钟通过引脚PA8输出,要进行引脚复用与功能设置。我们可以通过示波器(可惜我们没有,要到实验室去接)监控 MCO 引脚的时钟输出来验证系统时钟配置是否正确。STM32可以选择一个时钟信号输出到MCO上,可以选择为PLL输出的2分频、HSI、HSE、或者系统时钟(SYSCLK)。
(2) STM32的5个时钟源(蓝色框图)
这些时钟源由晶振电路或RC振荡器组成,为系统提供一定的工作频率。
-
HSI(high speed internal)是高速内部时钟,RC振荡器,频率为8MHz,精度不高。
-
HSE(high speed external)是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。
-
LSI(low speed internal)是低速内部时钟,RC振荡器,频率为40kHz,提供低功耗时钟。
-
LSE(low speed external)是低速外部时钟,接频率为32.768kHz的石英晶体。
-
PLL(phase locked loop)为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。
系统时钟SYSCLK可来源于三个时钟源:HSI振荡器时钟、HSE振荡器时钟、PLL时钟。
(3)几个重要的时钟(黑色字体)
这些时钟都是通过时钟源、倍频器和预分频器的各种配置而得到的:
- SYSCLK(系统时钟)
- AHB(advanced high performance bus,高级高性能总线)总线时钟
- APB1(advanced peripheral bus,高级外围设备总线)总线时钟(低速):速度最高为36MHz
- APB2总线时钟(高速):速度最高为72MHz
- PLL时钟
以及还有一些未提及的外设时钟(最右侧),这些外设的时钟要根据外设的需求来进行配置。
(4)预分频器(绿色框图)
分频器的作用是:系统时钟先经过固定的分频系数后产生相应频率的时钟,提供给单片机定时器的计时输入。
图中绿色的框图为预分频器,图中已写出每个分频器的可分频因子。分频器的相关知识数电讲过。
(5)选择器(灰色框图)
图中灰色的梯形框图为选择器,数电也曾经讲过。这个形状应该很熟悉吧!
(6)时钟安全系统CSS(橙色框图)
如果外部晶振出了问题(例如被短路),系统时钟(SYSCLK)就会崩溃。因此,STM32时钟系统设置了CSS时钟安全系统,一旦HSE失效,则自动切换至SYSCLK = HSI,意味着系统用内部RC振荡器作为时钟源(对照时钟树的路线看看是怎么进行切换的!)。当然RC振荡器并不是十分精确的,当外部晶振恢复正常后,我们还是希望系统切换回外部晶振。
2.其中一条线路举例(可对照时钟树进行查看)
OSC_IN (8MHz) -->
HSE_OSC -->
选择器 -->
选择器 -->
PLL (8MHz X 9 = 72MHz) -->
作为PLLCLK -->
选择器 -->
作为SYSCLK -->
AHB预分频器 -->
作为HCLK(advanced high performance bus clock,高级高性能总线时钟)。
3. 与RCC相关的寄存器定义以及函数
本节可参考STM32中文参考手册6.3节RCC寄存器描述。
与RCC寄存器相关的定义如下(位于stm32f10x.h
头文件):
/**
* @brief Reset and Clock Control
*/
typedef struct
{
__IO uint32_t CR; //HSI,HSE,CSS,PLL等的使能和就绪标志位
__IO uint32_t CFGR; //PLL等的时钟源选择,分频系数设定
__IO uint32_t CIR; // 清除/使能 时钟就绪中断
__IO uint32_t APB2RSTR; //APB2线上外设复位寄存器
__IO uint32_t APB1RSTR; //APB1线上外设复位寄存器
__IO uint32_t AHBENR; //DMA,SDIO等时钟使能
__IO uint32_t APB2ENR; //APB2线上外设时钟使能
__IO uint32_t APB1ENR; //APB1线上外设时钟使能
__IO uint32_t BDCR; //备份域控制寄存器
__IO uint32_t CSR; //控制状态寄存器
#ifdef STM32F10X_CL
__IO uint32_t AHBRSTR;
__IO uint32_t CFGR2;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || defined (STM32F10X_HD_VL)
uint32_t RESERVED0;
__IO uint32_t CFGR2;
#endif /* STM32F10X_LD_VL || STM32F10X_MD_VL || STM32F10X_HD_VL */
} RCC_TypeDef;
需要注意,STM32F10X_CL
这些都属于互联型,编写者为了兼容更多机型而采用了条件编译的办法。我们使用的是大容量(STM32F10X_HD
),因此有条件编译的语句只看STM32F10X_HD
的即可。在库函数中经常涉及这种情况。
与RCC寄存器相关的函数如下(位于stm32f10x.h
头文件):
(1)时钟使能配置:
RCC_LSEConfig() 、RCC_HSEConfig()、
RCC_HSICmd() 、 RCC_LSICmd() 、 RCC_PLLCmd() ……
(2)时钟源相关配置:
RCC_PLLConfig ()、 RCC_SYSCLKConfig() 、
RCC_RTCCLKConfig() …
(3)分频系数选择配置:
RCC_HCLKConfig() 、 RCC_PCLK1Config() 、 RCC_PCLK2Config()…
(4)外设时钟使能:
RCC_APB1PeriphClockCmd(): //APB1线上外设时钟使能
RCC_APB2PeriphClockCmd(); //APB2线上外设时钟使能
RCC_AHBPeriphClockCmd(); //AHB线上外设时钟使能
(5)其他外设时钟配置:
RCC_ADCCLKConfig (); RCC_RTCCLKConfig();
(6)状态参数获取参数:
RCC_GetClocksFreq();
RCC_GetSYSCLKSource();
RCC_GetFlagStatus()
(7)RCC中断相关函数:
RCC_ITConfig() 、 RCC_GetITStatus() 、 RCC_ClearITPendingBit()…
二、系统初始化时的时钟配置
本节将详细介绍时钟初始化的流程。
上一次学习笔记我们可以看到,即使我们没有一步一步对时钟进行配置,只是打开引脚相应的时钟,LED和蜂鸣器依然可以正常工作,这是为什么呢?原来,在系统刚启动时,会运行启动程序(该程序为汇编程序startup_stm32f10x_hd.s
),调用SystemInit函数,从而配置好默认的系统时钟:
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
这就意味着,当程序来到main函数里的时候,系统的时钟已经被配置成72MHz。
下面我们进入SystemInit()函数,去一探时钟初始化的究竟。
1. 时钟系统初始化函数SystemInit()
这个函数位于源文件system_stm32f10x.c
,它是与时钟配置相关的文件,在它的开头,定义了宏,可以看到默认情况下使用的是72MHz:
#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
#define SYSCLK_FREQ_24MHz 24000000
#else
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */
/* #define SYSCLK_FREQ_36MHz 36000000 */
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */
#define SYSCLK_FREQ_72MHz 72000000
#endif
在system_stm32f10x.c
的第200多行,为SystemInit()函数,可对照STM32中文参考手册6.3节RCC寄存器描述 (为简短起见,我们将无关代码删去了)(星号注释为本人翻译):
void SystemInit (void)
{
/* 重置RCC时钟配置至默认状态(为了调试功能) */
/* 置 HSION 位为1 */
RCC->CR |= (uint32_t)0x00000001;
/* 置 SW, HPRE, PPRE1, PPRE2, ADCPRE 和 MCO 位为0 */
RCC->CFGR &= (uint32_t)0xF0FF0000;
/* 置 HSEON, CSSON 和 PLLON 位为0 */
RCC->CR &= (uint32_t)0xFEF6FFFF;
/* 置 HSEBYP 位为1 */
RCC->CR &= (uint32_t)0xFFFBFFFF;
/* 置 PLLSRC, PLLXTPRE, PLLMUL 和 USBPRE/OTGFSPRE 位为1 */
RCC->CFGR &= (uint32_t)0xFF80FFFF;
/* 关掉中断相应的位 */
RCC->CIR = 0x009F0000;
/* 配置系统时钟 */
/* Configure the Flash Latency cycles and enable prefetch buffer */
SetSysClock(); // 下一节会涉及该函数的详细介绍
#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(),下面跳到该函数去看看!
2. 系统时钟配置函数SetSysClockTo72()
在system_stm32f10x.c
的第400多行,为SetSysClock()函数,可对照STM32中文参考手册6.3节RCC寄存器描述:
static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
SetSysClockTo72();
#endif
/* If none of the define above is enabled, the HSI is used as System clock
source (default after reset) */
}
根据该文件开头的宏定义,程序将执行函数SetSysClockTo72()
。在文件差不多1000多行,为该函数的完整定义(本程序已删去无关代码):
static void SetSysClockTo72(void)
{
__IO uint32_t StartUpCounter = 0, HSEStatus = 0;
// 1.本人注:在头文件core_cm3.h中将__IO宏定义为volatile
/* 配置 SYSCLK, HCLK, PCLK2 和 PCLK1 ---------------------------*/
/* 2.使能 HSE */
RCC->CR |= ((uint32_t)RCC_CR_HSEON);
/* 3.等待 HSE 就绪,如果超时,则退出循环不再等待 */
do
{
HSEStatus = RCC->CR & RCC_CR_HSERDY;
StartUpCounter++;
} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
// 4.本人注:标志HSE此时的状态是否启动成功
if ((RCC->CR & RCC_CR_HSERDY) != RESET)
{
HSEStatus = (uint32_t)0x01;
}
else
{
HSEStatus = (uint32_t)0x00;
}
// 本人注:如果HSE启动成功,则程序继续执行
if (HSEStatus == (uint32_t)0x01)
{
// 5.本人注:此处参考STM32F10xxx闪存编程手册3寄存器说明
/* Enable Prefetch Buffer */
FLASH->ACR |= FLASH_ACR_PRFTBE;
/* 6.Flash 2 wait state */
FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;
// 本人注:以下参考STM32中文参考手册6.3.2节时钟配置寄存器
/* 7.HCLK = SYSCLK = 72MHz*/
RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
/* 8.PCLK2 = HCLK = 72MHz*/
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;
/* 9.PCLK1 = HCLK / 2 = 36MHz (原文漏了/2,疑似笔误) */
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;
/* 10.锁相环PLL配置: 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);
/* 11.使能 PLL */
RCC->CR |= RCC_CR_PLLON;
/* 12.等待 PLL 就绪 */
while((RCC->CR & RCC_CR_PLLRDY) == 0)
{
}
/* 13.选择 PLL 作为系统时钟 */
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;
/* 14.等待 PLL 切换为系统时钟 */
while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
{
}
}
else
{ /* 15.如果HSE启动失败,说明时钟拥有错误的配置。
编写者可在这里添加处理错误的代码 */
}
}
#endif
对照时钟树发现程序的配置顺序和时钟树的顺序不一致,其实无所谓,打个比方,只有打开所有开关系统才能正常运行,打开部分开关还是运行不了。
初始化之后的状态:
- SYSCLK = 72MHz
- AHB = 72MHz
- PCLK1(peripheral bus clock,外围设备总线时钟) = 36MHz
- PCLK2 = 72MHz
- PLL = 72MHz
3.小结
系统初始化时钟的流程:startup(汇编程序) -> SystemInit() -> SetSysClock() -> SetSysClockTo72() -> main()。
三、使用库函数配置系统时钟HSE
在第一部分我们已经列举出了一些RCC常用的函数,接下来我们自己写一个函数来配置我们想要的时钟频率。代码如下(这里的步骤与上面代码的步骤一一对应):
#include "rcc_config.h"
#include "stm32f10x.h"
void HSE_SetSysClk( uint32_t RCC_PLLMul_x )
{
// 1.定义HSE标志位
ErrorStatus HSEStatus;
// 初始化时钟,这里的作用是清除复位
RCC_DeInit();
// 2.使能HSE
RCC_HSEConfig(RCC_HSE_ON);
// 3.等待 HSE 就绪,如果超时,则退出循环不再等待
HSEStatus = RCC_WaitForHSEStartUp();
// 4.如果HSE启动成功,则程序继续执行
if( HSEStatus == SUCCESS )
{
// 5&6.设置Flash预缓冲
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
FLASH_SetLatency(FLASH_Latency_2);
// 7.设置AHB时钟HCLK
RCC_HCLKConfig(RCC_SYSCLK_Div1);
// 8.设置低速APB时钟
RCC_PCLK1Config(RCC_HCLK_Div2);
// 9.设置高速APB时钟
RCC_PCLK2Config(RCC_HCLK_Div1);
// 10.设置PLL
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_x);
// 11.使能PLL
RCC_PLLCmd(ENABLE);
// 12.等待PLL就绪
while( RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
// 13.选择PLL作为系统时钟
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
// 14.等待 PLL 切换为系统时钟
while( RCC_GetSYSCLKSource() != 0x08 );
}
else
{
// 15.用户在这里添加处理错误的代码
}
}
现在我们在主函数里调用自己写的时钟配置函数。这里我写了个流水灯程序,在main函数一开始调用了自己写的HSE_SetSysClk()函数,参数为PLL锁相环的倍频因子,可以看到我这里用的是12倍频因子,8MHz*12=96MHz,超频(因为最高为72MHz),delay函数的延时效果会更加短。
#include "stm32f10x.h"
#include "led.h"
#include "rcc_config.h"
void delay( uint32_t count )
{
for(; count != 0; count--);
}
int main(void)
{
HSE_SetSysClk(RCC_PLLMul_12); //这里可以选择自己想要的倍数
//倍数越高,流水灯越快,但同时也有超频的可能
LED_Init();
while(1){
GPIO_SetBits(GPIOB,GPIO_Pin_5);
GPIO_ResetBits(GPIOE,GPIO_Pin_5);
delay(0xFFFFF);
GPIO_ResetBits(GPIOB,GPIO_Pin_5);
GPIO_SetBits(GPIOE,GPIO_Pin_5);
delay(0xFFFFF);
}
}