STM32F407的嵌套向量中断控制器(Nested Vectored Interrupt Controller,NVIC)管理所有中断,它有82个可屏蔽中断,还有13个系统中断。82个可屏蔽中断和部分系统中断可配置中断优先级,总共有16个优先级。
NVIC是ARM Cortex-M处理器内部的模块,负责管理处理器的中断。它的特性包括:
在STM32系列中,要使用中断,通常的步骤包括配置相关的外部硬件(如果需要)、配置NVIC以及编写中断服务程序。在初始化过程中,需要确保配置中断优先级和使能相应的中断。
STM32F4系列使用的是ARM Cortex-M内核,中断向量表是处理器中的一组特殊地址,存储着每个中断服务程序的入口地址。当发生中断时,处理器会根据中断编号查找中断向量表,并跳转到相应中断服务程序的入口地址执行。
在STM32F4中,中断向量表包括了两种类型的向量表项:
用STM32CubeMX生成代码后,在 startup_stm32f407zgtx.s 汇编文件中,我们可以看到中断向量表具体的定义和中断服务函数。
在STM32F4系列中,中断优先级用于确定当多个中断同时发生时,处理器应该先处理哪个中断。这个优先级系统是由ARM Cortex-M内核提供支持的。
可编程性:
STM32F4允许针对每个可中断的外设配置中断优先级。这使得开发者可以根据系统需求对中断进行优先级管理。
优先级位数:
Cortex-M内核中的中断优先级分为抢占优先级(Preemption Priority)和子优先级(Subpriority)。在STM32F4中,通常抢占优先级占据较高的位数,子优先级占据较低的位数。
数值越小,优先级越高:
优先级数值越小,表示优先级越高。比如,抢占优先级数值为0的中断优先级最高。
在STM32F4中,使用NVIC(Nested Vectored Interrupt Controller)来配置中断优先级。配置中断优先级的步骤如下:
确定优先级组分配:
Cortex-M内核允许将中断优先级分为不同的组,主要分为4-2-2(4位抢占优先级、2位子优先级)和3-1-4(3位抢占优先级、1位子优先级)两种模式。这决定了中断优先级的位分配,应根据系统需求进行选择。
设置抢占优先级和子优先级:
使用库函数或直接操作NVIC寄存器来为每个中断分配优先级。通常有函数如NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority)
用于设置中断的抢占优先级和子优先级。
优先级的影响:
在相同的优先级组中,较低位的抢占优先级数值越小,优先级越高;较低位的子优先级数值越小,优先级越高。
// 设置外设中断优先级为抢占优先级2,子优先级1
NVIC_SetPriority(USART1_IRQn, 2, 1);
// 启用USART1中断
NVIC_EnableIRQ(USART1_IRQn);
这个示例中,针对USART1的中断被配置为抢占优先级为2,子优先级为1。请注意,优先级配置必须在使能相应中断之前完成。
了解并正确配置中断优先级对于确保在系统中正确处理中断,并按照预期的顺序处理多个中断事件至关重要。
中断管理相关驱动程序的头文件是 stm32f4xx_hal_cortex.h ,常用函数如下图所示
函数名 | 功能 |
---|---|
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup); | 设置4位二进制数的优先级分组策略 |
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority); | 设置某个中断的抢占优先级和此优先级 |
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn); | 启用某个中断 |
void HAL_NVIC_DisableIRQ(IRQn_Type IRQn); | 禁用某个中断 |
uint32_t HAL_NVIC_GetPriorityGrouping(void); | 返回当前的优先级分组策略 |
void HAL_NVIC_GetPriority(IRQn_Type IRQn, uint32_t PriorityGroup, uint32_t* pPreemptPriority, uint32_t* pSubPriority); | 返回某个中断的抢占优先级、次优先级数值 |
uint32_t HAL_NVIC_GetPendingIRQ(IRQn_Type IRQn); | 检查某个中断是否被挂起 |
void HAL_NVIC_SetPendingIRQ(IRQn_Type IRQn); | 设置某个中断的挂起标志,表示发生了中断 |
void HAL_NVIC_ClearPendingIRQ(IRQn_Type IRQn); | 清除某个中断挂起标志 |
外部中断(External Interrupt,通常称为EXTI)在STM32F4系列微控制器中是一个重要的功能,它允许外部事件(例如按键、传感器输入等)引发处理器的中断。STM32F4通过外部中断线(External Interrupt Lines)来处理外部事件,而这些中断线可以与GPIO引脚相连。外部I/O端口的电平可以有三种触发中断的方式:上升沿、下降沿和双边沿触发,如下图所示:
其原理如下图所示,外部中断是由外部I/O端口产生中断触发,单片机内核中止当前工作,并处理中断。这时就进入了中间部分,这里包括了“CPU参与”方框部分,有CPU进行参与。在中断模式下,CPU参与处理中断服务函数,进而点亮LED灯。
STM32F407有23个外部中断,每个输入线都可以单独配置触发事件,如上跳沿触发、下跳沿触发或双边沿触发。每个EXTI中断可以单独屏蔽,有独立的中断标志,可以单独清除或保持其中断标志。如下图所示,是外部有信号从右边输入线输入,通过边缘检测电路,判断其中断标志位,进而一步步输送到NVIC中断控制器中。
STM32F407有23个外部中断,每个输入线都可以单独配置触发事件,其中EXTI0至EXTI4的每个中断有独立的ISR,EXTI线[9:5]中断共用一个中断号,也就是共用ISR,EXTI线[15:10]中断也共用ISR,见下表所示。若共用的ISR,需要在ISR里在判断具体是哪个EXTI线产生的中断,然后做相应处理。
另外7个EXTI线连接的不是某个实际的GPIO引脚,二十其他外设产生的事件信号。这7个EXTI线的中断有单独的ISR:
从正点原子的原理图中,选择4个按键,2个LED灯作为本次实验的对象,用4个按键来控制2个LED灯实现不同功能的开关LED灯的效果。
下图所示是4个按键KEY和LED对应的原理图,及相应的配置功能
名称 | 端口 | 引脚功能 | 特性 | 初始电平 |
---|---|---|---|---|
KeyUp(WK_UP) | PA0 | GPIO_EXTI0 | Pull-down 下拉 | N/A |
KeyLeft(KEY2) | PE2 | GPIO_EXTI2 | Pull-up 上拉 | N/A |
KeyDown(KEY1) | PE3 | GPIO_EXTI3 | Pull-up 上拉 | N/A |
KeyRight(KEY0) | PE4 | GPIO_EXTI4 | Pull-up 上拉 | N/A |
LED1 | PF9 | Output | Pushpull推挽输出, | 初始低电平 |
LED2 | PF10 | Output | Pushpull推挽输出, | 初始低电平 |
注:4个GPIO的中断禁止设置成0,由于代码编写时用到了Systick系统时钟,系统时钟计时是靠这个中断来产生1ms的精准定时,如果4个GPIO设置成0,则会导致Systick计时被抢占,导致陷入HAL_Delay()的死循环中,无法出来
我们在CubeMX中配置完成后生成CubeIDE项目的代码,然后在CubeIDE中贷款刚生成的代码。
所生成的代码已经完成了GPIO引脚的初始化,包括外部中断的初始化配置,还生成了外部中断ISR的代码框架。
文件main.c中的主程序代码如下图所示,他调用了函数MX_GPIO_Init()进行GPIO引脚的初始化:
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
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 */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
HAL_Init()函数用于HAL初始化,在CubeMX中设置的中断优先级分组策略是在这个函数里用代码实现的。HAL_Init()调用了一个弱函数HAL_MspInit(),在CubeMX生成的代码中有一个文件stm32f4xx_hal_msp.c,在这个文件里重新实现了函数HAL_MspInit(),其代码如下:
/**
* Initializes the Global MSP.
*/
void HAL_MspInit(void)
{
/* USER CODE BEGIN MspInit 0 */
/* USER CODE END MspInit 0 */
__HAL_RCC_SYSCFG_CLK_ENABLE();
__HAL_RCC_PWR_CLK_ENABLE();
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
/* System interrupt init*/
/* USER CODE BEGIN MspInit 1 */
/* USER CODE END MspInit 1 */
}
我们在CubeMX中为LED和按键的引脚都定义了用户标签,因此文件main.h中生成了这些引脚的引脚号、端口的宏定义,并且对于4个外部中断引脚,还有中断号的宏定义,全部定义如下:
/* Private defines -----------------------------------------------------------*/
#define KeyRight_Pin GPIO_PIN_2
#define KeyRight_GPIO_Port GPIOE
#define KeyRight_EXTI_IRQn EXTI2_IRQn
#define KeyDown_Pin GPIO_PIN_3
#define KeyDown_GPIO_Port GPIOE
#define KeyDown_EXTI_IRQn EXTI3_IRQn
#define KeyLeft_Pin GPIO_PIN_4
#define KeyLeft_GPIO_Port GPIOE
#define KeyLeft_EXTI_IRQn EXTI4_IRQn
#define LED1_Pin GPIO_PIN_9
#define LED1_GPIO_Port GPIOF
#define LED2_Pin GPIO_PIN_10
#define LED2_GPIO_Port GPIOF
#define KeyUp_Pin GPIO_PIN_0
#define KeyUp_GPIO_Port GPIOA
#define KeyUp_EXTI_IRQn EXTI0_IRQn
文件gpio.c中的函数MX_GPIO_Init()实现了GPIO引脚和EXTI中断的初始化,代码如下:
/** Configure pins as
* Analog
* Input
* Output
* EVENT_OUT
* EXTI
*/
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOF, LED1_Pin|LED2_Pin, GPIO_PIN_RESET);
/*Configure GPIO pins : PEPin PEPin PEPin */
GPIO_InitStruct.Pin = KeyRight_Pin|KeyDown_Pin|KeyLeft_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
/*Configure GPIO pins : PFPin PFPin */
GPIO_InitStruct.Pin = LED1_Pin|LED2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
/*Configure GPIO pin : PtPin */
GPIO_InitStruct.Pin = KeyUp_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(KeyUp_GPIO_Port, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
HAL_NVIC_SetPriority(EXTI2_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI2_IRQn);
HAL_NVIC_SetPriority(EXTI3_IRQn, 1, 2);
HAL_NVIC_EnableIRQ(EXTI3_IRQn);
HAL_NVIC_SetPriority(EXTI4_IRQn, 1, 1);
HAL_NVIC_EnableIRQ(EXTI4_IRQn);
}
这个函数前半部分是对LED和按键GPIO引脚的初始化设置,函数代码的后半部分是对4个外部中断的设置,主要是设置中断的优先级和开启中断,用到了函数HAL_NVIC_SetPriority()和HAL_NVIC_EnableIRQ()。
EXTI0到EXTI4都有独立的ISR,在文件stm32f4xx_it.c中自动生成了这4个ISR的代码框架,代码如下:
/******************************************************************************/
/* STM32F4xx Peripheral Interrupt Handlers */
/* Add here the Interrupt Handlers for the used peripherals. */
/* For the available peripheral interrupt handler names, */
/* please refer to the startup file (startup_stm32f4xx.s). */
/******************************************************************************/
/**
* @brief This function handles EXTI line0 interrupt.
*/
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 */
}
/**
* @brief This function handles EXTI line2 interrupt.
*/
void EXTI2_IRQHandler(void)
{
/* USER CODE BEGIN EXTI2_IRQn 0 */
/* USER CODE END EXTI2_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);
/* USER CODE BEGIN EXTI2_IRQn 1 */
/* USER CODE END EXTI2_IRQn 1 */
}
/**
* @brief This function handles EXTI line3 interrupt.
*/
void EXTI3_IRQHandler(void)
{
/* USER CODE BEGIN EXTI3_IRQn 0 */
/* USER CODE END EXTI3_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_3);
/* USER CODE BEGIN EXTI3_IRQn 1 */
/* USER CODE END EXTI3_IRQn 1 */
}
/**
* @brief This function handles EXTI line4 interrupt.
*/
void EXTI4_IRQHandler(void)
{
/* USER CODE BEGIN EXTI4_IRQn 0 */
/* USER CODE END EXTI4_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4);
/* USER CODE BEGIN EXTI4_IRQn 1 */
/* USER CODE END EXTI4_IRQn 1 */
}
我们在前文分析了外部中断ISR的执行原理,这些ISR追钟都要调用回调函数HAL_GPIO_EXTI_Callback(),因此用户需要重新实现这个回调函数,实现设计功能。
我们要处理外部中断,只需要重新实现回调函数HAL_GPIO_EXTI_Callback()。此外,我们可以在任何一个文件内重新实现这个回调函数(例如在main.c中实现,也可以在gpio.c内实现),并且无须在头文件中声明其函数原型。
我们在文件gpio.c中重新实现这个函数,但需要注意,这个函数的代码必须写在一个代码沙箱内。在文件gpio.c中重新实现这个函数的代码如下:
/* USER CODE BEGIN 2 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == KeyUp_Pin)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
HAL_Delay(500);
}
else if (GPIO_Pin == KeyRight_Pin)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_Delay(1000);
}
else if (GPIO_Pin == KeyLeft_Pin)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
HAL_Delay(1000);
}
else if (GPIO_Pin == KeyDown_Pin)
{
__HAL_GPIO_EXTI_GENERATE_SWIT(GPIO_PIN_0);
HAL_Delay(1000);
}
}
/* USER CODE END 2 */
函数的参数GPIO_Pin是触发外部中断的中断线,可用于判断发生了哪个外部中断。函数代码的功能很直观,就是实现一下预想的示例功能:
完成回调函数代码后,我们就可以构建项目,并将其下载到开发板上进行测试了。但是运行时,按键按下后的响应不如预期,,例如按下KeyUp按键后,两个LED会亮灭两次,虽然加了延时进行消抖处理,但是还是有按键抖动的影响。分析后发现,这是由ISR中调用的外部中断通用处理函数HAL_GPIO_EXTI_IRQHandler()的代码引起的,这个函数的代码如下:
/**
* @brief This function handles EXTI interrupt request.
* @param GPIO_Pin Specifies the pins connected EXTI line
* @retval None
*/
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
他检测到中断挂起标志后,先清除了中断挂起标志,然后再执行回调函数。一般的中断通用函数都是这样的处理流程,是为了硬件能够及时响应下一次中断。但是对于检测按键输入的外部中断,这是有问题的,因为清除中断挂起标志位后,按键的抖动就会触发下一次中断,并将中断挂起标志位置位,就会在执行一次回调函数。
所以对于外部输入的中断按键检测,需要修改一下HAL_GPIO_EXTI_IRQHandler()的代码,将清除中断挂起标志位的功能放到后面,先执行回调函数,进行相应的按键功能检测,然后在清除中断标志位,所以修改的代码如下所示,这样,代码就没有问题了:
/**
* @brief This function handles EXTI interrupt request.
* @param GPIO_Pin Specifies the pins connected EXTI line
* @retval None
*/
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
{
HAL_GPIO_EXTI_Callback(GPIO_Pin);
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
}
}
但是要注意,函数HAL_GPIO_EXTI_IRQHandler()时文件stm32f4xx_hal_gpio.c中HAL驱动的原始文件,这个函数里面没有代码沙箱。修改这个函数后,在CubeMX重新生成代码时,这个函数又变回原来的样子。所以,在使用CubeMX时,用户一定要将代码写在沙箱内,如果实在要修改HAL的原始代码,在CubeMX重新生成代码后又会还原回去,要记得再次该回去。