usb: enumerate devices behind a hub — route strings + transaction translators (B4b)

The core of hub support (docs/usb-hub.md): a device on a hub's
downstream port now reaches its class driver exactly like one on a root
port. The hub's interrupt status-change endpoint is armed with an
in-process subscription — completions set the hub's pending-port mask
instead of queuing a class-driver report — and setupHub seeds every
downstream port pending so a STATIC topology (a device present at
power-on) enumerates without waiting on the initial interrupt edge.

On the bus tick, each pending hub port is serviced: GET_STATUS + clear
the change bits, and on a connect reset the port, read the speed, then
enable a slot and Address Device with the composed route string
((parent_route<<4)|port), the inherited root port, and the parent hub's
slot/port as the TRANSACTION TRANSLATOR — so the controller routes a
full/low-speed device's split transactions through the hub's TT. The
device then enumerates and registers its interfaces through the normal
path (a compact topology-unique port key keeps the id tag within its
8-byte cap), recursing setupHub if it is itself a hub.

Verified in QEMU (a keyboard behind a USB2 hub on a second controller):
'hub slot 1 port 1 device vendor 0x0627 ... usb-hid-keyboard: ok
(device 35)'. Full suite 89/89. The user's SuperSpeed Genesys hub +
full-speed keyboard/mouse on its USB2 companion is the real-hardware
target, flagged separately.
This commit is contained in:
Daniel Samson
2026-07-21 22:06:54 +01:00
parent 92b03b8d07
commit 9da1a9899c
3 changed files with 213 additions and 10 deletions
@@ -242,6 +242,33 @@ fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
}
/// Bring up a device on hub downstream `port`: setup+address (with route string
/// and TT), enumerate, register its interfaces, and — if it is itself a hub —
/// set it up (recursion). Mirrors bringUpPort for a root-port device.
fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void {
const usb_device = engine.serviceHubPort(hub, port) orelse return; // empty/disconnect/failure
if (!engine.enumerate(usb_device)) {
std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port });
return;
}
std.log.info("hub slot {d} port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{
hub.slot_id, port,
usb_device.device_descriptor.vendor_id,
usb_device.device_descriptor.product_id,
usb_device.interface_count,
});
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
// A compact topology-unique port key: 1000 + slot*100 + port. Stays a few
// digits (the hid tag "P<key>I<iface>" has an 8-byte cap) while never
// colliding with a root port (1..N) or another (hub, port).
const port_key = 1000 + @as(u32, hub.slot_id) * 100 + port;
if (reportInterface(manager, port_key, interface.*)) |registered| {
interface.registered_device_id = registered;
}
}
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
}
/// Whether an enumerated device is a hub — class 9 at the device or the
/// interface level (a hub's single interface is class 9/0/0).
fn deviceIsHub(usb_device: *const library.Device) bool {
@@ -434,6 +461,13 @@ fn onNotification(badge: u64) void {
tearDownPort(manager, engine, port);
}
}
// Downstream hub-port changes (docs/usb-hub.md): a device connected on a
// hub's downstream port is enumerated and registered here, so a keyboard
// behind a hub reaches its class driver like one on a root port.
while (engine.takeHubChange()) |change| {
const manager = manager_handle orelse break;
bringUpBehindHub(manager, engine, change.hub, change.port);
}
while (engine.takeReport()) |report| {
var message = transfer.InterruptReport{
.device_token = report.device_token,