LV16-16-STM32时钟系统

本文主要是STM32开发——时钟系统的一些相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
  • STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

一、STM32时钟树概述

1. 时钟系统

时钟系统是由振荡器(信号源)、定时唤醒器、分频器等组成的电路。常用的信号源有晶体振荡器和RC振荡器。

时钟是嵌入式系统的脉搏,处理器内核在时钟驱动下完成指令执行,状态变换等动作,外设部件在时钟的驱动下完成各种工作,比如串口数据的发送、A/D转换、定时器计数等等。因此时钟对于计算机系统是至关重要的,通常时钟系统出现问题也是致命的,比如振荡器不起振、振荡不稳、停振等。

image-20230502201120397

2. 多时钟源?

时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而喻了。 STM32F103的时钟系统比较复杂,不像简单的51单片机一个系统时钟就可以解决一切。那么采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 首先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。

在 [STM32中文参考手册](https://www.stmcu.com.cn/Designresource/detail/localization_document /710001) 的 6.2 时钟 这一节中详细介绍了STM32的时钟系统以及各个相关寄存器。

二、时钟树分析

1. 整体框图

[STM32中文参考手册](https://www.stmcu.com.cn/Designresource/detail/localization_document /710001) 的 6.2 时钟 这一节的 图8 时钟树:

image-20230502195624949

在 STM32 中,有五个时钟源,为 HSI、 HSE、 LSI、 LSE、 PLL。 从时钟频率来分可以分为高速时钟源和低速时钟源,在这 5 个中 HIS, HSE 以及 PLL 是高速时钟, LSI 和 LSE 是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。

2. 序号编号的时钟

我们先来分析序号①⑨(①⑥属于是分析各个时钟源的去向,⑦~⑨是在分析各总线时钟的来源):

① 、HSI 是高速内部时钟, RC 振荡器, 频率为 8MHz ,我们使用STM32CubeMX来创建工程时若未选择时钟,那么默认使用的就是这个。

②、 HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,可以由有源晶振或者无源晶振提供。频率范围为 4MHz~16MHz。我使用的战舰V3的开发板接的是 8M 的晶振。当使用有源晶振时, 时钟从OSC_IN引脚进入,OSC_OUT引脚悬空,当选用无源晶振时,时钟从OSC_IN和OSC_OUT进入,并且要配谐振电容。HSE最常使用的就是8M的无源晶振。当确定PLL时钟来源的时候,HSE可以不分频或者2分频, 这个由时钟配置寄存器CFGR的位17:PLLXTPRE设置,我们一般设置为HSE不分频。

③、LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体, 这个主要是 RTC 的时钟源。

④、LSI 是低速内部时钟, RC 振荡器,频率为 40kHz。 独立看门狗的时钟源只能是 LSI,同时 LSI 还可以作为 RTC 的时钟源。

⑤、PLL 为锁相环倍频输出,其时钟输入源可选择为 HSI/2、 HSE 或者 HSE/2。倍频可选择为2~16 倍,但是其输出频率最大不得超过 72MHz。 从这里经过倍频后输出的时钟为PLLCLK。具体用哪个由时钟配置寄存器CFGR的位16:PLLSRC设置。 HSI是内部高速的时钟信号,频率为8M,根据温度和环境的情况频率会有漂移,一般不作为PLL的时钟来源。这里我们选HSE作为PLL的时钟来源。通过设置PLL的倍频因子,可以对PLL的时钟来源进行倍频,倍频因子可以是:[2, 3, … ,16], 具体设置成多少,由时钟配置寄存器CFGR的位21-18:PLLMUL[3:0]设置。我们这里一般设置为9倍频, 若上一步我们设置PLL的时钟来源为HSE=8M,那么经过PLL倍频之后的PLL时钟:PLLCLK = 8M * 9 = 72M。 72M是ST官方推荐的稳定运行时钟,如果想超频的话,增大倍频因子即可,最高为128M。 我们这里一般设置PLL时钟:PLLCLK = 8M * 9 = 72M。

⑥、SYSCLK表示系统时钟,从图中可以看出,系统时钟最高为72MHz, 从图中左侧的选择器SW可以看到来源有三个, 分别是:①内部高速时钟HSI(绿色) 、 ⑤锁相环时钟PLLCLK(紫色)和②外部高速时钟HSE(浅红) ,而锁相环时钟PLLCLK由内部高速时钟HSI或者外部高速时钟HSE, 经过分频和PLL锁相环倍频而来 。内部高速时钟HSI可直接经过选择器SW给系统时钟SYSCLK,此时系统时钟SYSCLK为8MHz; 内部高速时钟HSI先2分频,再经过选择器PLLSRC进入锁相环PLLMUL,最大倍频为16倍,得到64MHz的锁相环时钟PLLCLK给系统时钟SYSCLK。 当外部高速时钟HSE(假设外接晶振为8MHz时) 直接给选择器SW,则系统时钟SYSCLK为8MHz; 当外部高速时钟HSE(假设外接晶振为8MHz时) 直接经过选择器PLLXTPRE给PLLSRC,再经过PLLMUL 9倍频,得到72MHz的PLLCLK给系统时钟SYSCLK。具体的时钟配置寄存器CFGR的位1-0:SW[1:0]设置。 我们这里一般设置系统时钟:SYSCLK = PLLCLK = 72M。

⑦、AHB,高速接口总线 ,由⑥SYSCLK系统时钟分频得到,最高是系统时钟的72MHz。系统时钟SYSCLK经过AHB预分频器分频之后得到时钟叫APB总线时钟,即HCLK,分频因子可以是:[1, 2, 4,…,512], 具体的由时钟配置寄存器CFGR的位7-4 :HPRE[3:0]设置。片上大部分外设的时钟都是经过HCLK分频得到, 至于AHB总线上的外设的时钟设置为多少,得等到我们使用该外设的时候才设置, 我们这里一般只需要设置好APB的时钟即可。我们这里一般是设置为1分频,即HCLK=SYSCLK=72M。

⑧、APB1,外设总线1,来源于⑦高速接口总线AHB, APB1经过预分频器后输出,最终的输出时钟最高是36MHz 。APB1总线时钟PCLK1由HCLK经过低速APB预分频器得到,分频因子可以是:[1,2,4,8,16],具体的由时钟配置寄存器CFGR的位10-8:PRRE1[2:0]决定。 PLCK1属于低速的总线时钟,最高为36M,片上低速的外设就挂载到这条总线上,比如USART2/3/4/5、SPI2/3,I2C1/2等。 至于APB1总线上的外设的时钟设置为多少,得等到我们使用该外设的时候才设置,我们这里一般就设置好APB1的时钟即可。 我们这里一般设置为2分频,即PCLK1 = HCLK/2 = 36M。

⑨、APB2,外设总线2,来源于⑦高速接口总线AHB, APB1经过预分频器后输出,最终的输出时钟最高是72MHz 。APB2总线时钟PCLK2由HCLK经过高速APB2预分频器得到,分频因子可以是:[1,2,4,8,16],具体由时钟配置寄存器CFGR的位13-11:PPRE2[2:0]决定。 PLCK2属于高速的总线时钟,片上高速的外设就挂载到这条总线上,比如全部的GPIO、USART1、SPI1等。至于APB2总线上的外设的时钟设置为多少, 得等到我们使用该外设的时候才设置,我们这里一般就设置好APB2的时钟即可。我们这里一般设置为1分频,即PCLK2 = HCLK = 72M。

3. 字母编号的时钟

接着我们再来看外设的时钟,字母A~G:

A、MCO 是 STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出, 可以选择为 PLL 输出的 2 分频、 HSI、 HSE、或者系统时钟。这个时钟可以用来给外部其他系统提供时钟源。MCO是microcontroller clock output的缩写,是微控制器时钟输出引脚,在STM32 F1系列中 由 PA8复用所得, 主要作用是可以对外提供时钟,相当于一个有源晶振。MCO的时钟来源可以是:PLLCLK/2、HSI、HSE、SYSCLK, 具体选哪个由时钟配置寄存器CFGR的位26-24:MCO[2:0]决定。除了对外提供时钟这个作用之外, 我们还可以通过示波器监控MCO引脚的时钟输出来验证我们的系统时钟配置是否正确。

B、 从图中可以看出 B 处 USB 的时钟是来自 PLL 时钟源。 STM32 中有一个全速功能的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时钟源只能从 PLL 输出端获取(也就是只能使用PLLCLK),可以选择为 1.5 分频或者 1 分频,也就是,当需要使用 USB模块时, PLL 必须使能,并且时钟频率配置为 48MHz 或 72MHz。USB时钟是由PLLCLK经过USB预分频器得到,分频因子可以是:[1,1.5],具体的由时钟配置寄存器CFGR的位22:USBPRE配置。 USB的时钟最高是48M,根据分频因子反推过来算,PLLCLK只能是48M或者是72M。一般我们设置PLLCLK=72M,USBCLK=48M。 USB对时钟要求比较高,所以PLLCLK只能是由HSE倍频得到,不能使用HSI倍频。

C、这里的 C 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最终来源都是 SYSCLK。 SYSCLK 通过 AHB 分频器分频后送给各模块使用。这些模块包括:

(1)AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。

(2)通过 8 分频后送给 Cortex 的系统定时器时钟,也就是 systick 了。

(3)直接送给 Cortex 的空闲运行时钟 FCLK。

(4)送给 APB1 分频器。 APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大频率 36MHz),另一路送给定时器(Timer)2、 3、 4 倍频器使用。

(5)送给 APB2 分频器。 APB2 分频器分频输出一路供 APB2 外设使用(PCLK2,最大频率 72MHz),另一路送给定时器(Timer)1 倍频器使用。

D、这里是 RTC 时钟源,从图上可以看出, RTC 的时钟源可以选择 LSI, LSE,以及HSE 的 128 分频。RTC时钟可由HSE/128分频得到,也可由低速外部时钟信号LSE提供,频率为32.768KHZ,也可由低速内部时钟信号LSI提供, 具体选用哪个时钟由备份域控制寄存器BDCR的位9-8:RTCSEL[1:0]配置。

E、WDGCLK(独立看门狗) 的时钟,它来源于④内部低速时钟LSI的40kHz(橙色) 。独立看门狗的时钟由LSI提供, 且只能是由LSI提供,LSI是低速的内部时钟信号,频率为30~60KHZ直接不等,一般取40KHZ。

F、这里其实也算是外设的时钟了,但是这里还是单独写详细一点,毕竟是笔记吗。Cortex系统时钟由HCLK 8分频得到,等于9M, Cortex系统时钟用来驱动内核的系统定时器SysTick,SysTick一般用于操作系统的时钟节拍,也可以用做普通的定时。

G、ADC时钟由PCLK2经过ADC预分频器得到,分频因子可以是[2,4,6,8],具体的由时钟配置寄存器CFGR的位15-14:ADCPRE[1:0]决定。 很奇怪的是怎么没有1分频。ADC时钟最高只能是14M,如果采样周期设置成最短的1.5个周期的话,ADC的转换时间可以达到最短的1us。 如果真要达到最短的转换时间1us的话,那ADC的时钟就得是14M,反推PCLK2的时钟只能是:28M、56M、84M、112M, 鉴于PCLK2最高是72M,所以只能取28M和56M。

4. APB1与APB2

我们需要注意的是 APB1 上面连接的是低速外设,包括电源接口、备份接口、 CAN、 USB、 I2C1、 I2C2、 UART2、 UART3 等等, APB2 上面连接的是高速外设包括 UART1、 SPI1、 Timer1、 ADC1、 ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。居宁老师的《稀里糊涂玩 STM32》资料里面教大家的记忆方法是 2>1, APB2 下面所挂的外设的时钟要比 APB1 的高。

三、常见振荡器

1. 基本概念

振荡器是用来产生重复电子讯号的电子元件。其构成的电路叫振荡电路,能将直流电转换为具有一定频率交流信号输出的电子电路或装置。

振荡器主要分为RC,LC振荡器和晶体振荡器。RC振荡器是采用RC网络作为选频移相网络的振荡器。LC振荡器是采用LC振荡回路作为移相和选频网络的正反馈振荡器。晶体振荡器的振荡频率受石英晶体控制。

2. RC振荡器

RC振荡器是又电阻电容构成的振荡电路,能将直流电转换为具有一定频率交流信号输出的电子电路或装置。

image-20230502201226079

优点:实现的成本比较低,毕竟就是一个电阻电容。

缺点:是由于电阻电容的精度问题所以RC振荡器的震荡频率会有误差,同时受到温度、湿度的影响。

3. 晶体振荡器

石英晶体振荡器,就是我们平时说的晶振,是高精度和高稳定度的振荡器,被广泛应用于彩电、计算机、遥控器等各类振荡电路中,以及通信系统中用于频率发生器、为数据处理设备产生时钟信号和为特定系统提供基准信号

image-20230502201319447

优点:是相对来说震荡频率一般都比较稳定,同时精度也较高。

缺点:就是价格要稍微高点了,还有用晶体振荡器一般还需要接两个15-33pF起振电容。

四、时钟的配置

1. STM32CubeMX配置

我感觉用这个配置时钟是真的很方便,就是图形化的,选好后还可以检测是否合法

1.1 默认情况

image-20230502201650108

1.2 开启外部高速时钟

  • 首先我们要开启外部的高速时钟HSE:【RCC】→【HSE】→【Crystal/Veramic Resonator】
image-20230502202136660
  • 配置时钟
image-20230502202501255

这里我们将SYSCLK配置为72M,默认情况下APB1的分频是1,所以APB1的时钟就达到了72M,而APB1时钟最高位36M,显然超了,所以软件会标红报错,我们修改分频因子后降为36M就可以了。

1.3 HAL时钟初始化函数

我们通过图形化界面配置时钟,,在导出的工程中,会通过下边两个函数进行时钟配置,工程中自动调用,我们不需要去修改:

1
2
3
4
void SystemInit (void);//System_stm32f0xx.c中定义,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数
//进行系统一些初始化配置,复位 RCC 时钟配置为默认复位值(默认开启 HSI)

void SystemClock_Config(void);// 在main.c中定义,实现时钟的具体配置,配置PLL, 配置AHB和HPB的时钟

2. 库函数分析

上边我们直接通过图形界面配置了,很直观,接下来来了解一下HAL库函数实际是怎么操作的吧。

2.1 SystemInit()

  • HAL库中的 SystemInit
1
2
3
4
5
6
7
8
9
10
11
12
13
void SystemInit (void)
{
#if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) || defined(STM32F103xE) || defined(STM32F103xG)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif

/* Configure the Vector Table location -------------------------------------*/
#if defined(USER_VECT_TAB_ADDRESS)
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#endif /* USER_VECT_TAB_ADDRESS */
}

看完之后,似乎在里边什么也没做,但是标准库中的这个函数倒是进行了不少的配置。

  • 标准库中的SystemInit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void SystemInit (void)
{
/* 将 RCC 时钟配置重置为默认重置状态(用于调试)*/
RCC->CR |= (uint32_t)0x00000001; //打开 HSION 位

/* 设置 SW, HPRE, PPRE1, PPRE2, ADCPRE 和 MCO 位 */
#ifndef STM32F10X_CL
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */

RCC->CR &= (uint32_t)0xFEF6FFFF; // 复位 HSEON, CSSON 和 PLLON 位
RCC->CR &= (uint32_t)0xFFFBFFFF; // 复位 HSEBYP 位
RCC->CFGR &= (uint32_t)0xFF80FFFF;// 复位 CFGR 寄存器

#ifdef STM32F10X_CL
RCC->CR &= (uint32_t)0xEBFFFFFF; // 复位 PLL2ON 和 PLL3ON 位
RCC->CIR = 0x00FF0000; // 禁用所有中断并清除挂起位
RCC->CFGR2 = 0x00000000; // 重置 CFGR2 注册
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
RCC->CIR = 0x009F0000; // 禁用所有中断并清除挂起位
RCC->CFGR2 = 0x00000000; // 重置 CFGR2 注册
#else
RCC->CIR = 0x009F0000; // 禁用所有中断并清除挂起位
#endif /* STM32F10X_CL */

#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif

/* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
/* Configure the Flash Latency cycles and enable prefetch buffer */
SetSysClock();
/* 配置中断向量表地址=基地址+偏移地址 ------------------*/
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; // 内部 SRAM 中的向量表重定位
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; // 在内部 FLASH 中的向量表重定位
#endif
}

从上面代码可以看出, SystemInit 主要做了如下三个方面工作:

(1)复位 RCC 时钟配置为默认复位值(默认开始了 HIS)

(2)外部存储器配置

(3)中断向量表地址配置

2.2 SystemClock_Config()

上边HAL库中的 SystemInit 虽然什么也没做,但是生成的代码在主函数还会调用这样一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 时钟源为 HSE
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 打开 HSE
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; // HSE 预分频
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开 PLL
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // PLL 时钟源选择 HSE
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 主 PLL 倍频因子
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) // 初始化
{
Error_Handler();
}

// 选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 设置系统时钟时钟源为 PLL
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB 分频系数为 1
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1 分频系数为 2
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2 分频系数为 1
// 配置时钟,同时设置 FLASH 延时周期为 2WS,也就是 3 个 CPU 周期。
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}

从函数注释可知,函数 Stm32_Clock_Init 的作用是进行时钟系统配置,除了配置 PLL 相关参数确定 SYSCLK 值之外,还配置了 AHB,APB1 和 APB2 的分频系数,也就是确定了 HCLK,PCLK1 和 PCLK2 的时钟值。j接下来我们来详细看一看其中的某些部分。

1
2
3
4
5
6
7
8
9
10
11
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 时钟源为 HSE
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 打开 HSE
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; // HSE 预分频
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开 PLL
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // PLL 时钟源选择 HSE
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 主 PLL 倍频因子
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) // 初始化
{
Error_Handler();
}

通过该段函数,我们开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE,然后把Stm32_Clock_Init 的唯一的入口参数直接设置作为 PLL 的倍频因子。设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率,接下来我们就需要设置系统时钟,以及 AHB, APB1 和APB2 相关参数。

1
2
3
4
5
// 配置时钟,同时设置 FLASH 延时周期为 2WS,也就是 3 个 CPU 周期。
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}

HAL_RCC_ClockConfig函数有两个入口参数,第一个入口参数 RCC_ClkInitStruct 是结构体 RCC_ClkInitTypeDef指针类型,用来设置 SYSCLK 时钟源、AHB,APB1 和 APB2 的分频系数。第二个入口参数FLatency 用来设置 FLASH 延迟,这个参数我们后边再说。

RCC_ClkInitTypeDef 结构体类型定义非常简单,这里我们就不列出来,我们来看看Stm32_Clock_Init 函数中的配置内容:

1
2
3
4
5
6
7
// 选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 设置系统时钟时钟源为 PLL
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB 分频系数为 1
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1 分频系数为 2
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2 分频系数为 1

(1)第一个参数 ClockType 配置说明我们要配置的是 SYSCLK, HCLK,PCLK1 和 PCLK2 四个时钟。

(2)第二个参数 SYSCLKSource 配置选择系统时钟源为 PLL。

(3)第三个参数 AHBCLKDivider 配置 AHB 分频系数为 1。

(4)第四个参数 APB1CLKDivider 配置 APB1 分频系数为 2。

(5)第五个参数 APB2CLKDivider 配置 APB2 分频系数为 1。

这样我们可以计算出, PLL 时钟为 PLLCLK=HSE*9 =8MHz*9=72MHz,同时我们选择系统时钟源为PLL , 所 以 系 统 时 钟 SYSCLK=72MHz 。 AHB 分 频 系 数 为 1 , 故 其 频 率 为HCLK=SYSCLK/1=72MHz。 APB1 分频系数为 2,故其频率为 PCLK1=HCLK/2=36MHz。 APB2分频系数为 1,故其频率为PCLK2=HCLK/1=72/1=72MHz。最后我们总结一下通过调用函数SystemClock_Config()之后的关键时钟频率值:

1
2
3
4
5
SYSCLK(系统时钟)             = 72MHz
PLL 主时钟 = 72MHz
AHB 总线时钟(HCLK=SYSCLK/1) = 72MHz
APB1 总线时钟(PCLK1=HCLK/2) = 36MHz
APB2 总线时钟(PCLK2=HCLK/1) = 72MHz

可以看到这与我们前边在图形界面中配置的是一致的。

2.3 RCC_OscInitTypeDef

接下来我们看看结构体 RCC_OscInitTypeDef 的定义,由于我使用的是STM32F103ZET6,所以我直接把一些没有用的宏删掉了:

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
uint32_t OscillatorType; // 需要选择配置的振荡器类型*/
uint32_t HSEState; // HSE 状态
uint32_t HSEPredivValue; // Prediv1 值
uint32_t LSEState; // LSE 状态/
uint32_t HSIState; // HSI 状态
uint32_t HSICalibrationValue; // HIS 校准值
uint32_t LSIState; // LSI 状态/
RCC_PLLInitTypeDef PLL; // PLL 配置
} RCC_OscInitTypeDef;

对于这个结构体,前面几个参数主要是用来选择配置的振荡器类型。比如我们要开启 HSE,那么我们会设置 OscillatorType 的值为RCC_OSCILLATORTYPE_HSE,然后设置 HSEState 的值为 RCC_HSE_ON 开启 HSE。对于其他时钟源 HSI,LSI 和 LSE,配置方法类似。这个结构体还有一个很重要的成员变量是 PLL,它是结构体 RCC_PLLInitTypeDef 类型。它的作用是配置 PLL相关参数,我们来看看它的定义 :

1
2
3
4
5
6
typedef struct
{
uint32_t PLLState; // PLL 状态
uint32_t PLLSource; // PLL 时钟源
uint32_t PLLMUL; // PLL VCO 输入时钟的乘法因子r */
} RCC_PLLInitTypeDef;

从 RCC_PLLInitTypeDef 结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及相关分频倍频参数。

3. 时钟使能与配置

在配置好时钟系统之后,如果我们要使用某些外设,例如 GPIO, ADC 等,我们还要使能这些外设时钟。这里必须注意,如果在使用外设之前没有使能外设时钟,这个外设是不可能正常运行的。

STM32 的外设时钟使能是在 RCC 相关寄存器中配置的。因为 RCC 相关寄存器非常多,我们可以参考[STM32中文参考手册](https://www.stmcu.com.cn/Designresource/detail/localization_document /710001) 的6.3 小节查看所有 RCC 相关寄存器的配置。接下来我们来看一看通过 STM32F1 的 HAL库使能外设时钟的方法。

3.1 使能GPIOA的时钟

在 STM32F1 的 HAL 库中,外设时钟使能操作都是在 RCC 相关固件库文件头文件 stm32f1xx_hal_rcc.h 定义的。我们打开 stm32f1xx_hal_rcc.h 头文件可以看到文件中除了少数几个函数声明之外大部分都是宏定义标识符。

外设时钟使能在 HAL 库中都是通过宏定义标识符来实现的。首先,我们来看看 GPIOA 的外设时钟使能宏定义标识符:

1
2
3
4
5
6
7
#define __HAL_RCC_GPIOA_CLK_ENABLE()   do { \
__IO uint32_t tmpreg; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
/* Delay after an RCC peripheral clock enabling */\
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
UNUSED(tmpreg); \
} while(0U)

这几行代码主要是定义了一个宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE(),它的核心操作是通过下面这行代码实现的:

1
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);

这行代码的作用是,设置寄存器 RCC_APB2ENR 的相关位为 1,至于是哪个位,是由宏定义标识符 RCC_APB2ENR_IOPAEN 的值决定的,而它的值为:

1
2
3
#define RCC_APB2ENR_IOPAEN_Pos               (2U)                              
#define RCC_APB2ENR_IOPAEN_Msk (0x1UL << RCC_APB2ENR_IOPAEN_Pos) /*!< 0x00000004 */
#define RCC_APB2ENR_IOPAEN RCC_APB2ENR_IOPAEN_Msk /*!< I/O port A clock enable */

所以,我们很容易理解上面代码的作用是设置寄存器 RCC->APB2ENR 寄存器的位 2 为 1。我们可以从 STM32F1 的中文参考手册中搜索 APB2ENR 寄存器定义,位 2 的作用是用来使用GPIOA 时钟。 APB2ENR 寄存器的位 2 描述如下:

image-20230502210134591

那么我们只需要在我们的用户程序中调用宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE()就可以实现 GPIOA 时钟使能。使用方法为:

1
__HAL_RCC_GPIOA_CLK_ENABLE();// 使能 GPIOA 时钟

对于其他外设,同样都是在 stm32f1xx_hal_rcc.h 头文件中定义,只需要找到相关宏定义标识符即可,这里我们列出几个常用使能外设时钟的宏定义标识符使用方法:

1
2
3
__HAL_RCC_DMA1_CLK_ENABLE();   // 使能 DMA1 时钟
__HAL_RCC_USART2_CLK_ENABLE(); // 使能串口 2 时钟
__HAL_RCC_TIM1_CLK_ENABLE(); // 使能 TIM1 时钟

3.2 禁止GPIOA的时钟

我们使用外设的时候需要使能外设时钟,如果我们不需要使用某个外设,同样我们可以禁止某个外设时钟。禁止外设时钟使用方法和使能外设时钟非常类似,同样是头文件中定义的宏定义标识符。我们同样以 GPIOA 为例,宏定义标识符为:

1
#define __HAL_RCC_GPIOA_CLK_DISABLE()     (RCC->APB2ENR &= ~(RCC_APB2ENR_IOPAEN))

同样,宏定义标识符__HAL_RCC_GPIOA_CLK_DISABLE()的作用是设置 RCC->APB2ENR 寄存器的位 2 为 0,也就是禁止 GPIOA 时钟。我们这里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:

1
2
3
__HAL_RCC_DMA1_CLK_DISABLE();  // 禁止 DMA1 时钟
__HAL_RCC_USART2_CLK_DISABLE();// 禁止串口 2 时钟
__HAL_RCC_TIM1_CLK_DISABLE(); // 禁止 TIM1 时钟

3.3 复用的端口时钟

STM32F1 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。这部分知识在[STM32中文参考手册](https://www.stmcu.com.cn/Designresource/detail/localization_document /710001) 的 P109, P116~P121 有详细的讲解哪些 GPIO 管脚是可以复用为哪些内置外设的。而且前边学习GPIO的时候也有点了解过这个概念。

我们知道 MCU 都有串口, STM32 有好几个串口。比如说 STM32F103ZET6 有 5 个串口,我们可以查手册知道,串口 1 的引脚对应的 IO 为 PA9,PA10。PA9, PA10 默认功能是 GPIO,所以当PA9,PA10 引脚作为串口 1 的 TX,RX 引脚使用的时候,那就是端口复用。

image-20230502210716367

接下来我们以串口 1 为例来看一看配置 GPOPA.9,GPIOA.10 口为串口 1 复用功能的一般步骤。

(1)首先,我们要使用 IO 复用功能,必须先打开对应的 IO 时钟和复用功能外设时钟,这里我们使用了 GPIOA 以及 USART1,所以我们需要使能 GPIOA 和 USART1 时钟。方法如下:

1
2
3
__HAL_RCC_GPIOA_CLK_ENABLE();  // 使能 GPIOA 时钟
__HAL_RCC_USART1_CLK_ENABLE(); // 使能 USART1 时钟
__HAL_RCC_AFIO_CLK_ENABLE(); // 使能辅助功能 IO 时钟

(2)然后,我们在 GIPOx_MODER 寄存器中将所需 IO(对于串口 1 是 PA9,PA10) 配置为复用功能。

(3)最后,我们还需要对 IO 口的其他参数,例如上拉/下拉以及输出速度等进行配置。上面三步,在我们 HAL 库中是通过 HAL_GPIO_Init 函数来实现的:

1
2
3
4
5
6
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_9; // PA9
GPIO_Initure.Mode=GPIO_MODE_AF_PP; // 复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; // 上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;// 高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure); // 初始化 PA9

通过上面的配置, PA9 复用为串口 1 的发送引脚。这个时候, PA9 将不再作为普通的 IO 口使用。对于 PA10,配置方法一样,修改 Pin 成员变量值为 PIN_10 即可。