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 Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
    • RAL Overview
    • Register Access
    • Registers in Verification
      • Built-in Register Sequences and NO_* Attributes
      • Register Coverage
      • Memories in the Register Model
      • Multiple Maps and Address Spaces
      • Shadow, Alias and Paged Registers
      • Interrupts with the Register Model
      • The Register Model at SoC Scale
      • RAL Interview Set
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Registers in Verification
  1. Topics
  2. UVM Verification
  3. UVM Register Abstraction Layer (RAL)
  4. Registers in Verification
  5. The Register Model at SoC Scale

Part 9 · Register Model (RAL) · Intermediate

The Register Model at SoC Scale

Composing block models into a chip model, relocating bases, reusing block sequences, keeping huge models fast, and generated models in CI.

On this page

    The DMA ships in a chip with two instances, at 0x0000 and 0x1000. The integration team asks for the block's register tests at chip level by Friday, and someone starts copying dma_ral_reset_test and adding 0x1000 to every address. A senior engineer stops them: the block's model, the block's sequences and the block's tests should run unchanged. Only the chip model is new, and it is about thirty lines.

    This lesson composes block models into a chip model, relocates a block at run time, reuses block register sequences at chip level, keeps a model with a hundred thousand registers fast to build, and keeps generated models honest in CI. It also shows a chip-level bug that every register test passed: a write to one DMA that the model credited to the other.

    The idea: house numbers and street names

    A house number is fixed by the house; the street is fixed by the town plan. Move the house to another street and its number does not change, only its full address does. A block model is the house: CTRL is at offset 0x00 of the DMA whatever chip it is in. The chip model is the town plan: it places each block's map at a base address inside its own map (a submap). Addresses are resolved when the root model is locked, and again whenever a base moves, so nothing written against the block ever sees the chip's numbers.

    Three consequences make reuse work. A register sequence takes a model handle (any uvm_reg_block), so pointing it at soc.dma1 runs it on that instance. The block's map has no sequencer of its own at chip level; accesses go through the root map's sequencer and adapter. And one predictor on the chip bus decodes every block, because get_reg_by_offset on the root map knows the full addresses.

    The picture

    Flowchart: soc_reg_block with a root map containing dma0's map at 0x0000 and dma1's map at 0x1000, each holding its registers; a reset sequence targets dma0, and one predictor uses the root map.
    The chip model places each block's map as a submap at a base address. Block sequences take a sub-block as their model; accesses and prediction go through the root map.

    Build it: two DMAs in one model

    The chip block

    The chip block creates each block model, configures it with the chip as parent, builds it, and adds the block's map to its own. It is verified in a scratch test that reuses dma_reg_block from the bench unchanged:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // snippet: scratch chip model
    class soc_reg_block extends uvm_reg_block;
      `uvm_object_utils(soc_reg_block)
      rand dma_reg_block dma0, dma1;
      uvm_reg_map soc_map;
      function new(string name = "soc_reg_block");
        super.new(name, UVM_NO_COVERAGE);
      endfunction
      virtual function void build();
        soc_map = create_map("soc_map", 'h0, 4, UVM_LITTLE_ENDIAN);
        dma0 = dma_reg_block::type_id::create("dma0");
        dma0.configure(this);                  // parent first: names become soc.dma0.*
        dma0.build();
        dma1 = dma_reg_block::type_id::create("dma1");
        dma1.configure(this);
        dma1.build();
        soc_map.add_submap(dma0.map, 'h0000);
        soc_map.add_submap(dma1.map, 'h1000);
        lock_model();                          // the root lock resolves every address
      endfunction
    endclass

    dma_reg_block::build() calls lock_model() itself, which is convenient for a block bench and questionable for a reusable block. It works here: a sub-block's lock does not build the chip's address maps (only the root's lock does), and the chip's lock_model() skips children that are already locked. Generated blocks usually leave locking to the top, and that is the safer habit. Configure before build matters for another reason: a block built before it has a parent behaves as a root model of its own.

    At chip level, configure also takes an HDL path segment: dma0.configure(this, "u_dma0") with the chip block configured as configure(null, "tb_top.u_soc") makes every backdoor path in the instance resolve to tb_top.u_soc.u_dma0... without editing the block model. This course's runs use no HDL-path DPI, so that part is not shown running here.

    The env: one adapter and one predictor on the root map

    design.sv
    1
    2
    3
    4
    5
    // snippet: scratch env connect_phase
    soc.soc_map.set_sequencer(apb.sqr, adapter);   // the root map owns the bus
    predictor.map     = soc.soc_map;               // decodes full chip addresses
    predictor.adapter = adapter;
    apb.ap.connect(predictor.bus_in);

    Relocating a block, and reusing its sequence

    design.sv
    1
    2
    3
    4
    // snippet: scratch test run_phase
    dma1.map.set_base_addr('h2000);       // on a submap: the parent's set_submap_offset
    seq.model = soc.dma0;                 // the block's reset sequence, unchanged
    seq.start(null);

    set_base_addr on a submap calls the parent's set_submap_offset, and when the model is locked the root map rebuilds its address tables, so get_address() and the predictor's decoding follow at once. Block-level register sequences of your own get reuse for free if they follow the same convention: take a uvm_reg_block handle (or the typed block), never a hard-coded address, and never assume the block is the root.

    Watch it run

    Diagram
    scratch run, Verilator
    UVM_INFO scratch.sv(755) @ 0: uvm_test_top [SOC] dma0.len @8  dma1.len @1008  default_map of dma1: soc.dma1.map
    UVM_INFO scratch.sv(760) @ 0: uvm_test_top [SOC] after dma1.map.set_base_addr(2000): dma1.len @2008; soc_map offset 2008 -> soc.dma1.len
    UVM_INFO reg/sequences/uvm_reg_hw_reset_seq.svh(153) @ 0: reporter@@seq [uvm_reg_hw_reset_seq] Verifying reset value of register soc.dma0.ctrl in map "soc.dma0.map"...
    UVM_INFO reg/sequences/uvm_reg_hw_reset_seq.svh(153) @ 75000: reporter@@seq [uvm_reg_hw_reset_seq] Verifying reset value of register soc.dma0.len in map "soc.dma0.map"...
    UVM_INFO reg/sequences/uvm_reg_hw_reset_seq.svh(153) @ 115000: reporter@@seq [uvm_reg_hw_reset_seq] Verifying reset value of register soc.dma0.key in map "soc.dma0.map"...
    UVM_INFO scratch.sv(764) @ 155000: uvm_test_top [SOC] hw reset sequence passed on soc.dma0 through the SoC map
    UVM_ERROR :    0
    • dma1.len at 0x1008, then 0x2008: the block offset 0x08 never changed; the base did, and the root map's decoding (offset 2008 -> soc.dma1.len) followed immediately.

    • The reset sequence reports map `soc.dma0.map` but its accesses went out through the root map's sequencer. It checked the same three registers as at block level, for the same reason (STATUS and COUNT are volatile).

    • The scratch bench has one DMA on the bus, so only dma0 is exercised by traffic; dma1 is there to show addressing, which is where the next section's bug lives.

    What breaks: the write that landed in the other DMA's mirror

    The scratch test then wrote dma1's KEY through the chip model and printed both mirrors:

    Diagram
    scratch run, Verilator
    UVM_INFO scratch.sv(770) @ 215000: uvm_test_top [SOC] wrote dma1.key (@200c) = d1d1d1d1: dma1.key mirror=0, dma0.key mirror=d1d1d1d1

    The model asked for address 0x200C. The bench's apb_item has an 8-bit address, so reg2bus (it.addr = rw.addr) silently truncated it to 0x0C, and the APB bus carried a write to offset 0x0C. The predictor, decoding the monitored 0x0C with the chip map, found dma0.key and updated it. dma1's mirror never moved. Every block-level test passes on such a bench, and so does a chip-level reset test, because no address above 0xFF is ever compared. The fix belongs in the adapter, which is the only place that knows both widths:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // snippet: an adapter that refuses addresses its bus cannot carry
    virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
      apb_item it = apb_item::type_id::create("it");
      if (rw.addr >> $bits(it.addr))
        `uvm_error("ADDR_WIDTH", $sformatf("address %0h does not fit the %0d-bit bus", rw.addr, $bits(it.addr)))
      it.addr  = rw.addr;
      it.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE});
      it.data  = rw.data;
      return it;
    endfunction

    The same family includes an interconnect that decodes fewer address bits than the chip map uses, so two blocks alias in hardware: the model says two registers, the silicon has one. uvm_reg_mem_shared_access_seq and an address-walk test (write a unique value to every register through the chip map, then read every one back) catch both before firmware does.

    Huge models: what costs time and memory

    A chip model can hold tens of thousands of registers and hundreds of thousands of fields, each an object created through the factory at build time with its own configuration, policy string and value storage. The levers that matter, in order:

    Lever

    What it changes

    Caveat

    UVM_REG_DATA_WIDTH / UVM_REG_ADDR_WIDTH

    Both default to 64. uvm_reg_data_t and uvm_reg_addr_t are sized by them, so every stored value, mask and address in the model shrinks if you define them as 32

    A compile-time +define for the whole compile, including the library. lock_model() stops with a fatal if any register or field is wider than UVM_REG_DATA_WIDTH

    Build only what the test uses

    A config knob tells the chip block which sub-blocks to build; unused subsystems cost nothing

    The address map must not depend on absent blocks, and nothing can be added after lock_model()

    Coverage off by default

    No covergroup objects, no sampling on every access

    Turn it on per block for the tests that close register coverage

    One model per test, built once

    Build in the env's build_phase, pass the handle down through config_db

    Never build a model per sequence or per agent

    Avoid per-access string work

    Lookups such as get_reg_by_name() in hot loops are slow on big models

    Resolve handles once, keep them in the sequence

    Measure before tuning: build the chip model alone in a tiny test and time it with the simulator's profiler. The fix for a 20-minute build is almost never in the test code.

    Keeping generated models honest in CI

    At chip scale nobody writes register models by hand. One source (SystemRDL or IP-XACT) generates the RTL register file, the UVM model, C headers for firmware and the documentation. The model is only trustworthy if it stays generated from the same revision as the RTL:

    • Regenerate in CI, then diff. The job runs the generator from the source and fails if the checked-in model differs. Hand edits to generated files are caught the day they happen.

    • Pin the generator version. A generator upgrade that changes emitted code is a reviewed change, not a surprise in a nightly run.

    • Run the built-in sequences on every regenerated block. Reset and bit-bash on the block bench are the cheapest proof that the RTL and the model came from the same source.

    • Keep customisations outside the generated files. Callbacks, custom frontdoors and coverage extensions live in your own classes, attached by factory override or uvm_reg_field_cb::add, so regeneration never erases them.

    Interview

    "How do you reuse a block-level register model and its tests at SoC level?"

    A weak answer: "Copy the block model into the SoC model and add the base address to every register."

    A strong answer: "The block model is instantiated unchanged under a chip block: configure(this, hdl_segment), build(), and its map added as a submap at the block's base. The root locks once and resolves every address. The root map owns the sequencer and adapter, and one predictor on the chip bus decodes all blocks. Block sequences take a model handle, so seq.model = soc.dma1 reuses them as they are. Bases can move at run time with set_base_addr or set_submap_offset. At scale I size UVM_REG_DATA_WIDTH and UVM_REG_ADDR_WIDTH to the design, build only the subsystems a test needs, keep coverage off by default, and regenerate the model in CI so it cannot drift from the RTL."

    Follow-ups: Chip-level register tests pass but a write to one instance changes another's mirror. Where do you look? (address width in the adapter or the interconnect decode). Why must the sub-block be configured before it is built? (until it has a parent it is a root model, with its own name and address maps).

    Check yourself At chip level a test calls soc.dma1.map.set_sequencer(...). Is that needed?

    No. A submap uses its root map's sequencer and adapter; accesses through soc.dma1.map go out on the chip bus through soc_map. Setting a sequencer on the submap is at best redundant. Configure the bus once, on the root map.

    Check yourself The chip model is built with UVM_REG_DATA_WIDTH defined as 32 to save memory. One IP has a 64-bit timestamp register. What happens?

    lock_model() on the root reports a fatal: the model requires UVM_REG_DATA_WIDTH of at least the widest register or field, and it is checked once, when the root is locked. Either keep 64 bits for the whole compile, or model the timestamp as two 32-bit registers if the hardware allows two accesses.

    Check yourself Why did the chip-level reset test pass even though dma1's addresses were being truncated?

    The scratch bench ran the reset sequence on dma0 only, at base 0, where truncation changes nothing. Even run on dma1, it reads reset values, and both instances reset to the same values, so a read of the wrong DMA returns the expected number. Only a test that writes unique values per instance and reads them back, through the chip map, exposes the aliasing.

    Next: you have now seen the register layer from the inside, at block and at chip level. RAL Interview Set turns it into the answers senior interviewers listen for.

    Related topics

    • Up: Registers in Verification

    • Next: RAL Interview Set

    • Nested blocks and submaps

    Previous

    167 / 280

    Next: RAL Interview Set

    Up next

    Up nextRAL Interview Set

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Built-in Register Sequences and NO_* Attributes
    • Register Coverage
    • Memories in the Register Model