summaryrefslogtreecommitdiff
path: root/src/oracle/timg_cases.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/oracle/timg_cases.zig')
-rw-r--r--src/oracle/timg_cases.zig435
1 files changed, 435 insertions, 0 deletions
diff --git a/src/oracle/timg_cases.zig b/src/oracle/timg_cases.zig
new file mode 100644
index 0000000..34a31de
--- /dev/null
+++ b/src/oracle/timg_cases.zig
@@ -0,0 +1,435 @@
+//! TIMG's side of the differential test: the same timer and watchdog operations expressed as
+//! ESP-IDF's LL calls and as this project's HAL calls.
+//!
+//! **TIMG1 throughout, never TIMG0.** TIMG0 hosts MWDT0, the watchdog the rest of the system relies
+//! on staying quiet; this image's bootloader has already disabled it. A mistake in a case that ran
+//! against group 0 would not fail a comparison, it would reboot the board mid-run with nothing on
+//! the console to explain it.
+//!
+//! The block is restored by `configure` rather than by the harness pulsing a `reset_bit`, and the
+//! reason is the whole safety story of this peripheral: resetting a timer group re-arms
+//! `WDT_FLASHBOOT_MOD_EN`, which runs the watchdog independently of `WDT_EN`, so a bare reset-bit
+//! pulse arms a watchdog nobody is feeding. `clkrst.resetPeripheral(.timg1)` pulses the bit *and*
+//! clears that flag - exactly as `_timg_ll_reset_register` does (timg_ll.h:60-71) - and the harness's
+//! `reset_bit` path does only the pulse. So the restore goes through the HAL, and the reset sequence
+//! itself becomes one of the cases below instead.
+//!
+//! The restore deliberately leaves the watchdog **write-protected**. That makes the unlock half of
+//! every watchdog case load-bearing: an implementation that forgot to lift protection would have its
+//! stage and prescaler writes silently dropped and would differ from IDF's in the snapshot, rather
+//! than passing because both sides happened to be unlocked already.
+//!
+//! What is *not* here, and why: the timers' function-clock source and per-timer gate live in
+//! HP_SYS_CLKRST (PERI_CLK_CTRL20/21), and the group's bus-clock gate in SOC_CLK_CTRL2, none of
+//! which is inside this block. The harness compares one contiguous window of at most 512 words and
+//! HP_SYS_CLKRST is ~0x1e000 bytes away from TIMG1, so a gate case here would compare two identical
+//! TIMG snapshots and pass no matter what it wrote. Those pairings need a HP_SYS_CLKRST suite of
+//! their own; the reset case below is the one part of that story this window can see, and it does
+//! see it, because a group reset and the flashboot fixup both land in these 64 words.
+
+const std = @import("std");
+const hal = @import("hal");
+const regs = @import("regs");
+const mmio = @import("mmio");
+const types = @import("differ_types.zig");
+
+const timg = hal.timg;
+
+// ------------------------------------------------------------------- ESP-IDF's side, from timg_ref.c
+
+extern fn oracle_timg_set_divider(group: c_int, timer: c_uint, divider: c_uint) void;
+extern fn oracle_timg_set_direction_up(group: c_int, timer: c_uint, up: c_int) void;
+extern fn oracle_timg_set_auto_reload(group: c_int, timer: c_uint, en: c_int) void;
+extern fn oracle_timg_enable_counter(group: c_int, timer: c_uint, en: c_int) void;
+extern fn oracle_timg_enable_alarm(group: c_int, timer: c_uint, en: c_int) void;
+extern fn oracle_timg_set_alarm_value(group: c_int, timer: c_uint, value: c_ulonglong) void;
+extern fn oracle_timg_set_reload_value(group: c_int, timer: c_uint, value: c_ulonglong) void;
+extern fn oracle_timg_trigger_soft_reload(group: c_int, timer: c_uint) void;
+extern fn oracle_timg_read_counter(group: c_int, timer: c_uint) c_ulonglong;
+extern fn oracle_timg_reset_register(group: c_int) void;
+
+extern fn oracle_mwdt_set_stage(group: c_int, stage: c_uint, timeout: c_uint, action: c_uint) void;
+extern fn oracle_mwdt_disable_stage(group: c_int, stage: c_uint) void;
+extern fn oracle_mwdt_set_prescaler(group: c_int, prescaler: c_uint) void;
+extern fn oracle_mwdt_set_cpu_reset_length(group: c_int, length: c_uint) void;
+extern fn oracle_mwdt_set_sys_reset_length(group: c_int, length: c_uint) void;
+extern fn oracle_mwdt_set_flashboot_en(group: c_int, en: c_int) void;
+extern fn oracle_mwdt_set_enabled(group: c_int, en: c_int) void;
+extern fn oracle_mwdt_feed(group: c_int) void;
+extern fn oracle_mwdt_write_protect_disable(group: c_int) void;
+extern fn oracle_mwdt_write_protect_enable(group: c_int) void;
+
+/// The group under test, as a number for the C side. Deliberately a constant rather than a variable:
+/// unlike GPIO's pin, this is not a parameter to sweep, it is a safety property.
+const group_id: c_int = 1;
+const group: timg.Group = .timg1;
+
+/// The timer under test. A module-level `var` because Zig has no closures and the harness stores
+/// plain `fn` pointers; the suite runs the whole list once per timer in `timers`.
+pub var timer: timg.Timer = .t0;
+
+/// Both general-purpose timers of the group (TIMG_LL_GPTIMERS_PER_INST is 2 on the P4). Worth
+/// sweeping because the timer index is a *stride* in this HAL rather than a separate set of macros,
+/// and a wrong stride writes into the neighbouring timer's registers.
+pub const timers = [_]timg.Timer{ .t0, .t1 };
+
+inline fn timerId() c_uint {
+ return @intFromEnum(timer);
+}
+
+// ------------------------------------------------------------------------------------ restore
+
+fn restore() void {
+ // Pulses HP_RST_EN1's TIMERGRP1 bit and then clears WDT_FLASHBOOT_MOD_EN, which the pulse
+ // re-armed. Both halves matter; see the file comment.
+ // ESP-IDF's reset, not ours: this suite's `reset_register_clears_flashboot` case exists to
+ // compare the two, and restoring with ours would let a no-op reset pass it.
+ oracle_timg_reset_register(group_id);
+ // IDF's reset re-arms flash-boot protection and does not clear it, so clear it here through the
+ // register directly - the board reboots a few seconds later otherwise.
+ mmio.Reg.atAddress(@intCast(regs.TIMG_WDTCONFIG0_REG(1)))
+ .modify(.{mmio.Field.of(regs.TIMG_WDT_FLASHBOOT_MOD_EN_S, regs.TIMG_WDT_FLASHBOOT_MOD_EN_V).is(0)});
+ // Leave write protection on, so every watchdog case has to lift it itself.
+ timg.unlock(group).release();
+}
+
+// ------------------------------------------------------------------------------------- suite
+
+pub const suite: types.Suite = .{
+ .descriptor = .{
+ .name = "timg1",
+ // TIMG_T0CONFIG_REG is at +0x00 of the group's block (timer_group_reg.h:19), and the group
+ // stride is 0x1000 (:14).
+ .base = @intCast(regs.TIMG_T0CONFIG_REG(1)),
+ // 0x100 bytes: the last register in the block is TIMG_REGCLK_REG at +0xfc. The window has to
+ // reach it - TIMG_WDTWPROTECT_REG is at +0x64 and the four stage-timeout registers at
+ // +0x50..+0x5c, so a window that stopped at the timers (+0x48) would be blind to every
+ // watchdog case in this file.
+ .words = 64,
+ .volatile_words = &.{
+ (0x04 - 0x00) / 4, // TIMG_T0LO - the captured counter, which moves between snapshots
+ (0x08 - 0x00) / 4, // TIMG_T0HI
+ (0x28 - 0x00) / 4, // TIMG_T1LO
+ (0x2c - 0x00) / 4, // TIMG_T1HI
+ (0x68 - 0x00) / 4, // TIMG_RTCCALICFG - RTC calibration runs cyclically by default
+ (0x6c - 0x00) / 4, // TIMG_RTCCALICFG1 - and latches a new count each cycle
+ (0x74 - 0x00) / 4, // TIMG_INT_RAW_TIMERS - alarm/watchdog raw status, set by hardware
+ (0x78 - 0x00) / 4, // TIMG_INT_ST_TIMERS
+ (0x80 - 0x00) / 4, // TIMG_RTCCALICFG2
+ },
+ // TIMG1's bus clock: SOC_CLK_CTRL2 bit 22 (hp_sys_clkrst_reg.h:763, and timg_ll.h:35-42
+ // for the register it belongs to - not PERI_CLK_CTRL21, which is where this project's
+ // clkrst table had it until this suite was written). A snapshot of a gated block returns
+ // the last latched value rather than zeros, so the harness checks this first.
+ .clock = .{
+ .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL2_REG),
+ .bit = @intCast(regs.HP_SYS_CLKRST_REG_TIMERGRP1_APB_CLK_EN_S),
+ },
+ .restore = .{ .configure = restore },
+ },
+ .cases = &.{
+ // ---- prescaler. 2 is the hardware minimum and 65536 is the maximum, encoded as 0
+ // (timer_ll.h:191-199) - the one arithmetic edge in this peripheral.
+ .{ .name = "divider", .arg = 2, .idf = idfDivider2, .ours = ourDivider2 },
+ .{ .name = "divider", .arg = 1234, .idf = idfDivider1234, .ours = ourDivider1234 },
+ .{ .name = "divider", .arg = 65535, .idf = idfDivider65535, .ours = ourDivider65535 },
+ .{ .name = "divider_wraps_to_zero", .arg = 65536, .idf = idfDivider65536, .ours = ourDivider65536 },
+ // ---- direction, auto-reload, counter and alarm enables
+ .{ .name = "direction_up", .arg = 1, .idf = idfDirUp, .ours = ourDirUp },
+ .{ .name = "direction_down", .arg = 0, .idf = idfDirDown, .ours = ourDirDown },
+ .{ .name = "auto_reload_on", .arg = 1, .idf = idfReloadOn, .ours = ourReloadOn },
+ .{ .name = "auto_reload_off", .arg = 0, .idf = idfReloadOff, .ours = ourReloadOff },
+ .{ .name = "counter_enable", .arg = 1, .idf = idfCounterOn, .ours = ourCounterOn },
+ .{ .name = "counter_disable", .arg = 0, .idf = idfCounterOff, .ours = ourCounterOff },
+ .{ .name = "alarm_enable", .arg = 1, .idf = idfAlarmOn, .ours = ourAlarmOn },
+ .{ .name = "alarm_disable", .arg = 0, .idf = idfAlarmOff, .ours = ourAlarmOff },
+ // ---- the 54-bit pairs. 0x2a_5555_aaaa exercises all 22 bits of the high word: a value
+ // that fit in 32 bits would pass even if the high half were dropped entirely.
+ .{ .name = "alarm_value_54bit", .arg = 0x5555_aaaa, .idf = idfAlarmValue, .ours = ourAlarmValue },
+ .{ .name = "alarm_value_zero", .arg = 0, .idf = idfAlarmValueZero, .ours = ourAlarmValueZero },
+ .{ .name = "load_value_54bit", .arg = 0x1234_5678, .idf = idfLoadValue, .ours = ourLoadValue },
+ // Write-to-trigger: nothing in the compared window changes, and the counter registers are
+ // volatile. The case is here because it would catch the trigger landing on the wrong
+ // address - TIMG_T0LOAD_REG is one word past TIMG_T0LOADHI_REG - which is a live risk when
+ // the timer index is a stride rather than a distinct macro.
+ .{ .name = "soft_reload_trigger", .idf = idfSoftReload, .ours = ourSoftReload },
+ // The latch-then-read sequence. Register-identical by construction, so what it really
+ // proves is that our poll terminates: this peripheral acknowledges a capture by *clearing*
+ // TxUPDATE, and waiting for it to be set instead hangs the run.
+ .{ .name = "read_counter_latch", .idf = idfReadCounter, .ours = ourReadCounter },
+ // ---- watchdog. Every one of these has to lift write protection and put it back; the
+ // restored state has it on, so a dropped unlock shows up as a difference.
+ .{ .name = "wdt_write_protect_dance", .idf = idfWdtDance, .ours = ourWdtDance },
+ .{ .name = "wdt_stage0_interrupt", .arg = 2_000_000, .idf = idfWdtStage0, .ours = ourWdtStage0 },
+ .{ .name = "wdt_stage1_reset_cpu", .arg = 5_000, .idf = idfWdtStage1, .ours = ourWdtStage1 },
+ .{ .name = "wdt_stage2_reset_system", .arg = 123_456, .idf = idfWdtStage2, .ours = ourWdtStage2 },
+ .{ .name = "wdt_stage3_off", .idf = idfWdtStage3Off, .ours = ourWdtStage3Off },
+ .{ .name = "wdt_prescaler", .arg = 20_000, .idf = idfWdtPrescaler, .ours = ourWdtPrescaler },
+ .{ .name = "wdt_cpu_reset_length", .arg = 7, .idf = idfWdtCpuLen, .ours = ourWdtCpuLen },
+ .{ .name = "wdt_sys_reset_length", .arg = 4, .idf = idfWdtSysLen, .ours = ourWdtSysLen },
+ .{ .name = "wdt_flashboot_off", .arg = 0, .idf = idfWdtFlashbootOff, .ours = ourWdtFlashbootOff },
+ .{ .name = "wdt_feed", .idf = idfWdtFeed, .ours = ourWdtFeed },
+ // Safe on TIMG1 only because the restored state has all four stages off and flashboot mode
+ // cleared, so an enabled watchdog here has no action to take before the next restore.
+ .{ .name = "wdt_enable", .arg = 1, .idf = idfWdtEnable, .ours = ourWdtEnable },
+ .{ .name = "wdt_disable", .arg = 0, .idf = idfWdtDisable, .ours = ourWdtDisable },
+ // ---- the reset sequence itself, which is the only part of the clock/reset table this
+ // window can see: the group reset plus the flashboot fixup that has to follow it.
+ .{ .name = "reset_register_clears_flashboot", .idf = idfResetRegister, .ours = ourResetRegister },
+ },
+ .setup = setup,
+};
+
+/// The group's bus clock. Already 1 out of reset (hp_sys_clkrst_reg.h:763, default 1) and this image
+/// never runs `esp_perip_clk_init`, so this is belt-and-braces - but a snapshot of a gated block is
+/// stale rather than zero, and the harness would rather fail the gate check than compare noise.
+fn setup() void {
+ hal.clkrst.setClockEnabled(.timg1, true);
+}
+
+// -------------------------------------------------------------------------- the case pairs
+// Same operation, same arguments, twice. IDF's LL on one side, this HAL on the other; a read-back
+// through our own accessor would prove nothing, which is the whole point of the arrangement.
+
+fn idfDivider2() void {
+ oracle_timg_set_divider(group_id, timerId(), 2);
+}
+fn ourDivider2() void {
+ timg.setDivider(group, timer, 2);
+}
+fn idfDivider1234() void {
+ oracle_timg_set_divider(group_id, timerId(), 1234);
+}
+fn ourDivider1234() void {
+ timg.setDivider(group, timer, 1234);
+}
+fn idfDivider65535() void {
+ oracle_timg_set_divider(group_id, timerId(), 65535);
+}
+fn ourDivider65535() void {
+ timg.setDivider(group, timer, 65535);
+}
+fn idfDivider65536() void {
+ oracle_timg_set_divider(group_id, timerId(), 65536);
+}
+fn ourDivider65536() void {
+ timg.setDivider(group, timer, 65536);
+}
+
+fn idfDirUp() void {
+ oracle_timg_set_direction_up(group_id, timerId(), 1);
+}
+fn ourDirUp() void {
+ timg.setDirection(group, timer, .up);
+}
+fn idfDirDown() void {
+ oracle_timg_set_direction_up(group_id, timerId(), 0);
+}
+fn ourDirDown() void {
+ timg.setDirection(group, timer, .down);
+}
+
+fn idfReloadOn() void {
+ oracle_timg_set_auto_reload(group_id, timerId(), 1);
+}
+fn ourReloadOn() void {
+ timg.setAutoReload(group, timer, true);
+}
+fn idfReloadOff() void {
+ oracle_timg_set_auto_reload(group_id, timerId(), 0);
+}
+fn ourReloadOff() void {
+ timg.setAutoReload(group, timer, false);
+}
+
+fn idfCounterOn() void {
+ oracle_timg_enable_counter(group_id, timerId(), 1);
+}
+fn ourCounterOn() void {
+ timg.setCounterEnabled(group, timer, true);
+}
+fn idfCounterOff() void {
+ oracle_timg_enable_counter(group_id, timerId(), 0);
+}
+fn ourCounterOff() void {
+ timg.setCounterEnabled(group, timer, false);
+}
+
+fn idfAlarmOn() void {
+ oracle_timg_enable_alarm(group_id, timerId(), 1);
+}
+fn ourAlarmOn() void {
+ timg.setAlarmEnabled(group, timer, true);
+}
+fn idfAlarmOff() void {
+ oracle_timg_enable_alarm(group_id, timerId(), 0);
+}
+fn ourAlarmOff() void {
+ timg.setAlarmEnabled(group, timer, false);
+}
+
+/// 54 bits: 22 in the high word, 32 in the low one.
+const alarm_value: u64 = 0x2a_5555_aaaa;
+const load_value: u64 = 0x15_1234_5678;
+
+fn idfAlarmValue() void {
+ oracle_timg_set_alarm_value(group_id, timerId(), alarm_value);
+}
+fn ourAlarmValue() void {
+ timg.setAlarmValue(group, timer, alarm_value);
+}
+fn idfAlarmValueZero() void {
+ oracle_timg_set_alarm_value(group_id, timerId(), 0);
+}
+fn ourAlarmValueZero() void {
+ timg.setAlarmValue(group, timer, 0);
+}
+fn idfLoadValue() void {
+ oracle_timg_set_reload_value(group_id, timerId(), load_value);
+}
+fn ourLoadValue() void {
+ timg.setLoadValue(group, timer, load_value);
+}
+fn idfSoftReload() void {
+ oracle_timg_set_reload_value(group_id, timerId(), load_value);
+ oracle_timg_trigger_soft_reload(group_id, timerId());
+}
+fn ourSoftReload() void {
+ timg.setLoadValue(group, timer, load_value);
+ timg.load(group, timer);
+}
+
+fn idfReadCounter() void {
+ _ = oracle_timg_read_counter(group_id, timerId());
+}
+fn ourReadCounter() void {
+ // Discarding the value is the point: the comparison is over registers, and what this exercises
+ // is the handshake. A null return means our poll gave up after 10,000 reads, which IDF's
+ // version cannot report because it spins forever.
+ _ = timg.read(group, timer);
+}
+
+// ------------------------------------------------------------------------------ watchdog pairs
+
+fn idfWdtDance() void {
+ oracle_mwdt_write_protect_disable(group_id);
+ oracle_mwdt_write_protect_enable(group_id);
+}
+fn ourWdtDance() void {
+ const wdt = timg.unlock(group);
+ wdt.release();
+}
+
+fn idfWdtStage0() void {
+ oracle_mwdt_set_stage(group_id, 0, 2_000_000, @intFromEnum(timg.Action.interrupt));
+}
+fn ourWdtStage0() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setStage(.stage0, 2_000_000, .interrupt);
+}
+
+fn idfWdtStage1() void {
+ oracle_mwdt_set_stage(group_id, 1, 5_000, @intFromEnum(timg.Action.reset_cpu));
+}
+fn ourWdtStage1() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setStage(.stage1, 5_000, .reset_cpu);
+}
+
+fn idfWdtStage2() void {
+ oracle_mwdt_set_stage(group_id, 2, 123_456, @intFromEnum(timg.Action.reset_system));
+}
+fn ourWdtStage2() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setStage(.stage2, 123_456, .reset_system);
+}
+
+fn idfWdtStage3Off() void {
+ // Configure it to something first, so "off" has something to undo and the case cannot pass by
+ // both sides doing nothing.
+ oracle_mwdt_set_stage(group_id, 3, 999, @intFromEnum(timg.Action.interrupt));
+ oracle_mwdt_disable_stage(group_id, 3);
+}
+fn ourWdtStage3Off() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setStage(.stage3, 999, .interrupt);
+ wdt.disableStage(.stage3);
+}
+
+fn idfWdtPrescaler() void {
+ oracle_mwdt_set_prescaler(group_id, 20_000);
+}
+fn ourWdtPrescaler() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setPrescaler(20_000);
+}
+
+fn idfWdtCpuLen() void {
+ oracle_mwdt_set_cpu_reset_length(group_id, @intFromEnum(timg.ResetLength.us_3_2));
+}
+fn ourWdtCpuLen() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setCpuResetLength(.us_3_2);
+}
+
+fn idfWdtSysLen() void {
+ oracle_mwdt_set_sys_reset_length(group_id, @intFromEnum(timg.ResetLength.ns_500));
+}
+fn ourWdtSysLen() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setSysResetLength(.ns_500);
+}
+
+fn idfWdtFlashbootOff() void {
+ oracle_mwdt_set_flashboot_en(group_id, 0);
+}
+fn ourWdtFlashbootOff() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setFlashbootEnabled(false);
+}
+
+fn idfWdtFeed() void {
+ oracle_mwdt_feed(group_id);
+}
+fn ourWdtFeed() void {
+ timg.feed(group);
+}
+
+fn idfWdtEnable() void {
+ oracle_mwdt_set_enabled(group_id, 1);
+}
+fn ourWdtEnable() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setEnabled(true);
+}
+
+fn idfWdtDisable() void {
+ oracle_mwdt_set_enabled(group_id, 0);
+}
+fn ourWdtDisable() void {
+ const wdt = timg.unlock(group);
+ defer wdt.release();
+ wdt.setEnabled(false);
+}
+
+fn idfResetRegister() void {
+ oracle_timg_reset_register(group_id);
+}
+fn ourResetRegister() void {
+ // ESP-IDF's reset, not ours: this suite's `reset_register_clears_flashboot` case exists to
+ // compare the two, and restoring with ours would let a no-op reset pass it.
+ oracle_timg_reset_register(group_id);
+ // IDF's reset re-arms flash-boot protection and does not clear it, so clear it here through the
+ // register directly - the board reboots a few seconds later otherwise.
+ mmio.Reg.atAddress(@intCast(regs.TIMG_WDTCONFIG0_REG(1)))
+ .modify(.{mmio.Field.of(regs.TIMG_WDT_FLASHBOOT_MOD_EN_S, regs.TIMG_WDT_FLASHBOOT_MOD_EN_V).is(0)});
+}