UVM register access methods are the calls a test or sequence uses to work with the register model: write() and read() perform real bus transfers, set() and update() change values in two steps, mirror() reads and optionally checks a register, and predict() changes what the model believes without touching the DUT. Each one acts on a different copy of the register's value, and knowing which copy is the key to using them correctly.

The examples use the traffic controller register model from UVM Register Model Example, where m_ral_model.cfg holds a control register ctrl and timer registers timer[0] and timer[1].

9 min read | Intermediate

What You'll Learn

  • Explain the difference between the desired value, the mirrored value and the DUT value
  • Use write(), read(), set(), update(), mirror() and predict() correctly
  • Check the status of every bus access
  • Keep the mirror accurate with auto-prediction or a predictor

Three Copies of Every Register

Every uvm_reg and uvm_reg_field in the model holds two values, and the DUT holds a third:

  • The DUT value is what is really in the hardware register.
  • The mirrored value is what the model believes the DUT holds. It is updated from the bus traffic the model knows about.
  • The desired value is what you want the DUT to hold. Normally it equals the mirrored value; it only differs after a set() that hasn't been written yet.

Every method in this article reads or changes one or more of these three. When a register check fails unexpectedly, the first question is always: which copy changed, and which one did I compare against?

Syntax

All bus-access methods are tasks with a status output first, followed by optional arguments with defaults. The ones you will use most often are shown here for uvm_reg; uvm_reg_field has the same methods.


// Bus access (tasks; consume time)
task write (output uvm_status_e status, input uvm_reg_data_t value,
            input uvm_path_e path = UVM_DEFAULT_PATH, input uvm_reg_map map = null,
            input uvm_sequence_base parent = null, ...);
task read  (output uvm_status_e status, output uvm_reg_data_t value,
            input uvm_path_e path = UVM_DEFAULT_PATH, input uvm_reg_map map = null,
            input uvm_sequence_base parent = null, ...);
task update(output uvm_status_e status, input uvm_path_e path = UVM_DEFAULT_PATH, ...);
task mirror(output uvm_status_e status, input uvm_check_e check = UVM_NO_CHECK,
            input uvm_path_e path = UVM_DEFAULT_PATH, ...);

// Model only (functions; no bus traffic)
function void           set(uvm_reg_data_t value, ...);
function uvm_reg_data_t get(...);
function uvm_reg_data_t get_mirrored_value(...);
function bit            predict(uvm_reg_data_t value, uvm_reg_byte_en_t be = -1,
                                uvm_predict_e kind = UVM_PREDICT_DIRECT, ...);
function bit            needs_update();
function void           reset(string kind = "HARD");

The status output is UVM_IS_OK, UVM_NOT_OK or UVM_HAS_X. The path argument chooses UVM_FRONTDOOR (through the bus agent) or UVM_BACKDOOR (directly into the RTL through an HDL path); the default lets the model decide, which is front door unless configured otherwise. Back-door access, including peek() and poke(), is covered in UVM Register Backdoor Access.

write() and read()

write() sends a value to the DUT through the bus agent. read() reads the DUT and returns what it got. Both wait until the bus transfer is finished, and both update the mirrored and desired values to match what went over the bus.

Register accesses usually run inside a sequence. Pass the sequence as the parent argument, so the generated bus items run as children of it:


class timer_seq extends uvm_sequence;
  `uvm_object_utils(timer_seq)
  ral_sys_traffic m_ral_model;                       // set by the test before start()
  // Rest of the code (constructor)

  virtual task body();
    uvm_status_e   status;
    uvm_reg_data_t value;

    m_ral_model.cfg.timer[1].write(status, 32'hcafe_feed, .parent(this));
    if (status != UVM_IS_OK) `uvm_error("SEQ", "timer[1] write failed")

    m_ral_model.cfg.timer[1].read(status, value, .parent(this));
    if (status != UVM_IS_OK) `uvm_error("SEQ", "timer[1] read failed")
    if (value != 32'hcafe_feed)
      `uvm_error("SEQ", $sformatf("timer[1] read 0x%0h, expected 0xcafe_feed", value))
  endtask
endclass

Named argument binding such as .parent(this) skips the arguments you don't need to change, which keeps calls readable.

set(), get() and update()

set() changes only the desired value. Nothing happens on the bus. update() then writes the register, but only if its desired value differs from its mirrored value. This is how you change several fields of one register and write them all in a single transfer:


m_ral_model.cfg.ctrl.mod_en.set(1);              // desired value only
m_ral_model.cfg.ctrl.profile.set(1);             // desired value only
`uvm_info("SEQ", $sformatf("ctrl needs update: %0b", m_ral_model.cfg.ctrl.needs_update()), UVM_MEDIUM)
m_ral_model.cfg.ctrl.update(status, .parent(this));   // one bus write with both fields

update() also works on a whole block. m_ral_model.cfg.update(status) writes every register in the block whose desired value has changed, and skips the rest. That makes it the natural way to apply a randomized configuration: randomize the model, then call update() once.

set() follows the field's access policy, just like a real write would. Setting a read-only field does not change its desired value, so update() has nothing to write.

get() returns the desired value, and get_mirrored_value() returns the mirrored value. Neither reads the DUT. To know what is really in the hardware, use read() or mirror().

Quick Check

Q: A sequence calls ctrl.mod_en.set(1) and then ctrl.get(). The DUT never sees a write. What does get() return for mod_en, and what does get_mirrored_value() return?

Show Answer

get() returns 1, because it reports the desired value that set() just changed. get_mirrored_value() still returns the old value, because nothing has been written yet. The two only agree again after update() writes the register.

mirror(): Read and Check

mirror() reads the register from the DUT and updates the mirrored value with what it read. With UVM_CHECK, it first compares the value read against the current mirrored value and reports an error if they differ:


m_ral_model.cfg.ctrl.mirror(status, UVM_CHECK, .parent(this));   // error if DUT != mirror

This is the main self-checking tool of the register layer. After a sequence of writes, a mirror(status, UVM_CHECK) on each register, or on the whole block, confirms that the DUT holds what the model expects.

Fields whose value the DUT changes by itself, such as status bits or counters, would fail this check for no real reason. Exclude them with set_compare(UVM_NO_CHECK) on the field, or mark them volatile in the model, and they are skipped during the comparison.

predict(): Change the Mirror Without the Bus

predict() sets the mirrored and desired value directly, with no bus access. Use it when the DUT changes a register by itself and you know the new value. For example, when the testbench sees an interrupt that sets a status bit, it can predict the new status value, and a later mirror(UVM_CHECK) will check the DUT against it:


// The DUT has just loaded timer[0] with its reload value
void'(m_ral_model.cfg.timer[0].predict(32'h0000_0400));

The default kind, UVM_PREDICT_DIRECT, stores the value as it is. UVM_PREDICT_WRITE and UVM_PREDICT_READ apply the field access policies as if the value had been written or read on the bus, which is what the register predictor uses internally.

Keeping the Mirror Accurate

The mirror is only as good as the information that reaches it. There are two ways to keep it up to date with bus traffic:

  • Auto-prediction: call set_auto_predict(1) on the address map. After each write() or read(), the model predicts the new value itself. It is simple, but it only sees accesses made through the register model.
  • Explicit prediction: connect a uvm_reg_predictor to the bus monitor. It sees every transfer on the bus, including ones from other masters or from sequences that don't use the model, and updates the mirror for each. This is what UVM Register Environment sets up.

Auto-prediction is off by default. If you use neither method, write() and read() still work on the bus, but the mirror never changes and every mirror(UVM_CHECK) compares against stale values.

Calling reset() on a register or block restores the model's mirrored and desired values to their reset values. It does not touch the DUT. Call it when the DUT is reset, so the model and the hardware start from the same point.

Choosing the Right Method

GoalMethodBus traffic
Write a known value nowwrite()Yes
Read the DUT and use the valueread()Yes
Change several fields, then write onceset() then update()Only if changed
Check the DUT against the modelmirror(status, UVM_CHECK)Yes (read)
Tell the model about a DUT-side changepredict()No
Look at what the model holdsget(), get_mirrored_value()No

Common Mistakes

Mistake #1: Ignoring the status output

A bus error comes back as UVM_NOT_OK in status, and the call itself does not stop the test. Check status after every access, or the test carries on with garbage data.

Mistake #2: Expecting get() to read the hardware

get() returns the desired value from the model, without any bus access. Use read() or mirror() when you need the DUT's actual contents.

Mistake #3: No prediction at all

Without auto-prediction or a connected predictor, the mirror never follows the bus. update() then writes registers that don't need it, and mirror(UVM_CHECK) reports false errors.

Mistake #4: Checking volatile fields

Counters and status bits change on their own. Without set_compare(UVM_NO_CHECK) or a predict() of the new value, every mirror(UVM_CHECK) on such a register fails.

Summary

In this article, you learned:

  • Each register has a DUT value, a mirrored value and a desired value
  • write() and read() use the bus and update the mirror; set() changes only the desired value
  • update() writes only registers whose desired value differs from the mirror
  • mirror(status, UVM_CHECK) reads the DUT and compares it against the model
  • predict() updates the model without a bus access, and the mirror needs auto-prediction or a predictor to follow the bus
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5