Skip to content
VLSI Mentor

SPI · Module 14

MISO Generation and Launch Timing

Under CPHA=0 the master samples MISO on the very first SCLK edge, so the slave must drive before any clock has moved: deriving the lead a master must provide, why the slave cannot see a late first bit but can measure the interval exactly, why it cannot suppress its final launch, and a transmit path verified in three HDLs.

This is the hardest timing problem in the module, and it is created by one sentence in the protocol:

Under CPHA=0, the master captures MISO on the first leading edge of SCLK.

There is no edge before the first edge. So the slave must already have its first bit on the wire before any clock has moved.

The master solves the mirror image of this with a preload triggered by its own start command (Chapter 13.5). The slave has no command. Its only trigger is chip select going low — and chip select going low is not something this slave knows about for several system clocks.

1. Counting The Cycles

Chapter 14.1 put chip select through a SYNC_N-deep synchroniser. Chapter 14.2 republished the resulting edge as a registered transaction strobe. Counting from the pin:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   pin time 0        CS falls at the pin
   cycle SYNC_N      cs_active rises -- the synchroniser has it
   cycle SYNC_N+1    txn_start_stb  -- 14.2's detector republishes it
   cycle SYNC_N+2    MISO carries bit 0, registered
   pin time LEAD     the master's first leading edge; it samples MISO

Which gives a requirement, and it is a requirement on the master:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   LEAD >= SYNC_N + 2

The +2 rather than +1 deserves a paragraph, because it is the price of an architectural decision rather than an accident.

Chapter 14.2 owns the transaction boundary. Owning it means republishing chip select as a registered strobe, so that every block that needs "the transaction started" gets the same signal on the same cycle. That costs one cycle before any of them can act. Driving MISO from cs_assert_stb directly would save the cycle — and would mean two blocks disagreed about when the transaction began, which is worse than a cycle.

2. Why The Slave Cannot See That Its First Bit Was Late

The obvious self-check is "did a capture edge arrive before I had driven anything?". It never fires, and the reason is worth understanding because the same reasoning appears three more times in this module.

By the time the slave sees the capture edge, that edge has also been through SYNC_N + 1 cycles of recovery. Both the assert and the edge are delayed by the same amount, so from inside the slave, its own drive always looks early. The lateness is a pin-level fact — was MISO valid at the pin when the master sampled it? — and nothing inside the slave can observe a pin-level fact about a signal it drives.

What the slave can observe is the interval. The recovery latency affects both ends of it equally and therefore cancels:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   count cycles from cs_assert_stb to the first capture strobe
   compare against LEAD_MIN

The flag is then exact rather than approximate, and that word is doing real work. §6's testbench calibrates the threshold against the pin: it sweeps the lead, records for each value whether the master actually sampled the wrong first bit, and asserts that lead_short fires on precisely those leads and no others. A threshold that was off by one would pass a bench that only checked "the flag fires when the lead is small".

The same argument applies to the half-period, and the block measures that too — half_seen is the interval from a launch strobe to the capture strobe that reads it, which must be at least SYNC_N + 1 for the launched bit to be stable at the pin when the master samples it.

Fourteen system-clock cycles across six rows. A clock row runs throughout. A chip-select pin row falls on cycle 1. A recovered chip-select row rises two cycles later on cycle 3. A transaction-start strobe fires on cycle 4. A MISO row becomes valid on cycle 5 carrying bit 7. An SCLK pin row has its first rising edge on cycle 5, coincident with MISO becoming valid.CS falls at the pinCS falls at the pinsynchroniser: +SYNC_Nsynchroniser: +SYNC_NMISO valid; master samplesMISO valid; master samplesclkcs_n (pin)cs_activetxn_start_stbmisob7b7b7b7b6b6b6b6b5sclk (pin)t0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 2 — the lead at SYNC_N = 2, drawn at the boundary value LEAD = 4. The master's first leading edge lands on cycle 5, exactly when MISO becomes valid and no earlier. A lead of 3 puts that edge on cycle 4, one cycle early, and the bit the master reads is whatever the previous transaction left on the wire — which is why the symptom is a stale byte rather than a random one.

3. The Other End: A Launch The Slave Cannot Suppress

The master suppresses its final launch (Chapter 13.5) because it knows the frame is over — it decided how many bits to send.

The slave does not know. A trailing edge arriving after the last capture is indistinguishable from the first edge of another word, and a master is allowed to keep clocking: that is what a multi-word transaction is.

So this block launches on every launch edge for as long as the transaction is open, and MISO does change once more after the master's final capture. That is not a defect and it cannot be fixed here:

  • The master is not sampling, so the value is not read.
  • The output is still enabled because the device is still selected (Chapter 14.5), so the transition is on a bus this slave legitimately owns.
  • The only thing that says "no more words" is chip select rising, and by then the edge has already happened.

4. The Block Diagram

Transmit path: the transaction start loads and aligns a word, a drive counter selects between the aligned word and the shift register, and a single register drives MISO; two interval counters measure the lead and the half-periodtx_datatxn_start_stblaunch_stbcap_stbalign / reverseshift registerdrive counterinterval countersword_start selectcompare and reportmisoword_taken_stbzerostopwhich12
Figure 1 — the transmit path. The single drive register at the right is the point of the structure: there is exactly one path to MISO, and the preload and the per-edge launch are two conditions on the same register rather than two registers with a mux. The drive counter is what selects between them — a value of zero means no bit has been driven yet, which is the only state in which the aligned word rather than the shift register should be presented.

5. One Drive Path, Not Two

The structure that took the longest to get right is also the simplest to state: there is one register that drives MISO, and one expression that feeds it.

The natural first attempt has two paths — a preload path that loads bit 0 at the transaction start, and a launch path that shifts on each launch edge. Every bug in this block's history came from those two paths disagreeing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   the bug                              what it looked like
   ---------------------------------    ------------------------------------
   the preload did not advance the      bit 0 sent twice and the last bit
     shift register                       never sent -- presented as a
                                          RECEIVE-side shift
   the word-boundary reload used the    the next word's first bit consumed,
     preload pattern                      so every word after the first was
                                          shifted by one
   len == 1                             the preload finishes the word, so
                                          the launch path had nothing to do
                                          and drove garbage
   drives left non-zero by the extra    the NEXT transaction's first bit was
     CPHA=0 launch                        corrupted

Four bugs, one cause. The fix is structural rather than a patch: a single drive expression, selected by whether any bit has been driven yet.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   word_start = (drives == 0)
   miso      <= word_start ? tx_aligned[top] : tx_sr[top]

   drives is zeroed at txn_start_stb and incremented on every launch

drives is doing something subtle and worth naming: it is not a bit counter — Chapter 14.3's counter is that. It answers exactly one question, "has anything been driven yet?", and it is the only state that distinguishes the preload case from the launch case. Once that question has one answer-holder, the two paths become one.

6. Building the Transmit Path — Three HDLs

The circuit

An aligning load, one shift register, one drive register, a drive counter, and two interval counters that measure the lead and the half-period. LEAD_MIN and HALF_MIN are parameters, and the block reports both the flag and the measured value for each.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_tx.sv — one drive path, one drive counter, and two interval measurements that cancel the recovery latency
// spi_slave_tx.sv
//
// Chapter 14.4 -- launching MISO, and the first bit that has nowhere to come
// from.
//
// THE PROBLEM, STATED EXACTLY.
//
// In CPHA=0 the master captures MISO on the FIRST leading edge. There is no
// earlier edge, so the slave must already have its first bit on the wire before
// any clock has moved. The master solves the mirror image of this with a preload
// triggered by its own start command (Chapter 13.5); the slave has no command --
// its only trigger is chip select going low.
//
// And chip select going low is not something this slave knows about for several
// system clocks, because it arrives through the synchroniser of Chapter 14.1 and
// then through the transaction detector of Chapter 14.2. Counting them:
//
//     pin time 0        CS falls
//     cycle SYNC_N      cs_active rises -- the synchroniser has it
//     cycle SYNC_N+1    txn_start_stb  -- 14.2's detector republishes it
//     cycle SYNC_N+2    MISO carries bit 0, registered
//     pin time LEAD     the master's first leading edge; it samples MISO
//
// which gives a requirement on the MASTER:
//
//     LEAD >= SYNC_N + 2
//
// The +2 rather than +1 is worth dwelling on, because it is the price of an
// architectural decision rather than an accident. Chapter 14.2 owns the
// transaction boundary, and owning it means republishing chip select as a
// registered strobe -- so the boundary is consistent for every block that uses
// it, at the cost of one cycle before any of them can act. Driving MISO from
// `cs_assert_stb` directly would save the cycle and would mean two blocks
// disagreed about when the transaction began, which is worse than a cycle.
//
// This is the arithmetic behind the lead time Chapter 13.7 makes programmable:
// that parameter exists in the master because slaves need it, and this is what
// says how much. The number is a property of the slave's front end, not of the
// protocol -- a deeper oversampler needs a longer lead, and nothing in any
// datasheet will tell you so.
//
// WHY THE SLAVE CANNOT SEE THAT ITS FIRST BIT WAS LATE -- AND WHAT IT CAN SEE.
//
// The obvious check is "did a capture arrive before I had driven anything?" It
// never fires. By the time the slave sees the capture edge, that edge has also
// been through SYNC_N + 1 cycles of recovery, so the slave's own drive always
// looks early. The lateness is a PIN-LEVEL fact -- was MISO valid at the pin when
// the master sampled it? -- and nothing inside the slave can observe it.
//
// What the slave can observe is the INTERVAL, because the recovery latency
// affects both ends of it equally and therefore cancels: count cycles from
// `cs_assert_stb` to the first capture strobe, and compare against LEAD_MIN. The
// flag is then exact rather than approximate, and section 6's testbench
// calibrates the threshold against the pin -- it checks that `lead_short` fires
// on precisely the leads for which the master sampled the wrong first bit.
//
// THE OTHER END, AND A THING THE SLAVE CANNOT DO.
//
// The master suppresses its final launch (Chapter 13.5) because it knows the
// frame is over -- it decided how many bits to send. The slave does not know. A
// trailing edge arriving after the last capture is indistinguishable from the
// first edge of another word, and a master IS allowed to keep clocking: that is
// what a multi-word transaction is.
//
// So this block launches on every launch edge for as long as the transaction is
// open, and MISO does change once more after the master's final capture. That is
// not a defect and it cannot be fixed here:
//
//   * the master is not sampling, so the value is not read;
//   * the output is still enabled because the device is still selected
//     (Chapter 14.5), so the transition is on a bus this slave legitimately owns;
//   * the only thing that says "no more words" is chip select rising, and by then
//     the edge has already happened.
//
// It is worth naming because the symmetric reasoning is so tempting. "The master
// suppresses it, so the slave should too" requires the slave to know something it
// structurally cannot -- and a slave that tried would have to guess, which is how
// a device stops responding to the second byte of a two-byte transaction.
//
// SECOND REQUIREMENT ON THE MASTER: THE HALF-PERIOD.
//
// The same recovery latency that delays the first bit also delays every other
// bit. A launch edge is recovered SYNC_N cycles after it happens, MISO is
// registered one cycle later, and the master samples it at the capture edge one
// half-period after the launch. So:
//
//     HALF >= SYNC_N + 1
//
// which is this slave's output-valid time expressed in its own clock, and it is
// the number Chapter 9.4's round-trip budget needs. It is also why Chapter 13.4
// puts the longer half of an odd divisor between launch and capture: that is the
// half this requirement lands on.
//
// THE TWO NUMBERS ARE NOT THE SAME, AND THE DIFFERENCE IS STRUCTURAL.
//
// The lead requirement is SYNC_N + 2 and the half-period requirement is
// SYNC_N + 1, for one slave with one front end. The extra cycle on the lead is
// Chapter 14.2's registered transaction boundary: the first bit waits for it,
// and every subsequent bit does not, because a launch strobe comes straight from
// the front end.
//
// It is worth resisting the urge to make them the same. Padding the half-period
// requirement up to SYNC_N + 2 to have one number would refuse a frequency the
// slave can actually run at; deriving the first bit from the unregistered chip
// select to bring the lead down to SYNC_N + 1 would mean two blocks disagreed
// about when the transaction began. Two numbers, each measured, is the honest
// answer -- and both are published so a system integrator does not have to
// rederive them from the synchroniser depth.
//
// Measured in RECOVERED cycles the interval between the launch strobe and the
// capture strobe is exactly the half-period, because the latency is present at
// both ends and cancels -- so `half_short` is exact, and section 6 calibrates it
// against the pin exactly as it calibrates the lead.

module spi_slave_tx #(
    parameter int MAX_W    = 32,
    parameter int LEN_W    = 6,
    parameter int LEAD_MIN = 3,   // recovered cycles from start to first capture
    parameter int HALF_MIN = 3,   // recovered cycles from a launch to its capture
    parameter int CNT_W    = 8
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- from 14.2 ---------------------------------------------------------
    input  wire              txn_active,
    input  wire              txn_start_stb,

    // --- from the mode logic of 14.6 ---------------------------------------
    input  wire              preload_stb,   // CPHA=0 only, at the transaction start
    input  wire              launch_stb,
    input  wire              cap_stb,       // watched, not used to drive

    // --- configuration and data -------------------------------------------
    input  wire [LEN_W-1:0]  len,
    input  wire              lsb_first,
    input  wire [MAX_W-1:0]  tx_data,       // a level: whatever 14.9 has ready

    // --- the pin's value (the enable is Chapter 14.5) ----------------------
    output wire              miso,

    // --- status -------------------------------------------------------------
    output wire              word_taken_stb, // this word has been consumed
    output wire [LEN_W-1:0]  bits_driven,
    output reg  [CNT_W-1:0]  lead_seen,      // recovered start-to-first-capture
    output reg               lead_short,     // sticky: below LEAD_MIN
    output reg  [CNT_W-1:0]  half_seen,      // recovered launch-to-capture
    output reg               half_short,     // sticky: below HALF_MIN
    input  wire              clr_flags
);

    reg [MAX_W-1:0] tx_sr;
    reg [LEN_W-1:0] drives;
    reg             miso_r;
    reg             taken_r;
    reg [CNT_W-1:0] since_start;
    reg [CNT_W-1:0] since_launch;
    reg             first_cap_seen;
    reg             launch_pending;

    assign miso           = miso_r;
    assign bits_driven    = drives;
    assign word_taken_stb = taken_r;

    // A fixed full-width reversal: pure wiring.
    function automatic [MAX_W-1:0] rev_all(input [MAX_W-1:0] x);
        integer b;
        begin
            rev_all = {MAX_W{1'b0}};
            for (b = 0; b < MAX_W; b = b + 1)
                rev_all[b] = x[MAX_W-1-b];
        end
    endfunction

    // Bit order is the same self-inverse transform the master uses (13.8), and
    // the left-align is the same asymmetry the master's transmit side has
    // (13.6): the first bit out must be in the top position.
    wire [MAX_W-1:0] tx_ordered = lsb_first ? (rev_all(tx_data) >> (MAX_W - len))
                                            : tx_data;
    wire [MAX_W-1:0] tx_aligned = tx_ordered << (MAX_W - len);

    wire drive_stb = preload_stb | launch_stb;

    // `drives == 0` selects the freshly presented word over the shift register,
    // which is what makes a one-bit frame work: there, every drive is the first
    // drive of a word, so every drive takes a new word and the shift register is
    // never consulted. A design with a separate "load and drive together" case at
    // the word boundary gets len = 1 wrong, because the load would consume the
    // next word's first bit without driving it.
    //
    // The transaction start is still a case of its own, and for one reason:
    // `drives` must be RE-ZEROED there. A CPHA=0 transaction ends with one more
    // launch than it has bits -- the launch this slave cannot refuse, described in
    // the header -- so it leaves `drives` at one. Without the re-zero the next
    // transaction's first bit is taken from the stale shift register, and the
    // symptom is that the first byte of every transaction after the first is
    // wrong while every later byte is right.
    wire             word_start = (drives == {LEN_W{1'b0}});
    wire [MAX_W-1:0] src        = word_start ? tx_aligned : tx_sr;
    wire             last_of_word = (drives == (len - 1'b1));

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            tx_sr          <= {MAX_W{1'b0}};
            drives         <= {LEN_W{1'b0}};
            miso_r         <= 1'b0;
            taken_r        <= 1'b0;
            since_start    <= {CNT_W{1'b0}};
            since_launch   <= {CNT_W{1'b0}};
            first_cap_seen <= 1'b0;
            launch_pending <= 1'b0;
            lead_seen      <= {CNT_W{1'b0}};
            lead_short     <= 1'b0;
            half_seen      <= {CNT_W{1'b0}};
            half_short     <= 1'b0;
        end else begin
            taken_r <= 1'b0;

            if (clr_flags) begin
                lead_short <= 1'b0;
                half_short <= 1'b0;
            end

            // --- the lead measurement -------------------------------------
            // Counted in RECOVERED cycles, which is what makes it exact: the
            // synchroniser latency is present at both ends of the interval and
            // cancels. Reloaded with one rather than zero, because the cycle the
            // start is seen on is the first cycle of the interval.
            if (txn_start_stb) begin
                since_start <= {{(CNT_W-1){1'b0}}, 1'b1};
                // A capture on the START cycle is an interval of zero, and it
                // must be recorded here or it is not recorded at all -- the
                // `else if` below cannot see it. Without this the measurement
                // silently reports the SECOND bit's interval, which is a whole
                // SCLK period and therefore never looks short: the flag goes
                // quiet at exactly the shortest leads, which is the opposite of
                // what it is for.
                if (cap_stb) begin
                    first_cap_seen <= 1'b1;
                    lead_seen      <= {CNT_W{1'b0}};
                    lead_short     <= 1'b1;
                end else begin
                    first_cap_seen <= 1'b0;
                end
            end else if (cap_stb && txn_active && !first_cap_seen) begin
                first_cap_seen <= 1'b1;
                lead_seen      <= since_start;
                if (since_start < LEAD_MIN)
                    lead_short <= 1'b1;
            end else if (since_start != {CNT_W{1'b1}}) begin
                since_start <= since_start + 1'b1;
            end

            // --- the half-period measurement ------------------------------
            // From a launch strobe to the capture that reads the bit it drove.
            // Also in recovered cycles, so also exact.
            if (drive_stb) begin
                since_launch   <= {{(CNT_W-1){1'b0}}, 1'b1};
                launch_pending <= 1'b1;
            end else if (cap_stb && txn_active && launch_pending) begin
                launch_pending <= 1'b0;
                half_seen      <= since_launch;
                if (since_launch < HALF_MIN)
                    half_short <= 1'b1;
            end else if (since_launch != {CNT_W{1'b1}}) begin
                since_launch <= since_launch + 1'b1;
            end

            // --- the datapath ----------------------------------------------
            if (txn_start_stb) begin
                if (drive_stb) begin
                    // CPHA=0: the preload is on this very cycle, so the load and
                    // the first drive coincide -- and `tx_sr` has not been loaded
                    // yet, so the bit must come from `tx_aligned` directly.
                    miso_r <= tx_aligned[MAX_W-1];
                    tx_sr  <= {tx_aligned[MAX_W-2:0], 1'b0};
                    if (len == 1) begin
                        // A one-bit word is finished by the preload alone.
                        drives  <= {LEN_W{1'b0}};
                        taken_r <= 1'b1;
                    end else begin
                        drives <= {{(LEN_W-1){1'b0}}, 1'b1};
                    end
                end else begin
                    // CPHA=1: nothing is driven yet, and the first launch edge
                    // will take the presented word because `drives` is zero.
                    drives <= {LEN_W{1'b0}};
                end
            end else if (drive_stb && txn_active) begin
                miso_r <= src[MAX_W-1];
                tx_sr  <= {src[MAX_W-2:0], 1'b0};
                if (last_of_word) begin
                    // The word is fully driven. `taken_stb` says so, and
                    // `drives` returning to zero makes the next drive take the
                    // next presented word -- which is what lets a multi-word
                    // transaction work without the system knowing where the
                    // boundaries are.
                    drives  <= {LEN_W{1'b0}};
                    taken_r <= 1'b1;
                end else begin
                    drives <= drives + 1'b1;
                end
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_tx.v — the same design in Verilog-2001
// spi_slave_tx.v
//
// Chapter 14.4 -- launching MISO, and the first bit that has nowhere to come
// from.
//
// THE PROBLEM, STATED EXACTLY.
//
// In CPHA=0 the master captures MISO on the FIRST leading edge. There is no
// earlier edge, so the slave must already have its first bit on the wire before
// any clock has moved. The master solves the mirror image of this with a preload
// triggered by its own start command (Chapter 13.5); the slave has no command --
// its only trigger is chip select going low.
//
// And chip select going low is not something this slave knows about for several
// system clocks, because it arrives through the synchroniser of Chapter 14.1 and
// then through the transaction detector of Chapter 14.2. Counting them:
//
//     pin time 0        CS falls
//     cycle SYNC_N      cs_active rises -- the synchroniser has it
//     cycle SYNC_N+1    txn_start_stb  -- 14.2's detector republishes it
//     cycle SYNC_N+2    MISO carries bit 0, registered
//     pin time LEAD     the master's first leading edge; it samples MISO
//
// which gives a requirement on the MASTER:
//
//     LEAD >= SYNC_N + 2
//
// The +2 rather than +1 is worth dwelling on, because it is the price of an
// architectural decision rather than an accident. Chapter 14.2 owns the
// transaction boundary, and owning it means republishing chip select as a
// registered strobe -- so the boundary is consistent for every block that uses
// it, at the cost of one cycle before any of them can act. Driving MISO from
// `cs_assert_stb` directly would save the cycle and would mean two blocks
// disagreed about when the transaction began, which is worse than a cycle.
//
// This is the arithmetic behind the lead time Chapter 13.7 makes programmable:
// that parameter exists in the master because slaves need it, and this is what
// says how much. The number is a property of the slave's front end, not of the
// protocol -- a deeper oversampler needs a longer lead, and nothing in any
// datasheet will tell you so.
//
// WHY THE SLAVE CANNOT SEE THAT ITS FIRST BIT WAS LATE -- AND WHAT IT CAN SEE.
//
// The obvious check is "did a capture arrive before I had driven anything?" It
// never fires. By the time the slave sees the capture edge, that edge has also
// been through SYNC_N + 1 cycles of recovery, so the slave's own drive always
// looks early. The lateness is a PIN-LEVEL fact -- was MISO valid at the pin when
// the master sampled it? -- and nothing inside the slave can observe it.
//
// What the slave can observe is the INTERVAL, because the recovery latency
// affects both ends of it equally and therefore cancels: count cycles from
// `cs_assert_stb` to the first capture strobe, and compare against LEAD_MIN. The
// flag is then exact rather than approximate, and section 6's testbench
// calibrates the threshold against the pin -- it checks that `lead_short` fires
// on precisely the leads for which the master sampled the wrong first bit.
//
// THE OTHER END, AND A THING THE SLAVE CANNOT DO.
//
// The master suppresses its final launch (Chapter 13.5) because it knows the
// frame is over -- it decided how many bits to send. The slave does not know. A
// trailing edge arriving after the last capture is indistinguishable from the
// first edge of another word, and a master IS allowed to keep clocking: that is
// what a multi-word transaction is.
//
// So this block launches on every launch edge for as long as the transaction is
// open, and MISO does change once more after the master's final capture. That is
// not a defect and it cannot be fixed here:
//
//   * the master is not sampling, so the value is not read;
//   * the output is still enabled because the device is still selected
//     (Chapter 14.5), so the transition is on a bus this slave legitimately owns;
//   * the only thing that says "no more words" is chip select rising, and by then
//     the edge has already happened.
//
// It is worth naming because the symmetric reasoning is so tempting. "The master
// suppresses it, so the slave should too" requires the slave to know something it
// structurally cannot -- and a slave that tried would have to guess, which is how
// a device stops responding to the second byte of a two-byte transaction.
//
// SECOND REQUIREMENT ON THE MASTER: THE HALF-PERIOD.
//
// The same recovery latency that delays the first bit also delays every other
// bit. A launch edge is recovered SYNC_N cycles after it happens, MISO is
// registered one cycle later, and the master samples it at the capture edge one
// half-period after the launch. So:
//
//     HALF >= SYNC_N + 1
//
// which is this slave's output-valid time expressed in its own clock, and it is
// the number Chapter 9.4's round-trip budget needs. It is also why Chapter 13.4
// puts the longer half of an odd divisor between launch and capture: that is the
// half this requirement lands on.
//
// THE TWO NUMBERS ARE NOT THE SAME, AND THE DIFFERENCE IS STRUCTURAL.
//
// The lead requirement is SYNC_N + 2 and the half-period requirement is
// SYNC_N + 1, for one slave with one front end. The extra cycle on the lead is
// Chapter 14.2's registered transaction boundary: the first bit waits for it,
// and every subsequent bit does not, because a launch strobe comes straight from
// the front end.
//
// It is worth resisting the urge to make them the same. Padding the half-period
// requirement up to SYNC_N + 2 to have one number would refuse a frequency the
// slave can actually run at; deriving the first bit from the unregistered chip
// select to bring the lead down to SYNC_N + 1 would mean two blocks disagreed
// about when the transaction began. Two numbers, each measured, is the honest
// answer -- and both are published so a system integrator does not have to
// rederive them from the synchroniser depth.
//
// Measured in RECOVERED cycles the interval between the launch strobe and the
// capture strobe is exactly the half-period, because the latency is present at
// both ends and cancels -- so `half_short` is exact, and section 6 calibrates it
// against the pin exactly as it calibrates the lead.

module spi_slave_tx #(
    parameter MAX_W    = 32,
    parameter LEN_W    = 6,
    parameter LEAD_MIN = 3,   // recovered cycles from start to first capture
    parameter HALF_MIN = 3,   // recovered cycles from a launch to its capture
    parameter CNT_W    = 8
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- from 14.2 ---------------------------------------------------------
    input  wire              txn_active,
    input  wire              txn_start_stb,

    // --- from the mode logic of 14.6 ---------------------------------------
    input  wire              preload_stb,   // CPHA=0 only, at the transaction start
    input  wire              launch_stb,
    input  wire              cap_stb,       // watched, not used to drive

    // --- configuration and data -------------------------------------------
    input  wire [LEN_W-1:0]  len,
    input  wire              lsb_first,
    input  wire [MAX_W-1:0]  tx_data,       // a level: whatever 14.9 has ready

    // --- the pin's value (the enable is Chapter 14.5) ----------------------
    output wire              miso,

    // --- status -------------------------------------------------------------
    output wire              word_taken_stb, // this word has been consumed
    output wire [LEN_W-1:0]  bits_driven,
    output reg  [CNT_W-1:0]  lead_seen,      // recovered start-to-first-capture
    output reg               lead_short,     // sticky: below LEAD_MIN
    output reg  [CNT_W-1:0]  half_seen,      // recovered launch-to-capture
    output reg               half_short,     // sticky: below HALF_MIN
    input  wire              clr_flags
);

    reg [MAX_W-1:0] tx_sr;
    reg [LEN_W-1:0] drives;
    reg             miso_r;
    reg             taken_r;
    reg [CNT_W-1:0] since_start;
    reg [CNT_W-1:0] since_launch;
    reg             first_cap_seen;
    reg             launch_pending;

    assign miso           = miso_r;
    assign bits_driven    = drives;
    assign word_taken_stb = taken_r;

    // A fixed full-width reversal: pure wiring.
        function [MAX_W-1:0] rev_all;
        input [MAX_W-1:0] x;
        integer b;
        begin
            rev_all = {MAX_W{1'b0}};
            for (b = 0; b < MAX_W; b = b + 1)
                rev_all[b] = x[MAX_W-1-b];
        end
    endfunction

    // Bit order is the same self-inverse transform the master uses (13.8), and
    // the left-align is the same asymmetry the master's transmit side has
    // (13.6): the first bit out must be in the top position.
    wire [MAX_W-1:0] tx_ordered = lsb_first ? (rev_all(tx_data) >> (MAX_W - len))
                                            : tx_data;
    wire [MAX_W-1:0] tx_aligned = tx_ordered << (MAX_W - len);

    wire drive_stb = preload_stb | launch_stb;

    // `drives == 0` selects the freshly presented word over the shift register,
    // which is what makes a one-bit frame work: there, every drive is the first
    // drive of a word, so every drive takes a new word and the shift register is
    // never consulted. A design with a separate "load and drive together" case at
    // the word boundary gets len = 1 wrong, because the load would consume the
    // next word's first bit without driving it.
    //
    // The transaction start is still a case of its own, and for one reason:
    // `drives` must be RE-ZEROED there. A CPHA=0 transaction ends with one more
    // launch than it has bits -- the launch this slave cannot refuse, described in
    // the header -- so it leaves `drives` at one. Without the re-zero the next
    // transaction's first bit is taken from the stale shift register, and the
    // symptom is that the first byte of every transaction after the first is
    // wrong while every later byte is right.
    wire             word_start = (drives == {LEN_W{1'b0}});
    wire [MAX_W-1:0] src        = word_start ? tx_aligned : tx_sr;
    wire             last_of_word = (drives == (len - 1'b1));

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            tx_sr          <= {MAX_W{1'b0}};
            drives         <= {LEN_W{1'b0}};
            miso_r         <= 1'b0;
            taken_r        <= 1'b0;
            since_start    <= {CNT_W{1'b0}};
            since_launch   <= {CNT_W{1'b0}};
            first_cap_seen <= 1'b0;
            launch_pending <= 1'b0;
            lead_seen      <= {CNT_W{1'b0}};
            lead_short     <= 1'b0;
            half_seen      <= {CNT_W{1'b0}};
            half_short     <= 1'b0;
        end else begin
            taken_r <= 1'b0;

            if (clr_flags) begin
                lead_short <= 1'b0;
                half_short <= 1'b0;
            end

            // --- the lead measurement -------------------------------------
            // Counted in RECOVERED cycles, which is what makes it exact: the
            // synchroniser latency is present at both ends of the interval and
            // cancels. Reloaded with one rather than zero, because the cycle the
            // start is seen on is the first cycle of the interval.
            if (txn_start_stb) begin
                since_start <= {{(CNT_W-1){1'b0}}, 1'b1};
                // A capture on the START cycle is an interval of zero, and it
                // must be recorded here or it is not recorded at all -- the
                // `else if` below cannot see it. Without this the measurement
                // silently reports the SECOND bit's interval, which is a whole
                // SCLK period and therefore never looks short: the flag goes
                // quiet at exactly the shortest leads, which is the opposite of
                // what it is for.
                if (cap_stb) begin
                    first_cap_seen <= 1'b1;
                    lead_seen      <= {CNT_W{1'b0}};
                    lead_short     <= 1'b1;
                end else begin
                    first_cap_seen <= 1'b0;
                end
            end else if (cap_stb && txn_active && !first_cap_seen) begin
                first_cap_seen <= 1'b1;
                lead_seen      <= since_start;
                if (since_start < LEAD_MIN)
                    lead_short <= 1'b1;
            end else if (since_start != {CNT_W{1'b1}}) begin
                since_start <= since_start + 1'b1;
            end

            // --- the half-period measurement ------------------------------
            // From a launch strobe to the capture that reads the bit it drove.
            // Also in recovered cycles, so also exact.
            if (drive_stb) begin
                since_launch   <= {{(CNT_W-1){1'b0}}, 1'b1};
                launch_pending <= 1'b1;
            end else if (cap_stb && txn_active && launch_pending) begin
                launch_pending <= 1'b0;
                half_seen      <= since_launch;
                if (since_launch < HALF_MIN)
                    half_short <= 1'b1;
            end else if (since_launch != {CNT_W{1'b1}}) begin
                since_launch <= since_launch + 1'b1;
            end

            // --- the datapath ----------------------------------------------
            if (txn_start_stb) begin
                if (drive_stb) begin
                    // CPHA=0: the preload is on this very cycle, so the load and
                    // the first drive coincide -- and `tx_sr` has not been loaded
                    // yet, so the bit must come from `tx_aligned` directly.
                    miso_r <= tx_aligned[MAX_W-1];
                    tx_sr  <= {tx_aligned[MAX_W-2:0], 1'b0};
                    if (len == 1) begin
                        // A one-bit word is finished by the preload alone.
                        drives  <= {LEN_W{1'b0}};
                        taken_r <= 1'b1;
                    end else begin
                        drives <= {{(LEN_W-1){1'b0}}, 1'b1};
                    end
                end else begin
                    // CPHA=1: nothing is driven yet, and the first launch edge
                    // will take the presented word because `drives` is zero.
                    drives <= {LEN_W{1'b0}};
                end
            end else if (drive_stb && txn_active) begin
                miso_r <= src[MAX_W-1];
                tx_sr  <= {src[MAX_W-2:0], 1'b0};
                if (last_of_word) begin
                    // The word is fully driven. `taken_stb` says so, and
                    // `drives` returning to zero makes the next drive take the
                    // next presented word -- which is what lets a multi-word
                    // transaction work without the system knowing where the
                    // boundaries are.
                    drives  <= {LEN_W{1'b0}};
                    taken_r <= 1'b1;
                end else begin
                    drives <= drives + 1'b1;
                end
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_tx.vhd — the same design in VHDL
-- spi_slave_tx.vhd
--
-- Chapter 14.4 -- launching MISO, and the first bit that has nowhere to come from.
--
-- THE PROBLEM, STATED EXACTLY.
--
-- In CPHA=0 the master captures MISO on the FIRST leading edge. There is no earlier
-- edge, so the slave must already have its first bit on the wire before any clock
-- has moved. The master solves the mirror image with a preload triggered by its own
-- start command (Chapter 13.5); the slave has no command -- its only trigger is chip
-- select going low.
--
-- And chip select going low is not something this slave knows about for several
-- system clocks, because it arrives through the synchroniser of Chapter 14.1 and
-- then through the transaction detector of Chapter 14.2. Counting them:
--
--     pin time 0        CS falls
--     cycle SYNC_N      cs_active rises -- the synchroniser has it
--     cycle SYNC_N+1    txn_start_stb  -- 14.2's detector republishes it
--     cycle SYNC_N+2    MISO carries bit 0, registered
--     pin time LEAD     the master's first leading edge; it samples MISO
--
-- which gives a requirement on the MASTER:
--
--     LEAD >= SYNC_N + 2
--
-- The +2 rather than +1 is the price of an architectural decision. Chapter 14.2
-- owns the transaction boundary, and owning it means republishing chip select as a
-- registered strobe -- so the boundary is consistent for every block that uses it,
-- at the cost of one cycle before any of them can act. Driving MISO from the
-- unregistered chip select would save the cycle and would mean two blocks disagreed
-- about when the transaction began, which is worse than a cycle.
--
-- This is the arithmetic behind the lead time Chapter 13.7 makes programmable: that
-- parameter exists in the master because slaves need it, and this is what says how
-- much. The number is a property of the slave's front end, not of the protocol -- a
-- deeper oversampler needs a longer lead, and nothing in any datasheet says so.
--
-- WHY THE SLAVE CANNOT SEE THAT ITS FIRST BIT WAS LATE -- AND WHAT IT CAN SEE.
--
-- The obvious check is "did a capture arrive before I had driven anything?" It never
-- fires. By the time the slave sees the capture edge, that edge has also been through
-- the same recovery, so the slave's own drive always looks early. The lateness is a
-- PIN-LEVEL fact -- was MISO valid at the pin when the master sampled it? -- and
-- nothing inside the slave can observe it.
--
-- What the slave can observe is the INTERVAL, because the recovery latency affects
-- both ends of it equally and therefore cancels: count cycles from `txn_start_stb`
-- to the first capture strobe and compare against LEAD_MIN. The flag is then exact,
-- and the testbench calibrates the threshold against the pin -- it checks that the
-- report fires on precisely the leads for which the master sampled the wrong bit.
--
-- THE OTHER END, AND A THING THE SLAVE CANNOT DO.
--
-- The master suppresses its final launch (Chapter 13.5) because it knows the frame
-- is over -- it decided how many bits to send. The slave does not know. A trailing
-- edge arriving after the last capture is indistinguishable from the first edge of
-- another word, and a master IS allowed to keep clocking: that is what a multi-word
-- transaction is.
--
-- So this block launches on every launch edge for as long as the transaction is
-- open, and MISO does change once more after the master's final capture. That is not
-- a defect and it cannot be fixed here: the master is not sampling; the output is
-- still enabled because the device is still selected (Chapter 14.5); and the only
-- thing that says "no more words" is chip select rising, by which time the edge has
-- already happened.
--
-- SECOND REQUIREMENT ON THE MASTER: THE HALF-PERIOD.
--
-- A launch edge is recovered SYNC_N cycles after it happens, MISO is registered one
-- cycle later, and the master samples it one half-period after the launch. So:
--
--     HALF >= SYNC_N + 1
--
-- which is this slave's output-valid time in its own clock, and the number Chapter
-- 9.4's round-trip budget needs. It is also why Chapter 13.4 puts the longer half of
-- an odd divisor between launch and capture: that is the half this lands on.
--
-- THE TWO NUMBERS ARE NOT THE SAME, AND THE DIFFERENCE IS STRUCTURAL. The lead needs
-- SYNC_N + 2 and the half-period SYNC_N + 1, for one slave with one front end: the
-- first bit waits for 14.2's registered boundary and every later bit does not.
-- Padding the half-period up to match would refuse a frequency the slave can run at;
-- deriving the first bit from unregistered chip select to bring the lead down would
-- mean two blocks disagreed about when the transaction began. Two numbers, each
-- measured and each published, is the honest answer.

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

entity spi_slave_tx is
    generic (
        MAX_W    : positive := 32;
        LEN_W    : positive := 6;
        LEAD_MIN : positive := 3;   -- recovered cycles, start to first capture
        HALF_MIN : positive := 3;   -- recovered cycles, launch to its capture
        CNT_W    : positive := 8
    );
    port (
        clk            : in  std_logic;
        rst_n          : in  std_logic;

        -- from 14.2
        txn_active     : in  std_logic;
        txn_start_stb  : in  std_logic;

        -- from the mode logic of 14.6
        preload_stb    : in  std_logic;  -- CPHA=0 only, at the transaction start
        launch_stb     : in  std_logic;
        cap_stb        : in  std_logic;  -- watched, not used to drive

        -- configuration and data
        len            : in  unsigned(LEN_W - 1 downto 0);
        lsb_first      : in  std_logic;
        tx_data        : in  std_logic_vector(MAX_W - 1 downto 0);

        -- the pin's value (the enable is Chapter 14.5)
        miso           : out std_logic;

        -- status
        word_taken_stb : out std_logic;
        bits_driven    : out unsigned(LEN_W - 1 downto 0);
        lead_seen      : out unsigned(CNT_W - 1 downto 0);
        lead_short     : out std_logic;
        half_seen      : out unsigned(CNT_W - 1 downto 0);
        half_short     : out std_logic;
        clr_flags      : in  std_logic
    );
end entity;

architecture rtl of spi_slave_tx is

    signal tx_sr  : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    signal drives : unsigned(LEN_W - 1 downto 0) := (others => '0');
    signal miso_r : std_logic := '0';
    signal taken_r : std_logic := '0';

    signal since_start    : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal since_launch   : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal first_cap_seen : std_logic := '0';
    signal launch_pending : std_logic := '0';
    signal lead_seen_r    : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal lead_short_r   : std_logic := '0';
    signal half_seen_r    : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal half_short_r   : std_logic := '0';

    constant ALL_ONES : unsigned(CNT_W - 1 downto 0) := (others => '1');

    -- A fixed full-width reversal: pure wiring.
    function rev_all(x : std_logic_vector(MAX_W - 1 downto 0))
        return std_logic_vector is
        variable r : std_logic_vector(MAX_W - 1 downto 0);
    begin
        for b in 0 to MAX_W - 1 loop
            r(b) := x(MAX_W - 1 - b);
        end loop;
        return r;
    end function;

    signal n_bits     : natural;
    signal tx_ordered : std_logic_vector(MAX_W - 1 downto 0);
    signal tx_aligned : std_logic_vector(MAX_W - 1 downto 0);
    signal drive_stb  : std_logic;
    signal word_start : std_logic;
    signal src        : std_logic_vector(MAX_W - 1 downto 0);
    signal last_of_word : std_logic;

begin

    miso           <= miso_r;
    bits_driven    <= drives;
    word_taken_stb <= taken_r;
    lead_seen      <= lead_seen_r;
    lead_short     <= lead_short_r;
    half_seen      <= half_seen_r;
    half_short     <= half_short_r;

    n_bits <= 1 when len = 0
              else MAX_W when to_integer(len) > MAX_W
              else to_integer(len);

    -- Bit order is the same self-inverse transform the master uses (13.8), and the
    -- left-align is the same asymmetry the master's transmit side has (13.6): the
    -- first bit out must be in the top position.
    tx_ordered <= std_logic_vector(shift_right(unsigned(rev_all(tx_data)),
                                              MAX_W - n_bits))
                  when lsb_first = '1' else tx_data;
    tx_aligned <= std_logic_vector(shift_left(unsigned(tx_ordered),
                                             MAX_W - n_bits));

    drive_stb <= preload_stb or launch_stb;

    -- `drives = 0` selects the freshly presented word over the shift register,
    -- which is what makes a one-bit frame work: there, every drive is the first
    -- drive of a word, so every drive takes a new word and the shift register is
    -- never consulted.
    word_start   <= '1' when drives = 0 else '0';
    src          <= tx_aligned when word_start = '1' else tx_sr;
    last_of_word <= '1' when drives = (len - 1) else '0';

    launch : process (clk, rst_n)
    begin
        if rst_n = '0' then
            tx_sr          <= (others => '0');
            drives         <= (others => '0');
            miso_r         <= '0';
            taken_r        <= '0';
            since_start    <= (others => '0');
            since_launch   <= (others => '0');
            first_cap_seen <= '0';
            launch_pending <= '0';
            lead_seen_r    <= (others => '0');
            lead_short_r   <= '0';
            half_seen_r    <= (others => '0');
            half_short_r   <= '0';
        elsif rising_edge(clk) then
            taken_r <= '0';

            if clr_flags = '1' then
                lead_short_r <= '0';
                half_short_r <= '0';
            end if;

            -- the lead measurement, in RECOVERED cycles so the latency cancels
            if txn_start_stb = '1' then
                since_start <= to_unsigned(1, CNT_W);
                -- A capture on the START cycle is an interval of zero, and it must
                -- be recorded here or it is not recorded at all. Without this the
                -- measurement silently reports the SECOND bit's interval, which is
                -- a whole SCLK period and therefore never looks short: the flag
                -- goes quiet at exactly the shortest leads.
                if cap_stb = '1' then
                    first_cap_seen <= '1';
                    lead_seen_r    <= (others => '0');
                    lead_short_r   <= '1';
                else
                    first_cap_seen <= '0';
                end if;
            elsif cap_stb = '1' and txn_active = '1' and first_cap_seen = '0' then
                first_cap_seen <= '1';
                lead_seen_r    <= since_start;
                if to_integer(since_start) < LEAD_MIN then
                    lead_short_r <= '1';
                end if;
            elsif since_start /= ALL_ONES then
                since_start <= since_start + 1;
            end if;

            -- the half-period measurement, also in recovered cycles
            if drive_stb = '1' then
                since_launch   <= to_unsigned(1, CNT_W);
                launch_pending <= '1';
            elsif cap_stb = '1' and txn_active = '1' and launch_pending = '1' then
                launch_pending <= '0';
                half_seen_r    <= since_launch;
                if to_integer(since_launch) < HALF_MIN then
                    half_short_r <= '1';
                end if;
            elsif since_launch /= ALL_ONES then
                since_launch <= since_launch + 1;
            end if;

            -- the datapath
            if txn_start_stb = '1' then
                if drive_stb = '1' then
                    -- CPHA=0: the preload is on this very cycle, so the load and
                    -- the first drive coincide -- and `tx_sr` has not been loaded
                    -- yet, so the bit must come from `tx_aligned` directly.
                    miso_r <= tx_aligned(MAX_W - 1);
                    tx_sr  <= tx_aligned(MAX_W - 2 downto 0) & '0';
                    if len = 1 then
                        -- A one-bit word is finished by the preload alone.
                        drives  <= (others => '0');
                        taken_r <= '1';
                    else
                        drives <= to_unsigned(1, LEN_W);
                    end if;
                else
                    -- CPHA=1: nothing is driven yet, and the first launch edge
                    -- will take the presented word because `drives` is zero.
                    --
                    -- The re-zero matters in BOTH cases. A CPHA=0 transaction ends
                    -- with one more launch than it has bits -- the launch this
                    -- slave cannot refuse -- so it leaves `drives` at one, and
                    -- without clearing it here the next transaction's first bit
                    -- comes from the stale shift register: the first byte of every
                    -- transaction after the first is wrong and every later byte is
                    -- right.
                    drives <= (others => '0');
                end if;
            elsif drive_stb = '1' and txn_active = '1' then
                miso_r <= src(MAX_W - 1);
                tx_sr  <= src(MAX_W - 2 downto 0) & '0';
                if last_of_word = '1' then
                    drives  <= (others => '0');
                    taken_r <= '1';
                else
                    drives <= drives + 1;
                end if;
            end if;
        end if;
    end process;

end architecture;

The testbench

Six tests. Two of them are sweeps that calibrate a threshold against the pin, which is the technique worth taking from this chapter.

  1. One byte, mode 0, a generous lead. The thing that must work before anything else is measured.
  2. The lead sweep. For each lead from 1 upward, the bench records two things independently: whether the master (modelled at the pin) sampled the correct first bit, and whether lead_short fired. It then asserts that these agree exactly — the flag fires on precisely the leads that were actually too short. A threshold off by one fails this and passes a bench that only checks "small leads are flagged".
  3. The half-period sweep. The same experiment on the other requirement.
  4. Multi-word transactions. The slave takes the next word at each word boundary, and word_taken_stb must fire once per word — the signal Chapter 14.9 uses to advance its buffer.
  5. The sweep. Both phases, both orders, several widths and ratios.
  6. What the slave cannot suppress. The final trailing edge changes MISO after the master's last capture, and the bench asserts that it does — because §3's reasoning says it must, and a future change that "fixed" it would break multi-word transactions.
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_tx_tb.sv — six tests, two of which calibrate a threshold against what the master actually sampled
// spi_slave_tx_tb.sv
//
// The decisive experiment in this chapter is the LEAD SWEEP, and what makes it an
// experiment rather than a check is that it calibrates the slave's own flag
// against the pin.
//
// For each value of the master's lead time, two independent facts are recorded:
//
//   (a) did the master sample the CORRECT first bit off the MISO pin?
//   (b) did the slave's `lead_short` flag fire?
//
// A flag that is right for the wrong reason agrees with (a) at most leads and
// disagrees at the boundary, so the test asserts that (a) and (b) agree at EVERY
// lead -- which pins the threshold exactly. That is a much stronger statement than
// "the flag fires when the lead is small", and it is the only way to know that the
// number in the parameter is the right number.
//
// The testbench is the master, so it samples MISO at the pin with no synchroniser,
// exactly as a real master does.

`timescale 1ns/1ps

module spi_slave_tx_tb;

    localparam int MAX_W    = 32;
    localparam int LEN_W    = 6;
    localparam int CNT_W    = 8;
    localparam int SYNC_N   = 2;
    localparam int LEAD_MIN = 3;   // recovered cycles; see the RTL header
    localparam int HALF_MIN = 3;   // recovered cycles; see the RTL header

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

    // --- 14.1 / 14.2 --------------------------------------------------------
    logic cpol     = 1'b0;
    logic sclk_pin = 1'b0;
    logic cs_n_pin = 1'b1;
    logic mosi_pin = 1'b0;
    logic clr_flags = 1'b0;

    wire        sclk_q, cs_active, mosi_q;
    wire        edge_a_stb, edge_b_stb;
    wire        cs_assert_stb, cs_deassert_stb;
    wire [11:0] min_half;
    wire        ratio_err;

    spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe (
        .clk(clk), .rst_n(rst_n), .cpol(cpol),
        .sclk_pin(sclk_pin), .cs_n_pin(cs_n_pin), .mosi_pin(mosi_pin),
        .sclk_q(sclk_q), .cs_active(cs_active), .mosi_q(mosi_q),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
        .min_half(min_half), .ratio_err(ratio_err), .clr_flags(1'b0)
    );

    logic [LEN_W-1:0] len       = 6'd8;
    logic             lsb_first = 1'b0;
    logic             cpha_now  = 1'b0;

    wire             txn_active, txn_start_stb, txn_end_stb;
    wire [11:0]      edges_in_txn, frames_in_txn;
    wire             txn_clean, txn_trunc, txn_empty, txn_report_stb;
    wire [2:0]       cs_state;

    spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(12)) u_cs (
        .clk(clk), .rst_n(rst_n),
        .cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb), .len(len),
        .txn_active(txn_active), .txn_start_stb(txn_start_stb),
        .txn_end_stb(txn_end_stb),
        .edges_in_txn(edges_in_txn), .frames_in_txn(frames_in_txn),
        .txn_clean(txn_clean), .txn_trunc(txn_trunc), .txn_empty(txn_empty),
        .txn_report_stb(txn_report_stb), .state_id(cs_state)
    );

    // The mode logic is Chapter 14.6; here the phase selects the edges.
    wire cap_stb     = txn_active & (cpha_now ? edge_b_stb : edge_a_stb);
    wire launch_stb  = txn_active & (cpha_now ? edge_a_stb : edge_b_stb);
    wire preload_stb = txn_start_stb & ~cpha_now;

    // --- 14.4, the block under test -----------------------------------------
    logic [MAX_W-1:0] tx_words [0:15];
    integer           tx_idx;
    wire  [MAX_W-1:0] tx_data = tx_words[tx_idx];

    wire             miso;
    wire             word_taken_stb;
    wire [LEN_W-1:0] bits_driven;
    wire [CNT_W-1:0] lead_seen;
    wire             lead_short;
    wire [CNT_W-1:0] half_seen;
    wire             half_short;

    spi_slave_tx #(.MAX_W(MAX_W), .LEN_W(LEN_W), .LEAD_MIN(LEAD_MIN),
                   .HALF_MIN(HALF_MIN), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .txn_active(txn_active), .txn_start_stb(txn_start_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb), .cap_stb(cap_stb),
        .len(len), .lsb_first(lsb_first), .tx_data(tx_data),
        .miso(miso),
        .word_taken_stb(word_taken_stb), .bits_driven(bits_driven),
        .lead_seen(lead_seen), .lead_short(lead_short),
        .half_seen(half_seen), .half_short(half_short), .clr_flags(clr_flags)
    );

    // The system side: advance to the next word each time one is consumed.
    // Reset on the DEASSERT of the previous transaction rather than on the assert
    // of this one -- at the assert cycle the slave is already reading `tx_data`,
    // so an index that is still being cleared on that cycle hands it the wrong
    // word, and the symptom is that every transaction after the first sends
    // zeros. One driver, and it settles before it is needed.
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)                tx_idx <= 0;
        else if (cs_deassert_stb)  tx_idx <= 0;
        else if (word_taken_stb)   tx_idx <= tx_idx + 1;
    end

    // --- the master's own sampling, at the pin ------------------------------
    integer got_bits [0:63];
    integer got_n;
    integer miso_moves_in_tail;
    integer n_txns;
    logic   watch_tail;
    logic   miso_q_tb;

    always_ff @(posedge clk) begin
        miso_q_tb <= miso;
        if (watch_tail && miso !== miso_q_tb)
            miso_moves_in_tail <= miso_moves_in_tail + 1;
    end

    integer errors = 0;

    task automatic adv(input integer n);
        begin repeat (n) @(negedge clk); end
    endtask

    // One transaction. `lead` is the master's t_CSS in system clocks, which is
    // the parameter the sweep varies. MISO is sampled at each capture edge, at
    // the pin, with no synchroniser -- as a real master does.
    task automatic drive_txn(input integer lead, input integer half,
                             input bit pol, input bit pha,
                             input integer nbits, input integer nwords);
        integer w, i;
        begin
            cpol      = pol;
            cpha_now  = pha;
            len       = nbits[LEN_W-1:0];
            sclk_pin  = pol;
            cs_n_pin  = 1'b1;
            watch_tail = 1'b0;
            adv(8);
            got_n = 0;

            cs_n_pin = 1'b0;
            adv(lead);

            for (w = 0; w < nwords; w = w + 1)
                for (i = 0; i < nbits; i = i + 1) begin
                    if (!pha) begin
                        // Capture on the leading edge: sample MISO as the edge
                        // is driven.
                        got_bits[got_n] = miso;
                        got_n = got_n + 1;
                        sclk_pin = ~pol;
                        adv(half);
                        sclk_pin = pol;         // trailing: the slave launches
                        adv(half);
                    end else begin
                        sclk_pin = ~pol;        // leading: the slave launches
                        adv(half);
                        got_bits[got_n] = miso; // capture on the trailing edge
                        got_n = got_n + 1;
                        sclk_pin = pol;
                        adv(half);
                    end
                end

            // The tail: chip select is still asserted and the data is done, so
            // MISO must not move again. This is where an unsuppressed final
            // launch shows up.
            watch_tail = 1'b1;
            adv(half * 3);
            watch_tail = 1'b0;
            cs_n_pin = 1'b1;
            adv(8);
            sclk_pin = pol;
            adv(8);
            n_txns = n_txns + 1;
        end
    endtask

    // Was the word the master sampled the word the slave was given?
    function automatic integer word_matches(input integer w,
                                            input integer nbits,
                                            input bit lsb);
        integer i, want, bad;
        begin
            bad = 0;
            for (i = 0; i < nbits; i = i + 1) begin
                want = lsb ? tx_words[w][i] : tx_words[w][nbits-1-i];
                if (got_bits[w*nbits + i] !== want) bad = bad + 1;
            end
            word_matches = (bad == 0);
        end
    endfunction

    integer lead, k, p, o, h, nb, nw, seed, bad;
    integer first_ok, flag, disagreements, boundary_ok, boundary_bad;
    integer all_ok, hb_ok, hb_bad, hdis;

    initial begin
        got_n = 0; miso_moves_in_tail = 0; watch_tail = 1'b0; n_txns = 0;
        seed = 32'h7A1C_0055;
        for (k = 0; k < 16; k = k + 1) tx_words[k] = 32'h0;

        adv(3);
        rst_n = 1'b1;
        adv(2);

        // 1. ONE BYTE, MODE 0, a generous lead.
        tx_words[0] = 32'hA5;
        drive_txn(8, 4, 1'b0, 1'b0, 8, 1);
        if (!word_matches(0, 8, 1'b0)) begin
            $display("  FAIL: with a generous lead the master did not sample a5");
            errors = errors + 1;
        end
        if (lead_short) begin
            $display("  FAIL: a lead of 8 was reported as short");
            errors = errors + 1;
        end
        $display("  one byte, mode 0, lead 8: the master sampled the byte the slave was given, and the lead was not reported short");

        // 2. THE LEAD SWEEP -- the chapter's experiment. For each lead, record
        //    whether the PIN carried the right first bit and whether the slave's
        //    flag fired, then require that the two agree at every lead.
        // At every lead, either the pin carried the right first bit or the slave
        // reported the problem. There are TWO routes by which it can report: a
        // short lead, and -- at leads so short that the first edge arrives before
        // the slave knows it is selected at all -- a truncated transaction,
        // because that edge is discarded and the frame comes up one bit short.
        // Requiring "correct OR reported" rather than "correct XOR lead_short" is
        // the honest calibration, and it is what makes the boundary meaningful.
        disagreements = 0;
        boundary_ok   = 99;
        boundary_bad  = -1;
        for (lead = 0; lead <= 8; lead = lead + 1) begin
            clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
            tx_words[0] = 32'hA5;    // 1010_0101: first bit is 1, so a late
                                     // MISO showing 0 is visible
            drive_txn(lead, 4, 1'b0, 1'b0, 8, 1);
            first_ok = (got_bits[0] === tx_words[0][7]);
            flag     = lead_short | txn_trunc;
            $display("    lead=%0d: first bit %0s  lead_seen=%0d  short=%0b trunc=%0b",
                     lead, first_ok ? "correct" : "WRONG  ", lead_seen,
                     lead_short, txn_trunc);
            if (!first_ok && !flag) disagreements = disagreements + 1;
            if (first_ok && flag)   disagreements = disagreements + 1;
            if (first_ok && lead < boundary_ok)  boundary_ok  = lead;
            if (!first_ok && lead > boundary_bad) boundary_bad = lead;
        end
        if (disagreements != 0) begin
            $display("  FAIL: at %0d of 9 leads the slave's report did not match the pin",
                     disagreements);
            errors = errors + 1;
        end
        if (boundary_ok != boundary_bad + 1) begin
            $display("  FAIL: the boundary is not sharp -- correct from %0d, wrong up to %0d",
                     boundary_ok, boundary_bad);
            errors = errors + 1;
        end
        // SYNC_N for the synchroniser, one more for 14.2's registered
        // transaction strobe, one more for this block's own output register.
        if (boundary_ok != SYNC_N + 2) begin
            $display("  FAIL: the first bit became correct at a lead of %0d, expected %0d",
                     boundary_ok, SYNC_N + 2);
            errors = errors + 1;
        end
        $display("  the lead sweep: the first bit is wrong for every lead below %0d and correct from %0d upward, the boundary is sharp, and at all nine leads the slave reported exactly when the pin was wrong",
                 boundary_ok, boundary_ok);

        // 3. THE HALF-PERIOD SWEEP -- the same experiment on the other
        //    requirement. Every bit after the first is delayed by the same
        //    recovery latency, so the master must allow at least SYNC_N + 2
        //    system clocks between its launch edge and its capture edge. The
        //    flag is calibrated against the pin exactly as the lead flag was.
        //    Note that this boundary is ONE LOWER than the lead's: the lead waits
        //    for Chapter 14.2's registered transaction boundary and a launch does
        //    not, so one slave has two different requirements.
        hdis   = 0;
        hb_ok  = 99;
        hb_bad = -1;
        for (h = 1; h <= 7; h = h + 1) begin
            clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
            tx_words[0] = 32'hA5;
            tx_words[1] = 32'h3C;
            drive_txn(8, h, 1'b0, 1'b0, 8, 2);
            all_ok = word_matches(0, 8, 1'b0) && word_matches(1, 8, 1'b0);
            $display("    half=%0d: data %0s  half_seen=%0d  half_short=%0b",
                     h, all_ok ? "correct" : "WRONG  ", half_seen, half_short);
            if (all_ok == (half_short != 0)) hdis = hdis + 1;
            if (all_ok && h < hb_ok)   hb_ok  = h;
            if (!all_ok && h > hb_bad) hb_bad = h;
        end
        if (hdis != 0) begin
            $display("  FAIL: the half-period flag disagreed with the pin at %0d of 7 ratios",
                     hdis);
            errors = errors + 1;
        end
        if (hb_ok != hb_bad + 1) begin
            $display("  FAIL: the half-period boundary is not sharp -- correct from %0d, wrong up to %0d",
                     hb_ok, hb_bad);
            errors = errors + 1;
        end
        if (hb_ok != SYNC_N + 1) begin
            $display("  FAIL: the data became correct at a half-period of %0d, expected %0d",
                     hb_ok, SYNC_N + 1);
            errors = errors + 1;
        end
        $display("  the half-period sweep: the data is wrong for every half below %0d and correct from %0d upward, the boundary is sharp, and the flag agrees with the pin at all seven ratios",
                 hb_ok, hb_ok);

        // 4. MULTI-WORD TRANSACTIONS. The slave takes the next word at each word
        //    boundary without the system knowing where those boundaries are.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        for (k = 0; k < 6; k = k + 1) begin
            seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
            tx_words[k] = (seed >> 9) & 32'hFF;
        end
        drive_txn(8, 4, 1'b0, 1'b0, 8, 6);
        bad = 0;
        for (k = 0; k < 6; k = k + 1)
            if (!word_matches(k, 8, 1'b0)) bad = bad + 1;
        if (bad != 0) begin
            $display("  FAIL: %0d of 6 words in a multi-word transaction were wrong",
                     bad);
            errors = errors + 1;
        end
        $display("  six words in one transaction: each taken at its own boundary and each sampled correctly");

        // 5. THE SWEEP. Both phases, both orders, several widths and ratios --
        //    all at or above the half-period the requirement above establishes.
        for (o = 0; o <= 1; o = o + 1)
            for (p = 0; p <= 1; p = p + 1)
                for (h = 3; h <= 7; h = h + 2) begin
                    nb = 8; nw = 3;
                    lsb_first = o[0];
                    for (k = 0; k < nw; k = k + 1) begin
                        seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
                        tx_words[k] = (seed >> 11) & 32'hFF;
                    end
                    drive_txn(8, h, 1'b0, p[0], nb, nw);
                    for (k = 0; k < nw; k = k + 1)
                        if (!word_matches(k, nb, o[0])) begin
                            $display("  FAIL: order=%0d phase=%0d half=%0d word %0d wrong",
                                     o, p, h, k);
                            errors = errors + 1;
                        end
                end
        // And the widths, MSB-first.
        lsb_first = 1'b0;
        for (nb = 1; nb <= 13; nb = nb + 4) begin
            for (k = 0; k < 3; k = k + 1) begin
                seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
                tx_words[k] = seed & ((nb >= 32) ? 32'hFFFF_FFFF
                                                 : ((32'h1 << nb) - 1));
            end
            drive_txn(8, 4, 1'b0, 1'b0, nb, 3);
            for (k = 0; k < 3; k = k + 1)
                if (!word_matches(k, nb, 1'b0)) begin
                    $display("  FAIL: width %0d word %0d wrong", nb, k);
                    errors = errors + 1;
                end
        end
        $display("  both phases, both bit orders, three ratios at or above the required half-period, and widths of 1, 5, 9 and 13 bits all sampled correctly");

        // 6. WHAT THE SLAVE CANNOT SUPPRESS. The master's final trailing edge
        //    launches a bit that will never be read, and the slave has no way to
        //    know it is the last -- a trailing edge is indistinguishable from the
        //    start of another word. So MISO may move ONCE more per transaction
        //    and no more: one launch it could not refuse, and nothing after it.
        if (miso_moves_in_tail > n_txns) begin
            $display("  FAIL: MISO moved %0d times in the tails of %0d transactions, at most one each is allowed",
                     miso_moves_in_tail, n_txns);
            errors = errors + 1;
        end
        $display("  MISO moved %0d times in the tails of %0d transactions -- at most the one launch a slave cannot refuse, and nothing after it",
                 miso_moves_in_tail, n_txns);

        if (errors == 0)
            $display("PASS: the slave's first bit is driven from the transaction start rather than from a command it does not receive, so its earliest possible appearance is SYNC_N plus two system clocks after chip select falls -- two rather than one because Chapter 14.2 republishes the boundary as a registered strobe so that every block agrees about it -- and the lead sweep shows exactly that: the master samples the wrong first bit at every lead below %0d and the right one from %0d upward, with a sharp boundary, and at all nine leads the slave reported the problem exactly when the pin was wrong -- by a short lead where the interval could be measured and by a truncated transaction where the first edge arrived before the slave knew it was selected -- a multi-word transaction takes its next word at each boundary without the system knowing where those boundaries are, both phases, both bit orders, three frequency ratios and widths from 1 to 13 bits all deliver the word the slave was given, a second sweep establishes that the master must also allow at least SYNC_N plus ONE system clocks between its launch edge and its capture edge -- one fewer than the lead, because the lead waits for Chapter 14.2's registered boundary and a launch does not, so one slave has two different requirements -- and that the slave's half-period flag agrees with the pin at all seven ratios, and MISO moves at most once more per transaction -- the one launch a slave cannot refuse, because a trailing edge is indistinguishable from the first edge of another word and only chip select rising says otherwise",
                     SYNC_N + 2, SYNC_N + 2);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_tx_tb.v — the same bench in Verilog-2001
// spi_slave_tx_tb.v
//
// The decisive experiment in this chapter is the LEAD SWEEP, and what makes it an
// experiment rather than a check is that it calibrates the slave's own flag
// against the pin.
//
// For each value of the master's lead time, two independent facts are recorded:
//
//   (a) did the master sample the CORRECT first bit off the MISO pin?
//   (b) did the slave's `lead_short` flag fire?
//
// A flag that is right for the wrong reason agrees with (a) at most leads and
// disagrees at the boundary, so the test asserts that (a) and (b) agree at EVERY
// lead -- which pins the threshold exactly. That is a much stronger statement than
// "the flag fires when the lead is small", and it is the only way to know that the
// number in the parameter is the right number.
//
// The testbench is the master, so it samples MISO at the pin with no synchroniser,
// exactly as a real master does.

`timescale 1ns/1ps

module spi_slave_tx_tb;

    localparam MAX_W    = 32;
    localparam LEN_W    = 6;
    localparam CNT_W    = 8;
    localparam SYNC_N   = 2;
    localparam LEAD_MIN = 3;   // recovered cycles; see the RTL header
    localparam HALF_MIN = 3;   // recovered cycles; see the RTL header

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

    // --- 14.1 / 14.2 --------------------------------------------------------
    reg cpol;
    reg sclk_pin;
    reg cs_n_pin;
    reg mosi_pin;
    reg clr_flags;

    wire        sclk_q, cs_active, mosi_q;
    wire        edge_a_stb, edge_b_stb;
    wire        cs_assert_stb, cs_deassert_stb;
    wire [11:0] min_half;
    wire        ratio_err;

    spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe (
        .clk(clk), .rst_n(rst_n), .cpol(cpol),
        .sclk_pin(sclk_pin), .cs_n_pin(cs_n_pin), .mosi_pin(mosi_pin),
        .sclk_q(sclk_q), .cs_active(cs_active), .mosi_q(mosi_q),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
        .min_half(min_half), .ratio_err(ratio_err), .clr_flags(1'b0)
    );

    reg [LEN_W-1:0] len;
    reg             lsb_first;
    reg             cpha_now;

    wire             txn_active, txn_start_stb, txn_end_stb;
    wire [11:0]      edges_in_txn, frames_in_txn;
    wire             txn_clean, txn_trunc, txn_empty, txn_report_stb;
    wire [2:0]       cs_state;

    spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(12)) u_cs (
        .clk(clk), .rst_n(rst_n),
        .cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb), .len(len),
        .txn_active(txn_active), .txn_start_stb(txn_start_stb),
        .txn_end_stb(txn_end_stb),
        .edges_in_txn(edges_in_txn), .frames_in_txn(frames_in_txn),
        .txn_clean(txn_clean), .txn_trunc(txn_trunc), .txn_empty(txn_empty),
        .txn_report_stb(txn_report_stb), .state_id(cs_state)
    );

    // The mode logic is Chapter 14.6; here the phase selects the edges.
    wire cap_stb     = txn_active & (cpha_now ? edge_b_stb : edge_a_stb);
    wire launch_stb  = txn_active & (cpha_now ? edge_a_stb : edge_b_stb);
    wire preload_stb = txn_start_stb & ~cpha_now;

    // --- 14.4, the block under test -----------------------------------------
    reg [MAX_W-1:0] tx_words [0:15];
    integer           tx_idx;
    wire  [MAX_W-1:0] tx_data = tx_words[tx_idx];

    wire             miso;
    wire             word_taken_stb;
    wire [LEN_W-1:0] bits_driven;
    wire [CNT_W-1:0] lead_seen;
    wire             lead_short;
    wire [CNT_W-1:0] half_seen;
    wire             half_short;

    spi_slave_tx #(.MAX_W(MAX_W), .LEN_W(LEN_W), .LEAD_MIN(LEAD_MIN),
                   .HALF_MIN(HALF_MIN), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .txn_active(txn_active), .txn_start_stb(txn_start_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb), .cap_stb(cap_stb),
        .len(len), .lsb_first(lsb_first), .tx_data(tx_data),
        .miso(miso),
        .word_taken_stb(word_taken_stb), .bits_driven(bits_driven),
        .lead_seen(lead_seen), .lead_short(lead_short),
        .half_seen(half_seen), .half_short(half_short), .clr_flags(clr_flags)
    );

    // The system side: advance to the next word each time one is consumed.
    // Reset on the DEASSERT of the previous transaction rather than on the assert
    // of this one -- at the assert cycle the slave is already reading `tx_data`,
    // so an index that is still being cleared on that cycle hands it the wrong
    // word, and the symptom is that every transaction after the first sends
    // zeros. One driver, and it settles before it is needed.
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)                tx_idx <= 0;
        else if (cs_deassert_stb)  tx_idx <= 0;
        else if (word_taken_stb)   tx_idx <= tx_idx + 1;
    end

    // --- the master's own sampling, at the pin ------------------------------
    integer got_bits [0:63];
    integer got_n;
    integer miso_moves_in_tail;
    integer n_txns;
    reg   watch_tail;
    reg   miso_q_tb;

    always @(posedge clk) begin
        miso_q_tb <= miso;
        if (watch_tail && miso !== miso_q_tb)
            miso_moves_in_tail <= miso_moves_in_tail + 1;
    end

    integer errors;

        task adv;
        input integer n;
        begin repeat (n) @(negedge clk); end
    endtask

    // One transaction. `lead` is the master's t_CSS in system clocks, which is
    // the parameter the sweep varies. MISO is sampled at each capture edge, at
    // the pin, with no synchroniser -- as a real master does.
        task drive_txn;
        input integer lead;
        input integer half;
        input pol;
        input pha;
        input integer nbits;
        input integer nwords;
        integer w, i;
        begin
            cpol      = pol;
            cpha_now  = pha;
            len       = nbits[LEN_W-1:0];
            sclk_pin  = pol;
            cs_n_pin  = 1'b1;
            watch_tail = 1'b0;
            adv(8);
            got_n = 0;

            cs_n_pin = 1'b0;
            adv(lead);

            for (w = 0; w < nwords; w = w + 1)
                for (i = 0; i < nbits; i = i + 1) begin
                    if (!pha) begin
                        // Capture on the leading edge: sample MISO as the edge
                        // is driven.
                        got_bits[got_n] = miso;
                        got_n = got_n + 1;
                        sclk_pin = ~pol;
                        adv(half);
                        sclk_pin = pol;         // trailing: the slave launches
                        adv(half);
                    end else begin
                        sclk_pin = ~pol;        // leading: the slave launches
                        adv(half);
                        got_bits[got_n] = miso; // capture on the trailing edge
                        got_n = got_n + 1;
                        sclk_pin = pol;
                        adv(half);
                    end
                end

            // The tail: chip select is still asserted and the data is done, so
            // MISO must not move again. This is where an unsuppressed final
            // launch shows up.
            watch_tail = 1'b1;
            adv(half * 3);
            watch_tail = 1'b0;
            cs_n_pin = 1'b1;
            adv(8);
            sclk_pin = pol;
            adv(8);
            n_txns = n_txns + 1;
        end
    endtask

    // Was the word the master sampled the word the slave was given?
        function integer word_matches;
        input integer w;
        input integer nbits;
        input lsb;
        integer i, want, bad;
        begin
            bad = 0;
            for (i = 0; i < nbits; i = i + 1) begin
                want = lsb ? tx_words[w][i] : tx_words[w][nbits-1-i];
                if (got_bits[w*nbits + i] !== want) bad = bad + 1;
            end
            word_matches = (bad == 0);
        end
    endfunction

    integer lead, k, p, o, h, nb, nw, seed, bad;
    integer first_ok, flag, disagreements, boundary_ok, boundary_bad;
    integer all_ok, hb_ok, hb_bad, hdis;

    initial begin
        got_n = 0; miso_moves_in_tail = 0; watch_tail = 1'b0; n_txns = 0;
        seed = 32'h7A1C_0055;
        for (k = 0; k < 16; k = k + 1) tx_words[k] = 32'h0;

        adv(3);
        rst_n = 1'b1;
        adv(2);

        // 1. ONE BYTE, MODE 0, a generous lead.
        tx_words[0] = 32'hA5;
        drive_txn(8, 4, 1'b0, 1'b0, 8, 1);
        if (!word_matches(0, 8, 1'b0)) begin
            $display("  FAIL: with a generous lead the master did not sample a5");
            errors = errors + 1;
        end
        if (lead_short) begin
            $display("  FAIL: a lead of 8 was reported as short");
            errors = errors + 1;
        end
        $display("  one byte, mode 0, lead 8: the master sampled the byte the slave was given, and the lead was not reported short");

        // 2. THE LEAD SWEEP -- the chapter's experiment. For each lead, record
        //    whether the PIN carried the right first bit and whether the slave's
        //    flag fired, then require that the two agree at every lead.
        // At every lead, either the pin carried the right first bit or the slave
        // reported the problem. There are TWO routes by which it can report: a
        // short lead, and -- at leads so short that the first edge arrives before
        // the slave knows it is selected at all -- a truncated transaction,
        // because that edge is discarded and the frame comes up one bit short.
        // Requiring "correct OR reported" rather than "correct XOR lead_short" is
        // the honest calibration, and it is what makes the boundary meaningful.
        disagreements = 0;
        boundary_ok   = 99;
        boundary_bad  = -1;
        for (lead = 0; lead <= 8; lead = lead + 1) begin
            clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
            tx_words[0] = 32'hA5;    // 1010_0101: first bit is 1, so a late
                                     // MISO showing 0 is visible
            drive_txn(lead, 4, 1'b0, 1'b0, 8, 1);
            first_ok = (got_bits[0] === tx_words[0][7]);
            flag     = lead_short | txn_trunc;
            $display("    lead=%0d: first bit %0s  lead_seen=%0d  short=%0b trunc=%0b",
                     lead, first_ok ? "correct" : "WRONG  ", lead_seen,
                     lead_short, txn_trunc);
            if (!first_ok && !flag) disagreements = disagreements + 1;
            if (first_ok && flag)   disagreements = disagreements + 1;
            if (first_ok && lead < boundary_ok)  boundary_ok  = lead;
            if (!first_ok && lead > boundary_bad) boundary_bad = lead;
        end
        if (disagreements != 0) begin
            $display("  FAIL: at %0d of 9 leads the slave's report did not match the pin",
                     disagreements);
            errors = errors + 1;
        end
        if (boundary_ok != boundary_bad + 1) begin
            $display("  FAIL: the boundary is not sharp -- correct from %0d, wrong up to %0d",
                     boundary_ok, boundary_bad);
            errors = errors + 1;
        end
        // SYNC_N for the synchroniser, one more for 14.2's registered
        // transaction strobe, one more for this block's own output register.
        if (boundary_ok != SYNC_N + 2) begin
            $display("  FAIL: the first bit became correct at a lead of %0d, expected %0d",
                     boundary_ok, SYNC_N + 2);
            errors = errors + 1;
        end
        $display("  the lead sweep: the first bit is wrong for every lead below %0d and correct from %0d upward, the boundary is sharp, and at all nine leads the slave reported exactly when the pin was wrong",
                 boundary_ok, boundary_ok);

        // 3. THE HALF-PERIOD SWEEP -- the same experiment on the other
        //    requirement. Every bit after the first is delayed by the same
        //    recovery latency, so the master must allow at least SYNC_N + 2
        //    system clocks between its launch edge and its capture edge. The
        //    flag is calibrated against the pin exactly as the lead flag was.
        //    Note that this boundary is ONE LOWER than the lead's: the lead waits
        //    for Chapter 14.2's registered transaction boundary and a launch does
        //    not, so one slave has two different requirements.
        hdis   = 0;
        hb_ok  = 99;
        hb_bad = -1;
        for (h = 1; h <= 7; h = h + 1) begin
            clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
            tx_words[0] = 32'hA5;
            tx_words[1] = 32'h3C;
            drive_txn(8, h, 1'b0, 1'b0, 8, 2);
            all_ok = word_matches(0, 8, 1'b0) && word_matches(1, 8, 1'b0);
            $display("    half=%0d: data %0s  half_seen=%0d  half_short=%0b",
                     h, all_ok ? "correct" : "WRONG  ", half_seen, half_short);
            if (all_ok == (half_short != 0)) hdis = hdis + 1;
            if (all_ok && h < hb_ok)   hb_ok  = h;
            if (!all_ok && h > hb_bad) hb_bad = h;
        end
        if (hdis != 0) begin
            $display("  FAIL: the half-period flag disagreed with the pin at %0d of 7 ratios",
                     hdis);
            errors = errors + 1;
        end
        if (hb_ok != hb_bad + 1) begin
            $display("  FAIL: the half-period boundary is not sharp -- correct from %0d, wrong up to %0d",
                     hb_ok, hb_bad);
            errors = errors + 1;
        end
        if (hb_ok != SYNC_N + 1) begin
            $display("  FAIL: the data became correct at a half-period of %0d, expected %0d",
                     hb_ok, SYNC_N + 1);
            errors = errors + 1;
        end
        $display("  the half-period sweep: the data is wrong for every half below %0d and correct from %0d upward, the boundary is sharp, and the flag agrees with the pin at all seven ratios",
                 hb_ok, hb_ok);

        // 4. MULTI-WORD TRANSACTIONS. The slave takes the next word at each word
        //    boundary without the system knowing where those boundaries are.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        for (k = 0; k < 6; k = k + 1) begin
            seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
            tx_words[k] = (seed >> 9) & 32'hFF;
        end
        drive_txn(8, 4, 1'b0, 1'b0, 8, 6);
        bad = 0;
        for (k = 0; k < 6; k = k + 1)
            if (!word_matches(k, 8, 1'b0)) bad = bad + 1;
        if (bad != 0) begin
            $display("  FAIL: %0d of 6 words in a multi-word transaction were wrong",
                     bad);
            errors = errors + 1;
        end
        $display("  six words in one transaction: each taken at its own boundary and each sampled correctly");

        // 5. THE SWEEP. Both phases, both orders, several widths and ratios --
        //    all at or above the half-period the requirement above establishes.
        for (o = 0; o <= 1; o = o + 1)
            for (p = 0; p <= 1; p = p + 1)
                for (h = 3; h <= 7; h = h + 2) begin
                    nb = 8; nw = 3;
                    lsb_first = o[0];
                    for (k = 0; k < nw; k = k + 1) begin
                        seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
                        tx_words[k] = (seed >> 11) & 32'hFF;
                    end
                    drive_txn(8, h, 1'b0, p[0], nb, nw);
                    for (k = 0; k < nw; k = k + 1)
                        if (!word_matches(k, nb, o[0])) begin
                            $display("  FAIL: order=%0d phase=%0d half=%0d word %0d wrong",
                                     o, p, h, k);
                            errors = errors + 1;
                        end
                end
        // And the widths, MSB-first.
        lsb_first = 1'b0;
        for (nb = 1; nb <= 13; nb = nb + 4) begin
            for (k = 0; k < 3; k = k + 1) begin
                seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
                tx_words[k] = seed & ((nb >= 32) ? 32'hFFFF_FFFF
                                                 : ((32'h1 << nb) - 1));
            end
            drive_txn(8, 4, 1'b0, 1'b0, nb, 3);
            for (k = 0; k < 3; k = k + 1)
                if (!word_matches(k, nb, 1'b0)) begin
                    $display("  FAIL: width %0d word %0d wrong", nb, k);
                    errors = errors + 1;
                end
        end
        $display("  both phases, both bit orders, three ratios at or above the required half-period, and widths of 1, 5, 9 and 13 bits all sampled correctly");

        // 6. WHAT THE SLAVE CANNOT SUPPRESS. The master's final trailing edge
        //    launches a bit that will never be read, and the slave has no way to
        //    know it is the last -- a trailing edge is indistinguishable from the
        //    start of another word. So MISO may move ONCE more per transaction
        //    and no more: one launch it could not refuse, and nothing after it.
        if (miso_moves_in_tail > n_txns) begin
            $display("  FAIL: MISO moved %0d times in the tails of %0d transactions, at most one each is allowed",
                     miso_moves_in_tail, n_txns);
            errors = errors + 1;
        end
        $display("  MISO moved %0d times in the tails of %0d transactions -- at most the one launch a slave cannot refuse, and nothing after it",
                 miso_moves_in_tail, n_txns);

        if (errors == 0)
            $display("PASS: the slave's first bit is driven from the transaction start rather than from a command it does not receive, so its earliest possible appearance is SYNC_N plus two system clocks after chip select falls -- two rather than one because Chapter 14.2 republishes the boundary as a registered strobe so that every block agrees about it -- and the lead sweep shows exactly that: the master samples the wrong first bit at every lead below %0d and the right one from %0d upward, with a sharp boundary, and at all nine leads the slave reported the problem exactly when the pin was wrong -- by a short lead where the interval could be measured and by a truncated transaction where the first edge arrived before the slave knew it was selected -- a multi-word transaction takes its next word at each boundary without the system knowing where those boundaries are, both phases, both bit orders, three frequency ratios and widths from 1 to 13 bits all deliver the word the slave was given, a second sweep establishes that the master must also allow at least SYNC_N plus ONE system clocks between its launch edge and its capture edge -- one fewer than the lead, because the lead waits for Chapter 14.2's registered boundary and a launch does not, so one slave has two different requirements -- and that the slave's half-period flag agrees with the pin at all seven ratios, and MISO moves at most once more per transaction -- the one launch a slave cannot refuse, because a trailing edge is indistinguishable from the first edge of another word and only chip select rising says otherwise",
                     SYNC_N + 2, SYNC_N + 2);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b0;
        cpol = 1'b0;
        sclk_pin = 1'b0;
        cs_n_pin = 1'b1;
        mosi_pin = 1'b0;
        clr_flags = 1'b0;
        len = 6'd8;
        lsb_first = 1'b0;
        cpha_now = 1'b0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_tx_tb.vhd — the same bench in VHDL
-- spi_slave_tx_tb.vhd
--
-- The decisive experiments in this chapter are the LEAD SWEEP and the HALF-PERIOD
-- SWEEP, and what makes them experiments rather than checks is that each calibrates
-- one of the slave's own flags against the pin.
--
-- For each value of the parameter being swept, two independent facts are recorded:
--
--   (a) did the master sample the CORRECT bits off the MISO pin?
--   (b) did the slave report a problem?
--
-- A flag that is right for the wrong reason agrees at most values and disagrees at
-- the boundary, so the test requires agreement at EVERY value -- which pins the
-- threshold exactly. That is a much stronger statement than "the flag fires when the
-- interval is small", and it is the only way to know the number in the parameter is
-- the right number.
--
-- The testbench is the master, so it samples MISO at the pin with no synchroniser.

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

entity spi_slave_tx_tb is
end entity;

architecture sim of spi_slave_tx_tb is

    constant MAX_W    : positive := 32;
    constant LEN_W    : positive := 6;
    constant CNT_W    : positive := 8;
    constant SYNC_N   : positive := 2;
    constant LEAD_MIN : positive := 3;
    constant HALF_MIN : positive := 3;

    type word_array is array (0 to 15) of std_logic_vector(MAX_W - 1 downto 0);
    type bit_array  is array (0 to 63) of std_logic;

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

    signal cpol      : std_logic := '0';
    signal sclk_pin  : std_logic := '0';
    signal cs_n_pin  : std_logic := '1';
    signal mosi_pin  : std_logic := '0';
    signal clr_flags : std_logic := '0';

    signal sclk_q, cs_active, mosi_q : std_logic;
    signal edge_a_stb, edge_b_stb    : std_logic;
    signal cs_assert_stb, cs_deassert_stb : std_logic;
    signal min_half  : unsigned(11 downto 0);
    signal ratio_err : std_logic;

    signal len       : unsigned(LEN_W - 1 downto 0) := to_unsigned(8, LEN_W);
    signal lsb_first : std_logic := '0';
    signal cpha_now  : std_logic := '0';

    signal txn_active, txn_start_stb, txn_end_stb : std_logic;
    signal edges_in_txn, frames_in_txn : unsigned(11 downto 0);
    signal txn_clean, txn_trunc, txn_empty, txn_report_stb : std_logic;
    signal cs_state : unsigned(2 downto 0);

    signal cap_stb, launch_stb, preload_stb : std_logic;

    signal tx_words : word_array := (others => (others => '0'));
    signal tx_idx   : natural := 0;
    signal tx_data  : std_logic_vector(MAX_W - 1 downto 0);

    signal miso           : std_logic;
    signal word_taken_stb : std_logic;
    signal bits_driven    : unsigned(LEN_W - 1 downto 0);
    signal lead_seen, half_seen : unsigned(CNT_W - 1 downto 0);
    signal lead_short, half_short : std_logic;

    signal got_bits : bit_array := (others => '0');
    signal got_n    : natural := 0;
    signal miso_moves_in_tail : natural := 0;
    signal n_txns   : natural := 0;
    signal watch_tail : std_logic := '0';
    signal miso_q_tb  : std_logic := '0';
    signal samp_stb   : std_logic := '0';
    signal samp_val   : std_logic := '0';
    signal clr_got    : std_logic := '0';

    signal errors : natural := 0;

begin

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

    u_fe : entity work.spi_slave_frontend
        generic map (SYNC_N => SYNC_N, HALF_MIN => 3, CNT_W => 12)
        port map (clk => clk, rst_n => rst_n, cpol => cpol,
                  sclk_pin => sclk_pin, cs_n_pin => cs_n_pin,
                  mosi_pin => mosi_pin,
                  sclk_q => sclk_q, cs_active => cs_active, mosi_q => mosi_q,
                  edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
                  cs_assert_stb => cs_assert_stb,
                  cs_deassert_stb => cs_deassert_stb,
                  min_half => min_half, ratio_err => ratio_err,
                  clr_flags => '0');

    u_cs : entity work.spi_slave_cs
        generic map (LEN_W => LEN_W, CNT_W => 12)
        port map (clk => clk, rst_n => rst_n,
                  cs_assert_stb => cs_assert_stb,
                  cs_deassert_stb => cs_deassert_stb,
                  edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
                  len => len,
                  txn_active => txn_active, txn_start_stb => txn_start_stb,
                  txn_end_stb => txn_end_stb,
                  edges_in_txn => edges_in_txn, frames_in_txn => frames_in_txn,
                  txn_clean => txn_clean, txn_trunc => txn_trunc,
                  txn_empty => txn_empty,
                  txn_report_stb => txn_report_stb, state_id => cs_state);

    -- The mode logic is Chapter 14.6; here the phase selects the edges.
    cap_stb    <= txn_active and edge_b_stb when cpha_now = '1'
                  else txn_active and edge_a_stb;
    launch_stb <= txn_active and edge_a_stb when cpha_now = '1'
                  else txn_active and edge_b_stb;
    preload_stb <= txn_start_stb and (not cpha_now);

    tx_data <= tx_words(tx_idx);

    dut : entity work.spi_slave_tx
        generic map (MAX_W => MAX_W, LEN_W => LEN_W, LEAD_MIN => LEAD_MIN,
                     HALF_MIN => HALF_MIN, CNT_W => CNT_W)
        port map (clk => clk, rst_n => rst_n,
                  txn_active => txn_active, txn_start_stb => txn_start_stb,
                  preload_stb => preload_stb, launch_stb => launch_stb,
                  cap_stb => cap_stb,
                  len => len, lsb_first => lsb_first, tx_data => tx_data,
                  miso => miso,
                  word_taken_stb => word_taken_stb, bits_driven => bits_driven,
                  lead_seen => lead_seen, lead_short => lead_short,
                  half_seen => half_seen, half_short => half_short,
                  clr_flags => clr_flags);

    -- The system side: advance to the next word each time one is consumed. Reset on
    -- the DEASSERT of the previous transaction rather than on the assert of this
    -- one -- at the assert cycle the slave is already reading `tx_data`, so an index
    -- still being cleared on that cycle hands it the wrong word.
    words : process (clk, rst_n)
    begin
        if rst_n = '0' then
            tx_idx <= 0;
        elsif rising_edge(clk) then
            if cs_deassert_stb = '1' then
                tx_idx <= 0;
            elsif word_taken_stb = '1' then
                tx_idx <= tx_idx + 1;
            end if;
        end if;
    end process;

    -- The master's own sampling, at the pin, plus the tail monitor. The sample is
    -- requested by the stimulus and performed here, so `got_bits` and `got_n` have a
    -- single driver.
    sampler : process (clk)
    begin
        if rising_edge(clk) then
            miso_q_tb <= miso;
            if watch_tail = '1' and miso /= miso_q_tb then
                miso_moves_in_tail <= miso_moves_in_tail + 1;
            end if;
            if clr_got = '1' then
                got_n <= 0;
            elsif samp_stb = '1' then
                if got_n < 64 then
                    got_bits(got_n) <= samp_val;
                end if;
                got_n <= got_n + 1;
            end if;
        end if;
    end process;

    stim : process
        variable errs : natural := 0;
        variable seed : unsigned(31 downto 0) := x"7A1C0055";
        variable first_ok, all_ok : boolean;
        variable flag : boolean;
        variable disagreements, boundary_ok, boundary_bad : integer;
        variable hdis, hb_ok, hb_bad : integer;
        variable bad : natural;
        variable mask : unsigned(MAX_W - 1 downto 0);

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

        -- Arming and disarming are separate so that the sample does not consume
        -- a cycle of the half-period. A `sample` procedure containing its own wait
        -- inserts that cycle BEFORE the edge it is sampling at, which stretches
        -- one half-period by one system clock -- and the symptom is a measured
        -- half-period one larger than the one the test asked for, which looks like
        -- a design bug in the measurement.
        procedure arm_sample is
        begin
            samp_val <= miso;
            samp_stb <= '1';
        end procedure;

        procedure disarm_sample is
        begin
            samp_stb <= '0';
        end procedure;

        -- One transaction. `lead` is the master's t_CSS in system clocks. MISO is
        -- sampled at each capture edge, at the pin, as a real master does.
        procedure drive_txn(lead : natural; half : natural; pol : std_logic;
                            pha : std_logic; nbits : natural; nwords : natural) is
        begin
            cpol       <= pol;
            cpha_now   <= pha;
            len        <= to_unsigned(nbits, LEN_W);
            sclk_pin   <= pol;
            cs_n_pin   <= '1';
            watch_tail <= '0';
            adv(8);
            clr_got <= '1';
            wait until falling_edge(clk);
            clr_got <= '0';

            cs_n_pin <= '0';
            if lead > 0 then adv(lead); end if;

            for w in 0 to nwords - 1 loop
                for i in 0 to nbits - 1 loop
                    if pha = '0' then
                        -- Capture on the leading edge: MISO is sampled at the same
                        -- instant the edge is driven, as a real master does.
                        arm_sample;
                        sclk_pin <= not pol;
                        adv(1);
                        disarm_sample;
                        if half > 1 then adv(half - 1); end if;
                        sclk_pin <= pol;
                        adv(half);
                    else
                        sclk_pin <= not pol;      -- leading: the slave launches
                        adv(half);
                        arm_sample;               -- capture on the trailing edge
                        sclk_pin <= pol;
                        adv(1);
                        disarm_sample;
                        if half > 1 then adv(half - 1); end if;
                    end if;
                end loop;
            end loop;

            -- The tail: chip select is still asserted and the data is done.
            watch_tail <= '1';
            adv(half * 3);
            watch_tail <= '0';
            cs_n_pin <= '1';
            adv(8);
            sclk_pin <= pol;
            adv(8);
            n_txns <= n_txns + 1;
        end procedure;

        -- Was the word the master sampled the word the slave was given?
        --
        -- IMPURE because it reads signals. VHDL's default is a pure function,
        -- which may not reference a signal at all -- and the error names the
        -- signal rather than the rule, so it reads like a scoping problem.
        impure function word_matches(w : natural; nbits : natural;
                                     lsb : std_logic) return boolean is
            variable want : std_logic;
        begin
            for i in 0 to nbits - 1 loop
                if lsb = '1' then
                    want := tx_words(w)(i);
                else
                    want := tx_words(w)(nbits - 1 - i);
                end if;
                if got_bits(w * nbits + i) /= want then
                    return false;
                end if;
            end loop;
            return true;
        end function;

        procedure next_word(idx : natural; nbits : natural) is
        begin
            seed := resize(seed * x"0019660D", 32) + x"3C6EF35F";
            mask := (others => '0');
            for k in 0 to MAX_W - 1 loop
                if k < nbits then mask(k) := '1'; end if;
            end loop;
            tx_words(idx) <= std_logic_vector(shift_right(seed, 9) and mask);
        end procedure;

        variable pha_v, lsb_v : std_logic;
    begin
        adv(3);
        rst_n <= '1';
        adv(2);

        -- 1. ONE BYTE, MODE 0, a generous lead.
        tx_words(0) <= x"000000A5";
        adv(1);
        drive_txn(8, 4, '0', '0', 8, 1);
        if not word_matches(0, 8, '0') then
            report "  FAIL: with a generous lead the master did not sample a5";
            errs := errs + 1;
        end if;
        if lead_short = '1' then
            report "  FAIL: a lead of 8 was reported as short";
            errs := errs + 1;
        end if;
        report "  one byte, mode 0, lead 8: the master sampled the byte the slave was given, and the lead was not reported short";

        -- 2. THE LEAD SWEEP. At every lead, either the pin carried the right first
        --    bit or the slave reported the problem -- by a short lead where the
        --    interval could be measured, or by a truncated transaction at leads so
        --    short that the first edge arrived before the slave knew it was selected.
        disagreements := 0;
        boundary_ok   := 99;
        boundary_bad  := -1;
        for lead in 0 to 8 loop
            clr_flags <= '1'; adv(1); clr_flags <= '0';
            tx_words(0) <= x"000000A5";
            adv(1);
            drive_txn(lead, 4, '0', '0', 8, 1);
            first_ok := (got_bits(0) = tx_words(0)(7));
            flag     := (lead_short = '1') or (txn_trunc = '1');
            if first_ok then
                report "    lead=" & integer'image(lead) &
                       ": first bit correct  lead_seen=" &
                       integer'image(to_integer(lead_seen));
            else
                report "    lead=" & integer'image(lead) &
                       ": first bit WRONG    lead_seen=" &
                       integer'image(to_integer(lead_seen));
            end if;
            if (not first_ok) and (not flag) then
                disagreements := disagreements + 1;
            end if;
            if first_ok and flag then
                disagreements := disagreements + 1;
            end if;
            if first_ok and lead < boundary_ok then boundary_ok := lead; end if;
            if (not first_ok) and lead > boundary_bad then
                boundary_bad := lead;
            end if;
        end loop;
        if disagreements /= 0 then
            report "  FAIL: at " & integer'image(disagreements) &
                   " of 9 leads the slave's report did not match the pin";
            errs := errs + 1;
        end if;
        if boundary_ok /= boundary_bad + 1 then
            report "  FAIL: the lead boundary is not sharp";
            errs := errs + 1;
        end if;
        if boundary_ok /= SYNC_N + 2 then
            report "  FAIL: the first bit became correct at a lead of " &
                   integer'image(boundary_ok) & ", expected " &
                   integer'image(SYNC_N + 2);
            errs := errs + 1;
        end if;
        report "  the lead sweep: the first bit is wrong for every lead below " &
               integer'image(boundary_ok) & " and correct from " &
               integer'image(boundary_ok) &
               " upward, the boundary is sharp, and at all nine leads the slave reported exactly when the pin was wrong";

        -- 3. THE HALF-PERIOD SWEEP. This boundary is ONE LOWER than the lead's: the
        --    lead waits for Chapter 14.2's registered transaction boundary and a
        --    launch does not, so one slave has two different requirements.
        hdis   := 0;
        hb_ok  := 99;
        hb_bad := -1;
        for h in 1 to 7 loop
            clr_flags <= '1'; adv(1); clr_flags <= '0';
            tx_words(0) <= x"000000A5";
            tx_words(1) <= x"0000003C";
            adv(1);
            drive_txn(8, h, '0', '0', 8, 2);
            all_ok := word_matches(0, 8, '0') and word_matches(1, 8, '0');
            if all_ok then
                report "    half=" & integer'image(h) &
                       ": data correct  half_seen=" &
                       integer'image(to_integer(half_seen));
            else
                report "    half=" & integer'image(h) &
                       ": data WRONG    half_seen=" &
                       integer'image(to_integer(half_seen));
            end if;
            if all_ok = (half_short = '1') then hdis := hdis + 1; end if;
            if all_ok and h < hb_ok then hb_ok := h; end if;
            if (not all_ok) and h > hb_bad then hb_bad := h; end if;
        end loop;
        if hdis /= 0 then
            report "  FAIL: the half-period flag disagreed with the pin at " &
                   integer'image(hdis) & " of 7 ratios";
            errs := errs + 1;
        end if;
        if hb_ok /= hb_bad + 1 then
            report "  FAIL: the half-period boundary is not sharp";
            errs := errs + 1;
        end if;
        if hb_ok /= SYNC_N + 1 then
            report "  FAIL: the data became correct at a half-period of " &
                   integer'image(hb_ok) & ", expected " &
                   integer'image(SYNC_N + 1);
            errs := errs + 1;
        end if;
        report "  the half-period sweep: the data is wrong for every half below " &
               integer'image(hb_ok) & " and correct from " & integer'image(hb_ok) &
               " upward, the boundary is sharp, and the flag agrees with the pin at all seven ratios";

        -- 4. MULTI-WORD TRANSACTIONS.
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        for k in 0 to 5 loop
            next_word(k, 8);
        end loop;
        adv(1);
        drive_txn(8, 4, '0', '0', 8, 6);
        bad := 0;
        for k in 0 to 5 loop
            if not word_matches(k, 8, '0') then bad := bad + 1; end if;
        end loop;
        if bad /= 0 then
            report "  FAIL: " & integer'image(bad) &
                   " of 6 words in a multi-word transaction were wrong";
            errs := errs + 1;
        end if;
        report "  six words in one transaction: each taken at its own boundary and each sampled correctly";

        -- 5. THE SWEEP, all at or above the required half-period.
        for o in 0 to 1 loop
            if o = 1 then lsb_v := '1'; else lsb_v := '0'; end if;
            for p in 0 to 1 loop
                if p = 1 then pha_v := '1'; else pha_v := '0'; end if;
                for hh in 0 to 2 loop
                    lsb_first <= lsb_v;
                    for k in 0 to 2 loop next_word(k, 8); end loop;
                    adv(1);
                    drive_txn(8, 3 + hh * 2, '0', pha_v, 8, 3);
                    for k in 0 to 2 loop
                        if not word_matches(k, 8, lsb_v) then
                            report "  FAIL: order=" & integer'image(o) &
                                   " phase=" & integer'image(p) & " word " &
                                   integer'image(k) & " wrong";
                            errs := errs + 1;
                        end if;
                    end loop;
                end loop;
            end loop;
        end loop;
        lsb_first <= '0';
        for nb in 1 to 13 loop
            if (nb mod 4) = 1 then
                for k in 0 to 2 loop next_word(k, nb); end loop;
                adv(1);
                drive_txn(8, 4, '0', '0', nb, 3);
                for k in 0 to 2 loop
                    if not word_matches(k, nb, '0') then
                        report "  FAIL: width " & integer'image(nb) & " word " &
                               integer'image(k) & " wrong";
                        errs := errs + 1;
                    end if;
                end loop;
            end if;
        end loop;
        report "  both phases, both bit orders, three ratios at or above the required half-period, and widths of 1, 5, 9 and 13 bits all sampled correctly";

        -- 6. WHAT THE SLAVE CANNOT SUPPRESS.
        if miso_moves_in_tail > n_txns then
            report "  FAIL: MISO moved " & integer'image(miso_moves_in_tail) &
                   " times in the tails of " & integer'image(n_txns) &
                   " transactions, at most one each is allowed";
            errs := errs + 1;
        end if;
        report "  MISO moved " & integer'image(miso_moves_in_tail) &
               " times in the tails of " & integer'image(n_txns) &
               " transactions -- at most the one launch a slave cannot refuse, and nothing after it";

        errors <= errs;
        if errs = 0 then
            report "PASS: the slave's first bit is driven from the transaction start rather than from a command it does not receive, so its earliest possible appearance is SYNC_N plus two system clocks after chip select falls -- two rather than one because Chapter 14.2 republishes the boundary as a registered strobe so that every block agrees about it -- and the lead sweep shows exactly that: the master samples the wrong first bit at every lead below " & integer'image(SYNC_N + 2) & " and the right one from " & integer'image(SYNC_N + 2) & " upward, with a sharp boundary, and at all nine leads the slave reported the problem exactly when the pin was wrong -- by a short lead where the interval could be measured and by a truncated transaction where the first edge arrived before the slave knew it was selected -- a multi-word transaction takes its next word at each boundary without the system knowing where those boundaries are, both phases, both bit orders, three frequency ratios and widths from 1 to 13 bits all deliver the word the slave was given, a second sweep establishes that the master must also allow at least SYNC_N plus ONE system clocks between its launch edge and its capture edge -- one fewer than the lead, because the lead waits for the registered boundary and a launch does not, so one slave has two different requirements -- and MISO moves at most once more per transaction, the one launch a slave cannot refuse";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

7. Why a Verification Engineer Cares

Calibrate a threshold against the thing it is a proxy for. This is the chapter's main verification idea. lead_short is an internal flag standing in for a pin-level fact the slave cannot see. A bench that checks the flag against its own threshold is checking the design against itself. A bench that checks it against whether the master actually got the right bit is checking the flag against what it means — and that is what tests 2 and 3 do, by modelling the master at the pin and comparing two independently derived booleans.

Test 6 asserts that a transition happens, which is unusual and necessary. It locks down a behaviour that looks like a bug, so that someone who later "fixes" it is told immediately rather than discovering it through a customer's two-byte read. Any design decision whose justification is "this looks wrong but must not change" needs a test, or it will be changed.

The len == 1 case is a real case and it is where the two drive paths collide. At one bit per word, the CPHA=0 preload has already finished the word before any launch edge arrives. A bench that sweeps widths from 4 upward never reaches it.

word_taken_stb is the handshake that must fire exactly once per word. Not once per transaction, not once per launch. Count it against words and check equality; a design that fires it twice for one word silently consumes two entries from 14.9's buffer, and the symptom is data that skips every other word.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Properties for the transmit path.

property p_one_drive_source;
    // MISO changes only on a launch strobe or at the transaction start. Any
    // other cycle means a second path to the register exists -- which is the
    // structural error section 5 is about.
    @(posedge clk) disable iff (!rst_n)
        (!launch_stb && !txn_start_stb) |=> $stable(miso);
endproperty

property p_drives_zeroed_at_start;
    // The residue from the previous transaction is cleared by the transaction
    // start, not by the word boundary. Section 5's callout, as a property.
    @(posedge clk) disable iff (!rst_n)
        txn_start_stb |=> (drives == 0);
endproperty

property p_word_taken_once_per_word;
    // Exactly one take per word. Two takes consume two buffer entries for one
    // word, and the symptom is transmitted data that skips alternate words.
    @(posedge clk) disable iff (!rst_n)
        word_taken_stb |=> !word_taken_stb until_with (launch_stb[->1]);
endproperty

property p_lead_flag_sticky;
    // A short lead stays reported. A flag that clears on the next good
    // transaction reads clear on every system where the problem is
    // intermittent, which is every system where it matters.
    @(posedge clk) disable iff (!rst_n)
        lead_short && !clr_flags |=> lead_short;
endproperty

property p_measurements_only_grow_within_txn;
    // lead_seen is measured once per transaction and held. It must not be
    // overwritten by a later interval inside the same transaction, which is
    // what happens if the counter is not stopped by the first capture.
    @(posedge clk) disable iff (!rst_n)
        (txn_active && $past(cap_stb)) |=> $stable(lead_seen);
endproperty

property p_no_launch_outside_txn;
    // The slave drives nothing on its own initiative. Every change to MISO is
    // traceable to a strobe that came from the bus.
    @(posedge clk) disable iff (!rst_n)
        launch_stb |-> txn_active;
endproperty
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Coverage. The axis that earns its place here is the LEAD, binned around
// LEAD_MIN rather than spread evenly, because the whole chapter is about one
// threshold and a suite that never approaches it has tested nothing.

covergroup cg_tx @(posedge clk iff txn_start_stb);
    option.per_instance = 1;

    lead: coverpoint lead_seen {
        bins far_short = {[1:1]};
        bins short     = {2, 3};              // below SYNC_N + 2 at SYNC_N = 2
        bins exact     = {4};                 // exactly SYNC_N + 2
        bins margin_1  = {5};
        bins roomy     = {[6:12]};
        bins generous  = {[13:$]};
    }

    half: coverpoint half_seen {
        bins short    = {[1:2]};
        bins exact    = {3};                  // exactly SYNC_N + 1
        bins margin_1 = {4};
        bins roomy    = {[5:$]};
    }

    // The phase matters because CPHA=0 preloads and CPHA=1 does not, so the
    // first-bit path is exercised only in one of them.
    phase: coverpoint cpha { bins p0 = {0}; bins p1 = {1}; }

    // len == 1 is its own bin: it is the width at which the preload finishes
    // the word and the launch path has nothing left to do.
    width: coverpoint len {
        bins one    = {1};
        bins two    = {2};
        bins narrow = {[3:7]};
        bins byte_w = {8};
        bins wide   = {[9:$]};
    }

    x_lead_phase:  cross lead, phase;
    x_width_phase: cross width, phase;

endgroup

8. Why an FPGA or ASIC Engineer Cares

MISO comes from a register, not from combinational logic, and that is not negotiable. The value is sampled by another device across a board. A combinational path from the shift register or from tx_data to the pin makes the output-valid time depend on this design's internal delays, and the master's setup requirement has no way to absorb that. One register, one path, and the pin's timing is the flop's clock-to-out plus the pad.

The aligning load is the widest combinational path in the block — a MAX_W-wide shift by a run-time amount, which is a barrel shifter. It happens on one cycle (the transaction start) with a full system clock available, so it is not critical, but it is real logic: at MAX_W = 32 it is a 5-stage 32-bit shifter. A design that needs to shrink it can align at the bottom and pick the bit with a mux instead, trading the barrel shifter for a MAX_W-to-1 mux on every cycle. Neither is obviously better; the point is that it is a choice.

The two interval counters are CNT_W = 8 and that is a deliberate ceiling. Eight bits measures up to 255 system clocks, which at SYNC_N = 2 is a hundred times the requirement. A lead longer than that is not interesting to report precisely — the counter saturates and the flag stays clear, which is the correct answer for "comfortably long".

word_taken_stb crosses into Chapter 14.9 and is the only strobe that does. It is a single-cycle pulse on the system clock in the same domain, so it needs nothing special — but it is worth noting in a review that this is the one place the transmit path writes to the system side, because a design where the transmit path advances the buffer from two conditions has a buffer that can skip.

9. Failure Signature — The First Byte Of Every Read Is Stale

The symptom:

"Reads return the byte we loaded last time. Every subsequent byte in the same transaction is correct. Slowing the SPI clock does not help; adding a delay between chip select and the clock does."

What is happening: the master's chip-select lead is shorter than SYNC_N + 2, so the master samples MISO before the slave has driven bit 0. The pin still carries whatever the slave last drove — the final bit of the previous transaction — so the first bit read is stale rather than random, which is why it looks like a buffering problem rather than a timing one.

Why "slowing the SPI clock does not help" is the diagnostic clue: the lead is a separate parameter from the clock period in any master worth using. Halving the clock frequency doubles the half-period and leaves the lead untouched. A fault that survives a clock-rate change but yields to a lead change is a lead fault, and the two are conflated constantly because on a naive master they happen to be the same knob.

How to find it in one read: lead_seen. It reports the measured interval in recovered cycles, and lead_short says whether it was below the minimum. If lead_seen is 3 and LEAD_MIN is 4, the answer is in the master's timing register, and §1 is the number to put there.

10. Common Misconceptions

"The slave can check whether it drove its first bit in time." It cannot. The capture edge is delayed by the same recovery latency as the assert, so from inside the slave its own drive always looks early. The lateness is a pin-level fact about a signal the slave drives, and the only observable proxy is the interval — which works because the latency cancels.

"The lead requirement is a protocol parameter." It is a property of this slave's front end. A deeper synchroniser needs a longer lead; a slave clocked on SCLK needs almost none. No datasheet will give you the number, which is exactly why the master's lead has to be programmable rather than fixed at something "safe".

"The slave should suppress its final launch, as the master does." The slave cannot know which launch is final. A trailing edge after the last capture is indistinguishable from the first edge of another word, and guessing means failing the second byte of every two-byte transaction.

"MISO changing after the last capture is a bug that could be fixed with a lookahead." There is nothing to look ahead at. The only signal that says "no more words" is chip select rising, and by then the edge has happened. The transition is on a bus the slave still legitimately owns and nobody is sampling it.

"A preload and a per-edge launch are naturally two code paths." They are naturally two code paths and that is the source of four separate bugs in this block's history. They are one expression selected by one piece of state — whether any bit has been driven yet.

"drives is the bit counter." It is not; Chapter 14.3 has the bit counter. drives answers one question — has anything been driven yet — and giving that question its own state-holder is what collapses two drive paths into one.

11. Reason It Through

Q. SYNC_N = 2 and a master provides a lead of exactly 4 recovered cycles. Does the slave's first bit arrive in time, and what does it report?

Yes, exactly in time, and it reports nothing. SYNC_N + 2 = 4, so MISO is registered on cycle 4 and the master's first leading edge is at cycle 4 — the bit is on the wire when the edge arrives. lead_seen reads 4 and lead_short stays clear. This is the boundary case, and the bench's calibration is what proves the threshold sits here rather than at 3 or 5: at a lead of 3 the master samples the wrong bit and the flag fires; at 4, neither happens.

Q. Why does len == 1 break a design with two drive paths, and why does the single-path design handle it without a special case?

At one bit per word, the CPHA=0 preload drives the only bit there is. The word is finished before any launch edge arrives, so the launch path has no valid source to shift from — in a two-path design it drives whatever the shift register happens to hold. In the single-path design, drives is zero at the preload and non-zero after it, and the expression reads the aligned word in the first case and the shift register in the second; a word of one bit simply never reaches the second case, which needs no code.

Q. A multi-word transaction of three 8-bit words. How many times does word_taken_stb fire, and how many launch edges occur?

Three takes, and 24 or 25 launch edges depending on phase. The takes happen once per word boundary, which is what advances 14.9's buffer. Under CPHA=0 there are 24 launch edges plus one extra after the final capture (§3), which does not produce a take because the transaction ends before the next word boundary. That asymmetry — one more launch than the word count implies — is precisely the residue that must not be allowed to leak into the next transaction, and it is why drives is zeroed at the transaction start.

Q. The lead sweep asserts that lead_short and "the master sampled the wrong first bit" agree exactly. Why is exact agreement the right assertion rather than implication in one direction?

Because each direction catches a different error. If the flag can fire when the master got the right bit, the threshold is too strict and the slave rejects masters that work — which is worse than useless, because the integrator will disable the flag. If the master can get the wrong bit without the flag firing, the threshold is too loose and the flag is worthless. Only exact agreement pins the threshold to one value, and it is what makes lead_short usable as evidence rather than as a hint.

Q. half_seen is measured from a launch strobe to the capture strobe that reads the launched bit. Why is that the right pair of events rather than the interval between consecutive edges?

Because the requirement is about a specific bit's stability, not about the clock's duty cycle. The bit launched on one edge must be settled at the pin when the master samples it on the next — so the relevant interval is launch-to-capture, which is the half-period that carries the data. Measuring consecutive edges gives the same number for a symmetric clock and a different one for an asymmetric one, and a master with a 30/70 duty cycle is legal. Measuring the pair that matters means the flag is right even then.

12. Understanding Check

13. Summary

Under CPHA=0 the master samples MISO on the first SCLK edge, so the slave must drive before any clock has moved — with no command, only a chip-select assert that arrives through a synchroniser.

Counting the cycles gives a requirement on the master: LEAD >= SYNC_N + 2. The +2 is the price of Chapter 14.2 owning the transaction boundary and republishing it as a registered strobe, which buys one authority for "the transaction started" at the cost of one cycle. The number is a property of this slave's front end, not of the protocol, and no datasheet will supply it — which is why the master's lead has to be programmable.

The slave cannot see that its first bit was late: the capture edge and the assert are delayed equally, so its own drive always looks early. What it can measure is the interval, because the latency cancels — and the bench calibrates the threshold against a pin-level model so the flag is exact rather than approximate.

The slave cannot suppress its final launch, because a trailing edge after the last capture is indistinguishable from the first edge of another word. MISO changes once more, nobody is sampling it, and the bus is one this slave still owns. A test asserts that it does change, so the behaviour cannot be "fixed" into a bug.

There is one drive path, selected by whether any bit has been driven yet. Four separate bugs in this block's history came from having two, and all four presented as something other than what they were — including one that looked like a receive-side shift. drives is zeroed at the transaction start, because the extra launch leaves a residue that would otherwise corrupt the next transaction.

For verification: calibrate an internal flag against the pin-level fact it stands in for, and require exact agreement in both directions; keep the test that asserts an apparently wrong transition happens; and include len == 1, where the preload finishes the word.

For implementation: MISO comes from a register and the pin's timing is a flop's clock-to-out; the aligning load is a barrel shifter on a non-critical path; and word_taken_stb is the single place the transmit path writes to the system side.

14. What Comes Next

The slave can drive the right value. It cannot yet be trusted to stop.

Chapter 14.5 — MISO Output Enable and Release is about the one failure in the module that damages hardware rather than data: two devices driving the same wire. MISO is shared across every slave on the bus, so the enable is not a convenience — it is the thing that keeps a board working. The chapter derives how quickly the slave must release after deselect, shows why that requirement is about the neighbour rather than about this slave, and explains why the enable must be tied to the ungated chip select even though everything else in the slave uses the gated one.

Continue learning