//! 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, /// dma_attach: a class driver hands the controller a DMA-region capability (riding /// the call's cap slot) so the controller binds that buffer into its IOMMU domain /// and may then DMA to the physical addresses inside it. Needed once per buffer the /// class driver will name in a `bulk` transfer (its own, or one forwarded to it). dma_attach = 4, }; /// 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, }; /// dma_attach: the region capability rides the call's cap slot; the body only carries /// the device token (scoping) so the controller knows which caller is attaching. pub const DmaAttachRequest = extern struct { operation: u32 = @intFromEnum(Operation.dma_attach), reserved: u32 = 0, device_token: u64, }; pub const DmaAttachReply = extern struct { status: i32, reserved: u32 = 0, }; /// 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); }