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); //記得最后清除中斷標志位,為下一次中斷做准備
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM