summaryrefslogtreecommitdiff
path: root/src/board_memory.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/board_memory.zig')
-rw-r--r--src/board_memory.zig465
1 files changed, 0 insertions, 465 deletions
diff --git a/src/board_memory.zig b/src/board_memory.zig
deleted file mode 100644
index 4bee9e6e..00000000
--- a/src/board_memory.zig
+++ /dev/null
@@ -1,465 +0,0 @@
-//! The board's own address space and its pins, as text: the Peek, Poke, Hexdump and Gpio builtins'
-//! whole implementation.
-//!
-//! 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 `esp32p4`.
-//!
-//! Everything here goes through `*allowzero volatile` pointers. A peripheral
-//! register is not memory: reading UART_STATUS twice is two reads and must not
-//! be folded into one, a write to a write-only command register has no
-//! observable value for the optimizer to keep, and address 0 is an ordinary
-//! (unmapped) address on this bus rather than the null Zig assumes it is.
-//!
-//! The formatting side is a plain renderer over `Pardes.gpa`, so it lands in
-//! an output buffer the same way Jumplist and Config do: an output buffer is a
-//! file pane, so every motion, chord and Look works on a dump for free — you
-//! can right-click an address in a hexdump row and Peek it.
-const std = @import("std");
-const builtin = @import("builtin");
-const pardes = @import("pardes.zig");
-const Pardes = pardes.Pardes;
-const output_pane = @import("output_pane.zig");
-const limits = @import("limits.zig");
-
-/// THE ONE GATE, and it names the esp32p4 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.
-///
-/// 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 `esp32p4` excludes it by construction rather than by a term
-/// somebody has to keep remembering.
-pub const enabled = pardes.platform == .esp32p4;
-
-// The target is now the WITNESS rather than the gate: whatever else `esp32p4` 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 (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.
-///
-/// The number is set by the console, not by the memory: UART0 runs at 115200
-/// baud and measures ~11.9 KB/s on the wire, and a hexdump row is 76 bytes of
-/// text per 16 bytes of memory. 4 KiB is therefore 256 rows and ~19.5 KiB of
-/// text — under two seconds to paint the whole buffer, and ~4% of the 512 KiB
-/// heap the firmware hands over. `Hexdump 0x0 0xffffffff` would otherwise wedge
-/// the only console the board has for eleven hours, with no way to interrupt
-/// it, which makes an unbounded dump not a slow command but a lost session.
-///
-/// Peek's cap is the same 4 KiB window expressed in words, so `Peek a 1024`
-/// and `Hexdump a 4096` cover exactly the same bytes.
-pub const max_bytes: u32 = 4096;
-pub const max_words: u32 = max_bytes / 4;
-
-/// One address past the last: the reads below are bounded by this rather than
-/// wrapping, because `Hexdump 0xfffffff0 256` wrapping to 0 would silently
-/// show you the bottom of the space labelled with top-of-space addresses.
-const space: u64 = 1 << 32;
-
-pub const Error = error{
- MissingAddress,
- BadAddress,
- BadCount,
- MissingValue,
- BadValue,
- /// the ONE fault this file exists to prevent by hand: the RISC-V core
- /// traps an unaligned 32-bit access, and a trap in firmware with no
- /// handler is a watchdog reset that takes the session with it. Reported on
- /// 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
-/// write a decimal one.
-///
-/// This replaces base-0 parsing, which accepted `0x4ff40000` and `1341390848` and refused a bare
-/// `4ff40000` on the grounds that guessing between hex and decimal would make one typo address
-/// somewhere else entirely. That reasoning was sound and the conclusion was still wrong: the
-/// ambiguity it protected against is not a real one. Every address anybody has ever typed at these
-/// three words is hex - it came off a datasheet, a linker map, or a previous dump's own output, all
-/// of which print hex - so the base was never in doubt, and demanding `0x` on every one of them was
-/// a toll on the common case to guard a case that does not arise.
-///
-/// The COUNTS go with them, and that is the part worth stating out loud rather than leaving as a
-/// surprise: `Hexdump 4ff40000 100` shows 0x100 bytes, which is 256, not one hundred. One rule for
-/// every literal in the word is worth more than two rules that each fit their argument better,
-/// because the second kind is the sort of thing you have to remember at the moment you are already
-/// concentrating on something else. Everything these words PRINT is hex too, including the clamp
-/// notes, so a number can go back in where it came out.
-fn parseHex(comptime T: type, tok: []const u8, bad: Error) Error!T {
- // `parseInt` only honours an `0x` when its base is 0, so with base 16 the prefix has to come off
- // here. A bare `0x` leaves nothing behind and `parseInt` rejects the empty string, which is the
- // answer that wants giving.
- const body = if (tok.len > 2 and tok[0] == '0' and (tok[1] | 0x20) == 'x') tok[2..] else tok;
- return std.fmt.parseInt(T, body, 16) catch bad;
-}
-
-fn parseAddr(tok: []const u8) Error!u32 {
- return parseHex(u32, tok, Error.BadAddress);
-}
-
-fn parseCount(tok: []const u8) Error!u64 {
- return parseHex(u64, tok, Error.BadCount);
-}
-
-fn parseValue(tok: []const u8) Error!u32 {
- return parseHex(u32, tok, Error.BadValue);
-}
-
-/// A 32-bit peripheral or RAM read that the compiler may neither elide,
-/// duplicate, reorder past another access, nor narrow.
-fn readWord(addr: u32) u32 {
- const cell: *allowzero const volatile u32 = @ptrFromInt(@as(usize, addr));
- return cell.*;
-}
-
-fn writeWord(addr: u32, value: u32) void {
- const cell: *allowzero volatile u32 = @ptrFromInt(@as(usize, addr));
- cell.* = value;
-}
-
-fn readByte(addr: u32) u8 {
- const cell: *allowzero const volatile u8 = @ptrFromInt(@as(usize, addr));
- return cell.*;
-}
-
-const Limit = enum {
- /// the 4 KiB console cap above
- console,
- /// the end of the 32-bit address space
- space,
-};
-
-/// How many units this command will actually show, and WHY that is fewer than
-/// you asked for when it is. Never silent: the note below becomes the buffer's
-/// FIRST line, which is the one place a clamp cannot be missed — a trailing
-/// note on a 256-row dump is a note you scroll past.
-const Extent = struct {
- count: u32,
- /// the tighter of the two bounds, or null when neither applied
- limit: ?Limit,
-};
-
-fn extent(addr: u32, requested: u64, unit: u32, cap: u32) Extent {
- var count = requested;
- var limit: ?Limit = null;
- if (count > cap) {
- count = cap;
- limit = .console;
- }
- const fits = (space - addr) / unit;
- if (count > fits) {
- count = fits;
- limit = .space;
- }
- return .{ .count = @intCast(count), .limit = limit };
-}
-
-fn writeNote(w: *std.Io.Writer, e: Extent, requested: u64, unit_name: []const u8) !void {
- switch (e.limit orelse return) {
- // Hex, like everything else these words read and print, so the number in a clamp note can go
- // straight back into the command that produced it.
- .console => try w.print(
- "clamped: 0x{x} {s} requested, 0x{x} shown (0x{x}-byte cap, one 115200-baud console)\n",
- .{ requested, unit_name, e.count, max_bytes },
- ),
- .space => try w.print(
- "clamped: 0x{x} {s} requested, 0x{x} shown (the 32-bit address space ends at 0x100000000)\n",
- .{ requested, unit_name, e.count },
- ),
- }
-}
-
-// The two bounds and their reporting, on the one part of this file that is
-// pure arithmetic and therefore testable on any target — the accesses
-// themselves are only meaningful on the board.
-test "the clamp reports the tighter bound and never wraps the address space" {
- const eq = std.testing.expectEqual;
- // neither bound applied: what you asked for, and nothing to report
- try eq(Extent{ .count = 3, .limit = null }, extent(0x4ff40000, 3, 4, max_words));
- // the console cap, in words and in bytes
- try eq(Extent{ .count = max_words, .limit = .console }, extent(0x4ff40000, 99_999, 4, max_words));
- try eq(Extent{ .count = max_bytes, .limit = .console }, extent(0, 100_000, 1, max_bytes));
- // sixteen bytes left above 0xfffffff0 — the whole point, because wrapping
- // would show the BOTTOM of the space under top-of-space addresses
- try eq(Extent{ .count = 16, .limit = .space }, extent(0xfffffff0, 64, 1, max_bytes));
- try eq(Extent{ .count = 4, .limit = .space }, extent(0xfffffff0, 64, 4, max_words));
- // ...including the row that has no whole word left in it
- try eq(Extent{ .count = 0, .limit = .space }, extent(0xffffffff, 1, 4, max_words));
- // both bounds at once: the tighter one is the one reported
- try eq(Extent{ .count = max_bytes, .limit = .console }, extent(0xffff0000, 1 << 20, 1, max_bytes));
-}
-
-test "a clamp note is written exactly when something was clamped" {
- var buf: [256]u8 = undefined;
- var w: std.Io.Writer = .fixed(&buf);
-
- try writeNote(&w, extent(0x4ff40000, 3, 4, max_words), 3, "words");
- try std.testing.expectEqualStrings("", w.buffered());
-
- try writeNote(&w, extent(0x4ff40000, 99_999, 4, max_words), 99_999, "words");
- try std.testing.expectEqualStrings(
- "clamped: 0x1869f words requested, 0x400 shown (0x1000-byte cap, one 115200-baud console)\n",
- w.buffered(),
- );
-
- w = .fixed(&buf);
- try writeNote(&w, extent(0xfffffff0, 64, 1, max_bytes), 64, "bytes");
- try std.testing.expectEqualStrings(
- "clamped: 0x40 bytes requested, 0x10 shown (the 32-bit address space ends at 0x100000000)\n",
- w.buffered(),
- );
-}
-
-test "every literal is hex, with or without the prefix" {
- const eq = std.testing.expectEqual;
- // the prefix is optional, never required, and never changes the answer
- try eq(0x4ff40000, parseAddr("0x4ff40000"));
- try eq(0x4ff40000, parseAddr("4ff40000"));
- try eq(0x4ff40000, parseAddr("0X4FF40000"));
- try eq(0x4ff40000, parseAddr("4FF40000"));
- // a token that looks decimal is hex too - the whole point, and the thing to remember
- try eq(0x100, parseCount("100"));
- try eq(0x256, parseCount("256"));
- try eq(0xdeadbeef, parseValue("deadbeef"));
- // and the refusals still refuse
- try std.testing.expectError(Error.BadAddress, parseAddr("0x100000000"));
- try std.testing.expectError(Error.BadAddress, parseAddr("0x"));
- try std.testing.expectError(Error.BadAddress, parseAddr("nope"));
- try std.testing.expectError(Error.BadAddress, parseAddr("12g4"));
- try std.testing.expectError(Error.BadCount, parseCount("-1"));
- 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").esp32p4_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`
-/// chorded onto one re-reads or writes exactly that word.
-pub fn peek(p: *Pardes, id: usize, argument: []const u8) !void {
- var it = std.mem.tokenizeAny(u8, argument, " \t\r\n");
- const addr = try parseAddr(it.next() orelse return Error.MissingAddress);
- const requested = if (it.next()) |tok| try parseCount(tok) else 0x1;
- if (it.next() != null) return Error.ExtraArgument;
- if (addr % 4 != 0) return Error.MisalignedAddress;
-
- const e = extent(addr, requested, 4, max_words);
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- try writeNote(&out.writer, e, requested, "words");
- for (0..e.count) |i| {
- const at = addr + @as(u32, @intCast(i * 4));
- try out.writer.print("{x:0>8}: {x:0>8}\n", .{ at, readWord(at) });
- }
- const content = try out.toOwnedSlice();
- try fill(p, id, .{ .cmd = .Peek }, content);
-}
-
-/// `Poke <addr> <value>` — one 32-bit store, then one load back, both reported
-/// on the message row.
-///
-/// The READ-BACK is the whole point of the word and not a confirmation: on RAM
-/// it always equals what you wrote and tells you nothing, and on MMIO it
-/// almost never does — a write-only command register reads as 0, a W1C status
-/// bit reads back cleared, a reserved field reads back masked, and a register
-/// behind a gated clock reads back whatever the bus returns for nothing at
-/// all. Printing only the value written would show you your own argument.
-pub fn poke(p: *Pardes, id: usize, argument: []const u8) !void {
- var it = std.mem.tokenizeAny(u8, argument, " \t\r\n");
- const addr = try parseAddr(it.next() orelse return Error.MissingAddress);
- const value = try parseValue(it.next() orelse return Error.MissingValue);
- if (it.next() != null) return Error.ExtraArgument;
- if (addr % 4 != 0) return Error.MisalignedAddress;
-
- writeWord(addr, value);
- const back = readWord(addr);
- var buf: [96]u8 = undefined;
- p.setMessage(id, std.fmt.bufPrint(
- &buf,
- "{x:0>8}: wrote {x:0>8}, reads {x:0>8}",
- .{ addr, value, back },
- ) 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.
-///
-/// MOVED TO `src/board_pins.zig`, where the thirteen rows are DATA and this drawing is rendered
-/// from them at comptime. Not for tidiness: the board's 9P image (`src/esp32p4_9p.zig`) links no
-/// core, so it cannot import this file — this one imports `pardes.zig` — and that image serves this
-/// exact drawing as `gpio/pinout` while generating its per-pin directories from the same rows. The
-/// alternative was transcribing a schematic twice, which is two things to maintain and no test that
-/// could say which one was wrong. The provenance moved with the rows: which sheet of which
-/// schematic, how pin 8 was identified as unconnected, and what `--`, `C6_*` and `ES_I2C_*` mean.
-///
-/// The test below is unchanged, and it is still the check that matters HERE: whoever renders this
-/// drawing, the `Gpio` word's output has to fit the board's own grid in two aligned columns.
-const pinout = @import("board_pins.zig").jp1_text;
-
-/// `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 — see
-/// `limits.hexdump_row_bytes`, which is where that number and its reasoning
-/// live now.
-const row_bytes: u32 = limits.hexdump_row_bytes;
-
-/// `Hexdump <addr> [len]` — len bytes, `row_bytes` to a row, hex columns and an ASCII gutter, in
-/// `hexdump -C`'s layout because that is the one everyone can already read. BYTE reads, so a partial
-/// row at the end of the space is a short row rather than a refusal, and no alignment is required:
-/// this is the word you reach for when you do not yet know what is there.
-pub fn hexdump(p: *Pardes, id: usize, argument: []const u8) !void {
- var it = std.mem.tokenizeAny(u8, argument, " \t\r\n");
- const addr = try parseAddr(it.next() orelse return Error.MissingAddress);
- const requested = if (it.next()) |tok| try parseCount(tok) else 0x100;
- if (it.next() != null) return Error.ExtraArgument;
-
- const e = extent(addr, requested, 1, max_bytes);
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- try writeNote(&out.writer, e, requested, "bytes");
- var row: u32 = 0;
- while (row < e.count) : (row += row_bytes) {
- const n = @min(row_bytes, e.count - row);
- var bytes: [row_bytes]u8 = undefined;
- for (0..n) |i| bytes[i] = readByte(addr + row + @as(u32, @intCast(i)));
- try out.writer.print("{x:0>8} ", .{addr + row});
- for (0..row_bytes) |i| {
- // The gap at the halfway mark: the eye counts to four or eight, not to sixteen.
- if (i == row_bytes / 2) try out.writer.writeByte(' ');
- if (i < n)
- try out.writer.print(" {x:0>2}", .{bytes[i]})
- else
- try out.writer.writeAll(" ");
- }
- try out.writer.writeAll(" |");
- for (0..n) |i| try out.writer.writeByte(
- if (bytes[i] >= 0x20 and bytes[i] < 0x7f) bytes[i] else '.',
- );
- try out.writer.writeAll("|\n");
- }
- const content = try out.toOwnedSlice();
- try fill(p, id, .{ .cmd = .Hexdump }, content);
-}
-
-/// The shared tail. `fillResults` is the one public entry that REFILLS the
-/// buffer a command already opened instead of stacking a twin beside it, which
-/// is what a dump wants: peeking twenty addresses in a row is twenty renders
-/// of one window on memory, not twenty panes. The empty argument is what makes
-/// it one window — a dump is identified by the command, never by the address,
-/// so a second Peek replaces the first rather than opening a buffer per
-/// address and exhausting the pane slots.
-///
-/// Neither buffer `steps`, so nothing is armed on n/N and focus stays in the
-/// pane you typed the command in. `content` is gpa-owned and adopted there.
-fn fill(p: *Pardes, id: usize, from: output_pane.Origin, content: []u8) !void {
- const pane = p.panes[id] orelse {
- p.gpa.free(content);
- return error.MissingPane;
- };
- const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
- try output_pane.fillResults(p, id, dir, from, "", content, null);
-}