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()andpredict()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 eachwrite()orread(), 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_predictorto 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
| Goal | Method | Bus traffic |
|---|---|---|
| Write a known value now | write() | Yes |
| Read the DUT and use the value | read() | Yes |
| Change several fields, then write once | set() then update() | Only if changed |
| Check the DUT against the model | mirror(status, UVM_CHECK) | Yes (read) |
| Tell the model about a DUT-side change | predict() | No |
| Look at what the model holds | get(), 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()andread()use the bus and update the mirror;set()changes only the desired valueupdate()writes only registers whose desired value differs from the mirrormirror(status, UVM_CHECK)reads the DUT and compares it against the modelpredict()updates the model without a bus access, and the mirror needs auto-prediction or a predictor to follow the bus