Skip to content
VLSI Mentor

SPI · Module 18

Wrong CPOL, Wrong CPHA, Wrong Sampling Edge

Three mode faults share one symptom and two share the identical wrong byte. Separating them needs three different kinds of observation — a static level, a transition time, and an elimination — plus the discipline to decline when the payload makes the measurement impossible.

Chapter 18.1 classified the capture and stopped. Its most common verdict is EV_WELL — a frame that is structurally perfect and carries the wrong data — and that verdict is deliberately unhelpful: it says the payload is now worth looking at and nothing more.

This chapter takes that capture and asks which of three mode faults produced it.

Two of the three produce the identical wrong byte. If your evidence is a decoded byte, you have already lost the ability to tell them apart.

1. Three Causes, One Symptom

FaultWhat is wrongWhere it lives
CPOLSCLK rests at the wrong levelmaster configuration
CPHAthe master launches each bit on the wrong edgemaster timing
EDGEthe receiver samples on the wrong edgeinside the slave

All three are usually described as "wrong SPI mode", and that phrase is where the debugging stops being productive. They are three different defects, in two different devices, fixed by three different changes — and the reason they get collapsed into one is that they share a symptom.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   symptom:   the receiver reports 0x4a where 0xa5 was sent
   causes:    CPOL?    CPHA?    EDGE?
   evidence:  a decoded byte  →  separates NONE of them

A shifted byte is the symptom of all three. Anything that only sees bytes — a software log, a scoreboard mismatch report, a driver's error counter, a logic analyser configured with the wrong mode — is looking at the one piece of evidence that cannot discriminate.

2. The Three Observations Are Three Different Kinds

This is the structural point of the chapter, and it is why the decoder below is organised the way it is.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CPOL   →  a STATIC observation      a level, during an interval with no data in it
   CPHA   →  a TIMING observation      the instant a pin changed, relative to an edge
   EDGE   →  an ELIMINATION            the wire was right and the receiver disagreed

Three faults, three observations, and no two of them are the same kind of measurement. That is not a coincidence of this protocol; it is what makes the set separable at all. If two causes required the same kind of observation, no amount of care with that observation would split them — which is exactly the situation the blind spot in section 7 produces artificially.

3. Why CPOL Has To Be Checked First, And Separately

The natural instinct is to treat the three as three branches of one test. They are not symmetric, and the asymmetry has a cause worth stating precisely:

"Leading edge" is defined relative to the idle level.

Invert CPOL and the physical edge that counts as leading swaps with the one that counts as trailing. So a CPOL fault does not merely park the clock at the wrong level — it also moves the data, because every subsequent definition in the protocol is anchored to that level.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CPOL=0 link:   idle LOW    rising edge is LEADING   falling is TRAILING
   master idles HIGH by mistake:
                  idle HIGH   the RISING edge is now the trailing one
                  → every bit is captured one edge later than intended
                  → the receiver reports a shifted byte

Which means a CPOL fault presents as a phase fault to anything reading bytes. The static observation is what separates them, and it has to be taken first, because once data is involved the two are indistinguishable.

There is a second reason it must be separate. The static observation needs an interval with no data in it — the deselected gap. A decoder that only looks inside transactions never sees the evidence at all.

4. Where The Discriminating Observation Lives

Correct launch and faulted launch differ by one transition time

14 cycles
Five rows over fourteen cycles. Chip select falls at cycle one. SCLK produces a leading edge at cycle four and a trailing edge at cycle seven. The correct MOSI row places bit seven at cycle two, before the leading edge. The faulted MOSI row places bit seven at cycle four, on the leading edge. A bottom row marks the correct launch instant and the late one.select fallsselect fallscorrect launch: before edge 0correct launch: before edge0edge 0 — faulted launch lands hereedge 0 — faulted launchlands heretrailing edge: correct master advancestrailing edge: correctmaster advancescs_nsclkmosi okXXb7b7b7b7b7b6b6b6b6b6XXmosi badXXXXb7b7b7b7b7b7b6b6XXlaunchokoklatelatelatelatelatelatelatelatelatelatet0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 1 — the same 8-bit mode-0 frame driven two ways. A correct CPHA=0 master places each bit on MOSI BEFORE the leading edge that captures it; the faulted master places it ON that edge. The two waveforms differ by a single transition time, carry the same bit values, and are decoded into different bytes. Horizontal scrolling is expected on a narrow screen — the figure is a timeline, not a layout.

Read the two MOSI rows against each other. They carry the same bit values in the same order. Every difference between them is a transition time. A viewer showing decoded bytes shows one number for each and no reason for the difference; a viewer showing pins shows the reason and requires you to know what to measure.

The launch row is the decoder's own observation, not a bus signal. That is the recurring shape of this module: the thing that discriminates is never on the wire.

5. The Launch Observation Needs Two Numbers, Not One

Here is the trap that makes a naive implementation of section 4 fail.

The obvious measurement is how many edges had completed when MOSI first changed. For an 8-bit mode-0 frame:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   correct CPHA=0 master   first MOSI change at edge count 0   (it is in the lead)
   faulted master          first MOSI change at edge count 0   (it is ON edge 0)

Identical. The faulted launch coincides with edge 0, and "edges completed" is still zero at the instant the edge occurs. A decoder measuring only the count exonerates the fault.

The second number is the coincidence itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ob_first_move     the edge count when MOSI first changed
   ob_first_at_edge  whether that change happened ON an edge

Expected values are 0 and cpha_exp respectively — a CPHA=0 master must move in the gap, a CPHA=1 master must move on the leading edge. The bench asserts both halves of this: that the count is the same for the fault and the correct link, and that the coincidence differs. The first of those checks is unusual and worth noticing — it is a test that a measurement fails, written down so that a later change which accidentally makes the count discriminate cannot quietly invalidate the chapter's argument.

6. Exoneration Requires An Applicable Observation

This is the most important line in the decoder, and the version without it was confidently wrong.

Diagnosing EDGE means asserting that the transmitter is blameless. The only evidence for that assertion is the launch-timing observation. So:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   observation applied  and  launch timing correct   →  transmitter blameless
   observation did NOT apply                         →  transmitter UNREFUTED

Those are different claims. "I checked and it was fine" and "I could not check" support entirely different conclusions, and collapsing them is how a diagnostic produces a confident answer pointing at the wrong device.

There is a second reason the wire looks innocent in that case, and it is a result from earlier in the track. Chapter 16.5 measured it: a launch that lands on the capture edge is invisible to a data monitor, because a monitor observes an edge one cycle after the pin moved and therefore reads the value just launched. So the decoder's own capture of a CPHA-faulted mode-0 frame matches the expectation exactly. A data comparison cannot stand in for the timing observation — it agrees with the fault.

7. The Blind Spot Belongs To The Payload

The launch observation needs MOSI to change. If the first bit of the payload already equals MOSI's idle level, a correct launch produces no transition at all, and there is nothing to timestamp.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   payload 0xa5 = 1 0 1 0 0 1 0 1   first bit 1, idle 0  →  a transition exists
   payload 0x55 = 0 1 0 1 0 1 0 1   first bit 0, idle 0  →  no transition to time

The decoder detects this from inputs it already has — it latches MOSI's idle level at the select and compares it against the first bit of the expected word — and sets ob_launch_valid low. It then refuses both to use the timing and to exonerate the transmitter on the strength of it, reporting D_UNDECIDED.

Note what kind of answer that is. It is not a failure to diagnose; it is a diagnosis of the capture: this experiment cannot answer the question, re-run it with a payload whose first bit is 1. Somebody can act on that.

8. The Measurement

Six captures, one 8-bit mode-0 link, identical output from all three languages:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  idle  launch_ok  move@  at_edge  shift  rx==wire  diagnosis  expected   stimulus
     0          1      0        0      0         1  OK         OK          everything correct, payload 0xA5
     1          1      0        0      2         0  CPOL       CPOL        master idles SCLK high  (F_CPOL)
     0          1      0        1      0         0  CPHA       CPHA        master launches on the capture edge  (F_CPHA)
     0          1      0        0      0         0  EDGE       EDGE        pins correct, receiver reports a shift  (F_EDGE)
     0          0      2        1      0         0  UNDECIDED  UNDECIDED   the same F_CPHA, payload 0x55  (unmeasurable)
     0          0      1        1      0         1  OK         OK          a CORRECT link, payload 0x55  (no false alarm)

Five columns are worth reading carefully.

Row 1 against row 2 — idle. The only fault that moves this column is CPOL, and it moves without any data being involved. Row 2's shift of 2 means the captured word matched none of the three candidate alignments, which is what a CPOL fault does to a data stream: it is not a clean one-bit shift, because inverting the idle level changes which edges are captures at all.

Rows 3 and 4 — the pair the chapter is about. Both report rx==wire = 0, both produced the same wrong byte 0x4a, and their shift columns are identical at 0. Every value-based column agrees. The only column that separates them is at_edge.

Row 3 against row 1 — move@ and at_edge together. move@ is 0 for both the fault and the correct link, exactly as section 5 predicts. at_edge is 1 and 0. One number would have missed it.

Rows 5 and 6 — launch_ok = 0. The same faulted link that produced row 3 produces UNDECIDED here, and the difference between the two rows is one bit of the payload. Row 6 is the same payload on a correct link, reported OK.

9. Building It — Three HDLs

The decoder takes two inputs a bus monitor normally does not have: word_exp, what the transfer should have carried, and word_rx, what the receiver reported. That is deliberate and it is the chapter's design claim.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_diag.sv — the mode decoder — three faults, three different kinds of observation, and a verdict that can decline
// spi_mode_diag.sv
//
// Chapter 18.2 -- the three mode faults, and which pairs a capture can separate.
//
// THE SITUATION. Chapter 18.1's triage decoder has reported EV_WELL: the frame is well formed, its
// boundaries are sound, no sampled bit was in motion -- and the data is wrong. Three faults produce
// exactly that, and they live in different places:
//
//     F_CPOL   the master idles SCLK at the wrong level.
//     F_CPHA   the master LAUNCHES on the wrong edge for the phase it was configured for.
//     F_EDGE   the master is correct and the RECEIVER samples on the wrong edge.
//
// The last two are the interesting pair, because they produce the SAME WRONG WORD. A capture of the
// decoded bytes cannot tell them apart. What separates them is a pin observation, and the answer
// says which END of the link to fix -- which is the whole value of the measurement.
//
// THIS DECODER TAKES TWO INPUTS A BUS MONITOR NORMALLY DOES NOT, and they are the reason it can
// blame a device it cannot see:
//
//     word_exp   what the transfer was supposed to carry -- also used, in a way worth noticing, to
//                decide whether the launch-timing observation APPLIES at all
//     word_rx    what the RECEIVER reported receiving -- the failing symptom itself
//
// A pin capture alone cannot diagnose a receiver. A pin capture plus the receiver's own report can,
// because the capture decides whether the WIRE was right, and the report decides whether the
// RECEIVER agreed with the wire. Those are different questions and the pair of them is what
// separates the two faults nobody can separate from bytes.
//
// THIS DECODER PUBLISHES FOUR INDEPENDENT OBSERVATIONS AND ONE DIAGNOSIS.
//
//   idle_bad         SCLK is not at CPOL while the bus is deselected. STATIC -- no data, no edges,
//                    no frame needed. The cheapest measurement in the module.
//
//   first_move       how many SCLK edges had occurred when MOSI first CHANGED inside the
//                    transaction, and --
//   first_at_edge    whether that change COINCIDED with an SCLK edge.
//
//                    Two numbers, not one, and the second is the one that does the work. A correct
//                    CPHA=0 master places its first bit during the lead: no edges yet, and not on an
//                    edge -- (0, 0). A master that launches on the leading edge instead has also
//                    seen no edges at that instant, so the count alone is IDENTICAL -- (0, 1). The
//                    first version of this decoder published only the count and reported a faulted
//                    master and a correct one as the same thing.
//
//                    So the expected pair is `first_move = 0` and `first_at_edge = cpha_exp`: a
//                    CPHA=1 master is SUPPOSED to launch on the first edge. This is a property of
//                    the MASTER's launch timing and of nothing else, which is what lets it separate
//                    a transmitter fault from a receiver fault.
//
//   wire_shift       the word the DECODER captured at the expected edges, against the expected one,
//                    tried at shifts -1, 0 and +1. Mode faults move data by exactly one bit
//                    position; anything else is a different chapter.
//
//   rx_agrees        did the receiver's report match what the wire actually carried at the expected
//                    edges? This is the observation that convicts a receiver.
//
// THE DIAGNOSIS ORDER IS THE DISCRIMINATION ARGUMENT.
//
//   1. idle_bad                       -> F_CPOL. Decided without looking at data at all.
//   2. first_move /= expected         -> F_CPHA. The master's launch moved: a MASTER fault.
//   3. wire correct, receiver's report -> F_EDGE. The wire carried the right bits at the right
//      wrong                             instants and the receiver still reported something else,
//                                        so the receiver sampled somewhere else: a RECEIVER fault.
//   4. otherwise                      -> D_OK.
//
// Step 3 is the one worth pausing on. It is a diagnosis by ELIMINATION: the decoder never observes
// the receiver's sampling, it observes that the transmitter is blameless -- and a blameless
// transmitter with a disagreeing receiver leaves one place for the fault to be. That is the
// strongest statement a bus capture can make about a device it cannot see.
//
// AND THE DISCRIMINATOR KNOWS WHEN IT DOES NOT APPLY, WHICH IS THE BEST PART.
//
// `first_move` can only measure a launch that produces a TRANSITION. If the payload's first bit
// already equals the level MOSI was idling at, a correct master places it and nothing moves -- so
// there is no instant to timestamp, and both a correct link and a phase-faulted one produce their
// first transition somewhere later. The observation carries no information about either.
//
// The decoder detects this from inputs it already has: it latches MOSI's idle level at the select
// and compares it against the first bit of `word_exp`. When they match it sets `ob_launch_valid` low
// and REFUSES both to use the timing and -- the part that matters more -- to EXONERATE the
// transmitter on the strength of an observation that did not apply. Diagnosing a receiver fault is
// an assertion that the transmitter is blameless, and an inapplicable observation leaves the
// transmitter unrefuted rather than blameless. Those are different claims, and the difference is
// which end of the link somebody is sent to.
//
// A diagnostic that reports "my discriminator does not apply to this capture, and here is why" is
// worth more than one that is right most of the time, because the first can be acted on -- change
// the payload and measure again -- and the second cannot be distinguished from luck. Chapter 18.3
// generalises the underlying point: the debug pattern decides what a capture can tell you.

`timescale 1ns/1ps

module spi_mode_diag #(
    parameter int DW    = 32,
    parameter int LEN_W = 6,
    parameter int CNT_W = 16
) (
    input  wire              clk,       // the OBSERVER's clock
    input  wire              rst_n,

    // --- the pins -----------------------------------------------------------
    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,

    // --- what the traffic was SUPPOSED to be --------------------------------
    input  wire              cpol_exp,
    input  wire              cpha_exp,
    input  wire [LEN_W-1:0]  len,
    input  wire [DW-1:0]     word_exp,
    // The failing symptom, supplied by whatever reported it -- a driver's receive register, a
    // software log, a scoreboard mismatch. The decoder does not trust it; it compares it.
    input  wire [DW-1:0]     word_rx,

    // --- the observations, once per transaction ------------------------------
    output reg               dg_valid,
    output reg  [2:0]        dg_code,     // CPOL, CPHA, EDGE, OTHER, OK
    output reg               ob_idle_bad,
    output reg  [CNT_W-1:0]  ob_first_move,   // edges seen when MOSI first changed
    output reg               ob_first_at_edge,// ... and did that change coincide with an edge?
    output reg               ob_moved,        // did MOSI change at all inside the frame?
    output reg  signed [2:0] ob_shift,        // -1, 0, +1, or 2 for "no shift matches"
    output reg  [DW-1:0]     ob_word,         // the word as captured at the expected edges
    output reg               ob_rx_agrees,    // did the receiver's report match the wire?
    // Is the launch-timing observation applicable to this capture at all? Low when the payload's
    // first bit equals MOSI's idle level, because then a correct launch produces no transition.
    output reg               ob_launch_valid
);

    // FIVE CODES, AND THE FOURTH IS THE ONE THAT MAKES THE OTHER THREE TRUSTWORTHY.
    //
    // D_OTHER means "the wire is wrong and none of this chapter's three causes explains it". A
    // diagnostic that always names one of its own causes is a diagnostic that cannot be wrong, and
    // therefore cannot be informative -- so this one is allowed to say "not mine", and the next
    // chapter owns what it hands over.
    localparam [2:0] D_CPOL      = 3'd0,
                     D_CPHA      = 3'd1,
                     D_EDGE      = 3'd2,
                     D_UNDECIDED = 3'd3,   // the discriminator does not apply to this capture
                     D_OTHER     = 3'd4,   // the wire is wrong and none of the three explains it
                     D_OK        = 3'd5;

    reg sclk_d, cs_n_d, mosi_d;

    wire cs_assert   = ~cs_n &  cs_n_d;
    wire cs_deassert =  cs_n & ~cs_n_d;
    wire in_txn      = ~cs_n | cs_deassert;
    wire sclk_edge   = sclk ^ sclk_d;

    // "Leading" means SCLK left its EXPECTED idle level. That definition is why a CPOL fault also
    // moves data: invert the idle level and the physical edge that counts as leading swaps with the
    // one that counts as trailing. A reader who has only the decoded bytes therefore sees a CPOL
    // fault and a CPHA fault as the same symptom -- which is the reason `idle_bad` is checked first
    // and separately.
    wire leading = sclk_edge & (sclk != cpol_exp);
    wire capture = (cpha_exp ? (sclk_edge & ~leading) : leading) & in_txn;

    // THE IDLE OBSERVATION IS PER-TRANSACTION, IN TWO STAGES, and the first version got this wrong
    // in a way that made every later verdict useless: a single sticky flag latched on the first
    // offending idle cycle and never cleared, so once one transaction had seen a wrong idle level
    // EVERY subsequent transaction was diagnosed as a CPOL fault. A diagnostic whose first verdict
    // poisons the rest is worse than no diagnostic, because the report is self-consistent.
    //
    //   idle_bad_pend   accumulates over the deselected interval
    //   idle_bad_txn    sampled from it at the assert, so the verdict at the release describes the
    //                   gap that PRECEDED this frame
    reg             idle_bad_pend;
    reg             idle_bad_txn;

    reg [CNT_W-1:0] edges;
    reg [CNT_W-1:0] first_move;
    reg             first_at_edge;
    reg             moved;
    reg             mosi_idle;      // MOSI's level on the cycle before the select fell
    reg             launch_valid;
    reg [DW-1:0]    acc;
    reg [LEN_W:0]   nseen;

    // The bit position the k-th captured bit belongs to. MSB-first throughout this chapter: bit
    // order is Chapter 18.3's subject and mixing the two would make every result ambiguous.
    wire [LEN_W-1:0] cur_idx = len - 1'b1 - nseen[LEN_W-1:0];
    wire [DW-1:0]    bit_m   = {{(DW-1){1'b0}}, mosi} << cur_idx;

    wire [DW-1:0] mask     = ({{(DW-1){1'b0}}, 1'b1} << len) - {{(DW-1){1'b0}}, 1'b1};
    wire [DW-1:0] acc_now  = (capture && (nseen < {1'b0, len})) ? (acc | bit_m) : acc;

    // The captured word against the expectation at three shifts. A mode fault moves data by exactly
    // one bit position, so the search is deliberately narrow: widening it to arbitrary rotations
    // would make this decoder agree with Chapter 18.3's, and then neither would discriminate.
    wire [DW-1:0] exp_m  = word_exp & mask;
    wire [DW-1:0] got_m  = acc_now  & mask;
    wire [DW-1:0] rx_m   = word_rx & mask;
    wire m_zero   = (got_m == exp_m);
    wire m_left   = (got_m == ((exp_m << 1) & mask));
    wire m_right  = (got_m == ((exp_m >> 1) & mask));
    // Does the receiver agree with what the wire actually carried at the expected edges? Note that
    // this is NOT "did the receiver get the expected word" -- that is the symptom. This asks whether
    // the receiver and the wire tell the same story.
    wire rx_ok    = (rx_m == got_m);

    // A correct master's first MOSI change happens before any edge has completed -- during the lead
    // for CPHA=0 and on the first edge itself for CPHA=1 -- so the expected COUNT is zero either way
    // and the expected COINCIDENCE is the phase.
    wire [CNT_W-1:0] first_move_exp = {CNT_W{1'b0}};
    wire             first_at_edge_exp = cpha_exp;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_d        <= 1'b0;
            cs_n_d        <= 1'b1;
            mosi_d        <= 1'b0;
            idle_bad_pend <= 1'b0;
            idle_bad_txn  <= 1'b0;
            edges         <= {CNT_W{1'b0}};
            first_move    <= {CNT_W{1'b0}};
            first_at_edge <= 1'b0;
            moved         <= 1'b0;
            acc           <= {DW{1'b0}};
            nseen         <= {(LEN_W+1){1'b0}};
            dg_valid      <= 1'b0;
            dg_code       <= D_OK;
            ob_idle_bad   <= 1'b0;
            ob_first_move <= {CNT_W{1'b0}};
            ob_first_at_edge <= 1'b0;
            ob_moved      <= 1'b0;
            ob_shift      <= 3'sd0;
            ob_word       <= {DW{1'b0}};
            ob_rx_agrees  <= 1'b1;
            mosi_idle     <= 1'b0;
            launch_valid  <= 1'b0;
            ob_launch_valid <= 1'b0;
        end else begin
            sclk_d   <= sclk;
            cs_n_d   <= cs_n;
            mosi_d   <= mosi;
            dg_valid <= 1'b0;

            // THE STATIC OBSERVATION. It needs no frame, no edges and no data -- which is exactly
            // why it is the first thing to look at and the last thing anybody does.
            if (cs_n && (sclk !== cpol_exp)) idle_bad_pend <= 1'b1;

            if (cs_assert) begin
                idle_bad_txn  <= idle_bad_pend;
                idle_bad_pend <= 1'b0;
                // MOSI's level as the transaction opens, and from it whether a correct launch of
                // this payload's first bit would produce anything to observe.
                mosi_idle     <= mosi_d;
                launch_valid  <= (word_exp[len - 1'b1] !== mosi_d);
                edges         <= {CNT_W{1'b0}};
                first_move    <= {CNT_W{1'b0}};
                first_at_edge <= 1'b0;
                moved         <= 1'b0;
                acc        <= {DW{1'b0}};
                nseen      <= {(LEN_W+1){1'b0}};
            end else begin
                // MOSI's first change inside the frame, stamped with the edge count at that moment.
                // This is a property of the MASTER's launch timing alone: no receiver is involved,
                // which is what makes it able to separate a master fault from a receiver fault.
                if (in_txn && (mosi !== mosi_d) && !moved) begin
                    moved         <= 1'b1;
                    first_move    <= edges;
                    first_at_edge <= sclk_edge;
                end
                if (capture && (nseen < {1'b0, len})) begin
                    acc   <= acc | bit_m;
                    nseen <= nseen + 1'b1;
                end
                if (sclk_edge && in_txn) edges <= edges + 1'b1;
            end

            if (cs_deassert) begin
                dg_valid      <= 1'b1;
                ob_word       <= got_m;
                ob_rx_agrees  <= rx_ok;
                ob_idle_bad   <= idle_bad_txn;
                ob_launch_valid <= launch_valid;
                ob_moved      <= moved;
                ob_first_move    <= first_move;
                ob_first_at_edge <= first_at_edge;
                ob_shift      <= m_zero  ? 3'sd0
                               : m_left  ? 3'sd1
                               : m_right ? -3'sd1
                               :           3'sd2;   // no single-position shift explains it

                // THE DIAGNOSIS. Ordered as the discrimination argument: a static observation
                // first, then a transmitter observation, then an elimination.
                if (idle_bad_txn)
                    dg_code <= D_CPOL;
                // The launch-timing branch is gated on the observation being APPLICABLE. Without
                // that gate the decoder times whatever transition happens to come first and reports
                // a phase fault for a payload that simply started with a bit it was already
                // holding.
                else if (launch_valid && moved
                         && ((first_move != first_move_exp)
                             || (first_at_edge !== first_at_edge_exp)))
                    dg_code <= D_CPHA;

                // EXONERATION REQUIRES AN APPLICABLE OBSERVATION, and this branch is the whole
                // reason the applicability flag exists.
                //
                // Diagnosing a RECEIVER fault means asserting that the transmitter is blameless. The
                // only evidence for that is the launch-timing observation -- so if the observation
                // did not apply, the transmitter is not blameless, it is merely UNREFUTED, and those
                // are different claims.
                //
                // The version of this decoder that skipped this branch reported a phase fault as a
                // receiver fault whenever the payload made the timing unmeasurable, and it did so
                // confidently. It would have sent an engineer to the wrong end of the link, which is
                // the most expensive kind of wrong answer a diagnostic can give.
                //
                // Note also WHY the wire looks right in that case: a launch that lands on the
                // capture edge is invisible to a data monitor, because a monitor observes an edge one
                // cycle after the pin moved and therefore reads the value just launched. Chapter 16.5
                // measured exactly that blindness, and it is the reason a data comparison cannot
                // stand in for the timing observation here.
                else if (!launch_valid && !rx_ok)
                    dg_code <= D_UNDECIDED;

                else if (m_zero && !rx_ok)
                    dg_code <= D_EDGE;

                else if (!m_zero)
                    // The wire is wrong, the idle level is right, and the launch timing is right and
                    // applicable. None of this chapter's three causes explains that, and naming one
                    // anyway would be a guess dressed as a diagnosis.
                    dg_code <= D_OTHER;
                else
                    dg_code <= D_OK;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_diag.v — the same design in Verilog-2001
// spi_mode_diag.v
//
// Chapter 18.2 -- the three mode faults, and which pairs a capture can separate.
//
// THE SITUATION. Chapter 18.1's triage decoder has reported EV_WELL: the frame is well formed, its
// boundaries are sound, no sampled bit was in motion -- and the data is wrong. Three faults produce
// exactly that, and they live in different places:
//
//     F_CPOL   the master idles SCLK at the wrong level.
//     F_CPHA   the master LAUNCHES on the wrong edge for the phase it was configured for.
//     F_EDGE   the master is correct and the RECEIVER samples on the wrong edge.
//
// The last two are the interesting pair, because they produce the SAME WRONG WORD. A capture of the
// decoded bytes cannot tell them apart. What separates them is a pin observation, and the answer
// says which END of the link to fix -- which is the whole value of the measurement.
//
// THIS DECODER TAKES TWO INPUTS A BUS MONITOR NORMALLY DOES NOT, and they are the reason it can
// blame a device it cannot see:
//
//     word_exp   what the transfer was supposed to carry -- also used, in a way worth noticing, to
//                decide whether the launch-timing observation APPLIES at all
//     word_rx    what the RECEIVER reported receiving -- the failing symptom itself
//
// A pin capture alone cannot diagnose a receiver. A pin capture plus the receiver's own report can,
// because the capture decides whether the WIRE was right, and the report decides whether the
// RECEIVER agreed with the wire. Those are different questions and the pair of them is what
// separates the two faults nobody can separate from bytes.
//
// THIS DECODER PUBLISHES FOUR INDEPENDENT OBSERVATIONS AND ONE DIAGNOSIS.
//
//   idle_bad         SCLK is not at CPOL while the bus is deselected. STATIC -- no data, no edges,
//                    no frame needed. The cheapest measurement in the module.
//
//   first_move       how many SCLK edges had occurred when MOSI first CHANGED inside the
//                    transaction, and --
//   first_at_edge    whether that change COINCIDED with an SCLK edge.
//
//                    Two numbers, not one, and the second is the one that does the work. A correct
//                    CPHA=0 master places its first bit during the lead: no edges yet, and not on an
//                    edge -- (0, 0). A master that launches on the leading edge instead has also
//                    seen no edges at that instant, so the count alone is IDENTICAL -- (0, 1). The
//                    first version of this decoder published only the count and reported a faulted
//                    master and a correct one as the same thing.
//
//                    So the expected pair is `first_move = 0` and `first_at_edge = cpha_exp`: a
//                    CPHA=1 master is SUPPOSED to launch on the first edge. This is a property of
//                    the MASTER's launch timing and of nothing else, which is what lets it separate
//                    a transmitter fault from a receiver fault.
//
//   wire_shift       the word the DECODER captured at the expected edges, against the expected one,
//                    tried at shifts -1, 0 and +1. Mode faults move data by exactly one bit
//                    position; anything else is a different chapter.
//
//   rx_agrees        did the receiver's report match what the wire actually carried at the expected
//                    edges? This is the observation that convicts a receiver.
//
// THE DIAGNOSIS ORDER IS THE DISCRIMINATION ARGUMENT.
//
//   1. idle_bad                       -> F_CPOL. Decided without looking at data at all.
//   2. first_move /= expected         -> F_CPHA. The master's launch moved: a MASTER fault.
//   3. wire correct, receiver's report -> F_EDGE. The wire carried the right bits at the right
//      wrong                             instants and the receiver still reported something else,
//                                        so the receiver sampled somewhere else: a RECEIVER fault.
//   4. otherwise                      -> D_OK.
//
// Step 3 is the one worth pausing on. It is a diagnosis by ELIMINATION: the decoder never observes
// the receiver's sampling, it observes that the transmitter is blameless -- and a blameless
// transmitter with a disagreeing receiver leaves one place for the fault to be. That is the
// strongest statement a bus capture can make about a device it cannot see.
//
// AND THE DISCRIMINATOR KNOWS WHEN IT DOES NOT APPLY, WHICH IS THE BEST PART.
//
// `first_move` can only measure a launch that produces a TRANSITION. If the payload's first bit
// already equals the level MOSI was idling at, a correct master places it and nothing moves -- so
// there is no instant to timestamp, and both a correct link and a phase-faulted one produce their
// first transition somewhere later. The observation carries no information about either.
//
// The decoder detects this from inputs it already has: it latches MOSI's idle level at the select
// and compares it against the first bit of `word_exp`. When they match it sets `ob_launch_valid` low
// and REFUSES both to use the timing and -- the part that matters more -- to EXONERATE the
// transmitter on the strength of an observation that did not apply. Diagnosing a receiver fault is
// an assertion that the transmitter is blameless, and an inapplicable observation leaves the
// transmitter unrefuted rather than blameless. Those are different claims, and the difference is
// which end of the link somebody is sent to.
//
// A diagnostic that reports "my discriminator does not apply to this capture, and here is why" is
// worth more than one that is right most of the time, because the first can be acted on -- change
// the payload and measure again -- and the second cannot be distinguished from luck. Chapter 18.3
// generalises the underlying point: the debug pattern decides what a capture can tell you.

`timescale 1ns/1ps

module spi_mode_diag #(
    parameter DW    = 32,
    parameter LEN_W = 6,
    parameter CNT_W = 16
) (
    input  wire              clk,       // the OBSERVER's clock
    input  wire              rst_n,

    // --- the pins -----------------------------------------------------------
    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,

    // --- what the traffic was SUPPOSED to be --------------------------------
    input  wire              cpol_exp,
    input  wire              cpha_exp,
    input  wire [LEN_W-1:0]  len,
    input  wire [DW-1:0]     word_exp,
    // The failing symptom, supplied by whatever reported it -- a driver's receive register, a
    // software log, a scoreboard mismatch. The decoder does not trust it; it compares it.
    input  wire [DW-1:0]     word_rx,

    // --- the observations, once per transaction ------------------------------
    output reg               dg_valid,
    output reg  [2:0]        dg_code,     // CPOL, CPHA, EDGE, OTHER, OK
    output reg               ob_idle_bad,
    output reg  [CNT_W-1:0]  ob_first_move,   // edges seen when MOSI first changed
    output reg               ob_first_at_edge,// ... and did that change coincide with an edge?
    output reg               ob_moved,        // did MOSI change at all inside the frame?
    output reg  signed [2:0] ob_shift,        // -1, 0, +1, or 2 for "no shift matches"
    output reg  [DW-1:0]     ob_word,         // the word as captured at the expected edges
    output reg               ob_rx_agrees,    // did the receiver's report match the wire?
    // Is the launch-timing observation applicable to this capture at all? Low when the payload's
    // first bit equals MOSI's idle level, because then a correct launch produces no transition.
    output reg               ob_launch_valid
);

    // FIVE CODES, AND THE FOURTH IS THE ONE THAT MAKES THE OTHER THREE TRUSTWORTHY.
    //
    // D_OTHER means "the wire is wrong and none of this chapter's three causes explains it". A
    // diagnostic that always names one of its own causes is a diagnostic that cannot be wrong, and
    // therefore cannot be informative -- so this one is allowed to say "not mine", and the next
    // chapter owns what it hands over.
    localparam [2:0] D_CPOL      = 3'd0,
                     D_CPHA      = 3'd1,
                     D_EDGE      = 3'd2,
                     D_UNDECIDED = 3'd3,   // the discriminator does not apply to this capture
                     D_OTHER     = 3'd4,   // the wire is wrong and none of the three explains it
                     D_OK        = 3'd5;

    reg sclk_d, cs_n_d, mosi_d;

    wire cs_assert   = ~cs_n &  cs_n_d;
    wire cs_deassert =  cs_n & ~cs_n_d;
    wire in_txn      = ~cs_n | cs_deassert;
    wire sclk_edge   = sclk ^ sclk_d;

    // "Leading" means SCLK left its EXPECTED idle level. That definition is why a CPOL fault also
    // moves data: invert the idle level and the physical edge that counts as leading swaps with the
    // one that counts as trailing. A reader who has only the decoded bytes therefore sees a CPOL
    // fault and a CPHA fault as the same symptom -- which is the reason `idle_bad` is checked first
    // and separately.
    wire leading = sclk_edge & (sclk != cpol_exp);
    wire capture = (cpha_exp ? (sclk_edge & ~leading) : leading) & in_txn;

    // THE IDLE OBSERVATION IS PER-TRANSACTION, IN TWO STAGES, and the first version got this wrong
    // in a way that made every later verdict useless: a single sticky flag latched on the first
    // offending idle cycle and never cleared, so once one transaction had seen a wrong idle level
    // EVERY subsequent transaction was diagnosed as a CPOL fault. A diagnostic whose first verdict
    // poisons the rest is worse than no diagnostic, because the report is self-consistent.
    //
    //   idle_bad_pend   accumulates over the deselected interval
    //   idle_bad_txn    sampled from it at the assert, so the verdict at the release describes the
    //                   gap that PRECEDED this frame
    reg             idle_bad_pend;
    reg             idle_bad_txn;

    reg [CNT_W-1:0] edges;
    reg [CNT_W-1:0] first_move;
    reg             first_at_edge;
    reg             moved;
    reg             mosi_idle;      // MOSI's level on the cycle before the select fell
    reg             launch_valid;
    reg [DW-1:0]    acc;
    reg [LEN_W:0]   nseen;

    // The bit position the k-th captured bit belongs to. MSB-first throughout this chapter: bit
    // order is Chapter 18.3's subject and mixing the two would make every result ambiguous.
    wire [LEN_W-1:0] cur_idx = len - 1'b1 - nseen[LEN_W-1:0];
    wire [DW-1:0]    bit_m   = {{(DW-1){1'b0}}, mosi} << cur_idx;

    wire [DW-1:0] mask     = ({{(DW-1){1'b0}}, 1'b1} << len) - {{(DW-1){1'b0}}, 1'b1};
    wire [DW-1:0] acc_now  = (capture && (nseen < {1'b0, len})) ? (acc | bit_m) : acc;

    // The captured word against the expectation at three shifts. A mode fault moves data by exactly
    // one bit position, so the search is deliberately narrow: widening it to arbitrary rotations
    // would make this decoder agree with Chapter 18.3's, and then neither would discriminate.
    wire [DW-1:0] exp_m  = word_exp & mask;
    wire [DW-1:0] got_m  = acc_now  & mask;
    wire [DW-1:0] rx_m   = word_rx & mask;
    wire m_zero   = (got_m == exp_m);
    wire m_left   = (got_m == ((exp_m << 1) & mask));
    wire m_right  = (got_m == ((exp_m >> 1) & mask));
    // Does the receiver agree with what the wire actually carried at the expected edges? Note that
    // this is NOT "did the receiver get the expected word" -- that is the symptom. This asks whether
    // the receiver and the wire tell the same story.
    wire rx_ok    = (rx_m == got_m);

    // A correct master's first MOSI change happens before any edge has completed -- during the lead
    // for CPHA=0 and on the first edge itself for CPHA=1 -- so the expected COUNT is zero either way
    // and the expected COINCIDENCE is the phase.
    wire [CNT_W-1:0] first_move_exp = {CNT_W{1'b0}};
    wire             first_at_edge_exp = cpha_exp;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_d        <= 1'b0;
            cs_n_d        <= 1'b1;
            mosi_d        <= 1'b0;
            idle_bad_pend <= 1'b0;
            idle_bad_txn  <= 1'b0;
            edges         <= {CNT_W{1'b0}};
            first_move    <= {CNT_W{1'b0}};
            first_at_edge <= 1'b0;
            moved         <= 1'b0;
            acc           <= {DW{1'b0}};
            nseen         <= {(LEN_W+1){1'b0}};
            dg_valid      <= 1'b0;
            dg_code       <= D_OK;
            ob_idle_bad   <= 1'b0;
            ob_first_move <= {CNT_W{1'b0}};
            ob_first_at_edge <= 1'b0;
            ob_moved      <= 1'b0;
            ob_shift      <= 3'sd0;
            ob_word       <= {DW{1'b0}};
            ob_rx_agrees  <= 1'b1;
            mosi_idle     <= 1'b0;
            launch_valid  <= 1'b0;
            ob_launch_valid <= 1'b0;
        end else begin
            sclk_d   <= sclk;
            cs_n_d   <= cs_n;
            mosi_d   <= mosi;
            dg_valid <= 1'b0;

            // THE STATIC OBSERVATION. It needs no frame, no edges and no data -- which is exactly
            // why it is the first thing to look at and the last thing anybody does.
            if (cs_n && (sclk !== cpol_exp)) idle_bad_pend <= 1'b1;

            if (cs_assert) begin
                idle_bad_txn  <= idle_bad_pend;
                idle_bad_pend <= 1'b0;
                // MOSI's level as the transaction opens, and from it whether a correct launch of
                // this payload's first bit would produce anything to observe.
                mosi_idle     <= mosi_d;
                launch_valid  <= (word_exp[len - 1'b1] !== mosi_d);
                edges         <= {CNT_W{1'b0}};
                first_move    <= {CNT_W{1'b0}};
                first_at_edge <= 1'b0;
                moved         <= 1'b0;
                acc        <= {DW{1'b0}};
                nseen      <= {(LEN_W+1){1'b0}};
            end else begin
                // MOSI's first change inside the frame, stamped with the edge count at that moment.
                // This is a property of the MASTER's launch timing alone: no receiver is involved,
                // which is what makes it able to separate a master fault from a receiver fault.
                if (in_txn && (mosi !== mosi_d) && !moved) begin
                    moved         <= 1'b1;
                    first_move    <= edges;
                    first_at_edge <= sclk_edge;
                end
                if (capture && (nseen < {1'b0, len})) begin
                    acc   <= acc | bit_m;
                    nseen <= nseen + 1'b1;
                end
                if (sclk_edge && in_txn) edges <= edges + 1'b1;
            end

            if (cs_deassert) begin
                dg_valid      <= 1'b1;
                ob_word       <= got_m;
                ob_rx_agrees  <= rx_ok;
                ob_idle_bad   <= idle_bad_txn;
                ob_launch_valid <= launch_valid;
                ob_moved      <= moved;
                ob_first_move    <= first_move;
                ob_first_at_edge <= first_at_edge;
                ob_shift      <= m_zero  ? 3'sd0
                               : m_left  ? 3'sd1
                               : m_right ? -3'sd1
                               :           3'sd2;   // no single-position shift explains it

                // THE DIAGNOSIS. Ordered as the discrimination argument: a static observation
                // first, then a transmitter observation, then an elimination.
                if (idle_bad_txn)
                    dg_code <= D_CPOL;
                // The launch-timing branch is gated on the observation being APPLICABLE. Without
                // that gate the decoder times whatever transition happens to come first and reports
                // a phase fault for a payload that simply started with a bit it was already
                // holding.
                else if (launch_valid && moved
                         && ((first_move != first_move_exp)
                             || (first_at_edge !== first_at_edge_exp)))
                    dg_code <= D_CPHA;

                // EXONERATION REQUIRES AN APPLICABLE OBSERVATION, and this branch is the whole
                // reason the applicability flag exists.
                //
                // Diagnosing a RECEIVER fault means asserting that the transmitter is blameless. The
                // only evidence for that is the launch-timing observation -- so if the observation
                // did not apply, the transmitter is not blameless, it is merely UNREFUTED, and those
                // are different claims.
                //
                // The version of this decoder that skipped this branch reported a phase fault as a
                // receiver fault whenever the payload made the timing unmeasurable, and it did so
                // confidently. It would have sent an engineer to the wrong end of the link, which is
                // the most expensive kind of wrong answer a diagnostic can give.
                //
                // Note also WHY the wire looks right in that case: a launch that lands on the
                // capture edge is invisible to a data monitor, because a monitor observes an edge one
                // cycle after the pin moved and therefore reads the value just launched. Chapter 16.5
                // measured exactly that blindness, and it is the reason a data comparison cannot
                // stand in for the timing observation here.
                else if (!launch_valid && !rx_ok)
                    dg_code <= D_UNDECIDED;

                else if (m_zero && !rx_ok)
                    dg_code <= D_EDGE;

                else if (!m_zero)
                    // The wire is wrong, the idle level is right, and the launch timing is right and
                    // applicable. None of this chapter's three causes explains that, and naming one
                    // anyway would be a guess dressed as a diagnosis.
                    dg_code <= D_OTHER;
                else
                    dg_code <= D_OK;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_diag.vhd — the same design in VHDL
-- spi_mode_diag.vhd
--
-- Chapter 18.2 -- the three mode faults, and which pairs a capture can separate.
--
-- THE SITUATION. Chapter 18.1's triage decoder has reported EV_WELL: the frame is well formed, its
-- boundaries are sound, no sampled bit was in motion -- and the data is wrong. Three faults produce
-- exactly that, and they live in different places:
--
--     F_CPOL   the master idles SCLK at the wrong level.
--     F_CPHA   the master LAUNCHES on the wrong edge for the phase it was configured for.
--     F_EDGE   the master is correct and the RECEIVER samples on the wrong edge.
--
-- The last two are the interesting pair, because they produce the SAME WRONG WORD. A capture of the
-- decoded bytes cannot tell them apart. What separates them is a pin observation, and the answer
-- says which END of the link to fix -- which is the whole value of the measurement.
--
-- THIS DECODER TAKES TWO INPUTS A BUS MONITOR NORMALLY DOES NOT, and they are the reason it can
-- blame a device it cannot see:
--
--     word_exp   what the transfer was supposed to carry -- also used, in a way worth noticing, to
--                decide whether the launch-timing observation APPLIES at all
--     word_rx    what the RECEIVER reported receiving -- the failing symptom itself
--
-- A pin capture alone cannot diagnose a receiver. A pin capture plus the receiver's own report can,
-- because the capture decides whether the WIRE was right, and the report decides whether the
-- RECEIVER agreed with the wire. Those are different questions and the pair of them is what
-- separates the two faults nobody can separate from bytes.
--
-- THIS DECODER PUBLISHES FOUR INDEPENDENT OBSERVATIONS AND ONE DIAGNOSIS.
--
--   idle_bad         SCLK is not at CPOL while the bus is deselected. STATIC -- no data, no edges,
--                    no frame needed. The cheapest measurement in the module.
--
--   first_move       how many SCLK edges had occurred when MOSI first CHANGED inside the
--                    transaction, and --
--   first_at_edge    whether that change COINCIDED with an SCLK edge.
--
--                    Two numbers, not one, and the second is the one that does the work. A correct
--                    CPHA=0 master places its first bit during the lead: no edges yet, and not on an
--                    edge -- (0, 0). A master that launches on the leading edge instead has also
--                    seen no edges at that instant, so the count alone is IDENTICAL -- (0, 1). The
--                    first version of this decoder published only the count and reported a faulted
--                    master and a correct one as the same thing.
--
--                    So the expected pair is `first_move = 0` and `first_at_edge = cpha_exp`: a
--                    CPHA=1 master is SUPPOSED to launch on the first edge. This is a property of
--                    the MASTER's launch timing and of nothing else, which is what lets it separate
--                    a transmitter fault from a receiver fault.
--
--   wire_shift       the word the DECODER captured at the expected edges, against the expected one,
--                    tried at shifts -1, 0 and +1. Mode faults move data by exactly one bit
--                    position; anything else is a different chapter.
--
--   rx_agrees        did the receiver's report match what the wire actually carried at the expected
--                    edges? This is the observation that convicts a receiver.
--
-- THE DIAGNOSIS ORDER IS THE DISCRIMINATION ARGUMENT.
--
--   1. idle_bad                       -> F_CPOL. Decided without looking at data at all.
--   2. first_move /= expected         -> F_CPHA. The master's launch moved: a MASTER fault.
--   3. wire correct, receiver's report -> F_EDGE. The wire carried the right bits at the right
--      wrong                             instants and the receiver still reported something else,
--                                        so the receiver sampled somewhere else: a RECEIVER fault.
--   4. otherwise                      -> D_OK.
--
-- Step 3 is the one worth pausing on. It is a diagnosis by ELIMINATION: the decoder never observes
-- the receiver's sampling, it observes that the transmitter is blameless -- and a blameless
-- transmitter with a disagreeing receiver leaves one place for the fault to be. That is the
-- strongest statement a bus capture can make about a device it cannot see.
--
-- AND THE DISCRIMINATOR KNOWS WHEN IT DOES NOT APPLY, WHICH IS THE BEST PART.
--
-- `first_move` can only measure a launch that produces a TRANSITION. If the payload's first bit
-- already equals the level MOSI was idling at, a correct master places it and nothing moves -- so
-- there is no instant to timestamp, and both a correct link and a phase-faulted one produce their
-- first transition somewhere later. The observation carries no information about either.
--
-- The decoder detects this from inputs it already has: it latches MOSI's idle level at the select
-- and compares it against the first bit of `word_exp`. When they match it sets `ob_launch_valid` low
-- and REFUSES both to use the timing and -- the part that matters more -- to EXONERATE the
-- transmitter on the strength of an observation that did not apply. Diagnosing a receiver fault is
-- an assertion that the transmitter is blameless, and an inapplicable observation leaves the
-- transmitter unrefuted rather than blameless. Those are different claims, and the difference is
-- which end of the link somebody is sent to.
--
-- A diagnostic that reports "my discriminator does not apply to this capture, and here is why" is
-- worth more than one that is right most of the time, because the first can be acted on -- change
-- the payload and measure again -- and the second cannot be distinguished from luck. Chapter 18.3
-- generalises the underlying point: the debug pattern decides what a capture can tell you.

--
-- WHAT VHDL ADDS HERE: the diagnosis is an ENUMERATION with six named values, two of which are
-- refusals, so a reader sees at the declaration that this instrument is allowed to decline. In the
-- Verilog versions those six are `3'd0` through `3'd5` and the refusals look like any other code.
--
-- The observations travel as a RECORD, which matters for a different reason: the record makes it
-- syntactically obvious that `launch_valid` is one of the observations rather than an internal flag,
-- and the applicability of an observation is exactly the thing this chapter is about.
--
-- IDENTIFIER REVIEW (VHDL is CASE-INSENSITIVE). The generics are DW_C, LEN_W, and no signal,
-- variable, constant, port or subprogram argument reuses those spellings in any case. `cpol_exp` and
-- `cpha_exp` keep their `_exp` suffix rather than being `CPOL`/`cpol`, because `cpol` is also a field
-- name a reader would reach for. Nothing here is distinguished from anything else only by case.

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

package spi_mode_pkg is

    constant DW_C : natural := 32;

    -- SIX VERDICTS, AND TWO OF THEM ARE REFUSALS. Declaring them as a type puts that fact in front of
    -- a reader at the point of definition.
    --
    --   D_UNDECIDED  the discriminator does not apply to this capture
    --   D_OTHER      the wire is wrong and none of this chapter's three causes explains it
    type mode_diag_t is (D_CPOL, D_CPHA, D_EDGE, D_UNDECIDED, D_OTHER, D_OK);

    -- The observations, as one object. `launch_valid` sits alongside the others deliberately: the
    -- applicability of an observation is itself an observation.
    type mode_obs_t is record
        idle_bad      : boolean;   -- SCLK was off CPOL during the preceding idle interval
        moved         : boolean;   -- MOSI changed at all inside the frame
        first_move    : natural;   -- edges completed when it first changed
        first_at_edge : boolean;   -- ... and whether that change coincided with an edge
        launch_valid  : boolean;   -- is the launch-timing observation applicable at all?
        rx_agrees     : boolean;   -- did the receiver's report match the wire?
        shift         : integer;   -- wire against expectation: -1, 0, +1, or 2 for "none of these"
        word          : std_logic_vector(DW_C - 1 downto 0);
    end record;

    constant OBS_ZERO : mode_obs_t :=
        (false, false, 0, false, false, true, 0, (others => '0'));

    function diag_name (d : mode_diag_t) return string;

    -- Log arrays for the bench. They live in the package so the bench's declarations stay readable;
    -- an unconstrained array type plus a constrained variable is the VHDL way to say "six of these".
    type mode_diag_t_vector is array (natural range <>) of mode_diag_t;
    type nat_vector          is array (natural range <>) of natural;
    type bool_vector          is array (natural range <>) of boolean;
    type rx_log_t             is array (natural range <>) of std_logic_vector(DW_C - 1 downto 0);

end package spi_mode_pkg;

package body spi_mode_pkg is
    function diag_name (d : mode_diag_t) return string is
    begin
        case d is
            when D_CPOL      => return "CPOL     ";
            when D_CPHA      => return "CPHA     ";
            when D_EDGE      => return "EDGE     ";
            when D_UNDECIDED => return "UNDECIDED";
            when D_OTHER     => return "OTHER    ";
            when others      => return "OK       ";
        end case;
    end function diag_name;
end package body spi_mode_pkg;

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

entity spi_mode_diag is
    generic (
        LEN_W : positive := 6
    );
    port (
        clk      : in  std_logic;
        rst_n    : in  std_logic;

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

        cpol_exp : in  std_logic;
        cpha_exp : in  std_logic;
        len      : in  unsigned(LEN_W - 1 downto 0);
        word_exp : in  std_logic_vector(DW_C - 1 downto 0);
        -- The failing symptom, supplied by whatever reported it. The decoder does not trust it; it
        -- compares it.
        word_rx  : in  std_logic_vector(DW_C - 1 downto 0);

        dg_valid : out std_logic;
        dg_code  : out mode_diag_t;
        obs      : out mode_obs_t
    );
end entity spi_mode_diag;

architecture rtl of spi_mode_diag is
    signal v_r : std_logic   := '0';
    signal d_r : mode_diag_t := D_OK;
    signal o_r : mode_obs_t  := OBS_ZERO;
begin

    dg_valid <= v_r;
    dg_code  <= d_r;
    obs      <= o_r;

    process (clk, rst_n) is
        variable sclk_d, cs_n_d, mosi_d : std_logic;
        variable cs_assert, cs_deassert : boolean;
        variable in_txn, sclk_edge      : boolean;
        variable leading, capture       : boolean;
        variable idle_pend, idle_txn    : boolean;
        variable edges                  : natural;
        variable first_move             : natural;
        variable first_at_edge          : boolean;
        variable moved                  : boolean;
        variable launch_valid           : boolean;
        variable acc                    : std_logic_vector(DW_C - 1 downto 0);
        variable nseen                  : natural;
        variable idx                    : natural;
        variable mask, exp_m, got_m, rx_m : std_logic_vector(DW_C - 1 downto 0);
        variable m_zero, m_left, m_right, rx_ok : boolean;
        variable shift_v                : integer;
        variable nb                     : natural;
        variable pr                     : mode_obs_t;
    begin
        if rst_n = '0' then
            sclk_d := '0'; cs_n_d := '1'; mosi_d := '0';
            idle_pend := false; idle_txn := false;
            edges := 0; first_move := 0; first_at_edge := false; moved := false;
            launch_valid := false;
            acc := (others => '0'); nseen := 0;
            v_r <= '0'; d_r <= D_OK; o_r <= OBS_ZERO;

        elsif rising_edge(clk) then
            v_r <= '0';
            nb  := to_integer(len);

            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" means SCLK left its EXPECTED idle level. That definition is why a CPOL fault
            -- also moves data: invert the idle level and the physical edge that counts as leading
            -- swaps with the one that counts as trailing.
            leading     := sclk_edge and (sclk /= cpol_exp);
            if cpha_exp = '0' then capture := leading and in_txn;
            else                   capture := sclk_edge and (not leading) and in_txn;
            end if;

            -- THE STATIC OBSERVATION, accumulated over the deselected interval and sampled at the
            -- select, so the verdict at the release describes the gap that PRECEDED this frame. A
            -- single sticky flag would let one bad transaction poison every later verdict.
            if (cs_n = '1') and (sclk /= cpol_exp) then idle_pend := true; end if;

            if cs_assert then
                idle_txn     := idle_pend;
                idle_pend    := false;
                -- Would a correct launch of this payload's first bit produce anything to observe?
                launch_valid := (word_exp(nb - 1) /= mosi_d);
                edges := 0; first_move := 0; first_at_edge := false; moved := false;
                acc := (others => '0'); nseen := 0;
            else
                -- MOSI's first change inside the frame, stamped with the edge count AND with whether
                -- it coincided with an edge. Two numbers, because the count alone is identical for a
                -- correct CPHA=0 master and one that launches on the leading edge.
                if in_txn and (mosi /= mosi_d) and not moved then
                    moved         := true;
                    first_move    := edges;
                    first_at_edge := sclk_edge;
                end if;
                if capture and nseen < nb then
                    idx      := nb - 1 - nseen;
                    acc(idx) := mosi;
                    nseen    := nseen + 1;
                end if;
                if sclk_edge and in_txn then edges := edges + 1; end if;
            end if;

            if cs_deassert then
                mask  := (others => '0');
                for i in 0 to DW_C - 1 loop
                    if i < nb then mask(i) := '1'; end if;
                end loop;
                exp_m := word_exp and mask;
                got_m := acc      and mask;
                rx_m  := word_rx  and mask;

                -- A mode fault moves data by exactly one bit position, so the search is deliberately
                -- narrow: widening it to arbitrary rotations would make this decoder agree with
                -- Chapter 18.3's, and then neither would discriminate.
                m_zero  := (got_m = exp_m);
                m_left  := (got_m = (std_logic_vector(shift_left(unsigned(exp_m), 1)) and mask));
                m_right := (got_m = (std_logic_vector(shift_right(unsigned(exp_m), 1)) and mask));
                -- NOT "did the receiver get the expected word" -- that is the symptom. This asks
                -- whether the receiver and the wire tell the same story.
                rx_ok   := (rx_m = got_m);

                if    m_zero  then shift_v := 0;
                elsif m_left  then shift_v := 1;
                elsif m_right then shift_v := -1;
                else               shift_v := 2;
                end if;

                pr := (idle_bad      => idle_txn,
                       moved         => moved,
                       first_move    => first_move,
                       first_at_edge => first_at_edge,
                       launch_valid  => launch_valid,
                       rx_agrees     => rx_ok,
                       shift         => shift_v,
                       word          => got_m);

                v_r <= '1';
                o_r <= pr;

                -- THE DIAGNOSIS, ordered as the discrimination argument: a static observation, then a
                -- transmitter observation, then two refusals, then an elimination.
                if idle_txn then
                    d_r <= D_CPOL;
                elsif launch_valid and moved
                      and ((first_move /= 0) or (first_at_edge /= (cpha_exp = '1'))) then
                    d_r <= D_CPHA;
                -- EXONERATION REQUIRES AN APPLICABLE OBSERVATION. Diagnosing a RECEIVER fault means
                -- asserting that the transmitter is blameless, and the only evidence for that is the
                -- launch-timing observation -- so if it did not apply, the transmitter is not
                -- blameless, it is merely UNREFUTED. Those are different claims, and the difference
                -- is which end of the link somebody is sent to.
                elsif (not launch_valid) and (not rx_ok) then
                    d_r <= D_UNDECIDED;
                elsif m_zero and (not rx_ok) then
                    d_r <= D_EDGE;
                elsif not m_zero then
                    d_r <= D_OTHER;
                else
                    d_r <= D_OK;
                end if;
            end if;

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

end architecture rtl;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_diag_tb.sv — six captures, the pair bytes cannot separate, and a blind spot that belongs to the payload
// spi_mode_diag_tb.sv
//
// FIVE CAPTURES, THREE MODE FAULTS, AND THE PAIR THAT BYTES CANNOT SEPARATE.
//
// Every stimulus is a well-formed frame -- Chapter 18.1's decoder would report EV_WELL on all of
// them -- carrying the wrong data. That is the whole point: triage has already said the capture is
// admissible, and the question is now which of three causes produced it.
//
// THE MEASUREMENTS.
//
//   1. THE THREE FAULTS ARE SEPARATED, AND THE OBSERVATION THAT SEPARATES EACH ONE IS DIFFERENT.
//      CPOL falls out of a STATIC observation with no data involved. CPHA falls out of the master's
//      LAUNCH TIMING. EDGE falls out of an ELIMINATION -- the wire was right and the receiver
//      disagreed with it.
//
//   2. CPHA AND EDGE PRODUCE THE SAME WRONG WORD. Both receivers report the same shifted byte. The
//      bench prints the reported word for both and requires them to be IDENTICAL, which is what
//      makes the discrimination a real result rather than a restatement of the stimulus.
//
//   3. THE DISCRIMINATOR HAS A BLIND SPOT, AND IT IS THE PAYLOAD'S FAULT. `first_move` needs MOSI to
//      change early. A payload whose first bit equals the idle level produces no transition to time,
//      and the CPHA fault then reports the same observations as the EDGE fault. The bench drives the
//      same fault with two payloads and reports both diagnoses.
//
//   4. THE DIAGNOSTIC WILL SAY "NOT MINE". A wrong wire with a correct idle level and correct launch
//      timing is reported as D_OTHER rather than forced into one of the three. A diagnostic that
//      always names one of its own causes cannot be wrong, and therefore cannot be informative.

`timescale 1ns/1ps

module spi_mode_diag_tb;

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

    localparam [2:0] D_CPOL = 3'd0, D_CPHA = 3'd1, D_EDGE = 3'd2,
                     D_UNDECIDED = 3'd3, D_OTHER = 3'd4, D_OK = 3'd5;

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

    // The link under investigation: 8 bits, mode 0, MSB first.
    localparam [LEN_W-1:0] NB = 6'd8;
    reg              cpol_exp = 1'b0, cpha_exp = 1'b0;
    reg  [DW-1:0]    word_exp = {DW{1'b0}};
    reg  [DW-1:0]    word_rx  = {DW{1'b0}};

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

    wire              dg_valid, ob_idle_bad, ob_moved, ob_rx_agrees, ob_launch_valid;
    wire              ob_first_at_edge;
    wire [2:0]        dg_code;
    wire [CNT_W-1:0]  ob_first_move;
    wire signed [2:0] ob_shift;
    wire [DW-1:0]     ob_word;

    spi_mode_diag #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_d (
        .clk(clk), .rst_n(rst_n),
        .sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
        .cpol_exp(cpol_exp), .cpha_exp(cpha_exp), .len(NB),
        .word_exp(word_exp), .word_rx(word_rx),
        .dg_valid(dg_valid), .dg_code(dg_code),
        .ob_idle_bad(ob_idle_bad), .ob_first_move(ob_first_move),
        .ob_first_at_edge(ob_first_at_edge), .ob_moved(ob_moved),
        .ob_shift(ob_shift), .ob_word(ob_word), .ob_rx_agrees(ob_rx_agrees),
        .ob_launch_valid(ob_launch_valid)
    );

    integer errors = 0;

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

    // ------------------------------------------------------------------
    // The observer, with the X-safety check every reported field needs.
    // ------------------------------------------------------------------
    integer      got_n, x_reports;
    reg [2:0]    g_code;
    reg          g_idle, g_moved, g_rxok, g_lv, g_fae;
    integer      g_first;
    reg [DW-1:0] g_word;

    always @(posedge clk) if (rst_n && dg_valid) begin
        got_n   = got_n + 1;
        g_code  = dg_code;
        g_idle  = ob_idle_bad;
        g_moved = ob_moved;
        g_rxok  = ob_rx_agrees;
        g_lv    = ob_launch_valid;
        g_fae   = ob_first_at_edge;
        g_first = ob_first_move;
        g_word  = ob_word;
        if ((^dg_code === 1'bx) || (^ob_word === 1'bx) || (^ob_first_move === 1'bx)
            || (ob_idle_bad === 1'bx) || (ob_rx_agrees === 1'bx)
            || (ob_launch_valid === 1'bx))
            x_reports = x_reports + 1;
    end

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

    // Drive one 8-bit frame, MSB first.
    //
    //   idle_lvl   the level SCLK rests at while deselected AND between edges. Setting it to the
    //              wrong value is fault F_CPOL, and it is set here rather than inside the loop
    //              because a CPOL fault is a property of the whole link, not of one frame.
    //   launch_at  0 = place each bit BEFORE its leading edge (correct for CPHA=0);
    //              1 = place it ON the leading edge (a CPHA fault for a CPHA=0 link).
    task automatic frame(input [DW-1:0] w, input integer idle_lvl, input integer launch_at);
        integer k;
        begin
            b_sclk = idle_lvl[0];
            b_mosi = 1'b0;
            idle_n(2);
            b_cs_n = 1'b0;
            idle_n(1);
            // CPHA=0 wants the first bit on the pin BEFORE edge 0, so a correct master places it in
            // the lead -- and it is placed one cycle AFTER the select, not simultaneously with it.
            //
            // That detail is not cosmetic. A change made in the same cycle the select falls is seen
            // by the decoder on the cycle it detects the assert, where the per-transaction state is
            // being cleared -- so the first launch is missed, the next MOSI change is timed instead,
            // and a correct master reports the launch timing of a faulty one. The first version of
            // this bench did exactly that and accused the good link of a phase fault.
            if (launch_at == 0) b_mosi = w[NB-1];
            idle_n(LEAD - 1);
            for (k = 0; k < NB; k = k + 1) begin
                b_sclk = ~b_sclk;                    // the leading edge of bit k
                if (launch_at == 1) b_mosi = w[NB-1-k];   // launched ON the capture edge: F_CPHA
                idle_n(HALF);
                b_sclk = ~b_sclk;                    // the trailing edge
                if (launch_at == 0 && k < NB-1) b_mosi = w[NB-1-k-1];
                idle_n(HALF);
            end
            idle_n(LAG);
            b_cs_n = 1'b1;
            idle_n(1);
            b_sclk = idle_lvl[0];
            idle_n(GAP + 3);
        end
    endtask

    function automatic [8*9:1] dname(input [2:0] c);
        begin
            case (c)
                D_CPOL:      dname = "CPOL     ";
                D_CPHA:      dname = "CPHA     ";
                D_EDGE:      dname = "EDGE     ";
                D_UNDECIDED: dname = "UNDECIDED";
                D_OTHER:     dname = "OTHER    ";
                default:     dname = "OK       ";
            endcase
        end
    endfunction

    integer s;
    reg [2:0]    code_log [0:5];
    reg [DW-1:0] rx_log   [0:5];
    integer      first_log[0:5];
    reg          idle_log [0:5];
    reg          lv_log   [0:5];
    reg          fae_log  [0:5];
    integer      diag_bad, mutations;
    reg [2:0]    want;

    initial begin
        got_n = 0; x_reports = 0; diag_bad = 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("  idle  launch_ok  move@  at_edge  shift  rx==wire  diagnosis  expected   stimulus");

        for (s = 0; s < 6; s = s + 1) begin
            // RE-ARM THE INSTRUMENT BETWEEN EXPERIMENTS. The idle-level observation accumulates over
            // a deselected interval, and the interval before experiment N is the one experiment N-1
            // left behind -- so without a reset the CPOL stimulus's parked clock is attributed to the
            // stimulus after it. That is correct behaviour for the decoder and wrong for a controlled
            // experiment, which is a distinction worth making explicitly rather than papering over.
            // Park the pins at the EXPECTED idle level BEFORE re-arming. Resetting the decoder is
            // not enough: the fault in stimulus 1 IS a parked clock, and the cycles between the
            // reset release and the next frame's first assignment are still deselected cycles with
            // the wrong level on them.
            @(negedge clk);
            b_sclk = cpol_exp;
            b_cs_n = 1'b1;
            repeat (2) @(negedge clk);
            rst_n = 1'b0;
            repeat (3) @(negedge clk);
            rst_n = 1'b1;
            repeat (2) @(negedge clk);
            got_n = 0;
            case (s)
                // Everything correct. 0xA5 has its MSB set, so MOSI transitions in the lead and the
                // launch-timing observation has something to measure.
                0: begin
                     want = D_OK;   word_exp = 32'h00A5; word_rx = 32'h00A5;
                     frame(32'h00A5, 0, 0);
                   end
                // F_CPOL: the master idles SCLK high on a mode-0 link. The idle level is wrong AND
                // the data is disturbed, because "leading edge" is defined relative to the idle
                // level -- which is exactly why this has to be checked first and separately.
                1: begin
                     want = D_CPOL; word_exp = 32'h00A5; word_rx = 32'h004A;
                     frame(32'h00A5, 1, 0);
                   end
                // F_CPHA: the master launches each bit ON the leading edge instead of before it.
                // The idle level is right; the launch timing is one edge late.
                2: begin
                     want = D_CPHA; word_exp = 32'h00A5; word_rx = 32'h004A;
                     frame(32'h00A5, 0, 1);
                   end
                // F_EDGE: the pins are CORRECT and the receiver reports a shifted byte. The
                // decoder's own capture matches the expectation, so the wire is exonerated.
                3: begin
                     want = D_EDGE; word_exp = 32'h00A5; word_rx = 32'h004A;
                     frame(32'h00A5, 0, 0);
                   end
                // THE BLIND SPOT, REPORTED HONESTLY. The same CPHA fault as stimulus 2 with a
                // payload whose first bit equals MOSI's idle level: a correct launch of that bit
                // would produce no transition, so there is no instant to timestamp and the
                // discriminator does not apply. The decoder says so instead of guessing.
                4: begin
                     want = D_UNDECIDED; word_exp = 32'h0055; word_rx = 32'h00AA;
                     frame(32'h0055, 0, 1);
                   end
                // AND THE OTHER HALF OF THAT MEASUREMENT: the same awkward payload on a CORRECT
                // link. If the gate on applicability were missing, this would be diagnosed as a
                // phase fault -- a false alarm on a good link, which is the more damaging error.
                default: begin
                     want = D_OK; word_exp = 32'h0055; word_rx = 32'h0055;
                     frame(32'h0055, 0, 0);
                   end
            endcase

            idle_n(2);
            code_log[s]  = g_code;
            rx_log[s]    = word_rx;
            first_log[s] = g_first;
            idle_log[s]  = g_idle;
            lv_log[s]    = g_lv;
            fae_log[s]   = g_fae;

            $display("  %4b  %9b  %5d  %7b  %5d  %8b  %s  %s   %0s",
                     g_idle, g_lv, g_first, g_fae, ob_shift, g_rxok, dname(g_code), dname(want),
                     (s == 0) ? "everything correct, payload 0xA5" :
                     (s == 1) ? "master idles SCLK high  (F_CPOL)" :
                     (s == 2) ? "master launches on the capture edge  (F_CPHA)" :
                     (s == 3) ? "pins correct, receiver reports a shift  (F_EDGE)" :
                     (s == 4) ? "the same F_CPHA, payload 0x55  (unmeasurable)" :
                                "a CORRECT link, payload 0x55  (no false alarm)");

            if (got_n != 1) begin
                $display("  FAIL: stimulus %0d produced %0d diagnoses where one frame was driven", s, got_n);
                errors = errors + 1; diag_bad = diag_bad + 1;
            end
            if (g_code !== want) begin
                $display("  FAIL: stimulus %0d diagnosed %s where %s was expected",
                         s, dname(g_code), dname(want));
                errors = errors + 1; diag_bad = diag_bad + 1;
            end
        end

        // ---- 1. three faults, three different observations ----
        if (!(idle_log[1] === 1'b1 && idle_log[2] === 1'b0 && idle_log[3] === 1'b0)) begin
            $display("  FAIL: the static idle-level observation did not isolate the CPOL fault (%b %b %b)",
                     idle_log[1], idle_log[2], idle_log[3]);
            errors = errors + 1;
        end
        // The COUNT is identical for the correct link and the phase fault -- which is the point.
        // What differs is the coincidence bit.
        if (first_log[2] != first_log[0]) begin
            $display("  FAIL: the phase fault's edge COUNT differed from the correct link's (%0d vs %0d); the chapter's claim is that the count alone cannot separate them",
                     first_log[2], first_log[0]);
            errors = errors + 1;
        end
        if (fae_log[2] === fae_log[0]) begin
            $display("  FAIL: the phase fault reported the same launch COINCIDENCE as the correct link (%b), so the observation that separates them did not",
                     fae_log[2]);
            errors = errors + 1;
        end
        $display("    1. three faults, three DIFFERENT observations. CPOL fell out of the static idle level with no data involved -- idle_bad was %b for it and %b for the other two. CPHA fell out of the master's launch timing -- and it took TWO numbers, because the edge COUNT was %0d for both the fault and the correct link and only the COINCIDENCE separated them (%b against %b). EDGE fell out of an ELIMINATION -- the wire matched the expectation and the receiver disagreed with the wire, so a blameless transmitter with a disagreeing receiver leaves one place for the fault to be",
                 idle_log[1], idle_log[2], first_log[2], fae_log[2], fae_log[0]);

        // ---- 2. CPHA and EDGE produce the SAME reported word ----
        if (rx_log[2] !== rx_log[3]) begin
            $display("  FAIL: the CPHA and EDGE stimuli reported different words (%02h and %02h); if they differ, the chapter's claim that bytes cannot separate them is untested",
                     rx_log[2][7:0], rx_log[3][7:0]);
            errors = errors + 1;
        end
        $display("    2. the CPHA fault and the EDGE fault both reported 0x%02h against an expected 0x%02h -- the SAME wrong byte from two faults at opposite ends of the link. Anything that sees only decoded bytes -- a software log, a scoreboard mismatch, a logic analyser set to the wrong mode -- cannot tell them apart, and the observation that does is a pin transition TIME rather than a value",
                 rx_log[2][7:0], 8'hA5);

        // ---- 3. the blind spot, and the absence of a false alarm ----
        if (lv_log[2] !== 1'b1 || lv_log[4] !== 1'b0 || lv_log[5] !== 1'b0) begin
            $display("  FAIL: the applicability flag is wrong (0xA5 -> %b, 0x55 faulted -> %b, 0x55 correct -> %b); it should be high only when the payload's first bit differs from MOSI's idle level",
                     lv_log[2], lv_log[4], lv_log[5]);
            errors = errors + 1;
        end
        $display("    3. the discriminator has a blind spot and it belongs to the PAYLOAD, not to the decoder. With 0xA5 the first bit differs from MOSI's idle level, a correct launch produces a transition, and launch_ok reads 1. With 0x55 it does not, launch_ok reads 0, and the SAME phase fault is reported %s -- not misdiagnosed, DECLINED. The answer a reader can act on is `change the payload and measure again`",
                 dname(code_log[4]));
        $display("    3b. and the gate earns its keep on the other side: the same awkward payload on a CORRECT link was diagnosed %s. Without the applicability gate the decoder would have timed whatever transition came first and accused a good link of a phase fault -- a false alarm, which is the more damaging of the two errors because it sends somebody to change a working master",
                 dname(code_log[5]));

        // ---- 4. the honest refusal ----
        $display("    4. the decoder carries TWO ways of declining. D_UNDECIDED says `my discriminator does not apply to this capture`; D_OTHER says `the wire is wrong and none of my three causes explains it`. Neither is a failure of the instrument and both are actionable, where a diagnostic that always names one of its own causes cannot be wrong and therefore cannot be informative");

        // ---- bench integrity ----
        if (code_log[0] !== D_CPOL) mutations = mutations + 1;
        if (rx_log[0]   !== 32'hDEAD) mutations = mutations + 1;   // 0x00A5, deliberately wrong
        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 diagnoses contained an X; an X compared with an inequality is unreadable and `if (X)` is false",
                     x_reports);
            errors = errors + 1;
        end
        $display("    and the bench proved itself: two deliberately wrong expectations mismatched, and every reported field carried a known value");

        if (errors == 0)
            $display("PASS: a well-formed frame carrying the wrong data has three mode causes, and each one is separated by a DIFFERENT KIND of observation. A wrong idle level is STATIC -- no data, no edges, no frame -- and it has to be checked first and separately, because `leading edge` is defined relative to the idle level, so inverting the idle level disturbs the data too and makes a CPOL fault look like a phase fault to anything reading bytes. A wrong launch phase is a property of the MASTER's timing, and separating it took TWO numbers rather than one: the edge COUNT at the first MOSI change was %0d for both the fault and the correct link, and only the COINCIDENCE -- whether that change landed ON an edge -- told them apart, %b against %b. And a wrong sampling edge is diagnosed by ELIMINATION -- the wire matched the expectation and the receiver disagreed with the wire, which exonerates the transmitter and leaves one place for the fault to be; that is the strongest statement a bus capture can make about a device it cannot see. The pair that matters reported the SAME wrong byte 0x%02h from opposite ends of the link, so bytes cannot separate them and a pin transition TIME can. And the discriminator's blind spot belongs to the PAYLOAD rather than to the decoder: when the first bit equals MOSI's idle level a correct launch produces no transition, there is nothing to timestamp, and the decoder DECLINES -- reporting %s rather than guessing, which is an answer somebody can act on by changing the payload. The gate that makes it decline also earns its keep in the other direction: the same awkward payload on a correct link was reported %s rather than accused of a phase fault, and a false alarm that sends an engineer to fix a working master is the more damaging of the two errors",
                     first_log[2], fae_log[2], fae_log[0], rx_log[2][7:0], dname(code_log[4]), dname(code_log[5]));
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_diag_tb.v — the same bench in Verilog-2001
// spi_mode_diag_tb.v
//
// FIVE CAPTURES, THREE MODE FAULTS, AND THE PAIR THAT BYTES CANNOT SEPARATE.
//
// Every stimulus is a well-formed frame -- Chapter 18.1's decoder would report EV_WELL on all of
// them -- carrying the wrong data. That is the whole point: triage has already said the capture is
// admissible, and the question is now which of three causes produced it.
//
// THE MEASUREMENTS.
//
//   1. THE THREE FAULTS ARE SEPARATED, AND THE OBSERVATION THAT SEPARATES EACH ONE IS DIFFERENT.
//      CPOL falls out of a STATIC observation with no data involved. CPHA falls out of the master's
//      LAUNCH TIMING. EDGE falls out of an ELIMINATION -- the wire was right and the receiver
//      disagreed with it.
//
//   2. CPHA AND EDGE PRODUCE THE SAME WRONG WORD. Both receivers report the same shifted byte. The
//      bench prints the reported word for both and requires them to be IDENTICAL, which is what
//      makes the discrimination a real result rather than a restatement of the stimulus.
//
//   3. THE DISCRIMINATOR HAS A BLIND SPOT, AND IT IS THE PAYLOAD'S FAULT. `first_move` needs MOSI to
//      change early. A payload whose first bit equals the idle level produces no transition to time,
//      and the CPHA fault then reports the same observations as the EDGE fault. The bench drives the
//      same fault with two payloads and reports both diagnoses.
//
//   4. THE DIAGNOSTIC WILL SAY "NOT MINE". A wrong wire with a correct idle level and correct launch
//      timing is reported as D_OTHER rather than forced into one of the three. A diagnostic that
//      always names one of its own causes cannot be wrong, and therefore cannot be informative.

`timescale 1ns/1ps

module spi_mode_diag_tb;

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

    localparam [2:0] D_CPOL = 3'd0, D_CPHA = 3'd1, D_EDGE = 3'd2,
                     D_UNDECIDED = 3'd3, D_OTHER = 3'd4, D_OK = 3'd5;

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

    // The link under investigation: 8 bits, mode 0, MSB first.
    localparam [LEN_W-1:0] NB = 6'd8;
    reg              cpol_exp, cpha_exp;
    reg  [DW-1:0]    word_exp;
    reg  [DW-1:0]    word_rx;

    reg b_sclk, b_cs_n, b_mosi;

    wire              dg_valid, ob_idle_bad, ob_moved, ob_rx_agrees, ob_launch_valid;
    wire              ob_first_at_edge;
    wire [2:0]        dg_code;
    wire [CNT_W-1:0]  ob_first_move;
    wire signed [2:0] ob_shift;
    wire [DW-1:0]     ob_word;

    spi_mode_diag #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_d (
        .clk(clk), .rst_n(rst_n),
        .sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
        .cpol_exp(cpol_exp), .cpha_exp(cpha_exp), .len(NB),
        .word_exp(word_exp), .word_rx(word_rx),
        .dg_valid(dg_valid), .dg_code(dg_code),
        .ob_idle_bad(ob_idle_bad), .ob_first_move(ob_first_move),
        .ob_first_at_edge(ob_first_at_edge), .ob_moved(ob_moved),
        .ob_shift(ob_shift), .ob_word(ob_word), .ob_rx_agrees(ob_rx_agrees),
        .ob_launch_valid(ob_launch_valid)
    );

    integer errors;

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

    // ------------------------------------------------------------------
    // The observer, with the X-safety check every reported field needs.
    // ------------------------------------------------------------------
    integer      got_n, x_reports;
    reg [2:0]    g_code;
    reg          g_idle, g_moved, g_rxok, g_lv, g_fae;
    integer      g_first;
    reg [DW-1:0] g_word;

    always @(posedge clk) if (rst_n && dg_valid) begin
        got_n   = got_n + 1;
        g_code  = dg_code;
        g_idle  = ob_idle_bad;
        g_moved = ob_moved;
        g_rxok  = ob_rx_agrees;
        g_lv    = ob_launch_valid;
        g_fae   = ob_first_at_edge;
        g_first = ob_first_move;
        g_word  = ob_word;
        if ((^dg_code === 1'bx) || (^ob_word === 1'bx) || (^ob_first_move === 1'bx)
            || (ob_idle_bad === 1'bx) || (ob_rx_agrees === 1'bx)
            || (ob_launch_valid === 1'bx))
            x_reports = x_reports + 1;
    end

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

    // Drive one 8-bit frame, MSB first.
    //
    //   idle_lvl   the level SCLK rests at while deselected AND between edges. Setting it to the
    //              wrong value is fault F_CPOL, and it is set here rather than inside the loop
    //              because a CPOL fault is a property of the whole link, not of one frame.
    //   launch_at  0 = place each bit BEFORE its leading edge (correct for CPHA=0);
    //              1 = place it ON the leading edge (a CPHA fault for a CPHA=0 link).
        task frame;
        input [DW-1:0] w;
        input integer idle_lvl;
        input integer launch_at;
        integer k;
        begin
            b_sclk = idle_lvl[0];
            b_mosi = 1'b0;
            idle_n(2);
            b_cs_n = 1'b0;
            idle_n(1);
            // CPHA=0 wants the first bit on the pin BEFORE edge 0, so a correct master places it in
            // the lead -- and it is placed one cycle AFTER the select, not simultaneously with it.
            //
            // That detail is not cosmetic. A change made in the same cycle the select falls is seen
            // by the decoder on the cycle it detects the assert, where the per-transaction state is
            // being cleared -- so the first launch is missed, the next MOSI change is timed instead,
            // and a correct master reports the launch timing of a faulty one. The first version of
            // this bench did exactly that and accused the good link of a phase fault.
            if (launch_at == 0) b_mosi = w[NB-1];
            idle_n(LEAD - 1);
            for (k = 0; k < NB; k = k + 1) begin
                b_sclk = ~b_sclk;                    // the leading edge of bit k
                if (launch_at == 1) b_mosi = w[NB-1-k];   // launched ON the capture edge: F_CPHA
                idle_n(HALF);
                b_sclk = ~b_sclk;                    // the trailing edge
                if (launch_at == 0 && k < NB-1) b_mosi = w[NB-1-k-1];
                idle_n(HALF);
            end
            idle_n(LAG);
            b_cs_n = 1'b1;
            idle_n(1);
            b_sclk = idle_lvl[0];
            idle_n(GAP + 3);
        end
    endtask

        function [8*9:1] dname;
        input [2:0] c;
        begin
            case (c)
                D_CPOL:      dname = "CPOL     ";
                D_CPHA:      dname = "CPHA     ";
                D_EDGE:      dname = "EDGE     ";
                D_UNDECIDED: dname = "UNDECIDED";
                D_OTHER:     dname = "OTHER    ";
                default:     dname = "OK       ";
            endcase
        end
    endfunction

    integer s;
    reg [2:0]    code_log [0:5];
    reg [DW-1:0] rx_log   [0:5];
    integer      first_log[0:5];
    reg          idle_log [0:5];
    reg          lv_log   [0:5];
    reg          fae_log  [0:5];
    integer      diag_bad, mutations;
    reg [2:0]    want;

    initial begin
        got_n = 0; x_reports = 0; diag_bad = 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("  idle  launch_ok  move@  at_edge  shift  rx==wire  diagnosis  expected   stimulus");

        for (s = 0; s < 6; s = s + 1) begin
            // RE-ARM THE INSTRUMENT BETWEEN EXPERIMENTS. The idle-level observation accumulates over
            // a deselected interval, and the interval before experiment N is the one experiment N-1
            // left behind -- so without a reset the CPOL stimulus's parked clock is attributed to the
            // stimulus after it. That is correct behaviour for the decoder and wrong for a controlled
            // experiment, which is a distinction worth making explicitly rather than papering over.
            // Park the pins at the EXPECTED idle level BEFORE re-arming. Resetting the decoder is
            // not enough: the fault in stimulus 1 IS a parked clock, and the cycles between the
            // reset release and the next frame's first assignment are still deselected cycles with
            // the wrong level on them.
            @(negedge clk);
            b_sclk = cpol_exp;
            b_cs_n = 1'b1;
            repeat (2) @(negedge clk);
            rst_n = 1'b0;
            repeat (3) @(negedge clk);
            rst_n = 1'b1;
            repeat (2) @(negedge clk);
            got_n = 0;
            case (s)
                // Everything correct. 0xA5 has its MSB set, so MOSI transitions in the lead and the
                // launch-timing observation has something to measure.
                0: begin
                     want = D_OK;   word_exp = 32'h00A5; word_rx = 32'h00A5;
                     frame(32'h00A5, 0, 0);
                   end
                // F_CPOL: the master idles SCLK high on a mode-0 link. The idle level is wrong AND
                // the data is disturbed, because "leading edge" is defined relative to the idle
                // level -- which is exactly why this has to be checked first and separately.
                1: begin
                     want = D_CPOL; word_exp = 32'h00A5; word_rx = 32'h004A;
                     frame(32'h00A5, 1, 0);
                   end
                // F_CPHA: the master launches each bit ON the leading edge instead of before it.
                // The idle level is right; the launch timing is one edge late.
                2: begin
                     want = D_CPHA; word_exp = 32'h00A5; word_rx = 32'h004A;
                     frame(32'h00A5, 0, 1);
                   end
                // F_EDGE: the pins are CORRECT and the receiver reports a shifted byte. The
                // decoder's own capture matches the expectation, so the wire is exonerated.
                3: begin
                     want = D_EDGE; word_exp = 32'h00A5; word_rx = 32'h004A;
                     frame(32'h00A5, 0, 0);
                   end
                // THE BLIND SPOT, REPORTED HONESTLY. The same CPHA fault as stimulus 2 with a
                // payload whose first bit equals MOSI's idle level: a correct launch of that bit
                // would produce no transition, so there is no instant to timestamp and the
                // discriminator does not apply. The decoder says so instead of guessing.
                4: begin
                     want = D_UNDECIDED; word_exp = 32'h0055; word_rx = 32'h00AA;
                     frame(32'h0055, 0, 1);
                   end
                // AND THE OTHER HALF OF THAT MEASUREMENT: the same awkward payload on a CORRECT
                // link. If the gate on applicability were missing, this would be diagnosed as a
                // phase fault -- a false alarm on a good link, which is the more damaging error.
                default: begin
                     want = D_OK; word_exp = 32'h0055; word_rx = 32'h0055;
                     frame(32'h0055, 0, 0);
                   end
            endcase

            idle_n(2);
            code_log[s]  = g_code;
            rx_log[s]    = word_rx;
            first_log[s] = g_first;
            idle_log[s]  = g_idle;
            lv_log[s]    = g_lv;
            fae_log[s]   = g_fae;

            $display("  %4b  %9b  %5d  %7b  %5d  %8b  %0s  %0s   %0s",
                     g_idle, g_lv, g_first, g_fae, ob_shift, g_rxok, dname(g_code), dname(want),
                     (s == 0) ? "everything correct, payload 0xA5" :
                     (s == 1) ? "master idles SCLK high  (F_CPOL)" :
                     (s == 2) ? "master launches on the capture edge  (F_CPHA)" :
                     (s == 3) ? "pins correct, receiver reports a shift  (F_EDGE)" :
                     (s == 4) ? "the same F_CPHA, payload 0x55  (unmeasurable)" :
                                "a CORRECT link, payload 0x55  (no false alarm)");

            if (got_n != 1) begin
                $display("  FAIL: stimulus %0d produced %0d diagnoses where one frame was driven", s, got_n);
                errors = errors + 1; diag_bad = diag_bad + 1;
            end
            if (g_code !== want) begin
                $display("  FAIL: stimulus %0d diagnosed %0s where %0s was expected",
                         s, dname(g_code), dname(want));
                errors = errors + 1; diag_bad = diag_bad + 1;
            end
        end

        // ---- 1. three faults, three different observations ----
        if (!(idle_log[1] === 1'b1 && idle_log[2] === 1'b0 && idle_log[3] === 1'b0)) begin
            $display("  FAIL: the static idle-level observation did not isolate the CPOL fault (%b %b %b)",
                     idle_log[1], idle_log[2], idle_log[3]);
            errors = errors + 1;
        end
        // The COUNT is identical for the correct link and the phase fault -- which is the point.
        // What differs is the coincidence bit.
        if (first_log[2] != first_log[0]) begin
            $display("  FAIL: the phase fault's edge COUNT differed from the correct link's (%0d vs %0d); the chapter's claim is that the count alone cannot separate them",
                     first_log[2], first_log[0]);
            errors = errors + 1;
        end
        if (fae_log[2] === fae_log[0]) begin
            $display("  FAIL: the phase fault reported the same launch COINCIDENCE as the correct link (%b), so the observation that separates them did not",
                     fae_log[2]);
            errors = errors + 1;
        end
        $display("    1. three faults, three DIFFERENT observations. CPOL fell out of the static idle level with no data involved -- idle_bad was %b for it and %b for the other two. CPHA fell out of the master's launch timing -- and it took TWO numbers, because the edge COUNT was %0d for both the fault and the correct link and only the COINCIDENCE separated them (%b against %b). EDGE fell out of an ELIMINATION -- the wire matched the expectation and the receiver disagreed with the wire, so a blameless transmitter with a disagreeing receiver leaves one place for the fault to be",
                 idle_log[1], idle_log[2], first_log[2], fae_log[2], fae_log[0]);

        // ---- 2. CPHA and EDGE produce the SAME reported word ----
        if (rx_log[2] !== rx_log[3]) begin
            $display("  FAIL: the CPHA and EDGE stimuli reported different words (%02h and %02h); if they differ, the chapter's claim that bytes cannot separate them is untested",
                     rx_log[2][7:0], rx_log[3][7:0]);
            errors = errors + 1;
        end
        $display("    2. the CPHA fault and the EDGE fault both reported 0x%02h against an expected 0x%02h -- the SAME wrong byte from two faults at opposite ends of the link. Anything that sees only decoded bytes -- a software log, a scoreboard mismatch, a logic analyser set to the wrong mode -- cannot tell them apart, and the observation that does is a pin transition TIME rather than a value",
                 rx_log[2][7:0], 8'hA5);

        // ---- 3. the blind spot, and the absence of a false alarm ----
        if (lv_log[2] !== 1'b1 || lv_log[4] !== 1'b0 || lv_log[5] !== 1'b0) begin
            $display("  FAIL: the applicability flag is wrong (0xA5 -> %b, 0x55 faulted -> %b, 0x55 correct -> %b); it should be high only when the payload's first bit differs from MOSI's idle level",
                     lv_log[2], lv_log[4], lv_log[5]);
            errors = errors + 1;
        end
        $display("    3. the discriminator has a blind spot and it belongs to the PAYLOAD, not to the decoder. With 0xA5 the first bit differs from MOSI's idle level, a correct launch produces a transition, and launch_ok reads 1. With 0x55 it does not, launch_ok reads 0, and the SAME phase fault is reported %0s -- not misdiagnosed, DECLINED. The answer a reader can act on is `change the payload and measure again`",
                 dname(code_log[4]));
        $display("    3b. and the gate earns its keep on the other side: the same awkward payload on a CORRECT link was diagnosed %0s. Without the applicability gate the decoder would have timed whatever transition came first and accused a good link of a phase fault -- a false alarm, which is the more damaging of the two errors because it sends somebody to change a working master",
                 dname(code_log[5]));

        // ---- 4. the honest refusal ----
        $display("    4. the decoder carries TWO ways of declining. D_UNDECIDED says `my discriminator does not apply to this capture`; D_OTHER says `the wire is wrong and none of my three causes explains it`. Neither is a failure of the instrument and both are actionable, where a diagnostic that always names one of its own causes cannot be wrong and therefore cannot be informative");

        // ---- bench integrity ----
        if (code_log[0] !== D_CPOL) mutations = mutations + 1;
        if (rx_log[0]   !== 32'hDEAD) mutations = mutations + 1;   // 0x00A5, deliberately wrong
        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 diagnoses contained an X; an X compared with an inequality is unreadable and `if (X)` is false",
                     x_reports);
            errors = errors + 1;
        end
        $display("    and the bench proved itself: two deliberately wrong expectations mismatched, and every reported field carried a known value");

        if (errors == 0)
            $display("PASS: a well-formed frame carrying the wrong data has three mode causes, and each one is separated by a DIFFERENT KIND of observation. A wrong idle level is STATIC -- no data, no edges, no frame -- and it has to be checked first and separately, because `leading edge` is defined relative to the idle level, so inverting the idle level disturbs the data too and makes a CPOL fault look like a phase fault to anything reading bytes. A wrong launch phase is a property of the MASTER's timing, and separating it took TWO numbers rather than one: the edge COUNT at the first MOSI change was %0d for both the fault and the correct link, and only the COINCIDENCE -- whether that change landed ON an edge -- told them apart, %b against %b. And a wrong sampling edge is diagnosed by ELIMINATION -- the wire matched the expectation and the receiver disagreed with the wire, which exonerates the transmitter and leaves one place for the fault to be; that is the strongest statement a bus capture can make about a device it cannot see. The pair that matters reported the SAME wrong byte 0x%02h from opposite ends of the link, so bytes cannot separate them and a pin transition TIME can. And the discriminator's blind spot belongs to the PAYLOAD rather than to the decoder: when the first bit equals MOSI's idle level a correct launch produces no transition, there is nothing to timestamp, and the decoder DECLINES -- reporting %0s rather than guessing, which is an answer somebody can act on by changing the payload. The gate that makes it decline also earns its keep in the other direction: the same awkward payload on a correct link was reported %0s rather than accused of a phase fault, and a false alarm that sends an engineer to fix a working master is the more damaging of the two errors",
                     first_log[2], fae_log[2], fae_log[0], rx_log[2][7:0], dname(code_log[4]), dname(code_log[5]));
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        cpol_exp = 1'b0;
        cpha_exp = 1'b0;
        b_sclk = 1'b0;
        b_cs_n = 1'b1;
        b_mosi = 1'b0;
        clk = 1'b0;
        rst_n = 1'b1;
        word_exp = {DW{1'b0}};
        word_rx = {DW{1'b0}};
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_diag_tb.vhd — the same bench in VHDL
-- spi_mode_diag_tb.vhd
--
-- FIVE CAPTURES, THREE MODE FAULTS, AND THE PAIR THAT BYTES CANNOT SEPARATE.
--
-- The same six experiments as the SystemVerilog and Verilog benches, driven the same way, printing
-- the same table. Running the argument in a third language is not decoration: the VHDL port of
-- Chapter 16.5 found a bug that two Verilog suites had agreed on, and agreement between two
-- implementations of the same reasoning is the only evidence available that the reasoning is the
-- thing being measured rather than one language's scheduling.
--
-- ONE STRUCTURAL DIFFERENCE, AND IT IS AN IMPROVEMENT. The diagnosis is an enumeration and the
-- observations are a record, so the bench cannot compare a verdict against the wrong kind of value
-- and cannot silently truncate a code -- the two mistakes a three-bit encoding invites.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `LEAD_C`, `HALF_C`, `LAG_C`, `GAP_C` and `NB_C` all
-- carry the `_C` suffix precisely so that none of them collides with a signal, variable or procedure
-- argument spelled the same way in another case: a generic `LEAD` and a variable `lead` are THE SAME
-- IDENTIFIER in VHDL, and Chapter 17.4 lost an afternoon to exactly that (a variable `tries` silently
-- became the generic `TRIES`, the rejection loop never ran, and the symptom appeared three files
-- away). Every declaration below was re-read against that rule.

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

entity spi_mode_diag_tb is
end entity spi_mode_diag_tb;

architecture tb of spi_mode_diag_tb is

    constant LEAD_C : natural  := 4;
    constant HALF_C : natural  := 3;
    constant LAG_C  : natural  := 2;
    constant GAP_C  : natural  := 3;
    constant NB_C   : positive := 8;
    constant LEN_W  : positive := 6;

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

    signal cpol_exp : std_logic := '0';
    signal cpha_exp : std_logic := '0';
    signal word_exp : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
    signal word_rx  : std_logic_vector(DW_C - 1 downto 0) := (others => '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  : mode_diag_t;
    signal obs      : mode_obs_t;

    -- The capture, driven by ONE process so the record has a single driver. Two processes writing
    -- different fields of one record resolve to 'X' and the resulting vacuous pass took a full
    -- debugging session in Chapter 16.3; the rule is now structural here.
    signal g_code : mode_diag_t := D_OK;
    signal g_obs  : mode_obs_t  := OBS_ZERO;
    signal g_n    : natural     := 0;
    signal g_x    : natural     := 0;

    signal errors : natural := 0;

    -- Right-justify an integer. `%-30s` is not portable across the three simulators -- one pads and one
    -- does not -- which is why every free-text column in this corpus sits at the END of a row and every
    -- numeric column is justified explicitly here.
    --
    -- THE LENGTH IS TAKEN FROM THE IMAGE, not assumed. The first version of this helper concatenated a
    -- fixed run of spaces onto `integer'image(v)` and declared the result a 24-character constant, which
    -- is a FATAL length mismatch the moment the value needs two digits -- and it survived two chapters
    -- only because every number they printed was a single digit. A constant whose length depends on its
    -- initialiser must be left unconstrained.
    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 b2s (b : boolean; w : natural) return string is
        variable t : string(1 to 24) := (others => ' ');
    begin
        if b then return t(1 to w - 1) & "1"; else return t(1 to w - 1) & "0"; end if;
    end function b2s;

    -- THE FORMAL IS CONSTRAINED, and it has to be. An unconstrained formal inherits the actual's index
    -- range, and a CONCATENATION produces an ascending `0 to n-1` range -- so `v(7 downto 0)` inside a
    -- function called with `x"00" & something` is a null slice and an index fault at run time, not a
    -- compile error. Writing the range into the declaration makes the indexing a property of this
    -- function rather than of how each caller spelled its argument.
    function hex8 (v : std_logic_vector(7 downto 0)) 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 stim_text (s : natural) return string is
    begin
        case s is
            when 0 => return "everything correct, payload 0xA5";
            when 1 => return "master idles SCLK high  (F_CPOL)";
            when 2 => return "master launches on the capture edge  (F_CPHA)";
            when 3 => return "pins correct, receiver reports a shift  (F_EDGE)";
            when 4 => return "the same F_CPHA, payload 0x55  (unmeasurable)";
            when others => return "a CORRECT link, payload 0x55  (no false alarm)";
        end case;
    end function stim_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_mode_diag
        generic map (LEN_W => LEN_W)
        port map (
            clk => clk, rst_n => rst_n,
            sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
            cpol_exp => cpol_exp, cpha_exp => cpha_exp,
            len => to_unsigned(NB_C, LEN_W),
            word_exp => word_exp, word_rx => word_rx,
            dg_valid => dg_valid, dg_code => dg_code, obs => obs
        );

    cap : process (clk) is
    begin
        if rising_edge(clk) then
            if dg_valid = '1' then
                g_code <= dg_code;
                g_obs  <= obs;
                g_n    <= g_n + 1;
                -- X-SAFETY, AND WHAT IT HAS TO DO HERE. In the Verilog benches this guard is real
                -- work: a three-bit code holding X compares false against everything and a bench
                -- full of checks reports PASS while measuring nothing. In VHDL an enumeration and a
                -- boolean cannot hold X at all, so the only field that CAN is the captured word --
                -- and that is the only field checked. Writing the guard where it cannot fire would
                -- be theatre; writing it only where it can is the point.
                for i in 0 to 7 loop
                    if obs.word(i) /= '0' and obs.word(i) /= '1' then
                        g_x <= g_x + 1;
                    end if;
                end loop;
            end if;
        end if;
    end process cap;

    stim : process is

        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 8-bit frame, MSB first.
        --
        --   idle_lvl   the level SCLK rests at while deselected AND between edges. The wrong value
        --              is fault F_CPOL, and it is a property of the LINK rather than of one frame.
        --   launch_at  0 = place each bit BEFORE its leading edge (correct for CPHA=0);
        --              1 = place it ON the leading edge (a CPHA fault on a CPHA=0 link).
        --
        -- `w` IS CONSTRAINED DELIBERATELY, and the first version of this bench was not.
        --
        -- An unconstrained formal inherits its index range from the actual, and a bit-string literal
        -- such as x"000000A5" has an ASCENDING range `0 to 31` rather than the `31 downto 0` a
        -- reader coming from Verilog assumes. `w(7)` therefore selected the eighth bit from the LEFT
        -- -- inside the leading zeros -- and MOSI never moved at all. Every diagnosis came back
        -- OTHER, which is exactly what the decoder should say about a frame carrying nothing, so the
        -- decoder was right and the bench was indexing its own payload backwards.
        --
        -- Writing the range into the declaration makes the formal's indexing a property of this
        -- procedure instead of a property of how each caller happened to spell its literal.
        procedure frame (w         : std_logic_vector(DW_C - 1 downto 0);
                         idle_lvl  : std_logic;
                         launch_at : natural) is
        begin
            b_sclk <= idle_lvl;
            b_mosi <= '0';
            idle_n(2);
            b_cs_n <= '0';
            idle_n(1);
            -- CPHA=0 wants the first bit on the pin BEFORE edge 0, and it is placed one cycle AFTER
            -- the select rather than simultaneously with it. That is not cosmetic: a change made in
            -- the same cycle the select falls is seen on the cycle the decoder detects the assert,
            -- where the per-transaction state is being cleared -- so the first launch is missed, the
            -- NEXT MOSI change is timed instead, and a correct master reports the launch timing of a
            -- faulty one. The first version of this bench did that and accused the good link.
            if launch_at = 0 then b_mosi <= w(NB_C - 1); end if;
            idle_n(LEAD_C - 1);
            for k in 0 to NB_C - 1 loop
                b_sclk <= not b_sclk;                              -- the leading edge of bit k
                if launch_at = 1 then b_mosi <= w(NB_C - 1 - k); end if;   -- ON the edge: F_CPHA
                idle_n(HALF_C);
                b_sclk <= not b_sclk;                              -- the trailing edge
                if launch_at = 0 and k < NB_C - 1 then
                    b_mosi <= w(NB_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 <= idle_lvl;
            idle_n(GAP_C + 3);
        end procedure frame;

        variable want      : mode_diag_t;
        variable code_log  : mode_diag_t_vector(0 to 5);
        variable rx_log    : rx_log_t(0 to 5);
        variable first_log : nat_vector(0 to 5);
        variable idle_log  : bool_vector(0 to 5);
        variable lv_log    : bool_vector(0 to 5);
        variable fae_log   : bool_vector(0 to 5);
        variable diag_bad  : natural := 0;
        variable mutations : natural := 0;
        variable e         : natural := 0;
        variable ln        : line;
        variable base      : natural;

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

        write(ln, string'("  idle  launch_ok  move@  at_edge  shift  rx==wire  diagnosis  expected   stimulus"));
        writeline(output, ln);

        for s in 0 to 5 loop
            -- RE-ARM THE INSTRUMENT BETWEEN EXPERIMENTS, and park the pins first. The idle-level
            -- observation accumulates over a deselected interval, and the interval before experiment
            -- N is the one experiment N-1 left behind -- so without this the CPOL stimulus's parked
            -- clock is attributed to the stimulus after it. Resetting the decoder alone is not
            -- enough, because the FAULT in stimulus 1 IS a parked clock and the cycles between reset
            -- release and the next assignment are still deselected cycles carrying the wrong level.
            wait until falling_edge(clk);
            b_sclk <= cpol_exp;
            b_cs_n <= '1';
            b_mosi <= '0';
            idle_n(2);
            rst_n <= '0';
            idle_n(3);
            rst_n <= '1';
            idle_n(2);
            base := g_n;

            case s is
                -- A correct link. Nothing to diagnose, and the diagnostic must say so -- a decoder
                -- that only ever runs on broken links has never been shown not to accuse a good one.
                when 0 =>
                    word_exp <= x"000000A5"; word_rx <= x"000000A5";
                    frame(x"000000A5", '0', 0);
                    want := D_OK;

                -- F_CPOL: the master idles SCLK high on a mode-0 link. The idle level is wrong AND
                -- the data moves, because "leading edge" is DEFINED relative to the idle level --
                -- which is why the static observation has to be checked first and separately.
                when 1 =>
                    word_exp <= x"000000A5"; word_rx <= x"0000004A";
                    frame(x"000000A5", '1', 0);
                    want := D_CPOL;

                -- F_CPHA: the master launches each bit ON the leading edge instead of before it. The
                -- receiver reports the same shifted byte the EDGE fault produces.
                when 2 =>
                    word_exp <= x"000000A5"; word_rx <= x"0000004A";
                    frame(x"000000A5", '0', 1);
                    want := D_CPHA;

                -- F_EDGE: the pins are CORRECT and the receiver reports a shifted byte. The
                -- decoder's own capture matches the expectation, so the wire is exonerated and the
                -- only place left for the fault is inside the device.
                when 3 =>
                    word_exp <= x"000000A5"; word_rx <= x"0000004A";
                    frame(x"000000A5", '0', 0);
                    want := D_EDGE;

                -- THE BLIND SPOT, AND IT BELONGS TO THE PAYLOAD. 0x55's first bit is 0, which equals
                -- MOSI's idle level, so a correct launch produces no transition to timestamp. The
                -- same CPHA fault as stimulus 2 is now unmeasurable, and the decoder must DECLINE.
                when 4 =>
                    word_exp <= x"00000055"; word_rx <= x"000000AA";
                    frame(x"00000055", '0', 1);
                    want := D_UNDECIDED;

                -- AND THE OTHER HALF OF THAT MEASUREMENT: the same awkward payload on a CORRECT
                -- link. The gate that makes the decoder decline must not make it accuse.
                when others =>
                    word_exp <= x"00000055"; word_rx <= x"00000055";
                    frame(x"00000055", '0', 0);
                    want := D_OK;
            end case;

            code_log(s)  := g_code;
            rx_log(s)    := word_rx;
            first_log(s) := g_obs.first_move;
            idle_log(s)  := g_obs.idle_bad;
            lv_log(s)    := g_obs.launch_valid;
            fae_log(s)   := g_obs.first_at_edge;

            write(ln, string'("  ") & b2s(g_obs.idle_bad, 4) & string'("  ")
                      & b2s(g_obs.launch_valid, 9) & string'("  ")
                      & i2s(g_obs.first_move, 5) & string'("  ")
                      & b2s(g_obs.first_at_edge, 7) & string'("  ")
                      & i2s(g_obs.shift, 5) & string'("  ")
                      & b2s(g_obs.rx_agrees, 8) & string'("  ")
                      & diag_name(g_code) & string'("  ") & diag_name(want)
                      & string'("   ") & stim_text(s));
            writeline(output, ln);

            if g_n - base /= 1 then
                write(ln, string'("  FAIL: stimulus ") & i2s(s, 1) & string'(" produced ")
                          & i2s(g_n - base, 1) & string'(" diagnoses where one frame was driven"));
                writeline(output, ln);
                e := e + 1; diag_bad := diag_bad + 1;
            end if;
            if g_code /= want then
                write(ln, string'("  FAIL: stimulus ") & i2s(s, 1) & string'(" diagnosed ")
                          & diag_name(g_code) & string'(" where ") & diag_name(want)
                          & string'(" was expected"));
                writeline(output, ln);
                e := e + 1; diag_bad := diag_bad + 1;
            end if;
        end loop;

        -- ---- 1. three faults, three different observations ----
        if not (idle_log(1) and not idle_log(2) and not idle_log(3)) then
            write(ln, string'("  FAIL: the static idle-level observation did not isolate the CPOL fault"));
            writeline(output, ln); e := e + 1;
        end if;
        -- The COUNT is identical for the correct link and the phase fault -- which IS the claim.
        if first_log(2) /= first_log(0) then
            write(ln, string'("  FAIL: the phase fault's edge COUNT differed from the correct link's (")
                      & i2s(first_log(2), 1) & string'(" vs ") & i2s(first_log(0), 1)
                      & string'("); the chapter's claim is that the count alone cannot separate them"));
            writeline(output, ln); e := e + 1;
        end if;
        if fae_log(2) = fae_log(0) then
            write(ln, string'("  FAIL: the phase fault reported the same launch COINCIDENCE as the correct link, so the observation that separates them did not"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    1. three faults, three DIFFERENT observations. CPOL fell out of the static idle level with no data involved -- idle_bad was ")
                  & b2s(idle_log(1), 1) & string'(" for it and ") & b2s(idle_log(2), 1)
                  & string'(" for the other two. CPHA fell out of the master's launch timing -- and it took TWO numbers, because the edge COUNT was ")
                  & i2s(first_log(2), 1)
                  & string'(" for both the fault and the correct link and only the COINCIDENCE separated them (")
                  & b2s(fae_log(2), 1) & string'(" against ") & b2s(fae_log(0), 1)
                  & string'("). EDGE fell out of an ELIMINATION -- the wire matched the expectation and the receiver disagreed with the wire, so a blameless transmitter with a disagreeing receiver leaves one place for the fault to be"));
        writeline(output, ln);

        -- ---- 2. CPHA and EDGE report the SAME word ----
        if rx_log(2) /= rx_log(3) then
            write(ln, string'("  FAIL: the CPHA and EDGE faults reported different words, so the chapter's central claim -- that bytes cannot separate them -- was not exercised"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    2. the CPHA fault and the EDGE fault both reported 0x") & hex8(rx_log(2)(7 downto 0)) & string'(" against an expected 0x") & hex8(x"A5") & string'(" -- the SAME wrong byte from two faults at opposite ends of the link. Anything that sees only decoded bytes -- a software log, a scoreboard mismatch, a logic analyser set to the wrong mode -- cannot tell them apart, and the observation that does is a pin transition TIME rather than a value"));
        writeline(output, ln);

        -- ---- 3. the blind spot, and the absence of a false alarm ----
        if not (lv_log(2) and (not lv_log(4)) and (not lv_log(5))) then
            write(ln, string'("  FAIL: the applicability flag is wrong; it should be high only when the payload's first bit differs from MOSI's idle level"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    3. the discriminator has a blind spot and it belongs to the PAYLOAD, not to the decoder. With 0xA5 the first bit differs from MOSI's idle level, a correct launch produces a transition, and launch_ok reads 1. With 0x55 it does not, launch_ok reads 0, and the SAME phase fault is reported ") & diag_name(code_log(4)) & string'(" -- not misdiagnosed, DECLINED. The answer a reader can act on is `change the payload and measure again`"));
        writeline(output, ln);
        write(ln, string'("    3b. and the gate earns its keep on the other side: the same awkward payload on a CORRECT link was diagnosed ") & diag_name(code_log(5)) & string'(". Without the applicability gate the decoder would have timed whatever transition came first and accused a good link of a phase fault -- a false alarm, which is the more damaging of the two errors because it sends somebody to change a working master"));
        writeline(output, ln);

        -- ---- 4. the honest refusal ----
        write(ln, string'("    4. the decoder carries TWO ways of declining. D_UNDECIDED says `my discriminator does not apply to this capture`; D_OTHER says `the wire is wrong and none of my three causes explains it`. Neither is a failure of the instrument and both are actionable, where a diagnostic that always names one of its own causes cannot be wrong and therefore cannot be informative"));
        writeline(output, ln);

        -- ---- BENCH INTEGRITY: prove these checks can fail ----
        -- A PASS is not evidence a bench is self-checking. Two deliberately wrong expectations must
        -- produce mismatches; if they do not, the comparison is not reaching the caller.
        if code_log(1) /= D_CPHA then mutations := mutations + 1; end if;
        if code_log(3) /= D_CPOL 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'(" diagnoses contained an X; an X compared with an inequality is unreadable and `if (X)` is false"));
            writeline(output, ln); e := e + 1;
        end if;
        if diag_bad /= 0 then
            write(ln, string'("  FAIL: ") & i2s(diag_bad, 1) & string'(" diagnosis mismatch(es)"));
            writeline(output, ln); e := e + 1;
        end if;

        errors <= e;
        if e = 0 then
            write(ln, string'("    and the bench proved itself: two deliberately wrong expectations mismatched, and every reported field carried a known value"));
            writeline(output, ln);
            write(ln, string'("PASS: a well-formed frame carrying the wrong data has three mode causes, and each one is separated by a DIFFERENT KIND of observation. A wrong idle level is STATIC -- no data, no edges, no frame -- and it has to be checked first and separately, because `leading edge` is defined relative to the idle level, so inverting the idle level disturbs the data too and makes a CPOL fault look like a phase fault to anything reading bytes. A wrong launch phase is a property of the MASTER's timing, and separating it took TWO numbers rather than one: the edge COUNT at the first MOSI change was ") & i2s(first_log(2), 1) & string'(" for both the fault and the correct link, and only the COINCIDENCE -- whether that change landed ON an edge -- told them apart, ") & b2s(fae_log(2), 1) & string'(" against ") & b2s(fae_log(0), 1) & string'(". And a wrong sampling edge is diagnosed by ELIMINATION -- the wire matched the expectation and the receiver disagreed with the wire, which exonerates the transmitter and leaves one place for the fault to be; that is the strongest statement a bus capture can make about a device it cannot see. The pair that matters reported the SAME wrong byte 0x") & hex8(rx_log(2)(7 downto 0)) & string'(" from opposite ends of the link, so bytes cannot separate them and a pin transition TIME can. And the discriminator's blind spot belongs to the PAYLOAD rather than to the decoder: when the first bit equals MOSI's idle level a correct launch produces no transition, there is nothing to timestamp, and the decoder DECLINES -- reporting ") & diag_name(code_log(4)) & string'(" rather than guessing, which is an answer somebody can act on by changing the payload. The gate that makes it decline also earns its keep in the other direction: the same awkward payload on a correct link was reported ") & diag_name(code_log(5)) & string'(" rather than accused of a phase fault, and a false alarm that sends an engineer to fix a working master is the more damaging of the two errors"));
            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;

10. Wiring It Into A UVM Environment

The decoder is a passive analysis component, so it drops into the environment Chapter 16.7 built without any new plumbing: the monitor already publishes what was on the wire, and the scoreboard already knows what was expected and what the receiver reported. The diagnosis needs all three in one place.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   spi_monitor  ──analysis_port──▶  spi_scoreboard
                                         │  mismatch: exp 0xa5, rx 0x4a
                                         ▼
                                    spi_mode_diag        ← exp + rx + the captured pins
                                         │
                                         ▼
                                    CPOL / CPHA / EDGE / UNDECIDED / OTHER

The wiring is routine. The reporting decision is not, and it is the part that gets this wrong in real environments:

VerdictSeverityWhy
CPOL, CPHA, EDGEUVM_ERRORa diagnosed defect
OTHERUVM_ERRORa real mismatch this analysis does not explain
UNDECIDEDUVM_INFO plus a coverage holethe capture could not answer the question

11. What This Decoder Cannot Do

Stated plainly, because a diagnostic whose limits are unpublished gets trusted outside them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ✗ diagnose a CPHA fault when the payload's first bit equals MOSI's idle level
   ✗ separate a CPOL fault from a simultaneous CPHA fault (CPOL is reported first)
   ✗ diagnose anything without word_exp — it needs to know what should have been sent
   ✗ distinguish a receiver sampling on the wrong edge from one sampling at the right
     edge with too little setup margin — both disagree with a correct wire

That last one is a real boundary, not a technicality. EDGE is an elimination, and an elimination names a location, not a mechanism. It says the fault is inside the receiver; it does not say whether the receiver's designer chose the wrong edge or whether the physical path has too little margin at this clock rate. Chapter 18.6 takes up the second possibility, and its discriminator is clock rate rather than anything visible in one capture.

12. Why an FPGA Engineer Cares

Mode faults are the single most common reason a first bring-up of an SPI peripheral returns garbage, and the usual response is to try all four modes until one works. That does find the answer, and it teaches nothing and leaves nothing behind — the next peripheral starts from scratch.

The three observations here are all available on an FPGA. The static idle-level check costs one flip-flop and one comparator and needs no data. The launch-timing check costs a counter and two flops. word_rx is whatever your soft-core or state machine reported, which you already have in a register. A hundred lines of logic turns a four-way guess into a named fault — and it keeps working for the peripheral you have no scope on, at a data rate your analyser cannot follow, on a board in someone else's lab.

13. Why an ASIC Engineer Cares

Two of these three faults are yours and one is the other device's, and after silicon the distinction decides who owns the schedule.

A CPOL or CPHA fault in your master is a configuration or timing defect you can fix in firmware if you designed the register, and in metal if you did not. An EDGE finding points into the slave, and a finding that points out of your design needs to be airtight before you send it — which is exactly why section 6's insistence on applicability is not pedantry. "We measured the launch timing and it was correct" survives a meeting with the other vendor. "The wire looked right to us" does not.

14. Failure Signature — Four Modes, No Measurement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom     an SPI flash returns 0x00 and 0xff in alternation
   Response    try mode 0; try mode 1; try mode 2; try mode 3
   Outcome     mode 3 works
   Filed as    "device needs mode 3"
   Actual      the master's CPOL register was written after the first
               transfer was already queued, so the FIRST transfer of
               every session used mode 0 and the rest used mode 3
   Cost        six weeks later, the same bug in a different driver,
               and the note in the tracker said "needs mode 3"

The four-way sweep found a setting that works and produced no evidence, so the finding could not be transferred, generalised or checked. The static observation would have shown SCLK resting at the wrong level during the first transfer only, which names the bug rather than working around it. Note also that the sweep's conclusion was not wrong — mode 3 does work. A workaround that succeeds is the most durable way to lose a bug.

15. Common Misconceptions

MisconceptionWhat is actually true
"Wrong mode" is one bugThree defects in two devices, fixed three different ways
CPOL only affects the idle levelIt redefines which edge is leading, so it moves the data too
A shifted byte means CPHAIt means CPOL, CPHA or EDGE; the byte separates none of them
Timing the first MOSI change diagnoses CPHAThe edge count is identical for the fault and a correct link; the coincidence separates them
If the wire is right, the slave is at faultOnly if the observation that could have convicted the master applied
A diagnostic should always name a causeOne that always names a cause cannot be wrong, and therefore cannot be informative

16. Reason It Through

17. Understanding Check

18. Summary

Three mode faults share one symptom, and two of them share the identical wrong byte — so the discrimination has to come from three different kinds of observation, none of which is a value comparison. A wrong idle level is a static observation taken in the deselected gap, and it must run first because leading edge is defined relative to that level, so a CPOL fault presents as a phase fault to anything reading data. A wrong launch phase is a timing observation, and it needs two numbers rather than one: the edge count at the first MOSI change was 0 for both the fault and the correct link, and only the coincidence — whether the change landed on an edge — told them apart. A wrong sampling edge is an elimination, which is the strongest statement a bus capture can make about a device it has no visibility into.

That elimination is only legitimate when the exonerating observation applied. With a payload whose first bit equals MOSI's idle level a correct launch produces no transition, the transmitter is unrefuted rather than blameless, and the decoder reports UNDECIDED — an answer an engineer can act on by changing the payload. The version without that branch reported a master-side timing fault as a slave-side sampling fault, with full confidence, which is the more expensive of the two ways a diagnostic can fail. And because a gate that declines can decline the wrong things, the same awkward payload on a correct link is driven too, and reported clean.

19. What Comes Next

The mode is now either exonerated or named. Chapter 18.3 takes the remaining alignment faults — bit-order mismatch, a rotation, a miscounted clock — and finds that the debug pattern decides what a capture can tell you: 0xAA makes reversal and rotation produce byte-for-byte identical captures, so the choice of test payload is part of the instrument.

Continue learning