//! Does an interrupt actually get taken? The one question the differential harness cannot answer. //! //! zig build -Dapp=examples/intrcheck.zig run -Dseconds=10 //! //! The register differential proves that `hal.intr`'s writes land in the same registers ESP-IDF's do. //! It cannot prove that the CLIC then delivers anything, because delivery leaves no trace in any //! register it photographs: mtvec and MTVT are CSRs, the vector table is memory, and whether the //! core vectored to the right handler is a fact about control flow. //! //! So this is the behavioural half, and it is deliberately arranged so each failure mode prints //! something different rather than all of them looking like a silent hang: //! //! * counter 0 and pending 1 - the CLIC latched it and the core never took it: mtvec, MTVT or MIE. //! * counter 0 and pending 0 - never latched: the matrix write missed, or the timer never fired //! (which the raw status distinguishes). //! * counter > 1 - the handler returned without clearing the source, and a level //! interrupt re-enters forever. On this chip the CLIC has no //! acknowledge for a level source, so clearing at the peripheral is //! the only way out, and forgetting it looks exactly like a crash. //! * spurious > 0 - an interrupt arrived on a line nobody claimed: a routing write //! went somewhere unintended. //! //! The second phase is the sharper test, and it is the claim the whole CLIC port is least able to //! support any other way: raise the threshold *above* the line's priority, confirm the interrupt //! latches but is not delivered, then lower it and confirm the pending interrupt arrives. That is //! what shows the memory-mapped threshold register at 0x2080_0008 is the one the arbiter reads - //! rather than the `mintthresh` CSR, which on this die accepts writes and does nothing. //! //! TIMG1's interrupt-enable and interrupt-clear registers are reached here through `regs` directly, //! because `hal.timg` deliberately does not model interrupts. That is the register layer doing its //! job: a peripheral the HAL has not covered yet is still fully addressable. const std = @import("std"); const soc = @import("soc"); const hal = @import("hal"); const regs = @import("regs"); const mmio = @import("mmio"); pub const panic = std.debug.FullPanic(struct { fn call(msg: []const u8, _: ?usize) noreturn { soc.rom.print("MARK INTR_PANIC %s\r\n", .{msg.ptr}); while (true) {} } }.call); /// TIMG1's timer-0 alarm interrupt. Group index 1. const timg1_int_ena = mmio.Reg.atAddress(@intCast(regs.TIMG_INT_ENA_TIMERS_REG(1))); const timg1_int_raw = mmio.Reg.atAddress(@intCast(regs.TIMG_INT_RAW_TIMERS_REG(1))); const timg1_int_clr = mmio.Reg.atAddress(@intCast(regs.TIMG_INT_CLR_TIMERS_REG(1))); const t0_int_ena = mmio.Field.of(regs.TIMG_T0_INT_ENA_S, regs.TIMG_T0_INT_ENA_V); const t0_int_raw = mmio.Field.of(regs.TIMG_T0_INT_RAW_S, regs.TIMG_T0_INT_RAW_V); const t0_int_clr = mmio.Field.of(regs.TIMG_T0_INT_CLR_S, regs.TIMG_T0_INT_CLR_V); /// The CLIC line under test. 5 is arbitrary and free; the differential suite uses 5 and 24. const line: u5 = 5; var fired: u32 = 0; fn onAlarm(l: u5) void { fired += 1; // Two things, and both are needed to return exactly once. // // Clear at the *peripheral*: a level-triggered source stays asserted until the peripheral // deasserts it, and this chip's CLIC offers no acknowledge for one, so a handler that returns // without clearing re-enters immediately and forever with the console silent. // // Then disable the alarm. Clearing the status alone is not enough: with auto-reload off the // counter keeps running past the alarm value, the comparator stays satisfied, and the interrupt // is re-asserted as fast as it is cleared. That is the same silent re-entry by a different // route, and it is what this test hit first. // Mask globally first, before anything else. Any handler that can be re-entered before it has // deasserted its source is one console-silent hang away from being undiagnosable, and this test // exists to distinguish failure modes rather than to demonstrate a tidy handler. hal.intr.globalDisable(); timg1_int_clr.write(.{t0_int_clr.is(1)}); hal.timg.setAlarmEnabled(.timg1, .t0, false); _ = l; } fn armTimer(alarm_ticks: u64) void { hal.clkrst.setClockEnabled(.timg1, true); hal.clkrst.resetPeripheral(.timg1); // 40 MHz APB with a divider of 400 gives 100 kHz, so the alarm value is in units of 10 us. hal.timg.setDivider(.timg1, .t0, 400); hal.timg.setAutoReload(.timg1, .t0, false); hal.timg.setAlarmValue(.timg1, .t0, alarm_ticks); hal.timg.load(.timg1, .t0); timg1_int_ena.modify(.{t0_int_ena.is(1)}); hal.timg.setAlarmEnabled(.timg1, .t0, true); hal.timg.setCounterEnabled(.timg1, .t0, true); } fn disarmTimer() void { hal.timg.setCounterEnabled(.timg1, .t0, false); hal.timg.setAlarmEnabled(.timg1, .t0, false); timg1_int_ena.modify(.{t0_int_ena.is(0)}); timg1_int_clr.write(.{t0_int_clr.is(1)}); } export fn zig_main() noreturn { // Without this the board resets about ten seconds in, mid-test. _ = hal.rwdt.disable(); soc.rom.print("\r\nMARK INTR_START clic behavioural test\r\n", .{}); soc.rom.print("MARK INTR_CFG mtvt_csr=0x%x mintstatus_csr=0x%x nlbits=%u ext_offset=%u\r\n", .{ @as(u32, hal.intr.mtvt_csr), @as(u32, hal.intr.mintstatus_csr), @as(u32, hal.intr.NLBITS), @as(u32, hal.intr.ext_offset), }); // The bootloader hands over with mstatus.MIE set - `init()` masks it, and this records what it // found, because that fact is what makes the ordering below matter at all. const mie_at_boot = hal.intr.globalEnabled(); // A parked core is silent, and every mistake in a trap handler parks the core. This hook is the // difference between a diagnosis and a reflash. hal.intr.on_fault = struct { fn f(x: hal.intr.Fault) void { soc.rom.print("MARK INTR_FAULT mcause=0x%08x mepc=0x%08x mtval=0x%08x taken=%u last_id=%u fired=%u\r\n", .{ x.mcause, x.mepc, x.mtval, hal.intr.taken, @as(u32, hal.intr.last_clic_id), fired, }); } }.f; hal.intr.init(); // What the ROM left behind, captured before init() cleared it. A non-zero enabled_lines is the // whole explanation for the first version of this test hanging: the ROM hands over with lines // armed and MIE set, so the first globalEnable() delivers someone else's interrupt to a handler // that does not exist, and a level source then re-enters forever. soc.rom.print("MARK INTR_BOOT mie=%u rom_enabled_lines=0x%08x rom_routed_sources=%u mtvec=0x%08x mtvt=0x%08x entry=0x%08x table=0x%08x\r\n", .{ @as(u32, @intFromBool(hal.intr.boot_state.mie)), hal.intr.boot_state.enabled_lines, hal.intr.boot_state.routed_sources, hal.intr.readMtvec(), hal.intr.readMtvt(), hal.intr.trapEntryAddress(), hal.intr.vectorTableAddress(), }); _ = mie_at_boot; // Quiesce the source before its line is enabled. TIMG1's raw interrupt status survives a // reflash, and a level-triggered source that is already asserted fires the instant IE goes up - // which, before init() masked MIE, was an immediate re-entrant trap. timg1_int_clr.writeRaw(0xffff_ffff); // What the hardware will actually fetch. With SHV=1 the CLIC loads the handler address from // MTVT[id] and jumps there, so this slot - id 21 for line 5 - is the address the core will run. const tbl = hal.intr.vectorTableAddress(); soc.rom.print("MARK INTR_TABLE table=0x%08x slot21=0x%08x slot0=0x%08x expect_entry=0x%08x\r\n", .{ tbl, mmio.Reg.atAddress(tbl + 4 * 21).raw(), mmio.Reg.atAddress(tbl).raw(), hal.intr.trapEntryAddress(), }); hal.intr.setThreshold(0); hal.intr.attach(.tg1_t0, line, .{ .handler = onAlarm, .trigger = .level, .priority = 1 }); soc.rom.print("MARK INTR_ROUTE tg1_t0(49) -> line %u, routed_line=%u threshold=%u\r\n", .{ @as(u32, line), @as(u32, hal.intr.routedLine(.tg1_t0) orelse 99), @as(u32, hal.intr.getThreshold()), }); // ------------------------------------------- phase 0: does the source reach the CLIC at all? // No MIE, so nothing can be taken and nothing can hang: this asks only whether the matrix and // the CLIC latch a real peripheral event. If pending stays 0 here, everything after it is moot. timg1_int_clr.writeRaw(0xffff_ffff); armTimer(2_000); // 20 ms soc.rom.ets_delay_us(100_000); const p0_raw = timg1_int_raw.get(t0_int_raw); const p0_pending = hal.intr.isPending(line); disarmTimer(); soc.rom.print("MARK INTR_PHASE0 timer_raw=%u expect=1 clic_pending=%u expect=1 (no MIE, cannot hang)\r\n", .{ p0_raw, @as(u32, @intFromBool(p0_pending)), }); // ------------------------------------------------------------------ phase 1: take exactly one fired = 0; hal.intr.spurious = 0; armTimer(5_000); // 50 ms hal.intr.globalEnable(); soc.rom.ets_delay_us(200_000); hal.intr.globalDisable(); const took = fired; const spur = hal.intr.spurious; const raw_after = timg1_int_raw.get(t0_int_raw); const pend_after = hal.intr.isPending(line); disarmTimer(); // `taken` and `last_clic_id` split "latched but not delivered" in two: taken=0 means the trap // was never entered (mtvec, MTVT or SHV), taken>0 with fired=0 means it was entered and the // handler lookup missed. soc.rom.print("MARK INTR_PHASE1 fired=%u expect=1 taken=%u spurious=%u expect=0 last_clic_id=%u expect=21 timer_raw=%u pending=%u\r\n", .{ took, hal.intr.taken, spur, @as(u32, hal.intr.last_clic_id), raw_after, @as(u32, @intFromBool(pend_after)), }); if (took == 1 and spur == 0) { soc.rom.print("MARK INTR_PHASE1 PASS an interrupt was taken and vectored to its handler\r\n", .{}); } else if (took == 0 and pend_after) { soc.rom.print("MARK INTR_PHASE1 FAIL latched but not taken - mtvec, MTVT or MIE\r\n", .{}); } else if (took == 0 and raw_after == 0) { soc.rom.print("MARK INTR_PHASE1 FAIL the timer never fired; this measured nothing\r\n", .{}); } else if (took == 0) { soc.rom.print("MARK INTR_PHASE1 FAIL timer fired but never latched - the matrix write missed\r\n", .{}); } else { soc.rom.print("MARK INTR_PHASE1 FAIL re-entered %u times - the handler is not clearing the source\r\n", .{took}); } // ------------------------------------------- phase 2: is the memory-mapped threshold the real one // Priority 1 against a threshold of 7 must not be delivered. If the arbiter were reading the // mintthresh CSR instead - which this die does not implement, and which accepts writes silently - // the threshold would read back correctly and the interrupt would arrive anyway. fired = 0; hal.intr.spurious = 0; hal.intr.setThreshold(7); armTimer(5_000); hal.intr.globalEnable(); soc.rom.ets_delay_us(200_000); const blocked = fired; const pending_while_blocked = hal.intr.isPending(line); // Now drop the threshold with MIE still on: the latched interrupt must be delivered. hal.intr.setThreshold(0); soc.rom.ets_delay_us(50_000); hal.intr.globalDisable(); const after_drop = fired; disarmTimer(); soc.rom.print("MARK INTR_PHASE2 blocked=%u expect=0 pending_while_blocked=%u expect=1 after_drop=%u expect=1\r\n", .{ blocked, @as(u32, @intFromBool(pending_while_blocked)), after_drop, }); if (blocked == 0 and pending_while_blocked and after_drop >= 1) { soc.rom.print("MARK INTR_PHASE2 PASS the memory-mapped threshold at 0x20800008 is the one the arbiter reads\r\n", .{}); } else if (blocked > 0) { soc.rom.print("MARK INTR_PHASE2 FAIL delivered despite threshold 7 - the write is not reaching the arbiter\r\n", .{}); } else { soc.rom.print("MARK INTR_PHASE2 FAIL blocked but never delivered after the drop\r\n", .{}); } soc.rom.print("MARK INTR_DONE\r\n", .{}); hal.gpio.configureOutput(20, .{ .readback = true }); while (true) { hal.gpio.setHigh(20); soc.rom.ets_delay_us(500_000); hal.gpio.setLow(20); soc.rom.ets_delay_us(500_000); } } export fn _start() linksection(".text.entry") callconv(.naked) noreturn { asm volatile ( \\ li t0, 1 << 13 \\ csrs mstatus, t0 \\ la sp, __stack_top \\ mv fp, sp \\ la t0, __bss_start \\ la t1, __bss_end \\ bgeu t0, t1, 2f \\1: \\ sw zero, 0(t0) \\ addi t0, t0, 4 \\ bltu t0, t1, 1b \\2: \\ j zig_main ); }