From 18408b0666e5f886dbb859f04ec2e70e696dd6f7 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:50:11 +0100 Subject: [PATCH 1/8] usb: [diagnostic] dump all root-port PORTSC + log port-change events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branch-only debugging for the real SuperSpeed compound hub: the spin fix (27f87cb) stopped the hang — the SS hub's 4 SuperSpeed ports now enumerate (all empty, correct: the full-speed keyboard isn't a SuperSpeed device) — but the USB2 COMPANION hub, where the keyboard actually lives, never appears. Only root ports 19 (SS hub) and 21 (SS storage) connect. Dump every root port's raw PORTSC at scan (connect/enable/link-state/ speed) and log every root port-change event, so the next boot shows whether the companion is connected on a USB2 port we misread, connects late as a port-change event, or is simply absent. Diagnostic logging — to be removed once the companion path works. --- system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 339e646..5a24e70 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -196,6 +196,18 @@ fn scanPorts(manager: runtime.ipc.Handle) void { }; std.log.info("{d} root-hub ports", .{engine.max_ports}); + // Diagnostic: dump every root port's raw PORTSC (CCS = connect, PED = + // enabled, PLS = link state, speed) so a USB2 companion hub that isn't + // enumerating is visible — is it connected on a port we misread, or absent? + { + var p: u32 = 1; + while (p <= engine.max_ports) : (p += 1) { + const portsc = engine.portStatus(p); + if (portsc & 1 != 0 or (portsc >> 5) & 0xF != 5) // connected, or not in the disabled/disconnected link state + std.log.info("PORTSC[{d}] = 0x{x:0>8} ccs={d} ped={d} pls={d} speed={d}", .{ p, portsc, portsc & 1, (portsc >> 1) & 1, (portsc >> 5) & 0xF, (portsc >> 10) & 0xF }); + } + } + var port: u32 = 1; var connected: u32 = 0; while (port <= engine.max_ports) : (port += 1) { @@ -493,7 +505,9 @@ fn onNotification(badge: u64) void { engine.pump(); while (engine.takePortChange()) |port| { const manager = manager_handle orelse break; - if (engine.portConnected(port)) { + const connected = engine.portConnected(port); + std.log.info("root port {d} change: {s} (PORTSC 0x{x:0>8})", .{ port, if (connected) "connected" else "empty", engine.portStatus(port) }); + if (connected) { if (engine.deviceOnPort(port) == null) bringUpPort(manager, engine, port); } else { tearDownPort(manager, engine, port); From 10956c66609ea33e8090d8a0749fec6849b4df38 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:56:25 +0100 Subject: [PATCH 2/8] usb: [diagnostic] dump the xECP USB2/USB3 port map + all PORTSC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The USB2 companion hub carrying the user's full-speed keyboard never appears — only the SuperSpeed hub (port 19) and SS storage (port 21) connect. To find where the USB2 root ports are and whether the companion is presenting on one, walk the xECP Supported Protocol capabilities (logging each USB 2.0 / 3.0 root-port range) and dump every port's raw PORTSC unconditionally (ccs/ped/pls/pp/speed). QEMU confirms the dump (USB2 ports 5-8, USB3 ports 1-4). Branch-only diagnostic. --- system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 14 ++------ .../drivers/usb-xhci-bus/usb-xhci-library.zig | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 5a24e70..eef7f91 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -196,17 +196,9 @@ fn scanPorts(manager: runtime.ipc.Handle) void { }; std.log.info("{d} root-hub ports", .{engine.max_ports}); - // Diagnostic: dump every root port's raw PORTSC (CCS = connect, PED = - // enabled, PLS = link state, speed) so a USB2 companion hub that isn't - // enumerating is visible — is it connected on a port we misread, or absent? - { - var p: u32 = 1; - while (p <= engine.max_ports) : (p += 1) { - const portsc = engine.portStatus(p); - if (portsc & 1 != 0 or (portsc >> 5) & 0xF != 5) // connected, or not in the disabled/disconnected link state - std.log.info("PORTSC[{d}] = 0x{x:0>8} ccs={d} ped={d} pls={d} speed={d}", .{ p, portsc, portsc & 1, (portsc >> 1) & 1, (portsc >> 5) & 0xF, (portsc >> 10) & 0xF }); - } - } + // Diagnostic: the USB2/USB3 root-port map (xECP) + every port's raw PORTSC, + // to find a USB2 companion hub that isn't presenting a connection. + engine.dumpPortTopology(); var port: u32 = 1; var connected: u32 = 0; diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index 3dc3ba8..7b00547 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -484,6 +484,39 @@ pub const Controller = struct { } // PORTSC for 1-based port `port`. + /// Diagnostic: walk the xECP list and log each Supported Protocol capability + /// (USB 2.0 vs 3.x, the compatible root-port range), then dump every port's + /// raw PORTSC. Reveals where the USB2 root ports are and their state — for + /// finding a USB2 companion hub that isn't presenting a connection. + pub fn dumpPortTopology(self: *const Controller) void { + const hccparams1 = read32(self.register_base + cap_hccparams1); + var offset: usize = (hccparams1 >> 16) & 0xFFFF; // xECP: dword offset from register_base + var guard: u32 = 0; + while (offset != 0 and guard < 64) : (guard += 1) { + const cap_base = self.register_base + offset * 4; + const dw0 = read32(cap_base); + const id = dw0 & 0xFF; + if (id == 2) { // Supported Protocol + const dw2 = read32(cap_base + 8); + const major = (dw0 >> 24) & 0xFF; + const minor = (dw0 >> 16) & 0xFF; + const port_offset = dw2 & 0xFF; + const port_count = (dw2 >> 8) & 0xFF; + std.log.info("xECP USB {d}.{d}: root ports {d}..{d}", .{ major, minor, port_offset, port_offset + port_count - 1 }); + } + const next = (dw0 >> 8) & 0xFF; + if (next == 0) break; + offset += next; + } + var port: u32 = 1; + while (port <= self.max_ports) : (port += 1) { + const portsc = self.portStatus(port); + std.log.info("PORTSC[{d}] = 0x{x:0>8} ccs={d} ped={d} pls={d} pp={d} speed={d}", .{ + port, portsc, portsc & 1, (portsc >> 1) & 1, (portsc >> 5) & 0xF, (portsc >> 9) & 1, (portsc >> 10) & 0xF, + }); + } + } + pub fn portStatus(self: *const Controller, port: u32) u32 { return read32(self.op_base + op_portsc_base + op_portsc_stride * (port - 1)); } From 0b10a637ae1df2b741bef51a3e54a5c650f76bc7 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:06:49 +0100 Subject: [PATCH 3/8] =?UTF-8?q?usb:=20poll=20root=20ports=20on=20the=20tic?= =?UTF-8?q?k=20=E2=80=94=20catch=20a=20late=20USB2=20connection=20(edge-tr?= =?UTF-8?q?iggered)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-hardware root-cause: the user's keyboard works in the firmware boot menu but dies under danos. The port dump shows why — danos scans the root ports ~3 ms after the controller reset, but a USB2 connection needs ~100 ms to debounce, so the boot scan sees the USB2 companion hub's port empty (all USB2 ports ccs=0), while the SuperSpeed devices (hub, storage) train instantly and DO show up. danos then relied on a Port Status Change EVENT to catch the late USB2 connection, which does not fire reliably on this hardware. The tick now polls every root port and reconciles on the empty->connected EDGE (previous-state tracked per port, seeded from the boot scan so already-up ports never re-fire), bringing up a device that appears after the scan without depending on the event. Edge-triggered so a port that fails to enumerate is not retried every 8 ms. Branch-only until the keyboard is confirmed on real hardware; the diagnostic dumps stay for the next boot. --- system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index eef7f91..c2c5f8b 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -189,6 +189,10 @@ fn speedName(speed: u32) []const u8 { /// class driver against. var manager_handle: ?runtime.ipc.Handle = null; +// Per-root-port connected state from the previous tick, so the poll acts on +// empty->connected transitions (edge), never re-attempting a level every tick. +var prev_connected: [64]bool = [_]bool{false} ** 64; + fn scanPorts(manager: runtime.ipc.Handle) void { const engine = if (controller) |*c| c else { _ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n"); @@ -204,6 +208,7 @@ fn scanPorts(manager: runtime.ipc.Handle) void { var connected: u32 = 0; while (port <= engine.max_ports) : (port += 1) { if (!engine.portConnected(port)) continue; + if (port < prev_connected.len) prev_connected[port] = true; // don't re-fire the poll for these connected += 1; bringUpPort(manager, engine, port); } @@ -495,6 +500,29 @@ fn onNotification(badge: u64) void { if (badge & runtime.ipc.notify_timer_bit == 0) return; if (controller) |*engine| { engine.pump(); + // Poll every root port and reconcile — a device present but not yet + // enumerated is brought up; a device gone is torn down. This does NOT + // depend on a Port Status Change EVENT firing: the boot scan runs ~3 ms + // after the controller reset, far too early for a USB2 connection to + // debounce (~100 ms), and the SuperSpeed devices that DO show up early + // proved the event path unreliable for the late USB2 companion hub on + // real hardware. Polling catches it on the next tick regardless. + if (manager_handle) |manager| { + var port: u32 = 1; + while (port <= engine.max_ports and port <= prev_connected.len) : (port += 1) { + const connected = engine.portConnected(port); + const was = prev_connected[port]; + prev_connected[port] = connected; + if (connected and !was and engine.deviceOnPort(port) == null) { + // Rising edge the boot scan missed (it ran before the USB2 + // connection debounced): bring the device up now. + std.log.info("root port {d}: device appeared (PORTSC 0x{x:0>8})", .{ port, engine.portStatus(port) }); + bringUpPort(manager, engine, port); + } else if (!connected and was and engine.deviceOnPort(port) != null) { + tearDownPort(manager, engine, port); + } + } + } while (engine.takePortChange()) |port| { const manager = manager_handle orelse break; const connected = engine.portConnected(port); From 4ea4a040d2b53afbe74c729cd94a026d8ea872a3 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:15:16 +0100 Subject: [PATCH 4/8] usb-ids: className/interfaceName helpers; readable device classes in the bus log The logs read 'class 9/0/0' where they could say 'Hub'. usb-ids gains className (a device/interface class byte -> 'Hub', 'HID', 'Mass Storage', ...) and interfaceName (a HID boot interface -> 'HID boot keyboard' / 'HID boot mouse'), both host-tested with usb-ids. The bus driver's enumerate + register lines now read, e.g.: port 21 device: Hub vendor 0x05e3 product 0x0626, 1 interface(s) port 21 interface 0: Hub (9/0/0) registered as device 47 port 5 interface 0: HID boot keyboard (3/1/1) registered as device 32 so a real-hardware log is scannable by eye. Full USB case set green (hub regexes follow the new 'device: ' format). --- system/devices/usb-ids.zig | 47 ++++++++++++++++++++ system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 9 ++-- test/qemu_test.py | 4 +- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/system/devices/usb-ids.zig b/system/devices/usb-ids.zig index 470fc95..d1d907d 100644 --- a/system/devices/usb-ids.zig +++ b/system/devices/usb-ids.zig @@ -70,6 +70,53 @@ pub const Class = enum(u8) { _, }; +/// A human-readable name for a device/interface class code, for logs. Unknown +/// codes fall through to "class 0xNN". +pub fn className(class: u8) []const u8 { + return switch (@as(Class, @enumFromInt(class))) { + .per_interface => "per-interface", + .audio => "Audio", + .communications => "Communications", + .hid => "HID", + .physical => "Physical", + .image => "Image", + .printer => "Printer", + .mass_storage => "Mass Storage", + .hub => "Hub", + .cdc_data => "CDC Data", + .smart_card => "Smart Card", + .content_security => "Content Security", + .video => "Video", + .personal_healthcare => "Personal Healthcare", + .audio_video => "Audio/Video", + .billboard => "Billboard", + .type_c_bridge => "Type-C Bridge", + .bulk_display => "Bulk Display", + .mctp => "MCTP", + .i3c => "I3C", + .diagnostic => "Diagnostic", + .wireless_controller => "Wireless Controller", + .miscellaneous => "Miscellaneous", + .application_specific => "Application-specific", + .vendor_specific => "Vendor-specific", + _ => "Unknown", + }; +} + +/// A fuller name for a HID interface's (subclass, protocol): "HID boot keyboard" +/// / "HID boot mouse", else just the class name. Used to make input devices +/// recognizable in logs. +pub fn interfaceName(class: u8, subclass: u8, protocol: u8) []const u8 { + if (@as(Class, @enumFromInt(class)) == .hid and subclass == @intFromEnum(hid.SubClass.boot)) { + return switch (@as(hid.Protocol, @enumFromInt(protocol))) { + .keyboard => "HID boot keyboard", + .mouse => "HID boot mouse", + else => "HID", + }; + } + return className(class); +} + // Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the // protocol distinguishes the hub's transaction-translator arrangement. pub const hub = struct { diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index c2c5f8b..767cb31 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -230,8 +230,9 @@ fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u std.log.info("port {d} enumeration failed", .{port}); return; } - std.log.info("port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{ + std.log.info("port {d} device: {s} vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{ port, + usb_ids.className(usb_device.device_descriptor.device_class), usb_device.device_descriptor.vendor_id, usb_device.device_descriptor.product_id, usb_device.interface_count, @@ -279,8 +280,9 @@ fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hu 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)", .{ + std.log.info("hub slot {d} port {d} device: {s} vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{ hub.slot_id, port, + usb_ids.className(usb_device.device_descriptor.device_class), usb_device.device_descriptor.vendor_id, usb_device.device_descriptor.product_id, usb_device.interface_count, @@ -387,9 +389,10 @@ fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.In std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number }); return null; }; - std.log.info("port {d} interface {d} class {d}/{d}/{d} registered as device {d}", .{ + std.log.info("port {d} interface {d}: {s} ({d}/{d}/{d}) registered as device {d}", .{ port, interface.number, + usb_ids.interfaceName(interface.class, interface.subclass, interface.protocol), interface.class, interface.subclass, interface.protocol, diff --git a/test/qemu_test.py b/test/qemu_test.py index e321ec8..025bd92 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -494,7 +494,7 @@ CASES = [ # B4a: the hub powers its ports. B4b: the keyboard behind it enumerates # (route string + TT) and binds usb-hid-keyboard. "expect": r"(?s)(?=.*hub slot \d+: \d+ downstream ports powered)" - r"(?=.*hub slot \d+ port \d+ device vendor 0x0627)" + r"(?=.*hub slot \d+ port \d+ device:.*vendor 0x0627)" r"(?=.*usb-hid-keyboard: ok \(device 3)", "fail": r"DANOS-TEST-RESULT: FAIL"}, # USB mass storage end to end: the boot usb-storage device (the FAT32 image, @@ -676,7 +676,7 @@ CASES = [ "-device", "usb-hub,bus=xhci2.0,port=1", "-device", "usb-hub,bus=xhci2.0,port=1.1", "-device", "usb-kbd,bus=xhci2.0,port=1.1.1"], - "expect": r"(?s)(?=.*hub slot \d+ port \d+ device vendor 0x0409)" + "expect": r"(?s)(?=.*hub slot \d+ port \d+ device:.*vendor 0x0409)" r"(?=.*usb-hid-keyboard: ok \(device 3)", "fail": r"DANOS-TEST-RESULT: FAIL"}, # Hub-downstream disconnect (B4c): device_del the keyboard behind the hub; From ab732dc455850da9576768fb993b4a8e80365d3e Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:19:18 +0100 Subject: [PATCH 5/8] usb: readable PORTSC decode, link-state names, and device string descriptors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the USB diagnostic logs scannable by eye: - usb-ids gains speedName + linkStateName (USB3 PORTSC link states: U0, RxDetect, Polling, ...), host-tested with usb-ids. - The PORTSC dump decodes the register instead of printing hex flags: PORTSC[3] 0x00021203: connected, enabled, link=U0, power=on, SuperSpeed PORTSC[5] 0x00020ee1: connected, disabled, link=Polling, power=on, High-speed (the raw word stays for reference). A USB2 device reads connected+ disabled+Polling until reset — so the user's next log shows at a glance whether the companion port ever reaches that state. - The library reads STRING descriptors (readString, UTF-16LE -> ASCII, English langid), and the device line now names the maker + product: port 5 device: Hub "Genesys Logic USB3.0 Hub" (0x05e3:0x0626), 1 interface(s) instead of a bare vendor/product id pair. Full USB case set green (hub regexes follow the new 'device: " "' format). Branch diagnostics for the SuperSpeed compound-hub investigation. --- system/devices/usb-ids.zig | 32 +++++++++++++++ system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 16 +++++++- .../drivers/usb-xhci-bus/usb-xhci-library.zig | 39 ++++++++++++++++++- test/qemu_test.py | 4 +- 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/system/devices/usb-ids.zig b/system/devices/usb-ids.zig index d1d907d..c086aaf 100644 --- a/system/devices/usb-ids.zig +++ b/system/devices/usb-ids.zig @@ -103,6 +103,38 @@ pub fn className(class: u8) []const u8 { }; } +/// The USB speed class (as xHCI reports it in PORTSC/slot contexts) named. +pub fn speedName(speed: u32) []const u8 { + return switch (speed) { + 1 => "Full-speed", + 2 => "Low-speed", + 3 => "High-speed", + 4 => "SuperSpeed", + 5 => "SuperSpeedPlus", + else => "unknown-speed", + }; +} + +/// A USB3 Port Link State (xHCI PORTSC PLS field) named. +pub fn linkStateName(pls: u32) []const u8 { + return switch (pls) { + 0 => "U0", + 1 => "U1", + 2 => "U2", + 3 => "U3-suspended", + 4 => "Disabled", + 5 => "RxDetect", + 6 => "Inactive", + 7 => "Polling", + 8 => "Recovery", + 9 => "HotReset", + 10 => "Compliance", + 11 => "Test", + 15 => "Resume", + else => "reserved", + }; +} + /// A fuller name for a HID interface's (subclass, protocol): "HID boot keyboard" /// / "HID boot mouse", else just the class name. Used to make input devices /// recognizable in logs. diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 767cb31..313a92c 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -230,9 +230,15 @@ fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u std.log.info("port {d} enumeration failed", .{port}); return; } - std.log.info("port {d} device: {s} vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{ + var maker_buffer: [64]u8 = undefined; + var product_buffer: [64]u8 = undefined; + const maker = engine.readString(usb_device, @intFromEnum(usb_device.device_descriptor.manufacturer_index), &maker_buffer) orelse "?"; + const product = engine.readString(usb_device, @intFromEnum(usb_device.device_descriptor.product_index), &product_buffer) orelse "?"; + std.log.info("port {d} device: {s} \"{s} {s}\" (0x{x:0>4}:0x{x:0>4}), {d} interface(s)", .{ port, usb_ids.className(usb_device.device_descriptor.device_class), + maker, + product, usb_device.device_descriptor.vendor_id, usb_device.device_descriptor.product_id, usb_device.interface_count, @@ -280,9 +286,15 @@ fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hu std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port }); return; } - std.log.info("hub slot {d} port {d} device: {s} vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{ + var maker_buffer: [64]u8 = undefined; + var product_buffer: [64]u8 = undefined; + const maker = engine.readString(usb_device, @intFromEnum(usb_device.device_descriptor.manufacturer_index), &maker_buffer) orelse "?"; + const product = engine.readString(usb_device, @intFromEnum(usb_device.device_descriptor.product_index), &product_buffer) orelse "?"; + std.log.info("hub slot {d} port {d} device: {s} \"{s} {s}\" (0x{x:0>4}:0x{x:0>4}), {d} interface(s)", .{ hub.slot_id, port, usb_ids.className(usb_device.device_descriptor.device_class), + maker, + product, usb_device.device_descriptor.vendor_id, usb_device.device_descriptor.product_id, usb_device.interface_count, diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index 7b00547..feaef0b 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -22,6 +22,7 @@ const std = @import("std"); const runtime = @import("runtime"); const mmio = @import("mmio"); const usb_abi = @import("usb-abi"); +const usb_ids = @import("usb-ids"); const dma = runtime.dma; const system = runtime.system; @@ -511,12 +512,46 @@ pub const Controller = struct { var port: u32 = 1; while (port <= self.max_ports) : (port += 1) { const portsc = self.portStatus(port); - std.log.info("PORTSC[{d}] = 0x{x:0>8} ccs={d} ped={d} pls={d} pp={d} speed={d}", .{ - port, portsc, portsc & 1, (portsc >> 1) & 1, (portsc >> 5) & 0xF, (portsc >> 9) & 1, (portsc >> 10) & 0xF, + std.log.info("PORTSC[{d}] 0x{x:0>8}: {s}, {s}, link={s}, power={s}, {s}", .{ + port, + portsc, + if (portsc & 1 != 0) "connected" else "empty", + if (portsc & 2 != 0) "enabled" else "disabled", + usb_ids.linkStateName((portsc >> 5) & 0xF), + if (portsc & (1 << 9) != 0) "on" else "off", + usb_ids.speedName((portsc >> 10) & 0xF), }); } } + /// Read USB STRING descriptor `index` (English, langid 0x0409) into `out` as + /// ASCII, returning the slice — for logging manufacturer/product names. + /// Null for index 0 (no string) or a failed transfer. Non-ASCII code units + /// become '?'. + pub fn readString(self: *Controller, device: *Device, index: u8, out: []u8) ?[]const u8 { + if (index == 0) return null; + var raw: [256]u8 = undefined; + const request = usb_abi.Request{ + .request_type = .{ .recipient = .device, .kind = .standard, .direction = .device_to_host }, + .request_code = .get_descriptor, + .value = (@as(u16, 3) << 8) | index, // STRING descriptor + .index = 0x0409, // English (US) + .length = raw.len, + }; + if (!self.controlTransfer(device, request, raw[0..], true)) return null; + const length = raw[0]; // bLength; the UTF-16LE payload is bytes 2..length + if (length < 2) return null; + const chars = (@min(length, raw.len) - 2) / 2; + var n: usize = 0; + var i: usize = 0; + while (i < chars and n < out.len) : (i += 1) { + const unit = @as(u16, raw[2 + i * 2]) | (@as(u16, raw[2 + i * 2 + 1]) << 8); + out[n] = if (unit >= 0x20 and unit < 0x7F) @intCast(unit) else '?'; + n += 1; + } + return out[0..n]; + } + pub fn portStatus(self: *const Controller, port: u32) u32 { return read32(self.op_base + op_portsc_base + op_portsc_stride * (port - 1)); } diff --git a/test/qemu_test.py b/test/qemu_test.py index 025bd92..5fb6f9f 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -494,7 +494,7 @@ CASES = [ # B4a: the hub powers its ports. B4b: the keyboard behind it enumerates # (route string + TT) and binds usb-hid-keyboard. "expect": r"(?s)(?=.*hub slot \d+: \d+ downstream ports powered)" - r"(?=.*hub slot \d+ port \d+ device:.*vendor 0x0627)" + r"(?=.*hub slot \d+ port \d+ device:.*0x0627)" r"(?=.*usb-hid-keyboard: ok \(device 3)", "fail": r"DANOS-TEST-RESULT: FAIL"}, # USB mass storage end to end: the boot usb-storage device (the FAT32 image, @@ -676,7 +676,7 @@ CASES = [ "-device", "usb-hub,bus=xhci2.0,port=1", "-device", "usb-hub,bus=xhci2.0,port=1.1", "-device", "usb-kbd,bus=xhci2.0,port=1.1.1"], - "expect": r"(?s)(?=.*hub slot \d+ port \d+ device:.*vendor 0x0409)" + "expect": r"(?s)(?=.*hub slot \d+ port \d+ device:.*0x0409)" r"(?=.*usb-hid-keyboard: ok \(device 3)", "fail": r"DANOS-TEST-RESULT: FAIL"}, # Hub-downstream disconnect (B4c): device_del the keyboard behind the hub; From da5f404041ad331df1ccccb8663a647e3cc4029f Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:25:19 +0100 Subject: [PATCH 6/8] usb: post-reset recovery delay + Address Device retry on transaction error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-hardware root-cause, from the now-readable log: the tick-poll fix DID catch the user's full-speed devices (USB2 root ports 3, 9, 11 'device appeared — Full-speed'), but every one failed 'Address Device completion code 4' = USB Transaction Error. Cause: danos addressed the device immediately after the port reset, but USB 2.0 (spec 7.1.7.5) requires a reset-recovery interval (TRSTRCY, 10 ms) before a device answers SET_ADDRESS. QEMU tolerates the omission; real full-speed devices do not. setupDevice now waits 10 ms after a port reset before addressing, and addressDeviceCommand retries up to 3 times on a transaction error, re-resetting a root-port device between attempts (xHCI 4.6.5 recovery). QEMU USB cases still green. Real-hardware confirmation pending — this is the specific fix for the user's keyboard/mouse failing to address. --- .../drivers/usb-xhci-bus/usb-xhci-library.zig | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index feaef0b..656ca98 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -101,6 +101,7 @@ pub const TrbType = enum(u6) { pub const CompletionCode = enum(u8) { invalid = 0, success = 1, + usb_transaction_error = 4, short_packet = 13, _, }; @@ -836,19 +837,31 @@ pub const Controller = struct { } fn addressDeviceCommand(self: *Controller, device: *Device) bool { - const physical = self.submitCommand(.{ - .parameter = device.input_context.physical, - .control = trbControl(.address_device, @as(u32, device.slot_id) << 24), - }); - const code = self.awaitCommand(physical) orelse { - std.log.info("port {d} setup: Address Device timed out", .{device.port}); - return false; - }; - if (code != @intFromEnum(CompletionCode.success)) { - std.log.info("port {d} setup: Address Device completion code {d}", .{ device.port, code }); - return false; + // Retry on a USB Transaction Error (code 4): a freshly-reset device can + // miss the first SET_ADDRESS; re-reset the port and try again (xHCI + // 4.6.5). Up to 3 attempts. + var attempt: u32 = 0; + while (attempt < 3) : (attempt += 1) { + const physical = self.submitCommand(.{ + .parameter = device.input_context.physical, + .control = trbControl(.address_device, @as(u32, device.slot_id) << 24), + }); + const code = self.awaitCommand(physical) orelse { + std.log.info("port {d} setup: Address Device timed out (attempt {d})", .{ device.port, attempt + 1 }); + return false; + }; + if (code == @intFromEnum(CompletionCode.success)) return true; + std.log.info("port {d} setup: Address Device completion code {d} (attempt {d})", .{ device.port, code, attempt + 1 }); + if (code != @intFromEnum(CompletionCode.usb_transaction_error)) return false; + // Re-reset a root-port device and wait the recovery interval before + // retrying. (A device behind a hub is reset through the hub — not + // retried here; its port was reset in serviceHubPort.) + if (device.parent_slot == 0) { + if (!self.resetPort(device.port)) return false; + system.sleep(10); + } else return false; } - return true; + return false; } /// Reset the port, enable a slot, and address the device on it: after this the @@ -874,6 +887,11 @@ pub const Controller = struct { // (pre-reset reads misreport on real controllers; M20). effective_speed = (self.portStatus(port) >> 10) & 0xF; if (effective_speed == 0) effective_speed = speed; // defensive: keep the caller's read + // USB 2.0 spec 7.1.7.5: a device needs a reset-recovery interval + // (TRSTRCY, 10 ms) after reset before it answers SET_ADDRESS. + // Addressing immediately gives a USB Transaction Error (code 4) on + // real full-speed devices; QEMU tolerates the omission. + system.sleep(10); } const slot_id = self.enableSlot() orelse { std.log.info("port {d} setup: Enable Slot failed", .{port}); From 570f6f545cc06a626ca2837985bc42271eea82f9 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:33:12 +0100 Subject: [PATCH 7/8] usb: quiet the investigation diagnostics now that hubs work on real HW The compound-hub investigation is resolved (Keychron keyboard + ROG mouse enumerate through the Genesys USB2 companion hub and type on real hardware). Turn the debugging spam back down for main: the full 22-port PORTSC dump runs only when a scan finds NOTHING (a 'why is this empty' aid), not every boot; a downstream hub port logs only when a device actually appears or leaves, not for every empty seed-sweep port. The readable device names, class names, and port-change events stay. --- system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 313a92c..fae9393 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -200,10 +200,6 @@ fn scanPorts(manager: runtime.ipc.Handle) void { }; std.log.info("{d} root-hub ports", .{engine.max_ports}); - // Diagnostic: the USB2/USB3 root-port map (xECP) + every port's raw PORTSC, - // to find a USB2 companion hub that isn't presenting a connection. - engine.dumpPortTopology(); - var port: u32 = 1; var connected: u32 = 0; while (port <= engine.max_ports) : (port += 1) { @@ -212,7 +208,10 @@ fn scanPorts(manager: runtime.ipc.Handle) void { connected += 1; bringUpPort(manager, engine, port); } - if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n"); + if (connected == 0) { + _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n"); + engine.dumpPortTopology(); // help diagnose an empty scan: the xECP map + raw PORTSC + } } /// Bring up whatever is on `port`: setup + enumerate + register/report one child @@ -272,8 +271,9 @@ 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 != (existing != null)) // only when a device appears or leaves — not empty seed-sweep ports + std.log.info("hub slot {d} port {d}: {s} (status 0x{x:0>4})", .{ hub.slot_id, port, if (connected) "device connected" else "device removed", status & 0xFFFF }); if (!connected) { if (existing) |dev| tearDownHubDevice(manager, engine, dev); From 802d51ba74384ccb2382b527e22221dbe2ef471f Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:43:01 +0100 Subject: [PATCH 8/8] usb-ids: human-readable name for every class/subclass/protocol enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every enum in usb-ids now has a name function, so any code (logs, tools) can print the readable value instead of a raw byte: hub.protocolName, hid.subclassName/protocolName, mass_storage.subclassName/protocolName, communications.subclassName, wireless_controller.subclassName/ protocolName, miscellaneous.subclassName/protocolName, application_specific.subclassName (alongside the existing className, speedName, linkStateName). interfaceName is now comprehensive across classes — it decodes the well-known triples a log shows: 'HID boot keyboard', 'Mass Storage (Bulk-Only)', 'Hub (SuperSpeed)', 'Bluetooth' (E0/01/01), etc. So the bus log reads e.g. interface 0: Mass Storage (Bulk-Only) (8/6/80) registered as device 30 interface 0: HID boot keyboard (3/1/1) registered as device 33 Host-tested with usb-ids (the name decodings are pinned). Vendor-ID naming deliberately left out (needs a data table); this is pure standard class-code decoding. --- system/devices/usb-ids.zig | 174 +++++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 9 deletions(-) diff --git a/system/devices/usb-ids.zig b/system/devices/usb-ids.zig index c086aaf..1ebedda 100644 --- a/system/devices/usb-ids.zig +++ b/system/devices/usb-ids.zig @@ -135,18 +135,37 @@ pub fn linkStateName(pls: u32) []const u8 { }; } -/// A fuller name for a HID interface's (subclass, protocol): "HID boot keyboard" -/// / "HID boot mouse", else just the class name. Used to make input devices -/// recognizable in logs. +/// The most useful readable name for an interface's (class, subclass, protocol) +/// triple, decoding the well-known combinations recognizable in a log — e.g. +/// "HID boot keyboard", "Mass Storage SCSI Bulk-Only", "Bluetooth". Falls back +/// to the class name (and then "Unknown") for codes without a spelled-out combo. pub fn interfaceName(class: u8, subclass: u8, protocol: u8) []const u8 { - if (@as(Class, @enumFromInt(class)) == .hid and subclass == @intFromEnum(hid.SubClass.boot)) { - return switch (@as(hid.Protocol, @enumFromInt(protocol))) { + return switch (@as(Class, @enumFromInt(class))) { + .hid => if (subclass == @intFromEnum(hid.SubClass.boot)) switch (@as(hid.Protocol, @enumFromInt(protocol))) { .keyboard => "HID boot keyboard", .mouse => "HID boot mouse", - else => "HID", - }; - } - return className(class); + else => "HID boot device", + } else "HID", + .mass_storage => switch (@as(mass_storage.Protocol, @enumFromInt(protocol))) { + .bulk_only => "Mass Storage (Bulk-Only)", + .uas => "Mass Storage (UAS)", + else => "Mass Storage", + }, + .hub => switch (@as(hub.Protocol, @enumFromInt(protocol))) { + .super_speed => "Hub (SuperSpeed)", + .hi_speed_multi_tt => "Hub (Hi-Speed multi-TT)", + .hi_speed_single_tt => "Hub (Hi-Speed single-TT)", + else => "Hub", + }, + .wireless_controller => if (subclass == @intFromEnum(wireless_controller.SubClass.radio_frequency)) + wireless_controller.protocolName(protocol) + else + "Wireless Controller", + .communications => communications.subclassName(subclass), + .application_specific => application_specific.subclassName(subclass), + .miscellaneous => "Miscellaneous", + else => className(class), + }; } // Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the @@ -163,6 +182,16 @@ pub const hub = struct { super_speed = 0x03, _, }; + + pub fn protocolName(protocol: u8) []const u8 { + return switch (@as(Protocol, @enumFromInt(protocol))) { + .full_speed => "full-speed", + .hi_speed_single_tt => "Hi-Speed single-TT", + .hi_speed_multi_tt => "Hi-Speed multi-TT", + .super_speed => "SuperSpeed", + _ => "unknown", + }; + } }; // Subclass and protocol codes qualified by Class.hid. @@ -183,6 +212,23 @@ pub const hid = struct { mouse = 0x02, _, }; + + pub fn subclassName(subclass: u8) []const u8 { + return switch (@as(SubClass, @enumFromInt(subclass))) { + .none => "none", + .boot => "boot", + _ => "unknown", + }; + } + + pub fn protocolName(protocol: u8) []const u8 { + return switch (@as(Protocol, @enumFromInt(protocol))) { + .none => "none", + .keyboard => "keyboard", + .mouse => "mouse", + _ => "unknown", + }; + } }; // Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the @@ -226,6 +272,33 @@ pub const mass_storage = struct { vendor_specific = 0xFF, _, }; + + pub fn subclassName(subclass: u8) []const u8 { + return switch (@as(SubClass, @enumFromInt(subclass))) { + .not_reported => "SCSI (not reported)", + .rbc => "RBC", + .atapi => "ATAPI", + .qic_157 => "QIC-157", + .ufi => "UFI", + .sff_8070i => "SFF-8070i", + .scsi => "SCSI", + .lsd_fs => "LSD FS", + .ieee_1667 => "IEEE 1667", + .vendor_specific => "vendor-specific", + _ => "unknown", + }; + } + + pub fn protocolName(protocol: u8) []const u8 { + return switch (@as(Protocol, @enumFromInt(protocol))) { + .cbi_completion_interrupt => "CBI", + .cbi => "CBI (no completion IRQ)", + .bulk_only => "Bulk-Only", + .uas => "UAS", + .vendor_specific => "vendor-specific", + _ => "unknown", + }; + } }; // Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes @@ -261,6 +334,25 @@ pub const communications = struct { network_control = 0x0D, _, }; + + pub fn subclassName(subclass: u8) []const u8 { + return switch (@as(SubClass, @enumFromInt(subclass))) { + .direct_line => "Direct Line", + .abstract_control => "Abstract Control (modem/serial)", + .telephone => "Telephone", + .multi_channel => "Multi-Channel", + .capi => "CAPI", + .ethernet => "Ethernet", + .atm => "ATM", + .wireless_handset => "Wireless Handset", + .device_management => "Device Management", + .mobile_direct_line => "Mobile Direct Line", + .obex => "OBEX", + .ethernet_emulation => "Ethernet Emulation", + .network_control => "Network Control", + _ => "unknown", + }; + } }; // Subclass and protocol codes qualified by Class.wireless_controller. @@ -283,6 +375,23 @@ pub const wireless_controller = struct { bluetooth_amp = 0x04, _, }; + + pub fn subclassName(subclass: u8) []const u8 { + return switch (@as(SubClass, @enumFromInt(subclass))) { + .radio_frequency => "RF", + _ => "unknown", + }; + } + + pub fn protocolName(protocol: u8) []const u8 { + return switch (@as(Protocol, @enumFromInt(protocol))) { + .bluetooth => "Bluetooth", + .ultra_wideband => "Ultra-Wideband", + .remote_ndis => "Remote NDIS", + .bluetooth_amp => "Bluetooth AMP", + _ => "unknown", + }; + } }; // Subclass and protocol codes qualified by Class.miscellaneous. @@ -300,6 +409,20 @@ pub const miscellaneous = struct { interface_association = 0x01, _, }; + + pub fn subclassName(subclass: u8) []const u8 { + return switch (@as(SubClass, @enumFromInt(subclass))) { + .common => "common", + _ => "unknown", + }; + } + + pub fn protocolName(protocol: u8) []const u8 { + return switch (@as(Protocol, @enumFromInt(protocol))) { + .interface_association => "Interface Association", + _ => "unknown", + }; + } }; // Subclass and protocol codes qualified by Class.application_specific. @@ -313,6 +436,15 @@ pub const application_specific = struct { test_and_measurement = 0x03, _, }; + + pub fn subclassName(subclass: u8) []const u8 { + return switch (@as(SubClass, @enumFromInt(subclass))) { + .firmware_upgrade => "Device Firmware Upgrade", + .irda_bridge => "IrDA Bridge", + .test_and_measurement => "Test & Measurement", + _ => "unknown", + }; + } }; /// Pack a (class, subclass, protocol) triple into one 0xCCSSPP value — the @@ -359,6 +491,30 @@ test "class codes match the USB-IF assignments" { _ = application_specific.SubClass.firmware_upgrade; } +test "readable names decode the well-known triples" { + const std = @import("std"); + const eql = std.testing.expectEqualStrings; + + try eql("Hub", className(0x09)); + try eql("Unknown", className(0x42)); + + // interfaceName decodes the combos we log. + try eql("HID boot keyboard", interfaceName(0x03, 0x01, 0x01)); + try eql("HID boot mouse", interfaceName(0x03, 0x01, 0x02)); + try eql("Mass Storage (Bulk-Only)", interfaceName(0x08, 0x06, 0x50)); + try eql("Hub (SuperSpeed)", interfaceName(0x09, 0x00, 0x03)); + try eql("Bluetooth", interfaceName(0xE0, 0x01, 0x01)); + + // The per-enum name functions. + try eql("Bulk-Only", mass_storage.protocolName(0x50)); + try eql("SCSI", mass_storage.subclassName(0x06)); + try eql("Bluetooth", wireless_controller.protocolName(0x01)); + try eql("SuperSpeed", hub.protocolName(0x03)); + try eql("keyboard", hid.protocolName(0x01)); + try eql("SuperSpeed", speedName(4)); + try eql("Polling", linkStateName(7)); +} + test "packTriple / unpackTriple round-trip the identity a bus driver reports" { const std = @import("std"); const expectEqual = std.testing.expectEqual;