SPI · Module 18
CS Timing Faults and Partial Frames
The only fault family in this module whose evidence lives in a different frame from its symptom. A frame cut short leaves orphan bits, and the NEXT frame is structurally flawless and carries the wrong byte.
Everything so far has diagnosed a capture, or a pair of captures of the same read. This chapter breaks that pattern, because chip-select faults do something none of the others do.
The frame whose data is wrong is not the frame that is broken.
1. What This Chapter Is Not About
Chapter 18.1's triage already measures the lead and the lag of a single select — whether CS fell early enough before the first clock edge and rose late enough after the last — and reports EV_CSBND when either is short. That is a property of one capture and it is settled.
This chapter is about framing across a sequence: how many times CS asserted, how many clocks each assertion carried, and what one assertion leaves behind for the next.
2. The Mechanism Is A Design Decision, Not A Protocol Rule
A slave's bit counter has to be cleared by something. There are two choices:
clear on every CS assertion every frame starts from a known state
clear only on reset the counter just keeps countingThe second is extremely common. Such slaves work perfectly as long as the master sends whole words, and the specification does not require anything else — SPI has no frame-length field, no counter reset command, and no way for a slave to know how long a word is supposed to be except by counting clocks.
3. The Symptom Arrives One Frame Late
The slave's bit counter does not clear at the select
14 cyclesEverything visible inside the second assertion is correct. The fault is the number n=5 at the moment it begins — a count of bits left over by a frame that has already gone past. That number is what the decoder below publishes as carry_in, and it is the whole chapter in one value.
4. Six Framing Verdicts
| Verdict | Clocks in this assertion | Meaning |
|---|---|---|
D_OK | exactly a word | clean, and nothing inherited |
D_SHORT | fewer | truncated; leaves orphan bits |
D_GLITCH | fewer, and it completes the previous word | one word split by a spurious release |
D_MERGED | an exact multiple | several words in one assertion — a missing release |
D_RAGGED | more, not a multiple | merged and truncated at once |
D_INHERITED | exactly a word | flawless, and carrying inherited orphan bits |
D_INHERITED is the one that matters. It describes a frame as structurally perfect and its data as wrong in the same verdict, and nothing inside that frame supports the second half of the statement.
5. The Measurement
Five captures, seventeen framing intervals, identical output from all three languages:
cap iv edges c_in c_out ann word verdict expected note
-- capture A: five frames, the THIRD cut short after 5 of 8 clocks
0 0 8 0 0 1 8d OK OK a whole word
0 1 8 0 0 1 c3 OK OK a whole word
0 2 5 0 5 0 c3 SHORT SHORT CUT SHORT -- leaves 5 orphan bits
0 3 8 5 5 1 59 INHERITED INHERITED flawless frame, WRONG DATA
0 4 8 5 5 1 e7 INHERITED INHERITED still poisoned -- only a reset clears it
-- capture B: a MISSING release -- two words inside one assertion
1 5 16 0 0 2 c3 MERGED MERGED 16 clocks, 2 words, 1 assertion
1 6 8 0 0 1 5a OK OK a whole word
1 7 8 0 0 1 3c OK OK a whole word
-- capture C: a SPURIOUS release -- one word split across two assertions
2 8 3 0 3 0 00 SHORT SHORT 3 clocks then CS released
2 9 5 3 0 1 8d GLITCH GLITCH 5 more -- together exactly one word
2 10 8 0 0 1 c3 OK OK a whole word
-- capture D: 11 clocks in one assertion -- merged and truncated at once
3 11 11 0 3 1 8d RAGGED RAGGED one word plus 3 orphan bits
3 12 8 3 3 1 cb INHERITED INHERITED flawless frame, WRONG DATA
3 13 8 3 3 1 47 INHERITED INHERITED still poisoned
-- capture E: the SAME truncation as capture A, positioned LAST
4 14 8 0 0 1 8d OK OK a whole word
4 15 8 0 0 1 c3 OK OK a whole word
4 16 5 0 5 0 c3 SHORT SHORT CUT SHORT -- and nothing follows itInterval 3 is the chapter. Eight clocks, nothing inherited within it, and it announced 0x59 where its payload was 0x3c. The mixed byte is computed by the bench from the definition and required to match the decoder's prediction, so the data is wrong here and the fault is there is a measured statement with two numbers behind it.
6. The Poisoning Persists — And What That Sounds Like In A Bug Report
Interval 4 inherited the same five orphan bits and announced 0xe7. The residue does not drain, because the counter that holds it is cleared by reset and by nothing else. Every word from the truncation onward is a mix.
symptom in the field: "the link works, then goes wrong, and stays wrong"
"power-cycling it fixes it"
"it comes back after a while"7. Merge Against Glitch: A Ratio, Not A Per-Frame Check
Captures B and C are the two framing faults that leave every individual edge count defensible.
capture B 3 assertions delivered 4 words words OUTNUMBER assertions → a MISSING release
capture C 3 assertions delivered 2 words assertions outnumber words → a SPURIOUS releaseLook at capture C's edge counts on their own: 3, then 5, then 8. Not one of those is obviously wrong — a 3-clock transfer and a 5-clock transfer are legal SPI, just not what this link uses. A per-frame checker configured to expect 8 clocks flags two short frames and stops there. The fault is visible only as a ratio across the capture, and the sign of asserts − words names which of the two it is.
8. The Result That Was Not Designed In
Capture C produced something the chapter did not set out to show: the split word came back correct. Interval 9 announced 0x8d, exactly the payload interval 8 began.
Of course it did. A slave that does not clear on select simply carries on counting, so a spurious release costs it nothing at all. Which means:
| Slave design | A truncated frame | A spurious release |
|---|---|---|
| clears only on reset | poisoned indefinitely | survives it, data intact |
| clears on every select | loses one word, then recovers | corrupted, and cannot see it |
9. The End Of A Capture Is A Blind Spot
Capture E contains the identical truncation as capture A, positioned last.
mid-sequence SHORT, then INHERITED, then INHERITED 3 non-clean verdicts
at the end SHORT 1 non-clean verdictThe structural evidence survives — a short frame is a short frame. The data symptom never appears, because there is no following frame left to corrupt. So a capture that stops at the fault shows a short frame with no visible consequence, and a capture that stops one frame earlier shows nothing at all.
That makes capture more than you think you need a measured requirement rather than folklore, and it has a specific form: for this fault family the buffer must extend at least one full frame past the suspect event. A trigger set on the mismatch itself, with the buffer centred on the trigger, is the wrong configuration — the evidence is before the trigger, and it is not a data event.
10. Building It — Three HDLs
The modelled slave inside the decoder deliberately does not clear on CS assertion, because that is the design under suspicion. What the module produces is a prediction — this is the word a slave that does not clear on select would have reported — and the bench checks that prediction against an independent model of the same behaviour, announcement for announcement, on all seventeen intervals.
// spi_cs_diag.sv
//
// Chapter 18.5 -- chip select, and the only fault family in this module whose EVIDENCE lives in a
// different frame from its SYMPTOM.
//
// WHAT THIS CHAPTER IS NOT ABOUT. Chapter 18.1 already measures the lead and the lag of a single
// select -- whether CS fell early enough before the first edge and rose late enough after the last --
// and reports `EV_CSBND` when either is short. That is a property of ONE capture and it is settled.
//
// This chapter is about framing across a SEQUENCE: how many times CS asserted, how many clocks each
// assertion carried, and what one assertion leaves behind for the next.
//
// THE MECHANISM, AND IT IS A DESIGN DECISION RATHER THAN A PROTOCOL RULE.
//
// A slave's bit counter has to be cleared by something. The obvious choice is the select -- clear on
// every CS assertion, so every frame starts from a known state. Very many real slaves do not do that:
// they clear only on reset and rely on the master sending whole words. Those slaves work perfectly
// until a frame is truncated, and then:
//
// frame N is cut short after 5 of 8 clocks -> the slave holds 5 orphan bits
// frame N+1 is PERFECT -- 8 clocks, clean lead and lag, clean edges --
// and the word the slave announces during it is 5 bits of frame N
// followed by 3 bits of frame N+1
//
// So the frame whose DATA is wrong is not the frame that is BROKEN. An engineer who captures the
// mismatching transaction captures frame N+1, finds nothing wrong with it, and concludes the slave is
// unreliable. The evidence is one frame earlier, and it is structural rather than data.
//
// Worse, the residue PERSISTS. Every subsequent word is a mix, so the symptom is a stream that is
// permanently wrong until something resets the slave -- which is exactly the behaviour that gets
// described as "it works after a power cycle" and then filed as a supply problem.
//
// WHAT THIS MODULE PUBLISHES.
//
// ob_edges leading edges in this assertion
// ob_carry_in orphan bits this assertion INHERITED from the one before
// ob_carry_out orphan bits it leaves behind
// ob_words words the modelled slave announced during it
// ob_word the last word it announced -- the corrupted value, computed rather than guessed
// dg_code the framing verdict
//
// `ob_carry_in` is the whole chapter in one number. A frame diagnosed `D_INHERITED` is structurally
// flawless and carries wrong data, and the only thing that says so is a count of bits left over by a
// frame that has already gone past.
//
// THE MODELLED SLAVE IS A PREDICTION, NOT AN ASSUMPTION. The accumulator here deliberately does NOT
// clear on CS assertion, because that is the design under suspicion. What it produces is "this is the
// word a slave that does not clear on select would have reported", and the bench checks that
// prediction against an independent model of the same behaviour. A diagnostic that assumed the
// correct design would see nothing wrong with any of these captures.
`timescale 1ns/1ps
module spi_cs_diag #(
parameter int DW = 32,
parameter int NB = 8, // the word length the link is supposed to use
parameter int CNT_W = 8
) (
input wire clk,
input wire rst_n,
input wire sclk,
input wire cs_n,
input wire mosi,
input wire cpol,
input wire cpha,
output reg dg_valid, // one pulse per CS assertion, at its release
output reg [2:0] dg_code,
output reg [CNT_W-1:0] ob_edges,
output reg [CNT_W-1:0] ob_carry_in,
output reg [CNT_W-1:0] ob_carry_out,
output reg [CNT_W-1:0] ob_words, // announcements during this assertion
output reg [DW-1:0] ob_word, // the last one -- the value the slave would report
// Running totals for the whole capture. The SIGN of (asserts - words) is the observation that
// separates a merge from a glitch, and neither is visible inside one assertion.
output reg [CNT_W-1:0] ob_asserts,
output reg [CNT_W-1:0] ob_words_tot
);
localparam [2:0] D_OK = 3'd0,
// This assertion carried fewer than a whole word and left orphan bits behind.
D_SHORT = 3'd1,
// Fewer than a word, and it COMPLETED the word the previous assertion started --
// so the two together carried exactly one word split by a spurious release.
D_GLITCH = 3'd2,
// More than a word, an exact multiple: several words in one assertion because no
// release separated them.
D_MERGED = 3'd3,
// More than a word and not a multiple: merged AND truncated.
D_RAGGED = 3'd4,
// EXACTLY a word, clean in every respect, and carrying inherited orphan bits.
// This is the frame whose data is wrong, and nothing inside it says so.
D_INHERITED = 3'd5;
reg sclk_d, cs_n_d;
reg [CNT_W-1:0] edges, carry_in, words;
reg [CNT_W-1:0] nbits; // the modelled slave's bit counter -- cleared ONLY by reset
reg [DW-1:0] acc, last_word;
wire cs_assert = cs_n_d & ~cs_n;
wire cs_deassert = ~cs_n_d & cs_n;
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;
// `(carry_in + edges) mod NB` -- the orphan bits this assertion will leave. Written as a
// subtraction chain rather than a modulo because NB is a parameter and a divider is not wanted in
// a diagnostic that may sit permanently in a controller.
function [CNT_W-1:0] modnb(input [CNT_W+3:0] v);
reg [CNT_W+3:0] t;
integer i;
begin
t = v;
for (i = 0; i < (1 << (CNT_W-2)); i = i + 1)
if (t >= NB) t = t - NB;
modnb = t[CNT_W-1:0];
end
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
edges <= {CNT_W{1'b0}};
carry_in <= {CNT_W{1'b0}};
words <= {CNT_W{1'b0}};
nbits <= {CNT_W{1'b0}};
acc <= {DW{1'b0}};
last_word <= {DW{1'b0}};
dg_valid <= 1'b0;
dg_code <= D_OK;
ob_edges <= {CNT_W{1'b0}};
ob_carry_in <= {CNT_W{1'b0}};
ob_carry_out <= {CNT_W{1'b0}};
ob_words <= {CNT_W{1'b0}};
ob_word <= {DW{1'b0}};
ob_asserts <= {CNT_W{1'b0}};
ob_words_tot <= {CNT_W{1'b0}};
end else begin
dg_valid <= 1'b0;
if (cs_assert) begin
// The orphan count is sampled HERE, at the start of the interval it describes. Reading
// it at the release would report the count this interval leaves, not the one it
// inherited -- and the whole point of the number is that it belongs to the past.
carry_in <= nbits;
edges <= {CNT_W{1'b0}};
words <= {CNT_W{1'b0}};
ob_asserts <= ob_asserts + 1'b1;
// NOTE WHAT IS NOT CLEARED. `nbits` and `acc` survive the select, because the slave
// under suspicion does not clear them. Clearing them here would model the CORRECT
// design and this module would report every capture below as clean.
end else if (capture) begin
edges <= edges + 1'b1;
if (nbits + 1'b1 == NB[CNT_W-1:0]) begin
last_word <= {acc[DW-2:0], mosi};
nbits <= {CNT_W{1'b0}};
acc <= {acc[DW-2:0], mosi};
words <= words + 1'b1;
ob_words_tot <= ob_words_tot + 1'b1;
end else begin
acc <= {acc[DW-2:0], mosi};
nbits <= nbits + 1'b1;
end
end
if (cs_deassert) begin
dg_valid <= 1'b1;
ob_edges <= edges;
ob_carry_in <= carry_in;
ob_carry_out <= nbits;
ob_words <= words;
ob_word <= last_word;
if ((edges < NB[CNT_W-1:0]) && ((carry_in + edges) == NB[CNT_W-1:0]))
dg_code <= D_GLITCH;
else if (edges < NB[CNT_W-1:0])
dg_code <= D_SHORT;
else if ((edges > NB[CNT_W-1:0]) && (modnb({4'b0, carry_in + edges}) == {CNT_W{1'b0}})
&& (carry_in == {CNT_W{1'b0}}))
dg_code <= D_MERGED;
else if (edges > NB[CNT_W-1:0])
dg_code <= D_RAGGED;
else if (carry_in != {CNT_W{1'b0}})
dg_code <= D_INHERITED;
else
dg_code <= D_OK;
end
sclk_d <= sclk;
cs_n_d <= cs_n;
end
end
endmodule// spi_cs_diag.v
//
// Chapter 18.5 -- chip select, and the only fault family in this module whose EVIDENCE lives in a
// different frame from its SYMPTOM.
//
// WHAT THIS CHAPTER IS NOT ABOUT. Chapter 18.1 already measures the lead and the lag of a single
// select -- whether CS fell early enough before the first edge and rose late enough after the last --
// and reports `EV_CSBND` when either is short. That is a property of ONE capture and it is settled.
//
// This chapter is about framing across a SEQUENCE: how many times CS asserted, how many clocks each
// assertion carried, and what one assertion leaves behind for the next.
//
// THE MECHANISM, AND IT IS A DESIGN DECISION RATHER THAN A PROTOCOL RULE.
//
// A slave's bit counter has to be cleared by something. The obvious choice is the select -- clear on
// every CS assertion, so every frame starts from a known state. Very many real slaves do not do that:
// they clear only on reset and rely on the master sending whole words. Those slaves work perfectly
// until a frame is truncated, and then:
//
// frame N is cut short after 5 of 8 clocks -> the slave holds 5 orphan bits
// frame N+1 is PERFECT -- 8 clocks, clean lead and lag, clean edges --
// and the word the slave announces during it is 5 bits of frame N
// followed by 3 bits of frame N+1
//
// So the frame whose DATA is wrong is not the frame that is BROKEN. An engineer who captures the
// mismatching transaction captures frame N+1, finds nothing wrong with it, and concludes the slave is
// unreliable. The evidence is one frame earlier, and it is structural rather than data.
//
// Worse, the residue PERSISTS. Every subsequent word is a mix, so the symptom is a stream that is
// permanently wrong until something resets the slave -- which is exactly the behaviour that gets
// described as "it works after a power cycle" and then filed as a supply problem.
//
// WHAT THIS MODULE PUBLISHES.
//
// ob_edges leading edges in this assertion
// ob_carry_in orphan bits this assertion INHERITED from the one before
// ob_carry_out orphan bits it leaves behind
// ob_words words the modelled slave announced during it
// ob_word the last word it announced -- the corrupted value, computed rather than guessed
// dg_code the framing verdict
//
// `ob_carry_in` is the whole chapter in one number. A frame diagnosed `D_INHERITED` is structurally
// flawless and carries wrong data, and the only thing that says so is a count of bits left over by a
// frame that has already gone past.
//
// THE MODELLED SLAVE IS A PREDICTION, NOT AN ASSUMPTION. The accumulator here deliberately does NOT
// clear on CS assertion, because that is the design under suspicion. What it produces is "this is the
// word a slave that does not clear on select would have reported", and the bench checks that
// prediction against an independent model of the same behaviour. A diagnostic that assumed the
// correct design would see nothing wrong with any of these captures.
`timescale 1ns/1ps
module spi_cs_diag #(
parameter DW = 32,
parameter NB = 8, // the word length the link is supposed to use
parameter CNT_W = 8
) (
input wire clk,
input wire rst_n,
input wire sclk,
input wire cs_n,
input wire mosi,
input wire cpol,
input wire cpha,
output reg dg_valid, // one pulse per CS assertion, at its release
output reg [2:0] dg_code,
output reg [CNT_W-1:0] ob_edges,
output reg [CNT_W-1:0] ob_carry_in,
output reg [CNT_W-1:0] ob_carry_out,
output reg [CNT_W-1:0] ob_words, // announcements during this assertion
output reg [DW-1:0] ob_word, // the last one -- the value the slave would report
// Running totals for the whole capture. The SIGN of (asserts - words) is the observation that
// separates a merge from a glitch, and neither is visible inside one assertion.
output reg [CNT_W-1:0] ob_asserts,
output reg [CNT_W-1:0] ob_words_tot
);
localparam [2:0] D_OK = 3'd0,
// This assertion carried fewer than a whole word and left orphan bits behind.
D_SHORT = 3'd1,
// Fewer than a word, and it COMPLETED the word the previous assertion started --
// so the two together carried exactly one word split by a spurious release.
D_GLITCH = 3'd2,
// More than a word, an exact multiple: several words in one assertion because no
// release separated them.
D_MERGED = 3'd3,
// More than a word and not a multiple: merged AND truncated.
D_RAGGED = 3'd4,
// EXACTLY a word, clean in every respect, and carrying inherited orphan bits.
// This is the frame whose data is wrong, and nothing inside it says so.
D_INHERITED = 3'd5;
reg sclk_d, cs_n_d;
reg [CNT_W-1:0] edges, carry_in, words;
reg [CNT_W-1:0] nbits; // the modelled slave's bit counter -- cleared ONLY by reset
reg [DW-1:0] acc, last_word;
wire cs_assert = cs_n_d & ~cs_n;
wire cs_deassert = ~cs_n_d & cs_n;
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;
// `(carry_in + edges) mod NB` -- the orphan bits this assertion will leave. Written as a
// subtraction chain rather than a modulo because NB is a parameter and a divider is not wanted in
// a diagnostic that may sit permanently in a controller.
function [CNT_W-1:0] modnb;
input [CNT_W+3:0] v;
reg [CNT_W+3:0] t;
integer i;
begin
t = v;
for (i = 0; i < (1 << (CNT_W-2)); i = i + 1)
if (t >= NB) t = t - NB;
modnb = t[CNT_W-1:0];
end
endfunction
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
edges <= {CNT_W{1'b0}};
carry_in <= {CNT_W{1'b0}};
words <= {CNT_W{1'b0}};
nbits <= {CNT_W{1'b0}};
acc <= {DW{1'b0}};
last_word <= {DW{1'b0}};
dg_valid <= 1'b0;
dg_code <= D_OK;
ob_edges <= {CNT_W{1'b0}};
ob_carry_in <= {CNT_W{1'b0}};
ob_carry_out <= {CNT_W{1'b0}};
ob_words <= {CNT_W{1'b0}};
ob_word <= {DW{1'b0}};
ob_asserts <= {CNT_W{1'b0}};
ob_words_tot <= {CNT_W{1'b0}};
end else begin
dg_valid <= 1'b0;
if (cs_assert) begin
// The orphan count is sampled HERE, at the start of the interval it describes. Reading
// it at the release would report the count this interval leaves, not the one it
// inherited -- and the whole point of the number is that it belongs to the past.
carry_in <= nbits;
edges <= {CNT_W{1'b0}};
words <= {CNT_W{1'b0}};
ob_asserts <= ob_asserts + 1'b1;
// NOTE WHAT IS NOT CLEARED. `nbits` and `acc` survive the select, because the slave
// under suspicion does not clear them. Clearing them here would model the CORRECT
// design and this module would report every capture below as clean.
end else if (capture) begin
edges <= edges + 1'b1;
if (nbits + 1'b1 == NB[CNT_W-1:0]) begin
last_word <= {acc[DW-2:0], mosi};
nbits <= {CNT_W{1'b0}};
acc <= {acc[DW-2:0], mosi};
words <= words + 1'b1;
ob_words_tot <= ob_words_tot + 1'b1;
end else begin
acc <= {acc[DW-2:0], mosi};
nbits <= nbits + 1'b1;
end
end
if (cs_deassert) begin
dg_valid <= 1'b1;
ob_edges <= edges;
ob_carry_in <= carry_in;
ob_carry_out <= nbits;
ob_words <= words;
ob_word <= last_word;
if ((edges < NB[CNT_W-1:0]) && ((carry_in + edges) == NB[CNT_W-1:0]))
dg_code <= D_GLITCH;
else if (edges < NB[CNT_W-1:0])
dg_code <= D_SHORT;
else if ((edges > NB[CNT_W-1:0]) && (modnb({4'b0, carry_in + edges}) == {CNT_W{1'b0}})
&& (carry_in == {CNT_W{1'b0}}))
dg_code <= D_MERGED;
else if (edges > NB[CNT_W-1:0])
dg_code <= D_RAGGED;
else if (carry_in != {CNT_W{1'b0}})
dg_code <= D_INHERITED;
else
dg_code <= D_OK;
end
sclk_d <= sclk;
cs_n_d <= cs_n;
end
end
endmodule-- spi_cs_diag.vhd
--
-- Chapter 18.5 -- chip select, and the only fault family in this module whose EVIDENCE lives in a
-- different frame from its SYMPTOM.
--
-- WHAT THIS CHAPTER IS NOT ABOUT. Chapter 18.1 already measures the lead and the lag of a single
-- select -- whether CS fell early enough before the first edge and rose late enough after the last --
-- and reports `EV_CSBND` when either is short. That is a property of ONE capture and it is settled.
--
-- This chapter is about framing across a SEQUENCE: how many times CS asserted, how many clocks each
-- assertion carried, and what one assertion leaves behind for the next.
--
-- THE MECHANISM, AND IT IS A DESIGN DECISION RATHER THAN A PROTOCOL RULE.
--
-- A slave's bit counter has to be cleared by something. The obvious choice is the select -- clear on
-- every CS assertion, so every frame starts from a known state. Very many real slaves do not do that:
-- they clear only on reset and rely on the master sending whole words. Those slaves work perfectly
-- until a frame is truncated, and then:
--
-- frame N is cut short after 5 of 8 clocks -> the slave holds 5 orphan bits
-- frame N+1 is PERFECT -- 8 clocks, clean lead and lag, clean edges --
-- and the word the slave announces during it is 5 bits of frame N
-- followed by 3 bits of frame N+1
--
-- So the frame whose DATA is wrong is not the frame that is BROKEN. An engineer who captures the
-- mismatching transaction captures frame N+1, finds nothing wrong with it, and concludes the slave is
-- unreliable. The evidence is one frame earlier, and it is structural rather than data.
--
-- Worse, the residue PERSISTS. Every subsequent word is a mix, so the symptom is a stream that is
-- permanently wrong until something resets the slave -- which is exactly the behaviour that gets
-- described as "it works after a power cycle" and then filed as a supply problem.
--
-- WHAT THIS MODULE PUBLISHES.
--
-- ob_edges leading edges in this assertion
-- ob_carry_in orphan bits this assertion INHERITED from the one before
-- ob_carry_out orphan bits it leaves behind
-- ob_words words the modelled slave announced during it
-- ob_word the last word it announced -- the corrupted value, computed rather than guessed
-- dg_code the framing verdict
--
-- `ob_carry_in` is the whole chapter in one number. A frame diagnosed `D_INHERITED` is structurally
-- flawless and carries wrong data, and the only thing that says so is a count of bits left over by a
-- frame that has already gone past.
--
-- THE MODELLED SLAVE IS A PREDICTION, NOT AN ASSUMPTION. The accumulator here deliberately does NOT
-- clear on CS assertion, because that is the design under suspicion. What it produces is "this is the
-- word a slave that does not clear on select would have reported", and the bench checks that
-- prediction against an independent model of the same behaviour. A diagnostic that assumed the
-- correct design would see nothing wrong with any of these captures.
--
-- WHAT THE VHDL VERSION ADDS. `mod` is a language operator here, so the orphan-bit arithmetic is one
-- expression instead of the subtraction loop the Verilog versions need -- and the loop in those
-- versions is not stylistic: a run-time modulo by a parameter infers a divider, which is not wanted in
-- a diagnostic meant to sit permanently in a controller. The VHDL reads better and synthesises to the
-- same structure only because `NB_C` is a constant; a variable divisor would cost the same there.
--
-- The verdict is an enumeration whose six names include two that describe a frame as PERFECT and WRONG
-- at once -- `D_INHERITED` -- and having that sit in a named type is worth more than a comment.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). Generics are `NB_C` and `CNT_W`; the process variables
-- are `edges`, `carry_in`, `words`, `nbits`, `acc`. None of those is `NB`/`nb` or `CNT`/`cnt`, because a
-- variable that differs from a generic only in case IS that generic -- the failure Chapter 17.4 spent
-- an afternoon on, where a variable `tries` silently became the generic `TRIES` and a rejection loop
-- stopped executing with no diagnostic anywhere. Every declaration below was re-read against it.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package spi_cs_pkg is
constant DW_C : natural := 32;
-- Six framing verdicts.
--
-- D_SHORT fewer clocks than a word; leaves orphan bits behind
-- D_GLITCH fewer, and it COMPLETED the previous assertion's word -- one word, two selects
-- D_MERGED an exact multiple of a word: several words, one select
-- D_RAGGED more than a word and not a multiple: merged and truncated at once
-- D_INHERITED EXACTLY a word, clean in every respect, and carrying inherited orphan bits.
-- This is the frame whose data is wrong, and nothing inside it says so.
type cs_diag_t is (D_OK, D_SHORT, D_GLITCH, D_MERGED, D_RAGGED, D_INHERITED);
function diag_name (d : cs_diag_t) return string;
end package spi_cs_pkg;
package body spi_cs_pkg is
function diag_name (d : cs_diag_t) return string is
begin
case d is
when D_OK => return "OK ";
when D_SHORT => return "SHORT ";
when D_GLITCH => return "GLITCH ";
when D_MERGED => return "MERGED ";
when D_RAGGED => return "RAGGED ";
when others => return "INHERITED ";
end case;
end function diag_name;
end package body spi_cs_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_cs_pkg.all;
entity spi_cs_diag is
generic (
NB_C : positive := 8; -- the word length the link is supposed to use
CNT_W : positive := 8
);
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;
dg_valid : out std_logic; -- one pulse per CS assertion, at its release
dg_code : out cs_diag_t;
ob_edges : out natural;
ob_carry_in : out natural;
ob_carry_out : out natural;
ob_words : out natural;
ob_word : out std_logic_vector(DW_C - 1 downto 0);
-- Running totals for the whole capture. The SIGN of (asserts - words) separates a merge from a
-- glitch, and neither is visible inside one assertion.
ob_asserts : out natural;
ob_words_tot : out natural
);
end entity spi_cs_diag;
architecture rtl of spi_cs_diag is
signal v_r : std_logic := '0';
signal d_r : cs_diag_t := D_OK;
signal e_r, ci_r, co_r, w_r : natural := 0;
signal wd_r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal as_r, wt_r : natural := 0;
begin
dg_valid <= v_r;
dg_code <= d_r;
ob_edges <= e_r;
ob_carry_in <= ci_r;
ob_carry_out <= co_r;
ob_words <= w_r;
ob_word <= wd_r;
ob_asserts <= as_r;
ob_words_tot <= wt_r;
process (clk, rst_n) is
variable sclk_d, cs_n_d : std_logic;
variable cs_assert, cs_deassert : boolean;
variable in_txn, sclk_edge : boolean;
variable leading, capture : boolean;
variable edges, carry_in, words : natural;
variable nbits : natural; -- the modelled slave's counter: reset-cleared only
variable acc, last_word : std_logic_vector(DW_C - 1 downto 0);
begin
if rst_n = '0' then
sclk_d := '0'; cs_n_d := '1';
edges := 0; carry_in := 0; words := 0; nbits := 0;
acc := (others => '0'); last_word := (others => '0');
v_r <= '0'; d_r <= D_OK;
e_r <= 0; ci_r <= 0; co_r <= 0; w_r <= 0;
wd_r <= (others => '0'); as_r <= 0; wt_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;
if cs_assert then
-- Sampled HERE, at the start of the interval it describes. Reading it at the release
-- would report the count this interval LEAVES rather than the one it inherited, and the
-- whole point of the number is that it belongs to the past.
carry_in := nbits;
edges := 0;
words := 0;
as_r <= as_r + 1;
-- NOTE WHAT IS NOT CLEARED. `nbits` and `acc` survive the select, because the slave
-- under suspicion does not clear them. Clearing them here would model the CORRECT
-- design, and this module would then report every capture in the bench as clean.
elsif capture then
edges := edges + 1;
acc := acc(DW_C - 2 downto 0) & mosi;
if nbits + 1 = NB_C then
last_word := acc;
nbits := 0;
words := words + 1;
wt_r <= wt_r + 1;
else
nbits := nbits + 1;
end if;
end if;
if cs_deassert then
v_r <= '1';
e_r <= edges;
ci_r <= carry_in;
co_r <= nbits;
w_r <= words;
wd_r <= last_word;
if edges < NB_C and (carry_in + edges) = NB_C then
d_r <= D_GLITCH;
elsif edges < NB_C then
d_r <= D_SHORT;
elsif edges > NB_C and ((carry_in + edges) mod NB_C) = 0 and carry_in = 0 then
d_r <= D_MERGED;
elsif edges > NB_C then
d_r <= D_RAGGED;
elsif carry_in /= 0 then
d_r <= D_INHERITED;
else
d_r <= D_OK;
end if;
end if;
sclk_d := sclk;
cs_n_d := cs_n;
end if;
end process;
end architecture rtl;The Bench
// spi_cs_diag_tb.sv
//
// FIVE CAPTURES, SEVENTEEN FRAMING INTERVALS, AND A SYMPTOM THAT ARRIVES ONE FRAME AFTER ITS CAUSE.
//
// THE FOUR RESULTS.
//
// 1. THE BROKEN FRAME AND THE WRONG-DATA FRAME ARE DIFFERENT FRAMES. Capture A truncates its third
// interval. The fourth interval carries exactly eight clocks, a clean lead, a clean lag and no
// structural defect of any kind -- and the word announced during it is five bits of the third
// interval's payload followed by three of its own. The bench computes that mixed byte from the
// definition and requires the decoder to predict it, so "the data is wrong here and the fault is
// there" is a measured statement with two numbers behind it.
//
// 2. THE POISONING PERSISTS. Every interval after the truncation inherits the same orphan count, so
// every word from then on is a mix. The bench requires the inherited count to be IDENTICAL two
// intervals later -- which is what makes the field symptom "it comes back after a power cycle",
// because only a reset clears the counter.
//
// 3. THE SIGN OF (ASSERTS - WORDS) SEPARATES A MERGE FROM A GLITCH. Capture B delivers four words
// from three assertions -- a missing release merged two frames. Capture C delivers two words from
// three assertions -- a spurious release split one frame in half. Both corrupt data, both leave
// every individual edge count looking defensible, and neither is visible inside one interval.
//
// 4. THE DIAGNOSIS NEEDS A FRAME AFTER THE BROKEN ONE, SO THE END OF A CAPTURE IS A BLIND SPOT.
// Capture E contains the SAME truncation as capture A, positioned last. The structural evidence
// is still there; the data symptom never appears, because there is no following frame to corrupt.
// The bench counts the evidence each position yields and requires them to differ -- which turns
// "capture a bit more than you think you need" from advice into a measurement.
`timescale 1ns/1ps
module spi_cs_diag_tb;
localparam int DW = 32;
localparam int NB = 8;
localparam int CNT_W = 8;
localparam int LEAD = 3;
localparam int HALF = 2;
localparam int LAG = 2;
localparam int GAP = 3;
localparam [2:0] D_OK = 3'd0, D_SHORT = 3'd1, D_GLITCH = 3'd2,
D_MERGED = 3'd3, D_RAGGED = 3'd4, D_INHERITED = 3'd5;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b1;
reg cpol = 1'b0, cpha = 1'b0;
reg b_sclk = 1'b0, b_cs_n = 1'b1, b_mosi = 1'b0;
wire dg_valid;
wire [2:0] dg_code;
wire [CNT_W-1:0] ob_edges, ob_carry_in, ob_carry_out, ob_words, ob_asserts, ob_words_tot;
wire [DW-1:0] ob_word;
spi_cs_diag #(.DW(DW), .NB(NB), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.cpol(cpol), .cpha(cpha),
.dg_valid(dg_valid), .dg_code(dg_code),
.ob_edges(ob_edges), .ob_carry_in(ob_carry_in), .ob_carry_out(ob_carry_out),
.ob_words(ob_words), .ob_word(ob_word),
.ob_asserts(ob_asserts), .ob_words_tot(ob_words_tot)
);
integer errors = 0, x_reports = 0, got_n = 0;
reg [2:0] g_code;
reg [CNT_W-1:0] g_edges, g_cin, g_cout, g_words, g_asserts, g_wtot;
reg [DW-1:0] g_word;
always @(posedge clk) if (dg_valid) begin
got_n = got_n + 1;
g_code = dg_code; g_edges = ob_edges; g_cin = ob_carry_in;
g_cout = ob_carry_out; g_words = ob_words; g_word = ob_word;
g_asserts = ob_asserts; g_wtot = ob_words_tot;
if ((^dg_code === 1'bx) || (^ob_edges === 1'bx) || (^ob_carry_in === 1'bx)
|| (^ob_carry_out === 1'bx) || (^ob_words === 1'bx) || (^ob_word[7:0] === 1'bx)
|| (^ob_asserts === 1'bx) || (^ob_words_tot === 1'bx))
x_reports = x_reports + 1;
end
function [8*12:1] dname(input [2:0] c);
begin
case (c)
D_OK: dname = "OK ";
D_SHORT: dname = "SHORT ";
D_GLITCH: dname = "GLITCH ";
D_MERGED: dname = "MERGED ";
D_RAGGED: dname = "RAGGED ";
default: dname = "INHERITED ";
endcase
end
endfunction
task automatic idle_n(input integer n);
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// One framing interval: assert CS, drive `n` leading edges from a LEFT-ALIGNED bit field, release.
//
// THE FIELD IS LEFT-ALIGNED FOR A REASON. SPI is MSB-first, so a frame cut short after five of
// eight clocks has transmitted the payload's TOP five bits, not its bottom five. The first version
// of this task indexed from `bits[n-1]` downwards, which sends the LOW n bits -- a truncated frame
// then carried bits the master would never have put on the wire first, and the corrupted word the
// chapter is about was a mixture of the wrong halves. The physics decides the indexing.
//
// The number of edges is the stimulus variable, because every fault in this chapter is a mismatch
// between the number of clocks inside an assertion and the word length.
task automatic iv(input [DW-1:0] bits, input integer n);
integer k;
begin
b_sclk = cpol; b_mosi = 1'b0;
idle_n(2);
b_cs_n = 1'b0;
idle_n(1);
b_mosi = bits[DW-1];
idle_n(LEAD - 1);
for (k = 0; k < n; k = k + 1) begin
b_sclk = ~b_sclk;
idle_n(HALF);
b_sclk = ~b_sclk;
if (k < n-1) b_mosi = bits[DW-1-k-1];
idle_n(HALF);
end
idle_n(LAG);
b_cs_n = 1'b1;
idle_n(1);
b_sclk = cpol;
idle_n(GAP);
end
endtask
// THE INDEPENDENT ORACLE. A slave that clears its bit counter only on reset, modelled here from
// the description rather than by asking the decoder. It is fed the same stream of (bits, edges)
// pairs and announces words at exactly the same instants, so a disagreement is a real one.
reg [DW-1:0] o_acc;
integer o_nbits;
reg [DW-1:0] o_last;
integer o_words;
task automatic oracle_iv(input [DW-1:0] bits, input integer n);
integer k;
begin
o_words = 0;
for (k = 0; k < n; k = k + 1) begin
o_acc = {o_acc[DW-2:0], bits[DW-1-k]};
if (o_nbits + 1 == NB) begin
o_nbits = 0; o_last = o_acc & {{(DW-NB){1'b0}}, {NB{1'b1}}}; o_words = o_words + 1;
end else begin
o_nbits = o_nbits + 1;
end
end
end
endtask
localparam [7:0] W0 = 8'h8D, W1 = 8'hC3, W2 = 8'h5A, W3 = 8'h3C, W4 = 8'hF0;
integer s, iv_i, base, cap;
reg [2:0] code_log [0:19];
integer cin_log [0:19], cout_log[0:19], edge_log[0:19], ann_log[0:19];
reg [DW-1:0] word_log [0:19];
integer cap_log [0:19];
integer n_iv, mutations;
integer ev_mid, ev_end;
reg [2:0] want;
integer w_edges, w_cin;
// Run one interval through the DUT and the oracle together, log, print and check.
task automatic step(input integer capn, input [DW-1:0] bits, input integer n,
input [2:0] wcode, input integer wedges, input integer wcin,
input [8*40:1] note);
begin
base = got_n;
oracle_iv(bits, n);
iv(bits, n);
code_log[n_iv] = g_code; edge_log[n_iv] = g_edges; cin_log[n_iv] = g_cin;
cout_log[n_iv] = g_cout; ann_log[n_iv] = g_words; word_log[n_iv] = g_word;
cap_log[n_iv] = capn;
$display(" %0d %2d %5d %4d %5d %3d %02h %s %s %0s",
capn, n_iv, g_edges, g_cin, g_cout, g_words, g_word[7:0],
dname(g_code), dname(wcode), note);
if (got_n - base != 1) begin
$display(" FAIL: interval %0d produced %0d verdicts for one assertion", n_iv, got_n - base);
errors = errors + 1;
end
if (g_code !== wcode) begin
$display(" FAIL: interval %0d diagnosed %s where %s was expected",
n_iv, dname(g_code), dname(wcode));
errors = errors + 1;
end
if (g_edges != wedges[CNT_W-1:0]) begin
$display(" FAIL: interval %0d counted %0d edges where %0d were driven",
n_iv, g_edges, wedges);
errors = errors + 1;
end
if (g_cin != wcin[CNT_W-1:0]) begin
$display(" FAIL: interval %0d inherited %0d orphan bits where %0d were expected",
n_iv, g_cin, wcin);
errors = errors + 1;
end
// THE ORACLE CHECK. The decoder's prediction of what a non-clearing slave would report has
// to equal an independent model's, announcement for announcement.
if (g_words != o_words[CNT_W-1:0]) begin
$display(" FAIL: interval %0d announced %0d words where the oracle announced %0d",
n_iv, g_words, o_words);
errors = errors + 1;
end
if ((o_words > 0) && (g_word[7:0] !== o_last[7:0])) begin
$display(" FAIL: interval %0d predicted word %02h where the oracle says %02h",
n_iv, g_word[7:0], o_last[7:0]);
errors = errors + 1;
end
n_iv = n_iv + 1;
end
endtask
task automatic recap;
begin
@(negedge clk);
b_cs_n = 1'b1; b_sclk = cpol; b_mosi = 1'b0;
idle_n(2); rst_n = 1'b0; idle_n(3); rst_n = 1'b1; idle_n(2);
o_acc = {DW{1'b0}}; o_nbits = 0; o_last = {DW{1'b0}}; o_words = 0;
end
endtask
initial begin
n_iv = 0; mutations = 0;
rst_n = 1'b1; @(negedge clk); rst_n = 1'b0;
repeat (4) @(negedge clk); rst_n = 1'b1; repeat (4) @(negedge clk);
$display(" cap iv edges c_in c_out ann word verdict expected note");
// ---------------- CAPTURE A: a truncation in the middle of a sequence ----------------
$display(" -- capture A: five frames, the THIRD cut short after 5 of 8 clocks");
recap;
step(0, {W0, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(0, {W1, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(0, {W2, 24'b0}, 5, D_SHORT, 5, 0, "CUT SHORT -- leaves 5 orphan bits");
step(0, {W3, 24'b0}, 8, D_INHERITED, 8, 5, "flawless frame, WRONG DATA");
step(0, {W4, 24'b0}, 8, D_INHERITED, 8, 5, "still poisoned -- only a reset clears it");
// ---------------- CAPTURE B: a missing release merges two frames ----------------
$display(" -- capture B: a MISSING release -- two words inside one assertion");
recap;
step(1, {W0, W1, 16'b0}, 16, D_MERGED, 16, 0, "16 clocks, 2 words, 1 assertion");
step(1, {W2, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(1, {W3, 24'b0}, 8, D_OK, 8, 0, "a whole word");
// ---------------- CAPTURE C: a spurious release splits one frame ----------------
$display(" -- capture C: a SPURIOUS release -- one word split across two assertions");
recap;
step(2, {W0, 24'b0}, 3, D_SHORT, 3, 0, "3 clocks then CS released");
// The second half of the SPLIT word: the five bits the first assertion did not send, so the
// two assertions together carry exactly the payload W0 and nothing else.
step(2, {W0[4:0], 27'b0}, 5, D_GLITCH, 5, 3, "5 more -- together exactly one word");
step(2, {W1, 24'b0}, 8, D_OK, 8, 0, "a whole word");
// ---------------- CAPTURE D: merged AND truncated ----------------
$display(" -- capture D: 11 clocks in one assertion -- merged and truncated at once");
recap;
step(3, {W0, W1, 16'b0}, 11, D_RAGGED, 11, 0, "one word plus 3 orphan bits");
step(3, {W2, 24'b0}, 8, D_INHERITED, 8, 3, "flawless frame, WRONG DATA");
step(3, {W3, 24'b0}, 8, D_INHERITED, 8, 3, "still poisoned");
// ---------------- CAPTURE E: the same truncation, at the END of the capture ----------------
$display(" -- capture E: the SAME truncation as capture A, positioned LAST");
recap;
step(4, {W0, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(4, {W1, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(4, {W2, 24'b0}, 5, D_SHORT, 5, 0, "CUT SHORT -- and nothing follows it");
// ================= 1. the broken frame and the wrong-data frame differ =================
if (!(code_log[2] === D_SHORT && code_log[3] === D_INHERITED && edge_log[3] == NB
&& cin_log[3] == 5)) begin
$display(" FAIL: capture A did not separate the broken frame from the wrong-data frame (%s/%s, %0d edges, %0d inherited)",
dname(code_log[2]), dname(code_log[3]), edge_log[3], cin_log[3]);
errors = errors + 1;
end
if (word_log[3][7:0] === W3) begin
$display(" FAIL: the frame after the truncation announced its own payload %02h, so no corruption occurred and the chapter's claim is untested",
W3);
errors = errors + 1;
end
$display("");
$display(" 1. interval 2 was cut short after %0d of %0d clocks. Interval 3 carried exactly %0d clocks with a clean lead, a clean lag and no structural defect of any kind -- and the word it announced was %02h where its payload was %02h, because %0d orphan bits from interval 2 were still sitting in the slave's shift register. THE FRAME WHOSE DATA IS WRONG IS NOT THE FRAME THAT IS BROKEN, and an engineer who captures the mismatching transaction captures interval 3 and finds nothing",
edge_log[2], NB, edge_log[3], word_log[3][7:0], W3, cin_log[3]);
// ================= 2. the poisoning persists =================
if (cin_log[4] != cin_log[3]) begin
$display(" FAIL: the inherited orphan count changed from %0d to %0d, so the claim that the residue persists is unsupported",
cin_log[3], cin_log[4]);
errors = errors + 1;
end
if (code_log[4] !== D_INHERITED) begin
$display(" FAIL: the second frame after the truncation was diagnosed %s rather than INHERITED",
dname(code_log[4]));
errors = errors + 1;
end
$display(" 2. interval 4 inherited the SAME %0d orphan bits and announced %02h. The residue does not drain, because the counter that holds it is cleared by reset and by nothing else -- so every word from the truncation onward is a mix and the stream stays wrong indefinitely. That is the mechanism behind `it works again after a power cycle`, which is the single most misleading sentence in a bug report, because it points at supplies and sequencing rather than at framing",
cin_log[4], word_log[4][7:0]);
// ================= 3. the sign of (asserts - words) =================
// Capture B: three assertions delivered four words. Capture C: three delivered two.
if (!(ann_log[5] == 2 && ann_log[6] == 1 && ann_log[7] == 1)) begin
$display(" FAIL: capture B did not deliver 4 words from 3 assertions (%0d/%0d/%0d)",
ann_log[5], ann_log[6], ann_log[7]);
errors = errors + 1;
end
if (!(ann_log[8] == 0 && ann_log[9] == 1 && ann_log[10] == 1)) begin
$display(" FAIL: capture C did not deliver 2 words from 3 assertions (%0d/%0d/%0d)",
ann_log[8], ann_log[9], ann_log[10]);
errors = errors + 1;
end
$display(" 3. capture B delivered %0d words from 3 assertions and capture C delivered %0d from 3. A missing release makes words OUTNUMBER assertions; a spurious release makes assertions outnumber words. Every individual edge count in capture C is defensible on its own -- 3 clocks, then 5, then 8 -- and the fault is only visible as a RATIO across the capture, which is why a per-frame checker reports three unremarkable frames",
ann_log[5] + ann_log[6] + ann_log[7], ann_log[8] + ann_log[9] + ann_log[10]);
// ================= 4. the end of a capture is a blind spot =================
ev_mid = 0; ev_end = 0;
for (s = 2; s <= 4; s = s + 1) if (code_log[s] !== D_OK) ev_mid = ev_mid + 1;
for (s = 14; s <= 16; s = s + 1) if (code_log[s] !== D_OK) ev_end = ev_end + 1;
if (code_log[16] !== code_log[2]) begin
$display(" FAIL: the truncation at the end of capture E was diagnosed %s rather than %s, so it is not the same fault",
dname(code_log[16]), dname(code_log[2]));
errors = errors + 1;
end
if (ev_end >= ev_mid) begin
$display(" FAIL: the boundary position yielded %0d pieces of evidence against %0d mid-sequence; the claim is that it yields fewer",
ev_end, ev_mid);
errors = errors + 1;
end
$display(" 4. the SAME truncation was diagnosed %s in both positions, and it produced %0d non-clean verdicts mid-sequence against %0d at the end of the capture. The structural evidence survives; the DATA symptom does not, because there is no following frame left to corrupt. So a capture that stops at the fault shows a short frame and no consequence, and a capture that stops one frame earlier shows nothing at all -- which makes `capture more than you think you need` a measured requirement rather than folklore",
dname(code_log[2]), ev_mid, ev_end);
// ================= 5. the two slave designs fail on OPPOSITE faults =================
// Not designed in advance -- capture C produced it. The split word came back CORRECT.
if (word_log[9][7:0] !== W0) begin
$display(" FAIL: the split word reassembled as %02h rather than %02h, so the claim that a spurious release is data-transparent on this slave is wrong",
word_log[9][7:0], W0);
errors = errors + 1;
end
if (word_log[3][7:0] === W3) begin
$display(" FAIL: the truncation did not corrupt the following word, so the contrast in result 5 does not exist");
errors = errors + 1;
end
$display(" 5. and the measurement produced a result that was not designed in: the SPLIT word came back CORRECT. Interval 9 announced %02h, exactly the payload interval 8 began, because a slave that does not clear on select simply carries on counting and a spurious release costs it nothing. So the two possible slave designs fail on OPPOSITE faults -- a non-clearing slave survives a glitch and is poisoned indefinitely by a truncation, while a slave that clears on every select survives a truncation by losing one word and recovering, and is corrupted by a glitch it cannot even see. There is no design that is robust against both, which means the framing discipline has to be guaranteed by the master and cannot be recovered by the slave. And it means a debugger has to know which slave is in front of them before interpreting any of these captures",
word_log[9][7:0]);
// ================= BENCH INTEGRITY =================
// Two deliberately wrong expectations, compared by the same operator as the real ones.
if (code_log[3] !== D_OK) mutations = mutations + 1;
if (cin_log[3] != 0) mutations = mutations + 1;
if (mutations != 2) begin
$display(" FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
errors = errors + 1;
end
if (x_reports != 0) begin
$display(" FAIL: %0d reported fields carried X", x_reports);
errors = errors + 1;
end
if (n_iv != 17) begin
$display(" FAIL: %0d intervals were driven where 17 were expected", n_iv);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: two deliberately wrong expectations mismatched, every reported field carried a known value, and the decoder's prediction of a non-clearing slave's output matched an independent model on all %0d intervals",
n_iv);
$display("PASS: chip-select faults are the only family in this module whose EVIDENCE lives in a different frame from its SYMPTOM. A frame cut short after %0d of %0d clocks leaves orphan bits in a slave whose bit counter is cleared by reset and not by the select -- and the NEXT frame is then structurally flawless, with a clean lead, a clean lag and exactly %0d clocks, while the word it announces is %02h instead of %02h. The frame whose data is wrong is not the frame that is broken. The residue does not drain either: interval 4 inherited the same %0d orphan bits, so the stream stays wrong until something resets the slave, which is the mechanism behind `it works after a power cycle`. Two more faults leave every individual edge count defensible and are visible only as a RATIO across the capture -- a missing release made words outnumber assertions, 4 from 3, and a spurious release made assertions outnumber words, 3 delivering 2. And because the diagnosis needs a frame AFTER the broken one, the end of a capture is a blind spot: the identical truncation yielded %0d non-clean verdicts mid-sequence and %0d at the boundary, so the buffer length is part of the instrument. One result was not designed in and is the most useful thing here: the SPLIT word came back CORRECT, %02h, because a slave that does not clear on select simply carries on counting -- so the two possible slave designs fail on OPPOSITE faults, a non-clearing slave being poisoned by a truncation it survives a glitch through and a clearing slave being corrupted by a glitch it survives a truncation through. No slave is robust against both, so the framing discipline belongs to the master",
edge_log[2], NB, edge_log[3], word_log[3][7:0], W3, cin_log[4], ev_mid, ev_end,
word_log[9][7:0]);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
endmodule// spi_cs_diag_tb.v
//
// FIVE CAPTURES, SEVENTEEN FRAMING INTERVALS, AND A SYMPTOM THAT ARRIVES ONE FRAME AFTER ITS CAUSE.
//
// THE FOUR RESULTS.
//
// 1. THE BROKEN FRAME AND THE WRONG-DATA FRAME ARE DIFFERENT FRAMES. Capture A truncates its third
// interval. The fourth interval carries exactly eight clocks, a clean lead, a clean lag and no
// structural defect of any kind -- and the word announced during it is five bits of the third
// interval's payload followed by three of its own. The bench computes that mixed byte from the
// definition and requires the decoder to predict it, so "the data is wrong here and the fault is
// there" is a measured statement with two numbers behind it.
//
// 2. THE POISONING PERSISTS. Every interval after the truncation inherits the same orphan count, so
// every word from then on is a mix. The bench requires the inherited count to be IDENTICAL two
// intervals later -- which is what makes the field symptom "it comes back after a power cycle",
// because only a reset clears the counter.
//
// 3. THE SIGN OF (ASSERTS - WORDS) SEPARATES A MERGE FROM A GLITCH. Capture B delivers four words
// from three assertions -- a missing release merged two frames. Capture C delivers two words from
// three assertions -- a spurious release split one frame in half. Both corrupt data, both leave
// every individual edge count looking defensible, and neither is visible inside one interval.
//
// 4. THE DIAGNOSIS NEEDS A FRAME AFTER THE BROKEN ONE, SO THE END OF A CAPTURE IS A BLIND SPOT.
// Capture E contains the SAME truncation as capture A, positioned last. The structural evidence
// is still there; the data symptom never appears, because there is no following frame to corrupt.
// The bench counts the evidence each position yields and requires them to differ -- which turns
// "capture a bit more than you think you need" from advice into a measurement.
`timescale 1ns/1ps
module spi_cs_diag_tb;
localparam DW = 32;
localparam NB = 8;
localparam CNT_W = 8;
localparam LEAD = 3;
localparam HALF = 2;
localparam LAG = 2;
localparam GAP = 3;
localparam [2:0] D_OK = 3'd0, D_SHORT = 3'd1, D_GLITCH = 3'd2,
D_MERGED = 3'd3, D_RAGGED = 3'd4, D_INHERITED = 3'd5;
reg clk;
always #5 clk = ~clk;
reg rst_n;
reg cpol, cpha;
reg b_sclk, b_cs_n, b_mosi;
wire dg_valid;
wire [2:0] dg_code;
wire [CNT_W-1:0] ob_edges, ob_carry_in, ob_carry_out, ob_words, ob_asserts, ob_words_tot;
wire [DW-1:0] ob_word;
spi_cs_diag #(.DW(DW), .NB(NB), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.cpol(cpol), .cpha(cpha),
.dg_valid(dg_valid), .dg_code(dg_code),
.ob_edges(ob_edges), .ob_carry_in(ob_carry_in), .ob_carry_out(ob_carry_out),
.ob_words(ob_words), .ob_word(ob_word),
.ob_asserts(ob_asserts), .ob_words_tot(ob_words_tot)
);
integer errors, x_reports, got_n;
reg [2:0] g_code;
reg [CNT_W-1:0] g_edges, g_cin, g_cout, g_words, g_asserts, g_wtot;
reg [DW-1:0] g_word;
always @(posedge clk) if (dg_valid) begin
got_n = got_n + 1;
g_code = dg_code; g_edges = ob_edges; g_cin = ob_carry_in;
g_cout = ob_carry_out; g_words = ob_words; g_word = ob_word;
g_asserts = ob_asserts; g_wtot = ob_words_tot;
if ((^dg_code === 1'bx) || (^ob_edges === 1'bx) || (^ob_carry_in === 1'bx)
|| (^ob_carry_out === 1'bx) || (^ob_words === 1'bx) || (^ob_word[7:0] === 1'bx)
|| (^ob_asserts === 1'bx) || (^ob_words_tot === 1'bx))
x_reports = x_reports + 1;
end
function [8*12:1] dname;
input [2:0] c;
begin
case (c)
D_OK: dname = "OK ";
D_SHORT: dname = "SHORT ";
D_GLITCH: dname = "GLITCH ";
D_MERGED: dname = "MERGED ";
D_RAGGED: dname = "RAGGED ";
default: dname = "INHERITED ";
endcase
end
endfunction
task idle_n;
input integer n;
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// One framing interval: assert CS, drive `n` leading edges from a LEFT-ALIGNED bit field, release.
//
// THE FIELD IS LEFT-ALIGNED FOR A REASON. SPI is MSB-first, so a frame cut short after five of
// eight clocks has transmitted the payload's TOP five bits, not its bottom five. The first version
// of this task indexed from `bits[n-1]` downwards, which sends the LOW n bits -- a truncated frame
// then carried bits the master would never have put on the wire first, and the corrupted word the
// chapter is about was a mixture of the wrong halves. The physics decides the indexing.
//
// The number of edges is the stimulus variable, because every fault in this chapter is a mismatch
// between the number of clocks inside an assertion and the word length.
task iv;
input [DW-1:0] bits;
input integer n;
integer k;
begin
b_sclk = cpol; b_mosi = 1'b0;
idle_n(2);
b_cs_n = 1'b0;
idle_n(1);
b_mosi = bits[DW-1];
idle_n(LEAD - 1);
for (k = 0; k < n; k = k + 1) begin
b_sclk = ~b_sclk;
idle_n(HALF);
b_sclk = ~b_sclk;
if (k < n-1) b_mosi = bits[DW-1-k-1];
idle_n(HALF);
end
idle_n(LAG);
b_cs_n = 1'b1;
idle_n(1);
b_sclk = cpol;
idle_n(GAP);
end
endtask
// THE INDEPENDENT ORACLE. A slave that clears its bit counter only on reset, modelled here from
// the description rather than by asking the decoder. It is fed the same stream of (bits, edges)
// pairs and announces words at exactly the same instants, so a disagreement is a real one.
reg [DW-1:0] o_acc;
integer o_nbits;
reg [DW-1:0] o_last;
integer o_words;
task oracle_iv;
input [DW-1:0] bits;
input integer n;
integer k;
begin
o_words = 0;
for (k = 0; k < n; k = k + 1) begin
o_acc = {o_acc[DW-2:0], bits[DW-1-k]};
if (o_nbits + 1 == NB) begin
o_nbits = 0; o_last = o_acc & {{(DW-NB){1'b0}}, {NB{1'b1}}}; o_words = o_words + 1;
end else begin
o_nbits = o_nbits + 1;
end
end
end
endtask
localparam [7:0] W0 = 8'h8D, W1 = 8'hC3, W2 = 8'h5A, W3 = 8'h3C, W4 = 8'hF0;
integer s, iv_i, base, cap;
reg [2:0] code_log [0:19];
integer cin_log [0:19], cout_log[0:19], edge_log[0:19], ann_log[0:19];
reg [DW-1:0] word_log [0:19];
integer cap_log [0:19];
integer n_iv, mutations;
integer ev_mid, ev_end;
reg [2:0] want;
integer w_edges, w_cin;
// Run one interval through the DUT and the oracle together, log, print and check.
task step;
input integer capn;
input [DW-1:0] bits;
input integer n;
input [2:0] wcode;
input integer wedges;
input integer wcin;
input [8*40:1] note;
begin
base = got_n;
oracle_iv(bits, n);
iv(bits, n);
code_log[n_iv] = g_code; edge_log[n_iv] = g_edges; cin_log[n_iv] = g_cin;
cout_log[n_iv] = g_cout; ann_log[n_iv] = g_words; word_log[n_iv] = g_word;
cap_log[n_iv] = capn;
$display(" %0d %2d %5d %4d %5d %3d %02h %0s %0s %0s",
capn, n_iv, g_edges, g_cin, g_cout, g_words, g_word[7:0],
dname(g_code), dname(wcode), note);
if (got_n - base != 1) begin
$display(" FAIL: interval %0d produced %0d verdicts for one assertion", n_iv, got_n - base);
errors = errors + 1;
end
if (g_code !== wcode) begin
$display(" FAIL: interval %0d diagnosed %0s where %0s was expected",
n_iv, dname(g_code), dname(wcode));
errors = errors + 1;
end
if (g_edges != wedges[CNT_W-1:0]) begin
$display(" FAIL: interval %0d counted %0d edges where %0d were driven",
n_iv, g_edges, wedges);
errors = errors + 1;
end
if (g_cin != wcin[CNT_W-1:0]) begin
$display(" FAIL: interval %0d inherited %0d orphan bits where %0d were expected",
n_iv, g_cin, wcin);
errors = errors + 1;
end
// THE ORACLE CHECK. The decoder's prediction of what a non-clearing slave would report has
// to equal an independent model's, announcement for announcement.
if (g_words != o_words[CNT_W-1:0]) begin
$display(" FAIL: interval %0d announced %0d words where the oracle announced %0d",
n_iv, g_words, o_words);
errors = errors + 1;
end
if ((o_words > 0) && (g_word[7:0] !== o_last[7:0])) begin
$display(" FAIL: interval %0d predicted word %02h where the oracle says %02h",
n_iv, g_word[7:0], o_last[7:0]);
errors = errors + 1;
end
n_iv = n_iv + 1;
end
endtask
task recap;
begin
@(negedge clk);
b_cs_n = 1'b1; b_sclk = cpol; b_mosi = 1'b0;
idle_n(2); rst_n = 1'b0; idle_n(3); rst_n = 1'b1; idle_n(2);
o_acc = {DW{1'b0}}; o_nbits = 0; o_last = {DW{1'b0}}; o_words = 0;
end
endtask
initial begin
n_iv = 0; mutations = 0;
rst_n = 1'b1; @(negedge clk); rst_n = 1'b0;
repeat (4) @(negedge clk); rst_n = 1'b1; repeat (4) @(negedge clk);
$display(" cap iv edges c_in c_out ann word verdict expected note");
// ---------------- CAPTURE A: a truncation in the middle of a sequence ----------------
$display(" -- capture A: five frames, the THIRD cut short after 5 of 8 clocks");
recap;
step(0, {W0, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(0, {W1, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(0, {W2, 24'b0}, 5, D_SHORT, 5, 0, "CUT SHORT -- leaves 5 orphan bits");
step(0, {W3, 24'b0}, 8, D_INHERITED, 8, 5, "flawless frame, WRONG DATA");
step(0, {W4, 24'b0}, 8, D_INHERITED, 8, 5, "still poisoned -- only a reset clears it");
// ---------------- CAPTURE B: a missing release merges two frames ----------------
$display(" -- capture B: a MISSING release -- two words inside one assertion");
recap;
step(1, {W0, W1, 16'b0}, 16, D_MERGED, 16, 0, "16 clocks, 2 words, 1 assertion");
step(1, {W2, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(1, {W3, 24'b0}, 8, D_OK, 8, 0, "a whole word");
// ---------------- CAPTURE C: a spurious release splits one frame ----------------
$display(" -- capture C: a SPURIOUS release -- one word split across two assertions");
recap;
step(2, {W0, 24'b0}, 3, D_SHORT, 3, 0, "3 clocks then CS released");
// The second half of the SPLIT word: the five bits the first assertion did not send, so the
// two assertions together carry exactly the payload W0 and nothing else.
step(2, {W0[4:0], 27'b0}, 5, D_GLITCH, 5, 3, "5 more -- together exactly one word");
step(2, {W1, 24'b0}, 8, D_OK, 8, 0, "a whole word");
// ---------------- CAPTURE D: merged AND truncated ----------------
$display(" -- capture D: 11 clocks in one assertion -- merged and truncated at once");
recap;
step(3, {W0, W1, 16'b0}, 11, D_RAGGED, 11, 0, "one word plus 3 orphan bits");
step(3, {W2, 24'b0}, 8, D_INHERITED, 8, 3, "flawless frame, WRONG DATA");
step(3, {W3, 24'b0}, 8, D_INHERITED, 8, 3, "still poisoned");
// ---------------- CAPTURE E: the same truncation, at the END of the capture ----------------
$display(" -- capture E: the SAME truncation as capture A, positioned LAST");
recap;
step(4, {W0, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(4, {W1, 24'b0}, 8, D_OK, 8, 0, "a whole word");
step(4, {W2, 24'b0}, 5, D_SHORT, 5, 0, "CUT SHORT -- and nothing follows it");
// ================= 1. the broken frame and the wrong-data frame differ =================
if (!(code_log[2] === D_SHORT && code_log[3] === D_INHERITED && edge_log[3] == NB
&& cin_log[3] == 5)) begin
$display(" FAIL: capture A did not separate the broken frame from the wrong-data frame (%0s/%0s, %0d edges, %0d inherited)",
dname(code_log[2]), dname(code_log[3]), edge_log[3], cin_log[3]);
errors = errors + 1;
end
if (word_log[3][7:0] === W3) begin
$display(" FAIL: the frame after the truncation announced its own payload %02h, so no corruption occurred and the chapter's claim is untested",
W3);
errors = errors + 1;
end
$display("");
$display(" 1. interval 2 was cut short after %0d of %0d clocks. Interval 3 carried exactly %0d clocks with a clean lead, a clean lag and no structural defect of any kind -- and the word it announced was %02h where its payload was %02h, because %0d orphan bits from interval 2 were still sitting in the slave's shift register. THE FRAME WHOSE DATA IS WRONG IS NOT THE FRAME THAT IS BROKEN, and an engineer who captures the mismatching transaction captures interval 3 and finds nothing",
edge_log[2], NB, edge_log[3], word_log[3][7:0], W3, cin_log[3]);
// ================= 2. the poisoning persists =================
if (cin_log[4] != cin_log[3]) begin
$display(" FAIL: the inherited orphan count changed from %0d to %0d, so the claim that the residue persists is unsupported",
cin_log[3], cin_log[4]);
errors = errors + 1;
end
if (code_log[4] !== D_INHERITED) begin
$display(" FAIL: the second frame after the truncation was diagnosed %0s rather than INHERITED",
dname(code_log[4]));
errors = errors + 1;
end
$display(" 2. interval 4 inherited the SAME %0d orphan bits and announced %02h. The residue does not drain, because the counter that holds it is cleared by reset and by nothing else -- so every word from the truncation onward is a mix and the stream stays wrong indefinitely. That is the mechanism behind `it works again after a power cycle`, which is the single most misleading sentence in a bug report, because it points at supplies and sequencing rather than at framing",
cin_log[4], word_log[4][7:0]);
// ================= 3. the sign of (asserts - words) =================
// Capture B: three assertions delivered four words. Capture C: three delivered two.
if (!(ann_log[5] == 2 && ann_log[6] == 1 && ann_log[7] == 1)) begin
$display(" FAIL: capture B did not deliver 4 words from 3 assertions (%0d/%0d/%0d)",
ann_log[5], ann_log[6], ann_log[7]);
errors = errors + 1;
end
if (!(ann_log[8] == 0 && ann_log[9] == 1 && ann_log[10] == 1)) begin
$display(" FAIL: capture C did not deliver 2 words from 3 assertions (%0d/%0d/%0d)",
ann_log[8], ann_log[9], ann_log[10]);
errors = errors + 1;
end
$display(" 3. capture B delivered %0d words from 3 assertions and capture C delivered %0d from 3. A missing release makes words OUTNUMBER assertions; a spurious release makes assertions outnumber words. Every individual edge count in capture C is defensible on its own -- 3 clocks, then 5, then 8 -- and the fault is only visible as a RATIO across the capture, which is why a per-frame checker reports three unremarkable frames",
ann_log[5] + ann_log[6] + ann_log[7], ann_log[8] + ann_log[9] + ann_log[10]);
// ================= 4. the end of a capture is a blind spot =================
ev_mid = 0; ev_end = 0;
for (s = 2; s <= 4; s = s + 1) if (code_log[s] !== D_OK) ev_mid = ev_mid + 1;
for (s = 14; s <= 16; s = s + 1) if (code_log[s] !== D_OK) ev_end = ev_end + 1;
if (code_log[16] !== code_log[2]) begin
$display(" FAIL: the truncation at the end of capture E was diagnosed %0s rather than %0s, so it is not the same fault",
dname(code_log[16]), dname(code_log[2]));
errors = errors + 1;
end
if (ev_end >= ev_mid) begin
$display(" FAIL: the boundary position yielded %0d pieces of evidence against %0d mid-sequence; the claim is that it yields fewer",
ev_end, ev_mid);
errors = errors + 1;
end
$display(" 4. the SAME truncation was diagnosed %0s in both positions, and it produced %0d non-clean verdicts mid-sequence against %0d at the end of the capture. The structural evidence survives; the DATA symptom does not, because there is no following frame left to corrupt. So a capture that stops at the fault shows a short frame and no consequence, and a capture that stops one frame earlier shows nothing at all -- which makes `capture more than you think you need` a measured requirement rather than folklore",
dname(code_log[2]), ev_mid, ev_end);
// ================= 5. the two slave designs fail on OPPOSITE faults =================
// Not designed in advance -- capture C produced it. The split word came back CORRECT.
if (word_log[9][7:0] !== W0) begin
$display(" FAIL: the split word reassembled as %02h rather than %02h, so the claim that a spurious release is data-transparent on this slave is wrong",
word_log[9][7:0], W0);
errors = errors + 1;
end
if (word_log[3][7:0] === W3) begin
$display(" FAIL: the truncation did not corrupt the following word, so the contrast in result 5 does not exist");
errors = errors + 1;
end
$display(" 5. and the measurement produced a result that was not designed in: the SPLIT word came back CORRECT. Interval 9 announced %02h, exactly the payload interval 8 began, because a slave that does not clear on select simply carries on counting and a spurious release costs it nothing. So the two possible slave designs fail on OPPOSITE faults -- a non-clearing slave survives a glitch and is poisoned indefinitely by a truncation, while a slave that clears on every select survives a truncation by losing one word and recovering, and is corrupted by a glitch it cannot even see. There is no design that is robust against both, which means the framing discipline has to be guaranteed by the master and cannot be recovered by the slave. And it means a debugger has to know which slave is in front of them before interpreting any of these captures",
word_log[9][7:0]);
// ================= BENCH INTEGRITY =================
// Two deliberately wrong expectations, compared by the same operator as the real ones.
if (code_log[3] !== D_OK) mutations = mutations + 1;
if (cin_log[3] != 0) mutations = mutations + 1;
if (mutations != 2) begin
$display(" FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
errors = errors + 1;
end
if (x_reports != 0) begin
$display(" FAIL: %0d reported fields carried X", x_reports);
errors = errors + 1;
end
if (n_iv != 17) begin
$display(" FAIL: %0d intervals were driven where 17 were expected", n_iv);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: two deliberately wrong expectations mismatched, every reported field carried a known value, and the decoder's prediction of a non-clearing slave's output matched an independent model on all %0d intervals",
n_iv);
$display("PASS: chip-select faults are the only family in this module whose EVIDENCE lives in a different frame from its SYMPTOM. A frame cut short after %0d of %0d clocks leaves orphan bits in a slave whose bit counter is cleared by reset and not by the select -- and the NEXT frame is then structurally flawless, with a clean lead, a clean lag and exactly %0d clocks, while the word it announces is %02h instead of %02h. The frame whose data is wrong is not the frame that is broken. The residue does not drain either: interval 4 inherited the same %0d orphan bits, so the stream stays wrong until something resets the slave, which is the mechanism behind `it works after a power cycle`. Two more faults leave every individual edge count defensible and are visible only as a RATIO across the capture -- a missing release made words outnumber assertions, 4 from 3, and a spurious release made assertions outnumber words, 3 delivering 2. And because the diagnosis needs a frame AFTER the broken one, the end of a capture is a blind spot: the identical truncation yielded %0d non-clean verdicts mid-sequence and %0d at the boundary, so the buffer length is part of the instrument. One result was not designed in and is the most useful thing here: the SPLIT word came back CORRECT, %02h, because a slave that does not clear on select simply carries on counting -- so the two possible slave designs fail on OPPOSITE faults, a non-clearing slave being poisoned by a truncation it survives a glitch through and a clearing slave being corrupted by a glitch it survives a truncation through. No slave is robust against both, so the framing discipline belongs to the master",
edge_log[2], NB, edge_log[3], word_log[3][7:0], W3, cin_log[4], ev_mid, ev_end,
word_log[9][7:0]);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
initial begin
cpol = 1'b0;
cpha = 1'b0;
b_sclk = 1'b0;
b_cs_n = 1'b1;
b_mosi = 1'b0;
errors = 0;
x_reports = 0;
got_n = 0;
clk = 1'b0;
rst_n = 1'b1;
end
endmodule-- spi_cs_diag_tb.vhd
--
-- FIVE CAPTURES, SEVENTEEN FRAMING INTERVALS, AND A SYMPTOM THAT ARRIVES ONE FRAME AFTER ITS CAUSE.
--
-- The same five results as the other two languages, in the same order and with the same numbers. The
-- fifth was not designed in: capture C produced it, and the third implementation is part of why it is
-- believable -- three independent spellings agreeing that a split word reassembles correctly is
-- evidence about the mechanism rather than about one simulator's scheduling.
--
-- THE ORACLE IS INDEPENDENT. `oracle_iv` models a slave that clears its bit counter only on reset,
-- written from that description rather than by calling anything the decoder uses, and it announces
-- words at the same instants. A disagreement is therefore a real one, and the bench requires agreement
-- on all seventeen intervals.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `LEAD_C`, `HALF_C`, `LAG_C`, `GAP_C`, `NB_C`, `DW_C`
-- and `W0_C`..`W4_C` all carry suffixes so no signal, variable or subprogram argument can shadow them
-- in another case. Re-read against that rule in full.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.spi_cs_pkg.all;
entity spi_cs_diag_tb is
end entity spi_cs_diag_tb;
architecture tb of spi_cs_diag_tb is
constant LEAD_C : natural := 3;
constant HALF_C : natural := 2;
constant LAG_C : natural := 2;
constant GAP_C : natural := 3;
constant NB_C : positive := 8;
constant CNT_W : positive := 8;
subtype byte_t is std_logic_vector(7 downto 0);
constant W0_C : byte_t := x"8D";
constant W1_C : byte_t := x"C3";
constant W2_C : byte_t := x"5A";
constant W3_C : byte_t := x"3C";
constant W4_C : byte_t := x"F0";
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal run : boolean := true;
signal cpol : std_logic := '0';
signal cpha : std_logic := '0';
signal b_sclk : std_logic := '0';
signal b_cs_n : std_logic := '1';
signal b_mosi : std_logic := '0';
signal dg_valid : std_logic;
signal dg_code : cs_diag_t;
signal ob_edges, ob_carry_in, ob_carry_out, ob_words : natural;
signal ob_asserts, ob_words_tot : natural;
signal ob_word : std_logic_vector(DW_C - 1 downto 0);
signal g_code : cs_diag_t := D_OK;
signal g_edges, g_cin, g_cout, g_words : natural := 0;
signal g_word : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal g_n, g_x : natural := 0;
type nat_arr is array (natural range <>) of natural;
type dg_arr is array (natural range <>) of cs_diag_t;
type byte_arr is array (natural range <>) of byte_t;
function i2s (v : integer; w : natural) return string is
constant S : string := integer'image(v);
constant P : string(1 to 40) := (others => ' ');
begin
if S'length >= w then return S; end if;
return P(1 to w - S'length) & S;
end function i2s;
function hex8 (v : byte_t) return string is
constant D : string := "0123456789abcdef";
variable r : string(1 to 2);
variable u : natural := to_integer(unsigned(v));
begin
r(1) := D(u / 16 + 1);
r(2) := D(u mod 16 + 1);
return r;
end function hex8;
function note_text (i : natural) return string is
begin
case i is
when 2 => return "CUT SHORT -- leaves 5 orphan bits";
when 16 => return "CUT SHORT -- and nothing follows it";
when 3 | 12 => return "flawless frame, WRONG DATA";
when 4 => return "still poisoned -- only a reset clears it";
when 5 => return "16 clocks, 2 words, 1 assertion";
when 8 => return "3 clocks then CS released";
when 9 => return "5 more -- together exactly one word";
when 11 => return "one word plus 3 orphan bits";
when 13 => return "still poisoned";
when others => return "a whole word";
end case;
end function note_text;
begin
clk_gen : process is
begin
while run loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process clk_gen;
dut : entity work.spi_cs_diag
generic map (NB_C => NB_C, CNT_W => CNT_W)
port map (
clk => clk, rst_n => rst_n,
sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
cpol => cpol, cpha => cpha,
dg_valid => dg_valid, dg_code => dg_code,
ob_edges => ob_edges, ob_carry_in => ob_carry_in, ob_carry_out => ob_carry_out,
ob_words => ob_words, ob_word => ob_word,
ob_asserts => ob_asserts, ob_words_tot => ob_words_tot
);
cap : process (clk) is
begin
if rising_edge(clk) then
if dg_valid = '1' then
g_code <= dg_code; g_edges <= ob_edges; g_cin <= ob_carry_in;
g_cout <= ob_carry_out; g_words <= ob_words; g_word <= ob_word;
g_n <= g_n + 1;
-- The enumeration and the naturals cannot hold a metavalue, so the announced word is
-- the only field that can be, and it is the only one guarded.
for i in 0 to 7 loop
if ob_word(i) /= '0' and ob_word(i) /= '1' then g_x <= g_x + 1; end if;
end loop;
end if;
end if;
end process cap;
stim : process is
variable code_log : dg_arr(0 to 19);
variable word_log : byte_arr(0 to 19);
variable edge_log, cin_log, cout_log, ann_log : nat_arr(0 to 19);
variable n_iv, mutations, e, base : natural := 0;
variable ev_mid, ev_end : natural := 0;
variable o_acc : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
variable o_nbits, o_words : natural := 0;
variable o_last : byte_t := (others => '0');
variable ln : line;
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;
-- One framing interval: assert CS, drive `n` leading edges from a LEFT-ALIGNED field, release.
--
-- THE FIELD IS LEFT-ALIGNED FOR A REASON. SPI is MSB-first, so a frame cut short after five of
-- eight clocks has transmitted the payload's TOP five bits, not its bottom five. Indexing from
-- the low end sends bits the master would never have put on the wire first, and the corrupted
-- word this chapter is about then mixes the wrong halves. The physics decides the indexing.
procedure iv (bits : std_logic_vector(DW_C - 1 downto 0); n : natural) is
begin
b_sclk <= cpol; b_mosi <= '0';
idle_n(2);
b_cs_n <= '0';
idle_n(1);
b_mosi <= bits(DW_C - 1);
idle_n(LEAD_C - 1);
for k in 0 to n - 1 loop
b_sclk <= not b_sclk;
idle_n(HALF_C);
b_sclk <= not b_sclk;
if k < n - 1 then b_mosi <= bits(DW_C - 1 - k - 1); end if;
idle_n(HALF_C);
end loop;
idle_n(LAG_C);
b_cs_n <= '1';
idle_n(1);
b_sclk <= cpol;
idle_n(GAP_C);
end procedure iv;
-- THE INDEPENDENT ORACLE: a slave that clears its bit counter only on reset, fed the same
-- (bits, edges) stream and announcing at the same instants.
procedure oracle_iv (bits : std_logic_vector(DW_C - 1 downto 0); n : natural) is
begin
o_words := 0;
for k in 0 to n - 1 loop
o_acc := o_acc(DW_C - 2 downto 0) & bits(DW_C - 1 - k);
if o_nbits + 1 = NB_C then
o_nbits := 0;
o_last := o_acc(7 downto 0);
o_words := o_words + 1;
else
o_nbits := o_nbits + 1;
end if;
end loop;
end procedure oracle_iv;
procedure step (capn : natural;
bits : std_logic_vector(DW_C - 1 downto 0);
n : natural;
wcode : cs_diag_t;
wedges : natural;
wcin : natural) is
begin
base := g_n;
oracle_iv(bits, n);
iv(bits, n);
code_log(n_iv) := g_code; edge_log(n_iv) := g_edges; cin_log(n_iv) := g_cin;
cout_log(n_iv) := g_cout; ann_log(n_iv) := g_words;
word_log(n_iv) := g_word(7 downto 0);
write(ln, string'(" ") & i2s(capn, 1) & string'(" ") & i2s(n_iv, 2)
& string'(" ") & i2s(g_edges, 5) & string'(" ") & i2s(g_cin, 4)
& string'(" ") & i2s(g_cout, 5) & string'(" ") & i2s(g_words, 3)
& string'(" ") & hex8(g_word(7 downto 0)) & string'(" ")
& diag_name(g_code) & string'(" ") & diag_name(wcode)
& string'(" ") & note_text(n_iv));
writeline(output, ln);
if g_n - base /= 1 then
write(ln, string'(" FAIL: interval ") & i2s(n_iv, 1) & string'(" produced ")
& i2s(g_n - base, 1) & string'(" verdicts for one assertion"));
writeline(output, ln); e := e + 1;
end if;
if g_code /= wcode then
write(ln, string'(" FAIL: interval ") & i2s(n_iv, 1) & string'(" diagnosed ")
& diag_name(g_code) & string'(" where ") & diag_name(wcode)
& string'(" was expected"));
writeline(output, ln); e := e + 1;
end if;
if g_edges /= wedges then
write(ln, string'(" FAIL: interval ") & i2s(n_iv, 1) & string'(" counted ")
& i2s(g_edges, 1) & string'(" edges where ") & i2s(wedges, 1)
& string'(" were driven"));
writeline(output, ln); e := e + 1;
end if;
if g_cin /= wcin then
write(ln, string'(" FAIL: interval ") & i2s(n_iv, 1) & string'(" inherited ")
& i2s(g_cin, 1) & string'(" orphan bits where ") & i2s(wcin, 1)
& string'(" were expected"));
writeline(output, ln); e := e + 1;
end if;
if g_words /= o_words then
write(ln, string'(" FAIL: interval ") & i2s(n_iv, 1) & string'(" announced ")
& i2s(g_words, 1) & string'(" words where the oracle announced ")
& i2s(o_words, 1));
writeline(output, ln); e := e + 1;
end if;
if o_words > 0 and g_word(7 downto 0) /= o_last then
write(ln, string'(" FAIL: interval ") & i2s(n_iv, 1) & string'(" predicted word ")
& hex8(g_word(7 downto 0)) & string'(" where the oracle says ")
& hex8(o_last));
writeline(output, ln); e := e + 1;
end if;
n_iv := n_iv + 1;
end procedure step;
procedure recap is
begin
wait until falling_edge(clk);
b_cs_n <= '1'; b_sclk <= cpol; b_mosi <= '0';
idle_n(2); rst_n <= '0'; idle_n(3); rst_n <= '1'; idle_n(2);
o_acc := (others => '0'); o_nbits := 0; o_last := (others => '0'); o_words := 0;
end procedure recap;
begin
rst_n <= '1';
wait until falling_edge(clk);
rst_n <= '0';
idle_n(4);
rst_n <= '1';
idle_n(4);
write(ln, string'(" cap iv edges c_in c_out ann word verdict expected note"));
writeline(output, ln);
-- ---------------- CAPTURE A: a truncation in the middle of a sequence ----------------
write(ln, string'(" -- capture A: five frames, the THIRD cut short after 5 of 8 clocks"));
writeline(output, ln);
recap;
step(0, W0_C & x"000000", 8, D_OK, 8, 0);
step(0, W1_C & x"000000", 8, D_OK, 8, 0);
step(0, W2_C & x"000000", 5, D_SHORT, 5, 0);
step(0, W3_C & x"000000", 8, D_INHERITED, 8, 5);
step(0, W4_C & x"000000", 8, D_INHERITED, 8, 5);
-- ---------------- CAPTURE B: a missing release merges two frames ----------------
write(ln, string'(" -- capture B: a MISSING release -- two words inside one assertion"));
writeline(output, ln);
recap;
step(1, W0_C & W1_C & x"0000", 16, D_MERGED, 16, 0);
step(1, W2_C & x"000000", 8, D_OK, 8, 0);
step(1, W3_C & x"000000", 8, D_OK, 8, 0);
-- ---------------- CAPTURE C: a spurious release splits one frame ----------------
write(ln, string'(" -- capture C: a SPURIOUS release -- one word split across two assertions"));
writeline(output, ln);
recap;
step(2, W0_C & x"000000", 3, D_SHORT, 3, 0);
-- The second half of the SPLIT word: the five bits the first assertion did not send, so the two
-- assertions together carry exactly the payload W0_C and nothing else.
step(2, W0_C(4 downto 0) & "000" & x"000000", 5, D_GLITCH, 5, 3);
step(2, W1_C & x"000000", 8, D_OK, 8, 0);
-- ---------------- CAPTURE D: merged AND truncated ----------------
write(ln, string'(" -- capture D: 11 clocks in one assertion -- merged and truncated at once"));
writeline(output, ln);
recap;
step(3, W0_C & W1_C & x"0000", 11, D_RAGGED, 11, 0);
step(3, W2_C & x"000000", 8, D_INHERITED, 8, 3);
step(3, W3_C & x"000000", 8, D_INHERITED, 8, 3);
-- ---------------- CAPTURE E: the same truncation, at the END of the capture ----------------
write(ln, string'(" -- capture E: the SAME truncation as capture A, positioned LAST"));
writeline(output, ln);
recap;
step(4, W0_C & x"000000", 8, D_OK, 8, 0);
step(4, W1_C & x"000000", 8, D_OK, 8, 0);
step(4, W2_C & x"000000", 5, D_SHORT, 5, 0);
-- ================= 1. the broken frame and the wrong-data frame differ =================
if not (code_log(2) = D_SHORT and code_log(3) = D_INHERITED
and edge_log(3) = NB_C and cin_log(3) = 5) then
write(ln, string'(" FAIL: capture A did not separate the broken frame from the wrong-data frame"));
writeline(output, ln); e := e + 1;
end if;
if word_log(3) = W3_C then
write(ln, string'(" FAIL: the frame after the truncation announced its own payload, so no corruption occurred and the chapter's claim is untested"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" 1. interval 2 was cut short after ") & i2s(edge_log(2), 1)
& string'(" of ") & i2s(NB_C, 1) & string'(" clocks. Interval 3 carried exactly ")
& i2s(edge_log(3), 1)
& string'(" clocks with a clean lead, a clean lag and no structural defect of any kind -- and the word it announced was ")
& hex8(word_log(3)) & string'(" where its payload was ") & hex8(W3_C)
& string'(", because ") & i2s(cin_log(3), 1)
& string'(" orphan bits from interval 2 were still sitting in the slave's shift register. THE FRAME WHOSE DATA IS WRONG IS NOT THE FRAME THAT IS BROKEN, and an engineer who captures the mismatching transaction captures interval 3 and finds nothing"));
writeline(output, ln);
-- ================= 2. the poisoning persists =================
if cin_log(4) /= cin_log(3) then
write(ln, string'(" FAIL: the inherited orphan count changed, so the claim that the residue persists is unsupported"));
writeline(output, ln); e := e + 1;
end if;
if code_log(4) /= D_INHERITED then
write(ln, string'(" FAIL: the second frame after the truncation was diagnosed ")
& diag_name(code_log(4)) & string'(" rather than INHERITED"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 2. interval 4 inherited the SAME ") & i2s(cin_log(4), 1)
& string'(" orphan bits and announced ") & hex8(word_log(4))
& string'(". The residue does not drain, because the counter that holds it is cleared by reset and by nothing else -- so every word from the truncation onward is a mix and the stream stays wrong indefinitely. That is the mechanism behind `it works again after a power cycle`, which is the single most misleading sentence in a bug report, because it points at supplies and sequencing rather than at framing"));
writeline(output, ln);
-- ================= 3. the sign of (asserts - words) =================
if not (ann_log(5) = 2 and ann_log(6) = 1 and ann_log(7) = 1) then
write(ln, string'(" FAIL: capture B did not deliver 4 words from 3 assertions"));
writeline(output, ln); e := e + 1;
end if;
if not (ann_log(8) = 0 and ann_log(9) = 1 and ann_log(10) = 1) then
write(ln, string'(" FAIL: capture C did not deliver 2 words from 3 assertions"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 3. capture B delivered ") & i2s(ann_log(5) + ann_log(6) + ann_log(7), 1)
& string'(" words from 3 assertions and capture C delivered ")
& i2s(ann_log(8) + ann_log(9) + ann_log(10), 1)
& string'(" from 3. A missing release makes words OUTNUMBER assertions; a spurious release makes assertions outnumber words. Every individual edge count in capture C is defensible on its own -- 3 clocks, then 5, then 8 -- and the fault is only visible as a RATIO across the capture, which is why a per-frame checker reports three unremarkable frames"));
writeline(output, ln);
-- ================= 4. the end of a capture is a blind spot =================
for s in 2 to 4 loop
if code_log(s) /= D_OK then ev_mid := ev_mid + 1; end if;
end loop;
for s in 14 to 16 loop
if code_log(s) /= D_OK then ev_end := ev_end + 1; end if;
end loop;
if code_log(16) /= code_log(2) then
write(ln, string'(" FAIL: the truncation at the end of capture E was diagnosed ")
& diag_name(code_log(16)) & string'(" rather than ") & diag_name(code_log(2))
& string'(", so it is not the same fault"));
writeline(output, ln); e := e + 1;
end if;
if ev_end >= ev_mid then
write(ln, string'(" FAIL: the boundary position yielded ") & i2s(ev_end, 1)
& string'(" pieces of evidence against ") & i2s(ev_mid, 1)
& string'(" mid-sequence; the claim is that it yields fewer"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 4. the SAME truncation was diagnosed ") & diag_name(code_log(2))
& string'(" in both positions, and it produced ") & i2s(ev_mid, 1)
& string'(" non-clean verdicts mid-sequence against ") & i2s(ev_end, 1)
& string'(" at the end of the capture. The structural evidence survives; the DATA symptom does not, because there is no following frame left to corrupt. So a capture that stops at the fault shows a short frame and no consequence, and a capture that stops one frame earlier shows nothing at all -- which makes `capture more than you think you need` a measured requirement rather than folklore"));
writeline(output, ln);
-- ================= 5. the two slave designs fail on OPPOSITE faults =================
-- Not designed in advance -- capture C produced it. The split word came back CORRECT.
if word_log(9) /= W0_C then
write(ln, string'(" FAIL: the split word reassembled as ") & hex8(word_log(9))
& string'(" rather than ") & hex8(W0_C)
& string'(", so the claim that a spurious release is data-transparent on this slave is wrong"));
writeline(output, ln); e := e + 1;
end if;
if word_log(3) = W3_C then
write(ln, string'(" FAIL: the truncation did not corrupt the following word, so the contrast in result 5 does not exist"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 5. and the measurement produced a result that was not designed in: the SPLIT word came back CORRECT. Interval 9 announced ")
& hex8(word_log(9))
& string'(", exactly the payload interval 8 began, because a slave that does not clear on select simply carries on counting and a spurious release costs it nothing. So the two possible slave designs fail on OPPOSITE faults -- a non-clearing slave survives a glitch and is poisoned indefinitely by a truncation, while a slave that clears on every select survives a truncation by losing one word and recovering, and is corrupted by a glitch it cannot even see. There is no design that is robust against both, which means the framing discipline has to be guaranteed by the master and cannot be recovered by the slave. And it means a debugger has to know which slave is in front of them before interpreting any of these captures"));
writeline(output, ln);
-- ================= BENCH INTEGRITY =================
if code_log(3) /= D_OK then mutations := mutations + 1; end if;
if cin_log(3) /= 0 then mutations := mutations + 1; end if;
if mutations /= 2 then
write(ln, string'(" FAIL: a deliberately wrong expectation did not mismatch (")
& i2s(mutations, 1) & string'(" of 2)"));
writeline(output, ln); e := e + 1;
end if;
if g_x /= 0 then
write(ln, string'(" FAIL: ") & i2s(g_x, 1) & string'(" reported fields carried X"));
writeline(output, ln); e := e + 1;
end if;
if n_iv /= 17 then
write(ln, string'(" FAIL: ") & i2s(n_iv, 1)
& string'(" intervals were driven where 17 were expected"));
writeline(output, ln); e := e + 1;
end if;
if e = 0 then
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" and the bench proved itself: two deliberately wrong expectations mismatched, every reported field carried a known value, and the decoder's prediction of a non-clearing slave's output matched an independent model on all ")
& i2s(n_iv, 1) & string'(" intervals"));
writeline(output, ln);
write(ln, string'("PASS: chip-select faults are the only family in this module whose EVIDENCE lives in a different frame from its SYMPTOM. A frame cut short after ")
& i2s(edge_log(2), 1) & string'(" of ") & i2s(NB_C, 1)
& string'(" clocks leaves orphan bits in a slave whose bit counter is cleared by reset and not by the select -- and the NEXT frame is then structurally flawless, with a clean lead, a clean lag and exactly ")
& i2s(edge_log(3), 1) & string'(" clocks, while the word it announces is ")
& hex8(word_log(3)) & string'(" instead of ") & hex8(W3_C)
& string'(". The frame whose data is wrong is not the frame that is broken. The residue does not drain either: interval 4 inherited the same ")
& i2s(cin_log(4), 1)
& string'(" orphan bits, so the stream stays wrong until something resets the slave, which is the mechanism behind `it works after a power cycle`. Two more faults leave every individual edge count defensible and are visible only as a RATIO across the capture -- a missing release made words outnumber assertions, 4 from 3, and a spurious release made assertions outnumber words, 3 delivering 2. And because the diagnosis needs a frame AFTER the broken one, the end of a capture is a blind spot: the identical truncation yielded ")
& i2s(ev_mid, 1) & string'(" non-clean verdicts mid-sequence and ")
& i2s(ev_end, 1)
& string'(" at the boundary, so the buffer length is part of the instrument. One result was not designed in and is the most useful thing here: the SPLIT word came back CORRECT, ")
& hex8(word_log(9))
& string'(", because a slave that does not clear on select simply carries on counting -- so the two possible slave designs fail on OPPOSITE faults, a non-clearing slave being poisoned by a truncation it survives a glitch through and a clearing slave being corrupted by a glitch it survives a truncation through. No slave is robust against both, so the framing discipline belongs to the master"));
writeline(output, ln);
else
write(ln, string'("FAIL: ") & i2s(e, 1) & string'(" error(s)"));
writeline(output, ln);
end if;
run <= false;
wait;
end process stim;
end architecture tb;11. The Transaction Boundary Decides Which Bugs Are Expressible
A UVM monitor for SPI almost always emits one transaction per CS assertion. It is the obvious boundary: the select frames the transfer, so the select frames the transaction.
That choice makes this bug inexpressible.
monitor → txn[N] { edges: 5, data: partial }
txn[N+1] { edges: 8, data: 0x59 } ← the mismatch is reported here
scoreboard compares txn[N+1] against expected 0x3c and reports an error
every field of txn[N+1] is correct except the payloadThe scoreboard has no access to the fact that matters, because the fact is not in either transaction — it is in the relationship between them.
12. What This Decoder Cannot Do
✗ diagnose a truncation that is the last event in the capture (no data symptom)
✗ tell a spurious release from a truncation until the FOLLOWING assertion
✗ say anything about a slave that clears its counter on select — for that design
every capture here is clean, and a glitch becomes the fatal fault instead
✗ distinguish a master that released CS early from one whose clock stopped early
— both produce an assertion carrying too few edgesThat last one is a genuine ambiguity and it is worth naming. D_SHORT says this assertion carried fewer clocks than a word. Whether the master ended the frame or the clock source stopped is not visible in the framing at all: the evidence would be in the lag between the final edge and the release, which is Chapter 18.1's EV_CSBND measurement. The two chapters compose — a short frame with a normal lag is a deliberate early release, and a short frame with a stretched lag is a clock that stopped — and neither decoder makes that inference on its own.
13. Why an FPGA Engineer Cares
Truncated frames come from one of a small number of places, and all of them are in your RTL: a bit counter that compares with == against a value it can skip past, a state machine that leaves the transfer state on a condition other than the count, a CS driven from a register written a cycle early, or a DMA that ran out of data.
The decoder is cheap — two counters and a comparator per assertion — but the more valuable output is the assertion, not the diagnostic: every CS assertion shall carry exactly NB leading edges. That is one property, it is checkable in simulation and in hardware, and it catches the fault at the source instead of catching its consequence one frame later.
And if you are debugging a board rather than a simulation: extend the capture. The one change that matters most in this chapter costs nothing but buffer depth.
14. Why an ASIC Engineer Cares
If you are building the master, this is a protocol violation you own and it is cheap to prove absent. If you are building the slave, the design decision in section 2 is yours to make and to document — and the table in section 8 says you cannot make it safely, which means the datasheet has to state which behaviour the part has.
That documentation obligation is the real deliverable. A slave that does not clear on select is not defective; it is a part with a stated requirement that the master send whole words. A slave whose datasheet is silent on the point is a part that will be integrated wrongly, and the integration failure will present as intermittent corruption cleared by a power cycle on somebody else's board, months later, with your part named in the bug report.
15. Failure Signature — "The Slave Is Unreliable"
Symptom an SPI stream is correct for a while, then every word is wrong
Trigger set on the first mismatching word
Captured that word's frame: 8 clocks, clean lead, clean lag, correct
edges, correct mode, correct bit order -- nothing wrong with it
Concluded the slave corrupts data intermittently
Escalated to the slave's vendor, who cannot reproduce it
Actual four frames earlier, a DMA underrun released CS after 5 of 8
clocks; the slave's bit counter never cleared
Found by someone who extended the capture backwards and noticed one
assertion with five clocks in itEvery step was competent. The trigger was set on the symptom, which is where you set a trigger. The captured frame was examined thoroughly, and it was genuinely faultless. The conclusion followed from the evidence available — and the evidence available was the wrong evidence, because for this fault family the symptom and the cause are in different frames and the trigger was on the symptom.
16. Common Misconceptions
| Misconception | What is actually true |
|---|---|
| The frame with wrong data is the frame with the fault | For CS faults it is usually the frame after |
| A structurally perfect frame carries correct data | Not if the slave inherited orphan bits from before it |
| "It works after a power cycle" implicates supplies | It implicates state inside the slave — a counter is first on the list |
| A slave can be made robust against CS faults | The two designs fail on opposite faults; neither is safe against both |
| A short frame is always a master releasing early | A clock that stopped early looks identical in the framing |
| One transaction per CS assertion is the natural boundary | It is, and it makes this bug inexpressible in the scoreboard |
| A trigger on the mismatch captures the fault | The fault is earlier, and it is structural rather than data |
17. Reason It Through
18. Understanding Check
19. Summary
Chip-select faults are the only family in this module whose evidence lives in a different frame from its symptom. A frame cut short after 5 of 8 clocks leaves orphan bits in a slave whose bit counter is cleared by reset and not by the select — and the next frame is then structurally flawless, with a clean lead, a clean lag and exactly eight clocks, while the word it announces is 0x59 instead of 0x3c. The frame whose data is wrong is not the frame that is broken.
The residue does not drain. The following frame inherited the same five orphan bits, so the stream stays wrong until something resets the slave — which is the mechanism behind it works after a power cycle, an observation that is entirely accurate and points at supplies rather than at framing.
Two further faults leave every individual edge count defensible and are visible only as a ratio across the capture: a missing release made words outnumber assertions, four from three, and a spurious release made assertions outnumber words, three delivering two. Because the diagnosis needs a frame after the broken one, the end of a capture is a blind spot — the identical truncation yielded three non-clean verdicts mid-sequence and one at the boundary, so buffer depth is part of the instrument.
And one result was not designed in, and is the most useful thing here: the split word came back correct, because a slave that does not clear on select simply carries on counting. So the two possible slave designs fail on opposite faults — one poisoned by a truncation it survives a glitch through, the other corrupted by a glitch it survives a truncation through. No slave is robust against both, which makes framing a master obligation and makes which slave is this a question to settle before interpreting any capture.
20. What Comes Next
Everything so far has counted things — edges, bits, displacements, assertions. Chapter 18.6 turns to read corruption on MISO, where the two candidate causes are separated by neither a count nor a value but by what happens when you slow the clock down — and where one of the two causes cannot be modelled in RTL at all, which the chapter says out loud rather than pretending otherwise.
Continue learning
Related tutorials
- Related topic
Why SPI Has No Universal Frame Format
SPI standardizes how bits are clocked and how a transfer is delimited — and nothing above that. What the bus defines, what it leaves to the device, and why the same bytes can mean two different things.
- Related topic
A Systematic Waveform Debug Method
A capture is evidence and a cause is a hypothesis; the useful work of a debug session is the measurement that converts one into the other. Five evidence classes made mutually exclusive by a published priority, with two captures that set two predicates at once so the priority is shown to be load-bearing.
- Related topic
Wrong CPOL, Wrong CPHA, Wrong Sampling Edge
Three mode faults share one symptom and two share the identical wrong byte. Separating them needs three different kinds of observation — a static level, a transition time, and an elimination — plus the discipline to decline when the payload makes the measurement impossible.
- Related topic
Bit Shifts, Bit-Order Mismatch, and Clock-Count Errors
A slip has a side, and only the wire factors it. But whether it is decidable at all is a property of the debug pattern — 0xFF lets a reversed link report a clean byte, and 0x01, the walking one, reports a rotation as a reversal.
