UVM-24: RAL Introduction — VLSI Trainers
VLSI Trainers UVM Series · UVM-24
UVM Series · UVM-24

RAL Introduction

The UVM Register Abstraction Layer — why RAL exists, the five-layer model hierarchy, uvm_reg_field, uvm_reg, uvm_reg_block, uvm_reg_map, building a complete GPIO register model from scratch, and how the model fits into the testbench.

📋 Why RAL Exists

Without RAL, a sequence that configures a DUT must know the exact bus protocol it is running on. A sequence written for an APB testbench cannot be run on an AHB testbench without rewriting it — even if both buses access the same registers. This creates duplication and prevents vertical reuse.

The UVM Register Abstraction Layer (RAL) solves this by providing an abstract, protocol-independent interface to DUT registers. Sequences call methods like reg.write() or reg.read() — the RAL framework translates these into actual bus transactions via an adapter specific to the target bus. Swapping the adapter is enough to redirect the same sequences from APB to AHB to AXI.

RAL also maintains a mirror of the expected register state — a software copy of what the DUT’s registers should currently contain. This mirror enables scoreboard-free register checking: after any read, the RAL can compare the actual value against the predicted value automatically.

RAL — Protocol-Independent Register Access Sequence reg.write(status, data) reg.read(status, data) RAL Register Model gpio_reg_block mirror, coverage, predictor hooks APB Adapter AHB Adapter block level integration APB driver → DUT AHB driver → DUT Mirror: software copy of expected register state Same sequence — swap adapter for different bus → no sequence rewrite needed vlsitrainers.com
Figure 1 — RAL provides protocol-independent access. The sequence calls abstract register methods. The RAL model translates these into generic bus operations. The adapter (one per bus type) converts generic operations into bus-specific sequence items. Swapping the adapter redirects the entire register test suite from APB to AHB without changing any sequence code.

📋 The Five-Layer Hierarchy

A RAL register model is built from five types of objects, arranged in a hierarchy from the smallest (field) to the largest (block):

RAL Five-Layer Hierarchy uvm_reg_block (gpio_reg_block) Container for all registers + address map. One per hardware block. uvm_reg_map (APB_map) Named address map — locates registers at offsets, targets a sequencer uvm_reg (data_reg) uvm_reg (dir_reg) uvm_reg (ien_reg) uvm_mem (optional) … more regs … field[31:0] field[7:4] field[3:0] ← uvm_reg_field objects (each is one bit-group within the register) vlsitrainers.com
Figure 2 — RAL five-layer hierarchy. A uvm_reg_block contains all registers and memories and owns the address map. Each uvm_reg_map locates registers at specific offsets and associates with a target sequencer. uvm_reg objects model individual registers; each contains uvm_reg_field objects for its bit-fields. uvm_mem models memory regions.
ClassExtendsRepresentsKey methods
uvm_reg_fielduvm_objectOne named bit-group within a registerconfigure(), set(), get(), is_known_access()
uvm_reguvm_objectOne register — contains fieldsbuild(), configure(), write(), read(), get(), set(), update(), mirror()
uvm_memuvm_objectA memory region (address range)write(), read(), burst_write(), burst_read()
uvm_reg_mapuvm_objectNamed address map — locates registerscreate_map(), add_reg(), add_submap(), set_sequencer()
uvm_reg_blockuvm_objectBlock-level container for all the abovebuild(), configure(), lock_model(), get_registers(), get_block()

📋 uvm_reg_field

A field represents one semantically-meaningful group of bits within a register. Every field must be declared as a handle in the register class and constructed + configured in the register’s build() method.

// ── uvm_reg_field::configure() signature ─────────────────
// void configure(
//   uvm_reg        parent,        // the containing register
//   int unsigned   size,          // bit width of this field
//   int unsigned   lsb_pos,       // LSB position within register
//   string         access,        // "RW", "RO", "WO", "W1C" etc.
//   bit            volatile,      // 1 if hardware can modify this field
//   uvm_reg_data_t reset,         // reset value
//   bit            has_reset,     // 1 if field has a reset value
//   bit            is_rand,       // 1 if field can be randomised
//   bit            individually_accessible); // 1 if within one byte lane

// Example: 1-bit read/write field at bit 3, reset to 0
enable_field.configure(this, 1, 3, "RW", 0, 1'b0, 1, 1, 1);

// Example: 8-bit status field at bits [15:8], RO, volatile (HW updates it)
status_field.configure(this, 8, 8, "RO", 1, 8'h00, 1, 0, 1);
Access typeBehaviour on writeBehaviour on read
RWWrite value storedReturns stored value
ROWrite has no effect (ignored)Returns current hardware value
WOWrite stored; read returns 0Always returns 0
W1CWriting 1 clears bit; writing 0 has no effectReturns current value (set by hardware)
RCReturns value then auto-clears
RSReturns value then auto-sets

📋 uvm_reg

Each register is modelled by extending uvm_reg. The class declares handles for each field, constructs them in build(), and configures them. Note that build() here is not the UVM phase build_phase — it is a regular virtual function called explicitly when the block is built.

class gpio_data_reg extends uvm_reg;
  `uvm_object_utils(gpio_data_reg)

  // ── Field declarations ─────────────────────────────────
  rand uvm_reg_field gpio_out;    // [31:0] GPIO output data, RW

  function new(string name = "gpio_data_reg");
    // super.new(name, n_bits, has_coverage)
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    gpio_out = uvm_reg_field::type_id::create("gpio_out");
    // configure(parent, size, lsb, access, volatile, reset, has_reset, is_rand, byte_accessible)
    gpio_out.configure(this, 32, 0, "RW", 0, 32'h0, 1, 1, 1);
  endfunction
endclass

// ── A more complex register: GPIO direction ───────────────
class gpio_dir_reg extends uvm_reg;
  `uvm_object_utils(gpio_dir_reg)

  rand uvm_reg_field gpio_dir;    // [31:0] 0=input 1=output

  function new(string name = "gpio_dir_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    gpio_dir = uvm_reg_field::type_id::create("gpio_dir");
    gpio_dir.configure(this, 32, 0, "RW", 0, 32'h0, 1, 1, 1);
  endfunction
endclass

// ── GPIO interrupt status register (W1C bits) ─────────────
class gpio_isr_reg extends uvm_reg;
  `uvm_object_utils(gpio_isr_reg)

  uvm_reg_field int_status;       // [31:0] write-1-to-clear

  function new(string name = "gpio_isr_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    int_status = uvm_reg_field::type_id::create("int_status");
    // W1C: volatile=1 (HW sets), is_rand=0 (don't randomise status)
    int_status.configure(this, 32, 0, "W1C", 1, 32'h0, 1, 0, 1);
  endfunction
endclass

📋 uvm_reg_block and uvm_reg_map

The register block is the top-level container. Its build() method creates all registers, calls each register’s own build() to create fields, configures each register, creates an address map, and adds each register to the map at its byte offset. The final call to lock_model() finalises the address mapping and prevents further modifications.

class gpio_reg_block extends uvm_reg_block;
  `uvm_object_utils(gpio_reg_block)

  // ── Register handles ───────────────────────────────────
  rand gpio_data_reg data_reg;   // offset 0x00
  rand gpio_dir_reg  dir_reg;    // offset 0x04
  rand gpio_ien_reg  ien_reg;    // offset 0x08
  rand gpio_isr_reg  isr_reg;    // offset 0x0C

  // ── Address map ────────────────────────────────────────
  uvm_reg_map APB_map;

  function new(string name = "gpio_reg_block");
    super.new(name, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    // ① Create and configure each register
    data_reg = gpio_data_reg::type_id::create("data_reg");
    data_reg.configure(this, null, "");  // parent=this, no hdl path yet
    data_reg.build();

    dir_reg = gpio_dir_reg::type_id::create("dir_reg");
    dir_reg.configure(this, null, "");
    dir_reg.build();

    ien_reg = gpio_ien_reg::type_id::create("ien_reg");
    ien_reg.configure(this, null, "");
    ien_reg.build();

    isr_reg = gpio_isr_reg::type_id::create("isr_reg");
    isr_reg.configure(this, null, "");
    isr_reg.build();

    // ② Create address map: name, base_addr, n_bytes, endian
    APB_map = create_map("APB_map", 32'h0, 4, UVM_LITTLE_ENDIAN);

    // ③ Add registers to map at their byte offsets
    APB_map.add_reg(data_reg, 32'h00, "RW");
    APB_map.add_reg(dir_reg,  32'h04, "RW");
    APB_map.add_reg(ien_reg,  32'h08, "RW");
    APB_map.add_reg(isr_reg,  32'h0C, "RW");

    // ④ Finalise — prevents further changes
    lock_model();
  endfunction
endclass
lock_model() must be the last call in build(). It resolves all address offsets, finalises the model, and marks it as no longer modifiable. Calling any register method before lock_model() completes will produce unpredictable results. After lock_model(), the block is ready to be passed to the testbench via config_db.

📋 Complete GPIO Register Model — gpio_ien_reg

For completeness, the GPIO interrupt enable register with a multi-field structure:

class gpio_ien_reg extends uvm_reg;
  `uvm_object_utils(gpio_ien_reg)

  rand uvm_reg_field ien;    // [31:0] interrupt enable bits per GPIO

  function new(string name = "gpio_ien_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    ien = uvm_reg_field::type_id::create("ien");
    // RW, non-volatile, reset to 0 (all interrupts disabled)
    ien.configure(this, 32, 0, "RW", 0, 32'h0, 1, 1, 1);
  endfunction
endclass

📋 Instantiating the Model

The register block is instantiated as a uvm_object (not a component — it has no phases). It is created with new() and its build() is called explicitly. The block handle is then passed to the testbench via the env config object.

// ── In test build_phase ───────────────────────────────────
function void build_phase(uvm_phase phase);
  super.build_phase(phase);

  // ① Create the register block
  m_gpio_rm = gpio_reg_block::type_id::create("m_gpio_rm");
  m_gpio_rm.build();   // explicit call — not called by UVM phases

  // ② Assign the handle into the env config
  m_env_cfg.gpio_rm = m_gpio_rm;

  // ── Alternative: pass via config_db ───────────────────
  uvm_config_db #(gpio_reg_block)::set(
    this, "m_env", "gpio_rm", m_gpio_rm);

  m_env = gpio_env::type_id::create("m_env", this);
endfunction

// ── Register block is a uvm_object — use new(), not type_id::create()
// Both work, but new() is conventional for the register block itself.
// Always call build() explicitly after construction.

📋 How the Model Fits in the Testbench

The register model connects to the testbench in the env’s connect_phase by calling set_sequencer() on the address map. This links the map to the actual sequencer, enabling register accesses to send bus transactions:

// ── In gpio_env connect_phase ─────────────────────────────
function void connect_phase(uvm_phase phase);
  // Link the register map to the APB sequencer
  // When reg.write() is called, a transaction goes to this sequencer
  m_cfg.gpio_rm.APB_map.set_sequencer(
    m_agent.m_seqr,
    m_reg2apb_adapter    // adapter converts reg ops to APB items
  );

  // Enable predictor — keeps mirror in sync with actual bus
  // (predictor is covered in UVM-25)
  m_predictor.map = m_cfg.gpio_rm.APB_map;
  m_agent.ap.connect(m_predictor.bus_in);
endfunction

// ── Sequences access registers via the block handle ───────
task body();
  gpio_reg_block rm;
  uvm_status_e   status;
  uvm_reg_data_t data;

  // Get register model from config (via m_cfg or config_db)
  if (!uvm_config_db #(gpio_reg_block)::get(
        m_sequencer, "", "gpio_rm", rm))
    `uvm_fatal("SEQ", "No register model!")

  // Write the direction register: set all 32 GPIO pins as outputs
  rm.dir_reg.write(status, 32'hFFFF_FFFF, .parent(this));

  // Write the data register: toggle all outputs
  rm.data_reg.write(status, 32'hAAAA_AAAA, .parent(this));

  // Read back and verify via the mirror
  rm.data_reg.read(status, data, .parent(this));
  if (data != 32'hAAAA_AAAA)
    `uvm_error("SEQ", "Data register mismatch!")
endtask

📋 Register Access Types — Front Door vs Back Door

The RAL provides two paths to access DUT registers:

Access typeMechanismTimingWhen to use
Front door
UVM_FRONTDOOR
Goes through the bus agent — sequences items are driven on the actual bus interfaceConsumes bus cycles — realistic timingNormal stimulus, register reads/writes via the bus protocol. Default.
Back door
UVM_BACKDOOR
Uses simulator database access (force/deposit) to directly read/write register RTL bitsZero simulation timeInitialising registers before test, fast reset to known state, checking register state without bus activity
// ── Front door (default) — sends APB transaction ─────────
rm.data_reg.write(status, 32'hDEAD_BEEF,
  .path(UVM_FRONTDOOR), .parent(this));

// ── Back door — zero time, bypasses bus ──────────────────
rm.data_reg.write(status, 32'hDEAD_BEEF,
  .path(UVM_BACKDOOR));  // no parent needed — no sequence involved

// ── Peek/poke: back door read/write shorthand ─────────────
rm.data_reg.poke(status, 32'hCAFE);        // backdoor write
rm.data_reg.peek(status, data);             // backdoor read

// ── Back door requires HDL path set on the register ───────
// In register configure() call — provide the hdl path to the RTL
data_reg.configure(this, null,
  "pss_dut.u_gpio.u_data_reg.q");  // simulator-relative path

📋 Quick Reference

ItemKey fact
RAL purposeProtocol-independent register access. Swap the adapter to change bus — sequences unchanged.
uvm_reg_fieldOne named bit-group. configure(parent, size, lsb, access, volatile, reset, has_reset, is_rand, byte_acc)
uvm_reg base classuvm_object. Constructor: new(name, n_bits, has_coverage). Override build() to create fields.
uvm_reg build()NOT the UVM phase. Must be called explicitly. Creates and configures all field objects.
uvm_reg_blockContainer for all registers + address map. Override build() to create regs and map.
lock_model()MUST be last call in block build(). Finalises address resolution. Block unusable without it.
uvm_reg_map creationcreate_map("name", base_addr, n_bytes, endian)
Adding register to mapmap.add_reg(reg_handle, byte_offset, "RW")
InstantiationUse new() (or type_id::create()). Always call .build() explicitly. Not a component — no phases.
Linking to sequencermap.set_sequencer(m_seqr, adapter) in env connect_phase
Front door writereg.write(status, data, .parent(this)) — sends bus transaction
Front door readreg.read(status, data, .parent(this)) — performs bus read
Back door writereg.poke(status, data) or reg.write(…, .path(UVM_BACKDOOR)) — zero time
Back door readreg.peek(status, data) — zero time
Field access typesRW, RO, WO, W1C, RC, RS — set in field configure()
Scroll to Top