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攝像頭的效果。
 

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