USB驱动开发之远程访问USB设备扩展(linux平台USB设备数据采集端)

时间:2021-04-03 16:20:53

                                                                    by fanxiushu 2017-06-20 转载或引用请注明原始作者.


前面的章节陆续介绍了远程访问USB设备的相关知识,从数据采集端到虚拟总线驱动,到虚拟控制器和根集线器驱动等、

相关文章如下链接:
http://blog.csdn.net/fanxiushu/article/details/51420096   (USB设备驱动开发之远程访问USB设备(一USB设备数据采集端))
http://blog.csdn.net/fanxiushu/article/details/51494169   (USB设备驱动开发之远程访问USB设备(二 USB设备虚拟端))
http://blog.csdn.net/fanxiushu/article/details/51559720   (USB设备驱动开发之远程访问USB设备( 三 虚拟USB控制器和根集线器))
http://blog.csdn.net/fanxiushu/article/details/52761644   (USB设备驱动开发之扩展(利用USB虚拟总线驱动模拟USB摄像头))
介绍的都是windows平台下的USB相关的东西,
这篇文章介绍的是linux平台下的USB数据采集端。

我们一般在”延长“USB设备的时候,设备采集端采用多种操作系统,不一定是windows平台。
比如工业环境中某些使用USB接口的采集设备,采集设备通过USB接到电脑的windows平台,通过专业的软件分析采集到的数据。
这样采集设备不远处就得有一台PC电脑,操作人员在电脑上分析数据。
有些工业生产环境比较恶劣,距离采集设备太近可能会有危险。因此必须延长到一个合适的距离。
各种工业环境采集设备众多,接口也繁杂。
因此可能就有个想法,使用一个小设备(比如嵌入式linux系统)上边挤满各种各样的接口,
比如COM接口,USB接口等等。采集设备通过USB接口插到这个小设备上,小设备采集USB数据,通过网络比如WIFI无线网等,
把工业采集设备“延长”到电脑中,这样就可以在任何地方,通过电脑windows平台的专业软件分析采集设备的工业数据。
再比如在远程桌面或云桌面实现中尤其是云桌面,控制端采用瘦客户端,通常就是定制的一个嵌入式系统的小设备,
而开发商们通常使用嵌入式linux作为这些终端设备的选择,终端设备上布满大大小小的各种接口,尤其是USB接口。
这些USB接口设备都会被重定向到真正的云桌面系统中。要实现重定向,首先需要采集真正USB设备的数据。
因此我们必须在linux平台实现USB设备的采集。
还比如延长银行U盾设备,延长手机USB接口等等。。。
诸如此类的需求应该很多,这主要得益于USB接口的普及率非常高。

以上需求都有一个共同点,就是USB采集端都是小设备,一般都使用嵌入式linux。
如何才能在linux平台实现USB数据的采集呢? 这个需要从linux驱动方面去实现。
linux平台集成有个usbip项目,它实现了USB虚拟和USB采集两个方面, 应该是linux平台解决此类问题的最快捷办法。
usbip有个特点,它的网络通信是在内核层直接通讯,有自己的通讯协议,这可能也是个缺点,
很多时候,我们可能需要采集到的数据需要加密,压缩之类操作,或者通过自己的私有协议传输,这些需求都不得不修改usbip。
本着一向自己造*的习惯,这里并不介绍usbip,而是自己全新开发USB采集端驱动。
其实总体来说,比起windows的USB采集驱动简单的多。

http://blog.csdn.net/fanxiushu/article/details/51420096 连接简单介绍了windows平台USB数据采集的流程,
不管是windows还是linux,只要实现USB核心的四个数据传输方式(控制,中断,批量,同步),基本就完成了USB数据处理。

首先我们开发的驱动必须接管USB设备,因为各种各样的USB设备,在操作系统中可能存在默认的驱动,
一旦设备插到系统中,操作系统就会选择默认的驱动安装,
因此我们要采集某个指定的USB设备,就必须先要接管它,让操作系统加载的驱动变成我们的。
在介绍windows端USB数据采集的时候介绍过,接管它的默认驱动很麻烦。
需要开发一个专门的驱动来hook 被接管的USB设备物理对象所在的驱动的IRP_MJ_PNP派遣函数,
处理 IRP_MN_QUERY_ID 子请求,把PID和VID替换成我们驱动的某个固定值。
这样PnP即插即用管理器再次查询到PID和VID匹配的驱动(自然就是我们的驱动了)
然后就成功把指定的USB设备加载到我们的自己的驱动中了。
而在linux平台上做这个事情就变得很容易。
linux提供了udev来专门管理设备节点。这使得对USB设备的管理也变得非常容易,包括如何将USB设备绑定到指定的驱动。
在 /sys/bus/usb/drivers 这个固定目录下有许多驱动名字,这就是具体的USB驱动名,
如果我们为某个USB设备开发一个驱动,我们的驱动程序的名字也会出现在其中。比如我们的采集USB设备数据的驱动名字叫 usbcoll。
再进入 /sys/bus/usb/drivers/usbcoll子目录中,会发现有两个文件名 bind 和 unbind,这个就是绑定和取消绑定的属性名。
如何把已经绑定到其他驱动的设备解绑定然后再绑定到我们的驱动呢? 其实很简单。
每个插入到系统中的USB设备都存在一个总线ID,把它简单称呼为busid,(就是类似 1-1.3,2-1等一类字符串,不同版本稍有不同)
假设busid为 1-1.3 的USB设备原先绑定到usb驱动中,只需简单做如下操作就能绑定到usbcoll
  echo '1-1.3' >> /sys/bus/usb/drivers/usb/unbind    # 解除到USB驱动的绑定
  echo '1-1.3' >> /sys/bus/usb/drivers/usbcoll/bind   # 绑定到我们的usbcoll驱动中。
非常简单,因此佩服这种做法的设计者们,使得复杂问题能尽量简单化。

解决了接管USB默认驱动的问题,接着就是如何进行驱动开发了,
USB设备可以分为只有一个Interface的单一设备,或者具有多个Interface的复合设备。
操作系统会为每个Interface加载一个功能驱动,我们一般都会看到复合USB设备存在多个驱动。
因此我们给USB开发驱动,也会面临两个驱动类型,一个是为USB设备某个Interface专门开发的驱动,
另一个是给整个USB设备开发驱动,在这个驱动里,我们处理所有Interface的通讯。
linux系统默认已经为我们做好了整个USB设备的驱动,因此一般来说,只需要开发具体的Interface驱动就可以了。
在linux内核源代码 drivers/usb/core/generic.c 已经注册了一个通用的 usb_device_driver 数据结构,并且在
drivers/usb/core/usb.c的 usb_init初始化函数中调用 usb_register_device_driver 注册了这个通用的USB设备驱动,
在 generic.c的generic_probe函数中调用usb_choose_configuration和 usb_set_configuration来给每个Interface加载驱动。
这很有点windows平台的usbccgp.sys的味道。
( usbccgp.sys 就是 USB Composite Device,就是windows平台默认的复合设备驱动,
当发现是复合设备,则usbccgp.sys被加载,在usbccgp.sys驱动中接着加载各个Interface的驱动)
这个是linux内核唯一调用 usb_register_device_driver 函数的地方。

我们要采集整个USB设备的数据,包括各个Interface,因此我们的驱动也必须是调用 usb_register_device_driver  注册的,
而不是某个单一Interface的驱动框架。
这就是我们的驱动跟一般针对USB某个功能开发的驱动不同的地方。
usb_register_device_driver 函数注册和使用过程,几乎跟 drivers/usb/core/generic.c 差不多,
不同的是在 generic_probe和generic_disconnect的实现中。
首先在generic_probe获取到USB设备的busid,根据busid确定是不是我们需要采集USB设备,
(在应用层把busid写入 usbcoll的bind属性前,必须把busid预先告知给usbcoll驱动,让generic_probe函数进行判断处理。)
如果不是则简单返回 -ENODEV,让系统接着查找,
否则初始化各种参数,分配资源等操作。这样这个usb设备的任何通讯就能在我们的usbcoll驱动里完成。
在generic_disconnect中释放在generic_probe分配的资源。

接着就是核心的USB数据交换过程,根据我一向做事的习惯,把驱动层的数据通通传递到应用层来处理,
在应用层要加密,要压缩,要使用私有协议传输,或者要做其他什么事,都会比在驱动层里处理方便的多。
而这个处理过程很像在
http://blog.csdn.net/fanxiushu/article/details/52681705
(linux平台用VFS驱动实现目录重定向(文件驱动实现目录重定向 四))
等文章中介绍的处理办法了,
首先创建一个字符设备驱动,用来传递urb数据,
应用层程序调用open函数打开这个设备驱动,然后定义IOCTL,把某个usb的busid绑定到这个 open打开的设备文件。

接着使用write函数,把URB的具体请求发给usbcoll驱动,usbcoll驱动分析write写入的内容,填写 urb数据结构,
然后调用 usb_submit_urb 函数,把 URB请求提交给 usbcore,接着就是linux的usbcore内核跟USB硬件设备的处理,
处理完成之后,预先设置到urb的完成回调函数就会被调用,
然后在此回调函数中,通知应用层程序某个URB请求完成,
于是应用层调用 read函数读取已经完成了URB数据包。
这样整个URB通讯过程就完成了。

为了应用层程序跟驱动和跟其他程序更好的交换数据,一般会定义另一个数据结构来实现跟URB通讯,
我们采用如下数据结构来实现。

#pragma pack(1)

struct ioctl_exchange_header_t
{
    int             type;  // 1 提交URB请求; 2其他请求
    unsigned int    seqnum; ///唯一标识,应用层提供

    ///
    int             trans_type;  //// 0 control transfer; 1 bulk or interrupt ; 2 iso transfer

    int             trans_flags; //// USBDEVFS_URB_SHORT_NOT_OK and so on
    int             direction;   //// 方向: 1 是 IN , 0 是 OUT
    unsigned char   ep_address;  ////端口 如果 (ep_address&0x80) 则是IN, 否则OUT
    unsigned char   ep_interval; ////每个端口对应的时间间隔,bulk,intr有效
    unsigned char   padding[2];  ////
    int             start_frame;       /// ISO 传输有效
    int             number_of_packets; /// ISO传输有效,指示多少ISO数据包

    unsigned char   setup_packet[8]; // 8个字节的设置码

    int             result;          //返回码
    int             transfer_length; //传输数据长度,如果 direction 是 IN,则表示需要读取的字节,否则写入 USB的数据,这时候transfer_length==后面数据长度(ISO传输,包括packets长度)
    //后面接数据; 如果是 ISO传输,后面跟 usb_iso_packet_descriptor结构 大小为 number_of_packets*sizeof(usb_iso_packet_descriptor), 然后再跟数据
};

#pragma pack()

这样应用层的 write和read函数写入驱动和从驱动读取的数据的头部都是 ioctl_exchange_header_t 结构的头。

基本上linux平台下USB数据采集比起windows的驱动来说简单和容易实现。