//! 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, }; /// 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")); } /// `Peek [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("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 ` — 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); } /// 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 /// the address, forty-eight for the hex, a gap, and the eighteen-column ASCII gutter. The P4 drives /// a 56-column grid of which seven go to the line-number gutter, so a sixteen-byte row wraps onto a /// second display line and the columns stop lining up - which is the entire value of the layout. /// /// Eight fits in 46 and keeps every property that matters: address on the left, fixed-width hex /// columns, ASCII on the right, and a gap at the halfway mark because the eye counts in fours and /// eights rather than in sixteens. const row_bytes: u32 = if (pardes.platform == .p4) 8 else 16; /// `Hexdump [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("0x{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); }