This full UVM testbench puts every component of the register-block testbench together in one complete listing: the DUT and its interface, the transaction, driver, monitor, coverage collector, sequencer, sequence, agent, scoreboard, environment, test and top-level module. Each piece has its own article that explains it in detail. Here they appear in the order each one depends on the one before it, ready to compile and run.
Seeing all the code in one place makes it easier to follow a transaction from the sequence through the DUT and back to the scoreboard, and to understand how the pieces are wired together.
10 min read | Intermediate Level
What You'll Learn
- Assemble every class of a UVM testbench into one compilable project
- Trace the connections from sequence to DUT and from DUT to scoreboard and coverage
- Follow the clock-by-clock timing of a write and a read
- Recognise what a passing run should report
How the Pieces Connect
The test starts reg_sequence on the agent's sequencer. The driver pulls each transaction from the sequencer and drives it onto reg_if0. The monitor watches the same pins, rebuilds each transfer as a transaction, and writes it to one analysis port that feeds two subscribers: the coverage collector inside the agent and the scoreboard in the environment.
The code is usually split over a few files. The interface and DUT are design files, all classes go into one package so they share the UVM imports, and the top module ties everything together:
// reg_if.sv - interface reg_if
// reg_block.sv - module reg_block (the DUT)
// reg_pkg.sv - package reg_pkg: every class from section 2 to 11
// tb_top.sv - module tb_top
package reg_pkg;
import uvm_pkg::*;
`include "uvm_macros.svh"
// reg_transaction, reg_driver, reg_monitor, reg_coverage, reg_sequencer,
// reg_sequence, reg_agent, reg_scoreboard, reg_env, reg_test
endpackage
The Complete Testbench, in Build Order
1. The interface and the DUT
reg_block has sixteen 8-bit registers. A write stores wdata on the clock edge where wr_en is high. A read loads rdata on the edge where rd_en is high, so the value can be sampled from the next edge on.
interface reg_if (input bit clk);
logic rst_n;
logic wr_en;
logic rd_en;
logic [3:0] addr;
logic [7:0] wdata;
logic [7:0] rdata;
endinterface
module reg_block (reg_if vif);
logic [7:0] mem [0:15];
always @(posedge vif.clk or negedge vif.rst_n) begin
if (!vif.rst_n) begin
vif.rdata <= 8'h00;
end
else begin
if (vif.wr_en) begin
mem[vif.addr] <= vif.wdata;
end
else if (vif.rd_en) begin
vif.rdata <= mem[vif.addr];
end
end
end
endmodule
2. The transaction
One transaction is one write or one read. The constraint makes sure exactly one of wr_en and rd_en is set.
class reg_transaction extends uvm_sequence_item;
rand bit wr_en;
rand bit rd_en;
rand bit [3:0] addr;
rand bit [7:0] wdata;
bit [7:0] rdata;
constraint wr_xor_rd_c {
wr_en != rd_en;
}
`uvm_object_utils_begin(reg_transaction)
`uvm_field_int(wr_en, UVM_ALL_ON)
`uvm_field_int(rd_en, UVM_ALL_ON)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(wdata, UVM_ALL_ON)
`uvm_field_int(rdata, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "reg_transaction");
super.new(name);
endfunction
endclass
3. The driver
The driver applies each transaction for one clock and then clears wr_en and rd_en, so the DUT and the monitor see every transfer exactly once. For a read it waits one more edge and copies rdata back into the transaction.
class reg_driver extends uvm_driver #(reg_transaction);
`uvm_component_utils(reg_driver)
virtual reg_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(virtual reg_if)::get(this, "", "vif", vif)) begin
`uvm_fatal(get_type_name(), "Didn't get handle to virtual interface reg_if")
end
endfunction
virtual task run_phase(uvm_phase phase);
reg_transaction tr;
forever begin
seq_item_port.get_next_item(tr);
@(posedge vif.clk);
vif.wr_en <= tr.wr_en;
vif.rd_en <= tr.rd_en;
vif.addr <= tr.addr;
vif.wdata <= tr.wdata;
@(posedge vif.clk); // the DUT acts on this edge
vif.wr_en <= 0;
vif.rd_en <= 0;
if (tr.rd_en) begin
@(posedge vif.clk); // rdata is stable from here
tr.rdata = vif.rdata;
end
seq_item_port.item_done();
end
endtask
endclass
4. The monitor
The monitor checks the bus on every edge. A write is complete as soon as it is seen. For a read, it waits one more edge for rdata, exactly like the driver.
class reg_monitor extends uvm_monitor;
`uvm_component_utils(reg_monitor)
virtual reg_if vif;
uvm_analysis_port #(reg_transaction) mon_analysis_port;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon_analysis_port = new("mon_analysis_port", this);
if (!uvm_config_db #(virtual reg_if)::get(this, "", "vif", vif)) begin
`uvm_fatal(get_type_name(), "Didn't get handle to virtual interface reg_if")
end
endfunction
virtual task run_phase(uvm_phase phase);
reg_transaction tr;
forever begin
@(posedge vif.clk);
if (vif.wr_en) begin
tr = reg_transaction::type_id::create("tr", this);
tr.wr_en = 1;
tr.addr = vif.addr;
tr.wdata = vif.wdata;
mon_analysis_port.write(tr);
end
else if (vif.rd_en) begin
tr = reg_transaction::type_id::create("tr", this);
tr.rd_en = 1;
tr.addr = vif.addr;
@(posedge vif.clk);
tr.rdata = vif.rdata;
mon_analysis_port.write(tr);
end
end
endtask
endclass
5. Coverage
The coverage collector is a uvm_subscriber, which already contains an analysis export and calls write() for every transaction the monitor sends.
class reg_coverage extends uvm_subscriber #(reg_transaction);
`uvm_component_utils(reg_coverage)
reg_transaction trans;
covergroup cg_reg_transaction;
coverpoint trans.addr;
coverpoint trans.wr_en;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg_reg_transaction = new();
endfunction
virtual function void write(reg_transaction t);
trans = t;
cg_reg_transaction.sample();
endfunction
endclass
6. The sequencer
class reg_sequencer extends uvm_sequencer #(reg_transaction);
`uvm_component_utils(reg_sequencer)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
7. The sequence
The sequence sends five random transactions. Its pre_body() and post_body() raise and drop an objection only when the sequence was started as a phase's default sequence. Here the test starts it directly, so get_starting_phase() returns null and the test's own objection keeps the phase alive.
class reg_sequence extends uvm_sequence #(reg_transaction);
`uvm_object_utils(reg_sequence)
function new(string name = "reg_sequence");
super.new(name);
endfunction
virtual task pre_body();
uvm_phase starting_phase = get_starting_phase();
if (starting_phase != null)
starting_phase.raise_objection(this);
endtask
virtual task body();
repeat (5) begin
req = reg_transaction::type_id::create("req");
start_item(req);
if (!req.randomize()) `uvm_error(get_type_name(), "Randomization failed")
finish_item(req);
end
endtask
virtual task post_body();
uvm_phase starting_phase = get_starting_phase();
if (starting_phase != null)
starting_phase.drop_objection(this);
endtask
endclass
8. The agent
An active agent builds the sequencer and driver. The monitor and coverage collector are always built, so a passive copy of this agent could still observe and measure the bus.
class reg_agent extends uvm_agent;
`uvm_component_utils(reg_agent)
reg_driver m_driver;
reg_monitor m_monitor;
reg_sequencer m_sequencer;
reg_coverage m_coverage;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (get_is_active()) begin
m_sequencer = reg_sequencer::type_id::create("m_sequencer", this);
m_driver = reg_driver::type_id::create("m_driver", this);
end
m_monitor = reg_monitor::type_id::create("m_monitor", this);
m_coverage = reg_coverage::type_id::create("m_coverage", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active())
m_driver.seq_item_port.connect(m_sequencer.seq_item_export);
m_monitor.mon_analysis_port.connect(m_coverage.analysis_export);
endfunction
endclass
9. The scoreboard
The scoreboard keeps its own copy of the registers. Writes update it, and reads are compared against it. A read from an address that has never been written is reported and skipped, because the DUT does not reset its registers.
class reg_scoreboard extends uvm_scoreboard;
`uvm_component_utils(reg_scoreboard)
uvm_analysis_imp #(reg_transaction, reg_scoreboard) ap_imp;
bit [7:0] expected_mem [16];
bit written [16];
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap_imp = new("ap_imp", this);
endfunction
virtual function void write(reg_transaction tr);
if (tr.wr_en) begin
expected_mem[tr.addr] = tr.wdata;
written[tr.addr] = 1;
end
else if (tr.rd_en) begin
if (!written[tr.addr]) begin
`uvm_info(get_type_name(), $sformatf("addr=%0d read before any write, skipping check", tr.addr), UVM_LOW)
end
else if (tr.rdata != expected_mem[tr.addr]) begin
`uvm_error(get_type_name(), $sformatf("Mismatch at addr=%0d: expected=0x%0h actual=0x%0h", tr.addr, expected_mem[tr.addr], tr.rdata))
end
else begin
`uvm_info(get_type_name(), $sformatf("addr=%0d matched: 0x%0h", tr.addr, tr.rdata), UVM_HIGH)
end
end
endfunction
endclass
10. The environment
class reg_env extends uvm_env;
`uvm_component_utils(reg_env)
reg_agent m_agent;
reg_scoreboard m_scoreboard;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_agent = reg_agent::type_id::create("m_agent", this);
m_scoreboard = reg_scoreboard::type_id::create("m_scoreboard", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
m_agent.m_monitor.mon_analysis_port.connect(m_scoreboard.ap_imp);
endfunction
endclass
11. The test
The test builds the environment, prints the component tree once elaboration is complete, and runs the sequence between its own raise_objection() and drop_objection().
class reg_test extends uvm_test;
`uvm_component_utils(reg_test)
reg_env m_top_env;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_top_env = reg_env::type_id::create("m_top_env", this);
endfunction
virtual function void end_of_elaboration_phase(uvm_phase phase);
uvm_top.print_topology();
endfunction
virtual task run_phase(uvm_phase phase);
reg_sequence m_seq = reg_sequence::type_id::create("m_seq");
phase.raise_objection(this);
m_seq.start(m_top_env.m_agent.m_sequencer);
phase.drop_objection(this);
endtask
endclass
12. Testbench top
The top module generates the clock, applies reset, puts the interface into the configuration database for everything below the test, and calls run_test(). The scope "uvm_test_top.*" is what lets the driver and monitor, several levels down, find the interface.
module tb_top;
import uvm_pkg::*;
import reg_pkg::*;
`include "uvm_macros.svh"
bit clk;
always #10 clk = ~clk;
reg_if reg_if0 (clk);
reg_block dut0 (reg_if0);
initial begin
reg_if0.rst_n = 0;
reg_if0.wr_en = 0;
reg_if0.rd_en = 0;
#25 reg_if0.rst_n = 1;
end
initial begin
uvm_config_db #(virtual reg_if)::set(null, "uvm_test_top.*", "vif", reg_if0);
run_test("reg_test");
end
endmodule
Timing of One Write and One Read
Following a transfer edge by edge shows why the driver and monitor are written the way they are:
- Edge 1: the driver puts the transaction on the bus.
- Edge 2: the DUT acts on it, storing the write or loading rdata for a read. The monitor sees the request on this edge, and the driver clears
wr_enandrd_en. A write is now complete. - Edge 3 (reads only): rdata holds the value loaded on edge 2. The driver and monitor both sample it, and the monitor sends the read to the scoreboard.
Because the request is cleared after edge 2, the monitor never sees the same transfer twice. Without that, a write left on the bus for two edges would be reported twice, inflating coverage.
Running It
Compile the four files with a UVM-capable simulator and run with +UVM_TESTNAME=reg_test, or rely on the name passed to run_test(). A passing run shows:
- The component tree from
print_topology(), withm_top_env,m_agentand its four children, andm_scoreboard, matching the diagram above. - For any random read of an address that was not written earlier in the run, a "read before any write" message from the scoreboard.
- A report summary with zero
UVM_ERRORand zeroUVM_FATALmessages.
The "matched" messages are at UVM_HIGH, so they appear only when you raise the verbosity with +UVM_VERBOSITY=UVM_HIGH. With only five random transactions, many reads will hit unwritten addresses. Raising the count in the sequence makes read-after-write checks far more frequent.
Quick Check
Q: If reg_scoreboard reported a mismatch, should you start debugging in the scoreboard or somewhere else?
Show Answer
Somewhere else, usually. The scoreboard only compares values the monitor captured; it does not drive or sample anything. A mismatch points to the driver, the DUT or the monitor's capture timing. Check the scoreboard's own logic only after those are ruled out.
Common Mistakes
Mistake #1: Leaving the request asserted after the transfer
If the driver never clears wr_en and rd_en, the DUT repeats the operation and the monitor reports it again on every following edge. Clear the request once the DUT has acted on it.
Mistake #2: Setting the interface with too narrow a scope
Setting the virtual interface with the scope "uvm_test_top" makes it visible only to the test itself. The driver and monitor then fail their get(). Use "uvm_test_top.*", or have the test pass the handle further down.
Mistake #3: Forgetting to import the class package in the top module
run_test("reg_test") finds the test through the factory, but the test class is registered only if its package is compiled and imported. A missing import reg_pkg::* usually ends in a fatal "cannot find test" error at time zero.
Summary
In this article, you learned:
- How all twelve pieces of the register testbench fit together, in build order
- How one monitor analysis port feeds both the coverage collector and the scoreboard
- The edge-by-edge timing that the driver and monitor follow for writes and reads
- Why the driver clears the request after each transfer
- What a passing run of the testbench reports
Going Deeper
- Every UVM phase and its execution order: UVM Phases
- How objections keep a phase running: UVM Objection
- Full config_db mechanics, scope strings and precedence: UVM Config Database
- Replacing components without editing the environment: UVM Factory Override
- Every TLM port, export and imp: UVM TLM
- Shortcuts for start_item and finish_item: How to use `uvm_do sequence macros