UVM
Interrupt Checking
Verifying interrupt behavior against the spec — checking both that an interrupt fires when it should and that it does not fire when it shouldn't (no spurious interrupts), that the status register reports the right source, that servicing clears it and the line deasserts, and that priority and nesting are honored — using assertions for the signal handshake and a scoreboard that predicts the expected interrupt from the triggering condition.
Interrupt Verification · Module 25 · Page 25.3
The Engineering Problem
You've modeled the interrupt (Module 25.1) and provoked it deliberately, including the corners (25.2) — now you must verify it behaves correctly. And the checking is easy to get half-right. The obvious check is "when the triggering condition occurs, the interrupt fires" — the positive check. But that's only half: a checker that only verifies "fired when expected" passes a DUT that also fires when it shouldn't — a spurious interrupt (a glitch, a wrong condition, a phantom assertion without a real cause) sails through a one-sided checker, ships, and in the system wastes CPU, confuses firmware, or hangs. The full checking is two-sided (fires when it should AND does not fire when it shouldn't) plus the right source (status), correct clearing (the line deasserts, the status clears), and priority/nesting honored — checked against the spec. The problem this chapter solves is interrupt checking: the complete set of checks (both directions, source, clear, priority/nesting), the two layers (assertions for the signal handshake, scoreboard for the functional behavior), and the predictor that expects the interrupt from the condition — so the whole interrupt behavior is verified, spurious interrupts included.
Interrupt checking is verifying the full interrupt behavior against the spec, across both directions and two layers. The checks: fired when it should (the condition raises the interrupt), not when it shouldn't (no spurious interrupt — the negative check), the right source (the status register reports the correct pending source), correct clearing (servicing deasserts the line and clears the status bit), and priority/nesting honored (simultaneous → priority order; nested → preempt/return). The two layers: assertions (SVA, Module 21) check the signal/protocol handshake — the line asserts within N cycles of the condition, stays asserted until cleared, deasserts after clear, and never asserts spuriously; a scoreboard (Module 18) checks the functional/end-to-end behavior — a predictor expects the interrupt from the triggering condition (this condition → this source's IRQ), and the checker compares the observed interrupt (from the monitor) against the expected. The cardinal discipline: checking must be two-sided — a checker that only confirms expected interrupts misses spurious ones, so the negative check (no interrupt without a triggering condition) is essential. This chapter is interrupt checking: the two-sided checks, the source/clear/priority checks, the assertion and scoreboard layers, and the predictor.
How do you verify interrupt behavior — check that an interrupt fires when it should AND does not fire when it shouldn't (no spurious), that the status reports the right source, that servicing clears it and the line deasserts, and that priority and nesting are honored — using assertions for the signal handshake and a scoreboard predicting the expected interrupt from the condition?
Motivation — why interrupt checking must be two-sided
Half the interrupt checking is easy (did the expected interrupt fire?); the other half (did an unexpected one fire?) is easy to forget and essential. The reasons:
- A one-sided checker passes a DUT that fires spuriously. Checking only "fired when expected" confirms the positive but says nothing about the negative — so a spurious interrupt (no cause, yet it fired) passes. The negative check (no interrupt without a condition) is what catches it.
- Spurious interrupts are real, harmful bugs. In the system, a spurious interrupt wastes CPU (the firmware services a non-event), confuses the interrupt handler (which finds nothing to service), and can hang (an interrupt that can't be cleared because there's no source). So spurious interrupts must be caught.
- The source and clear are part of correctness. "The interrupt fired" isn't enough — the status must report the right source (especially with multiple sources on one line), and servicing must actually clear it (a clear that doesn't clear leaves the line stuck). These need explicit checks.
- Priority and nesting are spec rules to verify. The DUT's interrupt controller has priority and nesting logic — and the checking must verify it honors the spec (highest serviced first, preemption correct). Provoking the corners (25.2) is pointless without checking the behavior.
- Two layers catch different bugs. Assertions catch the signal/protocol bugs (line timing, stay-until-clear, no-spurious-glitch); the scoreboard catches the functional bugs (wrong source, wrong condition→interrupt mapping). Both are needed.
The motivation, in one line: interrupt checking is easy to get half-right (the positive "fired when expected") and easy to leave half-undone (the negative "didn't fire when not expected"), but a one-sided checker passes spurious interrupts (real, harmful bugs) — so checking must be two-sided, plus the source, clear, and priority/nesting checks, across two layers (assertions for the signal handshake, scoreboard for the functional behavior) with a predictor that expects the interrupt from the condition.
Mental Model
Hold interrupt checking as a fire-code inspection of the alarm system — verify it rings for a real fire and doesn't ring with no fire, indicates the right zone, can be reset, and prioritizes overlapping alarms per code:
A fire inspector certifying an alarm system doesn't just check that the alarm rings when there's a fire. That's necessary, but a system that rings for a fire and also rings randomly for no fire is broken — false alarms are a serious failure, because people stop trusting and responding to them, and the system wastes everyone's time. So the inspection is two-sided: the alarm must ring for a real fire (no missed alarms) and must not ring when there's no fire (no false alarms). Then there's more: when it rings, the panel must indicate the right zone (the right source), so responders go to the right place; the alarm must be resettable after the fire is dealt with (clear and deassert), or it rings forever; and when multiple zones alarm at once or one alarms during response to another, the system must prioritize per the fire code (priority and nesting). The inspector uses two tools. One is continuous monitoring of the alarm wiring and timing — does the bell ring within the required time of detection, stay ringing until reset, go silent after reset, and never glitch on — which is the signal-level check. The other is an audit of the response log against the events — for each actual fire event, was the right alarm raised and serviced, and was every alarm raised backed by a real event — which is the functional, end-to-end check. A one-sided inspection that only confirms the alarm rings for a fire certifies a system that also cries wolf, and a system people learn to ignore is worse than no alarm at all. A fire inspector certifying an alarm system doesn't just check it rings for a fire. A system that rings for a fire and rings randomly for no fire is broken — false alarms are a serious failure (people stop trusting them, the system wastes everyone's time). So the inspection is two-sided: the alarm must ring for a real fire (no missed alarms) and not ring with no fire (no false alarms). Then more: when it rings, the panel indicates the right zone (the right source); the alarm is resettable after the fire is dealt with (clear/deassert), or it rings forever; and overlapping alarms are prioritized per the fire code (priority/nesting). The inspector uses two tools: continuous monitoring of the wiring/timing (does the bell ring within time, stay until reset, go silent after, never glitch — the signal-level check), and an audit of the response log against the events (for each real fire, was the right alarm raised and serviced, and was every alarm backed by a real event — the functional check). A one-sided inspection that only confirms the alarm rings for a fire certifies a system that also cries wolf — and a system people learn to ignore is worse than no alarm.
So interrupt checking is a two-sided fire-code inspection: verify the interrupt fires for a real condition (no missed) AND doesn't fire with no condition (no spurious/false alarm), names the right source (status/zone), can be cleared (deassert/reset), and prioritizes overlapping/nested interrupts per spec. The two tools: assertions (the signal handshake — assert-within-N, stay-until-clear, deassert-after-clear, no-glitch) and a scoreboard (the response audit — predict the expected interrupt from the condition, compare to observed, check source/service). A one-sided checker that only confirms expected interrupts certifies a DUT that also cries wolf. Check both directions — fires-for-a-condition and never-without-one — plus source, clear, and priority, with assertions on the wiring and a scoreboard auditing the response.
Visual Explanation — the full set of interrupt checks
The defining picture is the checklist: fired-when-should, not-when-shouldn't, right-source, clears/deasserts, priority/nesting — the complete interrupt verification.
The figure shows the full set of interrupt checks. Fired when it should (the brand-colored top — positive): the triggering condition raises the interrupt (the expected IRQ occurs). Not when it shouldn't (the warning-colored — negative, no spurious): no interrupt without a triggering condition — the most-forgotten, essential check. Right source + correct clearing (the success-colored): the status reports the correct pending source; servicing deasserts the line and clears the status bit. Priority and nesting honored: simultaneous serviced in priority order; nested preempt and return — per the spec. The crucial reading is that all five are needed, and the warning-colored negative check (no spurious) is highlighted because it's the most-forgotten and essential. The positive check (fired when it should) is the obvious one — and a checker with only that looks complete (it confirms the interrupts fire). But without the negative (not when it shouldn't), a DUT that also fires spurious interrupts passes — the checker never asks "did an interrupt fire that shouldn't have?". So the negative is what makes the checking complete. The source check matters because "the interrupt fired" doesn't say which source (with multiple on one line, the status must name the right one). The clear check matters because a clear that doesn't clear leaves the line stuck. The priority/nesting check verifies the DUT's controller honors the spec (and uses the corner sequences from 25.2). The diagram is the complete checklist: fired-when-should (positive) + not-when-shouldn't (negative) + right-source + clears + priority/nesting — and the negative is the one not to forget. Check all five — fired-when-should, not-when-shouldn't (no spurious), right source, clears, priority/nesting — and never forget the negative check.
RTL / Simulation Perspective — assertions and the scoreboard predictor
In code, interrupt checking is assertions (the signal handshake, including no-spurious) and a scoreboard predictor (expect the interrupt from the condition). The example shows both.
// === ASSERTIONS: the signal/protocol handshake (Module 21) ===
// fired-when-should: the condition raises the interrupt within N cycles
a_irq_on_overflow: assert property (@(posedge clk) fifo_overflow |-> ##[1:3] irq);
// stay-until-clear (level): irq stays asserted until the clear is written
a_irq_held: assert property (@(posedge clk) (irq && !clear_written) |=> irq);
// deassert-after-clear: clearing deasserts the line within N cycles
a_irq_clears: assert property (@(posedge clk) clear_written |-> ##[1:3] !irq);
// NO-SPURIOUS (the NEGATIVE check): irq must NOT assert without a triggering condition
a_no_spurious: assert property (@(posedge clk) $rose(irq) |-> a_trigger_occurred);
// ← any condition that legally raises irq; if irq rose with NONE → SPURIOUS → fires (DebugLab)
// === SCOREBOARD PREDICTOR: expect the interrupt from the triggering condition (Module 18) ===
class irq_scoreboard extends uvm_scoreboard;
// when a triggering condition is observed, PREDICT the expected interrupt
function void write_condition(condition_txn c);
expected_irq_q.push_back(predict_irq(c)); // condition → expected (source bit, time window)
endfunction
// when an interrupt is observed (from the monitor, 25.1), CHECK it against the expected
function void write_irq(irq_txn obs);
if (expected_irq_q.size() == 0)
`uvm_error("IRQ_SB", "SPURIOUS interrupt — observed an IRQ with no expected condition") // negative!
else begin
expected_irq_t exp = expected_irq_q.pop_front();
if (obs.source != exp.source)
`uvm_error("IRQ_SB", $sformatf("wrong source: got %0h exp %0h", obs.source, exp.source))
end
endfunction
endclass
// ✗ MISTAKE: only check write_irq against expected, NEVER flag an IRQ with no expected → spurious ships (DebugLab)The code shows the two checking layers. Assertions (the signal handshake): a_irq_on_overflow (fired-when-should — fifo_overflow |-> ##[1:3] irq), a_irq_held (stay-until-clear — level), a_irq_clears (deassert-after-clear — clear_written |-> ##[1:3] !irq), and a_no_spurious (the negative — $rose(irq) |-> a_trigger_occurred: irq rising must be justified by some triggering condition; if it rose with none, spurious → fires). Scoreboard predictor: write_condition predicts the expected interrupt from the triggering condition (predict_irq(c) → source bit, time window, pushed to expected_irq_q); write_irq checks an observed interrupt against the expected — and crucially, if expected_irq_q is empty when an interrupt arrives, it's SPURIOUS (uvm_error("SPURIOUS interrupt — IRQ with no expected condition") — the negative check at the functional layer), and if the source mismatches, wrong source. The mistake (commented) is only checking observed against expected but never flagging an interrupt with no expected → spurious ships (the DebugLab). The shape to carry: interrupt checking is assertions (the signal handshake — fired-within-N, held, clears, no-spurious-glitch) and a scoreboard predictor (condition → expected, compare observed, flag spurious when an interrupt arrives with no expected). The no-spurious check appears at both layers: a_no_spurious (the line rose without a condition) and the scoreboard's empty-queue check (an interrupt arrived with no expected). Both are the negative check — essential and easy to omit. The predictor (predict_irq) is the reference — it encodes the spec's condition→interrupt mapping, so the scoreboard knows what to expect. Check the signal handshake with assertions (including no-spurious) and the functional behavior with a predictor scoreboard that flags any interrupt with no expected condition.
Verification Perspective — the two-sided check
The defining discipline is the two-sided check — no-missed (the condition must raise the interrupt) AND no-spurious (the interrupt must not fire without a condition). Seeing both directions clarifies why one-sided is incomplete.
The figure shows the two-sided interrupt check. The positive direction (no-missed): for every triggering condition, the expected interrupt must fire — a predictor expects the interrupt from the condition, and the checker confirms it occurred. The negative direction (no-spurious): for every interrupt that fires, there must be a triggering condition — an interrupt with no expected condition is spurious and flagged. The verification insight is that interrupt correctness is a bijection: every condition should produce its interrupt (no missed), and every interrupt should be backed by a condition (no spurious) — and checking both directions is what makes it complete. A one-sided checker (the positive only) verifies "every condition → its interrupt" but not "every interrupt → a condition" — so a spurious interrupt (firing with no condition) passes (the positive check never looks at unexpected interrupts). The brand-colored positive (no-missed) and the warning-colored negative (no-spurious) both feed the default-colored complete checking — and both are required. The implementation of the negative is the scoreboard's empty-expected-queue check (an interrupt arrived with no expected) and the assertion $rose(irq) |-> a_trigger_occurred (the line rose without a cause) — both catch the spurious. The reason the negative is forgotten is psychological: the positive is what you set out to verify ("the interrupt should fire on overflow"), so you write that check; the negative ("the interrupt should NOT fire on nothing") is less obvious (it's a check for the absence of behavior). But spurious interrupts are real bugs, so the negative is essential. The diagram is the two-sided check: positive (condition → interrupt, no-missed) + negative (interrupt → condition, no-spurious) → complete — and one-sided (positive only) is incomplete. Check both directions — every condition produces its interrupt (no missed) and every interrupt is backed by a condition (no spurious).
Runtime / Execution Flow — the predictor expecting and comparing
At run time, the scoreboard predicts the expected interrupt from the condition, then compares the observed interrupt — flagging a missed or spurious one. The flow shows it.
The flow shows the scoreboard predicting and comparing. Condition (step 1): a triggering condition is observed (the stimulus caused it); the predictor records the expected interrupt (source, time window). Observe (step 2): an interrupt arrives from the monitor; the checker matches it against the recorded expectations. Match (step 3): matches a pending expectation → correct (check the source); matches none → spurious, flagged. End (step 4): any expected interrupt never matched by an observed one is a missed interrupt, flagged. The runtime insight is that the predictor drives both directions of the two-sided check. The no-spurious check (step 3) is observed-with-no-expected: an interrupt arrives but matches no pending expectation → spurious. The no-missed check (step 4) is expected-with-no-observed: an expected interrupt was recorded (a condition occurred) but no matching interrupt arrived → missed. So the single predictor — condition → expected — is the reference for both the positive (every expected should be observed) and the negative (every observed should be expected) checks. The matching (step 3) also checks the source (the observed interrupt's source must match the expected — the right source). This is the scoreboard pattern (Module 18) for interrupts: a predictor (condition → expected interrupt) and a comparison (observed vs expected), with spurious = observed-not-expected and missed = expected-not-observed. The brand-colored condition/observe feed the success-colored match, with the default-colored end-check for missed. The crucial point is that one predictor enables both directions — you don't need separate mechanisms; the expected-vs-observed comparison, checked both ways (unexpected-observed = spurious, unobserved-expected = missed), is the complete check. The flow is the predictor's job: condition → expected; observed → match (correct/spurious); end → unmatched-expected = missed — both directions from one predictor. The predictor expects interrupts from conditions; matching observed against expected flags spurious (observed-not-expected) and missed (expected-not-observed).
Waveform Perspective — catching a spurious interrupt
The negative check is visible on a timeline: an interrupt fires with no triggering condition — and the check flags it. The waveform shows a spurious interrupt caught.
A spurious interrupt fires with no triggering condition — the negative check flags it
12 cyclesThe waveform shows catching a spurious interrupt. First, a legitimate interrupt: a triggering condition occurs (condition), the predictor expects an interrupt (expected high), and irq fires — matched, correct. Later, irq fires again but no triggering condition preceded it (condition stays low, expected stays low) — this is a spurious interrupt. The negative check fires (spurious_flag): an interrupt was observed with no expected condition. The crucial reading is the contrast between the two irq assertions: the first (cycle 2) is preceded by a condition (cycle 1) and a matching expectation — legitimate; the second (cycle 7) has no preceding condition and no expectation — spurious. The checker distinguishes them by the expectation: an interrupt with a matching expected is correct; one without is spurious. A one-sided checker — only confirming expected interrupts fire — would check the first (it matched an expectation) but never look at the second (it isn't an expected interrupt, so the positive check doesn't involve it) — and would pass the spurious one. The two-sided check catches it: the negative check asks "did an interrupt fire with no expectation?" and flags the second. The picture to carry is that a spurious interrupt looks identical to a legitimate one on the line (irq asserts the same way) — the only difference is whether a condition caused it — so only a check that looks for interrupts without a cause (the negative, comparing against the predictor's expectations) catches it. Reading the waveform this way — did every interrupt have a preceding condition, or did one fire with none? — is applying the negative check. The second irq firing with no condition, flagged spurious is the signature of the negative check doing its job — catching the interrupt the positive check ignores. A spurious interrupt looks like a legitimate one on the line — only the negative check (an interrupt with no expected condition) catches it.
DebugLab — the spurious interrupt the one-sided checker passed
A spurious-interrupt bug that shipped because the checker only verified expected interrupts
A block's interrupt verification was thorough on the positive side: a scoreboard checked that every triggering condition produced its interrupt — overflow → overflow IRQ, error → error IRQ, done → done IRQ — and every expected interrupt fired correctly, right source, cleared properly. The regression passed, the block shipped. In the system, firmware started reporting a baffling problem: the error interrupt was occasionally firing when there was no error — a spurious assertion. The firmware's error handler would run, find the error status register clean (no error to handle), and return confused — and occasionally, because the spurious interrupt left a transient pending bit the firmware couldn't clear, the system hung. Tracing it to the RTL, the team found a glitch in the error-detection logic: under a specific clock-domain-crossing condition, the error interrupt momentarily asserted without a real error. Reviewing the regression, this spurious assertion had occurred — but the scoreboard had never flagged it, because it only checked "did the expected interrupt fire?" — it never asked "did an interrupt fire that wasn't expected?".
The interrupt scoreboard was one-sided — it verified that expected interrupts fired (the positive direction) but never flagged an interrupt that fired without a triggering condition (the negative direction), so the spurious interrupt passed unchecked:
✗ ONE-SIDED checker — only verify expected interrupts fired (positive only):
function void write_irq(irq_txn obs);
expected_irq_t exp = expected_irq_q.pop_front(); // assumes there IS an expected
if (obs.source != exp.source) `uvm_error(...); // checks the EXPECTED one
endfunction
// → every EXPECTED interrupt is checked correct, regression passes
// BUT a SPURIOUS interrupt (no condition) — the queue is empty or has a DIFFERENT expected —
// is NEVER flagged as spurious → the glitch-induced error IRQ ships → firmware confusion / hang
✓ TWO-SIDED checker — flag an interrupt with NO expected condition (negative direction):
function void write_irq(irq_txn obs);
if (expected_irq_q.size() == 0) // NO expected → SPURIOUS
`uvm_error("IRQ_SB", "SPURIOUS interrupt — observed with no triggering condition");
else begin
expected_irq_t exp = expected_irq_q.pop_front();
if (obs.source != exp.source) `uvm_error("IRQ_SB", "wrong source");
end
endfunction
// + assertion: a_no_spurious: assert property ($rose(irq) |-> a_trigger_occurred);
// now the spurious error IRQ is flagged in regression → caught before siliconThis is the one-sided-checker bug — the cardinal interrupt-checking failure, and an application of the check-both-directions principle (Module 18.4's data-checking) to interrupts. The scoreboard was thorough on the positive side — it verified that every triggering condition produced its correct interrupt (right source, cleared properly) — which looked complete. But it only checked the positive direction ("did the expected interrupt fire?") and never the negative ("did an interrupt fire that wasn't expected?"). So when a glitch in the error-detection logic caused a spurious error interrupt (momentarily asserting without a real error, under a CDC condition), the scoreboard never flagged it — it wasn't an expected interrupt, and the one-sided checker only looked at expected ones. The spurious interrupt shipped, and in the system it confused firmware (the error handler found no error) and occasionally hung (a transient pending bit the firmware couldn't clear). The deep reason is that a spurious interrupt is invisible to a positive-only checker: the checker confirms the expected interrupts are correct, but a spurious one is not in the expected set, so the checker never examines it. The fix is the two-sided check: in write_irq, if the expected queue is empty (an interrupt arrived with no pending expectation), flag it spurious (uvm_error("SPURIOUS — observed with no triggering condition")), plus an assertion (a_no_spurious: $rose(irq) |-> a_trigger_occurred) — so the spurious error IRQ is flagged in regression and caught before silicon. The general lesson, and the chapter's thesis: interrupt checking must be two-sided — verify the interrupt fires when it should (positive) AND does not fire when it shouldn't (negative — no spurious); a checker that only verifies expected interrupts passes a DUT that also raises spurious ones, which are real, harmful bugs (wasted CPU, confused firmware, hangs); so add the negative check (flag any interrupt with no triggering condition, at both the scoreboard and assertion layers), because a one-sided checker that only confirms expected interrupts is blind to the spurious interrupts that fire with no cause. A spurious interrupt looks legitimate on the line and is invisible to a checker that only verifies expected interrupts — check both directions, and flag every interrupt that fires with no cause.
The tell is a spurious interrupt in the system that the regression passed. Diagnose a one-sided checker:
- Check whether the scoreboard flags unexpected interrupts. A checker that only compares observed against expected, never flagging an interrupt with no expected, is one-sided.
- Look for the negative assertion. A no-spurious assertion ($rose(irq) implies a triggering condition occurred) should exist; its absence is the gap.
- Map the system bug to the regression. A spurious interrupt that occurred in regression but wasn't flagged confirms the missing negative check.
- Audit both directions. Confirm the checker verifies both every condition produces its interrupt and every interrupt has a condition.
Check both directions:
- Flag any interrupt with no expected condition. In the scoreboard, an observed interrupt with no pending expectation is spurious — raise an error.
- Add a no-spurious assertion. Assert that an interrupt only rises when a legal triggering condition occurred.
- Treat interrupt checking as a bijection. Every condition produces its interrupt (no missed) and every interrupt is backed by a condition (no spurious) — verify both.
- Don't trust positive-only coverage. A passing positive-side checker says nothing about spurious interrupts; the negative check is separate and essential.
The one-sentence lesson: interrupt checking must be two-sided — verify the interrupt fires when it should and does not fire when it shouldn't, because a checker that only confirms expected interrupts passes a DUT that also raises spurious ones (real bugs that waste CPU, confuse firmware, and hang the system), so flag any interrupt with no triggering condition at both the scoreboard and assertion layers, since a spurious interrupt looks legitimate on the line and is invisible to a positive-only checker.
Common Mistakes
- Checking only that expected interrupts fire. A one-sided checker passes spurious interrupts; add the negative check that flags any interrupt with no triggering condition.
- Omitting the no-spurious assertion. Without an assertion that the line only rises on a legal condition, a glitch-induced interrupt is unflagged at the signal level.
- Not checking the source. "An interrupt fired" isn't enough with multiple sources; verify the status reports the correct pending source.
- Not checking the clear/deassert. A clear that doesn't deassert the line leaves it stuck; assert that clearing deasserts the line and clears the status bit.
- Skipping priority and nesting checks. Provoking the corners (25.2) is pointless without checking the DUT honors the spec's priority and nesting rules.
- Using only one checking layer. Assertions catch signal/protocol bugs, the scoreboard catches functional/source bugs; both are needed.
Senior Design Review Notes
Interview Insights
Two-sided interrupt checking means verifying both that an interrupt fires when it should — the positive direction — and that it does not fire when it shouldn't — the negative direction, no spurious interrupts — and it's essential because a checker that only does the positive passes a DUT that also raises spurious interrupts, which are real, harmful bugs. The positive check is the obvious one: for every triggering condition, the expected interrupt must fire, so you have a predictor that expects the interrupt from the condition and a checker that confirms it occurred, with the right source, cleared properly. That feels complete because it confirms the interrupts you care about fire correctly. But it's only half. The negative check is: for every interrupt that fires, there must be a triggering condition. An interrupt that fires with no cause is spurious, and a positive-only checker never looks at it, because it's not an expected interrupt — the checker only examines the expected set. So a spurious interrupt sails through. Why this matters is that spurious interrupts are serious bugs. In the system, a spurious interrupt makes the firmware run an interrupt handler that finds nothing to service, wasting CPU and confusing the handler, and it can hang the system if the spurious assertion leaves a pending state that can't be cleared. The classic failure is a glitch in the detection logic — say a clock-domain-crossing issue — that momentarily asserts an interrupt with no real event, which a positive-only checker passes because it never asks did an interrupt fire that wasn't expected. The implementation of the negative check is, in the scoreboard, flagging an observed interrupt that has no matching pending expectation as spurious, and at the assertion level, a no-spurious assertion that the line only rises when a legal triggering condition occurred. The reason the negative is forgotten is psychological — you set out to verify the interrupt should fire on a condition, so you write that, while the interrupt should not fire on nothing is a check for the absence of behavior, less obvious. But interrupt correctness is a bijection: every condition produces its interrupt and every interrupt is backed by a condition, and you must verify both. The fire-alarm analogy is that you check the alarm rings for a fire and doesn't ring for no fire — a false-alarming system is broken too.
The full set is: fired when it should, not when it shouldn't, the right source, correct clearing, and priority and nesting honored. Fired when it should is the positive check — the triggering condition raises the interrupt, the expected IRQ occurs within the required time. Not when it shouldn't is the negative check — no interrupt fires without a triggering condition, no spurious interrupts; this is the most-forgotten and is essential, because a spurious interrupt is a real bug a positive-only checker misses. The right source check — with multiple interrupt sources sharing one line, the status register must report the correct pending source, so just knowing an interrupt fired isn't enough; you verify the status names the right one, so firmware would service the right thing. Correct clearing — servicing the interrupt, writing the clear register, must actually deassert the line and clear the status bit; a clear that doesn't clear leaves the line stuck and the design hung, so you assert that clearing deasserts the line within the required time and clears the pending status. Priority and nesting honored — when multiple interrupts fire simultaneously, the DUT must service them in the spec's priority order, and when a higher-priority interrupt nests within a lower one's service, the DUT must preempt correctly, save context, service the higher, and return; these verify the interrupt controller's priority and preemption logic against the spec, and they pair with the corner sequences that provoke simultaneous and nested scenarios. All five are needed. The negative check is the one people forget, and the source and clear checks are easy to skip if you only check that an interrupt fired. You implement these across two layers: assertions for the signal and protocol handshake — fired within N cycles, stay until clear, deassert after clear, no spurious glitch — and a scoreboard for the functional behavior — predicting the expected interrupt from the condition, comparing the observed against it, checking the source, and flagging spurious and missed interrupts. So interrupt checking isn't just the interrupt fired; it's the complete behavior — both directions, the source, the clear, and the priority and nesting — verified against the specification, which is what makes the interrupt mechanism actually verified.
An interrupt scoreboard predictor expects the interrupt from the triggering condition — when a condition is observed, it computes which interrupt should fire, with what source and in what time window, records that expectation, and then matches observed interrupts against the recorded expectations, flagging spurious and missed interrupts. The flow is this. When a triggering condition is observed — the stimulus caused a FIFO overflow, an error, a transfer completion — the predictor uses the spec's condition-to-interrupt mapping to compute the expected interrupt: which source bit should be set, and the time window in which the interrupt should fire. It records this expectation, typically pushing it to a queue or table. Then when an interrupt is observed, from the interrupt monitor watching the line, the checker matches it against the recorded expectations. Three outcomes. If it matches a pending expectation, it's a correct interrupt — and you check the source matches and it's within the time window. If it matches no expectation — the expected queue is empty or has no matching entry — it's a spurious interrupt, flagged, because an interrupt fired with no cause; that's the negative check. And at the end of the test, any expectation that was recorded but never matched by an observed interrupt is a missed interrupt, flagged, because a condition occurred but the interrupt never fired; that's the positive check's failure case. So the single predictor drives both directions: spurious is observed-not-expected, missed is expected-not-observed. This is the scoreboard pattern from the scoreboards module, applied to interrupts: a predictor that models the expected behavior — here, condition produces interrupt — and a comparison of observed against expected. The predictor encodes the spec's mapping of conditions to interrupts, which is the reference. For multiple sources, the predictor tracks per-source expectations and the matching checks the source. For priority and nesting, the predictor and matching extend to model the expected service order and preemption. The key insight is that one predictor enables both the no-spurious and no-missed checks, because the expected-versus-observed comparison, checked both ways, is the complete two-sided check. So you build the predictor to expect interrupts from conditions, and the comparison flags both interrupts with no cause and conditions with no interrupt.
Because assertions catch the signal and protocol bugs while the scoreboard catches the functional and source bugs, and they cover different aspects of interrupt correctness — the two layers together verify the full behavior. Assertions, written in SVA, check the cycle-by-cycle signal handshake of the interrupt line. They verify the timing and protocol: the line asserts within the required number of cycles of the triggering condition, it stays asserted until the clear is written for a level interrupt, it deasserts within the required cycles after the clear, and crucially it never asserts spuriously — a no-spurious assertion that the line only rises when a legal condition occurred. These are precise, cycle-level, signal-domain checks that fire at the exact cycle of a protocol violation, catching things like the line not deasserting after clear, or a glitch asserting it. The scoreboard checks the functional, end-to-end behavior at the transaction level. It uses a predictor to expect the interrupt from the triggering condition, then compares observed interrupts against expected, checking that the right interrupt fired for the right condition, that the status reports the correct source, that the service happened, and flagging spurious interrupts — observed with no expected — and missed interrupts — expected with no observed. These are functional checks about the mapping of conditions to interrupts and the source identification, which assertions on the line alone don't capture, because the line going high doesn't tell you which source or whether it was the right interrupt for the event. So the two layers are complementary, the same way the scoreboard and assertions are complementary in general verification. Assertions catch a protocol bug like a line that doesn't deassert or a spurious glitch on the signal; the scoreboard catches a functional bug like the wrong source reported, or the wrong condition mapped to an interrupt, or a spurious interrupt at the transaction level. Neither alone is complete: assertions verify the signal behaves but not that the right interrupt fired for the right reason; the scoreboard verifies the functional mapping but works at the transaction abstraction and might miss a fine signal-timing issue. Notably, the no-spurious check appears in both layers — an assertion that the line only rises on a condition, and a scoreboard flag for an interrupt with no expected — giving defense in depth on the most important negative check. So you use both to verify both the signal-level protocol and the functional behavior of the interrupt mechanism.
You verify priority and nesting by provoking the simultaneous and nested corner scenarios with directed sequences and then checking that the DUT services interrupts in the spec's priority order and preempts and returns correctly — the checking pairs with the corner provocation, because provoking without checking verifies nothing. For priority, you trigger multiple interrupt sources simultaneously, in the same cycle or overlapping window, and then check that the DUT services them in the order the spec defines — the highest-priority source first. The check needs the spec's priority assignment as the reference: you encode which source outranks which, and the scoreboard verifies the observed service order, or the status and vector, matches the expected priority order. You also check that the handler reads all pending sources from the status, so none are dropped, and that lower-priority ones are serviced after the higher, not lost. For nesting, you trigger a higher-priority interrupt during a lower-priority one's service — aligning the high trigger with the low service window using a directed sequence with a wait. Then you check the preemption: the DUT suspends the low-priority service, saves its context, services the high-priority interrupt, and returns to complete the low-priority service where it left off, with its context intact, not restarted and not lost. The check verifies the suspend, the high service, and the correct resume of the low, against the spec's nesting rules. You'd model the expected nesting behavior in the scoreboard and compare, and you can use assertions for the signal-level preemption handshake. The crucial point is that these checks require the spec's priority and nesting rules as the reference, and they must be paired with the corner sequences that provoke the scenarios — because random stimulus rarely produces simultaneous or nested interrupts, you force them with directed sequences, and then the checks verify the DUT's controller honors the rules. Provoking a nested interrupt without checking the preemption is pointless; you'd exercise the corner but verify nothing. And you cover the interactions, so closure requires both that the corners were provoked and that the priority and nesting behavior was checked. So priority and nesting verification is the combination of forcing the multi-interrupt corners and checking the service order and preemption against the spec, which is what verifies the interrupt controller's hardest logic.
Exercises
- Add the negative check. Given a scoreboard that only checks expected interrupts, add the flag for an interrupt with no expected condition.
- Write the assertions. Write the SVA for fired-within-N, deassert-after-clear, and no-spurious.
- Predict and compare. Describe how the predictor flags both a spurious and a missed interrupt from one expected-versus-observed comparison.
- Check the source. Explain why "an interrupt fired" isn't enough with multiple sources and what the source check adds.
Summary
- Interrupt checking verifies the full interrupt behavior against the spec, two-sided: fired when it should (positive) AND not when it shouldn't (no spurious — the negative check), plus the right source (status), correct clearing (line deasserts, status bit clears), and priority/nesting honored.
- It uses two layers: assertions (SVA) for the signal/protocol handshake (assert-within-N, stay-until-clear, deassert-after-clear, no-spurious) and a scoreboard (Module 18) for the functional behavior — a predictor expects the interrupt from the triggering condition, compared against the observed.
- The predictor drives both directions from one mechanism: spurious = observed-not-expected (an interrupt with no expected condition), missed = expected-not-observed (a condition with no interrupt).
- The cardinal discipline: checking must be two-sided — a one-sided checker (positive only) passes a DUT that fires spurious interrupts (real bugs: wasted CPU, confused firmware, hangs), because a spurious interrupt looks legitimate on the line and is invisible to a positive-only checker.
- The durable rule of thumb: verify interrupts two-sided and in two layers — check that the interrupt fires when it should and does not fire when it shouldn't (flag any interrupt with no triggering condition, at both the scoreboard and a no-spurious assertion), that the status reports the right source, that servicing deasserts the line and clears the status bit, and that priority and nesting are honored against the spec; a checker that only confirms expected interrupts is blind to the spurious interrupts that fire with no cause, which look legitimate on the line and ship harmful bugs into the system.
Next — Interrupt Verification Strategies: the module closes with the overall strategy — pulling modeling, sequences, and checking into a complete interrupt verification plan. How to structure a reusable interrupt verification component, plan coverage of sources and interactions, integrate interrupt verification with the main functional verification, and sign off the interrupt mechanism with confidence that every source, corner, and negative case has been exercised and checked.