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