3. Linux USB子系统架构:USB核心层、USB主机控制器驱动、USB设备驱动

好,咱们今天来聊聊Linux USB子系统的整体架构。说实话,我刚接触USB驱动开发那会儿,看着那一堆代码也挺懵的。后来我画了一张图,把整个框架理了一遍,才慢慢摸到门道。

Linux USB子系统,说白了就是三层结构:USB核心层USB主机控制器驱动USB设备驱动。这三层各司其职,又紧密配合。我习惯把核心层比作「交通指挥中心」,主机控制器驱动是「高速公路」,设备驱动就是「收费站」。

核心要点:USB子系统采用分层设计,上层驱动不需要关心底层硬件细节,底层驱动也不需要知道上层在传输什么数据。这种解耦设计,让开发变得清爽很多。

Linux USB 子系统三层架构 USB 设备驱动 usb_driver / probe() / disconnect() 处理具体设备协议(如HID、Mass Storage、CDC) USB 核心层 (USB Core) 设备枚举、地址分配、配置管理 URB 管理、总线带宽分配 热插拔事件分发、驱动匹配 USB 主机控制器驱动 (HCD) ehci-hcd / ohci-hcd / xhci-hcd 直接操作硬件寄存器,处理传输事务 用户空间 / 内核空间 内核核心层 硬件抽象层 向上提供统一接口 承上启下 向下屏蔽硬件差异

3.1 USB核心层——承上启下的枢纽

USB核心层,我习惯叫它「USB Core」。它不直接跟硬件打交道,也不管你的设备是鼠标还是U盘。它的任务就一个:把上层驱动和下层控制器驱动串起来

核心层主要干这几件事:

  • 设备枚举:当USB设备插上时,核心层负责读取设备描述符、分配地址、配置端点。嗯,这个过程我调试过很多次,每次看到枚举成功,心里就踏实了。
  • URB管理:URB(USB Request Block)是USB数据传输的基本单元。核心层负责URB的分配、提交、回收。说白了,就是数据快递的中转站。
  • 驱动匹配:核心层维护着一张驱动表,当新设备插入时,它会遍历所有注册的usb_driver,找到匹配的驱动并调用probe()。
  • 热插拔事件:设备插入或拔出时,核心层会生成uevent,通知用户空间和内核空间的相关模块。

我的小技巧:调试USB枚举问题时,我经常在核心层的hub_event()函数里加printk。别看这方法土,但真的管用。我曾经靠这个定位过一个USB3.0兼容性问题,折腾了两天,最后发现是hub初始化时序不对。

3.2 USB主机控制器驱动——硬件的翻译官

USB主机控制器驱动(HCD),是直接跟硬件寄存器打交道的底层驱动。不同的USB规范对应不同的控制器:

控制器类型 对应驱动 速度 特点
UHCI uhci-hcd USB 1.1 (全速/低速) Intel主导,软件开销大
OHCI ohci-hcd USB 1.1 (全速/低速) Compaq主导,硬件开销大
EHCI ehci-hcd USB 2.0 (高速) 只处理高速传输,低速转给companion控制器
xHCI xhci-hcd USB 3.0/3.1/3.2 统一处理所有速度,支持更多端点

你想想看,为什么要有这么多控制器驱动?说白了,不同厂商的硬件实现不一样。Intel的UHCI和Compaq的OHCI,虽然都支持USB 1.1,但寄存器布局、传输描述符格式完全不同。

我记得有一次做ARM平台移植,板子上用的是DWC3控制器,但内核默认加载的是ehci-hcd。结果呢?设备根本枚举不出来。后来我查了datasheet才发现,DWC3需要xhci-hcd驱动。嗯,这种坑踩过一次就记住了。

注意:在调试USB主机控制器驱动时,一定要确认硬件对应的控制器类型。加载错误的HCD驱动,轻则设备不工作,重则内核panic。我曾经在客户现场遇到过这种情况,当时真是冷汗都下来了。

3.3 USB设备驱动——跟设备对话的人

USB设备驱动,才是我们平时写的最多的部分。它负责跟具体的USB设备「聊天」,理解设备的数据格式,完成用户想要的功能。

一个典型的USB设备驱动,需要实现这些回调:

  • probe():设备匹配成功时调用。在这里初始化设备、分配端点、注册接口。
  • disconnect():设备拔出时调用。清理资源、释放内存。
  • suspend()/resume():电源管理相关。我刚开始做驱动时经常忽略这两个函数,结果设备休眠后唤不醒,被测试同事追着改bug。

来看一个最简单的USB设备驱动框架:

/* 一个简化的USB设备驱动示例 */
static int my_usb_probe(struct usb_interface *interface,
                        const struct usb_device_id *id)
{
    struct usb_device *dev = interface_to_usbdev(interface);
    struct usb_host_interface *iface_desc;
    struct usb_endpoint_descriptor *endpoint;
    int ret;

    /* 获取接口描述符 */
    iface_desc = interface->cur_altsetting;
    
    /* 遍历端点,找到我们需要的 */
    for (int i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        endpoint = &iface_desc->endpoint[i].desc;
        
        if (usb_endpoint_is_bulk_in(endpoint)) {
            /* 找到了批量输入端点 */
            dev_info(&interface->dev, "Found bulk IN endpoint\n");
        }
    }

    /* 分配URB,准备数据传输 */
    /* ... 实际项目中这里会有更多初始化代码 ... */

    return 0;
}

static void my_usb_disconnect(struct usb_interface *interface)
{
    /* 清理工作:释放URB、取消未完成的传输 */
    dev_info(&interface->dev, "Device disconnected\n");
}

/* USB设备ID表 */
static const struct usb_device_id my_usb_table[] = {
    { USB_DEVICE(0x1234, 0x5678) },  /* VID:PID */
    { }  /* 结束标记 */
};
MODULE_DEVICE_TABLE(usb, my_usb_table);

/* USB驱动结构体 */
static struct usb_driver my_usb_driver = {
    .name       = "my_usb_device",
    .probe      = my_usb_probe,
    .disconnect = my_usb_disconnect,
    .id_table   = my_usb_table,
};

module_usb_driver(my_usb_driver);

这段代码看着简单,但实际项目中,probe()函数里往往要处理很多细节。比如端点方向判断、传输类型选择、缓冲区对齐等等。我习惯在probe()里加一些调试信息,把设备的VID/PID、端点信息都打印出来,方便后续排查问题。

避坑指南:写USB设备驱动时,千万别忘了检查端点描述符的有效性。我曾经遇到过一款山寨设备,它的端点描述符里bEndpointAddress字段居然填了0xFF,导致内核在urb提交时直接报错。后来我在probe()里加了端点有效性检查,才把这个坑填上。

3.4 三层之间的协作流程

这三层是怎么配合工作的?我举个例子你就明白了。

假设你插上一个U盘:

  1. HCD层检测到端口状态变化,通知核心层「有个设备插上来了」。
  2. 核心层开始枚举:复位设备、分配地址、读取设备描述符、配置描述符。这个过程会通过HCD层发送标准的USB请求。
  3. 核心层根据设备的bInterfaceClass等信息,查找匹配的驱动。比如U盘会匹配usb-storage驱动。
  4. 设备驱动的probe()被调用,驱动开始初始化,注册块设备或字符设备。
  5. 用户空间就能看到/dev/sda了,可以mount使用。

整个过程,HCD层负责「怎么传」,核心层负责「传什么」,设备驱动负责「传完怎么用」。各层各司其职,互不干扰。

总结一下:理解USB子系统架构,关键是记住三层之间的接口。核心层向上提供usb_driver注册接口,向下提供usb_hcd注册接口。你写设备驱动时,只需要关心usb_driver那一套API,不用管底层是EHCI还是xHCI。这种抽象,说白了就是Linux内核设计哲学的体现——「机制与策略分离」。

好了,这一章的内容就到这里。USB子系统的三层架构,是后续所有USB驱动开发的基础。把这个框架吃透了,后面写驱动就会顺手很多。


公众号:蓝海资料掘金营,微信deep3321