编写 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 网络驱动程序都基于该文件。可以在内核源代码树的 drivers/usb/usb-skeleton.c 中找到此 USB 框架。在本文中,我将介绍框架驱动程序的基础知识,解释不同的部分以及需要做些什么才能将其自定义为您的特定设备。

Linux USB 基础知识

如果您要编写 Linux USB 驱动程序,请熟悉 USB 协议规范。它可以在 USB 主页(请参阅资源)以及许多其他有用的文档中找到。有关 Linux USB 子系统的出色介绍,请参阅 USB 工作设备列表(请参阅资源)。它解释了 Linux USB 子系统的结构,并向读者介绍了 USB urbs(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_deregister() 函数在 USB 子系统中注销自身

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

还有其他宏可以用于描述支持整个 USB 驱动程序类的驱动程序的结构体 usb_device_id。有关此的更多信息,请参阅 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 系统中任何挂起的 urbs。

现在,设备已插入系统,并且驱动程序已绑定到该设备,任何尝试与设备通信的用户程序都会调用传递给 USB 子系统的 file_operations 结构中的任何函数。当程序尝试打开设备以进行 I/O 时,将首先调用 open 函数。我们递增我们的私有使用计数,并在文件结构中保存指向我们内部结构的指针。这样做是为了使将来对文件操作的调用能够使驱动程序确定用户正在寻址哪个设备。所有这些都通过以下代码完成

/* 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 是否成功完成,然后返回。

读取函数的工作方式与写入函数略有不同,我们不使用 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 设备通信,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 错误返回给用户空间程序。当最终调用释放函数时,它会确定是否存在设备,如果不存在,则执行 skel_disconnect 函数在设备上没有打开文件时正常执行的清理操作(请参阅列表 5)。

同步数据

此 usb-skeleton 驱动程序没有发送或接收到设备的任何中断或同步数据示例。中断数据的发送方式几乎与批量数据相同,只有一些小的例外。同步数据的工作方式不同,会发送或接收到设备的连续数据流。音频和视频摄像头驱动程序是处理同步数据的很好的例子,如果您也需要执行此操作,这将非常有用。

结论

正如 usb-skeleton 驱动程序所示,编写 Linux USB 设备驱动程序并非难事。该驱动程序与其他当前的 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