UVM-SystemC Bridge: Sequences, Sequencers, Drivers, and TLM
How stimulus flows from sequences through sequencers and drivers, and how TLM connects verification components.
UVM-SystemC Bridge: Sequences, Sequencers, Drivers, and TLM
In a robust verification environment, you must separate what you want to test (the scenario) from how you wiggle the pins (the protocol execution).
UVM-SystemC achieves this through a triad of objects:
- Sequences (
uvm_sequence): Generate dynamic streams of transactions (the "what"). - Sequencers (
uvm_sequencer): Arbitrate requests and pass transactions from sequences down to drivers. - Drivers (
uvm_driver): Pull transactions from the sequencer and convert them into physical signal wiggles or SystemC TLM-2.0 calls (the "how").
Furthermore, Monitors observe the bus and use UVM TLM Analysis Ports (uvm_analysis_port) to broadcast observed transactions to Scoreboards without tightly coupling them together.
The Sequence-Driver Handshake
The communication between a sequencer and a driver uses a specialized TLM port mechanism: seq_item_port. The driver executes an infinite loop during the run_phase:
seq_item_port.get_next_item(req);// Blocks until a sequence provides a transaction.- Drive the physical interface.
seq_item_port.item_done();// Signals to the sequence that the transaction is complete.
Complete Example: Sequence, Driver, and Analysis Port
The following complete, compilable sc_main example demonstrates a sequence generating transactions, a sequencer routing them to a driver, the driver executing them, and then broadcasting the completion to an Analysis Port (simulating a monitor).
#include <systemc>
#include <uvm>
#include <string>
// 1. The Transaction Object
class my_item : public uvm::uvm_sequence_item {
public:
UVM_OBJECT_UTILS(my_item);
int data;
my_item(const std::string& name = "my_item") : uvm::uvm_sequence_item(name), data(0) {}
};
// 2. The Sequence (Generates Stimulus)
class my_sequence : public uvm::uvm_sequence<my_item> {
public:
UVM_OBJECT_UTILS(my_sequence);
my_sequence(const std::string& name = "my_sequence") : uvm::uvm_sequence<my_item>(name) {}
// The sequence body contains the scenario logic
void body() override {
UVM_INFO("SEQUENCE", "Starting sequence generation...", uvm::UVM_LOW);
for (int i = 1; i <= 3; ++i) {
// Create a new item
my_item* req = my_item::type_id::create("req");
start_item(req); // Wait for sequencer arbitration
req->data = i * 10; // Randomize or configure the payload
finish_item(req); // Send to driver and wait for item_done()
UVM_INFO("SEQUENCE", ("Transaction " + std::to_string(i) + " completed.").c_str(), uvm::UVM_LOW);
}
}
};
// 3. The Driver (Executes Protocol)
class my_driver : public uvm::uvm_driver<my_item> {
public:
UVM_COMPONENT_UTILS(my_driver);
// An analysis port to broadcast completed transactions (simulating a monitor)
uvm::uvm_analysis_port<my_item> ap;
my_driver(uvm::uvm_component_name name) : uvm::uvm_driver<my_item>(name), ap("ap") {}
void run_phase(uvm::uvm_phase& phase) override {
my_item req;
while (true) {
// 1. Get the next transaction from the sequencer
seq_item_port.get_next_item(req);
// 2. Drive the protocol (simulate delay)
UVM_INFO("DRIVER", ("Driving data: " + std::to_string(req.data)).c_str(), uvm::UVM_LOW);
sc_core::wait(10, sc_core::SC_NS);
// 3. Broadcast to scoreboards via TLM analysis port
ap.write(req);
// 4. Signal completion back to the sequence
seq_item_port.item_done();
}
}
};
// 4. A Mock Scoreboard (Subscribes to Analysis Port)
class my_scoreboard : public uvm::uvm_subscriber<my_item> {
public:
UVM_COMPONENT_UTILS(my_scoreboard);
my_scoreboard(uvm::uvm_component_name name) : uvm::uvm_subscriber<my_item>(name) {}
// This is called automatically when the driver calls ap.write()
void write(const my_item& t) override {
UVM_INFO("SCOREBOARD", ("Received data: " + std::to_string(t.data)).c_str(), uvm::UVM_LOW);
}
};
// 5. The Top-Level Test
class test_tlm : public uvm::uvm_test {
public:
UVM_COMPONENT_UTILS(test_tlm);
uvm::uvm_sequencer<my_item>* sqr;
my_driver* drv;
my_scoreboard* sb;
test_tlm(uvm::uvm_component_name name) : uvm::uvm_test(name) {}
void build_phase(uvm::uvm_phase& phase) override {
uvm::uvm_test::build_phase(phase);
sqr = uvm::uvm_sequencer<my_item>::type_id::create("sqr", this);
drv = my_driver::type_id::create("drv", this);
sb = my_scoreboard::type_id::create("sb", this);
}
void connect_phase(uvm::uvm_phase& phase) override {
// Connect sequencer to driver
drv->seq_item_port.connect(sqr->seq_item_export);
// Connect driver's analysis port to scoreboard's analysis export
drv->ap.connect(sb->analysis_export);
}
void run_phase(uvm::uvm_phase& phase) override {
phase.raise_objection(this);
// Start the sequence on the sequencer
my_sequence* seq = my_sequence::type_id::create("seq");
seq->start(sqr);
phase.drop_objection(this);
}
};
int sc_main(int argc, char* argv[]) {
uvm::run_test("test_tlm");
return 0;
}Why Use uvm_analysis_port?
The uvm_analysis_port provides a publish-subscribe (broadcast) pattern. A monitor or driver calls .write(transaction) on the port without knowing who is listening.
You can connect zero, one, or ten scoreboards to that single analysis port during the connect_phase. If no one is connected, the .write() call safely returns immediately. This drastically improves the reusability of monitors and drivers, as they never have hardcoded dependencies on specific checking logic.
Comments and Corrections