【嵌入式系统】STM32时钟系统+时钟配置函数解析
1、时钟系统
时钟系统为整个硬件系统的各个模块提供时钟信号。时钟是整个数字电路的驱动之源,所有数字部件的运行都依赖时钟信号的输入才得以向前推进。
由于系统复杂性,各硬件模块可能对时钟信号有不同要求,因此在系统中应按需分别提供时钟信号。这些时钟信号或者来自不同振荡器,或者是从一个主振荡器开始,经过多次的倍频、分频、锁相环等电路而生成的独立时钟信号。不同时钟信号而非单一时钟的设计还有助于实现系统的低功耗:一些低速外设可以使用功耗更低的低速时钟,部分硬件没有使用时也可除能时钟位达到关闭的效果。
2、时钟树
注
在实际应用时,为避免因为外部时钟源失效造成MCU内部紊乱导致无可挽回的损失,引入CSS来监控外部时钟源,这是STM32作为一个可靠系统的必要条件。当外部时钟源出现故障失效时,CSS会主动切断外部时钟并开启内部时钟作为替代,同时产生一个时钟安全中断。此中断属于NMI,将不断执行直到CSS中断挂起位被清除。
3、时钟配置函数解析
①时钟配置常用寄存器组
用结构体复位与时钟控制**(RCC, Reset and Clock Control)**来表示。
typedef struct { __IO uint32_t CR; //HSI、HSE、CSS、PLL的使能和就绪标志位 __IO uint32_t CFGR; //PLL、USB等时钟源的选择和分频系数的设置 __IO uint32_t CIR; //清除/使能时钟就绪中断 __IO uint32_t APB2RSTR; //APB2上的外设复位寄存器 __IO uint32_t APB1RSTR; // APB1上的外设复位寄存器 __IO uint32_t AHBENR; //DMA、CRC等模块时钟使能 __IO uint32_t APB2ENR; //APB2外设时钟使能 __IO uint32_t APB1ENR; //APB1外设时钟使能 __IO uint32_t BDCR; //备份域控制寄存器 __IO uint32_t CSR; //控制状态寄存器 } RCC_TypeDef;
要使用某个模块或外设时,务必保证其驱动时钟被使能。
②系统初始化函数(战舰版)void SystemInit(void)
下面仅截取SystemInit()中与RCC配置相关,且采用STM32F10X_HD预编译头的代码
void SystemInit (void) { /* Reset the RCC clock configuration to the default reset state(for debug purpose) */ /* Set HSION bit */ RCC->CR |= (uint32_t)0x00000001; /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */ RCC->CFGR &= (uint32_t)0xF8FF0000; /* 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; /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; #endif /* STM32F10X_CL */ /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */ /* Configure the Flash Latency cycles and enable prefetch buffer */ SetSysClock(); }
这个初始化函数对时钟系统的作用是:初始化RCC中的相关寄存器,并配置系统时钟。
对于RCC中寄存器的配置可以完全对照手册与注释进行解读,例如:
RCC->CFGR &= (uint32_t)0xF8FF0000
值得注意,位配置的 |= 运算一般是置位Set;&=运算一般是复位Reset。根据0xF8FF0000位向量知,此条指令是将CFGR中的[15:0]以及[26:24]清空。如图3所示为参考手册给出的CFGR位模式,清零位对应的是SW, HPRE, PPRE1, PPRE2, ADCPRE和MCO。
下面考察系统时钟设置函数SetSysClock()
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 }
系统时钟频率的设置是通过条件编译进行的,若宏定义了SYSCLK_FREQ_72MHz,则SetSysClock()中就只编译执行SetSysClockTo72()
为了研究更底层的封装,再考察SetSysClockTo72()
static void SetSysClockTo72(void) { __IO uint32_t StartUpCounter = 0, HSEStatus = 0; /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/ /* 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)); if ((RCC->CR & RCC_CR_HSERDY) != RESET) HSEStatus = (uint32_t)0x01; else HSEStatus = (uint32_t)0x00; if (HSEStatus == (uint32_t)0x01) { /* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK/2 */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2; /* 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); /* Enable PLL */ RCC->CR |= RCC_CR_PLLON; /* Wait till PLL is ready */ while((RCC->CR & RCC_CR_PLLRDY) == 0) { } /* 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) { } } else { /* If HSE fails to start-up, the application will have wrong clock configuration. User can add here some code to deal with this error */ } }
此函数首先使能了时钟源HSE
RCC->CR |= ((uint32_t)RCC_CR_HSEON);
其中#define RCC_CR_HSEON ((uint32_t)0x00010000)
#define RCC_CR_HSERDY ((uint32_t)0x00020000)
因此这条指令作用是将RCC->CR的Bit16置1,结合图参考手册中图4可以确认,这条指令确实使能了HSE
接下来很重要的一步操作是等待使能成功,例如上述执行使能HSE指令后,硬件开始根据指令执行置位操作,这里存在软硬件时间差 ∆ t ∆t ∆t,在 ∆ t ∆t ∆t内HSE仍然是除能状态,因此等待使能成功的目的就是起到对 ∆ t ∆t ∆t的过渡作用,防止硬件误动作。
do { HSEStatus = RCC->CR & RCC_CR_HSERDY; StartUpCounter++; } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
这个while循环首先用HSEStatus获取HSE状态:若HSERDY=1即HSE已经使能,则HSEStatus=1;否则HSEStatus=0。
确认使能了HSE后,便开始配置一系列的时钟,这里将HCLK、PCLK2、PLLCLK配置为72MHz,将PCLK1配置为36MHz,例如:
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);
①通过&=进行复位。出于安全性考虑,置位操作中可能用到的位先清0,以防置位时产生进位引发错误。可以看到,这里将时钟源选择位、预分频位和倍频位清0,即此时PLL处于无时钟源、不分频、不倍频的状态。
②通过|=进行置位。RCC_CFGR_PLLSRC_HSE即选择HSE作为PLL时钟源,HSE一般接8MHz晶振;RCC_CFGR_PLLMULL9选择倍频系数为9,因此PLLCLK为8×9=72MHz的时钟。
至此,SystemInit()的原理讲解完毕。事实上,用户也可根据需要,自行选择相应寄存器来设计并封装初始化函数