編寫USB裝置驅動程式

作者:

Greg Kroah-Hartman

簡介

Linux USB子系統已經從僅支援2.2.7核心中的兩種不同型別的裝置(滑鼠和鍵盤),發展到2.4核心中的20多種不同型別的裝置。 Linux當前支援幾乎所有的USB類裝置(標準型別的裝置,如鍵盤、滑鼠、調變解調器、印表機和揚聲器)以及數量不斷增長的特定廠商裝置(如USB轉序列埠轉換器、數碼相機、乙太網裝置和MP3播放器)。 有關當前支援的不同USB裝置的完整列表,請參見資源。

Linux上不支援的剩餘型別的USB裝置幾乎都是特定廠商的裝置。 每個廠商決定實現自定義協議來與他們的裝置通訊,因此通常需要建立自定義驅動程式。 一些廠商公開他們的USB協議並幫助建立Linux驅動程式,而另一些廠商則不釋出它們,開發人員被迫進行逆向工程。 有關一些方便的逆向工程工具的連結,請參見資源。

因為每個不同的協議都會導致建立一個新的驅動程式,所以我編寫了一個通用的USB驅動程式框架,該框架模仿核心原始碼樹中的pci-skeleton.c檔案,許多PCI網路驅動程式都基於該檔案。 此USB框架可以在核心原始碼樹的drivers/usb/usb-skeleton.c中找到。 在本文中,我將介紹該框架驅動程式的基礎知識,解釋不同的部分以及需要完成哪些工作才能將其自定義為特定裝置。

Linux USB基礎知識

如果您要編寫Linux USB驅動程式,請熟悉USB協議規範。 可以在USB主頁(請參閱資源)上找到它以及許多其他有用的文件。 在USB工作裝置列表(請參閱資源)中可以找到對Linux USB子系統的出色介紹。 它解釋了Linux USB子系統的結構,並向讀者介紹了USB urb(USB請求塊)的概念,這對於USB驅動程式至關重要。

Linux USB驅動程式需要做的第一件事是在Linux USB子系統中註冊自己,並提供有關該驅動程式支援哪些裝置以及在系統插入或刪除該驅動程式支援的裝置時要呼叫哪些函式的資訊。 所有這些資訊都透過usb_driver結構傳遞給USB子系統。 框架驅動程式將usb_driver宣告為

static struct usb_driver skel_driver = {
        .name        = "skeleton",
        .probe       = skel_probe,
        .disconnect  = skel_disconnect,
        .suspend     = skel_suspend,
        .resume      = skel_resume,
        .pre_reset   = skel_pre_reset,
        .post_reset  = skel_post_reset,
        .id_table    = skel_table,
        .supports_autosuspend = 1,
};

變數名稱是描述驅動程式的字串。 它用於列印到系統日誌的資訊性訊息中。 當看到或刪除與id_table變數中提供的資訊匹配的裝置時,將呼叫probe和disconnect函式指標。

fops和minor變數是可選的。 大多數USB驅動程式都掛接到另一個核心子系統,例如SCSI,網路或TTY子系統。 這些型別的驅動程式在其他核心子系統中註冊自己,並且任何使用者空間互動都透過該介面提供。 但是對於沒有匹配的核心子系統的驅動程式,例如MP3播放器或掃描器,則需要一種與使用者空間互動的方法。 USB子系統提供了一種註冊次裝置號和一組file_operations函式指標的方法,這些函式指標啟用了此使用者空間互動。 框架驅動程式需要這種型別的介面,因此它提供了一個次起始編號和一個指向其file_operations函式的指標。

然後,透過呼叫usb_register()(通常在驅動程式的init函式中)來註冊USB驅動程式,如下所示

static int __init usb_skel_init(void)
{
        int result;

        /* register this driver with the USB subsystem */
        result = usb_register(&skel_driver);
        if (result < 0) {
                pr_err("usb_register failed for the %s driver. Error number %d\n",
                       skel_driver.name, result);
                return -1;
        }

        return 0;
}
module_init(usb_skel_init);

從系統解除安裝驅動程式時,它需要向USB子系統登出自身。 這透過usb_deregister()函式完成

static void __exit usb_skel_exit(void)
{
        /* deregister this driver with the USB subsystem */
        usb_deregister(&skel_driver);
}
module_exit(usb_skel_exit);

要使linux-hotplug系統在插入裝置時自動載入驅動程式,您需要建立一個MODULE_DEVICE_TABLE。 以下程式碼告訴熱插拔指令碼,此模組支援具有特定供應商和產品ID的單個裝置

/* 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);

在描述struct usb_device_id(用於支援整個USB驅動程式類別的驅動程式)時,可以使用其他宏。 有關更多資訊,請參見usb.h

裝置操作

當將與您的驅動程式在USB核心中註冊的裝置ID模式匹配的裝置插入USB匯流排時,將呼叫probe函式。 usb_device結構,介面號和介面ID傳遞給該函式

static int skel_probe(struct usb_interface *interface,
    const struct usb_device_id *id)

現在,驅動程式需要驗證此裝置實際上是它可以接受的裝置。 如果是,則返回0。如果不是,或者如果在初始化過程中發生任何錯誤,則從probe函式返回錯誤程式碼(例如-ENOMEM-ENODEV)。

在框架驅動程式中,我們確定哪些端點標記為批次輸入和批次輸出。 我們建立緩衝區以儲存將從裝置傳送和接收的資料,並初始化一個USB urb以將資料寫入裝置。

相反,當從USB匯流排中刪除裝置時,將使用裝置指標呼叫disconnect函式。 驅動程式需要清除此時已分配的任何私有資料,並關閉USB系統中任何掛起的urb。

現在,該裝置已插入系統,並且驅動程式已繫結到該裝置,來自嘗試與裝置通訊的使用者程式的傳遞給USB子系統的file_operations結構中的任何函式都將被呼叫。 呼叫的第一個函式將是open,因為程式嘗試開啟裝置以進行I/O。 我們增加私有使用計數,並將指向內部結構的指標儲存在檔案結構中。 這樣做是為了使將來對檔案操作的呼叫能夠使驅動程式確定使用者正在定址哪個裝置。 所有這些都是透過以下程式碼完成的

/* increment our usage count for the device */
kref_get(&dev->kref);

/* save our object in the file's private structure */
file->private_data = dev;

呼叫open函式之後,將呼叫read和write函式以接收資料並將其傳送到裝置。 在skel_write函式中,我們接收指向使用者想要傳送到裝置的資料的指標以及資料的大小。 該函式根據其建立的寫入urb的大小(此大小取決於裝置具有的批次輸出端點的大小)來確定可以傳送到裝置的資料量。 然後,它將資料從使用者空間複製到核心空間,將urb指向資料,並將urb提交給USB子系統。 這可以在以下程式碼中看到

/* we can only write as much as 1 urb will hold */
size_t writesize = min_t(size_t, count, MAX_TRANSFER);

/* copy the data from user space into our urb */
copy_from_user(buf, user_buffer, writesize);

/* set up our urb */
usb_fill_bulk_urb(urb,
                  dev->udev,
                  usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
                  buf,
                  writesize,
                  skel_write_bulk_callback,
                  dev);

/* send the data out the bulk port */
retval = usb_submit_urb(urb, GFP_KERNEL);
if (retval) {
        dev_err(&dev->interface->dev,
            "%s - failed submitting write urb, error %d\n",
            __func__, retval);
}

使用usb_fill_bulk_urb()函式使用適當的資訊填充寫入urb時,我們將urb的完成回撥指向呼叫我們自己的skel_write_bulk_callback函式。 當USB子系統完成urb時,將呼叫此函式。 回撥函式在中斷上下文中呼叫,因此必須注意不要在該時間進行過多的處理。 我們對skel_write_bulk_callback的實現僅報告urb是否已成功完成,然後返回。

read函式的工作方式與write函式略有不同,因為我們不使用urb將資料從裝置傳輸到驅動程式。 相反,我們呼叫usb_bulk_msg()函式,該函式可用於向裝置傳送或從裝置接收資料,而無需建立urb並處理urb完成回撥函式。 我們呼叫usb_bulk_msg()函式,為其提供一個緩衝區,以將從裝置接收到的任何資料放入其中,並提供一個超時值。 如果超時時間到期而未從裝置接收到任何資料,則該函式將失敗並返回錯誤訊息。 這可以透過以下程式碼顯示

/* do an immediate bulk read to get data from the device */
retval = usb_bulk_msg (skel->dev,
                       usb_rcvbulkpipe (skel->dev,
                       skel->bulk_in_endpointAddr),
                       skel->bulk_in_buffer,
                       skel->bulk_in_size,
                       &count, 5000);
/* if the read was successful, copy the data to user space */
if (!retval) {
        if (copy_to_user (buffer, skel->bulk_in_buffer, count))
                retval = -EFAULT;
        else
                retval = count;
}

對於執行到裝置的單個讀取或寫入,usb_bulk_msg()函式非常有用。但是,如果您需要不斷地讀取或寫入裝置,建議設定自己的urb並將其提交給USB子系統。

當用戶程式釋放它用於與裝置通訊的檔案控制代碼時,將呼叫驅動程式中的release函式。 在此函式中,我們減少私有使用計數,並等待可能掛起的寫入

/* decrement our usage count for the device */
--skel->open_count;

USB驅動程式必須能夠順利處理的更困難的問題之一是,即使程式當前正在與它通訊,也可以隨時從系統中刪除USB裝置。 它需要能夠關閉任何當前的讀取和寫入,並通知使用者空間程式該裝置已不存在。 以下程式碼(函式skel_delete)是如何執行此操作的示例

static inline void skel_delete (struct usb_skel *dev)
{
    kfree (dev->bulk_in_buffer);
    if (dev->bulk_out_buffer != NULL)
        usb_free_coherent (dev->udev, dev->bulk_out_size,
            dev->bulk_out_buffer,
            dev->write_urb->transfer_dma);
    usb_free_urb (dev->write_urb);
    kfree (dev);
}

如果程式當前具有裝置的開啟控制代碼,我們將重置標誌device_present。 對於期望裝置存在的每個讀取,寫入,釋放和其他函式,驅動程式首先檢查此標誌以檢視裝置是否仍然存在。 如果不是,則釋放該裝置已消失,並將-ENODEV錯誤返回給使用者空間程式。 最終呼叫release函式時,它將確定是否沒有裝置,如果沒有裝置,它將執行skel_disconnect函式通常在裝置上沒有開啟檔案時執行的清除操作(請參見列表5)。

等時資料

此usb-skeleton驅動程式沒有任何將中斷或等時資料傳送到裝置或從裝置傳送資料的示例。 中斷資料的傳送幾乎與批次資料的傳送完全相同,但有一些小的例外。 等時資料的工作方式不同,連續的資料流被髮送到裝置或從裝置傳送。 音訊和影片攝像頭驅動程式是處理等時資料的驅動程式的非常好的示例,如果您還需要這樣做,它將非常有用。

結論

編寫Linux USB裝置驅動程式並非難事,如usb-skeleton驅動程式所示。 該驅動程式與當前的其他USB驅動程式結合使用,應該提供足夠的示例來幫助新手作者在最短的時間內建立可工作的驅動程式。 linux-usb-devel郵件列表存檔也包含許多有用的資訊。

資源

Linux USB專案: http://www.linux-usb.org/

Linux熱插拔專案: http://linux-hotplug.sourceforge.net/

linux-usb郵件列表存檔: https://lore.kernel.org/linux-usb/

Linux USB裝置驅動程式程式設計指南: https://lmu.web.psi.ch/docu/manuals/software_manuals/linux_sl/usb_linux_programming_guide.pdf

USB主頁: https://www.usb.org