Skip to content
VLSI Mentor

SPI · Module 16

Driver Architecture

A driver owns every timing number in the protocol, so it is the one component that must be checked against something not written to agree with it. Thirty-two legal transactions violate nothing and exercise all eight rules; seven injected faults fire exactly the rules predicted.

A generator says what to send. A driver says when every pin moves. That division is the whole reason a driver gets its own chapter: it is where the protocol's timing contract is implemented exactly once, so a bug here is a bug in every test in the suite.

Which is also why it is the one component that cannot be allowed to check itself.

A driver that computes its lead wrongly will also assert that its lead is correct, with total conviction. What can check it?

1. The Timing Contract

Four numbers, from Module 14, in the units the driver counts in.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   LEAD   cycles from CS falling to the first SCLK edge          (14.4)
   HALF   cycles per SCLK half period                           (15.3)
   LAG    cycles from the last SCLK edge to CS rising            (14.5)
   GAP    cycles of CS high between transactions                 (14.5)

Each is a separate obligation with its own failure mode, which is why a driver cannot be written as "toggle a clock N times". A driver that satisfies three of the four passes most tests.

2. The Edge Arithmetic

A frame of N bits has 2N edges, numbered e = 0 .. 2N-1. Edge 2k is the leading edge of bit k — the edge on which SCLK leaves its idle level — and 2k+1 is the trailing one. With CPOL consumed by "leading means left idle", the mode reduces to CPHA alone:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CPHA = 0    capture on the leading edge, launch on the trailing edge
               ... so THE FIRST BIT MUST ALREADY BE ON THE PIN before edge 0,
                   which means it is placed during the LEAD

   CPHA = 1    launch on the leading edge, capture on the trailing edge
               ... so the pin may idle through the LEAD

That asymmetry is the most common driver bug in SPI, and the reason is that the wrong implementation works: a driver that always places the first bit on edge 0 is perfect in CPHA=1 and loses the first bit in CPHA=0 — in a way that looks like a slave problem.

Everything in the shift state is then decided from the edge's parity rather than from the level of SCLK. The level depends on CPOL and the parity does not, so a driver that reasons about levels needs four cases where this needs one.

A driver as four states — idle, lead, shift and lag — with a latched request, one reused counter, effective timing numbers after fault injection, and pin outputs checked by a rule monitorrequestlatched on acceptfault codeIDLELEADSHIFTLAGSCLK, CS, MOSIcaptured wordrule monitor12
Figure 1 — the driver as four states and one reused counter. The gap is enforced in IDLE rather than as a trailing delay, because the obligation belongs to the NEXT transaction's start; the request is latched on acceptance so the generator can move on; and the launch and capture decisions are taken from the edge's parity, which is what collapses the four CPOL/CPHA combinations into one path.

3. The Timing, on a Waveform

One frame, and where each of the four numbers is measured

20 cycles
Four rows over twenty cycles: a chip select that falls after a gap and rises after a lag, an SCLK with three-cycle half periods, MOSI changing only on trailing edges with its first bit placed during the lead, and the driver's state.CS fallsCS fallsedge 0: LEAD=4edge 0: LEAD=4HALF=3HALF=3LAG=2 from last edgeLAG=2 from last edgeCS_n11100000000000001111SCLKMOSIXXXb0b0b0b0b0b0b0b1b1b1b1b1b1b1XXXstateIIILLLLSSSSSSSSSGIIIt0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15t16t17t18t19
Figure 2 — one four-bit frame in CPHA=0 with LEAD=4, HALF=3, LAG=2 and GAP=3. The first data bit is placed during the lead, before edge 0, because CPHA=0 captures on the leading edge; MOSI then changes only on the trailing edges; and the lag is counted from the LAST edge rather than from the end of a further half period, which is the defect described in section 5.

Two things in that picture are the chapter's arguments made visible. MOSI carries b0 before edge 0 — the CPHA=0 requirement, satisfied in the lead. And CS rises two cycles after the last edge, not two cycles after a further half period has elapsed, which is the difference section 5 is about.

4. Fault Injection Is Part Of The Design

Each fault code breaks exactly one of the four timing numbers or the edge arithmetic, and the bench requires the rule monitor to name exactly the matching rule.

faultbreaksexpected rule
1LEAD shortened to one cycleR2, the lead
2HALF shortened to one cycleR3, the half period
3launch and capture edges swappedR4, MOSI in motion at a capture edge
4one edge dropped from the frameR5 and R1 — see below
5LAG removedR6, the lag
6GAP shortened to one cycleR7, the gap
7SCLK parked off its idle level in the gapR1, the idle level

Putting the fault input in the driver rather than editing the driver per test is deliberate. The alternative — break it, run, fix it back — is how a suite acquires a checker nobody has ever seen fire.

R8 has no entry, and the omission is the point. MISO is not a master driver's pin. There is no master fault that can reach it, and a suite reporting R8 against a master has a monitor pointed at the wrong agent. Chapter 16.7 is where R8 becomes somebody's responsibility.

5. The Measurement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   legal traffic: 32 transactions in 16 measurement windows

   rule  exercised  violated
   R1         3600         0
   R2           32         0
   R3          640         0
   R4          185         0
   R5           32         0
   R6           32         0
   R7           31         0
   R8         3600         0

Zero violations and all eight exercised. The second half is what makes the first mean something: a driver that never moved a pin would satisfy the zeros and fail the exercise counts.

Note R7 = 31 against R5 = 32: sixteen windows of two transactions give sixteen first-asserts with no preceding gap, and the counter correctly declines to count them. Note R4 = 185: that is how many times MOSI actually moved, which depends on the data pattern, and is the number that would collapse to zero if the suite sent 0x00.

And the fault matrix:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   fault  breaks                      expected  fired
       1  the lead                    00000010  00000010
       2  the half period             00000100  00000100
       3  which edge launches         00001000  00001000
       4  the frame's last edge       00010001  00010001
       5  the lag                     00100000  00100000
       6  the gap                     01000000  01000000
       7  the idle level              00000001  00000001

6. Two Bugs The Fault Matrix Found In The Driver

Neither was found by legal traffic. Both were found by a fault that refused to produce its rule.

7. The Check The Rule Monitor Cannot Make

MISO is driven as the complement of MOSI, so the driver's received word must be the complement of the word it sent — in both phases. The bench measures zero mismatches across all sixteen configurations.

That check exists because a pin-level rule checker is structurally blind to it. A driver that samples MISO on the launch edge instead of the capture edge produces pins that satisfy all eight rules and a received word that is garbage. Two independent checks with different blind spots is the recurring structure of this whole module.

8. Building It — Three HDLs

Azvya Education Pvt. Ltd.VLSI Mentor
spi_driver.sv — the driver, with the four timing numbers and a fault input per obligation
// spi_driver.sv
//
// Chapter 16.4 -- the driver, which is the component that turns a transaction into pin
// motion and is therefore the component that owns every timing number in the protocol.
//
// WHAT A DRIVER IS FOR, stated so the shape of this module is not a surprise.
//
// A generator produces transactions that say WHAT to send: data, width, mode, bit order.
// It says nothing about when a pin moves, and it must not, because the moment a
// generator knows about clock cycles it stops being reusable across every environment
// that shares the protocol. Everything about WHEN lives here.
//
// That division is the reason a driver is worth its own chapter. The driver is where the
// protocol's timing contract is implemented exactly once, and a bug here is a bug in
// every test -- which is also why a driver is the one component that must be checked
// against something other than itself.
//
// THIS DRIVER IS CHECKED BY CHAPTER 16.1'S RULE MONITOR.
//
// The bench instantiates `spi_rule_monitor` on the same pins and requires that legal
// traffic violates nothing while exercising all eight rules, and that each injected
// fault violates EXACTLY ONE rule. Two components written for different purposes
// agreeing about the same pins is evidence; a driver that checks itself is not.
//
// THE TIMING CONTRACT, in the units the driver counts in.
//
//     LEAD   cycles from CS falling to the first SCLK edge
//     HALF   cycles per SCLK half period
//     LAG    cycles from the last SCLK edge to CS rising
//     GAP    cycles of CS high between transactions
//
// These are Chapter 14's numbers, and they are the reason a driver cannot be written as
// "toggle a clock N times": every one of the four is a separate obligation with its own
// failure mode, and a driver that satisfies three of them passes most tests.
//
// THE EDGE ARITHMETIC, which is the part that is easy to get subtly wrong.
//
// A frame of N bits has 2N edges, numbered e = 0 .. 2N-1. Edge 2k is the LEADING edge of
// bit k -- the edge on which SCLK leaves its idle level -- and 2k+1 is the trailing one.
// With CPOL consumed by "leading means left idle", the mode reduces to CPHA alone:
//
//     CPHA = 0    capture on the leading edge, launch on the trailing edge,
//                 and therefore THE FIRST BIT MUST ALREADY BE ON THE PIN before edge 0,
//                 which means it is placed during the LEAD.
//     CPHA = 1    launch on the leading edge, capture on the trailing edge,
//                 and the pin may idle through the LEAD.
//
// That asymmetry is the single most common driver bug in SPI, because a driver that
// always places the first bit on edge 0 works perfectly in CPHA=1 and loses the first
// bit in CPHA=0 -- and loses it in a way that looks like a slave problem.
//
// FAULT INJECTION IS PART OF THE DESIGN, not an afterthought bolted onto the bench.
//
// Each `fault` code breaks exactly one of the four timing numbers or the edge
// arithmetic, and the bench requires the rule monitor to name exactly the matching rule.
// A driver with an input that makes it wrong in a controlled way is a driver whose
// checker can be trusted, and the alternative -- editing the driver to test the monitor
// and editing it back -- is how a suite acquires a checker nobody has ever seen fire.
//
//     0   no fault
//     1   LEAD shortened to one cycle                  -> R2, the lead
//     2   HALF shortened to one cycle                  -> R3, the half period
//     3   launch and capture edges swapped             -> R4, MOSI in motion at capture
//     4   one edge dropped from the last frame         -> R5, a partial frame
//     5   LAG removed                                  -> R6, the lag
//     6   GAP shortened to one cycle                   -> R7, the gap
//     7   SCLK parked off its idle level in the gap    -> R1, the idle level
//
// R8 -- MISO driven only while selected -- has no entry, and the omission is the point:
// MISO is not this component's pin. A master driver cannot violate R8 and a suite that
// claims otherwise has a monitor watching the wrong agent. Chapter 16.7's passive agent
// is where R8 becomes somebody's responsibility.

`timescale 1ns/1ps

module spi_driver #(
    parameter int LEAD  = 4,
    parameter int HALF  = 3,
    parameter int LAG   = 2,
    parameter int GAP   = 3,
    parameter int DW    = 32,
    parameter int LEN_W = 6,
    parameter int CNT_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- the transaction, as a request/accept pair -------------------------
    input  wire              start,
    input  wire [DW-1:0]     tx_data,
    input  wire [LEN_W-1:0]  nbits,
    input  wire              cpol,
    input  wire              cpha,
    input  wire              lsb_first,
    input  wire [2:0]        fault,

    output reg               busy,
    output reg               done,
    output reg  [DW-1:0]     rx_data,

    // --- the pins ----------------------------------------------------------
    output reg               sclk,
    output reg               cs_n,
    output reg               mosi,
    input  wire              miso
);

    localparam int S_IDLE  = 0,
                   S_LEADW = 1,
                   S_SHIFT = 2,
                   S_LAGW  = 3;

    reg [1:0]       state;
    reg [CNT_W-1:0] cnt;          // one counter, reused per state
    reg [CNT_W-1:0] idle_cnt;     // cycles of CS high, for the gap
    reg             had_txn;      // the first transaction has no gap before it

    // The latched transaction. A driver that reads its request signals after accepting
    // it is a driver that changes mid-transaction when the generator moves on, and the
    // symptom is a frame whose first half is one mode and second half another.
    reg [DW-1:0]    tx_q;
    reg [LEN_W-1:0] n_q;
    reg             cpol_q, cpha_q, lsb_q;
    reg [2:0]       flt_q;

    reg [LEN_W:0]   ecnt;         // edges emitted so far
    reg [LEN_W:0]   ptr;          // next bit to launch
    reg [LEN_W:0]   cptr;         // next bit to capture

    // --- the effective timing numbers, after fault injection ---------------
    wire [CNT_W-1:0] lead_eff = (flt_q == 3'd1) ? {{(CNT_W-1){1'b0}}, 1'b1} : LEAD[CNT_W-1:0];
    wire [CNT_W-1:0] half_eff = (flt_q == 3'd2) ? {{(CNT_W-1){1'b0}}, 1'b1} : HALF[CNT_W-1:0];
    wire [CNT_W-1:0] lag_eff  = (flt_q == 3'd5) ? {CNT_W{1'b0}}             : LAG[CNT_W-1:0];
    wire [CNT_W-1:0] gap_eff  = (flt_q == 3'd6) ? {{(CNT_W-1){1'b0}}, 1'b1} : GAP[CNT_W-1:0];

    // Fault 3 swaps which edge launches. Everything downstream reads `cpha_lnch`, so
    // the swap is expressed once rather than at every use -- which matters because a
    // fault expressed in two places is a fault that can be half-injected.
    wire cpha_lnch = (flt_q == 3'd3) ? ~cpha_q : cpha_q;

    // Fault 4 drops the final edge, leaving the frame one edge short of whole.
    wire [LEN_W:0] edges_total = ({1'b0, n_q} << 1) - ((flt_q == 3'd4) ? 1'b1 : 1'b0);

    // --- bit selection ------------------------------------------------------
    // One function, used for both directions, so the transmit order and the receive
    // order cannot drift apart. They did in an earlier version and the result was a
    // driver that passed every MSB-first test and reversed every LSB-first one.
    function automatic integer bit_index(input integer i);
        begin
            bit_index = lsb_q ? i : (n_q - 1 - i);
        end
    endfunction

    wire idle_lvl = cpol_q;

    // Fault 7 parks SCLK off its idle level for one cycle inside the gap, and returns
    // it before the next select. Both edges therefore happen while deselected, so they
    // are not attributed to any transaction -- which is what makes this fault reach R1
    // and nothing else.
    wire bad_idle = (flt_q == 3'd7) && had_txn && (idle_cnt == {CNT_W{1'b0}});

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state    <= S_IDLE[1:0];
            cnt      <= {CNT_W{1'b0}};
            idle_cnt <= {CNT_W{1'b0}};
            had_txn  <= 1'b0;
            busy     <= 1'b0;
            done     <= 1'b0;
            rx_data  <= {DW{1'b0}};
            sclk     <= 1'b0;
            cs_n     <= 1'b1;
            mosi     <= 1'b0;
            tx_q     <= {DW{1'b0}};
            n_q      <= {LEN_W{1'b0}};
            cpol_q   <= 1'b0;
            cpha_q   <= 1'b0;
            lsb_q    <= 1'b0;
            flt_q    <= 3'd0;
            ecnt     <= {(LEN_W+1){1'b0}};
            ptr      <= {(LEN_W+1){1'b0}};
            cptr     <= {(LEN_W+1){1'b0}};
        end else begin
            done <= 1'b0;

            case (state)

                // ------------------------------------------------------------
                // IDLE. The gap is enforced HERE rather than after the frame,
                // because the obligation is "CS high for GAP cycles before the next
                // select" -- an obligation on the next transaction's start, not on
                // this one's end. Enforcing it as a trailing delay works until a
                // test issues two transactions back to back with a long pause
                // between them, at which point the trailing version wastes the
                // pause and still inserts a gap.
                // ------------------------------------------------------------
                S_IDLE[1:0]: begin
                    // THE IDLE LEVEL FOLLOWS THE LIVE `cpol`, NOT THE LATCHED ONE, and
                    // that distinction cost three spurious rule violations before it was
                    // found.
                    //
                    // Parking at the LATCHED polarity means that after a mode change the
                    // pin sits at the previous mode's idle level until the next
                    // transaction is accepted -- and then moves to the new level in the
                    // same instant CS falls. The rule monitor sees three separate
                    // offences for one defect: R1, because SCLK was off its idle level
                    // while deselected; R2, because an SCLK edge coincident with the
                    // select has a lead of zero; and R5, because that spurious edge makes
                    // the frame's edge count odd. One line, three rules, and none of the
                    // three names the cause.
                    //
                    // A real master behaves the way this line does: the idle level is a
                    // function of the mode register and it takes effect when the mode is
                    // programmed, not when a transfer starts.
                    sclk     <= bad_idle ? ~cpol : cpol;
                    idle_cnt <= idle_cnt + 1'b1;

                    if (start && (!had_txn || ((idle_cnt + 1'b1) >= gap_eff))) begin
                        tx_q   <= tx_data;
                        n_q    <= nbits;
                        cpol_q <= cpol;
                        cpha_q <= cpha;
                        lsb_q  <= lsb_first;
                        flt_q  <= fault;

                        cs_n   <= 1'b0;
                        sclk   <= cpol;      // the new mode's idle level, before the lead
                        busy   <= 1'b1;
                        cnt    <= {CNT_W{1'b0}};
                        ecnt   <= {(LEN_W+1){1'b0}};
                        cptr   <= {(LEN_W+1){1'b0}};
                        rx_data <= {DW{1'b0}};

                        // CPHA=0 needs the first bit on the pin BEFORE edge 0, and the
                        // lead is the only place to put it. CPHA=1 launches on edge 0,
                        // so the pin may idle here.
                        if (!(fault == 3'd3 ? ~cpha : cpha)) begin
                            mosi <= lsb_first ? tx_data[0] : tx_data[nbits - 1];
                            ptr  <= {{LEN_W{1'b0}}, 1'b1};
                        end else begin
                            ptr  <= {(LEN_W+1){1'b0}};
                        end

                        state <= S_LEADW[1:0];
                    end
                end

                // ------------------------------------------------------------
                // LEAD. Counted from the cycle CS went low, and the comparison is
                // `cnt + 1 >= lead_eff` so that a lead of one cycle and a lead of
                // four are the same expression. An earlier version special-cased the
                // minimum and got the boundary wrong in the direction that passes.
                // ------------------------------------------------------------
                S_LEADW[1:0]: begin
                    if ((cnt + 1'b1) >= lead_eff) begin
                        sclk <= ~idle_lvl;           // edge 0
                        ecnt <= {{LEN_W{1'b0}}, 1'b1};
                        cnt  <= {CNT_W{1'b0}};

                        // Edge 0 is a leading edge. It launches if CPHA=1 and captures
                        // if CPHA=0.
                        if (cpha_lnch) begin
                            mosi <= tx_q[bit_index(0)];
                            ptr  <= {{LEN_W{1'b0}}, 1'b1};
                        end
                        if (!cpha_lnch) begin
                            rx_data[bit_index(0)] <= miso;
                            cptr <= {{LEN_W{1'b0}}, 1'b1};
                        end

                        // A frame short enough that edge 0 is also the last edge --
                        // reachable with fault 4 at a one-bit width -- must go straight
                        // to the lag rather than through the shift state, which would
                        // emit an edge that no frame asked for.
                        if (edges_total <= {{LEN_W{1'b0}}, 1'b1})
                            state <= S_LAGW[1:0];
                        else
                            state <= S_SHIFT[1:0];
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                // ------------------------------------------------------------
                // SHIFT. One toggle every half period, `edges_total` of them, with the
                // launch and capture decisions taken from the edge's PARITY rather than
                // from the level of SCLK -- because the level depends on CPOL and the
                // parity does not, and a driver that reasons about levels needs four
                // cases where this needs one.
                // ------------------------------------------------------------
                S_SHIFT[1:0]: begin
                    if ((cnt + 1'b1) >= half_eff) begin
                        cnt  <= {CNT_W{1'b0}};
                        sclk <= ~sclk;
                        ecnt <= ecnt + 1'b1;

                        // ecnt counts edges ALREADY emitted, so the edge being emitted
                        // now has index `ecnt`, and its PARITY decides its role: even is
                        // leading, odd is trailing. Deciding from the parity rather than
                        // from the level of SCLK is what collapses four CPOL/CPHA cases
                        // into one.
                        if (ecnt[0] == 1'b1) begin
                            // trailing edge
                            if (!cpha_lnch && (ptr < {1'b0, n_q})) begin
                                mosi <= tx_q[bit_index(ptr)];
                                ptr  <= ptr + 1'b1;
                            end
                            if (cpha_lnch && (cptr < {1'b0, n_q})) begin
                                rx_data[bit_index(cptr)] <= miso;
                                cptr <= cptr + 1'b1;
                            end
                        end else begin
                            // leading edge
                            if (cpha_lnch && (ptr < {1'b0, n_q})) begin
                                mosi <= tx_q[bit_index(ptr)];
                                ptr  <= ptr + 1'b1;
                            end
                            if (!cpha_lnch && (cptr < {1'b0, n_q})) begin
                                rx_data[bit_index(cptr)] <= miso;
                                cptr <= cptr + 1'b1;
                            end
                        end

                        // THE LAG STARTS AT THE LAST EDGE, and the first version of this
                        // module waited a whole further half period before starting to
                        // count it. Nothing about the pins looked wrong -- the frame was
                        // complete, the trailing level was the idle level, every legal
                        // test passed -- but the lag the pins actually showed was
                        // HALF + LAG, so a lag of zero still measured three cycles and R6
                        // could not be made to fire at all. A checker that cannot be made
                        // to fire has not been verified, it has been assumed, and the
                        // fault-injection matrix is what exposed it.
                        //
                        // After 2N toggles SCLK is back at its idle level, so the final
                        // edge IS the return to idle and there is nothing left to wait
                        // for.
                        if ((ecnt + 1'b1) >= edges_total)
                            state <= S_LAGW[1:0];
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                // ------------------------------------------------------------
                // LAG. `lag_eff` of zero must still terminate, which is why the
                // comparison is on `cnt + 1` and not on `cnt`.
                // ------------------------------------------------------------
                S_LAGW[1:0]: begin
                    if ((cnt + 1'b1) >= lag_eff) begin
                        cs_n     <= 1'b1;
                        busy     <= 1'b0;
                        done     <= 1'b1;
                        had_txn  <= 1'b1;
                        idle_cnt <= {CNT_W{1'b0}};
                        state    <= S_IDLE[1:0];
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                default: state <= S_IDLE[1:0];
            endcase
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_driver.v — the same design in Verilog-2001
// spi_driver.v
//
// Chapter 16.4 -- the driver, which is the component that turns a transaction into pin
// motion and is therefore the component that owns every timing number in the protocol.
//
// WHAT A DRIVER IS FOR, stated so the shape of this module is not a surprise.
//
// A generator produces transactions that say WHAT to send: data, width, mode, bit order.
// It says nothing about when a pin moves, and it must not, because the moment a
// generator knows about clock cycles it stops being reusable across every environment
// that shares the protocol. Everything about WHEN lives here.
//
// That division is the reason a driver is worth its own chapter. The driver is where the
// protocol's timing contract is implemented exactly once, and a bug here is a bug in
// every test -- which is also why a driver is the one component that must be checked
// against something other than itself.
//
// THIS DRIVER IS CHECKED BY CHAPTER 16.1'S RULE MONITOR.
//
// The bench instantiates `spi_rule_monitor` on the same pins and requires that legal
// traffic violates nothing while exercising all eight rules, and that each injected
// fault violates EXACTLY ONE rule. Two components written for different purposes
// agreeing about the same pins is evidence; a driver that checks itself is not.
//
// THE TIMING CONTRACT, in the units the driver counts in.
//
//     LEAD   cycles from CS falling to the first SCLK edge
//     HALF   cycles per SCLK half period
//     LAG    cycles from the last SCLK edge to CS rising
//     GAP    cycles of CS high between transactions
//
// These are Chapter 14's numbers, and they are the reason a driver cannot be written as
// "toggle a clock N times": every one of the four is a separate obligation with its own
// failure mode, and a driver that satisfies three of them passes most tests.
//
// THE EDGE ARITHMETIC, which is the part that is easy to get subtly wrong.
//
// A frame of N bits has 2N edges, numbered e = 0 .. 2N-1. Edge 2k is the LEADING edge of
// bit k -- the edge on which SCLK leaves its idle level -- and 2k+1 is the trailing one.
// With CPOL consumed by "leading means left idle", the mode reduces to CPHA alone:
//
//     CPHA = 0    capture on the leading edge, launch on the trailing edge,
//                 and therefore THE FIRST BIT MUST ALREADY BE ON THE PIN before edge 0,
//                 which means it is placed during the LEAD.
//     CPHA = 1    launch on the leading edge, capture on the trailing edge,
//                 and the pin may idle through the LEAD.
//
// That asymmetry is the single most common driver bug in SPI, because a driver that
// always places the first bit on edge 0 works perfectly in CPHA=1 and loses the first
// bit in CPHA=0 -- and loses it in a way that looks like a slave problem.
//
// FAULT INJECTION IS PART OF THE DESIGN, not an afterthought bolted onto the bench.
//
// Each `fault` code breaks exactly one of the four timing numbers or the edge
// arithmetic, and the bench requires the rule monitor to name exactly the matching rule.
// A driver with an input that makes it wrong in a controlled way is a driver whose
// checker can be trusted, and the alternative -- editing the driver to test the monitor
// and editing it back -- is how a suite acquires a checker nobody has ever seen fire.
//
//     0   no fault
//     1   LEAD shortened to one cycle                  -> R2, the lead
//     2   HALF shortened to one cycle                  -> R3, the half period
//     3   launch and capture edges swapped             -> R4, MOSI in motion at capture
//     4   one edge dropped from the last frame         -> R5, a partial frame
//     5   LAG removed                                  -> R6, the lag
//     6   GAP shortened to one cycle                   -> R7, the gap
//     7   SCLK parked off its idle level in the gap    -> R1, the idle level
//
// R8 -- MISO driven only while selected -- has no entry, and the omission is the point:
// MISO is not this component's pin. A master driver cannot violate R8 and a suite that
// claims otherwise has a monitor watching the wrong agent. Chapter 16.7's passive agent
// is where R8 becomes somebody's responsibility.

`timescale 1ns/1ps

module spi_driver #(
    parameter LEAD  = 4,
    parameter HALF  = 3,
    parameter LAG   = 2,
    parameter GAP   = 3,
    parameter DW    = 32,
    parameter LEN_W = 6,
    parameter CNT_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- the transaction, as a request/accept pair -------------------------
    input  wire              start,
    input  wire [DW-1:0]     tx_data,
    input  wire [LEN_W-1:0]  nbits,
    input  wire              cpol,
    input  wire              cpha,
    input  wire              lsb_first,
    input  wire [2:0]        fault,

    output reg               busy,
    output reg               done,
    output reg  [DW-1:0]     rx_data,

    // --- the pins ----------------------------------------------------------
    output reg               sclk,
    output reg               cs_n,
    output reg               mosi,
    input  wire              miso
);

    localparam S_IDLE  = 0,
                   S_LEADW = 1,
                   S_SHIFT = 2,
                   S_LAGW  = 3;

    reg [1:0]       state;
    reg [CNT_W-1:0] cnt;          // one counter, reused per state
    reg [CNT_W-1:0] idle_cnt;     // cycles of CS high, for the gap
    reg             had_txn;      // the first transaction has no gap before it

    // The latched transaction. A driver that reads its request signals after accepting
    // it is a driver that changes mid-transaction when the generator moves on, and the
    // symptom is a frame whose first half is one mode and second half another.
    reg [DW-1:0]    tx_q;
    reg [LEN_W-1:0] n_q;
    reg             cpol_q, cpha_q, lsb_q;
    reg [2:0]       flt_q;

    reg [LEN_W:0]   ecnt;         // edges emitted so far
    reg [LEN_W:0]   ptr;          // next bit to launch
    reg [LEN_W:0]   cptr;         // next bit to capture

    // --- the effective timing numbers, after fault injection ---------------
    wire [CNT_W-1:0] lead_eff = (flt_q == 3'd1) ? {{(CNT_W-1){1'b0}}, 1'b1} : LEAD[CNT_W-1:0];
    wire [CNT_W-1:0] half_eff = (flt_q == 3'd2) ? {{(CNT_W-1){1'b0}}, 1'b1} : HALF[CNT_W-1:0];
    wire [CNT_W-1:0] lag_eff  = (flt_q == 3'd5) ? {CNT_W{1'b0}}             : LAG[CNT_W-1:0];
    wire [CNT_W-1:0] gap_eff  = (flt_q == 3'd6) ? {{(CNT_W-1){1'b0}}, 1'b1} : GAP[CNT_W-1:0];

    // Fault 3 swaps which edge launches. Everything downstream reads `cpha_lnch`, so
    // the swap is expressed once rather than at every use -- which matters because a
    // fault expressed in two places is a fault that can be half-injected.
    wire cpha_lnch = (flt_q == 3'd3) ? ~cpha_q : cpha_q;

    // Fault 4 drops the final edge, leaving the frame one edge short of whole.
    wire [LEN_W:0] edges_total = ({1'b0, n_q} << 1) - ((flt_q == 3'd4) ? 1'b1 : 1'b0);

    // --- bit selection ------------------------------------------------------
    // One function, used for both directions, so the transmit order and the receive
    // order cannot drift apart. They did in an earlier version and the result was a
    // driver that passed every MSB-first test and reversed every LSB-first one.
        function integer bit_index;
        input integer i;
        begin
            bit_index = lsb_q ? i : (n_q - 1 - i);
        end
    endfunction

    wire idle_lvl = cpol_q;

    // Fault 7 parks SCLK off its idle level for one cycle inside the gap, and returns
    // it before the next select. Both edges therefore happen while deselected, so they
    // are not attributed to any transaction -- which is what makes this fault reach R1
    // and nothing else.
    wire bad_idle = (flt_q == 3'd7) && had_txn && (idle_cnt == {CNT_W{1'b0}});

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state    <= S_IDLE[1:0];
            cnt      <= {CNT_W{1'b0}};
            idle_cnt <= {CNT_W{1'b0}};
            had_txn  <= 1'b0;
            busy     <= 1'b0;
            done     <= 1'b0;
            rx_data  <= {DW{1'b0}};
            sclk     <= 1'b0;
            cs_n     <= 1'b1;
            mosi     <= 1'b0;
            tx_q     <= {DW{1'b0}};
            n_q      <= {LEN_W{1'b0}};
            cpol_q   <= 1'b0;
            cpha_q   <= 1'b0;
            lsb_q    <= 1'b0;
            flt_q    <= 3'd0;
            ecnt     <= {(LEN_W+1){1'b0}};
            ptr      <= {(LEN_W+1){1'b0}};
            cptr     <= {(LEN_W+1){1'b0}};
        end else begin
            done <= 1'b0;

            case (state)

                // ------------------------------------------------------------
                // IDLE. The gap is enforced HERE rather than after the frame,
                // because the obligation is "CS high for GAP cycles before the next
                // select" -- an obligation on the next transaction's start, not on
                // this one's end. Enforcing it as a trailing delay works until a
                // test issues two transactions back to back with a long pause
                // between them, at which point the trailing version wastes the
                // pause and still inserts a gap.
                // ------------------------------------------------------------
                S_IDLE[1:0]: begin
                    // THE IDLE LEVEL FOLLOWS THE LIVE `cpol`, NOT THE LATCHED ONE, and
                    // that distinction cost three spurious rule violations before it was
                    // found.
                    //
                    // Parking at the LATCHED polarity means that after a mode change the
                    // pin sits at the previous mode's idle level until the next
                    // transaction is accepted -- and then moves to the new level in the
                    // same instant CS falls. The rule monitor sees three separate
                    // offences for one defect: R1, because SCLK was off its idle level
                    // while deselected; R2, because an SCLK edge coincident with the
                    // select has a lead of zero; and R5, because that spurious edge makes
                    // the frame's edge count odd. One line, three rules, and none of the
                    // three names the cause.
                    //
                    // A real master behaves the way this line does: the idle level is a
                    // function of the mode register and it takes effect when the mode is
                    // programmed, not when a transfer starts.
                    sclk     <= bad_idle ? ~cpol : cpol;
                    idle_cnt <= idle_cnt + 1'b1;

                    if (start && (!had_txn || ((idle_cnt + 1'b1) >= gap_eff))) begin
                        tx_q   <= tx_data;
                        n_q    <= nbits;
                        cpol_q <= cpol;
                        cpha_q <= cpha;
                        lsb_q  <= lsb_first;
                        flt_q  <= fault;

                        cs_n   <= 1'b0;
                        sclk   <= cpol;      // the new mode's idle level, before the lead
                        busy   <= 1'b1;
                        cnt    <= {CNT_W{1'b0}};
                        ecnt   <= {(LEN_W+1){1'b0}};
                        cptr   <= {(LEN_W+1){1'b0}};
                        rx_data <= {DW{1'b0}};

                        // CPHA=0 needs the first bit on the pin BEFORE edge 0, and the
                        // lead is the only place to put it. CPHA=1 launches on edge 0,
                        // so the pin may idle here.
                        if (!(fault == 3'd3 ? ~cpha : cpha)) begin
                            mosi <= lsb_first ? tx_data[0] : tx_data[nbits - 1];
                            ptr  <= {{LEN_W{1'b0}}, 1'b1};
                        end else begin
                            ptr  <= {(LEN_W+1){1'b0}};
                        end

                        state <= S_LEADW[1:0];
                    end
                end

                // ------------------------------------------------------------
                // LEAD. Counted from the cycle CS went low, and the comparison is
                // `cnt + 1 >= lead_eff` so that a lead of one cycle and a lead of
                // four are the same expression. An earlier version special-cased the
                // minimum and got the boundary wrong in the direction that passes.
                // ------------------------------------------------------------
                S_LEADW[1:0]: begin
                    if ((cnt + 1'b1) >= lead_eff) begin
                        sclk <= ~idle_lvl;           // edge 0
                        ecnt <= {{LEN_W{1'b0}}, 1'b1};
                        cnt  <= {CNT_W{1'b0}};

                        // Edge 0 is a leading edge. It launches if CPHA=1 and captures
                        // if CPHA=0.
                        if (cpha_lnch) begin
                            mosi <= tx_q[bit_index(0)];
                            ptr  <= {{LEN_W{1'b0}}, 1'b1};
                        end
                        if (!cpha_lnch) begin
                            rx_data[bit_index(0)] <= miso;
                            cptr <= {{LEN_W{1'b0}}, 1'b1};
                        end

                        // A frame short enough that edge 0 is also the last edge --
                        // reachable with fault 4 at a one-bit width -- must go straight
                        // to the lag rather than through the shift state, which would
                        // emit an edge that no frame asked for.
                        if (edges_total <= {{LEN_W{1'b0}}, 1'b1})
                            state <= S_LAGW[1:0];
                        else
                            state <= S_SHIFT[1:0];
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                // ------------------------------------------------------------
                // SHIFT. One toggle every half period, `edges_total` of them, with the
                // launch and capture decisions taken from the edge's PARITY rather than
                // from the level of SCLK -- because the level depends on CPOL and the
                // parity does not, and a driver that reasons about levels needs four
                // cases where this needs one.
                // ------------------------------------------------------------
                S_SHIFT[1:0]: begin
                    if ((cnt + 1'b1) >= half_eff) begin
                        cnt  <= {CNT_W{1'b0}};
                        sclk <= ~sclk;
                        ecnt <= ecnt + 1'b1;

                        // ecnt counts edges ALREADY emitted, so the edge being emitted
                        // now has index `ecnt`, and its PARITY decides its role: even is
                        // leading, odd is trailing. Deciding from the parity rather than
                        // from the level of SCLK is what collapses four CPOL/CPHA cases
                        // into one.
                        if (ecnt[0] == 1'b1) begin
                            // trailing edge
                            if (!cpha_lnch && (ptr < {1'b0, n_q})) begin
                                mosi <= tx_q[bit_index(ptr)];
                                ptr  <= ptr + 1'b1;
                            end
                            if (cpha_lnch && (cptr < {1'b0, n_q})) begin
                                rx_data[bit_index(cptr)] <= miso;
                                cptr <= cptr + 1'b1;
                            end
                        end else begin
                            // leading edge
                            if (cpha_lnch && (ptr < {1'b0, n_q})) begin
                                mosi <= tx_q[bit_index(ptr)];
                                ptr  <= ptr + 1'b1;
                            end
                            if (!cpha_lnch && (cptr < {1'b0, n_q})) begin
                                rx_data[bit_index(cptr)] <= miso;
                                cptr <= cptr + 1'b1;
                            end
                        end

                        // THE LAG STARTS AT THE LAST EDGE, and the first version of this
                        // module waited a whole further half period before starting to
                        // count it. Nothing about the pins looked wrong -- the frame was
                        // complete, the trailing level was the idle level, every legal
                        // test passed -- but the lag the pins actually showed was
                        // HALF + LAG, so a lag of zero still measured three cycles and R6
                        // could not be made to fire at all. A checker that cannot be made
                        // to fire has not been verified, it has been assumed, and the
                        // fault-injection matrix is what exposed it.
                        //
                        // After 2N toggles SCLK is back at its idle level, so the final
                        // edge IS the return to idle and there is nothing left to wait
                        // for.
                        if ((ecnt + 1'b1) >= edges_total)
                            state <= S_LAGW[1:0];
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                // ------------------------------------------------------------
                // LAG. `lag_eff` of zero must still terminate, which is why the
                // comparison is on `cnt + 1` and not on `cnt`.
                // ------------------------------------------------------------
                S_LAGW[1:0]: begin
                    if ((cnt + 1'b1) >= lag_eff) begin
                        cs_n     <= 1'b1;
                        busy     <= 1'b0;
                        done     <= 1'b1;
                        had_txn  <= 1'b1;
                        idle_cnt <= {CNT_W{1'b0}};
                        state    <= S_IDLE[1:0];
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                default: state <= S_IDLE[1:0];
            endcase
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_driver.vhd — the same design in VHDL
-- spi_driver.vhd
--
-- Chapter 16.4 -- the driver, which is the component that turns a transaction into pin
-- motion and therefore the component that owns every timing number in the protocol.
--
-- WHAT A DRIVER IS FOR, stated so the shape of this entity is not a surprise.
--
-- A generator produces transactions that say WHAT to send: data, width, mode, bit order.
-- It says nothing about when a pin moves, and it must not, because the moment a generator
-- knows about clock cycles it stops being reusable across every environment that shares
-- the protocol. Everything about WHEN lives here.
--
-- That division is why a driver is worth its own chapter. The driver is where the
-- protocol's timing contract is implemented exactly once, so a bug here is a bug in every
-- test -- which is also why a driver is the one component that must be checked against
-- something other than itself. Chapter 16.1's rule monitor does the checking, and it
-- shares no line of code with this file.
--
-- THE TIMING CONTRACT, in the units the driver counts in.
--
--     LEAD   cycles from CS falling to the first SCLK edge
--     HALF   cycles per SCLK half period
--     LAG    cycles from the last SCLK edge to CS rising
--     GAP    cycles of CS high between transactions
--
-- These are Chapter 14's numbers, and they are why a driver cannot be written as "toggle
-- a clock N times": each of the four is a separate obligation with its own failure mode,
-- and a driver that satisfies three of them passes most tests.
--
-- THE EDGE ARITHMETIC, which is the part that is easy to get subtly wrong.
--
-- A frame of N bits has 2N edges, numbered e = 0 .. 2N-1. Edge 2k is the LEADING edge of
-- bit k -- the edge on which SCLK leaves its idle level -- and 2k+1 is the trailing one.
-- With CPOL consumed by "leading means left idle", the mode reduces to CPHA alone:
--
--     CPHA = 0    capture on the leading edge, launch on the trailing edge, and therefore
--                 THE FIRST BIT MUST ALREADY BE ON THE PIN before edge 0 -- which means
--                 it is placed during the LEAD.
--     CPHA = 1    launch on the leading edge, capture on the trailing edge, and the pin
--                 may idle through the LEAD.
--
-- That asymmetry is the most common driver bug in SPI, because a driver that always places
-- the first bit on edge 0 works perfectly in CPHA=1 and loses the first bit in CPHA=0 --
-- in a way that looks like a slave problem.
--
-- WHAT VHDL CONTRIBUTES HERE, beyond spelling.
--
-- The transaction is a RECORD and the fault code is an ENUMERATION. The record means the
-- request is one port rather than eight, so adding a field costs nothing at any
-- instantiation site; the enumeration means an unhandled fault is a case-statement error
-- at analysis time rather than a silently-inactive injection at run time. The
-- SystemVerilog and Verilog versions encode the fault as a three-bit number because
-- Icarus does not support the alternative here, and the difference is real: a numeric
-- fault code that nobody decodes injects nothing and reports nothing.
--
-- FAULT INJECTION IS PART OF THE DESIGN, not an afterthought bolted onto the bench, and
-- each code breaks exactly one thing:
--
--     F_NONE        no fault
--     F_LEAD        LEAD shortened to one cycle                  -> R2, the lead
--     F_HALF        HALF shortened to one cycle                  -> R3, the half period
--     F_PHASE       launch and capture edges swapped             -> R4, MOSI at capture
--     F_TRUNC       one edge dropped from the frame              -> R5 and R1 (see below)
--     F_LAG         LAG removed                                  -> R6, the lag
--     F_GAP         GAP shortened to one cycle                   -> R7, the gap
--     F_IDLE        SCLK parked off its idle level in the gap     -> R1, the idle level
--
-- F_TRUNC reaches TWO rules and that is arithmetic rather than sloppiness: an odd number
-- of edges cannot return SCLK to its idle level, so a partial frame is necessarily also an
-- idle-level offence. The testbench writes that expectation down rather than treating the
-- second rule as noise.
--
-- R8 -- MISO driven only while selected -- has no fault code, and the omission is the
-- point: MISO is not this component's pin. A master driver cannot violate R8, and a suite
-- that claims otherwise has a monitor watching the wrong agent.

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

package spi_driver_pkg is

    constant DW    : natural  := 32;
    constant LEN_W : positive := 6;

    -- The injectable faults. An enumeration rather than an integer, so that a fault the
    -- driver does not handle is a compile-time error.
    type spi_fault_t is (F_NONE, F_LEAD, F_HALF, F_PHASE, F_TRUNC, F_LAG, F_GAP, F_IDLE);

    -- The request. One port instead of eight, and a ninth field later costs nothing at
    -- any instantiation site.
    type spi_req_t is record
        data      : std_logic_vector(DW - 1 downto 0);
        nbits     : unsigned(LEN_W - 1 downto 0);
        cpol      : std_logic;
        cpha      : std_logic;
        lsb_first : std_logic;
        fault     : spi_fault_t;
    end record;

end package spi_driver_pkg;

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

entity spi_driver is
    generic (
        LEAD : natural := 4;
        HALF : natural := 3;
        LAG  : natural := 2;
        GAP  : natural := 3
    );
    port (
        clk     : in  std_logic;
        rst_n   : in  std_logic;

        start   : in  std_logic;
        req     : in  spi_req_t;

        busy    : out std_logic;
        done    : out std_logic;
        rx_data : out std_logic_vector(DW - 1 downto 0);

        sclk    : out std_logic;
        cs_n    : out std_logic;
        mosi    : out std_logic;
        miso    : in  std_logic
    );
end entity spi_driver;

architecture rtl of spi_driver is

    type state_t is (S_IDLE, S_LEADW, S_SHIFT, S_LAGW);

    signal state    : state_t := S_IDLE;
    signal cnt      : natural := 0;    -- one counter, reused per state
    signal idle_cnt : natural := 0;    -- cycles of CS high, for the gap
    signal had_txn  : boolean := false;

    -- The latched transaction. A driver that reads its request after accepting it is a
    -- driver that changes mid-transaction when the generator moves on, and the symptom is
    -- a frame whose first half is one mode and second half another.
    signal q        : spi_req_t := (data      => (others => '0'),
                                    nbits     => (others => '0'),
                                    cpol      => '0',
                                    cpha      => '0',
                                    lsb_first => '0',
                                    fault     => F_NONE);

    signal ecnt     : natural := 0;    -- edges emitted so far
    signal ptr      : natural := 0;    -- next bit to launch
    signal cptr     : natural := 0;    -- next bit to capture

    signal sclk_r   : std_logic := '0';
    signal cs_n_r   : std_logic := '1';
    signal mosi_r   : std_logic := '0';
    signal rx_r     : std_logic_vector(DW - 1 downto 0) := (others => '0');
    signal busy_r   : std_logic := '0';
    signal done_r   : std_logic := '0';

    -- The effective timing numbers, after fault injection.
    function eff (base : natural; floor_v : natural; hit : boolean) return natural is
    begin
        if hit then return floor_v; else return base; end if;
    end function eff;

    -- Which edge launches. Everything downstream reads this, so the swap is expressed
    -- once -- a fault expressed in two places is a fault that can be half-injected.
    function launch_on_leading (r : spi_req_t) return boolean is
    begin
        if r.fault = F_PHASE then
            return r.cpha = '0';
        else
            return r.cpha = '1';
        end if;
    end function launch_on_leading;

    -- One bit-index function, used for both directions, so the transmit order and the
    -- receive order cannot drift apart. They did in an earlier version, and the result was
    -- a driver that passed every MSB-first test and reversed every LSB-first one.
    function bit_index (i : natural; r : spi_req_t) return natural is
    begin
        if r.lsb_first = '1' then
            return i;
        else
            return to_integer(r.nbits) - 1 - i;
        end if;
    end function bit_index;

begin

    sclk    <= sclk_r;
    cs_n    <= cs_n_r;
    mosi    <= mosi_r;
    rx_data <= rx_r;
    busy    <= busy_r;
    done    <= done_r;

    process (clk, rst_n) is
        variable lead_eff, half_eff, lag_eff, gap_eff : natural;
        variable edges_total                          : natural;
        variable lnch_lead                            : boolean;
        variable bad_idle                             : boolean;
    begin
        if rst_n = '0' then
            state    <= S_IDLE;
            cnt      <= 0;
            idle_cnt <= 0;
            had_txn  <= false;
            ecnt     <= 0;
            ptr      <= 0;
            cptr     <= 0;
            sclk_r   <= '0';
            cs_n_r   <= '1';
            mosi_r   <= '0';
            rx_r     <= (others => '0');
            busy_r   <= '0';
            done_r   <= '0';
            q        <= (data => (others => '0'), nbits => (others => '0'),
                         cpol => '0', cpha => '0', lsb_first => '0', fault => F_NONE);

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

            lead_eff    := eff(LEAD, 1, q.fault = F_LEAD);
            half_eff    := eff(HALF, 1, q.fault = F_HALF);
            lag_eff     := eff(LAG,  0, q.fault = F_LAG);
            gap_eff     := eff(GAP,  1, q.fault = F_GAP);
            lnch_lead   := launch_on_leading(q);
            edges_total := 2 * to_integer(q.nbits);
            if q.fault = F_TRUNC and edges_total > 0 then
                edges_total := edges_total - 1;
            end if;
            bad_idle := (q.fault = F_IDLE) and had_txn and (idle_cnt = 0);

            case state is

                -- --------------------------------------------------------------
                -- IDLE. The gap is enforced HERE rather than as a trailing delay after
                -- the frame, because the obligation is "CS high for GAP cycles before the
                -- NEXT select" -- an obligation on the next transaction's start. Enforced
                -- as a trailing delay it works until a test issues two transactions with
                -- a long pause between them, at which point it wastes the pause and still
                -- inserts a gap.
                -- --------------------------------------------------------------
                when S_IDLE =>
                    -- THE IDLE LEVEL FOLLOWS THE LIVE `req.cpol`, NOT THE LATCHED ONE, and
                    -- that distinction cost three spurious rule violations before it was
                    -- found. Parking at the latched polarity leaves the pin at the
                    -- PREVIOUS mode's idle level after a mode change, and then moves it in
                    -- the same instant CS falls -- at which point the rule monitor reports
                    -- three separate offences for one defect: R1 because SCLK was off idle
                    -- while deselected, R2 because an edge coincident with the select has a
                    -- lead of zero, and R5 because that spurious edge makes the frame's
                    -- edge count odd. One line, three rules, and none of them names the
                    -- cause.
                    --
                    -- A real master behaves the way this line does: the idle level is a
                    -- function of the mode register and takes effect when the mode is
                    -- programmed, not when a transfer starts.
                    if bad_idle then
                        sclk_r <= not req.cpol;
                    else
                        sclk_r <= req.cpol;
                    end if;
                    idle_cnt <= idle_cnt + 1;

                    if start = '1' and ((not had_txn) or (idle_cnt + 1 >= gap_eff)) then
                        q       <= req;
                        cs_n_r  <= '0';
                        sclk_r  <= req.cpol;
                        busy_r  <= '1';
                        cnt     <= 0;
                        ecnt    <= 0;
                        cptr    <= 0;
                        rx_r    <= (others => '0');

                        -- CPHA=0 needs the first bit on the pin BEFORE edge 0, and the
                        -- lead is the only place to put it. CPHA=1 launches on edge 0, so
                        -- the pin may idle here.
                        if not launch_on_leading(req) then
                            mosi_r <= req.data(bit_index(0, req));
                            ptr    <= 1;
                        else
                            ptr    <= 0;
                        end if;

                        state <= S_LEADW;
                    end if;

                -- --------------------------------------------------------------
                -- LEAD. Counted from the cycle CS went low, and the comparison is
                -- `cnt + 1 >= lead_eff` so that a lead of one cycle and a lead of four are
                -- the same expression. An earlier version special-cased the minimum and
                -- got the boundary wrong in the direction that passes.
                -- --------------------------------------------------------------
                when S_LEADW =>
                    if cnt + 1 >= lead_eff then
                        sclk_r <= not q.cpol;            -- edge 0
                        ecnt   <= 1;
                        cnt    <= 0;

                        -- Edge 0 is a leading edge: it launches if CPHA=1, captures if not.
                        if lnch_lead then
                            mosi_r <= q.data(bit_index(0, q));
                            ptr    <= 1;
                        else
                            rx_r(bit_index(0, q)) <= miso;
                            cptr   <= 1;
                        end if;

                        -- A frame short enough that edge 0 is also the last edge --
                        -- reachable with F_TRUNC at a one-bit width -- must go straight to
                        -- the lag rather than through the shift state, which would emit an
                        -- edge no frame asked for.
                        if edges_total <= 1 then
                            state <= S_LAGW;
                        else
                            state <= S_SHIFT;
                        end if;
                    else
                        cnt <= cnt + 1;
                    end if;

                -- --------------------------------------------------------------
                -- SHIFT. One toggle every half period, with the launch and capture
                -- decisions taken from the edge's PARITY rather than from the level of
                -- SCLK -- because the level depends on CPOL and the parity does not, and a
                -- driver that reasons about levels needs four cases where this needs one.
                -- --------------------------------------------------------------
                when S_SHIFT =>
                    if cnt + 1 >= half_eff then
                        cnt    <= 0;
                        sclk_r <= not sclk_r;
                        ecnt   <= ecnt + 1;

                        if (ecnt mod 2) = 1 then
                            -- trailing edge
                            if (not lnch_lead) and ptr < to_integer(q.nbits) then
                                mosi_r <= q.data(bit_index(ptr, q));
                                ptr    <= ptr + 1;
                            end if;
                            if lnch_lead and cptr < to_integer(q.nbits) then
                                rx_r(bit_index(cptr, q)) <= miso;
                                cptr   <= cptr + 1;
                            end if;
                        else
                            -- leading edge
                            if lnch_lead and ptr < to_integer(q.nbits) then
                                mosi_r <= q.data(bit_index(ptr, q));
                                ptr    <= ptr + 1;
                            end if;
                            if (not lnch_lead) and cptr < to_integer(q.nbits) then
                                rx_r(bit_index(cptr, q)) <= miso;
                                cptr   <= cptr + 1;
                            end if;
                        end if;

                        -- THE LAG STARTS AT THE LAST EDGE, and the first version of this
                        -- module waited a whole further half period before starting to
                        -- count it. Nothing about the pins looked wrong -- the frame was
                        -- complete, the trailing level was the idle level, every legal
                        -- test passed -- but the lag the pins actually showed was
                        -- HALF + LAG, so a lag of zero still measured three cycles and R6
                        -- could not be made to fire at all. A checker that cannot be made
                        -- to fire has not been verified, it has been assumed, and the
                        -- fault matrix is what exposed it.
                        --
                        -- After 2N toggles SCLK is back at its idle level, so the final
                        -- edge IS the return to idle and there is nothing left to wait for.
                        if ecnt + 1 >= edges_total then
                            state <= S_LAGW;
                        end if;
                    else
                        cnt <= cnt + 1;
                    end if;

                -- --------------------------------------------------------------
                -- LAG. A lag of zero must still terminate, which is why the comparison is
                -- on `cnt + 1` and not on `cnt`.
                -- --------------------------------------------------------------
                when S_LAGW =>
                    if cnt + 1 >= lag_eff then
                        cs_n_r   <= '1';
                        busy_r   <= '0';
                        done_r   <= '1';
                        had_txn  <= true;
                        idle_cnt <= 0;
                        state    <= S_IDLE;
                    else
                        cnt <= cnt + 1;
                    end if;

            end case;
        end if;
    end process;

end architecture rtl;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_driver_tb.sv — legal traffic against Chapter 16.1's rule monitor, plus a seven-fault expectation matrix
// spi_driver_tb.sv
//
// The driver is checked by CHAPTER 16.1'S RULE MONITOR, instantiated here unmodified on
// the same pins. That is the whole architecture of this bench and it is worth saying why
// before reading any of it.
//
// A driver cannot check itself. Every self-check a driver can perform is a restatement of
// its own arithmetic, so a driver that computes its lead wrongly will also assert that
// its lead is correct, with total conviction. The rule monitor was written in a different
// chapter, for a different purpose, from the protocol's obligations rather than from any
// implementation -- and it consumes nothing but pins. Two components that share no code
// agreeing about the same wires is evidence.
//
// THERE ARE THREE MEASUREMENTS, and the third is the one that makes the first two mean
// something.
//
//   1. LEGAL TRAFFIC VIOLATES NOTHING. Sixteen configurations -- both polarities, both
//      phases, two frame widths, both bit orders -- produce zero violations of any of
//      the eight rules.
//
//   2. LEGAL TRAFFIC EXERCISES EVERYTHING. All eight rules report a non-zero exercise
//      count. Without this, measurement 1 is satisfied by a driver that never moves a
//      pin, and by a monitor whose rules are all unreachable.
//
//   3. EACH INJECTED FAULT VIOLATES EXACTLY THE RULES PREDICTED FOR IT. Seven faults, and
//      for each one a written-down SET of rules that must fire and a requirement that
//      nothing outside the set does. Six of the sets have one member; fault 4's has two,
//      for a reason that is arithmetic rather than sloppiness and is argued where it is
//      declared. A checker that fires for its own violation is half-verified; a checker
//      that fires for its own violation AND FOR NOTHING ELSE is one whose report can be
//      read as a diagnosis rather than as a hint.
//
// AND ONE INDEPENDENT CHECK THAT DOES NOT COME FROM THE MONITOR.
//
// MISO is driven as the complement of MOSI -- a loopback slave that returns what it was
// given, inverted. The driver's received word must therefore be the complement of the
// word it sent, in both phases, which tests something the rule monitor cannot see: that
// the driver samples MISO on the CAPTURE edge of the mode it was given. A driver that
// samples on the launch edge instead produces pins that satisfy all eight rules and a
// received word that is garbage, which is exactly the class of bug a pin-level checker is
// blind to and a data check catches instantly.
//
// THE FAULT-TO-RULE MAP is a claim about the protocol, not about this code, and that is
// why it is written out here and asserted rather than left implicit:
//
//     fault 1  lead shortened       -> R2  the lead
//     fault 2  half period shortened-> R3  the half period
//     fault 3  launch/capture swap  -> R4  MOSI in motion at a capture edge
//     fault 4  final edge dropped   -> R5  a partial frame, AND R1 the idle level, because
//                                          an odd number of edges cannot return SCLK to
//                                          its idle level. The two rules are coupled by
//                                          arithmetic, not by a defect in either checker,
//                                          and the expectation below says so explicitly.
//     fault 5  lag removed          -> R6  the lag
//     fault 6  gap shortened        -> R7  the gap
//     fault 7  SCLK parked in gap   -> R1  the idle level
//
// R8 is absent from that map on purpose: MISO is not the master driver's pin, so no
// master fault can reach it, and a suite that reports R8 against a master driver has a
// monitor pointed at the wrong agent.

`timescale 1ns/1ps

module spi_driver_tb;

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

    localparam int R_IDLE = 0, R_LEAD = 1, R_HALF = 2, R_LAUNCH = 3,
                   R_FRAME = 4, R_LAG = 5, R_GAP = 6, R_DRIVE = 7;

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

    reg              start = 1'b0;
    reg  [DW-1:0]    tx_data = {DW{1'b0}};
    reg  [LEN_W-1:0] nbits = 6'd8;
    reg              cpol = 1'b0, cpha = 1'b0, lsb_first = 1'b0;
    reg  [2:0]       fault = 3'd0;

    wire             busy, done;
    wire [DW-1:0]    rx_data;
    wire             sclk, cs_n, mosi;

    // The loopback slave: the complement of MOSI, continuously. It is not a model of
    // anything real and does not need to be -- its only job is to make the received word
    // a known function of the sent word so that the capture edge can be checked.
    wire miso = ~mosi;

    // A bus monitor's view of whether MISO is driven at all. Tied to the select, which
    // is legal by construction: this bench contains no slave that could get it wrong,
    // and R8 therefore reports zero violations for a reason the log states rather than
    // leaves to be assumed.
    wire miso_driven = ~cs_n;

    spi_driver #(
        .LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
        .DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)
    ) u_drv (
        .clk(clk), .rst_n(rst_n),
        .start(start), .tx_data(tx_data), .nbits(nbits),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
        .busy(busy), .done(done), .rx_data(rx_data),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso)
    );

    reg clr = 1'b0;
    wire [NRULES*CNT_W-1:0] exercised_flat, violated_flat;

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

    function automatic [CNT_W-1:0] exercised(input integer i);
        begin exercised = exercised_flat[i*CNT_W +: CNT_W]; end
    endfunction
    function automatic [CNT_W-1:0] violated(input integer i);
        begin violated = violated_flat[i*CNT_W +: CNT_W]; end
    endfunction

    integer errors = 0;

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

    // ------------------------------------------------------------------
    // Every wait in this bench is on the NEGEDGE -- Chapter 16.3's discipline, applied.
    // The monitor samples on the posedge; a bench that drove on the posedge too would be
    // measuring its own evaluation order, and Chapter 16.1's first version did exactly
    // that and reported zero exercises for every in-transaction rule while the waveform
    // looked perfect.
    // ------------------------------------------------------------------

    // A CONFIGURATION CHANGE IS NOT A PROTOCOL EVENT, AND THE CHECKER HAS TO BE TOLD.
    //
    // The monitor judges the pins against the `cpol` it is handed. When that input
    // changes, the pin it is judging has not moved yet -- the driver needs a cycle to
    // re-park SCLK at the new idle level -- so for that one cycle a correct driver looks
    // like a violation of R1. Nothing is wrong with either component: a pin-level checker
    // cannot distinguish a reconfiguration from an offence, because from the pins the two
    // are identical.
    //
    // So reconfiguration is bracketed: change the mode, let the pins settle, and only
    // then clear the counters and begin measuring. The alternative -- accumulating across
    // the change -- produces a suite with a small permanent violation count that everyone
    // learns to ignore, which is how a real violation gets ignored too.
    //
    // This is the pin-level form of a problem that reappears in Chapter 16.6: a checker
    // needs to know when the thing it is checking against has been re-specified.
    task automatic set_cfg(input [LEN_W-1:0] n, input integer pol, input integer pha,
                           input integer lsb, input [2:0] f, input [DW-1:0] d);
        begin
            @(negedge clk);
            nbits     = n;
            cpol      = pol[0];
            cpha      = pha[0];
            lsb_first = lsb[0];
            fault     = f;
            tx_data   = d;
            repeat (6) @(negedge clk);     // let the driver re-park SCLK
        end
    endtask

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

    // A BURST, NOT A SEQUENCE OF WAITED TRANSACTIONS, and the difference decides whether
    // the gap rule can be tested at all.
    //
    // An earlier version asserted `start` only after the previous transaction had
    // reported done and the bench had stepped a cycle. The driver was therefore always
    // asked to start LATER than its own gap logic would have allowed, the gap on the pins
    // was whatever the bench's handshake happened to produce, and shortening the driver's
    // gap to one cycle changed nothing observable -- R7 could not be made to fire.
    //
    // Holding `start` high across the burst hands the timing back to the component that
    // owns it. The gap on the pins is then the driver's gap, which is the only version
    // worth checking.
    task automatic run_burst(input integer ntxn);
        integer k;
        begin
            k = 0;
            @(negedge clk);
            start = 1'b1;
            while (k < ntxn) begin
                @(negedge clk);
                if (done) begin
                    k = k + 1;
                    if (k == ntxn) start = 1'b0;
                end
            end
            repeat (GAP + LAG + 6) @(negedge clk);
        end
    endtask

    // ------------------------------------------------------------------
    // Measurement 1 and 2: legal traffic across every configuration.
    // ------------------------------------------------------------------
    integer ipol, ipha, ilsb, iw, r;
    integer legal_txns, legal_cfgs;
    reg [DW-1:0] want;
    reg [LEN_W-1:0] w;
    integer rx_bad;
    integer tot_ex [0:NRULES-1];
    integer tot_vi [0:NRULES-1];

    // ------------------------------------------------------------------
    // Measurement 3: the fault matrix.
    // ------------------------------------------------------------------
    integer f, expect_rule, other, fc;
    reg [NRULES-1:0] fired, expect_set;
    integer matrix_bad, diag_bad;

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

        // ============================================================
        // 1 + 2. LEGAL TRAFFIC.
        //
        // One measurement WINDOW per configuration: set the mode, let the pins settle,
        // clear the counters, run a burst of two transactions -- two, because the gap and
        // the idle level are obligations BETWEEN transactions and one cannot exercise
        // them -- then read the counters and fold them into the totals.
        // ============================================================
        legal_txns = 0;
        legal_cfgs = 0;
        rx_bad     = 0;
        for (r = 0; r < NRULES; r = r + 1) begin
            tot_ex[r] = 0;
            tot_vi[r] = 0;
        end

        for (iw = 0; iw < 2; iw = iw + 1) begin
            w = (iw == 0) ? 6'd8 : 6'd13;
            for (ipol = 0; ipol < 2; ipol = ipol + 1)
            for (ipha = 0; ipha < 2; ipha = ipha + 1)
            for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
                // A pattern with changes in it, because R4 is exercised only by MOSI
                // actually moving: a suite that sends 0x00 has not tested it at all.
                set_cfg(w, ipol, ipha, ilsb, 3'd0, 32'h0000_1A5C);
                clear_counts();
                run_burst(2);
                legal_txns = legal_txns + 2;
                legal_cfgs = legal_cfgs + 1;

                for (r = 0; r < NRULES; r = r + 1) begin
                    tot_ex[r] = tot_ex[r] + exercised(r);
                    tot_vi[r] = tot_vi[r] + violated(r);
                    if (violated(r) != 0)
                        $display("  FAIL: legal traffic violated R%0d %0d time(s) at cpol=%0d cpha=%0d lsb=%0d n=%0d",
                                 r + 1, violated(r), ipol, ipha, ilsb, w);
                    if (violated(r) != 0) errors = errors + 1;
                end

                want = (~32'h0000_1A5C) & ((32'h1 << w) - 32'h1);
                if ((rx_data & ((32'h1 << w) - 32'h1)) !== want) begin
                    $display("  FAIL: rx mismatch at cpol=%0d cpha=%0d lsb=%0d n=%0d -- got %h want %h; the driver is not sampling MISO on the capture edge of this mode",
                             ipol, ipha, ilsb, w, rx_data & ((32'h1 << w) - 32'h1), want);
                    rx_bad = rx_bad + 1;
                    errors = errors + 1;
                end
            end
        end

        $display("  legal traffic: %0d transactions in %0d measurement windows (2 widths x 2 polarities x 2 phases x 2 bit orders)",
                 legal_txns, legal_cfgs);
        $display("  rule  exercised  violated");
        for (r = 0; r < NRULES; r = r + 1)
            $display("  R%0d    %9d  %8d", r + 1, tot_ex[r], tot_vi[r]);

        for (r = 0; r < NRULES; r = r + 1)
            if (tot_ex[r] == 0) begin
                $display("  FAIL: legal traffic never exercised R%0d, so its zero violation count means nothing",
                         r + 1);
                errors = errors + 1;
            end
        $display("    1. legal traffic violated none of the eight rules in any of the %0d configurations", legal_cfgs);
        $display("    2. and exercised all eight, so the zeros above are measurements rather than silence -- a driver that never moved a pin would satisfy measurement 1 and fail this one");
        $display("    and the received word was the complement of the sent word in every configuration (%0d mismatches), which tests what the pin monitor cannot see: that MISO is sampled on the CAPTURE edge of the mode in force. A driver that sampled on the launch edge would satisfy all eight rules and return garbage",
                 rx_bad);

        // ============================================================
        // 3. THE FAULT MATRIX.
        //
        // Each fault is run in TWO configurations, in its own measurement window, and the
        // rules that fired are OR-ed across both. Two, because a fault that only shows in
        // one phase is a fault whose rule is phase-dependent, and a single-mode matrix
        // would hide that.
        // ============================================================
        matrix_bad = 0;
        diag_bad   = 0;
        $display("  fault  breaks                      expected  rules that fired");
        for (f = 1; f <= 7; f = f + 1) begin
            case (f)
                1: begin expect_rule = R_LEAD;   expect_set = (1 << R_LEAD);   end
                2: begin expect_rule = R_HALF;   expect_set = (1 << R_HALF);   end
                3: begin expect_rule = R_LAUNCH; expect_set = (1 << R_LAUNCH); end
                // FAULT 4 IS EXPECTED TO FIRE TWO RULES, and the second one is not a
                // checker defect -- it is arithmetic.
                //
                // A frame of 2N edges returns SCLK to its idle level because an even
                // number of toggles does. Drop one edge and the count is odd, so SCLK is
                // NECESSARILY parked off its idle level when CS rises: R5, the partial
                // frame, and R1, the idle level, are the same fault seen twice.
                //
                // Writing the expectation as a single rule and calling the extra one
                // noise would have been the easy move, and it would have taught the wrong
                // thing. A diagonal fault matrix is itself a CLAIM -- that each fault has
                // exactly one pin-observable consequence -- and that claim is false here
                // for a reason worth knowing: when a report shows R1 and R5 together, an
                // engineer should look for ONE truncated frame, not two unrelated bugs.
                4: begin expect_rule = R_FRAME;  expect_set = (1 << R_FRAME) | (1 << R_IDLE); end
                5: begin expect_rule = R_LAG;    expect_set = (1 << R_LAG);    end
                6: begin expect_rule = R_GAP;    expect_set = (1 << R_GAP);    end
                default: begin expect_rule = R_IDLE; expect_set = (1 << R_IDLE); end
            endcase

            fired = {NRULES{1'b0}};
            for (fc = 0; fc < 2; fc = fc + 1) begin
                // Three transactions per window: the gap and the idle level are
                // obligations between transactions, and a burst of three exercises them
                // twice.
                set_cfg(6'd8, fc, fc, 0, f[2:0], (fc == 0) ? 32'h0000_1A5C : 32'h0000_0C3A);
                clear_counts();
                run_burst(3);
                for (r = 0; r < NRULES; r = r + 1)
                    if (violated(r) != 0) fired[r] = 1'b1;
            end

            $display("  %5d  %-26s  %b  %b", f,
                     (f == 1) ? "the lead" :
                     (f == 2) ? "the half period" :
                     (f == 3) ? "which edge launches" :
                     (f == 4) ? "the frame's last edge" :
                     (f == 5) ? "the lag" :
                     (f == 6) ? "the gap" : "the idle level",
                     expect_set, fired);

            for (other = 0; other < NRULES; other = other + 1) begin
                if (expect_set[other] === 1'b1 && fired[other] !== 1'b1) begin
                    $display("  FAIL: fault %0d did not make R%0d fire, so that rule is either unreachable or wrong -- and a rule that cannot be made to fire has been assumed, not verified",
                             f, other + 1);
                    errors   = errors + 1;
                    diag_bad = diag_bad + 1;
                end
                if (expect_set[other] !== 1'b1 && fired[other] === 1'b1) begin
                    $display("  FAIL: fault %0d also fired R%0d, which is not one of its expected consequences, so the monitor's report cannot be read as a diagnosis",
                             f, other + 1);
                    errors     = errors + 1;
                    matrix_bad = matrix_bad + 1;
                end
            end
        end

        $display("    3. every one of the seven faults fired EXACTLY the rules predicted for it and no others: %0d predicted rules that stayed silent, %0d rules that fired unpredicted. Six faults map to one rule each; fault 4 maps to two, because an odd edge count cannot return SCLK to its idle level, so a partial frame is necessarily also an idle-level offence -- the same fault seen twice rather than a checker defect. A monitor that fires for its own violation is half-verified; one that fires for exactly its own violations produces a report that names the defect instead of hinting at it",
                 diag_bad, matrix_bad);
        $display("    and R8 appears in no row, because MISO is not a master driver's pin. There is no master fault that can reach it, and a suite that reports R8 against a master has a monitor pointed at the wrong agent -- which Chapter 16.7 fixes by giving the passive agent its own");

        if (errors == 0)
            $display("PASS: a driver cannot check itself, because every self-check it can perform restates the arithmetic under test -- a driver that computes its lead wrongly asserts that its lead is correct with total conviction. So this driver is checked by Chapter 16.1's rule monitor, written in another chapter from the protocol's obligations rather than from any implementation, consuming nothing but pins, and sharing no line of code with it. Across %0d legal transactions in %0d measurement windows -- both polarities, both phases, two frame widths, both bit orders -- the pins violated none of the eight rules AND exercised all eight, which is the pair of results that makes the zeros a measurement instead of a silence: a driver that never moved a pin would pass the first half. The received word was the complement of the sent word in every configuration, which is the check the pin monitor is structurally blind to and which catches a driver that samples MISO on the launch edge -- pins perfectly legal, data garbage. And each of the seven injected faults fired EXACTLY the rules written down for it and no others -- %0d predicted rules silent, %0d unpredicted rules firing -- which is what separates a checker whose report is a diagnosis from one whose report is a hint; six faults map to a single rule and the seventh maps to two, because an odd edge count cannot return SCLK to its idle level, so a partial frame is also an idle-level offence by arithmetic and a report showing both should be read as ONE fault. The division of labour is the lesson: the generator owns WHAT, the driver owns WHEN, and every timing number in the protocol is implemented exactly once -- which is why a bug here is a bug in every test and why this is the one component that must be checked against something that was not written to agree with it",
                     legal_txns, legal_cfgs, diag_bad, matrix_bad);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_driver_tb.v — the same bench in Verilog-2001
// spi_driver_tb.v
//
// The driver is checked by CHAPTER 16.1'S RULE MONITOR, instantiated here unmodified on
// the same pins. That is the whole architecture of this bench and it is worth saying why
// before reading any of it.
//
// A driver cannot check itself. Every self-check a driver can perform is a restatement of
// its own arithmetic, so a driver that computes its lead wrongly will also assert that
// its lead is correct, with total conviction. The rule monitor was written in a different
// chapter, for a different purpose, from the protocol's obligations rather than from any
// implementation -- and it consumes nothing but pins. Two components that share no code
// agreeing about the same wires is evidence.
//
// THERE ARE THREE MEASUREMENTS, and the third is the one that makes the first two mean
// something.
//
//   1. LEGAL TRAFFIC VIOLATES NOTHING. Sixteen configurations -- both polarities, both
//      phases, two frame widths, both bit orders -- produce zero violations of any of
//      the eight rules.
//
//   2. LEGAL TRAFFIC EXERCISES EVERYTHING. All eight rules report a non-zero exercise
//      count. Without this, measurement 1 is satisfied by a driver that never moves a
//      pin, and by a monitor whose rules are all unreachable.
//
//   3. EACH INJECTED FAULT VIOLATES EXACTLY THE RULES PREDICTED FOR IT. Seven faults, and
//      for each one a written-down SET of rules that must fire and a requirement that
//      nothing outside the set does. Six of the sets have one member; fault 4's has two,
//      for a reason that is arithmetic rather than sloppiness and is argued where it is
//      declared. A checker that fires for its own violation is half-verified; a checker
//      that fires for its own violation AND FOR NOTHING ELSE is one whose report can be
//      read as a diagnosis rather than as a hint.
//
// AND ONE INDEPENDENT CHECK THAT DOES NOT COME FROM THE MONITOR.
//
// MISO is driven as the complement of MOSI -- a loopback slave that returns what it was
// given, inverted. The driver's received word must therefore be the complement of the
// word it sent, in both phases, which tests something the rule monitor cannot see: that
// the driver samples MISO on the CAPTURE edge of the mode it was given. A driver that
// samples on the launch edge instead produces pins that satisfy all eight rules and a
// received word that is garbage, which is exactly the class of bug a pin-level checker is
// blind to and a data check catches instantly.
//
// THE FAULT-TO-RULE MAP is a claim about the protocol, not about this code, and that is
// why it is written out here and asserted rather than left implicit:
//
//     fault 1  lead shortened       -> R2  the lead
//     fault 2  half period shortened-> R3  the half period
//     fault 3  launch/capture swap  -> R4  MOSI in motion at a capture edge
//     fault 4  final edge dropped   -> R5  a partial frame, AND R1 the idle level, because
//                                          an odd number of edges cannot return SCLK to
//                                          its idle level. The two rules are coupled by
//                                          arithmetic, not by a defect in either checker,
//                                          and the expectation below says so explicitly.
//     fault 5  lag removed          -> R6  the lag
//     fault 6  gap shortened        -> R7  the gap
//     fault 7  SCLK parked in gap   -> R1  the idle level
//
// R8 is absent from that map on purpose: MISO is not the master driver's pin, so no
// master fault can reach it, and a suite that reports R8 against a master driver has a
// monitor pointed at the wrong agent.

`timescale 1ns/1ps

module spi_driver_tb;

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

    localparam R_IDLE = 0, R_LEAD = 1, R_HALF = 2, R_LAUNCH = 3,
                   R_FRAME = 4, R_LAG = 5, R_GAP = 6, R_DRIVE = 7;

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

    reg              start;
    reg  [DW-1:0]    tx_data;
    reg  [LEN_W-1:0] nbits;
    reg              cpol, cpha, lsb_first;
    reg  [2:0]       fault;

    wire             busy, done;
    wire [DW-1:0]    rx_data;
    wire             sclk, cs_n, mosi;

    // The loopback slave: the complement of MOSI, continuously. It is not a model of
    // anything real and does not need to be -- its only job is to make the received word
    // a known function of the sent word so that the capture edge can be checked.
    wire miso = ~mosi;

    // A bus monitor's view of whether MISO is driven at all. Tied to the select, which
    // is legal by construction: this bench contains no slave that could get it wrong,
    // and R8 therefore reports zero violations for a reason the log states rather than
    // leaves to be assumed.
    wire miso_driven = ~cs_n;

    spi_driver #(
        .LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
        .DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)
    ) u_drv (
        .clk(clk), .rst_n(rst_n),
        .start(start), .tx_data(tx_data), .nbits(nbits),
        .cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
        .busy(busy), .done(done), .rx_data(rx_data),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso)
    );

    reg clr;
    wire [NRULES*CNT_W-1:0] exercised_flat, violated_flat;

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

        function [CNT_W-1:0] exercised;
        input integer i;
        begin exercised = exercised_flat[i*CNT_W +: CNT_W]; end
    endfunction
        function [CNT_W-1:0] violated;
        input integer i;
        begin violated = violated_flat[i*CNT_W +: CNT_W]; end
    endfunction

    integer errors;

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

    // ------------------------------------------------------------------
    // Every wait in this bench is on the NEGEDGE -- Chapter 16.3's discipline, applied.
    // The monitor samples on the posedge; a bench that drove on the posedge too would be
    // measuring its own evaluation order, and Chapter 16.1's first version did exactly
    // that and reported zero exercises for every in-transaction rule while the waveform
    // looked perfect.
    // ------------------------------------------------------------------

    // A CONFIGURATION CHANGE IS NOT A PROTOCOL EVENT, AND THE CHECKER HAS TO BE TOLD.
    //
    // The monitor judges the pins against the `cpol` it is handed. When that input
    // changes, the pin it is judging has not moved yet -- the driver needs a cycle to
    // re-park SCLK at the new idle level -- so for that one cycle a correct driver looks
    // like a violation of R1. Nothing is wrong with either component: a pin-level checker
    // cannot distinguish a reconfiguration from an offence, because from the pins the two
    // are identical.
    //
    // So reconfiguration is bracketed: change the mode, let the pins settle, and only
    // then clear the counters and begin measuring. The alternative -- accumulating across
    // the change -- produces a suite with a small permanent violation count that everyone
    // learns to ignore, which is how a real violation gets ignored too.
    //
    // This is the pin-level form of a problem that reappears in Chapter 16.6: a checker
    // needs to know when the thing it is checking against has been re-specified.
        task set_cfg;
        input [LEN_W-1:0] n;
        input integer pol;
        input integer pha;
        input integer lsb;
        input [2:0] f;
        input [DW-1:0] d;
        begin
            @(negedge clk);
            nbits     = n;
            cpol      = pol[0];
            cpha      = pha[0];
            lsb_first = lsb[0];
            fault     = f;
            tx_data   = d;
            repeat (6) @(negedge clk);     // let the driver re-park SCLK
        end
    endtask

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

    // A BURST, NOT A SEQUENCE OF WAITED TRANSACTIONS, and the difference decides whether
    // the gap rule can be tested at all.
    //
    // An earlier version asserted `start` only after the previous transaction had
    // reported done and the bench had stepped a cycle. The driver was therefore always
    // asked to start LATER than its own gap logic would have allowed, the gap on the pins
    // was whatever the bench's handshake happened to produce, and shortening the driver's
    // gap to one cycle changed nothing observable -- R7 could not be made to fire.
    //
    // Holding `start` high across the burst hands the timing back to the component that
    // owns it. The gap on the pins is then the driver's gap, which is the only version
    // worth checking.
        task run_burst;
        input integer ntxn;
        integer k;
        begin
            k = 0;
            @(negedge clk);
            start = 1'b1;
            while (k < ntxn) begin
                @(negedge clk);
                if (done) begin
                    k = k + 1;
                    if (k == ntxn) start = 1'b0;
                end
            end
            repeat (GAP + LAG + 6) @(negedge clk);
        end
    endtask

    // ------------------------------------------------------------------
    // Measurement 1 and 2: legal traffic across every configuration.
    // ------------------------------------------------------------------
    integer ipol, ipha, ilsb, iw, r;
    integer legal_txns, legal_cfgs;
    reg [DW-1:0] want;
    reg [LEN_W-1:0] w;
    integer rx_bad;
    integer tot_ex [0:NRULES-1];
    integer tot_vi [0:NRULES-1];

    // ------------------------------------------------------------------
    // Measurement 3: the fault matrix.
    // ------------------------------------------------------------------
    integer f, expect_rule, other, fc;
    reg [NRULES-1:0] fired, expect_set;
    integer matrix_bad, diag_bad;

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

        // ============================================================
        // 1 + 2. LEGAL TRAFFIC.
        //
        // One measurement WINDOW per configuration: set the mode, let the pins settle,
        // clear the counters, run a burst of two transactions -- two, because the gap and
        // the idle level are obligations BETWEEN transactions and one cannot exercise
        // them -- then read the counters and fold them into the totals.
        // ============================================================
        legal_txns = 0;
        legal_cfgs = 0;
        rx_bad     = 0;
        for (r = 0; r < NRULES; r = r + 1) begin
            tot_ex[r] = 0;
            tot_vi[r] = 0;
        end

        for (iw = 0; iw < 2; iw = iw + 1) begin
            w = (iw == 0) ? 6'd8 : 6'd13;
            for (ipol = 0; ipol < 2; ipol = ipol + 1)
            for (ipha = 0; ipha < 2; ipha = ipha + 1)
            for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
                // A pattern with changes in it, because R4 is exercised only by MOSI
                // actually moving: a suite that sends 0x00 has not tested it at all.
                set_cfg(w, ipol, ipha, ilsb, 3'd0, 32'h0000_1A5C);
                clear_counts();
                run_burst(2);
                legal_txns = legal_txns + 2;
                legal_cfgs = legal_cfgs + 1;

                for (r = 0; r < NRULES; r = r + 1) begin
                    tot_ex[r] = tot_ex[r] + exercised(r);
                    tot_vi[r] = tot_vi[r] + violated(r);
                    if (violated(r) != 0)
                        $display("  FAIL: legal traffic violated R%0d %0d time(s) at cpol=%0d cpha=%0d lsb=%0d n=%0d",
                                 r + 1, violated(r), ipol, ipha, ilsb, w);
                    if (violated(r) != 0) errors = errors + 1;
                end

                want = (~32'h0000_1A5C) & ((32'h1 << w) - 32'h1);
                if ((rx_data & ((32'h1 << w) - 32'h1)) !== want) begin
                    $display("  FAIL: rx mismatch at cpol=%0d cpha=%0d lsb=%0d n=%0d -- got %h want %h; the driver is not sampling MISO on the capture edge of this mode",
                             ipol, ipha, ilsb, w, rx_data & ((32'h1 << w) - 32'h1), want);
                    rx_bad = rx_bad + 1;
                    errors = errors + 1;
                end
            end
        end

        $display("  legal traffic: %0d transactions in %0d measurement windows (2 widths x 2 polarities x 2 phases x 2 bit orders)",
                 legal_txns, legal_cfgs);
        $display("  rule  exercised  violated");
        for (r = 0; r < NRULES; r = r + 1)
            $display("  R%0d    %9d  %8d", r + 1, tot_ex[r], tot_vi[r]);

        for (r = 0; r < NRULES; r = r + 1)
            if (tot_ex[r] == 0) begin
                $display("  FAIL: legal traffic never exercised R%0d, so its zero violation count means nothing",
                         r + 1);
                errors = errors + 1;
            end
        $display("    1. legal traffic violated none of the eight rules in any of the %0d configurations", legal_cfgs);
        $display("    2. and exercised all eight, so the zeros above are measurements rather than silence -- a driver that never moved a pin would satisfy measurement 1 and fail this one");
        $display("    and the received word was the complement of the sent word in every configuration (%0d mismatches), which tests what the pin monitor cannot see: that MISO is sampled on the CAPTURE edge of the mode in force. A driver that sampled on the launch edge would satisfy all eight rules and return garbage",
                 rx_bad);

        // ============================================================
        // 3. THE FAULT MATRIX.
        //
        // Each fault is run in TWO configurations, in its own measurement window, and the
        // rules that fired are OR-ed across both. Two, because a fault that only shows in
        // one phase is a fault whose rule is phase-dependent, and a single-mode matrix
        // would hide that.
        // ============================================================
        matrix_bad = 0;
        diag_bad   = 0;
        $display("  fault  breaks                      expected  rules that fired");
        for (f = 1; f <= 7; f = f + 1) begin
            case (f)
                1: begin expect_rule = R_LEAD;   expect_set = (1 << R_LEAD);   end
                2: begin expect_rule = R_HALF;   expect_set = (1 << R_HALF);   end
                3: begin expect_rule = R_LAUNCH; expect_set = (1 << R_LAUNCH); end
                // FAULT 4 IS EXPECTED TO FIRE TWO RULES, and the second one is not a
                // checker defect -- it is arithmetic.
                //
                // A frame of 2N edges returns SCLK to its idle level because an even
                // number of toggles does. Drop one edge and the count is odd, so SCLK is
                // NECESSARILY parked off its idle level when CS rises: R5, the partial
                // frame, and R1, the idle level, are the same fault seen twice.
                //
                // Writing the expectation as a single rule and calling the extra one
                // noise would have been the easy move, and it would have taught the wrong
                // thing. A diagonal fault matrix is itself a CLAIM -- that each fault has
                // exactly one pin-observable consequence -- and that claim is false here
                // for a reason worth knowing: when a report shows R1 and R5 together, an
                // engineer should look for ONE truncated frame, not two unrelated bugs.
                4: begin expect_rule = R_FRAME;  expect_set = (1 << R_FRAME) | (1 << R_IDLE); end
                5: begin expect_rule = R_LAG;    expect_set = (1 << R_LAG);    end
                6: begin expect_rule = R_GAP;    expect_set = (1 << R_GAP);    end
                default: begin expect_rule = R_IDLE; expect_set = (1 << R_IDLE); end
            endcase

            fired = {NRULES{1'b0}};
            for (fc = 0; fc < 2; fc = fc + 1) begin
                // Three transactions per window: the gap and the idle level are
                // obligations between transactions, and a burst of three exercises them
                // twice.
                set_cfg(6'd8, fc, fc, 0, f[2:0], (fc == 0) ? 32'h0000_1A5C : 32'h0000_0C3A);
                clear_counts();
                run_burst(3);
                for (r = 0; r < NRULES; r = r + 1)
                    if (violated(r) != 0) fired[r] = 1'b1;
            end

            $display("  %5d  %0s  %b  %b", f,
                     (f == 1) ? "the lead" :
                     (f == 2) ? "the half period" :
                     (f == 3) ? "which edge launches" :
                     (f == 4) ? "the frame's last edge" :
                     (f == 5) ? "the lag" :
                     (f == 6) ? "the gap" : "the idle level",
                     expect_set, fired);

            for (other = 0; other < NRULES; other = other + 1) begin
                if (expect_set[other] === 1'b1 && fired[other] !== 1'b1) begin
                    $display("  FAIL: fault %0d did not make R%0d fire, so that rule is either unreachable or wrong -- and a rule that cannot be made to fire has been assumed, not verified",
                             f, other + 1);
                    errors   = errors + 1;
                    diag_bad = diag_bad + 1;
                end
                if (expect_set[other] !== 1'b1 && fired[other] === 1'b1) begin
                    $display("  FAIL: fault %0d also fired R%0d, which is not one of its expected consequences, so the monitor's report cannot be read as a diagnosis",
                             f, other + 1);
                    errors     = errors + 1;
                    matrix_bad = matrix_bad + 1;
                end
            end
        end

        $display("    3. every one of the seven faults fired EXACTLY the rules predicted for it and no others: %0d predicted rules that stayed silent, %0d rules that fired unpredicted. Six faults map to one rule each; fault 4 maps to two, because an odd edge count cannot return SCLK to its idle level, so a partial frame is necessarily also an idle-level offence -- the same fault seen twice rather than a checker defect. A monitor that fires for its own violation is half-verified; one that fires for exactly its own violations produces a report that names the defect instead of hinting at it",
                 diag_bad, matrix_bad);
        $display("    and R8 appears in no row, because MISO is not a master driver's pin. There is no master fault that can reach it, and a suite that reports R8 against a master has a monitor pointed at the wrong agent -- which Chapter 16.7 fixes by giving the passive agent its own");

        if (errors == 0)
            $display("PASS: a driver cannot check itself, because every self-check it can perform restates the arithmetic under test -- a driver that computes its lead wrongly asserts that its lead is correct with total conviction. So this driver is checked by Chapter 16.1's rule monitor, written in another chapter from the protocol's obligations rather than from any implementation, consuming nothing but pins, and sharing no line of code with it. Across %0d legal transactions in %0d measurement windows -- both polarities, both phases, two frame widths, both bit orders -- the pins violated none of the eight rules AND exercised all eight, which is the pair of results that makes the zeros a measurement instead of a silence: a driver that never moved a pin would pass the first half. The received word was the complement of the sent word in every configuration, which is the check the pin monitor is structurally blind to and which catches a driver that samples MISO on the launch edge -- pins perfectly legal, data garbage. And each of the seven injected faults fired EXACTLY the rules written down for it and no others -- %0d predicted rules silent, %0d unpredicted rules firing -- which is what separates a checker whose report is a diagnosis from one whose report is a hint; six faults map to a single rule and the seventh maps to two, because an odd edge count cannot return SCLK to its idle level, so a partial frame is also an idle-level offence by arithmetic and a report showing both should be read as ONE fault. The division of labour is the lesson: the generator owns WHAT, the driver owns WHEN, and every timing number in the protocol is implemented exactly once -- which is why a bug here is a bug in every test and why this is the one component that must be checked against something that was not written to agree with it",
                     legal_txns, legal_cfgs, diag_bad, matrix_bad);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        cpol = 1'b0;
        cpha = 1'b0;
        lsb_first = 1'b0;
        clk = 1'b0;
        rst_n = 1'b1;
        start = 1'b0;
        tx_data = {DW{1'b0}};
        nbits = 6'd8;
        fault = 3'd0;
        clr = 1'b0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_driver_tb.vhd — the same bench in VHDL
-- spi_driver_tb.vhd
--
-- The driver is checked by CHAPTER 16.1'S RULE MONITOR, instantiated here unmodified on
-- the same pins. That is the whole architecture of this bench and it is worth saying why
-- before reading any of it.
--
-- A driver cannot check itself. Every self-check a driver can perform is a restatement of
-- its own arithmetic, so a driver that computes its lead wrongly will also assert that its
-- lead is correct, with total conviction. The rule monitor was written in a different
-- chapter, for a different purpose, from the protocol's obligations rather than from any
-- implementation -- and it consumes nothing but pins. Two components that share no code
-- agreeing about the same wires is evidence.
--
-- THERE ARE THREE MEASUREMENTS, and the third is what makes the first two mean anything.
--
--   1. LEGAL TRAFFIC VIOLATES NOTHING. Sixteen configurations -- both polarities, both
--      phases, two frame widths, both bit orders -- produce zero violations of any rule.
--
--   2. LEGAL TRAFFIC EXERCISES EVERYTHING. All eight rules report a non-zero exercise
--      count. Without this, measurement 1 is satisfied by a driver that never moves a pin
--      and by a monitor whose rules are all unreachable.
--
--   3. EACH INJECTED FAULT VIOLATES EXACTLY THE RULES PREDICTED FOR IT -- a written-down
--      SET per fault, with nothing outside the set permitted to fire. Six sets have one
--      member; F_TRUNC's has two, because an odd edge count cannot return SCLK to its idle
--      level, so a partial frame is necessarily also an idle-level offence. That is
--      arithmetic, not a checker defect, and writing it down is the difference between a
--      report that is a diagnosis and one that is a hint.
--
-- AND ONE INDEPENDENT CHECK THAT DOES NOT COME FROM THE MONITOR.
--
-- MISO is driven as the complement of MOSI -- a loopback slave that returns what it was
-- given, inverted. The driver's received word must therefore be the complement of the word
-- it sent, in both phases, which tests something the rule monitor cannot see: that the
-- driver samples MISO on the CAPTURE edge of the mode it was given. A driver that samples
-- on the launch edge produces pins that satisfy all eight rules and a received word that
-- is garbage -- exactly the class of bug a pin-level checker is blind to.
--
-- WHAT VHDL CONTRIBUTES: the fault is an ENUMERATION, so the fault-to-rule map below is a
-- table indexed by a type rather than by an integer, and a fault added to the type without
-- a row in the table is an analysis-time error rather than a silently untested case.

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

entity spi_driver_tb is
end entity spi_driver_tb;

architecture tb of spi_driver_tb is

    constant LEAD_C : natural := 4;
    constant HALF_C : natural := 3;
    constant LAG_C  : natural := 2;
    constant GAP_C  : natural := 3;
    constant HALF_T : time    := 5 ns;

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '1';
    signal done_sim : boolean := false;

    signal start : std_logic := '0';
    signal req   : spi_req_t := (data      => (others => '0'),
                                 nbits     => to_unsigned(8, LEN_W),
                                 cpol      => '0',
                                 cpha      => '0',
                                 lsb_first => '0',
                                 fault     => F_NONE);

    signal busy, done : std_logic;
    signal rx_data    : std_logic_vector(DW - 1 downto 0);
    signal sclk, cs_n, mosi : std_logic;

    -- The loopback slave: the complement of MOSI, continuously. Not a model of anything
    -- real and it does not need to be -- its only job is to make the received word a known
    -- function of the sent word so the capture edge can be checked.
    signal miso : std_logic;

    -- A bus monitor's view of whether MISO is driven at all. Tied to the select, which is
    -- legal by construction: this bench contains no slave that could get it wrong, and R8
    -- therefore reports zero violations for a reason the log states rather than leaves to
    -- be assumed.
    signal miso_driven : std_logic;

    signal clr : std_logic := '0';
    signal exercised, violated : rule_counts_t;

    -- THE FAULT-TO-RULE MAP is a claim about the protocol, not about this code, which is
    -- why it is a declared table and asserted rather than left implicit.
    type rule_set_t is array (0 to NRULES - 1) of boolean;
    type fault_row_t is record
        f     : spi_fault_t;
        tag   : string(1 to 26);
        want  : rule_set_t;
    end record;

    function one (i : natural) return rule_set_t is
        variable s : rule_set_t := (others => false);
    begin
        s(i) := true;
        return s;
    end function one;

    function two (i : natural; j : natural) return rule_set_t is
        variable s : rule_set_t := (others => false);
    begin
        s(i) := true;
        s(j) := true;
        return s;
    end function two;

    type fault_table_t is array (0 to 6) of fault_row_t;
    constant FAULTS : fault_table_t := (
        (F_LEAD,  "the lead                  ", one(R_LEAD)),
        (F_HALF,  "the half period           ", one(R_HALF)),
        (F_PHASE, "which edge launches       ", one(R_LAUNCH)),
        -- F_TRUNC reaches TWO rules, and the second is arithmetic rather than sloppiness:
        -- a frame of 2N edges returns SCLK to its idle level because an even number of
        -- toggles does. Drop one edge and the count is odd, so SCLK is NECESSARILY parked
        -- off its idle level when CS rises -- R5 and R1 are the same fault seen twice.
        -- Calling the extra one noise would have been easier and would have taught the
        -- wrong thing: a diagonal fault matrix is itself a CLAIM, and it is false here.
        -- When a report shows R1 and R5 together, look for ONE truncated frame.
        (F_TRUNC, "the frame's last edge     ", two(R_FRAME, R_IDLE)),
        (F_LAG,   "the lag                   ", one(R_LAG)),
        (F_GAP,   "the gap                   ", one(R_GAP)),
        (F_IDLE,  "the idle level            ", one(R_IDLE))
    );

    signal errors : integer := 0;

    -- Totals folded across measurement windows.
    type int_arr_t is array (0 to NRULES - 1) of integer;
    signal tot_ex, tot_vi : int_arr_t := (others => 0);

begin

    miso        <= not mosi;
    miso_driven <= not cs_n;

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

    dut : entity work.spi_driver
        generic map (LEAD => LEAD_C, HALF => HALF_C, LAG => LAG_C, GAP => GAP_C)
        port map (clk => clk, rst_n => rst_n, start => start, req => req,
                  busy => busy, done => done, rx_data => rx_data,
                  sclk => sclk, cs_n => cs_n, mosi => mosi, miso => miso);

    mon : entity work.spi_rule_monitor
        generic map (LEAD => LEAD_C, HALF => HALF_C, LAG => LAG_C, GAP => GAP_C,
                     LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  sclk => sclk, cs_n => cs_n, mosi => mosi, miso_driven => miso_driven,
                  cpol => req.cpol, cpha => req.cpha, len => req.nbits,
                  exercised => exercised, violated => violated, clr => clr);

    main : process is

        -- A CONFIGURATION CHANGE IS NOT A PROTOCOL EVENT, AND THE CHECKER HAS TO BE TOLD.
        --
        -- The monitor judges the pins against the `cpol` it is handed. When that input
        -- changes, the pin it is judging has not moved yet -- the driver needs a cycle to
        -- re-park SCLK at the new idle level -- so for that one cycle a correct driver
        -- looks like a violation of R1. Nothing is wrong with either component: from the
        -- pins, a reconfiguration and an offence are identical.
        --
        -- So reconfiguration is bracketed: change the mode, let the pins settle, and only
        -- then clear the counters and begin measuring. Accumulating across the change
        -- instead produces a suite with a small permanent violation count that everyone
        -- learns to ignore -- which is how a real violation gets ignored too.
        procedure set_cfg (n : natural; pol : std_logic; pha : std_logic;
                           lsb : std_logic; f : spi_fault_t;
                           d : std_logic_vector(DW - 1 downto 0)) is
        begin
            wait until falling_edge(clk);
            req <= (data => d, nbits => to_unsigned(n, LEN_W),
                    cpol => pol, cpha => pha, lsb_first => lsb, fault => f);
            for i in 0 to 5 loop wait until falling_edge(clk); end loop;
        end procedure set_cfg;

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

        -- A BURST, NOT A SEQUENCE OF WAITED TRANSACTIONS, and the difference decides
        -- whether the gap rule can be tested at all.
        --
        -- An earlier version asserted `start` only after the previous transaction had
        -- reported done and the bench had stepped a cycle. The driver was therefore always
        -- asked to start LATER than its own gap logic would have allowed, the gap on the
        -- pins was whatever the bench's handshake happened to produce, and shortening the
        -- driver's gap to one cycle changed nothing observable -- R7 could not be made to
        -- fire. Holding `start` high across the burst hands the timing back to the
        -- component that owns it.
        procedure run_burst (ntxn : natural) is
            variable k : natural := 0;
        begin
            k := 0;
            wait until falling_edge(clk);
            start <= '1';
            while k < ntxn loop
                wait until falling_edge(clk);
                if done = '1' then
                    k := k + 1;
                    if k = ntxn then start <= '0'; end if;
                end if;
            end loop;
            for i in 0 to GAP_C + LAG_C + 5 loop wait until falling_edge(clk); end loop;
        end procedure run_burst;

        constant PAT_A : std_logic_vector(DW - 1 downto 0) := x"00001A5C";
        constant PAT_B : std_logic_vector(DW - 1 downto 0) := x"00000C3A";

        variable w          : natural;
        variable legal_txns : natural := 0;
        variable legal_cfgs : natural := 0;
        variable rx_bad     : natural := 0;
        variable want       : std_logic_vector(DW - 1 downto 0);
        variable mask       : unsigned(DW - 1 downto 0);
        variable fired      : rule_set_t;
        variable diag_bad   : natural := 0;
        variable matrix_bad : natural := 0;
        variable pol, pha, lsb : std_logic;
        variable row        : fault_row_t;
        variable fset       : string(1 to NRULES);
        variable wset       : string(1 to NRULES);

        function setstr (s : rule_set_t) return string is
            variable r : string(1 to NRULES);
        begin
            for i in 0 to NRULES - 1 loop
                if s(NRULES - 1 - i) then r(i + 1) := '1'; else r(i + 1) := '0'; end if;
            end loop;
            return r;
        end function setstr;

    begin
        rst_n <= '1';
        for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        rst_n <= '0';
        for i in 0 to 3 loop wait until falling_edge(clk); end loop;
        rst_n <= '1';
        for i in 0 to 3 loop wait until falling_edge(clk); end loop;

        -- ==============================================================
        -- 1 + 2. LEGAL TRAFFIC.
        --
        -- One measurement WINDOW per configuration: set the mode, let the pins settle,
        -- clear the counters, run a burst of two transactions -- two, because the gap and
        -- the idle level are obligations BETWEEN transactions and one cannot exercise them
        -- -- then read the counters and fold them into the totals.
        -- ==============================================================
        for iw in 0 to 1 loop
            if iw = 0 then w := 8; else w := 13; end if;
            for ipol in 0 to 1 loop
                for ipha in 0 to 1 loop
                    for ilsb in 0 to 1 loop
                        if ipol = 0 then pol := '0'; else pol := '1'; end if;
                        if ipha = 0 then pha := '0'; else pha := '1'; end if;
                        if ilsb = 0 then lsb := '0'; else lsb := '1'; end if;

                        -- A pattern with changes in it, because R4 is exercised only by
                        -- MOSI actually moving: a suite that sends zero has not tested it.
                        set_cfg(w, pol, pha, lsb, F_NONE, PAT_A);
                        clear_counts;
                        run_burst(2);
                        legal_txns := legal_txns + 2;
                        legal_cfgs := legal_cfgs + 1;

                        for r in 0 to NRULES - 1 loop
                            tot_ex(r) <= tot_ex(r) + exercised(r);
                            tot_vi(r) <= tot_vi(r) + violated(r);
                            if violated(r) /= 0 then
                                report "  FAIL: legal traffic violated R" & integer'image(r + 1) &
                                       " " & integer'image(violated(r)) & " time(s) at cpol=" &
                                       std_logic'image(pol) & " cpha=" & std_logic'image(pha) &
                                       " lsb=" & std_logic'image(lsb) & " n=" & integer'image(w);
                                errors <= errors + 1;
                            end if;
                        end loop;
                        wait for 1 ns;

                        mask := shift_left(to_unsigned(1, DW), w) - 1;
                        want := (not PAT_A) and std_logic_vector(mask);
                        if (rx_data and std_logic_vector(mask)) /= want then
                            report "  FAIL: rx mismatch at cpol=" & std_logic'image(pol) &
                                   " cpha=" & std_logic'image(pha) & " lsb=" &
                                   std_logic'image(lsb) & " n=" & integer'image(w) &
                                   " -- the driver is not sampling MISO on the capture edge of this mode";
                            rx_bad := rx_bad + 1;
                            errors <= errors + 1;
                            wait for 1 ns;
                        end if;
                    end loop;
                end loop;
            end loop;
        end loop;

        report "  legal traffic: " & integer'image(legal_txns) & " transactions in " &
               integer'image(legal_cfgs) &
               " measurement windows (2 widths x 2 polarities x 2 phases x 2 bit orders)";
        report "  rule  exercised  violated   obligation";
        for r in 0 to NRULES - 1 loop
            report "  R" & integer'image(r + 1) & "    " & integer'image(tot_ex(r)) &
                   "  " & integer'image(tot_vi(r)) & "   " & rule_name(r);
        end loop;

        for r in 0 to NRULES - 1 loop
            if tot_ex(r) = 0 then
                report "  FAIL: legal traffic never exercised R" & integer'image(r + 1) &
                       ", so its zero violation count means nothing";
                errors <= errors + 1;
                wait for 1 ns;
            end if;
        end loop;
        report "    1. legal traffic violated none of the eight rules in any of the " &
               integer'image(legal_cfgs) & " configurations";
        report "    2. and exercised all eight, so the zeros above are measurements rather than silence -- a driver that never moved a pin would satisfy measurement 1 and fail this one";
        report "    and the received word was the complement of the sent word in every configuration (" &
               integer'image(rx_bad) &
               " mismatches), which tests what the pin monitor cannot see: that MISO is sampled on the CAPTURE edge of the mode in force. A driver that sampled on the launch edge would satisfy all eight rules and return garbage";

        -- ==============================================================
        -- 3. THE FAULT MATRIX.
        --
        -- Each fault is run in TWO configurations, each in its own measurement window, and
        -- the rules that fired are OR-ed across both -- because a fault that only shows in
        -- one phase is a fault whose rule is phase-dependent, and a single-mode matrix
        -- would hide that.
        -- ==============================================================
        report "  fault  breaks                      expected  fired";
        for fi in FAULTS'range loop
            row   := FAULTS(fi);
            fired := (others => false);

            for fc in 0 to 1 loop
                if fc = 0 then pol := '0'; pha := '0'; else pol := '1'; pha := '1'; end if;
                -- Three transactions per window: the gap and the idle level are obligations
                -- between transactions, and a burst of three exercises them twice.
                if fc = 0 then
                    set_cfg(8, pol, pha, '0', row.f, PAT_A);
                else
                    set_cfg(8, pol, pha, '0', row.f, PAT_B);
                end if;
                clear_counts;
                run_burst(3);
                for r in 0 to NRULES - 1 loop
                    if violated(r) /= 0 then fired(r) := true; end if;
                end loop;
            end loop;

            fset := setstr(fired);
            wset := setstr(row.want);
            report "  " & integer'image(fi + 1) & "      " & row.tag & "  " & wset &
                   "  " & fset;

            for r in 0 to NRULES - 1 loop
                if row.want(r) and not fired(r) then
                    report "  FAIL: fault " & integer'image(fi + 1) & " did not make R" &
                           integer'image(r + 1) &
                           " fire, so that rule is either unreachable or wrong -- and a rule that cannot be made to fire has been assumed, not verified";
                    errors   <= errors + 1;
                    diag_bad := diag_bad + 1;
                    wait for 1 ns;
                end if;
                if fired(r) and not row.want(r) then
                    report "  FAIL: fault " & integer'image(fi + 1) & " also fired R" &
                           integer'image(r + 1) &
                           ", which is not one of its expected consequences, so the monitor's report cannot be read as a diagnosis";
                    errors     <= errors + 1;
                    matrix_bad := matrix_bad + 1;
                    wait for 1 ns;
                end if;
            end loop;
        end loop;

        report "    3. every one of the seven faults fired EXACTLY the rules predicted for it and no others: " &
               integer'image(diag_bad) & " predicted rules that stayed silent, " &
               integer'image(matrix_bad) &
               " rules that fired unpredicted. Six faults map to one rule each; the truncation fault maps to two, because an odd edge count cannot return SCLK to its idle level, so a partial frame is necessarily also an idle-level offence -- the same fault seen twice rather than a checker defect";
        report "    and R8 appears in no row, because MISO is not a master driver's pin. There is no master fault that can reach it, and a suite that reports R8 against a master has a monitor pointed at the wrong agent -- which Chapter 16.7 fixes by giving the passive agent its own";

        wait for 1 ns;
        if errors = 0 then
            report "PASS: a driver cannot check itself, because every self-check it can perform restates the arithmetic under test -- a driver that computes its lead wrongly asserts that its lead is correct with total conviction. So this driver is checked by Chapter 16.1's rule monitor, written in another chapter from the protocol's obligations rather than from any implementation, consuming nothing but pins, sharing no line of code with it. Across " &
                   integer'image(legal_txns) & " legal transactions in " &
                   integer'image(legal_cfgs) &
                   " measurement windows -- both polarities, both phases, two frame widths, both bit orders -- the pins violated none of the eight rules AND exercised all eight, which is the pair of results that makes the zeros a measurement instead of a silence: a driver that never moved a pin would pass the first half. The received word was the complement of the sent word in every configuration, which is the check the pin monitor is structurally blind to and which catches a driver that samples MISO on the launch edge -- pins perfectly legal, data garbage. And each of the seven injected faults fired EXACTLY the rules written down for it and no others, six mapping to a single rule and the seventh to two, because an odd edge count cannot return SCLK to its idle level, so a partial frame is also an idle-level offence by arithmetic and a report showing both should be read as ONE fault. The division of labour is the lesson: the generator owns WHAT, the driver owns WHEN, every timing number in the protocol is implemented exactly once, and that is why a bug here is a bug in every test and why this is the one component that must be checked against something not written to agree with it"
                severity note;
        else
            report "FAIL: " & integer'image(errors) & " error(s)" severity error;
        end if;

        done_sim <= true;
        wait for 100 ns;
        std.env.stop;
    end process main;

end architecture tb;

9. Why a Verification Engineer Cares

Because "the driver is correct" is the assumption every other result in the environment rests on, and it is the assumption most often taken on faith.

The structural rule is the one to take away: a component's checker must not share its author's model of the problem. The rule monitor here was written in another chapter, from the protocol's obligations rather than from any implementation, and it consumes nothing but pins. That is what let it disagree with the driver — three times, each time correctly.

The second rule is about fault injection. A checker that has never been observed to fire is a checker with no evidence behind it, and the cheapest way to get that evidence is a fault input in the design under test. Two real driver bugs in this chapter were found not by a failing test but by a fault that refused to produce its expected failure — which is a signal a suite without fault injection cannot generate at all.

10. Why an FPGA or ASIC Engineer Cares

Because the four numbers in section 1 are the numbers in your datasheet, and this is the component that decides whether they were ever really tested.

A master whose lead is generous in every test will meet a slave that publishes the same minimum you do, and the transfer that fails will be the one where both sides are exactly at their limits. The exactly at the minimum bins from Chapter 16.2 and the boundary covers in section 8 are what turn a published number into a verified one.

The CPHA asymmetry matters for design review too. If a master loses the first bit in one phase and works in the other, the first suspicion falls on the slave — and the actual defect is a master that places bit 0 on edge 0 in both phases. Knowing that the lead is where CPHA=0's first bit lives makes that review a two-minute check instead of a day.

11. Failure Signature — A Rule That Cannot Be Made To Fail

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom          a fault is injected that should violate the lag rule. The
                    rule does not fire. Every other rule behaves correctly and
                    legal traffic is clean.

   What happened    the driver waited a further half period after its last edge
                    before starting to count the lag, so the lag on the pins was
                    always HALF + LAG. A requested lag of zero still measured
                    three cycles, which is above the minimum. The checker was
                    correct; the fault could not reach it.

   What would have  a fault matrix -- a written-down expectation per fault, and
   caught it        a requirement that the predicted rule fires. Without one,
                    this driver ships with a lag it has never been possible to
                    violate, and the first master that genuinely deasserts early
                    is found by a customer.

   The tell         the rule is one whose violation requires a SMALL value, and
                    the design has an unconditional wait in the same path.
                    Whenever a minimum cannot be violated by requesting zero,
                    something in the path is adding time the specification does
                    not describe.

12. Common Misconceptions

"The driver can validate its own timing." Every self-check a driver can perform restates the arithmetic under test. A driver that computes its lead wrongly asserts that its lead is correct, and the assertion is exactly as wrong as the computation.

"Legal traffic passing is the main result." Legal traffic passing with all eight rules exercised is a result. Legal traffic passing with an exercise count of zero somewhere is a result about nothing, and a driver that never moved a pin achieves the first half.

"Each fault should fire exactly one rule." That is a claim, and it is false for one of the seven here: an odd edge count cannot return SCLK to its idle level, so a partial frame is necessarily also an idle-level offence. The correct discipline is a written-down set per fault with nothing outside it permitted, not an assumed diagonal.

"Reasoning about SCLK's level is equivalent to reasoning about edge parity." Levels depend on CPOL, so level-based logic needs four cases and parity-based logic needs one. The four-case version is where three of them get tested and the fourth ships.

"The gap is a delay after the transfer." It is an obligation on the next transfer's start. Implemented as a trailing delay it works until a test issues two transactions with a long pause between them, at which point the pause is wasted and a gap is inserted anyway.

"A pin-level rule checker covers the driver." It is blind to which edge MISO is sampled on. A driver sampling the return path on the launch edge satisfies all eight rules and returns garbage, which is why the received-word check exists alongside them.

13. Reason It Through

Why must the first data bit be on MOSI before edge 0 in CPHA=0, and what is the symptom if it is not?

Because CPHA=0 captures on the leading edge, so edge 0 is a capture — the slave samples MOSI there. If the bit is placed on edge 0 the slave samples whatever preceded it, every bit arrives one position late, and the word is wrong by a shift. The symptom is a slave that appears to mis-shift, in one phase only.

Sixteen windows of two transactions give R5 = 32 and R7 = 31. Why?

R5 is checked at every deassert: 32. R7 is a gap check, and a gap exists only between transactions — the first assert of the run has no preceding transaction. Counting it would inflate every run's exercise count by exactly one, which is small enough never to be noticed and large enough to make a single-transaction run report a gap check that never happened.

Fault 4 fires R5 and R1. Construct the argument from the edge count alone.

A frame of 2N edges is an even number of toggles, so SCLK ends where it started — at the idle level. Dropping one edge makes the count odd, so SCLK ends inverted, and it is still inverted when CS rises. R5 sees an incomplete frame and R1 sees a wrong idle level; there is one fault.

Why is the mode change bracketed with a counter clear rather than simply avoided?

Because it cannot be avoided in a suite that tests four modes, and it is not a fault in either component: the monitor is handed a new cpol before the driver's pin can follow it, and from the pins a reconfiguration is indistinguishable from an offence. Bracketing makes the artefact explicit. The alternative is a permanent small violation count, and a suite with one of those has trained its readers to ignore violations.

The received-word check found nothing. What did its passing establish?

That MISO is sampled on the capture edge of the mode in force, in all sixteen configurations — a property no pin-level rule can see. Its value is not that it failed; it is that it is capable of failing for a fault the other eight checks cannot detect.

14. Understanding Check

15. Summary

A driver owns every timing number in the protocol, implemented once, which is why a bug in it is a bug in every test and why it is the one component that must be checked against something not written to agree with it. Chapter 16.1's rule monitor did the checking, unmodified, consuming nothing but pins: across 32 legal transactions in sixteen configurations the pins violated none of the eight rules and exercised all eight, and each of seven injected faults fired exactly the rules written down for it — six mapping to a single rule and the seventh to two, because an odd edge count cannot return SCLK to its idle level. The fault matrix found two real driver bugs that legal traffic could not: a lag that started a half period late and therefore could never be violated, and an idle level parked at the latched polarity that produced three rule violations from one line. And a received-word comparison caught what no pin rule can see — that MISO is sampled on the capture edge of the mode in force.

16. What Comes Next

The driver produces pins. Chapter 16.5 rebuilds transactions from those pins with no access to the driver at all, and measures what happens to a monitor that takes one shortcut.

Continue learning