Skip to content

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 type T and field name match, and why must you always check get'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 T must match. The database is keyed partly by type: a value set as uvm_config_db#(virtual my_if) lives in a different typed slot than one set as uvm_config_db#(int). A get with a different T looks 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_name string. "vif" and "vifc" are different keys; a typo means the get looks 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 set must execute before the get. Configuration is typically set in a parent's build_phase before 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, get returns 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 the get.

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:

set drops a typed parcel into a pigeonhole addressed by scope and field name; get collects 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 — cntxt plus inst_name, e.g. "everything under env.agent"), the label (field_name, e.g. "vif"), the kind of thing it is (the type T), 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.

set writes a typed entry keyed by scope, field, and type; get retrieves the matching entry and returns a found bitset → config DB entry → get (decoupled by the database)set → config DB entry → get (decoupled by the database)1set(cntxt, inst_name, field, value)a parent publishes: writes value into the DB under {scope, field,type T}.2Config DB stores the entrykeyed by scope (cntxt path + inst_name), field name, and type —persists until read.3get(cntxt, inst_name, field, var)a child retrieves: looks up the entry matching its scope, field,and type T.4Check the returned bit1 → var holds the value; 0 → not found (mismatch/missing) → fatalor default.
Figure 1 — set publishes a typed entry; get retrieves it. set(cntxt, inst_name, field, value) writes an entry into the config DB keyed by the scope (cntxt path + inst_name), the field name, and the type T. Later and elsewhere, get(cntxt, inst_name, field, var) looks up the entry matching its scope, field, and type — returning the value and a found bit. The setter and getter never reference each other; the database decouples them. The four must-match elements are type, field, scope, and timing.

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() and get() — the four arguments and the scope string
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endclass

The 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.

Decision flow for handling the return value of uvm_config_db get10handle itif uncheckedcalluvm_config_db#(T)::get(...)Return value?1 (found) → varholds theconfigured value→ proceed0 (not found) → varunchanged (default /null)Respond: uvm_fatal (required) or documented default (optional)Respond:uvm_fatal(required) ordocumented…Ignore the 0 → null var dereferenced phases later, far from the causeIgnore the 0 →null vardereferencedphases later,…
Figure 2 — why you must check get's return value. get returns 1 (found) or 0 (not found). On 1, the variable holds the configured value — proceed. On 0, the variable is unchanged (still its default — often null for a handle), and you must respond: uvm_fatal for required config, or apply a documented default for optional config. The failure mode is ignoring the 0: the variable stays at its default, no error is raised, and a null handle is dereferenced phases later — far from the actual cause.

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.

Top-down build order makes a parent's set in build_phase precede the child's get in build_phaseparent set (build_phase) precedes child get (build_phase)parent set (build_phase) precedes child get (build_phase)1Parent/test build_phase: set(...)publishes the value into the DB — runs first because build istop-down.2Parent builds its childrenthe child component is constructed (its build_phase is about torun).3Child build_phase: get(...)retrieves the value — it is already in the DB, set by the parentmoments earlier.4Timing satisfied automaticallyset-in-parent-build before get-in-child-build needs no manualsynchronization.
Figure 3 — set before get, enforced by top-down build. build_phase runs top-down: a parent builds before its children. A parent that sets configuration in its build_phase does so before the child's build_phase runs, so when the child gets the value it is already in the database. Setting in the parent (or test) build_phase and getting in the child build_phase satisfies the timing requirement automatically. Setting after the child is built — or getting before the parent set — is the timing failure (get returns 0).

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 cycles
set (parent build) then get (child build), both at zero time, before the runparent build_phase: set() publishes the value into the DBparent build_phase: se…child build_phase: get() retrieves it — set already happened (top-down)child build_phase: get…cfg_valid high: child now holds the configured value (e.g. non-null vif)cfg_valid high: child …run_phase: the configured value is used (data activity)run_phase: the configu…clkphaseBLDBLDBLDBLDRUNRUNRUNRUNRUNRUNsetgetcfg_validdata00000000A0A1B0B10000t0t1t2t3t4t5t6t7t8t9
Figure 4 — the config exchange is a zero-time, build-phase event. set pulses when the parent publishes the value into the DB; get pulses when the child retrieves it — set first (top-down build), get after, both before run_phase. cfg_valid goes high once the child's get succeeds, meaning the child now holds the configured value (e.g. a non-null vif). Then run_phase begins and the value is used (data activity). No pin activity during build, because set/get are zero-time database operations, not signal transactions.

The 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

Symptom

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.

Root cause

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:

why a correct set still left vif null
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 get

Two 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.

Diagnosis

The tell is a null configuration handle at run time when the set looks correct. Diagnose get-returned-0 bugs:

  1. Check the return value first. Find the get for the null variable and confirm whether its return value is checked. A void'(...)-wrapped or unchecked get is the immediate suspect — it's hiding a 0.
  2. Compare the four elements between set and get. Line up the set and get and check, in order: type T (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.
  3. 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.
  4. Add the check to localize. Temporarily wrap the get in if (!...) uvm_fatal (or uvm_error); the failure moves from the distant null-deref to the get itself, pinpointing the mismatch.
Prevention

Make the contract explicit and checked:

  1. Always check get's return value. Wrap required configuration in if (!uvm_config_db#(T)::get(...)) uvm_fatal(...). Never discard the bit with void'(...) unless the configuration is genuinely optional and the default is documented.
  2. 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 same uvm_config_db#(T).
  3. Set high and early, get low and in build. Set configuration in the parent/test build_phase and get it in the child build_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) and uvm_config_db#(my_if) are different typed slots; the get finds nothing. Set and get with the identical T.
  • Setting configuration too late. Setting after the child is built (in connect_phase/run_phase) means the child's get in build_phase already returned 0. Set in the parent/test build_phase.
  • Wrong inst_name on 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_name together form the scope. The searched scope is {cntxt.get_full_name(), ".", inst_name} — both arguments matter; inst_name alone 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

  1. Label the arguments. For uvm_config_db#(int)::set(this, "env.*", "count", 8), name each argument, write the full scope string assuming this is uvm_test_top, and write the matching get a component under env would use.
  2. 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.
  3. Find the mismatch. A set uses uvm_config_db#(virtual my_if) with field "vif"; a get uses uvm_config_db#(virtual my_if) with field "vifc". State what get returns, what the variable holds, and where the crash appears.
  4. Explain the timing. Explain why setting configuration in a parent's build_phase and getting it in the child's build_phase guarantees 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 into var and returns a bit (1 found, 0 not).
  • A set and get connect only when four things agree: the type T, the field name (exact string), the scope (setter's scope must reach the getter), and the timing (set before get) — and any mismatch makes get return 0 silently, leaving the variable at its default.
  • The scope string is {cntxt.get_full_name(), ".", inst_name}: cntxt anchors the base, inst_name is a relative path (often a wildcard on set, usually "" on get for a component's own scope).
  • Timing is usually free: set in the parent/test build_phase, get in the child build_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 valueif (!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 the get itself.

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.