linux平台实现USB虚拟总线驱动一(原理以及开发流程)

                                     by fanxiushu 2019-11-07 转载或引用请注明原始作者。

之前的文章阐述过在windows平台下,如何实现USB虚拟总线驱动, 以及如何在windows平台采集真实USB设备的数据,
然后通过网络传输,达到 ”延长“ USB线缆的效果。
 相关链接如下:
https://blog.csdn.net/fanxiushu/article/details/51420096  USB设备驱动开发之远程访问USB设备(一USB设备数据采集端)
https://blog.csdn.net/fanxiushu/article/details/51494169  USB设备驱动开发之远程访问USB设备(二 USB设备虚拟端)
https://blog.csdn.net/fanxiushu/article/details/51559720  USB设备驱动开发之远程访问USB设备( 三 虚拟USB控制器和根集线器)

以上都是windows平台下的,其中第一个链接是关于如何实现采集端的,后面两个链接阐述USB虚拟端。
当初想要实现USB虚拟驱动的目的比较另类,也在上面的文章中说明了。
因为偶尔会在macOS系统中做些iOS手机数据备份开发什么的,而我的macOS系统是装到vmware虚拟机中的,
结果每次都需要把手机USB数据线插到笔记本电脑上,总感觉这跟线多余,于是总想着有没有办法去掉,于是才有了上面的开发的文章。
实现的效果如下图这样的:


简单解释一下,图中A部分”Fanxiushu Virtual USB Host Controller“ 和 ”Fanxiushu Virtual USB Root HUB“ 是我实现的USB虚拟总线驱动,
这个总线驱动需要虚拟控制器和根集线器的,否则vmware这样的软件无法识别的,
而这个 驱动下“的Port1端口插入了” iPhone手机的,其实真实iPhone手机插到我的另一台windows台式机器上,
这里通过网络传输,然后模拟成”插入“ ”Fanxiushu Virtual USB Host Controller“ 的效果。
但是 Port1端口显示的是 ”Vmware USB Device“ , 说明这个USB设备已经被vmware接管了。
紧接着看图中的B部分。显示”Apple Fanxiushu-USB-Device1“设备已经连接进去vmware虚拟机中,
再看图中的 C部分, 在虚拟机中macOS系统中,已经识别到了我的iPhone手机。
这就是当初实现USB虚拟驱动,想要达到的目的。

本文即将描述的就是linux平台下的USB虚拟总线驱动的实现,也可以叫虚拟USB控制器驱动。
同时,也在以前的文章中,阐述过如何在linux平台中采集真实USB设备的数据,
链接如下:
https://blog.csdn.net/fanxiushu/article/details/73478924   USB驱动开发之远程访问USB设备扩展(linux平台USB设备数据采集端)

配合本文的虚拟USB总线驱动,可以实现在linux平台之间任意的共享USB设备,
如果再配合windows平台下的实现,则可实现windows,linux平台之间任意共享USB设备。
同时也可以把虚拟总线驱动单独拿出来,用于模拟各类通用的USB设备。
比如模拟USB摄像头,如下链接描述的就是利用USB总线驱动模拟 USB摄像头:
https://blog.csdn.net/fanxiushu/article/details/52761644  USB设备驱动开发之扩展(利用USB虚拟总线驱动模拟USB摄像头)
也可以模拟USB声卡,U盘,USB键盘鼠标,游戏手柄等,只有想不到的没有办不到的,因为USB接口太通用了。
也可以模拟某些私有协议的USB设备,当然前提是必须知道这类设备的USB通讯协议格式。

linux下实现USB虚拟总驱动并没有windows平台那么有用,因为使用linux的人太少了。
不过考虑在linux服务器下,尤其是作为桌面云服务器的linux宿主机,这个USB虚拟总线驱动用处却是比较大。
远程桌面,需要解决的一个问题就是USB设备的远程共享。
一般是终端的设备采集到USB设备数据,发送到云桌面服务器的宿主机端,linux宿主机使用虚线USB总线驱动模拟出USB设备,
然后就像上图vmware虚拟机把iPhone设备接管到vmware虚拟机那样,把这个模拟的设备转嫁到对应的虚拟机中。
当然这是其中一个办法,还有就是直接从终端设备采集到的USB数据,传输到虚拟机内部,
然后虚拟机内部的操作系统开发出的USB虚拟总线驱动模拟出对应的USB设备。
至于哪个方法比较好,取决于具体的情况。

回到正题.
linux下的USB虚拟总线驱动框架比起windows来说太简单了。
只需调用几个函数,注册几个回调函数,就能实现一个虚拟USB总线驱动框架。

linux内核从2.6版本开始,就实现了一种叫 platform总线的虚拟总线,这是一种通用的虚拟总线框架结构。
为什么会有这么一种架构,
因为在硬件的世界中,有些外设与CPU通讯是使用标准的总线的,比如 USB总线,I2C总线,PCI总线等等,
但是有些外设是与CPU连在一起,这些外设直接扩展到CPU的地址空间,比如SoC。
如果两类设备按照两套逻辑来处理,显然会给系统内核造成不必要的罗嗦和混乱,所以规定所有的设备都具有总线,
只不过Soc使用的虚拟总线,这就是platofrm总线的由来。
我们在开发虚拟USB总线驱动的时候,就是需要使用platfrom总线。使用它的方式也是很简单。

我们在代码中定义  platform_driver 和 platform_device 数据结构, 如下代码:

/// 驱动入口 

static struct platform_driver host_driver = {

.probe = host_add_device,

.remove = host_remove_device,

.suspend = host_suspend,

.resume = host_resume,

.driver = {

   .name = "usb_host", /// 和下面的device一样

   .owner = THIS_MODULE,

    },

};

 

static void platform_device_release(struct device *dev)

{

// do  nothing, is virtual host

printk("-- usb_host: platform_device_release\n");

}

static struct platform_device host_device = {

////

.name = "usb_host",

.id = -1,

.dev = {

    .release = platform_device_release,

     },

};
 

其中host_add_device,host_remove_device,host_suspend,host_resume是回调函数,
如果熟悉windows驱动,也比较好理解 host_add_device和host_remove_device含义,
host_add_deivce相当于windows中的AddDevice回调函数,是虚拟总线驱动加载的时候被调用,
host_remove_deivce是在驱动卸载时候被调用。

定义如上两个结构之后,在驱动初始化入口函数中调用 platform_driver_register注册总线驱动,
调用 platform_device_register 注册总线设备,如下伪代码:

static int __init host_driver_init(void)

{

int ret;

。。。

ret = platform_driver_register(&host_driver);

。。。。

//注册一个平台总线设备

ret = platform_device_register(&host_device);

。。。。

printk("-- usb_host: drive init ok.\n");

return 0;

}
在退出函数注销,如下伪代码:

static void __exit host_driver_exit(void)

{

platform_device_unregister(&host_device);

platform_driver_unregister(&host_driver);

printk("--- usb_host : driver exit.\n");

}
初始化模块:

module_init(host_driver_init);

module_exit(host_driver_exit);

这样,platform 总线驱动就建立起来了。是不是比起windows实现虚拟总线驱动简单得多了。
接着我们在host_add_device回调函数中初始化USB总线驱动,创建HCD,也就是 USB主机控制器。

开始之前,先大致来了解linux平台下,USB  Host端,也就是主机端驱动的总体框架流程。
主要分为三层:
 1, USB 设备驱动, 这里就是具体的USB设备,负责主机与USB设备通信。
            |
2,  USB Core , 负责连接和管理上下两层,并且对上面的USB设备驱动提供API接口,对下面对的USB主机驱动提供API接口。
            |
3, USB 主机控制器驱动,负责控制管理插入的USB设备。
       比如最常见的EHCI(USB2),XCHI(USB3),OCHI(USB1)主机控制器驱动


我们这里需要实现的就是第3个部分,USB主机控制器,同时管理我维护着我们的“虚拟USB设备”。 
在host_add_device回调函数中,调用usbcore提供的 usb_create_hcd 创建主机控制器,
然后调用 usb_add_hcd 函数把主机控制器加入到普拉头发柔美总线设备中,这样一个USB主机控制器就建立起来了。
当然还需要在usb_add_device回调函数做一些其他相关的工作。
其中 usb_add_hcd函数内部的实现很复杂,有兴趣可以去阅读linux内核源代码。
其中一个重要的就是在usb_add_hcd内部会创建一个root  hub 虚拟根集线器设备,用于管理插到主机上的USB设备或USBHUB,
usb_create_hcd函数会要求传递一个hc_driver数据结构变量。
这里边定义了所有关于USB数据交换,状态查询,USB控制等回调函数。用于查询和管理USB设备状态,URB数据传输。
 把hc_driver里边相关的回调函数实现了,就等于是完整的实现了一个USB控制器驱动。
因此,我们的主要任务就是实现hc_driver结构里边的回调函数。

hc_driver结构比较复杂,这里只实现我们在虚拟控制器驱动需要实现的内容,如下:

///host主机相关结构和回调函数

static struct hc_driver _hc_driver = {

.description = "usb_host",

.product_desc = "Fanxiushu Virtual USB Host Controller",

.hcd_priv_size = sizeof(struct usb_host_t),   //

 

.flags = HCD_USB2, //

 

.start = usb_host_start,   //主机控制器启动

.stop =  usb_host_stop,  //主机控制器停止

 

.urb_enqueue = usb_host_urb_enqueue, //上层的USB设备驱动发起了URB请求,递交到主机控制器中了

.urb_dequeue = usb_host_urb_dequeue,  //上层URB请求取消,或者主机检测到USB设备被拔出了

 

.get_frame_number = usb_host_get_frame_number,

 

        .hub_status_data = usb_host_hub_status,  //查询主机控制器的端口状态,
        .hub_control = usb_host_hub_control,            //设置,清除,查询端口状态。

 

.bus_suspend = usb_host_bus_suspend, //

        .bus_resume = usb_host_bus_resume, //

};


其中hub_status_data和hub_control回调函数的实现,可以查阅usbip的代码,或者借鉴linux内核中其他类似代码。
 无非就是对USB控制器的每个端口状态查询,设置等操作。

重点是urb_enqueue回调函数的实现,这个是USB的通讯核心数据包传递函数。
具体的说,就是上层的USB设备驱动调用usbcore提供的usb_submit_urb 函数的时候,
usb_submit_urb做些其他处理,然后调用usb_hcd_submit_urb函数,
usb_hcd_submit_urb最终进入到我们的主机驱动,调用 urb_enqueue 回调函数, 
如果是真正的USB主机控制器,则在urb_enqueue回调函数中把URB请求递交给USB硬件,
而这里是虚拟主机控制器,因此可以在urb_enqueue中以任何方式传递urb请求数据,
比如在usbip代码中,直接把urb数据通过socket网络传输给对方。
而在我们的代码实现中,为了方便和灵活使用,统一把URB请求数据包传递到应用层,然后在应用层再做其他方面的处理。
当我们的主机驱动处理完这个URB请的时候,调用usbcore提供的usb_hcd_giveback_urb 函数,
通知上层的usb设备驱动,URB请求已经完成。
这时候,上层sub驱动设置的urb回调函数就会被调用,从而上层的usb设备驱动就获取到已经完成的urb数据。

一个完整的urb通讯流程就这样完成了。
现在还有一个问题,如何模拟”插入“和”拔出“USB设备。
在hub_control控制回调函数中,usbcore会查询roothub的设备描述信息,在里边填写我们主机驱动提供的端口数,比如16个。
也就是我们的主机可以提供16个端口同时给16个USB设备。然后每个端口对应一个相应的状态,一开始都是未连接状态。
当我要在某个端口“插入”一个USB设备的时候,改变这个端口状态,然后调用 usb_hcd_poll_rh_status 函数通知usbcore。
usbcore会接着调用 hub_control 回调函数查询端口状态,发现某个端口已经插入了USB设备,
于是调用usb_submit_urb函数获取这个USB设备的设备描述符等相关信息,于是我们的urb_enqueue被调用。
获取描述符,然后根据设备描述符等信息,试图加载对应的USB设备驱动。
USB设备驱动加载之后,接着会调用usb_submit_urb建立起正常的USB设备通信。
在这里,usbcore的行为与windows平台PNP即插即用管理器的行为十分相似。

至此,一个完整的linux平台的USB虚拟控制器驱动内核部分就算实现了,
因为我们的驱动是把URB数据传递到应用层再来处理的。
接着需要处理的就是如何处理URB数据包
1, 如果是模拟一些USB设备,则直接填写相关数据,然后返回给驱动。
2,如果是实现类似usbip功能,则把数据整理打包,再通过网络传递给对方。
这里也就不再赘述。
下图是在 CentOS8系统中(linux内核版本是4.18), 模拟一个USB摄像头的效果图:
USB摄像头的模拟数据是根据以前所写的windows平台的模拟数据,
USB总线驱动不单可以模拟USB摄像头,还能模拟其他通用USB设备,这里为了方便,只模拟了USB摄像头。



未完待续,
下一章主要阐述如何把驱动移植到Android系统中,并且模拟出一个USB摄像头的效果。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章