Skip to content
VLSI Mentor

SPI · Module 13

CPOL/CPHA-Aware Edge Control

All four SPI modes from one datapath: why the launch and capture assignment depends on CPHA alone, why the two phases are not symmetric, and why CPHA=0 needs both a preload and a suppressed final launch.

Chapter 13.4 produced two edge strobes and refused to say what they were for. This chapter says. It is the whole of the mode logic, and it is one of the shortest blocks in the master.

The mode table has four rows. How many datapaths does a master need?

One. The four-row table invites four cases, and a design that writes them gets twice the logic, four times the verification, and the classic bug where modes 0 and 3 are tested and modes 1 and 2 are quietly broken.

1. CPOL Does Not Appear Here

The mode table is usually drawn like this, and the natural reading is that four things need handling:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   mode  CPOL  CPHA   idle    launch on      capture on
   0     0     0      low     falling        rising
   1     0     1      low     rising         falling
   2     1     0      high    rising         falling
   3     1     1      high    falling        rising

Read the last two columns and they look like four distinct behaviours. Now read them again in the divider's vocabulary from Chapter 13.4, where leading means "away from idle" and trailing means "back to idle":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   mode  CPHA   launch on      capture on
   0     0      trailing       leading
   1     1      leading        trailing
   2     0      trailing       leading
   3     1      leading        trailing

Four rows collapse to two. CPOL chose the idle level, and the divider already consumed it — which is exactly what the leading/trailing naming was for. The assignment of launch and capture depends on CPHA alone:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CPHA = 0   capture on the LEADING edge, launch on the TRAILING edge
   CPHA = 1   launch  on the LEADING edge, capture on the TRAILING edge

A design that cases on {cpol, cpha} is not wrong so much as it is twice the logic for no function, and it makes every downstream verification obligation four-way instead of two-way.

2. The Two Phases Are Not Symmetric

Collapsing four cases to two is the easy half. The hard half is that the two remaining cases are not mirror images, and treating them as though they were is the most common bug in an SPI master.

CPHA=1 is balanced. Each SCLK period carries one launch then one capture. An N-bit frame has N leading edges and N trailing edges, and needs nothing special at either end:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CPHA=1, 4 bits:   L C L C L C L C

CPHA=0 is not. The first capture happens on the first leading edge, so the first bit must already be on MOSI before any clock has moved. There is no earlier edge to launch it on, so the launch has to come from somewhere else — from the transaction's start:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CPHA=0, 4 bits:   P C L C L C L C  X
                     ^                ^
                     preload          suppressed

And at the far end there is a surplus edge. After the fourth capture on the fourth leading edge, the fourth trailing edge arrives — and there is no fifth bit to launch. Obeying it would push an extra bit onto MOSI after the last capture, which a slave sampling MOSI on a late edge can see. So the final launch is suppressed.

Count the drives in each row: four bits, four drives, both phases. They just do not arrive the same way, and the difference lives entirely in this block.

3. Why Both Ends Need Handling, Not Just One

It is tempting to fix only the preload. It is the visible half — without it the first captured bit is whatever MOSI happened to be holding, which shows up immediately as a wrong first bit in every CPHA=0 transfer.

The suppression is invisible in most testbenches, and that is why it matters. Its failure mode is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   MOSI changes AFTER the last capture edge, while CS is still asserted

A loopback test does not see it, because the master is not capturing any more. An ideal slave model does not see it, because it stops sampling too. What sees it is a real device whose internal sampling is a little later than the nominal edge — and then only sometimes, depending on temperature and the length of the wire.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   without suppression, CPHA=0, 4 bits:
     P C L C L C L C L        <- a fifth drive, after the frame
                     ^
                     MOSI moves here; a slave sampling late reads bit 4
                     instead of bit 3 for the final bit of the frame

The symptom is a final bit that is occasionally wrong, on some boards, at some temperatures, and correct in every simulation. It is worth the three gates.

4. The Block

A four-state view of one frame's progress through the mode logic. From idle, a start request with CPHA equal to zero produces a preload and enters the running state; with CPHA equal to one it enters running directly. In running, each capture edge advances the bit count and each launch edge advances the drive count. When the drive count reaches the frame length, further launch edges are suppressed. When the capture count reaches the frame length the frame is complete and the machine returns to idle.IDLEPRELOADRUNNINGSUPPRESSstart, CPHA=0start, CPHA=0bit 0 already on MOSIbit 0 already on MOSIstart, CPHA=1start, CPHA=1launch, then capturelaunch, then capturedrives = lendrives = lenlaunch edges ignoredlaunch edges ignoredcaptures = lencaptures =lencaptures = lencaptures =len
Figure 1 — the mode logic. CPHA selects which of the divider's two strobes is a capture and which is a launch; CPOL is absent, having been consumed by the divider. Two counters guard the ends: captures stop at the frame length, and drives stop at the frame length, which is what suppresses CPHA=0's surplus trailing edge.

5. The Two Phases Side By Side

Twenty cycles across five rows. An SCLK row shows four clock periods. A CPHA=0 event row marks a preload before the first edge, then alternating capture and launch events with no launch after the final capture. A CPHA=1 event row marks alternating launch and capture events beginning with a launch on the first leading edge. Two MOSI rows show the bit each phase presents.CPHA=0: MOSI valid before any edgeCPHA=0: MOSI valid beforeany edgeCPHA=1: first launch is hereCPHA=1: first launch ishereCPHA=0: this edge drives nothingCPHA=0: this edge drivesnothingsclkpha0 evPPCCLLCCLLCCLLCCXXXXpha0 mosib0b0b0b0b1b1b1b1b2b2b2b2b3b3b3b3b3b3b3b3pha1 ev00LLCCLLCCLLCCLLCCCCpha1 mosi----b0b0b0b0b1b1b1b1b2b2b2b2b3b3b3b3b3b3t0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15t16t17t18t19
Figure 2 — four bits in each phase, CPOL=0, divisor 4. In CPHA=0 the first bit is on MOSI before the clock moves and the final trailing edge drives nothing; in CPHA=1 every launch is on a leading edge and the pattern is perfectly paired. Both move four bits, and the two capture rows show why the same wire carries them at different moments.

Note the pha0 mosi row at the far right: it holds b3 through the final trailing edge and beyond. That is the suppression working. Without it the row would move to b4 at cycle 16 — after the last capture, while chip select is still asserted.

6. Building the Mode Logic — Three HDLs

The circuit

Four continuous assignments and two counters. The counters are what make the ends work:

caps counts captures, and it is the frame's position. Not launches — because the captured bits are what the transfer exists to collect, and because in CPHA=0 the launches do not run to len.

drives counts bits put on MOSI, preload included. It starts at one for CPHA=0 and zero for CPHA=1, and the guard drives < len is what suppresses the surplus edge. In CPHA=1 it bites only after the frame's final leading edge, where nothing is listening; in CPHA=0 it bites one edge earlier, which is the case it exists for.

frame_done is the block's completion signal and it has two subtleties that are both integration bugs from later chapters, reported here because this is where they were fixed:

It is not asserted on the frame's own start cycle. caps is cleared by the same clock edge that samples start_stb, so during that cycle it still holds the previous frame's total. A sequencer that restarts the engine and watches frame_done in the same cycle — which is exactly what Chapter 13.9's streaming controller does — would see the old frame's completion and end the new frame before its first edge.

It is asserted on the final capture, not the cycle after. Waiting for caps to reach len reports the frame one cycle late, and one cycle is enough: in CPHA=1 the final capture lands two cycles before the next leading edge is due, so the extra cycle lets the divider start that edge before the enable drops, and the enable dropping then drags SCLK back. The result is a runt ninth pulse, one system clock wide, on every frame.

That last one deserves emphasis because of how invisible it is. caps_full blocks any ninth capture, so the received word is correct and every internal count agrees. Nothing inside the master is wrong. A slave that recovers bit boundaries from the pin counts the runt pulse and is one bit out from the second byte onward — and only a pin-level model finds it, which is the strongest argument in this module for owning one.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_edges.sv — two muxes, two counters, and both ends of the asymmetry
// spi_mode_edges.sv
//
// Chapter 13.5 -- CPOL and CPHA in RTL, without four copies of the datapath.
//
// Chapter 13.4's divider emits two strobes and deliberately refuses to say
// what they are for:
//
//   edge_a_stb   the LEADING edge   (SCLK departs from its idle level)
//   edge_b_stb   the TRAILING edge  (SCLK returns to its idle level)
//
// This block is the whole of the mode logic. It decides which of the two
// launches a bit onto MOSI and which captures a bit from MISO. Everything
// downstream -- the shift registers of 13.6, the chip-select logic of 13.7 --
// sees only `launch_stb` and `capture_stb` and never learns what mode it is
// running in.
//
// THE FIRST CLAIM: CPOL DOES NOT APPEAR HERE.
//
// The mode table is usually drawn as four rows, and the natural reading is
// that four things need handling. They do not. CPOL chooses the idle level,
// and the divider already consumed it -- "leading" means "away from idle"
// whichever level idle is. Inverting CPOL inverts the pin and moves nothing
// else. So the assignment of launch and capture depends on CPHA ALONE:
//
//   CPHA = 0   capture on the LEADING edge, launch on the TRAILING edge
//   CPHA = 1   launch  on the LEADING edge, capture on the TRAILING edge
//
// A design that cases on {cpol, cpha} is not wrong so much as it is twice the
// logic and four times the verification, and it invites the classic bug where
// modes 0 and 3 are tested and modes 1 and 2 are quietly broken.
//
// THE SECOND CLAIM: THE TWO PHASES ARE NOT SYMMETRIC.
//
// CPHA = 1 is balanced. Each SCLK period carries one launch then one capture,
// so an N-bit frame has N leading edges and N trailing edges and needs
// nothing special at either end.
//
// CPHA = 0 is not. The first capture happens on the FIRST leading edge, so
// the first bit has to already be on MOSI before any clock has moved --
// hence `preload_stb`, fired at the start of the transfer. And at the other
// end, the final trailing edge has no bit left to launch: obeying it would
// push an extra bit out and change MOSI after the last capture, which a slave
// reading MOSI on a late edge can see. So the final launch is SUPPRESSED.
//
//     CPHA=0, 4 bits:  preload, cap, launch, cap, launch, cap, launch, cap, X
//     CPHA=1, 4 bits:  launch, cap, launch, cap, launch, cap, launch, cap
//
// Four bits drive MOSI four times in both phases. They just do not arrive the
// same way, and the difference lives entirely in this file.

module spi_mode_edges #(
    parameter int LEN_W = 6            // width of the bit-count field
) (
    input  wire               clk,
    input  wire               rst_n,

    input  wire               cpha,        // CPOL is deliberately absent
    input  wire [LEN_W-1:0]   len,         // bits in this frame, >= 1

    input  wire               active,      // a frame is shifting
    input  wire               start_stb,   // one cycle, before the first edge
    input  wire               edge_a_stb,  // leading  edge, from 13.4
    input  wire               edge_b_stb,  // trailing edge, from 13.4

    output wire               preload_stb, // drive bit 0 before the first edge
    output wire               launch_stb,  // put the next bit on MOSI
    output wire               capture_stb, // sample MISO
    output wire [LEN_W-1:0]   bit_idx,     // captures completed so far
    output wire               frame_done   // all `len` bits captured
);

    // Captures completed. This is the frame's position -- not launches,
    // because it is the captured bits the transfer exists to collect and
    // because in CPHA=0 the launches do not run to `len`.
    reg [LEN_W-1:0] caps;

    // Bits driven onto MOSI, preload included. The guard below uses it so the
    // block never drives more bits than the frame holds, whichever phase.
    reg [LEN_W-1:0] drives;

    wire caps_full   = (caps   >= len);
    wire drives_full = (drives >= len);

    // The entire mode decision.
    wire cap_edge    = cpha ? edge_b_stb : edge_a_stb;
    wire launch_edge = cpha ? edge_a_stb : edge_b_stb;

    assign preload_stb = start_stb & ~cpha;
    assign capture_stb = active & cap_edge & ~caps_full;

    // The suppression. In CPHA=1 `drives_full` bites only after the frame's
    // last leading edge, which `active` has usually already ended. In CPHA=0
    // it bites one edge earlier -- on the trailing edge that follows the last
    // capture -- and that is the case it exists for.
    assign launch_stb  = active & launch_edge & ~drives_full;

    assign bit_idx = caps;

    // Asserted ON the final capture, not the cycle after it. `caps` only
    // reaches `len` at the next clock edge, so a design that waits for
    // `caps_full` alone reports the frame one cycle late -- and one cycle is
    // enough. With CPHA=1 the final capture lands two cycles before the next
    // leading edge is due, so the extra cycle lets the divider START that edge
    // before the enable drops, and the enable dropping then drags SCLK back:
    // a RUNT NINTH PULSE, one system clock wide, on every frame.
    //
    // It is invisible from inside the master -- `caps_full` blocks any ninth
    // capture, so the received word is right and every internal count agrees
    // -- and a slave that recovers bit boundaries from the pin counts it and
    // is one bit out from the second byte onward. Only a pin-level model finds
    // it (Chapter 13.11), and it is the single best argument in this module for
    // owning one.
    wire last_cap = capture_stb & (caps == (len - 1'b1));

    // NOT ASSERTED ON THE FRAME'S OWN START CYCLE. `caps` is cleared by the
    // same clock edge that samples `start_stb`, so during that cycle it still
    // holds the PREVIOUS frame's total and `caps_full` is still true. A
    // sequencer that restarts the shift engine and watches `frame_done` in the
    // same cycle -- which is exactly what the streaming controller of Chapter
    // 13.9 does -- would see the old frame's completion and end the new frame
    // before its first edge. One term removes the whole class of stale-level
    // bug, and it costs nothing.
    assign frame_done = active & ~start_stb & (caps_full | last_cap);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            caps   <= {LEN_W{1'b0}};
            drives <= {LEN_W{1'b0}};
        end else if (start_stb) begin
            // The preload counts as a drive, so a CPHA=0 frame begins one bit
            // ahead and a CPHA=1 frame begins at zero.
            caps   <= {LEN_W{1'b0}};
            drives <= cpha ? {LEN_W{1'b0}}
                           : {{(LEN_W-1){1'b0}}, 1'b1};
        end else if (active) begin
            if (capture_stb) caps   <= caps   + 1'b1;
            if (launch_stb)  drives <= drives + 1'b1;
        end
    end

// Simulation-only checks: `SPI_CHECKS` is defined by the testbench and
// left undefined for synthesis.
`ifdef SPI_CHECKS
    // Two things that must never happen, stated where a reviewer sees them.
    always_ff @(posedge clk) if (rst_n) begin
        if (launch_stb && capture_stb)
            $fatal(1, "launch and capture on the same cycle");
        // Guarded by `active` and by the start cycle: between frames, and
        // on the very cycle `active` rises, `caps` still holds the previous
        // frame's total while `len` already carries the next frame's.
        // Comparing those two is meaningless, and an unguarded version of
        // this assertion fires on the first short frame that follows a long
        // one -- which looks exactly like a counter bug.
        if (active && !start_stb && caps > len)
            $fatal(1, "captured more bits than the frame holds");
    end
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_edges_tb.sv — the event order recorded under both polarities
// spi_mode_edges_tb.sv
//
// This testbench instantiates the REAL divider from Chapter 13.4 rather than
// a hand-written edge generator, because the claim under test is about how
// the two blocks compose: that the divider absorbs CPOL entirely, and that
// what reaches the datapath depends on CPHA alone.
//
// The central experiment records, for every transfer, the ORDER of events
// (preload / launch / capture) and the SCLK LEVEL at each one. Running the
// same frame under both polarities must give an identical order and an
// exactly inverted set of levels. That is what "CPOL only inverts the pin"
// means, said in a form a simulator can disagree with.

`timescale 1ns/1ps

module spi_mode_edges_tb;

    localparam int LEN_W = 6;
    localparam int DIV_W = 8;

    localparam int EV_PRELOAD = 1;
    localparam int EV_LAUNCH  = 2;
    localparam int EV_CAPTURE = 3;

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    always #5 clk = ~clk;

    // --- divider (Chapter 13.4) -------------------------------------------
    logic             en   = 1'b0;
    logic [DIV_W-1:0] div  = 8'd4;
    logic             cpol = 1'b0;

    wire              sclk;
    wire              edge_a_stb, edge_b_stb, bit_done;
    wire [DIV_W-1:0]  half_a, half_b;
    wire              div_err;

    spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
        .clk(clk), .rst_n(rst_n),
        .en(en), .div(div), .cpol(cpol),
        .sclk(sclk), .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .bit_done(bit_done),
        .half_a(half_a), .half_b(half_b), .div_err(div_err)
    );

    // --- mode logic (Chapter 13.5) ----------------------------------------
    logic             cpha      = 1'b0;
    logic [LEN_W-1:0] len       = 6'd8;
    logic             active    = 1'b0;
    logic             start_stb = 1'b0;

    wire              preload_stb, launch_stb, capture_stb;
    wire [LEN_W-1:0]  bit_idx;
    wire              frame_done;

    spi_mode_edges #(.LEN_W(LEN_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .cpha(cpha), .len(len),
        .active(active), .start_stb(start_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb), .bit_idx(bit_idx), .frame_done(frame_done)
    );

    // --- recorder ---------------------------------------------------------
    integer ev_code  [0:255];
    integer ev_level [0:255];
    integer ev_n;
    integer n_pre, n_launch, n_cap;
    integer coincident;        // two strobes on one cycle -- must stay zero
    integer early_capture;     // a capture with no bit driven for it
    integer recording;

    always_ff @(posedge clk) begin
        if (recording) begin
            if ((preload_stb + launch_stb + capture_stb) > 1)
                coincident <= coincident + 1;

            if (preload_stb || launch_stb || capture_stb) begin
                if (ev_n < 256) begin
                    ev_code[ev_n]  <= preload_stb ? EV_PRELOAD :
                                      launch_stb  ? EV_LAUNCH  : EV_CAPTURE;
                    ev_level[ev_n] <= sclk;
                end
                ev_n <= ev_n + 1;
            end

            if (preload_stb) n_pre    <= n_pre + 1;
            if (launch_stb)  n_launch <= n_launch + 1;
            if (capture_stb) begin
                n_cap <= n_cap + 1;
                // The bit being captured must already have been driven: the
                // count of drives so far has to exceed the count of captures
                // so far. This is the property that makes CPHA=0's preload
                // necessary rather than decorative.
                if ((n_pre + n_launch) < (n_cap + 1))
                    early_capture <= early_capture + 1;
            end
        end
    end

    // --- reference copy, for the polarity comparison ----------------------
    integer ref_code  [0:255];
    integer ref_level [0:255];
    integer ref_n;

    integer errors = 0;
    integer i;

    task automatic clear_record;
        begin
            ev_n = 0; n_pre = 0; n_launch = 0; n_cap = 0;
            coincident = 0; early_capture = 0;
            for (i = 0; i < 256; i = i + 1) begin
                ev_code[i]  = 0;
                ev_level[i] = 0;
            end
        end
    endtask

    // One complete frame. CS-assert (start_stb) comes first, the clock starts
    // a few cycles later, and `active` is deliberately held for one extra
    // SCLK period past the final capture so the edge that must NOT launch
    // actually occurs and can be seen to do nothing.
    task automatic run_frame(input integer dv, input bit pol, input bit pha,
                             input integer nbits);
        integer guard;
        begin
            @(negedge clk);
            en = 1'b0; active = 1'b0; start_stb = 1'b0;
            div = dv[DIV_W-1:0]; cpol = pol; cpha = pha;
            len = nbits[LEN_W-1:0];
            repeat (4) @(negedge clk);

            recording = 1;
            clear_record();

            @(negedge clk);
            start_stb = 1'b1; active = 1'b1;     // CS asserts; MOSI preloads
            @(negedge clk);
            start_stb = 1'b0;
            repeat (3) @(negedge clk);           // the lead time of 13.1

            en = 1'b1;                           // SCLK begins
            guard = dv * (nbits + 4) + 40;
            while (!frame_done && guard > 0) begin
                @(negedge clk);
                guard = guard - 1;
            end
            if (guard == 0) begin
                $display("  FAIL: frame never completed (div=%0d len=%0d)",
                         dv, nbits);
                errors = errors + 1;
            end
            repeat (dv + 2) @(negedge clk);      // the edge that must not launch
            en = 1'b0; active = 1'b0;
            repeat (3) @(negedge clk);
            recording = 0;
        end
    endtask

    task automatic save_as_reference;
        begin
            ref_n = ev_n;
            for (i = 0; i < 256; i = i + 1) begin
                ref_code[i]  = ev_code[i];
                ref_level[i] = ev_level[i];
            end
        end
    endtask

    // --- expected event sequence, derived from the rules, not the design ---
    // CPHA=0:  preload, then nbits captures with a launch between each pair.
    // CPHA=1:  nbits (launch, capture) pairs.
    function automatic integer expect_code(input bit pha, input integer k,
                                           input integer nbits);
        begin
            if (!pha) begin
                if (k == 0)            expect_code = EV_PRELOAD;
                else if (k[0] == 1'b1) expect_code = EV_CAPTURE;
                else                   expect_code = EV_LAUNCH;
            end else begin
                if (k[0] == 1'b0)      expect_code = EV_LAUNCH;
                else                   expect_code = EV_CAPTURE;
            end
        end
    endfunction

    function automatic integer expect_n(input bit pha, input integer nbits);
        begin
            // CPHA=0: 1 preload + nbits captures + (nbits-1) launches
            // CPHA=1: nbits launches  + nbits captures
            // Both come to 2*nbits, which is the arithmetic form of "the two
            // phases move the same number of bits by different means".
            expect_n = 2 * nbits;
        end
    endfunction

    task automatic check_sequence(input bit pha, input integer nbits,
                                  input string tag);
        integer k, bad;
        begin
            bad = 0;
            if (ev_n != expect_n(pha, nbits)) begin
                $display("  FAIL: %0s saw %0d events, expected %0d",
                         tag, ev_n, expect_n(pha, nbits));
                errors = errors + 1;
            end else begin
                for (k = 0; k < ev_n; k = k + 1)
                    if (ev_code[k] != expect_code(pha, k, nbits)) bad = bad + 1;
                if (bad != 0) begin
                    $display("  FAIL: %0s had %0d events out of order", tag, bad);
                    errors = errors + 1;
                end
            end
            if (n_cap != nbits) begin
                $display("  FAIL: %0s captured %0d bits, expected %0d",
                         tag, n_cap, nbits);
                errors = errors + 1;
            end
            if ((n_pre + n_launch) != nbits) begin
                $display("  FAIL: %0s drove MOSI %0d times, expected %0d",
                         tag, n_pre + n_launch, nbits);
                errors = errors + 1;
            end
            if (n_pre != (pha ? 0 : 1)) begin
                $display("  FAIL: %0s produced %0d preloads", tag, n_pre);
                errors = errors + 1;
            end
            if (coincident != 0) begin
                $display("  FAIL: %0s had %0d cycles with two strobes at once",
                         tag, coincident);
                errors = errors + 1;
            end
            if (early_capture != 0) begin
                $display("  FAIL: %0s captured %0d bits that were never driven",
                         tag, early_capture);
                errors = errors + 1;
            end
        end
    endtask

    integer dvs [0:3];
    integer lens[0:4];
    integer d, l, pha_i, k, diff_order, same_level;

    initial begin
        recording = 0;
        clear_record();
        dvs[0] = 2; dvs[1] = 3; dvs[2] = 4; dvs[3] = 6;
        lens[0] = 1; lens[1] = 2; lens[2] = 4; lens[3] = 8; lens[4] = 16;

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

        // 1. THE TWO PHASES, SHOWN SIDE BY SIDE.
        run_frame(4, 1'b0, 1'b0, 4);
        check_sequence(1'b0, 4, "mode 0, 4 bits");
        $write("  CPHA=0, 4 bits: ");
        for (k = 0; k < ev_n; k = k + 1)
            $write("%0s", ev_code[k] == EV_PRELOAD ? "P " :
                          ev_code[k] == EV_LAUNCH  ? "L " : "C ");
        $display("-- preload first, no launch after the last capture");

        run_frame(4, 1'b0, 1'b1, 4);
        check_sequence(1'b1, 4, "mode 1, 4 bits");
        $write("  CPHA=1, 4 bits: ");
        for (k = 0; k < ev_n; k = k + 1)
            $write("%0s", ev_code[k] == EV_PRELOAD ? "P " :
                          ev_code[k] == EV_LAUNCH  ? "L " : "C ");
        $display("-- launch first, perfectly paired");

        // 2. CPOL ONLY INVERTS THE PIN. Same frame, both polarities: the
        //    event order must be identical and every level exactly inverted.
        for (pha_i = 0; pha_i <= 1; pha_i = pha_i + 1) begin
            run_frame(4, 1'b0, pha_i[0], 8);
            save_as_reference();
            run_frame(4, 1'b1, pha_i[0], 8);

            diff_order = 0;
            same_level = 0;
            if (ev_n != ref_n) begin
                $display("  FAIL: CPHA=%0d gave %0d events at CPOL=1 and %0d at CPOL=0",
                         pha_i, ev_n, ref_n);
                errors = errors + 1;
            end else begin
                for (k = 0; k < ev_n; k = k + 1) begin
                    if (ev_code[k]  != ref_code[k])  diff_order = diff_order + 1;
                    if (ev_level[k] == ref_level[k]) same_level = same_level + 1;
                end
            end
            if (diff_order != 0) begin
                $display("  FAIL: CPHA=%0d changed its event order with CPOL",
                         pha_i);
                errors = errors + 1;
            end
            if (same_level != 0) begin
                $display("  FAIL: CPHA=%0d had %0d events at the same SCLK level under both polarities",
                         pha_i, same_level);
                errors = errors + 1;
            end
            $display("  CPHA=%0d: %0d events, identical order under both polarities, all %0d SCLK levels inverted",
                     pha_i, ev_n, ev_n);
        end

        // 3. A SINGLE-BIT FRAME is the corner where CPHA=0 launches nothing
        //    at all: the preload is the only drive, and the one trailing edge
        //    is suppressed.
        run_frame(4, 1'b0, 1'b0, 1);
        check_sequence(1'b0, 1, "mode 0, 1 bit");
        if (n_launch != 0) begin
            $display("  FAIL: a one-bit CPHA=0 frame launched %0d times",
                     n_launch);
            errors = errors + 1;
        end
        run_frame(4, 1'b0, 1'b1, 1);
        check_sequence(1'b1, 1, "mode 1, 1 bit");
        $display("  1 bit: CPHA=0 drives once via the preload and launches never; CPHA=1 launches once");

        // 4. THE SWEEP. Every phase, polarity, divisor and length.
        for (pha_i = 0; pha_i <= 1; pha_i = pha_i + 1)
            for (d = 0; d < 4; d = d + 1)
                for (l = 0; l < 5; l = l + 1) begin
                    run_frame(dvs[d], 1'b0, pha_i[0], lens[l]);
                    check_sequence(pha_i[0], lens[l], "sweep cpol=0");
                    run_frame(dvs[d], 1'b1, pha_i[0], lens[l]);
                    check_sequence(pha_i[0], lens[l], "sweep cpol=1");
                end
        $display("  80 frames swept: 2 phases x 2 polarities x 4 divisors x 5 lengths");

        if (errors == 0)
            $display("PASS: the launch and capture assignment follows CPHA alone and never CPOL -- the same frame under both polarities produces an identical event order with every SCLK level inverted -- a CPHA=0 frame preloads MOSI before the first edge and suppresses the launch on the final trailing edge while a CPHA=1 frame does neither, both driving exactly as many bits as they capture, no bit is ever captured before it was driven, launch and capture never fall on the same cycle, and all of it holds across 80 frames spanning both phases, both polarities, four divisors and lengths from 1 to 16 bits");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_edges.v — the same mode logic in Verilog-2001
// spi_mode_edges.v
//
// Chapter 13.5 -- CPOL and CPHA in RTL, without four copies of the datapath.
//
// Chapter 13.4's divider emits two strobes and deliberately refuses to say
// what they are for:
//
//   edge_a_stb   the LEADING edge   (SCLK departs from its idle level)
//   edge_b_stb   the TRAILING edge  (SCLK returns to its idle level)
//
// This block is the whole of the mode logic. It decides which of the two
// launches a bit onto MOSI and which captures a bit from MISO. Everything
// downstream -- the shift registers of 13.6, the chip-select logic of 13.7 --
// sees only `launch_stb` and `capture_stb` and never learns what mode it is
// running in.
//
// THE FIRST CLAIM: CPOL DOES NOT APPEAR HERE.
//
// The mode table is usually drawn as four rows, and the natural reading is
// that four things need handling. They do not. CPOL chooses the idle level,
// and the divider already consumed it -- "leading" means "away from idle"
// whichever level idle is. Inverting CPOL inverts the pin and moves nothing
// else. So the assignment of launch and capture depends on CPHA ALONE:
//
//   CPHA = 0   capture on the LEADING edge, launch on the TRAILING edge
//   CPHA = 1   launch  on the LEADING edge, capture on the TRAILING edge
//
// A design that cases on {cpol, cpha} is not wrong so much as it is twice the
// logic and four times the verification, and it invites the classic bug where
// modes 0 and 3 are tested and modes 1 and 2 are quietly broken.
//
// THE SECOND CLAIM: THE TWO PHASES ARE NOT SYMMETRIC.
//
// CPHA = 1 is balanced. Each SCLK period carries one launch then one capture,
// so an N-bit frame has N leading edges and N trailing edges and needs
// nothing special at either end.
//
// CPHA = 0 is not. The first capture happens on the FIRST leading edge, so
// the first bit has to already be on MOSI before any clock has moved --
// hence `preload_stb`, fired at the start of the transfer. And at the other
// end, the final trailing edge has no bit left to launch: obeying it would
// push an extra bit out and change MOSI after the last capture, which a slave
// reading MOSI on a late edge can see. So the final launch is SUPPRESSED.
//
//     CPHA=0, 4 bits:  preload, cap, launch, cap, launch, cap, launch, cap, X
//     CPHA=1, 4 bits:  launch, cap, launch, cap, launch, cap, launch, cap
//
// Four bits drive MOSI four times in both phases. They just do not arrive the
// same way, and the difference lives entirely in this file.

module spi_mode_edges #(
    parameter LEN_W = 6            // width of the bit-count field
) (
    input  wire               clk,
    input  wire               rst_n,

    input  wire               cpha,        // CPOL is deliberately absent
    input  wire [LEN_W-1:0]   len,         // bits in this frame, >= 1

    input  wire               active,      // a frame is shifting
    input  wire               start_stb,   // one cycle, before the first edge
    input  wire               edge_a_stb,  // leading  edge, from 13.4
    input  wire               edge_b_stb,  // trailing edge, from 13.4

    output wire               preload_stb, // drive bit 0 before the first edge
    output wire               launch_stb,  // put the next bit on MOSI
    output wire               capture_stb, // sample MISO
    output wire [LEN_W-1:0]   bit_idx,     // captures completed so far
    output wire               frame_done   // all `len` bits captured
);

    // Captures completed. This is the frame's position -- not launches,
    // because it is the captured bits the transfer exists to collect and
    // because in CPHA=0 the launches do not run to `len`.
    reg [LEN_W-1:0] caps;

    // Bits driven onto MOSI, preload included. The guard below uses it so the
    // block never drives more bits than the frame holds, whichever phase.
    reg [LEN_W-1:0] drives;

    wire caps_full   = (caps   >= len);
    wire drives_full = (drives >= len);

    // The entire mode decision.
    wire cap_edge    = cpha ? edge_b_stb : edge_a_stb;
    wire launch_edge = cpha ? edge_a_stb : edge_b_stb;

    assign preload_stb = start_stb & ~cpha;
    assign capture_stb = active & cap_edge & ~caps_full;

    // The suppression. In CPHA=1 `drives_full` bites only after the frame's
    // last leading edge, which `active` has usually already ended. In CPHA=0
    // it bites one edge earlier -- on the trailing edge that follows the last
    // capture -- and that is the case it exists for.
    assign launch_stb  = active & launch_edge & ~drives_full;

    assign bit_idx = caps;

    // Asserted ON the final capture, not the cycle after it. `caps` only
    // reaches `len` at the next clock edge, so a design that waits for
    // `caps_full` alone reports the frame one cycle late -- and one cycle is
    // enough. With CPHA=1 the final capture lands two cycles before the next
    // leading edge is due, so the extra cycle lets the divider START that edge
    // before the enable drops, and the enable dropping then drags SCLK back:
    // a RUNT NINTH PULSE, one system clock wide, on every frame.
    //
    // It is invisible from inside the master -- `caps_full` blocks any ninth
    // capture, so the received word is right and every internal count agrees
    // -- and a slave that recovers bit boundaries from the pin counts it and
    // is one bit out from the second byte onward. Only a pin-level model finds
    // it (Chapter 13.11), and it is the single best argument in this module for
    // owning one.
    wire last_cap = capture_stb & (caps == (len - 1'b1));

    // NOT ASSERTED ON THE FRAME'S OWN START CYCLE. `caps` is cleared by the
    // same clock edge that samples `start_stb`, so during that cycle it still
    // holds the PREVIOUS frame's total and `caps_full` is still true. A
    // sequencer that restarts the shift engine and watches `frame_done` in the
    // same cycle -- which is exactly what the streaming controller of Chapter
    // 13.9 does -- would see the old frame's completion and end the new frame
    // before its first edge. One term removes the whole class of stale-level
    // bug, and it costs nothing.
    assign frame_done = active & ~start_stb & (caps_full | last_cap);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            caps   <= {LEN_W{1'b0}};
            drives <= {LEN_W{1'b0}};
        end else if (start_stb) begin
            // The preload counts as a drive, so a CPHA=0 frame begins one bit
            // ahead and a CPHA=1 frame begins at zero.
            caps   <= {LEN_W{1'b0}};
            drives <= cpha ? {LEN_W{1'b0}}
                           : {{(LEN_W-1){1'b0}}, 1'b1};
        end else if (active) begin
            if (capture_stb) caps   <= caps   + 1'b1;
            if (launch_stb)  drives <= drives + 1'b1;
        end
    end

// Simulation-only checks: `SPI_CHECKS` is defined by the testbench and
// left undefined for synthesis.
`ifdef SPI_CHECKS
    // Two things that must never happen, stated where a reviewer sees them.
    always @(posedge clk) if (rst_n) begin
        if (launch_stb && capture_stb)
            $fatal(1, "launch and capture on the same cycle");
        // Guarded by `active` and by the start cycle: between frames, and
        // on the very cycle `active` rises, `caps` still holds the previous
        // frame's total while `len` already carries the next frame's.
        // Comparing those two is meaningless, and an unguarded version of
        // this assertion fires on the first short frame that follows a long
        // one -- which looks exactly like a counter bug.
        if (active && !start_stb && caps > len)
            $fatal(1, "captured more bits than the frame holds");
    end
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_edges_tb.v — the same 80-frame sweep in Verilog-2001
// spi_mode_edges_tb.v
//
// This testbench instantiates the REAL divider from Chapter 13.4 rather than
// a hand-written edge generator, because the claim under test is about how
// the two blocks compose: that the divider absorbs CPOL entirely, and that
// what reaches the datapath depends on CPHA alone.
//
// The central experiment records, for every transfer, the ORDER of events
// (preload / launch / capture) and the SCLK LEVEL at each one. Running the
// same frame under both polarities must give an identical order and an
// exactly inverted set of levels. That is what "CPOL only inverts the pin"
// means, said in a form a simulator can disagree with.

`timescale 1ns/1ps

module spi_mode_edges_tb;

    localparam LEN_W = 6;
    localparam DIV_W = 8;

    localparam EV_PRELOAD = 1;
    localparam EV_LAUNCH  = 2;
    localparam EV_CAPTURE = 3;

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

    // --- divider (Chapter 13.4) -------------------------------------------
    reg             en;
    reg [DIV_W-1:0] div;
    reg             cpol;

    wire              sclk;
    wire              edge_a_stb, edge_b_stb, bit_done;
    wire [DIV_W-1:0]  half_a, half_b;
    wire              div_err;

    spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
        .clk(clk), .rst_n(rst_n),
        .en(en), .div(div), .cpol(cpol),
        .sclk(sclk), .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .bit_done(bit_done),
        .half_a(half_a), .half_b(half_b), .div_err(div_err)
    );

    // --- mode logic (Chapter 13.5) ----------------------------------------
    reg             cpha;
    reg [LEN_W-1:0] len;
    reg             active;
    reg             start_stb;

    wire              preload_stb, launch_stb, capture_stb;
    wire [LEN_W-1:0]  bit_idx;
    wire              frame_done;

    spi_mode_edges #(.LEN_W(LEN_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .cpha(cpha), .len(len),
        .active(active), .start_stb(start_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb), .bit_idx(bit_idx), .frame_done(frame_done)
    );

    // --- recorder ---------------------------------------------------------
    integer ev_code  [0:255];
    integer ev_level [0:255];
    integer ev_n;
    integer n_pre, n_launch, n_cap;
    integer coincident;        // two strobes on one cycle -- must stay zero
    integer early_capture;     // a capture with no bit driven for it
    integer recording;

    always @(posedge clk) begin
        if (recording) begin
            if ((preload_stb + launch_stb + capture_stb) > 1)
                coincident <= coincident + 1;

            if (preload_stb || launch_stb || capture_stb) begin
                if (ev_n < 256) begin
                    ev_code[ev_n]  <= preload_stb ? EV_PRELOAD :
                                      launch_stb  ? EV_LAUNCH  : EV_CAPTURE;
                    ev_level[ev_n] <= sclk;
                end
                ev_n <= ev_n + 1;
            end

            if (preload_stb) n_pre    <= n_pre + 1;
            if (launch_stb)  n_launch <= n_launch + 1;
            if (capture_stb) begin
                n_cap <= n_cap + 1;
                // The bit being captured must already have been driven: the
                // count of drives so far has to exceed the count of captures
                // so far. This is the property that makes CPHA=0's preload
                // necessary rather than decorative.
                if ((n_pre + n_launch) < (n_cap + 1))
                    early_capture <= early_capture + 1;
            end
        end
    end

    // --- reference copy, for the polarity comparison ----------------------
    integer ref_code  [0:255];
    integer ref_level [0:255];
    integer ref_n;

    integer errors;
    integer i;

    task clear_record;
        begin
            ev_n = 0; n_pre = 0; n_launch = 0; n_cap = 0;
            coincident = 0; early_capture = 0;
            for (i = 0; i < 256; i = i + 1) begin
                ev_code[i]  = 0;
                ev_level[i] = 0;
            end
        end
    endtask

    // One complete frame. CS-assert (start_stb) comes first, the clock starts
    // a few cycles later, and `active` is deliberately held for one extra
    // SCLK period past the final capture so the edge that must NOT launch
    // actually occurs and can be seen to do nothing.
        task run_frame;
        input integer dv;
        input pol;
        input pha;
        input integer nbits;
        integer guard;
        begin
            @(negedge clk);
            en = 1'b0; active = 1'b0; start_stb = 1'b0;
            div = dv[DIV_W-1:0]; cpol = pol; cpha = pha;
            len = nbits[LEN_W-1:0];
            repeat (4) @(negedge clk);

            recording = 1;
            clear_record();

            @(negedge clk);
            start_stb = 1'b1; active = 1'b1;     // CS asserts; MOSI preloads
            @(negedge clk);
            start_stb = 1'b0;
            repeat (3) @(negedge clk);           // the lead time of 13.1

            en = 1'b1;                           // SCLK begins
            guard = dv * (nbits + 4) + 40;
            while (!frame_done && guard > 0) begin
                @(negedge clk);
                guard = guard - 1;
            end
            if (guard == 0) begin
                $display("  FAIL: frame never completed (div=%0d len=%0d)",
                         dv, nbits);
                errors = errors + 1;
            end
            repeat (dv + 2) @(negedge clk);      // the edge that must not launch
            en = 1'b0; active = 1'b0;
            repeat (3) @(negedge clk);
            recording = 0;
        end
    endtask

    task save_as_reference;
        begin
            ref_n = ev_n;
            for (i = 0; i < 256; i = i + 1) begin
                ref_code[i]  = ev_code[i];
                ref_level[i] = ev_level[i];
            end
        end
    endtask

    // --- expected event sequence, derived from the rules, not the design ---
    // CPHA=0:  preload, then nbits captures with a launch between each pair.
    // CPHA=1:  nbits (launch, capture) pairs.
        function integer expect_code;
        input pha;
        input integer k;
        input integer nbits;
        begin
            if (!pha) begin
                if (k == 0)            expect_code = EV_PRELOAD;
                else if (k[0] == 1'b1) expect_code = EV_CAPTURE;
                else                   expect_code = EV_LAUNCH;
            end else begin
                if (k[0] == 1'b0)      expect_code = EV_LAUNCH;
                else                   expect_code = EV_CAPTURE;
            end
        end
    endfunction

        function integer expect_n;
        input pha;
        input integer nbits;
        begin
            // CPHA=0: 1 preload + nbits captures + (nbits-1) launches
            // CPHA=1: nbits launches  + nbits captures
            // Both come to 2*nbits, which is the arithmetic form of "the two
            // phases move the same number of bits by different means".
            expect_n = 2 * nbits;
        end
    endfunction

        task check_sequence;
        input pha;
        input integer nbits;
        input [8*40:1] tag;
        integer k, bad;
        begin
            bad = 0;
            if (ev_n != expect_n(pha, nbits)) begin
                $display("  FAIL: %0s saw %0d events, expected %0d",
                         tag, ev_n, expect_n(pha, nbits));
                errors = errors + 1;
            end else begin
                for (k = 0; k < ev_n; k = k + 1)
                    if (ev_code[k] != expect_code(pha, k, nbits)) bad = bad + 1;
                if (bad != 0) begin
                    $display("  FAIL: %0s had %0d events out of order", tag, bad);
                    errors = errors + 1;
                end
            end
            if (n_cap != nbits) begin
                $display("  FAIL: %0s captured %0d bits, expected %0d",
                         tag, n_cap, nbits);
                errors = errors + 1;
            end
            if ((n_pre + n_launch) != nbits) begin
                $display("  FAIL: %0s drove MOSI %0d times, expected %0d",
                         tag, n_pre + n_launch, nbits);
                errors = errors + 1;
            end
            if (n_pre != (pha ? 0 : 1)) begin
                $display("  FAIL: %0s produced %0d preloads", tag, n_pre);
                errors = errors + 1;
            end
            if (coincident != 0) begin
                $display("  FAIL: %0s had %0d cycles with two strobes at once",
                         tag, coincident);
                errors = errors + 1;
            end
            if (early_capture != 0) begin
                $display("  FAIL: %0s captured %0d bits that were never driven",
                         tag, early_capture);
                errors = errors + 1;
            end
        end
    endtask

    integer dvs [0:3];
    integer lens[0:4];
    integer d, l, pha_i, k, diff_order, same_level;

    initial begin
        recording = 0;
        clear_record();
        dvs[0] = 2; dvs[1] = 3; dvs[2] = 4; dvs[3] = 6;
        lens[0] = 1; lens[1] = 2; lens[2] = 4; lens[3] = 8; lens[4] = 16;

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

        // 1. THE TWO PHASES, SHOWN SIDE BY SIDE.
        run_frame(4, 1'b0, 1'b0, 4);
        check_sequence(1'b0, 4, "mode 0, 4 bits");
        $write("  CPHA=0, 4 bits: ");
        for (k = 0; k < ev_n; k = k + 1)
            $write("%0s", ev_code[k] == EV_PRELOAD ? "P " :
                          ev_code[k] == EV_LAUNCH  ? "L " : "C ");
        $display("-- preload first, no launch after the last capture");

        run_frame(4, 1'b0, 1'b1, 4);
        check_sequence(1'b1, 4, "mode 1, 4 bits");
        $write("  CPHA=1, 4 bits: ");
        for (k = 0; k < ev_n; k = k + 1)
            $write("%0s", ev_code[k] == EV_PRELOAD ? "P " :
                          ev_code[k] == EV_LAUNCH  ? "L " : "C ");
        $display("-- launch first, perfectly paired");

        // 2. CPOL ONLY INVERTS THE PIN. Same frame, both polarities: the
        //    event order must be identical and every level exactly inverted.
        for (pha_i = 0; pha_i <= 1; pha_i = pha_i + 1) begin
            run_frame(4, 1'b0, pha_i[0], 8);
            save_as_reference();
            run_frame(4, 1'b1, pha_i[0], 8);

            diff_order = 0;
            same_level = 0;
            if (ev_n != ref_n) begin
                $display("  FAIL: CPHA=%0d gave %0d events at CPOL=1 and %0d at CPOL=0",
                         pha_i, ev_n, ref_n);
                errors = errors + 1;
            end else begin
                for (k = 0; k < ev_n; k = k + 1) begin
                    if (ev_code[k]  != ref_code[k])  diff_order = diff_order + 1;
                    if (ev_level[k] == ref_level[k]) same_level = same_level + 1;
                end
            end
            if (diff_order != 0) begin
                $display("  FAIL: CPHA=%0d changed its event order with CPOL",
                         pha_i);
                errors = errors + 1;
            end
            if (same_level != 0) begin
                $display("  FAIL: CPHA=%0d had %0d events at the same SCLK level under both polarities",
                         pha_i, same_level);
                errors = errors + 1;
            end
            $display("  CPHA=%0d: %0d events, identical order under both polarities, all %0d SCLK levels inverted",
                     pha_i, ev_n, ev_n);
        end

        // 3. A SINGLE-BIT FRAME is the corner where CPHA=0 launches nothing
        //    at all: the preload is the only drive, and the one trailing edge
        //    is suppressed.
        run_frame(4, 1'b0, 1'b0, 1);
        check_sequence(1'b0, 1, "mode 0, 1 bit");
        if (n_launch != 0) begin
            $display("  FAIL: a one-bit CPHA=0 frame launched %0d times",
                     n_launch);
            errors = errors + 1;
        end
        run_frame(4, 1'b0, 1'b1, 1);
        check_sequence(1'b1, 1, "mode 1, 1 bit");
        $display("  1 bit: CPHA=0 drives once via the preload and launches never; CPHA=1 launches once");

        // 4. THE SWEEP. Every phase, polarity, divisor and length.
        for (pha_i = 0; pha_i <= 1; pha_i = pha_i + 1)
            for (d = 0; d < 4; d = d + 1)
                for (l = 0; l < 5; l = l + 1) begin
                    run_frame(dvs[d], 1'b0, pha_i[0], lens[l]);
                    check_sequence(pha_i[0], lens[l], "sweep cpol=0");
                    run_frame(dvs[d], 1'b1, pha_i[0], lens[l]);
                    check_sequence(pha_i[0], lens[l], "sweep cpol=1");
                end
        $display("  80 frames swept: 2 phases x 2 polarities x 4 divisors x 5 lengths");

        if (errors == 0)
            $display("PASS: the launch and capture assignment follows CPHA alone and never CPOL -- the same frame under both polarities produces an identical event order with every SCLK level inverted -- a CPHA=0 frame preloads MOSI before the first edge and suppresses the launch on the final trailing edge while a CPHA=1 frame does neither, both driving exactly as many bits as they capture, no bit is ever captured before it was driven, launch and capture never fall on the same cycle, and all of it holds across 80 frames spanning both phases, both polarities, four divisors and lengths from 1 to 16 bits");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b0;
        en = 1'b0;
        div = 8'd4;
        cpol = 1'b0;
        cpha = 1'b0;
        len = 6'd8;
        active = 1'b0;
        start_stb = 1'b0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_edges.vhd — the same mode logic in VHDL
-- spi_mode_edges.vhd
--
-- Chapter 13.5 -- CPOL and CPHA in RTL, without four copies of the datapath.
--
-- Chapter 13.4's divider emits two strobes and deliberately refuses to say
-- what they are for:
--
--   edge_a_stb   the LEADING edge   (SCLK departs from its idle level)
--   edge_b_stb   the TRAILING edge  (SCLK returns to its idle level)
--
-- This block is the whole of the mode logic. Everything downstream sees only
-- `launch_stb` and `capture_stb` and never learns what mode it is running in.
--
-- THE FIRST CLAIM: CPOL DOES NOT APPEAR HERE. It chooses the idle level, and
-- the divider already consumed it -- "leading" means "away from idle"
-- whichever level idle is. So the assignment depends on CPHA ALONE:
--
--   CPHA = 0   capture on the LEADING edge, launch on the TRAILING edge
--   CPHA = 1   launch  on the LEADING edge, capture on the TRAILING edge
--
-- THE SECOND CLAIM: THE TWO PHASES ARE NOT SYMMETRIC. CPHA=1 is balanced --
-- one launch then one capture per period. CPHA=0 captures on the FIRST
-- leading edge, so bit zero must already be on MOSI before any clock moves
-- (`preload_stb`), and its FINAL trailing edge has no bit left to launch, so
-- that launch is suppressed rather than pushing an extra bit out.
--
--     CPHA=0, 4 bits:  preload, cap, launch, cap, launch, cap, launch, cap, X
--     CPHA=1, 4 bits:  launch, cap, launch, cap, launch, cap, launch, cap

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

entity spi_mode_edges is
    generic (
        LEN_W : positive := 6            -- width of the bit-count field
    );
    port (
        clk         : in  std_logic;
        rst_n       : in  std_logic;

        cpha        : in  std_logic;                      -- CPOL is absent
        len         : in  unsigned(LEN_W - 1 downto 0);   -- bits, >= 1

        active      : in  std_logic;   -- a frame is shifting
        start_stb   : in  std_logic;   -- one cycle, before the first edge
        edge_a_stb  : in  std_logic;   -- leading  edge, from 13.4
        edge_b_stb  : in  std_logic;   -- trailing edge, from 13.4

        preload_stb : out std_logic;   -- drive bit 0 before the first edge
        launch_stb  : out std_logic;   -- put the next bit on MOSI
        capture_stb : out std_logic;   -- sample MISO
        bit_idx     : out unsigned(LEN_W - 1 downto 0);
        frame_done  : out std_logic
    );
end entity;

architecture rtl of spi_mode_edges is

    -- Captures completed. This is the frame's position -- not launches,
    -- because it is the captured bits the transfer exists to collect and
    -- because in CPHA=0 the launches do not run to `len`.
    signal caps   : unsigned(LEN_W - 1 downto 0) := (others => '0');

    -- Bits driven onto MOSI, preload included.
    signal drives : unsigned(LEN_W - 1 downto 0) := (others => '0');

    signal caps_full   : std_logic;
    signal drives_full : std_logic;
    signal cap_edge    : std_logic;
    signal launch_edge : std_logic;
    signal launch_i    : std_logic;
    signal capture_i   : std_logic;
    signal last_cap    : std_logic;

begin

    caps_full   <= '1' when caps   >= len else '0';
    drives_full <= '1' when drives >= len else '0';

    -- The entire mode decision.
    cap_edge    <= edge_b_stb when cpha = '1' else edge_a_stb;
    launch_edge <= edge_a_stb when cpha = '1' else edge_b_stb;

    capture_i <= active and cap_edge and (not caps_full);

    -- The suppression. In CPHA=1 `drives_full` bites only after the frame's
    -- last leading edge. In CPHA=0 it bites one edge earlier -- on the
    -- trailing edge that follows the last capture -- and that is the case it
    -- exists for.
    launch_i  <= active and launch_edge and (not drives_full);

    preload_stb <= start_stb and (not cpha);
    capture_stb <= capture_i;
    launch_stb  <= launch_i;
    bit_idx     <= caps;

    -- Asserted ON the final capture, not the cycle after it. `caps` only
    -- reaches `len` at the next clock edge, so a design that waits for
    -- `caps_full` alone reports the frame one cycle late -- and one cycle is
    -- enough. With CPHA=1 the final capture lands two cycles before the next
    -- leading edge is due, so the extra cycle lets the divider START that edge
    -- before the enable drops, and the enable dropping then drags SCLK back:
    -- a RUNT NINTH PULSE, one system clock wide, on every frame.
    --
    -- It is invisible from inside the master -- `caps_full` blocks any ninth
    -- capture, so the received word is right and every internal count agrees
    -- -- and a slave that recovers bit boundaries from the pin counts it and is
    -- one bit out from the second byte onward. Only a pin-level model finds it
    -- (Chapter 13.11).
    --
    -- NOT ASSERTED ON THE FRAME'S OWN START CYCLE. `caps` is cleared by the
    -- same clock edge that samples `start_stb`, so during that cycle it still
    -- holds the PREVIOUS frame's total and `caps_full` is still true. A
    -- sequencer that restarts the shift engine and watches `frame_done` in the
    -- same cycle -- which is exactly what the streaming controller of Chapter
    -- 13.9 does -- would see the old frame's completion and end the new frame
    -- before its first edge.
    last_cap    <= capture_i when caps = (len - 1) else '0';
    frame_done  <= active and (not start_stb) and (caps_full or last_cap);

    count : process (clk, rst_n)
    begin
        if rst_n = '0' then
            caps   <= (others => '0');
            drives <= (others => '0');
        elsif rising_edge(clk) then
            if start_stb = '1' then
                -- The preload counts as a drive, so a CPHA=0 frame begins one
                -- bit ahead and a CPHA=1 frame begins at zero.
                caps <= (others => '0');
                if cpha = '1' then
                    drives <= (others => '0');
                else
                    drives <= to_unsigned(1, LEN_W);
                end if;
            elsif active = '1' then
                if capture_i = '1' then
                    caps <= caps + 1;
                end if;
                if launch_i = '1' then
                    drives <= drives + 1;
                end if;
            end if;
        end if;
    end process;

    -- Two things that must never happen, stated where a reviewer sees them.
    check : process (clk)
    begin
        if rising_edge(clk) and rst_n = '1' then
            assert not (launch_i = '1' and capture_i = '1')
                report "launch and capture on the same cycle" severity failure;
            -- Guarded by `active` and by the start cycle: between frames,
            -- and on the very cycle `active` rises, `caps` still holds the
            -- previous frame's total while `len` already carries the next
            -- frame's. Comparing those two is meaningless, and an unguarded
            -- version of this assertion fires on the first short frame that
            -- follows a long one -- which looks exactly like a counter bug.
            assert start_stb = '1' or active = '0' or caps <= len
                report "captured more bits than the frame holds" severity failure;
        end if;
    end process;

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_edges_tb.vhd — the same 80-frame sweep in VHDL
-- spi_mode_edges_tb.vhd
--
-- This testbench instantiates the REAL divider from Chapter 13.4 rather than
-- a hand-written edge generator, because the claim under test is about how
-- the two blocks compose: that the divider absorbs CPOL entirely, and that
-- what reaches the datapath depends on CPHA alone.
--
-- The central experiment records, for every transfer, the ORDER of events
-- (preload / launch / capture) and the SCLK LEVEL at each one. Running the
-- same frame under both polarities must give an identical order and an
-- exactly inverted set of levels.

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

entity spi_mode_edges_tb is
end entity;

architecture sim of spi_mode_edges_tb is

    constant LEN_W : positive := 6;
    constant DIV_W : positive := 8;

    constant EV_PRELOAD : natural := 1;
    constant EV_LAUNCH  : natural := 2;
    constant EV_CAPTURE : natural := 3;

    type ev_array is array (0 to 255) of natural;

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

    -- divider (Chapter 13.4)
    signal en   : std_logic := '0';
    signal div  : unsigned(DIV_W - 1 downto 0) := to_unsigned(4, DIV_W);
    signal cpol : std_logic := '0';

    signal sclk       : std_logic;
    signal edge_a_stb : std_logic;
    signal edge_b_stb : std_logic;
    signal bit_done   : std_logic;
    signal half_a     : unsigned(DIV_W - 1 downto 0);
    signal half_b     : unsigned(DIV_W - 1 downto 0);
    signal div_err    : std_logic;

    -- mode logic (Chapter 13.5)
    signal cpha      : std_logic := '0';
    signal len       : unsigned(LEN_W - 1 downto 0) := to_unsigned(8, LEN_W);
    signal active    : std_logic := '0';
    signal start_stb : std_logic := '0';

    signal preload_stb : std_logic;
    signal launch_stb  : std_logic;
    signal capture_stb : std_logic;
    signal bit_idx     : unsigned(LEN_W - 1 downto 0);
    signal frame_done  : std_logic;

    -- recorder
    signal ev_code  : ev_array := (others => 0);
    signal ev_level : ev_array := (others => 0);
    signal ev_n     : natural  := 0;
    signal n_pre, n_launch, n_cap : natural := 0;
    signal coincident    : natural := 0;   -- two strobes on one cycle
    signal early_capture : natural := 0;   -- a capture with no bit driven
    signal recording     : std_logic := '0';
    signal clear_stb     : std_logic := '0';

    signal errors : natural := 0;

    -- The expected event sequence, derived from the rules rather than from
    -- the design. CPHA=0: preload, then nbits captures with a launch between
    -- each pair. CPHA=1: nbits (launch, capture) pairs.
    function expect_code(pha : std_logic; k : natural) return natural is
    begin
        if pha = '0' then
            if k = 0 then
                return EV_PRELOAD;
            elsif (k mod 2) = 1 then
                return EV_CAPTURE;
            else
                return EV_LAUNCH;
            end if;
        else
            if (k mod 2) = 0 then
                return EV_LAUNCH;
            else
                return EV_CAPTURE;
            end if;
        end if;
    end function;

    -- CPHA=0: 1 preload + nbits captures + (nbits-1) launches
    -- CPHA=1: nbits launches  + nbits captures
    -- Both come to 2*nbits, which is the arithmetic form of "the two phases
    -- move the same number of bits by different means".
    function expect_n(nbits : natural) return natural is
    begin
        return 2 * nbits;
    end function;

begin

    clk <= not clk after 5 ns when not halt else '0';

    u_div : entity work.spi_clkdiv_strobe
        generic map (DIV_W => DIV_W)
        port map (
            clk => clk, rst_n => rst_n,
            en => en, div => div, cpol => cpol,
            sclk => sclk, edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
            bit_done => bit_done,
            half_a => half_a, half_b => half_b, div_err => div_err
        );

    dut : entity work.spi_mode_edges
        generic map (LEN_W => LEN_W)
        port map (
            clk => clk, rst_n => rst_n,
            cpha => cpha, len => len,
            active => active, start_stb => start_stb,
            edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
            preload_stb => preload_stb, launch_stb => launch_stb,
            capture_stb => capture_stb, bit_idx => bit_idx,
            frame_done => frame_done
        );

    record_p : process (clk)
        variable hits : natural;
        variable code : natural;
    begin
        if rising_edge(clk) then
            if clear_stb = '1' then
                ev_n <= 0; n_pre <= 0; n_launch <= 0; n_cap <= 0;
                coincident <= 0; early_capture <= 0;
                ev_code  <= (others => 0);
                ev_level <= (others => 0);
            elsif recording = '1' then
                hits := 0;
                if preload_stb = '1' then hits := hits + 1; end if;
                if launch_stb  = '1' then hits := hits + 1; end if;
                if capture_stb = '1' then hits := hits + 1; end if;
                if hits > 1 then
                    coincident <= coincident + 1;
                end if;

                if hits > 0 then
                    if preload_stb = '1' then
                        code := EV_PRELOAD;
                    elsif launch_stb = '1' then
                        code := EV_LAUNCH;
                    else
                        code := EV_CAPTURE;
                    end if;
                    if ev_n < 256 then
                        ev_code(ev_n)  <= code;
                        if sclk = '1' then
                            ev_level(ev_n) <= 1;
                        else
                            ev_level(ev_n) <= 0;
                        end if;
                    end if;
                    ev_n <= ev_n + 1;
                end if;

                if preload_stb = '1' then n_pre    <= n_pre + 1;    end if;
                if launch_stb  = '1' then n_launch <= n_launch + 1; end if;
                if capture_stb = '1' then
                    n_cap <= n_cap + 1;
                    -- The bit being captured must already have been driven.
                    -- This is the property that makes CPHA=0's preload
                    -- necessary rather than decorative.
                    if (n_pre + n_launch) < (n_cap + 1) then
                        early_capture <= early_capture + 1;
                    end if;
                end if;
            end if;
        end if;
    end process;

    stim : process
        variable errs : natural := 0;
        variable ref_code  : ev_array;
        variable ref_level : ev_array;
        variable ref_n     : natural;
        variable bad, diff_order, same_level, guard : natural;
        variable line_s : string(1 to 64);
        variable lp     : natural;

        -- One complete frame. CS-assert (start_stb) comes first, the clock
        -- starts a few cycles later, and `active` is deliberately held for
        -- one extra SCLK period past the final capture so the edge that must
        -- NOT launch actually occurs and can be seen to do nothing.
        procedure run_frame(dv : natural; pol : std_logic; pha : std_logic;
                            nbits : natural) is
        begin
            wait until falling_edge(clk);
            en <= '0'; active <= '0'; start_stb <= '0';
            div <= to_unsigned(dv, DIV_W); cpol <= pol; cpha <= pha;
            len <= to_unsigned(nbits, LEN_W);
            for k in 1 to 4 loop wait until falling_edge(clk); end loop;

            clear_stb <= '1';
            wait until falling_edge(clk);
            clear_stb <= '0';
            recording <= '1';

            wait until falling_edge(clk);
            start_stb <= '1'; active <= '1';    -- CS asserts; MOSI preloads
            wait until falling_edge(clk);
            start_stb <= '0';
            for k in 1 to 3 loop wait until falling_edge(clk); end loop;

            en <= '1';                          -- SCLK begins
            guard := dv * (nbits + 4) + 40;
            while frame_done = '0' and guard > 0 loop
                wait until falling_edge(clk);
                guard := guard - 1;
            end loop;
            if guard = 0 then
                report "  FAIL: frame never completed";
                errs := errs + 1;
            end if;
            for k in 1 to dv + 2 loop           -- the edge that must not launch
                wait until falling_edge(clk);
            end loop;
            en <= '0'; active <= '0';
            for k in 1 to 3 loop wait until falling_edge(clk); end loop;
            recording <= '0';
            wait until falling_edge(clk);
        end procedure;

        procedure check_sequence(pha : std_logic; nbits : natural;
                                 tag : string) is
            variable n_bad : natural := 0;
        begin
            if ev_n /= expect_n(nbits) then
                report "  FAIL: " & tag & " saw " & integer'image(ev_n) &
                       " events, expected " & integer'image(expect_n(nbits));
                errs := errs + 1;
            else
                for k in 0 to ev_n - 1 loop
                    if ev_code(k) /= expect_code(pha, k) then
                        n_bad := n_bad + 1;
                    end if;
                end loop;
                if n_bad /= 0 then
                    report "  FAIL: " & tag & " had " & integer'image(n_bad) &
                           " events out of order";
                    errs := errs + 1;
                end if;
            end if;
            if n_cap /= nbits then
                report "  FAIL: " & tag & " captured " & integer'image(n_cap) &
                       " bits, expected " & integer'image(nbits);
                errs := errs + 1;
            end if;
            if (n_pre + n_launch) /= nbits then
                report "  FAIL: " & tag & " drove MOSI " &
                       integer'image(n_pre + n_launch) & " times, expected " &
                       integer'image(nbits);
                errs := errs + 1;
            end if;
            if (pha = '0' and n_pre /= 1) or (pha = '1' and n_pre /= 0) then
                report "  FAIL: " & tag & " produced " & integer'image(n_pre) &
                       " preloads";
                errs := errs + 1;
            end if;
            if coincident /= 0 then
                report "  FAIL: " & tag & " had two strobes on one cycle";
                errs := errs + 1;
            end if;
            if early_capture /= 0 then
                report "  FAIL: " & tag & " captured a bit never driven";
                errs := errs + 1;
            end if;
        end procedure;

        type int_vec is array (natural range <>) of natural;
        constant DVS  : int_vec(0 to 3) := (2, 3, 4, 6);
        constant LENS : int_vec(0 to 4) := (1, 2, 4, 8, 16);
        variable pha_v : std_logic;
    begin
        for k in 0 to 2 loop wait until falling_edge(clk); end loop;
        rst_n <= '1';
        wait until falling_edge(clk);

        -- 1. THE TWO PHASES, SHOWN SIDE BY SIDE.
        run_frame(4, '0', '0', 4);
        check_sequence('0', 4, "mode 0, 4 bits");
        lp := 0;
        for k in 0 to ev_n - 1 loop
            if ev_code(k) = EV_PRELOAD then line_s(lp + 1) := 'P';
            elsif ev_code(k) = EV_LAUNCH then line_s(lp + 1) := 'L';
            else                              line_s(lp + 1) := 'C';
            end if;
            line_s(lp + 2) := ' ';
            lp := lp + 2;
        end loop;
        report "  CPHA=0, 4 bits: " & line_s(1 to lp) &
               "-- preload first, no launch after the last capture";

        run_frame(4, '0', '1', 4);
        check_sequence('1', 4, "mode 1, 4 bits");
        lp := 0;
        for k in 0 to ev_n - 1 loop
            if ev_code(k) = EV_PRELOAD then line_s(lp + 1) := 'P';
            elsif ev_code(k) = EV_LAUNCH then line_s(lp + 1) := 'L';
            else                              line_s(lp + 1) := 'C';
            end if;
            line_s(lp + 2) := ' ';
            lp := lp + 2;
        end loop;
        report "  CPHA=1, 4 bits: " & line_s(1 to lp) &
               "-- launch first, perfectly paired";

        -- 2. CPOL ONLY INVERTS THE PIN. Same frame, both polarities: the
        --    event order must be identical and every level exactly inverted.
        for p in 0 to 1 loop
            if p = 1 then pha_v := '1'; else pha_v := '0'; end if;

            run_frame(4, '0', pha_v, 8);
            ref_n     := ev_n;
            ref_code  := ev_code;
            ref_level := ev_level;
            run_frame(4, '1', pha_v, 8);

            diff_order := 0;
            same_level := 0;
            if ev_n /= ref_n then
                report "  FAIL: the event count changed with CPOL";
                errs := errs + 1;
            else
                for k in 0 to ev_n - 1 loop
                    if ev_code(k) /= ref_code(k) then
                        diff_order := diff_order + 1;
                    end if;
                    if ev_level(k) = ref_level(k) then
                        same_level := same_level + 1;
                    end if;
                end loop;
            end if;
            if diff_order /= 0 then
                report "  FAIL: the event order changed with CPOL";
                errs := errs + 1;
            end if;
            if same_level /= 0 then
                report "  FAIL: " & integer'image(same_level) &
                       " events kept the same SCLK level under both polarities";
                errs := errs + 1;
            end if;
            report "  CPHA=" & integer'image(p) & ": " & integer'image(ev_n) &
                   " events, identical order under both polarities, all " &
                   integer'image(ev_n) & " SCLK levels inverted";
        end loop;

        -- 3. A SINGLE-BIT FRAME is the corner where CPHA=0 launches nothing
        --    at all: the preload is the only drive, and the one trailing edge
        --    is suppressed.
        run_frame(4, '0', '0', 1);
        check_sequence('0', 1, "mode 0, 1 bit");
        if n_launch /= 0 then
            report "  FAIL: a one-bit CPHA=0 frame launched";
            errs := errs + 1;
        end if;
        run_frame(4, '0', '1', 1);
        check_sequence('1', 1, "mode 1, 1 bit");
        report "  1 bit: CPHA=0 drives once via the preload and launches never; CPHA=1 launches once";

        -- 4. THE SWEEP. Every phase, polarity, divisor and length.
        for p in 0 to 1 loop
            if p = 1 then pha_v := '1'; else pha_v := '0'; end if;
            for d in DVS'range loop
                for l in LENS'range loop
                    run_frame(DVS(d), '0', pha_v, LENS(l));
                    check_sequence(pha_v, LENS(l), "sweep cpol=0");
                    run_frame(DVS(d), '1', pha_v, LENS(l));
                    check_sequence(pha_v, LENS(l), "sweep cpol=1");
                end loop;
            end loop;
        end loop;
        report "  80 frames swept: 2 phases x 2 polarities x 4 divisors x 5 lengths";

        errors <= errs;
        if errs = 0 then
            report "PASS: the launch and capture assignment follows CPHA alone and never CPOL -- the same frame under both polarities produces an identical event order with every SCLK level inverted -- a CPHA=0 frame preloads MOSI before the first edge and suppresses the launch on the final trailing edge while a CPHA=1 frame does neither, both driving exactly as many bits as they capture, no bit is ever captured before it was driven, launch and capture never fall on the same cycle, and all of it holds across 80 frames spanning both phases, both polarities, four divisors and lengths from 1 to 16 bits";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

Parity

All three implementations run 80 frames across both phases, both polarities, four divisors and lengths from 1 to 16 bits. All three confirm the CPOL-independence experiment: identical event order under both polarities with every SCLK level inverted. And all three confirm the one-bit corner, where CPHA=0 drives once via the preload and launches never — the case where the suppression removes the only launch there would have been.

7. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_edges.sva — the mapping, the asymmetry, and what must never happen
// The specification is a mapping from edges to actions, so most of it is an
// equivalence rather than an implication. The interesting assertions are the two
// about the ENDS, because that is where the phases differ.

module spi_mode_edges_sva #(parameter int LEN_W = 6) (
    input logic             clk,
    input logic             rst_n,
    input logic             cpha,
    input logic [LEN_W-1:0] len,
    input logic             active,
    input logic             start_stb,
    input logic             edge_a_stb,
    input logic             edge_b_stb,
    input logic             preload_stb,
    input logic             launch_stb,
    input logic             capture_stb,
    input logic [LEN_W-1:0] bit_idx,
    input logic             frame_done
);

    default clocking cb @(posedge clk); endclocking
    default disable iff (!rst_n);

    // THE mapping. Stated as an equivalence so a capture on the wrong edge and
    // a missing capture both fail, and stated without CPOL because CPOL is not
    // an input to this block -- which is itself part of the specification.
    a_capture_edge: assert property (
        capture_stb |-> (cpha ? edge_b_stb : edge_a_stb)
    );
    a_launch_edge: assert property (
        launch_stb |-> (cpha ? edge_a_stb : edge_b_stb)
    );

    // Never both on one cycle. The two edges never coincide (Chapter 13.4), so
    // this can only fail through a mapping error -- which makes it a cheap and
    // very direct check on the mux.
    a_not_both: assert property (!(launch_stb && capture_stb));

    // The asymmetry, in both directions. A preload happens exactly when the
    // phase is zero, and never otherwise.
    a_preload_iff_pha0: assert property (preload_stb == (start_stb && !cpha));

    // No capture may exceed the frame. Guarded against the start cycle because
    // `bit_idx` still holds the previous frame's total there.
    a_no_overrun: assert property (
        active && !start_stb |-> bit_idx <= len
    );

    // THE SUPPRESSION, stated as a counting property: over one frame, the number
    // of drives equals the number of captures equals `len`. This is what a
    // loopback test cannot check and a real device notices.
    int drives, caps;
    always_ff @(posedge clk) begin
        if (!rst_n || start_stb) begin
            drives <= (start_stb && !cpha) ? 1 : 0;
            caps   <= 0;
        end else if (active) begin
            if (launch_stb)  drives <= drives + 1;
            if (capture_stb) caps   <= caps   + 1;
        end
    end
    a_drives_match: assert property (
        $rose(frame_done) |-> (drives == len) && (caps == len)
    );

    // A drive may never happen after the frame's last capture while the frame is
    // still active -- the failure the suppression exists to prevent, stated at
    // the point it would occur rather than as a count.
    a_no_late_drive: assert property (
        (caps == len) && active |-> !launch_stb
    );

    // And `frame_done` must not fire on the start cycle, which is the stale-level
    // bug that cut frames short in Chapter 13.9.
    a_done_not_at_start: assert property (start_stb |-> !frame_done);

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_mode_edges_cg.sv — phases, lengths, and the degenerate frame
// Two phases and a length is a two-dimensional space, and the interesting
// corners are all at small lengths -- where the preload IS the frame, or where
// the suppressed edge is the only edge.

covergroup cg_mode_edges @(posedge clk);

    pha: coverpoint cpha { bins zero = {0}; bins one = {1}; }

    // Length, with the corners named. One bit is the case where CPHA=0 launches
    // nothing at all; two is the smallest frame with an interior launch.
    length: coverpoint len iff (start_stb) {
        bins one    = {1};
        bins two    = {2};
        bins small  = {[3:8]};
        bins wide   = {[9:32]};
    }

    // CPOL must be swept even though this block does not receive it, because the
    // claim under test is that it makes no difference -- and a claim about a
    // signal is only verified if the signal moved.
    pol: coverpoint tb_cpol { bins low = {0}; bins high = {1}; }

    // How many launches a frame actually produced, relative to its length. The
    // two phases land in different bins and that is the point: CPHA=0 gives
    // len-1 and CPHA=1 gives len.
    launches: coverpoint (n_launches - len) iff (frame_done) {
        bins one_short = {-1};     // CPHA=0
        bins exact     = {0};      // CPHA=1
        bins too_many  = {[1:$]};  // the suppression failed
        bins too_few   = {[$:-2]}; // an edge was lost
    }

    // The suppressed edge must actually have OCCURRED and been ignored, which is
    // different from never happening. A frame that ended before the surplus edge
    // arrived has not exercised the suppression.
    suppressed: coverpoint surplus_edge_seen iff (frame_done && !cpha) {
        bins occurred_and_ignored = {1};
    }

    x_pha_len:  cross pha, length;
    x_pha_pol:  cross pha, pol;
    x_len_supp: cross length, suppressed;

endgroup

8. Why an FPGA or ASIC Engineer Cares

The whole block is two muxes and two counters. capture_stb and launch_stb are each one 2:1 mux on a strobe, ANDed with a guard. There is no arithmetic on the strobe path — the comparisons that produce the guards are against len, which comes from the configuration latch of Chapter 13.2 and is stable for the whole frame.

Collapsing four cases to two halves the logic and quarters the state space. A {cpol, cpha} decode is a 4:1 mux on both strobes; a cpha decode is a 2:1. More importantly, a four-way design has four paths that must each meet timing and each be verified, and two of them are the ones nobody runs.

The guards are on the enable, not the data. drives_full and caps_full gate whether a strobe is passed on; they do not select between values. That means they can be slow relative to the strobe without affecting anything, because the strobe arrives once per half-period and the guard was computed at frame start.

Cost. Two LEN_W counters, two comparators, four gates. At LEN_W = 6 that is 12 flops. The four-way alternative is the same flop count and roughly double the combinational logic, for no additional function.

9. Failure Signature — Modes 0 And 3 Work, Modes 1 And 2 Do Not

Symptom. A master is verified against a device in mode 0 and ships. A second device on the same bus needs mode 1, and every transfer to it returns data that is bit-shifted by one. Mode 3 is then tried on a third device and works. Mode 2 fails the same way as mode 1.

What the pattern says. Modes 0 and 3 work; modes 1 and 2 fail. Look at the table: 0 and 3 are the two modes where rising is the capture edge. So the design captures on the rising edge of SCLK unconditionally — it has implemented CPOL correctly (the idle level is right, since mode 3 works) and has implemented CPHA by hard-coding the voltage direction rather than deriving it from the idle level.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   mode 0   CPOL=0 CPHA=0   capture on rising    -> works by coincidence
   mode 1   CPOL=0 CPHA=1   capture on falling   -> captures on rising: wrong
   mode 2   CPOL=1 CPHA=0   capture on falling   -> captures on rising: wrong
   mode 3   CPOL=1 CPHA=1   capture on rising    -> works by coincidence

Why it survived verification. The suite was written against mode 0, which is by far the most common, and mode 3 was added later and passed — which was taken as evidence that the mode logic was general. It is the opposite: modes 0 and 3 are the diagonal of the table, and they are exactly the pair a rising-edge-only design gets right. Testing two modes out of four proves nothing unless they are chosen to differ in CPHA at constant CPOL.

How this chapter's structure prevents it. The strobes arrive already named for the idle level, so there is no rising or falling anywhere in the mode logic to hard-code. The block cannot express the bug, because the information it would need — which voltage is which — is not one of its inputs.

What to test to catch it in a design that does not have that structure. Fix CPOL and sweep CPHA. A suite that varies both together walks the diagonal and learns nothing; a suite that holds CPOL at 0 and tries both phases finds it in the second test case.

10. Common Misconceptions

"Four modes need four cases." Four modes need two, because CPOL is absorbed by the clock generator. Writing four is not incorrect, but it doubles the logic and the verification surface and creates two paths that are rarely exercised.

"CPHA=0 and CPHA=1 are mirror images." They are not. CPHA=1 is balanced; CPHA=0 needs a bit on MOSI before any edge exists and has a surplus edge at the far end. Both ends need handling and only one of them is visible in a loopback test.

"The preload is just the first launch, moved earlier." It is the first drive, and it must advance the shift register exactly as a launch does. A preload that drives without advancing makes the frame send bit zero twice and never send the last bit — and the damage appears on the receive side, two blocks from the cause.

"The suppressed edge is harmless, since nothing is listening." The master is not listening. A real slave whose sampling is slightly later than the nominal edge is, and it reads the extra bit as the frame's final bit. The failure is intermittent, board-dependent, and invisible in simulation against an ideal model.

"If the data comes back correct, the mode logic is right." A loopback path is symmetric, so a design that reverses both the launch and the capture assignment returns correct data from the wrong edges. The event order has to be checked, not only the payload — which is why the testbench records events rather than bytes.

11. Reason It Through

Why does the block count captures rather than launches as its notion of position?

Because captures are what the transfer exists to collect, and because in CPHA=0 the launches do not run to the frame length — the preload takes one and the final one is suppressed. A position derived from launches would be off by one in one phase and correct in the other, which is exactly the kind of asymmetry that produces a design with four cases.

In CPHA=1, does the drives_full guard ever actually suppress anything?

Yes, but only when active outlives the frame. The frame's final capture is on the last trailing edge, and the next leading edge would launch a bit that does not exist. Usually the enable has already dropped by then; the guard makes the block correct regardless of how long active is held, which matters because the streaming controller of Chapter 13.9 holds it across frame boundaries.

A one-bit frame in CPHA=0: how many launches?

Zero. The preload provides the single bit and the one trailing edge is the suppressed one. It is the corner where the suppression removes the only launch there would have been, which makes it the most valuable length in the sweep — a design that suppresses incorrectly produces either two drives for one bit or none at all.

Why must frame_done not assert on the frame's own start cycle?

Because the capture counter is cleared by the same edge that samples start_stb, so during that cycle it still holds the previous frame's total and looks full. A sequencer that restarts the engine and watches frame_done in the same cycle sees the old frame's completion, ends the new frame before its first edge, and produces a transfer of zero bits that reports success.

The runt ninth pulse is invisible to every count inside the master. What does that imply about where such bugs are found?

That a testbench built from the design's own abstractions cannot find them. Every strobe count, every bit index and every received word is correct, because the block that would have counted the ninth edge is the block that correctly refuses to. The pulse exists only on the pin, and only a model that recovers bit boundaries from the pin — as a real device does — sees it. That is the argument for having at least one pin-level slave model in the suite, and for it being the one used in the top-level test of Chapter 13.11.

12. Understanding Check

13. Summary

The four-row mode table collapses to two cases, because CPOL was consumed by the clock generator: its strobes are named for the idle level, so the launch and capture assignment depends on CPHA alone. A design that cases on both bits doubles the logic and creates two paths nobody exercises.

The claim is falsifiable and is tested as such: the same frame under both polarities must produce an identical event order with every SCLK level inverted.

The two remaining cases are not mirror images. CPHA=1 is balanced — one launch then one capture per period, nothing special at either end. CPHA=0 needs a preload, because its first capture is the first edge, and a suppressed final launch, because its last trailing edge has no bit left to send. Both phases move the same number of bits by different means.

The preload must advance the shift register, because it consumes a bit. Leaving it in place ships bit zero twice, never ships the last bit, and — because slaves echo — presents as a receive-side shift, two blocks from the cause.

The suppression is invisible to loopback and to ideal slave models. It is visible to a real device sampling slightly late, which reads the surplus bit as the frame's final bit: intermittent, board-dependent, and absent from every simulation.

frame_done must not assert on the frame's own start cycle, or a sequencer restarting the engine sees the previous frame's completion; and it must assert on the final capture rather than the cycle after, or the divider starts a ninth edge that the enable then drags back — a runt pulse that every count inside the master agrees is fine and that only a pin-level slave model detects.

For verification, the mapping assertions are equivalences, the suppression is a counting property over the frame, and coverage sweeps CPOL even though the block does not receive it, because a claim about a signal is only verified if the signal moved. Lengths down to one bit matter most, since that is where CPHA=0 launches nothing at all.

14. What Comes Next

Something now knows which edge means what. Nothing yet holds the bits.

Chapter 13.6 — TX and RX Shift Registers builds the datapath, and it opens with a genuinely tempting piece of cleverness: SPI is exactly full-duplex and exactly symmetric, so one circulating register can do both jobs. There are three reasons not to, and all three are about what happens when a transfer does not go as planned.

Continue learning