Skip to main content

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.

Illustrative, not a compiled deliverable

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.

Beyond this section

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.