diff options
Diffstat (limited to 'examples/memprobe.zig')
| -rw-r--r-- | examples/memprobe.zig | 215 |
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); |
