3、USB设备枚举与识别:VID/PID匹配、端点描述符解析、批量传输端点配置

好,咱们进入第三章。这一章讲的是USB设备枚举,说白了就是打印机插上之后,主机怎么认出它来。

我记得刚做驱动那会儿,打印机插上去没反应,我以为是硬件坏了。折腾了半天,才发现是VID/PID没配对。嗯,这种坑踩过一次就记住了。

3.1 VID/PID匹配——设备的第一张身份证

每个USB设备都有两个关键ID:VID(Vendor ID,厂商ID)和PID(Product ID,产品ID)。主机就是靠这两个ID来识别设备的。

VID由USB-IF组织分配,每个厂商一个。比如爱普生的VID是0x04B8,惠普是0x03F0。PID由厂商自己定,同一厂商的不同产品用不同PID。

在Linux驱动里,匹配代码长这样:

// USB设备ID表
static const struct usb_device_id printer_id_table[] = {
    { USB_DEVICE(0x04B8, 0x0202) },  // EPSON TM-T88
    { USB_DEVICE(0x0416, 0x5011) },  // Winbond POS Printer
    { }  // 结束标志
};

MODULE_DEVICE_TABLE(usb, printer_id_table);

关键点:匹配顺序很重要。内核会从上到下依次匹配,找到第一个匹配的就停。所以常用设备放前面,能加快枚举速度。

我遇到过一个问题:同一款打印机,不同批次PID居然不一样。后来发现是厂商升级固件时改了PID。解决方案?把新旧PID都加到表里。

3.2 端点描述符解析——数据通道的说明书

设备被识别后,主机要读取它的配置描述符。这里面最重要的就是端点描述符。

端点是什么?你可以把它想象成USB的数据管道。每个端点有方向(IN还是OUT)、传输类型(批量、中断、同步)、最大包长。

打印机的典型端点结构:

端点号 方向 传输类型 最大包长 用途
0x01 OUT 批量 64字节 发送打印数据
0x82 IN 批量 64字节 读取状态
0x83 IN 中断 8字节 纸尽/错误通知

解析端点描述符的代码:

// 遍历接口的所有端点
for (i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
    struct usb_endpoint_descriptor *ep = &iface_desc->endpoint[i].desc;
    
    if (usb_endpoint_is_bulk_out(ep)) {
        bulk_out_ep = ep;
        bulk_out_pipe = usb_sndbulkpipe(udev, ep->bEndpointAddress);
        printk("找到批量OUT端点: 0x%02x, 包长: %d\n",
               ep->bEndpointAddress, le16_to_cpu(ep->wMaxPacketSize));
    }
    
    if (usb_endpoint_is_bulk_in(ep)) {
        bulk_in_ep = ep;
        bulk_in_pipe = usb_rcvbulkpipe(udev, ep->bEndpointAddress);
    }
}

个人经验:解析端点时,一定要检查wMaxPacketSize。有些打印机标称64字节,实际只支持32字节。我曾经因为这个原因,发数据总是超时。后来改成按实际包长发送,问题就解决了。

3.3 批量传输端点配置——数据搬运的核心

打印机最常用的就是批量传输。为什么?因为它有错误重传机制,数据可靠性高。你想想看,打印小票要是丢了一行数据,那可就麻烦了。

配置批量传输端点,核心是设置好URB(USB Request Block)。URB就是USB的数据请求块,驱动通过它来收发数据。

发送数据的标准流程:

  1. 分配URB:usb_alloc_urb()
  2. 填充URB:设置端点、缓冲区、回调函数
  3. 提交URB:usb_submit_urb()
  4. 等待完成:在回调函数中处理结果

代码示例:

// 发送打印数据
static int printer_send_data(struct usb_device *udev, 
                             unsigned char *data, int len)
{
    struct urb *urb;
    int ret;
    
    // 分配URB
    urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!urb)
        return -ENOMEM;
    
    // 填充URB - 批量OUT传输
    usb_fill_bulk_urb(urb, udev,
                      usb_sndbulkpipe(udev, BULK_OUT_ENDPOINT),
                      data, len,
                      printer_write_callback,  // 回调函数
                      NULL);
    
    // 提交URB
    ret = usb_submit_urb(urb, GFP_KERNEL);
    if (ret) {
        printk("提交URB失败: %d\n", ret);
        usb_free_urb(urb);
        return ret;
    }
    
    // 注意:这里不要释放URB,回调函数里会处理
    return 0;
}

// 写完成回调
static void printer_write_callback(struct urb *urb)
{
    if (urb->status) {
        printk("批量传输错误: %d\n", urb->status);
        // 错误处理:重试或上报
    }
    
    usb_free_urb(urb);
}

注意:批量传输的URB回调是在中断上下文执行的。不要在回调里做耗时操作,比如打印日志、分配内存。我见过有人直接在回调里调用printk,结果系统卡死。正确的做法是用工作队列把任务推到进程上下文。

3.4 枚举失败排查指南

设备枚举不上,是驱动开发最头疼的问题。我总结了几种常见情况:

  • VID/PID不匹配:用lsusb查看设备ID,确认驱动表里有没有
  • 端点描述符解析错误:检查bEndpointAddress的位7(方向位)是否正确
  • 包长设置不对:wMaxPacketSize必须是端点支持的值
  • URB提交失败:检查USB设备是否已经被占用

调试技巧:在枚举阶段加usbmon抓包,能清楚看到主机和设备之间的交互过程。我曾经靠这个找到了一个固件bug——设备在枚举时返回了错误的描述符长度。

嗯,这一章的内容就这些。USB枚举看似简单,但细节很多。下一章我们讲打印数据的封装和发送,那才是真正考验驱动性能的地方。