编写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