1. 程式人生 > >Linux USB 驅動開發(五)—— USB驅動程式開發過程簡單總結

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

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

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

一、 Linux USB子系統分析

        在Linux系統中,USB主機驅動程式由3部分組成:USB主機控制器驅動(HCD)USB核心驅動(USBD)不同種類的USB裝置類驅動

,如下所示。其中HCD和USBD被稱為協議軟體或者協議棧,這兩部分共同處理與協議相關的操作。

       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_driversusb_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上登出調自己。
  /* remove our devfs node */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顯示屏上,通過觸控式螢幕進行互動控制。