嵌入式开发十: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时钟系统的全部内容,如有兴趣,感谢点赞、关注、收藏,若有不正地方,还请各位大佬多多指教!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/609094.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

力扣每日一题-统计已测试设备-2024.5.10

力扣题目&#xff1a;统计已测试设备 题目链接: 2960.统计已测试设备 题目描述 代码思路 根据题目内容&#xff0c;第一感是根据题目模拟整个过程&#xff0c;在每一步中修改所有设备的电量百分比。但稍加思索&#xff0c;发现可以利用已测试设备的数量作为需要减少的设备电…

16地标准化企业申请!安徽省工业和信息化领域标准化示范企业申报条件

安徽省工业和信息化领域标准化示范企业申报条件有哪些&#xff1f;合肥市 、黄山市 、芜湖市、马鞍山、安庆市、淮南市、阜阳市、淮北市、铜陵市、亳州市、宣城市、蚌埠市、六安市 、滁州市 、池州市、宿州市企业申报安徽省工业和信息化领域标准化示范企业有不明白的可在下文了…

机器学习各个算法的优缺点!(上篇) 建议收藏。

下篇地址&#xff1a;机器学习各个算法的优缺点&#xff01;&#xff08;下篇&#xff09; 建议收藏。-CSDN博客 今天有朋友聊起来&#xff0c;机器学习算法繁多&#xff0c;各个算法有各个算法的特点。 以及在不同场景下&#xff0c;不同算法模型能够发挥各自的优点。 今天…

Java之异常处理

一、认识异常 1.异常的概念 在 Java 中&#xff0c;将程序执行过程中发生的不正常行为称为异常。。比如之前写代码时经常遇到的&#xff1a; 1. 算术异常 System.out.println(10 / 0); // 执行结果 Exception in thread "main" java.lang.ArithmeticException: /…

深化产教融合,泰迪智能科技助力西南林业大学提质培优

2024年5月7日&#xff0c;泰迪智能科技昆明分公司院校部总监查良红和数据部负责人余雄亮赴西南林业大学理学院就工作室共建事宜进行交流会谈。西南林业大学理学院院长张雁、党委副书记魏轶、副院长谢爽、就业负责人罗丽及学生代表参与本次交流会。 会议伊始&#xff0c;谢副院长…

FPGA HDMI Sensor无线航模摄像头

FPGA方案&#xff0c;接收摄像头sensor 图像数据后&#xff0c;通过HDMI输出到后端 客户应用&#xff1a;无线航模摄像头 主要特性&#xff1a; 1.支持2K以下任意分辨率格式 2.支持多种型号sensor 3.支持自适应摄像头配置&#xff0c;并补齐输出时序 4.可定制功能&#xff…

休斯《公共管理导论》第4版教材精讲视频网课+考研真题讲解

内容简介 本课程是休斯《公共管理导论》&#xff08;第4版&#xff09;精讲班&#xff0c;为了帮助参加研究生招生考试指定考研参考书目为休斯《公共管理导论》&#xff08;第4版&#xff09;的考生复习专业课&#xff0c;我们根据教材和名校考研真题的命题规律精心讲解教材章节…

HR招聘面试测评,如何判断候选人的创新能力?

创新能力代表着一个人的未来发展潜力&#xff0c;创新能力突出的人&#xff0c;未来的上限就可能更高。而对于一个公司而言&#xff0c;一个具有创新能力的员工&#xff0c;会给公司带来新方案&#xff0c;新思路&#xff0c;对公司的长远发展拥有着十分积极的作用。 而在挑选…

【讲解下迭代加深搜索】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

科林算法_3 图

一、图论基础 多对多的关系 定义&#xff1a;G(V,E) Vertex顶点 Edge边 顶点的集合V{v1,v2} 边的结合E{(v1,v2)} 无向图(1,2) 有向图<1,2> 依附&#xff1a;边(v1,v2)依附于顶点v1,v2 路径&#xff1a;&#xff08;v1,v2)(v2,v3) 无权路径最短&#xff1a;边最少…

深入了解 Flask Request

文章目录 获取请求数据获取请求信息文件上传总结 Flask 是一个轻量级的 Python Web 框架&#xff0c;其简洁的设计和灵活的扩展性使其成为了许多开发者的首选。在 Flask 中&#xff0c;处理 HTTP 请求是至关重要的&#xff0c;而 Flask 提供了丰富而强大的 request 对象来处理…

【限时免费,手慢无】Unity 怪物资源包,MONSTER 动作超丰富,不领后悔!

Unity 怪物资源包&#xff0c;MONSTER 动作超丰富 前言资源包内容领取兑换码 前言 &#x1f47e; 突破想象&#xff01;惊艳众人的怪物模型登场 &#x1f47e; 今天要向大家介绍一款令人瞩目的游戏怪物模型&#xff01;这个看似丑陋的小怪物&#xff0c;却有着巨大的潜力&…

代码随想录刷题随记31-贪心5

代码随想录刷题随记31-贪心5 435. 无重叠区间 leetcode链接 按照右边界排序&#xff0c;从左向右记录非交叉区间的个数。 此时问题就是要求非交叉区间的最大个数。 这里记录非交叉区间的个数还是有技巧的&#xff0c;如图&#xff1a; 左边界排序可不可以呢&#xff1f; 也是…

前缀索引与单列联合索引的选择

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;面经 ⛺️稳中求进&#xff0c;晒太阳 前缀索引 当字段类型为字符串(varchar,text等) 时&#xff0c;有时候需要索引很长的字符串&#xff0c;这会让索引变得很大。查询的时候浪费大量的磁…

能恢复永久删除文件的十大数据恢复软件

当您不小心删除了重要数据&#xff0c;或者由于病毒攻击而丢失了重要数据时&#xff0c;请不要惊慌&#xff0c;我们已经为您准备好了。别无他处&#xff0c;这是您目前市场上最佳数据恢复软件列表的一站式目的地。 能恢复永久删除文件的十大数据恢复软件 1. 奇客数据恢复 这是…

教大家一键下载1688图片信息

电商图片是商品的视觉身份证&#xff0c;对销量有着决定性影响。它们不仅是展示产品&#xff0c;更是讲述品牌故事&#xff0c;激发情感共鸣的工具。高质量的图片能瞬间吸引顾客目光&#xff0c;准确传达产品细节&#xff0c;建立起顾客的信任与购买意愿。在无法亲身体验商品的…

使用网站内容进行多渠道品牌营销的主要优势

在选择服务提供商时&#xff0c;人们使用不同的方式来查找信息并与他们联系。有些人更喜欢网站&#xff0c;有些人则使用社交媒体或电子邮件。网站对于数字存在仍然至关重要&#xff0c;但跨多个渠道管理内容现在至关重要。多渠道营销以客户喜欢的方式与客户建立联系&#xff0…

mysql安装及基础设置

关系型数据库 MySQL是一种关系型数据库管理系统&#xff0c;采用了关系模型来组织数据的数据库&#xff0c;关系数据库将数据保存在不同的表中&#xff0c;用户通过查询 sql 来检索数据库中的数据。 yum 方式安装 mysql # yum -y install mysql-server # systemctl start my…

##12 深入了解正则化与超参数调优:提升神经网络性能的关键策略

文章目录 前言1. 正则化技术的重要性1.1 L1和L2正则化1.2 Dropout1.3 批量归一化 2. 超参数调优技术2.1 网格搜索2.2 随机搜索2.3 贝叶斯优化 3. 实践案例3.1 设置实验3.2 训练和测试 4. 结论 前言 在深度学习中&#xff0c;构建一个高性能的模型不仅需要一个好的架构&#xf…

《这就是ChatGPT》读书笔记

书名&#xff1a;这就是ChatGPT 作者&#xff1a;[美] 斯蒂芬沃尔弗拉姆&#xff08;Stephen Wolfram&#xff09; ChatGPT在做什么&#xff1f; ChatGPT可以生成类似于人类书写的文本&#xff0c;它基本任务是弄清楚如何针对它得到的任何文本产生“合理的延续”。当ChatGPT写…