
1. 中斷常見的書上關(guān)于中斷舉的例子差不多就是這樣“你正在看書廚房的水開了你起來去關(guān)火關(guān)了火以后繼續(xù)看書”這種例子說他好吧差不多把中斷的概念描述清楚了但是根本沒辦法和單片機的聯(lián)系起來因為這種例子所描述的中斷的概念光看“中斷”這兩個字也能猜個八九不離十說了等于沒說實際上從正面去講中斷的概念也并不復雜我甚至覺得不舉這種例子反而更好理解計算機程序正常情況都是從上到下依次執(zhí)行的當發(fā)生一些內(nèi)部或者外部事件的時候CPU本來應該繼續(xù)向下執(zhí)行的但是卻沒有繼續(xù)向下執(zhí)行反而從當前執(zhí)行的代碼處離開去執(zhí)行別處的代碼這個過程就叫中斷這些內(nèi)部或者外部的事件就叫做中斷源。這里指討論外部中斷等后面學到了其他的中斷在討論其他的。1.1 NVIC與EXTI眾所周知單片機的MCU是由內(nèi)核和片上外設組成的內(nèi)核就是一般說的某某內(nèi)核的芯片比如戰(zhàn)艦開發(fā)板采用 ARM Cortex-M3 內(nèi)核內(nèi)核就包括CPU核心以及一些內(nèi)部外設內(nèi)部外設的內(nèi)是相對內(nèi)核說的而片上外設就是內(nèi)核外面的那些外設下圖我框起來的部分Cortex-M3’s internal peripherals這部分映射的地址就是控制內(nèi)部外設的寄存器地址而右邊的那些(我的截圖不完整只截了一部分)非常多的就是片上外設的寄存器地址了。NVIC是內(nèi)部外設的一種EXTI則是片上外設他們兩個搭配起來就構(gòu)成了外部中斷的完整配置了。1.1.1 外部中斷與Cortex-M3內(nèi)核所謂外部中斷與M3內(nèi)核的關(guān)系其實就是介紹M3內(nèi)核的NVIC在啟動文件中可以看到一個叫做g_pfnVectors的變量(我用的不是keil編譯器有可能和keil的啟動文件會有些許差異估計應該差不了很多)這個變量是指向中斷向量表的指針中斷向量表里存的就是中斷服務函數(shù)的地址當我們進行了一些恰當?shù)呐渲靡院笕绻獠坑|發(fā)了某個中斷CPU就會去這個表里找對應函數(shù)的地址然后去該地址執(zhí)行相應的中斷服務函數(shù)比如在正點原子的外部中斷例程中有一個頭文件是exti.h,里面有一個宏定義是#define KEY0_INT_IRQHandler EXTI4_IRQHandler這個宏的右邊就是下圖啟動文件的第159行左邊就是對應的中斷服務函數(shù)因為C語言的函數(shù)名就是函數(shù)的地址所以這樣寫沒啥問題這也能解釋為什么最底層的中斷函數(shù)無人調(diào)用了當中斷觸發(fā)的時候由CPU去自己找對應的函數(shù)。中斷向量表的這些函數(shù)順序是芯片公司出廠就定義好的我們只能服從不能隨意修改。NVIC的寄存器介紹不在《STM32F10xxx參考手冊》中需要去《Cortex-M3權(quán)威指南》中查找相關(guān)寄存器的介紹關(guān)于這部分寄存器在代碼中的定義位于頭文件core_cm3.h中代碼中的名稱和權(quán)威指南中的名稱不太一樣我在下面的表做個記錄寄存器功能權(quán)威指南中的名稱代碼中的名稱中斷使能ISERSETENA中斷失能ICERCLRENA中斷掛起ISPRSETPEND中斷解掛ICPRCLRPEND中斷激活標志寄存器IABRACTIVE中斷優(yōu)先級寄存器IPPRI還有一個軟件觸發(fā)中斷寄存器這個我目前還不知道在那些場合會用到估計是操作系統(tǒng)吧反正也和外部中斷沒關(guān)系后面遇到了再說。中斷的使能與失能寄存器SETENAs和CLRENAs表示中斷的使能和去使失能寄存器s可以取0到7,總共8對寄存器每個寄存器又有32位按照每一位來配置一個中斷來看最多可以配置32*8256個中斷而M3內(nèi)核的SETENA7和CLRENA7只使用了16位所以M3最多可以支持240個中斷而在STM32F103ZET6中被進一步裁減只使用了60位也就是這兩組寄存器中s0,1的情況其他的外設寄存器使能和失能一般是寫1使能寫0失能NVIC比較特殊使能某個中斷就給SETENAs對應的位寫1失能就是給CLRENAs對應位寫1,寫0沒啥用如果使能和失能寄存器的對應位都寫1的話那就是哪個后執(zhí)行哪個生效先失能后使能那中斷有效先使能后失能那中斷無效不過應該沒人會這么干吧中斷的掛起與解掛起寄存器中斷在執(zhí)行的時候來了高優(yōu)先級的中斷會打斷當前中斷而來了同優(yōu)先級或者低優(yōu)先級的中斷那么中斷就不能立即得到響應此時中斷被掛起。中斷的掛起狀態(tài)可以通過“中斷掛起寄存器(SETPEND)”和“中斷解掛起寄存器(CLRPEND)”來讀取還可以通過軟件寫入對應位來手動掛起中斷不過使用情況不多一般都是硬件置位與清零。如果需要軟件寫入的話具體的使用方法和前面的使能和失能寄存器相同。中斷激活標志寄存器當CPU進入了某個中斷服務函數(shù)以后執(zhí)行了第一條匯編指令之后中斷激活標志寄存器的對應位就會被硬件置1這是一個只讀寄存器通過讀這個寄存器就能知道當前正在執(zhí)行哪個中斷。這類寄存器和之前一樣也是總共8個除了最后一個寄存器使用了16位之外其余的每一位對應一個中斷源。中斷優(yōu)先級配置寄存器通過配置這個寄存器就可以配置兩個或以上的中斷同時觸發(fā)的時候先執(zhí)行哪個再執(zhí)行哪個這類寄存器總共有240個和之前一樣STM32只用到了60個每一個寄存器對應一個中斷源但是這類寄存器只有八位所以它們相鄰的寄存器之間的地址偏移量是1上面提到的其他的寄存器偏移量是4。每個寄存器可以把寄存器的8位分成兩組數(shù)來分別表示搶占優(yōu)先級和響應優(yōu)先級搶占優(yōu)先級高的中斷可以先執(zhí)行并且搶占優(yōu)先級高的中斷可以打斷搶占優(yōu)先級低的中斷搶占優(yōu)先級相同的情況下響應優(yōu)先級高的先執(zhí)行搶占優(yōu)先級相同但是響應優(yōu)先級高的中斷不能打斷響應優(yōu)先級低的中斷如果這倆優(yōu)先級都一樣那就按自然優(yōu)先級來決定執(zhí)行順序自然優(yōu)先級就是在中斷向量表里的先后順序。搶占優(yōu)先級和響應優(yōu)先級的英文翻譯是preempt priority和subpriority根據(jù)pre和sub這兩個英文前綴也能看出來這兩個優(yōu)先級是什么意思不過我也不知道位啥要翻譯成響應優(yōu)先級這么怪的名字也許學到后面就懂了吧。1.1.2 外部中斷與STM32F103ZET6NVIC是內(nèi)核控制中斷的外設所有具有cortex-M3內(nèi)核的芯片都大差不差而如何把中斷信息傳遞到NVIC就是片上外設的事情了在stm32f103xe.h這個頭文件中定義了一個如下圖所示的枚舉82行的注釋下面的部分總共有60個就是之前說的可以把中斷信號傳遞到NVIC的60個外設其中第89到93行的EXTI就是根據(jù)外部的電平變化配置的中斷也是這次學習的主要內(nèi)容。EXTI是外部中斷和事件控制器根據(jù)上面的圖可以看出來這個芯片總共有五根EXTI線每根線之間相互獨立可以分別進行不同的配置比如輸入類型和觸發(fā)事件觸發(fā)事件指的是外部信號的上升沿觸發(fā)下降沿觸發(fā)或者雙沿觸發(fā)而輸入類型這個名詞是從正點原子給的開發(fā)指南上看到的我覺得更應該叫做EXTI的輸出類型EXTI接收的信號是外部的電平變化信號它對這些信號處理了以后再進行產(chǎn)生脈沖或者傳遞信號至NVIC所以相比EXTI來說它更應該被叫做輸出類型。來看參考手冊中的這個外部中斷/事件控制器框圖生事件/中斷請求的邊沿檢測器組成總共20根并行的獨立的線右下角的輸入線三電平變化信號輸入的地方它可以是任何一個GPIO口或者是一些其他外設事件進來之后經(jīng)過一個邊沿檢測電路這個邊沿檢測電路上面的那倆寄存器就是用來配置是上升沿下降沿還是雙沿觸發(fā)的通過配置對應的寄存器就可以配置相應的中斷源了然后經(jīng)過一個與軟件中斷事件寄存器組成的或門電路這個寄存器一般也用不到配置一直輸出0然后就到了EXTI的輸出岔路了一條是與事件屏蔽寄存器組成的與門電路另一條傳輸至請求掛起寄存器先看和事件屏蔽寄存器組成的這個與門電路這個與門電路的輸出是為了給其他外設電路產(chǎn)生脈沖信號比如定時器等通過配置事件屏蔽寄存器就可以屏蔽相應的外設事件從而不產(chǎn)生對應的脈沖信號起到屏蔽事件的作用另外一路傳到請求掛起寄存器當信號傳到這個寄存器的時候?qū)痪蜁挥布梦蝗缓蟾嬖VCPU現(xiàn)在來了個中斷能不能處理如果此時有更高優(yōu)先級的中斷正在執(zhí)行現(xiàn)在不能處理這個中斷的話CPU就會把NVIC的掛起寄存器也置位然后這個中斷就被掛起了如果可以執(zhí)行的話CPU就把這個中斷執(zhí)行了但是這個請求掛起寄存器的對應位清零的工作就得通過代碼來實現(xiàn)了一般都會放到中斷服務函數(shù)里面來清零很多初學者一開始看到中斷的代碼里清標志位也許都會一頭霧水至少我有一頭霧水這里就解釋了這個標志位的來源了然后信號經(jīng)過中斷屏蔽寄存器組成的與門之后進入NVIC這里和剛才說的事件屏蔽寄存器哪個與門意思差不多所以這里就是配置中斷屏蔽寄存器就是字面意思了就是為了屏蔽某一路中斷從而讓這個與門輸出0切斷它與NVIC的聯(lián)系。1.2 EXTI與GPIO配置剛才提到EXTI的輸入可以是任何一個GPIO口或者一些其他的外設事件總結(jié)如下EXTI線對應連接0-15GPIO輸入16PVD輸出17RTC鬧鐘18USB喚醒19以太網(wǎng)喚醒除了前面的0-15是現(xiàn)在需要討論的后面的都暫時不看因為我也不會而且戰(zhàn)艦開發(fā)板不是互聯(lián)型產(chǎn)品所以第19的以太網(wǎng)喚醒是沒有的。stm32f103系列總共有GPIOxx A - G7組每組16個總共7*16112個GPIO上面對應的中斷線只有16根顯然不夠但是注意到GPIO每組的個數(shù)和中斷線的條數(shù)是一樣的所以芯片公司的設計也是這樣的第0根中斷線對應的IO口可以選擇GPIOA.0、 GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0 和 GPIOG.0中的一個第1根可以選擇GPIOA.1、 GPIOB.1、GPIOC.1、GPIOD.1、GPIOE.1、GPIOF.1 和 GPIOG.1中的一個剩下的就同理了所以說GPIO能同時啟用中斷的IO口最多只有16個其實也很顯然因為只有16根線嘛配置這個對應關(guān)系的寄存器是這些AFIO_EXTICR1 ~ AFIO_EXTICR4有沒有感覺亂七八糟的打開芯片手冊找到描述這些寄存器的地方就看AFIO_EXTICR2吧它指的是第4到7根中斷線可選擇的IO口每四位組成一個四位整數(shù)以此來選擇GPIO的A-G其余的中斷配置方法就在這個同名的其他寄存器這個比較簡單就到此為止了。1.2 外部中斷的配置整體來看外部中斷配置的過程分為三個部分第一部分是把GPIO映射到對應的中斷上使得中斷信號能傳到EXTI第二部分是配置EXTI使得信號能傳遞到NVIC最后一部分就是NVIC的配置了接下來就來點個燈吧。1.2.1 IO口的配置首先要通過看原理圖來知道我們是從哪里獲取中斷源的下面這張圖的四個開關(guān)就是獲取中斷源的當開關(guān)按下的時候MCU根據(jù)相應的電平變化來進入相應的中斷服務。比如KEY_UP這個開關(guān)按下以后MCU的PA0就會變成3.3V的高電平電平變化是由低到高所以我們要把它對應的中斷配制成上升沿觸發(fā)其余的三個開關(guān)按下的時候?qū)腎O口就會變成低電平電平變化是由高到低所以要配制成下降沿觸發(fā)。它是要接收外部信號的所以肯定配制成輸入模式又因為要采集電平變化所以必然還得有一個上拉或者下拉來配制初始狀態(tài)以KEY_UP為例看一下。voidgpio_init(void){WKUP_GPIO_CLK_ENABLE();/* WKUP時鐘使能 */sys_gpio_set(WKUP_GPIO_PIN,SYS_GPIO_MODE_IN,SYS_GPIO_PUPD_PULLDOWN);/* WKUP引腳模式設置為下拉輸入 */}1.2.2 EXTI的配置EXTI的配置就兩步第一步就是配置AFIO來映射中斷配置之前先使能AFIO時鐘第二步是配置上升沿和下降沿。這個函數(shù)和正點原子會有點不同為了方便理解我把官方例程重新寫了但是這樣的結(jié)果就是每次只能配置一個IO口而官方例程可以同時配置同一組GPIO的多個口不過也沒啥用即使官方可以同時配置多個它的例程也是寫了多次而且這么多IO口在實際應用中應該也不會大量的使用同一組的GPIO的中斷。voidsys_nvic_ex_config(GPIO_TypeDef*p_gpiox,uint16_tpinx,uint8_ttmode){uint8_tgpio_num0;/* gpio編號, 0~7, 代表GPIOA~GPIOG *//* 計算gpio編號,因為每一組GPIO之間的地址偏移相差0x400所以要除以它 */gpio_num((uint32_t)p_gpiox-(uint32_t)GPIOA)/0X400;RCC-APB2ENR|10;/* AFIO 1,使能AFIO時鐘 *//* 配置AFIO寄存器*/switch(gpio_num){case0:case1:case2:case3:AFIO-EXTICR[0]~(0x000F(pinx%4));AFIO-EXTICR[0]|(gpio_num(pinx%4));break;case4:case5:case6:case7:AFIO-EXTICR[1]~(0x000F(pinx%4));AFIO-EXTICR[1]|(gpio_num(pinx%4));break;case8:case9:case10:case11:AFIO-EXTICR[2]~(0x000F(pinx%4));AFIO-EXTICR[2]|(gpio_num(pinx%4));break;case12:case13:case14:case15:AFIO-EXTICR[3]~(0x000F(pinx%4));AFIO-EXTICR[3]|(gpio_num(pinx%4));break;}/*配置上下拉觸發(fā)模式tmode遵循官方例程定義*/if(tmode0x01)EXTI-FTSR|1gpio_num;if(tmode0x02)EXTI-RTSR|1gpio_num;}最后就是EXTI的初始化了直接調(diào)用上面的函數(shù)就行了voidextix_init(void){sys_nvic_ex_config(KEY0_INT_GPIO_PORT,WKUP_GPIO_PIN,SYS_GPIO_RTIR);/* WKUP配置為上升沿觸發(fā)中斷 */}1.2.3 NVIC的配置NVIC的配置就是配置一些中斷優(yōu)先級分組以及配置中斷優(yōu)先級之類的voidsys_nvic_init(uint8_tpprio,uint8_tsprio,uint8_tch,uint8_tgroup){uint32_ttemp;sys_nvic_priority_group_config(group);/* 設置分組不設置的話會使用默認的分組0 */temppprio(4-group);/*把pprio放到表示搶占優(yōu)先級的高位*/temp|sprio(0x0fgroup);/*把pprio放到表示響應優(yōu)先級的低位*/temp0xf;/* 取低四位 */NVIC-ISER[ch/32]|(1(ch%32));/* 使能中斷位(要清除的話,設置ICER對應位為1即可) */NVIC-IP[ch]|temp4;/* 設置響應優(yōu)先級和搶斷優(yōu)先級 */}然后就是NVIC初始化其實是沒必要分這么細的就像官方例程那樣這些初始化的東西就應該和上面的別的初始化內(nèi)容放在一起。voidnvic_init(void){sys_nvic_init(3,2,WKUP_INT_IRQn,2);/* 搶占3子優(yōu)先級2組2 */}最后就該寫中斷服務函數(shù)了這個WK_UP開關(guān)對應的是MCU的PA0所以對應第0根中斷線也就是需要寫一個上面啟動文件中和89行同名的函數(shù)不過就像一開始說的一般都使用宏定義起更好聽的名字了。#defineWKUP_INT_IRQHandlerEXTI0_IRQHandlervoidWKUP_INT_IRQHandler(void){delay_ms(20);/* 消抖 */EXTI-PRWKUP_INT_GPIO_PIN;/* 清除WKUP所在中斷線 的中斷標志位 */if(WK_UP1){BEEP_TOGGLE();/* LED2 狀態(tài)取反 */}}通過看Cortex-M3權(quán)威指南發(fā)現(xiàn)中斷遠非上面寫的這么簡單不過對于日常的產(chǎn)品使用應該也勉強夠用了來日方長慢慢學。