試全攻略:從枚舉失敗到定位問題)
第一次調(diào)STM32的USB通信我以為跟調(diào)串口差不多插上線打開串口助手printf打印狀態(tài)。結(jié)果電腦上彈出一個黃色感嘆號設(shè)備管理器里寫著“未知USB設(shè)備設(shè)備描述符請求失敗”我的printf連個影子都沒打出來。那一刻我意識到USB調(diào)試和串口調(diào)試完全是兩種思路。后來我被這個問題折磨了幾天試過各種方法才慢慢總結(jié)出一套適合STM32的USB通信調(diào)試方法。這篇文章不打算講怎么把CubeMX點出USB CDC而是想說清楚當USB不通時你該怎么一步步把它查穿。內(nèi)容包括我自己常用的工具組合、從物理層到應(yīng)用層的排查主線以及幾個高頻故障的完整定位過程希望能幫你少走幾個彎路。1. USB靠printf調(diào)不通問題到底出在哪1.1 主從機制決定了你沒法“想打日志就打日志”串口調(diào)試的邏輯很簡單單片機主動往TX腳扔數(shù)據(jù)電腦那邊串口助手收數(shù)據(jù)流是雙向?qū)ΨQ的哪怕沒人理你日志也能打出來。USB完全不是這個路數(shù)。USB是嚴格的主從協(xié)議主機PC掌握著總線上的一切調(diào)度權(quán)。設(shè)備端不能主動往總線上發(fā)數(shù)據(jù)只能等主機發(fā)來IN令牌后才有機會把一個數(shù)據(jù)包放到總線上。換句話說你的STM32就算滿肚子話想說主機不發(fā)IN令牌你也只能憋著。這對于習慣了用printf看現(xiàn)場的人來說第一反應(yīng)就是“我的程序是不是沒跑起來”。更麻煩的是枚舉過程。USB枚舉是一套固定的狀態(tài)機設(shè)備插入后主機先發(fā)復位信號然后設(shè)備在默認地址0上回應(yīng)GET_DESCRIPTOR接著主機設(shè)置地址、再獲取完整描述符、設(shè)置配置最后才進入數(shù)據(jù)通信階段。任何一步超時、數(shù)據(jù)格式錯誤、校驗失敗整個枚舉直接失敗。設(shè)備管理器里就會出現(xiàn)各種“未知設(shè)備”報錯但這個過程中USB數(shù)據(jù)通道根本就沒建立起來你沒法用USB本身去打印USB調(diào)試信息。這就是USB調(diào)試的第一個反直覺點你需要的第一個日志通道往往不是USB自己而是一條和USB完全無關(guān)的路徑。1.2 先把“printf思維”移植到USB調(diào)試里既然USB通道在枚舉失敗時不可用就要先把日志輸出通道獨立出來。我常用的有三條路按方便程度排序第二路UART最樸素。GND、TX、RX三根線接一個USB轉(zhuǎn)串口模塊115200波特率printf重定向過去。優(yōu)點是什么環(huán)境都兼容缺點是占引腳。SWO引腳如果調(diào)試器支持。STM32的SWD接口里有一個SWO腳可以用SWVSerial Wire Viewer輸出調(diào)試信息不占UART速度還快。J-Link的RTT如果手頭有J-Link。RTT通過調(diào)試接口直接讀寫目標內(nèi)存輸出日志幾乎是零侵入比SWO更方便還支持雙向輸入命令。所以我的第一個建議是新工程先不要急著寫USB業(yè)務(wù)先把日志通道調(diào)通。在USB事件回調(diào)、狀態(tài)切換、收發(fā)完成這些關(guān)鍵節(jié)點上打點后面排查任何問題都事半功倍。日志要打得克制狀態(tài)變化打一次就夠了別在中斷回調(diào)里逐字節(jié)打印會干擾時序。2. 工具不是越貴越好我的USB調(diào)試工具箱2.1 邏輯分析儀采樣率別低于這個數(shù)排查USB問題邏輯分析儀是性價比最高的硬件工具。USB全速Full Speed信號速率是12Mbps按照奈奎斯特定理采樣率至少要24MS/s才能保證最基本的信息不丟但實際解碼時波形邊沿、噪聲、毛刺都需要余量。我的經(jīng)驗是采樣率低于50MS/s的儀器解碼USB全速包會出現(xiàn)莫名其妙的失敗丟包、誤碼、CRC報錯最后你會分不清到底是協(xié)議錯了還是工具錯了。買邏輯分析儀不用追貴100元以內(nèi)的8通道就夠用關(guān)鍵是支持USB協(xié)議解碼。Saleae的軟件生態(tài)最好USB 1.1解碼對全速調(diào)試來說綽綽有余國產(chǎn)的各種邏輯分析儀如果采樣率足夠、驅(qū)動穩(wěn)定也可以。我手頭那臺就是24M采樣率的便宜貨后來為了調(diào)USB專門換了臺能到100M的一次就把問題看清楚了。接線時注意D、D-兩個通道必須同時接GND也要和板子共地。如果只接一條D解出來的數(shù)據(jù)基本沒法看。還有就是探頭盡量靠近STM32的USB引腳別從USB座子末端引線板上走線太長會引入反射和干擾波形就不干凈了。2.2 Wireshark、Bus Hound、CubeMonitor-RX各守一層邏輯分析儀看到的是物理層字節(jié)流但USB還有更上層的邏輯URBUSB Request Block、描述符請求、端點數(shù)據(jù)傳輸。要看到這些需要軟件層抓包工具。Wireshark USBPcapWindows下裝好USBPcap后Wireshark會多出一個USBPcap1接口選擇它就能抓主機側(cè)所有USB流量。抓包時先用usb.idVendor 0x0483這類過濾條件把目標設(shè)備的流量篩出來然后就能看到枚舉、控制傳輸、批量數(shù)據(jù)傳輸?shù)耐暾^程。Linux下對應(yīng)的是usbmon接口dmesg也會在枚舉失敗時打印一些內(nèi)核錯誤比如device descriptor read/64, error -71這些信息對定位問題很有用。Bus Hound老牌USB抓包工具界面比Wireshark丑但對URB層的完成狀態(tài)展示得特別清楚。它能看到每一個IN/OUT請求是被ACK了還是NAK了這對于排查“數(shù)據(jù)發(fā)不出去”這種問題非常有價值。STM32CubeMonitor-RXST官方的變量可視化工具配合ST-Link可以實時看MCU內(nèi)部變量。說實話它對USB調(diào)試的幫助偏弱因為USB問題更多是事件時序問題不是波形或變量值問題。這三個工具的分工可以這樣理解Wireshark看“主機有沒有發(fā)請求、設(shè)備有沒有響應(yīng)”Bus Hound看“每個請求的握手狀態(tài)”邏輯分析儀看“總線上到底是什么電平波形”。三層對上了問題定位就是時間問題。2.3 SWO和RTT兩個真正適合USB調(diào)試的日志通道前面提到SWO和RTT這里展開講一下為什么它們比UART更適合USB調(diào)試。用UART打日志最大的問題不是速度而是阻塞。如果用阻塞式發(fā)送printf一個幾百字節(jié)的串會在低波特率下占用幾毫秒。這幾毫秒放在USB枚舉期間主機可能已經(jīng)超時了。所以很多人在USB調(diào)試時加上printf反而把問題弄得更隱蔽。SWO是ARM調(diào)試接口里的一個專用輸出腳通過ITM模塊往外吐數(shù)據(jù)不占用UART也不需要主動調(diào)用UART驅(qū)動硬件自動把數(shù)據(jù)送出去。在Keil里打開Trace設(shè)置或者在CubeIDE里配置好SWOprintf就可以重定向到ITM。缺點是有些調(diào)試器/開發(fā)板沒把SWO引腳引出來或者ST-Link的固件版本不支持需要折騰一下。RTT是SEGGER搞的機制J-Link通過調(diào)試接口直接讀寫目標片內(nèi)環(huán)形緩沖區(qū)。目標是往RAM里的一個環(huán)形緩沖寫日志J-Link那邊用RTT Viewer實時讀出來。這個方式的侵入性很小而且吞吐量比SWO高很多打幾千字節(jié)日志也不心疼。缺點是必須用J-Link調(diào)試器如果用ST-Link就沒法直接用官方RTT方案。我現(xiàn)在的習慣是板子上如果有SWO引腳優(yōu)先SWO如果正好用J-Link直接RTT。兩條通道都比UART日志對USB時序的影響小一個量級。3. 從D/D-到應(yīng)用層四層拆解排查法3.1 物理層先確認設(shè)備“被看見”了排查USB問題我從來不會上來就翻固件代碼先拿萬用表量電壓。USB設(shè)備插入主機后主機靠檢測D線上的上拉來判斷設(shè)備類型和設(shè)備是否在線。全速設(shè)備要求D有一個1.5kΩ上拉到3.3VD-保持低低速設(shè)備則反過來。所以我插上前先量一下D對地電壓正常應(yīng)該在3.3V附近。如果量出來是0V說明上拉沒生效。上拉不生效的原因有幾個板子上壓根沒焊上拉電阻或者上拉電阻接到了IO口而不是固定電源而固件沒把那個IO拉高又或者內(nèi)部上拉功能沒有使能。不同系列STM32的上拉控制不完全一樣F1和F4差異還挺大別想當然。接線和插座也值得反復確認。USB座子的D/D-順序焊反、D/D-接到同一個網(wǎng)絡(luò)、插座沒接地等這些低級錯誤我在幫人看問題時遇到過不止一次。還有一個很隱蔽的坑VBUS檢測腳。很多板子把VBUS分壓后接到MCU的ADC腳或IO口讓固件判斷外部電源是否插入。如果檢測腳沒接或者配置錯誤即使D上拉正常固件也可能認為自己不在USB總線上停止響應(yīng)。3.2 枚舉層主機和設(shè)備第一次握手物理層沒問題之后把邏輯分析儀接到D/D-上插拔一次USB線抓枚舉波形。你會看到主機發(fā)出一串復位信號SE0然后就是SETUP包、DATA包、握手包。這一串過程就是主機在“讀設(shè)備信息”。如果在這里抓不到任何SETUP包大概率是主機壓根沒檢測到設(shè)備問題回到物理層。如果抓到了SETUP包但設(shè)備沒有反應(yīng)或者反應(yīng)是STALL那是設(shè)備固件沒有正確響應(yīng)標準請求需要檢查設(shè)備庫的初始化、中斷是否開啟、描述符是否合法。枚舉階段最常見的失敗點是描述符。主機第一次請求設(shè)備描述符時只要求返回前8個字節(jié)因為它需要先知道bMaxPacketSize0的值然后重新復位再請求完整18字節(jié)的設(shè)備描述符。如果你的描述符里bMaxPacketSize0填得和硬件實際配置不一致或者返回的數(shù)據(jù)長度不對主機就會判定枚舉失敗。用Wireshark抓URB時能直接看到主機收到的原始字節(jié)對照USB規(guī)范一眼就能看出問題。3.3 傳輸層ACK、NAK和超時那些事枚舉成功不等于通信成功。進入數(shù)據(jù)通信階段后真正讓人頭疼的是NAK。主機發(fā)IN令牌設(shè)備端點沒有數(shù)據(jù)要發(fā)就回NAK主機發(fā)OUT令牌設(shè)備接收緩沖區(qū)還沒準備好也回NAK。NAK本身不是錯誤它是USB協(xié)議里的正常流量控制機制但大量NAK出現(xiàn)時通信效率會急劇下降行為上表現(xiàn)為“設(shè)備沒反應(yīng)”。排查NAK問題用Bus Hound或者邏輯分析儀看握手包最直接。如果主機發(fā)了100個IN令牌設(shè)備回了100個NAK說明應(yīng)用層根本沒有往發(fā)送端點塞數(shù)據(jù)問題在固件的數(shù)據(jù)發(fā)送邏輯。如果OUT端點一直NAK通常是設(shè)備沒有重新武裝接收緩沖區(qū)。很多HAL庫的實現(xiàn)里CDC數(shù)據(jù)接收是“一次性”的每次收到一包后在回調(diào)里必須重新調(diào)用接收函數(shù)否則端點就停在“未準備”狀態(tài)后續(xù)數(shù)據(jù)全部被NAK。超時也要留意。USB控制傳輸有超時限制主機發(fā)出請求后如果設(shè)備遲遲不應(yīng)答主機會放棄并要求設(shè)備復位。之前我遇到過一例USB中斷優(yōu)先級被意外設(shè)成低于某個定時器中斷定時器中斷一排隊就占了大量CPU時間導致USB響應(yīng)延遲超過主機容忍范圍于是反復枚舉失敗。后來把USB中斷優(yōu)先級調(diào)上去問題立刻消失。3.4 應(yīng)用層回調(diào)上下文和緩沖生命周期過了傳輸層還有最后一層應(yīng)用層。這一層的坑往往和緩沖區(qū)的生命周期有關(guān)。以CDC為例接收回調(diào)里拿到的指針指向的是USB棧內(nèi)部的接收緩沖。當你在這個回調(diào)里把數(shù)據(jù)復制走之前下一次USB傳輸可能已經(jīng)覆蓋了這塊內(nèi)存。如果應(yīng)用處理速度跟不上就會出現(xiàn)數(shù)據(jù)錯位或者“收到的數(shù)據(jù)對不上”。解決辦法是雙緩沖或者立即拷貝不要留著指針跨函數(shù)使用。還要注意回調(diào)的執(zhí)行上下文。USB中斷回調(diào)運行在中斷上下文里在里面做復雜運算、大內(nèi)存拷貝、阻塞發(fā)送都是大忌。正確做法是回調(diào)里只做最輕量的事比如置標志、把數(shù)據(jù)搬到自己的隊列、啟動下一次接收真正的解析和處理放到主循環(huán)或高優(yōu)先級任務(wù)里。字節(jié)序也容易踩坑。USB協(xié)議默認小端序很多傳感器和通信協(xié)議反而是大端序。如果兩邊解析方式不一致數(shù)據(jù)看著就是反的。遇到數(shù)據(jù)內(nèi)容錯亂先檢查字節(jié)序再懷疑協(xié)議解析邏輯。4. 實戰(zhàn)四個高頻故障的完整定位過程4.1 案例一設(shè)備描述符請求失敗這是最經(jīng)典的故障現(xiàn)象就是設(shè)備管理器里出現(xiàn)“未知USB設(shè)備設(shè)備描述符請求失敗”。我的排查鏈路一般是這樣第一步量D電壓。插上USB線但不插電腦端不量的是插電腦端之前的狀態(tài)D應(yīng)該有上拉。實際上插上電腦瞬間