diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/board_memory.zig | 191 | ||||
| -rw-r--r-- | src/builtins.zig | 18 | ||||
| -rw-r--r-- | src/config.zig | 2 | ||||
| -rw-r--r-- | src/host.zig | 12 | ||||
| -rw-r--r-- | src/p4.zig | 26 | ||||
| -rw-r--r-- | src/pardes.zig | 79 |
6 files changed, 276 insertions, 52 deletions
diff --git a/src/board_memory.zig b/src/board_memory.zig index d2878d2d..79a9a2cf 100644 --- a/src/board_memory.zig +++ b/src/board_memory.zig @@ -1,13 +1,13 @@ -//! The board's own address space, as text: the Peek, Poke and Hexdump -//! builtins' whole implementation. +//! The board's own address space and its pins, as text: the Peek, Poke, Hexdump and Gpio builtins' +//! whole implementation. //! -//! BARE METAL ONLY (`enabled` below), and the reason is not caution but -//! honesty: with no OS there is no MMU, no supervisor and no process — the -//! editor IS the system software — so every one of the 2^32 addresses is -//! legitimately this program's to read and write, and a word that could name -//! only some of them would be lying about where it is running. Under an OS the -//! same three words would be either a segfault or a syscall stub, so they are -//! absent from those builds entirely rather than present and refusing. +//! THE P4 BUILD ONLY (`enabled` below), and the reason is not caution but honesty: with no OS there +//! is no MMU, no supervisor and no process - the editor IS the system software - so every one of the +//! 2^32 addresses is legitimately this program's to read and write, and a word that could name only +//! some of them would be lying about where it is running. Under an OS the same words would be either +//! a segfault or a syscall stub, so they are absent from those builds entirely rather than present +//! and refusing. Absent means not compiled, not hidden: nothing below is analysed for a build whose +//! platform is not `p4`. //! //! Everything here goes through `*allowzero volatile` pointers. A peripheral //! register is not memory: reading UART_STATUS twice is two reads and must not @@ -25,30 +25,33 @@ const pardes = @import("pardes.zig"); const Pardes = pardes.Pardes; const output_pane = @import("output_pane.zig"); -/// The one gate, and it is derived from the TARGET rather than from -/// `pardes.platform`: these three words are not a product configuration, they -/// are a property of running with no operating system under you, and a -/// predicate spelled out of `builtin` cannot drift from that the way a -/// hand-maintained platform enum can. Same idiom as allocators.zig's tiers. +/// THE ONE GATE, and it names the p4 build, so `Peek`, `Poke`, `Hexdump` and `Gpio` are analysed +/// and emitted for that build and for no other. Nothing in this file reaches any other target's +/// binary: not the volatile accessors, not the JP1 pinout, not the parsers. /// -/// Wasm is `freestanding` too — that is the `web` platform — and it is exactly -/// what this must exclude: inside the browser's sandbox an address is an offset -/// into a linear memory the engine owns, so a "peek" there would read a number -/// that means nothing about any machine and a "poke" would corrupt the heap -/// this same editor is running out of. Bare metal is the freestanding target -/// whose addresses are the bus's. -pub const enabled = builtin.os.tag == .freestanding and !builtin.target.cpu.arch.isWasm(); +/// This used to be derived from the target - `os.tag == .freestanding and !isWasm()` - on the +/// argument that these words are a property of having no operating system rather than a product +/// configuration, and that a predicate spelled out of `builtin` cannot drift the way a +/// hand-maintained enum can. The argument was tidy and it answered the wrong question. A word +/// only exists if some SHELL offers it, and the shells are the platforms; `Gpio` settles it beyond +/// argument, because its whole content is one board's header, and a second freestanding port would +/// need its own pinout rather than inheriting this one. "Bare metal" was never the requirement, +/// "this board" was, and the two only looked identical because there is currently one of them. +/// +/// The old predicate's real work was excluding wasm, which is `freestanding` too - inside the +/// browser's sandbox an address is an offset into a linear memory the engine owns, so a `Peek` +/// would read a number that means nothing about any machine and a `Poke` would corrupt the heap +/// this same editor runs out of. Naming `p4` excludes it by construction rather than by a term +/// somebody has to keep remembering. +pub const enabled = pardes.platform == .p4; -// `pardes.platform` is not the gate, but it IS an independent witness, so each -// of the three interesting builds proves its own half of the predicate rather -// than leaving "wasm is freestanding" as a comment nobody re-checks. The one -// that matters is the middle line: without the `isWasm` term above, the web -// build would silently hand a browser tab a Poke that writes into the linear -// memory this editor's own heap lives in. +// The target is now the WITNESS rather than the gate: whatever else `p4` means, it has to still be +// a machine whose addresses are the bus's, and a hosted or wasm build reaching this line means the +// platform and the target disagree about what the firmware is. comptime { - if (pardes.hosted and enabled) @compileError("an OS is not bare metal"); - if (pardes.platform == .web and enabled) @compileError("wasm is not bare metal"); - if (pardes.platform == .p4 and !enabled) @compileError("the P4 firmware is bare metal"); + if (enabled and pardes.hosted) @compileError("an OS is not bare metal"); + if (enabled and builtin.os.tag != .freestanding) @compileError("the P4 firmware is freestanding"); + if (enabled and builtin.target.cpu.arch.isWasm()) @compileError("wasm addresses are not a bus"); } /// How much of the address space ONE command may render. @@ -83,6 +86,11 @@ pub const Error = error{ /// the message row instead. MisalignedAddress, ExtraArgument, + /// not a number, or a number the part does not have a pad for + BadPin, + /// the host brought no pads: every build but the firmware, where the word + /// is not registered at all, and a firmware too old to pass the hook + NoPads, }; /// EVERY literal these three words take is HEX, with or without an `0x`, and there is no way to @@ -247,6 +255,49 @@ test "every literal is hex, with or without the prefix" { try std.testing.expectError(Error.BadValue, parseValue("0x1_0000_0000")); } +// The pinout is the one thing here whose CORRECTNESS IS ITS SHAPE: a header drawn in two columns +// stops being a header the moment a row wraps, and it wraps on the board rather than on a +// developer's terminal, which is the worst place to find out. So the width is asserted against the +// grid the board is actually built with, and the alignment is asserted against the column the pin +// numbers are supposed to share. +test "the pinout fits the board's own grid, in two aligned columns" { + const cols: usize = @import("pardes_config").p4_cols; + // Seven columns of the shell's grid go to the line-number gutter before a pane's text starts. + const usable = cols - 7; + + var rows: usize = 0; + var pins: usize = 0; + var first_bar: ?usize = null; + var it = std.mem.splitScalar(u8, pinout, '\n'); + while (it.next()) |line| { + try std.testing.expect(line.len <= usable); + rows += 1; + // A pin row is one with two numbers in it; every one must put its bars in the same place, + // which is what "aligned in two columns" means when the check is mechanical. + const bar = std.mem.indexOfScalar(u8, line, '|') orelse continue; + if (line[line.len - 1] == '+') continue; + pins += 1; + if (first_bar) |b| try std.testing.expectEqual(b, bar) else first_bar = bar; + } + try std.testing.expectEqual(@as(usize, 13), pins); + try std.testing.expect(rows > 15); + + // Two independent facts about the board, each with a witness outside this file: GPIO20 is + // `05-zig-p4/build.zig`'s documented `-Dled` default ("JP1 pin 17"), and pin 8 is the one + // header pin the vendor schematic leaves unconnected. + try std.testing.expect(std.mem.indexOf(u8, pinout, "GPIO 20 | 17 |") != null); + try std.testing.expect(std.mem.indexOf(u8, pinout, "| 8 | --") != null); +} + +// The exception to the file's own rule, so it is written down as a test rather than only as a +// comment: a pin number is part of a name and is read as decimal, while every address beside it is +// hex. `Gpio 20` must mean the pin the schematic calls GPIO20, not 0x20. +test "a pin number is decimal, unlike every address in this file" { + try std.testing.expectEqual(@as(u16, 20), try std.fmt.parseInt(u16, "20", 10)); + try std.testing.expectEqual(@as(u32, 0x20), try parseAddr("20")); + try std.testing.expect(20 != 0x20); +} + /// `Peek <addr> [count]` — count 32-bit words at addr, one `addr: value` row /// each. One word per row rather than four so that every row carries its own /// address: the rows are then ordinary Look targets, and `Peek` or `Poke` @@ -296,6 +347,86 @@ pub fn poke(p: *Pardes, id: usize, argument: []const u8) !void { ) catch unreachable); } +/// JP1, the 26-pin header down the left edge of the JC-ESP32P4-M3-DEV, as the board wears it: two +/// columns, odd pins on the left, even on the right, pin 1 at the top. +/// +/// 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. +/// +/// `--` is a pin the header brings out with nothing behind it. `C6_*` are the ESP32-C6 companion's +/// pads, not the P4's, and toggling a P4 GPIO cannot reach them. `ES_I2C_*` is the audio codec's +/// bus, shared - driving either one by hand while the codec is live is a collision, which is a +/// reason to know the pin is there rather than a reason to hide it. +const pinout = + \\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. + \\ +; + +/// `Gpio <pin>` flips one pad and says what it did; `Gpio` alone draws JP1. +/// +/// THE PIN NUMBER IS DECIMAL, and it is the one literal in this file that is. Every other one is +/// hex because every other one is an address, and addresses come off datasheets and linker maps +/// that print hex. A GPIO number is not an address - it is part of a NAME. The schematic says +/// `GPIO47`, the silkscreen says 47, the datasheet's pin table says 47, and `Gpio 20` meaning pin +/// 32 would be a trap laid for the one argument a person types from memory. One rule per KIND of +/// literal beats one rule per file when the kinds are this different. +/// +/// The toggle is the host's to perform (`Host.VTable.pull_gpio_toggle`) even though `Poke` two +/// functions up would happily write GPIO_OUT_REG directly. Writing that register is not the job: +/// a pad has to be pointed at the GPIO peripheral in the IO MUX, routed in the GPIO matrix, have +/// its driver and input buffer enabled, and only then be driven - and getting that wrong on a pin +/// that boots as something else is how you lose the console you are typing on. +/// +/// Reported levels are the OUTPUT bits, before and after, because that is what a toggle means: the +/// level this board is DRIVING. A pad's input buffer on an unconnected header pin reads whatever +/// the air says. +pub fn gpio(p: *Pardes, id: usize, argument: []const u8) !void { + var it = std.mem.tokenizeAny(u8, argument, " \t\r\n"); + const tok = it.next() orelse { + // No argument is not an error and not inert: it is the question "which pins are there", + // and the answer is a picture of the header. + const content = try p.gpa.dupe(u8, pinout); + errdefer p.gpa.free(content); + return fill(p, id, .{ .cmd = .Gpio }, content); + }; + if (it.next() != null) return Error.ExtraArgument; + const pin = std.fmt.parseInt(u16, tok, 10) catch return Error.BadPin; + + const toggle = p.host.vtable.pull_gpio_toggle orelse return Error.NoPads; + var was: u8 = 0; + var now: u8 = 0; + if (!toggle(p.host.ctx, pin, &was, &now)) return Error.BadPin; + + var buf: [48]u8 = undefined; + p.setMessage(id, std.fmt.bufPrint(&buf, "GPIO {d}: {d}->{d}", .{ pin, was, now }) catch unreachable); +} + /// Bytes per dumped row, and it is a different number on the board. /// /// `hexdump -C`'s sixteen is the layout everyone can already read, and it needs 79 columns: ten for diff --git a/src/builtins.zig b/src/builtins.zig index 0c9993a7..23ae18c3 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -985,3 +985,21 @@ pub const Hexdump = struct { c.p.reportError(c.id, "hexdump", err); } }; + +/// `Gpio <pin>` — flip one pad, answered on the message row as `0->1`. Bare `Gpio` draws JP1's +/// pinout into a pane instead, because the first question about a header is which pins it has. +/// +/// The only word here whose argument is DECIMAL, and `board_memory.gpio` says why at length: a +/// GPIO number is part of a name, not an address. +pub const Gpio = struct { + pub const takes_arg = true; + pub const enabled = board_memory.enabled; + pub const output: OutputTraits = .{ .name = config.gpio_buffer }; + pub fn run(c: Ctx) void { + if (comptime enabled) apply(c) else unreachable; + } + fn apply(c: Ctx) void { + board_memory.gpio(c.p, c.id, c.arg orelse "") catch |err| + c.p.reportError(c.id, "gpio", err); + } +}; diff --git a/src/config.zig b/src/config.zig index c9d414ca..15a21866 100644 --- a/src/config.zig +++ b/src/config.zig @@ -235,6 +235,7 @@ pub const leader_path = paths: { table.set(.Peek, null); table.set(.Poke, null); table.set(.Hexdump, null); + table.set(.Gpio, null); } // The pane-local PDF commands exist only in MuPDF builds through their // explicit registry availability, so name their paths inside the same @@ -799,6 +800,7 @@ pub const changelog_buffer = "+Changelog"; /// from every hosted build along with the builtins that name them. pub const peek_buffer = "+Peek"; pub const hexdump_buffer = "+Hexdump"; +pub const gpio_buffer = "+Gpio"; /// The empty buffer New and Newcol open: no file behind it yet, so Save asks /// for a path (prefilled with the inherited directory). pub const scratch_buffer = "+New"; diff --git a/src/host.zig b/src/host.zig index e44318da..9ca882b1 100644 --- a/src/host.zig +++ b/src/host.zig @@ -80,6 +80,18 @@ pub const Host = struct { /// must not re-enter the core. pull_tty_taken: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null, + // ---- the board's own pads ---- + /// Flip one GPIO and report the level it held and the level it now holds. False means the + /// host would not do it: a pin number outside the part, or no pads at all. + /// + /// A pull, because there is one answer. The HOST answers it rather than the core reaching + /// for the registers itself - which `Peek` and `Poke` do two functions away - because + /// driving a pad correctly is not one register. It is the IO MUX function select, the GPIO + /// matrix output route, the pad's drive and input-buffer bits, and the output enable, keyed + /// by a per-pin table. The firmware already owns that code and checks it against ESP-IDF's + /// own headers on the die; a second copy in here would be a second copy nobody tests. + pull_gpio_toggle: ?*const fn (ctx: ?*anyopaque, pin: u16, was: *u8, now: *u8) bool = null, + // ---- the filesystem ---- /// `pane` travels with the bytes only so a host that posts a "saved" /// message row can name the right pane; the core already resolved the @@ -93,7 +93,9 @@ fn panicImpl(msg: []const u8, _: ?usize) noreturn { // cheapest possible defence: the firmware calls it first and refuses to continue on a mismatch. /// Bumped whenever any signature below changes, including a type. -const abi_version: u32 = 1; +/// 2 added `GpioFn` to `pardes_p4_init`. A firmware built against 1 passes five arguments where six +/// are read, which is exactly the silent-corruption case this counter exists to turn into a message. +const abi_version: u32 = 2; export fn pardes_p4_abi_version() callconv(.c) u32 { return abi_version; @@ -115,6 +117,15 @@ pub const Allocator = extern struct { /// How finished runs of ANSI leave this object. pub const WriteFn = *const fn (ctx: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void; +/// Flip one pad and report the level before and after; false if the firmware declines. OPTIONAL on +/// the wire, so a host with no pads (or one that has not implemented them yet) passes null and the +/// `Gpio` word answers "no pads" instead of the object having to know which firmwares exist. +/// +/// The board's side, not the editor's, because a correct toggle is the IO MUX, the GPIO matrix, the +/// pad's own bits and the output enable - four register files behind a per-pin table that the +/// firmware already has and checks against ESP-IDF. See `Host.VTable.pull_gpio_toggle`. +pub const GpioFn = *const fn (ctx: ?*anyopaque, pin: u16, was: *u8, now: *u8) callconv(.c) bool; + // ------------------------------------------------------------------- the allocator, rebuilt // One `std.mem.Allocator` whose vtable forwards to the four pointers above. The indirection is the // price of the seam and it is paid once per allocation, which on a first-fit heap is already the @@ -158,6 +169,7 @@ fn gpa() std.mem.Allocator { var out_write: WriteFn = undefined; var out_ctx: ?*anyopaque = null; +var host_gpio: ?GpioFn = null; var out_buf: [8192]u8 = undefined; var out: std.Io.Writer = undefined; @@ -247,12 +259,14 @@ fn vaxisSize() vaxis.Winsize { export fn pardes_p4_init( alloc: *const Allocator, write: WriteFn, + gpio: ?GpioFn, ctx: ?*anyopaque, cols: u16, rows: u16, ) callconv(.c) u32 { host_alloc = alloc.*; out_write = write; + host_gpio = gpio; out_ctx = ctx; out = .{ .vtable = &.{ .drain = drain }, .buffer = &out_buf }; @@ -594,7 +608,15 @@ export fn pardes_p4_quit() callconv(.c) bool { // ------------------------------------------------------------------------------------ the host -const pardes_host: pardes.Host.VTable = .{ .push_present = present }; +const pardes_host: pardes.Host.VTable = .{ .push_present = present, .pull_gpio_toggle = gpioToggle }; + +/// The `Gpio` word's one seam to the board. Nothing here knows what a pad is; it forwards, and +/// answers false when the firmware brought none, which is what puts "gpio: NoPads" on the message +/// row rather than a trap. +fn gpioToggle(_: ?*anyopaque, pin: u16, was: *u8, now: *u8) bool { + const f = host_gpio orelse return false; + return f(out_ctx, pin, was, now); +} /// The canonical surface -> the wire. Same shape as the tty shell's (`src/tty/tty.zig:1096`) minus /// the panel compositor and the kitty image path: neither has a reason to exist on a board with no diff --git a/src/pardes.zig b/src/pardes.zig index 9f20e0a3..4da64c3f 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -2418,6 +2418,64 @@ comptime { /// syntax, terminal ANSI palettes, and PDF tint colors deliberately are not /// here: those switch to `theme()` immediately while this small palette moves /// between themes over a handful of display frames. +/// WHAT THE BOARD BOOTS WITH. An empty buffer is honest and useless: the three words that make +/// this board interesting take an address, and a board's address space is precisely the thing you +/// cannot guess. So the buffer is a tour of it - every address below comes from this repository +/// rather than from memory, which is why they are worth trusting: the two flash figures and the two +/// RAM ones are the linker script's own ORIGINs (`05-zig-p4/build.zig`'s MEMORY block), and the +/// peripheral bases are `DR_REG_*` from ESP-IDF's headers as `05-zig-p4/src/hal` uses them. +/// +/// Each command sits alone on its line because an argument list ends at the last argument - a +/// trailing comment would be `ExtraArgument` - so the notes go above the lines they describe. Run +/// one by putting the cursor on it, `x` to select the line, Tab to execute. +/// +/// EVERY LINE IS SHORT ENOUGH TO RENDER WHOLE, which is asserted rather than eyeballed: see the +/// test below. A tour whose lines wrap is a worse first screen than no tour. +const boot_buffer = + \\x selects a line, Tab runs it. 0x optional. + \\ + \\-- flash: this image's rodata, then its code + \\Hexdump 40000020 40 + \\Hexdump 40050000 40 + \\-- L2MEM: firmware data, then the heap + \\Hexdump 4ff00000 40 + \\Hexdump 4ff40000 40 + \\-- the mask ROM + \\Hexdump 4fc00000 20 + \\-- UART0, the console you are reading on + \\Peek 500ca000 4 + \\-- LP_STORE0: write a word, read it back + \\Poke 5011002c deadbeef + \\Peek 5011002c + \\-- RNG_DATA: not memory. Run it twice. + \\Peek 501101a4 + \\Peek 501101a4 + \\-- the pins: Gpio draws JP1, Gpio 33 flips + \\Gpio + \\Gpio 33 +; + +// Three times in this port a line in that buffer has been one or two characters too long for the +// board's 56-column grid, and every time it was found by reading the die's screen rather than by +// reading the source - which is the expensive way to find a string literal's length. The bound is +// the grid minus the line-number gutter minus a column, and the margin below it is deliberate: +// pinning the exact gutter width would make this test a restatement of the renderer instead of a +// statement about the text. +test "every line of the board's boot buffer renders whole" { + const cols: usize = @import("pardes_config").p4_cols; + var it = std.mem.splitScalar(u8, boot_buffer, '\n'); + while (it.next()) |line| { + std.testing.expect(line.len + 8 <= cols) catch |err| { + std.debug.print("boot buffer line is {d} of {d} usable: \"{s}\"\n", .{ line.len, cols - 8, line }); + return err; + }; + } + // and the tour still visits what it says it visits + try std.testing.expect(std.mem.indexOf(u8, boot_buffer, "Hexdump 40000020") != null); + try std.testing.expect(std.mem.indexOf(u8, boot_buffer, "Poke 5011002c deadbeef") != null); + try std.testing.expect(std.mem.indexOf(u8, boot_buffer, "\nGpio\n") != null); +} + pub const ChromeTheme = struct { tag_bg: [3]u8, tag_fg: [3]u8, @@ -5785,26 +5843,7 @@ pub const Pardes = struct { // LP_SYSTEM_REG_RNG_DATA, the hardware random generator. Between them they demonstrate // the whole point of a volatile read: one address gives back what was written and the // other never gives the same answer twice. Both verified on this die. - const content = try p.gpa.dupe(u8, - \\x selects a line, Tab runs it. Hex, 0x optional. - \\ - \\-- flash: this image's rodata, then its code - \\Hexdump 40000020 40 - \\Hexdump 40050000 40 - \\-- L2MEM: firmware data, then the editor's heap - \\Hexdump 4ff00000 40 - \\Hexdump 4ff40000 40 - \\-- the mask ROM - \\Hexdump 4fc00000 20 - \\-- UART0, the console you are reading this on - \\Peek 500ca000 4 - \\-- LP_STORE0: write a word, then read it back - \\Poke 5011002c deadbeef - \\Peek 5011002c - \\-- RNG_DATA: a register is not memory. Run twice. - \\Peek 501101a4 - \\Peek 501101a4 - ); + const content = try p.gpa.dupe(u8, boot_buffer); errdefer p.gpa.free(content); _ = try output_pane.open(p, 0, "", .{ .cmd = .New }, "", content); p.ncol = 1; |
