summaryrefslogtreecommitdiff
path: root/examples/memprobe.zig
blob: 1ce8a0bebd2d352c1770c2cd2ca1dd64cfa9b2c2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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);