Skip to content

UVM RAL · Chapter 4 · Accessing Registers

Register Read & Write APIs

Write and read are the two operations you call more than any others, and most access-level confusion comes from a fuzzy picture of what each does. Write drives a real bus write and updates the mirror to reflect what it wrote, applying each field's policy. Read drives a real bus read, hands you the value, then automatically compares that value against the mirror and raises an error if they disagree before refreshing the mirror. That auto-check is the whole reason read is worth more than a bare bus read, since it turns every read into a check with no hand-coded expected value. Both share the same argument shape, including a status output you must examine. This page walks through each operation and breaks a test that ignores the status return, so a bus error is silently swallowed and invalid data is used as if it were real.

Foundation11 min readUVM RALreadwriteAuto-checkstatusFrontdoor

Chapter 4 · Section 4.1 · Accessing Registers

1. Why Should I Learn This?

write() and read() are where verification actually happens, and two things about them are constantly misunderstood: that read() checks (it is not a passive observation — it compares against the mirror and can fail the test), and that the status output must be examined (an access can fail at the bus level and hand you meaningless data). Get the first wrong and you either fear reads that are doing their job or lean on hand-coded checks you did not need; get the second wrong and you let real bus errors sail through while using garbage as if it were a register value.

Learning exactly what write() and read() do — the mirror update on write, the auto-check and refresh on read, and the meaning of status — is what lets you use the access model as the self-checking tool it is, and to trust or distrust an access for the right reasons. It is the foundation the rest of Chapter 4 builds on.

2. Industry Story — the error nobody checked

A test configures a peripheral and then polls a STATUS register in a loop, reading it until a done bit sets. The bus is an AXI-Lite-style interconnect that can return an error response (SLVERR) when an access hits a decode hole or an unmapped region. During a refactor, a register's address is temporarily wrong, so the STATUS reads return a bus error — and with the error, an undefined data value.

The test ignores the status output of read(). So instead of failing on the bus error, it takes the undefined value returned alongside it, checks the done bit inside that garbage, and — on the seeds where the garbage happens to have done set — passes. The suite is green while the register access is actually erroring on every call. The bug is found only much later, when the same wrong address causes a different, louder failure, and the team realizes the register poll had been reading error responses for weeks without complaint. The post-mortem rule: every register access returns a status, and a UVM_NOT_OK means the value handed back is invalid — a test that does not check status cannot tell a successful read from a bus error, and will happily verify garbage.

3. Concept — what write() and read() do

Both take the same argument shape: 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, ...) (and read similarly with value as an output).

  • write(status, value, ...) drives a bus write of value and updates the mirror to reflect it, applying each field's access policy (so a W1C field predicts the clear, an RW field predicts the value). It does not read anything back or check anything; its job is to make the change and record it in the mirror.
  • read(status, value, ...) drives a bus read, returns the value the DUT gave, then — by default — compares that value against the mirror and raises a UVM_ERROR if they differ, and finally refreshes the mirror to the read value. The compare is what makes read() self-verifying: you do not supply an expected value, the mirror is the expected value.
  • status (output): UVM_IS_OK if the bus access succeeded, UVM_NOT_OK if it failed (a bus error, an unresolved address). On UVM_NOT_OK, the returned value is not valid — you must check status before using it.
  • path: UVM_FRONTDOOR (default) drives the real bus through the adapter; UVM_BACKDOOR reaches the RTL directly (4.3).
  • map: which map/bus to use when a register has several (3.2); the default map otherwise.

Here is what read() does, including the two branches that decide whether it checks and what it returns:

read() flow: drive bus read, check bus status, compare against mirror, refresh mirror, returnnoyesnoyesthenreg.read(status,value)drive frontdoor busreadbus statusOK?status =UVM_NOT_OK;value INVALIDcompare returnedvalue vs mirrormatchesmirror?UVM_ERROR(mismatch)refresh mirror =read valuestatus =UVM_IS_OK;return value
Figure 1 — what read() does. It drives a frontdoor bus read; if the bus status is not OK, it returns UVM_NOT_OK and the value is invalid (you must check status). If the bus succeeded, read compares the returned value against the mirror and raises a UVM_ERROR on mismatch — this auto-check is why read() self-verifies. Either way it refreshes the mirror to the read value and returns. write() is simpler: drive the bus write and update the mirror, no check.

4. Mental Model — write records, read verifies

5. Working Example — write, read, and the auto-check

The workhorse pattern is a write followed by a self-checking read — and every access checks its status:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
uvm_status_e status;  logic [31:0] got;
 
// write(): drives the bus and updates the mirror to the written value. Check status.
ctrl.write(status, 32'hABCD_0001);
if (status != UVM_IS_OK)
  `uvm_error("REG", "CTRL write failed on the bus")
 
// read(): drives the bus, AUTO-COMPARES the returned value against the mirror
// (raising UVM_ERROR on mismatch), refreshes the mirror, and returns the value.
ctrl.read(status, got);
if (status != UVM_IS_OK)
  `uvm_error("REG", "CTRL read failed on the bus — 'got' is INVALID, do not use it")
// No hand-coded expected value: the read already checked 'got' against the mirror
// (which the write set to 0xABCD_0001). A DUT that returned anything else already
// raised a UVM_ERROR inside read().

The auto-check is what a mismatch demonstrates — you do not write the compare:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Suppose the DUT wrongly returns 0xABCD_0000 on the read above.
ctrl.read(status, got);
// Inside read(): status is UVM_IS_OK (the bus succeeded), but the returned value
// 0xABCD_0000 != mirror 0xABCD_0001, so read() itself raises:
//   UVM_ERROR: CTRL value read back 0xABCD0000, expected (mirror) 0xABCD0001
// You wrote no comparison — the mirror was the expected value.

write() records; read() verifies against that record; status says whether to trust the bus at all. That triad is the frontdoor access model.

6. Debugging Session — the status that was never checked

1

Ignoring the status return lets a bus error masquerade as a valid read, so the test verifies undefined data

UNCHECKED STATUS
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The register's address is temporarily wrong, so the bus returns an ERROR response.
uvm_status_e status;  logic [31:0] got;
forever begin
  status.read(status, got);      // BUG: status is UVM_NOT_OK; 'got' is undefined garbage
  if (got[0] == 1'b1) break;     // checks 'done' inside garbage; passes on lucky seeds
end
// The bus is erroring on every read, but the loop never notices, and 'got' is meaningless.
Symptom

The register poll sometimes passes and sometimes hangs, seed-dependent, even though the bus is erroring on every single access. On the passing seeds, the undefined value returned with the error happens to have the polled bit set, so the loop exits and the test proceeds as if the register were healthy; on others it spins. Nothing reports the bus error, so the failure — when it finally surfaces elsewhere — looks unrelated, and the fact that the register access had been erroring for a long time is invisible in the logs. The DUT never got a clean access, but the test never said so.

Root Cause

The status output of read() was never checked. Every register access reports whether the bus transaction succeeded, and a UVM_NOT_OK means the transaction failed (here, a bus error from a wrong address) and the returned value is invalid — not a register value at all, just whatever came back with the error. By ignoring status and using the returned value directly, the test cannot distinguish a successful read from an errored one, so it verifies undefined data and, on the seeds where that data happens to satisfy its condition, passes. The bus was failing the whole time; the test simply never asked whether it had succeeded.

Fix

Check status on every access and treat UVM_NOT_OK as a failure with an invalid value:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
forever begin
  status_reg.read(status, got);
  if (status != UVM_IS_OK)
    `uvm_fatal("REG", "STATUS read errored on the bus — access is failing")
  if (got[0] == 1'b1) break;   // only trust 'got' once status is OK
end

Now the bus error fails the test immediately and loudly, at the real point of failure, instead of being laundered into a passing poll. The rule the bug teaches: every register access returns a status; a UVM_NOT_OK means the value is invalid, so check status before using the value — an unchecked access cannot tell a good read from a bus error and will verify garbage.

7. Common Mistakes

  • Ignoring the status output. A UVM_NOT_OK access returns an invalid value; using it hides bus errors and verifies garbage.
  • Thinking read() is a passive observation. It auto-compares against the mirror and can fail the test — that check is its purpose.
  • Hand-coding an expected value for a read. The mirror already is the expected value; a manual compare duplicates (and can contradict) the auto-check.
  • Assuming write() checks something. It drives and updates the mirror; it does not read back or verify — a read does that.
  • Forgetting to pick the map on a multi-bus register. The access uses the default map unless you pass one (3.2).

8. Industry Best Practices

  • Check status on every access. Make UVM_NOT_OK a loud failure; never use a value whose access did not succeed.
  • Let read() do the checking. Rely on the mirror-based auto-check instead of hand-coded expected values; it is the self-checking the model exists to provide.
  • Use write() to program, read() to verify. Keep the roles distinct — write records intent, read confirms reality.
  • Be explicit about path and map when they matter. Frontdoor vs backdoor and which bus are deliberate choices; default only when the default is what you mean.
  • Prefer field access to change one field (2.2). A register write sets every field; use the field API when you mean one.

9. Interview / Review Questions

10. Key Takeaways

  • write(status, value, ...) drives a bus write and updates the mirror to what it wrote (per each field's policy); it does not read back or check.
  • read(status, value, ...) drives a bus read, auto-compares the returned value against the mirror (raising UVM_ERROR on mismatch), then refreshes the mirror — the auto-check is why read() self-verifies with no hand-coded expected value.
  • Always check status: UVM_NOT_OK means the bus access failed and the returned value is invalid — using it masks bus errors and verifies garbage.
  • Both share the argument shape (status, value, path, map) — frontdoor by default, backdoor available (4.3), and a map to pick the bus on a multi-bus register (3.2).
  • Treat write() as programming and read() as verification, and let the mirror-based auto-check do the comparing rather than hand-coded expected values.