Skip to content
VLSI Mentor

SPI · Module 10

Identifying the Required SPI Mode

The two observations that read CPOL and CPHA off any vendor timing diagram, why the picture is more trustworthy than the prose beside it, why trying all four modes cannot work, and the observer that infers the mode from a live capture.

Chapter 10.1's second pass asked for CPOL and CPHA. Most datasheets never write either word.

The datasheet has a timing diagram and no statement of the mode. How do you get the mode out of the picture — and why is the picture more trustworthy than the sentence next to it?

Two observations settle it. Both are visible in any correctly drawn timing diagram, and neither requires the vendor to have used the words.

1. Why the Words Are Not Enough

Three things go wrong with prose descriptions of the mode, and all three are common.

The mode number may be absent. Many parts simply draw the waveform and say nothing. Sensor datasheets in particular describe the interface entirely pictorially.

The mode number may be stated for the wrong convention. CPOL and CPHA are unambiguous; "SPI mode 0" is a convention built on them, and while it is nearly universal it is not defined in any standard anyone owns. A handful of parts number their modes differently, and a few describe "mode 0" while drawing mode 3.

The prose may describe the SLAVE's view. "Data is output on the rising edge" tells you what the device does with its own output, not what it expects on its input — and those are opposite edges. A reader who converts that sentence directly into CPHA gets the wrong answer, consistently.

The picture has none of these problems, because the picture shows the actual electrical behaviour. So read the picture.

2. Observation One — The Idle Level Gives CPOL

Look at SCLK outside the frame, where chip select is inactive.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   SCLK rests low  while CS is inactive  →  CPOL = 0
   SCLK rests high while CS is inactive  →  CPOL = 1

That is the entire definition, and it is the easier half of the job. Two cautions:

  • Look outside the frame, not at the start of it. A diagram that begins with the first clock edge may not show the idle level at all. Find the part of the figure where CS is deasserted.
  • A diagram drawn with SCLK starting from the middle of the page sometimes shows a level that is simply where the artist began the line. If CS is not drawn, treat the idle level as unconfirmed and look for a second figure.

3. Observation Two — The Launching Edge Gives CPHA

This is the half that needs care, and the reliable way to read it is to ignore the sampling arrows and look at where the data changes.

Number the SCLK edges inside the frame from 1.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   MOSI changes at CS assertion and on edges 2, 4, 6 (the EVEN edges)
       → data is sampled on the odd edges → CPHA = 0

   MOSI changes on edges 1, 3, 5 (the ODD edges)
       → data is sampled on the even edges → CPHA = 1

The distinguishing visual is the first bit. With CPHA=0 the first data bit is already valid before the first clock edge — it appears when CS falls. With CPHA=1 the first data bit appears on the first clock edge. A diagram in which the data line changes simultaneously with CS is showing you CPHA=0, and that is often the quickest read of the whole figure.

4. Reading It Off the Figure

Reading CPOL and CPHA off a capture

10 cycles
A chip select going low for six cycles. SCLK idles low outside the frame and toggles inside it. MOSI changes value at the moment chip select falls and again on alternate edges, holding each value across the edge that samples it.launch at CSlaunch at CSsampled heresampled herecs_nsclkmosi----D7D7D6D6D5D5----t0t1t2t3t4t5t6t7t8t9
Figure 1 — the two observations on one capture. SCLK rests low outside the frame, so CPOL is 0. MOSI changes when CS falls and again on the even edges — the trailing ones — so the odd edges sample and CPHA is 0. Mode 0.

Work the figure in the order of §2 and §3. Outside the frame SCLK sits low, so CPOL = 0. The first data bit appears at the moment CS falls rather than on the first edge, and every later change lands on an even edge, so CPHA = 0. The mode is 0.

Change one thing at a time to see the others. If SCLK rested high with the same data behaviour, the mode would be 2. If the data first changed on edge 1 instead of at CS, it would be 1 — and with SCLK resting high as well, 3.

5. Why "Try All Four" Does Not Work

The tempting shortcut is to sweep the mode until something responds. It fails for a specific reason worth understanding.

A wrong mode and a wrong command produce the same symptom. Both return data that is wrong. So a sweep over four modes, with any other detail also wrong, reports four failures and tells you nothing — and the engineer concludes the part is dead.

Two of the four modes often appear to work. Modes 0 and 3 differ only in the idle level of SCLK; the sampling relationship is identical. Many devices tolerate either, because what matters to the shift register is the edge sequence, not where the clock rested beforehand. So a sweep frequently finds two modes that work, which tells you nothing about which the datasheet specified — and the one you pick may be the one that fails at temperature or at a higher rate.

A sweep can appear to succeed and still be wrong. A mode that is wrong by CPHA shifts the data by half a bit time. At a low clock rate, with a slow device and generous margins, that can still sample correctly — so the sweep succeeds on the bench at 1 MHz and fails in the field at 20 MHz.

The two observations of §2 and §3 take a minute and give the answer the vendor intended. That is strictly better than a search whose success criterion cannot distinguish the cases.

6. Building the Mode Inferrer — Three HDLs

The circuit

Circuit. An oversampling observer that turns the two observations into logic.

State. A count of MOSI transitions attributed to odd edges, to even edges, and to the interval before any edge; the parity of the most recent edge; and the last-sampled idle level.

Datapath. CPOL is a single sample taken while CS is inactive. CPHA is a comparison of the two transition counters — the parity that MOSI changes on is CPHA.

Control. Evidence accumulates through the frame and the verdict is published on the rising edge of CS. Reporting mid-frame would produce a judgement based on one or two transitions that then changed as the frame continued.

Clock and reset. An oversampling clock much faster than SCLK; asynchronous active-low reset. Every bus input passes through a two-stage synchroniser, because an inference built on a metastable sample is worse than no inference.

Enables. mode_valid is withheld when the frame carried too few transitions to decide. An all-ones payload contains no phase information at all, and the honest output is silence.

Timing. The attribution window is the subtle part. A bit is launched at an edge, so a MOSI transition seen in the same oversampling cycle as an edge belongs to that edge — while a transition seen a cycle or two later, from real clock-to-out, belongs to the edge already recorded. Both must land on the same parity, and getting it wrong inverts CPHA in every mode while leaving CPOL correct.

Synthesis. Three counters, two synchronisers and a comparator. It is small enough to leave permanently instantiated on a debug build.

Limitations. It cannot decide CPHA from a payload with no transitions, and it reports conflict rather than guessing when a capture shows transitions on both parities.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_infer.sv — the parity of MOSI transitions is CPHA
// spi_mode_infer.sv
//
// Chapter 10.2 -- reading CPOL and CPHA off a capture instead of guessing.
//
// A vendor timing diagram tells you the mode in two observations, and both
// can be made by logic:
//
//   CPOL is the level SCLK rests at while CS is inactive. One sample.
//
//   CPHA is the edge on which data is LAUNCHED. Number the SCLK edges
//   inside a frame from 1. With CPHA=0 the first bit is launched at CS
//   assertion and later bits on the trailing edges -- 2, 4, 6, the EVEN
//   edges. With CPHA=1 every bit is launched on a leading edge -- 1, 3, 5,
//   the ODD ones. So the parity of the edges that MOSI changes after is
//   CPHA itself.
//
// This block oversamples the bus, counts transitions by edge parity, and
// reports the mode at the end of the frame. It exists because the usual
// alternative -- trying all four modes until one works -- cannot
// distinguish a wrong mode from a wrong command, and Chapter 3.8's failure
// signature is subtle enough that "it mostly works" is a common verdict.

module spi_mode_infer #(
    parameter int CNT_W         = 8,
    parameter int MIN_EVIDENCE  = 2   // transitions needed before reporting
) (
    input  logic       clk,        // oversampling clock, much faster than SCLK
    input  logic       rst_n,

    input  logic       sclk,       // observed bus
    input  logic       mosi,
    input  logic       cs_n,

    output logic [1:0] mode,       // {CPOL, CPHA} -- the mode number
    output logic       mode_valid, // a frame produced enough evidence
    output logic       conflict    // transitions on BOTH parities
);

    // Two-stage synchronisers. The bus is asynchronous to this clock, and
    // an inference built on a metastable sample is worse than no inference.
    logic sclk_s1, sclk_s2, sclk_q;
    logic mosi_s1, mosi_s2, mosi_q;
    logic cs_s1,   cs_s2,   cs_q;

    logic [CNT_W-1:0] odd_cnt;    // transitions following an odd  edge
    logic [CNT_W-1:0] even_cnt;   // transitions following an even edge
    logic [CNT_W-1:0] pre_cnt;    // transitions before any edge (CS launch)
    logic             any_edge;   // at least one SCLK edge seen this frame
    logic             edge_odd;   // parity of the most recent edge
    logic             cpol_r;

    wire sclk_edge = (sclk_s2 != sclk_q);
    // Parity of the edge this cycle belongs to. A bit is launched AT an
    // edge, so a MOSI transition seen in the same cycle as an edge belongs
    // to that edge, not to the previous one -- while a transition seen a
    // cycle or two later (real clock-to-out) belongs to the edge already
    // recorded. Both cases must land on the same parity, and getting this
    // wrong inverts CPHA in every mode while leaving CPOL correct.
    wire next_odd  = any_edge ? ~edge_odd : 1'b1;
    wire cur_odd   = sclk_edge ? next_odd : edge_odd;
    wire have_edge = any_edge | sclk_edge;
    wire mosi_edge = (mosi_s2 != mosi_q);
    wire in_frame  = (cs_s2 == 1'b0);
    wire cs_fall   = (cs_q == 1'b1) && (cs_s2 == 1'b0);
    wire cs_rise   = (cs_q == 1'b0) && (cs_s2 == 1'b1);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            {sclk_s1, sclk_s2, sclk_q} <= 3'b000;
            {mosi_s1, mosi_s2, mosi_q} <= 3'b000;
            {cs_s1,   cs_s2,   cs_q}   <= 3'b111;

            odd_cnt    <= {CNT_W{1'b0}};
            even_cnt   <= {CNT_W{1'b0}};
            pre_cnt    <= {CNT_W{1'b0}};
            any_edge   <= 1'b0;
            edge_odd   <= 1'b0;
            cpol_r     <= 1'b0;

            mode       <= 2'd0;
            mode_valid <= 1'b0;
            conflict   <= 1'b0;
        end else begin
            sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
            mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
            cs_s1   <= cs_n; cs_s2   <= cs_s1;   cs_q   <= cs_s2;

            // CPOL is simply the idle level. Sampling it continuously while
            // CS is inactive means the last sample before the frame is the
            // one that counts, which is exactly the definition.
            if (cs_s2 == 1'b1)
                cpol_r <= sclk_s2;

            if (cs_fall) begin
                // A new frame: discard the previous frame's evidence. Mode
                // is a property of one device, and two devices on the same
                // bus may legitimately differ.
                odd_cnt  <= {CNT_W{1'b0}};
                even_cnt <= {CNT_W{1'b0}};
                pre_cnt  <= {CNT_W{1'b0}};
                any_edge <= 1'b0;
                edge_odd <= 1'b0;
            end else if (in_frame) begin
                if (sclk_edge) begin
                    any_edge <= 1'b1;
                    // Edge 1 is odd, 2 even, and so on.
                    edge_odd <= next_odd;
                end

                if (mosi_edge) begin
                    if (!have_edge)    pre_cnt  <= pre_cnt  + 1'b1;
                    else if (cur_odd)  odd_cnt  <= odd_cnt  + 1'b1;
                    else               even_cnt <= even_cnt + 1'b1;
                end
            end

            if (cs_rise) begin
                // Report at the end of the frame, when the evidence is
                // complete. A mid-frame verdict would be based on one or
                // two transitions and would change as the frame went on.
                if ((odd_cnt + even_cnt + pre_cnt) >= CNT_W'(MIN_EVIDENCE)) begin
                    mode       <= {cpol_r, (odd_cnt > even_cnt)};
                    mode_valid <= 1'b1;
                end else begin
                    // Not enough transitions to tell. An all-ones or
                    // all-zeros payload carries no phase information at all,
                    // which is a real limit of the method and not a bug.
                    mode_valid <= 1'b0;
                end
                // Transitions on both parities mean the capture is not a
                // clean single-mode frame -- a mismatched slave, a glitching
                // clock, or a trigger that caught two frames.
                conflict <= (odd_cnt != {CNT_W{1'b0}}) &&
                            (even_cnt != {CNT_W{1'b0}});
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_infer_tb.sv — all four modes, driven and read back
// spi_mode_infer_tb.sv
//
// The testbench drives real frames in all four modes with a behavioural
// master, and requires the inference to name each one. It then drives the
// two captures that carry no phase information -- an all-ones payload --
// and requires the block to say so rather than guess.

`timescale 1ns/1ps

module spi_mode_infer_tb;

    localparam int CNT_W = 8;
    localparam time H     = 20ns;   // SCLK half period
    localparam time TCLK  = 2ns;    // oversampling period (10x per half)

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    always #(TCLK/2) clk = ~clk;

    logic sclk = 1'b0;
    logic mosi = 1'b0;
    logic cs_n = 1'b1;

    logic [1:0] mode;
    logic       mode_valid;
    logic       conflict;

    int errors = 0;

    spi_mode_infer #(.CNT_W(CNT_W), .MIN_EVIDENCE(2)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .mosi(mosi), .cs_n(cs_n),
        .mode(mode), .mode_valid(mode_valid), .conflict(conflict)
    );

    // A behavioural master. Leading edge means "away from idle", trailing
    // means "back to idle" -- the definitions from Chapter 2.2, which are
    // what make one body of code serve all four modes.
    // tco models the slave's clock-to-out: MOSI changes tco after the
    // launching edge rather than exactly on it. Zero is the idealised
    // capture; a non-zero value is what a scope actually shows.
    task automatic drive_frame_tco(input logic cpol, input logic cpha,
                                   input logic [7:0] data, input time tco);
        int i;
        begin
            sclk = cpol;
            cs_n = 1'b1;
            #(H*2);
            cs_n = 1'b0;
            if (cpha == 1'b0) begin              // CPHA=0 launches at CS
                #tco; mosi = data[7]; #(H-tco);
            end else begin
                #H;
            end
            for (i = 0; i < 8; i++) begin
                sclk = ~cpol;                    // leading edge
                if (cpha == 1'b1) begin
                    #tco; mosi = data[7-i]; #(H-tco);
                end else begin
                    #H;
                end
                sclk = cpol;                     // trailing edge
                if (cpha == 1'b0 && i < 7) begin
                    #tco; mosi = data[6-i]; #(H-tco);
                end else begin
                    #H;
                end
            end
            cs_n = 1'b1;
            #(H*2);
        end
    endtask

    task automatic drive_frame(input logic cpol, input logic cpha,
                               input logic [7:0] data);
        begin
            drive_frame_tco(cpol, cpha, data, 0ns);
        end
    endtask

    task automatic expect_mode(input logic cpol, input logic cpha,
                               input logic [7:0] data);
        logic [1:0] want;
        begin
            want = {cpol, cpha};
            drive_frame(cpol, cpha, data);
            if (!mode_valid) begin
                $display("  FAIL: mode %0d frame produced no verdict", want);
                errors++;
            end else if (mode !== want) begin
                $display("  FAIL: mode %0d frame was read as mode %0d", want, mode);
                errors++;
            end else begin
                $display("  mode %0d frame (data 0x%02h) inferred as mode %0d  cpol=%0b cpha=%0b",
                         want, data, mode, mode[1], mode[0]);
            end
            if (conflict) begin
                $display("  FAIL: clean mode %0d frame reported a conflict", want);
                errors++;
            end
        end
    endtask

    initial begin
        repeat (5) @(negedge clk);
        rst_n = 1'b1;
        @(negedge clk);

        // 1. All four modes, with a payload that transitions often enough
        //    to carry phase information.
        expect_mode(1'b0, 1'b0, 8'hA5);
        expect_mode(1'b0, 1'b1, 8'hA5);
        expect_mode(1'b1, 1'b0, 8'hA5);
        expect_mode(1'b1, 1'b1, 8'hA5);

        // 2. A different payload must not change the verdict -- the mode is
        //    a property of the clocking, not of the data.
        expect_mode(1'b0, 1'b1, 8'h5A);
        expect_mode(1'b1, 1'b0, 8'h33);

        // 3. A payload with no transitions carries no phase information.
        //    The honest answer is "cannot tell", and a block that guessed
        //    would be right half the time and confidently wrong the rest.
        drive_frame(1'b0, 1'b0, 8'hFF);
        if (mode_valid) begin
            $display("  FAIL: an all-ones payload produced a phase verdict");
            errors++;
        end else begin
            $display("  all-ones payload: no verdict offered, as it must be");
        end

        // 4. A real capture has clock-to-out: MOSI changes a few
        //    nanoseconds AFTER its launching edge, not exactly on it. The
        //    attribution must put both cases on the same parity, so every
        //    mode is re-run with a delay of a quarter of a half period.
        for (int m = 0; m < 4; m++) begin
            drive_frame_tco(m[1], m[0], 8'hA5, H/4);
            if (!mode_valid || mode !== m[1:0]) begin
                $display("  FAIL: with clock-to-out, mode %0d was read as %0d (valid=%0b)",
                         m, mode, mode_valid);
                errors++;
            end
        end
        $display("  all four modes re-inferred with a %0t clock-to-out delay", H/4);

        // 5. CPOL survives the case above: it needs no data transitions at
        //    all, only the idle level, so the two halves of the answer have
        //    genuinely different evidence requirements.
        drive_frame(1'b1, 1'b1, 8'hA5);
        if (!mode_valid || mode[1] !== 1'b1) begin
            $display("  FAIL: CPOL=1 not recovered from the idle level");
            errors++;
        end

        if (errors == 0)
            $display("PASS: all four modes are inferred from the edge parity of MOSI transitions, the verdict is independent of the payload, and a payload with no transitions is reported as no verdict rather than a guess");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule

Three things in that testbench are worth more than the rest.

It drives real frames in all four modes with one behavioural master, parameterised only by CPOL and CPHA — which is possible because "leading edge" and "trailing edge" are defined relative to the idle level (Chapter 2.2). One body of code covering four modes is a sign the abstraction is right.

It re-runs every mode with a clock-to-out delay, because a real capture never has MOSI changing exactly on its edge. That test is what proves the attribution window handles both cases rather than only the idealised one.

And it requires an all-ones payload to produce no verdict. A block that guessed there would be right half the time and confidently wrong the rest, which is worse than useless in a diagnostic.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_infer.v — the same observer in Verilog-2001
// spi_mode_infer.v
//
// Chapter 10.2 -- reading CPOL and CPHA off a capture, in Verilog-2001.
//
// CPOL is the level SCLK rests at while CS is inactive: one sample.
// CPHA is the parity of the edges MOSI is launched on -- number the SCLK
// edges inside a frame from 1, and CPHA=0 launches at CS assertion and on
// the EVEN edges, CPHA=1 on the ODD ones.

module spi_mode_infer #(
    parameter CNT_W        = 8,
    parameter MIN_EVIDENCE = 2    // transitions needed before reporting
) (
    input  wire       clk,        // oversampling clock, much faster than SCLK
    input  wire       rst_n,

    input  wire       sclk,       // observed bus
    input  wire       mosi,
    input  wire       cs_n,

    output reg  [1:0] mode,       // {CPOL, CPHA} -- the mode number
    output reg        mode_valid, // a frame produced enough evidence
    output reg        conflict    // transitions on BOTH parities
);

    // Two-stage synchronisers. The bus is asynchronous to this clock, and
    // an inference built on a metastable sample is worse than none.
    reg sclk_s1, sclk_s2, sclk_q;
    reg mosi_s1, mosi_s2, mosi_q;
    reg cs_s1,   cs_s2,   cs_q;

    reg [CNT_W-1:0] odd_cnt;    // transitions following an odd  edge
    reg [CNT_W-1:0] even_cnt;   // transitions following an even edge
    reg [CNT_W-1:0] pre_cnt;    // transitions before any edge (CS launch)
    reg             any_edge;   // at least one SCLK edge seen this frame
    reg             edge_odd;   // parity of the most recent edge
    reg             cpol_r;

    wire sclk_edge = (sclk_s2 != sclk_q);
    wire mosi_edge = (mosi_s2 != mosi_q);
    wire in_frame  = (cs_s2 == 1'b0);
    wire cs_fall   = (cs_q == 1'b1) && (cs_s2 == 1'b0);
    wire cs_rise   = (cs_q == 1'b0) && (cs_s2 == 1'b1);

    // Parity of the edge this cycle belongs to. A bit is launched AT an
    // edge, so a MOSI transition seen in the same cycle as an edge belongs
    // to that edge, not the previous one -- while a transition seen a cycle
    // or two later (real clock-to-out) belongs to the edge already
    // recorded. Both must land on the same parity; getting this wrong
    // inverts CPHA in every mode while leaving CPOL correct.
    wire next_odd  = any_edge ? ~edge_odd : 1'b1;
    wire cur_odd   = sclk_edge ? next_odd : edge_odd;
    wire have_edge = any_edge | sclk_edge;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_s1 <= 1'b0; sclk_s2 <= 1'b0; sclk_q <= 1'b0;
            mosi_s1 <= 1'b0; mosi_s2 <= 1'b0; mosi_q <= 1'b0;
            cs_s1   <= 1'b1; cs_s2   <= 1'b1; cs_q   <= 1'b1;

            odd_cnt    <= {CNT_W{1'b0}};
            even_cnt   <= {CNT_W{1'b0}};
            pre_cnt    <= {CNT_W{1'b0}};
            any_edge   <= 1'b0;
            edge_odd   <= 1'b0;
            cpol_r     <= 1'b0;

            mode       <= 2'd0;
            mode_valid <= 1'b0;
            conflict   <= 1'b0;
        end else begin
            sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
            mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
            cs_s1   <= cs_n; cs_s2   <= cs_s1;   cs_q   <= cs_s2;

            // CPOL is the idle level. Sampling continuously while CS is
            // inactive means the last sample before the frame is the one
            // that counts, which is exactly the definition.
            if (cs_s2 == 1'b1)
                cpol_r <= sclk_s2;

            if (cs_fall) begin
                // A new frame: discard the previous frame's evidence. Mode
                // is a property of one device, and two devices on the same
                // bus may legitimately differ.
                odd_cnt  <= {CNT_W{1'b0}};
                even_cnt <= {CNT_W{1'b0}};
                pre_cnt  <= {CNT_W{1'b0}};
                any_edge <= 1'b0;
                edge_odd <= 1'b0;
            end else if (in_frame) begin
                if (sclk_edge) begin
                    any_edge <= 1'b1;
                    edge_odd <= next_odd;        // edge 1 is odd, 2 even, ...
                end

                if (mosi_edge) begin
                    if (!have_edge)    pre_cnt  <= pre_cnt  + 1'b1;
                    else if (cur_odd)  odd_cnt  <= odd_cnt  + 1'b1;
                    else               even_cnt <= even_cnt + 1'b1;
                end
            end

            if (cs_rise) begin
                // Report at the end of the frame, when the evidence is
                // complete. A mid-frame verdict would rest on one or two
                // transitions and would change as the frame went on.
                if ((odd_cnt + even_cnt + pre_cnt) >= MIN_EVIDENCE) begin
                    mode       <= {cpol_r, (odd_cnt > even_cnt)};
                    mode_valid <= 1'b1;
                end else begin
                    // Not enough transitions to tell. An all-ones or
                    // all-zeros payload carries no phase information at all,
                    // which is a real limit of the method and not a bug.
                    mode_valid <= 1'b0;
                end
                conflict <= (odd_cnt != {CNT_W{1'b0}}) &&
                            (even_cnt != {CNT_W{1'b0}});
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_infer_tb.v — the same four modes in Verilog-2001
// spi_mode_infer_tb.v
//
// The same checks as the SystemVerilog testbench: all four modes inferred
// from real frames, a verdict independent of the payload, both attribution
// windows (a transition on the edge and one delayed by clock-to-out), and
// an honest "cannot tell" for a payload with no transitions.

`timescale 1ns/1ps

module spi_mode_infer_tb;

    parameter CNT_W = 8;
    parameter H     = 20;   // SCLK half period, ns
    parameter TCLK  = 2;    // oversampling period, ns (10x per half)

    reg clk;
    reg rst_n;
    reg sclk;
    reg mosi;
    reg cs_n;

    wire [1:0] mode;
    wire       mode_valid;
    wire       conflict;

    integer errors;
    integer m;

    initial begin
        clk = 1'b0; rst_n = 1'b0;
        sclk = 1'b0; mosi = 1'b0; cs_n = 1'b1;
        errors = 0;
    end
    always #(TCLK/2) clk = ~clk;

    spi_mode_infer #(.CNT_W(CNT_W), .MIN_EVIDENCE(2)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .mosi(mosi), .cs_n(cs_n),
        .mode(mode), .mode_valid(mode_valid), .conflict(conflict)
    );

    // A behavioural master. Leading edge means "away from idle", trailing
    // means "back to idle" -- the definitions that let one body of code
    // serve all four modes. tco models the slave's clock-to-out.
    task drive_frame_tco;
        input       cpol;
        input       cpha;
        input [7:0] data;
        input integer tco;
        integer i;
        begin
            sclk = cpol;
            cs_n = 1'b1;
            #(H*2);
            cs_n = 1'b0;
            if (cpha == 1'b0) begin              // CPHA=0 launches at CS
                #(tco); mosi = data[7]; #(H-tco);
            end else begin
                #(H);
            end
            for (i = 0; i < 8; i = i + 1) begin
                sclk = ~cpol;                    // leading edge
                if (cpha == 1'b1) begin
                    #(tco); mosi = data[7-i]; #(H-tco);
                end else begin
                    #(H);
                end
                sclk = cpol;                     // trailing edge
                if (cpha == 1'b0 && i < 7) begin
                    #(tco); mosi = data[6-i]; #(H-tco);
                end else begin
                    #(H);
                end
            end
            cs_n = 1'b1;
            #(H*2);
        end
    endtask

    task drive_frame;
        input       cpol;
        input       cpha;
        input [7:0] data;
        begin
            drive_frame_tco(cpol, cpha, data, 0);
        end
    endtask

    task expect_mode;
        input       cpol;
        input       cpha;
        input [7:0] data;
        reg   [1:0] want;
        begin
            want = {cpol, cpha};
            drive_frame(cpol, cpha, data);
            if (!mode_valid) begin
                $display("  FAIL: mode %0d frame produced no verdict", want);
                errors = errors + 1;
            end else if (mode !== want) begin
                $display("  FAIL: mode %0d frame was read as mode %0d", want, mode);
                errors = errors + 1;
            end else begin
                $display("  mode %0d frame (data 0x%02h) inferred as mode %0d  cpol=%0b cpha=%0b",
                         want, data, mode, mode[1], mode[0]);
            end
            if (conflict) begin
                $display("  FAIL: clean mode %0d frame reported a conflict", want);
                errors = errors + 1;
            end
        end
    endtask

    initial begin
        repeat (5) @(negedge clk);
        rst_n = 1'b1;
        @(negedge clk);

        // 1. All four modes, with a payload that transitions often enough
        //    to carry phase information.
        expect_mode(1'b0, 1'b0, 8'hA5);
        expect_mode(1'b0, 1'b1, 8'hA5);
        expect_mode(1'b1, 1'b0, 8'hA5);
        expect_mode(1'b1, 1'b1, 8'hA5);

        // 2. A different payload must not change the verdict -- the mode is
        //    a property of the clocking, not of the data.
        expect_mode(1'b0, 1'b1, 8'h5A);
        expect_mode(1'b1, 1'b0, 8'h33);

        // 3. A payload with no transitions carries no phase information.
        //    The honest answer is "cannot tell", and a block that guessed
        //    would be right half the time and confidently wrong the rest.
        drive_frame(1'b0, 1'b0, 8'hFF);
        if (mode_valid) begin
            $display("  FAIL: an all-ones payload produced a phase verdict");
            errors = errors + 1;
        end else begin
            $display("  all-ones payload: no verdict offered, as it must be");
        end

        // 4. A real capture has clock-to-out: MOSI changes a few nanoseconds
        //    AFTER its launching edge, not exactly on it. The attribution
        //    must put both cases on the same parity.
        for (m = 0; m < 4; m = m + 1) begin
            drive_frame_tco(m[1], m[0], 8'hA5, H/4);
            if (!mode_valid || mode !== m[1:0]) begin
                $display("  FAIL: with clock-to-out, mode %0d was read as %0d (valid=%0b)",
                         m, mode, mode_valid);
                errors = errors + 1;
            end
        end
        $display("  all four modes re-inferred with a %0d ns clock-to-out delay", H/4);

        // 5. CPOL survives the no-transition case: it needs no data
        //    transitions at all, only the idle level, so the two halves of
        //    the answer have genuinely different evidence requirements.
        drive_frame(1'b1, 1'b1, 8'hA5);
        if (!mode_valid || mode[1] !== 1'b1) begin
            $display("  FAIL: CPOL=1 not recovered from the idle level");
            errors = errors + 1;
        end

        if (errors == 0)
            $display("PASS: all four modes are inferred from the edge parity of MOSI transitions, the verdict is independent of the payload, and a payload with no transitions is reported as no verdict rather than a guess");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_infer.vhd — the same observer in VHDL
-- spi_mode_infer.vhd
--
-- Chapter 10.2 -- reading CPOL and CPHA off a capture, in VHDL.
--
-- CPOL is the level SCLK rests at while CS is inactive: one sample.
-- CPHA is the parity of the edges MOSI is launched on -- number the SCLK
-- edges inside a frame from 1, and CPHA=0 launches at CS assertion and on
-- the EVEN edges, CPHA=1 on the ODD ones.

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

entity spi_mode_infer is
    generic (
        CNT_W        : positive := 8;
        MIN_EVIDENCE : natural  := 2    -- transitions needed before reporting
    );
    port (
        clk        : in  std_logic;     -- oversampling clock, much faster than SCLK
        rst_n      : in  std_logic;

        sclk       : in  std_logic;     -- observed bus
        mosi       : in  std_logic;
        cs_n       : in  std_logic;

        mode       : out std_logic_vector(1 downto 0);  -- {CPOL, CPHA}
        mode_valid : out std_logic;
        conflict   : out std_logic
    );
end entity;

architecture rtl of spi_mode_infer is

    -- Two-stage synchronisers. The bus is asynchronous to this clock, and
    -- an inference built on a metastable sample is worse than none.
    signal sclk_s1, sclk_s2, sclk_q : std_logic := '0';
    signal mosi_s1, mosi_s2, mosi_q : std_logic := '0';
    signal cs_s1,   cs_s2,   cs_q   : std_logic := '1';

    signal odd_cnt  : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal even_cnt : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal pre_cnt  : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal any_edge : std_logic := '0';
    signal edge_odd : std_logic := '0';
    signal cpol_r   : std_logic := '0';

    signal mode_r  : std_logic_vector(1 downto 0) := (others => '0');
    signal valid_r : std_logic := '0';
    signal conf_r  : std_logic := '0';

    signal sclk_edge : std_logic;
    signal mosi_edge : std_logic;
    signal cs_fall   : std_logic;
    signal cs_rise   : std_logic;
    signal next_odd  : std_logic;
    signal cur_odd   : std_logic;
    signal have_edge : std_logic;

begin

    sclk_edge <= '1' when sclk_s2 /= sclk_q else '0';
    mosi_edge <= '1' when mosi_s2 /= mosi_q else '0';
    cs_fall   <= '1' when cs_q = '1' and cs_s2 = '0' else '0';
    cs_rise   <= '1' when cs_q = '0' and cs_s2 = '1' else '0';

    -- Parity of the edge this cycle belongs to. A bit is launched AT an
    -- edge, so a MOSI transition seen in the same cycle as an edge belongs
    -- to that edge, not the previous one -- while a transition seen a cycle
    -- or two later (real clock-to-out) belongs to the edge already
    -- recorded. Both must land on the same parity; getting this wrong
    -- inverts CPHA in every mode while leaving CPOL correct.
    next_odd  <= not edge_odd when any_edge = '1' else '1';
    cur_odd   <= next_odd when sclk_edge = '1' else edge_odd;
    have_edge <= any_edge or sclk_edge;

    observe : process (clk, rst_n)
    begin
        if rst_n = '0' then
            sclk_s1 <= '0'; sclk_s2 <= '0'; sclk_q <= '0';
            mosi_s1 <= '0'; mosi_s2 <= '0'; mosi_q <= '0';
            cs_s1   <= '1'; cs_s2   <= '1'; cs_q   <= '1';

            odd_cnt  <= (others => '0');
            even_cnt <= (others => '0');
            pre_cnt  <= (others => '0');
            any_edge <= '0';
            edge_odd <= '0';
            cpol_r   <= '0';

            mode_r  <= (others => '0');
            valid_r <= '0';
            conf_r  <= '0';
        elsif rising_edge(clk) then
            sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
            mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
            cs_s1   <= cs_n; cs_s2   <= cs_s1;   cs_q   <= cs_s2;

            -- CPOL is the idle level. Sampling continuously while CS is
            -- inactive means the last sample before the frame is the one
            -- that counts, which is exactly the definition.
            if cs_s2 = '1' then
                cpol_r <= sclk_s2;
            end if;

            if cs_fall = '1' then
                -- A new frame: discard the previous frame's evidence. Mode
                -- is a property of one device, and two devices on the same
                -- bus may legitimately differ.
                odd_cnt  <= (others => '0');
                even_cnt <= (others => '0');
                pre_cnt  <= (others => '0');
                any_edge <= '0';
                edge_odd <= '0';
            elsif cs_s2 = '0' then
                if sclk_edge = '1' then
                    any_edge <= '1';              -- edge 1 is odd, 2 even, ...
                    edge_odd <= next_odd;
                end if;

                if mosi_edge = '1' then
                    if have_edge = '0' then
                        pre_cnt <= pre_cnt + 1;
                    elsif cur_odd = '1' then
                        odd_cnt <= odd_cnt + 1;
                    else
                        even_cnt <= even_cnt + 1;
                    end if;
                end if;
            end if;

            if cs_rise = '1' then
                -- Report at the end of the frame, when the evidence is
                -- complete. A mid-frame verdict would rest on one or two
                -- transitions and would change as the frame went on.
                if to_integer(odd_cnt) + to_integer(even_cnt) +
                   to_integer(pre_cnt) >= MIN_EVIDENCE then
                    mode_r(1) <= cpol_r;
                    if odd_cnt > even_cnt then
                        mode_r(0) <= '1';
                    else
                        mode_r(0) <= '0';
                    end if;
                    valid_r <= '1';
                else
                    -- Not enough transitions to tell. An all-ones or
                    -- all-zeros payload carries no phase information at all,
                    -- which is a real limit of the method and not a bug.
                    valid_r <= '0';
                end if;
                if odd_cnt /= 0 and even_cnt /= 0 then
                    conf_r <= '1';
                else
                    conf_r <= '0';
                end if;
            end if;
        end if;
    end process;

    mode       <= mode_r;
    mode_valid <= valid_r;
    conflict   <= conf_r;

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_infer_tb.vhd — the same four modes in VHDL
-- spi_mode_infer_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: all four
-- modes inferred from real frames, a verdict independent of the payload,
-- both attribution windows (a transition on the edge and one delayed by
-- clock-to-out), and an honest "cannot tell" for a flat payload.

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

entity spi_mode_infer_tb is
end entity;

architecture sim of spi_mode_infer_tb is

    constant CNT_W : positive := 8;
    constant H     : time     := 20 ns;   -- SCLK half period
    constant TCLK  : time     := 2 ns;    -- oversampling period

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal halt  : boolean   := false;

    signal sclk : std_logic := '0';
    signal mosi : std_logic := '0';
    signal cs_n : std_logic := '1';

    signal mode       : std_logic_vector(1 downto 0);
    signal mode_valid : std_logic;
    signal conflict   : std_logic;

    signal errors : natural := 0;

begin

    clk <= not clk after TCLK / 2 when not halt else '0';

    dut : entity work.spi_mode_infer
        generic map (CNT_W => CNT_W, MIN_EVIDENCE => 2)
        port map (
            clk => clk, rst_n => rst_n,
            sclk => sclk, mosi => mosi, cs_n => cs_n,
            mode => mode, mode_valid => mode_valid, conflict => conflict
        );

    stim : process
        variable errs : natural := 0;
        variable mv   : std_logic_vector(1 downto 0);

        -- A behavioural master. Leading edge means "away from idle",
        -- trailing means "back to idle" -- the definitions that let one
        -- body of code serve all four modes. tco models clock-to-out.
        procedure drive_frame_tco(cpol : std_logic; cpha : std_logic;
                                  data : std_logic_vector(7 downto 0);
                                  tco  : time) is
        begin
            sclk <= cpol;
            cs_n <= '1';
            wait for H * 2;
            cs_n <= '0';
            if cpha = '0' then                    -- CPHA=0 launches at CS
                wait for tco; mosi <= data(7); wait for H - tco;
            else
                wait for H;
            end if;
            for i in 0 to 7 loop
                sclk <= not cpol;                 -- leading edge
                if cpha = '1' then
                    wait for tco; mosi <= data(7 - i); wait for H - tco;
                else
                    wait for H;
                end if;
                sclk <= cpol;                     -- trailing edge
                if cpha = '0' and i < 7 then
                    wait for tco; mosi <= data(6 - i); wait for H - tco;
                else
                    wait for H;
                end if;
            end loop;
            cs_n <= '1';
            wait for H * 2;
        end procedure;

        procedure drive_frame(cpol : std_logic; cpha : std_logic;
                              data : std_logic_vector(7 downto 0)) is
        begin
            drive_frame_tco(cpol, cpha, data, 0 ns);
        end procedure;

        procedure expect_mode(cpol : std_logic; cpha : std_logic;
                              data : std_logic_vector(7 downto 0)) is
            variable want : std_logic_vector(1 downto 0);
        begin
            want := cpol & cpha;
            drive_frame(cpol, cpha, data);
            if mode_valid /= '1' then
                report "  FAIL: mode " &
                       integer'image(to_integer(unsigned(want))) &
                       " frame produced no verdict";
                errs := errs + 1;
            elsif mode /= want then
                report "  FAIL: mode " &
                       integer'image(to_integer(unsigned(want))) &
                       " frame was read as mode " &
                       integer'image(to_integer(unsigned(mode)));
                errs := errs + 1;
            else
                report "  mode " & integer'image(to_integer(unsigned(want))) &
                       " frame inferred as mode " &
                       integer'image(to_integer(unsigned(mode)));
            end if;
            if conflict = '1' then
                report "  FAIL: a clean frame reported a conflict";
                errs := errs + 1;
            end if;
        end procedure;
    begin
        for i in 0 to 4 loop
            wait until falling_edge(clk);
        end loop;
        rst_n <= '1';
        wait until falling_edge(clk);

        -- 1. All four modes, with a payload that transitions often enough
        --    to carry phase information.
        expect_mode('0', '0', x"A5");
        expect_mode('0', '1', x"A5");
        expect_mode('1', '0', x"A5");
        expect_mode('1', '1', x"A5");

        -- 2. A different payload must not change the verdict -- the mode is
        --    a property of the clocking, not of the data.
        expect_mode('0', '1', x"5A");
        expect_mode('1', '0', x"33");

        -- 3. A payload with no transitions carries no phase information.
        --    The honest answer is "cannot tell", and a block that guessed
        --    would be right half the time and confidently wrong the rest.
        drive_frame('0', '0', x"FF");
        if mode_valid = '1' then
            report "  FAIL: an all-ones payload produced a phase verdict";
            errs := errs + 1;
        else
            report "  all-ones payload: no verdict offered, as it must be";
        end if;

        -- 4. A real capture has clock-to-out: MOSI changes a few
        --    nanoseconds AFTER its launching edge, not exactly on it. The
        --    attribution must put both cases on the same parity.
        for m in 0 to 3 loop
            mv := std_logic_vector(to_unsigned(m, 2));
            drive_frame_tco(mv(1), mv(0), x"A5", H / 4);
            if mode_valid /= '1' or mode /= mv then
                report "  FAIL: with clock-to-out, mode " &
                       integer'image(m) & " was read as " &
                       integer'image(to_integer(unsigned(mode)));
                errs := errs + 1;
            end if;
        end loop;
        report "  all four modes re-inferred with a clock-to-out delay";

        -- 5. CPOL survives the no-transition case: it needs no data
        --    transitions at all, only the idle level, so the two halves of
        --    the answer have genuinely different evidence requirements.
        drive_frame('1', '1', x"A5");
        if mode_valid /= '1' or mode(1) /= '1' then
            report "  FAIL: CPOL=1 not recovered from the idle level";
            errs := errs + 1;
        end if;

        errors <= errs;
        if errs = 0 then
            report "PASS: all four modes are inferred from the edge parity of MOSI transitions, the verdict is independent of the payload, and a payload with no transitions is reported as no verdict rather than a guess";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

Parity

All three implement the same observer: identical ports and generics, two-stage synchronisers on every bus input, CPOL from the idle level, CPHA from the parity of MOSI transitions, a verdict published on CS release, and no verdict when the evidence is insufficient. All three testbenches drive the same frames — four modes at two payloads, all four modes again with a clock-to-out delay, and an all-ones frame — and finish at the same simulation time with identical results.

7. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_infer.sva — what an inference must promise
   // 1. DETERMINISM. The same capture always yields the same verdict.
   //    An observer whose answer depends on when it was reset is not
   //    measuring the bus.
   a_stable_verdict : assert property (
       @(posedge clk) disable iff (!rst_n)
           (mode_valid && !cs_fall) |=> $stable(mode))
       else $error("the verdict changed without a new frame");

   // 2. HONESTY. A verdict is only published when the frame carried
   //    enough evidence. This is the property that separates a measurement
   //    from a guess.
   a_no_guessing : assert property (
       @(posedge clk) disable iff (!rst_n)
           (cs_rise && (odd_cnt + even_cnt + pre_cnt) < MIN_EVIDENCE)
               |=> !mode_valid)
       else $error("a verdict was published without evidence");

   // 3. CPOL needs no data at all -- it is the idle level, and it must be
   //    recoverable even from a frame that carries no phase information.
   a_cpol_independent : assert property (
       @(posedge clk) disable iff (!rst_n)
           (cs_fall) |-> (cpol_r == $past(sclk_s2)))
       else $error("CPOL was not taken from the idle level");

   // 4. The conflict flag means what it says: transitions on both
   //    parities, which a clean single-mode frame can never produce.
   a_conflict_real : assert property (
       @(posedge clk) disable iff (!rst_n)
           (cs_rise && conflict) |-> (odd_cnt != 0 && even_cnt != 0))
       else $error("conflict asserted without transitions on both parities");

Property 2 is the one that generalises. Any inference block must be able to say "I do not know" — and the temptation to always produce an answer is strong, because an output that is sometimes invalid is harder to consume. But a diagnostic that is confidently wrong half the time costs more debugging than it saves, and the all-ones payload makes that case reachable in every real system.

Property 3 records something the design makes easy to miss: the two halves of the answer have different evidence requirements. CPOL needs no data transitions whatever; CPHA needs several. A testbench that only checks the mode as a pair will never notice that one half is still correct when the other cannot be determined.

Coverage must reach the cases a directed test would skip:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_cg.sv — payloads, not just modes
   covergroup spi_mode_cg @(posedge cs_rise);
       cp_mode : coverpoint {cpol_r, cpha_inferred} {
           bins m0 = {2'b00}; bins m1 = {2'b01};
           bins m2 = {2'b10}; bins m3 = {2'b11};
       }

       // The payload decides whether CPHA is decidable at all. A suite
       // using only 0xA5 never exercises the "cannot tell" path, and the
       // honesty property above is then never checked.
       cp_payload : coverpoint transitions_in_frame {
           bins none      = {0};        // no phase information
           bins one       = {1};        // below MIN_EVIDENCE
           bins few       = {[2:3]};
           bins many      = {[4:$]};
       }

       // Clock-to-out places the transition in a different oversampling
       // cycle from its edge. Both windows must give the same parity.
       cp_tco : coverpoint tco_class {
           bins on_edge   = {TCO_ZERO};
           bins just_after = {TCO_SMALL};
           bins late      = {TCO_LARGE};
       }

       x_mode_payload : cross cp_mode, cp_payload;
       x_mode_tco     : cross cp_mode, cp_tco;
   endgroup

cp_payload is the coverpoint that matters and the one most often absent. The mode bins fill immediately from any directed suite; the payload bins are what prove the block behaves when the data cannot answer the question.

8. Why an FPGA or ASIC Engineer Cares

Instantiate the inferrer on the debug build. It costs a few hundred gates and answers, in hardware, the question that otherwise needs a scope and an afternoon: what mode is this link actually running in? On a board with several devices it answers it per chip select.

Make the mode a profile field, not a parameter. Chapter 10.1 argued this for the whole profile, and the mode is the field where it pays first: a controller serving two devices in different modes must switch between them per transaction, and a parameter cannot.

Apply the mode through reset or between frames, never during one. Chapter 8.1 made the same point about polarity: a mode change mid-frame produces a transfer matching no device, and the resulting capture is very hard to read.

Prefer mode 3 over mode 0 when a part accepts both, on a bus with other devices. SCLK idling high means the line is not sitting at the level a glitch would have to reach to look like an edge — and, more practically, it makes a stuck-low clock visibly distinct from an idle one on a scope.

Record the idle level in a status bit. One flip-flop sampling SCLK while CS is inactive turns "is the controller configured for the mode we think?" into a register read.

Symptom. Bring-up succeeds at a low clock rate. Every register reads correctly, the ID matches, and the part behaves. Raising SCLK works up to around 8 MHz and then reads become corrupted — not intermittently, but consistently above a threshold. The device is rated far higher.

What the low-rate success establishes. The wiring is right, the command encoding is right and the device is alive. So the fault is something that only matters as the period shrinks — which is a short list.

Plausible mechanisms.

  • A CPHA error. Sampling half a bit time away from the intended point still works while the bit time is long compared with every delay in the system. As the period shrinks, the half-bit error stops being absorbed by the margin. This is the mechanism that best fits a sharp threshold.
  • The round-trip limit of Chapter 9.4, which also produces a sharp threshold — but shifts data by whole bits rather than corrupting it.
  • Signal integrity, which usually degrades gradually rather than at a clean threshold.
  • A divisor that exceeds the device's true maximum for the specific command in use.

The discriminating observation. Compare a good capture at 1 MHz with a bad one at 10 MHz and look at where MOSI changes relative to the edges, not at the data. If the controller is launching on the wrong parity of edge, that is visible in both captures — it was simply tolerated at the lower rate. If the launch parity is identical in both and only the returned data is wrong, the problem is on the return path and belongs to Chapter 9.4.

Then check the mode directly: the inferrer of §6, or a single flip-flop sampling SCLK while CS is inactive plus one look at the first data transition.

The fix, and why it is often mis-applied. Correct the mode. The mis-application is to lower the clock and ship, because the link works at 4 MHz — which leaves a design whose margin is a half bit time and which fails on the first slower device or warmer enclosure.

Why the investigation goes wrong. Because a rate-dependent failure looks like a timing problem, and the whole investigation goes to trace lengths and terminations — which are real concerns from Chapter 9.4 and are simply not this. A mode error is a logical fault that presents as a timing symptom, and that mismatch between appearance and cause is what makes it expensive.

10. Common Misconceptions

11. Reason It Through

Work this before reading the answer.

A datasheet's timing diagram shows SCLK idling high. The text says: "Data on SDI is latched on the falling edge of SCLK. Data on SDO is updated on the rising edge of SCLK."

What mode is this, and which of the two sentences did the work?

Start with CPOL. SCLK idles high, so CPOL = 1. That is observation one and it is settled by the picture alone.

Now identify which sentence describes the master's view. SDI is the device's input — which is MOSI, the line the master drives. SDO is the device's output — MISO. The master's data is latched by the device on the falling edge of SCLK.

Translate the sampling edge into CPHA. With CPOL=1 the clock idles high, so the leading edge — the first edge of the frame, away from idle — is a falling edge. The device latches on falling edges, which are the leading ones, which are edges 1, 3, 5. Sampling on odd edges means CPHA = 0.

So the mode is 2 (CPOL=1, CPHA=0).

Which sentence did the work? The first one — the SDI sentence, describing the input. The SDO sentence describes the device's output timing and is the trap: it says "updated on the rising edge", and a reader who converts that into CPHA gets the opposite answer.

And why it is not a contradiction. The device samples its input on falling edges and updates its output on rising edges. Those are opposite edges, and they must be: that is exactly the full-duplex structure, where each side launches on one edge and samples on the other so that data is stable when it is looked at. A device that both sampled and launched on the same edge would have no setup time at all.

The general lesson. When a datasheet gives you two edge statements, one of them is about the master's data and one is about the device's. Identify which pin is the device's input before converting anything into CPHA — and if the pin names are SDI and SDO rather than MOSI and MISO, that naming is itself the clue, because they are named from the device's point of view.

12. Understanding Check

13. Summary

Most datasheets do not state CPOL and CPHA, and when they do the statement may describe the device's output rather than its input — opposite edges, and a consistently wrong answer. The picture is more reliable than the prose.

Observation one: the level SCLK rests at while CS is inactive is CPOL. Look outside the frame, not at its start.

Observation two: the parity of the edges MOSI changes on is CPHA. Changing at CS assertion and on even edges is 0; changing on odd edges is 1. The quickest tell is the first bit — valid before any edge means CPHA=0.

Read the transitions, not the sampling arrows, because every diagram must draw the transitions while the annotations vary by vendor.

Trying all four modes cannot work: a wrong mode and a wrong command look identical, modes 0 and 3 usually both appear to work, and a CPHA error passes at a low rate and fails at speed.

In hardware the same two observations are a small oversampling observer — CPOL from one sample, CPHA from the parity of MOSI transitions — with the subtlety being the attribution window, since a transition may arrive on its edge or a clock-to-out later and both must land on the same parity.

And it must be able to say no verdict: an all-ones payload carries no phase information, CPOL remains recoverable, and a guess that is confidently wrong half the time is worse than silence.

14. What Comes Next

The mode is one of the two numbers pass two of the route was after. The other is the rate — and behind it a table of setup, hold and valid times whose conditions matter as much as their values.

Chapter 10.3 — Extracting Setup, Hold, and Maximum SCLK turns that table into a constraint set you can actually close: which numbers belong to which direction, why a maximum frequency is meaningless without its load and corner, how to combine the device's numbers with the board's into the budget of Chapter 9.4 — and the monitor that measures the real link against the datasheet, in all three HDLs.

Continue learning