USB 串口驱动层,第二部分

作者:Greg Kroah-Hartman

在本文的第一部分 [LJ, 2003 年 2 月] 中,我介绍了 USB 串口层以及如何向该层注册驱动程序的基础知识。本文解释了关于数据如何流经该层以及 USB 串口设备如何在 sysfs 中显示的一些细节。

通用 USB 串口设备

在本文的第一部分中,我简要提到了通用 USB 驱动程序,它可以在无需自定义内核编程的情况下轻松实现 USB 设备的通信。不幸的是,我没有确切地解释如何做到这一点,许多人写信来询问。

要创建一个与通用 USB 串口驱动程序配合使用的 USB 设备,所需的只是设备上的两个批量 USB 端点,一个 IN 和一个 OUT。通用 USB 串口驱动程序会将这两个端点绑定在一起,形成一个可以从用户空间读取和写入的 tty 设备。例如,一个具有 /proc/bus/usb/devices(图 1)描述的端点的设备显示为一个单端口设备,并在插入时产生以下内核消息

Generic converter detected
Generic converter now attached to ttyUSB0
    (or usb/tts/0 for devfs)

然后任何用户都可以通过 /dev/ttyUSB0 节点向设备发送数据。

The USB Serial Driver Layer, Part II

图 1. /proc/bus/usb/devices 条目示例

如果一个设备具有多个批量 IN 和批量 OUT 对,则会为该设备分配多个端口。例如,一个具有 /proc/bus/usb/devices(图 2)描述的端点的设备显示为一个双端口设备,并在插入时产生以下内核消息

Generic converter detected
Generic converter now attached to ttyUSB0
    (or usb/tts/0 for devfs)
Generic converter now attached to ttyUSB1
    (or usb/tts/1 for devfs)

对于此设备,/dev/ttyUSB0 和 /dev/ttyUSB1 都可以用于通信。

The USB Serial Driver Layer, Part II

图 2. /proc/bus/usb/devices 中双端口设备的条目

端点的顺序并不重要,因此所有 IN 端点可以首先出现,然后是 OUT 端点(与之前的交替示例不同)。USB 串口核心将获取所有的 IN 和 OUT 端点,并按照它们出现的顺序进行配对。如果存在中断端点,它也会将其分配给批量对,但中断端点不会被通用驱动程序使用;它只能由内核中的 USB 串口驱动程序使用。

要使通用 USB 串口驱动程序绑定到设备,需要在加载 usbserial 模块时将 USB 供应商 ID 和产品 ID 指定为模块参数。例如,要绑定到之前描述的供应商 ID 为 ffff 和产品 ID 为 fff8 的设备,请使用以下命令

modprobe usbserial vendor=0xffff product=0xfff8

如果用户不能被期望使用特定的设备 ID 加载 usbserial 模块,或者如果通用 USB 串口驱动程序应该使用多个设备 ID,则可以编写一个非常小的驱动程序。清单 1 中显示了一个示例。在此驱动程序中,没有指定回调函数,只指定了应控制的设备的产品和供应商 ID。这在 struct usb_serial_device_type 的声明中显示

static struct usb_serial_device_type tiny_device = {
    .owner =            THIS_MODULE,
    .name =             "Tiny USB serial",
    .short_name =       "tiny",
    .id_table =         id_table,
    .num_interrupt_in = NUM_DONT_CARE,
    .num_bulk_in =      NUM_DONT_CARE,
    .num_bulk_out =     NUM_DONT_CARE,
    .num_ports =        1,
};
特定的供应商和产品 ID 应在 id_table 指针中列出
static struct usb_device_id id_table [] = {
    { USB_DEVICE(MY_PRODUCT_ID, MY_DEVICE_ID1) },
    { USB_DEVICE(MY_PRODUCT_ID, MY_DEVICE_ID2) },
    { USB_DEVICE(MY_PRODUCT_ID, MY_DEVICE_ID3) },
    { }     /* Terminating entry */
};

清单 1. 微型 USB 串口驱动程序

总而言之,此驱动程序仅包含两个函数,它们分别只有两行和三行长,以及三个变量定义。有了它,所有通用 USB 串口驱动程序的功能都将为指定的设备发生。驱动程序会在设备插入系统时自动加载,这也是一个不错的功能。这必须是可能的最小的工作 Linux 内核驱动程序之一。使用以下命令编译它

echo "obj-m := tiny_tiny_usbserial.o" > Makefile
make -C <path/to/kernel/src> SUBDIRS=$PWD modules

Windows 操作系统也通过 Windows USB OPOS 串口驱动程序支持这种设备接口,这将为设备创建虚拟“COM”端口。这允许硬件供应商创建 USB 设备,这些设备不需要为 Linux 和 Windows 机器进行任何自定义驱动程序开发,这可能是非常理想的。

USB 串口设备的生命周期

当插入 USB 转串口设备时,会执行一系列漫长的步骤,以允许特定的 USB 转串口驱动程序控制单个 tty 设备。步骤如下

  • USB 集线器驱动程序检测到一个新设备。它为设备分配一个 USB 编号,并从设备读取基本的 USB 描述,然后将其填充到一个 struct usb_device 中,其中包含许多代表整个 USB 设备的 struct usb_interfaces。

  • USB 核心获取设备并将 USB 接口注册到内核驱动程序核心。

  • 内核驱动程序核心查看当前注册的 USB 驱动程序列表,以确定是否有任何驱动程序会接受此设备。

  • 由于这是一个 USB 转串口设备,USB 串口核心从内核驱动程序核心接受设备的控制权。

  • USB 串口核心构建一个单独的 struct usb_serial,并使用此结构调用特定的 USB 串口驱动程序的 probe() 函数。

  • USB 串口驱动程序的 probe() 函数初始化设备(如果应该初始化),然后将控制权返回给 USB 串口核心。

  • USB 串口核心根据此特定设备上的串口数量创建 struct usb_serial_port 结构,然后调用 USB 串口驱动程序的 attach() 函数(如果存在)。

  • 在 attach() 函数返回后,各个 struct usb_serial_port 结构将注册到内核驱动程序核心。

  • 内核驱动程序核心为每个单独的端口回调到 USB 串口核心。

  • USB 串口核心调用 USB 串口驱动程序中端口的 individual port_probe() 函数(如果存在),然后向 tty 层注册端口,完成初始化过程。

在此过程之后,tty 设备节点绑定到各个 USB 串口端口。当用户打开设备节点时,内核中会发生以下步骤

  • 内核查找设备节点并确定 tty 层已注册此节点,因此它调用 tty 层的 open 函数。

  • tty 层查找设备并确定 USB 串口核心已向其注册此节点,因此它调用 drivers/usb/serial/usb-serial.c 文件中的 serial_open()。

  • serial_open() 函数确定为此节点注册了哪个特定的 USB 串口驱动程序。

  • 指定的 USB 串口驱动程序的模块计数会递增,以防止在用户与设备通信时卸载它。

  • 如果指定的 USB 串口驱动程序具有 open() 函数,则会调用它,并将特定端口的 struct usb_serial_port 传递给它。

  • 然后,USB 串口驱动程序可以执行任何需要的硬件特定打开功能,并发送任何必要的 USB urbs 以开始从设备接收数据。

当用户在设备节点上调用 write() 以将数据发送到指定的串口时,内核中会发生以下步骤

  • 内核调用 tty 核心中的 tty_write() 函数。它之前在 open 调用期间设置了此指针,因此它不会再次查找它。

  • tty_write() 为此特定 tty 设备调用线路规程的 write() 函数。

  • 线路规程调用 USB 串口核心 serial_write() 函数。

  • serial_write() 函数确定此文件使用的特定 USB 串口驱动程序,并调用它的 write() 函数。

  • 然后,USB 串口驱动程序可以将数据复制到缓冲区中,并通过 USB 连接将其发送到设备,处理设备可能需要的任何特殊格式问题。

  • 在数据完全发送后,驱动程序可以唤醒 tty 设备,以便向其发送任何缓冲的数据。这应该通过简单的调用来完成

schedule_work(&port->work);

当 USB 串口驱动程序为特定端口接收到数据时,它应该将数据放入分配给该端口的翻转缓冲区的特定 tty 结构中

for (i = 0; i < data_size; ++i) {
    if (tty->flip.count >= TTY_FLIPBUF_SIZE)
        tty_flip_buffer_push(tty);
    tty_insert_flip_char(tty, data[i], 0);
}
tty_flip_buffer_push(tty);
当用户在设备节点上调用 read() 时,将返回此端口的 tty 翻转缓冲区中的任何数据。

当用户关闭设备节点时,内核中会发生以下步骤

  • 内核调用 tty 核心中的 tty_release() 函数。

  • tty_release() 确定这是否是此设备节点上持有的最后一个引用(请记住,一个设备节点可以同时被多个程序打开)。如果是,则调用 USB 串口核心 serial_close() 函数。

  • serial_close() 函数调用 USB 串口驱动程序的 close() 函数,允许它关闭任何挂起的 USB 传输并进入静默状态。

  • 然后,USB 串口核心递减 USB 串口驱动程序的模块计数,可能允许卸载它。

sysfs 中 USB 串口设备的表示

在之前对 USB 串口设备如何绑定到特定 USB 串口驱动程序的描述中,内核驱动程序核心被调用了多次。发生这种情况是因为 USB 串口核心在内核驱动程序模型中被表示为一个总线,允许单个 USB 设备上存在多个端口。

例如,以下设备是系统上第一个 USB 总线上的八端口 USB 转串口设备。它在 sysfs 中的位置是 /sys/devices/pci0/00:09.0/usb1/1-1/1-1.1。在该目录中包含以下目录和文件:1-1.1:0/、bcdDevice、bConfigurationValue、bDeviceClass、bDeviceProtocol、bDeviceSubClass、bmAttributes、bMaxPower、bNumConfigurations、bNumInterfaces、idProduct、idVendor、manufacturer、name、power、product、serial、speed、ttyUSB0/、ttyUSB1/、ttyUSB2/、ttyUSB3/、ttyUSB4/、ttyUSB5/、ttyUSB6/ 和 ttyUSB7/。

此目录中的文件提供此设备的 USB 特定信息,1-1.1:0/ 目录中的文件也是如此,它是此设备上的第一个接口。ttyUSB* 目录由 USB 串口核心创建,包含以下文件:dev、name 和 power。

dev 文件包含此特定设备的主设备号和次设备号,然后可以用来确定与其通信的正确设备节点。在 /sys/bus/usb 目录中,此 USB 设备被视为绑定到 io_edgeport USB 驱动程序(图 3)。

The USB Serial Driver Layer, Part II

图 3. /sys/bus/usb 树

还有一个 usb-serial 总线,它显示了注册到内核的各个 USB 串口端口(图 4)。由于这些单独的端口是 tty 设备,它们也会出现在 tty 类目录中(图 5)。

The USB Serial Driver Layer, Part II

图 4. /sys/bus/usb-serial 树

The USB Serial Driver Layer, Part II

图 5. /sys/class/tty 树

通过所有这些指向单个 USB 设备的不同链接,可以轻松确定 USB 设备的类型、它有多少个 tty 端口以及哪种类型的 USB 串口驱动程序控制它。这也比本文第一部分中描述的 /proc/tty/driver/usb-serial 文件中显示的信息要多得多。

sysfs 接口在此仅作简要描述,但它包含关于给定时间系统中所包含的所有物理和虚拟设备的丰富信息。有关 sysfs 和内核驱动程序模型的更好描述,请参阅 Pat Mochel 在 2003 年 linux.conf.au 上发表的论文,网址为 www.kernel.org/pub/linux/kernel/people/mochel/doc/lca

Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护者。他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 与他联系。

加载 Disqus 评论