U盘是我们最常使用的一种USB设备,本文继续使用DOSUSB做驱动,试图以读取扇区的方式读取你的U盘。
本文可能涉及的协议可能会比较多。
一、了解你的U盘
首先我们用上一篇文章介绍的程序usbview.exe去看一下你的U盘,我在本文中用于测试的U盘情况如下:
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端点,有些可能会不一样;有些U盘可能还会有第三个端点,比如支持中断传输的U盘还会有一个Interrupt端点,不过这都没有关系。
我大概看了我手头有的5个U盘,都支持批量传输,且支持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设备,比如U盘、摄像头、打印机等,而本文将只针对我们经常使用的USB设备----U盘,如果你打算尝试本文所介绍的内容,请准备好一个U盘,什么样子的都行,或者是一个USB读卡器,不过要记得插一张卡进去,实际上本文所载范例就是使用一个USB的CF卡读卡器完成的,不用担心损害你的U盘中的数据,本文不会对U盘进行任何写操作,仅仅做一些读操作。
这个系列中我们需要针对U盘读更多的规范,如下:
- 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;再如:我们读取U盘的一个扇区,如果扇区的长度是512个字节,那么这个字段就要填512。
再下来是bmCBWFlags字段,这个字段只有bit 7有意义,为0表示要向设备传输数据,为1表示要从设备获得数据。
bCBWLUN字段总是填0,因为绝大多数的U盘都不支持多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中,批量传输没有那么简单,批量传输分为输出事务和输入事务,我们应该注意到,前面在看U盘的描述表时,在端点这一级有两个端点,一个叫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
最后的一个命令,我将从你的U盘上读出一个扇区。
最前面的两个命令,请翻阅《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
各种概念在前面已经介绍过了,程序无非就是实现这些概念,几乎所有的代码都是围绕着填写数据结构和显示返回结果的,所以代码本身并不难,更重要的是理解数据结构中个字段的含义,这可能不得不阅读一些规范,我想我不可能比规范说的更严谨更完整。要注意的是,你使用的U盘不可能和我的完全一致,一般情况下有可能有变化的是:设备地址devAddr、输出端点地址outEndpoint和输入端点地址inEndpoint,所以在编译程序之前一定要使用《USB系列之二》中的方法仔细查看一下你的U盘的各种描述符表,如果这些值和我的U盘不同,请在主程序开始的地方,更改这几个变量;另外,在主程序6th step中,scsiRead10(0),传递给scsiRead10的参数为0,含义是从LBA(Logical Block Address)为0的地方读取一个扇区,如果你向读取其它扇区,可以更改这个值,其最大值我们在实现 READ CAPACITY时已经读出了,可以参考;此外,注意CBW的字符顺序是little endian,所以我们在填写LBA和读取最大LBA时都做了相应的转换。
好了,应该没有什么了!
Enjoy it.