1)实验平台:alientek 阿波罗 STM32F767 开发板
2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

第四章 STM32F7 基础知识入门
这一章,我们将着重 STM32 开发的一些基础知识,让大家对 STM32 开发有一个初步的了
解,为后面 STM32 的学习做一个铺垫,方便后面的学习。这一章的内容大家第一次看的时候
可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。这章我们分 7
个小结,
·4.1 MDK 下 C 语言基础复习
·4.2 STM32F7 系统架构
·4.3 STM32F767 时钟系统
·4.4 IO 引脚复用器和映射
·4.5 STM32F7 NVIC 中断优先级管理
·4.6 MDK 中寄存器地址名称映射分析
·4.7 MDK 固件库快速开发技巧
·4.8 手把手教你入门 STM32CubeMX 图形配置工具
4.1 MDK 下 C 语言基础复习
这一节我们主要讲解一下 C 语言基础知识。C 语言知识博大精深,也不是我们三言两语能
讲解清楚,同时我们相信学 STM32F7 这种级别 MCU 的用户,C 语言基础应该都是没问题的。我
们这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的
用户能够快速开发 STM32 程序。同时希望这些用户能够多去复习一下 C 语言基础知识,C 语言
毕竟是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略
不看。
4.1.1 位操作
C 语言位操作相信学过 C 语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级
别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面
我们先讲解几种位操作符,然后讲解位操作使用技巧。C 语言支持如下 6 种位操作

表 4.1.1 16 种位操作
这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信
大家学 C 语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作
符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。
1) 不改变其他位的值的状况下,对某几个位进行设值。
这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,
然后用|操作符设值。比如我要改变 GPIOA->ODR 的状态,可以先对寄存器的值进行&清零
操作
GPIOA->ODR &=0XFF0F; //将第 4-7 位清 0
然后再与需要设置的值进行|或运算
GPIOA->ODR |=0X0040;
//设置相应位的值,不改变其他位的值
2) 移位操作提高代码的可读性。
移位操作在单片机开发中也非常重要,我们来看看下面一行代码
GPIOA->ODR| = 1 << 5;
这个操作就是将 ODR 寄存器的第 5 位设置为 1,为什么要通过左移而不是直接设置一个
固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以很直观
明了的知道,是将第 5 位设置为 1,其他位的值不变。如果你写成
GPIOA->ODR =0x0020;
这样的代码可读性非常差同时也不好重用。
3) ~取反操作使用技巧
例如 GPIOA->ODR 寄存器的每一位都用来设置一个 IO 口的输出状态,某个时刻我们
希望去设置某一位的值为 0,同时其他位都为 1,简单的作法是直接给寄存器设置一个值:
GPIOA->ODR =0xFFF7;
这样的作法设置第 3 位为 0,但是这样的写法可读性很差。看看如果我们使用取反操作怎
么实现:
GPIOA->ODR= (uint16_t)~(1<<3);
看这行代码应该很容易明白,我们设置的是 ODR 寄存器的第 3 位为 0,其他位为 1,可读性
非常强。
4.1.2 define 宏定义
define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供
方便。常见的格式:
#define 标识符 字符串
“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:
#define HSI_VALUE ((uint32_t)16000000)
定义标识符 HSI_VALUE 的值为 16000000。这样我们就可以在代码中直接使用标识符
HSI_VALUE,而不用直接使用常量 16000000,同时也很方便我们修改 HSI_VALUE 的值。
至于 define 宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。
4.1.3# ifdef 和 #if defined 条件编译
单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而
当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译,
否则编译程序段 2。 其中#else 部分也可以没有,即:
#ifdef
程序段 1
#endif
这个条件编译在 MDK 里面是用得很多的,在 stm32f7xx_hal_conf.h 这个头文件中会看到这样的
语句:
#ifdef HAL_GPIO_MODULE_ENABLED
#include "stm32f7xx_hal_gpio.h"
#endif
这段代码的作用是判断宏定义标识符 HAL_GPIO_MODULE_ENABLED 是否被定义,如果被定
义了,那么就引入头文件 stm32f7xx_hal_gpio.h。
对于条件编译,还有个常用的格式,如下:
#if defined XXX1
程序段 1
#elif defined XXX2
程序段 2
…
#elif defined XXXn
程序段 n
…
#endif
这种写法的作用实际跟 ifdef 很相似,不同的是 ifdef 只能在两个选择中判断是否定义,
而 if defined 可以在多个选择中判断是否定义。
条件编译也是 c 语言的基础知识,这里就给大家讲解到这里,不懂的大家可以查看在网上
搜索相关资料学习。
4.1.4 extern 变量申明
C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示
编译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于 extern 申明变量可
以多次,但定义只有一次。在我们的代码中你会看到看到这样的语句:
extern u16 USART_RX_STA;
这个语句是申明 USART_RX_STA 变量在其他文件中已经定义了,在这里要使用到。所以,你肯定
可以找到在某个地方有变量定义的语句:
u16 USART_RX_STA;
的出现。下面通过一个例子说明一下使用方法。
在 Main.c 定义的全局变量 id,id 的初始化都是在 Main.c 里面进行的。
Main.c 文件
u8 id;//定义只允许一次
main()
{
id=1;
printf("d%",id);//id=1
test();
printf("d%",id);//id=2
}
但是我们希望在main.c的 changeId(void)函数中使用变量id,这个时候我们就需要在main.c
里面去申明变量 id 是外部定义的了,因为如果不申明,变量 id 的作用域是到不了 main.c 文件
中。看下面 main.c 中的代码:
extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行
void test(void){
id=2;
}
在 main.c 中申明变量 id 在外部定义,然后在 main.c 中就可以使用变量 id 了。
对于 extern 申明函数在外部定义的应用,这里我们就不多讲解了。
4.1.5 typedef 类型别名
typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。
typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。
struct _GPIO
{
__IO uint32_t MODER;
__IO uint32_t OTYPER;
…
};
定义了一个结构体 GPIO,这样我们定义变量的方式为:
struct _GPIO GPIOA;//定义结构体变量 GPIOA
但是这样很繁琐,MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别
名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量了。
方法如下:
typedef struct
{
__IO uint32_t MODER;
__IO uint32_t OTYPER;
…
} GPIO_TypeDef;
Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结构体
变量:
GPIO_TypeDef _GPIOA,_GPIOB;
这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 这样是不是方便很多?
4.1.6 结构体
经常很多用户提到,他们对结构体使用不是很熟悉,但是 MDK 中太多地方使用结构体以及
结构体指针,这让他们一下子摸不着头脑,学习 STM32 的积极性大大降低,其实结构体并不是
那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下一节的“寄存器
地址名称映射分析”中讲到一些。
声明结构体类型:
Struct 结构体名{
成员列表;
}变量名列表;
例如:
Struct G_TYPE {
uint32_t Pin;
uint32_t Mode;
uint32_t Speed;
}GPIOA,GPIOB;
在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:
Struct 结构体名字 结构体变量列表 ;
例如:struct G_TYPE GPIOA,GPIOB;
结构体成员变量的引用方法是:
结构体变量名字.成员名
比如要引用 GPIOA 的成员 Mode,方法是:GPIOA. Mode;
结构体指针变量定义也是一样的,跟其他变量没有啥区别。
例如:struct G_TYPE *GPIOC;//定义结构体指针变量 GPIOC;
结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 GPIOC 结构体指针指向的结
构体的成员变量 Speed,方法是:
GPIOC-> Speed;
上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,
有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实
例回答一下这个问题。
在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如 IO 口。它的初始化状态
是由几个属性来决定的,比如模式,速度等。对于这种情况,在我们没有学习结构体的时候,
我们一般的方法是:
void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed);
这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里
面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入上下拉 Pull 这个入口参
数。于是我们的定义被修改为:
void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed,uint32_t Pull);
但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函
数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?
这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下,
只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。
结构体就是将多个变量组合为一个有机的整体。上面的函数中 Pin, Mode,
Speed 和 Pull 这些参数,他们对于 GPIO 而言,是一个有机整体,都是来设置 IO 口参数的,所
以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的:
typedef struct
{
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
uint32_t Alternate;
}GPIO_InitTypeDef;
于是,我们在初始化 GPIO 口的时候入口参数就可以是 GPIO_InitTypeDef 类型的变量或者指针
变量了,MDK 中是这样做的:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需
要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义
就可以达到增加变量的目的。
理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,
如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可
以提高你的代码的可读性。
使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作
用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只
是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲
解结构体的一些其他知识。
4.2 STM32F7 总线架构
STM32F7 的总线架构比 51 单片机就要强大很多了。STM32F7 总线架构的知识可以在
《STM32F7XX 中文参考手册》第二章有讲解,这里我们也把这一部分知识抽取出来讲解,是
为了大家在学习 STM32F7 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参
考手册中参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需
要详细深入的了解 STM32F7 的系统架构,还需要多看看《STM32F7 中文参考手册》或者在网
上搜索其他相关资料学习。
首先我们看看 STM32F7 的总线架构图如下图 4.2.1 所示:

图 4.2.1 STM32F767 系统架构图
主系统架构基于 2 个子系统:
一个 AXI 转 multi-AHB 总线桥,用于将 AXI4 协议转换为 AHB-Lite 协议。
① 一个连接到内嵌 flash 的 AXI 转 64 位 AHB 总线桥
② 三个连接到 AHB 总线矩阵的 AXI 转 32 位 AHB 总线桥
一个 multi-AHB 总线矩阵
multi-AHB 总线矩阵将所有主控总线和被控总线互联,它包括:
① 32 位 multi-AHB 总线矩阵
② 64 位 multi-AHB 总线矩阵:它将来自 CPU 的 64 位 AHB 总线(通过 AXI 转 AHB 总线桥)
和来自 GP DMA 与外设 DMA(增至 64 位)的 32 位 AHB 总线连接到内部 flash。
multi_AHB 总线矩阵可连接 12 个总线主控制器和 8 个总线从控制器:
十二个总线主控制器
① 3x32 位 AHB 总线以及 64 位 Cortex-M7 AXI 主控总线通过 AXI-AHB 总线桥分为 4 个总线
控制器
② 连接到内嵌 flash 的 1x16 位 AHB 总线
③ Cortex-M7 AHB 外设总线
④ DMA1 存储器总线
⑤ DMA2 存储器总线
⑥ DMA2 外设总线
⑦ 以太网 DMA 总线
⑧ USB OTG HS DMA 总线
⑨ LCD 控制器 DMA 总线
⑩ Chrom-Art
加速器(DMA2D)存储器总线
八个总线从控制器
① AHB 总线上的内嵌 Flash(用于 Flash 读/写访问,代码执行和数据访问)
② Cortex-M7 AHBS 从接口(仅用于 DTCM RAM 的 DMA 数据传输)
③ 主 SRAM1(240KB)
④ 辅助 SRAM2(16KB)
⑤ AHB1 外设(包括 AHB-APB 总线桥和 APB 外设)
⑥ AHB2 外设(包括 AHB-APB 总线桥和 APB 外设)
⑦ FMC
⑧ Quad SPI
下面我们简单讲解一下几个总线的作用。
① multi-AHB 总线矩阵
multi-AHB 总线矩阵用于主控制器之间的访问仲裁管理。仲裁采用循环调度算法。借助
该总线矩阵,可以实现主控总线到被控总线的访问,这样即使在多个高速外设同时运
行期间,系统也可以实现并发访问和高效运行。
② AHB/APB 总线桥(APB)
借助两个 AHB/APB 总线桥 APB1 和 APB2,可在 AHB 总线与两个 APB 总线之间实现完全
同步的连接,从而灵活选择外设频率。
③ CPU AXIM 总线
该总线通过 AXI-AHB 总线桥将带 FPU 的 Cortex-M7 内核的指令总线和数据总线连接到
multi-AHB 总线矩阵。
④ ITCM 总线
Cortex-M7 使用该总线对映射到 ITCM 接口上的内嵌 flash 进行取指和数据访问。但对于
ITCM RAM,该总线只能进行取指操作。
⑤ DTCM 总线
Cortex-M7 使用该总线对 DTCM RAM 进行数据访问,也可以进行取指。
⑥ CPU AHBS 总线
该总线将 Cortex-M7 的 AHB 被控总线连接到总线矩阵。该总线仅用于通用 DMA 和外设
DAM 到 DTCM RAM 上的数据传输。AHBS 上无法访问 ITCM 总线,因此 RAM 不能通过
ITCM 总线进行 DMA 数据传输。Flash 通过 ITCM 接口进行 DMA 传输时,将强制通过 AHB
总线进行所有传输。
⑦ AHB 外设总线
该总线将 Cortex-M7 的 AHB 外设总线连接到总线矩阵。内核使用该总线执行所有针对
外设的数据访问。该总线的访问目标是 AHB1 总线上的外设(包括 APB 总线上的外设
和 AHB2 总线上的外设)。
⑧ DMA 存储器总线
此总线用于将 DMA 存储器总线主接口连接到总线矩阵。DMA 通过此总线来执行存储
器数据的传入和传出。该总线的访问目标是数据存储器:内部 SRAM1,SRAM2 和 DTCM
以及内部 FLASH 和外部存储器。
⑨ DMA 外设总线
此总线用于将 DMA 外设主总线接口连接到总线矩阵。DMA 通过此总线访问 AHB 外设
或执行存储器间的数据传输。该总线的访问目标是 AHB 和 APB 总线上的外设以及数据
存储器:内部 SRAM1,SRAM2 和 DTCM 以及内部 FLASH 和外部存储器。
⑩ 以太网 DMA 总线
此总线用于将以太网 DMA 主接口连接到总线矩阵。以太网 DMA 通过此总线向存储器
存取数据。该总线的访问目标是数据存储器:内部 SRAM1,SRAM2 和 DTCM,内部 Flash
和外部存储器。
⑪ USB OTG HS DMA 总线
此总线用于将 USB OTG HS DMA 主接口连接到总线矩阵。USB OTG DMA 通过此总线向
存储器加载/存储数据。该总线的访问目标是数据存储器:内部 SRAM1,SRAM2 和 DTCM,
内部 Flash 和外部存储器。
⑫ LCD-TFT 控制器 DMA 总线
此总线用于将 LCD 控制器 DMA 主接口连接到总线矩阵。LCD-TFT DMA 通过此总线向存
储器加载/存储数据。该总线的访问目标是数据存储器:内部 SRAM1,SRAM2 和 DTCM,
外部 Flash 和外部存储器。
⑬ DMA2D 总线
此总线用于将 DMA2D 主接口连接到总线矩阵。DMA2D 图形加速器通过此总线向存储
器加载/存储数据。该总线的访问目标是数据存储器:内部 SRAM1,SRAM2 和 DTCM,
外部 Flash 和外部存储器。
对于系统架构的知识,在刚开始学习 STM32F7 的时候只需要一个大概的了解。对于寻址之
类的知识,这里就不做深入的讲解,中文参考手册都有很详细的讲解。
4.3 STM32F7 时钟系统
STM32F7 时钟系统的知识在《STM32F7 中文参考手册》第五章复位和时钟控制章节有非
常详细的讲解,网上关于时钟系统的讲解也基本都是参考的这里。这些知识也不是什么原创,
纯粹根据官方提供的中文参考手册和自己的应用心得来总结的,如有不合理之处望大家谅解。
这部分内容我们分 3 个小节来讲解:
·4.3.1 STM32F7 时钟树概述
·4.3.2 STM32F7 时钟初始化配置
·4.3.3 STM32F7 时钟使能和配置
4.3.1 STM32F7 时钟树概述
众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而
喻了。 STM32F7 的时钟系统比较复杂,不像简单的 51 单片机一个系统时钟就可以解决一切。
于是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首
先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,
比如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁
干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。
首先让我们来看看 STM32F7 的时钟系统图:

图 4.3.1.1 STM32F7 时钟系统图
在 STM32F7 中,有 5 个最重要的时钟源,为 HSI、HSE、LSI、LSE、PLL。其中 PLL 实
际是分为三个时钟源,分别为主 PLL 和 I2S 部分专用 PLLI2S 和 SAI 部分专用 PLLSAI。从时
钟频率来分可以分为高速时钟源和低速时钟源,在这 5 个中 HSI,HSE 以及 PLL 是高速时钟,
LSI 和 LSE 是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过
接晶振的方式获取时钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。下面我们
看看 STM32F7 的这 5 个时钟源,我们讲解顺序是按图中红圈标示的顺序:
①、LSI 是低速内部时钟,RC 振荡器,频率为 32kHz 左右。LSI 主要可以作为 IWDG 独立看门
狗时钟,LPTimer 低功耗定时器时钟以及 RTC 时钟。②、LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。
③、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为 4MHz~26MHz。
阿波罗 STM32F7 开发板接的是 25MHz 外部晶振。HSE 可以直接做为系统时钟或者 PLL 输入
时钟,同时它经过 2~31 分频后也可以作为 RTC 时钟。
④、HSI 是高速内部时钟,RC 振荡器,频率为 16MHz。可以直接作为系统时钟或者用作 PLL
输入,同时它经过 488 分频之后也可以作为 HDMI-CEC 时钟。
⑤、PLL 为锁相环倍频输出。STM32F7 有三个 PLL:
1) 主 PLL(PLL)由 HSE 或者 HSI 提供时钟信号,并具有两个不同的输出时钟。
第一个输出 PLLP 用于生成高速的系统时钟(最高 216MHz)
第二个输出 PLLQ 为 48M 时钟,用于 USB OTG FS 时钟,随机数发生器的时钟和 SDMMC
时钟。
2) 第一个专用 PLL(PLLI2S)用于生成精确时钟,在 I2S、SAI 和 SPDIFRX 上实现高品质音频性
能。其中,N 是用于 PLLI2S vco 的倍频系数,其取值范围是:50~432;R 是 I2S 时钟的
分频系数,其取值范围是:2~7;Q 是 SAI 时钟分频系数,其取值范围是:2~15;P 没
用到。
3) 第二个专用PLL(PLLSAI)用于为SAI接口生成时钟,生成LCD-TFT时钟以及可供USB OTG FS、
SDMMC 和 RNG 选择的 48MHz 时钟。其中,N 是用于 PLLSAI vco 的倍频系数,其取值
范围是:50~432;Q 是 SAI 时钟分频系数,其取值范围是:2~15;R 是 LTDC 时钟的分
频系数,其取值范围是:2~7;P 没用到。
这里我们着重看看主 PLL 时钟第一个高速时钟输出 PLLP 的计算方法,其他 PLL 时钟计算方法
类似。图 4.3.1.2 是主 PLL 的时钟图。

图 4.3.1.2 STM32F7 主 PLL 时钟图
从图 4.3.1.2 可以看出。主 PLL 时钟的时钟源要先经过一个分频系数为 M 的分频器,然后经过
倍频系数为 N 的倍频器出来之后还需要经过一个分频系数为 P(第一个输出 PLLP)或者 Q(第
二个输出 PLLQ)的分频器分频之后,最后才生成最终的主 PLL 时钟。
例如我们的外部晶振选择 25MHz。同时我们设置相应的分频器 M=25,倍频器倍频系数 N=432,
分频器分频系数 P=2,那么主 PLL 生成的第一个输出高速时钟 PLLP 为:
PLL=25MHz * N/ (M*P)=25MHz* 432/(25*2) = 216MHz
如果我们选择HSE为PLL时钟源,同时SYSCLK时钟源为PLL,那么SYSCLK时钟为216MHz。
这对于我们后面的实验都是采用这样的配置。
上面我们简要概括了 STM32F7 的时钟源,那么这 5 个时钟源是怎么给各个外设以及系统
提供时钟的呢?这里我们选择一些比较常用的时钟知识来讲解。
图 4.3.1.1 中我们用 A~R 标示我们要讲解的地方。
A.
这是低功耗定时器 LPTimer 时钟,从图中可以看出,LPTimer 有四个时钟源可以
选择,分别为 LSI、HSI、LSE 和 PCLKx,默认情况下 LPTimer 选用 PCLKx 作为
时钟源。
B.
这里是 USART 时钟源。从图中可以看出,USART 时钟源可选为 LSE、HSI、
SYSCLK 以及 PCLKx,默认情况下 USART 选用 PCLKx 作为时钟源。
C.
这里是硬件 I2C 时钟源,从图上可以看出,I2C 可选时钟源为 HSI、SYSCLK 以
及 PCLKx。默认情况下 I2C 选用 PCLKx 作为时钟源。
D.
这是 STM32F7 独立看门狗 IWDG 时钟,来源为 LSI。
E.
这里是 RTC 时钟源,可选 LSI、LSE 和 HSE 的 2~31 分频。
F.
这是SDMMC时钟源,来源为系统时钟SYSCLK或者PLL48CLK,其中PLL48CLK
来源为 PLLQ 或者 PLLSAIP。
G.
这是 STM32F7 输出时钟 MCO1 和 MCO2。MCO1是向芯片的PA8 引脚输出时钟。
它有四个时钟来源分别为:HSI,LSE,HSE 和 PLL 时钟,MCO1 时钟源经过 1~5 分
频后向 PA8 引脚输出时钟。MCO2 是向芯片的 PC9 输出时钟,它同样有四个时钟
来源分别为:HSE,PLL,SYSCLK 以及 PLLI2S 时钟,MCO2 时钟源同样经过 1~5
分频后向 PC9 引脚输出时钟。
H.
这是系统时钟 SYSCLK 时钟源,可选 HSI、HSE 和 PLLCLK。HSI 是内部 16MHz
时钟精度不够,HSE 是外部晶振产生时钟频率较低,大部分情况下系统都会选择
PLLCLK 作为系统时钟。
I.
这是以太网 PTP 时钟,来源为系统时钟 SYSCLK。
J.
这是 AHB 总线预分频器,分频系数为 2N (N=0~9)。系统时钟 SYSCLK 经过 AHB
预分频器之后产生 AHB 总线时钟 HCLK。
K.
这是 APBx 预分频器(分频系数可选 1,2,4,8,16),HCLK(AHB 总线时钟)经过
APBx预分频器之后,产生PCLKx。这里大家还要注意,APBx定时器时钟是PCLKx
经过倍频后得来,倍频系数为 1 或者 2,如果 APBx 预分频系数等于 1,那么这里
的倍频系数为 1,否则倍频系数为 2。
L~N.
这是 PLL 时钟。L 为主 PLL 时钟,M 为专用 PLL 时钟 PLLI2S,N 为专用 PLL
时钟 PLLSAI。主 PLL 主要用来产生 PLL 时钟作为系统时钟,同时 PLL48CLK 时
钟也可以选择 PLLQ 或者 PLLSAIP。PLLI2S 主要用来为 I2S、SAI 和 SPDIFRX
产生精确时钟。PLLSAIP 则为 SAI 接口生成时钟,生成 LCD-TFT 时钟以及可供
USB OTG FS、SDMMC 和 RNG 选择的 48MHz 时钟 PLL48CLK。
O.
这是 SPDIFRX 时钟,由 PLLI2SP 提供。
P.
这是 LCD-TFT 时钟,由 PLLSAIP 提供。
Q.
这是 STM32F7 内部以太网 MAC 时钟的来源。对于 MII 接口来说,必须向外部
PHY 芯片提供 25Mhz 的时钟,这个时钟,可以由 PHY 芯片外接晶振,或者使用
STM32F7 的 MCO 输出来提供。然后,PHY 芯片再给 STM32F7 提供
ETH_MII_TX_CLK 和 ETH_MII_RX_CLK 时钟。对于 RMII 接口来说,外部必须
提供 50Mhz 的时钟驱动 PHY 和 STM32F7 的 ETH_RMII_REF_CLK,这个 50Mhz
时钟可以来自 PHY、有源晶振或者 STM32F7 的 MCO。我们的开发板使用的是 RMII
接口,使用 PHY 芯片提供 50Mhz 时钟驱动 STM32F7 的 ETH_RMII_REF_CLK。
R.
这里是指外部 PHY 提供的 USB OTG HS(60MHZ)时钟。
这里还需要说明一下,Cortex 系统定时器 Systick 的时钟源可以是 AHB 时钟 HCLK 或
HCLK 的 8 分频。具体配置请参考 Systick 定时器配置,我们后面会在 5.1 小节讲解 delay 文件
夹代码的时候讲解。
在以上的时钟输出中,有很多是带使能控制的,例如 AHB 总线时钟、内核时钟、各种 APB1
外设、APB2 外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。后面我们讲解
实例的时候会讲解到时钟使能的方法。
4.3.2 STM32F7 时钟系统配置
上一小节我们对 STM32F7 时钟树进行了详细讲解,接下来我们来讲解通过 STM32F7 的
HAL 库进行 STM32F7 时钟系统配置步骤。实际上,STM32F7 的时钟系统配置也可以通过图形
化配置工具 STM32CubeMX 来配置生成,这里我们讲解初始化代码,是为了让大家对 STM32F7
时钟系统有更加清晰的理解。我们将在 4.8 小节讲解图形化配置工具 STM32CubeMX,大家可
以对比参考学习。
前面我们讲解过,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系
统一些初始化配置。那么我们先来看看 SystemInit 程序:
void SystemInit(void)
{
/* FPU 设置------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */
#endif
/* 复位 RCC 时钟配置为默认配置-----------*/
RCC->CR |= (uint32_t)0x00000001;//打开 HSION 位
RCC->CFGR = 0x00000000;//复位 CFGR 寄存器
RCC->CR &= (uint32_t)0xFEF6FFFF;//复位 HSEON, CSSON and PLLON 位
RCC->PLLCFGR = 0x24003010; //复位寄存器 PLLCFGR
RCC->CR &= (uint32_t)0xFFFBFFFF;//复位 HSEBYP 位
RCC->CIR = 0x00000000;//关闭所有中断
/* 配置中断向量表地址=基地址+偏移地址 ------------------*/
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
#endif
}
从上面代码可以看出,SystemInit 主要做了如下三个方面工作:
1) FPU 设置
2) 复位 RCC 时钟配置为默认复位值(默认开启 HSI)
3) 中断向量表地址配置
HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。HAL
库的 SystemInit 函数除了打开 HSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们必须编
写自己的时钟配置函数。首先我们打开工程模板看看我们在工程 SYSTEM 分组下面定义的 sys.c
文件中的时钟初始化函数 Stm32_Clock_Init 的内容:
//时钟设置函数
// VCO 频率 Fvco=Fs*(plln/pllm);
//系统时钟频率 Fsys=Fvco/pllp=Fs*(plln/(pllm*pllp));
// USB,SDIO,RNG 等的时钟频率 Fusb=Fvco/pllq=Fs*(plln/(pllm*pllq));
//Fs:PLL 输入时钟频率,可以是 HSI,HSE 等.
//plln:主 PLL 倍频系数(PLL 倍频),取值范围:64~432.
//pllm:主 PLL 和音频 PLL 分频系数(PLL 之前的分频),取值范围:2~63.
//pllp:系统时钟的主 PLL 分频系数(PLL 之后的分频),取值范围:2,4,6,8.(仅限这 4 个值!)
//pllq:USB/SDIO/随机数产生器等的主 PLL 分频系数(PLL 之后的分频),取值范围:2~15.
//外部晶振为 25M 的时候,推荐值:plln=432,pllm=25,pllp=2,pllq=9.
//得到:Fvco=25*(432/25)=432Mhz
// Fsys=432/2=316Mhz
// Fusb=432/9=48Mhz
//返回值:0,成功;1,失败
void Stm32_Clock_Init(u32 plln,u32 pllm,u32 pllp,u32 pllq)
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStructure;
RCC_ClkInitTypeDef RCC_ClkInitStructure;
__HAL_RCC_PWR_CLK_ENABLE(); //使能 PWR 时钟
__HAL_PWR_VOLTAGESCALING_CONFIG(
PWR_REGULATOR_VOLTAGE_SCALE1);//设置调压器输出电压级别
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开 HSE
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON;
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;
RCC_OscInitStructure.PLL.PLLM=pllm;
RCC_OscInitStructure.PLL.PLLN=plln;
RCC_OscInitStructure.PLL.PLLP=pllp;
RCC_OscInitStructure.PLL.PLLQ=pllq;
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);
if(ret!=HAL_OK) while(1);
ret=HAL_PWREx_EnableOverDrive(); //开启 Over-Driver 功能
if(ret!=HAL_OK) while(1);
//选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV4;
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV2;
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_7);
if(ret!=HAL_OK) while(1);
}
从函数注释可知,函数 Stm32_Clock_Init 的作用是进行时钟系统配置,除了配置 PLL 相关
参数确定 SYSCLK 值之外,还配置了 AHB,APB1 和 APB2 的分频系数,也就是确定了 HCLK,
PCLK1 和 PCLK2 的时钟值。我们首先来看看使用HAL库配置 STM32F7 时钟系统的一般步骤:
1) 使能 PWR 时钟:调用函数__HAL_RCC_PWR_CLK_ENABLE()。
2) 设置调压器输出电压级别:调用函数__HAL_PWR_VOLTAGESCALING_CONFIG()。
3) 选择是否开启 Over-Driver 功能:调用函数 HAL_PWREx_EnableOverDrive()。
4) 配置时钟源相关参数:调用函数 HAL_RCC_OscConfig()。
5) 配置系统时钟源以及 AHB,APB1 和 APB2 的分频系数:调用函数 HAL_RCC_ClockConfig()。
步骤 2 和 3,具有一定的关联性,我们放在后面讲解。对于步骤 1 之所以要使能 PWR 时钟,是
因为后面的步骤设置调节器输出电压级别以及开启 Over-Driver 功能都是电源控制相关配置,所
以必须开启 PWR 时钟。接下来我们先着重讲解步骤 4 和步骤 5 的内容,这也是时钟系统配置
的关键步骤。
对于步骤 4,使用 HAL 来配置时钟源相关参数,我们调用的函数为 HAL_RCC_OscConfig(),
该函数在 HAL 库关键头文件 stm32f7xx_hal_rcc.h 中声明,在文件 stm32f7xx_hal_rcc.c 中定义。
首先我们来看看该函数声明:
__weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);
该函数只有一个入口参数,就是结构体 RCC_OscInitTypeDef 类型指针。接下来我们看看结构体
RCC_OscInitTypeDef 的定义:
typedef struct
{
uint32_t OscillatorType; //需要选择配置的振荡器类型
uint32_t HSEState; //HSE 状态
uint32_t LSEState; //LSE 状态
uint32_t HSIState; //HIS 状态
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
相关参数,我们来看看它的定义:
typedef struct
{
uint32_t PLLState; //PLL 状态
uint32_t PLLSource; //PLL 时钟源
uint32_t PLLM;
//PLL 分频系数 M
uint32_t PLLN; //PLL 倍频系数 N
uint32_t PLLP; //PLL 分频系数 P
uint32_t PLLQ;
//PLL 分频系数 Q
}RCC_PLLInitTypeDef;
从 RCC_PLLInitTypeDef;结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及
相关分频倍频参数。
这个结构体的定义我们就不做过多讲解,接下来我们看看我们的时钟初始化函数
Stm32_Clock_Init 中的配置内容:
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开 HSE
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打开 PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;//PLL 时钟源为 HSE
RCC_OscInitStructure.PLL.PLLM=pllm;
RCC_OscInitStructure.PLL.PLLN=plln;
RCC_OscInitStructure.PLL.PLLP=pllp;
RCC_OscInitStructure.PLL.PLLQ=pllq;
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);
通过该段函数,我们开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE,然后把
Stm32_Clock_Init 的 4 个入口参数直接设置作为 PLL 的参数 M,N,P 和 Q 的值,这样就达到了设
置 PLL 时钟源相关参数的目的。设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率,
接下来我们就需要设置系统时钟,以及 AHB,APB1 和 APB2 相关参数,也就是我们前面提到
的步骤 5。
接下来我们来看看步骤 5 中提到的 HAL_RCC_ClockConfig()函数,声明如下:
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,
uint32_t FLatency);
该函数有两个入口参数,第一个入口参数 RCC_ClkInitStruct 是结构体 RCC_ClkInitTypeDef
指针类型,用来设置 SYSCLK 时钟源以及 AHB,APB1 和 APB2 的分频系数。第二个入口参数
FLatency 用来设置 FLASH 延迟,这个参数我们放在后面跟步骤 2 和步骤 3 一起讲解。
RCC_ClkInitTypeDef 结构体类型定义非常简单,这里我们就不列出来,我们来看看
Stm32_Clock_Init 函数中的配置内容:
//选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|\
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1
|RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;//系统时钟源 PLL
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;//AHB 分频系数为 1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV4; //APB1 分频系数为 4
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV2; //APB2 分频系数为 2
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_7);
第一个参数 ClockType 配置说明我们要配置的是 SYSCLK,HCLK,PCLK1 和 PCLK2 四个时钟。
第二个参数 SYSCLKSource 配置选择系统时钟源为 PLL。
第三个参数 AHBCLKDivider 配置 AHB 分频系数为 1。
第四个参数 APB1CLKDivider 配置 APB1 分频系数为 4。
第五个参数 APB2CLKDivider 配置 APB2 分频系数为 2。
根据我们在主函数中调用 Stm32_Clock_Init(436,25,2,9)时候设置的入口参数值,我们可以
计算出,PLL 时钟为 PLLCLK=HSE*N/M*P=25MHz*436/(25*2)=216MHz,同时我们选择系统
时钟源为 PLL,所以系统时钟 SYSCLK=216MHz。AHB 分频系数为 1,故其频率为
HCLK=SYSCLK/1=216MHz。APB1 分频系数为 4,故其频率为 PCLK1=HCLK/4=54MHz。APB2
分频系数为 2,故其频率为 PCLK2=HCLK/2=216/2=108MHz。最后我们总结一下通过调用函数
Stm32_Clock_Init(432,25,2,9)之后的关键时钟频率值:
SYSCLK(系统时钟)
=216MHz
PLL 主时钟
=216MHz
AHB 总线时钟(HCLK=SYSCLK/1)
=216MHz
APB1 总线时钟(PCLK1=HCLK/4)
=54MHz
APB2 总线时钟(PCLK2=HCLK/2)
=108MHz
最后我们来看看步骤 2,步骤 3 以及步骤 5 中函数 HAL_RCC_ClockConfig 第二个入口参数
FLatency 的含义。这里我们不想讲解得太复杂,大家只需要知道调压器输出电压级别 VOS,
Over-Driver 功能开启以及 FLASH 的延迟 Latency 三个参数,在我们芯片电源电压和 HCLK 固
定之后,他们三个参数也是固定的。首先我们来看看调压器输出电压级别 VOS,它是由 PWR
控制寄存器 CR 的位 15:14 来确定的:
位 15:14 VOS[1:0]
00:保留(默认模式 3 选中)
01:级别 3:HCLK 最大频率 144MHz
10:级别 2: HCLK 最大频率 168MHz,通过开启 Over-drive 模式可以达到 180MHz
11:级别 1:HCLK 最大频率 180MHz,通过开启 Over-drive 模式可以达到 216MHz。
所以如果我们要配置 HCLK 时钟为 216MHz,也就是在 AHB 的分频系数为 1 的情况下需
要系统时钟为 216MHz,那么我们必须配置调压器输出电压级别 VOS 为级别 1,同时开启
Over-drive 功能。所以函数 Stm32_Clock_Init 中步骤 3 和步骤 4 源码如下:
//步骤 3,设置调压器输出电压级别 1
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
ret=HAL_PWREx_EnableOverDrive(); //开启 Over-Driver 功能
配置好调压器输出电压级别 VOS 和 Over-drive 功能之后,如果需要 HCLK 达到 216MHz,
还需要配置 FLASH 延迟 Latency,。对于 STM32F7 系列,FLASH 延迟配置参数值是通过下表
来确定的:

表 4.3.2.1 STM32F7 系列等待周期表
从上表可以看出,在电压为 3.3V 的情况下,如果需要 HCLK 为 216MHz,那么等待周期必须为
7WS,也就 是 8 个 CPU 周期。下面我们看看我们在 Stm32_Clock_Init 中 调用函 数
HAL_RCC_ClockConfig 的时候,第二个入口参数设置值:
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_7);
从上可以看出,我们设置值为 FLASH_LATENCY_7,也就是 7WS,8 个 CPU 周期,与我们预
期一致。时钟系统配置相关知识就给大家讲解到这里。
4.3.3 STM32F7 时钟使能和配置
上一节我们讲解了时钟系统配置步骤。在配置好时钟系统之后,如果我们要使用某些外设,
例如 GPIO,ADC 等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前
没有使能外设时钟,这个外设是不可能正常运行的。STM32 的外设时钟使能是在 RCC 相关寄
存器中配置的。因为 RCC 相关寄存器非常多,有兴趣的同学可以直接打开《STM32F7 中文参
考手册》5.3 小节查看所有 RCC 相关寄存器的配置。接下来我们来讲解通过 STM32F7 的 HAL
库使能外设时钟的方法。
在 STM32F7 的 HAL 库中,外设时钟使能操作都是在 RCC 相关固件库文件头文件
stm32f7xx_hal_rcc.h 定义的。大家打开 stm32f7xx_hal_rcc.h 头文件可以看到文件中除了少数几
个函数声明之外大部分都是宏定义标识符。外设时钟使能在 HAL 库中都是通过宏定义标识符
来实现的。首先,我们来看看 GPIOA 的外设时钟使能宏定义标识符:
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0)
这几行代码非常简单,主要是定义了一个宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE(),
它的核心操作是通过下面这行代码实现的:
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);
这行代码的作用是,设置寄存器 RCC->AHB1ENR 的相关位为 1,至于是哪个位,是由宏定义
标识符 RCC_AHB1ENR_GPIOAEN 的值决定的,而它的值为:
#define RCC_AHB1ENR_GPIOAEN ((uint32_t)0x00000001)
所以,我们很容易理解上面代码的作用是设置寄存器 RCC->AHB1ENR 寄存器的最低位为 1。
我们可以从 STM32F7 的中文参考手册中搜索 AHB1ENR 寄存器定义,最低位的作用是用来使
用 GPIOA 时钟。AHB1ENR 寄存器的位 0 描述如下:
位 0
GPIOAEN:IO 端口 A 时钟使能
由软件置 1 和清零
0:禁止 IO 端口 A 时钟
1:使能 IO 端口 A 时钟
那么我们只需要在我们的用户程序中调用宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE()
就可以实现 GPIOA 时钟使能。使用方法为:
__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟
对于其他外设,同样都是在 stm32f7xx_hal_rcc.h 头文件中定义,大家只需要找到相关宏定义标
识符即可,这里我们列出几个常用使能外设时钟的宏定义标识符使用方法:
__HAL_RCC_DMA1_CLK_ENABLE();//使能 DMA1 时钟
__HAL_RCC_USART2_CLK_ENABLE();//使能串口 2 时钟
__HAL_RCC_TIM1_CLK_ENABLE();//使能 TIM1 时钟
我们使用外设的时候需要使能外设时钟,如果我们不需要使用某个外设,同样我们可以禁
止某个外设时钟。禁止外设时钟使用方法和使能外设时钟非常类似,同样是头文件中定义的宏
定义标识符。我们同样以 GPIOA 为例,宏定义标识符为:
#define __HAL_RCC_GPIOA_CLK_DISABLE() \
(RCC->AHB1ENR &= ~(RCC_AHB1ENR_GPIOAEN))
同样,宏定义标识符__HAL_RCC_GPIOA_CLK_DISABLE()的作用是设置 RCC->AHB1ENR 寄
存器的最低位为 0,也就是禁止 GPIOA 时钟。具体使用方法我们这里就不做过多讲解,我们这
里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:
__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 时钟
__HAL_RCC_USART2_CLK_DISABLE();//禁止串口 2 时钟
__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 时钟
关于 STM32F7 的外设时钟使能和禁止方法我们就给大家讲解到这里。
4.4 IO 引脚复用器和映射
STM32F7 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO
如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。
这部分知识在《STM32F7 中文参考手册》第六章和芯片数据手册有详细的讲解哪些 GPIO 管脚是
可以复用为哪些内置外设。
对于本小节知识,STM32F7 中文参考手册讲解比较详细,我们同样会从中抽取重要的知识点
罗列出来。同时,我们会以串口使用为例给大家讲解具体的引脚复用的配置。
STM32F7 系列微控制器 IO 引脚通过一个复用器连接到内置外设或模块。该复用器一次只允
许一个外设的复用功能(AF)连接到对应的 IO 口。这样可以确保共用同一个 IO 引脚的外设之
间不会发生冲突。
每个 IO 引脚都有一个复用器,该复用器采用 16 路复用功能输入(AF0 到 AF15),可通过
GPIOx_AFRL(针对引脚 0-7)和 GPIOx_AFRH(针对引脚 8-15)寄存器对这些输入进行配置,每
四位控制一路复用:1)完成复位后,所有 IO 都会连接到系统的复用功能 0(AF0)。
2)外设的复用功能映射到 AF1 到 AF13。
3)Cortex-M7 EVENTOUT 映射到 AF15。
简单的理解就是,每个引脚都可以配置为多个复用功能,那么这个引脚到底配置为哪个功
能,可以通过开关(配置)来设定,就像一个模拟开关一样。复用器示意图如下图 4.4.1:

图 4.4.1STM32F7 复用器示意图
接下来,我们简单说明一下这个图要如何看,举个例子,阿波罗 STM32F7 开发板的原理
图上 PC11 的原理图如图 4.4.2 所示:

图 4.4.2 阿波罗 STM32F7 开发板 PC11 原理图
如上图所示,PC11 可以作为 SPI3_MISO/U3_RX/U4_RX/SDIO_D3/DCMI_D4/I2S3ext_SD
等复用功能输出。这么多复用功能,如果这些外设都开启了,那么对 STM32F7 来说那就可能
乱套了,外设之间互相干扰。但是 STM32F7,由于有复用器功能,可以让 PC11 在某个时刻仅
连接到需要使用的特定的外设,因此不存在互相干扰的情况。
上图 4.4.1 是针对引脚 0-7,对于引脚 8-15,控制寄存器为 GPIOx_AFRH。从图中可以看出。
当需要使用复用功能的时候,我们配置相应的寄存器 GPIOx_AFRL 或者 GPIOx_AFRH,让对应引
脚通过复用器连接到对应的复用功能外设。这里我们列出 GPIOx_AFRL 寄存器的描述,
GPIOx_AFRH 的作用跟 GPIOx_AFRL 类似,只不过 GPIOx_AFRH 控制的是一组 IO 口的高八位,
GPIOx_AFRL 控制的是一组 IO 口的低八位。GPIOx_AFRL 寄存器描述如下图 4.4.3 所示:

图 4.4.3 GPIOx_AFRL 寄存器位描述
从表中可以看出,32 位寄存器 GPIOx_AFRL 每四个位控制一个 IO 口,所以每个寄存器控制
32/4=8 个 IO 口。寄存器对应四位的值配置决定这个 IO 映射到哪个复用功能 AF。
在微控制器完成复位后,所有 IO 口都会连接到系统复用功能 0(AF0)。这里大家需要注意,
对于系统复用功能 AF0,我们将 IO 口连接到 AF0 之后,还要根据所用功能进行配置:
1) JTAG/SWD:在器件复位之后,会将这些功能引脚指定为专用引脚。也就是说,这些引脚
在复位后默认就是 JTAG/SWD 功能。如果我们要作为 GPIO 来使用,就需要对对应的 IO
口复用器进行配置。
2) RTC_REFIN:此引脚在系统复位之后要使用的话要配置为浮空输入模式。
3) MCO1 和 MCO2:这些引脚在系统复位之后要使用的话要配置为复用功能模式。
对于外设复用功能的配置,除了 ADC 和 DAC 要将 IO 配置为模拟通道之外其他外设功能一律
要配置为复用功能模式,这个配置是在 IO 口对应的 GPIOx_MODER 寄存器中配置的。同时要配
置 GPIOx_AFRH 或者 GPIOx_AFRL 寄存器,将 IO 口通过复用器连接到所需要的复用功能对应的
AFx。
不是每个 IO 口都可以复用为任意复用功能外设。到底哪些 IO 可以复用为相关外设呢?这
在芯片对应的数据手册(请参考光盘目录:\7,硬件资料\3,芯片资料\STM32F767IGT6.pdf)上
面会有详细的表格列出来。对于 STM32F767,数据手册里面的 Table 12.Alternate function
mapping 表格列出了所有的端口 AF 映射表,因为表格比较大,所以这里只列出 PORTA 的几个端
口为例方便大家理解:


表 4.4.4 PORTA 部分端口 AF 映射表
从表 4.4.4 可以看出,PA9 连接 AF7 可以复用为串口 1 的发送引脚 USART1_TX,PA10 连接 AF7
可以复用为串口 2 的接收引脚 USART1_RX。
接下来我们以串口 1 为例来讲解配置 GPOPA.9,GPIOA.10 口为串口 1 复用功能的一般步骤。
① 首先,我们要使用 IO 复用功能,必须先打开对应的 IO 时钟和复用功能外设时钟,这里
我们使用了 GPIOA 以及 USART1,所以我们需要使能 GPIOA 和 USART1 时钟。方法如下:
__HAL_RCC_GPIOA_CLK_ENABLE();
//使能 GPIOA 时钟
__HAL_RCC_USART1_CLK_ENABLE();
//使能 USART1 时钟
② 其次,我们在 GIPOx_MODER 寄存器中将所需 IO(对于串口 1 是 PA9,PA10)配置为复用
功能(ADC 和 DAC 设置为模拟通道)。
③ 再次,我们还需要对 IO 口的其他参数,例如上拉/下拉以及输出速度等进行配置。
④ 最后,我们需要配置 GPIOx_AFRL 或者 GPIOx_AFRH 寄存器,将 IO 连接到所需的 AFx。
对于 PA9,PA10 复用为 USART1 的发送接收引脚,根据表 4.4.4 可知都需要连接 AF7。
上面三步,在我们 HAL 库中是通过 HAL_GPIO_Init 函数来实现的,参考代码如下:
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_FAST;
//高速
GPIO_Initure.Alternate=GPIO_AF7_USART1;//连接 AF7 复用为串口 1 的发送引脚
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9
通过上面的配置,PA9 就通过映射器链接到 AF7,也就是复用为串口 1 的发送引脚。这个时
候,PA9 将不再作为普通的 IO 口使用。对于 PA10,配置方法一样,同样也是链接 AF7,修改 Pin
成员变量值为 PIN_10 即可。从表 4.4.4 可以看出,PA9 还可以作为 TIM1_CH2 功能引脚,如果
我们希望 PA9 作为 TIM1_CH2 引脚,那么我们需要修改 PA9 的映射关系,修改方法如下:
GPIO_Initure.Alternate= GPIO_AF1_TIM2;//连接 AF1 复用为 TIM2_CH1 引脚
对于 GPIO 初始化结构体成员变量 Alternate 的取值范围,在 HAL 库中有详细定义,取值范
围如下:
#define IS_GPIO_AF(AF) (((AF) == GPIO_AF0_RTC_50Hz)||((AF) == GPIO_AF9_TIM14) || \
((AF) == GPIO_AF0_MCO) || ((AF) == GPIO_AF0_TAMPER) || \
((AF) == GPIO_AF0_SWJ) || ((AF) == GPIO_AF0_TRACE) || \
((AF) == GPIO_AF1_TIM1)|| ((AF) == GPIO_AF1_TIM2) || \
...//此处省略部分代码((AF) == GPIO_AF8_UART7)|| ((AF) == GPIO_AF8_UART8) || \
((AF) == GPIO_AF12_FMC) || ((AF) == GPIO_AF6_SAI1) || \
((AF) == GPIO_AF14_LTDC))
STM32F7 的端口复用和映射就给大家讲解到这里,希望大家课余结合相关实验工程和手册
巩固本小节知识。