UVM
Hierarchical Configuration
Bundling settings into configuration objects and nesting them into a config tree that mirrors the component tree — the top-down build-phase cascade where each level gets its config and relays each child's slice.
Configuration Database · Module 7 · Page 7.4
The Engineering Problem
The last two chapters covered the config DB mechanics: the set/get API (Module 7.2) and how matching and precedence resolve a get (Module 7.3). Those are the primitives. This chapter is about organizing configuration for a real, multi-level environment — an env with several agents, each with a driver, monitor, and sequencer — without drowning in scattered set calls.
The naïve approach sets every scalar individually: a set for each agent's is_active, each driver's virtual interface, each monitor's mode, each address range — dozens of string-keyed sets, each a chance for a field typo or type mismatch (Module 7.2's silent failures). It doesn't scale and it doesn't cohere. The disciplined approach is hierarchical configuration: bundle each component's related settings into a configuration object (a uvm_object subclass), and nest those objects into a configuration tree that mirrors the component tree — an env_config holding per-agent agent_config objects, each holding the pieces its driver and monitor need. The test populates this tree at the top and sets it once; then, as build_phase cascades top-down, each level gets its own config object and relays each child's slice down to it. Configuration flows down the hierarchy as a structured tree, one typed object per level, instead of a flat scatter of strings. This chapter is that pattern: config objects, the nested tree, and the build-phase cascade that distributes it.
How do you organize configuration for a multi-level environment — bundling settings into configuration objects, nesting them into a tree that mirrors the component hierarchy, and cascading them top-down so each level gets its config and relays each child's slice?
Motivation — why bundle and nest
Hierarchical configuration solves two problems at once — the fragility of scattered scalars and the incoherence of flat configuration — and each property of the pattern addresses one:
- Config objects replace many fragile string-keyed sets with one typed set. Every scalar
setis a string field name and a type that must match thegetexactly (Module 7.2), and each is an independent chance for a silent typo. Bundling an agent's settings into oneagent_configobject means one set/get of one type — the fields inside are real, compiler-checked members, not strings. Fewer keys, fewer silent mismatches. - Cohesion: related settings travel together. An agent's
is_active, its virtual interface, its address range, and its mode are one decision, not four independent ones. A config object keeps them together, so configuring an agent is configuring one object, and you can't half-configure it (set three of four scalars and forget the fourth). - Nesting mirrors the hierarchy, so configuration is structured like the thing it configures. A multi-agent env is a tree; its configuration should be a tree too. An
env_configcontainingagent_configobjects containing driver pieces parallels env → agents → drivers, so the configuration is navigable and obvious — you find an agent's config where you'd expect it, inside the env's config. - The cascade keeps each level responsible for its children. Rather than the test reaching deep into every leaf, each level gets its own config and relays each child's slice — so the env decides how to distribute to its agents, the agent to its driver. Responsibility is local, which is what makes the env reusable (it doesn't depend on the test knowing its internals).
- One set at the top, not dozens spread out. The test populates the whole tree and sets the top object once; distribution happens through the cascade. The test's configuration code is one coherent block building one tree, not a sprawl of individual sets.
The motivation, in one line: bundling settings into typed objects removes the fragility of scattered string sets, and nesting them into a tree that mirrors the hierarchy — distributed by a top-down cascade — makes configuration coherent, navigable, and local to each level.
Mental Model
Hold hierarchical configuration as nested briefing documents passed down an org chart:
Configuration is a master brief containing department briefs containing team briefs — and each manager receives their brief and hands each report the relevant sub-brief. The test is the executive who writes the master brief (the
env_config). Inside it are department briefs (oneagent_configper agent), and inside each of those are the specifics each team needs (the driver's interface, the monitor's mode). The executive hands the whole master brief to the env. The env reads its brief, and for each agent it tears off that agent's department brief and hands it down. Each agent reads its brief, and for its driver it hands down the driver's piece. So the brief cascades: each level receives one coherent document addressed to it, extracts the sub-documents for its reports, and passes those down — nobody reaches across the org to brief someone three levels away, and nobody receives a pile of loose memos. The shape of the briefing tree mirrors the shape of the org: you find the agent's brief inside the env's brief, exactly where the org chart says it should be.
So configuring a multi-level env is writing one nested brief at the top and letting it cascade: the test builds the tree, sets the top, and each level relays its children's slices. The contrast is the naïve world of loose memos — a separate scalar set for every setting of every component — which is both fragile (easy to misaddress) and incoherent (no structure mirroring the org).
Visual Explanation — the config tree mirrors the component tree
The defining idea is parallel structure: the configuration objects nest in the same shape as the components they configure. Seeing the two trees side by side makes the pattern click.
The figure shows the two trees and the correspondence between them. On the left is the configuration tree: env_config is the top object, and it contains (holds handles to) an agent_config for each agent, and each agent_config in turn contains the pieces its driver and monitor need (a driver config object, a virtual interface). On the right is the component tree: env builds its agents, each agent builds its driver and monitor. The horizontal "configures" links show the correspondence — each config object is the typed brief for the component at the same position: env_config configures the env, agent0_config configures agent0, the driver piece configures the driver. The power of this parallel structure is navigability and locality: an agent's configuration lives inside the env's configuration, exactly where you'd look, and each component reads one object — its own config — rather than fishing individual scalars out of a flat namespace. The test's job is to build this entire left-hand tree (create every object, populate every field, link the nesting) and set the top env_config once; the cascade then walks it down the right-hand tree. Configuration becomes a structured mirror of the design, not a scatter of keys.
RTL / Simulation Perspective — config objects and the cascade in code
Two things make hierarchical configuration concrete: the config object classes (with nested handles) and the cascade in each level's build_phase that gets its config and relays each child's slice.
// ── CONFIG OBJECTS: typed briefs, nested to mirror the hierarchy ──
class agent_config extends uvm_object;
`uvm_object_utils(agent_config)
uvm_active_passive_enum is_active = UVM_ACTIVE;
virtual my_if vif; // the driver's piece, held here
function new(string name="agent_config"); super.new(name); endfunction
endclass
class env_config extends uvm_object;
`uvm_object_utils(env_config)
agent_config agent0_cfg; // NESTED: one per agent
agent_config agent1_cfg;
function new(string name="env_config"); super.new(name); endfunction
endclass
// ── TEST: build & populate the WHOLE tree, set the top once ──
function void my_test::build_phase(uvm_phase phase);
env_config ecfg = env_config::type_id::create("ecfg");
ecfg.agent0_cfg = agent_config::type_id::create("a0cfg"); // create nested objects
ecfg.agent1_cfg = agent_config::type_id::create("a1cfg");
ecfg.agent0_cfg.vif = tb_top.if0; ecfg.agent1_cfg.vif = tb_top.if1; // populate
uvm_config_db#(env_config)::set(this, "env", "cfg", ecfg); // ONE set at the top
endfunction
// ── ENV: get its config, RELAY each child's nested slice ──
function void my_env::build_phase(uvm_phase phase);
if (!uvm_config_db#(env_config)::get(this, "", "cfg", ecfg))
`uvm_fatal("NOCFG", "env_config not set")
uvm_config_db#(agent_config)::set(this, "agent0", "cfg", ecfg.agent0_cfg); // relay down
uvm_config_db#(agent_config)::set(this, "agent1", "cfg", ecfg.agent1_cfg);
agent0 = my_agent::type_id::create("agent0", this); // then build children
agent1 = my_agent::type_id::create("agent1", this);
endfunctionThe code shows both halves. The config objects are uvm_object subclasses (registered with uvm_object_utils, Module 4.3): agent_config bundles an agent's settings, and env_config nests an agent_config handle per agent — the nesting is just member handles, which is what makes the config tree a tree. The cascade is the pattern in each build_phase. The test creates the entire tree (the env_config and every nested agent_config), populates the fields (here, each agent's vif), and does a single set of the top env_config at the env's scope. The env then gets its env_config, and relays: it sets each agent's nested agent_config (reached as ecfg.agent0_cfg) at that agent's scope, before creating the agents — so when each agent's build_phase runs and gets its agent_config, the relayed slice is already in the database. The agent would continue the cascade (relaying the driver's piece, or passing the vif down). Notice the shape repeats at every level: get my config → set each child's slice → build children. That repeated triple is the cascade, and it's what carries the config tree down the component tree.
Verification Perspective — why one config object beats scattered scalars
The bundling decision — one config object versus many individual scalar sets — is the choice that makes configuration scale. Seeing the two approaches side by side shows why bundling wins on every axis that matters in a large testbench.
The comparison is decisive in a real testbench. With scattered scalars (left), each of an agent's settings is its own set/get pair with its own string key and type — is_active, vif, mode, an address range, and so on. Every one of those keys is an independent opportunity for the silent failures of Module 7.2 (a typo'd field name, a mismatched type, a missing check), and there's no cohesion: nothing ties the four settings together, so it's easy to configure three and forget the fourth, and the configuration is spread across many sites. With a bundled config object (right), the agent's settings are members of one agent_config, set and got as a single typed entry. The number of string keys drops from four (or forty) to one; the fields become real, compiler-checked members accessed by dot notation rather than string lookup; and extending the configuration means adding a member to the class — automatically available everywhere the object is passed — rather than threading a new set/get pair through every level. The bundled approach also composes: an agent_config can itself nest a driver_config, which is exactly how the tree is built. For anything beyond a trivial environment, bundling isn't a stylistic preference — it's what keeps configuration correct and maintainable as the component count grows.
Runtime / Execution Flow — the build-phase cascade, top-down
The cascade is a repeated pattern that walks down the hierarchy during build_phase: each level gets its config, relays its children's slices, and builds its children — so configuration arrives at each component just before it's needed.
The cascade is one pattern applied at every level, and its correctness rests on top-down build order (Module 5.2). It starts with the test building the whole config tree and setting the top env_config. Then the env runs the triple: get its env_config, relay each agent's nested agent_config by setting it at the agent's scope, and build the agents. Because the env relays before it builds the agents — and because build is top-down — each agent's relayed config is already in the database when the agent's own build_phase runs. The agent runs the same triple: get its agent_config, relay the driver's piece, build the driver. The driver completes the chain: it gets its piece, which the agent relayed moments earlier. The repeated triple — get my config, set each child's slice, build children — is the entire mechanism, and it scales to any depth: every component is responsible only for receiving its own config and handing its direct children theirs. The top-down phase order is what makes the relays line up: a parent always relays before the child gets, so the cascade never races. This locality is also what keeps components reusable — each knows only its own config type and its children's, never the whole tree.
Waveform Perspective — configuration cascading down at build time
The cascade is a sequence of zero-time build events marching down the hierarchy. The timeline shows the relays firing top-down, each child's get following its parent's set, all before the run.
The config cascade: each level's relay precedes the child's get, top-down at build time
11 cyclesThe waveform shows the cascade as a clean top-down march. The test_set pulse comes first (the test publishing the top object), then env_relay (the env getting its config and relaying agent slices), then agent_relay (the agent getting its slice and relaying the driver's piece), then drv_get (the driver receiving its piece) — each event one step lower in the hierarchy than the last, and each child's receipt following its parent's relay because build is top-down. Once drv_get completes, configured goes high and stays high: the entire config tree has been distributed and every component holds its own config. All of this happens in the zero-time build region — there's no pin activity, because the cascade is moving configuration objects through the database, not driving signals. Only after the cascade completes does run_phase begin and the configured components start behaving. The picture to carry is this staircase: configuration entering at the top and stepping down level by level during build, so that when behaviour starts, every component — from env to driver — already holds the brief addressed to it.
DebugLab — the broken relay in the cascade
A subtree that came up unconfigured because one level forgot to relay
In a two-agent env, agent0 worked perfectly but agent1 came up completely unconfigured — its driver's virtual interface was null and it never drove anything. The test had clearly populated both agents' configuration: env_config was created with both agent0_cfg and agent1_cfg fully filled in, and the single top-level set was correct. Yet only one agent's configuration reached its destination. The failure was confined to one subtree while the rest of the env was fine.
The env relayed agent0's slice but forgot to relay agent1's — so agent1's get found nothing and returned 0:
test: ecfg.agent0_cfg = ...(populated); ecfg.agent1_cfg = ...(populated); // BOTH filled in
uvm_config_db#(env_config)::set(this, "env", "cfg", ecfg); // top set correct
env build_phase:
get(this, "", "cfg", ecfg); // env got its config ✓
uvm_config_db#(agent_config)::set(this, "agent0", "cfg", ecfg.agent0_cfg); // relayed agent0 ✓
// ✗ MISSING: set(this, "agent1", "cfg", ecfg.agent1_cfg); ← the relay for agent1
agent1 build_phase:
if (!get(this, "", "cfg", a1cfg)) `uvm_fatal(...) // get returns 0 → (if checked) fatal
// or (if unchecked) a1cfg null → crash later
result: the config tree had agent1's brief, but the env never handed it down → agent1 unconfigured
fix: relay EVERY child's slice — add the missing set(this, "agent1", "cfg", ecfg.agent1_cfg);This is the cascade-specific failure: a break in the relay chain. The configuration was complete — the test built the full tree, including agent1's populated agent_config — but hierarchical distribution depends on every level relaying every child's slice, and the env's build_phase was missing the one set for agent1. So agent1_cfg sat inside ecfg (the env held it the whole time, as ecfg.agent1_cfg), but it was never put into the database at agent1's scope, so agent1's get had nothing to find and returned 0. The tell that distinguishes this from a top-level mistake is that one subtree fails while siblings succeed: a missing top set would starve everything, but a missing relay starves only the child (and its descendants) below the break. The fix is to relay every child — restore the set for agent1 — and, more durably, to generate the relays in a loop over the children so none can be forgotten.
The tell is one subtree unconfigured while siblings are fine. Diagnose broken cascades:
- Localize the break by scope. Identify the shallowest component that's unconfigured. Its parent is where the relay is missing — the parent got its config but didn't
setthis child's slice. - Confirm the parent received its config. Check that the parent's own
getsucceeded (it has the nested slice in hand). If the parent is configured but the child isn't, the break is the relay between them. - Check that the slice exists in the tree. Confirm the test actually populated the child's nested config (it's non-null inside the parent's config object) — ruling out an incomplete tree versus a missing relay.
- Verify with the config/resource trace. The trace shows which scopes have sets; the unconfigured child's scope will have no matching entry, confirming the relay never happened.
Make every level relay every child:
- Relay in a loop, not by hand. Where a level has multiple like children (agents in an array), relay their slices in a loop over the children, so adding an agent can't leave a forgotten
set. - Always check each
get. A checkedget(if (!...) uvm_fatal) turns a missing relay into a loud, local fatal at the starved child — instead of a null-handle crash phases later (Module 7.2's discipline, applied per level). - Keep the cascade pattern uniform. Every component's
build_phasefollows the same triple — get my config, relay each child's slice, build children — so a missing relay is a visible omission from a familiar shape, easy to spot in review. - Verify the whole tree is distributed. Use the config/resource trace (or check
configured-style flags) to confirm every component received its config, not just the ones you happened to test.
The one-sentence lesson: hierarchical configuration only works if every level relays every child's slice, so when one subtree comes up unconfigured while its siblings are fine, the break is a missing relay in that subtree's parent — fix it by relaying every child (ideally in a loop) and checking each get so a missing relay fails loudly at the starved child.
Common Mistakes
- A missing relay in the cascade. A level that forgets to
setone child's slice leaves that subtree unconfigured while siblings work. Relay every child — ideally in a loop. - An incompletely populated config tree. Setting the top object but forgetting to
create/fill a nested config leaves a null slice that relays as null. Build and populate the whole tree before setting the top. - Falling back to scattered scalars. Setting each setting individually instead of bundling into a config object multiplies string keys and silent-miss chances. Bundle related settings into one typed object.
- Sharing one config handle across instances unintentionally. Reusing the same
agent_configfor two agents aliases them — mutating one affects the other. Give each instance its own object (or clone). - Not checking each level's
get. An uncheckedgetturns a missing relay or null slice into a distant crash. Check everygetso it fails loudly and locally. - Reaching across the hierarchy instead of cascading. Having the test set deep leaf scopes directly bypasses the cascade and couples the test to the env's internals. Let each level relay to its own children.
Senior Design Review Notes
Interview Insights
Hierarchical configuration is the practice of organizing a multi-level environment's configuration as a tree of typed configuration objects that mirrors the component tree, and distributing it top-down through the build-phase cascade. Instead of setting each scalar setting individually, you bundle each component's related settings into a configuration object — a uvm_object subclass — and you nest those objects so the structure parallels the components: an env_config holds an agent_config per agent, and each agent_config holds the pieces its driver and monitor need. The test creates and populates this whole config tree at the top and does a single set of the top env_config at the env's scope. Then, as build_phase runs top-down, each level performs the same triple: it gets its own config object, it relays each child's nested slice by setting it at that child's scope, and it builds its children. So the env gets env_config and relays each agent_config; each agent gets its agent_config and relays the driver's piece; the driver gets its piece. The configuration cascades down the hierarchy, one typed object per level. The benefits are that you replace many fragile string-keyed scalar sets with one typed set per component, related settings stay cohesive, the configuration is navigable because it mirrors the design, and each level is responsible only for its own config and its children's — which keeps components reusable.
Exercises
- Build the tree. Sketch the config classes for an env with two agents, each agent having a driver that needs a virtual interface — show the nesting (
env_config→agent_config→ vif) and the single top-levelset. - Write the relay. For the env in (1), write the env
build_phasethat getsenv_configand relays each agent'sagent_config, and explain why the relay must precede building the agents. - Diagnose the subtree.
agent1is unconfigured butagent0is fine, and the test populated both. Name the most likely cause, where it is, and the fix. - Defend against aliasing. Explain what goes wrong if both agents are given the same
agent_confighandle, and two ways to prevent it.
Summary
- Hierarchical configuration bundles each component's settings into a typed configuration object and nests those objects into a tree that mirrors the component tree —
env_configcontaining per-agentagent_configobjects containing their children's pieces. - The test populates the whole config tree at the top and sets the top object once; distribution happens through the build-phase cascade, where each level runs the same triple: get my config → relay each child's slice → build children.
- Top-down build order makes the cascade correct: a parent always relays a child's slice before the child's
build_phasegets it, so the relays never race — and each level is responsible only for its own config and its children's, which keeps components reusable. - Bundling beats scattered scalars on every axis — one typed set/get instead of many string keys, cohesion, compiler-checked members, and extensibility (add a member, not a new
set/geteverywhere); and the cascade-specific bug is a missing relay, which starves one subtree while siblings work. - The durable rule of thumb: model configuration as a typed object tree that mirrors the component tree, populate it fully at the top, and cascade it down with a uniform get-config / relay-every-child / build-children triple at each level — relaying children in a loop and checking every
get, because a broken relay or an incomplete tree silently starves a subtree.
Next — Config Patterns: with the config tree and cascade in place, the next chapter collects the reusable idioms built on them — default-then-override layering, configuration knobs, randomized configuration, and combining the factory with the config DB — the recipes that turn one environment into many scenarios.