UVM-SystemC Bridge: Objects, Factory, and Policy Classes
uvm_object, copy/compare/print hooks, factory creation, and reusable transaction design.
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: Objects, Factory, and Policy Classes
While uvm_component forms the static structural hierarchy of your testbench, uvm_object is the fundamental base class for all dynamic, transient data in UVM-SystemC.
Transactions, sequences, and configuration objects all inherit from uvm_object.
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.
Transaction Objects (Sequence Items)
A transaction object describes an atomic operation (e.g., a bus read, an ethernet packet, a register write). In UVM, transactions inherit from uvm_sequence_item (which itself inherits from uvm_object).
To ensure a transaction is fully reusable across scoreboards, drivers, and monitors, it must implement Policy Hooks.
Policy Hooks: Print, Copy, Compare, Pack
UVM-SystemC objects provide standard virtual methods that you are expected to override:
do_print(uvm_printer&): How the object represents itself as text.do_copy(const uvm_object&): How to deep-copy the object.do_compare(const uvm_object&, uvm_comparer&): How to check if two objects are equivalent.do_pack(uvm_packer&)/do_unpack(uvm_packer&): How to serialize the object to/from raw bit arrays.
By implementing these hooks, generic UVM components (like scoreboards) can compare transactions without knowing their specific underlying types.
The UVM Factory
The UVM Factory is a design pattern that allows you to substitute one object type for another dynamically at runtime.
If a testbench instantiates bus_transaction via the factory, a specific test can instruct the factory to substitute error_bus_transaction instead. The environment will automatically use the new type without a single line of the environment's source code being changed.
Complete Example: Transactions, Policies, and Factory Overrides
Here is a complete sc_main example demonstrates how to write a fully compliant uvm_sequence_item, override its policy hooks, register it with the factory, and dynamically override it from a test.
#include <systemc>
#include <uvm>
#include <string>
// 1. A Baseline Transaction Object
class bus_item : public uvm::uvm_sequence_item {
public:
// Register with the UVM object factory
UVM_OBJECT_UTILS(bus_item);
sc_dt::uint64 address;
sc_dt::uint32 data;
bool is_write;
// Default constructor is required by the factory
bus_item(const std::string& name = "bus_item")
: uvm::uvm_sequence_item(name), address(0), data(0), is_write(false) {}
// 2. Implement the Print hook
void do_print(uvm::uvm_printer& printer) const override {
uvm::uvm_sequence_item::do_print(printer);
printer.print_field_int("address", address, 64, uvm::UVM_HEX);
printer.print_field_int("data", data, 32, uvm::UVM_HEX);
printer.print_field_int("is_write", is_write, 1, uvm::UVM_BIN);
}
// 3. Implement the Copy hook
void do_copy(const uvm::uvm_object& rhs) override {
const bus_item* rhs_cast = dynamic_cast<const bus_item*>(&rhs);
if (rhs_cast == nullptr) {
UVM_FATAL("COPY", "Cast failed in do_copy");
}
uvm::uvm_sequence_item::do_copy(rhs);
this->address = rhs_cast->address;
this->data = rhs_cast->data;
this->is_write = rhs_cast->is_write;
}
// 4. Implement the Compare hook
bool do_compare(const uvm::uvm_object& rhs, uvm::uvm_comparer* comparer) const override {
const bus_item* rhs_cast = dynamic_cast<const bus_item*>(&rhs);
if (rhs_cast == nullptr) return false;
bool match = uvm::uvm_sequence_item::do_compare(rhs, comparer);
match &= comparer->compare_field_int("address", this->address, rhs_cast->address, 64);
match &= comparer->compare_field_int("data", this->data, rhs_cast->data, 32);
match &= comparer->compare_field_int("is_write", this->is_write, rhs_cast->is_write, 1);
return match;
}
};
// 5. An Error-Injection Subclass
class error_bus_item : public bus_item {
public:
UVM_OBJECT_UTILS(error_bus_item);
bool force_error;
error_bus_item(const std::string& name = "error_bus_item")
: bus_item(name), force_error(true) {}
void do_print(uvm::uvm_printer& printer) const override {
bus_item::do_print(printer); // Print parent fields
printer.print_field_int("force_error", force_error, 1, uvm::UVM_BIN);
}
};
// 6. A Component that uses the transaction
class generic_driver : public uvm::uvm_component {
public:
UVM_COMPONENT_UTILS(generic_driver);
generic_driver(uvm::uvm_component_name name) : uvm::uvm_component(name) {}
void run_phase(uvm::uvm_phase& phase) override {
phase.raise_objection(this);
// We ask the factory to create a "bus_item".
// If an override is set, we might get an "error_bus_item" instead!
bus_item* item = bus_item::type_id::create("item");
UVM_INFO("DRIVER", "Driver created transaction:", uvm::UVM_LOW);
item->print(); // Uses do_print() under the hood
phase.drop_objection(this);
}
};
// 7. The Top-Level Test
class factory_test : public uvm::uvm_test {
public:
UVM_COMPONENT_UTILS(factory_test);
generic_driver* driver;
factory_test(uvm::uvm_component_name name) : uvm::uvm_test(name) {}
void build_phase(uvm::uvm_phase& phase) override {
uvm::uvm_test::build_phase(phase);
// 8. Factory Override:
// Instruct the factory: Anytime someone asks to create a "bus_item",
// give them an "error_bus_item" instead.
set_type_override("bus_item", "error_bus_item");
driver = generic_driver::type_id::create("driver", this);
}
};
int sc_main(int argc, char* argv[]) {
// Run the test. The driver will ask for a bus_item, but will receive
// an error_bus_item due to the factory override in build_phase.
uvm::run_test("factory_test");
return 0;
}Under the Hood: The Factory and Dynamic Casting
UVM-SystemC implements the factory pattern via a central singleton called uvm_default_factory.
When set_type_override (or set_type_override_by_type) is called, the factory populates a map std::map<uvm_object_wrapper*, uvm_object_wrapper*> (conceptually). Later, when bus_item::type_id::create() executes, it consults the factory to resolve the most derived override. It then invokes the create_object() virtual method on the proxy wrapper to perform the C++ new allocation.
Furthermore, notice the mandatory use of dynamic_cast<const bus_item*>(&rhs) inside do_copy and do_compare. Because UVM-SystemC relies heavily on polymorphism, the framework passes generic uvm_object& references to policy hooks. C++ requires RTTI (Run-Time Type Information) to safely downcast the object back to the user-defined bus_item to access member variables like address and data.
A critical C++ implementation detail to be aware of: uvm_object::clone() is heavily used in verification environments (e.g., when a monitor captures a transaction). Under the hood, clone() executes uvm_object* obj = this->create(); obj->copy(this);. Therefore, failing to register your class with UVM_OBJECT_UTILS breaks create(), which catastrophically breaks clone().
Design Rules for Transactions
- Implement Policies: If you don't override
do_compare,item_a->compare(item_b)will only compare base class properties, silently ignoring your custom fields (likeaddressordata), breaking your scoreboards. - Use Factory Macros: Always use
UVM_OBJECT_UTILS(class_name)to register the object. Without this,type_id::createwill not compile. - Avoid Simulator Resources: Transactions should not own SystemC events (
sc_event), ports, or modules. They are pure data containers designed to be passed around, cloned, and deleted instantly.
Comments and Corrections