Skip to content

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, offset 0x0) — the preset/reload value; software owns it and writes it to set the count.
  • VALUE (RO, volatile, offset 0x4) — the current count; hardware owns it (software reads it; the hardware decrements it).
  • CONTROL (RW, offset 0x8) — enable, mode, prescaler.
  • INT_STATUS (W1C, offset 0xC) — timeout interrupt status; write 1 to clear.
  • INT_ENABLE (RW, offset 0x10) — interrupt enable.
Timer register block: LOAD (RW, sw-owned), VALUE (RO volatile, hw-owned), CONTROL, INT_STATUS (W1C), INT_ENABLELOAD @0x0LOAD @0x0RW · SW-owned presetRW · SW-owned presetVALUE @0x4VALUE @0x4RO + VOLATILE · HW-owned countRO + VOLATILE · HW-owned countCONTROL @0x8CONTROL @0x8RW · enable/mode/prescalerRW · enable/mode/prescalerINT_STATUS @0xCINT_STATUS @0xCW1C · timeout, write 1 to clearW1C · timeout, write 1 to clearINT_ENABLE @0x10INT_ENABLE @0x10RW · interrupt enableRW · interrupt enabletimer_reg_blocktimer_reg_blockAPB peripheralAPB peripheral
Figure 1 — the timer register block, split by OWNERSHIP. LOAD is software-owned (RW): software presets the count by writing it. VALUE is hardware-owned (RO + volatile): software reads the live count, but the HARDWARE sets and decrements it — software cannot write it. CONTROL/INT_ENABLE are RW; INT_STATUS is W1C. The LOAD-vs-VALUE ownership split is the timer's key lesson: you preset the count via LOAD (sw-owned), NOT by writing VALUE (hw-owned, RO) — writing VALUE while it is wrongly modelled RW makes the mirror run AHEAD of the DUT (the counterpart to GPIO's behind, 11.1).

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endclass

4. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endclass

5. 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):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

1

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
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.
Symptom

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.

Root Cause

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.

Fix

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) as RW. It is hardware-owned RO+volatile; an RW model predicts writes the DUT ignores → mirror ahead (1.x, 11.1).
  • Writing VALUE to preset the count. The count is preset via LOAD (software-owned); to reach a specific count for setup, backdoor poke VALUE (13.5).
  • Forgetting VALUE is volatile. Even read-only, the hardware changes it constantly — mark it volatile so reads do not false-fail (0.1).
  • Modelling INT_STATUS as RW. The timeout status is W1C (write 1 to clear) and hardware-set (1.5).
  • Frontdoor-polling VALUE for 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 via VALUE; never frontdoor-write the hardware-owned count.
  • Backdoor-poke for corner-case setup. Reach a specific count instantly by poke-ing VALUE for setup, then verify frontdoor (13.5).
  • Mark hardware-maintained values volatile. VALUE is 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-modelled RO register 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: LOAD is software-owned (RW, presets the count) and VALUE is hardware-owned (RO+volatile, the live count) — software reads VALUE but cannot write it.
  • The signature bug is modelling VALUE as RW and writing it to preset the count: the model predicts the value (mirror ahead) while the RO DUT ignores the write — an access-policy/ownership mismatch (1.x, 11.1 ahead).
  • Preset the count via LOAD (software-owned), observe via VALUE (read-only), and for corner-case setup reach a specific count by backdoor poke of VALUE, not a frontdoor write (13.5).
  • GPIO's IN (behind, hardware-updated-not-volatile) and the timer's VALUE (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.