Skip to content
VLSI Mentor

SPI · Module 16

Reference Model and Scoreboard

Two predictions of the same traffic through one scoreboard: one computed from the transaction, one produced by a second copy of the design. Both report a clean run on a correct design, and with a fault injected the copy records a pass on every transaction.

Chapter 16.5 produced an observation: what the pins carried. A scoreboard needs a second statement — what they should have carried — and the whole value of the comparison rests on where that second statement came from.

A reference model derived from the design predicts the design. It does not go quiet when the design is wrong; it agrees, in writing, on every transaction.

1. The Model Must Be Simpler Than The Design

A reference model is not a second implementation, and the test of that is not that it looks different — it is that it is simpler.

The driver in Chapter 16.4 reasons about lead times, half periods, edge parity, which edge launches in which phase, and where each bit sits for each bit order. The model here does none of that:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   the word observed on MOSI is the word the transaction asked to send
   the word observed on MISO is its complement, because that is the slave's
     contract
   the bit count observed is the frame width

And it does not look at CPOL, CPHA or the bit order at all. That absence is the model's central claim, and it is a claim about the protocol rather than about any implementation: the mode decides how bits travel, not which bits arrive. A frame of 0x5C is 0x5C in all four modes and in both bit orders.

Had the model needed CPHA, it would be re-deriving the driver's edge arithmetic — and two derivations of one arithmetic share their mistakes. The simplicity is the independence. A reference model that grows a case statement per mode has quietly become a copy.

A driver and monitor producing observations, compared by two scoreboards against two predictions — one from a transaction-derived reference model and one from a duplicate of the driver watched by a duplicate monitortransactiondriver under testreference modela second copymonitorits own monitorscoreboardscoreboardfour counters12
Figure 1 — one monitor, two predictions, two scoreboards. The independent model is fed the transaction and computes a prediction from the protocol's arithmetic; the copy is a second instance of the design driving its own pins, watched by its own monitor, so its prediction is literally what this design does. Both predictions reach the same scoreboard module, so nothing about the comparison differs except provenance.

2. The Four Failures A Scoreboard Has To Catch

A scoreboard that only compares payloads catches one class and lets three through.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   WRONG VALUE   the observed word differs from the predicted word.

   WRONG SHAPE   the payload matches and something else does not -- the bit
                 count, or the monitor's partial-frame flag. This is not a
                 hypothetical: see section 5.

   UNEXPECTED    an observation arrives with no prediction waiting. Traffic
                 nobody asked for is a failure even when every field of it is
                 plausible.

   MISSING       the test ends with predictions still queued. The failure that
                 silently passes in most suites, because a transaction that
                 never happened produces no mismatch, no error and no output.

The counters are kept separate on purpose. "The payload matched and the frame was the wrong length" is a different diagnosis from "the payload was wrong", and a suite that merges them loses the distinction exactly when it matters.

3. Both Scoreboards Pass On A Correct Design

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   legal traffic: 32 transactions in 16 configurations

   scoreboard   matched  value bad  shape bad  unexpected  overflow  depth
   independent       32          0          0           0         0      0
   copy              32          0          0           0         0      0

This is the measurement that explains the whole problem, and it is worth sitting with rather than skipping past.

A reference model that is a copy of the design is indistinguishable from a good one until the design is wrong — which is precisely when a reference model is supposed to earn its keep. There is nothing in this table to review, nothing to raise in a meeting, and no reason for anyone to look again. That is why the copy survives, sometimes for the life of a project.

4. The Fault

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   the launch/capture swap, 8 transactions:

   scoreboard   matched  value bad
   independent        0          8
   copy               8          0

Same design, same monitor, same instant. Only the source of the prediction differs.

The copy reported a clean run on every transaction. It did not stay silent: it computed an expected value, compared it, and recorded a pass, eight times out of eight, because it carries the same fault and two wrongs compare equal.

5. A Payload-Only Scoreboard Misses A Protocol Violation Entirely

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a frame with its final edge dropped, in CPHA=0, 3 transactions:
     value mismatches ........................... 0
     payload matched, SHAPE wrong ............... 3
     reported as clean .......................... 0

Every truncated frame was caught, and all three were caught only by the shape check, because the payload compared exactly equal.

The arithmetic is worth following, because it is why a payload-only scoreboard reports a clean run on a frame the protocol forbids. CPHA=0 captures on leading edges. The dropped edge is the frame's final trailing edge. So no capture is lost, every bit arrives, the bit count is right, and the reconstructed word is perfect. What is wrong is the number of edges — odd — which the monitor reports as partial, and which needs no prediction to judge.

6. The Emptiness Check, Proven To Fire

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a prediction with no traffic behind it:
     new mismatches of any kind ................. 0
     new matches ................................ 0
     predictions left queued (the depth) ........ 1

No mismatch, no error, no output of any kind. That is the entire failure mode of a missing transaction: it cannot be compared against anything, so every check stays quiet and the suite passes.

The only thing that finds it is the depth at the end of the test — and this phase exists so that the assertion is one that has been seen to fire rather than one that has always been silent. An end-of-test check that has never fired is in the same category as the dead checkers in Chapter 16.1: possibly correct, possibly unreachable, and indistinguishable from the report.

7. Building It — Three HDLs

Azvya Education Pvt. Ltd.VLSI Mentor
spi_ref_scoreboard.sv — the reference model and the scoreboard, with four distinct failure counters
// spi_ref_scoreboard.sv
//
// Chapter 16.6 -- the reference model and the scoreboard, and the one question that decides
// whether either of them is worth having: WHERE DID THE PREDICTION COME FROM?
//
// This file holds both components, because they are useless apart and because the argument
// for each is the argument against a particular way of writing the other.
//
//
// =====================================================================================
// PART 1 -- THE REFERENCE MODEL, AND WHY IT IS SO MUCH SIMPLER THAN THE DESIGN
// =====================================================================================
//
// A reference model predicts what a correct design would do. It is not a second
// implementation and it must not be written like one, and the test of that is whether it is
// SIMPLER than the design rather than merely different.
//
// This one is dramatically simpler. The driver in Chapter 16.4 reasons about lead times, half
// periods, edge parity, which edge launches in which phase, and where each bit sits for each
// bit order. The model below does none of that. It says:
//
//     the word observed on MOSI is the word the transaction asked to send
//     the word observed on MISO is its complement, because that is the slave's contract
//     the bit count observed is the frame width
//
// AND IT DOES NOT LOOK AT CPOL, CPHA OR THE BIT ORDER AT ALL. That absence is the model's
// central claim, and it is a claim about the protocol rather than about any implementation:
// the mode decides HOW bits travel and not WHICH bits arrive. A frame of 0x5C is 0x5C in all
// four modes and in both bit orders.
//
// If the model needed CPHA, it would be re-deriving the driver's edge arithmetic -- and two
// derivations of the same arithmetic share their mistakes. The simplicity IS the independence,
// and a reference model that grows a case statement per mode has quietly become a copy of the
// design.
//
//
// =====================================================================================
// PART 2 -- THE SCOREBOARD, AND THE THREE FAILURES IT HAS TO CATCH
// =====================================================================================
//
// A scoreboard that only compares payloads catches one class of bug and lets three through.
// This one is built around all four:
//
//   WRONG VALUE      the observed word differs from the predicted word.
//   WRONG SHAPE      the payload matches and something else does not -- here the bit count, or
//                    the monitor's partial-frame flag. A truncated frame can carry a payload
//                    that compares EXACTLY EQUAL: in CPHA=0, dropping the frame's final
//                    trailing edge removes no capture at all, so every bit arrives, the count
//                    is right, and a payload-only scoreboard reports a clean run on a frame
//                    the protocol forbids. The testbench counts how often that happens,
//                    because the number is the argument.
//   UNEXPECTED       an observation arrives with no prediction waiting. Traffic nobody asked
//                    for is a failure even when every field of it is plausible.
//   MISSING          the test ends with predictions still queued. This is the failure that
//                    silently passes in most suites: a transaction that never happened
//                    produces no mismatch, no error, and no output at all. Only an
//                    end-of-test emptiness check finds it, and that check has to be PROVEN
//                    to fire -- the testbench leaves one prediction unmatched on purpose.
//
// The queue is a fixed array with head and tail indices rather than a dynamic structure,
// because this component exists in three languages and one of them has no queues in the
// simulator these examples run in. The depth is checked against overflow, since a scoreboard
// that silently drops a prediction becomes a scoreboard that cannot detect a missing
// transaction -- which is the failure it is most needed for.

`timescale 1ns/1ps

module spi_ref_model #(
    parameter int DW    = 32,
    parameter int LEN_W = 6
) (
    input  wire              clk,
    input  wire              rst_n,

    // A transaction has been issued. The model is told WHAT was asked for, and nothing about
    // how the driver intends to send it.
    input  wire              req_valid,
    input  wire [DW-1:0]     req_data,
    input  wire [LEN_W-1:0]  req_nbits,

    output reg               pred_valid,
    output reg  [DW-1:0]     pred_mosi,
    output reg  [DW-1:0]     pred_miso,
    output reg  [LEN_W:0]    pred_nbits
);

    // The mask is the only arithmetic in the model, and it is the protocol's arithmetic
    // rather than the driver's: a frame of N bits carries the low N bits of the word.
    wire [DW-1:0] mask = ({{(DW-1){1'b0}}, 1'b1} << req_nbits) - {{(DW-1){1'b0}}, 1'b1};

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pred_valid <= 1'b0;
            pred_mosi  <= {DW{1'b0}};
            pred_miso  <= {DW{1'b0}};
            pred_nbits <= {(LEN_W+1){1'b0}};
        end else begin
            pred_valid <= req_valid;
            if (req_valid) begin
                pred_mosi  <= req_data & mask;
                pred_miso  <= (~req_data) & mask;
                pred_nbits <= {1'b0, req_nbits};
            end
        end
    end

endmodule


module spi_scoreboard #(
    parameter int DW    = 32,
    parameter int LEN_W = 6,
    parameter int DEPTH = 16,
    parameter int CNT_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    // predictions in
    input  wire              pred_valid,
    input  wire [DW-1:0]     pred_mosi,
    input  wire [DW-1:0]     pred_miso,
    input  wire [LEN_W:0]    pred_nbits,

    // observations in
    input  wire              obs_valid,
    input  wire [DW-1:0]     obs_mosi,
    input  wire [DW-1:0]     obs_miso,
    input  wire [LEN_W:0]    obs_nbits,

    // A PARTIAL FRAME NEEDS NO PREDICTION, and that makes this the most robust check in the
    // file. Every other comparison here depends on a model being right; this one depends on
    // nothing at all, because no correct transaction is ever partial. Checks that need no
    // prediction are exactly the checks a copy-of-the-design reference model cannot corrupt,
    // and a suite should have as many of them as the protocol allows.
    input  wire              obs_partial,

    output reg  [CNT_W-1:0]  n_match,
    output reg  [CNT_W-1:0]  n_val_bad,     // the payload differed
    output reg  [CNT_W-1:0]  n_shape_bad,   // the payload matched, the bit count did not
    output reg  [CNT_W-1:0]  n_unexpected,  // an observation with no prediction waiting
    output reg  [CNT_W-1:0]  n_overflow,    // a prediction dropped, which breaks everything
    output reg  [CNT_W-1:0]  depth          // still-unmatched predictions: MUST end at zero
);

    reg [DW-1:0]  q_mosi  [0:DEPTH-1];
    reg [DW-1:0]  q_miso  [0:DEPTH-1];
    reg [LEN_W:0] q_nbits [0:DEPTH-1];
    reg [CNT_W-1:0] head, tail;

    wire empty = (depth == {CNT_W{1'b0}});
    wire full  = (depth == DEPTH[CNT_W-1:0]);

    // A PREDICTION AND AN OBSERVATION CAN ARRIVE IN THE SAME CYCLE, and the first version of
    // this scoreboard handled that by pushing and then also matching against the prediction
    // port -- which left the entry it had just pushed in the queue forever. The depth stayed
    // correct, so nothing looked wrong; head and tail drifted one apart permanently, and
    // every subsequent match compared an observation against the PREVIOUS transaction's
    // prediction. A scoreboard whose bookkeeping is off by one reports mismatches that are
    // real and blames the wrong transaction for them, which is worse than reporting nothing.
    //
    // So the coincident case BYPASSES the queue entirely: it is matched on the spot and
    // never stored.
    wire bypass     = obs_valid  &  empty & pred_valid;
    wire push       = pred_valid & ~bypass & ~full;
    wire pop        = obs_valid  & ~empty;
    wire unexpected = obs_valid  &  empty & ~pred_valid;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            head         <= {CNT_W{1'b0}};
            tail         <= {CNT_W{1'b0}};
            depth        <= {CNT_W{1'b0}};
            n_match      <= {CNT_W{1'b0}};
            n_val_bad    <= {CNT_W{1'b0}};
            n_shape_bad  <= {CNT_W{1'b0}};
            n_unexpected <= {CNT_W{1'b0}};
            n_overflow   <= {CNT_W{1'b0}};
        end else begin
            // A DROPPED PREDICTION IS ITS OWN FAILURE and has to be counted rather than
            // absorbed: once a prediction is lost, the end-of-test emptiness check can no
            // longer detect a missing transaction, which is the failure the scoreboard is
            // most needed for.
            if (pred_valid && !bypass && full)
                n_overflow <= n_overflow + 1'b1;

            if (push) begin
                q_mosi[tail]  <= pred_mosi;
                q_miso[tail]  <= pred_miso;
                q_nbits[tail] <= pred_nbits;
                tail <= (tail == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : tail + 1'b1;
            end

            // TRAFFIC NOBODY ASKED FOR. Every field of it may be plausible, which is why it
            // has to be counted rather than ignored: a design that emits an extra frame is a
            // design that will one day emit it into a real system.
            if (unexpected)
                n_unexpected <= n_unexpected + 1'b1;

            if (bypass) begin
                if (obs_mosi !== pred_mosi || obs_miso !== pred_miso)
                    n_val_bad <= n_val_bad + 1'b1;
                else if (obs_nbits !== pred_nbits || obs_partial)
                    n_shape_bad <= n_shape_bad + 1'b1;
                else
                    n_match <= n_match + 1'b1;
            end else if (pop) begin
                if (obs_mosi !== q_mosi[head] || obs_miso !== q_miso[head])
                    n_val_bad <= n_val_bad + 1'b1;
                // THE SHAPE CHECK IS SEPARATE FROM THE VALUE CHECK, and the counters are
                // separate too, because "the payload matched and the frame was the wrong
                // length" is a different diagnosis from "the payload was wrong", and a suite
                // that merges them loses the distinction exactly when it matters.
                else if (obs_nbits !== q_nbits[head] || obs_partial)
                    n_shape_bad <= n_shape_bad + 1'b1;
                else
                    n_match <= n_match + 1'b1;
                head <= (head == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : head + 1'b1;
            end

            // One expression for the depth, covering all four combinations.
            if (push && pop)      depth <= depth;
            else if (push)        depth <= depth + 1'b1;
            else if (pop)         depth <= depth - 1'b1;
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_ref_scoreboard.v — the same design in Verilog-2001
// spi_ref_scoreboard.v
//
// Chapter 16.6 -- the reference model and the scoreboard, and the one question that decides
// whether either of them is worth having: WHERE DID THE PREDICTION COME FROM?
//
// This file holds both components, because they are useless apart and because the argument
// for each is the argument against a particular way of writing the other.
//
//
// =====================================================================================
// PART 1 -- THE REFERENCE MODEL, AND WHY IT IS SO MUCH SIMPLER THAN THE DESIGN
// =====================================================================================
//
// A reference model predicts what a correct design would do. It is not a second
// implementation and it must not be written like one, and the test of that is whether it is
// SIMPLER than the design rather than merely different.
//
// This one is dramatically simpler. The driver in Chapter 16.4 reasons about lead times, half
// periods, edge parity, which edge launches in which phase, and where each bit sits for each
// bit order. The model below does none of that. It says:
//
//     the word observed on MOSI is the word the transaction asked to send
//     the word observed on MISO is its complement, because that is the slave's contract
//     the bit count observed is the frame width
//
// AND IT DOES NOT LOOK AT CPOL, CPHA OR THE BIT ORDER AT ALL. That absence is the model's
// central claim, and it is a claim about the protocol rather than about any implementation:
// the mode decides HOW bits travel and not WHICH bits arrive. A frame of 0x5C is 0x5C in all
// four modes and in both bit orders.
//
// If the model needed CPHA, it would be re-deriving the driver's edge arithmetic -- and two
// derivations of the same arithmetic share their mistakes. The simplicity IS the independence,
// and a reference model that grows a case statement per mode has quietly become a copy of the
// design.
//
//
// =====================================================================================
// PART 2 -- THE SCOREBOARD, AND THE THREE FAILURES IT HAS TO CATCH
// =====================================================================================
//
// A scoreboard that only compares payloads catches one class of bug and lets three through.
// This one is built around all four:
//
//   WRONG VALUE      the observed word differs from the predicted word.
//   WRONG SHAPE      the payload matches and something else does not -- here the bit count, or
//                    the monitor's partial-frame flag. A truncated frame can carry a payload
//                    that compares EXACTLY EQUAL: in CPHA=0, dropping the frame's final
//                    trailing edge removes no capture at all, so every bit arrives, the count
//                    is right, and a payload-only scoreboard reports a clean run on a frame
//                    the protocol forbids. The testbench counts how often that happens,
//                    because the number is the argument.
//   UNEXPECTED       an observation arrives with no prediction waiting. Traffic nobody asked
//                    for is a failure even when every field of it is plausible.
//   MISSING          the test ends with predictions still queued. This is the failure that
//                    silently passes in most suites: a transaction that never happened
//                    produces no mismatch, no error, and no output at all. Only an
//                    end-of-test emptiness check finds it, and that check has to be PROVEN
//                    to fire -- the testbench leaves one prediction unmatched on purpose.
//
// The queue is a fixed array with head and tail indices rather than a dynamic structure,
// because this component exists in three languages and one of them has no queues in the
// simulator these examples run in. The depth is checked against overflow, since a scoreboard
// that silently drops a prediction becomes a scoreboard that cannot detect a missing
// transaction -- which is the failure it is most needed for.

`timescale 1ns/1ps

module spi_ref_model #(
    parameter DW    = 32,
    parameter LEN_W = 6
) (
    input  wire              clk,
    input  wire              rst_n,

    // A transaction has been issued. The model is told WHAT was asked for, and nothing about
    // how the driver intends to send it.
    input  wire              req_valid,
    input  wire [DW-1:0]     req_data,
    input  wire [LEN_W-1:0]  req_nbits,

    output reg               pred_valid,
    output reg  [DW-1:0]     pred_mosi,
    output reg  [DW-1:0]     pred_miso,
    output reg  [LEN_W:0]    pred_nbits
);

    // The mask is the only arithmetic in the model, and it is the protocol's arithmetic
    // rather than the driver's: a frame of N bits carries the low N bits of the word.
    wire [DW-1:0] mask = ({{(DW-1){1'b0}}, 1'b1} << req_nbits) - {{(DW-1){1'b0}}, 1'b1};

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pred_valid <= 1'b0;
            pred_mosi  <= {DW{1'b0}};
            pred_miso  <= {DW{1'b0}};
            pred_nbits <= {(LEN_W+1){1'b0}};
        end else begin
            pred_valid <= req_valid;
            if (req_valid) begin
                pred_mosi  <= req_data & mask;
                pred_miso  <= (~req_data) & mask;
                pred_nbits <= {1'b0, req_nbits};
            end
        end
    end

endmodule


module spi_scoreboard #(
    parameter DW    = 32,
    parameter LEN_W = 6,
    parameter DEPTH = 16,
    parameter CNT_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    // predictions in
    input  wire              pred_valid,
    input  wire [DW-1:0]     pred_mosi,
    input  wire [DW-1:0]     pred_miso,
    input  wire [LEN_W:0]    pred_nbits,

    // observations in
    input  wire              obs_valid,
    input  wire [DW-1:0]     obs_mosi,
    input  wire [DW-1:0]     obs_miso,
    input  wire [LEN_W:0]    obs_nbits,

    // A PARTIAL FRAME NEEDS NO PREDICTION, and that makes this the most robust check in the
    // file. Every other comparison here depends on a model being right; this one depends on
    // nothing at all, because no correct transaction is ever partial. Checks that need no
    // prediction are exactly the checks a copy-of-the-design reference model cannot corrupt,
    // and a suite should have as many of them as the protocol allows.
    input  wire              obs_partial,

    output reg  [CNT_W-1:0]  n_match,
    output reg  [CNT_W-1:0]  n_val_bad,     // the payload differed
    output reg  [CNT_W-1:0]  n_shape_bad,   // the payload matched, the bit count did not
    output reg  [CNT_W-1:0]  n_unexpected,  // an observation with no prediction waiting
    output reg  [CNT_W-1:0]  n_overflow,    // a prediction dropped, which breaks everything
    output reg  [CNT_W-1:0]  depth          // still-unmatched predictions: MUST end at zero
);

    reg [DW-1:0]  q_mosi  [0:DEPTH-1];
    reg [DW-1:0]  q_miso  [0:DEPTH-1];
    reg [LEN_W:0] q_nbits [0:DEPTH-1];
    reg [CNT_W-1:0] head, tail;

    wire empty = (depth == {CNT_W{1'b0}});
    wire full  = (depth == DEPTH[CNT_W-1:0]);

    // A PREDICTION AND AN OBSERVATION CAN ARRIVE IN THE SAME CYCLE, and the first version of
    // this scoreboard handled that by pushing and then also matching against the prediction
    // port -- which left the entry it had just pushed in the queue forever. The depth stayed
    // correct, so nothing looked wrong; head and tail drifted one apart permanently, and
    // every subsequent match compared an observation against the PREVIOUS transaction's
    // prediction. A scoreboard whose bookkeeping is off by one reports mismatches that are
    // real and blames the wrong transaction for them, which is worse than reporting nothing.
    //
    // So the coincident case BYPASSES the queue entirely: it is matched on the spot and
    // never stored.
    wire bypass     = obs_valid  &  empty & pred_valid;
    wire push       = pred_valid & ~bypass & ~full;
    wire pop        = obs_valid  & ~empty;
    wire unexpected = obs_valid  &  empty & ~pred_valid;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            head         <= {CNT_W{1'b0}};
            tail         <= {CNT_W{1'b0}};
            depth        <= {CNT_W{1'b0}};
            n_match      <= {CNT_W{1'b0}};
            n_val_bad    <= {CNT_W{1'b0}};
            n_shape_bad  <= {CNT_W{1'b0}};
            n_unexpected <= {CNT_W{1'b0}};
            n_overflow   <= {CNT_W{1'b0}};
        end else begin
            // A DROPPED PREDICTION IS ITS OWN FAILURE and has to be counted rather than
            // absorbed: once a prediction is lost, the end-of-test emptiness check can no
            // longer detect a missing transaction, which is the failure the scoreboard is
            // most needed for.
            if (pred_valid && !bypass && full)
                n_overflow <= n_overflow + 1'b1;

            if (push) begin
                q_mosi[tail]  <= pred_mosi;
                q_miso[tail]  <= pred_miso;
                q_nbits[tail] <= pred_nbits;
                tail <= (tail == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : tail + 1'b1;
            end

            // TRAFFIC NOBODY ASKED FOR. Every field of it may be plausible, which is why it
            // has to be counted rather than ignored: a design that emits an extra frame is a
            // design that will one day emit it into a real system.
            if (unexpected)
                n_unexpected <= n_unexpected + 1'b1;

            if (bypass) begin
                if (obs_mosi !== pred_mosi || obs_miso !== pred_miso)
                    n_val_bad <= n_val_bad + 1'b1;
                else if (obs_nbits !== pred_nbits || obs_partial)
                    n_shape_bad <= n_shape_bad + 1'b1;
                else
                    n_match <= n_match + 1'b1;
            end else if (pop) begin
                if (obs_mosi !== q_mosi[head] || obs_miso !== q_miso[head])
                    n_val_bad <= n_val_bad + 1'b1;
                // THE SHAPE CHECK IS SEPARATE FROM THE VALUE CHECK, and the counters are
                // separate too, because "the payload matched and the frame was the wrong
                // length" is a different diagnosis from "the payload was wrong", and a suite
                // that merges them loses the distinction exactly when it matters.
                else if (obs_nbits !== q_nbits[head] || obs_partial)
                    n_shape_bad <= n_shape_bad + 1'b1;
                else
                    n_match <= n_match + 1'b1;
                head <= (head == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : head + 1'b1;
            end

            // One expression for the depth, covering all four combinations.
            if (push && pop)      depth <= depth;
            else if (push)        depth <= depth + 1'b1;
            else if (pop)         depth <= depth - 1'b1;
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_ref_scoreboard.vhd — the same design in VHDL
-- spi_ref_scoreboard.vhd
--
-- Chapter 16.6 -- the reference model and the scoreboard, and the one question that decides
-- whether either is worth having: WHERE DID THE PREDICTION COME FROM?
--
-- Both components live in this file because they are useless apart, and because the argument
-- for each is the argument against a particular way of writing the other.
--
--
-- =====================================================================================
-- PART 1 -- THE REFERENCE MODEL, AND WHY IT IS SO MUCH SIMPLER THAN THE DESIGN
-- =====================================================================================
--
-- A reference model predicts what a correct design would do. It is not a second
-- implementation and must not be written like one, and the test of that is whether it is
-- SIMPLER than the design rather than merely different.
--
-- This one is dramatically simpler. The driver in Chapter 16.4 reasons about lead times, half
-- periods, edge parity, which edge launches in which phase, and where each bit sits for each
-- bit order. The model below does none of that:
--
--     the word observed on MOSI is the word the transaction asked to send
--     the word observed on MISO is its complement, because that is the slave's contract
--     the bit count observed is the frame width
--
-- AND IT DOES NOT LOOK AT CPOL, CPHA OR THE BIT ORDER AT ALL. That absence is the model's
-- central claim, and it is a claim about the protocol rather than about any implementation:
-- the mode decides HOW bits travel, not WHICH bits arrive. A frame of 0x5C is 0x5C in all four
-- modes and in both bit orders.
--
-- Had the model needed CPHA it would be re-deriving the driver's edge arithmetic, and two
-- derivations of one arithmetic share their mistakes. The simplicity IS the independence, and
-- a reference model that grows a case statement per mode has quietly become a copy.
--
--
-- =====================================================================================
-- PART 2 -- THE SCOREBOARD, AND THE FOUR FAILURES IT HAS TO CATCH
-- =====================================================================================
--
--   WRONG VALUE      the observed word differs from the predicted word.
--   WRONG SHAPE      the payload matches and something else does not -- the bit count, or the
--                    monitor's partial-frame flag. A truncated frame can carry a payload that
--                    compares EXACTLY EQUAL: in CPHA=0, dropping the frame's final trailing
--                    edge removes no capture at all, so every bit arrives, the count is right,
--                    and a payload-only scoreboard reports a clean run on a frame the protocol
--                    forbids.
--   UNEXPECTED       an observation arrives with no prediction waiting. Traffic nobody asked
--                    for is a failure even when every field of it is plausible.
--   MISSING          the test ends with predictions still queued. This is the failure that
--                    silently passes in most suites: a transaction that never happened
--                    produces no mismatch, no error and no output at all. Only an end-of-test
--                    emptiness check finds it, and that check has to be PROVEN to fire.
--
-- WHAT VHDL CONTRIBUTES. The queue holds a RECORD, so a prediction travels and is compared as
-- one object and a new field costs nothing at any comparison site; and the counters are a
-- PROTECTED TYPE, which is the language refusing the unprotected shared variable rather than
-- leaving the race to review. The SystemVerilog and Verilog versions spell the same structure
-- as parallel arrays and separate output ports, because the simulator these examples run in
-- offers no alternative -- and the parallel-array form is exactly where a queue's fields drift
-- out of step with each other.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package spi_sb_pkg is

    constant SDW    : natural  := 32;
    constant SLEN_W : positive := 6;

    -- A prediction, as one object.
    type spi_pred_t is record
        mosi  : std_logic_vector(SDW - 1 downto 0);
        miso  : std_logic_vector(SDW - 1 downto 0);
        nbits : natural;
    end record;

    constant PRED_ZERO : spi_pred_t := (mosi  => (others => '0'),
                                        miso  => (others => '0'),
                                        nbits => 0);

    type sb_counts_t is record
        matched    : natural;
        value_bad  : natural;
        shape_bad  : natural;
        unexpected : natural;
        overflow   : natural;
        depth      : natural;
    end record;

end package spi_sb_pkg;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_sb_pkg.all;

entity spi_ref_model is
    port (
        clk        : in  std_logic;
        rst_n      : in  std_logic;

        -- A transaction has been issued. The model is told WHAT was asked for, and nothing
        -- about how the driver intends to send it.
        req_valid  : in  std_logic;
        req_data   : in  std_logic_vector(SDW - 1 downto 0);
        req_nbits  : in  unsigned(SLEN_W - 1 downto 0);

        pred_valid : out std_logic;
        pred       : out spi_pred_t
    );
end entity spi_ref_model;

architecture rtl of spi_ref_model is
    signal v_r : std_logic  := '0';
    signal p_r : spi_pred_t := PRED_ZERO;
begin
    pred_valid <= v_r;
    pred       <= p_r;

    process (clk, rst_n) is
        variable mask : std_logic_vector(SDW - 1 downto 0);
    begin
        if rst_n = '0' then
            v_r <= '0';
            p_r <= PRED_ZERO;
        elsif rising_edge(clk) then
            v_r <= req_valid;
            if req_valid = '1' then
                -- The only arithmetic in the model, and it is the PROTOCOL's arithmetic
                -- rather than the driver's: a frame of N bits carries the low N bits.
                mask := std_logic_vector(shift_left(to_unsigned(1, SDW),
                                                    to_integer(req_nbits)) - 1);
                p_r.mosi  <= req_data and mask;
                p_r.miso  <= (not req_data) and mask;
                p_r.nbits <= to_integer(req_nbits);
            end if;
        end if;
    end process;
end architecture rtl;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_sb_pkg.all;

entity spi_scoreboard is
    generic (
        DEPTH : positive := 16
    );
    port (
        clk         : in  std_logic;
        rst_n       : in  std_logic;

        pred_valid  : in  std_logic;
        pred        : in  spi_pred_t;

        obs_valid   : in  std_logic;
        obs         : in  spi_pred_t;

        -- A PARTIAL FRAME NEEDS NO PREDICTION, and that makes this the most robust check in
        -- the file. Every other comparison here depends on a model being right; this one
        -- depends on nothing at all, because no correct transaction is ever partial. Checks
        -- that need no prediction are exactly the checks a copy-of-the-design reference model
        -- cannot corrupt, and a suite should have as many of them as the protocol allows.
        obs_partial : in  std_logic;

        counts      : out sb_counts_t
    );
end entity spi_scoreboard;

architecture rtl of spi_scoreboard is

    type q_t is array (0 to DEPTH - 1) of spi_pred_t;

    signal c_r : sb_counts_t := (0, 0, 0, 0, 0, 0);

begin

    counts <= c_r;

    process (clk, rst_n) is
        variable q          : q_t := (others => PRED_ZERO);
        variable head, tail : natural := 0;
        variable dep        : natural := 0;
        variable empty      : boolean;
        variable full       : boolean;
        variable bypass     : boolean;
        variable push, pop  : boolean;
        variable unexpected : boolean;
        variable cmp        : spi_pred_t;
    begin
        if rst_n = '0' then
            head := 0;
            tail := 0;
            dep  := 0;
            c_r  <= (0, 0, 0, 0, 0, 0);

        elsif rising_edge(clk) then
            empty := (dep = 0);
            full  := (dep = DEPTH);

            -- A PREDICTION AND AN OBSERVATION CAN ARRIVE IN THE SAME CYCLE, and the first
            -- version of this scoreboard handled that by pushing and then also matching
            -- against the prediction port -- which left the entry it had just pushed in the
            -- queue forever. The depth stayed correct, so nothing looked wrong; head and tail
            -- drifted one apart permanently, and every later match compared an observation
            -- against the PREVIOUS transaction's prediction. A scoreboard whose bookkeeping is
            -- off by one reports mismatches that are real and blames the wrong transaction for
            -- them, which is worse than reporting nothing.
            --
            -- So the coincident case BYPASSES the queue entirely.
            bypass     := (obs_valid = '1') and empty and (pred_valid = '1');
            push       := (pred_valid = '1') and (not bypass) and (not full);
            pop        := (obs_valid = '1') and (not empty);
            unexpected := (obs_valid = '1') and empty and (pred_valid = '0');

            -- A DROPPED PREDICTION IS ITS OWN FAILURE and has to be counted rather than
            -- absorbed: once a prediction is lost, the end-of-test emptiness check can no
            -- longer detect a missing transaction, which is the failure it is most needed for.
            if (pred_valid = '1') and (not bypass) and full then
                c_r.overflow <= c_r.overflow + 1;
            end if;

            if push then
                q(tail) := pred;
                if tail = DEPTH - 1 then tail := 0; else tail := tail + 1; end if;
                dep := dep + 1;
            end if;

            -- TRAFFIC NOBODY ASKED FOR. Every field of it may be plausible, which is why it is
            -- counted rather than ignored: a design that emits an extra frame is a design that
            -- will one day emit it into a real system.
            if unexpected then
                c_r.unexpected <= c_r.unexpected + 1;
            end if;

            if bypass or pop then
                if bypass then
                    cmp := pred;
                else
                    cmp := q(head);
                    if head = DEPTH - 1 then head := 0; else head := head + 1; end if;
                    dep := dep - 1;
                end if;

                if obs.mosi /= cmp.mosi or obs.miso /= cmp.miso then
                    c_r.value_bad <= c_r.value_bad + 1;
                -- THE SHAPE CHECK IS SEPARATE FROM THE VALUE CHECK, and the counters are
                -- separate too, because "the payload matched and the frame was the wrong
                -- length" is a different diagnosis from "the payload was wrong", and a suite
                -- that merges them loses the distinction exactly when it matters.
                elsif obs.nbits /= cmp.nbits or obs_partial = '1' then
                    c_r.shape_bad <= c_r.shape_bad + 1;
                else
                    c_r.matched <= c_r.matched + 1;
                end if;
            end if;

            c_r.depth <= dep;
        end if;
    end process;

end architecture rtl;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_ref_scoreboard_tb.sv — two predictions of the same traffic: one from the transaction, one from a copy of the design
// spi_ref_scoreboard_tb.sv
//
// TWO PREDICTIONS OF THE SAME TRAFFIC, ONE SCOREBOARD EACH, AND THE ONLY DIFFERENCE IS WHERE
// THE PREDICTION CAME FROM.
//
//   sb_indep  is fed by `spi_ref_model` -- a prediction computed from the TRANSACTION. It
//             knows the data and the width. It does not know CPOL, CPHA, the bit order, the
//             lead, the half period, or that SPI has edges.
//
//   sb_copy   is fed by a SECOND COPY OF THE DRIVER, wired to its own pins and watched by its
//             own monitor. Its prediction is literally "what this design does". This is the
//             single most common way a reference model goes wrong in real projects, and it
//             almost never looks like this in the source: it looks like a model that was
//             "validated against the RTL" until they agreed, or a model whose author fixed it
//             every time it disagreed until it stopped disagreeing. The end state is the same
//             object, and the twin here is its honest form.
//
// Both scoreboards pass on a correct design. That is why a copy survives review.
//
// THEN A FAULT IS INJECTED, and the results separate completely:
//
//   sb_indep  reports a value mismatch on every transaction. The bug is caught.
//   sb_copy   reports a clean run. The twin has the same fault, so it predicts the same wrong
//             word, and two wrongs compare equal.
//
// A reference model derived from the implementation predicts the implementation. It cannot do
// anything else, and the failure is not that it is silent -- it is that it AGREES, confidently,
// in writing, on every transaction.
//
// TWO MORE THINGS THIS BENCH MEASURES, because "the copy misses a bug" is only the headline.
//
//   A PAYLOAD-ONLY SCOREBOARD MISSES A PROTOCOL VIOLATION ENTIRELY. With the frame's final
//   edge dropped in CPHA=0, every captured bit still arrives -- the dropped edge is a trailing
//   edge and CPHA=0 captures on leading ones -- so the payload compares EXACTLY EQUAL and the
//   bit count is right. Only the partial-frame flag, which needs no prediction at all, says
//   anything. The bench counts these separately from value mismatches, and the count is the
//   argument for having shape checks at all.
//
//   A TEST THAT ENDS WITH PREDICTIONS STILL QUEUED HAS SILENTLY PASSED. A transaction that
//   never happened produces no mismatch, no error, and no output. The only thing that finds it
//   is an end-of-test emptiness check -- and that check has to be proven to fire, so the last
//   phase of this bench issues a prediction with no traffic behind it on purpose and requires
//   the depth to be non-zero.

`timescale 1ns/1ps

module spi_ref_scoreboard_tb;

    localparam int LEAD  = 4;
    localparam int HALF  = 3;
    localparam int LAG   = 2;
    localparam int GAP   = 3;
    localparam int DW    = 32;
    localparam int LEN_W = 6;
    localparam int CNT_W = 10;
    localparam int SCNT  = 16;

    localparam int F_NONE = 0, F_PHASE = 3, F_TRUNC = 4;

    reg clk = 1'b0;
    always #5 clk = ~clk;
    reg rst_n = 1'b1;

    reg              start = 1'b0;
    reg  [DW-1:0]    tx_data = {DW{1'b0}};
    reg  [LEN_W-1:0] nbits = 6'd8;
    reg              cpol = 1'b0, cpha = 1'b0, lsb_first = 1'b0;
    reg  [2:0]       fault = 3'd0;

    // ------------------------------------------------------------------
    // THE DESIGN UNDER TEST.
    // ------------------------------------------------------------------
    wire busy, done;
    wire [DW-1:0] drv_rx;
    wire sclk, cs_n, mosi;
    wire miso = ~mosi;

    spi_driver #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
                 .DW(DW), .LEN_W(LEN_W), .CNT_W(16)) u_drv (
        .clk(clk), .rst_n(rst_n),
        .start(start), .tx_data(tx_data), .nbits(nbits),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
        .busy(busy), .done(done), .rx_data(drv_rx),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso)
    );

    wire              o_valid, o_partial;
    wire [DW-1:0]     o_mosi, o_miso;
    wire [LEN_W:0]    o_nbits;
    wire [CNT_W-1:0]  o_edges;

    spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_mon (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .len(nbits),
        .t_valid(o_valid), .t_mosi(o_mosi), .t_miso(o_miso),
        .t_nbits(o_nbits), .t_edges(o_edges), .t_partial(o_partial)
    );

    // ------------------------------------------------------------------
    // THE COPY. Same inputs, its own pins, its own monitor. Nothing about it is a strawman:
    // it is the design, and that is the entire problem with it.
    // ------------------------------------------------------------------
    wire tw_busy, tw_done;
    wire [DW-1:0] tw_rx;
    wire tw_sclk, tw_cs_n, tw_mosi;
    wire tw_miso = ~tw_mosi;

    spi_driver #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
                 .DW(DW), .LEN_W(LEN_W), .CNT_W(16)) u_drv_twin (
        .clk(clk), .rst_n(rst_n),
        .start(start), .tx_data(tx_data), .nbits(nbits),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
        .busy(tw_busy), .done(tw_done), .rx_data(tw_rx),
        .sclk(tw_sclk), .cs_n(tw_cs_n), .mosi(tw_mosi), .miso(tw_miso)
    );

    wire              c_valid, c_partial;
    wire [DW-1:0]     c_mosi, c_miso;
    wire [LEN_W:0]    c_nbits;
    wire [CNT_W-1:0]  c_edges;

    spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_mon_twin (
        .clk(clk), .rst_n(rst_n),
        .sclk(tw_sclk), .cs_n(tw_cs_n), .mosi(tw_mosi), .miso(tw_miso),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .len(nbits),
        .t_valid(c_valid), .t_mosi(c_mosi), .t_miso(c_miso),
        .t_nbits(c_nbits), .t_edges(c_edges), .t_partial(c_partial)
    );

    // ------------------------------------------------------------------
    // THE INDEPENDENT MODEL.
    //
    // It is told THAT a transaction was issued and WHAT it asked for. `busy` rising is the
    // handshake, which is the same information a sequencer has -- not introspection. The
    // model is given no signal that describes how the driver intends to send anything.
    // ------------------------------------------------------------------
    reg busy_d = 1'b0;
    always @(posedge clk) if (rst_n) busy_d <= busy; else busy_d <= 1'b0;
    wire req_valid = busy & ~busy_d;

    // A prediction the bench can inject with no traffic behind it, to prove the end-of-test
    // emptiness check actually fires.
    reg extra_pred = 1'b0;

    wire              p_valid;
    wire [DW-1:0]     p_mosi, p_miso;
    wire [LEN_W:0]    p_nbits;

    spi_ref_model #(.DW(DW), .LEN_W(LEN_W)) u_ref (
        .clk(clk), .rst_n(rst_n),
        .req_valid(req_valid | extra_pred), .req_data(tx_data), .req_nbits(nbits),
        .pred_valid(p_valid), .pred_mosi(p_mosi), .pred_miso(p_miso), .pred_nbits(p_nbits)
    );

    // ------------------------------------------------------------------
    // TWO SCOREBOARDS. Identical module, identical observations, different predictions.
    // ------------------------------------------------------------------
    wire [15:0] i_match, i_val, i_shape, i_unexp, i_ovf, i_depth;
    wire [15:0] c_match, c_val, c_shape, c_unexp, c_ovf, c_depth;

    spi_scoreboard #(.DW(DW), .LEN_W(LEN_W), .DEPTH(SCNT), .CNT_W(16)) sb_indep (
        .clk(clk), .rst_n(rst_n),
        .pred_valid(p_valid), .pred_mosi(p_mosi), .pred_miso(p_miso), .pred_nbits(p_nbits),
        .obs_valid(o_valid), .obs_mosi(o_mosi), .obs_miso(o_miso), .obs_nbits(o_nbits),
        .obs_partial(o_partial),
        .n_match(i_match), .n_val_bad(i_val), .n_shape_bad(i_shape),
        .n_unexpected(i_unexp), .n_overflow(i_ovf), .depth(i_depth)
    );

    spi_scoreboard #(.DW(DW), .LEN_W(LEN_W), .DEPTH(SCNT), .CNT_W(16)) sb_copy (
        .clk(clk), .rst_n(rst_n),
        .pred_valid(c_valid), .pred_mosi(c_mosi), .pred_miso(c_miso), .pred_nbits(c_nbits),
        .obs_valid(o_valid), .obs_mosi(o_mosi), .obs_miso(o_miso), .obs_nbits(o_nbits),
        .obs_partial(1'b0),   // the copy is judged on its PREDICTION only, so that the one
                              // check needing no prediction cannot flatter it
        .n_match(c_match), .n_val_bad(c_val), .n_shape_bad(c_shape),
        .n_unexpected(c_unexp), .n_overflow(c_ovf), .depth(c_depth)
    );

    integer errors = 0;

    initial begin
        #400_000;
        $display("FAIL: the simulation did not finish within its time limit");
        $finish;
    end

    task automatic set_cfg(input [LEN_W-1:0] n, input integer pol, input integer pha,
                           input integer lsb, input [2:0] f, input [DW-1:0] d);
        begin
            @(negedge clk);
            nbits = n; cpol = pol[0]; cpha = pha[0]; lsb_first = lsb[0];
            fault = f; tx_data = d;
            repeat (6) @(negedge clk);
        end
    endtask

    task automatic run_burst(input integer ntxn);
        integer k;
        begin
            k = 0;
            @(negedge clk);
            start = 1'b1;
            while (k < ntxn) begin
                @(negedge clk);
                if (done) begin
                    k = k + 1;
                    if (k == ntxn) start = 1'b0;
                end
            end
            repeat (GAP + LAG + 8) @(negedge clk);
        end
    endtask

    integer iw, ipol, ipha, ilsb;
    reg [LEN_W-1:0] w;
    integer b_im, b_iv, b_is, b_cm, b_cv, b_cs;
    integer legal_txns, legal_cfgs;
    integer ph_txns;

    initial begin
        rst_n = 1'b1;
        repeat (2) @(negedge clk);
        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (4) @(negedge clk);

        // ============================================================
        // 1. BOTH SCOREBOARDS PASS ON A CORRECT DESIGN.
        //    This is the measurement that explains why a copy survives review.
        // ============================================================
        legal_cfgs = 0;
        for (iw = 0; iw < 2; iw = iw + 1) begin
            w = (iw == 0) ? 6'd8 : 6'd13;
            for (ipol = 0; ipol < 2; ipol = ipol + 1)
            for (ipha = 0; ipha < 2; ipha = ipha + 1)
            for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
                set_cfg(w, ipol, ipha, ilsb, F_NONE[2:0],
                        (iw == 0) ? 32'h0000_1A5C : 32'h0000_0C3A);
                run_burst(2);
                legal_cfgs = legal_cfgs + 1;
            end
        end
        legal_txns = i_match + i_val + i_shape;

        $display("  legal traffic: %0d transactions in %0d configurations", legal_txns, legal_cfgs);
        $display("  scoreboard   matched  value bad  shape bad  unexpected  overflow  depth");
        $display("  independent  %7d  %9d  %9d  %10d  %8d  %5d   prediction from the transaction",
                 i_match, i_val, i_shape, i_unexp, i_ovf, i_depth);
        $display("  copy         %7d  %9d  %9d  %10d  %8d  %5d   prediction from a second copy of the design",
                 c_match, c_val, c_shape, c_unexp, c_ovf, c_depth);

        if (legal_txns == 0) begin
            $display("  FAIL: no transactions reached the scoreboards at all, so nothing below means anything");
            errors = errors + 1;
        end
        if (i_match != legal_txns || i_val != 0 || i_shape != 0 || i_unexp != 0 || i_ovf != 0) begin
            $display("  FAIL: the independent scoreboard did not report a clean run on legal traffic");
            errors = errors + 1;
        end
        if (c_match != legal_txns || c_val != 0 || c_shape != 0 || c_unexp != 0 || c_ovf != 0) begin
            $display("  FAIL: the copy scoreboard did not report a clean run on legal traffic");
            errors = errors + 1;
        end
        if (i_depth != 0 || c_depth != 0) begin
            $display("  FAIL: a scoreboard ended the legal phase with %0d and %0d predictions still queued",
                     i_depth, c_depth);
            errors = errors + 1;
        end
        $display("    1. BOTH scoreboards reported a perfectly clean run on all %0d transactions, and both ended empty. That is not a weak result, it is the explanation: a reference model that is a copy of the design is indistinguishable from a good one until the design is wrong, which is precisely when a reference model is supposed to earn its keep",
                 legal_txns);

        // ============================================================
        // 2. THE FAULT. One design, two predictions, two different verdicts.
        // ============================================================
        b_im = i_match; b_iv = i_val; b_is = i_shape;
        b_cm = c_match; b_cv = c_val; b_cs = c_shape;

        for (ipol = 0; ipol < 2; ipol = ipol + 1)
        for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
            // CPHA=1, where the launch/capture swap moves the data by a bit position and is
            // therefore visible to a value comparison at all. Chapter 16.5 measured why the
            // other phase is not.
            set_cfg(6'd8, ipol, 1, ilsb, F_PHASE[2:0], 32'h0000_1A5C);
            run_burst(2);
        end
        ph_txns = (i_match - b_im) + (i_val - b_iv) + (i_shape - b_is);

        $display("  the launch/capture swap, %0d transactions:", ph_txns);
        $display("  scoreboard   matched  value bad");
        $display("  independent  %7d  %9d", i_match - b_im, i_val - b_iv);
        $display("  copy         %7d  %9d", c_match - b_cm, c_val - b_cv);

        if ((i_val - b_iv) != ph_txns) begin
            $display("  FAIL: the independent model agreed with the faulted design on %0d of %0d transactions; a prediction computed from the transaction cannot agree with a shifted word",
                     i_match - b_im, ph_txns);
            errors = errors + 1;
        end
        if ((c_match - b_cm) != ph_txns || (c_val - b_cv) != 0) begin
            $display("  FAIL: the copy model did NOT agree with the faulted design on every transaction (%0d matched, %0d value mismatches of %0d), so it is not actually a copy and the comparison proves nothing",
                     c_match - b_cm, c_val - b_cv, ph_txns);
            errors = errors + 1;
        end
        $display("    2. the independent model reported a value mismatch on ALL %0d faulted transactions and the copy reported a CLEAN RUN on all %0d. Same design, same monitor, same instant -- only the source of the prediction differs. The copy has the same fault, so it predicts the same wrong word, and two wrongs compare equal",
                 i_val - b_iv, c_match - b_cm);
        $display("       and the copy's failure is not silence. It wrote down an expected value, compared it, and recorded a PASS, on every transaction, with total confidence. A model validated against the RTL until they agreed ends up as exactly this object, and its source will not look like this file");

        // ============================================================
        // 3. A PAYLOAD-ONLY SCOREBOARD MISSES A PROTOCOL VIOLATION.
        // ============================================================
        b_im = i_match; b_iv = i_val; b_is = i_shape;
        set_cfg(6'd8, 0, 0, 0, F_TRUNC[2:0], 32'h0000_1A5C);
        run_burst(3);
        $display("  a frame with its final edge dropped, in CPHA=0, %0d transactions:",
                 (i_match - b_im) + (i_val - b_iv) + (i_shape - b_is));
        $display("    value mismatches ........................... %0d", i_val - b_iv);
        $display("    payload matched, SHAPE wrong ............... %0d", i_shape - b_is);
        $display("    reported as clean .......................... %0d", i_match - b_im);

        if ((i_shape - b_is) == 0) begin
            $display("  FAIL: no transaction was caught by the shape check, so this stimulus does not demonstrate the point");
            errors = errors + 1;
        end
        if ((i_match - b_im) != 0) begin
            $display("  FAIL: %0d truncated frames were reported clean by the independent scoreboard",
                     i_match - b_im);
            errors = errors + 1;
        end
        $display("    3. every truncated frame was caught, and %0d of them were caught ONLY by the shape check: the payload compared EXACTLY EQUAL. In CPHA=0 the dropped edge is a trailing edge and CPHA=0 captures on leading ones, so no bit is lost, the count is right, and the word is perfect -- on a frame the protocol forbids. A payload-only scoreboard reports a clean run here, and the check that catches it needs NO PREDICTION AT ALL, which is why it is also the one check a copy-based model cannot corrupt",
                 i_shape - b_is);

        // ============================================================
        // 4. THE EMPTINESS CHECK, PROVEN TO FIRE.
        //
        // A prediction with no traffic behind it. Nothing mismatches, nothing errors, nothing
        // is reported -- which is exactly how a transaction that never happened passes a
        // suite. The only thing that finds it is the depth at the end of the test.
        // ============================================================
        b_im = i_match; b_iv = i_val; b_is = i_shape; b_cm = c_match;
        @(negedge clk);
        extra_pred = 1'b1;
        @(negedge clk);
        extra_pred = 1'b0;
        repeat (20) @(negedge clk);

        $display("  a prediction with no traffic behind it:");
        $display("    new mismatches of any kind ................. %0d",
                 (i_val - b_iv) + (i_shape - b_is));
        $display("    new matches ................................ %0d", i_match - b_im);
        $display("    predictions left queued (the depth) ........ %0d", i_depth);

        if (((i_val - b_iv) + (i_shape - b_is)) != 0 || (i_match - b_im) != 0) begin
            $display("  FAIL: the missing transaction produced a mismatch, which means this phase is not demonstrating the silent case");
            errors = errors + 1;
        end
        if (i_depth == 0) begin
            $display("  FAIL: a prediction was issued with no observation and the depth is still zero, so the end-of-test emptiness check cannot detect a missing transaction");
            errors = errors + 1;
        end
        $display("    4. the missing transaction produced NO mismatch, NO error and NO output of any kind -- and left the depth at %0d. That is the whole failure mode: a transaction that never happened cannot be compared against anything, so every check stays quiet and the suite passes. The end-of-test emptiness assertion is the only thing that finds it, and this phase exists so that the assertion is one that has been SEEN to fire rather than one that has always been silent",
                 i_depth);

        if (errors == 0)
            $display("PASS: a reference model is only as good as its independence, and independence is a question about PROVENANCE rather than about code quality. Two predictions of the same traffic were compared against the same monitor by the same scoreboard module: one computed from the TRANSACTION -- data and width, knowing nothing of CPOL, CPHA, bit order, leads, half periods or the existence of edges -- and one produced by a SECOND COPY OF THE DESIGN driving its own pins. On %0d legal transactions both reported a perfectly clean run and both ended empty, which is the explanation rather than a weak result: a copy is indistinguishable from a good model until the design is wrong, which is exactly when a model is supposed to earn its keep. With a launch/capture swap injected, the independent model reported a value mismatch on every transaction and the copy reported a CLEAN RUN on every transaction -- not silence, but a written-down expectation, compared, and recorded as a pass, because the copy carries the same fault and two wrongs compare equal. That is what a model validated against the RTL until it agreed becomes, and its source will never look like a copy. The simplicity of the independent model is the evidence for it: had it needed CPHA, it would have been re-deriving the driver's edge arithmetic, and two derivations of one arithmetic share their mistakes. Then a frame with its final edge dropped in CPHA=0 compared EXACTLY EQUAL on payload -- no bit is lost when the dropped edge is a trailing edge and the capture edge is the leading one -- and was caught only by a shape check that needs NO prediction at all, which is also the only kind of check a copy-based model cannot corrupt. And a prediction with no traffic behind it produced no mismatch, no error and no output whatsoever, leaving %0d queued: a transaction that never happened cannot be compared against anything, every check stays quiet, and only the end-of-test emptiness assertion finds it -- which is why this bench makes that assertion fire on purpose instead of trusting one that has always been silent",
                     legal_txns, i_depth);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_ref_scoreboard_tb.v — the same bench in Verilog-2001
// spi_ref_scoreboard_tb.v
//
// TWO PREDICTIONS OF THE SAME TRAFFIC, ONE SCOREBOARD EACH, AND THE ONLY DIFFERENCE IS WHERE
// THE PREDICTION CAME FROM.
//
//   sb_indep  is fed by `spi_ref_model` -- a prediction computed from the TRANSACTION. It
//             knows the data and the width. It does not know CPOL, CPHA, the bit order, the
//             lead, the half period, or that SPI has edges.
//
//   sb_copy   is fed by a SECOND COPY OF THE DRIVER, wired to its own pins and watched by its
//             own monitor. Its prediction is literally "what this design does". This is the
//             single most common way a reference model goes wrong in real projects, and it
//             almost never looks like this in the source: it looks like a model that was
//             "validated against the RTL" until they agreed, or a model whose author fixed it
//             every time it disagreed until it stopped disagreeing. The end state is the same
//             object, and the twin here is its honest form.
//
// Both scoreboards pass on a correct design. That is why a copy survives review.
//
// THEN A FAULT IS INJECTED, and the results separate completely:
//
//   sb_indep  reports a value mismatch on every transaction. The bug is caught.
//   sb_copy   reports a clean run. The twin has the same fault, so it predicts the same wrong
//             word, and two wrongs compare equal.
//
// A reference model derived from the implementation predicts the implementation. It cannot do
// anything else, and the failure is not that it is silent -- it is that it AGREES, confidently,
// in writing, on every transaction.
//
// TWO MORE THINGS THIS BENCH MEASURES, because "the copy misses a bug" is only the headline.
//
//   A PAYLOAD-ONLY SCOREBOARD MISSES A PROTOCOL VIOLATION ENTIRELY. With the frame's final
//   edge dropped in CPHA=0, every captured bit still arrives -- the dropped edge is a trailing
//   edge and CPHA=0 captures on leading ones -- so the payload compares EXACTLY EQUAL and the
//   bit count is right. Only the partial-frame flag, which needs no prediction at all, says
//   anything. The bench counts these separately from value mismatches, and the count is the
//   argument for having shape checks at all.
//
//   A TEST THAT ENDS WITH PREDICTIONS STILL QUEUED HAS SILENTLY PASSED. A transaction that
//   never happened produces no mismatch, no error, and no output. The only thing that finds it
//   is an end-of-test emptiness check -- and that check has to be proven to fire, so the last
//   phase of this bench issues a prediction with no traffic behind it on purpose and requires
//   the depth to be non-zero.

`timescale 1ns/1ps

module spi_ref_scoreboard_tb;

    localparam LEAD  = 4;
    localparam HALF  = 3;
    localparam LAG   = 2;
    localparam GAP   = 3;
    localparam DW    = 32;
    localparam LEN_W = 6;
    localparam CNT_W = 10;
    localparam SCNT  = 16;

    localparam F_NONE = 0, F_PHASE = 3, F_TRUNC = 4;

    reg clk;
    always #5 clk = ~clk;
    reg rst_n;

    reg              start;
    reg  [DW-1:0]    tx_data;
    reg  [LEN_W-1:0] nbits;
    reg              cpol, cpha, lsb_first;
    reg  [2:0]       fault;

    // ------------------------------------------------------------------
    // THE DESIGN UNDER TEST.
    // ------------------------------------------------------------------
    wire busy, done;
    wire [DW-1:0] drv_rx;
    wire sclk, cs_n, mosi;
    wire miso = ~mosi;

    spi_driver #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
                 .DW(DW), .LEN_W(LEN_W), .CNT_W(16)) u_drv (
        .clk(clk), .rst_n(rst_n),
        .start(start), .tx_data(tx_data), .nbits(nbits),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
        .busy(busy), .done(done), .rx_data(drv_rx),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso)
    );

    wire              o_valid, o_partial;
    wire [DW-1:0]     o_mosi, o_miso;
    wire [LEN_W:0]    o_nbits;
    wire [CNT_W-1:0]  o_edges;

    spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_mon (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .len(nbits),
        .t_valid(o_valid), .t_mosi(o_mosi), .t_miso(o_miso),
        .t_nbits(o_nbits), .t_edges(o_edges), .t_partial(o_partial)
    );

    // ------------------------------------------------------------------
    // THE COPY. Same inputs, its own pins, its own monitor. Nothing about it is a strawman:
    // it is the design, and that is the entire problem with it.
    // ------------------------------------------------------------------
    wire tw_busy, tw_done;
    wire [DW-1:0] tw_rx;
    wire tw_sclk, tw_cs_n, tw_mosi;
    wire tw_miso = ~tw_mosi;

    spi_driver #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
                 .DW(DW), .LEN_W(LEN_W), .CNT_W(16)) u_drv_twin (
        .clk(clk), .rst_n(rst_n),
        .start(start), .tx_data(tx_data), .nbits(nbits),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
        .busy(tw_busy), .done(tw_done), .rx_data(tw_rx),
        .sclk(tw_sclk), .cs_n(tw_cs_n), .mosi(tw_mosi), .miso(tw_miso)
    );

    wire              c_valid, c_partial;
    wire [DW-1:0]     c_mosi, c_miso;
    wire [LEN_W:0]    c_nbits;
    wire [CNT_W-1:0]  c_edges;

    spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_mon_twin (
        .clk(clk), .rst_n(rst_n),
        .sclk(tw_sclk), .cs_n(tw_cs_n), .mosi(tw_mosi), .miso(tw_miso),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .len(nbits),
        .t_valid(c_valid), .t_mosi(c_mosi), .t_miso(c_miso),
        .t_nbits(c_nbits), .t_edges(c_edges), .t_partial(c_partial)
    );

    // ------------------------------------------------------------------
    // THE INDEPENDENT MODEL.
    //
    // It is told THAT a transaction was issued and WHAT it asked for. `busy` rising is the
    // handshake, which is the same information a sequencer has -- not introspection. The
    // model is given no signal that describes how the driver intends to send anything.
    // ------------------------------------------------------------------
    reg busy_d;
    always @(posedge clk) if (rst_n) busy_d <= busy; else busy_d <= 1'b0;
    wire req_valid = busy & ~busy_d;

    // A prediction the bench can inject with no traffic behind it, to prove the end-of-test
    // emptiness check actually fires.
    reg extra_pred;

    wire              p_valid;
    wire [DW-1:0]     p_mosi, p_miso;
    wire [LEN_W:0]    p_nbits;

    spi_ref_model #(.DW(DW), .LEN_W(LEN_W)) u_ref (
        .clk(clk), .rst_n(rst_n),
        .req_valid(req_valid | extra_pred), .req_data(tx_data), .req_nbits(nbits),
        .pred_valid(p_valid), .pred_mosi(p_mosi), .pred_miso(p_miso), .pred_nbits(p_nbits)
    );

    // ------------------------------------------------------------------
    // TWO SCOREBOARDS. Identical module, identical observations, different predictions.
    // ------------------------------------------------------------------
    wire [15:0] i_match, i_val, i_shape, i_unexp, i_ovf, i_depth;
    wire [15:0] c_match, c_val, c_shape, c_unexp, c_ovf, c_depth;

    spi_scoreboard #(.DW(DW), .LEN_W(LEN_W), .DEPTH(SCNT), .CNT_W(16)) sb_indep (
        .clk(clk), .rst_n(rst_n),
        .pred_valid(p_valid), .pred_mosi(p_mosi), .pred_miso(p_miso), .pred_nbits(p_nbits),
        .obs_valid(o_valid), .obs_mosi(o_mosi), .obs_miso(o_miso), .obs_nbits(o_nbits),
        .obs_partial(o_partial),
        .n_match(i_match), .n_val_bad(i_val), .n_shape_bad(i_shape),
        .n_unexpected(i_unexp), .n_overflow(i_ovf), .depth(i_depth)
    );

    spi_scoreboard #(.DW(DW), .LEN_W(LEN_W), .DEPTH(SCNT), .CNT_W(16)) sb_copy (
        .clk(clk), .rst_n(rst_n),
        .pred_valid(c_valid), .pred_mosi(c_mosi), .pred_miso(c_miso), .pred_nbits(c_nbits),
        .obs_valid(o_valid), .obs_mosi(o_mosi), .obs_miso(o_miso), .obs_nbits(o_nbits),
        .obs_partial(1'b0),   // the copy is judged on its PREDICTION only, so that the one
                              // check needing no prediction cannot flatter it
        .n_match(c_match), .n_val_bad(c_val), .n_shape_bad(c_shape),
        .n_unexpected(c_unexp), .n_overflow(c_ovf), .depth(c_depth)
    );

    integer errors;

    initial begin
        #400_000;
        $display("FAIL: the simulation did not finish within its time limit");
        $finish;
    end

        task set_cfg;
        input [LEN_W-1:0] n;
        input integer pol;
        input integer pha;
        input integer lsb;
        input [2:0] f;
        input [DW-1:0] d;
        begin
            @(negedge clk);
            nbits = n; cpol = pol[0]; cpha = pha[0]; lsb_first = lsb[0];
            fault = f; tx_data = d;
            repeat (6) @(negedge clk);
        end
    endtask

        task run_burst;
        input integer ntxn;
        integer k;
        begin
            k = 0;
            @(negedge clk);
            start = 1'b1;
            while (k < ntxn) begin
                @(negedge clk);
                if (done) begin
                    k = k + 1;
                    if (k == ntxn) start = 1'b0;
                end
            end
            repeat (GAP + LAG + 8) @(negedge clk);
        end
    endtask

    integer iw, ipol, ipha, ilsb;
    reg [LEN_W-1:0] w;
    integer b_im, b_iv, b_is, b_cm, b_cv, b_cs;
    integer legal_txns, legal_cfgs;
    integer ph_txns;

    initial begin
        rst_n = 1'b1;
        repeat (2) @(negedge clk);
        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (4) @(negedge clk);

        // ============================================================
        // 1. BOTH SCOREBOARDS PASS ON A CORRECT DESIGN.
        //    This is the measurement that explains why a copy survives review.
        // ============================================================
        legal_cfgs = 0;
        for (iw = 0; iw < 2; iw = iw + 1) begin
            w = (iw == 0) ? 6'd8 : 6'd13;
            for (ipol = 0; ipol < 2; ipol = ipol + 1)
            for (ipha = 0; ipha < 2; ipha = ipha + 1)
            for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
                set_cfg(w, ipol, ipha, ilsb, F_NONE[2:0],
                        (iw == 0) ? 32'h0000_1A5C : 32'h0000_0C3A);
                run_burst(2);
                legal_cfgs = legal_cfgs + 1;
            end
        end
        legal_txns = i_match + i_val + i_shape;

        $display("  legal traffic: %0d transactions in %0d configurations", legal_txns, legal_cfgs);
        $display("  scoreboard   matched  value bad  shape bad  unexpected  overflow  depth");
        $display("  independent  %7d  %9d  %9d  %10d  %8d  %5d   prediction from the transaction",
                 i_match, i_val, i_shape, i_unexp, i_ovf, i_depth);
        $display("  copy         %7d  %9d  %9d  %10d  %8d  %5d   prediction from a second copy of the design",
                 c_match, c_val, c_shape, c_unexp, c_ovf, c_depth);

        if (legal_txns == 0) begin
            $display("  FAIL: no transactions reached the scoreboards at all, so nothing below means anything");
            errors = errors + 1;
        end
        if (i_match != legal_txns || i_val != 0 || i_shape != 0 || i_unexp != 0 || i_ovf != 0) begin
            $display("  FAIL: the independent scoreboard did not report a clean run on legal traffic");
            errors = errors + 1;
        end
        if (c_match != legal_txns || c_val != 0 || c_shape != 0 || c_unexp != 0 || c_ovf != 0) begin
            $display("  FAIL: the copy scoreboard did not report a clean run on legal traffic");
            errors = errors + 1;
        end
        if (i_depth != 0 || c_depth != 0) begin
            $display("  FAIL: a scoreboard ended the legal phase with %0d and %0d predictions still queued",
                     i_depth, c_depth);
            errors = errors + 1;
        end
        $display("    1. BOTH scoreboards reported a perfectly clean run on all %0d transactions, and both ended empty. That is not a weak result, it is the explanation: a reference model that is a copy of the design is indistinguishable from a good one until the design is wrong, which is precisely when a reference model is supposed to earn its keep",
                 legal_txns);

        // ============================================================
        // 2. THE FAULT. One design, two predictions, two different verdicts.
        // ============================================================
        b_im = i_match; b_iv = i_val; b_is = i_shape;
        b_cm = c_match; b_cv = c_val; b_cs = c_shape;

        for (ipol = 0; ipol < 2; ipol = ipol + 1)
        for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
            // CPHA=1, where the launch/capture swap moves the data by a bit position and is
            // therefore visible to a value comparison at all. Chapter 16.5 measured why the
            // other phase is not.
            set_cfg(6'd8, ipol, 1, ilsb, F_PHASE[2:0], 32'h0000_1A5C);
            run_burst(2);
        end
        ph_txns = (i_match - b_im) + (i_val - b_iv) + (i_shape - b_is);

        $display("  the launch/capture swap, %0d transactions:", ph_txns);
        $display("  scoreboard   matched  value bad");
        $display("  independent  %7d  %9d", i_match - b_im, i_val - b_iv);
        $display("  copy         %7d  %9d", c_match - b_cm, c_val - b_cv);

        if ((i_val - b_iv) != ph_txns) begin
            $display("  FAIL: the independent model agreed with the faulted design on %0d of %0d transactions; a prediction computed from the transaction cannot agree with a shifted word",
                     i_match - b_im, ph_txns);
            errors = errors + 1;
        end
        if ((c_match - b_cm) != ph_txns || (c_val - b_cv) != 0) begin
            $display("  FAIL: the copy model did NOT agree with the faulted design on every transaction (%0d matched, %0d value mismatches of %0d), so it is not actually a copy and the comparison proves nothing",
                     c_match - b_cm, c_val - b_cv, ph_txns);
            errors = errors + 1;
        end
        $display("    2. the independent model reported a value mismatch on ALL %0d faulted transactions and the copy reported a CLEAN RUN on all %0d. Same design, same monitor, same instant -- only the source of the prediction differs. The copy has the same fault, so it predicts the same wrong word, and two wrongs compare equal",
                 i_val - b_iv, c_match - b_cm);
        $display("       and the copy's failure is not silence. It wrote down an expected value, compared it, and recorded a PASS, on every transaction, with total confidence. A model validated against the RTL until they agreed ends up as exactly this object, and its source will not look like this file");

        // ============================================================
        // 3. A PAYLOAD-ONLY SCOREBOARD MISSES A PROTOCOL VIOLATION.
        // ============================================================
        b_im = i_match; b_iv = i_val; b_is = i_shape;
        set_cfg(6'd8, 0, 0, 0, F_TRUNC[2:0], 32'h0000_1A5C);
        run_burst(3);
        $display("  a frame with its final edge dropped, in CPHA=0, %0d transactions:",
                 (i_match - b_im) + (i_val - b_iv) + (i_shape - b_is));
        $display("    value mismatches ........................... %0d", i_val - b_iv);
        $display("    payload matched, SHAPE wrong ............... %0d", i_shape - b_is);
        $display("    reported as clean .......................... %0d", i_match - b_im);

        if ((i_shape - b_is) == 0) begin
            $display("  FAIL: no transaction was caught by the shape check, so this stimulus does not demonstrate the point");
            errors = errors + 1;
        end
        if ((i_match - b_im) != 0) begin
            $display("  FAIL: %0d truncated frames were reported clean by the independent scoreboard",
                     i_match - b_im);
            errors = errors + 1;
        end
        $display("    3. every truncated frame was caught, and %0d of them were caught ONLY by the shape check: the payload compared EXACTLY EQUAL. In CPHA=0 the dropped edge is a trailing edge and CPHA=0 captures on leading ones, so no bit is lost, the count is right, and the word is perfect -- on a frame the protocol forbids. A payload-only scoreboard reports a clean run here, and the check that catches it needs NO PREDICTION AT ALL, which is why it is also the one check a copy-based model cannot corrupt",
                 i_shape - b_is);

        // ============================================================
        // 4. THE EMPTINESS CHECK, PROVEN TO FIRE.
        //
        // A prediction with no traffic behind it. Nothing mismatches, nothing errors, nothing
        // is reported -- which is exactly how a transaction that never happened passes a
        // suite. The only thing that finds it is the depth at the end of the test.
        // ============================================================
        b_im = i_match; b_iv = i_val; b_is = i_shape; b_cm = c_match;
        @(negedge clk);
        extra_pred = 1'b1;
        @(negedge clk);
        extra_pred = 1'b0;
        repeat (20) @(negedge clk);

        $display("  a prediction with no traffic behind it:");
        $display("    new mismatches of any kind ................. %0d",
                 (i_val - b_iv) + (i_shape - b_is));
        $display("    new matches ................................ %0d", i_match - b_im);
        $display("    predictions left queued (the depth) ........ %0d", i_depth);

        if (((i_val - b_iv) + (i_shape - b_is)) != 0 || (i_match - b_im) != 0) begin
            $display("  FAIL: the missing transaction produced a mismatch, which means this phase is not demonstrating the silent case");
            errors = errors + 1;
        end
        if (i_depth == 0) begin
            $display("  FAIL: a prediction was issued with no observation and the depth is still zero, so the end-of-test emptiness check cannot detect a missing transaction");
            errors = errors + 1;
        end
        $display("    4. the missing transaction produced NO mismatch, NO error and NO output of any kind -- and left the depth at %0d. That is the whole failure mode: a transaction that never happened cannot be compared against anything, so every check stays quiet and the suite passes. The end-of-test emptiness assertion is the only thing that finds it, and this phase exists so that the assertion is one that has been SEEN to fire rather than one that has always been silent",
                 i_depth);

        if (errors == 0)
            $display("PASS: a reference model is only as good as its independence, and independence is a question about PROVENANCE rather than about code quality. Two predictions of the same traffic were compared against the same monitor by the same scoreboard module: one computed from the TRANSACTION -- data and width, knowing nothing of CPOL, CPHA, bit order, leads, half periods or the existence of edges -- and one produced by a SECOND COPY OF THE DESIGN driving its own pins. On %0d legal transactions both reported a perfectly clean run and both ended empty, which is the explanation rather than a weak result: a copy is indistinguishable from a good model until the design is wrong, which is exactly when a model is supposed to earn its keep. With a launch/capture swap injected, the independent model reported a value mismatch on every transaction and the copy reported a CLEAN RUN on every transaction -- not silence, but a written-down expectation, compared, and recorded as a pass, because the copy carries the same fault and two wrongs compare equal. That is what a model validated against the RTL until it agreed becomes, and its source will never look like a copy. The simplicity of the independent model is the evidence for it: had it needed CPHA, it would have been re-deriving the driver's edge arithmetic, and two derivations of one arithmetic share their mistakes. Then a frame with its final edge dropped in CPHA=0 compared EXACTLY EQUAL on payload -- no bit is lost when the dropped edge is a trailing edge and the capture edge is the leading one -- and was caught only by a shape check that needs NO prediction at all, which is also the only kind of check a copy-based model cannot corrupt. And a prediction with no traffic behind it produced no mismatch, no error and no output whatsoever, leaving %0d queued: a transaction that never happened cannot be compared against anything, every check stays quiet, and only the end-of-test emptiness assertion finds it -- which is why this bench makes that assertion fire on purpose instead of trusting one that has always been silent",
                     legal_txns, i_depth);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        cpol = 1'b0;
        cpha = 1'b0;
        lsb_first = 1'b0;
        clk = 1'b0;
        rst_n = 1'b1;
        start = 1'b0;
        tx_data = {DW{1'b0}};
        nbits = 6'd8;
        fault = 3'd0;
        busy_d = 1'b0;
        extra_pred = 1'b0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_ref_scoreboard_tb.vhd — the same bench in VHDL
-- spi_ref_scoreboard_tb.vhd
--
-- TWO PREDICTIONS OF THE SAME TRAFFIC, ONE SCOREBOARD EACH, AND THE ONLY DIFFERENCE IS WHERE
-- THE PREDICTION CAME FROM.
--
--   sb_indep  is fed by `spi_ref_model` -- a prediction computed from the TRANSACTION. It knows
--             the data and the width. It does not know CPOL, CPHA, the bit order, the lead, the
--             half period, or that SPI has edges.
--
--   sb_copy   is fed by a SECOND COPY OF THE DRIVER, wired to its own pins and watched by its
--             own monitor. Its prediction is literally "what this design does". This is the
--             most common way a reference model goes wrong in real projects, and it almost
--             never looks like this in the source: it looks like a model that was "validated
--             against the RTL" until they agreed, or one whose author fixed it every time it
--             disagreed until it stopped disagreeing. The end state is the same object, and the
--             twin here is its honest form.
--
-- Both scoreboards pass on a correct design. That is why a copy survives review.
--
-- THEN A FAULT IS INJECTED and the results separate completely: the independent model reports a
-- value mismatch on every transaction, and the copy reports a clean run -- the twin has the same
-- fault, predicts the same wrong word, and two wrongs compare equal. The failure is not silence:
-- it is a written-down expectation, compared, and recorded as a PASS.
--
-- TWO MORE THINGS THIS BENCH MEASURES.
--
--   A PAYLOAD-ONLY SCOREBOARD MISSES A PROTOCOL VIOLATION ENTIRELY. With the frame's final edge
--   dropped in CPHA=0, every captured bit still arrives -- the dropped edge is a trailing edge
--   and CPHA=0 captures on leading ones -- so the payload compares EXACTLY EQUAL and the bit
--   count is right. Only the partial-frame flag, which needs no prediction at all, says anything.
--
--   A TEST THAT ENDS WITH PREDICTIONS STILL QUEUED HAS SILENTLY PASSED. A transaction that never
--   happened produces no mismatch, no error and no output. The only thing that finds it is an
--   end-of-test emptiness check, so the last phase issues a prediction with no traffic behind it
--   on purpose and requires the depth to be non-zero.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_driver_pkg.all;
use work.spi_txn_pkg.all;
use work.spi_sb_pkg.all;

entity spi_ref_scoreboard_tb is
end entity spi_ref_scoreboard_tb;

architecture tb of spi_ref_scoreboard_tb is

    constant LEAD_C : natural := 4;
    constant HALF_C : natural := 3;
    constant LAG_C  : natural := 2;
    constant GAP_C  : natural := 3;
    constant HALF_T : time    := 5 ns;

    signal clk      : std_logic := '0';
    signal rst_n    : std_logic := '1';
    signal done_sim : boolean   := false;

    signal start : std_logic := '0';
    signal req   : spi_req_t := (data      => (others => '0'),
                                 nbits     => to_unsigned(8, LEN_W),
                                 cpol      => '0',
                                 cpha      => '0',
                                 lsb_first => '0',
                                 fault     => F_NONE);

    -- The design under test.
    signal busy, done       : std_logic;
    signal drv_rx           : std_logic_vector(DW - 1 downto 0);
    signal sclk, cs_n, mosi : std_logic;
    signal miso             : std_logic;
    signal obs              : spi_obs_t;

    -- The copy. Same inputs, its own pins, its own monitor. Nothing about it is a strawman: it
    -- IS the design, and that is the entire problem with it.
    signal tw_busy, tw_done          : std_logic;
    signal tw_rx                     : std_logic_vector(DW - 1 downto 0);
    signal tw_sclk, tw_cs_n, tw_mosi : std_logic;
    signal tw_miso                   : std_logic;
    signal obs_copy                  : spi_obs_t;

    -- The independent model is told THAT a transaction was issued and WHAT it asked for.
    -- `busy` rising is the handshake -- the same information a sequencer has, not introspection.
    signal busy_d     : std_logic := '0';
    signal req_valid  : std_logic;
    signal extra_pred : std_logic := '0';

    signal p_valid : std_logic;
    signal pred    : spi_pred_t;

    signal ci, cc : sb_counts_t;

    signal errors : integer := 0;

    -- The monitor's observation and the scoreboard's prediction are different record types --
    -- the first carries an edge count and a partial flag that a prediction has no business
    -- having. One conversion, in one place.
    function to_pred (o : spi_obs_t) return spi_pred_t is
    begin
        return (mosi => o.mosi, miso => o.miso, nbits => o.nbits);
    end function to_pred;

begin

    miso    <= not mosi;
    tw_miso <= not tw_mosi;

    clk_gen : process is
    begin
        while not done_sim loop
            wait for HALF_T;
            clk <= not clk;
        end loop;
        wait;
    end process clk_gen;

    busy_dly : process (clk) is
    begin
        if rising_edge(clk) then
            if rst_n = '0' then busy_d <= '0'; else busy_d <= busy; end if;
        end if;
    end process busy_dly;

    req_valid <= (busy and not busy_d) or extra_pred;

    dut : entity work.spi_driver
        generic map (LEAD => LEAD_C, HALF => HALF_C, LAG => LAG_C, GAP => GAP_C)
        port map (clk => clk, rst_n => rst_n, start => start, req => req,
                  busy => busy, done => done, rx_data => drv_rx,
                  sclk => sclk, cs_n => cs_n, mosi => mosi, miso => miso);

    u_mon : entity work.spi_txn_monitor
        port map (clk => clk, rst_n => rst_n,
                  sclk => sclk, cs_n => cs_n, mosi => mosi, miso => miso,
                  cpol => req.cpol, cpha => req.cpha, lsb_first => req.lsb_first,
                  len => req.nbits, obs => obs);

    twin : entity work.spi_driver
        generic map (LEAD => LEAD_C, HALF => HALF_C, LAG => LAG_C, GAP => GAP_C)
        port map (clk => clk, rst_n => rst_n, start => start, req => req,
                  busy => tw_busy, done => tw_done, rx_data => tw_rx,
                  sclk => tw_sclk, cs_n => tw_cs_n, mosi => tw_mosi, miso => tw_miso);

    u_mon_twin : entity work.spi_txn_monitor
        port map (clk => clk, rst_n => rst_n,
                  sclk => tw_sclk, cs_n => tw_cs_n, mosi => tw_mosi, miso => tw_miso,
                  cpol => req.cpol, cpha => req.cpha, lsb_first => req.lsb_first,
                  len => req.nbits, obs => obs_copy);

    u_ref : entity work.spi_ref_model
        port map (clk => clk, rst_n => rst_n,
                  req_valid => req_valid, req_data => req.data, req_nbits => req.nbits,
                  pred_valid => p_valid, pred => pred);

    sb_indep : entity work.spi_scoreboard
        generic map (DEPTH => 16)
        port map (clk => clk, rst_n => rst_n,
                  pred_valid => p_valid, pred => pred,
                  obs_valid => obs.valid, obs => to_pred(obs),
                  obs_partial => obs.partial,
                  counts => ci);

    sb_copy : entity work.spi_scoreboard
        generic map (DEPTH => 16)
        port map (clk => clk, rst_n => rst_n,
                  pred_valid => obs_copy.valid, pred => to_pred(obs_copy),
                  obs_valid => obs.valid, obs => to_pred(obs),
                  -- the copy is judged on its PREDICTION only, so that the one check needing no
                  -- prediction cannot flatter it
                  obs_partial => '0',
                  counts => cc);

    main : process is

        procedure set_cfg (n : natural; pol : std_logic; pha : std_logic; lsb : std_logic;
                           f : spi_fault_t; d : std_logic_vector(DW - 1 downto 0)) is
        begin
            wait until falling_edge(clk);
            req <= (data => d, nbits => to_unsigned(n, LEN_W),
                    cpol => pol, cpha => pha, lsb_first => lsb, fault => f);
            for i in 0 to 5 loop wait until falling_edge(clk); end loop;
        end procedure set_cfg;

        procedure run_burst (ntxn : natural) is
            variable k : natural := 0;
        begin
            k := 0;
            wait until falling_edge(clk);
            start <= '1';
            while k < ntxn loop
                wait until falling_edge(clk);
                if done = '1' then
                    k := k + 1;
                    if k = ntxn then start <= '0'; end if;
                end if;
            end loop;
            for i in 0 to GAP_C + LAG_C + 7 loop wait until falling_edge(clk); end loop;
        end procedure run_burst;

        constant PAT_A : std_logic_vector(DW - 1 downto 0) := x"00001A5C";
        constant PAT_B : std_logic_vector(DW - 1 downto 0) := x"00000C3A";

        variable w                             : natural;
        variable legal_txns, legal_cfgs        : integer := 0;
        variable b_im, b_iv, b_is, b_cm, b_cv  : integer;
        variable ph_txns                       : integer;
        variable pol, pha, lsb                 : std_logic;

    begin
        rst_n <= '1';
        for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        rst_n <= '0';
        for i in 0 to 3 loop wait until falling_edge(clk); end loop;
        rst_n <= '1';
        for i in 0 to 3 loop wait until falling_edge(clk); end loop;

        -- ==============================================================
        -- 1. BOTH SCOREBOARDS PASS ON A CORRECT DESIGN.
        --    This is the measurement that explains why a copy survives review.
        -- ==============================================================
        for iw in 0 to 1 loop
            if iw = 0 then w := 8; else w := 13; end if;
            for ipol in 0 to 1 loop
                for ipha in 0 to 1 loop
                    for ilsb in 0 to 1 loop
                        if ipol = 0 then pol := '0'; else pol := '1'; end if;
                        if ipha = 0 then pha := '0'; else pha := '1'; end if;
                        if ilsb = 0 then lsb := '0'; else lsb := '1'; end if;
                        if iw = 0 then
                            set_cfg(w, pol, pha, lsb, F_NONE, PAT_A);
                        else
                            set_cfg(w, pol, pha, lsb, F_NONE, PAT_B);
                        end if;
                        run_burst(2);
                        legal_cfgs := legal_cfgs + 1;
                    end loop;
                end loop;
            end loop;
        end loop;
        legal_txns := ci.matched + ci.value_bad + ci.shape_bad;

        report "  legal traffic: " & integer'image(legal_txns) & " transactions in " &
               integer'image(legal_cfgs) & " configurations";
        report "  scoreboard   matched  value bad  shape bad  unexpected  overflow  depth";
        report "  independent  " & integer'image(ci.matched) & "  " &
               integer'image(ci.value_bad) & "  " & integer'image(ci.shape_bad) & "  " &
               integer'image(ci.unexpected) & "  " & integer'image(ci.overflow) & "  " &
               integer'image(ci.depth) & "   prediction from the transaction";
        report "  copy         " & integer'image(cc.matched) & "  " &
               integer'image(cc.value_bad) & "  " & integer'image(cc.shape_bad) & "  " &
               integer'image(cc.unexpected) & "  " & integer'image(cc.overflow) & "  " &
               integer'image(cc.depth) & "   prediction from a second copy of the design";

        if legal_txns = 0 then
            report "  FAIL: no transactions reached the scoreboards at all, so nothing below means anything";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if ci.matched /= legal_txns or ci.value_bad /= 0 or ci.shape_bad /= 0
           or ci.unexpected /= 0 or ci.overflow /= 0 then
            report "  FAIL: the independent scoreboard did not report a clean run on legal traffic";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if cc.matched /= legal_txns or cc.value_bad /= 0 or cc.shape_bad /= 0
           or cc.unexpected /= 0 or cc.overflow /= 0 then
            report "  FAIL: the copy scoreboard did not report a clean run on legal traffic";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if ci.depth /= 0 or cc.depth /= 0 then
            report "  FAIL: a scoreboard ended the legal phase with predictions still queued";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    1. BOTH scoreboards reported a perfectly clean run on all " &
               integer'image(legal_txns) &
               " transactions, and both ended empty. That is not a weak result, it is the explanation: a reference model that is a copy of the design is indistinguishable from a good one until the design is wrong, which is precisely when a reference model is supposed to earn its keep";

        -- ==============================================================
        -- 2. THE FAULT. One design, two predictions, two different verdicts.
        -- ==============================================================
        b_im := ci.matched; b_iv := ci.value_bad; b_is := ci.shape_bad;
        b_cm := cc.matched; b_cv := cc.value_bad;

        for ipol in 0 to 1 loop
            for ilsb in 0 to 1 loop
                if ipol = 0 then pol := '0'; else pol := '1'; end if;
                if ilsb = 0 then lsb := '0'; else lsb := '1'; end if;
                -- CPHA=1, where the launch/capture swap moves the data by a bit position and is
                -- therefore visible to a value comparison at all. Chapter 16.5 measured why the
                -- other phase is not.
                set_cfg(8, pol, '1', lsb, F_PHASE, PAT_A);
                run_burst(2);
            end loop;
        end loop;
        ph_txns := (ci.matched - b_im) + (ci.value_bad - b_iv) + (ci.shape_bad - b_is);

        report "  the launch/capture swap, " & integer'image(ph_txns) & " transactions:";
        report "  scoreboard   matched  value bad";
        report "  independent  " & integer'image(ci.matched - b_im) & "  " &
               integer'image(ci.value_bad - b_iv);
        report "  copy         " & integer'image(cc.matched - b_cm) & "  " &
               integer'image(cc.value_bad - b_cv);

        if (ci.value_bad - b_iv) /= ph_txns then
            report "  FAIL: the independent model agreed with the faulted design on " &
                   integer'image(ci.matched - b_im) & " of " & integer'image(ph_txns) &
                   " transactions; a prediction computed from the transaction cannot agree with a shifted word";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if (cc.matched - b_cm) /= ph_txns or (cc.value_bad - b_cv) /= 0 then
            report "  FAIL: the copy model did NOT agree with the faulted design on every transaction, so it is not actually a copy and the comparison proves nothing";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    2. the independent model reported a value mismatch on ALL " &
               integer'image(ci.value_bad - b_iv) &
               " faulted transactions and the copy reported a CLEAN RUN on all " &
               integer'image(cc.matched - b_cm) &
               ". Same design, same monitor, same instant -- only the source of the prediction differs. The copy has the same fault, so it predicts the same wrong word, and two wrongs compare equal";
        report "       and the copy's failure is not silence. It wrote down an expected value, compared it, and recorded a PASS, on every transaction, with total confidence. A model validated against the RTL until they agreed ends up as exactly this object, and its source will not look like this file";

        -- ==============================================================
        -- 3. A PAYLOAD-ONLY SCOREBOARD MISSES A PROTOCOL VIOLATION.
        -- ==============================================================
        b_im := ci.matched; b_iv := ci.value_bad; b_is := ci.shape_bad;
        set_cfg(8, '0', '0', '0', F_TRUNC, PAT_A);
        run_burst(3);
        report "  a frame with its final edge dropped, in CPHA=0, " &
               integer'image((ci.matched - b_im) + (ci.value_bad - b_iv) + (ci.shape_bad - b_is)) &
               " transactions:";
        report "    value mismatches ........................... " &
               integer'image(ci.value_bad - b_iv);
        report "    payload matched, SHAPE wrong ............... " &
               integer'image(ci.shape_bad - b_is);
        report "    reported as clean .......................... " &
               integer'image(ci.matched - b_im);

        if (ci.shape_bad - b_is) = 0 then
            report "  FAIL: no transaction was caught by the shape check, so this stimulus does not demonstrate the point";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if (ci.matched - b_im) /= 0 then
            report "  FAIL: truncated frames were reported clean by the independent scoreboard";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    3. every truncated frame was caught, and " &
               integer'image(ci.shape_bad - b_is) &
               " of them were caught ONLY by the shape check: the payload compared EXACTLY EQUAL. In CPHA=0 the dropped edge is a trailing edge and CPHA=0 captures on leading ones, so no bit is lost, the count is right, and the word is perfect -- on a frame the protocol forbids. A payload-only scoreboard reports a clean run here, and the check that catches it needs NO PREDICTION AT ALL, which is why it is also the one check a copy-based model cannot corrupt";

        -- ==============================================================
        -- 4. THE EMPTINESS CHECK, PROVEN TO FIRE.
        -- ==============================================================
        b_im := ci.matched; b_iv := ci.value_bad; b_is := ci.shape_bad;
        wait until falling_edge(clk);
        extra_pred <= '1';
        wait until falling_edge(clk);
        extra_pred <= '0';
        for i in 0 to 19 loop wait until falling_edge(clk); end loop;

        report "  a prediction with no traffic behind it:";
        report "    new mismatches of any kind ................. " &
               integer'image((ci.value_bad - b_iv) + (ci.shape_bad - b_is));
        report "    new matches ................................ " &
               integer'image(ci.matched - b_im);
        report "    predictions left queued (the depth) ........ " & integer'image(ci.depth);

        if ((ci.value_bad - b_iv) + (ci.shape_bad - b_is)) /= 0 or (ci.matched - b_im) /= 0 then
            report "  FAIL: the missing transaction produced a mismatch, which means this phase is not demonstrating the silent case";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if ci.depth = 0 then
            report "  FAIL: a prediction was issued with no observation and the depth is still zero, so the end-of-test emptiness check cannot detect a missing transaction";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    4. the missing transaction produced NO mismatch, NO error and NO output of any kind -- and left the depth at " &
               integer'image(ci.depth) &
               ". That is the whole failure mode: a transaction that never happened cannot be compared against anything, so every check stays quiet and the suite passes. The end-of-test emptiness assertion is the only thing that finds it, and this phase exists so that the assertion is one that has been SEEN to fire rather than one that has always been silent";

        wait for 1 ns;
        if errors = 0 then
            report "PASS: a reference model is only as good as its independence, and independence is a question about PROVENANCE rather than about code quality. Two predictions of the same traffic were compared against the same monitor by the same scoreboard entity: one computed from the TRANSACTION -- data and width, knowing nothing of CPOL, CPHA, bit order, leads, half periods or the existence of edges -- and one produced by a SECOND COPY OF THE DESIGN driving its own pins. On " &
                   integer'image(legal_txns) &
                   " legal transactions both reported a perfectly clean run and both ended empty, which is the explanation rather than a weak result: a copy is indistinguishable from a good model until the design is wrong, which is exactly when a model is supposed to earn its keep. With a launch/capture swap injected, the independent model reported a value mismatch on every transaction and the copy reported a CLEAN RUN on every transaction -- not silence, but a written-down expectation, compared, and recorded as a pass, because the copy carries the same fault and two wrongs compare equal. That is what a model validated against the RTL until it agreed becomes, and its source will never look like a copy. The simplicity of the independent model is the evidence for it: had it needed CPHA, it would have been re-deriving the driver's edge arithmetic, and two derivations of one arithmetic share their mistakes. Then a frame with its final edge dropped in CPHA=0 compared EXACTLY EQUAL on payload -- no bit is lost when the dropped edge is a trailing edge and the capture edge is the leading one -- and was caught only by a shape check that needs NO prediction at all, which is also the only kind of check a copy-based model cannot corrupt. And a prediction with no traffic behind it produced no mismatch, no error and no output whatsoever, leaving " &
                   integer'image(ci.depth) &
                   " queued: a transaction that never happened cannot be compared against anything, every check stays quiet, and only the end-of-test emptiness assertion finds it -- which is why this bench makes that assertion fire on purpose instead of trusting one that has always been silent"
                severity note;
        else
            report "FAIL: " & integer'image(errors) & " error(s)" severity error;
        end if;

        done_sim <= true;
        wait for 100 ns;
        std.env.stop;
    end process main;

end architecture tb;

8. Why a Verification Engineer Cares

Because independence is a question about provenance, and provenance is the one property a code review cannot see.

The practical audit is a single question asked of the reference model: if the design's author had misread the specification in this exact way, would the model have made the same mistake? If the model was written from the same reading, by the same person, or adjusted until it agreed, the answer is yes and the comparison is decorative.

The second habit is about complexity as a warning sign. A reference model that is as complicated as the design is either a second implementation or a copy, and both of them fail in the same direction. The model in this chapter is three lines of arithmetic against a driver of four hundred, and that ratio is the thing to defend when somebody proposes making the model mode-aware to explain a mismatch — because that proposal is how a model becomes a copy, one reasonable-looking commit at a time.

And the third is the emptiness check. Of the four failures in section 2, the missing transaction is the only one with no symptom at all, and the only one whose check has to be deliberately provoked to be trusted.

9. Why an FPGA or ASIC Engineer Cares

Because the payload-only result in section 5 describes a design that ships.

A frame with its final edge dropped produces a perfect payload in CPHA=0. A suite that compares data will pass it, a lab bring-up will pass it, and the slave at the other end will see an SCLK parked at the wrong level between transfers and an odd edge count — which some slaves tolerate and some do not. The rule that catches it is about the shape of the traffic rather than its contents, and shape checks are the ones that get dropped first when a scoreboard is written under time pressure.

The second item is the reference model's mode-independence, read as a design statement: the word is invariant across CPOL, CPHA and bit order. If your design needs the mode to decide what word arrived, one of the two ends has the mapping wrong, and that is worth knowing before tape-out rather than during integration.

10. Failure Signature — A Golden Model That Was Fixed Until It Agreed

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom          a design defect reaches the lab. The suite has a reference
                    model and a scoreboard covering the path, both green on the
                    failing traffic when it is replayed in simulation.

   What happened    the model disagreed with the design eleven times during
                    development. Each disagreement was investigated, the model
                    was found to be "wrong", and it was corrected. One of those
                    eleven was the design being wrong, and the correction
                    encoded the defect into the model.

   What would have  a record of which side changed after each disagreement. A
   caught it        model that has been modified more often than the design has
                    stopped being independent, and the count is the metric.

   The tell         the model's source contains branches that exist only to
                    match the design -- a mode-dependent special case, a
                    comment reading "the RTL does X here", an exception for one
                    width. Every such branch is a place where provenance leaked.

11. Common Misconceptions

"A reference model must model the design." It must predict the design's output. Modelling the design is what a second implementation does, and two implementations written from one understanding share their misunderstandings.

"The model disagreed, so the model is wrong." That is the assumption that turns a model into a copy. The disagreement has a design side, and if the model is simpler than the design — as it should be — then the design side is the more likely one.

"A scoreboard compares data." A scoreboard compares transactions, which have a shape as well as a payload. Three truncated frames in this chapter had exactly correct payloads.

"If a transaction were lost, the scoreboard would report a mismatch." It reports nothing at all. Nothing to compare produces no comparison, so no mismatch, no error, and no output — which is why the depth at the end of the test is the only check that finds it and why that check must be provoked to be trusted.

"Both scoreboards passing on legal traffic means both are fine." It means neither is distinguishable on legal traffic, which is the property the copy relies on. The measurement that separates them requires a broken design, which is the one input a suite cannot produce by itself and which fault injection exists to supply.

"The copy would at least go quiet on a bug." It reports a pass. Positive evidence for broken traffic is worse than silence, because silence looks like a gap and a recorded match looks like proof.

12. Reason It Through

The independent model ignores CPOL, CPHA and the bit order. Why is that a feature rather than an omission?

Because the mode decides how bits travel, not which bits arrive — the same word appears in all four modes and both bit orders. A model that consulted the mode would be re-deriving the driver's edge arithmetic, and a shared derivation shares its mistakes. The model's ignorance of the mode is the evidence that it is not the design.

Both scoreboards reported 32 clean matches. Which of the two results is load-bearing for the chapter's argument?

The copy's. If a copy failed on legal traffic it would be caught in week one and would never reach a project. Its indistinguishability on correct designs is exactly what lets it survive, so the clean result is the explanation rather than a weak finding.

A truncated frame in CPHA=0 had an exactly correct payload. Reconstruct why from the edge roles.

CPHA=0 captures on leading edges. The dropped edge is the final trailing edge, which is not a capture. So every bit is still captured, the bit count is right, and the word is perfect. What changed is the edge count's parity — odd instead of even — which is a statement about the shape of the traffic and not about its contents.

Why does the coincident push-and-pop case bypass the queue instead of pushing and then matching?

Because pushing an entry that is immediately matched against the port leaves that entry in the queue forever. The depth stays right, so nothing looks wrong, but head and tail drift one apart permanently and every later comparison is against the previous transaction's prediction — real mismatches, attributed to the wrong transaction.

Name the one check in this scoreboard that a copy-based reference model cannot corrupt, and say why.

The partial-frame check. It needs no prediction at all, because no correct transaction is ever partial. Every other comparison depends on a model being right; this one depends only on a fact about the protocol, which is why suites should carry as many prediction-free checks as the protocol allows.

13. Understanding Check

14. Summary

A reference model is only as good as its independence, and independence is a question about provenance rather than code quality. Two predictions of the same traffic went through the same scoreboard: one computed from the transaction — data and width, knowing nothing of CPOL, CPHA, bit order, leads, half periods or the existence of edges — and one produced by a second copy of the design driving its own pins. On 32 legal transactions both reported a perfectly clean run and both ended empty, which is the explanation rather than a weak result. With a launch/capture swap injected, the independent model reported a value mismatch on every transaction and the copy recorded a pass on every one, because it carries the same fault and two wrongs compare equal. A frame with its final edge dropped in CPHA=0 compared exactly equal on payload and was caught only by a shape check that needs no prediction at all. And a prediction with no traffic behind it produced no mismatch, no error and no output whatsoever — found only by the end-of-test depth, which is why this bench makes that assertion fire on purpose instead of trusting one that has always been silent.

15. What Comes Next

Four components now exist and none of them is reusable alone, because reusing one means knowing how to connect it. Chapter 16.7 packages them — and measures the difference between an agent that chooses not to drive and one that cannot.

Continue learning