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.
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 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.
// 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
endclassThe 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.
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.
// 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);
endIf 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.
A scratch run (Verilator, the 1800.2 library) builds the subsystem model and asks it for addresses in both directions:
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 : 0get_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.
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:
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 : 0Now 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.
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.
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.