標籤:
隨身碟是我們最常使用的一種USB裝置,本文繼續使用DOSUSB做驅動,試圖以讀取扇區的方式讀取你的隨身碟。
本文可能涉及的協議可能會比較多。
一、瞭解你的隨身碟
首先我們用上一篇文章介紹的程式usbview.exe去看一下你的隨身碟,我在本文中用於測試的隨身碟情況如下:
Device Descriptor: (裝置描述符) USB Address: 1 Length: 18 Descriptor Type: 1 USB Specification nr.: 0x0110 Calss Code: Class code specified by interface Subclass Code: 0x00 Protocol Code: 0x00 MAX Packet Size: 0x08 Vendor ID: 0x058f Product ID: 0x9321 Device Code: 0x0100 Manufacture Index: 1 Product Index: 2 Serial Number Index: 0 Number of Configuration: 1 String Descriptor: (字串描述符) Manufacturer: Alcor Micro Product: Mass Storage Device Configuration Descriptor: (配置描述符) Length: 9 Descriptor Type: 2 Total Length: 32 Number of Interfaces: 1 Configuration Value: 1 Configuration Index: 0 Attributes: Bus Powered Max Power: 50mA Interface Descriptor: (介面描述符) Length: 9 Descriptor Type: 4 Interface Number: 0 Alternate Setting: 0 Number of Endpoints: 2 Interface Class: Mass Storage Device Interface Sub Class: 6 Interface Protocol: 80 Interface Index: 0 Endpoint Descriptor: (端點描述符) Length: 7 Descriptor Type: 5 Endpoint Address: 1 OUT endpoint Attributes: Bulk Max Packet Size: 64 Interval: 0 Endpoint Descriptor: (端點描述符) Length: 7 Descriptor Type: 5 Endpoint Address: 2 IN endpoint Attributes: Bulk Max Packet Size: 64 Interval: 0
各種描述符的含義在以前的文章中介紹過了,或者去翻閱USB的specification,這裡就不多說了,我們從介面描述符開始就一些關鍵點進行一下說明。
首先看介面描述符,Interface Class = 8,表明是Mass Storage Device;Sub Class = 6,表明執行SCSI命令;Interface Protocol = 0x80,表明支援Bulk傳輸;另外,Number of Endpoints = 2,表明有兩個端點。
兩個端點描述符要注意的是,Endpoint Address = 1的是OUT端點,Endpoint Address = 2的是IN端點,有些可能會不一樣;有些隨身碟可能還會有第三個端點,比如支援中斷傳輸的隨身碟還會有一個Interrupt端點,不過這都沒有關係。
我大概看了我手頭有的5個隨身碟,都支援批量傳輸,且支援SCSI命令,所以,這可能是一個比較典型的例子,我們就以它為例。
二、CBW(Command Block Wrapper)和CSW(Command Status Wrapper)
在《USB系列之一》中,我們安裝了一個DOSUSB,在《USB系列之二》中,我們利用USBDOS讀取了所有的描述表,掌握這些內容需要瞭解USB協議1.1(USB Specification Revision 1.1)即可,當然還要瞭解USBDOS,不過這個比較簡單。
在系列一和系列二中,我們已經對DOSUSB的一個資料結構URB有所瞭解,本文中還要大量用到,我們還接觸了一個結構叫device_request,這個結構是在USB協議中定義的,用於向裝置發送命令(Request),本文也會用到。
與前面不同的是,前面的兩個系列可以針對任何USB裝置,比如隨身碟、網路攝影機、印表機等,而本文將只針對我們經常使用的USB裝置----隨身碟,如果你打算嘗試本文所介紹的內容,請準備好一個隨身碟,什麼樣子的都行,或者是一個USB讀卡機,不過要記得插一張卡進去,實際上本文所載範例就是使用一個USB的CF卡讀卡機完成的,不用擔心損害你的隨身碟中的資料,本文不會對隨身碟進行任何寫操作,僅僅做一些讀操作。
這個系列中我們需要針對隨身碟讀更多的規範,如下:
- Universal Serial Bus Mass Storage Class Specification Overview
:http://blog.hengch.com/specification/usb_msc_overview_1.2.pdf
- Universal Serial Bus Mass Storage Class -- Bulk-Only Transport
:http://blog.hengch.com/specification/usbmassbulk_10.pdf
- SCSI Primary Commands - 3(SPC-3)
:http://blog.hengch.com/specification/SCSI_Primary_Commands-3.pdf
- SCSI Block Commands - 2(SBC-2)
:http://blog.hengch.com/specification/SCSI_Block_Command-2.pdf
不用為規範發愁,實際上,前兩個規範都很短,其中第一個對實際編程沒有什麼作用,但最好看一下;第二個規範連目錄一共22頁,其中13頁以前的內容可以跳過(很多和USB Specification中相同),第三個規範主要看第6章,第四個規範主要看第5章,後兩個規範在編程時需要經常翻閱,以便瞭解你正在實現的SCSI命令的具體格式和參數。
本節我們主要介紹兩個新的資料結構,這兩個結構都是在第二個規範中定義的。
第一個資料結構叫CBW(Command Block Wrapper)
這個結構將承載具體的與裝置有關的命令發送到裝置上去,這個結構分成兩部分,第一部分從byte[0]--byte[14]共15個位元組,第而部分從byte[15]--byte[30]共16個位元組,整個資料結構為31個位元組。規範中並沒有定義第二部分的內容,這是因為第二部分承載的具體的命令,既與命令集(SCSI命令集)有關,也與具體的命令有關,我們使用SCSI命令集,所以後16個位元組的內容在前面提到的後面兩個規範中有定義。
比如我們要向裝置發出一個SCSI命令INQUIRY(我們姑且先不要管命令的含義),那麼這個命令的結構在SPC-3的第142頁有定義,如下:
對於SCSI INQUIRY這條命令而言,CBW的第二部分的定義就是上面的這六個位元組,不同的命令,定義也會不同。
好,我們回到CSW的結構上來,根據規範,dCBWSignature的值必須是0X43425355,其實就是USBC這幾個字母倒過來,這是因為CBW的字元順序是little endian(這個東東在以前有關網路編程的文章中介紹過),而我們PC機中的字元順序是big endian,所以要顛倒一下,總之寫dCBWSignature = 0X43425355就OK了;dCBWTag僅僅是一個標誌,你可以填任何值,這裡要先說一下CSW(Command Status Wrapper),我們每發出一個命令,裝置都會返回一個CSW(這個東東下面很快就要介紹了),以說明命令的執行狀態,這個結構中也有Signature和Tag這兩個欄位,其中Tag欄位和發出命令時CBW中的Tag欄位相同,這樣就可以區分這個CSW是和那個CBW對應的了,至於Signature,下面再說。
下一個欄位是dCBWDataTransferLength,表示的是當這個命令發出後,我們希望裝置返回資料的字元數或者我們要向裝置傳輸的字元數,本文僅涉及從裝置返回資料,不涉及向裝置傳輸資料;舉例來說:我們發送INQIURY命令到裝置,按照SPC-3第144頁的說明,該命令返回的資料至少為36個位元組,所以,此時這個位元組應該填36;再如:我們讀取隨身碟的一個扇區,如果扇區的長度是512個位元組,那麼這個欄位就要填512。
再下來是bmCBWFlags欄位,這個欄位只有bit 7有意義,為0表示要向裝置傳輸資料,為1表示要從裝置獲得資料。
bCBWLUN欄位總是填0,因為絕大多數的隨身碟都不支援多LUN(Logical Unit Number),只有一個邏輯單元自然好嗎就是0了。
bCBWCBLength欄位是只CBW第二部分的長度,像前面舉例的INQUIRY命令,長度為6個位元組,則這個欄位就應該填6,再如:READ(10)命令的長度是10個位元組(SBC-2第42頁有定義),這個欄位當然要填10了。
第二個要說的資料結構是CSW,當host向device發送一個CBW後,接著就可以從device收到資料(或者發資料到device),當接受完所需的的資料後,就可以從device獲得一個CSW(Command Status Wrapper),CSW的結構如下:
前面說過,在CBW中的dCBWSignature的值恒為:0x43425355,得到的CSW中的dCSWSignature的值為:0x53425355,dCSWTag與dCBWTag中的一致。
在得到的CSW中,恒定有13個位元組,bCSWStatus的定義如下:
三、發送命令和接收資料
我們知道USB協議中定義了三種傳輸方式,控制傳輸、批量傳輸、中斷傳輸和即時傳輸,在《USB系列二》中我們一直都在使用控制傳輸,我們應該比較熟悉了,本文中將涉及批量傳輸。
我們在使用控制傳輸時,我們設定好URB啟動傳輸事務,相應的結果將返回到制定得buffer中,批量傳輸沒有那麼簡單,批量傳輸分為輸出事務和輸入事務,我們應該注意到,前面在看隨身碟的描述表時,在端點這一級有兩個端點,一個叫OUT端點,一個叫IN端點,當我們啟動一個輸出事務時,一定要發送給OUT端點,當我們啟動一個輸入事務時,一定要發送到輸入端點。下面我們簡單描述一下如何啟動批量傳輸事務。
在使用控制傳輸時,我們應該閱讀過DOSUSB的說明,並且對URB結構比較熟悉,URB中有一個欄位叫transation_type,當這個值為0x2d時為控制傳輸;當為0x69時為批量傳輸的IN事務;當為0xe1時為批量傳輸的OUT事務;當我們啟動一個傳輸時,一定要正確地設定這個值。
我們以一個具體的例子來說明如何啟動一個傳輸,我們以SCSI INQUIRY命令為了,關於這個命令的定義在SPC-3的第142頁--157頁有說明,篇幅很長,但絕大多數篇幅用來解釋返回資料的含義,我們可以暫時不去理會。首先我們要填寫CBW結構,CBW結構的第一部分的填寫前面已經說的很明白了,第二部分的定義在SPC-3的第142頁,共有6個位元組,我們要按照定義填寫好,實際上只要填兩個欄位,一個是OPERATION CODE = 0X12,第二個就是ALLOCATION CODE = 36,表示需要返回36個位元組的內容;CBW填好後,我們開始填寫URB,首先把CBW的位移和段地址放到URB的buffer_off和buffer_seg中,把transation_type=0xe1,表示一個輸出事務,注意把end_point欄位一定要放OUT endpoint的地址,從前面的描述符表中看,應該是1(2是IN endpoint的地址,你的機器可能不同),其它欄位的填法在《USB系列二》中已經介紹過了,填完以後調用DOSUSB,這樣,一個承載著INQUIRY命令的輸出事務就發送到由URB中dev_add和end_point兩個欄位指定的端點上去了。
接下來我們要接收device返回的執行INQUIRY命令的結果,這要啟動一個輸入事務,相對容易一些,只要填寫URB就可以了,把transation_type=0x69,把end_point填上OUT endpoint的地址,本例中為2,buffer_off和buffer_seg指向緩衝區buffer,把buffer_length和actual_length均填為64,因為前面端點描述符表中寫明包的最大長度為64,其它欄位按常規填寫,調用DOSUSB,在buffer中就可以得到返回的內容,按照SPC-3中對返回內容的解釋即可瞭解裝置的一些情況。
接收晚資料後,不要忘了接收CSW,方法也是啟動一個輸入事務,與接收資料完全相同,然後根據CSW的結構解釋其含義。至此一個命令執行完畢。
四、範例
在本文的範例中,我們實現了如下內容:
- 實現了Bulk-Only Mass Storage Reset
- 實現了Get Max LUN
- 實現了SCSI INQUIRY Command
- 實現了SCSI READ CAPACITY (10) Command
- 實現了SCSI REQUEST SENSE Command
- 實現了SCSI TEST UNIT READY Command
- 實現了SCSI READ (10) Command
最後的一個命令,我將從你的隨身碟上讀出一個扇區。
最前面的兩個命令,請翻閱《Universal Serial Bus Mass Storage Class - Bulk-Only Transport》第7頁;INQUIRY、REQUEST SENSE、TEST UNIT READY三個命令請翻閱SPC-3的第142、221和232頁;READ CAPACITY(10)和READ(10)命令,請翻閱SBC-2的第42和44頁。
原始碼請在下面網址下載:
http://blog.hengch.com/source/reader.rar
各種概念在前面已經介紹過了,程式無非就是實現這些概念,幾乎所有的代碼都是圍繞著填寫資料結構和顯示返回結果的,所以代碼本身並不難,更重要的是理解資料結構中個欄位的含義,這可能不得不閱讀一些規範,我想我不可能比規範說的更嚴謹更完整。要注意的是,你使用的隨身碟不可能和我的完全一致,一般情況下有可能有變化的是:裝置地址devAddr、輸出端點地址outEndpoint和輸入端點地址inEndpoint,所以在編譯器之前一定要使用《USB系列之二》中的方法仔細查看一下你的隨身碟的各種描述符表,如果這些值和我的隨身碟不同,請在主程式開始的地方,更改這幾個變數;另外,在主程式6th step中,scsiRead10(0),傳遞給scsiRead10的參數為0,含義是從LBA(Logical Block Address)為0的地方讀取一個扇區,如果你向讀取其它扇區,可以更改這個值,其最大值我們在實現 READ CAPACITY時已經讀出了,可以參考;此外,注意CBW的字元順序是little endian,所以我們在填寫LBA和讀取最大LBA時都做了相應的轉換。
好了,應該沒有什麼了!
Enjoy it.
USB系列之三:從你的隨身碟裡讀出更多的內容