diff options
Diffstat (limited to 'examples/differ.zig')
| -rw-r--r-- | examples/differ.zig | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/examples/differ.zig b/examples/differ.zig new file mode 100644 index 0000000..865af3e --- /dev/null +++ b/examples/differ.zig @@ -0,0 +1,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 + ); +} |
