3.1普通方式
3.1.1 普通方式工作原理
按键 GPIO 端口有两个方案可以选择,一是采用上拉输入模式,因为按键在没按下的时候,是默认为高电平的,采且内部上拉模式正好符合这个要求。第二个方案是直接采用浮空输入模式,因为按照硬件电路图,在芯片外部接了上拉电阻,其实就没必要再配置成内部上拉输入模式了,因为在外部上拉与内部上拉效果是一样的。
图1按键电路 3.1.2 STM32Cube生成工程
关于如何使用使用STM32Cube新建工程在前文已经讲解过了,这里直说配置GPIO部分内容。本文要实现按键功能,通过按键实现LED的亮灭。我门在第一个程序的基础上进行修改即可,不必每次都新建工程。根据图1所示的电路,KEY1的引脚是PA0,我们将PA0的GPIO设置为下拉的输入模式,保留3个LED的GPIO配置。
初始化基本配置后,我们重新生成工程,接下来按键编程。
3.1.3普通方式的具体代码分析
在看代码前,我们先看看按键扫描编程的流程:
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
*
*
© Copyright (c) 2020 STMicroelectronics.
* All rights reserved.
*
* 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 %drn", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
3.2 EXTI方式
3.2.1 EXTI的工作原理
EXTI(External Interrupt) 就是指外部中断,通过 GPIO 检测输入脉冲,引起中断事件,打断原来的代码执行流程,进入到中断服务函数中进行处理,处理完后再返回到中断之前的代码中执行。
STM32 的中断和异常
Cortex 内核具有强大的异常响应系统,它把能够打断当前代码执行流程的事件分为异常(exception)和中断(interrupt),并把它们用一个表管理起来,编号为 0 ~ 15 的称为内核异常,而 16 以上的则称为外部中断(外是相对内核而言),这个表就称为中断向量表。
而 STM32 对这个表重新进行了编排,把编号从 –3 至 6 的中断向量定义为系统异常,编号为负的内核异常不能被设置优先级,如复位(Reset)、不可屏蔽中断 (NMI)、硬错误(Hardfault)。从编号 7 开始的为外部中断,这些中断的优先级都是可以自行设置的。 详细的 STM32 中断向量表见表1。
NVIC 中断控制器
STM32 的中断如此之多,配置起来并不容易,因此我们需要一个强大而方便的中断控制器 NVIC (Nested Vectored Interrupt Controller)。NVIC 是属于 Cortex 内核的器件,不可屏蔽中断 (NMI)和外部中断都由它来处理,而 SYSTICK 不是由 NVIC 来控制的。
图3 NVIC 在内核中的位置 EXTI 功能框图
EXTI 的功能框图包含了 EXTI 最核心内容,掌握了功能框图,对 EXTI 就有一个整体的把握,在编程时思路就非常清晰。 EXTI 功能框图见图4。在图4可以看到很多在信号线上打一个斜杠并标注“20”字样,这个表示在控制器内部类似的信号线路有 20 个,这与 EXTI 总共有 20 个中断/事件线是吻合的。所以我们只要明白其中一个的原理,那其他 19 个线路原理也就知道了。
图4 EXTI 可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件上就有所不同。首先我们来看图4中红色虚线指示的电路流程。它是一个产生中断的线路,最终信号流入到 NVIC 控制器内。
编号 1 是输入线, EXTI 控制器有 19 个中断/事件输入线,这些输入线可以通过寄存器设置为任意一个 GPIO,也可以是一些外设的事件,这部分内容我们将在后面专门讲解。输入线一般是存在电平变化的信号。
编号 2 是一个边沿检测电路,它会根据上升沿触发选择寄存器(EXTI_RTSR)和下降沿触发选择寄存器(EXTI_FTSR)对应位的设置来控制信号触发。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号 1 给编号 3 电路,否则输出无效信号0。而 EXTI_RTSR 和 EXTI_FTSR 两个寄存器可以控制器需要检测哪些类型的电平跳变过程,可以是只有上升沿触发、只有下降沿触发或者上升沿和下降沿都触发。
编号 3 电路实际就是一个或门电路,它一个输入来自编号 2 电路,另外一个输入来自软件中断事件寄存器(EXTI_SWIER)。 EXTI_SWIER 允许我们通过程序控制就可以启动中断/事件线,这在某些地方非常有用。我们知道或门的作用就是有 1 就为 1,所以这两个输入随便一个有有效信号 1 就可以输出 1 给编号 4 和编号 6 电路。
编号 4 电路是一个与门电路,它一个输入是编号 3 电路,另外一个输入来自中断屏蔽寄存器(EXTI_IMR)。与门电路要求输入都为 1 才输出 1,导致的结果是如果 EXTI_IMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,最终编号 4 电路输出的信号都为 0;如果 EXTI_IMR 设置为 1 时,最终编号 4 电路输出的信号才由编号 3 电路的输出信号决定,这样我们可以简单的控制 EXTI_IMR 来实现是否产生中断的目的。编号 4 电路输出的信号会被保存到挂起寄存器(EXTI_PR)内,如果确定编号 4 电路输出为 1 就会把 EXTI_PR 对应位置 1。
编号 5 是将 EXTI_PR 寄存器内容输出到 NVIC 内,从而实现系统中断事件控制。接下来我们来看看绿色虚线指示的电路流程。它是一个产生事件的线路,最终输出一个脉冲信号。
产生事件线路是在编号 3 电路之后与中断线路有所不同,之前电路都是共用的。编号6 电路是一个与门,它一个输入来自编号 3 电路,另外一个输入来自事件屏蔽寄存器(EXTI_EMR)。如果 EXTI_EMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,
编号 6 电路输出的信号都为 0;如果 EXTI_EMR 设置为 1 时,最终编号 6 电路输出的信号才由编号 3 电路的输出信号决定,这样我们可以简单的控制 EXTI_EMR 来实现是否产生事件的目的。
编号 7 是一个脉冲发生器电路,当它的输入端,即编号 6 电路的输出端,是一个有效信号 1 时就会产生一个脉冲;如果输入端是无效信号就不会输出脉冲。
编号 8 是一个脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他外设电路使用,比如定时器 TIM、模拟数字转换器 ADC 等等,这样的脉冲信号一般用来触发 TIM 或者 ADC 开始转换。
产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。
抢占优先级和响应优先级
对于中断的配置,最重要的便是配置其优先级,但 STM32 的同一个中断向量为什么需要设置两种优先级?这两种优先级有什么区别?STM32 的中断向量具有两个属性,一个为抢占属性,另一个为响应属性,其属性编号越小,表明它的优先级别越高。
抢占,是指打断其他中断的属性,即因为具有这个属性会出现嵌套中断(在执行中断服务函数 A 的过程中被中断 B 打断,执行完中断服务函数 B 再继续执行中断服务函数A)。
而响应属性则应用在抢占属性相同的情况下,当 两个中断向量的抢占优先级相同时,如 果两个中断同时到达,则先处理响应优先级高的中断。例如,现在有三个中断向量,见表2。
表2中断向量举例 [tr]中断向量抢占优先级响应优先级[/tr]
若内核正在执行 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 种中断向量都是只有抢占属性,没有响应属性。
要配置这些优先级组,可以采用库函数HAL_NVIC_SetPriorityGrouping(),可输入的参数为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 的连接方式见图 5。
观察图5可知,PA0 ~ PG0 连接到 EXTI0 、PA1 ~ PG1 连接到 EXTI1、……、PA15 ~ PG15 连接到 EXTI15。这里大家要注意的是 :PAx ~ PGx 端口的中断事件都连接到了 EXTIx,即同一时刻 EXTIx 只能响应一个端口的事件触发,不能够同一时间响应所有GPIO 端口的事件,但可以分时复用。它可以配置为上升沿触发、下降沿触发或双边沿触发。EXTI 最普通的应用就是接上一个按键,设置为下降沿触发,用中断来检测按键。
图5外部中断通用I/O映像 3.2.2 STM32Cube生成工程
根据图1所示的电路,KEY1的引脚是PA0,我们将PA0的GPIO设置为上升沿触发的外部中断模式,保留3个LED的GPIO配置。
图6 GPIO配置 如上图中对KEY的GPIO进行了初始化配置,接下来就要进行NVIC配置。NVIC选项用于设置中断的优先级,这里先设置优先级组为4位抢占式优先级为1,响应式优先级为0;EXTI[15:10];EXTI10- EXTI15中短线在中断向量表中占用同一个优先级,所以EXTI10- EXTI15中断线优先级都是一样的,同意配置。EXTI5 EXTI9情况也是一样。
图7 NVIC配置 最后生成工程文件即可。
3.2.3 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.中断向量号
中断向量号在stm32f103xe.h中定义的。
2.抢占优先级:设置了两位抢占优先级,那么抢占优先级可以是00-11,即0-3。
3.响应优先级:同样是两位。
HAL_NVIC_EnableIRQ()函数用于使能外部中断线,外部中断线10-15是共用一个中断向量的。
这里要注意的是,如果用的 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 中的中断向量表定义一致。
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()就是回调函数。
__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 取反。
3.3实验现象
编译好程序后,下载到板子上,不管是普通方式还是中断方式,当按在按键S1时,LED或亮或灭。
3.1普通方式
3.1.1 普通方式工作原理
按键 GPIO 端口有两个方案可以选择,一是采用上拉输入模式,因为按键在没按下的时候,是默认为高电平的,采且内部上拉模式正好符合这个要求。第二个方案是直接采用浮空输入模式,因为按照硬件电路图,在芯片外部接了上拉电阻,其实就没必要再配置成内部上拉输入模式了,因为在外部上拉与内部上拉效果是一样的。
图1按键电路 3.1.2 STM32Cube生成工程
关于如何使用使用STM32Cube新建工程在前文已经讲解过了,这里直说配置GPIO部分内容。本文要实现按键功能,通过按键实现LED的亮灭。我门在第一个程序的基础上进行修改即可,不必每次都新建工程。根据图1所示的电路,KEY1的引脚是PA0,我们将PA0的GPIO设置为下拉的输入模式,保留3个LED的GPIO配置。
初始化基本配置后,我们重新生成工程,接下来按键编程。
3.1.3普通方式的具体代码分析
在看代码前,我们先看看按键扫描编程的流程:
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
*
*
© Copyright (c) 2020 STMicroelectronics.
* All rights reserved.
*
* 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 %drn", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
3.2 EXTI方式
3.2.1 EXTI的工作原理
EXTI(External Interrupt) 就是指外部中断,通过 GPIO 检测输入脉冲,引起中断事件,打断原来的代码执行流程,进入到中断服务函数中进行处理,处理完后再返回到中断之前的代码中执行。
STM32 的中断和异常
Cortex 内核具有强大的异常响应系统,它把能够打断当前代码执行流程的事件分为异常(exception)和中断(interrupt),并把它们用一个表管理起来,编号为 0 ~ 15 的称为内核异常,而 16 以上的则称为外部中断(外是相对内核而言),这个表就称为中断向量表。
而 STM32 对这个表重新进行了编排,把编号从 –3 至 6 的中断向量定义为系统异常,编号为负的内核异常不能被设置优先级,如复位(Reset)、不可屏蔽中断 (NMI)、硬错误(Hardfault)。从编号 7 开始的为外部中断,这些中断的优先级都是可以自行设置的。 详细的 STM32 中断向量表见表1。
NVIC 中断控制器
STM32 的中断如此之多,配置起来并不容易,因此我们需要一个强大而方便的中断控制器 NVIC (Nested Vectored Interrupt Controller)。NVIC 是属于 Cortex 内核的器件,不可屏蔽中断 (NMI)和外部中断都由它来处理,而 SYSTICK 不是由 NVIC 来控制的。
图3 NVIC 在内核中的位置 EXTI 功能框图
EXTI 的功能框图包含了 EXTI 最核心内容,掌握了功能框图,对 EXTI 就有一个整体的把握,在编程时思路就非常清晰。 EXTI 功能框图见图4。在图4可以看到很多在信号线上打一个斜杠并标注“20”字样,这个表示在控制器内部类似的信号线路有 20 个,这与 EXTI 总共有 20 个中断/事件线是吻合的。所以我们只要明白其中一个的原理,那其他 19 个线路原理也就知道了。
图4 EXTI 可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件上就有所不同。首先我们来看图4中红色虚线指示的电路流程。它是一个产生中断的线路,最终信号流入到 NVIC 控制器内。
编号 1 是输入线, EXTI 控制器有 19 个中断/事件输入线,这些输入线可以通过寄存器设置为任意一个 GPIO,也可以是一些外设的事件,这部分内容我们将在后面专门讲解。输入线一般是存在电平变化的信号。
编号 2 是一个边沿检测电路,它会根据上升沿触发选择寄存器(EXTI_RTSR)和下降沿触发选择寄存器(EXTI_FTSR)对应位的设置来控制信号触发。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号 1 给编号 3 电路,否则输出无效信号0。而 EXTI_RTSR 和 EXTI_FTSR 两个寄存器可以控制器需要检测哪些类型的电平跳变过程,可以是只有上升沿触发、只有下降沿触发或者上升沿和下降沿都触发。
编号 3 电路实际就是一个或门电路,它一个输入来自编号 2 电路,另外一个输入来自软件中断事件寄存器(EXTI_SWIER)。 EXTI_SWIER 允许我们通过程序控制就可以启动中断/事件线,这在某些地方非常有用。我们知道或门的作用就是有 1 就为 1,所以这两个输入随便一个有有效信号 1 就可以输出 1 给编号 4 和编号 6 电路。
编号 4 电路是一个与门电路,它一个输入是编号 3 电路,另外一个输入来自中断屏蔽寄存器(EXTI_IMR)。与门电路要求输入都为 1 才输出 1,导致的结果是如果 EXTI_IMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,最终编号 4 电路输出的信号都为 0;如果 EXTI_IMR 设置为 1 时,最终编号 4 电路输出的信号才由编号 3 电路的输出信号决定,这样我们可以简单的控制 EXTI_IMR 来实现是否产生中断的目的。编号 4 电路输出的信号会被保存到挂起寄存器(EXTI_PR)内,如果确定编号 4 电路输出为 1 就会把 EXTI_PR 对应位置 1。
编号 5 是将 EXTI_PR 寄存器内容输出到 NVIC 内,从而实现系统中断事件控制。接下来我们来看看绿色虚线指示的电路流程。它是一个产生事件的线路,最终输出一个脉冲信号。
产生事件线路是在编号 3 电路之后与中断线路有所不同,之前电路都是共用的。编号6 电路是一个与门,它一个输入来自编号 3 电路,另外一个输入来自事件屏蔽寄存器(EXTI_EMR)。如果 EXTI_EMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,
编号 6 电路输出的信号都为 0;如果 EXTI_EMR 设置为 1 时,最终编号 6 电路输出的信号才由编号 3 电路的输出信号决定,这样我们可以简单的控制 EXTI_EMR 来实现是否产生事件的目的。
编号 7 是一个脉冲发生器电路,当它的输入端,即编号 6 电路的输出端,是一个有效信号 1 时就会产生一个脉冲;如果输入端是无效信号就不会输出脉冲。
编号 8 是一个脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他外设电路使用,比如定时器 TIM、模拟数字转换器 ADC 等等,这样的脉冲信号一般用来触发 TIM 或者 ADC 开始转换。
产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。
抢占优先级和响应优先级
对于中断的配置,最重要的便是配置其优先级,但 STM32 的同一个中断向量为什么需要设置两种优先级?这两种优先级有什么区别?STM32 的中断向量具有两个属性,一个为抢占属性,另一个为响应属性,其属性编号越小,表明它的优先级别越高。
抢占,是指打断其他中断的属性,即因为具有这个属性会出现嵌套中断(在执行中断服务函数 A 的过程中被中断 B 打断,执行完中断服务函数 B 再继续执行中断服务函数A)。
而响应属性则应用在抢占属性相同的情况下,当 两个中断向量的抢占优先级相同时,如 果两个中断同时到达,则先处理响应优先级高的中断。例如,现在有三个中断向量,见表2。
表2中断向量举例 [tr]中断向量抢占优先级响应优先级[/tr]
若内核正在执行 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 种中断向量都是只有抢占属性,没有响应属性。
要配置这些优先级组,可以采用库函数HAL_NVIC_SetPriorityGrouping(),可输入的参数为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 的连接方式见图 5。
观察图5可知,PA0 ~ PG0 连接到 EXTI0 、PA1 ~ PG1 连接到 EXTI1、……、PA15 ~ PG15 连接到 EXTI15。这里大家要注意的是 :PAx ~ PGx 端口的中断事件都连接到了 EXTIx,即同一时刻 EXTIx 只能响应一个端口的事件触发,不能够同一时间响应所有GPIO 端口的事件,但可以分时复用。它可以配置为上升沿触发、下降沿触发或双边沿触发。EXTI 最普通的应用就是接上一个按键,设置为下降沿触发,用中断来检测按键。
图5外部中断通用I/O映像 3.2.2 STM32Cube生成工程
根据图1所示的电路,KEY1的引脚是PA0,我们将PA0的GPIO设置为上升沿触发的外部中断模式,保留3个LED的GPIO配置。
图6 GPIO配置 如上图中对KEY的GPIO进行了初始化配置,接下来就要进行NVIC配置。NVIC选项用于设置中断的优先级,这里先设置优先级组为4位抢占式优先级为1,响应式优先级为0;EXTI[15:10];EXTI10- EXTI15中短线在中断向量表中占用同一个优先级,所以EXTI10- EXTI15中断线优先级都是一样的,同意配置。EXTI5 EXTI9情况也是一样。
图7 NVIC配置 最后生成工程文件即可。
3.2.3 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.中断向量号
中断向量号在stm32f103xe.h中定义的。
2.抢占优先级:设置了两位抢占优先级,那么抢占优先级可以是00-11,即0-3。
3.响应优先级:同样是两位。
HAL_NVIC_EnableIRQ()函数用于使能外部中断线,外部中断线10-15是共用一个中断向量的。
这里要注意的是,如果用的 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 中的中断向量表定义一致。
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()就是回调函数。
__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 取反。
3.3实验现象
编译好程序后,下载到板子上,不管是普通方式还是中断方式,当按在按键S1时,LED或亮或灭。
举报