//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or //! mass-storage driver) says to the xHCI bus driver over `/protocol/usb-transfer` //! to drive its device. The class driver owns no hardware — it reaches its device //! entirely through these packets, the way a PS/2 keyboard driver reaches the //! 8042 through the ps2-bus. //! //! Defined through the envelope (docs/os-development/protocol-namespace.md), so //! every packet begins with the folded `Header`. **`Header.target` is the device //! token** — the per-open handle the bus driver hands back, which every request //! but `open` addressed through a `device_token` field of its own before the //! rebase. `open` itself addresses the *assigned device id*, because that is what //! the caller has before there is a token. //! //! The shape: //! - **open** (a capability-passing call): the class driver hands over its own //! endpoint (for asynchronous interrupt reports); the target is its assigned //! device id, and the reply carries a `device_token` plus its interface's //! endpoints. //! - **control / bulk** (synchronous calls): one transfer, answered when it //! completes. Control data travels **in the packet's tail** in both //! directions (descriptors, HID/MSC class requests are all small), so the //! fixed parts stay tiny and `Status.len` is the transferred length — the //! envelope's own field for "how many bytes follow", which is precisely what //! the old `actual_length` said. 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 packet floor. //! - **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 `interrupt_report` event. //! It stays one of **this protocol's own verbs**, not the reserved //! `subscribe`: the reserved verb means "push me this provider's events" and //! carries the subscriber's endpoint, while this names one endpoint address //! on one device and a poll length, and the endpoint it pushes to was handed //! over at `open`. Same word, different contract. //! - **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). //! //! Single controller assumption: one provider serves QEMU's one xHCI. A //! multi-controller machine would need the controller in the target (or the //! spawner wiring each class driver its own channel); noted, not built. const std = @import("std"); const envelope = @import("envelope"); /// The largest control-transfer data stage. It rides the packet's tail, so the /// bound is the call floor less the header and the fixed request part — derived /// rather than declared, which is what keeps it honest when a field moves. pub const max_inline_data: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(Control); /// The largest interrupt report pushed asynchronously. An event packet is the /// header plus the payload within 64 bytes, so this is what is left after the /// report's own four bytes of framing: boot keyboard reports are 8 bytes, boot /// mouse reports 3–4, and the whole HID boot vocabulary fits many times over. /// A device that produces more has its report truncated, never split. pub const max_report_data: usize = 40; /// 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; /// 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 }, }; // --- the per-operation request and reply parts ------------------------------ // // Each names the bytes AFTER the prefix. Nothing here carries an operation or a // device token: those are the packet header's, folded in once. No reply carries // a status either — that is the `Status` every reply begins with. /// The answer to `open`: the token every later packet puts in `Header.target`, /// the interface's class triple (a sanity check), and its endpoints. pub const Opened = extern struct { device_token: u64, endpoint_count: u32, interface_class: u8, interface_subclass: u8, interface_protocol: u8, interface_number: u8, 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 on `Header.target`. `setup` is a bit-cast /// `usb_abi.Request`. For an OUT transfer the data stage is the request's tail; /// for an IN transfer it comes back as the reply's tail, and `Status.len` is how /// much of it arrived. pub const Control = extern struct { setup: [8]u8, /// 1 = device-to-host (IN), 0 = host-to-device (OUT). direction_in: u8, _padding: u8 = 0, /// Bytes of data stage: what an IN transfer asks for, and what an OUT /// transfer's tail carries. data_length: u16, _padding2: u32 = 0, }; /// `interrupt_subscribe`: begin periodic IN polling of an interrupt endpoint of /// `Header.target`. Each report the device returns is pushed to the endpoint the /// caller handed over at `open`, as an `interrupt_report` event. pub const InterruptSubscribe = extern struct { endpoint_address: u8, _padding: u8 = 0, /// Bytes to request per poll (the endpoint's max packet size). max_length: u16, }; /// `bulk`: one bulk IN or OUT transfer on `Header.target`. `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 Bulk = extern struct { physical_address: u64, length: u32, endpoint_address: u8, _padding: u8 = 0, _padding2: u16 = 0, }; /// How many bytes a bulk transfer actually moved. It cannot ride `Status.len` /// the way a control transfer's does: nothing follows a bulk reply, because the /// data went to the caller's DMA buffer rather than into the packet. pub const Transferred = extern struct { actual_length: u32 }; /// One asynchronous interrupt report, pushed to the endpoint the class driver /// handed over at `open`. The device it came from is `Header.target`. pub const InterruptReport = extern struct { endpoint_address: u8, length: u8, _padding: u16 = 0, data: [max_report_data]u8 = [_]u8{0} ** max_report_data, }; pub const Protocol = envelope.Define(.{ .name = "usb-transfer", .version = 1, .operations = &.{ // open: the target is the interface's assigned device id (its argv[1]), // and the class driver's receive endpoint rides as the capability. .{ .name = "open", .reply = Opened }, .{ .name = "control", .request = Control }, .{ .name = "interrupt_subscribe", .request = InterruptSubscribe }, .{ .name = "bulk", .request = Bulk, .reply = Transferred }, // dma_attach: the region capability rides the call's cap slot; the // target says which caller's device is attaching, so there is nothing // left for a body to carry. .{ .name = "dma_attach" }, }, .events = &.{ .{ .name = "interrupt_report", .payload = InterruptReport }, }, }); pub const Operation = Protocol.Operation; pub const Event = Protocol.Event; /// What both sides size their buffers to — the call floor, as every protocol does. pub const message_maximum: usize = Protocol.message_maximum; test "the budgets, re-verified by Define rather than by hand" { // What the hand-rolled comptime asserts used to say, now said by `Define` // — and counting the header, which the old checks did not. try std.testing.expectEqual(@as(usize, 224), max_inline_data); try std.testing.expect(Protocol.request_maximum <= envelope.packet_maximum); try std.testing.expect(Protocol.reply_maximum <= envelope.packet_maximum); // The report was 48 bytes of data in a 64-byte struct that had no room left // for a header. Folding the device token into the target and trimming the // data to 40 leaves the whole packet at 60 of the 64-byte push floor. try std.testing.expectEqual(@as(usize, 44), @sizeOf(InterruptReport)); try std.testing.expectEqual(@as(usize, 60), Protocol.event_maximum); try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum); } test "the verb numbering, and the device token in the header" { try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open)); try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.control)); try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.interrupt_subscribe)); try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.bulk)); try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.dma_attach)); try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.interrupt_report)); var buffer: [message_maximum]u8 = undefined; const packet = Protocol.encodeRequest(.control, 9, .{ .setup = .{ 0, 6, 0, 1, 0, 0, 18, 0 }, .direction_in = 1, .data_length = 18, }, &.{}, &buffer).?; try std.testing.expectEqual(@as(u64, 9), envelope.headerOf(packet).?.target); try std.testing.expectEqual(@as(u16, 18), Protocol.decodeRequest(.control, packet).?.data_length); } test "a control OUT carries its data stage as the packet's tail" { var buffer: [message_maximum]u8 = undefined; const payload = [_]u8{ 1, 2, 3, 4 }; const packet = Protocol.encodeRequest(.control, 5, .{ .setup = .{ 0x21, 11, 0, 0, 0, 0, 4, 0 }, .direction_in = 0, .data_length = payload.len, }, &payload, &buffer).?; try std.testing.expectEqualSlices(u8, &payload, Protocol.requestTail(.control, packet)); // And the answer to an IN: the bytes follow the (empty) fixed reply part, // with `Status.len` counting exactly them. const answered = Protocol.encodeReply(.control, 0, {}, &payload, &buffer).?; try std.testing.expectEqual(@as(u32, payload.len), envelope.statusOf(answered).?.len); try std.testing.expectEqualSlices(u8, &payload, Protocol.replyTail(.control, answered)); }