Chapter 10: UVM-SystemC

System Verification, SCV & UVM-SystemC

Moving from simple testbenches to robust verification using the SystemC Verification Library (SCV) and UVM-SystemC.

System Verification & UVM-SystemC

When building simple models, a basic sc_main with a few std::cout statements and assert() calls is sufficient. However, for industrial System-on-Chips (SoCs), you need robust verification environments capable of constrained random stimulus, functional coverage, and automated checking.

This is where the SystemC Verification Library (SCV) and the IEEE standard UVM-SystemC come into play.

The SystemC Verification Library (SCV)

SCV is an Accellera standard library built on top of SystemC. It provides three main features:

  1. Data Introspection: Dynamically inspecting the fields of a C++ struct or class.
  2. Constrained Randomization: Generating random data that mathematically adheres to complex hardware constraints.
  3. Transaction Recording: Automatically dumping TLM generic payloads to a database for waveform viewing (like GTKWave).

Complete SCV Constrained Randomization Example

In standard C++, rand() generates uniform distributions. SCV introduces scv_smart_ptr and scv_constraint to build powerful declarative generators similar to SystemVerilog's randc.

#include <systemc>
#include <scv.h>
 
// 1. Define a standard C++ struct representing a Bus Packet
struct Packet {
    int address;
    int payload[4];
};
 
// 2. Define introspection (so SCV knows the memory layout)
SCV_EXTENSIONS(Packet) {
public:
    scv_extensions<int> address;
    scv_extensions<int[4]> payload;
    SCV_EXTENSIONS_CTOR(Packet) {
        SCV_FIELD(address);
        SCV_FIELD(payload);
    }
};
 
// 3. Create a constraint class
class PacketConstraint : public scv_constraint_base {
public:
    scv_smart_ptr<Packet> p;
 
    SCV_CONSTRAINT_CTOR(PacketConstraint) {
        // Constrain address between 0x1000 and 0x2000
        SCV_CONSTRAINT( p->address() >= 0x1000 && p->address() <= 0x2000 );
        
        // Constrain payload values to be strictly positive
        for(int i = 0; i < 4; i++) {
            SCV_CONSTRAINT( p->payload()[i] > 0 );
        }
    }
};
 
SC_MODULE(Testbench) {
    SC_CTOR(Testbench) {
        SC_THREAD(generator_thread);
    }
 
    void generator_thread() {
        PacketConstraint gen("gen");
        std::cout << "--- Generating Constrained Random Packets ---" << std::endl;
        
        for(int i = 0; i < 5; i++) {
            gen.next(); // Solves constraints and generates a new packet
            std::cout << "Packet " << i << " | Addr: 0x" << std::hex << gen.p->address() 
                      << " | Payload[0]: " << std::dec << gen.p->payload()[0] << std::endl;
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    Testbench tb("tb");
    sc_core::sc_start();
    return 0;
}

UVM-SystemC

The Universal Verification Methodology (UVM) is the industry standard for verifying RTL logic using SystemVerilog. UVM-SystemC is an Accellera standard that ports the entire UVM architecture (phases, components, configuration databases, TLM connections) to standard C++.

Why use UVM-SystemC?

  1. Reusability via DPI-C: You can build a UVM-SystemC testbench to verify your high-level C++ Virtual Platform. Later, when the RTL is written in Verilog, you can reuse the exact same C++ testbench via SystemVerilog DPI-C to verify the final RTL.
  2. Standardization: Verification engineers already know UVM. UVM-SystemC allows them to apply their SystemVerilog knowledge directly to C++ ESL models using a nearly identical API.

UVM-SystemC Architecture Example

Just like SystemVerilog UVM, you build components derived from standard base classes like uvm_driver, uvm_monitor, and uvm_scoreboard. The phasing mechanism (run_phase) replaces standard SC_THREAD execution.

#include <systemc>
#include <uvm>
 
class MyTransaction : public uvm::uvm_sequence_item {
public:
    int data;
    UVM_OBJECT_UTILS(MyTransaction);
    MyTransaction(const std::string& name = "MyTransaction") : uvm::uvm_sequence_item(name), data(0) {}
};
 
class MyDriver : public uvm::uvm_driver<MyTransaction> {
public:
    UVM_COMPONENT_UTILS(MyDriver);
 
    MyDriver(uvm::uvm_component_name name) : uvm::uvm_driver<MyTransaction>(name) {}
 
    // The run phase executes as a coroutine (like SC_THREAD)
    void run_phase(uvm::uvm_phase& phase) {
        while(true) {
            MyTransaction req;
            
            // In a real environment, this blocks until a sequencer sends an item
            // seq_item_port->get_next_item(req);
            
            UVM_INFO("DRIVER", "Driving transaction to DUT...", uvm::UVM_LOW);
            wait(10, sc_core::SC_NS); // Simulate driving delay on the bus
 
            // seq_item_port->item_done();
            
            // Break to avoid infinite loop in this mock example
            break; 
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    // In a real UVM environment, you would call uvm::run_test();
    // This example isolates the UVM driver for structural clarity.
    
    MyDriver driver("driver");
    sc_core::sc_start(50, sc_core::SC_NS);
    return 0;
}

Summary

If you are building a quick prototype, standard sc_main C++ testing is fine. But for production-grade IP modeling, leveraging SCV for constrained randomization and UVM-SystemC for hierarchical testbench architecture ensures your models are rigorously verified and interoperable across the hardware engineering lifecycle.

Comments and Corrections