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