Skip to content

UVM

Interrupt Modeling

Representing an interrupt and its handling in a UVM testbench — the source, the line, and its assertion; level versus edge interrupts; the status-service-clear-deassert flow; and structuring a concurrent observe-and-react environment (a monitor watching the line plus a forked handler) because interrupts are asynchronous and fire independent of the main stimulus — never polled in the main sequence.

Interrupt Verification · Module 25 · Page 25.1

The Engineering Problem

A transaction you drive and wait for — but an interrupt is different: the DUT raises it whenever an event occurs — asynchronously, concurrently with whatever the testbench is doing. The DUT asserts an interrupt line to demand attention, and the testbench must notice, service the source (read which interrupt fired, handle it), and clear it so the line deasserts. The challenge is the asynchrony: you can't model an interrupt the way you model a transaction — you can't "drive an interrupt and wait for the response," because the interrupt fires on the DUT's schedule, not yours. If you poll the line in your main sequencechecking it between transactions — you'll miss interrupts that fire and clear between checks, and you'll service them late (or stall the design) when the main sequence is busy. And the line's behaviorlevel-sensitive (stays high until cleared) versus edge-sensitive (a pulse) — must be modeled correctly, or the handling flow breaks. The problem this chapter solves is interrupt modeling: how to represent the interrupt source and line, observe its assertion, model level versus edge, capture the status→service→clear→deassert flow, and structure a concurrent observe-and-react environment — because interrupts are asynchronous, not driven.

Interrupt modeling is representing the interrupt mechanism in the testbench: the source (what raises it), the line (the signal the DUT asserts), its assertion (level: stays high until cleared; edge: a pulse), and the status→service→clear→deassert flow (the DUT sets a status bit and asserts the line; the handler reads the status register to find which source, services it, and writes the clear register to deassert). Because interrupts are asynchronous and concurrent — they fire whenever the condition occurs, independent of the main stimulus — the testbench needs a concurrent observe-and-react structure: a monitor (or interrupt monitor) watches the interrupt line in the virtual interface and detects assertion, broadcasting an interrupt event; and a handler (a forked sequence/component) reactswhenever the line asserts, it services and clears the interrupt. The cardinal structural rule: watch the line with a concurrent handler, never poll it in the main sequence — polling misses interrupts that fire-and-clear between checks and services late when the main sequence is busy. And level versus edge must be modeled correctlylevel needs clear-to-deassert, edge needs detect-the-edge-and-clear-pending. This chapter is interrupt modeling: the source/line/assertion, level vs edge, the status/clear flow, and the concurrent structure.

How do you model an interrupt in a UVM testbench — represent the source and line, observe its assertion, handle level versus edge, capture the status-service-clear-deassert flow, and structure a concurrent observe-and-react environment because interrupts are asynchronous and must not be polled in the main sequence?

Motivation — why interrupts can't be modeled like transactions

An interrupt breaks the drive-and-wait model that works for transactions — and forcing it into that model fails. The reasons:

  • Interrupts are asynchronous to the stimulus. A transaction happens because you drove it; an interrupt happens because a condition occurred in the DUT — on the DUT's schedule, whenever. You can't "drive an interrupt" and wait — it fires when it fires.
  • They're concurrent with whatever the testbench is doing. An interrupt can fire while the main sequence is mid-transaction, waiting, or idle. So you can't handle it in the main flow — you need a parallel watcher that reacts independently of the main sequence's state.
  • Polling misses and delays. Checking the line in the main sequence (between transactions) misses interrupts that fire and clear between checks, and services them late (only when the main sequence gets around to checking) — wrong for a real-time mechanism.
  • Level versus edge is a modeling decision. A level interrupt stays high until cleared — so servicing must clear it; an edge interrupt is a pulse — so you must detect the edge (a level check misses a deasserted pulse). Mis-modeling the type breaks the handling.
  • The status/clear flow is the protocol. Real interrupts have a register protocol: status (which sources pending), clear (deassert). Modeling the interrupt means modeling this flowread status, service, clear, observe deassert — not just the line.

The motivation, in one line: an interrupt is asynchronous and concurrent (fires on the DUT's schedule, independent of the stimulus), so it can't be modeled like a driven transactionpolling it in the main sequence misses and delays, and the level/edge type and the status/clear flow are modeling decisions — so interrupt modeling requires a concurrent observe-and-react structure (a monitor watching the line + a forked handler) that services it whenever it fires.

Mental Model

Hold an interrupt as a doorbell that rings asynchronously — you post a dedicated listener that, the instant it rings, answers, deals with whoever's there, and resets the bell, rather than checking the door occasionally while doing chores:

An interrupt is a doorbell. Someone can ring it at any moment, while you're in the middle of a chore, on the phone, or idle — it's asynchronous to whatever you're doing. The wrong way to handle a doorbell is to check the door occasionally between chores: if someone rings and leaves while you're busy, you miss them, and even if they wait, you answer late. The right way is to post a dedicated listener whose only job is to listen for the bell, and the instant it rings, answer the door — find out who's there (read the status: which interrupt source is pending), deal with them (service the source), and reset the bell so it stops (clear the interrupt so the line deasserts). And the bell's behavior matters. A level doorbell holds the button down and keeps ringing until you physically reset it — so it stays ringing until you answer and clear it, and if you never clear it, it rings forever (and the visitor — the design — waits, blocked). An edge doorbell gives one ring per press and stops on its own — so you must catch each ring as it happens, because if you're not listening at that instant, the ring is gone. So you model an interrupt by posting a listener on the bell line that watches for the assertion, and a responder that, whenever the bell rings, answers, services, and resets it — running concurrently with everything else, because the bell rings on the visitor's schedule, not yours. Picture an interrupt as a doorbell. Someone can ring it at any moment — while you're mid-chore, on the phone, or idle — it's asynchronous to whatever you're doing. The wrong way is to check the door occasionally between chores: if someone rings and leaves while you're busy, you miss them, and even if they wait, you answer late. The right way is to post a dedicated listener whose only job is to listen for the bell, and the instant it rings, answer the doorfind out who's there (read the status: which source is pending), deal with them (service the source), and reset the bell (clear the interrupt → the line deasserts). And the bell's behavior matters: a level doorbell holds the button down and keeps ringing until you reset it (stays ringing until answered and cleared; if never cleared, it rings forever and the visitor — the design — waits, blocked); an edge doorbell gives one ring per press and stops on its own (you must catch each ring as it happens, or it's gone). So you model an interrupt by posting a listener on the bell line (a monitor) that watches for the assertion, and a responder (a handler) that, whenever the bell rings, answers, services, and resets it — running concurrently with everything else, because the bell rings on the visitor's schedule, not yours.

So an interrupt is a doorbell that rings asynchronously: you post a dedicated listener (monitor on the interrupt line) that detects the ring (assertion), and a responder (forked handler) that, whenever it rings, answers (reads the status — which source), deals with whoever's there (services), and resets the bell (clears → deasserts). It runs concurrently (the bell rings on the DUT's schedule), not by checking occasionally (polling — which misses and delays). And the bell type matters: level (keeps ringing until reset — must clear), edge (one ring per press — must catch the instant). Post a dedicated listener on the interrupt line and a responder that answers, services, and resets it whenever it rings — never check the door occasionally.

Visual Explanation — the interrupt mechanism modeled

The defining picture is the flow: source raises → line asserts → monitor detects → handler services (read status, clear) → line deasserts.

Interrupt flow: source raises, monitor detects, handler reads status/services/clears, line deassertssource raises (sets status, asserts line) → monitor detects assertion → handler reads status (which source) + services + clears → line deassertssource raises (sets status, asserts line) → monitor detects assertion → handler reads status (which source) + services + clears → line deasserts1Source raises the interruptan event in the DUT sets a status bit and asserts the interruptline — asynchronously to the stimulus.2Monitor detects the assertiona monitor watching the interrupt line detects the assertion andbroadcasts an interrupt event.3Handler reads status, services, clearsthe handler reads the status register (which source is pending),services it, and writes the clear register.4Line deassertsclearing the source causes the interrupt line to deassert — theservice flow completes.
Figure 1 — the interrupt mechanism, modeled end to end. The interrupt source in the DUT raises an event, setting a status bit and asserting the interrupt line. A monitor watching the line detects the assertion and broadcasts an interrupt event. A handler reacts: it reads the interrupt status register to find which source is pending, services that source, and writes the clear register. Clearing the source causes the line to deassert. The testbench models this full status-service-clear-deassert flow, observing the line and reacting concurrently, not by polling.

The figure shows the interrupt mechanism, modeled end to end. Raise (step 1): an event in the DUT sets a status bit and asserts the interrupt lineasynchronously to the stimulus. Detect (step 2): a monitor watching the interrupt line detects the assertion and broadcasts an interrupt event. Service (step 3): the handler reads the status register (which source is pending), services it, and writes the clear register. Deassert (step 4): clearing the source causes the interrupt line to deassert — the service flow completes. The crucial reading is the full flow the testbench must modelnot just "is the line high?" but the entire raise→detect→service→clear→deassert protocol. The source raises (an internal DUT event — a FIFO full, a transfer done, an error); the line asserts (the observable signal); the monitor detects (the testbench's eye on the line); the handler services (the register protocol: read status to find which source, handle it, write clear); and the line deasserts (the response completes). This models the real hardware/firmware interrupt-service routine: something raises an interrupt, the CPU reads which device, services it, and clears it. The brand-colored raise feeds the success-colored detect/service, yielding the default-colored deassert. The key structural fact (detailed next) is that steps 2–3 (detect, service) run concurrently with the main stimulus — the monitor watches always, the handler reacts whenever — because step 1 (the raise) happens asynchronously. The diagram is the interrupt protocol: raise (status+line) → detect (monitor) → service (read status, clear) → deassert (line) — the full mechanism the testbench models and verifies. Model the full flow — source raises and asserts, monitor detects, handler reads status and clears, line deasserts.

RTL / Simulation Perspective — the monitor and the forked handler

In code, interrupt modeling is a monitor watching the line and a forked handler servicing it — concurrently with the main stimulus. The example shows both, and the level-vs-edge detection.

an interrupt monitor (watch the line) and a forked handler (service + clear), concurrent
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// === INTERRUPT MONITOR: watch the line, detect assertion, broadcast (in a forever loop) ===
class irq_monitor extends uvm_monitor;
  virtual irq_if vif;  uvm_analysis_port#(irq_txn) ap;
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.irq);            // LEVEL: detect assertion (edge of the level going high)
      // for EDGE-sensitive: @(posedge vif.irq_pulse) — catch the pulse the instant it occurs
      irq_txn t = irq_txn::type_id::create("t");
      ap.write(t);                   // broadcast: "an interrupt asserted"
      @(negedge vif.irq);            // observe deassert (after the handler clears it) — LEVEL behavior
    end
  endtask
endclass
 
// === HANDLER: react WHENEVER the interrupt fires — read status, service, clear (FORKED, concurrent) ===
task irq_handler_seq::body();
  forever begin
    wait (vif.irq == 1);             // wait for the interrupt (concurrent with main stimulus)
    bit [31:0] status;
    reg_read(STATUS_REG, status);    // read WHICH source is pending
    service_source(status);          // handle it
    reg_write(CLEAR_REG, status);    // write-1-to-clear → LEVEL interrupt DEASSERTS
    // (for EDGE: the pending bit is cleared; the line was already a pulse)
  end
endtask
 
// === STRUCTURE: the handler runs CONCURRENTLY with the main stimulus (forked) ===
task my_test::run_phase(uvm_phase phase);
  fork
    main_stimulus_seq.start(sqr);    // the normal transactions
    irq_handler_seq.start(irq_sqr);  // the interrupt handler — INDEPENDENT, reacts whenever IRQ fires
  join_any
endtask
 
// ✗ MISTAKE: poll the IRQ in the main sequence (between transactions) → miss/late interrupts (DebugLab)
//   repeat(N) begin drive_txn(); if (vif.irq) handle(); end   // ← misses IRQs between txns!

The code shows the concurrent observe-and-react structure. The interrupt monitor (irq_monitor) watches the line in a forever loop: @(posedge vif.irq) detects the assertion (for level, the edge of the level going high; for edge-sensitive, @(posedge vif.irq_pulse) catches the pulse), broadcasts an irq_txn ("an interrupt asserted"), and @(negedge vif.irq) observes the deassert (after the handler clears it — level behavior). The handler (irq_handler_seq) reacts whenever the interrupt fires: wait(vif.irq == 1) (concurrent with main stimulus), then reads the status register (which source), services it, and writes the clear register (write-1-to-clear → the level interrupt deasserts). The structure forks the handler concurrently with the main stimulus (fork main_stimulus_seq...; irq_handler_seq...; join_any) — so the handler is independent and reacts whenever the IRQ fires. The mistake (commented) is polling the IRQ in the main sequence (repeat(N) begin drive_txn(); if (vif.irq) handle(); end) — which misses IRQs between transactions (the DebugLab). The shape to carry: interrupt modeling is (1) a monitor that watches the line (detects assertion, observes deassert) and (2) a forked handler that reacts whenever the interrupt fires (read status, service, clear) — both concurrent with the main stimulus. The forever loop in the monitor and the forked handler are what make it concurrent — the monitor always watching, the handler always ready — versus polling in the main sequence (which only checks when the main flow reaches the check). The level-vs-edge shows in the detection (@(posedge irq) for level, @(posedge irq_pulse) for edge) and the clear (level deasserts on clear; edge's pending clears). Watch the line in a monitor's forever loop and fork a handler that services whenever the interrupt fires — never poll in the main sequence.

Verification Perspective — the concurrent structure

The defining structural choice is concurrency: the interrupt handler runs parallel to the main stimulus, not within it. Seeing the structure clarifies why polling fails.

Concurrent interrupt handler (monitor + handler, parallel) versus polling in the main sequenceasserts(asynchronously)detects → reactsparallel to main stimulusparallel tomain…or poll in main seq?miss / lateInterrupt lineasserts wheneverInterrupt monitorwatches continuouslyHandler (forked)reacts the instant it firesConcurrent withstimulusindependent of main seqPoll in main sequencecheck between txnsMissed / serviced latefires between checks or whilebusy12
Figure 2 — the concurrent interrupt structure versus polling. In the correct structure, the interrupt monitor and handler run concurrently with the main stimulus: the monitor watches the line continuously, and the handler reacts the instant the interrupt fires, independent of what the main sequence is doing. In the polling anti-pattern, the main sequence checks the interrupt line itself between transactions, so an interrupt that fires while the main sequence is mid-transaction is serviced late, and one that fires and clears between checks is missed entirely. Interrupts are asynchronous, so they need a concurrent watcher, not a check folded into the main flow.

The figure shows the concurrent structure versus polling. In the correct structure, the interrupt monitor and handler run concurrently with the main stimulus: the monitor watches the line continuously, and the handler reacts the instant the interrupt fires, independent of what the main sequence is doing (the concurrent path). In the polling anti-pattern, the main sequence checks the interrupt line itself between transactions — so an interrupt that fires while the main sequence is mid-transaction is serviced late, and one that fires and clears between checks is missed entirely (the warning/default-colored poll→miss path). The verification insight is that interrupts are asynchronous, so they need a concurrent watcher, not a check folded into the main flow. The correct structure decouples the interrupt handling from the main stimulus's timing: the monitor is always watching (a forever loop), and the handler is always ready (forked, waiting on the line) — so whenever the interrupt fires, it's detected and serviced immediately, regardless of where the main sequence is. The polling structure couples them: the interrupt check only happens when the main sequence reaches the check — so if the main sequence is blocked (mid-transaction, waiting), the interrupt waits too (late), and if the interrupt fires-and-clears (a pulse, or a level cleared by something else) between checks, it's never seen (missed). The brand/success-colored concurrent structure (monitor + forked handler, parallel) is correct; the warning/default-colored polling is the anti-pattern. This is the fundamental structural lesson of interrupt verification: the handler must be concurrent. It mirrors the real system — a CPU doesn't poll for interrupts in its main loop; an interrupt controller asynchronously vectors to a handler. The testbench models this with a forked, concurrent handler. The diagram is the structure: interrupt line → (monitor + forked handler, concurrent with stimulus) is correct; interrupt line → polled in main sequence → missed/late is wrong. Run the interrupt monitor and handler concurrently with the main stimulus — polling in the main sequence misses and delays asynchronous interrupts.

Runtime / Execution Flow — level versus edge interrupts

At run time, level and edge interrupts behave differently — and modeling each correctly matters. The flow shows the two assertion/clear behaviors.

Level interrupt: stays high until cleared; edge interrupt: pulse, detect and clear pendingstays high until clearedstays highuntil…handler must clear → deassertshandler mustclear →…a pulse, deasserts on its owna pulse,deasserts on…must detect the edge + clear pendingmust detect theedge + clear…Level interruptstays assertedHolds high until cleareddesign waits if unclearedClear → deassertsread status, write clearEdge interrupta pulseDeasserts on its owna level check misses itDetect edge + clearpendingcatch the instant12
Figure 3 — level versus edge interrupts, and how each is modeled. A level interrupt asserts and stays high until the handler clears the source; the handler must read the status and write the clear register, and only then does the line deassert — if it's never cleared, it stays asserted and the design waits. An edge interrupt is a pulse: it asserts for a moment on the triggering event and deasserts on its own; the handler must detect the edge the instant it occurs and clear a pending bit, because a later level check would miss the already-deasserted pulse. Modeling the wrong type breaks the handling: treating level as edge leaves it uncleared, treating edge as level misses it.

The figure shows level versus edge interrupts. A level interrupt asserts and stays high until the handler clears the source: the handler must read the status and write the clear register, and only then does the line deassertif never cleared, it stays asserted and the design waits. An edge interrupt is a pulse: it asserts for a moment on the triggering event and deasserts on its own; the handler must detect the edge the instant it occurs and clear a pending bit, because a later level check would miss the already-deasserted pulse. The runtime insight is that the type determines both the detection and the clearing, and modeling the wrong type breaks the handling. For level: detection is "is the line high?" (it stays high, so you can check it), and clearing is essential (the line won't deassert until you clear the source — so an unmodeled or missing clear leaves the line stuck high, and the design may wait forever for service). For edge: detection must catch the edge (the pulse@(posedge) — because a level check "is the line high?" misses it once it's deasserted), and clearing is a pending bit (the line was already a pulse; you clear the latched pending state). So treating level as edge → you detect the edge but don't clear the level → the line stays high (design hangs); treating edge as level → you check the level (wait(irq==1)) but the pulse already deasserted → you miss it. The warning-colored hold-until-cleared (level) and deasserts-on-its-own (edge) are the behaviors that drive the success-colored correct modeling (clear for level, detect-edge for edge). The diagram is the type distinction: level (stays high → must clear to deassert) versus edge (pulse → must detect the edge + clear pending) — and modeling each correctly is essential. Model level as stays-high-until-cleared (must clear to deassert) and edge as a pulse (must catch the edge) — mixing them up breaks the handling.

Waveform Perspective — a level interrupt's assert-service-clear-deassert

The level interrupt's full flow is visible on the line: it asserts, stays high while the handler reads status and clears, then deasserts — the complete protocol. The waveform shows it.

A level interrupt asserts, stays high while the handler services and clears it, then deasserts

12 cycles
A level interrupt asserts, stays high while the handler services and clears it, then deassertsevent raises the interrupt → irq asserts and STAYS high (level-sensitive)event raises the inter…handler reads the status register → which source is pending (rd_status)handler reads the stat…handler writes the clear register (wr_clear) after servicinghandler writes the cle…clearing the source → irq DEASSERTS; if never cleared, it would stay highclearing the source → …clkirqrd_statuswr_clearservicedt0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — a level interrupt's full flow. An event raises the interrupt: the line asserts (irq high) and stays high — it's level-sensitive. The handler, watching concurrently, detects the assertion and reads the status register to find which source is pending (rd_status). It services the source, then writes the clear register (wr_clear). Clearing the source causes the line to deassert (irq goes low). The line stayed asserted the whole time until cleared — if the handler had never cleared it, it would stay high and the design would keep waiting. The assert-service-clear-deassert sequence is the level interrupt protocol the testbench models.

The waveform shows a level interrupt's full flow. An event raises the interrupt: the line asserts (irq high) and stays high — it's level-sensitive. The handler, watching concurrently, detects the assertion and reads the status register to find which source is pending (rd_status). It services the source, then writes the clear register (wr_clear). Clearing the source causes the line to deassert (irq goes low), and the interrupt is serviced. The crucial reading is the level behavior: the line stays asserted the whole time (cycles 1–6) until cleared — it does not deassert on its own; it requires the handler's clear (wr_clear at cycle 5) to go low (cycle 7). This is the defining property of a level interrupt: it holds until serviced. If the handler had never cleared it, irq would stay high indefinitely, and the design would keep waiting for service (a hang, or repeated re-interruption). The sequenceassert (event) → read status (which source) → servicewrite cleardeassert — is the level interrupt protocol the testbench models. The concurrency is implicit here: the handler's rd_status/wr_clear happen while the main stimulus (not shown) continues — the handler reacted to the assertion independent of the main flow. The picture to carry is that a level interrupt is a held signal that the testbench must observe asserting, service through the status/clear registers, and observe deasserting — the full handshake, not just a pulse. Reading the waveform this way — does the line assert, stay high through the service, and deassert only after the clear? — is verifying the level interrupt flow. The line held high until the clear, then deasserting is the signature of a correctly-modeled level interrupt: assert → service → clear → deassert. A level interrupt holds asserted until the handler reads status, services, and clears it — then it deasserts; model the full assert-service-clear-deassert handshake.

DebugLab — the interrupts missed by polling in the main sequence

A test that missed interrupts because it polled the line between transactions instead of watching it concurrently

Symptom

A test was supposed to verify the DUT's error interrupt — an edge interrupt the DUT pulses when it detects a bad transaction. The test injected bad transactions and expected to count the resulting interrupts. But the interrupt count was wrongfewer interrupts handled than bad transactions injected — and intermittently, some runs handled all, others missed several. The handling code was in the main sequence: it drove a transaction, then checked if (vif.irq) handle_interrupt(), in a loop. When the team added waveform tracing, they saw the truth: the DUT was pulsing the interrupt correctly on every bad transaction — but the pulses (edge interrupts, one cycle wide) fired and deasserted while the main sequence was mid-transaction (driving the next one), so by the time the loop reached the if (vif.irq) check, the pulse was gonevif.irq was back to 0. The interrupts weren't missed by the DUT; they were missed by the polling test.

Root cause

The test polled the interrupt line in the main sequence (between transactions) instead of watching it with a concurrent handler — so edge interrupts that fired and deasserted between the polls were never seen:

why a polling test missed edge interrupts that the DUT fired correctly
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ POLL the interrupt in the main sequence (check between transactions):
  repeat (N) begin
    drive_txn(bad_txn);              // drive a transaction (takes several cycles)
    if (vif.irq) handle_interrupt(); // ← check the IRQ — but only HERE, between txns
  end
  // the DUT pulses irq (1 cycle) DURING drive_txn of the NEXT iteration
  //   → by the time the loop reaches `if (vif.irq)`, the pulse already DEASSERTED → MISSED
  // intermittent: depends on whether the pulse happens to overlap a check → non-deterministic
 
✓ WATCH the line with a CONCURRENT handler (forked, always ready):
  fork
    main_stimulus();                 // drive the bad transactions
    forever begin
      @(posedge vif.irq);            // catch the EDGE the INSTANT it fires (concurrent)
      handle_interrupt();            // service it — independent of the main stimulus
    end
  join_any
  // the handler is ALWAYS watching → every pulse is caught at the moment it asserts → none missed

This is the poll-instead-of-concurrent bug — the cardinal interrupt-modeling failure. The test polled the interrupt line in the main sequencechecking if (vif.irq) only between transactions, in a loop — instead of watching it with a concurrent handler. The DUT correctly pulsed the edge interrupt (one cycle wide) on every bad transaction, but the pulses fired and deasserted while the main sequence was mid-transaction (driving the next one), so by the time the loop reached the check, vif.irq was already back to 0 — the pulse was gone, the interrupt missed. The intermittency (some runs caught all, others missed) is the signature of a polling race: whether a pulse is caught depends on whether it happens to overlap a checknon-deterministic. The deep reason is the fundamental mismatch: interrupts are asynchronous and concurrent (they fire on the DUT's schedule), but polling checks them synchronously (only when the main flow reaches the check) — so anything that fires-and-clears between checks (an edge pulse, a fast level) is missed, and anything that fires while the main sequence is busy is serviced late. The fix is to watch the line with a concurrent handler: fork ... forever begin @(posedge vif.irq); handle_interrupt(); end ... join_any — a forked loop that catches the edge the instant it fires, independent of the main stimulus, so every pulse is caught and none missed. The general lesson, and the chapter's thesis: interrupts are asynchronous and concurrent, so you must watch the interrupt line with a concurrent monitor/handler — never poll it in the main sequencepolling misses interrupts that fire-and-clear between checks (especially edge pulses) and services late when the main sequence is blocked, and the resulting bug is intermittent and non-deterministic (a polling race); fork a dedicated handler that watches the line continuously and reacts the instant the interrupt asserts, modeling the real asynchronous interrupt-service structure. An interrupt fires on the DUT's schedule, not the testbench's — watch it with a concurrent handler, or a polling check between transactions will race the pulse and miss it.

Diagnosis

The tell is intermittent, non-deterministic missed interrupts. Diagnose polling instead of concurrent handling:

  1. Check where the interrupt is handled. Handling in the main sequence, checked between transactions, is polling — it misses interrupts that fire between checks.
  2. Confirm the DUT actually fired the interrupt. Waveform tracing showing the DUT pulsing the line, while the test missed it, points at the test's polling, not the DUT.
  3. Look for edge interrupts and short pulses. A one-cycle pulse is almost certain to be missed by polling; it must be caught at the instant it asserts.
  4. Recognize the intermittency as a race. Missing some interrupts but not others, run to run, is the signature of a polling race against asynchronous pulses.
Prevention

Watch the line concurrently, never poll:

  1. Fork a concurrent interrupt handler. A forked forever loop on the interrupt line catches every assertion the instant it fires, independent of the main stimulus.
  2. Catch edges at the assertion instant. Use @(posedge irq) to catch a pulse, not a periodic level check that can miss a deasserted pulse.
  3. Keep interrupt handling out of the main sequence. The main stimulus and the interrupt handler are separate concurrent flows, mirroring the real asynchronous mechanism.
  4. Verify against the DUT's actual assertions. Cross-check handled interrupts against the line's assertions on the waveform to confirm none are missed.

The one-sentence lesson: interrupts are asynchronous and concurrent, so you must watch the interrupt line with a concurrent monitor or forked handler that reacts the instant it asserts — never poll it in the main sequence between transactions, because polling races the asynchronous pulse and misses interrupts that fire and clear between checks (especially edge pulses) while servicing late when the main sequence is busy, producing intermittent, non-deterministic failures.

Common Mistakes

  • Polling the interrupt in the main sequence. Checking the line between transactions misses interrupts that fire and clear between checks; fork a concurrent handler.
  • Treating a level interrupt as edge. Detecting the edge but not clearing the source leaves the line stuck high and the design waiting; clear to deassert.
  • Treating an edge interrupt as level. Checking for a held level misses an already-deasserted pulse; catch the edge at the instant it asserts.
  • Not modeling the status/clear flow. Just watching the line isn't enough; model reading the status register, servicing, and writing the clear to deassert.
  • Servicing in the same thread as the main stimulus. The handler must be concurrent and independent, so an interrupt is serviced whenever it fires, not when the main flow gets around to it.
  • Ignoring the deassert. The interrupt isn't fully modeled until you observe the line deassert after the clear; verify the full handshake.

Senior Design Review Notes

Interview Insights

You model an interrupt with a concurrent observe-and-react structure — a monitor watching the interrupt line plus a forked handler that services it whenever it fires — and you can't treat it like a transaction because an interrupt is asynchronous, firing on the DUT's schedule rather than because you drove it. A transaction follows a drive-and-wait model: you drive it and wait for the response, because you initiated it. An interrupt is the opposite: the DUT raises it whenever an internal event occurs — a FIFO fills, a transfer completes, an error is detected — concurrently with whatever the testbench is doing. So you can't drive an interrupt and wait; it fires when it fires. The modeling has a few parts. The source is what raises the interrupt in the DUT. The line is the signal the DUT asserts. The assertion behavior is level — stays high until cleared — or edge — a pulse. And the handling is the status-service-clear-deassert flow: the DUT sets a status bit and asserts the line; the handler reads the status register to find which source is pending, services it, and writes the clear register, which causes the line to deassert. Because interrupts are asynchronous and concurrent, the testbench needs a concurrent structure. A monitor watches the interrupt line in a forever loop, detecting the assertion and broadcasting an interrupt event. A handler, forked to run concurrently with the main stimulus, waits on the line and, the instant it fires, reads the status, services, and clears. The critical rule is to watch the line with this concurrent handler, never poll it in the main sequence, because polling — checking the line between transactions — misses interrupts that fire and clear between checks and services them late when the main sequence is busy. The mental model is a doorbell: it rings asynchronously while you're doing chores, so you post a dedicated listener that answers the instant it rings, rather than checking the door occasionally. This mirrors the real system: a CPU doesn't poll for interrupts in its main loop; an interrupt controller asynchronously vectors to a handler. The testbench models that with a forked, concurrent handler, which is why you structure it as observe-and-react rather than drive-and-wait.

Because interrupts are asynchronous — they fire on the DUT's schedule, independent of the main stimulus — so a handler folded into the main sequence would only check for interrupts when the main flow reaches the check, missing interrupts that fire between checks and servicing late those that fire while the main sequence is busy. The fundamental issue is timing decoupling. The main stimulus has its own flow: driving transactions, waiting, idling. An interrupt can fire at any point in that flow, including while the main sequence is mid-transaction or blocked waiting for something. If the interrupt handling is part of the main sequence — say, checking the line between transactions — then the check only executes at those specific points. Two failures follow. First, missed interrupts: if an interrupt fires and deasserts between two checks — which is almost guaranteed for an edge interrupt that pulses for one cycle, or a level interrupt cleared quickly by something else — the polling check never sees it, because by the time the loop reaches the check, the line is back to its idle state. Second, late servicing: if an interrupt fires while the main sequence is blocked on a transaction, it isn't serviced until the main sequence completes that transaction and gets around to the check, which is too late for a real-time mechanism and can stall the design if it's a level interrupt the DUT is waiting to have serviced. A concurrent handler solves both. You fork a handler that runs in parallel with the main stimulus, with a forever loop that waits on the interrupt line. The instant the interrupt asserts, the handler wakes — regardless of what the main sequence is doing — and services it. So every interrupt is caught at the moment it fires, none are missed, and none are serviced late. The handler is always watching and always ready. This mirrors real hardware: interrupt handling is asynchronous, with a controller that vectors to a handler the moment an interrupt arrives, not a polled check in the CPU's main loop. The symptom of getting this wrong is intermittent, non-deterministic missed interrupts — whether a polled check catches an interrupt depends on whether the interrupt happens to overlap a check, which is a race. So concurrency isn't optional for interrupts; it's the structural requirement that matches their asynchronous nature.

A level interrupt stays asserted until the handler clears the source, so you model it by clearing to deassert; an edge interrupt is a pulse that deasserts on its own, so you model it by catching the edge at the instant it fires — and modeling the wrong type breaks the handling. A level-sensitive interrupt holds the line high from the triggering event until the source is cleared. So the DUT asserts it and keeps it asserted; the handler reads the status to find the source, services it, and writes the clear register, and only then does the line deassert. The key properties are that you can detect it by checking whether the line is high — it stays high, so a level check works — but you must clear it, because the line won't deassert on its own. If you never clear it, it stays asserted indefinitely, and the design may wait forever for service or keep re-interrupting. An edge-sensitive interrupt is a pulse: the line asserts for a moment on the triggering event — typically a rising or falling edge — and deasserts on its own. So you must detect the edge the instant it occurs, using an edge-sensitive construct like at-posedge, because a later level check would find the line already deasserted and miss it. The clearing is different too: the line was already a pulse, so you clear a latched pending bit rather than the line itself. Now the modeling errors. If you treat a level interrupt as edge — detecting the edge but not clearing the source — the line stays high because you never cleared it, and the design hangs waiting for service. If you treat an edge interrupt as level — checking for a held level with something like wait line equals one — you miss it, because the pulse already deasserted by the time you check, or you catch it only if you happen to check during the one-cycle pulse, which is a race. So the type determines both detection — level check versus edge catch — and clearing — clear the source to deassert versus clear a pending bit. You have to know which type the interrupt is and model it accordingly. In practice you read the spec to determine level versus edge, and you build the monitor and handler to match: a level interrupt's monitor watches the held line and the handler clears to deassert, while an edge interrupt's monitor catches the pulse and the handler clears the pending state.

The status-service-clear-deassert flow is the full interrupt protocol — the DUT sets a status bit and asserts the line, the handler reads the status register to find which source is pending, services that source, writes the clear register, and the line deasserts — and you model all of it because verifying interrupts means verifying the complete handshake, not just that the line went high. Real interrupts aren't just a line toggling; they're a register-based protocol. When an event occurs, the DUT sets a bit in an interrupt status register indicating which source fired, and asserts the interrupt line. There can be multiple sources sharing one line, so the line going high only says some interrupt fired, not which. The handler's job is to read the status register to determine which source or sources are pending — that's the status read. Then it services each pending source — doing whatever the interrupt requires, like reading data, handling an error, or acknowledging completion. Then it writes the clear register to clear the serviced sources — often a write-1-to-clear register, where writing a 1 to a bit clears that source's pending status. Clearing the source causes the line to deassert, for a level interrupt, completing the handshake. You model all of this for several reasons. First, completeness: the bug might be in any part — the status register reporting the wrong source, the clear not actually deasserting the line, a source that can't be cleared, or the line not deasserting after a clear. Just watching the line miss all of these. Second, the source identification: with multiple sources on one line, you must read the status to know which fired, and verify the DUT reported the right one. Third, the clear behavior: you verify that the clear actually works — that writing the clear register deasserts the line — which is a common bug, a clear that doesn't clear. Fourth, the deassert: the interrupt isn't fully handled until the line deasserts, so you observe that as the completion of the flow. So modeling the full flow lets you verify the whole interrupt protocol: the right source is reported, servicing happens, the clear works, and the line deasserts properly. A model that only watches the line would pass an interrupt that reported the wrong source or failed to clear, which are real, important bugs. So you model status read, service, clear write, and deassert observation as the complete handshake.

You structure it with a dedicated interrupt path — a monitor watching the interrupt line and a handler servicing it — running concurrently with the main stimulus, often organized as an interrupt agent or a forked handler sequence, mirroring the asynchronous hardware mechanism. The core components are a few. First, an interrupt monitor: a monitor connected to the interrupt line in the virtual interface, running a forever loop that detects the line asserting — by edge for an edge interrupt, by the level going high for a level one — and broadcasts an interrupt event or transaction on its analysis port, so the rest of the testbench knows an interrupt occurred. This gives observability of the raw interrupt assertions. Second, an interrupt handler: a component or sequence that reacts to the interrupt by performing the service flow — reading the status register to find the source, servicing it, and writing the clear. This is often a sequence running on a sequencer that can issue register accesses. Third, the concurrency structure: the handler runs concurrently with the main stimulus, typically by forking it in the test's run_phase alongside the main stimulus sequence, so the handler is always ready to react independent of the main flow. You might fork the handler and the main stimulus and join appropriately. The organization can be an interrupt agent — bundling the monitor and the handling — parallel to the functional agents, which is clean and reusable. The key structural principles are: the interrupt handling is a separate concurrent flow from the main stimulus, never folded into it; the monitor provides observability of assertions for checking; and the handler models the full status-service-clear-deassert protocol. For checking, you cross-reference the interrupts the DUT asserted, seen by the monitor, against the conditions that should have caused them and the handling that occurred, verifying every interrupt fired when it should, reported the right source, and was serviced and cleared properly. You also handle the interaction with the main stimulus — the interrupt service might need to pause or coordinate with other register traffic, which the concurrency must manage. So the environment has an interrupt observation path and a concurrent service path, structured to react to the asynchronous interrupts whenever they fire, which is the testbench analog of a hardware interrupt controller and service routine running alongside the main program. This sets up the later concerns — generating interrupt scenarios, checking the handling, and overall strategy — on a foundation of correct concurrent modeling.

Exercises

  1. Structure the handler. Sketch the fork that runs an interrupt handler concurrently with the main stimulus, watching the line.
  2. Model level versus edge. For a level interrupt and an edge interrupt, write the monitor's detection and explain the clearing for each.
  3. Trace the flow. Describe the status-service-clear-deassert sequence for a level interrupt with two sources on one line.
  4. Fix the polling bug. Given a main sequence that checks the IRQ between transactions, explain what it misses and rewrite it concurrently.

Summary

  • Interrupt modeling represents the interrupt mechanism: the source, the line, its assertion (level: stays high until cleared; edge: a pulse), and the status→service→clear→deassert flow (the DUT sets a status bit and asserts; the handler reads the status register for the source, services, writes the clear, and the line deasserts).
  • Because interrupts are asynchronous and concurrent (they fire on the DUT's schedule, independent of the main stimulus), the testbench needs a concurrent observe-and-react structure: a monitor watching the line that detects assertion, and a forked handler that services and clears the interrupt whenever it fires.
  • The cardinal rule: watch the line with a concurrent handler, never poll it in the main sequence — polling misses interrupts that fire-and-clear between checks (especially edge pulses) and services late when the main sequence is blocked, producing intermittent, non-deterministic failures.
  • Level versus edge must be modeled correctly: a level interrupt stays asserted until cleared (so the handler must clear it to deassert, or the line stays stuck high), while an edge interrupt is a pulse (so the handler must catch the edge at the instant it asserts and clear a pending bit).
  • The durable rule of thumb: model an interrupt as an asynchronous doorbell — post a concurrent monitor watching the interrupt line and a forked handler that, the instant the interrupt asserts, reads the status register for the source, services it, and clears it so the line deasserts; never poll the line in the main sequence (it races the asynchronous pulse and misses interrupts), model level-versus-edge correctly (clear-to-deassert for level, catch-the-edge for edge), and verify the full status-service-clear-deassert handshake against the DUT's actual assertions.

Next — Interrupt Sequences: with the interrupt modeled, the next chapter generates interrupt scenarios — the stimulus side. How to provoke interrupts deliberately (driving the conditions that raise them), coordinate interrupt-handling sequences with the main stimulus, model nested and simultaneous interrupts, and structure sequences that exercise the interrupt mechanism's corner cases rather than waiting for interrupts to happen by chance.