Chapter 10: UVM-SystemC

The UVM Phasing Mechanism

Learn how UVM phases structure the lifecycle of a testbench, from construction to execution and cleanup.

Listen to this lessonAudiobook mode

How to Read This Lesson

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.

Standard and source context

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.

  1. uvm_phase classes: Every phase is represented by an object inheriting from uvm_phase (which itself derives from uvm_object). The kernel maintains a graph (domain) of these phase nodes.
  2. Traversal Strategies: For zero-time pre-run phases, the kernel executes a graph traversal. Pre-run phases map directly to SystemC's end_of_elaboration and start_of_simulation callbacks. Classes like uvm_topdown_phase (for build_phase) and uvm_bottomup_phase (for connect_phase) determine the order in which the kernel iterates over the uvm_root component hierarchy.
  3. Run-Time Threads (sc_spawn): When the simulation transitions into the run_phase, it enters the time-consuming domain. Under the hood, the UVM kernel calls SystemC's dynamic process generation sc_spawn() to launch the run_phase() of each component as an independent, concurrent SC_THREAD. Because they are standard SystemC threads, you can freely use sc_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 the uvm_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_phase is spawned as a concurrent thread process (using SystemC's sc_spawn under the hood). Every component's run_phase executes 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.

  1. When you call phase.raise_objection(this), the global objection counter increments.
  2. The uvm_phase state machine is blocked from transitioning out of the run_phase as long as m_objection_count > 0.
  3. When phase.drop_objection(this) is called and the counter hits zero, it triggers a system-wide dropped() callback. The phase state machine then automatically kills the sc_spawn'd threads and transitions into the extract_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.

Accellera Source Implementation: uvm_phase::execute

How does the UVM phasing engine actually run the phases? In src/uvmsc/phasing/uvm_phase.cpp, the execute() method is the heart of the state machine.

// Abstract representation of uvm_phase::execute
void uvm_phase::execute(uvm_component* comp) {
    // 1. Wait for phase to start
    m_phase_ready.wait();
    
    // 2. Call the user's virtual method
    if(this->get_name() == "build") {
        comp->build_phase(this);
    } else if(this->get_name() == "run") {
        // Run phase is a task (SC_THREAD)
        sc_core::sc_spawn(sc_bind(&uvm_component::run_phase, comp, this));
    }
    
    // 3. Wait for objections to drop before ending
    uvm_objection* obj = this->get_objection();
    obj->wait_for_zero();
    
    // 4. Transition to next phase
    m_phase_done.notify();
}

Notice that the run_phase explicitly uses sc_spawn to fork an independent OS thread in the SystemC kernel, which is why it consumes simulation time, unlike the bottom-up function phases.

Lesson self-check

Can you answer these clearly?

Keep moving when you can answer each question without looking back at the lesson.

Comments and Corrections