Day 11 / 25
← Day 10 Day 12 →
HomeVerificationDay 11 — UVM RAL
Track 2 — UVM Core

UVM RAL — Register Abstraction Layer

By EcrioniX · Updated June 2026

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.

⏱ 30 min read📖 Day 11 of 25🎯 RAL · uvm_reg · Frontdoor · Backdoor
Contents
  1. Why RAL
  2. uvm_reg — Field Declaration & Access Policies
  3. uvm_reg_block — Grouping Registers
  4. uvm_reg_adapter — Protocol Translation
  5. Frontdoor Access
  6. Backdoor Access
  7. Mirror vs Desired Value
  8. Built-in Register Sequences
  9. Complete Register Block Example
  10. FAQ

1. Why RAL

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.

2. uvm_reg — Field Declaration & Access Policies

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:

PolicyReadWriteTypical use
RWReturns current valueSets fieldConfiguration registers
ROReturns current valueNo effectStatus/ID registers
WOReturns 0Sets fieldCommand registers
W1CReturns current valueWriting 1 clears bitInterrupt status registers
W1SReturns current valueWriting 1 sets bitSet-only control bits
RCReturns value then clearsNo effectClear-on-read status

3. uvm_reg_block — Grouping Registers

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

4. uvm_reg_adapter — Protocol Translation

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

5. Frontdoor Access

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

6. Backdoor Access

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
FeatureFrontdoorBackdoor
Simulation timeYes — real bus cyclesZero
Tests bus interfaceYesNo
Requires adapterYesNo
Requires HDL pathNoYes
Use forProtocol testing, functional testsFast init, reset check, debug

7. Mirror vs Desired Value

Every UVM register field has two software-side copies maintained by the RAL:

// 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
When mirror diverges from desired: If the RTL autonomously changes a register (e.g., a status bit sets itself after an event), the mirror becomes stale. Call 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.

8. Built-in Register Sequences

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:

SequenceWhat it tests
uvm_reg_hw_reset_seqReads every register after reset and compares to the declared reset value
uvm_reg_bit_bash_seqWalks a 1-bit through every writable field (checks field width and access)
uvm_reg_access_seqWrites a pattern to every register and reads it back to verify bus connectivity
uvm_mem_walk_seqWalks 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

9. Complete Register Block Example

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

Key Takeaways — Day 11

Frequently Asked Questions

What is the UVM Register Abstraction Layer?
The UVM Register Abstraction Layer (RAL) is a standardised model for describing and accessing DUT registers in a testbench. Instead of writing raw bus transactions to specific addresses, you interact with named register objects and the RAL automatically translates these into the correct bus protocol transactions via a register adapter. This makes testbenches protocol-independent and reusable.
What is the difference between frontdoor and backdoor register access?
Frontdoor access uses the actual bus protocol (APB, AXI, etc.) to write or read a register — it goes through the driver, interface, and DUT bus port, consuming real simulation time. Backdoor access uses DPI or VPI to directly read/write the RTL register variable at zero simulation time. Frontdoor tests the actual bus interface. Backdoor is used for fast register initialisation and golden-value checking without spending simulation cycles.
What are mirror and desired values in UVM RAL?
Every UVM register field has two software-side copies: the desired value (what you want the register to contain — set by write()) and the mirror value (what the RAL believes the hardware currently contains — updated by read() or explicit mirror() calls). They can diverge if the hardware changes a register field autonomously. Call reg.mirror() to issue a read and synchronise the mirror, or reg.predict() to update it without a bus access.
What does a uvm_reg_adapter do?
The uvm_reg_adapter translates the generic uvm_reg_bus_op struct that the RAL produces into a protocol-specific sequence item your existing driver understands, and vice versa. You write one adapter per bus protocol. This keeps the register model protocol-agnostic — the same register block can be reused with an APB agent or an AXI agent by simply swapping adapters.
Next → Day 12
UVM Factory
type_id::create(), factory overrides (type and instance), debug with factory.print(), and override-based reuse patterns.