diff options
Diffstat (limited to 'src/oracle/timg_cases.zig')
| -rw-r--r-- | src/oracle/timg_cases.zig | 435 |
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)}); +} |
