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
    • Monitors, Scoreboards & Reference Models
      • Monitor Design
      • Scoreboard Architecture
      • Compare and End-of-Test
      • Reference Models and Coverage
      • PacketFIFO Step: A Monitor and Scoreboard
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration26
  • Senior & Interview Preparation23
  • Principal-Level Verification34
Loading lesson

←Monitors, Scoreboards & Reference Models
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. Monitors, Scoreboards & Reference Models
  5. PacketFIFO Step: A Monitor and Scoreboard
On this page

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

    PacketFIFO Step: A Monitor and Scoreboard

    Build the checking side of the PacketFIFO project: two passive monitors, an in-order scoreboard and end-of-test checks, in one runnable bench.

    Check the FIFO, not the stimulus

    In Part 0 the PacketFIFO bench checked one hand-written packet. Here the checking becomes reusable: one monitor watches each port, rebuilds packets from the accepted bytes, and a scoreboard compares what went in with what came out.

    PacketFIFO step: optional. It reuses only ideas from this section: the passive monitor, the accepted-beat rule, the in-order FIFO compare and the drain window. Everything it needs is in this lesson's listing.

    Piece

    Job in the PacketFIFO bench

    Taught in

    pf_pkt

    Header fields, payload queue, check byte, equals() and str()

    Transaction Design for Stimulus

    pf_monitor (two copies)

    Counts a byte only when valid and ready are both 1 at the edge; publishes a packet at last

    Monitor Design

    pf_scoreboard

    Pairs the n-th packet in with the n-th packet out

    Scoreboard Architecture

    End-of-test block

    Waits for every packet, then checks counts, leftovers and an empty FIFO

    Compare and End-of-Test

    The bench

    The FIFO is the Part 0 design, unchanged. A small driver sends six packets of different kind, destination and length while the consumer drops out_ready one cycle in three, so the 4-entry FIFO fills and back-pressures the driver.

    • Driver: changes pins on the falling edge and holds each byte until in_ready is 1 at a rising edge.

    • Monitors: sample on the rising edge. The driver moves on the falling edge, so every sample sees settled values.

    • Scoreboard: the order is the check. The FIFO must keep packet order, so a plain queue compare is enough.

    • End of test: wait for the output monitor to see all six packets, drain four more cycles, then check.

    packet_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
    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
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    // Part 7 PacketFIFO step: monitor + scoreboard for the Part 0 packet_fifo.
    module packet_fifo #(
      parameter WIDTH = 9,
      parameter DEPTH = 4,
      parameter AW    = 2
    ) (
      input  wire             clk,
      input  wire             rst_n,
      input  wire             in_valid,
      output wire             in_ready,
      input  wire [WIDTH-1:0] in_data,
      output wire             out_valid,
      input  wire             out_ready,
      output wire [WIDTH-1:0] out_data
    );
      reg [WIDTH-1:0] mem [0:DEPTH-1];
      reg [AW-1:0]    wptr, rptr;
      reg [AW:0]      count;
      wire push = in_valid && in_ready;
      wire pop  = out_valid && out_ready;
      assign in_ready  = (count != DEPTH);
      assign out_valid = (count != 0);
      assign out_data  = mem[rptr];
      always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          wptr <= 0; rptr <= 0; count <= 0;
        end else begin
          if (push) begin mem[wptr] <= in_data; wptr <= wptr + 1'b1; end
          if (pop) rptr <= rptr + 1'b1;
          case ({push, pop})
            2'b10:   count <= count + 1'b1;
            2'b01:   count <= count - 1'b1;
            default: count <= count;
          endcase
        end
    endmodule
    
    interface pf_if (input logic clk);
      logic       in_valid, in_ready, out_valid, out_ready;
      logic [8:0] in_data, out_data;
    endinterface
    
    // One packet as the testbench sees it: header fields, payload, check byte.
    class pf_pkt;
      bit [1:0] kind;
      bit [1:0] dest;
      bit [3:0] len;
      bit [7:0] payload [$];
      bit [7:0] chk;
    
      function bit [7:0] header();
        return {kind, dest, len};
      endfunction
    
      function void seal();
        chk = header();
        foreach (payload[i]) chk ^= payload[i];
      endfunction
    
      function bit equals(pf_pkt o);
        return header() == o.header() && payload == o.payload && chk == o.chk;
      endfunction
    
      function string str();
        string s = $sformatf("kind=%0d dest=%0d len=%0d chk=%02h data=", kind, dest, len, chk);
        foreach (payload[i]) s = {s, $sformatf("%02h ", payload[i])};
        return s;
      endfunction
    endclass
    
    // Minimal driver: holds each byte until the FIFO accepts it.
    class pf_driver;
      virtual pf_if vif;
      mailbox #(pf_pkt) in_mbx;
    
      function new(virtual pf_if vif, mailbox #(pf_pkt) in_mbx);
        this.vif = vif;
        this.in_mbx = in_mbx;
      endfunction
    
      task send_byte(bit [7:0] b, bit last);
        vif.in_valid = 1;
        vif.in_data  = {last, b};
        do @(posedge vif.clk); while (!vif.in_ready);   // accepted on this edge
        @(negedge vif.clk);
      endtask
    
      task run();
        pf_pkt p;
        forever begin
          in_mbx.get(p);
          send_byte(p.header(), 1'b0);
          foreach (p.payload[i]) send_byte(p.payload[i], 1'b0);
          send_byte(p.chk, 1'b1);
          vif.in_valid = 0;
          repeat (2) @(negedge vif.clk);
        end
      endtask
    endclass
    
    // Passive monitor: counts only accepted bytes and publishes a packet at the last byte.
    class pf_monitor;
      virtual pf_if vif;
      bit               on_input;   // 1: producer port, 0: consumer port
      mailbox #(pf_pkt) out_mbx;
      int unsigned      pkts, bad;
    
      function new(virtual pf_if vif, bit on_input, mailbox #(pf_pkt) out_mbx);
        this.vif = vif;
        this.on_input = on_input;
        this.out_mbx = out_mbx;
      endfunction
    
      task run();
        bit [7:0] bytes [$];
        bit       accepted, last;
        bit [7:0] b;
        forever begin
          @(posedge vif.clk);   // the driver moves on negedge, so posedge reads settled values
          accepted = on_input ? (vif.in_valid === 1 && vif.in_ready === 1)
                              : (vif.out_valid === 1 && vif.out_ready === 1);
          if (accepted) begin
            b    = on_input ? vif.in_data[7:0] : vif.out_data[7:0];
            last = on_input ? vif.in_data[8]   : vif.out_data[8];
            bytes.push_back(b);
            if (last) begin
              build(bytes);
              bytes.delete();
            end
          end
        end
      endtask
    
      function void build(bit [7:0] bytes [$]);
        pf_pkt p = new();
        if (bytes.size() < 2) begin bad++; return; end
        p.kind = bytes[0][7:6];
        p.dest = bytes[0][5:4];
        p.len  = bytes[0][3:0];
        for (int i = 1; i < bytes.size() - 1; i++) p.payload.push_back(bytes[i]);
        p.chk = bytes[bytes.size() - 1];
        pkts++;
        void'(out_mbx.try_put(p));   // unbounded mailbox: never blocks the monitor
      endfunction
    endclass
    
    // In-order scoreboard: the packet seen going in must be the packet seen coming out.
    class pf_scoreboard;
      mailbox #(pf_pkt) exp_mbx, act_mbx;
      int unsigned compares, mismatches;
    
      function new(mailbox #(pf_pkt) exp_mbx, mailbox #(pf_pkt) act_mbx);
        this.exp_mbx = exp_mbx;
        this.act_mbx = act_mbx;
      endfunction
    
      task run();
        pf_pkt e, a;
        forever begin
          exp_mbx.get(e);
          act_mbx.get(a);
          compares++;
          if (!e.equals(a)) begin
            mismatches++;
            $display("MISMATCH #%0d exp: %s", compares, e.str());
            $display("               act: %s", a.str());
          end
        end
      endtask
    endclass
    
    Terminal
    PASS: 6 packets, 6 compares, 0 differences, FIFO drained
    Predict first The consumer is stalled for a third of the cycles. Why does the input monitor still count exactly one packet per last byte?

    A byte counts only on an edge where in_valid and in_ready are both 1. While the FIFO is full the driver holds the same byte with in_ready low, and the monitor ignores those edges, so no byte is counted twice.

    Break it on purpose

    A scoreboard you have never seen fail is a guess. Change one line of the FIFO so that its output flips bit 0 whenever the read pointer is 1.

    • Replace assign out_data = mem[rptr]; with assign out_data = mem[rptr] ^ ((rptr == 2'd1) ? 9'h001 : 9'h000);

    • Run the same bench. The first two lines of its output are shown below.

    Terminal
    MISMATCH #1 exp: kind=0 dest=2 len=2 chk=2b data=a5 ac
                   act: kind=0 dest=2 len=2 chk=2b data=a4 ac

    The dump shows the first difference at a glance: payload byte 0 arrived as a4 instead of a5. The final line is a FAIL, so a regression would catch the run.

    The end-of-test block guards against a hollow PASS. If a miswired monitor saw nothing, compares would be 0 and the run would fail on the SENT check, not pass quietly.

    Check yourself Why is the end-of-test wait on omon.pkts and not on the driver finishing?

    The driver finishes when the last byte is accepted, but packets can still be inside the FIFO. Only the output monitor knows the last packet has left, so the drain waits on it, then adds a short window for stragglers.

    Check yourself The scoreboard compares packets in order. When would that stop being enough for a FIFO?

    Never for this FIFO, because order is part of its contract. A design that may reorder, such as an arbiter with several consumers, needs a by-id lookup instead.

    How this was checked. Run with Verilator 5.048 (--binary --timing --assert); the PASS line above is its output, and the broken-FIFO output was produced the same way. The run proves the checks work for these six packets and one fault. It does not prove the FIFO correct for all packets or for random stalls.

    Related topics

    • Compare and End-of-Test

    • The PacketFIFO Project (Part 0)

    • Reference Models and Coverage

    Previous

    185 / 275

    Next: Self-Checking Tests & Regression

    Up next

    Up nextSelf-Checking Tests & Regression

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Monitor Design
    • Scoreboard Architecture
    • Compare and End-of-Test