Example Walkthrough: A Minimal FIFO Testbench
Sections B through F built this exact testbench piece by piece — interface and transaction, driver, monitor, sequencer and sequence, scoreboard, agent and environment, test and testbench-top, plus everything since about phasing, the factory, configuration, TLM, and sequence execution along the way. This page is the capstone: every one of those pieces, shown together in one place, in the order they'd actually be designed.
This code is written to be syntactically correct and conceptually complete, but it hasn't been run through a simulator — treat it as a reference for how the pieces fit together, not a drop-in verified testbench.
The DUT
A synchronous FIFO with a simple push/pop interface: wr_en/wr_data to push, full when it can't accept more; rd_en to pop, with the popped value appearing on rd_data, and empty when there's nothing left.
interface fifo_if (input bit clk, input bit rst_n);
logic wr_en;
logic [7:0] wr_data;
logic full;
logic rd_en;
logic [7:0] rd_data;
logic empty;
endinterface
The transaction
One fifo_txn represents either a write or a read. wr_data is randomized for writes; rd_data is filled in later by the monitor, from what it observes on the DUT's pins, not randomized by the sequence.
class fifo_txn extends uvm_sequence_item;
typedef enum {WRITE, READ} op_e;
rand op_e op;
rand bit [7:0] wr_data;
bit [7:0] rd_data;
`uvm_object_utils(fifo_txn)
function new(string name = "fifo_txn");
super.new(name);
endfunction
endclass
The sequence: push four values, then pop four values
class fifo_write_read_seq extends uvm_sequence #(fifo_txn);
`uvm_object_utils(fifo_write_read_seq)
function new(string name = "fifo_write_read_seq");
super.new(name);
endfunction
task body();
fifo_txn txn;
repeat (4) begin
txn = fifo_txn::type_id::create("txn");
start_item(txn);
if (!txn.randomize() with { op == WRITE; })
`uvm_error("SEQ", "Randomization failed")
finish_item(txn);
end
repeat (4) begin
txn = fifo_txn::type_id::create("txn");
start_item(txn);
if (!txn.randomize() with { op == READ; })
`uvm_error("SEQ", "Randomization failed")
finish_item(txn);
end
endtask
endclass
The driver: turns transactions into pin wiggles
class fifo_driver extends uvm_driver #(fifo_txn);
`uvm_component_utils(fifo_driver)
virtual fifo_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual fifo_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "Virtual interface not set for fifo_driver")
endfunction
task run_phase(uvm_phase phase);
vif.wr_en <= 0;
vif.rd_en <= 0;
forever begin
fifo_txn txn;
seq_item_port.get_next_item(txn);
@(posedge vif.clk);
if (txn.op == fifo_txn::WRITE) begin
vif.wr_en <= 1;
vif.wr_data <= txn.wr_data;
end else begin
vif.rd_en <= 1;
end
@(posedge vif.clk);
vif.wr_en <= 0;
vif.rd_en <= 0;
seq_item_port.item_done();
end
endtask
endclass
The monitor: watches the pins, broadcasts what it sees
class fifo_monitor extends uvm_monitor;
`uvm_component_utils(fifo_monitor)
virtual fifo_if vif;
uvm_analysis_port #(fifo_txn) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual fifo_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "Virtual interface not set for fifo_monitor")
endfunction
task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
if (vif.wr_en && !vif.full) begin
fifo_txn txn = fifo_txn::type_id::create("txn");
txn.op = fifo_txn::WRITE;
txn.wr_data = vif.wr_data;
ap.write(txn);
end
if (vif.rd_en && !vif.empty) begin
fifo_txn txn = fifo_txn::type_id::create("txn");
txn.op = fifo_txn::READ;
txn.rd_data = vif.rd_data;
ap.write(txn);
end
end
endtask
endclass
The scoreboard: an independent model of "what should come out"
class fifo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(fifo_scoreboard)
uvm_analysis_imp #(fifo_txn, fifo_scoreboard) ap_imp;
bit [7:0] expected_q[$];
function new(string name, uvm_component parent);
super.new(name, parent);
ap_imp = new("ap_imp", this);
endfunction
function void write(fifo_txn txn);
if (txn.op == fifo_txn::WRITE) begin
expected_q.push_back(txn.wr_data);
`uvm_info("SB", $sformatf("Pushed expected data=%0h", txn.wr_data), UVM_HIGH)
end else begin
bit [7:0] expected;
if (expected_q.size() == 0) begin
`uvm_error("SB", "Read observed with nothing expected in the FIFO")
return;
end
expected = expected_q.pop_front();
if (txn.rd_data !== expected)
`uvm_error("SB", $sformatf("Mismatch: expected=%0h actual=%0h", expected, txn.rd_data))
else
`uvm_info("SB", $sformatf("Match: data=%0h", txn.rd_data), UVM_HIGH)
end
endfunction
endclass
The agent: bundles sequencer, driver, and monitor
class fifo_agent extends uvm_agent;
`uvm_component_utils(fifo_agent)
fifo_driver driver;
fifo_monitor monitor;
uvm_sequencer #(fifo_txn) sequencer;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
monitor = fifo_monitor::type_id::create("monitor", this);
if (get_is_active() == UVM_ACTIVE) begin
driver = fifo_driver::type_id::create("driver", this);
sequencer = uvm_sequencer#(fifo_txn)::type_id::create("sequencer", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active() == UVM_ACTIVE)
driver.seq_item_port.connect(sequencer.seq_item_export);
endfunction
endclass
The environment: agent + scoreboard, wired together
class fifo_env extends uvm_env;
`uvm_component_utils(fifo_env)
fifo_agent agent;
fifo_scoreboard scoreboard;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = fifo_agent::type_id::create("agent", this);
scoreboard = fifo_scoreboard::type_id::create("scoreboard", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
agent.monitor.ap.connect(scoreboard.ap_imp);
endfunction
endclass
The test: builds the environment, starts the sequence, owns the objection
class fifo_base_test extends uvm_test;
`uvm_component_utils(fifo_base_test)
fifo_env env;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = fifo_env::type_id::create("env", this);
endfunction
task run_phase(uvm_phase phase);
fifo_write_read_seq seq = fifo_write_read_seq::type_id::create("seq");
phase.raise_objection(this);
seq.start(env.agent.sequencer);
phase.drop_objection(this);
endtask
endclass
Testbench-top: where UVM meets the DUT
module tb_top;
bit clk, rst_n;
always #5 clk = ~clk;
fifo_if fifo_if_inst (.clk(clk), .rst_n(rst_n));
sync_fifo dut (
.clk (clk),
.rst_n (rst_n),
.wr_en (fifo_if_inst.wr_en),
.wr_data (fifo_if_inst.wr_data),
.full (fifo_if_inst.full),
.rd_en (fifo_if_inst.rd_en),
.rd_data (fifo_if_inst.rd_data),
.empty (fifo_if_inst.empty)
);
initial begin
rst_n = 0;
#20 rst_n = 1;
uvm_config_db#(virtual fifo_if)::set(null, "*", "vif", fifo_if_inst);
run_test("fifo_base_test");
end
endmodule
How it all connects
Tracing one transaction end to end: fifo_base_test.run_phase starts fifo_write_read_seq on the agent's sequencer → the sequence's body() randomizes a fifo_txn and hands it to the sequencer via start_item/finish_item → the driver's run_phase picks it up via get_next_item, drives wr_en/wr_data for one clock, and signals item_done → the monitor, watching the same pins independently, reconstructs the same transaction and broadcasts it through its analysis port → the scoreboard receives it via the connected analysis imp and pushes the expected data onto its reference queue. The same path runs in reverse for reads, with the scoreboard popping its reference queue and comparing against what the monitor observed on rd_data. Every one of those arrows is a mechanism from an earlier page: phases and objections start the test, sequences generate stimulus, the driver/monitor split observes vs. drives, TLM analysis ports connect monitor to scoreboard, uvm_config_db got the virtual interface to both the driver and monitor in the first place, and `uvm_info/ `uvm_error report what happened along the way.
Everything through this page — architecture, phasing, the factory, configuration, TLM, sequence execution and coordination, and reporting/utility policies — is enough to read, extend, and write a real block-level testbench. What's not yet covered, each substantial enough to be its own section: the Register Abstraction Layer for register-heavy DUTs, direct HDL backdoor access and the singleton pattern as advanced/specialized topics, and a set of guidelines for writing genuinely reusable verification components — the discipline that turns a working testbench like this one into one other engineers can safely build on top of.