如何编写 Linux 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 驱动程序,请熟悉 USB 协议规范。可以在 USB 主页上找到它以及许多其他有用的文档(请参阅“资源”部分)。有关 Linux USB 子系统的出色介绍,请参阅 USB 工作设备列表(请参阅“资源”部分)。它解释了 Linux USB 子系统的结构,并向读者介绍了 USB urb 的概念,这对于 USB 驱动程序至关重要。

Linux USB 驱动程序需要做的第一件事是在 Linux USB 子系统中注册自身,提供有关驱动程序支持哪些设备以及在插入或从系统中移除驱动程序支持的设备时要调用的函数的一些信息。所有这些信息都通过 usb_driver 结构传递给 USB 子系统。框架驱动程序将 usb_driver 声明为

static struct usb_driver skel_driver = {
     name:        "skeleton",
     probe:       skel_probe,
     disconnect:  skel_disconnect,
     fops:        &skel_fops,
     minor:       USB_SKEL_MINOR_BASE,
     id_table:    skel_table,
};

变量名是一个描述驱动程序的字符串。它用于打印到系统日志的信息性消息中。当看到或移除与 id_table 变量中提供的信息匹配的设备时,将调用 probe 和 disconnect 函数指针。

fops 和 minor 变量是可选的。大多数 USB 驱动程序都挂钩到另一个内核子系统,例如 SCSI、网络或 TTY 子系统。这些类型的驱动程序在其他内核子系统中注册自身,并且任何用户空间交互都通过该接口提供。但是对于没有匹配的内核子系统的驱动程序,例如 MP3 播放器或扫描仪,需要一种与用户空间交互的方法。USB 子系统提供了一种注册次设备号和一组 file_operations 函数指针的方法,以启用这种用户空间交互。框架驱动程序需要这种接口,因此它提供了一个次设备起始编号和一个指向其 file_operations 函数的指针。

然后通过调用 usb_register 注册 USB 驱动程序,通常在驱动程序的 init 函数中,如清单 1 所示。

清单 1. 注册 USB 驱动程序

当从系统中卸载驱动程序时,它需要使用 usb_unregister 函数向 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。以下代码告诉 hotplug 脚本,此模块支持具有特定厂商 ID 和产品 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 总线的设备与您的驱动程序在 USB 核心中注册的设备 ID 模式匹配时,将调用 probe 函数。usb_device 结构、接口编号和接口 ID 将传递给该函数

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

驱动程序现在需要验证此设备是否确实是它可以接受的设备。如果不是,或者如果在初始化期间发生任何错误,则从 probe 函数返回 NULL 值。否则,将返回指向包含此设备驱动程序状态的私有数据结构的指针。该指针存储在 usb_device 结构中,并且对驱动程序的所有回调都传递该指针。

在框架驱动程序中,我们确定哪些端点标记为批量输入和批量输出。我们创建缓冲区来保存将要发送和接收自设备的数据,并初始化一个 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 函数。驱动程序需要清除此时已分配的任何私有数据,并关闭 USB 系统中任何挂起的 urb。驱动程序还使用以下调用从 devfs 子系统注销自身

/* remove our devfs node */
devfs_unregister(skel->devfs);

现在设备已插入系统并且驱动程序已绑定到设备,用户程序尝试与设备通信时,将调用传递给 USB 子系统的 file_operations 结构中的任何函数。调用的第一个函数将是 open,因为程序尝试打开设备进行 I/O。在框架驱动程序的 open 函数中,如果它是带有 MODULE_INC_USE_COUNT 调用的模块,我们会增加驱动程序的使用计数。通过此宏调用,如果驱动程序编译为模块,则在调用相应的 MODULE_DEC_USE_COUNT 宏之前,驱动程序无法卸载。我们还会增加我们的私有使用计数,并将指向我们的内部结构的指针保存在文件结构中。这样做是为了使将来对文件操作的调用能够使驱动程序确定用户正在寻址哪个设备。所有这些都是通过以下代码完成的

/* increment our usage count for the module */
MOD_INC_USE_COUNT;
++skel->open_count;
/* save our object in the file's private structure */
file->private_data = skel;
在调用 open 函数之后,将调用 read 和 write 函数以接收和发送数据到设备。在 skel_write 函数中,我们接收指向用户希望发送到设备的一些数据以及数据大小的指针。该函数根据它创建的写入 urb 的大小(此大小取决于设备具有的批量输出端点的大小)确定它可以发送到设备的数据量。然后,它将数据从用户空间复制到内核空间,将 urb 指向数据,并将 urb 提交给 USB 子系统(请参阅清单 2)。

清单 2. skel_write 函数

当使用 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 函数,为其提供一个缓冲区以放置从设备接收到的任何数据和一个超时值。如果超时期限到期而没有从设备接收到任何数据,则该函数将失败并返回错误消息(请参阅清单 3)。

清单 3. usb_bulk_msg 函数

usb_bulk_msg 函数对于对设备进行单次读取或写入非常有用;但是,如果您需要持续读取或写入设备,建议您设置自己的 urb 并将其提交给 USB 子系统。

当用户程序释放它一直用于与设备通信的文件句柄时,将调用驱动程序中的 release 函数。在此函数中,我们使用 MOD_DEC_USE_COUNT 调用来减少模块使用计数(以匹配我们之前对 MOD_INC_USE_COUNT 的调用)。我们还确定当前是否有任何其他程序正在与设备通信(一个设备可以同时被多个程序打开)。如果这是设备的最后一个用户,那么我们将关闭任何可能正在进行的写入操作。这一切都通过以下方式完成

/* decrement our usage count for the device */
--skel->open_count;
if (skel->open_count <= 0) {
   /* shutdown any bulk writes that might be
      going on */
   usb_unlink_urb (skel->write_urb);
   skel->open_count = 0;
}
/* decrement our usage count for the module */
MOD_DEC_USE_COUNT;

USB 驱动程序必须能够顺利处理的更困难的问题之一是,即使程序当前正在与 USB 设备通信,USB 设备也可能在任何时间点从系统中移除。它需要能够关闭任何当前的读取和写入,并通知用户空间程序设备已不再存在(请参阅清单 4)。

清单 4. skel_disconnect 函数

如果程序当前具有设备的打开句柄,我们仅将本地结构中的 usb_device 结构置为空,因为它现在已经消失了。对于每个期望设备存在的读取、写入、释放和其他函数,驱动程序首先检查此 usb_device 结构是否仍然存在。如果不存在,则释放设备已消失,并向用户空间程序返回 -ENODEV 错误。当最终调用 release 函数时,它会确定是否没有 usb_device 结构,如果没有,则执行 skel_disconnect 函数通常在设备上没有打开文件时执行的清理操作(请参阅清单 5)。

清单 5. 清理

此 usb-skeleton 驱动程序没有任何关于中断或等时数据发送到设备或从设备发送的示例。中断数据的发送方式几乎与批量数据相同,只有一些小的例外。等时数据的工作方式不同,连续的数据流被发送到设备或从设备发送。音频和视频摄像头驱动程序是处理等时数据的非常好的示例,如果您也需要这样做,它们将非常有用。

正如 usb-skeleton 驱动程序所示,编写 Linux USB 设备驱动程序并不是一项困难的任务。此驱动程序与当前的其他 USB 驱动程序相结合,应提供足够的示例来帮助初学者在最短的时间内创建可工作的驱动程序。linux-usb-devel 邮件列表存档也包含许多有用的信息。

资源

Greg Kroah-Hartman 是 Linux 内核 USB 开发人员之一。他的自由软件比他曾经受雇开发的任何闭源项目都被更多人使用。

加载 Disqus 评论