← Back to the series

Verification, From First Principles — Part 7 of 10

How a UVM Test Actually Runs

Part 6 named the components. This part traces what actually happens, in what order, from the moment a UVM test starts to the moment it reports pass or fail.

Every component in a UVM testbench is a uvm_component, and every one of them goes through the same sequence of phase methods, in the same order, automatically — you never call these yourself. Knowing this order is what makes a UVM log file readable instead of a wall of noise.

Build-time phases: assembling the testbench

  1. build_phase — runs top-down (env before its agents, agents before their driver/monitor/sequencer). This is where each component creates its children, usually via the factory (my_driver::type_id::create(...)) rather than plain new(), so that a test can later swap in a different implementation without touching the environment's code.
  2. connect_phase — runs bottom-up. By now every component exists, so this is where TLM ports get connected — a driver's seq_item_port to its sequencer's export, a monitor's analysis port to a scoreboard's analysis import.
  3. end_of_elaboration_phase / start_of_simulation_phase — last chances to tweak configuration or print a testbench topology before any time-consuming activity begins.

run_phase: where simulated time actually passes

Every other phase executes instantly, in zero simulation time. run_phase is different — it's a task, not a function, meaning it can consume simulation time (waiting for clock edges, waiting for a signal to change). Every component's run_phase starts at the same simulated time and runs concurrently: the driver's run_phase is pulling and driving items while the monitor's run_phase is simultaneously watching pins, for as long as the phase stays alive.

Because run_phase runs in parallel across components with no obvious single line that means "we're done," UVM needs an explicit mechanism to decide when to end it: objections. A component calls phase.raise_objection(this) to say "don't end run_phase yet, I still have work in flight," and phase.drop_objection(this) once that work is done. run_phase ends only once every outstanding objection has been dropped.

my_test.sv — illustrative, not runnable as-is class my_test extends uvm_test; `uvm_component_utils(my_test) my_env env; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); env = my_env::type_id::create("env", this); endfunction task run_phase(uvm_phase phase); my_seq seq; phase.raise_objection(this); seq = my_seq::type_id::create("seq"); seq.start(env.agent.sqr); phase.drop_objection(this); endtask endclass

This is exactly why a UVM test with a missing drop_objection call hangs forever — the simulator is correctly waiting, because something is still telling it "not done yet."

Wind-down phases: did it pass?

  1. extract_phase — pull final state out of components (counts, collected data) before anything gets checked.
  2. check_phase — where components report any final pass/fail conclusions — a scoreboard might check here that its internal queues ended up empty, for instance.
  3. report_phase — where the final summary is printed: how many UVM_ERROR and UVM_FATAL messages were logged during the run. This count, not a human reading the log, is usually what a regression script checks to decide pass or fail.

Takeaway

build (top-down) then connect (bottom-up) assemble the testbench in zero time. run_phase is the only phase where simulated time passes, and it ends only when every raised objection has been dropped. Everything after that reports what happened. Once this order is second nature, any UVM log becomes readable.

With the mechanics in place, the rest of this series applies them to one real DUT: an asynchronous FIFO. Part 8 starts with the theory of what makes it hard to verify in the first place.