Chapter 10: UVM-SystemC

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.

  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.

Comments and Corrections