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
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
    • Events, Semaphores, Mailboxes and Processes
      • Events: Edge Waits and Level Fixes
      • Semaphores: One Key and Many
      • Mailboxes: Typed Pipes with Backpressure
      • Threads and Control: Fork, Disable, Wait, Process
      • Synchronization Patterns: Handshakes, Phasing and Objections
      • Lab: Generator-to-Driver Handshake
    • OOP Patterns for Testbenches
  • 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
  • Principal-Level Verification28
Loading lesson

‹Events, Semaphores, Mailboxes and Processes
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Events, Semaphores, Mailboxes and Processes
  5. Lab: Generator-to-Driver Handshake

Part 2 · OOP for Verification · Intermediate

Lab: Generator-to-Driver Handshake

A deterministic generator-driver handshake bench with counters and drain.

On this page

    Why this matters

    Every class-based testbench starts with this loop: make an item, hand it over, drive it, count it. Getting the handoff and the finish right once makes every later bench simpler.

    After this lesson you can: build a generator, bounded mailbox and driver with a completion counter, and connect it to a DUT through a virtual interface.

    What the lab builds

    This lab assembles the topic into one bench: a generator fills a bounded two-slot mailbox, a driver moves each item across a two-clock transfer, and a completion counter ends the test. No DUT is needed.

    One item at a time: generator, bounded mailbox, driver, pins, and a counter that ends the test.

    The complete bench

    The class version: a generator puts randomized items into a two-slot mailbox, and a driver takes each one and spends two clocks (20 ns) on it. randomize() picks values for the rand fields that obey the constraint; the Randomization part teaches both.

    top_lab.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
    timeunit 1ns;      // one time unit for the classes and the module
    timeprecision 1ps;
    
    class txn_c;
      rand bit [15:0] addr;
      rand bit [31:0] data;
      constraint c_addr { addr inside {[16'h0000:16'h00FF]}; }
    endclass
    class generator_c2;
      mailbox #(txn_c) mb;
      function new(mailbox #(txn_c) m);
        mb = m;
      endfunction
      task run(int n);
        repeat (n) begin
          txn_c t = new();
          void'(t.randomize());
          mb.put(t);
        end
      endtask
    endclass
    class driver_c2;
      mailbox #(txn_c) mb;
      int done_count;
      function new(mailbox #(txn_c) m);
        mb = m;
      endfunction
      task run();
        txn_c t;
        forever begin
          mb.get(t);
          #20;            // the two-clock transfer, pins omitted
          done_count++;
        end
      endtask
    endclass
    module top_lab;
      mailbox #(txn_c) mb;
      generator_c2 gen;
      driver_c2 drv;
      initial begin
        mb  = new(2);
        gen = new(mb);
        drv = new(mb);
        fork
          gen.run(8);
          drv.run();      // daemon: never returns
        join_none
        wait (drv != null && drv.done_count == 8);   // null guard: Verilator evaluates this early
        $display("PASS: 8 of 8 randomized items driven");
        $finish;
      end
    endmodule
    Terminal
    PASS: 8 of 8 randomized items driven

    The same bench without classes

    Eight items, deterministic values, blocking handoff both ways, nonblocking drives at clock edges, and a level-sensitive finish condition, all in one module.

    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    module tb_gen_drv_handshake;
      timeunit 1ns;
      timeprecision 1ps;
      bit clk = 0;
      always #5 clk = ~clk;
      int q_addr[2];
      int q_data[2];
      int head = 0;
      int tail = 0;
      int count = 0;
      int errors = 0;
      int generated = 0;
      int done_count = 0;
      logic bus_valid = 0;
      logic [15:0] bus_addr = 0;
      logic [31:0] bus_data = 0;
      int beats = 0;
      always @(posedge clk)   // monitor: checks every beat on the bus itself
        if (bus_valid) begin
          if (bus_addr !== beats[15:0]) errors++;
          if (bus_data !== (32'hA000 + beats)) errors++;
          beats++;
        end
      initial begin : producer
        for (int i = 0; i < 8; i++) begin
          wait (count < 2);
          q_addr[tail] = i;
          q_data[tail] = 32'hA000 + i;
          tail = (tail + 1) % 2;
          count++;
          generated++;
        end
      end
      initial begin : consumer
        int want = 0;
        int got_addr = 0;
        int got_data = 0;
        repeat (8) begin
          wait (count > 0);
          got_addr = q_addr[head];
          got_data = q_data[head];
          head = (head + 1) % 2;
          count--;
          @(posedge clk);
          bus_valid <= 1;
          bus_addr <= got_addr[15:0];
          bus_data <= got_data;
          @(posedge clk);
          bus_valid <= 0;
          if (got_addr !== want) errors++;
          if (got_data !== (32'hA000 + want)) errors++;
          want++;
          done_count++;
        end
      end
      initial begin
        wait (done_count == 8);
        #1;   // let the monitor sample the last beat at this edge
        if (generated !== 8) errors++;
        if (count !== 0) errors++;
        if (beats !== 8) errors++;
        if (errors == 0) $display("PASS: 8 of 8 items driven with 2-clock transfers");
        else $fatal(1, "FAIL: done=%0d generated=%0d", done_count, generated);
        $finish;
      end
    endmodule
    Terminal
    PASS: 8 of 8 items driven with 2-clock transfers

    Why each piece is shaped this way

    The mailbox, or the store in the module version, holds two items, small enough that the instant producer visibly stalls behind the two-clock driver. That stall is the backpressure lesson made concrete.

    Completion uses a counter with a level wait, immune to trigger order. An edge count on a done event would undercount whenever a trigger precedes its waiter, so the canonical form here is the counter.

    Drives use nonblocking assignment at clock edges. A blocking drive in the same timestep as a sampler would leave the sampled value to scheduler order.

    Common pitfall

    Joining the driver forever loop with plain join hangs the test, since the driver never returns. Fork daemons with join_none and block on the level completion condition instead.

    Interview

    Sketching this handshake is the standard whiteboard answer: mailbox for data, counter for completion, bounded depth for flow control.

    • Say why the bound is 2 and what new(0) would change.

    • Map generator to sequence and the handoff to item done.

    • Note the UVM driver port is not a mailbox, so habits must move up.

    Check yourself The mailbox bound changes from 2 to 0 with a slow driver. What changes in the run?

    Puts never block, so the queue grows without limit while the driver lags. The run still completes but memory creeps, hiding the slow consumer.

    Experiments and mapping

    Predict first Change mb = new(2) to mb = new(8). Does the generator still wait?

    No. All 8 items fit at once, so every put returns immediately; the run takes as long as before, because the driver is the slow part.

    Predict first Replace the done counter with an event the driver triggers after each item, and have the test count @(done_ev) in a loop. Can the count come up short?

    Yes. If a trigger fires while the test is not waiting at @(done_ev), that occurrence is lost; the counter with a level wait cannot miss one.

    Try both, and wrap the finish wait in a watchdog with a generous limit so a lost item fails instead of hanging.

    The structure maps onto UVM: generator to sequence, handoff to the sequencer-driver port, completion to item done. The UVM driver port is a different mechanism, so carry the pattern, not the mailbox.

    Project step

    Build PacketFIFO's stimulus path: pkt_gen puts fresh pkt_txn objects into a mailbox #(pkt_txn) of depth 2, pkt_driver gets each one and drives its bytes through a virtual pkt_if into PacketFIFO v0, and the bench checks every byte that comes out.

    One way to write it:

    pkt_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
    typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
    typedef bit [7:0] bytes_t[$];
    
    interface pkt_if (input logic clk, input logic rst_n);
      logic       valid, ready, last;
      logic [7:0] data;
    endinterface
    
    class pkt_txn;
      kind_e    kind;
      bit [1:0] dest;
      bit [7:0] payload[$];
      function bit [7:0] header();
        return {kind, dest, 4'(payload.size())};
      endfunction
      function bytes_t to_bytes();             // header, payload, check byte
        bytes_t b = {header()};
        bit [7:0] c = header();
        foreach (payload[i]) begin
          b.push_back(payload[i]);
          c ^= payload[i];
        end
        b.push_back(c);
        return b;
      endfunction
    endclass
    
    class pkt_gen;                             // makes packets, one fresh object each
      mailbox #(pkt_txn) mb;
      function new(mailbox #(pkt_txn) mb);
        this.mb = mb;
      endfunction
      task run(int n);
        for (int i = 0; i < n; i++) begin
          pkt_txn p = new();
          p.kind = (i % 2 == 1) ? CTRL : DATA;
          p.dest = 2'(i);
          p.payload = {8'hA5, 8'(i)};
          mb.put(p);                           // blocks while the mailbox is full
        end
      endtask
    endclass
    
    class pkt_driver;                          // takes packets and drives their bytes
      mailbox #(pkt_txn) mb;
      virtual pkt_if vif;
      int sent;
      function new(mailbox #(pkt_txn) mb, virtual pkt_if vif);
        this.mb = mb;
        this.vif = vif;
      endfunction
      task run();
        pkt_txn p;
        bytes_t b;
        forever begin
          mb.get(p);
          b = p.to_bytes();
          foreach (b[i]) begin
            @(posedge vif.clk);
            vif.valid <= 1'b1;
            vif.data  <= b[i];
            vif.last  <= (i == b.size() - 1);
            do @(posedge vif.clk); while (!vif.ready);
            vif.valid <= 1'b0;
          end
          sent++;
        end
      endtask
    endclass
    
    // DEPTH is a power of two of at least 2, so PTR_W is at least 1.
    module fifo #(parameter int DEPTH = 16, parameter int DATA_W = 32) (
      input  logic              clk, rst_n, wr_en, rd_en,
      input  logic [DATA_W-1:0] wdata,
      output logic [DATA_W-1:0] rdata,
      output logic              full, empty
    );
      localparam int PTR_W = $clog2(DEPTH);
      logic [DATA_W-1:0] mem [DEPTH];
      logic [PTR_W:0] wr_ptr, rd_ptr;
      assign empty = (wr_ptr == rd_ptr);
      assign full  = (wr_ptr[PTR_W] != rd_ptr[PTR_W]) &&
                     (wr_ptr[PTR_W-1:0] == rd_ptr[PTR_W-1:0]);
      assign rdata = mem[rd_ptr[PTR_W-1:0]];
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          wr_ptr <= '0;
          rd_ptr <= '0;
        end else begin
          if (wr_en && !full) begin
            mem[wr_ptr[PTR_W-1:0]] <= wdata;
            wr_ptr <= wr_ptr + 1;
          end
          if (rd_en && !empty) rd_ptr <= rd_ptr + 1;
        end
      end
    endmodule
    
    // PacketFIFO v0 with pkt_if on both sides.
    module pkt_fifo (pkt_if in, pkt_if out);
      logic       full, empty;
      logic [8:0] rdata;
      fifo #(.DEPTH(4), .DATA_W(9)) u_f (
        .clk(in.clk), .rst_n(in.rst_n), .wr_en(in.valid), .rd_en(out.ready),
        .wdata({in.last, in.data}), .rdata(rdata), .full(full), .empty(empty));
      assign in.ready  = !full;
      assign out.valid = !empty;
      assign {out.last, out.data} = rdata;
    endmodule
    
    Terminal
    out: '{'h2, 'ha5, 'h0, 'ha7, 'h52, 'ha5, 'h1, 'hf6, 'h22, 'ha5, 'h2, 'h85}
    PASS: 3 packets went generator -> mailbox -> pkt_driver -> PacketFIFO and came out byte for byte

    Remember

    • Mailbox for data, bounded for flow control, a counter for completion.

    • Fork daemons with join_none and wait on the finish condition.

    • Build a fresh object for every item.

    Rule of thumb: the test ends on a count, never on a delay.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    The finish wait in the PacketFIFO bench polls a class field once per clock: Verilator 5.048 evaluated a wait() on that field before the class handle was set and stopped with a null-pointer error.

    Related topics

    • Next topic: OOP Patterns

    Previous

    59 / 259

    Next: OOP Patterns for Testbenches

    Up next

    Up nextOOP Patterns for Testbenches

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Events: Edge Waits and Level Fixes
    • Semaphores: One Key and Many
    • Mailboxes: Typed Pipes with Backpressure