diff options
Diffstat (limited to 'src/oracle/intr_cases.zig')
| -rw-r--r-- | src/oracle/intr_cases.zig | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/src/oracle/intr_cases.zig b/src/oracle/intr_cases.zig new file mode 100644 index 0000000..78f2965 --- /dev/null +++ b/src/oracle/intr_cases.zig @@ -0,0 +1,595 @@ +//! The interrupt controller's side of the differential test. +//! +//! **What this can and cannot prove.** A register comparison is weaker evidence for an interrupt +//! controller than for any other peripheral here, and pretending otherwise would be the worst thing +//! this file could do. What it establishes is that for each operation below, this HAL leaves the +//! same words behind that ESP-IDF's code does - the matrix address arithmetic, the `+ 16` offset, +//! the two different priority encodings, the trigger encoding, and which bits each operation is +//! allowed to disturb. What it cannot establish is that an interrupt is ever *taken*: that depends +//! on mtvec, MTVT, mstatus.MIE, the trap entry's register save and the CLIC's arbitration, none of +//! which appear in any register this harness photographs. The behavioural test is spelled out at +//! the foot of this file and the parent must run it separately. +//! +//! **Three windows, three suites.** The controller is three disjoint pieces of address space: +//! * the interrupt matrix at DR_REG_INTERRUPT_CORE0_BASE, 128 words, one per source; +//! * the CLIC's global registers at 0x2080_0000, three words, the third being the threshold; +//! * the CLIC's per-interrupt control file at 0x2080_1000, 48 words, one per CLIC ID. +//! A single window spanning them would have to read about a thousand words of address space nothing +//! is mapped at. Each descriptor below covers only registers these cases actually touch. +//! +//! **Nothing here enables interrupts.** No case calls `hal.intr.init()`, sets mstatus.MIE or writes +//! mtvec; the cases enable *lines* at the CLIC, which with MIE clear is inert. `setup` clears MIE +//! explicitly, so that is true even if something earlier in the image set it. +//! +//! **The two sides reach the hardware by different paths**, which is what makes this a test rather +//! than a tautology: IDF's side goes through `src/oracle/intr_ref.c` into IDF's own inlines and +//! macros, ours goes through `src/hal/intr.zig`, and the harness compares raw words rather than +//! either side's read-back. + +const std = @import("std"); +const hal = @import("hal"); +const regs = @import("regs"); +const mmio = @import("mmio"); +const types = @import("differ_types.zig"); + +const intr = hal.intr; +const Reg = mmio.Reg; +const Field = mmio.Field; + +// ---------------------------------------------------------------- ESP-IDF's side + +extern fn oracle_intr_intthresh_standard() c_int; +extern fn oracle_intr_mintstatus_csr() c_int; +extern fn oracle_intr_mtvt_csr() c_int; +extern fn oracle_intr_nlbits() c_int; +extern fn oracle_intr_ext_offset() c_int; +extern fn oracle_intr_thresh_reg_addr() c_uint; +extern fn oracle_intr_ctrl_reg_addr(clic_id: c_uint) c_uint; + +extern fn oracle_intr_route(intr_src: c_uint, line: c_uint) void; +extern fn oracle_intr_unroute(intr_src: c_uint) void; +extern fn oracle_intr_set_vectored(line: c_uint, vectored: c_int) void; +extern fn oracle_intr_get_type(line: c_uint) c_int; +extern fn oracle_intr_get_priority(line: c_uint) c_int; +extern fn oracle_intr_enable(line: c_uint) void; +extern fn oracle_intr_disable(line: c_uint) void; +extern fn oracle_intr_set_type(line: c_uint, trig: c_uint) void; +extern fn oracle_intr_set_priority(line: c_uint, priority: c_uint) void; +extern fn oracle_intr_edge_ack(line: c_uint) void; +extern fn oracle_intr_enabled_mask() c_uint; +extern fn oracle_intr_set_threshold(level: c_uint) void; +extern fn oracle_intr_get_threshold() c_uint; +extern fn oracle_intr_set_mtvt(mtvt: c_uint) void; + +/// The constants ESP-IDF's half was compiled with. Every one of these is a way the experiment could +/// be quietly meaningless, so the harness should print them rather than assume them: +/// * `intthresh_standard` **must be 0**. A 1 means the C side switched to the `mintthresh` CSR, +/// which this die does not implement, and the threshold comparison would be against a register +/// the interrupt arbiter never reads. (`intr_ref.c` also makes this a compile error.) +/// * `mintstatus_csr` must be 0x346 - the non-standard number for pre-v3 silicon. 0xFB1 would mean +/// the rev-3 header path got selected. +/// * `ext_offset` must be 16 and `nlbits` 3: all the arithmetic on both sides rests on those two. +/// * `thresh_reg` must be 0x20800008, not a CSR number. +pub const RefConfig = struct { + intthresh_standard: u32, + mintstatus_csr: u32, + mtvt_csr: u32, + nlbits: u32, + ext_offset: u32, + thresh_reg: u32, +}; + +pub fn refConfig() RefConfig { + return .{ + .intthresh_standard = @intCast(oracle_intr_intthresh_standard()), + .mintstatus_csr = @intCast(oracle_intr_mintstatus_csr()), + .mtvt_csr = @intCast(oracle_intr_mtvt_csr()), + .nlbits = @intCast(oracle_intr_nlbits()), + .ext_offset = @intCast(oracle_intr_ext_offset()), + .thresh_reg = @intCast(oracle_intr_thresh_reg_addr()), + }; +} + +// ---------------------------------------------------------------- what is under test + +/// The source under test. A module-level variable because Zig has no closures and the harness holds +/// plain `fn` pointers - the same reason `gpio_cases.zig:39` has one. +pub var source: intr.Source = .tg1_t0; + +/// The external line under test, 0..31. +pub var line: u5 = 5; + +/// Sources worth routing, chosen to exercise the address arithmetic rather than to be interesting: +/// the first mapping register (`lp_rtc`, +0x000), the last one that exists on this die +/// (`assist_debug`, +0x1FC), and three in between. A wrong scale factor or a wrong base would be +/// invisible at `lp_rtc` and unmissable at `assist_debug`. +/// +/// The three rev-3-only sources (IDs 133-135) are deliberately absent: their mapping registers are +/// not implemented on pre-v3 silicon, so a comparison there would compare two reads of nothing. +pub const sources = [_]intr.Source{ .lp_rtc, .i2c1, .tg1_t0, .gpio_intr3, .assist_debug }; + +/// Lines worth testing: one in the low half, one in the high half. A line is offset by 16 before it +/// indexes the CLIC, so line 24 lands at CLIC ID 40 - past the point where a missing offset would +/// still have landed inside the 48-word table and gone unnoticed. +pub const lines = [_]u5{ 5, 24 }; + +// ---------------------------------------------------------------- addresses + +const matrix_base: u32 = @intCast(regs.DR_REG_INTERRUPT_CORE0_BASE); +const clic_ctrl_base: u32 = @intCast(regs.DR_REG_CLIC_CTRL_BASE); + +const int_map = Field.of(regs.INTERRUPT_CORE0_UART0_INT_MAP_S, regs.INTERRUPT_CORE0_UART0_INT_MAP_V); +const int_ctl = Field.of(regs.CLIC_INT_CTL_S, regs.CLIC_INT_CTL_V); +const int_attr_trig = Field.of(regs.CLIC_INT_ATTR_TRIG_S, regs.CLIC_INT_ATTR_TRIG_V); +const int_attr_shv = Field.of(regs.CLIC_INT_ATTR_SHV_S, regs.CLIC_INT_ATTR_SHV_V); +const int_ie = Field.of(regs.CLIC_INT_IE_S, regs.CLIC_INT_IE_V); + +inline fn mapReg(source_id: u8) Reg { + return Reg.atAddress(matrix_base + 4 * @as(u32, source_id)); +} +inline fn ctrlReg(clic_id: u32) Reg { + return Reg.atAddress(clic_ctrl_base + 4 * clic_id); +} + +// ---------------------------------------------------------------- restore + +/// Reset value of the CLIC_INT_CTL priority field: 0x1f (`soc/clic_reg.h:70`). Restoring to 0 would +/// be restoring to a state the hardware never boots in, and the two implementations would then be +/// compared from a starting point neither of them produces. +const ctl_reset: u32 = 0x1f; + +/// Bring all three blocks back to a known state between the two implementations. +/// +/// Configuring rather than resetting, and here that is not a preference: neither the matrix nor the +/// CLIC has a reset bit in HP_SYS_CLKRST, and there is no sound way for code to reset the interrupt +/// controller of the core it is running on. Configuring is legitimate because every field this +/// touches is plain read/write. +/// +/// Three things this does that a narrower restore would not, each for a reason: +/// +/// 1. **All five sources, not just the current one.** A case that wrote the wrong mapping register +/// would otherwise leave that write behind; the next case's two snapshots would both inherit it +/// and compare equal. The bug would be visible exactly once and then absorbed. +/// +/// 2. **A sweep of all 128 mapping registers for anything pointing at a line under test.** This is +/// what makes "nothing can assert into these lines during the run" true rather than hoped. The +/// ROM bootloader is under no obligation to leave the matrix clear, and a live peripheral routed +/// to CLIC ID 21 or 40 would set that line's pending bit between the two snapshots and read as a +/// false difference. 128 reads is nothing; guessing is not free. +/// +/// 3. **Both lines, not just the current one**, for the same reason as (1). +fn restore() void { + for (sources) |s| intr.unroute(s); + + // (2): detach anything at all that aims at a line this suite uses. + for (lines) |l| { + const id = @as(u32, l) + intr.ext_offset; + var src: u32 = 0; + while (src <= intr.max_source_id) : (src += 1) { + const r = mapReg(@intCast(src)); + if (r.get(int_map) == id) r.modify(.{int_map.is(0)}); + } + } + + for (lines) |l| { + ctrlReg(@as(u32, l) + intr.ext_offset).modify(.{ + int_ctl.is(ctl_reset), + int_attr_trig.is(0), + int_attr_shv.is(0), + int_ie.is(0), + }); + } + + intr.setThreshold(0); +} + +/// Run once, before anything. Makes the "no interrupt can be taken during this suite" claim true +/// rather than assumed: the cases enable CLIC lines, and an enabled line with MIE set would vector +/// through whatever mtvec the bootloader happened to leave behind. +fn setup() void { + intr.globalDisable(); + restore(); +} + +// ---------------------------------------------------------------- the matrix suite + +/// The interrupt matrix: 128 mapping registers, source 0 at +0x000 through `assist_debug` at +/// +0x1FC. The whole block is in the window deliberately - the cases touch five of the 128, and the +/// other 123 are the point: a routing write that landed on the wrong register shows up as a +/// difference in a word no case names. +/// +/// No `volatile_words`. Each word is a 6-bit read/write field plus reserved bits; nothing here is +/// read-to-clear, nothing self-clears, and no hardware writes these - they are pure configuration. +/// +/// No `clock` either, and that is structural rather than lucky: the matrix and the CLIC are in the +/// CPU's own clock domain and have no gate in HP_SYS_CLKRST, because a core cannot be allowed to +/// gate off the block that delivers its own interrupts. +pub const suite: types.Suite = .{ + .descriptor = .{ + .name = "intr_matrix", + .base = matrix_base, + .words = 128, + .restore = .{ .configure = restore }, + }, + .setup = setup, + .cases = &.{ + .{ .name = "route", .idf = idfRoute, .ours = ourRoute }, + .{ .name = "unroute", .idf = idfUnroute, .ours = ourUnroute }, + .{ .name = "route_rewrite", .idf = idfRouteTwice, .ours = ourRouteTwice }, + .{ .name = "route_preserves_reserved", .idf = idfRouteOverJunk, .ours = ourRouteOverJunk }, + }, +}; + +// ---------------------------------------------------------------- the CLIC control suite + +/// The CLIC's per-interrupt control file: 48 words, one per CLIC ID, the 16 internal IDs included. +/// They are in the window on purpose - every accessor in `hal/intr.zig` adds 16 to the caller's line +/// number, and an implementation that forgot to would write into IDs 5 and 24 instead of 21 and 40. +/// Both of those are inside this window and neither is excluded below, so a missing offset is a +/// visible difference rather than silence. +/// +/// **The pending bits, and why only three words are excluded.** CLIC_INT_IP is bit 0 of every one of +/// these words and the hardware sets it on its own when a source asserts. For the 32 external IDs +/// that cannot happen during this run: `restore` sweeps all 128 mapping registers and detaches +/// anything aimed at a line under test, and the other 30 external lines have nothing routed to them +/// that these cases did not route. The three excluded words are the standard RISC-V internal +/// interrupts, whose pending bits are driven by the core's own timer and software-interrupt +/// hardware rather than by the matrix, and which this file therefore cannot promise are quiet. +/// Excluding them costs nothing: no operation here can reach an internal ID except by the off-by-16 +/// bug, and that bug lands on IDs 5 and 24, which are still compared. +pub const clic_suite: types.Suite = .{ + .descriptor = .{ + .name = "intr_clic", + .base = clic_ctrl_base, + .words = 48, + .volatile_words = &.{ + 3, // machine software interrupt - IP driven by the msip mechanism + 7, // machine timer interrupt - IP driven by the core timer, which is running + 11, // machine external interrupt - IP driven from outside the matrix + }, + .restore = .{ .configure = restore }, + }, + .setup = setup, + .cases = &.{ + .{ .name = "enable", .arg = 1, .idf = idfEnable, .ours = ourEnable }, + .{ .name = "disable", .arg = 0, .idf = idfDisable, .ours = ourDisable }, + .{ .name = "enable_other_line", .idf = idfEnableOther, .ours = ourEnableOther }, + .{ .name = "trigger_level", .arg = 0, .idf = idfTrigLevel, .ours = ourTrigLevel }, + .{ .name = "trigger_rising", .arg = 1, .idf = idfTrigRising, .ours = ourTrigRising }, + .{ .name = "trigger_falling", .arg = 3, .idf = idfTrigFalling, .ours = ourTrigFalling }, + .{ .name = "priority", .arg = 7, .idf = idfPrio7, .ours = ourPrio7 }, + .{ .name = "priority", .arg = 1, .idf = idfPrio1, .ours = ourPrio1 }, + .{ .name = "priority", .arg = 0, .idf = idfPrio0, .ours = ourPrio0 }, + .{ .name = "vectored_on", .arg = 1, .idf = idfVectoredOn, .ours = ourVectoredOn }, + .{ .name = "vectored_off", .arg = 0, .idf = idfVectoredOff, .ours = ourVectoredOff }, + .{ .name = "edge_ack", .idf = idfEdgeAck, .ours = ourEdgeAck }, + .{ .name = "configure_line", .arg = 3, .idf = idfConfigure, .ours = ourConfigure }, + }, +}; + +// ---------------------------------------------------------------- the threshold suite + +/// The CLIC's three global registers, and the reason this file exists at all. +/// +/// 0x2080_0000 CLIC_INT_CONFIG (R/W in MNLBITS, untouched here), 0x2080_0004 CLIC_INT_INFO (RO, +/// reads 48 interrupts / 4 CTL bits), 0x2080_0008 CLIC_INT_THRESH. Three words, contiguous, all +/// mapped - a window of exactly the registers involved, rather than a widened one that reaches the +/// threshold by crossing 4 KiB of nothing. +/// +/// **This is the die where the threshold is a register and not a CSR.** `soc/interrupt_reg.h:28-40` +/// sets INTTHRESH_STANDARD 0 under CONFIG_ESP32P4_SELECTS_REV_LESS_V3, and `riscv/csr_clic.h:37-47` +/// then never defines MINTTHRESH_CSR. A HAL that wrote CSR 0x347 instead would pass every other +/// case in this file and fail every one of these three, which is exactly the discrimination the +/// suite is for: the mistake is silent everywhere else. +pub const thresh_suite: types.Suite = .{ + .descriptor = .{ + .name = "intr_thresh", + .base = @intCast(regs.DR_REG_CLIC_BASE), + .words = 3, + .restore = .{ .configure = restoreThreshold }, + }, + .setup = setup, + .cases = &.{ + .{ .name = "threshold", .arg = 0, .idf = idfThresh0, .ours = ourThresh0 }, + .{ .name = "threshold", .arg = 3, .idf = idfThresh3, .ours = ourThresh3 }, + .{ .name = "threshold", .arg = 7, .idf = idfThresh7, .ours = ourThresh7 }, + }, +}; + + +/// Restored through ESP-IDF's side, never through the code under test. `differ.zig` runs restore, +/// idf, snapshot, restore, ours, snapshot: with the HAL on both the restore and the "ours" side, a +/// HAL function that does nothing leaves run B's snapshot equal to run A's and the case passes. That +/// makes a suite blind to precisely the failure it was written to catch. +fn restoreThreshold() void { + oracle_intr_set_threshold(0); +} + +// ---------------------------------------------------------------- matrix cases + +fn idfRoute() void { + oracle_intr_route(@intFromEnum(source), line); +} +fn ourRoute() void { + intr.route(source, line); +} + +fn idfUnroute() void { + oracle_intr_route(@intFromEnum(source), line); + oracle_intr_unroute(@intFromEnum(source)); +} +fn ourUnroute() void { + intr.route(source, line); + intr.unroute(source); +} + +/// Re-routing a source that is already routed. The mapping register is a read-modify-write of the +/// low 6 bits (`interrupt_clic_ll.h:46`, RV_INT_MASK 63 at line 25), so the second write must +/// *replace* the first rather than OR into it. An `|=` implementation passes the single-write case +/// and fails this one: 31+16 = 47 or'd with 5+16 = 21 is 63, not 21. +fn idfRouteTwice() void { + oracle_intr_route(@intFromEnum(source), 31); + oracle_intr_route(@intFromEnum(source), line); +} +fn ourRouteTwice() void { + intr.route(source, 31); + intr.route(source, line); +} + +/// Route over a word whose reserved bits [31:6] are all set. Both sides must preserve them - IDF's +/// REG_SET_BITS masks with 63, ours is a `Field` of the same width - and this is the case that says +/// so rather than assuming it. A `write` where a `modify` belonged clears them. +fn dirtyMapReg() void { + mapReg(@intFromEnum(source)).writeRaw(0xffff_ffc0); +} +fn idfRouteOverJunk() void { + dirtyMapReg(); + oracle_intr_route(@intFromEnum(source), line); +} +fn ourRouteOverJunk() void { + dirtyMapReg(); + intr.route(source, line); +} + +// ---------------------------------------------------------------- CLIC control cases + +fn idfEnable() void { + oracle_intr_enable(line); +} +fn ourEnable() void { + intr.setEnabled(line, true); +} + +fn idfDisable() void { + oracle_intr_enable(line); + oracle_intr_disable(line); +} +fn ourDisable() void { + intr.setEnabled(line, true); + intr.setEnabled(line, false); +} + +fn otherLine() u5 { + return if (line == lines[0]) lines[1] else lines[0]; +} + +/// Enable the line under test and then enable and disable the *other* one. Catches an index bug +/// that a single-line case cannot: both lines are inside the window, so touching the wrong one is a +/// visible difference rather than an invisible no-op, and the enable/disable pair means the correct +/// answer is "only the first line ends up enabled". +fn idfEnableOther() void { + oracle_intr_enable(line); + oracle_intr_enable(otherLine()); + oracle_intr_disable(otherLine()); +} +fn ourEnableOther() void { + intr.setEnabled(line, true); + intr.setEnabled(otherLine(), true); + intr.setEnabled(otherLine(), false); +} + +fn idfTrigLevel() void { + oracle_intr_set_type(line, 0); +} +fn ourTrigLevel() void { + intr.setTrigger(line, .level); +} + +fn idfTrigRising() void { + oracle_intr_set_type(line, 1); +} +fn ourTrigRising() void { + intr.setTrigger(line, .rising_edge); +} + +/// 0b11, falling edge. IDF's own non-ROM helper only ever writes rising - `esp_tee_rv_utils.h:97-98` +/// is a TODO saying as much - so this is an encoding IDF documents (`clic_reg.h:84-88`) but does not +/// exercise, and its reference is the transcribed REG_SET_FIELD from `esp_rom_clic.c:21` rather than +/// a call into IDF. That makes it the case here most likely to disagree, which is why it is here. +fn idfTrigFalling() void { + oracle_intr_set_type(line, 3); +} +fn ourTrigFalling() void { + intr.setTrigger(line, .falling_edge); +} + +fn idfPrio7() void { + oracle_intr_set_priority(line, 7); +} +fn ourPrio7() void { + intr.setPriority(line, 7); +} + +fn idfPrio1() void { + oracle_intr_set_priority(line, 1); +} +fn ourPrio1() void { + intr.setPriority(line, 1); +} + +/// Priority 0 writes 0x00 over the reset value 0x1f, so this is the case that proves the low +/// `8 - NLBITS` bits are being *cleared*. If either side padded them with ones - which is what the +/// *threshold* encoding does, `csr_clic.h:59` - the two words would differ by 0x1F000000 and by +/// nothing else. Priorities 1 and 7 both leave those bits zero either way and cannot see it. +fn idfPrio0() void { + oracle_intr_set_priority(line, 0); +} +fn ourPrio0() void { + intr.setPriority(line, 0); +} + +fn idfVectoredOn() void { + oracle_intr_set_vectored(line, 1); +} +fn ourVectoredOn() void { + intr.setVectored(line, true); +} + +fn idfVectoredOff() void { + oracle_intr_set_vectored(line, 1); + oracle_intr_set_vectored(line, 0); +} +fn ourVectoredOff() void { + intr.setVectored(line, true); + intr.setVectored(line, false); +} + +/// Writing 1 to IP. With nothing routed to this line there is nothing pending to clear, so what is +/// compared is the *store*: which word, which bit, and whether the surrounding fields survive. That +/// the hardware then reads that write as an acknowledgement is behavioural and out of reach here. +fn idfEdgeAck() void { + oracle_intr_set_type(line, 1); + oracle_intr_edge_ack(line); +} +fn ourEdgeAck() void { + intr.setTrigger(line, .rising_edge); + intr.edgeAck(line); +} + +/// The whole per-line configuration in one go. Our side goes through `configureLine`, not through +/// four separate calls, because that function - not its pieces - is what a driver will use, and +/// because four fields in one word is where an ordering bug or a `write` that should have been a +/// `modify` shows up while each field alone still passes. +fn idfConfigure() void { + oracle_intr_set_type(line, 1); + oracle_intr_set_priority(line, 3); + oracle_intr_set_vectored(line, 0); + oracle_intr_enable(line); +} +fn ourConfigure() void { + intr.configureLine(line, .{ + .handler = noopHandler, + .trigger = .rising_edge, + .priority = 3, + .vectored = false, + }); +} + +/// Installed and never called: `configureLine` requires a handler and the differ never sets MIE. +/// Its address goes into a RAM array no descriptor's window covers, so it cannot perturb a +/// comparison. +fn noopHandler(_: u5) void {} + +// ---------------------------------------------------------------- threshold cases + +fn idfThresh0() void { + oracle_intr_set_threshold(0); +} +fn ourThresh0() void { + intr.setThreshold(0); +} + +fn idfThresh3() void { + oracle_intr_set_threshold(3); +} +fn ourThresh3() void { + intr.setThreshold(3); +} + +fn idfThresh7() void { + oracle_intr_set_threshold(7); +} +fn ourThresh7() void { + intr.setThreshold(7); +} + +// --------------------------------------------------------------------------------------------- +// THE BEHAVIOURAL TEST - which the parent must run, because this file cannot. +// --------------------------------------------------------------------------------------------- +// +// Everything above compares register *state*. None of it touches the parts of this peripheral that +// exist only while an interrupt is in flight: mtvec's mode bits, the MTVT fetch, the CLIC's +// arbitration against the threshold, mcause's EXCCODE, the trap entry's register save, and `mret`. +// A HAL that passes every case above and still never delivers an interrupt is entirely possible - +// it is in fact the expected failure mode, because the single most likely mistake here (writing the +// `mintthresh` CSR instead of CLIC_INT_THRESH_REG) leaves no trace in any register. +// +// The cheap test, with the numbers it needs: +// +// 1. `hal.clkrst.init(.timg1)` - clock on, reset pulsed, flash-boot protection cleared. TIMG's +// reset re-arms the flash-boot watchdog; skipping the clear reboots the board a second later +// with nothing on the console to explain it. +// 2. Arm TIMG1 timer 0 for a one-shot alarm a few milliseconds out, alarm enabled, and the +// timer's own interrupt enable set (TIMG_T0_INT_ENA). +// 3. `hal.intr.init()` - fills the 48-entry vector table with the trap entry, writes MTVT +// (CSR 0x307), writes mtvec = trap_entry | 3, and opens the threshold to 0. +// 4. `hal.intr.attach(.tg1_t0, 5, .{ .handler = h, .trigger = .level, .priority = 1 })`. +// `.tg1_t0` is source ID 49, so the mapping register is DR_REG_INTERRUPT_CORE0_BASE + 0xC4 and +// the value written is 5 + 16 = 21. Priority 1 against threshold 0 is the minimum that is not +// masked: the threshold comparison is inclusive, so priority 0 would never fire. +// 5. The handler increments a counter and **clears TIMG1's interrupt status**. That is mandatory +// for a level source: the CLIC has no acknowledge for level, so a handler that returns without +// clearing the peripheral re-enters immediately and the board sits inside the trap entry with +// the console silent. That failure looks exactly like a crash and is not one. +// 6. `hal.intr.globalEnable()`, spin ~50 ms, `hal.intr.globalDisable()`. +// +// Pass is `counter == 1` **and** `hal.intr.spurious == 0`. Both halves matter: a counter of 1 with a +// non-zero spurious count means an interrupt also arrived on a line nobody claimed, i.e. a matrix +// write went somewhere unintended. +// +// Diagnostics worth printing on failure, because they separate the ways this can go wrong: +// * `hal.intr.getThreshold()` beside `oracle_intr_get_threshold()` - a disagreement means the +// threshold mechanism is the fault, which is what this die's non-standard CLIC invites. +// * `hal.intr.routedLine(.tg1_t0)` - null means the matrix write missed. +// * `hal.intr.isPending(5)` with the counter at 0 - the CLIC latched it and the core never took +// it, so the fault is mtvec, MTVT or MIE, and is neither the matrix nor the threshold. +// * TIMG1's raw interrupt status - if that is 0 the timer never fired and the test is measuring +// something else entirely. +// +// A second, sharper test once the first passes: set the threshold to 7 *before* enabling, confirm +// the counter stays 0 while `isPending(5)` becomes 1, then drop the threshold to 0 and confirm the +// pending interrupt is delivered. That is the only way to show the memory-mapped threshold register +// is the one the arbiter actually reads, and it is the claim this whole file is least able to +// support on its own. + +// --------------------------------------------------------------------------------------------- + +test "the line-to-CLIC-ID offset the cases assume is the one the header defines" { + try std.testing.expectEqual(@as(u32, 16), intr.ext_offset); + try std.testing.expectEqual(@as(u32, 48), intr.total_ids); + // Both test lines land inside the 48-word CLIC window, which is what makes an off-by-16 in + // hal/intr.zig visible to the harness rather than silent... + for (lines) |l| try std.testing.expect(@as(u32, l) + intr.ext_offset < intr.total_ids); + // ...and neither un-offset line is one of the three words excluded as volatile, or the bug + // would land in a word the harness ignores. + for (lines) |l| for (clic_suite.descriptor.volatile_words) |w| try std.testing.expect(w != l); +} + +test "the matrix window covers every source the cases route" { + for (sources) |s| { + try std.testing.expect(!s.isRev3Only()); + try std.testing.expect(@intFromEnum(s) < suite.descriptor.words); + } + // The extremes really are in the set - that is the point of the choice. + try std.testing.expectEqual(@as(u8, 0), @intFromEnum(sources[0])); + try std.testing.expectEqual(@as(u8, 127), @intFromEnum(sources[sources.len - 1])); +} + +test "the threshold window is the register block, not a CSR, and holds all three words" { + try std.testing.expectEqual(@as(u32, 0x2080_0000), thresh_suite.descriptor.base); + // CLIC_INT_THRESH_REG is the third word. If this ever stops being true the window is wrong. + try std.testing.expectEqual( + @as(u32, 0x2080_0008), + thresh_suite.descriptor.base + 4 * (thresh_suite.descriptor.words - 1), + ); +} |
