Chapter 10: UVM-SystemC

The UVM Configuration Database (uvm_config_db)

Discover how to pass virtual interfaces, integers, and objects across your UVM hierarchy without hardcoding paths using the uvm_config_db.

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 large verification environment, passing configuration parameters down a deep component hierarchy can be extremely tedious. If a monitor deeply nested inside an agent needs to know whether the bus is configured for 32-bit or 64-bit mode, passing that boolean down through the test, the environment, the agent, and finally the monitor requires boilerplate code at every level.

Worse, what if you need to pass a handle to the physical pins (a virtual interface or a SystemC signal pointer)?

The UVM Configuration Database (uvm_config_db) solves this. It acts as a central repository where any component can store a variable (a set), and any component can retrieve that variable (a get), provided they know the correct hierarchy path and variable name. According to the IEEE 1800.2 standard, the database utilizes type-safe parameterized classes built upon a generic resource pool.

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

To appreciate the uvm_config_db, we must look at how the uvm-systemc repository implements it:

  1. uvm_resource_pool: The config database is actually a type-safe wrapper around a global singleton called the uvm_resource_pool. The resource pool stores a collection of type-erased uvm_resource_base objects.
  2. Type-Safety via Templates: uvm_config_db<T> is a template class. When you call set<int>(), the compiler instantiates a specific version of the database. This guarantees that if a component performs a set<int>("var"), and another component performs a get<bool>("var"), the cast fails safely and returns false, preventing catastrophic memory errors.
  3. Regular Expressions and Caching: The hierarchical strings (like "env.*.driver") you provide to set() are translated into POSIX regular expressions. Because compiling regex during simulation is slow, UVM-SystemC heavily caches these lookups inside the uvm_resource_pool to ensure fast retrieval during the build_phase.

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.

How uvm_config_db works

The uvm_config_db is a parameterized class. You must specify the type of the data you are storing or retrieving.

The two primary static methods are set() and get():

static void set(uvm_component* cntxt, const std::string& inst_name, const std::string& field_name, const T& value);
 
static bool get(uvm_component* cntxt, const std::string& inst_name, const std::string& field_name, T& value);
  • cntxt: The starting point in the UVM hierarchy (usually this). If setting globally, use uvm_top (or nullptr).
  • inst_name: The relative hierarchical path to the component(s) that should receive this configuration. Wildcards (*) are heavily used here.
  • field_name: The string identifier for the variable you are storing.
  • value: The actual data being stored (for set) or the variable to populate (for get).

Complete Configuration Example

Below is a complete, fully compilable sc_main program demonstrating how to pass a physical interface pointer (virtual interface) and a configuration integer from the top-level test bench down to a nested driver component using the uvm_config_db.

#include <systemc>
#include <uvm>
 
// A dummy transaction for the driver
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) {}
};
 
// A dummy physical interface wrapper
class my_bus_if {
public:
    sc_core::sc_signal<bool> clk;
    my_bus_if(const char* name) : clk(name) {}
};
 
// 1. The Driver retrieves the configuration
class my_driver : public uvm::uvm_driver<my_transaction> {
public:
    UVM_COMPONENT_UTILS(my_driver);
    
    my_bus_if* vif;
    int is_active_;
 
    my_driver(uvm::uvm_component_name name) : uvm::uvm_driver<my_transaction>(name) {}
 
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_driver<my_transaction>::build_phase(phase);
 
        // Retrieve the virtual interface
        if (!uvm::uvm_config_db<my_bus_if*>::get(this, "", "vif", vif)) {
            UVM_FATAL("DRV/NOVIF", "No virtual interface specified for this driver instance");
        }
        
        // Retrieve the active flag (default to active if not found)
        if (!uvm::uvm_config_db<int>::get(this, "", "is_active", is_active_)) {
            is_active_ = 1;
        }
    }
 
    void run_phase(uvm::uvm_phase& phase) override {
        phase.raise_objection(this);
        if (is_active_) {
            UVM_INFO("DRV", "Driver is active, driving virtual interface pins.", uvm::UVM_LOW);
            vif->clk.write(true);
        } else {
            UVM_INFO("DRV", "Driver is passive.", uvm::UVM_LOW);
        }
        phase.drop_objection(this);
    }
};
 
// 2. The Agent instantiates the driver
class my_agent : public uvm::uvm_agent {
public:
    my_driver* driver;
    UVM_COMPONENT_UTILS(my_agent);
 
    my_agent(uvm::uvm_component_name name) : uvm::uvm_agent(name) {}
 
    void build_phase(uvm::uvm_phase& phase) override {
        uvm::uvm_agent::build_phase(phase);
        driver = my_driver::type_id::create("driver", this);
    }
};
 
// 3. The Environment instantiates the agent
class my_env : public uvm::uvm_env {
public:
    my_agent* agent;
    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);
        agent = my_agent::type_id::create("agent", this);
    }
};
 
// 4. The Test sets configurations for the descendants
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);
        
        // Target: "env.agent.driver" inside this context
        uvm::uvm_config_db<int>::set(this, "env.agent.driver", "is_active", 0); 
        // Notice we are turning the driver off (passive) for this specific test
        
        env = my_env::type_id::create("env", this);
    }
};
 
// 5. The Top-Level sets the virtual interface globally
int sc_main(int argc, char* argv[]) {
    // Instantiate the physical bus interface
    my_bus_if bus("bus");
 
    // Pass the pointer to the bus into the UVM configuration database
    // We use a global context (nullptr) and target all instances ("*")
    uvm::uvm_config_db<my_bus_if*>::set(nullptr, "*", "vif", &bus);
 
    // Start the UVM test
    uvm::run_test("my_test");
    return 0;
}

Best Practices

  1. Do it in build_phase: Setting and getting configurations should generally happen during the build_phase. If you wait until connect_phase or run_phase, children may have already been constructed with incorrect defaults.
  2. Check the return value of get(): Always verify that get() returns true, and provide a sensible default (or issue a UVM_FATAL) if it returns false.
  3. Minimize wildcards: While setting a configuration to "*" is easy, it can cause performance issues in massive testbenches and makes it hard to track who is configuring what. Be as specific as possible with the inst_name argument (e.g., "env.agent.driver").
  4. Configuration Objects: Instead of passing dozens of integers and booleans individually, wrap them in a configuration class deriving from uvm_object, and pass a pointer to that object via the database.

Deep Dive: Accellera Source for sc_signal and update()

The sc_signal<T> channel perfectly illustrates the Evaluate-Update paradigm of SystemC. In the Accellera source (src/sysc/communication/sc_signal.cpp), sc_signal inherits from sc_prim_channel.

The write() Implementation

When you call write(const T&), the signal does not immediately change its value. Instead, it stores the requested value in m_new_val and registers itself with the kernel:

template<class T>
inline void sc_signal<T>::write(const T& value_) {
    if( !(m_new_val == value_) ) {
        m_new_val = value_;
        this->request_update(); // Inherited from sc_prim_channel
    }
}

The request_update() call appends the channel to sc_simcontext::m_update_list.

The update() Phase

After the Evaluate phase finishes (all ready processes have run), the kernel iterates over m_update_list and calls the update() virtual function on each primitive channel. For sc_signal, this looks like:

template<class T>
inline void sc_signal<T>::update() {
    if( !(m_new_val == m_cur_val) ) {
        m_cur_val = m_new_val;
        m_value_changed_event.notify(SC_ZERO_TIME); // Notify processes sensitive to value_changed_event()
    }
}

This guarantees that all concurrent processes see the same old value until the delta cycle advances, perfectly mimicking hardware register delays.

Comments and Corrections