summaryrefslogtreecommitdiff
path: root/src/oracle/timg_cases.zig
blob: 34a31de9bc072c7472d8863a86cc5de342385958 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
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)});
}