Chapter 10: UVM-SystemC

TLM Communication in UVM

Understand how UVM components communicate transaction objects using Transaction Level Modeling (TLM) ports and exports.

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.

A core philosophy of UVM is that verification components should be modular and highly reusable. If a driver accesses a monitor's variables directly, they become tightly coupled. To enforce loose coupling, UVM relies entirely on Transaction Level Modeling (TLM) for inter-component communication.

UVM-SystemC builds its TLM capabilities directly on top of the IEEE 1666 SystemC TLM standard. The IEEE 1800.2 UVM standard outlines three primary types of TLM interfaces used in verification: unidirectional point-to-point (put/get), bidirectional point-to-point (transport/req-rsp), and broadcast (analysis).

Under the Hood: C++ Implementation in Accellera UVM-SystemC

How does UVM-SystemC implement these ports? By tightly integrating with the standard SystemC TLM-1.0 and TLM-2.0 core libraries.

  1. uvm_port_base: All UVM ports inherit from uvm_port_base. This base class handles the UVM-specific connection resolution during the end_of_elaboration_phase. It ensures that every port is ultimately connected to an export that implements the required interface.
  2. SystemC Interface Binding: A uvm_blocking_put_port<T> is effectively a wrapper around sc_core::sc_port<tlm::tlm_blocking_put_if<T>>. When you connect a port to an export, the UVM framework dynamically resolves the connection down to standard SystemC bind() calls.
  3. uvm_analysis_port<T> Implementation: If you inspect the uvm_analysis_port source, you'll see it is incredibly lightweight. It contains an internal std::vector of connected uvm_analysis_export pointers. When you call ap.write(tx), the C++ implementation simply executes a for loop over this vector, synchronously calling export->write(tx) for every connected subscriber. Because write() is a void function, it must execute in zero simulation time.

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.

TLM-1.0 Blocking Ports

In UVM, components pass data using uvm_sequence_item or uvm_transaction objects. To send data from one component to another, we use ports and exports.

  • Port (uvm_blocking_put_port): The initiator of the communication. It requires an implementation of a method (like put()) to exist.
  • Export (uvm_blocking_put_export / uvm_port_base): The provider of the communication. It supplies the implementation.

In SystemC, the class supplying the export typically implements the interface methods (e.g. tlm::tlm_blocking_put_if) and binds the export to *this.

Analysis Ports (Broadcast Communication)

Blocking put and get ports are strictly one-to-one point-to-point connections. But what if a Monitor sees a transaction on the bus and needs to send it to a Scoreboard, a Coverage Collector, and a Protocol Checker all at once?

For this, UVM uses Analysis Ports. Analysis ports implement a broadcast (publish/subscribe) mechanism.

  • An Analysis Port can be bound to zero, one, or many Analysis Exports.
  • The transaction is passed by calling write().
  • write() is strictly a void non-blocking function. It must execute in zero time.

UVM Analysis Ports in SystemC

UVM-SystemC provides convenient wrappers around SystemC's tlm_analysis_port:

  • uvm_analysis_port<T>
  • uvm_analysis_export<T>
  • uvm_analysis_imp<T, IMP>

On the receiving end, you typically use a uvm_subscriber. The uvm_subscriber base class automatically provides an analysis_export and requires you to implement the write() function.

Sequencer-Driver Communication

The most specialized TLM connection in UVM is the communication between a uvm_sequencer and a uvm_driver.

UVM defines a dedicated request-response channel (uvm_tlm_req_rsp_channel) and specialized ports (uvm_seq_item_pull_port and uvm_seq_item_pull_export) for this. The driver acts as the active "puller", requesting items from the sequencer using methods like get_next_item() and item_done().

Complete TLM Example

Below is a complete, fully compilable sc_main example demonstrating a one-to-one point-to-point connection and a broadcast analysis connection.

#include <systemc>
#include <uvm>
 
// The data object exchanged between components
class my_transaction : public uvm::uvm_transaction {
public:
    int data;
    UVM_OBJECT_UTILS(my_transaction);
    my_transaction(const std::string& name = "my_transaction") : uvm::uvm_transaction(name), data(0) {}
};
 
// 1. Producer: Initiates a transaction via a blocking put port
class producer : public uvm::uvm_component {
public:
    UVM_COMPONENT_UTILS(producer);
 
    uvm::uvm_blocking_put_port<my_transaction> put_port;
 
    producer(uvm::uvm_component_name name) 
        : uvm::uvm_component(name), put_port("put_port") {}
 
    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        my_transaction tx;
        tx.data = 42;
        
        UVM_INFO("PROD", "Sending transaction data: 42", uvm::UVM_LOW);
        
        // This is a blocking call; it will wait until the consumer finishes processing
        put_port.put(tx); 
        
        phase.drop_objection(this);
    }
};
 
// 2. Consumer/Monitor: Receives point-to-point and Broadcasts via Analysis Port
class consumer : public uvm::uvm_component, 
                 public tlm::tlm_blocking_put_if<my_transaction> {
public:
    UVM_COMPONENT_UTILS(consumer);
 
    // TLM-1 export for receiving point-to-point data
    sc_core::sc_export<tlm::tlm_blocking_put_if<my_transaction>> put_export;
    
    // Analysis port for broadcasting data
    uvm::uvm_analysis_port<my_transaction> ap;
 
    consumer(uvm::uvm_component_name name) 
        : uvm::uvm_component(name), put_export("put_export"), ap("ap") {
        
        // Bind the export to 'this' component
        put_export.bind(*this);
    }
 
    // Implementation of the blocking put method
    void put(const my_transaction& tx) override {
        UVM_INFO("CONS", "Received transaction, consuming time...", uvm::UVM_LOW);
        sc_core::wait(10, sc_core::SC_NS); // Consume simulation time
        
        UVM_INFO("CONS", "Broadcasting transaction to subscribers", uvm::UVM_LOW);
        // Non-blocking broadcast
        ap.write(tx);
    }
};
 
// 3. Subscriber (Scoreboard): Listens to Analysis Broadcasts
class scoreboard : public uvm::uvm_subscriber<my_transaction> {
public:
    UVM_COMPONENT_UTILS(scoreboard);
 
    scoreboard(uvm::uvm_component_name name) : uvm::uvm_subscriber<my_transaction>(name) {}
 
    // Implement the write() function mandated by uvm_subscriber
    void write(const my_transaction& t) override {
        UVM_INFO("SB", "Scoreboard intercepted broadcast transaction!", uvm::UVM_LOW);
        if (t.data == 42) {
            UVM_INFO("SB", "Data matches expected value.", uvm::UVM_LOW);
        }
    }
};
 
// 4. Environment: Connects them all
class my_env : public uvm::uvm_env {
public:
    producer* prod;
    consumer* cons;
    scoreboard* sb;
    
    UVM_COMPONENT_UTILS(my_env);
 
    my_env(uvm::uvm_component_name name) : uvm::uvm_env(name) {}
 
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_env::build_phase(phase);
        prod = producer::type_id::create("prod", this);
        cons = consumer::type_id::create("cons", this);
        sb   = scoreboard::type_id::create("sb", this);
    }
    
    void connect_phase(uvm::uvm_phase& phase) override {
        // Point-to-point: Port binds to Export
        prod->put_port.connect(cons->put_export);
        
        // Broadcast: Analysis Port binds to Analysis Export (provided by uvm_subscriber)
        cons->ap.connect(sb->analysis_export);
    }
};
 
class my_test : public uvm::uvm_test {
public:
    my_env* env;
    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);
        env = my_env::type_id::create("env", this);
    }
};
 
int sc_main(int argc, char* argv[]) {
    uvm::run_test("my_test");
    return 0;
}

By heavily leveraging TLM, UVM components remain oblivious to what they are connected to, guaranteeing that your VIP (Verification Intellectual Property) can be reused seamlessly across different projects.

Standard and Source Deep Dive: Port Binding

Port binding is the topological glue of a SystemC model. The IEEE 1666-2023 LRM Sections 4.2.1 (Elaboration) and Section 6.11-6.13 (Ports, Exports, Interfaces) rigidly define how structural connections are made and verified.

Inside the Accellera Source: sc_port_b and sc_port_registry

In src/sysc/communication/sc_port.h/cpp, all specialized sc_port<IF> classes derive from a non-template base class sc_port_b. When you declare sc_port<BusIf> bus{"bus"};, the constructor ultimately calls sc_simcontext::get_port_registry()->insert(this).

The sc_port_registry (located in src/sysc/kernel/sc_simcontext.cpp) is the global list of every port in the simulation.

When you write cpu.bus.bind(subsystem.target); in your C++ code, you are invoking the bind() method on sc_port. However, this does not immediately resolve the C++ pointer! Instead, the port simply stores a generic pointer to the bound object in an internal array (because a port can be bound to multiple channels if the port's N parameter is > 1).

The Elaboration Phase: complete_binding()

The real magic happens when sc_start() is called. Before simulation begins, sc_start() invokes sc_simcontext::elaborate(), which ultimately calls sc_port_registry::complete_binding().

If you trace sysc/kernel/sc_simcontext.cpp, you will see complete_binding() iterate over every single port in the design. For each port:

  1. It traverses the binding tree. If Port A is bound to Port B, and Port B is bound to Channel C, it recursively walks from A -> B -> C to find the actual sc_interface implementation.
  2. Type Checking: It uses C++ RTTI (dynamic_cast) to verify that the target object actually implements the interface required by the port.
    // Abstract representation of the kernel's check:
    sc_interface* target_if = dynamic_cast<sc_interface*>(bound_object);
    if (!target_if) { SC_REPORT_ERROR("Port binding failed: interface mismatch"); }
  3. It resolves the final interface pointer and stores it directly inside the port's m_interface pointer array.

Zero-Overhead Simulation Dispatch

Why delay pointer resolution until complete_binding()? Because once elaboration finishes, the port has an absolute, direct C++ pointer to the implementing channel.

In src/sysc/communication/sc_port.h, the overloaded operator-> is extraordinarily simple:

template <class IF>
inline IF* sc_port<IF>::operator -> () {
    return m_interface;
}

During simulation, when a thread executes bus->write(0x10, data);, there are no map lookups, no string comparisons, and no routing tables. It is exactly equivalent to a direct C++ virtual function call on the channel object.

Comments and Corrections