Part 6 · Testbench Architecture · Intermediate
tb_top: The Static Harness
Clock and reset generation, interface instantiation, DUT hookup, passing virtual interfaces to classes, and kicking off run().
What lives in tb_top
tb_top is the only module you write for the testbench. It owns everything that must exist at elaboration: clock and reset generation, the interface instance, the DUT instantiation, and the initial block that constructs the class-world test and starts it. Nothing in tb_top is intelligent — all behavior lives in classes.
tb_top RESPONSIBILITIES
┌──────────────── module tb_top ────────────────────┐
│ │
│ 1. clock gen always #5 clk = ~clk │
│ 2. reset gen initial: assert, hold, release │
│ 3. interface bus_if bif (clk, rst_n) │
│ 4. DUT hookup dut u_dut ( bif.dut_mp ... ) │
│ 5. vif handoff test.vif = bif (into classes) │
│ 6. kickoff initial begin t.run(); end │
│ 7. plumbing waves, timeout watchdog, finish │
│ │
└────────────────────────────────────────────────────┘
Static structure here ── dynamic behavior in classes.Complete harness example
// ---------- the signal layer: interface ----------
interface bus_if (input logic clk, input logic rst_n);
logic valid, ready, write;
logic [15:0] addr;
logic [31:0] wdata, rdata;
clocking drv_cb @(posedge clk);
default input #1step output #2;
output valid, write, addr, wdata;
input ready, rdata;
endclocking
clocking mon_cb @(posedge clk);
default input #1step;
input valid, ready, write, addr, wdata, rdata;
endclocking
modport DUT (input clk, rst_n, valid, write, addr, wdata,
output ready, rdata);
endinterface
// ---------- the static harness ----------
module tb_top;
logic clk = 0;
logic rst_n;
// 1. clock: free-running, before and after reset
always #5 clk = ~clk;
// 2. reset: assert asynchronously, release synchronously
initial begin
rst_n = 0;
repeat (4) @(posedge clk);
rst_n <= 1;
end
// 3. interface instance — the one physical copy of the pins
bus_if bif (.clk(clk), .rst_n(rst_n));
// 4. DUT hookup through the modport
simple_dut u_dut (
.clk (clk),
.rst_n (rst_n),
.valid (bif.valid),
.write (bif.write),
.addr (bif.addr),
.wdata (bif.wdata),
.ready (bif.ready),
.rdata (bif.rdata)
);
// 5+6. hand the vif into the class world, then run
base_test t;
initial begin
t = new();
t.vif = bif; // virtual interface assignment
wait (rst_n === 1); // do not drive into reset
t.run(); // everything else happens in classes
$display("TEST DONE");
$finish;
end
// 7. watchdog: never let a hung handshake run forever
initial begin
#1ms;
$display("FATAL: global timeout");
$finish;
end
initial begin
$dumpfile("waves.vcd");
$dumpvars(0, tb_top);
end
endmoduleWalkthrough
The interface owns clocking blocks: drv_cb for driving with output skew, mon_cb input-only for sampling.
Reset is asserted at time 0 and released with a nonblocking assignment on a clock edge — clean, race-free release.
t.vif = bif copies a handle, not the pins — every class holding this vif sees the same physical interface.
wait (rst_n === 1) before run() keeps class-world driving out of the reset window (the driver still re-checks reset itself).
The watchdog initial block is non-negotiable in any handshake protocol TB — a missing ready hangs forever without it.
The vif handoff pattern
Classes cannot instantiate interfaces; they hold virtual bus_if handles. The handoff flows top-down: tb_top assigns the test's vif, the test passes it into the env, and the env distributes it to driver and monitor at construction time. In hand-built testbenches this replaces UVM's uvm_config_db.
class driver;
virtual bus_if vif; // handle, set by env before run()
// ...
endclass
class env;
virtual bus_if vif;
driver drv;
monitor mon;
function void build();
drv = new(); mon = new();
drv.vif = vif; // distribute downward
mon.vif = vif;
endfunction
endclassInterview angle
Expect: “why virtual interfaces at all?” The crisp answer: modules and interfaces are static and elaborated; classes are dynamic and constructed at run time. A class cannot contain an interface, so it holds a typed reference — the virtual interface — which is assigned at run time and decouples class code from any specific interface instance, enabling multi-instance reuse.
Key takeaways
tb_top owns all static structure: clocks, reset, interface instance, DUT, kickoff, watchdog.
Clocking blocks in the interface give race-free driving (output skew) and sampling (#1step input).
The vif handoff is top-down at build time: tb_top → test → env → driver/monitor.
Always include a global timeout watchdog — handshake protocols hang silently without one.
Common pitfalls
Driving interface signals from both tb_top and the driver class — multiply-driven X soup.
Forgetting to assign vif before run() — null virtual interface access, often a confusing fatal at first pin touch.
Releasing reset with a blocking assignment at the same edge the driver starts — classic time-zero race.
Putting protocol behavior (handshake waits) in tb_top initial blocks — that logic belongs in the driver class.