3、提高程序实时性神器之中断(IRQ)
1、主流的三种响应系统
-
轮询式系统
-
指的是在程序运行时,首先对所有的硬件进行初始化,然后在主程序中写一个死循环,需要运行的功能按照顺序进行执行
-
轮询系统是一种简单可靠的方式,一般适用于在只需要按照顺序执行的并且没有外部事件的影响的情况下。
-
但是主程序需要一直循环中,浪费内存资源
-
int main() { //1. 对所有的硬件进行初始化(LED、BEEP、KEY......) //2.进入死循环 while(1) { //点灯 //警报 //按键 } }
-
-
前后台系统
- 相比于轮询系统,前后台系统增加****中断****的概念,
- 如果外部事件发生,则在中断中进行处理,主程序在轮询系统中运行,
- 中断被称为前台,主程序中的while(1)就称为后台。
- 中断会终止后台程序的运行,然后跳转到对应的中断服务函数中去处理,处理完成后,在继续执行后台的程序。
- 如果使用前后台系统,可以极大程度的提高程序的实时响应能力,避免造成外部事件的缺失。
//不需要手动调用,当进程捕获到SIGINT信号时,系统会暂停主程序的执行,来执行该函数 void signal_handler(int signum) { //处理动作 } int main() { //1.对中断进行注册 signal(SIGINT,signal_handler); while(1) { //执行动作,while(1)不允许退出 } }
-
多任务系统
-
相比于前后台系统,多任务系统的外部事件也是在中断中进行响应,但是外部事件的处理是任务中进行处理。
-
任务具有优先级,优先级高的任务先处理,所以程序就会被分割为一个个的任务,
-
任务是一个独立的死循环,并且不能返回,可以由操作系统进行任务的调度,
-
程序段的实时响应能力又得到提升
//任务1
void * task1(void *arg)
{
while(1)
{
}
}
//任务2
void * task2(void *arg)
{
while(1)
{
}
}
int main()
{
//1.对所有的硬件进行初始化
//2.创建任务,并设置任务的属性(优先级、内存分配、时间片)
//3.交给系统的调度器去执行
}
2、那么中断是什么呢?
1、概念
- 中断指的是CPU来处理和响应外部发生的异常,
- 中断意味着打断,
- 比如打断正在做的事,然后去处理一个紧急的事,
- 处理完成后在继续做刚才没做完的事。
- 比如打游戏,女朋友来电话。
- 注意:中断是允许嵌套的!
2、中断源分析
- 中断源指的是中断发生的源头,
- 中断源也称为向量表
- 中断源在内核中已经定义好了
- 向量表在STM32F4中文参考手册参考。
- 在内核文档中明确介绍芯片中断
- Cortex-M4内核一共支持256个中断,其中有16个内核中断,240个外部中断,
- 对于STM32F407系列来说,只用到了一部分,
- 包含了10个内核中断(不可屏蔽中断,无法通过软件进行控制)、82个外部中断(可屏蔽中断,可以通过软件进行控制)。
3、那么如何管理和使用这些中断呢?
1、嵌套向量中断控制器(NVIC)
-
NVIC指的是嵌套向量中断控制器,属于内核中的外设,作用是管理所有的中断,
-
-
使能(打开)和失能(关闭)中断
-
NVIC管理中断通道的打开与关闭,
- 可以把NVIC理解为所有中断的开关,
- 想要使用中断发送中断请求,就必须提前打开中断的通道。
- 关于NVIC的使用都存储在一个结构体中,这个结构体和NVIC的函数接口都定义在misc.c和misc.h(关于内核的文档)中。
-
在mis.c文件中
如何配置使用中断,第一步则是配置NVIC中断优先级
-
使用NVIC_Inite()函数配置和使能
-
//NVIC中参数为结构体指针,且指针指向的是NVIC_InitTypeDef结构体类型 void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
-
-
- 选择中断通道则是选择接收哪一种中断类型,具体通道列表请看stm32f4xx.h
- 类似于GPIO外设配置,根据相关提示给结构体成员赋值
-
-
-
中断的优先级设置
-
NVIC利用4bit的优先级来管理所有的中断通道,
-
STM32中断的优先级分为两种:抢占式优先级(主优先级)+ 响应式优先级(子优先级)
-
抢占优先级(主优先级):抢占优先级高的中断可以打断正在执行的抢占优先级低的中断!!!
-
响应优先级(子优先级):在同时发生多个中断的情况下,响应优先级高的中断可先执行!!!
-
(1) 抢占优先级高的中断可以打断正在执行的抢占优先级低的中断
(2) 抢占优先级相同的中断同时发生,响应优先级高的中断先执行
(3) 抢占优先级相同的多个中断发生,响应优先级高的中断不能打断响应优先级低的中断
(4) 抢占优先级和响应优先级相同的多个中断同时发生,则按照向量表的中断编号来执行
-
-
每种都有16个优先级(0~15),数字越小,优先级越高
-
为什么要使用优先级:
- 如果同时发生多个中断请求,但是又不能同时处理,就根据中断请求的优先级来处理和响应中断。
-
-
为了方便用户管理和响应中断,NVIC可以对中断优先级进行分组,这样用户可以方便配置
-
-
注意:该函数必须在主程序的入口进行调用,并且整个项目应该只调用一次,因为设置好优先级分组之后就不应该随意更改分组,否则中断管理比较混乱。
-
2、外部中断/事件控制器 (EXTI)
-
基本概念
-
EXTI指的是外部中断/事件控制器,一共有23个,每个都有一个内部的边沿检测器,可以检测上升沿或者下降沿,每根线(每个IO端口)都可以产生事件或者中断。
-
每个边沿检测器独立开关
-
检测边沿 上升沿/下降沿
- 电平发生变化时,边沿跟着变换的
- 由高电平变为低电平产生下降沿
- 低电平变高电平产生上升沿
- 电平发生变化时,边沿跟着变换的
-
-
-
1. 中断函数无返回值的原因
中断函数通常用于响应硬件事件或异常情况,其核心目的是快速处理特定的任务并恢复系统的正常运行。由于中断的发生具有不可预测性和实时性,因此不需要向调用者提供任何反馈信息。具体原因如下:
- 实时性需求
中断函数的设计目标是尽快完成任务并释放资源,以便系统能够继续其他操作。如果引入返回值机制,则会增加额外的时间开销,违背了中断处理的高效性原则。 - 上下文切换特性
当发生中断时,CPU会暂停当前正在执行的任务并将控制权交给中断服务程序。一旦中断处理完毕,CPU立即恢复到被中断的状态。这种行为决定了中断函数无需也不应该通过返回值与外部交互。 - 功能单一化
中断函数的主要职责是对特定事件作出反应(如读取传感器数据、更新计时器等),而不是参与复杂的业务逻辑运算。因此,没有必要为其设计返回值接口。
2. 中断函数无参数的原因
同样基于中断的特点及其应用场景,中断函数一般不会接受显式的输入参数。以下是主要原因:
- 固定关联关系
中断号本身已经隐含了足够的信息来定位触发该中断的具体设备或事件源。例如,在Linux驱动开发中,dev_id
和irq
可以用来唯一标识某个硬件设备。这意味着即使没有显式传入参数,也可以通过全局环境或其他方式获取所需的数据。 - 减少复杂度
如果允许传递参数给中断函数,则可能需要维护额外的栈帧或者共享内存区域,这不仅增加了实现难度,还可能导致性能下降以及潜在的安全隐患。 - 独立性强
大多数情况下,中断处理过程完全依赖本地变量或预先配置好的静态数据结构即可满足需求。比如在网络协议栈中解析IP包头字段时,并不需要用户提供额外的帮助信息。
- 实时性需求
-
[!NOTE]
-
注意:每个GPIO引脚都可以配置为外部中断,但是和GPIO相关的外部中断线一共有16根,分别为EXTI0~EXTI15。
-
另外七根 EXTI 线连接方式如下:
-
EXTI 线 16 连接到 PVD 输出
-
EXTI 线 17 连接到 RTC 闹钟事件
-
EXTI 线 18 连接到 USB OTG FS 唤醒事件
-
EXTI 线 19 连接到以太网唤醒事件
-
EXTI 线 20 连接到 USB OTG HS(在 FS 中配置)唤醒事件
-
EXTI 线 21 连接到 RTC 入侵和时间戳事件
-
EXTI 线 22 连接到 RTC 唤醒事件
-
-
-
那么GPIO口这么多,外部中断线只有16根怎么分配?
-
思考:STM32F407系列有114个GPIO口,那如何和外部中断线进行关联?
-
通过映射的方式
-
在 STM32F407 中,每个 GPIO 引脚都有一个唯一的编号,表示为
GPIOx.PinN
,其中x
表示端口名称(如 A、B、C 等),PinN
表示引脚序号(范围为 0 ~ 15)。外部中断线 EXTI 的编号与其对应的 GPIO 引脚序号一致,即 EXTI0 对应 Pin0,EXTI1 对应 Pin1,依此类推直到 EXTI15 对应 Pin15。具体来说:
- 每个 GPIO 引脚都可以通过配置成为某一特定中断线的有效触发源。
- 同一中断线上的多个 GPIO 引脚之间存在竞争关系,仅有一个能被激活为真正的中断源。
-
-
使用多路选择器,每个端口下相同编号的引脚同时只能触发一个,各引脚之间为互斥关系
-
-
-
-
-
4、中断编写具体流程
-
NVIC 和 EXTI 的协作机制
- 当某个 GPIO 引脚检测到电平变化或边沿触发时,会通过 EXTI 控制器生成一个中断请求。
- 该请求会被发送至 NVIC,在 NVIC 内部经过优先级评估后决定是否立即响应此中断
- 如果当前没有更高优先级的任务正在执行,则 CPU 将跳转至对应的中断服务程序 (ISR)。
-
既然要使用IO端口进行信号接收,则需要
-
所有端口都具有外部中断功能。要使用外部中断线,必须将端口配置为输入模式,
-
将IO端口设置为输入模式
-
打开外设时钟
-
配置GPIO上拉下拉寄存器
//定义结构体变量 GPIO_InitTypeDef GPIO_InitStructure; //打开GPIO外设时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF , ENABLE); //配置相关属性 GPIO_InitStructure.GPIO_Pin =GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ; 进行初始化 GPIO_Init(GPIOF,&GPIO_InitStructure);
-
-
IO需要与EXTI中断线进行映射
-
利用SYSCFG外设的寄存器对GPIOA端口的引脚PA0以及EXTI0建立映射关系(自动建立)
-
//打开SYSCFG时钟 RCC_APB2PeriphResetCmd(RCC_APB2Periph_SYSCFG, ENABLE); //进行函数映射 SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOF,EXTI_PinSource5)
-
-
配置EXTI进行中断信号接收配置
-
定义EXTI外设的结构体变量,对结构体成员进行初始化(编号+模式+边沿+状态),如下
-
//配置EXTI外部中断线参数 EXTI_InitTypeDef EXTI_InitStructure; //哪个外部中断线开启 EXTI_InitStructure.EXTI_Line = EXTI_Line0; //中断/事件触发模式选择 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //触发器选择上升还是下降沿触发 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; //EXTI初始化 EXTI_Init(&EXTI_InitStructure);
-
-
EXITI配置之后,需要用NVIC来设置中断优先级
-
//定义NVIC结构体变量 NVIC_InitTypeDef NVIC_InitStructure; //EXTI 线路 5 到线路 9 被组合到了同一个中断向量中 NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn; //设置子优先级 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //设置抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //设置通道是否启用 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //NVIC 初始化 NVIC_Init(&NVIC_InitStructure);
-
-
中断函数构建
-
对EXTI中断通道配置完成后,需要编写对应的****中断服务函数****,中断服务函数的格式固定
-
void EXTI9_5_IRQHandler(void) { //1.判断外部中断线是否触发中断 if(EXTI_GetITStatus(EXTI_Line0) != RESET) { //清楚EXTI中断线的状态 EXTI_ClearITPendingBit(EXTI_Line5); } }
-
[!IMPORTANT]
- 注意:中断服务函数的名字是已经提前定义在启动文件中,启动文件是在程序运行之前先运行的一个汇编文件(startup_stm32f40_41xxx.s结尾的),用户在选择使用某个外设的中断的时候,必须从启动文件中复制中断服务函数的名称,比如EXTI5外部中断线的中断服务函数名字是EXTI9_5_IRQHandler
- 注意:在满足中断触发标志之后,需要在中断服务函数判断中断标志是否成立,并且在中断服务函数应该及时的去清除中断标志,因为需要考虑下一次中断的触发。
-
-
5、提高程序稳定性
-
由于我们所用为电平边沿触发,容易受到电磁干扰跳变。不是很可靠,不能准确检测是否为人为操作
-
边沿的产生可能是由于用户按下按键生成的,还有一种情况是外部因素影响出现的
-
这种情况需要避免的,比如由于EMI(电磁干扰)干扰导致电平发生了瞬间的变化,但是马上恢复了,此时MCU就会根据电平的变化来前台程序,此时相当于误触了。
-
解决该问题的方案有很多,比如软件开发工程师就需要添加一些条件来进行人为的检测,其实就是对按键进行消抖处理,消抖方式有很多,一般常用的延时消抖,如果打算实现准确的延时,就需要使用定时器。