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
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
    • Senior Debug Playbook
    • Reviewing Verification Code
    • Verification Strategy: Spec to Sign-Off
    • SystemVerilog Interview Q&A Bank
    • Capstone: Verify a Parameterized FIFO
      • Spec Analysis and Verification Plan
      • Harness and Stimulus
      • Monitor, Scoreboard and Model
      • Assertions, Coverage and Closure
  • Principal-Level Verification28
Loading lesson

‹Capstone: Verify a Parameterized FIFO
  1. Topics
  2. SystemVerilog
  3. Senior & Interview Preparation
  4. Capstone: Verify a Parameterized FIFO
  5. Harness and Stimulus

Part 8 · Senior & Interview Prep · Intermediate

Harness and Stimulus

Interface, harness, transactions, generator, driver and environment, with a runnable demo.

On this page

    The interface

    One interface carries both sides plus the flags. The monitors need cross-side visibility, so splitting push and read sides would blind them.

    The listing below is an interface with no testbench, so this course's checks do not run it on its own.

    fifo_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
    // not-run: fragment: an interface only, no top module
    interface fifo_if #(parameter int WIDTH = 8)
              (input logic clk, input logic rst_n);
      timeunit 1ns; timeprecision 1ps;
    
      // write (push) side
      logic             in_valid;
      logic             in_ready;
      logic [WIDTH-1:0] in_data;
    
      // read (pop) side
      logic             out_valid;
      logic             out_ready;
      logic [WIDTH-1:0] out_data;
    
      // status
      logic             full;
      logic             empty;
    
      // driver clocking: sample pre-edge, drive 2ns after the edge
      clocking drv_cb @(posedge clk);
        default input #1step output #2ns;
        output in_valid, in_data, out_ready;
        input  in_ready, out_valid, out_data, full, empty;
      endclocking
    
      // monitor clocking: inputs only — a monitor cannot drive, by construction
      clocking mon_cb @(posedge clk);
        default input #1step;
        input in_valid, in_ready, in_data;
        input out_valid, out_ready, out_data;
        input full, empty;
      endclocking
    
      modport DRV (clocking drv_cb, input rst_n);
      modport MON (clocking mon_cb, input rst_n);
    endinterface
    • Both clocking blocks sample pre-edge values, exactly what the DUT flops see.

    • Only the three TB-driven signals are clocking outputs, driven 2ns past the edge, clear of hold.

    • The monitor block is input-only, so a monitor cannot cause contention by construction.

    • Reset stays outside the clocking blocks, since reset logic needs raw asynchronous access.

    Watch in_ready and full. The fourth push parks the FIFO at full, and the 0xB1 offer is ignored while the pop proceeds. Each slot shows values just after its rising edge: the edge that starts slot 5 saw in_ready 0 from slot 4, so 0xB1 was not taken, and in_valid drops before the next edge.

    • Slot 0: reset: in_valid 0, in_ready 1, full 0, empty 1.

    • Slot 1: first push 0xA1, empty falls.

    • Slot 2: push 0xA2.

    • Slot 3: push 0xA3.

    • Slot 4: fourth push 0xA4 fills the FIFO: full 1, in_ready 0.

    • Slot 5: out_ready 1: the pop proceeds while in_valid is still 1, so 0xB1 is ignored and count falls to 3.

    • Slot 6: drain continues, count 2.

    • Slot 7: count 1, out_valid still 1.

    Harness

    tb_top owns the clock, the reset, the single interface instance, the DUT and the environment. Nothing else lives at the top.

    This harness is not run in this course's checks on its own. It needs the fifo RTL, which ships separately, and the environment classes from the listings below.

    design.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
    // not-run: fragment: needs the fifo RTL (shipped separately) and the env classes from the other listings
    module tb_top;
      timeunit 1ns; timeprecision 1ps;
      parameter int WIDTH = 8;
      parameter int DEPTH = 16;
    
      logic clk;
      logic rst_n;
    
      // clock: 100 MHz
      initial clk = 0;
      always #5 clk = ~clk;
    
      // reset: assert async, deassert at the inactive (falling) edge,
      // so no flop sees rst_n change at the same edge it samples
      initial begin
        rst_n = 0;
        repeat (4) @(posedge clk);
        @(negedge clk);
        rst_n = 1;
      end
    
      // the one real interface instance
      fifo_if #(.WIDTH(WIDTH)) fif (.clk(clk), .rst_n(rst_n));
    
      // DUT hookup
      fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
        .clk       (clk),
        .rst_n     (rst_n),
        .in_valid  (fif.in_valid),
        .in_ready  (fif.in_ready),
        .in_data   (fif.in_data),
        .out_valid (fif.out_valid),
        .out_ready (fif.out_ready),
        .out_data  (fif.out_data),
        .full      (fif.full),
        .empty     (fif.empty)
      );
    
      // environment: generator, driver, monitors, scoreboard (built below)
      fifo_env #(.WIDTH(WIDTH), .DEPTH(DEPTH)) e;
    
      initial begin
        e = new(fif);
        wait (rst_n == 1);
        repeat (2) @(posedge clk);   // two clean edges: no stimulus into reset
        e.run();
      end
    
      // global watchdog — a hang becomes a report, not a lost regression slot
      initial begin
        #2ms;
        $fatal(1, "GLOBAL TIMEOUT: test did not finish");
      end
    endmodule
    • The timeunit gives the unitless #5 its unit. #2ms is a time literal and carries its own.

    • Reset deasserts at a falling edge, away from the rising edge the flops sample on, and the environment waits two more edges.

    • Exactly one interface instance exists. Every component receives a virtual handle to it.

    Transactions and the generator

    The transaction carries two constraint layers. The legality layer encodes what is always true, and the scenario layer uses soft constraints and knob fields so tests dial traffic shape without new classes.

    These listings are class declarations with no top module, so this course's checks do not run them.

    design.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
    // not-run: fragment: class declarations only, no top module
    typedef enum bit [1:0] { OP_PUSH, OP_POP, OP_BOTH, OP_IDLE } op_e;
    
    class fifo_txn #(parameter int WIDTH = 8);
      // randomized fields
      rand op_e             op;
      rand bit [WIDTH-1:0]  data;
      rand int unsigned     gap;        // idle cycles before this op
    
      // scenario knobs — set by the generator, not randomized
      int push_wt  = 5;                 // relative op weights
      int pop_wt   = 5;
      int both_wt  = 2;
      int idle_wt  = 1;
      int max_gap  = 3;
    
      // legality layer (hard)
      constraint c_gap_legal { gap <= max_gap; }
    
      // scenario layer (weights via knobs; soft data shaping)
      constraint c_op_dist {
        op dist { OP_PUSH := push_wt, OP_POP  := pop_wt,
                  OP_BOTH := both_wt, OP_IDLE := idle_wt };
      }
      constraint c_data_interesting {
        if (WIDTH > 1 && WIDTH < 30) {
          soft data dist { '0 := 1, '1 := 1, [1:(2**WIDTH)-2] :/ 8 };
        } else {
          // WIDTH 1 has no middle values; very wide WIDTH skips the
          // enumerated middle range (2**WIDTH overflows an int).
          soft data dist { '0 := 1, '1 := 1 };
        }
      }
    
      function void display(string tag);
        $display("[%0t] %s op=%s data=0x%0h gap=%0d",
                 $time, tag, op.name(), data, gap);
      endfunction
    endclass
    design.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
    // not-run: fragment: needs fifo_txn from the listing above and has no top module
    typedef enum { SCN_SMOKE, SCN_BURST_FILL, SCN_DRAIN,
                   SCN_MIXED, SCN_BACKPRESSURE } scenario_e;
    
    class generator #(parameter int WIDTH = 8);
      mailbox #(fifo_txn #(WIDTH)) gen2drv;
      int n_txns = 200;
    
      function new(mailbox #(fifo_txn #(WIDTH)) mb);
        gen2drv = mb;
      endfunction
    
      // configure one txn's knobs for a scenario phase
      function void shape(fifo_txn #(WIDTH) t, scenario_e s);
        case (s)
          SCN_BURST_FILL:   begin t.push_wt=10; t.pop_wt=0;  t.both_wt=0; t.idle_wt=0; t.max_gap=0; end
          SCN_DRAIN:        begin t.push_wt=0;  t.pop_wt=10; t.both_wt=0; t.idle_wt=0; t.max_gap=0; end
          SCN_MIXED:        begin t.push_wt=5;  t.pop_wt=5;  t.both_wt=3; t.idle_wt=1; t.max_gap=2; end
          SCN_BACKPRESSURE: begin t.push_wt=8;  t.pop_wt=1;  t.both_wt=0; t.idle_wt=1; t.max_gap=1; end
          default:          begin t.push_wt=6;  t.pop_wt=4;  t.both_wt=1; t.idle_wt=1; t.max_gap=3; end
        endcase
      endfunction
    
      task run_scenario(scenario_e s, int count);
        fifo_txn #(WIDTH) t;
        repeat (count) begin
          t = new();
          shape(t, s);
          if (!t.randomize())
            $fatal(1, "generator: randomize failed in scenario %0d", s);
          gen2drv.put(t);              // bounded mailbox: backpressure for free
        end
      endtask
    
      // default mix: fill, drain, then sustained mixed traffic
      task run();
        run_scenario(SCN_BURST_FILL,   n_txns/4);  // drive toward full (F-02)
        run_scenario(SCN_DRAIN,        n_txns/4);  // drive toward empty (F-03)
        run_scenario(SCN_MIXED,        n_txns/2);  // hammer simultaneity (F-07)
      endtask
    endclass
    Diagram
    FILL LEVEL OVER ONE DEFAULT RUN
    
      DEPTH ┤        ╭────╮ burst-fill parks at full ── F-02, F-05 hit
            │       ╱      ╲
            │      ╱        ╲ drain to empty ── F-03, F-06 hit
            │     ╱          ╲      ╭─╮ ╭──╮
            │    ╱            ╲    ╱   ╲╱   ╲   mixed: boundary crossings,
          0 ┤───╯              ╰──╯          ╰─ simultaneity, wrap-around
            └────────────────────────────────────────► time
            flat 50/50 instead: ~~~~~ half-full noise, boundaries never seen
    • A flat 50/50 push/pop mix hovers around half-full forever. Phased scenarios walk the fill level to each boundary instead.

    • Re-shaping traffic is an assignment to knobs, not a new class. Directed tests pin data because the distribution is soft.

    • The generator checks every randomize call. A knob typo fails loudly instead of freezing stimulus.

    Driver and environment

    The driver translates each transaction into pin wiggles on the driver clocking block. The environment builds every mailbox, starts every thread, and wires reset and end-of-test discipline.

    These listings are class declarations that depend on the other listings, with no top module, so this course's checks do not run them.

    design.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
    // not-run: fragment: needs fifo_if and fifo_txn from the listings above and has no top module
    class fifo_driver #(parameter int WIDTH = 8);
      virtual fifo_if #(WIDTH).DRV vif;
      mailbox #(fifo_txn #(WIDTH)) gen2drv;
    
      function new(virtual fifo_if #(WIDTH).DRV v,
                   mailbox #(fifo_txn #(WIDTH)) mb);
        vif = v;
        gen2drv = mb;
      endfunction
    
      task run();
        fifo_txn #(WIDTH) t;
        forever begin
          gen2drv.get(t);
          repeat (t.gap) @(vif.drv_cb);   // pacing gap: idle cycles before the op
          if (!vif.rst_n) continue;       // reset-skip: never drive into reset
          case (t.op)
            OP_PUSH: begin vif.drv_cb.in_valid <= 1'b1; vif.drv_cb.in_data <= t.data; vif.drv_cb.out_ready <= 1'b0; end
            OP_POP:  begin vif.drv_cb.in_valid <= 1'b0; vif.drv_cb.out_ready <= 1'b1; end
            OP_BOTH: begin vif.drv_cb.in_valid <= 1'b1; vif.drv_cb.in_data <= t.data; vif.drv_cb.out_ready <= 1'b1; end
            OP_IDLE: begin vif.drv_cb.in_valid <= 1'b0; vif.drv_cb.out_ready <= 1'b0; end
          endcase
          @(vif.drv_cb);                  // hold one cycle so the handshake can complete
          vif.drv_cb.in_valid <= 1'b0;    // return to idle: every op is one cycle wide
          vif.drv_cb.out_ready <= 1'b0;
        end
      endtask
    endclass
    design.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
    // not-run: fragment: needs the generator, driver, monitor and scoreboard classes and has no top module
    class fifo_env #(parameter int WIDTH = 8, parameter int DEPTH = 16);
      virtual fifo_if #(WIDTH) vif;
      mailbox #(fifo_txn #(WIDTH)) gen2drv;
      mailbox #(logic [WIDTH-1:0]) mb_in, mb_out;   // 4-state: an X word survives
      generator #(WIDTH) gen;
      fifo_driver #(WIDTH) drv;
      fifo_in_mon #(WIDTH) in_mon;
      fifo_out_mon #(WIDTH) out_mon;
      fifo_sb #(WIDTH) sb;
      int n_txns = 200;
    
      function new(virtual fifo_if #(WIDTH) v);
        vif = v;
        gen2drv = new(1);               // bounded: generator blocks when driver stalls
        mb_in = new();
        mb_out = new();
        gen = new(gen2drv);
        drv = new(vif, gen2drv);
        in_mon = new(vif, mb_in);
        out_mon = new(vif, mb_out);
        sb = new(mb_in, mb_out);
        gen.n_txns = n_txns;
      endfunction
    
      // reset asserted mid-traffic: the model drops in-flight data (spec rule 8)
      task reset_watcher();
        forever begin
          @(negedge vif.rst_n);
          sb.flush();
        end
      endtask
    
      task run();
        fork
          drv.run();
          in_mon.run();
          out_mon.run();
          sb.run();
          reset_watcher();
        join_none
        gen.run();                      // blocking: returns after n_txns
        // num() is a method call, not a signal, so wait() on it may never wake: poll per clock
        while (gen2drv.num() != 0 || mb_in.num() != 0 || mb_out.num() != 0) @(vif.mon_cb);
        repeat (5) @(vif.mon_cb);        // settle: last beats cross the scoreboard
        // the pins carry no count, so the test supplies DUT occupancy
        // with a hierarchical probe (or the L4 coverage mirror cov.count)
        sb.report(tb_top.dut.count);
      endtask
    endclass
    • Every op maps to pins explicitly: push and pop each hold for one cycle, then return to idle.

    • The bounded mailbox couples generator and driver loosely. A slow DUT stalls the generator, not the test.

    • Reset has two wirings: the driver skips reset cycles, and the watcher flushes the scoreboard on reset assert.

    Testbench

    A depth-4 FIFO with a directed handshake test. It pushes four words, proves full, then pops all four in order and proves empty.

    fifo4.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
    module fifo4 (
      input  logic       clk,
      input  logic       rst_n,
      input  logic       in_valid,
      output logic       in_ready,
      input  logic [7:0] in_data,
      output logic       out_valid,
      input  logic       out_ready,
      output logic [7:0] out_data,
      output logic       full,
      output logic       empty
    );
      localparam int DEPTH = 4;
      logic [7:0] mem [DEPTH];
      logic [1:0] wr_ptr = 2'd0, rd_ptr = 2'd0;
      logic [2:0] count = 3'd0;
      wire push = in_valid && in_ready;
      wire pop = out_valid && out_ready;
      assign full = (count == 3'd4);
      assign empty = (count == 3'd0);
      assign in_ready = !full;
      assign out_valid = !empty;
      assign out_data = mem[rd_ptr];
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          count <= 3'd0;
          wr_ptr <= 2'd0;
          rd_ptr <= 2'd0;
        end else begin
          if (push) mem[wr_ptr] <= in_data;
          if (push) wr_ptr <= wr_ptr + 2'd1;
          if (pop) rd_ptr <= rd_ptr + 2'd1;
          if (push && !pop) count <= count + 3'd1;
          else if (pop && !push) count <= count - 3'd1;
        end
      end
    endmodule
    

    Common pitfall

    Driving DUT inputs from raw interface signals instead of the driver clocking block brings the races back. Forgetting the watchdog lets a hung corner test burn the nightly regression slot.

    Interview

    Likely question: “Burst traffic never reaches full. What do you change?”

    • Phase the stimulus: burst-fill, then drain, then mixed.

    • Shape with knobs, not new classes: push weight up, gap to zero.

    • Prove the corner with a cover bin, not a waveform glance.

    Check yourself The generator sets push weight 10 and pop weight 0 with max_gap 0. What fill level does the run approach, and which features does that target?

    Full. Sustained pushes with no pops park at full, targeting F-02 (full exact) and F-05 (push blocked).

    Check yourself A teammate writes the data shaping as a hard data inside {'0, '1};. A directed test then pins data to A5 with an inline constraint and randomize fails. What is wrong?

    Scenario shaping went into a hard constraint, so it fights the directed test. Shaping must be soft, as c_data_interesting is, so directed tests override it without conflict.

    Related topics

    • Monitor, Scoreboard and Model

    Previous

    229 / 259

    Next: Monitor, Scoreboard and Model

    Up next

    Up nextMonitor, Scoreboard and Model

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Assertions, Coverage and Closure
    clkin_validin_readyin_data0a1a2a3a4b1out_readyout_validfullempty
    Four pushes fill a depth-4 FIFO, then a drain with the producer still offering