首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

STM32的系统时钟设置

上一篇文章中,我们先介绍了MCU工作的基本原理, 然后建立了一个极精简的工程项目,并在此基础上分析了STM32的启动过程。在本文中,我们将介绍处理器的脉搏——系统时钟及其配置方法。

时钟是处理器得以正常运行的关键。CPU是一个典型的时序逻辑,它需要一个时钟信号来驱动,进而从内存中读取指令和数据进行运算。 此外,STM32上的很多外设也需要时钟信号来驱动。驱动时钟频率越高,处理器的运算速度就越快。

本文中,我们将详细分析STM32的时钟系统。

1. STM32的时钟源

右图是直接从STM32的参考手册上抠下来的图。 可以看到有三种时钟源(高速外部时钟HSE、高速内部时钟HSI、锁相环时钟PLLCLK)可以用于系统时钟SYSCLK。 对于F407系列处理器而言,SYSCLK的最高频率为168MHz。

使用外部时钟HSE作为系统时钟源的好处在于,它所产生的时钟频率较为精确。 我们可以直接输入一个用户时钟作为HSE,也可以通过振荡器和电容生成HSE。 我们选择的开发板采用的就是后一种方案,用一个8MHz的晶振作为HSE,接到OSC_IN和OSC_OUT脚两端。 对于这种方案,为了尽量减小输出时钟的变形,缩短时钟起震时间, 应当保证振荡器和电容尽可能的接近OSC_IN和OSC_OUT。 具体的电路设计参考探索者开发板原理图和ST的参考手册。

内部高速时钟HSI是由芯片上的一个16MHz的RC振荡器产生的,可以直接用作系统时钟或者锁相环的输入。 HSI的优势在于功耗低,不需要额外的器件,起震快。 而芯片上的RC振荡器的频率与生产过程有关,因而不同芯片之间的存在差异,精度不能保证,而且电压和温度都对其有较大的影响。

F4系列的芯片都有两个锁相环,分别被称为PLL和PLLI2S。PLL的输入是HSE或者HSI,它可以输出两路时钟, 一路用于最高168MHz的系统时钟,另一路则生成最高48MHz的时钟驱动USB OTG FS,随机信号生成器(random analog generator), 和SDIO(Secure digital input/output interface)。PLLI2S是为了生成高品质的音响效果,专门为I2S接口提供的锁相环时钟。 顾名思义,锁相环是一个更精确的系统时钟源。

此外,F407还有LSE和LSI两个低速的时钟源。LSE用于给实时时钟RTC提供精确的时钟信号。 通常是由一个32.768KHz的晶振配合电容提供的,它也可以直接输入一个用户时钟到OSC32_IN并保持OSC32_OUT高阻态, 只是这个时钟频率必须超过1MHz。LSI则用于在Stop/Standby模式下,驱动独立看门狗(IWDG)和自动唤醒单元(AWU),其工作频率为32KHz。

2. 外设和总线时钟

F407的外设都接在了AHB/APB总线上。具体外设挂在了那个总线上, 参考Reference Manual的2.3节和 DataSheet的第4节关于内存映射(memory mapping)的内容。 AHB的总线频率最高为168MHz。高速APB(APB2)的最高频率为84MHz,低速APB(APB1)的最高频率为42MHz。 在系统刚上电时AHB和APB上的各个外设时钟默认是关闭的,使用时需要通过软件开启。

除了几个特殊外设外,所有的外设的时钟都是从系统时钟(SYSCLK)派生出来的。 USB OTG FS,随机信号生成器(random analog generator)和SDIO是由PLL直接驱动的。I2S的驱动时钟专门由PLLI2S驱动, 也可以通过直接输入一个精确的时钟信号到I2S_CKIN,而USB OTG HS和以太网卡(Ethernet MAC)的时钟由外部设备提供。 在使用以太网时,AHB总线的频率最低不能小于25MHz。

SYSCLK经过AHB分频器(AHB PRESC)分频后得到HCLK用来驱动AHB总线、内核(core)、内存和DMA。 此外HCLK还经过一个8分频驱动Cortex内核的系统计时器SysTick。 而FCLK则是Cortex-M4的FPU时钟,以后会在介绍FPU时详细介绍。

3. 系统时钟寄存器

根据Reference Manual, 我们知道系统的时钟是通过系统复位和时钟控制(RCC)寄存器配置的。 在第6.3节中列举了25个RCC寄存器的位定义和偏移地址。参考STM32官方库函数, 这里做了一些简化,定义如下的结构体用于访问RCC的每个寄存器:

        typedef struct rcc_regs {
            volatile  uint32 CR;            /* 时钟控制寄存器, offset: 0x00 */
            volatile  uint32 PLLCFGR;       /* 锁相环配置寄存器, offset: 0x04 */
            volatile  uint32 CFGR;          /* 时钟配置寄存器, offset: 0x08 */
            volatile  uint32 CIR;           /* 时钟中断寄存器, offset: 0x0C */
            volatile  uint32 AHB1RSTR;      /* AHB1外设复位寄存器, offset: 0x10 */
            volatile  uint32 AHB2RSTR;      /* AHB2外设复位寄存器, offset: 0x14 */
            volatile  uint32 AHB3RSTR;      /* AHB3外设复位寄存器, offset: 0x18 */
            uint32 RESERVED0;               /* 保留, 0x1C */
            volatile  uint32 APB1RSTR;      /* APB1外设复位寄存器, offset: 0x20 */
            volatile  uint32 APB2RSTR;      /* APB2外设复位寄存器, offset: 0x24 */
            uint32  RESERVED1[2];           /* 保留, 0x28-0x2C */
            volatile  uint32 AHB1ENR;       /* AHB1外设时钟使能寄存器, offset: 0x30 */
            volatile  uint32 AHB2ENR;       /* AHB2外设时钟使能寄存器, offset: 0x34 */
            volatile  uint32 AHB3ENR;       /* AHB3外设时钟使能寄存器, offset: 0x38 */
            uint32 RESERVED2;               /* 保留, 0x3C */
            volatile  uint32 APB1ENR;       /* APB1外设时钟使能寄存器, offset: 0x40 */
            volatile  uint32 APB2ENR;       /* APB2外设时钟使能寄存器, offset: 0x44 */
            uint32 RESERVED3[2];            /* 保留, 0x48-0x4C */
            volatile  uint32 AHB1LPENR;     /* AHB1低电压模式下外设时钟使能寄存器, offset: 0x50 */
            volatile  uint32 AHB2LPENR;     /* AHB2低电压模式下外设时钟使能寄存器, offset: 0x54 */
            volatile  uint32 AHB3LPENR;     /* AHB3低电压模式下外设时钟使能寄存器, offset: 0x58 */
            uint32 RESERVED4;               /* 保留, 0x5C */
            volatile  uint32 APB1LPENR;     /* APB1低电压模式下外设时钟使能寄存器, offset: 0x60 */
            volatile  uint32 APB2LPENR;     /* APB2低电压模式下外设时钟使能寄存器, offset: 0x64 */
            uint32 RESERVED5[2];            /* 保留, 0x68-0x6C */
            volatile  uint32 BDCR;          /* Backup domain控制寄存器, offset: 0x70 */
            volatile  uint32 CSR;           /* 时钟控制和状态寄存器, offset: 0x74 */
            uint32 RESERVED6[2];            /* 保留, 0x78-0x7C */
            volatile  uint32 SSCGR;         /* 扩频时钟发生器寄存器(听起来很高大上,有什么用暂时不知道,以后会详细解析) offset: 0x80 */
            volatile  uint32 PLLI2SCFGR;    /* 锁相环PLLI2S配置寄存器, offset: 0x84 */
        } rcc_regs_t; 

在结构体rcc_regs的注释中,每个寄存器都有一个偏移量(offset)。这个偏移量是相对于RCC寄存器的基地址而言的。 在DataSheet的内存映射描述中指出, RCC寄存器被映射到了0x4002 3800 - 0x4002 3BFF地址空间中,挂载在AHB1总线上。 在下一篇文章中,我们会详细提到访问一个片上外设,需要准备三个要素:描述外设的结构体, 外设的起始地址,指向该地址的指针。rcc_regs_t就是所谓的描述外设的结构体,关于起始地址和指针,我们可以通过如下两个宏定义来实现。

        #define RCC_BASE 0x40023800
        #define RCC ((rcc_regs_t *)RCC_BASE)

那么我们就可以通过如下的形式来访问RCC的寄存器了。

        RCC->CR
        RCC->CFGR

那么是什么原理,使得我们可以通过这几个宏定义访问外设的寄存器?在C语言中,宏定义的使用都是一个简单的文本替换的过程。 例如这里的RCC->CR展开之后就是((rcc_regs_t *)0x40023800)->CR。 我们先来看其中的(rcc_regs_t *)0x40023800,这是一个强制类型转换,表示把数值0x40023800解释成为一个rcc_regs_t的指针。 有人会奇怪,0x40023800分明是一个数,怎么可以对它进行强制类型转换呢?实际上,我们可以将其看做是对一个值为0x40023800的常量进行的类型转换:

        const unsigned int tmp = 0x40023800;
        (rcc_regs_t *)tmp
那么这个tmp就是一个指向rcc_regs_t的指针,只是它的值是不可以改变的。因而我们可以得到类似tmp->CR的语句, 就达到了通过指针访问外设的CR寄存器的目的。

4. 系统时钟配置

我们对官方库中涉及到系统初始化的函数SystemInit进行了如下的简化,删去了各种与具体设备相关的条件编译选项:

        #include <stm32f4xx.h>

        #define PLL_M 8 
        #define PLL_N 336
        #define PLL_P 2
        #define PLL_Q 7

        void SystemInit(void) {
            // 设置FPU
            SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));

            // 开外部高速时钟
            RCC->CR |= RCC_CR_HSEON;
            while (!(RCC->CR & RCC_CR_HSERDY));
            // 配置PLL时钟源、各个时钟分频
            RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV2 | RCC_CFGR_PPRE1_DIV4;
            RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) - 1) << 16) | (RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24);
    
            FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN |FLASH_ACR_DCEN |FLASH_ACR_LATENCY_5WS;
            // 打开PLL
            RCC->CR |= RCC_CR_PLLON;
            while (!(RCC->CR & RCC_CR_PLLRDY));
            // 以PLL输出作为系统时钟源
            RCC->CFGR &= ~RCC_CFGR_SW;
            RCC->CFGR |= RCC_CFGR_SW_PLL;
            while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
        } 

在这段代码中,我们选用外部高速时钟HSE作为PLL的输入时钟,以PLL的输出作为系统的时钟源。 先开启了HSE,然后配置PLL时钟源和分频系数,最后打开PLL配置系统时钟源。 其中第10行不涉及时钟的配置,本来我也想把它们也删掉的,但是这样做的话系统就运行不起来,原因还没有探明就暂时保留。

在第19行中,我们向FLASH的ACR寄存器写了一些配置内容,打开了指令预取、指令缓存和数据缓存功能,这样将有效的提高CPU的效率。 此外,我们还设置了5个CPU周期的等待时间,这是根据我们开发板是3.3V供电并且工作在168MHz的频率下决定的。 详细原因可以参考片上闪存

此外,在函数之外还定义了四个宏PLL_M, PLL_N, PLL_P, PLL_Q,这四个宏定义将在第17行中用于配置锁相环的输出频率。 根据Reference Manual中对时钟复位控制器的PLL配置寄存器RCC_PLLCFGR的描述,可以得到PLL的几个输出频率可以用如下的公式计算: $$ f_{(VCO\ clock)} = f_{(PLL\ input)} \times (PLLN / PLLM) $$ $$ f_{(PLL\ general\ clock\ output)} = f_{(VCO\ clock)} / PLLP $$ $$ f_{(USB\ OTG\ FS, SDIO, RNG\ clock\ output)} = f_{(VCO\ clock)} / PLLQ $$ 根据探索者原理图,可以看出F407的外部时钟是一个8MHz的晶振, 我们希望系统总线运行在168MHz的频率上。套用上面的公式,PLL_M = 8, PLL_N = 336, PLL_P = 2可以满足我们对于总线时钟的需求。

图1 探索者开发板的原理图上显示F407是由一个8MHz的晶振提供的

5. 总结

F407有三种时钟源可以用作系统时钟:内部高速时钟、外部高速时钟、PLL时钟。 一般我们希望芯片工作在最高频率168MHz,而无论是内部还是外部时钟都是达不到的,所以通常都是用PLL时钟作为系统时钟。 外部时钟通常都比内部时钟要稳定精确,所以一般还会用外部时钟作为PLL的输入。 F407还有一个低速时钟用来驱动RTC,以及满足低电压模式下的功能需求。

通常系统总线AHB的频率设置为168MHz,高速外设总线APB2频率设置为84MHz,低速外设总线APB1频率设置为42MHz。 这些总线频率可以通过配置RCC_CFGR和RCC_PLLCFGR实现。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1