動的完整實現(xiàn))
簡介面向初學(xué) 32 位單片機開發(fā)的讀者這份資源圍繞 STM32 外部中斷 EXTI 實現(xiàn)按鍵控制 LED 燈提供一套可直接導(dǎo)入的 Keil 工程。與輪詢按鍵相比中斷方式讓 MCU 在按鍵按下瞬間響應(yīng)既能降低 CPU 占用也更貼近實際產(chǎn)品的中斷驅(qū)動設(shè)計適合作為學(xué)習(xí)外部中斷、GPIO 輸入和 NVIC 中斷管理的入門練手項目。資源為 rar 壓縮包共 185 個文件約 4.75MB。文件構(gòu)成以 .c/.h 源碼、.uvprojx 工程文件、.hex/.axf 可執(zhí)行燒錄文件為主同時保留 .o/.map/.lst 等編譯輔助文件便于查看工程結(jié)構(gòu)和編譯輸出信息解壓后即可打開工程對照學(xué)習(xí)。目前已有 4928 人學(xué)習(xí)下載。包內(nèi)完整示例覆蓋 GPIO 初始化、EXTI 線映射與下降沿觸發(fā)配置、NVIC 優(yōu)先級設(shè)置以及中斷服務(wù)函數(shù)編寫核心代碼注釋清晰能夠幫助讀者理解 EXTI0_IRQHandler 的執(zhí)行流程掌握后還可遷移至傳感器信號檢測、無線接收喚醒等場景。1. 按鍵控制LED的進階玩法用EXTI把CPU從輪詢里解放出來做單片機開發(fā)的人應(yīng)該都有過這種經(jīng)歷用輪詢方式掃描按鍵主循環(huán)里不停讀引腳電平為了消抖還得塞幾個毫秒的延時。按鍵少的時候還能忍一旦按鍵數(shù)量上來或者主邏輯里有復(fù)雜的顯示、通信任務(wù)輪詢就讓系統(tǒng)變得又卡又難維護。這個基于STM32外部中斷EXTI的按鍵控制LED項目就是來解決這個問題的。它把按鍵觸發(fā)從“CPU主動查”變成“硬件通知CPU”按鍵按下時自動跳進中斷服務(wù)函數(shù)主循環(huán)只負責(zé)響應(yīng)標志位邏輯干凈擴展也容易。適合剛?cè)腴T中斷機制的初學(xué)者也適合想把自己按鍵邏輯重構(gòu)成事件驅(qū)動的開發(fā)者拿來當(dāng)模板。項目本身不大但覆蓋了GPIO輸入配置、EXTI外部中斷線、NVIC優(yōu)先級、中斷服務(wù)函數(shù)寫法和按鍵消抖這套完整鏈路。你把它跑通之后相當(dāng)于掌握了STM32中斷系統(tǒng)里最常用的一類應(yīng)用場景后面做編碼器、紅外遙控、觸摸按鍵思路都是相通的。2. 項目拆解硬件接線、EXTI工作鏈路與開發(fā)環(huán)境準備2.1 硬件接線和物料清單做這個項目需要的物料非?;A(chǔ)一塊STM32F103系列的核心板常見的是C8T6也就是藍丸那種板型一個輕觸按鍵一個發(fā)光二極管和限流電阻加上幾根杜邦線。接線關(guān)系也很直觀按鍵一端接GND另一端接任意的PA0引腳LED正極通過一個220歐姆到1千歐的限流電阻接到PB0引腳LED負極直接接GND。這里按鍵的接法是“按下接地”也就是低電平觸發(fā)。選擇PA0是有講究的它對應(yīng)EXTI0中斷線配置起來比較直接。如果你手頭只有其他引腳也可以選PA1到PA15但要記得把EXTI線和NVIC通道同步改掉后面會專門講這個坑。硬件連接好之后還有一個容易忽略的點按鍵兩端最好并一個100nF的陶瓷電容或者給PA0接一個10k歐姆上拉電阻。雖然STM32內(nèi)部可以開啟上拉但外部加一個上拉能減少懸空導(dǎo)致的誤觸發(fā)。開發(fā)板上如果已經(jīng)集成按鍵和LED那就省事多了直接跳線帽接好就能用。2.2 EXTI的工作機制從引腳電平變化到CPU響應(yīng)外部中斷EXTI的本質(zhì)是把引腳上的電平變化上升沿、下降沿或者雙邊沿映射到對應(yīng)的中斷線再由NVIC向CPU發(fā)出中斷請求。STM32F103一共有16條EXTI線EXTI0到EXTI15對應(yīng)PA0到PA15也就是說同一編號的引腳只能有一條能映射到該EXTI線。如果PA0和PB0同時使用EXTI0配置時會沖突所以引腳選擇上要避開同號引腳重復(fù)。整個觸發(fā)鏈路是這樣的引腳電平變化 - 經(jīng)過輸入數(shù)據(jù)寄存器采樣 - EXTI邊沿檢測 - 置位掛起寄存器 - NVIC判斷優(yōu)先級和使能狀態(tài) - 跳轉(zhuǎn)到對應(yīng)中斷服務(wù)函數(shù)。我們工作里不需要關(guān)心寄存器細節(jié)用標準外設(shè)庫或HAL庫配置好就行但腦子里要清楚這鏈條上每一環(huán)都可能成為問題點。比如引腳模式配置錯了輸入采樣就永遠讀不到變化NVIC沒使能就算是EXTI掛起了也不會進中斷中斷服務(wù)函數(shù)名字寫錯鏈接時找不到入口程序就直接跑飛。開發(fā)環(huán)境方面我用的是Keil MDK加標準外設(shè)庫也就是很多人熟悉的固件庫。這個組合在資料數(shù)量和工程移植上最省心代碼里每個外設(shè)一個結(jié)構(gòu)體初始化流程一目了然。如果你用STM32CubeMX生成HAL代碼邏輯也差不多只是函數(shù)名變成了HAL_GPIO_EXTI_Callback核心思路是一樣的。3. 核心實現(xiàn)從GPIO配置到中斷服務(wù)函數(shù)的完整代碼講解3.1 按鍵和LED的GPIO初始化整個工程的起點是GPIO配置。在STM32里任何外設(shè)使用前都要打開對應(yīng)的時鐘然后配置引腳模式。按鍵引腳PA0要做成上拉輸入LED引腳PB0要做成推挽輸出。這里特別注意按鍵按下接地所以內(nèi)部上拉讓默認狀態(tài)是高電平按下時變成低電平這樣下降沿就是有效的觸發(fā)邊沿。void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 打開GPIOA和GPIOB的時鐘 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 按鍵引腳 PA0: 上拉輸入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉輸入 GPIO_Init(GPIOA, GPIO_InitStructure); // LED引腳 PB0: 推挽輸出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽輸出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); }按鍵用上拉輸入平時引腳讀到高電平按下時由于按鍵另一端接GND引腳被拉低。這種接法的好處是徹底避免了浮空輸入時電平隨機變化導(dǎo)致的誤觸發(fā)。LED配置成推挽輸出輸出高電平點亮。速率這里設(shè)成50MHz雖然LED用不了這么高的翻轉(zhuǎn)速度但統(tǒng)一配置不容易出錯。3.2 EXTI外部中斷線配置與NVIC優(yōu)先級GPIO配置好之后要把PA0關(guān)聯(lián)到EXTI0線。標準庫的做法是先配置GPIO的復(fù)用功能映射到EXTI然后設(shè)置EXTI的觸發(fā)邊沿和使能狀態(tài)最后在NVIC里打開對應(yīng)中斷通道。void EXTI_Config(void) { EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 把PA0映射到EXTI0 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); // 配置EXTI0: 下降沿觸發(fā) EXTI_InitStructure.EXTI_Line EXTI_Line0; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; // 下降沿 EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure); // NVIC配置: 中斷通道、優(yōu)先級、使能 NVIC_InitStructure.NVIC_IRQChannel EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }這里GPIO_EXTILineConfig是標準庫里專門做引腳到中斷線映射的函數(shù)。如果跳過這一步即使EXTI0配置了引腳PA0的電平變化也不會送進EXTI0。觸發(fā)方式選下降沿因為我們的按鍵按下時引腳從高變低。NVIC優(yōu)先級里搶占優(yōu)先級設(shè)為1子優(yōu)先級設(shè)為1。這個項目里只有這一個是外部中斷源優(yōu)先級不用太糾結(jié)但如果是多中斷嵌套的場景就需要注意搶占優(yōu)先級高的能打斷低的。標準庫的NVIC配置對分組也有要求通常在main函數(shù)開頭調(diào)用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)來設(shè)置分組兩組搶占優(yōu)先級、兩子優(yōu)先級。3.3 中斷服務(wù)函數(shù)與高效的主循環(huán)邏輯中斷服務(wù)函數(shù)的名字在啟動文件里已經(jīng)定義好了標準庫的啟動文件中EXTI0_IRQHandler就是這個中斷線的入口。函數(shù)內(nèi)部需要先判斷中斷標志位執(zhí)行完邏輯后務(wù)必清除標志位。否則中斷會反復(fù)進入卡死主循環(huán)。// 外部中斷0的服務(wù)函數(shù) void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { // 按鍵按下翻轉(zhuǎn)LED GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0))); // 清除中斷掛起標志位否則會反復(fù)進入中斷 EXTI_ClearITPendingBit(EXTI_Line0); } }中斷函數(shù)里翻轉(zhuǎn)LED的邏輯比較取巧先讀當(dāng)前輸出電平用1減去當(dāng)前值就是取反。如果你不想用BitAction強轉(zhuǎn)也可以分開寫判斷讀到的輸出是1就寫0讀到的輸出是0就寫1。主循環(huán)則非常輕松不需要任何按鍵掃描代碼int main(void) { // 中斷優(yōu)先級分組2位搶占優(yōu)先級2位子優(yōu)先級 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); GPIO_Config(); EXTI_Config(); while (1) { // 主循環(huán)只做其他事情LED由中斷處理 } }這個設(shè)計把按鍵邏輯從主循環(huán)里完全剝離了。主循環(huán)哪怕去跑一個復(fù)雜的狀態(tài)機、做串口打印或者驅(qū)動屏幕也不會影響按鍵響應(yīng)。中斷觸發(fā)的延遲只在硬件級微秒級別就能完成遠比輪詢高效。3.4 進中斷之前還要不要做軟件消抖按鍵是機械觸點按下瞬間會有幾毫秒的電平抖動。在輪詢方案里通常用延時或者狀態(tài)機消抖在中斷方案里如果直接在中斷里消抖延時會阻塞整個中斷通道嚴重的會拖慢其他中斷響應(yīng)。我見過不少初學(xué)者在中斷服務(wù)函數(shù)里塞delay_ms(20)結(jié)果中斷頻繁進入時主循環(huán)被拖得幾乎卡死。常見的做法有兩類。一類是不在中斷里消抖而是在中斷服務(wù)函數(shù)里只置一個標志位并記錄當(dāng)前系統(tǒng)時間由主循環(huán)在合適的時機判斷消抖。另一類是使用定時器掃描按鍵配合中斷喚醒。對于純學(xué)習(xí)項目我建議用更簡單的方案中斷里翻轉(zhuǎn)LED后立即清除標志位退出機械抖動可能在短時間內(nèi)觸發(fā)多次翻轉(zhuǎn)但最終效果肉眼看不出差別。如果追求嚴謹就在中斷里加入標志位主循環(huán)檢測到標志位后延時10毫秒再讀一次引腳確認電平。4. 工程結(jié)構(gòu)與一鍵跑通用標準庫組織可復(fù)用的代碼4.1 標準庫工程的最小目錄結(jié)構(gòu)用Keil MDK新建工程時官方標準庫工程模板通常把文件分成幾個組。一個精簡到不能再精簡的工程至少要有四個目錄CMSIS存放內(nèi)核和系統(tǒng)啟動相關(guān)文件StdPeriph_Driver存放外設(shè)庫驅(qū)動源文件User存放用戶代碼Startup存放啟動匯編文件。STM32_EXTI_Button/ ├── CMSIS/ │ ├── core_cm3.h │ ├── stm32f10x.h │ └── system_stm32f10x.c ├── StdPeriph_Driver/ │ ├── inc/ │ │ ├── stm32f10x_gpio.h │ │ ├── stm32f10x_exti.h │ │ ├── stm32f10x_nvic.h │ │ └── stm32f10x_rcc.h │ └── src/ │ ├── stm32f10x_gpio.c │ ├── stm32f10x_exti.c │ ├── stm32f10x_nvic.c │ └── stm32f10x_rcc.c ├── Startup/ │ └── startup_stm32f10x_hd.s └── User/ ├── main.c ├── stm32f10x_it.c └── stm32f10x_conf.h實際使用中stm32f10x_it.c是專門放中斷服務(wù)函數(shù)的地方。有些教程把EXTI0_IRQHandler寫在main.c里也能編譯通過但工程規(guī)范起見中斷函數(shù)統(tǒng)一放到stm32f10x_it.c更利于后續(xù)維護。4.2 編譯配置里最容易翻車的三個選項Keil工程創(chuàng)建好后在Options for Target里需要確認三個關(guān)鍵配置。第一個是Device選擇必須和芯片型號一致比如STM32F103C8T6對應(yīng)的是STM32F103C8如果選成高容量型號外設(shè)寄存器地址映射可能不對。第二個是宏定義標準庫要求定義STM32F10X_MD中容量或USE_STDPERIPH_DRIVER前者決定啟動文件的配置后者決定是否包含外設(shè)驅(qū)動頭文件。第三個是Debug里選擇ST-Link或J-Link下載器型號選錯會直接導(dǎo)致無法連接目標芯片。這三個配置看著不起眼新手報錯往往就在這些地方。比如忘記定義STM32F10X_MD編譯時系統(tǒng)時鐘配置函數(shù)可能使用了錯誤的分頻系數(shù)程序能燒錄但運行異常這種問題排查半天都找不到根源。4.3 燒錄驗證的完整步驟燒錄前建議先給板子供電確認LED沒有異常點亮然后用ST-Link連接到SWD接口。在Keil里點擊Download按鈕如果一切正常下載器會寫入flash并自動復(fù)位運行。上電后初始狀態(tài)應(yīng)該是LED滅。按下按鍵LED狀態(tài)翻轉(zhuǎn)松開按鍵不會再翻轉(zhuǎn)。如果發(fā)現(xiàn)按下時LED閃了一下又恢復(fù)或者松手時才翻轉(zhuǎn)多半是觸發(fā)沿和消抖邏輯的問題。我這里給一個實際的驗證思路連續(xù)快速按十次按鍵看LED是否每次都正確翻轉(zhuǎn)。如果有漏翻轉(zhuǎn)說明中斷標志位或優(yōu)先級有沖突如果一次按下去翻轉(zhuǎn)了兩次說明抖動導(dǎo)致的重復(fù)觸發(fā)沒被過濾。5. 避坑指南按鍵中斷里我踩過的五個真實問題5.1 中斷服務(wù)函數(shù)沒有清標志位程序卡死現(xiàn)象按下按鍵后系統(tǒng)仿佛進入死循環(huán)主循環(huán)里的代碼不再執(zhí)行用調(diào)試器暫停后發(fā)現(xiàn)程序停留在EXTI0_IRQHandler內(nèi)。原因中斷標志位未被清除時NVIC會認為該中斷一直處于掛起狀態(tài)退中斷后再次進入形成無限中斷。解決在EXTI0_IRQHandler里處理完業(yè)務(wù)后立刻調(diào)用EXTI_ClearITPendingBit(EXTI_Line0)。還有一個細節(jié)是必須在讀取標志位之后、退出函數(shù)之前清除順序搞反會清掉誤判。5.2 GPIO_EXTILineConfig配置遺漏中斷不觸發(fā)現(xiàn)象所有配置代碼看起來都正確GPIO和NVIC都初始化了但按鍵怎么按程序都沒反應(yīng)調(diào)試器里看到EXTI掛起寄存器始終為0。原因PA0沒有通過GPIO_EXTILineConfig映射到EXTI0。標準庫里這句話是單獨的一步很多人照著教程寫卻漏了這行。解決在EXTI初始化前調(diào)用GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0)。同時注意如果PB0也用了要獨占EXTI0不能和PA0同時映射。5.3 中斷服務(wù)函數(shù)名字寫錯編譯卻通過現(xiàn)象工程編譯無報錯下載后按按鍵無反應(yīng)。檢查后發(fā)現(xiàn)啟動文件里有EXTI0_IRQHandler但自己寫的函數(shù)名是EXTI0_IRQHandler少個字母鏈接器沒有報錯是因為它被當(dāng)成普通函數(shù)處理。原因標準庫啟動文件中每個中斷向量都有固定的函數(shù)名如果用戶函數(shù)名不一致中斷向量表仍然指向啟動文件里的弱定義空函數(shù)相當(dāng)于沒寫。解決打開啟動文件搜索EXTI0_IRQHandler逐字母復(fù)制到自己代碼里。如果使用HAL庫回調(diào)函數(shù)名HAL_GPIO_EXTI_Callback也一樣拼錯不會報警告。5.4 中斷里執(zhí)行耗時操作按鍵響應(yīng)遲鈍現(xiàn)象中斷里加了一個串口打印函數(shù)或者延時消抖按鍵按下后LED要幾十毫秒才翻轉(zhuǎn)期間其他中斷也被拖慢。原因CPU正在執(zhí)行冗長的中斷服務(wù)函數(shù)其他中斷無法搶占同等優(yōu)先級主循環(huán)也被阻塞。解決中斷服務(wù)函數(shù)只做“置標志位”和“清標志位”兩個動作具體翻轉(zhuǎn)LED的操作放到主循環(huán)里做。如果必須立即翻轉(zhuǎn)那也要先退出中斷再去延時不要在中斷里消耗時間。5.5 同號引腳沖突PA0和PB0同時配置EXTI現(xiàn)象后來擴展功能時把PB0也配置成外部中斷發(fā)現(xiàn)原本PA0的中斷失效或者兩個引腳觸發(fā)會串?dāng)_。原因EXTI0只能由一個引腳映射GPIO_EXTILineConfig將PA0和PB0都映射到EXTI0后配置的覆蓋前面的。解決重新選擇引腳比如把按鍵放在PA0其他中斷源放在PB1對應(yīng)EXTI1。設(shè)計引腳分配時先把同編號的EXTI線沖突避開這是一個血淚教訓(xùn)。6. 二次開發(fā)從單按鍵到多按鍵的事件驅(qū)動架構(gòu)改造這個項目最簡單的改造是把單按鍵變成多按鍵。以三個按鍵分別控制三個LED為例需要三條中斷線EXTI0、EXTI1、EXTI2。對應(yīng)引腳用PA0、PA1、PA2LED用PB0、PB1、PB2。每個引腳都要配置GPIO輸入三個EXTI線分別配置三套NVIC通道。中斷服務(wù)函數(shù)下面這樣組織void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { button_event_flag | BTN0_PRESSED; EXTI_ClearITPendingBit(EXTI_Line0); } } void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) ! RESET) { button_event_flag | BTN1_PRESSED; EXTI_ClearITPendingBit(EXTI_Line1); } }主循環(huán)里根據(jù)button_event_flag的位來分發(fā)事件。這里就把“中斷觸發(fā)”和“業(yè)務(wù)處理”解耦了按鍵按下后中斷只負責(zé)登記事件主循環(huán)空閑時才執(zhí)行LED翻轉(zhuǎn)、蜂鳴器發(fā)聲或者屏幕刷新。這種模式在正式工程里叫事件標志位比直接在中斷里操作硬件要穩(wěn)健得多。如果想做更實用的長按、短按、雙擊檢測中斷里需要記錄系統(tǒng)時間戳。用systick或TIM2產(chǎn)生一個毫秒時基按鍵觸發(fā)時把當(dāng)前時間存到變量然后在主循環(huán)狀態(tài)機里對比時間差。這個方向比單純翻轉(zhuǎn)LED有意思也更能體會中斷加狀態(tài)機的威力。我習(xí)慣的做法是寫一個按鍵驅(qū)動模塊把GPIO初始化、EXTI配置、事件標志位封裝成獨立文件這樣換芯片平臺時只需要改底層映射。自從我把所有按鍵邏輯都遷移到這種中斷加標志位模式后再也沒為按鍵卡主循環(huán)操過心。希望這個項目能讓你少走一點彎路。本文還有配套的精品資源點擊獲取