USB虛擬總線驅動開發擴展之(利用虛擬USB總線驅動實現U盤模擬)

                                by fanxiushu 2020-03-25 轉載或引用請註明原始作者。

USB虛擬總線驅動的使用範圍是非常廣泛的,可以使用它來模擬各種通用的USB設備。
以前的文章闡述過基於windows平臺和基於linux平臺中的USB虛擬總線驅動開發,
比如如下鏈接闡述的是在linux平臺中的虛擬USB總線驅動開發原理:
https://blog.csdn.net/fanxiushu/article/details/102967402
稍微再早一點的文章闡述了windows平臺中的虛擬USB總線驅動開發,但是windows中的虛擬USB總線驅動實現起來比起linux複雜得多。

到目前爲止,使用虛擬USB總線驅動模擬了虛擬攝像頭,虛擬聲卡和虛擬麥克風,虛擬鼠標鍵盤,虛擬觸摸屏,。。。
好像日常用到的都模擬遍了,不對,U盤還沒模擬。
因此這篇文章就是闡述模擬U盤的具體通訊協議過程。

絕大部分U盤通訊協議都是基於SCSI通訊的,也就是在底層的USB通訊協議中封裝了SCSI協議來實現U盤的功能。

正如上一篇文章在闡述windows平臺實現基於AVStream框架的虛擬攝像頭的時候所說的一樣。
如果你的需求只是實現一個即插即用的移動硬盤,
不應該採用這種通過虛擬USB總線驅動模擬U盤的辦法,因爲同樣的原因:代價是高昂的,過程是冗餘的。
因此本文介紹的內容研究意義可能大於實際使用價值,不過對掌握硬件U盤實際通訊過程也有一定參考意義。
當然這裏還有一個好處就是基於USB通訊封裝的SCSI協議是跨平臺的。
因此本儘量闡述協議部分,不與具體的(比如windows或linux)虛擬USB總線驅動聯繫起來。

實際上,在windows這樣的系統中,可以直接使用Storport 這樣的磁盤驅動框架來實現虛擬磁盤。
而且基本上幾百行代碼就能實現一個基於 Storport的虛擬磁盤驅動框架。
具體原理可以查看我很早前發佈的文章,如下所示,
同時本文介紹的利用USB總線驅動模擬U盤,使用的SCSI通訊協議和下面鏈接中使用的SCSI基本上是一樣的。
https://blog.csdn.net/fanxiushu/article/details/9903123  (磁盤驅動與虛擬磁盤MINIPORT驅動一)
https://blog.csdn.net/fanxiushu/article/details/11713357  (磁盤驅動與虛擬磁盤MINIPORT驅動二)
上面的文章,尤其是第二篇介紹的SCSI命令,在本文中會同樣使用到。

要成功模擬U盤,當然第一步肯定是正確模擬出USB的設備描述符,配置描述符,接口描述符,端口描述符。
U盤的這些描述符其實是挺簡單的。我們可以直接把某個現成的U盤的描述符copy過來,直接使用。
絕大部分U盤都是基於 Bulk-Only 方式來傳輸數據的,這種方式簡單而且容易理解。
也就是主機用 ”控制傳輸方式“ 從U盤獲取各種描述符等基本信息之後,
之後所有基於SCSI的命令,都是封裝到 ”Bulk傳輸“ 中進行通訊的。
BULK傳輸需要有兩個方向:從U盤傳輸到主機,從主機傳輸到U盤,需要兩個BULK端點。
(爲了下文引用的方便,這裏把從U盤到主機端點定義成 Bulk-In, 把從主機到U盤的端點定義成 Bulk-Out)
因此U盤通常是包含設備描述符,一個配置描述符,配置描述符中包含一個接口描述符,接口描述符中包含兩個端點描述符。

接着就是如何在Bulk傳輸中通訊SCSI協議命令。
我們知道,USB通訊都是主機主動發起USB通訊,USB設備響應命令的主-從方式。
因此第一個數據包數由主機首先發起,使用 Bulk-Out端點傳輸,是一個CBW頭(Command Block Wraper),
這個頭用於指示接下來需要傳輸多少傳輸多少數據,數據傳輸方向,SCSI的CBD頭信息等,如下定義:
struct usb_cbw_t
{
    __u32  sig; /// fixd  'USBC'
    __u32  tag; ///
    __u32  data_transfer_length;
    __u8   dir; ///方向 0x80 從設備到主機,  0x00 host -> device
    __u8   lun;   ///
    __u8   cb_length;
    __u8   cb_data[16];
};
總共31個字節。
其中 sig固定爲 0x43425355(‘USBC’),tag是主機隨機生成的一個數字,用於在回覆CSW的時候使用。
data_transfer_length 就是表示接下來需要傳輸的數據大小,如果爲0,表示就只傳輸CBW頭,不傳輸數據。
dir是接下來的數據傳輸方向,
如果0x80表示從設備傳輸到主機,這個時候使用Bulk-IN端點傳輸。如果0 表示從主機傳輸到設備,使用Bulk-Out傳輸。
lun表示 SCSI磁盤設備的邏輯位置,一般一個U盤就設置一個SCSI磁盤,因此通常都是 0,
cb_data 就是SCSI通訊定義的CDB頭,不超過16字節,具體大小由cb_length指定。
我們可以通過CDB頭,指定發起了哪個SCSI命令。

主機通過BULK-Out發起了一個CBW包之後,接下來,如果data_transfer_length 大於0, 則開始傳輸實際的數據。
然後根據CBW中的dir參數判斷方向, 從而判斷是採用 Bulk-In,還是Bulk-Out傳輸。
如果指定的data_transfer_length長度數據傳輸完成,U盤會通過 Bulk-IN端點,回覆主機一個CSW包。
如果傳輸過程中,U盤出現故障等問題,則直接回復 STALL的USB通訊錯誤。
如果data_transfer_length長度爲0,則不需要傳數據,但是U盤同樣需要通過 Bulk-In端點回復CSW數據包。
CSW(Command Status Wrapper)數據包定義如下:
struct usb_csw_t
{
    __u32  sig;  /// 'USBS'
    __u32  tag;  ////
    __u32  data_rest;  ///還剩下多少字節需要傳輸
    __u8   status;     //// 0 success, 1 error
};
一共13個字節。
其中sig固定爲0x53425355(‘USBS’),tag跟主機發起的CWB包中的tag保持一致。
data_reset表示回覆的時候,還需要多少數據需要傳輸。
status表示本次SCSI傳輸是成功,還是失敗, 0 表示成功,非0表示失敗。

總結一下,通過Bulk-In和Bulk-Out兩個端點,完成一個SCSI命令的傳輸過程:
1,主機  發送 31個字節的 CBW 頭(通過 Bulk-Out)
2,傳輸數據(如果有的話),(通過Bulk-In或Bulk-Out,具體根本CBW中的參數決定。)
3,U盤迴復13個字節的CSW包,(通過Bulk-In)

以上傳輸需要嚴格按照順序進行,如果出現錯亂,通常主機就會發起 reset device 的USB命令。

接下來,撥開USB通訊部分,分析SCSI命令。
SCSI命令是非常多的,好在我們實現U盤,其實只需關心其中幾個比較關鍵的命令。
SCSI的CDB頭的第一個字節表示的就是當前命令類型,
比如 Inquiry 是SCSIOP_INQUIRY(0x12),這個是主機獲取SCSI設備的基本信息,U盤需要回復一個INQUIRYDATA結構。
結構描述可直接查詢 WDK驅動的 storport.h 頭文件的描述。
因爲SCSI命令是跨平臺,所以在WDK中描述的這些結構同樣適合於 linux 這樣的平臺。
通常實現一個U盤需要使用到的SCSI命令如下:
SCSIOP_INQUIRY                      掃描磁盤
SCSIOP_READ_CAPACITY       獲得磁盤容量
SCSIOP_READ                          讀磁盤
SCSIOP_WRITE                         寫磁盤
SCSIOP_MODE_SENSE             獲得磁盤相關參數

SCSIOP_TEST_UNIT_READY
SCSIOP_SYNCHRONIZE_CACHE
SCSIOP_START_STOP_UNIT
SCSIOP_VERIFY           以上4個命令跟首次使用磁盤時候,檢查磁盤單元有關。
這個與在
https://blog.csdn.net/fanxiushu/article/details/11713357  (磁盤驅動與虛擬磁盤MINIPORT驅動二)
中描述的基本一致,因此要了解詳細信息,可去查閱如上鍊接的文章。

下圖是利用前段時間開發的基於linux平臺的虛擬USB總線驅動,模擬出來的一個U盤。
估計是大家對windows平臺都爛熟了,所以來個比較新奇的linux平臺下的模擬效果圖。





 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章