完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
1)实验平台:正点原子水星 STM32F4/F7 开发板
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 变量在其他文件中已经定义了,在这里要使用到。所以,你肯定 可以找到在某个地方有变量 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 } 但是我们希望在 test.c 的 changeId(void)函数中使用变量 id,这个时候我们就需要在 test.c 里面去申明变量 id 是外部定义的,因为如果不申明,变量id的作用域是到不了test.c 文件中。 看下面 test.c 中的代码: extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行void changId(void){ id=2; } 在 test.c 中申明变量 id 在外部定义,然后在 test.c 中就可以使用 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 是外部晶振产生的高速时钟,PLL 能产生相比 HSI 和 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 等的时钟频率 Fu***=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 // Fu***=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 的端口复用和映射就给大家讲解到这里,希望大家课余结合相关实验工程和手册 巩固本小节知识。 4.5 STM32 NVIC 中断优先级管理 CM7 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有 256 级的可编程中断设置。但 STM32F767 并没有使用 CM7 内核的全部东西,而是只用了它的一部 分。STM32F767xx 总共有 118 个中断,以下仅以 STM32F767xx 为例讲解。 STM32F767xx 的 118 个中断里面,包括 10 个内核中断和 108 个可屏蔽中断,具有 16 级可 编程的中断优先级,而我们常用的就是这 108 个可屏蔽中断。在 MDK 内,与 NVIC 相关的寄 存器,MDK 为其定义了如下的结构体: typedef struct { __IOM uint32_t ISER[8U]; //Interrupt Set Enable Register uint32_t RESERVED0[24U]; __IOM uint32_t ICER[8U]; //Interrupt Clear Enable Register uint32_t RSERVED1[24U]; __IOM uint32_t ISPR[8U]; //Interrupt Set Pending Register uint32_t RESERVED2[24U]; __IOM uint32_t ICPR[8U]; //Interrupt Clear Pending Register uint32_t RESERVED3[24U]; __IOM uint32_t IABR[8U]; //Interrupt Active bit Register uint32_t RESERVED4[56U]; __IOM uint8_t IP[240U]; //Interrupt Priority Register (8Bit wide) uint32_t RESERVED5[644U]; __OM uint32_t STIR; //Software Trigger Interrupt Register } NVIC_Type; STM32F767 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能 方便的使用 STM32F767 的中断。下面重点介绍这几个寄存器: ISER[8]:ISER 全称是:Interrupt Set Enable Registers,这是一个中断使能寄存器组。上面 说了 CM7 内核支持 256 个中断,这里用 8 个 32 位寄存器来控制,每个位控制一个中断。但是 STM32F767 的可屏蔽中断最多只有 108 个,所以对我们来说,有用的就是四个(ISER[0~3]]), 总共可以表示 128 个中断。而 STM32F767 只用了其中的 108 个。ISER[0]的 bit0~31 分别对应 中断 0~31;ISER[1]的 bit0~32 对应中断 32~63;其他以此类推,这样总共 108 个中断就可以分 别对应上了。你要使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能(这里仅仅是 使能,还要配合中断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。具体每一位 对应哪个中断,请参考 stm32f767xx.h 里面的第 69 行处。 ICER[8]:全称是:Interrupt Clear Enable Registers,是一个中断除能寄存器组。该寄存器组 与 ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER 一样。 这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄 存器都是写 1 有效的,写 0 是无效的。 ISPR[8]:全称是:Interrupt Set Pending Registers,是一个中断挂起控制寄存器组。每个位 对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别 的中断。写 0 是无效的。 ICPR[8]:全称是:Interrupt Clear Pending Registers,是一个中断解挂控制寄存器组。其作 用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断接挂。写 0 无效。 IABR[8]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位 所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄 存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。 IP[240]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄 存器组相当重要!STM32F767 的中断分组与这个寄存器组密切相关。IP 寄存器组由 240 个 8bit 的寄存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。而 STM32F767 只用到了其中的 108 个。IP[109]~IP[0]分别对应中断 109~0(其中,98 和 79 没用到,所以,总 共还是 108 个)。而每个可屏蔽中断占用的 8bit 并没有全部使用,而是 只用了高 4 位。这 4 位, 又分为抢占优先级和子优先级。抢占优先级在前,子优先级在后。而这两个优先级各占几个位 又要根据 SCB->AIRCR 中的中断分组设置来决定。 这里简单介绍一下 STM32F767 的中断分组:STM32F767 将中断分为 5 个组,组 0~4。该 分组的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如表 5.2.6.1 所示: 表 5.2.6.1 AIRCR 中断分组设置表 通过这个表,我们就可以清楚的看到组 0~4 对应的配置关系,例如组设置为 3,那么此时 所有的 108 个中断,每个中断的中断优先寄存器的高四位中的最高 3 位是抢占优先级,低 1 位 是响应优先级。每个中断,你可以设置抢占优先级为 0~7,响应优先级为 1 或 0。抢占优先级 的级别高于响应优先级。而数值越小所代表的优先级就越高。 这里需要注意两点:第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看 哪个中断先发生就先执行;第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级 中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。 结合实例说明一下:假定设置中断优先级组为 2,然后设置中断 3(RTC_WKUP 中断)的抢 占优先级为 2,响应优先级为 1。中断 6(外部中断 0)的抢占优先级为 3,响应优先级为 0。中 断 7(外部中断 1)的抢占优先级为 2,响应优先级为 0。那么这 3 个中断的优先级顺序为:中 断 7>中断 3>中断 6。 上面例子中的中断 3 和中断 7 都可以打断中断 6 的中断。而中断 7 和中断 3 却不可以相互 打断! 通过以上介绍,我们熟悉了 STM32F7 中断设置的大致过程。接下来我们介绍如何使用 HAL 库实现以上中断分组设置以及中断优先级管理,使中断配置简单化。NVIC 中断管理相关函数 主要在 HAL 库关键文件 stm32f7xx_hal_cortex.c 中定义。 首先要讲解的是中断优先级分组函数 HAL_NVIC_SetPriorityGrouping,其函数申明如下: void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup); 这个函数的作用是对中断的优先级进行分组,这个函数在系统中只需要被调用一次,一旦分组确定就最好不要更改,否则容易造成程序分组混乱。这个函数我们可以找到其函数体内容 如下: void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup) { /* Check the parameters */ assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup)); /* Set the PRIGROUP[10:8] bits according to the PriorityGroup parameter value */ NVIC_SetPriorityGrouping(PriorityGroup); } 从函数体以及注释可以看出,这个函数是通过调用函数 NVIC_SetPriorityGrouping 来进行中断 优先级分组设置。通过查找(参考 3.5.3 小节 MDK 中“Go to definition of”的使用方法),我们可 以知道函数 NVIC_SetPriorityGrouping 是在文件 core_cm4.h 头文件中定义的。接下来,我们来 分析一下函数 NVIC_SetPriorityGrouping 函数定义。定义如下: __STATIC_INLINE void NVIC_SetPriorityGrouping(uint32_t PriorityGroup) { uint32_t reg_value; uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL); reg_value= SCB->AIRCR; /* read old register configuration */ reg_value&=~((uint32_t)(SCB_AIRCR_VECTKEY_Msk |SCB_AIRCR_PRIGROUP_Msk)); reg_value = (reg_value|((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) | (PriorityGroupTmp << 8U) ); SCB->AIRCR = reg_value; } 从函数内容可以看出,这个函数主要作用是通过设置 SCB->AIRCR 寄存器的值来设置中断优先 级分组,这在前面寄存器讲解的过程中已经讲到。 关于函数 HAL_NVIC_SetPriorityGrouping 的函数体内容解读我就给大家介绍到这里。接下 来我们来看看这个函数的入口参数。大家继续回到函数 HAL_NVIC_SetPriorityGrouping 的定义 可以看到,函数的最开头有这样一行函数: assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup)); 其中函数 assert_param 是断言函数,它的作用主要是对入口参数的有效性进行判断。也就是说 我们可以通过这个函数知道入口参数在哪些范围内是有效的。而其入口参数通过在 MDK 中双 击选中 “IS_NVIC_PRIORITY_GROUP”,然后右键“Go to defition of …”可以查看到为: #define IS_NVIC_PRIORITY_GROUP(GROUP) (((GROUP) == NVIC_PriorityGroup_0) || ((GROUP) == NVIC_PriorityGroup_1) || ((GROUP) == NVIC_PriorityGroup_2) || ((GROUP) == NVIC_PriorityGroup_3) || ((GROUP) == NVIC_PriorityGroup_4)) 从这个内容可以看出,当 GROUP 的值为 NVIC_PriorityGroup_0~ NVIC_PriorityGroup_4 的时候, IS_NVIC_PRIORITY_GROUP 的值才为真。这也就是我们上面表 4.5.1 讲解的,分组范围为 0-4, 对应的入口参数为宏定义值 NVIC_PriorityGroup_0~ NVIC_PriorityGroup_4。比如我们设置整个 系统的中断优先级分组值为 2,那么方法是: HAL_NVIC_SetPriorityGrouping (NVIC_PriorityGroup_2); 这样就确定了中断优先级分组为 2,也就是 2 位抢占优先级,2 位响应优先级,抢占优先级和响 应优先级的值的范围均为 0-3。 讲到这里,大家对怎么进行系统的中断优先级分组设置,以及具体的中断优先级设置函数 HAL_NVIC_SetPriorityGrouping 的内部函数实现都有了一个详细的理解。接下来我们来看看在 HAL 库里面,是怎样调用 HAL_NVIC_SetPriorityGrouping 函数进行分组设置的。 打开 stm32f7xx_hal.c 文件可以看到,文件内部定义了 HAL 库初始化函数 HAL_Init,这个 函数非常重要,其作用主要是对中断优先级分组,FLASH 以及硬件层进行初始化,我们在 3.1 小节对其进行了比较详细的讲解。这里我们只需要知道,在系统主函数 main 开头部分,我们都 会首先调用 HAL_Init 函数进行一些初始化操作。在 HAL_Init 内部,有如下一行代码: HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); 这行代码的作用是把系统中断优先级分组设置为分组 4,这在我们前面已经详细讲解。也 就是说,在主函数中调用 HAL_Init 函数之后,在 HAL_Init 函数内部会通过调用我们前面讲解 的 HAL_NVIC_SetPriorityGrouping 函数来进行系统中断优先级分组设置。所以,我们要进行中 断优先级分组设置,只需要修改 HAL_Init 函数内部的这行代码即可。中断优先级分组的内容我 们就给大家讲解到这里。 设置好了系统中断分组,那么对于每个中断我们又怎么确定他的抢占优先级和响应优先级 呢?官方 HAL 库文件 stm32f7xx_hal_cortex.c 中定义了三个单个中断优先级设置函数。函数如 下: void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority); void HAL_NVIC_EnableIRQ(IRQn_Type IRQn); void HAL_NVIC_DisableIRQ(IRQn_Type IRQn); 第一个函数 HAL_NVIC_SetPriority 是用来设置单个优先级的抢占优先级和响应优先级的值。 第二个函数 HAL_NVIC_EnableIRQ 是用来使能某个中断通道。 第三个函数 HAL_NVIC_DisableIRQ 是用来清除某个中断使能的,也就是中断失能。 这三个函数的使用都非常简单,对于具体的调用方法,大家可以参考我们后面第九章外部中断 实验讲解。 这里大家还需要注意,中断优先级分组和中断优先级设置是两个不同的概念。中断优先级 分组是用来设置整个系统对于中断分组设置为哪个分组,分组号为 0-4,设置函数为 HAL_NVIC_SetPriorityGrouping,确定了中断优先级分组号,也就确定了系统对于单个中断的 抢占优先级和响应优先级设置各占几个位(对应表 4.5.1)。设置好中断优先级分组,确定了分 组号之后,接下来我们就是要对单个优先级进行中断优先级设置。也就是这个中断的抢占优先 级和响应优先级的值,设置方法就是我们上面讲解的三个函数。 最后我们总结一下中断优先级设置的步骤: ①系统运行开始的时候设置中断分组。确定组号,也就是确定抢占优先级和响应优先级的 分配位数。设置函数为 HAL_NVIC_PriorityGroupConfig。对于 HAL 库,在文件 stm32f7xx_hal.c 内部定义函数 HAL_Init 中有调用 HAL_NVIC_PriorityGroupConfig 函数进行相关设置,所以我 们只需要修改 HAL_Init 内部对中断优先级分组设置即可。 ② 设置单个中断的中断优先级别和使能相应中断通道,使用到的函数函数主要为函数 HAL_NVIC_SetPriority 和函数 HAL_NVIC_EnableIRQ。 4.6 HAL 库中寄存器地址名称映射分析 之所以要讲解这部分知识,是因为经常会遇到客户提到不明白 HAL 库中那些结构体是怎么 与寄存器地址对应起来的。这里我们就做一个简要的分析吧。 首先我们看看 51 中是怎么做的。51 单片机开发中经常会引用一个 reg51.h 的头文件,下 面我们看看他是怎么把名字和寄存器联系起来的: sfr P0 =0x80; sfr 也是一种扩充数据类型,点用一个内存单元,值域为 0~255。利用它可以访问 51 单片 机内部的所有特殊功能寄存器。如用 sfr P1 = 0x90 这一句定义 P1 为 P1 端口在片内的寄存 器。然后我们往地址为 0x80 的寄存器设值的方法是:P0=value; 那么在 STM32 中,是否也可以这样做呢??答案是肯定的。肯定也可以通过同样的方 式来做,但是 STM32 因为寄存器太多太多,如果一一以这样的方式列出来,那要好大的篇 幅,既不方便开发,也显得太杂乱无序的感觉。所以 MDK 采用的方式是通过结构体来将 寄存器组织在一起。下面我们就讲解 MDK 是怎么把结构体和地址对应起来的,为什么我 们修改结构体成员变量的值就可以达到操作对应寄存器的值。这些事情都是在 stm32f7xx.h 文件中完成的。我们通过 GPIOA 的几个寄存器的地址来讲解吧。 首先我们可以查看《STM32F7 中文参考手册》中的寄存器地址映射表 22(P197)。这里 我们选用 GPIOA 为例来讲解。GPIOA 寄存器地址映射如下表 4.6.1: 表 4.6.1 GIPOA 寄存器地址偏移表 从这个表我们可以看出,因为 GIPO 寄存器都是 32 位,所以每组 GPIO 的 10 个寄存器 中,每个寄存器占有 4 个地址,一共占用 40 个地址,地址偏移范围为(0x00~0x24)。这个 地址偏移是相对 GPIOA 的基地址而言的。GPIOA 的基地址是怎么算出来的呢?因为 GPIO 都是挂载在 AHB1 总线之上,所以它的基地址是由 AHB1 总线的基地址加上 GPIOA 在 AHB1 总线上的偏移地址决定的。同理依次类推,我们便可以算出 GPIOA 基地址了。下面 我们打开 stm32f767xx.h 定位到 GPIO_TypeDef 定义处: typedef struct { __IO uint32_t MODER; __IO uint32_t OTYPER; __IO uint32_t OSPEEDR; __IO uint32_t PUPDR; __IO uint32_t IDR; __IO uint32_t ODR; __IO uint32_t BSRR; __IO uint32_t LCKR; __IO uint32_t AFR[2]; } GPIO_TypeDef; 然后定位到: #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 可以看出,GPIOA 是将 GPIOA_BASE 强制转换为 GPIO_TypeDef 结构体指针,这句话的 意思是,GPIOA 指向地址 GPIOA_BASE,GPIOA_BASE 存放的数据类型为 GPIO_TypeDef。 然后在 MDK 中双击“GPIOA_BASE”选中之后右键选中“Go to definition of ”,便可以查 看 GPIOA_BASE 的宏定义: #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000U) 依次类推,可以找到最顶层: #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000U) #define PERIPH_BASE 0x40000000U 所以我们便可以算出 GPIOA 的基地址位: GPIOA_BASE= 0x40000000+0x00020000+0x0000=0x40020000 下面我们再跟《STM32F7 中文参考手册》比较一下看看 GPIOA 的基地址是不是 0x40020000 。 截图 P53 存储器映射表我们可以看到,GPIOA 的起始地址也就是基地址确实是 0x40020000: 图 4.6.2 GPIO 存储器地址映射表 同样的道理,我们可以推算出其他外设的基地址。 上面我们已经知道 GPIOA 的基地址,那么那些 GPIOA 的 10 个寄存器的地址又是怎么 算出来的呢?在上面我们讲过 GPIOA 的各个寄存器对于 GPIOA 基地址的偏移地址,所以 我们自然可以算出来每个寄存器的地址。 GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相对 GPIOA 基地址的偏移值 这个偏移值在上面的寄存器地址映像表中可以查到。 那么在结构体里面这些寄存器又是怎么与地址一一对应的呢?这里涉及到结构体成员 变量地址对齐方式方面的知识,这方面的知识大家可以在网上查看相关资料复习一下,这 里我们不做详细讲解。在我们定义好地址对齐方式之后,每个成员变量对应的地址就可以 根据其基地址来计算。对于结构体类型 GPIO_TypeDef,他的所有成员变量都是 32 位,成 员变量地址具有连续性。所以自然而然我们就可以算出 GPIOA 指向的结构体成员变量对应 地址了。 表 4.6.3 GPIOA 各寄存器实际地址表 我们可以把 GPIO_TypeDef 的定义中的成员变量的顺序和 GPIOx 寄存器地址映像对比 可以发现,他们的顺序是一致的,如果不一致,就会导致地址混乱了。 这就是为什么固件库里面:GPIOA->BSRR=value;就是设置地址为 0x40020000 +0x18 (BSRR 偏移量)=0x40020018 的寄存器 BSRR 的值了。它和 51 里面 P0=value 是设置 地址为 0x80 的 P0 寄存器的值是一样的道理。 看到这里你是否会学起来踏实一点呢?STM32 使用的方式虽然跟 51 单片机不一样, 但是原理都是一致的。 4.7 MDK 中使用 HAL 库快速组织代码技巧 这一节主要讲解在 MDK 中使用 HAL 库开发的一些小技巧,仅供初学者参考。这节的知识 大家可以在学习第一个跑马灯实验的时候参考一下,对初学者应该很有帮助。我们就用最简单 的 GPIO 初始化函数为例。 现 在 我 们 要 初 始 化 某 个 GPIO 端 口 , 我 们 要 怎 样 快 速 操 作 呢 ? 在 头 文 件 stm32f7xx_hal_gpio.h 头文件中,声明 GPIO 初始化函数为: void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init); 现在我们想写初始化函数,那么我们在不参考其他代码的前提下,怎么快速组织代码呢? 首先,我们可以看出,函数的入口参数是 GPIO_TypeDef 类型指针和 GPIO_InitTypeDef 类 型指针,因为 GPIO_TypeDef 入口参数比较简单,所以我们 就通过第二个入口参数 GPIO_InitTypeDef 类型指针来讲解。双击 GPIO_InitTypeDef 后右键选择“Go to definition of…”, 如下图 4.7.1: 图 4.7.1 查看类型定义方法 于是定位到 stm32f7xx_hal_gpio.h 中 GPIO_InitTypeDef 的定义处: typedef struct { uint32_t Pin; uint32_t Mode; uint32_t Pull; uint32_t Speed; uint32_t Alternate; }GPIO_InitTypeDef; 可以看到这个结构体有 5 个成员变量,这也告诉我们一个信息,一个 GPIO 口的状态是由模式 (Mode),速度(Speed)以及上下拉(Pull)来决定的。我们首先要定义一个结构体变量,下面 我们定义: GPIO_InitTypeDef GPIO_InitStructure; 接着我们要初始化结构体变量 GPIO_InitStructure。首先我们要初始化成员变量 Pin,这个时候我 们就有点迷糊了,这个变量到底可以设置哪些值呢?这些值的范围有什么规定吗? 这里我们就回到 HAL_GPIO_Init 声明处,同样双击 HAL_GPIO_Init,右键点击“Go to definition of …”,这样光标定位到 stm32f7xx_hal_gpio.c 文件中的 HAL_GPIO_Init 函数体开始处, 我们可以看到在函数中有如下几行: void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init) { …//此处省略部分代码 assert_param(IS_GPIO_ALL_INSTANCE(GPIOx)); assert_param(IS_GPIO_PIN(GPIO_Init->Pin)); assert_param(IS_GPIO_MODE(GPIO_Init->Mode)); assert_param(IS_GPIO_PULL(GPIO_Init->Pull)); …//此处省略部分代码 assert_param(IS_GPIO_AF(GPIO_Init->Alternate)); …//此处省略部分代码 } 顾名思义,assert_param 是断言语句,是对函数入口参数的有效性进行判断,所以我们可以从 这个函数入手,确定入口参数范围。第一行是对第一个参数 GPIOx 进行有效性判断,双击 “IS_GPIO_ALL_INSTANCE”右键点击“go to defition of…” 定位到了下面的定义: #define IS_GPIO_ALL_INSTANCE(INSTANCE) (((INSTANCE) == GPIOA) || ((INSTANCE) == GPIOB) || ((INSTANCE) == GPIOC) || ((INSTANCE) == GPIOD) || …//此处省略部分代码 ((INSTANCE) == GPIOJ) || ((INSTANCE) == GPIOK)) 很明显可以看出,GPIOx 的取值规定只允许是 GPIOA~GPIOK。 同样的办法,我们双击“IS_GPIO_PIN” 右键点击“go to defition of…”,定位到下面的定义: #define IS_GPIO_PIN(PIN) (((PIN) & GPIO_PIN_MASK ) != (uint32_t)0x00) 同时,宏定义标识符 GPIO_PIN_MASK 的定义为: #define GPIO_PIN_MASK ((uint32_t)0x0000FFFF) 从上面可以看出,PIN 取值只要低 16 位不为 0 即可。这里需要大家注意,因为一组 IO 口只有 16 个 IO,实际上 PIN 的值在这里只有低 16 位有效,所以 PIN 的取值范围为 0x0001~0xFFFF。 那么是不是我们写代码初始化就是直接给一个 16 位的数字呢?这也是可以的,但是大多数情况 下,我们不会直接在入口参数处设置一个简单的数字,因为这样代码的可读性太差,HAL 库会 将这些数字的含义 通过宏定义定义出来,这样可读性大大增强。我们可以看到在 GPIO_PIN_MASK 宏定义的上面还有数行宏定义: #define GPIO_PIN_0 ((uint16_t)0x0001) #define GPIO_PIN_1 ((uint16_t)0x0002) #define GPIO_PIN_2 ((uint16_t)0x0004) …//此处省略部分定义 #define GPIO_PIN_14 ((uint16_t)0x4000) #define GPIO_PIN_15 ((uint16_t)0x8000) #define GPIO_PIN_All ((uint16_t)0xFFFF) 这些宏定义 GPIO_PIN_0 ~ GPIO_PIN_All 就是 HAL 库事先定义好的,我们写代码的时候初始 化结构体 成员变量 Pin 的时候入口参数可以是这些宏定义标识符。 同理,对于成员变量 Pull,我们用同样的方法,可以找到其取值范围定义为: #define IS_GPIO_PULL(PULL) (((PULL) == GPIO_NOPULL) || ((PULL) == GPIO_PULLUP) || ((PULL) == GPIO_PULLDOWN)) 也就是 PULL 的 取 值 范 围 只 能 是 标 识 符 GPIO_NOPULL , GPIO_PULLUP 以 及 GPIO_PULLDOWN。 对于其他成员变量 Mode 以及 Alternate,方法都是一样的,这里基于篇幅考虑我们就不重 复讲解。讲到这里,我们基本对 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; //复用为 USART1 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9 接着又有一个问题会被提出来,这个初始化函数一次只能初始化一个 IO 口吗?我要同时 初始化很多个 IO 口,是不是要复制很多次这样的初始化代码呢? 这里又有一个小技巧了。从上面的 GPIO_PIN_X 的宏定义我们可以看出,这些值是 0,1,2,4 这样的数字,所以每个 IO 口选定都是对应着一个位,16 位的数据一共对应 16 个 IO 口。这个 位为 0 那么这个对应的 IO 口不选定,这个位为 1 对应的 IO 口选定。如果多个 IO 口,他们都 是对应同一个 GPIOx,那么我们可以通过|(或)的方式同时初始化多个 IO 口。这样操作的前 提是,他们的 Mode,Speed,Pull 和 Alternate 参数值相同,因为这些参数并不能一次定义多种。 所以初始化多个具有相同配置的 IO 口的方式可以是如下: GPIO_InitTypeDef GPIO_Initure; GPIO_Initure.Pin=GPIO_PIN_9| GPIO_PIN_10| GPIO_PIN_11; //PA9,PA10,PA11 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速 GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为 USART1 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9 ,PA10,PA11 对于那些参数可以通过|(或)的方式连接,这既有章可循,同时也靠大家在开发过程中不断积累。 大家会觉得上面讲解有点麻烦,每次要去查找 assert_param()这个函数去寻找,那么有没有 更好的办法呢?大家可以打开 GPIO_InitTypeDef 结构体定义: typedef struct { uint32_t Pin; /*!< Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define */ uint32_t Mode; /*!< Specifies the operating mode for the selected pins. This parameter can be a value of @ref GPIO_mode_define */ uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins. This parameter can be a value of @ref GPIO_pull_define */ uint32_t Speed; /*!< Specifies the speed for the selected pins. This parameter can be a value of @ref GPIO_speed_define */ uint32_t Alternate; /*!< Peripheral to be connected to the selected pins. This parameter can be a value of @ref GPIO_Alternate_function_selection */ }GPIO_InitTypeDef; 从上图的结构体成员后面的注释我们可以看出 Pin 的意思是 “Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define”。 从这段注释可以看出 Pin 的取值需要参考注释 GPIO_pins_define,大家可以在 MDK 中搜索注释 GPIO_pins_define,就可以找到上面我们提到的 Pin 的取值范围宏定义。如果要确定详细的信息 我们就得去查看手册了。对于去查看手册的哪个地方,你可以在函数 HAL_GPIO_Init ()的函数 体中搜索 Pin 关键字,然后查看库函数设置 Pin 是设置的哪个寄存器的哪个位,然后去中文参 考手册查看该寄存器相应位的定义以及前后文的描述。 这一节我们就讲解到这里,希望能对大家的开发有帮助。 |
|
相关推荐
|
|
很棒棒哦,值得学习
|
|
|
|
|
|
自定义系统时钟频率后,用库函数和微带两种方式点亮LED灯,为什么灯的闪烁频率不一致?
745 浏览 2 评论
912 浏览 1 评论
1758 浏览 0 评论
【嵌入式SD NAND】基于FATFS/Littlefs文件系统的日志框架实现
671 浏览 0 评论
求助!STM32F4单片机编程与设计sEMG信号采集电路问题
868 浏览 0 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-3-29 02:48 , Processed in 0.658398 second(s), Total 63, Slave 46 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 深圳华秋电子有限公司
电子发烧友 (电路图) 粤公网安备 44030402000349 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号