USB虚拟总线驱动开发扩展之(利用虚拟USB总线驱动实现U盘模拟)

17 篇文章 1 订阅
2 篇文章 1 订阅

                                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平台下的模拟效果图。





 

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
要查询USB总线驱动上的所有驱动的PID和VID信息,可以使用Windows驱动程序开发工具包(WDK)中的函数来遍历设备树并获取设备属性。以下是WDF驱动代码实现查询USB总线驱动上的所有驱动的PID和VID信息的步骤: 1. 获取USB总线设备接口GUID和设备对象 ```C++ LPGUID usbDeviceGuid = (LPGUID)&GUID_DEVINTERFACE_USB_DEVICE; WDFDEVICE usbDevice = WdfDeviceCreateDeviceInterface( Device, usbDeviceGuid, NULL); ``` 2. 遍历设备树并获取设备属性 ```C++ WDFDEVICE usbChildDevice = WdfChildListRetrieveNextDevice( ChildList, NULL); while (usbChildDevice != NULL) { UNICODE_STRING deviceInstanceId; WdfStringCreate(&deviceInstanceId, NULL); // 获取设备实例ID if (WdfDeviceAllocAndQueryProperty(usbChildDevice, DevicePropertyHardwareID, NonPagedPoolNx, WDF_NO_OBJECT_ATTRIBUTES, &deviceInstanceId)) { // 解析设备实例ID PWSTR deviceId = deviceInstanceId.Buffer; while (*deviceId) { if (wcsstr(deviceId, L"USB\\") == deviceId) { // 获取PID和VID信息 ULONG pid, vid; if (swscanf(deviceId, L"USB\\VID_%04X&PID_%04X", &vid, &pid) == 2) { // 处理PID和VID信息 } } // 指向下一个设备实例ID deviceId += wcslen(deviceId) + 1; } } // 获取下一个设备 usbChildDevice = WdfChildListRetrieveNextDevice( ChildList, usbChildDevice); } ``` 这样就可以遍历所有USB总线驱动上的设备,并获取它们的PID和VID信息。注意,此代码仅适用于USB总线设备,对于其他类型的设备可能需要使用不同的接口和属性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值