pos机gp什么意思(探索者 STM32F407 开发板资料连载第四章 F4 开发基础知识入门)

快鱼网 16 0

1)实验平台:探索者 STM32F407 开发板

2)摘自《STM32F4 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

第四章 STM32F4 开发基础知识入门

这一章,我们将着重 STM32 开发的一些基础知识,让大家对 STM32 开发有一个初步的了

解,为后面 STM32 的学习做一个铺垫,方便后面的学习。这一章的内容大家第一次看的时候

可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。这章我们分 7

个小结,

·4.1 MDK 下 C 语言基础复习

·4.2 STM32F4 系统架构

·4.3 STM32F4 时钟系统

·4.4 IO 引脚复用器和映射

·4.5 STM32F4 NVIC 中断优先级管理

·4.6 MDK 中寄存器地址名称映射分析

·4.7 MDKHAL 库快速开发技巧

4.1 MDK 下 C 语言基础复习

这一节我们主要讲解一下 C 语言基础知识。C 语言知识博大精深,也不是我们三言两语能

讲解清楚,同时我们相信学 STM32F4 这种级别 MCU 的用户,C 语言基础应该都是没问题的。我

们这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的

用户能够快速开发 STM32 程序。同时希望这些用户能够多去复习一下 C 语言基础知识,C 语言

毕竟是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略

不看。

4.1.1 位操作

C 语言位操作相信学过 C 语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级

别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面

我们先讲解几种位操作符,然后讲解位操作使用技巧。

C 语言支持如下 6 种位操作

表 4.1.1 16 种位操作

这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信

大家学 C 语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作

符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。

1) 不改变其他位的值的状况下,对某几个位进行设值。

这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,

然后用|操作符设值。比如我要改变 GPIOA-> BSRRL 的状态,可以先对寄存器的值进行&

清零操作

GPIOA-> BSRRL &=0XFF0F; //将第 4-7 位清 0

然后再与需要设置的值进行|或运算

GPIOA-> BSRRL |=0X0040;//设置相应位的值,不改变其他位的值2) 移位操作提高代码的可读性。

移位操作在单片机开发中也非常重要,我们来看看下面一行代码

GPIOx->ODR = (((uint32_t)0x01) << pinpos);

这个操作就是将 ODR 寄存器的第 pinpos 位设置为 1,为什么要通过左移而不是直接设

置一个固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以

很直观明了的知道,是将第 pinpos 位设置为 1。如果你写成

GPIOx->ODR =0x0030;

这样的代码就不好看也不好重用了。

3) ~取反操作使用技巧

SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时

其他位都保留为 1,简单的作法是直接给寄存器设置一个值:

TIMx->SR=0xFFF7;

这样的作法设置第 3 位为 0,但是这样的作法同样不好看,并且可读性很差。看看库函数

代码中怎样使用的:

TIMx->SR = (uint16_t)~TIM_FLAG;

而 TIM_FLAG 是通过宏定义定义的值:

#define TIM_FLAG_Update ((uint16_t)0x0001)

#define TIM_FLAG_CC1 ((uint16_t)0x0002)

看这个应该很容易明白,可以直接从宏定义中看出 TIM_FLAG_Update 就是设置的第 0 位了,

可读性非常强。

4.1.2 define 宏定义

define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供

方便。常见的格式:

#define 标识符 字符串

“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:

#define PLL_M 8

定义标识符 PLL_M 的值为 8。

至于 define 宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。

4.1.3 ifdef 条件编译

单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而

当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:

#ifdef 标识符

程序段 1

#else

程序段 2

#endif

它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译,

否则编译程序段 2。 其中#else 部分也可以没有,即:

#ifdef

程序段 1

#endif

这个条件编译在MDK里面是用得很多的,在stm32f4xx.h这个头文件中经常会看到这样的语句:

#if defined (STM32F40_41xxx)

STM32F40x 系列和 STM32F41x 系列芯片需要的一些变量定义

#end

而(STM32F40_41xxx 则是我们通过#define 来定义的。条件编译也是 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 U_TYPE {

Int BaudRate

Int WordLength;

}usart1,usart2;

在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:

Struct 结构体名字 结构体变量列表 ;

例如:struct U_TYPE usart1,usart2;

结构体成员变量的引用方法是:

结构体变量名字.成员名

比如要引用 usart1 的成员 BaudRate,方法是:usart1.BaudRate;

结构体指针变量定义也是一样的,跟其他变量没有啥区别。

例如:struct U_TYPE *usart3;//定义结构体指针变量 usart1;

结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 usart3 结构体指针指向的结

构体的成员变量 BaudRate,方法是:

Usart3->BaudRate;

上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,

有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实

例回答一下这个问题。

在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态

是由几个属性来决定的,比如串口号,波特率,极性,以及模式等。对于这种情况,在我们没

有学习结构体的时候,我们一般的方法是:

void USART_Init(u8 usartx,u32 u32 BaudRate,u8 parity,u8 mode);

这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里

面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入字长这个入口参数。于

是我们的定义被修改为:

void USART_Init (u8 usartx,u32 BaudRate, u8 parity,u8 mode,u8 wordlength );

但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函

数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?

这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下,

只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。

结构体就是将多个变量组合为一个有机的整体。上面的函数,BaudRate,wordlength,

Parity,mode,wordlength 这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参

数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的:

typedef struct

{

uint32_t USART_BaudRate;

uint16_t USART_WordLength;

uint16_t USART_StopBits;

uint16_t USART_Parity;

uint16_t USART_Mode;

uint16_t USART_HardwareFlowControl;

} USART_InitTypeDef;

于是,我们在初始化串口的时候入口参数就可以是 USART_InitTypeDef 类型的变量或者指针变

量了,MDK 中是这样做的:

void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);

这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需

要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义

就可以达到增加变量的目的。

理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,

如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可

以提高你的代码的可读性。

使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作

用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只

是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲

解结构体的一些其他知识。

4.2 STM32F4 总线架构

STM32F4 的总线架构比 51 单片机就要强大很多了。STM32F4 总线架构的知识可以在

《STM32F4XX 中文参考手册》第二章有讲解,这里我们也把这一部分知识抽取出来讲解,是

为了大家在学习 STM32F4 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参

考手册中参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需

要详细深入的了解 STM32 的系统架构,还需要多看看《STM32F4XX 中文参考手册》或者在网

上搜索其他相关资料学习。

我们这里所讲的 STM32F4 系统架构主要针对的 STM32F407 系列芯片。首先我们看看

STM32 的总线架构图:

图 4.2.1 STM32F407 系统架构图

主系统由 32 位多层 AHB 总线矩阵构成。总线矩阵用于主控总线之间的访问仲裁管理。仲裁采

取循环调度算法。总线矩阵可实现以下部分互联:

八条主控总线是:

Cortex-M4 内核 I 总线, D 总线和 S 总线;

DMA1 存储器总线, DMA2 存储器总线;

DMA2 外设总线;

以太网 DMA 总线;

USB OTG HS DMA 总线;

七条被控总线:

内部 FLASH ICode 总线;

内部 FLASH DCode 总线;

主要内部 SRAM1(112KB)

辅助内部 SRAM2(16KB);

辅助内部 SRAM3(64KB) (仅适用 STM32F42xx 和 STM32F43xx 系列器件);

AHB1 外设 和 AHB2 外设;

FSMC

下面我们具体讲解一下图中几个总线的知识。

① I 总线(S0):此总线用于将 Cortex-M4 内核的指令总线连接到总线矩阵。内核通过此总

线获取指令。此总线访问的对象是包括代码的存储器。

② D 总线(S1):此总线用于将 Cortex-M4 数据总线和 64KB CCM 数据 RAM 连接到总线矩

阵。内核通过此总线进行立即数加载和调试访问。

③ S 总线(S2):此总线用于将 Cortex-M4 内核的系统总线连接到总线矩阵。此总线用于访

问位于外设或 SRAM 中的数据。

④ DMA 存储器总线(S3,S4):此总线用于将 DMA 存储器总线主接口连接到总线矩阵。

DMA 通过此总线来执行存储器数据的传入和传出。

⑤ DMA 外设总线:此总线用于将 DMA 外设主总线接口连接到总线矩阵。DMA 通过此

总线访问 AHB 外设或执行存储器之间的数据传输。

⑥ 以太网 DMA 总线:此总线用于将以太网 DMA 主接口连接到总线矩阵。以太网 DMA

通过此总线向存储器存取数据。

⑦ USB OTG HS DMA 总线(S7):此总线用于将 USB OTG HS DMA 主接口连接到总线矩

阵。USB OTG HS DMA 通过此总线向存储器加载/存储数据。

对于系统架构的知识,在刚开始学习 STM32 的时候只需要一个大概的了解,大致知道是个

什么情况即可。对于寻址之类的知识,这里就不做深入的讲解,中文参考手册都有很详细的讲

解。

4.3 STM32F4 时钟系统

STM32F4 时钟系统的知识在《STM32F4 中文参考手册》第六章复位和时钟控制章节有非

常详细的讲解,网上关于时钟系统的讲解也基本都是参考的这里,讲不出啥特色,不过作为一

个完整的参考手册,我们必然要提到时钟系统的知识。这些知识也不是什么原创,纯粹根据官

方提供的中文参考手册和自己的应用心得来总结的,如有不合理之处望大家谅解。

这部分内容我们分 3 个小节来讲解:

·4.3.1 STM32F4 时钟树概述

·4.3.2 STM32F4 时钟初始化配置

·4.3.3 STM32F4 时钟使能和配置

4.3.1 STM32F4 时钟树概述

众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而

喻了。 STM32F4 的时钟系统比较复杂,不像简单的 51 单片机一个系统时钟就可以解决一切。

于是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首

先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,

比如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁

干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。

首先让我们来看看 STM32F4 的时钟系统图:

图 4.3.1.1STM32 时钟系统图

在 STM32F4 中,有 5 个最重要的时钟源,为 HSI、HSE、LSI、LSE、PLL。其中 PLL 实

际是分为两个时钟源,分别为主 PLL 和专用 PLL。从时钟频率来分可以分为高速时钟源和低速

时钟源,在这 5 个中 HSI,HSE 以及 PLL 是高速时钟,LSI 和 LSE 是低速时钟。从来源可分为

外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 HSE 和

LSE 是外部时钟源,其他的是内部时钟源。下面我们看看 STM32F4 的这 5 个时钟源,我们讲

解顺序是按图中红圈标示的顺序:①、LSI 是低速内部时钟,RC 振荡器,频率为 32kHz 左右。供独立看门狗和自动唤醒单元使用。

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

③、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为 4MHz~26MHz。

我们的开发板接的是 8M 的晶振。HSE 也可以直接做为系统时钟或者 PLL 输入。

④、HSI 是高速内部时钟,RC 振荡器,频率为 16MHz。可以直接作为系统时钟或者用作 PLL

输入。

⑤、PLL 为锁相环倍频输出。STM32F4 有两个 PLL:

1) 主 PLL(PLL)由 HSE 或者 HSI 提供时钟信号,并具有两个不同的输出时钟。

第一个输出 PLLP 用于生成高速的系统时钟(最高 168MHz)

第二个输出 PLLQ 用于生成 USB OTG FS 的时钟(48MHz),随机数发生器的时钟和 SDIO

时钟。

2)专用 PLL(PLLI2S)用于生成精确时钟,从而在 I2S 接口实现高品质音频性能。

这里我们着重看看主 PLL 时钟第一个高速时钟输出 PLLP 的计算方法。图 4.3.1.2 是主 PLL 的

时钟图。

图 4.3.1.2 STM32F4 主 PLL 时钟图

从图 4.3.1.2 可以看出。主 PLL 时钟的时钟源要先经过一个分频系数为 M 的分频器,然后经过

倍频系数为 N 的倍频器出来之后的时候还需要经过一个分频系数为 P(第一个输出 PLLP)或

者 Q(第二个输出 PLLQ)的分频器分频之后,最后才生成最终的主 PLL 时钟。

例如我们的外部晶振选择 8MHz。同时我们设置相应的分频器 M=8,倍频器倍频系数 N=336,

分频器分频系数 P=2,那么主 PLL 生成的第一个输出高速时钟 PLLP 为:

PLL=8MHz * N/ (M*P)=8MHz* 336 /(8*2) = 168MHz

如果我们选择HSE为PLL时钟源,同时SYSCLK时钟源为PLL,那么SYSCLK时钟为 168MHz。

这对于我们后面的实验都是采用这样的配置。

上面我们简要概括了 STM32 的时钟源,那么这 5 个时钟源是怎么给各个外设以及系统提

供时钟的呢?这里我们选择一些比较常用的时钟知识来讲解。

图 4.3.1.1 中我们用 A~G 标示我们要讲解的地方。

A.

这里是看门狗时钟输入。从图中可以看出,看门狗时钟源只能是低速的 LSI 时钟。

B.

这里是 RTC 时钟源,从图上可以看出,RTC 的时钟源可以选择 LSI,LSE,以及

HSE 分频后的时钟,HSE 分频系数为 2~31。

C.

这里是 STM32F4 输出时钟 MCO1 和 MCO2。MCO1 是向芯片的 PA8 引脚输出时

钟。它有四个时钟来源分别为:HSI,LSE,HSE 和 PLL 时钟。MCO2 是向芯片的PC9 输出时钟,它同样有四个时钟来源分别为:HSE,PLL,SYSCLK 以及 PLLI2S

时钟。MCO 输出时钟频率最大不超过 100MHz。

D.

这里是系统时钟。从图 4.3.1 可以看出,SYSCLK 系统时钟来源有三个方面:

HSI,HSE 和 PLL。在我们实际应用中,因为对时钟速度要求都比较高我们才会选

用 STM32F4 这种级别的处理器,所以一般情况下,都是采用 PLL 作为 SYSCLK

时钟源。根据前面的计算公式,大家就可以算出你的系统的 SYSCLK 是多少。

E.

这里我们指的是以太网 PTP 时钟,AHB 时钟,APB2 高速时钟,APB1 低速时钟。

这些时钟都是来源于 SYSCLK 系统时钟。其中以太网 PTP 时钟是使用系统时钟。

AHB,APB2 和 APB1 时钟是经过 SYSCLK 时钟分频得来。这里大家记住,AHB

最大时钟为168MHz, APB2高速时钟最大频率为84MHz,而APB1低速时钟最大频

率为 42MHz。

F.

这里是指 I2S 时钟源。从图 4.3.1 可以看出,I2S 的时钟源来源于 PLLI2S 或者映

射到 I2S_CKIN 引脚的外部时钟。I2S 出于音质的考虑,对时钟精度要求很高。探

索者 STM32F4 开发板使用的是内部 PLLI2SCLK。

G.

这是 STM32F4 内部以太网 MAC 时钟的来源。对于 MII 接口来说,必须向外部

PHY 芯片提供 25Mhz 的时钟,这个时钟,可以由 PHY 芯片外接晶振,或者使用

STM32F4 的 MCO 输 出 来 提 供 。 然 后 , PHY 芯 片 再 给 STM32F4 提 供

ETH_MII_TX_CLK 和 ETH_MII_RX_CLK 时钟。对于 RMII 接口来说,外部必须

提供 50Mhz 的时钟驱动 PHY 和 STM32F4 的 ETH_RMII_REF_CLK,这个 50Mhz

时钟可以来自 PHY、有源晶振或者 STM32F4 的 MCO。我们的开发板使用的是

RMII 接 口 , 使 用 PHY 芯 片 提 供 50Mhz 时 钟 驱 动 STM32F4 的

ETH_RMII_REF_CLK。

H.

这里是指外部 PHY 提供的 USB OTG HS(60MHZ)时钟。

这里还需要说明一下,Cortex 系统定时器 Systick 的时钟源可以是 AHB 时钟 HCLK 或

HCLK 的 8 分频。具体配置请参考 Systick 定时器配置,我们后面会在 5.1 小节讲解 delay 文件

夹代码的时候讲解。

在以上的时钟输出中,有很多是带使能控制的,例如 AHB 总线时钟、内核时钟、各种 APB1

外设、APB2 外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。后面我们讲解

实例的时候会讲解到时钟使能的方法。

4.3.2 STM32F4 时钟初始化配置

上一小节我们对 STM32F407 时钟树进行了详细讲解,接下来我们来讲解通过 STM32F4 的

HAL 库进行 STM32F407 时钟系统配置步骤。实际上,STM32F4 的时钟系统配置也可以通过图

形化配置工具 STM32CubeMX 来配置生成,这里我们讲解初始化代码,是为了让大家对 STM32

时钟系统有更加清晰的理解。图形化配置工具 STM32CubeMX 在另外的 Cube MX 教程,大家

可以对比参考学习。

前面我们讲解过,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系

统一些初始化配置。那么我们先来看看 SystemInit 程序:

void SystemInit(void){ /* FPU settings ------------------------------------------------------------*/ #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)、 SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */ #endif /* Reset the RCC clock configuration to the default reset state ------------*/ /* Set HSION bit */ RCC->CR |= (uint32_t)0x; /* Reset CFGR register */ RCC->CFGR = 0x; /* Reset HSEON, CSSON and PLLON bits */ RCC->CR &= (uint32_t)0xFEF6FFFF; /* Reset PLLCFGR register */ RCC->PLLCFGR = 0x; /* Reset HSEBYP bit */ RCC->CR &= (uint32_t)0xFFFBFFFF; /* Disable all interrupts */ RCC->CIR = 0x;#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM) SystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */ /* Configure the Vector Table location add offset address ------------------*/#ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */#else SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */#endif}

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

1) FPU 设置

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

3) 外部存储器配置

4) 中断向量表地址配置

HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。HAL

库的 SystemInit 函数除了打开 HSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们必须编

写自己的时钟配置函数。首先我们打开工程模板看看我们在工程 SYSTEM 分组下面定义的 sys.c

文件中的时钟初始化函数 Stm32_Clock_Init 的内容:

//时钟系统配置函数//Fvco=Fs*(plln/pllm);//SYSCLK=Fvco/pllp=Fs*(plln/(pllm*pllp));//Fusb=Fvco/pllq=Fs*(plln/(pllm*pllq));//Fvco:VCO 频率//SYSCLK:系统时钟频率//Fusb:USB,SDIO,RNG 等的时钟频率//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.//外部晶振为 8M 的时候,推荐值:plln=336,pllm=8,pllp=2,pllq=7.//得到:Fvco=8*(336/8)=336Mhz// SYSCLK=336/2=168Mhz// Fusb=336/7=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);//设置调压器输出电压级别 1 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; //主 PLL 和音频 PLL 分频系数(PLL 之前的分频),取值范围:2~63. RCC_OscInitStructure.PLL.PLLN=plln; //主 PLL 倍频系数(PLL 倍频),取值范围:64~432. RCC_OscInitStructure.PLL.PLLP=pllp;//系统时钟的主 PLL 分频系数(PLL 之后的分频),取值范围:2,4,6,8.(仅限这 4 个值!) RCC_OscInitStructure.PLL.PLLQ=pllq; //USB/SDIO/随机数产生器等的主 PLL 分频系数(PLL 之后的分频),取值范围:2~15. ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化 if(ret!=HAL_OK) while(1); //选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2RCC_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_5);//同时设置 FLASH 延时周期为 5WS,也就是 6 个 CPU 周期。if(ret!=HAL_OK) while(1);//STM32F405x/407x/415x/417x Z 版本的器件支持预取功能if (HAL_GetREVID() == 0x1001){__HAL_FLASH_PREFETCH_BUFFER_ENABLE(); //使能 flash 预取}}

从函数注释可知,函数 Stm32_Clock_Init 的作用是进行时钟系统配置,除了配置 PLL 相关

参数确定 SYSCLK 值之外,还配置了 AHB,APB1 和 APB2 的分频系数,也就是确定了 HCLK,

PCLK1 和 PCLK2 的时钟值。我们首先来看看使用 HAL 库配置 STM32F407 时钟系统的一般步

骤:

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 库关键头文件 stm32f4xx_hal_rcc.h 中声明,在文件 stm32f4xx_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; //PL

从 RCC_PLLInitTypeDef;结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及

相关分频倍频参数。

这个结构体的定义我们就不做过多讲解,接下来我们看看我们的时钟初始化函数

Stm32_Clock_Init 中的配置内容:

RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSERCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开 HSERCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打开 PLLRCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE; //PLL 时钟源为 HSERCC_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_5);

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

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

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

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

第五个参数 APB2CLKDivider 配置 APB2 分频系数为 2。

根据我们在主函数中调用 Stm32_Clock_Init(336,8,2,7)时候设置的入口参数值,我们可以计

算出,PLL 时钟为 PLLCLK=HSE*N/M*P=8MHz*336/(8*2)=168MHz,同时我们选择系统时钟

源 为 PLL , 所 以 系 统 时 钟 SYSCLK=168MHz 。 AHB 分 频 系 数 为 1 , 故 其 频 率 为

HCLK=SYSCLK/1=168MHz。APB1 分频系数为 4,故其频率为 PCLK1=HCLK/4=42MHz。APB2

分频系数为 2,故其频率为 PCLK2=HCLK/2=168/2=84MHz。最后我们总结一下通过调用函数

Stm32_Clock_Init(336,8,2,7)之后的关键时钟频率值:

SYSCLK(系统时钟)

=168MHz

PLL 主时钟

=168MHz

AHB 总线时钟(HCLK=SYSCLK/1)

=168MHz

APB1 总线时钟(PCLK1=HCLK/4)

=42MHz

APB2 总线时钟(PCLK2=HCLK/2)

=84MHz

时钟系统配置相关知识就给大家讲解到这里。

4.4 IO 引脚复用器和映射

STM32F4 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO

如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。

这部分知识在《STM32F4 中文参考手册》第七章和芯片数据手册有详细的讲解哪些 GPIO 管脚是

可以复用为哪些内置外设。

对于本小节知识,STM32F4 中文参考手册讲解比较详细,我们同样会从中抽取重要的知识点

罗列出来。同时,我们会以串口使用为例给大家讲解具体的引脚复用的配置。

STM32F4 系列微控制器 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-M4 EVENTOUT 映射到 AF15。

复用器示意图如下图 4.4.1:

图 4.4.1 复用器示意图

接下来,我们简单说明一下这个图要如何看,举个例子,探索者 STM32F407 开发板的原

理图上 PC11 的原理图如图 4.4.2 所示:

图 4.4.2 探索者 STM32F407 开发板 PC11 原理图

如上图所示,PC11 可以作为 SPI3_MISO/U3_RX/U4_RX/SDIO_D3/DCMI_D4/I2S3ext_SD

等复用功能输出,这么多复用功能,如果这些外设都开启了,那么对 STM32F1 来说,那就可

能乱套了,外设之间可互相干扰,但是 STM32F4,由于有复用功能选择功能,可以让 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 口的低八位。

图 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 可以复用为相关外设呢?这

在芯片对应的数据手册(请参考光盘目录:)上面会有详细的表格列出来。对于 STM32F407,数

据手册里面的 Table 9.Alternate function mapping 表格列出了所有的端口 AF 映射表,因为

表格比较大,所以这里只列出 PORTA 的几个端口为例方便大家理解:

表 4.4.4 PORTA 部分端口 AF 映射表

上一节我们讲解了时钟系统配置步骤。在配置好时钟系统之后,如果我们要使用某些外设,

例如 GPIO,ADC 等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前

没有使能外设时钟,这个外设是不可能正常运行的。STM32 的外设时钟使能是在 RCC 相关寄

存器中配置的。因为 RCC 相关寄存器非常多,有兴趣的同学可以直接打开《STM32F4 中文参

考手册》6.3 小节查看所有 RCC 相关寄存器的配置。接下来我们来讲解通过 STM32F4 的 HAL

库使能外设时钟的方法。

在 STM32F4 的 HAL 库中,外设时钟使能操作都是在 RCC 相关 HAL 库文件头文件

stm32f4xx_hal_rcc.h 定义的。大家打开 stm32f4xx_hal_rcc.h 头文件可以看到文件中除了少数几

个函数声明之外大部分都是宏定义标识符。外设时钟使能在 HAL 库中都是通过宏定义标识符

来实现的。首先,我们来看看 GPIOA 的外设时钟使能宏定义标识符:

#define __HAL_RCC_GPIOA_CLK_ENABLE()

do { \

__IO uint32_t tmpreg = 0x00; \

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)0x)

所以,我们很容易理解上面代码的作用是设置寄存器 RCC->AHB1ENR 寄存器的最低位为 1。

我们可以从 STM32F4 的中文参考手册中搜索 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 时钟

对于其他外设,同样都是在 stm32f4xx_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 时钟

关于 STM32F4 的外设时钟使能和禁止方法我们就给大家讲解到这里。

这类函数跟前面讲解的外设时钟函数使用方法基本一致,不同的是一个是用来使能外设时

钟,一个是用来复位对应的外设。这里大家在调用函数的时候一定不要混淆。

对于这些时钟操作函数,我们就不一一列举出来,大家可以打开 RCC 对应的文件仔细了解。

ST32F4 的端口复用和映射就给大家讲解到这里,希望大家课余结合相关实验工程和手册巩

固本小节知识。

4.5 STM32 NVIC 中断优先级管理

CM4 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有 256

级的可编程中断设置。但 STM32F407 并没有使用 CM4 内核的全部东西,而是只用了它的一部

分。STM32F4xx 则总共有 101 个中断,以下仅以 STM32F407xx 为例讲解。

STM32F407xx 的 96 个中断里面,包括 10 个内核中断和 91 个可屏蔽中断,具有 16 级可编

程的中断优先级,而我们常用的就是这 91 个可屏蔽中断。在 MDK 内,与 NVIC 相关的寄存器,

MDK 为其定义了如下的结构体:

typedef struct

{

__IO uint32_t ISER[8];

uint32_t RESERVED0[24];

__IO uint32_t ICER[8];

uint32_t RSERVED1[24];

__IO uint32_t ISPR[8];

uint32_t RESERVED2[24];

__IO uint32_t ICPR[8];

uint32_t RESERVED3[24];

__IO uint32_t IABR[8];

uint32_t RESERVED4[56];

__IO uint8_t IP[240];

uint32_t RESERVED5[644];

__O uint32_t STIR;

} NVIC_Type;

STM32F407 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能

方便的使用 STM32F407 的中断。下面重点介绍这几个寄存器:

ISER[8]:ISER 全称是:Interrupt Set-Enable Registers,这是一个中断使能寄存器组。上面

说了 CM4 内核支持 256 个中断,这里用 8 个 32 位寄存器来控制,每个位控制一个中断。但是

STM32F407 的可屏蔽中断最多只有 91 个,所以对我们来说,有用的就是三个(ISER[0~2]]),

总共可以表示 96 个中断。而 STM32F407 只用了其中的前 91 个。ISER[0]的 bit0~31 分别对应

中断 0~31;ISER[1]的 bit0~32 对应中断 32~63;ISER[2]的 bit0~26 对应中断 64~90;这样总共

91 个中断就分别对应上了。你要使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使

能(这里仅仅是使能,还要配合中断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。

具体每一位对应哪个中断,请参考 STM32F407xx.h 里面的第 84 行处。

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,是一个中断优先级控制的寄存器组。这个寄

存器组相当重要!STM32F407 的中断分组与这个寄存器组密切相关。IP 寄存器组由 240 个 8bit

的寄存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。而 STM32F407

只用到了其中的 91 个。IP[90]~IP[0]分别对应中断 90~0。而每个可屏蔽中断占用的 8bit 并没有

全部使用,而是 只用了高 4 位。这 4 位,又分为抢占优先级和子优先级。抢占优先级在前,子

优先级在后。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。

这里简单介绍一下 STM32F407 的中断分组:STM32F407 将中断分为 5 个组,组 0~4。该

分组的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如表 4.5.1 所示:

表 4.5.1AIRCR 中断优先级分组设置表

通过这个表,我们就可以清楚的看到组 0~4 对应的配置关系,例如组设置为 3,那么此时

所有的 91 个中断,每个中断的中断优先寄存器的高四位中的最高 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 却不可以相互

打断!

通过以上介绍,我们熟悉了 STM32F407 中断设置的大致过程。接下来我们介绍如何使用

HAL 库实现以上中断分组设置以及中断优先级管理,使中断配置简单化。NVIC 中断管理相关

函数主要在 HAL 库关键文件 stm32f4xx_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 函数进行分组设置的。

打开 stm32f4xx_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 库文件 stm32f4xx_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 库,在文件 stm32f4xx_hal.c

内部定义函数 HAL_Init 中有调用 HAL_NVIC_PriorityGroupConfig 函数进行相关设置,所以我

们只需要修改 HAL_Init 内部对中断优先级分组设置即可。

① 设置单个中断的中断优先级别和使能相应中断通道,使用到的函数函数主要为函数

HAL_NVIC_SetPriority 和函数 HAL_NVIC_EnableIRQ。

4.6 MDK 中寄存器地址名称映射分析

之所以要讲解这部分知识,是因为经常会遇到客户提到不明白 HAL 库中那些结构体是怎么

与寄存器地址对应起来的。这里我们就做一个简要的分析吧。

首先我们看看 51 中是怎么做的。51 单片机开发中经常会引用一个 reg51.h 的头文件,下

面我们看看他是怎么把名字和寄存器联系起来的:

sfr P0 =0x80;

sfr 也是一种扩充数据类型,点用一个内存单元,值域为 0~255。利用它可以访问 51 单片

机内部的所有特殊功能寄存器。如用 sfr P1 = 0x90 这一句定义 P1 为 P1 端口在片内的寄存

器。然后我们往地址为 0x80 的寄存器设值的方法是:P0=value;

那么在 STM32 中,是否也可以这样做呢??答案是肯定的。肯定也可以通过同样的方

式来做,但是 STM32 因为寄存器太多太多,如果一一以这样的方式列出来,那要好大的篇

幅,既不方便开发,也显得太杂乱无序的感觉。所以 MDK 采用的方式是通过结构体来将

寄存器组织在一起。下面我们就讲解 MDK 是怎么把结构体和地址对应起来的,为什么我

们修改结构体成员变量的值就可以达到操作对应寄存器的值。这些事情都是在 stm32f4xx.h

文件中完成的。我们通过 GPIOA 的几个寄存器的地址来讲解吧。

首先我们可以查看《STM32F4 中文参考手册》中的寄存器地址映射表(P193)。这里我

们选用 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 基地址了。下面

我们打开 stm32f429.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 + 0x0000)

依次类推,可以找到最顶层:

#define AHB1PERIPH_BASE (PERIPH_BASE + 0x)

#define PERIPH_BASE ((uint32_t)0x)

所以我们便可以算出 GPIOA 的基地址位:

GPIOA_BASE= 0x+0x+0x0000=0x

下面我们再跟《STM32F 中文参考手册》比较一下看看 GPIOA 的基地址是不是 0x 。

截图 P53 存储器映射表我们可以看到,GPIOA 的起始地址也就是基地址确实是 0x:

图 4.6.2 GPIO 存储器地址映射表

同样的道理,我们可以推算出其他外设的基地址。

上面我们已经知道 GPIOA 的基地址,那么那些 GPIOA 的 10 个寄存器的地址又是怎么

算出来的呢?在上面我们讲过 GPIOA 的各个寄存器对于 GPIOA 基地址的偏移地址,所以

我们自然可以算出来每个寄存器的地址。

GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相对 GPIOA 基地址的偏移值

这个偏移值在上面的寄存器地址映像表中可以查到。

那么在结构体里面这些寄存器又是怎么与地址一一对应的呢?这里涉及到结构体成员

变量地址对齐方式方面的知识,这方面的知识大家可以在网上查看相关资料复习一下,这

里我们不做详细讲解。在我们定义好地址对齐方式之后,每个成员变量对应的地址就可以

根据其基地址来计算。对于结构体类型 GPIO_TypeDef,他的所有成员变量都是 32 位,成

员变量地址具有连续性。所以自然而然我们就可以算出 GPIOA 指向的结构体成员变量对应

地址了。

表 4.6.3 GPIOA 各寄存器实际地址表

我们可以把 GPIO_TypeDef 的定义中的成员变量的顺序和 GPIOx 寄存器地址映像对比

可以发现,他们的顺序是一致的,如果不一致,就会导致地址混乱了。

这就是为什么 HAL 库里面:GPIOA->BSRR=value;就是设置地址为 0x

+0x18 (BSRR 偏移量)=0x 的寄存器 BSRR 的值了。它和 51 里面 P0=value 是设置

地址为 0x80 的 P0 寄存器的值是一样的道理。

看到这里你是否会学起来踏实一点呢?STM32 使用的方式虽然跟 51 单片机不一样,

但是原理都是一致的。

4.7 MDK 代码快速组织代码技巧

这一节主要讲解在 MDK 中使用 HAL 库开发的一些小技巧,仅供初学者参考。这节的知识

大家可以在学习第一个跑马灯实验的时候参考一下,对初学者应该很有帮助。我们就用最简单

的 GPIO 初始化函数为例。

现 在 我 们 要 初 始 化 某 个 GPIO 端 口 , 我 们 要 怎 样 快 速 操 作 呢 ? 在 头 文 件

stm32f4xx_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…”,

(前提是打开了“Browse Information”选项,可以参考前面 3.3.2 章节说明,勾选上打开),如

下图 4.7.1:

图 4.7.1 查看类型定义方法

于是定位到 stm32f4xx_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 …”,这样光标定位到 stm32f4xx_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 是设置的哪个寄存器的哪个位,然后去中文参

考手册查看该寄存器相应位的定义以及前后文的描述。

这一节我们就讲解到这里,希望能对大家的开发有帮助。

标签: 时钟

抱歉,评论功能暂时关闭!