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
      • The Access API: write, read, set, get, update, mirror, predict, peek, poke
      • The Adapter Contract: reg2bus, bus2reg and provides_responses
      • Prediction: Auto, Explicit and Passive
      • Field Access Policies and Callbacks
      • Volatile Fields and Mirror Checks
      • Backdoor Access: HDL Paths and User Backdoors
      • User-Defined Frontdoors: Indirect and Paged Registers
    • Registers in Verification
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Register Access
  1. Topics
  2. UVM Verification
  3. UVM Register Abstraction Layer (RAL)
  4. Register Access
  5. Field Access Policies and Callbacks

Part 9 · Register Model (RAL) · Intermediate

Field Access Policies and Callbacks

The 25 built-in field access policies, the DMA's choices, define_access for custom names, a post_predict callback that models a DUT rule, set_compare, and a block update() that cleared an interrupt.

On this page

    The bench's busy-LEN test ends with a RegModel error, on purpose. The DMA ignores writes to LEN while a job is running, the model does not know that rule, and mirror(UVM_CHECK) catches the disagreement. In a real project that error is the start of a decision: is it a DUT bug or a model gap? Here the spec says the DUT is right. So the model must learn the rule, and you need to know where in the model rules like that live.

    The idea: every field carries a contract

    A field's access policy is its contract with software: what a write does to the stored value, and what a read does to it. Think of the switches in a hotel room. Some are ordinary toggles (RW). The doorbell button does something when pressed but has no state you can see (WO). The "do not disturb" light can be turned on from inside and is cleared by housekeeping (set by hardware, cleared by a write). The model needs to know which kind each switch is, so it can predict the room after every press.

    The library uses the policy in three places. Prediction computes the new mirror value from the old one and the data written or read. `update()` computes which value to write to reach the desired value. Checks skip write-only fields entirely. The policy is a string passed to configure(), and the 25 built-in names are predefined by uvm_reg_field:

    Policy

    On a write of value w

    On a read

    RO

    No effect

    No effect

    RW

    Field = w

    No effect

    WO

    Field = w

    Not readable (excluded from checks)

    RC / RS

    No effect

    Clears / sets all bits

    WC / WS

    Clears / sets all bits, whatever w is

    No effect

    WRC / WRS

    Field = w

    Clears / sets all bits

    WSRC / WCRS

    Sets / clears all bits

    Clears / sets all bits

    W1C / W1S / W1T

    Each 1 in w clears / sets / toggles that bit

    No effect

    W0C / W0S / W0T

    Each 0 in w clears / sets / toggles that bit

    No effect

    W1SRC / W1CRS

    Each 1 sets / clears that bit

    Clears / sets all bits

    W0SRC / W0CRS

    Each 0 sets / clears that bit

    Clears / sets all bits

    WOC / WOS

    Clears / sets all bits

    Not readable

    W1

    First write after a hard reset sets the field; later writes are ignored

    No effect

    WO1

    As W1, but not readable

    Not readable

    get_access() can also return NOACCESS, when the map's rights for the register contradict the field's policy (a WO field in a register mapped RO, for example). Read effects are applied only to frontdoor and predictor accesses: a backdoor peek() of an RC field does not clear the mirror, because a backdoor read does not clear the hardware either.

    The picture: where policies and callbacks act

    Flowchart: an access goes to the policy step for writes or reads, then through post_predict callbacks, then into the mirror and desired value; predict() writes the mirror directly.
    One field prediction inside uvm_reg_field::do_predict. The policy runs first, then every registered post_predict callback, and only then is the mirror written. A direct predict() skips both.

    Build it: the DMA's choices

    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    // snippet: from the course DMA bench
    // CTRL @0x00: GO is a trigger (write 1 to start, always reads 0), IRQ_EN is RW.
    class dma_ctrl_reg extends uvm_reg;
      `uvm_object_utils(dma_ctrl_reg)
      uvm_reg_field go;
      rand uvm_reg_field irq_en;
    
      function new(string name = "dma_ctrl_reg");
        super.new(name, 32, UVM_NO_COVERAGE);
      endfunction
    
      virtual function void build();
        go     = uvm_reg_field::type_id::create("go");
        irq_en = uvm_reg_field::type_id::create("irq_en");
        //               parent size lsb access volatile reset has_reset is_rand indiv
        go.configure    (this, 1,   0,  "WO",  0,       1'b0, 1,        0,      0);
        irq_en.configure(this, 1,   1,  "RW",  0,       1'b0, 1,        1,      0);
      endfunction
    endclass
    
    // STATUS @0x04: BUSY is read-only; DONE and ERR are set by the hardware and
    // cleared by writing 1. All three change without a bus write, so all are volatile.
    class dma_status_reg extends uvm_reg;
      `uvm_object_utils(dma_status_reg)
      uvm_reg_field busy, done, err;
    
      function new(string name = "dma_status_reg");
        super.new(name, 32, UVM_NO_COVERAGE);
      endfunction
    
      virtual function void build();
        busy = uvm_reg_field::type_id::create("busy");
        done = uvm_reg_field::type_id::create("done");
        err  = uvm_reg_field::type_id::create("err");
        busy.configure(this, 1, 0, "RO",  1, 1'b0, 1, 0, 0);
        done.configure(this, 1, 1, "W1C", 1, 1'b0, 1, 0, 0);
        err.configure (this, 1, 2, "W1C", 1, 1'b0, 1, 0, 0);
      endfunction
    endclass
    • GO is WO and not volatile. Writing 1 starts a job; reading always returns 0. WO keeps it out of every compare, so a read of CTRL never false-fails on it. The price is the mirror holding 1 after the first job (lesson 1). A callback can model the self-clear (below).

    • IRQ_EN is RW and randomizable. Plain storage.

    • BUSY is RO and volatile. The hardware changes it; software cannot.

    • DONE and ERR are W1C and volatile. The hardware sets them and software clears them by writing 1. Modelling them as RW would make a clear write of 1 predict DONE = 1, the opposite of the truth. With W1C the model predicts mirror & ~w natively; no custom predictor code is needed.

    • COUNT is RO and volatile (in dma_count_reg): it counts while the job runs.

    define_access: a custom policy name

    Your spec may use a name the library lacks, such as a W1C bit that only clears when the block is idle. uvm_reg_field::define_access("W1CBUSY") registers it, returning 1 the first time and 0 if the name already exists; call it before any configure() uses the name. Registering a name gives it no behaviour: prediction falls to the default branch and treats a write like RW, and update() and checks treat it like an ordinary readable field. The behaviour comes from a callback. A custom name is still worth having, because register coverage, generated documentation and your own callbacks can key on it.

    A post_predict callback: "LEN and KEY are ignored while BUSY"

    uvm_reg_cbs has hooks around accesses (pre_write, post_write, pre_read, post_read) and one hook inside prediction: post_predict. It is called for every field prediction from a write or read (frontdoor, predictor or backdoor), after the policy has computed the new value and before the mirror is written. It gets the field, the previous mirror value, the new value as an inout you may change, the kind, the path and the map. That is exactly where a side-effect rule belongs. The callback below tracks the DMA's busy state in the model and keeps LEN and KEY unchanged while it is set:

    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
    30
    31
    32
    // snippet: verified in a scratch run with the DMA bench
    class dma_busy_ignore_cb extends uvm_reg_cbs;
      `uvm_object_utils(dma_busy_ignore_cb)
      dma_reg_block rm;
      bit busy;                                  // the model's belief about STATUS.BUSY
      function new(string name = "dma_busy_ignore_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 (fld == rm.ctrl.go && kind == UVM_PREDICT_WRITE && value[0] && !busy)
          busy = (rm.len.value.get_mirrored_value() != 0);    // GO starts a job only if LEN != 0
        else if (fld == rm.status.busy && kind == UVM_PREDICT_READ)
          busy = value[0];                                     // a STATUS read tells the truth
        else if ((fld == rm.len.value || fld == rm.key.value) &&
                 kind == UVM_PREDICT_WRITE && path != UVM_BACKDOOR && busy) begin
          `uvm_info("BUSY_CB", $sformatf("%s write of %0h ignored while BUSY, mirror keeps %0h",
                    fld.get_parent().get_name(), value, previous), UVM_LOW)
          value = previous;
        end
      endfunction
    endclass
    
    // In the test's connect_phase: one callback object, registered on four fields.
    //   cb.rm = renv.regmodel;
    //   uvm_reg_field_cb::add(renv.regmodel.ctrl.go,     cb);
    //   uvm_reg_field_cb::add(renv.regmodel.status.busy, cb);
    //   uvm_reg_field_cb::add(renv.regmodel.len.value,   cb);
    //   uvm_reg_field_cb::add(renv.regmodel.key.value,   cb);

    Be honest about what it cannot see. The model learns that a job ended only when someone reads STATUS (or when you add a hook from the interrupt monitor). A test that writes LEN after the job finished but before any STATUS read would see the callback wrongly keep the old value. The same pattern models the GO trigger cleanly: a callback on ctrl.go that sets value = 0 on every write prediction makes the mirror hold 0, so update() works for the next job.

    set_compare: switching a field out of checks

    fld.set_compare(UVM_NO_CHECK) removes one field from every mirror compare without changing its policy or its prediction; set_compare(UVM_CHECK) puts it back, and get_compare() reads it. Configuring a field as volatile sets UVM_NO_CHECK for you. Use it for a field whose value you cannot predict but which is not changed by hardware either: an ID register that differs per tape-out, or a reserved field the designers may still use. Lesson 5 is about what that does to checks.

    Watch it run

    The scratch test is the bench's busy-LEN test with the callback registered (and define_access called once, twice, to show its return values). Compare it with the bench log in lesson 5, which ends with a RegModel error:

    Diagram
    scratch run: +UVM_TESTNAME=s_busy_cb_test (dma_ral_busy_len_test plus the callback; auto_predict = 1)
    UVM_INFO sacc_tb.sv(251) @ 0: uvm_test_top [POL] define_access("W1CBUSY") returned 1, again 0
    UVM_INFO sacc_tb.sv(232) @ 195000: reporter [BUSY_CB] len write of 9 ignored while BUSY, mirror keeps 4
    UVM_INFO dma_ral_tb.sv(385) @ 195000: uvm_test_top [RAL] LEN mirror after the write: 4
    UVM_INFO sacc_tb.sv(232) @ 195000: reporter [BUSY_CB] len write of 9 ignored while BUSY, mirror keeps 4
    UVM_INFO dma_ral_tb.sv(390) @ 235000: uvm_test_top [RAL] DUT LEN (backdoor peek)=4, mirror now 4
    UVM_INFO dma_tb.sv(411) @ 625000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
    UVM_ERROR :    0
    UVM_FATAL :    0
    • `mirror keeps 4` and `LEN mirror after the write: 4`: the model now knows the rule, so mirror(UVM_CHECK) reads 4, expects 4, and reports nothing. UVM_ERROR : 0.

    • The callback fired twice for one write. This test runs with auto prediction on and the explicit predictor connected (lesson 3), so the write was predicted twice. A pure function of its inputs survives that; a callback that counts events or toggles state would not.

    • `define_access` returned 1, then 0. The second call found the name already registered.

    What breaks: a block update() that cleared the interrupt

    Policies also drive update(), and that is where a correct-looking call bit a scratch test written for this lesson. After a job, DONE is 1 in the DUT. A sequence changes KEY with set() and pushes it with the block-level regmodel.update(st), meaning "write whatever changed":

    Diagram
    scratch run: +UVM_TESTNAME=s_block_update_test +UVM_VERBOSITY=UVM_HIGH (APB monitor lines kept)
    UVM_INFO sacc_tb.sv(497) @ 225000: uvm_test_top [UPD] DONE pending; STATUS mirror=0, status.needs_update=1, ctrl.needs_update=0
    UVM_INFO dma_tb.sv(151) @ 265000: uvm_test_top.env.apb.mon [APB_MON] WR addr=04 data=00000006
    UVM_INFO dma_tb.sv(151) @ 305000: uvm_test_top.env.apb.mon [APB_MON] WR addr=0c data=000000bb
    UVM_INFO sacc_tb.sv(501) @ 345000: uvm_test_top [UPD] after regmodel.update(): STATUS=0, KEY mirror=bb
    UVM_INFO dma_tb.sv(151) @ 345000: uvm_test_top.env.apb.mon [APB_MON] RD addr=04 rdata=00000000

    The block update wrote 0x6 to STATUS before writing KEY, and DONE was gone. Two rules combined. First, needs_update() is always true for a writable volatile field, so STATUS is always "changed". Second, for a W1C field update() writes the complement of the desired value, because writing 1 is how you make a W1C bit 0. The desired DONE was 0, so update() wrote 1 to it, and to ERR. On a real chip that is an interrupt status cleared by a configuration sequence before the interrupt handler ever saw it.

    The fix is a rule for the team: call update() on the register you changed, never on a block that contains W1C status registers, or exclude those registers in the update sequence. The deeper lesson is that update() is a model-driven write. It does exactly what the policies say, which is not always what the person calling it meant.

    Interview

    "A status bit is set by hardware and cleared by writing 1. How do you model it, and what goes wrong if you model it as RW?"

    A weak answer: "RW, and the predictor clears it when software writes 1."

    A strong answer: "W1C and volatile. W1C makes the model predict mirror & ~written natively, so a clear write is predicted correctly with no custom code. Volatile tells the model the hardware sets it on its own, which turns off checking for it and makes needs_update() always true. As RW, a clear write of 1 would predict the bit as 1, the opposite of the hardware. And I would warn the team that a block-level update() writes 1 to every W1C bit whose desired value is 0, which clears pending status."

    Follow-ups: How do you model a side effect the policies cannot express? (A uvm_reg_cbs::post_predict callback on the field, or a custom predictor.) What does `define_access` give you? (A registered name only; behaviour comes from callbacks.)

    Check yourself A field is documented as "read to clear". A debug sequence peeks it through the backdoor, and later a frontdoor read is checked. What does each access do to the mirror?

    Model it as RC. The peek() is a backdoor read: the library does not apply read side effects for the backdoor path, so the mirror takes the peeked value and the hardware is not cleared either. The frontdoor read returns the value and then predicts the field as 0, matching the hardware's clear-on-read.

    Check yourself You register a custom policy name with define_access("RWLOCK") and configure a field with it, but add no callback. A write arrives while the lock is set. What does the model predict?

    The written value. Unknown names fall to the default branch of the prediction, which behaves like RW. A registered name adds no behaviour; the lock rule needs a post_predict callback (or a custom predictor).

    Check yourself Why does the bench's callback check path != UVM_BACKDOOR before keeping the old LEN value?

    The "ignored while BUSY" rule belongs to the bus interface. A backdoor deposit writes the flip-flop directly, whatever the DMA's state, so its prediction must not be blocked.

    Next: the model now knows the DMA's rules, but some fields change on their own. Volatile Fields and Mirror Checks decides which bits a check can trust.

    Related topics

    • Up: Register Access

    • Blocks, registers and fields

    • Callbacks: types and hooks

    Previous

    157 / 280

    Next: Volatile Fields and Mirror Checks

    Up next

    Up nextVolatile Fields and Mirror Checks

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Access API: write, read, set, get, update, mirror, predict, peek, poke
    • The Adapter Contract: reg2bus, bus2reg and provides_responses
    • Backdoor Access: HDL Paths and User Backdoors