STM32L475基于完全空白的项目,完成时钟树初始化配置并验证

初始化时钟包含两个层面
  1. SYSCLK 系统主时钟, 这个时钟决定着整个系统, 总线的运行速度
  2. 外设时钟使能, 这些时钟只有在外设需要工作时才激活, 平时为了低功耗不激活

所以时钟的初始化就遵循这样的原则:

系统主时钟在main执行前期就配置好, 其余时钟按需开启

时钟源的种类简写及含义
复制代码
H: high        高
L: low         低
S: speed       速度
I: internal    内部的
E: external    外部的
M: Multi       多速的

HSE: 高速外部时钟源
HSI: 高速内部时钟源
MSI: 多速内部时钟源
LSE: 低速外部时钟源
LSI: 低速内部时钟源
分析芯片手册了解时钟源

通过阅读芯片手册, 通过如下内容

可以知道, 当时钟没有初始化时, 芯片的默认运行频率是4MHz, 这个时钟就是系统主时钟

系统主时钟决定着芯片运行能力的上限, 所以第一步要将系统主时钟设置为芯片手册所规定的上限频率, 即80MHz.

确定系统时钟源的来源

通过这段描述可知, 系统主时钟可以通过4种时钟源获得

  1. 通过高速外部晶振(HSE)提供脉冲作为主时钟源
  2. 通过内部RC振荡器(HSI)产生的16Mhz脉冲作为时钟源
  3. 通过内部多速RC振荡器(MSI)提供的脉冲作为主时钟源
  4. 通过由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

  1. 关于PLLSRC, 就是选择PLL的时钟源, 手册里内容如下:

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

    设置为000, 代表M=1
  3. PLLN

    这里是说, PLLN只能设置8-86之间的数值, 我们这里直接设置N=20即可
  4. 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按钮来控制计数的起始.

至此, 我们的时钟初始化完成, 且每一步都得到有效验证.

相关推荐
XINVRY-FPGA2 小时前
XC7VX690T-2FFG1761I Xilinx AMD FPGA Virtex-7
arm开发·嵌入式硬件·fpga开发·硬件工程·fpga
良许Linux3 小时前
STM32F103每个符号的意思是什么?
stm32·单片机·嵌入式硬件
小痞同学3 小时前
【铁头山羊STM32】HAL库 4.时钟系统部分
stm32·单片机·嵌入式硬件
周周记笔记4 小时前
ESP32-S3 :开发方式笔记(五)
笔记·单片机·嵌入式硬件
我怕是好4 小时前
学习STM32 ESP8266
stm32·嵌入式硬件·学习
LS_learner4 小时前
Ubuntu启动盘制作
嵌入式硬件
S火星人S5 小时前
软件调试基础(四【断点和单步执行】4.3【陷阱标志】)
stm32·单片机·嵌入式硬件
NEWEVA__zzera225 小时前
AM32开源项目固件解析(STM32G071)
stm32·单片机·嵌入式硬件
Hello_Embed6 小时前
RS485 双串口通信 + LCD 实时显示(中断版)
c语言·笔记·单片机·学习·操作系统·嵌入式