UVM Components and Hierarchy
Explore the foundation of UVM-SystemC testbenches using uvm_component and the UVM hierarchy.
Welcome to the UVM Deep Dive! In this chapter, we explore the core building blocks of the Universal Verification Methodology in SystemC (UVM-SystemC). If you've written plain SystemC testbenches, you'll immediately see how UVM brings standardized structure and reusability to your verification environments. According to the IEEE 1800.2 standard for UVM, components form the static hierarchy of the verification environment.
The Foundation: uvm_component
In UVM-SystemC, the structural foundation of your testbench is built using classes derived from uvm_component. While transient data like transactions derive from uvm_object, static architectural pieces—like drivers, monitors, and scoreboards—derive from uvm_component.
In the UVM-SystemC library, uvm_component relies on multiple inheritance. It is derived from both sc_core::sc_module (giving it native SystemC hierarchy and simulation semantics) and uvm_report_object (giving it built-in reporting capabilities). Every UVM component is therefore a SystemC module, which allows it to have SystemC ports, exports, and processes.
Pre-defined Component Types
Rather than directly extending uvm_component for everything, UVM provides semantic base classes. While they function identically to uvm_component, they clarify the intent of your architecture:
uvm_env: The top-level container for a verification environment.uvm_agent: Encapsulates a sequencer, driver, and monitor for a specific protocol.uvm_driver: Requests transactions from a sequencer and drives them onto the physical DUT interface.uvm_monitor: Passively samples the DUT interface and broadcasts transactions via analysis ports.uvm_scoreboard: Checks predicted behavior against actual DUT outputs.uvm_test: The top-level test class that configures the environment and initiates stimulus.
Constructing Components
When constructing a UVM component, you always provide a name to identify it in the UVM hierarchy. Thanks to the factory pattern (which we will cover later), components are typically instantiated dynamically rather than statically. The UVM standard dictates that components must be created using the factory create method during the build_phase.
Here is a complete, fully compilable example demonstrating how to define, build, and execute a component hierarchy using uvm_component and the factory.
#include <systemc>
#include <uvm>
// A dummy transaction for the driver
class my_transaction : public uvm::uvm_sequence_item {
public:
int data;
UVM_OBJECT_UTILS(my_transaction);
my_transaction(const std::string& name = "my_transaction")
: uvm::uvm_sequence_item(name), data(0) {}
};
// 1. Define a UVM Driver
class my_driver : public uvm::uvm_driver<my_transaction> {
public:
// UVM Component Registration Macro
UVM_COMPONENT_UTILS(my_driver);
// Standard UVM Component Constructor
explicit my_driver(uvm::uvm_component_name name)
: uvm::uvm_driver<my_transaction>(name) {
}
void run_phase(uvm::uvm_phase& phase) override {
UVM_INFO("DRV", "Driver is running", uvm::UVM_LOW);
}
};
// 2. Define a UVM Environment containing the Driver
class my_env : public uvm::uvm_env {
public:
my_driver* driver;
UVM_COMPONENT_UTILS(my_env);
explicit my_env(uvm::uvm_component_name name) : uvm::uvm_env(name) {}
// Components are instantiated in the build_phase
void build_phase(uvm::uvm_phase& phase) override {
uvm::uvm_env::build_phase(phase);
// Use the UVM factory to create the driver
driver = my_driver::type_id::create("driver", this);
UVM_INFO("ENV", "Environment built driver component", uvm::UVM_LOW);
}
};
// 3. Define the UVM Test containing the Environment
class my_test : public uvm::uvm_test {
public:
my_env* env;
UVM_COMPONENT_UTILS(my_test);
explicit 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);
}
void run_phase(uvm::uvm_phase& phase) override {
phase.raise_objection(this);
UVM_INFO("TEST", "Running test...", uvm::UVM_LOW);
sc_core::wait(10, sc_core::SC_NS); // Consuming simulation time
// Print the component hierarchy
uvm::uvm_root::get()->print_topology();
phase.drop_objection(this);
}
};
// 4. Standard SystemC Entry Point
int sc_main(int argc, char* argv[]) {
// Initiate UVM phasing by starting the test
uvm::run_test("my_test");
return 0;
}The Component Hierarchy and uvm_top
Because UVM components are created dynamically (typically during the build_phase), UVM maintains its own internal hierarchy tree, distinct from (but mapped onto) the SystemC module hierarchy.
At the absolute root of this hierarchy sits a singleton called uvm_root. The UVM-SystemC implementation provides a global pointer to this singleton named uvm_top.
Any uvm_component instantiated without an explicit UVM parent automatically becomes a child of uvm_top.
You can traverse or search this hierarchy using built-in methods provided by uvm_component:
get_parent(): Returns a pointer to the component's parent.get_full_name(): Returns the full hierarchical string path (e.g.,uvm_test_top.env.driver).get_children(): Returns a vector of pointers to the component's immediate children.
Why use uvm_component instead of sc_module?
Since uvm_component inherits from sc_module, why use UVM at all? The answer lies in the standardized interfaces uvm_component introduces as defined by the Accellera standard:
- Phasing: Standardized lifecycle callbacks (like
build_phase,run_phase) instead of manually relying solely onsc_mainorend_of_elaboration. - Configuration: Seamless integration with the UVM Configuration Database (
uvm_config_db). - Reporting: Standardized, hierarchically controllable logging (
UVM_INFO,UVM_ERROR). - Factory: Dynamic type overrides (replacing a base driver with an error-injecting driver without changing the testbench code).
In the next tutorial, we will look at the UVM Phasing Mechanism, which strictly controls how these components are constructed, connected, and executed.
Comments and Corrections