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 withreg.build()(which runs the fieldconfigure()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:
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:
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
endclassWith the model locked, the block is the single handle a test uses, and hierarchy reads naturally:
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 offsetEvery 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
A block whose build() never calls lock_model() leaves offsets unresolved, so the first frontdoor write has no address to go to
MISSING LOCKvirtual 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 resolvedFrontdoor 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.
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.
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_submapfor 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_blockper 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_blockis 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)andbuild()it,create_map,add_regat its offset, nest sub-blocks withadd_submap, thenlock_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.