Skip to content

UVM RAL · Chapter 2 · Register Model Basics

uvm_reg_block — The Register Container

The register block is the model's top-level container. It owns the registers, memories, sub-blocks, and address maps for a block of the design, and it is the single handle a test uses to reach any register by name. Understanding the block means understanding how a model is assembled. In its build method it creates each register, configures each register's parent and builds its fields, creates one or more address maps, and adds each register to a map at its spec offset. It then locks the model to freeze the structure and resolve every offset into a real address. Two ideas make the block more than a bag of registers: hierarchy, where a block can nest sub-blocks into a tree, and locking, the moment offsets become addresses. This lesson assembles a block end to end and breaks the common bring-up bug of forgetting to lock it, so the first write has nowhere to go.

Foundation12 min readUVM RALuvm_reg_blockbuildlock_modelHierarchyRegister Model

Chapter 2 · Section 2.1 · Register Model Basics

1. Why Should I Learn This?

The block is where a register model is born and where it is wired to the rest of the environment, so almost every model bring-up problem is a block problem: a register that does not respond because it was never added to a map, a write that goes nowhere because the model was never locked, a sub-block that does not resolve because it was never configured with its parent. If you understand how build() assembles a block and what lock_model() finalizes, these stop being mysteries and become a short checklist you can run down in seconds.

Learning the block also gives you the mental map for everything that follows. The next sections build the uvm_reg and uvm_reg_field that live inside the block; adapters, predictors, and sequences all attach to the block's map. The block is the frame the rest of the model hangs on, so getting its assembly and locking right is the foundation the whole track stands on.

2. Industry Story — the register that was never in the room

An engineer extends a working block with a new INT_MASK register. They write the register class carefully — fields, access, reset all correct — create it in the block's build(), and move on. But they forget the two lines that make a register part of the model: they never call configure(this) and build() on it to attach it to the block, and never add_reg it to the map. The register class is perfect and completely invisible: reg_model.int_mask is a null handle, any access through it errors, and the map has no address for it.

The bug wastes a morning because the register looks present — the class exists, the file compiles, the field definitions are right there — so the engineer debugs the register's internals instead of its membership. The tech lead spots it in a minute with the block-assembly checklist: a register is only in the model if it was created, configured with the block as parent, built, and added to a map — four steps, and this one had two. The lesson: writing a register class does not put it in the model; the block's build() must create it, configure its parent, build its fields, and add it to a map — a register that skips any of those is not in the room.

3. Concept — assembling a block in build()

A uvm_reg_block is assembled in its build() method, in a fixed order, and finalized by locking:

  • Create each register with the factory (type_id::create), so it is a real object the block owns.
  • Configure each register's parent with reg.configure(this), wiring it into the block, then build its fields with reg.build() (which runs the field configure() calls from Chapter 1).
  • Create the address map(s) with create_map(name, base_addr, n_bytes, endian). The map is where addressing lives (Chapter 3).
  • Add each register to a map with map.add_reg(reg, offset, rights), placing it at its spec offset.
  • Lock the model with lock_model(). This freezes the structure and resolves every offset into an address; before locking, offsets are numbers and addresses do not exist. Locking is also what lets RAL build the internal maps a frontdoor access needs.

Hierarchy adds one idea: a block can contain sub-blocks. A parent block creates a child block, calls the child's configure(this) and build(), and adds the child's map into the parent's map (add_submap) at a base offset — so a large design's model is a tree of blocks, each reusable on its own. Here is a small block's structure, from the block down to fields:

Register block containment: block owns a map, registers, and a sub-block; registers own fieldsdefault_mapdefault_mapuvm_reg_mapuvm_reg_mapenableenablefield [0]field [0]modemodefield [2:1]field [2:1]ctrlctrluvm_reg @0x00uvm_reg @0x00busybusyfield [0]field [0]statusstatusuvm_reg @0x04uvm_reg @0x04timertimeruvm_reg_block (sub)uvm_reg_block (sub)periph_blkperiph_blkuvm_reg_blockuvm_reg_block
Figure 1 — a register block's containment tree. The uvm_reg_block (root) owns an address map (default_map) and its registers (ctrl, status, and a nested timer sub-block). Each uvm_reg owns its uvm_reg_field children. A test holds the block and reaches any register by name (reg_model.ctrl); the map gives each register an address; locking the model resolves the offsets into those addresses. Sub-blocks nest the same way, so a whole SoC model is a tree of these.

4. Mental Model — the block is the model's namespace and address space

5. Working Example — a block, a sub-block, and the lock

A block builds its registers, creates a map, adds them at their offsets, nests a sub-block, and locks — the full assembly in one place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class periph_blk extends uvm_reg_block;
  `uvm_object_utils(periph_blk)
  rand ctrl_reg    ctrl;      // 0x00
  rand status_reg  status;    // 0x04
  rand timer_blk   timer;     // sub-block based at 0x100
 
  function new(string name = "periph_blk");
    super.new(name, UVM_NO_COVERAGE);
  endfunction
 
  virtual function void build();
    // 1. Create + configure(parent) + build each register.
    ctrl = ctrl_reg::type_id::create("ctrl");
    ctrl.configure(this); ctrl.build();
    status = status_reg::type_id::create("status");
    status.configure(this); status.build();
 
    // 2. Create the address map.
    default_map = create_map("default_map", .base_addr('h0),
                             .n_bytes(4), .endian(UVM_LITTLE_ENDIAN));
 
    // 3. Add each register to the map at its spec offset.
    default_map.add_reg(ctrl,   .offset('h00), .rights("RW"));
    default_map.add_reg(status, .offset('h04), .rights("RO"));
 
    // 4. Create + configure + build a SUB-BLOCK and merge its map at a base.
    timer = timer_blk::type_id::create("timer");
    timer.configure(this);      // parent = this block
    timer.build();
    default_map.add_submap(timer.default_map, .offset('h100));
 
    // 5. Lock: freeze structure and resolve every offset into an address.
    lock_model();
  endfunction
endclass

With the model locked, the block is the single handle a test uses, and hierarchy reads naturally:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
uvm_status_e s;  logic [31:0] got;
reg_model.ctrl.write(s, 32'h3);         // top-level register
reg_model.status.read(s, got);          // top-level register
reg_model.timer.load.write(s, 32'hFF);  // register inside the sub-block, addressed at 0x100 + its offset

Every register reached by name resolves to an address because the model is locked; the sub-block's registers sit at 0x100 plus their own offsets because its map was merged there.

6. Debugging Session — the model that was never locked

1

A block whose build() never calls lock_model() leaves offsets unresolved, so the first frontdoor write has no address to go to

MISSING LOCK
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
virtual function void build();
  ctrl = ctrl_reg::type_id::create("ctrl");
  ctrl.configure(this); ctrl.build();
  default_map = create_map("default_map", .base_addr('h0),
                           .n_bytes(4), .endian(UVM_LITTLE_ENDIAN));
  default_map.add_reg(ctrl, .offset('h00), .rights("RW"));
  // BUG: no lock_model() — the structure is never frozen, offsets never resolve.
endfunction
 
// ... later ...
reg_model.ctrl.write(s, 32'h3);   // errors / UVM_NOT_OK: address not resolved
Symptom

Frontdoor accesses fail before any bus activity — a write/read errors with a map/address-resolution message or returns UVM_NOT_OK, and the waveform shows the sequencer idle: the DUT never sees a transaction. Registers that were carefully created and added to the map still cannot be reached, which makes it look like the map or the adapter is broken. It is neither — the model was assembled correctly but never finalized, so the offsets are still just numbers.

Root Cause

lock_model() was never called, so the block's structure was never frozen and its offsets were never resolved into addresses. Until a model is locked, a register's offset is a relative number with no absolute address behind it, and RAL cannot build the internal maps a frontdoor access needs — so the access has no address to drive to. Everything upstream (register classes, configure, build, add_reg) was correct; the model was simply left in draft. Locking is the publishing step that turns the draft into an addressable model, and skipping it leaves the whole map inert.

Fix

Call lock_model() at the end of the top-level block's build(), after all registers and sub-maps are added (as in Section 5). Locking freezes the structure, resolves every offset into an address, and lets RAL construct the maps a frontdoor access uses — after which reg_model.ctrl.write(...) drives a real transaction to the resolved address. The bring-up checklist the bug teaches, run top to bottom on any non-responding model: is the register created, configured with the block as parent, built, added to a map, and is the model locked? Five steps; a non-responding register or a dead map is almost always a missing one of them, and a completely dead map is almost always the missing lock.

7. Common Mistakes

  • Forgetting lock_model(). The whole map stays a draft; every frontdoor access fails with no bus activity.
  • Creating a register but not configuring/building/adding it. A register class that is not created, configured with the block as parent, built, and added to a map is not in the model — the null-handle bug.
  • Not configuring a sub-block with its parent. child.configure(this) wires the hierarchy; skip it and the sub-block does not resolve.
  • Forgetting add_submap for a sub-block's map. The sub-block exists but its registers have no place in the parent's address space.
  • Locking too early. Lock the top block once, after all registers and sub-maps are added; locking before the structure is complete freezes an incomplete model.

8. Industry Best Practices

  • Follow a fixed build order. Create, configure(parent), build, create map, add_reg, (nest sub-blocks), lock — the same order every time, so a missing step is obvious.
  • Lock only the top-level block, once. Locking the root finalizes the whole tree; do not sprinkle locks through sub-blocks.
  • Model the hierarchy to match the design. One uvm_reg_block per reusable design block, nested to mirror the RTL, so a block model is reusable wherever the block instantiates.
  • Run the block-assembly checklist on any non-responding register. Created, configured, built, added, locked — five checks that resolve most bring-up failures fast.
  • Prefer generated blocks for large designs. IP-XACT/RALF generation builds the block, maps, and hierarchy consistently (Chapter 13), removing hand-assembly slips.

9. Interview / Review Questions

10. Key Takeaways

  • The uvm_reg_block is the register model's container and single handle — it owns the registers, memories, sub-blocks, and maps, and a test reaches any register by name through it.
  • A block is assembled in build(): create each register, configure(this) and build() it, create_map, add_reg at its offset, nest sub-blocks with add_submap, then lock_model().
  • A register is 'in the model' only after four steps — created, configured with the block as parent, built, and added to a map; skip any and it is invisible or unaddressable.
  • Nothing has an address until you lock; lock_model() resolves offsets into addresses and lets RAL route frontdoor accesses — a dead map is almost always a missing lock.
  • Model the hierarchy to match the design — one block per reusable unit, nested to mirror the RTL — for reusable IP-level models and single-point relocation.