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. The Access API: write, read, set, get, update, mirror, predict, peek, poke

Part 9 · Register Model (RAL) · Intermediate

The Access API: write, read, set, get, update, mirror, predict, peek, poke

Which of the desired, mirrored and DUT values each register call moves, the path argument and status, field-level calls, and why update() can write nothing for a trigger bit.

On this page

    The first DMA job runs through the register model without a hitch. For the second job, a colleague writes the tidy version: rm.ctrl.go.set(1); rm.ctrl.update(status);. The test then waits for the interrupt, and waits, until the timeout fires. No error, no warning, and the APB monitor shows that no write to CTRL ever went out. The register model did exactly what it was told. The question is what each call actually tells it.

    The idea: three copies of every field

    A register model keeps two values for every field, and the DUT holds a third. The desired value is what you want the field to become. The mirrored value is what the model believes the DUT holds right now. The DUT value is the truth, sitting in a flip-flop. Every access call moves one or more of these, and nearly every RAL bug is a case where you assumed a call moved one that it did not.

    Think of a kitchen. The desired value is your shopping list, the mirror is the inventory sheet taped to the pantry door, and the DUT is the pantry itself. Writing on the list changes nothing in the pantry. Restocking the pantry is pointless if nobody updates the sheet. And the sheet is only as good as the last time someone actually looked inside. update() is the person who compares the list with the sheet, not with the pantry, and shops only for what differs. That one detail is the whole opening bug.

    The picture: which call moves which value

    Flowchart: set and randomize change only the desired value; write, update, read and mirror go through the bus to the DUT and are then predicted into the mirror; peek and poke go through the backdoor and also update the mirror; predict and reset change the mirror; every prediction also sets the desired value.
    The three values. Only frontdoor and backdoor accesses touch the DUT; set() and randomize() touch only the desired value; predict() and reset() touch only the model.

    One arrow surprises most people: every prediction overwrites the desired value too. In the library, uvm_reg_field::do_predict ends by setting the mirrored value, the desired value and the field's value member to the same number. So a set() you have not yet pushed with update() is lost the moment any access to that register is predicted.

    Build it: the calls, side by side

    Call

    Path

    Desired

    Mirrored

    DUT

    Takes time

    set(v) / get()

    none

    set / read

    -

    -

    no

    randomize()

    none

    random value

    -

    -

    no

    write(st, v)

    frontdoor (default)

    v

    predicted

    written

    yes

    update(st)

    frontdoor

    -

    predicted

    written only if needs_update()

    yes, or no access at all

    read(st, v)

    frontdoor

    predicted

    predicted

    read

    yes

    mirror(st, check)

    frontdoor

    predicted

    predicted

    read; compared if UVM_CHECK

    yes

    predict(v)

    none

    v

    v

    -

    no

    peek(st, v) / poke(st, v)

    backdoor

    predicted

    predicted

    read / written

    no bus time

    reset()

    none

    reset value

    reset value

    -

    no

    • `needs_update()` is true when any field's mirrored value differs from its desired value, or the field is volatile. Read-only fields (RO, RC, RS) never count. update() calls it first and returns at once, with UVM_IS_OK, when it is false.

    • The path argument. write, read, update and mirror take UVM_FRONTDOOR or UVM_BACKDOOR (the default comes from the block, normally frontdoor). write(st, v, UVM_BACKDOOR) is not the same as poke(): it emulates the field policies, so a W1C bit written with 1 is cleared, while poke() deposits the raw value. The same holds for read(..., UVM_BACKDOOR) against peek() for read-clear fields.

    • Status. Every task returns a uvm_status_e: UVM_IS_OK, UVM_NOT_OK (a bus error, or no backdoor), or UVM_HAS_X. Ignoring it is how a test that never reached the DUT passes. Do not count on UVM_HAS_X to catch X on the bus: uvm_reg_bus_op.data is 2-state (uvm_reg_data_t is bit), so through a standard adapter an X or Z read arrives as 0, the status is UVM_IS_OK, and the mirror takes the 0. Catch X where it is still 4-state: an assertion on PRDATA, or a check in the monitor.

    • Randomize the model, then push it. The model's rand fields (the bench's irq_en, LEN's and KEY's value) randomize like any object, and update() writes only what changed: check the result of regmodel.randomize() with { len.value.value inside {[1:16]}; }, then update the configuration registers one by one (regmodel.len.update(status);, regmodel.key.update(status);). Not the whole block: a block-level update() also writes STATUS back and clears pending DONE or ERR. A field configured with is_rand = 0 keeps its value.

    • `reset()` is model-only. It loads reset values into the desired and mirrored values; it never touches the DUT. Call it when the DUT is reset, so the mirror agrees with the hardware again.

    Here is how the bench's job test drives a whole DMA job by name. write() sets the desired value and drives the bus in one call, which is why the bench never needs set() for a one-off access:

    design.sv
    1
    2
    3
    4
    5
    6
    // snippet: from dma_ral_job_test in the course DMA bench (dma_ral_tb.sv)
    rm.key.write(st, 32'hCAFE_F00D);
    rm.len.write(st, 6);
    // GO is a trigger: write CTRL directly. update() would skip it on the next
    // job, because the mirror already holds GO = 1 from this write.
    rm.ctrl.write(st, 32'h3);

    Note

    In IEEE 1800.2 the path type is uvm_door_e (UVM_FRONTDOOR, UVM_BACKDOOR, UVM_DEFAULT_DOOR). UVM 1.2 calls it uvm_path_e with UVM_DEFAULT_PATH. The 2020 library keeps uvm_path_e as a typedef, so code using the enum literals compiles on both.

    Field-level calls versus register-level calls

    rm.status.done.write(st, 1) looks like a one-bit write. On APB it cannot be: the bus has no byte enables, and the bench configures every field with individually_accessible = 0. The library then writes the whole register, and it has to invent values for the other fields. It uses 0 for W1C, W1S, W1T, RO, RC and RS neighbours (so it does not clear or set them by accident), all ones for W0-type neighbours, and the mirrored value for everything else, including RW and WO fields. For STATUS that is harmless. For CTRL it is not, because GO is WO and its mirror still holds the 1 from the last job.

    Watch it run

    The scratch test below walks KEY through every call in order and prints the three values after each step (the DUT column is read straight from dma_bd_if, so it does not disturb the mirror). The env uses the bench's default, explicit prediction.

    Diagram
    scratch run: +UVM_TESTNAME=s_values_test (KEY; the DUT column is read through dma_bd_if)
    UVM_INFO sacc_tb.sv(36) @ 0: uvm_test_top [VAL] after reset                  desired=0        mirrored=0        DUT=0        needs_update=0
    UVM_INFO sacc_tb.sv(36) @ 0: uvm_test_top [VAL] set(1111_1111)               desired=11111111 mirrored=0        DUT=0        needs_update=1
    UVM_INFO sacc_tb.sv(36) @ 75000: uvm_test_top [VAL] update() returns             desired=11111111 mirrored=0        DUT=0        needs_update=1
    UVM_INFO sacc_tb.sv(36) @ 76000: uvm_test_top [VAL] update() + 1ns               desired=11111111 mirrored=11111111 DUT=11111111 needs_update=0
    UVM_INFO sacc_tb.sv(36) @ 76000: uvm_test_top [VAL] predict(2222_2222)           desired=22222222 mirrored=22222222 DUT=11111111 needs_update=0
    UVM_INFO sacc_tb.sv(36) @ 115000: uvm_test_top [VAL] mirror() returns             desired=22222222 mirrored=22222222 DUT=11111111 needs_update=0
    UVM_INFO sacc_tb.sv(36) @ 116000: uvm_test_top [VAL] mirror() + 1ns               desired=11111111 mirrored=11111111 DUT=11111111 needs_update=0
    UVM_INFO sacc_tb.sv(36) @ 156000: uvm_test_top [VAL] write(3333_3333) + 1ns       desired=33333333 mirrored=33333333 DUT=33333333 needs_update=0
    UVM_WARNING dma_ral_tb.sv(195) @ 156000: reporter [BD_RO] this backdoor is read-only
    UVM_WARNING reg/uvm_reg.svh(1664) @ 156000: reporter [PREDICT_NOK] status UVM_NOT_OK; skip prediction.
    UVM_INFO sacc_tb.sv(50) @ 156000: uvm_test_top [VAL] poke status=UVM_NOT_OK
    UVM_INFO sacc_tb.sv(36) @ 156000: uvm_test_top [VAL] reset()                      desired=0        mirrored=0        DUT=33333333 needs_update=0
    UVM_INFO sacc_tb.sv(36) @ 156000: uvm_test_top [VAL] randomize()                  desired=bf95e7fb mirrored=0        DUT=33333333 needs_update=1
    UVM_INFO sacc_tb.sv(36) @ 156000: uvm_test_top [VAL] peek()                       desired=33333333 mirrored=33333333 DUT=33333333 needs_update=0
    • `update() returns` with the mirror still 0. The DUT column also shows 0 here, but that is the backdoor-versus-clock race from lesson 6: the flop takes the value at the edge that ends the APB transfer, and a read in the same time step can still see the old one. The explicit predictor updates the mirror a moment later in that same time step. One nanosecond later all three agree. Lesson 3 is about exactly this gap.

    • `predict()` lies on purpose. The mirror says 2222_2222 while the DUT holds 1111_1111, and needs_update is 0: the model now believes there is nothing to write.

    • `mirror()` repairs it. It reads the DUT and the prediction overwrites both desired and mirrored. With UVM_CHECK it would have reported the mismatch first.

    • `poke()` fails cleanly. The bench's backdoor refuses writes, so the status is UVM_NOT_OK and the library skips the prediction (PREDICT_NOK).

    • `reset()` and `randomize()` never touch the DUT. After reset() the model says 0 while the DUT holds 3333_3333. A peek() brings the model back to the truth.

    What breaks: a trigger bit and update()

    This is the opening bug, and it is war story 13 from the bench. CTRL.GO is a trigger: writing 1 starts a job, and it always reads back 0. The model declares it WO. When the first job writes CTRL with 3, the predictor applies the WO policy, which predicts the written value, so GO's mirror becomes 1 and stays 1. A read of CTRL does not fix it, because a read never changes the mirror of a WO field. Then go.set(1) makes desired = 1 = mirrored, needs_update() is false, and update() returns without a bus access. The scratch run proves it:

    Diagram
    scratch run: s_go_update_test
    UVM_INFO sacc_tb.sv(80) @ 325000: uvm_test_top [GO] job 1 done; CTRL reads 2, GO mirror=1
    UVM_INFO sacc_tb.sv(83) @ 325000: uvm_test_top [GO] go.set(1): desired=1 mirrored=1 needs_update=0
    UVM_INFO sacc_tb.sv(87) @ 395000: uvm_test_top [GO] after update(): STATUS=0 (busy=0)
    UVM_INFO sacc_tb.sv(90) @ 395000: uvm_test_top [GO] go.predict(0); go.set(1): needs_update=1
    UVM_INFO sacc_tb.sv(94) @ 505000: uvm_test_top [GO] after update(): STATUS=1 (busy=1)
    UVM_INFO dma_tb.sv(411) @ 685000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
    UVM_ERROR :    0

    The bench's fix is the plain one: write CTRL directly with write(st, 3) for every job, because a trigger is an action, not a state. If a sequence must use update(), tell the model the trigger self-cleared with go.predict(0) after each use, as the second half of the log does, or put the same rule in a callback (lesson 4).

    The field-level trap has the same root. After the first job, the scratch test clears DONE with a field write and then turns off the interrupt with rm.ctrl.irq_en.write(st, 0). That field write becomes a whole-register write with GO taken from its mirror, which is 1, so it starts a second job nobody asked for:

    Diagram
    scratch run: s_field_write_test
    UVM_WARNING reg/uvm_reg_field.svh(1831) @ 225000: reporter [RegModel] Individual field access not available for field 'regmodel.status.done'. Accessing complete register instead.
    UVM_INFO sacc_tb.sv(116) @ 305000: uvm_test_top [FLD] after status.done.write(1): STATUS=0
    UVM_WARNING reg/uvm_reg_field.svh(1831) @ 305000: reporter [RegModel] Individual field access not available for field 'regmodel.ctrl.irq_en'. Accessing complete register instead.
    UVM_INFO sacc_tb.sv(120) @ 415000: uvm_test_top [FLD] after ctrl.irq_en.write(0): STATUS=1 (busy=1)

    The DMA was busy again 110 ns after a write that only meant to clear IRQ_EN. If your bus has byte enables and the field fills its own byte lanes, field access can be truly individual (see the adapter contract). On a bus like APB3, write the register.

    Interview

    "What is the difference between `write()` and `set()` followed by `update()`?"

    A weak answer: "They do the same thing; update() is just the two-step version."

    A strong answer: "write() always drives the bus. set() only changes the desired value, and update() drives the bus only if needs_update() finds a field whose desired value differs from the mirror, or a volatile field. So update() is a diff against the model's belief, not against the hardware. It is right for configuring many registers from a randomized model, and wrong for trigger bits: once a WO trigger's mirror holds 1, setting it to 1 again writes nothing. Also, any prediction overwrites the desired value, so a set() you never pushed can be lost."

    Follow-ups: Does `predict()` touch the DUT? (No; it sets mirrored and desired, and it warns if the register is mid-access.) What does `reset()` do to the hardware? (Nothing; it reloads the model's reset values.)

    Check yourself A sequence calls rm.len.set(10), then rm.len.read(st, v) to check the old value, then rm.len.update(st). LEN is RW and the DUT holds 6. What does the bus see?

    A read of 6 and no write. The read's prediction sets both the mirrored and the desired value to 6, so the set(10) is overwritten before update() runs, and needs_update() is false. Do the read first, or use write(st, 10).

    Check yourself Why does rm.status.done.write(st, 1) clear only DONE, while rm.ctrl.irq_en.write(st, 0) starts a job?

    Both become whole-register writes because the field is not individually accessible on APB. The library fills W1C neighbours (ERR) and RO neighbours (BUSY) with 0, so the STATUS write touches only DONE. In CTRL the neighbour GO is WO, so it is filled from its mirror, which holds 1 from the last job: the write carries GO=1.

    Check yourself After a DUT reset in the middle of a test, mirror checks start failing on registers nobody wrote. What call is missing?

    regmodel.reset() when the DUT reset is applied. The DUT went back to its reset values but the mirror kept the old ones. reset() reloads the model's reset values into the mirrored and desired values without any bus traffic.

    Next: every frontdoor call above ends up as an apb_item on the bus. The Adapter Contract opens the translator in the middle and shows what it must promise.

    Related topics

    • Up: Register Access

    • The frontdoor path, end to end

    • Blocks, registers and fields

    Previous

    154 / 280

    Next: The Adapter Contract: reg2bus, bus2reg and provides_responses

    Up next

    Up nextThe Adapter Contract: reg2bus, bus2reg and provides_responses

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Prediction: Auto, Explicit and Passive
    • Field Access Policies and Callbacks
    • Volatile Fields and Mirror Checks