178 lines
7.1 KiB
Zig
178 lines
7.1 KiB
Zig
//! 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);
|
||
}
|