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:
* 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_stbfires only for a word of exactlylenbits, 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:
the first word of the transaction AFTER the abort is wrong
and every later word is rightWhich 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
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.
// 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// 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-- 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.
- A whole transaction produces words and no partial. The baseline.
- 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.
- 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.
- 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. - 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.
- An illegal width is reported, at zero and above
MAX_W, and a width of exactlyMAX_Wis legal. - The continuous properties.
word_valid_stbnever fires for a partial;partial_valid_stbnever fires with a zero bit count; the two never coincide.
The run covers 34 words and 9 partials across the widths and cases.
// 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// 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-- 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.
// 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// 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;
endgroup8. 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
Related tutorials
- Related topic
Partial and Aborted Transactions
Chip select deasserting mid-frame: why an abort is legitimate, why an empty frame and a partial word need different responses, and the guard that classifies a frame's ending so a partial word never commits.
- Related topic
Busy/Done/Valid Interface, Reset, and Abort
Stopping a transfer without leaving a slave stranded: why abort is orderly and reset is instant, why the abort input belongs in the block that owns the pins, and why a partial word must be reported rather than silently absent.
- Related topic
Slave Microarchitecture and Clocking Assumptions
The two architectures available to a slave that does not own its clock, the one precondition the chosen architecture rests on and why no simulation can test it, why MOSI must pass through exactly as many flops as SCLK, and a delay-matched front end verified in three HDLs.
- Related topic
CS Detection and Transaction Boundaries
Chip select is SPI's only framing and therefore its only resynchronisation point: why counters must reset on assert, how to classify a transaction with a running remainder instead of a divider, why a CPHA mismatch is invisible to the edge count, and a transaction detector verified in three HDLs.
