Deep Dive into Linux USB Core: Device Model, URBs, and Host Controller Drivers

This article provides a comprehensive analysis of the Linux USB core architecture, detailing the device and interface layers, the struct usb_device model, USB Request Block handling, hub driver operations, EHCI host controller scheduling, and a complete example of a USB mouse client driver, complete with code snippets.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Deep Dive into Linux USB Core: Device Model, URBs, and Host Controller Drivers

The Linux USB subsystem implements a layered communication model that separates devices, interfaces, and endpoints. This article examines the internal architecture of the USB core, the role of the host controller driver (HCD), and how client drivers interact with the core.

USB Core Device Model

The core provides two driver layers:

USB Device Layer – Represents a physical USB device. The hub creates a struct usb_device for each attached device, including the root hub. The driver for this layer is usually usb_generic_driver, which only creates the child interface devices.

USB Interface Layer – Represents a functional interface (a "function") of a device. A struct usb_interface is created by the device driver’s probe() routine, and a normal client driver (e.g., a HID driver) binds to it.

Key structures:

struct usb_device {
    struct device dev;          // embedded generic device
    struct usb_bus *bus;
    unsigned int devnum;
    /* … other fields … */
};

struct usb_device_driver {
    const char *name;
    int (*probe)(struct usb_device *dev, const struct usb_device_id *id);
    void (*remove)(struct usb_device *dev);
    /* … */
};

struct usb_interface {
    struct device dev;
    struct usb_device *device;
    /* … */
};

struct usb_driver {
    const char *name;
    int (*probe)(struct usb_interface *intf, const struct usb_device_id *id);
    void (*disconnect)(struct usb_interface *intf);
    /* … */
};

Device creation occurs in two situations:

When the root hub is registered ( usb_add_hcd()usb_alloc_dev()).

When a hub detects a new device on a port ( hub_event()usb_alloc_dev()).

Drivers are registered with usb_register_device_driver() for device‑level drivers and usb_register_driver() for interface‑level drivers. The usb_bus_type bus uses the dev.type field to distinguish between usb_device_type and usb_if_device_type, allowing the match function to select the appropriate driver.

USB Request Block (URB)

URBs are the primary data‑transfer objects used by client drivers. A typical control transfer looks like this:

static int usb_internal_control_msg(struct usb_device *dev,
                                    unsigned int pipe,
                                    struct usb_ctrlrequest *cmd,
                                    void *data, int len, int timeout)
{
    struct urb *urb;
    int ret, length;

    urb = usb_alloc_urb(0, GFP_NOIO);
    if (!urb)
        return -ENOMEM;

    usb_fill_control_urb(urb, dev, pipe,
                          (unsigned char *)cmd, data, len,
                          usb_api_blocking_completion, NULL);

    ret = usb_start_wait_urb(urb, timeout, &length);
    return (ret < 0) ? ret : length;
}

static int usb_start_wait_urb(struct urb *urb, int timeout, int *actual_length)
{
    struct api_context ctx;
    unsigned long expire;
    int retval;

    init_completion(&ctx.done);
    urb->context = &ctx;
    urb->actual_length = 0;
    retval = usb_submit_urb(urb, GFP_NOIO);
    if (unlikely(retval))
        goto out;

    expire = timeout ? msecs_to_jiffies(timeout) : MAX_SCHEDULE_TIMEOUT;
    if (!wait_for_completion_timeout(&ctx.done, expire)) {
        usb_kill_urb(urb);
        retval = (ctx.status == -ENOENT) ? -ETIMEDOUT : ctx.status;
    } else {
        retval = ctx.status;
    }
out:
    if (actual_length)
        *actual_length = urb->actual_length;
    usb_free_urb(urb);
    return retval;
}

For normal devices the URB is handed to the host controller via usb_submit_urb()usb_hcd_submit_urb(). For the virtual root hub a special path rh_urb_enqueue() handles status and control requests.

USB Hub Driver

The hub driver monitors port status changes and creates or destroys struct usb_device objects accordingly.

static void hub_port_connect(struct usb_hub *hub, int port1,
                              u16 portstatus, u16 portchange)
{
    struct usb_device *udev;
    int i, status;

    for (i = 0; i < PORT_INIT_TRIES; i++) {
        udev = usb_alloc_dev(hdev, hdev->bus, port1);
        if (!udev)
            goto done;
        choose_devnum(udev);
        if (udev->devnum <= 0) {
            status = -ENOTCONN;
            goto loop;
        }
        usb_lock_port(port_dev);
        status = hub_port_init(hub, udev, port1, i);
        usb_unlock_port(port_dev);
        status = usb_new_device(udev);
    }
}

static void hub_port_disconnect(struct usb_hub *hub, int port1)
{
    struct usb_device *udev = /* get device from port */;
    if (udev) {
        usb_disconnect(&port_dev->child);
    }
}

The driver also creates a single URB to poll the hub’s status endpoint:

static int hub_configure(struct usb_hub *hub,
                         struct usb_endpoint_descriptor *ep)
{
    hub->urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!hub->urb)
        return -ENOMEM;
    usb_fill_int_urb(hub->urb, hdev, pipe,
                    hub->buffer, maxp, hub_irq, hub, ep->bInterval);
    hub_activate(hub, HUB_INIT);
    return 0;
}

static void hub_activate(struct usb_hub *hub, enum hub_activation_type type)
{
    usb_submit_urb(hub->urb, GFP_NOIO);
}

USB Host Controller Driver (HCD)

Linux supports OHCI/UHCI for USB 1.0, EHCI for USB 2.0, and XHCI for USB 3.0. The article focuses on EHCI, which includes companion OHCI controllers for full‑speed devices.

EHCI uses two scheduling mechanisms:

Periodic Schedule – Handles Isochronous and Interrupt transfers. Implemented as a two‑level linked list (frame list → queue heads).

Asynchronous Schedule – Handles Control and Bulk transfers. Implemented as a single list of Queue Heads.

Key descriptor types:

iTD – Isochronous Transfer Descriptor.

qTD – Queue Element Transfer Descriptor.

QH – Queue Head (used in both schedules).

EHCI driver registration:

static const struct hc_driver ehci_hc_driver = {
    .description   = hcd_name,
    .product_desc  = "EHCI Host Controller",
    .hcd_priv_size = sizeof(struct ehci_hcd),
    .irq           = ehci_irq,
    .flags         = HCD_MEMORY | HCD_DMA | HCD_USB2 | HCD_BH,
    .reset         = ehci_setup,
    .start         = ehci_run,
    .stop          = ehci_stop,
    .shutdown      = ehci_shutdown,
    .urb_enqueue   = ehci_urb_enqueue,
    .urb_dequeue   = ehci_urb_dequeue,
    .endpoint_disable = ehci_endpoint_disable,
    .endpoint_reset   = ehci_endpoint_reset,
    .get_frame_number = ehci_get_frame,
    .hub_status_data  = ehci_hub_status_data,
    .hub_control      = ehci_hub_control,
    .bus_suspend      = ehci_bus_suspend,
    .bus_resume       = ehci_bus_resume,
    .free_dev         = ehci_remove_device,
};

static int ehci_platform_probe(struct platform_device *dev)
{
    ehci_init_driver(&ehci_platform_hc_driver, &platform_overrides);
    hcd = usb_create_hcd(&ehci_platform_hc_driver, &dev->dev,
                         dev_name(&dev->dev));
    return usb_add_hcd(hcd, irq, IRQF_SHARED);
}

The driver maps client URBs onto the EHCI schedule via ehci_urb_enqueue() and removes them with ehci_urb_dequeue(). It also creates a virtual root hub device during initialization.

USB Client Software Example – USB Mouse Driver

The mouse driver demonstrates a complete client driver that registers with the USB core, allocates an URB, and reports input events.

static const struct usb_device_id usb_mouse_id_table[] = {
    { USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,
                        USB_INTERFACE_PROTOCOL_MOUSE) },
    { }
};
MODULE_DEVICE_TABLE(usb, usb_mouse_id_table);

static struct usb_driver usb_mouse_driver = {
    .name   = "usbmouse",
    .probe  = usb_mouse_probe,
    .disconnect = usb_mouse_disconnect,
    .id_table = usb_mouse_id_table,
};
module_usb_driver(usb_mouse_driver);

static int usb_mouse_probe(struct usb_interface *intf,
                           const struct usb_device_id *id)
{
    struct usb_device *dev = interface_to_usbdev(intf);
    struct usb_endpoint_descriptor *ep;
    struct usb_mouse *mouse;
    struct input_dev *input_dev;
    int pipe, maxp, error;

    /* Expect exactly one interrupt IN endpoint */
    if (intf->cur_altsetting->desc.bNumEndpoints != 1)
        return -ENODEV;
    ep = &intf->cur_altsetting->endpoint[0].desc;
    if (!usb_endpoint_is_int_in(ep))
        return -ENODEV;

    pipe = usb_rcvintpipe(dev, ep->bEndpointAddress);
    maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe));

    mouse = kzalloc(sizeof(*mouse), GFP_KERNEL);
    input_dev = input_allocate_device();
    if (!mouse || !input_dev)
        goto fail1;

    mouse->data = usb_alloc_coherent(dev, 8, GFP_ATOMIC, &mouse->data_dma);
    if (!mouse->data)
        goto fail1;

    mouse->irq = usb_alloc_urb(0, GFP_KERNEL);
    if (!mouse->irq)
        goto fail2;

    /* Fill input device fields */
    input_dev->name = mouse->name;
    input_dev->phys = mouse->phys;
    usb_make_path(dev, mouse->phys, sizeof(mouse->phys));
    strlcat(mouse->phys, "/input0", sizeof(mouse->phys));
    input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REL);
    input_dev->keybit[BIT_WORD(BTN_MOUSE)] =
        BIT_MASK(BTN_LEFT) | BIT_MASK(BTN_RIGHT) | BIT_MASK(BTN_MIDDLE) |
        BIT_MASK(BTN_SIDE) | BIT_MASK(BTN_EXTRA);
    input_dev->relbit[0] = BIT_MASK(REL_X) | BIT_MASK(REL_Y) | BIT_MASK(REL_WHEEL);
    input_set_drvdata(input_dev, mouse);
    input_dev->open = usb_mouse_open;
    input_dev->close = usb_mouse_close;

    /* Prepare the interrupt URB */
    usb_fill_int_urb(mouse->irq, dev, pipe, mouse->data,
                     (maxp > 8) ? 8 : maxp, usb_mouse_irq, mouse,
                     ep->bInterval);
    mouse->irq->transfer_dma = mouse->data_dma;
    mouse->irq->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;

    error = input_register_device(input_dev);
    if (error)
        goto fail3;

    usb_set_intfdata(intf, mouse);
    return 0;

fail3:  usb_free_urb(mouse->irq);
fail2:  usb_free_coherent(dev, 8, mouse->data, mouse->data_dma);
fail1:  input_free_device(input_dev);
        kfree(mouse);
        return error;
}

static int usb_mouse_open(struct input_dev *dev)
{
    struct usb_mouse *mouse = input_get_drvdata(dev);
    mouse->irq->dev = mouse->usbdev;
    return usb_submit_urb(mouse->irq, GFP_KERNEL) ? -EIO : 0;
}

static void usb_mouse_irq(struct urb *urb)
{
    struct usb_mouse *mouse = urb->context;
    signed char *data = mouse->data;
    struct input_dev *dev = mouse->dev;
    int status;

    switch (urb->status) {
    case 0:   /* success */ break;
    case -ECONNRESET:
    case -ENOENT:
    case -ESHUTDOWN:
        return;
    default:
        goto resubmit;
    }

    input_report_key(dev, BTN_LEFT,   data[0] & 0x01);
    input_report_key(dev, BTN_RIGHT,  data[0] & 0x02);
    input_report_key(dev, BTN_MIDDLE, data[0] & 0x04);
    input_report_key(dev, BTN_SIDE,   data[0] & 0x08);
    input_report_key(dev, BTN_EXTRA,  data[0] & 0x10);
    input_report_rel(dev, REL_X, data[1]);
    input_report_rel(dev, REL_Y, data[2]);
    input_report_rel(dev, REL_WHEEL, data[3]);
    input_sync(dev);

resubmit:
    status = usb_submit_urb(urb, GFP_ATOMIC);
    if (status)
        dev_err(&mouse->usbdev->dev,
                "can't resubmit intr, %s-%s/input0, status %d
",
                mouse->usbdev->bus->bus_name,
                mouse->usbdev->devpath, status);
}

This driver registers its device ID table, allocates a coherent buffer for interrupt data, creates an interrupt URB, and reports mouse movements and button presses through the Linux input subsystem.

References

Enhanced Host Controller Interface Specification

USB 2.0 Specification

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

KernelLinuxUSBURBEHCI
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.