Skip to content
VLSI Mentor

SPI · Module 16

Monitor and Transaction Reconstruction

Two instances of one monitor on one set of pins, differing only in where they got CPHA. The specification's view catches an injected fault; the implementation's view reports the intended word with total confidence, and passivity is measured with a pin hash.

A monitor may read the pins and the protocol's configuration. It may not read the design under test — not its shift register, not its state, not its decision about which edge it launches on.

That rule sounds like an aesthetic preference. This chapter measures what it is actually worth, by building two instances of the same monitor on the same pins that differ in exactly one input.

A monitor that shares the DUT's opinion cannot disagree with it. It does not go quiet — it reports the intended answer, confidently, in writing.

1. Configuration Versus Peeking

The line matters and it is not obvious, because both arrive through a port list and look identical there.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CONFIGURATION -- a monitor may be told this
   ---------------------------------------------------------------------
   CPOL, CPHA                 in the specification. A scope user is told them.
   the frame width            likewise
   the bit order              likewise

   PEEKING -- a monitor may NOT be told this
   ---------------------------------------------------------------------
   which edge THIS DUT        an implementation fact. If the DUT gets it wrong
     launches on                the monitor must not follow.
   the DUT's shift register   the answer, taken from the thing being checked
   the DUT's state            ditto

Peeking rarely looks like peeking in real source. It looks like a monitor handed a launch_edge signal from inside the DUT, or one whose author "checked the RTL" to settle which edge to sample on, or a model that was fixed every time it disagreed until it stopped disagreeing. The effect is identical in every case.

2. The Reconstruction

A monitor taking four pins and four configuration inputs, deriving capture edges, accumulating MOSI and MISO words with an edge count, and emitting a reconstructed transaction on the chip-select deassertSCLK, CS, MOSI, MISOCPOL, CPHA, order,widthselect edgescapture edgeedge countMOSI and MISO wordsemit on deasserttransaction12
Figure 1 — the monitor's inputs are pins and configuration, and nothing else. It detects the select, clears on the assert, counts every edge inside the transaction so it can judge wholeness without being told how many edges to expect, assembles both directions on the capture edges, and emits on the deassert — which is the first moment a transaction is known to be over.

Two Details That Are Not Optional

An edge coincident with the deassert belongs to the transaction that is ENDING. A monitor testing ~cs_n attributes it to no transaction, loses the last bit of every frame whose master deasserts promptly, and reports a partial frame for a master with no fault. Chapter 16.1 hit this and the fix is the same: in_txn = ~cs_n | cs_deassert.

The values emitted on the deassert cycle must include that cycle's capture. The accumulators update non-blockingly and the emit happens in the same cycle, so reading the registers loses exactly one bit of exactly one frame shape — which is the hardest kind of loss to notice.

3. What Reconstruction Looks Like

A sequence diagram showing pins reporting a chip-select assert, four capture edges each delivering one bit in each direction, and a deassert at which the monitor emits a reconstructed transaction to the analysis port.Pins in, one transaction outpinsmonitoranalysisCS falls — clear theaccumulatorsedge 1 — capture:MOSI bit 3, MISO bit3edge 3 — capture:MOSI bit 2, MISO bit2edge 5 — capture:MOSI bit 1, MISO bit1edge 7 — capture:MOSI bit 0, MISO bit0CS rises — 8 edges,4 bits, wholetransaction: words,count, not partial
Figure 2 — the monitor's view of a four-bit frame in CPHA=1. It sees a select, a sequence of edges, and pin levels; from those alone it produces a word, a bit count and a verdict on wholeness. Nothing in the sequence comes from the driver: the only inputs besides the pins are the mode and the width, both of which a scope user would also be told.

The Same Frame as Pin Levels

What the monitor sees, and what it builds

18 cycles
Five rows over eighteen cycles: chip select low across a four-bit frame, SCLK with three-cycle half periods, MOSI holding one bit per bit-time, the monitor's accumulated word filling in on trailing edges, and a running edge count reaching eight.clear on assertclear on assertcapture: trailingcapture: trailingemit: even edgesemit: even edgesCS_n100000000000000011SCLKMOSIXb3b3b3b3b3b3b2b2b2b2b2b2b1b1b1b1Xword0000000b3b3b3b3b3b332323232321edges000011122233344444t0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15t16t17
Figure 3 — the same CPHA=1 frame as levels. The monitor's capture edges are the trailing ones, and the bit it stores at each is the MOSI level at that edge. The two rows below the pins are what the monitor BUILDS, not what it observes: the accumulated word and the running edge count, from which wholeness is decided without being told how many edges to expect.

The word and edges rows are the monitor's state rather than signals on the bus, and they are drawn because they are the whole of the reconstruction: a word that fills in one bit per capture edge, and a count whose parity at the deassert decides wholeness. Nothing on the bus says how many edges to expect.

4. The First Measurement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   legal traffic: 32 transactions across 16 configurations
     reconstructed words matching the intent .... 32 of 32
     bit-count mismatches ....................... 0
     MISO-is-not-the-complement-of-MOSI ......... 0
     partial frames flagged ..................... 0

Every word rebuilt exactly, in both directions, across both polarities, both phases, two widths and both bit orders — with no access to the driver's state, shift register or edge decision. Configuration was enough; introspection was not needed. That is the claim the rest of the chapter gets to lean on.

The MISO check is doing specific work. MISO is driven as the complement of MOSI, so the rebuilt MISO word must be the complement of the rebuilt MOSI word — which is what notices a monitor that captures the return path on the launch edge instead of the capture edge.

5. The Second Measurement

Two instances of the monitor, on the same pins, in the same instant. One is told CPHA by the transaction; the other is told CPHA by what the driver actually does. Then a launch/capture swap is injected — the most common SPI driver bug, and one that leaves every pin-level timing rule satisfied.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   the launch/capture swap, by phase:
     phase   txns   spec monitor disagreed   R4 violations   peeking monitor agreed
     CPHA=0      8                       0              32                       8
     CPHA=1      8                       8              32                       8

The peeking monitor agreed with the intent on all 16, in both phases. It did not stay silent. It reported the intended word, so a scoreboard fed from it compares two copies of one expectation and passes with total confidence while the pins carry the wrong data. That is the failure mode, and it is worse than a missing check, because a missing check is at least visible as a gap in the plan.

And the specification-configured monitor split by phase, which is the finding nobody plans for.

6. Partial Frames, From The Edge Count

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   truncated frames: 4 of 4 flagged partial

Flagged on every truncated frame and on no legal one — the pair of results a flag needs, because one that fires for nothing has not been shown to work and one that fires for everything is worse than absent.

The detection comes from the edge count rather than the bit count, and the two catch different shapes. A master that stops halfway through the last bit has produced an odd number of edges and a bit count that may still look right in one phase. No DUT signal announces stopping early; the parity of an edge count is available from the pins.

7. Passivity, Measured

A monitor that can drive the bus it observes is a monitor that changes what it observes, and the symptom — a DUT that behaves differently when the monitor is connected — is the hardest class of bug to believe. So the claim is measured rather than asserted.

The same stimulus is run twice with the monitor's frame width deliberately wrong the second time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   passivity:  run            monitor width   pin hash    words judged wrong
               correct                    8   0270565f                    0
               misconfigured              7   0270565f                    3

An identical hash of every pin transition, and a different verdict. The monitor's configuration reached its own conclusion and did not reach the pins. The changed verdict is the half that stops the identical hash from being a result about a monitor that does nothing at all.

8. Building It — Three HDLs

Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_monitor.sv — the monitor, rebuilding transactions from pins and configuration alone
// spi_txn_monitor.sv
//
// Chapter 16.5 -- the monitor, which rebuilds transactions from pins and from nothing else.
//
// THE RULE THAT DEFINES THIS COMPONENT.
//
// A monitor may read the pins and the protocol's CONFIGURATION. It may not read the design
// under test. Not its shift register, not its state, not its decision about which edge it
// launches on -- nothing that would not be visible on a bench with an oscilloscope.
//
// That rule sounds like an aesthetic preference and it is not. A monitor that reads the DUT
// shares the DUT's opinion, and a monitor that shares the DUT's opinion cannot disagree
// with it. The chapter's central measurement is exactly this: the same monitor, configured
// from the SPECIFICATION, catches an injected fault; configured from the IMPLEMENTATION --
// which is what peeking amounts to -- it reports the intended transaction with total
// confidence and the fault is invisible.
//
// WHAT COUNTS AS CONFIGURATION AND WHAT COUNTS AS PEEKING, since the line matters.
//
//     CPOL, CPHA, the frame width, the bit order       CONFIGURATION. These are in the
//                                                      spec. A scope user is told them.
//     which edge this particular DUT launches on       PEEKING. That is an implementation
//                                                      fact, and if the DUT gets it wrong
//                                                      the monitor must NOT follow.
//
// The two look identical in a port list, which is why the distinction has to be made in
// the environment that connects them and cannot be enforced here.
//
// THE RECONSTRUCTION, stated before the code.
//
// A frame of N bits has 2N edges. Edge 2k is leading -- SCLK has left its idle level -- and
// 2k+1 is trailing. CPHA=0 captures on leading edges, CPHA=1 on trailing ones. The monitor
// therefore:
//
//     * detects the select, and clears its accumulators on the assert
//     * counts every edge inside the transaction, so it can tell a whole frame from a
//       partial one WITHOUT being told how many edges to expect
//     * assembles MOSI and MISO into words on the capture edges, placing the k-th bit at
//       the index the bit order specifies
//     * emits the transaction on the DEASSERT, because that is the first moment the
//       transaction is known to be over
//
// TWO DETAILS THAT ARE NOT OPTIONAL.
//
// An edge coincident with the deassert belongs to the transaction that is ENDING. A monitor
// that tests `~cs_n` attributes that edge to no transaction at all, loses the last bit of
// every frame whose master deasserts promptly, and reports a partial frame for a master
// with no fault. Chapter 16.1 hit this and the fix is the same here: `in_txn` includes the
// deassert cycle.
//
// And the values EMITTED on the deassert cycle must include that cycle's capture, which
// means the emitted word is a combinational function of the accumulator and the current
// bit rather than the accumulator's registered value. Getting this wrong loses exactly one
// bit, of exactly one frame shape, which is the hardest kind of loss to notice.
//
// WHAT IS DELIBERATELY ABSENT.
//
// There is no output that reaches a pin. Every pin port is an input, and Chapter 16.3's
// modport argument is the structural version of the same claim: a monitor that CAN drive is
// a monitor that changes what it observes, and the symptom -- a DUT that behaves
// differently when the monitor is connected -- is the hardest class of bug to believe. The
// testbench measures this rather than asserting it, by hashing the pin activity of two runs
// that differ only in the monitor's configuration.

`timescale 1ns/1ps

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

    // --- the pins, all inputs, and that is the whole architecture -----------
    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,
    input  wire              miso,

    // --- the protocol's configuration ---------------------------------------
    input  wire              cpol,
    input  wire              cpha,
    input  wire              lsb_first,
    input  wire [LEN_W-1:0]  len,

    // --- the reconstructed transaction --------------------------------------
    output reg               t_valid,
    output reg  [DW-1:0]     t_mosi,
    output reg  [DW-1:0]     t_miso,
    output reg  [LEN_W:0]    t_nbits,
    output reg  [CNT_W-1:0]  t_edges,
    output reg               t_partial
);

    reg sclk_d, cs_n_d;

    wire cs_assert   = ~cs_n &  cs_n_d;
    wire cs_deassert =  cs_n & ~cs_n_d;

    // An edge coincident with the deassert belongs to the transaction that is ending.
    wire in_txn    = ~cs_n | cs_deassert;
    wire sclk_edge = sclk ^ sclk_d;

    wire leading  = sclk_edge & (sclk != cpol);
    wire trailing = sclk_edge & (sclk == cpol);
    wire cap_edge = cpha ? trailing : leading;
    wire cap      = cap_edge & in_txn;

    reg [DW-1:0]    acc_mosi, acc_miso;
    reg [LEN_W:0]   nseen;
    reg [CNT_W-1:0] eseen;

    // Where the k-th captured bit goes. Derived from the bit order in the CONFIGURATION,
    // which is the only place a monitor may get it from.
    wire [LEN_W-1:0] cur_idx = lsb_first ? nseen[LEN_W-1:0]
                                         : (len - 1'b1 - nseen[LEN_W-1:0]);

    // THE BIT IS ONLY STORED IF IT BELONGS INSIDE THE WORD, and the guard is not defensive
    // padding. A monitor told the wrong frame width -- which the testbench's passivity
    // experiment does on purpose -- receives more capture edges than its word has bits, and
    // the MSB-first index above then goes negative, wrapping to a shift that silently writes
    // nothing. The VHDL sibling of this file cannot do that: a negative index is a range
    // violation and the simulation stops. Writing the guard explicitly is what makes the two
    // languages agree on purpose rather than by accident.
    //
    // The COUNT keeps incrementing past the width, because that overflow is exactly what
    // tells the emit below that the frame did not match the configuration.
    wire in_word = (nseen < {1'b0, len});

    wire [DW-1:0] bit_m = in_word ? ({{(DW-1){1'b0}}, mosi} << cur_idx) : {DW{1'b0}};
    wire [DW-1:0] bit_s = in_word ? ({{(DW-1){1'b0}}, miso} << cur_idx) : {DW{1'b0}};

    // The values AS THEY WILL BE once this cycle's capture is folded in. The emit happens
    // in the same cycle, and the accumulators update non-blockingly, so reading the
    // registers would lose the last bit of every frame whose final capture coincides with
    // the deassert.
    wire [DW-1:0]    mosi_now  = cap ? (acc_mosi | bit_m) : acc_mosi;
    wire [DW-1:0]    miso_now  = cap ? (acc_miso | bit_s) : acc_miso;
    wire [LEN_W:0]   nseen_now = cap                    ? nseen + 1'b1 : nseen;
    wire [CNT_W-1:0] eseen_now = (sclk_edge && in_txn)  ? eseen + 1'b1 : eseen;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_d    <= 1'b0;
            cs_n_d    <= 1'b1;
            acc_mosi  <= {DW{1'b0}};
            acc_miso  <= {DW{1'b0}};
            nseen     <= {(LEN_W+1){1'b0}};
            eseen     <= {CNT_W{1'b0}};
            t_valid   <= 1'b0;
            t_mosi    <= {DW{1'b0}};
            t_miso    <= {DW{1'b0}};
            t_nbits   <= {(LEN_W+1){1'b0}};
            t_edges   <= {CNT_W{1'b0}};
            t_partial <= 1'b0;
        end else begin
            sclk_d  <= sclk;
            cs_n_d  <= cs_n;
            t_valid <= 1'b0;

            if (cs_assert) begin
                acc_mosi <= {DW{1'b0}};
                acc_miso <= {DW{1'b0}};
                nseen    <= {(LEN_W+1){1'b0}};
                eseen    <= {CNT_W{1'b0}};
            end else begin
                if (cap) begin
                    acc_mosi <= acc_mosi | bit_m;
                    acc_miso <= acc_miso | bit_s;
                    nseen    <= nseen + 1'b1;
                end
                if (sclk_edge && in_txn)
                    eseen <= eseen + 1'b1;
            end

            if (cs_deassert) begin
                t_valid <= 1'b1;
                t_mosi  <= mosi_now;
                t_miso  <= miso_now;
                t_nbits <= nseen_now;
                t_edges <= eseen_now;

                // A PARTIAL FRAME IS DETECTED FROM THE EDGE COUNT, not from the bit count
                // alone, and the difference is not pedantry. A master that stops halfway
                // through the last bit has produced an odd number of edges and a bit count
                // that may still look right in one phase. Both tests are cheap and each one
                // catches a shape the other misses.
                t_partial <= (eseen_now[0] != 1'b0) || (nseen_now != {1'b0, len});

                acc_mosi <= {DW{1'b0}};
                acc_miso <= {DW{1'b0}};
                nseen    <= {(LEN_W+1){1'b0}};
                eseen    <= {CNT_W{1'b0}};
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_monitor.v — the same design in Verilog-2001
// spi_txn_monitor.v
//
// Chapter 16.5 -- the monitor, which rebuilds transactions from pins and from nothing else.
//
// THE RULE THAT DEFINES THIS COMPONENT.
//
// A monitor may read the pins and the protocol's CONFIGURATION. It may not read the design
// under test. Not its shift register, not its state, not its decision about which edge it
// launches on -- nothing that would not be visible on a bench with an oscilloscope.
//
// That rule sounds like an aesthetic preference and it is not. A monitor that reads the DUT
// shares the DUT's opinion, and a monitor that shares the DUT's opinion cannot disagree
// with it. The chapter's central measurement is exactly this: the same monitor, configured
// from the SPECIFICATION, catches an injected fault; configured from the IMPLEMENTATION --
// which is what peeking amounts to -- it reports the intended transaction with total
// confidence and the fault is invisible.
//
// WHAT COUNTS AS CONFIGURATION AND WHAT COUNTS AS PEEKING, since the line matters.
//
//     CPOL, CPHA, the frame width, the bit order       CONFIGURATION. These are in the
//                                                      spec. A scope user is told them.
//     which edge this particular DUT launches on       PEEKING. That is an implementation
//                                                      fact, and if the DUT gets it wrong
//                                                      the monitor must NOT follow.
//
// The two look identical in a port list, which is why the distinction has to be made in
// the environment that connects them and cannot be enforced here.
//
// THE RECONSTRUCTION, stated before the code.
//
// A frame of N bits has 2N edges. Edge 2k is leading -- SCLK has left its idle level -- and
// 2k+1 is trailing. CPHA=0 captures on leading edges, CPHA=1 on trailing ones. The monitor
// therefore:
//
//     * detects the select, and clears its accumulators on the assert
//     * counts every edge inside the transaction, so it can tell a whole frame from a
//       partial one WITHOUT being told how many edges to expect
//     * assembles MOSI and MISO into words on the capture edges, placing the k-th bit at
//       the index the bit order specifies
//     * emits the transaction on the DEASSERT, because that is the first moment the
//       transaction is known to be over
//
// TWO DETAILS THAT ARE NOT OPTIONAL.
//
// An edge coincident with the deassert belongs to the transaction that is ENDING. A monitor
// that tests `~cs_n` attributes that edge to no transaction at all, loses the last bit of
// every frame whose master deasserts promptly, and reports a partial frame for a master
// with no fault. Chapter 16.1 hit this and the fix is the same here: `in_txn` includes the
// deassert cycle.
//
// And the values EMITTED on the deassert cycle must include that cycle's capture, which
// means the emitted word is a combinational function of the accumulator and the current
// bit rather than the accumulator's registered value. Getting this wrong loses exactly one
// bit, of exactly one frame shape, which is the hardest kind of loss to notice.
//
// WHAT IS DELIBERATELY ABSENT.
//
// There is no output that reaches a pin. Every pin port is an input, and Chapter 16.3's
// modport argument is the structural version of the same claim: a monitor that CAN drive is
// a monitor that changes what it observes, and the symptom -- a DUT that behaves
// differently when the monitor is connected -- is the hardest class of bug to believe. The
// testbench measures this rather than asserting it, by hashing the pin activity of two runs
// that differ only in the monitor's configuration.

`timescale 1ns/1ps

module spi_txn_monitor #(
    parameter DW    = 32,
    parameter LEN_W = 6,
    parameter CNT_W = 10
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- the pins, all inputs, and that is the whole architecture -----------
    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,
    input  wire              miso,

    // --- the protocol's configuration ---------------------------------------
    input  wire              cpol,
    input  wire              cpha,
    input  wire              lsb_first,
    input  wire [LEN_W-1:0]  len,

    // --- the reconstructed transaction --------------------------------------
    output reg               t_valid,
    output reg  [DW-1:0]     t_mosi,
    output reg  [DW-1:0]     t_miso,
    output reg  [LEN_W:0]    t_nbits,
    output reg  [CNT_W-1:0]  t_edges,
    output reg               t_partial
);

    reg sclk_d, cs_n_d;

    wire cs_assert   = ~cs_n &  cs_n_d;
    wire cs_deassert =  cs_n & ~cs_n_d;

    // An edge coincident with the deassert belongs to the transaction that is ending.
    wire in_txn    = ~cs_n | cs_deassert;
    wire sclk_edge = sclk ^ sclk_d;

    wire leading  = sclk_edge & (sclk != cpol);
    wire trailing = sclk_edge & (sclk == cpol);
    wire cap_edge = cpha ? trailing : leading;
    wire cap      = cap_edge & in_txn;

    reg [DW-1:0]    acc_mosi, acc_miso;
    reg [LEN_W:0]   nseen;
    reg [CNT_W-1:0] eseen;

    // Where the k-th captured bit goes. Derived from the bit order in the CONFIGURATION,
    // which is the only place a monitor may get it from.
    wire [LEN_W-1:0] cur_idx = lsb_first ? nseen[LEN_W-1:0]
                                         : (len - 1'b1 - nseen[LEN_W-1:0]);

    // THE BIT IS ONLY STORED IF IT BELONGS INSIDE THE WORD, and the guard is not defensive
    // padding. A monitor told the wrong frame width -- which the testbench's passivity
    // experiment does on purpose -- receives more capture edges than its word has bits, and
    // the MSB-first index above then goes negative, wrapping to a shift that silently writes
    // nothing. The VHDL sibling of this file cannot do that: a negative index is a range
    // violation and the simulation stops. Writing the guard explicitly is what makes the two
    // languages agree on purpose rather than by accident.
    //
    // The COUNT keeps incrementing past the width, because that overflow is exactly what
    // tells the emit below that the frame did not match the configuration.
    wire in_word = (nseen < {1'b0, len});

    wire [DW-1:0] bit_m = in_word ? ({{(DW-1){1'b0}}, mosi} << cur_idx) : {DW{1'b0}};
    wire [DW-1:0] bit_s = in_word ? ({{(DW-1){1'b0}}, miso} << cur_idx) : {DW{1'b0}};

    // The values AS THEY WILL BE once this cycle's capture is folded in. The emit happens
    // in the same cycle, and the accumulators update non-blockingly, so reading the
    // registers would lose the last bit of every frame whose final capture coincides with
    // the deassert.
    wire [DW-1:0]    mosi_now  = cap ? (acc_mosi | bit_m) : acc_mosi;
    wire [DW-1:0]    miso_now  = cap ? (acc_miso | bit_s) : acc_miso;
    wire [LEN_W:0]   nseen_now = cap                    ? nseen + 1'b1 : nseen;
    wire [CNT_W-1:0] eseen_now = (sclk_edge && in_txn)  ? eseen + 1'b1 : eseen;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_d    <= 1'b0;
            cs_n_d    <= 1'b1;
            acc_mosi  <= {DW{1'b0}};
            acc_miso  <= {DW{1'b0}};
            nseen     <= {(LEN_W+1){1'b0}};
            eseen     <= {CNT_W{1'b0}};
            t_valid   <= 1'b0;
            t_mosi    <= {DW{1'b0}};
            t_miso    <= {DW{1'b0}};
            t_nbits   <= {(LEN_W+1){1'b0}};
            t_edges   <= {CNT_W{1'b0}};
            t_partial <= 1'b0;
        end else begin
            sclk_d  <= sclk;
            cs_n_d  <= cs_n;
            t_valid <= 1'b0;

            if (cs_assert) begin
                acc_mosi <= {DW{1'b0}};
                acc_miso <= {DW{1'b0}};
                nseen    <= {(LEN_W+1){1'b0}};
                eseen    <= {CNT_W{1'b0}};
            end else begin
                if (cap) begin
                    acc_mosi <= acc_mosi | bit_m;
                    acc_miso <= acc_miso | bit_s;
                    nseen    <= nseen + 1'b1;
                end
                if (sclk_edge && in_txn)
                    eseen <= eseen + 1'b1;
            end

            if (cs_deassert) begin
                t_valid <= 1'b1;
                t_mosi  <= mosi_now;
                t_miso  <= miso_now;
                t_nbits <= nseen_now;
                t_edges <= eseen_now;

                // A PARTIAL FRAME IS DETECTED FROM THE EDGE COUNT, not from the bit count
                // alone, and the difference is not pedantry. A master that stops halfway
                // through the last bit has produced an odd number of edges and a bit count
                // that may still look right in one phase. Both tests are cheap and each one
                // catches a shape the other misses.
                t_partial <= (eseen_now[0] != 1'b0) || (nseen_now != {1'b0, len});

                acc_mosi <= {DW{1'b0}};
                acc_miso <= {DW{1'b0}};
                nseen    <= {(LEN_W+1){1'b0}};
                eseen    <= {CNT_W{1'b0}};
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_monitor.vhd — the same design in VHDL
-- spi_txn_monitor.vhd
--
-- Chapter 16.5 -- the monitor, which rebuilds transactions from pins and from nothing else.
--
-- THE RULE THAT DEFINES THIS COMPONENT.
--
-- A monitor may read the pins and the protocol's CONFIGURATION. It may not read the design
-- under test. Not its shift register, not its state, not its decision about which edge it
-- launches on -- nothing that would not be visible on a bench with an oscilloscope.
--
-- That sounds like an aesthetic preference and it is not. A monitor that reads the DUT
-- shares the DUT's opinion, and a monitor that shares the DUT's opinion cannot disagree with
-- it. The chapter's central measurement is exactly this: the same monitor, configured from
-- the SPECIFICATION, catches an injected fault; configured from the IMPLEMENTATION -- which
-- is what peeking amounts to -- it reports the intended transaction with total confidence
-- and the fault is invisible.
--
-- WHAT COUNTS AS CONFIGURATION AND WHAT COUNTS AS PEEKING, since the line matters.
--
--     CPOL, CPHA, the frame width, the bit order      CONFIGURATION. These are in the spec.
--                                                     A scope user is told them.
--     which edge THIS DUT launches on                 PEEKING. An implementation fact, and
--                                                     if the DUT gets it wrong the monitor
--                                                     must not follow.
--
-- The two look identical in a port list, which is why the distinction has to be made by the
-- environment that connects them and cannot be enforced here.
--
-- WHAT VHDL CONTRIBUTES: the reconstructed transaction is a RECORD, so the monitor's output
-- is one port that an analysis component can consume whole, and a field added later -- a
-- timestamp, a lane index -- costs nothing at any connection. The SystemVerilog and Verilog
-- versions spell the same thing as six separate ports because Icarus cannot carry a struct
-- through a port list, and the shape of the component is otherwise identical.
--
-- THE RECONSTRUCTION, stated before the code.
--
-- A frame of N bits has 2N edges. Edge 2k is leading -- SCLK has left its idle level -- and
-- 2k+1 is trailing. CPHA=0 captures on leading edges, CPHA=1 on trailing ones. So the
-- monitor detects the select and clears its accumulators on the assert; counts every edge
-- inside the transaction, so it can tell a whole frame from a partial one WITHOUT being told
-- how many edges to expect; assembles MOSI and MISO into words on the capture edges, placing
-- the k-th bit where the bit order says; and emits on the DEASSERT, the first moment the
-- transaction is known to be over.
--
-- TWO DETAILS THAT ARE NOT OPTIONAL.
--
-- An edge coincident with the deassert belongs to the transaction that is ENDING. A monitor
-- that tests `cs_n = '0'` attributes that edge to no transaction, loses the last bit of every
-- frame whose master deasserts promptly, and reports a partial frame for a master with no
-- fault. Chapter 16.1 hit this; the fix is the same here.
--
-- And the values EMITTED on the deassert cycle must include that cycle's capture, which is
-- why the emitted word is computed from variables inside the process rather than read back
-- from the accumulator signals. Getting this wrong loses exactly one bit of exactly one frame
-- shape -- the hardest kind of loss to notice.
--
-- WHAT IS DELIBERATELY ABSENT: any output that reaches a pin. Every pin port is mode `in`,
-- which the ANALYSER enforces, so this file cannot drive the bus it observes even by
-- accident. The testbench measures the same claim behaviourally by hashing the pin activity
-- of two runs that differ only in the monitor's configuration.

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

package spi_txn_pkg is

    constant TDW    : natural  := 32;
    constant TLEN_W : positive := 6;

    -- The reconstructed transaction. One record, so an analysis component consumes it whole.
    type spi_obs_t is record
        valid   : std_logic;
        mosi    : std_logic_vector(TDW - 1 downto 0);
        miso    : std_logic_vector(TDW - 1 downto 0);
        nbits   : natural;
        edges   : natural;
        partial : std_logic;
    end record;

    constant OBS_IDLE : spi_obs_t := (valid   => '0',
                                      mosi    => (others => '0'),
                                      miso    => (others => '0'),
                                      nbits   => 0,
                                      edges   => 0,
                                      partial => '0');

end package spi_txn_pkg;

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

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

        -- The pins. Every one mode `in`, and the analyser is what enforces that.
        sclk      : in  std_logic;
        cs_n      : in  std_logic;
        mosi      : in  std_logic;
        miso      : in  std_logic;

        -- The protocol's configuration.
        cpol      : in  std_logic;
        cpha      : in  std_logic;
        lsb_first : in  std_logic;
        len       : in  unsigned(TLEN_W - 1 downto 0);

        obs       : out spi_obs_t
    );
end entity spi_txn_monitor;

architecture rtl of spi_txn_monitor is

    signal obs_r : spi_obs_t := OBS_IDLE;

begin

    obs <= obs_r;

    process (clk, rst_n) is
        variable sclk_d, cs_n_d : std_logic;
        variable acc_mosi       : std_logic_vector(TDW - 1 downto 0);
        variable acc_miso       : std_logic_vector(TDW - 1 downto 0);
        variable nseen, eseen   : natural;
        variable cs_assert      : boolean;
        variable cs_deassert    : boolean;
        variable in_txn         : boolean;
        variable sclk_edge      : boolean;
        variable cap_edge       : boolean;
        variable cap            : boolean;
        variable idx            : natural;
    begin
        if rst_n = '0' then
            sclk_d   := '0';
            cs_n_d   := '1';
            acc_mosi := (others => '0');
            acc_miso := (others => '0');
            nseen    := 0;
            eseen    := 0;
            obs_r    <= OBS_IDLE;

        elsif rising_edge(clk) then
            obs_r.valid <= '0';

            cs_assert   := (cs_n = '0') and (cs_n_d = '1');
            cs_deassert := (cs_n = '1') and (cs_n_d = '0');

            -- An edge coincident with the deassert belongs to the transaction that is
            -- ending, so the test includes it.
            in_txn      := (cs_n = '0') or cs_deassert;
            sclk_edge   := (sclk /= sclk_d);

            if cpha = '0' then
                cap_edge := sclk_edge and (sclk /= cpol);   -- leading
            else
                cap_edge := sclk_edge and (sclk = cpol);    -- trailing
            end if;
            cap := cap_edge and in_txn;

            if cs_assert then
                acc_mosi := (others => '0');
                acc_miso := (others => '0');
                nseen    := 0;
                eseen    := 0;
            end if;

            -- The order here is what makes the emit on the deassert cycle correct: the
            -- accumulators are VARIABLES, so this cycle's capture is already folded in by
            -- the time the emit below reads them. A signal-based accumulator would still
            -- hold the previous value and the last bit of the frame would be lost.
            if cap then
                -- THE BIT IS ONLY STORED IF IT BELONGS INSIDE THE WORD, and the guard is not
                -- defensive padding. A monitor told the wrong frame width -- which the
                -- testbench's passivity experiment does on purpose -- receives more capture
                -- edges than its word has bits, and the MSB-first index then goes NEGATIVE.
                -- In VHDL that is a range violation and the simulation stops; in a language
                -- with wrapping arithmetic it silently writes nothing and the difference in
                -- behaviour between the two would have been a difference in the CHAPTER.
                --
                -- The count keeps incrementing past the width, because that overflow is
                -- exactly what tells the emit below that the frame did not match the
                -- configuration.
                if nseen < to_integer(len) then
                    if lsb_first = '1' then
                        idx := nseen;
                    else
                        idx := to_integer(len) - 1 - nseen;
                    end if;
                    acc_mosi(idx) := mosi;
                    acc_miso(idx) := miso;
                end if;
                nseen := nseen + 1;
            end if;
            if sclk_edge and in_txn then
                eseen := eseen + 1;
            end if;

            if cs_deassert then
                obs_r.valid <= '1';
                obs_r.mosi  <= acc_mosi;
                obs_r.miso  <= acc_miso;
                obs_r.nbits <= nseen;
                obs_r.edges <= eseen;

                -- A PARTIAL FRAME IS DETECTED FROM THE EDGE COUNT, not from the bit count
                -- alone, and the difference is not pedantry: a master that stops halfway
                -- through the last bit produces an odd number of edges and a bit count that
                -- may still look right in one phase. Both tests are cheap and each catches a
                -- shape the other misses.
                if (eseen mod 2) /= 0 or nseen /= to_integer(len) then
                    obs_r.partial <= '1';
                else
                    obs_r.partial <= '0';
                end if;

                acc_mosi := (others => '0');
                acc_miso := (others => '0');
                nseen    := 0;
                eseen    := 0;
            end if;

            sclk_d := sclk;
            cs_n_d := cs_n;
        end if;
    end process;

end architecture rtl;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_monitor_tb.sv — two instances of one monitor differing only in where they got CPHA, plus a measured passivity check
// spi_txn_monitor_tb.sv
//
// TWO MONITORS, ONE SET OF PINS, AND THE ONLY DIFFERENCE BETWEEN THEM IS WHERE THEY GOT
// THEIR CONFIGURATION.
//
//   u_spec   is told CPHA by the TRANSACTION -- the mode the traffic was supposed to use.
//            This is the specification's view, and it is what a scope user is told.
//   u_impl   is told CPHA by the DRIVER'S ACTUAL BEHAVIOUR -- the edge the implementation
//            really launches on. This is what "peeking" amounts to in practice: a monitor
//            written by reading the DUT's source, or handed a signal from inside it, so
//            that it always looks at the edge the DUT chose.
//
// They are the same module. Neither is a strawman. On a correct DUT they agree on every
// transaction, which is why a suite can carry the peeking one for years without noticing.
//
// THEN A FAULT IS INJECTED that swaps which edge launches -- the single most common SPI
// driver bug, and one that leaves every pin-level timing rule satisfied.
//
//   u_spec reports a word that DISAGREES with what the test asked for. The bug is caught.
//   u_impl reports the intended word EXACTLY. The bug is invisible.
//
// That is the chapter. A monitor that shares the DUT's opinion cannot disagree with it, and
// the failure mode is not a missing check -- it is a check that passes with total
// confidence while the pins carry the wrong data.
//
// THREE MORE MEASUREMENTS, because one demonstration is an anecdote.
//
//   RECONSTRUCTION IS SUFFICIENT. Across every configuration, the spec monitor's rebuilt
//   word equals the intended word, its bit count equals the frame width, and it flags no
//   partial frame. Pins alone are enough; nothing about the DUT was needed.
//
//   BOTH DIRECTIONS, ON THE RIGHT EDGES. MISO is driven as the complement of MOSI, so the
//   monitor's rebuilt MISO word must be the complement of its rebuilt MOSI word. A monitor
//   that captured MISO on the launch edge would get a shifted word and this is what
//   notices.
//
//   PARTIAL FRAMES, FROM THE EDGE COUNT. A truncated frame must be flagged on every
//   transaction, and a legal frame on none. A checker that flags nothing has not been shown
//   to work, and one that flags everything is worse than useless.
//
// AND THE PASSIVITY CLAIM IS MEASURED RATHER THAN ASSERTED.
//
// The same stimulus is run twice with the monitor's frame width deliberately wrong the
// second time. A rolling hash of every pin transition must be IDENTICAL across the two runs
// -- the monitor cannot change what it observes -- while the monitor's verdict must CHANGE,
// proving the second run really did reconfigure something. Two results together: a
// difference that reaches the verdict and not the pins.

`timescale 1ns/1ps

module spi_txn_monitor_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;

    // The driver's fault codes, repeated here so the bench reads without the driver open.
    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;

    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)
    );

    // The width the SPEC monitor is told. Normally the real one; the passivity experiment
    // makes it wrong on purpose.
    reg [LEN_W-1:0] mon_len = 6'd8;

    // THE PEEKING MONITOR'S CONFIGURATION, and this single line is the whole difference.
    //
    // It reproduces what the driver actually does rather than what the transaction asked
    // for. In a real suite this appears in far less obvious forms -- a monitor handed a
    // `launch_edge` signal from the DUT, or a monitor whose author "checked the RTL" to
    // settle which edge to sample on. The effect is identical.
    wire cpha_impl = (fault == F_PHASE[2:0]) ? ~cpha : cpha;

    wire              s_valid, i_valid;
    wire [DW-1:0]     s_mosi, s_miso, i_mosi, i_miso;
    wire [LEN_W:0]    s_nbits, i_nbits;
    wire [CNT_W-1:0]  s_edges, i_edges;
    wire              s_partial, i_partial;

    spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_spec (
        .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(mon_len),
        .t_valid(s_valid), .t_mosi(s_mosi), .t_miso(s_miso),
        .t_nbits(s_nbits), .t_edges(s_edges), .t_partial(s_partial)
    );

    spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_impl (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
        .cpol(cpol), .cpha(cpha_impl), .lsb_first(lsb_first), .len(nbits),
        .t_valid(i_valid), .t_mosi(i_mosi), .t_miso(i_miso),
        .t_nbits(i_nbits), .t_edges(i_edges), .t_partial(i_partial)
    );

    // CHAPTER 16.1'S RULE MONITOR IS HERE TOO, and measurement 2 is the reason.
    //
    // A transaction monitor and a pin-rule monitor have DIFFERENT blind spots, and the
    // launch/capture swap lands in one of them in one phase and the other in the other.
    // Carrying both in this bench is what lets the chapter say which check catches what
    // instead of claiming that one check is enough.
    reg clr = 1'b0;
    wire [8*16-1:0] r_ex_flat, r_vi_flat;
    wire miso_driven = ~cs_n;

    spi_rule_monitor #(
        .LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
        .LEN_W(LEN_W), .CNT_W(16), .NRULES(8)
    ) u_rules (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso_driven(miso_driven),
        .cpol(cpol), .cpha(cpha), .len(nbits),
        .exercised_flat(r_ex_flat), .violated_flat(r_vi_flat), .clr(clr)
    );

    function automatic [15:0] r_vi(input integer i);
        begin r_vi = r_vi_flat[i*16 +: 16]; end
    endfunction

    localparam int R_LAUNCH = 3;

    // ------------------------------------------------------------------
    // The scoring, accumulated continuously into totals and read as deltas. Nothing is
    // reset mid-run: a counter cleared by one process and incremented by another is a race,
    // and a race in the bookkeeping is indistinguishable from a bug in the DUT.
    // ------------------------------------------------------------------
    reg [DW-1:0]     exp_data = {DW{1'b0}};
    reg [LEN_W-1:0]  exp_len  = 6'd8;

    integer s_ok = 0, s_bad = 0, i_ok = 0, i_bad = 0;
    integer n_txn = 0, p_flag = 0, miso_bad = 0, nb_bad = 0;

    wire [DW-1:0] exp_mask = ({{(DW-1){1'b0}}, 1'b1} << exp_len) - {{(DW-1){1'b0}}, 1'b1};

    always @(posedge clk) if (rst_n) begin
        if (s_valid) begin
            n_txn = n_txn + 1;
            if ((s_mosi & exp_mask) === (exp_data & exp_mask)) s_ok  = s_ok  + 1;
            else                                              s_bad = s_bad + 1;
            if ((s_miso & exp_mask) !== ((~s_mosi) & exp_mask)) miso_bad = miso_bad + 1;
            if (s_nbits !== {1'b0, mon_len})                    nb_bad   = nb_bad + 1;
            if (s_partial)                                      p_flag   = p_flag + 1;
        end
        if (i_valid) begin
            if ((i_mosi & exp_mask) === (exp_data & exp_mask)) i_ok  = i_ok  + 1;
            else                                              i_bad = i_bad + 1;
        end
    end

    // ------------------------------------------------------------------
    // The pin hash. A rolling function of every cycle's pin state, so that two runs
    // producing the same pin activity produce the same number and two runs producing
    // different activity almost certainly do not.
    // ------------------------------------------------------------------
    reg         hash_en = 1'b0;
    reg [31:0]  pin_hash = 32'h1;

    always @(posedge clk) if (rst_n && hash_en)
        pin_hash <= {pin_hash[30:0], pin_hash[31]} ^ {29'b0, sclk, cs_n, mosi};

    integer errors = 0;

    initial begin
        #600_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,
                           input [LEN_W-1:0] mlen);
        begin
            @(negedge clk);
            nbits     = n;
            mon_len   = mlen;
            cpol      = pol[0];
            cpha      = pha[0];
            lsb_first = lsb[0];
            fault     = f;
            tx_data   = d;
            exp_data  = d;
            exp_len   = n;
            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 + 6) @(negedge clk);
        end
    endtask

    task automatic clear_rules;
        begin
            @(negedge clk);
            clr = 1'b1;
            @(negedge clk);
            clr = 1'b0;
            @(negedge clk);
        end
    endtask

    integer iw, ipol, ipha, ilsb;
    reg [LEN_W-1:0] w;
    integer base_s_ok, base_s_bad, base_i_ok, base_i_bad;
    integer base_txn, base_p, base_miso, base_nb;
    integer legal_txns, legal_cfgs;
    integer caught, missed;
    integer d_caught [0:1];
    integer d_total  [0:1];
    integer p_agreed [0:1];
    integer r4_fired [0:1];
    integer part_txns, part_flagged;
    reg [31:0] hash_a, hash_b;
    integer verdict_a, verdict_b;

    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. RECONSTRUCTION FROM PINS IS SUFFICIENT.
        // ============================================================
        base_s_ok = s_ok; base_s_bad = s_bad; base_txn = n_txn;
        base_p = p_flag; base_miso = miso_bad; base_nb = nb_bad;
        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, w);
                run_burst(2);
                legal_cfgs = legal_cfgs + 1;
            end
        end

        legal_txns = n_txn - base_txn;
        $display("  legal traffic: %0d transactions across %0d configurations", legal_txns, legal_cfgs);
        $display("    reconstructed words matching the intent .... %0d of %0d",
                 s_ok - base_s_ok, legal_txns);
        $display("    bit-count mismatches ....................... %0d", nb_bad - base_nb);
        $display("    MISO-is-not-the-complement-of-MOSI ......... %0d", miso_bad - base_miso);
        $display("    partial frames flagged ..................... %0d  (must be zero here)",
                 p_flag - base_p);

        if (legal_txns == 0) begin
            $display("  FAIL: no transactions were observed at all, so nothing below means anything");
            errors = errors + 1;
        end
        if ((s_ok - base_s_ok) != legal_txns) begin
            $display("  FAIL: the spec monitor rebuilt %0d of %0d words wrongly; reconstruction from pins is supposed to be exact",
                     s_bad - base_s_bad, legal_txns);
            errors = errors + 1;
        end
        if ((nb_bad - base_nb) != 0 || (miso_bad - base_miso) != 0 || (p_flag - base_p) != 0) begin
            $display("  FAIL: legal traffic produced %0d bit-count mismatches, %0d MISO mismatches and %0d spurious partial flags",
                     nb_bad - base_nb, miso_bad - base_miso, p_flag - base_p);
            errors = errors + 1;
        end
        $display("    1. every one of the %0d transactions was rebuilt EXACTLY from the pins -- data, bit count and both directions -- with no access to the driver's state, its shift register or its edge decision. Configuration was enough; introspection was not needed",
                 legal_txns);
        $display("       and the rebuilt MISO word was the complement of the rebuilt MOSI word in every transaction, which is what catches a monitor that captures the return path on the launch edge instead of the capture edge");

        // ============================================================
        // 2. THREE OBSERVERS, ONE FAULT, AND THREE DIFFERENT ANSWERS.
        //
        // The fault swaps which edge launches. What each observer makes of it depends on
        // the phase, and the split is the point:
        //
        //   CPHA=1   the swap moves the data by a whole bit position. The driver launches
        //            bit k+1 on the very edge the monitor captures bit k, so every
        //            reconstructed word is shifted and the spec monitor disagrees with the
        //            intent on every transaction. Caught.
        //   CPHA=0   the swap does NOT move the data, and the reason is Chapter 16.3's
        //            subject returning. The faulted driver launches on the LEADING edge,
        //            which is also the capture edge, so MOSI moves at the exact instant a
        //            slave samples it. A monitor observes that edge one cycle after the pin
        //            moved -- it has no choice; that is when the transition is detectable --
        //            so it reads the newly launched bit and rebuilds the INTENDED word,
        //            perfectly, every time. The data check is blind here, and Chapter 16.1's
        //            rule R4, `MOSI moves only on launch edges`, is not.
        //
        // So the two monitors are not redundant. Each is blind to a fault shape the other
        // sees, and the CPHA=0 swap is the shape that separates them: a real slave sampling
        // at that edge races the master's change and may latch either value, while every
        // word a data monitor reconstructs looks perfect. The data is not wrong; the data is
        // UNRELIABLE, and only a rule about WHEN pins move can express that.
        //
        // The peeking monitor, meanwhile, agrees with the intent in BOTH phases. It has no
        // blind spot -- it has no independence.
        // ============================================================
        for (ipha = 0; ipha < 2; ipha = ipha + 1) begin
            base_s_bad = s_bad; base_i_ok = i_ok; base_txn = n_txn;
            clear_rules();
            for (ipol = 0; ipol < 2; ipol = ipol + 1)
            for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
                set_cfg(6'd8, ipol, ipha, ilsb, F_PHASE[2:0], 32'h0000_1A5C, 6'd8);
                run_burst(2);
            end
            d_total[ipha]  = n_txn - base_txn;
            d_caught[ipha] = s_bad - base_s_bad;
            p_agreed[ipha] = i_ok  - base_i_ok;
            r4_fired[ipha] = r_vi(R_LAUNCH);
        end

        $display("  the launch/capture swap, by phase:");
        $display("    phase   txns   spec monitor disagreed   R4 violations   peeking monitor agreed");
        for (ipha = 0; ipha < 2; ipha = ipha + 1)
            $display("    CPHA=%0d  %5d   %21d   %13d   %21d",
                     ipha, d_total[ipha], d_caught[ipha], r4_fired[ipha], p_agreed[ipha]);

        // CPHA=1: the data moved, so the data check must catch every transaction.
        if (d_caught[1] != d_total[1]) begin
            $display("  FAIL: in CPHA=1 the spec monitor agreed with the intent on %0d of %0d faulted transactions; the swap shifts the data by a bit position and a monitor sampling the mode's capture edge must see it",
                     d_total[1] - d_caught[1], d_total[1]);
            errors = errors + 1;
        end
        // CPHA=0: the data did NOT move. If the data check fires here the analysis above is
        // wrong and the chapter's claim about complementary blind spots is unfounded.
        if (d_caught[0] != 0) begin
            $display("  FAIL: in CPHA=0 the spec monitor disagreed on %0d transactions, so the swap DID move the data and the claim that the data check is blind in this phase is wrong",
                     d_caught[0]);
            errors = errors + 1;
        end
        // ...and the rule monitor must be the one that sees it, in both phases.
        if (r4_fired[0] == 0 || r4_fired[1] == 0) begin
            $display("  FAIL: R4 did not fire in one of the phases (CPHA=0: %0d, CPHA=1: %0d); the swap puts MOSI in motion at a capture edge and the pin-rule monitor is what must see that",
                     r4_fired[0], r4_fired[1]);
            errors = errors + 1;
        end
        if (p_agreed[0] != d_total[0] || p_agreed[1] != d_total[1]) begin
            $display("  FAIL: the peeking monitor failed to reproduce the intent in some transactions (%0d of %0d, %0d of %0d), so it is not actually peeking and the comparison proves nothing",
                     p_agreed[0], d_total[0], p_agreed[1], d_total[1]);
            errors = errors + 1;
        end

        caught = d_caught[0] + d_caught[1];
        missed = p_agreed[0] + p_agreed[1];

        $display("    2. the peeking monitor reproduced the intended word on ALL %0d faulted transactions, in both phases. Same module, same pins, same instant as the spec monitor -- the only difference is that one was told which edge the SPECIFICATION captures on and the other which edge the DUT ACTUALLY LAUNCHES ON",
                 missed);
        $display("       and the failure mode is worse than a missing check: it did not stay silent, it REPORTED THE INTENDED WORD, so a scoreboard fed from it compares two copies of one expectation and passes with total confidence while the pins carry the wrong data");
        $display("       the spec monitor caught the swap in CPHA=1, where it shifts the data by a bit position, and was BLIND to it in CPHA=0, where the launch lands on the capture edge: the reconstructed word is perfect and MOSI is in motion at the instant a slave samples it. R4 fired in both phases. The data is not wrong there, the data is UNRELIABLE, and only a rule about WHEN pins move can express that -- which is the real argument for carrying a pin-rule checker alongside a transaction monitor rather than choosing between them");

        // ============================================================
        // 3. PARTIAL FRAMES, FROM THE EDGE COUNT.
        // ============================================================
        base_txn = n_txn; base_p = p_flag;
        for (ipha = 0; ipha < 2; ipha = ipha + 1) begin
            set_cfg(6'd8, 0, ipha, 0, F_TRUNC[2:0], 32'h0000_1A5C, 6'd8);
            run_burst(2);
        end
        part_txns    = n_txn - base_txn;
        part_flagged = p_flag - base_p;
        $display("  truncated frames: %0d of %0d flagged partial", part_flagged, part_txns);
        if (part_flagged != part_txns) begin
            $display("  FAIL: %0d truncated frames went unflagged", part_txns - part_flagged);
            errors = errors + 1;
        end
        $display("    3. every truncated frame was flagged and no legal frame was, which is the pair of results a flag needs: one that fires for nothing has not been shown to work and one that fires for everything is worse than absent. The detection comes from the EDGE count, which a monitor can obtain from the pins -- no DUT signal says `I stopped early`");

        // ============================================================
        // 4. PASSIVITY, MEASURED.
        // ============================================================
        set_cfg(6'd8, 0, 0, 0, F_NONE[2:0], 32'h0000_1A5C, 6'd8);
        base_s_bad = s_bad;
        pin_hash = 32'h1;
        hash_en  = 1'b1;
        run_burst(3);
        hash_en  = 1'b0;
        hash_a    = pin_hash;
        verdict_a = s_bad - base_s_bad;

        // The same stimulus, with the MONITOR told the wrong frame width. The driver's
        // inputs are untouched.
        set_cfg(6'd8, 0, 0, 0, F_NONE[2:0], 32'h0000_1A5C, 6'd7);
        base_s_bad = s_bad;
        pin_hash = 32'h1;
        hash_en  = 1'b1;
        run_burst(3);
        hash_en  = 1'b0;
        hash_b    = pin_hash;
        verdict_b = s_bad - base_s_bad;

        $display("  passivity:  run          monitor width   pin hash    words judged wrong");
        $display("              correct      %13d   %08h   %18d", 8, hash_a, verdict_a);
        $display("              misconfigured%13d   %08h   %18d", 7, hash_b, verdict_b);

        if (hash_a !== hash_b) begin
            $display("  FAIL: the pin hash changed when only the monitor's configuration changed, which means the monitor is reaching the pins");
            errors = errors + 1;
        end
        if (verdict_a == verdict_b) begin
            $display("  FAIL: misconfiguring the monitor changed no verdict, so the second run did not reconfigure anything and the passivity result is vacuous");
            errors = errors + 1;
        end
        $display("    4. the pin hash was IDENTICAL across the two runs while the verdict changed from %0d wrong words to %0d. The monitor's configuration reached its own conclusion and did not reach the pins, which is passivity measured rather than asserted -- and the changed verdict is what stops the identical hash from being a result about a monitor that does nothing at all",
                 verdict_a, verdict_b);

        if (errors == 0)
            $display("PASS: a monitor may read the pins and the protocol's configuration, and may not read the design under test -- and the reason is not tidiness. Across %0d legal transactions in %0d configurations this monitor rebuilt every word EXACTLY from the pins alone: data, bit count, and the return path as the complement of the forward path, with no access to the driver's state, shift register or edge decision. Then two instances of the SAME monitor were pointed at the SAME pins, differing only in where they got CPHA -- one from the specification, one from what the driver actually does, which is what peeking amounts to in practice -- and a launch/capture swap was injected, the most common SPI driver bug and one that leaves every pin-level timing rule satisfied. The peeking monitor missed it on every transaction in both phases, and missed it in the worst possible way: not by staying silent but by REPORTING THE INTENDED WORD, so a scoreboard fed from it compares two copies of one expectation and passes with total confidence while the wire carries the wrong data. The spec monitor caught it in CPHA=1, where the swap shifts every word by a bit position -- and was BLIND to it in CPHA=0, where the launch lands on the capture edge itself, so a monitor that necessarily observes an edge one cycle after the pin moved reads the newly launched bit and rebuilds the intended word perfectly while MOSI is in motion at the exact instant a slave samples it; Chapter 16.1's rule R4 fired in both phases. That split is the honest result and it is Chapter 16.3's subject returning at the transaction level: the data there is not wrong, it is UNRELIABLE, only a rule about WHEN pins move can say so, and the argument is therefore for carrying a transaction monitor and a pin-rule monitor together rather than choosing between them. Truncated frames were flagged on every occurrence and on no legal frame, detected from the EDGE count because no DUT signal announces stopping early. And passivity was measured rather than claimed: the same stimulus run twice with the monitor's width deliberately wrong produced an IDENTICAL hash of every pin transition and a DIFFERENT verdict -- a change that reached the conclusion and not the wires, which is the only form of that claim worth making",
                     legal_txns, legal_cfgs);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_monitor_tb.v — the same bench in Verilog-2001
// spi_txn_monitor_tb.v
//
// TWO MONITORS, ONE SET OF PINS, AND THE ONLY DIFFERENCE BETWEEN THEM IS WHERE THEY GOT
// THEIR CONFIGURATION.
//
//   u_spec   is told CPHA by the TRANSACTION -- the mode the traffic was supposed to use.
//            This is the specification's view, and it is what a scope user is told.
//   u_impl   is told CPHA by the DRIVER'S ACTUAL BEHAVIOUR -- the edge the implementation
//            really launches on. This is what "peeking" amounts to in practice: a monitor
//            written by reading the DUT's source, or handed a signal from inside it, so
//            that it always looks at the edge the DUT chose.
//
// They are the same module. Neither is a strawman. On a correct DUT they agree on every
// transaction, which is why a suite can carry the peeking one for years without noticing.
//
// THEN A FAULT IS INJECTED that swaps which edge launches -- the single most common SPI
// driver bug, and one that leaves every pin-level timing rule satisfied.
//
//   u_spec reports a word that DISAGREES with what the test asked for. The bug is caught.
//   u_impl reports the intended word EXACTLY. The bug is invisible.
//
// That is the chapter. A monitor that shares the DUT's opinion cannot disagree with it, and
// the failure mode is not a missing check -- it is a check that passes with total
// confidence while the pins carry the wrong data.
//
// THREE MORE MEASUREMENTS, because one demonstration is an anecdote.
//
//   RECONSTRUCTION IS SUFFICIENT. Across every configuration, the spec monitor's rebuilt
//   word equals the intended word, its bit count equals the frame width, and it flags no
//   partial frame. Pins alone are enough; nothing about the DUT was needed.
//
//   BOTH DIRECTIONS, ON THE RIGHT EDGES. MISO is driven as the complement of MOSI, so the
//   monitor's rebuilt MISO word must be the complement of its rebuilt MOSI word. A monitor
//   that captured MISO on the launch edge would get a shifted word and this is what
//   notices.
//
//   PARTIAL FRAMES, FROM THE EDGE COUNT. A truncated frame must be flagged on every
//   transaction, and a legal frame on none. A checker that flags nothing has not been shown
//   to work, and one that flags everything is worse than useless.
//
// AND THE PASSIVITY CLAIM IS MEASURED RATHER THAN ASSERTED.
//
// The same stimulus is run twice with the monitor's frame width deliberately wrong the
// second time. A rolling hash of every pin transition must be IDENTICAL across the two runs
// -- the monitor cannot change what it observes -- while the monitor's verdict must CHANGE,
// proving the second run really did reconfigure something. Two results together: a
// difference that reaches the verdict and not the pins.

`timescale 1ns/1ps

module spi_txn_monitor_tb;

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

    // The driver's fault codes, repeated here so the bench reads without the driver open.
    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;

    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)
    );

    // The width the SPEC monitor is told. Normally the real one; the passivity experiment
    // makes it wrong on purpose.
    reg [LEN_W-1:0] mon_len;

    // THE PEEKING MONITOR'S CONFIGURATION, and this single line is the whole difference.
    //
    // It reproduces what the driver actually does rather than what the transaction asked
    // for. In a real suite this appears in far less obvious forms -- a monitor handed a
    // `launch_edge` signal from the DUT, or a monitor whose author "checked the RTL" to
    // settle which edge to sample on. The effect is identical.
    wire cpha_impl = (fault == F_PHASE[2:0]) ? ~cpha : cpha;

    wire              s_valid, i_valid;
    wire [DW-1:0]     s_mosi, s_miso, i_mosi, i_miso;
    wire [LEN_W:0]    s_nbits, i_nbits;
    wire [CNT_W-1:0]  s_edges, i_edges;
    wire              s_partial, i_partial;

    spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_spec (
        .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(mon_len),
        .t_valid(s_valid), .t_mosi(s_mosi), .t_miso(s_miso),
        .t_nbits(s_nbits), .t_edges(s_edges), .t_partial(s_partial)
    );

    spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_impl (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
        .cpol(cpol), .cpha(cpha_impl), .lsb_first(lsb_first), .len(nbits),
        .t_valid(i_valid), .t_mosi(i_mosi), .t_miso(i_miso),
        .t_nbits(i_nbits), .t_edges(i_edges), .t_partial(i_partial)
    );

    // CHAPTER 16.1'S RULE MONITOR IS HERE TOO, and measurement 2 is the reason.
    //
    // A transaction monitor and a pin-rule monitor have DIFFERENT blind spots, and the
    // launch/capture swap lands in one of them in one phase and the other in the other.
    // Carrying both in this bench is what lets the chapter say which check catches what
    // instead of claiming that one check is enough.
    reg clr;
    wire [8*16-1:0] r_ex_flat, r_vi_flat;
    wire miso_driven = ~cs_n;

    spi_rule_monitor #(
        .LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
        .LEN_W(LEN_W), .CNT_W(16), .NRULES(8)
    ) u_rules (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso_driven(miso_driven),
        .cpol(cpol), .cpha(cpha), .len(nbits),
        .exercised_flat(r_ex_flat), .violated_flat(r_vi_flat), .clr(clr)
    );

        function [15:0] r_vi;
        input integer i;
        begin r_vi = r_vi_flat[i*16 +: 16]; end
    endfunction

    localparam R_LAUNCH = 3;

    // ------------------------------------------------------------------
    // The scoring, accumulated continuously into totals and read as deltas. Nothing is
    // reset mid-run: a counter cleared by one process and incremented by another is a race,
    // and a race in the bookkeeping is indistinguishable from a bug in the DUT.
    // ------------------------------------------------------------------
    reg [DW-1:0]     exp_data;
    reg [LEN_W-1:0]  exp_len;

    integer s_ok, s_bad, i_ok, i_bad;
    integer n_txn, p_flag, miso_bad, nb_bad;

    wire [DW-1:0] exp_mask = ({{(DW-1){1'b0}}, 1'b1} << exp_len) - {{(DW-1){1'b0}}, 1'b1};

    always @(posedge clk) if (rst_n) begin
        if (s_valid) begin
            n_txn = n_txn + 1;
            if ((s_mosi & exp_mask) === (exp_data & exp_mask)) s_ok  = s_ok  + 1;
            else                                              s_bad = s_bad + 1;
            if ((s_miso & exp_mask) !== ((~s_mosi) & exp_mask)) miso_bad = miso_bad + 1;
            if (s_nbits !== {1'b0, mon_len})                    nb_bad   = nb_bad + 1;
            if (s_partial)                                      p_flag   = p_flag + 1;
        end
        if (i_valid) begin
            if ((i_mosi & exp_mask) === (exp_data & exp_mask)) i_ok  = i_ok  + 1;
            else                                              i_bad = i_bad + 1;
        end
    end

    // ------------------------------------------------------------------
    // The pin hash. A rolling function of every cycle's pin state, so that two runs
    // producing the same pin activity produce the same number and two runs producing
    // different activity almost certainly do not.
    // ------------------------------------------------------------------
    reg         hash_en;
    reg [31:0]  pin_hash;

    always @(posedge clk) if (rst_n && hash_en)
        pin_hash <= {pin_hash[30:0], pin_hash[31]} ^ {29'b0, sclk, cs_n, mosi};

    integer errors;

    initial begin
        #600_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;
        input [LEN_W-1:0] mlen;
        begin
            @(negedge clk);
            nbits     = n;
            mon_len   = mlen;
            cpol      = pol[0];
            cpha      = pha[0];
            lsb_first = lsb[0];
            fault     = f;
            tx_data   = d;
            exp_data  = d;
            exp_len   = n;
            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 + 6) @(negedge clk);
        end
    endtask

    task clear_rules;
        begin
            @(negedge clk);
            clr = 1'b1;
            @(negedge clk);
            clr = 1'b0;
            @(negedge clk);
        end
    endtask

    integer iw, ipol, ipha, ilsb;
    reg [LEN_W-1:0] w;
    integer base_s_ok, base_s_bad, base_i_ok, base_i_bad;
    integer base_txn, base_p, base_miso, base_nb;
    integer legal_txns, legal_cfgs;
    integer caught, missed;
    integer d_caught [0:1];
    integer d_total  [0:1];
    integer p_agreed [0:1];
    integer r4_fired [0:1];
    integer part_txns, part_flagged;
    reg [31:0] hash_a, hash_b;
    integer verdict_a, verdict_b;

    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. RECONSTRUCTION FROM PINS IS SUFFICIENT.
        // ============================================================
        base_s_ok = s_ok; base_s_bad = s_bad; base_txn = n_txn;
        base_p = p_flag; base_miso = miso_bad; base_nb = nb_bad;
        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, w);
                run_burst(2);
                legal_cfgs = legal_cfgs + 1;
            end
        end

        legal_txns = n_txn - base_txn;
        $display("  legal traffic: %0d transactions across %0d configurations", legal_txns, legal_cfgs);
        $display("    reconstructed words matching the intent .... %0d of %0d",
                 s_ok - base_s_ok, legal_txns);
        $display("    bit-count mismatches ....................... %0d", nb_bad - base_nb);
        $display("    MISO-is-not-the-complement-of-MOSI ......... %0d", miso_bad - base_miso);
        $display("    partial frames flagged ..................... %0d  (must be zero here)",
                 p_flag - base_p);

        if (legal_txns == 0) begin
            $display("  FAIL: no transactions were observed at all, so nothing below means anything");
            errors = errors + 1;
        end
        if ((s_ok - base_s_ok) != legal_txns) begin
            $display("  FAIL: the spec monitor rebuilt %0d of %0d words wrongly; reconstruction from pins is supposed to be exact",
                     s_bad - base_s_bad, legal_txns);
            errors = errors + 1;
        end
        if ((nb_bad - base_nb) != 0 || (miso_bad - base_miso) != 0 || (p_flag - base_p) != 0) begin
            $display("  FAIL: legal traffic produced %0d bit-count mismatches, %0d MISO mismatches and %0d spurious partial flags",
                     nb_bad - base_nb, miso_bad - base_miso, p_flag - base_p);
            errors = errors + 1;
        end
        $display("    1. every one of the %0d transactions was rebuilt EXACTLY from the pins -- data, bit count and both directions -- with no access to the driver's state, its shift register or its edge decision. Configuration was enough; introspection was not needed",
                 legal_txns);
        $display("       and the rebuilt MISO word was the complement of the rebuilt MOSI word in every transaction, which is what catches a monitor that captures the return path on the launch edge instead of the capture edge");

        // ============================================================
        // 2. THREE OBSERVERS, ONE FAULT, AND THREE DIFFERENT ANSWERS.
        //
        // The fault swaps which edge launches. What each observer makes of it depends on
        // the phase, and the split is the point:
        //
        //   CPHA=1   the swap moves the data by a whole bit position. The driver launches
        //            bit k+1 on the very edge the monitor captures bit k, so every
        //            reconstructed word is shifted and the spec monitor disagrees with the
        //            intent on every transaction. Caught.
        //   CPHA=0   the swap does NOT move the data, and the reason is Chapter 16.3's
        //            subject returning. The faulted driver launches on the LEADING edge,
        //            which is also the capture edge, so MOSI moves at the exact instant a
        //            slave samples it. A monitor observes that edge one cycle after the pin
        //            moved -- it has no choice; that is when the transition is detectable --
        //            so it reads the newly launched bit and rebuilds the INTENDED word,
        //            perfectly, every time. The data check is blind here, and Chapter 16.1's
        //            rule R4, `MOSI moves only on launch edges`, is not.
        //
        // So the two monitors are not redundant. Each is blind to a fault shape the other
        // sees, and the CPHA=0 swap is the shape that separates them: a real slave sampling
        // at that edge races the master's change and may latch either value, while every
        // word a data monitor reconstructs looks perfect. The data is not wrong; the data is
        // UNRELIABLE, and only a rule about WHEN pins move can express that.
        //
        // The peeking monitor, meanwhile, agrees with the intent in BOTH phases. It has no
        // blind spot -- it has no independence.
        // ============================================================
        for (ipha = 0; ipha < 2; ipha = ipha + 1) begin
            base_s_bad = s_bad; base_i_ok = i_ok; base_txn = n_txn;
            clear_rules();
            for (ipol = 0; ipol < 2; ipol = ipol + 1)
            for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
                set_cfg(6'd8, ipol, ipha, ilsb, F_PHASE[2:0], 32'h0000_1A5C, 6'd8);
                run_burst(2);
            end
            d_total[ipha]  = n_txn - base_txn;
            d_caught[ipha] = s_bad - base_s_bad;
            p_agreed[ipha] = i_ok  - base_i_ok;
            r4_fired[ipha] = r_vi(R_LAUNCH);
        end

        $display("  the launch/capture swap, by phase:");
        $display("    phase   txns   spec monitor disagreed   R4 violations   peeking monitor agreed");
        for (ipha = 0; ipha < 2; ipha = ipha + 1)
            $display("    CPHA=%0d  %5d   %21d   %13d   %21d",
                     ipha, d_total[ipha], d_caught[ipha], r4_fired[ipha], p_agreed[ipha]);

        // CPHA=1: the data moved, so the data check must catch every transaction.
        if (d_caught[1] != d_total[1]) begin
            $display("  FAIL: in CPHA=1 the spec monitor agreed with the intent on %0d of %0d faulted transactions; the swap shifts the data by a bit position and a monitor sampling the mode's capture edge must see it",
                     d_total[1] - d_caught[1], d_total[1]);
            errors = errors + 1;
        end
        // CPHA=0: the data did NOT move. If the data check fires here the analysis above is
        // wrong and the chapter's claim about complementary blind spots is unfounded.
        if (d_caught[0] != 0) begin
            $display("  FAIL: in CPHA=0 the spec monitor disagreed on %0d transactions, so the swap DID move the data and the claim that the data check is blind in this phase is wrong",
                     d_caught[0]);
            errors = errors + 1;
        end
        // ...and the rule monitor must be the one that sees it, in both phases.
        if (r4_fired[0] == 0 || r4_fired[1] == 0) begin
            $display("  FAIL: R4 did not fire in one of the phases (CPHA=0: %0d, CPHA=1: %0d); the swap puts MOSI in motion at a capture edge and the pin-rule monitor is what must see that",
                     r4_fired[0], r4_fired[1]);
            errors = errors + 1;
        end
        if (p_agreed[0] != d_total[0] || p_agreed[1] != d_total[1]) begin
            $display("  FAIL: the peeking monitor failed to reproduce the intent in some transactions (%0d of %0d, %0d of %0d), so it is not actually peeking and the comparison proves nothing",
                     p_agreed[0], d_total[0], p_agreed[1], d_total[1]);
            errors = errors + 1;
        end

        caught = d_caught[0] + d_caught[1];
        missed = p_agreed[0] + p_agreed[1];

        $display("    2. the peeking monitor reproduced the intended word on ALL %0d faulted transactions, in both phases. Same module, same pins, same instant as the spec monitor -- the only difference is that one was told which edge the SPECIFICATION captures on and the other which edge the DUT ACTUALLY LAUNCHES ON",
                 missed);
        $display("       and the failure mode is worse than a missing check: it did not stay silent, it REPORTED THE INTENDED WORD, so a scoreboard fed from it compares two copies of one expectation and passes with total confidence while the pins carry the wrong data");
        $display("       the spec monitor caught the swap in CPHA=1, where it shifts the data by a bit position, and was BLIND to it in CPHA=0, where the launch lands on the capture edge: the reconstructed word is perfect and MOSI is in motion at the instant a slave samples it. R4 fired in both phases. The data is not wrong there, the data is UNRELIABLE, and only a rule about WHEN pins move can express that -- which is the real argument for carrying a pin-rule checker alongside a transaction monitor rather than choosing between them");

        // ============================================================
        // 3. PARTIAL FRAMES, FROM THE EDGE COUNT.
        // ============================================================
        base_txn = n_txn; base_p = p_flag;
        for (ipha = 0; ipha < 2; ipha = ipha + 1) begin
            set_cfg(6'd8, 0, ipha, 0, F_TRUNC[2:0], 32'h0000_1A5C, 6'd8);
            run_burst(2);
        end
        part_txns    = n_txn - base_txn;
        part_flagged = p_flag - base_p;
        $display("  truncated frames: %0d of %0d flagged partial", part_flagged, part_txns);
        if (part_flagged != part_txns) begin
            $display("  FAIL: %0d truncated frames went unflagged", part_txns - part_flagged);
            errors = errors + 1;
        end
        $display("    3. every truncated frame was flagged and no legal frame was, which is the pair of results a flag needs: one that fires for nothing has not been shown to work and one that fires for everything is worse than absent. The detection comes from the EDGE count, which a monitor can obtain from the pins -- no DUT signal says `I stopped early`");

        // ============================================================
        // 4. PASSIVITY, MEASURED.
        // ============================================================
        set_cfg(6'd8, 0, 0, 0, F_NONE[2:0], 32'h0000_1A5C, 6'd8);
        base_s_bad = s_bad;
        pin_hash = 32'h1;
        hash_en  = 1'b1;
        run_burst(3);
        hash_en  = 1'b0;
        hash_a    = pin_hash;
        verdict_a = s_bad - base_s_bad;

        // The same stimulus, with the MONITOR told the wrong frame width. The driver's
        // inputs are untouched.
        set_cfg(6'd8, 0, 0, 0, F_NONE[2:0], 32'h0000_1A5C, 6'd7);
        base_s_bad = s_bad;
        pin_hash = 32'h1;
        hash_en  = 1'b1;
        run_burst(3);
        hash_en  = 1'b0;
        hash_b    = pin_hash;
        verdict_b = s_bad - base_s_bad;

        $display("  passivity:  run          monitor width   pin hash    words judged wrong");
        $display("              correct      %13d   %08h   %18d", 8, hash_a, verdict_a);
        $display("              misconfigured%13d   %08h   %18d", 7, hash_b, verdict_b);

        if (hash_a !== hash_b) begin
            $display("  FAIL: the pin hash changed when only the monitor's configuration changed, which means the monitor is reaching the pins");
            errors = errors + 1;
        end
        if (verdict_a == verdict_b) begin
            $display("  FAIL: misconfiguring the monitor changed no verdict, so the second run did not reconfigure anything and the passivity result is vacuous");
            errors = errors + 1;
        end
        $display("    4. the pin hash was IDENTICAL across the two runs while the verdict changed from %0d wrong words to %0d. The monitor's configuration reached its own conclusion and did not reach the pins, which is passivity measured rather than asserted -- and the changed verdict is what stops the identical hash from being a result about a monitor that does nothing at all",
                 verdict_a, verdict_b);

        if (errors == 0)
            $display("PASS: a monitor may read the pins and the protocol's configuration, and may not read the design under test -- and the reason is not tidiness. Across %0d legal transactions in %0d configurations this monitor rebuilt every word EXACTLY from the pins alone: data, bit count, and the return path as the complement of the forward path, with no access to the driver's state, shift register or edge decision. Then two instances of the SAME monitor were pointed at the SAME pins, differing only in where they got CPHA -- one from the specification, one from what the driver actually does, which is what peeking amounts to in practice -- and a launch/capture swap was injected, the most common SPI driver bug and one that leaves every pin-level timing rule satisfied. The peeking monitor missed it on every transaction in both phases, and missed it in the worst possible way: not by staying silent but by REPORTING THE INTENDED WORD, so a scoreboard fed from it compares two copies of one expectation and passes with total confidence while the wire carries the wrong data. The spec monitor caught it in CPHA=1, where the swap shifts every word by a bit position -- and was BLIND to it in CPHA=0, where the launch lands on the capture edge itself, so a monitor that necessarily observes an edge one cycle after the pin moved reads the newly launched bit and rebuilds the intended word perfectly while MOSI is in motion at the exact instant a slave samples it; Chapter 16.1's rule R4 fired in both phases. That split is the honest result and it is Chapter 16.3's subject returning at the transaction level: the data there is not wrong, it is UNRELIABLE, only a rule about WHEN pins move can say so, and the argument is therefore for carrying a transaction monitor and a pin-rule monitor together rather than choosing between them. Truncated frames were flagged on every occurrence and on no legal frame, detected from the EDGE count because no DUT signal announces stopping early. And passivity was measured rather than claimed: the same stimulus run twice with the monitor's width deliberately wrong produced an IDENTICAL hash of every pin transition and a DIFFERENT verdict -- a change that reached the conclusion and not the wires, which is the only form of that claim worth making",
                     legal_txns, legal_cfgs);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        cpol = 1'b0;
        cpha = 1'b0;
        lsb_first = 1'b0;
        s_ok = 0;
        s_bad = 0;
        i_ok = 0;
        i_bad = 0;
        n_txn = 0;
        p_flag = 0;
        miso_bad = 0;
        nb_bad = 0;
        clk = 1'b0;
        rst_n = 1'b1;
        start = 1'b0;
        tx_data = {DW{1'b0}};
        nbits = 6'd8;
        fault = 3'd0;
        mon_len = 6'd8;
        clr = 1'b0;
        exp_data = {DW{1'b0}};
        exp_len = 6'd8;
        hash_en = 1'b0;
        pin_hash = 32'h1;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_monitor_tb.vhd — the same bench in VHDL
-- spi_txn_monitor_tb.vhd
--
-- TWO MONITORS, ONE SET OF PINS, AND THE ONLY DIFFERENCE BETWEEN THEM IS WHERE THEY GOT
-- THEIR CONFIGURATION.
--
--   u_spec   is told CPHA by the TRANSACTION -- the mode the traffic was supposed to use.
--            The specification's view, and what a scope user is told.
--   u_impl   is told CPHA by the DRIVER'S ACTUAL BEHAVIOUR -- the edge the implementation
--            really launches on. This is what "peeking" amounts to in practice: a monitor
--            written by reading the DUT's source, or handed a signal from inside it, so that
--            it always looks at the edge the DUT chose.
--
-- They are the same entity. Neither is a strawman. On a correct DUT they agree on every
-- transaction, which is why a suite can carry the peeking one for years without noticing.
--
-- THEN A FAULT IS INJECTED that swaps which edge launches -- the most common SPI driver bug,
-- and one that leaves every pin-level timing rule satisfied.
--
-- THE RESULT SPLITS BY PHASE, and the split is the chapter's real finding:
--
--   CPHA=1   the swap moves the data by a bit position. The spec monitor disagrees with the
--            intent on every transaction. Caught.
--   CPHA=0   the swap does NOT move the data. The faulted driver launches on the LEADING
--            edge, which is also the capture edge, so MOSI moves at the exact instant a slave
--            samples it. A monitor observes an edge one cycle after the pin moved -- it has
--            no choice, that is when the transition is detectable -- so it reads the newly
--            launched bit and rebuilds the INTENDED word, perfectly, every time. The data
--            check is blind, and Chapter 16.1's rule R4 is not.
--
-- The peeking monitor agrees with the intent in BOTH phases. It has no blind spot; it has no
-- independence.
--
-- So the two monitors are not redundant: each is blind to a fault shape the other sees. In
-- CPHA=0 the data is not wrong, the data is UNRELIABLE, and only a rule about WHEN pins move
-- can express that.
--
-- THREE MORE MEASUREMENTS, because one demonstration is an anecdote.
--
--   RECONSTRUCTION IS SUFFICIENT. Across every configuration the spec monitor's rebuilt word
--   equals the intended word, its bit count equals the frame width, and it flags no partial
--   frame. Pins alone were enough.
--
--   BOTH DIRECTIONS, ON THE RIGHT EDGES. MISO is the complement of MOSI, so the rebuilt MISO
--   word must be the complement of the rebuilt MOSI word -- which is what notices a monitor
--   that captures the return path on the launch edge.
--
--   PARTIAL FRAMES, FROM THE EDGE COUNT. Flagged on every truncated frame and on no legal
--   one. A flag that fires for nothing has not been shown to work; one that fires for
--   everything is worse than absent.
--
-- AND PASSIVITY IS MEASURED RATHER THAN ASSERTED: the same stimulus twice with the monitor's
-- frame width deliberately wrong the second time, requiring an IDENTICAL hash of every pin
-- transition and a DIFFERENT verdict. A change that reaches the conclusion and not the wires.

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

entity spi_txn_monitor_tb is
end entity spi_txn_monitor_tb;

architecture tb of spi_txn_monitor_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);

    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 miso_driven      : std_logic;

    -- The width the SPEC monitor is told. Normally the real one; the passivity experiment
    -- makes it wrong on purpose.
    signal mon_len : unsigned(TLEN_W - 1 downto 0) := to_unsigned(8, TLEN_W);

    -- THE PEEKING MONITOR'S CONFIGURATION, and this single line is the whole difference. It
    -- reproduces what the driver actually does rather than what the transaction asked for. In
    -- a real suite it appears in far less obvious forms -- a monitor handed a `launch_edge`
    -- signal from the DUT, or one whose author "checked the RTL" to settle which edge to
    -- sample on. The effect is identical.
    signal cpha_impl : std_logic;

    signal obs_spec, obs_impl : spi_obs_t;

    signal clr : std_logic := '0';
    signal r_ex, r_vi : rule_counts_t;

    -- Scoring, accumulated continuously and read as deltas. Nothing is reset mid-run: a
    -- counter cleared by one process and incremented by another is a race, and a race in the
    -- bookkeeping is indistinguishable from a bug in the DUT.
    type score_t is protected
        procedure obs_spec_txn (ok : boolean; miso_ok : boolean;
                                nb_ok : boolean; partial : boolean);
        procedure obs_impl_txn (ok : boolean);
        impure function s_ok    return integer;
        impure function s_bad   return integer;
        impure function i_ok    return integer;
        impure function n_txn   return integer;
        impure function p_flag  return integer;
        impure function mi_bad  return integer;
        impure function nb_bad  return integer;
    end protected score_t;

    type score_t is protected body
        variable v_s_ok, v_s_bad, v_i_ok, v_n, v_p, v_mi, v_nb : integer := 0;
        procedure obs_spec_txn (ok : boolean; miso_ok : boolean;
                                nb_ok : boolean; partial : boolean) is
        begin
            v_n := v_n + 1;
            if ok then v_s_ok := v_s_ok + 1; else v_s_bad := v_s_bad + 1; end if;
            if not miso_ok then v_mi := v_mi + 1; end if;
            if not nb_ok   then v_nb := v_nb + 1; end if;
            if partial     then v_p  := v_p  + 1; end if;
        end procedure;
        procedure obs_impl_txn (ok : boolean) is
        begin
            if ok then v_i_ok := v_i_ok + 1; end if;
        end procedure;
        impure function s_ok   return integer is begin return v_s_ok;  end function;
        impure function s_bad  return integer is begin return v_s_bad; end function;
        impure function i_ok   return integer is begin return v_i_ok;  end function;
        impure function n_txn  return integer is begin return v_n;     end function;
        impure function p_flag return integer is begin return v_p;     end function;
        impure function mi_bad return integer is begin return v_mi;    end function;
        impure function nb_bad return integer is begin return v_nb;    end function;
    end protected body score_t;

    shared variable score : score_t;

    signal exp_data : std_logic_vector(DW - 1 downto 0) := (others => '0');
    signal exp_len  : natural := 8;

    -- The pin hash: a rolling function of every cycle's pin state, so two runs with the same
    -- pin activity produce the same number and two runs with different activity almost
    -- certainly do not.
    signal hash_en  : boolean := false;
    signal hash_clr : boolean := false;
    signal pin_hash : unsigned(31 downto 0) := to_unsigned(1, 32);

    signal errors : integer := 0;

begin

    miso        <= not mosi;
    miso_driven <= not cs_n;
    cpha_impl   <= not req.cpha when req.fault = F_PHASE else req.cpha;

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

    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_spec : 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 => mon_len, obs => obs_spec);

    u_impl : 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 => cpha_impl, lsb_first => req.lsb_first,
                  len => req.nbits, obs => obs_impl);

    -- Chapter 16.1's rule monitor is here too, and measurement 2 is the reason: a transaction
    -- monitor and a pin-rule monitor have DIFFERENT blind spots, and the launch/capture swap
    -- lands in one of them in one phase. Carrying both is what lets the chapter say which
    -- check catches what instead of claiming one check is enough.
    u_rules : entity work.spi_rule_monitor
        generic map (LEAD => LEAD_C, HALF => HALF_C, LAG => LAG_C, GAP => GAP_C,
                     LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  sclk => sclk, cs_n => cs_n, mosi => mosi, miso_driven => miso_driven,
                  cpol => req.cpol, cpha => req.cpha, len => req.nbits,
                  exercised => r_ex, violated => r_vi, clr => clr);

    score_proc : process (clk) is
        variable mask : std_logic_vector(DW - 1 downto 0);
    begin
        if rising_edge(clk) and rst_n = '1' then
            mask := std_logic_vector(shift_left(to_unsigned(1, DW), exp_len) - 1);
            if obs_spec.valid = '1' then
                score.obs_spec_txn((obs_spec.mosi and mask) = (exp_data and mask),
                                   (obs_spec.miso and mask) = ((not obs_spec.mosi) and mask),
                                   obs_spec.nbits = to_integer(mon_len),
                                   obs_spec.partial = '1');
            end if;
            if obs_impl.valid = '1' then
                score.obs_impl_txn((obs_impl.mosi and mask) = (exp_data and mask));
            end if;
        end if;
    end process score_proc;

    hash_proc : process (clk) is
    begin
        if rising_edge(clk) then
            if hash_clr then
                pin_hash <= to_unsigned(1, 32);
            elsif rst_n = '1' and hash_en then
                pin_hash <= (pin_hash(30 downto 0) & pin_hash(31))
                            xor ("" & (28 downto 0 => '0') & sclk & cs_n & mosi);
            end if;
        end if;
    end process hash_proc;

    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);
                           mlen : natural) 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);
            mon_len  <= to_unsigned(mlen, TLEN_W);
            exp_data <= d;
            exp_len  <= n;
            for i in 0 to 5 loop wait until falling_edge(clk); end loop;
        end procedure set_cfg;

        procedure clear_rules is
        begin
            wait until falling_edge(clk);
            clr <= '1';
            wait until falling_edge(clk);
            clr <= '0';
            wait until falling_edge(clk);
        end procedure clear_rules;

        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 + 5 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";

        type i2_t is array (0 to 1) of integer;

        variable w                                     : natural;
        variable b_s_ok, b_s_bad, b_i_ok, b_txn        : integer;
        variable b_p, b_mi, b_nb                       : integer;
        variable legal_txns, legal_cfgs                : integer := 0;
        variable d_total, d_caught, p_agreed, r4_fired : i2_t := (0, 0);
        variable part_txns, part_flagged               : integer;
        variable hash_a, hash_b                        : unsigned(31 downto 0);
        variable verdict_a, verdict_b                  : 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. RECONSTRUCTION FROM PINS IS SUFFICIENT.
        -- ==============================================================
        b_s_ok := score.s_ok; b_s_bad := score.s_bad; b_txn := score.n_txn;
        b_p := score.p_flag;  b_mi := score.mi_bad;   b_nb := score.nb_bad;

        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, w);
                        else
                            set_cfg(w, pol, pha, lsb, F_NONE, PAT_B, w);
                        end if;
                        run_burst(2);
                        legal_cfgs := legal_cfgs + 1;
                    end loop;
                end loop;
            end loop;
        end loop;

        legal_txns := score.n_txn - b_txn;
        report "  legal traffic: " & integer'image(legal_txns) & " transactions across " &
               integer'image(legal_cfgs) & " configurations";
        report "    reconstructed words matching the intent .... " &
               integer'image(score.s_ok - b_s_ok) & " of " & integer'image(legal_txns);
        report "    bit-count mismatches ....................... " &
               integer'image(score.nb_bad - b_nb);
        report "    MISO-is-not-the-complement-of-MOSI ......... " &
               integer'image(score.mi_bad - b_mi);
        report "    partial frames flagged ..................... " &
               integer'image(score.p_flag - b_p) & "  (must be zero here)";

        if legal_txns = 0 then
            report "  FAIL: no transactions were observed at all, so nothing below means anything";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if (score.s_ok - b_s_ok) /= legal_txns then
            report "  FAIL: the spec monitor rebuilt " & integer'image(score.s_bad - b_s_bad) &
                   " of " & integer'image(legal_txns) &
                   " words wrongly; reconstruction from pins is supposed to be exact";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if (score.nb_bad - b_nb) /= 0 or (score.mi_bad - b_mi) /= 0 or (score.p_flag - b_p) /= 0 then
            report "  FAIL: legal traffic produced " & integer'image(score.nb_bad - b_nb) &
                   " bit-count mismatches, " & integer'image(score.mi_bad - b_mi) &
                   " MISO mismatches and " & integer'image(score.p_flag - b_p) &
                   " spurious partial flags";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    1. every one of the " & integer'image(legal_txns) &
               " transactions was rebuilt EXACTLY from the pins -- data, bit count and both directions -- with no access to the driver's state, its shift register or its edge decision. Configuration was enough; introspection was not needed";
        report "       and the rebuilt MISO word was the complement of the rebuilt MOSI word in every transaction, which is what catches a monitor that captures the return path on the launch edge instead of the capture edge";

        -- ==============================================================
        -- 2. THREE OBSERVERS, ONE FAULT, THREE DIFFERENT ANSWERS.
        -- ==============================================================
        for ipha in 0 to 1 loop
            b_s_bad := score.s_bad; b_i_ok := score.i_ok; b_txn := score.n_txn;
            clear_rules;
            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 ipha = 0 then pha := '0'; else pha := '1'; end if;
                    if ilsb = 0 then lsb := '0'; else lsb := '1'; end if;
                    set_cfg(8, pol, pha, lsb, F_PHASE, PAT_A, 8);
                    run_burst(2);
                end loop;
            end loop;
            d_total(ipha)  := score.n_txn - b_txn;
            d_caught(ipha) := score.s_bad - b_s_bad;
            p_agreed(ipha) := score.i_ok  - b_i_ok;
            r4_fired(ipha) := r_vi(R_LAUNCH);
        end loop;

        report "  the launch/capture swap, by phase:";
        report "    phase   txns   spec monitor disagreed   R4 violations   peeking monitor agreed";
        for ipha in 0 to 1 loop
            report "    CPHA=" & integer'image(ipha) & "  " & integer'image(d_total(ipha)) &
                   "   " & integer'image(d_caught(ipha)) & "   " &
                   integer'image(r4_fired(ipha)) & "   " & integer'image(p_agreed(ipha));
        end loop;

        if d_caught(1) /= d_total(1) then
            report "  FAIL: in CPHA=1 the spec monitor agreed with the intent on " &
                   integer'image(d_total(1) - d_caught(1)) & " of " & integer'image(d_total(1)) &
                   " faulted transactions; the swap shifts the data by a bit position and a monitor sampling the mode's capture edge must see it";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if d_caught(0) /= 0 then
            report "  FAIL: in CPHA=0 the spec monitor disagreed on " &
                   integer'image(d_caught(0)) &
                   " transactions, so the swap DID move the data and the claim that the data check is blind in this phase is wrong";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if r4_fired(0) = 0 or r4_fired(1) = 0 then
            report "  FAIL: R4 did not fire in one of the phases (CPHA=0: " &
                   integer'image(r4_fired(0)) & ", CPHA=1: " & integer'image(r4_fired(1)) &
                   "); the swap puts MOSI in motion at a capture edge and the pin-rule monitor is what must see that";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if p_agreed(0) /= d_total(0) or p_agreed(1) /= d_total(1) then
            report "  FAIL: the peeking monitor failed to reproduce the intent in some transactions, so it is not actually peeking and the comparison proves nothing";
            errors <= errors + 1; wait for 1 ns;
        end if;

        report "    2. the peeking monitor reproduced the intended word on ALL " &
               integer'image(p_agreed(0) + p_agreed(1)) &
               " faulted transactions, in both phases. Same entity, same pins, same instant as the spec monitor -- the only difference is that one was told which edge the SPECIFICATION captures on and the other which edge the DUT ACTUALLY LAUNCHES ON";
        report "       and the failure mode is worse than a missing check: it did not stay silent, it REPORTED THE INTENDED WORD, so a scoreboard fed from it compares two copies of one expectation and passes with total confidence while the pins carry the wrong data";
        report "       the spec monitor caught the swap in CPHA=1, where it shifts the data by a bit position, and was BLIND to it in CPHA=0, where the launch lands on the capture edge: the reconstructed word is perfect and MOSI is in motion at the instant a slave samples it. R4 fired in both phases. The data is not wrong there, the data is UNRELIABLE, and only a rule about WHEN pins move can express that";

        -- ==============================================================
        -- 3. PARTIAL FRAMES, FROM THE EDGE COUNT.
        -- ==============================================================
        b_txn := score.n_txn; b_p := score.p_flag;
        for ipha in 0 to 1 loop
            if ipha = 0 then pha := '0'; else pha := '1'; end if;
            set_cfg(8, '0', pha, '0', F_TRUNC, PAT_A, 8);
            run_burst(2);
        end loop;
        part_txns    := score.n_txn - b_txn;
        part_flagged := score.p_flag - b_p;
        report "  truncated frames: " & integer'image(part_flagged) & " of " &
               integer'image(part_txns) & " flagged partial";
        if part_flagged /= part_txns then
            report "  FAIL: " & integer'image(part_txns - part_flagged) &
                   " truncated frames went unflagged";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    3. every truncated frame was flagged and no legal frame was, which is the pair of results a flag needs: one that fires for nothing has not been shown to work and one that fires for everything is worse than absent. The detection comes from the EDGE count, which a monitor can obtain from the pins -- no DUT signal announces stopping early";

        -- ==============================================================
        -- 4. PASSIVITY, MEASURED.
        -- ==============================================================
        set_cfg(8, '0', '0', '0', F_NONE, PAT_A, 8);
        b_s_bad := score.s_bad;
        hash_clr <= true;  wait until falling_edge(clk);  hash_clr <= false;
        hash_en  <= true;
        run_burst(3);
        hash_en  <= false;
        hash_a    := pin_hash;
        verdict_a := score.s_bad - b_s_bad;

        -- The same stimulus, with the MONITOR told the wrong frame width. The driver's inputs
        -- are untouched.
        set_cfg(8, '0', '0', '0', F_NONE, PAT_A, 7);
        b_s_bad := score.s_bad;
        hash_clr <= true;  wait until falling_edge(clk);  hash_clr <= false;
        hash_en  <= true;
        run_burst(3);
        hash_en  <= false;
        hash_b    := pin_hash;
        verdict_b := score.s_bad - b_s_bad;

        -- The hash is printed as its low 31 bits: `to_integer` on a full 32-bit unsigned
        -- overflows VHDL's INTEGER. The COMPARISON below uses all 32.
        report "  passivity:  run            monitor width   pin hash (low 31 bits)   words judged wrong";
        report "              correct                    8   " &
               integer'image(to_integer(hash_a(30 downto 0))) & "   " & integer'image(verdict_a);
        report "              misconfigured              7   " &
               integer'image(to_integer(hash_b(30 downto 0))) & "   " & integer'image(verdict_b);

        if hash_a /= hash_b then
            report "  FAIL: the pin hash changed when only the monitor's configuration changed, which means the monitor is reaching the pins";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if verdict_a = verdict_b then
            report "  FAIL: misconfiguring the monitor changed no verdict, so the second run did not reconfigure anything and the passivity result is vacuous";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    4. the pin hash was IDENTICAL across the two runs while the verdict changed from " &
               integer'image(verdict_a) & " wrong words to " & integer'image(verdict_b) &
               ". The monitor's configuration reached its own conclusion and did not reach the pins, which is passivity measured rather than asserted -- and the changed verdict is what stops the identical hash from being a result about a monitor that does nothing at all";
        report "       and in VHDL the structural half of the same claim is free: every pin port of the monitor is mode `in`, so the ANALYSER refused any assignment to them before this simulation existed";

        wait for 1 ns;
        if errors = 0 then
            report "PASS: a monitor may read the pins and the protocol's configuration, and may not read the design under test -- and the reason is not tidiness. Across " &
                   integer'image(legal_txns) & " legal transactions in " &
                   integer'image(legal_cfgs) &
                   " configurations this monitor rebuilt every word EXACTLY from the pins alone: data, bit count, and the return path as the complement of the forward path, with no access to the driver's state, shift register or edge decision. Then two instances of the SAME entity were pointed at the SAME pins, differing only in where they got CPHA -- one from the specification, one from what the driver actually does, which is what peeking amounts to in practice -- and a launch/capture swap was injected, the most common SPI driver bug and one that leaves every pin-level timing rule satisfied. The peeking monitor missed it on every transaction in both phases, and missed it in the worst possible way: not by staying silent but by REPORTING THE INTENDED WORD, so a scoreboard fed from it compares two copies of one expectation and passes with total confidence while the wire carries the wrong data. The spec monitor caught it in CPHA=1, where the swap shifts every word by a bit position, and was BLIND to it in CPHA=0, where the launch lands on the capture edge itself, so a monitor that necessarily observes an edge one cycle after the pin moved reads the newly launched bit and rebuilds the intended word perfectly while MOSI is in motion at the exact instant a slave samples it; Chapter 16.1's rule R4 fired in both phases. That split is the honest result and it is Chapter 16.3's subject returning at the transaction level: the data there is not wrong, it is UNRELIABLE, only a rule about WHEN pins move can say so, and the argument is therefore for carrying a transaction monitor and a pin-rule monitor together rather than choosing between them. Truncated frames were flagged on every occurrence and on no legal frame, detected from the EDGE count because no DUT signal announces stopping early. And passivity was measured rather than claimed: the same stimulus run twice with the monitor's width deliberately wrong produced an IDENTICAL hash of every pin transition and a DIFFERENT verdict -- a change that reached the conclusion and not the wires, which is the only form of that claim worth making, and which VHDL's `in` port mode already guarantees structurally"
                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;

9. Why a Verification Engineer Cares

Because the peeking monitor is the failure that no amount of running finds, and it produces a passing report.

The structural habit worth adopting: audit the monitor's inputs, not its logic. A monitor's logic can be reviewed carefully and be entirely correct while the component is useless, because the defect is in where one of its inputs came from. The question to ask of every input is: would somebody with an oscilloscope and a datasheet know this? If the answer is no, the monitor has lost its independence and no line of its body will show it.

The second habit is about redundancy that is not redundant. Two checkers that overlap on most faults and differ on one are worth far more than their overlap suggests, and the CPHA=0 result is the example: the data check is blind, the rule check is not, and a team that deleted the rule checker as "covered by the scoreboard" would have shipped a master that races its slave in one of four modes.

10. Why an FPGA or ASIC Engineer Cares

Because the CPHA=0 result describes a real silicon failure with a clean simulation.

A master that changes MOSI on the same edge the slave samples it produces perfect data in simulation — the monitor reads the new value, the slave model reads the new value, everything agrees — and on silicon the slave's setup window is violated and the bit is whatever the process, voltage and temperature decide. The functional report is green and the bench measurement is intermittent.

That is why the pin-level rule matters to a designer rather than only to a verification engineer: R4 is a setup-window requirement in disguise, and it is the only check in this environment that can see it.

11. Failure Signature — A Scoreboard That Agrees With A Broken Design

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom          a design defect is reported from the lab. The suite has a
                    monitor and a scoreboard covering the exact path. Both are
                    green, on the failing traffic, reproduced in simulation.

   What happened    the monitor takes one input from inside the DUT -- an edge
                    select, a valid, a mode -- so it observes the design's
                    intent rather than the bus. It reconstructs what the design
                    MEANT and the scoreboard compares that against what the
                    design meant.

   What would have  an audit of the monitor's inputs against a single question:
   caught it        would somebody with a scope and a datasheet know this? Any
                    input that fails that question is peeking.

   The tell         the scoreboard is not silent, it is CONFIDENT. It reports
                    matches on the failing transactions. A silent checker looks
                    suspicious in a report; a checker reporting successful
                    comparisons on broken traffic looks like proof the traffic
                    is fine.

12. Common Misconceptions

"A monitor needs to know how the DUT works in order to interpret the bus." It needs the specification. Every configuration input in this monitor is something a datasheet states; the moment one of them describes this particular implementation, the monitor can no longer disagree with it.

"A peeking monitor is at least a partial check." It is worse than none, because it produces positive evidence. A missing check is visible as a gap; a check that reports matches on broken traffic is indistinguishable from proof.

"If the reconstructed data is right, the bus is fine." In CPHA=0 the swap fault produces perfectly reconstructed data on a bus a real slave would sample during a transition. Correct data and unreliable data look identical to a data check.

"A transaction monitor makes a pin-rule checker redundant." The two have different blind spots, demonstrated here on one fault: the data check sees the CPHA=1 shift and not the CPHA=0 race; R4 sees both. Deleting either one loses a fault class.

"Passivity is obvious from reading the code." It is obvious from reading this code, which is why it is measured instead: an identical pin hash across two runs that differ only in the monitor's configuration, together with a verdict that changed. Reading establishes intent; the hash establishes behaviour.

"Bit count is enough to detect a truncated frame." A master that stops mid-bit can leave a bit count that looks right in one phase. The edge count's parity catches that shape, and the two checks together cost nothing.

13. Reason It Through

In CPHA=0 the swap fault produced perfectly reconstructed words. Explain why, from the monitor's sampling instant.

The faulted driver launches on the leading edge, which is CPHA=0's capture edge. A monitor detects an edge on the cycle after the pin changed — that is when the transition is visible — so when it samples at the capture edge it reads the value the driver has just launched. That value is the correct next bit, so the word is right. What the monitor cannot see is that the pin was moving at the instant a slave samples it.

Why does the monitor count edges as well as bits?

Because they fail differently. A frame stopped mid-bit leaves an odd edge count, which no bit count can express; a frame whose width disagrees with the configuration leaves a wrong bit count, which the edge parity may not show. Both tests are one comparison each.

The passivity experiment required two results. Why is the identical hash insufficient alone?

Because a monitor that does nothing at all also produces an identical hash. The changed verdict proves the second run really did reconfigure something, which is what makes the unchanged hash a statement about the monitor's reach rather than about its inactivity.

Given the CPHA=0 result, argue for keeping Chapter 16.1's rule monitor in a suite that already has a working scoreboard.

Because R4 catches a fault class the scoreboard is structurally blind to: a pin in motion at a capture instant. The scoreboard compares values, and the values are correct; the defect is about timing, and only a timing rule can express it. The two checkers overlap on most faults, which is exactly why the fault they do not share is easy to argue away and expensive to lose.

Where would a real project accidentally build the peeking monitor?

Any of: a monitor handed a launch_edge or phase signal from the DUT for convenience; a monitor whose author resolved an ambiguity by reading the RTL; a config object that grew a field describing the implementation; or a monitor that was adjusted every time it disagreed until it agreed. None of these look like peeking in a diff.

14. Understanding Check

15. Summary

A monitor may read the pins and the specification, and the reason is not tidiness. Across 32 legal transactions in sixteen configurations this monitor rebuilt every word exactly from pins alone — data, bit count, and the return path as the complement of the forward path. Then two instances of the same module on the same pins, differing only in where they got CPHA, were shown a launch/capture swap: the peeking one reported the intended word on every transaction in both phases, which is worse than silence because it produces positive evidence for broken traffic. The specification-configured one caught the swap in CPHA=1, where it shifts every word, and was blind to it in CPHA=0, where the launch lands on the capture edge and a monitor that necessarily observes an edge a cycle late rebuilds the intended word perfectly while MOSI moves at the instant a slave samples it — a case R4 caught in both phases. Truncated frames were flagged on every occurrence and on none of the legal ones, detected from the edge count because no DUT signal announces stopping early. And passivity was measured rather than claimed: an identical hash of every pin transition across two runs, with a verdict that changed.

16. What Comes Next

The monitor says what happened. Something has to say what should have happened, and independently. Chapter 16.6 builds that, and builds the version that agrees with a broken design.

Continue learning