9 Commits
Author SHA1 Message Date
Daniel Samson ded555961c Merge usb-superspeed-hub: hubs work on real hardware (M22)
USB hub support, root-caused and validated on the user's real machine:
a Keychron keyboard and ROG mouse now enumerate through a Genesys
compound USB3 hub (its USB2 companion + transaction translators) and
type on danos.

Fixes found by reading logs off the stick, each invisible to QEMU:
- SuperSpeed hubs have change bits a USB2 hub lacks; not clearing them
  spun the driver. Clear them (gated on speed) + a per-tick service cap.
- The boot scan runs ~3 ms after the controller reset, too early for a
  USB2 connection to debounce; the SuperSpeed devices train instantly
  and hid the problem. Poll the root ports on the tick (edge-triggered).
- Full-speed devices failed Address Device with a USB Transaction Error:
  USB 2.0 requires a 10 ms reset-recovery interval before SET_ADDRESS.
  Wait it out, and retry Address Device on a transaction error.

Plus richer logs: readable PORTSC decode, USB link-state and speed
names, device string descriptors (maker/product), and a readable name
for every class/subclass/protocol enum in usb-ids. Full suite 92/92.

Open follow-up: 'port 11 enumeration failed' — a third device that did
not come up; to be investigated separately.
2026-07-21 23:48:29 +01:00
Daniel Samson 802d51ba74 usb-ids: human-readable name for every class/subclass/protocol enum
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.
2026-07-21 23:43:01 +01:00
Daniel Samson 570f6f545c 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.
2026-07-21 23:33:12 +01:00
Daniel Samson da5f404041 usb: post-reset recovery delay + Address Device retry on transaction error
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.
2026-07-21 23:25:19 +01:00
Daniel Samson ab732dc455 usb: readable PORTSC decode, link-state names, and device string descriptors
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: <class>
"<maker> <product>"' format). Branch diagnostics for the SuperSpeed
compound-hub investigation.
2026-07-21 23:19:18 +01:00
Daniel Samson 4ea4a040d2 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: <name>' format).
2026-07-21 23:15:16 +01:00
Daniel Samson 0b10a637ae usb: poll root ports on the tick — catch a late USB2 connection (edge-triggered)
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.
2026-07-21 23:06:49 +01:00
Daniel Samson 10956c6660 usb: [diagnostic] dump the xECP USB2/USB3 port map + all PORTSC
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.
2026-07-21 22:56:25 +01:00
Daniel Samson 18408b0666 usb: [diagnostic] dump all root-port PORTSC + log port-change events
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.
2026-07-21 22:50:11 +01:00
4 changed files with 390 additions and 20 deletions
+235
View File
@@ -70,6 +70,104 @@ 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",
};
}
/// 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",
};
}
/// 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 {
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 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 // Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the
// protocol distinguishes the hub's transaction-translator arrangement. // protocol distinguishes the hub's transaction-translator arrangement.
pub const hub = struct { pub const hub = struct {
@@ -84,6 +182,16 @@ pub const hub = struct {
super_speed = 0x03, 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. // Subclass and protocol codes qualified by Class.hid.
@@ -104,6 +212,23 @@ pub const hid = struct {
mouse = 0x02, 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 // Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the
@@ -147,6 +272,33 @@ pub const mass_storage = struct {
vendor_specific = 0xFF, 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 // Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes
@@ -182,6 +334,25 @@ pub const communications = struct {
network_control = 0x0D, 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. // Subclass and protocol codes qualified by Class.wireless_controller.
@@ -204,6 +375,23 @@ pub const wireless_controller = struct {
bluetooth_amp = 0x04, 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. // Subclass and protocol codes qualified by Class.miscellaneous.
@@ -221,6 +409,20 @@ pub const miscellaneous = struct {
interface_association = 0x01, 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. // Subclass and protocol codes qualified by Class.application_specific.
@@ -234,6 +436,15 @@ pub const application_specific = struct {
test_and_measurement = 0x03, 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 /// Pack a (class, subclass, protocol) triple into one 0xCCSSPP value — the
@@ -280,6 +491,30 @@ test "class codes match the USB-IF assignments" {
_ = application_specific.SubClass.firmware_upgrade; _ = 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" { test "packTriple / unpackTriple round-trip the identity a bus driver reports" {
const std = @import("std"); const std = @import("std");
const expectEqual = std.testing.expectEqual; const expectEqual = std.testing.expectEqual;
+55 -6
View File
@@ -189,6 +189,10 @@ fn speedName(speed: u32) []const u8 {
/// class driver against. /// class driver against.
var manager_handle: ?runtime.ipc.Handle = null; 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 { fn scanPorts(manager: runtime.ipc.Handle) void {
const engine = if (controller) |*c| c else { const engine = if (controller) |*c| c else {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n"); _ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n");
@@ -200,10 +204,14 @@ fn scanPorts(manager: runtime.ipc.Handle) void {
var connected: u32 = 0; var connected: u32 = 0;
while (port <= engine.max_ports) : (port += 1) { while (port <= engine.max_ports) : (port += 1) {
if (!engine.portConnected(port)) continue; if (!engine.portConnected(port)) continue;
if (port < prev_connected.len) prev_connected[port] = true; // don't re-fire the poll for these
connected += 1; connected += 1;
bringUpPort(manager, engine, port); 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 /// Bring up whatever is on `port`: setup + enumerate + register/report one child
@@ -221,8 +229,15 @@ fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u
std.log.info("port {d} enumeration failed", .{port}); std.log.info("port {d} enumeration failed", .{port});
return; return;
} }
std.log.info("port {d} device 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, port,
usb_ids.className(usb_device.device_descriptor.device_class),
maker,
product,
usb_device.device_descriptor.vendor_id, usb_device.device_descriptor.vendor_id,
usb_device.device_descriptor.product_id, usb_device.device_descriptor.product_id,
usb_device.interface_count, usb_device.interface_count,
@@ -256,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 { fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void {
const status = engine.hubPortStatusAck(hub, port) orelse return; const status = engine.hubPortStatusAck(hub, port) orelse return;
const connected = library.Controller.hubPortConnected(status); 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); 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 (!connected) {
if (existing) |dev| tearDownHubDevice(manager, engine, dev); if (existing) |dev| tearDownHubDevice(manager, engine, dev);
@@ -270,8 +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 }); std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port });
return; return;
} }
std.log.info("hub slot {d} port {d} device 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, 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.vendor_id,
usb_device.device_descriptor.product_id, usb_device.device_descriptor.product_id,
usb_device.interface_count, usb_device.interface_count,
@@ -378,9 +401,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 }); std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number });
return null; 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, port,
interface.number, interface.number,
usb_ids.interfaceName(interface.class, interface.subclass, interface.protocol),
interface.class, interface.class,
interface.subclass, interface.subclass,
interface.protocol, interface.protocol,
@@ -491,9 +515,34 @@ fn onNotification(badge: u64) void {
if (badge & runtime.ipc.notify_timer_bit == 0) return; if (badge & runtime.ipc.notify_timer_bit == 0) return;
if (controller) |*engine| { if (controller) |*engine| {
engine.pump(); 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| { while (engine.takePortChange()) |port| {
const manager = manager_handle orelse break; 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); if (engine.deviceOnPort(port) == null) bringUpPort(manager, engine, port);
} else { } else {
tearDownPort(manager, engine, port); tearDownPort(manager, engine, port);
@@ -22,6 +22,7 @@ const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const mmio = @import("mmio"); const mmio = @import("mmio");
const usb_abi = @import("usb-abi"); const usb_abi = @import("usb-abi");
const usb_ids = @import("usb-ids");
const dma = runtime.dma; const dma = runtime.dma;
const system = runtime.system; const system = runtime.system;
@@ -100,6 +101,7 @@ pub const TrbType = enum(u6) {
pub const CompletionCode = enum(u8) { pub const CompletionCode = enum(u8) {
invalid = 0, invalid = 0,
success = 1, success = 1,
usb_transaction_error = 4,
short_packet = 13, short_packet = 13,
_, _,
}; };
@@ -484,6 +486,73 @@ pub const Controller = struct {
} }
// PORTSC for 1-based port `port`. // 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}: {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 { pub fn portStatus(self: *const Controller, port: u32) u32 {
return read32(self.op_base + op_portsc_base + op_portsc_stride * (port - 1)); return read32(self.op_base + op_portsc_base + op_portsc_stride * (port - 1));
} }
@@ -768,19 +837,31 @@ pub const Controller = struct {
} }
fn addressDeviceCommand(self: *Controller, device: *Device) bool { fn addressDeviceCommand(self: *Controller, device: *Device) bool {
// 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(.{ const physical = self.submitCommand(.{
.parameter = device.input_context.physical, .parameter = device.input_context.physical,
.control = trbControl(.address_device, @as(u32, device.slot_id) << 24), .control = trbControl(.address_device, @as(u32, device.slot_id) << 24),
}); });
const code = self.awaitCommand(physical) orelse { const code = self.awaitCommand(physical) orelse {
std.log.info("port {d} setup: Address Device timed out", .{device.port}); std.log.info("port {d} setup: Address Device timed out (attempt {d})", .{ device.port, attempt + 1 });
return false; return false;
}; };
if (code != @intFromEnum(CompletionCode.success)) { if (code == @intFromEnum(CompletionCode.success)) return true;
std.log.info("port {d} setup: Address Device completion code {d}", .{ device.port, code }); std.log.info("port {d} setup: Address Device completion code {d} (attempt {d})", .{ device.port, code, attempt + 1 });
return false; 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 /// Reset the port, enable a slot, and address the device on it: after this the
@@ -806,6 +887,11 @@ pub const Controller = struct {
// (pre-reset reads misreport on real controllers; M20). // (pre-reset reads misreport on real controllers; M20).
effective_speed = (self.portStatus(port) >> 10) & 0xF; effective_speed = (self.portStatus(port) >> 10) & 0xF;
if (effective_speed == 0) effective_speed = speed; // defensive: keep the caller's read 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 { const slot_id = self.enableSlot() orelse {
std.log.info("port {d} setup: Enable Slot failed", .{port}); std.log.info("port {d} setup: Enable Slot failed", .{port});
+2 -2
View File
@@ -494,7 +494,7 @@ CASES = [
# B4a: the hub powers its ports. B4b: the keyboard behind it enumerates # B4a: the hub powers its ports. B4b: the keyboard behind it enumerates
# (route string + TT) and binds usb-hid-keyboard. # (route string + TT) and binds usb-hid-keyboard.
"expect": r"(?s)(?=.*hub slot \d+: \d+ downstream ports powered)" "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)", r"(?=.*usb-hid-keyboard: ok \(device 3)",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# USB mass storage end to end: the boot usb-storage device (the FAT32 image, # 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",
"-device", "usb-hub,bus=xhci2.0,port=1.1", "-device", "usb-hub,bus=xhci2.0,port=1.1",
"-device", "usb-kbd,bus=xhci2.0,port=1.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)", r"(?=.*usb-hid-keyboard: ok \(device 3)",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# Hub-downstream disconnect (B4c): device_del the keyboard behind the hub; # Hub-downstream disconnect (B4c): device_del the keyboard behind the hub;