Chapter 10: UVM-SystemC

UVM-SystemC Bridge: Verification Architecture

How UVM-SystemC layers standard Universal Verification Methodology on top of the SystemC simulation kernel.

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.

UVM-SystemC Bridge: Verification Architecture

UVM-SystemC brings the Universal Verification Methodology (UVM)—the gold standard for hardware verification in SystemVerilog—directly into the C++ SystemC ecosystem.

While SystemC provides the fundamental discrete-event simulation kernel and modeling paradigms (like TLM), UVM-SystemC provides the verification structure. It introduces standardized architectures for testbenches, phasing, configuration, and reporting, ensuring that verification environments are highly reusable and predictable.

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 UVM-SystemC Architecture

According to the Accellera UVM-SystemC Language Reference Manual, the methodology is built on a few core pillars:

  1. uvm_object: The base class for dynamic, transient data (like transactions and sequences).
  2. uvm_component: The base class for structural, permanent hierarchy (like drivers, monitors, and scoreboards).
  3. Phasing: A standardized execution flow (build_phase, connect_phase, run_phase, etc.) that coordinates the entire testbench.
  4. The Factory: A mechanism allowing types to be overridden at runtime, enabling tests to replace generic transactions or components with specialized ones without touching the original source code.
  5. Configuration DB: A centralized database (uvm_config_db) for passing parameters and interface handles down the component hierarchy.

Typical Component Hierarchy

A UVM-SystemC environment strictly separates the Design Under Test (DUT) from the verification logic. The testbench hierarchy typically looks like this:

  • uvm_test: The top-level block. It configures the environment and starts the stimulus (sequences).
    • uvm_env: The environment grouping agents and scoreboards.
      • uvm_agent: A reusable block encapsulating a specific protocol (e.g., AXI, UART).
        • uvm_sequencer: Arbitrates and feeds transactions to the driver.
        • uvm_driver: Translates transactions into pin wiggles or TLM calls.
        • uvm_monitor: Observes the bus and publishes observed transactions.
      • uvm_scoreboard: Compares observed transactions against expected behavior.

Complete Example: The Top-Level UVM Architecture

The following complete, compilable example demonstrates the absolute baseline architecture of a UVM-SystemC simulation. It defines a component hierarchy, implements the standard UVM phases, and uses uvm::run_test() to bootstrap the verification environment inside sc_main.

#include <systemc>
#include <uvm>
 
// 1. A dummy Driver Component
class my_driver : public uvm::uvm_driver<uvm::uvm_sequence_item> {
public:
    UVM_COMPONENT_UTILS(my_driver); // Register with the UVM factory
 
    my_driver(uvm::uvm_component_name name) : uvm::uvm_driver<uvm::uvm_sequence_item>(name) {}
 
    // The run_phase consumes time and drives the simulation
    void run_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("DRIVER", "Driver is starting execution...", uvm::UVM_LOW);
        
        // Raise an objection to prevent the simulation from ending
        phase.raise_objection(this);
        
        sc_core::wait(100, sc_core::SC_NS);
        UVM_INFO("DRIVER", "Driving transaction 1...", uvm::UVM_LOW);
        
        sc_core::wait(100, sc_core::SC_NS);
        UVM_INFO("DRIVER", "Driving transaction 2...", uvm::UVM_LOW);
        
        // Drop objection to allow simulation to finish
        phase.drop_objection(this);
    }
};
 
// 2. An Agent grouping the driver
class my_agent : public uvm::uvm_agent {
public:
    UVM_COMPONENT_UTILS(my_agent);
 
    my_driver* driver;
 
    my_agent(uvm::uvm_component_name name) : uvm::uvm_agent(name), driver(nullptr) {}
 
    // The build_phase constructs children top-down
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_agent::build_phase(phase);
        UVM_INFO("AGENT", "Building driver...", uvm::UVM_MEDIUM);
        
        // Create the driver using the UVM Factory
        driver = my_driver::type_id::create("driver", this);
    }
};
 
// 3. The Environment grouping agents and scoreboards
class my_env : public uvm::uvm_env {
public:
    UVM_COMPONENT_UTILS(my_env);
 
    my_agent* agent;
 
    my_env(uvm::uvm_component_name name) : uvm::uvm_env(name), agent(nullptr) {}
 
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_env::build_phase(phase);
        UVM_INFO("ENV", "Building agent...", uvm::UVM_MEDIUM);
        agent = my_agent::type_id::create("agent", this);
    }
};
 
// 4. The Top-Level Test
class my_test : public uvm::uvm_test {
public:
    UVM_COMPONENT_UTILS(my_test);
 
    my_env* env;
 
    my_test(uvm::uvm_component_name name) : uvm::uvm_test(name), env(nullptr) {}
 
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_test::build_phase(phase);
        UVM_INFO("TEST", "Building environment...", uvm::UVM_MEDIUM);
        env = my_env::type_id::create("env", this);
    }
    
    // Check phase runs after simulation finishes
    void check_phase(uvm::uvm_phase& phase) override {
        UVM_INFO("TEST", "Performing final end-of-test checks.", uvm::UVM_LOW);
    }
};
 
// 5. The sc_main Entry Point
int sc_main(int argc, char* argv[]) {
    // Instead of explicitly instantiating components and calling sc_start(),
    // UVM-SystemC takes control of the simulation execution.
    
    // run_test() automatically creates the test component specified by 
    // the +UVM_TESTNAME command-line argument (or passed dynamically).
    // It then automatically executes the UVM phases and invokes sc_start() internally.
    uvm::run_test("my_test");
 
    return 0;
}

Key Differences from Pure SystemC

  1. Instantiation: You do not use standard C++ new or SC_MODULE constructors for the hierarchy. Instead, components are instantiated in the build_phase using the factory (type_id::create). This is what makes UVM reusable.
  2. Execution Control: sc_start() is completely hidden. uvm::run_test() manages the SystemC kernel for you.
  3. Objections: The simulation stops automatically when all run_phase objections are dropped, rather than running indefinitely or stopping via a hardcoded time limit.

Under the Hood: run_test and UVM_COMPONENT_UTILS

While UVM-SystemC abstractly mirrors SystemVerilog UVM, it heavily relies on C++ metaprogramming to bridge into the SystemC kernel.

When you call uvm::run_test("my_test"), the UVM-SystemC library fetches the singleton uvm_root::get(). uvm_root is a specialized uvm_component that serves as the invisible top-level module (the parent of my_test). Inside run_test(), the core library queries the UVM factory to instantiate the test string provided. It then manually invokes sc_core::sc_start() internally. During the run_phase, UVM-SystemC spawns threads (SC_THREAD) for every component's run_phase() method. When the global objection count hits zero, uvm_root calls sc_core::sc_stop().

The magic of the factory is implemented through the UVM_COMPONENT_UTILS(T) macro. In the uvm-systemc repository, this macro expands to declare a nested type_id struct and a static factory registration proxy. At C++ static initialization time (before main even starts), this proxy object registers a string-to-creator mapping in the global uvm_factory. This is why you can call type_id::create("name") and receive a dynamically allocated polymorphic instance without ever hardcoding the new keyword.

In the next tutorial, we will explore the uvm_object, the factory, and how to write reusable transaction data structures.

Comments and Corrections