Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
    • Testbench Anatomy & Layering
      • The Layered Testbench
      • The Static Harness and Environment
      • Tests as Classes
      • Complete Mini Testbench
    • Stimulus: Generators, Transactions & Drivers
    • Monitors, Scoreboards & Reference Models
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Testbench Anatomy & Layering
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. Testbench Anatomy & Layering
  5. The Static Harness and Environment

Part 6 · Testbench Architecture · Intermediate

The Static Harness and Environment

Clock, reset, interface, DUT, virtual-interface handoff, environment wiring, and the program debate.

On this page

    What lives in tb_top

    tb_top is the only module in the testbench. It owns everything that must exist at elaboration, and it holds no behavior: all behavior lives in classes.

    Diagram
    tb_top RESPONSIBILITIES
    
      1. clock gen      always #5 clk = ~clk
      2. reset gen      assert, hold, release synchronously
      3. interface      bus_if bif (clk, rst_n)
      4. DUT hookup     per-pin connections from bif signals
      5. vif handoff    test.vif = bif, into the class world
      6. kickoff        wait out of reset, then t.run()
      7. plumbing       waves, timeout watchdog, finish
    
      Static structure here, dynamic behavior in classes.

    Clocking block

    Direction

    Job

    drv_cb

    Driver writes with output skew, reads with input sampling

    Race-free driving just after the edge

    mon_cb

    Input only, sampled just before the edge

    Race-free observation of settled pins

    One harness, fixed

    The full harness below runs in this course's checks on Verilator 5.048. It ends with a verdict: every generated transaction must have completed its handshake at the pins.

    bus_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    `timescale 1ns/1ps
    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
    endinterface
    
    module simple_dut (
      input  logic        clk,
      input  logic        rst_n,
      input  logic        valid,
      input  logic        write,
      input  logic [15:0] addr,
      input  logic [31:0] wdata,
      output logic        ready,
      output logic [31:0] rdata
    );
      logic [31:0] mem [0:65535];
      assign ready = 1'b1;
      always @(posedge clk or negedge rst_n) begin
        if (!rst_n) rdata <= 32'h0;
        else if (valid && ready && write) mem[addr] <= wdata;
        else if (valid && ready && !write) rdata <= mem[addr];
      end
    endmodule
    
    class bus_txn;
      rand bit         write;
      rand bit [15:0]  addr;
      rand bit [31:0]  data;
      bit [31:0]       rdata;
      function string sprint();
        return $sformatf("%s addr=%h data=%h rdata=%h",
                         write ? "WR" : "RD", addr, data, rdata);
      endfunction
    endclass
    
    class generator;
      mailbox #(bus_txn) out;
      int unsigned n;
      bit done_flag = 0;
      event done;
      function new(mailbox #(bus_txn) out, int unsigned n);
        this.out = out; this.n = n;
      endfunction
      task run();
        repeat (n) begin
          bus_txn t = new();
          if (!t.randomize()) begin
            $display("GENERATOR: randomize failed, stopping");
            $finish;
          end
          out.put(t);
        end
        done_flag = 1;
        -> done;
      endtask
    endclass
    
    class driver;
      mailbox #(bus_txn) in;
      virtual bus_if vif;
      int unsigned n_done = 0;     // handshakes completed at the pins
      function new(mailbox #(bus_txn) in);
        this.in = in;
      endfunction
      task run();
        forever begin
          bus_txn t;
          in.get(t);
          @(vif.drv_cb);
          vif.drv_cb.valid <= 1;
          vif.drv_cb.write <= t.write;
          vif.drv_cb.addr  <= t.addr;
          vif.drv_cb.wdata <= t.data;
          do @(vif.drv_cb); while (vif.drv_cb.ready !== 1);
          vif.drv_cb.valid <= 0;
          n_done++;
        end
      endtask
    endclass
    
    class env;
      virtual bus_if vif;
      int unsigned num_txns = 100;
      int unsigned timeout_cycles = 50000;   // 500 us: inside the 1 ms watchdog
      generator gen;
      driver    drv;
      function void build();
        mailbox #(bus_txn) gen2drv = new(1);
        gen = new(gen2drv, num_txns);
        drv = new(gen2drv);
        drv.vif = vif;
      endfunction
      task run();
        bit finished = 0;
        fork
          begin
            fork
              gen.run();
              drv.run();
            join_none
            // Generator done is not enough: the last items may still sit
            // in the mailbox or in the driver. End on driver completion.
            wait (gen.done_flag && drv.n_done == num_txns);
            finished = 1;
          end
          begin
            repeat (timeout_cycles) @(posedge vif.clk);
            if (!finished) $display("ENV: end-of-test timeout reached");
          end
        join_any
        disable fork;
      endtask
    endclass
    
    class base_test;
      env e;
      virtual bus_if vif;
      virtual task run();
        e = new();
        e.vif = vif;
        e.build();
        e.run();
      endtask
    endclass
    

    Four legacy defects are fixed here. The timescale covers the millisecond watchdog, and the unused modport is deleted because the DUT has plain ports. The generator sets a done flag before triggering its event, so an early finish cannot miss the wakeup. The environment ends on the driver's completion count, not on the generator: when the generator finishes, one item can still sit in the mailbox and one in the driver, and disable fork would kill them.

    The listing's DUT ties ready high, so it never stalls. This wave uses a DUT that drops ready, to show the driver's wait. Watch valid against ready. A transfer happens only in slots where both are high.

    • Slot 0: idle, valid 0 and ready 0, data 0.

    • Slot 1: driver has asserted valid with data a1, DUT not yet ready.

    • Slot 2: ready joins, so the a1 transfer completes here.

    • Slot 3: driver has returned valid low for one idle cycle while data holds a1.

    • Slot 4: driver presents a new attempt, valid 1 with data b2, ready 0.

    • Slot 5: ready joins and the b2 transfer completes.

    The virtual-interface handoff

    Classes cannot contain interfaces, so each class holds a typed reference called a virtual interface. The handoff flows top-down at build time: tb_top assigns the test handle, and the environment distributes it to driver and monitor before anything runs.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // snippet
    // Connection by assignment: every holder of the handle
    // sees the same physical interface instance.
    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

    Expect why virtual interfaces exist at all.

    • Modules elaborate, classes construct at run time, and a class cannot contain an interface.

    • The virtual handle decouples class code from any one instance, which is what allows multi-instance reuse.

    Program blocks: use or skip

    A program block runs its statements in the Reactive region, after design activity for the timeslot has settled. The theory is that testbench code then always sees post-update values, so a class of races disappears by construction.

    Most modern flows skip program blocks anyway. Clocking blocks fix races at the pin boundary where they actually occur, with simpler semantics than global scheduling regions.

    automatic.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // snippet
    // Approach A: program block (Spear-era style)
    program automatic test_prog (bus_if bif);
      initial begin
        base_test t = new();
        t.vif = bif;
        t.run();              // runs in the Reactive region
      end
    endprogram                  // sim ends when this initial completes
    
    // Approach B: plain module plus clocking blocks (industry-common)
    module tb_top_plain;
      // ... clock, reset, interface bif, DUT as in the harness above ...
      base_test t;
      initial begin
        t = new();
        // t.vif = bif; t.run();
      end
      // Race safety comes from the interface clocking blocks:
      //   @(vif.drv_cb)  vif.drv_cb.valid <= 1;   // skewed drive
      //   data = vif.mon_cb.rdata;                // #1step sample
    endmodule
    • Implicit end of simulation when program initials finish fights explicit end-of-test orchestration.

    • Clocking blocks still required: program schedules code late but does not specify drive skew at the pins.

    • UVM runs in modules, so mixing Reactive-region semantics with UVM phasing buys confusion, not safety.

    Common pitfall

    Driving interface signals from both tb_top and the driver class makes multiply-driven soup. Forgetting the vif assignment before run() fails at the first pin touch, and releasing reset with a blocking assignment on the driver start edge rebuilds the time-zero race.

    Testbench

    A runnable subset of the harness discipline: negedge drives, settled sampling, and a bounded ready wait around a small DUT.

    hs_dut.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    `timescale 1ns/1ps
    module hs_dut (
      input  logic        clk,
      input  logic        rst_n,
      input  logic        valid,
      input  logic        write,
      input  logic [3:0]  addr,
      input  logic [31:0] wdata,
      output logic        ready,
      output logic [31:0] rdata
    );
      logic [31:0] mem [0:15];
      assign ready = 1'b1;
      always @(posedge clk or negedge rst_n) begin
        if (!rst_n) rdata <= 32'h0;
        else if (valid && write) begin
          mem[addr] <= wdata;
          rdata <= 32'h0;
        end
        else if (valid && !write) rdata <= mem[addr];
      end
    endmodule
    
    Check yourself Why does the environment wait on gen.done_flag instead of gen.done.triggered?

    triggered is true only during the timestep that fires the event. A generator that finishes before the environment starts waiting misses the wakeup and hangs until the watchdog. The flag stays set, so the wait always observes it.

    Related topics

    • Synchronization

    Previous

    164 / 259

    Next: Tests as Classes

    Up next

    Up nextTests as Classes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Complete Mini Testbench
    clkvalidreadydata0a1b2
    A generic valid/ready handshake against a DUT that stalls