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_pathon the block (and sub-blocks) sopeek/pokeknow 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 inbuild_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:
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:
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
endclassA test then just uses the model, confident it is assembled, locked, reset, and connected before run_phase starts:
// 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
Assembling the register model in run_phase makes its readiness race the sequence that uses it
WRONG PHASE// 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);
endtaskThe 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.
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.
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_sequencerneeds the agent's sequencer, which is built inbuild_phase; wire it inconnect_phasewhen 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_phasemeans the first read of any register has a correct reference. - Set backdoor HDL paths at build time. So
peek/pokework 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) inconnect_phase. connect_phasefor the wiring because the agent's sequencer and monitor are built inbuild_phaseand guaranteed to exist only byconnect_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.