---
# System prepended metadata

title: STM的NVIC與GPIO外部中斷
tags: [STM32]

---

[TOC]





# STM中斷運作方式

## 中斷流程

1. 中斷事件的<font color="red">觸發</font>(接腳狀態變化、計時器溢出、數據接收等等)
2. 對應的中斷源暫存器的<font color="red">中斷Flag</font>會被立起，當這類Flag立起，表示這個中斷正在等待處理。以外部中斷來說，外部中斷控制器(EXTI)的Pending Rigister對應的bit立起，表示對應的外部中斷待處理。
3. NVIC控制器接收到立起的flag消息會再<font color="red">判斷NVIC內部暫存器是否有啟用這個中斷，以及中斷的優先級處理</font>
4. 確認要處理的中斷後，NVIC向CPU送出<font color="red">中斷請求(IRQ)</font>信號給處理器
5. NVIC查詢中斷向量表(interrupt vector table)讓處理器<font color="red">進入中斷服務例程(ISR)</font>
6. <font color="red">清除第2步的flag</font>

## 從程式看中斷
1. 上述流程第1~3部分是電路上的動作。
2. 從專案資料夾的startup_stm32f407xx.s檔來看，內部有對應的中斷名稱與中斷處理函式(IRQ_Handler)，這個檔案可視為中斷向量表，會燒錄進記憶體
3. 假設今天知道發生的中斷源是EXTI Line0，從向量表可知對應的處理函式是 EXTI0_IRQHandler，在stm32f4xx_it.c可以找到函式定義。函式內有一個HAL_GPIO_EXTI_IRQHandler，在stm32f4xx_hal_gpio.c可以找到函式定義。
    ``` c++=
    @ startup_stm32f407xx.s
    ...
    ...
    .word     FLASH_IRQHandler                  /* FLASH                        */                                          
  	.word     RCC_IRQHandler                    /* RCC                          */
  	.word     EXTI0_IRQHandler                  /* EXTI Line0                   */
  	.word     EXTI1_IRQHandler                  /* EXTI Line1                   */
  	.word     EXTI2_IRQHandler                  /* EXTI Line2                   */
    ...
    ...
    ```
    
    ``` c++=
    @ stm32f4xx_it.c
    
    void EXTI0_IRQHandler(void)
	{
	  /* USER CODE BEGIN EXTI0_IRQn 0 */
	
	  /* USER CODE END EXTI0_IRQn 0 */
	  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
	  /* USER CODE BEGIN EXTI0_IRQn 1 */
	
	  /* USER CODE END EXTI0_IRQn 1 */
	}
    ```
    
    ``` c++=
    @ stm32f4xx_hal_gpio.c
    
    void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
	{
	  /* EXTI line interrupt detected */
      
      // 判斷中斷旗標是否立起
	  if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
	  {
        // 清除中斷旗標
	    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
        
        // 回調函式
	    HAL_GPIO_EXTI_Callback(GPIO_Pin);
	  }
	}
    ```
4. 本篇例子中把在ISR想做的事情放在 HAL_GPIO_EXTI_Callback。注意這時候已經清除中斷旗標

# NVIC設定
- 上面已經說明NVIC在一般中斷流程中的角色，具體NVIC內部的暫存器結構及其他功能可參閱野火-HAL開發指南，或看看stm32f4xx_hal_cortex.c或core_cm4.h內的程式說明
- 針對CubeMX設定來看
    ![image](https://hackmd.io/_uploads/S1F-ayL2R.png)
    1. 只要在其他功能設定有選擇要產生中斷，Interrupt Table就會顯示對應中斷名稱
    2. <font color="red">要使用中斷就得勾選Enable</font>，因為在中斷流程中會藉由確認這個Enable決定可不可以觸發中斷
    3. 左上角的Priority Group和右邊兩欄決定各中斷的[優先級](https://blog.csdn.net/qq_53918631/article/details/125245342)。Preemption和Sub Priority總共佔4bit，Priority Group可決定這4個bit的分配。
    4. 優先級先看Preemption，再看Sub，兩個都一樣就看rm0090的 Table 62.。


# 外部中斷(EXTI)
- 野火-HAL實戰開發 第17章
- [STM EXTI](https://blog.csdn.net/iamyangbei/article/details/128158326)
- rm0090 12.2節

## 簡介
- 23種 interrupt/event request，野火 : *产生中断线路目的是把输入信号输入到NVIC，进一步会运行中断服务函数，实现功能，这样是软件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用，并且是电路级别的信号*
传输，属于硬件级的
- 以GPIO外部中斷來說，多個Port的同個Pin是共用同一條EXTI Line的。所以不可能有PA0和PD0都做為外部中斷的情況，CubeMX也不讓選
    ![image](https://hackmd.io/_uploads/Hk3DTxUnR.png)
    ![image](https://hackmd.io/_uploads/S18h6lUnR.png)
- 不同暫存器的功能與介紹可參閱rm0090，以中斷旗標來說，控制所有EXTI Line的中斷旗標都在EXTI_PR暫存器

## 程式使用

- 以GPIO中斷為例，根據對應的腳位，Callback只需添加如下範例
    ``` c++=
	void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
	{
	  if (GPIO_Pin == GPIO_PIN_X) 
	    // 中斷處理
	  else
	  {
	    __NOP();
	  }
	}
	```
- 其他外部中斷種類可至HAL庫相關的.c檔尋找


# 外部中斷實例 - 按鈕觸發外部中斷控制LED
- 板上的藍色按鈕已經接到PA0腳位，我們用它實現外部中斷
- 在UM1472手冊中有按鈕的電路圖
    ![image](https://hackmd.io/_uploads/SkIRvHH2C.png =60%x)



## CubeMX配置
- 一開始一樣初始化
- PA0腳位做設置
 ![image](https://hackmd.io/_uploads/B1ocrHS3C.png)
- NVIC 中斷開啟
    ![image](https://hackmd.io/_uploads/BkmW8LS2R.png)
- 同樣設置板上四顆LED對應的腳位為輸出

## 程式編寫1
- 在 USER CODE BEGIN 4區域加入以下程式

	``` c++=
	void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
	{
	  if (GPIO_Pin == GPIO_PIN_0) // or if (GPIO_Pin == **User Label Pin Name**)
	    // 翻轉LED4腳位的輸出
	  else
	  {
	    __NOP();
	  }
	}
	```
- 在 主迴圈while(1)中加入程式如
	``` c++=
	/* Infinite loop */
	/* USER CODE BEGIN WHILE */
	while (1){
	    /* USER CODE END WHILE */
	
	
	    /* USER CODE BEGIN 3 */
	    HAL_GPIO_WritePin(GPIOD, LD3_Pin, GPIO_PIN_RESET);
	    HAL_Delay(3000);
	    HAL_GPIO_WritePin(GPIOD, LD3_Pin, GPIO_PIN_SET);
	    HAL_Delay(3000);
	}
	/* USER CODE END 3 */
	```
## 程式編寫2
- 修改程式，把HAL_GPIO_EXTI_Callback都註解掉
- 改在主迴圈內讀取PA0狀態並做判斷翻轉LED4
    ``` c++=
    while(1){
        if(PA0讀取到HIGH)
            LED亮;
        else
            LED暗;
        HAL_GPIO_WritePin(GPIOD, LD3_Pin, GPIO_PIN_RESET);
        HAL_Delay(3000);
        HAL_GPIO_WritePin(GPIOD, LD3_Pin, GPIO_PIN_SET);
        HAL_Delay(3000);
    }
    ```
## 小結
- 程式1的結果讓按鈕偵測可以隨時進行，反觀程式2要等到延遲結束才能進行按鈕的判讀
- 在控制或其他應用上有實時需求或不容許太多程式阻塞(blocking)的話，試著用中斷處理你的程式
- 注意中斷執行的程式應該輕薄短小，避免過多的運算。任何可能造成阻塞的程式都很不建議放在中斷內，譬如<font color="red">while、HAL_Delay</font>等等

