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
    • Testbench Anatomy & Layering
    • Stimulus: Generators, Transactions & Drivers
      • Transaction Design for Stimulus
      • Layered Constraint Architecture
      • Shaping Stimulus with Knobs and Policies
      • Pipelined & Backpressured Driving
      • Protocol Legality and Error Injection
      • Randomizing Scenarios, Not Just Items
      • Generator Patterns
      • Driver Design
      • Mixing Directed and Random Stimulus
    • Monitors, Scoreboards & Reference Models
    • Testbench Synchronization & Phasing
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
  • Principal-Level Verification28
Loading lesson

‹Stimulus: Generators, Transactions & Drivers
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. Stimulus: Generators, Transactions & Drivers
  5. Driver Design

Part 6 · Testbench Architecture · Intermediate

Driver Design

Pin-accurate drivers: handshakes, reset, pipelining, and shared-bus locking.

On this page

    The driver contract

    The driver is the only stimulus-side class that touches pins. It consumes one transaction at a time, expands it into legal cycles, holds a driven idle between transfers, and reacts to reset at any moment.

    Watch valid against ready across the six slots. Payload holds through the stall and the gap stays idle.

    • Slot 0: valid 1 and ready 0, so addr a0 waits.

    • Slot 1: the stall persists and addr still reads a0.

    • Slot 2: valid and ready are both 1, so the transfer accepts.

    • Slot 3: valid is 0 and addr is 0, the gap idle cycle.

    • Slot 4: valid 1 with addr b1 accepts at once.

    • Slot 5: valid is 0 and the bus rests idle.

    Reset-aware driving

    The next listing is a fragment and is not run in this course's checks: bus_if and bus_txn are defined elsewhere, and it has no top 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
    // not-run: fragment: bus_if and bus_txn are defined elsewhere, no top module
    class driver;
      mailbox #(bus_txn) in;
      virtual bus_if vif;
      int unsigned driven_n;
      function new(mailbox #(bus_txn) in);
        this.in = in;
      endfunction
      task run();
        drive_idle();
        forever begin
          bus_txn t;
          in.get(t);
          repeat (t.gap) @(vif.drv_cb);
          fork
            begin               // isolating fork: disable fork stops only these two
              fork
                drive_one(t);
                watch_reset();
              join_any
              disable fork;
            end
          join
          if (vif.rst_n !== 1) handle_reset();
        end
      endtask
      task drive_one(bus_txn t);
        @(vif.drv_cb);
        vif.drv_cb.valid <= 1;
        vif.drv_cb.write <= (t.kind == WRITE);
        vif.drv_cb.addr <= t.addr;
        vif.drv_cb.wdata <= t.wdata;
        t.t_driven = $time;
        do @(vif.drv_cb); while (vif.drv_cb.ready !== 1);
        drive_idle();
        driven_n++;
      endtask
      task drive_idle();
        vif.drv_cb.valid <= 0;
        vif.drv_cb.write <= 0;
        vif.drv_cb.addr <= '0;
        vif.drv_cb.wdata <= '0;
      endtask
      task watch_reset();
        @(negedge vif.rst_n);
      endtask
      task handle_reset();
        bus_txn dropped;
        drive_idle();
        while (in.try_get(dropped))
          $display("DRV: flushed txn%0d on reset", dropped.id);
        @(posedge vif.rst_n);
        repeat (2) @(vif.drv_cb);
      endtask
    endclass

    All pin writes use the clocking block with nonblocking assigns, so output skew removes drive races. The ready wait compares with !== 1, so an X stalls instead of passing as truthy. The drive-or-reset race sits inside its own fork ... join, so disable fork kills only this driver's two threads. A named disable would kill that block in every driver instance.

    • Payload stays stable while valid is high and ready is low, per handshake rules.

    • Reset handling idles pins first, flushes pre-reset stimulus, then re-synchronizes.

    • The driver honors gap spacing on top of one idle cycle it always inserts after a handshake, so gap 0 still gives one idle cycle. True back-to-back needs the next item driven at the handshake edge when gap is 0.

    Pipelined driving and bus locking

    The next listing is a fragment and is not run in this course's checks: axi_if and bus_txn are defined elsewhere, and it has no top 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
    // not-run: fragment: axi_if and bus_txn are defined elsewhere, no top module
    class pipelined_driver;
      mailbox #(bus_txn) in;
      virtual axi_if vif;
      bus_txn pending [bit [3:0]];
      semaphore id_pool;      // bounds how many are outstanding
      bit [3:0] free_ids[$];  // which IDs are not in flight
      function new(mailbox #(bus_txn) in, int max_out = 8);
        if (max_out > 16) $fatal(1, "max_out %0d exceeds the 16 IDs", max_out);
        this.in = in;
        id_pool = new(max_out);
        for (int i = 0; i < 16; i++) free_ids.push_back(i);
      endfunction
      task run();
        vif.drv_cb.rready <= 1;   // always accept responses
        fork
          issue_loop();
          response_loop();
        join_none
      endtask
      task issue_loop();
        forever begin
          bus_txn t;
          in.get(t);
          id_pool.get(1);
          t.id_tag = free_ids.pop_front();   // never an ID still in flight
          pending[t.id_tag] = t;
          @(vif.drv_cb);
          vif.drv_cb.arvalid <= 1;
          vif.drv_cb.arid <= t.id_tag;
          vif.drv_cb.araddr <= t.addr;
          do @(vif.drv_cb); while (vif.drv_cb.arready !== 1);
          vif.drv_cb.arvalid <= 0;
        end
      endtask
      task response_loop();
        forever begin
          bit [3:0] rid;
          @(vif.drv_cb);
          if (vif.drv_cb.rvalid === 1) begin   // rready is held at 1 by run()
            rid = vif.drv_cb.rid;
            if (!pending.exists(rid))
              $display("DRV: response for unknown id %0d", rid);
            else begin
              pending[rid].rdata = vif.drv_cb.rdata;
              pending.delete(rid);
              free_ids.push_back(rid);   // the ID may be reused now
              id_pool.put(1);
            end
          end
        end
      endtask
    endclass

    Issue and response run as separate processes joined by an id-keyed pending array, so out-of-order responses match by lookup. The semaphore throttles outstanding count with one get per issue and one put per response. A free-ID queue picks the tag: a plain counter would wrap onto an ID that is still pending when one response is slow, and overwrite it. A one-key semaphore around the pin sequence serializes two masters onto a shared bus.

    Common mistakes

    Common pitfall

    Matching responses by issue order on a reordering protocol scrambles the scoreboard. Tag every request and match responses by id through the pending array.

    Interview

    Likely question: what happens when the DUT stalls?

    Hold valid and payload stable until ready. Never retract or rewrite a stalled request, and let the throttle propagate the stall upstream.

    Check yourself The response path forgets id_pool.put(1). What breaks?

    The bench deadlocks at exactly max_outstanding transactions. Every key stays checked out, so issue blocks forever.

    Check yourself Two drivers share one bus. What keeps pin sequences atomic?

    A one-key semaphore around the full pin sequence. The holder owns the bus for one complete transaction.

    Testbench

    A sink that stalls its first cycles and checks the handshake rules at the pins, plus a testbench that drives two transfers through it like the driver above: hold through the stall, one idle gap, then the next transfer.

    stall_sink.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
    module stall_sink (
      input  wire       clk,
      input  wire       valid,
      input  wire [7:0] addr,
      output reg        ready,
      output reg  [7:0] got0,
      output reg  [7:0] got1,
      output reg  [1:0] n_acc,
      output reg        moved
    );
      timeunit 1ns; timeprecision 1ps;
      reg [2:0] c = 0;
      reg [7:0] last = 0;
      reg       stalled = 0;
      initial begin ready = 0; n_acc = 0; moved = 0; got0 = 0; got1 = 0; end
      always @(posedge clk) begin
        c <= c + 3'd1;
        ready <= (c >= 3'd2);                       // stalls the first cycles
        if (valid && ready) begin                   // accept
          if (n_acc == 0) got0 <= addr; else got1 <= addr;
          n_acc <= n_acc + 2'd1;
        end
        if (stalled && (!valid || addr !== last)) moved <= 1;  // retracted or changed
        stalled <= valid && !ready;
        last <= addr;
      end
    endmodule
    

    Related topics

    • Generator Patterns

    • Mixing Directed and Random Stimulus

    Previous

    247 / 363

    Next: Mixing Directed and Random Stimulus

    Up next

    Up nextMixing Directed and Random Stimulus

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Transaction Design for Stimulus
    • Layered Constraint Architecture
    • Shaping Stimulus with Knobs and Policies
    clkvalidreadyaddra00b10
    Valid/ready stall with a gap idle cycle