summaryrefslogtreecommitdiff
path: root/examples/memprobe.zig
diff options
context:
space:
mode:
Diffstat (limited to 'examples/memprobe.zig')
-rw-r--r--examples/memprobe.zig215
1 files changed, 215 insertions, 0 deletions
diff --git a/examples/memprobe.zig b/examples/memprobe.zig
new file mode 100644
index 0000000..1ce8a0b
--- /dev/null
+++ b/examples/memprobe.zig
@@ -0,0 +1,215 @@
+//! What RAM does this board actually have, and where?
+//!
+//! The linker script maps one 128 KiB window at 0x4FF00000 and has never needed more. Hosting a
+//! real application needs an answer with more than one digit in it, and the answer cannot be read
+//! off ESP-IDF's linker fragments, because the fragment that matters
+//! (`esp_system/ld/esp32p4/memory.ld.in:18-33`) is parameterised on two things this image does not
+//! have:
+//!
+//! * `CONFIG_ESP32P4_SELECTS_REV_LESS_V3` - true for this rev v1.3 die, which selects a SPLIT
+//! layout: a low region 0x4FF00000..0x4FF2BBD0 and a high region from 0x4FF40000, with the
+//! mask ROM's own .data/.bss in between at 0x4FF3FBA4..0x4FF40000.
+//! * `CONFIG_CACHE_L2_CACHE_SIZE` - the L2 cache is carved out of the SAME 768 KiB array, from
+//! the TOP, so `SRAM_HIGH_SIZE = 0x80000 - cache_size`. Its Kconfig default is 128 KiB, but the
+//! help text says "to be set on application startup" - the APPLICATION sets it, and this
+//! application does not. So the live size is whatever the ROM and the second-stage bootloader
+//! left behind, which is exactly the sort of thing that has to be measured.
+//!
+//! So: probe. For every 4 KiB page in the array, save the first word, write a value derived from
+//! the page's own address, read it back, and restore. An address-derived pattern is the point - a
+//! constant cannot distinguish real memory from an alias, and aliasing is the specific failure mode
+//! of poking at a region the cache controller owns. A page that reads back what it was given is
+//! RAM; anything else is reported with what it actually returned.
+//!
+//! Two pages are never touched: the one holding this image's own .data/.bss/stack, and the mask
+//! ROM's reserved window - `soc.rom.print` is the only way this program can report anything, and
+//! corrupting the ROM's statics would take the console down with it.
+//!
+//! A page that is neither RAM nor decoded may raise a bus fault, and this image has no trap
+//! handler, so a fault is a silent hang. That is why the scan prints its cursor as it goes: if this
+//! stops, the last address printed is the one that killed it, which is itself the result.
+
+const std = @import("std");
+const soc = @import("soc");
+
+/// The whole L2MEM array, per `soc/esp32p4/include/soc/soc.h:161-164`
+/// (SOC_DRAM_LOW 0x4ff00000, SOC_DRAM_HIGH 0x4ffc0000).
+const l2mem_low: u32 = 0x4FF0_0000;
+const l2mem_high: u32 = 0x4FFC_0000;
+
+/// The mask ROM's .data/.bss, from `bootloader.memory.ld.in:13-16`. Not reclaimable while anything
+/// still calls into the ROM, and `soc.rom.print` does.
+const rom_data_low: u32 = 0x4FF3_FBA4;
+const rom_data_high: u32 = 0x4FF4_0000;
+
+/// Where PSRAM appears once a driver has trained it (`soc.h:151-153`). Nothing here trains it, so
+/// this is expected to fail; it is probed anyway because the cost is four instructions and the
+/// alternative is assuming.
+const psram_base: u32 = 0x4800_0000;
+
+const page: u32 = 0x1000;
+
+/// This image's own footprint, from the linker script's symbols. `.data` starts at the region base
+/// and `__stack_top` is the last thing in it, so [l2mem_low, __stack_top) is off limits.
+extern const __stack_top: anyopaque;
+
+fn selfEnd() u32 {
+ return @intFromPtr(&__stack_top);
+}
+
+/// The heap span the linker script hands over (`build.zig`'s MEMORY block defines both from
+/// `l2high`). Referenced here so the report states the same numbers the linker will give the real
+/// application, rather than a second copy of them written down in Zig.
+extern const __heap_start: anyopaque;
+extern const __heap_end: anyopaque;
+
+/// The value page `addr` must return if it is real, distinct memory.
+inline fn pattern(addr: u32) u32 {
+ // Not `addr` itself: an address bus stuck high would return something that looks plausible.
+ // XOR with a constant that has bits set where an address never does.
+ return addr ^ 0xA5A5_0F0F;
+}
+
+/// One saved word per page, so the array can be written whole and read back whole.
+const max_pages = (l2mem_high - l2mem_low) / page;
+var saved: [max_pages]u32 = @splat(0);
+
+/// Is this page one the program refuses to touch?
+fn skipped(addr: u32) bool {
+ return (addr < selfEnd()) or (addr + page > rom_data_low and addr < rom_data_high);
+}
+
+/// WHY THIS IS TWO PASSES, and not a save/write/read/restore per page.
+///
+/// The per-page version is what this file did first, and it cannot distinguish the three things it
+/// most needs to: a store immediately followed by a load of the SAME address returns the stored
+/// value under real distinct SRAM, under an address mirror, and under a dirty line in any cache
+/// covering L2MEM. The pattern being address-derived does not help, because the alias is written and
+/// read through the alias. Cache residency is not excluded by scan length either: 192 pages touch
+/// one cache line each, ~12 KiB in total, which fits in any plausible L1 and so is never evicted.
+///
+/// Writing every page before reading any page fixes both. If 0x4FF80000 mirrors 0x4FF00000, the
+/// later write lands on the earlier page and the read pass sees the WRONG pattern at one of them.
+/// The distance between the two passes is 192 pages of traffic, which no L1 holds.
+///
+/// This matters more than a tidier loop: `__heap_end` hands the upper span straight to an allocator,
+/// so a mirror reported as RAM is silent heap corruption.
+fn writePass() void {
+ var addr = l2mem_low;
+ while (addr < l2mem_high) : (addr += page) {
+ if (skipped(addr)) continue;
+ const p: *volatile u32 = @ptrFromInt(addr);
+ saved[(addr - l2mem_low) / page] = p.*;
+ p.* = pattern(addr);
+ }
+}
+
+/// Read every page back, then put the original word back. Restoring in the same pass is safe: the
+/// comparison for this page is already done, and a mirror has by now already been detected at
+/// whichever of the two aliases was read second.
+fn readPass(addr: u32) Kind {
+ if (skipped(addr)) return .skipped;
+ const p: *volatile u32 = @ptrFromInt(addr);
+ const got = p.*;
+ p.* = saved[(addr - l2mem_low) / page];
+ return if (got == pattern(addr)) .ram else .dead;
+}
+
+/// What a page turned out to be. Three outcomes, not two: a page this program refuses to write is
+/// neither RAM nor dead, and folding "skipped" into "dead" is what made the first run of this
+/// report `dead 0x00000000..0x4ff02000`, a range that does not exist.
+const Kind = enum {
+ ram,
+ dead,
+ skipped,
+
+ fn label(k: Kind) [*:0]const u8 {
+ return switch (k) {
+ .ram => "ram",
+ .dead => "dead",
+ .skipped => "skipped",
+ };
+ }
+};
+
+/// Report a maximal run of pages that all behaved the same way.
+fn flush(kind: Kind, start: u32, end: u32) void {
+ if (end <= start) return;
+ soc.rom.print("MARK MEM_RANGE %s 0x%08x..0x%08x %u KiB\r\n", .{
+ kind.label(), start, end, (end - start) / 1024,
+ });
+}
+
+export fn zig_main() noreturn {
+ soc.rom.print("\r\nMARK MEM_BOOT probing L2MEM 0x%08x..0x%08x\r\n", .{ l2mem_low, l2mem_high });
+ soc.rom.print("MARK MEM_SELF image occupies 0x%08x..0x%08x\r\n", .{ l2mem_low, selfEnd() });
+ soc.rom.print("MARK MEM_ROMRSV rom .data 0x%08x..0x%08x (never written)\r\n", .{ rom_data_low, rom_data_high });
+ soc.rom.print("MARK MEM_HEAP linker gives 0x%08x..0x%08x %u KiB\r\n", .{
+ @as(u32, @intFromPtr(&__heap_start)),
+ @as(u32, @intFromPtr(&__heap_end)),
+ (@as(u32, @intFromPtr(&__heap_end)) - @as(u32, @intFromPtr(&__heap_start))) / 1024,
+ });
+
+ // Write every page first, read every page second. See `writePass` for why one pass cannot
+ // answer this question at all.
+ soc.rom.print("MARK MEM_PASS write\r\n", .{});
+ writePass();
+ soc.rom.print("MARK MEM_PASS read\r\n", .{});
+
+ // Runs are coalesced so the output is a map rather than 192 lines. Every page belongs to
+ // exactly one run, and every run is printed, so the ranges tile the array with no gaps - which
+ // is the property that makes the report checkable.
+ var run: Kind = .skipped;
+ var run_start: u32 = l2mem_low;
+ var addr: u32 = l2mem_low;
+ while (addr < l2mem_high) : (addr += page) {
+ const kind = readPass(addr);
+ if (kind != run) {
+ flush(run, run_start, addr);
+ run = kind;
+ run_start = addr;
+ }
+ }
+ flush(run, run_start, l2mem_high);
+
+ // PSRAM, untrained. The question here is only "does the bus answer at all", not "is it
+ // distinct", so a single write-read-restore is the right shape - and it is expected to fault.
+ // The line is printed BEFORE the access so a hang is unambiguous.
+ soc.rom.print("MARK MEM_PSRAM probing 0x%08x (untrained, may hang)\r\n", .{psram_base});
+ const pp: *volatile u32 = @ptrFromInt(psram_base);
+ const ps_saved = pp.*;
+ pp.* = pattern(psram_base);
+ const ps = pp.*;
+ pp.* = ps_saved;
+ soc.rom.print("MARK MEM_PSRAM read 0x%08x expect 0x%08x %s\r\n", .{
+ ps, pattern(psram_base), if (ps == pattern(psram_base)) "answers".ptr else "absent".ptr,
+ });
+
+ soc.rom.print("MARK MEM_DONE\r\n", .{});
+ while (true) {}
+}
+
+export fn _start() linksection(".text.entry") callconv(.naked) noreturn {
+ asm volatile (
+ \\ li t0, 1 << 13
+ \\ csrs mstatus, t0
+ \\ la sp, __stack_top
+ \\ mv fp, sp
+ \\ la t0, __bss_start
+ \\ la t1, __bss_end
+ \\ bgeu t0, t1, 2f
+ \\1:
+ \\ sw zero, 0(t0)
+ \\ addi t0, t0, 4
+ \\ bltu t0, t1, 1b
+ \\2:
+ \\ j zig_main
+ );
+}
+
+pub const panic = std.debug.FullPanic(struct {
+ fn call(msg: []const u8, _: ?usize) noreturn {
+ soc.rom.print("MARK MEM_PANIC %s\r\n", .{msg.ptr});
+ while (true) {}
+ }
+}.call);