嵌入式开发十:STM32开发基础入门知识补充

        本篇博客主要是针对前面STM32入门基础知识的补充,为后面的真正开发学习做好准备。

目录

一、IO 引脚复用器和映射

 1.1 引脚复用的概念

1.2 如何设计实现复用

1.3  复用功能固件库配置过程

二、STM32 NVIC 中断优先级管理

 2.1 NVIC中断优先级管理结构体介绍    

 2.2  NVIC中断优先级管理结构体的成员(寄存器)介绍

 2.3 中断优先级分组

2.4 使用库函数实现以上中断分组设置以及中断优先级管理

2.5 中断优先级设置的步骤

三、MDK 固件库快速组织代码技巧

3.1 如何查看初始化函数的参数对应结构体的成员变量的值

3.2 如何快速查看外设是挂载在哪个总线之下的?


一、IO 引脚复用器和映射

        这部分知识在《STM32F4 中文参考手册》第七章和芯片数据手册有详细的讲解哪些 GPIO 管脚是可以复用为哪些内置外设。 对于本小节知识,STM32F4 中文参考手册讲解比较详细,我同样会从中抽取重要的知识点 罗列出来。同时,我会以串口使用为例给大家讲解具体的引脚复用的配置。

 1.1 引脚复用的概念

        STM32F4 有很多的内置外设(内核里面),这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO 如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。

1.2 如何设计实现复用

        STM32F4 系列微控制器 IO 引脚通过一个复用器连接到内置外设或模块。该复用器一次只允 许一个外设的复用功能(AF)连接到对应的 IO 口。这样可以确保共用同一个 IO 引脚的外设之间不会发生冲突。每个 IO 引脚(16个口)都有一个复用器,该复用器采用 16 路复用功能输入(AF0 到 AF15),可通过 GPIOx_AFRL(针对引脚 0-7)和 GPIOx_AFRH(针对引脚 8-15)寄存器对这些输入进行配置,每四位控制一路复用:

1)完成复位后,所有 IO 都会连接到系统的复用功能 0(AF0)。

2)外设的复用功能映射到 AF1 到 AF13。

3)Cortex-M4 EVENTOUT 映射到 AF15。

复用器示意图如下图 :

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

如上图所示,PC11 可以作为 SPI3_MISO/U3_RX/U4_RX/SDIO_D3/DCMI_D4/I2S3ext_SD 等复用功能输出,这么多复用功能,如果这些外设都开启了,那么对 STM32F4来说,那就可能乱套了,外设之间可互相干扰,但是 STM32F4,由于有复用功能选择功能,可以让 PC11 仅连接到某个特定的外设,因此不存在互相干扰的情况。

上图是针对引脚 0-7,对于引脚 8-15,控制寄存器为 GPIOx_AFRH。从图中可以看出。 当需要使用复用功能的时候,我们配置相应的寄存器 GPIOx_AFRL 或者 GPIOx_AFRH,让对应引脚通过复用器连接到对应的复用功能外设。这里我们列出 GPIOx_AFRL 寄存器的描述, GPIOx_AFRH 的作用跟 GPIOx_AFRL 类似,只不过 GPIOx_AFRH 控制的是一组 IO 口的高八位, GPIOx_AFRL 控制的是一组 IO 口的低八位。

从表中可以看出,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 的几个端口为例方便大家理解:

从表 可以看出,PA9 连接 AF7 可以复用为串口 1 的发送引脚 USART1_TX,PA10 连接 AF7 可以复用为串口 2 的接受引脚 USART1_RX。接下来我们以串口 1 为例来讲解怎么配置 GPOPA.9,GPIOA.10 口为串口 1 复用功能。

1.3  复用功能固件库配置过程

第一步:首先,我们要使用 IO 复用功能外设,必须先打开对应的 IO 时钟和复用功能外设时钟。

/*使能 GPIOA 时钟*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); 
/*使能 USART1 时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);

 这里需要说明一下,官方库提供了五个打开 GPIO 和外设时钟的函数分别为:

void RCC_AHB1PeriphClockCmd(uint32_t RCC_AHB1Periph, FunctionalState NewState);
void RCC_AHB2PeriphClockCmd(uint32_t RCC_AHB2Periph, FunctionalState NewState);
void RCC_AHB3PeriphClockCmd(uint32_t RCC_AHB3Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

这五个函数分别用来打开相应的总线下 GPIO 和外设时钟。比如我们的串口 1 是挂载在 APB2 总线之下,所以我们调用对应的 APB2 总线下外设时钟使能函数 RCC_APB2PeriphClockCmd 来使能串口 1 时钟。对于其他外设我们调用相应的函数即可。具体库函数要怎么快速找到对应 的外设使能函数,大家可以参考我们接下来的快速组织代码技巧,我有详细的举例说明。

第二步: 其次,我们在 GIPOx_MODER 寄存器中将所需 IO(对于串口 1 是 PA9,PA10)配置为复用功能(ADC 和 DAC 设置为模拟通道)

第三步:再次,我们还需要对 IO 口的其他参数,例如类型,上拉/下拉以及输出速度。

上面两步,在我们库函数中是通过 GPIO_Init 函数来实现的,参考代码如下:

GPIOA9 与 GPIOA10 初始化

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; 
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度 50MHz
 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
 GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化 PA9,PA10

 第四步:最后,我们配置 GPIOx_AFRL 或者 GPIOx_AFRH 寄存器,将 IO 连接到所需的 AFx。 这些步骤对于我们使用库函数来操作的话,是调用的 GPIO_PinAFConfig 函数来实现的。具 体操作代码如下:

/*PA9 连接 AF7,复用为 USART1_TX */
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); 
/* PA10 连接 AF7,复用为 USART1_RX*/
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); 

 对于函数 GPIO_PinAFConfig 函数,入口第一个第二个参数很好理解,可以确定是哪个 IO, 对于第三个参数,实际上我们确定了这个 IO 到底是复用为哪种功能之后,这个参数也很好选 择,因为可选的参数在 stm32f4xx_gpio.h 列出来非常详细,如下:

参考这些宏定义标识符,能很快找到函数的入口参数。 ST32F4 的端口复用和映射就给大家讲解到这里,希望大家课余结合相关实验工程和手册巩固本小节知识。 

二、STM32 NVIC 中断优先级管理

 2.1 NVIC中断优先级管理结构体介绍    

       CM4 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有 256 级的可编程中断设置。但 STM32F4 并没有使用 CM4 内核的全部东西,而是只用了它的一 部分。          STM32F40xx总共有 92 个中断, 在 92 个中断里面,包括 10 个内核中断和 82 个可屏蔽中断,具有16 级可编程的中断优先级,而我们常用的就是这 82 个可屏蔽中断。在 MDK 内,与 NVIC 相关的寄存器,MDK 为其定义了如下的结构体:

typedef struct
{
 __IO uint32_t ISER[8]; /*!< Interrupt Set Enable Register */
 uint32_t RESERVED0[24];
 __IO uint32_t ICER[8]; /*!< Interrupt Clear Enable Register */
 uint32_t RSERVED1[24];
 __IO uint32_t ISPR[8]; /*!< Interrupt Set Pending Register */
 uint32_t RESERVED2[24];
 __IO uint32_t ICPR[8]; /*!< Interrupt Clear Pending Register */
 uint32_t RESERVED3[24];
 __IO uint32_t IABR[8]; /*!< Interrupt Active bit Register */
 uint32_t RESERVED4[56];
 __IO uint8_t IP[240]; /*!< Interrupt Priority Register, 8Bit wide */
 uint32_t RESERVED5[644];
 __O uint32_t STIR; /*!< Software Trigger Interrupt Register */
} NVIC_Type;

STM32F4 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便的使用 STM32F4 的中断。

 2.2  NVIC中断优先级管理结构体的成员(寄存器)介绍

下面重点介绍这几个寄存器: 

ISER[8]:ISER 全称是:Interrupt Set-Enable Registers,这是一个中断使能寄存器组。上面 说了 CM4 内核支持 256 个中断,这里用 8 个 32 位寄存器来控制,每个位控制一个中断。但是 STM32F4 的可屏蔽中断最多只有 82 个,所以对我们来说,有用的就是三个(ISER[0~2]),总共可以表示 96 个中断。而 STM32F4 只用了其中的前 82 个。ISER[0]的 bit0~31 分别对应中断 0~31;ISER[1]的 bit0~32 对应中断 32~63;ISER[2]的 bit0~17 对应中断 64~81;这样总共 82 个 中断就分别对应上了。你要使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能(这 里仅仅是使能,还要配合中断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。具 体每一位对应哪个中断,请参考 stm32f4xx.h 里面的第 188 行处。

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,是一个中断优先级控制的寄存器组。这个寄 存器组相当重要!STM32F4 的中断分组与这个寄存器组密切相关。IP 寄存器组由 240 个 8bit 的寄存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。而 STM32F4 只用到了其中的 82 个。IP[81]~IP[0]分别对应中断 81~0。而每个可屏蔽中断占用的 8bit 并没有全部使用,而是只用了高 4 位。这 4 位,又分为抢占优先级和响应优先级。抢占优先级在前, 响应优先级在后。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。

 2.3 中断优先级分组

      这里简单介绍一下 STM32F4 的中断分组:STM32F4 将中断分为 5 个组,组 0~4。该分组 的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如表 所示:

通过这个表,我们就可以清楚的看到组 0~4 对应的配置关系,例如组设置为 3,那么此时所有的 82 个中断,每个中断的中断优先寄存器的高四位中的最高 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 却不可以相互 打断!

     通过以上介绍,我们熟悉了 STM32F4 中断设置的大致过程。 

2.4 使用库函数实现以上中断分组设置以及中断优先级管理

         接下来我们介绍如何使用库函数实现以上中断分组设置以及中断优先级管理,使得我们以后的中断设置简单化NVIC 中断管理函数主要在 misc.c 文件里面。

1.  中断优先级分组函数 NVIC_PriorityGroupConfig

       首先要讲解的是中断优先级分组函数 NVIC_PriorityGroupConfig,其函数申明如下:这个函数的作用是对中断的优先级进行分组,这个函数在系统中只能被调用一次,一旦分 组确定就最好不要更改。这个函数我们可以找到其实现:

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
//具体实现
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{
    assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));
    SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}

       从函数体可以看出,这个函数唯一目的就是通过设置 SCB->AIRCR 寄存器来设置中断优先级分组,这在前面寄存器讲解的过程中已经讲到。而其入口参数通过双击选中函数体里面的 “IS_NVIC_PRIORITY_GROUP”然后右键“Go to defition of …”可以查看到为:这也是我们上面表 讲解的,分组范围为 0-4。

#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))

比如我们设置整个系统的中断优先级分组值 为 2,那么方法是:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

这样就确定了一共为“2 位抢占优先级,2 位响应优先级”。设置好了系统中断分组,那么对于每个中断我们又怎么确定他的抢占优先级和响应优先级呢?

2. 中断初始化函数 NVIC_Init   

下面我们讲解一个重要的函数为中断初始化函数 NVIC_Init,其函数申明为:

void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)

 其中 NVIC_InitTypeDef 是一个结构体,我们可以看看结构体的成员变量:

typedef struct
{
 uint8_t NVIC_IRQChannel; 
 uint8_t NVIC_IRQChannelPreemptionPriority;
 uint8_t NVIC_IRQChannelSubPriority; 
 FunctionalState NVIC_IRQChannelCmd; 
} NVIC_InitTypeDef;

       NVIC_InitTypeDef 结构体中间有四个成员变量,接下来我们一一来看看这些成员变量的含 义。

  1. NVIC_IRQChannel:定义初始化的是哪个中断,这个我们可以在 stm32f4xx.h 中找到每个中断对应的名字。例如 USART1_IRQn。
  2. NVIC_IRQChannelPreemptionPriority:定义这个中断的抢占优先级别。
  3. NVIC_IRQChannelSubPriority:定义这个中断的子优先级别,也叫响应优先级。
  4. NVIC_IRQChannelCmd:该中断通道是否使能。

比如我们要使能串口 1 的中断,同时设置抢占优先级为 1,响应优先级位 2,初始化的方法 是:

NVIC_InitTypeDef NVIC_InitStructure;;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口 1 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;// 抢占优先级为 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;// 响应优先级位 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); //根据上面指定的参数初始化 NVIC 寄存器

 这里我们讲解了中断分组的概念以及设置单个中断优先级的方法。对于每个中断,还有一些类似清除中断,查看中断状态的操作,这在后面我们讲解每个中断的时候会详细讲解怎么使用。

2.5 中断优先级设置的步骤

1. 系统运行开始的时候设置中断分组。确定组号,也就是确定抢占优先级和响应优先级 的分配位数。调用函数为 NVIC_PriorityGroupConfig();

2. 设置所用到的中断的中断优先级别。对每个中断调用函数为 NVIC_Init();

三、MDK 固件库快速组织代码技巧

       这一节主要讲解在使用 MDK 固件库开发的时候的一些小技巧,仅供初学者参考。这节的知识对初学者应该很有帮助。我们就用最简单的 GPIO 初始化函数为例。

3.1 如何查看初始化函数的参数对应结构体的成员变量的值

       现在我们要初始化某个 GPIO 端口,我们要怎样快速操作呢?现在我们想写初始化函数,那么我们在不参考其他代码的前提下,怎么组织代码呢? 

在头文件 stm32f4xx_gpio.h 头文件中,定义 GPIO 初始化函数为:
 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct); 

      首先,我们可以看出,函数的入口参数是 GPIO_TypeDef 类型指针和 GPIO_InitTypeDef 类 型 指 针 , 因 为 GPIO_TypeDef 入 口 参 数 比 较 简 单 , 所 以 我 们 通 过 第 二 个 入 口 参 数 GPIO_InitTypeDef 类型指针来讲解。双击 GPIO_InitTypeDef 后右键选择“Go to definition…”,如 下图 :

于是定位到 stm32f4xx_gpio.h 中 GPIO_InitTypeDef 的定义处:

typedef struct
{
 uint32_t GPIO_Pin; 
 GPIOMode_TypeDef GPIO_Mode; 
 GPIOSpeed_TypeDef GPIO_Speed; 
 GPIOOType_TypeDef GPIO_OType; 
 GPIOPuPd_TypeDef GPIO_PuPd; 
}GPIO_InitTypeDef;

      可以看到这个结构体有 5 个成员变量,这也告诉我们一个信息:

一个 GPIO 口的状态是由:模式 (GPIO_Mode),速度(GPIO_Speed),输出类型(GPIO_OType)以及上下来属性(GPIO_PuPd) 来决定的。

因此,我们首先要定义一个结构体变量,下面我们定义:

GPIO_InitTypeDef GPIO_InitStructure;

接着我们要初始化结构体变量 GPIO_InitStructure。首先我们要初始化成员变量 GPIO_Pin,这个 时候我们就有点迷糊了,这个变量到底可以设置哪些值呢?这些值的范围有什么规定吗? 这里我们就要找到 GPIO_Init()函数的实现处,同样,双击 GPIO_Init,右键点击“Go to definition of …”,这样光标定位到 stm32f4xx_gpio.c 文件中的 GPIO_Init 函数体开始处,我们可以看到在函数的开始处有如下几行:

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
    …
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
assert_param(IS_GPIO_PUPD(GPIO_InitStruct->GPIO_PuPd));
……
 assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
……
assert_param(IS_GPIO_OTYPE(GPIO_InitStruct->GPIO_OType));
……
}

顾名思义,assert_param 函数式对入口参数的有效性进行判断,所以我们可以从这个函数入手, 确定我们的入口参数的范围。第一行是对第一个参数 GPIOx 进行有效性判断,双击 “IS_GPIO_ALL_PERIPH”右键点击“go to defition of…” 定位到了下面的定义:

#define IS_GPIO_ALL_PERIPH(PERIPH) (((PERIPH) == GPIOA) || \
 ((PERIPH) == GPIOB) || \
((PERIPH) == GPIOC) || \
 ((PERIPH) == GPIOD) || \
((PERIPH) == GPIOE) || \
((PERIPH) == GPIOF) || \
((PERIPH) == GPIOG) || \
 ((PERIPH) == GPIOH) || \
 ((PERIPH) == GPIOI) || \
((PERIPH) == GPIOJ) || \
((PERIPH) == GPIOK))

很明显可以看出,GPIOx 的取值规定只允许是 GPIOA~GPIOK。同样的办法,我们双击“IS_GPIO_MODE” 右键点击“go to defition of…”,定位到下面的定义:

typedef enum
{ 
 GPIO_Mode_IN = 0x00, /*!< GPIO Input Mode */
 GPIO_Mode_OUT = 0x01, /*!< GPIO Output Mode */
 GPIO_Mode_AF = 0x02, /*!< GPIO Alternate function Mode */
 GPIO_Mode_AN = 0x03 /*!< GPIO Analog Mode */
}GPIOMode_TypeDef;
#define IS_GPIO_MODE(MODE) (((MODE) == GPIO_Mode_IN) || 
((MODE) == GPIO_Mode_OUT) || \
 ((MODE) == GPIO_Mode_AF)||
((MODE) == GPIO_Mode_AN))

所以 GPIO_InitStruct->GPIO_Mode 成员的取值范围只能是上面定义的 4 种。这 4 种模式是通过 一个枚举类型组织在一起的。 同样的方法我们双击“IS_GPIO_PIN” 右键点击“go to defition of…”,定位到下面的定义:

#define IS_GPIO_PIN(PIN) ((((PIN) & (uint16_t)0x00) == 0x00) && ((PIN) != 
(uint16_t)0x00))

可以看出,GPIO_Pin 成员变量的取值范围为 0x0000 到 0xffff,那么是不是我们写代码初始化就 是直接给一个 16 位的数字呢?这也是可以的,但是大多数情况下,MDK 不会让你直接在入口 参数处设置一个简单的数字,因为这样代码的可读性太差,MDK 会将这些数字的意思通过宏 定义定义出来,这样可读性大大增强。我们可以看到在 IS_GPIO_PIN(PIN)宏定义的上面还有数 行宏定义:

#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */
#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */
#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */
#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */
……
#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */
#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */
#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */
#define IS_GPIO_PIN(PIN) ((((PIN) & (uint16_t)0x00) == 0x00) && ((PIN) != 
(uint16_t)0x00))

这些宏定义 GPIO_Pin_0~GPIO_Pin_ All 就是 MDK 事先定义好的,我们写代码的时候初始化 GPIO_Pin 的时候入口参数可以是这些宏定义。对于这种情况,MDK 一般把取值范围的宏定义 放在判断有效性语句的上方,这样是为了方便大家查找。讲到这里,我们基本对 GPIO_Init 的入口参数有比较详细的了解了。于是我们可以组织起 来下面的代码:

GPIO_InitTypeDef GPIO_InitStructure;
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 ;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
 GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化

大家会觉得上面讲解有点麻烦,每次要去查找 assert_param()这个函数去寻找,那么有没有 更好的办法呢?大家可以打开 GPIO_InitTypeDef 结构体定义: 

 从上图的结构体成员后面的注释我们可以看出 GPIO_Mode 的意思是 :

“Specifies the operating mode for the selected pins.

This parameter can be a value of @ref GPIOMode_TypeDef”。

      从这段注释可以看出 GPIO_Mode 的取值为 GPIOMode_TypeDef 枚举类型的枚举值,大家同样可以用之前讲解的方法右键双击“GPIOMode_TypeDef”选择“Go to definition of …”即可查看 其取值范围。如果要确定详细的信息呢我们就得去查看手册了。对于去查看手册的哪个地方, 你可以在函数 GPIO_Init()的函数体中搜索 GPIO_Mode 关键字,然后查看库函数设置 GPIO_Mode 是设置的哪个寄存器的哪个位,然后去中文参考手册查看该寄存器相应位的定义以 及前后文的描述。

        接着又有一个问题会被提出来,这个初始化函数一次只能初始化一个 IO 口吗?我要同时初始化很多个 IO 口,是不是要复制很多次这样的初始化代码呢? 这里又有一个小技巧了。从上面的 GPIO_Pin_x 的宏定义我们可以看出,这些值是 0,1,2,4 这样的数字,所以每个 IO 口选定都是对应着一个位,16 位的数据一共对应 16 个 IO 口。这个 位为 0 那么这个对应的 IO 口不选定,这个位为 1 对应的 IO 口选定。

如果多个 IO 口,他们都是对应同一个 GPIOx,那么我们可以通过|(或)的方式同时初始化多个 IO 口。这样操作的前提是,他们的 Mode 和 Speed 参数相同,因为 Mode 和 Speed 参数并不能一次定义多种。所以 初始化多个 IO 口的方式可以是如下:

 GPIO_InitTypeDef GPIO_InitStructure;
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10| GPIO_Pin_11;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
 GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化

对于那些参数可以通过|(或)的方式连接,这既有章可循,同时也靠大家在开发过程中不断积累。 

3.2 如何快速查看外设是挂载在哪个总线之下的?

         如果每次使能时钟的时候都要去查看时钟树看那些外设是挂载在那个总线之下的,这好麻烦。学到这里我相信大家就可以很快速的解决这个问题了。 在 stm32f4xx.h 文件里面我们可以看到如下的宏定义:

#define RCC_AHB1Periph_GPIOA ((uint32_t)0x00000001)
#define RCC_AHB1Periph_GPIOB ((uint32_t)0x00000002)
#define RCC_AHB1Periph_GPIOC ((uint32_t)0x00000004)
#define RCC_AHB2Periph_DCMI ((uint32_t)0x00000001)
#define RCC_AHB2Periph_CRYP ((uint32_t)0x00000010)
#define RCC_AHB2Periph_HASH ((uint32_t)0x00000020)
#define RCC_AHB2Periph_RNG ((uint32_t)0x00000040)
#define RCC_APB1Periph_TIM2 ((uint32_t)0x00000001)
#define RCC_APB1Periph_TIM3 ((uint32_t)0x00000002)
#define RCC_APB1Periph_TIM4 ((uint32_t)0x00000004)
#define RCC_APB2Periph_TIM1 ((uint32_t)0x00000001)
#define RCC_APB2Periph_TIM8 ((uint32_t)0x00000002)
#define RCC_APB2Periph_USART1 ((uint32_t)0x00000010)
#define RCC_AHB3Periph_FSMC ((uint32_t)0x00000001)

从上图定义的标识符名称可以很明显的看出 GPIOA~GPIOC 是挂载在 AHB1 下面,TIM2~TIM4 是挂载在 APB1 下面,TIM1 和 TIM8 是挂载在 APB2 下面。

所以在使能 GPIO 的时候记住要调 用 的 是 RCC_AHB1PeriphClockCmd () 函 数 使 能 , 在 使 能 TIM2 的 时 候 调 用 的 是 RCC_APB1PeriphResetCmd()函数使能。

     这一节我们就讲解到这里,希望能对大家的开发有帮助。 以上便是STM32时钟系统的全部内容,如有兴趣,感谢点赞、关注、收藏,若有不正地方,还请各位大佬多多指教!

  • 15
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未来可期,静待花开~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值