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