From 3fb9d5936a50ba62c822f35aec90ecdf3c652fef Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:11:00 +0100 Subject: [PATCH] USB driver stack: xHCI transfers, HID keyboard/mouse, mass storage Flesh out the xHCI host-controller driver into a full transfer engine and build the three USB class drivers on top, all verified end to end under QEMU. - xHCI engine (usb-xhci-library.zig): controller reset, command/event rings with cycle-bit bookkeeping (gated on a No-Op-command proof), device slots, Address Device, control transfers, full chapter-9 enumeration, Configure Endpoint, and interrupt/bulk transfers. Each interface is device_registered with its (class,subclass,protocol) identity, unique per (port,interface). - Bus<->class transfer protocol (usb-transfer-protocol.zig + runtime.usb): open / control / interrupt-subscribe (async report pump on a poll timer) / bulk-by- physical-address, so sector data never crosses the 256-byte IPC limit. - USB HID keyboard + mouse (usb-hid/): decode boot-protocol reports and publish to the input service. A USB usage is already the input protocol's keycode. - USB mass storage (usb-storage/): Bulk-Only Transport + transparent SCSI, serving a block device under the new .block service id (block-protocol). - device-manager matches USB interfaces to class drivers (usbDriverForIdentity). - usb-abi / usb-ids made importable modules; add HID and mass-storage class requests, packTriple, and a usb_device DeviceClass. - Fix test/qemu_test.py on macOS: the QMP unix-socket path was built from the deep worktree path and exceeded the 104-byte sun_path limit, so QEMU exited before booting. It now lives under a short temp path. Tests: usb-report, usb-hid, usb-storage pass under python3 test/qemu_test.py; host units (usb-abi, usb-ids, hid-report, bulk-only-transport, scsi) green. --- build.zig | 57 + library/runtime/runtime.zig | 4 + library/runtime/usb.zig | 162 +++ system/abi.zig | 2 + system/devices/device-abi.zig | 5 + system/devices/usb-abi.zig | 190 +++- system/devices/usb-ids.zig | 81 +- system/drivers/usb-hid/hid-report.zig | 191 ++++ system/drivers/usb-hid/keyboard.zig | 174 +++ system/drivers/usb-hid/mouse.zig | 140 +++ .../usb-storage/bulk-only-transport.zig | 73 ++ system/drivers/usb-storage/scsi.zig | 86 ++ system/drivers/usb-storage/usb-storage.zig | 179 +++ .../usb-xhci-bus/usb-transfer-protocol.zig | 159 +++ system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 303 ++++- .../drivers/usb-xhci-bus/usb-xhci-library.zig | 1004 +++++++++++++++++ system/kernel/tests.zig | 38 + system/parameters.zig | 9 +- system/services/block/protocol.zig | 40 + .../device-manager/device-manager.zig | 36 + test/qemu_test.py | 33 +- 21 files changed, 2856 insertions(+), 110 deletions(-) create mode 100644 library/runtime/usb.zig create mode 100644 system/drivers/usb-hid/hid-report.zig create mode 100644 system/drivers/usb-hid/keyboard.zig create mode 100644 system/drivers/usb-hid/mouse.zig create mode 100644 system/drivers/usb-storage/bulk-only-transport.zig create mode 100644 system/drivers/usb-storage/scsi.zig create mode 100644 system/drivers/usb-storage/usb-storage.zig create mode 100644 system/drivers/usb-xhci-bus/usb-transfer-protocol.zig create mode 100644 system/services/block/protocol.zig diff --git a/build.zig b/build.zig index 63fec3f..a478b0d 100644 --- a/build.zig +++ b/build.zig @@ -142,6 +142,28 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("system/devices/acpi-ids.zig"), }); + // The USB device-framework wire ABI (chapter-9 set-up packets, standard + + // class requests, descriptors) and the USB class-code taxonomy — the flat + // reference the xHCI bus driver, the USB class drivers, and the device + // manager's identity matcher all share. Pure data, like pci-class/acpi-ids. + const usb_abi_module = b.addModule("usb-abi", .{ + .root_source_file = b.path("system/devices/usb-abi.zig"), + }); + const usb_ids_module = b.addModule("usb-ids", .{ + .root_source_file = b.path("system/devices/usb-ids.zig"), + }); + // The USB transfer protocol: what a USB class driver says to the xHCI bus + // driver to drive its device (open / control / interrupt / bulk). A protocol + // module like vfs-protocol, shared by the bus driver and every class driver. + const usb_transfer_protocol_module = b.addModule("usb-transfer-protocol", .{ + .root_source_file = b.path("system/drivers/usb-xhci-bus/usb-transfer-protocol.zig"), + }); + // The block-device protocol: read/write of fixed-size blocks, spoken between a + // filesystem and a block driver (usb-storage). A protocol module like the rest. + const block_protocol_module = b.addModule("block-protocol", .{ + .root_source_file = b.path("system/services/block/protocol.zig"), + }); + // Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of // compile-time constants, imported wherever a knob is read; keeps the trade-offs // in one place instead of scattered across the tree. See system/parameters.zig. @@ -225,6 +247,9 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("system/services/device-manager/device-manager-protocol.zig"), }); runtime_module.addImport("device-manager-protocol", device_manager_protocol_module); + // The USB transfer protocol, so runtime.usb (the class-driver client) can speak + // it, the way runtime.input speaks the input protocol. + runtime_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module); // The power protocol: system power's domain-named surface (docs/power.md). const power_protocol_module = b.addModule("power-protocol", .{ @@ -350,6 +375,23 @@ pub fn build(b: *std.Build) void { const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig"); const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig"); + // The xHCI bus driver builds chapter-9 requests and decodes descriptors from + // usb-abi, and reports each interface's (class,subclass,protocol) identity via + // usb-ids.packTriple. + usb_xhci_bus_exe.root_module.addImport("usb-abi", usb_abi_module); + usb_xhci_bus_exe.root_module.addImport("usb-ids", usb_ids_module); + usb_xhci_bus_exe.root_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module); + // The USB HID class drivers: keyboard and mouse. They own no hardware — each + // opens its device through runtime.usb (the transfer protocol) and publishes to + // the input service. They build chapter-9 class requests from usb-abi. + const usb_hid_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-keyboard", "system/drivers/usb-hid/keyboard.zig"); + usb_hid_keyboard_exe.root_module.addImport("usb-abi", usb_abi_module); + const usb_hid_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-mouse", "system/drivers/usb-hid/mouse.zig"); + usb_hid_mouse_exe.root_module.addImport("usb-abi", usb_abi_module); + // The USB mass-storage class driver: opens its device via runtime.usb, drives it + // with Bulk-Only Transport + SCSI, and serves the block protocol under `.block`. + const usb_storage_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-storage", "system/drivers/usb-storage/usb-storage.zig"); + usb_storage_exe.root_module.addImport("block-protocol", block_protocol_module); const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig"); // The PCI bus driver decodes each function's class triple to human names in its // boot log (class/subclass/prog-IF), so pull in the shared pci-class reference. @@ -376,6 +418,9 @@ pub fn build(b: *std.Build) void { const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); // Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330. device_manager_exe.root_module.addImport("pci-class", pci_class_module); + // The manager matches reported USB interfaces by their (class,subclass,protocol) + // triple (usbDriverForIdentity), built from the named usb-ids codes. + device_manager_exe.root_module.addImport("usb-ids", usb_ids_module); // The input service and its exercisers: the fan-out server, a hardware-free synthetic // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig"); @@ -402,6 +447,12 @@ pub fn build(b: *std.Build) void { mk_run.addFileArg(ps2_mouse_exe.getEmittedBin()); mk_run.addArg("usb-xhci-bus"); mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin()); + mk_run.addArg("usb-hid-keyboard"); + mk_run.addFileArg(usb_hid_keyboard_exe.getEmittedBin()); + mk_run.addArg("usb-hid-mouse"); + mk_run.addFileArg(usb_hid_mouse_exe.getEmittedBin()); + mk_run.addArg("usb-storage"); + mk_run.addFileArg(usb_storage_exe.getEmittedBin()); mk_run.addArg("pci-bus"); mk_run.addFileArg(pci_bus_exe.getEmittedBin()); mk_run.addArg("crash-test"); @@ -433,6 +484,9 @@ pub fn build(b: *std.Build) void { .{ ps2_keyboard_exe, "system/drivers" }, .{ ps2_mouse_exe, "system/drivers" }, .{ usb_xhci_bus_exe, "system/drivers" }, + .{ usb_hid_keyboard_exe, "system/drivers" }, + .{ usb_hid_mouse_exe, "system/drivers" }, + .{ usb_storage_exe, "system/drivers" }, }) |entry| { const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } }); b.getInstallStep().dependOn(&step.step); @@ -581,6 +635,9 @@ pub fn build(b: *std.Build) void { "library/mmio/mmio.zig", // barriers assemble + registers round-trip "system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine "system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly + "system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode + "system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes + "system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian) }) |root| { const mod_tests = b.addTest(.{ .root_module = b.createModule(.{ diff --git a/library/runtime/runtime.zig b/library/runtime/runtime.zig index 9cca735..1dc5fb0 100644 --- a/library/runtime/runtime.zig +++ b/library/runtime/runtime.zig @@ -38,6 +38,10 @@ pub const device = @import("device.zig"); /// DMA-capable memory for drivers: contiguous, pinned, uncacheable buffers. pub const dma = @import("dma.zig"); +/// USB class-driver client: open a device on the xHCI bus and drive it +/// (control / interrupt / bulk transfers). See library/runtime/usb.zig. +pub const usb = @import("usb.zig"); + /// Re-exported so a user binary can `pub const panic = runtime.panic;`. pub const panic = start.panic; diff --git a/library/runtime/usb.zig b/library/runtime/usb.zig new file mode 100644 index 0000000..4eb0788 --- /dev/null +++ b/library/runtime/usb.zig @@ -0,0 +1,162 @@ +//! USB class-driver client: the helper a keyboard, mouse, or mass-storage driver +//! uses to reach its device through the xHCI bus driver, so it never hand-rolls +//! the transfer-protocol IPC. Layered over `ipc` and the shared +//! `usb-transfer-protocol` wire format, the way `input.zig` layers over the input +//! service and `device.zig` over the raw device calls. +//! +//! A class driver, spawned with its interface's assigned device id as argv[1]: +//! if (!usb.helloManager(id)) return; // meet the spawn deadline +//! var device = usb.open(id) orelse return; // open + get its endpoints +//! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors +//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously +//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop +//! +//! Reports are delivered to `device.endpoint` as asynchronous `InterruptReport` +//! messages (the class driver runs a bare `replyWait` loop to read them, because +//! the service harness drops buffered-message payloads — see service.zig). + +const std = @import("std"); +const ipc = @import("ipc.zig"); +const system = @import("system.zig"); +const protocol = @import("usb-transfer-protocol"); +const device_manager = @import("device-manager-protocol"); + +pub const Endpoint = protocol.Endpoint; +pub const InterruptReport = protocol.InterruptReport; +pub const max_report_data = protocol.max_report_data; + +// Endpoint transfer types (EndpointDescriptor attributes), for `findEndpoint`. +pub const transfer_type_bulk: u8 = 2; +pub const transfer_type_interrupt: u8 = 3; + +/// An opened USB device: the bus endpoint to send requests to, this driver's own +/// endpoint that reports arrive on, the device token, and the interface's +/// endpoints (so a driver need not re-read the configuration descriptor). +pub const Device = struct { + bus: ipc.Handle, + endpoint: ipc.Handle, + token: u64, + class: u8, + subclass: u8, + protocol_code: u8, + interface_number: u8, + endpoint_count: usize = 0, + endpoints: [protocol.max_reported_endpoints]Endpoint = undefined, + + /// The interface's first endpoint of the given transfer type and direction + /// (`transfer_type_bulk` / `transfer_type_interrupt`), or null. + pub fn findEndpoint(self: *const Device, transfer_type: u8, direction_in: bool) ?Endpoint { + for (self.endpoints[0..self.endpoint_count]) |endpoint| { + if (endpoint.transfer_type == transfer_type and (endpoint.address & 0x80 != 0) == direction_in) return endpoint; + } + return null; + } + + fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize { + var request = protocol.ControlRequest{ + .device_token = self.token, + .setup = setup, + .direction_in = @intFromBool(direction_in), + .data_length = @intCast(data.len), + }; + if (!direction_in and data.len > 0) @memcpy(request.data[0..data.len], data); + var reply: [@sizeOf(protocol.ControlReply)]u8 = undefined; + const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null; + if (length < @sizeOf(protocol.ControlReply)) return null; + const control_reply = std.mem.bytesToValue(protocol.ControlReply, reply[0..@sizeOf(protocol.ControlReply)]); + if (control_reply.status != 0) return null; + const actual = @min(control_reply.actual_length, data.len); + if (direction_in and actual > 0) @memcpy(data[0..actual], control_reply.data[0..actual]); + return actual; + } + + /// A control transfer with no data stage (SET_PROTOCOL, SET_IDLE, ...). The + /// `setup` is a bit-cast `usb_abi.Request`. + pub fn controlOut(self: *Device, setup: [8]u8) bool { + return self.controlTransfer(setup, false, &.{}) != null; + } + + /// A device-to-host control transfer, returning the bytes read into `out`. + pub fn controlIn(self: *Device, setup: [8]u8, out: []u8) ?usize { + return self.controlTransfer(setup, true, out); + } + + /// Begin periodic IN polling of an interrupt endpoint; reports flow back to + /// `self.endpoint` as asynchronous `InterruptReport` messages. + pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool { + var request = protocol.InterruptSubscribeRequest{ + .device_token = self.token, + .endpoint_address = endpoint_address, + .max_length = max_length, + }; + var reply: [@sizeOf(protocol.InterruptSubscribeReply)]u8 = undefined; + const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return false; + if (length < @sizeOf(protocol.InterruptSubscribeReply)) return false; + return std.mem.bytesToValue(protocol.InterruptSubscribeReply, reply[0..@sizeOf(protocol.InterruptSubscribeReply)]).status == 0; + } + + /// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or + /// from the caller's own DMA buffer at `physical`. Returns the bytes moved. + pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 { + var request = protocol.BulkRequest{ + .device_token = self.token, + .physical_address = physical, + .length = length, + .endpoint_address = endpoint_address, + }; + var reply: [@sizeOf(protocol.BulkReply)]u8 = undefined; + const replied = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null; + if (replied < @sizeOf(protocol.BulkReply)) return null; + const bulk_reply = std.mem.bytesToValue(protocol.BulkReply, reply[0..@sizeOf(protocol.BulkReply)]); + if (bulk_reply.status != 0) return null; + return bulk_reply.actual_length; + } +}; + +/// Look up the USB bus and open the device with the assigned id, handing over a +/// freshly created endpoint for asynchronous interrupt reports. Retries while the +/// bus is still coming up (a class driver races the bus driver at boot). +pub fn open(device_id: u64) ?Device { + var attempts: usize = 0; + const bus = while (attempts < 100) : (attempts += 1) { + if (ipc.lookup(.usb_bus)) |handle| break handle; + system.sleep(20); + } else return null; + + const endpoint = ipc.createIpcEndpoint() orelse return null; + var request = protocol.OpenRequest{ .device_id = device_id }; + var reply: [@sizeOf(protocol.OpenReply)]u8 = undefined; + const result = ipc.callCap(bus, std.mem.asBytes(&request), &reply, endpoint) catch return null; + if (result.len < @sizeOf(protocol.OpenReply)) return null; + const open_reply = std.mem.bytesToValue(protocol.OpenReply, reply[0..@sizeOf(protocol.OpenReply)]); + if (open_reply.status != 0) return null; + + var device = Device{ + .bus = bus, + .endpoint = endpoint, + .token = open_reply.device_token, + .class = open_reply.interface_class, + .subclass = open_reply.interface_subclass, + .protocol_code = open_reply.interface_protocol, + .interface_number = open_reply.interface_number, + .endpoint_count = @min(open_reply.endpoint_count, protocol.max_reported_endpoints), + }; + for (0..device.endpoint_count) |index| device.endpoints[index] = open_reply.endpoints[index]; + return device; +} + +/// Hello the device manager as a class driver (Role.device) so a supervised +/// spawn meets its hello deadline. Retries while the manager comes up. +pub fn helloManager(device_id: u64) bool { + var attempts: usize = 0; + const manager = while (attempts < 100) : (attempts += 1) { + if (ipc.lookup(.device_manager)) |handle| break handle; + system.sleep(20); + } else return false; + + const hello = device_manager.Hello{ .role = @intFromEnum(device_manager.Role.device), .device_id = device_id }; + var reply: [device_manager.message_maximum]u8 = undefined; + const length = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch return false; + if (length < device_manager.reply_size) return false; + return std.mem.bytesToValue(device_manager.HelloReply, reply[0..device_manager.reply_size]).status == 0; +} diff --git a/system/abi.zig b/system/abi.zig index 61f37e5..9c770ab 100644 --- a/system/abi.zig +++ b/system/abi.zig @@ -178,6 +178,8 @@ pub const ServiceId = enum(u32) { ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md) power = 5, // system power: events (button, lid, battery) + shutdown (docs/power.md; domain-named per docs/discovery.md — the acpi service registers it on x86, a PSCI service will on ARM) + usb_bus = 6, // the xHCI host-controller driver's transfer endpoint; USB class drivers look it up and `callCap`-open their device to get a private per-device transfer channel (docs/driver-model.md) + block = 7, // a block-device driver (USB mass storage today): read/write of fixed-size blocks, the storage a filesystem sits on _, }; diff --git a/system/devices/device-abi.zig b/system/devices/device-abi.zig index 4507e38..3eb087a 100644 --- a/system/devices/device-abi.zig +++ b/system/devices/device-abi.zig @@ -33,6 +33,11 @@ pub const DeviceClass = enum(u32) { /// a broad io_port grant for OperationRegion access, and the SCI interrupt. /// The one node whose claimant is trusted to run firmware bytecode. acpi_tables, + /// One interface of a USB device, registered by the xHCI bus driver. It owns + /// no MMIO — it is reached through its controller — so it carries no + /// resources; the (class, subclass, protocol) triple that says what it is + /// travels in the bus report's identity, not here. + usb_device, unknown, }; diff --git a/system/devices/usb-abi.zig b/system/devices/usb-abi.zig index d94bb6c..5c205b7 100644 --- a/system/devices/usb-abi.zig +++ b/system/devices/usb-abi.zig @@ -8,7 +8,7 @@ //! buffer at any offset, and bitmap bytes are packed structs so no caller ever needs a magic //! mask. Class, subclass, and protocol code tables live in usb-ids.zig. -const DeviceState = enum(u8) { +pub const DeviceState = enum(u8) { // Immediately after the USB device is attached to the USB system, it is in this state. // The USB specifications do not define the state of a USB device that is detached from // a USB system. @@ -47,7 +47,7 @@ const DeviceState = enum(u8) { suspended, }; -const RequestCode = enum(u8) { +pub const RequestCode = enum(u8) { get_status = 0, clear_feature = 1, set_feature = 3, @@ -59,10 +59,15 @@ const RequestCode = enum(u8) { get_interface = 10, set_interface = 11, sync_frame = 12, + // Non-exhaustive: class-specific requests (HID, mass storage) reuse this byte + // field with codes from their own class's namespace — see the class-request + // constructors below. Some class codes numerically coincide with a standard + // one; the wire byte is what matters, and the constructors set it explicitly. + _, }; // Direction of an endpoint, from the host's point of view -const EndpointDirection = enum(u1) { +pub const EndpointDirection = enum(u1) { out = 0, in = 1, }; @@ -74,7 +79,7 @@ const EndpointDirection = enum(u1) { // The bus address of a device, assigned by the host with SET_ADDRESS. Addresses are 7 bits // wide. -const DeviceAddress = enum(u7) { +pub const DeviceAddress = enum(u7) { // The default address every device answers at after a reset, until SET_ADDRESS // completes default = 0, @@ -82,7 +87,7 @@ const DeviceAddress = enum(u7) { }; // Identifies a configuration; from ConfigurationDescriptor.configuration_value. -const ConfigurationValue = enum(u8) { +pub const ConfigurationValue = enum(u8) { // Not configured: returned by GET_CONFIGURATION while the device is in the address // state, and passed to SET_CONFIGURATION to return a configured device to the address // state @@ -92,11 +97,11 @@ const ConfigurationValue = enum(u8) { // Identifies an interface within a configuration; from // InterfaceDescriptor.interface_number. -const InterfaceNumber = enum(u8) { _ }; +pub const InterfaceNumber = enum(u8) { _ }; // Selects between the alternate settings of one interface; from // InterfaceDescriptor.alternate_setting. -const AlternateSetting = enum(u8) { +pub const AlternateSetting = enum(u8) { // The default setting of an interface default = 0, _, @@ -104,7 +109,7 @@ const AlternateSetting = enum(u8) { // The number of an endpoint within a device, 4 bits wide. The direction bit carried // alongside it tells the two endpoints sharing a number apart. -const EndpointNumber = enum(u4) { +pub const EndpointNumber = enum(u4) { // Endpoint zero: the default control pipe every device provides default_control = 0, _, @@ -112,7 +117,7 @@ const EndpointNumber = enum(u4) { // Index of a STRING descriptor, stored in descriptors that reference a string and passed to // GET_DESCRIPTOR to read it. -const StringIndex = enum(u8) { +pub const StringIndex = enum(u8) { // The device has no string descriptor for this field none = 0, _, @@ -121,7 +126,7 @@ const StringIndex = enum(u8) { // Characteristics of a device request (the bmRequestType field of a set-up packet). Fields are // declared least-significant first: recipient occupies bits 4...0, kind bits 6...5, and // direction bit 7. -const RequestType = packed struct(u8) { +pub const RequestType = packed struct(u8) { // The recipient of the request (values 4...31 are reserved) recipient: Recipient, // The type of the request @@ -129,27 +134,27 @@ const RequestType = packed struct(u8) { // Data transfer direction. The value of this bit is ignored when length is zero. direction: Direction, - const Recipient = enum(u5) { + pub const Recipient = enum(u5) { device = 0, interface = 1, endpoint = 2, other = 3, }; - const Kind = enum(u2) { + pub const Kind = enum(u2) { standard = 0, class = 1, vendor = 2, reserved = 3, }; - const Direction = enum(u1) { + pub const Direction = enum(u1) { host_to_device = 0, device_to_host = 1, }; }; -const Request = extern struct { +pub const Request = extern struct { // Characteristics of the request request_type: RequestType, // Specific request @@ -175,7 +180,7 @@ const Request = extern struct { // The format of the index field when request_type specifies an endpoint as the // recipient. The host should always set the direction bit to zero (but the device // should accept either value) when the endpoint is part of a control pipe. - const EndpointIndex = packed struct(u16) { + pub const EndpointIndex = packed struct(u16) { // Endpoint number number: EndpointNumber, // Reserved (reset to zero) @@ -188,7 +193,7 @@ const Request = extern struct { // The format of the index field when request_type specifies an interface as the // recipient. - const InterfaceIndex = packed struct(u16) { + pub const InterfaceIndex = packed struct(u16) { // Interface number number: u8, // Reserved (reset to zero) @@ -199,7 +204,7 @@ const Request = extern struct { // descriptor type in the high byte, and the descriptor index in the low byte. The index // is used to select a specific descriptor (only for CONFIGURATION and STRING // descriptors) when several descriptors of that type are implemented by a device. - const DescriptorValue = packed struct(u16) { + pub const DescriptorValue = packed struct(u16) { // Descriptor index index: u8 = 0, // Descriptor type @@ -209,7 +214,7 @@ const Request = extern struct { // Feature selectors, used as the value field of CLEAR_FEATURE and SET_FEATURE requests. The // comment on each value notes the recipient the selector applies to. -const FeatureSelector = enum(u16) { +pub const FeatureSelector = enum(u16) { // Halts an endpoint (recipient: endpoint) endpoint_halt = 0, // Enables or disables the device's remote wakeup capability (recipient: device) @@ -223,7 +228,7 @@ const FeatureSelector = enum(u16) { // with the test_mode feature selector. Values 06h...3Fh are reserved for standard test // selectors and C0h...FFh for vendor-specific test modes; all other unlisted values are // reserved. -const TestMode = enum(u8) { +pub const TestMode = enum(u8) { test_j = 0x01, test_k = 0x02, test_se0_nak = 0x03, @@ -234,7 +239,7 @@ const TestMode = enum(u8) { // The two bytes returned by a GET_STATUS request directed at a device. Fields are declared // least-significant first. -const DeviceStatus = packed struct(u16) { +pub const DeviceStatus = packed struct(u16) { // Whether the device is currently self-powered (as opposed to bus-powered). This bit // cannot be changed with the SET_FEATURE or CLEAR_FEATURE requests. self_powered: bool, @@ -248,7 +253,7 @@ const DeviceStatus = packed struct(u16) { // The two bytes returned by a GET_STATUS request directed at an endpoint. (A GET_STATUS // request directed at an interface returns two bytes that are entirely reserved.) -const EndpointStatus = packed struct(u16) { +pub const EndpointStatus = packed struct(u16) { // Whether the endpoint is currently halted. Set with the SET_FEATURE request using the // endpoint_halt feature selector, and cleared with CLEAR_FEATURE. halted: bool, @@ -258,7 +263,7 @@ const EndpointStatus = packed struct(u16) { // A target for the standard requests that may be directed at the device, an interface, or // an endpoint. -const Target = union(enum) { +pub const Target = union(enum) { device, interface: InterfaceNumber, endpoint: Request.EndpointIndex, @@ -287,7 +292,7 @@ const Target = union(enum) { // Reads the status of the given target: bit-cast the two bytes the device returns into a // DeviceStatus or an EndpointStatus. (The two bytes returned for an interface are entirely // reserved.) -fn getStatus(target: Target) Request { +pub fn getStatus(target: Target) Request { return .{ .request_type = .{ .recipient = target.recipient(), @@ -303,7 +308,7 @@ fn getStatus(target: Target) Request { // Clears or disables the given feature. A device cannot be taken out of a test mode with // this request; test_mode is only cleared by cycling power. -fn clearFeature(feature: FeatureSelector, target: Target) Request { +pub fn clearFeature(feature: FeatureSelector, target: Target) Request { return .{ .request_type = .{ .recipient = target.recipient(), @@ -319,7 +324,7 @@ fn clearFeature(feature: FeatureSelector, target: Target) Request { // Sets or enables the given feature. For the test_mode feature selector, use setTestMode // instead: the test selector rides in the high byte of the index field. -fn setFeature(feature: FeatureSelector, target: Target) Request { +pub fn setFeature(feature: FeatureSelector, target: Target) Request { return .{ .request_type = .{ .recipient = target.recipient(), @@ -335,7 +340,7 @@ fn setFeature(feature: FeatureSelector, target: Target) Request { // Puts a hi-speed device into the given test mode: a SET_FEATURE request with the test_mode // feature selector and the test selector in the high byte of the index field. -fn setTestMode(mode: TestMode) Request { +pub fn setTestMode(mode: TestMode) Request { return .{ .request_type = .{ .recipient = .device, @@ -352,7 +357,7 @@ fn setTestMode(mode: TestMode) Request { // Assigns the device its bus address, moving it from the default state to the address // state. The device does not answer at the new address until the status stage of this // request completes. -fn setAddress(address: DeviceAddress) Request { +pub fn setAddress(address: DeviceAddress) Request { return .{ .request_type = .{ .recipient = .device, @@ -372,7 +377,7 @@ fn setAddress(address: DeviceAddress) Request { // - language_id selects the language of a string descriptor, and is zero otherwise. // - length is the number of bytes to read; a device never returns more than length bytes, // but may return less if the descriptor is shorter. -fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request { +pub fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request { return .{ .request_type = .{ .recipient = .device, @@ -389,7 +394,7 @@ fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, l // Updates an existing descriptor or adds a new one (optional; many devices do not support // this request). The parameters mirror getDescriptor; the descriptor itself is sent in the // DATA stage. -fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request { +pub fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request { return .{ .request_type = .{ .recipient = .device, @@ -405,7 +410,7 @@ fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, l // Reads the currently active configuration: @enumFromInt the byte the device returns into a // ConfigurationValue, which is none while the device is not configured. -fn getConfiguration() Request { +pub fn getConfiguration() Request { return .{ .request_type = .{ .recipient = .device, @@ -422,7 +427,7 @@ fn getConfiguration() Request { // Selects the configuration with the given configuration_value (from // ConfigurationDescriptor.configuration_value), moving the device from the address state to // the configured state. Selecting none returns the device to the address state. -fn setConfiguration(configuration_value: ConfigurationValue) Request { +pub fn setConfiguration(configuration_value: ConfigurationValue) Request { return .{ .request_type = .{ .recipient = .device, @@ -438,7 +443,7 @@ fn setConfiguration(configuration_value: ConfigurationValue) Request { // Reads the alternate setting currently selected for the given interface: @enumFromInt the // byte the device returns into an AlternateSetting. -fn getInterface(interface: InterfaceNumber) Request { +pub fn getInterface(interface: InterfaceNumber) Request { return .{ .request_type = .{ .recipient = .interface, @@ -454,7 +459,7 @@ fn getInterface(interface: InterfaceNumber) Request { // Selects an alternate setting (from InterfaceDescriptor.alternate_setting) for the given // interface. -fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting) Request { +pub fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting) Request { return .{ .request_type = .{ .recipient = .interface, @@ -470,7 +475,7 @@ fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting) // Reads the two-byte number of the frame in which the given isochronous endpoint's // repeating pattern of transfers begins. -fn syncFrame(endpoint: Request.EndpointIndex) Request { +pub fn syncFrame(endpoint: Request.EndpointIndex) Request { return .{ .request_type = .{ .recipient = .endpoint, @@ -484,7 +489,79 @@ fn syncFrame(endpoint: Request.EndpointIndex) Request { }; } -const DescriptorType = enum(u8) { +// Class-specific requests. These carry a `kind = .class` request_type and a +// request_code from the interface's class namespace (not the standard +// RequestCode set above); the code is written into the same byte field, which +// is why RequestCode is non-exhaustive. Each is directed at an interface, whose +// number rides in the index field. + +// The HID class request codes (USB HID 1.11 §7.2). Only the ones danos issues +// are named; the field on the wire is the raw byte. +pub const HidRequestCode = enum(u8) { + get_report = 0x01, + get_idle = 0x02, + get_protocol = 0x03, + set_report = 0x09, + set_idle = 0x0A, + set_protocol = 0x0B, +}; + +// The two protocols a boot-capable HID device can run (USB HID 1.11 §7.2.5). +// A driver selects `boot` for the simplified fixed-format boot report, usable +// before a full report-descriptor parser exists. +pub const HidProtocol = enum(u8) { + boot = 0, + report = 1, +}; + +// SET_PROTOCOL: choose the boot or report protocol on a HID interface. +pub fn setProtocol(interface: InterfaceNumber, protocol: HidProtocol) Request { + return .{ + .request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device }, + .request_code = @enumFromInt(@intFromEnum(HidRequestCode.set_protocol)), + .value = @intFromEnum(protocol), + .index = @intFromEnum(interface), + .length = 0, + }; +} + +// SET_IDLE: bound a HID interface's report rate. `duration` is in 4 ms units +// (0 means report only on change); `report_id` selects a report (0 = all). +pub fn setIdle(interface: InterfaceNumber, duration: u8, report_id: u8) Request { + return .{ + .request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device }, + .request_code = @enumFromInt(@intFromEnum(HidRequestCode.set_idle)), + .value = (@as(u16, duration) << 8) | report_id, + .index = @intFromEnum(interface), + .length = 0, + }; +} + +// Bulk-Only Mass Storage Reset (USB MSC BOT §3.1): ready a mass-storage +// interface for the next Command Block Wrapper after a protocol error. +pub fn bulkOnlyMassStorageReset(interface: InterfaceNumber) Request { + return .{ + .request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device }, + .request_code = @enumFromInt(0xFF), + .value = 0, + .index = @intFromEnum(interface), + .length = 0, + }; +} + +// Get Max LUN (USB MSC BOT §3.2): read the highest logical unit number the +// device supports (0 for a single-LUN flash drive). One byte is returned. +pub fn getMaxLun(interface: InterfaceNumber) Request { + return .{ + .request_type = .{ .recipient = .interface, .kind = .class, .direction = .device_to_host }, + .request_code = @enumFromInt(0xFE), + .value = 0, + .index = @intFromEnum(interface), + .length = 1, + }; +} + +pub const DescriptorType = enum(u8) { device = 1, configuration = 2, string = 3, @@ -496,7 +573,7 @@ const DescriptorType = enum(u8) { _, }; -const DeviceDescriptor = extern struct { +pub const DeviceDescriptor = extern struct { // Size of this descriptor in bytes length: u8, // DEVICE Descriptor Type @@ -541,7 +618,7 @@ const DeviceDescriptor = extern struct { configuration_count: u8, }; -const DeviceQualifierDescriptor = extern struct { +pub const DeviceQualifierDescriptor = extern struct { // Size of this descriptor in bytes length: u8, // DEVICE_QUALIFIER Descriptor Type @@ -564,7 +641,7 @@ const DeviceQualifierDescriptor = extern struct { reserved: u8, }; -const ConfigurationDescriptor = extern struct { +pub const ConfigurationDescriptor = extern struct { // Size of this descriptor in bytes length: u8, // CONFIGURATION Descriptor Type @@ -593,7 +670,7 @@ const ConfigurationDescriptor = extern struct { max_power: u8, // Configuration characteristics. Fields are declared least-significant first. - const Attributes = packed struct(u8) { + pub const Attributes = packed struct(u8) { // Reserved, reset to zero (D4...0) reserved: u5, // Whether Remote Wakeup is supported by this configuration (D5) @@ -612,9 +689,9 @@ const ConfigurationDescriptor = extern struct { // its alternative speed. The structure of the OTHER_SPEED_CONFIGURATION is identical to that // of the CONFIGURATION descriptor; the only difference is that the descriptor_type field // reflects that the descriptor is an OTHER_SPEED_CONFIGURATION descriptor. -const OtherSpeedConfigurationDescriptor = ConfigurationDescriptor; +pub const OtherSpeedConfigurationDescriptor = ConfigurationDescriptor; -const InterfaceDescriptor = extern struct { +pub const InterfaceDescriptor = extern struct { // Size of this descriptor in bytes length: u8, // INTERFACE Descriptor Type @@ -654,7 +731,7 @@ const InterfaceDescriptor = extern struct { interface_index: StringIndex, }; -const EndpointDescriptor = extern struct { +pub const EndpointDescriptor = extern struct { // Size of this descriptor in bytes length: u8, // ENDPOINT Descriptor Type @@ -683,7 +760,7 @@ const EndpointDescriptor = extern struct { interval: u8, // The address of an endpoint. Fields are declared least-significant first. - const Address = packed struct(u8) { + pub const Address = packed struct(u8) { // Endpoint Number (D3...0) number: EndpointNumber, // Reserved, reset to zero (D6...4) @@ -693,7 +770,7 @@ const EndpointDescriptor = extern struct { }; // An endpoint's attributes. Fields are declared least-significant first. - const Attributes = packed struct(u8) { + pub const Attributes = packed struct(u8) { // Transfer Type (D1...0) transfer_type: TransferType, // Synchronization Type; isochronous endpoints only, reserved and reset to zero for @@ -706,21 +783,21 @@ const EndpointDescriptor = extern struct { reserved: u2, }; - const TransferType = enum(u2) { + pub const TransferType = enum(u2) { control = 0, isochronous = 1, bulk = 2, interrupt = 3, }; - const Synchronization = enum(u2) { + pub const Synchronization = enum(u2) { none = 0, asynchronous = 1, adaptive = 2, synchronous = 3, }; - const Usage = enum(u2) { + pub const Usage = enum(u2) { data = 0, feedback = 1, implicit_feedback_data = 2, @@ -728,7 +805,7 @@ const EndpointDescriptor = extern struct { }; // The maximum packet size of an endpoint. Fields are declared least-significant first. - const MaxPacketSize = packed struct(u16) { + pub const MaxPacketSize = packed struct(u16) { // Maximum packet size in bytes (bits 10...0) size: u11, // Number of additional transaction opportunities per microframe, for high-speed @@ -739,7 +816,7 @@ const EndpointDescriptor = extern struct { reserved: u3, }; - const AdditionalTransactions = enum(u2) { + pub const AdditionalTransactions = enum(u2) { // None (1 transaction per microframe) none = 0, // 1 additional (2 transactions per microframe) @@ -755,7 +832,7 @@ const EndpointDescriptor = extern struct { // header, followed by the variable-length payload: // - index 0: an array of two-byte LANGID codes (wLangID[0] through wLangID[x]) // - other indices: a Unicode string of N bytes -const StringDescriptor = extern struct { +pub const StringDescriptor = extern struct { // Size of this descriptor in bytes length: u8, // STRING Descriptor Type @@ -834,7 +911,7 @@ test "bitmap packings match the specification" { try expect(hid_type != .device); } -fn expectRequestBytes(request: Request, expected: [8]u8) !void { +pub fn expectRequestBytes(request: Request, expected: [8]u8) !void { try std.testing.expectEqualSlices(u8, &expected, std.mem.asBytes(&request)); } @@ -855,3 +932,14 @@ test "standard request constructors encode the specification's set-up packets" { try expectRequestBytes(setInterface(@enumFromInt(2), @enumFromInt(1)), .{ 0x01, 11, 1, 0, 2, 0, 0, 0 }); try expectRequestBytes(syncFrame(.{ .number = @enumFromInt(3), .direction = .in }), .{ 0x82, 12, 0, 0, 0x83, 0, 2, 0 }); } + +test "class request constructors encode the specification's set-up packets" { + // bmRequestType for a host-to-device class request to an interface = 0x21; + // device-to-host = 0xA1. The request_code byte is the class code, not a + // standard one — SET_PROTOCOL 0x0B, SET_IDLE 0x0A, BOT reset 0xFF, Max LUN 0xFE. + try expectRequestBytes(setProtocol(@enumFromInt(0), .boot), .{ 0x21, 0x0B, 0, 0, 0, 0, 0, 0 }); + try expectRequestBytes(setProtocol(@enumFromInt(1), .report), .{ 0x21, 0x0B, 1, 0, 1, 0, 0, 0 }); + try expectRequestBytes(setIdle(@enumFromInt(1), 0, 0), .{ 0x21, 0x0A, 0, 0, 1, 0, 0, 0 }); + try expectRequestBytes(bulkOnlyMassStorageReset(@enumFromInt(0)), .{ 0x21, 0xFF, 0, 0, 0, 0, 0, 0 }); + try expectRequestBytes(getMaxLun(@enumFromInt(0)), .{ 0xA1, 0xFE, 0, 0, 0, 0, 1, 0 }); +} diff --git a/system/devices/usb-ids.zig b/system/devices/usb-ids.zig index 4c7278e..470fc95 100644 --- a/system/devices/usb-ids.zig +++ b/system/devices/usb-ids.zig @@ -11,7 +11,7 @@ // Base class codes (assigned by the USB-IF). The comment on each value notes where the code // may legally appear: in the device descriptor, in interface descriptors, or both. -const Class = enum(u8) { +pub const Class = enum(u8) { // Use class information in the interface descriptors (device descriptor only). Each // interface within a configuration specifies its own class information and the various // interfaces operate independently. @@ -72,8 +72,8 @@ const Class = enum(u8) { // Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the // protocol distinguishes the hub's transaction-translator arrangement. -const hub = struct { - const Protocol = enum(u8) { +pub const hub = struct { + pub const Protocol = enum(u8) { // Full-speed hub full_speed = 0x00, // Hi-speed hub with a single transaction translator @@ -87,8 +87,8 @@ const hub = struct { }; // Subclass and protocol codes qualified by Class.hid. -const hid = struct { - const SubClass = enum(u8) { +pub const hid = struct { + pub const SubClass = enum(u8) { // No subclass none = 0x00, // Boot interface: the device also supports the simplified boot protocol, usable by @@ -98,7 +98,7 @@ const hid = struct { }; // Only meaningful when the subclass is boot - const Protocol = enum(u8) { + pub const Protocol = enum(u8) { none = 0x00, keyboard = 0x01, mouse = 0x02, @@ -109,8 +109,8 @@ const hid = struct { // Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the // command set the device understands; the protocol identifies the transport used to carry // commands, data, and status over the bus. -const mass_storage = struct { - const SubClass = enum(u8) { +pub const mass_storage = struct { + pub const SubClass = enum(u8) { // SCSI command set not reported; de facto, treat as scsi not_reported = 0x00, // Reduced Block Commands: typically flash devices @@ -134,7 +134,7 @@ const mass_storage = struct { _, }; - const Protocol = enum(u8) { + pub const Protocol = enum(u8) { // Control/Bulk/Interrupt with command completion interrupt cbi_completion_interrupt = 0x00, // Control/Bulk/Interrupt without command completion interrupt @@ -152,8 +152,8 @@ const mass_storage = struct { // Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes // are model-specific; the useful invariant is the subclass, which selects the control model // the interface implements. -const communications = struct { - const SubClass = enum(u8) { +pub const communications = struct { + pub const SubClass = enum(u8) { // Direct line control model direct_line = 0x01, // Abstract control model: USB modems and serial adapters @@ -185,15 +185,15 @@ const communications = struct { }; // Subclass and protocol codes qualified by Class.wireless_controller. -const wireless_controller = struct { - const SubClass = enum(u8) { +pub const wireless_controller = struct { + pub const SubClass = enum(u8) { // Radio frequency controllers radio_frequency = 0x01, _, }; // Only meaningful when the subclass is radio_frequency - const Protocol = enum(u8) { + pub const Protocol = enum(u8) { // Bluetooth programming interface bluetooth = 0x01, // Ultra-wideband radio control @@ -207,15 +207,15 @@ const wireless_controller = struct { }; // Subclass and protocol codes qualified by Class.miscellaneous. -const miscellaneous = struct { - const SubClass = enum(u8) { +pub const miscellaneous = struct { + pub const SubClass = enum(u8) { // Common class common = 0x02, _, }; // Only meaningful when the subclass is common - const Protocol = enum(u8) { + pub const Protocol = enum(u8) { // Interface association descriptor: at the device level, announces that the // configuration groups interfaces into functions with IADs interface_association = 0x01, @@ -224,8 +224,8 @@ const miscellaneous = struct { }; // Subclass and protocol codes qualified by Class.application_specific. -const application_specific = struct { - const SubClass = enum(u8) { +pub const application_specific = struct { + pub const SubClass = enum(u8) { // Device firmware upgrade firmware_upgrade = 0x01, // IrDA bridge @@ -236,6 +236,23 @@ const application_specific = struct { }; }; +/// Pack a (class, subclass, protocol) triple into one 0xCCSSPP value — the +/// bus-native identity a USB bus driver reports in `ChildAdded.identity` and the +/// device manager matches on (the USB analog of a packed PCI class code). Mirrors +/// `pci_class.ClassCode.pack`, so both sides build/decode the identical u64. +pub fn packTriple(class: u8, subclass: u8, protocol: u8) u64 { + return (@as(u64, class) << 16) | (@as(u64, subclass) << 8) | protocol; +} + +/// The inverse of `packTriple`. +pub fn unpackTriple(triple: u64) struct { class: u8, subclass: u8, protocol: u8 } { + return .{ + .class = @truncate(triple >> 16), + .subclass = @truncate(triple >> 8), + .protocol = @truncate(triple), + }; +} + test "class codes match the USB-IF assignments" { const std = @import("std"); const expectEqual = std.testing.expectEqual; @@ -262,3 +279,29 @@ test "class codes match the USB-IF assignments" { _ = miscellaneous.Protocol.interface_association; _ = application_specific.SubClass.firmware_upgrade; } + +test "packTriple / unpackTriple round-trip the identity a bus driver reports" { + const std = @import("std"); + const expectEqual = std.testing.expectEqual; + + // A boot keyboard interface: HID / boot / keyboard. + const keyboard = packTriple( + @intFromEnum(Class.hid), + @intFromEnum(hid.SubClass.boot), + @intFromEnum(hid.Protocol.keyboard), + ); + try expectEqual(@as(u64, 0x03_01_01), keyboard); + + // A flash drive interface: mass storage / SCSI / bulk-only. + const storage = packTriple( + @intFromEnum(Class.mass_storage), + @intFromEnum(mass_storage.SubClass.scsi), + @intFromEnum(mass_storage.Protocol.bulk_only), + ); + try expectEqual(@as(u64, 0x08_06_50), storage); + + const parts = unpackTriple(storage); + try expectEqual(@as(u8, 0x08), parts.class); + try expectEqual(@as(u8, 0x06), parts.subclass); + try expectEqual(@as(u8, 0x50), parts.protocol); +} diff --git a/system/drivers/usb-hid/hid-report.zig b/system/drivers/usb-hid/hid-report.zig new file mode 100644 index 0000000..7320c02 --- /dev/null +++ b/system/drivers/usb-hid/hid-report.zig @@ -0,0 +1,191 @@ +//! Pure decoders for USB HID **boot-protocol** reports — the simplified, +//! fixed-format reports a boot keyboard and boot mouse send, the USB analog of +//! the PS/2 scancode and mouse-packet decoders. No I/O: these turn report bytes +//! into make/break transitions and motion, which the usb-hid drivers publish to +//! the input service. Host-testable in isolation (like mouse-packet.zig). +//! +//! "Boot protocol" is a USB HID term (USB HID 1.11 §B) — the device reports in +//! this fixed layout after SET_PROTOCOL(boot); it has nothing to do with system +//! boot. + +const std = @import("std"); + +// --- keyboard --------------------------------------------------------------- + +/// The 8-byte boot keyboard report: a modifier bitmap, a reserved byte, and up +/// to six concurrently-pressed key usages. +pub const KeyboardReport = extern struct { + modifiers: u8 = 0, + reserved: u8 = 0, + keys: [6]u8 = .{ 0, 0, 0, 0, 0, 0 }, +}; + +// The modifier byte's bits (HID keyboard boot report). +pub const modifier_left_control: u8 = 1 << 0; +pub const modifier_left_shift: u8 = 1 << 1; +pub const modifier_left_alt: u8 = 1 << 2; +pub const modifier_left_gui: u8 = 1 << 3; +pub const modifier_right_control: u8 = 1 << 4; +pub const modifier_right_shift: u8 = 1 << 5; +pub const modifier_right_alt: u8 = 1 << 6; +pub const modifier_right_gui: u8 = 1 << 7; + +pub const TransitionKind = enum { pressed, released }; + +/// One key going down or up. `usage` is a HID keyboard-page usage — modifier keys +/// map to usages 224..231 — which is exactly the input protocol's `Keycode`. +pub const Transition = struct { kind: TransitionKind, usage: u8 }; + +// A report can change at most all 8 modifiers and all 6 keys at once. +pub const max_transitions = 8 + 6; + +pub const Transitions = struct { + items: [max_transitions]Transition = undefined, + count: usize = 0, + + fn add(self: *Transitions, transition: Transition) void { + if (self.count < self.items.len) { + self.items[self.count] = transition; + self.count += 1; + } + } + + pub fn slice(self: *const Transitions) []const Transition { + return self.items[0..self.count]; + } +}; + +/// Turns a stream of boot keyboard reports into make/break transitions by diffing +/// each report against the last. +pub const KeyboardDecoder = struct { + previous: KeyboardReport = .{}, + + pub fn feed(self: *KeyboardDecoder, current: KeyboardReport) Transitions { + var out = Transitions{}; + + // Rollover: 0x01 (ErrorRollOver) means more keys are held than the report + // can carry, so the key array is invalid. Emit nothing and keep the prior + // state (so the eventual releases still resolve against real keys). + for (current.keys) |key| { + if (key == 0x01) return out; + } + + // Modifiers: one make/break per changed bit; modifier usages are 224..231. + const changed = current.modifiers ^ self.previous.modifiers; + var bit: u3 = 0; + while (true) : (bit += 1) { + const mask = @as(u8, 1) << bit; + if (changed & mask != 0) { + out.add(.{ + .kind = if (current.modifiers & mask != 0) .pressed else .released, + .usage = 224 + @as(u8, bit), + }); + } + if (bit == 7) break; + } + + // Keys made: present now, absent before. + for (current.keys) |key| { + if (key != 0 and !contains(&self.previous.keys, key)) out.add(.{ .kind = .pressed, .usage = key }); + } + // Keys broken: present before, absent now. + for (self.previous.keys) |key| { + if (key != 0 and !contains(¤t.keys, key)) out.add(.{ .kind = .released, .usage = key }); + } + + self.previous = current; + return out; + } +}; + +fn contains(keys: *const [6]u8, value: u8) bool { + for (keys) |key| { + if (key == value) return true; + } + return false; +} + +// --- mouse ------------------------------------------------------------------ + +/// A decoded boot mouse report: the button bitmap and relative motion. The wheel +/// byte is present only on 4-byte reports (QEMU's usb-mouse sends one). +pub const MouseReport = struct { + buttons: u8 = 0, + dx: i8 = 0, + dy: i8 = 0, + wheel: i8 = 0, + has_wheel: bool = false, +}; + +pub const mouse_button_left: u8 = 1 << 0; +pub const mouse_button_right: u8 = 1 << 1; +pub const mouse_button_middle: u8 = 1 << 2; + +/// Parse a 3- or 4-byte boot mouse report. Note HID reports Y in screen +/// convention (positive = down), so — unlike PS/2 — `dy` is NOT negated. +pub fn parseMouse(bytes: []const u8) ?MouseReport { + if (bytes.len < 3) return null; + return .{ + .buttons = bytes[0], + .dx = @bitCast(bytes[1]), + .dy = @bitCast(bytes[2]), + .wheel = if (bytes.len >= 4) @bitCast(bytes[3]) else 0, + .has_wheel = bytes.len >= 4, + }; +} + +// --- tests ------------------------------------------------------------------ + +test "keyboard diff produces make and break transitions" { + var decoder = KeyboardDecoder{}; + + // Press 'a' (usage 4). + var t = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } }); + try std.testing.expectEqual(@as(usize, 1), t.count); + try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind); + try std.testing.expectEqual(@as(u8, 4), t.items[0].usage); + + // Hold 'a', press 'b' (usage 5): only 'b' is new. + t = decoder.feed(.{ .keys = .{ 4, 5, 0, 0, 0, 0 } }); + try std.testing.expectEqual(@as(usize, 1), t.count); + try std.testing.expectEqual(@as(u8, 5), t.items[0].usage); + + // Release everything: 'a' and 'b' both break. + t = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } }); + try std.testing.expectEqual(@as(usize, 2), t.count); + try std.testing.expectEqual(TransitionKind.released, t.items[0].kind); + + // Press Left Shift (modifier bit 1 -> usage 225). + t = decoder.feed(.{ .modifiers = modifier_left_shift }); + try std.testing.expectEqual(@as(usize, 1), t.count); + try std.testing.expectEqual(@as(u8, 225), t.items[0].usage); + try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind); +} + +test "rollover report is ignored but state is preserved" { + var decoder = KeyboardDecoder{}; + _ = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } }); // press 'a' + + const rollover = decoder.feed(.{ .keys = .{ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 } }); + try std.testing.expectEqual(@as(usize, 0), rollover.count); + + // 'a' is still considered down, so releasing all keys now breaks it. + const release = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } }); + try std.testing.expectEqual(@as(usize, 1), release.count); + try std.testing.expectEqual(@as(u8, 4), release.items[0].usage); + try std.testing.expectEqual(TransitionKind.released, release.items[0].kind); +} + +test "mouse report parses motion without inverting Y" { + const three = parseMouse(&.{ mouse_button_left, 5, 0xFB }).?; // dy = -5 + try std.testing.expectEqual(mouse_button_left, three.buttons); + try std.testing.expectEqual(@as(i8, 5), three.dx); + try std.testing.expectEqual(@as(i8, -5), three.dy); + try std.testing.expect(!three.has_wheel); + + const four = parseMouse(&.{ 0, 0, 0, 0xFF }).?; // wheel = -1 + try std.testing.expect(four.has_wheel); + try std.testing.expectEqual(@as(i8, -1), four.wheel); + + try std.testing.expect(parseMouse(&.{ 0, 0 }) == null); // too short +} diff --git a/system/drivers/usb-hid/keyboard.zig b/system/drivers/usb-hid/keyboard.zig new file mode 100644 index 0000000..6bf0f41 --- /dev/null +++ b/system/drivers/usb-hid/keyboard.zig @@ -0,0 +1,174 @@ +//! USB HID boot keyboard driver. +//! +//! Spawned by the device manager when the xHCI bus driver reports a HID / boot / +//! keyboard interface (class 3, subclass 1, protocol 1); its assigned device id +//! arrives as argv[1] and an optional layout name ("us", "gb", ...) as argv[2]. +//! It owns no hardware: it opens its device through the USB transfer protocol +//! (`runtime.usb`), asks the device for the boot protocol, subscribes to its +//! interrupt-IN endpoint, and turns each 8-byte boot report into input-protocol +//! events, published to the input service — the USB analogue of ps2-bus/keyboard. +//! +//! interrupt report -> hid-report diff -> key_down / key_up +//! -> xkeyboard-config -> character -> key_press +//! +//! Because a USB keyboard's usages ARE the input protocol's keycodes (both are +//! HID keyboard page 0x07), the decode is nearly 1:1 — no scancode translation. + +const std = @import("std"); +const runtime = @import("runtime"); +const usb_abi = @import("usb-abi"); +const xkb = @import("xkeyboard-config"); +const hid = @import("hid-report.zig"); +const ipc = runtime.ipc; +const process = runtime.process; +const input_protocol = runtime.input_protocol; + +fn writeLine(comptime fmt: []const u8, arguments: anytype) void { + var line: [128]u8 = undefined; + _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); +} + +// The modifier state a character lookup needs — derived from the report's +// modifier byte, plus the driver-tracked caps-lock toggle. +const ModifierSnapshot = struct { + shift: bool, + control: bool, + right_alt: bool, + caps_lock: bool, +}; + +/// The character a key produces under `modifiers`, or 0 for none — the layout +/// lookup for printable keys, with ASCII control characters for the keys every +/// consumer expects (Enter, Tab, Backspace, Escape), exactly as ps2-bus/keyboard. +fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: ModifierSnapshot) u32 { + const mapping = xkb.map(layout, usage, .{ + .shift = modifiers.shift, + .caps_lock = modifiers.caps_lock, + .level3 = modifiers.right_alt, + .control = modifiers.control, + }); + if (mapping.character) |character| return character; + return switch (@as(input_protocol.Keycode, @enumFromInt(usage))) { + .enter, .keypad_enter => '\n', + .tab => '\t', + .backspace => 0x08, + .escape => 0x1B, + else => 0, + }; +} + +fn modifierWord(modifiers: u8) u32 { + var word: u32 = 0; + if (modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0) word |= input_protocol.modifier_shift; + if (modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0) word |= input_protocol.modifier_control; + if (modifiers & (hid.modifier_left_alt | hid.modifier_right_alt) != 0) word |= input_protocol.modifier_alt; + return word; +} + +pub fn main(init: runtime.process.Init) void { + const argument = init.arguments.get(1) orelse { + _ = runtime.system.write("/system/drivers/usb-hid/keyboard: missing device id (argv[1])\n"); + return; + }; + const device_id = std.fmt.parseInt(u64, argument, 10) catch { + writeLine("/system/drivers/usb-hid/keyboard: malformed device id '{s}'\n", .{argument}); + return; + }; + const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us; + + // Hello the manager first (meet the spawn deadline), then open the device. + if (!runtime.usb.helloManager(device_id)) { + _ = runtime.system.write("/system/drivers/usb-hid/keyboard: hello to device manager failed\n"); + return; + } + var device = runtime.usb.open(device_id) orelse { + writeLine("/system/drivers/usb-hid/keyboard: could not open device {d}\n", .{device_id}); + return; + }; + const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse { + _ = runtime.system.write("/system/drivers/usb-hid/keyboard: no interrupt-IN endpoint\n"); + return; + }; + + // Ask for the boot protocol and an indefinite idle (report only on change). + _ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot))); + _ = device.controlOut(@bitCast(usb_abi.setIdle(@enumFromInt(device.interface_number), 0, 0))); + + if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) { + _ = runtime.system.write("/system/drivers/usb-hid/keyboard: interrupt subscribe failed\n"); + return; + } + + var source = runtime.input.connectSource() orelse { + _ = runtime.system.write("/system/drivers/usb-hid/keyboard: input service unavailable\n"); + return; + }; + _ = process.bindSignals(device.endpoint); + writeLine("/system/drivers/usb-hid/keyboard: ok (device {d}, interface {d}, layout {s})\n", .{ device_id, device.interface_number, layout.name }); + + var decoder = hid.KeyboardDecoder{}; + var caps_lock = false; + var receive: [64]u8 = undefined; + while (true) { + const got = ipc.replyWait(device.endpoint, &.{}, &receive, null); + if (!got.isNotification()) continue; + if (process.signalsFrom(got.badge)) |signals| { + if (signals.has(.terminate)) return; + continue; + } + if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue; + + const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]); + if (message.length < @sizeOf(hid.KeyboardReport)) continue; + const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]); + const transitions = decoder.feed(report); + + // Caps Lock toggles on its own key-down (a stateful lock, not a modifier). + for (transitions.slice()) |transition| { + if (transition.kind == .pressed and @as(input_protocol.Keycode, @enumFromInt(transition.usage)) == .caps_lock) caps_lock = !caps_lock; + } + + const modifiers = ModifierSnapshot{ + .shift = report.modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0, + .control = report.modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0, + .right_alt = report.modifiers & hid.modifier_right_alt != 0, + .caps_lock = caps_lock, + }; + const modifier_word = modifierWord(report.modifiers); + + for (transitions.slice()) |transition| { + switch (transition.kind) { + .pressed => { + _ = source.publishKeyboardEvent(.{ + .kind = @intFromEnum(input_protocol.EventKind.key_down), + .keycode = transition.usage, + .character = 0, + .modifiers = modifier_word, + }); + const character = characterFor(layout, transition.usage, modifiers); + if (character != 0) { + _ = source.publishKeyboardEvent(.{ + .kind = @intFromEnum(input_protocol.EventKind.key_press), + .keycode = transition.usage, + .character = character, + .modifiers = modifier_word, + }); + } + }, + .released => { + _ = source.publishKeyboardEvent(.{ + .kind = @intFromEnum(input_protocol.EventKind.key_up), + .keycode = transition.usage, + .character = 0, + .modifiers = modifier_word, + }); + }, + } + } + } +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; +} diff --git a/system/drivers/usb-hid/mouse.zig b/system/drivers/usb-hid/mouse.zig new file mode 100644 index 0000000..74dd31a --- /dev/null +++ b/system/drivers/usb-hid/mouse.zig @@ -0,0 +1,140 @@ +//! USB HID boot mouse driver. +//! +//! Spawned by the device manager when the xHCI bus driver reports a HID / boot / +//! mouse interface (class 3, subclass 1, protocol 2); its assigned device id +//! arrives as argv[1]. Like the keyboard driver it owns no hardware: it opens its +//! device through the USB transfer protocol (`runtime.usb`), asks for the boot +//! protocol, subscribes to its interrupt-IN endpoint, and turns each 3- or 4-byte +//! boot report into input-protocol mouse events published to the input service. +//! +//! Unlike PS/2, HID reports Y in screen convention (positive = down), so motion +//! is passed straight through (the decode in hid-report.zig does not negate it). + +const std = @import("std"); +const runtime = @import("runtime"); +const usb_abi = @import("usb-abi"); +const hid = @import("hid-report.zig"); +const ipc = runtime.ipc; +const process = runtime.process; +const input_protocol = runtime.input_protocol; + +fn writeLine(comptime fmt: []const u8, arguments: anytype) void { + var line: [128]u8 = undefined; + _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); +} + +// The current pressed-button bitmask in input-protocol terms. +fn buttonMask(buttons: u8) u32 { + var mask: u32 = 0; + if (buttons & hid.mouse_button_left != 0) mask |= input_protocol.mouse_button_left; + if (buttons & hid.mouse_button_right != 0) mask |= input_protocol.mouse_button_right; + if (buttons & hid.mouse_button_middle != 0) mask |= input_protocol.mouse_button_middle; + return mask; +} + +pub fn main(init: runtime.process.Init) void { + const argument = init.arguments.get(1) orelse { + _ = runtime.system.write("/system/drivers/usb-hid/mouse: missing device id (argv[1])\n"); + return; + }; + const device_id = std.fmt.parseInt(u64, argument, 10) catch { + writeLine("/system/drivers/usb-hid/mouse: malformed device id '{s}'\n", .{argument}); + return; + }; + + if (!runtime.usb.helloManager(device_id)) { + _ = runtime.system.write("/system/drivers/usb-hid/mouse: hello to device manager failed\n"); + return; + } + var device = runtime.usb.open(device_id) orelse { + writeLine("/system/drivers/usb-hid/mouse: could not open device {d}\n", .{device_id}); + return; + }; + const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse { + _ = runtime.system.write("/system/drivers/usb-hid/mouse: no interrupt-IN endpoint\n"); + return; + }; + + _ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot))); + + if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) { + _ = runtime.system.write("/system/drivers/usb-hid/mouse: interrupt subscribe failed\n"); + return; + } + + var source = runtime.input.connectSource() orelse { + _ = runtime.system.write("/system/drivers/usb-hid/mouse: input service unavailable\n"); + return; + }; + _ = process.bindSignals(device.endpoint); + writeLine("/system/drivers/usb-hid/mouse: ok (device {d}, interface {d})\n", .{ device_id, device.interface_number }); + + var previous_buttons: u8 = 0; + var receive: [64]u8 = undefined; + while (true) { + const got = ipc.replyWait(device.endpoint, &.{}, &receive, null); + if (!got.isNotification()) continue; + if (process.signalsFrom(got.badge)) |signals| { + if (signals.has(.terminate)) return; + continue; + } + if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue; + + const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]); + const length = @min(message.length, message.data.len); + const report = hid.parseMouse(message.data[0..length]) orelse continue; + const mask = buttonMask(report.buttons); + + // Button transitions: one event per changed button bit. + const changed = report.buttons ^ previous_buttons; + inline for (.{ + .{ hid.mouse_button_left, input_protocol.mouse_button_left }, + .{ hid.mouse_button_right, input_protocol.mouse_button_right }, + .{ hid.mouse_button_middle, input_protocol.mouse_button_middle }, + }) |pair| { + if (changed & pair[0] != 0) { + _ = source.publishMouseEvent(.{ + .kind = @intFromEnum(if (report.buttons & pair[0] != 0) input_protocol.MouseEventKind.button_down else input_protocol.MouseEventKind.button_up), + .button = pair[1], + .dx = 0, + .dy = 0, + .scroll_x = 0, + .scroll_y = 0, + .buttons = mask, + }); + } + } + previous_buttons = report.buttons; + + // Relative motion (dy straight through — HID Y is already screen convention). + if (report.dx != 0 or report.dy != 0) { + _ = source.publishMouseEvent(.{ + .kind = @intFromEnum(input_protocol.MouseEventKind.motion), + .button = 0, + .dx = report.dx, + .dy = report.dy, + .scroll_x = 0, + .scroll_y = 0, + .buttons = mask, + }); + } + + // Wheel (4-byte reports only): positive = scroll up. + if (report.has_wheel and report.wheel != 0) { + _ = source.publishMouseEvent(.{ + .kind = @intFromEnum(input_protocol.MouseEventKind.scroll), + .button = 0, + .dx = 0, + .dy = 0, + .scroll_x = 0, + .scroll_y = report.wheel, + .buttons = mask, + }); + } + } +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; +} diff --git a/system/drivers/usb-storage/bulk-only-transport.zig b/system/drivers/usb-storage/bulk-only-transport.zig new file mode 100644 index 0000000..238d5f2 --- /dev/null +++ b/system/drivers/usb-storage/bulk-only-transport.zig @@ -0,0 +1,73 @@ +//! USB Mass Storage Bulk-Only Transport (BOT) wire structures — the Command and +//! Command Status Wrappers that bracket every command (USB MSC BOT §5). Pure data +//! definitions, host-testable in isolation. The command inside the CBW is a SCSI +//! CDB (see scsi.zig); the transport here just carries it and reports status. +//! +//! One command is three bulk transfers: CBW out, an optional data stage, CSW in. + +const std = @import("std"); + +/// "USBC" — the signature at the head of every Command Block Wrapper. +pub const cbw_signature: u32 = 0x43425355; +/// "USBS" — the signature at the head of every Command Status Wrapper. +pub const csw_signature: u32 = 0x53425355; + +/// CBW `flags`: set for a device-to-host (IN) data stage, clear for OUT. +pub const flag_data_in: u8 = 0x80; + +/// The 31-byte Command Block Wrapper, sent on the bulk-OUT endpoint. +pub const CommandBlockWrapper = extern struct { + signature: u32 align(1) = cbw_signature, + tag: u32 align(1), + data_transfer_length: u32 align(1), + flags: u8, + lun: u8, + cdb_length: u8, + cdb: [16]u8 = [_]u8{0} ** 16, +}; + +/// A device's answer to a command (the CSW `status` byte). +pub const CommandStatus = enum(u8) { + passed = 0, + failed = 1, + phase_error = 2, + _, +}; + +/// The 13-byte Command Status Wrapper, read from the bulk-IN endpoint. +pub const CommandStatusWrapper = extern struct { + signature: u32 align(1) = csw_signature, + tag: u32 align(1), + data_residue: u32 align(1), + status: u8, +}; + +comptime { + std.debug.assert(@sizeOf(CommandBlockWrapper) == 31); + std.debug.assert(@sizeOf(CommandStatusWrapper) == 13); +} + +test "wrapper sizes and signatures match the specification" { + const cbw = CommandBlockWrapper{ + .tag = 0x11223344, + .data_transfer_length = 512, + .flags = flag_data_in, + .lun = 0, + .cdb_length = 10, + }; + const bytes = std.mem.asBytes(&cbw); + try std.testing.expectEqual(@as(usize, 31), bytes.len); + // "USBC" little-endian. + try std.testing.expectEqualSlices(u8, "USBC", bytes[0..4]); + try std.testing.expectEqual(flag_data_in, bytes[12]); + + const csw = std.mem.bytesToValue(CommandStatusWrapper, &[_]u8{ + 0x55, 0x53, 0x42, 0x53, // "USBS" + 0x44, 0x33, 0x22, 0x11, // tag + 0x00, 0x00, 0x00, 0x00, // residue + 0x00, // passed + }); + try std.testing.expectEqual(csw_signature, csw.signature); + try std.testing.expectEqual(@as(u32, 0x11223344), csw.tag); + try std.testing.expectEqual(@as(u8, @intFromEnum(CommandStatus.passed)), csw.status); +} diff --git a/system/drivers/usb-storage/scsi.zig b/system/drivers/usb-storage/scsi.zig new file mode 100644 index 0000000..d144abc --- /dev/null +++ b/system/drivers/usb-storage/scsi.zig @@ -0,0 +1,86 @@ +//! The SCSI command descriptor blocks a transparent-SCSI (subclass 0x06) mass +//! storage device understands, and the parsers for what they return. Pure data — +//! host-testable. These CDBs go inside a Bulk-Only-Transport CBW (see +//! bulk-only-transport.zig). +//! +//! Every multi-byte SCSI field is **big-endian** — the opposite of the USB wire +//! ABI — so the LBA and transfer-length encodings are the load-bearing detail. + +const std = @import("std"); + +// SCSI operation codes. +const op_test_unit_ready: u8 = 0x00; +const op_request_sense: u8 = 0x03; +const op_inquiry: u8 = 0x12; +const op_read_capacity_10: u8 = 0x25; +const op_read_10: u8 = 0x28; +const op_write_10: u8 = 0x2A; + +/// INQUIRY: standard device data (36 bytes: peripheral type, removable, vendor +/// and product strings). +pub fn inquiry(allocation_length: u8) [6]u8 { + return .{ op_inquiry, 0, 0, 0, allocation_length, 0 }; +} + +/// TEST UNIT READY: no data; success (CSW passed) means the unit is ready. +pub fn testUnitReady() [6]u8 { + return .{ op_test_unit_ready, 0, 0, 0, 0, 0 }; +} + +/// REQUEST SENSE: 18 bytes of sense data (sense key + ASC/ASCQ) explaining the +/// previous failure. +pub fn requestSense(allocation_length: u8) [6]u8 { + return .{ op_request_sense, 0, 0, 0, allocation_length, 0 }; +} + +/// READ CAPACITY(10): 8 bytes back — the last LBA and the block size, both u32 +/// big-endian. Block count is last_lba + 1. +pub fn readCapacity10() [10]u8 { + return .{ op_read_capacity_10, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +} + +/// READ(10): read `blocks` logical blocks starting at `lba` into the data stage. +pub fn read10(lba: u32, blocks: u16) [10]u8 { + var cdb = [_]u8{0} ** 10; + cdb[0] = op_read_10; + std.mem.writeInt(u32, cdb[2..6], lba, .big); + std.mem.writeInt(u16, cdb[7..9], blocks, .big); + return cdb; +} + +/// WRITE(10): write `blocks` logical blocks starting at `lba` from the data stage. +pub fn write10(lba: u32, blocks: u16) [10]u8 { + var cdb = [_]u8{0} ** 10; + cdb[0] = op_write_10; + std.mem.writeInt(u32, cdb[2..6], lba, .big); + std.mem.writeInt(u16, cdb[7..9], blocks, .big); + return cdb; +} + +/// Decode an 8-byte READ CAPACITY(10) reply. +pub fn parseCapacity(bytes: [8]u8) struct { last_lba: u32, block_size: u32 } { + return .{ + .last_lba = std.mem.readInt(u32, bytes[0..4], .big), + .block_size = std.mem.readInt(u32, bytes[4..8], .big), + }; +} + +test "read/write CDBs encode the LBA and length big-endian" { + const read = read10(0x01020304, 8); + try std.testing.expectEqualSlices(u8, &.{ 0x28, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x08, 0x00 }, &read); + + const write = write10(0xAABBCCDD, 1); + try std.testing.expectEqualSlices(u8, &.{ 0x2A, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0x00, 0x00, 0x01, 0x00 }, &write); + + try std.testing.expectEqual(@as(u8, 0x25), readCapacity10()[0]); + try std.testing.expectEqual(@as(u8, 0x12), inquiry(36)[0]); + try std.testing.expectEqual(@as(u8, 36), inquiry(36)[4]); + try std.testing.expectEqual(@as(u8, 0x00), testUnitReady()[0]); +} + +test "read capacity parses last LBA and block size" { + // last_lba = 0x0003FFFF (262144 blocks), block_size = 512. + const capacity = parseCapacity(.{ 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x02, 0x00 }); + try std.testing.expectEqual(@as(u32, 0x0003FFFF), capacity.last_lba); + try std.testing.expectEqual(@as(u32, 512), capacity.block_size); +} diff --git a/system/drivers/usb-storage/usb-storage.zig b/system/drivers/usb-storage/usb-storage.zig new file mode 100644 index 0000000..cbd6395 --- /dev/null +++ b/system/drivers/usb-storage/usb-storage.zig @@ -0,0 +1,179 @@ +//! USB mass-storage class driver (Bulk-Only Transport + transparent SCSI). +//! +//! Spawned by the device manager when the xHCI bus driver reports a mass-storage +//! / SCSI / bulk-only interface (class 8, subclass 6, protocol 0x50); its device +//! id arrives as argv[1]. It owns no hardware: it opens its device through the +//! USB transfer protocol (`runtime.usb`), then drives it with the BOT command +//! cycle — CBW out, an optional data stage, CSW in — carrying SCSI commands +//! (READ CAPACITY, READ(10), WRITE(10)). Upward it is a block device: it serves +//! the block protocol under `.block`, the storage a FAT filesystem sits on. +//! +//! Block data never crosses IPC: read/write name a caller-owned DMA buffer by +//! physical address, which the data stage DMAs straight to/from. + +const std = @import("std"); +const runtime = @import("runtime"); +const scsi = @import("scsi.zig"); +const bot = @import("bulk-only-transport.zig"); +const block_protocol = @import("block-protocol"); +const dma = runtime.dma; + +fn writeLine(comptime fmt: []const u8, arguments: anytype) void { + var line: [128]u8 = undefined; + _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); +} + +var device_id: u64 = 0; +var device: runtime.usb.Device = undefined; +var bulk_in: runtime.usb.Endpoint = undefined; +var bulk_out: runtime.usb.Endpoint = undefined; + +// DMA buffers for the transport: the 31-byte CBW, the 13-byte CSW, and a page +// for the small command data (INQUIRY / READ CAPACITY / the self-check sector). +var command_wrapper: dma.Region = undefined; +var status_wrapper: dma.Region = undefined; +var command_data: dma.Region = undefined; + +var next_tag: u32 = 1; +var block_size: u32 = 512; +var block_count: u64 = 0; + +/// One Bulk-Only-Transport command: send the CBW, run the data stage (to/from +/// `data_physical`), read and validate the CSW. Returns true on a passed status. +fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length: u32) bool { + const tag = next_tag; + next_tag +%= 1; + + const wrapper: *bot.CommandBlockWrapper = @ptrFromInt(command_wrapper.virtual); + wrapper.* = .{ + .tag = tag, + .data_transfer_length = data_length, + .flags = if (direction_in) bot.flag_data_in else 0, + .lun = 0, + .cdb_length = @intCast(cdb.len), + }; + @memcpy(wrapper.cdb[0..cdb.len], cdb); + + if (device.bulk(bulk_out.address, command_wrapper.physical, @sizeOf(bot.CommandBlockWrapper)) == null) return false; + if (data_length > 0) { + const endpoint = if (direction_in) bulk_in.address else bulk_out.address; + if (device.bulk(endpoint, data_physical, data_length) == null) return false; + } + if (device.bulk(bulk_in.address, status_wrapper.physical, @sizeOf(bot.CommandStatusWrapper)) == null) return false; + + const status: *const bot.CommandStatusWrapper = @ptrFromInt(status_wrapper.virtual); + if (status.signature != bot.csw_signature or status.tag != tag) return false; + return status.status == @intFromEnum(bot.CommandStatus.passed); +} + +fn initialise(endpoint: runtime.ipc.Handle) bool { + _ = endpoint; + if (!runtime.usb.helloManager(device_id)) { + _ = runtime.system.write("/system/drivers/usb-storage: hello to device manager failed\n"); + return false; + } + device = runtime.usb.open(device_id) orelse { + writeLine("/system/drivers/usb-storage: could not open device {d}\n", .{device_id}); + return false; + }; + bulk_in = device.findEndpoint(runtime.usb.transfer_type_bulk, true) orelse { + _ = runtime.system.write("/system/drivers/usb-storage: no bulk-IN endpoint\n"); + return false; + }; + bulk_out = device.findEndpoint(runtime.usb.transfer_type_bulk, false) orelse { + _ = runtime.system.write("/system/drivers/usb-storage: no bulk-OUT endpoint\n"); + return false; + }; + command_wrapper = dma.alloc(4096, dma.coherent) orelse return false; + status_wrapper = dma.alloc(4096, dma.coherent) orelse return false; + command_data = dma.alloc(4096, dma.coherent) orelse return false; + + // Bring the LUN up: wait for it to be ready (clearing the initial unit-attention + // with REQUEST SENSE), identify it, and read its capacity. + var tries: u32 = 0; + while (tries < 10) : (tries += 1) { + const ready = scsi.testUnitReady(); + if (transact(&ready, false, 0, 0)) break; + const sense = scsi.requestSense(18); + _ = transact(&sense, true, command_data.physical, 18); + runtime.system.sleep(50); + } + const inquiry = scsi.inquiry(36); + _ = transact(&inquiry, true, command_data.physical, 36); + + const capacity_command = scsi.readCapacity10(); + if (!transact(&capacity_command, true, command_data.physical, 8)) { + _ = runtime.system.write("/system/drivers/usb-storage: READ CAPACITY failed\n"); + return false; + } + var capacity_bytes: [8]u8 = undefined; + const capacity_source: [*]const u8 = @ptrFromInt(command_data.virtual); + @memcpy(&capacity_bytes, capacity_source[0..8]); + const capacity = scsi.parseCapacity(capacity_bytes); + block_size = capacity.block_size; + block_count = @as(u64, capacity.last_lba) + 1; + writeLine("/system/drivers/usb-storage: ready ({d} blocks x {d} bytes)\n", .{ block_count, block_size }); + + // Self-check: read block 0 and log its trailing signature (0x55AA for a boot + // sector) — proof READ(10) works end to end over the bulk path. + const read0 = scsi.read10(0, 1); + if (block_size <= 4096 and transact(&read0, true, command_data.physical, block_size)) { + const sector: [*]const u8 = @ptrFromInt(command_data.virtual); + writeLine("/system/drivers/usb-storage: block 0 signature 0x{x:0>2}{x:0>2}\n", .{ sector[510], sector[511] }); + } + return true; +} + +/// Serve the block protocol: geometry, and whole-block read/write to/from the +/// caller's DMA buffer (named by physical address). +fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { + _ = sender; + _ = capability; + if (message.len < block_protocol.request_size) return 0; + const request = std.mem.bytesToValue(block_protocol.Request, message[0..block_protocol.request_size]); + switch (request.operation) { + @intFromEnum(block_protocol.Operation.geometry) => { + return writeReply(reply, .{ .status = 0, .block_size = block_size, .block_count = block_count }); + }, + @intFromEnum(block_protocol.Operation.read) => { + const count: u16 = @intCast(request.count); + const cdb = scsi.read10(@intCast(request.lba), count); + const ok = transact(&cdb, true, request.physical, request.count * block_size); + return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 }); + }, + @intFromEnum(block_protocol.Operation.write) => { + const count: u16 = @intCast(request.count); + const cdb = scsi.write10(@intCast(request.lba), count); + const ok = transact(&cdb, false, request.physical, request.count * block_size); + return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 }); + }, + else => return 0, + } +} + +fn writeReply(reply: []u8, value: block_protocol.Reply) usize { + const bytes = std.mem.asBytes(&value); + @memcpy(reply[0..bytes.len], bytes); + return bytes.len; +} + +pub fn main(init: runtime.process.Init) void { + const argument = init.arguments.get(1) orelse { + _ = runtime.system.write("/system/drivers/usb-storage: missing device id (argv[1])\n"); + return; + }; + device_id = std.fmt.parseInt(u64, argument, 10) catch { + writeLine("/system/drivers/usb-storage: malformed device id '{s}'\n", .{argument}); + return; + }; + runtime.service.run(block_protocol.message_maximum, .{ + .service = .block, + .init = initialise, + .on_message = onMessage, + }); +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; +} diff --git a/system/drivers/usb-xhci-bus/usb-transfer-protocol.zig b/system/drivers/usb-xhci-bus/usb-transfer-protocol.zig new file mode 100644 index 0000000..7f3d21d --- /dev/null +++ b/system/drivers/usb-xhci-bus/usb-transfer-protocol.zig @@ -0,0 +1,159 @@ +//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or +//! mass-storage driver) says to the xHCI bus driver over its well-known +//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware — +//! it reaches its device entirely through these messages, the way a PS/2 keyboard +//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by +//! `Operation`, the vfs-protocol / device-manager-protocol pattern. +//! +//! The shape: +//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over +//! its own endpoint (for asynchronous interrupt reports) and its assigned +//! device id, and receives a `device_token` plus its interface's endpoints. +//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when +//! it completes. Control data travels inline (descriptors, HID/MSC class +//! requests are all small); bulk data travels by **physical address** — the +//! class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has +//! to cross the 256-byte IPC boundary. +//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an +//! interrupt endpoint; each report the device produces is then pushed to the +//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`), +//! exactly how the input service delivers events. +//! +//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI. +//! A multi-controller machine would need a per-controller endpoint (the device +//! manager handing each class driver the right one); noted, not built. + +/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM). +pub const message_maximum: usize = 256; + +/// The largest inline control-transfer payload. Sized so a whole message +/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class +/// requests are all far smaller. +pub const max_inline_data: usize = 200; + +/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport` +/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are +/// 8 bytes, boot mouse reports 3–4. +pub const max_report_data: usize = 48; + +/// Endpoints per interface reported back in an open reply (a boot HID interface +/// has one interrupt endpoint, a mass-storage interface two bulk endpoints). +pub const max_reported_endpoints: usize = 4; + +pub const Operation = enum(u32) { + open = 0, + control = 1, + interrupt_subscribe = 2, + bulk = 3, +}; + +/// The endpoint facts a class driver needs, lifted from the endpoint descriptor +/// the bus driver already parsed during enumeration. +pub const Endpoint = extern struct { + /// EndpointDescriptor address: direction in bit 7, number in bits 3:0. + address: u8, + /// 0 control, 1 isochronous, 2 bulk, 3 interrupt. + transfer_type: u8, + max_packet_size: u16, + interval: u8, + reserved: [3]u8 = .{ 0, 0, 0 }, +}; + +/// open: the class driver's receive endpoint rides as the call's capability, and +/// `device_id` is the interface's assigned id (its argv[1]). +pub const OpenRequest = extern struct { + operation: u32 = @intFromEnum(Operation.open), + reserved: u32 = 0, + device_id: u64, +}; + +/// The answer to open: a token scoping every later request to this device, the +/// interface's class triple (a sanity check), and its endpoints. +pub const OpenReply = extern struct { + status: i32, + endpoint_count: u32, + device_token: u64, + interface_class: u8, + interface_subclass: u8, + interface_protocol: u8, + interface_number: u8, + reserved2: u32 = 0, + endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints, +}; + +/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`. +/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the +/// reply carries up to `data_length` bytes back. +pub const ControlRequest = extern struct { + operation: u32 = @intFromEnum(Operation.control), + reserved: u32 = 0, + device_token: u64, + setup: [8]u8, + direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT) + reserved2: u8 = 0, + data_length: u16, + reserved3: u32 = 0, + data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data, +}; + +pub const ControlReply = extern struct { + status: i32, // 0 success, negative on failure/stall + actual_length: u32, + data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data, +}; + +/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each +/// report the device returns is pushed to the caller's endpoint (handed over at +/// open) as an asynchronous `InterruptReport`. +pub const InterruptSubscribeRequest = extern struct { + operation: u32 = @intFromEnum(Operation.interrupt_subscribe), + reserved: u32 = 0, + device_token: u64, + endpoint_address: u8, + reserved2: u8 = 0, + max_length: u16, // bytes to request per poll (the endpoint's max packet size) +}; + +pub const InterruptSubscribeReply = extern struct { + status: i32, + reserved: u32 = 0, +}; + +/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own +/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk +/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT. +pub const BulkRequest = extern struct { + operation: u32 = @intFromEnum(Operation.bulk), + reserved: u32 = 0, + device_token: u64, + physical_address: u64, + length: u32, + endpoint_address: u8, + reserved2: u8 = 0, + reserved3: u16 = 0, +}; + +pub const BulkReply = extern struct { + status: i32, + actual_length: u32, +}; + +/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's +/// endpoint. `Received.isMessage()` is set; there is no reply owed. +pub const InterruptReport = extern struct { + device_token: u64, + endpoint_address: u8, + length: u8, + reserved: u16 = 0, + data: [max_report_data]u8 = [_]u8{0} ** max_report_data, +}; + +comptime { + const std = @import("std"); + // Every synchronous message must fit one IPC message; the async report must + // fit an ipc_send payload slot. + std.debug.assert(@sizeOf(ControlRequest) <= message_maximum); + std.debug.assert(@sizeOf(ControlReply) <= message_maximum); + std.debug.assert(@sizeOf(OpenReply) <= message_maximum); + std.debug.assert(@sizeOf(InterruptReport) <= 64); +} diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 9bef352..3a409c2 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -17,6 +17,52 @@ const std = @import("std"); const runtime = @import("runtime"); const protocol = runtime.device_manager_protocol; const device = runtime.device; +const usb_ids = @import("usb-ids"); +const usb_abi = @import("usb-abi"); +const transfer = @import("usb-transfer-protocol"); +const library = @import("usb-xhci-library.zig"); + +/// The controller engine (reset, rings, transfers), stood up in `initialise`. +var controller: ?library.Controller = null; + +/// This driver's service endpoint (registered as `.usb_bus`), where class-driver +/// requests, signals, and the interrupt-poll timer all arrive. +var service_endpoint: runtime.ipc.Handle = 0; + +/// How often the driver drains the event ring for interrupt reports (~125 Hz), +/// re-armed each tick. Frequent enough for responsive input. +const poll_interval_ms: u64 = 8; + +/// The class driver endpoints that opened each device, so interrupt reports can +/// be pushed back to them. Keyed by the device token (the interface's device id). +const Open = struct { + used: bool = false, + device_token: u64 = 0, + report_endpoint: usize = 0, +}; +var opens = [_]Open{.{}} ** 16; + +fn recordOpen(device_token: u64, report_endpoint: usize) void { + for (&opens) |*open| { + if (open.used and open.device_token == device_token) { + open.report_endpoint = report_endpoint; + return; + } + } + for (&opens) |*open| { + if (!open.used) { + open.* = .{ .used = true, .device_token = device_token, .report_endpoint = report_endpoint }; + return; + } + } +} + +fn reportEndpointFor(device_token: u64) ?usize { + for (&opens) |*open| { + if (open.used and open.device_token == device_token) return open.report_endpoint; + } + return null; +} /// Format one whole log line and emit it in a single `debug_write`, so /// concurrent instances (one per controller) can never interleave mid-line. @@ -31,7 +77,7 @@ var controller_id: u64 = protocol.no_device; /// manager. Any failure returns false: the process exits cleanly, which the /// manager reads as "meant to stop" — a missing assignment is not a crash loop. fn initialise(endpoint: runtime.ipc.Handle) bool { - _ = endpoint; + service_endpoint = endpoint; if (!device.claim(controller_id)) { writeLine("/system/drivers/usb-xhci-bus: unable to claim controller device {d}\n", .{controller_id}); return false; @@ -72,6 +118,26 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { return false; }; + // Bring the controller up: reset it, stand up the command and event rings, + // and start it running (the hardware half lives in usb-xhci-library.zig). + controller = library.Controller.init(register_base) orelse { + _ = runtime.system.write("/system/drivers/usb-xhci-bus: controller reset/bring-up failed\n"); + return false; + }; + writeLine("/system/drivers/usb-xhci-bus: controller running ({d} slots, {d}-byte contexts)\n", .{ + controller.?.max_slots, + controller.?.context_size, + }); + // The proof of life: a No-Op command round-trips the command ring, the event + // ring, the doorbell, and the cycle-bit bookkeeping. If this completes, the + // engine is sound; transfers build on exactly this machinery. + if (controller.?.noOpCommand()) { + _ = runtime.system.write("/system/drivers/usb-xhci-bus: command ring running (no-op ok)\n"); + } else { + _ = runtime.system.write("/system/drivers/usb-xhci-bus: no-op command did not complete\n"); + return false; + } + // The handshake: role, protocol version, assignment — inside the manager's // deadline (the lookup retries cover the manager still registering). var manager: ?runtime.ipc.Handle = null; @@ -97,17 +163,15 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { _ = runtime.system.write("/system/drivers/usb-xhci-bus: hello acknowledged\n"); scanPorts(h); + + // Arm the poll timer that drains interrupt reports from the event ring. It is + // re-armed on each tick in onNotification; class drivers subscribe later. + _ = runtime.system.timerOnce(service_endpoint, poll_interval_ms); return true; } var register_base: usize = 0; -/// One 32-bit volatile register read at `offset` from the mapped window. -fn readRegister(offset: usize) u32 { - const register: *volatile u32 = @ptrFromInt(register_base + offset); - return register.*; -} - /// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10) /// decoded to human names — the boot-log breadcrumb for what actually enumerated on /// a port, the USB analog of the pci-bus class-code line. A controller may redefine @@ -124,50 +188,217 @@ fn speedName(speed: u32) []const u8 { }; } -/// The root-hub port scan: read the capability registers for the port count -/// and the operational-register offset, then one PORTSC per port. The connect -/// bit (CCS) and the speed field reflect hardware state directly — no -/// controller reset or run needed to *see* the devices; driving them needs the -/// rings (the USB track). +/// The root-hub scan and enumeration: for each connected port, bring the device +/// up (reset → enable slot → address), read its descriptors, and register + +/// report one child per interface — carrying the interface's (class, subclass, +/// protocol) triple as identity, which is what the device manager matches a +/// class driver against. fn scanPorts(manager: runtime.ipc.Handle) void { - // Capability registers: CAPLENGTH is byte 0 of the first dword; HCSPARAMS1 - // carries MaxPorts in bits 31:24. - const capability_length = readRegister(0) & 0xFF; - const structural = readRegister(0x04); - const maximum_ports: u32 = structural >> 24; - writeLine("/system/drivers/usb-xhci-bus: {d} root-hub ports\n", .{maximum_ports}); + const engine = if (controller) |*c| c else { + _ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n"); + return; + }; + writeLine("/system/drivers/usb-xhci-bus: {d} root-hub ports\n", .{engine.max_ports}); - // PORTSC registers: operational base + 0x400 + 0x10 per port (1-based). var port: u32 = 1; var connected: u32 = 0; - while (port <= maximum_ports) : (port += 1) { - const port_status = readRegister(capability_length + 0x400 + 0x10 * (port - 1)); + while (port <= engine.max_ports) : (port += 1) { + const port_status = engine.portStatus(port); if (port_status & 1 == 0) continue; // CCS: nothing connected connected += 1; const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class writeLine("/system/drivers/usb-xhci-bus: port {d} connected — {s} (speed class {d})\n", .{ port, speedName(speed), speed }); - const report = protocol.ChildAdded{ - .parent = controller_id, - .bus_address = port, - .identity = speed, - }; - var reply: [protocol.message_maximum]u8 = undefined; - _ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch { - writeLine("/system/drivers/usb-xhci-bus: child report for port {d} failed\n", .{port}); + const usb_device = engine.setupDevice(port, speed) orelse { + writeLine("/system/drivers/usb-xhci-bus: port {d} device setup failed\n", .{port}); continue; }; + if (!engine.enumerate(usb_device)) { + writeLine("/system/drivers/usb-xhci-bus: port {d} enumeration failed\n", .{port}); + continue; + } + writeLine("/system/drivers/usb-xhci-bus: port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)\n", .{ + port, + usb_device.device_descriptor.vendor_id, + usb_device.device_descriptor.product_id, + usb_device.interface_count, + }); + + for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| { + // Record the id each interface was registered as, so a class driver + // opening the interface (by that id) resolves to it. + if (reportInterface(manager, port, interface.*)) |registered| { + interface.registered_device_id = registered; + } + } } if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n"); } -/// No bus protocol to serve yet — transfer requests arrive with the USB track. +/// Register one interface as a resource-less child of the controller and report +/// it to the device manager. The identity is the packed USB class triple, so the +/// manager can match a class driver (HID keyboard, mouse, mass storage); the +/// registered device id becomes that driver's argv[1] assignment. Returns the +/// registered device id, or null if registration or the report failed. +fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.InterfaceInfo) ?u64 { + const identity = usb_ids.packTriple(interface.class, interface.subclass, interface.protocol); + + // A USB device is reached through its controller, not by MMIO, so the child + // carries no resources; register() allows that. Its bus-local identity — the + // (port, interface) address, written as a short "PI" tag in + // the hid field — makes each interface a distinct kernel node (the register + // dedup keys on class/pci_class/hid/resources, all otherwise identical here) + // and keeps re-registration idempotent across a bus restart: the same port + // and interface always map back to the same device id. + var descriptor = std.mem.zeroes(device.DeviceDescriptor); + descriptor.class = @intFromEnum(device.DeviceClass.usb_device); + descriptor.pci_class = device.no_pci_class; + descriptor.resource_count = 0; + var hid_buffer: [8]u8 = undefined; + const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }) catch ""; + descriptor.hid_len = hid_text.len; + @memcpy(descriptor.hid[0..hid_text.len], hid_text); + const registered = device.register(controller_id, &descriptor) orelse { + writeLine("/system/drivers/usb-xhci-bus: register refused for port {d} interface {d}\n", .{ port, interface.number }); + return null; + }; + + const report = protocol.ChildAdded{ + .parent = controller_id, + .bus_address = (@as(u64, port) << 8) | interface.number, + .identity = identity, + .device_id = registered, + }; + var reply: [protocol.message_maximum]u8 = undefined; + _ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch { + writeLine("/system/drivers/usb-xhci-bus: child report for port {d} interface {d} failed\n", .{ port, interface.number }); + return null; + }; + writeLine("/system/drivers/usb-xhci-bus: port {d} interface {d} class {d}/{d}/{d} registered as device {d}\n", .{ + port, + interface.number, + interface.class, + interface.subclass, + interface.protocol, + registered, + }); + return registered; +} + +/// Serve the USB transfer protocol: a class driver opens its device, then issues +/// control / interrupt-subscribe / bulk requests against it. fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { - _ = message; - _ = reply; _ = sender; - _ = capability; - return 0; + if (message.len < 4) return 0; + const operation = std.mem.readInt(u32, message[0..4], .little); + return switch (operation) { + @intFromEnum(transfer.Operation.open) => handleOpen(message, reply, capability), + @intFromEnum(transfer.Operation.control) => handleControl(message, reply), + @intFromEnum(transfer.Operation.interrupt_subscribe) => handleSubscribe(message, reply), + @intFromEnum(transfer.Operation.bulk) => handleBulk(message, reply), + else => 0, + }; +} + +fn writeReply(reply: []u8, value: anytype) usize { + const bytes = std.mem.asBytes(&value); + @memcpy(reply[0..bytes.len], bytes); + return bytes.len; +} + +/// open: resolve the assigned device id to an interface, remember the caller's +/// endpoint (for interrupt reports), and answer with a device token + the +/// interface's endpoints so the class driver need not re-read the config. +fn handleOpen(message: []const u8, reply: []u8, capability: ?runtime.ipc.Handle) usize { + if (message.len < @sizeOf(transfer.OpenRequest)) return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 }); + const request = std.mem.bytesToValue(transfer.OpenRequest, message[0..@sizeOf(transfer.OpenRequest)]); + const engine = if (controller) |*c| c else return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 }); + const found = engine.findInterface(request.device_id) orelse return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 }); + + if (capability) |endpoint| recordOpen(request.device_id, endpoint); + + var open_reply = transfer.OpenReply{ + .status = 0, + .endpoint_count = found.interface.endpoint_count, + .device_token = request.device_id, + .interface_class = found.interface.class, + .interface_subclass = found.interface.subclass, + .interface_protocol = found.interface.protocol, + .interface_number = found.interface.number, + }; + const count = @min(found.interface.endpoint_count, transfer.max_reported_endpoints); + for (found.interface.endpoints[0..count], 0..) |endpoint, index| { + open_reply.endpoints[index] = .{ + .address = endpoint.address, + .transfer_type = endpoint.transfer_type, + .max_packet_size = endpoint.max_packet_size, + .interval = endpoint.interval, + }; + } + return writeReply(reply, open_reply); +} + +/// control: one EP0 control transfer, small data inline both ways. +fn handleControl(message: []const u8, reply: []u8) usize { + if (message.len < @sizeOf(transfer.ControlRequest)) return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 }); + const request = std.mem.bytesToValue(transfer.ControlRequest, message[0..@sizeOf(transfer.ControlRequest)]); + const engine = if (controller) |*c| c else return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 }); + const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 }); + + const setup = std.mem.bytesToValue(usb_abi.Request, &request.setup); + const direction_in = request.direction_in != 0; + const data_length = @min(request.data_length, transfer.max_inline_data); + var data: [transfer.max_inline_data]u8 = undefined; + if (!direction_in) @memcpy(data[0..data_length], request.data[0..data_length]); + + const ok = engine.controlTransfer(found.device, setup, data[0..data_length], direction_in); + var control_reply = transfer.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 }; + if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]); + return writeReply(reply, control_reply); +} + +/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously. +fn handleSubscribe(message: []const u8, reply: []u8) usize { + if (message.len < @sizeOf(transfer.InterruptSubscribeRequest)) return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 }); + const request = std.mem.bytesToValue(transfer.InterruptSubscribeRequest, message[0..@sizeOf(transfer.InterruptSubscribeRequest)]); + const engine = if (controller) |*c| c else return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 }); + const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 }); + const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 }); + const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 }); + const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint); + return writeReply(reply, transfer.InterruptSubscribeReply{ .status = if (ok) 0 else -1 }); +} + +/// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical +/// address), so sector-sized data never crosses IPC. +fn handleBulk(message: []const u8, reply: []u8) usize { + if (message.len < @sizeOf(transfer.BulkRequest)) return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 }); + const request = std.mem.bytesToValue(transfer.BulkRequest, message[0..@sizeOf(transfer.BulkRequest)]); + const engine = if (controller) |*c| c else return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 }); + const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 }); + const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 }); + const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length); + return writeReply(reply, transfer.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 }); +} + +/// The poll timer landed: drain any interrupt reports off the event ring and push +/// each to the class driver that subscribed, then re-arm the timer. +fn onNotification(badge: u64) void { + if (badge & runtime.ipc.notify_timer_bit == 0) return; + if (controller) |*engine| { + engine.pump(); + while (engine.takeReport()) |report| { + var message = transfer.InterruptReport{ + .device_token = report.device_token, + .endpoint_address = report.endpoint_address, + .length = @intCast(@min(report.length, transfer.max_report_data)), + }; + const n = @min(report.length, transfer.max_report_data); + @memcpy(message.data[0..n], report.data[0..n]); + _ = runtime.ipc.send(report.report_endpoint, std.mem.asBytes(&message)); + } + } + _ = runtime.system.timerOnce(service_endpoint, poll_interval_ms); } pub fn main(init: runtime.process.Init) void { @@ -179,9 +410,11 @@ pub fn main(init: runtime.process.Init) void { writeLine("/system/drivers/usb-xhci-bus: malformed controller device id '{s}'\n", .{argument}); return; }; - runtime.service.run(protocol.message_maximum, .{ + runtime.service.run(transfer.message_maximum, .{ + .service = .usb_bus, .init = initialise, .on_message = onMessage, + .on_notification = onNotification, }); } diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index e69de29..7d2f511 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -0,0 +1,1004 @@ +//! The xHCI (USB 3) host-controller engine: controller bring-up, the command +//! and event rings, device slots, and transfers. This is the hardware half of +//! the /system/drivers/usb-xhci-bus driver — usb-xhci-bus.zig is the ring-3 +//! process shell (claim, MMIO map, device-manager handshake, IPC dispatch) and +//! calls into a `Controller` here for everything that touches registers or DMA. +//! +//! References: the xHCI 1.2 specification (register layout §5, TRBs §6, the +//! bring-up sequence §4.2). QEMU's `qemu-xhci` is the target; where the spec +//! allows latitude the simplest compliant choice is taken (a single-segment +//! event ring, a single-segment command ring with a Link TRB, polled event +//! delivery rather than MSI — see the notes on each). +//! +//! Interrupt strategy: **polling**. The event ring is coherent DMA the CPU can +//! read directly, so enumeration spin-polls it and the running driver drains it +//! on a timer tick. `qemu-xhci` presents MSI-X (a BAR-resident table) while the +//! kernel's `msi_bind` returns a config-space MSI capability's (address, data); +//! rather than gamble on the capability layout, we poll — correct on QEMU and +//! any real controller, with a marked hook to add MSI once the poll path is +//! proven. + +const std = @import("std"); +const runtime = @import("runtime"); +const mmio = @import("mmio"); +const usb_abi = @import("usb-abi"); +const dma = runtime.dma; +const system = runtime.system; + +// --- register offsets ------------------------------------------------------- + +// Capability registers (at the mapped BAR base). +const cap_caplength = 0x00; // low byte: length of the capability region +const cap_hcsparams1 = 0x04; // MaxSlots[7:0], MaxIntrs[18:8], MaxPorts[31:24] +const cap_hcsparams2 = 0x08; // Max Scratchpad Buffers (Hi[25:21], Lo[31:27]) +const cap_hccparams1 = 0x10; // CSZ[2] = 64-byte contexts, xECP[31:16] +const cap_dboff = 0x14; // doorbell array offset (dword-aligned, bits[31:2]) +const cap_rtsoff = 0x18; // runtime register space offset (bits[31:5]) + +// Operational registers (at base + CAPLENGTH). +const op_usbcmd = 0x00; // R/S[0], HCRST[1], INTE[2] +const op_usbsts = 0x04; // HCH[0], EINT[3], PCD[4], CNR[11] +const op_dcbaap = 0x30; // Device Context Base Address Array Pointer (64-bit) +const op_config = 0x38; // MaxSlotsEn[7:0] +const op_crcr = 0x18; // Command Ring Control Register (64-bit) +const op_portsc_base = 0x400; // PORTSC[n] = op + 0x400 + 0x10*(n-1) +const op_portsc_stride = 0x10; + +// USBCMD / USBSTS bits. +const usbcmd_run = 1 << 0; +const usbcmd_reset = 1 << 1; +const usbcmd_interrupter_enable = 1 << 2; +const usbsts_halted = 1 << 0; +const usbsts_controller_not_ready = 1 << 11; + +// PORTSC bits (per port). Several are write-1-to-clear (PED and the change bits), +// so any read-modify-write must write those as 0 to avoid clobbering them. +const portsc_connected = 1 << 0; // CCS (current connect status, read-only) +const portsc_enabled = 1 << 1; // PED (write 1 disables — write 0 to preserve) +const portsc_reset = 1 << 4; // PR (write 1 to reset the port) +const portsc_power = 1 << 9; // PP (port power, read-write) +const portsc_reset_change = 1 << 21; // PRC (write 1 to clear) +const portsc_change_mask: u32 = 0x7F << 17; // CSC..CEC change bits (write 1 clears) +// The write-1-to-clear bits a read-modify-write must not disturb: PED + changes. +const portsc_write_1_to_clear: u32 = portsc_enabled | portsc_change_mask; + +// Runtime registers (at base + RTSOFF). Interrupter 0 lives at +0x20. +const runtime_interrupter0 = 0x20; +const interrupter_management = 0x00; // IMAN: IP[0], IE[1] +const interrupter_moderation = 0x04; // IMOD +const event_ring_segment_table_size = 0x08; // ERSTSZ +const event_ring_segment_table_base = 0x10; // ERSTBA (64-bit) +const event_ring_dequeue_pointer = 0x18; // ERDP (64-bit), EHB = bit 3 + +// A Transfer Request Block: 16 bytes, the unit of every ring. `control` carries +// the cycle bit (bit 0) and the TRB type (bits 15:10); the rest is type-specific. +pub const Trb = extern struct { + parameter: u64 = 0, + status: u32 = 0, + control: u32 = 0, +}; + +pub const TrbType = enum(u6) { + normal = 1, + setup_stage = 2, + data_stage = 3, + status_stage = 4, + link = 6, + enable_slot = 9, + address_device = 11, + configure_endpoint = 12, + evaluate_context = 13, + no_op_command = 23, + transfer_event = 32, + command_completion_event = 33, + port_status_change_event = 34, +}; + +// The completion code carried in a Command Completion / Transfer Event's status +// field (bits 31:24). Only the ones the driver reasons about are named. +pub const CompletionCode = enum(u8) { + invalid = 0, + success = 1, + short_packet = 13, + _, +}; + +const cycle_bit: u32 = 1 << 0; +const link_toggle_cycle: u32 = 1 << 1; + +fn trbControl(kind: TrbType, extra: u32) u32 { + return (@as(u32, @intFromEnum(kind)) << 10) | extra; +} + +fn trbType(control: u32) u6 { + return @truncate(control >> 10); +} + +fn completionCode(status: u32) u8 { + return @truncate(status >> 24); +} + +// One Event Ring Segment Table entry: base + size of a single event-ring segment. +const ErstEntry = extern struct { + ring_segment_base: u64 = 0, + ring_segment_size: u32 = 0, // low 16 bits used + reserved: u32 = 0, +}; + +const page_size = 4096; +const trbs_per_ring = page_size / @sizeOf(Trb); // 256 + +// A producer ring (command ring, or a transfer ring): a page of TRBs whose last +// entry is a Link TRB back to the start. `cycle` is the producer cycle state. +const ProducerRing = struct { + region: dma.Region, + enqueue_index: usize = 0, + cycle: bool = true, + + fn trbs(self: ProducerRing) [*]volatile Trb { + return @ptrFromInt(self.region.virtual); + } + + // Arm the Link TRB at the end of the ring to point back to the start, with + // the Toggle-Cycle bit so the producer/consumer cycle state flips on wrap. + fn installLink(self: *ProducerRing) void { + const link = &self.trbs()[trbs_per_ring - 1]; + link.parameter = self.region.physical; + link.status = 0; + link.control = trbControl(.link, link_toggle_cycle) | (if (self.cycle) cycle_bit else 0); + } + + // Enqueue `trb` (its cycle bit is set here), returning the physical address + // of the slot it landed in — the token a completion event echoes back. + fn push(self: *ProducerRing, trb: Trb) u64 { + const index = self.enqueue_index; + const slot = &self.trbs()[index]; + const control = (trb.control & ~cycle_bit) | (if (self.cycle) cycle_bit else 0); + // Fill the payload first, publish the cycle bit last: the controller + // treats the TRB as owned once the cycle bit matches, so `control` (which + // holds it) is written after `parameter`/`status`, with a barrier between. + slot.parameter = trb.parameter; + slot.status = trb.status; + mmio.wmb(); + slot.control = control; + const physical = self.region.physical + index * @sizeOf(Trb); + self.enqueue_index += 1; + if (self.enqueue_index == trbs_per_ring - 1) { + // Reached the Link slot: hand the ring back and flip the producer + // cycle. (Commands are infrequent — a boot enumeration issues a few + // dozen — so in practice the ring never wraps; this keeps it correct + // if it ever does.) + self.installLink(); + self.enqueue_index = 0; + self.cycle = !self.cycle; + } + return physical; + } +}; + +// The event ring: a single segment the controller fills and the driver drains. +// `cycle` is the consumer cycle state, flipped each time the dequeue wraps. +const EventRing = struct { + segment: dma.Region, + table: dma.Region, + dequeue_index: usize = 0, + cycle: bool = true, + + fn trbs(self: EventRing) [*]volatile Trb { + return @ptrFromInt(self.segment.virtual); + } +}; + +// The default max packet size for endpoint 0, by the PORTSC/Slot-Context speed +// class (1 Full, 2 Low, 3 High, 4/5 SuperSpeed[+]). A device may report a smaller +// one in its descriptor; QEMU's HID/MSC devices use these defaults. +fn defaultMaxPacketSize0(speed: u32) u32 { + return switch (speed) { + 2 => 8, // Low-speed + 1 => 8, // Full-speed (may be 8/16/32/64; 8 is the safe default) + 3 => 64, // High-speed + 4, 5 => 512, // SuperSpeed / SuperSpeedPlus + else => 8, + }; +} + +// The Doorbell Context Index of an endpoint: EP0 is 1, and endpoint number `n` +// with direction gives DCI = 2n + (IN ? 1 : 0). The classic off-by-one lives here. +pub fn doorbellContextIndex(endpoint_number: u8, direction_in: bool) u32 { + return 2 * @as(u32, endpoint_number) + @intFromBool(direction_in); +} + +// The xHCI Endpoint Context Interval field for an interrupt endpoint. High/Super +// speed encode the descriptor's bInterval as 2^(bInterval-1) microframes, so the +// field is bInterval-1 (clamped). Full/low speed use a conservative default (the +// exact microframe encoding is not needed for the polled boot devices QEMU shows). +fn intervalFor(speed: u32, b_interval: u8) u32 { + return switch (speed) { + 3, 4, 5 => if (b_interval == 0) 0 else @min(@as(u32, b_interval) - 1, 15), + else => 6, + }; +} + +// Upper bounds on what one device's active configuration describes. A boot +// keyboard or mouse has one interface with one interrupt endpoint; a flash drive +// has one interface with two bulk endpoints. Generous for those. +pub const max_interfaces = 4; +pub const max_endpoints_per_interface = 4; + +// The endpoint-descriptor facts a class driver needs to talk to an endpoint: its +// address (direction + number), transfer type, packet size, and poll interval. +pub const EndpointInfo = struct { + address: u8 = 0, // EndpointDescriptor.Address bit-cast (dir bit 7, number bits 3:0) + transfer_type: u8 = 0, // 0 control, 1 isochronous, 2 bulk, 3 interrupt + max_packet_size: u16 = 0, + interval: u8 = 0, +}; + +// One interface of a device's active configuration: its class triple and its +// endpoints (alternate setting 0 only — the boot devices have no alternates). +pub const InterfaceInfo = struct { + number: u8 = 0, + class: u8 = 0, + subclass: u8 = 0, + protocol: u8 = 0, + endpoint_count: u8 = 0, + endpoints: [max_endpoints_per_interface]EndpointInfo = [_]EndpointInfo{.{}} ** max_endpoints_per_interface, + // The kernel device id this interface was registered as (its class driver's + // argv[1]); a class driver opens the interface by presenting this id. + registered_device_id: u64 = 0, +}; + +// A transfer ring the driver has configured for one of a device's endpoints +// (interrupt or bulk), keyed by its Doorbell Context Index. +const ConfiguredEndpoint = struct { + dci: u32 = 0, + ring: ProducerRing = .{ .region = .{ .virtual = 0, .physical = 0 } }, +}; +const max_configured_endpoints = max_interfaces * max_endpoints_per_interface; + +// One addressed USB device behind this controller: its hardware slot, its EP0 +// (control) transfer ring, the DMA context + bounce buffer the control pipe uses, +// and the interfaces its active configuration describes. Endpoint (interrupt/ +// bulk) rings for those interfaces are added when a class driver opens it. +pub const Device = struct { + used: bool = false, + slot_id: u8 = 0, + port: u32 = 0, + speed: u32 = 0, + max_packet_size_0: u32 = 8, + input_context: dma.Region = .{ .virtual = 0, .physical = 0 }, + device_context: dma.Region = .{ .virtual = 0, .physical = 0 }, + ep0_ring: ProducerRing = .{ .region = .{ .virtual = 0, .physical = 0 } }, + // A page-sized bounce buffer for control-transfer data (descriptors are read + // here, then copied out to the caller). + control_buffer: dma.Region = .{ .virtual = 0, .physical = 0 }, + device_descriptor: usb_abi.DeviceDescriptor = std.mem.zeroes(usb_abi.DeviceDescriptor), + configuration_value: u8 = 0, + interface_count: u8 = 0, + interfaces: [max_interfaces]InterfaceInfo = [_]InterfaceInfo{.{}} ** max_interfaces, + // Transfer rings configured for this device's interrupt/bulk endpoints. + endpoint_ring_count: u8 = 0, + endpoint_rings: [max_configured_endpoints]ConfiguredEndpoint = [_]ConfiguredEndpoint{.{}} ** max_configured_endpoints, +}; + +// A standing interrupt-IN subscription: the endpoint's ring is kept armed with a +// Normal TRB pointing at `buffer`, and each report the device returns is copied +// into the controller's report queue for the bus layer to push to the subscriber. +const Subscription = struct { + active: bool = false, + slot_id: u8 = 0, + dci: u32 = 0, + endpoint_address: u8 = 0, + ring: *ProducerRing = undefined, + buffer: dma.Region = .{ .virtual = 0, .physical = 0 }, + max_length: u16 = 0, + armed_trb_physical: u64 = 0, + // The bus layer's per-subscription IPC state (opaque here): the class driver's + // device token and the endpoint handle its reports are sent to. + device_token: u64 = 0, + report_endpoint: usize = 0, +}; + +// One interrupt report waiting for the bus layer to push it to a subscriber. +pub const Report = struct { + report_endpoint: usize = 0, + device_token: u64 = 0, + endpoint_address: u8 = 0, + length: u16 = 0, + data: [64]u8 = [_]u8{0} ** 64, +}; + +// How many addressed devices this driver tracks at once. QEMU presents a handful +// (a keyboard, a mouse, a storage stick); a fuller machine would grow this. +const max_devices = 8; +const max_subscriptions = 8; +const report_queue_capacity = 16; + +pub const Controller = struct { + register_base: usize, + op_base: usize, + runtime_base: usize, + doorbell_base: usize, + max_slots: u32, + max_ports: u32, + context_size: usize, // 32 or 64 (CSZ) + + device_context_array: dma.Region, + command_ring: ProducerRing, + event_ring: EventRing, + devices: [max_devices]Device = [_]Device{.{}} ** max_devices, + subscriptions: [max_subscriptions]Subscription = [_]Subscription{.{}} ** max_subscriptions, + report_queue: [report_queue_capacity]Report = [_]Report{.{}} ** report_queue_capacity, + report_count: usize = 0, + // Transferred length of the most recent awaited transfer (requested minus the + // event residual); read right after a control or bulk transfer returns true. + last_transfer_length: u32 = 0, + + // --- register access --------------------------------------------------- + + fn read8(address: usize) u8 { + return @as(*volatile u8, @ptrFromInt(address)).*; + } + fn read32(address: usize) u32 { + return @as(*volatile u32, @ptrFromInt(address)).*; + } + fn write32(address: usize, value: u32) void { + @as(*volatile u32, @ptrFromInt(address)).* = value; + } + // xHCI 64-bit registers are safely accessed as an ordered pair of 32-bit + // writes (low dword first) — the portable form some controllers require. + fn write64(address: usize, value: u64) void { + write32(address, @truncate(value)); + write32(address + 4, @truncate(value >> 32)); + } + + fn operational(self: *const Controller, offset: usize) usize { + return self.op_base + offset; + } + fn interrupter(self: *const Controller, offset: usize) usize { + return self.runtime_base + runtime_interrupter0 + offset; + } + + // PORTSC for 1-based port `port`. + pub fn portStatus(self: *const Controller, port: u32) u32 { + return read32(self.op_base + op_portsc_base + op_portsc_stride * (port - 1)); + } + pub fn writePortStatus(self: *const Controller, port: u32, value: u32) void { + write32(self.op_base + op_portsc_base + op_portsc_stride * (port - 1), value); + } + + // --- bring-up ---------------------------------------------------------- + + /// Reset and start the controller, standing up the command and event rings. + /// Returns null on any failure (a wedged register handshake or an out-of-DMA + /// condition) — the caller treats that as a driver that could not start. + pub fn init(register_base: usize) ?Controller { + const cap_length = read32(register_base + cap_caplength) & 0xFF; + const hcsparams1 = read32(register_base + cap_hcsparams1); + const hccparams1 = read32(register_base + cap_hccparams1); + const dboff = read32(register_base + cap_dboff) & ~@as(u32, 0x3); + const rtsoff = read32(register_base + cap_rtsoff) & ~@as(u32, 0x1F); + + var self = Controller{ + .register_base = register_base, + .op_base = register_base + cap_length, + .runtime_base = register_base + rtsoff, + .doorbell_base = register_base + dboff, + .max_slots = hcsparams1 & 0xFF, + .max_ports = hcsparams1 >> 24, + .context_size = if (hccparams1 & (1 << 2) != 0) 64 else 32, + .device_context_array = undefined, + .command_ring = undefined, + .event_ring = undefined, + }; + + // Wait for the controller to report ready, then halt it if it is running. + if (!waitClear(self.operational(op_usbsts), usbsts_controller_not_ready)) return null; + if (read32(self.operational(op_usbcmd)) & usbcmd_run != 0) { + write32(self.operational(op_usbcmd), read32(self.operational(op_usbcmd)) & ~@as(u32, usbcmd_run)); + if (!waitSet(self.operational(op_usbsts), usbsts_halted)) return null; + } + + // Reset. HCRST self-clears when the reset completes; then CNR clears. + write32(self.operational(op_usbcmd), usbcmd_reset); + if (!waitClear(self.operational(op_usbcmd), usbcmd_reset)) return null; + if (!waitClear(self.operational(op_usbsts), usbsts_controller_not_ready)) return null; + + // Enable all device slots the controller supports. + write32(self.operational(op_config), self.max_slots); + + // The Device Context Base Address Array (entry 0 = scratchpad array). + self.device_context_array = dma.alloc(page_size, dma.coherent) orelse return null; + self.setupScratchpad(register_base); + write64(self.operational(op_dcbaap), self.device_context_array.physical); + + // The command ring: a page of TRBs, last entry a Link back to the start. + self.command_ring = .{ .region = dma.alloc(page_size, dma.coherent) orelse return null }; + self.command_ring.installLink(); + write64(self.operational(op_crcr), self.command_ring.region.physical | cycle_bit); + + // The event ring: one segment + a one-entry segment table. + self.event_ring = .{ + .segment = dma.alloc(page_size, dma.coherent) orelse return null, + .table = dma.alloc(page_size, dma.coherent) orelse return null, + }; + const table: *volatile ErstEntry = @ptrFromInt(self.event_ring.table.virtual); + table.ring_segment_base = self.event_ring.segment.physical; + table.ring_segment_size = trbs_per_ring; + write32(self.interrupter(event_ring_segment_table_size), 1); + write64(self.interrupter(event_ring_dequeue_pointer), self.event_ring.segment.physical); + write64(self.interrupter(event_ring_segment_table_base), self.event_ring.table.physical); + write32(self.interrupter(interrupter_moderation), 0); + + // Run. (Interrupts are left disabled — the event ring is polled.) + mmio.wmb(); + write32(self.operational(op_usbcmd), read32(self.operational(op_usbcmd)) | usbcmd_run); + if (!waitClear(self.operational(op_usbsts), usbsts_halted)) return null; + return self; + } + + // Scratchpad buffers the controller asks the host to reserve for its own use. + // Entry 0 of the device-context array points at an array of their physical + // addresses. QEMU usually requests none, in which case DCBAA[0] stays zero. + fn setupScratchpad(self: *Controller, register_base: usize) void { + const hcsparams2 = read32(register_base + cap_hcsparams2); + const high = (hcsparams2 >> 21) & 0x1F; + const low = (hcsparams2 >> 27) & 0x1F; + const count = (high << 5) | low; + const array: [*]volatile u64 = @ptrFromInt(self.device_context_array.virtual); + if (count == 0) { + array[0] = 0; + return; + } + // One page per scratchpad buffer, plus a page holding their address array. + const pointers = dma.alloc(page_size, dma.coherent) orelse return; + const pointer_array: [*]volatile u64 = @ptrFromInt(pointers.virtual); + var index: u32 = 0; + while (index < count) : (index += 1) { + const buffer = dma.alloc(page_size, dma.coherent) orelse return; + pointer_array[index] = buffer.physical; + } + array[0] = pointers.physical; + } + + // Spin (with a deadline) until every bit in `mask` reads back as zero / one. + fn waitClear(address: usize, mask: u32) bool { + const deadline = system.clock() + 1_000_000_000; // 1 s + while (read32(address) & mask != 0) { + if (system.clock() >= deadline) return false; + } + return true; + } + fn waitSet(address: usize, mask: u32) bool { + const deadline = system.clock() + 1_000_000_000; + while (read32(address) & mask == 0) { + if (system.clock() >= deadline) return false; + } + return true; + } + + // --- rings ------------------------------------------------------------- + + fn ringDoorbell(self: *const Controller, slot: u32, target: u32) void { + write32(self.doorbell_base + slot * 4, target); + } + + /// Enqueue a command TRB, ring the command doorbell, and return the physical + /// address of the enqueued TRB (which the Command Completion Event echoes). + fn submitCommand(self: *Controller, trb: Trb) u64 { + const physical = self.command_ring.push(trb); + mmio.wmb(); + self.ringDoorbell(0, 0); // doorbell 0, target 0 = command ring + return physical; + } + + /// Consume the next event, or null if none has arrived by `deadline_ns`. + fn nextEvent(self: *Controller, deadline_ns: u64) ?Trb { + const ring = self.event_ring.trbs(); + while (true) { + const slot = &ring[self.event_ring.dequeue_index]; + const control = slot.control; + if ((control & cycle_bit != 0) == self.event_ring.cycle) { + mmio.rmb(); + const event = Trb{ .parameter = slot.parameter, .status = slot.status, .control = control }; + self.event_ring.dequeue_index += 1; + if (self.event_ring.dequeue_index >= trbs_per_ring) { + self.event_ring.dequeue_index = 0; + self.event_ring.cycle = !self.event_ring.cycle; + } + const dequeue = self.event_ring.segment.physical + self.event_ring.dequeue_index * @sizeOf(Trb); + // Write the new dequeue pointer and clear the Event Handler Busy bit. + write64(self.interrupter(event_ring_dequeue_pointer), dequeue | (1 << 3)); + return event; + } + if (system.clock() >= deadline_ns) return null; + } + } + + /// Wait for the Command Completion Event matching `command_physical`. Interrupt + /// reports that land while waiting are dispatched (so a standing subscription is + /// serviced even during a command); other events are ignored. Returns the + /// completion code, or null on timeout. + fn awaitCommand(self: *Controller, command_physical: u64) ?u8 { + const deadline = system.clock() + 1_000_000_000; + while (true) { + const event = self.nextEvent(deadline) orelse return null; + const kind = trbType(event.control); + if (kind == @intFromEnum(TrbType.command_completion_event) and + (event.parameter & ~@as(u64, 0xF)) == command_physical) + { + return completionCode(event.status); + } + if (kind == @intFromEnum(TrbType.transfer_event)) _ = self.serviceInterruptEvent(event); + } + } + + /// A No-Op Command: the cheapest end-to-end proof that reset, the command + /// ring, the event ring, the doorbell, and the cycle-bit bookkeeping are all + /// correct. Returns true if the controller completed it with success. + pub fn noOpCommand(self: *Controller) bool { + const physical = self.submitCommand(.{ .control = trbControl(.no_op_command, 0) }); + const code = self.awaitCommand(physical) orelse return false; + return code == @intFromEnum(CompletionCode.success); + } + + // --- device slots + control transfers ---------------------------------- + + /// Reset the given 1-based root-hub port and wait for it to enable. A port + /// must be reset before the device on it can be addressed. Returns false if + /// the reset does not complete or the port does not enable. + pub fn resetPort(self: *const Controller, port: u32) bool { + // Set PR while writing 0 to every write-1-to-clear bit (so the change + // bits and PED are untouched) and preserving PP. + const before = self.portStatus(port); + self.writePortStatus(port, (before & ~portsc_write_1_to_clear) | portsc_reset); + const deadline = system.clock() + 500_000_000; + while (self.portStatus(port) & portsc_reset_change == 0) { + if (system.clock() >= deadline) return false; + } + // Clear the Port Reset Change bit (write 1 to PRC, 0 to the rest). + const after = self.portStatus(port); + self.writePortStatus(port, (after & ~portsc_write_1_to_clear) | portsc_reset_change); + return self.portStatus(port) & portsc_enabled != 0; + } + + /// Issue an Enable Slot command and return the slot id the controller + /// assigned (carried in bits 31:24 of the completion event's control field). + fn enableSlot(self: *Controller) ?u8 { + const physical = self.submitCommand(.{ .control = trbControl(.enable_slot, 0) }); + const deadline = system.clock() + 1_000_000_000; + while (true) { + const event = self.nextEvent(deadline) orelse return null; + if (trbType(event.control) == @intFromEnum(TrbType.command_completion_event) and + (event.parameter & ~@as(u64, 0xF)) == physical) + { + if (completionCode(event.status) != @intFromEnum(CompletionCode.success)) return null; + return @truncate(event.control >> 24); + } + } + } + + fn allocateDevice(self: *Controller) ?*Device { + for (&self.devices) |*device| { + if (!device.used) return device; + } + return null; + } + + // A pointer to dword `dword_index` of context `context_index` within a context + // array at `virtual`. Contexts are strided by `context_size` (32 or 64), so + // the meaningful first 8 dwords sit at the base of each stride. + fn contextDword(virtual: usize, context_index: usize, dword_index: usize, context_size: usize) *volatile u32 { + return @ptrFromInt(virtual + context_index * context_size + dword_index * 4); + } + + // Build the Input Context for Address Device: Input Control Context add-flags + // A0 (slot) | A1 (EP0), a Slot Context (route 0, speed, one context entry, + // root-hub port), and an EP0 Control endpoint context pointing at the device's + // EP0 ring. Contexts start zeroed (the DMA region is), so only set fields. + fn buildAddressInputContext(self: *Controller, device: *Device) void { + const cs = self.context_size; + const base = device.input_context.virtual; + // Input Control Context (index 0): Add flags in dword 1 = A0 | A1. + contextDword(base, 0, 1, cs).* = 0b11; + // Slot Context (index 1): speed[23:20], Context Entries[31:27] = 1. + contextDword(base, 1, 0, cs).* = (device.speed << 20) | (@as(u32, 1) << 27); + // Root Hub Port Number[23:16]. + contextDword(base, 1, 1, cs).* = device.port << 16; + // EP0 Context (index 2): CErr[2:1]=3, EP Type[5:3]=Control(4), MPS[31:16]. + contextDword(base, 2, 1, cs).* = (@as(u32, 3) << 1) | (@as(u32, 4) << 3) | (device.max_packet_size_0 << 16); + // TR Dequeue Pointer (dwords 2:3) with Dequeue Cycle State = 1. + const dequeue = device.ep0_ring.region.physical | 1; + contextDword(base, 2, 2, cs).* = @truncate(dequeue); + contextDword(base, 2, 3, cs).* = @truncate(dequeue >> 32); + // Average TRB Length (dword 4): 8 is the conventional value for control. + contextDword(base, 2, 4, cs).* = 8; + } + + 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 return false; + return code == @intFromEnum(CompletionCode.success); + } + + /// Reset the port, enable a slot, and address the device on it: after this the + /// device answers control transfers on its EP0. Returns the tracked `Device`, + /// 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; + device.* = .{ + .used = true, + .slot_id = slot_id, + .port = port, + .speed = speed, + .max_packet_size_0 = defaultMaxPacketSize0(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); + device.ep0_ring = .{ .region = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device) }; + device.ep0_ring.installLink(); + device.control_buffer = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device); + + self.buildAddressInputContext(device); + const array: [*]volatile u64 = @ptrFromInt(self.device_context_array.virtual); + array[device.slot_id] = device.device_context.physical; + if (!self.addressDeviceCommand(device)) return self.abandon(device); + return device; + } + + fn abandon(self: *Controller, device: *Device) ?*Device { + _ = self; + device.used = false; + return null; + } + + fn awaitTransfer(self: *Controller, requested_length: u32) ?u8 { + const deadline = system.clock() + 1_000_000_000; + while (true) { + const event = self.nextEvent(deadline) orelse return null; + if (trbType(event.control) == @intFromEnum(TrbType.transfer_event)) { + if (self.serviceInterruptEvent(event)) continue; // a subscription's report + const residual = event.status & 0xFFFFFF; + self.last_transfer_length = if (residual >= requested_length) 0 else requested_length - residual; + return completionCode(event.status); // our transfer's completion (or error) + } + } + } + + /// Run one control transfer on a device's EP0: a Setup stage (the 8-byte + /// request inline), an optional Data stage through the device's bounce buffer, + /// and a Status stage. For an IN transfer the returned data lands in `data`; + /// for an OUT transfer `data` is sent. Returns false on any failure or stall. + pub fn controlTransfer(self: *Controller, device: *Device, request: usb_abi.Request, data: []u8, direction_in: bool) bool { + const has_data = data.len > 0; + const transfer_type: u32 = if (!has_data) 0 else if (direction_in) 3 else 2; // TRT: none/OUT/IN + + // Setup Stage: the 8-byte setup packet inline (IDT), TRT in bits 17:16. + const setup_bytes: u64 = @bitCast(request); + _ = device.ep0_ring.push(.{ + .parameter = setup_bytes, + .status = 8, + .control = trbControl(.setup_stage, (1 << 6) | (transfer_type << 16)), + }); + + if (has_data) { + if (!direction_in) { + const buffer: [*]u8 = @ptrFromInt(device.control_buffer.virtual); + @memcpy(buffer[0..data.len], data); + } + _ = device.ep0_ring.push(.{ + .parameter = device.control_buffer.physical, + .status = @intCast(data.len), + .control = trbControl(.data_stage, if (direction_in) (1 << 16) else 0), // DIR bit 16 + }); + } + + // Status Stage: opposite direction to the data (IN when there was no data + // or the data was OUT), Interrupt On Completion so we get one event. + const status_direction: u32 = if (has_data and direction_in) 0 else (1 << 16); + _ = device.ep0_ring.push(.{ + .control = trbControl(.status_stage, status_direction | (1 << 5)), // DIR | IOC + }); + + mmio.wmb(); + self.ringDoorbell(device.slot_id, 1); // DCI 1 = EP0 + const code = self.awaitTransfer(@intCast(data.len)) orelse return false; + if (code != @intFromEnum(CompletionCode.success) and code != @intFromEnum(CompletionCode.short_packet)) return false; + + if (has_data and direction_in) { + const buffer: [*]u8 = @ptrFromInt(device.control_buffer.virtual); + @memcpy(data, buffer[0..data.len]); + } + return true; + } + + /// Read a device's 18-byte DEVICE descriptor over its control pipe. + pub fn getDeviceDescriptor(self: *Controller, device: *Device) ?usb_abi.DeviceDescriptor { + var bytes: [18]u8 = undefined; + const request = usb_abi.getDescriptor(.device, 0, 0, 18); + if (!self.controlTransfer(device, request, bytes[0..], true)) return null; + return std.mem.bytesToValue(usb_abi.DeviceDescriptor, &bytes); + } + + /// Full chapter-9 enumeration of an addressed device: read the device and + /// configuration descriptors, parse the configuration's interfaces and + /// 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. + pub fn enumerate(self: *Controller, device: *Device) bool { + device.device_descriptor = self.getDeviceDescriptor(device) orelse return false; + + // The configuration descriptor's own 9 bytes carry the total length of + // the whole configuration block (interfaces + endpoints follow it). + var header: [9]u8 = undefined; + if (!self.controlTransfer(device, usb_abi.getDescriptor(.configuration, 0, 0, 9), header[0..], true)) return false; + const configuration = std.mem.bytesToValue(usb_abi.ConfigurationDescriptor, &header); + device.configuration_value = @intFromEnum(configuration.configuration_value); + + // Read the whole block into a local buffer and parse it here (in the bus + // driver) so the parse never has to cross the 256-byte IPC boundary. + var blob: [512]u8 = undefined; + const length = @min(configuration.total_length, blob.len); + if (!self.controlTransfer(device, usb_abi.getDescriptor(.configuration, 0, 0, @intCast(length)), blob[0..length], true)) return false; + parseConfiguration(device, blob[0..length]); + + // Select the configuration, moving the device to the configured state. + if (!self.controlTransfer(device, usb_abi.setConfiguration(configuration.configuration_value), &.{}, false)) return false; + return true; + } + + // Walk a configuration block, recording each interface (alternate setting 0) + // and the endpoints that follow it. Endpoints belong to the most recent + // interface. Unknown descriptor types (HID, class-specific) are skipped by + // their length. + fn parseConfiguration(device: *Device, blob: []const u8) void { + device.interface_count = 0; + var current: ?*InterfaceInfo = null; + var offset: usize = 0; + while (offset + 2 <= blob.len) { + const length = blob[offset]; + const descriptor_type = blob[offset + 1]; + if (length < 2 or offset + length > blob.len) break; + switch (@as(usb_abi.DescriptorType, @enumFromInt(descriptor_type))) { + .interface => if (length >= @sizeOf(usb_abi.InterfaceDescriptor)) { + const descriptor = std.mem.bytesToValue(usb_abi.InterfaceDescriptor, blob[offset .. offset + @sizeOf(usb_abi.InterfaceDescriptor)]); + if (@intFromEnum(descriptor.alternate_setting) != 0) { + current = null; // ignore alternate settings for now + } else if (device.interface_count < max_interfaces) { + const slot = &device.interfaces[device.interface_count]; + slot.* = .{ + .number = @intFromEnum(descriptor.interface_number), + .class = descriptor.interface_class, + .subclass = descriptor.interface_subclass, + .protocol = descriptor.interface_protocol, + }; + current = slot; + device.interface_count += 1; + } + }, + .endpoint => if (length >= @sizeOf(usb_abi.EndpointDescriptor)) { + if (current) |interface| { + if (interface.endpoint_count < max_endpoints_per_interface) { + const descriptor = std.mem.bytesToValue(usb_abi.EndpointDescriptor, blob[offset .. offset + @sizeOf(usb_abi.EndpointDescriptor)]); + interface.endpoints[interface.endpoint_count] = .{ + .address = @bitCast(descriptor.endpoint_address), + .transfer_type = @intFromEnum(descriptor.attributes.transfer_type), + .max_packet_size = descriptor.max_packet_size.size, + .interval = descriptor.interval, + }; + interface.endpoint_count += 1; + } + } + }, + else => {}, + } + offset += length; + } + } + + // --- endpoint configuration + interrupt / bulk transfers --------------- + + /// Find the tracked device and interface an assigned device id belongs to. + pub fn findInterface(self: *Controller, device_id: u64) ?struct { device: *Device, interface: *InterfaceInfo } { + for (&self.devices) |*device| { + if (!device.used) continue; + for (device.interfaces[0..device.interface_count]) |*interface| { + if (interface.registered_device_id == device_id) return .{ .device = device, .interface = interface }; + } + } + return null; + } + + /// The endpoint of `interface` with the given address, or null. + pub fn endpointForAddress(interface: *const InterfaceInfo, address: u8) ?EndpointInfo { + for (interface.endpoints[0..interface.endpoint_count]) |endpoint| { + if (endpoint.address == address) return endpoint; + } + return null; + } + + fn findEndpointRing(device: *Device, dci: u32) ?*ProducerRing { + for (device.endpoint_rings[0..device.endpoint_ring_count]) |*configured| { + if (configured.dci == dci) return &configured.ring; + } + return null; + } + + // Get (configuring on first use) the transfer ring for an endpoint. The first + // use issues a Configure Endpoint command that adds the endpoint context to the + // device and points it at a fresh ring. + fn getOrConfigureEndpoint(self: *Controller, device: *Device, endpoint: EndpointInfo) ?*ProducerRing { + const number: u8 = endpoint.address & 0x0F; + const direction_in = endpoint.address & 0x80 != 0; + const dci = doorbellContextIndex(number, direction_in); + if (findEndpointRing(device, dci)) |ring| return ring; + if (device.endpoint_ring_count >= device.endpoint_rings.len) return null; + + const configured = &device.endpoint_rings[device.endpoint_ring_count]; + configured.dci = dci; + configured.ring = .{ .region = dma.alloc(page_size, dma.coherent) orelse return null }; + configured.ring.installLink(); + self.buildConfigureEndpointInputContext(device, endpoint, dci, &configured.ring); + if (!self.configureEndpointCommand(device)) return null; + device.endpoint_ring_count += 1; + return &configured.ring; + } + + // Build the Input Context for a Configure Endpoint command adding one endpoint: + // Add flags A0 (slot) | A(dci), a Slot Context whose Context Entries covers the + // new endpoint, and the endpoint context (type, packet size, ring, interval). + fn buildConfigureEndpointInputContext(self: *Controller, device: *Device, endpoint: EndpointInfo, dci: u32, ring: *const ProducerRing) void { + const cs = self.context_size; + const base = device.input_context.virtual; + // Zero the contexts we touch (the region last held the Address Device input). + const dwords: [*]volatile u32 = @ptrFromInt(base); + const touched = (2 + @as(usize, dci)) * (cs / 4); + var i: usize = 0; + while (i < touched) : (i += 1) dwords[i] = 0; + + // Input Control Context: Add flags A0 (slot) | A(dci) (the endpoint). + contextDword(base, 0, 1, cs).* = (@as(u32, 1) << 0) | (@as(u32, 1) << @as(u5, @intCast(dci))); + // Slot Context: speed, Context Entries = dci, root hub port. + contextDword(base, 1, 0, cs).* = (device.speed << 20) | (dci << 27); + contextDword(base, 1, 1, cs).* = device.port << 16; + // Endpoint Context at index (1 + dci). + const direction_in = endpoint.address & 0x80 != 0; + const endpoint_type = @as(u32, endpoint.transfer_type) + (if (direction_in) @as(u32, 4) else 0); + const interval = if (endpoint.transfer_type == 3) intervalFor(device.speed, endpoint.interval) else 0; + contextDword(base, 1 + @as(usize, dci), 0, cs).* = interval << 16; // Interval bits 23:16 + contextDword(base, 1 + @as(usize, dci), 1, cs).* = (@as(u32, 3) << 1) | (endpoint_type << 3) | (@as(u32, endpoint.max_packet_size) << 16); + const dequeue = ring.region.physical | 1; + contextDword(base, 1 + @as(usize, dci), 2, cs).* = @truncate(dequeue); + contextDword(base, 1 + @as(usize, dci), 3, cs).* = @truncate(dequeue >> 32); + contextDword(base, 1 + @as(usize, dci), 4, cs).* = endpoint.max_packet_size; // Average TRB Length + } + + fn configureEndpointCommand(self: *Controller, device: *Device) bool { + const physical = self.submitCommand(.{ + .parameter = device.input_context.physical, + .control = trbControl(.configure_endpoint, @as(u32, device.slot_id) << 24), + }); + const code = self.awaitCommand(physical) orelse return false; + return code == @intFromEnum(CompletionCode.success); + } + + /// One bulk transfer (IN or OUT per the endpoint address's direction bit) to or + /// from a caller-owned DMA buffer at `physical`. Returns the number of bytes + /// transferred, or null on failure/stall. The data never crosses IPC. + pub fn bulkTransfer(self: *Controller, device: *Device, endpoint: EndpointInfo, physical: u64, length: u32) ?u32 { + const ring = self.getOrConfigureEndpoint(device, endpoint) orelse return null; + _ = ring.push(.{ + .parameter = physical, + .status = length, + .control = trbControl(.normal, (1 << 5)), // IOC + }); + mmio.wmb(); + const number: u8 = endpoint.address & 0x0F; + const direction_in = endpoint.address & 0x80 != 0; + self.ringDoorbell(device.slot_id, doorbellContextIndex(number, direction_in)); + const code = self.awaitTransfer(length) orelse return null; + if (code != @intFromEnum(CompletionCode.success) and code != @intFromEnum(CompletionCode.short_packet)) return null; + return self.last_transfer_length; + } + + fn allocateSubscription(self: *Controller) ?*Subscription { + for (&self.subscriptions) |*subscription| { + if (!subscription.active) return subscription; + } + return null; + } + + /// Start periodic IN polling of an interrupt endpoint. Each report the device + /// returns is queued (see `takeReport`), tagged with `device_token` and + /// `report_endpoint` so the bus layer can push it to the subscriber. Returns + /// false if the endpoint cannot be configured or no subscription slot is free. + pub fn subscribeInterrupt(self: *Controller, device: *Device, endpoint: EndpointInfo, device_token: u64, report_endpoint: usize) bool { + const ring = self.getOrConfigureEndpoint(device, endpoint) orelse return false; + const subscription = self.allocateSubscription() orelse return false; + const buffer = dma.alloc(page_size, dma.coherent) orelse return false; + const number: u8 = endpoint.address & 0x0F; + const direction_in = endpoint.address & 0x80 != 0; + subscription.* = .{ + .active = true, + .slot_id = device.slot_id, + .dci = doorbellContextIndex(number, direction_in), + .endpoint_address = endpoint.address, + .ring = ring, + .buffer = buffer, + .max_length = endpoint.max_packet_size, + .device_token = device_token, + .report_endpoint = report_endpoint, + }; + self.armInterrupt(subscription); + return true; + } + + // Arm (or re-arm) a subscription's endpoint with a Normal TRB pointing at its + // report buffer, and ring the endpoint's doorbell so the controller polls it. + fn armInterrupt(self: *Controller, subscription: *Subscription) void { + subscription.armed_trb_physical = subscription.ring.push(.{ + .parameter = subscription.buffer.physical, + .status = subscription.max_length, + .control = trbControl(.normal, (1 << 5)), // IOC + }); + mmio.wmb(); + self.ringDoorbell(subscription.slot_id, subscription.dci); + } + + // A transfer event came off the ring: if it completes a subscription's armed + // interrupt transfer, copy the report into the queue and re-arm. Returns whether + // it belonged to a subscription (so the awaiting caller knows it was consumed). + fn serviceInterruptEvent(self: *Controller, event: Trb) bool { + const trb_pointer = event.parameter & ~@as(u64, 0xF); + for (&self.subscriptions) |*subscription| { + if (!subscription.active or subscription.armed_trb_physical != trb_pointer) continue; + const code = completionCode(event.status); + if (code == @intFromEnum(CompletionCode.success) or code == @intFromEnum(CompletionCode.short_packet)) { + const residual = event.status & 0xFFFFFF; + const transferred: u16 = if (residual >= subscription.max_length) 0 else @intCast(subscription.max_length - residual); + self.enqueueReport(subscription, transferred); + } + self.armInterrupt(subscription); // keep polling + return true; + } + return false; + } + + fn enqueueReport(self: *Controller, subscription: *Subscription, length: u16) void { + if (self.report_count >= self.report_queue.len) return; // full: drop the newest + const report = &self.report_queue[self.report_count]; + report.report_endpoint = subscription.report_endpoint; + report.device_token = subscription.device_token; + report.endpoint_address = subscription.endpoint_address; + report.length = length; + const source: [*]const u8 = @ptrFromInt(subscription.buffer.virtual); + const n = @min(length, report.data.len); + @memcpy(report.data[0..n], source[0..n]); + self.report_count += 1; + } + + /// Dequeue the oldest queued interrupt report, or null if none. + pub fn takeReport(self: *Controller) ?Report { + if (self.report_count == 0) return null; + const report = self.report_queue[0]; + var i: usize = 1; + while (i < self.report_count) : (i += 1) self.report_queue[i - 1] = self.report_queue[i]; + self.report_count -= 1; + return report; + } + + /// Drain any events currently on the event ring, dispatching interrupt reports + /// into the queue. Non-blocking — called on the driver's timer tick. + pub fn pump(self: *Controller) void { + while (true) { + const event = self.nextEvent(system.clock()) orelse return; // deadline=now: null when empty + if (trbType(event.control) == @intFromEnum(TrbType.transfer_event)) _ = self.serviceInterruptEvent(event); + } + } +}; diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index bfca441..c984109 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -144,6 +144,10 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { driverRestartTest(boot_information); } else if (eql(case, "usb-report")) { usbReportTest(boot_information); + } else if (eql(case, "usb-hid")) { + usbHidTest(boot_information); + } else if (eql(case, "usb-storage")) { + usbStorageTest(boot_information); } else if (eql(case, "device-list")) { deviceListTest(boot_information); } else if (eql(case, "pci-scan")) { @@ -1968,6 +1972,40 @@ fn pciScanTest(boot_information: *const BootInformation) void { /// the acpi service publishes it; init runs the stop sequence over its children /// and asks the power service for S5; the machine powers off (QEMU exits). The /// kernel test only spawns init — the ordered chain is the harness assertion. +/// The USB HID chain, end to end: boot the full service tree (init spawns vfs, +/// input, device-manager), and let discovery run — the manager matches the PCI +/// host bridge to pci-bus, pci-bus reports the xHCI controller, usb-xhci-bus +/// enumerates the HID interfaces, and the manager spawns the class drivers. The +/// harness's expect regex requires usb-xhci-bus to register the boot-keyboard +/// interface, the manager to spawn usb-hid-keyboard, and that driver to come up +/// (open its device, ask for boot protocol, subscribe) — proof the transfer +/// protocol works class-driver to controller. +fn usbHidTest(boot_information: *const BootInformation) void { + bootServiceTreeTest(boot_information, "usb-hid"); +} + +/// The USB storage chain: same full-tree boot, but the harness attaches a +/// usb-storage device and the expect regex requires usb-storage to come up +/// (open its device, run the BOT bring-up, read its capacity, and read block 0). +fn usbStorageTest(boot_information: *const BootInformation) void { + bootServiceTreeTest(boot_information, "usb-storage"); +} + +fn bootServiceTreeTest(boot_information: *const BootInformation, comptime label: []const u8) void { + log("DANOS-TEST-BEGIN: " ++ label ++ "\n", .{}); + if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over init and the initial_ramdisk", false); + result(); + return; + } + const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + process.setInitialRamdisk(ramdisk); + const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; + const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false; + check("init spawned (boots vfs, input, device-manager, and the USB chain)", spawned); + result(); +} + fn orderlyShutdownTest(boot_information: *const BootInformation) void { log("DANOS-TEST-BEGIN: orderly-shutdown\n", .{}); if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) { diff --git a/system/parameters.zig b/system/parameters.zig index e6517af..1b6d8c3 100644 --- a/system/parameters.zig +++ b/system/parameters.zig @@ -17,10 +17,13 @@ pub const maximum_cpus = 128; /// Maximum tasks (kernel threads) alive at once — the static task-table size. Each /// online core consumes one slot for its idle task, plus task 0 on the BSP. Sized -/// for the initial-ramdisk sweep (15 bundled binaries spawned at once) plus the +/// for the initial-ramdisk sweep (the bundled binaries spawned at once) plus the /// device manager's supervised children with room to grow — at 16 the sweep -/// started failing spawns once the bundle passed a dozen binaries. -pub const maximum_tasks = 32; +/// started failing spawns once the bundle passed a dozen binaries. Raised to 48 +/// for the USB stack: the xHCI bus driver spawns a supervised class-driver instance +/// per matched interface (keyboard, mouse, mass storage), on top of the FAT and +/// block servers and the growing ramdisk bundle. +pub const maximum_tasks = 48; /// Each task's kernel stack (also each AP's bring-up stack), in bytes. pub const kernel_stack_size = 16 * 1024; diff --git a/system/services/block/protocol.zig b/system/services/block/protocol.zig new file mode 100644 index 0000000..76bd9fc --- /dev/null +++ b/system/services/block/protocol.zig @@ -0,0 +1,40 @@ +//! The block-device wire protocol — what a filesystem (the FAT server) says to a +//! block driver (usb-storage) over its well-known `.block` endpoint. A protocol +//! module like vfs-protocol / usb-transfer-protocol: extern-struct messages, an +//! `Operation` tag, everything in one IPC message. +//! +//! Data path: read and write move whole blocks to or from a **caller-owned DMA +//! buffer**, named by its physical address — the same physical-address handoff +//! usb-storage already uses toward the controller, one layer up. So a 512-byte +//! sector never has to cross the 256-byte IPC boundary; only the small request / +//! reply headers do. (Safe while the IOMMU is unenforced; see docs/driver-model.md.) + +pub const Operation = enum(u32) { + /// geometry() -> { block_size, block_count } + geometry = 0, + /// read(lba, count, physical): read `count` blocks from `lba` into the buffer + read = 1, + /// write(lba, count, physical): write `count` blocks at `lba` from the buffer + write = 2, +}; + +pub const Request = extern struct { + operation: u32, + reserved: u32 = 0, + lba: u64, + count: u32, // number of blocks (read/write) + reserved2: u32 = 0, + physical: u64, // caller's DMA buffer physical address (read/write) +}; + +pub const Reply = extern struct { + status: i32, // 0 on success, negative on failure + reserved: u32 = 0, + block_size: u32, // geometry: bytes per block (512) + reserved2: u32 = 0, + block_count: u64, // geometry: total blocks; read/write: blocks moved +}; + +pub const message_maximum: usize = 256; +pub const request_size: usize = @sizeOf(Request); +pub const reply_size: usize = @sizeOf(Reply); diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index dec6380..5daca05 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -19,6 +19,7 @@ const std = @import("std"); const runtime = @import("runtime"); const acpi_ids = @import("acpi-ids"); const pci_class = @import("pci-class"); +const usb_ids = @import("usb-ids"); const protocol = runtime.device_manager_protocol; const device = runtime.device; const system = runtime.system; @@ -61,6 +62,36 @@ fn hidDriverFor(hid: []const u8) ?[]const u8 { return null; } +/// The driver that serves a *reported* USB interface by its (class, subclass, +/// protocol) triple — the third bus after PCI and ACPI (docs/device-manager.md: +/// matching stays code until the third bus). The xHCI bus driver reports each +/// interface with this packed triple as its identity; the matched class driver is +/// spawned with the interface's registered id as argv[1], which it presents to the +/// bus driver to open the device. +fn usbDriverForIdentity(identity: u64) ?[]const u8 { + const keyboard = comptime usb_ids.packTriple( + @intFromEnum(usb_ids.Class.hid), + @intFromEnum(usb_ids.hid.SubClass.boot), + @intFromEnum(usb_ids.hid.Protocol.keyboard), + ); + const mouse = comptime usb_ids.packTriple( + @intFromEnum(usb_ids.Class.hid), + @intFromEnum(usb_ids.hid.SubClass.boot), + @intFromEnum(usb_ids.hid.Protocol.mouse), + ); + const storage = comptime usb_ids.packTriple( + @intFromEnum(usb_ids.Class.mass_storage), + @intFromEnum(usb_ids.mass_storage.SubClass.scsi), + @intFromEnum(usb_ids.mass_storage.Protocol.bulk_only), + ); + return switch (identity) { + keyboard => "usb-hid-keyboard", + mouse => "usb-hid-mouse", + storage => "usb-storage", + else => null, + }; +} + /// Whether some driver entry already serves registered device `device_id` — /// a re-report after a bus restart must not spawn a second instance. fn driverForDevice(device_id: u64) bool { @@ -411,6 +442,11 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize { if (pciDriverForIdentity(report.identity)) |child_driver| { if (!driverForDevice(report.device_id)) addDriver(child_driver, report.device_id, true); } + // USB interface match: the reported identity is the packed class triple, + // and the class driver is spawned with the interface's registered id. + if (usbDriverForIdentity(report.identity)) |usb_driver| { + if (!driverForDevice(report.device_id)) addDriver(usb_driver, report.device_id, true); + } // ACPI _HID match (M20.3): ps2-bus is a singleton that finds its own // devices by hid, so spawn it once, without a device assignment. const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len; diff --git a/test/qemu_test.py b/test/qemu_test.py index df03195..92a92e3 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -25,6 +25,7 @@ import shutil import socket import subprocess import sys +import tempfile import time REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -284,6 +285,31 @@ CASES = [ r"device-manager: restarting usb-xhci-bus[\s\S]*" r"device-manager: child added", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # USB HID end to end: boot the full tree, enumerate the xHCI, and let the + # manager spawn the USB keyboard driver, which opens its device over the + # transfer protocol, asks for boot protocol, subscribes to its interrupt + # endpoint, and comes up — proof the class-driver <-> controller path works. + {"name": "usb-hid", + "smp": 4, + "timeout": 150, + "qemu_extra": ["-device", "qemu-xhci,id=xhci", + "-device", "usb-kbd,bus=xhci.0", + "-device", "usb-mouse,bus=xhci.0"], + "expect": r"(?=[\s\S]*usb-hid/keyboard: ok)(?=[\s\S]*usb-hid/mouse: ok)", + "fail": r"DANOS-TEST-RESULT: FAIL"}, + # USB mass storage end to end: attach a usb-storage device (a FAT volume via + # QEMU's VVFAT, so it has a real boot sector), boot the full tree, and let the + # manager spawn usb-storage, which opens the device, runs the Bulk-Only / + # SCSI bring-up, reads its capacity, and reads block 0 (the 0x55AA boot sig) — + # proof of the bulk transfer path + BOT + SCSI end to end. + {"name": "usb-storage", + "smp": 4, + "timeout": 150, + "qemu_extra": ["-device", "qemu-xhci,id=xhci", + "-drive", "if=none,id=stick,format=raw,file=fat:rw:" + os.path.join(REPO, "zig-out"), + "-device", "usb-storage,drive=stick,bus=xhci.0"], + "expect": r"usb-storage: ready[\s\S]*usb-storage: block 0 signature 0x55aa", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # M20.1: the ring-3 AML parse (the acpi service maps the blobs and parses # them) finds exactly the Device count the kernel's own parse produced. {"name": "acpi-parse", @@ -492,8 +518,11 @@ def run_case(arch, case): if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case cmd += case["qemu_extra"] # A QMP control socket, always present (additive): how a case's `qmp_after` - # hook injects host-side events into the guest mid-run. - qmp_path = os.path.join(WORK, "qmp.sock") + # hook injects host-side events into the guest mid-run. Kept under a short temp + # dir, not WORK: a unix socket path is capped at ~104 bytes (sun_path), and a + # deep worktree path (e.g. .claude/worktrees//zig-out/qemu-test/qmp.sock) + # blows that limit on macOS, so QEMU fails to bind and exits before booting. + qmp_path = os.path.join(tempfile.gettempdir(), f"danos-qmp-{os.getpid()}.sock") if os.path.exists(qmp_path): os.remove(qmp_path) cmd += ["-qmp", f"unix:{qmp_path},server,nowait"]