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. Shadow, Alias and Paged Registers

Part 9 · Register Model (RAL) · Intermediate

Shadow, Alias and Paged Registers

Modelling double-buffered, aliased and paged registers with field callbacks, user frontdoors and custom predictors, keeping the mirror check instead of silencing it.

On this page

    A threshold register is written with 0x40, and the very next mirror(UVM_CHECK) fails: read 16'h10, mirrored 16'h40. The designer is unbothered. THRESH is double-buffered: software writes a pending copy, and the new value takes effect, and reads back, only after a write to COMMIT. The RTL is right, the spec is right, and the register model, which assumes that a written value is the value you read back, is wrong.

    Real register files are full of storage that one address does not map to one-to-one: shadow (double-buffered) registers, alias addresses for the same flops, paged and indexed windows. Every one of them breaks the model's default assumption. This lesson models the first two with field callbacks, verified on the scratch slave, and shows where paged registers need a user frontdoor and a custom predictor instead.

    The idea: draft, published, and two doors to one room

    A shadow register is a draft and a published document: you edit the draft (the pending copy), and nothing changes for readers until you press publish (the commit trigger). An alias is a second door into the same room: two addresses, one set of flops, sometimes with different access policies at each address (a read-only view, or a write-1-to-set view of a RW register). A paged register is a lift: one address (the window) reaches different floors depending on a page register you wrote first.

    The register model's default is one storage element per register, updated by its own field policy. The tools to change that are, in order of reach:

    Tool

    Scope

    Use it for

    Field callback (uvm_reg_cbs::post_predict)

    One field: it may change the value about to be stored, and may predict other fields

    Shadow registers, aliases, side effects of one write on another register

    Custom predictor (extend uvm_reg_predictor)

    Every observed bus transfer, with state of your own

    Rules spanning many registers, paged windows seen on the bus

    User frontdoor (uvm_reg_frontdoor)

    How the model reaches a register at all

    Paged and indexed registers with no address of their own

    Custom access policies and the callback hooks themselves are introduced in Access policies and callbacks; this lesson applies them to storage shapes.

    The picture: what each prediction does

    Sequence diagram: a THRESH write is predicted, the shadow callback stores 40 as pending and restores the mirror to 10; a COMMIT write is predicted, the commit callback predicts THRESH to 40 and sets GO back to 0.
    A write to THRESH stores the new value as pending and keeps the mirror at the active value. A write to COMMIT moves pending into THRESH's mirror and keeps GO reading 0.

    post_predict runs inside do_predict after the field policy has computed the new value and before it is stored. Its value argument is inout, so a callback can replace the value that lands in the mirror. It runs for write and read predictions, but not for a direct predict() call (UVM_PREDICT_DIRECT, the default kind of predict()), so a callback checks kind and acts only on the case it models.

    Build it: three callbacks on the scratch slave

    THRESH: keep the mirror at the active copy

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // snippet: scratch callbacks (uvm_path_e is the UVM 1.2 name; IEEE 1800.2 keeps it as an alias of uvm_door_e)
    class shadow_cb extends uvm_reg_cbs;
      uvm_reg_data_t pending = 'h10;           // THRESH's reset value
      function new(string name = "shadow_cb");
        super.new(name);
      endfunction
      virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
                                         inout uvm_reg_data_t value, input uvm_predict_e kind,
                                         input uvm_path_e path, input uvm_reg_map map);
        if (kind == UVM_PREDICT_WRITE) begin
          pending = value;                     // the write lands in the pending copy
          value   = previous;                  // what a read returns has not changed
        end
      endfunction
    endclass

    COMMIT: publish, and behave like a trigger

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // snippet: scratch callbacks
    class commit_cb extends uvm_reg_cbs;
      shadow_cb     shadow;
      uvm_reg_field target;                    // THRESH.value
      function new(string name = "commit_cb");
        super.new(name);
      endfunction
      virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
                                         inout uvm_reg_data_t value, input uvm_predict_e kind,
                                         input uvm_path_e path, input uvm_reg_map map);
        if (kind == UVM_PREDICT_WRITE && value[0]) begin
          void'(target.predict(shadow.pending));
          value = 0;                           // GO is a trigger: the mirror keeps 0
        end
      endfunction
    endclass

    The last line solves a second problem for free. Without it, GO's mirror holds 1 after the first commit, so commit.go.set(1); commit.update() sees desired equal to mirrored and writes nothing: the trap the bench avoids for CTRL.GO by writing CTRL directly. With the callback, GO's mirror returns to 0 and update() works.

    CFG_ALIAS: two addresses, one set of flops

    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
    // snippet: scratch callbacks
    class alias_cb extends uvm_reg_cbs;
      uvm_reg_field a, b;                      // CFG.mode and CFG_ALIAS.mode
      local bit busy;
      function new(string name = "alias_cb");
        super.new(name);
      endfunction
      virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
                                         inout uvm_reg_data_t value, input uvm_predict_e kind,
                                         input uvm_path_e path, input uvm_reg_map map);
        if (busy) return;                      // defensive: see the text
        busy = 1;
        if (fld == a) void'(b.predict(value));
        else          void'(a.predict(value));
        busy = 0;
      endfunction
    endclass
    
    // in the env, after the model is built:
    cm.shadow = sh;  cm.target = rm.thresh.value;
    al.a = rm.cfg.mode;  al.b = rm.cfg_alias.mode;
    uvm_reg_field_cb::add(rm.thresh.value, sh);
    uvm_reg_field_cb::add(rm.commit.go,    cm);
    uvm_reg_field_cb::add(rm.cfg.mode,       al);
    uvm_reg_field_cb::add(rm.cfg_alias.mode, al);

    b.predict(value) is a direct prediction, and direct predictions do not call callbacks (the library's uvm_reg_field::do_predict calls post_predict only for write and read predictions), so the partner's callback does not fire and the pair cannot ping-pong. The busy flag is there for the day someone changes that call to predict(value, .kind(UVM_PREDICT_WRITE)) to get policy handling: write predictions do run callbacks, and the two fields would call each other forever. If the alias has a different policy (say CFG_ALIAS is W1S, a set-bits view), the callback predicts the resulting value on the other field rather than copying the written one.

    Watch it run

    The scratch test writes THRESH = 0x40, checks it, writes COMMIT, checks again, writes CFG_ALIAS = 0x33 and checks CFG. Scratch run, Verilator:

    Diagram
    UVM_INFO mini_tb.sv(412) @ 145000: uvm_test_top [SHADOW] after writing 40: thresh mirror=10
    UVM_INFO mini_tb.sv(417) @ 225000: uvm_test_top [SHADOW] after COMMIT: thresh mirror=40, commit.go mirror=0
    UVM_INFO mini_tb.sv(421) @ 305000: uvm_test_top [SHADOW] after writing 33 to CFG_ALIAS: cfg mirror=33
    UVM_INFO mini_tb.sv(424) @ 335000: uvm_test_top [SHADOW] commit.go.set(1): needs_update=1
    UVM_ERROR :    0

    All three mirror checks pass, and the last line shows update() on COMMIT would now really write. The predictions come from the explicit predictor on the CPU bus, so the callbacks fire for writes made by any sequence or by any agent on that bus, not only for accesses made through the model.

    What breaks: one wrong prediction becomes two errors

    The same test with the callbacks not registered (Verilator, scratch run):

    Diagram
    UVM_INFO mini_tb.sv(412) @ 145000: uvm_test_top [SHADOW] after writing 40: thresh mirror=40
    UVM_ERROR reg/uvm_reg.svh(2812) @ 175000: reporter [RegModel] Register "rm.thresh" value read from DUT (0x0000000000000010) does not match mirrored value (0x0000000000000040) (valid bit mask = 0x000000000000ffff)
    UVM_INFO mini_tb.sv(417) @ 225000: uvm_test_top [SHADOW] after COMMIT: thresh mirror=10, commit.go mirror=1
    UVM_ERROR reg/uvm_reg.svh(2812) @ 255000: reporter [RegModel] Register "rm.thresh" value read from DUT (0x0000000000000040) does not match mirrored value (0x0000000000000010) (valid bit mask = 0x000000000000ffff)
    UVM_INFO mini_tb.sv(421) @ 305000: uvm_test_top [SHADOW] after writing 33 to CFG_ALIAS: cfg mirror=0
    UVM_ERROR reg/uvm_reg.svh(2812) @ 335000: reporter [RegModel] Register "rm.cfg" value read from DUT (0x0000000000000033) does not match mirrored value (0x0000000000000000) (valid bit mask = 0x00000000000000ff)
    UVM_INFO mini_tb.sv(424) @ 335000: uvm_test_top [SHADOW] commit.go.set(1): needs_update=0
    UVM_ERROR :    3

    Read the middle two errors together; they are the instructive part. The first check fails as expected. But mirror() also updates the mirror with what it read, so after the failed check the model holds 0x10, correct for that instant. COMMIT then makes the DUT return 0x40, the model knows nothing about commits, and the second check fails in the opposite direction. A reviewer seeing expected 40, got 10 followed by expected 10, got 40 often suspects a flaky DUT. The real cause is one missing rule in the model, and the fix is to model the rule, never to add set_compare(UVM_NO_CHECK) to THRESH and lose the check altogether.

    Paged registers: when the register has no address of its own

    A paged register file has one data window and a PAGE register; the register you reach depends on PAGE. Two things change. The model reaches such a register through a user frontdoor, a sequence that writes PAGE and then accesses the window, attached with add_reg(rg, offset, rights, 1 /*unmapped*/, frontdoor) or set_frontdoor(). And the explicit predictor can no longer help: the bus shows writes to PAGE and to the window, never to bank 1 CFG, so get_reg_by_offset finds the window register, not the paged one.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // snippet: a user frontdoor for a paged register
    class paged_frontdoor extends uvm_reg_frontdoor;
      `uvm_object_utils(paged_frontdoor)
      uvm_reg        page_sel, window;        // PAGE and the shared data window
      uvm_reg_data_t page;                    // the page this register lives in
      function new(string name = "paged_frontdoor");
        super.new(name);
      endfunction
      virtual task body();
        uvm_status_e st;
        page_sel.write(st, page, .parent(this));
        if (rw_info.kind == UVM_WRITE) window.write(st, rw_info.value[0], .parent(this));
        else                           window.read (st, rw_info.value[0], .parent(this));
        rw_info.status = st;
      endtask
    endclass

    Choose one of two prediction strategies and write it down. Either predict paged registers from their own accesses (auto prediction on for them: the map predicts after the frontdoor finishes) and accept that writes made outside the model are invisible, or extend uvm_reg_predictor so its write() tracks the last PAGE value seen on the bus and predicts the paged register whenever a window access arrives. The second is more work and is the only one that survives firmware or a second master writing the window. Model the window register itself with UVM_NO_CHECK, since its mirror holds whichever page was touched last. The library also ships uvm_reg_indirect_data for the common index-plus-data pattern.

    Interview

    "How would you model a double-buffered register in RAL?"

    A weak answer: "Mark it volatile so the mirror check does not fail."

    A strong answer: "Volatile throws the check away. I keep the check and teach the model the rule: a post_predict callback on the field stores a written value as pending and restores the previous value, so the mirror tracks the active copy; a callback on the commit trigger predicts the pending value into the register and sets the trigger's own mirror back to 0, which also keeps update() working. Because the callbacks fire from do_predict, they work with explicit prediction for any master on that bus. Aliases get a callback that predicts the partner field, with a re-entry guard. Paged registers get a user frontdoor, and a custom predictor if writes from outside the model matter."

    Follow-ups: Why did the unmodelled register fail twice with opposite values? (mirror() updates the mirror after a mismatch). Where would a callback not be enough? (when one bus transfer changes registers by a rule that needs state from other transfers, such as paging).

    Check yourself The shadow callback ignores kind == UVM_PREDICT_READ. Why is that correct?

    A read returns the active copy, which is exactly what the mirror should hold, so the value the policy computed from the read is already right. Only a write needs special handling, because it lands in the pending copy that reads cannot see. Intercepting reads too would stop the mirror from ever resynchronising with the DUT.

    Check yourself Someone removes the busy flag from alias_cb and, separately, changes b.predict(value) to b.predict(value, .kind(UVM_PREDICT_WRITE)). What happens on the first write to CFG?

    Removing the flag alone changes nothing: predict() defaults to a direct prediction, which does not call post_predict. With the write kind, the prediction of CFG_ALIAS.mode runs its callbacks, which is the same object, which predicts CFG.mode with the write kind again, and so on: unbounded recursion until the simulator runs out of stack. The guard makes each side update the other exactly once whatever kind is used.

    Check yourself A paged register is modelled with a user frontdoor and the env uses explicit prediction only. A test writes it through the model and then calls mirror(UVM_CHECK). What happens, and why?

    The check fails (or passes only by luck). The predictor sees a PAGE write and a window write; it predicts the window register, never the paged one, so the paged register's mirror never changed. Turn on auto prediction for the paged registers, or extend the predictor to track PAGE and predict the paged register from window accesses.

    Next: status registers that the hardware sets on its own are the hardest storage of all to predict. Interrupts with the Register Model writes an interrupt handler that reads and clears them through the model.

    Related topics

    • Up: Registers in Verification

    • Next: Interrupts with the Register Model

    • Access policies and callbacks

    Previous

    165 / 280

    Next: Interrupts with the Register Model

    Up next

    Up nextInterrupts with the Register Model

    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