网络知识 娱乐 中断-NVIC与EXTI外设详解(超全面)

中断-NVIC与EXTI外设详解(超全面)

✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转STM32
💬推荐一款模拟面试、刷题神器,从基础到大厂面试题👉点击跳转刷题网站进行注册学习

目录

  • 一.NVIC-嵌套向量中断控制器
    • 1.中断向量表
    • 2.NVIC内核外设寄存器
    • 3.中断编程
  • 二.EXTI—外部中断/事件控制器
    • 1.外部中断/事件线路映像
    • 2.EXTI功能框图
    • 3.选择中断线与EXTI 初始化结构体详解
  • 三.外部中断控制实验
    • 实验原理
    • 编程要点
    • 实验效果
  • 四.总结

一.NVIC-嵌套向量中断控制器

NVIC :嵌套向量中断控制器,属于内核外设,管理着包括内核和片上所有外设的中断相关的功能。

这里解释一下片上外设与内核外设他们都在芯片里面,但内核外设是在内核CPU里面,片上外设就是内核之外咯。
在这里插入图片描述
在这里插入图片描述

NVIC 是嵌套向量中断控制器,控制着整个芯片中断相关的功能,它跟内核紧密耦合,是内核里面的一个外设。但是各个芯片厂商在设计芯片的时候会对 Cortex-M3 内核里面的 NVIC 进行裁剪,把不需要的部分去掉,所以STM32 的 NVIC 是 Cortex-M3 的 NVIC 的一个子集。

几个关于内核外设重要的库文件:
Cortex-M3 内核的外设也比较多,但STM32并没有用到这么多内核外设对其进行了裁剪,STM32重要的内核外设用到的库函数放在了misc.c文件之中所以core_cm3.c文件用的较少。

core_cm3.c:内核外设的驱动固件库
core_cm3.h:实现了内核(CPU)里面的外设的寄存器映射,还有很多关于内核外设的库函数。
misc.h:NVIC_InitTypeDef结构体,以及库函数的参数和声明
misc.c:NVIC(嵌套向量中断控制器)、SysTick(系统滴答定时器)相关函数

1.中断向量表

CM3 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有 256级的可编程中断设置。但 STM32 并没有使用 CM3 内核的全部东西,而是只用了它的一部分。STM32 有 84 个中断,包括 16 个内核中断和 68 个可屏蔽中断,具有 16 级可编程的中断优先级。而我们常用的就是这 68 个可屏蔽中断,但是 STM32 的 68 个可屏蔽中断,在 STM32F103 系列上面,又只有 60 个(在 107 系列才有 68 个)。

而我要讲的是103系列其中系统异常有 8 个(如果把 Reset 和 HardFault 也算上的话就是 10 个),外部中断有 60个。除了个别中断的优先级被定死外,其它中断的优先级都是可编程的。

下面灰色的就是系统异常(中断),中断就是异常,异常就是中断这里就不区分了,表中的优先级是硬件编号,数字越小优先级越高这里复位中断的编号最小,所以一按板子上的复位键会立马执行复位程序。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.NVIC内核外设寄存器

NVIC管理着中断向量表中的60个中断
在固件库中,NVIC 的结构体定义给每个寄存器都预留了很多位,恐怕为的是日后扩展功能。不过 STM32F103 可用不了这么多,只是用了部分而已

在这里插入图片描述
具体使用了各个寄存器使用了多少个请看图

在这里插入图片描述
在这里插入图片描述

  • ISER[8]:ISER 全称是:Interrupt Set-Enable Registers:这是一个中断使能寄存器组(有8个这样的寄存器)。上面STM32F103 的可屏蔽中断只有 60 个,一个寄存器有32位一位可以表示一个中断两个寄存器总共可以表示 64 个中断。而 STM32F103 只用了其中的前 60 位。所以对我们来说,有用的就是两个(ISER[0]和 ISER[1]),ISER[0]的 bit0-bit31 分别对应中断 0-31。ISER[1]的 bit0-27 对应中中断32~59;这样总共 60 个中断就分别对应上了。你要使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。

在这里插入图片描述

  • ICER[8]:全称是:Interrupt Clear-Enable Registers:是一个中断除能寄存器组。该寄存器组与 ISER 的作用恰好相反,是用来清除某个中断的使能的。向寄存器写1清除写0无效。

  • ISPR[8]:全称是:Interrupt Set-Pending Registers:是一个中断挂起控制寄存器组。每个位对应的中断和 ISER 是一样的。通过置 1当置位中断挂起寄存器的时候,相应的中断将会被挂起,此时这个中断将不会立即执行,而是等待可执行的时候再执行;比如高低级别的中断同时产生,就先挂起低级别的中断,等高级别的中断执行完毕,解除并执行低级中断这个过程一般是自发进行

  • ICPR[8]:全称是:Interrupt Clear-Pending Registers:是一个中断解挂控制寄存器组。其作用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断解除挂起。写 0 无效。

  • IABR[8]:全称是:Interrupt Active Bit Registers:是一个中断激活标志位寄存器组。对应位所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。

  • IP[240]:全称是:Interrupt Priority Registers:是一个中断优先级控制的寄存器组。这个寄存器组相当重要!STM32 的中断分组与这个寄存器组密切相关。240 个 8bit 的寄存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。而 STM32 只用到了其中的前 60 个。IP[59]-IP[0]分别对应中断 59~0也就是说:一个中断需要一个IP寄存器来配置优先级。 比如说中断硬件编号为20的中断配置的寄存器就为IP[20]

在这里插入图片描述
看图所知这里只分配了80个IP寄存器但对我们配置60中断绰绰有余,这里建议一下一定要去看看Cortex-M3编程手册有关NIVC的寄存器,这样心底有个底。

IP寄存器 宽度为 8bit,原则上每个外部中断可配置的优先级为 0~255(十进制),数值越小,优先级越高。但是绝大多数 CM3 芯片都会精简设计,以致实际上支持的优先级数减少,在F103 中,只使用了高 4bit,如下所示:

在这里插入图片描述
用于表达优先级的这 4bit,又被分组成抢占优先级和响应优先级

STM32 将中断分为 5 个组,组 0-4。该分组的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。
在这里插入图片描述
比如组2来说:2位配置抢占式优先级(00 01 10 11)换成十进制不就是0~3嘛,2位配置响应式优先级0 ~3,数字越小优先级越高其他分组以此类推。
注意:组1抢占式优先级0位,那就没有抢占式优先级,

  • 配置分组

在系统代码执行的过程只进行一次中断优先级分组,设置分组之后一般不会进行变动,不然中断执行会混乱,如:假设你分成组2,抢占式优先级有2位,后面改成组3的话,就会变成3位抢占式优先级,则之前的一位响应优先级变成了抢占式优先级,则之前配置的抢占式和响应优先级的值就不确定了乱套了。

在这里插入图片描述

  • 抢占式优先级与响应式优先级的区别
    在这里插入图片描述
    加深理解:
    1.抢占式优先级高的可以打断正在执行的(抢占式优先级低)中断(挂起),转而执行抢占式优先级高的中断执行完毕后在返回原来(抢占式优先级低)中断继续执行,这就是所谓的中断嵌套。而响应优先级并不能嵌套

2.若抢占式优先级与响应式优先级都相同则硬件编号(在中断向量表的排序顺序)小的先执行

3.中断编程

一般中断使能一般有两个门,外设使能相应的中断然后送入NVIC再使能,外设使能中断是小门,NVIC使能中断是大门,只有都使能才能响应中断。
请添加图片描述

1.使能外设某个中断,这个具体由每个外设的相关中断使能位控制。

2.配置中断优先级分组,然后初始化 NVIC_InitTypeDef 结构体,设置抢占优先级和子优先级,使能中断请求。NVIC_InitTypeDef 结构体在固件库头文件 misc.h 中定义。

配置中断优先级分组
在这里插入图片描述
初始化 NVIC_InitTypeDef 结构体

在这里插入图片描述

  • NVIC_IROChannel:用来设置中断源,不同的中断中断源不一样,且不可写错,即使写错了程序也不会报错,只会导致不响应中断。具体的成员配置可参考stm32f10x.h 头文件里面的 IRQn_Type 结构体定义,这个结构体包含了所有的中断源。

在这里插入图片描述

  • NVIC_IRQChannelPreemptionPriority:抢占优先级,具体的值要根据优先级分组来确定。

  • NVIC_IRQChannelSubPriority:子优先级(响应优先级),具体的值要根据优先级分组来确定 。

  • NVIC_IRQChannelCmd:中断使能(ENABLE)或者(DISABLE)。操作的是 NVIC_ISER 和 NVIC_ICER 这两个寄存器。

配置好 NVIC_InitTypeDef 结构体然后就调用NVIC_Init()函数,由函数将参数写入寄存器

现在来具体来分析一下这个函数加深我们对NVIC寄存器的理解

void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
{
  uint32_t tmppriority = 0x00, tmppre = 0x00, tmpsub = 0x0F;
  
  /* Check the parameters */
  assert_param(IS_FUNCTIONAL_STATE(NVIC_InitStruct->NVIC_IRQChannelCmd));
  assert_param(IS_NVIC_PREEMPTION_PRIORITY(NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority));  
  assert_param(IS_NVIC_SUB_PRIORITY(NVIC_InitStruct->NVIC_IRQChannelSubPriority));
    
  if (NVIC_InitStruct->NVIC_IRQChannelCmd != DISABLE)
  {
    /* Compute the Corresponding IRQ Priority --------------------------------*/    
    tmppriority = (0x700 - ((SCB->AIRCR) & (uint32_t)0x700))>> 0x08;
    tmppre = (0x4 - tmppriority);
    tmpsub = tmpsub >> tmppriority;

    tmppriority = (uint32_t)NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority << tmppre;
    tmppriority |=  NVIC_InitStruct->NVIC_IRQChannelSubPriority & tmpsub;
    tmppriority = tmppriority << 0x04;
        
    NVIC->IP[NVIC_InitStruct->NVIC_IRQChannel] = tmppriority;
    
    /* Enable the Selected IRQ Channels --------------------------------------*/
    NVIC->ISER[NVIC_InitStruct->NVIC_IRQChannel >> 0x05] =
      (uint32_t)0x01 << (NVIC_InitStruct->NVIC_IRQChannel & (uint8_t)0x1F);
  }
  else
  {
    /* Disable the Selected IRQ Channels -------------------------------------*/
    NVIC->ICER[NVIC_InitStruct->NVIC_IRQChannel >> 0x05] =
      (uint32_t)0x01 << (NVIC_InitStruct->NVIC_IRQChannel & (uint8_t)0x1F);
  }
}

在这里插入图片描述
3、编写中断服务函数
在启动文件 startup_stm32f10x_hd.s 中我们预先为每个中断都写了一个中断服务函数,只是这些中断函数都是为空,为的只是初始化中断向量表。实际的中断服务函数都需要我们重新编写,为了方便管理我们把中断服务函数统一写在 stm32f10x_it.c 这个库文件中。
在这里插入图片描述

如果一切配置完毕,当中断来临时,CPU会根据相应的中断去中断向量表找到对应的中断函数的地址,然后调用执行中断服务函数。

二.EXTI—外部中断/事件控制器

这里的外部中断是指由外部条件触发例如按键触发(GPIO),对于互联型产品(F107),外部中断/事件控制器由20个产生事件/中断请求的边沿检测器组成,对于其它产品(我们这里是F103),则有19个能产生事件/中断请求的边沿检测器。每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求
在这里插入图片描述

1.外部中断/事件线路映像

而中断线每次只能连接到 1 个 IO 口上,这样就需要通过配置来决定对应的中断线配置到哪个 GPIO 上了。
在这里插入图片描述
EXTI16~19也作为EXTI外设的输入线
在这里插入图片描述
16个中断线的不是每个中断都有独立的中断服务函数,IO口外部中断在中断向量表中只分配了7个中断向量,也就是只能使用7个中断服务函数
在这里插入图片描述
从表中可以看出,外部中断线5~9分配一个中断向量,共用一个服务函数。
外部中断线10~15分配一个中断向量,共用一个中断服务函数。

对应的中断服务函数,直接去启动文件里面找以防写错。
在这里插入图片描述

2.EXTI功能框图

信号线上打一个斜杠并标注“20”字样,这个表示在控制器内部类似的信号线路有 20 个,这与 EXTI 总共有 20 个中断/事件线是吻合的。所以我们只要明白其中一个的原理,那其他 19 个线路原理也就知道了

在这里插入图片描述
通过在软件中断/事件寄存器写’1’,也可以通过软件产生中断/事件请求
在这里插入图片描述

脉冲发生器:
在这里插入图片描述
输入一个有效信号 1 时就会产生一个脉冲,如果输入端是无效信号就不会输出脉冲。这个脉冲信号可以给其他外设电路使用,比如定时器 TIM、模拟数字转换器 ADC 等等,这样的脉冲信号一般用来触发 TIM 或者 ADC 开始转换

中断与事件的区别:

  • 中断:需要CPU参与,需要调用软件的中断服务函数才能完成中断后产生的结果
  • 事件:靠脉冲发生器产生一个脉冲,进而由硬件自动完成这个事件产生的结果,当然相应的联动部件需要先设置好,触发TIM计时,AD转换等,事件不要软件的参与,降低了CPU的负荷,而且硬件速度快于软件速度

详情推荐一篇文章:《中断与事件的区别》

接下来逐一介绍用到的寄存器,进一步理解框图原理:

  • 外部中断配置寄存器

在这里插入图片描述

  • 上升&下降沿触发选择寄存器

在同一中断线上,可以同时设置上升沿和下降沿触发。即任一边沿都可触发中断
在这里插入图片描述
在这里插入图片描述

  • 软件中断事件寄存器(EXTI_SWIER)

在这里插入图片描述

  • 挂起寄存器

在这里插入图片描述
中断或事件屏蔽寄存器
在这里插入图片描述

3.选择中断线与EXTI 初始化结构体详解

  • 选择中断线
    在配置中断线时一定要先使能AFIO外设的时钟,因为配置中断线是用到ADIO外设的寄存器,我们知道配置寄存器必须要有时钟

使能时钟
在这里插入图片描述
配置中断线
在这里插入图片描述

在这里插入图片描述

  • 配置EXTI初始化结构体
    在这里插入图片描述

1) EXTI_Line:EXTI 中断/事件线选择,可选 EXTI0 至 EXTI19。

2) EXTI_Mode:EXTI 模式选择,可选为产生中断(EXTI_Mode_Interrupt)或者产生事件(EXTI_Mode_Event)。

3) EXTI_Trigger:EXTI 边沿触发事件可选上升沿触发(EXTI_Trigger_Rising)、下降沿触发 ( EXTI_Trigger_Falling) 或者上升沿和下降沿都触发( EXTI_Trigger_Rising_Falling)。

4) EXTI_LineCmd:控制是否使能 EXTI 线,可选使能 EXTI 线(ENABLE)或禁用(DISABLE)。

最后调用EXTI_Init函数,将结构体配置好的参数写入对应的寄存器,这个比较简单我就不讲了,这里提一下这个结构体中断/事件线的选择并不是配置中断线/事件线,配置线的函数上面已经提及,这里选择线是为了知道中断线在寄存器哪个位置(019)对应中断线(EXTI019),好配置相应的寄存器。

三.外部中断控制实验

实验目的:利用按键产生一个下降沿,让系统产生一个中断,执行中断服务函数,函数将GPIO电平翻转使得灯,按一下亮按一下灭。

实验原理

在这里插入图片描述
EXTI程序框图分析:
在这里插入图片描述

编程要点

1) 初始化用来产生中断的 GPIO;
2) 初始化 EXTI;
3) 配置 NVIC;
4) 编写中断服务函数;

直接上代码咯:

exti.c

#include "exti.h"

static void NVIC_EXTI_Config(void)
{
	NVIC_InitTypeDef  NVIC_InitStruct;
	//中断优先级分组这里是组1
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
	//选择中断源
	NVIC_InitStruct.NVIC_IRQChannel= EXTI15_10_IRQn;
	//设置抢占式优先级
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =1;
	//设置响应式优先级
	NVIC_InitStruct.NVIC_IRQChannelSubPriority =1;
	//使能中断源
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	//调用NVIC初始化函数
	NVIC_Init(&NVIC_InitStruct);
}


void EXTI_Key_Config(void)
{		

	 GPIO_InitTypeDef  GPIO_InitStruct;
	 EXTI_InitTypeDef  EXTI_InitStruct;
  
	 RCC_APB2PeriphClockCmd(EXTI_Key1_GPIO_CLK,ENABLE);
	 //上拉输入
	 GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;
	 GPIO_InitStruct.GPIO_Pin= EXTI_Key1_GPIO_PIN ;
   GPIO_Init(EXTI_Key1_GPIO_POTR,&GPIO_InitStruct);
	 //配置NVIC中断
	 NVIC_EXTI_Config();
   //一定要使能外设AFIO外设的时钟
	 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	 //选择信号源
	 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource15);
	 //选择中断线
	 EXTI_InitStruct.EXTI_Line = EXTI_Line15;
	 //选择模式这里选择中断模式
	 EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
	 //下降沿模式
	 EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
	 //使能中断
	 EXTI_InitStruct.EXTI_LineCmd = ENABLE;
	 EXTI_Init(&EXTI_InitStruct);
	
}

exti.h

#ifndef __EXTI_H
#define __EXTI_H


#include "stm32f10x.h"


#define   EXTI_Key1_GPIO_PIN         GPIO_Pin_15
#define   EXTI_Key1_GPIO_POTR        GPIOA
#define   EXTI_Key1_GPIO_CLK         RCC_APB2Periph_GPIOA

void EXTI_Key_Config(void);
#endif /*__EXTI_H */


main.c

#include "stm32f10x.h"
#include "led.h"
#include "exti.h"

#define SOFT_DELAY Delay(0x0FFFFF);

void Delay(__IO u32 nCount); 


int main(void)
{	
	/* LED 端口初始化 */
	LED_GPIO_Config();	 
  EXTI_Key_Config();
	LED_G(OFF);
	LED_R(OFF);

	while(1)
	{
	}
}

void Delay(__IO uint32_t nCount)	 //简单的延时函数
{
	for(; nCount != 0; nCount--);
}

中断服务函数

void EXTI15_10_IRQHandler(void)
{
	//防抖
	  Delay(0x0FFFF);
	//判断按键是否按下
	if( GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)== 0)
	{
	
    if(  EXTI_GetITStatus(EXTI_Line15) != RESET )
	  {
			//电平翻转
		  GPIOA->ODR ^= GPIO_Pin_8;
		}
  	//清除中断挂起位
	 EXTI_ClearITPendingBit(EXTI_Line15);
	}

}

注意:执行完函数时一定要清除中断挂起为,不然系统会一直进入中断函数

在这里插入图片描述

实验效果

请添加图片描述

四.总结

由于本文涉及的知识点太多,参考了很多资料然后结合自己的理解写出这篇文章快接近万字了,到这终于写完啦,如果有错误还请指正,如果涉及到初始化函数那些寄存器具体怎么运算写入的不懂的可以评论区问我,建议虽然是库函数数编程,但尽量去看看函数是如何将参数写入寄存器的以及各个寄存器的作用,这样可以极大加深我们对原理的理解。觉得文章对你有所帮助就快快点赞收藏叭!!!

结束语:
最近发现一款刷题神器,如果大家想提升编程水平,玩转C语言指针,还有常见的数据结构(最重要的是链表和队列)后面嵌入式学习操作系统的时如freerots、RT-Thread等操作系统,链表与队列知识大量使用。
大家可以点击下面连接进入牛客网刷题

点击跳转进入网站(C语言方向)
点击跳转进入网站(数据结构算法方向)
在这里插入图片描述