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