UVM RAL · Chapter 3 · Register Maps
Multiple & Hierarchical Maps
A map is a view of a set of registers from one bus, and real systems need more than one view. Two situations drive this. A block is often reachable from more than one bus, such as a fast functional interconnect and a separate low-speed debug bus, so it carries one map per bus, each with its own base, sequencer, and adapter, all addressing the same registers. And a system model is a tree of blocks, so each sub-block's map is nested into its parent's map at a base offset with the add submap call. Addresses then compose: a deep register's address is the sum of the base offsets down the tree plus its own offset, which lets you build an IP map once and drop it into an SoC at any base. This page covers both, and breaks a model where a forgotten add submap call leaves a sub-block's registers unreachable from the top.
Foundation12 min readUVM RALuvm_reg_mapadd_submapHierarchyMultiple MapsAddress Space
Chapter 3 · Section 3.2 · Register Maps
1. Why Should I Learn This?
The moment a design is larger than one block on one bus — which is almost always — you need more than one map. A system model that does not compose its sub-block maps into a system map has registers that exist in isolation but cannot be reached from the top; a multi-bus block that models only one bus can never verify the second access path. Both are structural gaps that make whole regions of the register space untestable, and both are invisible until something tries to reach the un-mapped view.
Learning multiple and hierarchical maps lets you build reusable IP-level models that drop into any system at any base, verify every bus that can reach a block, and reason about how a deep register's address composes from the hierarchy. It is what turns a pile of block models into a coherent, addressable system model.
2. Industry Story — the sub-block that vanished at the top
An SoC register model nests a timer sub-block inside the top soc block. The engineer builds the timer block correctly — its registers, its own default_map, its offsets — and adds the timer as a sub-block of the top. But they forget one line: they never call add_submap to merge the timer's map into the top-level map at the timer's base offset (0x1000).
Unit-level timer tests, which use the timer block's own map, pass — the timer's registers are addressable within the timer's map. But every system-level test, which reaches registers through the top soc map, fails to find the timer registers: soc_model.timer.load.write(...) cannot resolve, because timer.load has an address in the timer's map but no address in the top map — it was never composed in. The timer registers are simultaneously fully working (in their own view) and completely unreachable (from the system view), which is baffling until you see that a map is a view and the timer was never added to the top view. Days are lost because the sub-block 'obviously exists' and its unit tests pass. The fix is the one missing line. The lesson: a sub-block's registers are only reachable from a parent map if the sub-block's map is composed into it with add_submap; nesting the block is not enough — you must nest the map.
3. Concept — multiple views and composed views
Two independent capabilities, both about maps as views:
- Multiple maps (one block, several buses). Call
create_mapmore than once — one map per bus — each with its ownbase_addr,n_bytes,endian, and (viaset_sequencer) its own sequencer and adapter. Add the same registers to each map, at that map's offsets. The registers are shared objects with one behaviour; each map is a different road to them. A frontdoor access selects the road by the map it uses (reg.write(status, value, .map(debug_map))), andget_default_mapgives the one used when none is specified. - Hierarchical maps (composing a system address space). A parent block merges a child block's map into its own with
parent_map.add_submap(child.default_map, base_offset). Addresses then compose: a register in the child appears in the parent map atbase_offset + child_offset, and recursively up the tree, so a deep register's system address is the sum of the base offsets down to its block plus its own offset. This is what makes a sub-block model relocatable — its internal offsets never change; only the base at which its map is composed does.
The two combine: a system map is built by composing sub-block maps, and a block can still have multiple such maps for multiple system buses. Here is the composition — a top map nesting sub-block maps, with addresses summing down the tree:
4. Mental Model — a map is a view; nesting the block is not nesting the view
5. Working Example — two buses, and a composed hierarchy
A block reachable from two buses carries two maps addressing the same registers:
virtual function void build();
// ... create + configure + build registers ...
// Map 1: the fast functional bus.
fast_map = create_map("fast_map", .base_addr('h0), .n_bytes(4), .endian(UVM_LITTLE_ENDIAN));
fast_map.add_reg(ctrl, .offset('h00), .rights("RW"));
fast_map.add_reg(status, .offset('h04), .rights("RO"));
// Map 2: the debug/config bus — SAME registers, its own address space.
dbg_map = create_map("dbg_map", .base_addr('h0), .n_bytes(4), .endian(UVM_LITTLE_ENDIAN));
dbg_map.add_reg(ctrl, .offset('h00), .rights("RW"));
dbg_map.add_reg(status, .offset('h04), .rights("RO"));
default_map = fast_map; // the map used when none is specified
lock_model();
endfunctionA parent composes each sub-block's map into a system map with add_submap:
class soc_blk extends uvm_reg_block;
`uvm_object_utils(soc_blk)
rand timer_blk timer;
rand uart_blk uart;
virtual function void build();
default_map = create_map("default_map", .base_addr('h4000_0000),
.n_bytes(4), .endian(UVM_LITTLE_ENDIAN));
timer = timer_blk::type_id::create("timer");
timer.configure(this); timer.build();
default_map.add_submap(timer.default_map, .offset('h1000)); // COMPOSE the timer's map
uart = uart_blk::type_id::create("uart");
uart.configure(this); uart.build();
default_map.add_submap(uart.default_map, .offset('h2000)); // COMPOSE the uart's map
lock_model();
endfunction
endclassNow both views and the composed hierarchy work, and addresses sum down the tree:
// Choose the bus by choosing the map:
periph.ctrl.write(s, 32'h3); // via default (fast) map
periph.ctrl.write(s, 32'h3, .map(periph.dbg_map)); // via the debug bus
// System-level access: timer.load resolves to 0x4000_0000 + 0x1000 + 0x00.
soc_model.timer.load.write(s, 32'hFF); // reachable because the timer map was composedThe registers are the same objects across maps; the addresses compose because each sub-block's map was added into the parent — the one line the next section forgets.
6. Debugging Session — the sub-block map that was never composed
Nesting a sub-block without add_submap leaves its registers addressable in their own map but unreachable from the parent
MISSING ADD_SUBMAPvirtual function void build(); // in soc_blk
default_map = create_map("default_map", .base_addr('h4000_0000),
.n_bytes(4), .endian(UVM_LITTLE_ENDIAN));
timer = timer_blk::type_id::create("timer");
timer.configure(this);
timer.build();
// BUG: the timer BLOCK is nested, but its MAP is never composed into the top map.
// (missing) default_map.add_submap(timer.default_map, .offset('h1000));
lock_model();
endfunction
// System-level access cannot resolve the timer's registers:
soc_model.timer.load.write(s, 32'hFF); // no address in the top map -> resolution errorUnit-level timer tests pass — the timer's registers work through the timer's own map — but every system-level access through the top soc map fails to resolve the timer's registers, as if they had no address. The sub-block 'obviously exists' (its class is there, its unit tests are green), so the failure is baffling: the same register is reachable one way and invisible another. It looks like a hierarchy or handle bug, when in fact the registers simply have no address in the top-level view.
Nesting the sub-block wired it into the component/handle hierarchy — soc_model.timer is a valid handle — but nesting the block does not compose its map into the parent's address space. add_submap is the step that merges the sub-block's map into the parent map at a base offset, and without it the timer's registers have addresses only in the timer's own map, not in the top soc map. So a unit test using the timer map resolves them, and a system test using the soc map cannot — the registers are simultaneously working (their view) and unreachable (the parent view). The block is present; its addresses were never added to the top.
Compose the sub-block's map into the parent: default_map.add_submap(timer.default_map, .offset('h1000));. Now soc_model.timer.load resolves to 0x4000_0000 + 0x1000 + 0x00 and system-level access reaches it. The rule the bug teaches, and the review check for any hierarchical model: nesting a sub-block is component structure; reaching its registers from a parent needs the sub-block's map composed in with add_submap. When a sub-block works in isolation but is unreachable from the top, the missing add_submap is the first suspect.
7. Common Mistakes
- Forgetting
add_submap. The sub-block is nested but its registers have no address in the parent map — reachable in isolation, invisible from the top. - Modelling only one bus for a multi-bus block. The second access path is never verifiable; give each bus its own map.
- Adding a register to only one of several maps. It is then reachable through one view and missing from the others.
- Baking system base offsets into a sub-block's internal offsets. The sub-block stops being relocatable; set the base only where you
add_submap. - Assuming the default map is the one you want. A multi-map block drives whichever map you pass (or the default); be explicit when it matters.
8. Industry Best Practices
- Compose every sub-block's map with
add_submap. Nesting the block is not enough; nest the map to give its registers parent addresses. - One map per access path. Model every bus that can reach a block as its own map, so each path is independently verifiable.
- Keep sub-block offsets local and relocatable. A sub-block's internal offsets are fixed; its system position is set once, at the
add_submapbase. - Be explicit about which map an access uses. Pass the map for non-default buses; do not rely on the default when a block has several.
- Prefer generated hierarchies for large SoCs. Generation composes sub-block maps and multiple views consistently from a machine-readable spec (Chapter 13).
9. Interview / Review Questions
10. Key Takeaways
- A map is a view of a register set from one bus; a register is a single shared object, and each map is a different road to it.
- Multiple maps model a block reachable from several buses — one map per bus, each with its own base,
n_bytes,endian, sequencer, and adapter, all addressing the same registers; a frontdoor access picks its bus by its map. - Hierarchical maps compose a system address space:
add_submapmerges a sub-block's map into a parent at a base offset, and a register's address sums the base offsets down the tree plus its own offset. - Address composition makes a sub-block model relocatable — internal offsets stay local and fixed; the system position is set once, at
add_submap. - The signature bug is a missing
add_submap: the sub-block is nested but its map is not composed, so its registers work in isolation yet are unreachable from the parent — nesting the block is component structure, nesting the map is addressing.