The UVM Phasing Mechanism
Learn how UVM phases structure the lifecycle of a testbench, from construction to execution and cleanup.
How to Read This Lesson
UVM-SystemC is methodology in C++ clothing. Keep the verification intent in view: reusable components, controlled stimulus, reporting, and phase-aware execution.
In a standard SystemC simulation, you have elaboration (construction) and simulation (sc_start()). The Universal Verification Methodology (UVM) imposes a much more rigorous, standardized execution schedule on top of SystemC called the Phasing Mechanism.
Phases ensure that all components in the verification environment instantiate, connect, run, and shut down in a predictable, synchronized manner.
Source and LRM Trail
For UVM-SystemC, use Docs/LRMs/uvm-systemc-language-reference-manual.pdf as the methodology contract. In source, inspect .codex-src/uvm-systemc/src/uvmsc: components, phases, factory macros, sequences, sequencers, TLM ports, reporting, and configuration helpers.
The Three Categories of UVM Phases
Phases in UVM are executed sequentially. Every component in the hierarchy must complete a phase before the entire environment transitions to the next phase. The phases are divided into three main categories: Pre-run, Run-time, and Post-run.
Under the Hood: The C++ Implementation in Accellera UVM-SystemC
How does the UVM kernel orchestrate these phases across the entire uvm_component tree? If you look inside the uvm-systemc repository, phasing is implemented as a sophisticated State Machine.
uvm_phaseclasses: Every phase is represented by an object inheriting fromuvm_phase(which itself derives fromuvm_object). The kernel maintains a graph (domain) of these phase nodes.- Traversal Strategies: For zero-time pre-run phases, the kernel executes a graph traversal. Pre-run phases map directly to SystemC's
end_of_elaborationandstart_of_simulationcallbacks. Classes likeuvm_topdown_phase(forbuild_phase) anduvm_bottomup_phase(forconnect_phase) determine the order in which the kernel iterates over theuvm_rootcomponent hierarchy. - Run-Time Threads (
sc_spawn): When the simulation transitions into therun_phase, it enters the time-consuming domain. Under the hood, the UVM kernel calls SystemC's dynamic process generationsc_spawn()to launch therun_phase()of each component as an independent, concurrentSC_THREAD. Because they are standard SystemC threads, you can freely usesc_core::wait()to suspend execution.
1. Pre-run Phases (Zero Time)
Pre-run phases are used for structural setup. They execute in zero simulation time. In UVM-SystemC, these map conceptually to SystemC's elaboration steps.
build_phase(Top-down): This is where you instantiate your sub-components and retrieve configuration settings from theuvm_config_db. Because it executes top-down, parents can set configurations before their children are built.connect_phase(Bottom-up): Once all components are built, TLM ports and exports are bound together here.end_of_elaboration_phase(Bottom-up): Final structural adjustments and topology checks.start_of_simulation_phase(Bottom-up): Pre-run activities like printing banners, dumping the testbench topology, or initializing debug files.
2. Run-time Phases (Consumes Time)
Run-time phases are where the actual simulation stimulus and protocol execution happen.
run_phase: This is the primary workhorse. Unlike the pre-run phases,run_phaseis spawned as a concurrent thread process (using SystemC'ssc_spawnunder the hood). Every component'srun_phaseexecutes concurrently.
UVM also defines parallel sub-phases within the run-time domain (such as reset_phase, configure_phase, main_phase, and shutdown_phase), but run_phase is the most commonly used for general component logic.
3. Post-run Phases (Zero Time)
Once the run-time phases are explicitly terminated, the simulation moves into cleanup and checking.
extract_phase(Bottom-up): Retrieve final data from coverage collectors and scoreboards.check_phase(Bottom-up): Validate the extracted data to determine if the test passed or failed.report_phase(Bottom-up): Print the final results (e.g., "TEST PASSED" or coverage percentages).final_phase(Top-down): Final teardown, like closing open file handles.
Implementing a Phase
To participate in a phase, a component simply overrides the virtual method for that phase. Below is a complete, fully compilable example demonstrating all three categories of UVM phases.
#include <systemc>
#include <uvm>
class my_transaction : public uvm::uvm_transaction {
public:
UVM_OBJECT_UTILS(my_transaction);
my_transaction(const std::string& name = "my_transaction") : uvm::uvm_transaction(name) {}
};
class my_monitor : public uvm::uvm_monitor {
public:
UVM_COMPONENT_UTILS(my_monitor);
uvm::uvm_analysis_port<my_transaction> ap;
my_monitor(uvm::uvm_component_name name) : uvm::uvm_monitor(name), ap("ap") {}
// Pre-run: Construction
void build_phase(uvm::uvm_phase& phase) override {
uvm::uvm_monitor::build_phase(phase);
UVM_INFO("MON", "Building monitor...", uvm::UVM_LOW);
}
// Run-time: Execution (consumes time)
void run_phase(uvm::uvm_phase& phase) override {
// Objections control when the simulation finishes
phase.raise_objection(this);
for(int i = 0; i < 3; i++) {
// Wait for simulated time to pass
sc_core::wait(10, sc_core::SC_NS);
UVM_INFO("MON", "Sampling bus...", uvm::UVM_LOW);
// Broadcast dummy transaction
my_transaction tx;
ap.write(tx);
}
phase.drop_objection(this);
}
// Post-run: Cleanup
void report_phase(uvm::uvm_phase& phase) override {
UVM_INFO("MON", "Simulation finished successfully.", uvm::UVM_LOW);
}
};
class my_test : public uvm::uvm_test {
public:
my_monitor* mon;
UVM_COMPONENT_UTILS(my_test);
my_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}
void build_phase(uvm::uvm_phase& phase) override {
uvm::uvm_test::build_phase(phase);
mon = my_monitor::type_id::create("mon", this);
}
};
int sc_main(int argc, char* argv[]) {
uvm::run_test("my_test");
return 0;
}Controlling the Run Phase: Objections
Because run_phase executes concurrently across many components, UVM needs a way to know when the test is "done." If it waited for all run_phase threads to exit, simulations might run forever (due to infinite while(true) loops in monitors and drivers).
The C++ Objection Implementation
In the Accellera implementation, uvm_objection acts as a distributed reference counter.
- When you call
phase.raise_objection(this), the global objection counter increments. - The
uvm_phasestate machine is blocked from transitioning out of therun_phaseas long asm_objection_count > 0. - When
phase.drop_objection(this)is called and the counter hits zero, it triggers a system-widedropped()callback. The phase state machine then automatically kills thesc_spawn'd threads and transitions into theextract_phase.
UVM solves this with Objections.
raise_objection(): "I am busy executing the test, do not end the simulation."drop_objection(): "I am done with my part of the test."
The run_phase (and the entire simulation) ends when all raised objections have been dropped. This logic is typically handled in the uvm_test or inside a uvm_sequence.
By standardizing when things happen (phases) and how we agree to finish (objections), UVM creates highly deterministic and predictable testbenches out of independent, modular components.
Comments and Corrections