# 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`,其餘偶發事件根本不會發生。