:從GPIO到NVIC的完整鏈路)
開始用寄存器寫STM32之后我最大的感受是庫函數(shù)把外設(shè)包裝得再漂亮最后真正一起配合工作的還是那一堆寄存器。上一章聊過GPIO這一章繼續(xù)走寄存器路線把外部中斷這一件事用大白話講透。外部中斷入門簡單但真正踩坑的人非常多因為它和GPIO不同不再是你寫一個寄存器位就能完事的東西而是橫跨GPIO、復(fù)用功能、中斷控制器、內(nèi)核向量表四塊內(nèi)容任何一個環(huán)節(jié)斷了現(xiàn)象都是“按鍵沒反應(yīng)”。這篇內(nèi)容適合兩類人剛把GPIO點燈跑通、想進一步理解芯片底層到底怎么響應(yīng)外部信號的初學者以及用固件庫寫了好幾個項目、卻說不清中斷服務(wù)函數(shù)為什么能被執(zhí)行的老工程師。這一章按我的習慣來先講清楚整條鏈路再給寄存器代碼最后把我實際調(diào)過的坑全部擺出來。1. 為什么第二章講外部中斷先建立一個整體框圖1.1 中斷不是外設(shè)而是一條完整的事件管道很多新手把外部中斷當成一個獨立外設(shè)來學這是個誤區(qū)。外部中斷沒有自己的狀態(tài)機它實際上是一套“檢測并傳遞事件”的機制引腳電平發(fā)生變化先送到邊沿檢測電路邊沿檢測電路把結(jié)果記錄到一個掛起標志里然后由中斷控制器決定要不要打斷CPU正在執(zhí)行的代碼CPU被允許打斷后跳到一個固定地址執(zhí)行中斷服務(wù)函數(shù)執(zhí)行完再回到原來的位置繼續(xù)干活。這個鏈條里每一環(huán)都有自己的寄存器而且分散在芯片的不同地址區(qū)域。我在第一章講GPIO時說過點燈的本質(zhì)就是往ODR寄存器里寫0或1。但外部中斷的整個鏈路要復(fù)雜得多GPIO負責把引腳狀態(tài)送進來AFIO負責選擇“這個引腳連接到哪一條EXTI線”EXTI寄存器負責檢測上升沿或下降沿并產(chǎn)生請求NVIC負責仲裁這個請求的優(yōu)先級最后CPU響應(yīng)后跳轉(zhuǎn)到向量表指向的中斷函數(shù)。寫中斷代碼本質(zhì)上就是在配置這條管道上的每一個閥門。1.2 這一章學完你能做什么當你把這條管道全部用寄存器串起來你會獲得幾個非常實用的能力。第一手動配置外部中斷初始化不再依賴圖形化配置工具生成的那堆代碼出了問題自己看得懂。第二真正理解“清標志位”為什么那么重要以及誤操作會導(dǎo)致什么現(xiàn)象。第三掌握一套通用的中斷調(diào)試思路以后學串口、定時器、ADC時處理中斷相關(guān)問題的邏輯是一樣的只是換了個外設(shè)名而已。我見過不少開發(fā)者固件庫用了兩三年遇到“中斷只進一次”依然只能靠網(wǎng)上搜答案。原因很簡單他們把中斷當函數(shù)調(diào)用沒有理解硬件掛起和軟件清除的關(guān)系。這一章把這些東西用寄存器層面對齊你會發(fā)現(xiàn)自己以后寫任何中斷外設(shè)都有底氣了。2. 動手寫代碼前先把“誰管誰”搞清楚2.1 EXTI、NVIC、CPU三者的分工拿到參考手冊你會看到外部中斷相關(guān)的寄存器主要集中在“EXTI”這一章但真正用起來還牽扯到GPIO、AFIO、NVIC甚至啟動文件里的向量表。我第一次接觸時也覺得頭大后來用一個比喻才理清EXTI是門鈴按鈕NVIC是處理門鈴聲音的控制面板CPU是正在書房里干活的屋主。門鈴按鈕被按下只是產(chǎn)生了一個“有人來了”的電信號控制面板判斷這個信號是否重要、是否要打擾屋主以及把哪一路門鈴排在最優(yōu)先的位置屋主聽到鈴聲后停下手頭的工作走到門口處理事情處理完再回書房繼續(xù)干活。所以當你寫外部中斷初始化時至少要做四件事把引腳配置成輸入讓GPIO能讀到外部電平把引腳通過AFIO連接到對應(yīng)的EXTI線配置EXTI檢測邊沿并使能中斷請求最后配置NVIC讓CPU“愿意”響應(yīng)這個請求。絕大多數(shù)“不觸發(fā)”的問題都出在這四件事里漏了一件。2.2 中斷模式和事件模式不是一回事EXTI單元本身提供兩種輸出中斷請求和事件請求。中斷請求意味著向NVIC發(fā)出信號NVIC再決定是否讓CPU停下來執(zhí)行中斷服務(wù)函數(shù)。事件請求則不走NVIC它直接把一個脈沖信號送給其他外設(shè)比如觸發(fā)ADC開始一次轉(zhuǎn)換、觸發(fā)定時器捕捉輸入全程不需要CPU參與。這兩種模式對應(yīng)的使能寄存器不同中斷模式用EXTI_IMR事件模式用EXTI_EMR邊沿檢測配置用的都是RTSR和FTSR。初學階段我建議先把中斷模式用熟因為事件模式的收益需要配合其他外設(shè)才體現(xiàn)得出來比如用外部事件讓ADC進行高速采樣能省掉頻繁進入中斷的開銷。但要心里有數(shù)如果哪天你看到別人代碼里只開了EMR、沒開IMR還跟你說“能采樣”那不是寫錯了而是用了事件模式。2.3 每條EXTI線只能接一個引腳EXTI線的編號是固定的從EXTI0到EXTI15一共16根。GPIO也有16個引腳PA0、PB0、PC0這些引腳編號相同的引腳共享同一條EXTI線。也就是說PB0和PC0如果想用外部中斷都得通過EXTI0這條線但同一時刻EXTI0只能從它們中間選一個作為輸入源。這個選擇就是AFIO寄存器組里EXTICR系列寄存器干的事它的每一個四比特位域?qū)?yīng)的就是一條EXTI線值0代表PA組1代表PB組2代表PC組以此類推。這也是初學者最容易被繞暈的地方GPIOA端口號也好GPIOB端口號也好不管哪個端口只要引腳編號是0走的就是EXTI0。換句話說外部中斷的線編號跟著引腳號走不跟著端口號走。PC13按鍵的中斷對應(yīng)EXTI13PB5對應(yīng)EXTI5非常機械。3. 從零配置一個按鍵中斷初始化代碼逐步拆解3.1 實驗場景與接線方式拿一個最常見也最穩(wěn)妥的實驗來說一個按鍵接在PA0引腳按鍵另一端接GND板子上PA0不接其他東西。芯片內(nèi)部配置成上拉輸入這樣按鍵沒按下時PA0讀到高電平按下時PA0被拉到GND讀到低電平松手后又回到高電平產(chǎn)生一個下降沿。我們的目標是在這個下降沿觸發(fā)EXTI0中斷在中斷里翻轉(zhuǎn)PA1上的LED狀態(tài)。如果你的按鍵接法剛好相反一端接電源、一端接PA0那觸發(fā)沿要改成上升沿內(nèi)部下拉也通過ODR位寫0來實現(xiàn)除此之外其他邏輯不變。記住一個原則邊沿方向要和外部電平變化方向一致否則中斷永遠不會來。3.2 初始化順序與完整代碼先說順序這個順序我建議第一次寫的時候嚴格遵守開GPIO時鐘和AFIO時鐘把引腳配置成合適的輸入模式配置AFIO選擇EXTI線來源配置EXTI邊沿檢測使能EXTI中斷請求配置NVIC優(yōu)先級最后寫中斷服務(wù)函數(shù)。順序亂了比如先配NVIC再開時鐘代碼也能跑但排查問題時會多繞很多彎。完整的初始化代碼和中斷服務(wù)函數(shù)如下我直接給出可以復(fù)制到工程里驗證的版本。void EXTI0_Init(void) { // 1. 打開GPIOA和AFIO的時鐘 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; RCC-APB2ENR | RCC_APB2ENR_AFIOEN; // 2. PA0配置為上拉輸入注意是輸入模式不是輸出模式 GPIOA-CRL ~((uint32_t)0x0F 0); GPIOA-CRL | ((uint32_t)0x08 0); GPIOA-ODR | (uint32_t)1 0; // 3. AFIO選擇EXTI0的來源為PA0PA0對應(yīng)值0默認也是0 AFIO-EXTICR[0] ~((uint32_t)0x0F 0); // 4. 下降沿觸發(fā)上升沿禁止邊沿使能 EXTI-FTSR | (uint32_t)1 0; EXTI-RTSR ~((uint32_t)1 0); // 5. 使能EXTI0的中斷請求不是事件請求 EXTI-IMR | (uint32_t)1 0; // 6. 配置NVIC這里用分組2搶占優(yōu)先級1子優(yōu)先級0 SCB-AIRCR 0x05FA0000 | (0x500); NVIC-IP[6] (uint8_t)((1 4) | (0 0x0F)); NVIC-ISER[0] | (uint32_t)1 6; } void EXTI0_IRQHandler(void) { if (EXTI-PR (uint32_t)1 0) { // 先清除掛起標志再執(zhí)行實際功能 EXTI-PR (uint32_t)1 0; GPIOA-ODR ^ (uint32_t)1 1; } }這里面第6步的NVIC配置比較有講究。SCB-AIRCR那行設(shè)置優(yōu)先級分組0x500在常用的固件庫里對應(yīng)“2位搶占優(yōu)先級2位子優(yōu)先級”也就是總共4位優(yōu)先級寄存器拆成前兩位和后兩位。NVIC-IP[6]是配置EXTI0中斷優(yōu)先級的地方高4位有效我寫的是搶占優(yōu)先級1、子優(yōu)先級0兩個數(shù)值都只在各自范圍內(nèi)有意義。最后NVIC-ISER[0]的bit6對應(yīng)的就是EXTI0中斷編號。3.3 GPIO配置成輸入為什么還要搞四個位在CRL寄存器里PA0對應(yīng)最低一個四比特位域其中低兩位是MODE高兩位是CNF。如果你直接對著手冊看會覺得繁瑣但組合起來很有規(guī)律MODE為00表示輸入CNF為10表示上拉或下拉輸入CNF為01表示浮空輸入CNF為00表示模擬輸入。我上面寫0x08二進制就是1000放到四比特位域里正好是CNF10、MODE00輸入模式里的上拉/下拉輸入。那到底上拉還是下拉由ODR寄存器的同一位決定。ODR位寫1是上拉寫0是下拉。所以第3行GPIOA-ODR | 1 0;就是為了把PA0內(nèi)部上拉打開。很多人把ODR當成輸出數(shù)據(jù)寄存器覺得在輸入模式下沒用實際上它在輸入模式下就是上下拉的開關(guān)這點特別容易漏。3.4 中斷函數(shù)名和向量表是綁死的EXTI0的中斷服務(wù)函數(shù)只能叫EXTI0_IRQHandler這個名字不是隨便起的它定義在啟動文件的中斷向量表里。啟動文件給每個中斷安排了一個固定地址里面放了一個函數(shù)指針這個指針默認指向一個空處理函數(shù)。你寫了一個同名函數(shù)鏈接器就會把它放到向量表對應(yīng)的位置你寫錯了名字比如寫成EXTI0_Handler鏈接器會認為向量表里還是那個空處理函數(shù)觸發(fā)中斷后程序會跳進一個空循環(huán)里看起來就是“沒反應(yīng)”。如果按鍵觸發(fā)時程序跑飛或者卡死先查一下中斷服務(wù)函數(shù)名字和向量表是否一致。常見的外部中斷函數(shù)名我列在下面方便對照EXTI0到EXTI4是對應(yīng)中斷號6到10的EXTI0_IRQHandler、EXTI1_IRQHandler直到EXTI4_IRQHandlerEXTI5到EXTI9共用EXTI9_5_IRQHandlerEXTI10到EXTI15共用EXTI15_10_IRQHandler。4. 中斷觸發(fā)與清掛起最容易翻車的細節(jié)4.1 寫1清除掛起為什么這么反直覺外部中斷請求產(chǎn)生后硬件會在EXTI_PR寄存器里把對應(yīng)位置1這就是“掛起標志”。只要掛起標志還是1中斷控制器就會一直看到這個請求存在但它不會無休止地進入同一個中斷因為NVIC在同一優(yōu)先級下會合并掛起狀態(tài)。關(guān)鍵問題在于這個標志怎么清除很多人第一次寫會寫成EXTI-PR ~(1 0);想把位置0。結(jié)果發(fā)現(xiàn)中斷要么不觸發(fā)要么永遠卡在中斷里不出來。原因很簡單EXTI_PR是一個寫1清除寄存器硬件約定“軟件往這個位寫1表示確認處理完這次事件硬件隨即把該位清0”。它和普通讀寫寄存器完全反著來你寫0對它來說相當于沒操作。我推薦直接寫EXTI-PR (1 0);而不是用或賦值。原因是在高速觸發(fā)場景下讀改寫操作存在競態(tài)風險你在讀PR的瞬間另一個外部事件已經(jīng)置位了另一個掛起位你改完再寫回去可能會把這個新事件也一并處理掉。直接寫自己那一位既完成清除又不影響其他位。4.2 掛起不止一層NVIC里還有一個Pend很多人在清完EXTI_PR之后發(fā)現(xiàn)中斷還是會連續(xù)進入兩次于是懷疑清標志的代碼沒生效。其實這里藏著一層更深的機制NVIC自己也有一個掛起狀態(tài)叫Pending。外部中斷觸發(fā)時EXTI_PR置1同時NVIC收到請求也會在內(nèi)部記錄一個掛起。如果你的中斷服務(wù)函數(shù)正在執(zhí)行此時同一條EXTI線又來了一次觸發(fā)EXTI_PR會被重新置1NVIC也會把新的掛起疊加上去。你在函數(shù)末尾清EXTI_PR只能清除外設(shè)級的掛起NVIC內(nèi)部可能還壓著一條請求函數(shù)一退出它又立刻進入一次。這種“清不干凈”的典型表現(xiàn)就是一次機械按鍵按下去中斷函數(shù)連續(xù)跑了兩遍第二遍看起來莫名其妙。如果遇到這個問題除了檢查按鍵硬件抖動還可以在中斷服務(wù)函數(shù)里清PR之后把NVIC的掛起也清一下寫NVIC-ICPR[0] | (1 6);。這個寄存器同樣是寫1清除它的工作就是把NVIC里殘留的掛起請求清掉。當然這只是一種兜底更關(guān)鍵的是理解你的程序里是否真的會產(chǎn)生多余的NVIC掛起。4.3 優(yōu)先級分組決定了誰能打斷誰有個高頻誤解我把外部中斷優(yōu)先級寫成了數(shù)字1怎么還是被打斷了你得先搞清這個數(shù)字的哪幾位是搶占優(yōu)先級、哪幾位是子優(yōu)先級這完全由優(yōu)先級分組決定。我上面代碼里用的是分組2也就是2位搶占、2位子優(yōu)先級此時優(yōu)先級寄存器里高兩位是搶占優(yōu)先級低兩位是子優(yōu)先級。我配置NVIC-IP[6]時寫入(1 4)意思是搶占優(yōu)先級為1子優(yōu)先級為0注意寫入時要左移4位把值放到高四位去。搶占優(yōu)先級才是決定能否打斷別人的關(guān)鍵。如果串口中斷的搶占優(yōu)先級是2外部中斷是1那么外部中斷發(fā)生時會打斷串口中斷。如果兩者搶占優(yōu)先級都是1那么子優(yōu)先級決定誰先響應(yīng)如果搶占和子優(yōu)先級都一樣NVIC會按中斷編號的自然順序來決定編號小的先響應(yīng)。在項目里配置多個中斷時一定要先分好組再給每個中斷定搶占優(yōu)先級否則會出現(xiàn)兩個中斷互相等待或者響應(yīng)順序混亂的問題。5. 實際項目里常見的五個坑和排查思路5.1 怎么按都不進中斷按鍵按到手酸中斷一次都不進這是最常見的開局。我建議不要亂改代碼按順序做一次靜態(tài)檢查時鐘GPIO模式AFIO映射EXTI邊沿EXTI使能NVIC中斷函數(shù)名。這七項里任何一項錯了都進不去。其中有兩個坑最容易藏一個是忘記打開AFIO時鐘導(dǎo)致EXTI始終選不到想要的引腳另一個是把GPIO配置成了輸出模式輸出模式下引腳不讀外部電平中斷自然永遠不觸發(fā)。如果靜態(tài)檢查沒問題打開調(diào)試器看寄存器實時值。程序全速跑在中斷服務(wù)函數(shù)第一行斷點按一下按鍵如果停不下來看EXTI_PR寄存器按鍵按下后如果它變成1說明外部信號已經(jīng)到達EXTI問題出在NVIC或者函數(shù)名上如果它一直是0問題出在GPIO配置或者外部電路上。這一招能把排查范圍直接砍掉一半。5.2 觸發(fā)一次以后再也沒動靜和“完全不動”不同能進一次說明大鏈路是通的后面不再動常見原因有五個。第一個也是最常見的清PR時用了寫0操作其實標志一直沒被清掉程序可能已經(jīng)卡在中斷里或者邏輯異常。第二個按鍵按下時電平確實變化了但松開時又產(chǎn)生了一個反方向的沿而你可能兩個沿都開著了進了兩次之后狀態(tài)又翻轉(zhuǎn)回去看起來像“再也沒反應(yīng)”。第三個EXTI線被其他程序段重新改成了別的引腳AFIO配置被覆蓋。第四個老代碼里把GPIO重新配置成了復(fù)用功能導(dǎo)致引腳不再是一根普通輸入腳。第五個使用中斷函數(shù)的同時代碼里又定義了一個同名的中斷處理函數(shù)在鏈接時把真正要用的覆蓋了。排查手段還是看PR但這次要分兩個時間點按下時看PR松開時再看PR。如果松開時PR也置1了那就是上升沿也被觸發(fā)了去RTSR里把那一位清掉就行。5.3 一次按鍵觸發(fā)了兩次甚至三四次按鍵機械抖動是首要嫌疑。按鍵按下的瞬間金屬彈片會連續(xù)接觸斷開幾次時間短則幾毫秒長則幾十毫秒也就是說你按下一次邊沿檢測可能看到好幾個有效沿。硬件上可以給按鍵并聯(lián)一個RC低通濾波器或者直接加一個施密特觸發(fā)器芯片但最簡單可靠的辦法是軟件消抖。我不推薦在中斷服務(wù)函數(shù)里Delay因為延時會讓這條中斷的優(yōu)先級變得形同虛設(shè)其他緊急事情全部被堵住。推薦做法是中斷里只置一個標志位主循環(huán)檢測到標志后用狀態(tài)計數(shù)消抖。比如每次掃描都讀按鍵電平如果連續(xù)讀到同一個電平均多少次以上才認為狀態(tài)確認為改變了。這個做法不會浪費CPU響應(yīng)也足夠快。uint8_t btn_count 0; uint8_t btn_state 1; void KeyScan(void) { uint8_t level ((GPIOA-IDR (1 0)) 0) ? 1 : 0; if (level btn_state) { btn_count 0; } else { btn_count; if (btn_count 5) { btn_state level; btn_count 0; } } }這段代碼在主循環(huán)里反復(fù)執(zhí)行l(wèi)evle為1表示按下狀態(tài)連續(xù)5次都讀到1才確認按鍵真的按下。這個方案比在中斷里Delay要健康得多原因是主循環(huán)哪怕稍微慢了一點也不會阻塞其他中斷的及時響應(yīng)。5.4 中斷服務(wù)函數(shù)里干了太多活系統(tǒng)卡頓這是新手最容易又最隱蔽的坑。外部中斷觸發(fā)了你想在中斷里打印日志、刷新屏幕、寫Flash然后把整個邏輯都放進ISR。中斷服務(wù)函數(shù)執(zhí)行期間CPU處于特權(quán)狀態(tài)大部分其他中斷都在等它如果ISR執(zhí)行了10毫秒這10毫秒內(nèi)連系統(tǒng)滴答定時器都得不到及時處理表現(xiàn)出來的就是系統(tǒng)卡頓、界面停頓。正確的做法是讓ISR短小精悍先判斷掛起位清掛起置一個全局標志或者讀取需要的狀態(tài)然后立即退出。真正耗時的工作放到主循環(huán)里處理。ISR里只做“貼封條”的動作別在里面搞“施工”。如果確實需要快速響應(yīng)外部信號并保存波形數(shù)據(jù)那也要保證每個操作都是必要的不能出現(xiàn)打印這種慢速外設(shè)操作。5.5 讀寄存器發(fā)現(xiàn)寫的值和讀出來的不一樣寫寄存器后回讀不對這種事我遇到過好幾次大部分時候不是芯片壞了。最常見的原因是外設(shè)時鐘沒開。你在RCC里沒打開GPIO時鐘往GPIO寄存器寫值寫操作就像打到了空氣墻上硬件外設(shè)根本沒有工作時鐘寄存器不狀態(tài)。調(diào)試器的外設(shè)窗口還會顯示你寫入的值因為調(diào)試接口模擬了總線寫入但實際內(nèi)部電路沒跑起來。所以一開始我總會強調(diào)開時鐘永遠是第一步。另一個常見原因是讀改寫之間的殘留。比如你想把PA0模式改成輸入只在CRL寄存器用了按位或而沒有先清除原有四位的值這就會和之前的配置疊加出奇怪的模式。正確做法是先與一個反掩碼清零再或上目標值。代碼習慣上我把這兩句寫在一起寧可多花一遍讀寫也不要讓狀態(tài)殘留。6. 調(diào)試驗證與進階思路6.1 最實用的五步調(diào)試法我把外部中斷的調(diào)試總結(jié)成五步每一步都在回答“信號走到了哪”。第一步在中斷服務(wù)函數(shù)第一行打斷點全速運行按按鍵看能不能停住。第二步如果停不住看EXTI_PR有沒有因為按鍵而變成1以此判斷外部信號是否進入了EXTI。第三步如果EXTI_PR是1但仍沒進中斷去查NVIC的使能位和掛起狀態(tài)看中斷請求是否到達了NVIC。第四步如果NVIC里一切都對查中斷服務(wù)函數(shù)名和向量表是否匹配。第五步如果中斷函數(shù)確實進去了但功能沒實現(xiàn)再查ISR內(nèi)部的清標志和執(zhí)行邏輯。這套思路最大的好處是每一步都對應(yīng)一個獨立的硬件模塊不會出現(xiàn)“改一下AFIO、改一下NVIC、再改一下GPIO”這種亂槍打鳥式的操作。我調(diào)試寄存器外設(shè)時基本都是這么一層一層剝開的。6.2 不同型號的寄存器差異要注意上面所有代碼基于STM32F1系列。如果你用的是其他系列的芯片寄存器名字和結(jié)構(gòu)會有變化。F4系列里AFIO這個模塊被SYSCFG替代引腳的EXTI選擇寄存器不叫AFIO_EXTICR而是放在SYSCFG里名字也變成了SYSCFG_EXTICR寫代碼時如果照著F1的寄存器名找編譯都過不了。EXTI本身的功能大同小異但IMR、EMR、RTSR、FTSR這些寄存器的偏移和位數(shù)也可能在不同系列里有細微差別。所以我強調(diào)一個習慣拿到新板子先打開對應(yīng)型號的參考手冊找到EXTI章節(jié)對照寄存器地址和位定義寫代碼不要直接抄其他型號的程序。固件庫和HAL庫里的寄存器結(jié)構(gòu)體定義已經(jīng)幫你把地址算好了但不代表寄存器含義也一定一樣查手冊永遠是最終答案。6.3 三個可以繼續(xù)練下去的小實驗基礎(chǔ)按鍵中斷跑通之后建議做三個擴展實驗把這塊真正吃透。第一個實驗把按鍵從PA0挪到PB0只改AFIO_EXTICR的配置體會端口不同但EXTI線編號不變的邏輯。第二個實驗試著同時使用兩個不同的EXTI線比如PA0和PC13分別進不同的中斷服務(wù)函數(shù)看看NVIC的兩個使能位怎么獨立工作。第三個實驗再看一眼EXTI_EMR把它使能后觸發(fā)事件配合ADC采樣或者定時器輸入捕獲體會一下“事件”和“中斷”的區(qū)別到底在哪。實驗做到第三步你會對“外部中斷”這個詞有一個完全不一樣的理解它不只是讓CPU停下工作去處理按鍵更是一套通用的信號分發(fā)機制可以把外部脈沖送給不同的外設(shè)而不一定非要CPU介入。這種理解一旦建立后面學串口空閑中斷、定時器捕獲比較中斷都能輕松遷移。最后說點我在實際調(diào)試里的體會。外部中斷這類東西十個新手九個栽在清標志上還有一個栽在AFIO時鐘上。遇到怪現(xiàn)象別急著改代碼先開調(diào)試器的外設(shè)寄存器窗口把EXTI-PR盯住按一下按鍵看看它有沒有變化。這一位變了問題就在后面的NVIC或中斷函數(shù)這一位不變問題就在前面的GPIO或電路。這個思路幫我在好幾個項目里省了大把時間希望對你也有用。