UVM
set() and get()
The uvm_config_db set()/get() API — the four arguments, the scope string from cntxt plus inst_name, type parameterization, and the return-value check that decides whether a configuration is published and retrieved correctly.
Configuration Database · Module 7 · Page 7.2
The Engineering Problem
The previous chapter established the configuration database as a structure (Module 7.1) — a typed, hierarchical key-value store that lets a parent set values a child reads, without either knowing the other directly. This chapter covers the two calls that use it: uvm_config_db#(T)::set(...) to publish a value and uvm_config_db#(T)::get(...) to retrieve it. Almost everything you do with configuration — passing a virtual interface to a driver, an is_active flag to an agent, a config object to an env — is a set somewhere up the tree and a get somewhere down it.
The problem is that these two calls have a precise contract, and getting any part of it wrong fails silently. A set and a get only connect when four things agree: the type T (the parameterization must match), the field name (the key string must match exactly), the scope (the setter's scope string must cover the getter's location), and the timing (the set must happen before the get). Miss any one — wrong T, a typo'd field, a scope that doesn't reach, a get that runs first — and get simply returns 0 (not found), leaving your variable at its default. If you didn't check that return value, the failure is invisible until something downstream dereferences a null handle. This chapter is the set/get contract: the four arguments, how the scope string is built, why the type must match, and why you must always check what get returns.
What do
uvm_config_db#(T)::set()and::get()actually do — what are their four arguments, how is the scope string formed, why must the typeTand field name match, and why must you always checkget's return value?
Motivation — why the contract has four parts
Each part of the set/get contract exists because the config DB is a decoupled, typed, hierarchical store, and each property imposes one matching requirement:
- Typed → the type
Tmust match. The database is keyed partly by type: a value set asuvm_config_db#(virtual my_if)lives in a different typed slot than one set asuvm_config_db#(int). Agetwith a differentTlooks in the wrong slot and finds nothing. The type parameter is not decoration — it's part of the key. - Keyed by name → the field name must match exactly. Within a scope, entries are distinguished by the
field_namestring."vif"and"vifc"are different keys; a typo means thegetlooks up a key nobody set. Strings are unchecked by the compiler, so this is a common silent miss. - Hierarchical → the scope must reach. The setter names a scope (a path, possibly wildcarded) that defines which getters can see the value. If the setter's scope doesn't cover the getter's location, the value is invisible to it — set correctly, but out of reach. (The precise matching rules are Module 7.3; here we need the basic scope-string construction.)
- Decoupled → timing must be right. Setter and getter don't reference each other; they meet only through the database. So the value must be in the database when the getter looks — the
setmust execute before theget. Configuration is typically set in a parent'sbuild_phasebefore the child that gets it is built (top-down build order, Module 5.2). - Returns a status → you must check it. Because all four of the above can fail silently,
getreturns a bit telling you whether it found anything. Ignoring that bit is how a missing or mismatched configuration becomes a null-handle crash three phases later instead of a clear message at theget.
The motivation, in one line: the config DB's power — decoupled, typed, hierarchical passing — is exactly what makes its failures quiet, so the set/get contract demands that type, name, scope, and timing all align, and that you check the return value to catch it loudly when they don't.
Mental Model
Hold set/get as an addressed parcel service with a typed, named pigeonhole grid:
setdrops a typed parcel into a pigeonhole addressed by scope and field name;getcollects the parcel from the pigeonhole matching its address, name, and type — and tells you whether one was actually there. The grid of pigeonholes is the config database. To deliver (set), you write four things on the parcel: the area it's for (the scope —cntxtplusinst_name, e.g. "everything under env.agent"), the label (field_name, e.g. "vif"), the kind of thing it is (the typeT), and the contents (value). To collect (get), you go to your own location, ask for a parcel with a given label and kind, and the service checks whether a parcel addressed to an area covering you, with that label and kind, is waiting. If yes, you receive its contents and a "found" receipt (return 1); if the label is misspelled, the kind is wrong, the area didn't cover you, or nothing was dropped yet, you get an "empty-handed" receipt (return 0) — and the crucial discipline is to read the receipt, because walking away assuming you got your parcel when you didn't is how you discover, much later, that you're holding nothing (a null handle).
So every configuration is this parcel exchange: a set that addresses a parcel by area + label + kind, and a get that collects by the same three and checks the receipt. The four ways it fails — wrong kind (type), wrong label (field), wrong area (scope), too late (timing) — are just four ways the parcel and the collector don't meet.
Visual Explanation — set publishes, get retrieves
The two calls are a decoupled publish/retrieve pair: set writes a typed entry into the database; get, elsewhere and possibly later, reads it. Neither references the other — they meet only at the database.
The flow shows the decoupling that makes config DB powerful and its failures quiet. The setter (step 1) — typically a parent component or the test — calls set with four arguments and writes the value into the database under a key composed of the scope (where it's visible), the field name (its label), and the type T. The database (step 2) simply holds that entry; it persists, indifferent to who set it or who will read it. The getter (step 3) — typically a child — calls get with its own four arguments and asks the database for the entry matching its scope, field, and type. Crucially, the setter and getter never name each other: a driver doesn't know which parent supplied its virtual interface, and the parent doesn't hold a handle to the driver — they communicate only through the database, keyed by agreement on type/field/scope. That decoupling is the whole point (a parent can configure a child many levels down without threading handles through every layer), but it's also why a mismatch is silent: there's no direct link to break loudly, just a get that quietly finds nothing. Hence step 4 — check the returned bit — is not optional; it's the only loud signal the mechanism provides.
RTL / Simulation Perspective — the four arguments and the scope string
The set and get signatures are identical in shape — four arguments — and the key to using them is understanding what each argument is and how the first two combine into the scope string.
// ── SET: publish a virtual interface from the test for the whole env ──
// uvm_config_db#(T)::set( cntxt, inst_name, field_name, value );
uvm_config_db#(virtual my_if)::set(this, "env.agnt.drv", "vif", my_vif);
// │ │ │ │
// cntxt ─┘ │ │ └─ value (the thing stored)
// inst_name (rel path) ─┘ └─ field_name (the key string)
// scope searched = {this.get_full_name(), ".", "env.agnt.drv"} e.g. "uvm_test_top.env.agnt.drv"
// ── GET: the driver retrieves it, and CHECKS the return value ──
class my_driver extends uvm_driver#(my_item);
virtual my_if vif;
function void build_phase(uvm_phase phase);
if (!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif)) // "" = this component itself
`uvm_fatal("NOVIF", "virtual interface 'vif' not set for driver") // loud on miss
endfunction
endclassThe four arguments are the same for both calls. cntxt (context) is a uvm_component handle that anchors the scope — its full hierarchical name is the base of the scope string. inst_name is a path relative to cntxt; the actual scope is the concatenation {cntxt.get_full_name(), ".", inst_name}. field_name is the string key that labels the entry within that scope. value is the data — for set, the value to store; for get, the variable (passed by reference) that receives it. Two idioms dominate. On set, you often pass this as cntxt and a relative path like "env.agnt.drv" (or a wildcard like "*") as inst_name, building a scope that reaches the intended getters. On get, you almost always pass this and "" as inst_name — meaning "this component's own scope" — because a component is reading configuration for itself. And the get is wrapped in if (!...) uvm_fatal: the type virtual my_if and field "vif" must match the set exactly, and if they don't (or the value was never set), get returns 0 and the fatal fires immediately — instead of vif staying null and crashing in run_phase.
Verification Perspective — always check the return value
The single most important discipline with get is checking its return value. Because all four match conditions fail silently, that returned bit is the only built-in signal that a configuration is missing — and ignoring it converts a clear, local error into a confusing, distant crash.
The return value is the contract's safety mechanism, and using it is the difference between a clear error and a debugging session. get returns 1 when it found a matching entry — the variable now holds the configured value and you proceed normally. It returns 0 when it found nothing — and critically, the variable is left unchanged, sitting at whatever default it had (for an unassigned class handle or virtual interface, that's null). What you do with a 0 depends on whether the configuration is required or optional. For required configuration (a driver's virtual interface, an agent's config object), a 0 means the testbench is mis-wired and you should uvm_fatal immediately, at the get, where the message points straight at the missing key. For optional configuration, a 0 means "use the default" and you proceed with a documented fallback. The catastrophic path is the third one: ignoring the 0 — writing void'(uvm_config_db#(...)::get(...)) or not testing the result — so the variable silently stays null, no error is raised, and the null handle is finally dereferenced in run_phase, many components and phases away from the actual cause (a typo'd field, a wrong type, an unreachable scope). So the rule is absolute: every get either checks its return value or has a deliberate, documented reason not to.
Runtime / Execution Flow — set before get, top-down
The timing requirement — set before get — is satisfied automatically by UVM's top-down build order, if you set configuration in the right place. Understanding the order shows where set and get belong.
The timing requirement is real but usually free, because it aligns with the phase order you already have. build_phase executes top-down (Module 5.2): a parent's build_phase runs before its children's. So if a parent (or the test) calls set inside its build_phase, that set executes before the child is even built — and certainly before the child's build_phase, where the matching get lives. By the time the child calls get, the value has been sitting in the database since the parent ran moments earlier. This is why the canonical pattern is set in the parent/test build_phase, get in the child build_phase: the top-down order guarantees set-before-get with no manual synchronization. The timing failure happens when you violate this alignment — setting configuration after the children are already built (e.g., in connect_phase or run_phase, by which point the child's get in build_phase already ran and returned 0), or getting in a phase that precedes the set. As long as configuration flows the natural direction — set high and early, get low and during build — the timing part of the contract takes care of itself, and the bugs you have to worry about are the other three: type, field, and scope.
Waveform Perspective — set and get on the build timeline
The set/get exchange happens entirely at zero time during build, before any behaviour. The waveform locates both calls in the build sliver and shows the value becoming available to the run.
set (parent build) then get (child build), both at zero time, before the run
10 cyclesThe waveform places the whole exchange where it belongs: in the zero-time build sliver, before any behaviour. The set pulse comes first (the parent publishing in its build_phase), then the get pulse (the child retrieving in its build_phase) — the order guaranteed by top-down build. Once the get succeeds, cfg_valid goes high: the child now holds a valid configured value (a non-null virtual interface, a populated config object), which is the precondition for it to function. Only after all of this — when run_phase begins — does the configured value actually get used, producing the data activity. There's no pin activity during build because set/get are database operations, not signal transactions: they move a value from a parent to a child through the config DB, at zero time, with no clock involved. This is the mental timing you should carry: configuration is wired up entirely before the run, set-then-get at zero time, so that when behaviour starts every component already holds what it needs — and if a get returned 0 back in that build sliver and wasn't checked, the crash will surface here, in the run, far from where it actually went wrong.
DebugLab — the get that silently returned 0
A driver that crashed in run_phase because its get quietly found nothing
A testbench crashed with a null-handle dereference in the driver's run_phase — the driver tried to drive vif.cb.data and hit a null virtual interface. But the test did set the interface: uvm_config_db#(virtual my_if)::set(this, "env.agent.driver", "vif", my_vif) was right there in the test's build_phase. The set looked correct, the path looked correct, and yet the driver's vif was null at run time. The crash was three phases and several components away from anything obviously wrong.
The driver's get used a different field name than the set ("vintf" vs "vif"), so get returned 0, left vif null — and the return value wasn't checked:
set (test): uvm_config_db#(virtual my_if)::set(this, "env.agent.driver", "vif", my_vif);
get (driver): void'(uvm_config_db#(virtual my_if)::get(this, "", "vintf", vif)); // ✗ "vintf" ≠ "vif"
│ └─ return value DISCARDED
└─ field-name mismatch → get returns 0
result: get finds no entry under "vintf" → returns 0 → vif left null (unchanged)
the discarded return value hid the miss → null deref in run_phase, far away
fix: match the field name AND check the return:
if (!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "virtual interface 'vif' not set") // would have fired loudly at the getTwo of the contract's safeguards were broken at once. First, the field name didn't match: the set used "vif", the get used "vintf" — a typo. Since the field name is part of the key, the get looked up an entry that nobody set and found nothing, returning 0. (The same silent 0 results from a type mismatch — e.g., setting uvm_config_db#(virtual my_if) but getting uvm_config_db#(my_if) — or a scope that doesn't reach.) Second, and what made it silent, the return value was discarded with void'(...): had the code checked the bit, the uvm_fatal would have fired at the get with a message naming the missing field, immediately and locally. Instead, vif stayed null, build finished cleanly, and the failure surfaced phases later as a null-handle deref in run_phase, far from the typo. The fix restores both safeguards: match the field name exactly and check the return value with a uvm_fatal on miss.
The tell is a null configuration handle at run time when the set looks correct. Diagnose get-returned-0 bugs:
- Check the return value first. Find the
getfor the null variable and confirm whether its return value is checked. Avoid'(...)-wrapped or uncheckedgetis the immediate suspect — it's hiding a 0. - Compare the four elements between set and get. Line up the
setandgetand check, in order: typeT(parameterization identical?), field name (string identical?), scope (does the setter's scope reach the getter?), timing (set before get?). One of them won't match. - Field name and type are the usual culprits. Both are unchecked by the compiler — a string typo or a slightly different parameterization is the most common silent 0.
- Add the check to localize. Temporarily wrap the
getinif (!...) uvm_fatal(oruvm_error); the failure moves from the distant null-deref to thegetitself, pinpointing the mismatch.
Make the contract explicit and checked:
- Always check
get's return value. Wrap required configuration inif (!uvm_config_db#(T)::get(...)) uvm_fatal(...). Never discard the bit withvoid'(...)unless the configuration is genuinely optional and the default is documented. - Keep field names consistent and centralized. Use a shared constant or a documented convention for field-name strings so set and get can't drift (
"vif"always, never"vintf"). Matching types likewise — set and get the sameuvm_config_db#(T). - Set high and early, get low and in build. Set configuration in the parent/test
build_phaseand get it in the childbuild_phase, so the top-down order guarantees set-before-get and the only things that can go wrong are type, field, and scope — all caught by the return-value check.
The one-sentence lesson: a set and get connect only when type, field name, scope, and timing all agree, and any mismatch makes get return 0 and leave the variable at its default — so always check get's return value (if (!...) uvm_fatal) to turn a silent null-handle crash three phases away into a loud, local error at the get itself.
Common Mistakes
- Discarding
get's return value.void'(uvm_config_db#(...)::get(...))hides a 0, so a missing configuration leaves the variable null and crashes later. Always check the bit (if (!...) uvm_fatal). - Field-name typos between set and get. The string is part of the key and is unchecked by the compiler;
"vif"vs"vintf"silently fails. Keep field names consistent or centralized. - Type mismatch between set and get.
uvm_config_db#(virtual my_if)anduvm_config_db#(my_if)are different typed slots; thegetfinds nothing. Set and get with the identicalT. - Setting configuration too late. Setting after the child is built (in
connect_phase/run_phase) means the child'sgetinbuild_phasealready returned 0. Set in the parent/testbuild_phase. - Wrong
inst_nameon get. A component reading its own configuration uses""(or its own path) — not a child path. Passing the wrong relative path looks in the wrong scope. - Forgetting that
cntxt+inst_nametogether form the scope. The searched scope is{cntxt.get_full_name(), ".", inst_name}— both arguments matter;inst_namealone is not the full scope.
Senior Design Review Notes
Interview Insights
They are the publish and retrieve calls of the configuration database. uvm_config_db#(T)::set(cntxt, inst_name, field_name, value) stores a typed value into the database, and uvm_config_db#(T)::get(cntxt, inst_name, field_name, var) retrieves it into a variable and returns a bit indicating whether it was found. Both take the same four arguments. cntxt is a uvm_component handle whose full hierarchical name forms the base of the scope. inst_name is a path relative to cntxt, so the actual scope searched is the concatenation {cntxt.get_full_name(), ".", inst_name}. field_name is the string key that labels the entry within that scope. value is the data — for set, the value to store; for get, the variable passed by reference that receives it. The type parameter T is part of the key: a value set as uvm_config_db#(virtual my_if) lives in a different slot than one set as uvm_config_db#(int). The defining property is decoupling: the setter and getter never reference each other, they communicate only through the database by agreeing on type, field name, and scope. The canonical idioms are set with this and a relative path (or a wildcard) from a parent or the test, and get with this and "" from a child reading its own configuration — always checking the returned bit.
Exercises
- Label the arguments. For
uvm_config_db#(int)::set(this, "env.*", "count", 8), name each argument, write the full scope string assumingthisisuvm_test_top, and write the matchinggeta component underenvwould use. - Add the check. Given
void'(uvm_config_db#(my_cfg)::get(this, "", "cfg", cfg));, rewrite it so a missing configuration fails loudly and locally, and state what symptom the original would produce instead. - Find the mismatch. A
setusesuvm_config_db#(virtual my_if)with field"vif"; agetusesuvm_config_db#(virtual my_if)with field"vifc". State whatgetreturns, what the variable holds, and where the crash appears. - Explain the timing. Explain why setting configuration in a parent's
build_phaseand getting it in the child'sbuild_phaseguarantees set-before-get, and give one phase placement that would break it.
Summary
uvm_config_db#(T)::set(cntxt, inst_name, field_name, value)publishes a typed value into the database under a key of scope + field name + type;::get(cntxt, inst_name, field_name, var)retrieves it intovarand returns a bit (1 found, 0 not).- A
setandgetconnect only when four things agree: the typeT, the field name (exact string), the scope (setter's scope must reach the getter), and the timing (set before get) — and any mismatch makesgetreturn 0 silently, leaving the variable at its default. - The scope string is
{cntxt.get_full_name(), ".", inst_name}:cntxtanchors the base,inst_nameis a relative path (often a wildcard onset, usually""ongetfor a component's own scope). - Timing is usually free: set in the parent/test
build_phase, get in the childbuild_phase, and top-down build order guarantees set-before-get — leaving only type, field, and scope to verify. - The durable rule of thumb: always check
get's return value —if (!uvm_config_db#(T)::get(...)) uvm_fatal(...)for required configuration — because a discarded return value turns a typo'd field, a wrong type, or an unreachable scope into a silent null handle that crashes phases later, while a checked one turns it into a loud, local error at thegetitself.
Next — Configuration Database Scope Rules: this chapter assumed a set's scope simply "reaches" the getter — the next one makes that precise: how instance-name patterns and wildcards match, how the hierarchy is walked, and what happens when multiple sets match the same getter (precedence), which is where the config DB's real subtlety lives.