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.zig191
1 files changed, 161 insertions, 30 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