Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations18
  • UVM Phases & Simulation Lifecycle94
  • Factory & Configuration37
  • Transaction-Level Modeling (TLM)42
  • UVM Sequences & Stimulus44
  • UVM Agents & Protocol VIP71
  • Environment & Test Classes55
  • Checking & Coverage in UVM37
  • UVM Register Abstraction Layer (RAL)53
    • RAL Overview
      • Why a Register Model: Desired, Mirrored and the DUT
      • Blocks, Registers, Fields and Memories
      • Generated vs Handwritten RAL: Workflow and Extension Strategy
      • Maps, Offsets and Endianness
      • Nested Blocks and Submaps
      • Build, Lock and Generated Models
      • The Frontdoor Path, End to End
      • The Register Layer: Full Source
    • Register Access
    • Registers, Fields, Access Policies & Reset
    • Bus Adapters & Built-in Register Sequences
    • Registers in Verification
    • Mirror & Predict: Desired, Mirrored, and Checking
    • Frontdoor vs Backdoor Access
    • RAL Integration in the Environment
  • Advanced UVM in Practice44
  • Debug & Regression10
  • Coding Practice47
  • Senior Prep: Architecture, Leadership and Interviews92
  • Verification in Industry Practice10
Loading lesson

‹RAL Overview
  1. Topics
  2. UVM Verification
  3. UVM Register Abstraction Layer (RAL)
  4. RAL Overview
  5. Build, Lock and Generated Models

Part 9 · Register Model (RAL) · Intermediate

Build, Lock and Generated Models

The order a register model is built in, what lock_model() freezes and enables, the configure/build/lock pitfalls with their real messages, and generated models (IP-XACT, SystemRDL with PeakRDL-uvm, ralgen) kept to one source of truth.

On this page

    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.

    The idea: assemble, then freeze, then use

    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

    type_id::create() for registers and blocks, new() for a uvm_mem

    parent's build()

    The parent owns its children.

    2. Attach

    child.configure(this, hdl_path)

    right after create

    Sets the parent, so names and HDL paths compose.

    3. Assemble

    child.build()

    after configure

    Creates the child's fields or registers.

    4. Map

    create_map(), add_reg(), add_mem(), add_submap()

    end of build()

    Every register must exist before it is placed.

    5. Freeze

    lock_model()

    once, on the root

    Builds the address table for the whole tree.

    6. Connect

    map.set_sequencer(), predictor map and adapter

    env connect_phase

    The sequencer and monitor exist only after build_phase.

    The picture: who calls what, and when

    Sequence diagram: the env creates the block and calls build in build_phase; the block creates, configures and builds each register, which creates and configures its fields; the block creates the map and adds five registers, then locks the model, which builds the address table; in connect_phase the env binds the map to the APB sequencer and adapter and wires the predictor.
    The DMA model's build order. Everything structural happens inside the block's build(); the env only creates it, and connects it once the agents exist.

    Build it: the DMA's order, line by line

    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:

    design.sv
    // snippet: from dma_ral_env::build_phase
    regmodel = dma_reg_block::type_id::create("regmodel");
    regmodel.build();                    // creates, maps and locks the model

    The 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.

    The pitfalls, with their real messages

    A scratch run built a block by hand and broke the order twice. Looking up an address before the lock:

    Diagram
    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.len

    And adding a register after it:

    Diagram
    UVM_ERROR reg/uvm_reg_block.svh(1231) @ 0: reporter [RegModel] Cannot add register to locked block model
    • Lookup 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.

    Generated models: one source of truth

    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:

    Diagram
    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.

    What breaks: the extension that ran too late

    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.

    Interview

    "What does lock_model() do, and where do you call it?"

    A weak answer: "it locks the model so nobody can change it; you call it at the end of build." True, but it says nothing about what it enables or which build.

    A strong answer: "It freezes the structure and finishes it: the library resolves every register to its bus addresses through all the submaps, builds the reverse table that get_reg_by_offset and the predictor use, warns about overlaps and registers names for lookup. Before it, reverse lookups fail; after it, adding registers, maps or blocks fails. I call it once, on the root, after every sub-block is composed, normally in the env's build_phase right after build(), then reset() to load the reset values, and connect the map to the sequencer in connect_phase."

    Follow-ups: How do you keep a generated model and the RTL in step? (generate both from one source and regenerate in CI with a diff check) and Where does hand-written behaviour go? (in callbacks, derived classes or a wrapper, never in the generated files).

    Check yourself An env builds the model in 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.

    Check yourself A regenerated model drops a helper task someone had added to the generated 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.

    Related topics

    • The frontdoor path, end to end

    • Nested blocks and submaps

    • Blocks, registers, fields and memories

    Previous

    408 / 658

    Next: The Frontdoor Path, End to End

    Up next

    Up nextThe Frontdoor Path, End to End

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why a Register Model: Desired, Mirrored and the DUT
    • Blocks, Registers, Fields and Memories
    • Generated vs Handwritten RAL: Workflow and Extension Strategy