summaryrefslogtreecommitdiff
path: root/examples/differ.zig
blob: 865af3e67170048659b725eb8c6a5d83571279aa (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
//! Differential test: this project's Zig HAL against ESP-IDF's own LL, in one image, on the die.
//!
//!     zig build diff -Doracle -Dapp=examples/differ.zig
//!
//! Both implementations are compiled into the same binary - IDF's `*_ll.h` by Zig's clang, ours by
//! Zig - so they run on the same boot, the same clocks and the same silicon. For each operation the
//! harness brings the peripheral to a known state, runs ESP-IDF's version, photographs the register
//! block, restores, runs ours, photographs again, and compares. A pass means: for this operation and
//! these arguments, our sequence leaves the hardware in the state ESP-IDF's does.
//!
//! Four rules this harness follows because adversarial review measured what happens without them:
//!
//!  1. **A snapshot can have side effects.** `UART_FIFO_REG` is at offset 0x000 of every UART block -
//!     the first word a "read the whole block" loop touches - and reading it *pops the RX FIFO*. The
//!     header annotates it `RO`. So each peripheral declares offsets that must not be read.
//!  2. **A block cannot be restored by writing its snapshot back.** About 10% of this chip's fields
//!     perform an action when written; writing one saved word back to a UART's offset 0 transmits a
//!     character, and restoring GPIO's saved `ENABLE_W1TC` would clear the enables just set. Restore
//!     is either the peripheral's reset bit or a deliberate configure function - never a write-back.
//!  3. **A clock-gated block reads stale data, silently.** Not zeros: the last value latched. Two
//!     snapshots of a gated peripheral can compare *equal* while describing nothing, so the bus
//!     clock is checked before every comparison.
//!  4. **Equal registers do not prove equal sequences.** Ordering is invisible in the final state,
//!     and ordering is where the interesting bugs are - LEDC's shadow registers commit on a
//!     self-clearing bit that leaves no trace. Where a peripheral's correctness is an order rather
//!     than a state, its case list says so.

const std = @import("std");
const soc = @import("soc");
const hal = @import("hal");
const regs = @import("regs");
const mmio = @import("mmio");
const oracle = @import("oracle");

pub const panic = std.debug.FullPanic(struct {
    fn call(msg: []const u8, _: ?usize) noreturn {
        soc.rom.print("MARK DIFF_PANIC %s\r\n", .{msg.ptr});
        while (true) {}
    }
}.call);

/// Widest register block any suite compares. 400 words covers GPIO through its matrix
/// configuration; two snapshots at that size are 3.2 KB of L2MEM, which this image has to spare.
const max_words = 512;
var snap_a: [max_words]u32 = @splat(0);
var snap_b: [max_words]u32 = @splat(0);

var cases_run: u32 = 0;
var failures: u32 = 0;

fn contains(haystack: []const u32, needle: u32) bool {
    for (haystack) |h| if (h == needle) return true;
    return false;
}

fn snapshot(p: oracle.types.Peripheral, out: []u32) void {
    for (0..p.words) |i| {
        const w: u32 = @intCast(i);
        if (contains(p.no_read, w)) {
            // A value hardware cannot produce, so a diff involving it is obviously a harness bug
            // rather than a peripheral difference.
            out[i] = 0xdead_0000 | w;
            continue;
        }
        out[i] = mmio.Reg.atAddress(p.base + w * 4).raw();
    }
}

fn restore(p: oracle.types.Peripheral) void {
    switch (p.restore) {
        .configure => |f| f(),
        .reset_bit => |b| {
            // Assert then deassert, with interrupts masked: these bits share a register with every
            // other peripheral's reset.
            const guard = hal.clkrst.maskInterrupts();
            defer guard.release();
            const r = mmio.Reg.atAddress(b.reg);
            r.writeRaw(r.raw() | (@as(u32, 1) << b.bit));
            r.writeRaw(r.raw() & ~(@as(u32, 1) << b.bit));
        },
    }
}

fn runSuite(suite: oracle.types.Suite) void {
    const p = suite.descriptor;
    if (p.words > max_words) {
        soc.rom.print("MARK DIFF_SKIP %s wants %u words, harness holds %u\r\n", .{ p.name, p.words, @as(u32, max_words) });
        failures += 1;
        return;
    }
    if (suite.setup) |s| s();

    for (suite.cases) |c| {
        cases_run += 1;

        // Rule 3: a gated block returns the last latched value, so two snapshots of it can agree
        // and mean nothing.
        if (p.clock) |clk| {
            if (mmio.Reg.atAddress(clk.reg).raw() & (@as(u32, 1) << clk.bit) == 0) {
                soc.rom.print("MARK DIFF SKIP  %s.%s bus clock is off; a snapshot would be stale\r\n", .{ p.name, c.name });
                failures += 1;
                continue;
            }
        }

        restore(p);
        c.idf();
        snapshot(p, &snap_a);

        restore(p);
        c.ours();
        snapshot(p, &snap_b);

        var diffs: u32 = 0;
        for (0..p.words) |i| {
            const w: u32 = @intCast(i);
            if (contains(p.volatile_words, w)) continue;
            if (snap_a[i] == snap_b[i]) continue;
            diffs += 1;
            if (diffs <= 4) {
                soc.rom.print("  DIFF %s+0x%03x idf=0x%08x ours=0x%08x xor=0x%08x\r\n", .{
                    p.name, w * 4, snap_a[i], snap_b[i], snap_a[i] ^ snap_b[i],
                });
            }
        }
        if (diffs == 0) {
            soc.rom.print("MARK DIFF ok    %s.%s(%u) %u words identical\r\n", .{ p.name, c.name, c.arg, p.words });
        } else {
            failures += 1;
            soc.rom.print("MARK DIFF FAIL  %s.%s(%u) %u of %u words differ\r\n", .{ p.name, c.name, c.arg, diffs, p.words });
        }
    }
}

export fn zig_main() noreturn {
    // First, before anything long-running: take the RTC watchdog off the board.
    //
    // The bootloader arms it to cover the handover and expects the application to take it over.
    // Nothing in this repo ever did, and nothing noticed, because no run had exceeded eight seconds.
    // This harness passed 26 cases, then 64, and then started resetting mid-run - which looked
    // exactly like "the newest suite crashes the board" and was in fact a ten-second fuse that had
    // been burning since the first image.
    const wdt_was_armed = hal.rwdt.armed();
    const wdt_off = hal.rwdt.disable();
    soc.rom.print("\r\nMARK DIFF_START esp-idf LL vs zig HAL, one image, on the die\r\n", .{});
    soc.rom.print("MARK DIFF_WDT armed_at_entry=%u disabled=%u (bootloader leaves the RTC watchdog running)\r\n", .{
        @as(u32, @intFromBool(wdt_was_armed)),
        @as(u32, @intFromBool(wdt_off)),
    });
    // If IDF's LL was compiled to call the mask ROM, the comparison would be against
    // `rom_gpio_set_output_level` rather than against IDF's register sequence. src/oracle/
    // oracle_sdkconfig.h exists to keep this at 0.
    soc.rom.print("MARK DIFF_CFG gpio_ll_uses_rom_api=%u expect=0\r\n", .{
        @as(u32, @intFromBool(oracle.gpio.usesRomApi())),
    });

    // GPIO's two suites run once per pin, because the bank split at 32 is where its arithmetic
    // differs - and because the pad registers live in a different register file from the GPIO block,
    // far enough away that one window cannot cover both.
    for (oracle.gpio.pins) |p| {
        oracle.gpio.pin = p;
        soc.rom.print("MARK DIFF_PIN %u\r\n", .{@as(u32, p)});
        runSuite(oracle.gpio.suite);
        runSuite(oracle.gpio.iomux_suite);
    }

    // Every other registered peripheral. The two GPIO suites are skipped here because the loop above
    // already ran them once per pin.
    inline for (oracle.suites) |suite| {
        const n = comptime std.mem.span(suite.descriptor.name);
        if (comptime !std.mem.eql(u8, n, "gpio") and !std.mem.eql(u8, n, "iomux")) runSuite(suite);
    }

    soc.rom.print("MARK DIFF_TOTAL cases=%u failures=%u\r\n", .{ cases_run, failures });
    soc.rom.print("MARK DIFF_DONE\r\n", .{});

    // Leave the board as the rest of the project expects it: LED pin an output, blinking.
    hal.gpio.configureOutput(20, .{ .readback = true });
    while (true) {
        hal.gpio.setHigh(20);
        soc.rom.ets_delay_us(500_000);
        hal.gpio.setLow(20);
        soc.rom.ets_delay_us(500_000);
    }
}

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
    );
}