09. 外部中断
一、外部中断简介
EXTI 即是 外部中断和事件控制器,它是由 20 个产生事件/中断请求的 边沿检测器 组成。每一条输入线都可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求。
中断:要进入 NVIC,有相应的中断服务函数,需要 CPU 处理;
事件:不进入 NVIC,仅用于内部硬件自动控制,如:TIM、DMA、ADC;
二、EXTI工作原理
从 EXTI 功能框图可以看到有两条主线,一条是由输入线到 NVIC 中断控制器,一条是由输入线到脉冲发生器。这就恰恰是 EXTI 的两大部分功能,产生中断与产生事件,两者从硬件上就存在不同。
EXTI 功能框图的产生中断的线路,最终信号是流入 NVIC 控制器中。输入线是线路的信息输入端,它可以通过配置寄存器设置为任何一个 GPIO 口,或者是一些外设的事件。输入线一般都是存在电平变化的信号。
标号 ① 是一个 边沿检测电路,包括 边沿检测电路,上升沿触发选择寄存器(EXTI_RTSR)和 下降沿触发选择寄存器(EXTI_FTSR)。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号‘1’,就输出有效信号‘1’到标号 ② 部分电路,否则输入无效信号‘0’。边沿跳变的标准在于开始的时候对于上升沿触发选择寄存器或下降沿触发选择寄存器对应位的设置。
标号 ② 是一个 或门电路,它的两个信号输入端分别是 软件中断事件寄存器(EXTI_SWIER)和 边沿检测电路的输入信号。或门电路只要输入端有信号‘1’,就会输出‘1’,所以就会输出‘1’到标号 ③ 电路和标号 ④ 电路。通过对软件中断事件寄存器的读写操作就可以启动中断/事件线,即相当于输出有效信号‘1’到或门电路输入端。
标号③是一个 与门电路,它的两个信号输入端分别是 中断屏蔽寄存器(EXTI_IMR)和 标号 ② 电路输出信号。与门电路要求输入都为‘1’才输出‘1’,这样子的情况下,如果中断屏蔽寄存器(EXTI_IMR)设置为 0 时,不管从标号 ② 电路输出的信号特性如何,最终标号 ③ 电路输出的信号都是 0;假如中断屏蔽寄存器(EXTI_IMR)设置为 1 时,最终标号 ③ 电路输出的信号才由标号 ② 电路输出信号决定,这样子就可以简单控制 EXTI_IMR 来实现中断的目的。标号 ④ 电路输出‘1’就会把请求挂起寄存器(EXTI_PR)对应位置 1。
最后,请求挂起寄存器(EXTI_PR)的内容就输出到 NVIC 内,实现系统中断事件的控制。
产生事件线路是从标号 ② 之后与中断线路有所不用,之前的线路都是共用的。标号 ④ 是一个 与门,输入端来自 标号 ② 电路 以及来自于 事件屏蔽寄存器(EXTI_EMR)。如果 EXTI_EMR 寄存器设置为 0,那不管标号 ② 电路输出的信号是‘0’还是‘1’,最终标号 ④ 输出的是‘0’;如果 EXTI_EMR 寄存器设置为 1,最终标号 ④ 电路输出信号就由标号 ③ 电路输出的信号决定,这样子就可以简单的控制 EXTI_EMR 来实现是否产生事件的目的。
标号 ④ 电路输出有效信号 1 就会使脉冲发生器电路产生一个脉冲,而无效信号就不会使其产生脉冲信号。脉冲信号产生可以给其他外设电路使用,例如定时器,模拟数字转换器等,这样的脉冲信号一般用来触发 TIM 或者 ADC 开始转换。
产生中断线路目的使把输入信号输入到 NVIC,进一步运行中断服务函数,实现功能。而产生事件线路目的是传输一个脉冲信号给其他外设使用,属于硬件级功能。
EXTI 支持 23 个外部中断/事件请求,这些都是信息输入端,也就是上面提及到了输入线,具体如下:
- EXTI 线 0 ~ 15:对应外部 IO 口的输入中断
- EXTI 线 16:连接到 PVD 输出
- EXTI 线 17:连接到 RTC 闹钟事件
- EXTI 线 18:连接到 USB 唤醒事件
- EXTI 线 19:连接到以太网唤醒事件
- EXTI 线 20:连接到 USB OTG HS(在 FS 中配置)唤醒事件
- EXTI 线 21:连接到 RTC 入侵和时间戳事件
- EXTI 线 22:连接到 RTC 唤醒事件
从上面可以看出,STM32F407 供给 IO 口使用的中断线只有 16 个,但是 STM32F407 的 IO 口却远远不止 16 个,所以 STM32 把 GPIO 管脚 GPIOx_PIN_0 ~ GPIOx_PIN_15(x=A, B, C, D, E, F, G) 分别对应中断线 0 ~ 15。这样子每个中断线对应了最多 7 个 IO 口,以线 0 为例:它对应了 GPIOA_PIN_0、GPIOB_PIN_0、GPIOC_PIN_0、GPIOD_PIN_0、GPIOE_PIN_0、GPIOF_PIN_0 和 GPIOG_PIN_0。而中断线每次只能连接到 1 个 IO 口上,这样就需要通过配置决定对应的中断线配置到哪个 GPIO 上了。
三、EXTI和IO映射关系
SYSCFG,全称为 System Configuration Controller,即 系统配置控制器,用于外部中断映射配置等。我们使用 SYSCFG 外部中断配置寄存器配置 EXTI 中断线 0 ~ 15 具体对应到哪个具体 IO 口。
四、EXTI常用寄存器
4.1、中断屏蔽寄存器
4.2、事件屏蔽寄存器
4.3、上升沿触发选择寄存器
4.4、下降沿触发选择寄存器
4.5、软件中断事件寄存器
4.6、挂起寄存器
五、EXTI配置步骤
5.1、使能GPIO时钟
有关使能 GPIO 时钟的宏定义代码定义在 stm32f4xx_hal_rcc.h 和 stm32f4xx_hal_rcc_ex.h 头文件中。
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOB_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOC_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOH_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOHEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOHEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOD_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIODEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIODEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOE_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOEEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOEEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOF_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOG_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOGEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOGEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOI_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOIEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOIEN);\
UNUSED(tmpreg); \
} while(0U)
5.2、设置IO口模式和触发条件
使用 HAL 库封装的 HAL_GPIO_Init() 函数,我们就可以一步 设置 GPIO 输入模式,使能 SYSCFG 的时钟,设置 EXTI 和 IO 的对应关系,设置 EXTI 触发模式。这些步骤 HAL 库全部封装在 HAL_GPIO_Init() 函数里面,我们只需要设置好对应的参数,再调用 HAL_GPIO_Init() 函数即可完成配置。它的声明如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
该函数的第一个形参 GPIOx 用来指定端口号,可选值如下:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
第二个参数是 GPIO_InitTypeDef 类型的结构体变量,用来设置 GPIO 的工作模式,其定义如下:
typedef struct
{
uint32_t Pin; // 引脚号
uint32_t Mode; // 模式设置
uint32_t Pull; // 上下拉设置
uint32_t Speed; // 速度设置
uint32_t Alternate; // 复用功能设置
}GPIO_InitTypeDef;
成员 Pin 表示 引脚号,范围:GPIO_PIN_0 到 GPIO_PIN_15,另外还有 GPIO_PIN_All 和GPIO_PIN_MASK 可选。
#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_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 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 GPIO_PIN_MASK 0x0000FFFFU /* PIN mask for assert test */
成员 Mode 是 GPIO 的 模式选择,有以下选择项:
#define GPIO_MODE_IT_RISING 0x10110000U // 外部中断,上升沿触发检测
#define GPIO_MODE_IT_FALLING 0x10210000U // 外部中断,下降沿触发检测
#define GPIO_MODE_IT_RISING_FALLING 0x10310000U // 外部中断,双边沿触发检测
#define GPIO_MODE_EVT_RISING 0x10120000U // 外部事件,上升沿触发检测
#define GPIO_MODE_EVT_FALLING 0x10220000U // 外部事件,下降沿触发检测
#define GPIO_MODE_EVT_RISING_FALLING 0x10320000U // 外部事件,双边沿触发检测
成员 Pull 用于 配置上下拉电阻,有以下选择项:
#define GPIO_NOPULL 0x00000000U // 无上下拉
#define GPIO_PULLUP 0x00000001U // 上拉
#define GPIO_PULLDOWN 0x00000002U // 下拉
5.3、使能中断
5.3.1、设置中断优先级分组
HAL_NVIC_SetPriorityGrouping() 函数是设置中断优先级分组函数。其声明如下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
其中,参数 PriorityGroup 是 中断优先级分组号,可以选择范围如下:
#define NVIC_PRIORITYGROUP_0 0x00000007U /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1 0x00000006U /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2 0x00000005U /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3 0x00000004U /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4 0x00000003U /*!< 4 bits for pre-emption priority
0 bits for subpriority */
这个函数在一个工程里基本只调用一次,而且是在程序 HAL 库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。如果调用了多次,则以最后一次为准。
5.3.2、设置中断优先级
HAL_NVIC_SetPriority() 函数是设置中断优先级函数。其声明如下:
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
其中,参数 IRQn 是 中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。
typedef enum
{
EXTI0_IRQn = 6, /*!< EXTI Line0 Interrupt */
EXTI1_IRQn = 7, /*!< EXTI Line1 Interrupt */
EXTI2_IRQn = 8, /*!< EXTI Line2 Interrupt */
EXTI3_IRQn = 9, /*!< EXTI Line3 Interrupt */
EXTI4_IRQn = 10, /*!< EXTI Line4 Interrupt */
EXTI9_5_IRQn = 23, /*!< External Line[9:5] Interrupts */
EXTI15_10_IRQn = 40, /*!< External Line[15:10] Interrupts */
} IRQn_Type;
参数 PreemptPriority 是 抢占优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。
参数 SubPriority 是 响应优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。
5.3.3、使能中断
HAL_NVIC_EnableIRQ() 函数是中断使能函数。其声明如下:
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
其中,参数 IRQn 是 中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。
5.4、编写中断服务函数
每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU 将找不到中断服务函数)。中断服务函数接口厂家已经在 startup_stm32f407xx.s 中写好了。STM32F407 的IO 口外部中断函数只有 7 个,分别为:
void EXTI0_IRQHandler(void);
void EXTI1_IRQHandler(void);
void EXTI2_IRQHandler(void);
void EXTI3_IRQHandler(void);
void EXTI4_IRQHandler(void);
void EXTI9_5_IRQHandler(void);
void EXTI15_10_IRQHandler(void);
中断线 0 ~ 4,每个中断线对应一个中断函数,中断线 5~ 9 共用中断函数 EXTI9_5_IRQHandler()
,中断线 10 ~ 15 共用中断函数 EXTI15_10_IRQHandler()
。一般情况下,我们可以把中断控制逻辑直接编写在中断服务函数中,但是 HAL 库把中断处理过程进行了简单封装。
HAL 库为了用户使用方便,提供了一个中断通用入口函数 HAL_GPIO_EXTI_IRQHandler()
,在该函数内部直接调用回调函数 HAL_GPIO_EXTI_Callback()
。
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);
}
}
该函数实现的作用非常简单,通过入口参数 GPIO_Pin 判断中断来自哪个 IO 口,然后清除相应的中断标志位,最后调用回调函数 HAL_GPIO_EXTI_Callback() 实现控制逻辑。在所有的外部中断服务函数中直接调用外部中断共用处理函数 HAL_GPIO_EXTI_IRQHandler(),然后在回调函数HAL_GPIO_EXTI_Callback() 中通过判断中断是来自哪个 IO 口编写相应的中断服务控制逻辑。
六、原理图
K1、K2 和 K3 设计为按键按下时采样 GPIO 电平为低电平,而 KEY_UP 设置为按键按下时采用 GPIO 电平为高电平。并且,按键外部没有上下拉电阻,所以需要在 STM32F407 内部设置上下拉。。因此,K1、K2 和 K3 可以设置为 上拉模式,下降沿触发,KEY_UP 可以设置为 下拉模式,高电平触发。
七、程序源码
EXTI2 初始化函数内容如下:
/**
* @brief EXTI2初始化函数
*
*/
void EXTI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOE_CLK_ENABLE(); // 使能GPIOE的时钟
GPIO_InitStruct.Pin = GPIO_PIN_2; // GPIO引脚
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP; // 使用上拉
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); // GPIO初始化
HAL_NVIC_SetPriority(EXTI2_IRQn, 2, 0); // 设置中断优先级
HAL_NVIC_EnableIRQ(EXTI2_IRQn); // 使能中断
}
EXTI2 中断服务函数内容如下:
/**
* @brief EXTI2中断服务函数
*
*/
void EXTI2_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2); // 调用HAL库的EXTI公共中断处理函数
}
重写 HAL 库的 EXTI 回调函数:
/**
* @brief 重写HAL库的EXTI回调函数
*
* @param GPIO_Pin EXTI的引脚
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_2)
{
HAL_Delay(20); // 按键消抖
if (HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == GPIO_PIN_RESET) // 读取GPIO引脚电平,再次判断按键是否按下
{
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9); // GPIO引脚电平的翻转
}
}
}
main() 函数内容如下:
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
LED_Init();
EXTI2_Init();
while (1)
{
}
return 0;
}
如果使用 HAL_Delay() 函数卡死,那可能是因为 SysTick 的中断优先级是最低的,我们可以在 HAL_Init() 函数中将 SysTick 的中断优先级调高。