usb: don't reset an enabled SuperSpeed port; name every setup failure

Real-PC diagnose boot (photo + OCR): the boot stick connects at
SuperSpeed on port 21 and 'device setup failed' lands in the SAME
millisecond — an instant failure, not a timeout. setupDevice reset
every port unconditionally; that is required to enable USB2 ports, but
a SuperSpeed port that trained its link is ALREADY enabled (xHCI
advances USB3 ports to Enabled, no reset — spec 4.3), and driving a hot
reset into the live link drops PED mid-reset on real silicon. QEMU
tolerates the spurious reset, which is why the harness never saw it.

An enabled speed>=4 port now skips the reset (a not-yet-enabled SS link
still gets one). Every setup step names its failure — port reset with
the PORTSC value, Enable Slot, device-slot exhaustion, Address Device
with its completion code — so the on-screen transcript of the next
failure identifies the exact xHCI command instead of one blanket line.
block.open's give-up window drops 60 s -> 30 s (the slowest observed
healthy chain completed at ~24 s); a machine whose stick failed setup
should not sit a further minute pretending otherwise.
This commit is contained in:
Daniel Samson
2026-07-21 19:37:24 +01:00
parent f587e7e05e
commit 59ba95a315
2 changed files with 33 additions and 6 deletions
+4 -1
View File
@@ -61,7 +61,10 @@ pub fn open() ?Device {
// enumeration, mass-storage bring-up) must complete first, which can take
// tens of seconds under emulation.
var attempts: usize = 0;
while (attempts < 1200) : (attempts += 1) {
// 30 s covers the slowest observed healthy chain (a flaky QEMU enumeration
// completed at ~24 s); a machine whose stick genuinely failed setup should
// not sit a further minute pretending otherwise.
while (attempts < 600) : (attempts += 1) {
if (ipc.lookup(.block)) |handle| return .{ .endpoint = handle };
system.sleep(50);
}
@@ -620,8 +620,15 @@ pub const Controller = struct {
.parameter = device.input_context.physical,
.control = trbControl(.address_device, @as(u32, device.slot_id) << 24),
});
const code = self.awaitCommand(physical) orelse return false;
return code == @intFromEnum(CompletionCode.success);
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;
}
return true;
}
/// Reset the port, enable a slot, and address the device on it: after this the
@@ -629,9 +636,25 @@ pub const Controller = struct {
/// or null on any failure. The EP0 MPS is taken from the speed default and
/// corrected from the device descriptor by `refreshMaxPacketSize0` if needed.
pub fn setupDevice(self: *Controller, port: u32, speed: u32) ?*Device {
if (!self.resetPort(port)) return null;
const slot_id = self.enableSlot() orelse return null;
const device = self.allocateDevice() orelse return null;
// A SuperSpeed port that has trained its link is ALREADY enabled — the
// xHCI advances USB3 ports to Enabled with no reset (spec 4.3). Driving
// a hot reset into a live SS link drops PED mid-reset on real silicon
// (observed: "setup failed" in the same millisecond as "connected").
// Only a not-yet-enabled port — every USB2 device, or a stuck SS link —
// needs the reset to enable.
const already_enabled = speed >= 4 and self.portStatus(port) & portsc_enabled != 0;
if (!already_enabled and !self.resetPort(port)) {
std.log.info("port {d} setup: port reset failed (PORTSC 0x{x:0>8})", .{ port, self.portStatus(port) });
return null;
}
const slot_id = self.enableSlot() orelse {
std.log.info("port {d} setup: Enable Slot failed", .{port});
return null;
};
const device = self.allocateDevice() orelse {
std.log.info("port {d} setup: no free device slot", .{port});
return null;
};
device.* = .{
.used = true,
.slot_id = slot_id,
@@ -652,6 +675,7 @@ pub const Controller = struct {
return device;
}
fn abandon(self: *Controller, device: *Device) ?*Device {
_ = self;
device.used = false;