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