Chapter 10: UVM-SystemC

The UVM Phasing Mechanism

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

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.

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.

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).

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