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