Linux下的硬件驅動——USB設備

前言

USB是英文"Universal Serial Bus"的縮寫,意爲"通用串行總線"。是由Compaq(康柏)、DEC、IBM、Intel、NEC、微軟以及Northern Telecom(北方電訊)等公司於1994年11月共同提出的,主要目的就是爲了解決接口標準太多的弊端。USB使用一個4針插頭作爲標準插頭,並通過這個標準接頭,採用菊花瓣形式把所有外設連接起來,它採用串行方式傳輸數據,目前最大數據傳輸率爲12Mbps, 支持多數據流和多個設備並行操作,允許外設熱插拔。

目前USB接口雖然只發展了2代(USB1.0/1.1,USB2.0),但是USB綜合了一個多平臺標準的所有優點 -- 包括降低成本,增加兼容性,可連接大量的外部設備,融合先進的功能和品質。使其逐步成爲PC接口標準,進入了高速發展期。

那麼對於使用Linux系統,正確支持和配置常見的USB設備,就是其使用必不可少的關鍵一步。

相關技術基礎

模塊(驅動程序)

模塊(module)是在內核空間運行的程序,實際上是一種目標對象文件,沒有鏈接,不能獨立運行,但是可以裝載到系統中作爲內核的一部分運行,從而可以動態擴充內核的功能。模塊最主要的用處就是用來實現設備驅動程序。

Linux下對於一個硬件的驅動,可以有兩種方式:直接加載到內核代碼中,啓動內核時就會驅動此硬件設備。另一種就是以模塊方式,編譯生成一個.o文件。當應用程序需要時再加載進內核空間運行。所以我們所說的一個硬件的驅動程序,通常指的就是一個驅動模塊。

設備文件

對於一個設備,它可以在/dev下面存在一個對應的邏輯設備節點,這個節點以文件的形式存在,但它不是普通意義上的文件,它是設備文件,更確切的說,它是設備節點。這個節點是通過mknod命令建立的,其中指定了主設備號和次設備號。主設備號表明了某一類設備,一般對應着確定的驅動程序;次設備號一般是區分不同屬性,例如不同的使用方法,不同的位置,不同的操作。這個設備號是從/proc/devices文件中獲得的,所以一般是先有驅動程序在內核中,纔有設備節點在目錄中。這個設備號(特指主設備號)的主要作用,就是聲明設備所使用的驅動程序。驅動程序和設備號是一一對應的,當你打開一個設備文件時,操作系統就已經知道這個設備所對應的驅動程序。

SCSI 設備

SCSI是有別於IDE的一個計算機標準接口。現在大部分平板式掃描儀、CD-R刻錄機、MO光磁盤機等漸漸趨向使用SCSI接口,加之SCSI又能提供一個高速傳送通道,所以,接觸到SCSI設備的用戶會越來越多。Linux支持很多種的SCSI設備,例如:SCSI硬盤、SCSI光驅、SCSI磁帶機。更重要的是,Linux提供了IDE設備對SCSI的模擬(ide-scsi.o模塊),我們通常會就把IDE光驅模擬爲SCSI光驅進行訪問。因爲在Linux中很多軟件都只能操作SCSI光驅。例如大多數刻錄軟件、一些媒體播放軟件。通常我們的USB存儲設備,也模擬爲SCSI硬盤而進行訪問。

Linux硬件驅動架構

對於一個硬件,Linux是這樣來進行驅動的:首先,我們必須提供一個.o的驅動模塊文件(這裏我們只說明模塊方式,其實內核方式是類似的)。我們要使用這個驅動程序,首先要加載運行它(insmod *.o)。這樣驅動就會根據自己的類型(字符設備類型或塊設備類型,例如鼠標就是字符設備而硬盤就是塊設備)向系統註冊,註冊成功系統會反饋一個主設備號,這個主設備號就是系統對它的唯一標識(例如硬盤塊設備在/proc/devices中顯示的主設備號爲3 ,我們用ls -l /dev/had看到的主設備就肯定是3)。驅動就是根據此主設備號來創建一個一般放置在/dev目錄下的設備文件(mknod命令用來創建它,它必須用主設備號這個參數)。在我們要訪問此硬件時,就可以對設備文件通過open、read、write等命令進行。而驅動就會接收到相應的read、write操作而根據自己的模塊中的相應函數進行了。

其中還有幾個比較有關係的東西:一個是/lib/modules/2.4.XX目錄,它下面就是針對當前內核版本的模塊。只要你的模塊依賴關係正確(可以通過depmod設置),你就可以通過modprobe 命令加載而不需要知道具體模塊文件位置。 另一個是/etc/modules.conf文件,它定義了一些常用設備的別名。系統就可以在需要此設備支持時,正確尋找驅動模塊。例如alias eth0 e100,就代表第一塊網卡的驅動模塊爲e100.o。他們的關係圖如下:


 

配置USB設備

內核中配置.

要啓用 Linux USB 支持,首先進入"USB support"節並啓用"Support for USB"選項(對應模塊爲usbcore.o)。儘管這個步驟相當直觀明瞭,但接下來的 Linux USB 設置步驟則會讓人感到糊塗。特別地,現在需要選擇用於系統的正確 USB 主控制器驅動程序。選項是"EHCI" (對應模塊爲ehci-hcd.o)、"UHCI" (對應模塊爲usb-uhci.o)、"UHCI (alternate driver)"和"OHCI" (對應模塊爲usb-ohci.o)。這是許多人對 Linux 的 USB 開始感到困惑的地方。

要理解"EHCI"及其同類是什麼,首先要知道每塊支持插入 USB 設備的主板或 PCI 卡都需要有 USB 主控制器芯片組。這個特別的芯片組與插入系統的 USB 設備進行相互操作,並負責處理允許 USB 設備與系統其它部分通信所必需的所有低層次細節。

Linux USB 驅動程序有三種不同的 USB 主控制器選項是因爲在主板和 PCI 卡上有三種不同類型的 USB 芯片。"EHCI"驅動程序設計成爲實現新的高速 USB 2.0 協議的芯片提供支持。"OHCI"驅動程序用來爲非 PC 系統上的(以及帶有 SiS 和 ALi 芯片組的 PC 主板上的)USB 芯片提供支持。"UHCI"驅動程序用來爲大多數其它 PC 主板(包括 Intel 和 Via)上的 USB 實現提供支持。只需選擇與希望啓用的 USB 支持的類型對應的"?HCI"驅動程序即可。如有疑惑,爲保險起見,可以啓用"EHCI"、"UHCI" (兩者中任選一種,它們之間沒有明顯的區別)和"OHCI"。( 趙明注:根據文檔,EHCI已經包含了UHCI和OHCI,但目前就我個人的測試,單獨加EHCI是不行的,通常我的做法是根據主板類型加載UHCI或OHCI後,再加載EHCI這樣纔可以支持USB2.0設備)。

啓用了"USB support"和適當的"?HCI"USB 主控制器驅動程序後,使 USB 啓動並運行只需再進行幾個步驟。應該啓用"Preliminary USB device filesystem",然後確保啓用所有特定於將與 Linux 一起使用的實際 USB 外圍設備的驅動程序。例如,爲了啓用對 USB 遊戲控制器的支持,我啓用了"USB Human Interface Device (full HID) support"。我還啓用了主"Input core support" 節下的"Input core support"和"Joystick support"。

一旦用新的已啓用 USB 的內核重新引導後,若/proc/bus/usb下沒有相應USB設備信息,應輸入以下命令將 USB 設備文件系統手動掛裝到 /proc/bus/usb:

# mount -t usbdevfs none /proc/bus/usb 

爲了在系統引導時自動掛裝 USB 設備文件系統,請將下面一行添加到 /etc/fstab 中的 /proc 掛裝行之後:

none /proc/bus/usb usbdevfs defaults 0 0 

模塊的配置方法.

在很多時候,我們的USB設備驅動並不包含在內核中。其實我們只要根據它所需要使用的模塊,逐一加載。就可以使它啓作用。

首先要確保在內核編譯時以模塊方式選擇了相應支持。這樣我們就應該可以在/lib/modules/2.4.XX目錄看到相應.o文件。在加載模塊時,我們只需要運行modprobe xxx.o就可以了(modprobe主要加載系統已經通過depmod登記過的模塊,insmod一般是針對具體.o文件進行加載)

對應USB設備下面一些模塊是關鍵的。

usbcore.o 要支持usb所需要的最基礎模塊
usb-uhci.o (已經提過)
usb-ohci.o (已經提過)
uhci.o 另一個uhci驅動程序,我也不知道有什麼用,一般不要加載,會死機的
ehci-hcd.o (已經提過 usb2.0)
hid.o USB人機界面設備,像鼠標呀、鍵盤呀都需要
usb-storage.o USB存儲設備,U盤等用到

相關模塊

ide-disk.o IDE硬盤
ide-scsi.o 把IDE設備模擬SCSI接口
scsi_mod.o SCSI支持

注意kernel config其中一項:

	Probe all LUNs on each SCSI device

最好選上,要不某些同時支持多個口的讀卡器只能顯示一個。若模塊方式就要帶參數安裝或提前在/etc/modules.conf中加入以下項,來支持多個LUN。

	add options scsi_mod max_scsi_luns=9  

sd_mod.o SCSI硬盤
sr_mod.o SCSI光盤
sg.o SCSI通用支持(在某些探測U盤、SCSI探測中會用到)

常見USB設備及其配置

在Linux 2.4的內核中已經支持不下20種設備。它支持幾乎所有的通用設備如鍵盤、鼠標、modem、打印機等,並不斷地添加廠商新的設備象數碼相機、MP3、網卡等。下面就是幾個最常見設備的介紹和使用方法:

USB鼠標:

鍵盤和鼠標屬於低速的輸入設備,對於已經爲用戶認可的PS/2接口,USB鍵盤和USB鼠標似乎並沒有太多更優越的地方。現在的大部分鼠標採用了PS/2接口,不過USB接口的鼠標也越來越多,兩者相比,各有優勢:一般來說,USB的鼠標接口的帶寬大於PS/2鼠標,也就是說在同樣的時間內,USB鼠標掃描次數就要多於PS/2鼠標,這樣在定位上USB鼠標就更爲精確;同時USB接口鼠標的默認採樣率也比較高,達到125HZ,而PS/2接口的鼠標僅有40HZ(Windows 9x/Me)或是60HZ(Windows NT/2000)。

對於USB設備你當然必須先插入相應的USB控制器模塊:usb-uhci.o或usb-ohci.o

	modprobe usb-uhci

USB鼠標爲了使其正常工作,您必須先插入模塊usbmouse.o和mousedev.o

	modprobe usbmouse
	modprobe mousedev

若你把HID input layer支持和input core 支持也作爲模塊方式安裝,那麼啓動hid模塊和input模塊也是必要的。

	modprobe hid
	modprobe input

USB鍵盤:

一般的,我們現在使用的鍵盤大多是PS/2的,USB鍵盤還比較少見,但是下來的發展,鍵盤將向USB接口靠攏。使用USB鍵盤基本上沒有太多的要求,只需在主板的BIOS設定對USB鍵盤的支持,就可以在各系統中完全無障礙的使用,而且更可以真正做到在即插即用和熱插拔使用,並能提供兩個USB連接埠:讓您可以輕易地直接將具有USB接頭的裝置接在您的鍵盤上,而非計算機的後面。

同樣你當然必須先插入相應的USB控制器模塊:usb-uhci.o或usb-ohci.o

	modprobe usb-uhci

然後您還必須插入鍵盤模塊usbkbd.o,以及keybdev.o,這樣usb鍵盤才能夠正常工作。此時,運行的系統命令:

modprobe usbkbd
modprobe keybdev

同樣若你把HID input layer支持和input core 支持也作爲模塊方式安裝,那麼啓動hid模塊和input模塊也是必要的。

U盤和USB讀卡器:

數碼存儲設備現在對我們來說已經是相當普遍的了。CF卡、SD卡、Memory Stick等存儲卡已經遍及我們的身邊,通常,他們的讀卡器都是USB接口的。另外,很多MP3、數碼相機也都是USB接口和計算機進行數據傳遞。更我們的U盤、USB硬盤,作爲移動存儲設備,已經成爲我們的必須裝備。

在Linux下這些設備通常都是以一種叫做usb-storage的方式進行驅動。要使用他們必須加載此模塊

	modprobe usb-storage

當然,usbcore.o 和usb-uhci.o或usb-ohci也肯定是不可缺少的。另外,若你係統中SCSI支持也是模塊方式,那麼下面的模塊也要加載

	modprobe scsi_mod
	modprobe sd_mod

在加載完這些模塊後,我們插入U盤或存儲卡,就會發現系統中多了一個SCSI硬盤,通過正確地mount它,就可以使用了(SCSI硬盤一般爲/dev/sd?,可參照文章後面的常見問題解答)。

	mount /dev/sda1 /mnt

Linux支持的其他USB設備。

MODEM--(比較常見)


網絡設備


攝像頭--(比較常見)例如ov511.o


聯機線--可以讓你的兩臺電腦用USB線實現網絡功能。usbnet.o


顯示器--(我沒見過)


遊戲杆


電視盒--(比較常見)


手寫板--(比較常見)


掃描儀--(比較常見)


刻錄機--(比較常見)


打印機--(比較常見)


注意:上面所說的每個驅動模塊,並不是都要手動加載,有很多系統會在啓動或你的應用需要時自動加載的,寫明這些模塊,是便於你在不能夠使用USB設備時,可以自行檢查。只要用lsmod確保以上模塊已經被系統加載,你的設備就應該可以正常工作了。當然注意有些模塊已經以內核方式在kernel啓動時存在了(這些模塊文件在/lib/modules/2.4.XX中是找不到的)。

最常遇見的USB問題

  1. 有USB設備的系統安裝完redhat 7.3啓動死機問題

    有USB設備,當你剛裝完redhat 7.3第一次啓動時,總會死掉。主要原因是Linux在安裝時探測到有usb-uhci和ehci-hcd兩個控制器,但在啓動時,加載完usb-uhci再加載ehci-hcd就會有衝突。分析認爲redhat7.3系統內核在支持USB2.0標準上存在問題。在其他版本的Linux中均不存在此問題。

    解決辦法:在lilo或grub啓動時用命令行傳遞參數init=/sbin/init。這樣在啓動後就不運行其他服務而直接啓動shell。然後運行
    mount -o remount,rw / 使/ 可寫,init直接啓動的系統默認只mount /爲只讀 
    然後vi /etc/modules.config文件 
    刪除alias usb-controller1 ehci-hcd一行。或前面加#註釋掉 
    然後mount -o remount,ro / 使/ 只讀,避免直接關機破壞文件系統 
    然後就可以按Ctrl-Alt-Delete直接重啓了 
    或許,你有更簡單的辦法:換USB鍵盤和鼠標爲PS2接口,啓動後修改/etc/modules.config文件。

  2. 我們已經知道U盤在Linux中會模擬爲SCSI設備去訪問,可怎麼知道它對應那個SCSI設備呢?

    方法1:推測。通常你第一次插入一個SCSI設備,它就是sda,第二個就是sdb以此類推。你啓動Linux插入一個U盤,就試試sda,換了一個就可能是sdb。這裏注意兩個特例:1) 你用的是聯想U盤,它可能存在兩個設備區(一個用於加密或啓動電腦),這樣就可能一次用掉兩個sda、sdb,換個U盤就是sdc、sdd。2) 聯想數碼電腦中,可能已經有了六合一讀卡器。它同樣也是USB存儲設備。它會佔掉一個或兩個SCSI設備號。

    方法2:看信息。其實,只要你提前把usb-storage.o、scsi_mod.o、sd_mod.o模塊加載(直接在kernel中也可以)了,在你插入和拔出U盤時,系統會自動打出信息如下:

    SCSI device sda: 60928 512-byte hdwr sectors ( 31 MB )
    sda: Write Protect is on
    

    根據此信息,你就知道它在sda上了。當然,可能你的系統信息級別比較高,上述信息可能沒有打出,這時候你只要tail /var/log/messages就可以看到了。

    方法3:同樣,cat /proc/partitions也可以看到分區信息,其中sd?就是U盤所對應的了。若根本沒有sd設備,就要檢查你的SCSI模塊和usb-storage模塊是否正確加載了。

  3. 在使用U盤或存儲卡時,我該mount /dev/sda還是/dev/sda1呢?

    這是一個歷史遺留問題。存儲卡最初尺寸很小,很多廠商在使用時,就直接使用存儲,不含有分區表信息。而隨着存儲卡尺寸的不斷擴大,它也就引入了類似硬盤分區的概念。例如/dev/hda你可以分成主分區hda1、hda2擴展分區hda3,然後把擴展分區hda3又分爲邏輯分區hda5、hda6、hda7等。這樣,通常的U盤就被分成一個分區sda1,類似把硬盤整個分區分成一個主分區hda1。實際上,我們完全可以通過fdisk /dev/sda對存儲卡進行完全類似硬盤的分區方式分成sda1、sda2甚至邏輯分區sda5、sda6。實際上,對USB硬盤目前你的確需要這樣,因爲它通常都是多少G的容量。而且通常,它裏面就是筆記本硬盤。

    一個好玩的問題。你在Linux下用fdisk /dev/sda 對U盤進行了多分區,這時候到windows下,你會發現怎麼找,怎麼格式化,U盤都只能找到第一個分區大小尺寸,而且使用看不出任何問題。這主要是windows驅動對U盤都只支持一個分區的緣故。你是不是可以利用它來進行一些文件的隱藏和保護?你是不是可以和某些人沒玩過Linux的人開些玩笑:你的U盤容量變小了J。

    現在較多的數碼設備也和windows一樣,是把所有U盤容量分爲一個,所以在對待U盤的時候,通常你mount的是sda1。但對於某些特殊的數碼設備格式化的U盤或存儲卡(目前我發現的是一款聯想的支持模擬USB軟盤的U盤和我的一個數碼相機),你就要mount /dev/sda。因爲它根本就沒分區表(若mount /dev/sda1通常的效果是死掉)。其實,這些信息,只要你注意了/proc/partitions文件,都應該注意到的。

  4. 每次插入U盤,都要尋找對應設備文件名,都要手動mount,我能不能做到象windows那樣插入就可以使用呢。

    當然可以,不過你需要做一些工作。我這裏只提供一些信息幫助你去嘗試完成設置:Linux內核提供了一種叫hotplug支持的東西,它可以讓你係統在PCI設備、USB等設備插拔時做一些事情。而automount 功能可以使你的軟驅、光盤等設備的分區自動掛載和自動卸載。你甚至可以在KDE桌面中創建相應的圖標,方便你操作。具體設置方法就要你自己去嘗試了。反正我使用Linux已經麻木了,不就是敲一行命令嘛。

    USB驅動開發

    在掌握了USB設備的配置後,對於程序員,我們就可以嘗試進行一些簡單的USB驅動的修改和開發了。這一段落,我們會講解一個最基礎USB框架的基礎上,做兩個小的USB驅動的例子。

    USB骨架

    在Linux kernel源碼目錄中driver/usb/usb-skeleton.c爲我們提供了一個最基礎的USB驅動程序。我們稱爲USB骨架。通過它我們僅需要修改極少的部分,就可以完成一個USB設備的驅動。我們的USB驅動開發也是從她開始的。

    那些linux下不支持的USB設備幾乎都是生產廠商特定的產品。如果生產廠商在他們的產品中使用自己定義的協議,他們就需要爲此設備創建特定的驅動程序。當然我們知道,有些生產廠商公開他們的USB協議,並幫助Linux驅動程序的開發,然而有些生產廠商卻根本不公開他們的USB協議。因爲每一個不同的協議都會產生一個新的驅動程序,所以就有了這個通用的USB驅動骨架程序, 它是以pci 骨架爲模板的。

    如果你準備寫一個linux驅動程序,首先要熟悉USB協議規範。USB主頁上有它的幫助。一些比較典型的驅動可以在上面發現,同時還介紹了USB urbs的概念,而這個是usb驅動程序中最基本的。

    Linux USB 驅動程序需要做的第一件事情就是在Linux USB 子系統裏註冊,並提供一些相關信息,例如這個驅動程序支持那種設備,當被支持的設備從系統插入或拔出時,會有哪些動作。所有這些信息都傳送到USB 子系統中,在usb骨架驅動程序中是這樣來表示的:

    static struct usb_driver skel_driver = {
         name:        "skeleton",
         probe:       skel_probe,
         disconnect:  skel_disconnect,
         fops:        &skel_fops,
         minor:       USB_SKEL_MINOR_BASE,
         id_table:    skel_table,
    };
    

    變量name是一個字符串,它對驅動程序進行描述。probe 和disconnect 是函數指針,當設備與在id_table 中變量信息匹配時,此函數被調用。

    fops和minor變量是可選的。大多usb驅動程序鉤住另外一個驅動系統,例如SCSI,網絡或者tty子系統。這些驅動程序在其他驅動系統中註冊,同時任何用戶空間的交互操作通過那些接口提供,比如我們把SCSI設備驅動作爲我們USB驅動所鉤住的另外一個驅動系統,那麼我們此USB設備的read、write等操作,就相應按SCSI設備的read、write函數進行訪問。但是對於掃描儀等驅動程序來說,並沒有一個匹配的驅動系統可以使用,那我們就要自己處理與用戶空間的read、write等交互函數。Usb子系統提供一種方法去註冊一個次設備號和file_operations函數指針,這樣就可以與用戶空間實現方便地交互。

    USB骨架程序的關鍵幾點如下:

    1. USB驅動的註冊和註銷

      Usb驅動程序在註冊時會發送一個命令給usb_register,通常在驅動程序的初始化函數裏。

      當要從系統卸載驅動程序時,需要註銷usb子系統。即需要usb_unregister 函數處理:

      static void __exit usb_skel_exit(void)
      {
         /* deregister this driver with the USB subsystem */
         usb_deregister(&skel_driver);
      }
      module_exit(usb_skel_exit);
      

      當usb設備插入時,爲了使linux-hotplug(Linux中PCI、USB等設備熱插拔支持)系統自動裝載驅動程序,你需要創建一個MODULE_DEVICE_TABLE。代碼如下(這個模塊僅支持某一特定設備):

      /* table of devices that work with this driver */
      static struct usb_device_id skel_table [] = {
          { USB_DEVICE(USB_SKEL_VENDOR_ID,
            USB_SKEL_PRODUCT_ID) },
          { }                      /* Terminating entry */
      };
      MODULE_DEVICE_TABLE (usb, skel_table);
      

      USB_DEVICE宏利用廠商ID和產品ID爲我們提供了一個設備的唯一標識。當系統插入一個ID匹配的USB設備到USB總線時,驅動會在USB core中註冊。驅動程序中probe 函數也就會被調用。usb_device 結構指針、接口號和接口ID都會被傳遞到函數中。

      static void * skel_probe(struct usb_device *dev,
      unsigned int ifnum, const struct usb_device_id *id)
      

      驅動程序需要確認插入的設備是否可以被接受,如果不接受,或者在初始化的過程中發生任何錯誤,probe函數返回一個NULL值。否則返回一個含有設備驅動程序狀態的指針。通過這個指針,就可以訪問所有結構中的回調函數。

      在骨架驅動程序裏,最後一點是我們要註冊devfs。我們創建一個緩衝用來保存那些被髮送給usb設備的數據和那些從設備上接受的數據,同時USB urb 被初始化,並且我們在devfs子系統中註冊設備,允許devfs用戶訪問我們的設備。註冊過程如下:

      /* initialize the devfs node for this device
         and register it */
      sprintf(name, "skel%d", skel->minor);
      skel->devfs = devfs_register 
                    (usb_devfs_handle, name,
                     DEVFS_FL_DEFAULT, USB_MAJOR,
                     USB_SKEL_MINOR_BASE + skel->minor,
                     S_IFCHR | S_IRUSR | S_IWUSR |
                     S_IRGRP | S_IWGRP | S_IROTH,
                     &skel_fops, NULL);
      

      如果devfs_register函數失敗,不用擔心,devfs子系統會將此情況報告給用戶。

      當然最後,如果設備從usb總線拔掉,設備指針會調用disconnect 函數。驅動程序就需要清除那些被分配了的所有私有數據、關閉urbs,並且從devfs上註銷調自己。

      /* remove our devfs node */
      devfs_unregister(skel->devfs);
      

      現在,skeleton驅動就已經和設備綁定上了,任何用戶態程序要操作此設備都可以通過file_operations結構所定義的函數進行了。首先,我們要open此設備。在open函數中MODULE_INC_USE_COUNT 宏是一個關鍵,它的作用是起到一個計數的作用,有一個用戶態程序打開一個設備,計數器就加一,例如,我們以模塊方式加入一個驅動,若計數器不爲零,就說明仍然有用戶程序在使用此驅動,這時候,你就不能通過rmmod命令卸載驅動模塊了。

      /* increment our usage count for the module */
      MOD_INC_USE_COUNT;
      ++skel->open_count;
      /* save our object in the file's private structure */
      file->private_data = skel;
      

      當open完設備後,read、write函數就可以收、發數據了。

    2. skel的write、和read函數

      他們是完成驅動對讀寫等操作的響應。

      在skel_write中,一個FILL_BULK_URB函數,就完成了urb 系統callbak和我們自己的skel_write_bulk_callback之間的聯繫。注意skel_write_bulk_callback是中斷方式,所以要注意時間不能太久,本程序中它就只是報告一些urb的狀態等。

      read 函數與write 函數稍有不同在於:程序並沒有用urb 將數據從設備傳送到驅動程序,而是我們用usb_bulk_msg 函數代替,這個函數能夠不需要創建urbs 和操作urb函數的情況下,來發送數據給設備,或者從設備來接收數據。我們調用usb_bulk_msg函數並傳提一個存儲空間,用來緩衝和放置驅動收到的數據,若沒有收到數據,就失敗並返回一個錯誤信息。

    3. usb_bulk_msg函數

      當對usb設備進行一次讀或者寫時,usb_bulk_msg 函數是非常有用的; 然而, 當你需要連續地對設備進行讀/寫時,建議你建立一個自己的urbs,同時將urbs 提交給usb子系統。

    4. skel_disconnect函數

      當我們釋放設備文件句柄時,這個函數會被調用。MOD_DEC_USE_COUNT宏會被用到(和MOD_INC_USE_COUNT剛好對應,它減少一個計數器),首先確認當前是否有其它的程序正在訪問這個設備,如果是最後一個用戶在使用,我們可以關閉任何正在發生的寫,操作如下:

      /* decrement our usage count for the device */
      --skel->open_count;
      if (skel->open_count <= 0) {
         /* shutdown any bulk writes that might be
            going on */
         usb_unlink_urb (skel->write_urb);
         skel->open_count = 0;
      }
      /* decrement our usage count for the module */
      MOD_DEC_USE_COUNT;
      

      最困難的是,usb 設備可以在任何時間點從系統中取走,即使程序目前正在訪問它。usb驅動程序必須要能夠很好地處理解決此問題,它需要能夠切斷任何當前的讀寫,同時通知用戶空間程序:usb設備已經被取走。

      如果程序有一個打開的設備句柄,在當前結構裏,我們只要把它賦值爲空,就像它已經消失了。對於每一次設備讀寫等其它函數操作,我們都要檢查usb_device結構是否存在。如果不存在,就表明設備已經消失,並返回一個-ENODEV錯誤給用戶程序。當最終我們調用release 函數時,在沒有文件打開這個設備時,無論usb_device結構是否存在、它都會清空skel_disconnect函數所作工作。

      Usb 骨架驅動程序,提供足夠的例子來幫助初始人員在最短的時間裏開發一個驅動程序。更多信息你可以到linux usb開發新聞組去尋找。

    U盤、USB讀卡器、MP3、數碼相機驅動

    對於一款windows下用的很爽的U盤、USB讀卡器、MP3或數碼相機,可能Linux下卻不能支持。怎麼辦?其實不用傷心,也許經過一點點的工作,你就可以很方便地使用它了。通常是此U盤、USB讀卡器、MP3或數碼相機在WindowsXP中不需要廠商專門的驅動就可以識別爲移動存儲設備,這樣的設備才能保證成功,其他的就看你的運氣了。

    USB存儲設備,他們的read、write等操作都是通過上章節中提到的鉤子,把自己的操作鉤到SCSI設備上去的。我們就不需要對其進行具體的數據讀寫處理了。

    第一步:我們通過cat /proc/bus/usb/devices得到當前系統探測到的USB總線上的設備信息。它包括Vendor、ProdID、Product等。下面是我買的一款雜牌CF卡讀卡器插入後的信息片斷:

    T: Bus=01 Lev=01 Prnt=01 Port=01 Cnt=02 Dev#= 5 Spd=12 MxCh= 0 
    D: Ver= 1.10 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=8 #Cfgs= 1 
    P: Vendor=07c4 ProdID=a400 Rev= 1.13 
    S: Manufacturer=USB 
    S: Product=Mass Storage 
    C:* #Ifs= 1 Cfg#= 1 Atr=80 MxPwr=70mA 
    I: If#= 0 Alt= 0 #EPs= 2 Cls=08(vend.) Sub=06 Prot=50 Driver=usb-storage 
    E: Ad=81(I) Atr=02(Bulk) MxPS= 64 Ivl= 0ms
    E: Ad=02(O) Atr=02(Bulk) MxPS= 64 Ivl= 0ms
    

    其中,我們最關心的是Vendor=07c4 ProdID=a400和Manufacturer=USB(果然是雜牌,廠商名都看不到)Product= Mass Storage。

    對於這些移動存儲設備,我們知道Linux下都是通過usb-storage.o驅動模擬成scsi設備去支持的,之所以不支持,通常是usb-storage驅動未包括此廠商識別和產品識別信息(在類似skel_probe的USB最初探測時被屏蔽了)。對於USB存儲設備的硬件訪問部分,通常是一致的。所以我們要支持它,僅需要修改usb-storage中關於廠商識別和產品識別列表部分。

    第二部,打開drivers/usb/storage/unusual_devs.h文件,我們可以看到所有已知的產品登記表,都是以UNUSUAL_DEV(idVendor, idProduct, bcdDeviceMin, bcdDeviceMax, vendor_name, product_name, use_protocol, use_transport, init_function, Flags)方式登記的。其中相應的涵義,你就可以根據命名來判斷了。所以只要我們如下填入我們自己的註冊,就可以讓usb-storage驅動去認識和發現它。

    UNUSUAL_DEV(07c4, a400, 0x0000, 0xffff, 
    " USB ", " Mass Storage ", 
    US_SC_SCSI, US_PR_BULK, NULL, 
    US_FL_FIX_INQUIRY | US_FL_START_STOP |US_FL_MODE_XLATE )
    

    注意:添加以上幾句的位置,一定要正確。比較發現,usb-storage驅動對所有註冊都是按idVendor, idProduct數值從小到大排列的。我們也要放在相應位置。

    最後,填入以上信息,我們就可以重新編譯生成內核或usb-storage.o模塊。這時候插入我們的設備就可以跟其他U盤一樣作爲SCSI設備去訪問了。

    鍵盤飛梭支持

    目前很多鍵盤都有飛梭和手寫板,下面我們就嘗試爲一款鍵盤飛梭加入一個驅動。在通常情況,當我們插入USB接口鍵盤時,在/proc/bus/usb/devices會看到多個USB設備。比如:你的USB鍵盤上的飛梭會是一個,你的手寫板會是一個,若是你的USB鍵盤有USB擴展連接埠,也會看到。

    下面是具體看到的信息

    T:  Bus=02 Lev=00 Prnt=00 Port=00 Cnt=00 Dev#=  1 Spd=12  MxCh= 2
    B:  Alloc= 11/900 us ( 1%), #Int=  1, #Iso=  0
    D:  Ver= 1.00 Cls=09(hub  ) Sub=00 Prot=00 MxPS= 8 #Cfgs=  1
    P:  Vendor=0000 ProdID=0000 Rev= 0.00
    S:  Product=USB UHCI Root Hub
    S:  SerialNumber=d800
    C:* #Ifs= 1 Cfg#= 1 Atr=40 MxPwr=  0mA
    I:  If#= 0 Alt= 0 #EPs= 1 Cls=09(hub  ) Sub=00 Prot=00 Driver=hub
    E:  Ad=81(I) Atr=03(Int.) MxPS=   8 Ivl=255ms
    T:  Bus=02 Lev=01 Prnt=01 Port=01 Cnt=01 Dev#=  3 Spd=12  MxCh= 3
    D:  Ver= 1.10 Cls=09(hub  ) Sub=00 Prot=00 MxPS= 8 #Cfgs=  1
    P:  Vendor=07e4 ProdID=9473 Rev= 0.02
    S:  Manufacturer=ALCOR
    S:  Product=Movado USB Keyboard
    C:* #Ifs= 1 Cfg#= 1 Atr=e0 MxPwr=100mA
    I:  If#= 0 Alt= 0 #EPs= 1 Cls=09(hub  ) Sub=00 Prot=00 Driver=hub
    E:  Ad=81(I) Atr=03(Int.) MxPS=   1 Ivl=255ms
    

    找到相應的信息後就可開始工作了。實際上,飛梭的定義和鍵盤鍵碼通常是一樣的,所以我們參照drivers/usb/usbkbd..c代碼進行一些改動就可以了。因爲沒能拿到相應的硬件USB協議,我無從知道飛梭在按下時通訊協議衆到底發什麼,我只能把它的信息打出來進行分析。幸好,它比較簡單,在下面代碼的usb_kbd_irq函數中if(kbd->new[0] == (char)0x01)和if(((kbd->new[1]>>4)&0x0f)!=0x7)就是判斷飛梭左旋。usb_kbd_irq函數就是鍵盤中斷響應函數。他的掛接,就是在usb_kbd_probe函數中

    FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp,
    		usb_kbd_irq, kbd, endpoint->bInterval);
    

    一句中實現。

    從usb骨架中我們知道,usb_kbd_probe函數就是在USB設備被系統發現是運行的。其他部分就都不是關鍵了。你可以根據具體的探測值(Vendor=07e4 ProdID=9473等)進行一些修改就可以了。值得一提的是,在鍵盤中斷中,我們的做法是收到USB飛梭消息後,把它模擬成左方向鍵和右方向鍵,在這裏,就看你想怎麼去響應它了。當然你也可以響應模擬成F14、F15等擴展鍵碼。

    在瞭解了此基本的驅動後,對於一個你已經拿到通訊協議的鍵盤所帶手寫板,你就應該能進行相應驅動的開發了吧。

    程序見附錄1: 鍵盤飛梭驅動。

    使用此驅動要注意的問題:在加載此驅動時你必須先把hid設備卸載,加載完usbhkey.o模塊後再加載hid.o。因爲若hid存在,它的probe會屏蔽系統去利用我們的驅動發現我們的設備。其實,飛梭本來就是一個hid設備,正確的方法,或許你應該修改hid的probe函數,然後把我們的驅動融入其中。

    參考資料

    1. 《LINUX設備驅動程序》 
      ALESSANDRO RUBINI著 
      LISOLEG 譯 
    2. 《Linux系統分析與高級編程技術》 
      周巍鬆 編著 
    3. Linux Kernel-2.4.20源碼和文檔說明 

    附錄1:鍵盤飛梭驅動

    #include <linux/kernel.h>
    #include <linux/slab.h>
    #include <linux/module.h>
    #include <linux/input.h>
    #include <linux/init.h>
    #include <linux/usb.h>
    #include <linux/kbd_ll.h>
    /*
     * Version Information
     */
    #define DRIVER_VERSION ""
    #define DRIVER_AUTHOR "TGE HOTKEY "
    #define DRIVER_DESC "USB HID Tge hotkey driver"
    #define USB_HOTKEY_VENDOR_ID 0x07e4
    #define USB_HOTKEY_PRODUCT_ID 0x9473
    //廠商和產品ID信息就是/proc/bus/usb/devices中看到的值
    MODULE_AUTHOR( DRIVER_AUTHOR );
    MODULE_DESCRIPTION( DRIVER_DESC );
    struct usb_kbd {
      struct input_dev dev;
      struct usb_device *usbdev;
      unsigned char new[8];
      unsigned char old[8];
      struct urb irq, led;
    //  devrequest dr;     
    //這一行和下一行的區別在於kernel2.4.20版本對usb_kbd鍵盤結構定義發生了變化
          struct usb_ctrlrequest dr;
      unsigned char leds, newleds;
      char name[128];
      int open;
    };
    //此結構來自內核中drivers/usb/usbkbd..c
    static void usb_kbd_irq(struct urb *urb)
    {
      struct usb_kbd *kbd = urb->context;
            int *new;
            new = (int *) kbd->new;
      if(kbd->new[0] == (char)0x01)
      {
        if(((kbd->new[1]>>4)&0x0f)!=0x7)
        {
    handle_scancode(0xe0,1);
    handle_scancode(0x4b,1);
                    handle_scancode(0xe0,0);
                    handle_scancode(0x4b,0);
        }
        else
        {
            handle_scancode(0xe0,1);
                    handle_scancode(0x4d,1);
                    handle_scancode(0xe0,0);
                    handle_scancode(0x4d,0);
        }
      }
      
      
      printk("new=%x %x %x %x %x %x %x %x",
        kbd->new[0],kbd->new[1],kbd->new[2],kbd->new[3],
        kbd->new[4],kbd->new[5],kbd->new[6],kbd->new[7]); 
        
    }
    static void *usb_kbd_probe(struct usb_device *dev, unsigned int ifnum,
                               const struct usb_device_id *id)
    {
      struct usb_interface *iface;
            struct usb_interface_descriptor *interface;
      struct usb_endpoint_descriptor *endpoint;
            struct usb_kbd *kbd;
            int  pipe, maxp;
      iface = &dev->actconfig->interface[ifnum];
            interface = &iface->altsetting[iface->act_altsetting];
      if ((dev->descriptor.idVendor != USB_HOTKEY_VENDOR_ID) ||
        (dev->descriptor.idProduct != USB_HOTKEY_PRODUCT_ID) ||
        (ifnum != 1))
      {
        return NULL;
      }
      if (dev->actconfig->bNumInterfaces != 2)
      {
        return NULL;  
      }
      if (interface->bNumEndpoints != 1) return NULL;
            endpoint = interface->endpoint + 0;
            pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
            maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe));
            usb_set_protocol(dev, interface->bInterfaceNumber, 0);
            usb_set_idle(dev, interface->bInterfaceNumber, 0, 0);
      printk(KERN_INFO "GUO: Vid = %.4x, Pid = %.4x, Device = %.2x,
                         ifnum = %.2x, bufCount = %.8x\\n",
      dev->descriptor.idVendor,dev->descriptor.idProduct,
                    dev->descriptor.bcdDevice, ifnum, maxp);
            if (!(kbd = kmalloc(sizeof(struct usb_kbd), GFP_KERNEL))) return NULL;
            memset(kbd, 0, sizeof(struct usb_kbd));
            kbd->usbdev = dev;
            FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp,
        usb_kbd_irq, kbd, endpoint->bInterval);
      kbd->irq.dev = kbd->usbdev;
      if (dev->descriptor.iManufacturer)
                    usb_string(dev, dev->descriptor.iManufacturer, kbd->name, 63);
      if (usb_submit_urb(&kbd->irq)) {
                    kfree(kbd);
                    return NULL;
            }
      
      printk(KERN_INFO "input%d: %s on usb%d:%d.%d\\n",
                     kbd->dev.number, kbd->name, dev->bus->busnum, 
                                 dev->devnum, ifnum);
            return kbd;
    }
    static void usb_kbd_disconnect(struct usb_device *dev, void *ptr)
    {
      struct usb_kbd *kbd = ptr;
            usb_unlink_urb(&kbd->irq);
            kfree(kbd);
    }
    static struct usb_device_id usb_kbd_id_table [] = {
      { USB_DEVICE(USB_HOTKEY_VENDOR_ID, USB_HOTKEY_PRODUCT_ID) },
      { }            /* Terminating entry */
    };
    MODULE_DEVICE_TABLE (usb, usb_kbd_id_table);
    static struct usb_driver usb_kbd_driver = {
      name:    "Hotkey",
      probe:    usb_kbd_probe,
      disconnect:  usb_kbd_disconnect,
      id_table:  usb_kbd_id_table,
      NULL,
    };
    static int __init usb_kbd_init(void)
    {
      usb_register(&usb_kbd_driver);
      info(DRIVER_VERSION ":" DRIVER_DESC);
      return 0;
    }
    static void __exit usb_kbd_exit(void)
    {
      usb_deregister(&usb_kbd_driver);
    }
    module_init(usb_kbd_init);
    module_exit(usb_kbd_exit);
    



發佈了9 篇原創文章 · 獲贊 6 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章