These were the awkward ones. Each began with an operation packed into a single byte — two of them with a version wedged in beside it — so there was no wrapping them: the layouts had to be rebuilt. The device manager's own enumerate and subscribe become the reserved verbs that mean the same thing everywhere, its replies lose three status structs the envelope already carries, and a device id becomes the packet's target. Power drops the version it repeated on every request, because describe is the handshake, and stops claiming a 64-byte ceiling it never needed for calls. USB moves a control transfer's data to the packet tail in both directions, which makes the status length the transferred length and retires a field that had been saying the same thing twice. The danger in this one was not the protocols but their readers. Init recognised a power button by two bytes at the head of a message, the ACPI service dispatched on the first byte, the xHCI driver read its operation with a raw integer load, and the HID drivers reinterpreted a report wholesale — none of which would have failed to compile once the layouts moved. They would simply have stopped: no shutdown on the power button, no reports from the keyboard. Every one of them now reads through the generated types, and the shutdown gate that answers only a subscriber is the same code it was. Two sizes were decided by measuring rather than assuming. The child-added message is both a request and the event broadcast to subscribers, and alignment rounds it to 48 bytes, which puts its packet exactly on the 64-byte push floor — a test pins that, because a field added carelessly would now overflow it. The interrupt report gives up eight bytes of inline room to make space for the header; the two drivers that produce reports send eight and four. Suite 110/110.
210 lines
9.6 KiB
Zig
210 lines
9.6 KiB
Zig
//! library/device/driver — the driver author's interface: enumerate the kernel's device
|
|
//! table, claim a device, map its MMIO, bind its interrupt (the claim is the capability the
|
|
//! kernel checks before mapping registers or routing an IRQ), and say `hello` to the device
|
|
//! manager at startup. The whole kernel + manager surface a driver needs, in one import.
|
|
|
|
const std = @import("std");
|
|
const abi = @import("abi");
|
|
const device_abi = @import("device-abi");
|
|
const sc = @import("system-call");
|
|
const channel = @import("channel");
|
|
const envelope = @import("envelope");
|
|
const ipc = @import("ipc");
|
|
const time = @import("time");
|
|
const device_manager_protocol = @import("device-manager-protocol");
|
|
|
|
pub const DeviceDescriptor = device_abi.DeviceDescriptor;
|
|
pub const ResourceDescriptor = device_abi.ResourceDescriptor;
|
|
pub const DeviceClass = device_abi.DeviceClass;
|
|
pub const ResourceKind = device_abi.ResourceKind;
|
|
|
|
inline fn failed(r: usize) bool {
|
|
return r > ~@as(usize, 0) - 4095;
|
|
}
|
|
|
|
/// Copy up to `buffer.len` device descriptors into `buffer`; returns the total count.
|
|
pub fn enumerate(buffer: []DeviceDescriptor) usize {
|
|
return sc.systemCall2(.device_enumerate, @intFromPtr(buffer.ptr), buffer.len);
|
|
}
|
|
|
|
/// Take exclusive ownership of device `id`. Returns false if taken or invalid.
|
|
pub fn claim(id: u64) bool {
|
|
return !failed(sc.systemCall1(.device_claim, id));
|
|
}
|
|
|
|
/// Map resource `resource_index` (which must be an MMIO window) of claimed device
|
|
/// `device_id` into this address space; returns the register base virtual address.
|
|
pub fn mmioMap(device_id: u64, resource_index: u64) ?usize {
|
|
const r = sc.systemCall2(.mmio_map, device_id, resource_index);
|
|
return if (failed(r)) null else r;
|
|
}
|
|
|
|
/// `DeviceDescriptor.parent` for a device with no parent.
|
|
pub const no_parent = device_abi.no_parent;
|
|
|
|
/// `DeviceDescriptor.pci_class` for a device that is not a PCI function. Set this on
|
|
/// descriptors passed to `register` unless the child really is one.
|
|
pub const no_pci_class = device_abi.no_pci_class;
|
|
|
|
/// Publish `descriptor` as a child of `parent_id`, which this process must have claimed.
|
|
/// Returns the new device id. The child is left unclaimed, so whichever driver owns
|
|
/// that class of device can `claim` it — that is how a bus hands off a device.
|
|
///
|
|
/// Every resource in `descriptor` must be **contained** in a parent resource of the same
|
|
/// kind: a sub-window of the parent's MMIO, or one of its IRQs. The kernel refuses
|
|
/// anything else, because a device descriptor is a licence to map physical memory and
|
|
/// a bus driver may only subdivide what it already owns. `descriptor.id` and `descriptor.parent`
|
|
/// are ignored. A device with no resources at all is fine — a USB device is reached
|
|
/// through its controller, not by MMIO.
|
|
pub fn register(parent_id: u64, descriptor: *const DeviceDescriptor) ?u64 {
|
|
const r = sc.systemCall2(.device_register, parent_id, @intFromPtr(descriptor));
|
|
return if (failed(r)) null else r;
|
|
}
|
|
|
|
/// Bind resource `resource_index` (which must be an IRQ) of claimed device `device_id` to
|
|
/// `endpoint`. From then on the interrupt arrives as an asynchronous notification:
|
|
/// `ipc.replyWait` on that endpoint returns with the high bit set in `badge` and the
|
|
/// low bits carrying the GSI. The kernel masks the line before waking you.
|
|
pub fn irqBind(device_id: u64, resource_index: u64, endpoint: usize) bool {
|
|
return !failed(sc.systemCall3(.irq_bind, device_id, resource_index, endpoint));
|
|
}
|
|
|
|
/// Re-arm a bound IRQ. Call this **after** quieting the device (clearing whatever
|
|
/// status register holds its line asserted) — the kernel left the line masked
|
|
/// precisely because it could not do that for you. Skip it and the interrupt never
|
|
/// fires again; call it before the device is quiet and a level-triggered line storms.
|
|
pub fn irqAck(device_id: u64, resource_index: u64) bool {
|
|
return !failed(sc.systemCall2(.irq_ack, device_id, resource_index));
|
|
}
|
|
|
|
/// The Message-Signalled Interrupt address/data a driver programs into its device's
|
|
/// MSI capability. The device raises the interrupt by writing `data` to `address`.
|
|
pub const Msi = struct { address: u64, data: u32 };
|
|
|
|
/// Set up MSI for a claimed device: the kernel allocates a per-device edge-triggered
|
|
/// vector, binds it to `endpoint` (delivered like `irqBind`, but with no mask and no
|
|
/// `irqAck` cycle), and returns the (address, data) to write into the device's MSI
|
|
/// capability — found by mmio_mapping the device's ECAM config space (resource 0) and
|
|
/// walking its capability list. Returns null on failure. Two return values (address in
|
|
/// rax, data in rdx), so a hand-written stub.
|
|
pub fn msiBind(device_id: u64, endpoint: usize) ?Msi {
|
|
var rax: usize = undefined;
|
|
var rdx: usize = undefined;
|
|
asm volatile ("syscall"
|
|
: [rax] "={rax}" (rax),
|
|
[rdx] "={rdx}" (rdx),
|
|
: [n] "{rax}" (@intFromEnum(abi.SystemCall.msi_bind)),
|
|
[a0] "{rdi}" (device_id),
|
|
[a1] "{rsi}" (endpoint),
|
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
|
if (failed(rax)) return null;
|
|
return .{ .address = rax, .data = @intCast(rdx) };
|
|
}
|
|
|
|
/// Map a delegated DMA-region (or shared-memory) capability into a claimed device's
|
|
/// IOMMU domain, so the device may DMA to that buffer. The caller must own `device_id`
|
|
/// and hold `handle` (received over IPC or from its own `dma.alloc(.. | shareable)`).
|
|
/// Idempotent. Returns true on success (and trivially when no IOMMU is present).
|
|
pub fn dmaBind(device_id: u64, handle: usize) bool {
|
|
return !failed(sc.systemCall2(.dma_bind, device_id, handle));
|
|
}
|
|
|
|
/// Unmap a previously `dmaBind`'d buffer from the device's domain.
|
|
pub fn dmaUnbind(device_id: u64, handle: usize) bool {
|
|
return !failed(sc.systemCall2(.dma_unbind, device_id, handle));
|
|
}
|
|
|
|
/// Drain and log any pending IOMMU translation faults, returning the count seen. A
|
|
/// diagnostic: a driver that suspects its device attempted an out-of-domain DMA (or a
|
|
/// test proving enforcement) forces the hardware's fault records to the log now. Returns
|
|
/// 0 when no IOMMU is present.
|
|
pub fn iommuFaultDrain() usize {
|
|
return sc.systemCall0(.iommu_fault_drain);
|
|
}
|
|
|
|
/// Read `width` bytes (1, 2, or 4) from a port in a claimed device's `io_port`
|
|
/// resource, at byte `offset` within it. Ring 3 has no direct `in`/`out`, so a legacy
|
|
/// driver (PS/2, 16550 UART) reaches its ports through this claim-gated call — each
|
|
/// access is a syscall, which is fine for the low-rate hardware that needs it. Returns
|
|
/// null if the capability check fails (device not claimed, wrong resource, out of
|
|
/// range). A device that decodes no data returns all-ones, which is a valid value, not
|
|
/// a failure.
|
|
pub fn ioRead(device_id: u64, resource_index: u64, offset: u64, width: u8) ?u32 {
|
|
const r = sc.systemCall4(.io_read, device_id, resource_index, offset, width);
|
|
return if (failed(r)) null else @intCast(r);
|
|
}
|
|
|
|
/// Write `value` (its low `width` bytes, 1/2/4) to a port in a claimed device's
|
|
/// `io_port` resource, at byte `offset`. Same capability gate as `ioRead`.
|
|
pub fn ioWrite(device_id: u64, resource_index: u64, offset: u64, width: u8, value: u32) bool {
|
|
return !failed(sc.systemCall5(.io_write, device_id, resource_index, offset, width, value));
|
|
}
|
|
|
|
/// Find DeviceDescription by hid
|
|
///
|
|
/// Utility function for driver development
|
|
pub fn findDeviceDescriptorByHid(buffer: []DeviceDescriptor, hid_needle: []const u8) ?DeviceDescriptor {
|
|
const total = enumerate(buffer);
|
|
const n = @min(total, buffer.len);
|
|
for (@as([]DeviceDescriptor, buffer[0..n])) |d| {
|
|
const hid_haystack = d.hid[0..@intCast(d.hid_len)];
|
|
if (std.mem.eql(u8, hid_haystack, hid_needle)) {
|
|
return d;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// --- device-manager handshake (folded in from the former device-manager.zig) ---
|
|
|
|
/// What kind of driver is announcing itself (a bus that reports children, or a leaf
|
|
/// device). Re-exported so callers name it without importing the protocol.
|
|
pub const Role = device_manager_protocol.Role;
|
|
|
|
const lookup_attempts: u32 = 100;
|
|
const lookup_pause_ms: u64 = 20;
|
|
|
|
/// Say hello to the device manager and return its endpoint, or null if there is no manager
|
|
/// (best-effort standalone bring-up) or it refused the handshake. Bus drivers keep the handle
|
|
/// to report children through; a driver that runs fine unsupervised discards it with `_ =`,
|
|
/// and one that requires supervision bails on null. Logs the outcome itself.
|
|
///
|
|
/// The device this driver was assigned is the packet's `Header.target` — the manager's
|
|
/// object addressing, so `no_device` here is a driver that serves none.
|
|
pub fn hello(role: Role, device_id: u64) ?ipc.Handle {
|
|
var attempts: u32 = 0;
|
|
const manager = while (attempts < lookup_attempts) : (attempts += 1) {
|
|
if (channel.openEndpoint("device-manager")) |handle| break handle;
|
|
time.sleepMillis(lookup_pause_ms);
|
|
} else {
|
|
std.log.info("no device manager to hello", .{});
|
|
return null;
|
|
};
|
|
|
|
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
const framed = device_manager_protocol.Protocol.encodeRequest(
|
|
.hello,
|
|
device_id,
|
|
.{ .role = @intFromEnum(role) },
|
|
&.{},
|
|
&packet,
|
|
) orelse return null;
|
|
|
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
const length = ipc.call(manager, framed, &reply) catch {
|
|
std.log.info("hello call failed", .{});
|
|
return null;
|
|
};
|
|
const status = envelope.statusOf(reply[0..length]) orelse {
|
|
std.log.info("hello answered nothing readable", .{});
|
|
return null;
|
|
};
|
|
if (status.status != 0) {
|
|
std.log.info("hello refused", .{});
|
|
return null;
|
|
}
|
|
std.log.info("hello acknowledged", .{});
|
|
return manager;
|
|
}
|