summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/board_memory.zig191
-rw-r--r--src/builtins.zig18
-rw-r--r--src/config.zig2
-rw-r--r--src/host.zig12
-rw-r--r--src/p4.zig26
-rw-r--r--src/pardes.zig79
6 files changed, 276 insertions, 52 deletions
diff --git a/src/board_memory.zig b/src/board_memory.zig
index d2878d2d..79a9a2cf 100644
--- a/src/board_memory.zig
+++ b/src/board_memory.zig
@@ -1,13 +1,13 @@
-//! The board's own address space, as text: the Peek, Poke and Hexdump
-//! builtins' whole implementation.
+//! The board's own address space and its pins, as text: the Peek, Poke, Hexdump and Gpio 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.
+//! 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 `p4`.
//!
//! Everything here goes through `*allowzero volatile` pointers. A peripheral
//! register is not memory: reading UART_STATUS twice is two reads and must not
@@ -25,30 +25,33 @@ 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.
+/// THE ONE GATE, and it names the p4 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.
///
-/// 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();
+/// 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 `p4` excludes it by construction rather than by a term
+/// somebody has to keep remembering.
+pub const enabled = pardes.platform == .p4;
-// `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.
+// The target is now the WITNESS rather than the gate: whatever else `p4` 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 (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");
+ 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.
@@ -83,6 +86,11 @@ pub const Error = error{
/// 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
@@ -247,6 +255,49 @@ test "every literal is hex, with or without the prefix" {
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").p4_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`
@@ -296,6 +347,86 @@ pub fn poke(p: *Pardes, id: usize, argument: []const u8) !void {
) 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.
+///
+/// READ OFF THE VENDOR SCHEMATIC, sheet 2 "Expand IO"
+/// (`01-esp32p4-m3/docs/schematics/2_EXPAND_IO&BAT.png`), which is the only document that carries
+/// this mapping - the specification PDF's "Interface Description" page is a marketing render, and
+/// there is no board user guide. The sheet is a 872x1168 raster, so the assignment was taken from
+/// the drawing's own geometry rather than by eye: thirteen wires leave each side of the symbol, a
+/// net wire runs ~100 px to its label and a power stub ~21 px, which is what identifies pin 8 as
+/// unconnected rather than as the first of the GPIO4x labels. Cross-checked against a second,
+/// independent source: `05-zig-p4/build.zig` has always documented `-Dled=20` as "JP1 pin 17", and
+/// GPIO20 lands on pin 17 here.
+///
+/// `--` is a pin the header brings out with nothing behind it. `C6_*` are the ESP32-C6 companion's
+/// pads, not the P4's, and toggling a P4 GPIO cannot reach them. `ES_I2C_*` is the audio codec's
+/// bus, shared - driving either one by hand while the codec is live is a collision, which is a
+/// reason to know the pin is there rather than a reason to hide it.
+const pinout =
+ \\JP1 header - 26 pins, pin 1 top left.
+ \\Every number here is DECIMAL.
+ \\
+ \\ +---------+
+ \\ 3V3 | 1 | 2 | 5V
+ \\ 3V3 | 3 | 4 | 5V
+ \\ GND | 5 | 6 | GND
+ \\ GPIO 1 | 7 | 8 | --
+ \\ GPIO 2 | 9 | 10 | GPIO 47
+ \\ GPIO 3 | 11 | 12 | GPIO 46
+ \\ GPIO 4 | 13 | 14 | GPIO 45
+ \\ GPIO 5 | 15 | 16 | GND
+ \\ GPIO 20 | 17 | 18 | 3V3
+ \\ GPIO 32 | 19 | 20 | C6_U0RXD
+ \\ GPIO 33 | 21 | 22 | C6_U0TXD
+ \\ES_I2C_SDA | 23 | 24 | C6_IO9
+ \\ES_I2C_SCL | 25 | 26 | C6_CHIP_PU
+ \\ +---------+
+ \\
+ \\Gpio <pin> flips one: 0->1 or 1->0.
+ \\
+;
+
+/// `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.
///
/// `hexdump -C`'s sixteen is the layout everyone can already read, and it needs 79 columns: ten for
diff --git a/src/builtins.zig b/src/builtins.zig
index 0c9993a7..23ae18c3 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -985,3 +985,21 @@ pub const Hexdump = struct {
c.p.reportError(c.id, "hexdump", err);
}
};
+
+/// `Gpio <pin>` — flip one pad, answered on the message row as `0->1`. Bare `Gpio` draws JP1's
+/// pinout into a pane instead, because the first question about a header is which pins it has.
+///
+/// The only word here whose argument is DECIMAL, and `board_memory.gpio` says why at length: a
+/// GPIO number is part of a name, not an address.
+pub const Gpio = struct {
+ pub const takes_arg = true;
+ pub const enabled = board_memory.enabled;
+ pub const output: OutputTraits = .{ .name = config.gpio_buffer };
+ pub fn run(c: Ctx) void {
+ if (comptime enabled) apply(c) else unreachable;
+ }
+ fn apply(c: Ctx) void {
+ board_memory.gpio(c.p, c.id, c.arg orelse "") catch |err|
+ c.p.reportError(c.id, "gpio", err);
+ }
+};
diff --git a/src/config.zig b/src/config.zig
index c9d414ca..15a21866 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -235,6 +235,7 @@ pub const leader_path = paths: {
table.set(.Peek, null);
table.set(.Poke, null);
table.set(.Hexdump, null);
+ table.set(.Gpio, null);
}
// The pane-local PDF commands exist only in MuPDF builds through their
// explicit registry availability, so name their paths inside the same
@@ -799,6 +800,7 @@ pub const changelog_buffer = "+Changelog";
/// from every hosted build along with the builtins that name them.
pub const peek_buffer = "+Peek";
pub const hexdump_buffer = "+Hexdump";
+pub const gpio_buffer = "+Gpio";
/// The empty buffer New and Newcol open: no file behind it yet, so Save asks
/// for a path (prefilled with the inherited directory).
pub const scratch_buffer = "+New";
diff --git a/src/host.zig b/src/host.zig
index e44318da..9ca882b1 100644
--- a/src/host.zig
+++ b/src/host.zig
@@ -80,6 +80,18 @@ pub const Host = struct {
/// must not re-enter the core.
pull_tty_taken: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null,
+ // ---- the board's own pads ----
+ /// Flip one GPIO and report the level it held and the level it now holds. False means the
+ /// host would not do it: a pin number outside the part, or no pads at all.
+ ///
+ /// A pull, because there is one answer. The HOST answers it rather than the core reaching
+ /// for the registers itself - which `Peek` and `Poke` do two functions away - because
+ /// driving a pad correctly is not one register. It is the IO MUX function select, the GPIO
+ /// matrix output route, the pad's drive and input-buffer bits, and the output enable, keyed
+ /// by a per-pin table. The firmware already owns that code and checks it against ESP-IDF's
+ /// own headers on the die; a second copy in here would be a second copy nobody tests.
+ pull_gpio_toggle: ?*const fn (ctx: ?*anyopaque, pin: u16, was: *u8, now: *u8) bool = null,
+
// ---- the filesystem ----
/// `pane` travels with the bytes only so a host that posts a "saved"
/// message row can name the right pane; the core already resolved the
diff --git a/src/p4.zig b/src/p4.zig
index 05610eca..8e46b2ff 100644
--- a/src/p4.zig
+++ b/src/p4.zig
@@ -93,7 +93,9 @@ fn panicImpl(msg: []const u8, _: ?usize) noreturn {
// cheapest possible defence: the firmware calls it first and refuses to continue on a mismatch.
/// Bumped whenever any signature below changes, including a type.
-const abi_version: u32 = 1;
+/// 2 added `GpioFn` to `pardes_p4_init`. A firmware built against 1 passes five arguments where six
+/// are read, which is exactly the silent-corruption case this counter exists to turn into a message.
+const abi_version: u32 = 2;
export fn pardes_p4_abi_version() callconv(.c) u32 {
return abi_version;
@@ -115,6 +117,15 @@ pub const Allocator = extern struct {
/// How finished runs of ANSI leave this object.
pub const WriteFn = *const fn (ctx: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void;
+/// Flip one pad and report the level before and after; false if the firmware declines. OPTIONAL on
+/// the wire, so a host with no pads (or one that has not implemented them yet) passes null and the
+/// `Gpio` word answers "no pads" instead of the object having to know which firmwares exist.
+///
+/// The board's side, not the editor's, because a correct toggle is the IO MUX, the GPIO matrix, the
+/// pad's own bits and the output enable - four register files behind a per-pin table that the
+/// firmware already has and checks against ESP-IDF. See `Host.VTable.pull_gpio_toggle`.
+pub const GpioFn = *const fn (ctx: ?*anyopaque, pin: u16, was: *u8, now: *u8) callconv(.c) bool;
+
// ------------------------------------------------------------------- the allocator, rebuilt
// One `std.mem.Allocator` whose vtable forwards to the four pointers above. The indirection is the
// price of the seam and it is paid once per allocation, which on a first-fit heap is already the
@@ -158,6 +169,7 @@ fn gpa() std.mem.Allocator {
var out_write: WriteFn = undefined;
var out_ctx: ?*anyopaque = null;
+var host_gpio: ?GpioFn = null;
var out_buf: [8192]u8 = undefined;
var out: std.Io.Writer = undefined;
@@ -247,12 +259,14 @@ fn vaxisSize() vaxis.Winsize {
export fn pardes_p4_init(
alloc: *const Allocator,
write: WriteFn,
+ gpio: ?GpioFn,
ctx: ?*anyopaque,
cols: u16,
rows: u16,
) callconv(.c) u32 {
host_alloc = alloc.*;
out_write = write;
+ host_gpio = gpio;
out_ctx = ctx;
out = .{ .vtable = &.{ .drain = drain }, .buffer = &out_buf };
@@ -594,7 +608,15 @@ export fn pardes_p4_quit() callconv(.c) bool {
// ------------------------------------------------------------------------------------ the host
-const pardes_host: pardes.Host.VTable = .{ .push_present = present };
+const pardes_host: pardes.Host.VTable = .{ .push_present = present, .pull_gpio_toggle = gpioToggle };
+
+/// The `Gpio` word's one seam to the board. Nothing here knows what a pad is; it forwards, and
+/// answers false when the firmware brought none, which is what puts "gpio: NoPads" on the message
+/// row rather than a trap.
+fn gpioToggle(_: ?*anyopaque, pin: u16, was: *u8, now: *u8) bool {
+ const f = host_gpio orelse return false;
+ return f(out_ctx, pin, was, now);
+}
/// The canonical surface -> the wire. Same shape as the tty shell's (`src/tty/tty.zig:1096`) minus
/// the panel compositor and the kitty image path: neither has a reason to exist on a board with no
diff --git a/src/pardes.zig b/src/pardes.zig
index 9f20e0a3..4da64c3f 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -2418,6 +2418,64 @@ comptime {
/// syntax, terminal ANSI palettes, and PDF tint colors deliberately are not
/// here: those switch to `theme()` immediately while this small palette moves
/// between themes over a handful of display frames.
+/// WHAT THE BOARD BOOTS WITH. An empty buffer is honest and useless: the three words that make
+/// this board interesting take an address, and a board's address space is precisely the thing you
+/// cannot guess. So the buffer is a tour of it - every address below comes from this repository
+/// rather than from memory, which is why they are worth trusting: the two flash figures and the two
+/// RAM ones are the linker script's own ORIGINs (`05-zig-p4/build.zig`'s MEMORY block), and the
+/// peripheral bases are `DR_REG_*` from ESP-IDF's headers as `05-zig-p4/src/hal` uses them.
+///
+/// Each command sits alone on its line because an argument list ends at the last argument - a
+/// trailing comment would be `ExtraArgument` - so the notes go above the lines they describe. Run
+/// one by putting the cursor on it, `x` to select the line, Tab to execute.
+///
+/// EVERY LINE IS SHORT ENOUGH TO RENDER WHOLE, which is asserted rather than eyeballed: see the
+/// test below. A tour whose lines wrap is a worse first screen than no tour.
+const boot_buffer =
+ \\x selects a line, Tab runs it. 0x optional.
+ \\
+ \\-- flash: this image's rodata, then its code
+ \\Hexdump 40000020 40
+ \\Hexdump 40050000 40
+ \\-- L2MEM: firmware data, then the heap
+ \\Hexdump 4ff00000 40
+ \\Hexdump 4ff40000 40
+ \\-- the mask ROM
+ \\Hexdump 4fc00000 20
+ \\-- UART0, the console you are reading on
+ \\Peek 500ca000 4
+ \\-- LP_STORE0: write a word, read it back
+ \\Poke 5011002c deadbeef
+ \\Peek 5011002c
+ \\-- RNG_DATA: not memory. Run it twice.
+ \\Peek 501101a4
+ \\Peek 501101a4
+ \\-- the pins: Gpio draws JP1, Gpio 33 flips
+ \\Gpio
+ \\Gpio 33
+;
+
+// Three times in this port a line in that buffer has been one or two characters too long for the
+// board's 56-column grid, and every time it was found by reading the die's screen rather than by
+// reading the source - which is the expensive way to find a string literal's length. The bound is
+// the grid minus the line-number gutter minus a column, and the margin below it is deliberate:
+// pinning the exact gutter width would make this test a restatement of the renderer instead of a
+// statement about the text.
+test "every line of the board's boot buffer renders whole" {
+ const cols: usize = @import("pardes_config").p4_cols;
+ var it = std.mem.splitScalar(u8, boot_buffer, '\n');
+ while (it.next()) |line| {
+ std.testing.expect(line.len + 8 <= cols) catch |err| {
+ std.debug.print("boot buffer line is {d} of {d} usable: \"{s}\"\n", .{ line.len, cols - 8, line });
+ return err;
+ };
+ }
+ // and the tour still visits what it says it visits
+ try std.testing.expect(std.mem.indexOf(u8, boot_buffer, "Hexdump 40000020") != null);
+ try std.testing.expect(std.mem.indexOf(u8, boot_buffer, "Poke 5011002c deadbeef") != null);
+ try std.testing.expect(std.mem.indexOf(u8, boot_buffer, "\nGpio\n") != null);
+}
+
pub const ChromeTheme = struct {
tag_bg: [3]u8,
tag_fg: [3]u8,
@@ -5785,26 +5843,7 @@ pub const Pardes = struct {
// LP_SYSTEM_REG_RNG_DATA, the hardware random generator. Between them they demonstrate
// the whole point of a volatile read: one address gives back what was written and the
// other never gives the same answer twice. Both verified on this die.
- const content = try p.gpa.dupe(u8,
- \\x selects a line, Tab runs it. Hex, 0x optional.
- \\
- \\-- flash: this image's rodata, then its code
- \\Hexdump 40000020 40
- \\Hexdump 40050000 40
- \\-- L2MEM: firmware data, then the editor's heap
- \\Hexdump 4ff00000 40
- \\Hexdump 4ff40000 40
- \\-- the mask ROM
- \\Hexdump 4fc00000 20
- \\-- UART0, the console you are reading this on
- \\Peek 500ca000 4
- \\-- LP_STORE0: write a word, then read it back
- \\Poke 5011002c deadbeef
- \\Peek 5011002c
- \\-- RNG_DATA: a register is not memory. Run twice.
- \\Peek 501101a4
- \\Peek 501101a4
- );
+ const content = try p.gpa.dupe(u8, boot_buffer);
errdefer p.gpa.free(content);
_ = try output_pane.open(p, 0, "", .{ .cmd = .New }, "", content);
p.ncol = 1;