danos/library/protocol/usb-transfer/usb-transfer-protocol.zig

219 lines
11 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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 34, 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));
}