本帖最后由 正点原子运营 于 2024-3-22 16:30 编辑
1)实验平台:正点原子 M144Z-M3 STM32F103最小系统板
2) 章节摘自【正点原子】M144Z-M3最小系统板使用指南——STM32F103版
6)正点原子STM32技术交流QQ群:725095144
MCU都是基于时序控制的系统,本章将为STM32F103的时钟系统作一个简单的介绍,帮助读者更全面、系统地认识STM32F103的时钟系统结构,并掌握STM32F103的时钟配置。 本章分为如下几个小节: 8.1 认识时钟树 8.2 配置系统主频 8.3 开启和关闭外设时钟
8.1 认识时钟树 从数字电路的知识可以知道:任意复杂的电路控制系统都可以经由门电路组成的组合电路实现。STM32内部也是由多种多样的电路模块组合在一起实现的。若一个电路越复杂,在达到正确的输出结果前,它可能因为延时会有一些短暂的中间状态,而这些中间状态有时会导致输出结果会有一个短暂的错误,这叫做电路中的“毛刺现象”,如果电路需要运行得足够快,那么这些错误状态会被其它电路作为输入采样,最终形成一系列的系统错误。为了解决这个问题,在单片机系统中,设计时以时序电路控制替代纯粹的组合电路,在每一级输出结果前对各个信号进行采样,从而使得电路中某些信号即使出现延时也可以保证各个信号的同步,可以避免电路中发生的“毛刺现象”,达到精确控制输出的效果。 由于时序电路的重要性,因此在MCU设计时就设计了专门用于控制时序的电路,在芯片设计中称为时钟树设计。由此设计出来的时钟,可以精确控制单片机系统,这也是本小节要展开分析的。为什么是时钟树而不是时钟呢?一个MCU越复杂,时钟系统也会相应的变得复杂,如STM32F103的时钟系统就比较复杂,不像简单的51单片机,一个系统时钟就可以解决一切。对于STM32F103系列的芯片,正常工作的主频可以达到72MHz,但并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及RTC只需要几十KHz的时钟频率即可工作。同一个电路,时钟频率越快功耗就越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的MCU一般都是采取多时钟的方法来解决这些问题。 STM32本身相对复杂,外设资源非常丰富,为了保证低功耗,STM32上电或复位后默认不开启这些外设功能,即不为这些外设提供时钟。用户可以根据自己的需要决定STM32芯片要使用的功能,即为这些外设提供时钟,同时也要配置时钟源和时钟的频率 上图就是STM32F103的时钟树框图 ①:该部分是四个时钟源,分别为:LSI(内部低速时钟)、LSE(外部低速时钟)、HSI(内部高速时钟)、HSE(外部高速时钟),这四个时钟源是整个时钟树的“源头”。 ②:该部分为PLL(锁相环),这一部分内部是较为复杂的模拟电路,主要用于对输入的时钟信号倍频后输出,输入的时钟源可以来自HSI或HSE。 ③:该部分主要用于选择系统时钟的时钟源,从图中可以看出,该选择器的输入有HSE、HSI和PLL1,输出作用于SYSCLK,而SYSCLK通过AHB Prescaler分频后输出,该输出将直接或间接地作为AHB、APB1和APB2总线上外设的时钟源,其中FCLK就是MCU内核的时钟,这部分对整个系统的运行起着至关重要的作用。 ④:AHBPrescaler分频后的时钟再通过APB1Prescaler和APB2Prescaler分频后分别作为APB1和APB2总线上外设的时钟。 8.1.1 时钟源 STM32F103有四个时钟源,分别为:LSI(内部低速时钟)、LSE(外部低速时钟)、HSI(内部高速时钟)、HSE(外部高速时钟)。按低速时钟源和高速时钟源可分为LSI、LSE和HSI、HSE,按内部时钟源和外部时钟源可分为LSI、HSI和LSE、HSE,其中内部时钟源LSI和HSI是MCU芯片内部的时钟源,芯片上电后即可输出时钟,无需借助外部电路。 ①:外部高速时钟HSE(High Speed External Clock) 该时钟信号由外部晶体、陶瓷谐振振荡器或外部时钟源产生,频率范围为4MHz~26MHz,正点原子M144Z-M3最小系统板STM32F103版使用频率为8MHz的晶体振荡器产生该时钟信号。 ②:外部低速时钟LSE(Low Speed External Clock) 该时钟信号由外部晶体、陶瓷谐振振荡器或外部时钟源产生,频率为32.768KHz,主要作为RTC的时钟源,正点原子M144Z-M3最小系统板STM32F103版使用频率为32.768KHz的晶体振荡器产生该时钟信号。 ③:内部高速时钟HSI(High Speed Internal Clock) 该时钟信号由内部RC振荡器产生,频率为16MHz。 ④:内部低速时钟LSI(Low Speed Internal Clock) 该时钟信号由内部RC振荡器产生,频率为20KHz~35KHz(受温度、电压影响),主要作为独立看门狗或RTC的时钟源。 MCU上电时默认会使用HSI作为系统时钟启动,只有完成了相关的时钟配置,MCU才会根据配置切换到对应的时钟源,因此同时了解这几个时钟源是很有必要的,下文将会有提到时钟配置方法的章节。 8.1.2 锁相环 锁相环(PLL)是自动控制系统中常用的一个反馈电路,在STM32主控中,锁相环主要有对输入时钟进行净化和倍频这两个作用,前者是利用锁相环电路的反馈机制实现的,后者是用于使STM32主控稳定地工作于更高的时钟频率。 在STM32中,锁相环的输出时钟可以配置为系统时钟的时钟源,如下图所示: 图8.1.2.1 PLL1输出时钟作为系统时钟示例 从上图中可以看出,HSE的时钟HSECLK被作为PLL的输入时钟输入PLL,随后在PLL中经过倍频后作为PLLCLK输出,PLLCLK被作为SYSCLK。 上面过程中的配置基本可以由时钟配置寄存器(RCC_CFGR)完成配置。 图8.1.2.2 时钟配置寄存器PLLXTPRE位 PLLXTPRE位用于对输出PLL的HSECLK进行分频,可选不分频(写0)或二分频(写1)。将该位配置为0,即不对HSECLK进行分频。 PLLSRC位用于选择PLL的输入时钟,可选HIS二分频(写0)或HSE(写1)。将该位配置位配置位1,即可使用HSE输入的8MHz时钟信号作为PLL的输入。 PLLMUL位用于配置PLL的倍频系数,可选倍频系数位2~16,将改为配置为9,即可将输入PLL的8MHz时钟信号倍频为72MHz。 SW位用于选择SYSCLK的时钟源,可选HIS(写0)、HSE(写1)或PLL(写2)。将该位配置为2,即可使用PLL输出的72MHz时钟信号作为SYSCLK的时钟源。 8.1.3 系统时钟 STM32的系统时钟(SYSCLK)为整个芯片的绝大多数外设和内核提供时钟信号,对于相同稳定运行的系统,时钟信号的频率越高,系统运行的速度也就越快,单位时间能够处理的事务也就越多。在上一小节中,将频率为8MHz的HSE时钟信号通过PLL倍频为72MHz的时钟信号作为SYSCLK的时钟源。SYSCLK时钟信号可直接或间接地作为AHB总线、APB1总线、APB2总线和内核的时钟源。 ①:AHB预分频器,可对SYSCLK进行分频,分频范围为1~512。对于示例,AHB预分频器选择不分频(/1),那么AHB总线、内核、APB1总线和APB2总线输入的时钟信号频率都为72MHz。 ②:APB1预分频器,可对待输入APB1总线的时钟信号进行分频,分频范围为1~16,APB1总线的时钟频率最大值为36MHz。对于示例,APB1预分频器选择2,即对频率为72MHz的时钟信号进行2分频为36MHz后作为APB1总线的时钟频率。 ③:APB2预分频器,可对待输入APB2总线的时钟信号进行分频,分频范围为1~16,APB2总线的时钟频率最大值为72MHz。对于示例,APB2预分频器选择1,即使用72MHz的时钟信号作为APB2总线的时钟。
8.2 配置系统主频 STM32F103在默认情况下,使用频率为8MHz的HSI作为系统时钟的时钟源,因此无需外部晶振也能够正常烧录和运行程序。 在上一小节中,介绍了如何配置时钟树,以让内核在72MHz的主频下运行,72MHz是ST官方针对STM32F103推荐的最大工作频率。 本节将介绍如何在代码中具体的对STM32F103的时钟进行配置。本书配置实验例程都是在main()函数中调用sys_stm32_clock_init()对STM32F103的时钟进行配置的,如下所示: - int main(void)
- {
- /* 省略其他无关代码 */
-
- sys_stm32_clock_init(RCC_PLL_MUL9); /* 配置系统时钟 */
-
- /* 省略其他无关代码 */
- }
复制代码相信读者对sys_stm32_clock_init()函数传入的参数并不陌生,这就是在上一小节示例中配置时钟配置寄存器中PLLMUL的值。 sys_stm32_clock_init()函数的代码比较多,请读者在实验例程中查看本函数的具体实现,本小节中仅列出该函数的关键代码,如下所示: - voidsys_stm32_clock_init(uint32_t plln)
- {
- RCC_OscInitTypeDef rcc_osc_init = {0};
- RCC_ClkInitTypeDef rcc_clk_init = {0};
-
- /* 配置PLL */
- rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE;
- rcc_osc_init.HSEState = RCC_HSE_ON;
- rcc_osc_init.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
- rcc_osc_init.PLL.PLLState = RCC_PLL_ON;
- rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE;
- rcc_osc_init.PLL.PLLMUL = plln;
- HAL_RCC_OscConfig(&rcc_osc_init);
-
- /* 配置时钟 */
- rcc_clk_init.ClockType = ( RCC_CLOCKTYPE_SYSCLK |
- RCC_CLOCKTYPE_HCLK |
- RCC_CLOCKTYPE_PCLK1|
- RCC_CLOCKTYPE_PCLK2);
- rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
- rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1;
- rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV2;
- rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1;
- HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_2);
- }
复制代码跟上一小节介绍的一致,AHB、APB2和APB1总线的预分频器分别配置为不分频、2分频和不分频;根据函数的传入参数配置时钟配置寄存器的PLLMUL位的值,并将PLL的时钟源配置为HSE;将PLLCLK配置为SYSCLK的时钟源。上面列出的代码仅是一些关键代码,除了列出代码的操作外还需要一些额外的操作,例如使能HSE并等待HSE稳定等,建议读者打开本书配套的任意实验例程,在sys.c文件中详细地查看本函数的具体实现。 通过sys_stm32_clock_init()函数就能够非常方便地配置系统主频了,例如,在考虑对MCU的性能需求和功耗的场景中,可以适当降低MCU的主频,来达到性能和功耗的平衡。
8.3 开启和关闭外设时钟 在前面的章节中,分析了STM32F103的时钟系统和相关的配置步骤,但是在使用某些外设之前,还需要手动开启相应外设的时钟,这是因为MCU在上电或复位后会默认关闭大部分外设的时钟,以降低功耗,因此,在程序开发中,对于某些开启的时钟但不再使用的外设,也应将其时钟关闭。 STM32F103外设的时钟由RCC(Reset andClock Control)控制,RCC中控制外设时钟的寄存器被按总线进行分类,涉及了五个寄存器,分别为RCC_AHB1ENR、RCC_AHB2ENR、RCC_AHB3ENR、RCC_APB1ENR和RCC_APB2ENR,这五个寄存器分别用于管理AHB总线、APB1总线和APB2总线上外设的时钟。 下面以GPIOA为例,介绍如何配置RCC中相应的寄存器来开启或关闭GPIOA的时钟。首先能够确定GPIOA是位于APB2总线上的外设,因此可以判断用于控制GPIOA时钟的寄存器为RCC_APB2ENR。在RCC_APB2ENR寄存器中可以找到IOPAEN位,该位就是用于控制GPIOA外设时钟使能和禁止的,如下图所示: 图8.3.3 RCC_APB2ENR寄存器IOPAEN位 由上图可知,往RCC_APB2ENR寄存器的IOPAEN位写入1后,就能配置使能GPIOA外设的时钟,往RCC_APB2ENR寄存器的IOPAEN位写入0后,就能配置禁止GPIOA外设的时钟。 在HAL库中提供了用于开启和关闭外设时钟的宏定义,这些宏定义被定义在stm32f1xx_hal_rcc.h文件中,如以所示: - #define __HAL_RCC_GPIOA_CLK_ENABLE() \
- do { \
- __IO uint32_t tmpreg; \
- SET_BIT(RCC->APB2ENR,RCC_APB2ENR_IOPAEN); \
- /* Delayafter an RCC peripheral clock enabling */ \
- tmpreg = READ_BIT(RCC->APB2ENR,RCC_APB2ENR_IOPAEN); \
- UNUSED(tmpreg); \
- }while(0U)
- #define __HAL_RCC_GPIOA_CLK_DISABLE() (RCC->APB2ENR &= ~(RCC_APB2ENR_IOPAEN))
复制代码上面的两个宏定义中,__HAL_RCC_GPIOA_CLK_ENABLE()用于使能GPIOA端口的时钟,而__HAL_RCC_GPIOA_CLK_DISABLE()用于失能GPIOA端口的时钟。 细心的读者可能会发现HAL库提供的用于使能和失能外设时钟的宏定义,其命名规则大致为__HAL_RCC_+外设名称+_CLK_+ENABLE或DISABLE()。 |