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.zig323
1 files changed, 323 insertions, 0 deletions
diff --git a/src/board_memory.zig b/src/board_memory.zig
new file mode 100644
index 00000000..853ae5da
--- /dev/null
+++ b/src/board_memory.zig
@@ -0,0 +1,323 @@
+//! The board's own address space, as text: the Peek, Poke and Hexdump
+//! 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.
+//!
+//! 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");
+
+/// 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.
+///
+/// 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();
+
+// `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.
+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");
+}
+
+/// 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,
+};
+
+/// hex (`0x4ff40000`), decimal (`1341718528`), and — for free, from base 0 —
+/// binary and octal. A bare `4ff40000` is deliberately NOT hex: it is a
+/// legal-looking decimal number, so guessing the base would make one typo
+/// silently address somewhere else entirely.
+fn parseAddr(tok: []const u8) Error!u32 {
+ return std.fmt.parseInt(u32, tok, 0) catch return Error.BadAddress;
+}
+
+fn parseCount(tok: []const u8) Error!u64 {
+ return std.fmt.parseInt(u64, tok, 0) catch return Error.BadCount;
+}
+
+fn parseValue(tok: []const u8) Error!u32 {
+ return std.fmt.parseInt(u32, tok, 0) catch return 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) {
+ .console => try w.print(
+ "clamped: {d} {s} requested, {d} shown ({d}-byte cap, one 115200-baud console)\n",
+ .{ requested, unit_name, e.count, max_bytes },
+ ),
+ .space => try w.print(
+ "clamped: {d} {s} requested, {d} 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: 99999 words requested, 1024 shown (4096-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: 64 bytes requested, 16 shown (the 32-bit address space ends at 0x100000000)\n",
+ w.buffered(),
+ );
+}
+
+test "an address is hex or decimal, and a bare hex-looking token is decimal" {
+ try std.testing.expectEqual(0x4ff40000, parseAddr("0x4ff40000"));
+ try std.testing.expectEqual(0x4ff40000, parseAddr("1341390848"));
+ // a bare hex-looking token is a decimal number, never a guess
+ try std.testing.expectError(Error.BadAddress, parseAddr("4ff40000"));
+ try std.testing.expectError(Error.BadAddress, parseAddr("0x100000000"));
+ try std.testing.expectError(Error.BadCount, parseCount("-1"));
+ try std.testing.expectError(Error.BadValue, parseValue("0x1_0000_0000"));
+}
+
+/// `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 1;
+ 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("0x{x:0>8}: 0x{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,
+ "0x{x:0>8}: wrote 0x{x:0>8}, reads 0x{x:0>8}",
+ .{ addr, value, back },
+ ) catch unreachable);
+}
+
+/// `Hexdump <addr> [len]` — len bytes, 16 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 256;
+ 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 += 16) {
+ const n = @min(@as(u32, 16), e.count - row);
+ var bytes: [16]u8 = undefined;
+ for (0..n) |i| bytes[i] = readByte(addr + row + @as(u32, @intCast(i)));
+ try out.writer.print("0x{x:0>8} ", .{addr + row});
+ for (0..16) |i| {
+ // hexdump -C's gap after the eighth column: the eye counts to
+ // eight, not to sixteen
+ if (i == 8) 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);
+}