UVM RAL · Chapter 12 · Advanced RAL
Register Arrays
A register array is an array of real, identical register instances: the same register repeated many times, such as sixteen channel-configuration registers, an interrupt-enable register per source, or a bank of identical timers. Unlike a virtual register, which is a view over memory with no storage of its own, each element here is a genuine, separately-instanced register with its own storage, its own mirror, and its own address, sharing only the field layout. You build one in a loop: for each index, create a new instance, configure it, and add it to the map at its own offset. That word new is the whole subject, because the signature bug is rooted in the fact that SystemVerilog class handles are references. If the loop stores the same handle in every slot, all elements alias one object and share one mirror, so writing one silently changes another. This lesson breaks that aliasing bug caused by creating the instance outside the loop.
Foundation12 min readUVM RALregister arraysinstancesaliasinghandles
Chapter 12 · Section 12.3 · Advanced RAL
1. Why Should I Learn This?
Repeated identical registers — per-channel, per-source, per-entry — are everywhere, and a register array is how you model them without hand-writing N copies. But the loop that builds them hides a SystemVerilog trap: forget to create a fresh instance each iteration and all N elements become the same register, sharing one mirror, so writes to one silently appear on all. Knowing to create a new instance per element (and add each at its own offset) is what makes a register array N distinct registers instead of one register with N names.
Learning register arrays complements dynamic models (12.1, which decides how many) and virtual registers (12.2, a view over memory): a register array is N real, distinct instances, and its correctness hinges on the SystemVerilog reference-semantics discipline of a fresh create per element.
2. Industry Story — the sixteen registers that were one
A team models sixteen identical channel-config registers as a register array, built in a loop. But create was called once, outside the loop, and the same handle was stored in all sixteen array slots. Channel 0 tests pass. Then things get strange: configuring channel 3 also changes what channel 5 reads back; the sixteen 'registers' behave as if they are one.
They were one. Because a class handle is a reference, storing the same created object in all sixteen slots made array[0] through array[15] alias a single uvm_reg instance — one object, sixteen names. That single object had one mirror, so writing array[3] and reading array[5] touched the same mirrored value; and the map had one register, not sixteen. It looked like a bizarre cross-coupling bug, but there was no coupling — there was one register pretending to be sixteen. The fix was to move create inside the loop with a unique name per iteration, so each element was a distinct instance with its own storage, mirror, and offset. The post-mortem lesson: class handles are references, so a register array must create a fresh instance for each element inside the loop — reusing one handle across all slots makes every element alias the same register (one shared mirror, one identity), so a write to one appears on all; it is not an offset or coupling bug, it is N array slots pointing at one object, and the fix is a per-element create with a unique name, each added at its own offset.
3. Concept — N distinct instances, a fresh create per element
A register array is N real, distinct register instances that share a field layout. Building it correctly:
- Each element is its own instance. Every
array[i]is a separateuvm_regwith its own storage, mirror, and address — they share only the class (field layout), not the object. - Create a fresh instance per element, inside the loop. For each
i:createa new instance with a unique name (e.g.$sformatf("cfg%0d", i)),configure/buildit, andadd_regit to the map at its own offset (base + i * stride). The unique name and the per-iterationcreateare what make the elements distinct. - The trap is SystemVerilog reference semantics. A class handle is a reference; assigning one created object to all slots (or
createoutside the loop) makes every slot alias the same object. Then N 'registers' are one register with N names — one mirror, one identity — so a write to one appears on all. - Aliasing is an object bug, not an offset bug. The elements are the same object; even distinct offsets cannot separate them, because there is only one register underneath.
Here is what aliasing does: a write to array[3] shows up on array[5] because they are the same object with one mirror:
4. Mental Model — N slots must hold N objects, not N copies of one reference
5. Working Example — building an array of distinct instances
Create a fresh instance per element inside the loop, each added at its own offset:
// A register array: N identical channel-config registers, each a DISTINCT instance.
class dma_reg_block extends uvm_reg_block;
cfg_reg ch_cfg[]; // an array of HANDLES — N labels, need N objects
virtual function void build();
default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
ch_cfg = new[16];
foreach (ch_cfg[i]) begin
// CREATE A FRESH INSTANCE per element, with a UNIQUE NAME — this is what makes them distinct:
ch_cfg[i] = cfg_reg::type_id::create($sformatf("ch_cfg%0d", i)); // fresh object each iteration
ch_cfg[i].configure(this);
ch_cfg[i].build();
default_map.add_reg(ch_cfg[i], 'h100 + i*'h4); // its OWN offset (base + i*stride)
end
endfunction
endclass// Because each element is a DISTINCT object, they have INDEPENDENT mirrors and addresses:
uvm_status_e s; uvm_reg_data_t v5;
ch_cfg[3].write(s, 32'h0000_000F); // writes ONLY channel 3's register/mirror
ch_cfg[5].read(s, v5); // reads channel 5's OWN value — unaffected by channel 3
// v5 reflects channel 5 alone, because ch_cfg[3] and ch_cfg[5] are SEPARATE uvm_reg objects.// The correctness hinge: create() is INSIDE the loop with a UNIQUE name per element.
// create inside loop + unique name -> N distinct instances (N mirrors, N offsets). CORRECT.
// create OUTSIDE loop / one handle in all slots -> N labels aliasing ONE object. BUG (next section).Creating a fresh instance each iteration makes the sixteen elements sixteen distinct registers with independent mirrors and offsets. Creating one instance and reusing its handle — the bug of the next section — makes them one register with sixteen names.
6. Debugging Session — every array element aliasing one register
Creating one instance outside the loop and storing its handle in every slot makes all array elements alias the same register, so a write to one appears on all
FRESH create PER ELEMENT// create() is OUTSIDE the loop; the SAME handle is stored in every array slot:
cfg_reg one = cfg_reg::type_id::create("ch_cfg"); // ONE instance created, ONCE
one.configure(this); one.build();
ch_cfg = new[16];
foreach (ch_cfg[i]) begin
ch_cfg[i] = one; // BUG: every slot holds the SAME object (a reference)
default_map.add_reg(ch_cfg[i], 'h100 + i*'h4); // adds the SAME register 16 times
end
// ch_cfg[0..15] all ALIAS 'one' -> ONE mirror, ONE identity. Writing ch_cfg[3] changes ch_cfg[5].Channel 0 tests pass, but the sixteen 'registers' behave as one: configuring channel 3 also changes what channel 5 reads back, and every element seems to hold the same value regardless of which one you write. It looks like a bizarre cross-coupling between channels — as if the hardware were wiring them together — but the DUT is fine. The tell is that any write to any element appears on all elements: not partial coupling, but complete sharing, because they are literally the same object.
SystemVerilog class handles are references, and here a single uvm_reg instance (one) was created outside the loop and its handle stored in every array slot. So ch_cfg[0] through ch_cfg[15] do not name sixteen registers — they are sixteen references to the same object. That one object has one mirror and one identity, so writing ch_cfg[3] updates the mirror and reading ch_cfg[5] reads the same mirror — they are indistinguishable because there is only one register underneath. Adding ch_cfg[i] to the map sixteen times added the same register at sixteen offsets (aliasing at the map level too). This is not an offset bug (distinct offsets cannot separate one object) and not hardware coupling (the DUT has sixteen real registers); it is aliasing — N array slots pointing at one instance because create was called once instead of per element. Channel 0 'passed' only because, with one shared object, any single-element test is self-consistent; the aliasing shows only when two different elements are used and found to share state.
Create a fresh instance for each element inside the loop, with a unique name: ch_cfg[i] = cfg_reg::type_id::create($sformatf("ch_cfg%0d", i)), then configure/build and add each at its own offset. Now each element is a distinct uvm_reg with its own storage, mirror, and address, and writing ch_cfg[3] no longer touches ch_cfg[5]. The rule the bug teaches: a register array is N distinct instances, so you must create a fresh instance for each element (unique name, inside the loop) — because class handles are references, reusing one handle across all slots makes every element alias the same register (one shared mirror, one identity), so a write to one appears on all. It is an object-identity bug, not an offset or coupling bug: when array elements cross-affect each other completely, count how many times create was called — the fix is a per-element create, not a change to the offsets or the DUT.
7. Common Mistakes
- Calling
createonce outside the loop. Storing that one handle in every slot aliases all elements to one register —createa fresh instance per element, inside the loop. - Reusing one handle across the array. Class handles are references; one object in N slots is one register with N names, sharing one mirror.
- Duplicate or missing element names. Each instance needs a unique name (e.g. index-suffixed) — non-unique names cause factory/lookup collisions.
- Adding all elements at the same offset. Each element needs its own offset (
base + i * stride) — even distinct objects at one offset collide in the map. - Reading complete cross-affecting as coupling. If a write to any element appears on all, it is aliasing (one object), not hardware coupling — check the
create.
8. Industry Best Practices
- Create a fresh instance per element, inside the loop. Unique name each iteration (
$sformatf(...)) so eacharray[i]is a distinct object. - Add each element at its own offset.
base + i * stride, so the N distinct registers occupy N distinct addresses. - Model repeated identical registers as arrays. Per-channel/per-source/per-entry registers — build them in a loop rather than hand-writing N copies.
- Diagnose element cross-affecting as aliasing. Complete sharing across elements points at a shared instance (one
create), not coupling or offsets. - Keep element names unique and index-derived. So instances are distinguishable in the factory, hierarchy, and reports.
9. Interview / Review Questions
10. Key Takeaways
- A register array is an array of real, distinct register instances — the same register repeated N times (per-channel/per-source/per-entry) — each with its own storage, mirror, and address; unlike a virtual register (12.2), which is a view over memory.
- Build it in a loop that, for each element, creates a fresh instance with a unique name, configures/builds it, and adds it at its own offset (
base + i * stride). - The signature bug is aliasing from SystemVerilog reference semantics:
createonce (outside the loop) and store that one handle in every slot, and all elements reference the same object — one mirror, one identity — so a write to one appears on all. - Aliasing is an object-identity bug, not an offset bug (distinct offsets can't separate one object) or hardware coupling (the DUT has N real registers) — its tell is complete cross-affecting (write any element, all change).
- A single-element test passes even when aliased (one shared object is self-consistent) — the definitive check is writing distinct values to different elements and confirming each reads back independently; the fix is a per-element fresh
create, not offsets or the DUT.