UVM RAL · Chapter 14 · Industry Case Studies
Case Study: Interrupt Controller Registers
This capstone models an interrupt controller, whose defining RAL challenge is that several register views act on one shared underlying state. The controller has a pending state, one bit per source, and exposes it through multiple registers with different semantics. A status register shows the pending bits, a set register writes one to raise a bit, a clear register writes one to clear a bit, an enable register masks per source, and a masked-status view shows the effective interrupt. The crucial fact is that set, clear, and status are three views of one pending-state register, not independent registers that happen to relate. This is shared-storage aliasing at the peripheral level, and it is exactly where the signature bug lives. This case builds the model and breaks set, clear, and status modelled as independent mirrors, so a write to set is not visible when reading status.
Foundation14 min readUVM RALcase studyinterrupt controllershared stateW1S W1C
Chapter 14 · Section 14.4 · Industry Case Studies · Capstone
1. The Peripheral and Its Register Map
An interrupt controller aggregates many sources into a pending state and exposes it through several access views.
IRQ_STATUS(RO, offset0x0) — the pending bits; hardware sets them on events (volatile).IRQ_SET(WO/W1S, offset0x4) — write1to set a pending bit (software-trigger / test).IRQ_CLEAR(WO/W1C, offset0x8) — write1to clear a pending bit.IRQ_ENABLE(RW, offset0xC) — per-source enable/mask.IRQ_MASKED_STATUS(RO, offset0x10) — pending AND enabled (the effective interrupt).
2. Industry Context — why the interrupt controller is the shared-state case
Interrupt controllers are ubiquitous, and every one exposes its pending state through multiple access views — separate SET/CLEAR/STATUS (or RAW/MASKED) registers that all touch one underlying state. This set-clear-status trio over shared storage is the interrupt controller's signature structure, and modelling it faithfully — as one state seen several ways, not several independent registers — is the whole challenge. It generalizes the aliasing lesson (3.4, 12.5) to a peripheral where getting it wrong is especially visible: a software-triggered interrupt (IRQ_SET) that does not appear in IRQ_STATUS breaks the most basic interrupt-handling flow.
3. The Register Model — one shared state, several views
Model the shared pending state so that SET/CLEAR effects are reflected in IRQ_STATUS (via prediction/callbacks tying the views to one state), not as independent mirrors:
// The interrupt controller: SET/CLEAR/STATUS are VIEWS of ONE pending state. Tie them together.
class intc_reg_block extends uvm_reg_block;
`uvm_object_utils(intc_reg_block)
rand uvm_reg IRQ_STATUS, IRQ_SET, IRQ_CLEAR, IRQ_ENABLE, IRQ_MASKED_STATUS;
function new(string name="intc_reg_block"); super.new(name, UVM_NO_COVERAGE); endfunction
virtual function void build();
default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
IRQ_STATUS = uvm_reg::type_id::create("IRQ_STATUS"); IRQ_STATUS.configure(this); IRQ_STATUS.build();
default_map.add_reg(IRQ_STATUS, 'h0); // RO + volatile (hardware sets pending bits)
IRQ_SET = uvm_reg::type_id::create("IRQ_SET"); IRQ_SET.configure(this); IRQ_SET.build();
default_map.add_reg(IRQ_SET, 'h4); // WO / W1S
IRQ_CLEAR = uvm_reg::type_id::create("IRQ_CLEAR"); IRQ_CLEAR.configure(this); IRQ_CLEAR.build();
default_map.add_reg(IRQ_CLEAR, 'h8); // WO / W1C
IRQ_ENABLE = uvm_reg::type_id::create("IRQ_ENABLE"); IRQ_ENABLE.configure(this); IRQ_ENABLE.build();
default_map.add_reg(IRQ_ENABLE, 'hC); // RW
IRQ_MASKED_STATUS = uvm_reg::type_id::create("IRQ_MASKED_STATUS"); IRQ_MASKED_STATUS.configure(this); IRQ_MASKED_STATUS.build();
default_map.add_reg(IRQ_MASKED_STATUS, 'h10); // RO = pending AND enable
lock_model();
endfunction
endclass// A callback ties IRQ_SET/IRQ_CLEAR writes to the SHARED pending state seen via IRQ_STATUS (9.x):
class intc_shared_state_cb extends uvm_reg_cbs;
intc_reg_block regs;
virtual task post_write(uvm_reg_item rw);
// On IRQ_SET write: SET those bits in IRQ_STATUS's predicted value. On IRQ_CLEAR: clear them.
if (rw.element == regs.IRQ_SET)
void'(regs.IRQ_STATUS.predict(regs.IRQ_STATUS.get_mirrored_value() | rw.value[0])); // W1S -> STATUS
else if (rw.element == regs.IRQ_CLEAR)
void'(regs.IRQ_STATUS.predict(regs.IRQ_STATUS.get_mirrored_value() & ~rw.value[0])); // W1C -> STATUS
endtask
endclass
// Now a write to IRQ_SET is reflected in IRQ_STATUS's mirror — the views share ONE state.4. Adapter, Predictor, and an Interrupt-Flow Sequence
Wire to APB (as before), and drive the basic interrupt flow: trigger, observe, clear:
// Trigger interrupt source 3 (IRQ_SET), confirm it in IRQ_STATUS, then clear it (IRQ_CLEAR).
class intc_flow_seq extends uvm_reg_sequence#(uvm_sequence#(uvm_reg_item));
intc_reg_block regs;
virtual task body();
uvm_status_e s; uvm_reg_data_t st;
regs.IRQ_ENABLE.write(s, 32'h0000_0008); // enable source 3
regs.IRQ_SET.write(s, 32'h0000_0008); // W1S: trigger source 3's pending bit
regs.IRQ_STATUS.read(s, st); // MUST show bit 3 set (shared state)
regs.IRQ_CLEAR.write(s, 32'h0000_0008); // W1C: clear source 3
regs.IRQ_STATUS.read(s, st); // now shows bit 3 cleared
endtask
endclass5. The Masked Status View
IRQ_MASKED_STATUS is pending AND enable — a derived read-only view; model it as reflecting the shared pending state gated by IRQ_ENABLE:
// IRQ_MASKED_STATUS = IRQ_STATUS (pending) AND IRQ_ENABLE. A derived RO view of the shared state.
// (predict it from pending & enable, or verify it against status & enable in the scoreboard)
uvm_reg_data_t effective = regs.IRQ_STATUS.get_mirrored_value() & regs.IRQ_ENABLE.get_mirrored_value();
// effective is what IRQ_MASKED_STATUS should read — the enabled subset of the pending bits.6. Debugging Walkthrough — the interrupt you set that never showed
Modelling IRQ_SET, IRQ_CLEAR, and IRQ_STATUS as independent registers makes a write to IRQ_SET update only its own mirror, so reading IRQ_STATUS shows the bit not set — the views diverge from the one shared hardware state
ONE SHARED STATE, SEVERAL VIEWS// SET/CLEAR/STATUS modelled as INDEPENDENT registers with INDEPENDENT mirrors, no tie between them:
// (no callback/prediction linking IRQ_SET/IRQ_CLEAR writes to IRQ_STATUS's state)
regs.IRQ_SET.write(s, 32'h0000_0008); // updates only IRQ_SET's OWN mirror
regs.IRQ_STATUS.read(s, st); // BUG: model expects IRQ_STATUS unchanged -> auto-compare MISMATCH
// The DUT sets bit 3 in the shared pending state (visible via IRQ_STATUS), but the MODEL's IRQ_STATUS
// mirror was never updated by the IRQ_SET write -> model says bit 3 is 0, DUT says 1 -> mismatch.After writing IRQ_SET to trigger source 3, reading IRQ_STATUS produces an auto-compare mismatch: the DUT shows bit 3 set (the shared pending state was updated), but the model's IRQ_STATUS mirror shows bit 3 clear (its mirror was never touched by the IRQ_SET write). The most basic interrupt flow — trigger then observe — 'fails,' even though the DUT behaves correctly. The tell: a write via one register (IRQ_SET) has a real effect visible via another register (IRQ_STATUS) in the DUT, but the model treats them as unrelated, so its IRQ_STATUS never reflects the IRQ_SET.
IRQ_SET, IRQ_CLEAR, and IRQ_STATUS are three views of one shared pending-state register — writing IRQ_SET sets bits in the shared state that read back through IRQ_STATUS — but they were modelled as independent registers with independent mirrors, with nothing tying them together. So the IRQ_SET write updated only IRQ_SET's mirror; the model's IRQ_STATUS mirror was never updated, and it still predicted bit 3 clear. Meanwhile the DUT's shared pending state did set bit 3 (correctly), so a frontdoor read of IRQ_STATUS returned bit 3 set, mismatching the model's stale (independent) mirror. This is shared-storage aliasing (3.4, 12.5) at the peripheral level: multiple register views onto one state must be modelled as one state, or a write through one view is invisible through the others in the model. The DUT, bus, and prediction are fine; the model structure is wrong — it represented one shared state as several independent registers, so the views cannot stay coherent. (This is the interrupt-controller analog of modelling aliased registers as independent, 12.5.)
Model the shared pending state so SET/CLEAR effects are reflected in IRQ_STATUS: tie the views together with prediction/callbacks — on an IRQ_SET write, set those bits in IRQ_STATUS's predicted value (W1S semantics); on an IRQ_CLEAR write, clear them (W1C) — the callback of Section 3. Then a write to IRQ_SET updates the shared pending value that IRQ_STATUS reads, and the views stay coherent. The lesson the interrupt-controller case teaches: when several registers are views of one shared state (an interrupt controller's IRQ_SET/IRQ_CLEAR/IRQ_STATUS over the pending state), they must be modelled as one shared state seen through several access views, not as independent registers with independent mirrors — otherwise a write through one view (IRQ_SET) is invisible through another (IRQ_STATUS) in the model, and the views diverge from the single hardware state. It is the peripheral form of shared-storage aliasing (3.4, 12.5).
7. Common Mistakes
- Modelling
SET/CLEAR/STATUSas independent registers. They are views of one shared pending state — aSETmust be visible inSTATUS; tie them with prediction/callbacks (3.4, 12.5). - Not marking
IRQ_STATUSvolatile. The hardware sets pending bits, so its mirror falls behind if not volatile (0.1, 11.1). - Treating
IRQ_MASKED_STATUSas independent. It is pending AND enable — a derived view of the shared state gated byIRQ_ENABLE. - Modelling
IRQ_SET/IRQ_CLEARasRW. They areWO/W1S/W1C— writing1sets/clears; there is no meaningful read-back of the write register itself (1.5). - Forgetting the
W1S/W1Cdirection.SETsets on write-1,CLEARclears on write-1— opposite effects on the same state.
8. Industry Best Practices
- Model shared state once, expose it through views.
SET/CLEAR/STATUS(andMASKED) reflect one pending state — tie them with prediction/callbacks. - Reflect
SET/CLEARwrites intoSTATUS. A callback that sets (W1S) or clears (W1C) the shared value keeps the views coherent (9.x). - Mark
IRQ_STATUSvolatile. Hardware sets pending bits asynchronously (0.1). - Model
MASKED_STATUSas derived. Pending AND enable — predict or check it from the shared state andIRQ_ENABLE. - Diagnose 'set not visible in status' as shared-state modelled independent. A write via one view invisible via another means the views were not tied to one state (12.5).
9. Interview / Review Questions
10. Key Takeaways
- The interrupt controller's defining challenge is that several register views act on one shared pending state:
IRQ_STATUSreads it,IRQ_SET(W1S) sets bits in it,IRQ_CLEAR(W1C) clears bits in it,IRQ_ENABLEmasks it,IRQ_MASKED_STATUS= pending AND enable. IRQ_SET/IRQ_CLEAR/IRQ_STATUSare three views of one register — a write viaIRQ_SETmust be visible viaIRQ_STATUS— so they must be modelled as one shared state seen through several views, not independent registers.- The signature bug is modelling them independent (independent mirrors): a write to
IRQ_SETupdates only its own mirror, soIRQ_STATUSshows the bit not set and the views diverge from the single hardware state — breaking trigger-then-observe. - The fix ties the views to one state via prediction/callbacks: reflect
IRQ_SET(set) andIRQ_CLEAR(clear) writes intoIRQ_STATUS's value — the shared-storage aliasing pattern (3.4, 12.5) plusW1S/W1Cside effects (1.5). - Mark
IRQ_STATUSvolatile (hardware sets pending bits), modelIRQ_MASKED_STATUSas derived (pending AND enable), and diagnose 'set not visible in status' as shared state modelled independently.