diff options
Diffstat (limited to 'src/board_memory.zig')
| -rw-r--r-- | src/board_memory.zig | 191 |
1 files changed, 161 insertions, 30 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 |
