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 Foundations35
  • Object-Oriented SystemVerilog for Verification35
  • Constraint Randomization — Interview-Critical Deep Dive66
  • SystemVerilog Assertions — Interview-Critical Deep Dive63
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog38
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
    • Senior Debug Playbook
    • Reviewing Verification Code
    • Verification Strategy: Spec to Sign-Off
    • SystemVerilog Interview Q&A Bank
      • Q&A: Language Core
      • Q&A: Arrays & Queues
      • Q&A: Testbench Plumbing
      • Q&A: Classes and Interfaces
      • Q&A: OOP
      • Q&A: Constraint and Assertion Fire-Drill
      • Q&A: Interfaces & Virtual Interfaces
      • Q&A: Processes & IPC
      • Q&A: Scenario and Design Questions
      • Q&A: Constraints Quick-Fire
      • Q&A: SVA Quick-Fire
    • Capstone: Verify a Parameterized FIFO
  • Principal-Level Verification28
Loading lesson

‹SystemVerilog Interview Q&A Bank
  1. Topics
  2. SystemVerilog
  3. Senior & Interview Preparation
  4. SystemVerilog Interview Q&A Bank
  5. Q&A: Testbench Plumbing

Part 8 · Senior & Interview Prep · Intermediate

Q&A: Testbench Plumbing

Queues, array methods, fork discipline, mailboxes, semaphores and events for testbench plumbing.

On this page

    Plumbing moves transactions between testbench parts without races or leaks. Containers hold them, threads drive them, and channels synchronize them.

    Choose the container by access pattern

    Q: Dynamic array, associative array or queue, how do you choose

    Pattern

    Container

    Example

    Known size, dense indices

    Dynamic array

    A packet payload allocated once

    Sparse or non-integer keys

    Associative array

    A memory model keyed by address

    Ordered, grows and shrinks

    Queue

    Scoreboards, generators, monitors

    Reading a missing associative entry returns the type default without allocating. Guard reads with exists() so defaults never masquerade as data.

    The listing below is class declarations only, with no top module, so this course's checks do not run it. It shows the queue scoreboard idiom with its empty guard.

    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
    // not-run: fragment: class declarations only, no top module
    class fifo_txn;
      bit [7:0] data;
      function bit compare(fifo_txn other);
        return data == other.data;
      endfunction
    endclass
    
    class scoreboard;
      fifo_txn exp_q[$];
      function void write_in(fifo_txn t);
        exp_q.push_back(t);
      endfunction
      function void write_out(fifo_txn t);
        fifo_txn exp;
        if (exp_q.size() == 0) begin
          $display("DUT output with no expected transaction");
          return;
        end
        exp = exp_q.pop_front();
        if (!t.compare(exp)) $display("data ordering wrong");
      endfunction
    endclass

    The same FIFO idiom runs here on plain integers, with the empty guard included. The testbench pushes five values and proves ordered drain plus end-of-test emptiness.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    module tb_int_fifo;
      int exp_q[$];
      int got;
      bit empty_flag;
      int errors = 0;
      initial begin
        for (int i = 1; i <= 5; i++) exp_q.push_back(i * 10);
        for (int i = 1; i <= 5; i++) begin
          if (exp_q.size() == 0) empty_flag = 1;
          else begin empty_flag = 0; got = exp_q.pop_front(); end
          if (empty_flag !== 1'b0 || got !== i * 10) errors++;
        end
        if (exp_q.size() !== 0) errors++;
        if (exp_q.size() == 0) empty_flag = 1;
        if (empty_flag !== 1'b1) errors++;
        if (errors == 0) $display("PASS: int queue drained in order with the empty guard set");
        else $fatal(1, "FAIL: queue order wrong");
        $finish;
      end
    endmodule

    Q: What are the memory implications of each array type

    Fixed and dynamic arrays allocate every element, used or not. Associative arrays allocate per written element plus lookup overhead, which is the only sane way to model gigabytes of address space.

    Queues allocate to current size and grow as needed. An unbounded queue that is pushed but never popped is the classic testbench leak: memory climbs and the run slows over hours.

    The defense is an end-of-test queue-empty check plus periodic watermark prints on long runs. The leaking queue is then obvious.

    One-liners without loops

    Q: Find errors, sum a field, unique values, sort by field

    Array locator and ordering methods with with replace hand-written loops. Each is one line, and the return types are the trap: locator methods return a queue, possibly empty, never a scalar.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet
    // errs holds every matching transaction; idxs holds their positions
    // fifo_txn errs[$] = q.find with (item.is_err);
    // int idxs[$]     = q.find_index with (item.len > 8);
    // int total       = q.sum with (int'(item.len));
    // fifo_txn big[$] = q.max with (item.len);   // queue of size 0 or 1
    // fifo_txn ul[$]  = q.unique with (item.len);  // one txn per distinct len
    // q.sort with (item.addr);                   // ascending, in place

    Cast inside sum so a narrow field cannot wrap silently. Guard max, min and unique results with size() before indexing element zero. unique returns elements, not the with values: to get the distinct lengths, collect ul[i].len in a loop.

    Threads that outlive their welcome

    Q: The three fork variants, when is each used

    fork...join waits for all branches, for parallel drive of independent interfaces. fork...join_any resumes at the first finished branch, the timeout-watchdog pattern.

    fork...join_none resumes immediately and launches background services. With join_any and join_none the other branches keep running; resuming is not killing.

    join_none children start only when the parent yields. Code that forks and then immediately changes a shared variable hands the child the changed value.

    Q: What is wrong with forking on the loop variable

    Every branch captures the same loop variable, and join_none branches start after the loop completes. All four branches see the loop's exit value, 4, one past the last port. (Icarus shows this; Verilator 5.048 copies the loop variable per iteration and prints 0 1 2 3, so the bench below passes there even without the fix.)

    The fix copies the loop variable into an automatic local inside the fork. The initializer runs at fork time and each branch owns a private copy.

    Q: What is dangerous about disable fork

    disable fork kills every child of the current thread, not just the intended fork. A timeout disable after an earlier join_none monitor launch silently kills the monitors, and the test passes vacuously.

    Wrap the disable-able fork in an isolating fork...join so only the inner children are in scope. Named blocks with disable name are the other option.

    The listing below is a single task that calls an undefined wait_response, so this course's checks do not run it. It shows the isolation wrapper around a timeout.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // not-run: fragment: one task; wait_response is not defined here
    task automatic run_with_timeout();
      fork begin : isolated
        fork
          wait_response();
          #10us;
        join_any
        disable fork;      // kills only the wait and timeout pair
      end join
    endtask

    The same fork rules run here on plain threads. The testbench proves per-branch loop capture, then proves join_any plus disable fork ends the slow branch.

    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
    module tb_fork_catch;
      timeunit 1ns; timeprecision 1ps;
      int errors = 0;
      int hit[4];
      bit slow_done = 0;
      initial begin
        for (int i = 0; i < 4; i++) hit[i] = -1;
        for (int i = 0; i < 4; i++) begin : gen
          automatic int idx = i;
          fork begin : worker
            #10;
            hit[idx] = idx;
          end join_none
        end
        #11;
        for (int i = 0; i < 4; i++) if (hit[i] !== i) errors++;
        fork
          begin #5; end
          begin #100; slow_done = 1; end
        join_any
        disable fork;
        #110;
        if (slow_done !== 1'b0) errors++;
        if (errors == 0) $display("PASS: loop capture gave 0 1 2 3 and disable ended the slow branch");
        else $fatal(1, "FAIL: fork behavior wrong");
        $finish;
      end
    endmodule

    Mailboxes, semaphores and events

    Q: Mailbox versus queue plus event, why the mailbox

    A mailbox is a thread-safe FIFO with blocking built in: get suspends while empty and a bounded put suspends while full. A bare queue has no synchronization.

    The hand-rolled queue-plus-event version races when the trigger lands before the wait is posted. Bound the mailbox to a few entries so generation stays coupled to consumption instead of racing thousands ahead.

    Q: When do you use a semaphore

    A semaphore manages tokens for a shared resource: get blocks until keys exist, put returns them. One key is a mutex; N keys model limited outstanding transactions.

    Keys leak when a holder is killed before put, which deadlocks everyone behind it. put also mints unchecked keys, so a double-put silently inflates the pool.

    The listing below is declarations and a task with no module, so this course's checks do not run it. Verilator 5.048 supports mailboxes and semaphores; Icarus 13 has no semaphores.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // not-run: fragment: declarations and a task, no module
    mailbox #(int) gen2drv = new(8);   // bounded: backpressure for free
    semaphore bus_lock = new(1);       // one key: mutex
    
    task automatic drive_atomic(int t);
      bus_lock.get(1);
      // drive(t);                     // critical section: sole bus owner
      bus_lock.put(1);                 // always return the key
    endtask

    Q: What does event triggered solve that at-event does not

    @(ev) only sees a trigger that arrives while already waiting. ev.triggered stays true for the rest of the time step, so wait(done.triggered) passes whether the process arrived before or after the trigger.

    The flag clears at the next step. Cross-timestep handoffs still belong in a mailbox, not an event.

    Common pitfall

    disable fork after join_any kills background monitors launched earlier by the same thread. Isolate timeout forks, or passing tests may be checking nothing.

    Interview

    Likely whiteboard: fork four drivers in a loop and predict the wrong value. The answer names both halves: deferred branch start plus the shared variable, fixed by an automatic copy.

    Check yourself Why does a bounded mailbox beat an unbounded one between generator and driver?

    The bound adds backpressure: a fast generator cannot race thousands of transactions ahead. Memory stays flat and stimulus keeps responding to DUT state.

    Check yourself A branch holds a semaphore key when disable fork kills it. What does the rest of the test see?

    A hang. Everyone behind get blocks forever on a key that never returns, which looks like a DUT deadlock.

    Related topics

    • OOP: interprocess communication

    • Testbench: anatomy

    • Testbench: monitors and scoreboards

    Previous

    320 / 363

    Next: Q&A: Classes and Interfaces

    Up next

    Up nextQ&A: Classes and Interfaces

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Q&A: Language Core
    • Q&A: OOP
    • Q&A: Constraint and Assertion Fire-Drill