初始化时钟包含两个层面
- SYSCLK 系统主时钟, 这个时钟决定着整个系统, 总线的运行速度
- 外设时钟使能, 这些时钟只有在外设需要工作时才激活, 平时为了低功耗不激活
所以时钟的初始化就遵循这样的原则:
系统主时钟在main执行前期就配置好, 其余时钟按需开启
时钟源的种类简写及含义
H: high 高
L: low 低
S: speed 速度
I: internal 内部的
E: external 外部的
M: Multi 多速的
HSE: 高速外部时钟源
HSI: 高速内部时钟源
MSI: 多速内部时钟源
LSE: 低速外部时钟源
LSI: 低速内部时钟源
分析芯片手册了解时钟源
通过阅读芯片手册, 通过如下内容

可以知道, 当时钟没有初始化时, 芯片的默认运行频率是4MHz, 这个时钟就是系统主时钟
系统主时钟决定着芯片运行能力的上限, 所以第一步要将系统主时钟设置为芯片手册所规定的上限频率, 即80MHz.
确定系统时钟源的来源

通过这段描述可知, 系统主时钟可以通过4种时钟源获得
- 通过高速外部晶振(HSE)提供脉冲作为主时钟源
- 通过内部RC振荡器(HSI)产生的16Mhz脉冲作为时钟源
- 通过内部多速RC振荡器(MSI)提供的脉冲作为主时钟源
- 通过由HSE, HSI, MSI等时钟源经PLL倍频器经过适当倍频+分频后产生的脉冲作为主时钟源
上述4种SYSCLK主时钟的时钟源, 如何选择, 参考下表:
| 时钟源 | 精度 | 成本 | 功耗 | 启动速度 | SYSCLK 最高频率 |
|---|---|---|---|---|---|
| MSI | 校准后≈±0.25%,未校准一般 | 最低 | 最低 | 最快 | 48MHz |
| HSI16 | 中等(软件微调) | 低 | 低 | 快 | 16MHz |
| HSE | 最高(晶振级,± 几十 ppm) | 最高 | 中 | 慢 | 48MHz |
| PLL | 取决于输入源精度 | 输入源 | 最高 | 最慢 | 80MHz |
为了发挥板子最佳性能, 我们一般选择外部高速震荡器HSE作为时钟源, 经PLL倍频后得到最大80MHz的工作频率

通过板子的原理图可以看到有两个外部时钟源, 一个8MHz的HSE高速外部时钟源, 还有一个32.768Khz的低速外部时钟源LSE
这里我们选择HSE外部高速时钟源8MHz经PLL倍频之后获得精准的80MHz主时钟
除了设置SYSCLK主时钟还需要显式设置AHB和APB预分频器
系统主时钟SYSCLK初始化好之后, 确定了CPU的工作频率
之后要显式地设置两个预分频器AHB(Advanced High-performance Bus) 和APB(Advanced Peripheral Bus)的频率.
AHB的时钟频率称为HCLK, 连着系统高速总线, 这条总线上挂载着内存, DMA, CPU, 高速GPIO, ADC等设备
APB是外设总线, 有APB1和APB2两条
APB1的时钟频率称为PCLK1, 低速外设总线, 上面挂载着I2C, USART2等外设
APB2的时钟频率称为PCLK2, 高速外设总线, 上面挂载着USART1, SPI1,等外设

上述内容说AHB和APB可以设置的最大频率是80MHz
代码配置
整个项目的目录树如图所示
├─CMSIS
│ │ cmsis_armcc.h
│ │ cmsis_compiler.h
│ │ cmsis_version.h
│ │ core_cm4.h
│ │ mpu_armv7.h
│ │
│ └─stm32l4xx
│ stm32l475xx.h
│ stm32l4xx.h
│ system_stm32l4xx.h
│
├─CORE
│ startup_stm32l475xx.s
│
├─OBJ
└─USER
│ main.c
│ test03.uvprojx
除了main.c外, 其他文件均拷贝自ST官网的Cube固件包stm32cubel4-v1-18-2.zip
如下是自己编写的main.c文件
c
#include "stm32l4xx.h"
void system_clock_init(void) {
/* 1. 使能 HSE 并等待稳定 */
RCC->CR |= RCC_CR_HSEON; // 使能HSE
while (!(RCC->CR & RCC_CR_HSERDY)) { } // 等待HSE稳定
/* 2. 配置 PLL: HSE=8MHz -> VCO=160MHz -> PLL_R=80MHz */
// 重要:STM32L4 的 PLL_R 输出必须 ÷2(因为 R=1 不允许用于 SYSCLK)
// 正确配置:M=1, N=20, R=2 → (8/1)*20/2 = 80 MHz
RCC->PLLCFGR =
(3U << RCC_PLLCFGR_PLLSRC_Pos) | // 11: HSE
(0U << RCC_PLLCFGR_PLLM_Pos) | // M = 1
(20U << RCC_PLLCFGR_PLLN_Pos) | // N = 20 (must be 8~86)
(0U << RCC_PLLCFGR_PLLR_Pos) | // R = 2 (for SYSCLK)
RCC_PLLCFGR_PLLREN; // Enable PLL R output
/* 3. 使能 PLL 并等待锁定 */
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY)){}
/* 4. 配置 AHB/APB 分频器 (all /1) */
RCC->CFGR =
(0 << RCC_CFGR_HPRE_Pos) | // AHB = HCLK /1
(0 << RCC_CFGR_PPRE1_Pos) | // APB1 = PCLK1 /1
(0 << RCC_CFGR_PPRE2_Pos); // APB2 = PCLK2 /1
/* 4.5 配置 Flash 等待周期(80MHz 必须设置,否则会 HardFault) */
// 80MHz 对应 4 WS (Wait States)
FLASH->ACR &= ~FLASH_ACR_LATENCY; // 先置零
FLASH->ACR |= FLASH_ACR_LATENCY_4WS; // 再设置等待周期为4
/* 5. 切换 SYSCLK 到 PLL */
RCC->CFGR |= RCC_CFGR_SW_PLL; // SW = 10 (PLL selected)
while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL){}
}
void dwt_init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT
DWT->CYCCNT = 0; // 清零计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT
}
void SystemInit(void)
{
/* 开启 FPU 访问权限 (CP10, CP11) */
SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2));
/* 可以在这里重置 RCC 为默认状态,防止旧状态干扰 */
RCC->CR |= RCC_CR_MSION;
}
int main(void)
{
system_clock_init();
dwt_init();
while (1) {
__NOP();
}
}
代码解释及效果验证

通过汇编启动文件可知, STM32启动的顺序
1. Reset_Handler, 复位后首先运行这个函数
2. SystemInit 汇编第一件事就是跳转到系统初始化函数中
3. __main, 这个是C库的初始化函数, 负责初始化全局变量, 堆栈管理等
4. main函数
然后我们在SystemInit中进行了FPU初始化的操作
void SystemInit(void)
{
/* 开启 FPU 访问权限 (CP10, CP11) */
SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2));
/* 可以在这里重置 RCC 为默认状态,防止旧状态干扰 */
RCC->CR |= RCC_CR_MSION;
}
因为在进入main函数之后, 就要直接用到浮点数运算了. 哪怕声明了一个float变量, 如果FPU没有开启, 就会触发HardFault导致直接崩溃. 当我们调试的时候, 仿真器是默认FPU开启了的

所以如果在程序中不初始化FPU, 会导致断点调试时跑崩掉.
然后根据STM32L4 编程手册(STM32L475VE Programming manual.pdf)中如下描述的方式开启FPU

验证是否开启FPU

根据上图描述, CPACR寄存器地址为0xE000ED88, 观察这个地址对应的值, 即可确定FPU已经正确开启

验证HSE成功启动且准备就绪
RCC->CR |= RCC_CR_HSEON; // 使能HSE
while (!(RCC->CR & RCC_CR_HSERDY)) { } // 等待HSE稳定
通过调试, 执行到while之后, 右侧HSERDY和HSEON都是选中状态, 代表外部晶振起振且工作稳定

验证主时钟PLL倍频分频设置是否正确
RCC->PLLCFGR =
(3U << RCC_PLLCFGR_PLLSRC_Pos) | // 11: HSE
(0U << RCC_PLLCFGR_PLLM_Pos) | // M = 1
(20U << RCC_PLLCFGR_PLLN_Pos) | // N = 20 (must be 8~86)
(0U << RCC_PLLCFGR_PLLR_Pos) | // R = 2 (for SYSCLK)
RCC_PLLCFGR_PLLREN; // Enable PLL R output
这里设置PLL时钟源, 并且设置M, N, R分频倍频参数
它们的关系式为: HSE ÷ M x N ÷ R = 结果频率
我们这里要设置的是: M=1, N=20, R=2 得到 8MHz ÷ 1 x 20 ÷ 2 = 80MHz
- 关于PLLSRC, 就是选择PLL的时钟源, 手册里内容如下:

二进制的11, 也就是十进制的3, 代表HSE外部高速晶振, 我们的HSE是8MHz的外部晶振 - PLLM

设置为000, 代表M=1 - PLLN

这里是说, PLLN只能设置8-86之间的数值, 我们这里直接设置N=20即可 - PLLR

设置值为00代表R=2
以上都设置成功后, 调试验证结果如下:

使能PLL并验证寄存器变化

手册中规定, 在PLL开启(PLLON=1)的情况下, 禁止修改RCC_PLLCFGR寄存器
如果先开启PLLON再设置M,N,R, 会导致硬件在参数写到一半的时候尝试锁定频率, 最终PLL输出的频率极不稳定
显式配置AHB和APB预分频器
RCC->CFGR =
(0 << RCC_CFGR_HPRE_Pos) | // AHB = HCLK /1
(0 << RCC_CFGR_PPRE1_Pos) | // APB1 = PCLK1 /1
(0 << RCC_CFGR_PPRE2_Pos); // APB2 = PCLK2 /1


AHB总线时钟被称为HCLK, APB总线被称为PCLK
AHB对应的输入源是SYSCLK, PCLK则由AHB分频得到
为了追求性能最大化, 所以AHB和APB初始状态都不分频, 都等于系统主时钟80MHz
根据手册, HPRE, PPRE寄存器都配置为0代表不分频, 配置后调试结果如图所示:

配置FLASH等待周期
当系统时钟配置为80MHz时, Flash存储器的频率跟不上, 就会在CPU切到80MHz后立即崩溃, 产生HardFault错误
当CPU去Flash取指令时, Flash的响应速度没有那么快, 所以在硬件上就设置了一个LATENCY延时机制
CPU能够在80MHz的频率取指令, 但Flash只能运行在16MHz-20MHz的频率下
相关寄存器手册内容如下

为了解决这个问题,硬件设计了一个"延时器",也就是 LATENCY:
000 (0 WS):HCLK ≤ 16MHz。CPU 招之即来,Flash 瞬间能给。
001 (1 WS):16MHz < HCLK ≤ 32MHz。CPU 跑得稍快,请等 1 个周期。
010 (2 WS):32MHz < HCLK ≤ 48MHz。请等 2 个周期。
011 (3 WS):48MHz < HCLK ≤ 64MHz。请等 3 个周期。
100 (4 WS):64MHz < HCLK ≤ 80MHz。必须等待 4 个周期。
设置后调试验证设置结果:

完成所有配置后, 切换SYSCLK到PLL
到这一步, 切换完成后, 整个板子才会工作在80MHz的频率下
RCC->CFGR |= RCC_CFGR_SW_PLL; // SW = 10 (PLL selected)
while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL){}

根据手册介绍, SYSCLK默认运行在4MHz下, 当发生SYSCLK时钟源切换, 只有在新的时钟源状态稳定, 在RCC_ICSCR寄存器中置位时, 才会将系统时钟源切换为新的时钟源.


SW 和 SWS 的区别
在 RCC->CFGR 寄存器中,关于系统时钟切换有两个非常相似的位域,但功能完全不同:
SW (System clock switch):位于第 [1:0] 位。这是由软件写入的。写 10(即 RCC_CFGR_SW_PLL),代表"命令"硬件切换到 PLL。
SWS (System clock switch status):位于第 [3:2] 位。这是由硬件写入的(只读)。它反映了当前系统真正运行在哪个时钟源上。
代码第二行的解释
RCC->CFGR & RCC_CFGR_SWS_Msk:使用屏蔽位(Mask)把 CFGR 寄存器的第 [3:2] 位抠出来,不看其他位。
RCC_CFGR_SWS_PLL:根据手册,当 SWS 位等于 10(二进制)时,代表 PLL 已成为系统时钟源。
循环逻辑:如果 SWS 还不等于 10,说明硬件还在路上,CPU 就在这里空转等待。一旦相等,说明"握手成功"
调试验证结果如下:

通过调试我们可以看到在CFGR寄存器的SW写入0x03成功, 0x03也就是二进制的11说名设置系统时钟为PLL成功, 然后在SWS读取出来的值也是0x03, 说明硬件读出的SYSCLK的时钟源也已经是PLL.
这里证明SYSCLK时钟源切换成功.
下一步就是证明当前CPU运行频率是多少. 我们采用DWT CYCCNT计数法. 每经历一个CPU周期, 这个计数器就会加一, 也就是说, 如果CPU是按照80MHz的速度在运行, 那么1秒钟, 这个计数器的值就应该是80M.
DWT(Data watchpoint trigger)是Cortex-M4内置的用来观测数据的寄存器

我们采用如下代码初始化DWT
void dwt_init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT
DWT->CYCCNT = 0; // 清零计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT
}
然后在调用dwt_init()所在行打断点, 然后进入调试模式.
dwt_init()执行完之后, CYCCNT计数器清零.
通过文档Cortex-M4 Technical Reference Manua.pdf 可知DWT->CYCCNT计数器的寄存器地址为0xE0001004

所以我们在memory1中观察该数值累计即可. 在测试结果图中, 我掐了10秒, 得出结果约804M, 证明此时CPU已经运行在80MHz的频率上了.
还有就是通过左上角的Run和Stop按钮来控制计数的起始.
至此, 我们的时钟初始化完成, 且每一步都得到有效验证.