Register Abstraction Layer (RAL)

A standard object-oriented model to mirror design registers. It simplifies register verification by abstracting physical bus transactions.

What is RAL?

The UVM Register Abstraction Layer (RAL) is a standardized high-level object-oriented model that mirrors the memory-mapped registers in your Design Under Test (DUT).

Think of RAL as a "Twin":
It is a software twin of your hardware registers. When you write to the RAL model, it automatically handles the bus transaction to update the real hardware.

It abstracts the physical bus layer, allowing you to read and write registers by name (e.g., reg_model.my_reg.write()) rather than hardcoded addresses.

Architecture: Front Door vs. Back Door

One of the most powerful features of RAL is the ability to choose how you access registers without changing your test sequence.

RAL Access Mechanism
UVM Sequence
model.regA.write(data)
RAL Model
(Register Twins)
🔵 Front Door (Bus)
Uses the physical bus protocol (APB/AHB). Consumes simulation time. Verifies the bus logic.
🟠 Back Door (DPI)
Uses Zero-time C paths (DPI/VPI). Instant access. Great for specific setup phases.

Why RAL? - The Abstraction Value

RAL solves the "Memory Map Chaos." In the past, verification engineers hardcoded addresses (e.g., write(0x1004, data)). But if the hardware specification changed the address to 0x2004, every single test case had to be rewritten.

The Object-Oriented Solution

  • Abstraction: Instead of addresses, you use names: reg_model.STATUS.write(data).
  • Maintainability: When the HW spec changes, you re-generate the RAL model (automatically). The test code remains untouched.
  • Reuse: The same sequence works whether the block is at address 0x0 (block level) or 0x8000 (SoC level).

Configuring Components

The heart of RAL is the `configure()` method. Understanding its arguments is crucial for creating accurate models.

1. Configuring a Field


class status_reg extends uvm_reg;
    rand uvm_reg_field busy;
    rand uvm_reg_field error;
    function new(string name = "status_reg");
        super.new(name, 32, UVM_NO_COVERAGE); // 32-bit register
    endfunction
    virtual function void build();
        busy = uvm_reg_field::type_id::create("busy");
        // configure(parent, size, lsb, access, volatile, reset, has_reset, is_rand, individually_accessible)
        busy.configure(this, 1, 0, "RO", 1, 0, 1, 0, 0);
        error = uvm_reg_field::type_id::create("error");
        error.configure(this, 1, 1, "W1C", 0, 0, 1, 1, 0);
    endfunction
endclass
                            

Key Parameters:

  • access: "RW", "RO", "WO", "W1C" (Write 1 to Clear), "W1S" (Write 1 to Set).
  • volatile: If 1, the model won't check the value after a read (useful for status bits that HW changes).

The Register Block

The `uvm_reg_block` connects everything. It instantiates registers, creates the address map, and locks the model.


class my_controller_blk extends uvm_reg_block;
    rand status_reg  status;
    rand control_reg control;
    uvm_reg_map      default_map;
    virtual function void build();
        // 1. Create Registers
        status = status_reg::type_id::create("status");
        status.configure(this);
        status.build();
        control = control_reg::type_id::create("control");
        control.configure(this);
        control.build();
        // 2. Create Map (Name, Base Addr, Bus Width, Endianness)
        default_map = create_map("default_map", 'h1000, 4, UVM_LITTLE_ENDIAN);
        // 3. Add Registers to Map (Reg, Offset, Rights)
        default_map.add_reg(status,  'h00, "RO");
        default_map.add_reg(control, 'h04, "RW");
        // 4. Lock Model
        lock_model();
    endfunction
endclass
                            

Endianness & Addressing

The uvm_reg_map handles the translation from register offsets to physical bus addresses.

  • Base Address: The starting address of the block in the system memory map.
  • Byte Addressing: UVM assumes byte addressing. If your bus is 32-bit (4 bytes), offsets should typically increment by 4 (0x0, 0x4, 0x8).
  • Endianness:
    • UVM_LITTLE_ENDIAN: LSB is at lowest address (standard for ARM/x86).
    • UVM_BIG_ENDIAN: MSB is at lowest address.

Mirrored vs Desired Values (Interview Essential)

Frequently Asked: "What's the difference between mirrored and desired value in RAL?"

RAL maintains three distinct values for every register:

Value Type Description Updated When
Desired What the test wants to write set() method
Mirrored What RAL thinks is in HW After write()/read()
Actual (HW) Real value in DUT registers Bus transaction completes
Example: get() vs get_mirrored_value()

// set() changes DESIRED value only (no bus transaction)
reg_model.ctrl_reg.set(32'hDEAD_BEEF);
// get() returns DESIRED value
$display("Desired: %h", reg_model.ctrl_reg.get()); // 0xDEAD_BEEF
// get_mirrored_value() returns what RAL thinks is in HW
$display("Mirrored: %h", reg_model.ctrl_reg.get_mirrored_value()); // Could be 0x0000
// update() writes only if desired != mirrored
reg_model.ctrl_reg.update(status); // Triggers bus write
// After update, mirrored = desired = actual
                            

Interview Tip

Why this matters: The update() method is efficient because it only sends a bus transaction when desired ≠ mirrored. This avoids redundant writes and speeds up simulation.

Memory Model (uvm_mem)

Besides registers, RAL can also model memories using uvm_mem. This is useful for modeling SRAMs, FIFOs, or any large memory blocks in your DUT.

Step 1: Define the Memory Class

class my_memory extends uvm_mem;
    function new(string name = "my_memory");
        // Arguments: name, size_in_words, data_width, access, has_coverage
        // This creates a 1024-word memory, each word is 32 bits
        super.new(name, 1024, 32, "RW", UVM_NO_COVERAGE);
    endfunction
endclass
                        
Step 2: Add Memory to Register Block

class my_reg_block extends uvm_reg_block;
    my_memory mem;
    virtual function void build();
        // Create and configure memory
        mem = my_memory::type_id::create("mem");
        mem.configure(this);
        // Add to address map at offset 0x2000
        default_map.add_mem(mem, 'h2000);
        lock_model();
    endfunction
endclass
                        
Step 3: Accessing the Memory

task body();
    uvm_status_e status;
    uvm_reg_data_t rd_data;
    // === Single Word Access ===
    // Write 0xDEADBEEF to offset 0 (address = 0x2000 + 0)
    reg_model.mem.write(status, .offset(0), .value(32'hDEAD_BEEF));
    // Read from offset 0
    reg_model.mem.read(status, .offset(0), .value(rd_data));
    // rd_data = 0xDEADBEEF
    // === Burst Access (Multiple Words) ===
    // Prepare data array to write
    uvm_reg_data_t wr_burst[4];
    wr_burst[0] = 32'h1111_1111;
    wr_burst[1] = 32'h2222_2222;
    wr_burst[2] = 32'h3333_3333;
    wr_burst[3] = 32'h4444_4444;
    // Burst write 4 words starting at offset 10
    reg_model.mem.burst_write(status, .offset(10), .value(wr_burst));
    // Prepare array to receive read data
    uvm_reg_data_t rd_burst[4];
    // Burst read 4 words starting at offset 10
    reg_model.mem.burst_read(status, .offset(10), .value(rd_burst));
    // rd_burst[0] = 0x11111111, rd_burst[1] = 0x22222222, ...
endtask
                        

What the code does:

  • write(status, offset, value) - Writes a single word to the memory at the specified offset
  • read(status, offset, value) - Reads a single word from the memory
  • burst_write(status, offset, data_array) - Writes multiple consecutive words starting at offset
  • burst_read(status, offset, data_array) - Reads multiple consecutive words into the array

Key Difference: Registers vs Memories

  • Registers: Have defined fields, can be randomized, accessed by name
  • Memories: Large arrays, accessed by offset, no field structure, support burst operations