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
      • Why a Register Model: Desired, Mirrored and the DUT
      • Blocks, Registers, Fields and Memories
      • 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 in Verification
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹RAL Overview
  1. Topics
  2. UVM Verification
  3. UVM Register Abstraction Layer (RAL)
  4. RAL Overview
  5. Nested Blocks and Submaps

Part 9 · Register Model (RAL) · Intermediate

Nested Blocks and Submaps

Composing block models into a subsystem model: two dma_reg_block instances under one block with add_submap, address and HDL-path composition, lookups on the root map, and an overlapping base that only warns.

On this page

    The DMA is going into a subsystem with a second DMA behind it. The register model for the subsystem could be written from scratch, ten registers in one block. Then the DMA's spec changes, a STATUS bit is added, and someone has to remember to edit two models. Or the subsystem model could contain the DMA's model twice, unchanged, at two base addresses. That is what submaps are for.

    This lesson composes two dma_reg_block instances into one subsystem model, checks the addresses and HDL paths it produces in a scratch run, and shows the copy-paste slip that the library reports only as a warning.

    The idea: a model of models

    A block can contain sub-blocks: child.configure(this) makes the child part of the parent's tree, so its registers get names like ss.dma1.len. Addresses compose separately, through maps: the parent creates its own map and calls add_submap(child.map, offset), which places the child's whole map at that offset. The child keeps its own offsets (LEN is still at 0x08 inside it); the parent decides where the child sits.

    Think of street addresses. The DMA's model is a building with numbered flats. The subsystem is a street that says "building one is at number 0, building two is at number 1000". Nobody renumbers the flats when a second building goes up.

    This is the register side of vertical reuse. The course's subsystem bench, dma_subsys_tb.sv, reuses dma_env twice, and each DMA there has its own APB port, so each would keep its own model and map. When both DMAs sit behind one APB bridge in a single address space, which is the usual case in a chip, you compose them like this.

    The picture

    Tree diagram: the subsystem block with a map at base 0x4000_0000 contains dma0 as a submap at 0x0000 and dma1 as a submap at 0x1000; dma0.len is at 0x4000_0008 and dma1.len at 0x4000_1008.
    Two unchanged DMA models under one subsystem block. Each address is the root base, plus the submap offset, plus the register's own offset.

    Build it: the subsystem block

    The parent creates its map first, then creates, configures and builds each child, and adds each child's map as a submap. The order inside each child matters: configure(this, ...) before build(), so the child knows its parent before it builds (and, in this bench, locks) itself. The second argument of configure is the child's HDL path relative to the parent, used later by backdoor access.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // snippet: scratch-run code, not part of the bench
    class dma_subsys_reg_block extends uvm_reg_block;
      `uvm_object_utils(dma_subsys_reg_block)
      rand dma_reg_block dma0, dma1;
      uvm_reg_map        map;
      int unsigned       dma1_base = 'h1000;
    
      function new(string name = "dma_subsys_reg_block");
        super.new(name, UVM_NO_COVERAGE);
      endfunction
    
      virtual function void build();
        map  = create_map("map", 'h4000_0000, 4, UVM_LITTLE_ENDIAN);
        dma0 = dma_reg_block::type_id::create("dma0");
        dma0.configure(this, "u_dma0");
        dma0.build();
        dma1 = dma_reg_block::type_id::create("dma1");
        dma1.configure(this, "u_dma1");
        dma1.build();
        map.add_submap(dma0.map, 'h0000);
        map.add_submap(dma1.map, dma1_base);
        lock_model();
      endfunction
    endclass

    The test that builds it gives the root block its own HDL path, ss.configure(null, "top.u_ss"), then calls ss.build(). In an env, the predictor and set_sequencer use ss.map, the root map, because that is the map whose addresses appear on the bus.

    Note

    dma_reg_block calls lock_model() at the end of its own build(), which is convenient for the block bench. Inside a parent, that locks each child before the parent has finished composing. It works here, because the parent's own lock_model() still builds the address table for the whole tree, but a locked child can no longer take new registers or maps. A block meant for reuse leaves the lock to whoever owns the root; Build, Lock and Generated Models explains why.

    Many instances of one block

    Two DMAs are written out by hand above so that each line is visible. A chip with eight channels uses an array and a stride instead, so the placement rule lives in one expression and a test can loop over the channels the same way. The HDL path follows the same index, so the backdoor of channel 5 reaches u_dma5 without anyone typing it.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet
    rand dma_reg_block ch[8];
    // ...in build(), after create_map:
    foreach (ch[i]) begin
      ch[i] = dma_reg_block::type_id::create($sformatf("ch%0d", i));
      ch[i].configure(this, $sformatf("u_dma%0d", i));
      ch[i].build();
      map.add_submap(ch[i].map, i * 'h1000);
    end

    If the hardware can move a block at run time (a remap register, a BAR in PCIe), the parent map's set_submap_offset(child_map, new_offset) changes the base; on a locked model it rebuilds the address table, so the predictor follows the move.

    Watch it run

    A scratch run (Verilator, the 1800.2 library) builds the subsystem model and asks it for addresses in both directions:

    Diagram
    UVM_INFO ralov_tb.sv(37) @ 0: uvm_test_top [SS] dma0.len @ 40000008, dma1.len @ 40001008, dma1.count @ 40001010
    UVM_INFO ralov_tb.sv(39) @ 0: uvm_test_top [SS] root map, 'h4000_1008 -> ss.dma1.len
    UVM_INFO ralov_tb.sv(41) @ 0: uvm_test_top [SS] root map, 'h4000_1014 -> null
    UVM_INFO ralov_tb.sv(45) @ 0: uvm_test_top [SS] dma1.map base: 40001000, root of dma1.map: ss.map
    UVM_INFO ralov_tb.sv(60) @ 0: uvm_test_top [SS] dma1 hdl path: top.u_ss.u_dma1
    UVM_ERROR :    0
    • get_address() composes the root base, the submap offset and the register offset: 0x4000_0000 + 0x1000 + 0x08.

    • The root map answers the reverse lookup with the full system address, and returns null for the gap after COUNT.

    • dma1.map now reports a base of 0x4000_1000 and ss.map as its root: the child map has become part of the parent's address space.

    • The HDL path composes the same way: the root's top.u_ss, then the child's u_dma1. A register's backdoor path would add its own slice below that.

    Look addresses up on the root map with system addresses. The predictor sees the bus, and the bus carries 0x4000_1008, not 0x08.

    What breaks: two DMAs at one address, and only warnings

    In the scratch run, the second submap's base was then set to 0, the slip you make when you copy the dma0 line and forget to edit it. The model built, the test ran, and the summary said UVM_ERROR : 0. The library had noticed: it printed twelve RegModel warnings at lock time, five of them like this:

    Diagram
    UVM_WARNING reg/uvm_reg_map.svh(1611) @ 0: reporter [RegModel] In map 'ss.dma1.map' register 'ss.dma1.len' maps to same address as register 'ss.dma0.len': 'h40000008
    UVM_INFO ralov_tb.sv(77) @ 0: uvm_test_top [OVL] root map, 'h4000_0008 -> ss.dma0.len
    UVM_WARNING :   14
    UVM_ERROR :    0

    Now follow a write to dma1's LEN in a real bench. The bus carries 0x4000_0008, the predictor asks the root map, and the root map says dma0.len. dma0's mirror changes; dma1's never does. Every later failure points at the wrong DMA. The fix is cheap: in your base test, turn this one warning into an error with a report catcher that matches "maps to same address" (the RegModel ID is shared by many harmless messages), so that the slip fails the first run instead of a regression three weeks later.

    One more composition rule: a map can have only one parent map. Adding the same child map to two parents reports an error. A register that must be reachable from two buses goes into two maps, one per bus, each with its own sequencer and predictor; Registers in Verification covers multiple maps.

    Interview

    "How do you build a register model for a subsystem from the block models?"

    A weak answer: "copy the block registers into a new top-level model." That forks the source and throws away every block-level check.

    A strong answer: "Instantiate each block model unchanged as a sub-block: configure it with the parent and its HDL path, build it, and add its map to the parent's map with add_submap at the block's base. Lock once at the root, after composition, and give the root map to the predictor and set_sequencer, because the bus carries system addresses. Repeated instances use the same class at strided bases. I turn the overlap warning into an error, because a wrong base only warns and then mispredicts the wrong block."

    Follow-ups: How do you move a block at run time, say after a remap register is written? (set_submap_offset on the parent map; after lock it rebuilds the address table) and Where does the HDL path come from? (each level's configure path, concatenated from the root).

    Check yourself A write to dma1's CTRL shows up in the log as a prediction on dma0's CTRL. Which composition term is wrong, and how do you confirm it?

    A submap base: the leaf offsets are shared by construction, so the error is where dma1's map was placed. Print ss.dma1.ctrl.get_address() and look for the lock-time RegModel warning "maps to same address as register"; an overlap always produces one.

    Check yourself The predictor in a subsystem env is given ss.dma1.map instead of ss.map. What happens to an observed write at 0x4000_1008?

    The address table that holds system addresses belongs to the root map. Asked for 0x4000_1008, the child map finds nothing, returns null, and the predictor drops the transfer with only a UVM_FULL message, so dma1's mirror never moves. Give the predictor the root map; the scratch run shows ss.map resolving 0x4000_1008 to ss.dma1.len.

    Next: building and composing has an order, and one call freezes it. Build, Lock and Generated Models makes that order explicit.

    Related topics

    • Build, lock and generated models

    • Maps, offsets and endianness

    • Vertical reuse

    • Registers in Verification

    Previous

    150 / 280

    Next: Build, Lock and Generated Models

    Up next

    Up nextBuild, Lock and Generated Models

    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
    • The Frontdoor Path, End to End