From d8cf533b739c0a4340b30b5a4b749abfa2f521f8 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:35:36 +0100 Subject: [PATCH] usb: correct EP0's max packet size from the device; sample speed after reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../drivers/usb-xhci-bus/usb-xhci-library.zig | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index b889fd9..fec7895 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -643,9 +643,17 @@ pub const Controller = struct { // 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; + 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) }); + 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 { std.log.info("port {d} setup: Enable Slot failed", .{port}); @@ -659,8 +667,8 @@ pub const Controller = struct { .used = true, .slot_id = slot_id, .port = port, - .speed = speed, - .max_packet_size_0 = defaultMaxPacketSize0(speed), + .speed = effective_speed, + .max_packet_size_0 = defaultMaxPacketSize0(effective_speed), }; 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); @@ -768,7 +776,47 @@ pub const Controller = struct { /// endpoints into `device`, and select the configuration. After this the /// device is in the configured state and its interfaces are ready to match a /// 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 { + if (!self.refreshMaxPacketSize0(device)) return false; device.device_descriptor = self.getDeviceDescriptor(device) orelse return false; // The configuration descriptor's own 9 bytes carry the total length of