Handling interrupts in UVM sequences means running an interrupt service routine (ISR) sequence whenever the DUT raises an interrupt, while other stimulus is already running. A control sequence watches the interrupt line through a configuration object, starts the ISR sequence when it fires, and the ISR uses grab() or a higher priority to get to the driver ahead of the normal traffic.

An interrupt is a sideband event: it is not part of the bus protocol that the agent drives, and it can arrive at any moment. A sequence cannot see signals by itself, so the testbench needs a way to wait for the interrupt from inside a sequence and a way to make the ISR's bus accesses jump the queue, just as a CPU drops what it is doing to service an interrupt.

8 min read | Intermediate

What You'll Learn

  • Let sequences wait for DUT signals through a configuration object
  • Write an ISR sequence that grabs the sequencer and clears the interrupt
  • Run normal traffic and interrupt handling in parallel from one control sequence
  • Choose between exclusive, prioritized and signal-triggered handling

Waiting for a Signal from a Sequence

Sequences are objects, not components, and have no virtual interface of their own. The usual solution is to put a few small waiting tasks in the agent's configuration object, which already holds the virtual interface, and let sequences call them:


class irq_cfg extends uvm_object;
  `uvm_object_utils(irq_cfg)
  virtual irq_if vif;
  // Rest of the code (constructor)

  // Returns at the first clock edge where the interrupt line is high
  task wait_for_irq();
    @(posedge vif.clk iff vif.irq);
  endtask

  // Lets sequences wait in clock cycles instead of absolute time
  task wait_clocks(int unsigned n);
    repeat (n) @(posedge vif.clk);
  endtask
endclass

The test creates the object, sets its interface handle and puts it in the configuration database. A sequence fetches it using its sequencer as the context, because the sequencer is a component with a place in the hierarchy:


irq_cfg m_cfg;
if (!uvm_config_db #(irq_cfg)::get(m_sequencer, "", "irq_cfg", m_cfg))
  `uvm_fatal("CFG", "irq_cfg not found")

Waiting in clock cycles through wait_clocks() is also the better way to add delays in a sequence. A sequence that uses #100ns breaks as soon as the clock frequency changes.

The ISR Sequence

The ISR behaves like interrupt-handling software: read the interrupt status register to find the source, deal with it, and clear it. The grab() at the start gives it the very next slot on the sequencer, and nothing else reaches the driver until ungrab():


class isr_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(isr_seq)
  // Rest of the code (constructor)

  virtual task body();
    bit [31:0] status;
    grab();                                         // take over the sequencer now

    req = bus_item::type_id::create("req");
    start_item(req);
    if (!req.randomize() with { write == 0; addr == 8'hF0; }) `uvm_error("ISR", "Randomization failed")
    finish_item(req);
    status = req.rdata;                             // filled in by the driver before item_done()

    if (status[0]) `uvm_info("ISR", "DMA done interrupt", UVM_MEDIUM)
    if (status[1]) `uvm_info("ISR", "FIFO overflow interrupt", UVM_MEDIUM)

    req = bus_item::type_id::create("req");
    start_item(req);
    if (!req.randomize() with { write == 1; addr == 8'hF0; data == status; }) `uvm_error("ISR", "Randomization failed")
    finish_item(req);                               // write-1-to-clear every pending source

    ungrab();                                       // normal traffic may continue
  endtask
endclass

Reading req.rdata right after finish_item() works because this driver fills in the read data before it calls item_done(). With a driver that returns results as responses instead, the ISR would call get_response(), as described in UVM Sequence Responses. How grab() differs from lock() is covered in UVM Lock, Grab and Stopping Sequences.

The Control Sequence

A control sequence ties everything together. It starts the normal traffic in one thread and an interrupt watcher in another. The watcher loops forever: wait for the interrupt, run the ISR, wait again.


class irq_test_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(irq_test_seq)
  bit isr_active;
  // Rest of the code (constructor)

  virtual task body();
    irq_cfg     m_cfg;
    traffic_seq traffic = traffic_seq::type_id::create("traffic");
    isr_seq     isr     = isr_seq::type_id::create("isr");

    if (!uvm_config_db #(irq_cfg)::get(m_sequencer, "", "irq_cfg", m_cfg))
      `uvm_fatal("CFG", "irq_cfg not found")

    fork
      traffic.start(m_sequencer, this);             // the main activity
      forever begin                                 // the interrupt watcher
        m_cfg.wait_for_irq();
        isr_active = 1;
        isr.start(m_sequencer, this);
        isr_active = 0;
      end
    join_any                                        // the traffic has finished

    wait (!isr_active);                             // let a running ISR complete
    disable fork;                                   // then stop the watcher
  endtask
endclass

The order at the end matters. join_any returns when the traffic sequence finishes, while the watcher is still running. Killing it straight away with disable fork could stop an ISR in the middle of its work while it still holds the grab, which would leave the sequencer blocked. Waiting for isr_active to clear first makes sure every started ISR completes and releases the sequencer.

Because the ISR clears the interrupt before it finishes, the interrupt line is low again by the time the watcher waits for the next one. If the DUT needs a few cycles to drop the line after the clear, add a short wait_clocks() after the ISR, or wait for the falling edge, so the same interrupt is not serviced twice.

Quick Check

Q: Why does the watcher thread start the ISR with this as the parent sequence?

Show Answer

So the ISR runs as a child of the control sequence. It appears under it in the sequence hierarchy and in transaction recordings, and it inherits the control sequence's priority unless one is given. The grab works either way, because it is requested by the ISR on the shared sequencer.

Other Ways to Handle Interrupts

Prioritized ISRs

Instead of grabbing the sequencer, the watcher can start the ISR with a higher priority, for example isr.start(m_sequencer, this, 500), on a sequencer whose arbitration mode honours priority, such as UVM_SEQ_ARB_STRICT_FIFO. This is less disruptive and can model nested interrupts, with a high-priority ISR overtaking a lower one.

The catch is that priority only decides who wins each arbitration round. The ISR's items are not guaranteed to be back to back, and items from a lower-priority ISR or from the traffic can still slip in between when the ISR is not requesting. Use priority when that is acceptable, and grab when the ISR's accesses must not be interleaved.

Signal-triggered sequences

The same watcher pattern works for any sideband event, not just interrupts. A testbench can wait for a FIFO almost-empty flag and then start a refill sequence, or wait for a DMA-done pulse and start a sequence that checks the results. The trigger doesn't need to grab anything if the triggered sequence can simply take its turn.

If the DUT has an interrupt controller, the ISR should behave like software does: read the controller to find the source, service it, and clear it through the controller. That way the test also verifies the controller's prioritisation and masking, instead of bypassing them.

Common Mistakes

Mistake #1: Killing the watcher while an ISR holds a grab

A disable fork that stops an ISR between grab() and ungrab() leaves the sequencer grabbed, and every later sequence on it hangs. Let a running ISR finish before disabling the watcher.

Mistake #2: Not clearing the interrupt

If the ISR doesn't clear the source, the level-sensitive interrupt line stays high and the watcher immediately starts the ISR again, in an endless loop.

Mistake #3: Absolute delays in sequences

# delays tie a sequence to one clock frequency. Wait in clock cycles through a task in the configuration object instead.

Mistake #4: Using priority when the ISR must be atomic

Priority does not reserve the sequencer between the ISR's items. If the read-and-clear must happen without anything in between, use grab().

Summary

In this article, you learned:

  • Sequences wait for DUT signals through tasks in a configuration object that holds the virtual interface
  • An ISR sequence grabs the sequencer, reads the status, clears the interrupt and ungrabs
  • A control sequence runs the traffic and an interrupt watcher in parallel
  • A running ISR must finish before the watcher is disabled, or the sequencer stays grabbed
  • Priority-based ISRs are less disruptive but not atomic; grabs are atomic
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5