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.
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.
A RAL register model is built from five types of objects, arranged in a hierarchy from the smallest (field) to the largest (block):
| Class | Extends | Represents | Key methods |
|---|---|---|---|
uvm_reg_field | uvm_object | One named bit-group within a register | configure(), set(), get(), is_known_access() |
uvm_reg | uvm_object | One register — contains fields | build(), configure(), write(), read(), get(), set(), update(), mirror() |
uvm_mem | uvm_object | A memory region (address range) | write(), read(), burst_write(), burst_read() |
uvm_reg_map | uvm_object | Named address map — locates registers | create_map(), add_reg(), add_submap(), set_sequencer() |
uvm_reg_block | uvm_object | Block-level container for all the above | build(), configure(), lock_model(), get_registers(), get_block() |
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 type | Behaviour on write | Behaviour on read |
|---|---|---|
RW | Write value stored | Returns stored value |
RO | Write has no effect (ignored) | Returns current hardware value |
WO | Write stored; read returns 0 | Always returns 0 |
W1C | Writing 1 clears bit; writing 0 has no effect | Returns current value (set by hardware) |
RC | — | Returns value then auto-clears |
RS | — | Returns value then auto-sets |
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
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
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
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.
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
The RAL provides two paths to access DUT registers:
| Access type | Mechanism | Timing | When to use |
|---|---|---|---|
Front doorUVM_FRONTDOOR | Goes through the bus agent — sequences items are driven on the actual bus interface | Consumes bus cycles — realistic timing | Normal stimulus, register reads/writes via the bus protocol. Default. |
Back doorUVM_BACKDOOR | Uses simulator database access (force/deposit) to directly read/write register RTL bits | Zero simulation time | Initialising 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
| Item | Key fact |
|---|---|
| RAL purpose | Protocol-independent register access. Swap the adapter to change bus — sequences unchanged. |
| uvm_reg_field | One named bit-group. configure(parent, size, lsb, access, volatile, reset, has_reset, is_rand, byte_acc) |
| uvm_reg base class | uvm_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_block | Container 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 creation | create_map("name", base_addr, n_bytes, endian) |
| Adding register to map | map.add_reg(reg_handle, byte_offset, "RW") |
| Instantiation | Use new() (or type_id::create()). Always call .build() explicitly. Not a component — no phases. |
| Linking to sequencer | map.set_sequencer(m_seqr, adapter) in env connect_phase |
| Front door write | reg.write(status, data, .parent(this)) — sends bus transaction |
| Front door read | reg.read(status, data, .parent(this)) — performs bus read |
| Back door write | reg.poke(status, data) or reg.write(…, .path(UVM_BACKDOOR)) — zero time |
| Back door read | reg.peek(status, data) — zero time |
| Field access types | RW, RO, WO, W1C, RC, RS — set in field configure() |