A register test fails at time 0 with Cannot get register by offset: Block raw is not locked. Another, written by someone extending a generated model, fails with Cannot add register to locked block model. Neither message names the line that is wrong, because in both cases the line that is wrong is the order of two calls.
This lesson makes the build order explicit, shows what lock_model() freezes and what it switches on, and then steps back: real models are generated, and the order that matters most is the one between the spec and the generator.
A register model is built like flat-pack furniture: create the parts, connect each to its parent, assemble the insides, then tighten the bolts. lock_model() is tightening the bolts. Before it, you can add registers, maps and sub-blocks; after it, you cannot, and in exchange the model is ready for use: the library resolves every register into its bus addresses, builds the reverse address table, checks for overlaps and registers every name for lookup.
Step | Call | Where | Why here |
|---|---|---|---|
1. Create |
| parent's | The parent owns its children. |
2. Attach |
| right after create | Sets the parent, so names and HDL paths compose. |
3. Assemble |
| after configure | Creates the child's fields or registers. |
4. Map |
| end of | Every register must exist before it is placed. |
5. Freeze |
| once, on the root | Builds the address table for the whole tree. |
6. Connect |
| env | The sequencer and monitor exist only after build_phase. |
dma_reg_block::build() (shown in Blocks, Registers, Fields and Memories) does steps 1 to 5 in order: five creates, five configure(this); build(); pairs, the map, and lock_model(). The env's side is short:
// snippet: from dma_ral_env::build_phase
regmodel = dma_reg_block::type_id::create("regmodel");
regmodel.build(); // creates, maps and locks the modelThe model is created with the factory, so a test could override dma_reg_block with a derived model, and it is built in build_phase so that it exists before any connect. It is not a component, so it has no parent and no phases of its own; the env calls build() explicitly. Its root has no configure() call because the DMA's backdoor does not use HDL paths.
The bench locks inside build() because the block is used on its own. A block written for reuse leaves the lock out and lets the owner of the root call it after composition, regmodel.build(); regmodel.lock_model();. A child that locks itself still composes (the subsystem scratch run in the previous lesson worked), but it is frozen early: nobody can add a debug map or an extra register to it later.
A scratch run built a block by hand and broke the order twice. Looking up an address before the lock:
UVM_ERROR reg/uvm_reg_map.svh(1519) @ 0: reporter [RegModel] Cannot get register by offset: Block raw is not locked.
UVM_INFO ralov_tb.sv(104) @ 0: uvm_test_top [NOLOCK] before lock: null
UVM_INFO ralov_tb.sv(107) @ 0: uvm_test_top [NOLOCK] after lock: raw.lenAnd adding a register after it:
UVM_ERROR reg/uvm_reg_block.svh(1231) @ 0: reporter [RegModel] Cannot add register to locked block modelLookup before lock returns null, and a predictor that receives traffic before the lock drops it. Lock in build_phase; traffic starts in run_phase.
Changes after lock are refused: registers, maps and sub-blocks. Generated models plus hand-written extensions hit this when the extension runs after the generated build() has locked.
build() before configure() builds a child that does not know its parent yet. In this bench, where build() ends with lock_model(), the child would lock as if it were the root.
Two root models with the same name are reported at lock by the IEEE 1800.2 library (UVM/REG/DUPLROOT), because names are registered globally for lookup.
A register wider than 64 bits needs +define+UVM_REG_DATA_WIDTH=128 (or more); the root's lock stops with a fatal error otherwise.
Forgetting the mirror's starting point. A model's fields start at 0 until reset() loads their reset values. The DMA's reset values are all 0, so it does not show here; on a block with non-zero resets, call regmodel.reset() after the lock, or the first mirror check compares against zeros.
Nobody hand-writes a model of three thousand registers. The register map is written once, in a machine-readable format, and generators produce the RTL register file, the C header for firmware, the documentation and the UVM model from it. The common sources are IP-XACT (IEEE 1685, an XML format most EDA tools read), SystemRDL 2.0 (an Accellera language written by hand), and vendor formats such as Synopsys's RALF, which its ralgen tool turns into a UVM model. For SystemRDL, the open-source PeakRDL tools include PeakRDL-uvm, which writes a UVM register model package.
The DMA's registers in SystemRDL look roughly like this (a sketch: check which properties your generator maps to which UVM policy). One file, and the model in dma_ral_tb.sv is the kind of model a generator makes from it:
addrmap dma_regs {
default regwidth = 32;
reg {
field { sw = w; hw = r; singlepulse; } go[0:0] = 0;
field { sw = rw; hw = r; } irq_en[1:1] = 0;
} CTRL @ 0x00;
reg {
field { sw = r; hw = w; } busy[0:0] = 0;
field { sw = rw; hwset; woclr; } done[1:1] = 0;
field { sw = rw; hwset; woclr; } err[2:2] = 0;
} STATUS @ 0x04;
reg { field { sw = rw; hw = r; } value[15:0] = 0; } LEN @ 0x08;
reg { field { sw = rw; hw = r; } value[31:0] = 0; } KEY @ 0x0C;
reg { field { sw = r; hw = w; } value[15:0] = 0; } COUNT @ 0x10;
};Three rules keep a generated model honest. Never edit the output: anything hand-written goes in a separate file that extends or wraps the generated classes, such as callbacks, a derived predictor, or helper tasks. Regenerate in CI: a job runs the generator on the checked-in source and fails if the output differs from what is checked in, so a hand edit or a stale model cannot merge. Generate everything from the same source: if the RTL decoder and the UVM model come from one file, the swapped-offset bug from the maps lesson cannot happen, and the remaining bugs are in the spec, where they belong.
What the source cannot express still has to be written by hand. The busy-LEN rule is prose in the DMA's spec, not a field property, so a generated model believes the LEN write exactly as this one does. Keep a list of such rules next to the source, and a callback or test for each.
A common report from teams adopting a generated model: they add a small hand-written block to a generated subsystem, say a debug register, by calling configure() and add_reg() from their env after regmodel.build(). The generated build() already ended with lock_model(), so the add fails with Cannot add register to locked block model, and the register silently never exists in the address table. The fix is the order: put the extension in the generator's source, or keep lock_model() out of the generated build() and call it once from the env, after every extension has been added.
connect_phase instead of build_phase, after apb.ap.connect(predictor.bus_in). The predictor's map handle is set in the same function. What can go wrong?The predictor needs a locked model with a map before the first transfer, and it gets one, since traffic starts in run_phase. But any component that looks up the model in its own build_phase or connect_phase (a sequence config, a coverage collector) may run first and find null. Building in build_phase and connecting in connect_phase makes the order independent of which component's phase runs first.
dma_reg_block file. Which rule was broken, and what would have caught it?Hand-written code went into a generated file, which the generator overwrites. The task belongs in a separate file (a derived class or a wrapper). A CI job that regenerates the model and diffs it against the checked-in copy would have failed the moment the generated file was edited by hand.
Next: the model is built, locked and connected. The Frontdoor Path, End to End follows one access through it.