diff --git a/docs/build-packages-plan.md b/docs/build-packages-plan.md index 6e00dcf..37d5a1c 100644 --- a/docs/build-packages-plan.md +++ b/docs/build-packages-plan.md @@ -113,9 +113,9 @@ images, release ISO, check steps) and `build/qemu.zig` (the run steps + OVMF probing), imported by a short root `build.zig`. **Afterwards** (outside this plan): the intel-uhd-graphics-750 driver is -(re)created as a greenfield package — the "Adding a driver" checklist's build -step (docs/device-driver-development/devices-csv.md, step 1) gets rewritten -against the package template at that point. +(re)created as a greenfield package. The new-driver checklist's build step +(docs/device-driver-development/new-driver-checklist.md, step 2) is already +rewritten against the package template. ## Execution notes (the finished shape) @@ -159,6 +159,7 @@ style: no Co-Authored-By trailers. struct sizes) — they moved into their binaries' packages with their waves, discharging the carry-along obligation. - Doc updates ride each phase: docs/README.md (repo layout + source map), - docs/device-driver-development/devices-csv.md ("Adding a driver", step 1), - and the docs that cite the build recipe (driver-model.md, threading.md, - system-requirements.md) reference build shapes that keep changing. + docs/device-driver-development/new-driver-checklist.md (step 2) and + devices-csv.md ("Adding a driver"), and the docs that cite the build recipe + (driver-model.md, threading.md, system-requirements.md) reference build + shapes that keep changing. diff --git a/docs/device-driver-development/devices-csv.md b/docs/device-driver-development/devices-csv.md index 44a66f4..203bea9 100644 --- a/docs/device-driver-development/devices-csv.md +++ b/docs/device-driver-development/devices-csv.md @@ -96,6 +96,9 @@ in different namespaces — against the right `bus` column. ## Adding a driver +(The step-by-step walkthrough with a worked example is +[new-driver-checklist.md](new-driver-checklist.md).) + 1. Create `system/drivers//` with the driver source plus a ~15-line package `build.zig` + `build.zig.zon` (copy an existing driver package, e.g. `system/drivers/pci-bus/`; per-driver extras go through diff --git a/docs/device-driver-development/new-driver-checklist.md b/docs/device-driver-development/new-driver-checklist.md new file mode 100644 index 0000000..41767d2 --- /dev/null +++ b/docs/device-driver-development/new-driver-checklist.md @@ -0,0 +1,214 @@ +# New driver: the minimum steps + +The shortest path from "a device shows up in the boot log" to "my process is +running with its registers mapped". This is the checklist; the reasoning behind +every step lives in [Writing a driver](drivers.md), the matching rules in +[devices.csv](devices-csv.md), and interrupts in +[device interrupts](device-interrupts.md). + +Worked example throughout: the Intel UHD 750 iGPU, which the boot log reports as + +``` +pci-bus: 0:2.0 bus=pci base=03 class=00 prog_if=00 vendor=8086 device=4C8A ... +``` + +## 1. Create the source file + +`system/drivers//.zig` — kebab-case, abbreviations spelled out +([coding standards](../coding-standards.md)). The directory name, the binary +name, and the `devices.csv` driver path must all agree; a mismatch fails +silently (the device-manager logs the spawn failure, nothing else happens). + +The complete minimal driver — claims its device, logs every resource, maps the +register window, then sleeps in the harness loop: + +```zig +//! /system/drivers/intel-uhd-graphics-750 — spawned by the device manager with +//! the device-tree id as argv[1]; claims that device and no other. + +const std = @import("std"); +const device = @import("driver"); +const ipc = @import("ipc"); +const memory = @import("memory"); +const process = @import("process"); +const service = @import("service"); + +/// No protocol yet: the kernel's IPC ceiling (MESSAGE_MAXIMUM) sizes the buffers. +const message_maximum = 256; + +var controller_id: u64 = 0; +var register_base: usize = 0; + +fn initialise(endpoint: ipc.Handle) bool { + _ = endpoint; // needed later, for irq binding and timers + + if (!device.claim(controller_id)) { + std.log.err("unable to claim device {d}", .{controller_id}); + return false; + } + + // Fetch our own descriptor back for the device's resources. + const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch return false; + defer memory.allocator().free(buffer); + const total = device.enumerate(buffer); + const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| { + if (d.id == controller_id) break d; + } else { + std.log.err("device {d} not in the device tree", .{controller_id}); + return false; + }; + + // Log every resource BEFORE choosing one (see step 5). + var register_index: u64 = 0; + for (descriptor.resources[0..@intCast(descriptor.resource_count)], 0..) |resource, index| { + std.log.info("resource {d}: kind={d} start=0x{x} len=0x{x}", .{ + index, resource.kind, resource.start, resource.len, + }); + // The 16 MiB window is GTTMMADR, the register BAR (this device also has + // a 256 MiB memory BAR, GMADR — "first memory resource" would be wrong). + if (resource.kind == @intFromEnum(device.ResourceKind.memory) and + resource.len == 16 * 1024 * 1024) register_index = index; + } + if (register_index == 0) { + std.log.err("register BAR not found", .{}); + return false; + } + + register_base = device.mmioMap(controller_id, register_index) orelse { + std.log.err("mmio_map failed", .{}); + return false; + }; + std.log.info("registers mapped at 0x{x}", .{register_base}); + return true; +} + +fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize { + _ = message; + _ = reply; + _ = sender; + _ = capability; + return 0; // no protocol yet; the zero-length ping is answered by the harness +} + +pub fn main(init: process.Init) void { + const argument = init.arguments.get(1) orelse { + std.log.err("missing device id (argv[1])", .{}); + return; + }; + controller_id = std.fmt.parseInt(u64, argument, 10) catch { + std.log.err("malformed device id '{s}'", .{argument}); + return; + }; + service.run(message_maximum, .{ + .init = initialise, + .on_message = onMessage, + // .on_notification only once an IRQ or timer is bound + }); +} +``` + +`claim` is the capability gate: MMIO mapping, DMA grants, and IRQ binding all +require it, and it pins the IOMMU domain to this process +([drivers.md — claim before touch](drivers.md#the-capability-claim-before-touch)). + +## 2. Create the build package and register it in the root build + +The driver directory is its own build package +([build-packages-plan.md](../build-packages-plan.md)): a ~15-line `build.zig` +plus a `build.zig.zon` beside the source. Copy both from an existing driver — +`system/drivers/pci-bus/` is the template — and adjust the name, root source +file, and per-driver extras: + +```zig +pub fn build(b: *std.Build) void { + const exe = build_support.userBinary(b, .{ + .name = "intel-uhd-graphics-750", + .root_source_file = b.path("intel-uhd-graphics-750.zig"), + .domains = build_support.domains(b), + }); + build_support.programModule(exe).addImport("pci", b.dependency("device", .{}).module("pci")); // if the pci library is used + b.installArtifact(exe); +} +``` + +The zon declares `build-support`, the four domain packages, and any extras' +homes by relative path (again, copy pci-bus's and adjust); for the +`.fingerprint` field, leave the copied value in place and `zig build` will +reject it and suggest the fresh one to paste. + +Then three one-liners in the root build register the package: the dependency +and a row in the boot-tree array in `build.zig` (search for +`virtio_gpu_package` to land in the right places), + +```zig +const intel_uhd_graphics_750_exe = b.dependency("intel-uhd-graphics-750", .{}).artifact("intel-uhd-graphics-750"); +``` + +```zig +.{ .path = "system/drivers/intel-uhd-graphics-750", .binary = intel_uhd_graphics_750_exe.getEmittedBin() }, +``` + +and the path entry in the root `build.zig.zon`: + +```zig +.@"intel-uhd-graphics-750" = .{ .path = "system/drivers/intel-uhd-graphics-750" }, +``` + +Without the boot-tree row the binary never reaches the image and the +device-manager has nothing to spawn. (The package also builds standalone: +`cd system/drivers/intel-uhd-graphics-750 && zig build`.) + +## 3. Add the match rule to `etc/devices.csv` + +One row: bus, class triplet, vendor/device, driver path. **Copy the class +triplet from the pci-bus boot log line, not from another row** — for the iGPU +above the correct rule is + +``` +pci, 03, 00, 00, 8086, 4C8A, *, *, /system/drivers/intel-uhd-graphics-750 +``` + +Field-by-field rules and the most-specific-wins policy: [devices.csv](devices-csv.md). +The registry is authoritative: an unmatched device is logged unbound, never +guessed — so a wrong nibble here means the driver simply never starts. + +## 4. First contact: read, predict, verify + +Before writing any register, read one whose value you can predict from state +the firmware already programmed (for a display controller: the pipe source +size of the live mode). Registers are volatile loads at `register_base + +offset`, where `offset` is what the device's manual lists: + +```zig +fn read32(offset: usize) u32 { + return @as(*volatile u32, @ptrFromInt(register_base + offset)).*; +} +``` + +A matching read proves the whole chain — CSV match, spawn, claim, BAR choice, +mapping — with zero risk to the hardware. + +## 5. Verify the plumbing + +- `zig build test` still passes. +- On the image: `/var/log//system/services/device-manager.log` + shows `spawned for device `, and + `/var/log//system/drivers/.log` holds the resource list and + your first read. +- If the driver did not spawn, diagnose in this order: binary on the image + (step 2) → CSV row matches the log line exactly (step 3) → path identical in + both (step 1). + +## Later, when the device needs them + +- **Interrupts**: MSI/MSI-X via the `pci` module, delivered as notifications to + `on_notification` — see [device interrupts](device-interrupts.md) and the + xHCI driver's `setupMsi` (QEMU trap documented there: enable MSI-X before + unmasking the device's own interrupt-enable bit). +- **DMA**: grant-backed buffers, bounded by the IOMMU domain established at + claim time ([driver model](driver-model.md)). +- **Children**: a bus driver publishes what it finds via `device_register` + ([drivers.md — publishing children](drivers.md#publishing-children-device_register)). +- **A protocol**: replace `message_maximum` with the protocol's own maximum and + dispatch on the operation word in `onMessage` — every service under + `system/services/` is an example.