//! /system/drivers/usb-xhci-bus — the xHCI (USB 3) host-controller bus driver. //! The device manager spawns **one instance per controller** it discovers (a //! machine can carry several), passing the controller's device-tree id as //! argv[1]; this instance claims that device and no other, so multiple //! instances never fight over hardware. //! //! M18.2 (this increment): after the hello, real hardware — map the xHC's //! register window (the first memory BAR; resource 0 is the ECAM config //! space), read the capability registers, and walk the root-hub ports: one //! `child_added` report to the manager per connected port, carrying the port //! number and the PORTSC speed class as identity. No transfer rings yet — //! descriptors and USB class matching are the USB track; the connect bit and //! speed come straight from PORTSC, which reflects hardware state whether or //! not the controller is running. 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. 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 controller_id: u64 = protocol.no_device; /// Claim the assigned controller, find its register window, and hello the /// 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 { 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; } // Fetch our own descriptor back for the controller's resources. const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { _ = runtime.system.write("/system/drivers/usb-xhci-bus: out of memory\n"); return false; }; const total = device.enumerate(buffer); const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| { if (d.id == controller_id) break d; } else { writeLine("/system/drivers/usb-xhci-bus: device {d} not in the device tree\n", .{controller_id}); return false; }; // The xHC's registers live behind the first memory BAR. Resource 0 is the // function's ECAM configuration space (M15), so the walk starts at 1. var register_index: u64 = 0; const register_window = for (descriptor.resources[1..@intCast(descriptor.resource_count)], 1..) |resource, index| { if (resource.kind == @intFromEnum(device.ResourceKind.memory)) { register_index = index; break resource; } } else { writeLine("/system/drivers/usb-xhci-bus: controller device {d} has no register BAR\n", .{controller_id}); return false; }; writeLine("/system/drivers/usb-xhci-bus: claimed controller device {d} (registers at 0x{x}, {d} bytes)\n", .{ controller_id, register_window.start, register_window.len, }); register_base = device.mmioMap(controller_id, register_index) orelse { _ = runtime.system.write("/system/drivers/usb-xhci-bus: mmio_map failed\n"); 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; var tries: u32 = 0; while (manager == null and tries < 100) : (tries += 1) { manager = runtime.ipc.lookup(.device_manager); if (manager == null) runtime.system.sleep(20); } const h = manager orelse { _ = runtime.system.write("/system/drivers/usb-xhci-bus: no device manager to hello\n"); return false; }; const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = controller_id }; var reply: [protocol.message_maximum]u8 = undefined; const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch { _ = runtime.system.write("/system/drivers/usb-xhci-bus: hello call failed\n"); return false; }; if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) { _ = runtime.system.write("/system/drivers/usb-xhci-bus: hello refused\n"); return false; } _ = 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; /// 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 /// these through its Supported Protocol capability, but the defaults cover every /// speed QEMU and real hardware report at this (pre-descriptor) stage. fn speedName(speed: u32) []const u8 { return switch (speed) { 1 => "Full-speed (USB 2.0, 12 Mb/s)", 2 => "Low-speed (USB 2.0, 1.5 Mb/s)", 3 => "High-speed (USB 2.0, 480 Mb/s)", 4 => "SuperSpeed (USB 3.0, 5 Gb/s)", 5 => "SuperSpeedPlus (USB 3.1, 10 Gb/s)", else => "unknown speed", }; } /// 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 { 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}); var port: u32 = 1; var connected: u32 = 0; 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 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"); } /// 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 { _ = sender; 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 { const argument = init.arguments.get(1) orelse { _ = runtime.system.write("/system/drivers/usb-xhci-bus: missing controller device id (argv[1])\n"); return; }; controller_id = std.fmt.parseInt(u64, argument, 10) catch { writeLine("/system/drivers/usb-xhci-bus: malformed controller device id '{s}'\n", .{argument}); return; }; runtime.service.run(transfer.message_maximum, .{ .service = .usb_bus, .init = initialise, .on_message = onMessage, .on_notification = onNotification, }); } pub const panic = runtime.panic; comptime { _ = &runtime.start._start; // pull the runtime entry shim into the image }