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
    • Testbench Anatomy & Layering
    • Stimulus: Generators, Transactions & Drivers
    • Monitors, Scoreboards & Reference Models
      • Monitor Design
      • Scoreboard Architecture
      • Compare and End-of-Test
      • Reference Models and Coverage
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Monitors, Scoreboards & Reference Models
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. Monitors, Scoreboards & Reference Models
  5. Monitor Design

Part 6 · Testbench Architecture · Intermediate

Monitor Design

Sample pins passively, rebuild multi-beat transactions and broadcast one copy per consumer.

On this page

    The passive contract

    A monitor rebuilds transactions from pins and never drives a signal. A monitor that writes the bus observes its own stimulus, and the bench can no longer catch DUT bugs on those pins.

    The next listing is an interface only, so this course's checks do not run it. It shows the structural fix: an input-only clocking block, so a drive attempt is a compile error.

    bus_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // not-run: interface only, no module to run
    interface bus_if (input logic clk);
      logic        valid;
      logic        ready;
      logic        last;
      logic [31:0] data;
    
      // Driver clocking block: drives valid/data, samples ready
      clocking drv_cb @(posedge clk);
        output valid, last, data;
        input  ready;
      endclocking
    
      // Monitor clocking block: EVERYTHING is an input
      clocking mon_cb @(posedge clk);
        input valid, ready, last, data;
      endclocking
    
      modport MON (clocking mon_cb);   // passive view only
    endinterface

    Sampling through mon_cb also settles races. The clocking block samples just before the edge, so the monitor sees the values the DUT flops saw.

    Rebuilding multi-beat transactions

    Pins carry beats and consumers want transactions. The monitor assembles beats into a transaction and publishes it only at the beat tagged last.

    Only accepted beats count: a beat belongs to the transaction when valid && ready at the edge. Beats offered during back-pressure are presented again, never double-counted.

    Watch accepted: it rises only on edges where both valid and ready are high.

    • Slot 0: idle, valid is 0, so accepted is 0.

    • Slot 1: valid and ready are 1 with data a0, so accepted is 1.

    • Slot 2: ready is 0, so data b0 is offered but not accepted and accepted stays 1.

    • Slot 3: ready is 1 again, so b0 is accepted and accepted is 2.

    • Slot 4: data c0 arrives with last 1, so accepted is 3 and done pulses.

    • Slot 5: valid is 0, so accepted rests at 3 and done is 0.

    Broadcasting to consumers

    The next listing is a fragment that uses bus_if from the listing above and has no top module, so this course's checks do not run it. Each consumer gets its own transaction copy over its own mailbox.

    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
    56
    57
    58
    59
    // not-run: fragment: uses bus_if from the listing above, no top module
    class bus_txn;
      int unsigned   id;
      logic [31:0]   data_q[$];   // collected beats
      time           start_t;
      time           end_t;
    
      function string convert2string();
        return $sformatf("txn id=%0d beats=%0d first=0x%08h last=0x%08h", id,
                         data_q.size(),
                         data_q.size() != 0 ? data_q[0] : 32'h00000000,
                         data_q.size() != 0 ? data_q[$] : 32'h00000000);
      endfunction
    endclass
    
    class bus_monitor;
      virtual bus_if.MON vif;
      mailbox #(bus_txn) mbx_scb;   // to scoreboard
      mailbox #(bus_txn) mbx_cov;   // to coverage collector
      int unsigned       txn_count;
      bit                stop;      // set by env at drain so run() can exit
    
      function new(virtual bus_if.MON vif,
                   mailbox #(bus_txn) mbx_scb,
                   mailbox #(bus_txn) mbx_cov);
        this.vif     = vif;
        this.mbx_scb = mbx_scb;
        this.mbx_cov = mbx_cov;
      endfunction
    
      task run();
        while (!stop) begin
          bus_txn tr = new();
          collect_one(tr);
          if (stop) break;
          txn_count++;
          broadcast(tr);
        end
      endtask
    
      // IDLE: wait for first accepted beat. COLLECT: gather through last.
      task collect_one(bus_txn tr);
        // === 1: an X on valid or ready must not count as an accepted beat
        do @(vif.mon_cb); while (!(vif.mon_cb.valid === 1 && vif.mon_cb.ready === 1));
        tr.start_t = $time;
        forever begin
          tr.data_q.push_back(vif.mon_cb.data);
          if (vif.mon_cb.last) break;
          do @(vif.mon_cb); while (!(vif.mon_cb.valid === 1 && vif.mon_cb.ready === 1));
        end
        tr.end_t = $time;
      endtask
    
      function void broadcast(bus_txn tr);
        bus_txn cp = new tr;   // each consumer gets its own copy
        void'(mbx_scb.try_put(tr));
        void'(mbx_cov.try_put(cp));
      endfunction
    endclass

    Choice

    Why

    try_put into unbounded mailboxes

    A monitor must never stall on a slow consumer and miss pin activity.

    One mailbox per consumer

    Consumers read at different speeds; a shared mailbox couples them.

    txn_count in the monitor

    It is the ground truth for the end-of-test sanity check.

    stop flag set by the env

    The loop exits at its next iteration. A thread blocked in get() or on a clock edge never re-reads it, so real shutdown is $finish or disable fork on the component threads.

    Common mistakes

    Common pitfall

    Counting offered beats (valid alone) instead of accepted beats (valid && ready) duplicates every beat held up by back-pressure. Pushing one shared handle to two mailboxes lets one consumer corrupt the other's view.

    Interview

    Likely questions: “Why must a monitor be passive?” and “Why sample via a clocking block?”

    • Passive because checking must observe the DUT, not the bench's own stimulus.

    • Clocking block because pre-edge sampling removes same-timestep races.

    • Be ready to sketch the burst reconstruction FSM on a whiteboard.

    Check yourself The monitor counts 8 beats but the driver offered 10. Is the monitor broken?

    Not necessarily. Two beats may have been offered while ready was low. Compare against accepted beats (valid && ready), not offered ones.

    Check yourself One consumer mutates a transaction to annotate it. Why does the scoreboard now see the annotation?

    Both consumers received the same handle. Broadcast a separate copy per mailbox so annotation stays private.

    Testbench

    A small accepted-beat monitor plus a testbench with one back-pressured beat.

    beat_monitor.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    module beat_monitor (
      input  logic       clk, valid, ready, last,
      input  logic [7:0] data,
      output logic [7:0] accepted,     // accepted-beat total
      output logic [7:0] txn_count,    // completed transactions
      output logic [7:0] open_beats    // beats in the still-open transaction
    );
      timeunit 1ns; timeprecision 1ps;
      initial begin
        accepted = 8'd0; txn_count = 8'd0; open_beats = 8'd0;
      end
      always @(posedge clk) begin
        if (valid && ready) begin
          accepted <= accepted + 8'd1;
          if (last) begin txn_count <= txn_count + 8'd1; open_beats <= 8'd0; end
          else          open_beats <= open_beats + 8'd1;
        end
      end
    endmodule
    

    Related topics

    • Testbench anatomy

    Previous

    175 / 259

    Next: Scoreboard Architecture

    Up next

    Up nextScoreboard Architecture

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Compare and End-of-Test
    • Reference Models and Coverage
    clkvalidreadydata0a0b0c00lastaccepted0123done
    Accepted beats assembling into one transaction across back-pressure