By Fanxiushu 2016-05-22 转载或引用请注明原始作者
接上文,
在处理好USB数据采集端的问题之后,接下来进入核心的部分,虚拟USB设备端的开发工作。
上文简单介绍过,需要开发虚拟总线驱动来模拟USB设备。
所谓虚拟总线驱动,就是安装于System系统设备下的一个驱动,由PnP管理器创建出一个虚拟的总线PDO设备,
我们的虚拟总线驱动Attach到这个PDO上,形成一个FDO功能设备驱动,
然后在我们的驱动中,根据需要创建出若干个 Child PDO设备,
这些 Child PDO设备就是我们根据需要模拟出来的虚拟设备。
我们的总线驱动每当创建出一个 Child PDO并且初始化之后,
调用 IoInvalidateDeviceRelations函数,通知PnP管理器我们的的Child PDO有变化。
于是PnP管理器接着发送 IRP_MN_QUERY_DEVICE_RELATIONS即插即用消息给我们的驱动,
等我们把新的所有Child PDO列表告诉给PnP管理器,它接着比较他内部维护的新旧的PDO列表,
知道哪些PDO被新添加,哪些已经被移除。
对于新添加的设备,PnP管理器发送查询设备ID的消息IRP_MN_QUERY_ID给我们创建的Child PDO,查询设备的各种ID,
然后PnP管理器根据设备ID从注册表查找是否已经为这个Child PDO安装了功能驱动,
如果已经安装,则加载它,没安装则提示用户安装新的驱动。
这就是虚拟总线驱动的大致框架,原理上来说并不复杂,而且有微软提供的 例子代码,
可以阅读它的例子代码进一步加深对总线驱动原理的理解,或者可以查看我提供在CSDN上的源代码来加深理解。
我们的总线驱动模拟的是USB设备接口,因此Child PDO必须具备USB接口的特性,
USB接口核心部分要处理的,其实就是上文简单介绍过的USB接口的四种数据传输方式:
一,控制传输,二中断传输,三批量传输,四,同步传输。
中断,批量,同步传输都比较好处理,而控制传输牵涉到的命令很多,因此需要处理多种命令。
windows平台把跟USB接口的设备进行数据通讯统一使用URB数据包,每个包都指定一个Function功能号,
也就是URB的功能种类。Function的种类大概有20多个,其实依然是从USB接口的四种通讯方式派生出来的,
比如URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER 这个URB功能就是中断传输和批量传输的合并。
URB_FUNCTION_ISOCH_TRANSFER就是同步传输,
而余下来的20多个 URB_FUNCTION_XXX可以完全理解成控制传输的某个命令。
比如URB_FUNCTION_GET_DESCRIPTOR_FROM_DEVICE就是控制传输中,从USB设备获取设备描述符。
首先列举中我们的驱动中需要处理的URB_FUNCTION_XXX命令:
(以下是中断,批量,同步传输命令)
URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER(中断或者批量传输)
URB_FUNCTION_ISOCH_TRANSFER(同步传输)
(以下全是控制传输命令)
通用的控制传输命令,当USB设备传输的命令不在微软定义的URB_FUNCTION时候,可以用它进行传输
URB_FUNCTION_CONTROL_TRANSFER
获取设备,接口,端点描述符
URB_FUNCTION_GET_DESCRIPTOR_FROM_DEVICE
URB_FUNCTION_GET_DESCRIPTOR_FROM_INTERFACE
URB_FUNCTION_GET_DESCRIPTOR_FROM_ENDPOINT
选择配置描述符, 接口可选描述符
URB_FUNCTION_SELECT_CONFIGURATION
URB_FUNCTION_SELECT_INTERFACE
获取USB设备的Class或Vendor信息
URB_FUNCTION_CLASS_DEVICE
URB_FUNCTION_CLASS_INTERFACE
URB_FUNCTION_CLASS_ENDPOINT
URB_FUNCTION_CLASS_OTHER
URB_FUNCTION_VENDOR_DEVICE
URB_FUNCTION_VENDOR_INTERFACE
URB_FUNCTION_VENDOR_ENDPOINT
URB_FUNCTION_VENDOR_OTHER
重置或者中断在某个端点的传输
URB_FUNCTION_RESET_PIPE
URB_FUNCTION_ABORT_PIPE
获取设备,接口,端点状态
URB_FUNCTION_GET_STATUS_FROM_DEVICE
URB_FUNCTION_GET_STATUS_FROM_INTERFACE
URB_FUNCTION_GET_STATUS_FROM_ENDPOINT
URB_FUNCTION_GET_STATUS_FROM_OTHER
获取当前配置,当前接口,当前framenumbber。当前的framehnumber用于同步传输
URB_FUNCTION_GET_CONFIGURATION
URB_FUNCTION_GET_INTERFACE
URB_FUNCTION_GET_CURRENT_FRAME_NUMBER
以下是设置或清除FEATURE,主要用于HUB,当然可能某些USB设备会有用到
URB_FUNCTION_SET_FEATURE_TO_DEVICE
URB_FUNCTION_SET_FEATURE_TO_INTERFACE
URB_FUNCTION_SET_FEATURE_TO_ENDPOINT
URB_FUNCTION_SET_FEATURE_TO_OTHER
URB_FUNCTION_CLEAR_FEATURE_TO_DEVICE
URB_FUNCTION_CLEAR_FEATURE_TO_INTERFACE
URB_FUNCTION_CLEAR_FEATURE_TO_ENDPOINT
URB_FUNCTION_CLEAR_FEATURE_TO_OTHER
windows为何要搞出这么多FUNCTION命令,估计是为了理解和处理USB控制命令的方便。
但是这些控制命令经过 Host Controller进入真正的USB设备前,Host Controller依然要把它转换成 8个字节的SetupPacket控制命令。
这是硬件需求,而我们是虚拟设备,因此没必要非得转成 SetupPacket格式,只要网络通信中适合我们的就可以。
我们的USB数据采集端和虚拟USB端,都属于windows平台,转成Setuppacket再转成FUNCTION,反而麻烦,
因此基本是根据URB_FUNCTION做些简单转换,这样方便也快捷。
但是如果你的采集端和USB虚拟端分别属于不同的平台,比如linux,windows,macos,等各种平台都有,那得使用一个统一的通讯方式。
到时USB通讯协议中规定的格式估计是更好的选择。
知道哪些URB_FUNCTION命令需要处理,可能大家还是不大明白如何处理这些URB,如何完整的模拟一个USB接口,
从而实现把远方的数据采集端的USB设备搬到虚拟端来。
假设你已经熟悉了虚拟总线驱动的框架。
虚拟总线驱动应该与应用层程序有个通讯接口,应用程序使用IOCTL跟驱动通讯。
应用层程序通过网络连接到USB数据采集端,获取到某个需要被远程访问的USB设备的硬件ID,兼容ID等初步信息,
通过CreatePDO IOCTL传递给虚拟总线驱动,虚拟总线驱动根据硬件ID等各种参数创建child PDO设备,
创建成功后调用IoInvalidateDeviceRelations通知PnP管理器,接下来就是PnP管理器该做的事。
当PnP管理器正确加载根据硬件ID对应的功能驱动之后,这个功能驱动就开始工作。这个功能驱动开始构造URB包,
并且发送URB包到我们在虚拟总线驱动中创建的Child PDO设备上,
接下来,我们的总线驱动必须把这些URB数据正确的传递到远端的USB数据采集端,并且得到正确的响应。
至于如何处理这个主要和核心的过程,每个工程师可能有不同的处理办法,我们是采用把URB数据传递到应用层,
然后在应用层通过socket套接字传递给数据采集端,得到采集端的回应数据包之后,再把它传递给驱动,
最后我们的虚拟总线驱动完成从功能驱动发下来的这个URB数据包。
我们在应用层创建一个信号量,传递到驱动,总线驱动使用这个信号量通知应用层程序有新的URB数据包到达。
比如上层的功能驱动有个 URB_FUNCTION_GET_DESCRIPTOR_FROM_DEVICE 的URB数据包投递给我们的总线驱动,
于是总线驱动的把这个URB包挂载到Child PDO的等待处理的队列中,然后增加信号量,通知应用层有URB数据包到达。
应用层程序有一个或者多个线程调用 WaitForSingleObject 函数等待这个信号量,
当WaitForSingleObject成功返回,说明有URB数据包,于是通过DeviceIoControl函数,投递一个 BEGIN
IOCTL到总线驱动,
我们的总线驱动从Child PDO的等待队列取出一个URB数据包,分析处理这个URB数据包,
然后再把这个URB挂载到Child PDO的忙碌队列中,同时生成一个seqno唯一标识这个URB包,完成这个BEGIN IOCTL。
应用层程序根据从BEGIN IOCTL获取到的请求数据 ,发送到远方的USB数据采集端,等待对方的回应。
USB数据采集端回应这个数据包之后,应用层程序调用 一个 END IOCTL 到总线驱动,
我们的虚拟总线驱动根据seqno从Child PDO的忙碌队列查找对应的URB包,把从 END IOCTL传递的数据,正确的填写到URB数据包中,
最后完成这个URB包。
这个就是我的总线驱动对URB数据包的处理工程,这个跟前几篇文章介绍的
“文件过滤驱动实现目录重定向“(http://blog.csdn.net/fanxiushu/article/details/43845699)的处理框架是一致的。
如果不熟悉这个过程,可以去看看过滤驱动实现目录重定向的章节。
上边介绍过的URB_FUNCTION_XXX非常之多,为了在 BEGIN IOCTL和END IOCTL简化数据包,
都统一使用一个数据结构与驱动交互。
如下
struct ioctl_usbtx_header_t
{
ULONGLONG inter_handle; // 是等待处理的文件IRP 指针
LONG inter_seqno; // 每个IRP的序列号,由驱动产生,和inter_handle一起用来保证请求包的唯一性验证
LONG data_length; // 数据的长度; 如果是读设备,则读取的字节数; 如果是写数据到设备,写入前是需要写入的字节数,写入成功后,实际写入的字节数,ISO 传输会包括 iso_packet_hdr_t结构大小
LONG result; // 返回是否成功
LONG reserved1; // 保留
////
int type; // 1 获取描述符, 2 vendor or class , 3 传输数据, 4 重置, 5 获取状态, 6 操作feature
int reserved[ 3 ]; //保留
/////
union{
///
struct{
int type; // 1 获取或设置设备描述符, 2 设置配置描述符, 3 获取或设置接口描述符, 4 获取或设置端口描述符
int subtype; // (type=1,3,4) 1 获取设备描述符, 2 获取配置描述符, 3 获取字符串;;;;; (type=2) 1设置config(index=-1 & value=-1 unconfigure), 2 设置 interface
int is_read; // (type=1,3,4) is_read为TRUE获取描述符,FALSE 设置描述符
int index; // 序号
int value; // 值, 获取string时定义成language_id
}descriptor;
////////
struct{
int type; //1 CLASS请求, 2 VENDOR请求
int subtype; //1 device; 2 interface ; 3 endpoint; 4 other
int is_read; //是从设备读,还是写入设备
int request;
int index;
int value;
}vendor;
////
struct {
int type; // 1 控制传输, 2 中断或批量传输, 3 同步传输
int ep_address; //端口位置 如果 (ep_address &0x80) 则是读,否则写; 控制传输时候,如果为0表示使用默认端口
int is_read; //是从设备读,还是写入设备
union{
int number_packets; //同步传输时候,包个数,如果为0,则组合到一起传输,>0则在头后面跟iso_packet_hdr_t结构,大小为 ISO_PACKET_HDR_SIZE + number_packets*sizeof(iso_packet_t)
struct{
unsigned char setup_packet[8]; /////控制传输时候,发送的8个字节的控制码
};
};
char is_split; ///中断批量传输,或同步传输是否拆分成多块,
char reserved[3]; ///
}transfer;
////////
struct {
int type; /// 1 IOCTL_INTERNAL_USB_RESET_PORT重置设备; 2 IOCTL_INTERNAL_USB_CYCLE_PORT 重置设备; 3 重置端口URB_FUNCTION_RESET_PIPE; 4 中断端口 URB_FUNCTION_ABORT_PIPE
int ep_address;
}reset;
/////
struct {
int type; /// 1 device; 2 interface ; 3 endpoint; 4 other status; 5 获取当前配置描述符;6 根据interface获取当前接口的alterantesetting; 7 获取current frame number
int index; ///
}status;
//////
struct {
int type; //// 1 SET请求, 2 CLEAR请求
int subtype; /// 1 device; 2 interface ; 3 endpoint; 4 other
int index; ///
int value; ///
}feature;
////////
};
////////////
};
看起来似乎有点多,实际上BEGIN IOCTL和END IOCTL都使用 ioctl_usbtx_header_t 来传递各种URB数据,反而方便许多,
到了数据采集端,也使用同样的结构进行处理,因为都是windows平台,处理的各种转换反而少了许多。
数据结构的定义或使用,请下载CSDN上提供的工程。
到此为止,一个基于虚拟总线驱动的实现USB设备远程访问的功能,基本算完成了,
但是有个不太完善的地方,这样的虚拟USB设备像个无主孤魂一样存在于系统中,它既不附着在某个虚拟跟集线器上,
也没有对应的虚拟USB控制器,因此在某些应用层程序看来,会把它当作不存在。
比如某些按照USB设备栈的方式枚举系统中存在的USB设备 ,这样的虚拟USB设备是枚举不出来的,因为他没ROOTHUB,也没USB控制器。
这个概念就跟以前介绍过的虚拟磁盘驱动很类似,使用 微软的ScsiPort或StorePort模型的虚拟磁盘驱动, 会被当成真正的磁盘,
在磁盘管理器能找到我们的虚拟磁盘,而且可以像真正的磁盘那样进行分区,格式化等各种基本的磁盘操作。
而在网上提供的一个类似 filedisk框架的虚拟磁盘驱动,也能提供磁盘访问的功能,但是并不具备StorePort等框架的提供的磁盘驱动功能,
并不被系统视作一个磁盘系统。
我们现在实现的虚拟USB设备也跟filedisk一样,不会被系统视作一个真正的USB设备。
但是它依然能欺骗大部分软件,就跟filedisk一样。
如何到达我们的虚拟USB尽善尽美呢? 需要实现虚拟ROOTHUB和虚拟USB控制器。
敬请关注下文关于RootHUB和USB控制器得开发过程。