Linux下的硬體驅動——USB裝置(下)(驅動開發部分)(轉)

post0發表於2007-08-09
Linux下的硬體驅動——USB裝置(下)(驅動開發部分)(轉)[@more@]

聯想軟體設計中心嵌入式研發處系統設計工程師

2003年7月

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開發新聞組去尋找。

隨身碟、USB讀卡器、MP3、數位相機驅動

對於一款windows下用的很爽的隨身碟、USB讀卡器、MP3或數位相機,可能Linux下卻不能支援。怎麼辦?其實不用傷心,也許經過一點點的工作,你就可以很方便地使用它了。通常是此隨身碟、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模組。這時候插入我們的裝置就可以跟其他隨身碟一樣作為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

#include

#include

#include

#include

#include

#include

/*

* 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 ",

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 ",

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等嵌入式系統研究。您可以透過carl__zhao@163.com與他聯絡。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-938888/,如需轉載,請註明出處,否則將追究法律責任。

相關文章