SchOS/docs/architecture/usb.md
Nexoniarz fa7669254e usb: plug and play -- hot-plug, unplug, every interface, real mice
Devices can now arrive and leave after boot. A "usbhotplug" kernel thread
handles root-port changes, which it is told about by Port Status Change
events and also checks for every quarter second. It also handles changes
behind hubs, which each hub reports on its status-change endpoint. An
unplugged device has its endpoints closed and any transfer in flight
failed. Then its drivers' disconnect hooks run, the slot is disabled and
its memory is freed. Everything behind a hub that goes goes with it. This
needed the interrupt handler to file command and endpoint-0 completions
for their waiters, so that enumeration can run while keyboards interrupt.

On real hardware: the controller is taken from the firmware through USB
Legacy Support before reset, with SMIs off. On Intel 7/8/9 series parts
the ports are switched over from EHCI, which is where firmware leaves
them.

Every interface of a device is offered to the class drivers, not just
the first, so a receiver's mouse on interface 1 gets a driver. Configure
Endpoint now copies the slot context from the controller. Before, it
rebuilt it and lost the route string and TT fields of devices behind
hubs.

HID: pointers are read in report protocol from their report descriptor.
That covers buttons, X/Y and wheel, relative or absolute, and report IDs.
Boot protocol is kept as the fallback for mice and for keyboards.
Non-pointer HID interfaces are declined rather than treated as tablets.
USB keyboards get software key repeat. Mass storage handles up to four
sticks, each unregistered from the block layer when it is pulled out,
along with its cached blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
2026-10-22 11:38:13 +02:00

6.3 KiB

USB

An xHCI host controller driver, a device-independent core, and three class drivers. Written from the specification.


Why not ported from BSD

"Take the USB stack from BSD" is the obvious plan and it does not survive contact with the code.

FreeBSD's sys/dev/usb is roughly 100k lines welded to newbus, devclass, bus_dma, callouts, taskqueues and FreeBSD's locking primitives, none of which exist here. Porting it means porting FreeBSD's device framework first and then discovering the USB stack underneath. NetBSD's is more portable and the same argument applies with a smaller multiplier.

The specification is public and the controller is not complicated.

BSD is much more valuable as the reference you check yourself against than as the code you paste.

Licensing points the same way: the BSD stacks are BSD-2/3-clause and compatible, Linux's drivers are GPL-2.0-only and are not — see ../licensing.md.

What the spec gives you for free

Everything in include/usb.h comes from USB 2.0 chapter 9, which every device implements regardless of speed, class or vendor.

That is what makes enumeration possible at all: before you know anything about a device you can still ask it what it is, because the question is standardised.

The controller

kernel/drivers/bus/xhci/. The full bring-up path:

  reset → DCBAA → command ring → event ring → scratchpad
  → port enumeration → Enable Slot → Address Device → control transfers
  → configuration and interface descriptors → SET_CONFIGURATION
  → interrupt and bulk endpoints

Rings are producer rings: the driver writes TRBs and the controller reads them, with a Link TRB as the last entry pointing back at the start — which is how a fixed-size buffer becomes an endless queue. 64 TRBs per ring, 16 device slots.

Enumeration polls; data uses interrupts

And that is not laziness. pci_bind_all() runs from kmain() with interrupts disabled and no scheduler, so a driver that waited for an interrupt during probe would wait forever.

Interrupts are registered afterwards, for when a device is actually delivering data. From then on the interrupt handler is the event ring's only reader: command completions and endpoint-0 completions are filed per slot, and whoever issued them is woken to collect them. That is what lets enumeration happen again later, from a thread, while keyboards are interrupting.

Before reset the driver takes the controller from the firmware (USB Legacy Support: OS Owned, wait for BIOS Owned to clear, SMIs off), and on Intel 7/8/9 series chipsets switches the ports over from the EHCI controllers, which is where the firmware leaves them.

Hot-plug

The usbhotplug kernel thread owns every enumeration after boot. It looks at the root ports whenever the controller reports a port change and every quarter second regardless, and at hubs whenever their status-change endpoint reports one. A device that arrives is debounced for 100 ms and enumerated exactly as at boot; one that leaves has its endpoints closed (failing any transfer in flight), its drivers' disconnect called, its slot disabled and its memory freed, and the same for everything behind it if it was a hub. A device that will not enumerate is left alone until it is unplugged, not retried for ever.

The thread also drives key repeat for USB keyboards, which do not repeat by themselves: 500 ms, then about 33 a second, as the PS/2 keyboard's firmware does.

Class drivers

HID (usb/hid/) Keyboards in boot protocol. Pointers in report protocol, from their report descriptor: buttons, X, Y and wheel, relative (mice) or absolute (tablets, VM pointers), with report IDs. A boot mouse whose descriptor cannot be used falls back to boot protocol. Feeds the same input paths as PS/2.
Mass storage (usb/storage/) Bulk-only transport and SCSI. Up to four sticks, usb0..usb3, each registered with the block layer with its partitions; pulling one out unregisters it.
Hub (usb/hub/) Route strings, hub slot contexts, per-port reset and power, and the status-change endpoint for hot-plug behind the hub.

Class drivers are offered every interface of a device, each as its own usb_dev, once the configuration has been read, from the list in kernel/drivers/registry.c — a wireless receiver's keyboard and mouse are interfaces 0 and 1 and get a driver each. Whichever claims it publishes a struct device in the bus-agnostic list, with the class the driver chose rather than the one the descriptor claimed — the descriptor says what the device says it is, and the driver knows what it made of it.

What proves the hub works

A mouse behind a hub enumerating a tier deep. Hubs are the part of USB where route strings and slot contexts stop being theoretical, and a hub driver that handles only directly-attached devices looks identical to a working one until something is plugged into it.

The USB HID grab detail

The HID path updates its prev[] key state before returning even when the device is grabbed — otherwise the next report looks like every held key had just been pressed again. See graphics.md for the grab itself.

Testing

bootcheck -Usb attaches an xHCI controller with a keyboard, a mouse and a mass-storage device.

Nothing on the default machine has a USB port, so without that switch the driver is never asked to do anything and a broken one looks exactly like a working one.

echo u > /proc/sysrq and echo s > /proc/sysrq drive the storage path.


What it does not do

  • Pulling out a stick a filesystem is mounted from panics on that filesystem's next read, by the rule in blkdev_rw(): a filesystem that went on with missing blocks would be guessing.
  • HID is keyboards and pointers. Consumer-control interfaces (volume keys), game pads and the rest are declined. Keyboard LEDs are not driven.
  • USB 3 hubs are not driven: the hub driver speaks the USB 2 hub protocol.
  • No isochronous transfers, so no audio and no cameras.
  • No USB 3 SuperSpeed-specific features beyond what enumeration needs — the USB2 and USB3 descriptor paths both work, the rest of the difference is unused.
  • 16 device slots, compile-time.
  • One controller. A second xHCI card would bind and then contend for state the driver keeps per-driver rather than per-instance.