Linux下的硬體驅動——USB設備(下)(驅動開發部分

火星人 @ 2014-03-09 , reply:0



聯想軟體設計中心嵌入式研發處系統設計工程師
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開發新聞組去尋找.

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





[火星人 via ] Linux下的硬體驅動——USB設備(下)(驅動開發部分已經有303次圍觀

http://www.coctec.com/docs/linux/show-post-57581.html