Skip to content

UVM

Interrupt Sequences

Generating interrupt scenarios deliberately — driving the conditions that raise each interrupt source rather than waiting for chance, coordinating the handler with the main stimulus through a virtual sequencer, and forcing the corner cases (nested, simultaneous, back-to-back, storm, clear-race) that random stimulus rarely aligns and where interrupt bugs hide.

Interrupt Verification · Module 25 · Page 25.2

The Engineering Problem

You've modeled the interrupt — a monitor watches the line, a forked handler services it (Module 25.1). But modeling only lets you react to interrupts; to verify them, you must make them happendeliberately, on demand, in the patterns that stress the mechanism. The tempting shortcut is to let interrupts happen by chance during random stimulus — but that gives poor, unrepeatable coverage: the common interrupts get some hits, but the corner cases — a higher-priority interrupt firing exactly during a lower-priority one's service (nested), multiple sources asserting in the same cycle (simultaneous), a new event arriving just as you clear (clear race), a rapid storm — are almost never aligned by chance, and that's exactly where the interrupt bugs hide. And triggering interrupts isn't free: the handler's register accesses (read status, write clear) share the bus with the main stimulus, so they must be coordinated, not left to collide. The problem this chapter solves is interrupt sequences: how to provoke interrupts deliberately (drive the conditions that raise each source), coordinate the handler with the main stimulus, and force the corner cases — nested, simultaneous, storm, clear-race — that random would rarely hit.

Interrupt sequences are the deliberate stimulus that provokes and stresses interrupts. Provoke deliberately: instead of waiting for chance, drive the conditions that raise each interrupt source (fill a FIFO to trigger overflow, inject an error to trigger error-IRQ, complete a transfer to trigger done-IRQ) — triggering each source on demand for repeatable, complete coverage. Coordinate with the main stimulus: the handler (a forked sequence servicing the interrupt) shares the bus with the main stimulus, so a virtual sequencer orchestrates them — the interrupt-triggering sequences, the main traffic, and the handler — so register accesses don't collide. Force the corner cases: nested (trigger a higher-priority interrupt during a lower one's service — does the DUT preempt, save context, return?), simultaneous (trigger multiple sources at once — does the handler read all pending and service in priority order?), back-to-back and storm (rapid succession — does the handler keep up?), and clear race (a new event as you clear — is it lost?). These corners are where bugs hide and what random misses, so directed interrupt sequences force them. This chapter is interrupt sequences: deliberate provocation, coordination, and the corner-case scenarios.

How do you generate interrupt scenarios — provoke interrupts deliberately by driving the conditions that raise each source, coordinate the handler with the main stimulus through a virtual sequencer, and force the corner cases (nested, simultaneous, storm, clear-race) that random stimulus rarely aligns and where interrupt bugs hide?

Motivation — why deliberate provocation beats waiting for chance

Waiting for interrupts to happen during random stimulus under-verifies the mechanism — deliberate provocation is required. The reasons:

  • Coverage of interrupts must be repeatable and complete. To verify each interrupt source, you must exercise it — and random stimulus hits sources unpredictably and incompletely. Deliberate sequences trigger each source on demand, giving repeatable, complete coverage (and closing the coverage on interrupts, Module 19).
  • The corners are where the bugs are, and chance rarely aligns them. Nested, simultaneous, clear-race interrupts require precise timing alignment that random almost never produces — yet these are exactly the hard scenarios where the DUT's interrupt logic is most likely buggy. Directed sequences force the alignment.
  • Triggering shares the bus — coordination is required. The handler's register accesses (read status, write clear) and the interrupt-triggering sequences use the same bus as the main stimulus. Without coordination, they collide (corrupted transactions, deadlock). A virtual sequencer orchestrates them.
  • Nesting and simultaneity test the DUT's interrupt controller. A real interrupt controller has priority, masking, nesting, vectoring — and only multi-interrupt scenarios (simultaneous, nested) exercise that logic. Single, isolated interrupts don't test it.
  • You can't verify what you don't provoke. An interrupt scenario that never occurs in your stimulus is unverified — and a bug there ships. Deliberate provocation is the only way to guarantee the scenario is exercised.

The motivation, in one line: waiting for interrupts to happen during random stimulus gives poor, unrepeatable coverage and almost never aligns the corner timings (nested, simultaneous, clear-race) where the DUT's interrupt logic is most likely buggy — so deliberate interrupt sequences are required: trigger each source on demand (repeatable, complete coverage), coordinate the handler with the main stimulus (share the bus without colliding), and force the corners (the scenarios random misses and where bugs hide).

Mental Model

Hold interrupt sequences as staging fire drills — you deliberately trigger the alarms in specific patterns to exercise the response system's corners, rather than waiting for a real fire:

The interrupt model from the last chapter is the building's alarm-and-response system: an alarm rings, a responder answers it. But having a response system doesn't mean it works — you have to test it, and you don't test it by waiting for a real fire and hoping it covers every scenario. You schedule fire drills, deliberately triggering the alarms in specific patterns to exercise the response. You trigger one alarm and verify the response. You trigger several alarms at once and check the responder handles all of them in the right priority. You trigger a second, higher-priority alarm while the responder is still dealing with the first, and check it correctly preempts — drops what it's doing, handles the urgent one, then returns. You trigger rapid back-to-back alarms and check the responder keeps up. And you trigger an alarm at the exact moment the responder is resetting a previous one, to check the new alarm isn't lost in the reset. These corner patterns — simultaneous, nested, rapid, reset-race — almost never happen by chance in normal operation, which is exactly why you must stage them deliberately: they're where the response system is most likely to fail, and a real fire that happens to hit one of them, with no prior drill, is a disaster. You also coordinate the drills with the building's normal activity, so the alarm-response traffic doesn't collide with everything else going on. So you're the drill director, deliberately staging the scenarios that stress the response — especially the corners — not a passive occupant waiting for fires to reveal whether the system works. The interrupt model (25.1) is the building's alarm-and-response system. Having it doesn't mean it works — you test it, not by waiting for a real fire and hoping, but by scheduling fire drills: deliberately triggering the alarms in specific patterns. Trigger one and verify the response. Trigger several at once (simultaneous — handle all, in priority). Trigger a higher-priority alarm while answering the first (nested — preempt, handle the urgent, return). Trigger rapid back-to-back (storm — keep up). Trigger one as you're resetting a previous (clear race — not lost in the reset). These corner patterns almost never happen by chance — which is exactly why you stage them deliberately: they're where the system is most likely to fail, and a real fire hitting one with no prior drill is a disaster. You coordinate the drills with the building's normal activity (the bus) so they don't collide. You're the drill director, staging the stressing scenariosespecially the cornersnot a passive occupant waiting for fires.

So interrupt sequences are staging fire drills: you deliberately trigger the interrupts in specific patterns (one, simultaneous, nested, storm, clear-race) to exercise the response system (the handler + DUT interrupt logic), especially the corners (where it's most likely buggy and chance never aligns), coordinated with the main stimulus (the bus) so they don't collide. You're the director staging the scenarios, not waiting for them. Stage the interrupt scenarios deliberately — especially the corners random never aligns — and coordinate them with the main stimulus.

Visual Explanation — deliberate provocation versus waiting for chance

The defining picture is the deliberate trigger: drive the condition that raises the interrupt → the interrupt fires → the handler servicesverifyon demand, not by chance.

Deliberate provocation: drive the condition, interrupt raised, handler services, verifyinterrupt sequence drives the triggering condition (fill FIFO / inject error / complete transfer) → the source raises the interrupt → the handler services and clears → verify the scenariointerrupt sequence drives the triggering condition (fill FIFO / inject error / complete transfer) → the source raises the interrupt → the handler services and clears → verify the scenario1Drive the triggering conditionthe sequence drives exactly what raises a source — fill a FIFO,inject an error, complete a transfer.2The source raises the interruptthe driven condition causes the DUT to assert the interrupt — ondemand, repeatably.3The handler services and clearsthe modeled handler (Module 25.1) reads the status, services thesource, and clears it.4Verify the scenariocheck the interrupt fired when it should, the right source, andcorrect handling (Module 25.3).
Figure 1 — provoking an interrupt deliberately. An interrupt sequence drives the specific condition that raises a source — filling a FIFO to overflow, injecting an error, completing a transfer. That condition raises the interrupt, which the modeled handler services and clears. The scenario is then verified. This is on-demand: you choose the source, drive its triggering condition, and the interrupt fires repeatably — versus waiting for random stimulus to happen to raise it, which is unpredictable, incomplete, and almost never aligns the corner cases.

The figure shows provoking an interrupt deliberately. Drive (step 1): the sequence drives exactly what raises a sourcefill a FIFO, inject an error, complete a transfer. Raise (step 2): the driven condition causes the DUT to assert the interrupton demand, repeatably. Service (step 3): the modeled handler (25.1) reads the status, services the source, and clears it. Verify (step 4): check the interrupt fired when it should, the right source, and correct handling (25.3). The crucial reading is on-demand versus by-chance. Deliberate provocation chooses the source and drives its triggering condition, so the interrupt fires predictably and repeatably — you control which source, when, and (with corner sequences) in what pattern. Waiting for chance (the anti-pattern) relies on random stimulus to happen to raise the interrupt — unpredictable (you don't know when or if), incomplete (some sources rarely triggered), and almost never aligning the corners (the precise timing for nested/simultaneous won't occur by chance). The deliberate approach also closes coverage (Module 19) — you can systematically trigger every source and every corner, covering the interrupt space. The brand-colored drive feeds the success-colored raise/service, yielding the default-colored verify. The first step (drive the triggering condition) is the key — it requires knowing what raises each source (the spec: this condition → this interrupt), so the sequence can cause it. This is directed stimulus (Module 20.1) applied to interrupts: cause the specific scenario, don't wait for it. The diagram is the deliberate trigger: drive the condition → interrupt raised → service → verifyon demand, the foundation of interrupt verification. Drive the condition that raises each source on demand, so the interrupt fires repeatably — don't wait for random stimulus to happen to raise it.

RTL / Simulation Perspective — triggering and the corner-case sequences

In code, an interrupt sequence drives the triggering condition, and corner sequences force nested/simultaneous/storm scenarios. The example shows each.

interrupt sequences: trigger a source, and force the nested/simultaneous/storm corners
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// === TRIGGER ONE SOURCE: drive the condition that raises it (deliberate, on demand) ===
class overflow_irq_seq extends base_seq;
  task body();
    // drive exactly what raises the OVERFLOW interrupt: fill the FIFO past full
    repeat (FIFO_DEPTH + 1) write_fifo($urandom);   // → DUT raises OVERFLOW interrupt
    // the forked handler (25.1) will service+clear it; this seq just CAUSES it
  endtask
endclass
 
// === SIMULTANEOUS: trigger MULTIPLE sources in the same window (force the corner) ===
class simultaneous_irq_seq extends base_seq;
  task body();
    fork
      inject_error();          // raises ERROR irq
      complete_transfer();     // raises DONE irq   } both in the same cycle/window
      overflow_fifo();         // raises OVERFLOW irq
    join                       // → multiple sources pending at once → handler must read ALL + prioritize
  endtask
endclass
 
// === NESTED: trigger a HIGHER-priority irq DURING a lower one's service (force the corner) ===
class nested_irq_seq extends base_seq;
  task body();
    trigger_low_priority_irq();             // raises LOW irq → handler starts servicing it
    wait_until_servicing(LOW_IRQ);          // wait until the handler is MID-service of LOW
    trigger_high_priority_irq();            // raise HIGH irq DURING LOW's service → must PREEMPT
    // check: DUT saves context, services HIGH, returns to finish LOW (Module 25.3)
  endtask
endclass
 
// === STORM / BACK-TO-BACK: rapid succession (force the keep-up corner) ===
class irq_storm_seq extends base_seq;
  task body(); repeat (50) begin trigger_irq(); #1; end endtask  // 50 in rapid succession
endclass
 
// === COORDINATION: a VIRTUAL SEQUENCER orchestrates main stimulus + triggers + handler (share the bus) ===
//   vseq runs main_stimulus on bus_sqr, irq triggers on irq_sqr, handler on reg_sqr — arbitrated
 
// ✗ MISTAKE: rely on RANDOM stimulus to hit interrupts → corners (nested/simultaneous) NEVER aligned (DebugLab)

The code shows interrupt sequences. Trigger one source: overflow_irq_seq drives exactly what raises itrepeat(FIFO_DEPTH+1) write_fifo(...) fills the FIFO past full → the DUT raises OVERFLOW; the seq just causes it (the forked handler services+clears). Simultaneous: simultaneous_irq_seq forks inject_error(), complete_transfer(), overflow_fifo() in the same windowmultiple sources pending at once → the handler must read all and prioritize. Nested: nested_irq_seq triggers a low-priority irq, waits until the handler is mid-service, then triggers a high-priority irq during it → the DUT must preempt (save context, service HIGH, return to LOW). Storm: irq_storm_seq triggers 50 in rapid succession → tests the keep-up corner. Coordination: a virtual sequencer orchestrates the main stimulus, the triggers, and the handler (they share the bus, arbitrated). The mistake (commented) is relying on random stimulus → the corners never align (the DebugLab). The shape to carry: an interrupt sequence drives the triggering condition for a specific source (knowing what raises it), and corner sequences force the hard scenarios — simultaneous (fork multiple triggers), nested (trigger during service), storm (rapid succession) — deliberately, with a virtual sequencer coordinating the bus usage. The nested sequence's wait_until_servicing(LOW_IRQ) is the key — it aligns the high trigger with the low service window (the precise timing random can't hit). The corner sequences are directed (Module 20.1) — they force the exact scenario, which is why they find the corner bugs. Drive the triggering condition for each source, and force the corners with directed simultaneous/nested/storm sequences — coordinated by a virtual sequencer.

Verification Perspective — the corner-case scenarios

The value of interrupt sequences is in the corners — the multi-interrupt and timing-aligned scenarios where bugs hide and random misses. Seeing the corners together is the interrupt-stress toolkit.

Interrupt corners: single, simultaneous, nested, storm, clear-racebaselinemultiple at once → read all, prioritizemultiple at once →read all,…higher during service → preempthigher duringservice →…rapid succession → keep uprapidsuccession →…event at clear → not lostevent atclear → not…Interrupt scenariosforce the cornersSingleone source (baseline)Simultaneousread all + prioritizeNestedpreempt + returnBack-to-back / stormkeep upClear racenot lost at clear12
Figure 2 — the interrupt corner-case scenarios. Single: one source triggered, the baseline. Simultaneous: multiple sources assert at once — the handler must read all pending and service in priority order. Nested: a higher-priority interrupt fires during a lower one's service — the DUT must preempt, save context, service the higher, and return. Back-to-back and storm: rapid succession — the handler must keep up without dropping any. Clear race: a new event arrives just as the handler clears — it must not be lost. These corners require precise timing alignment that random stimulus rarely produces, yet they are where the DUT's interrupt logic is most likely buggy.

The figure shows the interrupt corner-case scenarios. Single: one source triggered — the baseline. Simultaneous: multiple sources assert at once — the handler must read all pending and service in priority order. Nested: a higher-priority interrupt fires during a lower one's service — the DUT must preempt, save context, service the higher, and return. Back-to-back and storm: rapid succession — the handler must keep up without dropping any. Clear race: a new event arrives just as the handler clears — it must not be lost. The verification insight is that each corner exercises a distinct part of the DUT's interrupt logic, and each requires precise timing that random rarely produces. Simultaneous exercises the priority/arbitration logic (which source first?) and the status-reads-all (does the handler find all pending?). Nested exercises the preemption/context-save logic (can a higher interrupt interrupt a handler?). Storm exercises throughput (can the handler keep up with a flood?). Clear race exercises the clear/set atomicity (if a new event arrives as you clear, is it preserved or swallowed?). The warning-colored corners (simultaneous, nested, storm, clear-race) are where the DUT's interrupt logic is most likely buggy — and exactly what random misses (the baseline success-colored single is the easy one random might hit). The reason random misses them is timing alignment: nested requires a high interrupt exactly during a low service window (a narrow window, random rarely hits); simultaneous requires two sources in the same cycle (random rarely aligns); clear race requires an event in the exact clear window (vanishingly rare by chance). So only directed sequences (Figure's RTL — wait_until_servicing then trigger, fork simultaneous triggers) force these. This is the directed-for-corners principle (Module 20.1) for interrupts: the corners are unreachable by random, so directed sequences must force them. The diagram is the corner toolkit: single (baseline), simultaneous (prioritize), nested (preempt), storm (keep up), clear-race (not lost) — the scenarios that stress the interrupt logic and random misses. Force the corners — simultaneous, nested, storm, clear-race — with directed sequences; they exercise the interrupt logic random can't align.

Runtime / Execution Flow — coordinating the handler with the main stimulus

At run time, the interrupt service and the main stimulus share the bus — so a virtual sequencer coordinates them. The flow shows the coordination.

Virtual sequencer coordinates main stimulus, interrupt triggers, and handler on the shared busmain trafficinterrupt triggersservice (status read, clear)service(status read…shares thebusshares thebusVirtual sequencerorchestrates + arbitratesMain stimulusnormal trafficInterrupt triggersprovoke interruptsHandlerstatus read + clearShared busarbitrated, no collision12
Figure 3 — coordinating the interrupt handler with the main stimulus on the shared bus. A virtual sequencer orchestrates three concurrent flows: the main stimulus driving normal traffic, the interrupt-triggering sequences provoking interrupts, and the handler servicing them with register reads and writes. All three use the same bus, so the virtual sequencer arbitrates their access — interleaving the handler's status-read and clear-write with the main traffic without collision. Without coordination, the handler's accesses would collide with the main stimulus, corrupting transactions or deadlocking.

The flow shows coordinating the handler with the main stimulus. A virtual sequencer orchestrates three concurrent flows: the main stimulus (normal traffic), the interrupt-triggering sequences (provoking interrupts), and the handler (servicing them with register reads and writes). All three use the same bus, so the virtual sequencer arbitrates their accessinterleaving the handler's status-read and clear-write with the main traffic without collision. The runtime insight is that interrupt service is not free of the main stimulus — it competes for the bus. When an interrupt fires, the handler must read the status register and write the clear registerbus transactions that share the bus with the main stimulus's transactions. Without coordination, the handler's accesses and the main accesses would collideinterleaving badly (a partial main transaction interrupted by a handler access), corrupting transactions, or deadlocking (both waiting for the bus). The virtual sequencer solves this: it owns the bus sequencer and arbitratesgrant the handler a window to service (read status, clear) between (or with priority over) main transactions, then resume the main stimulus. This mirrors the real system — a CPU services an interrupt between (or preempting) its main program, sharing the same memory/bus. The brand-colored virtual sequencer orchestrates the success-colored flows, which share the warning-colored bus (arbitrated). The coordination is why the handler is a sequence on a (virtual) sequencernot a free-running fork that drives the bus directly (which would collide): it goes through the arbitration. The flow is the coordination: virtual sequencer → (main + triggers + handler) → shared bus (arbitrated)interrupt service interleaved with main traffic without collision. Coordinate the handler and main stimulus through a virtual sequencer arbitrating the shared bus — interrupt service must interleave with main traffic, not collide.

Waveform Perspective — a nested interrupt preempting service

The nested corner is visible on a timeline: a low-priority interrupt is being serviced when a high-priority one fires, the handler preempts (services the high), then returns to finish the low. The waveform shows the nesting.

A high-priority interrupt fires during a low one's service; the handler preempts, then returns

12 cycles
A high-priority interrupt fires during a low one's service; the handler preempts, then returnsirq_low fires → handler starts servicing it (svc_low)irq_low fires → handle…irq_high fires DURING svc_low (nested) — directed seq aligned this exactlyirq_high fires DURING …handler PREEMPTS: suspends low, services high (svc_high)handler PREEMPTS: susp…high done → returns to finish low (svc_low resumes) — verify the nestinghigh done → returns to…clkirq_lowsvc_lowirq_highsvc_hight0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — a nested interrupt. A low-priority interrupt fires (irq_low) and the handler starts servicing it (svc_low). While mid-service, a higher-priority interrupt fires (irq_high) — the nested scenario. The DUT/handler preempts: it suspends the low-priority service, services the high-priority one (svc_high), and on completion returns to finish the low-priority service (svc_low resumes). The nesting is the corner: a directed sequence triggered irq_high exactly during svc_low — a timing alignment random would rarely produce — to verify the DUT preempts, saves context, and returns correctly.

The waveform shows a nested interrupt. A low-priority interrupt fires (irq_low) and the handler starts servicing it (svc_low). While mid-service, a higher-priority interrupt fires (irq_high) — the nested scenario. The DUT/handler preempts: it suspends the low-priority service, services the high-priority one (svc_high), and on completion returns to finish the low-priority service (svc_low resumes). The crucial reading is the preemption and return: svc_low starts (cycle 2), then suspends when irq_high fires (cycle 3–4), svc_high runs (cycles 4–5), and svc_low resumes (cycle 7) to finish — the nesting handshake. This is the corner a directed sequence forces: the nested_irq_seq triggered irq_high exactly during svc_low (the wait_until_servicing then trigger) — a timing alignment random would rarely produce (a high interrupt firing precisely in the narrow window while the low is being serviced). The verification checks the DUT preempts correctly: suspends the low, saves its context, services the high, and returns to complete the low where it left off (not restarting, not losing the low). The picture to carry is that nesting is a precise-timing corner — the high must fire during the low's service window, and the DUT must handle the preemption/return — and only a directed sequence (aligning the trigger with the service window) can force it. Reading the waveform this way — does the high preempt the low's service and the low resume after? — is verifying nesting. The svc_low suspending for svc_high then resuming is the signature of correct nesting — the DUT preempting and returning. A nested interrupt preempts the lower service and returns after — a directed sequence aligns the high trigger with the low service window to force the corner random can't hit.

DebugLab — the nested-interrupt bug random never found

A preemption bug that shipped because random stimulus never aligned a nested interrupt

Symptom

A block's interrupt verification looked thorough: random stimulus ran for weeks, interrupts fired and were serviced, the interrupt-source coverage was high (every source triggered many times), and the regression passed. The block shipped. In silicon, a rare but catastrophic bug surfaced: when a high-priority interrupt fired exactly while a low-priority interrupt was being serviced (a nested interrupt), the DUT's interrupt controller failed to save the low-priority context before vectoring to the high-priority handler — so when the high-priority service returned, the low-priority service resumed with corrupted context and produced wrong results. Reviewing the regression, the team found that this nested scenario had never once occurred in weeks of random stimulus: a high-priority interrupt firing in the exact, narrow window during a low-priority service had never aligned by chance. The interrupt-source coverage was highevery source was hit — but the nested-interaction coverage was zero. The bug was in a corner the random stimulus structurally could not reach.

Root cause

The team relied on random stimulus to exercise interrupts and never wrote a directed sequence to force the nested corner — so the precise timing alignment (a high-priority interrupt exactly during a low-priority service) never occurred, and the preemption bug in that corner shipped unexercised:

why high interrupt-source coverage still shipped a nested-interrupt bug
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ RELY on random stimulus for interrupts (no directed corner sequences):
  // weeks of random → every interrupt SOURCE triggered (high source coverage) → "interrupts verified"
  // BUT: NESTED requires irq_high to fire in the NARROW window DURING irq_low's service
  //   random almost NEVER aligns this → the nested scenario coverage = ZERO
  // the preemption/context-save bug in that corner is NEVER exercised → ships
 
✓ WRITE a DIRECTED sequence that FORCES the nested corner:
  class nested_irq_seq extends base_seq;
    task body();
      trigger_low_priority_irq();        // start LOW service
      wait_until_servicing(LOW_IRQ);     // align: wait until MID-service of LOW
      trigger_high_priority_irq();       // fire HIGH exactly DURING LOW service → forces NESTING
    endtask
  endclass
  // now the nested corner is exercised every run → the context-save bug FIRES in regression → caught
  // + cover the nested INTERACTION (not just sources): cross(servicing_low, irq_high) must be hit (19.5)

This is the random-misses-the-corner bug — the cardinal interrupt-sequence failure, and a direct application of the directed-for-corners principle (Module 20.1) to interrupts. The team relied on random stimulus, achieving high interrupt-source coverage (every source triggered) — which looked thorough — but never wrote a directed sequence for the nested corner. The nested scenario requires a high-priority interrupt to fire in the exact, narrow window during a low-priority service — a timing alignment that random almost never produces (the window is narrow, and random interrupts rarely overlap precisely). So the nested-interaction coverage was zero despite high source coverage, and the preemption/context-save bug in that corner shipped unexercised. The deep reason is that the corners require precise timing alignment that random is structurally unlikely to producesource coverage (each interrupt hit) is necessary but not sufficient; the interactions (nested, simultaneous, clear-race) are a separate, harder coverage that only directed sequences reach. The fix is a directed nested_irq_seq that forces the corner: trigger the low-priority irq, wait until mid-service (wait_until_servicing), then trigger the high-priority irq exactly during itforcing the nesting every run, so the context-save bug fires in regression and is caught. And cover the nested interaction (a cross servicing_low × irq_high, Module 19.5) so closure requires the corner to be exercised. The general lesson, and the chapter's thesis: provoke interrupts deliberately, especially the corners, because random stimulus rarely aligns the precise timing for nested, simultaneous, and clear-race interruptssource coverage (each interrupt hit) is not enough; the interaction corners (a high interrupt during a low service, two sources in one cycle, an event at clear) are where the DUT's interrupt logic is most likely buggy and exactly what random misses — so write directed interrupt sequences that force each corner and cover the interactions, because an interrupt corner that random never aligns is a corner that ships unverified. High interrupt-source coverage with no directed corner sequences is the interrupt-layer false confidence — the nested and simultaneous corners, where the bugs are, never happen by chance.

Diagnosis

The tell is high interrupt-source coverage but a corner-interaction bug. Diagnose missing corner sequences:

  1. Check interaction coverage, not just source coverage. Every source triggered (high source coverage) doesn't mean nested/simultaneous interactions were exercised; check the interaction crosses.
  2. Look for directed corner sequences. If interrupts are only hit via random stimulus, the corners (nested, simultaneous, clear-race) are almost certainly unexercised.
  3. Map the shipped bug to its corner. A bug in nested or simultaneous handling that random never produced confirms a missing directed corner sequence.
  4. Confirm the timing alignment is rare. A corner requiring a precise window (interrupt during service) that random structurally rarely aligns is the signature.
Prevention

Force the corners with directed sequences:

  1. Write a directed sequence for each interrupt corner. Nested, simultaneous, back-to-back, storm, and clear-race each get a sequence that forces its precise scenario.
  2. Cover interactions, not just sources. Add coverage crosses for nested and simultaneous interactions, and require them for closure.
  3. Align the timing explicitly. Use waits to align a trigger with a service window or another trigger, forcing the alignment random can't produce.
  4. Don't trust source coverage as interrupt closure. High source coverage is necessary but not sufficient; the interaction corners are separate and must be directed.

The one-sentence lesson: provoke interrupts deliberately, especially the corners, because random stimulus rarely aligns the precise timing for nested, simultaneous, and clear-race interrupts, so high interrupt-source coverage with no directed corner sequences leaves the interaction corners — where the DUT's interrupt logic is most likely buggy — unexercised and shipping, so write directed sequences that force each corner and cover the interactions, not just the sources.

Common Mistakes

  • Relying on random stimulus to hit interrupts. Random rarely aligns the corner timings; write directed sequences that force each scenario, especially the corners.
  • Covering sources but not interactions. High interrupt-source coverage isn't enough; nested and simultaneous interactions need their own directed sequences and coverage.
  • Not coordinating the handler with the main stimulus. The handler's register accesses share the bus; use a virtual sequencer to arbitrate, or they collide.
  • Skipping the nested and simultaneous corners. These exercise the priority and preemption logic where bugs hide; force them with aligned triggers.
  • Ignoring the clear race. A new event arriving as the handler clears can be lost; trigger an event in the clear window to verify it isn't.
  • Triggering one interrupt at a time only. Single isolated interrupts don't test the interrupt controller's multi-interrupt logic; stage multi-interrupt scenarios.

Senior Design Review Notes

Interview Insights

Because waiting for interrupts to happen during random stimulus gives poor, unrepeatable coverage and almost never aligns the corner cases, where the interrupt bugs hide — so you must deliberately drive the conditions that raise each source. To verify an interrupt, you have to exercise it, and random stimulus hits interrupt sources unpredictably and incompletely. Some sources might fire often, others rarely, and you can't repeat a specific scenario reliably. Deliberate sequences drive exactly what raises a source — fill a FIFO to overflow, inject an error, complete a transfer — so the interrupt fires on demand, repeatably, and you can systematically trigger every source and close coverage. But the bigger reason is the corners. The hard interrupt scenarios — a higher-priority interrupt firing exactly during a lower one's service (nested), multiple sources asserting in the same cycle (simultaneous), a new event arriving just as the handler clears (clear race), a rapid storm — require precise timing alignment that random stimulus is structurally unlikely to produce. A high-priority interrupt firing in the narrow window during a low-priority service window almost never happens by chance, even in weeks of random stimulus. Yet these corners are exactly where the DUT's interrupt controller logic — priority, preemption, context save, arbitration — is most likely to be buggy. So if you rely on random, you get high source coverage, every interrupt triggered, which looks thorough, but zero interaction coverage, the corners never exercised, and a bug in nested or simultaneous handling ships unverified. The fix is directed sequences that force the corners by aligning the timing explicitly — trigger a low interrupt, wait until the handler is mid-service, then trigger a high one to force nesting. The mental model is fire drills: you don't wait for a real fire to test the response system; you deliberately trigger the alarms in specific patterns, especially the corner patterns that rarely happen but are where the system fails. So deliberate provocation gives repeatable, complete coverage and, critically, forces the timing-aligned corners that random structurally can't reach.

The corners are simultaneous, nested, back-to-back or storm, and clear race, and each exercises a distinct part of the DUT's interrupt logic that single isolated interrupts don't. Simultaneous: multiple sources assert at once, in the same cycle or overlapping. This exercises the priority and arbitration logic — which source does the handler service first — and the status-reads-all behavior — does the handler read the status register and find all pending sources, or does it miss some. A bug here services one and drops the others. Nested: a higher-priority interrupt fires while a lower-priority one is being serviced. This exercises preemption and context save — can a higher interrupt interrupt an in-progress handler, does the DUT save the lower handler's context before vectoring to the higher, and does it return correctly to finish the lower afterward. A bug here corrupts the lower service's context. Back-to-back and storm: interrupts in rapid succession. This exercises throughput — can the handler keep up with a flood, or does it drop interrupts when they come faster than it services. A bug here loses interrupts under load. Clear race: a new event arrives at the exact moment the handler clears a previous interrupt. This exercises the clear-set atomicity — if an event arrives as you clear, is it preserved and re-raised, or swallowed by the clear. A bug here loses an interrupt in the race. Why each matters is that the interrupt controller's complex logic — priority, masking, nesting, vectoring, atomic clear — is only exercised by these multi-interrupt and timing-aligned scenarios. A single isolated interrupt just tests the basic assert-service-clear flow, not the controller's hard logic. And these corners require precise timing alignment that random stimulus rarely produces, so they're both where the bugs are and what random misses. So you force each with a directed sequence: fork multiple triggers for simultaneous, trigger during service for nested, rapid succession for storm, an event in the clear window for clear race. And you cover the interactions, not just the sources, so closure requires the corners to be exercised. Each corner is a separate, important piece of interrupt verification that single interrupts and random stimulus leave untested.

You force a nested interrupt by triggering a lower-priority interrupt, waiting until the handler is mid-service of it, and then triggering a higher-priority interrupt exactly during that service window, so the precise timing alignment that random rarely produces is created deliberately. The nested scenario is a higher-priority interrupt firing while a lower-priority one is being serviced, which requires the high interrupt to fire in the narrow window after the low interrupt has been raised and the handler has started servicing it but before the service completes. Random stimulus almost never aligns this, so you create it explicitly. The sequence does three things. First, trigger the low-priority interrupt — drive its condition so it's raised, and the forked handler starts servicing it. Second, wait until the handler is mid-service of the low-priority interrupt — this is the alignment step, using a wait or synchronization on a signal or event that indicates the handler has begun servicing, like waiting until a servicing-low state or flag is active. This ensures you're inside the service window. Third, trigger the high-priority interrupt — drive its condition so it's raised exactly during the low-priority service. Now the nested scenario is forced: the high interrupt fires during the low service, and the DUT must preempt. The verification then checks the DUT preempts correctly — suspends the low service, saves its context, services the high, and returns to finish the low where it left off, not restarting and not losing the low. The key technique is the explicit wait that aligns the high trigger with the low service window — that's what converts a rare random coincidence into a deterministic, every-run scenario. You'd typically also cover the nested interaction with a coverage cross, like servicing-low crossed with high-interrupt-fired, so closure requires the nesting to have been exercised. And you might parameterize the sequence to nest at different points in the service, or nest multiple levels deep, to stress the preemption logic further. So forcing nesting is about deliberate timing alignment: raise the low, wait until it's being serviced, then raise the high during that window, creating the precise overlap that exercises the DUT's preemption and context-save logic, which random can't reliably produce.

Because the handler's register accesses to service the interrupt — reading the status register, writing the clear register — share the same bus as the main stimulus's transactions, so without coordination they collide, corrupting transactions or deadlocking; you coordinate them through a virtual sequencer that arbitrates the shared bus. When an interrupt fires, the handler doesn't service it in isolation — it issues bus transactions to read which source is pending and to write the clear. Meanwhile, the main stimulus is also driving bus transactions, the normal traffic. Both use the same bus. If the handler just forked off and drove the bus directly, independent of the main stimulus, the two streams of bus accesses would interleave uncontrolled. A main transaction in progress could be interrupted by a handler access, corrupting it; or both could try to drive the bus at once, causing a protocol violation; or both could wait for the bus, deadlocking. So you need arbitration. The solution is a virtual sequencer that orchestrates the concurrent flows. The main stimulus runs as a sequence, the interrupt triggers run as sequences, and the handler runs as a sequence, all on sequencers the virtual sequencer controls, and they ultimately funnel to the bus sequencer. The virtual sequencer arbitrates access to the bus, interleaving the handler's status-read and clear-write with the main traffic cleanly — granting the handler a window to service between or with priority over main transactions, then resuming the main stimulus. This mirrors the real system: a CPU services an interrupt between or preempting its main program, sharing the same memory and bus, with the interrupt controller and bus arbitration managing the access. The reason the handler is a sequence on a sequencer rather than a free-running fork driving the bus is exactly this — it goes through the arbitration rather than racing the main stimulus. Practically, you decide the policy — does interrupt service preempt main traffic, or wait for a transaction boundary — and the virtual sequencer enforces it. So coordination is necessary because interrupt service competes for the bus with the main stimulus, and you achieve it with a virtual sequencer that arbitrates the shared bus so the handler's accesses interleave with the main traffic without collision.

Because source coverage — every interrupt source triggered — is necessary but not sufficient; it doesn't measure the interaction corners — nested, simultaneous, clear-race — where the DUT's interrupt logic is most likely buggy and where random stimulus structurally fails to reach. Interrupt-source coverage tells you each interrupt source was raised at least once, which confirms the basic assert-service-clear flow works for each source individually. That's important, but it's only the baseline. The complex, bug-prone part of an interrupt controller is the multi-interrupt and timing-aligned logic: priority and arbitration when multiple sources fire at once, preemption and context save when a higher interrupt nests within a lower one's service, throughput under a storm, and atomic clear when an event races a clear. None of these interactions is measured by source coverage — you can have every source triggered many times, high source coverage, while never once having two sources fire simultaneously or a high interrupt nest within a low service. The classic failure is exactly this: weeks of random stimulus achieve high source coverage and the team believes interrupts are verified, but a nested-interrupt preemption bug ships because the nested scenario never occurred — the precise timing of a high interrupt during a low service never aligned by chance. So source coverage gave false confidence. For real interrupt closure, you need interaction coverage — coverage items and crosses for the corners: a cross of servicing-low and high-interrupt-fired for nesting, a coverpoint for simultaneous-multiple-sources, items for back-to-back and clear-race. And you need directed sequences that force these interactions, because random can't produce them, so the coverage would never close on its own. This parallels the general coverage lesson — passing tests or high coverage on the easy cases isn't done when the corners are unexercised — applied to interrupts. So interrupt closure requires both source coverage, each interrupt exercised, and interaction coverage, each corner scenario exercised, with directed sequences forcing the corners and the interaction crosses required for sign-off. High source coverage alone leaves the interaction corners, the bug-prone part, unverified, which is why it's not enough.

Exercises

  1. Trigger a source. Write a sequence that deliberately raises a FIFO-overflow interrupt by driving its condition.
  2. Force simultaneity. Show how to trigger two interrupt sources in the same window to test prioritization.
  3. Align a nest. Describe the wait-and-trigger steps to fire a high-priority interrupt during a low one's service.
  4. Cover the interaction. Explain why source coverage misses nesting and what coverage item would require it.

Summary

  • Interrupt sequences are the deliberate stimulus that provokes and stresses interrupts: provoke deliberately (drive the conditions that raise each source — fill a FIFO, inject an error, complete a transfer — rather than waiting for chance, for repeatable, complete coverage).
  • Coordinate the handler with the main stimulus: the handler's register accesses (status read, clear write) share the bus with the main traffic, so a virtual sequencer orchestrates and arbitrates the triggers, the main stimulus, and the handler so they don't collide.
  • Force the corner cases: nested (a higher-priority interrupt during a lower one's service → preempt/return), simultaneous (multiple sources at once → read all + prioritize), back-to-back/storm (rapid succession → keep up), and clear race (an event at clear → not lost) — each exercises a distinct part of the interrupt logic.
  • The cardinal lesson: random stimulus rarely aligns the corner timings (a high interrupt exactly during a low service, two sources in one cycle), so high interrupt-source coverage is not interrupt closure — the interaction corners, where the bugs hide, need directed sequences and interaction coverage.
  • The durable rule of thumb: provoke interrupts deliberately by driving the conditions that raise each source, coordinate the handler with the main stimulus through a virtual sequencer arbitrating the shared bus, and force every corner — nested, simultaneous, storm, clear-race — with directed sequences that align the precise timing, because random stimulus structurally rarely produces the interrupt corners where the DUT's interrupt logic is most likely buggy, so high source coverage with no directed corner sequences ships the nested and simultaneous bugs random never aligned; cover the interactions, not just the sources.

Next — Interrupt Checking: with interrupts modeled and provoked, the next chapter verifies them — the checking side. How to confirm an interrupt fired exactly when it should (and not when it shouldn't), that the status register reports the right source, that servicing clears it and the line deasserts, that priority and nesting are honored, and how to build the scoreboard and assertions that check the full interrupt behavior against the specification.