NVIC和外部中断

一、基本概念

1.1 轮询、中断和DMA三种方式

对I/O设备的访问常见有轮询、中断、DMA三种方式;

1.1.1 什么是轮询

对I/O设备的程序轮询(Polling)的方式,是早期的计算机系统对I/O设备的一种管理方式。它定时对各种设备轮流询问一遍有无处理要求。轮流询问之后,有要求的,则加以处理。在处理I/O设备的要求之后,处理机返回继续工作。
尽管轮询需要时间,但轮询不比I/O设备的速度要快得多,所以一般不会发生不能及时处理的问题。
当然,再快的处理机,能处理的输入输出设备的数量也是有一定限度的。而且,程序轮询毕竟占据了CPU相当一部分处理时间,因此程序轮询是一种效率较低的方式,在现代计算机系统中已很少应用。

轮询也需要硬件支持,提供标志位来向CPU指示自身工作状态;

1.1.2 什么是中断

处理器的高速和输入输出设备的低速是一对矛盾,是设备管理要解决的一个重要问题。为了提高整体效率,减少在程序直接控制方式中CPU之间的数据传送,是很必要的。

中断是一种硬件机制,在这种机制中,设备会在就绪条件达成后主动通知CPU它需要引起注意,中断可以随时发生,需要CPU立即处理。因此,当CPU通过指示中断请求线收到中断信号时,CPU停止当前进程并通过将控制权传递给服务设备的中断处理程序来响应该中断。

不过,中断方式仍然存在一些问题。首先,现代计算机系统通常配置有各种各样的输入输出设备。如果这些I/O设备都同过中断处理方式进行并行操作,那么中断次数的急剧增加会造成CPU无法响应中断和出现数据丢失现象。
其次,如果I/O控制器的数据缓冲区比较小,在缓冲区装满数据之后将会发生中断。那么,在数据传送过程中,发生中断的机会较多,这将耗去大量的CPU处理时间。

中断可能是内部或者可预期的,如定时器,ADC中断等

也可能是外部或不可预期的,如外部中断,UART/IIC/SPI/CAN接收中断等

1.1.3 轮询 VS 中断

轮询

中断

本质

软件协议/程序

硬件机制

触发

顺序执行

随时发生

响应

主循环中的程序根据条件针对执行

执行主循环之外的中断服务子程序

和其他任务的关系

同步

异步

特点

不耗费硬件资源
需要CPU不停的查询,执行效率低
多任务间难以协调,容易丢失外部信号

需要硬件支持
CPU被动处理,执行效率高
与其他任务互不干扰,不会丢失外部信号

适用范围

适于处理对时间响应要求低的场合
程序设计简单

适于处理对响应要求非常高的事件
适于处理持续时间非常短的时间
适于低功耗应用
程序设计较复杂

1.1.4 前后台+时间片轮询 程序框架

单一的轮询程序框架程序执行效率低,任务间的互相干扰严重,引入中断后,我们在单片机的裸机开发上经常使用下述的 前后台+时间片轮询 程序框架。

前台为中断线, 定时器中断产生时间片轮询时基;

外部中断对异步信号及时响应,中断快进快出,只为主循环提供轮询标志;

后台为轮询线,通过查询各类标志判断任务就绪条件执行;

轮询周期相对稳定,实时性较高;

1.1.5 直接内存存取(DMA)方式

DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。

1.2 中断详解

1.2.1 中断的基本流程

1)中断使能

想使用中断,必须先将先将相应的中断使能,一般中断有全局、设备、事件三级使能管理,必须三级都使能,才能使用相应的中断机制。

设备/寄存器

简介

默认状态

全局中断使能

PRIMASK

内核中的特殊功能寄存器,这是个只有单一比特的寄存器。 在它被置 1 后,就关掉所有可屏蔽的异常,只剩下 NMI 和硬 fault 可以响应

打开

FAULTMASK

内核中的特殊功能寄存器,这是个只有 1 个位的寄存器。当它置 1 时,只有 NMI 才能响应,所有其它的异常,甚至是硬 fault,也通通闭嘴。它的缺省值也是 0,表示没有关异常。

打开

设备中断使能

由NVIC管理

有中断机制的设备通过中断线连接到NVIC,一般一台设备使用一个中断号,对应一条中断线,NVIC对系统所有的异常/中断进行管理,包括中断号的的使能,挂起,优先级和嵌套等操作。详见中断向量表和NVIC部分内容

关闭

事件中断使能

由各设备的中断控制器管理

同一个中断号下,可能有多个中断源,如串口就有发送完成,接收就绪,空闲,校验出错等多种中断,他们都共用一个中断号/线,再由其设备的中断控制器管理,分别还有其使能位和标志位寄存器。

关闭

只有三级中断管理都使能了,相应的中断源才能产生中断请求,触发中断机制。

2)中断的标志 / 挂起位 IF

  • 每一个中断源都有对应的中断标志位
  • 中断标志位将引发向CPU的中断请求(中断号)
  • 中断触发后,通过查询设备的IF位,可以了解到当次触发的中断源是哪个
  • 通过读/特殊的写操作可以清除中断标志位,避免重复触发

3)中断的上下文 —— 中断栈

中断发生后,程序将从正常流程中跳转到相应的中断服务处理子程执行,完成后再跳转回正常流程的原断点继续执行。

为了达成这样的跳转,必须在中断发生时保存现场,在中断结束时,恢复现场。而断点的现场信息,即当时各寄存器的信息,体现在当时的内核各寄存器中。

响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC, LR, R12以及R3-R0由硬件自动压入适当的堆栈中:

如果当响应异常时,当前的代码正在使用PSP,则压入PSP,也就是使用进程堆栈;否则就压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。

栈是一种带特殊顺序的数据结构,使用它可以方便的保存和恢复现场,即使在多层嵌套的情况下也不会出错。

4)中断的索引 —— 中断向量表

当发生了异常/中断并且要响应它时, CM3 需要定位其服务例程的入口地址。这些(中断服务子程序ISR)入口地址存储在所谓的“中断向量表”中。缺省情况下, CM3 认为该表位于零地址处,且各向量占用 4 字节。

  • 中断向量表是一段连续的存储空间,在复位后有默认的起始位置(0x0000 0000),可以通过改写中断向量基址寄存器重新定位的(0x0800 0000)
  • 每个中断在向量表中都有相应的表项,该表项的值为该中断对应的服务程序的地址(函数指针)。

以上信息可以对照工程中的启动文件来理解。

5)中断处理 —— 中断服务子程

中断服务子程ISR是预先编写的特定程序,当异常或中断这样的特殊事件发生时,紧急执行其来处理。

中断服务子程ISR的共同特点:

  • 是被CPU硬件自动调用的,而不是由其他程序在代码中调用;
  • 在ISR执行前、后, CPU自动进行了堆栈出入等操作;
  • 写成C函数的参数和返回值都应为void;
  • 需要记得清除中断标志位IF;
  • 如果允许中断嵌套,则在中断函数中保持中断允许;
  • 对每一个中断,必须调用对应的ISR

5)中断的嵌套 —— 优先级

  • 多个中断同时出现时,高优先级中断先得到响应
  • 中断优先级可以是固定的或编程指定的
    • 固定优先级: 根据中断向量表顺序,比如 ARM Cortex M3 内核中部分不可编程的异常
    • 设定优先级:每个中断都有优先级设置位,比如ARM Cortex M3 最多支持16个优先级
  • 相同优先级的中断,按先后顺序处理

1.2.2 STM32的中断管理 —— NVIC

NVIC的全称是Nested vectoredinterrupt controller,即嵌套向量中断控制器。是ARM为Cortex-M3内核设计的用于管理单片机中的中断源的使能和优先级响应。

Cortex-M3内核的NVIC理论上最多可以管理16个异常源和240个中断源,每个中断源都在 NVIC 的下列寄存器中“挂号”,拥有一个中断号和对应的一系列寄存器位:

1)使能与除能寄存器

CM3 中可以有 240 对使能位/除能位(SETENA 位/CLRENA 位),每个中断拥有一对。这 240 个对子分布在 8 对 32 位寄存器中(最后一对没有用完)。欲使能一个中断,我们需要写 1 到对应 SETENA 的位中;欲除能一个中断,你需要写 1 到对应的 CLRENA 位中。如果往它们中写 0,则不会有任何效果。写零无效是个很关键的设计理念:通过这种方式,使能/除能中断时只需把“当事位”写成 1,其它的位可以全部为零。再也不用像以前那样,害怕有些位被写入0 而破坏其对应的中断设置(反正现在写 0 没有效果了),从而实现每个中断都可以自顾地设置,而互不侵犯——只需单一的写指令,不再需要读-改-写三步曲。

2)挂起与解挂寄存器

如果中断发生时,正在处理同级或高优先级异常,或者被掩蔽,则中断不能立即得到响应。此时中断被挂起。中断的挂起状态可以通过“中断设置悬起寄存器(SETPEND)”和“中断挂起清除寄存器(CLRPEND)”来读取,还可以写它们来手工挂起中断。

3)优先级寄存器

每个外部中断都有一个对应的优先级寄存器,每个寄存器占用 8 位,即最多256个不同的优先级。不过实际硬件一般没有这么多个中断号,也无需使用这么多中断源。例如STM32F103RBT6芯片上,只使用了其中4位来设置优先级。

根据优先级组的设置,优先级可以被分为高低两个位段,分别是抢占优先级和亚优先级。对4位的分配,使得STM32F103RB有以下几种优先级组模式配置。

抢占优先级

次优先级

模式0

16(4)

0

模式1

8(3)

2(1)

模式2

4

4

模式3

2

8

模式4

0

16

4)活动状态寄存器

每个外部中断都有一个活动状态位。在处理器执行了其 ISR 的第一条指令后,它的活动位就被置 1,并且直到 ISR 返回时才硬件清零。由于支持嵌套,允许高优先级异常抢占某个 ISR。然而,哪怕中断被抢占,其活动状态也依然为 1。活动状态表明了哪些已经开始执行但还未执行完的程序,帮助其再被抢占后继续执行。

5)NVIC处理中断流程

结合上述硬件支持,可以理解NVIC的中断处理流程:

1、首先是看该IRQ是否使能,只有使能位为1,才能受理申请,如果是禁能状态,则直接无视;
2、然后查看优先级寄存器IP[IRQn],该IRQ的优先级;
3、再通过查看ACTIVE可知,目前处于活动状态的所有中断;
4、如果有正在活动状态的中断(即正在服务中),则进行中断优先级比较。如果活动的中断优先级高,则先挂起这个IRQ。    反复此步,直到没有更高优先级的中断需要服务;
5、轮到这个IRQ享受服务(由编号查向量表进入对应的中断服务程序,服务开始后置位它对应的中断活动状态位),解挂。
第5步里,就是NVIC对挂起和解挂的操作,这算是CM3内核的硬件自动完成的。但它也允许软件来手动干预,手动挂起相当于来一次软件IRQ申请,手动解挂相当于软件撤消IRQ申请。

二、外部中断控制器

EXTI(External interrupt/event controller)—外部中断/事件控制器,管理了控制器的 20 个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。 EXTI 可以实现对每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。

2.1 EXTI中断线

STM32 的每个 IO 都可以作为外部中断的中断输入口,这点也是 STM32 的强大之处。 STM32F103 的中断控制器支持 19 个外部中断/事件请求(互联型产品多一条)。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。 STM32F103 的20个外部中断为:

从上面可以看出, STM32 供 IO 口使用的中断线只有 16 个,但是 STM32 的 IO 口却远远不止 16 个,那么 STM32 是怎么把 16 个中断线和 IO 口一一对应起来的呢?于是 STM32 就这样设计, GPIO 的管脚GPIOx.0~GPIOx.15(x=A,B,C,D,E, F,G)分别对应中断线 0~15。这样每个中断线对应了最多 7 个 IO 口,以线 0 为例:它对应了 GPIOA.0、 GPIOB.0、 GPIOC.0、GPIOD.0、 GPIOE.0、 GPIOF.0、 GPIOG.0。而中断线每次只能连接到1 个 IO 口上,这样就需要通过配置来决定对应的中断线配置到哪个 GPIO 上了。下面我们看看 GPIO 跟中断线的映射关系图:

这些外部中断线并不都独占一个中断号,0/1/2/3/4独占一条;

5-9,10-15分别共占一条;相应的他们的ISR也是共用的,进入后需要先查询其标志位IF来判断具体是哪条中断线触发;



 

2.2 EXTI功能简介

EXTI 0~15完全一致,我们分析其中一个即可;

其内部 可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件上就有所不同。

2.2.1 中断过程

首先我们来看图 中红色虚线指示的电路流程。它是一个产生中断的线路,最终信号流入到NVIC 控制器内。
1# 是输入线, EXTI 控制器有 19 个中断/事件输入线,这些输入线可以通过寄存器设置为任意一个 GPIO,也可以是一些外设的事件,这部分内容我们将在后面专门讲解。输入线一般是存在电平变化的信号。
2# 是一个边沿检测电路,它会根据上升沿触发选择寄存器 (EXTI_RTSR) 和下降沿触发选择寄存器 (EXTI_FTSR) 对应位的设置来控制信号触发。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号 1 给3# 电路,否则输出无效信号 0。而 EXTI_RTSR 和 EXTI_FTSR 两个寄存器可以控制器需要检测哪些类型的电平跳变过程,可以是只有上升沿触发、
只有下降沿触发或者上升沿和下降沿都触发。
3# 电路实际就是一个或门电路,它一个输入来自2# 电路,另外一个输入来自软件中断事件寄存器 (EXTI_SWIER)。 EXTI_SWIER 允许我们通过程序控制就可以启动中断/事件线,这在某些地方非常有用。我们知道或门的作用就是有 1 就为 1,所以这两个输入随便一个有有效信号 1就可以输出 1 给4# 和6# 电路。
4# 电路是一个与门电路,它一个输入是3# 电路,另外一个输入来自中断屏蔽寄存器(EXTI_IMR)。与门电路要求输入都为 1 才输出 1,导致的结果是如果 EXTI_IMR 设置为 0 时,那不管3# 电路的输出信号是 1 还是 0,最终4# 电路输出的信号都为 0;如果 EXTI_IMR设置为 1 时,最终编4# 电路输出的信号才由3# 电路的输出信号决定,这样我们可以简单的控制 EXTI_IMR 来实现是否产生中断的目的。4# 电路输出的信号会被保存到挂起寄存器(EXTI_PR) 内,如果确定4# 电路输出为 1 就会把 EXTI_PR 对应位置 1。
5# 是将 EXTI_PR 寄存器内容输出到 NVIC 内,从而实现系统中断事件控制。

2.2.2 事件过程

接下来我们来看看绿色虚线指示的电路流程。所谓事件,它的触发源于中断相同,但它的结果并不向中断一样,导致CPU中断主程序去执行ISR;它将产生产生一个脉冲信号,输入给有事件机制的某个外设,让外设接收到信号后,按预定的方式动作。可以看到其机制的实现,并不需要经过CPU,更无需打断主程序,即可实现一些简单的硬件动作。

产生事件线路是在3# 电路之后与中断线路有所不同,之前电路都是共用的。6# 电路是一个与门,它一个输入来自3# 电路,另外一个输入来自事件屏蔽寄存器 (EXTI_EMR)。如果EXTI_EMR 设置为 0 时,那不管3# 电路的输出信号是 1 还是 0,最终6# 电路输出的信号都为 0;如果 EXTI_EMR 设置为 1 时,最终6# 电路输出的信号才由3# 电路的输出信号决定,这样我们可以简单的控制 EXTI_EMR 来实现是否产生事件的目的。

7# 是一个脉冲发生器电路,当它的输入端,即6# 电路的输出端,是一个有效信号 1 时就会产生一个脉冲;如果输入端是无效信号就不会输出脉冲。

8# 是一个脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他外设电路使用,比如定时器 TIM、模拟数字转换器 ADC 等等,这样的脉冲信号一般用来触发 TIM 或者 ADC开始转换。

产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。

另外, EXTI 是在 APB2 总线上的,在编程时候需要注意到这点。

2.3 EXTI的HAL库编程

接下来我们来看看使用 HAL 库配置外部的一般步骤。 HAL 中外部相关配置函数和定义在文件stm32f1xx_hal_exti.h 和 stm32f1xx_hal_exti.c 文件中。

1)使能 IO 口时钟

首先,我们要使用 IO 口作为中断输入,所以我们要使能响应的 IO 口时钟,具体的操作方法跟我们按键实验是一致的,这里就不做过多讲解。

2)设置 IO 模式,触发条件,设置 IO 口与中断线的映射关系。

当我们使用 HAL 库的时候,调用函数 HAL_GPIO_Init 完成的。例如我们要设置 PA0 链接中断线 0.,并且为下降沿触发,代码为:

GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_0; //PA0
GPIO_Initure.Mode=GPIO_MODE_IT_FALLING; //上升沿触发
GPIO_Initure.Pull=GPIO_PULLUP; //是否上下拉要看按键连接关系
HAL_GPIO_Init(GPIOA,&GPIO_Initure);

是否上下拉要看按键的硬件连接关系

因为我们这样初始的是 PA0,调用该函数后中断线 0 会自动连接到 PA0。
如果某个时间,我们又同样的方式初始化了 PB0,那么 PA0 与中断线的链接将被覆盖,而直接连接 PB0 到中断线 0

3)配置中断优先级(NVIC),并使能中断

我们设置好中断线和 GPIO 映射关系,然后又设置好了中断的触发模式等初始化参数。既然是外部中断,涉及到中断我们当然还要设置 NVIC 中断优先级。这个在前面已经讲解过,这里我们就接着上面的范例,设置中断线 0 的中断优先级并使能外部中断 0 的方法为:

HAL_NVIC_SetPriority(EXTI0_IRQn,2,2); //抢占优先级为 2,子优先级为 2
HAL_NVIC_EnableIRQ(EXTI0_IRQn); //使能中断线 0

4)编写中断服务函数

我们配置完中断优先级之后,接着要做的就是编写中断服务函数。中断服务函数的名字是在 HAL 库中事先有定义的。这里需要说明一下, STM32F1 的 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。一般情况下,我们可以把中断控制逻辑直接编写在中断服务函数中,但是 HAL 库把中断处理过程进行了简单封装,请看下面步骤 5 讲解 。

5) 编写中断处理回调函数 HAL_GPIO_EXTI_Callback

在使用 HAL 库的时候,我们也可以使用标准库一样,在中断服务函数中编写控制逻辑。
但 是 HAL 库 为了用户使用方便 ,它提供了一个中断通用入口参数HAL_GPIO_EXTI_IRQHandler,在该函数内部直接调用 回调函数 HAL_GPIO_EXTI_Callback
我们可以看看 HAL_GPIO_EXTI_IRQHandler 函数定义:

Void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
    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()实现控制逻辑。所以我们编写中断控制逻辑将跟串口实验类似,在中断服务函数中直接调用外部中断共用处理函数HAL_GPIO_EXTI_IRQHandler,然后在回调函数 HAL_GPIO_EXTI_Callback 中通过判断中断是来自哪个 IO 口编写相应的中断服务子程序ISR控制逻辑。
通过以上几个步骤的设置,我们就可以正常使用外部中断了。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值