RAL replaces brittle address-constant writes with a named, self-documenting register model. Once you build the model and hook up an adapter, every test interacts with registers by name — and the RAL handles all bus-protocol mechanics transparently.
Without RAL, testbenches write registers by constructing raw bus transactions with hardcoded addresses and bit masks:
// Without RAL — fragile, unreadable, not reusable apb_trans t = apb_trans::type_id::create("t"); t.addr = 32'h4000_0008; // magic address — what register? t.data = 32'h0000_0003; // magic value — which field? t.write = 1; start_item(t); finish_item(t);
This breaks whenever the register map changes, is illegible, and cannot be reused across protocols. With RAL:
// With RAL — readable, portable, self-documenting regmodel.ctrl.write(status, 32'h3, UVM_FRONTDOOR); regmodel.ctrl.read(status, data, UVM_FRONTDOOR);
The RAL also tracks register state (mirror/desired values), provides built-in test sequences, and supports both frontdoor and backdoor access seamlessly.
Each RTL register maps to a class that extends uvm_reg. Inside, you declare fields with uvm_reg_field objects and configure them with access policies and reset values:
// A 32-bit control register with three fields class ctrl_reg extends uvm_reg; `uvm_object_utils(ctrl_reg) // Field handles — public for direct access in tests uvm_reg_field enable; // bit [0] uvm_reg_field mode; // bits [2:1] uvm_reg_field irq_mask; // bit [3] function new(string name = "ctrl_reg"); super.new(name, 32, UVM_NO_COVERAGE); // 32 bits wide endfunction function void build(); enable = uvm_reg_field::type_id::create("enable"); mode = uvm_reg_field::type_id::create("mode"); irq_mask = uvm_reg_field::type_id::create("irq_mask"); // configure(parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, individually_accessible) enable.configure (this, 1, 0, "RW", 0, 8'h0, 1, 1, 1); mode.configure (this, 2, 1, "RW", 0, 8'h0, 1, 1, 1); irq_mask.configure(this, 1, 3, "RW", 0, 8'h0, 1, 1, 1); endfunction endclass
Common field access policies and their meanings:
| Policy | Read | Write | Typical use |
|---|---|---|---|
RW | Returns current value | Sets field | Configuration registers |
RO | Returns current value | No effect | Status/ID registers |
WO | Returns 0 | Sets field | Command registers |
W1C | Returns current value | Writing 1 clears bit | Interrupt status registers |
W1S | Returns current value | Writing 1 sets bit | Set-only control bits |
RC | Returns value then clears | No effect | Clear-on-read status |
The register block groups all registers and creates the address map that maps each register to its bus address offset:
class my_reg_block extends uvm_reg_block; `uvm_object_utils(my_reg_block) // Register handles ctrl_reg ctrl; status_reg status; irq_reg irq; // Register map (address map) uvm_reg_map apb_map; function new(string name = "my_reg_block"); super.new(name, UVM_NO_COVERAGE); endfunction function void build(); // Create and build register instances ctrl = ctrl_reg::type_id::create("ctrl"); status = status_reg::type_id::create("status"); irq = irq_reg::type_id::create("irq"); ctrl.build(); status.build(); irq.build(); // Create address map: base_addr=0x4000_0000, n_bytes=4, endian=UVM_LITTLE_ENDIAN apb_map = create_map("apb_map", 32'h4000_0000, 4, UVM_LITTLE_ENDIAN); // Add registers to the map with their offsets apb_map.add_reg(ctrl, 32'h00, "RW"); // @ 0x4000_0000 apb_map.add_reg(status, 32'h04, "RO"); // @ 0x4000_0004 apb_map.add_reg(irq, 32'h08, "RW"); // @ 0x4000_0008 lock_model(); // freeze the model after build endfunction endclass
The adapter is the bridge between the RAL's generic bus operation struct and your protocol-specific sequence item. You implement two functions: reg2bus() and bus2reg():
class apb_reg_adapter extends uvm_reg_adapter; `uvm_object_utils(apb_reg_adapter) function new(string name = "apb_reg_adapter"); super.new(name); supports_byte_enable = 0; // APB does not support byte enables provides_responses = 0; // read data comes back via bus2reg, not rsp endfunction // RAL → bus: convert uvm_reg_bus_op into protocol sequence item function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw); apb_trans t = apb_trans::type_id::create("t"); t.addr = rw.addr; t.data = rw.data; t.write = (rw.kind == UVM_WRITE); return t; endfunction // bus → RAL: copy bus response data back into the bus_op struct function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw); apb_trans t; if (!$cast(t, bus_item)) `uvm_fatal("ADAPT", "bus_item is not apb_trans") rw.kind = t.write ? UVM_WRITE : UVM_READ; rw.addr = t.addr; rw.data = t.data; rw.status = UVM_IS_OK; endfunction endclass
Frontdoor access issues real bus transactions through your driver and physical interface. It fully exercises the bus protocol, takes simulation time, and exercises the DUT's bus interface logic. This is the default access mode:
// In a test sequence — frontdoor write and read task body(); uvm_status_e status; uvm_reg_data_t rdata; // Write to ctrl register — goes through APB driver regmodel.ctrl.write(status, 32'h5, UVM_FRONTDOOR); if (status != UVM_IS_OK) `uvm_error("REG", "ctrl write failed") // Read back — issues real APB read transaction regmodel.ctrl.read(status, rdata, UVM_FRONTDOOR); `uvm_info("REG", $sformatf("ctrl readback = 0x%08h", rdata), UVM_MEDIUM) // Access individual field regmodel.ctrl.enable.set(1); // set desired value regmodel.ctrl.update(status, UVM_FRONTDOOR); // write only changed fields endtask // Integration: set map's sequencer and adapter in env connect_phase function void connect_phase(uvm_phase phase); regmodel.apb_map.set_sequencer(apb_agent.sequencer, adapter); regmodel.apb_map.set_base_addr(32'h4000_0000); endfunction
Backdoor access directly reads or writes RTL memory variables using DPI-C or VPI, consuming zero simulation time. It is ideal for test setup (pre-loading registers without driving bus protocol) and fast golden-value checking:
// Backdoor write — no bus activity, zero sim time regmodel.ctrl.write(status, 32'hFF, UVM_BACKDOOR); // Backdoor read — reads directly from RTL hierarchy regmodel.ctrl.read(status, rdata, UVM_BACKDOOR); // HDL path must be registered in the reg block // In uvm_reg_block.build(): add_hdl_path("tb_top.dut"); // DUT HDL scope prefix // In ctrl_reg.build(), register the RTL register path: add_hdl_path_slice("ctrl_reg_rtl", 0, 32); // signal, offset, size // Example: check post-reset state instantly (zero time) task body(); uvm_status_e status; uvm_reg_data_t rdata; regmodel.ctrl.read(status, rdata, UVM_BACKDOOR); if (rdata !== regmodel.ctrl.get_reset()) `uvm_error("RST", "ctrl reset value mismatch") endtask
| Feature | Frontdoor | Backdoor |
|---|---|---|
| Simulation time | Yes — real bus cycles | Zero |
| Tests bus interface | Yes | No |
| Requires adapter | Yes | No |
| Requires HDL path | No | Yes |
| Use for | Protocol testing, functional tests | Fast init, reset check, debug |
Every UVM register field has two software-side copies maintained by the RAL:
set() or write().read() or explicit mirror() call returns data from the DUT.// set() updates desired only — no bus activity regmodel.ctrl.enable.set(1); `uvm_info("RAL", $sformatf("Desired=0x%h Mirror=0x%h", regmodel.ctrl.get(), // desired regmodel.ctrl.get_mirrored_value() // mirror ), UVM_MEDIUM) // update() writes only the fields whose desired != mirror regmodel.ctrl.update(status, UVM_FRONTDOOR); // mirror() reads hardware and updates the mirror value // check=UVM_CHECK also compares against the current mirror regmodel.ctrl.mirror(status, UVM_CHECK, UVM_FRONTDOOR); // predict() updates mirror without bus access (for HW-driven changes) regmodel.ctrl.predict(32'h3); // tell RAL what HW changed it to
reg.mirror(status, UVM_CHECK) to read hardware and check against the RAL's expectation, or reg.predict() to update the mirror to the new hardware value.UVM ships with standard register test sequences that run automatically against your entire register model. They are a fast sanity check before writing protocol-specific tests:
| Sequence | What it tests |
|---|---|
uvm_reg_hw_reset_seq | Reads every register after reset and compares to the declared reset value |
uvm_reg_bit_bash_seq | Walks a 1-bit through every writable field (checks field width and access) |
uvm_reg_access_seq | Writes a pattern to every register and reads it back to verify bus connectivity |
uvm_mem_walk_seq | Walks through every address in a memory, checking write-read integrity |
// Running built-in sequences in a test task body(); uvm_reg_hw_reset_seq rst_seq; uvm_reg_bit_bash_seq bash_seq; // Reset check — verify all resets match declared reset values rst_seq = uvm_reg_hw_reset_seq::type_id::create("rst_seq"); rst_seq.model = regmodel; rst_seq.start(null); // Bit-bash — walk a 1 through every writable bit bash_seq = uvm_reg_bit_bash_seq::type_id::create("bash_seq"); bash_seq.model = regmodel; bash_seq.start(null); endtask
A minimal but complete three-register block (control, status, interrupt) with adapter and testbench integration:
// --- status_reg: read-only, hardware writes bits --- class status_reg extends uvm_reg; `uvm_object_utils(status_reg) uvm_reg_field busy; uvm_reg_field err_code; function new(string name="status_reg"); super.new(name,32,UVM_NO_COVERAGE); endfunction function void build(); busy = uvm_reg_field::type_id::create("busy"); err_code = uvm_reg_field::type_id::create("err_code"); busy.configure (this, 1, 0, "RO", 1, 0, 1, 0, 1); err_code.configure(this, 4, 4, "RO", 1, 0, 1, 0, 1); endfunction endclass // --- irq_reg: W1C interrupt status --- class irq_reg extends uvm_reg; `uvm_object_utils(irq_reg) uvm_reg_field overflow; uvm_reg_field underflow; uvm_reg_field done; function new(string name="irq_reg"); super.new(name,32,UVM_NO_COVERAGE); endfunction function void build(); overflow = uvm_reg_field::type_id::create("overflow"); underflow = uvm_reg_field::type_id::create("underflow"); done = uvm_reg_field::type_id::create("done"); overflow.configure (this, 1, 0, "W1C", 1, 0, 1, 0, 1); underflow.configure(this, 1, 1, "W1C", 1, 0, 1, 0, 1); done.configure (this, 1, 2, "W1C", 1, 0, 1, 0, 1); endfunction endclass // --- Register block --- class my_reg_block extends uvm_reg_block; `uvm_object_utils(my_reg_block) ctrl_reg ctrl; status_reg status; irq_reg irq; uvm_reg_map apb_map; function new(string name="my_reg_block"); super.new(name,UVM_NO_COVERAGE); endfunction function void build(); ctrl = ctrl_reg::type_id::create("ctrl"); ctrl.build(); status = status_reg::type_id::create("status"); status.build(); irq = irq_reg::type_id::create("irq"); irq.build(); apb_map = create_map("apb_map", 32'h4000_0000, 4, UVM_LITTLE_ENDIAN); apb_map.add_reg(ctrl, 32'h00, "RW"); apb_map.add_reg(status, 32'h04, "RO"); apb_map.add_reg(irq, 32'h08, "RW"); lock_model(); endfunction endclass // --- Test using the register model --- task body(); uvm_status_e status; uvm_reg_data_t rdata; // Enable the DUT and set mode=2 regmodel.ctrl.write(status, 32'h5, UVM_FRONTDOOR); regmodel.status.read(status, rdata, UVM_FRONTDOOR); `uvm_info("TEST", $sformatf("Status = 0x%08h", rdata), UVM_MEDIUM) // Clear all interrupt flags regmodel.irq.write(status, 32'h7, UVM_FRONTDOOR); // W1C — write 1 to clear endtask