USB设备驱动开发之远程访问USB设备

Posted 雨中风华

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了USB设备驱动开发之远程访问USB设备相关的知识,希望对你有一定的参考价值。

                                                                                                                                    By Fanxiushu 2016 05-15  转载或引用本文,请注明原始作者。


使用过vmware的人都应该知道,vmware虚拟机有这样的一个功能,
当在宿主机上插入一个USB设备的时候,通过设置,可以在vmware的虚拟机系统里边能访问到这个USB设备,
而且访问这个USB设备,就跟真的把这个USB设备插入到这个虚拟系统中一样,跟真实的几乎没任何区别。
再看一种情况,假设有两台机器C和S,C 机器是你正在使用的机器, S机器在远端,你只能通过远程控制S。
S机器的配置和功能都很强大,大部分时间你都通过远程桌面等方式连接到 S机器。
假如你手上有些USB接口的设备,比如iPhone,iPad,USB摄像头等,很想把他们使用起来,
C机器在你身边,你能而且是只能把这些USB设备插入到C机器,但是你肯定是非常希望插入这些设备后,S机器也能正常使用。
这种远程使用更为强大的S机器的办法,就是现在所谓的云桌面,虚拟云桌面之类的概念。
因此对于虚拟云桌面开发商而言,解决远程访问本地设备,也是基本和重要的课题之一。
再看一个对普通人比较陌生,对ios开发的人比较熟悉的例子,
iOS的app应用安装问题,非常烦,不像windows程序,只要开发出来,可以到处复制,到处运行。
自己开发的app,需要Xcode开发环境部署到手机上,以前做这样的事情,还得花钱买账户,升级到Xcode7才稍微开放了一些。
通过Xcode部署到自己手机到也方便,可是如何部署到别人的手机上,而且那个人也不在同一个地方,无法把它的手机直接接到电脑上。
于是,能不能通过远程方式实现Xcode部署,首先要解决的就是USB的远程访问的问题,
正是基于这样的原因,同时也想掌握USB设备驱动,才开始研究和开发 USB设备驱动,来尝试实现这么一种功能。
(当然有其他更好的方式实现 iOS APP内测,我只是比较另类非要通过Xcode部署App,
我的MacOS是安装到vmware虚拟机中,通过vmware虚拟USB的方式来访问宿主机的USB接口的,
尝试着在windows宿主机中虚拟出USB设备,再尝试让这个虚拟USB被vmware转向到 MacOS中,
希望这一想法最终能实现,我可不想再去做MacOS系统的USB驱动)

以下讨论的都是基于windows平台的USB设备驱动开发。

USB只是接口,是设备和主机进行数据交换的协议接口而已。数据交换无非两个方向,从设备到主机和从主机到设备。
大家所说各种USB设备,其实是具有USB接口的实现自己某种特定功能的硬件设备,
比如USB摄像头,首先这个硬件是摄像头,它是通过USB接口连接到电脑。
这里不讨论USB接口的各种硬件特性,也不是软件开发的范畴。

windows驱动按照种类来分,大致分为总线驱动,功能驱动,过滤驱动三大类。
USB是硬件接口,肯定跟最底层的总线驱动脱离不了关系。
总线驱动负责管理连在某类总线(比如USB总线)上的所有设备,
它负责监控总线上的设备的插入和移除,创建设备的PDO(物理设备对象),并通知PnP管理器有新硬件添加,等等。
再看看USB总线,事实上在电脑基本总线上(比如PCI总线等)应该有一个或者多个USB的控制器,
从硬件上来说,就是集成到主板上的控制器芯片。
每个USB控制器有唯一的一个RootHUb(根集线器),根集线器有多个PORT,简单的说,就是在电脑上看到的USB插口。
因此我们可以简单把USB总线驱动理解成是 USB控制器驱动和RootHUB驱动,
因为USB控制器总是首先被发现,接下来RootHUB设备交给USB控制器驱动处理,
然后RootHUB驱动接着管理自己的PORT和连接到PORT的USB设备。

根集线器上的PORT不单可以接真正的USB设备,也可以再次连接子HUB,
每个子HUB可以接真正的USB设备,或者再接孙子HUB, 这样形成了一颗以RootHUB为根的树,
正是依靠这样的结构,每个USB控制器可以管理最多127个USB设备(理论上是这样)。
当我们把USB设备插入到RootHUB, 或者子HUB,或者孙子HUB等中,这些HUB会上报设备插入通知,
其实是USB控制器轮询这些HUB端口状态,从而获得通知,
RootHUB驱动负责创建这个设备的PDO(物理设备对象),并且通知PnP管理器有新设备添加。
PnP管理器负责根据这个设备的信息,加载这个设备对应的功能驱动程序。
对应的USB设备功能驱动加载成功之后,就开始真正的USB接口通讯了。
对于功能驱动来说,它只需要把数据直接发给RootHUB驱动创建的 PDO就能完成通讯了。

所谓的功能驱动就是这个USB设备是做什么用的,比如是个USB摄像头,或者是个USB键盘等。
功能驱动在总线驱动的上面,USB接口协议是标准通用的协议,凡是具备USB接口的设备,底层通讯都是一样的。
这个就是能实现远程共享各种USB设备基础。

USB功能驱动在windows平台下通讯使用URB(USB Request BLOCK)的方式,
(这个URB是不是跟前几篇文章中介绍的磁盘驱动通讯使用的SRB很相近, 几乎是同一个模子里刻出来的)
windows已经帮我们实现了大部分的USB底层通讯内容,
我们只需构造 适当的URB数据包,就可以跟USB设备进行数据交互(这种URB包的种类大概有20来个)。
直接给 PDO 发送 IRP_MJ_INTERNAL_DEVICE_CONTROL 命令,
命令中包含 URB包就可以跟USB设备完成一次数据交互。

而USB设备的PDO,接收到URB的IRP_MJ_INTERNAL_DEVICE_CONTROL命令之后,
开始真正的跟USB设备进行物理层级别的数据通信,
它得把URB数据转交给RootHUB设备,RootHUB再交给真正的设备。
至于如何完成通讯过程,具体到硬件处理过程。
这就不是这篇文章讨论的内容,除非你想去实现一个真正的USB控制器驱动。

如何实现远程访问USB设备呢?
通过上边的简单介绍,应该对USB通讯过程有个大致的了解,
我们只需在USB总线层中,拦截到某个USB通讯数据,把这些数据通过网络转发到远程机器,
在远程机器上虚拟出一个USB设备,再把数据输入给这个虚拟设备,于是这个虚拟USB设备,就能被正确识别和使用。
而且因为拦截和处理的是USB接口的底层数据,所以凡是具备USB接口的设备,都能被正确识别,
也就是如果是个USB接口的摄像头,远程机器的虚拟USB也被识别成同样的摄像头,
如果是个USB键盘,远程机器的虚拟USB也被当成是USB键盘。
原理并不复杂,得开发一个虚拟总线驱动,由虚拟USB总线驱动模拟出虚拟USB设备,
这个跟以前介绍过的虚拟磁盘很相似,
可惜的是对于虚拟磁盘驱动,微软提供了专门的ScsiPort或StorPort模块来完成类似功能。
而USB虚拟设备驱动得我们自己开发USB总线驱动。
上面还介绍过,USB总线驱动包括USB设备控制器驱动和RootHUB驱动,
按照层次来说,USB控制器管理RootHUB,RootHUB管理USB设备,USB属于最低级的雇员,
如果真要按照真实硬件的层次来实现虚拟USB系统,可够受的。
好在因为是虚拟USB设备,而不是真正的硬件,虚拟环境下,能把不必要的一些东西简化。
可以去掉控制器和RootHUB,只需虚拟USB设备即可,
虽然这对某些特殊程序不能用外,大部分情况都能正常使用。
(至于如何按照真实硬件层次同时实现虚拟USB控制器和虚拟RootHUB,后续章节会介绍到)

总线驱动开发的框架这里就不做过多介绍,下章介绍如何处理虚拟USB设备时候,会做些解释。
详细的可以查看WDK驱动例子里边的toaster例子代码,
WDK7提供了WDM和WDF的例子,WDK8和WDK10把WDM给删除了,只提供了WDF的代码,
你如要研究总线驱动究竟做了些什么,还是最好看他的WDM例子,稍后在CSDN上提供的虚拟USB驱动,
也是采用WDM开发的工程,并且也并不是抄袭WDM的toaster例子在上边填写自己的代码,
而是按照自己的习惯重新组织了代码框架。

要远程访问USB设备,首先我们得把它分成两大模块,
首先,得有采集真实USB设备数据的采集端,为了方便,下文统称为采集端或者服务端。
其次才能在远端虚拟出USB设备,然后输入采集到的数据,完成USB设备访问, 下文统称为客户端或者虚拟USB端。

为了采集USB数据,倒是费了一些周折,首先想到的就是过滤驱动来采集USB设备数据。
我们先看看USB接口的通讯方式,USB接口就是用来数据通信的,通讯方式是它的核心内容之一。
一共有4种通讯方式:
一,控制传输,主要是发送各种控制命令给USB设备,主要使用的是默认端口0进行传输,任何USB设备一旦连接上主机,
      Host都会给他一个默认端口0,否则就没法跟主机通讯了。
二,中断传输,看名字好像是真的硬件中断一样,不然,USB的中断是伪中断,其实就是USB控制器定时查询USB状态,
       看到标记为中断的标志,然后才进行数据传输,中断的相应速度,就得看USB控制器得轮询速度了。
三,批量传输,顾名思义,就是大数据传输,用于非常大数据传输的场所,比如U盘。
四,同步传输,这个传输方式我是比较费解的,主机给一块大的内存块,然后设置一些区块(packet),
       每个区块设置在这块大内存块的偏移以及每个区块读写长度,
       然后给USB设备,USB设备根据packet,同时填充每个区块的数据,可能有些区块传输不到数据或者只一部分数据,
       这是同步传输允许的,它是一种不保证数据完整的传输,主要用于USB视频等要求比较实时但是对数据质量相对不高的情况,
       比如USB摄像头。

不管哪种传输,USB接口的通讯总是主机主动发起的,
即使数据是从设备传输到主机,也依然是主机首先发起传输命令给USB设备,
USB设备再把数据填写到主机发起的这个命令提供的buffer。

一开始想着给USB类驱动挂载 LowerFilters 过滤驱动,想着这样就能拦截所有的USB数据。
但是这样想,总觉得不大对劲(因为所有URB包都是主机主动发起的,过滤驱动的拦截对我们这样的需求没有意义)。
后来明白了其实所有URB包,都是主机主动发起的,那为何干脆不找到某个USB设备在系统中创建的PDO设备,
直接给他发送URB包来采集数据。这么想了,也这么做了。结果数据倒是采集到了,当然也把整个系统给弄蓝屏了。
因为当我发送URB_FUNCTION_SELECT_CONFIGURATION重新选择配置描述符时候,
加载到这个USB设备的他自己的功能驱动还在运行, 因为发送select命令,迫使USB设备重新选择配置描述符,
上边的功能驱动并不知道,还在使用它原来的配置,结果自然得蓝屏罢工了。
而我们要远程访问USB设备,就是在远程的虚拟USB设备完全控制本地的真实USB,它得被独占使用。
否则如果远程虚拟USB设备发送一个类似SELECT等改变设备状态的控制命令过来,加载到真实USB设备上边的功能驱动将无法正常运行。

明白了这个道理,总算知道了该如何采集USB设备数据。
就是给USB设备开发自己的功能驱动,让他替换掉原来的真正的功能驱动。
自己开发的这个功能驱动中,处理所有的URB数据包;
根据USB虚拟设备通过网络传递来的USB请求数据,生成URB包发给USB设备进行数据传输处理。

至于USB的功能驱动如何开发,对我们来说,最主要和唯一要做的就是如何处理USB的四种数据传输方式。
这样的例子,WDK例子代码里也提供了关于批量传输和同步传输的例子,把他们组合起来使用,就是我们需要的。
也可以寻求应用层级别的解决方案,当然首先想到的就是WINUSB,
这个号称是应用层的USB驱动,其实就是把USB的四种传输方式封装到应用层来给不熟悉驱动开发的程序员使用。
不过非常可惜的是老的WINUSB不支持同步传输,在win8.1以上的系统才开始支持同步传输。
如果真采用winsub,现在大量使用的win7,winxp用户就没法用了。
还有开源的libusb,这个工程倒是不错,他主要活跃在linux平台,也有对应的windows版本,
同样比较可惜的是,他虽然处理了同步传输,但是对于同步传输需要传递小区块(packet),它并没实现。
而是一笼统的跟批量传输一样,只提供一个大内存,对于我们的虚拟USB设备,需要根据Packet返回的信息,
确定每个小区块的传输情况。当然也可以适当小修改,让libusb完成这么一个功能。

我是自己开发的驱动,当然得感谢libusb提供的优秀代码,简洁而易懂。
否则也不会这么快掌握和开发出自己的驱动来处理USB设备的数据采集。

采集USB数据的功能驱动倒是开发出来了,可是却有个非常大的麻烦,如何把我们自己的驱动加载到各种USB设备上,
让windows替换掉原来的驱动,而且在不再使用我们的驱动的时候,再换回原来的驱动。

再回来看看PnP管理器如何给某个设备加载功能驱动,
当总线驱动枚举到有某个设备插入进来,创建PDO,并且调用IoInvalidateDeviceRelations 函数通知PnP管理器设备列表有改变,
PnP管理发送IRP_MN_QUERY_DEVICE_RELATIONS给总线驱动查询所有PDOs列表,并且比较新旧列表,知道某个PDO被添加进来
于是,PnP管理器发送IRP_MN_QUERY_ID给这个PDO查询硬件ID,
查询到硬件ID之后,PnP管理器搜索注册表的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum 下已经安装的驱动,
他根据硬件ID来查找enum下的子项,找到之后,就开始加载驱动。没找到会找兼容ID对应的驱动。
如果都没找到,则弹出需要安装驱动的提示框。

显然,我们只要在我们的自己的功能驱动的inf安装文件中,填写正硬件ID,就能被正确加载。
然而我们的目的是让我们的驱动,能加载到各种USB设备上边,各个USB设备的硬件ID都不一样,
不可能每个设备都制作一个inf安装文件,那可够呛。
也许我们可以给我们的inf生成一个USB通用的兼容ID,
但是PnP管理器首先查询的是硬件ID,如果对应硬件ID的驱动没装,到时可以找到我们的驱动并且安装,
可是大部分USB设备都是有驱动的,有些是windows自己都有的,大部分设备都能被windows识别并且安装他自己的驱动。

如何解决这个问题呢,最好是在应用层找到解决办法,
幸好有vmware,虽然vmware没提供源代码,但是它已经做到这种效果了,可以从它的程序中查找蛛丝马迹。
vmware安装目录中有个vmware-usbarbitrator64.exe程序,看着程序的名字就是处理USB的,
用depends查看这个程序使用了哪些WIN32 API,终于,vmware-usbarbitrator64.exe导入的setupapi.dll动态库中,
使用了一个函数SetupDiSetSelectedDriverW,这个肯定就是跟如何安装驱动有关的,
于是再用Google满世界的搜索这个函数的相关连接,终于找到 http://www.google.com/patents/US8825909
(中国的用户需要翻墙才能访问),原来他们早就解决了这么一个问题,居然还申请了专利,可想而知他们对知识产权的重视。
大致原理就是给原来设备的硬件ID,在注册表中添加我们的自己开发的功能驱动的硬件ID(这个硬件ID可以随意,只要是唯一的就行),
然后利用setupapi动态库中函数,重新构造驱动列表,这时候我们自己的驱动就会在他的列表中,然后使用SetupDiSetSelectedDriver,
SetupDiSetSelectedDevice,InstallSelectedDriver等函数动态加载我们的驱动。
如何给注册表添加我们的硬件ID,主要用到 CM_Add_ID 函数,其实这个函数在底层用得是 SetupDiSetDeviceRegistryProperty函数。
SetupDiSetDeviceRegistryProperty 使用SPDRP_HARDWAREID 参数就可以设置硬件ID,
本来这个功能在win7,winxp,甚至win8都能工作的好好的,到了win10,被微软给禁止了,不允许设置硬件ID,
不过这也说得通,本来硬件ID对每个设备就是唯一的,不允许随意修改。
可是这样可就苦了我,得另外找办法解决win10下动态加载自己的驱动的问题。
也许把vmware的办法稍微做些修改,就又能在win10 正常使用了,总的思路还是得想法修改硬件ID,
这样才能欺骗PnP管理器加载我们提供的驱动程序。
在写这篇文章的时候,我正在忙着研究开发虚拟USB控制器和虚拟RootHUB的功能,
因此没没时间再去查找资料如何解决win10下动态安装驱动的问题,
反正利用 http://www.google.com/patents/US8825909 说的办法已经能在win7下正常处理这个问题了,等以后有时间再来解决。

下章继续虚拟USB设备的开发,敬请关注稍后在CSDN上提供的部分源代码。
                   未完待续。。。

以上是关于USB设备驱动开发之远程访问USB设备的主要内容,如果未能解决你的问题,请参考以下文章

Linux下USB驱动开发之USB光谱仪驱动

RDP协议之USB重定向虚拟通道

在云端开发,但通过远程桌面通过 USB 在本地设备上调试 Android 应用程序

USB Development Kit (UsbDk) Design and Architecture 中文版

DIY—USB学习板设计以及驱动开发

Linux驱动开发: 编写USB接口光谱仪驱动