目录
前言
中断是MCU一个基础的功能,而什么是中断,我们为什么要使用中断,本节我们来介绍什么中断的相关概念与外部中断如何使用
1、什么是中断系统
到中午饭点了,切菜炒菜做点饭吧,卧槽憋不住了,快去上个厕所,在这个过程中,做饭可以代表我的主程序,而上厕所这个打断我主程序的行为就可以称为中断
而在单片机系统中,如果遇到需要紧急处理的突发事件时,CPU需要迅速的作出反应,暂停正在运行的程序来处理突发事件,这时就需要中断,发生突发事件从而打断当前程序,转而去处理这一事件,当处理完成后再回到原来被打断出继续执行原程序的过程
2、我们为什么要使用中断
在蒸饭的过程中,我需要做的也仅仅是等待饭蒸好,而这个过程我不可能一直站在这里等待它,我想去做别的事情,比如说舒舒服服的坐在沙发上看电视,在这个场景中,我是唯一具有处理能力的主体,不管是蒸饭、关水龙头还是看电视,同一个时间点上我只能干一件事情。但是,在我专心致志干一件事情时,总有许多或紧迫或不紧迫的事情突然出现在面前,都需要去关注,有些还需要我停下手头的工作马上去处理。只有在处理完之后,方能回头完成先前的任务。
中断机制不仅赋予了我处理意外情况的能力,如果我能充分发挥这个机制的妙用,就可以“同时”完成多个任务了。正是由于中断机制,MCU才能有条不紊地“同时”完成多个任务,中断机制实质上帮助MCU提高了并发“处理”能力。
3、STM32中断优先级
在现实生活中,一件事很急,但总有比他更急迫的事,中断也是如此,分为高优先级中断与低优先级中断,一旦同时触发中断,一定是先执行优先级高的中断任务,再去执行低优先级的中断任务,最后再回到主程序继续执行,如果低优先级中断已经触发了,高优先级也会打断低优先级的中断先执行。
而STM32F系列的MCU的NVIC(嵌套向量中断控制器)采用4位二进制数设置中断优先级,并且分为抢占优先级和次优先级,优先级数字越小表示优先级别越高,中断0优先级要大于中断1,如果两个优先级相同的中断触发,那就谁先触发就执行谁,先判断抢占优先级,相同的情况下再判断次优先级。
HAL库中断管理常用函数
函数名 | 功能 |
HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup) | 设置4位二进制数的优先级分组策略 |
HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority) | 设置某个中断的抢占优先级和次优先级 |
HAL_NVIC_EnableIRQ(IRQn_Type IRQn) | 启用某个中断 |
HAL_NVIC_DisableIRQ(IRQn_Type IRQn) | 禁用某个中断 |
HAL_NVIC_GetPriorityGrouping(void) | 返回当前的优先级分组策略 |
HAL_NVIC_GetPriority(IRQn_Type IRQn, uint32_t PriorityGroup, uint32_t* pPreemptPriority, uint32_t* pSubPriority) | 返回某个中断的抢占优先级和次优先级的数值 |
HAL_NVIC_GetPendingIRQ(IRQn_Type IRQn) | 检查某个中断是否被挂起 |
HAL_NVIC_SetPendingIRQ(IRQn_Type IRQn) | 设置某个中断的挂起标志,表示发生了中断 |
HAL_NVIC_ClearPendingIRQ(IRQn_Type IRQn) | 清除某个中断的挂起标志 |
以上函数相关的驱动程序的头文件在 stm32f1xx_hal_cortex.h 中,其中前三个用于CubeMX自动生成的代码,其他函数用于用户代码,几个常用函数的详细介绍如下,其中一些函数的详细定义和功能可查看源程序中的头文件
函数 HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
其中参数uint32_t PriorityGroup是用于设置优先级分组策略,可使用头文件中定义的几个宏定义常量,入夏表示,他们表示不同的分组策略
#define IS_NVIC_PRIORITY_GROUP(GROUP) (((GROUP) == NVIC_PRIORITYGROUP_0) || \
((GROUP) == NVIC_PRIORITYGROUP_1) || \
((GROUP) == NVIC_PRIORITYGROUP_2) || \
((GROUP) == NVIC_PRIORITYGROUP_3) || \
((GROUP) == NVIC_PRIORITYGROUP_4))
函数 HAL_NVIC_EnableIRQ(IRQn_Type IRQn)
函数的功能是在NVIC控制器中开启某个中断,只有在NVIC中开启某个中断后,NVIC才会对这个中断请求做出响应,执行相应的ISR,其中枚举类型IRQn_Type的参数IRQn是中断号的枚举值
4、STM32外部中断EXTI
基础的外部中断是MCU上GPIO引脚作为输入引脚时,由引脚上的电平变化所产生的中断,例如连接按键输入的引脚就可以由按键长生的不外部中断信号,此外还有一些内部信号作为EXTI中断线的输入,例如RTC唤醒事件信号连续在EXTI线22上
EXTI0至EXTI15这16个外部中断以GPIO引脚作为输入线,EXTO0可以选择PA0,PB0至PI0的某个引脚作为输入线,如果设置了PA0为EXTI0的输入线,那么PB0和PC0等就不能再作为EXTI0的输入线,以GPIO引脚作为输入线的EXTI可以用于检测外部输入事件,例如按键连续的GPIO引脚信号,通过外部中断方式检测比轮询模式更有效
HAL库外部中断相关函数
函数名 | 功能描述 |
__HAL_GPIO_EXTI_GET_IT(__EXTI_LINE__) (EXTI->PR & (__EXTI_LINE__)) | 检查某个外部中断线是否有挂起的中断 |
__HAL_GPIO_EXTI_CLEAR_IT(__EXTI_LINE__) (EXTI->PR = (__EXTI_LINE__)) | 清除某个外部中断线的挂起标志位 |
__HAL_GPIO_EXTI_GET_FLAG(__EXTI_LINE__) (EXTI->PR & (__EXTI_LINE__)) | 和函数1一样的功能 |
__HAL_GPIO_EXTI_CLEAR_FLAG(__EXTI_LINE__) (EXTI->PR = (__EXTI_LINE__)) | 和函数2一样的功能 |
__HAL_GPIO_EXTI_GENERATE_SWIT(__EXTI_LINE__) (EXTI->SWIER |= (__EXTI_LINE__)) | 在某个外部中断线上产生软中断 |
HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) | 外部中断ISR中调用的通用处理函数 |
HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) | 外部中断处理的回调函数,需要用户自己重新实现 |
外部中断相关函数的定义在文件stm32f1xx_hal_gpio.h中,相关参数可以跳转头文件查询
在HAL库中,以 __HAL 为前缀的都是宏函数,表中前几个函数都是宏函数,例如__HAL_GPIO_EXTI_GET_IT(__EXTI_LINE__) (EXTI->PR & (__EXTI_LINE__)) ,它的功能为检查外部中断挂起寄存器(EXTI_PR)中某个中断线的挂起标志为是否置位,参数(__EXTI_LINE__) 是某个外部中断线,用GPIO_PIN_0、GPIO_PIN_1等宏定义常量来表示。
函数的返回值只要不等于0(用宏RESET表示0),就表示外部中断线挂起标志位被置位,有未处理的中断事件
而函数 __HAL_GPIO_EXTI_CLEAR_IT(__EXTI_LINE__) (EXTI->PR = (__EXTI_LINE__)) 用于清除某个中断线的标志位,向外部中断挂起寄存器(EXTI_PR)的某个中断线写入1,就可以清除该中断线的挂起标志,在外部中断ISR里处理完中断后,我们需要调用这个函数清除挂起标志位,以便于再次相应下次中断
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
这个函数的代码很简单,如果检测到中断线GPIO_PIN的中断挂起标志位不为0,就清除中断挂起标志位,然后执行函数HAL_GPIO_EXTI_Callback(),这个函数是对中断进行相应处理的回调函数,它的代码框架在文件stm32f1xx_hal_gpio.c中,代码如下
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(GPIO_Pin);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_GPIO_EXTI_Callback could be implemented in the user file
*/
}
这个函数的前面有个修饰符__weak 就是用来定义弱函数的,所谓弱函数就是HAL库中预先定义的带有__weak修饰符的函数,如果用户没有重新实现这些函数,编译时就编译这些弱函数,如果在用户程序中重新实现了这些函数,那么就编译用户自己写的函数
在STM32CubeMX中,用户只需要搞清楚与中断事件对应的回调函数,然后重新实现回调函数即可,对于外部中断,只有一个中断事件源,所以只有一个回调函数HAL_GPIO_EXTI_Callback(),在对外部中断进行处理时,只需要重新实验这个函数即可。
5、STM32CubeMX配置外部中断
点击对应管脚,设置对应模式,外部中断为GPIO输入模式,原理为检测高低电平触发中断
而下面这就是外部中断的六种模式,上升沿就是低电平变为高电平的过程,而下降沿就是高电平变为低电平的过程,我们的按键选择下降沿触发
然后我们来配置外部中断的使能和优先级
然后我们将时钟树和项目管理配置完之后点击右上角的蓝色小按钮GENERATE CODE生成文件,将自己编写的代码放进代码沙盒中,这样下次再次更改CubeMX配置生成的文件就不会将用户的代码给清除了
在HAL库中,中断运行结束后不会立刻退出,而是会先进入相对应的中断回调函数,处理该函数中的代码之后,才会退出中断,所以在HAL库中我们一般将中断需要处理代码放在中断回调函数中,接下来我们编写一段重新实现中断回调函数的代码
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == KeyUP_Pin) //PA0 = KeyUP
{
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_2); //翻转LED
HAL_Delay(500); //软件消除按键抖动
}
else if(GPIO_Pin == KeyRight_Pin) //PE2 = KeyRight
{
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_4); //翻转另外一个LED
HAL_Delay(1000); //观察优先级的作用
}
else if(GPIO_Pin == KeyDown_Pin) //PE3 = KeyDown
{
__HAL_GPIO_EXTI_GENERATE_SWIT(GPIO_PIN_0); //产生EXTI0软中断
HAL_Delay(1000); //这个延迟是必要的,否则会由于抖动触发两次
}
}
最后,我们需要改写函数 HAL_GPIO_EXTI_IRQHandler() 来增强代码的性能,源代码它在检测到中断挂起标志后,先清除中断挂起标志后,再执行回调函数,一般的中断通用处理函数都是这样的处理流程,是为了硬件能及时响应下一次中断,但是对于检测按键输入的外部中断,这样是有问题的,因为清除中断挂起标志后,按键的抖动就会触发下一次中断,并将中断挂起标志置位,虽然再回调函数中使用了延时,但是回调函数退出后,NVIC检测到中断位被挂起,又会执行一次回调函数。
所以对于外部中断的按键检测输入,我们需要修改一下的 HAL_GPIO_EXTI_IRQHandler() 代码,将清除中断挂起标志位的功能放在后面,既修改为如下的代码,这样修改后就不存在问题了
// 未修改!!!
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
// 已修改!!!
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
HAL_GPIO_EXTI_Callback(GPIO_Pin);
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
}
}
但要注意,这个函数是头文件中自带的原始驱动文件,不存在代码沙箱,修改完这个函数后,重新生成代码会将这个代码变回为原来的样子,所以在使用CubeMX时,用户一定要将代码写进代码沙箱中,如果要更改HAL库原始文件,重新生成后也一定要再次更改!!