Skip to content

UVM RAL · Chapter 12 · Advanced RAL

Dynamic Register Models

Most register models are static: the block, its registers, fields, and map are fixed in build and never change. But real IP is often reconfigurable or parameterized, like an N-channel DMA where N is a build parameter, and a static model cannot follow that. A dynamic register model decides its structure at build time from a configuration object: how many instances exist, which registers are present, and how the map is laid out are all computed from config rather than hardcoded. You still build once from that config and then lock the model, because dynamic does not mean mutable after lock. This lesson explains how to build structure from config and breaks the signature bug: a model built from a different config than the DUT actually uses, so registers are missing or wrong and accesses fail systemically.

Foundation12 min readUVM RALdynamic modelconfigurationparameterizedstructure

Chapter 12 · Section 12.1 · Advanced RAL

1. Why Should I Learn This?

Reconfigurable and parameterized IP is everywhere, and a static register model simply cannot represent a block whose register set changes with configuration. Knowing how to build a dynamic model — structure computed from config, then locked — and, above all, to build it from the same config the DUT uses, is what lets your register model track the actual hardware instead of a fixed guess that silently mismatches.

Learning dynamic register models opens the advanced chapter and underpins several of its topics — register arrays (12.3), multi-instance IP (12.6), and multi-bus SoC RAL (12.7) all rest on building structure from configuration. It also sharpens the lifecycle discipline (build-then-lock) from a static model into a config-driven one.

2. Industry Story — the model built for the wrong configuration

A team verifies a configurable N-channel DMA. The DUT is instantiated in 4-channel mode for this SoC, but the register model was built from its default (1-channel) configuration — nobody wired the same config into the model build. Channel 0 works perfectly; every test that touches channel 0 passes.

Then tests for channels 1, 2, and 3 fail systemically: accesses to those channels' registers resolve to the wrong registers or to nothing, because in the model those registers do not exist — it was built for one channel. It looked like a swarm of addressing bugs, but there was no addressing bug: the model's structure simply did not match the DUT's structure, because it was built from a different configuration. The registers for channels 1–3 were absent from the model while present in the DUT, so the model could not reach them. The fix was not in any register or the map arithmetic; it was to build the model from the same 4-channel config the DUT used, so the structure matched. The post-mortem lesson: a dynamic register model must be built from the same configuration the DUT actually uses — building it from a different config produces a structural mismatch (wrong register count, absent registers, wrong map layout) that makes accesses resolve to the wrong or absent registers systemically; it is not a per-register or addressing bug, it is the model's shape not matching the hardware's, and the fix is a single shared config, not register-by-register patching.

3. Concept — structure from config, built once, then locked

A dynamic register model computes its structure from configuration at build time. The discipline:

  • Structure from config, not hardcoded. In build(), read the config (channel count, feature flags, sizes) and construct exactly the registers, fields, and map the configuration implies — loop to create N channels' registers, conditionally include feature registers, lay out the map by the configured sizes.
  • Build from the same config the DUT uses. The model's config must be the config the DUT was instantiated with — share one configuration source between DUT and model, or the model's structure will not match the hardware.
  • Build once, then lock. Dynamic decides structure at build time from config; after that, lock_model() freezes it exactly as for a static model. Dynamic does not mean mutable after lock — you do not add/remove registers at runtime; you configured the structure before locking.
  • The failure is structural, not per-register. A config mismatch makes the model's shape wrong (register count/offsets/which registers exist), so accesses resolve to wrong or absent registers systemically — distinct from a single-register or map-arithmetic bug.

Here is static versus dynamic build, and the mismatch path:

Static vs dynamic build: static fixed; dynamic reads config, builds matching structure, locks; mismatch = model built from wrong configsame config as DUTwrong configSTATIC: build()-> FIXEDstructure ->lockDYNAMIC: build()READS config(channels,flags, sizes)construct MATCHING structure (N channel regs, conditional regs, config-sized map)construct MATCHINGstructure (N channelregs, conditionalregs, config-sized…LOCK the configuredstructure (NOTmutable after lock)MISMATCH: model built from a DIFFERENT config than the DUT -> STRUCTURE driftsMISMATCH:model builtfrom aDIFFERENT…accesses resolve to WRONG/ABSENT registers systemically -> fix = one SHARED configaccesses resolveto WRONG/ABSENTregisterssystemically ->…
Figure 1 — static vs dynamic register-model build. STATIC: build() constructs a FIXED structure every time. DYNAMIC: build() READS the config (channel count, feature flags) and constructs a MATCHING structure (N channels' registers, conditional feature registers, config-sized map) — then, like static, LOCKS it (dynamic is not mutable-after-lock). The MISMATCH path is the signature bug: the model is built from a DIFFERENT config than the DUT uses, so its STRUCTURE drifts from the hardware (absent/extra registers, wrong offsets) and accesses resolve to the wrong or absent registers systemically. The fix is one SHARED config between DUT and model.

4. Mental Model — the model is built to spec, and the spec is the config the DUT used

5. Working Example — building structure from a shared config

Build the model's structure from the same config the DUT uses, then lock it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A dynamic block: the number of channel register-blocks is READ FROM CONFIG, not hardcoded.
class dma_reg_block extends uvm_reg_block;
  dma_cfg cfg;                       // the SHARED config — same object the DUT was built from
  channel_reg_block channels[];      // dynamic: count decided by cfg
 
  virtual function void build();
    default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
    channels = new[cfg.num_channels];                         // STRUCTURE from config
    foreach (channels[i]) begin
      channels[i] = channel_reg_block::type_id::create($sformatf("ch%0d", i));
      channels[i].configure(this);
      channels[i].build();
      default_map.add_submap(channels[i].default_map, 'h1000 * i);  // config-sized layout
    end
    if (cfg.has_error_regs) build_error_registers();          // CONDITIONAL registers from a flag
  endfunction
endclass
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Build the model from the SAME config the DUT uses, then LOCK (build once; not mutable after lock).
dma_reg_block reg_model = dma_reg_block::type_id::create("reg_model");
reg_model.cfg = env_cfg.dma_cfg;   // <-- the SAME config that instantiated the DUT (shared source)
reg_model.build();                 // structure now MATCHES the DUT's configuration
reg_model.lock_model();            // freeze the configured structure (dynamic != mutable-after-lock)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The correctness hinge: reg_model.cfg MUST equal the DUT's config. If they differ, the STRUCTURE
// mismatches (absent/extra channels, wrong offsets) and accesses resolve to wrong/absent registers.
if (reg_model.cfg.num_channels != dut_cfg.num_channels)
  `uvm_fatal("DYNRAL", "model config != DUT config -> STRUCTURAL mismatch (build from ONE shared config)")

Building from the shared config makes the model's structure match the DUT; locking freezes it. Building from a different config — the bug of the next section — silently produces a mismatched structure.

6. Debugging Session — a model built from the wrong configuration

1

A dynamic model built from the default config while the DUT runs a 4-channel config has a structure that does not match the hardware, so channels 1-3 registers are absent or wrong

BUILD FROM THE SAME CONFIG AS THE DUT
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The DUT is instantiated in 4-channel mode, but the model is built from the DEFAULT (1-channel) config:
dma_reg_block reg_model = dma_reg_block::type_id::create("reg_model");
// BUG: reg_model.cfg left at default (num_channels = 1) — NOT the DUT's 4-channel config.
reg_model.build();      // builds ONLY channel 0's registers — channels 1-3 DO NOT EXIST in the model
reg_model.lock_model();
// Accesses to channels 1-3 resolve to nothing / the wrong register -> systemic failures on those channels.
Symptom

Channel 0 works perfectly — every test touching it passes. But all tests for channels 1, 2, and 3 fail systemically: accesses to those channels' registers resolve to nothing or to the wrong register. It looks like a swarm of addressing bugs on the upper channels, but each register, examined individually, seems fine — there is no single register or offset to blame. The pattern (channel 0 fine, channels 1–3 all broken) is the tell: it tracks the configuration difference (1 channel modelled vs 4 in the DUT), not any register.

Root Cause

A structural config mismatch. The DUT was instantiated in 4-channel mode, but the model was built from the default 1-channel config (reg_model.cfg was never set to the DUT's config). Because the model's structure is computed from its config, building from a 1-channel config produced a model with only channel 0's registers — the registers for channels 1–3 do not exist in the model at all, even though they exist in the DUT. So accesses to channels 1–3 resolve to nothing (absent registers) or to the wrong place, systemically across those channels. This is not a per-register bug, an offset bug, or a map-arithmetic bug (3.x) — every modelled register and offset is internally consistent for a 1-channel model; the problem is that the model is the wrong shape because it was built from the wrong configuration. The build 'succeeded' silently because a 1-channel model is a perfectly valid structure — just not the one the DUT has. The mismatch is between the model's config and the DUT's config, and it manifests as the model's structure not matching the hardware's.

Fix

Build the model from the same config the DUT was instantiated with — set reg_model.cfg to the DUT's 4-channel config (ideally the one shared config object that also built the DUT), then build() and lock_model(); the model now has channels 0–3 matching the hardware, and the systemic channel-1–3 failures vanish together. Add an assertion that the model's config equals the DUT's config at bring-up (a structural gate). The rule the bug teaches: a dynamic register model must be built from the same configuration the DUT actually uses — from a different config it builds a structurally mismatched model (absent/extra registers, wrong layout) whose accesses resolve to the wrong or absent registers systemically; it is a structure mismatch, not a per-register or addressing bug, and the fix is one shared config between DUT and model, not register-by-register patching. The tell is a failure pattern that tracks a configuration axis (a whole channel/feature broken) rather than a single register.

7. Common Mistakes

  • Building the model from a different config than the DUT. Produces a structural mismatch (absent/extra registers, wrong layout) — share one config between DUT and model.
  • Hardcoding structure that should be config-driven. A reconfigurable IP needs structure computed from config, not a fixed register set that only matches one configuration.
  • Thinking dynamic means mutable after lock. You decide structure from config at build time, then lock_model() freezes it — never add/remove registers at runtime.
  • Reading a config mismatch as addressing bugs. A whole channel/feature failing systemically is a structural mismatch, not many per-register bugs (3.x).
  • No structural gate. Not asserting model-config == DUT-config lets a silent structural mismatch through bring-up.

8. Industry Best Practices

  • Share one config source between DUT and model. The model's structure must be built from the same configuration the DUT was instantiated with.
  • Compute structure from config in build(). Loop for N instances, conditionally include feature registers, lay out the map by configured sizes.
  • Build once, then lock. Decide structure from config at build time; freeze with lock_model() — dynamic is not runtime-mutable.
  • Gate on model-config == DUT-config. Assert the configurations match at bring-up so a structural mismatch fails loudly, not as scattered access failures.
  • Diagnose config-axis failure patterns as structural. A whole channel/feature broken points at a config mismatch, not per-register bugs.

9. Interview / Review Questions

10. Key Takeaways

  • A dynamic register model decides its structure from configuration at build time — instance count, which registers exist, and map layout are read from a config object and built to match, so one model definition serves every configuration of a reconfigurable/parameterized IP.
  • 'Dynamic' means structure-from-config, not mutable-after-lock — you compute the structure from config at build time, then lock_model() freezes it exactly as for a static model; you never add/remove registers at runtime.
  • The model must be built from the same config the DUT actually uses — share one config source between DUT and model, or the structure drifts silently.
  • The signature bug is a structural config mismatch: the model built from a different config than the DUT has the wrong shape (absent/extra registers, wrong offsets), so accesses resolve to wrong/absent registers systemicallynot a per-register or addressing bug (3.x).
  • The tell is a failure pattern aligned to a configuration axis (a whole channel/feature broken while another works) — diagnose it as structural, fix it with one shared config (not register-by-register patching), and add a model-config == DUT-config gate at bring-up.