
做嵌入式或者驅動開發(fā)的人遲早要跟USB里的兩個核心概念硬碰硬端點和管道。我當年第一次用Wireshark抓USB包看著URB結構里那些endpoint 0x81、pipe 0x00000001之類的字段完全不知道它們是怎么對應到手邊這把U盤和那根串口線上的。后來啃了幾個月的USB 2.0規(guī)范把枚舉流程、端點描述符、管道機制一條條對到抓包數(shù)據(jù)里才算真正搞明白。這篇文章就用最直白的方式把這些東西講透適合正在寫USB固件、調驅動、或者被“USB設備無法識別”折磨的工程師。只要你看過一眼lsusb的輸出或者聽說過CDC虛擬串口、USB轉串口驅動這類東西這篇文章就能幫你把疑惑串起來。1. 端點和管道到底解的是什么問題1.1 一條線纜上塞進幾十條數(shù)據(jù)通道USB物理層其實就是4根線VBUS供電、GND地線加上D和D-一對差分數(shù)據(jù)線。所有設備都掛在同一條總線上靠地址區(qū)分。這就相當于一整棟樓的住戶共享一條走廊快遞員主機不可能同時給所有人送快遞必須按門牌號逐個找。端點和管道就是這套“門牌號”和“投遞線路”的具體實現(xiàn)。主機要跟設備里的某個功能模塊通信比如給虛擬串口發(fā)一個字節(jié)、向U盤寫一塊扇區(qū)總不能把數(shù)據(jù)隨便丟到總線上。數(shù)據(jù)必須有個明確的目的地這個目的地就是設備內部的端點Endpoint。而主機側為了管理這次通信需要建立一條從主機軟件到該端點的邏輯通道這條通道就是管道Pipe。很多新手第一次接觸這組概念時容易混淆端點是硬件實體還是軟件抽象我的理解是端點是設備內部的物理/FIFO/寄存器實體管道的本質是主機側的邏輯綁定兩者通過“端點號方向傳輸類型最大包大小”這組參數(shù)建立對應關系。換句話說端點決定“數(shù)據(jù)送到哪兒”管道決定“怎么送、按什么規(guī)則送”。1.2 管道的核心屬性端點號、方向、類型、包大小USB規(guī)范里管道不像TCP連接那樣需要復雜的握手和狀態(tài)機它更像是一張“配置單”。管道由四個關鍵屬性決定端點號Endpoint Number設備內部的編號比如EP1、EP2范圍是0到15。方向DirectionIN表示設備到主機OUT表示主機到設備。注意這個方向是站在主機視角看的IN就是主機的接收方向。同一個端點號IN和OUT是兩個完全獨立的端點。傳輸類型Transfer Type控制、批量、中斷、等時四種決定了管道的調度規(guī)則。最大包大小wMaxPacketSize管道里每個包最多能裝多少字節(jié)這個值直接關系傳輸效率和枚舉成敗。把這些屬性組合起來主機才能為一個端點創(chuàng)建一條正確的管道。你可以在Linux里用lsusb -v看到每個端點描述符也可以在Wireshark抓包時看到URB里攜帶的端點號和方向——這就是管道在主機軟件側留下的“影子”。2. 四種傳輸類型對應四種不同脾氣2.1 控制傳輸先聊兩句再干正事控制傳輸是所有USB設備必須具備的傳輸方式它走的是端點0也就是默認控制管道。控制傳輸?shù)奶攸c是雙向、可靠、帶寬有限專門用來做設備枚舉和命令控制。每次控制傳輸分為三個階段Setup階段主機發(fā)送8字節(jié)請求、Data階段根據(jù)請求類型決定數(shù)據(jù)方向、Status階段完成握手。比如主機發(fā)送GET_DESCRIPTOR請求讀取設備描述符就是通過這條控制管道完成的。全速設備的控制端點最大包大小可以是8、16、32或64字節(jié)高速設備固定為64字節(jié)。這個值寫錯了設備可能直接無法被識別??刂苽鬏?shù)挠猛具h不止枚舉。后續(xù)的SET_CONFIGURATION、SET_INTERFACE、類別請求比如CDC虛擬串口設置波特率的SET_LINE_CODING都走這條管道。所以很多虛擬串口上位機在設置波特率時實質就是通過控制管道發(fā)了一組特定請求。2.2 批量傳輸量大管飽但沒準點批量傳輸Bulk Transfer適合傳輸大量數(shù)據(jù)但協(xié)議上不保證帶寬和延遲。U盤讀寫、虛擬串口收發(fā)、USB網(wǎng)卡的數(shù)據(jù)通道基本都是批量傳輸。批量傳輸有CRC校驗和自動重傳機制可靠性非常高。全速批量端點的最大包大小是8、16、32或64字節(jié)高速批量端點固定為512字節(jié)。批量傳輸沒有固定的輪詢間隔主機有空就來取數(shù)據(jù)數(shù)據(jù)量大時可能搶不到帶寬所以它不適合音視頻這類需要實時性的場景。調試批量端點時最常見的現(xiàn)象是設備端FIFO里沒有數(shù)據(jù)主機發(fā)了IN令牌設備返回NAK主機過一會兒再重試。抓包的時候看到一長串NAK基本可以斷定設備FIFO沒填數(shù)據(jù)或者填得太慢。這屬于正常協(xié)議行為不是故障。2.3 中斷傳輸我可是有出勤時間的中斷傳輸Interrupt Transfer名字容易誤導人它其實也是主機主動輪詢只不過規(guī)定了固定的輪詢間隔。鍵盤、鼠標、游戲手柄這類設備用中斷傳輸因為它們的數(shù)據(jù)包小、對延遲敏感。端點描述符里的bInterval字段決定輪詢間隔。低速和全速設備直接以毫秒為單位比如bInterval10表示10毫秒輪詢一次高速設備則按2的冪次計算公式是2^(bInterval-1) * 125微秒所以高速中斷端點的實際間隔最小是125微秒。很多人的驅動問題就出在這個字段把高速設備的中斷輪詢間隔寫成10結果實際間隔變成了1280微秒延遲高得離譜。中斷端點的最大包大小也有明確限制低速1到8字節(jié)全速1到64字節(jié)高速1到1024字節(jié)高帶寬模式。2.4 等時傳輸準點發(fā)車丟了不補等時傳輸Isochronous Transfer專門為音視頻這種實時數(shù)據(jù)流設計。它保證帶寬和周期性但不保證可靠性——沒有重傳機制丟包就丟了反正一幀畫面丟了正好趕下一幀。USB攝像頭、USB聲卡、USB音頻設備幾乎都用等時端點。全速等時端點的最大包大小是1到1023字節(jié)高速等時端點則是1到1024字節(jié)并且每個微幀可以執(zhí)行多次事務通過wMaxPacketSize的高位字段指示。等時端點的描述符比普通端點復雜它的bmAttributes字段里還細分了同步類型異步、自適應、同步和用法類型數(shù)據(jù)、反饋、隱式反饋這些參數(shù)對于實現(xiàn)音頻設備特別重要。熱詞里的esp32直接連接usb相機、usb藍牙rgb控制器這類場景最終都會落到端點類型的選擇上攝像頭走等時端點藍牙控制器的HID部分走中斷端點RGB燈的控制通道可能走批量或控制端點具體由固件決定。3. 管道怎么建立從設備插進電腦那一刻說起3.1 默認管道所有USB設備的第一條管道端點0是每個USB設備自帶的特殊端點它是雙向控制端點主機和設備所有枚舉通信都通過這條默認管道完成。設備上電后不需要任何配置端點0就處于可用狀態(tài)。很多人對“默認管道”這個詞沒有概念。你可以把它想象成新員工入職時的接待窗口不管你是誰第一天報到都是先到這個窗口填表、領工牌、分配到工位。USB設備也一樣任何設備必須先通過端點0完成“報到”其余端點管道才能在配置完成后投入使用。因為端點0是所有USB設備通信的起點它的最大包大小bMaxPacketSize0必須寫對。這個值在設備描述符的偏移7處主機第一次讀取設備描述符時只能讀到一個包的長度所以如果這個值錯了整個枚舉就直接失敗了。熱詞里那些“stm32無法識別usb設備”“usb設備無法識別”的案例很多問題都出在這個初始握手環(huán)節(jié)。3.2 一次完整的枚舉過程把枚舉過程完整走一遍比背規(guī)范有效得多設備插入主機檢測到D/D-電平變化判斷設備是全速、高速還是低速。主機對總線復位設備回到默認地址0。主機通過默認管道端點0向地址0發(fā)送GET_DESCRIPTOR請求設備返回設備描述符前8字節(jié)其中就包含bMaxPacketSize0。主機發(fā)送SET_ADDRESS請求給設備分配一個1到127的地址。主機用新地址再次讀取完整設備描述符和配置描述符配置描述符里包含接口和端點信息。主機發(fā)送SET_CONFIGURATION選擇某個配置。配置生效后該配置下所有非默認端點的管道才真正可以使用。注意第6步很關鍵即使端點描述符里寫得清清楚楚只要主機不執(zhí)行SET_CONFIGURATION這些端點是不會被激活的。很多設備固件在枚舉后忘了準備端點接收數(shù)據(jù)上位機一連接就超時就是因為設備端認為“配置過了”實際上固件并沒有正確初始化端點緩沖區(qū)。3.3 配置、接口、端點三級結構決定管道數(shù)量管道不是隨便建立的它受制于USB的描述符層級結構一個設備可以有多個配置Configuration一個配置下有多個接口Interface一個接口下可以有多個端點Endpoint。這個層級結構在驅動開發(fā)里非常重要。比如標準的CDC ACM虛擬串口設備通常有兩條接口接口0是通信接口里面有一個中斷端點用于上報線路狀態(tài)接口1是數(shù)據(jù)接口里面有一個批量IN端點和一個批量OUT端點。Windows的usbser.sys驅動會按接口類和端點類型自動匹配然后把批量端點映射成串口的讀寫信道。如果固件里的接口數(shù)量、端點數(shù)量或者端點屬性與驅動預期不符就會出現(xiàn)“設備能被識別但驅動無法啟動”或“設備感嘆號”的現(xiàn)象。我遇到過不少國產(chǎn)USB轉串口模塊的固件把數(shù)據(jù)接口的端點寫成了中斷傳輸結果在Linux下被識別為HID設備數(shù)據(jù)收發(fā)一塌糊涂。所以認清三級結構是配置管道的第一步。4. 實操把端點和管道“抓出來”看看4.1lsusb -v一行命令看到所有端點在Linux下枚舉信息最好用的工具就是lsusb。插上任意一個USB設備先用lsusb找到它的Bus和Device編號再執(zhí)行詳細查詢。lsusb # Bus 001 Device 012: ID 0483:5740 STMicroelectronics Virtual COM Port lsusb -d 0483:5740 -v假設你插的是一塊STM32做的CDC虛擬串口輸出里會有類似這樣的關鍵片段Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0043 bNumInterfaces 2 ... Interface Descriptor: bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 2 Communications ... Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt wMaxPacketSize 0x0008 8 bytes bInterval 10 Interface Descriptor: bInterfaceNumber 1 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 10 CDC Data ... Endpoint Descriptor: bEndpointAddress 0x81 EP 1 IN bmAttributes 2 Transfer Type Bulk wMaxPacketSize 0x0040 64 bytes bInterval 0 Endpoint Descriptor: bEndpointAddress 0x01 EP 1 OUT bmAttributes 2 Transfer Type Bulk wMaxPacketSize 0x0040 64 bytes bInterval 0bEndpointAddress的0x81怎么讀低4位是端點號1bit4是方向1即IN合起來就是EP1 IN。0x01則是EP1 OUT。bmAttributes2表示批量傳輸3表示中斷傳輸。wMaxPacketSize0x0040即64字節(jié)。這些字段直接對應管道的四個關鍵屬性??催@些輸出時我建議你把描述符層級跟實物對應起來一個配置描述符管一套“供電模式和工作狀態(tài)”一個接口描述符管一種“設備功能”端點描述符則定義了這個功能里的數(shù)據(jù)通道??炊嗔艘院笠谎劬湍芘袛噙@個設備掛出來的是串口、網(wǎng)卡還是攝像頭。4.2 用Wireshark抓USB流量把管道里的包看穿想真正理解管道機制靠描述符還不夠必須抓包。Linux下先加載usbmon模塊Windows下裝USBPcapWireshark都能直接識別USB流量。sudo modprobe usbmon # 打開Wireshark選擇 usbmon0 或對應的usbmonX接口抓包時重點關注幾個字段URB id唯一標識一次請求Type表示URB類型Transfer Type表示傳輸類型Endpoint表示端點號方向通過Direction字段區(qū)分IN/OUT。我舉一個真實排障經(jīng)歷調試一個自研STM32虛擬串口上位機能打開端口但發(fā)送數(shù)據(jù)后設備毫無反應。用Wireshark抓包發(fā)現(xiàn)主機確實在往EP1 OUT發(fā)出BULK數(shù)據(jù)但設備側一直沒有返回ACK反復出現(xiàn)NAK。順藤摸瓜查到固件里根本沒寫EP1_OUT的中斷處理函數(shù)設備端點緩沖區(qū)沒有及時讀走數(shù)據(jù)主機發(fā)多少都被NAK擋回去。改完中斷處理函數(shù)數(shù)據(jù)立刻通了。這是典型的“管道已建立、但設備端不受理”問題不抓包很難定位。CTF比賽里的.pcapng usb ctf流量包也是同一個道理。很多題就是鍵盤或鼠標的USB抓包你要從中斷端點里把HID報文還原成按鍵值遇到U盤鏡像題則要分析批量端點里的SCSI命令和扇區(qū)數(shù)據(jù)。掌握端點管道視角處理這類題目會輕松很多。4.3 端點描述符逐字節(jié)解讀結合原始描述符數(shù)據(jù)看印象更深刻。標準端點描述符固定7字節(jié)比如原始byte流07 05 81 02 40 00 00逐字節(jié)解釋07描述符長度固定7。05描述符類型0x05表示端點描述符。81bEndpointAddress。低4位是端點號1bit4為1表示IN方向。02bmAttributes。低2位為2表示批量傳輸。40 00wMaxPacketSize小端表示0x0040即64字節(jié)。00bInterval。批量傳輸沒有固定輪詢間隔所以為0。如果換成中斷端點可能是07 05 82 03 08 00 0A第5字節(jié)03表示中斷傳輸?shù)?字節(jié)08表示最大包大小8字節(jié)第7字節(jié)0A表示輪詢間隔10毫秒。自己寫固件的時候我習慣把配置描述符數(shù)組里的這些字節(jié)挨個核對一遍尤其注意wMaxPacketSize的字節(jié)序和bInterval是否填反了。很多“設備無法識別”其實就是這里字節(jié)寫錯導致主機解析配置描述符時直接崩掉。別問我怎么知道的。5. 真實排障那些被端點和管道坑到崩潰的案例5.1 STM32虛擬串口能枚舉但發(fā)不出數(shù)據(jù)用STM32做CDC虛擬串口是最典型的入門項目但很多人栽在同一個坑設備管理器識別出了COM口、驅動也正常但上位機發(fā)數(shù)據(jù)設備收不到或者設備發(fā)數(shù)據(jù)上位機收不到。先確認管道兩端的配置用lsusb -v或抓包查看數(shù)據(jù)接口的批量IN/OUT端點是否正確存在。如果確認端點描述符沒問題用Wireshark抓包看BULK OUT的URB到達設備后是否返回NAK。如果一直是NAK說明設備端的端點FIFO沒被及時取走大概率是固件里沒寫OUT端點中斷處理函數(shù)或寫錯了回調函數(shù)入口。如果看到URB正常ACK但應用層收不到數(shù)據(jù)那就是中斷回調到應用層的通知機制斷了屬于代碼邏輯問題。反過來設備發(fā)不出數(shù)據(jù)先檢查BULK IN端點是否在主機發(fā)IN令牌之前就已經(jīng)把數(shù)據(jù)放進FIFO。USB規(guī)范里來自設備的IN事務是主機主動發(fā)起的設備FIFO里沒數(shù)據(jù)就會自動回NAK主機就會一直重試。解決辦法是把數(shù)據(jù)準備好再調用端點發(fā)送函數(shù)讓FIFO在IN令牌到達前處于“有貨”狀態(tài)而不是等收到IN令牌才去準備數(shù)據(jù)。5.2 “USB設備無法識別”先查端點0Windows下最常見的“無法識別的USB設備”十有八九是枚舉階段就失敗了跟系統(tǒng)缺不缺驅動關系不大。枚舉全靠端點0這條默認管道所以排查重點應該放在端點0相關的描述符和時序上。先查bMaxPacketSize0是否正確。全速設備這個值必須與固件實際配置的端點0包大小一致高速設備必須寫64。再查設備描述符里的bcdUSB版本號有些老固件在USB 2.0主機上填了USB 1.1版本雖然也能跑但可能導致主機對端點0包大小有錯誤預期。最后檢查SET_ADDRESS之后的響應時序地址改完后設備必須在主機新地址下正常響應不能仍用地址0應答。如果抓包結果顯示主機發(fā)了GET_DESCRIPTOR但設備一直沒有ACK大概率是設備固件卡在某個初始化流程里沒有進入應答狀態(tài)或者端點0的發(fā)送邏輯沒寫好。注意控制傳輸?shù)腄ata階段可能是IN也可能是OUT設備MCU代碼里要區(qū)分處理別只寫了發(fā)送沒寫接收。5.3 USB轉串口裝不上驅動可能是芯片沒枚舉成功網(wǎng)上搜ft231x usb uart驅動、ch340驅動下載這類詞的場景通常是插上USB轉串口線系統(tǒng)提示“Unknown Device”。很多人第一反應是下驅動但實際上驅動只解決“識別后再匹配”的問題如果枚舉都失敗裝什么驅動都沒用。CH340、FT231X、CP2102這些芯片本質上都是帶端點的USB設備它們的BULK端點負責收發(fā)串口數(shù)據(jù)控制端點負責配置波特率。芯片通電后如果晶體沒起振、D/D-上的上拉電阻不對、或者PCB走線導致信號質量差枚舉就會失敗。這時候應該先換一根線、換一個USB口、甚至用示波器量一下D/D-波形而不是反復重裝驅動。同理熱詞里的周立功usb轉canfd接口卡、usb轉485驅動、usb轉iic這些工具內部都是不同芯片公司做的USB橋接設備有的遵循CDC ACM類有的是私有類。遇到裝不上驅動先確定它在設備管理器里是什么狀態(tài)如果是“Unknown Device”走枚舉排查如果有設備名但感嘆號那才輪到驅動版本或PID/VID匹配問題的排查。5.4 虛擬機掛USB設備說到底是整個設備樹的映射熱詞里還有個有意思的方向給物理服務器掛USB硬盤、ESXi掛載USB硬盤、怎么把USB掛載到指定虛擬機上。這個場景跟端點管道有什么關系虛擬化平臺把物理USB設備“直通”給虛擬機時本質上就是把設備的整個描述符結構配置、接口、端點映射給虛擬機里的USB控制器虛擬機里的操作系統(tǒng)和驅動面對的就是真實設備的端點和管道。如果虛擬機里識別不到設備先檢查虛擬機的USB控制器是否開啟再檢查宿主機的USB設備是否已被其他占用。設備直通后虛擬機里的驅動匹配邏輯和物理機完全一樣照樣看PID/VID和接口描述符。5.5 USB抓包時看到一個很長的管道字段怎么讀有時候Wireshark里會看到pipe字段像0x0000000b這種十六進制數(shù)。它其實是主機側USB控制器驅動給這條管道分配的句柄具體數(shù)值由控制器驅動決定不是USB規(guī)范定義的標準字段。如果你需要在Wireshark里按端點篩選直接用usb.endpoint_number、usb.transfer_type、usb.direction這些標準字段更準確。6. 別把這三個“管道”混在一起6.1 USB管道、匿名管道、物理管道“USB端點管道”這個標題搜索量大有很大一部分原因是“管道”這個詞在多領域撞車。經(jīng)常跟USB一起被搜出來的還有操作系統(tǒng)里的匿名管道Anonymous Pipe、命名管道Named Pipe以及管道機器人、燃氣管道圖像數(shù)據(jù)集、水下管道裂縫數(shù)據(jù)集這些完全不同的物理管道。USB協(xié)議里的管道Pipe是主機與設備端點之間的邏輯通信通道它不是一個實體。Windows/Linux里的匿名管道是進程間通信機制一個進程往里寫、另一個進程往外讀數(shù)據(jù)只在內存里流轉。物理管道更不用說了鋼管、PE管、PVC管那是真的用來送水送氣的。三者完全不是一回事但搜索引擎只認關鍵詞所以總被攪在一起。6.2 “端點”也有同名歧義“端點”這個詞也容易誤傷。ARCgis里標志地塊端點坐標那是地理信息里的多邊形節(jié)點坐標端點星工程助手是某個軟件系統(tǒng)里的服務端點概念Docker、微服務架構里常說的“服務端點”Endpoint指IP地址加端口。做USB開發(fā)時說的“端點”特指USB設備內部的數(shù)據(jù)傳輸節(jié)點。聽到別人說“服務端點不可達”或者“配置管理端點”得先分清上下文別一上來就套USB那一套。遇到這種同名不同義的情況我的習慣是先在腦子里把領域范圍框死凡是涉及D/D-信號、設備描述符、URB、lsusb、usbmon的都是USB協(xié)議域凡是涉及socket、HTTP URL、服務發(fā)現(xiàn)的是網(wǎng)絡協(xié)議域凡是涉及進程間通信、重定向流水線的是操作系統(tǒng)概念。域定了概念自然就不亂了。寫到最后分享一點個人經(jīng)驗新手學USB最忌諱一上來就背規(guī)范。最好的路徑是先抓一次枚舉過程的包從GET_DESCRIPTOR到SET_CONFIGURATION一步一步對照著看再拿一個USB轉串口模塊用lsusb -v把它的端點描述符翻出來然后故意改一個參數(shù)比如把端點0的包大小寫錯觀察枚舉失敗的現(xiàn)場。這套流程走完端點、管道、傳輸類型、描述符層級這些東西就不再是抽象名詞了。我現(xiàn)在拿到一個不認識的USB設備第一件事就是抓枚舉包看它掛出來幾個接口、哪些端點、什么傳輸類型心里就有數(shù)了。這套路子強烈建議你也試試。