//! 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);