Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
    • RAL Overview
    • Register Access
    • Registers in Verification
      • Built-in Register Sequences and NO_* Attributes
      • Register Coverage
      • Memories in the Register Model
      • Multiple Maps and Address Spaces
      • Shadow, Alias and Paged Registers
      • Interrupts with the Register Model
      • The Register Model at SoC Scale
      • RAL Interview Set
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Registers in Verification
  1. Topics
  2. UVM Verification
  3. UVM Register Abstraction Layer (RAL)
  4. Registers in Verification
  5. Interrupts with the Register Model

Part 9 · Register Model (RAL) · Intermediate

Interrupts with the Register Model

An ISR sequence that reads W1C status through the model and clears what it read, explicit prediction during interrupt storms, backdoor waiting, and the update() trap.

On this page

    An interrupt handler sequence works for weeks. Then someone tidies it: after clearing STATUS, it calls regmodel.update() "to make sure the hardware matches the model". Under random traffic, interrupts start going missing, one in a few hundred. A scratch run on the DMA bench shows what that one line does even when nothing is pending: regmodel.update() wrote addr 04 1 time(s), data 6. It wrote 1 to both write-1-to-clear bits of STATUS. Had DONE or ERR been set at that moment, the write would have cleared it before any handler read it.

    Interrupt status is where the register model's assumptions are weakest: the hardware sets bits on its own, software clears them by writing ones, and several sequences touch the same register at unpredictable times. This lesson builds an interrupt service routine (ISR) sequence that reads and clears STATUS through the model, explains why the value it read is the only trustworthy one, and shows how to wait for a register to change without flooding the bus.

    The idea: a mailbox flag

    A mailbox flag goes up when the postman delivers, not when you look at it. To handle the mail you open the box (read), take out exactly what you found (clear what you saw), and close it. You never decide to put the flag down because your notebook says there should be no mail: the postman may have come while you were walking back. The DMA works the same way. STATUS.DONE and STATUS.ERR are set by the hardware and cleared by writing 1 (W1C); irq = IRQ_EN & (DONE | ERR), so the interrupt line stays high until both are cleared.

    In model terms, DONE and ERR are volatile: they change without a bus write, so the mirror cannot know their current value. Three rules follow, and every rule below is one of them applied:

    • The read's return value is the truth; the mirror is a stale note. Decide from the value read() returned.

    • Clear exactly what you read, with an explicit write() of those bits. A W1C write of a bit you did not see may clear an event that arrived after your read.

    • Never let generic code write status registers: update(), reset() followed by update(), or any "sync the model" helper.

    The picture

    Sequence diagram: the DUT raises irq, the test starts the ISR sequence, which reads STATUS through the model and gets 2, then writes 2 back to clear DONE; the predictor updates the mirror after each access; irq falls.
    The handler decides from the value its read returned. The predictor updates the mirror a moment after each access, so the mirror is never the place to make the decision.

    Build it: from raw APB to a register-model ISR

    Where the bench started

    The bench's raw-APB handler, used by dma_vseq, grabs the APB sequencer, reads STATUS and writes 0x6. The interrupt itself is detected by the interrupt agent, which triggers a uvm_event that the test or a virtual sequence waits on.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // snippet: from the course DMA bench
    // The interrupt handler: grab the APB sequencer so the handler's accesses go
    // ahead of any queued polling, read STATUS, clear DONE/ERR, then let go.
    class dma_irq_seq extends uvm_sequence #(apb_item);
      `uvm_object_utils(dma_irq_seq)
      bit [31:0] status;
    
      function new(string name = "dma_irq_seq");
        super.new(name);
      endfunction
    
      task access(bit [7:0] addr, bit wr, bit [31:0] data, output bit [31:0] rdata);
        apb_item it = apb_item::type_id::create("it");
        start_item(it);
        if (!it.randomize() with { addr == local::addr; write == wr; data == local::data; })
          `uvm_fatal("RAND", "apb_item randomize failed")
        finish_item(it);
        rdata = it.rdata;
      endtask
    
      task body();
        bit [31:0] unused;
        grab();                                  // jump the queue, hold the sequencer
        `uvm_info("IRQ", "handler has the APB sequencer", UVM_MEDIUM)
        access(STATUS, 0, 0, status);
        access(STATUS, 1, 32'h6, unused);        // write 1 to clear DONE and ERR
        ungrab();
      endtask
    endclass

    It works on the bench because one job runs at a time, but look at the last access: it clears DONE and ERR whatever it read. If ERR arrives between the read and the write, the handler destroys it unseen. That is the mailbox mistake in two lines.

    The ISR through the model

    The register-model version reads STATUS by name and writes back exactly the W1C bits it saw. It was verified in a scratch test on the bench:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // snippet: scratch ISR sequence
    class dma_ral_isr_seq extends uvm_sequence;
      `uvm_object_utils(dma_ral_isr_seq)
      dma_reg_block  rm;
      uvm_reg_data_t status;                 // what the handler saw, for the caller
      function new(string name = "dma_ral_isr_seq");
        super.new(name);
      endfunction
      task body();
        uvm_status_e st;
        rm.status.read(st, status, .parent(this));
        if (st != UVM_IS_OK) `uvm_error("ISR", "STATUS read failed")
        if (status & 32'h6)
          rm.status.write(st, status & 32'h6, .parent(this));   // W1C what was seen, nothing else
      endtask
    endclass

    Three details carry the design. The decision uses status, the value read() returned, never rm.status.done.get_mirrored_value(). The clear is an explicit write(), not set() plus update(). And passing .parent(this) makes the register accesses children of the ISR sequence, so if the handler must not be interleaved with other register traffic it can lock() or grab() the APB sequencer first, as dma_irq_seq does, and its own accesses still go through. Through the model that is two lines in the ISR's body(): regmodel.default_map.get_sequencer().lock(this); before the first access and ...unlock(this); after the last.

    Explicit prediction during an interrupt storm

    With several sequences polling, servicing and configuring at once, prediction must come from the bus. The explicit predictor sees every transfer, from the ISR, from a polling sequence, from a raw-APB sequence that never heard of the model, and updates the mirror for each one. What it cannot do is update it instantly: the bench's own job test shows the lag. Right after status.read() returns 2, the mirror still says DONE = 0, and asking it costs two warnings:

    Diagram
    +UVM_TESTNAME=dma_ral_job_test   (run.sh dma_ral_tb.sv)
    UVM_WARNING reg/uvm_reg_field.svh(1327) @ 425000: reporter [UVM/FLD/GET_MIRRORED_VAL/VOL] Mirrored value returned for volatile field "regmodel.status.done" may not reflect the actual current value.
    UVM_WARNING reg/uvm_reg_field.svh(1327) @ 425000: reporter [UVM/FLD/GET_MIRRORED_VAL/VOL] Mirrored value returned for volatile field "regmodel.status.err" may not reflect the actual current value.
    UVM_INFO dma_ral_tb.sv(346) @ 425000: uvm_test_top [RAL] STATUS=2: done=0 err=0

    The library is telling you the same thing as the mailbox: a volatile field's mirror is a note, not the box. An ISR that branched on the mirror here would conclude there was nothing to service and leave the interrupt line high forever.

    Waiting without the bus: backdoor polling and wait_for_change

    Polling STATUS over APB until DONE appears works (apb_poll_seq does it), but every poll is bus traffic that can shift the timing you are trying to test. Two backdoor alternatives cost no bus cycles. The simple one is a peek() loop on a clock edge. The library's own mechanism is an active-mirror backdoor: override is_auto_updated() to name the fields to track and wait_for_change() to block until the hardware changes, then call start_update_thread(). The library then re-reads the register through the backdoor after every change and predicts the tracked fields, so their mirror follows the DUT with no bus traffic at all.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // snippet: scratch extension of the bench's backdoor
    class live_backdoor extends dma_reg_backdoor;
      function new(string name = "live_backdoor");
        super.new(name);
      endfunction
      virtual function bit is_auto_updated(uvm_reg_field field);
        return field.get_parent().get_name() == "count";
      endfunction
      virtual task wait_for_change(uvm_object element);
        @(vif.count);                        // the bound interface sees the DUT's COUNT
      endtask
    endclass
    // end_of_elaboration: rm.count.set_backdoor(live);
    // run_phase, before traffic: live.start_update_thread(rm.count);

    The bench's dma_bd_if exposes KEY, LEN and COUNT, not STATUS, so the scratch test tracks COUNT. With STATUS on the interface, the same three methods would let a test wait on DONE without a single APB read.

    Watch it run

    The scratch test runs three jobs: 4 of 4 words, then LEN = 8 with only 3 words sent (early TLAST, so the DMA reports ERR), then 2 of 2. After each interrupt it prints the live COUNT mirror, starts the ISR and samples the interrupt line 20 ns later. Scratch run, Verilator:

    Diagram
    UVM_INFO scratch.sv(617) @ 365000: uvm_test_top [ISR] irq high; COUNT mirror (live backdoor) = 4
    UVM_INFO scratch.sv(620) @ 445000: uvm_test_top [ISR] handler read STATUS=2, cleared 2
    UVM_INFO scratch.sv(622) @ 465000: uvm_test_top [ISR] irq after the handler = 0
    UVM_INFO scratch.sv(617) @ 645000: uvm_test_top [ISR] irq high; COUNT mirror (live backdoor) = 3
    UVM_INFO scratch.sv(620) @ 725000: uvm_test_top [ISR] handler read STATUS=4, cleared 4
    UVM_INFO scratch.sv(622) @ 745000: uvm_test_top [ISR] irq after the handler = 0
    UVM_INFO scratch.sv(617) @ 925000: uvm_test_top [ISR] irq high; COUNT mirror (live backdoor) = 2
    UVM_INFO scratch.sv(620) @ 1005000: uvm_test_top [ISR] handler read STATUS=2, cleared 2
    UVM_INFO scratch.sv(622) @ 1025000: uvm_test_top [ISR] irq after the handler = 0
    UVM_INFO dma_tb.sv(411) @ 1125000: uvm_test_top.env.sb [SB] 9 words matched, 0 mismatched
    UVM_ERROR :    0
    • COUNT mirror = 4, 3, 2 with no COUNT read on the bus: the active-mirror backdoor kept it current. Note the second job: COUNT = 3 of LEN 8, the words that arrived before TLAST.

    • STATUS = 4 on the second job: ERR alone, no DONE. The handler cleared exactly bit 2. A handler that branched on DONE would have left this interrupt pending.

    • irq = 0 each time, 20 ns after the handler: the clear really reached the hardware. That one sample is the ISR's own check; without it, a handler that clears the wrong bit passes silently.

    What breaks: update() on a block with W1C status

    Back to the opening. Two library rules combine. First, in the IEEE 1800.2 library this course runs, uvm_reg_field::needs_update() returns 1 for every volatile field that is not RO, RC or RS, whatever the desired and mirrored values are. DONE and ERR are volatile W1C fields, so STATUS always needs an update. Second, update() writes, for a W1C field, the inverse of the desired value: to make a W1C bit read as 0 you write 1. With the desired value 0, that is a 1 in both bits. The same scratch test that confirmed the bench's GO trap shows both rules:

    Diagram
    scratch run, Verilator
    UVM_INFO scratch.sv(677) @ 315000: uvm_test_top [UPD] ctrl.needs_update()=0 status.needs_update()=1
    UVM_INFO scratch.sv(680) @ 365000: uvm_test_top [UPD] APB writes to CTRL after ctrl.update(): 0
    UVM_INFO scratch.sv(684) @ 455000: uvm_test_top [UPD] regmodel.update() wrote addr 04 1 time(s), data 6

    The first two lines are war story 13 confirmed: after ctrl.write(3) the mirror already holds GO = 1, so go.set(1); ctrl.update() writes nothing and no second job starts. The third line is the lost interrupt: the block-level update() skipped every register that matched its mirror and wrote STATUS anyway, with 0x6. Keep update() for configuration registers you have just set(), call it per register rather than per block, and service status only with explicit reads and writes.

    Interview

    "Write an interrupt handler sequence using the register model. What can go wrong?"

    A weak answer: "Read STATUS, check done.get(), and write 0xFFFF_FFFF to clear everything."

    A strong answer: "The handler reads STATUS with read() and decides from the returned value, because DONE and ERR are volatile: the mirror cannot know when hardware set them, and with explicit prediction it is updated a moment after the access anyway. It clears exactly the W1C bits it read with an explicit write(), so an event arriving after the read survives for the next pass, and it confirms the line dropped. It runs as a sequence with the accesses parented to it, so it can grab the sequencer if it must not be interleaved. The predictor sees every master's traffic, so the mirror stays as good as it can be. And nothing generic touches STATUS: block-level update() writes volatile W1C registers every time, with ones."

    Follow-ups: How do you wait for DONE without polling the bus? (backdoor peek() on a clock, or an active-mirror backdoor with is_auto_updated, wait_for_change and start_update_thread). Why is writing 0x6 after reading 0x2 dangerous? (it clears an ERR that may have arrived after the read).

    Check yourself The ISR reads STATUS = 2 and writes back 2. Between the read and the write, the DMA sets ERR. What does the interrupt line do, and why is that correct?

    The write clears only DONE, so ERR stays set and, with IRQ_EN on, the interrupt line stays high (or rises again). The handler runs again and services ERR. That is the point of clearing only what you read: an event that arrives during servicing is never lost. Writing 0x6 instead would have cleared ERR without anyone seeing it.

    Check yourself A colleague wants the handler to check rm.status.done.get_mirrored_value() before reading, "to save a bus access when nothing is pending". What is wrong with that?

    DONE is volatile: the hardware sets it without a bus write, so the mirror only changes after someone reads STATUS. The mirror would say 0 when DONE is set, the handler would skip the read, and the interrupt would stay pending forever. The library even warns when a volatile field's mirror is read. The read is the only way to know.

    Check yourself An active-mirror backdoor tracks COUNT. Does a test still need to read COUNT through the bus at the end of a job?

    If the plan item is "COUNT is readable through the register interface", yes: the backdoor proves the hardware value, not the APB read path, its address decode or its read mux. The backdoor is for waiting and for cheap checks during the job; at least one frontdoor read per test keeps the bus path covered.

    Next: one DMA, one model. The Register Model at SoC Scale puts two DMA models into a chip model, moves one of them, reuses the block's sequences unchanged, and keeps a model with a hundred thousand registers fast.

    Related topics

    • Up: Registers in Verification

    • Next: The Register Model at SoC Scale

    • Interrupt agents

    Previous

    166 / 280

    Next: The Register Model at SoC Scale

    Up next

    Up nextThe Register Model at SoC Scale

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Built-in Register Sequences and NO_* Attributes
    • Register Coverage
    • Memories in the Register Model