RAL Backdoor Access
Every register access described so far — frontdoor, through reg.write()/read(), the adapter, and a real sequencer — travels the same path an actual bus master would use, at real protocol timing. Backdoor access takes an entirely different route: reaching directly into the DUT's internal register storage through its HDL hierarchy, skipping the bus, the adapter, and the driver altogether. This page covers when that's actually the right tool, how to wire it up, and where it can go wrong.
Why bypass the bus at all
- Speed — initializing many registers to specific values before a directed test even starts, without spending simulation time on protocol transactions for setup that isn't what the test is actually checking.
- Reaching what the bus can't —
STATUS.runningis read-only from the bus's perspective by design; backdoorpeek()can read it directly for a check without needing the design to expose a write path that shouldn't exist. In some testbenches,poke()can also force a specific internal condition for a corner-case test the protocol has no legal sequence to reach.
Neither of these makes backdoor access a substitute for frontdoor testing — a backdoor write updates RAL's model of the register, but no actual bus transaction ever happened. Anything only ever verified through the backdoor hasn't verified the bus path at all.
Wiring an HDL path
class pwm_reg_block extends uvm_reg_block;
// ... ctrl/period/duty/status declared as in the previous two pages ...
virtual function void build();
// ... registers created, configured, and added to default_map as before ...
set_hdl_path_root("tb_top.dut");
endfunction
endclass
class pwm_ctrl_reg extends uvm_reg;
// ... fields as in The Register Abstraction Layer ...
virtual function void build();
// ... fields created and configured as before ...
add_hdl_path_slice("ctrl_q", 0, 32);
endfunction
endclass
set_hdl_path_root(), called once at the block level, gives every register below a common hierarchical prefix instead of repeating a full path per register. add_hdl_path_slice(name, offset, size), called per register, names the actual HDL signal holding that register's storage — resolved relative to the root, so "ctrl_q" here means tb_top.dut.ctrl_q must be the exact signal name the DUT's RTL uses for the CTRL register. pwm_period_reg, pwm_duty_reg, and pwm_status_reg each get their own add_hdl_path_slice() call the same way, naming period_q, duty_q, and status_q respectively.
Frontdoor access only ever cares about the bus protocol — it works identically no matter what the DUT's internal signals happen to be named. Backdoor access has no such independence: add_hdl_path_slice()'s string is a literal hierarchical reference into the DUT, and it silently stops resolving to anything real the moment the RTL is refactored and a signal gets renamed. This page's wiring only works as long as the DUT side is fixed and known.
peek() and poke(): the backdoor equivalents of read()/write()
uvm_status_e status;
uvm_reg_data_t rdata;
regmodel.status.peek(status, rdata); // reads tb_top.dut.status_q directly
regmodel.ctrl.poke(status, 32'h3); // deposits directly into tb_top.dut.ctrl_q
peek()/poke() still update RAL's mirrored value, the same as a predicted frontdoor access would — but neither goes anywhere near the sequencer, driver, or adapter. No bus cycle is spent, and no protocol-level timing applies at all. Internally, both resolve to the same primitive HDL deposit/read routines HDL Backdoor Access Routines covers directly, for testbenches with no register model in play at all — RAL's peek()/poke() is a convenience layer over exactly that mechanism, the same relationship uvm_config_db has to uvm_resource_db from Resource DB & Config DB.
Timing gotchas
poke() deposits a value instantly, with no respect for the DUT's clock edge or any internal pipeline/latch stage a real bus write would pass through. Poking a register the instant before the DUT's own logic was already about to update or clear it, on that same clock edge, is a genuine race — the backdoor write can be silently overwritten one delta cycle later by the DUT's own always block, with no error or warning raised anywhere. The usual guard is to poke only during reset or before the clock starts toggling, not mid-simulation while the DUT is actively running.
Fields folded into combinational logic, renamed by a synthesis-adjacent tool transform, or living inside a vendor IP block with no visible internal signal, have no single flat HDL path pointing at their storage — add_hdl_path_slice() has nothing correct to name. Backdoor access is a simulation-only, RTL-hierarchy-dependent feature; it isn't guaranteed to exist for every register in every DUT.
poke() can't reproduce special field hardware behaviorpoke() is a raw deposit onto a storage element — it forces a value in, with no awareness of any special access semantics a field's access policy implies. A field configured "W1C" (write-one-to-clear), for instance, only clears on a frontdoor write where a 1 is written to it; poke()-ing that same storage element straight to 0 sets the stored value directly, bypassing the clear-on-write-one hardware behavior entirely, since there's no logic in the path for poke() to trigger. Anywhere a field's real hardware behavior on write matters (not just its resting value), frontdoor write() is the only way to exercise it — poke() is for setting up state cheaply beforehand, not for testing how a field actually responds to being written.
Checking whether it actually worked
uvm_reg::is_hdl_path_defined() returns whether a register has any HDL path configured at all. Checking this before relying on peek()/poke() in a sequence turns a missing or misspelled add_hdl_path_slice() call into an early, readable failure, instead of a silent no-op that leaves a test passing for the wrong reason.
What's next
The frontdoor and backdoor pieces are both in place now. The next page assembles the complete PWM register-block testbench — register model, adapter, predictor, and backdoor paths together — as one worked example, the same way Example Walkthrough did for the FIFO testbench.