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实现。