summaryrefslogtreecommitdiff
path: root/src/board_pins.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-27 16:42:15 -0300
committerGabriel Schneider <[email protected]>2026-08-27 22:07:32 -0300
commit147ebd4a36ec7199074ba05bcfb79d4a656c0b74 (patch)
tree400441fc7b103152cd741aec7ee42b82b943aed8 /src/board_pins.zig
parentdef843b2f59b867ee9b1d501f559f59fb335d4cc (diff)
downloadpardes-147ebd4a36ec7199074ba05bcfb79d4a656c0b74.tar.gz
pardes-147ebd4a36ec7199074ba05bcfb79d4a656c0b74.zip
9p: the client half, and a board that serves its own tree over the UART
Step 5 of the 9P chain (docs/9p.typ 12.5, docs/registry.typ 9P-22, 9P-11, BOARD-1). THE CLIENT. `Client` in src/9p.zig is the mirror of `Server` and the same shape: sans-io, no allocator, no threads, no descriptor, caller-owned buffers, and it builds freestanding. 152 bytes of struct against the server's 9,488, because a client owns neither a fid table nor a park table -- the far end does. The API is submit / push+output+wrote / take. Completion is a PULL: a callback would fire inside push, inside the transport's read, inside the host's poll dispatch, which is exactly where fs9_service says filesystem work must not happen. `take()` returns the next completed operation or null, which is `Server.next()`'s loop-until-null contract read from the other side. Tags are a fixed 16-entry table indexed BY the tag, so an out-of-order reply -- which 9P allows and both reference clients rely on -- costs one bounds check. The reply's TYPE is checked against the request's op, because a tag is only as good as the table behind it. A `Done` borrows the input buffer and is valid until the next call; `take()` releases the previous frame on entry, so the rule is mechanical rather than remembered, and read data and error strings are zero-copy. And one real caller, so this is not a library with no user: the `9p` word takes a dial and a path, walks another instance's tree, and opens the bytes in a pane like any other `Look`. THE BOARD. A SECOND image, not a second role: the console runtime keeps UART0 bidirectionally and is behaviourally untouched. On the new one the UART carries 9P AND NOTHING ELSE -- no ANSI, no vaxis, no allocator, no heap module. The loop is uart.read -> push / retry+next -> handle -> reply / output -> writeSome -> wrote. `writeSome` is new and additive: `write`'s bounded spin DROPS bytes on a stalled transmitter, which on a protocol stream truncates a reply mid-message and desynchronises for good, where a short count cannot. BOARD-1's one divider write raises the line to 921600. 88,000 B text, 49,424 B bss, an 88,080-byte image -- 5.7% of the 1,536,000 B partition, against the console image's 809,536 B. THE COMPTIME BRIDGE, which is the part worth reading. `board9p.caps` is the ONLY place the GPIO tree is described; node ids, parents, names, permissions, handlers, buffer size and the per-pin directories are all derived from it, and `fan.dirs` makes `gpio/<n>/value` one table entry serving eleven pins. Modes are derived from which handlers a file has rather than declared. A second capability is a table entry, not new tree code. JP1 became a real table in the new leaf `src/board_pins.zig`, with the ASCII drawing RENDERED from it at comptime and the pin list COLLECTED from it -- the 9P image links no core and so cannot import board_memory.zig, and copying the table was not acceptable. A golden test pins the drawing byte for byte, the console's own shape test still passes, and the identical bytes are present in all three artifacts. PROVED. Two daemons: B read A's `/1/body` through the `9p` word into a pane, byte-identical to plan9port's `9p read` of the same path. Both board images build. No hardware was attached, so nothing about the board is claimed beyond what builds and what the host tests cover. zig build unit-test 585/585. fs-bench unchanged and still zero allocations on every read row. --- REVIEW FIXES FOLDED IN. Steps 3, 4 and 5 were verified on the happy path and then adversarially reviewed by three agents; eight defects, six fixed here, five of them reproduced with measurements before and after. Full writeup in docs/registry.typ `9P-27`. In brief: * a remote crash of the WHOLE daemon: one `size[4]` of zero plus one byte hit `unreachable` in `fs9_service.fill`. Also 99.7% of a core when the stuck buffer made `room == 0` return without reading. Now `srv.dead` is a hangup, checked before the room guard. * the editor froze 177 s on a dial: `connect(2)` ran on a still-BLOCKING socket before the deadline existed, and a full accept backlog waits forever. Now non-blocking with the wait spent against the budget. After: 2.03 s. * a 64 KiB pty read is exactly `queue_cap` and wiped every unread byte AND dropped itself. `notePtyOutput` splits at half the cap. Deterministic. * four silent sockets denied `--fs9` forever; connections now expire on the same five-second rule the frontend transport already had. * EMFILE spun a core; the listener pauses and leaves the poll set, as the frontend listener does. * `max_fids = 32` made `find` over `9pfuse` fail with 57 consecutive `Rerror`s -- refuting this step's own acceptance clause. 256 for a host, `board_fids` 32 for the microcontroller. Found clean and worth recording: `sig` reaches the foreground process group; the two-namespace pty lookup is right over both transports; `PaneFile`'s u4 wall is guarded; reader counts release on every abrupt-death path; `fs_origin` routing and the reply arithmetic hold under probing.
Diffstat (limited to 'src/board_pins.zig')
-rw-r--r--src/board_pins.zig186
1 files changed, 186 insertions, 0 deletions
diff --git a/src/board_pins.zig b/src/board_pins.zig
new file mode 100644
index 00000000..134cd92c
--- /dev/null
+++ b/src/board_pins.zig
@@ -0,0 +1,186 @@
+//! JP1, the JC-ESP32P4-M3-DEV's 26-pin header, as ONE TABLE that everything else is derived from:
+//! the ASCII drawing the `Gpio` word prints, and the pin directories the board's 9P tree generates.
+//!
+//! WHY THIS IS ITS OWN FILE, and it is the whole reason it exists. The drawing lived in
+//! `src/board_memory.zig`, which imports `pardes.zig` and therefore the entire core; the board's 9P
+//! image (`src/esp32p4_9p.zig`) links no core at all, so it could not have reached it. The two
+//! ways out of that were a second copy of the header in the 9P tree — a table of thirteen rows
+//! transcribed off a schematic, maintained twice, with no test that could tell you the day they
+//! disagreed — or this: a LEAF that imports `std` and nothing else, so both sides import the same
+//! thirteen rows. `board_memory.zig` keeps its `pinout` name as an alias of `jp1_text` and its own
+//! shape test, so the console word's output is unchanged to the byte.
+//!
+//! WHY A TABLE AND NOT THE STRING. The string was the source before, and a string is fine for one
+//! consumer that prints it. It is no use at all to the second, which needs to know WHICH of these
+//! twenty-six pins are the P4's own GPIOs, because that is the set of directories its tree has. A
+//! consumer would have to parse the drawing back out — scan for `GPIO `, take the digits, hope
+//! nobody aligned a column differently — which is exactly the sort of code that works until the
+//! day the drawing is edited. So the rows are data, the drawing is RENDERED from them at comptime,
+//! and `gpio_pins` is COLLECTED from them at comptime. Adding a pin to the header is one row, and
+//! the drawing, the pin list and the 9P tree all move together because there is only one of them.
+//!
+//! READ OFF THE VENDOR SCHEMATIC, sheet 2 "Expand IO"
+//! (`01-esp32p4-m3/docs/schematics/2_EXPAND_IO&BAT.png`), which is the only document that carries
+//! this mapping — the specification PDF's "Interface Description" page is a marketing render, and
+//! there is no board user guide. The sheet is a 872x1168 raster, so the assignment was taken from
+//! the drawing's own geometry rather than by eye: thirteen wires leave each side of the symbol, a
+//! net wire runs ~100 px to its label and a power stub ~21 px, which is what identifies pin 8 as
+//! unconnected rather than as the first of the GPIO4x labels. Cross-checked against a second,
+//! independent source: `05-zig-p4/build.zig` has always documented `-Dled=20` as "JP1 pin 17", and
+//! GPIO20 lands on pin 17 here.
+const std = @import("std");
+
+/// What is behind one header pin, and the ONE distinction that matters to both consumers: whether
+/// this pad is a GPIO of the ESP32-P4 this program is running on.
+///
+/// `.none` is a pin the header brings out with nothing behind it (pin 8). `.net` is a pad that is
+/// not the P4's to drive as a GPIO: `3V3`, `5V` and `GND` are power, `C6_*` are the ESP32-C6
+/// companion's pins — toggling a P4 GPIO cannot reach them — and `ES_I2C_*` is the audio codec's
+/// bus. The codec's two ARE P4 pads, and they are `.net` anyway, deliberately: the schematic does
+/// not name their GPIO numbers, and a tree that invented one would offer a file that drives an
+/// unknown pin. They stay in the drawing because a shared bus is a reason to know the pin is there.
+pub const Pad = union(enum) {
+ none,
+ /// a P4 GPIO, by the number the schematic, the silkscreen and the datasheet all use
+ gpio: u8,
+ /// a named net that is not a P4 GPIO
+ net: []const u8,
+
+ /// The text this pad wears in the drawing. `GPIO 47` and not `GPIO47`: the space is what the
+ /// header has always printed, and the shape test in `board_memory.zig` matches on it.
+ pub fn label(p: Pad) []const u8 {
+ return switch (p) {
+ .none => "--",
+ .gpio => |n| std.fmt.comptimePrint("GPIO {d}", .{n}),
+ .net => |s| s,
+ };
+ }
+};
+
+/// One row of the header: the odd pin on the left, the even pin on its right, exactly as the board
+/// wears it. The pin NUMBERS are not stored — row `i` is pins `2i+1` and `2i+2` — because a
+/// hand-written number beside a row is a number that can disagree with its position.
+pub const Row = struct { left: Pad, right: Pad };
+
+/// JP1 itself: thirteen rows, pin 1 at the top left. THE SINGLE SOURCE for the drawing below, for
+/// `gpio_pins`, and for the per-pin directories in `src/board9p.zig`.
+pub const jp1 = [13]Row{
+ .{ .left = .{ .net = "3V3" }, .right = .{ .net = "5V" } },
+ .{ .left = .{ .net = "3V3" }, .right = .{ .net = "5V" } },
+ .{ .left = .{ .net = "GND" }, .right = .{ .net = "GND" } },
+ .{ .left = .{ .gpio = 1 }, .right = .none },
+ .{ .left = .{ .gpio = 2 }, .right = .{ .gpio = 47 } },
+ .{ .left = .{ .gpio = 3 }, .right = .{ .gpio = 46 } },
+ .{ .left = .{ .gpio = 4 }, .right = .{ .gpio = 45 } },
+ .{ .left = .{ .gpio = 5 }, .right = .{ .net = "GND" } },
+ .{ .left = .{ .gpio = 20 }, .right = .{ .net = "3V3" } },
+ .{ .left = .{ .gpio = 32 }, .right = .{ .net = "C6_U0RXD" } },
+ .{ .left = .{ .gpio = 33 }, .right = .{ .net = "C6_U0TXD" } },
+ .{ .left = .{ .net = "ES_I2C_SDA" }, .right = .{ .net = "C6_IO9" } },
+ .{ .left = .{ .net = "ES_I2C_SCL" }, .right = .{ .net = "C6_CHIP_PU" } },
+};
+
+/// The row format, and it is load-bearing rather than cosmetic: a header drawn in two columns stops
+/// being a header the moment a row wraps or a column slips, and the widest row here is 34 columns
+/// against the board's own 80-column grid. Ten for the left label right-aligned, two for each pin
+/// number, and the three bars land under the box's own corners because the left label's field plus
+/// one space is eleven characters and `+---------+` is eleven wide.
+///
+/// `board_memory.zig`'s "the pinout fits the board's own grid" test is the check that this stays
+/// true, and it checks the RENDERED text mechanically — every pin row's first bar in the same
+/// column — rather than trusting this string.
+const row_format = "{s:>10} | {d:>2} | {d:>2} | {s}\n";
+
+/// The box the pin numbers sit inside. Eleven characters, indented by the left label's field width
+/// plus the space before the first bar, so its corners are the bars.
+const border = " +---------+\n";
+
+/// JP1 as the text the `Gpio` word prints and a read of the 9P tree's `gpio/pinout` returns — the
+/// SAME BYTES, which is a test in `src/board9p.zig` and not a hope.
+///
+/// The trailer names the `Gpio` word, which the 9P image does not have. It is here anyway, because
+/// "the same bytes" is worth more than a sentence that is true of both faces and useful to neither:
+/// a person reading this table through 9P is a person who has the editor's own console in the other
+/// window, and telling them the word that flips a pin is telling them something they can use. The
+/// 9P equivalent — writing `0` or `1` to `gpio/<n>/value` — is documented where a 9P client will
+/// look for it, which is the tree's own doc comment.
+pub const jp1_text = text: {
+ var out: []const u8 =
+ \\JP1 header - 26 pins, pin 1 top left.
+ \\Every number here is DECIMAL.
+ \\
+ \\
+ ;
+ out = out ++ border;
+ for (jp1, 0..) |row, i| out = out ++ std.fmt.comptimePrint(
+ row_format,
+ .{ row.left.label(), 2 * i + 1, 2 * i + 2, row.right.label() },
+ );
+ break :text out ++ border ++
+ \\
+ \\Gpio <pin> flips one: 0->1 or 1->0.
+ \\
+ ;
+};
+
+/// Every P4 GPIO JP1 brings out, ascending. THE SET OF PIN DIRECTORIES the board's 9P tree has, so
+/// that tree has exactly the pins this board has and not a range somebody typed.
+///
+/// Ascending rather than in header order, because the consumer is `ls`: the header's order puts 47
+/// between 2 and 3, and a directory listing that counts 1 2 3 4 5 20 32 33 45 46 47 is one a person
+/// can scan. Nothing depends on the order — the names are the pin numbers — so it may as well be
+/// the readable one.
+pub const gpio_pins = pins: {
+ var found: [2 * jp1.len]u8 = undefined;
+ var n: usize = 0;
+ for (jp1) |row| for ([2]Pad{ row.left, row.right }) |p| switch (p) {
+ .gpio => |g| {
+ found[n] = g;
+ n += 1;
+ },
+ else => {},
+ };
+ std.mem.sort(u8, found[0..n], {}, std.sort.asc(u8));
+ break :pins found[0..n].*;
+};
+
+// The drawing, byte for byte, because it is the one thing here whose CORRECTNESS IS ITS SHAPE and
+// because it used to be a string literal: this is the check that the renderer above reproduces what
+// the console has always printed. A golden test is the right kind of duplication — the expectation
+// is the thing being asserted, and if the two ever differ the diff says which byte.
+test "the rendered header is the drawing the console has always printed" {
+ try std.testing.expectEqualStrings(
+ \\JP1 header - 26 pins, pin 1 top left.
+ \\Every number here is DECIMAL.
+ \\
+ \\ +---------+
+ \\ 3V3 | 1 | 2 | 5V
+ \\ 3V3 | 3 | 4 | 5V
+ \\ GND | 5 | 6 | GND
+ \\ GPIO 1 | 7 | 8 | --
+ \\ GPIO 2 | 9 | 10 | GPIO 47
+ \\ GPIO 3 | 11 | 12 | GPIO 46
+ \\ GPIO 4 | 13 | 14 | GPIO 45
+ \\ GPIO 5 | 15 | 16 | GND
+ \\ GPIO 20 | 17 | 18 | 3V3
+ \\ GPIO 32 | 19 | 20 | C6_U0RXD
+ \\ GPIO 33 | 21 | 22 | C6_U0TXD
+ \\ES_I2C_SDA | 23 | 24 | C6_IO9
+ \\ES_I2C_SCL | 25 | 26 | C6_CHIP_PU
+ \\ +---------+
+ \\
+ \\Gpio <pin> flips one: 0->1 or 1->0.
+ \\
+ , jp1_text);
+}
+
+// The pin list is the tree's shape, so it is asserted as a list rather than as a count: a row edited
+// wrongly changes WHICH pins the board offers, and a count would not notice a 45 that became a 44.
+test "the header's own GPIOs, and only those" {
+ try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3, 4, 5, 20, 32, 33, 45, 46, 47 }, &gpio_pins);
+ // Pin 8 is unconnected and pin 24 is the C6's, so neither contributes a pad. Both are counted
+ // here rather than only drawn, because "the tree has exactly the pins the board has" is a claim
+ // about what is ABSENT as much as what is present.
+ try std.testing.expectEqual(Pad.none, jp1[3].right);
+ try std.testing.expectEqualStrings("C6_IO9", jp1[11].right.net);
+}