與端點模型詳解:從包結(jié)構(gòu)到枚舉流程)
做了這么多年嵌入式我有個特別深的感觸搞單片機的朋友玩I2C、SPI、UART那叫一個得心應手示波器一夾、邏輯分析儀一掛幾分鐘就能定位問題??梢坏経SB就開始發(fā)怵總覺得這玩意兒像個黑盒子硬件上電就能用一旦出問題就不知道從哪里下嘴。上篇咱們把USB從誕生到普及的歷史捋了一遍這篇進入正題把USB的整體架構(gòu)和端點通信模型一層層剝開講清楚數(shù)據(jù)到底是怎么在一條總線上“排著隊”走的。這篇文章適合兩類人。一類是正在做USB設(shè)備端的固件工程師比如你要調(diào)一個自定義HID設(shè)備、一個USB轉(zhuǎn)串口或者一個U盤主控理解架構(gòu)能幫你少走很多彎路。另一類是寫上位機或者調(diào)試驅(qū)動時被USB問題折磨的工程師——你不需要會寫固件但你需要知道設(shè)備在枚舉時到底發(fā)生了什么才能從一堆抓包數(shù)據(jù)里找到蛛絲馬跡。我會盡量把話說得通俗但該給的技術(shù)細節(jié)一個也不會少。1. 整個總線只有一個“指揮官”USB主從架構(gòu)的真相1.1 為什么USB長得像一棵倒掛的樹USB的拓撲結(jié)構(gòu)說穿了就是一棵以主機Host為根、向下分叉的樹。主機里有根HubRoot Hub根Hub下面可以接設(shè)備也可以接外置Hub外置Hub再往下接更多設(shè)備。整個結(jié)構(gòu)是這樣的主機在最頂上Hub是中間的分叉節(jié)點設(shè)備是末端的葉子。理論上一個根Hub下面最多可以掛127個設(shè)備因為USB協(xié)議只給了7位地址空間地址0還留作默認地址所以實際可用地址是1到127。很多人第一次看這個拓撲會問為什么不用總線型或者星型這里有個物理上的硬約束——USB的電氣信號傳輸距離有限線纜一長信號就衰減而且一個端口驅(qū)動能力有限所以必須靠Hub做信號中繼和供電分發(fā)。Hub本質(zhì)上是一個“信號放大器端口擴展器”它把主機發(fā)下來的包廣播到所有下游端口又負責把下游設(shè)備返回的數(shù)據(jù)包轉(zhuǎn)發(fā)給主機。這就引出了USB一個極其重要的特性這條總線是共享的而且只有一個“指揮官”。任何設(shè)備都不能主動向總線上發(fā)數(shù)據(jù)所有通信都必須由主機發(fā)起。哪怕鼠標動了、鍵盤按了設(shè)備也只能干等著直到主機發(fā)一個IN令牌包來問它“你有沒有數(shù)據(jù)要給我”它才能把數(shù)據(jù)放上總線。這種模式叫“主機輪詢”Host Polling和以太網(wǎng)那種“誰想發(fā)就發(fā)沖突了再退避”的對等模型完全不是一回事。1.2 主從架構(gòu)的得與失簡單可靠但主動不了USB選擇這種嚴格的主從架構(gòu)我當時第一次接觸時覺得“這也太憋屈了”設(shè)備跟個受氣包似的。但后來仔細想想這恰恰是USB能做得這么便宜、這么皮實的關(guān)鍵。因為有了明確的主從關(guān)系總線上的沖突問題從根本上被避免了。以太網(wǎng)要搞CSMA/CD那一套來應對碰撞而USB根本不需要——主機統(tǒng)一調(diào)度同一時刻只會有一個設(shè)備在傳輸數(shù)據(jù)包不會“撞車”。這個特性帶來的直接好處是協(xié)議實現(xiàn)簡單設(shè)備端可以做得非常便宜一個幾毛錢的單片機都能掛USB不需要像以太網(wǎng)那樣復雜的仲裁機制硬件成本低確定性好主機可以精確控制帶寬分配這對音視頻等實時性要求高的場景特別關(guān)鍵但代價也很明顯——設(shè)備端永遠沒辦法主動發(fā)起通信。比如你想讓設(shè)備在某個事件發(fā)生時立刻通知主機在USB的世界里做不到“立刻”它只能等主機下一次輪詢到它時才能上報。所以USB的中斷傳輸本質(zhì)上也是輪詢只不過輪詢間隔可以設(shè)得很短讓人感覺像“中斷”一樣。這個特性在設(shè)計中會帶來一些連鎖反應。舉個例子很多USB設(shè)備需要“異步通知”功能比如一個USB轉(zhuǎn)串口芯片收到了串口數(shù)據(jù)它想趕緊告訴主機。但由于設(shè)備不能主動發(fā)數(shù)據(jù)它只能靠主機不斷輪詢它的中斷端點來取數(shù)據(jù)。所以你在寫上位機時會發(fā)現(xiàn)讀取這類設(shè)備時經(jīng)常要開一個線程循環(huán)去讀這個“主動讀”的動作本質(zhì)上就是在配合USB這種輪詢機制。1.3 帶寬是大家的超了就得排隊既然是一條共享總線帶寬就是所有人搶著用的。USB 2.0高速模式的理論帶寬是480Mbps注意這個數(shù)字是“總線帶寬”不是“某個設(shè)備的專享帶寬”。同一時刻總線上只能有一個傳輸在跑其他設(shè)備都得等著。這個感覺有點像老式電話線的“分時復用”——每個人都在一條線上說話輪到誰誰才能開口。這就帶來一個在系統(tǒng)設(shè)計上很重要的認知當你決定用USB連接多個設(shè)備時總帶寬是需要做預算的。一個USB攝像頭如果占了200Mbps一個U盤再跑個150Mbps留給其他設(shè)備的帶寬就不多了。尤其要注意的是USB協(xié)議為了保證實時性會優(yōu)先分配等時傳輸和中斷傳輸?shù)膸捠O碌牟沤o批量傳輸用。所以如果你接了一個不間斷傳輸數(shù)據(jù)的等時音頻設(shè)備U盤的讀寫速度會明顯被壓下來。這不是故障而是協(xié)議本身的帶寬調(diào)度策略。2. 數(shù)據(jù)在線上長什么樣從差分信號到包封裝2.1 一根線怎么既傳數(shù)據(jù)又傳時鐘看USB 2.0的線序四根線VBUS、GND、D、D-其中D/D-是一對差分信號線。數(shù)據(jù)不是用“高低電平”單端傳輸?shù)亩怯谩皟筛€上的電壓差”來表示邏輯狀態(tài)。這樣做的最大好處是抗干擾能力強——共模噪聲會同時疊加在兩根線上做差之后噪聲就被抵消了。但這里有個更巧妙的設(shè)計USB數(shù)據(jù)線上沒有單獨的時鐘線時鐘是編碼在數(shù)據(jù)里的。USB用的是NRZI編碼Non Return to Zero Inverted規(guī)則是遇到邏輯0電平翻轉(zhuǎn)遇到邏輯1電平保持不變。也就是說接收端不是靠邊沿來對齊時鐘的而是靠數(shù)據(jù)的翻轉(zhuǎn)來恢復時鐘。但這就帶來一個問題——如果連續(xù)傳輸多個1電平就一直不變接收端沒法知道過了多少個位。所以USB規(guī)定了一個“位填充”Bit Stuffing機制連續(xù)出現(xiàn)6個邏輯1之后發(fā)送端必須強制插入一個邏輯0讓電平翻轉(zhuǎn)一下這樣接收端就能靠這個翻轉(zhuǎn)重新同步時鐘。我第一次看這個編碼機制的時候想到一個生活類比——這就像兩個人約好每隔一段時間必須say一句話哪怕沒話說也要“喂”一聲就是為了讓對方知道“我還活著、你還在線上”。USB的位填充就是這個“喂”一聲的動作。2.2 SYNC、PID、地址和端點號一次USB傳輸?shù)摹靶欧狻盪SB協(xié)議里所有數(shù)據(jù)傳輸都被封裝成“包”Packet。一個包的結(jié)構(gòu)從前往后大致是SYNC字段一串固定的同步序列讓接收端對齊時鐘和位流PIDPacket Identifier包類型標識告訴接收端“我是誰”——是令牌包、數(shù)據(jù)包還是握手包地址和端點字段令牌包才有包含7位設(shè)備地址和4位端點號數(shù)據(jù)字段數(shù)據(jù)包才有承載實際數(shù)據(jù)CRC校驗檢錯用的令牌包用CRC5數(shù)據(jù)包用CRC16這里要注意PID只有4位但因為后面還要跟一個逐位取反的副本用于校驗所以實際在線上是8位。協(xié)議的冗余設(shè)計比你想象中要多這也是USB可靠性高的重要原因。在USB世界里包一共分三大類類型常見PID誰發(fā)的作用令牌包IN、OUT、SETUP、SOF主機發(fā)起一個事務指定設(shè)備和端點數(shù)據(jù)包DATA0、DATA1、DATA2、MDATA主機或設(shè)備承載實際數(shù)據(jù)握手包ACK、NAK、STALL、NYET設(shè)備或主機確認/忙/錯誤/未就緒事務Transaction就是由這種包按固定順序拼出來的。主機先發(fā)一個令牌包“點名”然后數(shù)據(jù)階段傳數(shù)據(jù)最后握手階段告訴對方“收到了”或“沒收到”。三個包合起來才算完成一次事務。2.3 一個批量讀事務的完整過程拿最常見的批量讀來舉例。假設(shè)主機想從一個U盤讀取數(shù)據(jù)整個過程是這樣的主機發(fā)一個IN令牌包里面寫著“設(shè)備地址3端點號2我要向你要數(shù)據(jù)”設(shè)備收到后如果數(shù)據(jù)準備好了就把數(shù)據(jù)打包成DATA1包發(fā)回給主機主機正確收到數(shù)據(jù)后發(fā)一個ACK握手包告訴設(shè)備“數(shù)據(jù)完整收到你可以發(fā)下一批了”如果設(shè)備數(shù)據(jù)沒準備好它會回一個NAK握手包主機就知道“現(xiàn)在沒數(shù)據(jù)我改天再來”然后這個事務就結(jié)束了。NAK不是錯誤是設(shè)備在說“我現(xiàn)在忙但沒壞”。如果這個寄存器設(shè)備發(fā)現(xiàn)端點的Halt特性被設(shè)置了它就會回STALL這個就屬于錯誤狀態(tài)了需要主機介入處理。區(qū)分NAK和STALL是踩坑高發(fā)區(qū)很多人在抓包軟件里看到STALL就以為設(shè)備掛了其實很多時候是設(shè)備固件里設(shè)了STALL來表示“這個請求我不支持”比如HID設(shè)備收到一個它不認識的類特定請求時就可以回STALL。理解了這個“令牌-數(shù)據(jù)-握手”的三段式結(jié)構(gòu)后面看抓包就輕松多了因為你一眼就能看出哪段是主機發(fā)起的、哪段是設(shè)備響應的、哪段是確認收尾的。3. 端點是USB世界的“門牌號”理解端點與管道模型3.1 端點不是一根線而是設(shè)備內(nèi)部的一小塊“緩沖區(qū)”端點是USB協(xié)議里最核心、也最容易讓人誤解的概念。很多人習慣把端點想象成“物理引腳”或者“接口”其實完全不是。端點是設(shè)備內(nèi)部的一個數(shù)據(jù)緩沖區(qū)是設(shè)備與主機之間數(shù)據(jù)傳輸?shù)摹笆瞻l(fā)窗口”。主機通過端點號和數(shù)據(jù)方向來尋址這個窗口往里寫數(shù)據(jù)或從里面取數(shù)據(jù)。每個端點都有四個關(guān)鍵屬性端點號0~15、方向IN/OUT、傳輸類型控制/批量/中斷/等時、最大包大小。這些屬性不是憑空定的而是設(shè)備在枚舉時通過端點描述符告訴主機的。比如一個全速批量端點最大包大小通常是64字節(jié)一個高速批量端點最大包大小是512字節(jié)。這里要特別強調(diào)一個很多人都會踩的坑除了端點0一個端點號通常只對應一個方向。也就是說端點1的輸出和端點1的輸入實際是兩個不同的端點。你在寫固件時要是想做一個既能收又能發(fā)的功能得同時配置1-IN端點和1-OUT端點并分別分配緩沖區(qū)。我看到不少新手在配置寄存器時只配了一個方向然后調(diào)試半天發(fā)現(xiàn)數(shù)據(jù)通道不通其實就是沒搞清楚這個規(guī)則。用個生活化的方式來理解端點就像小區(qū)樓下的收件柜。每個柜子有編號端點號有的柜子是“只收件”O(jiān)UT有的是“只取件”IN有的柜子大批量能放大量包裹有的柜子小中斷一次只能放一個有的柜子有響應確認批量/中斷/控制收了要回執(zhí)有的柜子沒有等時放了就走丟件不管。3.2 管道已經(jīng)約定好傳輸方式的訪問通道管道Pipe是主機視角下的概念。主機不是直接往端點里讀寫數(shù)據(jù)的而是先跟端點建立一條“管道”之后的數(shù)據(jù)傳輸都沿著這條管道走。管道可以理解成“主機與設(shè)備端點之間的一條邏輯連接”它把端點的各種屬性端點號、方向、傳輸類型、最大包大小都封裝好了上層軟件只需要對著管道讀寫而不用每次都重復指定這些參數(shù)。管道分兩類控制管道雙向的專門走控制傳輸默認管道0在設(shè)備一上電時就建立好了用來做枚舉和標準請求流管道單向的可以是中斷、批量或等時傳輸數(shù)據(jù)按流的方式流動沒有命令語義“流管道”這個詞值得琢磨一下。它強調(diào)這幾種傳輸類型是“數(shù)據(jù)流模式”的一次發(fā)一批數(shù)據(jù)發(fā)了就完了沒有“請求-應答”的協(xié)議語義嵌套在里面。真正有命令語義的是控制傳輸它像一套“帶外信號”專門用來配置設(shè)備。3.3 端點0所有設(shè)備都必須有的“公共服務窗口”端點0是USB里最特殊的端點。它在設(shè)備上電、還沒有分配地址之前就已經(jīng)存在了主機通過默認地址0向端點0發(fā)送控制請求。USB規(guī)范要求所有設(shè)備必須支持端點0的控制傳輸端點0是雙向的只有它一個端點“既做IN又做OUT”。端點0的最大包大小是設(shè)備自己聲明的低速設(shè)備是8字節(jié)全速設(shè)備可以是8/16/32/64字節(jié)中的一個高速設(shè)備固定64字節(jié)。設(shè)備在枚舉最開始必須先響應一個請求讓主機知道“我的端點0最大包是多少”然后主機會重新復位設(shè)備給設(shè)備分配一個正式地址再繼續(xù)讀取完整的信息。把端點0想象成小區(qū)物業(yè)的“服務總臺”就很好理解了任何住戶主機搬進來之前先要到總臺報到登記基本信息讀設(shè)備描述符、領(lǐng)取門牌號設(shè)置地址、了解小區(qū)規(guī)定讀配置描述符然后才能正式入住。在入住之前所有溝通都只能通過總臺進行這個總臺就是端點0。4. 四種傳輸類型怎么選控制、批量、中斷、等時4.1 各有脾氣從可靠性、時延、帶寬三個維度看USB協(xié)議定義了四種傳輸類型每種都是在可靠性、時延、帶寬三個維度上做了取舍。理解這層關(guān)系你在選型時就能少走彎路??刂苽鬏旊p向、可靠性最高專門用來傳輸配置命令和狀態(tài)信息。它的特點是“請求-應答”式一次控制傳輸分為建立階段、數(shù)據(jù)階段可選和狀態(tài)階段。建立階段主機發(fā)SETUP包數(shù)據(jù)階段傳數(shù)據(jù)狀態(tài)階段確認結(jié)束??刂苽鬏斦加脦拑?yōu)先級最高但數(shù)據(jù)量通常很小基本就是枚舉時和發(fā)命令用的。批量傳輸單向、可靠性高保證數(shù)據(jù)一定送達出錯會重傳但不保證時延。適合U盤、USB轉(zhuǎn)串口、MSC類存儲設(shè)備這類對數(shù)據(jù)完整性要求極高、對實時性不敏感的場景。批量傳輸在所有傳輸里優(yōu)先級最低只能排在控制和等時后面“撿剩飯”。如果你的批量設(shè)備發(fā)現(xiàn)速率不穩(wěn)定大概率就是總線上有其他實時傳輸在占用帶寬。中斷傳輸單向、可靠性高且保證輪詢間隔。雖然名字叫中斷但它其實是主機按固定周期輪詢設(shè)備。適合鼠標、鍵盤、游戲手柄這類數(shù)據(jù)量小但希望延遲可控的設(shè)備。全速中斷端點的輪詢間隔可以設(shè)置為1~255ms高速可以按微幀來算最短125us輪詢一次。注意低速設(shè)備只支持控制傳輸和中斷傳輸而且低速中斷端點最大包只有8字節(jié)。等時傳輸單向、不保證可靠性出錯不重傳但保證帶寬和時延。適合USB攝像頭、USB聲卡、麥克風這類對實時性要求極高、丟一兩幀無所謂的場景。等時傳輸沒有ACK/NAK握手數(shù)據(jù)包發(fā)了就完了丟了也不補。所以你在設(shè)計和調(diào)試這類設(shè)備時一定要在應用層做容錯處理。4.2 對照真實設(shè)備選型我整理了一個平時做項目時常用的對照表基本覆蓋了常見設(shè)備類型設(shè)備類型傳輸類型為什么這么選U盤、移動硬盤批量數(shù)據(jù)量大、完整性要求極高延遲不是關(guān)鍵USB鼠標、鍵盤中斷數(shù)據(jù)量小、希望快速響應輪詢機制夠用USB串口/CDC批量數(shù)據(jù)流可靠性優(yōu)先延遲可接受USB攝像頭等時/批量高帶寬實時性優(yōu)先掉幀可以通過上層補償USB聲卡等時音頻流實時性要求極高偶爾丟一幀影響不大自定義HID設(shè)備中斷雙向小數(shù)據(jù)交換延遲可控有一個比較典型的誤區(qū)是很多人以為“中斷”傳輸比“批量”傳輸更快。實際恰恰相反在USB 2.0高速模式下批量傳輸?shù)膯未瓮话l(fā)數(shù)據(jù)量遠大于中斷端點能達到每秒幾十MB而中斷傳輸靠的是“實時性”不是“吞吐量”。所以如果你的需求是傳輸大批量數(shù)據(jù)不要選中斷傳輸它一幀微幀能傳的數(shù)據(jù)有限整體帶寬遠不如批量。反過來說如果你需要低延遲的周期性數(shù)據(jù)上報批量傳輸就不合適因為它在帶寬調(diào)度時可能被其他傳輸擠到后面延遲不可控。4.3 帶寬分配幀與微幀里的時間片游戲USB 2.0在時間上把總線劃分成“幀”Frame和“微幀”Microframe。全速模式是1毫秒一個幀高速模式是125微秒一個微幀一個幀等于8個微幀。每個幀/微幀開始主機都會發(fā)一個SOFStart of Frame令牌包相當于“新的一輪調(diào)度開始”。然后主機根據(jù)當前各個端點的需求在這段時間片里安排各種事務。調(diào)度策略大致是控制和等時傳輸優(yōu)先級最高先安排中斷傳輸其次批量傳輸最后只能占用剩余時間USB規(guī)范還給批量傳輸設(shè)定了一個限制在高速模式下批量傳輸不能占用超過80%的總帶寬。這個限制其實是為了保證控制傳輸永遠不會被餓死。因為如果批量設(shè)備持續(xù)滿負荷傳輸把總線占滿了控制傳輸就沒機會執(zhí)行了設(shè)備連“急剎車”的指令都收不到整個總線就失控了。理解這個調(diào)度邏輯對排查性能問題特別有幫助。比如你在同一個USB Hub上掛了一個U盤和一個USB攝像頭如果U盤拷貝速度明顯變慢不要急著懷疑U盤壞了先看看是不是攝像頭的等時傳輸把微幀里的時間片占了留給批量的只剩一小塊。5. 設(shè)備“報到”全過程枚舉怎么把設(shè)備家底問清楚5.1 五個標準請求主機和設(shè)備靠這些“對話”USB枚舉Enumeration就是設(shè)備插入后、驅(qū)動加載前主機與設(shè)備之間的一系列控制傳輸對話。這個對話主要是通過一組標準請求來完成的比較核心的有GET_DESCRIPTOR讀取設(shè)備的各類描述符SET_ADDRESS給設(shè)備分配一個正式地址GET_CONFIGURATION / SET_CONFIGURATION查詢/設(shè)置設(shè)備的配置GET_STATUS / CLEAR_FEATURE / SET_FEATURE讀寫設(shè)備的某些狀態(tài)和特性SET_INTERFACE / GET_INTERFACE切換接口的Alternate Setting如果你在抓包工具里看到這些請求的交互過程那就是典型的枚舉階段。前幾步幾乎是固定的設(shè)備插入Hub檢測到D/D-電平變化向主機報告“有設(shè)備插入”主機讓Hub給設(shè)備復位設(shè)備進入默認狀態(tài)地址為0主機向地址0端點0發(fā)GET_DESCRIPTOR(Device)但只讀前8字節(jié)目的就是拿到端點0的最大包大小主機再次復位設(shè)備發(fā)SET_ADDRESS(新地址)設(shè)備從此用新地址通信主機發(fā)GET_DESCRIPTOR(Device)讀完整18字節(jié)設(shè)備描述符主機發(fā)GET_DESCRIPTOR(Config)讀配置描述符包括接口描述符和端點描述符主機根據(jù)識別到的類代碼加載對應驅(qū)動驅(qū)動發(fā)SET_CONFIGURATION(1)設(shè)備進入配置狀態(tài)開始正常工作整個過程通常在毫秒級完成肉眼是感覺不到的但抓包看每一步都非常清晰。5.2 描述符鏈設(shè)備的“簡歷”是怎么組織的描述符是USB設(shè)備“自我介紹”的數(shù)據(jù)結(jié)構(gòu)主機的驅(qū)動識別設(shè)備全靠它。它的組織方式是嵌套的樹形結(jié)構(gòu)設(shè)備描述符Device Descriptor └── 配置描述符Configuration Descriptor └── 接口描述符Interface Descriptor └── 端點描述符Endpoint Descriptor設(shè)備描述符是整個設(shè)備的“封面頁”包含USB版本號、設(shè)備類代碼、廠商IDVID、產(chǎn)品IDPID、端點0的最大包大小、設(shè)備版本等。其中VID和PID是驅(qū)動匹配的重要依據(jù)你在設(shè)備管理器里看到的“VID_1234PID_5678”就是從這里來的。配置描述符描述一個配置的總體信息包括接口數(shù)量、供電方式自供電還是總線供電、最大電流消耗。一個設(shè)備可以有多個配置但同一時刻只能啟用一個。比如一個USB設(shè)備可以有一個“高功耗高性能”配置和一個“低功耗低性能”配置根據(jù)供電情況切換。接口描述符描述一個“功能”。一個配置可以包含多個接口每個接口對應一個獨立的功能。這個概念在復合設(shè)備Composite Device上特別重要比如一個USB設(shè)備既做鍵盤又做鼠標又做串口它就可以有三個接口每個接口對應一種功能。Windows會對每個接口枚舉成一個獨立的設(shè)備節(jié)點所以你會在設(shè)備管理器里看到“USB輸入設(shè)備”和“COM口”同時出現(xiàn)。端點描述符定義端點的各項參數(shù)端點號、方向、傳輸類型、最大包大小、輪詢間隔。一個全速設(shè)備最多可以有16個端點端點0 15個IN 15個OUT但實際上大多數(shù)設(shè)備只用到幾個。在Linux下用lsusb -v可以看到完整的描述符樹格式非常直觀Bus 001 Device 004: ID 046d:c534 Logitech, Inc. Unifying Receiver Device Descriptor: idVendor 0x046d Logitech, Inc. idProduct 0xc534 bcdDevice 32.00 iManufacturer 1 Logitech iProduct 2 USB Receiver ... bNumConfigurations 1 Configuration Descriptor: bNumInterfaces 3 Interface Descriptor: bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 2 Mouse Endpoint Descriptor: bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt wMaxPacketSize 0x0008 1x 8 bytes bInterval 10這里能看到鼠標的中斷端點屬性端點2 IN、中斷傳輸、最大包8字節(jié)、輪詢間隔10ms。字段都寫得明明白白。5.3 枚舉失敗怎么查我踩過的三個典型坑做USB設(shè)備調(diào)試最痛苦的就是枚舉失敗。這里分享三個我真正踩過、也幫別人排查過的問題。第一個D/D-接反了。聽著像是低級錯誤但實際操作中特別容易發(fā)生因為有些板子的D/D-絲印不明顯或者轉(zhuǎn)接板標注不統(tǒng)一。癥狀就是主機完全檢測不到設(shè)備dmesg里什么都沒有。排查辦法很簡單用示波器看D/D-有沒有波形或者干脆量一下設(shè)備端的D上拉電阻是否連接到了正確的那根線。USB 2.0全速/高速設(shè)備要求在D上接1.5kΩ上拉電阻到3.3V低速設(shè)備在D-上接。如果上拉接錯了線主機就識別不了設(shè)備速度。第二個端點描述符的配置和固件實際行為不一致。有一次我調(diào)一個復合設(shè)備描述了三個接口但固件里只初始化了兩個。Windows在枚舉時按描述符去找對應的端點和接口結(jié)果找不到直接報“設(shè)備描述符請求失敗”Code 43。這個坑很隱蔽因為設(shè)備本身能響應控制請求枚舉流程前一半都正常但到了配置階段就崩潰。排查方法是把描述符導出來一字節(jié)一字節(jié)對確認bNumInterfaces和實際固件里初始化的一致。第三個供電不足導致設(shè)備反復復位。設(shè)備插入后如果瞬間電流需求超過了端口能供給的上限Hub會切斷供電設(shè)備掉電重啟然后又被檢測到又掉電形成一個死循環(huán)?,F(xiàn)象是設(shè)備在設(shè)備管理器里閃爍不停dmesg里能看到反復的“USB disconnect”和“USB connect”消息。解決方法是改用帶外部供電的Hub或者獨立供電的開發(fā)板。5.4 Linux上的USB調(diào)試三板斧既然我們寫技術(shù)文章Linux下的USB調(diào)試工具還是得熟悉一下這比Windows下用Bus Hound要輕量得多。第一板斧是lsusb -v。它能列出所有USB設(shè)備以及完整的描述符。當你想確認設(shè)備到底上報了哪些描述符或者想查看設(shè)備是不是進入了異常狀態(tài)用這個命令非常方便。第二板斧是dmesg。內(nèi)核的USB子系統(tǒng)會打印非常詳細的枚舉過程信息包括設(shè)備速度、地址分配、配置選擇、驅(qū)動綁定等。設(shè)備插入后跑一下dmesg | tail -20基本能看到問題出在哪個環(huán)節(jié)。第三板斧是usbmon Wireshark。加載usbmon內(nèi)核模塊后Wireshark可以直接抓USB總線上的所有包。抓包能讓你看到每一個控制請求、每一次批量傳輸?shù)奈帐智闆r。之前調(diào)一個USB轉(zhuǎn)串口不定時丟數(shù)據(jù)的問題就是靠這個抓包發(fā)現(xiàn)設(shè)備在數(shù)據(jù)壓力大時頻繁回NAK從而定位到是固件里FIFO太小導致緩沖區(qū)溢出而不是串口芯片的硬件問題。用usbmon抓包的簡要步驟sudo modprobe usbmon sudo wireshark在Wireshark的接口列表里選擇usbmon1對應USB bus 1就能開始抓了。抓包時如果想縮小范圍可以在過濾器里寫usb.addr 3.2只看地址3設(shè)備端點2相關(guān)的流量非常精準。這里要給一個實際建議如果產(chǎn)品在Windows和Linux下表現(xiàn)不一致優(yōu)先抓包對比。很多時候是Windows驅(qū)動和Linux驅(qū)動對描述符某些字段的解釋不同導致的抓包一看就能看出來。6. 寫在最后的調(diào)試心得這篇講了架構(gòu)、物理層編碼、包結(jié)構(gòu)、端點模型、四種傳輸類型和枚舉流程其實核心邏輯串起來就一句話USB是一個嚴格的主從式共享總線所有通信都由主機發(fā)起通信的基本單元是事務事務由包組成端點是通信的窗口管道是通信的通道四種傳輸類型的取舍決定了設(shè)備的實時性和可靠性。把這個思維框架建好后面不管是看協(xié)議文檔還是調(diào)Bug你都有了一個定位坐標系——先分清當前是枚舉階段還是數(shù)據(jù)傳輸階段再從“主機/設(shè)備/線纜/Hub”四個環(huán)節(jié)去排查很多問題瞬間就清晰了。我個人的經(jīng)驗是調(diào)USB問題最忌諱的就是“瞎試”。這個板子換一下、那個參數(shù)改一下不如老老實實抓一次包。因為USB協(xié)議雖然復雜但它是完全確定的——總線上每一個包都有明確的目的地和含義問題一定能從抓包里找到蛛絲馬跡。所以下次遇到USB設(shè)備不工作別慌先抓包再說別的。下一篇我們聊USB的class類是怎么定義的尤其是HID和CDC這兩個最常用的類它們的協(xié)議細節(jié)和實際項目里的配置方法。到時候見。