第 4 章:Linux USB 驱动框架简介

很多人在做 USB OTG 调试时,一遇到加载失败就慌了。其实,当你理解了 Linux USB 驱动框架,很多问题自己就能找到头绪。我当年刚接触 USB 驱动时,也是被一堆术语搞得晕头转向——URB、端点、接口、配置……后来亲自调了一次驱动,才明白这框架设计的妙处。

说白了,Linux USB 驱动分三层:上层的 设备驱动(比如 U 盘、鼠标、网卡)、中间的 USB 核心(负责管理总线、设备和驱动的匹配)、以及底层的 主机控制器驱动(HCD,直接跟硬件打交道)。你想想看,OTG 驱动加载失败,绝大多数问题都出在这三层的接口上——要么设备驱动注册不对,要么核心没找到匹配的驱动,要么 HCD 没识别到设备。

4.1 整体分层架构

我们先看一张图,这图我每次讲课时都会画一遍,基本能把框架说清楚。

Linux USB 驱动分层架构图 用户空间 (User Space) USB 设备驱动 (USB Device Driver) 鼠标 / 键盘 / 存储 / 网卡 / OTG 功能驱动 USB 核心 (USB Core) 总线管理 / 驱动匹配 / URB 管理 / 配置解析 usbcore.ko / usb_common.ko 主机控制器驱动 (HCD) EHCI / OHCI / UHCI / xHCI USB 硬件控制器 每一层都通过标准接口与相邻层通信,驱动加载失败往往对应某层接口不匹配

这张图里,最上面是用户空间,比如你用 lsusb 看到的设备信息。中间的内核空间才是我们开发的重点。我个人习惯把 USB 驱动分成三个关注点:

  • 设备驱动层:关注 USB 设备的特定功能(比如 U 盘做存储、鼠标做 HID)。
  • USB 核心层:关心总线上的设备枚举、驱动匹配、URB 管理。
  • HCD 层:处理主机控制器寄存器,产生 USB 帧。

避坑提示:OTG 设备驱动加载失败,我遇到过好几次是因为 HCD 层没正确初始化。先检查 dmesg 里有没有 "hub 1-0:1.0: USB hub found" 这行——如果没有,说明 HCD 都没跑起来,后面的事情就不用看了。

4.2 核心数据结构

框架里最绕不开的是几个数据结构:usb_device、usb_interface、usb_host、usb_driver。我列个表格,方便你对比记忆。

结构体 作用 对应概念 加载失败常见原因
usb_device 代表一个 USB 设备(物理或逻辑) 设备对象 设备描述符解析失败,厂商ID/产品ID不匹配
usb_interface 设备的一个接口(functional unit) 接口对象 接口描述符读取异常,备选设置错误
usb_driver 驱动结构体,包含 probe、disconnect、id_table 驱动对象 id_table 未正确注册,probe 函数返回负数
urb USB 请求块,所有数据传输的载体 传输单元 URB 提交失败,端点类型不匹配,DMA 缓冲区问题

为什么强调这些?我记得有一次调一个 OTG 网卡驱动,probe 函数明明被调用了,但设备就是报错。最后发现是 usb_interface 的 cur_altsetting 指向了错误的备选设置,导致端点描述符和实际硬件不匹配。嗯,这种问题光靠看 dmesg 根本看不出来,得自己加打印。

4.3 驱动匹配流程

当设备插入时,USB 核心会执行以下流程:

  1. HCD 上报设备接入:主机控制器检测到端口状态变化,调用 hub_events。
  2. 设备枚举:复位设备,获取设备描述符、配置描述符、接口描述符、端点描述符。
  3. 分配 usb_device:将枚举结果填入结构体,注册到 USB 总线。
  4. 驱动匹配:遍历已注册的 usb_driver,比对 id_table 中的 VID/PID 或 class/subclass/protocol。
  5. 调用 probe:匹配成功则调用驱动的 probe 函数,进行初始化。
  6. 接口绑定:如果设备有多个接口,每个接口会独立匹配驱动(比如 OTG 设备可能有 CDC 控制接口和数据接口)。

关键点:驱动加载失败,90% 发生在第 4 步(匹配不到)或第 5 步(probe 失败)。你只需要在 dmesg 里搜 "new USB device found" 和 "driver probe" 就能定位问题。

4.4 URB 与端点通信

有了设备和驱动,数据怎么传?靠 URB。URB 是 USB 驱动里的核心传输载体。我通常会关注它的这几个字段:

struct urb {
    struct usb_device *dev;       /* 目标设备 */
    unsigned int pipe;            /* 管道(端点方向+类型) */
    int status;                   /* 传输状态 */
    unsigned int transfer_flags;  /* 比如 URB_NO_TRANSFER_DMA_MAP */
    void *transfer_buffer;        /* 数据缓冲区 */
    int transfer_buffer_length;   /* 缓冲区长度 */
    int actual_length;            /* 实际传输长度 */
    int interval;                 /* 中断/等时传输的间隔 */
    /* ... 还有很多 */
};

URB 的 status 字段是调试故障的关键。我遇到过几次 status = -EPIPE(端点停止),原因是端点没有正确设置 halt 清除。这问题在 USB 存储设备上尤其常见。

注意:提交 URB 时必须保证 transfer_buffer 的 DMA 一致性。如果你用栈变量或普通 kmalloc 内存做 DMA 缓冲区,在某些架构(比如 ARM)上会直接崩溃。我踩过这个坑,后来养成了用 usb_alloc_coherent 的习惯。

4.5 USB 驱动模块组织

实际开发中,驱动通常编译成一个 .ko 文件。模块加载时会调用 usb_register 把 usb_driver 注册到核心。我建议你在模块入口加一个打印,确认加载顺序:

static int __init my_driver_init(void)
{
    int ret;
    pr_info("My USB OTG driver initializing...\n");
    ret = usb_register(&my_usb_driver);
    if (ret)
        pr_err("usb_register failed: %d\n", ret);
    return ret;
}
module_init(my_driver_init);

加载失败时,首先确认 modprobe/sys/bus/usb/drivers 目录下有没有你的驱动名字。如果没有,说明注册就没成功。我曾经帮一个同事排查问题,他的驱动一直不加载,最后发现是 module_init 和 module_exit 宏写错了——少了一个分号,编译过去但运行时符号没导出,真是让人哭笑不得。

4.6 常见加载失败模式

基于上面的框架,我把常见失败原因归纳成一张清单,你可以对照排查:

现象 可能出错层 排查点
dmesg 无任何 USB 设备信息 HCD / 硬件 检查 USB 控制器是否使能,时钟、电源、中断
设备枚举失败(重连或超时) USB 核心 描述符解析出错,复位时序问题,电压不足
驱动不 probe USB 核心 → 驱动层 id_table 匹配规则,驱动是否注册到正确总线
probe 返回负数 设备驱动层 资源申请失败(内存、GPIO、中断),端点检查失败
传输时 status 为 -EPROTO 或 -EILSEQ HCD / 设备驱动层 端点管道配置错误,USB 信号完整性问题

速查小技巧:当你不确定是驱动还是核心的问题时,可以用 cat /sys/kernel/debug/usb/devices 查看设备树。如果设备在 debugfs 里出现了,说明枚举和核心没问题,问题肯定在驱动 probe 或 URB 提交上。