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問題
- 有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檔案。 - 我們已經知道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模組是否正確載入了。
- 在使用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檔案,都應該注意到的。
- 每次插入U盤,都要尋找對應裝置檔名,都要手動mount,我能不能做到象windows那樣插入就可以使用呢。
當然可以,不過你需要做一些工作。我這裡只提供一些資訊幫助你去嘗試完成設定:Linux核心提供了一種叫hotplug支援的東西,它可以讓你係統在PCI裝置、USB等裝置插拔時做一些事情。而automount 功能可以使你的軟碟機、光碟等裝置的分割槽自動掛載和自動解除安裝。你甚至可以在KDE桌面中建立相應的圖示,方便你操作。具體設定方法就要你自己去嘗試了。反正我使用Linux已經麻木了,不就是敲一行命令嘛。
USB骨架程式(usb-skeleton),是USB驅動程式的基礎,通過對它原始碼的學習和理解,可以使我們迅速地瞭解USB驅動架構,迅速地開發我們自己的USB硬體的驅動。
前言
在上篇《 Linux下的硬體驅動--USB裝置(上)(驅動配製部分)》中,我們知道了在Linux下如何去使用一些最常見的USB裝置。但對於做系統設計的程式設計師來說,這是遠遠不夠的,我們還需要具有驅動程式的閱讀、修改和開發能力。在此下篇中,就是要通過簡單的USB驅動的例子,隨您一起進入USB驅動開發的世界。
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骨架程式的關鍵幾點如下:
- 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函式就可以收、發資料了。
- 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函式並傳提一個儲存空間,用來緩衝和放置驅動收到的資料,若沒有收到資料,就失敗並返回一個錯誤資訊。
- usb_bulk_msg函式
當對usb裝置進行一次讀或者寫時,usb_bulk_msg 函式是非常有用的; 然而, 當你需要連續地對裝置進行讀/寫時,建議你建立一個自己的urbs,同時將urbs 提交給usb子系統。
- 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函式,然後把我們的驅動融入其中。
參考資料
- 《LINUX裝置驅動程式》
ALESSANDRO RUBINI著
LISOLEG 譯 - 《Linux系統分析與高階程式設計技術》
周巍鬆 編著 - 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); |
關於作者
趙明,聯想軟體設計中心嵌入式研發處系統設計工程師,一直致力於WinCE、WinXPE、Linux等嵌入式系統研究。您可以通過 [email protected]與他聯絡。 |