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
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
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),
);
}
|