diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cda98da --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig: https://editorconfig.org/ +# Follows the Zig style guide: https://ziglang.org/documentation/0.16.0/#Style-Guide + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.zig] +# "Line length: aim for 100; use common sense." +max_line_length = 100 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..00fa2ec --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.zig text eol=lf \ No newline at end of file diff --git a/build.zig b/build.zig index 090b7e6..269bef1 100644 --- a/build.zig +++ b/build.zig @@ -331,6 +331,7 @@ pub fn build(b: *std.Build) void { const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig"); const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig"); + const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig"); const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); // The input service and its exercisers: the fan-out server, a hardware-free synthetic // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. @@ -360,6 +361,8 @@ pub fn build(b: *std.Build) void { mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin()); mk_run.addArg("ps2-mouse"); mk_run.addFileArg(ps2_mouse_exe.getEmittedBin()); + mk_run.addArg("usb-xhci-bus"); + mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin()); mk_run.addArg("device-manager"); mk_run.addFileArg(device_manager_exe.getEmittedBin()); mk_run.addArg("input"); @@ -384,6 +387,7 @@ pub fn build(b: *std.Build) void { .{ ps2_bus_exe, "system/drivers" }, .{ ps2_keyboard_exe, "system/drivers" }, .{ ps2_mouse_exe, "system/drivers" }, + .{ usb_xhci_bus_exe, "system/drivers" }, }) |entry| { const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } }); b.getInstallStep().dependOn(&step.step); @@ -455,6 +459,19 @@ pub fn build(b: *std.Build) void { const run_efi = b.addSystemCommand(&.{ "qemu-system-x86_64", + "-device", + "qemu-xhci,id=xhci", + "-device", + "usb-mouse,bus=xhci.0", + "-device", + "usb-kbd,bus=xhci.0", + // "-usb", + // "-device", + // "usb-ehci,id=ehci", + // "-device", + // "usb-tablet,bus=usb-bus.0", + // "-device", + // "usb-mouse,bus=ehci.0", "-machine", "q35", "-m", @@ -513,6 +530,8 @@ pub fn build(b: *std.Build) void { "system/devices/device-abi.zig", "system/devices/pci-class.zig", // class/subclass/prog-IF name decoding "system/devices/acpi-ids.zig", // _HID name decoding + "system/devices/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings + "system/devices/usb-ids.zig", // class/subclass/protocol code assignments "library/mmio/mmio.zig", // barriers assemble + registers round-trip "system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine "system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly diff --git a/docs/README.md b/docs/README.md index bd69c58..11d2cde 100644 --- a/docs/README.md +++ b/docs/README.md @@ -52,11 +52,22 @@ rather than restate it. Roughly in the order things happen at runtime: microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the supervision link as the kill authority, and child-exit notifications over the same endpoints IRQs arrive on. -16. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard, +16. **[process-lifecycle.md](process-lifecycle.md) — the process lifecycle.** Design: + signals over IPC as the one lifecycle vocabulary every process speaks — the + POSIX.1-1990 words with message delivery instead of stack hijack, the stable + `runtime.process` interface, exit reasons, published exit events any stateful + service can subscribe to (the VFS releasing dead clients' handles), and the two + iron rules (cleanup is the kernel's job; kill is not a signal). +17. **[device-manager.md](device-manager.md) — the device manager.** Design: the + tree, the matcher, and the supervisor. Tree structure lives in the manager, + authority stays in the kernel; bus drivers report what they see; drivers are + restarted through the lifecycle vocabulary — the plan that turns + [resilience.md](resilience.md)'s restart goal into increments. +18. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard, mouse, joystick): why a synchronous rendezvous can't fan out to many listeners, the asynchronous `ipc_send` primitive built to fix it, and the per-device subscribe/publish service layered on top. -17. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and +19. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and how `while (true) hlt` parks the CPU safely once there's nothing left to do. Start with the north star: diff --git a/docs/coding-standards.md b/docs/coding-standards.md index 5b5e678..d074949 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -150,3 +150,19 @@ input output" in code — that expansion is what the acronym *is for*. But `msg` test for "is this an abbreviation I must expand" is simply: *is there a longer word this is a clipped form of?* If yes, write the word. If it's an initialism standing in for a phrase, leave it. + +## Zen of Zig + +* Communicate intent precisely. +* Edge cases matter. +* Favor reading code over writing code. +* Only one obvious way to do things. +* Runtime crashes are better than bugs. +* Compile errors are better than runtime crashes. +* Incremental improvements. +* Avoid local maximums. +* Reduce the amount one must remember. +* Focus on code rather than style. +* Resource allocation may fail; resource deallocation must succeed. +* Memory is a resource. +* Together we serve the users. diff --git a/docs/device-manager.md b/docs/device-manager.md new file mode 100644 index 0000000..aecc3ee --- /dev/null +++ b/docs/device-manager.md @@ -0,0 +1,144 @@ +# The device manager + +**Status: design.** The primitives this builds on are real ([process-management.md](process-management.md): +spawn/supervise/kill/exit-notification; [driver-model.md](driver-model.md): the device +table as a capability system; [drivers.md](drivers.md): claim/map/IRQ), and the first +per-device driver spawn works (the device manager matches the xHCI controller by PCI +class and spawns `usb-xhci-bus` with the device id as argv[1]). This document designs +the rest: the device manager as **the tree, the matcher, and the supervisor** — the +policy process that turns [resilience.md](resilience.md)'s restart goal into practice +for drivers. + +How processes stop, reload, and report their deaths is deliberately **not** in this +document: that is the universal lifecycle every danos process speaks — +[process-lifecycle.md](process-lifecycle.md), signals over IPC and the stable +`runtime.process` interface. The device manager is that design's first serious +customer, not its owner. Its own protocol contains nothing lifecycle-shaped; a +driver is stopped, health-checked, and buried exactly like any other process. + +## The tree: structure in the manager, authority in the kernel + +The device tree is two things fused: *information* (what exists, how it nests) and +*authority* (a descriptor is a licence to map physical memory). They separate: + +- The **kernel keeps the capability system** — device, I/O-port, and interrupt + claims, resource containment on `device_register`, the + `mmio_map`/`irq_bind`/`msi_bind` gates — and **cleans all of it up when a process + dies** (settled; it is increment 1 of + [process-lifecycle.md](process-lifecycle.md)). The three invariants in + [driver-model.md](driver-model.md) stay exactly where they are. A device manager + that could mint MMIO mappings by its own say-so would be a second kernel, and a + buggy one would un-earn everything the microkernel bought. +- The **device manager owns the tree as data** — identity, topology, naming, driver + matching, hotplug events, and being the one process everything else asks about + devices. Firmware discovery seeds it (today via the kernel's snapshot); **bus + drivers grow it** by reporting what they see; applications query and watch it. + `device_enumerate` fades to a manager-internal (then deleted) seam. + +Long-term, discovery itself leaves the kernel — but not *into* the manager. PCI +enumeration is a **pci-bus driver**: the manager spawns it against the host bridge +(already a device with the ECAM window as a resource), it scans, it reports functions +like any bus reports children. ACPI becomes an **acpi service** that interprets the +tables and reports the namespace. The manager only orchestrates and merges. Moving +AML interpretation out of ring 0 is its own project on its own track; nothing here +depends on when it lands. + +## The protocol + +A `device-manager-protocol` module (the vfs-protocol pattern): extern-struct +messages, a version in the handshake, reserved fields everywhere. The manager is a +well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is +talking; the same endpoint receives its children's exit notifications — one loop, +one world. + +| Direction | Message | Purpose | +|---|---|---| +| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock | +| bus → manager | `child_added { parent, identity, resources }` | one node the bus discovered | +| bus → manager | `child_removed { id }` | unplug, or the bus lost it | +| app → manager | `enumerate` | snapshot of the tree (read-only) | +| app → manager | `subscribe` | receive published add/remove events | + +`hello` is the one deadline the manager enforces itself: spawned and silent past the +deadline means wrong binary, wrong protocol version, or wedged before main — apply +the stop sequence and the restart policy. Everything else lifecycle-shaped +(terminate, the common `ping` liveness call, exit reasons) arrives through +[process-lifecycle.md](process-lifecycle.md)'s vocabulary, not this protocol. + +Assignment stays argv (`usb-xhci-bus `) for now — simple, and it works. +The step after `hello` exists is delegation: the manager claims (or is granted) the +devices and passes the claim to the driver over IPC (the M13 capability-transfer +mechanism), replacing first-come-first-served `device_claim` with policy. Identity in +`child_added` is per-bus: PCI children carry the class triple (`pci_class`, as the +xHCI match already uses); USB children carry the (class, subclass, protocol) triple +from usb-ids.zig — each bus's native language, decoded by the shared ids modules. + +## Supervision and restart + +Every driver is spawned with the manager's exit endpoint (`spawnSupervised` — built). +On a death notification: + +1. **Read the reason** ([process-lifecycle.md](process-lifecycle.md) increment 2). + Clean exit → it meant to; don't restart. Fault or missed `hello` deadline → + restart with **backoff**, and a crash-loop cap (three fast deaths → mark failed, + stop respawning, log loudly; a later `reload` to the manager can retry). +2. **Prune the subtree** the dead bus driver reported. Its children describe + protocol state (xHCI slot ids, transfer rings) that died with the process; + keeping the nodes would be keeping a lie. Watchers receive `child_removed` — the + input service losing, then regaining, a keyboard is the *honest* description of + what happened. The restarted instance rediscovers and re-reports. +3. **The claim is already free** because the kernel released it at death — the + restarted instance claims the same controller and comes up. + +Who supervises the supervisor: **init** (PID 1), which already supervises the +services it starts. If the manager dies, drivers keep running (they hold their +claims; the kernel doesn't care who their supervisor was — though their exit +notifications now dangle harmlessly). The restarted manager re-learns the world: +kernel snapshot, then a re-`hello` round — drivers answer a broadcast or are stopped +and respawned. Full state handoff is deliberately not attempted. + +## Thin drivers, class protocols + +The [driver-model.md](driver-model.md) three-shape split, restated as processes: + +- A **bus driver** (usb-xhci-bus) owns its controller — claim, MMIO, IRQ/MSI, DMA + rings — and offers a *transfer* protocol ("submit a control transfer to device N", + built from the usb-abi request constructors) plus tree reports to the manager. +- A **class driver** (usb-hid, usb-storage) owns nothing: it is matched to a reported + child by its identity triple, speaks the bus's transfer protocol downward and its + service's protocol upward — HID reports to the input service, blocks to the block + service. It works unchanged over any controller. +- **Services** (input, display, block) aggregate class drivers and face applications. + +Each arrow is a protocol module. The manager routes none of the data plane — it +introduces the parties (matching), supervises them (lifecycle), and gets out of the +way. + +## Increments + +Increments 1–4 are the lifecycle prerequisites and live in +[process-lifecycle.md](process-lifecycle.md) (claim cleanup on death, exit reasons, +published exit events, signals + `runtime.process`). On top of those: + +5. **device-manager-protocol**: `hello`, supervised spawn with restart policy; + usb-xhci-bus becomes the first conforming driver. +6. **Tree reports**: `child_added`/`child_removed`; the manager mirrors; xHCI reports + the mouse and keyboard QEMU already hangs off it. +7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats + to a manager-internal seam. +8. **Discovery migration**: pci-bus driver first, acpi service second, kernel scan + retired last. (AML-in-user-space is its own track.) + +## Settled questions (2026-07-12) + +- **Stateful buses**: pruning the subtree on bus-driver death is right for USB. A + future storage bus with in-flight writes wants drain-before-terminate — which is + exactly the `deadline_ms` parameter `stop()` already has; a per-driver deadline + is one value in the manager's policy table when such a bus arrives. No design + change. +- **Manager death**: drivers survive the manager; the restarted manager re-learns + the world (above). Checkpointing driver state with the manager is deferred until + something demonstrates the need. +- **Matching stays code until the third bus.** `driverFor`/`pciDriverFor` are + honest at two bus types; the third triggers the manifest (a driver declares what + it binds: a PCI class triple, a USB class triple, an ACPI `_HID`). diff --git a/docs/m17-m18-plan.md b/docs/m17-m18-plan.md new file mode 100644 index 0000000..b0a0f46 --- /dev/null +++ b/docs/m17-m18-plan.md @@ -0,0 +1,161 @@ +# M17–M18 execution plan: process lifecycle + device manager + +The operational plan for building [process-lifecycle.md](process-lifecycle.md) +(M17) and [device-manager.md](device-manager.md) increments 5–7 (M18). Design is +settled in those documents; this file is the build order — one phase at a time, +each phase green before the next starts. Delete or archive this file when M18 +lands. + +**Definition of green, every phase:** `zig build` clean, `zig build test` clean, +`python3 test/qemu_test.py` passes (existing scenarios plus the phase's new one), +and the relevant design doc's "known gaps" / status lines updated. Commit per +green phase (no co-author trailers). + +**Workflow (settled 2026-07-12):** work happens in a dedicated git worktree, on +feature branches cut from `main` — `feat/process-lifecycle` (M17.1–17.4), +`feat/device-manager` (M18.1), `feat/usb-xhci-bus` (M18.2–18.3). When a branch's +phases are all green it is **auto-merged into `main`**; branches are kept after +merge, not deleted. Merges and branches are pushed to origin. Phase 0 (once): +commit the design docs, merge the outstanding `feat/usb` work into `main`, and +run the existing QEMU suite green before any new work starts. + +**Numbering note:** continues the milestone sequence (driver track ended at M16). + +--- + +## M17.1 — the kernel releases a dead process's claims + +The cleanup half of iron rule 1; the prerequisite for every restart story. + +- `system/kernel/devices-broker.zig`: `releaseAllOwnedBy(owner: u32)` — clear + every `claimed[]` slot holding this task id. +- `system/kernel/process.zig`: call it from the reap path, alongside the existing + IRQ-binding release (the ordering comment there says why IRQs go first — claims + slot in after them, before the exit notification). +- MSI vectors: find where `msi_bind` records per-device vectors (interrupts + module) and release those by owner in the same pass. +- Docs: remove the claims bullet from process-management.md "Known gaps". + +**Test:** new QEMU scenario `claim-release` — a test child claims an unclaimed +device, is killed, is respawned, and claims the same device again successfully; +assert both claims in the serial log. Kernel-side unit coverage in +`system/kernel/tests.zig` for `releaseAllOwnedBy` (claim two devices as two owners, +release one owner, verify exactly its claims freed). + +## M17.2 — exit reasons + +- `system/abi.zig`: `ExitReason` (exited, aborted, segmentation_fault, + illegal_instruction, arithmetic_fault, killed). +- Kernel: record the reason at every death site — clean exit path, each fault + class in `onException`, the kill path. Bounded recent-exits table (ids are never + reused, so a small ring keyed by id is enough). +- New system call `process_exit_reason(id)` — supervisor-gated, like kill; returns + the recorded reason or `-ESRCH` once evicted. +- `library/runtime/process.zig`: `ExitReason` + `exitReason(id: u32)`. +- Docs: remove the no-exit-status bullet from process-management.md. + +**Test:** extend the `supervision` scenario — three children: one exits cleanly, +one faults (the fault-recovery pattern), one is killed; the supervisor asserts all +three reasons. + +## M17.3 — published exit events + +- Kernel: bounded subscriber table (endpoints); new system call + `process_subscribe(endpoint)` (ungated, like `process_enumerate`); every death + posts `notify_exit_bit | id` to each subscriber — the same post the supervisor + path already uses. +- `library/runtime/process.zig`: `subscribeExits(endpoint)`. +- VFS becomes the first subscriber: on an exit event, release every handle keyed + by that task id (badges already are task ids). Log the release. +- Docs: note the convention in ipc.md (exit events reuse the exit-notification + badge encoding). + +**Test:** new QEMU scenario `vfs-client-death` — a client opens a file and is +killed without closing; assert the VFS logs the handle release and its open-handle +count returns to baseline. + +## M17.4 — signals and the service harness + +- Kernel: per-task pending mask + bound endpoint; system calls + `signal_bind(endpoint)` and `process_signal(id, signal)` (supervisor-or-self + gated); delivery posts `notify_signal_bit | pending mask`, coalescing; pending + signals with no bound endpoint pend silently. +- `library/runtime/process.zig`: `Signal`, `SignalSet`, `bindSignals`, + `signalsFrom`, `sendSignal`, `stop(id, deadline_ms)` (terminate → wait for exit + notification → kill). Implement `terminate`, `reload`, `user_1`, `user_2`; + `interrupt`/`quit` are enum members with no sender yet; `alarm` stays unbuilt. +- Kernel: **one-shot timer notifications** — `timer_bind(endpoint, ms)` posts a + notification badge when the deadline lands (IRQ-as-IPC again, on the timer + wheel `sleep` already uses). This is the missing timed-wait primitive: + `replyWait` blocks forever and `sleep` blocks the whole process, but `stop()`'s + escalation, the device manager's `hello` deadline (M18.1), and restart backoff + all need a deadline while staying responsive. It is also the mechanism `alarm` + gets for free later. +- New `library/runtime/service.zig`: the harness — `run(callbacks)` owning the + replyWait loop, folding protocol messages, signals, and child-exit notifications + into `init` / `on_message` / `on_reload` / `on_terminate`; answers the common + `ping` automatically. Define the reserved `ping` request encoding here and + document it in ipc.md (one obvious encoding; smallest that cannot collide with + existing protocols). +- Convert one existing service (input-source or hpet) to the harness as proof it + subtracts code rather than adding it. + +**Test:** extend `supervision` — a harness-built child: `sendSignal(reload)` +observed in its log, `ping` answered, `stop()` produces a clean exit with reason +`exited`; a second child that ignores signals (no bind) is killed by `stop()`'s +deadline with reason `killed`. + +## M18.1 — device-manager protocol: hello + restart policy + +- New `system/services/device-manager/device-manager-protocol.zig` module + (vfs-protocol pattern): `hello { version, role, device_id }`; version constant; + reserved fields. +- Device manager: register the `.device_manager` endpoint; spawn drivers with its + exit endpoint; enforce the hello deadline; restart policy — backoff, crash-loop + cap (three fast deaths → mark failed, log, stop), reasons from M17.2 deciding + restart vs not. +- usb-xhci-bus: adopt the harness + send hello. hpet/ps2-bus follow only if the + conversion is mechanical; otherwise they keep working unconverted (the manager + only enforces hello on drivers spawned with an assignment). +- build.zig: test-loop entry for the protocol module if it grows pure logic. + +**Test:** new QEMU scenario `driver-restart` — the xHCI driver takes a test-only +argv flag to fault after hello on its first run; assert: fault, exit reason +recorded, manager respawns with backoff, second run claims the controller +(M17.1) and hellos clean. Assert the crash-loop cap by a driver that always +faults (a tiny test driver, not xhci). + +## M18.2 — bus tree reports + +- Protocol: `child_added { parent, identity, resources }` / `child_removed { id }`. +- usb-xhci-bus: bring-up to **port scan only** — map the MMIO window (claimed in + M16-era work), controller reset/start per xHCI spec, walk the port registers, + report one `child_added` per connected port with speed + port number as + identity. **No transfer rings, no descriptors** — reading device/interface + descriptors (and therefore USB class triples for matching) is the follow-on USB + track, not this plan. +- Device manager: mirror reports into its tree; prune the subtree (emitting + `child_removed`) when a bus driver dies; assert re-report on restart. + +**Test:** QEMU already attaches usb-kbd + usb-mouse on xhci.0 — assert two +`child_added` events reach the manager and appear in its tree dump; kill the +driver, assert two `child_removed` then two fresh `child_added` after respawn. + +## M18.3 — the application surface + +- Protocol: `enumerate` (tree snapshot) + `subscribe` (published add/remove + events, input-service pattern). +- A small client (`device-list`, the `ps` analog) exercising both; the manager + becomes the one answer to "what devices exist" for user space. + `device_enumerate` stays for drivers/kernel seeding — its retreat is tied to the + discovery migration, out of this plan. + +**Test:** QEMU scenario — `device-list` shows the tree including USB children; +during a driver restart the subscribing client logs remove + add events. + +--- + +**Explicitly out of scope** (own tracks, after M18): discovery migration (pci-bus +driver, acpi service, retiring the kernel scan), USB control transfers + +descriptors + class-driver matching, the musl layer, `interrupt`/`quit` senders +(needs a console), job control. diff --git a/docs/process-lifecycle.md b/docs/process-lifecycle.md new file mode 100644 index 0000000..6944e8a --- /dev/null +++ b/docs/process-lifecycle.md @@ -0,0 +1,321 @@ +# Process lifecycle: signals over IPC + +**Status: design.** The primitives underneath are built ([process-management.md](process-management.md): +spawn, the supervision link, kill, child-exit notifications); this document designs +the layer above them — the standard vocabulary a danos process speaks about its own +life, and the stable `runtime.process` interface that carries it. Nothing here is +device- or driver-specific: a driver, the VFS, and a user application all stop, +reload, and die the same way. The device manager is simply this design's first +serious customer ([device-manager.md](device-manager.md)). + +**"POSIX" in this document means the concepts, never the letter of the standard.** +danos borrows the ideas and the hard-won lessons (what SIGTERM *means*, why SIGPIPE +was a mistake) without inheriting the mechanism, the API, or the names. The naming +rule is danos's own and it is strict: plain words that communicate intent +(`terminate`, `reload`, `exited`) and the IPC vocabulary the system already speaks +(`bind`, `subscribe`, `publish`, `endpoint`) — never `SIG*`, never a second word for +a concept that already has one. Literal POSIX arrives later and lives elsewhere: a +**musl-based C layer** (growing out of library/posix) that wires C programs to the +danos runtime — musl's syscall surface retargeted at danos system calls and IPC +protocols (files onto the VFS protocol, `sigaction`/`wait` onto this lifecycle, +sockets onto whatever networking becomes). Ported programs see POSIX; the system +underneath never does. + +## Why a standard vocabulary + +A supervisor can only manage processes it has never heard of if "please exit" means +the same thing to all of them. That is the one thing POSIX signals got deeply right: +`SIGTERM` means the same thing to nginx and to a five-line script, which is why +process supervision on Unix (init systems, container runtimes) is possible at all. +danos wants that property from day one, because supervision-and-restart is the +system's core motivation ([resilience.md](resilience.md)). + +What POSIX got wrong — for a system like this — is the **delivery mechanism**: +asynchronous control-flow hijack. A Unix handler runs on a stolen stack at an +arbitrary instruction boundary, which is why the async-signal-safe function list +exists, why `errno` must be saved, and why the canonical signal bug is a SIGTERM +handler innocently calling `printf` mid-`malloc`. That entire bug class comes from +the mechanism, not the vocabulary, and none of it is worth importing. + +A microkernel already has the right channel: **a signal is a message.** QNX delivers +POSIX signals over its message passing; seL4 has notification objects; Erlang turned +"death is a message to whoever linked" into a reliability philosophy. danos has +already done it once without naming it: a child's death arrives as a notification +badge on the supervisor's endpoint — the microkernel's SIGCHLD, the IRQ-as-IPC +pattern reused. Signals are the same pattern reused a third time. + +## The mechanism + +- **`signal_bind(endpoint)`** — a process nominates the endpoint its signals arrive + on, exactly as `irq_bind` nominates where a device's interrupts land. The runtime + does this at startup for any program that opts in. +- **`process_signal(id, signal)`** — posts the signal as an asynchronous + notification to the target's bound endpoint: badge = `notify_badge_bit | + notify_signal_bit | pending signals`. Non-blocking for the sender, always. +- **Pending signals coalesce** in a per-process bitmask until the target next waits + — exactly like interrupt notifications, and exactly POSIX's own semantics for + non-realtime signals (two pending SIGTERMs are one SIGTERM). The bitmask *is* the + design: signals carry no payload. Anything with a payload is a protocol message. +- **Authority**: the supervisor may signal its children — the same link that is + already the kill authority. A process may signal itself. Anything broader waits + for transferable process handles. +- **No binding, no problem**: a process that never calls `signal_bind` is not + broken — its signals pend unread and only `process_kill` works on it. Simple + programs stay simple; the vocabulary is opt-in, the kill authority is not. + +Because delivery is a message into the process's own event loop, there is no +async-signal-safe list in danos: a handler is ordinary code running at a point the +process chose. The bug class is gone by construction, not by discipline. + +## The vocabulary: POSIX.1-1990, sorted honestly + +The full 1990 set, and what each becomes. Two intrinsically problematic cases get a +defense below the table. + +| POSIX.1-1990 | danos disposition | Notes | +|---|---|---| +| SIGTERM | signal `terminate` | finish up and exit; the supervisor's polite half | +| SIGHUP | signal `reload` | re-read configuration / re-scan | +| SIGINT | signal `interrupt` | interactive interrupt; meaningful once a console can send it, in the vocabulary now so numbering is stable | +| SIGQUIT | signal `quit` | as SIGINT, without the core-dump baggage | +| SIGALRM | signal `alarm` | timer expiry as a message; the Unix SIGALRM+`longjmp` timeout hacks are impossible here. In the vocabulary, unbuilt: no consumer yet, and when one appears it is runtime sugar over the existing timer — zero kernel work | +| SIGUSR1, SIGUSR2 | signals `user_1`, `user_2` | service-defined | +| SIGCHLD | **already exists** — the exit notification | the badge carries the child id, dodging the classic coalescing bug (Unix code must loop `waitpid`) | +| SIGKILL | `process_kill` — kernel mechanism | its definition is "cannot be handled"; it was never really a signal | +| SIGABRT | exit reason `abort` | `abort()` is synchronous self-termination, not an event | +| SIGSEGV, SIGILL, SIGFPE | exit reasons, **never delivered** | see below | +| SIGPIPE | **an error return**, not a signal | see below | +| SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGCONT | deferred | job control needs terminals, sessions, and process groups; stop/continue is scheduler territory | + +**The fault signals (SIGSEGV, SIGILL, SIGFPE) are intrinsically wrong for messages.** +They are *synchronous* — raised at a specific faulting instruction, not "sometime +soon". A message cannot be delivered to a process whose next instruction re-faults; +it never reaches its event loop to read it. POSIX only makes fault handlers "work" +via the async hijack (run the handler *instead of* the instruction), and even there, +returning from a SIGSEGV handler without curing the cause is undefined behavior. +danos's architecture already has the better answer: fault → the kernel kills the +process ([resilience.md](resilience.md) step 2, built) → the supervisor reads the +reason → restart. Recovery is restart, not a handler. This is also truer to the 1990 +standard than handling is: the standard's default action for all three was +"terminate the process". + +**SIGPIPE deserves special contempt.** Its default kills a process that writes to a +closed pipe — which is why "the whole server died because one client disconnected" +is roughly every network daemon's first production bug, and why every mature codebase +contains the same fix: ignore SIGPIPE, handle the `EPIPE` error return. danos made +the right choice natively already — a reply owed to a dead peer fails with `-EPEER`. +Errors from operations are error returns from those operations. The posix layer can +synthesize SIGPIPE for ported code that expects it. + +### Statements, not questions + +A signal and a protocol message both travel over IPC — the difference is the +**contract**, not the transport. danos IPC has two primitives, both already in +daily use: the **asynchronous notification** (a badge — bits that coalesce into a +pending mask; the sender never blocks; no payload, *no reply path*; how IRQs and +exit events arrive) and the **synchronous call** (a rendezvous — payload both +ways, the caller waits for the reply; how VFS requests work). A signal is the +first kind: a *statement*. `terminate` wants no reply — the exit notification is +its acknowledgement. + +A health probe is the second kind: a *question*, worthless without its answer — +and the answer's absence within a deadline is the very thing being measured. +Asked as a signal it has no reply channel (a coalescing bit can't carry an answer, +and the authority rule forbids a child signalling its supervisor back); asked as a +call, the timeout-is-the-diagnosis semantics come free. So there is no `health` +signal. Liveness is the common **`ping`**: a reserved request every harness-run +service answers automatically on its main endpoint — still free for the service +author, still one obvious way — and a supervisor's probe is a `ping` call with a +deadline. + +## The two iron rules + +1. **Cleanup is the kernel's job.** A process can die with no warning — fault, + kill, power. Correctness must never depend on a `terminate` handler running. On + any death the kernel releases the address space, IPC handles, IRQ bindings, and + owed replies (built), and must also release **device, I/O-port, and interrupt + claims and MSI vectors** (the known gap in + [process-management.md](process-management.md); increment 1). A signal handler is + for *graceful* work — flushing, deregistering, saving — never for *necessary* + work. +2. **Kill is not a signal, and exit reasons are load-bearing.** The standard stop + sequence is *terminate → deadline → `process_kill`*; the unhandleable kill stays + a kernel mechanism. And a supervisor deciding whether to restart must know *how* + the child died: clean exit (meant to — don't restart), fault (restart with + backoff), killed (the supervisor did it). The exit notification today carries + only the id; it grows a reason. Restart policy cannot be written without it. + +## Who learns of a death + +A death has three audiences, and conflating them is how systems end up with either +zombie state or privileged snooping: + +1. **The supervisor** — gets the exit notification on the endpoint it gave at spawn + (built), which grows the `ExitReason` (increment 2). The supervisor is the only + audience that needs the *reason*, because it is the only one deciding whether to + restart. +2. **The peer owed a reply** — already built: a client that dies mid-request fails + the server's reply with `-EPEER`; a server that dies fails its waiting clients + the same way. This covers the *synchronous* case only. +3. **The subscribers** — the new piece, and it is the input service's + publish/subscribe shape ([input.md](input.md)) applied to exits. A stateful + service accumulates per-client state across many requests: the VFS holds a dead + client's open file handles, the input service holds its subscriptions, a future + network stack holds its sockets. None of these are the client's supervisor, and + none learn anything from a failed reply if the client simply never calls again. + So the kernel **publishes every exit** to whoever subscribed: + `process_subscribe(endpoint)` adds a subscriber, and each death posts a + notification to every subscriber (badge = `notify_exit_bit | process id` — the + same encoding supervisors already decode, the IRQ-as-IPC pattern once more). The + subscriber filters for ids it holds state for and releases what the dead client + held. Correlating is free of bookkeeping: an IPC sender's badge already *is* its + task id (`runtime.ipc.Received`), so the id a service has been keying client + state by all along is the id the exit event carries. + + Subscription, not broadcast-to-everyone: only processes that asked receive + events, the kernel keeps a bounded subscriber table, and delivery is the same + non-blocking coalescing notification as everything else — a dying process never + waits on its mourners. Subscribing is ungated, like `process_enumerate`: what is + running (and dying) is not a secret between cooperating processes. Subscribers + do not receive the exit reason — the VFS does not care *why* the client died. + +This is the service-side mirror of iron rule 1: **a service must never depend on +its clients cleaning up after themselves.** Handle release on client death is the +service's job, triggered by the published exit event — never by a courtesy +"closing now" message that a crashed client will never send. + +## The stable interface: `runtime.process` + +`runtime.process` already owns what a process receives at birth (`Init`, the +argv contract). It grows to own the other end of life. + +**The runtime is the stable interface; the numbers are not.** danos applications do +not make system calls — they call the runtime library, and the system-call numbers, +notification bits, and signal bit positions beneath it are a **private kernel ↔ +runtime contract** that may change at any time (settled 2026-07-12). This is why +the runtime exists. Today kernel and runtime ship from one tree in one image, so +"stability" is simply building them together. When driver binaries start shipping +as separately-versioned applications — the whole point of the restart design — the +binary's embedded runtime version becomes compatibility metadata (the same idea as +the protocol version in the device manager's `hello`), and the kernel refuses what +it cannot serve. Signals therefore need no reserved numbering scheme: the enum +below is vocabulary, not ABI. + +```zig +/// The signal vocabulary. The value is the bit position in the pending mask — a +/// private kernel/runtime detail, free to change while they ship together. +pub const Signal = enum(u5) { + terminate = 0, // SIGTERM: finish up and exit + reload = 1, // SIGHUP: re-read configuration + interrupt = 2, // SIGINT + quit = 3, // SIGQUIT + alarm = 4, // SIGALRM + user_1 = 5, // SIGUSR1 + user_2 = 6, // SIGUSR2 +}; + +/// A decoded pending mask: the coalesced set of signals a notification delivered. +pub const SignalSet = struct { + pending: u32, + pub fn has(set: SignalSet, signal: Signal) bool { ... } + pub fn iterate(set: SignalSet) Iterator { ... } +}; + +/// Nominate `endpoint` as this process's signal endpoint (signal_bind). The +/// runtime's service harness calls this; a bare program may call it directly and +/// fold signals into its own replyWait loop. +pub fn bindSignals(endpoint: usize) bool { ... } + +/// Decode a received badge into signals, or null if the badge is not a signal +/// notification (mirrors ipc.Received.isChildExit). +pub fn signalsFrom(badge: usize) ?SignalSet { ... } + +/// Send `signal` to process `id`. Supervisor-gated, like kill; non-blocking. +pub fn sendSignal(id: u32, signal: Signal) bool { ... } + +/// The standard stop sequence: terminate, wait up to `deadline_ms` for the exit +/// notification, then process_kill. The one call a supervisor needs. +pub fn stop(id: u32, deadline_ms: u64) void { ... } + +/// Subscribe `endpoint` to published exit events (process_subscribe). Every +/// process death posts an asynchronous notification: badge = notify_exit_bit | +/// process id — the same encoding a supervisor's exit notification uses, decoded +/// by the same ipc.Received helpers. For stateful services: release what the dead +/// client held (file handles, subscriptions, sockets). Ungated, like +/// process_enumerate. +pub fn subscribeExits(endpoint: usize) bool { ... } + +/// How a process ended — from the exit notification. What restart policy reads. +pub const ExitReason = enum { + exited, // returned from main / clean exit + aborted, // abort() — deliberate self-termination (SIGABRT's ghost) + segmentation_fault, // SIGSEGV's ghost + illegal_instruction, // SIGILL's ghost + arithmetic_fault, // SIGFPE's ghost + killed, // process_kill +}; +``` + +Two deliberate absences. There is no `mask`/`block` API — a process that is not +ready for a signal simply has not waited on its endpoint yet; the pending mask *is* +the blocked set. And there is no per-signal handler registration at this layer — +dispatch is the process's own `switch` over `SignalSet`, or the service harness's +callbacks (`on_terminate`, `on_reload`) for programs that want defaults. + +### The service harness + +`runtime.service` owns the `replyWait` loop and folds every event source — signals, +child exits, protocol messages — into callbacks, with the vocabulary's defaults: +`terminate` returns from the loop (clean exit), the common `ping` is answered automatically, +`reload` is ignored unless overridden. One loop, no locking, nothing reentrant. A +service author writes domain logic; the lifecycle contract is satisfied by the +harness. A process that bypasses the harness and ignores its signals meets the +deadline-then-kill escalation — you cannot force a process to implement an +interface, but you can make compliance free and non-compliance fatal. + +### The musl layer later + +The POSIX C layer is a **musl port**: musl's arch/syscall layer retargeted so that +what musl believes are kernel syscalls become danos runtime calls and IPC — `open` +and `read` onto the VFS protocol, `kill`/`sigaction`/`waitpid` onto this document's +vocabulary, `exit` onto the runtime's exit path. `sigaction` handlers registered +through it are invoked by the runtime's loop when the signal message arrives — +synchronous underneath, async-looking to ported code, delivered at wait boundaries +the way most Unix programs already experience signals (at syscalls). No stack hijack +ever happens, `SA_RESTART` semantics come free because nothing was interrupted, and +SIGPIPE can be synthesized from `-EPEER` for the programs that expect it. C programs +get POSIX; danos-native programs never pay for it. + +## Increments + +1. **Kernel: release device/port/IRQ claims and MSI vectors on death** — the + cleanup half of iron rule 1, and the prerequisite for any restart story. Test: + kill a claiming driver, spawn it again, the claim succeeds. +2. **Exit reason in the death notification** (`ExitReason` above). +3. **Exit events**: `process_subscribe` in the kernel (bounded subscriber table, + publishes on every death), `runtime.process.subscribeExits`; the VFS becomes the + first subscriber — releasing a dead client's handles is its proof test. +4. **Signals**: `signal_bind` + `process_signal` + the pending mask in the kernel; + `runtime.process` grows the interface above; the service harness handles + `terminate` and answers the common `ping`; `stop()` for supervisors. + +[device-manager.md](device-manager.md) builds directly on all four. + +## Settled questions (2026-07-12) + +- **Signal numbering is not ABI**: the runtime is the stable interface; the numbers + beneath it are a private kernel ↔ runtime contract (see "The stable interface"). +- **Liveness is a `ping` call, not a signal**: signals are statements, questions + are synchronous calls (see "Statements, not questions"). A service wanting *deep* + health ("can I reach my hardware?") defines its own protocol message on top. +- **Process handles: deferred.** Pids + the supervisor gate cover everything + planned; transferable handles (Fuchsia-style, delegating signalling without + delegating kill) wait for the capability table to grow types beyond endpoints. +- **`alarm`: in the vocabulary, unbuilt.** No consumer yet; when one appears it is + runtime sugar over the existing timer (arm a timer that posts your own signal) — + zero kernel work, so deferring costs nothing. +- **Subscription granularity: all exits**, subscriber-side filtering — one + subscription per service, a bounded kernel table. Per-id subscriptions only if + event volume ever matters (hundreds of processes, not before). +- **Client identity across the exit boundary: no convention needed** — an IPC + sender's badge already is its task id (see "Who learns of a death"). diff --git a/library/runtime/device.zig b/library/runtime/device.zig index 0bc1525..3bb5217 100644 --- a/library/runtime/device.zig +++ b/library/runtime/device.zig @@ -37,6 +37,10 @@ pub fn mmioMap(device_id: u64, resource_index: u64) ?usize { /// `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. @@ -124,4 +128,4 @@ pub fn findDeviceDescriptorByHid(buffer: []DeviceDescriptor, hid_needle: []const } return null; -} \ No newline at end of file +} diff --git a/library/runtime/input.zig b/library/runtime/input.zig index bac3f4d..64a771a 100644 --- a/library/runtime/input.zig +++ b/library/runtime/input.zig @@ -37,7 +37,8 @@ pub const MouseEventKind = protocol.MouseEventKind; pub const JoystickEventKind = protocol.JoystickEventKind; pub const Keycode = protocol.Keycode; -/// Interest masks re-exported so a caller can `subscribe(input.device_keyboard | input.device_mouse)`. +/// Interest masks re-exported so a caller can `subscribe(input.device_keyboard | +/// input.device_mouse)`. pub const device_keyboard = protocol.device_keyboard; pub const device_mouse = protocol.device_mouse; pub const device_joystick = protocol.device_joystick; diff --git a/library/runtime/system-call.zig b/library/runtime/system-call.zig index 016be8f..5406a90 100644 --- a/library/runtime/system-call.zig +++ b/library/runtime/system-call.zig @@ -19,34 +19,49 @@ pub inline fn systemCall0(n: SystemCall) usize { pub inline fn systemCall1(n: SystemCall, a0: usize) usize { return asm volatile ("syscall" : [ret] "={rax}" (-> usize), - : [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), + : [n] "{rax}" (@intFromEnum(n)), + [a0] "{rdi}" (a0), : .{ .rcx = true, .r11 = true, .memory = true }); } pub inline fn systemCall2(n: SystemCall, a0: usize, a1: usize) usize { return asm volatile ("syscall" : [ret] "={rax}" (-> usize), - : [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), + : [n] "{rax}" (@intFromEnum(n)), + [a0] "{rdi}" (a0), + [a1] "{rsi}" (a1), : .{ .rcx = true, .r11 = true, .memory = true }); } pub inline fn systemCall3(n: SystemCall, a0: usize, a1: usize, a2: usize) usize { return asm volatile ("syscall" : [ret] "={rax}" (-> usize), - : [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2), + : [n] "{rax}" (@intFromEnum(n)), + [a0] "{rdi}" (a0), + [a1] "{rsi}" (a1), + [a2] "{rdx}" (a2), : .{ .rcx = true, .r11 = true, .memory = true }); } pub inline fn systemCall4(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize) usize { return asm volatile ("syscall" : [ret] "={rax}" (-> usize), - : [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2), [a3] "{r10}" (a3), + : [n] "{rax}" (@intFromEnum(n)), + [a0] "{rdi}" (a0), + [a1] "{rsi}" (a1), + [a2] "{rdx}" (a2), + [a3] "{r10}" (a3), : .{ .rcx = true, .r11 = true, .memory = true }); } pub inline fn systemCall5(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize) usize { return asm volatile ("syscall" : [ret] "={rax}" (-> usize), - : [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2), [a3] "{r10}" (a3), [a4] "{r8}" (a4), + : [n] "{rax}" (@intFromEnum(n)), + [a0] "{rdi}" (a0), + [a1] "{rsi}" (a1), + [a2] "{rdx}" (a2), + [a3] "{r10}" (a3), + [a4] "{r8}" (a4), : .{ .rcx = true, .r11 = true, .memory = true }); } diff --git a/system/boot-handoff.zig b/system/boot-handoff.zig index 1ce2d87..d357c2b 100644 --- a/system/boot-handoff.zig +++ b/system/boot-handoff.zig @@ -32,10 +32,10 @@ pub const PixelFormat = enum(u32) { /// Output Protocol (a headless server, say). The kernel must treat on-screen /// output as optional and never assume a framebuffer exists. pub const Framebuffer = extern struct { - base: usize, // the memory address where pixel data starts (0 = none) - width: u32, // visible pixels per row (e.g. 1920) - height: u32, // visible rows (e.g. 1080) - pitch: u32, // bytes from the start of one row to the start of the next + base: usize, // the memory address where pixel data starts (0 = none) + width: u32, // visible pixels per row (e.g. 1920) + height: u32, // visible rows (e.g. 1080) + pitch: u32, // bytes from the start of one row to the start of the next format: PixelFormat, /// Whether a usable framebuffer was handed over. diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index 2f13a49..2f68c15 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -52,7 +52,8 @@ pub const PowerInformation = struct { reset: RegisterAccess = .{}, reset_value: u8 = 0, reset_supported: bool = false, - /// SLP_TYP values for S5 (soft off) and S3 (suspend), from the AML sleep-state (`_Sx`) packages. + /// SLP_TYP values for S5 (soft off) and S3 (suspend), from the AML sleep-state (`_Sx`) + /// packages. s5: ?aml.SleepType = null, s3: ?aml.SleepType = null, }; @@ -198,7 +199,8 @@ const ExtendedSystemDescriptorPointer = extern struct { root_system_description_table_address: u32 align(1), /// The size of the RSDP. length: u32 align(1), - /// A 64-bit physical address pointing to the XSDT. If the revision is at least 2, the XSDT should be used regardless of architecture, as the RSDT was deprecated. + /// A 64-bit physical address pointing to the XSDT. If the revision is at least 2, the XSDT + /// should be used regardless of architecture, as the RSDT was deprecated. extended_system_descriptor_table_address: u64 align(1), /// A checksum used for the entire table. extended_checksum: u8, diff --git a/system/devices/aml/aml.zig b/system/devices/aml/aml.zig index c7386ba..8a37648 100644 --- a/system/devices/aml/aml.zig +++ b/system/devices/aml/aml.zig @@ -126,17 +126,22 @@ test "parses a nested namespace and finds the sleep package" { // Scope(\_SB) packagelen=0x27 0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F, // Device(PCI0) packagelen=0x1F - 0x5B, 0x82, 0x1F, 0x50, 0x43, 0x49, 0x30, + 0x5B, 0x82, 0x1F, 0x50, 0x43, + 0x49, 0x30, // Name(_HID, 0x11) 0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11, // Method(MTHD, flags=1) empty, packagelen=0x06 - 0x14, 0x06, 0x4D, 0x54, 0x48, 0x44, 0x01, + 0x14, 0x06, 0x4D, + 0x54, 0x48, 0x44, 0x01, // Method(CALL, flags=0) { MTHD(Zero) }, packagelen=0x0B - 0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D, 0x54, 0x48, 0x44, 0x00, + 0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D, + 0x54, 0x48, 0x44, 0x00, // OperationRegion(DBG0, SystemIO, Word 0x0402, Byte 1) - 0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B, 0x02, 0x04, 0x0A, 0x01, + 0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B, + 0x02, 0x04, 0x0A, 0x01, // Field(DBG0, flags=1) { DBGB, 8 }, packagelen=0x0B - 0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01, 0x44, 0x42, 0x47, 0x42, 0x08, + 0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01, + 0x44, 0x42, 0x47, 0x42, 0x08, }; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); diff --git a/system/devices/device-abi.zig b/system/devices/device-abi.zig index 35f4c96..9b04bf4 100644 --- a/system/devices/device-abi.zig +++ b/system/devices/device-abi.zig @@ -56,6 +56,10 @@ pub const maximum_device_resources = 8; /// `DeviceDescriptor.parent` for a device with no parent — a root of the device tree. pub const no_parent: u64 = ~@as(u64, 0); +/// `DeviceDescriptor.pci_class` for a device that is not a PCI function. (Zero would be +/// ambiguous: 0x000000 is a real class code, "unclassified device".) +pub const no_pci_class: u64 = ~@as(u64, 0); + /// A device, as snapshotted for user space by `device_enumerate`. A driver scans /// these to find the hardware it owns, claims it, and maps its MMIO. /// @@ -69,6 +73,11 @@ pub const DeviceDescriptor = extern struct { id: u64, parent: u64, // a device id, or `no_parent` class: u64, // a DeviceClass value + // The PCI class/subclass/prog-IF triple packed as 0xCCSSPP when this device is a PCI + // function, or `no_pci_class` otherwise. This is how a manager tells *what* a + // `pci_device` is (an xHCI controller, an AHCI controller) — decode the triple into + // names with the pci-class module. + pci_class: u64, hid_len: u64, resource_count: u64, hid: [8]u8, diff --git a/system/devices/device-model.zig b/system/devices/device-model.zig index 84f63d0..3629645 100644 --- a/system/devices/device-model.zig +++ b/system/devices/device-model.zig @@ -198,8 +198,7 @@ fn dumpNode(device: *const Device, depth: usize, emit: *const fn ([]const u8) vo std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s} ({s})\n", .{ device.name(), @tagName(device.class), device.hid(), desc }) catch return else std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s}\n", .{ device.name(), @tagName(device.class), device.hid() }) catch return; - } else - std.fmt.bufPrint(buffer[indent..], "{s} [{s}]\n", .{ device.name(), @tagName(device.class) }) catch return; + } else std.fmt.bufPrint(buffer[indent..], "{s} [{s}]\n", .{ device.name(), @tagName(device.class) }) catch return; emit(buffer[0 .. indent + body.len]); // For a PCI function, decode its class code — the (class / subclass / prog-IF) diff --git a/system/devices/usb-abi.zig b/system/devices/usb-abi.zig new file mode 100644 index 0000000..d94bb6c --- /dev/null +++ b/system/devices/usb-abi.zig @@ -0,0 +1,857 @@ +//! USB device-framework wire ABI: the set-up packets, standard requests, and standard +//! descriptors every USB device speaks over its default control pipe, as defined by chapter 9 +//! of the USB 2.0 specification (see https://wiki.osdev.org/Universal_Serial_Bus). Pure data +//! definitions — no hardware access — shared by the host-controller bus drivers (which build +//! the requests) and anything that parses what devices return (device naming, driver +//! matching, configuration). The structs mirror the wire byte-for-byte: multi-byte fields are +//! little-endian and align(1), so a descriptor can be bit-cast straight out of a transfer +//! buffer at any offset, and bitmap bytes are packed structs so no caller ever needs a magic +//! mask. Class, subclass, and protocol code tables live in usb-ids.zig. + +const DeviceState = enum(u8) { + // Immediately after the USB device is attached to the USB system, it is in this state. + // The USB specifications do not define the state of a USB device that is detached from + // a USB system. + attached, + // A device is in this state after it has both been attached to the bus, and the VBUS line is + // applied to the device (the host controller drives the VBUS at +5V, however this is only + // particularly important for hardware developers). In this state, the device must not respond + // to any bus transactions. The USB specification recognizes three potential scenarios with + // respect to how a device draws power: + // - Self-Powered Devices draw power from an external power source (e.g, a USB printer plugs + // into the wall as well as a USB port). Although the device may be considered + // technically "powered" even before attachment to the USB, it is still only considered + // powered after the VBUS line is applied to the device. + // - Bus-Powered Devices draw power solely from the USB up to 100mA. + // - Self- or Bus-Powered Devices may draw power from either the bus or an external power + // source, depending on the configuration. These devices may change power source at any + // time. If a device is currently self-powered and requires more than 100mA of power, but + // switches to being bus-powered, then the device must return to the Address state. + powered, + // A device in the powered state enters the default state after receiving a bus reset. In this + // state, the device is addressable at the default, reserved address of 0. At this point, the + // device is operating at the correct speed. The host is expected to allow 10 milliseconds + // before expecting the device to respond to data transfers after reset. + default, + // A device enters this state after the host assigns it an address via the default control pipe, + // which is always accessible whether the device's address has been set or not. + address, + // A device is in this state after the host examines its possible configurations and selects + // one. All endpoint's data toggle bits are initialized to zero when a device enters this state. + configured, + // When no traffic is observed on the bus for a period of 1 millisecond, a USB device enters + // this state, characterized by its low power consumption. The device's address and + // configuration settings are maintained while suspended. A device exits the suspended state as + // soon as it begins seeing bus activity again. The host is expected to allow 10 milliseconds + // before expecting the device to respond to data transfers after resume. + suspended, +}; + +const RequestCode = enum(u8) { + get_status = 0, + clear_feature = 1, + set_feature = 3, + set_address = 5, + get_descriptor = 6, + set_descriptor = 7, + get_configuration = 8, + set_configuration = 9, + get_interface = 10, + set_interface = 11, + sync_frame = 12, +}; + +// Direction of an endpoint, from the host's point of view +const EndpointDirection = enum(u1) { + out = 0, + in = 1, +}; + +// Identifier newtypes: distinct wire-sized types for values that identify something on the +// device rather than count something. Each is a non-exhaustive enum whose values originate +// in the descriptors below and flow, still typed, into the standard request constructors — +// so an interface number can never be passed where a configuration value is expected. + +// The bus address of a device, assigned by the host with SET_ADDRESS. Addresses are 7 bits +// wide. +const DeviceAddress = enum(u7) { + // The default address every device answers at after a reset, until SET_ADDRESS + // completes + default = 0, + _, +}; + +// Identifies a configuration; from ConfigurationDescriptor.configuration_value. +const ConfigurationValue = enum(u8) { + // Not configured: returned by GET_CONFIGURATION while the device is in the address + // state, and passed to SET_CONFIGURATION to return a configured device to the address + // state + none = 0, + _, +}; + +// Identifies an interface within a configuration; from +// InterfaceDescriptor.interface_number. +const InterfaceNumber = enum(u8) { _ }; + +// Selects between the alternate settings of one interface; from +// InterfaceDescriptor.alternate_setting. +const AlternateSetting = enum(u8) { + // The default setting of an interface + default = 0, + _, +}; + +// The number of an endpoint within a device, 4 bits wide. The direction bit carried +// alongside it tells the two endpoints sharing a number apart. +const EndpointNumber = enum(u4) { + // Endpoint zero: the default control pipe every device provides + default_control = 0, + _, +}; + +// Index of a STRING descriptor, stored in descriptors that reference a string and passed to +// GET_DESCRIPTOR to read it. +const StringIndex = enum(u8) { + // The device has no string descriptor for this field + none = 0, + _, +}; + +// Characteristics of a device request (the bmRequestType field of a set-up packet). Fields are +// declared least-significant first: recipient occupies bits 4...0, kind bits 6...5, and +// direction bit 7. +const RequestType = packed struct(u8) { + // The recipient of the request (values 4...31 are reserved) + recipient: Recipient, + // The type of the request + kind: Kind, + // Data transfer direction. The value of this bit is ignored when length is zero. + direction: Direction, + + const Recipient = enum(u5) { + device = 0, + interface = 1, + endpoint = 2, + other = 3, + }; + + const Kind = enum(u2) { + standard = 0, + class = 1, + vendor = 2, + reserved = 3, + }; + + const Direction = enum(u1) { + host_to_device = 0, + device_to_host = 1, + }; +}; + +const Request = extern struct { + // Characteristics of the request + request_type: RequestType, + // Specific request + request_code: RequestCode, + // Word-sized field that may (or may not) serve as a parameter to the request, depending + // on the specific request. For GET_DESCRIPTOR and SET_DESCRIPTOR, bit-cast a + // DescriptorValue into this field. + value: u16 align(1), + // Word-sized field that may (or may not) serve as a parameter to the request, depending + // on the specific request. Typically this field holds an index or an offset value. When + // request_type specifies an endpoint or an interface as the recipient, bit-cast an + // EndpointIndex or an InterfaceIndex into this field. + index: u16 align(1), + // Number of bytes to transfer if there is a DATA stage. + // - If this field is non-zero, and request_type indicates a transfer from + // device-to-host, then the device must never return more than length bytes of data. + // However, a device may return less. + // - If this field is non-zero, and request_type indicates a transfer from + // host-to-device, then the host must send exactly length bytes of data. If the host + // sends more than length bytes, the behavior of the device is undefined. + length: u16 align(1), + + // The format of the index field when request_type specifies an endpoint as the + // recipient. The host should always set the direction bit to zero (but the device + // should accept either value) when the endpoint is part of a control pipe. + const EndpointIndex = packed struct(u16) { + // Endpoint number + number: EndpointNumber, + // Reserved (reset to zero) + reserved: u3 = 0, + // Selects the OUT or the IN endpoint with the specified endpoint number + direction: EndpointDirection, + // Reserved (reset to zero) + reserved_high: u8 = 0, + }; + + // The format of the index field when request_type specifies an interface as the + // recipient. + const InterfaceIndex = packed struct(u16) { + // Interface number + number: u8, + // Reserved (reset to zero) + reserved: u8 = 0, + }; + + // The format of the value field of GET_DESCRIPTOR and SET_DESCRIPTOR requests: the + // descriptor type in the high byte, and the descriptor index in the low byte. The index + // is used to select a specific descriptor (only for CONFIGURATION and STRING + // descriptors) when several descriptors of that type are implemented by a device. + const DescriptorValue = packed struct(u16) { + // Descriptor index + index: u8 = 0, + // Descriptor type + kind: DescriptorType, + }; +}; + +// Feature selectors, used as the value field of CLEAR_FEATURE and SET_FEATURE requests. The +// comment on each value notes the recipient the selector applies to. +const FeatureSelector = enum(u16) { + // Halts an endpoint (recipient: endpoint) + endpoint_halt = 0, + // Enables or disables the device's remote wakeup capability (recipient: device) + device_remote_wakeup = 1, + // Puts a hi-speed device into a test mode, selected by a TestMode value in the high + // byte of the index field (recipient: device) + test_mode = 2, +}; + +// Test mode selectors, passed in the high byte of the index field of a SET_FEATURE request +// with the test_mode feature selector. Values 06h...3Fh are reserved for standard test +// selectors and C0h...FFh for vendor-specific test modes; all other unlisted values are +// reserved. +const TestMode = enum(u8) { + test_j = 0x01, + test_k = 0x02, + test_se0_nak = 0x03, + test_packet = 0x04, + test_force_enable = 0x05, + _, +}; + +// The two bytes returned by a GET_STATUS request directed at a device. Fields are declared +// least-significant first. +const DeviceStatus = packed struct(u16) { + // Whether the device is currently self-powered (as opposed to bus-powered). This bit + // cannot be changed with the SET_FEATURE or CLEAR_FEATURE requests. + self_powered: bool, + // Whether the device is currently enabled to request remote wakeup. Changed with the + // SET_FEATURE and CLEAR_FEATURE requests using the device_remote_wakeup feature + // selector. + remote_wakeup: bool, + // Reserved (reset to zero) + reserved: u14, +}; + +// The two bytes returned by a GET_STATUS request directed at an endpoint. (A GET_STATUS +// request directed at an interface returns two bytes that are entirely reserved.) +const EndpointStatus = packed struct(u16) { + // Whether the endpoint is currently halted. Set with the SET_FEATURE request using the + // endpoint_halt feature selector, and cleared with CLEAR_FEATURE. + halted: bool, + // Reserved (reset to zero) + reserved: u15, +}; + +// A target for the standard requests that may be directed at the device, an interface, or +// an endpoint. +const Target = union(enum) { + device, + interface: InterfaceNumber, + endpoint: Request.EndpointIndex, + + fn recipient(target: Target) RequestType.Recipient { + return switch (target) { + .device => .device, + .interface => .interface, + .endpoint => .endpoint, + }; + } + + fn index(target: Target) u16 { + return switch (target) { + .device => 0, + .interface => |number| @intFromEnum(number), + .endpoint => |endpoint| @bitCast(endpoint), + }; + } +}; + +// Constructors for the standard device requests, one per RequestCode. Each returns a +// ready-to-send set-up packet with the request_type, value, index, and length fields the +// specification prescribes for that request. + +// Reads the status of the given target: bit-cast the two bytes the device returns into a +// DeviceStatus or an EndpointStatus. (The two bytes returned for an interface are entirely +// reserved.) +fn getStatus(target: Target) Request { + return .{ + .request_type = .{ + .recipient = target.recipient(), + .kind = .standard, + .direction = .device_to_host, + }, + .request_code = .get_status, + .value = 0, + .index = target.index(), + .length = 2, + }; +} + +// Clears or disables the given feature. A device cannot be taken out of a test mode with +// this request; test_mode is only cleared by cycling power. +fn clearFeature(feature: FeatureSelector, target: Target) Request { + return .{ + .request_type = .{ + .recipient = target.recipient(), + .kind = .standard, + .direction = .host_to_device, + }, + .request_code = .clear_feature, + .value = @intFromEnum(feature), + .index = target.index(), + .length = 0, + }; +} + +// Sets or enables the given feature. For the test_mode feature selector, use setTestMode +// instead: the test selector rides in the high byte of the index field. +fn setFeature(feature: FeatureSelector, target: Target) Request { + return .{ + .request_type = .{ + .recipient = target.recipient(), + .kind = .standard, + .direction = .host_to_device, + }, + .request_code = .set_feature, + .value = @intFromEnum(feature), + .index = target.index(), + .length = 0, + }; +} + +// Puts a hi-speed device into the given test mode: a SET_FEATURE request with the test_mode +// feature selector and the test selector in the high byte of the index field. +fn setTestMode(mode: TestMode) Request { + return .{ + .request_type = .{ + .recipient = .device, + .kind = .standard, + .direction = .host_to_device, + }, + .request_code = .set_feature, + .value = @intFromEnum(FeatureSelector.test_mode), + .index = @as(u16, @intFromEnum(mode)) << 8, + .length = 0, + }; +} + +// Assigns the device its bus address, moving it from the default state to the address +// state. The device does not answer at the new address until the status stage of this +// request completes. +fn setAddress(address: DeviceAddress) Request { + return .{ + .request_type = .{ + .recipient = .device, + .kind = .standard, + .direction = .host_to_device, + }, + .request_code = .set_address, + .value = @intFromEnum(address), + .index = 0, + .length = 0, + }; +} + +// Reads a descriptor from the device. +// - descriptor_index selects among descriptors of the same type, and is only used for +// configuration and string descriptors. +// - language_id selects the language of a string descriptor, and is zero otherwise. +// - length is the number of bytes to read; a device never returns more than length bytes, +// but may return less if the descriptor is shorter. +fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request { + return .{ + .request_type = .{ + .recipient = .device, + .kind = .standard, + .direction = .device_to_host, + }, + .request_code = .get_descriptor, + .value = @bitCast(Request.DescriptorValue{ .index = descriptor_index, .kind = kind }), + .index = language_id, + .length = length, + }; +} + +// Updates an existing descriptor or adds a new one (optional; many devices do not support +// this request). The parameters mirror getDescriptor; the descriptor itself is sent in the +// DATA stage. +fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request { + return .{ + .request_type = .{ + .recipient = .device, + .kind = .standard, + .direction = .host_to_device, + }, + .request_code = .set_descriptor, + .value = @bitCast(Request.DescriptorValue{ .index = descriptor_index, .kind = kind }), + .index = language_id, + .length = length, + }; +} + +// Reads the currently active configuration: @enumFromInt the byte the device returns into a +// ConfigurationValue, which is none while the device is not configured. +fn getConfiguration() Request { + return .{ + .request_type = .{ + .recipient = .device, + .kind = .standard, + .direction = .device_to_host, + }, + .request_code = .get_configuration, + .value = 0, + .index = 0, + .length = 1, + }; +} + +// Selects the configuration with the given configuration_value (from +// ConfigurationDescriptor.configuration_value), moving the device from the address state to +// the configured state. Selecting none returns the device to the address state. +fn setConfiguration(configuration_value: ConfigurationValue) Request { + return .{ + .request_type = .{ + .recipient = .device, + .kind = .standard, + .direction = .host_to_device, + }, + .request_code = .set_configuration, + .value = @intFromEnum(configuration_value), + .index = 0, + .length = 0, + }; +} + +// Reads the alternate setting currently selected for the given interface: @enumFromInt the +// byte the device returns into an AlternateSetting. +fn getInterface(interface: InterfaceNumber) Request { + return .{ + .request_type = .{ + .recipient = .interface, + .kind = .standard, + .direction = .device_to_host, + }, + .request_code = .get_interface, + .value = 0, + .index = @intFromEnum(interface), + .length = 1, + }; +} + +// Selects an alternate setting (from InterfaceDescriptor.alternate_setting) for the given +// interface. +fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting) Request { + return .{ + .request_type = .{ + .recipient = .interface, + .kind = .standard, + .direction = .host_to_device, + }, + .request_code = .set_interface, + .value = @intFromEnum(alternate_setting), + .index = @intFromEnum(interface), + .length = 0, + }; +} + +// Reads the two-byte number of the frame in which the given isochronous endpoint's +// repeating pattern of transfers begins. +fn syncFrame(endpoint: Request.EndpointIndex) Request { + return .{ + .request_type = .{ + .recipient = .endpoint, + .kind = .standard, + .direction = .device_to_host, + }, + .request_code = .sync_frame, + .value = 0, + .index = @bitCast(endpoint), + .length = 2, + }; +} + +const DescriptorType = enum(u8) { + device = 1, + configuration = 2, + string = 3, + interface = 4, + endpoint = 5, + device_qualifier = 6, + other_speed_configuration = 7, + interface_power = 8, + _, +}; + +const DeviceDescriptor = extern struct { + // Size of this descriptor in bytes + length: u8, + // DEVICE Descriptor Type + descriptor_type: DescriptorType, + // USB Specification Release Number in Binary-Coded Decimal (i.e, 2.10 is expressed as 210h). + // Identifies the release of the USB Specification with with the device and its + // descriptors are compliant. + bcd_usb: u16 align(1), + // Class code (assigned by the USB-IF) + // - This field is reset to zero if each interface within a configuration specifies its own + // class information and the various interfaces operate independently. + // - A value of FFh in this field indicates the device class is vendor-specific. + device_class: u8, + // Subclass Code (assigned by the USB-IF) + // - The subclass code of a device is qualified by the class code of that device. + // - If device_class is reset to zero, then this field must also be reset to zero. + // - When device_class is not set to FFh, then all values for this field are reserved for + // assignment by the USB-IF. + device_subclass: u8, + // Protocol code (assigned by the USB-IF) + // - The protocol code of a device is qualified by both the class and subclass codes of + // that device. + // - A value of 00h in this field means that the device may specify class-specific + // protocols on an interface basis, though this is not a requirement. + // - If this field is set to FFh, then the device uses a vendor-specific protocol. + device_protocol: u8, + // Maximum packet size for endpoint zero (8, 16, 32, or 64 are the only valid options) + max_packet_size_0: u8, + // Vendor ID (assigned by the USB-IF) + vendor_id: u16 align(1), + // Product ID (assigned by the USB-IF) + product_id: u16 align(1), + // Device release number in binary-coded decimal + bcd_device: u16 align(1), + // Index of STRING descriptor describing manufacturer + manufacturer_index: StringIndex, + // Index of STRING descriptor describing product + product_index: StringIndex, + // Index of STRING descriptor describing the device's serial number + serial_number_index: StringIndex, + // Number of possible configurations + configuration_count: u8, +}; + +const DeviceQualifierDescriptor = extern struct { + // Size of this descriptor in bytes + length: u8, + // DEVICE_QUALIFIER Descriptor Type + descriptor_type: DescriptorType, + // USB Specification Release Number in Binary-Coded Decimal (i.e, 2.00 is expressed as 200h). + // Identifies the release of the USB Specification with with the device and its + // descriptors are compliant. This field must be at least 0200h. + bcd_usb: u16 align(1), + // Class code (assigned by the USB-IF) + device_class: u8, + // Subclass Code (assigned by the USB-IF) + device_subclass: u8, + // Protocol code (assigned by the USB-IF) + device_protocol: u8, + // Maximum packet size for endpoint zero (8, 16, 32, or 64 are the only valid options) + max_packet_size_0: u8, + // Number of possible configurations + configuration_count: u8, + // Reserved for future uses, must be zero. + reserved: u8, +}; + +const ConfigurationDescriptor = extern struct { + // Size of this descriptor in bytes + length: u8, + // CONFIGURATION Descriptor Type + descriptor_type: DescriptorType, + // The total combined length in bytes of all the descriptors returned with the request for + // this CONFIGURATION descriptor (including CONFIGURATION, INTERFACE, ENDPOINT, class- and + // vendor-specific descriptors). + total_length: u16 align(1), + // Number of interfaces supported by this configuration + interface_count: u8, + // Value which when used as an argument in the SET_CONFIGURATION request, causes the device + // to assume the configuration described by this descriptor. + configuration_value: ConfigurationValue, + // Index of STRING descriptor describing this configuration. + configuration_index: StringIndex, + // Configuration Characteristics + attributes: Attributes, + // Maximum power consumption of this device from the bus when fully operational and using + // this configuration. Expressed in units of 2mA (i.e., a value of 50 in this field + // indicates 100mA). + // - A device reports with the attributes field whether the configuration is bus- or + // self-powered, but the device status (retrieved with a GET_STATUS request) reports + // whether the device is currently self-powered. + // - If a device is disconnected from an external power source, it may not draw more + // power from the bus than specified in this field. + max_power: u8, + + // Configuration characteristics. Fields are declared least-significant first. + const Attributes = packed struct(u8) { + // Reserved, reset to zero (D4...0) + reserved: u5, + // Whether Remote Wakeup is supported by this configuration (D5) + remote_wakeup: bool, + // Self-Powered (D6) + // - false: Device runs on power supplied by the bus + // - true: Device provides a local power source; if max_power is non-zero, the + // device also may use bus power. + self_powered: bool, + // Reserved, must be set to one for historical reasons (D7) + reserved_one: u1, + }; +}; + +// This descriptor describes the configuration of a high-speed device if it were operating at +// its alternative speed. The structure of the OTHER_SPEED_CONFIGURATION is identical to that +// of the CONFIGURATION descriptor; the only difference is that the descriptor_type field +// reflects that the descriptor is an OTHER_SPEED_CONFIGURATION descriptor. +const OtherSpeedConfigurationDescriptor = ConfigurationDescriptor; + +const InterfaceDescriptor = extern struct { + // Size of this descriptor in bytes + length: u8, + // INTERFACE Descriptor Type + descriptor_type: DescriptorType, + // Number of this interface. Zero-based value which identifies the index of this interface + // in the array of interfaces supported within a configuration. + interface_number: InterfaceNumber, + // Value used to select the alternate settings described by this INTERFACE descriptor for + // the interface with the interface_number in the previous field. This value is zero if + // this descriptor describes the default settings for a particular interface. + alternate_setting: AlternateSetting, + // Number of endpoints used by this interface, not including endpoint zero. + endpoint_count: u8, + // Class code (assigned by the USB-IF) + // - A value of zero here is reserved for future standardization. + // - If this value is FFh, the interface class is vendor-specific. + // - All other values are reserved for assignment by the USB-IF. + interface_class: u8, + // Subclass code (assigned by the USB-IF) + // - The subclass code in this field is qualified by the value of the interface_class + // field. + // - If interface_class is reset to zero, then this field must also be reset to zero. + // - If interface_class is not set to the value of FFh, then all values of this field are + // reserved for assignment by the USB-IF. + interface_subclass: u8, + // Protocol code (assigned by the USB-IF) + // - The protocol code in this field is qualified by the values of the interface_class + // and interface_subclass fields. + // - If an interface supports class-specific requests, then this field identifies the + // protocols that the device uses as defined by the specifications of the device class. + // - If this field is reset to zero, then the device does not use a class-specific + // protocol on this interface. + // - If this field is set to FFh, then the device uses a vendor-specific protocol on + // this interface. + interface_protocol: u8, + // Index of STRING descriptor describing this interface + interface_index: StringIndex, +}; + +const EndpointDescriptor = extern struct { + // Size of this descriptor in bytes + length: u8, + // ENDPOINT Descriptor Type + descriptor_type: DescriptorType, + // The address of the endpoint on the USB device described by this descriptor + endpoint_address: Address, + // The endpoint's attributes + attributes: Attributes, + // Maximum packet size that this endpoint is capable of sending or receiving. For + // isochronous endpoints, this value is used to reserve bus time; the pipe, however, may + // not always use all of the reserved bus time. + max_packet_size: MaxPacketSize align(1), + // Interval for polling a device during a data transfer, expressed in units of microframes + // for high-speed devices, and frames for low- and full-speed devices. The exact meaning of + // the value in this field depends on the endpoint type and the operating speed of the + // device: + // - Full- and High-speed isochronous endpoints, and high-speed interrupt endpoints: + // This field must be in the range from 1 to 16, and is used to calculate the period + // as 2^(interval - 1). That is, a value of 4 calculates to 2^(4 - 1) = 2^3 = 8. + // - Full- and Low-speed interrupt endpoints: This field must be in the range from + // 1 to 255. + // - High-speed bulk and control OUT endpoints: This field must be in the range from + // 0 to 255, and specifies the maximum NAK rate of the endpoint. A value of zero + // indicates that the endpoint never NAKs; other values indicate at most 1 NAK each + // interval number of microframes. + interval: u8, + + // The address of an endpoint. Fields are declared least-significant first. + const Address = packed struct(u8) { + // Endpoint Number (D3...0) + number: EndpointNumber, + // Reserved, reset to zero (D6...4) + reserved: u3, + // Direction, ignored for control endpoints (D7) + direction: EndpointDirection, + }; + + // An endpoint's attributes. Fields are declared least-significant first. + const Attributes = packed struct(u8) { + // Transfer Type (D1...0) + transfer_type: TransferType, + // Synchronization Type; isochronous endpoints only, reserved and reset to zero for + // other endpoint types (D3...2) + synchronization: Synchronization, + // Usage Type; isochronous endpoints only, reserved and reset to zero for other + // endpoints (D5...4) + usage: Usage, + // Reserved, reset to zero (D7...6) + reserved: u2, + }; + + const TransferType = enum(u2) { + control = 0, + isochronous = 1, + bulk = 2, + interrupt = 3, + }; + + const Synchronization = enum(u2) { + none = 0, + asynchronous = 1, + adaptive = 2, + synchronous = 3, + }; + + const Usage = enum(u2) { + data = 0, + feedback = 1, + implicit_feedback_data = 2, + _, + }; + + // The maximum packet size of an endpoint. Fields are declared least-significant first. + const MaxPacketSize = packed struct(u16) { + // Maximum packet size in bytes (bits 10...0) + size: u11, + // Number of additional transaction opportunities per microframe, for high-speed + // isochronous and interrupt endpoints; reserved and reset to zero for other + // endpoints (bits 12...11) + additional_transactions: AdditionalTransactions, + // Reserved, must be reset to zero (bits 15...13) + reserved: u3, + }; + + const AdditionalTransactions = enum(u2) { + // None (1 transaction per microframe) + none = 0, + // 1 additional (2 transactions per microframe) + one = 1, + // 2 additional (3 transactions per microframe) + two = 2, + _, + }; +}; + +// A STRING descriptor at index zero returns the list of LANGID codes supported by the +// device; all other indices return a Unicode string. Both forms start with this two-byte +// header, followed by the variable-length payload: +// - index 0: an array of two-byte LANGID codes (wLangID[0] through wLangID[x]) +// - other indices: a Unicode string of N bytes +const StringDescriptor = extern struct { + // Size of this descriptor in bytes + length: u8, + // STRING Descriptor Type + descriptor_type: DescriptorType, +}; + +const std = @import("std"); + +test "wire sizes and offsets match the specification" { + const expectEqual = std.testing.expectEqual; + + try expectEqual(8, @sizeOf(Request)); + try expectEqual(18, @sizeOf(DeviceDescriptor)); + try expectEqual(10, @sizeOf(DeviceQualifierDescriptor)); + try expectEqual(9, @sizeOf(ConfigurationDescriptor)); + try expectEqual(9, @sizeOf(InterfaceDescriptor)); + try expectEqual(7, @sizeOf(EndpointDescriptor)); + try expectEqual(2, @sizeOf(StringDescriptor)); + + try expectEqual(2, @offsetOf(DeviceDescriptor, "bcd_usb")); + try expectEqual(8, @offsetOf(DeviceDescriptor, "vendor_id")); + try expectEqual(17, @offsetOf(DeviceDescriptor, "configuration_count")); + try expectEqual(2, @offsetOf(ConfigurationDescriptor, "total_length")); + try expectEqual(4, @offsetOf(EndpointDescriptor, "max_packet_size")); +} + +test "bitmap packings match the specification" { + const expectEqual = std.testing.expectEqual; + const expect = std.testing.expect; + + // bmRequestType for GET_DESCRIPTOR: device-to-host | standard | device = 80h + const request_type = RequestType{ + .recipient = .device, + .kind = .standard, + .direction = .device_to_host, + }; + try expectEqual(0x80, @as(u8, @bitCast(request_type))); + + // wValue for GET_DESCRIPTOR(CONFIGURATION, index 0) = 0200h + const descriptor_value = Request.DescriptorValue{ .kind = .configuration }; + try expectEqual(0x0200, @as(u16, @bitCast(descriptor_value))); + + // wIndex for the IN endpoint 1 = 0081h + const endpoint_index = Request.EndpointIndex{ .number = @enumFromInt(1), .direction = .in }; + try expectEqual(0x0081, @as(u16, @bitCast(endpoint_index))); + + // Endpoint address 81h = IN endpoint 1 + const address: EndpointDescriptor.Address = @bitCast(@as(u8, 0x81)); + try expectEqual(1, @intFromEnum(address.number)); + try expectEqual(.in, address.direction); + + // Endpoint attributes 03h = interrupt transfer + const attributes: EndpointDescriptor.Attributes = @bitCast(@as(u8, 0x03)); + try expectEqual(.interrupt, attributes.transfer_type); + + // wMaxPacketSize 0008h = 8 bytes, no additional transactions + const max_packet_size: EndpointDescriptor.MaxPacketSize = @bitCast(@as(u16, 0x0008)); + try expectEqual(8, max_packet_size.size); + try expectEqual(.none, max_packet_size.additional_transactions); + + // Configuration attributes C0h = self-powered, with the historical D7 bit set + const configuration_attributes: ConfigurationDescriptor.Attributes = @bitCast(@as(u8, 0xC0)); + try expect(configuration_attributes.self_powered); + try expect(!configuration_attributes.remote_wakeup); + try expectEqual(1, configuration_attributes.reserved_one); + + // GET_STATUS words: device 0001h = self-powered; endpoint 0001h = halted + const device_status: DeviceStatus = @bitCast(@as(u16, 0x0001)); + try expect(device_status.self_powered and !device_status.remote_wakeup); + const endpoint_status: EndpointStatus = @bitCast(@as(u16, 0x0001)); + try expect(endpoint_status.halted); + + // DescriptorType is non-exhaustive: class-specific values (HID = 21h) pass through + const hid_type: DescriptorType = @enumFromInt(0x21); + try expectEqual(0x21, @intFromEnum(hid_type)); + try expect(hid_type != .device); +} + +fn expectRequestBytes(request: Request, expected: [8]u8) !void { + try std.testing.expectEqualSlices(u8, &expected, std.mem.asBytes(&request)); +} + +test "standard request constructors encode the specification's set-up packets" { + try expectRequestBytes(getStatus(.device), .{ 0x80, 0, 0, 0, 0, 0, 2, 0 }); + try expectRequestBytes(getStatus(.{ .interface = @enumFromInt(3) }), .{ 0x81, 0, 0, 0, 3, 0, 2, 0 }); + try expectRequestBytes(getStatus(.{ .endpoint = .{ .number = @enumFromInt(2), .direction = .in } }), .{ 0x82, 0, 0, 0, 0x82, 0, 2, 0 }); + try expectRequestBytes(clearFeature(.endpoint_halt, .{ .endpoint = .{ .number = @enumFromInt(1), .direction = .out } }), .{ 0x02, 1, 0, 0, 0x01, 0, 0, 0 }); + try expectRequestBytes(setFeature(.device_remote_wakeup, .device), .{ 0x00, 3, 1, 0, 0, 0, 0, 0 }); + try expectRequestBytes(setTestMode(.test_packet), .{ 0x00, 3, 2, 0, 0, 0x04, 0, 0 }); + try expectRequestBytes(setAddress(@enumFromInt(5)), .{ 0x00, 5, 5, 0, 0, 0, 0, 0 }); + try expectRequestBytes(getDescriptor(.device, 0, 0, 18), .{ 0x80, 6, 0, 1, 0, 0, 18, 0 }); + try expectRequestBytes(getDescriptor(.string, 2, 0x0409, 255), .{ 0x80, 6, 2, 3, 0x09, 0x04, 255, 0 }); + try expectRequestBytes(setDescriptor(.string, 2, 0x0409, 16), .{ 0x00, 7, 2, 3, 0x09, 0x04, 16, 0 }); + try expectRequestBytes(getConfiguration(), .{ 0x80, 8, 0, 0, 0, 0, 1, 0 }); + try expectRequestBytes(setConfiguration(@enumFromInt(1)), .{ 0x00, 9, 1, 0, 0, 0, 0, 0 }); + try expectRequestBytes(getInterface(@enumFromInt(2)), .{ 0x81, 10, 0, 0, 2, 0, 1, 0 }); + try expectRequestBytes(setInterface(@enumFromInt(2), @enumFromInt(1)), .{ 0x01, 11, 1, 0, 2, 0, 0, 0 }); + try expectRequestBytes(syncFrame(.{ .number = @enumFromInt(3), .direction = .in }), .{ 0x82, 12, 0, 0, 0x83, 0, 2, 0 }); +} diff --git a/system/devices/usb-ids.zig b/system/devices/usb-ids.zig new file mode 100644 index 0000000..4c7278e --- /dev/null +++ b/system/devices/usb-ids.zig @@ -0,0 +1,264 @@ +//! USB class-code decoding: turn the (class, subclass, protocol) triple a USB device or +//! interface reports in its descriptors into typed values. The device descriptor carries one +//! triple for the whole device, and each interface descriptor carries its own; a class code +//! of zero at the device level defers entirely to the interfaces. Subclass and protocol +//! codes are qualified by the class code — the same value means different things under +//! different classes — so there is no single SubClass or Protocol enum: each class with +//! spec-defined codes gets its own namespace below. Pure reference data (from the USB-IF +//! defined class codes; see https://www.usb.org/defined-class-codes) — no hardware access — +//! so it is shared by kernel discovery and any user-space tool (device naming, driver +//! matching). + +// Base class codes (assigned by the USB-IF). The comment on each value notes where the code +// may legally appear: in the device descriptor, in interface descriptors, or both. +const Class = enum(u8) { + // Use class information in the interface descriptors (device descriptor only). Each + // interface within a configuration specifies its own class information and the various + // interfaces operate independently. + per_interface = 0x00, + // Audio: speakers, microphones, sound cards (interface) + audio = 0x01, + // Communications and CDC control: modems, network adapters (both) + communications = 0x02, + // Human Interface Device: keyboards, mice, game controllers (interface) + hid = 0x03, + // Physical: force-feedback devices (interface) + physical = 0x05, + // Image: still-imaging cameras, scanners (interface) + image = 0x06, + // Printer (interface) + printer = 0x07, + // Mass storage: flash drives, external disks, card readers (interface) + mass_storage = 0x08, + // Hub (device descriptor only) + hub = 0x09, + // CDC-Data: the data interfaces paired with a communications control interface + // (interface) + cdc_data = 0x0A, + // Smart card readers (interface) + smart_card = 0x0B, + // Content security (interface) + content_security = 0x0D, + // Video: webcams (interface) + video = 0x0E, + // Personal healthcare devices (interface) + personal_healthcare = 0x0F, + // Audio/Video devices (interface) + audio_video = 0x10, + // Billboard: describes alternate modes a USB Type-C device supports (device descriptor + // only) + billboard = 0x11, + // USB Type-C bridge (interface) + type_c_bridge = 0x12, + // USB Bulk Display Protocol devices (interface) + bulk_display = 0x13, + // MCTP over USB protocol endpoint devices (interface) + mctp = 0x14, + // I3C devices (interface) + i3c = 0x3C, + // Diagnostic devices (both) + diagnostic = 0xDC, + // Wireless controllers: Bluetooth adapters (interface) + wireless_controller = 0xE0, + // Miscellaneous (both) + miscellaneous = 0xEF, + // Application-specific: firmware upgrade, IrDA bridges, test and measurement + // (interface) + application_specific = 0xFE, + // Vendor-specific (both) + vendor_specific = 0xFF, + _, +}; + +// Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the +// protocol distinguishes the hub's transaction-translator arrangement. +const hub = struct { + const Protocol = enum(u8) { + // Full-speed hub + full_speed = 0x00, + // Hi-speed hub with a single transaction translator + hi_speed_single_tt = 0x01, + // Hi-speed hub with multiple transaction translators + hi_speed_multi_tt = 0x02, + // SuperSpeed hub (USB 3) + super_speed = 0x03, + _, + }; +}; + +// Subclass and protocol codes qualified by Class.hid. +const hid = struct { + const SubClass = enum(u8) { + // No subclass + none = 0x00, + // Boot interface: the device also supports the simplified boot protocol, usable by + // firmware before a full HID report-descriptor parser is available + boot = 0x01, + _, + }; + + // Only meaningful when the subclass is boot + const Protocol = enum(u8) { + none = 0x00, + keyboard = 0x01, + mouse = 0x02, + _, + }; +}; + +// Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the +// command set the device understands; the protocol identifies the transport used to carry +// commands, data, and status over the bus. +const mass_storage = struct { + const SubClass = enum(u8) { + // SCSI command set not reported; de facto, treat as scsi + not_reported = 0x00, + // Reduced Block Commands: typically flash devices + rbc = 0x01, + // MMC-5 (ATAPI): CD and DVD drives + atapi = 0x02, + // QIC-157 tape drives (obsolete) + qic_157 = 0x03, + // UFI: floppy disk drives + ufi = 0x04, + // SFF-8070i (obsolete) + sff_8070i = 0x05, + // Transparent SCSI command set: the common case for flash drives and disks + scsi = 0x06, + // LSD FS: negotiated access to large storage devices + lsd_fs = 0x07, + // IEEE 1667 + ieee_1667 = 0x08, + // Vendor-specific + vendor_specific = 0xFF, + _, + }; + + const Protocol = enum(u8) { + // Control/Bulk/Interrupt with command completion interrupt + cbi_completion_interrupt = 0x00, + // Control/Bulk/Interrupt without command completion interrupt + cbi = 0x01, + // Bulk-only transport: the common case for flash drives and disks + bulk_only = 0x50, + // USB attached SCSI + uas = 0x62, + // Vendor-specific + vendor_specific = 0xFF, + _, + }; +}; + +// Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes +// are model-specific; the useful invariant is the subclass, which selects the control model +// the interface implements. +const communications = struct { + const SubClass = enum(u8) { + // Direct line control model + direct_line = 0x01, + // Abstract control model: USB modems and serial adapters + abstract_control = 0x02, + // Telephone control model + telephone = 0x03, + // Multi-channel control model + multi_channel = 0x04, + // CAPI control model + capi = 0x05, + // Ethernet networking control model + ethernet = 0x06, + // ATM networking control model + atm = 0x07, + // Wireless handset control model + wireless_handset = 0x08, + // Device management + device_management = 0x09, + // Mobile direct line model + mobile_direct_line = 0x0A, + // OBEX + obex = 0x0B, + // Ethernet emulation model + ethernet_emulation = 0x0C, + // Network control model + network_control = 0x0D, + _, + }; +}; + +// Subclass and protocol codes qualified by Class.wireless_controller. +const wireless_controller = struct { + const SubClass = enum(u8) { + // Radio frequency controllers + radio_frequency = 0x01, + _, + }; + + // Only meaningful when the subclass is radio_frequency + const Protocol = enum(u8) { + // Bluetooth programming interface + bluetooth = 0x01, + // Ultra-wideband radio control + ultra_wideband = 0x02, + // Remote NDIS + remote_ndis = 0x03, + // Bluetooth AMP controller + bluetooth_amp = 0x04, + _, + }; +}; + +// Subclass and protocol codes qualified by Class.miscellaneous. +const miscellaneous = struct { + const SubClass = enum(u8) { + // Common class + common = 0x02, + _, + }; + + // Only meaningful when the subclass is common + const Protocol = enum(u8) { + // Interface association descriptor: at the device level, announces that the + // configuration groups interfaces into functions with IADs + interface_association = 0x01, + _, + }; +}; + +// Subclass and protocol codes qualified by Class.application_specific. +const application_specific = struct { + const SubClass = enum(u8) { + // Device firmware upgrade + firmware_upgrade = 0x01, + // IrDA bridge + irda_bridge = 0x02, + // Test and measurement + test_and_measurement = 0x03, + _, + }; +}; + +test "class codes match the USB-IF assignments" { + const std = @import("std"); + const expectEqual = std.testing.expectEqual; + + try expectEqual(0x03, @intFromEnum(Class.hid)); + try expectEqual(0x09, @intFromEnum(Class.hub)); + try expectEqual(0xFF, @intFromEnum(Class.vendor_specific)); + + // A typical flash drive: mass storage, transparent SCSI, bulk-only transport. + try expectEqual(0x06, @intFromEnum(mass_storage.SubClass.scsi)); + try expectEqual(0x50, @intFromEnum(mass_storage.Protocol.bulk_only)); + + // A boot keyboard: HID, boot subclass, keyboard protocol. + try expectEqual(0x01, @intFromEnum(hid.SubClass.boot)); + try expectEqual(0x01, @intFromEnum(hid.Protocol.keyboard)); + + // Class codes are non-exhaustive: unlisted values pass through undamaged. + const unknown: Class = @enumFromInt(0x42); + try expectEqual(0x42, @intFromEnum(unknown)); + + _ = hub.Protocol.hi_speed_multi_tt; + _ = communications.SubClass.abstract_control; + _ = wireless_controller.Protocol.bluetooth; + _ = miscellaneous.Protocol.interface_association; + _ = application_specific.SubClass.firmware_upgrade; +} diff --git a/system/drivers/bus/bus.zig b/system/drivers/bus/bus.zig index 07e9d33..9203290 100644 --- a/system/drivers/bus/bus.zig +++ b/system/drivers/bus/bus.zig @@ -100,6 +100,7 @@ pub fn main() void { while (n < n_children) : (n += 1) { var child = std.mem.zeroes(device.DeviceDescriptor); child.class = @intFromEnum(device.DeviceClass.timer); + child.pci_class = device.no_pci_class; child.hid_len = 6; child.hid[0..6].* = "hpet-t".*; child.resource_count = 1; diff --git a/system/drivers/ps2-bus/ps2-bus.zig b/system/drivers/ps2-bus/ps2-bus.zig index dbe1c98..2438a92 100644 --- a/system/drivers/ps2-bus/ps2-bus.zig +++ b/system/drivers/ps2-bus/ps2-bus.zig @@ -118,8 +118,8 @@ pub fn main() void { maybe_controller = controller; maybe_interrupt_index = findInterruptResourceIndex(controller_device_descriptor); - controller.disablePort(.One); - controller.disablePort(.Two); + controller.disablePort(.one); + controller.disablePort(.two); controller.flushOutputBuffer(); const current = controller.readConfigurationByte() orelse { @@ -136,7 +136,7 @@ pub fn main() void { return; } - if (controller.performSelfTest()) | reply | { + if (controller.performSelfTest()) |reply| { if (reply != ps2.response_controller_test_passed) { _ = runtime.system.write("system/drivers/ps2-bus: perform controller self test failed\n"); return; @@ -154,20 +154,20 @@ pub fn main() void { if (has_two_channels) { _ = runtime.system.write("system/drivers/ps2-bus: has two channels\n"); // keep the bus quiet until we have tested the ports and are ready to use them - controller.disablePort(.Two); + controller.disablePort(.two); } else { _ = runtime.system.write("system/drivers/ps2-bus: has one channel\n"); } // interface tests: always test port 1, test port 2 only if it exists - const port_one_works = (controller.testPort(.One) orelse { + const port_one_works = (controller.testPort(.one) orelse { _ = runtime.system.write("system/drivers/ps2-bus: port 1 test timed out\n"); return; }) == ps2.response_port_test_passed; var port_two_works = false; if (has_two_channels) { - port_two_works = (controller.testPort(.Two) orelse { + port_two_works = (controller.testPort(.two) orelse { _ = runtime.system.write("system/drivers/ps2-bus: port 2 test timed out\n"); return; }) == ps2.response_port_test_passed; @@ -181,20 +181,20 @@ pub fn main() void { // Enable the working ports. Their interrupts stay off until IRQ1 is bound // below — reset and identify use polled reads, which must never race the // interrupt-driven drain loop for bytes. - controller.enablePort(.One); - if (port_two_works) controller.enablePort(.Two); + controller.enablePort(.one); + if (port_two_works) controller.enablePort(.two); // reset each working device; a failing device is logged but does not // abort bring-up of the other one if (port_one_works) { - if (controller.resetDevice(.One)) |passed| { + if (controller.resetDevice(.one)) |passed| { if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset failed\n"); } else { _ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset timed out\n"); } } if (port_two_works) { - if (controller.resetDevice(.Two)) |passed| { + if (controller.resetDevice(.two)) |passed| { if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset failed\n"); } else { _ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset timed out\n"); @@ -204,8 +204,8 @@ pub fn main() void { // Identify the device on each working port and hand it off to the driver // that matches what it reported — a port is not assumed to be a keyboard // or a mouse by its number. - if (port_one_works) port_device_types[@intFromEnum(ps2.Port.One)] = spawnIdentifiedDriver(controller, .One); - if (port_two_works) port_device_types[@intFromEnum(ps2.Port.Two)] = spawnIdentifiedDriver(controller, .Two); + if (port_one_works) port_device_types[@intFromEnum(ps2.Port.one)] = spawnIdentifiedDriver(controller, .one); + if (port_two_works) port_device_types[@intFromEnum(ps2.Port.two)] = spawnIdentifiedDriver(controller, .two); } else { _ = runtime.system.write("system/drivers/ps2-bus: no PS/2 controller found\n"); return; @@ -243,7 +243,7 @@ pub fn main() void { // claim that node too and route its IRQ to the same endpoint. The IRQ belongs // to the *port*, whatever device identify found on it. var maybe_auxiliary_interrupt: ?struct { device_id: u64, interrupt_index: u64, gsi: u64 } = null; - if (port_device_types[@intFromEnum(ps2.Port.Two)] != null) { + if (port_device_types[@intFromEnum(ps2.Port.two)] != null) { if (device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_mouse.hid())) |descriptor| { if (findInterruptResourceIndex(descriptor)) |auxiliary_index| { if (device.claim(descriptor.id) and device.irqBind(descriptor.id, auxiliary_index, endpoint)) { @@ -263,8 +263,8 @@ pub fn main() void { _ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n"); return; }; - if (port_device_types[@intFromEnum(ps2.Port.One)] != null) configuration |= ps2.Port.One.interruptBit(); - if (maybe_auxiliary_interrupt != null) configuration |= ps2.Port.Two.interruptBit(); + if (port_device_types[@intFromEnum(ps2.Port.one)] != null) configuration |= ps2.Port.one.interruptBit(); + if (maybe_auxiliary_interrupt != null) configuration |= ps2.Port.two.interruptBit(); _ = controller.writeConfigurationByte(configuration); _ = runtime.system.write("system/drivers/ps2-bus: ok\n"); @@ -284,7 +284,7 @@ pub fn main() void { const current_status = ps2.status(controller.device_id, controller.status_index); if (current_status & ps2.status_output_buffer_full == 0) break; const byte = device.ioRead(controller.device_id, controller.data_index, 0, 1) orelse break; - const port: ps2.Port = if (current_status & ps2.status_auxiliary_output != 0) .Two else .One; + const port: ps2.Port = if (current_status & ps2.status_auxiliary_output != 0) .two else .one; if (port_endpoints[@intFromEnum(port)]) |child| { const forwarded = ps2.ForwardedByte{ .port = @intFromEnum(port), .byte = byte }; _ = ipc.send(child, std.mem.asBytes(&forwarded)); diff --git a/system/drivers/ps2-bus/ps2-library.zig b/system/drivers/ps2-bus/ps2-library.zig index d5cd630..0eeac4d 100644 --- a/system/drivers/ps2-bus/ps2-library.zig +++ b/system/drivers/ps2-bus/ps2-library.zig @@ -146,52 +146,53 @@ pub fn readData(id: u64, status_index: u64, data_index: u64, timeout_nanoseconds } pub fn writeData(id: u64, status_index: u64, data_index: u64, byte: u8, timeout_nanoseconds: u64) bool { - // IBF lives in the status register (0x64); wait for it to clear there, then write the data port (0x60) + // IBF lives in the status register (0x64); wait for it to clear there, then write the data port + // (0x60) if (!waitWritable(id, status_index, timeout_nanoseconds)) return false; return device.ioWrite(id, data_index, 0, 1, byte); } pub const Port = enum(u2) { - One, - Two, + one, + two, /// Command register byte that disables this port. fn disableCommand(self: Port) u8 { return switch (self) { - .One => cmd_disable_first_port, - .Two => cmd_disable_second_port, + .one => cmd_disable_first_port, + .two => cmd_disable_second_port, }; } /// Command register byte that enables this port (and its clock). fn enableCommand(self: Port) u8 { return switch (self) { - .One => cmd_enable_first_port, - .Two => cmd_enable_second_port, + .one => cmd_enable_first_port, + .two => cmd_enable_second_port, }; } /// Command register byte that runs this port's interface test. fn testCommand(self: Port) u8 { return switch (self) { - .One => cmd_test_first_port, - .Two => cmd_test_second_port, + .one => cmd_test_first_port, + .two => cmd_test_second_port, }; } /// Configuration-byte bit that, when set, disables this port's clock. pub fn clockDisabledBit(self: Port) u8 { return switch (self) { - .One => configuration_first_port_clock_disabled, - .Two => configuration_second_port_clock_disabled, + .one => configuration_first_port_clock_disabled, + .two => configuration_second_port_clock_disabled, }; } /// Configuration-byte bit that, when set, enables this port's interrupt. pub fn interruptBit(self: Port) u8 { return switch (self) { - .One => configuration_first_port_interrupt, - .Two => configuration_second_port_interrupt, + .one => configuration_first_port_interrupt, + .two => configuration_second_port_interrupt, }; } @@ -199,8 +200,8 @@ pub const Port = enum(u2) { /// output buffer (makes a byte appear as if it came from the device). pub fn writeOutputBufferCommand(self: Port) u8 { return switch (self) { - .One => cmd_write_first_port_output, - .Two => cmd_write_second_port_output, + .one => cmd_write_first_port_output, + .two => cmd_write_second_port_output, }; } @@ -209,24 +210,24 @@ pub const Port = enum(u2) { /// prefix (null); port 2 requires the "write second port input" command. pub fn deviceInputCommand(self: Port) ?u8 { return switch (self) { - .One => null, - .Two => cmd_write_second_port_input, + .one => null, + .two => cmd_write_second_port_input, }; } /// Controller output-port bit driving this port's clock line. pub fn outputPortClockBit(self: Port) u8 { return switch (self) { - .One => output_port_first_port_clock, - .Two => output_port_second_port_clock, + .one => output_port_first_port_clock, + .two => output_port_second_port_clock, }; } /// Controller output-port bit driving this port's data line. pub fn outputPortDataBit(self: Port) u8 { return switch (self) { - .One => output_port_first_port_data, - .Two => output_port_second_port_data, + .one => output_port_first_port_data, + .two => output_port_second_port_data, }; } @@ -234,8 +235,8 @@ pub const Port = enum(u2) { /// (wired to the port's IRQ line). pub fn outputPortBufferFullBit(self: Port) u8 { return switch (self) { - .One => output_port_first_port_output_full, - .Two => output_port_second_port_output_full, + .one => output_port_first_port_output_full, + .two => output_port_second_port_output_full, }; } }; @@ -392,9 +393,9 @@ pub const Controller = struct { /// enabled; the caller should disable it again to keep the bus quiet until /// device bring-up. pub fn hasTwoChannels(self: Controller) ?bool { - self.enablePort(.Two); + self.enablePort(.two); const configuration = self.readConfigurationByte() orelse return null; - return (configuration & Port.Two.clockDisabledBit()) == 0; + return (configuration & Port.two.clockDisabledBit()) == 0; } /// Reset the device attached to `port` (device command 0xFF) and wait for @@ -481,4 +482,4 @@ pub const Controller = struct { _ = self.sendToDevice(port, device_cmd_enable_scanning); return device_type; } -}; \ No newline at end of file +}; diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig new file mode 100644 index 0000000..f13694a --- /dev/null +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -0,0 +1,71 @@ +//! /system/drivers/usb-xhci-bus — the xHCI (USB 3) host-controller bus driver. +//! The device manager spawns **one instance per controller** it discovers (a machine +//! can carry several), passing the controller's device-tree id as argv[1]; this +//! instance claims that device and no other, so multiple instances never fight over +//! hardware. This increment proves the plumbing: parse the id, claim the controller, +//! and report its MMIO window. The next increments map the registers and bring the +//! controller up (reset, rings, port scan), then enumerate the USB devices on the +//! bus with the usb-abi request builders and publish each with `device_register`. + +const std = @import("std"); +const runtime = @import("runtime"); +const device = runtime.device; + +/// Format one whole log line and emit it in a single `debug_write`, so concurrent +/// instances (one per controller) can never interleave mid-line. +fn writeLine(comptime fmt: []const u8, arguments: anytype) void { + var line: [128]u8 = undefined; + _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); +} + +pub fn main(init: runtime.process.Init) void { + const argument = init.arguments.get(1) orelse { + _ = runtime.system.write("usb-xhci-bus: missing controller device id (argv[1])\n"); + return; + }; + const controller_id = std.fmt.parseInt(u64, argument, 10) catch { + writeLine("usb-xhci-bus: malformed controller device id '{s}'\n", .{argument}); + return; + }; + + if (!device.claim(controller_id)) { + writeLine("usb-xhci-bus: unable to claim controller device {d}\n", .{controller_id}); + return; + } + + // Fetch our own descriptor back for the controller's resources. + const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { + _ = runtime.system.write("usb-xhci-bus: out of memory\n"); + return; + }; + const total = device.enumerate(buffer); + const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| { + if (d.id == controller_id) break d; + } else { + writeLine("usb-xhci-bus: device {d} not in the device tree\n", .{controller_id}); + return; + }; + + // The controller's operational registers live behind BAR0, enumerated as the + // device's first memory resource. + const register_window = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| { + if (resource.kind == @intFromEnum(device.ResourceKind.memory)) break resource; + } else { + writeLine("usb-xhci-bus: controller device {d} has no MMIO window\n", .{controller_id}); + return; + }; + writeLine("usb-xhci-bus: claimed controller device {d} (registers at 0x{x}, {d} bytes)\n", .{ + controller_id, + register_window.start, + register_window.len, + }); + + // Controller bring-up (map the window, reset, rings, port scan) is the next + // increment; stay resident as the bus's supervisor in the meantime. + while (true) runtime.system.sleep(1000); +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; // pull the runtime entry shim into the image +} diff --git a/system/drivers/usb-xhci-bus/usb-xhci-libary.zig b/system/drivers/usb-xhci-bus/usb-xhci-libary.zig new file mode 100644 index 0000000..e69de29 diff --git a/system/kernel/architecture/x86_64/cpu.zig b/system/kernel/architecture/x86_64/cpu.zig index 9fb7b8a..f2eac82 100644 --- a/system/kernel/architecture/x86_64/cpu.zig +++ b/system/kernel/architecture/x86_64/cpu.zig @@ -490,8 +490,7 @@ pub fn saveInterrupts() u64 { \\cli : [f] "=r" (flags), : - : .{ .memory = true } - ); + : .{ .memory = true }); return flags; } diff --git a/system/kernel/architecture/x86_64/idt.zig b/system/kernel/architecture/x86_64/idt.zig index fe6d819..fd7ec2b 100644 --- a/system/kernel/architecture/x86_64/idt.zig +++ b/system/kernel/architecture/x86_64/idt.zig @@ -78,22 +78,22 @@ fn defaultFault(_: *const CpuState) noreturn { /// Names for the 32 defined exception vectors, for readable output. const names = [_][]const u8{ - "divide error", "debug", - "NMI", "breakpoint", - "overflow", "bound range exceeded", - "invalid opcode", "device not available", - "double fault", "coprocessor segment overrun", - "invalid TSS", "segment not present", - "stack-segment fault", "general protection fault", - "page fault", "reserved (15)", - "x87 floating-point", "alignment check", - "machine check", "SIMD floating-point", - "virtualization", "control protection", - "reserved (22)", "reserved (23)", - "reserved (24)", "reserved (25)", - "reserved (26)", "reserved (27)", - "hypervisor injection", "VMM communication", - "security exception", "reserved (31)", + "divide error", "debug", + "NMI", "breakpoint", + "overflow", "bound range exceeded", + "invalid opcode", "device not available", + "double fault", "coprocessor segment overrun", + "invalid TSS", "segment not present", + "stack-segment fault", "general protection fault", + "page fault", "reserved (15)", + "x87 floating-point", "alignment check", + "machine check", "SIMD floating-point", + "virtualization", "control protection", + "reserved (22)", "reserved (23)", + "reserved (24)", "reserved (25)", + "reserved (26)", "reserved (27)", + "hypervisor injection", "VMM communication", + "security exception", "reserved (31)", }; pub fn vectorName(vector: u64) []const u8 { diff --git a/system/kernel/architecture/x86_64/paging.zig b/system/kernel/architecture/x86_64/paging.zig index 857d31a..2d2293c 100644 --- a/system/kernel/architecture/x86_64/paging.zig +++ b/system/kernel/architecture/x86_64/paging.zig @@ -166,8 +166,7 @@ pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot asm volatile ("mov %[pml4], %%cr3" : : [pml4] "r" (pml4), - : .{ .memory = true } - ); + : .{ .memory = true }); on_own_tables = true; // now on the kernel's physmap (covers all RAM) init_done = true; // the kernel half is fixed from here } @@ -409,6 +408,5 @@ fn invalidate(virtual: u64) void { \\invlpg (%%rax) : : [v] "r" (virtual), - : .{ .rax = true, .memory = true } - ); + : .{ .rax = true, .memory = true }); } diff --git a/system/kernel/console.zig b/system/kernel/console.zig index c876a57..3b42145 100644 --- a/system/kernel/console.zig +++ b/system/kernel/console.zig @@ -154,5 +154,3 @@ pub const Console = struct { while (x < self.fb.width) : (x += 1) destination[x] = source[x]; } }; - - diff --git a/system/kernel/devices-broker.zig b/system/kernel/devices-broker.zig index 6422e5c..49b90f9 100644 --- a/system/kernel/devices-broker.zig +++ b/system/kernel/devices-broker.zig @@ -66,6 +66,7 @@ fn record(node: *platform.Device, parent_id: u64) u64 { d.id = count; d.parent = parent_id; d.class = @intFromEnum(node.class); + d.pci_class = if (node.ids.pci_class) |code| code else device_abi.no_pci_class; const h = node.hid(); d.hid_len = @min(h.len, d.hid.len); @memcpy(d.hid[0..d.hid_len], h[0..d.hid_len]); @@ -170,6 +171,7 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.Device d.id = count; d.parent = parent_id; d.class = descriptor.class; + d.pci_class = descriptor.pci_class; d.hid_len = @min(descriptor.hid_len, d.hid.len); @memcpy(d.hid[0..@intCast(d.hid_len)], descriptor.hid[0..@intCast(d.hid_len)]); d.resource_count = descriptor.resource_count; diff --git a/system/kernel/kernel.zig b/system/kernel/kernel.zig index c2aa038..900bf25 100644 --- a/system/kernel/kernel.zig +++ b/system/kernel/kernel.zig @@ -87,7 +87,7 @@ fn kmain(boot_information: *const BootInformation) noreturn { log.print(" pitch : {d} bytes\n", .{fb.pitch}); log.print(" format : {s}\n", .{@tagName(fb.format)}); log.print(" framebuffer: 0x{x:0>16}\n", .{fb.base}); - log.print (" footprint : {d} MiB\n", .{(fb.pitch * fb.height) / (1024 * 1024)}); + log.print(" footprint : {d} MiB\n", .{(fb.pitch * fb.height) / (1024 * 1024)}); // Summarise the physical memory the loader handed us. The array is danos's // own MemoryRegion, so this is a plain slice — no firmware layout in sight. diff --git a/system/kernel/process.zig b/system/kernel/process.zig index 6fe3b09..4b18438 100644 --- a/system/kernel/process.zig +++ b/system/kernel/process.zig @@ -1082,4 +1082,4 @@ pub fn spawnProcessSupervised(image: []const u8, priority: u3, argv: []const []c /// a spin count, and time short delays. fn systemClock(state: *architecture.CpuState) void { architecture.setSystemCallResult(state, architecture.nanos()); -} \ No newline at end of file +} diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index e0a68f3..4aed2e4 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -2023,7 +2023,10 @@ fn faultNull() void { // access). `allowzero` skips the same null check on the cast. The write then // hits the unmapped page 0 and takes a real hardware #PF. var address: u64 = 0; - address = asm ("" : [ret] "=r" (-> u64) : [in] "0" (address)); + address = asm ("" + : [ret] "=r" (-> u64), + : [in] "0" (address), + ); const p: *allowzero volatile u64 = @ptrFromInt(address); p.* = 1; } @@ -2049,8 +2052,7 @@ fn faultDoubleFault() void { \\ud2 : : [sp] "r" (bad_sp), - : .{ .memory = true } - ); + : .{ .memory = true }); bad_sp += 0; } @@ -2069,8 +2071,7 @@ fn apDoubleFaultTask() void { \\ud2 : : [sp] "r" (bad_sp), - : .{ .memory = true } - ); + : .{ .memory = true }); bad_sp += 0; } diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 3f92e13..82310c3 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -42,6 +42,35 @@ fn driverFor(d: device.DeviceDescriptor) ?[]const u8 { }; } +/// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller: +/// Serial Bus Controller (0x0C) / USB Controller (0x03) / XHCI (0x30) — the names +/// pci-class.zig decodes. +const xhci_pci_class: u64 = 0x0C_03_30; + +/// The bus driver that serves a PCI function, or null. Unlike the singleton drivers +/// in `driverFor`, a machine can carry several identical controllers — so the caller +/// spawns one driver instance *per device*, passing the device id as argv[1] for the +/// instance to claim. +fn pciDriverFor(d: device.DeviceDescriptor) ?[]const u8 { + if (d.class != @intFromEnum(device.DeviceClass.pci_device)) return null; + return switch (d.pci_class) { + xhci_pci_class => "usb-xhci-bus", + else => null, + }; +} + +/// Spawn one instance of `driver_name` to serve the specific device `id` — the id +/// arrives as argv[1]. No isProcessRunning gate here: the name alone cannot tell two +/// instances apart, and this manager is the sole spawner of drivers. +fn spawnForDevice(driver_name: []const u8, id: u64) void { + var text: [20]u8 = undefined; + const id_text = std.fmt.bufPrint(&text, "{d}", .{id}) catch return; + if (system.spawnWithArguments(driver_name, &.{id_text}) != null) { + writeLine("device-manager: spawned {s} for device {d}\n", .{ driver_name, id }); + } else { + writeLine("device-manager: failed to spawn {s} for device {d}\n", .{ driver_name, id }); + } +} pub fn main() void { // Enumerate into a heap buffer (too big for the one-page user stack). @@ -54,6 +83,11 @@ pub fn main() void { var matched: usize = 0; for (buffer[0..count]) |descriptor| { + if (pciDriverFor(descriptor)) |driver_name| { + matched += 1; + spawnForDevice(driver_name, descriptor.id); + continue; + } const driver_name = driverFor(descriptor) orelse continue; matched += 1; if (!system.isProcessRunning(driver_name)) { diff --git a/system/services/vfs/protocol.zig b/system/services/vfs/protocol.zig index 397be77..c2ce854 100644 --- a/system/services/vfs/protocol.zig +++ b/system/services/vfs/protocol.zig @@ -8,8 +8,8 @@ //! spellings (`stat`, `O_CREAT`, ...) live only in the POSIX layer //! (library/posix/unistd.zig), which translates to these. //! -//! This is user-space only — the kernel knows nothing of files or paths; it only -//! moves the bytes. Shared by library/posix/unistd.zig (client) and system/services/vfs/vfs.zig (server). +//! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes. +//! Shared by library/posix/unistd.zig (client) and system/services/vfs/vfs.zig (server). pub const Operation = enum(u32) { open, // open(path) -> node id diff --git a/tools/rewrap-comments.py b/tools/rewrap-comments.py new file mode 100755 index 0000000..813bc2e --- /dev/null +++ b/tools/rewrap-comments.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Rewrap over-long Zig comment lines at a column limit (default 100). + +Rules: +- Only comment-only lines are touched; trailing comments after code are left alone. +- Consecutive comment lines with the same indentation and marker (`//`, `///`, `//!`) + form a block. Blank comment lines separate paragraphs within a block. +- A line whose text starts with `- ` begins a bullet paragraph; its continuation + lines are the ones indented to align under the bullet's text (bullet lead + 2). +- Plain paragraphs join consecutive lines with the same text indentation; + wrapped lines align where the first line's text begins. +- A paragraph is rewrapped only if at least one of its lines exceeds the limit, + so deliberate short line breaks elsewhere are preserved. +""" + +import argparse +import difflib +import re +import subprocess +import sys +import textwrap + +LIMIT = 100 +COMMENT_RE = re.compile(r"^(\s*)(//[/!]?)(?:\s(.*))?$") +BULLET_RE = re.compile(r"^(\s*)- (.*)$") + + +def split_paragraphs(texts): + """texts: list of comment text (None for a bare marker line). + Returns paragraphs: dicts with lead/hang/bullet/texts/lines(indices).""" + paragraphs = [] + current = None + for index, text in enumerate(texts): + if text is None or text.strip() == "": + paragraphs.append({"literal": True, "lines": [index]}) + current = None + continue + lead = len(text) - len(text.lstrip(" ")) + bullet = BULLET_RE.match(text) + if bullet: + current = { + "lead": len(bullet.group(1)), + "hang": len(bullet.group(1)) + 2, + "bullet": True, + "texts": [bullet.group(2)], + "lines": [index], + } + paragraphs.append(current) + elif current is not None and lead == current["hang"]: + current["texts"].append(text) + current["lines"].append(index) + else: + current = { + "lead": lead, + "hang": lead, + "bullet": False, + "texts": [text], + "lines": [index], + } + paragraphs.append(current) + return paragraphs + + +def wrap_paragraph(paragraph, prefix): + joined = re.sub(r"\s+", " ", " ".join(t.strip() for t in paragraph["texts"])) + if paragraph["bullet"]: + initial = prefix + " " * paragraph["lead"] + "- " + else: + initial = prefix + " " * paragraph["lead"] + subsequent = prefix + " " * paragraph["hang"] + return textwrap.wrap( + joined, + width=LIMIT, + initial_indent=initial, + subsequent_indent=subsequent, + break_long_words=False, + break_on_hyphens=False, + ) + + +def rewrap_block(original_lines, indent, marker, texts): + prefix = indent + marker + " " + output = [] + for paragraph in split_paragraphs(texts): + block_originals = [original_lines[i] for i in paragraph["lines"]] + if paragraph.get("literal") or all(len(l) <= LIMIT for l in block_originals): + output.extend(block_originals) + else: + output.extend(wrap_paragraph(paragraph, prefix)) + return output + + +def process(source): + lines = source.split("\n") + result = [] + i = 0 + while i < len(lines): + match = COMMENT_RE.match(lines[i]) + if not match: + result.append(lines[i]) + i += 1 + continue + indent, marker = match.group(1), match.group(2) + block_lines, texts = [], [] + while i < len(lines): + m = COMMENT_RE.match(lines[i]) + if not m or m.group(1) != indent or m.group(2) != marker: + break + block_lines.append(lines[i]) + texts.append(m.group(3)) + i += 1 + result.extend(rewrap_block(block_lines, indent, marker, texts)) + return "\n".join(result) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--write", action="store_true", help="apply changes (default: diff only)") + parser.add_argument("files", nargs="*", help="files to process (default: git ls-files '*.zig')") + args = parser.parse_args() + + files = args.files or subprocess.run( + ["git", "ls-files", "*.zig"], capture_output=True, text=True, check=True + ).stdout.split() + + changed = 0 + for path in files: + with open(path, encoding="utf-8") as f: + source = f.read() + rewrapped = process(source) + if rewrapped == source: + continue + changed += 1 + if args.write: + with open(path, "w", encoding="utf-8") as f: + f.write(rewrapped) + print(f"rewrapped {path}") + else: + sys.stdout.writelines( + difflib.unified_diff( + source.splitlines(keepends=True), + rewrapped.splitlines(keepends=True), + fromfile=path, + tofile=path, + ) + ) + if not changed: + print("no comments over the limit") + + +if __name__ == "__main__": + main()