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 ofvalueand updates the mirror to reflect it, applying each field's access policy (so aW1Cfield predicts the clear, anRWfield 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 aUVM_ERRORif they differ, and finally refreshes the mirror to the read value. The compare is what makesread()self-verifying: you do not supply an expected value, the mirror is the expected value.status(output):UVM_IS_OKif the bus access succeeded,UVM_NOT_OKif it failed (a bus error, an unresolved address). OnUVM_NOT_OK, the returnedvalueis not valid — you must checkstatusbefore using it.path:UVM_FRONTDOOR(default) drives the real bus through the adapter;UVM_BACKDOORreaches 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:
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:
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:
// 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
Ignoring the status return lets a bus error masquerade as a valid read, so the test verifies undefined data
UNCHECKED STATUS// 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.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.
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.
Check status on every access and treat UVM_NOT_OK as a failure with an invalid value:
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
endNow 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
statusoutput. AUVM_NOT_OKaccess 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
mapon a multi-bus register. The access uses the default map unless you pass one (3.2).
8. Industry Best Practices
- Check
statuson every access. MakeUVM_NOT_OKa 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
pathandmapwhen 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 (raisingUVM_ERRORon mismatch), then refreshes the mirror — the auto-check is whyread()self-verifies with no hand-coded expected value.- Always check
status:UVM_NOT_OKmeans 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 amapto pick the bus on a multi-bus register (3.2). - Treat
write()as programming andread()as verification, and let the mirror-based auto-check do the comparing rather than hand-coded expected values.