SPI · Module 17
Corner Cases That Break SPI Designs
Six corner cases and one slave written twice. Three of the cases are perfectly legal and must produce no report at all; the other three are fixed by three decisions about when a value is sampled, not by new logic. And two of them are caught by counting announcements, not by comparing payloads.
A corner case is not the same thing as an illegal stimulus, and conflating the two is how a suite ends up testing neither.
Three of the six cases in this chapter are perfectly legal. A slave that reports a problem on those is broken in the other direction, and a table containing only failures cannot tell a robust design from a paranoid one.
1. The Six
LEGAL, and a correct slave must handle all three silently
--------------------------------------------------------------------------
C4 a half period at the minimum
C5 a gap at the minimum
C6 a one-bit frame
ABNORMAL, and a real system produces all three anyway
--------------------------------------------------------------------------
C1 reset asserted in the middle of a frame
C2 chip select released in the middle of a frame
C3 the frame width reprogrammed between two frames of a burst2. The Slave, Written Twice
Both slaves see identical pins. The difference between them is three decisions, and none of them is new logic:
| naive | hardened | |
|---|---|---|
| the frame width | read from the input continuously | latched at the select |
rx_valid | raised on any deassert | raised only for a frame that reached its expected bit count |
| the shift register | cleared only at reset | cleared at every select |
That is the whole of it. Corner-case robustness in a protocol slave is almost always a decision about when a value is sampled or whether an output is qualified, and almost never new state.
3. What C3 Looks Like
A width change between two frames of a burst
16 cyclesThe change is made while the bus is idle, which is the legal moment to make it. The naive slave still applies it to a frame already in progress, because it never latched the old one.
4. The Measurement
case naive slave hardened slave
C1 reset mid-frame words 2 WRONG words 1 ok
C2 select released mid-frame words 2 WRONG words 1 ok
C3 width reprogrammed between frames words 2, last a9 WRONG words 2, last 09 ok
C4 half period at the minimum (LEGAL) words 1, last 5a words 1, last 5a
C5 gap at the minimum (LEGAL) words 2, last 99 words 2, last 99
C6 one-bit frame (LEGAL) words 1, last 99 words 1, last 01The three abnormal events all broke the naive slave and none broke the hardened one. And the three legal extremes: the hardened slave handled all three correctly and silently, and the naive one mishandled one — C6, where it returned the previous frame's word with its bottom bit replaced.
That polarity is half the measurement. We tested the corner cases means nothing until the legal ones are known to be quiet.
5. The Two Cases That A Payload Comparison Cannot See
That is the transferable result of the chapter, and it generalises past SPI: a checker that compares values cannot see a transaction that should not exist.
6. Building It — Three HDLs
// spi_slave_lite.sv
//
// Chapter 17.6 -- the corner cases that break SPI designs, and a slave written twice so that they
// have something to break.
//
// A CORNER CASE IS NOT THE SAME THING AS AN ILLEGAL STIMULUS, and conflating the two is how a
// suite ends up testing neither. Of the six cases this chapter drives, THREE ARE PERFECTLY LEGAL
// -- a one-bit frame, a half period at the minimum, a gap at the minimum -- and a correct slave
// must handle all three silently. The other three are abnormal events a real system produces
// anyway: a reset in the middle of a frame, a chip select released in the middle of a frame, and a
// frame width reprogrammed between two frames of a burst.
//
// So the measurement has to have BOTH polarities: the legal extremes must produce no report, and
// the abnormal events must. A table with only failures in it does not distinguish a robust design
// from a paranoid one.
//
// THIS MODULE IS THE SAME SLAVE TWICE, selected by `hard`.
//
// hard = 0 THE NAIVE SLAVE, and everything it gets wrong is something a first implementation
// gets wrong:
// * it reads the frame width from its input CONTINUOUSLY, so reprogramming the
// width between two frames of a burst changes the width of a frame already in
// progress;
// * it raises `rx_valid` on any deassert, so an aborted frame delivers a garbage
// word that looks exactly like a real one;
// * it does not clear its shift register on reset mid-frame, so the next frame
// starts with the previous frame's bits still in it;
// The fourth item people expect to find here -- an off-by-one at a width of one --
// is NOT one of its defects. The measurement says so: at a one-bit frame the naive
// slave returns the PREVIOUS frame's word with its bottom bit replaced, which is the
// uncleared shift register again rather than an index error. Three defects, and two of
// the six corner cases are just the same one arriving by a different route.
//
// hard = 1 THE HARDENED SLAVE. The width is LATCHED at the select; `rx_valid` is raised only
// for a frame that reached its expected bit count; and the shift register is cleared
// at every select rather than only at reset. Three changes, and they fix all three
// abnormal cases and the one-bit case together.
//
// NOTE WHAT IS NOT IN THE HARDENED VERSION: no extra state, no error output, no configuration.
// Every difference is a decision about WHEN a value is sampled or WHETHER an output is qualified.
// Corner-case robustness in a protocol slave is almost always that, and almost never new logic.
`timescale 1ns/1ps
module spi_slave_lite #(
parameter int DW = 32,
parameter int LEN_W = 6
) (
input wire clk,
input wire rst_n,
input wire hard, // 0 = the naive slave, 1 = the hardened one
// --- the pins -----------------------------------------------------------
input wire sclk,
input wire cs_n,
input wire mosi,
// --- the configuration --------------------------------------------------
input wire cpol,
input wire cpha,
input wire [LEN_W-1:0] nbits,
// --- the received word --------------------------------------------------
output reg rx_valid,
output reg [DW-1:0] rx_data,
output reg [LEN_W:0] rx_nbits
);
reg sclk_d, cs_n_d;
wire cs_assert = ~cs_n & cs_n_d;
wire cs_deassert = cs_n & ~cs_n_d;
wire in_txn = ~cs_n | cs_deassert;
wire sclk_edge = sclk ^ sclk_d;
wire leading = sclk_edge & (sclk != cpol);
wire capture = (cpha ? (sclk_edge & ~leading) : leading) & in_txn;
reg [DW-1:0] sh;
reg [LEN_W:0] k; // bits captured so far
reg [LEN_W-1:0] n_latched; // the hardened slave's latched width
// THE ONE LINE THAT IS THE WHOLE OF CORNER CASE 3. The naive slave reads `nbits` as it is
// NOW; the hardened one reads the value latched when the select fell. A width reprogrammed
// between two frames of a burst reaches the naive slave in the middle of a frame.
wire [LEN_W-1:0] n_eff = hard ? n_latched : nbits;
wire [LEN_W-1:0] idx = n_eff - 1'b1 - k[LEN_W-1:0];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
sh <= {DW{1'b0}};
k <= {(LEN_W+1){1'b0}};
n_latched <= {LEN_W{1'b0}};
rx_valid <= 1'b0;
rx_data <= {DW{1'b0}};
rx_nbits <= {(LEN_W+1){1'b0}};
end else begin
sclk_d <= sclk;
cs_n_d <= cs_n;
rx_valid <= 1'b0;
if (cs_assert) begin
n_latched <= nbits;
k <= {(LEN_W+1){1'b0}};
// THE SECOND DIFFERENCE. The hardened slave clears its shift register at every
// select; the naive one clears it only at reset, so a frame aborted or cut short
// leaves its bits in place and the NEXT frame starts contaminated. That is corner
// cases 1 and 2 arriving one frame late, which is what makes them hard to find:
// the failing frame is not the one the stimulus was aimed at.
if (hard) sh <= {DW{1'b0}};
end else if (capture) begin
if (k < {1'b0, n_eff}) begin
sh[idx] <= mosi;
k <= k + 1'b1;
end
end
if (cs_deassert) begin
rx_data <= (capture && (k < {1'b0, n_eff}))
? (sh | ({{(DW-1){1'b0}}, mosi} << idx)) : sh;
rx_nbits <= (capture && (k < {1'b0, n_eff})) ? k + 1'b1 : k;
// THE THIRD DIFFERENCE, and the dangerous one. The naive slave announces a word
// on ANY deassert. The hardened one announces only a frame that reached its
// expected bit count, so an aborted frame produces silence rather than a garbage
// word that is indistinguishable from a real one.
if (hard)
rx_valid <= (((capture && (k < {1'b0, n_eff})) ? k + 1'b1 : k)
== {1'b0, n_eff});
else
rx_valid <= 1'b1;
k <= {(LEN_W+1){1'b0}};
end
end
end
endmodule// spi_slave_lite.v
//
// Chapter 17.6 -- the corner cases that break SPI designs, and a slave written twice so that they
// have something to break.
//
// A CORNER CASE IS NOT THE SAME THING AS AN ILLEGAL STIMULUS, and conflating the two is how a
// suite ends up testing neither. Of the six cases this chapter drives, THREE ARE PERFECTLY LEGAL
// -- a one-bit frame, a half period at the minimum, a gap at the minimum -- and a correct slave
// must handle all three silently. The other three are abnormal events a real system produces
// anyway: a reset in the middle of a frame, a chip select released in the middle of a frame, and a
// frame width reprogrammed between two frames of a burst.
//
// So the measurement has to have BOTH polarities: the legal extremes must produce no report, and
// the abnormal events must. A table with only failures in it does not distinguish a robust design
// from a paranoid one.
//
// THIS MODULE IS THE SAME SLAVE TWICE, selected by `hard`.
//
// hard = 0 THE NAIVE SLAVE, and everything it gets wrong is something a first implementation
// gets wrong:
// * it reads the frame width from its input CONTINUOUSLY, so reprogramming the
// width between two frames of a burst changes the width of a frame already in
// progress;
// * it raises `rx_valid` on any deassert, so an aborted frame delivers a garbage
// word that looks exactly like a real one;
// * it does not clear its shift register on reset mid-frame, so the next frame
// starts with the previous frame's bits still in it;
// The fourth item people expect to find here -- an off-by-one at a width of one --
// is NOT one of its defects. The measurement says so: at a one-bit frame the naive
// slave returns the PREVIOUS frame's word with its bottom bit replaced, which is the
// uncleared shift register again rather than an index error. Three defects, and two of
// the six corner cases are just the same one arriving by a different route.
//
// hard = 1 THE HARDENED SLAVE. The width is LATCHED at the select; `rx_valid` is raised only
// for a frame that reached its expected bit count; and the shift register is cleared
// at every select rather than only at reset. Three changes, and they fix all three
// abnormal cases and the one-bit case together.
//
// NOTE WHAT IS NOT IN THE HARDENED VERSION: no extra state, no error output, no configuration.
// Every difference is a decision about WHEN a value is sampled or WHETHER an output is qualified.
// Corner-case robustness in a protocol slave is almost always that, and almost never new logic.
`timescale 1ns/1ps
module spi_slave_lite #(
parameter DW = 32,
parameter LEN_W = 6
) (
input wire clk,
input wire rst_n,
input wire hard, // 0 = the naive slave, 1 = the hardened one
// --- the pins -----------------------------------------------------------
input wire sclk,
input wire cs_n,
input wire mosi,
// --- the configuration --------------------------------------------------
input wire cpol,
input wire cpha,
input wire [LEN_W-1:0] nbits,
// --- the received word --------------------------------------------------
output reg rx_valid,
output reg [DW-1:0] rx_data,
output reg [LEN_W:0] rx_nbits
);
reg sclk_d, cs_n_d;
wire cs_assert = ~cs_n & cs_n_d;
wire cs_deassert = cs_n & ~cs_n_d;
wire in_txn = ~cs_n | cs_deassert;
wire sclk_edge = sclk ^ sclk_d;
wire leading = sclk_edge & (sclk != cpol);
wire capture = (cpha ? (sclk_edge & ~leading) : leading) & in_txn;
reg [DW-1:0] sh;
reg [LEN_W:0] k; // bits captured so far
reg [LEN_W-1:0] n_latched; // the hardened slave's latched width
// THE ONE LINE THAT IS THE WHOLE OF CORNER CASE 3. The naive slave reads `nbits` as it is
// NOW; the hardened one reads the value latched when the select fell. A width reprogrammed
// between two frames of a burst reaches the naive slave in the middle of a frame.
wire [LEN_W-1:0] n_eff = hard ? n_latched : nbits;
wire [LEN_W-1:0] idx = n_eff - 1'b1 - k[LEN_W-1:0];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
sh <= {DW{1'b0}};
k <= {(LEN_W+1){1'b0}};
n_latched <= {LEN_W{1'b0}};
rx_valid <= 1'b0;
rx_data <= {DW{1'b0}};
rx_nbits <= {(LEN_W+1){1'b0}};
end else begin
sclk_d <= sclk;
cs_n_d <= cs_n;
rx_valid <= 1'b0;
if (cs_assert) begin
n_latched <= nbits;
k <= {(LEN_W+1){1'b0}};
// THE SECOND DIFFERENCE. The hardened slave clears its shift register at every
// select; the naive one clears it only at reset, so a frame aborted or cut short
// leaves its bits in place and the NEXT frame starts contaminated. That is corner
// cases 1 and 2 arriving one frame late, which is what makes them hard to find:
// the failing frame is not the one the stimulus was aimed at.
if (hard) sh <= {DW{1'b0}};
end else if (capture) begin
if (k < {1'b0, n_eff}) begin
sh[idx] <= mosi;
k <= k + 1'b1;
end
end
if (cs_deassert) begin
rx_data <= (capture && (k < {1'b0, n_eff}))
? (sh | ({{(DW-1){1'b0}}, mosi} << idx)) : sh;
rx_nbits <= (capture && (k < {1'b0, n_eff})) ? k + 1'b1 : k;
// THE THIRD DIFFERENCE, and the dangerous one. The naive slave announces a word
// on ANY deassert. The hardened one announces only a frame that reached its
// expected bit count, so an aborted frame produces silence rather than a garbage
// word that is indistinguishable from a real one.
if (hard)
rx_valid <= (((capture && (k < {1'b0, n_eff})) ? k + 1'b1 : k)
== {1'b0, n_eff});
else
rx_valid <= 1'b1;
k <= {(LEN_W+1){1'b0}};
end
end
end
endmodule-- spi_slave_lite.vhd
--
-- Chapter 17.6 -- the corner cases that break SPI designs, and a slave written twice so that they
-- have something to break.
--
-- A CORNER CASE IS NOT THE SAME THING AS AN ILLEGAL STIMULUS, and conflating the two is how a
-- suite ends up testing neither. Of the six cases this chapter drives, THREE ARE PERFECTLY LEGAL
-- -- a one-bit frame, a half period at the minimum, a gap at the minimum -- and a correct slave
-- must handle all three silently. The other three are abnormal events a real system produces
-- anyway: a reset in the middle of a frame, a chip select released in the middle of a frame, and a
-- frame width reprogrammed between two frames of a burst.
--
-- So the measurement has to have BOTH polarities: the legal extremes must produce no report, and
-- the abnormal events must. A table with only failures in it does not distinguish a robust design
-- from a paranoid one.
--
-- THIS MODULE IS THE SAME SLAVE TWICE, selected by `hard`.
--
-- hard = 0 THE NAIVE SLAVE, and everything it gets wrong is something a first implementation
-- gets wrong:
-- * it reads the frame width from its input CONTINUOUSLY, so reprogramming the
-- width between two frames of a burst changes the width of a frame already in
-- progress;
-- * it raises `rx_valid` on any deassert, so an aborted frame delivers a garbage
-- word that looks exactly like a real one;
-- * it does not clear its shift register on reset mid-frame, so the next frame
-- starts with the previous frame's bits still in it;
-- The fourth item people expect to find here -- an off-by-one at a width of one --
-- is NOT one of its defects. The measurement says so: at a one-bit frame the naive
-- slave returns the PREVIOUS frame's word with its bottom bit replaced, which is the
-- uncleared shift register again rather than an index error. Three defects, and two of
-- the six corner cases are just the same one arriving by a different route.
--
-- hard = 1 THE HARDENED SLAVE. The width is LATCHED at the select; `rx_valid` is raised only
-- for a frame that reached its expected bit count; and the shift register is cleared
-- at every select rather than only at reset. Three changes, and they fix all three
-- abnormal cases and the one-bit case together.
--
-- NOTE WHAT IS NOT IN THE HARDENED VERSION: no extra state, no error output, no configuration.
-- Every difference is a decision about WHEN a value is sampled or WHETHER an output is qualified.
-- Corner-case robustness in a protocol slave is almost always that, and almost never new logic.
--
-- WHAT VHDL ADDS HERE: the two behaviours are selected by a GENERIC rather than a port, so the
-- naive slave and the hardened one are different elaborations rather than one design with a mode
-- bit. That is Chapter 16.7's argument applied to a design instead of an agent -- when a difference
-- matters, spend structure on it -- and it means no runtime condition can turn a hardened slave
-- back into a naive one.
--
-- The testbench instantiates both, so the two elaborations see identical pins.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_lite is
generic (
DW : positive := 32;
LEN_W : positive := 6;
-- false = the naive slave, true = the hardened one. A GENERIC, so the choice is made at
-- elaboration and cannot be changed by anything at run time.
HARD : boolean := false
);
port (
clk : in std_logic;
rst_n : in std_logic;
sclk : in std_logic;
cs_n : in std_logic;
mosi : in std_logic;
cpol : in std_logic;
cpha : in std_logic;
nbits : in unsigned(LEN_W - 1 downto 0);
rx_valid : out std_logic;
rx_data : out std_logic_vector(DW - 1 downto 0);
rx_nbits : out natural
);
end entity spi_slave_lite;
architecture rtl of spi_slave_lite is
signal v_r : std_logic := '0';
signal d_r : std_logic_vector(DW - 1 downto 0) := (others => '0');
signal k_r : natural := 0;
begin
rx_valid <= v_r;
rx_data <= d_r;
rx_nbits <= k_r;
process (clk, rst_n) is
variable sclk_d, cs_n_d : std_logic;
variable sh : std_logic_vector(DW - 1 downto 0);
variable k : natural;
variable n_latched : natural;
variable n_eff : natural;
variable idx : natural;
variable cs_assert : boolean;
variable cs_deassert : boolean;
variable in_txn : boolean;
variable sclk_edge : boolean;
variable leading : boolean;
variable capture : boolean;
variable k_final : natural;
variable d_final : std_logic_vector(DW - 1 downto 0);
begin
if rst_n = '0' then
sclk_d := '0';
cs_n_d := '1';
sh := (others => '0');
k := 0;
n_latched := 0;
v_r <= '0';
d_r <= (others => '0');
k_r <= 0;
elsif rising_edge(clk) then
v_r <= '0';
cs_assert := (cs_n = '0') and (cs_n_d = '1');
cs_deassert := (cs_n = '1') and (cs_n_d = '0');
in_txn := (cs_n = '0') or cs_deassert;
sclk_edge := (sclk /= sclk_d);
leading := sclk_edge and (sclk /= cpol);
if cpha = '0' then capture := leading and in_txn;
else capture := sclk_edge and (not leading) and in_txn;
end if;
-- THE ONE LINE THAT IS THE WHOLE OF CORNER CASE 3. The naive slave reads `nbits` as it
-- is NOW; the hardened one reads the value latched when the select fell. A width
-- reprogrammed between two frames of a burst reaches the naive slave in the middle of
-- a frame.
if HARD then n_eff := n_latched; else n_eff := to_integer(nbits); end if;
if cs_assert then
n_latched := to_integer(nbits);
k := 0;
-- THE SECOND DIFFERENCE. The hardened slave clears its shift register at every
-- select; the naive one clears it only at reset, so a frame aborted or cut short
-- leaves its bits in place and the NEXT frame starts contaminated.
if HARD then sh := (others => '0'); end if;
elsif capture then
if k < n_eff and n_eff > 0 then
idx := n_eff - 1 - k;
sh(idx) := mosi;
k := k + 1;
end if;
end if;
if cs_deassert then
-- The values AS THEY WILL BE once this cycle's capture is folded in: a master that
-- releases on the same cycle as its final capture edge would otherwise lose a bit.
if capture and k < n_eff and n_eff > 0 then
d_final := sh;
d_final(n_eff - 1 - k) := mosi;
k_final := k + 1;
else
d_final := sh;
k_final := k;
end if;
d_r <= d_final;
k_r <= k_final;
-- THE THIRD DIFFERENCE, and the dangerous one. The naive slave announces a word on
-- ANY deassert. The hardened one announces only a frame that reached its expected
-- bit count, so an aborted frame produces silence rather than a garbage word that
-- is indistinguishable from a real one.
if HARD then
if k_final = n_eff then v_r <= '1'; else v_r <= '0'; end if;
else
v_r <= '1';
end if;
k := 0;
end if;
sclk_d := sclk;
cs_n_d := cs_n;
end if;
end process;
end architecture rtl;The Bench
// spi_slave_lite_tb.sv
//
// SIX CORNER CASES, TWO SLAVES, AND A TABLE WITH BOTH POLARITIES IN IT.
//
// Three of the six cases are LEGAL and a correct slave must handle them silently:
//
// C4 a half period at the minimum
// C5 a gap at the minimum
// C6 a one-bit frame
//
// Three are abnormal events a real system produces anyway:
//
// C1 reset asserted in the middle of a frame
// C2 chip select released in the middle of a frame
// C3 the frame width reprogrammed between two frames of a burst
//
// Both slaves see identical pins. The measurement is, for each case, whether the received word
// matches what was sent and whether a word was announced at all -- and the table is only useful
// because it has both polarities: a slave that reports nothing on C1 to C3 is broken, and a slave
// that reports a problem on C4 to C6 is broken in the other direction. A suite that only measures
// failures cannot tell a robust design from a paranoid one.
//
// AND THE FINDING THAT MAKES C1 AND C2 WORTH THEIR OWN CASES is about the CHECK rather than the
// design. The naive slave's offence is not a wrong word -- the clean frame that follows overwrites
// the contaminated bits, so every payload compares equal. Its offence is announcing the aborted
// frame as a word at all: two words where one transaction completed, and a downstream consumer has
// no way to tell the extra one from a real one. An earlier version of this bench compared only
// payloads and both slaves passed.
`timescale 1ns/1ps
module spi_slave_lite_tb;
localparam int LEAD = 4;
localparam int HALF = 3;
localparam int LAG = 2;
localparam int GAP = 3;
localparam int DW = 32;
localparam int LEN_W = 6;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b1;
reg [LEN_W-1:0] nbits = 6'd8;
reg cpol = 1'b0, cpha = 1'b0;
// The bench drives the pins directly. Every corner case here is a statement about the SHAPE
// of the traffic, and a driver with a fault input reaches the shapes its faults were written
// for and no others.
reg b_sclk = 1'b0, b_cs_n = 1'b1, b_mosi = 1'b0;
wire n_valid, h_valid;
wire [DW-1:0] n_data, h_data;
wire [LEN_W:0] n_bits, h_bits;
spi_slave_lite #(.DW(DW), .LEN_W(LEN_W)) u_naive (
.clk(clk), .rst_n(rst_n), .hard(1'b0),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.cpol(cpol), .cpha(cpha), .nbits(nbits),
.rx_valid(n_valid), .rx_data(n_data), .rx_nbits(n_bits)
);
spi_slave_lite #(.DW(DW), .LEN_W(LEN_W)) u_hard (
.clk(clk), .rst_n(rst_n), .hard(1'b1),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.cpol(cpol), .cpha(cpha), .nbits(nbits),
.rx_valid(h_valid), .rx_data(h_data), .rx_nbits(h_bits)
);
integer errors = 0;
initial begin
#400_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
// ------------------------------------------------------------------
// Observation. Every announced word is recorded per slave, so the checks can ask about the
// frame AFTER the corner case as well as the frame it happened on.
// ------------------------------------------------------------------
integer n_words, h_words;
reg [DW-1:0] n_log [0:15];
reg [DW-1:0] h_log [0:15];
reg [LEN_W:0] n_blog [0:15];
reg [LEN_W:0] h_blog [0:15];
always @(posedge clk) if (rst_n) begin
if (n_valid) begin
if (n_words < 16) begin n_log[n_words] = n_data; n_blog[n_words] = n_bits; end
n_words = n_words + 1;
end
if (h_valid) begin
if (h_words < 16) begin h_log[h_words] = h_data; h_blog[h_words] = h_bits; end
h_words = h_words + 1;
end
end
task automatic zero_logs;
begin
@(negedge clk);
n_words = 0; h_words = 0;
end
endtask
task automatic idle_n(input integer n);
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// One frame of `n` bits carrying `word`, with a half period of `h`, optionally aborted after
// `abort_at` bits, and optionally with a reset pulse after `reset_at` bits.
task automatic frame(input [DW-1:0] word, input integer n, input integer h,
input integer abort_at, input integer reset_at, input integer gap_c);
integer bit_i;
begin
b_cs_n = 1'b0;
idle_n(LEAD);
for (bit_i = 0; bit_i < n; bit_i = bit_i + 1) begin
// CPHA=0: the bit is on the pin before the leading edge, which captures it.
b_mosi = word[n - 1 - bit_i];
idle_n(1);
b_sclk = ~b_sclk; // leading edge -- the capture
idle_n(h);
b_sclk = ~b_sclk; // trailing edge
idle_n(h - 1);
if (abort_at == bit_i + 1) begin
b_cs_n = 1'b1; // released mid-frame
b_sclk = cpol;
idle_n(gap_c + 4);
bit_i = n; // stop
end
if (reset_at == bit_i + 1) begin
rst_n = 1'b0; // reset mid-frame
idle_n(3);
rst_n = 1'b1;
idle_n(3);
b_cs_n = 1'b1;
b_sclk = cpol;
idle_n(gap_c + 4);
bit_i = n;
end
end
if (b_cs_n == 1'b0) begin
idle_n(LAG);
b_cs_n = 1'b1;
idle_n(gap_c + 4);
end
end
endtask
integer c, caught_n, caught_h;
reg [4:0] nfail, hfail; // one bit per corner case
integer legal_noise_n, legal_noise_h;
// Case 1 and 2 need TWO frames: the aborted one and the one after it, because the naive
// slave's contamination shows up on the second.
task automatic run_case(input integer which);
begin
zero_logs();
case (which)
// C1 -- reset in the middle of a frame, then a clean frame.
1: begin
nbits = 6'd8;
frame(32'h00A5, 8, HALF, 0, 4, GAP);
frame(32'h003C, 8, HALF, 0, 0, GAP);
end
// C2 -- select released in the middle of a frame, then a clean frame.
2: begin
nbits = 6'd8;
frame(32'h00A5, 8, HALF, 4, 0, GAP);
frame(32'h003C, 8, HALF, 0, 0, GAP);
end
// C3 -- the width reprogrammed between two frames of a burst. The change is made
// while the bus is idle, which is the legal moment to make it -- and the naive
// slave still applies it to a frame already in progress, because it never
// latched the old one.
3: begin
nbits = 6'd8;
frame(32'h00A5, 8, HALF, 0, 0, GAP);
nbits = 6'd4;
frame(32'h0009, 4, HALF, 0, 0, GAP);
end
// C4 -- a half period at the minimum. LEGAL.
4: begin
nbits = 6'd8;
frame(32'h005A, 8, 1, 0, 0, GAP);
end
// C5 -- a gap at the minimum. LEGAL.
5: begin
nbits = 6'd8;
frame(32'h0066, 8, HALF, 0, 0, 1);
frame(32'h0099, 8, HALF, 0, 0, 1);
end
// C6 -- a one-bit frame. LEGAL.
default: begin
nbits = 6'd1;
frame(32'h0001, 1, HALF, 0, 0, GAP);
end
endcase
end
endtask
initial begin
n_words = 0; h_words = 0;
nfail = 5'd0; hfail = 5'd0;
legal_noise_n = 0; legal_noise_h = 0;
rst_n = 1'b1;
@(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (4) @(negedge clk);
$display(" case naive slave hardened slave");
// ---------------- C1: reset mid-frame ----------------
run_case(1);
// THE CHECK IS ON THE WORD COUNT, not on the word.
//
// An earlier version of this bench checked only the last word, and both slaves passed --
// because the clean frame that follows overwrites the contaminated bits. The naive slave's
// actual offence is that it announced the ABORTED frame as a word at all: two words where
// one transaction completed. A downstream consumer has no way to tell that extra word from
// a real one, and a check that only compares payloads never sees it.
caught_n = (n_words != 1) || (n_log[n_words-1] !== 32'h003C);
caught_h = (h_words != 1) || (h_log[h_words-1] !== 32'h003C);
$display(" C1 reset mid-frame words %0d, last %h %s words %0d, last %h %s",
n_words, (n_words > 0) ? n_log[n_words-1] : 32'hx, caught_n ? "WRONG" : "ok ",
h_words, (h_words > 0) ? h_log[h_words-1] : 32'hx, caught_h ? "WRONG" : "ok");
if (!caught_n) begin
$display(" FAIL: the naive slave survived a reset mid-frame; it is supposed to announce the interrupted frame as a word");
errors = errors + 1;
end
if (caught_h) begin
$display(" FAIL: the hardened slave got the frame AFTER a mid-frame reset wrong");
errors = errors + 1;
end
// ---------------- C2: CS released mid-frame ----------------
run_case(2);
caught_n = (n_words != 1) || (n_log[n_words-1] !== 32'h003C);
caught_h = (h_words != 1) || (h_log[h_words-1] !== 32'h003C);
$display(" C2 select released mid-frame words %0d, last %h %s words %0d, last %h %s",
n_words, n_log[n_words-1], caught_n ? "WRONG" : "ok ",
h_words, (h_words > 0) ? h_log[h_words-1] : 32'hx, caught_h ? "WRONG" : "ok");
if (!caught_n) begin
$display(" FAIL: the naive slave survived a mid-frame release; it is supposed to announce the aborted frame as a word");
errors = errors + 1;
end
if (caught_h) begin
$display(" FAIL: the hardened slave did not deliver exactly one correct word (got %0d words, last %h)",
h_words, (h_words > 0) ? h_log[h_words-1] : 32'hx);
errors = errors + 1;
end
// ---------------- C3: width reprogrammed mid-burst ----------------
run_case(3);
caught_n = (n_words < 2) || (n_log[1] !== 32'h0009);
caught_h = (h_words < 2) || (h_log[1] !== 32'h0009);
$display(" C3 width reprogrammed between frames words %0d, last %h %s words %0d, last %h %s",
n_words, (n_words > 1) ? n_log[1] : 32'hx, caught_n ? "WRONG" : "ok ",
h_words, (h_words > 1) ? h_log[1] : 32'hx, caught_h ? "WRONG" : "ok");
if (caught_h) begin
$display(" FAIL: the hardened slave got the reprogrammed-width frame wrong (%h)",
(h_words > 1) ? h_log[1] : 32'hx);
errors = errors + 1;
end
// ---------------- C4, C5, C6: the LEGAL extremes ----------------
run_case(4);
legal_noise_n = legal_noise_n + ((n_words != 1 || n_log[0] !== 32'h005A) ? 1 : 0);
legal_noise_h = legal_noise_h + ((h_words != 1 || h_log[0] !== 32'h005A) ? 1 : 0);
$display(" C4 half period at the minimum (LEGAL) words %0d, last %h words %0d, last %h",
n_words, (n_words > 0) ? n_log[0] : 32'hx, h_words, (h_words > 0) ? h_log[0] : 32'hx);
run_case(5);
legal_noise_n = legal_noise_n + ((n_words != 2 || n_log[1] !== 32'h0099) ? 1 : 0);
legal_noise_h = legal_noise_h + ((h_words != 2 || h_log[1] !== 32'h0099) ? 1 : 0);
$display(" C5 gap at the minimum (LEGAL) words %0d, last %h words %0d, last %h",
n_words, (n_words > 1) ? n_log[1] : 32'hx, h_words, (h_words > 1) ? h_log[1] : 32'hx);
run_case(6);
legal_noise_n = legal_noise_n + ((n_words != 1 || n_log[0] !== 32'h0001) ? 1 : 0);
legal_noise_h = legal_noise_h + ((h_words != 1 || h_log[0] !== 32'h0001) ? 1 : 0);
$display(" C6 one-bit frame (LEGAL) words %0d, last %h words %0d, last %h",
n_words, (n_words > 0) ? n_log[0] : 32'hx, h_words, (h_words > 0) ? h_log[0] : 32'hx);
if (legal_noise_h != 0) begin
$display(" FAIL: the hardened slave mishandled %0d of the three LEGAL extremes; a corner case that is legal must be silent",
legal_noise_h);
errors = errors + 1;
end
$display(" 1. the three LEGAL extremes -- a minimum half period, a minimum gap, and a one-bit frame -- were handled correctly by the hardened slave with no report of any kind, and the naive slave mishandled %0d of them. That polarity is half the measurement: a table containing only failures cannot tell a robust design from a paranoid one, and `we tested the corner cases` means nothing until the legal ones are known to be quiet",
legal_noise_n);
$display(" 2. the three abnormal events all broke the naive slave and none of them broke the hardened one, and the three differences between the two are not new logic: the width is LATCHED at the select instead of read continuously, `rx_valid` is qualified by the expected bit count instead of raised on any deassert, and the shift register is cleared at every select instead of only at reset. Corner-case robustness in a protocol slave is almost always a decision about WHEN a value is sampled or WHETHER an output is qualified");
$display(" 3. and C1 and C2 are a lesson about the CHECK rather than the design. Every payload in those two cases compares EQUAL, because the clean frame that follows overwrites the contaminated bits -- an earlier version of this bench compared only words and both slaves passed. The offence is that the naive slave announced the aborted frame as a word at all: two words where one transaction completed, indistinguishable downstream from real traffic. Counting the announcements is what finds it");
if (errors == 0)
$display("PASS: a corner case is not the same thing as an illegal stimulus, and a table that contains only failures cannot tell a robust design from a paranoid one. Three of the six cases here are perfectly LEGAL -- a half period at the minimum, a gap at the minimum, and a one-bit frame -- and the hardened slave handled all three correctly and silently while the naive one mishandled %0d. The other three are abnormal events a real system produces anyway: a reset mid-frame, a select released mid-frame, and a frame width reprogrammed between two frames of a burst. All three broke the naive slave and none broke the hardened one -- and the three differences between the two designs are not new logic. The width is LATCHED at the select instead of read continuously; `rx_valid` is qualified by the expected bit count instead of raised on any deassert; and the shift register is cleared at every select instead of only at reset. Corner-case robustness in a protocol slave is almost always a decision about WHEN a value is sampled or WHETHER an output is qualified. And the two abort cases carry a lesson about the CHECK rather than the design: every payload in them compares EQUAL, because the clean frame that follows overwrites the contaminated bits, and an earlier version of this bench compared only words and passed both slaves. The offence is that the naive slave announced the aborted frame as a word at all -- two words where one transaction completed, indistinguishable downstream from real traffic -- so the measurement that finds it is a count of announcements, not a comparison of payloads",
legal_noise_n);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_slave_lite_tb.v
//
// SIX CORNER CASES, TWO SLAVES, AND A TABLE WITH BOTH POLARITIES IN IT.
//
// Three of the six cases are LEGAL and a correct slave must handle them silently:
//
// C4 a half period at the minimum
// C5 a gap at the minimum
// C6 a one-bit frame
//
// Three are abnormal events a real system produces anyway:
//
// C1 reset asserted in the middle of a frame
// C2 chip select released in the middle of a frame
// C3 the frame width reprogrammed between two frames of a burst
//
// Both slaves see identical pins. The measurement is, for each case, whether the received word
// matches what was sent and whether a word was announced at all -- and the table is only useful
// because it has both polarities: a slave that reports nothing on C1 to C3 is broken, and a slave
// that reports a problem on C4 to C6 is broken in the other direction. A suite that only measures
// failures cannot tell a robust design from a paranoid one.
//
// AND THE FINDING THAT MAKES C1 AND C2 WORTH THEIR OWN CASES is about the CHECK rather than the
// design. The naive slave's offence is not a wrong word -- the clean frame that follows overwrites
// the contaminated bits, so every payload compares equal. Its offence is announcing the aborted
// frame as a word at all: two words where one transaction completed, and a downstream consumer has
// no way to tell the extra one from a real one. An earlier version of this bench compared only
// payloads and both slaves passed.
`timescale 1ns/1ps
module spi_slave_lite_tb;
localparam LEAD = 4;
localparam HALF = 3;
localparam LAG = 2;
localparam GAP = 3;
localparam DW = 32;
localparam LEN_W = 6;
reg clk;
always #5 clk = ~clk;
reg rst_n;
reg [LEN_W-1:0] nbits;
reg cpol, cpha;
// The bench drives the pins directly. Every corner case here is a statement about the SHAPE
// of the traffic, and a driver with a fault input reaches the shapes its faults were written
// for and no others.
reg b_sclk, b_cs_n, b_mosi;
wire n_valid, h_valid;
wire [DW-1:0] n_data, h_data;
wire [LEN_W:0] n_bits, h_bits;
spi_slave_lite #(.DW(DW), .LEN_W(LEN_W)) u_naive (
.clk(clk), .rst_n(rst_n), .hard(1'b0),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.cpol(cpol), .cpha(cpha), .nbits(nbits),
.rx_valid(n_valid), .rx_data(n_data), .rx_nbits(n_bits)
);
spi_slave_lite #(.DW(DW), .LEN_W(LEN_W)) u_hard (
.clk(clk), .rst_n(rst_n), .hard(1'b1),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.cpol(cpol), .cpha(cpha), .nbits(nbits),
.rx_valid(h_valid), .rx_data(h_data), .rx_nbits(h_bits)
);
integer errors;
initial begin
#400_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
// ------------------------------------------------------------------
// Observation. Every announced word is recorded per slave, so the checks can ask about the
// frame AFTER the corner case as well as the frame it happened on.
// ------------------------------------------------------------------
integer n_words, h_words;
reg [DW-1:0] n_log [0:15];
reg [DW-1:0] h_log [0:15];
reg [LEN_W:0] n_blog [0:15];
reg [LEN_W:0] h_blog [0:15];
always @(posedge clk) if (rst_n) begin
if (n_valid) begin
if (n_words < 16) begin n_log[n_words] = n_data; n_blog[n_words] = n_bits; end
n_words = n_words + 1;
end
if (h_valid) begin
if (h_words < 16) begin h_log[h_words] = h_data; h_blog[h_words] = h_bits; end
h_words = h_words + 1;
end
end
task zero_logs;
begin
@(negedge clk);
n_words = 0; h_words = 0;
end
endtask
task idle_n;
input integer n;
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// One frame of `n` bits carrying `word`, with a half period of `h`, optionally aborted after
// `abort_at` bits, and optionally with a reset pulse after `reset_at` bits.
task frame;
input [DW-1:0] word;
input integer n;
input integer h;
input integer abort_at;
input integer reset_at;
input integer gap_c;
integer bit_i;
begin
b_cs_n = 1'b0;
idle_n(LEAD);
for (bit_i = 0; bit_i < n; bit_i = bit_i + 1) begin
// CPHA=0: the bit is on the pin before the leading edge, which captures it.
b_mosi = word[n - 1 - bit_i];
idle_n(1);
b_sclk = ~b_sclk; // leading edge -- the capture
idle_n(h);
b_sclk = ~b_sclk; // trailing edge
idle_n(h - 1);
if (abort_at == bit_i + 1) begin
b_cs_n = 1'b1; // released mid-frame
b_sclk = cpol;
idle_n(gap_c + 4);
bit_i = n; // stop
end
if (reset_at == bit_i + 1) begin
rst_n = 1'b0; // reset mid-frame
idle_n(3);
rst_n = 1'b1;
idle_n(3);
b_cs_n = 1'b1;
b_sclk = cpol;
idle_n(gap_c + 4);
bit_i = n;
end
end
if (b_cs_n == 1'b0) begin
idle_n(LAG);
b_cs_n = 1'b1;
idle_n(gap_c + 4);
end
end
endtask
integer c, caught_n, caught_h;
reg [4:0] nfail, hfail; // one bit per corner case
integer legal_noise_n, legal_noise_h;
// Case 1 and 2 need TWO frames: the aborted one and the one after it, because the naive
// slave's contamination shows up on the second.
task run_case;
input integer which;
begin
zero_logs();
case (which)
// C1 -- reset in the middle of a frame, then a clean frame.
1: begin
nbits = 6'd8;
frame(32'h00A5, 8, HALF, 0, 4, GAP);
frame(32'h003C, 8, HALF, 0, 0, GAP);
end
// C2 -- select released in the middle of a frame, then a clean frame.
2: begin
nbits = 6'd8;
frame(32'h00A5, 8, HALF, 4, 0, GAP);
frame(32'h003C, 8, HALF, 0, 0, GAP);
end
// C3 -- the width reprogrammed between two frames of a burst. The change is made
// while the bus is idle, which is the legal moment to make it -- and the naive
// slave still applies it to a frame already in progress, because it never
// latched the old one.
3: begin
nbits = 6'd8;
frame(32'h00A5, 8, HALF, 0, 0, GAP);
nbits = 6'd4;
frame(32'h0009, 4, HALF, 0, 0, GAP);
end
// C4 -- a half period at the minimum. LEGAL.
4: begin
nbits = 6'd8;
frame(32'h005A, 8, 1, 0, 0, GAP);
end
// C5 -- a gap at the minimum. LEGAL.
5: begin
nbits = 6'd8;
frame(32'h0066, 8, HALF, 0, 0, 1);
frame(32'h0099, 8, HALF, 0, 0, 1);
end
// C6 -- a one-bit frame. LEGAL.
default: begin
nbits = 6'd1;
frame(32'h0001, 1, HALF, 0, 0, GAP);
end
endcase
end
endtask
initial begin
n_words = 0; h_words = 0;
nfail = 5'd0; hfail = 5'd0;
legal_noise_n = 0; legal_noise_h = 0;
rst_n = 1'b1;
@(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (4) @(negedge clk);
$display(" case naive slave hardened slave");
// ---------------- C1: reset mid-frame ----------------
run_case(1);
// THE CHECK IS ON THE WORD COUNT, not on the word.
//
// An earlier version of this bench checked only the last word, and both slaves passed --
// because the clean frame that follows overwrites the contaminated bits. The naive slave's
// actual offence is that it announced the ABORTED frame as a word at all: two words where
// one transaction completed. A downstream consumer has no way to tell that extra word from
// a real one, and a check that only compares payloads never sees it.
caught_n = (n_words != 1) || (n_log[n_words-1] !== 32'h003C);
caught_h = (h_words != 1) || (h_log[h_words-1] !== 32'h003C);
$display(" C1 reset mid-frame words %0d, last %h %0s words %0d, last %h %0s",
n_words, (n_words > 0) ? n_log[n_words-1] : 32'hx, caught_n ? "WRONG" : "ok ",
h_words, (h_words > 0) ? h_log[h_words-1] : 32'hx, caught_h ? "WRONG" : "ok");
if (!caught_n) begin
$display(" FAIL: the naive slave survived a reset mid-frame; it is supposed to announce the interrupted frame as a word");
errors = errors + 1;
end
if (caught_h) begin
$display(" FAIL: the hardened slave got the frame AFTER a mid-frame reset wrong");
errors = errors + 1;
end
// ---------------- C2: CS released mid-frame ----------------
run_case(2);
caught_n = (n_words != 1) || (n_log[n_words-1] !== 32'h003C);
caught_h = (h_words != 1) || (h_log[h_words-1] !== 32'h003C);
$display(" C2 select released mid-frame words %0d, last %h %0s words %0d, last %h %0s",
n_words, n_log[n_words-1], caught_n ? "WRONG" : "ok ",
h_words, (h_words > 0) ? h_log[h_words-1] : 32'hx, caught_h ? "WRONG" : "ok");
if (!caught_n) begin
$display(" FAIL: the naive slave survived a mid-frame release; it is supposed to announce the aborted frame as a word");
errors = errors + 1;
end
if (caught_h) begin
$display(" FAIL: the hardened slave did not deliver exactly one correct word (got %0d words, last %h)",
h_words, (h_words > 0) ? h_log[h_words-1] : 32'hx);
errors = errors + 1;
end
// ---------------- C3: width reprogrammed mid-burst ----------------
run_case(3);
caught_n = (n_words < 2) || (n_log[1] !== 32'h0009);
caught_h = (h_words < 2) || (h_log[1] !== 32'h0009);
$display(" C3 width reprogrammed between frames words %0d, last %h %0s words %0d, last %h %0s",
n_words, (n_words > 1) ? n_log[1] : 32'hx, caught_n ? "WRONG" : "ok ",
h_words, (h_words > 1) ? h_log[1] : 32'hx, caught_h ? "WRONG" : "ok");
if (caught_h) begin
$display(" FAIL: the hardened slave got the reprogrammed-width frame wrong (%h)",
(h_words > 1) ? h_log[1] : 32'hx);
errors = errors + 1;
end
// ---------------- C4, C5, C6: the LEGAL extremes ----------------
run_case(4);
legal_noise_n = legal_noise_n + ((n_words != 1 || n_log[0] !== 32'h005A) ? 1 : 0);
legal_noise_h = legal_noise_h + ((h_words != 1 || h_log[0] !== 32'h005A) ? 1 : 0);
$display(" C4 half period at the minimum (LEGAL) words %0d, last %h words %0d, last %h",
n_words, (n_words > 0) ? n_log[0] : 32'hx, h_words, (h_words > 0) ? h_log[0] : 32'hx);
run_case(5);
legal_noise_n = legal_noise_n + ((n_words != 2 || n_log[1] !== 32'h0099) ? 1 : 0);
legal_noise_h = legal_noise_h + ((h_words != 2 || h_log[1] !== 32'h0099) ? 1 : 0);
$display(" C5 gap at the minimum (LEGAL) words %0d, last %h words %0d, last %h",
n_words, (n_words > 1) ? n_log[1] : 32'hx, h_words, (h_words > 1) ? h_log[1] : 32'hx);
run_case(6);
legal_noise_n = legal_noise_n + ((n_words != 1 || n_log[0] !== 32'h0001) ? 1 : 0);
legal_noise_h = legal_noise_h + ((h_words != 1 || h_log[0] !== 32'h0001) ? 1 : 0);
$display(" C6 one-bit frame (LEGAL) words %0d, last %h words %0d, last %h",
n_words, (n_words > 0) ? n_log[0] : 32'hx, h_words, (h_words > 0) ? h_log[0] : 32'hx);
if (legal_noise_h != 0) begin
$display(" FAIL: the hardened slave mishandled %0d of the three LEGAL extremes; a corner case that is legal must be silent",
legal_noise_h);
errors = errors + 1;
end
$display(" 1. the three LEGAL extremes -- a minimum half period, a minimum gap, and a one-bit frame -- were handled correctly by the hardened slave with no report of any kind, and the naive slave mishandled %0d of them. That polarity is half the measurement: a table containing only failures cannot tell a robust design from a paranoid one, and `we tested the corner cases` means nothing until the legal ones are known to be quiet",
legal_noise_n);
$display(" 2. the three abnormal events all broke the naive slave and none of them broke the hardened one, and the three differences between the two are not new logic: the width is LATCHED at the select instead of read continuously, `rx_valid` is qualified by the expected bit count instead of raised on any deassert, and the shift register is cleared at every select instead of only at reset. Corner-case robustness in a protocol slave is almost always a decision about WHEN a value is sampled or WHETHER an output is qualified");
$display(" 3. and C1 and C2 are a lesson about the CHECK rather than the design. Every payload in those two cases compares EQUAL, because the clean frame that follows overwrites the contaminated bits -- an earlier version of this bench compared only words and both slaves passed. The offence is that the naive slave announced the aborted frame as a word at all: two words where one transaction completed, indistinguishable downstream from real traffic. Counting the announcements is what finds it");
if (errors == 0)
$display("PASS: a corner case is not the same thing as an illegal stimulus, and a table that contains only failures cannot tell a robust design from a paranoid one. Three of the six cases here are perfectly LEGAL -- a half period at the minimum, a gap at the minimum, and a one-bit frame -- and the hardened slave handled all three correctly and silently while the naive one mishandled %0d. The other three are abnormal events a real system produces anyway: a reset mid-frame, a select released mid-frame, and a frame width reprogrammed between two frames of a burst. All three broke the naive slave and none broke the hardened one -- and the three differences between the two designs are not new logic. The width is LATCHED at the select instead of read continuously; `rx_valid` is qualified by the expected bit count instead of raised on any deassert; and the shift register is cleared at every select instead of only at reset. Corner-case robustness in a protocol slave is almost always a decision about WHEN a value is sampled or WHETHER an output is qualified. And the two abort cases carry a lesson about the CHECK rather than the design: every payload in them compares EQUAL, because the clean frame that follows overwrites the contaminated bits, and an earlier version of this bench compared only words and passed both slaves. The offence is that the naive slave announced the aborted frame as a word at all -- two words where one transaction completed, indistinguishable downstream from real traffic -- so the measurement that finds it is a count of announcements, not a comparison of payloads",
legal_noise_n);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
cpol = 1'b0;
cpha = 1'b0;
b_sclk = 1'b0;
b_cs_n = 1'b1;
b_mosi = 1'b0;
clk = 1'b0;
rst_n = 1'b1;
nbits = 6'd8;
errors = 0;
end
endmodule-- spi_slave_lite_tb.vhd
--
-- SIX CORNER CASES, TWO SLAVES, AND A TABLE WITH BOTH POLARITIES IN IT.
--
-- Three of the six cases are LEGAL and a correct slave must handle them silently:
--
-- C4 a half period at the minimum
-- C5 a gap at the minimum
-- C6 a one-bit frame
--
-- Three are abnormal events a real system produces anyway:
--
-- C1 reset asserted in the middle of a frame
-- C2 chip select released in the middle of a frame
-- C3 the frame width reprogrammed between two frames of a burst
--
-- Both slaves see identical pins. The measurement is, for each case, whether the received word
-- matches what was sent and whether a word was announced at all -- and the table is only useful
-- because it has both polarities: a slave that reports nothing on C1 to C3 is broken, and a slave
-- that reports a problem on C4 to C6 is broken in the other direction. A suite that only measures
-- failures cannot tell a robust design from a paranoid one.
--
-- AND THE FINDING THAT MAKES C1 AND C2 WORTH THEIR OWN CASES is about the CHECK rather than the
-- design. The naive slave's offence is not a wrong word -- the clean frame that follows overwrites
-- the contaminated bits, so every payload compares equal. Its offence is announcing the aborted
-- frame as a word at all: two words where one transaction completed, and a downstream consumer has
-- no way to tell the extra one from a real one. An earlier version of this bench compared only
-- payloads and both slaves passed.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_lite_tb is
end entity spi_slave_lite_tb;
architecture tb of spi_slave_lite_tb is
constant LEAD_C : natural := 4;
constant HALF_C : natural := 3;
constant LAG_C : natural := 2;
constant GAP_C : natural := 3;
constant DW : positive := 32;
constant LEN_W : positive := 6;
constant HALF_T : time := 5 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal done_sim : boolean := false;
signal nbits : unsigned(LEN_W - 1 downto 0) := to_unsigned(8, LEN_W);
signal cpol : std_logic := '0';
signal cpha : std_logic := '0';
-- The bench drives the pins directly. Every corner case here is a statement about the SHAPE of
-- the traffic, and a driver with a fault input reaches the shapes its faults were written for
-- and no others.
signal b_sclk : std_logic := '0';
signal b_cs_n : std_logic := '1';
signal b_mosi : std_logic := '0';
signal n_valid, h_valid : std_logic;
signal n_data, h_data : std_logic_vector(DW - 1 downto 0);
signal n_bits, h_bits : natural;
-- Observation. Every announced word is recorded per slave, so the checks can ask about the
-- COUNT of announcements as well as their contents.
type log_t is array (0 to 15) of std_logic_vector(DW - 1 downto 0);
signal n_log, h_log : log_t := (others => (others => '0'));
signal n_words, h_words : natural := 0;
signal log_clr : boolean := false;
signal errors : integer := 0;
begin
clk_gen : process is
begin
while not done_sim loop
wait for HALF_T;
clk <= not clk;
end loop;
wait;
end process clk_gen;
u_naive : entity work.spi_slave_lite
generic map (DW => DW, LEN_W => LEN_W, HARD => false)
port map (clk => clk, rst_n => rst_n,
sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
cpol => cpol, cpha => cpha, nbits => nbits,
rx_valid => n_valid, rx_data => n_data, rx_nbits => n_bits);
u_hard : entity work.spi_slave_lite
generic map (DW => DW, LEN_W => LEN_W, HARD => true)
port map (clk => clk, rst_n => rst_n,
sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
cpol => cpol, cpha => cpha, nbits => nbits,
rx_valid => h_valid, rx_data => h_data, rx_nbits => h_bits);
observe : process (clk) is
begin
if rising_edge(clk) then
if log_clr then
n_words <= 0;
h_words <= 0;
else
if n_valid = '1' then
if n_words < 16 then n_log(n_words) <= n_data; end if;
n_words <= n_words + 1;
end if;
if h_valid = '1' then
if h_words < 16 then h_log(h_words) <= h_data; end if;
h_words <= h_words + 1;
end if;
end if;
end if;
end process observe;
main : process is
procedure idle_n (n : natural) is
begin
for i in 1 to n loop wait until falling_edge(clk); end loop;
end procedure idle_n;
procedure zero_logs is
begin
wait until falling_edge(clk);
log_clr <= true;
wait until falling_edge(clk);
log_clr <= false;
wait until falling_edge(clk);
end procedure zero_logs;
-- One frame of `n` bits carrying `word`, with a half period of `h`, optionally aborted
-- after `abort_at` bits, and optionally with a reset pulse after `reset_at` bits.
procedure frame (word : natural; n : natural; h : natural;
abort_at : natural; reset_at : natural; gap_c : natural) is
variable w : std_logic_vector(DW - 1 downto 0);
variable stop : boolean := false;
begin
w := std_logic_vector(to_unsigned(word, DW));
stop := false;
b_cs_n <= '0';
idle_n(LEAD_C);
for bit_i in 0 to n - 1 loop
if not stop then
-- CPHA=0: the bit is on the pin before the leading edge, which captures it.
b_mosi <= w(n - 1 - bit_i);
idle_n(1);
b_sclk <= not b_sclk; -- leading edge -- the capture
idle_n(h);
b_sclk <= not b_sclk; -- trailing edge
idle_n(h - 1);
if abort_at = bit_i + 1 then
b_cs_n <= '1'; -- released mid-frame
b_sclk <= cpol;
idle_n(gap_c + 4);
stop := true;
end if;
if reset_at = bit_i + 1 then
rst_n <= '0'; -- reset mid-frame
idle_n(3);
rst_n <= '1';
idle_n(3);
b_cs_n <= '1';
b_sclk <= cpol;
idle_n(gap_c + 4);
stop := true;
end if;
end if;
end loop;
if not stop then
idle_n(LAG_C);
b_cs_n <= '1';
idle_n(gap_c + 4);
end if;
end procedure frame;
variable caught_n, caught_h : boolean;
variable legal_noise_n : natural := 0;
variable legal_noise_h : natural := 0;
function yn (b : boolean) return string is
begin
if b then return "WRONG"; else return "ok "; end if;
end function yn;
begin
rst_n <= '1';
idle_n(1);
rst_n <= '0';
idle_n(4);
rst_n <= '1';
idle_n(4);
report " case naive slave hardened slave";
-- ---------------- C1: reset mid-frame ----------------
zero_logs;
nbits <= to_unsigned(8, LEN_W);
frame(16#A5#, 8, HALF_C, 0, 4, GAP_C);
frame(16#3C#, 8, HALF_C, 0, 0, GAP_C);
-- THE CHECK IS ON THE WORD COUNT, not on the word. An earlier version of this bench checked
-- only the last word and both slaves passed, because the clean frame that follows overwrites
-- the contaminated bits. The naive slave's actual offence is announcing the INTERRUPTED
-- frame as a word: two words where one transaction completed.
caught_n := (n_words /= 1) or (n_log(n_words - 1) /= std_logic_vector(to_unsigned(16#3C#, DW)));
caught_h := (h_words /= 1) or (h_log(h_words - 1) /= std_logic_vector(to_unsigned(16#3C#, DW)));
report " C1 reset mid-frame words " & integer'image(n_words) &
" " & yn(caught_n) & " words " & integer'image(h_words) & " " & yn(caught_h);
if not caught_n then
report " FAIL: the naive slave survived a reset mid-frame; it is supposed to announce the interrupted frame as a word";
errors <= errors + 1; wait for 1 ns;
end if;
if caught_h then
report " FAIL: the hardened slave got the frame AFTER a mid-frame reset wrong";
errors <= errors + 1; wait for 1 ns;
end if;
-- ---------------- C2: CS released mid-frame ----------------
zero_logs;
nbits <= to_unsigned(8, LEN_W);
frame(16#A5#, 8, HALF_C, 4, 0, GAP_C);
frame(16#3C#, 8, HALF_C, 0, 0, GAP_C);
caught_n := (n_words /= 1) or (n_log(n_words - 1) /= std_logic_vector(to_unsigned(16#3C#, DW)));
caught_h := (h_words /= 1) or (h_log(h_words - 1) /= std_logic_vector(to_unsigned(16#3C#, DW)));
report " C2 select released mid-frame words " & integer'image(n_words) &
" " & yn(caught_n) & " words " & integer'image(h_words) & " " & yn(caught_h);
if not caught_n then
report " FAIL: the naive slave survived a mid-frame release; it is supposed to announce the aborted frame as a word";
errors <= errors + 1; wait for 1 ns;
end if;
if caught_h then
report " FAIL: the hardened slave did not deliver exactly one correct word";
errors <= errors + 1; wait for 1 ns;
end if;
-- ---------------- C3: width reprogrammed mid-burst ----------------
zero_logs;
nbits <= to_unsigned(8, LEN_W);
frame(16#A5#, 8, HALF_C, 0, 0, GAP_C);
-- The change is made while the bus is idle, which is the legal moment to make it -- and the
-- naive slave still applies it to a frame already in progress, because it never latched the
-- old one.
nbits <= to_unsigned(4, LEN_W);
frame(16#9#, 4, HALF_C, 0, 0, GAP_C);
caught_n := (n_words < 2) or (n_log(1) /= std_logic_vector(to_unsigned(16#9#, DW)));
caught_h := (h_words < 2) or (h_log(1) /= std_logic_vector(to_unsigned(16#9#, DW)));
report " C3 width reprogrammed between frames words " & integer'image(n_words) &
" " & yn(caught_n) & " words " & integer'image(h_words) & " " & yn(caught_h);
if caught_h then
report " FAIL: the hardened slave got the reprogrammed-width frame wrong";
errors <= errors + 1; wait for 1 ns;
end if;
-- ---------------- C4, C5, C6: the LEGAL extremes ----------------
zero_logs;
nbits <= to_unsigned(8, LEN_W);
frame(16#5A#, 8, 1, 0, 0, GAP_C);
if n_words /= 1 or n_log(0) /= std_logic_vector(to_unsigned(16#5A#, DW)) then
legal_noise_n := legal_noise_n + 1;
end if;
if h_words /= 1 or h_log(0) /= std_logic_vector(to_unsigned(16#5A#, DW)) then
legal_noise_h := legal_noise_h + 1;
end if;
report " C4 half period at the minimum (LEGAL) words " & integer'image(n_words) &
" words " & integer'image(h_words);
zero_logs;
frame(16#66#, 8, HALF_C, 0, 0, 1);
frame(16#99#, 8, HALF_C, 0, 0, 1);
if n_words /= 2 or n_log(1) /= std_logic_vector(to_unsigned(16#99#, DW)) then
legal_noise_n := legal_noise_n + 1;
end if;
if h_words /= 2 or h_log(1) /= std_logic_vector(to_unsigned(16#99#, DW)) then
legal_noise_h := legal_noise_h + 1;
end if;
report " C5 gap at the minimum (LEGAL) words " & integer'image(n_words) &
" words " & integer'image(h_words);
zero_logs;
nbits <= to_unsigned(1, LEN_W);
frame(1, 1, HALF_C, 0, 0, GAP_C);
if n_words /= 1 or n_log(0) /= std_logic_vector(to_unsigned(1, DW)) then
legal_noise_n := legal_noise_n + 1;
end if;
if h_words /= 1 or h_log(0) /= std_logic_vector(to_unsigned(1, DW)) then
legal_noise_h := legal_noise_h + 1;
end if;
report " C6 one-bit frame (LEGAL) words " & integer'image(n_words) &
" words " & integer'image(h_words);
if legal_noise_h /= 0 then
report " FAIL: the hardened slave mishandled " & integer'image(legal_noise_h) &
" of the three LEGAL extremes; a corner case that is legal must be silent";
errors <= errors + 1; wait for 1 ns;
end if;
report " 1. the three LEGAL extremes -- a minimum half period, a minimum gap, and a one-bit frame -- were handled correctly by the hardened slave with no report of any kind, and the naive slave mishandled " &
integer'image(legal_noise_n) &
" of them. That polarity is half the measurement: a table containing only failures cannot tell a robust design from a paranoid one, and `we tested the corner cases` means nothing until the legal ones are known to be quiet";
report " 2. the three abnormal events all broke the naive slave and none of them broke the hardened one, and the three differences between the two are not new logic: the width is LATCHED at the select instead of read continuously, the valid is qualified by the expected bit count instead of raised on any deassert, and the shift register is cleared at every select instead of only at reset. Corner-case robustness in a protocol slave is almost always a decision about WHEN a value is sampled or WHETHER an output is qualified";
report " 3. and C1 and C2 are a lesson about the CHECK rather than the design. Every payload in those two cases compares EQUAL, because the clean frame that follows overwrites the contaminated bits -- an earlier version of this bench compared only words and both slaves passed. The offence is that the naive slave announced the aborted frame as a word at all: two words where one transaction completed, indistinguishable downstream from real traffic. Counting the announcements is what finds it";
wait for 1 ns;
if errors = 0 then
report "PASS: a corner case is not the same thing as an illegal stimulus, and a table that contains only failures cannot tell a robust design from a paranoid one. Three of the six cases here are perfectly LEGAL -- a half period at the minimum, a gap at the minimum, and a one-bit frame -- and the hardened slave handled all three correctly and silently while the naive one mishandled " &
integer'image(legal_noise_n) &
". The other three are abnormal events a real system produces anyway: a reset mid-frame, a select released mid-frame, and a frame width reprogrammed between two frames of a burst. All three broke the naive slave and none broke the hardened one -- and the three differences between the two designs are not new logic. The width is LATCHED at the select instead of read continuously; the valid is qualified by the expected bit count instead of raised on any deassert; and the shift register is cleared at every select instead of only at reset. Corner-case robustness in a protocol slave is almost always a decision about WHEN a value is sampled or WHETHER an output is qualified. And the two abort cases carry a lesson about the CHECK rather than the design: every payload in them compares EQUAL, because the clean frame that follows overwrites the contaminated bits, and an earlier version of this bench compared only words and passed both slaves. The offence is that the naive slave announced the aborted frame as a word at all -- two words where one transaction completed, indistinguishable downstream from real traffic -- so the measurement that finds it is a count of announcements, not a comparison of payloads. In VHDL the two behaviours are separate ELABORATIONS selected by a generic rather than one design with a mode bit, so no runtime condition can turn a hardened slave back into a naive one"
severity note;
else
report "FAIL: " & integer'image(errors) & " error(s)" severity error;
end if;
done_sim <= true;
wait for 100 ns;
std.env.stop;
end process main;
end architecture tb;7. Driving These From A UVM Environment
Reviewed code, per Chapter 16.3's toolchain note. Two of the six cases cannot be expressed as a sequence item at all, and that is the interesting part.
// THREE OF THE SIX ARE ITEMS. They are legal traffic at an extreme, so they belong in the
// sequence library and in the coverage model -- and a `dist` clause is what makes them arrive
// (Chapter 17.4).
class spi_extremes_seq extends uvm_sequence #(spi_item);
`uvm_object_utils(spi_extremes_seq)
spi_cfg cfg;
task body();
// C6 -- a one-bit frame.
`uvm_do_with(req, { nbits == 1; })
// C4 -- the half period at its minimum.
`uvm_do_with(req, { half == cfg.half_min; })
// C5 -- two frames with the gap at its minimum. The SECOND frame is the interesting one,
// because the gap is an obligation on the next transaction's start (Chapter 16.4).
`uvm_do_with(req, { gap == cfg.gap_min; })
`uvm_do_with(req, { gap == cfg.gap_min; })
endtask
endclass
// TWO OF THE SIX ARE NOT ITEMS. A mid-frame reset and a mid-frame release are not transactions --
// they are events that INTERRUPT one, and a sequence item cannot describe its own abortion.
//
// They belong in the driver, reached by a field that says "abandon this transfer at bit k". A
// testbench that tries to express them as items ends up with a driver that special-cases its
// own item type, which is where a driver starts growing test-specific behaviour.
class spi_item_abortable extends spi_item;
`uvm_object_utils(spi_item_abortable)
rand int abort_at_bit; // 0 = run to completion
rand bit reset_mid_frame;
constraint c_abort { abort_at_bit inside {[0 : nbits]}; }
endclass
class spi_abort_driver extends spi_master_driver;
`uvm_component_utils(spi_abort_driver)
virtual task drive_txn(spi_item t);
spi_item_abortable a;
if ($cast(a, t) && a.abort_at_bit != 0) begin
drive_partial(a, a.abort_at_bit);
if (a.reset_mid_frame) pulse_reset(); // C1
else release_select(); // C2
return;
end
super.drive_txn(t);
endtask
endclass
// AND THE SCOREBOARD CHECK THAT THE TWO ABORT CASES NEED, which is the section 5 result in its
// UVM form. `n_observed` against `n_predicted` is a different comparison from
// `obs.data == pred.data`, and only the first one sees a transaction that should not exist.
//
// Chapter 16.6's scoreboard already has this: an observation with no prediction waiting is
// counted as UNEXPECTED rather than compared. An aborted frame announced by the DUT arrives as
// exactly that -- and a scoreboard that pops a prediction for every observation, instead of
// checking that one was waiting, silently consumes the next real transaction's prediction and
// reports the mismatch one frame late.
function void check_phase(uvm_phase phase);
super.check_phase(phase);
if (sb.n_unexpected != 0)
`uvm_error("SPI_EXTRA",
$sformatf("%0d transactions were announced that no sequence asked for", sb.n_unexpected))
endfunction
// THE SIXTH CASE -- the width reprogrammed between frames -- is a CONFIGURATION change, and it has
// to happen between transactions. `uvm_config_db` set from a sequence mid-burst is the realistic
// version, and it is also how a real driver acquires a stale width.
class spi_reconfig_seq extends uvm_sequence #(spi_item);
`uvm_object_utils(spi_reconfig_seq)
spi_cfg cfg;
task body();
`uvm_do_with(req, { nbits == 8; })
// The change is made while the bus is idle, which is the legal moment. A slave that reads its
// width continuously still applies it to the NEXT frame's midpoint.
cfg.nbits = 4;
`uvm_do_with(req, { nbits == 4; })
endtask
endclassThe point of that code is the line that is absent from most environments: n_unexpected. A scoreboard that compares payload for payload cannot report a transaction nobody asked for, and two of these six corner cases produce exactly that.
8. Why a Verification Engineer Cares
Because a corner-case list with only faults in it cannot distinguish robustness from paranoia, and because two of these six are invisible to the check most scoreboards actually implement.
The habit worth taking: every corner-case table needs both polarities. For each case, state whether the protocol permits it, and check the permitted ones for silence as explicitly as the forbidden ones for a report. A design that rejects legal traffic fails in the field exactly as reliably as one that accepts illegal traffic.
The second is about what a scoreboard compares. A payload comparison cannot see a transaction that should not exist — and an aborted frame announced by a slave arrives with a valid, a width and a plausible payload. The check is n_observed against n_predicted, which Chapter 16.6 called unexpected and built in for a different reason.
And the third is about where corner cases live. Two of these six are not sequence items at all — a mid-frame reset and a mid-frame release interrupt a transaction rather than being one — so they belong in the driver behind a field, and a suite that tries to express them as items grows test-specific behaviour inside a component every test shares.
9. Why an FPGA or ASIC Engineer Cares
Because the three fixes are three lines and they are the three lines a first implementation gets wrong.
Latch the configuration at the select. A width, a mode, an address width read continuously is a value that can change under a transfer, and the legal moment to reprogram it — while the bus is idle — is exactly when the naive slave is most exposed.
Qualify the valid. An output that says a transaction happened must be qualified by the transaction having completed. Raised on any deassert, it delivers a garbage word that is indistinguishable downstream from a real one, and the consumer has no way to reject it.
Clear state at the start of a transaction, not only at reset. Reset is not the only way a transfer ends early, and a shift register that survives an abort contaminates the next frame — which means the failing frame is not the one the event happened on.
That last consequence is worth remembering for debug: a corner-case failure often surfaces one transaction late.
10. Failure Signature — A Slave That Delivers A Word Nobody Sent
Symptom a driver occasionally receives an extra word. Its payload is
plausible, its width field is plausible, and the transaction
before and after it are both correct. It is intermittent and
correlates with nothing in the software.
What happened the master aborted a frame -- a timeout, an arbitration loss,
a reset -- and the slave announced the partial frame as a
completed transaction, because its valid is raised on any
deassert rather than on reaching the expected bit count.
What would have a scoreboard that counts observations against predictions
caught it rather than comparing payload for payload. Every payload in
this failure mode compares equal.
The tell the extra word appears where a transfer was interrupted, and
the transaction AFTER it is fine. A contaminated shift
register shifts the symptom one frame later, so look at the
frame before the suspicious one rather than at the suspicious
one.11. Common Misconceptions
"A corner case is an illegal stimulus." Half of these six are legal, and a slave that reports a problem on them is broken in the other direction. The table needs both polarities or it measures neither.
"A one-bit frame needs special index handling." The measurement says the naive slave's one-bit failure is the uncleared shift register, not an index bug. Two of the six cases are the same defect by different routes, which is worth knowing before writing a third fix.
"If the payloads match, the transaction was correct." Every payload in the two abort cases matched. The defect was an extra announcement — a transaction that should not exist — which no payload comparison can see.
"Robustness means extra logic." All three fixes here are decisions about when a value is sampled or whether an output is qualified. No new state, no error output, no configuration.
"A mid-frame reset is a sequence item." It interrupts a transaction rather than being one. Expressed as an item it forces the driver to special-case its own item type, which is where a shared component starts acquiring test-specific behaviour.
"The failing frame is the one the corner case happened on." A contaminated shift register delivers the wrong word on the next frame. A check scoped to the frame the stimulus was aimed at can miss the failure entirely.
12. Reason It Through
Both slaves produce identical payloads in C1 and C2. What is the naive slave's actual offence, and which measurement finds it?
It announces the aborted frame as a completed transaction: two rx_valid pulses where one transfer completed. The clean frame that follows overwrites the contaminated shift register, so payloads all match. Counting announcements against expected transactions finds it; comparing words does not.
Why does the naive slave get a one-bit frame wrong, given that its index arithmetic is correct at width one?
Because it never clears its shift register at the select. sh still holds the previous frame's word, only bit 0 is overwritten, and the result is the old word with its bottom bit replaced. It is the same defect as C1 and C2 arriving through a third symptom.
C3's reconfiguration happens while the bus is idle, which is the legal moment. Why does the naive slave still fail?
Because it reads the width continuously, so the new value is in force during the next frame's midpoint — it never captured the value that frame started with. Latching at the select is what makes "reprogram while idle" a safe operation from the slave's side.
Name the three fixes and say what they have in common.
Latch the width at the select; qualify the valid by the expected bit count; clear the shift register at every select. All three are decisions about when a value is sampled or whether an output is qualified — none of them adds state or logic.
Why do two of these six corner cases not belong in a sequence item?
Because a mid-frame reset and a mid-frame release interrupt a transaction rather than describing one, and an item cannot describe its own abortion. They belong in the driver behind a field, or every test's driver grows a special case for a test-specific item type.
13. Understanding Check
14. Summary
A corner case is not the same thing as an illegal stimulus, and a table containing only failures cannot tell a robust design from a paranoid one. Three of the six cases here are perfectly legal — a half period at the minimum, a gap at the minimum, and a one-bit frame — and the hardened slave handled all three correctly and silently while the naive one mishandled one, returning the preceding frame's word with its bottom bit replaced. The other three are abnormal events a real system produces anyway: a reset mid-frame, a select released mid-frame, and a frame width reprogrammed between two frames of a burst. All three broke the naive slave and none broke the hardened one — and the three differences between the designs are not new logic: the width is latched at the select, the valid is qualified by the expected bit count, and the shift register is cleared at every select. And the two abort cases carry a lesson about the check rather than the design: every payload in them compares equal, because the clean frame that follows overwrites the contaminated bits, and an earlier version of this bench compared only words and passed both slaves. The offence is that the naive slave announced the aborted frame as a word at all — two announcements where one transfer completed, indistinguishable downstream from real traffic — so the measurement that finds it is a count of announcements, not a comparison of payloads.
15. What Comes Next
Every check in this module and the last one now exists and has been measured. Chapter 17.7 asks the only remaining question — whether that is enough — and gives an answer that is not a percentage.
Continue learning
Related tutorials
- Related topic
Reset Behaviour and Safe Idle
A slave's reset lands mid-transaction, and the fact it most needs is destroyed by the reset that created the situation because the synchronisers reset to deselected. Recovering that fact exactly from timing instead, why refusing one transaction beats guessing, and a safe-idle gate verified in three HDLs.
- Related topic
Full-Duplex Exchange
Every SPI transfer moves a bit in both directions on every edge, whether the software wanted it to or not. Where dummy bytes come from, why bytes received during a command phase exist but mean nothing, and why read and write are interpretations rather than modes.
- Related topic
Slave Output Valid Timing
How long after a clock edge a peripheral may take before MISO is trustworthy. What clock-to-output includes, why the datasheet number is conditional on a load your board probably exceeds, and why it dominates the return-path budget.
- Related topic
CPHA — Clock Phase
The second mode bit: which logical edge carries the sample role, why that is independent of polarity, and why one of its values forces a transmitter to place its first bit before any clock edge exists.
