# Virtual COM 101 這篇文章要手把手教你寫一個簡易的USB Stack,帶著你了解實現USB通訊需要在軟體層面做到什麼。 我們的目標是完成一個能用的Virtual COM,讓嵌入式裝置(通常是個MCU)能透過USB纜線在PC上顯示為一個COM連接埠。 文中出現的開頭大寫且斜體的英文詞彙,代表是USB協議的專有名詞。 參考連結: * [USB in a NutShell](https://www.beyondlogic.org/usbnutshell/usb1.shtml):本文的主要參考 * [USB-IF](https://www.usb.org/):寫Protocol Stack能不看協議文件的嗎 * [UsbTreeView](https://www.uwe-sieber.de/usbtreeview_e.html):透過這個軟體你可以看到USB裝置的一些資訊以協助除錯。 # USB協議基礎知識 USB是一種主從式通訊,中心稱為*Host*,隨從稱為*Device*。USB的拓樸為匯流排,允許存在一個*Host*與多個*Device*。所有通訊皆由*Host*主動發起。 與許多序列通訊(如RS-232、SPI)不同,USB有規範傳輸資料的格式,而且還有許多處理並非是應用程式會知道的,需要有一個USB Stack來處理。 這章講的低階工作大多由硬體自動解決,不須我們煩心,但有所了解的話會比較容易與硬體的datasheet銜接。 以下內容主要基於*Full-speed*模式。 ## Device Address 由於匯流排上允許存在多個*Device*,區分它們的方式便是由*Host*為每個*Device*分配位址。位址為一個1--127的整數,0被保留作為特殊用途。 ## Endpoint *Endpoint*是資料的起點與終點。*Device*上可以存在多個*Endpoint*,有點類似TCP的port。 ## Packet USB通訊的最小單位,帶有*Device Address*與*Endpoint*位址資訊。 USB Hub依據*Device Address*正確轉送*Packet*。接收者收到*Packet*後依據*Endpoint*位址將資料分發給對應的應用程式。 ## Transaction 一次完整的通訊稱為*Transaction*。一個*Transaction*由以下三個*Packet*構成: 1. *Token*:表示*Transaction*的用途 1. *Data*:*Transaction*的資料酬載。某些狀況下會沒有*Data Packet* 1. *Handshake*:接收方回應通訊狀態 除了*Token*必定由*Host*發出,其餘則依據*Transaction*有不同的發送者。 ### Token Packet *Token*有以下三種: - IN:*Host*想跟*Device*要資料 - OUT:*Host*想傳資料給*Device* - SETUP:發起*Control*傳輸 表示不同類型的*Transaction*。後面會出現 **IN Transaction** 這種稱呼,便是指以IN開頭的*Transaction*。 ### Data Packet 在*Full-speed*以下會見到 DATA0, DATA1 這兩種*Packet*。傳輸時會交替使用DATA0, DATA1,底層硬體通常會提供自動交替的功能,不必上層軟體親自操刀。 依據傳輸速度不同,*Data Packet*能裝載的資料量也有差別。*Low-speed*只能放8bytes,*Full-speed*就可達1023bytes。 + 在*OUT Transaction*與*SETUP Transaction*,*Data*的發送者是*Host* + 在*IN Transaction*,*Data*的發送者是*Device* USB允許裝載的資料長度為0,這種*Data Packet*又稱為ZLP (zero-length packet)。在某些場合中會需要使用ZLP讓傳輸繼續運作。 ### Handshake *Handshake*有以下三種: - ACK:成功接收,沒有任何格式上的錯誤,也沒有無法處理而捨棄資料。 - NAK:回傳NAK並非發生錯誤,它的意義更像是向*Host*表示我有正確收到*Token*了,但目前無法收下資料(OUT *Token*)、無法提供資料(IN *Token*),請稍後再來。 NAK讓*Device*得以控制流量。例如緩衝區滿了,*Device*就在*OUT Transaction*以NAK表示暫時無法接收資料。*Host*就可以稍後再嘗試發送資料。 - STALL:*Device*處在一個需要*Host*介入的狀態。又分成以下兩種: - Function stall:*Endpoint*進入*Halt*狀態後就只會回應STALL,無法收發資料。*Halt*表示*Endpoint*處於一個必須由*Host*採取復原措施的狀態。*Halt*狀態必須由*Host*以*USB Request*解除。*Host*也能以*USB Request*主動使*Endpoint*進入*Halt*狀態。 - Protocol stall:告知*Host*不支援收到的*USB Request*或出了某種差錯。在各*Request*的實作說明中會指出何時該回應STALL。 *Host*不能發出NAK與STALL。 如果收到的*Packet*毀損(bit stuffing錯誤、CRC錯誤、不合法PID等等),接收方不需要回應任何*Handshake*。 ### SOF 全稱Start-of-Frame,是一種不參與*Transaction*的特殊*Packet*。在*Full-speed*下,每1ms被稱為一個*Frame*,而*Host*會在每個*Frame*的開始發出SOF。SOF除了會以固定間隔發送外,上面還有一個遞增的流水號。 大部分的應用無須理會SOF,包含Virtual COM程式。 ## 傳輸型態 USB共有四種傳輸型態。 ### Bulk (大批傳輸) 大批傳輸適合傳輸總量大,但並不要求傳輸延遲的應用,例如USB隨身碟、USB印表機。這種傳輸型態限定*Full-speed*與*High-speed*才有。 ### Interrupt (中斷傳輸) 中斷傳輸是一種實現中斷(interrupt)功能的傳輸型態。中斷傳統上是一種由周邊裝置發起、資料量小但low-latency的通訊機制。但USB是主從式架構,只能以**輪詢**方式模擬而無法完全重現。 *Host*會以*Device*指定的間隔發起*IN Transaction*,此時*Device*就能將資料送回去。間隔設定得太長,從事件發生到*Host*收到中斷之間的latency就可能長到失去作用;間隔設定得太短,則會過度占用USB頻寬。 ### Isochronous (同時傳輸) 同時傳輸跟中斷傳輸一樣也是週期性進行傳輸,差別在同時傳輸沒有*Handshake*。若傳輸出問題就給他出問題,發送者不會重傳資料,適合要求延遲但允許資料丟失的串流應用,例如音訊、視訊。 另一個差別是同時傳輸一次能傳的資料量遠大於中斷傳輸。 ### Control (控制傳輸) 控制傳輸專門用於*USB Request*,而不用於傳輸一般的資料。 控制傳輸比較特別,分成多個階段,流程不完全固定隨實際狀況變化。具體細節留待實作篇再介紹。 # USB Descriptor USB的一大特色是隨插即用,*Host*無法事先得知插上來的設備的細節。*Descriptor*(描述子)便是*Device*用來告訴*Host*自身資訊的資料結構。 ## Standard Descriptor 不同的USB應用中會有著不同的描述子,但*Standard Descriptor*這一類是所有USB應用都會用到的。 ### Device Descriptor 描述整個*Device*的資訊,例如Vendor ID、Product ID、支援的USB版本等等。 *Device Descriptor*會記載擁有的*Configuration Descriptor*數量。 ### Configuration Descriptor 一個*Device*能提供多種組態(configuration)。組態能設定裝置是否有自行供電、會從USB上抽取多少電流等等。*Host*能命令*Device*切換組態,但儘管USB提供此種彈性,絕大部分的裝置只提供一種組態而已。 *Configuration Descriptor*會記載擁有的*Interface Descriptor*數量。 ### Interface Descriptor 介面(interface)是提供功能的單元,能將數個*Endpoint*分成一組用以實現單一功能。 *Interface Descriptor*會記載擁有的*Endpoint Descriptor*數量。 介面有一種替代設定(Alternative Setting)機制。*Host*能命令*Device*切換至替代版介面。例如替代版介面可以用不同傳輸型態的*Endpoint*,臨時提昇throughput之類的。 ### Endpoint Descriptor 描述*Endpoint*,記載其位址、使用的傳輸型態、單次可傳輸的最大長度等等。 ### String Descriptor 描述一個人類看得懂的文字字串,使用**UTF-16LE**編碼,而不是網際協議中比較常見的UTF-8編碼。USB使用*String Descriptor*這種方式傳輸人類可讀的文字字串。 每個*String Descriptor*都有編號,*Host*依照編號向*Device*取得對應的字串。編號0的*String Descriptor*為特殊格式,它不記載字串而是數個2byte的*Language ID*,表示裝置接受哪國語言。最常見的是0x0409(en-US)。 # USB Request *USB Request*是*Host*向*Device*發出的命令,可偵測與設定USB裝置,以及執行功能,例如設定*Device*的位址、取得描述子、檢查*Endpoint*的狀態等等。 *Host*使用*Control*傳輸向*Endpoint* 0發出*USB Request*。*SETUP Transaction*的*Data Packet*含有8bytes的*Request* 依據*Request*的*Type*(類型)、*Recipient*(接收者)屬性可再分類*USB Request*,例如*Standard Device Request*就是`Type=Standard, Recipient=Device` ### Standard Request *Type*為*Standard*的*Request*。任何類型的USB應用都必須支援*Standard Request*。 ### USB Enumeration 從物理連接*Device*後到*Host*找出該怎麼正確使用它的過程。*Host*藉由發出多個*USB Request*進行*USB Enumeration*。例如*Host*就是藉由`GET_DESCRIPTOR`這個*Standard Request*取得*Device Descriptor* # USB-CDC USB標準將各種不同的應用定義為一個個的分類(class),其中CDC(Communication Device Class)是一個關於**通訊裝置**(Communication Device)的分類。CDC包山包海,跟通訊有關的幾乎都被包進來了,例如公共交換電話網路(PSTN)、整合服務數位網路(ISDN)等通訊系統。而本篇的主題---實現Virtual COM---便是基於CDC實現。 ## Subclass 一個*Class*包山包海,於是底下就再細分出*Subclass*(子分類)。實現Virtual COM會用到的ACM(Abstract Control Model)就是CDC下的一個*Subclass*。 ## Virtual COM的運作原理 其實Virtual COM的運作原理很簡單,PC端開啟Virtual COM後就會持續發起*IN Transaction*,嵌入式裝置在收到IN後將要送給PC的資料發送出去就好。反過來當PC端有資料要發給嵌入式裝置,它就會發起*OUT Transaction*並將資料跟著送過來。 然而Virtual COM不止交換資料,還能模擬例如DSR、DCD等訊號;以及最重要的,我們該如何說服PC把這個USB設備當成Virtual COM使用?這就需要特殊的描述子與*USB Request*。 ## Class-specific Descriptor 不同的USB應用中有著不同的描述子,*Class-specific Descriptor*顧名思義就是專屬某分類的描述子。想使用CDC免不了一堆這種描述子。 ### Functional Descriptor 這種描述子是一種*Class-specific Descriptor*,用於記載專屬該*Class*的功能,所以稱為「功能」描述子。 ## Class-specific Request 顧名思義就是專屬某分類的*USB Request*。*Class-specific Request*的*Type*為*Class*。 ## Notification *Notification*是一種使用*Interrupt*傳輸,讓*Device*「通知」*Host*的方式。CDC的一部份功能使用到了這種設計。 前面說過*Interrupt*傳輸可以設定間隔,透過設定間隔就能控制*Notification*的latency。 # 實作 接下來將一步步帶你了解如何寫出USB Stack。 這篇的實作並不依賴特定硬體,只要求底層具有以下能力: - 能分辨收到的是OUT或SETUP *Token*,且能讓上層程式碼處理*Data Packet*上的資料。 - 上層程式碼能決定收到IN後用*Data Packet*傳輸什麼資料,或發出NAK/STALL。 - 能識別*USB Reset*訊號,並讓上層程式碼重設USB Stack的狀態。 例如某個MCU的USB Peripheral就設計成具有直接記憶體存取(DMA)能力,能將*OUT Transaction*的*Data Packet*上的資料自動塞進RAM,並引發中斷進行處理。碰到*IN Transaction*能自動從RAM將資料送出去,並引發中斷裝填下一筆資料。 ## Descriptor實作 所有描述子的開頭都是由這2byte構成: - `bLength(1)`:整個描述子的長度(bytes),也包含`bLength`這1byte - `bDescriptorType(1)`:描述子的種類 後續內容則依據描述子的種類決定。這是一個典型的[TLV格式](https://en.wikipedia.org/wiki/Type%E2%80%93length%E2%80%93value) ### USB格式欄位命名 在USB裡,所有格式的欄位都能從名稱的前綴判斷其資料型別: - b:1byte整數 - w:2byte整數。在USB的格式中多位元組欄位都採用Little-endian - dw:4byte整數 - bm:bit-map,不是點陣圖,是指以位元為單位來承載資訊的結構,類似C bit-field - bcd:binary-coded decimal,以4bit對應一位的十進制數 - i:1byte的整數,記載一個*String Descriptor*的索引,表示它的值是對應的字串。 以下描述子的各欄位按照它們出現在描述子的順序列出。`()`中是欄位的大小(bytes) ### Device Descriptor - `bDescriptorType(1)` = `1` - `bcdUSB(2)` 表示USB裝置支援的版本。格式為`0xAABC`,`AA`是主版本號、`B`是副版本號、`C`是子副版本號。例如USB1.1就是 ==`0x0110`== - `bDeviceClass(1)` 裝置所屬的Class。對我們而言填 ==`0x02`== (CDC) - `bDeviceSubClass(1)` 裝置所屬的SubClass。對我們而言填 ==`0`== - `bDeviceProtocol(1)` 裝置所屬的Protocol。對我們而言填 ==`0`== - `bMaxPacketSize(1)` Endpoint 0 能收發的最大資料長度,有`8`, `16`, `32`, ==`64`== 可選。 - `idVendor(2)` - `idProduct(2)` Vendor ID 與 Product ID 其實可以 ==隨便填==,PC端並不依據這兩個欄位判定是否為Virtual COM。 - `bcdDevice(2)` 裝置的版本號,跟`bcdUSB`相同格式。這欄位==想填什麼就填什麼== - `iManufacturer(1)` 製造商名稱的*String Descriptor*的索引,==不提供就填0==。如果填了非0數值,就要確保程式能處理帶有此索引值的`GET_DESCRIPTOR` - `iProduct(1)` 產品名稱的*String Descriptor*的索引,==不提供就填0== - `iSerialNumber(1)` 序列號的*String Descriptor*的索引,==不提供就填0== - `bNumConfigurations(1)` Configuration數量。對我們而言填 ==`1`== :::info 在結構體的定義加上`__attribute__ ((packed))`能避免產生記憶體對齊的padding,這樣就能以`struct`定義描述子。 ::: ### Configuration Descriptor - `bLength(1)` = `9` - `bDescriptorType(1)` = `2` (Configuration Descriptor) - `wTotalLength(2)` *Host*在以`GET_DESCRIPTOR`讀取組態時,不只會獲得*Configuration Descriptor*,還會一口氣讀出隸屬該組態的所有描述子。這個欄位就用來記載整個組態的長度(bytes)。 如果使用 C struct 定義`GET_DESCRIPTOR`會回傳的內容,那就可以用`sizeof()`計算全長。這點可以參考後面的範例。 - `bNumInterfaces(1)` 這個*Configuration*擁有的*Interface*數量。對我們而言填 ==`2`== - `bConfigurationValue(1)` 這個*Configuration*的編號,從1開始。對我們而言填 ==`1`== - `iConfiguration(1)` 描述這個組態的*String Descriptor*的索引,==不提供就填0== - `bmAttributes(1)` = `0x80` - bit[7]:保留,但歷史因素**必須設為1** - bit[6]:表示這個組態是自我供電(1)或依賴匯流排供電(0)。但就算是自我供電也能從匯流排獲取電力,不超過`bMaxPower`限制就好。 - bit[5]:支援(1) 或 不支援(0) *Remote Wakeup*。對我們而言填 ==`0`== - bit[4:0]:保留,設為0 - `bMaxPower(1)` 在這組態下能從匯流排獲取的最大電流,每單位為2mA,最大值 ==`0xFA`== 代表500mA ### Interface Descriptor (Communication Class) Virtual COM需要實作兩個*Interface*。第一個是由*Communication Interface Class*所定義。 - `bLength(1)` = `9` - `bDescriptorType(1)` = `4` - `bInterfaceNumber(1)` 這個*Interface*的編號,從0開始。==這裡填`0`就好==。 - `bAlternateSetting(1)` *Host*選擇*Alternate Interface*就是依據此數值。對我們而言填 ==`0`== - `bNumEndpoints(1)` 隸屬這個*Interface*的*Endpoint*數量。對我們而言填 ==`1`== - `bInterfaceClass(1)` 介面所屬的Class Code。對我們而言填 ==`0x02`== (CDC Control) - `bInterfaceSubClass(1)` 介面所屬的SubClass Code。對我們而言填 ==`0x02`== (CDC Abstract Control Model) - `bInterfaceProtocol(1)` 介面所屬的Protocol Code。對我們而言填 ==`0`== (No class specific protocol required) - `iInterface(1)` 描述這個介面的*String Descriptor*的索引,==不提供就填0== ### CDC Header Functional Descriptor 從這個描述子開始會有一連串CDC專屬的*Functional Descriptor*。首先是告知CDC實作版本的Header。 - `bLength(1)` = `5` - `bDescriptorType(1)` = `0x24` (class specific interface descriptor) - `bDescriptorSubtype(1)` = `0x00` (Header Functional Descriptor) - `bcdCDC(2)` CDC版本。對我們而言填 ==`0x120`== ### Abstract Control Management Functional Descriptor 描述ACM的細節 - `bLength(1)` = `4` - `bDescriptorType(1)` = `0x24` (class specific interface descriptor) - `bDescriptorSubtype(1)` = `0x02` (Abstract Control Management Functional Descriptor) - `bmCapabilities(1)` 設為1的位元表示支援該功能 - bit[3]:支援 `NETWORK_CONNECTION` *Notification* - bit[2]:支援 `SEND_BREAK` *Request* - bit[1]:支援 `SET_LINE_CODING`, `SET_CONTROL_LINE_STATE`, `GET_LINE_CODING` *Request*,與 `SERIAL_STATE` *Notification* - bit[0]:支援 `SET_COMM_FEATURE`, `CLEAR_COMM_FEATURE`, `GET_COMM_FEATURE` *Request* 本範例只支援LINE相關功能,所以`bmCapabilities`設為 ==`0x02`== ### Union Functional Descriptor 用來描述一群*Interface*共同組成單一功能。*Union Functional Descriptor*的長度不固定,隨*Interface*數量增加,但對於特定應用而言就是固定的。 - `bLength(1)` = `5` - `bDescriptorType(1)` = `0x24` (class specific interface descriptor) - `bDescriptorSubtype(1)` = `0x06` (Union Functional Descriptor) - `bControlInterface(1)` *Union Functional Descriptor*會指定一個*Interface*作為這個功能的控制介面,對我們而言填 ==`0`==,也就是前面定義的那個。 - `bSubordinateInterface0(1)` 從屬介面的*Interface*編號。CDC-ACM需要一個從屬介面,對我們而言填 ==`1`== 如果一個功能需要更多的從屬介面,那就會有 bSubordinateInterface1, bSubordinateInterface2 ...,依序傳給*Host*即可。 ### Call Management Functional Descriptor 關於通話管理的部分。因為我們實際上不是一台數據機,所以含糊帶過就好。 - `bLength(1)` = `5` - `bDescriptorType(1)` = `0x24` (class specific interface descriptor) - `bDescriptorSubtype(1)` = `0x01` (Call Management Functional Descriptor) - `bmCapabilities(1)` 對我們而言填 ==`0x00`== - `bDataInterface(1)` 對我們而言填 ==`1`== ### Endpoint Descriptor (Notification) 實現CDC-ACM的*Notification* * `bLength(1)` = `7` * `bDescriptorType(1)` = `5` (Endpoint Descriptor) * `bEndpointAddress(1)` = `0x80 | (1)` - bit[7]:*Endpoint*的傳輸方向為 host-to-device(0) 或 device-to-host(1)。*Endpoint*的傳輸方向是固定的,不會臨時變更。 - bit[3:0]:*Endpoint*的位址。每個位址上最多只能有一個host-to-device與一個device-to-host的*Endpoint Descriptor* * `bmAttributes(1)` = `0x03` - bit[1:0]:*Endpoint*的傳輸型態 --- *Control* (0)、*Isochronous* (1)、*Bulk* (2)、*Interrupt* (3) - bit[3:2]:僅*Isochronous*會用到,表示同步類型。 - bit[5:4]:僅*Isochronous*會用到,表示用途。 * `wMaxPacketSize(2)` 這個*Endpoint*能接受的資料payload最大長度。==配合硬體的設定填寫==。 * `bInterval(1)` *Interrupt*傳輸的間隔。單位是*Frame*。*Control*與*Bulk*傳輸用不到這欄位,*Isochronous*則必須設為1。 這裡給個 ==`10`== 就行了。設定太短會占用USB頻寬。 ### Interface Descriptor (Data Class) Virtual COM需要的第二個*Interface*,由*Data Interface Class*所定義 - `bLength(1)` = `9` - `bDescriptorType(1)` = `4` - `bInterfaceNumber(1)` = `1` - `bAlternateSetting(1)` = `0` - `bNumEndpoints(1)` = `2` - `bInterfaceClass(1)` = `0x0A` (CDC Data) - `bInterfaceSubClass(1)` = `0` - `bInterfaceProtocol(1)` = `0` - `iInterface(1)` = `0` ### Endpoint Descriptor (Data-OUT) 負責接收*Host*傳來的資料的*Endpoint* * `bLength(1)` = `7` * `bDescriptorType(1)` = `5` (Endpoint Descriptor) * `bEndpointAddress(1)` = `2` * `bmAttributes(1)` = `0x02` (Bulk) * `wMaxPacketSize(2)`:搭配硬體設定 * `bInterval(1)` = `0` ### Endpoint Descriptor (Data-IN) 負責將資料送往*Host*的*Endpoint* 你會發現這個 Data-IN *Endpoint*與 Data-OUT *Endpoint* 擁有相同位址。這是因為一個*Endpoint Descriptor*只適用一個方向的傳輸,另一個方向就需要再寫一個。 * `bLength(1)` = `7` * `bDescriptorType(1)` = `5` (Endpoint Descriptor) * `bEndpointAddress(1)` = `0x80 | (2)` * `bmAttributes(1)` = `0x02` (Bulk) * `wMaxPacketSize(2)`:搭配硬體設定 * `bInterval(1)` = `0` ### Descriptor範例 各個*Descriptor*的結構體定義就請自行思考囉。 ```c struct CDC_ACM_configs { struct ConfigurationDescriptor cfg_desc; struct InterfaceDescriptor communication_class_interface_description; struct CDCHeaderFunctionalDescriptor cdc_header_func_desc; struct AbstractControlManagementFunctionalDescriptor abstract_control_manage_func_desc; struct UnionFunctionalDescriptor_1 cdc_union_func_desc; struct CallManagementFunctionalDescriptor call_manage_func_desc; struct EndpointDescriptor endpoint_notification; struct InterfaceDescriptor data_class_interface_description; struct EndpointDescriptor endpoint_data_out; struct EndpointDescriptor endpoint_data_in; } __attribute__ ((packed)); struct CDC_ACM_configs configuration_set = { .cfg_desc = { .bLength = 9, .bDescriptorType = 2, .wTotalLength = sizeof(struct CDC_ACM_configs), .bNumInterfaces = 2, .bConfigurationValue = 1, .iConfiguration = 0, .bmAttributes = 0x80, .bMaxPower = 0xFA, }, .communication_class_interface_description = { .bLength = 9, .bDescriptorType = 4, .bInterfaceNumber = 0, .bAlternateSetting = 0, .bNumEndpoints = 1, .bInterfaceClass = 0x02, .bInterfaceSubClass = 0x02, .bInterfaceProtocol = 0, .iInterface = 0, }, .cdc_header_func_desc = { .bLength = 5, .bDescriptorType = 0x24, .bDescriptorSubtype = 0x00, .bcdCDC = 0x120, }, .abstract_control_manage_func_desc = { .bLength = 4, .bDescriptorType = 0x24, .bDescriptorSubtype = 0x02, .bmCapabilities = (0x01u << 1), }, .cdc_union_func_desc = { .bLength = 5, .bDescriptorType = 0x24, .bDescriptorSubtype = 0x06, .bControlInterface = 0, .bSubordinateInterface0 = 1, }, .call_manage_func_desc = { .bLength = 5, .bDescriptorType = 0x24, .bDescriptorSubtype = 0x01, .bmCapabilities = 0x00, .bDataInterface = 1, }, .endpoint_notification = { .bLength = 7, .bDescriptorType = 5, .bEndpointAddress = 0x80 | (1), .bmAttributes = 0x03, .wMaxPacketSize = 64, .bInterval = 10, }, .data_class_interface_description = { .bLength = 9, .bDescriptorType = 4, .bInterfaceNumber = 1, .bAlternateSetting = 0, .bNumEndpoints = 2, .bInterfaceClass = 0x0A, .bInterfaceSubClass = 0, .bInterfaceProtocol = 0, .iInterface = 0, }, .endpoint_data_out = { .bLength = 7, .bDescriptorType = 5, .bEndpointAddress = 2, .bmAttributes = 0x02, .wMaxPacketSize = 64, .bInterval = 0, }, .endpoint_data_in = { .bLength = 7, .bDescriptorType = 5, .bEndpointAddress = 0x80 | (2), .bmAttributes = 0x02, .wMaxPacketSize = 64, .bInterval = 0, }, }; ``` ## 處理Request *USB Request*使用*Control*傳輸發給*Device*。*Control*傳輸的詳細步驟如下: 1. *Setup*階段:*Host*向*Default Endpoint*(*Endpoint* 0的別稱)發起*SETUP Transaction*,並送出*Request*內容。 *SETUP Transaction*的*Handshake*比較特殊,只要SETUP與DATA0都沒毀損,*Device*就必須回應ACK,而不能回應NAK/STALL 2. *Data*階段:在這階段*Host*會送出額外的資料或從*Device*接收資料。如果*Request*沒有*Data*階段,就進入*Status*階段 - *Direction* = OUT,*Host*會發起*OUT Transaction*發送資料。*Device*就依據*Request*處理這些資料。 - *Direction* = IN,*Host*會發起*IN Transaction*接收資料。大多底層硬體會設計成上層程式要先填入資料,一收到IN就會立刻發出,這種設計就不能等收到IN才填要送出的資料。 - 由於一次*Transaction*能傳輸的資料有限,*Host*在*Data*階段可能會發起多個*Transaction*以完整傳輸資料。 3. *Status*階段:在這階段*Device*透過*Handshake*回應*Request*成功與否。如果*Request*處理成功... - *Direction* = OUT(沒有*Data*階段的*Request*都是OUT),*Host*會發起*IN Transaction*,此時*Device*要發送一個ZLP。 - *Direction* = IN,*Host*會發起*OUT Transaction*,並發送一個ZLP。此時*Device*回應一個ACK。 ### ReqErr *Request*的規格會指出何種狀況下要回應*ReqErr*:*Device*要在下一個*IN/OUT Transaction*回應一個STALL。 以下場合也必須回應*ReqErr*: - *Device*不支援此*Request* - 當前的USB狀態不接受此*Request* - *Request*的參數有誤 ### USB狀態 在處理*USB Request*時還需要考慮當前的USB狀態。完整實作所有狀態有點複雜,這篇實作簡化成以下狀態: - Default:收到*USB Reset*後的狀態 - Address:接受*Host*指派位址後的狀態 - Configured:選定組態後的狀態。USB設備的大部分功能都要在進入Configured狀態後才能執行。 ### Request格式 *Host*在*Setup*階段送出*Request*的內容,長度固定8bytes。格式如下: - `bmRequestType(1)` - bit[7]:Direction,命令的傳輸方向,有 IN(1) 與 OUT(0) 兩種。如果缺乏*Data*階段,*Direction* = 0 - bit[6:5]:Type,有 Standard, Class, Vendor 三種 - bit[4:0]:Recipient,有 Device, Interface, Endpoint, Other 四種 - `bRequest(1)` *Request*的ID - `wValue(2)` - `wIndex(2)` - `wLength(2)` 這三個「參數」的具體用法依*Request*而定。 ### 時間限制 + 整個*USB Request*最多可花上五秒。 + 對於有*Data*階段的*Standard Device Request*,每個*Data Packet*都要在上個*Packet*的500ms以內開始傳輸。 + *Standard Device Request*的*Status*階段必須在上個*Packet*傳輸完成的50ms內開始。 時間限制算是相當寬鬆。 ## Standard Request實作 *Standard Request*很多,這篇只挑其中幾個Virtual COM會遇到的實作。 名稱後面的括號就是`bRequest`的值。 `bmRequestType` 的 `Type` = 0 (Standard) ### GET_CONFIGURATION (8) 詢問*Device*目前使用哪個Configuration + Direction = IN + Recipient = 0 (Device) + wValue = 0 + wIndex = 1 + wLength = 1 Data階段回傳1byte資料,目前所使用的組態的`bConfigurationValue` Default狀態下的反應未定義;Address狀態下回傳`0x00`;Configured狀態下正常回傳。 ### GET_DESCRIPTOR (6) 讀取描述子 + Direction = IN + Recipient = 0 (Device) + wValue的高位元組表示想讀取的描述子的種類:Device(1), Configuration(2), String(3) 低位元組表示想讀取的描述子的索引,只有*Configuration Descriptor*與*String Descriptor*會用到。索引從0開始,直到該類描述子的數量減1。 + wIndex在取得*String Descriptor*時用來指定*Language ID*;其他描述子此欄位填`0` + wLength表示最多只能回傳幾byte的資料。如果要回傳的描述子長度大於wLength,只回傳前面的部分就好,不能超過wLength位元組。 Data階段回傳描述子的內容。通常需要多個*IN Transaction*才能傳完描述子。當*Data Packet*長度小於`bMaxPacketSize`,*Host*就知道傳完了。如果描述子長度等於`bMaxPacketSize`的整數倍,最後要額外回傳一個ZLP讓*Host*知道結束了。 前面提過讀取*Configuration Descriptor*時,`GET_DESCRIPTOR`不只回傳*Configuration Descriptor*,還有所有隸屬它的描述子,按階層結構輸出。*Class-specific Descriptor*也是跟在它們所延伸的*Interface Descriptor*或*Endpoint Descriptor*後輸出。 如果描述子不存在,回應*ReqErr* 在Default, Address, Configured狀態都支援`GET_DESCRIPTOR` #### 回傳 String Descriptor 要能處理*Host*以`wValue`=`0x0300`詢問*Language ID*,起碼回傳`0x04 0x03 0x09 0x04` (en-US) ### SET_ADDRESS (5) 指派*Device Address* + Direction = OUT + Recipient = 0 (Device) + wValue為*Device Address*。若大於127,行為未定義。 + wIndex = 0 + wLength = 0 沒有Data階段。 在Default狀態,收到非0的`wValue`會使裝置進入Address狀態。 在Address狀態,收到`wValue`=0會使裝置返回Default狀態。非0數值會變更位址。 在Configured狀態是未定義行為。 通常需要上層程式將收到的*Device Address*塞給底層硬體,好讓它能自動匹配位址。實作上要注意我們在*Setup階段*就能得知新的位址,但後面還有一個*Status*階段,此時*Host*依然使用舊的位址與*Device*溝通,所以不能在*Status*階段結束前就更新位址。 ### SET_CONFIGURATION (9) 設定*Device*使用哪個Configuration + Direction = OUT + Recipient = 0 (Device) + wValue的低位元組為想要使用的*bConfigurationValue*。高位元組的用途保留。 + wIndex = 0 + wLength = 0 沒有Data階段。 在Default狀態是未定義行為。 在Address狀態與Configured狀態,若`wValue`=0,進入Address狀態;若`wValue`不為0,進入Configured狀態。 `wValue`指定的*Configuration*不存在,回應*ReqErr* ## CDC-ACM Class-specific Request 實作 由於在Abstract Control Management Functional Descriptor的`bmCapabilities`可以寫明支援的*Request*,這下就能光明正大地只實作部分*Request*了。 `bmRequestType` 的 `Type` = 1 (Class) ### SET_LINE_CODING (0x20) 設定通訊所用的Line Coding,也就是UART的字元格式、baud、parity等等。 + Direction = OUT + Recipient = 1 (Interface) + wValue = 0 + wIndex = 0 + wLength = 7 *Host*在*Data*階段會傳7byte的Line Coding: * dwDTERate(4):資料速率(baud),32位元無號整數。 * bCharFormat(1):StopBit長度,0 (1bit), 1 (1.5bit), 2 (2bit) * bParityType(1):Parity種類,0 (無校驗), 1 (Odd), 2 (Even), 3 (Mark), 4 (Space) * bDataBits(1):資料的位元長度,有 5, 6, 7, 8, 16 等選項。 由於這篇實作Virtual COM的目的是讓嵌入式裝置以USB與PC溝通,替代傳統的COM連接埠,所以Line Coding並沒有實際功能。Virtual COM不會按照你在`dwDTERate`設定的傳輸速率收發資料,是看匯流排與Host的負荷狀況而定。==所以我建議不需要進一步處理這7byte的設定==。 ### GET_LINE_CODING (0x21) 取得通訊所用的Line Coding + Direction = IN + Recipient = 1 (Interface) + wValue = 0 + wIndex = 0 + wLength = 7 *Device*在*Data*階段回傳7byteLine Coding結構,具體請見`SET_LINE_CODING`。 ### SET_CONTROL_LINE_STATE (0x22) 設定 RS-232 風格的控制訊號。 + Direction = OUT + Recipient = 1 (Interface) + wValue為控制訊號,結構如下: - bit[0]對應RS-232的DTR - bit[1]對應RS-232的RTS + wIndex = 0 + wLength = 0 沒有*Data*階段。 ## 收發資料實作 前面講了一大堆描述子與請求處理,至此終於是Virtual COM的主要功能---收發資料。 實作起來很簡單,*Host*會向寫在*Data Class*介面下的*Endpoint*(在前面的*Descriptor*範例該*Endpoint*的位址為`2`)發起*IN/OUT Transaction*,傳過來或發過去的資料就是透過Virtual COM收發的資料,不須額外處理。 具體怎麼做還要搭配底層硬體。例如前面提過的會在收到IN Token後自動將預先設定好的資料輸出的設計,我們就可以把緩衝區的記憶體位址給底層硬體,待發送完畢後就再接上下一個緩衝區。 ## Notification實作 最後,Virtual COM還需要實現CDC-ACM的*Notification* *Host*會向寫在*Communication Class*介面下的*Endpoint*(在前面的*Descriptor*範例該*Endpoint*的位址為`1`)發起*IN Transaction*,然後*Device*回傳的就是*Notification*。該*Endpoint*使用*Interrupt*傳輸,*Host*便以固定頻率輪詢*Device*,我們就能確保通知送達的latency。 每則*Notification*的長度不一,前8byte沿用了*Request*的格式。然而用途終究不同,並不遵守*Request*的規則,請按照*Notification*的說明去填欄位。 ### SERIAL_STATE 回報目前的UART狀態。 + bmRequestType = 0xA1 + bRequest = 0x20 + wValue = 0 + wIndex為發出此通知的介面的編號,以我們來說 ==填`0`== + wLength = 2 後面再跟著一個名為序列狀態(Serial State)的2byte資料,結構如下: - bit[15:7]:保留 - bit[6]:(1)發生overrun錯誤 - bit[5]:(1)發生parity錯誤 - bit[4]:(1)發生frame錯誤 - bit[3]:(1)偵測到響鈴 - bit[2]:(1)偵測到Break - bit[1]:對應RS-232的DSR - bit[0]:對應RS-232的DCD `SERIAL_STATE`比較特殊,它只在序列狀態變化時產生通知,並在發送通知後清除並重新計算序列狀態。 - 對於持續性訊號(DSR, DCD)而言,只會在訊號發生變化時產生通知。 - 對於偶發事件(如Break, error等等)而言,只會在事件發生時產生通知。而且下次的通知裡對應的位元會是0,除非在這期間又發生事件。 以我們實作Virtual COM的目的來說,大概只有模擬DSR與DCD時用得到`SERIAL_STATE`,其餘偶發事件根本不會發生。