docs: commit the new-driver checklist, step 2 rewritten for packages
The checklist (with the Intel UHD 750 worked example) existed only as an untracked file in the main checkout — the plan referenced it but no branch could see it. It lands here with its build step rewritten against the finished package template: create the driver's own build.zig/zon from the pci-bus template, then one dependency + one boot-tree row + one zon line in the root build. devices-csv.md's adding-a-driver section and the plan's checklist references point at it again.
This commit is contained in:
@@ -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`.
|
probing), imported by a short root `build.zig`.
|
||||||
|
|
||||||
**Afterwards** (outside this plan): the intel-uhd-graphics-750 driver is
|
**Afterwards** (outside this plan): the intel-uhd-graphics-750 driver is
|
||||||
(re)created as a greenfield package — the "Adding a driver" checklist's build
|
(re)created as a greenfield package. The new-driver checklist's build step
|
||||||
step (docs/device-driver-development/devices-csv.md, step 1) gets rewritten
|
(docs/device-driver-development/new-driver-checklist.md, step 2) is already
|
||||||
against the package template at that point.
|
rewritten against the package template.
|
||||||
|
|
||||||
## Execution notes (the finished shape)
|
## 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,
|
struct sizes) — they moved into their binaries' packages with their waves,
|
||||||
discharging the carry-along obligation.
|
discharging the carry-along obligation.
|
||||||
- Doc updates ride each phase: docs/README.md (repo layout + source map),
|
- Doc updates ride each phase: docs/README.md (repo layout + source map),
|
||||||
docs/device-driver-development/devices-csv.md ("Adding a driver", step 1),
|
docs/device-driver-development/new-driver-checklist.md (step 2) and
|
||||||
and the docs that cite the build recipe (driver-model.md, threading.md,
|
devices-csv.md ("Adding a driver"), and the docs that cite the build recipe
|
||||||
system-requirements.md) reference build shapes that keep changing.
|
(driver-model.md, threading.md, system-requirements.md) reference build
|
||||||
|
shapes that keep changing.
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ in different namespaces — against the right `bus` column.
|
|||||||
|
|
||||||
## Adding a driver
|
## 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/<name>/` with the driver source plus a ~15-line
|
1. Create `system/drivers/<name>/` with the driver source plus a ~15-line
|
||||||
package `build.zig` + `build.zig.zon` (copy an existing driver package,
|
package `build.zig` + `build.zig.zon` (copy an existing driver package,
|
||||||
e.g. `system/drivers/pci-bus/`; per-driver extras go through
|
e.g. `system/drivers/pci-bus/`; per-driver extras go through
|
||||||
|
|||||||
@@ -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/<name>/<name>.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/<boot-stamp>/system/services/device-manager.log`
|
||||||
|
shows `spawned <name> for device <N>`, and
|
||||||
|
`/var/log/<boot-stamp>/system/drivers/<name>.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.
|
||||||
Reference in New Issue
Block a user