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.

diagram
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

systemverilog
// ---------- 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
endmodule

Walkthrough

  1. The interface owns clocking blocks: drv_cb for driving with output skew, mon_cb input-only for sampling.

  2. Reset is asserted at time 0 and released with a nonblocking assignment on a clock edge — clean, race-free release.

  3. t.vif = bif copies a handle, not the pins — every class holding this vif sees the same physical interface.

  4. wait (rst_n === 1) before run() keeps class-world driving out of the reset window (the driver still re-checks reset itself).

  5. 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.

systemverilog
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
endclass

Interview 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.