Skip to content
VLSI Mentor

SPI · Module 18

CS Timing Faults and Partial Frames

The only fault family in this module whose evidence lives in a different frame from its symptom. A frame cut short leaves orphan bits, and the NEXT frame is structurally flawless and carries the wrong byte.

Everything so far has diagnosed a capture, or a pair of captures of the same read. This chapter breaks that pattern, because chip-select faults do something none of the others do.

The frame whose data is wrong is not the frame that is broken.

1. What This Chapter Is Not About

Chapter 18.1's triage already measures the lead and the lag of a single select — whether CS fell early enough before the first clock edge and rose late enough after the last — and reports EV_CSBND when either is short. That is a property of one capture and it is settled.

This chapter is about framing across a sequence: how many times CS asserted, how many clocks each assertion carried, and what one assertion leaves behind for the next.

2. The Mechanism Is A Design Decision, Not A Protocol Rule

A slave's bit counter has to be cleared by something. There are two choices:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   clear on every CS assertion    every frame starts from a known state
   clear only on reset            the counter just keeps counting

The second is extremely common. Such slaves work perfectly as long as the master sends whole words, and the specification does not require anything else — SPI has no frame-length field, no counter reset command, and no way for a slave to know how long a word is supposed to be except by counting clocks.

3. The Symptom Arrives One Frame Late

The slave's bit counter does not clear at the select

14 cycles
Five rows over fourteen cycles spanning two chip-select assertions. The first ends after its fifth clock; the slave's bit-count row stops at five and holds through the gap. The second assertion is well formed, and on its third clock the bit count reaches eight and a word-announced marker fires.truncated frametruncated frameflawless frame, wrong dataflawless frame, wrong datarelease after 5 of 8 clocksrelease after 5 of 8 clocksnext select — nothing is clearednext select — nothing isclearedword = 5 old bits + 3 new bitsword = 5 old bits + 3 newbitscs_nsclkmosib3b3b4b4XXXXc0c0c1c1c2c2bit countn=3n=4n=4n=5n=5n=5n=5n=5n=5n=6n=6n=7n=7n=0announceWORDt0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 1 — the boundary between a truncated frame and the flawless frame that follows it. The truncated frame is released after 5 of its 8 clocks, so the slave's bit counter stops at 5. The select does not clear it. Three clocks into the next frame the counter reaches 8 and a word is announced — five bits of the old payload and three of the new. Horizontal scrolling is expected on a narrow screen; the figure is a timeline.

Everything visible inside the second assertion is correct. The fault is the number n=5 at the moment it begins — a count of bits left over by a frame that has already gone past. That number is what the decoder below publishes as carry_in, and it is the whole chapter in one value.

4. Six Framing Verdicts

VerdictClocks in this assertionMeaning
D_OKexactly a wordclean, and nothing inherited
D_SHORTfewertruncated; leaves orphan bits
D_GLITCHfewer, and it completes the previous wordone word split by a spurious release
D_MERGEDan exact multipleseveral words in one assertion — a missing release
D_RAGGEDmore, not a multiplemerged and truncated at once
D_INHERITEDexactly a wordflawless, and carrying inherited orphan bits

D_INHERITED is the one that matters. It describes a frame as structurally perfect and its data as wrong in the same verdict, and nothing inside that frame supports the second half of the statement.

5. The Measurement

Five captures, seventeen framing intervals, identical output from all three languages:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cap   iv   edges   c_in   c_out   ann   word   verdict       expected      note
  -- capture A: five frames, the THIRD cut short after 5 of 8 clocks
   0    0       8      0       0     1    8d   OK            OK            a whole word
   0    1       8      0       0     1    c3   OK            OK            a whole word
   0    2       5      0       5     0    c3   SHORT         SHORT         CUT SHORT -- leaves 5 orphan bits
   0    3       8      5       5     1    59   INHERITED     INHERITED     flawless frame, WRONG DATA
   0    4       8      5       5     1    e7   INHERITED     INHERITED     still poisoned -- only a reset clears it
  -- capture B: a MISSING release -- two words inside one assertion
   1    5      16      0       0     2    c3   MERGED        MERGED        16 clocks, 2 words, 1 assertion
   1    6       8      0       0     1    5a   OK            OK            a whole word
   1    7       8      0       0     1    3c   OK            OK            a whole word
  -- capture C: a SPURIOUS release -- one word split across two assertions
   2    8       3      0       3     0    00   SHORT         SHORT         3 clocks then CS released
   2    9       5      3       0     1    8d   GLITCH        GLITCH        5 more -- together exactly one word
   2   10       8      0       0     1    c3   OK            OK            a whole word
  -- capture D: 11 clocks in one assertion -- merged and truncated at once
   3   11      11      0       3     1    8d   RAGGED        RAGGED        one word plus 3 orphan bits
   3   12       8      3       3     1    cb   INHERITED     INHERITED     flawless frame, WRONG DATA
   3   13       8      3       3     1    47   INHERITED     INHERITED     still poisoned
  -- capture E: the SAME truncation as capture A, positioned LAST
   4   14       8      0       0     1    8d   OK            OK            a whole word
   4   15       8      0       0     1    c3   OK            OK            a whole word
   4   16       5      0       5     0    c3   SHORT         SHORT         CUT SHORT -- and nothing follows it

Interval 3 is the chapter. Eight clocks, nothing inherited within it, and it announced 0x59 where its payload was 0x3c. The mixed byte is computed by the bench from the definition and required to match the decoder's prediction, so the data is wrong here and the fault is there is a measured statement with two numbers behind it.

6. The Poisoning Persists — And What That Sounds Like In A Bug Report

Interval 4 inherited the same five orphan bits and announced 0xe7. The residue does not drain, because the counter that holds it is cleared by reset and by nothing else. Every word from the truncation onward is a mix.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   symptom in the field:   "the link works, then goes wrong, and stays wrong"
                           "power-cycling it fixes it"
                           "it comes back after a while"

7. Merge Against Glitch: A Ratio, Not A Per-Frame Check

Captures B and C are the two framing faults that leave every individual edge count defensible.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   capture B    3 assertions delivered 4 words     words OUTNUMBER assertions  → a MISSING release
   capture C    3 assertions delivered 2 words     assertions outnumber words  → a SPURIOUS release

Look at capture C's edge counts on their own: 3, then 5, then 8. Not one of those is obviously wrong — a 3-clock transfer and a 5-clock transfer are legal SPI, just not what this link uses. A per-frame checker configured to expect 8 clocks flags two short frames and stops there. The fault is visible only as a ratio across the capture, and the sign of asserts − words names which of the two it is.

8. The Result That Was Not Designed In

Capture C produced something the chapter did not set out to show: the split word came back correct. Interval 9 announced 0x8d, exactly the payload interval 8 began.

Of course it did. A slave that does not clear on select simply carries on counting, so a spurious release costs it nothing at all. Which means:

Slave designA truncated frameA spurious release
clears only on resetpoisoned indefinitelysurvives it, data intact
clears on every selectloses one word, then recoverscorrupted, and cannot see it

9. The End Of A Capture Is A Blind Spot

Capture E contains the identical truncation as capture A, positioned last.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   mid-sequence   SHORT, then INHERITED, then INHERITED     3 non-clean verdicts
   at the end     SHORT                                     1 non-clean verdict

The structural evidence survives — a short frame is a short frame. The data symptom never appears, because there is no following frame left to corrupt. So a capture that stops at the fault shows a short frame with no visible consequence, and a capture that stops one frame earlier shows nothing at all.

That makes capture more than you think you need a measured requirement rather than folklore, and it has a specific form: for this fault family the buffer must extend at least one full frame past the suspect event. A trigger set on the mismatch itself, with the buffer centred on the trigger, is the wrong configuration — the evidence is before the trigger, and it is not a data event.

10. Building It — Three HDLs

The modelled slave inside the decoder deliberately does not clear on CS assertion, because that is the design under suspicion. What the module produces is a prediction — this is the word a slave that does not clear on select would have reported — and the bench checks that prediction against an independent model of the same behaviour, announcement for announcement, on all seventeen intervals.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_diag.sv — the framing decoder — six verdicts, one of which calls a frame flawless and wrong at the same time
// spi_cs_diag.sv
//
// Chapter 18.5 -- chip select, and the only fault family in this module whose EVIDENCE lives in a
// different frame from its SYMPTOM.
//
// WHAT THIS CHAPTER IS NOT ABOUT. Chapter 18.1 already measures the lead and the lag of a single
// select -- whether CS fell early enough before the first edge and rose late enough after the last --
// and reports `EV_CSBND` when either is short. That is a property of ONE capture and it is settled.
//
// This chapter is about framing across a SEQUENCE: how many times CS asserted, how many clocks each
// assertion carried, and what one assertion leaves behind for the next.
//
// THE MECHANISM, AND IT IS A DESIGN DECISION RATHER THAN A PROTOCOL RULE.
//
// A slave's bit counter has to be cleared by something. The obvious choice is the select -- clear on
// every CS assertion, so every frame starts from a known state. Very many real slaves do not do that:
// they clear only on reset and rely on the master sending whole words. Those slaves work perfectly
// until a frame is truncated, and then:
//
//     frame N     is cut short after 5 of 8 clocks   -> the slave holds 5 orphan bits
//     frame N+1   is PERFECT -- 8 clocks, clean lead and lag, clean edges --
//                 and the word the slave announces during it is 5 bits of frame N
//                 followed by 3 bits of frame N+1
//
// So the frame whose DATA is wrong is not the frame that is BROKEN. An engineer who captures the
// mismatching transaction captures frame N+1, finds nothing wrong with it, and concludes the slave is
// unreliable. The evidence is one frame earlier, and it is structural rather than data.
//
// Worse, the residue PERSISTS. Every subsequent word is a mix, so the symptom is a stream that is
// permanently wrong until something resets the slave -- which is exactly the behaviour that gets
// described as "it works after a power cycle" and then filed as a supply problem.
//
// WHAT THIS MODULE PUBLISHES.
//
//     ob_edges      leading edges in this assertion
//     ob_carry_in   orphan bits this assertion INHERITED from the one before
//     ob_carry_out  orphan bits it leaves behind
//     ob_words      words the modelled slave announced during it
//     ob_word       the last word it announced -- the corrupted value, computed rather than guessed
//     dg_code       the framing verdict
//
// `ob_carry_in` is the whole chapter in one number. A frame diagnosed `D_INHERITED` is structurally
// flawless and carries wrong data, and the only thing that says so is a count of bits left over by a
// frame that has already gone past.
//
// THE MODELLED SLAVE IS A PREDICTION, NOT AN ASSUMPTION. The accumulator here deliberately does NOT
// clear on CS assertion, because that is the design under suspicion. What it produces is "this is the
// word a slave that does not clear on select would have reported", and the bench checks that
// prediction against an independent model of the same behaviour. A diagnostic that assumed the
// correct design would see nothing wrong with any of these captures.

`timescale 1ns/1ps

module spi_cs_diag #(
    parameter int DW    = 32,
    parameter int NB    = 8,     // the word length the link is supposed to use
    parameter int CNT_W = 8
) (
    input  wire              clk,
    input  wire              rst_n,

    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,

    input  wire              cpol,
    input  wire              cpha,

    output reg               dg_valid,     // one pulse per CS assertion, at its release
    output reg  [2:0]        dg_code,

    output reg  [CNT_W-1:0]  ob_edges,
    output reg  [CNT_W-1:0]  ob_carry_in,
    output reg  [CNT_W-1:0]  ob_carry_out,
    output reg  [CNT_W-1:0]  ob_words,     // announcements during this assertion
    output reg  [DW-1:0]     ob_word,      // the last one -- the value the slave would report

    // Running totals for the whole capture. The SIGN of (asserts - words) is the observation that
    // separates a merge from a glitch, and neither is visible inside one assertion.
    output reg  [CNT_W-1:0]  ob_asserts,
    output reg  [CNT_W-1:0]  ob_words_tot
);

    localparam [2:0] D_OK        = 3'd0,
                     // This assertion carried fewer than a whole word and left orphan bits behind.
                     D_SHORT     = 3'd1,
                     // Fewer than a word, and it COMPLETED the word the previous assertion started --
                     // so the two together carried exactly one word split by a spurious release.
                     D_GLITCH    = 3'd2,
                     // More than a word, an exact multiple: several words in one assertion because no
                     // release separated them.
                     D_MERGED    = 3'd3,
                     // More than a word and not a multiple: merged AND truncated.
                     D_RAGGED    = 3'd4,
                     // EXACTLY a word, clean in every respect, and carrying inherited orphan bits.
                     // This is the frame whose data is wrong, and nothing inside it says so.
                     D_INHERITED = 3'd5;

    reg             sclk_d, cs_n_d;
    reg [CNT_W-1:0] edges, carry_in, words;
    reg [CNT_W-1:0] nbits;          // the modelled slave's bit counter -- cleared ONLY by reset
    reg [DW-1:0]    acc, last_word;

    wire cs_assert   =  cs_n_d & ~cs_n;
    wire cs_deassert = ~cs_n_d &  cs_n;
    wire in_txn      = ~cs_n | cs_deassert;
    wire sclk_edge   = (sclk !== sclk_d);
    wire leading     = sclk_edge && (sclk !== cpol);
    wire capture     = (cpha ? (sclk_edge && !leading) : leading) && in_txn;

    // `(carry_in + edges) mod NB` -- the orphan bits this assertion will leave. Written as a
    // subtraction chain rather than a modulo because NB is a parameter and a divider is not wanted in
    // a diagnostic that may sit permanently in a controller.
    function [CNT_W-1:0] modnb(input [CNT_W+3:0] v);
        reg [CNT_W+3:0] t;
        integer i;
        begin
            t = v;
            for (i = 0; i < (1 << (CNT_W-2)); i = i + 1)
                if (t >= NB) t = t - NB;
            modnb = t[CNT_W-1:0];
        end
    endfunction

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_d       <= 1'b0;
            cs_n_d       <= 1'b1;
            edges        <= {CNT_W{1'b0}};
            carry_in     <= {CNT_W{1'b0}};
            words        <= {CNT_W{1'b0}};
            nbits        <= {CNT_W{1'b0}};
            acc          <= {DW{1'b0}};
            last_word    <= {DW{1'b0}};
            dg_valid     <= 1'b0;
            dg_code      <= D_OK;
            ob_edges     <= {CNT_W{1'b0}};
            ob_carry_in  <= {CNT_W{1'b0}};
            ob_carry_out <= {CNT_W{1'b0}};
            ob_words     <= {CNT_W{1'b0}};
            ob_word      <= {DW{1'b0}};
            ob_asserts   <= {CNT_W{1'b0}};
            ob_words_tot <= {CNT_W{1'b0}};
        end else begin
            dg_valid <= 1'b0;

            if (cs_assert) begin
                // The orphan count is sampled HERE, at the start of the interval it describes. Reading
                // it at the release would report the count this interval leaves, not the one it
                // inherited -- and the whole point of the number is that it belongs to the past.
                carry_in   <= nbits;
                edges      <= {CNT_W{1'b0}};
                words      <= {CNT_W{1'b0}};
                ob_asserts <= ob_asserts + 1'b1;
                // NOTE WHAT IS NOT CLEARED. `nbits` and `acc` survive the select, because the slave
                // under suspicion does not clear them. Clearing them here would model the CORRECT
                // design and this module would report every capture below as clean.
            end else if (capture) begin
                edges <= edges + 1'b1;
                if (nbits + 1'b1 == NB[CNT_W-1:0]) begin
                    last_word    <= {acc[DW-2:0], mosi};
                    nbits        <= {CNT_W{1'b0}};
                    acc          <= {acc[DW-2:0], mosi};
                    words        <= words + 1'b1;
                    ob_words_tot <= ob_words_tot + 1'b1;
                end else begin
                    acc   <= {acc[DW-2:0], mosi};
                    nbits <= nbits + 1'b1;
                end
            end

            if (cs_deassert) begin
                dg_valid     <= 1'b1;
                ob_edges     <= edges;
                ob_carry_in  <= carry_in;
                ob_carry_out <= nbits;
                ob_words     <= words;
                ob_word      <= last_word;

                if ((edges < NB[CNT_W-1:0]) && ((carry_in + edges) == NB[CNT_W-1:0]))
                    dg_code <= D_GLITCH;
                else if (edges < NB[CNT_W-1:0])
                    dg_code <= D_SHORT;
                else if ((edges > NB[CNT_W-1:0]) && (modnb({4'b0, carry_in + edges}) == {CNT_W{1'b0}})
                         && (carry_in == {CNT_W{1'b0}}))
                    dg_code <= D_MERGED;
                else if (edges > NB[CNT_W-1:0])
                    dg_code <= D_RAGGED;
                else if (carry_in != {CNT_W{1'b0}})
                    dg_code <= D_INHERITED;
                else
                    dg_code <= D_OK;
            end

            sclk_d <= sclk;
            cs_n_d <= cs_n;
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_diag.v — the same design in Verilog-2001
// spi_cs_diag.v
//
// Chapter 18.5 -- chip select, and the only fault family in this module whose EVIDENCE lives in a
// different frame from its SYMPTOM.
//
// WHAT THIS CHAPTER IS NOT ABOUT. Chapter 18.1 already measures the lead and the lag of a single
// select -- whether CS fell early enough before the first edge and rose late enough after the last --
// and reports `EV_CSBND` when either is short. That is a property of ONE capture and it is settled.
//
// This chapter is about framing across a SEQUENCE: how many times CS asserted, how many clocks each
// assertion carried, and what one assertion leaves behind for the next.
//
// THE MECHANISM, AND IT IS A DESIGN DECISION RATHER THAN A PROTOCOL RULE.
//
// A slave's bit counter has to be cleared by something. The obvious choice is the select -- clear on
// every CS assertion, so every frame starts from a known state. Very many real slaves do not do that:
// they clear only on reset and rely on the master sending whole words. Those slaves work perfectly
// until a frame is truncated, and then:
//
//     frame N     is cut short after 5 of 8 clocks   -> the slave holds 5 orphan bits
//     frame N+1   is PERFECT -- 8 clocks, clean lead and lag, clean edges --
//                 and the word the slave announces during it is 5 bits of frame N
//                 followed by 3 bits of frame N+1
//
// So the frame whose DATA is wrong is not the frame that is BROKEN. An engineer who captures the
// mismatching transaction captures frame N+1, finds nothing wrong with it, and concludes the slave is
// unreliable. The evidence is one frame earlier, and it is structural rather than data.
//
// Worse, the residue PERSISTS. Every subsequent word is a mix, so the symptom is a stream that is
// permanently wrong until something resets the slave -- which is exactly the behaviour that gets
// described as "it works after a power cycle" and then filed as a supply problem.
//
// WHAT THIS MODULE PUBLISHES.
//
//     ob_edges      leading edges in this assertion
//     ob_carry_in   orphan bits this assertion INHERITED from the one before
//     ob_carry_out  orphan bits it leaves behind
//     ob_words      words the modelled slave announced during it
//     ob_word       the last word it announced -- the corrupted value, computed rather than guessed
//     dg_code       the framing verdict
//
// `ob_carry_in` is the whole chapter in one number. A frame diagnosed `D_INHERITED` is structurally
// flawless and carries wrong data, and the only thing that says so is a count of bits left over by a
// frame that has already gone past.
//
// THE MODELLED SLAVE IS A PREDICTION, NOT AN ASSUMPTION. The accumulator here deliberately does NOT
// clear on CS assertion, because that is the design under suspicion. What it produces is "this is the
// word a slave that does not clear on select would have reported", and the bench checks that
// prediction against an independent model of the same behaviour. A diagnostic that assumed the
// correct design would see nothing wrong with any of these captures.

`timescale 1ns/1ps

module spi_cs_diag #(
    parameter DW    = 32,
    parameter NB    = 8,     // the word length the link is supposed to use
    parameter CNT_W = 8
) (
    input  wire              clk,
    input  wire              rst_n,

    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,

    input  wire              cpol,
    input  wire              cpha,

    output reg               dg_valid,     // one pulse per CS assertion, at its release
    output reg  [2:0]        dg_code,

    output reg  [CNT_W-1:0]  ob_edges,
    output reg  [CNT_W-1:0]  ob_carry_in,
    output reg  [CNT_W-1:0]  ob_carry_out,
    output reg  [CNT_W-1:0]  ob_words,     // announcements during this assertion
    output reg  [DW-1:0]     ob_word,      // the last one -- the value the slave would report

    // Running totals for the whole capture. The SIGN of (asserts - words) is the observation that
    // separates a merge from a glitch, and neither is visible inside one assertion.
    output reg  [CNT_W-1:0]  ob_asserts,
    output reg  [CNT_W-1:0]  ob_words_tot
);

    localparam [2:0] D_OK        = 3'd0,
                     // This assertion carried fewer than a whole word and left orphan bits behind.
                     D_SHORT     = 3'd1,
                     // Fewer than a word, and it COMPLETED the word the previous assertion started --
                     // so the two together carried exactly one word split by a spurious release.
                     D_GLITCH    = 3'd2,
                     // More than a word, an exact multiple: several words in one assertion because no
                     // release separated them.
                     D_MERGED    = 3'd3,
                     // More than a word and not a multiple: merged AND truncated.
                     D_RAGGED    = 3'd4,
                     // EXACTLY a word, clean in every respect, and carrying inherited orphan bits.
                     // This is the frame whose data is wrong, and nothing inside it says so.
                     D_INHERITED = 3'd5;

    reg             sclk_d, cs_n_d;
    reg [CNT_W-1:0] edges, carry_in, words;
    reg [CNT_W-1:0] nbits;          // the modelled slave's bit counter -- cleared ONLY by reset
    reg [DW-1:0]    acc, last_word;

    wire cs_assert   =  cs_n_d & ~cs_n;
    wire cs_deassert = ~cs_n_d &  cs_n;
    wire in_txn      = ~cs_n | cs_deassert;
    wire sclk_edge   = (sclk !== sclk_d);
    wire leading     = sclk_edge && (sclk !== cpol);
    wire capture     = (cpha ? (sclk_edge && !leading) : leading) && in_txn;

    // `(carry_in + edges) mod NB` -- the orphan bits this assertion will leave. Written as a
    // subtraction chain rather than a modulo because NB is a parameter and a divider is not wanted in
    // a diagnostic that may sit permanently in a controller.
        function [CNT_W-1:0] modnb;
        input [CNT_W+3:0] v;
        reg [CNT_W+3:0] t;
        integer i;
        begin
            t = v;
            for (i = 0; i < (1 << (CNT_W-2)); i = i + 1)
                if (t >= NB) t = t - NB;
            modnb = t[CNT_W-1:0];
        end
    endfunction

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_d       <= 1'b0;
            cs_n_d       <= 1'b1;
            edges        <= {CNT_W{1'b0}};
            carry_in     <= {CNT_W{1'b0}};
            words        <= {CNT_W{1'b0}};
            nbits        <= {CNT_W{1'b0}};
            acc          <= {DW{1'b0}};
            last_word    <= {DW{1'b0}};
            dg_valid     <= 1'b0;
            dg_code      <= D_OK;
            ob_edges     <= {CNT_W{1'b0}};
            ob_carry_in  <= {CNT_W{1'b0}};
            ob_carry_out <= {CNT_W{1'b0}};
            ob_words     <= {CNT_W{1'b0}};
            ob_word      <= {DW{1'b0}};
            ob_asserts   <= {CNT_W{1'b0}};
            ob_words_tot <= {CNT_W{1'b0}};
        end else begin
            dg_valid <= 1'b0;

            if (cs_assert) begin
                // The orphan count is sampled HERE, at the start of the interval it describes. Reading
                // it at the release would report the count this interval leaves, not the one it
                // inherited -- and the whole point of the number is that it belongs to the past.
                carry_in   <= nbits;
                edges      <= {CNT_W{1'b0}};
                words      <= {CNT_W{1'b0}};
                ob_asserts <= ob_asserts + 1'b1;
                // NOTE WHAT IS NOT CLEARED. `nbits` and `acc` survive the select, because the slave
                // under suspicion does not clear them. Clearing them here would model the CORRECT
                // design and this module would report every capture below as clean.
            end else if (capture) begin
                edges <= edges + 1'b1;
                if (nbits + 1'b1 == NB[CNT_W-1:0]) begin
                    last_word    <= {acc[DW-2:0], mosi};
                    nbits        <= {CNT_W{1'b0}};
                    acc          <= {acc[DW-2:0], mosi};
                    words        <= words + 1'b1;
                    ob_words_tot <= ob_words_tot + 1'b1;
                end else begin
                    acc   <= {acc[DW-2:0], mosi};
                    nbits <= nbits + 1'b1;
                end
            end

            if (cs_deassert) begin
                dg_valid     <= 1'b1;
                ob_edges     <= edges;
                ob_carry_in  <= carry_in;
                ob_carry_out <= nbits;
                ob_words     <= words;
                ob_word      <= last_word;

                if ((edges < NB[CNT_W-1:0]) && ((carry_in + edges) == NB[CNT_W-1:0]))
                    dg_code <= D_GLITCH;
                else if (edges < NB[CNT_W-1:0])
                    dg_code <= D_SHORT;
                else if ((edges > NB[CNT_W-1:0]) && (modnb({4'b0, carry_in + edges}) == {CNT_W{1'b0}})
                         && (carry_in == {CNT_W{1'b0}}))
                    dg_code <= D_MERGED;
                else if (edges > NB[CNT_W-1:0])
                    dg_code <= D_RAGGED;
                else if (carry_in != {CNT_W{1'b0}})
                    dg_code <= D_INHERITED;
                else
                    dg_code <= D_OK;
            end

            sclk_d <= sclk;
            cs_n_d <= cs_n;
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_diag.vhd — the same design in VHDL
-- spi_cs_diag.vhd
--
-- Chapter 18.5 -- chip select, and the only fault family in this module whose EVIDENCE lives in a
-- different frame from its SYMPTOM.
--
-- WHAT THIS CHAPTER IS NOT ABOUT. Chapter 18.1 already measures the lead and the lag of a single
-- select -- whether CS fell early enough before the first edge and rose late enough after the last --
-- and reports `EV_CSBND` when either is short. That is a property of ONE capture and it is settled.
--
-- This chapter is about framing across a SEQUENCE: how many times CS asserted, how many clocks each
-- assertion carried, and what one assertion leaves behind for the next.
--
-- THE MECHANISM, AND IT IS A DESIGN DECISION RATHER THAN A PROTOCOL RULE.
--
-- A slave's bit counter has to be cleared by something. The obvious choice is the select -- clear on
-- every CS assertion, so every frame starts from a known state. Very many real slaves do not do that:
-- they clear only on reset and rely on the master sending whole words. Those slaves work perfectly
-- until a frame is truncated, and then:
--
--     frame N     is cut short after 5 of 8 clocks   -> the slave holds 5 orphan bits
--     frame N+1   is PERFECT -- 8 clocks, clean lead and lag, clean edges --
--                 and the word the slave announces during it is 5 bits of frame N
--                 followed by 3 bits of frame N+1
--
-- So the frame whose DATA is wrong is not the frame that is BROKEN. An engineer who captures the
-- mismatching transaction captures frame N+1, finds nothing wrong with it, and concludes the slave is
-- unreliable. The evidence is one frame earlier, and it is structural rather than data.
--
-- Worse, the residue PERSISTS. Every subsequent word is a mix, so the symptom is a stream that is
-- permanently wrong until something resets the slave -- which is exactly the behaviour that gets
-- described as "it works after a power cycle" and then filed as a supply problem.
--
-- WHAT THIS MODULE PUBLISHES.
--
--     ob_edges      leading edges in this assertion
--     ob_carry_in   orphan bits this assertion INHERITED from the one before
--     ob_carry_out  orphan bits it leaves behind
--     ob_words      words the modelled slave announced during it
--     ob_word       the last word it announced -- the corrupted value, computed rather than guessed
--     dg_code       the framing verdict
--
-- `ob_carry_in` is the whole chapter in one number. A frame diagnosed `D_INHERITED` is structurally
-- flawless and carries wrong data, and the only thing that says so is a count of bits left over by a
-- frame that has already gone past.
--
-- THE MODELLED SLAVE IS A PREDICTION, NOT AN ASSUMPTION. The accumulator here deliberately does NOT
-- clear on CS assertion, because that is the design under suspicion. What it produces is "this is the
-- word a slave that does not clear on select would have reported", and the bench checks that
-- prediction against an independent model of the same behaviour. A diagnostic that assumed the
-- correct design would see nothing wrong with any of these captures.

--
-- WHAT THE VHDL VERSION ADDS. `mod` is a language operator here, so the orphan-bit arithmetic is one
-- expression instead of the subtraction loop the Verilog versions need -- and the loop in those
-- versions is not stylistic: a run-time modulo by a parameter infers a divider, which is not wanted in
-- a diagnostic meant to sit permanently in a controller. The VHDL reads better and synthesises to the
-- same structure only because `NB_C` is a constant; a variable divisor would cost the same there.
--
-- The verdict is an enumeration whose six names include two that describe a frame as PERFECT and WRONG
-- at once -- `D_INHERITED` -- and having that sit in a named type is worth more than a comment.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). Generics are `NB_C` and `CNT_W`; the process variables
-- are `edges`, `carry_in`, `words`, `nbits`, `acc`. None of those is `NB`/`nb` or `CNT`/`cnt`, because a
-- variable that differs from a generic only in case IS that generic -- the failure Chapter 17.4 spent
-- an afternoon on, where a variable `tries` silently became the generic `TRIES` and a rejection loop
-- stopped executing with no diagnostic anywhere. Every declaration below was re-read against it.

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

package spi_cs_pkg is

    constant DW_C : natural := 32;

    -- Six framing verdicts.
    --
    --   D_SHORT      fewer clocks than a word; leaves orphan bits behind
    --   D_GLITCH     fewer, and it COMPLETED the previous assertion's word -- one word, two selects
    --   D_MERGED     an exact multiple of a word: several words, one select
    --   D_RAGGED     more than a word and not a multiple: merged and truncated at once
    --   D_INHERITED  EXACTLY a word, clean in every respect, and carrying inherited orphan bits.
    --                This is the frame whose data is wrong, and nothing inside it says so.
    type cs_diag_t is (D_OK, D_SHORT, D_GLITCH, D_MERGED, D_RAGGED, D_INHERITED);

    function diag_name (d : cs_diag_t) return string;

end package spi_cs_pkg;

package body spi_cs_pkg is
    function diag_name (d : cs_diag_t) return string is
    begin
        case d is
            when D_OK     => return "OK          ";
            when D_SHORT  => return "SHORT       ";
            when D_GLITCH => return "GLITCH      ";
            when D_MERGED => return "MERGED      ";
            when D_RAGGED => return "RAGGED      ";
            when others   => return "INHERITED   ";
        end case;
    end function diag_name;
end package body spi_cs_pkg;

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

entity spi_cs_diag is
    generic (
        NB_C  : positive := 8;      -- the word length the link is supposed to use
        CNT_W : positive := 8
    );
    port (
        clk   : in  std_logic;
        rst_n : in  std_logic;

        sclk  : in  std_logic;
        cs_n  : in  std_logic;
        mosi  : in  std_logic;

        cpol  : in  std_logic;
        cpha  : in  std_logic;

        dg_valid : out std_logic;   -- one pulse per CS assertion, at its release
        dg_code  : out cs_diag_t;

        ob_edges      : out natural;
        ob_carry_in   : out natural;
        ob_carry_out  : out natural;
        ob_words      : out natural;
        ob_word       : out std_logic_vector(DW_C - 1 downto 0);

        -- Running totals for the whole capture. The SIGN of (asserts - words) separates a merge from a
        -- glitch, and neither is visible inside one assertion.
        ob_asserts    : out natural;
        ob_words_tot  : out natural
    );
end entity spi_cs_diag;

architecture rtl of spi_cs_diag is
    signal v_r    : std_logic := '0';
    signal d_r    : cs_diag_t := D_OK;
    signal e_r, ci_r, co_r, w_r : natural := 0;
    signal wd_r   : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
    signal as_r, wt_r : natural := 0;
begin

    dg_valid     <= v_r;
    dg_code      <= d_r;
    ob_edges     <= e_r;
    ob_carry_in  <= ci_r;
    ob_carry_out <= co_r;
    ob_words     <= w_r;
    ob_word      <= wd_r;
    ob_asserts   <= as_r;
    ob_words_tot <= wt_r;

    process (clk, rst_n) is
        variable sclk_d, cs_n_d         : std_logic;
        variable cs_assert, cs_deassert : boolean;
        variable in_txn, sclk_edge      : boolean;
        variable leading, capture       : boolean;
        variable edges, carry_in, words : natural;
        variable nbits                  : natural;   -- the modelled slave's counter: reset-cleared only
        variable acc, last_word         : std_logic_vector(DW_C - 1 downto 0);
    begin
        if rst_n = '0' then
            sclk_d := '0'; cs_n_d := '1';
            edges := 0; carry_in := 0; words := 0; nbits := 0;
            acc := (others => '0'); last_word := (others => '0');
            v_r <= '0'; d_r <= D_OK;
            e_r <= 0; ci_r <= 0; co_r <= 0; w_r <= 0;
            wd_r <= (others => '0'); as_r <= 0; wt_r <= 0;

        elsif rising_edge(clk) then
            v_r <= '0';

            cs_assert   := (cs_n = '0') and (cs_n_d = '1');
            cs_deassert := (cs_n = '1') and (cs_n_d = '0');
            in_txn      := (cs_n = '0') or cs_deassert;
            sclk_edge   := (sclk /= sclk_d);
            leading     := sclk_edge and (sclk /= cpol);
            if cpha = '0' then capture := leading and in_txn;
            else               capture := sclk_edge and (not leading) and in_txn;
            end if;

            if cs_assert then
                -- Sampled HERE, at the start of the interval it describes. Reading it at the release
                -- would report the count this interval LEAVES rather than the one it inherited, and the
                -- whole point of the number is that it belongs to the past.
                carry_in := nbits;
                edges    := 0;
                words    := 0;
                as_r     <= as_r + 1;
                -- NOTE WHAT IS NOT CLEARED. `nbits` and `acc` survive the select, because the slave
                -- under suspicion does not clear them. Clearing them here would model the CORRECT
                -- design, and this module would then report every capture in the bench as clean.
            elsif capture then
                edges := edges + 1;
                acc   := acc(DW_C - 2 downto 0) & mosi;
                if nbits + 1 = NB_C then
                    last_word := acc;
                    nbits     := 0;
                    words     := words + 1;
                    wt_r      <= wt_r + 1;
                else
                    nbits := nbits + 1;
                end if;
            end if;

            if cs_deassert then
                v_r  <= '1';
                e_r  <= edges;
                ci_r <= carry_in;
                co_r <= nbits;
                w_r  <= words;
                wd_r <= last_word;

                if edges < NB_C and (carry_in + edges) = NB_C then
                    d_r <= D_GLITCH;
                elsif edges < NB_C then
                    d_r <= D_SHORT;
                elsif edges > NB_C and ((carry_in + edges) mod NB_C) = 0 and carry_in = 0 then
                    d_r <= D_MERGED;
                elsif edges > NB_C then
                    d_r <= D_RAGGED;
                elsif carry_in /= 0 then
                    d_r <= D_INHERITED;
                else
                    d_r <= D_OK;
                end if;
            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_cs_diag_tb.sv — five captures, seventeen framing intervals, and a symptom that arrives one frame after its cause
// spi_cs_diag_tb.sv
//
// FIVE CAPTURES, SEVENTEEN FRAMING INTERVALS, AND A SYMPTOM THAT ARRIVES ONE FRAME AFTER ITS CAUSE.
//
// THE FOUR RESULTS.
//
//   1. THE BROKEN FRAME AND THE WRONG-DATA FRAME ARE DIFFERENT FRAMES. Capture A truncates its third
//      interval. The fourth interval carries exactly eight clocks, a clean lead, a clean lag and no
//      structural defect of any kind -- and the word announced during it is five bits of the third
//      interval's payload followed by three of its own. The bench computes that mixed byte from the
//      definition and requires the decoder to predict it, so "the data is wrong here and the fault is
//      there" is a measured statement with two numbers behind it.
//
//   2. THE POISONING PERSISTS. Every interval after the truncation inherits the same orphan count, so
//      every word from then on is a mix. The bench requires the inherited count to be IDENTICAL two
//      intervals later -- which is what makes the field symptom "it comes back after a power cycle",
//      because only a reset clears the counter.
//
//   3. THE SIGN OF (ASSERTS - WORDS) SEPARATES A MERGE FROM A GLITCH. Capture B delivers four words
//      from three assertions -- a missing release merged two frames. Capture C delivers two words from
//      three assertions -- a spurious release split one frame in half. Both corrupt data, both leave
//      every individual edge count looking defensible, and neither is visible inside one interval.
//
//   4. THE DIAGNOSIS NEEDS A FRAME AFTER THE BROKEN ONE, SO THE END OF A CAPTURE IS A BLIND SPOT.
//      Capture E contains the SAME truncation as capture A, positioned last. The structural evidence
//      is still there; the data symptom never appears, because there is no following frame to corrupt.
//      The bench counts the evidence each position yields and requires them to differ -- which turns
//      "capture a bit more than you think you need" from advice into a measurement.

`timescale 1ns/1ps

module spi_cs_diag_tb;

    localparam int DW    = 32;
    localparam int NB    = 8;
    localparam int CNT_W = 8;
    localparam int LEAD  = 3;
    localparam int HALF  = 2;
    localparam int LAG   = 2;
    localparam int GAP   = 3;

    localparam [2:0] D_OK = 3'd0, D_SHORT = 3'd1, D_GLITCH = 3'd2,
                     D_MERGED = 3'd3, D_RAGGED = 3'd4, D_INHERITED = 3'd5;

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

    reg cpol = 1'b0, cpha = 1'b0;
    reg b_sclk = 1'b0, b_cs_n = 1'b1, b_mosi = 1'b0;

    wire             dg_valid;
    wire [2:0]       dg_code;
    wire [CNT_W-1:0] ob_edges, ob_carry_in, ob_carry_out, ob_words, ob_asserts, ob_words_tot;
    wire [DW-1:0]    ob_word;

    spi_cs_diag #(.DW(DW), .NB(NB), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
        .cpol(cpol), .cpha(cpha),
        .dg_valid(dg_valid), .dg_code(dg_code),
        .ob_edges(ob_edges), .ob_carry_in(ob_carry_in), .ob_carry_out(ob_carry_out),
        .ob_words(ob_words), .ob_word(ob_word),
        .ob_asserts(ob_asserts), .ob_words_tot(ob_words_tot)
    );

    integer errors = 0, x_reports = 0, got_n = 0;
    reg [2:0]       g_code;
    reg [CNT_W-1:0] g_edges, g_cin, g_cout, g_words, g_asserts, g_wtot;
    reg [DW-1:0]    g_word;

    always @(posedge clk) if (dg_valid) begin
        got_n     = got_n + 1;
        g_code    = dg_code;      g_edges = ob_edges;    g_cin  = ob_carry_in;
        g_cout    = ob_carry_out; g_words = ob_words;    g_word = ob_word;
        g_asserts = ob_asserts;   g_wtot  = ob_words_tot;
        if ((^dg_code === 1'bx) || (^ob_edges === 1'bx) || (^ob_carry_in === 1'bx)
            || (^ob_carry_out === 1'bx) || (^ob_words === 1'bx) || (^ob_word[7:0] === 1'bx)
            || (^ob_asserts === 1'bx) || (^ob_words_tot === 1'bx))
            x_reports = x_reports + 1;
    end

    function [8*12:1] dname(input [2:0] c);
        begin
            case (c)
                D_OK:        dname = "OK          ";
                D_SHORT:     dname = "SHORT       ";
                D_GLITCH:    dname = "GLITCH      ";
                D_MERGED:    dname = "MERGED      ";
                D_RAGGED:    dname = "RAGGED      ";
                default:     dname = "INHERITED   ";
            endcase
        end
    endfunction

    task automatic idle_n(input integer n);
        integer i;
        begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
    endtask

    // One framing interval: assert CS, drive `n` leading edges from a LEFT-ALIGNED bit field, release.
    //
    // THE FIELD IS LEFT-ALIGNED FOR A REASON. SPI is MSB-first, so a frame cut short after five of
    // eight clocks has transmitted the payload's TOP five bits, not its bottom five. The first version
    // of this task indexed from `bits[n-1]` downwards, which sends the LOW n bits -- a truncated frame
    // then carried bits the master would never have put on the wire first, and the corrupted word the
    // chapter is about was a mixture of the wrong halves. The physics decides the indexing.
    //
    // The number of edges is the stimulus variable, because every fault in this chapter is a mismatch
    // between the number of clocks inside an assertion and the word length.
    task automatic iv(input [DW-1:0] bits, input integer n);
        integer k;
        begin
            b_sclk = cpol; b_mosi = 1'b0;
            idle_n(2);
            b_cs_n = 1'b0;
            idle_n(1);
            b_mosi = bits[DW-1];
            idle_n(LEAD - 1);
            for (k = 0; k < n; k = k + 1) begin
                b_sclk = ~b_sclk;
                idle_n(HALF);
                b_sclk = ~b_sclk;
                if (k < n-1) b_mosi = bits[DW-1-k-1];
                idle_n(HALF);
            end
            idle_n(LAG);
            b_cs_n = 1'b1;
            idle_n(1);
            b_sclk = cpol;
            idle_n(GAP);
        end
    endtask

    // THE INDEPENDENT ORACLE. A slave that clears its bit counter only on reset, modelled here from
    // the description rather than by asking the decoder. It is fed the same stream of (bits, edges)
    // pairs and announces words at exactly the same instants, so a disagreement is a real one.
    reg [DW-1:0] o_acc;
    integer      o_nbits;
    reg [DW-1:0] o_last;
    integer      o_words;

    task automatic oracle_iv(input [DW-1:0] bits, input integer n);
        integer k;
        begin
            o_words = 0;
            for (k = 0; k < n; k = k + 1) begin
                o_acc = {o_acc[DW-2:0], bits[DW-1-k]};
                if (o_nbits + 1 == NB) begin
                    o_nbits = 0; o_last = o_acc & {{(DW-NB){1'b0}}, {NB{1'b1}}}; o_words = o_words + 1;
                end else begin
                    o_nbits = o_nbits + 1;
                end
            end
        end
    endtask

    localparam [7:0] W0 = 8'h8D, W1 = 8'hC3, W2 = 8'h5A, W3 = 8'h3C, W4 = 8'hF0;

    integer s, iv_i, base, cap;
    reg [2:0]       code_log [0:19];
    integer         cin_log  [0:19], cout_log[0:19], edge_log[0:19], ann_log[0:19];
    reg [DW-1:0]    word_log [0:19];
    integer         cap_log  [0:19];
    integer         n_iv, mutations;
    integer         ev_mid, ev_end;
    reg [2:0]       want;
    integer         w_edges, w_cin;

    // Run one interval through the DUT and the oracle together, log, print and check.
    task automatic step(input integer capn, input [DW-1:0] bits, input integer n,
                        input [2:0] wcode, input integer wedges, input integer wcin,
                        input [8*40:1] note);
        begin
            base = got_n;
            oracle_iv(bits, n);
            iv(bits, n);
            code_log[n_iv] = g_code;  edge_log[n_iv] = g_edges; cin_log[n_iv] = g_cin;
            cout_log[n_iv] = g_cout;  ann_log[n_iv]  = g_words; word_log[n_iv] = g_word;
            cap_log[n_iv]  = capn;

            $display("   %0d   %2d   %5d   %4d   %5d   %3d    %02h   %s  %s  %0s",
                     capn, n_iv, g_edges, g_cin, g_cout, g_words, g_word[7:0],
                     dname(g_code), dname(wcode), note);

            if (got_n - base != 1) begin
                $display("  FAIL: interval %0d produced %0d verdicts for one assertion", n_iv, got_n - base);
                errors = errors + 1;
            end
            if (g_code !== wcode) begin
                $display("  FAIL: interval %0d diagnosed %s where %s was expected",
                         n_iv, dname(g_code), dname(wcode));
                errors = errors + 1;
            end
            if (g_edges != wedges[CNT_W-1:0]) begin
                $display("  FAIL: interval %0d counted %0d edges where %0d were driven",
                         n_iv, g_edges, wedges);
                errors = errors + 1;
            end
            if (g_cin != wcin[CNT_W-1:0]) begin
                $display("  FAIL: interval %0d inherited %0d orphan bits where %0d were expected",
                         n_iv, g_cin, wcin);
                errors = errors + 1;
            end
            // THE ORACLE CHECK. The decoder's prediction of what a non-clearing slave would report has
            // to equal an independent model's, announcement for announcement.
            if (g_words != o_words[CNT_W-1:0]) begin
                $display("  FAIL: interval %0d announced %0d words where the oracle announced %0d",
                         n_iv, g_words, o_words);
                errors = errors + 1;
            end
            if ((o_words > 0) && (g_word[7:0] !== o_last[7:0])) begin
                $display("  FAIL: interval %0d predicted word %02h where the oracle says %02h",
                         n_iv, g_word[7:0], o_last[7:0]);
                errors = errors + 1;
            end
            n_iv = n_iv + 1;
        end
    endtask

    task automatic recap;
        begin
            @(negedge clk);
            b_cs_n = 1'b1; b_sclk = cpol; b_mosi = 1'b0;
            idle_n(2); rst_n = 1'b0; idle_n(3); rst_n = 1'b1; idle_n(2);
            o_acc = {DW{1'b0}}; o_nbits = 0; o_last = {DW{1'b0}}; o_words = 0;
        end
    endtask

    initial begin
        n_iv = 0; mutations = 0;

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

        $display("  cap   iv   edges   c_in   c_out   ann   word   verdict       expected      note");

        // ---------------- CAPTURE A: a truncation in the middle of a sequence ----------------
        $display("  -- capture A: five frames, the THIRD cut short after 5 of 8 clocks");
        recap;
        step(0, {W0, 24'b0}, 8, D_OK,        8, 0, "a whole word");
        step(0, {W1, 24'b0}, 8, D_OK,        8, 0, "a whole word");
        step(0, {W2, 24'b0}, 5, D_SHORT,     5, 0, "CUT SHORT -- leaves 5 orphan bits");
        step(0, {W3, 24'b0}, 8, D_INHERITED, 8, 5, "flawless frame, WRONG DATA");
        step(0, {W4, 24'b0}, 8, D_INHERITED, 8, 5, "still poisoned -- only a reset clears it");

        // ---------------- CAPTURE B: a missing release merges two frames ----------------
        $display("  -- capture B: a MISSING release -- two words inside one assertion");
        recap;
        step(1, {W0, W1, 16'b0}, 16, D_MERGED, 16, 0, "16 clocks, 2 words, 1 assertion");
        step(1, {W2, 24'b0},      8, D_OK,      8, 0, "a whole word");
        step(1, {W3, 24'b0},      8, D_OK,      8, 0, "a whole word");

        // ---------------- CAPTURE C: a spurious release splits one frame ----------------
        $display("  -- capture C: a SPURIOUS release -- one word split across two assertions");
        recap;
        step(2, {W0, 24'b0}, 3, D_SHORT,  3, 0, "3 clocks then CS released");
        // The second half of the SPLIT word: the five bits the first assertion did not send, so the
        // two assertions together carry exactly the payload W0 and nothing else.
        step(2, {W0[4:0], 27'b0}, 5, D_GLITCH, 5, 3, "5 more -- together exactly one word");
        step(2, {W1, 24'b0}, 8, D_OK,     8, 0, "a whole word");

        // ---------------- CAPTURE D: merged AND truncated ----------------
        $display("  -- capture D: 11 clocks in one assertion -- merged and truncated at once");
        recap;
        step(3, {W0, W1, 16'b0}, 11, D_RAGGED,    11, 0, "one word plus 3 orphan bits");
        step(3, {W2, 24'b0},      8, D_INHERITED,  8, 3, "flawless frame, WRONG DATA");
        step(3, {W3, 24'b0},      8, D_INHERITED,  8, 3, "still poisoned");

        // ---------------- CAPTURE E: the same truncation, at the END of the capture ----------------
        $display("  -- capture E: the SAME truncation as capture A, positioned LAST");
        recap;
        step(4, {W0, 24'b0}, 8, D_OK,    8, 0, "a whole word");
        step(4, {W1, 24'b0}, 8, D_OK,    8, 0, "a whole word");
        step(4, {W2, 24'b0}, 5, D_SHORT, 5, 0, "CUT SHORT -- and nothing follows it");

        // ================= 1. the broken frame and the wrong-data frame differ =================
        if (!(code_log[2] === D_SHORT && code_log[3] === D_INHERITED && edge_log[3] == NB
              && cin_log[3] == 5)) begin
            $display("  FAIL: capture A did not separate the broken frame from the wrong-data frame (%s/%s, %0d edges, %0d inherited)",
                     dname(code_log[2]), dname(code_log[3]), edge_log[3], cin_log[3]);
            errors = errors + 1;
        end
        if (word_log[3][7:0] === W3) begin
            $display("  FAIL: the frame after the truncation announced its own payload %02h, so no corruption occurred and the chapter's claim is untested",
                     W3);
            errors = errors + 1;
        end
        $display("");
        $display("    1. interval 2 was cut short after %0d of %0d clocks. Interval 3 carried exactly %0d clocks with a clean lead, a clean lag and no structural defect of any kind -- and the word it announced was %02h where its payload was %02h, because %0d orphan bits from interval 2 were still sitting in the slave's shift register. THE FRAME WHOSE DATA IS WRONG IS NOT THE FRAME THAT IS BROKEN, and an engineer who captures the mismatching transaction captures interval 3 and finds nothing",
                 edge_log[2], NB, edge_log[3], word_log[3][7:0], W3, cin_log[3]);

        // ================= 2. the poisoning persists =================
        if (cin_log[4] != cin_log[3]) begin
            $display("  FAIL: the inherited orphan count changed from %0d to %0d, so the claim that the residue persists is unsupported",
                     cin_log[3], cin_log[4]);
            errors = errors + 1;
        end
        if (code_log[4] !== D_INHERITED) begin
            $display("  FAIL: the second frame after the truncation was diagnosed %s rather than INHERITED",
                     dname(code_log[4]));
            errors = errors + 1;
        end
        $display("    2. interval 4 inherited the SAME %0d orphan bits and announced %02h. The residue does not drain, because the counter that holds it is cleared by reset and by nothing else -- so every word from the truncation onward is a mix and the stream stays wrong indefinitely. That is the mechanism behind `it works again after a power cycle`, which is the single most misleading sentence in a bug report, because it points at supplies and sequencing rather than at framing",
                 cin_log[4], word_log[4][7:0]);

        // ================= 3. the sign of (asserts - words) =================
        // Capture B: three assertions delivered four words. Capture C: three delivered two.
        if (!(ann_log[5] == 2 && ann_log[6] == 1 && ann_log[7] == 1)) begin
            $display("  FAIL: capture B did not deliver 4 words from 3 assertions (%0d/%0d/%0d)",
                     ann_log[5], ann_log[6], ann_log[7]);
            errors = errors + 1;
        end
        if (!(ann_log[8] == 0 && ann_log[9] == 1 && ann_log[10] == 1)) begin
            $display("  FAIL: capture C did not deliver 2 words from 3 assertions (%0d/%0d/%0d)",
                     ann_log[8], ann_log[9], ann_log[10]);
            errors = errors + 1;
        end
        $display("    3. capture B delivered %0d words from 3 assertions and capture C delivered %0d from 3. A missing release makes words OUTNUMBER assertions; a spurious release makes assertions outnumber words. Every individual edge count in capture C is defensible on its own -- 3 clocks, then 5, then 8 -- and the fault is only visible as a RATIO across the capture, which is why a per-frame checker reports three unremarkable frames",
                 ann_log[5] + ann_log[6] + ann_log[7], ann_log[8] + ann_log[9] + ann_log[10]);

        // ================= 4. the end of a capture is a blind spot =================
        ev_mid = 0; ev_end = 0;
        for (s = 2; s <= 4; s = s + 1) if (code_log[s] !== D_OK) ev_mid = ev_mid + 1;
        for (s = 14; s <= 16; s = s + 1) if (code_log[s] !== D_OK) ev_end = ev_end + 1;
        if (code_log[16] !== code_log[2]) begin
            $display("  FAIL: the truncation at the end of capture E was diagnosed %s rather than %s, so it is not the same fault",
                     dname(code_log[16]), dname(code_log[2]));
            errors = errors + 1;
        end
        if (ev_end >= ev_mid) begin
            $display("  FAIL: the boundary position yielded %0d pieces of evidence against %0d mid-sequence; the claim is that it yields fewer",
                     ev_end, ev_mid);
            errors = errors + 1;
        end
        $display("    4. the SAME truncation was diagnosed %s in both positions, and it produced %0d non-clean verdicts mid-sequence against %0d at the end of the capture. The structural evidence survives; the DATA symptom does not, because there is no following frame left to corrupt. So a capture that stops at the fault shows a short frame and no consequence, and a capture that stops one frame earlier shows nothing at all -- which makes `capture more than you think you need` a measured requirement rather than folklore",
                 dname(code_log[2]), ev_mid, ev_end);

        // ================= 5. the two slave designs fail on OPPOSITE faults =================
        // Not designed in advance -- capture C produced it. The split word came back CORRECT.
        if (word_log[9][7:0] !== W0) begin
            $display("  FAIL: the split word reassembled as %02h rather than %02h, so the claim that a spurious release is data-transparent on this slave is wrong",
                     word_log[9][7:0], W0);
            errors = errors + 1;
        end
        if (word_log[3][7:0] === W3) begin
            $display("  FAIL: the truncation did not corrupt the following word, so the contrast in result 5 does not exist");
            errors = errors + 1;
        end
        $display("    5. and the measurement produced a result that was not designed in: the SPLIT word came back CORRECT. Interval 9 announced %02h, exactly the payload interval 8 began, because a slave that does not clear on select simply carries on counting and a spurious release costs it nothing. So the two possible slave designs fail on OPPOSITE faults -- a non-clearing slave survives a glitch and is poisoned indefinitely by a truncation, while a slave that clears on every select survives a truncation by losing one word and recovering, and is corrupted by a glitch it cannot even see. There is no design that is robust against both, which means the framing discipline has to be guaranteed by the master and cannot be recovered by the slave. And it means a debugger has to know which slave is in front of them before interpreting any of these captures",
                 word_log[9][7:0]);

        // ================= BENCH INTEGRITY =================
        // Two deliberately wrong expectations, compared by the same operator as the real ones.
        if (code_log[3] !== D_OK)    mutations = mutations + 1;
        if (cin_log[3]  != 0)       mutations = mutations + 1;
        if (mutations != 2) begin
            $display("  FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
            errors = errors + 1;
        end
        if (x_reports != 0) begin
            $display("  FAIL: %0d reported fields carried X", x_reports);
            errors = errors + 1;
        end
        if (n_iv != 17) begin
            $display("  FAIL: %0d intervals were driven where 17 were expected", n_iv);
            errors = errors + 1;
        end

        if (errors == 0) begin
            $display("");
            $display("    and the bench proved itself: two deliberately wrong expectations mismatched, every reported field carried a known value, and the decoder's prediction of a non-clearing slave's output matched an independent model on all %0d intervals",
                     n_iv);
            $display("PASS: chip-select faults are the only family in this module whose EVIDENCE lives in a different frame from its SYMPTOM. A frame cut short after %0d of %0d clocks leaves orphan bits in a slave whose bit counter is cleared by reset and not by the select -- and the NEXT frame is then structurally flawless, with a clean lead, a clean lag and exactly %0d clocks, while the word it announces is %02h instead of %02h. The frame whose data is wrong is not the frame that is broken. The residue does not drain either: interval 4 inherited the same %0d orphan bits, so the stream stays wrong until something resets the slave, which is the mechanism behind `it works after a power cycle`. Two more faults leave every individual edge count defensible and are visible only as a RATIO across the capture -- a missing release made words outnumber assertions, 4 from 3, and a spurious release made assertions outnumber words, 3 delivering 2. And because the diagnosis needs a frame AFTER the broken one, the end of a capture is a blind spot: the identical truncation yielded %0d non-clean verdicts mid-sequence and %0d at the boundary, so the buffer length is part of the instrument. One result was not designed in and is the most useful thing here: the SPLIT word came back CORRECT, %02h, because a slave that does not clear on select simply carries on counting -- so the two possible slave designs fail on OPPOSITE faults, a non-clearing slave being poisoned by a truncation it survives a glitch through and a clearing slave being corrupted by a glitch it survives a truncation through. No slave is robust against both, so the framing discipline belongs to the master",
                     edge_log[2], NB, edge_log[3], word_log[3][7:0], W3, cin_log[4], ev_mid, ev_end,
                     word_log[9][7:0]);
        end else begin
            $display("FAIL: %0d error(s)", errors);
        end
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_diag_tb.v — the same bench in Verilog-2001
// spi_cs_diag_tb.v
//
// FIVE CAPTURES, SEVENTEEN FRAMING INTERVALS, AND A SYMPTOM THAT ARRIVES ONE FRAME AFTER ITS CAUSE.
//
// THE FOUR RESULTS.
//
//   1. THE BROKEN FRAME AND THE WRONG-DATA FRAME ARE DIFFERENT FRAMES. Capture A truncates its third
//      interval. The fourth interval carries exactly eight clocks, a clean lead, a clean lag and no
//      structural defect of any kind -- and the word announced during it is five bits of the third
//      interval's payload followed by three of its own. The bench computes that mixed byte from the
//      definition and requires the decoder to predict it, so "the data is wrong here and the fault is
//      there" is a measured statement with two numbers behind it.
//
//   2. THE POISONING PERSISTS. Every interval after the truncation inherits the same orphan count, so
//      every word from then on is a mix. The bench requires the inherited count to be IDENTICAL two
//      intervals later -- which is what makes the field symptom "it comes back after a power cycle",
//      because only a reset clears the counter.
//
//   3. THE SIGN OF (ASSERTS - WORDS) SEPARATES A MERGE FROM A GLITCH. Capture B delivers four words
//      from three assertions -- a missing release merged two frames. Capture C delivers two words from
//      three assertions -- a spurious release split one frame in half. Both corrupt data, both leave
//      every individual edge count looking defensible, and neither is visible inside one interval.
//
//   4. THE DIAGNOSIS NEEDS A FRAME AFTER THE BROKEN ONE, SO THE END OF A CAPTURE IS A BLIND SPOT.
//      Capture E contains the SAME truncation as capture A, positioned last. The structural evidence
//      is still there; the data symptom never appears, because there is no following frame to corrupt.
//      The bench counts the evidence each position yields and requires them to differ -- which turns
//      "capture a bit more than you think you need" from advice into a measurement.

`timescale 1ns/1ps

module spi_cs_diag_tb;

    localparam DW    = 32;
    localparam NB    = 8;
    localparam CNT_W = 8;
    localparam LEAD  = 3;
    localparam HALF  = 2;
    localparam LAG   = 2;
    localparam GAP   = 3;

    localparam [2:0] D_OK = 3'd0, D_SHORT = 3'd1, D_GLITCH = 3'd2,
                     D_MERGED = 3'd3, D_RAGGED = 3'd4, D_INHERITED = 3'd5;

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

    reg cpol, cpha;
    reg b_sclk, b_cs_n, b_mosi;

    wire             dg_valid;
    wire [2:0]       dg_code;
    wire [CNT_W-1:0] ob_edges, ob_carry_in, ob_carry_out, ob_words, ob_asserts, ob_words_tot;
    wire [DW-1:0]    ob_word;

    spi_cs_diag #(.DW(DW), .NB(NB), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
        .cpol(cpol), .cpha(cpha),
        .dg_valid(dg_valid), .dg_code(dg_code),
        .ob_edges(ob_edges), .ob_carry_in(ob_carry_in), .ob_carry_out(ob_carry_out),
        .ob_words(ob_words), .ob_word(ob_word),
        .ob_asserts(ob_asserts), .ob_words_tot(ob_words_tot)
    );

    integer errors, x_reports, got_n;
    reg [2:0]       g_code;
    reg [CNT_W-1:0] g_edges, g_cin, g_cout, g_words, g_asserts, g_wtot;
    reg [DW-1:0]    g_word;

    always @(posedge clk) if (dg_valid) begin
        got_n     = got_n + 1;
        g_code    = dg_code;      g_edges = ob_edges;    g_cin  = ob_carry_in;
        g_cout    = ob_carry_out; g_words = ob_words;    g_word = ob_word;
        g_asserts = ob_asserts;   g_wtot  = ob_words_tot;
        if ((^dg_code === 1'bx) || (^ob_edges === 1'bx) || (^ob_carry_in === 1'bx)
            || (^ob_carry_out === 1'bx) || (^ob_words === 1'bx) || (^ob_word[7:0] === 1'bx)
            || (^ob_asserts === 1'bx) || (^ob_words_tot === 1'bx))
            x_reports = x_reports + 1;
    end

        function [8*12:1] dname;
        input [2:0] c;
        begin
            case (c)
                D_OK:        dname = "OK          ";
                D_SHORT:     dname = "SHORT       ";
                D_GLITCH:    dname = "GLITCH      ";
                D_MERGED:    dname = "MERGED      ";
                D_RAGGED:    dname = "RAGGED      ";
                default:     dname = "INHERITED   ";
            endcase
        end
    endfunction

        task idle_n;
        input integer n;
        integer i;
        begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
    endtask

    // One framing interval: assert CS, drive `n` leading edges from a LEFT-ALIGNED bit field, release.
    //
    // THE FIELD IS LEFT-ALIGNED FOR A REASON. SPI is MSB-first, so a frame cut short after five of
    // eight clocks has transmitted the payload's TOP five bits, not its bottom five. The first version
    // of this task indexed from `bits[n-1]` downwards, which sends the LOW n bits -- a truncated frame
    // then carried bits the master would never have put on the wire first, and the corrupted word the
    // chapter is about was a mixture of the wrong halves. The physics decides the indexing.
    //
    // The number of edges is the stimulus variable, because every fault in this chapter is a mismatch
    // between the number of clocks inside an assertion and the word length.
        task iv;
        input [DW-1:0] bits;
        input integer n;
        integer k;
        begin
            b_sclk = cpol; b_mosi = 1'b0;
            idle_n(2);
            b_cs_n = 1'b0;
            idle_n(1);
            b_mosi = bits[DW-1];
            idle_n(LEAD - 1);
            for (k = 0; k < n; k = k + 1) begin
                b_sclk = ~b_sclk;
                idle_n(HALF);
                b_sclk = ~b_sclk;
                if (k < n-1) b_mosi = bits[DW-1-k-1];
                idle_n(HALF);
            end
            idle_n(LAG);
            b_cs_n = 1'b1;
            idle_n(1);
            b_sclk = cpol;
            idle_n(GAP);
        end
    endtask

    // THE INDEPENDENT ORACLE. A slave that clears its bit counter only on reset, modelled here from
    // the description rather than by asking the decoder. It is fed the same stream of (bits, edges)
    // pairs and announces words at exactly the same instants, so a disagreement is a real one.
    reg [DW-1:0] o_acc;
    integer      o_nbits;
    reg [DW-1:0] o_last;
    integer      o_words;

        task oracle_iv;
        input [DW-1:0] bits;
        input integer n;
        integer k;
        begin
            o_words = 0;
            for (k = 0; k < n; k = k + 1) begin
                o_acc = {o_acc[DW-2:0], bits[DW-1-k]};
                if (o_nbits + 1 == NB) begin
                    o_nbits = 0; o_last = o_acc & {{(DW-NB){1'b0}}, {NB{1'b1}}}; o_words = o_words + 1;
                end else begin
                    o_nbits = o_nbits + 1;
                end
            end
        end
    endtask

    localparam [7:0] W0 = 8'h8D, W1 = 8'hC3, W2 = 8'h5A, W3 = 8'h3C, W4 = 8'hF0;

    integer s, iv_i, base, cap;
    reg [2:0]       code_log [0:19];
    integer         cin_log  [0:19], cout_log[0:19], edge_log[0:19], ann_log[0:19];
    reg [DW-1:0]    word_log [0:19];
    integer         cap_log  [0:19];
    integer         n_iv, mutations;
    integer         ev_mid, ev_end;
    reg [2:0]       want;
    integer         w_edges, w_cin;

    // Run one interval through the DUT and the oracle together, log, print and check.
        task step;
        input integer capn;
        input [DW-1:0] bits;
        input integer n;
        input [2:0] wcode;
        input integer wedges;
        input integer wcin;
        input [8*40:1] note;
        begin
            base = got_n;
            oracle_iv(bits, n);
            iv(bits, n);
            code_log[n_iv] = g_code;  edge_log[n_iv] = g_edges; cin_log[n_iv] = g_cin;
            cout_log[n_iv] = g_cout;  ann_log[n_iv]  = g_words; word_log[n_iv] = g_word;
            cap_log[n_iv]  = capn;

            $display("   %0d   %2d   %5d   %4d   %5d   %3d    %02h   %0s  %0s  %0s",
                     capn, n_iv, g_edges, g_cin, g_cout, g_words, g_word[7:0],
                     dname(g_code), dname(wcode), note);

            if (got_n - base != 1) begin
                $display("  FAIL: interval %0d produced %0d verdicts for one assertion", n_iv, got_n - base);
                errors = errors + 1;
            end
            if (g_code !== wcode) begin
                $display("  FAIL: interval %0d diagnosed %0s where %0s was expected",
                         n_iv, dname(g_code), dname(wcode));
                errors = errors + 1;
            end
            if (g_edges != wedges[CNT_W-1:0]) begin
                $display("  FAIL: interval %0d counted %0d edges where %0d were driven",
                         n_iv, g_edges, wedges);
                errors = errors + 1;
            end
            if (g_cin != wcin[CNT_W-1:0]) begin
                $display("  FAIL: interval %0d inherited %0d orphan bits where %0d were expected",
                         n_iv, g_cin, wcin);
                errors = errors + 1;
            end
            // THE ORACLE CHECK. The decoder's prediction of what a non-clearing slave would report has
            // to equal an independent model's, announcement for announcement.
            if (g_words != o_words[CNT_W-1:0]) begin
                $display("  FAIL: interval %0d announced %0d words where the oracle announced %0d",
                         n_iv, g_words, o_words);
                errors = errors + 1;
            end
            if ((o_words > 0) && (g_word[7:0] !== o_last[7:0])) begin
                $display("  FAIL: interval %0d predicted word %02h where the oracle says %02h",
                         n_iv, g_word[7:0], o_last[7:0]);
                errors = errors + 1;
            end
            n_iv = n_iv + 1;
        end
    endtask

    task recap;
        begin
            @(negedge clk);
            b_cs_n = 1'b1; b_sclk = cpol; b_mosi = 1'b0;
            idle_n(2); rst_n = 1'b0; idle_n(3); rst_n = 1'b1; idle_n(2);
            o_acc = {DW{1'b0}}; o_nbits = 0; o_last = {DW{1'b0}}; o_words = 0;
        end
    endtask

    initial begin
        n_iv = 0; mutations = 0;

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

        $display("  cap   iv   edges   c_in   c_out   ann   word   verdict       expected      note");

        // ---------------- CAPTURE A: a truncation in the middle of a sequence ----------------
        $display("  -- capture A: five frames, the THIRD cut short after 5 of 8 clocks");
        recap;
        step(0, {W0, 24'b0}, 8, D_OK,        8, 0, "a whole word");
        step(0, {W1, 24'b0}, 8, D_OK,        8, 0, "a whole word");
        step(0, {W2, 24'b0}, 5, D_SHORT,     5, 0, "CUT SHORT -- leaves 5 orphan bits");
        step(0, {W3, 24'b0}, 8, D_INHERITED, 8, 5, "flawless frame, WRONG DATA");
        step(0, {W4, 24'b0}, 8, D_INHERITED, 8, 5, "still poisoned -- only a reset clears it");

        // ---------------- CAPTURE B: a missing release merges two frames ----------------
        $display("  -- capture B: a MISSING release -- two words inside one assertion");
        recap;
        step(1, {W0, W1, 16'b0}, 16, D_MERGED, 16, 0, "16 clocks, 2 words, 1 assertion");
        step(1, {W2, 24'b0},      8, D_OK,      8, 0, "a whole word");
        step(1, {W3, 24'b0},      8, D_OK,      8, 0, "a whole word");

        // ---------------- CAPTURE C: a spurious release splits one frame ----------------
        $display("  -- capture C: a SPURIOUS release -- one word split across two assertions");
        recap;
        step(2, {W0, 24'b0}, 3, D_SHORT,  3, 0, "3 clocks then CS released");
        // The second half of the SPLIT word: the five bits the first assertion did not send, so the
        // two assertions together carry exactly the payload W0 and nothing else.
        step(2, {W0[4:0], 27'b0}, 5, D_GLITCH, 5, 3, "5 more -- together exactly one word");
        step(2, {W1, 24'b0}, 8, D_OK,     8, 0, "a whole word");

        // ---------------- CAPTURE D: merged AND truncated ----------------
        $display("  -- capture D: 11 clocks in one assertion -- merged and truncated at once");
        recap;
        step(3, {W0, W1, 16'b0}, 11, D_RAGGED,    11, 0, "one word plus 3 orphan bits");
        step(3, {W2, 24'b0},      8, D_INHERITED,  8, 3, "flawless frame, WRONG DATA");
        step(3, {W3, 24'b0},      8, D_INHERITED,  8, 3, "still poisoned");

        // ---------------- CAPTURE E: the same truncation, at the END of the capture ----------------
        $display("  -- capture E: the SAME truncation as capture A, positioned LAST");
        recap;
        step(4, {W0, 24'b0}, 8, D_OK,    8, 0, "a whole word");
        step(4, {W1, 24'b0}, 8, D_OK,    8, 0, "a whole word");
        step(4, {W2, 24'b0}, 5, D_SHORT, 5, 0, "CUT SHORT -- and nothing follows it");

        // ================= 1. the broken frame and the wrong-data frame differ =================
        if (!(code_log[2] === D_SHORT && code_log[3] === D_INHERITED && edge_log[3] == NB
              && cin_log[3] == 5)) begin
            $display("  FAIL: capture A did not separate the broken frame from the wrong-data frame (%0s/%0s, %0d edges, %0d inherited)",
                     dname(code_log[2]), dname(code_log[3]), edge_log[3], cin_log[3]);
            errors = errors + 1;
        end
        if (word_log[3][7:0] === W3) begin
            $display("  FAIL: the frame after the truncation announced its own payload %02h, so no corruption occurred and the chapter's claim is untested",
                     W3);
            errors = errors + 1;
        end
        $display("");
        $display("    1. interval 2 was cut short after %0d of %0d clocks. Interval 3 carried exactly %0d clocks with a clean lead, a clean lag and no structural defect of any kind -- and the word it announced was %02h where its payload was %02h, because %0d orphan bits from interval 2 were still sitting in the slave's shift register. THE FRAME WHOSE DATA IS WRONG IS NOT THE FRAME THAT IS BROKEN, and an engineer who captures the mismatching transaction captures interval 3 and finds nothing",
                 edge_log[2], NB, edge_log[3], word_log[3][7:0], W3, cin_log[3]);

        // ================= 2. the poisoning persists =================
        if (cin_log[4] != cin_log[3]) begin
            $display("  FAIL: the inherited orphan count changed from %0d to %0d, so the claim that the residue persists is unsupported",
                     cin_log[3], cin_log[4]);
            errors = errors + 1;
        end
        if (code_log[4] !== D_INHERITED) begin
            $display("  FAIL: the second frame after the truncation was diagnosed %0s rather than INHERITED",
                     dname(code_log[4]));
            errors = errors + 1;
        end
        $display("    2. interval 4 inherited the SAME %0d orphan bits and announced %02h. The residue does not drain, because the counter that holds it is cleared by reset and by nothing else -- so every word from the truncation onward is a mix and the stream stays wrong indefinitely. That is the mechanism behind `it works again after a power cycle`, which is the single most misleading sentence in a bug report, because it points at supplies and sequencing rather than at framing",
                 cin_log[4], word_log[4][7:0]);

        // ================= 3. the sign of (asserts - words) =================
        // Capture B: three assertions delivered four words. Capture C: three delivered two.
        if (!(ann_log[5] == 2 && ann_log[6] == 1 && ann_log[7] == 1)) begin
            $display("  FAIL: capture B did not deliver 4 words from 3 assertions (%0d/%0d/%0d)",
                     ann_log[5], ann_log[6], ann_log[7]);
            errors = errors + 1;
        end
        if (!(ann_log[8] == 0 && ann_log[9] == 1 && ann_log[10] == 1)) begin
            $display("  FAIL: capture C did not deliver 2 words from 3 assertions (%0d/%0d/%0d)",
                     ann_log[8], ann_log[9], ann_log[10]);
            errors = errors + 1;
        end
        $display("    3. capture B delivered %0d words from 3 assertions and capture C delivered %0d from 3. A missing release makes words OUTNUMBER assertions; a spurious release makes assertions outnumber words. Every individual edge count in capture C is defensible on its own -- 3 clocks, then 5, then 8 -- and the fault is only visible as a RATIO across the capture, which is why a per-frame checker reports three unremarkable frames",
                 ann_log[5] + ann_log[6] + ann_log[7], ann_log[8] + ann_log[9] + ann_log[10]);

        // ================= 4. the end of a capture is a blind spot =================
        ev_mid = 0; ev_end = 0;
        for (s = 2; s <= 4; s = s + 1) if (code_log[s] !== D_OK) ev_mid = ev_mid + 1;
        for (s = 14; s <= 16; s = s + 1) if (code_log[s] !== D_OK) ev_end = ev_end + 1;
        if (code_log[16] !== code_log[2]) begin
            $display("  FAIL: the truncation at the end of capture E was diagnosed %0s rather than %0s, so it is not the same fault",
                     dname(code_log[16]), dname(code_log[2]));
            errors = errors + 1;
        end
        if (ev_end >= ev_mid) begin
            $display("  FAIL: the boundary position yielded %0d pieces of evidence against %0d mid-sequence; the claim is that it yields fewer",
                     ev_end, ev_mid);
            errors = errors + 1;
        end
        $display("    4. the SAME truncation was diagnosed %0s in both positions, and it produced %0d non-clean verdicts mid-sequence against %0d at the end of the capture. The structural evidence survives; the DATA symptom does not, because there is no following frame left to corrupt. So a capture that stops at the fault shows a short frame and no consequence, and a capture that stops one frame earlier shows nothing at all -- which makes `capture more than you think you need` a measured requirement rather than folklore",
                 dname(code_log[2]), ev_mid, ev_end);

        // ================= 5. the two slave designs fail on OPPOSITE faults =================
        // Not designed in advance -- capture C produced it. The split word came back CORRECT.
        if (word_log[9][7:0] !== W0) begin
            $display("  FAIL: the split word reassembled as %02h rather than %02h, so the claim that a spurious release is data-transparent on this slave is wrong",
                     word_log[9][7:0], W0);
            errors = errors + 1;
        end
        if (word_log[3][7:0] === W3) begin
            $display("  FAIL: the truncation did not corrupt the following word, so the contrast in result 5 does not exist");
            errors = errors + 1;
        end
        $display("    5. and the measurement produced a result that was not designed in: the SPLIT word came back CORRECT. Interval 9 announced %02h, exactly the payload interval 8 began, because a slave that does not clear on select simply carries on counting and a spurious release costs it nothing. So the two possible slave designs fail on OPPOSITE faults -- a non-clearing slave survives a glitch and is poisoned indefinitely by a truncation, while a slave that clears on every select survives a truncation by losing one word and recovering, and is corrupted by a glitch it cannot even see. There is no design that is robust against both, which means the framing discipline has to be guaranteed by the master and cannot be recovered by the slave. And it means a debugger has to know which slave is in front of them before interpreting any of these captures",
                 word_log[9][7:0]);

        // ================= BENCH INTEGRITY =================
        // Two deliberately wrong expectations, compared by the same operator as the real ones.
        if (code_log[3] !== D_OK)    mutations = mutations + 1;
        if (cin_log[3]  != 0)       mutations = mutations + 1;
        if (mutations != 2) begin
            $display("  FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
            errors = errors + 1;
        end
        if (x_reports != 0) begin
            $display("  FAIL: %0d reported fields carried X", x_reports);
            errors = errors + 1;
        end
        if (n_iv != 17) begin
            $display("  FAIL: %0d intervals were driven where 17 were expected", n_iv);
            errors = errors + 1;
        end

        if (errors == 0) begin
            $display("");
            $display("    and the bench proved itself: two deliberately wrong expectations mismatched, every reported field carried a known value, and the decoder's prediction of a non-clearing slave's output matched an independent model on all %0d intervals",
                     n_iv);
            $display("PASS: chip-select faults are the only family in this module whose EVIDENCE lives in a different frame from its SYMPTOM. A frame cut short after %0d of %0d clocks leaves orphan bits in a slave whose bit counter is cleared by reset and not by the select -- and the NEXT frame is then structurally flawless, with a clean lead, a clean lag and exactly %0d clocks, while the word it announces is %02h instead of %02h. The frame whose data is wrong is not the frame that is broken. The residue does not drain either: interval 4 inherited the same %0d orphan bits, so the stream stays wrong until something resets the slave, which is the mechanism behind `it works after a power cycle`. Two more faults leave every individual edge count defensible and are visible only as a RATIO across the capture -- a missing release made words outnumber assertions, 4 from 3, and a spurious release made assertions outnumber words, 3 delivering 2. And because the diagnosis needs a frame AFTER the broken one, the end of a capture is a blind spot: the identical truncation yielded %0d non-clean verdicts mid-sequence and %0d at the boundary, so the buffer length is part of the instrument. One result was not designed in and is the most useful thing here: the SPLIT word came back CORRECT, %02h, because a slave that does not clear on select simply carries on counting -- so the two possible slave designs fail on OPPOSITE faults, a non-clearing slave being poisoned by a truncation it survives a glitch through and a clearing slave being corrupted by a glitch it survives a truncation through. No slave is robust against both, so the framing discipline belongs to the master",
                     edge_log[2], NB, edge_log[3], word_log[3][7:0], W3, cin_log[4], ev_mid, ev_end,
                     word_log[9][7:0]);
        end else begin
            $display("FAIL: %0d error(s)", errors);
        end
        $finish;
    end


    initial begin
        cpol = 1'b0;
        cpha = 1'b0;
        b_sclk = 1'b0;
        b_cs_n = 1'b1;
        b_mosi = 1'b0;
        errors = 0;
        x_reports = 0;
        got_n = 0;
        clk = 1'b0;
        rst_n = 1'b1;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_diag_tb.vhd — the same bench in VHDL
-- spi_cs_diag_tb.vhd
--
-- FIVE CAPTURES, SEVENTEEN FRAMING INTERVALS, AND A SYMPTOM THAT ARRIVES ONE FRAME AFTER ITS CAUSE.
--
-- The same five results as the other two languages, in the same order and with the same numbers. The
-- fifth was not designed in: capture C produced it, and the third implementation is part of why it is
-- believable -- three independent spellings agreeing that a split word reassembles correctly is
-- evidence about the mechanism rather than about one simulator's scheduling.
--
-- THE ORACLE IS INDEPENDENT. `oracle_iv` models a slave that clears its bit counter only on reset,
-- written from that description rather than by calling anything the decoder uses, and it announces
-- words at the same instants. A disagreement is therefore a real one, and the bench requires agreement
-- on all seventeen intervals.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `LEAD_C`, `HALF_C`, `LAG_C`, `GAP_C`, `NB_C`, `DW_C`
-- and `W0_C`..`W4_C` all carry suffixes so no signal, variable or subprogram argument can shadow them
-- in another case. Re-read against that rule in full.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.spi_cs_pkg.all;

entity spi_cs_diag_tb is
end entity spi_cs_diag_tb;

architecture tb of spi_cs_diag_tb is

    constant LEAD_C : natural  := 3;
    constant HALF_C : natural  := 2;
    constant LAG_C  : natural  := 2;
    constant GAP_C  : natural  := 3;
    constant NB_C   : positive := 8;
    constant CNT_W  : positive := 8;

    subtype byte_t is std_logic_vector(7 downto 0);

    constant W0_C : byte_t := x"8D";
    constant W1_C : byte_t := x"C3";
    constant W2_C : byte_t := x"5A";
    constant W3_C : byte_t := x"3C";
    constant W4_C : byte_t := x"F0";

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '1';
    signal run   : boolean   := true;

    signal cpol : std_logic := '0';
    signal cpha : std_logic := '0';
    signal b_sclk : std_logic := '0';
    signal b_cs_n : std_logic := '1';
    signal b_mosi : std_logic := '0';

    signal dg_valid : std_logic;
    signal dg_code  : cs_diag_t;
    signal ob_edges, ob_carry_in, ob_carry_out, ob_words : natural;
    signal ob_asserts, ob_words_tot : natural;
    signal ob_word : std_logic_vector(DW_C - 1 downto 0);

    signal g_code : cs_diag_t := D_OK;
    signal g_edges, g_cin, g_cout, g_words : natural := 0;
    signal g_word : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
    signal g_n, g_x : natural := 0;

    type nat_arr  is array (natural range <>) of natural;
    type dg_arr   is array (natural range <>) of cs_diag_t;
    type byte_arr is array (natural range <>) of byte_t;

    function i2s (v : integer; w : natural) return string is
        constant S : string          := integer'image(v);
        constant P : string(1 to 40) := (others => ' ');
    begin
        if S'length >= w then return S; end if;
        return P(1 to w - S'length) & S;
    end function i2s;

    function hex8 (v : byte_t) return string is
        constant D : string := "0123456789abcdef";
        variable r : string(1 to 2);
        variable u : natural := to_integer(unsigned(v));
    begin
        r(1) := D(u / 16 + 1);
        r(2) := D(u mod 16 + 1);
        return r;
    end function hex8;

    function note_text (i : natural) return string is
    begin
        case i is
            when 2       => return "CUT SHORT -- leaves 5 orphan bits";
            when 16      => return "CUT SHORT -- and nothing follows it";
            when 3 | 12  => return "flawless frame, WRONG DATA";
            when 4       => return "still poisoned -- only a reset clears it";
            when 5       => return "16 clocks, 2 words, 1 assertion";
            when 8       => return "3 clocks then CS released";
            when 9       => return "5 more -- together exactly one word";
            when 11      => return "one word plus 3 orphan bits";
            when 13      => return "still poisoned";
            when others  => return "a whole word";
        end case;
    end function note_text;

begin

    clk_gen : process is
    begin
        while run loop
            clk <= '0'; wait for 5 ns;
            clk <= '1'; wait for 5 ns;
        end loop;
        wait;
    end process clk_gen;

    dut : entity work.spi_cs_diag
        generic map (NB_C => NB_C, CNT_W => CNT_W)
        port map (
            clk => clk, rst_n => rst_n,
            sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
            cpol => cpol, cpha => cpha,
            dg_valid => dg_valid, dg_code => dg_code,
            ob_edges => ob_edges, ob_carry_in => ob_carry_in, ob_carry_out => ob_carry_out,
            ob_words => ob_words, ob_word => ob_word,
            ob_asserts => ob_asserts, ob_words_tot => ob_words_tot
        );

    cap : process (clk) is
    begin
        if rising_edge(clk) then
            if dg_valid = '1' then
                g_code <= dg_code;  g_edges <= ob_edges;  g_cin <= ob_carry_in;
                g_cout <= ob_carry_out; g_words <= ob_words; g_word <= ob_word;
                g_n <= g_n + 1;
                -- The enumeration and the naturals cannot hold a metavalue, so the announced word is
                -- the only field that can be, and it is the only one guarded.
                for i in 0 to 7 loop
                    if ob_word(i) /= '0' and ob_word(i) /= '1' then g_x <= g_x + 1; end if;
                end loop;
            end if;
        end if;
    end process cap;

    stim : process is

        variable code_log : dg_arr(0 to 19);
        variable word_log : byte_arr(0 to 19);
        variable edge_log, cin_log, cout_log, ann_log : nat_arr(0 to 19);
        variable n_iv, mutations, e, base : natural := 0;
        variable ev_mid, ev_end : natural := 0;
        variable o_acc  : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
        variable o_nbits, o_words : natural := 0;
        variable o_last : byte_t := (others => '0');
        variable ln : line;

        procedure idle_n (n : natural) is
        begin
            for i in 1 to n loop
                wait until falling_edge(clk);
            end loop;
        end procedure idle_n;

        -- One framing interval: assert CS, drive `n` leading edges from a LEFT-ALIGNED field, release.
        --
        -- THE FIELD IS LEFT-ALIGNED FOR A REASON. SPI is MSB-first, so a frame cut short after five of
        -- eight clocks has transmitted the payload's TOP five bits, not its bottom five. Indexing from
        -- the low end sends bits the master would never have put on the wire first, and the corrupted
        -- word this chapter is about then mixes the wrong halves. The physics decides the indexing.
        procedure iv (bits : std_logic_vector(DW_C - 1 downto 0); n : natural) is
        begin
            b_sclk <= cpol; b_mosi <= '0';
            idle_n(2);
            b_cs_n <= '0';
            idle_n(1);
            b_mosi <= bits(DW_C - 1);
            idle_n(LEAD_C - 1);
            for k in 0 to n - 1 loop
                b_sclk <= not b_sclk;
                idle_n(HALF_C);
                b_sclk <= not b_sclk;
                if k < n - 1 then b_mosi <= bits(DW_C - 1 - k - 1); end if;
                idle_n(HALF_C);
            end loop;
            idle_n(LAG_C);
            b_cs_n <= '1';
            idle_n(1);
            b_sclk <= cpol;
            idle_n(GAP_C);
        end procedure iv;

        -- THE INDEPENDENT ORACLE: a slave that clears its bit counter only on reset, fed the same
        -- (bits, edges) stream and announcing at the same instants.
        procedure oracle_iv (bits : std_logic_vector(DW_C - 1 downto 0); n : natural) is
        begin
            o_words := 0;
            for k in 0 to n - 1 loop
                o_acc := o_acc(DW_C - 2 downto 0) & bits(DW_C - 1 - k);
                if o_nbits + 1 = NB_C then
                    o_nbits := 0;
                    o_last  := o_acc(7 downto 0);
                    o_words := o_words + 1;
                else
                    o_nbits := o_nbits + 1;
                end if;
            end loop;
        end procedure oracle_iv;

        procedure step (capn   : natural;
                        bits   : std_logic_vector(DW_C - 1 downto 0);
                        n      : natural;
                        wcode  : cs_diag_t;
                        wedges : natural;
                        wcin   : natural) is
        begin
            base := g_n;
            oracle_iv(bits, n);
            iv(bits, n);
            code_log(n_iv) := g_code;  edge_log(n_iv) := g_edges; cin_log(n_iv) := g_cin;
            cout_log(n_iv) := g_cout;  ann_log(n_iv)  := g_words;
            word_log(n_iv) := g_word(7 downto 0);

            write(ln, string'("   ") & i2s(capn, 1) & string'("   ") & i2s(n_iv, 2)
                      & string'("   ") & i2s(g_edges, 5) & string'("   ") & i2s(g_cin, 4)
                      & string'("   ") & i2s(g_cout, 5) & string'("   ") & i2s(g_words, 3)
                      & string'("    ") & hex8(g_word(7 downto 0)) & string'("   ")
                      & diag_name(g_code) & string'("  ") & diag_name(wcode)
                      & string'("  ") & note_text(n_iv));
            writeline(output, ln);

            if g_n - base /= 1 then
                write(ln, string'("  FAIL: interval ") & i2s(n_iv, 1) & string'(" produced ")
                          & i2s(g_n - base, 1) & string'(" verdicts for one assertion"));
                writeline(output, ln); e := e + 1;
            end if;
            if g_code /= wcode then
                write(ln, string'("  FAIL: interval ") & i2s(n_iv, 1) & string'(" diagnosed ")
                          & diag_name(g_code) & string'(" where ") & diag_name(wcode)
                          & string'(" was expected"));
                writeline(output, ln); e := e + 1;
            end if;
            if g_edges /= wedges then
                write(ln, string'("  FAIL: interval ") & i2s(n_iv, 1) & string'(" counted ")
                          & i2s(g_edges, 1) & string'(" edges where ") & i2s(wedges, 1)
                          & string'(" were driven"));
                writeline(output, ln); e := e + 1;
            end if;
            if g_cin /= wcin then
                write(ln, string'("  FAIL: interval ") & i2s(n_iv, 1) & string'(" inherited ")
                          & i2s(g_cin, 1) & string'(" orphan bits where ") & i2s(wcin, 1)
                          & string'(" were expected"));
                writeline(output, ln); e := e + 1;
            end if;
            if g_words /= o_words then
                write(ln, string'("  FAIL: interval ") & i2s(n_iv, 1) & string'(" announced ")
                          & i2s(g_words, 1) & string'(" words where the oracle announced ")
                          & i2s(o_words, 1));
                writeline(output, ln); e := e + 1;
            end if;
            if o_words > 0 and g_word(7 downto 0) /= o_last then
                write(ln, string'("  FAIL: interval ") & i2s(n_iv, 1) & string'(" predicted word ")
                          & hex8(g_word(7 downto 0)) & string'(" where the oracle says ")
                          & hex8(o_last));
                writeline(output, ln); e := e + 1;
            end if;
            n_iv := n_iv + 1;
        end procedure step;

        procedure recap is
        begin
            wait until falling_edge(clk);
            b_cs_n <= '1'; b_sclk <= cpol; b_mosi <= '0';
            idle_n(2); rst_n <= '0'; idle_n(3); rst_n <= '1'; idle_n(2);
            o_acc := (others => '0'); o_nbits := 0; o_last := (others => '0'); o_words := 0;
        end procedure recap;

    begin
        rst_n <= '1';
        wait until falling_edge(clk);
        rst_n <= '0';
        idle_n(4);
        rst_n <= '1';
        idle_n(4);

        write(ln, string'("  cap   iv   edges   c_in   c_out   ann   word   verdict       expected      note"));
        writeline(output, ln);

        -- ---------------- CAPTURE A: a truncation in the middle of a sequence ----------------
        write(ln, string'("  -- capture A: five frames, the THIRD cut short after 5 of 8 clocks"));
        writeline(output, ln);
        recap;
        step(0, W0_C & x"000000", 8, D_OK,        8, 0);
        step(0, W1_C & x"000000", 8, D_OK,        8, 0);
        step(0, W2_C & x"000000", 5, D_SHORT,     5, 0);
        step(0, W3_C & x"000000", 8, D_INHERITED, 8, 5);
        step(0, W4_C & x"000000", 8, D_INHERITED, 8, 5);

        -- ---------------- CAPTURE B: a missing release merges two frames ----------------
        write(ln, string'("  -- capture B: a MISSING release -- two words inside one assertion"));
        writeline(output, ln);
        recap;
        step(1, W0_C & W1_C & x"0000", 16, D_MERGED, 16, 0);
        step(1, W2_C & x"000000",       8, D_OK,      8, 0);
        step(1, W3_C & x"000000",       8, D_OK,      8, 0);

        -- ---------------- CAPTURE C: a spurious release splits one frame ----------------
        write(ln, string'("  -- capture C: a SPURIOUS release -- one word split across two assertions"));
        writeline(output, ln);
        recap;
        step(2, W0_C & x"000000", 3, D_SHORT,  3, 0);
        -- The second half of the SPLIT word: the five bits the first assertion did not send, so the two
        -- assertions together carry exactly the payload W0_C and nothing else.
        step(2, W0_C(4 downto 0) & "000" & x"000000", 5, D_GLITCH, 5, 3);
        step(2, W1_C & x"000000", 8, D_OK,     8, 0);

        -- ---------------- CAPTURE D: merged AND truncated ----------------
        write(ln, string'("  -- capture D: 11 clocks in one assertion -- merged and truncated at once"));
        writeline(output, ln);
        recap;
        step(3, W0_C & W1_C & x"0000", 11, D_RAGGED,    11, 0);
        step(3, W2_C & x"000000",       8, D_INHERITED,  8, 3);
        step(3, W3_C & x"000000",       8, D_INHERITED,  8, 3);

        -- ---------------- CAPTURE E: the same truncation, at the END of the capture ----------------
        write(ln, string'("  -- capture E: the SAME truncation as capture A, positioned LAST"));
        writeline(output, ln);
        recap;
        step(4, W0_C & x"000000", 8, D_OK,    8, 0);
        step(4, W1_C & x"000000", 8, D_OK,    8, 0);
        step(4, W2_C & x"000000", 5, D_SHORT, 5, 0);

        -- ================= 1. the broken frame and the wrong-data frame differ =================
        if not (code_log(2) = D_SHORT and code_log(3) = D_INHERITED
                and edge_log(3) = NB_C and cin_log(3) = 5) then
            write(ln, string'("  FAIL: capture A did not separate the broken frame from the wrong-data frame"));
            writeline(output, ln); e := e + 1;
        end if;
        if word_log(3) = W3_C then
            write(ln, string'("  FAIL: the frame after the truncation announced its own payload, so no corruption occurred and the chapter's claim is untested"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'(""));
        writeline(output, ln);
        write(ln, string'("    1. interval 2 was cut short after ") & i2s(edge_log(2), 1)
                  & string'(" of ") & i2s(NB_C, 1) & string'(" clocks. Interval 3 carried exactly ")
                  & i2s(edge_log(3), 1)
                  & string'(" clocks with a clean lead, a clean lag and no structural defect of any kind -- and the word it announced was ")
                  & hex8(word_log(3)) & string'(" where its payload was ") & hex8(W3_C)
                  & string'(", because ") & i2s(cin_log(3), 1)
                  & string'(" orphan bits from interval 2 were still sitting in the slave's shift register. THE FRAME WHOSE DATA IS WRONG IS NOT THE FRAME THAT IS BROKEN, and an engineer who captures the mismatching transaction captures interval 3 and finds nothing"));
        writeline(output, ln);

        -- ================= 2. the poisoning persists =================
        if cin_log(4) /= cin_log(3) then
            write(ln, string'("  FAIL: the inherited orphan count changed, so the claim that the residue persists is unsupported"));
            writeline(output, ln); e := e + 1;
        end if;
        if code_log(4) /= D_INHERITED then
            write(ln, string'("  FAIL: the second frame after the truncation was diagnosed ")
                      & diag_name(code_log(4)) & string'(" rather than INHERITED"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    2. interval 4 inherited the SAME ") & i2s(cin_log(4), 1)
                  & string'(" orphan bits and announced ") & hex8(word_log(4))
                  & string'(". The residue does not drain, because the counter that holds it is cleared by reset and by nothing else -- so every word from the truncation onward is a mix and the stream stays wrong indefinitely. That is the mechanism behind `it works again after a power cycle`, which is the single most misleading sentence in a bug report, because it points at supplies and sequencing rather than at framing"));
        writeline(output, ln);

        -- ================= 3. the sign of (asserts - words) =================
        if not (ann_log(5) = 2 and ann_log(6) = 1 and ann_log(7) = 1) then
            write(ln, string'("  FAIL: capture B did not deliver 4 words from 3 assertions"));
            writeline(output, ln); e := e + 1;
        end if;
        if not (ann_log(8) = 0 and ann_log(9) = 1 and ann_log(10) = 1) then
            write(ln, string'("  FAIL: capture C did not deliver 2 words from 3 assertions"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    3. capture B delivered ") & i2s(ann_log(5) + ann_log(6) + ann_log(7), 1)
                  & string'(" words from 3 assertions and capture C delivered ")
                  & i2s(ann_log(8) + ann_log(9) + ann_log(10), 1)
                  & string'(" from 3. A missing release makes words OUTNUMBER assertions; a spurious release makes assertions outnumber words. Every individual edge count in capture C is defensible on its own -- 3 clocks, then 5, then 8 -- and the fault is only visible as a RATIO across the capture, which is why a per-frame checker reports three unremarkable frames"));
        writeline(output, ln);

        -- ================= 4. the end of a capture is a blind spot =================
        for s in 2 to 4 loop
            if code_log(s) /= D_OK then ev_mid := ev_mid + 1; end if;
        end loop;
        for s in 14 to 16 loop
            if code_log(s) /= D_OK then ev_end := ev_end + 1; end if;
        end loop;
        if code_log(16) /= code_log(2) then
            write(ln, string'("  FAIL: the truncation at the end of capture E was diagnosed ")
                      & diag_name(code_log(16)) & string'(" rather than ") & diag_name(code_log(2))
                      & string'(", so it is not the same fault"));
            writeline(output, ln); e := e + 1;
        end if;
        if ev_end >= ev_mid then
            write(ln, string'("  FAIL: the boundary position yielded ") & i2s(ev_end, 1)
                      & string'(" pieces of evidence against ") & i2s(ev_mid, 1)
                      & string'(" mid-sequence; the claim is that it yields fewer"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    4. the SAME truncation was diagnosed ") & diag_name(code_log(2))
                  & string'(" in both positions, and it produced ") & i2s(ev_mid, 1)
                  & string'(" non-clean verdicts mid-sequence against ") & i2s(ev_end, 1)
                  & string'(" at the end of the capture. The structural evidence survives; the DATA symptom does not, because there is no following frame left to corrupt. So a capture that stops at the fault shows a short frame and no consequence, and a capture that stops one frame earlier shows nothing at all -- which makes `capture more than you think you need` a measured requirement rather than folklore"));
        writeline(output, ln);

        -- ================= 5. the two slave designs fail on OPPOSITE faults =================
        -- Not designed in advance -- capture C produced it. The split word came back CORRECT.
        if word_log(9) /= W0_C then
            write(ln, string'("  FAIL: the split word reassembled as ") & hex8(word_log(9))
                      & string'(" rather than ") & hex8(W0_C)
                      & string'(", so the claim that a spurious release is data-transparent on this slave is wrong"));
            writeline(output, ln); e := e + 1;
        end if;
        if word_log(3) = W3_C then
            write(ln, string'("  FAIL: the truncation did not corrupt the following word, so the contrast in result 5 does not exist"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    5. and the measurement produced a result that was not designed in: the SPLIT word came back CORRECT. Interval 9 announced ")
                  & hex8(word_log(9))
                  & string'(", exactly the payload interval 8 began, because a slave that does not clear on select simply carries on counting and a spurious release costs it nothing. So the two possible slave designs fail on OPPOSITE faults -- a non-clearing slave survives a glitch and is poisoned indefinitely by a truncation, while a slave that clears on every select survives a truncation by losing one word and recovering, and is corrupted by a glitch it cannot even see. There is no design that is robust against both, which means the framing discipline has to be guaranteed by the master and cannot be recovered by the slave. And it means a debugger has to know which slave is in front of them before interpreting any of these captures"));
        writeline(output, ln);

        -- ================= BENCH INTEGRITY =================
        if code_log(3) /= D_OK then mutations := mutations + 1; end if;
        if cin_log(3)  /= 0     then mutations := mutations + 1; end if;
        if mutations /= 2 then
            write(ln, string'("  FAIL: a deliberately wrong expectation did not mismatch (")
                      & i2s(mutations, 1) & string'(" of 2)"));
            writeline(output, ln); e := e + 1;
        end if;
        if g_x /= 0 then
            write(ln, string'("  FAIL: ") & i2s(g_x, 1) & string'(" reported fields carried X"));
            writeline(output, ln); e := e + 1;
        end if;
        if n_iv /= 17 then
            write(ln, string'("  FAIL: ") & i2s(n_iv, 1)
                      & string'(" intervals were driven where 17 were expected"));
            writeline(output, ln); e := e + 1;
        end if;

        if e = 0 then
            write(ln, string'(""));
            writeline(output, ln);
            write(ln, string'("    and the bench proved itself: two deliberately wrong expectations mismatched, every reported field carried a known value, and the decoder's prediction of a non-clearing slave's output matched an independent model on all ")
                      & i2s(n_iv, 1) & string'(" intervals"));
            writeline(output, ln);
            write(ln, string'("PASS: chip-select faults are the only family in this module whose EVIDENCE lives in a different frame from its SYMPTOM. A frame cut short after ")
                      & i2s(edge_log(2), 1) & string'(" of ") & i2s(NB_C, 1)
                      & string'(" clocks leaves orphan bits in a slave whose bit counter is cleared by reset and not by the select -- and the NEXT frame is then structurally flawless, with a clean lead, a clean lag and exactly ")
                      & i2s(edge_log(3), 1) & string'(" clocks, while the word it announces is ")
                      & hex8(word_log(3)) & string'(" instead of ") & hex8(W3_C)
                      & string'(". The frame whose data is wrong is not the frame that is broken. The residue does not drain either: interval 4 inherited the same ")
                      & i2s(cin_log(4), 1)
                      & string'(" orphan bits, so the stream stays wrong until something resets the slave, which is the mechanism behind `it works after a power cycle`. Two more faults leave every individual edge count defensible and are visible only as a RATIO across the capture -- a missing release made words outnumber assertions, 4 from 3, and a spurious release made assertions outnumber words, 3 delivering 2. And because the diagnosis needs a frame AFTER the broken one, the end of a capture is a blind spot: the identical truncation yielded ")
                      & i2s(ev_mid, 1) & string'(" non-clean verdicts mid-sequence and ")
                      & i2s(ev_end, 1)
                      & string'(" at the boundary, so the buffer length is part of the instrument. One result was not designed in and is the most useful thing here: the SPLIT word came back CORRECT, ")
                      & hex8(word_log(9))
                      & string'(", because a slave that does not clear on select simply carries on counting -- so the two possible slave designs fail on OPPOSITE faults, a non-clearing slave being poisoned by a truncation it survives a glitch through and a clearing slave being corrupted by a glitch it survives a truncation through. No slave is robust against both, so the framing discipline belongs to the master"));
            writeline(output, ln);
        else
            write(ln, string'("FAIL: ") & i2s(e, 1) & string'(" error(s)"));
            writeline(output, ln);
        end if;

        run <= false;
        wait;
    end process stim;

end architecture tb;

11. The Transaction Boundary Decides Which Bugs Are Expressible

A UVM monitor for SPI almost always emits one transaction per CS assertion. It is the obvious boundary: the select frames the transfer, so the select frames the transaction.

That choice makes this bug inexpressible.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   monitor  →  txn[N]   { edges: 5, data: partial }
               txn[N+1] { edges: 8, data: 0x59 }     ← the mismatch is reported here
   scoreboard compares txn[N+1] against expected 0x3c and reports an error
   every field of txn[N+1] is correct except the payload

The scoreboard has no access to the fact that matters, because the fact is not in either transaction — it is in the relationship between them.

12. What This Decoder Cannot Do

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ✗ diagnose a truncation that is the last event in the capture (no data symptom)
   ✗ tell a spurious release from a truncation until the FOLLOWING assertion
   ✗ say anything about a slave that clears its counter on select — for that design
     every capture here is clean, and a glitch becomes the fatal fault instead
   ✗ distinguish a master that released CS early from one whose clock stopped early
     — both produce an assertion carrying too few edges

That last one is a genuine ambiguity and it is worth naming. D_SHORT says this assertion carried fewer clocks than a word. Whether the master ended the frame or the clock source stopped is not visible in the framing at all: the evidence would be in the lag between the final edge and the release, which is Chapter 18.1's EV_CSBND measurement. The two chapters compose — a short frame with a normal lag is a deliberate early release, and a short frame with a stretched lag is a clock that stopped — and neither decoder makes that inference on its own.

13. Why an FPGA Engineer Cares

Truncated frames come from one of a small number of places, and all of them are in your RTL: a bit counter that compares with == against a value it can skip past, a state machine that leaves the transfer state on a condition other than the count, a CS driven from a register written a cycle early, or a DMA that ran out of data.

The decoder is cheap — two counters and a comparator per assertion — but the more valuable output is the assertion, not the diagnostic: every CS assertion shall carry exactly NB leading edges. That is one property, it is checkable in simulation and in hardware, and it catches the fault at the source instead of catching its consequence one frame later.

And if you are debugging a board rather than a simulation: extend the capture. The one change that matters most in this chapter costs nothing but buffer depth.

14. Why an ASIC Engineer Cares

If you are building the master, this is a protocol violation you own and it is cheap to prove absent. If you are building the slave, the design decision in section 2 is yours to make and to document — and the table in section 8 says you cannot make it safely, which means the datasheet has to state which behaviour the part has.

That documentation obligation is the real deliverable. A slave that does not clear on select is not defective; it is a part with a stated requirement that the master send whole words. A slave whose datasheet is silent on the point is a part that will be integrated wrongly, and the integration failure will present as intermittent corruption cleared by a power cycle on somebody else's board, months later, with your part named in the bug report.

15. Failure Signature — "The Slave Is Unreliable"

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom     an SPI stream is correct for a while, then every word is wrong
   Trigger set on the first mismatching word
   Captured    that word's frame: 8 clocks, clean lead, clean lag, correct
               edges, correct mode, correct bit order -- nothing wrong with it
   Concluded   the slave corrupts data intermittently
   Escalated   to the slave's vendor, who cannot reproduce it
   Actual      four frames earlier, a DMA underrun released CS after 5 of 8
               clocks; the slave's bit counter never cleared
   Found       by someone who extended the capture backwards and noticed one
               assertion with five clocks in it

Every step was competent. The trigger was set on the symptom, which is where you set a trigger. The captured frame was examined thoroughly, and it was genuinely faultless. The conclusion followed from the evidence available — and the evidence available was the wrong evidence, because for this fault family the symptom and the cause are in different frames and the trigger was on the symptom.

16. Common Misconceptions

MisconceptionWhat is actually true
The frame with wrong data is the frame with the faultFor CS faults it is usually the frame after
A structurally perfect frame carries correct dataNot if the slave inherited orphan bits from before it
"It works after a power cycle" implicates suppliesIt implicates state inside the slave — a counter is first on the list
A slave can be made robust against CS faultsThe two designs fail on opposite faults; neither is safe against both
A short frame is always a master releasing earlyA clock that stopped early looks identical in the framing
One transaction per CS assertion is the natural boundaryIt is, and it makes this bug inexpressible in the scoreboard
A trigger on the mismatch captures the faultThe fault is earlier, and it is structural rather than data

17. Reason It Through

18. Understanding Check

19. Summary

Chip-select faults are the only family in this module whose evidence lives in a different frame from its symptom. A frame cut short after 5 of 8 clocks leaves orphan bits in a slave whose bit counter is cleared by reset and not by the select — and the next frame is then structurally flawless, with a clean lead, a clean lag and exactly eight clocks, while the word it announces is 0x59 instead of 0x3c. The frame whose data is wrong is not the frame that is broken.

The residue does not drain. The following frame inherited the same five orphan bits, so the stream stays wrong until something resets the slave — which is the mechanism behind it works after a power cycle, an observation that is entirely accurate and points at supplies rather than at framing.

Two further faults leave every individual edge count defensible and are visible only as a ratio across the capture: a missing release made words outnumber assertions, four from three, and a spurious release made assertions outnumber words, three delivering two. Because the diagnosis needs a frame after the broken one, the end of a capture is a blind spot — the identical truncation yielded three non-clean verdicts mid-sequence and one at the boundary, so buffer depth is part of the instrument.

And one result was not designed in, and is the most useful thing here: the split word came back correct, because a slave that does not clear on select simply carries on counting. So the two possible slave designs fail on opposite faults — one poisoned by a truncation it survives a glitch through, the other corrupted by a glitch it survives a truncation through. No slave is robust against both, which makes framing a master obligation and makes which slave is this a question to settle before interpreting any capture.

20. What Comes Next

Everything so far has counted things — edges, bits, displacements, assertions. Chapter 18.6 turns to read corruption on MISO, where the two candidate causes are separated by neither a count nor a value but by what happens when you slow the clock down — and where one of the two causes cannot be modelled in RTL at all, which the chapter says out loud rather than pretending otherwise.

Continue learning