diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index ca59841..339e646 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -256,6 +256,7 @@ fn hubPortKey(hub_slot: u8, port: u16) u32 { fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void { const status = engine.hubPortStatusAck(hub, port) orelse return; const connected = library.Controller.hubPortConnected(status); + std.log.info("hub slot {d} port {d} status 0x{x:0>8} ({s})", .{ hub.slot_id, port, status, if (connected) "connected" else "empty" }); const existing = engine.deviceOnHubPort(hub, port); if (!connected) { @@ -501,9 +502,15 @@ fn onNotification(badge: u64) void { // 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. + // Cap per tick: even if a hub's change bits refuse to clear, the driver + // must not spin here — it services a bounded batch and yields to the + // next tick (and to storage, input, everything else). + var serviced: u32 = 0; while (engine.takeHubChange()) |change| { const manager = manager_handle orelse break; bringUpBehindHub(manager, engine, change.hub, change.port); + serviced += 1; + if (serviced >= 32) break; } while (engine.takeReport()) |report| { var message = transfer.InterruptReport{ diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index c88bc0f..3dc3ba8 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -951,8 +951,18 @@ pub const Controller = struct { /// bits (so it can signal again). Returns the wPortStatus word. pub fn hubPortStatusAck(self: *Controller, hub: *Device, port: u16) ?u32 { const status = self.readHubPortStatus(hub, port) orelse return null; + // Acknowledge every latched change bit. A SuperSpeed hub has extra ones + // (link-state, BH-reset) beyond a USB2 hub's connection/reset — leaving + // any set makes the hub's status-change endpoint re-report the same port + // forever, spinning the driver (a real SuperSpeed hub hung boot here; + // QEMU's USB2 hub has none of these). Clearing an inapplicable feature + // is harmless (the hub STALLs it and we move on). _ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_connection, port), &.{}, false); _ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_reset, port), &.{}, false); + if (hub.speed >= 4) { + _ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_link_state, port), &.{}, false); + _ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_bh_port_reset, port), &.{}, false); + } return status; }