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 withtype_id::createrather thannew?
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_phaseruns before its children's, so a parent cansetconfiguration into the database that its childrengetwhen 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::createroutes 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. Usingnewbypasses 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_phaseis 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 (getyour configuration from the database — your manager set it before you started). Second, you hire your team (createyour 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 (settheir 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.
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.
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
endclassEach 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.
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.
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 cyclesThe 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
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.
The driver was created with new instead of the factory, so construction bypassed the factory and the override never applied:
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 factoryA 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.
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:
- Check how the component is created. It must be
type::type_id::create("name", this). If it'stype::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. - Confirm the type is registered. The component must use
`uvm_component_utils(or`uvm_object_utils) so it has atype_idand is known to the factory. Without registration,type_id::createwon't resolve. - 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.
Always create through the factory, never with new:
- Use
type_id::createfor every component and object.type::type_id::create("name", this)for components,type::type_id::create("name")for objects — nevernew. This routes construction through the factory so overrides can apply. - Register every class with the utils macro.
`uvm_component_utils/`uvm_object_utilsgive the class atype_idand register it; without it, the factory can't create or override it. - 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
newsomewhere.
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
newinstead oftype_id::create.newbypasses the factory, so type and instance overrides are silently ignored. Always create components and objects viatype_id::createso they're substitutable. - Forgetting
super.build_phase(phase). Omitting thesupercall can skip base-class build work. Callsuper.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'sgetmisses it and it comes up unconfigured (and the path must be relative tothis, Module 3.5). - Trying to reference a grandchild in build. A parent's
build_phaseruns before its children's, so a grandchild (a child's child) doesn't exist yet — cross-level references belong inconnect_phase(Module 4.7). - Doing time-consuming work in build.
build_phaseis a zero-time function; waiting or driving belongs inrun_phase(Module 5.1). Build only constructs and configures. - Not guarding
get. An unguardeduvm_config_db::getthat fails leaves the handle null; guard it withuvm_fatalso 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
- The three jobs. Write a
build_phasefor 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. - 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.
- 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.
- Function constraints. State two things you must not do in
build_phasebecause it is a zero-time function, and where each belongs instead.
Summary
build_phaseis 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, nevernew).- 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), notnew, so their types can be overridden from above; creating withnewbypasses the factory and silently ignores overrides — the classic "override does nothing" bug. build_phaseis 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 withtype_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.