Skip to content

UVM

build_phase

The top-down construction phase — getting configuration, creating children via the factory (type_id::create not new), setting children's config, and why overrides need the factory.

UVM Phasing · Module 5 · Page 5.2

The Engineering Problem

The phasing overview gave you the whole schedule; now we drill into the most important phase. build_phase is where the entire component tree comes into existence — and it does three specific jobs, in a specific order, exploiting one specific property of its traversal. Getting any of the three wrong, or misunderstanding the traversal, is a frequent source of bugs: an environment that builds the wrong components, a configuration that never reaches its target, or a factory override that mysteriously does nothing.

The three jobs are: read my own configuration (set by my parent), create my children (via the factory), and set configuration for those children (which they'll read when they build). The enabling property is that build_phase runs top-down — a parent's build_phase completes before its children's begins — which is exactly what lets a parent configure children before they exist and read it. And the one rule that makes the whole thing flexible: components are created with the factory (type_id::create), not new, so any component's type can be overridden from above without editing the tree. This chapter makes each of the three jobs, the top-down ordering, and the factory rule precise.

What exactly happens in build_phase — reading configuration, creating children via the factory, and configuring children — how does top-down traversal make parent-configures-child work, and why must components be created with type_id::create rather than new?

Motivation — why build_phase's structure matters

The three jobs, the top-down order, and the factory rule each solve a concrete problem:

  • Creating children top-down builds the tree correctly. Because a parent constructs its children, construction must flow from the root down — the test builds the env, which builds the agent, which builds the driver. build_phase's top-down traversal is exactly this cascade, and it's why the whole tree exists by the end of the phase.
  • Top-down enables configure-before-build. A parent's build_phase runs before its children's, so a parent can set configuration into the database that its children get when they build moments later. This is the mechanism behind reuse (Module 3.5): components are configured from above, and the timing works because of the top-down order.
  • The factory enables substitution without editing. Creating components with type_id::create routes construction through the factory, so a test can register an override (set_type_override) and have a derived type built everywhere — an error-injecting driver instead of the normal one — without touching the environment. Using new bypasses this entirely.
  • Getting the order wrong breaks configuration. If a component reads config that was never set (wrong scope), or a parent forgets to set a child's config, the child comes up unconfigured. Understanding the get/set/create choreography is what makes configuration reliably reach its target.

The motivation, in one line: build_phase is where the environment is constructed and configured, and its three jobs, top-down order, and factory rule are precisely what make construction correct, configuration reach its targets, and types remain substitutable.

Mental Model

Hold build_phase as each manager setting up their department, top-down, from a blueprint they can swap:

build_phase is every manager, from the top down, setting up their department. When it's your turn (and your manager has already set up theirs, because build is top-down), you do three things. First, you read the brief your manager left for you (get your configuration from the database — your manager set it before you started). Second, you hire your team (create your children) — but you don't hire by hand; you go through central HR (the factory), so the organisation can substitute a specialist for a standard hire without you knowing or caring. Third, you leave briefs for each of your new hires (set their configuration), which they'll read when they set up their departments next. Because everyone does this top-down, every manager's brief is waiting when they start, and the whole organisation is staffed and briefed from the top down in one pass.

So when you write a build_phase, do the three jobs: get your config, create your children through the factory, set your children's config. The top-down order guarantees your config is ready when you run and your children's config is ready when they run; the factory guarantees your "hires" can be substituted from above.

Visual Explanation — the three jobs of build_phase

Every component's build_phase does the same three things: read its own configuration, create its children through the factory, and set configuration for those children. The order and the factory are the discipline.

build_phase three jobs: get own config, set children config, create children via factoryGet config → set children's config → create children (via the factory)Get config → set children's config → create children (via the factory)1Get my configurationread my own config from the database — my parent set it before mybuild ran (top-down).2Set my children's configurationwrite config for the children I'm about to create; they'll get itin their own build_phase.3Create children via the factorytype_id::create(name, this) — NOT new — so each child's type can beoverridden from above.4Tree built top-downrepeated at every level, the tree is constructed and configuredfrom the root down in one pass.
Figure 1 — the three jobs of build_phase. (1) Get: read this component's own configuration from the config database (set by its parent). (2) Set: write configuration for the children it's about to create, which they'll read in their own build_phase. (3) Create: construct the children via the factory (type_id::create), so their types can be overridden. Done top-down, this constructs and configures the whole tree from the root down.

The three jobs map to three mechanisms. Get is uvm_config_db::get(this, ...) — reading the configuration your parent placed for you. Set is uvm_config_db::set(this, "child", ...) — placing configuration your children will read. Create is type_id::create("child", this) — constructing children through the factory so their types are substitutable. (The relative order of set and create within your own build_phase doesn't matter for config delivery — set just stores into the database, and the child reads it in its later build — but the convention is get-mine, set-theirs, create-them.) Done at every level, top-down, this single pattern constructs the entire tree and threads configuration down it, which is why build_phase is the phase where an environment both comes into being and gets parameterised.

RTL / Simulation Perspective — build_phase in code

The three jobs are explicit in code: get your config, set your children's config, create them with the factory. The top-down order means your parent already ran its build_phase (and set your config) before this runs.

build_phase — get config, set children's config, create via the factory
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  my_agent agent;  env_cfg cfg;
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
 
    // 1. GET my own configuration — my parent set it before my build_phase ran (top-down)
    if (!uvm_config_db#(env_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "env_cfg not provided from above")
 
    // 2. SET configuration for my child — it will GET this in its own build_phase
    uvm_config_db#(agent_cfg)::set(this, "agent", "cfg", cfg.agent_cfg);
 
    // 3. CREATE the child via the FACTORY — type_id::create, NOT new (so it's overridable)
    agent = my_agent::type_id::create("agent", this);
  endfunction
endclass

Each line is one of the three jobs. The get retrieves env_cfg that this env's parent (the test) set before this build_phase ran — possible only because build is top-down. The set places agent_cfg scoped to the child "agent", which the agent will get in its own build_phase (which runs after this one). The create constructs the agent through the factory with type_id::create("agent", this) — passing this as the parent (ownership, Module 4.7) and routing through the factory so the agent's type can be overridden from above. Note super.build_phase(phase) first (base-class setup) and that the get is guarded with uvm_fatal so a missing config is reported, not silently null. This same three-job pattern appears in every component's build_phase, scaled to its children.

Verification Perspective — top-down, and why the factory matters

Two properties make build_phase work: it runs top-down (so configuration flows down as the tree is built), and it creates components through the factory (so types are substitutable). The factory is the one that's easy to get wrong.

Top-down build: env sets and creates agent, agent sets and creates driver; type_id::create consults the factory which may return an overridden typeset cfg + create (factory)set cfg +create…set cfg + create (factory)set cfg +create…type_id::createregistered or overridden typeregistered oroverridden…env.build_phase#1 (top-down)agent.build_phase#2driver#3 createdtype_id::createroutes through factoryFactoryreturns registered / overriddentype12
Figure 2 — top-down build with configuration flowing down, and the factory enabling overrides. As build_phase runs top-down (env, then agent, then driver), each parent sets config that the child gets when it builds next. Creation goes through the factory: type_id::create consults the factory, which returns the registered type — or an overridden type if the test registered one (e.g., an error-injecting driver). Using new instead would bypass the factory and ignore any override.

The two properties combine to give build_phase its power. Top-down means configuration flows down naturally: each parent sets its children's config, and because the parent builds first, that config is in the database when the child builds and gets it — no manual ordering needed. The factory means every type_id::create consults the factory rather than hard-constructing a type: by default it returns the registered type, but if a test has registered an override (e.g., "build an error_driver everywhere a my_driver would be built"), the factory returns the derived type instead — and the environment didn't change at all. This is the mechanism behind test-specific specialisation (Module 3.5). Using new instead of type_id::create hard-wires the type and bypasses the factory, so overrides silently do nothing — which is exactly the DebugLab. Top-down for configuration, the factory for substitutability: both live in build_phase.

Runtime / Execution Flow — the build cascade

build_phase executes as a single top-down cascade: the framework calls it on the root, which creates its children, then on each of those children, which create their children, and so on, until the leaves. Configuration set by each parent is read by each child as the cascade reaches it.

build_phase cascade: test builds env, env builds agent, agent builds driver/monitor, configuration flowing down at each stepThe top-down build cascadeThe top-down build cascade1test.build_phasesets env config; creates the env via the factory.2env.build_phasegets the config the test set; sets agent config; creates the agent.3agent.build_phasegets the config the env set; sets driver/monitor config; createsthem.4driver/monitor.build_phasegets the config the agent set; (leaves — no children to create).Tree complete.
Figure 3 — the build_phase cascade. The framework calls build_phase top-down: the test builds first (gets its config, sets and creates the env), then the env (gets the config the test set, sets and creates the agent), then the agent (gets the config the env set, sets and creates the driver/monitor), then the leaves. Each level's set is read by the next level's get, because the parent always builds before the child. One top-down pass constructs and configures the whole tree.

The cascade is why configuration "just works" when set from above. At each step, a parent sets config and creates a child; when the framework then calls that child's build_phase, the child gets the config the parent just set — and this lines up perfectly because the parent's build_phase always completes before the child's begins (top-down). So configuration set by the test reaches the env, config set by the env reaches the agent, config set by the agent reaches the driver — a clean downward flow, established in one pass, with no manual sequencing. The cascade also explains a timing subtlety from Module 4.7: a parent cannot reference a grandchild during its own build_phase, because the grandchild isn't created until the child's build_phase runs later in the cascade — which is why cross-level wiring waits for connect_phase. Build cascades down, configuration rides along, and the tree is complete when the cascade reaches the leaves.

Waveform Perspective — build completes at time zero, before the run

build_phase is zero-time: the entire top-down cascade — every get, set, and create — completes at time zero, before the first clock edge matters. The waveform shows the build region collapsed at the start, with all pin activity confined to the later run phase.

build_phase completes at time zero, top-down, before run_phase drives anything

10 cycles
build_phase completes at time zero, top-down, before run_phase drives anythingbuild_phase (zero time): env builds, then agent, then driver — top-down cascadebuild_phase (zero time…each parent gets its config, creates children via the factory, sets their configeach parent gets its c…tree complete → run_phase begins and time advances (pins move)tree complete → run_ph…clkbuildingphaseBLDBLDRUNRUNRUNRUNRUNRUNRUNRUNvaliddata0000A0A100B0B1000000t0t1t2t3t4t5t6t7t8t9
Figure 4 — build_phase (the building region) executes the entire top-down cascade at time zero: env builds, then agent, then driver — each getting config, setting children's config, and creating children — with no simulation time elapsed and no pin activity. Only when the tree is fully built and (connect done) does run_phase begin and the pins move. The whole construct-and-configure pass is instantaneous; behaviour comes later, in the run phase.

The building region collapsed at time zero is the point: the entire construct-and-configure cascade — possibly dozens of components, every get/set/create — happens in an instant of zero simulation time, before any pin moves. This is what "zero-time function phase" means concretely: build_phase is a function, so it cannot consume time, and the whole tree springs into existence at time zero. The pins stay flat through the build region and only move once run_phase begins, because behaviour belongs to the run phase. Reading this waveform reinforces the schedule from Module 5.1: build (and the other construction phases) are an instantaneous setup sliver, and all of simulation time is the run phase that follows. The build cascade is fast not because it's small, but because it's zero-time by definition.

DebugLab — the factory override that did nothing

An error-injecting driver that was never built — created with new instead of the factory

Symptom

A test needed to inject errors, so it registered a factory override: "everywhere a my_driver would be built, build an err_driver instead" (my_driver::type_id::set_type_override(err_driver::get_type())). The override had no effect — the simulation ran with the normal my_driver, no errors were injected, and the test behaved exactly like the non-error test. The override was correctly registered, yet completely ignored.

Root cause

The driver was created with new instead of the factory, so construction bypassed the factory and the override never applied:

why the factory override was ignored
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
agent.build_phase:  drv = my_driver::new("drv", this);          // bypasses the factory ✗
test registered:    my_driver::type_id::set_type_override(err_driver::get_type());
factory override:   only applies to construction via type_id::create(...)
result:             new("drv") hard-constructs my_driver directly → override ignored → no err_driver
fix:                drv = my_driver::type_id::create("drv", this);   // route through the factory

A factory override works by intercepting type_id::create and returning the overriding type. When a component is built with new (or by directly calling the constructor), construction never consults the factory, so no override — type override or instance override — can take effect. The override was registered correctly; the creation simply didn't go through the factory that the override lives in.

Diagnosis

The tell is a registered factory override that has no effect — almost always a creation that bypassed the factory. Diagnose override problems at the creation site:

  1. Check how the component is created. It must be type::type_id::create("name", this). If it's type::new(...) or a direct constructor call, the factory (and any override) is bypassed. This is the first thing to inspect when an override does nothing.
  2. Confirm the type is registered. The component must use `uvm_component_utils (or `uvm_object_utils) so it has a type_id and is known to the factory. Without registration, type_id::create won't resolve.
  3. Match the override target to the created type. The override must name the exact type being created (and, for instance overrides, the right path). A mismatch means the create resolves to the original type.
Prevention

Always create through the factory, never with new:

  1. Use type_id::create for every component and object. type::type_id::create("name", this) for components, type::type_id::create("name") for objects — never new. This routes construction through the factory so overrides can apply.
  2. Register every class with the utils macro. `uvm_component_utils / `uvm_object_utils give the class a type_id and register it; without it, the factory can't create or override it.
  3. Verify overrides take effect. When you register an override, confirm the substituted type is actually built (print the topology or the type name) — an override silently ignored is the signature of a new somewhere.

The one-sentence lesson: factory overrides only apply to components created with type_id::create; a component built with new bypasses the factory, so its overrides are silently ignored — always create through the factory.

Common Mistakes

  • Creating components with new instead of type_id::create. new bypasses the factory, so type and instance overrides are silently ignored. Always create components and objects via type_id::create so they're substitutable.
  • Forgetting super.build_phase(phase). Omitting the super call can skip base-class build work. Call super.build_phase(phase) in overridden build methods.
  • Setting a child's config in the wrong scope. set(this, "agent", "cfg", ...) must name a scope that reaches the child; a wrong scope means the child's get misses it and it comes up unconfigured (and the path must be relative to this, Module 3.5).
  • Trying to reference a grandchild in build. A parent's build_phase runs before its children's, so a grandchild (a child's child) doesn't exist yet — cross-level references belong in connect_phase (Module 4.7).
  • Doing time-consuming work in build. build_phase is a zero-time function; waiting or driving belongs in run_phase (Module 5.1). Build only constructs and configures.
  • Not guarding get. An unguarded uvm_config_db::get that fails leaves the handle null; guard it with uvm_fatal so a missing config is reported at build, not as a crash later.

Senior Design Review Notes

Interview Insights

build_phase is the top-down construction phase where the component tree is built, and each component does three jobs. First, it gets its own configuration from the config database — configuration its parent set before this component's build ran (possible because build is top-down). Second, it sets configuration for the children it's about to create, which those children will get in their own (later) build_phase. Third, it creates its children via the factory using type_id::create("name", this) — not new — passing itself as the parent so the child is placed in the tree, and routing through the factory so the child's type can be overridden from above. This same three-job pattern runs at every level, top-down: the test builds the env, the env builds the agent, the agent builds the driver and monitor, with configuration flowing down as the cascade descends. By the end of the phase the entire tree exists and is configured. build_phase is a zero-time function, so it does only construction and configuration — no waiting, no driving, and no connections (those happen in connect_phase).

Exercises

  1. The three jobs. Write a build_phase for an env that has one agent: show the get of its own config, the set of the agent's config, and the create of the agent — with the correct factory call and parent.
  2. Spot the bypass. A test's factory override has no effect. Name the most likely cause at the creation site, and the one-line change that fixes it.
  3. Order and timing. Explain why a parent can set a child's config in build_phase and trust the child will get it, citing the top-down order. Then explain why the parent cannot reference a grandchild in the same phase.
  4. Function constraints. State two things you must not do in build_phase because it is a zero-time function, and where each belongs instead.

Summary

  • build_phase is the top-down construction phase, and each component does three jobs: get its own configuration (set by its parent), set configuration for its children (read in their build), and create its children via the factory (type_id::create, never new).
  • It runs top-down — parent before child — which is what makes configure-from-above work: a parent sets config and creates a child, and the child gets that config when its (later) build runs. The whole tree is constructed and configured in one downward cascade.
  • Components must be created with the factory (type_id::create), not new, so their types can be overridden from above; creating with new bypasses the factory and silently ignores overrides — the classic "override does nothing" bug.
  • build_phase is a zero-time function: it only constructs and configures (no waiting, driving, connecting, or referencing not-yet-built grandchildren), and the entire cascade completes at time zero before the run phase.
  • The durable rule of thumb: in build_phase, get your config, set your children's config, and create them through the factory with type_id::create — top-down construction threads configuration down the tree, and the factory keeps every type substitutable, so build the tree this way and it's both correct and overridable.

Next — connect_phase: the tree is built; the next phase wires it. connect_phase runs bottom-up after build, and is where TLM ports are connected and coordination handles assigned — establishing the data-flow graph over the structure that build_phase just created.