以下内容全部来自正点原子,本人只是对主要知识点进行整理,方便以后查看。
一、串口配置
想要使用外部中断就需要对针脚进行配置,具体串口配置方法可以看stm32f407关于串口配置的各种函数(一)(基于HAL库)
比如对F端口10引脚设置中断:
void extix_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
gpio_init_struct.Pin = GPIO_PIN_10;
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿触发 */
gpio_init_struct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOF, &gpio_init_struct); /* 配置为下降沿触发中断 */
}
二、外部中断相关知识
中断就是在程序运行时突然收到中断请求,去运行中断函数,运行完中断函数后返回原来被中断的地方继续正常运行函数。
2.1可用外部中断口(EXTI)
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 唤醒事件
用户经常可以用到的外部中断有16个,分别为EXTI0~EXTI15,正好对应引脚GPIO_PIN_0~GPIO_PIN_15。
我们知道,不同端口都有16个引脚,那么每个端口的不同引脚都是独立的外部中断吗?当然不是。GPIOA_0(表示A端口0引脚)的中断和GPIOB_0、GPIOC_0等等不同端口的引脚0用的都是同一个EXTI0中断,总的来说就是:如果我对GPIOA_0配置了中断,那么GPIOB_0就不能配置中断;对GPIOA_5配置了中断,那么GPIOF_5就不能配置中断。
STM32F407 的 IO 口外部中断函数只有 7 个,分别为:
void EXTI0_IRQHandler();
void EXTI1_IRQHandler();
void EXTI2_IRQHandler();
void EXTI3_IRQHandler();
void EXTI4_IRQHandler();
void EXTI9_5_IRQHandler();
void EXTI15_10_IRQHandler();
中断线0-4,每个中断线对应一个中断函数,中断线5-9共用中断函数EXTI9_5_IRQHandler, 中断线 10-15 共用中断函数 EXTI15_10_IRQHandler
2.2外部中断配置的相关函数
中断使能函数:配置中断首先需要将中断进行使能:HAL_NVIC_EnableIRQ(EXTIx_IRQn)
比如对中断0进行使能
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
中断失能函数:用于对已经使能的中断进行失能
HAL_NVIC_DisableIRQ(EXTI0_IRQn);
系统复位函数:用于对系统进行复位(没有输入函数)
HAL_NVIC_SystemReset();
中断优先级分组函数:void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
可以选择范围:NVIC_PRIORITYGROUP_0 到 NVIC_PRIORITYGROUP_4(共 5 组)。
不同分组的主优先级(也叫抢占优先级)和子优先级(也叫响应优先级)的等级数如下:
优先级分组 | 主优先级 | 子优先级 | 描述 |
NVIC_PriorityGroup_0 | 0 | 0-15 | 主-0bit,子-4bit |
NVIC_PriorityGroup_1 | 0-1 | 0-7 | 主-1bit,子-3bit |
NVIC_PriorityGroup_2 | 0-3 | 0-3 | 主-2bit,子-2bit |
NVIC_PriorityGroup_3 | 0-7 | 0-1 | 主-3bit,子-1bit |
NVIC_PriorityGroup_4 | 0-15 | 0 | 主-4bit,子-0bit |
例如将优先级分组设为2级:
HAL_NVIC_SetPriorityGrouping(NVIC_PriorityGroup_2)
设定为分组2后,可以使用的主优先级就只有0~3,子优先级也只有0~3。
注:HAL的初始化函数 HAL_Init() 已经将优先级分组默认设为了第4组
中断优先级函数:void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
比如需要EXTI0的抢占优先级设为2,响应优先级设为1,则代码为:
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 1);
注:在正点原子的实例中,并没有对中断优先级分组进行配置,也就是说它的分组是用的默认的4分组,4分组响应优先级应该只有0才对,但是在它对中断优先级进行配置时用的响应优先级为2,是它出错了,还是它在什么地方重新分组了但是我没有发现?
2.3、中断服务函数和HAL库的中断服务函数
中断服务函数就是产生中断后要执行的那个函数。
中断标志位清理函数:__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
系统产生中断后会将相关中断标志位设为1,系统会根据标志位运动中断服务函数,所以需要在运行中断服务函数时需要先将标志位清零,否则此中断会一直运行。
stm32有它自身的中断服务函数:EXTIx_IRQHandler();x为中断代号,比如EXTI0的中断服务函数就是:
void EXTI0_IRQHandler(void)
{
/*写上需要执行的代码*/
}
HAL为了方便,也对中断要进行的函数进行了封装,函数为:HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
其定义为:
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提供的函数中,先将中断标志位清零,然后执行HAL_GPIO_EXTI_Callback(GPIO_Pin);函数,用户可以在HAL_GPIO_EXTI_Callback(GPIO_Pin)函数中编写自己需要的代码。
比如我的中断是引脚0引起的,我的中断可以写成:
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/*写上需要执行的代码*/
}
产生中断后执行顺序为:
可以发现,其实在 EXTI0_IRQHandler(void)函数中就可以直接先清理标志位,然后再执行自己想要执行的代码。
这里的代码却是先转到函数HAL_GPIO_EXTI_IRQHandler(GPIO_PIN),在HAL_GPIO_EXTI_IRQHandler(GPIO_PIN)函数里面清理标志位,再转到函数HAL_GPIO_EXTI_Callback(GPIO_PIN),在HAL_GPIO_EXTI_Callback(GPIO_PIN)中才是自己想要执行的代码,这样确实是复杂化了,但是这里的中断只配置了一个,当需要配置很多中断,并且不同中断直接需要执行同一个函数的时候,这种嵌套结构反而会降低编程量。
三、实战篇
现在有两个按键,分别连接在GPIOA_0和GPIOA_1,还有两个led灯,分别连接在GPIOF_10和GPIOG_11,现在要求用中断写一个函数,当KEY1按下时LED1灯状态反转,当KEY2按下时LED2灯状态反转。
#include "stm32f4xx.h"
#include "core_cm4.h"
#include "stm32f4xx_hal.h"
#include "stdio.h"
/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓初始化led↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */
void led_int(void)
{
GPIO_InitTypeDef gpio_init_struct; /* 定义结构体 */
gpio_init_struct.Pin = GPIO_PIN_10; /* LED1引脚 */
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(GPIOF, &gpio_init_struct); /* 初始化LED1引脚 */
gpio_init_struct.Pin = GPIO_PIN_11; /* LED2引脚 */
HAL_GPIO_Init(GPIOF, &gpio_init_struct); /* 初始化LED2引脚 */
}
/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓对按键的中断进行初始化↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */
void extix_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
gpio_init_struct.Pin = GPIO_PIN_0;
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿触发 */
gpio_init_struct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &gpio_init_struct); /* KEY0配置为下降沿触发中断 */
gpio_init_struct.Pin = GPIO_PIN_1;
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿触发 */
gpio_init_struct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &gpio_init_struct); /* KEY1配置为下降沿触发中断 */
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 2); /* 抢占0,子优先级2 */
HAL_NVIC_EnableIRQ(EXTI0_IRQn); /* 使能中断线0 */
HAL_NVIC_SetPriority(EXTI1_IRQn, 1, 2); /* 抢占1,子优先级2 */
HAL_NVIC_EnableIRQ(EXTI1_IRQn); /* 使能中断线1 */
}
/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓对中断0进行初始化↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); /* 调用中断处理公用函数 清除KEY0所在中断线 的中断标志位 */
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}
/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓对中断1进行初始化↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */
void EXTI1_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1); /* 调用中断处理公用函数 清除KEY0所在中断线 的中断标志位 */
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_1); /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}
/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓HAL的回调函数↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
delay_ms(20); /* 消抖 */
switch(GPIO_Pin)
{
case GPIO_PIN_0:
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)==0) /* 判断按键是不是按下了 */
{
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
}
break;
case GPIO_PIN_1:
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1)==0) /* 判断按键是不是按下了 */
{
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_11);
}
break;
default : break;
}
}
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能A端口时钟 */
__HAL_RCC_GPIOF_CLK_ENABLE(); /* 使能F端口时钟 */
led_init(); /* 对LED串口进行初始化*/
extix_init; /* 对KEY串口进行初始化*/
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);/* 将LED初始化*/
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_11,GPIO_PIN_RESET);
while(1)
{
delay_ms(1000);
}
}
注:代码中用的delay函数是用的正点原子自带的,这里用来表示一下,具体delay函数在后面时钟才会学到。需要特地注意的是,一般用的delay函数都是通过时钟中断实现的,但是在按键中断中用时钟中断会造成两个中断冲突,所以一般来说,在一个中断中使用其他中断要非常注意两个中断的抢占优先级。
正点原子这里的delay函数并不是用中断实现的,而是通过监视时钟参数实现的,后面会学到。