UVM RAL · Chapter 14 · Industry Case Studies
Case Study: Timer Register Block
This case study builds a timer peripheral register model end to end and contrasts it with the GPIO case. Where GPIO showed the mirror falling behind the design, the timer shows the mirror running ahead, so the two together teach both directions of a mirror mismatch. The registers split by ownership: software owns LOAD, the reload value it writes, while hardware owns VALUE, the live count it decrements and software can only read. That split is where the signature bug lives. A test that tries to preset the count by writing VALUE, while VALUE is wrongly modelled read-write instead of read-only, makes the model predict a value the design never accepts, so the mirror runs ahead and read-back mismatches. The fix is to model VALUE as read-only and volatile and preset the count through LOAD or a backdoor poke.
Foundation14 min readUVM RALcase studytimerhardware ownershipmirror ahead
Chapter 14 · Section 14.2 · Industry Case Studies · Capstone
1. The Peripheral and Its Register Map
A timer counts (typically down) from a loaded value and raises an interrupt at timeout. Its registers split cleanly by ownership — which is the lesson of this build.
LOAD(RW, offset0x0) — the preset/reload value; software owns it and writes it to set the count.VALUE(RO,volatile, offset0x4) — the current count; hardware owns it (software reads it; the hardware decrements it).CONTROL(RW, offset0x8) — enable, mode, prescaler.INT_STATUS(W1C, offset0xC) — timeout interrupt status; write1to clear.INT_ENABLE(RW, offset0x10) — interrupt enable.
2. Industry Context — ownership is the timer's defining lesson
Every timer separates the value software sets (LOAD) from the value hardware maintains (VALUE/current count), and confusing the two is one of the most common peripheral-modelling mistakes. Software presets the counter by writing LOAD; it observes progress by reading VALUE; it cannot write VALUE (the counter is hardware-driven). Get the ownership — and therefore the access policy — right, and the model tracks the hardware; get it wrong (model VALUE as RW), and you create a register the model thinks software can set but the hardware ignores, which is the mirror-ahead bug. This LOAD-vs-VALUE split recurs wherever hardware maintains a value software can only read (a UART's RX data, a FIFO's level, a counter's count).
3. The Register Model — ownership-correct policies
Model LOAD as software-owned RW and VALUE as hardware-owned RO+volatile:
// Timer registers — LOAD is SW-owned (RW), VALUE is HW-owned (RO + volatile).
class timer_reg_block extends uvm_reg_block;
`uvm_object_utils(timer_reg_block)
rand uvm_reg LOAD, VALUE, CONTROL, INT_STATUS, INT_ENABLE;
uvm_reg_field load_f, value_f, ctrl_f, ist_f, ien_f;
function new(string name="timer_reg_block"); super.new(name, UVM_NO_COVERAGE); endfunction
virtual function void build();
default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
LOAD = uvm_reg::type_id::create("LOAD"); LOAD.configure(this); LOAD.build();
load_f = uvm_reg_field::type_id::create("load_f");
load_f.configure(LOAD, 32, 0, "RW", 0, 32'h0, 1, 1, 1); // SW-owned preset
default_map.add_reg(LOAD, 'h0);
VALUE = uvm_reg::type_id::create("VALUE"); VALUE.configure(this); VALUE.build();
value_f = uvm_reg_field::type_id::create("value_f");
value_f.configure(VALUE, 32, 0, "RO", 1 /* VOLATILE */, 32'h0, 1, 1, 1); // HW-owned count (RO + volatile)
default_map.add_reg(VALUE, 'h4);
CONTROL = uvm_reg::type_id::create("CONTROL"); CONTROL.configure(this); CONTROL.build();
ctrl_f = uvm_reg_field::type_id::create("ctrl_f");
ctrl_f.configure(CONTROL, 32, 0, "RW", 0, 32'h0, 1, 1, 1);
default_map.add_reg(CONTROL, 'h8);
INT_STATUS = uvm_reg::type_id::create("INT_STATUS"); INT_STATUS.configure(this); INT_STATUS.build();
ist_f = uvm_reg_field::type_id::create("ist_f");
ist_f.configure(INT_STATUS, 32, 0, "W1C", 1, 32'h0, 1, 1, 1); // timeout, W1C
default_map.add_reg(INT_STATUS, 'hC);
INT_ENABLE = uvm_reg::type_id::create("INT_ENABLE"); INT_ENABLE.configure(this); INT_ENABLE.build();
ien_f = uvm_reg_field::type_id::create("ien_f");
ien_f.configure(INT_ENABLE, 32, 0, "RW", 0, 32'h0, 1, 1, 1);
default_map.add_reg(INT_ENABLE, 'h10);
lock_model();
endfunction
endclass4. Adapter, Predictor, and a Start Sequence
Wire to APB (as in 14.1), and write a start sequence that presets via LOAD — the software-owned register:
// Env wiring (same pattern as 14.1): map -> APB sequencer via adapter; predictor <- monitor (13.4).
reg_model.default_map.set_sequencer(apb_agent.sequencer, apb_adapter);
predictor.map = reg_model.default_map; predictor.adapter = apb_adapter;
apb_agent.monitor.ap.connect(predictor.bus_in);// Start the timer: PRESET the count via LOAD (sw-owned), then enable via CONTROL. Read VALUE to observe.
class timer_start_seq extends uvm_reg_sequence#(uvm_sequence#(uvm_reg_item));
timer_reg_block regs;
virtual task body();
uvm_status_e s; uvm_reg_data_t cur;
regs.LOAD.write(s, 32'h0000_1000); // preset the count via LOAD (SW owns this) — NOT by writing VALUE
regs.CONTROL.write(s, 32'h0000_0001); // enable the timer
regs.VALUE.read(s, cur); // OBSERVE the live count (RO) — do not try to write it
endtask
endclass5. Setting a Specific Count Fast — backdoor for setup
To reach a specific count for a corner case (e.g. near rollover) without waiting, poke the hardware-owned VALUE by backdoor (setup, not verification — 13.5):
// Reaching a near-timeout state instantly: backdoor poke VALUE (hw-owned) for SETUP — no frontdoor write.
regs.VALUE.poke(s, 32'h0000_0002); // seed the count near rollover directly (setup, 13.5/8.4)
// This is legitimate for SETUP (positioning); you still VERIFY timeout behaviour via the frontdoor.
// You do NOT frontdoor-WRITE VALUE to set it — it is RO; a frontdoor write is ignored (next section).6. Debugging Walkthrough — the count you couldn't write
Writing the hardware-owned VALUE register to preset the count, while VALUE is wrongly modelled RW, makes the model predict a value the read-only DUT ignores, so the mirror runs ahead
VALUE IS HW-OWNED (RO) — PRESET VIA LOAD// VALUE is wrongly modelled RW, and a test writes it to 'preset the count':
value_f.configure(VALUE, 32, 0, "RW", 0, 32'h0, 1, 1, 1); // BUG: VALUE is HW-owned RO, not RW
// ...
regs.VALUE.write(s, 32'h0000_0500); // BUG: writing a HW-owned RO register to set the count
// The model (RW) PREDICTS VALUE = 0x500 (mirror moves AHEAD), but the DUT ignores the write (RO) ->
// read-back returns the real count (not 0x500) -> mismatch, mirror AHEAD of DUT.The write to VALUE 'has no effect': a read-back does not return the written 0x500, and the auto-compare flags a mismatch — the mirror holds 0x500 (what the model predicted from the write) while the DUT holds the real count. It looks like the DUT is ignoring writes to VALUE, but the DUT is correct: VALUE is read-only. The tell is that the mirror holds a value the DUT never took (the written 0x500), and the register in question is the current count — a quintessentially hardware-owned value.
Applying the mirror-mismatch method (11.1): the mirror is ahead of the DUT — the model moved (predicted 0x500 from the write) but the DUT didn't (it holds the real count) — and a fresh frontdoor/backdoor read agree the DUT never took 0x500. The ahead direction points at 'a write the DUT never accepted,' and here the specific cause is an access-policy / ownership mismatch: VALUE is the current count, which the hardware owns and software can only read (it is RO), but it was modelled RW, so the model wrongly predicted that a write would change it. The write to a read-only register is legitimately ignored by the DUT, while the RW model predicted success — so the mirror ran ahead. This is the mirror-image of GPIO's IN bug: GPIO's IN was hardware-updated-not-volatile (mirror behind); the timer's VALUE is hardware-owned-modelled-writable (mirror ahead). Both are ownership/policy fidelity bugs; they differ only in direction. The DUT, bus, and prediction are all fine; the model's policy for VALUE is wrong, and the test tried to write a register software does not own.
Model VALUE as hardware-owned RO+volatile (software reads it; hardware sets it), and preset the count via LOAD — the software-owned register that exists precisely for that purpose. For a corner-case setup (a specific count near rollover), backdoor poke VALUE (setup, 13.5) rather than frontdoor-writing it. The lesson the timer case teaches: respect hardware-vs-software ownership — a hardware-owned register like the timer's current count is RO (and volatile), and writing it via the frontdoor makes an RW-modelled mirror run ahead of a DUT that legitimately ignores the write (11.1 ahead); set the value through the software-owned register (LOAD), or poke it by backdoor for setup. GPIO's IN (behind) and the timer's VALUE (ahead) are the two directions of the same ownership-fidelity lesson.
7. Common Mistakes
- Modelling
VALUE(current count) asRW. It is hardware-ownedRO+volatile; anRWmodel predicts writes the DUT ignores → mirror ahead (1.x, 11.1). - Writing
VALUEto preset the count. The count is preset viaLOAD(software-owned); to reach a specific count for setup, backdoorpokeVALUE(13.5). - Forgetting
VALUEis volatile. Even read-only, the hardware changes it constantly — mark it volatile so reads do not false-fail (0.1). - Modelling
INT_STATUSasRW. The timeout status isW1C(write1to clear) and hardware-set (1.5). - Frontdoor-polling
VALUEfor timeout. Polling a hardware-owned count over the bus is slow and perturbs timing — verify timeout via the interrupt, and use backdoor for setup (13.5).
8. Industry Best Practices
- Split by ownership. Software-owned preset (
LOAD,RW) vs hardware-owned count (VALUE,RO+volatile) — model each per its owner. - Preset via the software-owned register. Set the count through
LOAD; observe viaVALUE; never frontdoor-write the hardware-owned count. - Backdoor-poke for corner-case setup. Reach a specific count instantly by
poke-ingVALUEfor setup, then verify frontdoor (13.5). - Mark hardware-maintained values volatile.
VALUEis read-only and volatile (hardware changes it) so reads do not false-fail (0.1). - Diagnose mirror-ahead as a write-the-DUT-ignored. For a hardware-owned register, mirror ahead = an
RW-modelledROregister written (11.1).
9. Interview / Review Questions
10. Key Takeaways
- The timer case contrasts with GPIO by teaching the mirror-ahead direction (vs GPIO's behind) — the two together cover both branches of the mirror-mismatch method (11.1).
- The timer's defining lesson is the ownership split:
LOADis software-owned (RW, presets the count) andVALUEis hardware-owned (RO+volatile, the live count) — software readsVALUEbut cannot write it. - The signature bug is modelling
VALUEasRWand writing it to preset the count: the model predicts the value (mirror ahead) while theRODUT ignores the write — an access-policy/ownership mismatch (1.x, 11.1 ahead). - Preset the count via
LOAD(software-owned), observe viaVALUE(read-only), and for corner-case setup reach a specific count by backdoorpokeofVALUE, not a frontdoor write (13.5). - GPIO's
IN(behind, hardware-updated-not-volatile) and the timer'sVALUE(ahead, hardware-owned-modelled-writable) are the two directions of the same ownership-fidelity lesson — recur wherever hardware maintains a value software can only read.