前言
從一無所知到開發USB設備,需要經歷怎樣的程序?
????我剛接觸USB模塊時,有無從下手的感覺,經過“摸石頭過河”式的學習后,才算有了大致概念,雖說USB檔案齊全、原理詳實,但入門還是有一定的門檻,因此,我把自己從零開始的學習USB的程序記錄分享,希望能給USB這條大河搭個橋,以供參考,本文提供一種自上而下的學習程序,無意深刻剖析直達底層原理,只盼所述能使人對完整的USB知識體系有清晰的架構認知,
理論學習
本章將由淺入深介紹USB原理,逐步解釋以下問題:
????第一節:USB從接入到使用,講述USB設備接入主機后經歷了哪些程序;
????第二節:USB通信程序,解釋USB設備和主機之間如何通信;
????第三節:從機的屬性,介紹如何區分不同型別的USB設備;
????第四節:列舉的詳細程序,概括主機認識USB設備的具體程序;
另外,下文將用主機/從機統一描述USB主機和USB設備:
????主機:USB主機(Win/Android/Mac等)
????從機:USB設備(滑鼠/鍵盤/U盤等)
USB從接入到使用
????主機發現從機接入后,開始識別從機,成功識別后就可以使用從機的功能了,其中,發現從機接入/拔出的程序稱為USB拔插,識別從機的程序稱為列舉,
USB拔插:主機發現從機的接入/拔出
【摘要】主機通過檢測USB D+/D-的電平變化感知從機接入/拔出,
一般USB介面包含4根線(OTG為5根),分別是:Vcc, D+, D-, GND,如圖所示: 
????主機端D+/D-下拉15KΩ電阻到GND(0V),從機端D+/D-上拉1.5KΩ電阻到3.3V,當從機接入主機時,D+/D-上的電壓變為3V,雙方通過電平變化就可以發現USB的拔插事件,
USB列舉:主機認識從機的方式
【摘要】主機通過獲取設備的描述符集合來識別USB設備,這個程序稱為“列舉”,
????USB設備(從機)的型別非常多,常見的有滑鼠、鍵盤、游戲手柄等USB HID(Human Interface Device)設備,串口除錯的CDC(Communication Device Class)設備,User自定義傳輸內容的WINUSB設備等,那么主機如何區分這些USB設備呢?
????因此,每個USB設備都必須有一個描述符集合,這個集合詳細描述了從機的所有功能和用途,USB連接后,主機通過訪問描述符集合來識別從機并配置從機(列舉程序),就可以根據從機提供的資訊使用從機的功能,
USB使用:主機使用從機的功能
【摘要】從機以等待主機輪詢的方式發資料,以中斷的方式收資料,從而實作相應的功能,
????列舉成功后,從機開始履行自己的職責,以滑鼠為例,列舉后它向主機發送報告(Report)來控制游標移動、點擊,但從機并非任意時刻都能發送資料,它必須等主機已經準備好通信了才開始發送,因此,從機準備好發送的資料后必須進入等待,直到主機輪詢到此功能時,才開始發送,假設從機可以任意觸發資料的發送程序,且主機連接多個從機,那么當多個從機同時發送資料到主機的USB總線上時就會引發沖突,
????反之,當主機需要發送資料時,從機必須盡快接收,所以從機一般會用中斷處理主機發送資料的請求,這是因為主機需要輪詢很多從機,每次輪詢都有固定的時間,超時后就通信失敗了,
【Q】從機發送/接收資料,主機發送/接收資料是否容易概念混淆?
【A】 是的,因此USB的資料傳輸程序描述以主機端為主,“從機–>主機”(Device-to-host) 方向的資料傳輸稱為輸入(Data In),“從機<–主機”(Host-to-device) 方向的資料傳輸為輸出(Data Out),
【Q】主機輪詢到從機的輸入功能時,沒有資料要發送怎么辦?
【A】 當然是PASS,從機直接回復NAK(即沒有資料)或STALL(設備掛起),
USB通信程序
主機如何訪問指定USB設備?
【摘要】主機為所有從機分配唯一的設備地址,通過該地址來訪問從機,
????以PC為例,一般PC的USB設備可能包括滑鼠、鍵盤、HUB擴展塢、藍牙/WiFi配接器等,那么假如PC想訪問滑鼠設備時,該如何實作呢?

????答案是設備地址,主機給所有已連接的從機分配設備地址,并確保不會重復,對剛接入還沒來得及分配地址的從機,主機使用默認地址<Addr0>與之通信,交換少量的資訊后,主機分配新地址,然后雙方用新地址(Addr1~AddrN)通信,
【Q】列舉成功后,從機再次拔插還可以用之前分配的地址通信嗎?
【A】 主機會重新分配設備地址,但可能分配的碰巧就是之前的地址,
【Q】主機為分配地址前,如何與從機通信?
【A】 USB規定,對于剛接入的從機,主機用默認地址(Addr0)通信,
主機如何訪問指定USB設備的指定功能?
【摘要】主機通過<設備地址(Address),設備端點(Endpoint)>訪問指定從機的指定介面(功能),
????假設設備A是USB復合設備,同時支持滑鼠、鍵盤、CDC功能,那么主機給設備A分配設備地址后,如何訪問從機A的其中一個功能(比如鍵盤功能)?且當這個鍵盤功能同時支持發送和接收資料時,如何避免收發沖突呢?

????答案是用端點(Endpoint,EP)加以區分,主機通過設備地址找到從機后,再通過端點訪問從機的指定功能的指定用途,端點具有唯一性,它們和從機的功能及用途一一對應,按照端點的屬性構建專用的端點通道(Pipe)來通信,另外,端點還標識了特定用途的資料傳輸方向,因此,對于USB復合設備A,通過端點號可區分鍵盤功能的發送或接收,
【Q】有多少個功能/用途就分配多少個設備地址不就可以了嗎?
【A】 如果這么做,當主機接入多個USB設備,而每個USB設備又支持多種功能、每個功能又包含多個用途時,主機需要分配的地址數量非常之多,且每次拔插設備需要多次分配地址,最終通信效率變低了,
【Q】主機未識別從機的功能之前用什么端點通信?
【A】 與默認地址0一樣,從機也會有默認端點0(Default Endpoint, EP0),準確來講,對初次接入的從機,雙方通過<Addr0,EP0>進行通信,
主機、從機如何讀/寫資料
【摘要】主機用默認端點0(EP0)創建通道列舉從機,根據描述符集中的其他端點創建對應通道訪問其他功能,
????首先,從機必須支持默認端點EP0,對剛接入的從機,主機使用<Addr0, EP0>訪問從機,創建EP0的端點通道,開始列舉并分配地址,然后使用<new Addr, EP0>重新列舉,列舉成功后,主機根據從機提供的資訊創建相應的資源和通道,訪問從機的功能,
????當然,從機的功能多種多樣,可能要持續傳輸大量資料,也可能要求實時性高,或是偶爾傳輸資料等,那么訪問的需求不一樣,主機怎么區分呢?
????當然是給端點加上屬性(Attribute),在端點描述符中宣告屬性,可以告訴主機構建什么樣的資料通道,以何種方式讀/寫資料,
一次完整的通信程序
【摘要】一次完整的通信分為三個程序:請求程序(令牌包)、資料程序(資料包)和狀態程序(握手包),沒有資料要傳輸時,跳過資料程序,
????通信程序包含以下三種情況:

????主機發送令牌包(Token)開始請求程序,如果請求中宣告有資料要傳輸則有資料程序,最后由資料接收方(有資料程序)或從機(無資料程序)發起狀態程序,結束本次通信,
????與USB全速設備通信時,主機將每秒等分為1000個幀(Frame),主機在每幀開始時,向所有從機廣播一個幀起始令牌包(Start Of Frame,SOF包),它的作用有兩個:一是通知所有從機,主機的USB總線正常作業;二是從機以此同步主機的時序,
????與USB高速設備通信時,主機將幀進一步等分為8個微幀(Microframe),每個微幀占125
μ
\mu
μs,在同一幀內,8個微幀的幀號都等于當前SOF包的幀號,
注意: 下文所有USB包結構均不包括前導碼(同步碼),
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Token_SOF_t{
uint8_t bPID; // 0xA5, SOF(0101B)
uint16_t b11FrameID:11; // 幀號
uint16_t b5CRC:5; // wFrameID欄位(11bit)的CRC校驗碼
}USB_Token_SOF_t;
【Q】為什么PID是4bit的,欄位長度卻有8bit?
【A】 因為PID欄位高4bit是低4bit的校驗位:pid(i+4) = ~pid(i),
【Q】為什么CRC不校驗PID欄位?
【A】 因為PID欄位本身帶有校驗位,
請求程序(請求包)
????主機廣播SOF包之后,會發送帶有地址和端點資訊的令牌包(Token) 來指定要訪問的從機,分別有:建立令牌包(SETUP)、輸出令牌包(OUT)、輸入令牌包(IN),
????這三種令牌包統稱為請求包,結構如下:
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Token_t{
uint8_t bPID; // 0xE1, OUT (0001B);
0x69, IN (1001B);
0x2D, SETUP (1101B);
uint16_t b7Addr:7; // 要訪問的設備地址
uint16_t b4Endpoint:4; // 要訪問的端點號
uint16_t b5CRC:5; // wFrameID欄位(11bit)的CRC校驗碼
}USB_Token_t;
????主機可以通過請求包指定要訪問的從機,發起請求程序,配置從機或指示從機準備發送/接收資料,在列舉程序中,主機使用SETUP包請求從機的資訊,列舉成功后,主機使用IN包請求輸入資料,OUT包請求輸出資料,
????列舉時,在SETUP包的后面會緊跟一個8B長度的請求(Request),用于描述主機的具體意圖,結構如下:
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Request_t{
uint8_t bmRequestType; // 請求型別
uint8_t bRequest; // 具體請求,參考USB 2.0 Spec Chapter 9.4
uint16_t wValue; // 內容和Request有關
uint16_t wIndex; // 內容和Request有關
uint16_t wLength; // 資料程序可傳輸的最大位元組數
}USB_Request_t;
typedef struct _bmRequestType_t{
uint8_t b5Recipient:5; // 0 = Device, 1 = Interface
2 = Endpoint, 3 = Other
4..31 = Reserved
uint8_t b2Type:2; // 0 = Standard, 1 = Class
2 = Vendor, 3 = Reserved
uint8_t b1Direction:1; // 0 = Host-to-device
1 = Device-to-host
}bmRequestType_t;
????在“請求程序”階段,被訪問的從機會接收并決議請求,若wLength欄位不為0,則進入資料程序,否則進入狀態程序,
【Q】從機收到不支持的請求怎么辦?
【A】 可以直接進入狀態程序,從機發送STALL包,
【Q】有了IN/OUT包,為什么還要在請求中宣告傳輸方向(Direction)?
【A】 IN/OUT包后面不會帶有請求,從機在收到IN/OUT包后直接進入資料程序,發送資料或回復NAK(沒有資料要發送),
資料程序(資料包)
????請求的bmRequestType欄位中,Direction標志位宣告了資料要傳輸的方向,
????當請求為輸出(Data OUT,Direction = 1)時,從機接收不超過wLength欄位中宣告長度的資料,并根據請求的內容決議接收到的資料;當請求為輸入時(Data IN,Direction = 0)時,從機根據請求的內容發送對應的資料(不超過wLength中宣告的長度),
????資料包(Data Packets)的結構如下:
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Data_Packet_t{
uint8_t bPID; // 0xC3, DATA0 (0011B); even
0x4B, DATA1 (1011B); odd
0x87, DATA2 (0111B); for usb high speed
0x0F, MDATA (1111B); for usb high speed
uint8_t bData[]; // 0 ~ 8192B
uint16_t wCRC16; // bData欄位的CRC校驗碼
}USB_Data_Packet_t;
【Q】為什么要分DATA0和DATA1?
【A】 在USB全速設備中,資料包以DATA0、DATA1的PID交替發送,當接收方連續收到兩個PID相同的DATA包時,就知道丟包了,而DATA2與MDATA則是USB高速設備所使用的PID,參考《USB 2.0 Spec》Chapter 5.9.2,
狀態程序(握手包)
????進入狀態程序后,發送的包是握手包(Handshake Packets),結構如下:
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Handshake_t{
uint8_t bPID; // 0xD2, ACK (0010B); 確認接收
0x5A, NAK (1010B); 沒有資料要回傳
0x1E, STALL (1110B); 無法執行的請求
0x96, NYET (0110B); 接收成功但無法
接收下一次資料,僅在usb高速設備中使用,下
次主機發送資料需要先發送PING包試探設備,
}USB_Handshake_t;
????沒有資料程序時,握手包的發送方是從機;
????資料程序為Data Out時,握手包的發送方是從機;
????資料程序為Data In時,握手包的發送方是主機;
????當然,除了上述USB包,還有特殊包(Special Packets):PING(0100B) / SPLIT(1000B) / PRE(1100B) / ERR(1100B),這些特殊包的作用參考《USB 2.0 Spec》Chapter 8,
通信例外
????當從機還沒準備好時主機請求資料;從機收到未知請求;端點通信資料量溢位;主機不應發送的請求;或沒有資料要發送等情況時,本輪通信會直接進入狀態程序,從機發送NYET/ERR/STALL/NAK包,
????當資料傳輸出錯時,資料的發送方停止發送資料,直到本次通信超時,
從機的屬性
【摘要】描述符集描述了從機的所有功能細節,它包含唯一的設備描述符,至少一個配置描述符和介面描述符,每個介面描述符至少包含一個端點描述符,此外還有其他可選的特殊描述符進行補充,
????前文提到,主機通過請求從機的描述符集來認識從機,那么描述符集包含了哪些資訊呢?
????描述符集主要包含設備描述符(Device Descriptor)、配置描述符(Configuration Descriptor)、介面描述符(Interface Descriptor)、端點描述符(Endpoint Descriptor)、字串描述符(String Descriptor)及其他描述符,
描述符集的層次結構

????一個USB設備有且僅有一個設備描述符;
????一個設備描述符指向一個(或多個)配置描述符;
????一個配置描述符指向一個(或多個)介面描述符;
????一個介面描述符指向一個(或多個)端點描述符,還可能帶有介面補充描述符;
????上述描述符如果帶有字串索引號(String Index),主機會根據索引號向從機請求對應的字串描述符,進一步提供可供用戶閱讀的資訊,
????對于一些介面(HID/CDC等),配置集合就包含一種介面補充描述符——特殊類描述符,不同的介面補充描述符作用不同,結構也可能不一樣,如HID描述符會宣告報告描述符的存在,由報告描述符進一步補充介面資訊,如果補充描述符中又宣告了其他描述符,主機會按介面索引號單獨向從機請求其他描述符,
????需要注意的是,同一時間從機只能有一個生效的配置集合,生效的配置通過主機選擇(Set Configuration)來指定,因為配置集合“復用”了從機的硬體資源,
????上述描述符中,除其他特殊描述符外主機能夠單獨獲取的只有設備描述符、字串和配置描述符,因為這些描述符是全域有效的,但介面描述符、端點描述符和特殊類描述符是某個配置集合內(區域)生效的,需要補充配置描述符一起發送,事實上,列舉程序中主機會一次性獲取整個配置集合,
設備描述符(Device Descriptor)
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Desc_Device_t {
uint8_t bLength; // 固定值18B
uint8_t bDescriptorType; // 固定值Device(0x01)
uint16_t wBcdUSB; // USB Spec版本
uint8_t bDeviceClass; // 設備型別
uint8_t bDeviceSubClass; // 設備子型別
uint8_t bDeviceProtocol; // 協議型別
uint8_t bMaxPacketSize0; // EP0的最大包長度
uint16_t wIdVendor; // 廠商ID
uint16_t wIdProduct; // 產品ID
uint16_t wBcdDevice; // 設備軟體版本
uint8_t bStringIndexManufacturer; // 廠商名稱字串索引號
uint8_t bStringIndexProduct; // 產品名稱字串索引號
uint8_t bStringIndexSerialNumber; // 序列號索字串引號
uint8_t bNumConfigurations // 配置數量>=1
}USB_Desc_Device_t;
????其中,設備型別、設備子型別、協議型別參考USB IF的定義,EP0最大包長度則為從機默認端點EP0一次可傳輸的最大包的大小,其典型值為64B,早期的USB設備為8B,字串索引號分別對應一個字串,主機用它向從機請求對應的文本資訊,
【Q】Vendor ID和Product ID有什么作用?
【A】 Vendor ID(VID)的商用需要向USB組織申請,開發者可直接使用開發平臺的廠商ID,Product ID(PID)由廠商自行管理,VID和PID的作用是讓主機快速識別某些著名的設備(Windows可以在完成列舉之前依此直接派發驅動),它們也常常作為搜索從機的條件(如libusb),
配置描述符(Configuration Descriptor)
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Desc_Configuration_t {
uint8_t bLength; // 固定值9B
uint8_t bDescriptorType; // 固定值Configuration(0x02)
uint16_t wTotalConfigurationSize; // 配置集合的總大小
uint8_t bTotalInterfaces; // 配置集合的介面數量
uint8_t bConfigurationNumber; // 當前配置的序號(從1開始)
uint8_t bConfigurationStrIndex; // 配置名稱的字串索引號
uint8_t bConfigAttributes; // 配置集合的屬性
uint8_t bMaxPowerConsumption; // 最大供電電流,單位是2mA
}USB_Desc_Configuration_t;
// 配置集合的屬性
typedef struct _bConfigAttributes_t{
uint8_t b5reserved:5; // 保留置0
uint8_t b1RemoteWakeup:1; // 置1表示支持遠程喚醒
uint8_t b1Selfpowerd:1; // 置1表示支持自己供電
uint8_t b1reserved:1; // 保留置1
}bConfigAttributes_t;
????配置集合的總大小是當前配置集合內配置描述符、介面描述符、端點描述符和特殊類描述符的總長度,需注意,如果供電電流為100mA,“bMaxPowerConsumption”欄位的值應當為50,
介面描述符(Interface Descriptor)
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Desc_Interface_t {
uint8_t bLength; // 固定值9B
uint8_t bDescriptorType; // 固定值Interface(0x04)
uint8_t bInterfaceNum; // 介面索引號
uint8_t bAlternateSetting; // 備用介面號
uint8_t bNumberEndpoints; // 端點數量
uint8_t bInterfaceClass; // 介面型別
uint8_t bInterfaceSubclass; // 介面子型別
uint8_t bInterfaceProtocol; // 介面協議
uint8_t bInterfaceStringIndex; // 介面名稱的字串索引號
}USB_Desc_Interface_t;
????其中,介面型別、子型別、介面協議參考USB IF的定義,備用介面號用于宣告另一個可以替代當前介面的備用介面,
端點描述符(Endpoint Descriptor)
#pragma data_alignment=1 //對齊方式為Byte
//參考USB Spec 2.0 Table 9-13
typedef struct _USB_Desc_Endpoint_t{
uint8_t bLength; // 固定值7B
uint8_t bDescriptorType; // 固定值Endpoint(0x05)
uint8_t bEndpointAddress; // 端點地址
uint8_t bmAttributes; // 端點屬性
uint16_t wMaxPacketSize; // 端點支持的最大包大小
uint8_t bInterval; // 輪詢間擱(僅中斷端點有效)
}USB_Desc_Endpoint_t;
// 端點地址
typedef struct _bEndpointAddress_t{
uint8_t b4EndpointNumber:4; // 端點號
uint8_t b3Reserved:3; // 保留置0
uint8_t b1Direction:1; // 傳輸方向(IN/OUT)
}bEndpointAddress_t;
// 端點屬性
typedef struct _bmAttributes_t{
uint8_t b2TransferType:2; // 傳輸型別
** 00 = Control
** 01 = Isochronous
** 10 = Bulk
** 11 = Interrupt
uint8_t b2SynchronizationType:2; // 僅iso傳輸有效
uint8_t b2UsageType:2; // 僅iso傳輸有效
uint8_t b2Reserved:2; // 保留置0
}bmAttributes_t;
????端點支持的最大包大小是端點通道一次可以傳輸的最大資料量,在批量傳輸(bulk transfer)中,超過該值的資料會被分包傳輸,一般來說,如果接收方接收到恰好為最大包長度的資料,則會認為還有資料要傳輸,當然,bulk傳輸的方式本身是可以自定義的,具體行為可以由開發者控制,而在中斷傳輸(interrupt transfer)中,不允許超過最大包長度的資料量傳輸,
批量傳輸(Bulk Transfer)
????批量傳輸是最好理解的,它幾乎沒有什么限制,全看怎么實作,語法、語意都是私有的,它適合需要傳輸大量資料且對資料實時性要求不高的場景,一般來說,傳輸程序中會以傳輸包是否小于最大包長度作為本輪傳輸結束的標志,下文的例程Winusb就是使用這種傳輸方式,具體參考USB Spec 2.0 Chapter 5.8,
控制傳輸(Control Transfer)
????控制傳輸適用于資料量少且對時序有嚴格要求的場景,顧名思義,它就是用來傳輸設備資訊和主機資訊的,所有的從機都必須支持控制傳輸,以便和主機交換資訊,也就是說,從機的默認端點0的型別都是控制傳輸,具體參考USB Spec 2.0 Chapter 5.5,
中斷傳輸(Interrupt Transfer)
????中斷傳輸適用于傳輸資料量少但需要定時詢問的場景,如鍵鼠設備,端點描述符的輪詢間擱欄位宣告了主機兩次訪問之間的最長間擱,具體參考USB Spec 2.0 Chapter 5.7,
同步傳輸(Synchronous Transfer)*
????參考USB Spec 2.0 Chapter 5.6,同步傳輸適合資料量大且實時性要求高的場景,比如音頻傳輸,
【Q】端點EP in 1(0x01)和端點EP out 1(0x81)是同一個端點嗎?
【A】 端點號
≠
\neq
?=端點地址,EP in 1和EP out 1的端點號雖然相同,但傳輸方向不同,構建的端點通道(Pipe)也不同,因此不能認為它們是同一個端點,
字串描述符(String Descriptor)
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Desc_String_t{
uint8_t bLength; // 字串描述符的長度
uint8_t bDescriptorType; // 固定值String(0x03)
wchar_t wUnicodeString[];
}USB_Desc_String_t;
????UnicodeString是wchar_t型字串,如果希望定義設備為"DevName",則需定義L"DevName"(長度16B,包含了停止位L"\0"),bLength欄位的值則為14,
其他描述符
????除了上述基本的描述符,USB設備還會帶有其他特殊的描述符,對設備功能、資訊作進一步補充,以下列舉一些常見的特殊描述符:
設備限定符描述符(Qualifier Descriptor)
#pragma data_alignment=1 //對齊方式為Byte
typedef struct _USB_Desc_Device_Qualifier_t{
uint8_t bLength; // 固定值18B
uint8_t bDescriptorType; // 固定值Device(0x01)
uint16_t wBcdUSB; // USB Spec版本
uint8_t bDeviceClass; // 設備型別
uint8_t bDeviceSubClass; // 設備子型別
uint8_t bDeviceProtocol; // 協議型別
uint8_t bMaxPacketSize0; // EP0的最大包長度
uint8_t bNumConfigurations // 配置數量>=1
uint8_t bReserved; // 保留置0
}USB_Desc_Device_Qualifier_t;
????可以看到,設備限定描述符的結構就是設備描述符的一部分,假如主機和從機正在全速USB的速率通信,結果主機發現從機帶有設備限定描述符,且在描述符中宣告從機支持高速USB通信,那么主機就會復位從機,重新以高速USB的通信速率進行通信,
????
特殊類描述符(Class-specific Descriptor)
????特殊類描述符的結構取決于介面的實際型別,比如HID描述符:
#pragma data_alignment=1 //對齊方式為Byte
//Human Interface Device Descriptor,參考 Device Class Definition for HID 1.11 Chapter 6.2.1
typedef struct _USB_Desc_HID_t{
uint8_t bLength;
uint8_t bDescriptorType;
uint16_t wBcdHID; // 遵循的Hid協議版本
uint8_t bCountryCode; // 國區代碼
uint8_t bNumDescriptors; // 其他特殊描述符的個數
uint8_t bDescriptorType; // 其他特殊描述符的型別,一般為Report(0x22)
uint16_t wDescriptorLength; // 其他特殊描述符的長度
(optional)uint8_t bDescriptorType;
(optional)uint16_t wDescriptorLength;
...
}USB_Desc_HID_t;
//Hub Descriptor,參考 USB Spec 2.0 Chapter 11.23.2
typedef struct _USB_Desc_Hub_t{
uint8_t bDescLength;
uint8_t bDescriptorType;
uint8_t bNbrPorts;
uint16_t wHubCharacteristics;
uint8_t bPwrOn2PwrGood;
uint8_t bHubContrCurrent;
uint8_t abDeivceRemovable[];
uint8_t abPortPwrCtrlMask[];
}USB_Desc_Hub_t;
功能描述符(Functional Descriptor)
????以下功能描述符的通用結構:
#pragma data_alignment=1 //對齊方式為Byte
//參考 CDC120-20101103-track Chapter 5.2.3
typedef struct _USB_Desc_Functional_t{
uint8_t bFunctionLength;
uint8_t bDescriptorType;
uint8_t bDescriptorSubType;
uint8_t abFunctionSpecificData[]; // data[0] ~ data[N - 1]
}USB_Desc_Functional_t;
物理描述符(Physical Descriptor)
????參考Device Class Definition for HID 1.11 Chapter 6.2.3,
微軟系統描述符(Microsoft OS Descriptor)
????微軟系統描述符是由微軟定義的,參考Microsoft docs,
列舉的詳細程序
????①USB設備接入后,主機復位從機,使用<addr0, EP0>構建端點通道(Pipe)請求設備描述符,從機發送完整的設備描述符或只發送前8B內容(當EP0最大包長度只有8B);
????②主機分配唯一的設備地址并發送Set Address請求,收到應答后再次復位從機;
????③主機再次請求完整的設備描述符,當一次請求不足以獲取完整的描述符,主機會請求多次;
????④主機請求完整的配置描述符;
????⑤根據設備描述符和配置描述符中宣告的字串描述符索引號,請求所有字串描述符;
????⑥(可選)主機請求限定符描述符,當描述符中宣告了支持更高速的USB協議時,主機復位從機,用新的USB協議重新列舉從機,當獲取描述符失敗時,認為從機不支持此功能,按原協議重新列舉并跳過此步驟;
????⑦根據配置描述符中宣告的集合長度,請求配置集合,其中配置集合包括配置描述符、介面描述符、端點描述符以及特殊類描述符,當從機包含多個配置描述符集合時,會多次請求,
????⑧主機請求選擇配置(Set Configuration);
????⑨主機選擇介面,請求介面空閑狀態(Set Idle),此時介面生效,根據介面描述符,可能會請求其他的特殊描述符(一般這些描述符是對介面描述符的補充描述),如果從機包含多個介面,此步驟會重復多次;
????⑩主機知道USB設備的型別、通信方式和作業方式后,采用恰當的對策輪詢USB設備,在Windows平臺,主機完成列舉后會給從機派發相應的驅動(符合官方支持的設備標準)或者不派發驅動(找不到對應驅動,需要手動安裝),
USB常用的除錯工具和SDK
為了印證上述理論和除錯開發,以下是我們常用到的USB工具/SDK:
【USB View】
????用于查看從機描述符集,Windows SDK Debugger工具之一,
【Bus Hound】
????記錄主機與從機之間傳輸的資料(包括列舉)的工具,但它并不統計所有資料,比如部分被NAK回復的主機請求不會被記錄,
【Libusb】
????用戶在主機端直接訪問從機的開源庫,但Windows下使用libusb訪問從機時,需確保從機不是復合設備(windows下libusb沒有訪問復合設備的權限,且libusb會直接使用復合設備的第一個功能),還要確保windows也給從機分發了驅動“winusb.sys”,可以使用Zadig工具手動給設備安裝驅動(當然,從機必須是winusb設備),
【MichaelTien8901/STM32WINUSB】
????WINUSB設備開發,參考這位仁兄把STM32例程中的USB CDC改成WINUSB的做法,
【STM32CubeMx】
????用STM32平臺開發,可以用官方工具直接生成USB HID/CDC的例程,
例程1:WINUSB設備
????Winusb設備的實作相對簡單,也很好理解,初學者可以嘗試開發winusb設備,對usb列舉和bulk傳輸也會有一個比較清晰的印象,
????需要注意的是,本文給出的所有USB包結構均按char型對齊,使用這些結構開發時需注意,
以下是winusb設備常見的描述符集:
#pragma data_alignment=1 //對齊方式為Byte
const USB_Desc_Device_t stDevWinusb = {
0x12, // sizeof(USB_Desc_Device_t)
0x01, // descriptor type: device
0x0200, // USB Spec 2.0
0x00, // no device class
0x00, // no device subclass
0x00, // no device protocol
0x40, // max ep0 packet size: 64B
0x1234, // vendor id
0x5678, // product id
0x0001, // product release number
0x01, // manufacturer string index
0x02, // product string index
0x03, // serial number string index
1 // configuration numbers
};
typedef struct _USB_Winusb_Configuration_t{
USB_Desc_Configuration_t stDescConfiguration;
USB_Desc_Interface_t stDescInterface;
USB_Desc_Endpoint_t stDescEndpointIn;
USB_Desc_Endpoint_t stDescEndpointOut;
}USB_Winusb_Configuration_t;
const USB_Winusb_Configuration_t stConfWinusb = {
// configuration descriptor
{
0x09, // sizeof(USB_Desc_Configuration_t)
0x02, // descriptor type: configuration
0x0020, // sizeof(USB_Winusb_Configuration_t)
0x01, // interface numbers
0x01, // configuration index
0x00, // no configuation string
0x80, // no attributes
0x32 // max power: 50*2 = 100 mA
},
// interface descriptor
{
0x09, // sizeof(USB_Desc_Interface_t)
0x04, // descriptor type: interface
0x00, // index of interface
0x00, // no alternate setting
0x02, // endpoint numbers: 2
0xFF, //Interface Class: Vendor defined
0x00, //Interface Subclass: none
0x00, //Interface Protocol: none
},
// endpoint descriptor
{
0x07, // sizeof(USB_Desc_Endpoint_t)
0x05, // descriptor type: endpoint
0x81, // endpoint in 1
0x02, // transfer type: bulk
0x40, // max packet size: 64B
0x00, // useless in bulk
},
// endpoint descriptor
{
0x07, // sizeof(USB_Desc_Endpoint_t)
0x05, // descriptor type: endpoint
0x01, // endpoint out 1
0x02, // transfer type: bulk
0x40, // max packet size: 64B
0x00, // useless in bulk
}
}
//當主機請求index為 manufacturer string index(0x01)的字串時
USB_Desc_String_t stVendorStr = {
0x1A, // 1 + 1 + sizeof(L"SampleVendor") - 2
0x03, // descriptor type: string
L"SampleVendor"
}
//當主機請求index為 product string index(0x02)的字串時
USB_Desc_String_t stProductStr = {
0x1C, // 1 + 1 + sizeof(L"SampleProduct") - 2
0x03, // descriptor type: string
L"SampleProduct"
}
//當主機請求index為 serial number string index(0x03)的字串時
USB_Desc_String_t stSerialStr = {
0x14, // 1 + 1 + sizeof(L"W20201022") - 2
0x03, // descriptor type: string
L"W20201022"
}
????上述描述符集合,就是一個winusb設備的簡單實作,但有了這些資料,還要將它們按主機列舉的規則來發送,所以我們還要實作它們的通信部分,
????一般在各個MCU平臺都會實作USB最底層的部分:SOF包同步、接收并處理令牌包、接收并決議請求、配置設備地址、讀寫IO中斷等,要實作winusb設備,我們只需要在這些平臺上完成以下事情:
????1、在USB中斷架構中對不同的描述符請求回傳正確的資料;
????2、根據端點描述符構建所有端點對應的端點通道;
????3、實作端點bulk傳輸的讀寫IO;
????最后,把它接入到主機,就可以看到主機能夠識別到這個設備并顯示出對應的文本資訊(字串描述),如果你想拋棄MCU的USB架構從零開始實作,可以參考圈圈所著的 《圈圈教你玩USB》,
????當然,作為一個標準的winusb設備,上述功能還不能算是完整的,在Windows平臺,所有的USB設備都需要安裝驅動(又一個龐大的知識體系)后才能使用,只不過一些知名的USB設備是支持免驅的(實際上是Windows為USB設備安裝了默認的驅動),Microsoft規定:想要Windows為winusb設備自動派發winusb.sys(即免驅功能),設備應當提供OS描述符,
例程2:HID鍵盤設備
????本例程的目標是實作一個鍵盤設備,它屬于HID(Human Interface Device)類別,即可以與人互動的設備,常見的鍵盤設備主要包含三個功能:
????- 輸入按鍵資訊(ESC/Win/Ctrl/A/B等);
????- (可選)主機輸出按鍵狀態(Numlock/Capslock/Scroll等);
????- (可選)輸入多媒體控制(快進/快退/暫停等);
????那么,我們就分別需要3個端點來對應上述功能:輸入端點1對應輸入按鍵、輸出端點1對應按鍵狀態、輸入端點2對應多媒體控制,然而,在USB HID設備中,多媒體控制和輸入按鍵是可以通過唯一的報告標識號(Report ID)來區分的,所以輸入端點只要一個就可以了(只要資料前面使用Report ID),當然,第二、三個功能即使不支持也是可以的,那么這樣一個鍵盤設備就只需要一個端點,
以下是鍵盤設備的描述符集合:
#pragma data_alignment=1 //對齊方式為Byte
const USB_Desc_Device_t stDevKeyboard = {
0x12, // sizeof(USB_Desc_Device_t)
0x01, // descriptor type: device
0x0200, // USB Spec 2.0
0x00, // no device class
0x00, // no device subclass
0x00, // no device protocol
0x40, // max ep0 packet size: 64B
0x1234, // vendor id
0x5679, // product id
0xABCD, // product release number
0x01, // manufacturer string index
0x02, // product string index
0x03, // serial number string index
1 // configuration numbers
};
typedef struct _USB_Keyboard_Configuration_t{
USB_Desc_Configuration_t stDescConfiguration;
USB_Desc_Interface_t stDescInterface;
USB_Desc_HID_t stDescHid;
USB_Desc_Endpoint_t stDescEndpointIn;
USB_Desc_Endpoint_t stDescEndpointOut;
}USB_Keyboard_Configuration_t;
USB_Keyboard_Configuration_t stConfKeyboard = {
// configuration descriptor
{
0x09, // sizeof(USB_Desc_Configuration_t)
0x02, // descriptor type: configuration
0x003B, // sizeof(USB_Winusb_Configuration_t)
0x01, // interface numbers
0x01, // configuration index
0x00, // no configuation string
0x80, // no attributes
0x32 // max power: 50*2 = 100 mA
},
// interface descriptor
{
0x09, // sizeof(USB_Desc_Interface_t)
0x04, // descriptor type: interface
0x00, // index of interface
0x00, // no alternate setting
0x02, // endpoint numbers: 2
0x03, // Interface Class: HID
0x01, // Interface Subclass: Boot Supported
0x00, // Interface Protocol: none
},
// hid descriptor
{
0x09, // sizeof(USB_Desc_Interface_t)
0x21, // descriptor type: HID
0x111, // Hid Spec Version 1.1.1
0x21, // Country Code: US
0x01, // Descriptor Numbers
0x22, // Descriptor Type: Report
0xXXXX, // Descriptor Length: sizeof(bReportKeyboard)
},
// endpoint descriptor
{
0x07, // sizeof(USB_Desc_Endpoint_t)
0x05, // descriptor type: endpoint
0x81, // endpoint in 1
0x03, // transfer type: interrupt
0x10, // max packet size: 16B
0x0A, // polling interval: 10ms
},
// endpoint descriptor
{
0x07, // sizeof(USB_Desc_Endpoint_t)
0x05, // descriptor type: endpoint
0x01, // endpoint out 1
0x03, // transfer type: interrupt
0x08, // max packet size: 8B
0x0A, // polling interval: 10ms
}
};
//當主機請求index為 manufacturer string index(0x01)的字串時
USB_Desc_String_t stVendorStr = {
0x14, // 1 + 1 + sizeof(L"SampleHid") - 2
0x03, // descriptor type: string
L"SampleHid"
}
//當主機請求index為 product string index(0x02)的字串時
USB_Desc_String_t stProductStr = {
0x1E, // 1 + 1 + sizeof(L"SampleKeyboard") - 2
0x03, // descriptor type: string
L"SampleKeyboard"
}
//當主機請求index為 serial number string index(0x03)的字串時
USB_Desc_String_t stSerialStr = {
0x14, // 1 + 1 + sizeof(L"K20201022") - 2
0x03, // descriptor type: string
L"K20201022"
}
????而HID描述符中宣告的報告描述符(Report Descriptor)是什么呢?
????上文提到,報告標識號(Report ID)可以將鍵盤輸入的按鍵資訊、多媒體控制區分開來,這個Report ID就是在報告描述符中定義的,而Report ID本身,以及按鍵、多媒體控制、按鍵狀態等資料的輸入/輸出,統稱為報告(Report),可以說,HID設備所有功能的具體內容、格式、作用,都由報告描述符給出詳細、徹底的定義,描述成一個個實際的報告,
????這里先給出鍵盤的報告描述符:
#define SUPPORT_KEYBOARD_SWITCH //支持獲取主機按鍵狀態
#define SUPPORT_MEDIA_CONTROL //支持多媒體控制
const uint8_t bReportKeyboard[] = {
0x05, 0x01, // USAGE_PAGE(Generic Desktop)
0x09, 0x06, // USAGE(Keyboard)
0xA1, 0x01, // COLLECTION(Application)
0x05, 0x07, // USAGE(Keypad)
#ifndef SUPPORT_KEYBOARD_SWITCH //如果只有一個輸入報告可以忽略Report ID欄位
0x85, 0x01, // REPORT_ID(0x01)
#endif
0x19, 0xE0, // USAGE_MINIMUM(Left Control)
0x29, 0xE7, // USAGE_MAXIMUM(Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM(0)
0x25, 0x01, // LOGICAL_MAXIMUM(1)
0x95, 0x08, // REPORT_COUNT(8)
0x75, 0x01, // REPORT_SIZE(1)
0x81, 0x02, // INPUT(Data, Var, Abs)
0x95, 0x01, // REPORT_COUNT(1)
0x75, 0x08, // REPORT_SIZE(8)
0x81, 0x03, // INPUT(Const, Var, Abs)
0x05, 0x07, // USAGE(Keypad)
0x19, 0x00, // USAGE_MINIMUM(0)
0x29, 0x68, // USAGE_MAXIMUM(104)
0x15, 0x00, // LOGICAL_MINIMUM(0)
0x25, 0x68, // LOGICAL_MAXIMUM(104)
0x95, 0x06, // REPORT_COUNT(6)
0x75, 0x08, // REPORT_SIZE(8)
0x81, 0x00, // INPUT(Data, Array, Abs)
#ifdef SUPPORT_KEYBOARD_SWITCH
0x05, 0x08, // USAGE(LEDs)
0x19, 0x01, // USAGEMinimum (NumLock)
0x29, 0x05, // USAGEMaximum (Kana)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size Bit(s) (1)
0x91, 0x02, // Output (Data, Var, Abs)
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size Bit(s) (3)
0x91, 0x01 // Output(Const, Array, Abs)
#endif
0xC0 // End Collection
#ifdef SUPPORT_MEDIA_CONTROL
,
0x05, 0x0C, // USAGE_PAGE(Consumer)
0x09, 0x01, // USAGE(Consumer Control)
0xA1, 0x01, // COLLECTION(Application)
0x85, 0x02, // REPORT_ID(Media Control)
0x09, 0xB5, // USAGE(Scan Next Track)
0x09, 0xB6, // USAGE(Scan Previous Track)
0x09, 0xB7, // USAGE(Stop)
0x09, 0xCD, // USAGE(Play/Pause)
0x09, 0xE2, // USAGE(Mute)
0x09, 0xE9, // USAGE(Volume Up)
0x09, 0xEA, // USAGE(Volume Down)
0x15, 0x00, // LOGICAL_MINIMUM(0)
0x25, 0x01, // LOGICAL_MAXIMUM(1)
0x75, 0x01, // REPORT_SIZE(1)
0x95, 0x07, // REPORT_COUNT(7)
0x81, 0x02, // INPUT(Data, Var, Abs)
0x95, 0x01, // REPORT_COUNT(1)
0x81, 0x03 // INPUT(Cnst, Var, Abs)
0xC0 // END_COLLECTION
#endif
};
????可以看到,報告描述符的長度不是固定的,它隨著功能的變化而變化,換句話說,報告描述符詳細規定了報告的所有細節,而組成報告描述符的單位,就是短條目(Short Item),
長條目(Long Item):既然有短條目,當然有長條目,不過長條目當前只是預留的,為了避免未來短條目不夠用,
短條目(Short Item):參考《HID Spec 1.1.1》 Chapter 5.2,
????短條目是標準的TLV結構,只不過T和L在同一位元組(T占6bit、L占2bit),不過,當Length=11b時,短條目的Data欄位長度是4B而非3B,
| Byte | 0 | 1,2,3,4 |
|---|---|---|
| 欄位 | Tag+Length | Data(0B~4B) |
| bit | 7,6,5,4 | 3,2 | 1,0 |
|---|---|---|---|
| Parts | bTag | bType | bSize |
????對于上文的鍵盤報告描述符,每行都是一個短條目,它們的意義需要在《HID Usage Tables 1.2》中查表,
????因此報告描述符就像是一本翻譯指南:查字典、造句,
????首先,“0x05, 0x01”查詢短條目(《HID Spec 1.1.1》Chapter 5.2)的Tag定義可知需要查詢用途頁0x01(《HID Usage Tables 1.2》),也就是說,讓我們把“字典”翻到頁面:Generic Desktop(通用桌面);
????接下來,“0x09, 0x06”可知需要在Generic Desktop用途頁下查詢用途0x06:Keyboard(鍵盤);
????同理,“0xA1, 0x01”可知開始構建一個App Collection(應用集合),這個集合的用途就是鍵盤;
????… …(大家可以試著自己決議一下,再往下看)
????通過一個個短條目,就構建了一個完整的報告描述符,而上文的報告描述符,其實就是宣告了三個報告:
????第一個報告是Report ID為0x01的輸入報告,長度為9B,作用:從機告訴主機按了XX鍵,比如按了“Ctrl+Alt+W+D”,發送的報告為:0x01, 0x05, 0x00, 0x07, 0x1A, 0x00, 0x00, 0x00, 0x00,其中0x1A為“W”的鍵值,0x07為“D”的鍵值,
| Byte | 0 | 1 | 2 | 3~9 |
|---|---|---|---|---|
| 欄位 | 0x01 | Sepecial Key | Reserved | Normal Key(0~6B) |
????Special Key:
| bit | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 欄位 | LCtrl | LShift | LAlt | LWin | RCtrl | RShift | RAlt | RWin |
????第二個報告是沒有Report ID的輸出報告(因為輸出型別的報告只有一個,可以省略ID欄位),長度為1B,作用:主機告訴從機當前按鍵狀態:numlock、capslock等,其結構為:低5bit分別對應一個按鍵的狀態,高3bit為常量,為了對5bit資料進行對齊,
????第三個報告是Report ID為0x02的輸入報告,長度為2B,作用:從機告訴主機暫停/繼續播放、快進/快退、音量+/-、靜音,其結構為:第一個位元組為Report ID 0x02,第二個位元組為7bit控制位+1bit位元組對齊,
????最后,上述報告描述符(Report Descriptor)構建完畢后,需要在主機的對應介面請求中回傳給主機,和介面、類、端點描述符不同,它是可以單獨獲取的,在開發程序中,也有人習慣稱之為:Report Map,它和Report Descriptor是同一個東西,
參考檔案
USB.org:USB規范的官方組織,
圈圈教你玩USB(第二版):USB原理介紹非常清晰,剛學USB的人都愛它,
USB 2.0 Spec:USB協議的官方標準,
HID Usage Tables 1.2:查詢HID設備報告描述符的條目(Item)代碼,
HID Spec 1.11:HID設備的定義,
CDC120-20101103-track:CDC設備的定義,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/189385.html
標籤:其他
上一篇:C#番外篇-SpinWait
下一篇:引領追逐,工業互聯網的新軌跡
