G
N
I
D
A
O
L

STM32学习笔记(4)——NVIC中断优先级管理和外部中断EXTI

一、NVIC中断优先级管理

1. 中断简介

在Cortex-M3(CM3)内核中,每个中断的优先级都是用寄存器中的8位来设置的,这样就有2^8 =256级中断,意味着可以支持256个中断,这其中包含了16个内核中断和240个外部中断,并且具有256级的可编程中断设置。但许多芯片厂商并没有使用CM3内核的全部东西,而是只用了它的一部分,而多余的部分应该是设计者考虑到后续应用发展而冗余设计的。

实际情况中,芯片厂商根据自己生产的芯片做出了调整。比如ST(意法半导体)公司的STM32F1xx和F4xx系列只使用了这个(寄存器NVIC->IPR,如图所示)8位中的高四位[7:4],低四位取零,这样2^4=16,只能表示16级中断嵌套。

STM32有84个中断,包括16个内核中断和68个可屏蔽中断,具有16级可编程的中断优先级。我们使用的是STM32F103系列,只有60个可屏蔽中断,而在107系列有68个。

2. 中断向量表

中断向量表为每个外设作了硬件编号,可以把它理解为默认顺序。如果有两个外设工作顺序发生冲突(一般在NVIC设置好后就很少发生这种情况)时,就按照这个表来分执行先后。

STM32的中断向量表如下(可对照STM32中文参考手册9.1.2节中断和异常向量中的表):

其中灰色的部分(图片未显示)为异常向量,其余白色部分为中断向量。

在头文件stm32f10x.h的163行开始中定义了各中断向量的顺序编号,现摘录该定义(IRQ = Interrupt Request):

/******  Cortex-M3 Processor Exceptions Numbers 内核处理器异常编号(用户一般不使用,说白就是不用管) ***************************************************/
  NonMaskableInt_IRQn         = -14,    /*!< 2 Non Maskable Interrupt                             */
 ······

/******  STM32 specific Interrupt Numbers STM32特定中断号 *********************************************************/
  WWDG_IRQn                   = 0,      /*!< Window WatchDog Interrupt                            */
  PVD_IRQn                    = 1,      /*!< PVD through EXTI Line detection Interrupt            */
  TAMPER_IRQn                 = 2,      /*!< Tamper Interrupt                                     */
  ······
  
//省略号表示下面还有很多很多,而且使用的是条件编译,因为不同型号对应不同中断号。

在头文件core_cm3.h中可以看到配置与中断相关的寄存器。实际上ST芯片用不到这么大的寄存器,因此我们在网上借鉴了一段代码,反映了ST芯片真实使用到的寄存器大小。其余未使用到的空间均为保留。

/*
cortex-m3内核分组方式(8组)结构体表达方式:
*/
typedef struct
{
  __IO uint32_t ISER[8];             //中断使能设置寄存器,作用:用来使能中断
  //32位寄存器,每个位控制一个中断的使能。STM32F10x只有60个可屏蔽中断,所以只使用了其中的ISER[0]和ISER[1]。
  //ISER[0]的bit0~bit31分别对应中断0~31。ISER[1]的bit0~27对应中断32~59;
          /*!< 偏移量: 0x000  Interrupt Set Enable Register           */
       uint32_t RESERVED0[24];      //这些保留的不用看,也不要使用                             
  __IO uint32_t ICER[8];              //中断清除使能寄存器,作用:用来失能中断
  //32位寄存器,每个位控制一个中断的失能。STM32F10x只有60个可屏蔽中断,所以只使用了其中的ICER[0]和ICER[1]。
  //ICER[0]的bit0~bit31分别对应中断0~31。ICER[1]的bit0~27对应中断32~59,下面都差不多
        /*!<偏移量: 0x080  Interrupt Clear Enable Register        */
       uint32_t RSERVED1[24];                                    
  __IO uint32_t ISPR[8];              //中断挂起设置寄存器,作用:用来挂起中断,就是我之前关掉这个中断,但现在我想打开它了!
        /*!< 偏移量: 0x100  Interrupt Set Pending Register          */
       uint32_t RESERVED2[24];                                   
  __IO uint32_t ICPR[8];              //中断清除挂起寄存器,作用:用来解挂中断,说白了就是我不想用这个中断,暂时关掉!
        /*!<偏移量: 0x180  Interrupt Clear Pending Register        */
       uint32_t RESERVED3[24];                                   
  __IO uint32_t IABR[8];               //中断激活状态位寄存器,作用:只读,通过它可以知道当前在执行的中断是哪一个
  //问题:既然只读的话为何不声明 __I ?没搞懂
       /*!< 偏移量: 0x200  Interrupt Active bit Register           */
       uint32_t RESERVED4[56];                                   
  __IO uint8_t  IP[240];               //中断优先级寄存器 
  //240个8位寄存器,每个中断使用一个寄存器来确定优先级。STM32F10x系列一共60个可屏蔽中断,使用IP[59]~IP[0]。
  //之前已经讲过,每个IP寄存器的高4位用来设置抢占和响应优先级(根据分组),低4位没有用到。
  /*!< 偏移量: 0x300  Interrupt Priority Register (8Bit wide) */
       uint32_t RESERVED5[644];        //软件触发方式寄存器                          
  __O  uint32_t STIR;                         /*!< 偏移量: 0xE00  Software Trigger Interrupt Register     */
}  NVIC_Type;  


/*
实际STM32分组(5组)方式结构体表达方式
*/
typedef struct
{
  vu32 ISER[2]; //v表示volatile,这个关键字很重要,以后有时间去研究研究
  u32  RESERVED0[30];
  vu32 ICER[2];
  u32  RSERVED1[30];
  vu32 ISPR[2];
  u32  RESERVED2[30];
  vu32 ICPR[2];
  u32  RESERVED3[30];
  vu32 IABR[2];
  u32  RESERVED4[62];
  vu32 IPR[15]; //Interrupt Priority Registers,这里可以与上面的IP对应
  //大家可以算算,每个寄存器仅占用4位,15*4=60是不是正好。
} NVIC_TypeDef;

这么多的中断,该怎样管理呢?NVIC这时要出场了。

3. 嵌套向量中断控制器(NVIC)

NVIC的全称是Nested vectoredinterrupt controller,即嵌套向量中断控制器。

需要注意一点:NVIC是Cortex-M3核心的一部分,因此就不要在STM32中文参考手册里面找了(STM32:关我鸟事?),应查阅ARM的Cortex-M3技术参考手册。

NVIC一个很重要的概念是优先级分组。和51单片机不同,NVIC将优先级分为两个:抢占优先级(PreemptionPriority)和响应优先级(SubPriority)。从英文就知道了,抢占优先级比响应优先级要高。同一个优先级上,数字越小,优先级越高。工作原理如下:

【情况一】外设B工作时遇到中断请求,外设A需要工作,因为A的抢占优先级高,这时外设B会立刻停止,外设A抢占B开始工作。如果遇到多个中断请求,还会进入中断嵌套,形成“套娃”。所以,抢占优先级是可以嵌套的

如果是A和B同时到来,因为A的抢占优先级高,会先执行A,后执行B。

外设 抢占优先级 响应优先级 中断向量号
A 1 2 3
B 2 1 4

【情况二】外设B工作时遇到中断请求,外设A需要工作,因为两者抢占优先级相同,此时只能实行先到先得,B弄完后A再来了。所以,响应优先级是不能嵌套的

如果是A和B同时到来,因为两者抢占优先级相同,此时继续比较响应优先级,B的响应优先级高,会先执行B,后执行A。

外设 抢占优先级 响应优先级 中断向量号
A 1 2 3
B 1 1 4

【情况三】外设B工作时遇到中断请求,外设A需要工作,因为两者抢占优先级和响应优先级相同,所以实行的是先到先得的办法,谁先执行就让谁了。

如果是A和B同时到来,因为A的中断向量号小,会先执行A,后执行B。

外设 抢占优先级 响应优先级 中断向量号
A 1 1 3
B 1 1 4

4. NVIC的定义以及库函数

在STM32中,优先级编号不是你想多少就多少的,是有规定的。首先,NVIC对STM32中断进行分组,一共5个组,组0~4。同时,对每个中断设置一个抢占优先级和一个响应优先级值。分组配置是在寄存器SCB->AIRCR(可在头文件core_cm3.h找到)中配置的。

如下表:

对应:

  • 第0组:所有4位(仅能用0-15设置优先级别,下同)用于指定响应优先级
  • 第1组:最高1位(0-1)用于指定抢占式优先级,最低3位(0-7)用于指定响应优先级
  • 第2组:最高2位(0-3)用于指定抢占式优先级,最低2位(0-3)用于指定响应优先级
  • 第3组:最高3位(0-7)用于指定抢占式优先级,最低1位(0-1)用于指定响应优先级
  • 第4组:所有4位(0-15)用于指定抢占式优先级

NVIC的定义(位于头文件misc.h,80行还有一个和上面一样的表格)如下:

typedef struct
{
  uint8_t NVIC_IRQChannel;      //指定是哪个外设需要中断,各外设向量号在stm32f10x.h可以看到

  uint8_t NVIC_IRQChannelPreemptionPriority;   //设置抢占优先级编号(至于最大可以多少参照上表)

  uint8_t NVIC_IRQChannelSubPriority;      //设置响应优先级编号(至于最大可以多少参照上表)

  FunctionalState NVIC_IRQChannelCmd;       //使能中断通道
} NVIC_InitTypeDef;

NVIC的库函数定义(位于头文件misc.h)如下:

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);

//与挂起和解挂有关的函数:
static __INLINE void NVIC_SetPendingIRQ(IRQn_Type IRQn);
static __INLINE uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn);
static __INLINE void NVIC_ClearPendingIRQ(IRQn_Type IRQn);

//与中断标志激活位有关的函数(作用就是看这个中断有没挂起,别想多了):
static __INLINE uint32_t NVIC_GetActive(IRQn_Type IRQn);

使用NVIC的流程如下:

NVIC_InitTypeDef NVIC_InitStructure;
// 1.选择优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
// 2.选择需要产生中断的外设
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
// 3.设置抢占优先级级别
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
// 4.设置响应优先级级别
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
// 5.使能中断
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
// 6.初始化中断
NVIC_Init(&NVIC_InitStructure);

二、外部中断EXTI

EXTI(External interrupt / event controller)又叫外部中断/事件控制器,EXTI是ST公司在其STM32产品上扩展的外中断控制。它负责管理映射到GPIO引脚上的外中断和片内几个集成外设的中断(PVD,RTC闹钟,USB唤醒,以太网),以及软件中断。其输出最终被映射到NVIC的相应通道。因此,配置EXTI中断的过程必然包含对NVIC的配置。(不严谨理解:NVIC包含EXTI)

在头文件stm32f10x.h中,我们用的是STM32F10X_HD型,所以有关EXTI的中断向量号分别如下(尤其需要注意最后两个):

  EXTI0_IRQn                  = 6,      /*!< EXTI Line0 Interrupt                                 */
  EXTI1_IRQn                  = 7,      /*!< EXTI Line1 Interrupt                                 */
  EXTI2_IRQn                  = 8,      /*!< EXTI Line2 Interrupt                                 */
  EXTI3_IRQn                  = 9,      /*!< EXTI Line3 Interrupt                                 */
  EXTI4_IRQn                  = 10, 
  EXTI9_5_IRQn                = 23,     /*!< External Line[9:5] Interrupts                        */
  EXTI15_10_IRQn              = 40,     /*!< External Line[15:10] Interrupts                      */

EXTI由19个(互联型为20个)产生事件/中断请求的边沿检测器组成,每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。

1. EXTI功能框图

(该图来自火哥PPT)

对照图中编号顺序来讲讲各个元素(以下寄存器配置对照STM32中文参考手册9.3EXTI寄存器描述):

(0)信号线

图中箭头为信号线,可以看到箭头所指方向为信号传输方向,双箭头表示信号可双向传导。上面的“/20”表示在控制器内部类似的信号线路有20个,这里是省略了其余19个信号线(这里显示的是互联型)。这里详细说一下哪些可以作为输入:每个IO都可以作为外部中断输入,中断控制器支持19个外部中断/事件请求。具体如下:

  • 线(EXTI_Linex)0-15:对应外部IO口的输入中断(PX0、PX1、···、PX15,X = A、B、C、D、E、F、G、H、I)
  • 线16:连接到PVD输出
  • 线17:连接到RTC闹钟事件
  • 线18:连接到USB唤醒事件
  • 线19(只适用于互联型):连接到以太网唤醒事件

下图显示的是在AFIO_EXTICR寄存器的EXTIx位(AFIO是复用GPIO,以后会讲到):

(1)输入线

EXTI有19个中断/事件输入线,中断输入可以来自GPIO,也可以来自外设。

(2)边沿检测电路

可以通过寄存器设置电路是上升沿触发(EXTI_RTSR)或下降沿触发(EXTI_FTSR)或两者皆触发。若检测到有效信号则输出高电平(1),否则低电平(0)。

(3)或门

输入分别是来自边沿检测电路和软件中断寄存器(EXTI_SWIER),只要有一个输入为高电平,输出即为高电平。

(4)与门

输入分别来自中断屏蔽寄存器(EXTI_IMR)和请求挂起寄存器(EXTI_PR),需要两个输入为高电平,输出才为高电平。

(5)输出至NVIC

将EXTI_PR寄存器内容输出到NVIC内,从而实现系统中断事件控制。以上均为NVIC路线。

(6)与门

输入分别来自事件屏蔽寄存器(EXTI_EMR)和或门输出,需要两个输入为高电平,输出才为高电平。

(7)脉冲发生器

当输入端,即与门(6)的输出端,是一个高电平就会产生一个脉冲;如果输入端是低电平就不会输出脉冲。

(8)产生事件

脉冲发生器产生的脉冲信号,是事件线路最终的产物,这个脉冲信号可以给其他外设电路使用。

2. EXTI的定义以及库函数

在头文件stm32f10x_exti.h中定义了结构体EXTI:

typedef struct
{
  uint32_t EXTI_Line;   
  // EXTI中断/事件线选择,参数为EXTI_Linex 
  EXTIMode_TypeDef EXTI_Mode;       
  // EXTI模式选择:中断(EXTI_Mode_Interrupt)触发或者事件(EXTI_Mode_Event)触发    
  EXTITrigger_TypeDef EXTI_Trigger;
  // EXTI边沿触发类型选择:上升沿触发(EXTI_Trigger_Rising)、下降沿触发(EXTI_Trigger_Falling) 
  //或者上升沿和下降沿都触发(EXTI_Trigger_Rising_Falling)
  FunctionalState EXTI_LineCmd;     
  // 使能EXTI
}EXTI_InitTypeDef;

同时,也定义了库函数(参考原子PPT):

①void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
//设置IO口与中断线的映射关系

   exp:  GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);

②void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
//初始化中断线:触发方式等

③ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
//判断中断线中断状态,是否发生

④void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
//清除中断线上的中断标志位

在设置好EXTI后,还要指定中断后程序要干什么,这就需要用户自己定义中断服务函数,但是函数名不能乱起,在startup_stm32f10x_hd.s文件中已经写好了中断向量表的顺序(从62行__Vectors开始),这些就是函数名。至于怎么写,待会举个例子就好了。

使用EXTI的流程较繁琐,如下:

// 0.先配置想要产生中断的GPIO,用GPIO配置的方法配置
EXTI_InitTypeDef EXTI_InitStructure;
// 1.开启(使能)GPIO复用时钟(关键一步)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 2.选择IO口与中断线的映射关系
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3);
// 3.EXTI中断/事件线选择	
EXTI_InitStructure.EXTI_Line = EXTI_Line3;
// 4.EXTI边沿触发类型选择
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
// 5.EXTI模式选择
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
// 6.使能EXTI
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
// 7.初始化EXTI
EXTI_Init(&EXTI_InitStructure);
// 8.编写中断服务函数,中断后你想干嘛都写在这里面
EXTIx_IRQHandler(){···}
// 9.别忘了在中断服务函数里面清除中断标志位
EXTI_ClearITPendingBit();

三、一个简单的例程

功能:按键KEY1按下,实现一次LED0反转。初始状态时LED0灭。用NVIC和EXTI中断实现。

部分程序(省略led部分)如下:

// main.c
#include  "stm32f10x.h"
#include  "led.h"
#include  "delay.h"
#include  "exti_config.h"

int main(void)
{ 
	LED_Init();
	delay_init();
	EXTI_Key_Config();
	EXTI_NVIC_Config();
	
	while(1){}
		
}


//exti_config.h
#ifndef __EXTI_CONFIG_H
#define __EXTI_CONFIG_H

#include "stm32f10x.h"
#include "led.h"
#include "delay.h"

#define KEY1 GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_3)

void EXTI_Key_Config(void);
void EXTI_NVIC_Config(void);

#endif /* __EXTI_CONFIG_H */


//exti_config.c
#include "exti_config.h"

void EXTI_NVIC_Config(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); // NVIC组别为0
	
	NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //响应优先级为1
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

// KEY1: PE3
void EXTI_Key_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	EXTI_InitTypeDef EXTI_InitStructure;
	
	//初始化GPIOE.3 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOE, &GPIO_InitStructure);
	
	//为GPIOE.3配置EXTI
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3);
	
	EXTI_InitStructure.EXTI_Line = EXTI_Line3;
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_InitStructure);
}

//中断服务函数
void EXTI3_IRQHandler(void)
{
	delay_ms(10); //按键延时判断
	if(KEY1 == 0)
	{
		LED0 = !LED0; 
	}
	EXTI_ClearITPendingBit(EXTI_Line3); //记得最后清除中断标志位,为下一次中断做准备
}
posted @ 2021-05-07 13:51  漫舞八月(Mount256)  阅读(3294)  评论(0编辑  收藏  举报