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
  • Principal-Level Verification28
    • Reusable Testbench Architecture
      • Policy Classes
      • Abstract BFM
      • Parameterized Interfaces
      • Interface Classes
      • Layered Stimulus
    • Checking Out-of-Order Traffic
    • Assertion Depth
    • Coverage Modelling for Real Features
    • Capstone: Out-of-Order Memory Controller
    • Principal Judgment
Loading lesson

‹Reusable Testbench Architecture
  1. Topics
  2. SystemVerilog
  3. Principal-Level Verification
  4. Reusable Testbench Architecture
  5. Layered Stimulus

Part 9 · Principal-Level Verification · Advanced

Layered Stimulus

Scenario, transaction and beat layers joined by bounded mailboxes, with back-pressure flowing upward and a monitor that rebuilds bursts for the scoreboard.

On this page

    Why layers

    Tests think in operations: "write a 6-beat burst to this region". Pins move beats: one data word per valid/ready handshake. If the test produces beats directly, every test reimplements burst splitting, and a protocol change touches every test.

    Layered stimulus puts one translation in each layer. A scenario layer produces high-level items, a translation layer turns each item into lower-level items, and a driver moves the lowest layer onto the pins. Monitors do the reverse, so the scoreboard compares at the level the test thinks in.

    Diagram
    STIMULUS (down)                          CHECKING (up)
    
      scenario layer   burst{id, payload[]}     scoreboard  compares bursts
            │ burst_mb (bounded 1)                   ▲
      translator       burst -> beats, last          │ rebuilt bursts
            │ beat_mb  (bounded 2)                   │
      beat driver      valid/ready handshake     monitor   beats -> burst on last
            │                                        ▲
            └──────────────► DUT (2-entry FIFO) ─────┘  out_ready from sink
    
      Back-pressure flows UP: DUT full -> driver holds -> beat_mb fills
      -> translator blocks in put() -> burst_mb fills -> scenario blocks.

    Back-pressure between layers

    A bounded mailbox is what joins two layers. put() blocks when it is full, so a stalled DUT slows the translator, which slows the scenario layer. Nothing upstream runs ahead of the pins, and nothing needs a separate flow-control protocol between classes.

    Unbounded mailboxes also work functionally, but the scenario layer then finishes generating long before the DUT accepts the data. Any feedback (for example a scenario that reacts to a response or to a credit count) then sees stale state. Small bounds keep the layers in step.

    Layer

    Owns

    Must not know

    Scenario

    What the test wants: ids, lengths, data, ordering

    Beats, handshakes, signal names

    Translator

    How an item maps to lower items: split, last, header

    Timing on the pins

    Driver

    Handshake timing on the pins

    What a burst is

    Monitor

    Rebuilding items from pin activity

    What the test intended

    Scoreboard

    Comparing rebuilt items with expected ones

    How they were transported

    Runnable example: bursts through a FIFO

    The DUT is a two-entry beat FIFO with valid/ready on both sides. The scenario layer randomizes 40 bursts of 1 to 8 beats and records each one as expected. The translator splits bursts into beats and marks the last one. The driver handshakes beats in, a sink applies back-pressure on the output from a fixed LFSR, and the monitor rebuilds bursts on last for the scoreboard.

    beat_fifo.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
    // DUT: two-entry beat FIFO with valid/ready on both sides.
    module beat_fifo (
      input  logic        clk, rst_n,
      input  logic        in_valid,
      output logic        in_ready,
      input  logic [19:0] in_beat,     // {id[3:0], data[15:0]}
      input  logic        in_last,
      output logic        out_valid,
      input  logic        out_ready,
      output logic [19:0] out_beat,
      output logic        out_last
    );
      logic [20:0] mem [2];
      logic        wp, rp;
      logic [1:0]  count;
      wire push = in_valid  && in_ready;
      wire pop  = out_valid && out_ready;
      assign in_ready  = (count != 2);
      assign out_valid = (count != 0);
      assign {out_last, out_beat} = mem[rp];
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          wp <= 0; rp <= 0; count <= 0;
        end else begin
          if (push) begin mem[wp] <= {in_last, in_beat}; wp <= ~wp; end
          if (pop)  rp <= ~rp;
          count <= count + 2'(push) - 2'(pop);
        end
    endmodule
    
    // Layer 1: scenario. A burst is what a test thinks in.
    class burst;
      rand bit [3:0]    id;
      rand int unsigned len;
      bit [15:0]        payload [$];
      constraint c_len { len inside {[1:8]}; }
      function void post_randomize();
        payload.delete();
        repeat (len) payload.push_back(16'($urandom));
      endfunction
      function bit same(burst o);
        if (id != o.id || payload.size() != o.payload.size()) return 0;
        foreach (payload[k]) if (payload[k] != o.payload[k]) return 0;
        return 1;
      endfunction
    endclass
    
    // Layer 2: beat. What the pins carry.
    class beat;
      bit [3:0]  id;
      bit [15:0] data;
      bit        last;
    endclass
    

    Payload words are generated in post_randomize() from the burst length. Constraining the size of a dynamic array was avoided on purpose: in our runs Verilator 5.048 returned a size of 0 for payload.size() inside {[1:8]} whenever randomize() also had an inline with constraint (a plain randomize() sized it correctly), which would produce empty bursts and a hang. The bench also ends with a timeout branch so that a hang becomes a FAIL with a count instead of a stuck job.

    The bench checks more than data. It fails if any expected burst never arrived, if a beat's id differs from its burst's id, and if back-pressure never reached the driver. Without the last check, a sink that was always ready would pass while never testing the stall path.

    Mutant runs. Making the FIFO always ready (assign in_ready = 1'b1) overwrites entries when full and fails on the first burst (4 of 8 beats arrived) and also reports that back-pressure never reached the driver. Dropping last on the final beat of every burst with id 7 makes the monitor merge two bursts, and the run fails with "4 bursts never arrived" plus a compare error on the merged burst.

    Common pitfall

    A driver that keeps valid high after its last item. If the driver only calls a blocking get() for the next beat, valid stays asserted with stale data while it waits, and the DUT accepts a duplicate beat. The driver here uses try_get() each cycle and drives valid low when it has nothing to send.

    Interview

    Likely question: “How do you generate stimulus for a burst protocol so tests stay protocol-independent?” Answer: layer it. Tests randomize bursts; a translator splits them into beats; a driver owns the handshake. Bounded mailboxes carry back-pressure up, and the monitor rebuilds bursts so the scoreboard compares at burst level. In UVM this is a layered sequencer or a translation sequence running on the lower agent's sequencer.

    Check yourself The driver reads in_ready on the falling edge but the handshake happens on the rising edge. Why is that safe here?

    in_ready depends only on the FIFO count, which changes only at the rising edge. Its value at the falling edge is therefore the value the next rising edge sees. If ready depended combinationally on valid or on inputs that change mid-cycle, the driver would have to sample it at the edge instead, for example through a clocking block.

    Check yourself The scenario layer finishes generating after 3 microseconds, but the last burst reaches the scoreboard at 30 microseconds. What does that tell you about the mailbox bounds?

    At least one mailbox between the scenario and the driver is unbounded or very deep, so the scenario ran far ahead of the pins. That is legal, but any scenario logic that reacts to DUT state is now working with stale information. Bound the mailboxes to keep layers in step.

    Related topics

    • Previous: Interface Classes

    • Randomizing Scenarios, Not Just Items (Part 7)

    • Mailboxes: Typed Pipes with Backpressure

    • Next topic: Checking Out-of-Order Traffic

    Previous

    236 / 259

    Next: Checking Out-of-Order Traffic

    Up next

    Up nextChecking Out-of-Order Traffic

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Policy Classes
    • Abstract BFM
    • Parameterized Interfaces