diff options
Diffstat (limited to 'src/board_memory.zig')
| -rw-r--r-- | src/board_memory.zig | 465 |
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); -} |
