【ARM Cortex-M 开发实战指南(基础篇)】第5章 按键

开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F103ZET6

5.1普通方式

5.1.1 普通方式工作原理

按键 GPIO 端口有两个方案可以选择,一是采用上拉输入模式,因为按键在没按下的时候,是默认为高电平的,采且内部上拉模式正好符合这个要求。第二个方案是直接采用浮空输入模式,因为按照硬件电路图,在芯片外部接了上拉电阻,其实就没必要再配置成内部上拉输入模式了,因为在外部上拉与内部上拉效果是一样的。

L5I6Ve.png

5.1.2普通方式实现-标准库

完整代码请参附件,这里只贴出核心代码。

GPIO 初始化配置

void Key_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    /*开启按键端口(PA)的时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; 
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入 
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

Key_GPIO_Confi g() 与 LED 的 GPIO 初始化函数 LED_GPIO_Confi g() 类似,区别只是在这个函数中,要开启的 GPIO 的端口时钟不一样,并且把检测按键用的引脚 Pin 的模式设置为适合按键应用的上拉输入模式(由于接了外部上拉电阻,也可以使用浮空输入,读者可自行修改代码做实验)。若 GPIO 被设置为输入模式,不需要设置 GPIO 端口的最大输出速度,当然,如果配置了这个速度也没关系,GPIO_Init() 函数会自动忽略它。在 RCC_APB2PeriphClockCmd() 和 GPIO_InitStructure.GPIO_Pin 的输入参数设置之中,我们可以用符号“|”,同时配置多个参数。如 :

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG,ENABLE);

输入参数为RCC_APB2Periph_GPIOB| RCC_APB2Periph_GPIOG ,这样调用之后,就把 GPIOB 和 GPIOG 的时钟都开启了。

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;

以上代码则表示将要同时配置 GPIOG 端口的 Pin5 和 Pin6。

按键消抖

uint8_t Key_Scan(GPIO_TypeDef* GPIOx,u16 GPIO_Pin,uint8_t Down_state)
{           
    /*检测是否有按键按下 */
    if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == Down_state ) 
    {      
        /*延时消抖*/
        Key_Delay(10000);       
        if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == Down_state )  
        {    
            /*等待按键释放 */
            while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == Down_state);   
            return  KEY_ON;  
        }
        else
            return KEY_OFF;
    }
    else
        return KEY_OFF;
}

相信延时消抖的原理大家在学习其他单片机时就已经了解了,本函数的功能就是扫描输入参数中指定的引脚,检测其电平变化,并作延时消抖处理,最终对按键消息进行确认。

  • 利用 GPIO_ReadInputDataBit() 读取输入数据,若从相应引脚读取的数据等于 0(KEY_ON),低电平,表明可能有按键按下,调用延时函数。否则返回 KEY_OFF,表示按键没有被按下。

  • 延时之后再次利用 GPIO_ReadInputDataBit() 读取输入数据,若依然为低电平,表明确实有按键被按下了。否则返回 KEY_OFF,表示按键没有被按下。

  • 循环调用 GPIO_ReadInputDataBit() 一直检测按键的电平,直至按键被释放,被释放后,返回表示按键被按下的标志 KEY_ON。以上是按键消抖的流程,调用了一个库函数 GPIO_ReadInputDataBit()。输入参数为要读取的端口、引脚,返回引脚的输入电平状态,高电平为 1,低电平为 0。

5.1.3普通方式实现-HAL库

5.1.3.1 STM32Cube生成工程

关于如何使用STM32Cube新建工程在前文已经讲解过了,这里直说配置GPIO部分内容。本文要实现按键功能,通过按键实现LED的亮灭。我门在第一个程序的基础上进行修改即可,不必每次都新建工程。根据按键电路,KEY1的引脚是PA0,我们将PA0的GPIO设置为下拉的输入模式,保留3个LED的GPIO配置。

L5okGR.md.png

初始化基本配置后,我们重新生成工程,接下来按键编程。

5.1.3.2具体代码分析

在看代码前,我们先看看按键扫描编程的流程:

1)使能按键引脚时钟,本文的引脚是PA0;
2)初始化按键,即初始化GPIO机构体,在前文已经详细讲解过了;
3)在无限循环中不断读取PA0的电平值,同时进行按键消抖;
4)判断按键被按下时,进行相应的处理。

GPIO 初始化配置

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOG_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOG, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);

  /*Configure GPIO pin : PA0 */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /*Configure GPIO pin : PB0 */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /*Configure GPIO pins : PG6 PG7 */
  GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

}

按键与 LED 的 GPIO 初始化函数类似,区别只是在这个函数中,要开启的 GPIO 的端口时钟不一样,并且把检测按键用的引脚 Pin 的模式设置为适合按键应用的上拉输入模式(由于接了外部上拉电阻,也可以使用浮空输入,读者可自行修改代码做实验)。若 GPIO 被设置为输入模式,不需要设置 GPIO 端口的最大输出速度。

按键状态监测及按键消抖

uint8_t Key_Scan(void)
{           
    if(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0) == KEY_DOWN_LEVEL ) 
    {      
        HAL_Delay(10);      
        if(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0) == KEY_DOWN_LEVEL )  
        {    
            while(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0) == KEY_DOWN_LEVEL);   
            return  KEY_DOWN;    
        }
        else
        {
            return KEY_UP;
        }
    }
    return KEY_UP;
}

相信延时消抖的原理大家在学习其他单片机时就已经了解了,本函数的功能就是扫描输入参数中指定的引脚,检测其电平变化,并作延时消抖处理,最终对按键消息进行确认。

  • 利用HAL_GPIO_ReadPin()函数读取输入数据,若从相应引脚读取的数据等于 0(KEY_DOWN),低电平,表明可能有按键按下,调用延时函数。否则返回 KEY_UP,表示按键没有被按下。

  • 延时之后再次利用 HAL_GPIO_ReadPin()函数读取输入数据,若依然为低电平,表明确实有按键被按下了。否则返回 KEY_UP,表示按键没有被按下。

  • 循环调用HAL_GPIO_ReadPin()函数一直检测按键的电平,直至按键被释放,被释放后,返回表示按键被按下的标志 KEY_DOWN。以上是按键消抖的流程,调用了一个库函数 HAL_GPIO_ReadPin()函数。输入参数为要读取的端口、引脚,返回引脚的输入电平状态,高电平为 1,低电平为 0。

Main函数如下:

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>© Copyright (c) 2020 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
typedef enum{
    KEY_UP = 0,
    KEY_DOWN = 1,
}KEYState_Type;
/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define KEY_GPIO GPIOA
#define KEY_GPIO_PIN GPIO_PIN_0
#define KEY_DOWN_LEVEL 1
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
uint8_t Key_Scan(void);
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
        if(KEY_DOWN_LEVEL == Key_Scan())
        {
            HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);

            HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_6);

            HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_7);

        }
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief Key Scan
  * @retval uint8_t
  */
uint8_t Key_Scan(void)
{           
    if(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0) == KEY_DOWN_LEVEL ) 
    {      
        HAL_Delay(10);      
        if(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0) == KEY_DOWN_LEVEL )  
        {    
            while(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0) == KEY_DOWN_LEVEL);   
            return  KEY_DOWN;    
        }
        else
        {
            return KEY_UP;
        }
    }

    return KEY_UP;
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOG_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOG, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);

  /*Configure GPIO pin : PA0 */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /*Configure GPIO pin : PB0 */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /*Configure GPIO pins : PG6 PG7 */
  GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */

  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

5.2 EXTI方式

5.2.1 EXTI的工作原理

EXTI(External Interrupt) 就是指外部中断,通过 GPIO 检测输入脉冲,引起中断事件,打断原来的代码执行流程,进入到中断服务函数中进行处理,处理完后再返回到中断之前的代码中执行。

STM32 的中断和异常

Cortex 内核具有强大的异常响应系统,它把能够打断当前代码执行流程的事件分为异常(exception)和中断(interrupt),并把它们用一个表管理起来,编号为 0 ~ 15 的称为内核异常,而 16 以上的则称为外部中断(外是相对内核而言),这个表就称为中断向量表。
而 STM32 对这个表重新进行了编排,把编号从 –3 至 6 的中断向量定义为系统异常,编号为负的内核异常不能被设置优先级,如复位(Reset)、不可屏蔽中断 (NMI)、硬错误(Hardfault)。从编号 7 开始的为外部中断,这些中断的优先级都是可以自行设置的。详细的 STM32 中断向量表见下表。

L5oYsf.md.png

L5odoQ.md.png

L5oDWn.md.png

NVIC 中断控制器
STM32 的中断如此之多,配置起来并不容易,因此我们需要一个强大而方便的中断控制器 NVIC (Nested Vectored Interrupt Controller)。NVIC 是属于 Cortex 内核的器件,不可屏蔽中断 (NMI)和外部中断都由它来处理,而 SYSTICK 不是由 NVIC 来控制的。

L5oyQ0.md.png

NVIC 结构体成员
当我们要使用 NVIC 来配置中断时,自然想到 ST 库肯定也已经把它封装成库函数了。查找库帮助文档,发现在 Modules->ST M32F10x_StdPeriph_Driver->misc 查找到一个NVIC_Init() 函数。对 NVIC 初始化,首先要定义并填充一个 NVIC_InitTypeDef 类型的结构体。这个结构体有 4 个成员,见下表。

结构体成员名称 描述
NVIC_IRQChannel 需要配置的中断向量
NVIC_IRQChannelCmd 使能或关闭相应中断向量的中断响应
NVIC_IRQChannelPreemptionPriority 配置相应中断向量抢占优先级
NVIC_IRQChannelSubPriority 配置相应中断向量的响应优先级

前面两个结构体成员都很好理解,首先要用 NVIC_IRQChannel 参数来选择将要配置的中断向量,用 NVIC_IRQChannelCmd 参数来进行使能(ENABLE)或关闭(DISABLE)该中断。在 NVIC_IRQChannelPreemptionPriority 成员要配置中断向量的抢占优先级,在NVIC_IRQChannelSubPriority 需要配置中断向量的响应优先级。对于中断的配置,最重要的便是配置其优先级,但 STM32 的同一个中断向量为什么需要设置两种优先级?这两种优先级有什么区别?

抢占优先级和响应优先级
STM32 的中断向量具有两个属性,一个为抢占属性,另一个为响应属性,其属性编号越小,表明它的优先级别越高。

抢占,是指打断其他中断的属性,即因为具有这个属性会出现嵌套中断(在执行中断服务函数 A 的过程中被中断 B 打断,执行完中断服务函数 B 再继续执行中断服务函数A),抢占属性由 NVIC_IRQChannelPreemptionPriority 的参数配置。

而响应属性则应用在抢占属性相同的情况下,当 两个中断向量的抢占优先级相同时,如 果两个中断同时到达,则先处理响应优先级高的中断,响应属性 由NVIC_IRQChannelSubPriority 参数配置。例如,现在有三个中断向量,见下表。

中断向量 抢占优先级 响应优先级
A 0 0
B 1 0
C 1 1

若内核正在执行 C 的中断服务函数,则它能被抢占优先级更高的中断 A 打断,由于 B和 C 的抢占优先级相同,所以 C 不能被 B 打断。但如果 B 和 C 中断是同时到达的,内核就会首先响应响应优先级别更高的 B 中断。

NVIC 的优先级组
在配置优先级的时候,还要注意一个很重要的问题,即中断种类的数量。NVIC 只可以配置 16 种中断向量的优先级,也就是说,抢占优先级和响应优先级的数量由一个 4 位的数字来决定,把这个 4 位数字的位数分配成抢占优先级部分和响应优先级部分。有 5 组分配方式 :

  • 第 0 组: 所有 4 位用来配置响应优先级。即 16 种中断向量具有都不相同的响应优先级。

  • 第 1 组:最高 1 位用来配置抢占优先级,低 3 位用来配置响应优先级。表示有 21=2 种级别的抢占优先级(0 级,1 级),有 23=8 种响应优先级,即在 16 种中断向量之中,有8 种中断,其抢占优先级都为 0 级,而它们的响应优先级分别为 0 ~ 7,其余 8 种中断向量的抢占优先级则都为 1 级,响应优先级别分别为 0 ~ 7。

  • 第 2 组:2 位用来配置抢占优先级,2 位用来配置响应优先级。即 22=4 种抢占优先级,22=4 种响应优先级。

  • 第 3 组:高 3 位用来配置抢占优先级,最低 1 位用来配置响应优先级。即有 8 种抢占优先级,2 种响应 2 优先级。

  • 第 4 组:所有 4 位用来配置抢占优先级,即 NVIC 配置的 24 =16 种中断向量都是只有抢占属性,没有响应属性。

要配置这些优先级组,可以采用库函数 NVIC_PriorityGroupConfi g(),可输入的参数为NVIC_PriorityGroup_0 ~ NVIC_PriorityGroup_4,分别为以上介绍的 5 种分配组。

于是,有读者觉得疑惑了, 如此强 的 STM32, 所有GPIO都能够配置成外部中断,USART、ADC 等外设也有中断,而 NVIC 只能配置 16 种中断向量,那么在某个工程中使用超过 16 个中断怎么办呢?注意 NVIC 能配置的是 16 种中断向量,而不是16 个,当工程中有超过 16 个中断向量时,必然有两个以上的中断向量是使用相同的中断种类,而具有相同中断种类的中断向量不能互相嵌套。

STM2 单片机的所有 I/O 端口都可以配置为 EXTI 中断模式,用来捕捉外部信号,可以配置为下降沿中断、上升沿中断和上升下降沿中断这三种模式。它们以图 3- 2 所示方式连接到 16 个外部中断 / 事件线上。

EXTI 外部中断
STM32 的所有 GPIO 都引入到 EXTI 外部中断线上,使得所有的 GPIO 都能作为外部中断的输入源。GPIO 与 EXTI 的连接方式见下图。

观察下图可知,PA0 ~ PG0 连接到 EXTI0 、PA1 ~ PG1 连接到 EXTI1、……、PA15 ~ PG15 连接到 EXTI15。这里大家要注意的是 :PAx ~ PGx 端口的中断事件都连接到了 EXTIx,即同一时刻 EXTIx 只能响应一个端口的事件触发,不能够同一时间响应所有GPIO 端口的事件,但可以分时复用。它可以配置为上升沿触发、下降沿触发或双边沿触发。EXTI 最普通的应用就是接上一个按键,设置为下降沿触发,用中断来检测按键。

L5TGtJ.md.png

5.2.2 EXTI的寄存器描述

EXTI 寄存器的寄存器主要有6个,下面分别描述。

中断屏蔽寄存器(EXTI_IMR)

L5TstH.md.png

事件屏蔽寄存器(EXTI_EMR)

L5T2ct.md.png

上升沿触发选择寄存器(EXTI_RTSR)

L5TH9s.md.png

注意: 外部唤醒线是边沿触发的,这些线上不能出现毛刺信号。在写EXTI_RTSR寄存器时,在外部中断线上的上升沿信号不能被识别,挂起位也不会被置位。在同一中断线上,可以同时设置上升沿和下降沿触发。即任一边沿都可触发中断

下降沿触发选择寄存器(EXTI_FTSR)

L5Tqcq.md.png

注意: 外部唤醒线是边沿触发的,这些线上不能出现毛刺信号。在写EXTI_FTSR寄存器时,在外部中断线上的下降沿信号不能被识别,挂起位不会被置位。在同一中断线上,可以同时设置上升沿和下降沿触发。即任一边沿都可触发中断。

软件中断事件寄存器(EXTI_SWIER)

L5TXuV.md.png

挂起寄存器(EXTI_PR)

L57mUe.md.png

5.2.3 EXTI方式实现-标准库

配置外部中断
现在我们重点分析 EXTI_PA0_Confi g() 这个函数,它完成了配置一个 I/O 为 EXTI 中断的一般步骤,主要有以下功能 :

1)使能 EXTIx 线的时钟和第二功能 AFIO 时钟。
2)配置 EXTIx 线的中断优先级。
3)配置 EXTI 中断线 I/O。
4)选定要配置为 EXTI 的 I/O 口线和 I/O 口的工作模式。
5)EXTI 中断线工作模式配置。

void EXTI_PA0_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure; 
    EXTI_InitTypeDef EXTI_InitStructure;

    /* config the extiline clock and AFIO clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO,ENABLE);

    /* config the NVIC */
    NVIC_Configuration();

    /* EXTI line gpio config*/  
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;       
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;  // 下拉输入
  GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* EXTI line mode config */
  GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); 
  EXTI_InitStructure.EXTI_Line = EXTI_Line0;
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿触发中断
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  EXTI_Init(&EXTI_InitStructure); 
}

EXTI_PA0_Config() 代码中,配置好 NVIC 后,还要对 GPIOA 进行初始化,这部分和按键轮询的设置类似。

接下来,调用 GPIO_EXTILineConfi g() 函数把 GPIOA、Pin0 设置为 EXTI 输入线。选择好了 GPIO,开始填写 EXTI 的初始化结构体。从这些参数的名字,相信读者已经知道如何把它应用到按键检测中。

1).EXTI_Line = EXTI_Line0 : 给 EXTI_Line 成员赋值。选择 EXTI_Line0 线进行配置,因为按键的 PA0 连接到了 EXTI_Line0。

2).EXTI_Mode = EXTI_Mode_Interrupt :给 EXTI_Mode 成员赋值。 把 EXTI_Line0的模式设置为中断模式(EXTI_Mode_Interrupt)。这个结构体成员也可以赋值为事件模式EXTI_Mode_Event ,这个模式不会立刻触发中断,而只是在 寄存器上把相应的事件标志位置 1,应用这个模式需要不停地查询相应的寄存器。

3).EXTI_Trigger = EXTI_Trigger_Falling :给 EXTI_Trigger 成员赋值。把触发方式(EXTI_Trigger)设置为下降沿触发(EXTI_Trigger_Falling)。

4).EXTI_LineCmd = ENABLE :给 EXTI_LineCmd 成员赋值。把 EXTI_LineCmd 设置为使能。

5)最后调用 EXTI_Init() 把 EXTI 初始化结构体的参数写入寄存器。

AFIO 时钟
代码中调用RCC_APB2PeriphClockCmd() 时还输入了参数RCC_APB2Periph_AFIO,表示开启 AFIO的时钟。
AFIO (alternate-function I/O),指 GPIO 端口的复用功能,GPIO 除了用作普通的输入输出(主功能),还可以作为片上外设的复用输入输出,如串口、ADC,这些就是复用功能。大多数 GPIO 都有一个默认复用功能,有的 GPIO 还有重映射功能。重映射功能是指把原来属于 A 引脚的默认复用功能,转移到 B 引脚进行使用,前提是 B 引脚具有这个重映射功能。

当把 GPIO 用作 EXTI 外部中断或使用重映射功能的时候,必须开启 AFIO 时钟,而在使用默认复用功能的时候,就不必开启 AFIO 时钟了。

NVIC 初始化配置

static void NVIC_Configuration(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;

  /* Configure one bit for preemption priority */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

  /* 配置中断源 */
  NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}

本代码中调用了 NVIC_PriorityGroupConfi g() 库函数,把 NVIC 中断优先级分组设置为第 1 组。接下来开始向 NVIC 初始化结构体写入参数 .NVIC_IRQChannel =EXTI0_IRQn,表示要配置的为 EXTI 第 1 线的中断向量。因为按键 PA0 对应的 EXTI 线为EXTI0。这些可写入的参数可以在 stm32f10x.h 文件的 IRQn 类型定义中查找到。然后配置抢占优先级和响应优先级,因为这个工程简单,就直接把它设置为最高级中断。填充完结构体,别忘记最后要调用 NVIC_Init() 函数来向寄存器写入参数。这里要注意的是,如果用的 IO 口是 IO0 ~ IO4,那么对应的中断向量是 EXTI0_IRQn ~ EXTI4_IRQn,如果用的 IO 是I05 ~ IO9 中的一个的话,对应的中断向量只能是 EXTI9_5_IRQn, 如果用的 IO 是 I010~IO15中的一个的话,对应的中断向量只能是 EXTI15_10_IRQn。举例:如果 PE5 或者 PE6 作为EXTI 中断口,那么对应的中断向量都是 EXTI9_5_IRQn,在同一时刻只能相应来自一个IO 的 EXTI 中断。

编写中断服务函数
在这个 EXTI 设置中我们把 PA0 连接到内部的 EXTI0,GPIO 配置为上拉输入,工作在下降沿中断。在外围电路上我们将 PA0 接到了 key上。当按键没有按下时,PA0 始终为高,当按键按下时 PA0 变为低,从而 PA0 上产生一个下降沿跳变,EXTI0 会捕捉到这一跳变,并产生相应的中断,中断服务程序在 stm32f10x_it.c 中实现。stm32f10x_it.c 文件是专门用来存放中断服务函数的。文件中默认只有几个关于系统异常的中断服务函数,而且都是空函数,在需要的时候自行编写。那么中断服务函数名是不是可以自己定义呢?不可以。中断服务函数的名字必须要与启动文件startup_stm32f10x_hd.s 中的中断向量表定义一致。

L577VO.png

EXTI0_.IRQHandler 表示为 EXTI0 中断向量的服务函数名。于是,我们就可以在 stm32f10x_it.c 文件中加入名为 EXTI0_IRQHandler() 的函数。

void EXTI0_IRQHandler(void)
{
    if(EXTI_GetITStatus(EXTI_Line0) != RESET) //确保是否产生了 EXTI Line 中断
    {
        // LED取反
        LED_TOGGLE;
        EXTI_ClearITPendingBit(EXTI_Line0);    //清除中断标志位
    }  
}

其内容比较容易理解,进入中断后,调用库函数 EXTI_GetITStatus() 来重新检查是否产生了 EXTI_Line 中断,接下来把 LED 取反,操作完毕后,调用 EXTI_ClearITPendingBit()清除中断标志位再退出中断服务函数。

5.2.4 EXTI方式实现-HAL库

5.2.4.1 STM32Cube生成工程

根据按键电路,KEY1的引脚是PA0,我们将PA0的GPIO设置为上升沿触发的外部中断模式,保留3个LED的GPIO配置。

L5HmLV.md.png

如上图中对KEY的GPIO进行了初始化配置,接下来就要进行NVIC配置。NVIC选项用于设置中断的优先级,这里先设置优先级组为4位抢占式优先级为1,响应式优先级为0;EXTI[15:10];EXTI10- EXTI15中短线在中断向量表中占用同一个优先级,所以EXTI10- EXTI15中断线优先级都是一样的,同意配置。EXTI5 EXTI9情况也是一样。

L5Hli4.md.png

最后生成工程文件即可。

5.2.4.2 EXTI代码分析

在看代码前,我们先看看按键中断编程的流程:

1)使能AFIO时钟,设置NVIC优先级NVIC_PRIORITYGROUP_4;
2)使能按键引脚PA0,将其设置为上升沿触发中断模式并使能下拉;
3)配置按键引脚中断优先级并使能中断;
4)编写中断回调函数,同时进行消抖处理。

配置GPIO

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOG_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOG, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);

  /*Configure GPIO pin : PA0 */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /*Configure GPIO pin : PB0 */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /*Configure GPIO pins : PG6 PG7 */
  GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

  /* EXTI interrupt init*/
  HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(EXTI0_IRQn);

}

这部分和按键轮询的设置类似,需要对GPIO进行初始化设置。

AFIO 时钟
代码中调用__HAL_RCC_AFIO_CLK_ENABLE()函数,表示开启 AFIO的时钟。这个函数HAL_Init函数调用HAL_MspInit()函数实现的。

AFIO (alternate-function I/O),指 GPIO 端口的复用功能,GPIO 除了用作普通的输入输出(主功能),还可以作为片上外设的复用输入输出,如串口、ADC,这些就是复用功能。大多数 GPIO 都有一个默认复用功能,有的 GPIO 还有重映射功能。重映射功能是指把原来属于 A 引脚的默认复用功能,转移到 B 引脚进行使用,前提是 B 引脚具有这个重映射功能。

当把 GPIO 用作 EXTI 外部中断或使用重映射功能的时候,必须开启 AFIO 时钟,而在使用默认复用功能的时候,就不必开启 AFIO 时钟了。

NVIC 初始化配置

HAL_StatusTypeDef HAL_Init(void)
{
…
  /* Set Interrupt Group Priority */
  HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
…
} 

static void MX_GPIO_Init(void)
{
….
  /* EXTI interrupt init*/
  HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(EXTI0_IRQn);

}

本代码中调用了HAL_NVIC_SetPriorityGrouping()库函数,把 NVIC 中断优先级分组设置为4组。MX_GPIO_Init()函数的最后两个函数是关于中断优先级分组和使能中断的。
HAL_NVIC_SetPriority(),共有三个参数:

1.中断向量号

L5HWTS.md.png

中断向量号在stm32f103xe.h中定义的。

2.抢占优先级:设置了两位抢占优先级,那么抢占优先级可以是00-11,即0-3。

3.响应优先级:同样是两位。
HAL_NVIC_EnableIRQ()函数用于使能外部中断线,外部中断线10-15是共用一个中断向量的。

L5bVte.md.png

这里要注意的是,如果用的 IO 口是 IO0~ IO4,那么对应的中断向量是 EXTI0_IRQn ~ EXTI4_IRQn,如果用的 IO 是I05 ~ IO9 中的一个的话,对应的中断向量只能是 EXTI9_5_IRQn, 如果用的 IO 是 I010 ~ IO15中的一个的话,对应的中断向量只能是 EXTI15_10_IRQn。举例:如果 PE5 或者 PE6 作为EXTI 中断口,那么对应的中断向量都是 EXTI9_5_IRQn,在同一时刻只能相应来自一个IO 的 EXTI 中断。

编写中断服务函数
在这个 EXTI 设置中我们把 PA0 连接到内部的 EXTI0,GPIO 配置为上拉输入,工作在下降沿中断。在外围电路上我们将 PA0 接到了 key上。当按键没有按下时,PA0 始终为高,当按键按下时 PA0 变为低,从而 PA0 上产生一个下降沿跳变,EXTI0 会捕捉到这一跳变,并产生相应的中断,中断服务程序在 stm32f10x_it.c 中实现。stm32f10x_it.c 文件是专门用来存放中断服务函数的。文件中默认只有几个关于系统异常的中断服务函数,而且都是空函数,在需要的时候自行编写。那么中断服务函数名是不是可以自己定义呢?不可以。中断服务函数的名字必须要与启动文件startup_stm32f10x_hd.s 中的中断向量表定义一致。

L577VO.png

EXTI0_IRQHandler 表示为 EXTI0 中断向量的服务函数名。于是,我们就可以在 stm32f10x_it.c 文件中加入名为 EXTI0_IRQHandler() 的函数。

void EXTI0_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI0_IRQn 0 */

  /* USER CODE END EXTI0_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
  /* USER CODE BEGIN EXTI0_IRQn 1 */

  /* USER CODE END EXTI0_IRQn 1 */
}

EXTI0_IRQHandler()函数调用HAL_GPIO_EXTI_IRQHandler()函数,我们进入HAL_GPIO_EXTI_IRQHandler()函数,发现又调用了函数HAL_GPIO_EXTI_Callback(),再此进入HAL_GPIO_EXTI_Callback()函数,HAL_GPIO_EXTI_Callback()就是回调函数。

L5bGtg.md.png

__ weak 是一个弱化标识,带有这个的函数就是一个弱化函数,就是你可以在其他地方写一个名称和参数都一模一样的函数,编译器就会忽略这一个函数,而去执行你写的那个函数;而UNUSED(GPIO_Pin) ,这就是一个防报错的定义,当传进来的GPIO端口号没有做任何处理的时候,编译器也不会报出警告。其实我们在开发的时候已经不需要去理会中断服务函数了,只需要找到这个中断回调函数并将其重写即可而这个回调函数还有一点非常便利的地方这里没有体现出来,就是当同时有多个中断使能的时候,STM32CubeMX会自动地将几个中断的服务函数规整到一起并调用一个回调函数,也就是无论几个中断,我们只需要重写一个回调函并判断传进来的端口号即可。

那么接下来我们就在stm32f4xx_it.c这个文件的最下面添加以下代码:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if(GPIO_Pin==KEY_GPIO_PIN)
    {
        HAL_Delay(100);
        if(HAL_GPIO_ReadPin(KEY_GPIO,KEY_GPIO_PIN) == KEY_DOWN_LEVEL)
        {
            HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
            HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_6);
            HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_7);
        }
    }
}

其内容比较容易理解,进入中断后,调用库函数 HAL_GPIO_ReadPin来重新检查是否产生了中断,接下来把 LED 取反。

5.3实验现象

编译好程序后,下载到板子上,不管是普通方式还是中断方式,当按在按键S1时,LED1或亮或灭。


欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎


欢迎订阅我的微信公众号

嵌入式实验楼

资源获取方式

1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码

Related posts

Leave a Comment