USB
USB replaced a collection of device-specific buses and connectors (PS/2, serial, parallel, ADB) with a single electrical and protocol standard general enough to cover keyboards, mass storage, audio, and networking alike. That generality is also what makes USB one of the more involved pieces of hardware a kernel supports directly: unlike a fixed-purpose device with a handful of registers, a USB host has to enumerate whatever happens to be plugged in, discover what it is through a standardized but open-ended description format, and route traffic to it through one of four fundamentally different transfer mechanisms depending on what that device needs.
Host controllers
Section titled “Host controllers”Software talks to USB hardware through a host controller, and which register interface that controller exposes has changed with every major USB speed increase. UHCI (Universal Host Controller Interface), Intel’s design for USB 1.1, pushes most of the scheduling work onto software, walking a frame list the driver itself builds and links every millisecond. OHCI (Open Host Controller Interface), a competing 1.1-era design used by most non-Intel chipsets of the time, moves more of that scheduling into hardware via its own linked-list formats, at the cost of being a completely different register interface a driver has to support separately from UHCI. EHCI (Enhanced Host Controller Interface) handles USB 2.0’s High Speed (480 Mbit/s) traffic, but by design only that traffic: a EHCI controller hands Low- and Full-Speed devices back to a companion UHCI or OHCI controller sitting alongside it, splitting a single physical port’s traffic across two different register interfaces depending on the device’s negotiated speed. xHCI (Extensible Host Controller Interface) unifies all of this: one register interface and one set of data structures cover every USB speed from Low through SuperSpeed, with most of the per-transfer scheduling handled by the controller itself rather than by software walking linked lists, which is also why xHCI is the only one of the four with native support for USB 3’s SuperSpeed. All four are identified on the PCI bus under class 0x0C (serial bus controller), subclass 0x03 (USB), with the programming interface byte distinguishing which of the four register interfaces a given controller implements.
Descriptors
Section titled “Descriptors”A USB device describes itself to the host through a fixed hierarchy of descriptors, each a small binary structure with a defined layout the host parses without needing any driver specific to that exact device just to read them. A device descriptor sits at the top, one per device, carrying a vendor ID and product ID pair (the same kind of identifier scheme PCI uses, though drawn from USB’s own separate registry), a device class, and the number of configurations the device supports. Below that, a configuration descriptor describes one particular power and functionality profile a device can be placed into; most devices expose exactly one. Nested inside a configuration are one or more interface descriptors, each representing a distinct function the device exposes: a USB webcam with a built-in microphone would expose one interface for video and a separate one for audio, letting the host bind entirely different drivers to each half of the same physical device. Each interface in turn lists one or more endpoint descriptors, describing the actual communication channels the interface uses: a direction (host-to-device or device-to-host), a transfer type, a maximum packet size, and for interrupt endpoints, a polling interval. This nesting means reading a device’s full descriptor set is a matter of walking a known tree shape rather than parsing an arbitrary format, even though what any particular device’s tree contains varies enormously between a keyboard and a hard drive.
Enumeration
Section titled “Enumeration”A device is invisible to software until enumeration assigns it an identity the host can address directly. Every device attaches to the bus initially at address 0, the one address every device is required to respond to before configuration, specifically to make this bootstrap possible without a chicken-and-egg addressing problem. The host issues a GET_DESCRIPTOR request to address 0 to read the device descriptor’s first few bytes, learning the device’s maximum packet size for its default control endpoint before communicating with it further. It then issues SET_ADDRESS, moving the device to a host-assigned address (1 through 127) that it will respond to for the remainder of the session, freeing address 0 for the next device to attach. With a stable address assigned, the host reads the full device, configuration, interface, and endpoint descriptor set, and finally issues SET_CONFIGURATION to select which configuration the device should actually operate under, at which point the device’s endpoints become usable for real transfers. A hub, itself just another USB device with a device class dedicated to the purpose, repeats this dance for each downstream device it detects attaching or detaching, which is how a tree of hubs and devices gets enumerated without every device needing a directly wired path to the host controller.
Transfer types
Section titled “Transfer types”USB defines four transfer types, and an endpoint is permanently committed to exactly one of them for the life of a given configuration. Control transfers are bidirectional, reliable, and used for device configuration and the enumeration requests described above; every device has at least one control endpoint (endpoint 0) purely for this purpose. Interrupt transfers are polled by the host at a fixed interval the endpoint descriptor specifies, despite the name suggesting the device pushes data unprompted; a USB keyboard’s endpoint might specify a 10 ms interval, meaning the host asks it for new data one hundred times a second whether or not a key has actually changed state. Bulk transfers carry large amounts of data reliably but with no timing guarantee, using whatever bus bandwidth is left over after other transfer types have taken their reserved share; mass storage devices use bulk transfers almost exclusively, since a large file copy cares about eventual correctness and throughput far more than latency. Isochronous transfers guarantee bus bandwidth and timing at the cost of reliability entirely: a dropped isochronous packet is simply gone, appropriate for streaming audio or video where a brief glitch is preferable to the transfer stalling to retransmit a packet that has already become irrelevant by the time a retry would arrive.
Legacy emulation and boot-time keyboards
Section titled “Legacy emulation and boot-time keyboards”Firmware and host controllers commonly implement USB legacy emulation specifically so that a USB keyboard works before any USB-aware operating system driver has loaded, intercepting the device’s actual USB HID reports at the hardware or firmware level and re-presenting them to the CPU as ordinary PS/2 keyboard controller traffic. This is why a USB keyboard visibly works in a BIOS setup screen or an early boot loader that has never heard of USB: nothing in that code path is actually speaking USB at all, and the operating system’s own USB stack, once it initializes, typically has to explicitly disable legacy emulation for that device before it can address it directly and take over from the emulated PS/2 interface.
Implementation notes
Section titled “Implementation notes”Writing a driver against all four host controller types is considerably more work than the transfer-type and descriptor model above suggests, since each of UHCI, OHCI, EHCI, and xHCI has its own entirely distinct data structures for describing pending transfers to the controller, despite exposing the same logical USB semantics to software above that layer; a hobbyist kernel commonly implements only UHCI or only xHCI first, accepting reduced hardware compatibility, rather than all four at once. SET_ADDRESS has a specific timing requirement worth respecting exactly: the device must complete the status stage of that request and then has a defined recovery time before it is guaranteed to respond at its new address, and software that immediately issues further requests without that delay risks a device that appears to have accepted the address but doesn’t yet answer to it. Finally, a maximum packet size read from a device descriptor’s very first eight bytes, before the rest of the descriptor is even known, is what an implementation needs before it can safely issue the second, complete GET_DESCRIPTOR request for that same descriptor, since a control transfer’s packet size is exactly the piece of information the host doesn’t yet have when it first talks to a device at address 0.
References
Section titled “References”- ^ USB Implementers Forum, Universal Serial Bus Specification, revision 2.0 (defines descriptor formats, transfer types, and the enumeration sequence)
- ^ Intel, Enhanced Host Controller Interface Specification for Universal Serial Bus (defines the EHCI register interface and companion-controller handoff)
- ^ OSDev Wiki, “Universal Serial Bus” (a hobbyist overview with host-controller-specific implementation notes)