目錄
前言
如何使用STM32F1系列的標準庫完成外部中斷的抽象
初始化我們的GPIO為輸入的一個模式
初識GPIO復用,開啟GPIO的復用功能時鐘
GPIO_EXTILineConfig和EXTI_Init配置外部中斷參數
插入一個小知識——如何正確的配置結構體?
初始化中斷:NVIC注冊
處理中斷回調函數
本篇的實驗代碼
前言
在之前,我們就詳細的討論過了最簡單的GPIO設備的驅動和使用,但是如你所見,這些調用都是同步,即我們程序主動的去設置和讀取GPIO的狀態。這很正常——我們只有在想要主動出擊的時候,比如說點亮LED電平,查詢按鈕的狀態的時候,我們主動的申請和讀取結果。這是很正常的事情。
但是仍然有一些場景,我們實際上并不是這樣的,甚至完全與此相反。舉個例子,一些事情我們更希望是外部來告知我們發生了。舉個例子,一個經典的場景就是——我們希望在按鍵嗯下的時候,我們來做一部分事情,比如說后面我們切換LCD界面啊等等的事情。按照我們之前的方法,那就是:
while(1)
{if(isKeyPressed()){do_things();}else{do_other_things();}...
}
這個的問題看起來不大,但是實際上你仔細思考一下,問題很大。在我們上一個小節中,我們做一個改動,您可以嘗試一下:
while(1)
{if(isKeyPressed()){reverse_gpio_status(&led0);}else{system_clock_delay_ms(1000); // delay for 1 seconds}...
}
你會發現一個致命的問題,整個系統罷工了,為什么,我們仔細看看這個代碼——我們在一瞬間檢查完Key沒有按下之后,整個系統直接掛起來1秒鐘,如果我們之后寫更加龐大的項目的時候,問題還能更加尖銳(比如說使用ESP8266等待網絡請求,那可就是一個等待就是好幾秒),這下你的客戶如果發了瘋的按按鈕就會發現根本不起作用的時候,你的好日子也就到頭了。那咋辦?
仔細想下,我們的邏輯是不是有點問題——分明是我們想要“按下按鈕的時候,我們才會翻轉LED的電平。”,整個程序的邏輯是——詢問一下按鈕按下了嘛?有則處理按鈕按下的邏輯,沒有處理其他程序。這不對的。
問題就在于,我們需要把邏輯顛倒過來!一個事件發生了,我們需要如何處理這個事情。而不是反復的詢問這個事情有沒有發生。因此,我們就需要一個機制,這個機制,就是外部中斷,外部中斷按照一種通知的方式,通知我們的單片機一個抽象的事件發生了。
一個光傳感器察覺到了一個光亮度的變化的時候,光傳感器發送了一個電平脈沖。與其一直輪循電平的變化,不如直接讓我們的GPIO處于一個中斷的前端,讓接受到電平變化的引腳申請我們的單片機觸發一個中斷,極其短暫的打斷我們的程序執行,跳轉道一個新的處理流處理我們的電平變化的事件。比如說,光暗了,我們需要開燈,這個時候,我們處理光傳感器發過來的“光暗下去了”的信號,處理的程序就是打開一個LED燈。你看,簡單吧!
因此,按照這種方式,我們就可以擺脫我們的輪循式的詢問,轉向語義更好的“中斷事件通知了”。
關于中斷的本質和STM32的中斷,我們放到旋轉編碼器和光傳感器的之后作為一個硬核原理篇章,仔細的介紹其根本原理,這里,我們仍然只是保留到會用即可的水平。
下面,我們就來聊一聊,標準庫是如何使用外部中斷來完成我們的事情的。
如何使用STM32F1系列的標準庫完成外部中斷的抽象
標準庫已經給我們提供好了腳手架了,只需要我們按照步驟進行配置即可。簡單的說,就是做這些事情
-
初始化我們的GPIO為輸入的一個模式
-
初始化GPIO外部中斷的資源寄存器
-
使能我們的外部中斷
這需要我們一步步來看。
初始化我們的GPIO為輸入的一個模式
為什么是輸入模式呢?你想想,我們接受外部的中斷是不是需要從外界獲取信息?是的,這就是我們的GPIO中斷模式本質上需要以輸入模式進行配置的原因,如果你還看筆者的HAL庫教程,你就會知道,HAL庫直接封裝好了體系,將輸入模式中的一部分特化出來了外部中斷,合并了我們標準庫的配置,標準庫比較原始,因此我們需要做的事情就是將這些步驟正確的組合起來。
關于輸入模式的配置,這里我們不再重復談論了,實在乏味。我們馬上進入下一個階段,配置外部中斷的GPIO引腳資源。
初識GPIO復用,開啟GPIO的復用功能時鐘
關于復用的底層原理,一并合并到我們的GPIO中斷中討論,這里,你只需要知道的是:GPIO現在具備了片上外設和外部設備的溝通能力,在這里,就是我們的中斷控制器現在可以監控我們的外部設備的電平變化了,我們這一步就是做這個事情。
開啟GPIO的AF時鐘
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
開啟這個時鐘,我們的片上外設復用就變得可用,下一步,就是提交我們要監察的外部中斷是如何的。
GPIO_EXTILineConfig和EXTI_Init配置外部中斷參數
使用的函數是GPIO_EXTILineConfig來注冊EXTI中斷控制子系統,連接GPIO和外部中斷線,進而再EXTI_Init函數來初始化。
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
EXTI_InitTypeDef里,提供的就是我們寫的參數了
typedef struct
{uint32_t EXTI_Line; ? ? ? ? ? ? ? /*!< Specifies the EXTI lines to be enabled or disabled.This parameter can be any combination of @ref EXTI_Lines */EXTIMode_TypeDef EXTI_Mode; ? ? ? /*!< Specifies the mode for the EXTI lines.This parameter can be a value of @ref EXTIMode_TypeDef */
?// 筆者注釋:之前的版本中這里的注釋類型寫錯了EXTITrigger_TypeDef EXTI_Trigger; /*!< Specifies the trigger signal active edge for the EXTI lines.This parameter can be a value of @ref EXTITrigger_TypeDef */
?FunctionalState EXTI_LineCmd; ? ? /*!< Specifies the new state of the selected EXTI lines.This parameter can be set either to ENABLE or DISABLE */
}EXTI_InitTypeDef;
插入一個小知識——如何正確的配置結構體?
我發現,大部分嵌入式的人,不太會查看手冊/查看注釋來進行學習。你看,我們的標準庫實際上已經把事情說的很明白了,我們這里實際上需要填寫的參數
第一個蠶食,人家的說法是任意組合的EXTI_Line,我們一看,嗯很,沒有具體說明,一個最快速的辦法就是看注釋,EXTI_LINE的一個組合,這也就說明了,直接全局搜索一個EXTI_LINE,這里的EXTI_Mode對應的就是對應的GPIO_Pin所在的EXTI線,舉個例子,GPIO_Pin_0對應的就是EXTI_Line0,GPIO_Pin_1對應的就是EXTI_Line1,類推!
#define EXTI_Line0 ? ? ? ((uint32_t)0x00001) /*!< External interrupt line 0 */
#define EXTI_Line1 ? ? ? ((uint32_t)0x00002) /*!< External interrupt line 1 */
#define EXTI_Line2 ? ? ? ((uint32_t)0x00004) /*!< External interrupt line 2 */
#define EXTI_Line3 ? ? ? ((uint32_t)0x00008) /*!< External interrupt line 3 */
#define EXTI_Line4 ? ? ? ((uint32_t)0x00010) /*!< External interrupt line 4 */
#define EXTI_Line5 ? ? ? ((uint32_t)0x00020) /*!< External interrupt line 5 */
#define EXTI_Line6 ? ? ? ((uint32_t)0x00040) /*!< External interrupt line 6 */
#define EXTI_Line7 ? ? ? ((uint32_t)0x00080) /*!< External interrupt line 7 */
#define EXTI_Line8 ? ? ? ((uint32_t)0x00100) /*!< External interrupt line 8 */
#define EXTI_Line9 ? ? ? ((uint32_t)0x00200) /*!< External interrupt line 9 */
#define EXTI_Line10 ? ? ((uint32_t)0x00400) /*!< External interrupt line 10 */
#define EXTI_Line11 ? ? ((uint32_t)0x00800) /*!< External interrupt line 11 */
#define EXTI_Line12 ? ? ((uint32_t)0x01000) /*!< External interrupt line 12 */
#define EXTI_Line13 ? ? ((uint32_t)0x02000) /*!< External interrupt line 13 */
#define EXTI_Line14 ? ? ((uint32_t)0x04000) /*!< External interrupt line 14 */
#define EXTI_Line15 ? ? ((uint32_t)0x08000) /*!< External interrupt line 15 */
EXTI_Mode是一個enum枚舉類型
typedef enum
{EXTI_Mode_Interrupt = 0x00,EXTI_Mode_Event = 0x04
}EXTIMode_TypeDef;
這里有兩個參數,我們理解和學習的是中斷,很顯然是EXTI_Mode_Interrupt,但是這里筆者還是要簡單的說明一下這兩個參數有什么區別。
特性 | EXTI_Mode_Interrupt (中斷模式) | EXTI_Mode_Event (事件模式) |
---|---|---|
是否觸發中斷 | ?? (調用我們處理的回調函數) | ? (僅硬件事件) |
是否喚醒 CPU | ?? | ? |
CPU 參與 | 需要 CPU 處理中斷 | 無需 CPU 干預 |
典型應用 | 緊急任務(如按鍵、故障檢測) | 硬件觸發(如 DMA、ADC) |
EXTI_Mode_Event不是我們這里可以管的,因此,等到我們談論到了DMA,ADC等概念的時候,我們會回來學習事件模式的。
EXTITrigger_TypeDef說明我們如何觸發這個中斷。
typedef enum
{EXTI_Trigger_Rising = 0x08,EXTI_Trigger_Falling = 0x0C, ?EXTI_Trigger_Rising_Falling = 0x10
}EXTITrigger_TypeDef;
嗯,上升?下降?上升下降?這些是什么東西啊?別急,我們來仔細思考一下。一個很自然的疑問,舉個例子:我們如何認為外部的GPIO中斷道來了呢?你可能會說這是我關心的嘛?你需要關心!,這需要你仔細閱讀器件的手冊,舉一個例子,筆者的光傳感器,查看手冊后,可以做出這樣的一個時序圖。
我們看到,這個器件的特性是閾值觸發類型的傳感器,也就是掃描過一次閾值,就會觸發一次信號。可以看到,我們觸發一次,我們的光傳感器就會快速向外發射一個低電平,因此,我們只需要檢測低電平,就能捕捉到我們的光強變化事件了!
仔細看看下面的這個圖,思考一下上面的三個枚舉,你認為,這三個枚舉,想要監控的是哪些電平變化呢?答案如下!
現在我們很清楚了,對于我們的器件,如果你的手冊上說:光強變化給一個下降觸發的時候,你就需要配置成Falling了。
剩下的Cmd實際上就是Enable,沒啥好說的。這就是使能的意思。
很好,我們申請GPIO中斷的第一個子步驟,是注冊GPIO的中斷Pin線,使用的函數是
/*** @brief Selects the GPIO pin used as EXTI Line.* @param GPIO_PortSource: selects the GPIO port to be used as source for EXTI lines.* ? This parameter can be GPIO_PortSourceGPIOx where x can be (A..G).* @param GPIO_PinSource: specifies the EXTI line to be configured.* ? This parameter can be GPIO_PinSourcex where x can be (0..15).* @retval None*/void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
比如說,我們想要給GPIOB注冊中斷,辦法就是提供:GPIO_PortSourceGPIOB和GPIO_PinSource0。這個時候,使用這個函數的后,我們的GPIO_Pin就能根我們的外部中斷線連接起來,下一步只需要把中斷使能起來,就可以使用外部中斷來監控事件了。
初始化中斷:NVIC注冊
NVIC是啥呢?NVIC的全稱是Nested vectored interrupt controller,即嵌套向量中斷控制器。關于通用的中斷,我們需要做的理解就是跟上面說的一致——是事件(中斷也可以來自內部,但對于最核心的CPU而言,你認為他們都是外部好像毛病也不大,總而言之,就是一些事件需要觸發我們的處理器暫停手下的活來處理通知的中斷事件,這里的嵌套你可以認為中斷是可以嵌套的)
如果感覺中斷不好理解,筆者在手搓操作系統的博客中有打過這樣的比喻:
現實生活中,我們可能往往會被任何啥東西打斷,比如說,此時此刻我正在寫一個闡述中斷的博客,我的一個好朋友突然給我發QQ,向我抱怨該死的爬蟲實在是太慢了,需要優化一下并發的結構。這個時候,我就跑去回應他,剛準備打幾個字,我的母上大人著急的找我問點事情,我又拋下對這個哥們的回復,沖過去回復我母上大人的問題,之后再回復我兄弟的抱怨,最后回來,繼續抓耳撓腮的寫下這段話。
從0開始的操作系統手搓教程11:為底層添磚加瓦:中斷_intr指令-CSDN博客
Charliechen114514/CCOperateSystem: A Tutorial Trying teach you how to make an os in modern tools with gcc, nasm and bochs!
理解好中斷之后,我們繼續說明我們的配置。STM32對我們的中斷管理非常的細致,將中斷分組,其描述優先級的位實際上非常了兩個比較大的部分。如下所示
-
搶占優先級(Preemption Priority):決定中斷是否可以打斷正在執行的其他中斷
-
子優先級(Sub-Priority):當多個掛起中斷具有相同搶占優先級時,決定它們的執行順序
換而言之,回復我母上大人的消息的搶占優先級是1,回復我兄弟的抱怨搶占優先級是2的時候,我顯然會先處理母上大人的事情,這個時候,盡管我正在回復我的兄弟,我也會讓他靠邊站!,這就是搶占優先級的效果。
那子優先級呢?答案是當我們沒有發生中斷的時候,或者說發生的是同級優先級的中斷的時候(可以認為是特殊的“沒有中斷發生”),我們決定先處理誰。舉個例子,我跟好朋友A和好朋友B中,我更喜歡跟A相處的時候,我優先回復A的消息,完事了之后回復B的消息。因此,在存在多個中斷配置的時候,我們需要牢記好,誰會霸道的優先發生,這樣,在處理中斷的時候,就會理清楚基本的思路。
來,舉個例子,我們的Systicks更新中斷總是放到優先級的最后,這個時候,如果你在高優先級的中斷中,調用了跟Systicks相關的系統中斷,你知道會發生什么的。那就是你希望Systicks更新,好衡量給定的毫秒數,但是你又在高優先級中斷中,沒辦法執行Systicks更新的回調函數,導致Systicks又不更新,這個事情在HAL中就會非常明顯,HAL庫就是一個強依賴軟件更新的時鐘基礎系統,因此,這個時候調用基于這個時鐘的延時,都會導致矛盾:希望Systicks中斷正常工作觸發軟件定時器更新,但是你處于高優先級中斷中無法打斷此時此刻的中斷去更新軟件定時器。導致系統直接死機。
配置上,對于STM32F103C8T6而言,就是這樣了。
分組 | 搶占優先級位數 | 子優先級位數 | 搶占優先級范圍 | 子優先級范圍 |
---|---|---|---|---|
Group0 | 0位 | 4位 | 0 | 0-15 |
Group1 | 1位 | 3位 | 0-1 | 0-7 |
Group2 | 2位 | 2位 | 0-3 | 0-3 |
Group3 | 3位 | 1位 | 0-7 | 0-1 |
Group4 | 4位 | 0位 | 0-15 | 0 |
這個中斷,就對應了如下的結構體的NVIC_IRQChannelPreemptionPriority參數和NVIC_IRQChannelSubPriority參數,我們依次對應了搶占優先級和子優先級:
typedef struct
{uint8_t NVIC_IRQChannel; ? ? ? ? ? ? ? ? ? /*!< Specifies the IRQ channel to be enabled or disabled.This parameter can be a value of @ref IRQn_Type (For the complete STM32 Devices IRQ Channels list, pleaserefer to stm32f10x.h file) */
?uint8_t NVIC_IRQChannelPreemptionPriority; /*!< Specifies the pre-emption priority for the IRQ channelspecified in NVIC_IRQChannel. This parameter can be a valuebetween 0 and 15 as described in the table @ref NVIC_Priority_Table */
?uint8_t NVIC_IRQChannelSubPriority; ? ? ? ? /*!< Specifies the subpriority level for the IRQ channel specifiedin NVIC_IRQChannel. This parameter can be a valuebetween 0 and 15 as described in the table @ref NVIC_Priority_Table */
?FunctionalState NVIC_IRQChannelCmd; ? ? ? ? /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannelwill be enabled or disabled. This parameter can be set either to ENABLE or DISABLE */ ?
} NVIC_InitTypeDef;
剩下的NVIC_IRQChannelCmd我們直接填寫enable即可。那NVIC_IRQChannel說明什么呢?答案是——標識使能哪一個中斷。中斷也是需要使能的,牢記一句話——我們的一切電子設備幾乎都是依靠時鐘才能進行節拍有序的工作,沒有基準時鐘,所有的器件沒有了節拍,自然也就黯淡失色
隨后,我們把這個結構體提交給NVIC_Init函數即可。
處理中斷回調函數
剩下的發生的事情,那就是交給單片機了,他經過一系列復雜的機制,終于到達了ST工程師們預設的中斷,對于PB0(舉個例子),這個IRQ_Handler是誰呢?EXTI0_IRQHandler。是隨便起的名字嘛?不是。請看匯編——
g_pfnVectors:
?.word _estack.word Reset_Handler.......word EXTI0_IRQHandler.word EXTI1_IRQHandler.word EXTI2_IRQHandler.word EXTI3_IRQHandler.word EXTI4_IRQHandler.......word EXTI9_5_IRQHandler.......word EXTI15_10_IRQHandler......
在這一串代碼里,我們很快找到了幾個我們看起來很熟悉的東西,對的,就是這些,你會發現這些就是著名的IRQHandler,也就是中斷處理回調函數,對于應用層次,你只需要理解的是發生了這個中斷,就會觸發回調(回調回調,回來調用)函數,來執行你的處理邏輯。
PB0對應的外部中斷線是EXTI0線,也就是EXTI0_IRQn,觸發的函數回調就是EXTI0_IRQHandler。因此,我們只需要在EXTI0_IRQHandler寫上我們的處理邏輯即可。
需要注意的是,中斷處理結束后,需要清理標置位,這個東西相當于我們的中斷是否發生的標志,只有標志為——處理結束,沒有發生了之后,我們才會理回新的中斷!
本篇的實驗代碼
出于篇幅原因,這里不再粘貼代碼,地址在這里:
BetterATK/103c8t6/standard/3_GPIO_EXIT at STM32F1 · Charliechen114514/BetterATKhttps://github.com/Charliechen114514/BetterATK/tree/STM32F1/103c8t6/standard/3_GPIO_EXIT
這里需要注意的是——我們引入了一個新的器件就是OLED器件,關于OLED器件的編程屬于額外的內容,你需要做的就是將SCL接到PB8,SDA接到PB9,筆者寫的CCGraphic框架會自動的在你的OLED屏上展示內容。具體的原理需要等待你掌握IIC協議和SPI協議之后,我們才能開始真正的修改內部的代碼。
現象如下