Skip to content

UVM RAL · Chapter 12 · Advanced RAL

Multi-Instance IP

Real SoCs replicate IP: four identical UARTs, eight identical DMA engines, the same register block instantiated many times. This differs from a register array, which repeats registers within one block; here a whole block is replicated, so you need N distinct register-block instances, one per IP instance. Creating the instances is necessary but not enough, because each must be bound to its own copy of the hardware in two ways. Each instance needs a per-instance base address so frontdoor accesses reach the right region, and a per-instance HDL path root so a backdoor peek or poke reaches that instance's RTL hierarchy. The signature bug is not object aliasing but binding collapse: all instances share one root or base, so accesses to instances one through N resolve to instance zero's hardware and every instance looks like the first. This page shows correct per-instance bindings and breaks the shared-root bug.

Foundation12 min readUVM RALmulti-instancereplicated IPhdl_path rootbase address

Chapter 12 · Section 12.6 · Advanced RAL

1. Why Should I Learn This?

SoCs are full of replicated IP, and each instance needs its own register model bound to its own hardware — its own base address for the frontdoor and its own hdl_path root for the backdoor. Getting the per-instance bindings right is what keeps an access to instance N from silently landing on instance 0; miss the per-instance root and every instance looks like the first one, hiding real per-instance bugs behind a single instance's behaviour.

Learning multi-instance IP distinguishes block replication (this page) from register replication (12.3) and dynamic count (12.1), and it ties the per-instance hdl_path root back to backdoor path debugging (11.4) — the backdoor is only per-instance if its root is.

2. Industry Story — four UARTs that were all UART 0

A team instantiates four identical UARTs in an SoC and creates four distinct register-block instances for them — correctly, each its own object. Backdoor setup and checks seem to work. But subtle failures accumulate: configuring uart1 via backdoor does not affect uart1's behaviour, and a backdoor peek of uart2 returns values that track uart0.

Every instance was distinct as an object, but all four register blocks were given the same hdl_path root (tb.dut.uart0) — the per-instance root was never distinguished. So every instance's backdoor resolved to uart0's RTL: a poke to uart1 landed on uart0's flops, a peek of uart2 read uart0's. The four register models were four distinct objects bound to one instance's hardware. It looked like flaky per-instance behaviour, but the models were fine and the DUT was fine — the backdoor bindings had all collapsed onto uart0. Giving each block its own root (tb.dut.uart{i}) made each instance's backdoor reach its own RTL, and the per-instance behaviour resolved. The post-mortem lesson: replicated IP needs N distinct register-block instances, but distinct objects are not enough — each instance must be bound to its own hardware, with a per-instance base address (frontdoor) and a per-instance hdl_path root (backdoor); if the per-instance root is not distinguished, every instance's backdoor collapses onto instance 0's RTL, and all instances silently look like the first one.

3. Concept — N distinct blocks, each with its own base and its own hdl_path root

Multi-instance IP replicates a whole register block N times, and each instance must be bound to its own hardware:

  • N distinct block instances. The same register-block class is instantiated N times as N distinct objects (one per IP instance) — like the register-array discipline (12.3) but at the block level (a fresh create per instance, unique name).
  • Per-instance base address (frontdoor). Each instance's block is placed in the address map at its own base, so frontdoor accesses to instance i reach instance i's address region (base_i).
  • Per-instance hdl_path root (backdoor). Each instance's block gets its own root pointing at that instance's RTL hierarchy (tb.dut.uart0, tb.dut.uart1, …), so a backdoor peek/poke of instance i reaches instance i's flops (11.4).
  • The bug is a collapsed binding, not object aliasing. The block objects are distinct, but if the per-instance root (or base) is not distinguished, all instances share one binding and accesses to 1..N resolve to instance 0's hardware — every instance looks like instance 0.

Here is the SoC replicating an IP block, each instance with its own register block and its own root:

SoC with four UART instances, each with its own register block bound to its own base address and hdl_path rootuart0 reg_blockuart0 reg_blockbase 0x1000 · root tb.dut.uart0base 0x1000 · root tb.dut.uart0uart1 reg_blockuart1 reg_blockbase 0x2000 · root tb.dut.uart1base 0x2000 · root tb.dut.uart1uart2 reg_blockuart2 reg_blockbase 0x3000 · root tb.dut.uart2base 0x3000 · root tb.dut.uart2uart3 reg_blockuart3 reg_blockbase 0x4000 · root tb.dut.uart3base 0x4000 · root tb.dut.uart3soc_reg_envsoc_reg_envregister environmentregister environment
Figure 1 — multi-instance IP. The SoC instantiates the same IP block N times (uart0..uart3), and the register environment has N DISTINCT register-block instances, each bound to ITS OWN hardware: its own base address (frontdoor) and its own hdl_path root (backdoor, e.g. tb.dut.uart0 vs tb.dut.uart1). Distinct objects alone are not enough — if the per-instance root (or base) is shared, every instance's access collapses onto instance 0 (a backdoor peek of uart1 hits uart0). Each instance needs its OWN base AND its OWN root.

4. Mental Model — N models is only right if each is wired to its own copy of the hardware

5. Working Example — replicating a block with per-instance bindings

Create N distinct block instances, each with its own base and its own hdl_path root:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// N identical UARTs: N DISTINCT register-block instances, each BOUND to its own hardware.
class soc_reg_env extends uvm_reg_block;
  uart_reg_block uart[];
  virtual function void build();
    default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
    uart = new[4];
    foreach (uart[i]) begin
      uart[i] = uart_reg_block::type_id::create($sformatf("uart%0d", i));   // distinct object per instance (12.3)
      uart[i].configure(this);
      uart[i].build();
      // PER-INSTANCE BASE (frontdoor): each block at its OWN base in the map.
      default_map.add_submap(uart[i].default_map, 'h1000 * (i+1));           // uart0@0x1000, uart1@0x2000, ...
      // PER-INSTANCE hdl_path ROOT (backdoor): each block points at ITS OWN RTL hierarchy.
      uart[i].default_map.set_hdl_path_root($sformatf("tb.dut.uart%0d", i)); // tb.dut.uart0, tb.dut.uart1, ...
    end
  endfunction
endclass
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Because each instance has its OWN base AND root, accesses reach the intended instance's hardware:
uart[1].some_reg.write(s, 32'hCAFE);   // frontdoor -> uart1's address region (base 0x2000)
uart[2].some_reg.peek(s, v);           // backdoor  -> tb.dut.uart2's flops (root tb.dut.uart2)
// uart1's write does NOT touch uart0; uart2's peek reads uart2, not uart0.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The correctness hinge: BOTH bindings must be per-instance.
// per-instance base + per-instance root -> each access reaches its OWN instance.        CORRECT.
// shared base OR shared root             -> accesses to 1..N collapse onto instance 0.   BUG (next section).

Distinct objects plus per-instance base plus per-instance root make each UART model reach its own hardware. Sharing the root — the bug of the next section — collapses every instance's backdoor onto uart0.

6. Debugging Session — all instances sharing one hdl_path root

1

Distinct block instances all given the same hdl_path root make every instance's backdoor resolve to instance 0's RTL, so all instances silently look like instance 0

PER-INSTANCE hdl_path ROOT
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Four DISTINCT uart register-block instances — but all given the SAME hdl_path root:
foreach (uart[i]) begin
  uart[i] = uart_reg_block::type_id::create($sformatf("uart%0d", i));   // objects ARE distinct
  uart[i].configure(this); uart[i].build();
  default_map.add_submap(uart[i].default_map, 'h1000 * (i+1));           // per-instance base OK
  uart[i].default_map.set_hdl_path_root("tb.dut.uart0");                 // BUG: SAME root for ALL instances
end
// Every instance's BACKDOOR resolves to tb.dut.uart0 -> peek/poke of uart1..3 all hit uart0's RTL.
Symptom

The register-block objects are distinct and frontdoor accesses reach the right address regions, so it seems to work — but backdoor behaviour is subtly wrong: configuring uart1 via backdoor does not change uart1's behaviour, a backdoor peek of uart2 returns values that track uart0, and per-instance backdoor setup silently has no per-instance effect. It looks like flaky per-instance behaviour or intermittent DUT issues, but every backdoor access to any UART returns uart0's state. The tell: all instances' backdoor values are identical (they all read uart0), and per-instance backdoor pokes never take on the instance you aimed at.

Root Cause

The block objects are distinct (a fresh create per instance, 12.3-correct), and the frontdoor bindings are per-instance (each block at its own base), so frontdoor works. But the backdoor binding collapsed: every block was given the same hdl_path root (tb.dut.uart0), so every instance's backdoor resolves into uart0's RTL hierarchy. A peek of uart2 reads tb.dut.uart0's flops; a poke to uart1 writes tb.dut.uart0. The four register models are four distinct objects all bound, for backdoor, to one instance's hardware — so every instance looks like uart0 through the backdoor. This is not object aliasing (the objects are distinct, 12.3) and not a per-register hdl_path bug (each register's path is fine relative to the root); it is a per-instance binding collapse — the root, which should distinguish the instances, was left shared. Frontdoor masks the bug because its binding (base) was correctly per-instance; only the backdoor (root) collapsed, which is why the failure is quiet and per-instance-backdoor-specific.

Fix

Give each instance its own hdl_path root pointing at that instance's RTL hierarchy: uart[i].default_map.set_hdl_path_root($sformatf("tb.dut.uart%0d", i)), so uart{i}'s backdoor reaches tb.dut.uart{i}. After the fix, each instance's backdoor reads and writes its own flops, and the per-instance behaviour resolves. The rule the bug teaches: replicated IP needs N distinct block instances and per-instance bindings — each instance's own base address (frontdoor) and its own hdl_path root (backdoor); a shared root collapses every instance's backdoor onto instance 0, so all instances look like the first one. It is a binding bug, not object aliasing (12.3) or a per-register path bug (11.4): distinct objects whose per-instance address/path bindings were not distinguished. The tell: replicated instances whose backdoor values are all identical (all reading instance 0) while frontdoor works — check the per-instance root.

7. Common Mistakes

  • Sharing one hdl_path root across instances. Every instance's backdoor collapses onto instance 0's RTL — give each instance its own root (tb.dut.uart{i}).
  • Sharing one base address across instances. Frontdoor accesses collapse/overlap — place each instance's block at its own base.
  • Thinking distinct objects are sufficient. N distinct block objects still need per-instance bindings (base + root) or they all reach one instance's hardware.
  • Confusing block replication with register arrays. 12.3 repeats registers within a block; this repeats a whole block N times — different level.
  • Diagnosing collapsed bindings as flaky DUT behaviour. Identical per-instance backdoor values = a shared root, not the DUT or the models.

8. Industry Best Practices

  • Bind each instance to its own base and its own root. Per-instance base (frontdoor) and per-instance hdl_path root (backdoor) — both, for every instance.
  • Derive the root from the instance index. $sformatf("tb.dut.uart%0d", i) so each block reaches its own RTL hierarchy (11.4).
  • Create a distinct block object per instance. A fresh create with a unique name per IP instance (block-level 12.3 discipline).
  • Verify per-instance backdoor at bring-up. Poke distinct values to different instances and confirm each reads back its own — catches a shared root (11.4).
  • Diagnose identical-instance backdoor as a shared binding. All instances reading instance 0 is a collapsed root/base, not the DUT.

9. Interview / Review Questions

10. Key Takeaways

  • Multi-instance IP replicates a whole register block N times (four UARTs, eight DMAs) — needing N distinct uvm_reg_block instances — distinct from a register array (12.3), which repeats registers within one block.
  • Distinct block objects are necessary but not sufficient: each instance must be bound to its own hardware via two per-instance bindings — a per-instance base address (frontdoor) and a per-instance hdl_path root (backdoor).
  • The signature bug is a collapsed binding (not object aliasing, 12.3): distinct objects whose per-instance root (or base) is not distinguished, so accesses to instances 1..N resolve to instance 0's hardware — every instance looks like the first one.
  • A shared root is quieter and more dangerous than a shared base: the frontdoor still works while the backdoor silently collapses onto instance 0 (plausible wrong values), hiding per-instance bugs and silently mis-directing per-instance setup — the 'resolves to the wrong place' hazard (11.4) at the instance level.
  • Bind each instance to its own base and its own root (derive both from the instance index), and verify per-instance backdoor at bring-up with distinct-value pokes — identical backdoor values across instances is the tell of a shared root, not the DUT or the models.