UVM
Config Patterns
The reusable config DB idioms that turn one env into many scenarios — default-then-override layering, configuration knobs, randomized configuration, and combining the factory (types) with the config DB (values).
Configuration Database · Module 7 · Page 7.5
The Engineering Problem
The previous chapters built the configuration machinery: the set/get API (Module 7.2), the matching and precedence rules (Module 7.3), and the hierarchical config-object tree and cascade (Module 7.4). That machinery has a purpose — to turn one reusable environment into many test scenarios without editing the environment. This chapter is the catalog of idioms that achieve it: the patterns experienced teams reach for when they want the same env to run a smoke test, a stress test, an error-injection test, and a randomized regression — all from configuration alone.
There are four patterns worth knowing by name, because together they cover almost everything you'll do with configuration. Default-then-override layering: the env (or a base test) sets sensible defaults, and a derived test overrides only what it needs — relying directly on the precedence rule from 7.3. Configuration knobs: the config object exposes tunable fields (counts, rates, enables) that tests flip to reshape behavior without touching code. Randomized configuration: the config object's fields are rand and constrained, so each run gets a different but legal configuration — diversity for free. Factory-plus-config-DB: the two top-down mechanisms combined — the factory substitutes types, the config DB passes values — so a test can both swap a component's class and configure it. This chapter is those four patterns: what each is, when to use it, and the one layering pitfall that catches everyone.
What are the reusable config DB patterns that turn one environment into many scenarios — default-then-override layering, configuration knobs, randomized configuration, and combining the factory with the config DB — and how does each build on the config DB mechanics?
Motivation — why these patterns exist
Each pattern exists to make one reusable env serve a different need, and each leans on a specific piece of the config DB machinery:
- Layering exists so tests don't re-specify everything. A test usually wants the env's normal behavior with one thing changed. Default-then-override lets the env (or base test) provide a complete working configuration and a derived test change only the delta — built directly on precedence (the higher/later setter wins, Module 7.3). Without it, every test would have to fully configure the env.
- Knobs exist to separate what can vary from code. Behaviors that legitimately differ between tests — how many transactions, what error rate, whether coverage is on — belong in the config object as named fields, not hardcoded in components. Tests then reshape behavior by setting knobs, and the components stay scenario-agnostic and reusable.
- Randomized configuration exists to get diversity cheaply. Many bugs hide in configuration corners no one thought to write a directed test for. Making config
randand constraining it to the legal space means every regression run exercises a different valid configuration, so coverage of the configuration space accumulates automatically. - Factory-plus-config-DB exists because type and value are different axes. Some scenarios need a different class (an error-injecting driver) — that's the factory (Module 6). Others need different values (a higher error rate) — that's the config DB. Real scenarios often need both, and because both are top-down mechanisms, a test can apply them together cleanly.
- They compose. A base test can set defaults (layering) including knob values, a derived test can override one knob, and a regression can randomize the rest — all at once. The patterns aren't alternatives; they stack.
The motivation, in one line: these patterns turn the config DB from a value-passing mechanism into a scenario-generation mechanism — defaults make the env work, overrides specialize it, knobs tune it, randomization diversifies it, and factory+config combine type and value substitution — so one env yields a whole regression.
Mental Model
Hold the config patterns as the control surface of a reconfigurable instrument:
The env is one instrument; the config patterns are the controls that make it play a different piece each time, without rebuilding the instrument. The factory presets are the defaults — the env comes up playing something sensible out of the box (default-then-override: the env/base test sets a complete working configuration). Your manual adjustments are the overrides — a derived test nudges one or two controls and leaves the rest at preset. The faders and dials are the knobs — named, tunable controls (transaction count, error rate, coverage on/off) that reshape the performance without rewiring. The randomize button is randomized configuration — press it and every run sets the controls to a different but legal combination, so the regression explores the space. And swapping a module is the factory — when adjusting dials isn't enough and you need a different component entirely (an error driver), the factory changes the part while the config DB still tunes it. One instrument, one control surface, endless performances — and the skill is knowing which control to reach for: a preset-and-tweak (layering), a dial (knob), the randomize button (random config), or a module swap (factory).
So building a regression is operating this control surface: defaults so it plays at all, overrides for targeted variation, knobs for tunable variation, randomization for broad variation, and factory swaps when a value change can't express the scenario. The patterns stack on one env, which is exactly the reuse the config DB was built to enable.
Visual Explanation — default-then-override layering
The foundational pattern is layering: defaults at the bottom, overrides on top, with precedence resolving to the most authoritative value. It's the pattern every other one builds on, and it's a direct application of Module 7.3's precedence rule.
The layering pattern is precedence put to work. The env provides a complete, working default configuration — every field has a sensible value, so the env runs correctly even with no test overrides at all (a smoke test is "run the env with its defaults"). A base test sits above it, setting defaults common to a family of tests (a standard transaction count, a standard timeout). A derived test sits at the top, overriding only the delta its scenario needs — bump the error rate, disable a checker — and inheriting everything else. When any component gets a field, the precedence rule (Module 7.3) resolves the stack: the derived test's set (highest/last) wins over the base test's, which wins over the env's. The power is that each layer specifies only what it changes: a derived test is a few lines of override, not a full reconfiguration, because it stands on the layers beneath it. This is the essence of reuse — the env defines "normal," and each test expresses itself as a small deviation from normal. Every other pattern in this chapter layers on top of this one: knobs are fields you layer values onto, randomization is a layer that sets random values, and factory overrides layer type changes alongside the value layers.
RTL / Simulation Perspective — the four patterns in code
Each pattern is a small, recognizable code idiom. Seeing them together shows how they stack on the same config object.
// ── KNOBS: the config object exposes tunable, optionally rand fields ──
class env_config extends uvm_object;
`uvm_object_utils(env_config)
rand int unsigned num_txns; // knob (and randomizable)
rand int unsigned error_rate; // knob (and randomizable)
bit enable_coverage = 1;
constraint c_legal { num_txns inside {[10:1000]}; error_rate inside {[0:20]}; }
function new(string name="env_config"); super.new(name); endfunction
endclass
// ── LAYERING: base test sets defaults; derived test overrides the delta ──
class base_test extends uvm_test;
function void build_phase(uvm_phase phase);
cfg = env_config::type_id::create("cfg");
cfg.num_txns = 100; cfg.error_rate = 0; // DEFAULTS
uvm_config_db#(env_config)::set(this, "env", "cfg", cfg);
env = my_env::type_id::create("env", this);
endfunction
endclass
class err_test extends base_test;
function void build_phase(uvm_phase phase);
super.build_phase(phase); // base sets defaults FIRST...
cfg.error_rate = 15; // ...THEN override (last wins — see pitfall)
endfunction
endclass
// ── RANDOMIZED CONFIG: each run gets a different legal configuration ──
class rand_test extends base_test;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!cfg.randomize()) `uvm_fatal("RAND","config randomize failed") // constrained-random
endfunction
endclass
// ── FACTORY + CONFIG DB: swap the TYPE and configure the VALUE together ──
class err_inject_test extends base_test;
function void build_phase(uvm_phase phase);
set_type_override_by_type(my_driver::get_type(), err_driver::get_type()); // TYPE (factory)
super.build_phase(phase);
cfg.error_rate = 15; // VALUE (config DB)
endfunction
endclassThe four idioms share one config object and stack cleanly. Knobs are the named fields of env_config — num_txns, error_rate, enable_coverage — that tests set to reshape behavior; components read them and behave accordingly (a sequence reads num_txns to decide how many items to send). Layering is base_test setting defaults and err_test overriding just error_rate, inheriting the rest. Randomized configuration makes the knobs rand with a legality constraint, and cfg.randomize() gives each run a different valid combination (note the checked return — a failed randomize is a fatal, like a checked get). Factory + config DB is err_inject_test doing both: a set_type_override_by_type to swap the driver's class (factory, Module 6) and a config set to raise the error rate (config DB) — type and value substitution in one test. Notice the order in the derived tests: the override / randomize / knob change comes after super.build_phase() — that placement is deliberate, and the next sections explain why getting it wrong silently loses the override.
Verification Perspective — factory and config DB are complementary axes
The pattern that most clarifies the config DB's role is pairing it with the factory. They are the two top-down substitution mechanisms, and they substitute different things — understanding the split is what lets you reach for the right one.
The two mechanisms divide the substitution space cleanly. The factory (Module 6) answers which class is built — it substitutes the type, so my_driver can become err_driver without the env knowing. The config DB answers what settings the built component uses — it substitutes the value, so error_rate can be 15 instead of 0. These are independent axes: a scenario might need a different class but the same settings (a logging monitor in place of a plain one), or the same class with different settings (the normal driver with a higher injection rate), or — commonly — both (an error driver and a high rate). Both are top-down: the test applies them, and they flow down to the components without editing anything in between. The skill is diagnosing which axis a scenario actually moves: if the behavior you need can't be expressed by any setting of the existing class, you need the factory (a new type); if it's a matter of values the class already honors, you need the config DB (a new value); if it's both, use both. Reaching for the factory when a knob would do (subclassing to hardcode a value) or for config when only a type change suffices (adding a mode flag the class branches on) are the two ways teams blur the axes — keeping them distinct keeps both the components and the tests clean.
Runtime / Execution Flow — randomized configuration
Randomized configuration is the pattern that turns the config object into a coverage engine: declare the legal space with constraints, randomize per run, and every regression cycle exercises a different valid configuration.
Randomized configuration reframes the config object as a generator of legal scenarios. Step 1 declares the space: the knobs become rand fields and a constraint pins down what's legal (a transaction count in a sensible range, an error rate within bounds, mutually consistent mode combinations) — this is the same constrained-random discipline used for stimulus, applied to configuration. Step 2 randomizes: cfg.randomize() asks the solver for a valid assignment within the constraints, and — like every checked operation in UVM — the return value is checked, because a failed randomize (over-constrained, contradictory) should be a loud fatal, not a silent default. Step 3 sets and cascades the result exactly like any configuration (Module 7.4) — randomization changes what the config holds, not how it's distributed. Step 4 is the payoff: because each run randomizes independently (a different seed gives a different valid assignment), a regression of N runs exercises N different points in the configuration space, so coverage of configuration corners accumulates without anyone hand-writing a directed test for each combination. This is why randomized configuration is high-leverage: it turns the configuration space from something you sample manually into something the regression explores automatically — and it layers on top of defaults, so unconstrained fields keep their sensible values while the rand ones vary.
Waveform Perspective — a knob reshaping the run
Knobs reshape run-time behavior from build-time configuration. A timeline of two runs of the same env with different knob values shows configuration translating into different behavior.
One env, two knob settings: num_txns reshapes the run without code changes
11 cyclesThe waveform shows configuration becoming behavior. At build time, the cfg_read pulse marks the sequence reading the num_txns knob — a zero-time config get that fixes how many transactions this run will drive. Then the two txn traces show the same env behaving differently purely because the knob differed: in run A (num_txns small) the sequence drives a few transactions and run_phase ends; in run B (num_txns large) the identical env drives many more before ending. Nothing in the env's code changed between the runs — no recompile, no edited component — only the configured knob value, set from the test. This is the knob pattern in one picture: a tunable field, read at build time, that reshapes run-time behavior, so a smoke test (small count) and a stress test (large count) are the same env with different configuration. Extend the idea and the other patterns appear: layering decides the knob's default and overrides, randomization picks the knob's value per run, and a factory override could additionally swap the driver feeding these transactions — all reshaping the run from the top, without touching the env.
DebugLab — the override that lost to its own base default
A derived test's override silently lost because it ran before super.build_phase()
A derived test, err_test extends base_test, set cfg.error_rate = 15 to inject errors. The base test sets a default of error_rate = 0. But the test ran with error_rate = 0 — no errors injected — as if the override never happened. The override was on the right field, the right config object, with no error or warning. The derived test's intent was simply ignored, and the base's default prevailed.
The derived test set its override before calling super.build_phase(), so the base's default — set inside super.build_phase(), and therefore later — won by the same-level last-wins rule:
err_test::build_phase:
cfg.error_rate = 15; // ① derived override set FIRST
super.build_phase(phase); // ② base runs now: cfg.error_rate = 0 ← set LATER, same component
both sets are from the SAME component (uvm_test_top) → equal precedence → LAST wins (Module 7.3)
② ran after ① → error_rate = 0 wins → the derived override is silently lost
fix — set the override AFTER super.build_phase():
super.build_phase(phase); // ① base sets default = 0 first
cfg.error_rate = 15; // ② derived override set LAST → winsThis is the layering-order pitfall, and it comes straight from the precedence rule. Both the base's default and the derived's override act on the configuration from the same component — the one test instance (uvm_test_top) — so they have equal hierarchical precedence, and the tiebreaker is order: last wins (Module 7.3). When the derived test sets its override before super.build_phase(), the base's default (running inside super.build_phase()) is registered afterward and therefore wins — silently overriding the override. The fix is to set derived overrides after super.build_phase(), so they're last. (Whether the config is a shared object field mutated in place, as here, or separate config_db::set calls, the principle is identical: among same-level sets, the last one wins, so the override must come last.) Note the deliberate contrast with factory overrides (Module 6.7), which go before super.build_phase() — because a factory override must exist before the children are created during super.build_phase(), whereas a config value isn't read until the child's later build_phase, so the config override can (and must) come after to win the order tiebreaker.
The tell is a derived test's override having no effect while the base default prevails. Diagnose layering-order bugs:
- Confirm it's silent and the base value is used. No error, and the field holds the base default, not the override — that points to the override being overwritten, not missing entirely.
- Check the order around
super.build_phase(). Find where the derived test sets its override relative tosuper.build_phase(). An override beforesuper.build_phase()is the signature — the base's set runs later and wins. - Confirm both sets are same-level. Base and derived both configure from the test (same component), so precedence is equal and order decides. (If they were different hierarchy levels, hierarchy — not order — would decide.)
- Verify with the config trace. The config/resource trace shows both sets and which won — confirming the later base set overrode the earlier derived one.
Order derived overrides after the defaults they override:
- Set derived overrides after
super.build_phase(). So they're registered last and win the same-level tiebreaker. Make "call super first, then override" the reflex for config in derived tests. - Remember config and factory overrides have opposite order. Factory overrides go before
super.build_phase()(before children are created); config overrides go after (to be last). Keep the two straight by what each needs. - Centralize defaults in the base, deltas in the derived. The base sets a complete default config; each derived test changes only its delta, after super — so layering is a readable "defaults then deltas" shape.
- Verify the override took effect. Check the resolved value (config trace or a printed config) — layering-order bugs are silent, so confirm the override actually won, especially for the field the test exists to change.
The one-sentence lesson: among config sets from the same component, the last one wins, so a derived test must set its overrides after super.build_phase() (the opposite of factory overrides) — set them before, and the base's default, registered later, silently wins and the override is lost.
Common Mistakes
- Setting a derived override before
super.build_phase(). The base's default runs later and wins by same-level last-wins; set overrides after super so they're last. - Confusing config and factory override order. Factory overrides go before
super.build_phase(); config overrides go after. They're opposite because they need different timing. - Not checking
randomize()'s return. A failed (over-constrained) randomize silently leaves stale values; check the return and fatal on failure, like a checkedget. - Hardcoding values that should be knobs. Burying a count or rate in a component forces a code change (or a subclass) to vary it; expose it as a config field instead.
- Reaching for the factory when a knob would do. Subclassing a component just to change a value blurs the type/value axes; use a config knob for values, the factory only for genuine type changes.
- Unconstrained randomized configuration. Randomizing without legality constraints produces illegal configs that fail confusingly; constrain the legal space so every randomization is valid.
Senior Design Review Notes
Interview Insights
Four patterns cover most configuration work, and they turn one reusable env into many scenarios. Default-then-override layering: the env or a base test sets a complete default configuration, and a derived test overrides only the fields its scenario needs, relying on precedence so the override wins and the rest is inherited — this keeps each test a small delta rather than a full reconfiguration. Configuration knobs: the config object exposes named tunable fields — transaction counts, error rates, enable flags — that tests set to reshape behavior, so the behaviors that legitimately vary live in configuration rather than hardcoded in components, keeping components scenario-agnostic. Randomized configuration: the config object's fields are made rand and bounded by legality constraints, and randomizing per run gives each regression cycle a different valid configuration, so configuration-space coverage accumulates automatically. Factory plus config DB: the two top-down mechanisms combined — the factory substitutes the type (which class is built) and the config DB substitutes the value (what settings it uses) — so a test can both swap a component's class and configure it. The key insight is that these patterns stack on one config object and one env: a base sets defaults including knobs, a derived overrides a knob, a regression randomizes the rest, and a factory override can swap a type alongside — all reshaping the env from the top without editing it, which is exactly the reuse the config DB exists to enable.
After super.build_phase(). The reason is the precedence rule: a derived test's override and the base test's default both act on configuration from the same component — the single test instance — so they have equal hierarchical precedence, and the tiebreaker among equal-precedence sets is that the last one wins. The base's default is set inside super.build_phase(). If you set your override before calling super.build_phase(), then the base's default runs afterward, is registered later, and silently wins — your override is lost with no error, and the test runs with the base default. If you set your override after super.build_phase(), it's registered last and wins, which is what you want. So the rule is: call super first to let the base establish defaults, then apply your overrides so they're last. This is the opposite of factory overrides, which go before super.build_phase() — and the contrast is instructive: a factory override must exist before the children are created, and creation happens during super.build_phase(), so the factory override has to precede it; a config value, by contrast, isn't read until the child's own build_phase later, so the config override doesn't need to be early, and being last is what makes it win the order tiebreaker. Knowing both rules and the reason for each — children-created-during-super for factory, last-wins for config — is what a reviewer is checking.
Exercises
- Name the pattern. For each need — (a) run the same env with 10 vs 1000 transactions, (b) inject errors with a different driver class, (c) give each regression run a different legal config, (d) a smoke test that just runs normally — name the config pattern that fits.
- Fix the order. A derived test sets
cfg.timeout = 500beforesuper.build_phase()and the base's defaulttimeout = 100is used instead. Rewrite it and explain the rule. - Split the axes. A scenario needs an error-injecting driver running at a 20% error rate. State which part is the factory's job and which is the config DB's, and write both lines.
- Constrain the randomization. Add
randfields and a legality constraint to a config object sonum_txnsis 10–1000 anderror_rateis 0–20, and show the checkedrandomize()call.
Summary
- Four config patterns turn one reusable env into many scenarios: default-then-override layering (env/base defaults, derived deltas, resolved by precedence), knobs (tunable config fields tests flip), randomized configuration (
rand, constrained fields for per-run diversity), and factory + config DB (substitute type and value together). - Layering is the foundation: the env defines a complete "normal" configuration and each test expresses a small deviation — so a test is a few lines of override, not a full reconfiguration, and every other pattern stacks on top.
- The factory and config DB are complementary top-down axes: the factory substitutes the type (which class), the config DB the value (which settings); choose by what the scenario moves, and combine them when it needs both.
- The layering-order pitfall: among config sets from the same component, the last wins, so a derived test must set overrides after
super.build_phase()— the opposite of factory overrides (which go before, to precede child creation) — or the base's later default silently wins. - The durable rule of thumb: express scenarios as configuration, not code — defaults so the env works, layered overrides (after
super.build_phase()) for targeted change, knobs for tunable behavior, constrained+checked randomization for diversity, and the factory alongside the config DB when a scenario needs a new type as well as new values — so one env yields a whole regression without being edited.
Next — Configuration Database Gotchas: these patterns assume configuration behaves predictably — the next chapter collects the traps that break that assumption: shared-handle aliasing, set/get type subtleties, wildcard surprises, and timing mistakes, with the debugging techniques to catch them.