Skip to content

UVM RAL · Chapter 2 · Register Model Basics

Configuring, Building & Locking the Model

A register model is not a UVM component. It is a data object, so it does not get phased automatically the way an agent or scoreboard does. Instead you drive its construction lifecycle by hand, and getting that lifecycle right is the difference between a model that is ready when the first sequence runs and one that is not. The steps are short and always the same. The environment creates the model, calls build to assemble every register, field, and map, calls lock to freeze the structure and resolve offsets into addresses, resets to prime the mirror, and configures backdoor HDL paths, all inside the build phase. It then wires the model to the bus in the connect phase. This lesson lays out that lifecycle as a checklist, explains where each step belongs, and breaks an environment that builds the model too late so sequences race an unassembled model.

Foundation12 min readUVM RALbuild_phaselock_modelLifecycleresetEnvironment

Chapter 2 · Section 2.5 · Register Model Basics

1. Why Should I Learn This?

Most 'RAL does not work at all' problems are lifecycle problems: the model was not built, not locked, not reset, or built in the wrong phase, so it simply was not ready when the first access happened. These failures look like deep RAL bugs and are almost always a missing or mis-placed step in a five-item checklist. Knowing that checklist — and knowing that a register model is data you assemble by hand rather than a component UVM phases for you — turns 'the whole model is broken' into 'which step did I miss.'

Learning the lifecycle also fixes where the model lives in your environment: created and assembled in the env's build_phase, connected in connect_phase, ready before any run_phase sequence touches it. Get that placement right once and every register test in the environment has a model that is present, addressable, and primed when it runs.

2. Industry Story — the model that was not there yet

An engineer stands up a new register environment. They correctly write the block, registers, and fields, and they create the model — but they put the assembly in the test's run_phase, right before starting the register sequence: reg_model = periph_blk::type_id::create(...); reg_model.build(); reg_model.lock_model(); and then seq.start(...). It works on their machine, most of the time.

In regression it fails intermittently: some seeds hit a null-handle error deep in the sequence, others pass. The cause is a phasing race. The register model is data the environment is supposed to own and assemble in build_phase, well before any sequence runs; by constructing it in run_phase next to the sequence start, its readiness now depends on run-time ordering that the seed and the scheduler perturb. On some runs the model is assembled before the sequence dereferences it; on others the sequence's first register access reaches a model whose registers are not built yet, and it nulls out. The fix moves the entire assembly into the env's build_phase (and the connection into connect_phase), after which the model is guaranteed present before run_phase begins. The lesson: a register model is data with a construction lifecycle you drive in build_phase, not a thing to assemble on the fly in run_phase — build and lock it before any sequence can run, or its readiness becomes a race.

3. Concept — the lifecycle, step by step and phase by phase

A register model's construction is a fixed sequence, and each step has a home phase:

  • Create (build_phase). The environment instantiates the top block with the factory: reg_model = periph_blk::type_id::create("reg_model").
  • Build (build_phase). reg_model.build() runs the block's assembly from 2.1 — creating and configuring every register and field, creating maps, adding registers at their offsets, and nesting sub-blocks.
  • Lock (build_phase). reg_model.lock_model() freezes the structure and resolves every offset into an address (2.1). After this, nothing can be added.
  • Reset (build_phase). reg_model.reset() primes every field's mirror to its reset value (1.3), so the first read has a correct reference.
  • Configure HDL paths (build_phase). Set the backdoor hdl_path on the block (and sub-blocks) so peek/poke know where in the RTL to reach — needed for backdoor access (later chapters).
  • Connect (connect_phase). Wire the model to the bus: map.set_sequencer(sequencer, adapter) and hook the predictor to the monitor (Chapters 5 and 6). This is a different phase because it depends on the agent existing, which is also built in build_phase.

The reason it is manual is the key idea: a uvm_reg_block is a uvm_object, not a uvm_component, so UVM's phaser never calls a build_phase on it — the environment (which is a component) builds it as part of its build_phase. Here is the lifecycle as a flow:

Register model lifecycle: create, build, lock, reset, set HDL paths in build_phase; connect in connect_phase; then ready for run_phasebuild_phasenext phasecreate model(factory)build() — assembleregs/fields/mapslock_model() —freeze + resolveaddressesreset() — prime themirrorset HDL paths(backdoor)connect_phase:set_sequencer +predictorready —run_phasesequences canaccess
Figure 1 — the register model's construction lifecycle, all in the environment's build_phase except the final wiring. Create the model, build it (assemble registers/fields/maps), lock it (freeze + resolve addresses), reset it (prime the mirror), and set HDL paths — then, in connect_phase, wire it to the bus. The model is a uvm_object (data), so the environment drives this by hand; it is not phased automatically. Miss a step or run it in the wrong phase and the model is not ready when a run_phase sequence reaches it.

4. Mental Model — the model is data you assemble before the doors open

5. Working Example — the model assembled in the environment

The environment owns the model and drives its whole lifecycle — assembly in build_phase, wiring in connect_phase:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class periph_env extends uvm_env;
  `uvm_component_utils(periph_env)
  periph_blk        reg_model;   // the register model (a uvm_object)
  apb_agent         agent;       // the bus agent (a uvm_component)
  reg2apb_adapter   adapter;
  apb_reg_predictor predictor;
 
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
 
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agent = apb_agent::type_id::create("agent", this);
 
    // --- Register model lifecycle: all here, in build_phase ---
    reg_model = periph_blk::type_id::create("reg_model");  // 1. create
    reg_model.build();                                     // 2. assemble regs/fields/maps
    reg_model.lock_model();                                // 3. freeze + resolve addresses
    reg_model.reset();                                     // 4. prime the mirror
    reg_model.default_map.set_hdl_path_root("tb.dut");     // 5. backdoor HDL root
 
    adapter   = reg2apb_adapter::type_id::create("adapter");
    predictor = apb_reg_predictor::type_id::create("predictor", this);
  endfunction
 
  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    // Wire the model to the bus HERE — the agent now exists.
    reg_model.default_map.set_sequencer(agent.sequencer, adapter);
    predictor.map     = reg_model.default_map;
    predictor.adapter = adapter;
    agent.monitor.ap.connect(predictor.bus_in);
  endfunction
endclass

A test then just uses the model, confident it is assembled, locked, reset, and connected before run_phase starts:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// In the test's run_phase — the model is already ready.
reg_seq seq = reg_seq::type_id::create("seq");
seq.model = env.reg_model;      // handed the fully-built model
seq.start(env.agent.sequencer);

Every step is in its phase: assembly and priming in build_phase, bus wiring in connect_phase, use in run_phase. Nothing about the model's readiness depends on run-time ordering.

6. Debugging Session — the model built too late

1

Assembling the register model in run_phase makes its readiness race the sequence that uses it

WRONG PHASE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The model is created and assembled in the TEST's run_phase, next to the sequence:
virtual task run_phase(uvm_phase phase);
  phase.raise_objection(this);
  reg_model = periph_blk::type_id::create("reg_model");  // BUG: assembling in run_phase
  reg_model.build();
  reg_model.lock_model();
  reg_seq seq = reg_seq::type_id::create("seq");
  seq.model = reg_model;
  seq.start(env.agent.sequencer);    // may dereference registers not yet ready on some seeds
  phase.drop_objection(this);
endtask
Symptom

The environment fails intermittently — a null-handle error deep inside the register sequence on some seeds, a clean pass on others — and the failure moves with the seed and the machine, the classic signature of a race. It is maddening because the model code is correct and the sequence code is correct; only their relative timing is wrong. Re-running 'fixes' it, then it comes back, and the null pointer points at a register access whose register looks perfectly well defined in the source.

Root Cause

The register model is a uvm_object the environment is responsible for assembling in build_phase, before any run_phase activity. Constructing and building it in run_phase, alongside the sequence start, makes the model's readiness depend on run-time scheduling — and run_phase ordering is not something you can rely on for a data structure a sequence dereferences. On some seeds the model is fully built before the sequence's first register access; on others the access lands before assembly completes and hits a not-yet-built register, nulling out. Nothing is wrong with the model or the sequence individually; the model was simply assembled too late, turning its availability into a race.

Fix

Move the entire model lifecycle into the environment's build_phase — create, build, lock, reset, HDL paths — and the bus wiring into connect_phase, as in Section 5; hand the fully-built model to the sequence in run_phase. Now the model is guaranteed assembled and connected before any sequence runs, and the intermittent null vanishes because there is no longer any ordering to lose. The rule the bug teaches: a register model is data you assemble in build_phase; never build it in run_phase, where its readiness races the sequences that use it. When a model 'sometimes works,' suspect its lifecycle placement before its contents.

7. Common Mistakes

  • Assembling the model in run_phase. Its readiness then races the sequences — the intermittent null above.
  • Forgetting a lifecycle step. Not built (null registers), not locked (no addresses), not reset (stale mirror), no HDL path (backdoor fails) — each a distinct dead-model symptom.
  • Connecting in build_phase. set_sequencer needs the agent's sequencer, which is built in build_phase; wire it in connect_phase when the agent exists.
  • Building the model more than once. Re-running build()/lock_model() on an already-assembled model corrupts it; assemble exactly once.
  • Treating the model like a phased component. It is a uvm_object; UVM never phases it — you assemble it by hand.

8. Industry Best Practices

  • Own the model in the environment, assembled in build_phase. One place, one time, before any sequence runs.
  • Follow the fixed lifecycle order. Create, build, lock, reset, set HDL paths — then connect in connect_phase. The same order every time makes a missing step obvious.
  • Reset the model after locking. Priming the mirror in build_phase means the first read of any register has a correct reference.
  • Set backdoor HDL paths at build time. So peek/poke work from the first access, not only after a later fix-up.
  • Run the lifecycle checklist on any dead model. Created, built, locked, reset, HDL-pathed, connected — six checks that resolve nearly every 'RAL does not work' report.

9. Interview / Review Questions

10. Key Takeaways

  • A register model is a uvm_object (data), not a phased component — the environment assembles it by hand, UVM never phases it for you.
  • The build-time lifecycle is create → build() → lock_model() → reset() → set HDL paths, all in the env's build_phase; wire it to the bus (set_sequencer, predictor) in connect_phase.
  • connect_phase for the wiring because the agent's sequencer and monitor are built in build_phase and guaranteed to exist only by connect_phase.
  • Never assemble the model in run_phase — its readiness then races the sequences that use it, producing intermittent, seed-following null-handle failures.
  • Each omitted step has a distinct symptom (absent model, null registers, unresolved addresses, stale mirror, broken backdoor) — run the lifecycle checklist to localize a dead model fast.