天天看點

Linux USB 驅動開發(五)—— USB驅動程式開發過程簡單總結

       裝置驅動程式是作業系統核心和機器硬體之間的接口,由一組函數和一些私有資料組成,是應用程式和硬體裝置之間的橋梁。在應用程式看來,硬體裝置隻是一個裝置檔案,應用程式可以像操作普通檔案一樣對硬體裝置進行操作。

      裝置驅動程式是核心的一部分,主要完成以下功能:對裝置的初始化和釋放;把資料從核心傳送到硬體裝置和從硬體裝置讀取資料;讀取應用程式資料傳送給裝置檔案和回送應用程式請求的資料;檢測和處理硬體裝置出現的錯誤。

一、 Linux USB子系統分析

        在Linux系統中,USB主機驅動程式由3部分組成:USB主機控制器驅動(HCD)、USB核心驅動(USBD)和不同種類的USB裝置類驅動,如下所示。其中HCD和USBD被稱為協定軟體或者協定棧,這兩部分共同處理與協定相關的操作。

       USB裝置類驅動可以包含多個,不同的功能接口對應不同的驅動程式,它們不直接與USB裝置硬體打交道,而是通過協定軟體的抽象處理來完成與裝置的不同功能接口之間的通信。

Linux USB 驅動開發(五)—— USB驅動程式開發過程簡單總結

       在Linux USB子系統中,HCD是直接和硬體進行互動的軟體子產品,是USB協定棧的最底層部分,是USB主機控制器硬體和資料傳輸的一種抽象。

      HCD向上僅對USB總線驅動程式服務,HCD提供了一個軟體接口,即HCDI,使得各種USB主機控制器的硬體特性都被軟體化,并受USB總線驅動程式的調用和管理。HCD向下則直接管理和檢測主要制器硬體的各種行為。HCD提供的功能主要有:主機控制器硬體初始化;為USBD層提供相應的接口函數;提供根HUB(ROOT HUB)裝置配置、控制功能;完成4種類型的資料傳輸等。

      USBD部分是整個USB主機驅動的核心,主要實作的功能有:USB總線管理;USB總線裝置管理、USB總線帶寬管理、USB的4種類型資料傳輸、USB HUB驅動、為USB裝置驅動提供相關接口、提供應用程式通路USB系統的檔案接口等。其中USB HUB作為一類特殊的USB裝置,其驅動程式被包含在USBD層。

     在嵌入式Linux系統中,已經包含HCD子產品和USB核心驅動USBD,不需要使用者重新編寫,使用者僅僅需要完成USB裝置類驅動即可。

二、Linux系統中USB子系統的主要資料結構

        Linux系統中,USBD通過定義一組宏、資料結構和函數來抽象出所有硬體或是裝置具有依賴關系的部分。

USBD中主要有四個資料結構,分别是:

1.usb_device儲存一個USB裝置的資訊,包括裝置位址,裝置描述符,配置描述符等。

2.usb_bus儲存一個USB總線系統的資訊,包括總線上裝置位址資訊,根集線器,帶寬使用情況等。一個USB總線系統至少有一個主機控制器和一個根集線器,Linux系統支援多USB總線系統。

3.usb_driver儲存客戶驅動資訊,包括驅動名稱,以及驅動提供給USB核心使用的函數指針等。

4.URB(Universal Request Block)是進行USB通信的資料結構,USBD通過URB在USB裝置類驅動和USBD、USBD和HCD間進行資料傳輸。

三、Linux系統中USB裝置的加載與解除安裝

       當把一個USB裝置插入到一個USB HUB的某個端口時,集中器就會檢測到裝置的接入,進而在下一次受到主機通過中斷互動查詢時就會向其報告。集中器的端口在沒有裝置接入時都處于關閉狀态,插入裝置之後也不會自動打開,必須由主機通過控制互動發出指令予以打開。是以,在得到集中器的報告之後,主機的USB驅動程式就會為新插入的裝置排程若幹個控制互動,并向集中器發出打開這個端口的指令,這樣新插入的裝置就會出現在USB總線上了,并為該裝置配置設定唯一的位址。

       HUB驅動程式調用函數usb_connect(struct usb_device *dev)和usb_new_device(struct usb_device *dev)解析裝置的各種描述符資訊,配置設定資源,并與相應的裝置驅動程式建立聯系。

函數usb_new_device主要完成以下工作:

1.調用usb_set_address把新配置設定的裝置位址傳送給裝置。

2.調用usb_get_descriptor獲得裝置的裝置描述符,得到裝置端點的包的最大長度,接下來的控制傳輸按這個資料包最大長度進行。

3.調用usb_get_configuration得到裝置的所有配置描述符、接口描述符和端點描述符資訊。

4.調用usb_set_configuration激活目前的配置作為預設工作配置。

5.在目錄“proc/bus/usb”中為裝置建立節點。

6.在USB子系統中,通過函數usb_find_drivers和usb_find_interface_driver,為裝置的每一個接口尋找相應的驅動程式,驅動程式對接口進行配置并為它們配置設定所需的資源。當每個接口被成功驅動後,此裝置就能正常工作了。

      裝置拔下時,與之相聯的集線器首先檢測到裝置的拔下信号,通過中斷傳輸将資訊傳送給集線器的驅動,集線器的驅動先驗證裝置是否被拔下,如果是則調用usb_disconnect(struct usb_device **pdev)進行處理。裝置斷開後,USB系統找到裝置目前活動配置的每個接口的驅動程式,調用它們提供的disconnect接口函數,中斷它們與各個接口的資料傳輸操作,釋放它們為每個接口配置設定的資源。如果此裝置是集線器,則遞歸調用usb_disconnect來處理它的子裝置,釋放裝置位址,通過usbdevfs_remove_device函數釋放給裝置建立的檔案節點,通過usb_free_dev釋放USBD給裝置配置設定的資源。

四、編寫USB驅動程式步驟

1、所有usb驅動都必須建立主要結構體struct usb_driver

struct usb_driver

->struct module *owner

   (有他可正确對該驅動程式引用計數,應為THIS_MODULE)

->const char *name

   (驅動名字,運作時可在檢視 /sys/bus/usb/drivers/)

->const struct usb_device_id *id_table

   (包含該驅動可支援的所有不同類型的驅動裝置,沒添探測回調函數不會被調用)

->int (*probe)(struct usb_interface *intf,const struct usb_device_id *id)

   (usb驅動探測函數,确認後struct usb_interface 應恰當初始化,然後返0,如果出錯則返負值)

->void(*disconnect)(struct usb_interface *intf)

   (當struct usb_interface 被從系統中移除或驅動正從usb核心中解除安裝時,usb核心将調用此函數)

代碼執行個體:

static struct usb_driver skel_driver={
    .owner = THIS_MODULE,
    .name = "skeleton",
    .id_table = skel_table,
    .probe = skel_probe,
    .disconnect = skel_disconnect,
};
           

2、usb_register()注冊将struct usb_driver 注冊到usb核心,傳統是在usb驅動程式子產品初始化代碼中完成該工作的

static int __init usb_skel_init(void)
{
       ... 
       usb_register(&skel_driver);
       ...
}
           

3、struct usb_device_id usb核心用該表判斷哪個裝置該使用哪個驅動程式,熱插拔腳本使用它來确定當一個特定的裝置插入到系統時該自動裝載哪個驅動程式。

->__u16 match_flags(确定裝置和結構體中下列字段中哪一個相比對)

->__u16 idVendor(裝置的usb制造商id)

->__u16 idProduct(裝置的usb産品id) 

4、USB骨架程式的關鍵幾點如下:

a -- USB驅動的注冊和登出 

   Usb驅動程式在注冊時會發送一個指令給usb_register,通常在驅動程式的初始化函數裡。

   當要從系統解除安裝驅動程式時,需要登出usb子系統。即需要usb_unregister 函數處理。

b -- 當usb裝置插入時,為了使linux-hotplug(Linux中PCI、USB等裝置熱插拔支援)系統自動裝載驅動程式,你需要建立一個MODULE_DEVICE_TABLE

代碼如下(這個子產品僅支援某一特定裝置):

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都會被傳遞到函數中。

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

       驅動程式需要确認插入的裝置是否可以被接受,如果不接受,或者在初始化的過程中發生任何錯誤,probe函數傳回一個NULL值。否則傳回一個含有裝置驅動程式狀态的指針。通過這個指針,就可以通路所有結構中的回調函數。

d -- 在骨架驅動程式裡,最後一點是我們要注冊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上登出調自己。

  devfs_unregister(skel->;devfs);

5、其他

a -- struct usb_host_endpoint(描述usb端點)

→(包含)struct usb_endpoint_descriptor(含真正端點資訊,資料格式,是真正驅動關心的字段)

 端點描述符:

bEndpointAddress = 81(in)(第8位為1是輸入裝置)(usb的端點位址,包含端點方向)

bmAttibutes = 03(interrupt)(端點類型,為中斷傳輸)

wMaxPacketSize = 0008(每次傳8個位元組)(端點每次可處理最大位元組長度)

bInterval = 08(8ms)(如端點為中斷,該值為輪詢間隔)

b -- usb端點捆綁為接口,usb接口隻處理一種usb邏輯連接配接,如滑鼠鍵盤等

   一個usb裝置可有多接口,usb揚聲器:一個usb鍵盤用于按鍵,一個usb音頻流,則需兩個不同的驅動程式。

   usb驅動 通常将struct usb_interface 轉成 struct usb_device 用函數 interface_to_usbdev轉 

c -- struct usb_interface 描述usb接口

   →struct usb_host_interface * altsetting(接口結構體數組,包含所有可能用于該接口的可選設定)

    →struct usb_host_endpoint

   →unsigned num_altsetting(可選設定的數量)

   →struct usb_host_interface * cur_altsetting(接口目前活動設定)

   →int minor(usb核心配置設定給接口的次裝置号,成功調用usb_register_dev有效) 

d -- usb裝置非常複雜,由許多不同邏輯單元組成,簡單關系如下:

   裝置通常有一個以上的配置

   配置經常有一個以上接口

   接口通常有一個以上設定

   接口通常有一個以上端點

   裝置描述-》配置描述-》接口描述-》端點描述 

e -- usb sysfs裝置命名方案

   根集線器-集線器端口号:配置。接口

   對于usb hub樹中層次更高的字樹命名方案

   根集線器-集線器端口号-集線器端口号:配置。接口 

f --  linux核心的代碼通過一個成為urb(usb請求塊)和所有usb裝置通信.  

 用struct urb描述(include/linux/usb.h中定義) 

   ->urb用異步同usb裝置特定usb端點發送/接收資料,使用類似網絡代碼中的struct skbuff

   -> urb 被動态建立,随時可被驅動程式或usb核心取消,内部有引用計數,可被多次調用,使他們可在最後一個使用者釋放他們時自動地銷毀

   -> urb使得流處理或其他複雜的重疊的通信成為可能,獲得高資料傳輸速度。 

   ->usb_alloc_urb() 建立urb包 usb_free_urb() 釋放urb包 

   ->usb_fill_int_urb()正确初始化将發送到usb裝置的中斷端點urb

     usb_fill_bulk_urb() .. .. .. ... 批量傳輸端點urb

     usb_fill_control_urb() .. .. .. ... 控制端點urb

     等時urb在送出給核心時必須手動初始化(很不幸,沒函數)

   ->usb_submit_urb()urb被usb驅動正确建立和初始化後,就可送出到usb核心,發送到usb裝置上了,如果調用成功,函數返0,urb控制權轉給usb核心

   ->usb_kill_urb() or usb_unlink_urb()取消已經被送出給核心的urb 

五、USB驅動開發簡單示例

1、嵌入式Linux系統中USB攝像頭驅動程式實作

     通常USB裝置類驅動程式需要提供兩個資料結構接口,一個針對USBD層,一個針對檔案系統。USB攝像頭驅動程式需要做的第一件事情就是在USB子系統裡注冊,并提供一些相關資訊,包括該驅動程式支援哪些裝置,當被支援的裝置從總線插入或拔出時,會有哪些動作等,所有這些資訊通過usb_driver的形式傳送到USBD中,具體實作如下:

static struct usb_driver cam_driver = {
	.name: "cam_video",
	.probe: cam_probe,
	.disconnect: cam_disconnect,
	.id_table: cam_ids,
};	
           

其中

cam_video是用戶端驅動程式的字元串名稱,用于避免驅動程式的重複安裝和解除安裝;

cam_probe則指向USB驅動程式的探測函數指針,提供給USB核心的函數,用于判斷驅動程式是否能對裝置的某個接口進行驅動;

cam_disconnect指向USB驅動程式中的斷開函數的指針,當從系統中被移除或者驅動程式正在從USB核心中解除安裝時,USB核心将調用該函數;

cam_ids清單包含了一系列該驅動程式可以支援的所有不同類型的USB裝置,如沒有設定該清單,則該驅動程式中的探測回調函數不會被調用。

       當一個攝像頭連接配接到USB總線上時,USB核心通過調用camDrive.c中的cam_probe函數判斷是否支援該裝置,如果支援,為該裝置建立裝置檔案節點,以後應用程式就可以通過标準POSIX函數,把該裝置當成普通檔案來通路。攝像頭驅動程式定義的檔案系統接口如下:

struct file_operations cam_fops = {
	.owner     = THIS_MODULE,
	.open      = cam_v 4l2_open,
	.release   = cam_v4l2_release,
	.ioctl     = cam_v4l2_ioctl,
	.llseek    = no_llseek,
	.read      = cam_v4l2_read,
	.mmap      = cam_v4l2_mmap,
	.poll      = cam_v4l2_poll,
};
           

     在USB攝像頭驅動程式的初始化函數中,通過usb_register進行裝置注冊;當從系統解除安裝驅動程式時,需要通過usb_deregister進行解除安裝。當驅動程式向USB子系統注冊後,插入一個新的USB裝置後總是要調用cam_probe函數進行裝置驅動程式的查找,以确定新的USB裝置硬體中的生産廠商ID和産品自定義ID是否與驅動程式相符,進而确定是否使用該驅動程式。

 2、USB攝像頭驅動程式測試

       在嵌入式Linux系統中,USB攝像頭被注冊為一個标準的視訊裝置/dev/video,通過影像裝置API接口Video4Linux來擷取視訊和音頻資料。

現有的Video4Linux有兩個版本:v4l和v4l2。通過v4l2 API接口擷取視訊圖像的主要操作步驟如下:

a -- 打開視訊裝置

在Linux系統中,攝像頭的裝置檔案為/dev/video0,調用系統函數open打開該裝置。

fd = open (dev_name, O_RDWR);

b -- 擷取視訊裝置所支援的V4L2特性

      所有的V4L2裝置驅動都需要支援VIDIOC_QUERYCAP_ioctl的系統調用。通過該調用,确定該驅動程式是否與V4L2規範相相容,同時擷取該裝置所支援的V4L2特性。在攝像頭應用程式的開發過程中,需要判定該裝置是否支援視訊捕獲。

ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);

c -- 擷取視訊裝置支援的各種特性

      接着,利用ioctl(fd,VIDIOC_QUERYCAP,&cap)函數讀取struct v4l2_capability中有關攝像頭的資訊。該函數成功傳回後,這些資訊從核心空間拷貝到使用者程式空間capability各成員分量中。

ioctl(device_fd, VIDIOCGCAP, &vidcap);

d -- 設定視訊捕獲的圖像格式

memset(&fmt, 0, sizeof(struct v4l2_format));

fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

fmt.fmt.pix.width = vd->width;

fmt.fmt.pix.height = vd->height;

fmt.fmt.pix.pixelformat = vd->formatIn;

ret = ioctl(fd, VIDIOC_S_FMT, &fmt);

e -- 視訊資料幀捕獲

ioctl (fd, VIDIOC_DQBUF, &buf);

擷取到視訊資料之後,放到buf緩沖區中,通過QT桌面應用開發系統,顯示到LCD顯示屏上,通過觸摸屏進行互動控制。

繼續閱讀