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的数据请求块,驱动通过它来收发数据。
发送数据的标准流程:
- 分配URB:usb_alloc_urb()
- 填充URB:设置端点、缓冲区、回调函数
- 提交URB:usb_submit_urb()
- 等待完成:在回调函数中处理结果
代码示例:
// 发送打印数据
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枚举看似简单,但细节很多。下一章我们讲打印数据的封装和发送,那才是真正考验驱动性能的地方。