試踩坑指南:從No target connected到串口亂碼的排查方法)
做STM32開發(fā)這些年我見過太多人卡在同一個地方程序?qū)懞昧司幾g也過了一點(diǎn)下載——No target connected。然后開始懷疑人生板子壞了芯片燒了線沒接對其實(shí)大概率都不是。這種經(jīng)歷我太熟了所以這篇文章不打算講STM32基礎(chǔ)也不寫高深算法就專門聊聊STM32開發(fā)調(diào)試過程中那些真正浪費(fèi)過時間的坑以及我是怎么一步步排查出來的。不管你是正在做畢業(yè)設(shè)計(jì)的學(xué)生、剛轉(zhuǎn)嵌入式的新人還是寫了幾年程序的工程師在調(diào)試這條路上總有那么幾個坑能讓你對號入座。我盡量把每個坑的根因、排查鏈路和解決辦法都寫得具體一點(diǎn)方便你直接對照自己的項(xiàng)目。畢竟調(diào)試這種事最怕的不是問題難而是連從哪兒下手都不知道。1. 環(huán)境搭建與工程樣板先解決“能不能編譯、能不能下載”1.1 Keil5裝完卻找不到STM32芯片包很多新手第一次裝Keil5裝完興沖沖打開結(jié)果在Device選擇列表里翻半天都找不到STM32。不是軟件有問題而是Keil MDK從V5版本開始把芯片支持從安裝包里拆出來了變成了獨(dú)立的Pack包。MDK安裝包本身默認(rèn)只帶ARM通用編譯器具體芯片的器件支持需要額外安裝。解決辦法是打開Pack Installer找到STM32F1系列的DFP包如Keil.STM32F1xx_DFP安裝。但這里有個很現(xiàn)實(shí)的坑Pack Installer需要聯(lián)網(wǎng)下載國內(nèi)網(wǎng)絡(luò)狀況下經(jīng)??ㄔ谙螺d進(jìn)度條不動或者下載一半失敗。我遇到過好幾次Pack下載失敗后還殘留了部分文件導(dǎo)致重新安裝時報(bào)錯。如果你也遇到這種情況直接去Keil官網(wǎng)下載對應(yīng)DFP安裝包手動雙擊安裝比Pack Installer里干等靠譜得多。還有一個小細(xì)節(jié)安裝DFP時最好先關(guān)掉Keil工程否則可能出現(xiàn)安裝成功但工程里仍然顯示不了芯片的情況需要重啟工程。另一個很常見的坑是C51和STM32共存的問題。Keil的C51版本和MDK版本其實(shí)是兩個獨(dú)立產(chǎn)品一個管8051一個管ARM。如果你電腦上先裝了C51的uVision4后來又裝了MDK的uVision5或者反過來很容易出現(xiàn)我裝了Keil但打開工程提示Device不存在的情況。因?yàn)閮蓚€IDE雖然看起來差不多但支持的芯片列表完全不同。我的做法是C51和MDK都裝安裝路徑分開圖標(biāo)雖然很像但打開前看清楚是哪個版本。如果你手頭有51的工程和STM32的工程要輪流折騰千萬別圖省事只裝一個那才是真正的折磨。1.2 STM32標(biāo)準(zhǔn)庫新建工程的三個隱藏雷雖然現(xiàn)在新項(xiàng)目很多人直接用STM32CubeMX加HAL庫但標(biāo)準(zhǔn)庫依然有大量存量代碼和教材尤其是做畢業(yè)設(shè)計(jì)和老項(xiàng)目維護(hù)的場景。新建標(biāo)準(zhǔn)庫工程時我踩過的雷可以列出三個第一個是啟動文件選錯。STM32F1系列啟動文件分了好幾種startup_stm32f10x_ld.s、md.s、hd.s分別對應(yīng)低容量、中容量、高容量。很多教程里默認(rèn)用hd.s如果你手里的芯片是F103C8T6這種中容量的用hd.s也不是不能啟動但如果你選的是F103RCT6高容量芯片卻用了md.s程序能燒進(jìn)去運(yùn)行卻會莫名其妙出問題。因?yàn)閱游募锒x了堆棧大小和中斷向量表的位置容量不匹配會導(dǎo)致內(nèi)存映射錯位。第二個是宏定義沒加。標(biāo)準(zhǔn)庫工程必須在C/C的Define里加上USE_STDPERIPH_DEVICE以及STM32F10X_HD或者STM32F10X_MD取決于你的芯片容量。很多教程建工程時根本不提這個導(dǎo)致編譯報(bào)一堆未定義錯誤。我當(dāng)年第一次建工程編譯報(bào)錯幾十條還以為是庫文件沒拷全其實(shí)是宏定義漏了。第三個是頭文件路徑?jīng)]有包含全。標(biāo)準(zhǔn)庫的源碼按文件夾組織你需要把Misc、GPIO、RCC、USART等用到的外設(shè)頭文件路徑都加進(jìn)Include Paths。漏一個就編譯報(bào)無法打開文件。建議直接把整個Libraries文件夾路徑加進(jìn)去省得來回改。這其實(shí)是Keil工程配置里的基本功但新人在這一步卡一天很正常。我?guī)У膸讉€實(shí)習(xí)生幾乎每一個都在新建工程上耗過半天一旦跑通一次后面就順了。1.3 標(biāo)準(zhǔn)庫和HAL庫到底怎么選關(guān)于選庫這件事屬于每次群里都有人問的問題。很多人糾結(jié)標(biāo)準(zhǔn)庫字符級操作直接、好多教程都用標(biāo)準(zhǔn)庫HAL庫配合CubeMX生成代碼效率高但代碼量太大。我的看法是標(biāo)準(zhǔn)庫封裝層次低離寄存器近適合學(xué)習(xí)原理、調(diào)試底層問題、做資源緊張的簡單項(xiàng)目。ST官方已經(jīng)停止維護(hù)標(biāo)準(zhǔn)庫新出的芯片也沒標(biāo)準(zhǔn)庫可用但F1、F4系列老片子用戶量太大網(wǎng)上資料幾乎取之不盡。HAL庫抽象層更高配合STM32CubeMX可以快速生成初始化代碼適合快速原型、復(fù)雜外設(shè)協(xié)同、多項(xiàng)目復(fù)用。但Debug時函數(shù)調(diào)用層級深出問題了追蹤起來費(fèi)勁。HAL庫本身也有一些黑魔法比如狀態(tài)機(jī)管理、超時機(jī)制你要是沒吃透它的設(shè)計(jì)思路初次上手容易覺得怎么這么繞。LL庫介于兩者之間更接近寄存器操作但用的人相對少。如果你的目標(biāo)是搞懂原理或者項(xiàng)目要求極致的執(zhí)行效率走標(biāo)準(zhǔn)庫沒問題如果目標(biāo)是快速出活、接新項(xiàng)目、用新芯片直接學(xué)HALCubeMX更劃算。最怕的是學(xué)到一半想換代碼寫到一半從標(biāo)準(zhǔn)庫遷HAL庫那種痛我真的不建議你體驗(yàn)。順帶提一下VSCode配置STM32的問題。最近問的人確實(shí)多用EIDE插件或者CMake加ARM GCC工具鏈確實(shí)能擺脫Keil的界面。但我的建議是如果你還在調(diào)試階段頻繁踩坑先把Keil用好再考慮VSCode。VSCode編輯代碼舒服但調(diào)試器配置、燒錄器配置、cmsis-dap或OpenOCD的target配置任何一個沒配對都能讓你折騰一晚上。工具是為主業(yè)服務(wù)的不是用來折騰的。2. 下載器連接失敗從No target connected說起2.1 插上ST-Link電腦卻提示無法識別USB設(shè)備先說一個最算不上故障的故障USB設(shè)備無法識別。排查順序很重要我一般是這樣的換一根USB線?,F(xiàn)在很多充電線只有電源線沒有數(shù)據(jù)線插上去能供電但設(shè)備管理器毫無反應(yīng)。這種偽故障占比非常高尤其是你隨手從抽屜里摸出來一根線的時候。換一個USB口。前置面板的USB口供電和信號質(zhì)量經(jīng)常不如主板后置口尤其是接ST-Link這種需要穩(wěn)定供電的設(shè)備。檢查設(shè)備管理器。如果看到未知設(shè)備或者帶感嘆號的設(shè)備嘗試右鍵更新驅(qū)動。ST-Link的驅(qū)動有專門的安裝包老版本W(wǎng)indows下經(jīng)常需要手動裝一次。確認(rèn)ST-Link固件版本。太舊的ST-Link固件在某些機(jī)器上會識別異常用STM32CubeProgrammer或者官方ST-Link Upgrade工具升級一下固件。如果設(shè)備管理器里能看到ST-Link設(shè)備但連接目標(biāo)板時報(bào)錯那問題就在目標(biāo)板這邊了。常見原因包括目標(biāo)板沒有獨(dú)立供電ST-Link的3.3V輸出能力有限、SWD接線過長信號不穩(wěn)定、目標(biāo)板復(fù)位電路異常。這個時候先把SWD線盡量縮短把下載速率調(diào)低往往就能解決問題。下載速率太高的坑特別容易忽略ST-Link默認(rèn)可能跑1.8MHz飛線搭的板子信號質(zhì)量差降到500kHz甚至100kHz就穩(wěn)定了。2.2 第一次下載成功第二次就No target connected這個坑我印象太深了因?yàn)樗l(fā)生的時間點(diǎn)永遠(yuǎn)是你覺得穩(wěn)了的時候。說的就是程序第一次下載成功板子也跑起來了然后你改了幾行代碼想重新下載點(diǎn)擊下載卻直接報(bào)No target connected。根因大概率不是你板子壞了而是你的程序里把SWD引腳給禁用了。STM32的SWD和JTAG共用一組引腳如果你在初始化代碼里調(diào)用了類似GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)這樣的函數(shù)調(diào)試口會被釋放成普通GPIO。程序一旦跑起來SWDIO和SWCLK兩個引腳就不聽調(diào)試器指揮了下一次下載當(dāng)然連不上。這種坑最惡心的地方在于它很隱蔽。你只是按照某個例程加了一句禁用JTAG防止干擾的代碼結(jié)果把下載口也禁了。排查的方法是按住目標(biāo)板的復(fù)位鍵不松手然后點(diǎn)下載在程序執(zhí)行到初始化之前把連接搶下來或者把BOOT0拉高進(jìn)ISP模式用串口全片擦除芯片內(nèi)的程序。如果是已經(jīng)禁用了調(diào)試口的固件最穩(wěn)妥的辦法就是進(jìn)ISP擦除。后來我給自己定了一條規(guī)矩開發(fā)調(diào)試階段絕不輕易在代碼里禁用SWD/JTAG功能。即使量產(chǎn)階段為了省電和IO資源需要禁用也一定保留一段可以通過跳線或者特定操作恢復(fù)的方案否則后續(xù)升級固件就成了噩夢。2.3 ST-Link Utility和STM32CubeProgrammer的救援價(jià)值很多人不知道下載器除了燒錄程序還能用來救命。ST官方早年的ST-Link Utility現(xiàn)在已經(jīng)升級為STM32CubeProgrammer可以繞過正常下載流程直接操作芯片內(nèi)部Flash。最常見的救援場景是芯片讀寫保護(hù)打開后無法連接。有些芯片在量產(chǎn)階段被設(shè)置了讀保護(hù)RDP等級1此時調(diào)試器默認(rèn)無法讀取Flash。你用ST-Link連接時會報(bào)錯但用STM32CubeProgrammer執(zhí)行一次Full chip erase全片擦除會把保護(hù)位一起清掉芯片就能重新下載了。注意全片擦除會清掉所有代碼和配置操作前確認(rèn)數(shù)據(jù)已備份。STM32CubeProgrammer比ST-Link Utility更值得常用它支持多種連接方式ST-Link、UART串口ISP、USB DFU。當(dāng)SWD口被意外禁用時你還有一條逃生通道——UART ISP。只要BOOT0拉高、BOOT1拉低芯片上電進(jìn)入系統(tǒng)存儲器里的Bootloader用串口就能擦除和重刷Flash。這個方法我在折騰自制最小系統(tǒng)板時救過好幾次命。還有一個小技巧STM32CubeProgrammer可以讀取和修改Option Bytes。如果你需要調(diào)整讀保護(hù)等級、看門狗模式或者BOOT配置不用額外寫代碼直接在這個軟件里操作就行。調(diào)試階段建議大家把讀保護(hù)關(guān)了否則每次下載前都要先擦除麻煩得很。3. 時鐘、延時與啟動程序跑飛前最隱蔽的三個坑3.1 時鐘樹配置不對串口亂碼和延時失準(zhǔn)的共同源頭STM32的時鐘系統(tǒng)比51單片機(jī)復(fù)雜得多這也是很多人從51轉(zhuǎn)過來時最不適應(yīng)的點(diǎn)。51單片機(jī)接個晶振就能跑STM32內(nèi)部有HSI內(nèi)部高速時鐘、HSE外部高速晶振、PLL倍頻、總線分頻這一整套時鐘樹。調(diào)試經(jīng)驗(yàn)里凡是程序能跑但串口亂碼延時時間完全不對USB枚舉失敗這些癥狀十有八九和時鐘配置有關(guān)。最經(jīng)典的場景你的開發(fā)板上有一個8MHz外部晶振代碼里SystemInit默認(rèn)使用HSE作為系統(tǒng)時鐘源。但如果你的板子沒有焊外部晶振或者晶振虛焊HSE啟動會失敗。STM32F1的啟動邏輯在HSE失敗后會自動退回HSI運(yùn)行程序不會死但系統(tǒng)時鐘從72MHz掉到了內(nèi)部8MHz所有依賴時間的計(jì)算全崩了——串口波特率不對、定時器溢出時間變了、delay函數(shù)的時間尺度也變了。表現(xiàn)出來就是板子好像所有功能都不太對勁。排查手段其實(shí)很直接。先用萬用表量晶振兩腳波形或者用示波器看有沒有起振再進(jìn)調(diào)試器看RCC-CR寄存器的HSERDY位是否為1如果HSEON為1但HSERDY一直為0就是外部晶振電路沒起來。很多人會忽略電路本身的問題晶振需要兩個負(fù)載電容如果板子上沒焊或者盡最大努力只是看起來焊了實(shí)際上虛焊也會導(dǎo)致不起振。還有一種情況是代碼里明明寫了外部8M晶振但實(shí)際板子上裝的是12M晶振——頻率不對串口亂碼、定時器時間整體偏差。有一個經(jīng)驗(yàn)如果你用的是內(nèi)部HSI時源USB外設(shè)大概率無法正常工作因?yàn)閁SB要求48MHz的精確時鐘內(nèi)部RC振蕩器精度不夠。很多基于F103的USB虛擬串口項(xiàng)目調(diào)試不通過最后都栽在時鐘源上。所以不是能用內(nèi)部時鐘就省事得先看清外設(shè)的時鐘需求。3.2 延時函數(shù)卡死SysTick被誰搶了延時卡死可以說是嵌入式開發(fā)里的固有節(jié)目。明明代碼里寫了一個Delay(1000)結(jié)果程序執(zhí)行到這就再也不走了。這類問題我從標(biāo)準(zhǔn)庫轉(zhuǎn)到HAL庫之后又遇到過一次根因還不完全一樣。標(biāo)準(zhǔn)庫下常見的Delay實(shí)現(xiàn)是基于SysTick定時器中斷實(shí)現(xiàn)??ㄋ赖脑蛴袔讉€某段代碼把全局中斷關(guān)了SysTick中斷一直在Pending狀態(tài)無法執(zhí)行計(jì)數(shù)器永遠(yuǎn)不會更新Delay就永遠(yuǎn)退不出來。你的工程里其他地方重寫了SysTick_Handler標(biāo)準(zhǔn)庫的延時計(jì)數(shù)邏輯被覆蓋了。比如有些人從網(wǎng)上拷貝一段代碼里面自己寫了一個SysTick_Handler結(jié)果把系統(tǒng)那個頂替了。優(yōu)化等級太高編譯優(yōu)化把循環(huán)判斷條件里的變量給優(yōu)化掉了導(dǎo)致for循環(huán)延時永久執(zhí)行。排查方法也不復(fù)雜在Delay調(diào)用前點(diǎn)亮一個LED調(diào)用后再熄滅看LED行為進(jìn)調(diào)試器查看SysTick的CTRL寄存器和Current Value寄存器確認(rèn)SysTick是否在跑如果懷疑優(yōu)化等級把優(yōu)化等級臨時改成-O0試一下。如果問題消失就是優(yōu)化引起的你可以用volatile修飾循環(huán)變量或者改用官方的SysTick延時邏輯。HAL庫的HAL_Delay卡死也有一個隱蔽原因HAL_Delay依賴一個全局變量uwTick而這個變量是在SysTick_Handler里調(diào)用HAL_IncTick遞增的。如果你用標(biāo)準(zhǔn)庫的習(xí)慣寫代碼或者移植項(xiàng)目時忘了調(diào)用HAL_InitSysTick中斷沒有初始化HAL_Delay就會永遠(yuǎn)等下去。還有人在自己的中斷服務(wù)程序里長時間阻塞SysTick中斷一直沒有機(jī)會執(zhí)行HAL_Delay也會表現(xiàn)異常。3.3 HardFault_Handler數(shù)組越界與棧溢出的表象程序跑著跑著突然進(jìn)入HardFault_Handler死循環(huán)這是嵌入式開發(fā)的經(jīng)典夢魘。很多人一進(jìn)HardFault就慌其實(shí)它本質(zhì)上是CPU遇到不可恢復(fù)的錯誤后跳轉(zhuǎn)到異常向量大多數(shù)情況下不是芯片壞了而是你的代碼觸發(fā)了非法操作。最常見的幾個觸發(fā)原因數(shù)組越界寫入尤其是DMA數(shù)據(jù)傳輸緩沖區(qū)定義小了數(shù)據(jù)寫入越界。棧溢出在中斷里定義了超大局部數(shù)組或者遞歸調(diào)用過深把棧撐爆了。操作了未使能時鐘的外設(shè)寄存器。比如你忘了開啟GPIOA的時鐘卻直接往GPIOA的ODR寄存器寫數(shù)據(jù)。在F1系列上這會導(dǎo)致總線錯誤最終進(jìn)HardFault。函數(shù)指針為空、未初始化就被調(diào)用。排查HardFault的關(guān)鍵是定位觸發(fā)點(diǎn)。在Keil里當(dāng)程序停在HardFault_Handler時打開Call Stack窗口看調(diào)用棧通常能看到觸發(fā)異常前的最后幾層調(diào)用。再用Peripherals窗口查看SCB-CFSR寄存器的Fault狀態(tài)位能分辨是總線錯誤還是用法錯誤還是斷言失敗。最實(shí)用的辦法是記錄下R14(LR)和R15(PC)的值在反匯編窗口輸入地址看當(dāng)前執(zhí)行到了哪條指令再對應(yīng)到源碼里的具體行。我調(diào)過最長的一個HardFault案例是串口空閑中斷加DMA接收不定長數(shù)據(jù)時接收長度判斷邏輯出了錯DMA把數(shù)據(jù)寫到了數(shù)組末尾之外。癥狀是程序運(yùn)行一段時間后隨機(jī)死機(jī)一度懷疑是硬件問題最后在Fault Reports里發(fā)現(xiàn)是總線錯誤再查DMA緩沖區(qū)地址才鎖定了越界。從那以后凡是用DMA的地方我都強(qiáng)制在緩沖數(shù)組尾部多留幾個字節(jié)的冗余空間不為別的就是為了排查時少掉幾根頭發(fā)。3.4 啟動文件與宏定義選錯的連鎖反應(yīng)之前講了建工程時的啟動文件選擇但啟動文件的問題不止是編譯期影響。有幾次程序編譯下載都成功了上電就是跑不起來最后發(fā)現(xiàn)是啟動文件里的堆棧尺寸設(shè)置太小。啟動文件頂部有一段Stack和Heap的配置比如Stack_Size EQU 0x400如果你在代碼里大量使用了局部數(shù)組、嵌入式實(shí)時操作系統(tǒng)、或者malloc動態(tài)內(nèi)存默認(rèn)的棧空間可能不夠。棧溢出不一定會立刻死機(jī)而是在某個深層調(diào)用或者中斷嵌套時才崩。這種問題極其難查因?yàn)橐?guī)律不明顯可能運(yùn)行幾十秒才出一次問題。我的習(xí)慣是調(diào)試階段就把棧改成0x800或者更大省得被棧溢出干擾判斷。另外還有一個容易被忽略的坑啟動文件里定義了中斷向量表如果你在工程里另外寫了一個包含相同中斷向量的文件可能編譯報(bào)重復(fù)定義——這是好情況。壞情況是鏈接器選擇了你后加的那個向量表導(dǎo)致定時器或其他外設(shè)的中斷根本進(jìn)不去程序看起來一切正常但功能不響應(yīng)。所以新建工程時不要貪多啟動文件、系統(tǒng)時鐘文件、標(biāo)準(zhǔn)庫外設(shè)文件凡是從網(wǎng)上拷來的大雜燴工程模板都值得你花時間拆開看一遍確認(rèn)每一部分的作用再拼裝。用別人整合好的一鍵工程出了問題你真的不知道怎么排查因?yàn)槟悴磺宄锩嬗心男╇[藏配置。4. 串口通信與外設(shè)調(diào)試從亂碼到數(shù)據(jù)丟失4.1 串口亂碼的排查鏈路串口通信是STM32項(xiàng)目里最常用的外設(shè)基本每個項(xiàng)目都用但亂碼問題也最多。我總結(jié)了一條排查鏈路你按順序走一遍絕大多數(shù)亂碼都能解決第一步排除接線問題。TX接RX、RX接TX這兩個腳位接錯是最常見的。如果你用的是USB轉(zhuǎn)串口模塊還要確認(rèn)模塊電平是否兼容——3.3V和5V混接可能導(dǎo)致通信不穩(wěn)定甚至燒壞引腳。第二步確認(rèn)上位機(jī)波特率和代碼一致??雌饋硎菑U話但真的很多人把代碼里的9600和工具里的115200搞混或者代碼里改了波特率工具沒改。最簡單的方式是用串口助手自發(fā)自收也就是把USB轉(zhuǎn)串口的TX和RX短接在電腦上發(fā)什么收什么。自發(fā)自收正常說明USB轉(zhuǎn)串口沒問題問題在STM32端。第三步用示波器量STM32的TX引腳。波特率是否正確可以直接測量一個字節(jié)的位寬來判斷。比如9600波特率一位的時間是104微秒左右示波器上能看到脈沖寬度明顯不對就是波特率的計(jì)算結(jié)果錯了。第四步回到時鐘樹問題。之前說過外部晶振沒起振或頻率不一致會導(dǎo)致PCLK計(jì)算錯誤波特率必然不對。F1系列的USART波特率發(fā)生器是從PCLK分頻來的系統(tǒng)時鐘是72M還是8M同一個波特率寄存器數(shù)值對應(yīng)的實(shí)際波特率差了9倍。如果你的串口在低速率下看著正常高速率全亂大概率還是時鐘問題不是串口代碼本身的問題。通信波特率不對還有一個非常容易忽略的情形外部晶振虛焊導(dǎo)致HSE啟動失敗但芯片沒死退回HSI后程序繼續(xù)跑串口打印出來的內(nèi)容偶爾正常偶爾亂碼會誤導(dǎo)你以為是串口代碼有BUG。這時候不要反復(fù)改串口參數(shù)回頭檢查外部晶振。4.2 HAL庫串口中斷發(fā)送與接收的坑HAL庫是很多人從標(biāo)準(zhǔn)庫轉(zhuǎn)過來的第一站但它的串口接口和標(biāo)準(zhǔn)庫用法差異很大踩坑點(diǎn)也完全不同。先說HAL_UART_Transmit_IT。這個函數(shù)是異步發(fā)送調(diào)用后立即返回實(shí)際數(shù)據(jù)通過中斷逐字節(jié)發(fā)出去。問題在于如果你上一次發(fā)送還沒結(jié)束就再次調(diào)用函數(shù)會返回HAL_BUSY數(shù)據(jù)直接丟掉。很多人寫一個while循環(huán)里調(diào)用發(fā)送結(jié)果數(shù)據(jù)丟得一塌糊涂因?yàn)榘l(fā)送速度跟不上調(diào)用速度。解決辦法是維護(hù)一個發(fā)送完成標(biāo)志位在HAL_UART_TxCpltCallback回調(diào)里置0調(diào)用前先判斷標(biāo)志位。或者直接用阻塞式的HAL_UART_Transmit省心但會占用CPU。HAL_UART_Receive_IT也有類似問題它是一次性接收——接收完指定字節(jié)數(shù)后觸發(fā)回調(diào)之后就不會再接收了需要在回調(diào)里重新調(diào)用一次才能繼續(xù)收。很多人不知道這個機(jī)制以為調(diào)用一次就能永遠(yuǎn)收結(jié)果發(fā)現(xiàn)只收到一次數(shù)據(jù)就再也不進(jìn)了。不定長串口數(shù)據(jù)接收我推薦的做法是空閑中斷加DMA。配置串口的DMA接收為循環(huán)模式數(shù)據(jù)到了自動進(jìn)緩沖區(qū)然后在串口空閑中斷里讀取DMA剩余計(jì)數(shù)計(jì)算出本次收到的數(shù)據(jù)長度。這個方法能處理不定長數(shù)據(jù)包不丟字節(jié)CPU占用也低。但有幾個坑要避開串口空閑中斷和DMA中斷的優(yōu)先級要合理安排而且要在中斷里快速讀出長度再去處理數(shù)據(jù)不能讓中斷處理太久。DMA循環(huán)模式的數(shù)據(jù)是不斷覆蓋寫的你必須在空閑中斷里拷貝出當(dāng)前這批數(shù)據(jù)到應(yīng)用緩沖區(qū)否則下一次數(shù)據(jù)來了會覆蓋?;卣{(diào)函數(shù)里訪問緩沖區(qū)時務(wù)必注意邊界別把固定數(shù)組寫爆了。4.3 USB虛擬串口設(shè)備管理器里那個感嘆號STM32F103的USB虛擬串口項(xiàng)目很多人按照例程做完插上USB線電腦卻提示未知設(shè)備或者設(shè)備管理器里一個黃色感嘆號。這類問題有固定的排查方向第一個方向是時鐘。F103的USB外設(shè)需要48MHz時鐘如果系統(tǒng)時鐘不是由8MHz晶振經(jīng)過PLL倍頻到72MHz再二分頻得到48MHzUSB的枚舉過程就會失敗。如果你用的是內(nèi)部HSI或者外部晶振不是8MHzUSB極大概率無法工作。第二個方向是硬件上拉。USB設(shè)備通過DP引腳的1.5k上拉電阻告知主機(jī)這是一個全速設(shè)備。很多STM32開發(fā)板把DP上拉做到芯片內(nèi)部由固件控制PA12實(shí)現(xiàn)也有的板子需要外接上拉電阻。如果你用的板子需要外接但沒焊這個電阻自然枚舉不了??丛韴D時專門找一下DP引腳附近有沒有上拉電阻。第三個方向是堆棧和中斷配置。USB庫占用的RAM不少如果你的啟動文件堆棧設(shè)得太小枚舉過程可能跑飛。USB相關(guān)中斷優(yōu)先級也要正確配置不能把其他中斷長時間阻塞USB中斷。還有一個常被忽略的細(xì)節(jié)USB虛擬串口發(fā)送數(shù)據(jù)不能在USB的中斷回調(diào)里直接調(diào)用發(fā)送接口否則可能造成死鎖或者丟數(shù)據(jù)。正確做法是把要發(fā)的數(shù)據(jù)存到循環(huán)緩沖區(qū)在應(yīng)用層空閑時批量發(fā)送。4.4 定時器捕獲測頻與編碼器模式的小細(xì)節(jié)定時器和編碼器是電機(jī)控制、測速項(xiàng)目里的常規(guī)外設(shè)但很多人第一次用輸入捕獲測頻率時得到的第一個值總是錯的。原因是輸入捕獲的第一次捕獲往往發(fā)生在定時器計(jì)數(shù)器的任意位置兩次捕獲之間的差值并不能正確反映一個完整周期。正確的做法是第一次捕獲值先丟棄從第二次捕獲開始計(jì)算周期或者連續(xù)捕獲多次求平均。另外如果信號頻率很低捕獲周期大于定時器的溢出周期你必須處理更新中斷把溢出次數(shù)也一起計(jì)進(jìn)去否則測出來的頻率會突然跳變。編碼器模式也一樣配置成編碼器接口后定時器計(jì)數(shù)值隨編碼器旋轉(zhuǎn)方向自動增減。常見的坑是有人把編碼器A相接到定時器通道1、B相接通道2但代碼里配置成了只計(jì)數(shù)單通道結(jié)果方向無法判斷。還有Z相清零的問題如果你需要絕對位置參考得把Z相信號接到另一個引腳并在捕獲到Z相時清零計(jì)數(shù)否則位置數(shù)據(jù)會一直累加漂移。超聲波測距模塊也是熱門畢設(shè)題材。用定時器輸入捕獲來測量回波脈寬比用GPIO輪詢要靠譜得多。GPIO輪詢在近距離時容易因?yàn)榇a執(zhí)行時延誤差導(dǎo)致測量結(jié)果不準(zhǔn)而輸入捕獲是硬件記錄邊沿時刻精度明顯更高。不過要注意把GPIO配置成正確的輸入模式數(shù)字輸入和模擬輸入的引腳行為差異很大配置錯了回波信號根本讀不到。5. 疑難雜癥的排查思路把經(jīng)驗(yàn)變成方法論5.1 三板斧先復(fù)位、再點(diǎn)燈、后打量調(diào)試經(jīng)驗(yàn)積累到一定程度你會發(fā)現(xiàn)大多數(shù)問題都可以歸納為三板斧排查法。這個方法土但靠譜。第一板斧是硬件確認(rèn)。用萬用表量電壓——3.3V是否到位、GND是否連通、復(fù)位按鍵是否為高電平。很多宣稱程序有問題的情況最后發(fā)現(xiàn)是板子供電接觸不良或者杜邦線松了一根。不要小看這類低級問題他們占的比例一點(diǎn)都不低。第二板斧是軟件最小化。把主循環(huán)里所有業(yè)務(wù)代碼注釋掉只留一個LED翻轉(zhuǎn)或者GPIO翻轉(zhuǎn)看系統(tǒng)還能不能跑。如果帶動業(yè)務(wù)代碼就死機(jī)去掉業(yè)務(wù)代碼就正常說明問題出在業(yè)務(wù)邏輯里跟你懷疑的編譯器、鏈接器、芯片都沒關(guān)系。如果連最小系統(tǒng)都不跑那才輪得到懷疑初始化配置或者硬件。第三板斧是調(diào)試器讀寄存器。進(jìn)調(diào)試器看Peripherals里的GPIO ODR、RCC CR、USART SR這些寄存器能直接判斷外設(shè)是否初始化成功。這個方法比printf高效得多比如一個GPIO輸出沒電平先看GPIO時鐘有沒有開再看引腳配置對不對兩步就能定位。我印象很深的一次排查是超聲波測距模塊始終返回0。主循環(huán)點(diǎn)LED沒問題按理說初始化也沒問題但就是測不到。最后用調(diào)試器看GPIO配置寄存器發(fā)現(xiàn)把ECHO引腳配成了模擬輸入模擬輸入狀態(tài)下數(shù)字電平讀取永遠(yuǎn)返回0。改成浮空輸入后立刻恢復(fù)正常。如果當(dāng)時不打開寄存器窗口光靠猜代碼可能半天都定位不到。5.2 用好調(diào)試器別全靠printf很多初學(xué)者調(diào)試單片機(jī)時習(xí)慣用串口printf這也是江科大這類視頻教程帶出來的慣性。printf沒有原罪但它有兩個明顯的副作用一是在中斷里調(diào)用printf會嚴(yán)重干擾時序。中斷服務(wù)程序里花幾百微秒做字符串格式化輸出對其他中斷響應(yīng)的影響是災(zāi)難性的尤其是有定時器捕獲、USB通信這類對時序敏感的功能時。二是printf本身也可能改變程序行為。有個經(jīng)典現(xiàn)象加上printf就正常去掉printf就死機(jī)這種墨菲定律式問題通常不是printf解決了什么邏輯錯誤而是printf消耗的時間恰好掩蓋了某個時序競爭。遇到這種情況不要覺得是不加上就不行的魔法應(yīng)該警惕背后真實(shí)的競態(tài)條件。我建議的方式是用調(diào)試器打斷點(diǎn)、看變量值、看寄存器這些是第一手段。printf作為第二手段盡量只在非中斷上下文調(diào)用而且打上明顯的調(diào)試開關(guān)產(chǎn)品發(fā)布前統(tǒng)一關(guān)掉。真要在HardFault里打印診斷信息直接把PC和LR寄存器值格式化后發(fā)出來比盲猜強(qiáng)太多。5.3 我的三個真理時刻與給新手的成長路線做嵌入式這些年我印象最深的三個時刻都跟解決問題的方式有關(guān)第一次是當(dāng)我終于靜下心來讀RM0008參考手冊而不是到處搜代碼片段很多之前搞不動的疑惑突然就通了。網(wǎng)上代碼質(zhì)量參差不齊二手信息滿天飛真正一手的正確答案永遠(yuǎn)在芯片手冊里。第二次是發(fā)現(xiàn)自己從開發(fā)板上拿來的出廠例程有問題。例程為了展示方便很多配置并不嚴(yán)謹(jǐn)有的是時鐘配置過度依賴默認(rèn)值有的是外設(shè)初始化順序不對。從那時起我每拿到一個開發(fā)板第一件事是自己重新寫一份最小系統(tǒng)初始化而不是直接依賴?yán)?。第三次是開始用示波器和邏輯分析儀輔助調(diào)試。以前遇到奇怪的問題總是反復(fù)看代碼其實(shí)很多答案在信號層面早就擺在眼前了——晶振起振沒有、串口波形對不對、PWM頻率是不是設(shè)置的值示波器掃一眼全知道。如果你現(xiàn)在剛?cè)肟覵TM32我建議的成長路線很樸素準(zhǔn)備一塊F103C8T6最小系統(tǒng)板加ST-Link有條件再配一個幾十塊錢的邏輯分析儀。從閃燈開始依次走通串口、定時器、中斷、DMA、USB這幾個關(guān)鍵外設(shè)的調(diào)試流程。這個過程中你會把下載器、工程配置、時鐘系統(tǒng)、調(diào)試技巧全都踩一遍等這些基本功扎實(shí)了再去做環(huán)境監(jiān)測、智能臺燈、雙輪小車這些畢業(yè)設(shè)計(jì)或項(xiàng)目就是水到渠成的事。其實(shí)踩坑從來都不是壞事真正浪費(fèi)時間的是重復(fù)踩同一個坑還不總結(jié)。希望這篇總結(jié)能幫你少走幾段彎路把省下來的時間留給真正值得研究的功能和代碼。