usb: correct EP0's max packet size from the device; sample speed after reset

Two real-hardware correctness fixes from the M20 list, both invisible
to QEMU's forgiving controller:

refreshMaxPacketSize0 existed only as a comment. The EP0 context kept
the SPEED-DEFAULT max packet size (full-speed: 8) even when the device
declares 16/32/64 — real controllers fault the very first full
descriptor read on the mismatch, which is the standing suspect for
full-speed mice dying on the user's PC. Enumeration now probes the
device descriptor's first 8 bytes (deliverable at any legal MPS0),
and issues Evaluate Context to correct EP0 before any longer transfer,
naming the failure and codes if the controller refuses.

And a USB2 port's PORTSC speed field is only meaningful once the port
reset ENABLES the port: the speed (and the MPS0 default derived from
it) is now sampled after the reset instead of trusting the connect-time
read.
This commit is contained in:
Daniel Samson
2026-07-21 20:35:36 +01:00
parent 1638845a4b
commit d8cf533b73
@@ -643,10 +643,18 @@ pub const Controller = struct {
// Only a not-yet-enabled port — every USB2 device, or a stuck SS link — // Only a not-yet-enabled port — every USB2 device, or a stuck SS link —
// needs the reset to enable. // needs the reset to enable.
const already_enabled = speed >= 4 and self.portStatus(port) & portsc_enabled != 0; const already_enabled = speed >= 4 and self.portStatus(port) & portsc_enabled != 0;
if (!already_enabled and !self.resetPort(port)) { var effective_speed = speed;
if (!already_enabled) {
if (!self.resetPort(port)) {
std.log.info("port {d} setup: port reset failed (PORTSC 0x{x:0>8})", .{ port, self.portStatus(port) }); std.log.info("port {d} setup: port reset failed (PORTSC 0x{x:0>8})", .{ port, self.portStatus(port) });
return null; return null;
} }
// A USB2 port's PORTSC speed field is only meaningful once the port
// is enabled by the reset — sample it NOW, not at connect time
// (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
}
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});
return null; return null;
@@ -659,8 +667,8 @@ pub const Controller = struct {
.used = true, .used = true,
.slot_id = slot_id, .slot_id = slot_id,
.port = port, .port = port,
.speed = speed, .speed = effective_speed,
.max_packet_size_0 = defaultMaxPacketSize0(speed), .max_packet_size_0 = defaultMaxPacketSize0(effective_speed),
}; };
device.input_context = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device); device.input_context = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device);
device.device_context = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device); device.device_context = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device);
@@ -768,7 +776,47 @@ pub const Controller = struct {
/// endpoints into `device`, and select the configuration. After this the /// endpoints into `device`, and select the configuration. After this the
/// device is in the configured state and its interfaces are ready to match a /// device is in the configured state and its interfaces are ready to match a
/// class driver. Returns false on any control-transfer failure. /// class driver. Returns false on any control-transfer failure.
/// Correct EP0's max packet size from the device itself. The context starts
/// with the SPEED-DEFAULT (full-speed: 8, but the true value may be 8/16/32/
/// 64 — byte 7 of the device descriptor). Read just the descriptor's first
/// 8 bytes (always deliverable at any legal MPS0), and when the device
/// disagrees with the context, issue Evaluate Context to fix EP0 before any
/// longer transfer. Real controllers fault the full 18-byte read on a wrong
/// MPS0; QEMU forgives it — the classic full-speed-mouse-on-real-hardware
/// failure (M20).
fn refreshMaxPacketSize0(self: *Controller, device: *Device) bool {
var head: [8]u8 = undefined;
const request = usb_abi.getDescriptor(.device, 0, 0, 8);
if (!self.controlTransfer(device, request, head[0..], true)) return false;
const actual: u32 = head[7];
if (actual == 0 or actual == device.max_packet_size_0) return true;
// Input Control Context: add-flag A1 (EP0 only); EP0 context rebuilt
// with the corrected MPS. Fields not being changed stay zero (the
// controller evaluates only the added context).
const cs = self.context_size;
const base = device.input_context.virtual;
@memset(@as([*]u8, @ptrFromInt(base))[0 .. 3 * cs], 0);
contextDword(base, 0, 1, cs).* = 0b10; // A1
contextDword(base, 2, 1, cs).* = (@as(u32, 3) << 1) | (@as(u32, 4) << 3) | (actual << 16);
const physical = self.submitCommand(.{
.parameter = device.input_context.physical,
.control = trbControl(.evaluate_context, @as(u32, device.slot_id) << 24),
});
const code = self.awaitCommand(physical) orelse {
std.log.info("slot {d}: Evaluate Context (MPS0 {d} -> {d}) timed out", .{ device.slot_id, device.max_packet_size_0, actual });
return false;
};
if (code != @intFromEnum(CompletionCode.success)) {
std.log.info("slot {d}: Evaluate Context (MPS0 {d} -> {d}) completion code {d}", .{ device.slot_id, device.max_packet_size_0, actual, code });
return false;
}
device.max_packet_size_0 = actual;
return true;
}
pub fn enumerate(self: *Controller, device: *Device) bool { pub fn enumerate(self: *Controller, device: *Device) bool {
if (!self.refreshMaxPacketSize0(device)) return false;
device.device_descriptor = self.getDeviceDescriptor(device) orelse return false; device.device_descriptor = self.getDeviceDescriptor(device) orelse return false;
// The configuration descriptor's own 9 bytes carry the total length of // The configuration descriptor's own 9 bytes carry the total length of