Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 burst

2. The Slave, Written Twice

Both slaves see identical pins. The difference between them is three decisions, and none of them is new logic:

naivehardened
the frame widthread from the input continuouslylatched at the select
rx_validraised on any deassertraised only for a frame that reached its expected bit count
the shift registercleared only at resetcleared 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 cycles
Five rows over sixteen cycles. Chip select falls for a first frame and again for a second. The programmed width changes while the bus is idle between them. One row shows the naive slave's effective width changing mid-frame and another shows the hardened slave's latched width holding.reprogrammed while idlereprogrammed while idlehardened latches herehardened latches hereCS_n1000011000001111SCLKnbits8888884444444444naive width8888884444444444hard width0888888444444444t0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15
Figure 1 — the width reprogrammed between two frames, which is the legal moment to do it. The naive slave reads `nbits` continuously, so the new value reaches it during the second frame and it counts to the wrong total; the hardened slave latched the width when the select fell and is unaffected. The `width seen` rows are each slave's own effective width, not a signal on the bus.

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 01

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_lite.sv — the same slave twice, naive and hardened, differing by three sampling decisions
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_lite.v — the same design in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_lite.vhd — the same design in VHDL
-- 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

Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_lite_tb.sv — six corner cases with both polarities, and the two that a payload comparison cannot see
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_lite_tb.v — the same bench in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_lite_tb.vhd — the same bench in VHDL
-- 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endclass

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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