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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here5
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive39
  • SystemVerilog Assertions — Interview-Critical Deep Dive41
  • Functional Coverage in SystemVerilog25
  • Testbench Architecture in SystemVerilog24
    • Testbench Anatomy & Layering
    • Stimulus: Generators, Transactions & Drivers
      • Transaction Design for Stimulus
      • Layered Constraint Architecture
      • Shaping Stimulus with Knobs and Policies
      • Protocol Legality and Error Injection
      • Randomizing Scenarios, Not Just Items
      • Generator Patterns
      • Driver Design
      • Mixing Directed and Random Stimulus
    • Monitors, Scoreboards & Reference Models
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration26
  • Senior & Interview Preparation23
  • Principal-Level Verification34
Loading lesson

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

    Part 6 of 9, lesson 11 of 24Part 6 of 9/Testbench Architecture/Lesson 11 of 24Intermediate

    Driver Design

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

    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.

    Generator, mailbox and driver timing

    The generator and driver run as separate threads. The mailbox is the only thing between them, so its depth decides how far the generator may run ahead. The figure uses a depth-2 mailbox and a DUT that holds ready low.

    Generator, mailbox and driver over six time slots. The generator fills the mailbox while the driver is held up by ready, then waits until the driver takes the next item.

    With an unbounded mailbox the generator would never wait, which is fine for a short test and wasteful for a long one. A bounded mailbox gives the generator natural back-pressure without any extra code.

    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

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

    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

    179 / 275

    Next: Mixing Directed and Random Stimulus

    Up next

    Up nextMixing Directed and Random Stimulus

    Sign in to save progress

    Practice this lessonPrevious 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