//! 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 [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 ` — 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 [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); }