USB 串行驱动层
在我的上一篇文章 [见 LJ 2002 年 12 月刊] 中,我们介绍了 2.5(有望很快成为 2.6)内核树中的串行层。我们顺便提到,内核中的 USB 转串行驱动层有助于使用这些类型的设备驱动程序。这次我们更深入地讨论该层。
很久以前(至少在内核开发时间上),编写了一个单一的 USB 转串行设备驱动程序并被内核树接受。它几乎不能用于一种类型的设备,并且根本无法在 SMP 机器上工作。由于不存在标准的 USB 转串行协议,因此所有设备都使用了各个供应商创建的自定义协议。没有标准协议的原因是一个漫长而肮脏的故事;请查看 linux-usb-devel 邮件列表的存档以了解详细信息。很快,在第一个驱动程序中实现了第二种类型的 USB 转串行设备,共享保留的主次设备号。随着时间的推移,越来越多的设备被添加到驱动程序中,直到它变得难以管理。在 Peter Berger 和 Al Borchers 的帮助下,驱动程序的原始作者重写了基础架构,并创建了现在称为 USB 转串行层的结构。这段代码允许用最少的代码编写不同的 USB 转串行驱动程序,所有驱动程序都共享相同的主次设备号范围。它将各个驱动程序与 tty 层和 USB 层中的一些复杂性隔离开来。它还允许驱动程序编译为单独的模块,并且仅在需要时加载。
在 2.5 开发周期中,创建了串行层,以便提供一种更简单的方法来编写串行端口驱动程序,从而不必直接处理 tty 层。希望有一天 USB 层和串行层能够合并。两位维护人员都希望看到这种情况发生,但他们没有时间去做。(如果有人正在寻找项目,他们很乐意接受完成此操作的补丁。)
在本文中,我们将介绍 USB 转串行层的基础知识,详细介绍如何注册和注销驱动程序,以及如何设置驱动程序所需的主要结构。
本文中的所有代码和示例均适用于 2.5/2.6 内核树。2.4 和 2.2 内核树也支持 USB 转串行驱动程序,但它们的接口在某些地方略有不同。为了便于使用,我们只关注一个内核树。如果您在将 USB 转串行驱动程序移植到这些较旧的树(一旦它在 2.5 上运行)时遇到任何问题,请告诉我。
要向内核注册 USB 转串行驱动程序,驱动程序必须做两件事:向 USB 转串行核心注册,并向 USB 核心注册。向 USB 转串行核心注册告诉它在 USB 子系统找到新设备时调用您的驱动程序,而向 USB 核心注册是需要告诉它您的驱动程序可以接受哪种类型的设备。
要向 USB 核心注册,您只需要一个您的驱动程序将适用的 USB 设备列表,采用传统的 USB 设备 ID 格式
static struct usb_device_id id_table [] = { {USB_DEVICE(MY_PRODUCT_ID, MY_DEVICE_ID)}, {} /* Terminating entry */ }; MODULE_DEVICE_TABLE (usb, id_table);
需要此表,以便 USB 核心知道驱动程序可以接受哪些设备,以及用户空间热插拔代码知道使用哪种类型的设备。有关此表以及热插拔代码如何使用它的更多信息,请参阅我的文章“PCI 热插拔驱动程序文件系统的工作原理”,LJ 2002 年 5 月刊。
然后,使用此 ID 信息创建一个简单的 USB 设备驱动程序结构
static struct usb_driver tiny_driver = { .name = "tiny", .probe = usb_serial_probe, .disconnect = usb_serial_disconnect, .id_table = id_table, };
.probe 和 .disconnect 字段必须设置为指向 USB 串行核心的函数,因为该类型的逻辑由它处理,而不是由您的驱动程序处理。
然后,一个简单的调用将此驱动程序注册到 USB 核心
usb_register (&tiny_driver);
在此之后,必须通过调用以下函数通知 USB 串行驱动程序
usb_serial_register (&tiny_device);此函数接受指向 struct usb_serial_driver_type 的指针,这将在以下部分中解释。
要注销驱动程序,必须执行相同的步骤,但顺序相反。首先,我们向 USB 串行核心注销
usb_serial_unregister (&tiny_device);
然后,我们向 USB 核心注销
usb_unregister (&tiny_driver);
要向 USB 串行核心注册,必须填写 usb_serial_device_type 结构。此结构可以在 drivers/usb/serial/usb-serial.h 中找到,并定义如下
struct usb_serial_device_type { struct module *owner; char *name; const struct usb_device_id *id_table; char num_interrupt_in; char num_bulk_in; char num_bulk_out; char num_ports; struct list_head driver_list; int (*probe) (struct usb_serial *serial); int (*attach) (struct usb_serial *serial); int (*calc_num_ports) (struct usb_serial *serial); void (*shutdown) (struct usb_serial *serial); int (*open) (struct usb_serial_port *port, struct file * filp); void (*close) (struct usb_serial_port *port, struct file * filp); int (*write) (struct usb_serial_port *port, int from_user, const unsigned char *buf, int count); int (*write_room) (struct usb_serial_port *port); int (*ioctl) (struct usb_serial_port *port, struct file * file, unsigned int cmd, unsigned long arg); void (*set_termios) (struct usb_serial_port *port, struct termios * old); void (*break_ctl) (struct usb_serial_port *port, int break_state); int (*chars_in_buffer) (struct usb_serial_port *port); void (*throttle) (struct usb_serial_port *port); void (*unthrottle) (struct usb_serial_port *port); void (*read_int_callback)(struct urb *urb); void (*read_bulk_callback)(struct urb *urb); void (*write_bulk_callback)(struct urb *urb); };
这是一个相当大的结构,但它仍然小于 tty 层的结构或串行层结构的组合,这两者都是使用 USB 串行层的替代方案。
让我们描述一下所有这些字段的用途以及它们是否是必需的。owner 字段是指向拥有此设备的模块的指针。它应该设置为 THIS_MODULE 宏。设置此项后,模块引用计数逻辑由 USB 串行核心处理,这比尝试自己处理要安全得多。
name 字段是指向描述此设备的字符串的指针。当插入或移除设备时,此字符串用于 syslog 消息中。它也用于 /proc/tty/driver/usb-serial 文件中,以显示哪个设备连接到哪个端口。
/proc/tty/driver/usb-serial 文件
id_table 字段是指向 usb_device_id 结构列表的指针,该列表定义了此结构可以支持的所有设备。此字段可以与传递给 USB 核心的指针相同。但是,如果您的驱动程序需要为不同类型的设备执行不同的操作,您可以设置不同的结构来描述这些设备。Keyspan 驱动程序就是一个例子,它处理所有 Keyspan USB 串行设备,并且需要为不同的设备调用不同的函数。
num_interrupt_in 字段是此设备将具有的预期中断输入端点数。端点是一个 USB 术语,在 USB 规范 (www.usb.org) 中定义。如果您不关心让 USB 串行核心检查此值(将其与任何已见设备进行匹配),请使用 usb-serial.h 中定义的 NUM_DONT_CARE 宏。
num_bulk_in 和 num_bulk_out 字段声明了此设备将具有的批量输入和批量输出端点数。同样,如果您不希望核心关注此值,则可以在此处使用 NUM_DONT_CARE 宏。
num_ports 字段指示此设备将具有的不同端口数。单个 USB 串行设备可以包含许多不同的物理串行端口。
driver_list 字段由 USB 串行核心用于跟踪所有已注册到它的不同驱动程序;它不应由各个驱动程序使用。
结构中的其余字段都是可选的函数指针。如果未设置字段,则将调用通用 USB 串行驱动程序的相关函数。这允许使用最少的代码编写驱动程序,如果它恰好以与通用驱动程序相同的方式工作。如果不是,则几乎所有这些函数都需要定义。这些函数指针分为三组:USB 生命周期指针、tty 生命周期指针和 urb 回调指针。
USB 生命周期函数指针包括 probe、calc_num_ports、attach 和 shutdown。当 USB 设备初始化和关闭时,它们会在不同的时间点被调用。probe 函数在将与 id_table 设备之一匹配的设备插入系统时调用。此调用发生在设备由 USB 串行核心完全初始化之前。它可以用于将任何需要的固件下载到设备。此外,此时可以发送设备需要的任何其他早期初始化命令。如果返回 0,则 USB 串行核心继续执行初始化序列。任何其他值都将中止调用,并通知 USB 核心此设备未被任何驱动程序声明。
calc_num_ports 函数被调用以确定此设备有多少个端口。这应该仅由可以动态确定其端口的设备使用。任何返回值都会覆盖 usb_serial_device_type 结构中的 num_ports 字段。它在 probe 函数调用之后但在 attach 函数调用之前调用。
attach 函数在 struct usb_serial 结构完全设置好后调用。设备的任何本地初始化或任何私有内存结构分配都可以在此函数中完成。shutdown 函数在设备已从系统中移除时调用。此时应释放为此设备分配的任何本地内存。
tty 层函数指针包括 open、close、write、write_room、ioctl、set_termios、break_ctl、chars_in_buffer、throttle 和 unthrottle。如果您回忆起关于 tty 层的文章 [“tty 层”,LJ 2002 年 8 月刊],这些函数与同名的 tty 层函数调用匹配,但有一些曲折。首先,它们都传入一个指向正在操作的特定 usb_serial_port 结构的指针,并且某些函数仅在需要发生某些事情时才被调用。
open 函数在第一次对端口调用 open() 时调用,但不会在后续对 open() 的调用中调用。驱动程序需要执行以开始接收数据的任何 urb 提交,或应发送的任何特定于设备的消息,都应在此时完成。如果发生任何错误,应返回错误;否则,返回 0 以表示成功。
close 函数为从用户空间调用的最后一个 close() 调用调用。应关闭任何正在运行的 urb,并且现在应发送任何需要的特定于设备的命令。
write 函数的调用方式与 tty 层 write 函数的调用方式完全相同。传递给函数的数据需要发送到指定的端口。应返回发送到设备的字节数。请记住,设备不必发送用户希望它发送的所有数据;只要驱动程序通知用户空间发生了这种情况,就可以发生短写。这允许驱动程序逻辑更简单。如果发生错误,则应将其作为负数返回。
write_room 和 chars_in_buffer 函数密切相关。tty 层调用 write_room 函数来询问驱动程序此时可以接受多少字节写入。调用 chars_in_buffer 函数是为了找出仍要发送到设备的字节数。
ioctl 函数使用各种 tty ioctl 值调用。如果驱动程序无法处理特定值,则应返回 -ENOIOCTLCMD。这将允许 tty 层尝试提供默认函数。用户空间请求的一些更常见的值在前面提到的 tty 驱动程序文章中有所记录。
set_termios 函数用于设置特定端口的终端设置。这包括波特率、流量控制、数据位和其他线路设置。break_ctl 函数用于设置端口的 BREAK 值。状态 -1 表示应打开 BREAK 状态,状态 0 表示应关闭 BREAK 状态。throttle 和 unthrottle 函数用于停止和恢复从串行端口接收的数据。
read_int_callback、read_bulk_callback 和 write_bulk_callback 函数指针都由 USB 串行核心用于为这些类型的 USB 端点设置初始回调。如果驱动程序未指定读取或写入批量回调函数,则使用通用回调。没有通用读取中断回调函数,因此如果您的设备具有中断端点,则必须提供此回调。
通用读取批量回调的操作是将 USB urb 接收到的数据添加到端口的 tty 缓冲区,以便在调用 read() 时发送到用户空间。然后,它将 urb 重新提交到设备。如果您的设备不需要以任何方式解释接收到的数据,我建议使用此函数而不是编写新函数。通用批量写入回调要小得多,并且仅唤醒 tty 层(以防它处于休眠状态,等待数据传输到设备)。
在本文中,我们解释了如何注册和注销 USB 串行驱动程序,以及所有 USB 串行驱动程序必须提供的主要 usb_serial_driver_type 结构的基础知识。下次,我们将深入探讨 USB 串行驱动层的工作原理,并提供一个示例驱动程序。
Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护者。他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 联系到他。