Skip to content
VLSI Mentor

SPI · Module 14

Transfer Width, Partial Frames, and Aborts

Five different faults produce identical pins, so the slave needs one policy for all of them: report and let software decide. Why complete words and partials must travel on separate channels, why the receive path is cleared at the transaction start rather than its end, and a framing block verified in three HDLs across 34 words and 9 partials.

A master knows whether it meant to stop. A slave sees a transaction that ended after three bits of an eight-bit word, and there is nothing in SPI that says which of these happened:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   * a master that intended three bits and is configured for a different width
   * a master that intended eight and aborted            (Chapter 13.10)
   * a master whose driver was killed mid-transfer
   * a CPHA mismatch                                     (Chapter 14.6)
   * a lost SCLK edge                                    (Chapter 14.1)

All five produce the same pins. So the slave must have one policy for all of them — and the policy cannot be "work out which", because the information that distinguishes them does not exist on the bus.

Three bits of an eight-bit word arrived and then chip select went high. What should the slave do with them?

1. Three Possible Policies

Discard silently. The partial bits are dropped and nothing is said.

This is the default behaviour of any design that only publishes a word at the width boundary — so it is what you get by not deciding. And it has a specific cost: a CPHA mismatch then looks like a device that never responds, which is a long way from the truth and sends the integrator to the wrong subsystem entirely.

Present it through the normal path. The three bits are handed over as if they were a word.

Now every consumer must ask "how many bits was that?", and the ones that do not ask read right-aligned rubbish. Worse: a three-bit fragment presented as a word is indistinguishable from a legitimate three-bit word, so a system that uses several widths cannot tell a configuration change from a truncation.

Present it separately, with its count, and never through the normal path. A consumer that ignores the partial channel loses the partial bits — which is correct, because they are not a word. A consumer that cares reads the count and the bits. And nothing reading the normal channel can be confused.

This block implements the third, and the discipline that makes it work is one line of specification:

word_valid_stb fires only for a word of exactly len bits, and a partial never reaches it.

2. What Must Survive A Partial

The words that completed before it. That sounds obvious, and it is the property most easily broken — because the natural place to clear the receive path is "at the end of the transaction", which is exactly when the partial is discovered.

Clearing there discards the partial and the last complete word's hold register, so a four-and-a-bit-word transaction delivers three words instead of four. The symptom is a system that loses the last byte of every aborted transfer, which reads as an off-by-one in the buffer rather than as a clear in the wrong place.

The clear belongs at the transaction start, where Chapter 14.2's rule already puts it.

3. What Must Survive After A Partial

The next transaction. An aborted transaction leaves counters part-way through a word, and every one of them has to be re-zeroed by the next assert rather than by the abort — because an abort is not an event the slave can be sure it has seen the end of.

This is the receive-side twin of the transmit-side re-zero in Chapter 14.4, and the failure mode has the same shape:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   the first word of the transaction AFTER the abort is wrong
   and every later word is right

Which is the worst attribution distance there is: the fault appears one transaction after its cause, in a transaction that was itself completely legal.

4. The State Machine

Framing state machine: IDLE waits for a transaction start, COLLECT publishes complete words, and PARTIAL publishes a fragment with its bit count before returning to IDLEIDLECOLLECTPARTIALtxn start: clear alltxn start: clear allreport: bits heldreport: bits heldreport: none heldreport: none heldpublishedpublishedwhole word: publishwhole word: publish
Figure 1 — the framing states. COLLECT accumulates complete words; PARTIAL exists for the one cycle needed to publish a fragment with its bit count. The transition into PARTIAL is taken on the report strobe rather than the end strobe, for Chapter 14.2's reason: the verdict that says whether a partial exists is not valid until then.
Fourteen system-clock cycles across six rows. A capture strobe fires on six cycles. A bit-index row counts 0 to 3, returns to 0 at the word boundary, then counts to 2. A word-valid strobe fires once, the cycle after the fourth capture. A partial-valid strobe fires at the report strobe near the end, with a partial bit count of 2.a whole word: normal channela whole word: normalchannela fragment: partial channela fragment: partial channelclkcap_stbbit_idxword_valid_stbtxn_report_stbpartial_bitst0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 2 — one complete word and a two-bit fragment, at a frame width of 4. The two channels never fire on the same cycle: the word is published the cycle after its fourth capture, and the fragment only at the report strobe, when the verdict that says a fragment exists becomes valid. A design that published the fragment at the END strobe one cycle earlier would be reading the previous transaction's verdict.

5. The Illegal Width

A frame width of zero makes the word boundary unreachable — the comparison bit_idx == len - 1 never matches, so no word is ever published and the transaction produces one enormous partial. A width above MAX_W truncates silently, delivering words whose top bits were never on the wire.

Both are reported through len_err, and the comparison is against an integer rather than against a same-width constant. That detail is Chapter 13.7's bug repeating: len > MAX_W[LEN_W-1:0] truncates MAX_W to LEN_W bits, which for MAX_W = 32 and LEN_W = 6 gives zero — and then every width is illegal. Comparing as integers avoids it, and the bench checks a legal width of exactly MAX_W specifically because that is the boundary the truncation bug moves.

6. Building the Framing Block — Three HDLs

The circuit

Three states, a word counter, a held-bits register, and a partial path that reads Chapter 14.3's published in-progress shift register rather than keeping its own. There is no shift logic in this block at all, which is the payoff from that publication.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_frame.sv — three states, one hold register, and a partial path with no shift logic in it
// spi_slave_frame.sv
//
// Chapter 14.7 -- short frames, and the fact that a slave cannot tell an abort from
// one.
//
// THE OBSERVATION THAT DECIDES THE DESIGN.
//
// A master knows whether it meant to stop. A slave sees a transaction that ended
// after three bits of an eight-bit word, and there is nothing in SPI that says
// whether that was:
//
//   * a master that intended three bits and is configured for a different width;
//   * a master that intended eight and aborted (Chapter 13.10);
//   * a master whose driver was killed;
//   * a CPHA mismatch, which ends every transaction one bit short (Chapter 14.6);
//   * or a lost edge, which ends it one bit short too (Chapter 14.1).
//
// All five produce the same pins. So the slave must have ONE policy for all of them,
// and the policy cannot be "work out which" -- it has to be "report what happened
// and let software decide", because software has context the slave does not.
//
// THREE POSSIBLE POLICIES, AND WHY ONLY ONE IS ACCEPTABLE.
//
//   DISCARD SILENTLY. The partial bits are dropped and nothing is said. This is the
//   default behaviour of any design that only publishes a word at the width boundary,
//   so it is what you get by not deciding -- and it makes a CPHA mismatch look like a
//   device that never responds, which is a long way from the truth.
//
//   PRESENT IT THROUGH THE NORMAL PATH. The three bits are handed over as if they were
//   a word. Now every consumer must ask "how many bits was that?" and the ones that
//   do not ask read a value that is right-aligned rubbish. Worse, it is
//   indistinguishable from a legitimate narrow word, so a system that uses several
//   widths cannot tell a configuration change from a truncation.
//
//   PRESENT IT SEPARATELY, WITH ITS COUNT, AND NEVER THROUGH THE NORMAL PATH. A
//   consumer that ignores the partial channel loses the partial bits, which is
//   correct: they are not a word. A consumer that cares reads the count and the bits.
//   And nothing that reads the normal channel can be confused.
//
// This block implements the third, and the discipline that makes it work is one line
// of specification: `rx_valid_stb` fires ONLY for a word of exactly `len` bits, and a
// partial NEVER reaches it.
//
// WHAT MUST SURVIVE A PARTIAL.
//
// The words that completed before it. That sounds obvious and it is the property most
// easily broken, because the natural place to clear the receive path is "at the end of
// the transaction" -- which is exactly when the partial is discovered. Clearing there
// discards the partial AND the last complete word's hold register, so a four-and-a-bit
// word transaction delivers three words instead of four. The clear belongs at the
// transaction START, where Chapter 14.2's rule already puts it.
//
// AND WHAT MUST SURVIVE AFTER IT.
//
// The next transaction. An aborted transaction leaves counters part-way through a
// word, and every one of them has to be re-zeroed by the next assert rather than by
// the abort -- because an abort is not an event the slave can be sure it has seen the
// end of. This is the receive-side twin of the transmit-side re-zero in Chapter 14.4,
// and the failure mode is the same shape: the first word of the transaction AFTER the
// abort is wrong and every later word is right.

module spi_slave_frame #(
    parameter int MAX_W = 32,
    parameter int LEN_W = 6,
    parameter int CNT_W = 12
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- configuration ----------------------------------------------------
    input  wire [LEN_W-1:0]  len,

    // --- from 14.2 ---------------------------------------------------------
    input  wire              txn_start_stb,
    input  wire              txn_report_stb,
    input  wire              txn_trunc,
    input  wire              txn_empty,

    // --- from 14.3 ---------------------------------------------------------
    input  wire [MAX_W-1:0]  rx_data,
    input  wire              rx_valid_stb,
    input  wire [LEN_W-1:0]  bit_idx,
    input  wire [MAX_W-1:0]  rx_partial_sr,   // the incomplete word, as it stands

    // --- the clean stream: complete words only -----------------------------
    output wire [MAX_W-1:0]  word_data,
    output wire              word_valid_stb,
    output wire [CNT_W-1:0]  words_complete,

    // --- the partial channel, which is deliberately separate ---------------
    output reg  [MAX_W-1:0]  partial_data,
    output reg  [LEN_W-1:0]  partial_bits,
    output reg               partial_valid_stb,

    // --- status -------------------------------------------------------------
    output wire              len_err,
    output reg               aborted,      // sticky: a transaction ended mid-word
    output wire [1:0]        state_id,
    input  wire              clr_flags
);

    localparam [1:0] S_IDLE    = 2'd0,
                     S_COLLECT = 2'd1,
                     S_PARTIAL = 2'd2;

    reg [1:0]       state;
    reg [CNT_W-1:0] words;
    reg [LEN_W-1:0] held_bits;
    reg [MAX_W-1:0] held_sr;

    // A width of zero would make the word boundary unreachable; one above the
    // datapath would truncate silently. Compared as an integer, not against a
    // truncated MAX_W (Chapter 13.8's lesson, and Chapter 13.7's).
    assign len_err = (len == {LEN_W{1'b0}}) || (len > MAX_W);

    // The clean stream is a pass-through, gated by nothing: Chapter 14.3 already
    // raises its valid only at the width boundary. Passing it through unchanged is
    // the point -- a partial cannot reach this channel because it never produced a
    // valid in the first place, and the guarantee is therefore structural rather
    // than a condition someone could forget.
    assign word_data      = rx_data;
    assign word_valid_stb = rx_valid_stb;
    assign words_complete = words;
    assign state_id       = state;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state             <= S_IDLE;
            words             <= {CNT_W{1'b0}};
            held_bits         <= {LEN_W{1'b0}};
            held_sr           <= {MAX_W{1'b0}};
            partial_data      <= {MAX_W{1'b0}};
            partial_bits      <= {LEN_W{1'b0}};
            partial_valid_stb <= 1'b0;
            aborted           <= 1'b0;
        end else begin
            partial_valid_stb <= 1'b0;

            if (clr_flags)
                aborted <= 1'b0;

            // The incomplete word is tracked CONTINUOUSLY rather than sampled at the
            // end. At the moment the transaction ends, `bit_idx` has already been
            // cleared by nothing -- but the shift register is about to be cleared by
            // the next assert, and sampling on the report strobe is one cycle too
            // late to be sure. Holding a copy every cycle costs one register and
            // removes the question.
            held_bits <= bit_idx;
            held_sr   <= rx_partial_sr;

            case (state)
                S_IDLE: begin
                    if (txn_start_stb) begin
                        // Every counter re-zeroed HERE, by the assert -- not by the
                        // abort. An abort is not an event the slave can be sure it
                        // has seen the end of, and clearing on it leaves the first
                        // word of the NEXT transaction wrong.
                        words <= {CNT_W{1'b0}};
                        state <= S_COLLECT;
                    end
                end

                S_COLLECT: begin
                    if (rx_valid_stb)
                        words <= words + 1'b1;

                    if (txn_report_stb) begin
                        if (txn_trunc && !txn_empty) begin
                            // A partial exists. It is published on its own channel
                            // and the clean stream is not disturbed.
                            state   <= S_PARTIAL;
                            aborted <= 1'b1;
                        end else begin
                            state <= S_IDLE;
                        end
                    end
                end

                S_PARTIAL: begin
                    partial_data      <= held_sr;
                    partial_bits      <= held_bits;
                    partial_valid_stb <= 1'b1;
                    state             <= S_IDLE;
                end

                default: state <= S_IDLE;
            endcase
        end
    end

`ifdef SPI_CHECKS
    // The one-line specification, stated where a reviewer looks: a partial never
    // reaches the clean stream.
    always_ff @(posedge clk) if (rst_n) begin
        if (word_valid_stb && partial_valid_stb)
            $fatal(1, "a complete word and a partial on the same cycle");
        if (partial_valid_stb && partial_bits == {LEN_W{1'b0}})
            $fatal(1, "a partial of zero bits was published");
        if (partial_valid_stb && partial_bits >= len)
            $fatal(1, "a partial as wide as a whole word was published");
    end
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_frame.v — the same design in Verilog-2001
// spi_slave_frame.v
//
// Chapter 14.7 -- short frames, and the fact that a slave cannot tell an abort from
// one.
//
// THE OBSERVATION THAT DECIDES THE DESIGN.
//
// A master knows whether it meant to stop. A slave sees a transaction that ended
// after three bits of an eight-bit word, and there is nothing in SPI that says
// whether that was:
//
//   * a master that intended three bits and is configured for a different width;
//   * a master that intended eight and aborted (Chapter 13.10);
//   * a master whose driver was killed;
//   * a CPHA mismatch, which ends every transaction one bit short (Chapter 14.6);
//   * or a lost edge, which ends it one bit short too (Chapter 14.1).
//
// All five produce the same pins. So the slave must have ONE policy for all of them,
// and the policy cannot be "work out which" -- it has to be "report what happened
// and let software decide", because software has context the slave does not.
//
// THREE POSSIBLE POLICIES, AND WHY ONLY ONE IS ACCEPTABLE.
//
//   DISCARD SILENTLY. The partial bits are dropped and nothing is said. This is the
//   default behaviour of any design that only publishes a word at the width boundary,
//   so it is what you get by not deciding -- and it makes a CPHA mismatch look like a
//   device that never responds, which is a long way from the truth.
//
//   PRESENT IT THROUGH THE NORMAL PATH. The three bits are handed over as if they were
//   a word. Now every consumer must ask "how many bits was that?" and the ones that
//   do not ask read a value that is right-aligned rubbish. Worse, it is
//   indistinguishable from a legitimate narrow word, so a system that uses several
//   widths cannot tell a configuration change from a truncation.
//
//   PRESENT IT SEPARATELY, WITH ITS COUNT, AND NEVER THROUGH THE NORMAL PATH. A
//   consumer that ignores the partial channel loses the partial bits, which is
//   correct: they are not a word. A consumer that cares reads the count and the bits.
//   And nothing that reads the normal channel can be confused.
//
// This block implements the third, and the discipline that makes it work is one line
// of specification: `rx_valid_stb` fires ONLY for a word of exactly `len` bits, and a
// partial NEVER reaches it.
//
// WHAT MUST SURVIVE A PARTIAL.
//
// The words that completed before it. That sounds obvious and it is the property most
// easily broken, because the natural place to clear the receive path is "at the end of
// the transaction" -- which is exactly when the partial is discovered. Clearing there
// discards the partial AND the last complete word's hold register, so a four-and-a-bit
// word transaction delivers three words instead of four. The clear belongs at the
// transaction START, where Chapter 14.2's rule already puts it.
//
// AND WHAT MUST SURVIVE AFTER IT.
//
// The next transaction. An aborted transaction leaves counters part-way through a
// word, and every one of them has to be re-zeroed by the next assert rather than by
// the abort -- because an abort is not an event the slave can be sure it has seen the
// end of. This is the receive-side twin of the transmit-side re-zero in Chapter 14.4,
// and the failure mode is the same shape: the first word of the transaction AFTER the
// abort is wrong and every later word is right.

module spi_slave_frame #(
    parameter MAX_W = 32,
    parameter LEN_W = 6,
    parameter CNT_W = 12
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- configuration ----------------------------------------------------
    input  wire [LEN_W-1:0]  len,

    // --- from 14.2 ---------------------------------------------------------
    input  wire              txn_start_stb,
    input  wire              txn_report_stb,
    input  wire              txn_trunc,
    input  wire              txn_empty,

    // --- from 14.3 ---------------------------------------------------------
    input  wire [MAX_W-1:0]  rx_data,
    input  wire              rx_valid_stb,
    input  wire [LEN_W-1:0]  bit_idx,
    input  wire [MAX_W-1:0]  rx_partial_sr,   // the incomplete word, as it stands

    // --- the clean stream: complete words only -----------------------------
    output wire [MAX_W-1:0]  word_data,
    output wire              word_valid_stb,
    output wire [CNT_W-1:0]  words_complete,

    // --- the partial channel, which is deliberately separate ---------------
    output reg  [MAX_W-1:0]  partial_data,
    output reg  [LEN_W-1:0]  partial_bits,
    output reg               partial_valid_stb,

    // --- status -------------------------------------------------------------
    output wire              len_err,
    output reg               aborted,      // sticky: a transaction ended mid-word
    output wire [1:0]        state_id,
    input  wire              clr_flags
);

    localparam [1:0] S_IDLE    = 2'd0,
                     S_COLLECT = 2'd1,
                     S_PARTIAL = 2'd2;

    reg [1:0]       state;
    reg [CNT_W-1:0] words;
    reg [LEN_W-1:0] held_bits;
    reg [MAX_W-1:0] held_sr;

    // A width of zero would make the word boundary unreachable; one above the
    // datapath would truncate silently. Compared as an integer, not against a
    // truncated MAX_W (Chapter 13.8's lesson, and Chapter 13.7's).
    assign len_err = (len == {LEN_W{1'b0}}) || (len > MAX_W);

    // The clean stream is a pass-through, gated by nothing: Chapter 14.3 already
    // raises its valid only at the width boundary. Passing it through unchanged is
    // the point -- a partial cannot reach this channel because it never produced a
    // valid in the first place, and the guarantee is therefore structural rather
    // than a condition someone could forget.
    assign word_data      = rx_data;
    assign word_valid_stb = rx_valid_stb;
    assign words_complete = words;
    assign state_id       = state;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state             <= S_IDLE;
            words             <= {CNT_W{1'b0}};
            held_bits         <= {LEN_W{1'b0}};
            held_sr           <= {MAX_W{1'b0}};
            partial_data      <= {MAX_W{1'b0}};
            partial_bits      <= {LEN_W{1'b0}};
            partial_valid_stb <= 1'b0;
            aborted           <= 1'b0;
        end else begin
            partial_valid_stb <= 1'b0;

            if (clr_flags)
                aborted <= 1'b0;

            // The incomplete word is tracked CONTINUOUSLY rather than sampled at the
            // end. At the moment the transaction ends, `bit_idx` has already been
            // cleared by nothing -- but the shift register is about to be cleared by
            // the next assert, and sampling on the report strobe is one cycle too
            // late to be sure. Holding a copy every cycle costs one register and
            // removes the question.
            held_bits <= bit_idx;
            held_sr   <= rx_partial_sr;

            case (state)
                S_IDLE: begin
                    if (txn_start_stb) begin
                        // Every counter re-zeroed HERE, by the assert -- not by the
                        // abort. An abort is not an event the slave can be sure it
                        // has seen the end of, and clearing on it leaves the first
                        // word of the NEXT transaction wrong.
                        words <= {CNT_W{1'b0}};
                        state <= S_COLLECT;
                    end
                end

                S_COLLECT: begin
                    if (rx_valid_stb)
                        words <= words + 1'b1;

                    if (txn_report_stb) begin
                        if (txn_trunc && !txn_empty) begin
                            // A partial exists. It is published on its own channel
                            // and the clean stream is not disturbed.
                            state   <= S_PARTIAL;
                            aborted <= 1'b1;
                        end else begin
                            state <= S_IDLE;
                        end
                    end
                end

                S_PARTIAL: begin
                    partial_data      <= held_sr;
                    partial_bits      <= held_bits;
                    partial_valid_stb <= 1'b1;
                    state             <= S_IDLE;
                end

                default: state <= S_IDLE;
            endcase
        end
    end

`ifdef SPI_CHECKS
    // The one-line specification, stated where a reviewer looks: a partial never
    // reaches the clean stream.
    always @(posedge clk) if (rst_n) begin
        if (word_valid_stb && partial_valid_stb)
            $fatal(1, "a complete word and a partial on the same cycle");
        if (partial_valid_stb && partial_bits == {LEN_W{1'b0}})
            $fatal(1, "a partial of zero bits was published");
        if (partial_valid_stb && partial_bits >= len)
            $fatal(1, "a partial as wide as a whole word was published");
    end
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_frame.vhd — the same design in VHDL
-- spi_slave_frame.vhd
--
-- Chapter 14.7 -- short frames, and the fact that a slave cannot tell an abort from
-- one.
--
-- THE OBSERVATION THAT DECIDES THE DESIGN.
--
-- A master knows whether it meant to stop. A slave sees a transaction that ended after
-- three bits of an eight-bit word, and there is nothing in SPI that says whether that
-- was:
--
--   * a master that intended three bits and is configured for a different width;
--   * a master that intended eight and aborted (Chapter 13.10);
--   * a master whose driver was killed;
--   * a CPHA mismatch, which ends every transaction one bit short (Chapter 14.6);
--   * or a lost edge, which ends it one bit short too (Chapter 14.1).
--
-- All five produce the same pins. So the slave must have ONE policy for all of them,
-- and the policy cannot be "work out which" -- it has to be "report what happened and
-- let software decide", because software has context the slave does not.
--
-- THREE POSSIBLE POLICIES, AND WHY ONLY ONE IS ACCEPTABLE.
--
--   DISCARD SILENTLY. The partial bits are dropped and nothing is said. This is the
--   default behaviour of any design that only publishes a word at the width boundary,
--   so it is what you get by not deciding -- and it makes a CPHA mismatch look like a
--   device that never responds.
--
--   PRESENT IT THROUGH THE NORMAL PATH. The three bits are handed over as if they were
--   a word. Now every consumer must ask "how many bits was that?" and the ones that do
--   not read right-aligned rubbish. Worse, it is indistinguishable from a legitimate
--   narrow word, so a system that uses several widths cannot tell a configuration
--   change from a truncation.
--
--   PRESENT IT SEPARATELY, WITH ITS COUNT, AND NEVER THROUGH THE NORMAL PATH. A
--   consumer that ignores the partial channel loses the partial bits, which is
--   correct: they are not a word. A consumer that cares reads the count and the bits.
--   And nothing that reads the normal channel can be confused.
--
-- This block implements the third, and the discipline that makes it work is one line
-- of specification: the word valid fires ONLY for a word of exactly `len` bits, and a
-- partial NEVER reaches it.
--
-- WHAT MUST SURVIVE A PARTIAL. The words that completed before it -- and that is the
-- property most easily broken, because the natural place to clear the receive path is
-- "at the end of the transaction", which is exactly when the partial is discovered.
-- Clearing there discards the partial AND the last complete word's hold register, so a
-- four-and-a-bit word transaction delivers three words instead of four. The clear
-- belongs at the transaction START, where Chapter 14.2's rule already puts it.
--
-- AND WHAT MUST SURVIVE AFTER IT. The next transaction. An aborted transaction leaves
-- counters part-way through a word, and every one of them has to be re-zeroed by the
-- next assert rather than by the abort -- because an abort is not an event the slave
-- can be sure it has seen the end of. This is the receive-side twin of the
-- transmit-side re-zero in Chapter 14.4, and the failure mode is the same shape: the
-- first word of the transaction AFTER the abort is wrong and every later word is right.

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

entity spi_slave_frame is
    generic (
        MAX_W : positive := 32;
        LEN_W : positive := 6;
        CNT_W : positive := 12
    );
    port (
        clk               : in  std_logic;
        rst_n             : in  std_logic;

        -- configuration
        len               : in  unsigned(LEN_W - 1 downto 0);

        -- from 14.2
        txn_start_stb     : in  std_logic;
        txn_report_stb    : in  std_logic;
        txn_trunc         : in  std_logic;
        txn_empty         : in  std_logic;

        -- from 14.3
        rx_data           : in  std_logic_vector(MAX_W - 1 downto 0);
        rx_valid_stb      : in  std_logic;
        bit_idx           : in  unsigned(LEN_W - 1 downto 0);
        rx_partial_sr     : in  std_logic_vector(MAX_W - 1 downto 0);

        -- the clean stream: complete words only
        word_data         : out std_logic_vector(MAX_W - 1 downto 0);
        word_valid_stb    : out std_logic;
        words_complete    : out unsigned(CNT_W - 1 downto 0);

        -- the partial channel, which is deliberately separate
        partial_data      : out std_logic_vector(MAX_W - 1 downto 0);
        partial_bits      : out unsigned(LEN_W - 1 downto 0);
        partial_valid_stb : out std_logic;

        -- status
        len_err           : out std_logic;
        aborted           : out std_logic;
        state_id          : out unsigned(1 downto 0);
        clr_flags         : in  std_logic
    );
end entity;

architecture rtl of spi_slave_frame is

    constant S_IDLE    : unsigned(1 downto 0) := "00";
    constant S_COLLECT : unsigned(1 downto 0) := "01";
    constant S_PARTIAL : unsigned(1 downto 0) := "10";

    signal state     : unsigned(1 downto 0) := S_IDLE;
    signal words     : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal held_bits : unsigned(LEN_W - 1 downto 0) := (others => '0');
    signal held_sr   : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    signal part_d    : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    signal part_b    : unsigned(LEN_W - 1 downto 0) := (others => '0');
    signal part_v    : std_logic := '0';
    signal aborted_r : std_logic := '0';

begin

    -- A width of zero would make the word boundary unreachable; one above the datapath
    -- would truncate silently. Compared as an integer, not against a truncated MAX_W
    -- (Chapter 13.8's lesson, and Chapter 13.7's).
    len_err <= '1' when (len = 0) or (to_integer(len) > MAX_W) else '0';

    -- The clean stream is a pass-through, gated by nothing: Chapter 14.3 already raises
    -- its valid only at the width boundary. Passing it through unchanged is the point
    -- -- a partial cannot reach this channel because it never produced a valid in the
    -- first place, so the guarantee is structural rather than a condition someone could
    -- forget.
    word_data      <= rx_data;
    word_valid_stb <= rx_valid_stb;
    words_complete <= words;
    state_id       <= state;

    partial_data      <= part_d;
    partial_bits      <= part_b;
    partial_valid_stb <= part_v;
    aborted           <= aborted_r;

    fsm : process (clk, rst_n)
    begin
        if rst_n = '0' then
            state     <= S_IDLE;
            words     <= (others => '0');
            held_bits <= (others => '0');
            held_sr   <= (others => '0');
            part_d    <= (others => '0');
            part_b    <= (others => '0');
            part_v    <= '0';
            aborted_r <= '0';
        elsif rising_edge(clk) then
            part_v <= '0';

            if clr_flags = '1' then
                aborted_r <= '0';
            end if;

            -- The incomplete word is tracked CONTINUOUSLY rather than sampled at the
            -- end. Holding a copy every cycle costs one register and removes the
            -- question of whether the sample point is one cycle too late.
            held_bits <= bit_idx;
            held_sr   <= rx_partial_sr;

            case to_integer(state) is

                when 0 =>                         -- S_IDLE
                    if txn_start_stb = '1' then
                        -- Every counter re-zeroed HERE, by the assert -- not by the
                        -- abort. An abort is not an event the slave can be sure it
                        -- has seen the end of, and clearing on it leaves the first
                        -- word of the NEXT transaction wrong.
                        words <= (others => '0');
                        state <= S_COLLECT;
                    end if;

                when 1 =>                         -- S_COLLECT
                    if rx_valid_stb = '1' then
                        words <= words + 1;
                    end if;

                    if txn_report_stb = '1' then
                        if txn_trunc = '1' and txn_empty = '0' then
                            -- A partial exists. It is published on its own channel and
                            -- the clean stream is not disturbed.
                            state     <= S_PARTIAL;
                            aborted_r <= '1';
                        else
                            state <= S_IDLE;
                        end if;
                    end if;

                when 2 =>                         -- S_PARTIAL
                    part_d <= held_sr;
                    part_b <= held_bits;
                    part_v <= '1';
                    state  <= S_IDLE;

                when others =>
                    state <= S_IDLE;

            end case;
        end if;
    end process;

    -- The one-line specification, stated where a reviewer looks: a partial never
    -- reaches the clean stream.
    check : process (clk)
    begin
        if rising_edge(clk) and rst_n = '1' then
            assert not (rx_valid_stb = '1' and part_v = '1')
                report "a complete word and a partial on the same cycle"
                severity failure;
            assert not (part_v = '1' and part_b = 0)
                report "a partial of zero bits was published" severity failure;
            assert not (part_v = '1' and part_b >= len)
                report "a partial as wide as a whole word was published"
                severity failure;
        end if;
    end process;

end architecture;

The testbench

Seven tests. The third is the one that catches §3's bug, and it is the one a bench would not naturally contain.

  1. A whole transaction produces words and no partial. The baseline.
  2. Words plus a partial. The completed words must all arrive — this is §2's property, and the test counts them rather than checking the last one.
  3. Recovery. The transaction after a partial must be fully correct, including its first word. This is the test that finds a clear in the wrong place, and it only exists if someone has thought about §3.
  4. A partial of a narrow word. With len = 3, two bits spare is a partial and one bit is too — the boundary arithmetic has to work at widths where the partial is almost the whole word.
  5. An empty transaction is not a partial. There is nothing to report, and reporting a zero-bit partial would be a partial channel that fires on every select glitch.
  6. An illegal width is reported, at zero and above MAX_W, and a width of exactly MAX_W is legal.
  7. The continuous properties. word_valid_stb never fires for a partial; partial_valid_stb never fires with a zero bit count; the two never coincide.

The run covers 34 words and 9 partials across the widths and cases.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_frame_tb.sv — seven tests, 34 words and 9 partials, including the transaction after the interesting one
// spi_slave_frame_tb.sv
//
// The whole receive chain runs here -- the front end of 14.1, the transaction detector
// of 14.2, the mode logic of 14.6 and the capture path of 14.3 -- because the property
// under test is about what happens at the END of a transaction, and the end is a
// recovered event that only the real chain produces.
//
// Three properties get the attention, and the last is the one that catches the bug
// this chapter's header warns about:
//
//   A PARTIAL NEVER REACHES THE CLEAN STREAM. Checked continuously, not at the end:
//   the failure is one extra valid pulse, and a test that counts words at the end
//   would see the right total if a partial replaced a real word.
//
//   THE WORDS BEFORE A PARTIAL SURVIVE IT. A transaction of four whole words and three
//   spare bits must deliver four words, not three -- which is what clearing the
//   receive path on the abort rather than on the next assert produces.
//
//   THE TRANSACTION AFTER A PARTIAL IS FULLY CORRECT. This is the receive-side twin of
//   Chapter 14.4's transmit-side re-zero, and the signature is identical: the first
//   word after the abort is wrong and every later word is right.

`timescale 1ns/1ps

module spi_slave_frame_tb;

    localparam int MAX_W = 32;
    localparam int LEN_W = 6;
    localparam int CNT_W = 12;

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

    logic cpol      = 1'b0;
    logic cpha      = 1'b0;
    logic lsb_first = 1'b0;
    logic clr_flags = 1'b0;

    logic sclk_pin = 1'b0;
    logic cs_n_pin = 1'b1;
    logic mosi_pin = 1'b0;

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

    spi_slave_frontend #(.SYNC_N(2), .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;

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

    spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(CNT_W)) 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)
    );

    wire cap_stb, launch_stb, preload_stb;
    wire cpol_mismatch, phase_suspect;
    wire [3:0] trunc_run;

    spi_slave_mode #(.SUSPECT_N(3), .CNT_W(4)) u_mode (
        .clk(clk), .rst_n(rst_n), .cpol(cpol), .cpha(cpha),
        .sclk_q(sclk_q), .mosi_q(mosi_q), .cs_assert_stb(cs_assert_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .txn_active(txn_active), .txn_start_stb(txn_start_stb),
        .txn_report_stb(txn_report_stb), .txn_clean(txn_clean),
        .txn_trunc(txn_trunc),
        .cap_stb(cap_stb), .launch_stb(launch_stb), .preload_stb(preload_stb),
        .cpol_mismatch(cpol_mismatch), .phase_suspect(phase_suspect),
        .moved_run(), .trunc_run(trunc_run), .clr_flags(1'b0)
    );

    wire [MAX_W-1:0] rx_data, rx_partial_sr;
    wire             rx_valid_stb;
    wire [LEN_W-1:0] bit_idx;
    wire [CNT_W-1:0] words_in_txn;

    spi_slave_rx #(.MAX_W(MAX_W), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_rx (
        .clk(clk), .rst_n(rst_n),
        .txn_active(txn_active), .txn_start_stb(txn_start_stb),
        .cap_stb(cap_stb), .mosi_q(mosi_q),
        .len(len), .lsb_first(lsb_first),
        .rx_data(rx_data), .rx_valid_stb(rx_valid_stb),
        .bit_idx(bit_idx), .words_in_txn(words_in_txn),
        .rx_partial_sr(rx_partial_sr)
    );

    // --- 14.7, the block under test -----------------------------------------
    wire [MAX_W-1:0] word_data;
    wire             word_valid_stb;
    wire [CNT_W-1:0] words_complete;
    wire [MAX_W-1:0] partial_data;
    wire [LEN_W-1:0] partial_bits;
    wire             partial_valid_stb;
    wire             len_err, aborted;
    wire [1:0]       frame_state;

    spi_slave_frame #(.MAX_W(MAX_W), .LEN_W(LEN_W), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n), .len(len),
        .txn_start_stb(txn_start_stb), .txn_report_stb(txn_report_stb),
        .txn_trunc(txn_trunc), .txn_empty(txn_empty),
        .rx_data(rx_data), .rx_valid_stb(rx_valid_stb), .bit_idx(bit_idx),
        .rx_partial_sr(rx_partial_sr),
        .word_data(word_data), .word_valid_stb(word_valid_stb),
        .words_complete(words_complete),
        .partial_data(partial_data), .partial_bits(partial_bits),
        .partial_valid_stb(partial_valid_stb),
        .len_err(len_err), .aborted(aborted), .state_id(frame_state),
        .clr_flags(clr_flags)
    );

    // --- the expectation ----------------------------------------------------
    integer seed;
    logic [MAX_W-1:0] exp_q [0:63];
    integer exp_w, got_w;
    integer word_bad, n_words, n_partials, both_channels;
    logic [MAX_W-1:0] last_partial;
    integer           last_partial_bits;

    always_ff @(posedge clk) begin
        if (rst_n) begin
            if (txn_start_stb) got_w <= 0;
            if (word_valid_stb) begin
                n_words <= n_words + 1;
                if (got_w < exp_w && word_data !== exp_q[got_w])
                    word_bad <= word_bad + 1;
                got_w <= got_w + 1;
            end
            if (partial_valid_stb) begin
                n_partials        <= n_partials + 1;
                last_partial      <= partial_data;
                last_partial_bits <= partial_bits;
            end
            if (word_valid_stb && partial_valid_stb)
                both_channels <= both_channels + 1;
        end
    end

    integer errors = 0;

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

    // Drives `nbits_total` bits into one transaction, MSB-first per word, from a
    // known pattern. Driving BITS rather than words is what lets the test produce a
    // transaction that is not a whole number of words.
    task automatic drive_bits(input integer nbits_total, input integer half,
                              input [63:0] pattern);
        integer i;
        begin
            cpol = 1'b0; sclk_pin = 1'b0; cs_n_pin = 1'b1;
            adv(10);
            cs_n_pin = 1'b0;
            adv(4);
            for (i = 0; i < nbits_total; i = i + 1) begin
                mosi_pin = pattern[63 - i];
                adv(2);
                sclk_pin = 1'b1;          // leading: captured here (CPHA=0)
                adv(half);
                sclk_pin = 1'b0;
                adv(half > 2 ? half - 2 : 1);
            end
            adv(4);
            cs_n_pin = 1'b1;
            adv(10);
            sclk_pin = 1'b0;
            adv(8);
        end
    endtask

    // The expected words, taken from the same pattern the driver used.
    task automatic expect_from(input [63:0] pattern, input integer nwords,
                               input integer nbits);
        integer w, b;
        logic [MAX_W-1:0] v;
        begin
            exp_w = 0;
            for (w = 0; w < nwords; w = w + 1) begin
                v = {MAX_W{1'b0}};
                for (b = 0; b < nbits; b = b + 1)
                    v[nbits-1-b] = pattern[63 - (w*nbits + b)];
                exp_q[exp_w] = v;
                exp_w = exp_w + 1;
            end
        end
    endtask

    integer k, p, want_bits;
    logic [MAX_W-1:0] want_partial;
    logic [63:0] pat;

    initial begin
        exp_w = 0; got_w = 0; word_bad = 0; n_words = 0; n_partials = 0;
        both_channels = 0; last_partial = 0; last_partial_bits = 0;
        seed = 32'h2B7F_0E91;

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

        // 1. A WHOLE TRANSACTION produces words and no partial.
        pat = 64'hA5_3C_5A_C3_00_00_00_00;
        expect_from(pat, 4, 8);
        drive_bits(32, 4, pat);
        if (got_w != 4 || words_complete != 4) begin
            $display("  FAIL: four whole words gave %0d valid pulses and a count of %0d",
                     got_w, words_complete);
            errors = errors + 1;
        end
        if (n_partials != 0) begin
            $display("  FAIL: a whole transaction produced %0d partials", n_partials);
            errors = errors + 1;
        end
        if (aborted) begin
            $display("  FAIL: a whole transaction set the abort flag");
            errors = errors + 1;
        end
        $display("  four whole eight-bit words: four valid pulses, a count of four, no partial and no abort flag");

        // 2. WORDS PLUS A PARTIAL. The completed words must all arrive -- this is the
        //    property that clearing on the abort rather than on the next assert
        //    breaks.
        for (k = 1; k <= 7; k = k + 1) begin
            clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
            expect_from(pat, 3, 8);
            drive_bits(24 + k, 4, pat);
            if (got_w != 3) begin
                $display("  FAIL: three whole words plus %0d bits delivered %0d words",
                         k, got_w);
                errors = errors + 1;
            end
            if (n_partials == 0) begin
                $display("  FAIL: %0d spare bits produced no partial", k);
                errors = errors + 1;
            end
            if (last_partial_bits != k) begin
                $display("  FAIL: %0d spare bits were reported as %0d",
                         k, last_partial_bits);
                errors = errors + 1;
            end
            // The partial's bits, right-aligned in the order they arrived.
            want_partial = {MAX_W{1'b0}};
            for (p = 0; p < k; p = p + 1)
                want_partial[k-1-p] = pat[63 - (24 + p)];
            if (last_partial !== want_partial) begin
                $display("  FAIL: %0d spare bits gave %08h, expected %08h",
                         k, last_partial, want_partial);
                errors = errors + 1;
            end
            if (!aborted) begin
                $display("  FAIL: a transaction with %0d spare bits did not set the abort flag",
                         k);
                errors = errors + 1;
            end
        end
        $display("  three whole words plus 1 to 7 spare bits: all three words delivered every time, with the partial reported on its own channel with the right count and the right bits");

        // 3. RECOVERY. The transaction after a partial must be fully correct -- the
        //    receive-side twin of Chapter 14.4's re-zero.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        expect_from(pat, 2, 8);
        drive_bits(19, 4, pat);          // two words and three spare bits
        pat = 64'h11_22_44_88_00_00_00_00;
        expect_from(pat, 4, 8);
        drive_bits(32, 4, pat);
        if (got_w != 4) begin
            $display("  FAIL: after an aborted transaction the next delivered %0d of 4 words",
                     got_w);
            errors = errors + 1;
        end
        $display("  the transaction after an aborted one delivers all four words correctly, including the first");

        // 4. A PARTIAL OF A NARROW WORD. With len = 3, two bits spare is a partial and
        //    three would be a word -- the boundary the count has to get right.
        len = 6'd3;
        adv(2);
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        pat = 64'hFF_00_FF_00_00_00_00_00;
        expect_from(pat, 3, 3);
        drive_bits(11, 4, pat);          // three 3-bit words and two spare
        if (got_w != 3) begin
            $display("  FAIL: len=3, 11 bits delivered %0d words", got_w);
            errors = errors + 1;
        end
        if (last_partial_bits != 2) begin
            $display("  FAIL: len=3, 11 bits reported a partial of %0d bits",
                     last_partial_bits);
            errors = errors + 1;
        end
        len = 6'd8;
        adv(2);
        $display("  at a width of three bits, eleven bits give three words and a two-bit partial");

        // 5. AN EMPTY TRANSACTION is not a partial. There is nothing to report, and
        //    reporting a zero-bit partial would be a channel that fires on nothing.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        k = n_partials;
        cs_n_pin = 1'b0; adv(10); cs_n_pin = 1'b1; adv(12);
        if (n_partials != k) begin
            $display("  FAIL: an empty transaction produced a partial");
            errors = errors + 1;
        end
        if (aborted) begin
            $display("  FAIL: an empty transaction set the abort flag");
            errors = errors + 1;
        end
        $display("  an empty transaction produces no partial and no abort flag -- there is nothing to report");

        // 6. AN ILLEGAL WIDTH is reported.
        len = 6'd0; adv(2);
        if (!len_err) begin
            $display("  FAIL: a width of 0 was not reported"); errors = errors + 1;
        end
        len = 6'd33; adv(2);
        if (!len_err) begin
            $display("  FAIL: a width of 33 was not reported on a 32-bit datapath");
            errors = errors + 1;
        end
        len = 6'd32; adv(2);
        if (len_err) begin
            $display("  FAIL: a width of 32 was reported on a 32-bit datapath");
            errors = errors + 1;
        end
        len = 6'd8; adv(2);
        $display("  widths of 0 and 33 are reported on a 32-bit datapath and 32 is not");

        // 7. THE CONTINUOUS PROPERTIES.
        if (word_bad != 0) begin
            $display("  FAIL: %0d of %0d words on the clean stream were wrong",
                     word_bad, n_words);
            errors = errors + 1;
        end
        if (both_channels != 0) begin
            $display("  FAIL: %0d cycles carried a word and a partial together",
                     both_channels);
            errors = errors + 1;
        end
        $display("  %0d words on the clean stream, none wrong, and never a word and a partial on the same cycle across %0d partials",
                 n_words, n_partials);

        if (errors == 0)
            $display("PASS: a slave cannot distinguish an abort from a short frame, a width mismatch, a phase mismatch or a lost edge -- all five leave the same pins -- so there is one policy for all of them: complete words go to the clean stream and a partial goes to a separate channel with its bit count, and never to the clean stream, which is structural because Chapter 14.3 raises no valid for an incomplete word at all -- three whole words followed by 1 to 7 spare bits deliver all three words every time with the partial reported with the right count and the right bits, the transaction after an aborted one delivers every word including the first because the counters are re-zeroed by the next assert rather than by the abort, a narrow width moves the partial boundary with no other change, an empty transaction reports nothing because there is nothing to report, an illegal width is reported, and across %0d words and %0d partials no cycle ever carried both",
                     n_words, n_partials);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_frame_tb.v — the same bench in Verilog-2001
// spi_slave_frame_tb.v
//
// The whole receive chain runs here -- the front end of 14.1, the transaction detector
// of 14.2, the mode logic of 14.6 and the capture path of 14.3 -- because the property
// under test is about what happens at the END of a transaction, and the end is a
// recovered event that only the real chain produces.
//
// Three properties get the attention, and the last is the one that catches the bug
// this chapter's header warns about:
//
//   A PARTIAL NEVER REACHES THE CLEAN STREAM. Checked continuously, not at the end:
//   the failure is one extra valid pulse, and a test that counts words at the end
//   would see the right total if a partial replaced a real word.
//
//   THE WORDS BEFORE A PARTIAL SURVIVE IT. A transaction of four whole words and three
//   spare bits must deliver four words, not three -- which is what clearing the
//   receive path on the abort rather than on the next assert produces.
//
//   THE TRANSACTION AFTER A PARTIAL IS FULLY CORRECT. This is the receive-side twin of
//   Chapter 14.4's transmit-side re-zero, and the signature is identical: the first
//   word after the abort is wrong and every later word is right.

`timescale 1ns/1ps

module spi_slave_frame_tb;

    localparam MAX_W = 32;
    localparam LEN_W = 6;
    localparam CNT_W = 12;

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

    reg cpol;
    reg cpha;
    reg lsb_first;
    reg clr_flags;

    reg sclk_pin;
    reg cs_n_pin;
    reg mosi_pin;

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

    spi_slave_frontend #(.SYNC_N(2), .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;

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

    spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(CNT_W)) 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)
    );

    wire cap_stb, launch_stb, preload_stb;
    wire cpol_mismatch, phase_suspect;
    wire [3:0] trunc_run;

    spi_slave_mode #(.SUSPECT_N(3), .CNT_W(4)) u_mode (
        .clk(clk), .rst_n(rst_n), .cpol(cpol), .cpha(cpha),
        .sclk_q(sclk_q), .mosi_q(mosi_q), .cs_assert_stb(cs_assert_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .txn_active(txn_active), .txn_start_stb(txn_start_stb),
        .txn_report_stb(txn_report_stb), .txn_clean(txn_clean),
        .txn_trunc(txn_trunc),
        .cap_stb(cap_stb), .launch_stb(launch_stb), .preload_stb(preload_stb),
        .cpol_mismatch(cpol_mismatch), .phase_suspect(phase_suspect),
        .moved_run(), .trunc_run(trunc_run), .clr_flags(1'b0)
    );

    wire [MAX_W-1:0] rx_data, rx_partial_sr;
    wire             rx_valid_stb;
    wire [LEN_W-1:0] bit_idx;
    wire [CNT_W-1:0] words_in_txn;

    spi_slave_rx #(.MAX_W(MAX_W), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_rx (
        .clk(clk), .rst_n(rst_n),
        .txn_active(txn_active), .txn_start_stb(txn_start_stb),
        .cap_stb(cap_stb), .mosi_q(mosi_q),
        .len(len), .lsb_first(lsb_first),
        .rx_data(rx_data), .rx_valid_stb(rx_valid_stb),
        .bit_idx(bit_idx), .words_in_txn(words_in_txn),
        .rx_partial_sr(rx_partial_sr)
    );

    // --- 14.7, the block under test -----------------------------------------
    wire [MAX_W-1:0] word_data;
    wire             word_valid_stb;
    wire [CNT_W-1:0] words_complete;
    wire [MAX_W-1:0] partial_data;
    wire [LEN_W-1:0] partial_bits;
    wire             partial_valid_stb;
    wire             len_err, aborted;
    wire [1:0]       frame_state;

    spi_slave_frame #(.MAX_W(MAX_W), .LEN_W(LEN_W), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n), .len(len),
        .txn_start_stb(txn_start_stb), .txn_report_stb(txn_report_stb),
        .txn_trunc(txn_trunc), .txn_empty(txn_empty),
        .rx_data(rx_data), .rx_valid_stb(rx_valid_stb), .bit_idx(bit_idx),
        .rx_partial_sr(rx_partial_sr),
        .word_data(word_data), .word_valid_stb(word_valid_stb),
        .words_complete(words_complete),
        .partial_data(partial_data), .partial_bits(partial_bits),
        .partial_valid_stb(partial_valid_stb),
        .len_err(len_err), .aborted(aborted), .state_id(frame_state),
        .clr_flags(clr_flags)
    );

    // --- the expectation ----------------------------------------------------
    integer seed;
    reg [MAX_W-1:0] exp_q [0:63];
    integer exp_w, got_w;
    integer word_bad, n_words, n_partials, both_channels;
    reg [MAX_W-1:0] last_partial;
    integer           last_partial_bits;

    always @(posedge clk) begin
        if (rst_n) begin
            if (txn_start_stb) got_w <= 0;
            if (word_valid_stb) begin
                n_words <= n_words + 1;
                if (got_w < exp_w && word_data !== exp_q[got_w])
                    word_bad <= word_bad + 1;
                got_w <= got_w + 1;
            end
            if (partial_valid_stb) begin
                n_partials        <= n_partials + 1;
                last_partial      <= partial_data;
                last_partial_bits <= partial_bits;
            end
            if (word_valid_stb && partial_valid_stb)
                both_channels <= both_channels + 1;
        end
    end

    integer errors;

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

    // Drives `nbits_total` bits into one transaction, MSB-first per word, from a
    // known pattern. Driving BITS rather than words is what lets the test produce a
    // transaction that is not a whole number of words.
        task drive_bits;
        input integer nbits_total;
        input integer half;
        input [63:0] pattern;
        integer i;
        begin
            cpol = 1'b0; sclk_pin = 1'b0; cs_n_pin = 1'b1;
            adv(10);
            cs_n_pin = 1'b0;
            adv(4);
            for (i = 0; i < nbits_total; i = i + 1) begin
                mosi_pin = pattern[63 - i];
                adv(2);
                sclk_pin = 1'b1;          // leading: captured here (CPHA=0)
                adv(half);
                sclk_pin = 1'b0;
                adv(half > 2 ? half - 2 : 1);
            end
            adv(4);
            cs_n_pin = 1'b1;
            adv(10);
            sclk_pin = 1'b0;
            adv(8);
        end
    endtask

    // The expected words, taken from the same pattern the driver used.
        task expect_from;
        input [63:0] pattern;
        input integer nwords;
        input integer nbits;
        integer w, b;
        reg [MAX_W-1:0] v;
        begin
            exp_w = 0;
            for (w = 0; w < nwords; w = w + 1) begin
                v = {MAX_W{1'b0}};
                for (b = 0; b < nbits; b = b + 1)
                    v[nbits-1-b] = pattern[63 - (w*nbits + b)];
                exp_q[exp_w] = v;
                exp_w = exp_w + 1;
            end
        end
    endtask

    integer k, p, want_bits;
    reg [MAX_W-1:0] want_partial;
    reg [63:0] pat;

    initial begin
        exp_w = 0; got_w = 0; word_bad = 0; n_words = 0; n_partials = 0;
        both_channels = 0; last_partial = 0; last_partial_bits = 0;
        seed = 32'h2B7F_0E91;

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

        // 1. A WHOLE TRANSACTION produces words and no partial.
        pat = 64'hA5_3C_5A_C3_00_00_00_00;
        expect_from(pat, 4, 8);
        drive_bits(32, 4, pat);
        if (got_w != 4 || words_complete != 4) begin
            $display("  FAIL: four whole words gave %0d valid pulses and a count of %0d",
                     got_w, words_complete);
            errors = errors + 1;
        end
        if (n_partials != 0) begin
            $display("  FAIL: a whole transaction produced %0d partials", n_partials);
            errors = errors + 1;
        end
        if (aborted) begin
            $display("  FAIL: a whole transaction set the abort flag");
            errors = errors + 1;
        end
        $display("  four whole eight-bit words: four valid pulses, a count of four, no partial and no abort flag");

        // 2. WORDS PLUS A PARTIAL. The completed words must all arrive -- this is the
        //    property that clearing on the abort rather than on the next assert
        //    breaks.
        for (k = 1; k <= 7; k = k + 1) begin
            clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
            expect_from(pat, 3, 8);
            drive_bits(24 + k, 4, pat);
            if (got_w != 3) begin
                $display("  FAIL: three whole words plus %0d bits delivered %0d words",
                         k, got_w);
                errors = errors + 1;
            end
            if (n_partials == 0) begin
                $display("  FAIL: %0d spare bits produced no partial", k);
                errors = errors + 1;
            end
            if (last_partial_bits != k) begin
                $display("  FAIL: %0d spare bits were reported as %0d",
                         k, last_partial_bits);
                errors = errors + 1;
            end
            // The partial's bits, right-aligned in the order they arrived.
            want_partial = {MAX_W{1'b0}};
            for (p = 0; p < k; p = p + 1)
                want_partial[k-1-p] = pat[63 - (24 + p)];
            if (last_partial !== want_partial) begin
                $display("  FAIL: %0d spare bits gave %08h, expected %08h",
                         k, last_partial, want_partial);
                errors = errors + 1;
            end
            if (!aborted) begin
                $display("  FAIL: a transaction with %0d spare bits did not set the abort flag",
                         k);
                errors = errors + 1;
            end
        end
        $display("  three whole words plus 1 to 7 spare bits: all three words delivered every time, with the partial reported on its own channel with the right count and the right bits");

        // 3. RECOVERY. The transaction after a partial must be fully correct -- the
        //    receive-side twin of Chapter 14.4's re-zero.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        expect_from(pat, 2, 8);
        drive_bits(19, 4, pat);          // two words and three spare bits
        pat = 64'h11_22_44_88_00_00_00_00;
        expect_from(pat, 4, 8);
        drive_bits(32, 4, pat);
        if (got_w != 4) begin
            $display("  FAIL: after an aborted transaction the next delivered %0d of 4 words",
                     got_w);
            errors = errors + 1;
        end
        $display("  the transaction after an aborted one delivers all four words correctly, including the first");

        // 4. A PARTIAL OF A NARROW WORD. With len = 3, two bits spare is a partial and
        //    three would be a word -- the boundary the count has to get right.
        len = 6'd3;
        adv(2);
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        pat = 64'hFF_00_FF_00_00_00_00_00;
        expect_from(pat, 3, 3);
        drive_bits(11, 4, pat);          // three 3-bit words and two spare
        if (got_w != 3) begin
            $display("  FAIL: len=3, 11 bits delivered %0d words", got_w);
            errors = errors + 1;
        end
        if (last_partial_bits != 2) begin
            $display("  FAIL: len=3, 11 bits reported a partial of %0d bits",
                     last_partial_bits);
            errors = errors + 1;
        end
        len = 6'd8;
        adv(2);
        $display("  at a width of three bits, eleven bits give three words and a two-bit partial");

        // 5. AN EMPTY TRANSACTION is not a partial. There is nothing to report, and
        //    reporting a zero-bit partial would be a channel that fires on nothing.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        k = n_partials;
        cs_n_pin = 1'b0; adv(10); cs_n_pin = 1'b1; adv(12);
        if (n_partials != k) begin
            $display("  FAIL: an empty transaction produced a partial");
            errors = errors + 1;
        end
        if (aborted) begin
            $display("  FAIL: an empty transaction set the abort flag");
            errors = errors + 1;
        end
        $display("  an empty transaction produces no partial and no abort flag -- there is nothing to report");

        // 6. AN ILLEGAL WIDTH is reported.
        len = 6'd0; adv(2);
        if (!len_err) begin
            $display("  FAIL: a width of 0 was not reported"); errors = errors + 1;
        end
        len = 6'd33; adv(2);
        if (!len_err) begin
            $display("  FAIL: a width of 33 was not reported on a 32-bit datapath");
            errors = errors + 1;
        end
        len = 6'd32; adv(2);
        if (len_err) begin
            $display("  FAIL: a width of 32 was reported on a 32-bit datapath");
            errors = errors + 1;
        end
        len = 6'd8; adv(2);
        $display("  widths of 0 and 33 are reported on a 32-bit datapath and 32 is not");

        // 7. THE CONTINUOUS PROPERTIES.
        if (word_bad != 0) begin
            $display("  FAIL: %0d of %0d words on the clean stream were wrong",
                     word_bad, n_words);
            errors = errors + 1;
        end
        if (both_channels != 0) begin
            $display("  FAIL: %0d cycles carried a word and a partial together",
                     both_channels);
            errors = errors + 1;
        end
        $display("  %0d words on the clean stream, none wrong, and never a word and a partial on the same cycle across %0d partials",
                 n_words, n_partials);

        if (errors == 0)
            $display("PASS: a slave cannot distinguish an abort from a short frame, a width mismatch, a phase mismatch or a lost edge -- all five leave the same pins -- so there is one policy for all of them: complete words go to the clean stream and a partial goes to a separate channel with its bit count, and never to the clean stream, which is structural because Chapter 14.3 raises no valid for an incomplete word at all -- three whole words followed by 1 to 7 spare bits deliver all three words every time with the partial reported with the right count and the right bits, the transaction after an aborted one delivers every word including the first because the counters are re-zeroed by the next assert rather than by the abort, a narrow width moves the partial boundary with no other change, an empty transaction reports nothing because there is nothing to report, an illegal width is reported, and across %0d words and %0d partials no cycle ever carried both",
                     n_words, n_partials);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


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

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_frame_tb.vhd — the same bench in VHDL
-- spi_slave_frame_tb.vhd
--
-- The whole receive chain runs here -- the front end of 14.1, the transaction detector
-- of 14.2, the mode logic of 14.6 and the capture path of 14.3 -- because the property
-- under test is about what happens at the END of a transaction, and the end is a
-- recovered event that only the real chain produces.
--
-- Three properties get the attention, and the last is the one that catches the bug this
-- chapter's header warns about:
--
--   A PARTIAL NEVER REACHES THE CLEAN STREAM. Checked continuously, not at the end: the
--   failure is one extra valid pulse, and a test that counts words at the end would see
--   the right total if a partial replaced a real word.
--
--   THE WORDS BEFORE A PARTIAL SURVIVE IT. A transaction of three whole words and some
--   spare bits must deliver three words -- which is what clearing the receive path on
--   the abort rather than on the next assert breaks.
--
--   THE TRANSACTION AFTER A PARTIAL IS FULLY CORRECT. This is the receive-side twin of
--   Chapter 14.4's transmit-side re-zero, with the identical signature: the first word
--   after the abort is wrong and every later word is right.

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

entity spi_slave_frame_tb is
end entity;

architecture sim of spi_slave_frame_tb is

    constant MAX_W : positive := 32;
    constant LEN_W : positive := 6;
    constant CNT_W : positive := 12;

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

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

    signal cpol      : std_logic := '0';
    signal cpha      : std_logic := '0';
    signal lsb_first : std_logic := '0';
    signal clr_flags : std_logic := '0';

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

    signal sclk_q, cs_active, mosi_q : std_logic;
    signal edge_a_stb, edge_b_stb, 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 txn_active, txn_start_stb, txn_end_stb, txn_report_stb : std_logic;
    signal edges_in_txn, frames_in_txn : unsigned(CNT_W - 1 downto 0);
    signal txn_clean, txn_trunc, txn_empty : std_logic;
    signal cs_state : unsigned(2 downto 0);

    signal cap_stb, launch_stb, preload_stb : std_logic;
    signal cpol_mismatch, phase_suspect : std_logic;
    signal trunc_run : unsigned(3 downto 0);
    signal mode_moved_run : unsigned(3 downto 0);

    signal rx_data, rx_partial_sr : std_logic_vector(MAX_W - 1 downto 0);
    signal rx_valid_stb : std_logic;
    signal bit_idx : unsigned(LEN_W - 1 downto 0);
    signal words_in_txn : unsigned(CNT_W - 1 downto 0);

    signal word_data      : std_logic_vector(MAX_W - 1 downto 0);
    signal word_valid_stb : std_logic;
    signal words_complete : unsigned(CNT_W - 1 downto 0);
    signal partial_data   : std_logic_vector(MAX_W - 1 downto 0);
    signal partial_bits   : unsigned(LEN_W - 1 downto 0);
    signal partial_valid_stb : std_logic;
    signal len_err, aborted : std_logic;
    signal frame_state : unsigned(1 downto 0);

    signal exp_q : word_array := (others => (others => '0'));
    signal exp_w : natural := 0;
    signal got_w : natural := 0;
    signal word_bad, n_words, n_partials, both_channels : natural := 0;
    signal last_partial : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    signal last_partial_bits : natural := 0;

    signal errors : natural := 0;

    function hex8(v : std_logic_vector) return string is
        constant DIGITS : string(1 to 16) := "0123456789abcdef";
        variable u : unsigned(MAX_W - 1 downto 0);
        variable r : string(1 to 8);
    begin
        u := unsigned(v);
        for k in 8 downto 1 loop
            r(k) := DIGITS(to_integer(u(3 downto 0)) + 1);
            u := shift_right(u, 4);
        end loop;
        return r;
    end function;

begin

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

    u_fe : entity work.spi_slave_frontend
        generic map (SYNC_N => 2, 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 => CNT_W)
        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);

    u_mode : entity work.spi_slave_mode
        generic map (SUSPECT_N => 3, CNT_W => 4)
        port map (clk => clk, rst_n => rst_n, cpol => cpol, cpha => cpha,
                  sclk_q => sclk_q, mosi_q => mosi_q,
                  cs_assert_stb => cs_assert_stb,
                  edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
                  txn_active => txn_active, txn_start_stb => txn_start_stb,
                  txn_report_stb => txn_report_stb, txn_clean => txn_clean,
                  txn_trunc => txn_trunc,
                  cap_stb => cap_stb, launch_stb => launch_stb,
                  preload_stb => preload_stb,
                  cpol_mismatch => cpol_mismatch, phase_suspect => phase_suspect,
                  moved_run => mode_moved_run, trunc_run => trunc_run, clr_flags => '0');

    u_rx : entity work.spi_slave_rx
        generic map (MAX_W => MAX_W, LEN_W => LEN_W, CNT_W => CNT_W)
        port map (clk => clk, rst_n => rst_n,
                  txn_active => txn_active, txn_start_stb => txn_start_stb,
                  cap_stb => cap_stb, mosi_q => mosi_q,
                  len => len, lsb_first => lsb_first,
                  rx_data => rx_data, rx_valid_stb => rx_valid_stb,
                  bit_idx => bit_idx, words_in_txn => words_in_txn,
                  rx_partial_sr => rx_partial_sr);

    dut : entity work.spi_slave_frame
        generic map (MAX_W => MAX_W, LEN_W => LEN_W, CNT_W => CNT_W)
        port map (clk => clk, rst_n => rst_n, len => len,
                  txn_start_stb => txn_start_stb,
                  txn_report_stb => txn_report_stb,
                  txn_trunc => txn_trunc, txn_empty => txn_empty,
                  rx_data => rx_data, rx_valid_stb => rx_valid_stb,
                  bit_idx => bit_idx, rx_partial_sr => rx_partial_sr,
                  word_data => word_data, word_valid_stb => word_valid_stb,
                  words_complete => words_complete,
                  partial_data => partial_data, partial_bits => partial_bits,
                  partial_valid_stb => partial_valid_stb,
                  len_err => len_err, aborted => aborted,
                  state_id => frame_state, clr_flags => clr_flags);

    monitor : process (clk)
    begin
        if rising_edge(clk) then
            if rst_n = '1' then
                if txn_start_stb = '1' then
                    got_w <= 0;
                end if;
                if word_valid_stb = '1' then
                    n_words <= n_words + 1;
                    if got_w < exp_w and word_data /= exp_q(got_w) then
                        word_bad <= word_bad + 1;
                    end if;
                    got_w <= got_w + 1;
                end if;
                if partial_valid_stb = '1' then
                    n_partials        <= n_partials + 1;
                    last_partial      <= partial_data;
                    last_partial_bits <= to_integer(partial_bits);
                end if;
                if word_valid_stb = '1' and partial_valid_stb = '1' then
                    both_channels <= both_channels + 1;
                end if;
            end if;
        end if;
    end process;

    stim : process
        variable errs : natural := 0;
        variable pat  : std_logic_vector(63 downto 0);
        variable want : std_logic_vector(MAX_W - 1 downto 0);
        variable k    : natural;

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

        -- Drives `nbits_total` bits into one transaction, MSB-first per word. Driving
        -- BITS rather than words is what lets the test produce a transaction that is
        -- not a whole number of words.
        procedure drive_bits(nbits_total : natural; half : natural;
                             pattern : std_logic_vector(63 downto 0)) is
        begin
            cpol <= '0'; sclk_pin <= '0'; cs_n_pin <= '1';
            adv(10);
            cs_n_pin <= '0';
            adv(4);
            for i in 0 to nbits_total - 1 loop
                mosi_pin <= pattern(63 - i);
                adv(2);
                sclk_pin <= '1';          -- leading: captured here (CPHA=0)
                adv(half);
                sclk_pin <= '0';
                if half > 2 then adv(half - 2); else adv(1); end if;
            end loop;
            adv(4);
            cs_n_pin <= '1';
            adv(10);
            sclk_pin <= '0';
            adv(8);
        end procedure;

        -- The expected words, taken from the same pattern the driver used.
        procedure expect_from(pattern : std_logic_vector(63 downto 0);
                              nwords : natural; nbits : natural) is
            variable v : std_logic_vector(MAX_W - 1 downto 0);
        begin
            for w in 0 to nwords - 1 loop
                v := (others => '0');
                for b in 0 to nbits - 1 loop
                    v(nbits - 1 - b) := pattern(63 - (w * nbits + b));
                end loop;
                exp_q(w) <= v;
            end loop;
            exp_w <= nwords;
            wait until falling_edge(clk);
        end procedure;
    begin
        adv(3);
        rst_n <= '1';
        adv(2);

        -- 1. A WHOLE TRANSACTION produces words and no partial.
        pat := x"A53C5AC300000000";
        expect_from(pat, 4, 8);
        drive_bits(32, 4, pat);
        if got_w /= 4 or to_integer(words_complete) /= 4 then
            report "  FAIL: four whole words gave " & integer'image(got_w) &
                   " valid pulses and a count of " &
                   integer'image(to_integer(words_complete));
            errs := errs + 1;
        end if;
        if n_partials /= 0 then
            report "  FAIL: a whole transaction produced " &
                   integer'image(n_partials) & " partials";
            errs := errs + 1;
        end if;
        if aborted = '1' then
            report "  FAIL: a whole transaction set the abort flag";
            errs := errs + 1;
        end if;
        report "  four whole eight-bit words: four valid pulses, a count of four, no partial and no abort flag";

        -- 2. WORDS PLUS A PARTIAL.
        for k2 in 1 to 7 loop
            clr_flags <= '1'; adv(1); clr_flags <= '0';
            expect_from(pat, 3, 8);
            drive_bits(24 + k2, 4, pat);
            if got_w /= 3 then
                report "  FAIL: three whole words plus " & integer'image(k2) &
                       " bits delivered " & integer'image(got_w) & " words";
                errs := errs + 1;
            end if;
            if last_partial_bits /= k2 then
                report "  FAIL: " & integer'image(k2) &
                       " spare bits were reported as " &
                       integer'image(last_partial_bits);
                errs := errs + 1;
            end if;
            want := (others => '0');
            for p in 0 to k2 - 1 loop
                want(k2 - 1 - p) := pat(63 - (24 + p));
            end loop;
            if last_partial /= want then
                report "  FAIL: " & integer'image(k2) & " spare bits gave " &
                       hex8(last_partial) & ", expected " & hex8(want);
                errs := errs + 1;
            end if;
            if aborted /= '1' then
                report "  FAIL: a transaction with " & integer'image(k2) &
                       " spare bits did not set the abort flag";
                errs := errs + 1;
            end if;
        end loop;
        report "  three whole words plus 1 to 7 spare bits: all three words delivered every time, with the partial reported on its own channel with the right count and the right bits";

        -- 3. RECOVERY.
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        expect_from(pat, 2, 8);
        drive_bits(19, 4, pat);
        pat := x"1122448800000000";
        expect_from(pat, 4, 8);
        drive_bits(32, 4, pat);
        if got_w /= 4 then
            report "  FAIL: after an aborted transaction the next delivered " &
                   integer'image(got_w) & " of 4 words";
            errs := errs + 1;
        end if;
        report "  the transaction after an aborted one delivers all four words correctly, including the first";

        -- 4. A PARTIAL OF A NARROW WORD.
        len <= to_unsigned(3, LEN_W);
        adv(2);
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        pat := x"FF00FF0000000000";
        expect_from(pat, 3, 3);
        drive_bits(11, 4, pat);
        if got_w /= 3 then
            report "  FAIL: len=3, 11 bits delivered " & integer'image(got_w) &
                   " words";
            errs := errs + 1;
        end if;
        if last_partial_bits /= 2 then
            report "  FAIL: len=3, 11 bits reported a partial of " &
                   integer'image(last_partial_bits) & " bits";
            errs := errs + 1;
        end if;
        len <= to_unsigned(8, LEN_W);
        adv(2);
        report "  at a width of three bits, eleven bits give three words and a two-bit partial";

        -- 5. AN EMPTY TRANSACTION is not a partial.
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        k := n_partials;
        cs_n_pin <= '0'; adv(10); cs_n_pin <= '1'; adv(12);
        if n_partials /= k then
            report "  FAIL: an empty transaction produced a partial";
            errs := errs + 1;
        end if;
        if aborted = '1' then
            report "  FAIL: an empty transaction set the abort flag";
            errs := errs + 1;
        end if;
        report "  an empty transaction produces no partial and no abort flag -- there is nothing to report";

        -- 6. AN ILLEGAL WIDTH is reported.
        len <= to_unsigned(0, LEN_W); adv(2);
        if len_err /= '1' then
            report "  FAIL: a width of 0 was not reported"; errs := errs + 1;
        end if;
        len <= to_unsigned(33, LEN_W); adv(2);
        if len_err /= '1' then
            report "  FAIL: a width of 33 was not reported on a 32-bit datapath";
            errs := errs + 1;
        end if;
        len <= to_unsigned(32, LEN_W); adv(2);
        if len_err = '1' then
            report "  FAIL: a width of 32 was reported on a 32-bit datapath";
            errs := errs + 1;
        end if;
        len <= to_unsigned(8, LEN_W); adv(2);
        report "  widths of 0 and 33 are reported on a 32-bit datapath and 32 is not";

        -- 7. THE CONTINUOUS PROPERTIES.
        if word_bad /= 0 then
            report "  FAIL: " & integer'image(word_bad) & " of " &
                   integer'image(n_words) &
                   " words on the clean stream were wrong";
            errs := errs + 1;
        end if;
        if both_channels /= 0 then
            report "  FAIL: " & integer'image(both_channels) &
                   " cycles carried a word and a partial together";
            errs := errs + 1;
        end if;
        report "  " & integer'image(n_words) &
               " words on the clean stream, none wrong, and never a word and a partial on the same cycle across " &
               integer'image(n_partials) & " partials";

        errors <= errs;
        if errs = 0 then
            report "PASS: a slave cannot distinguish an abort from a short frame, a width mismatch, a phase mismatch or a lost edge -- all five leave the same pins -- so there is one policy for all of them: complete words go to the clean stream and a partial goes to a separate channel with its bit count, and never to the clean stream, which is structural because Chapter 14.3 raises no valid for an incomplete word at all -- three whole words followed by 1 to 7 spare bits deliver all three words every time with the partial reported with the right count and the right bits, the transaction after an aborted one delivers every word including the first because the counters are re-zeroed by the next assert rather than by the abort, a narrow width moves the partial boundary with no other change, an empty transaction reports nothing because there is nothing to report, an illegal width is reported, and across " & integer'image(n_words) & " words and " & integer'image(n_partials) & " partials no cycle ever carried both";
        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

Test 3 is the shape worth stealing: check the transaction after the interesting one. A bench that verifies each transaction independently cannot find state that leaks across a boundary, and leaked state is the dominant bug class in this module — three blocks, same shape. The cheap version of this discipline is to make every directed test run twice, back to back, and assert the second run is identical to the first.

The partition between the two channels needs a mutual-exclusion check, not two separate checks. word_valid_stb and partial_valid_stb must never coincide, and neither may fire without the other being silent. Checking each against its own condition permits both to be wrong in the same direction.

A zero-bit partial must be checked for explicitly. It is the natural output of a design that publishes a partial whenever a transaction ends, and it would fire on every empty select pulse — turning the partial channel into noise. Test 5 asserts silence.

Sweep the width including len == MAX_W. That is where the truncation bug of §5 lives, and it is a legal width that a naive bound check rejects.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Properties for the framing block.

property p_channels_exclusive;
    // The partition. A cycle carrying both means a fragment reached the normal
    // path, which is the one thing the two-channel structure exists to prevent.
    @(posedge clk) disable iff (!rst_n)
        not (word_valid_stb && partial_valid_stb);
endproperty

property p_no_zero_bit_partial;
    // A partial always carries at least one bit. A zero-bit partial fires on
    // every empty select pulse and makes the channel useless.
    @(posedge clk) disable iff (!rst_n)
        partial_valid_stb |-> (partial_bits != 0);
endproperty

property p_partial_narrower_than_word;
    // And at most len - 1 bits: a partial of len bits is a word, and publishing
    // it on the partial channel would lose it from the normal one.
    @(posedge clk) disable iff (!rst_n)
        partial_valid_stb |-> (partial_bits < len);
endproperty

property p_cleared_at_start_not_end;
    // Section 2 and 3's rule, checkable directly: the word count is zero one
    // cycle after a start, and is NOT disturbed by the report strobe.
    @(posedge clk) disable iff (!rst_n)
        txn_start_stb |=> (words_complete == 0);
endproperty

property p_words_survive_a_partial;
    // The count of complete words does not decrease when a partial is
    // published. This is the property that fails when the clear is at the end.
    @(posedge clk) disable iff (!rst_n)
        partial_valid_stb |-> (words_complete == $past(words_complete));
endproperty

property p_len_err_sticky;
    // An illegal width stays reported, because a driver that writes a bad width
    // once and a good one afterwards has still broken a transaction.
    @(posedge clk) disable iff (!rst_n)
        len_err && !clr_flags |=> len_err;
endproperty
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Coverage. The axis that matters is HOW MUCH of a word was left over, because
// that is what the two channels partition -- and it must be crossed with the
// width, since "two bits spare" means something different at len 3 and len 32.

covergroup cg_frame @(posedge clk iff txn_report_stb);
    option.per_instance = 1;

    leftover: coverpoint bits_held_at_end {
        bins none      = {0};                 // a clean transaction
        bins one_bit   = {1};                 // the narrowest partial
        bins few       = {[2:4]};
        bins most      = {[5:30]};
        bins all_but_1 = {31};                // at len = 32, a 31-bit partial
    }

    width: coverpoint len {
        bins illegal_low  = {0};
        bins one          = {1};
        bins narrow       = {[2:7]};
        bins byte_w       = {8};
        bins wide         = {[9:31]};
        bins full         = {32};
        bins illegal_high = {[33:$]};
    }

    // How many complete words preceded the partial. Zero is the case where the
    // transaction was nothing but a fragment; a large number is where section
    // 2's clear-at-the-end bug loses one.
    preceding: coverpoint words_complete {
        bins none = {0};
        bins one  = {1};
        bins few  = {[2:4]};
        bins many = {[5:$]};
    }

    x_leftover_width:  cross leftover, width;
    x_leftover_before: cross leftover, preceding;

endgroup

8. Why an FPGA or ASIC Engineer Cares

This block contains no shift register, which is worth noting because it looks like it should. The partial's data comes from Chapter 14.3's published in-progress register, so the cost here is one MAX_W-wide hold register and two counters. Duplicating the shift register would have doubled the receive path's flop count for no function.

The hold register is the only wide state here and it exists for one reason: the last complete word must survive until the transaction's verdict is known. It is MAX_W flops spent on §2's property, and a design that skipped it would deliver words as they arrive and be unable to hold one back — which is fine until the verdict says the transaction was truncated and software wants to know whether the last word predates the truncation.

len_err is combinational from len and is deliberately not gated on a transaction. An illegal width is a configuration fault, so it should be visible as soon as software writes it rather than only after a transfer has been corrupted. That is a different choice from the other flags in the module, all of which describe something that happened on the bus.

The width comparison must be integer, and on an FPGA the difference is invisible in simulation and visible in a wrong constant. len > MAX_W with MAX_W truncated to LEN_W bits is a comparison against a value the designer never wrote, and the synthesised constant folds to zero — so the tool reports no warning, the logic is smaller than expected, and every width is rejected.

9. Failure Signature — The Last Byte Of An Aborted Transfer Is Missing

The symptom:

"When the host aborts a transfer, we get one fewer byte than it sent. If it sends five bytes and aborts during the sixth, we see four complete bytes plus a partial, not five plus a partial."

What is happening: the receive path is cleared at the end of the transaction rather than at its start. The fifth complete word is in the hold register when the deassert arrives, and the end-of-transaction clear discards it along with the fragment.

Why it reads as an off-by-one in the buffer: because it is exactly one byte, every time, and only on aborted transfers. The natural hypothesis is that the buffer's write pointer is advanced before the data lands, or that the last entry is being overwritten — both of which are plausible and both of which are in Chapter 14.9 rather than here.

How to tell: send a clean five-byte transfer and an aborted five-and-a-bit-byte transfer. If the clean one delivers five and the aborted one delivers four, the fault is a clear on the end-of-transaction event, not a buffer fault. The distinguishing observation is that the count is correct whenever the transaction was clean — a buffer bug would not care.

10. Common Misconceptions

"A partial should be delivered through the normal channel with a length field." Then correctness depends on every consumer reading the length, and a consumer that does not gets right-aligned rubbish that is indistinguishable from a legitimate narrow word. Two channels make ignoring the exception the correct behaviour.

"Discarding the partial is the safe default." It is the default, and it makes a CPHA mismatch look like a device that never responds. Silence about an anomaly is a decision, not an absence of one.

"The slave can tell an abort from a short frame." It cannot. Five distinct faults produce identical pins. The slave's job is to report what happened with enough detail that software — which has context the slave lacks — can decide.

"The receive path should be cleared when the transaction ends." That is exactly when the exceptional cases are discovered, so clearing there discards the evidence. Clear at the start, which is unambiguous.

"An empty transaction is a zero-bit partial." It is nothing, and publishing a zero-bit partial makes the partial channel fire on every select glitch. There is a difference between "a fragment arrived" and "nothing arrived", and the partial channel is for the first.

"A width equal to MAX_W is a boundary case that should probably be rejected." It is entirely legal and it is where the truncation bug of §5 lives. Rejecting it is the symptom of that bug rather than a conservative choice.

11. Reason It Through

Q. len = 8, a transaction carries 68 SCLK edges, and then chip select rises. What does this block publish?

Four words and a two-bit partial. 68 edges is 34 captures, which is four whole words (32 captures) plus two. The four words are published as they complete, during the transaction; the partial is published on the report strobe with partial_bits = 2. Chapter 14.2 independently reports txn_trunc with edges_in_txn = 68, and the two reports together are what software needs.

Q. Why must the partial be published on the report strobe rather than the end strobe?

Because the verdict that says whether a partial exists is not valid until the report strobe — that is Chapter 14.2's §2. Publishing on the end strobe would read the previous transaction's txn_trunc, so a clean transaction following a truncated one would emit a spurious partial and a truncated one following a clean one would emit none.

Q. A transaction ends after exactly len bits of its third word. Is that a partial?

No — it is a complete word, and the transaction is clean. A partial requires fewer than len bits held. This is why the property partial_bits < len is checked: a design that published a full word on the partial channel would lose it from the normal one, and the consumer that ignores partials would see two words where three arrived.

Q. Someone proposes making len_err gated on txn_active, arguing that an illegal width only matters when a transfer happens. What is the argument against?

That the point of the flag is to catch the configuration error before it corrupts a transfer. An ungated len_err is visible the moment software writes a bad width, which is when the driver author is still looking; a gated one appears only after a transaction has already produced garbage, mixed in with the flags describing that garbage. The flag describes the configuration, not the bus, and gating it on bus activity conflates the two.

Q. MAX_W = 32, LEN_W = 6, and the width check is written len > MAX_W[LEN_W-1:0]. What happens, and why is it hard to spot?

MAX_W truncated to 6 bits is 32 & 0x3F = 32 — which happens to be correct here. Change MAX_W to 64 with LEN_W = 7 and 64 & 0x7F = 64, also correct. The bug bites when MAX_W is exactly a power of two equal to 2^LEN_W: at MAX_W = 64 with LEN_W = 6, 64 & 0x3F = 0, and every width is illegal. It is hard to spot because it depends on the relationship between two parameters rather than on either one, and it is correct for most combinations — which is the same shape as Chapter 13.7's N_CS bug.

12. Understanding Check

13. Summary

Five different faults — a width disagreement, a deliberate abort, a killed driver, a CPHA mismatch, a lost edge — produce identical pins. So the slave needs one policy, and it cannot be to diagnose: it reports what happened and software decides, because software has context the slave does not.

Of the three policies, a separate channel with a bit count is the right one. Discarding silently makes a CPHA mismatch look like a dead device. Presenting a fragment as a word makes correctness depend on every consumer reading a length field, and makes a three-bit fragment indistinguishable from a legitimate three-bit word. The separate channel makes ignoring the exception the correct behaviour — which is the general principle for interfaces that carry an exception alongside a normal case.

The specification is one line: word_valid_stb fires only for a word of exactly len bits, and a partial never reaches it.

What must survive a partial is the words that completed before it — so the clear belongs at the transaction start, not its end, because the end is when the partial is discovered. Clearing at the end loses the last complete word, and the symptom reads as a buffer off-by-one.

What must survive after a partial is the next transaction. Counters left part-way through a word are re-zeroed by the next assert, not by the abort — the receive-side twin of Chapter 14.4's re-zero, with the same failure shape: the first word after the abort is wrong and every later word is right.

Three blocks in this module have made that same mistake, which makes it a rule rather than three bugs: an end-of-transaction event is not a reliable place to clear state.

An illegal width is reported, the comparison is against an integer, and a width of exactly MAX_W is legal — the boundary that a truncated-constant bound check gets wrong.

For verification: check the transaction after the interesting one, because leaked state is this module's dominant bug class; check the two channels as a partition; assert that a zero-bit partial never appears; and cross the leftover bit count with the width, because two bits spare means different things at 3 and at 32.

For implementation: no shift register here, one MAX_W hold register spent on the survive-a-partial property, and len_err deliberately ungated because it describes a configuration rather than a bus event.

14. What Comes Next

The slave handles short frames, aborts and illegal widths. It has been assuming that reset happens when nothing is going on.

Chapter 14.8 — Reset Behaviour and Safe Idle removes that assumption, and the problem it creates is sharper than it looks. Reset is asynchronous to SCLK, so it can land in the middle of a transaction — and when it does, the slave comes out of reset selected, halfway through a word, with no way to know how far through. Worse, the fact it most needs is destroyed by the reset that created the situation: the synchronisers reset to deselected, so the slave cannot even tell that the select was already low. The chapter recovers the information from timing instead.

Continue learning