SPI · Module 18
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.
Chapter 18.2 exonerated or named the mode. What is left is the family of faults that move data without breaking the frame, and which everybody describes with the same four words: the bits are shifted.
The debug pattern is part of the instrument. Choose it by habit and you have chosen your instrument by habit.
1. Three Faults That All "Shift The Bits"
| Fault | Mechanism |
|---|---|
| Bit-order mismatch | one end is MSB-first, the other LSB-first |
| Rotation | an off-by-one in a shift register's load or capture |
| Clock-count error | the master issued N+1 or N−1 pulses for an N-bit payload |
Every one of them produces a structurally perfect capture — Chapter 18.1's triage returns EV_WELL for all three — carrying a permuted payload. And the phrase the bits are shifted is where the investigation usually stops, because it names a symptom that all three share.
2. A Slip Has A Side
This is the first of the chapter's two ideas, and it is the one a capture settles immediately.
The relation between what you intended and what was reported is the composition of two relations: what the master put on the wire, and what the slave did with it. A byte-level mismatch report gives you the composition. A pin capture factors it:
word_exp ──rel_tx──▶ the bits on MOSI ──rel_rx──▶ word_rx
(the master) (the slave)So the decoder below computes the relation twice. A master transmitting LSB-first and a slave receiving LSB-first produce the identical reported byte — the same mismatch, the same error counter, the same log line, and opposite ends of the link. The measurement in section 7 drives both and requires the two reported bytes to be equal before splitting them, so the discrimination is a result rather than a restatement of the stimulus.
3. Six Candidate Transforms
The decoder considers exactly six relations, and the choice is deliberate.
X_IDENT nothing happened
X_REV bit-order mismatch
X_ROTL a one-bit rotation left
X_ROTR a one-bit rotation right
X_SHL ONE EXTRA clock pulse -- shifted up, MOSI's idle level shifted in
X_SHR ONE MISSING clock pulse -- shifted down, top bit never loadedSix, and not more: widening the set to arbitrary rotations would make this decoder agree with any capture at all, which is the failure mode section 8 is about. The set is small enough that its collisions can be enumerated, and enumerating them is the chapter.
X_ROTL and X_SHL differ in exactly one bit — the one shifted in. So they are distinguishable only when that bit is observable, and that is a property of the payload rather than of the fault.
4. A Clock-Count Error Is A Shift, Not A Rotation
The ninth pulse shifts in MOSI's idle level
14 cyclesThree things follow from that figure.
The wire is clean. Over the payload's eight bit-times MOSI carried exactly the intended bits, so rel_tx is IDENT — and the fault is entirely the master's. A decoder that reported only where the data changed would blame the slave.
The receiver holds 0x1a, not a rotation of 0x8d. A rotation would have brought bit 7 around into the bottom; the extra pulse brought in MOSI's idle level instead. For 0x8d, whose top bit is 1, those differ and the fault is nameable. For a payload whose top bit is 0 they are the same value and it is not.
And the fault is doubly visible: eighteen SCLK edges is not 2N, so Chapter 18.1's triage flags EV_COUNT on the same capture. Two independent detectors agreeing on one fault is a feature, not redundancy — it is how you find out that one of them is broken.
5. Grading The Pattern
Here is the second idea, and it is the reason this chapter exists as more than a taxonomy.
Whether any of the six transforms is identifiable depends on the payload. Three one-bit properties predict most of it, and each one is derivable with a pencil:
| Property | Consequence |
|---|---|
the payload is a palindrome over len bits | reversal maps it to itself → X_REV is invisible |
| its top bit is 0 | X_ROTL and X_SHL produce the same value |
| its bottom bit is 0 | X_ROTR and X_SHR produce the same value |
So the module publishes those three bits alongside a grade: the number of distinct images the six transforms produce for this payload, from 6 (every candidate fault distinguishable) down to 1 (a capture says nothing at all). The grade is combinational in the payload — no clock, no frame, no capture — so a regression can ask whether its debug pattern is fit for purpose before it drives a single edge.
6. The Measurement — Grading Seven Patterns
No stimulus, no frames. Identical output from all three languages:
pattern grade pal msb0 lsb0 bound verdict
0x00 1 1 1 1 3 BLIND -- all six transforms give the same image
0xff 3 1 0 0 5 weak -- at least three faults share a symptom
0xaa 3 0 0 1 5 weak -- at least three faults share a symptom
0x55 3 0 1 0 5 weak -- at least three faults share a symptom
0x01 4 0 1 0 5 usable -- one pair still collides
0x0f 5 0 1 0 5 usable -- one pair still collides
0x8d 6 0 0 0 6 full -- every candidate fault is distinguishableRead the first five rows and then reflect on which patterns a bring-up script actually uses. 0x00, 0xFF, 0xAA, 0x55 and 0x01 are the five most common debug payloads in the industry, and not one of them can separate the six faults. 0x00 cannot separate any of them: all six images are 0x00, so every one of these faults reports the expected byte.
Two rows are exact — 0x0f and 0x8d — and the other five lose more than the pencil rules predict.
7. The Measurement — Seven Frames, Two Relations Each
pat wire rx rel_tx amb rel_rx amb expected_tx/rx stimulus
0x8d 0x8d 0x8d IDENT 1 IDENT 1 IDENT /IDENT a correct link, pattern 0x8d (grade 6)
0x8d 0xb1 0xb1 REV 1 IDENT 1 REV /IDENT the MASTER transmits LSB-first
0x8d 0x8d 0xb1 IDENT 1 REV 1 IDENT /REV the SLAVE receives LSB-first -- same byte as above
0x8d 0x8d 0x1a IDENT 1 SHL 1 IDENT /SHL ONE EXTRA clock pulse: clean wire, shifted receiver
0xaa 0x55 0x55 REV 4 IDENT 1 REV /IDENT 0xaa reversed: right answer, four hypotheses
0xff 0xff 0xff IDENT 4 IDENT 4 IDENT /IDENT 0xff reversed: a BROKEN link reports a clean byte
0x01 0x80 0x80 REV 2 IDENT 1 REV /IDENT 0x01 rotated right: reported REVERSED -- wrongRows 2 and 3 are the factorisation. Both report 0xb1. The rx column is identical; the wire column is not, and that single difference assigns the fault to a device.
Row 4 is the clock-count error from section 4: clean wire, rel_rx = SHL, ambiguity 1 — a unique answer, because 0x8d's top bit is 1.
Row 5 is the right answer for the wrong reason. rel_tx reads REV, which is correct, and amb reads 4: reversal, both rotations and a right shift all carry 0xaa to 0x55. The verdict is one hypothesis out of four and the module says so.
8. Two Results That Should Change How You Pick A Pattern
The practical rule is one line long, and it is the whole chapter: before trusting a slip diagnosis, grade the payload. If the grade is below 6, the capture cannot separate the candidates, and the fix is to change the stimulus rather than to stare harder at the waveform.
9. Building It — Three HDLs
Everything above is synthesizable. Bit reversal is wiring; the rotations and shifts are multiplexers; the relation set is six comparators; the grade is fifteen more.
// spi_slip_diag.sv
//
// Chapter 18.3 -- alignment faults, and the discovery that the DEBUG PATTERN is part of the
// instrument.
//
// THE FAULTS THIS CHAPTER IS ABOUT all move data without breaking the frame. Bit-order mismatch, a
// one-bit rotation, an extra or missing clock pulse: every one of them produces a well-formed
// capture carrying a permuted payload, and every one of them is routinely described as "the bits are
// shifted".
//
// TWO IDEAS, AND THE SECOND ONE IS THE CHAPTER.
//
// FIRST: A SLIP HAS A SIDE. The relation between what was intended and what was reported is the
// composition of two relations -- what the MASTER put on the wire, and what the SLAVE did with it --
// and only a pin capture can factor it. This module therefore computes the relation TWICE:
//
// rel_tx word_exp -> the bits actually captured on MOSI (the master's doing)
// rel_rx the captured bits -> word_rx (the slave's doing)
//
// A master transmitting LSB-first and a slave receiving LSB-first produce the SAME reported byte. No
// amount of care with that byte separates them. The wire does, immediately, and that is the whole
// reason a capture is worth taking.
//
// SECOND, AND THIS IS THE POINT OF THE CHAPTER: WHETHER EITHER RELATION IS DECIDABLE AT ALL IS A
// PROPERTY OF THE PATTERN, NOT OF THE INSTRUMENT.
//
// Six candidate transforms are considered. For some payloads they produce six distinct images and
// any one of them can be identified. For others they collide, and a capture cannot tell which
// transform it is looking at. For 0x00 all six images are identical and the capture says NOTHING.
//
// 0xFF reversal, rotate-left and rotate-right all map it to itself -- a broken link
// reports the expected byte and the session ends with "works fine"
// 0xAA reversal, rotate-left and rotate-right all produce 0x55 -- three faults, one symptom
// 0x01 the classic walking-one pattern: reversal and rotate-right BOTH give 0x80
//
// So the module grades the pattern as well as the capture, combinationally and without needing a
// frame, and it publishes three one-bit properties of the payload that PREDICT the collisions:
//
// pat_pal the payload is a palindrome over `len` bits -> reversal is invisible
// pat_msb0 its top bit is 0 -> rotate-left == shift-left
// pat_lsb0 its bottom bit is 0 -> rotate-right == shift-right
//
// Those three rules are derivable with a pencil, and the bench checks the derivation against the
// measured grade for seven payloads. A prediction and a measurement that must agree is a far better
// bug detector than either alone -- which is the same argument Chapter 17.4 made about reporting the
// work a solver did alongside its result.
`timescale 1ns/1ps
module spi_slip_diag #(
parameter int DW = 32,
parameter int LEN_W = 6
) (
input wire clk,
input wire rst_n,
// The pins.
input wire sclk,
input wire cs_n,
input wire mosi,
input wire cpol,
input wire cpha,
input wire [LEN_W-1:0] len,
// What the transfer should have carried, and what the receiver reported.
input wire [DW-1:0] word_exp,
input wire [DW-1:0] word_rx,
// STIMULUS GRADING. Combinational in `word_exp` and `len`: no frame, no clock, no capture. A
// bench can ask "is this pattern able to tell my candidate faults apart?" before it drives
// anything, which is the order the question ought to be asked in.
output wire [3:0] pat_grade, // distinct images under the six transforms, 1..6
output wire pat_pal,
output wire pat_msb0,
output wire pat_lsb0,
// PER-FRAME DIAGNOSIS.
output reg dg_valid,
output reg [DW-1:0] ob_word, // the bits actually captured on MOSI
output reg [2:0] rel_tx, // word_exp -> wire
output reg [2:0] rel_rx, // wire -> word_rx
output reg [3:0] amb_tx, // how many transforms explain rel_tx (1 = unique)
output reg [3:0] amb_rx
);
// The six candidate transforms, and why exactly these six.
//
// X_IDENT nothing happened
// X_REV bit-order mismatch -- one end is MSB-first and the other LSB-first
// X_ROTL a one-bit rotation left: the classic off-by-one in a shift register's load
// X_ROTR a one-bit rotation right
// X_SHL ONE EXTRA clock pulse. The receiver shifted nine times for eight bits, so it holds
// the payload shifted up with whatever MOSI was resting at shifted in.
// X_SHR ONE MISSING clock pulse: the payload shifted down, top bit never loaded.
//
// X_ROTL AND X_SHL DIFFER IN EXACTLY ONE BIT -- the one shifted in -- so a payload whose top bit
// is 0 makes a rotation and a clock-count error IDENTICAL. That is a different mechanism from
// Chapter 18.2's blind spot (there the observation did not exist; here two observations exist and
// coincide) and it is why `pat_msb0` is published as a warning rather than left implicit.
localparam [2:0] X_IDENT = 3'd0,
X_REV = 3'd1,
X_ROTL = 3'd2,
X_ROTR = 3'd3,
X_SHL = 3'd4,
X_SHR = 3'd5,
X_NONE = 3'd6; // no candidate transform explains the pair
// One transform, applied to the low `nb` bits and zero above them.
//
// Every index here is a variable index into a fixed-width vector rather than a part-select,
// because `len` is a run-time input: `w[nb-2:0]` is not legal Verilog when `nb` is not a
// constant, and writing it that way is a mistake that compiles in some tools by silently
// freezing the width.
function [DW-1:0] xf(input integer k, input [DW-1:0] w, input integer nb);
integer i;
reg [DW-1:0] r;
begin
r = {DW{1'b0}};
for (i = 0; i < DW; i = i + 1) begin
if (i < nb) begin
case (k)
0: r[i] = w[i];
1: r[i] = w[nb-1-i];
2: r[i] = (i == 0) ? w[nb-1] : w[i-1];
3: r[i] = (i == nb-1) ? w[0] : w[i+1];
4: r[i] = (i == 0) ? 1'b0 : w[i-1];
5: r[i] = (i == nb-1) ? 1'b0 : w[i+1];
default: r[i] = w[i];
endcase
end
end
xf = r;
end
endfunction
// Which transforms carry `a` to `b`. A SET, not a winner -- because the size of the set is the
// measurement that says whether a winner means anything.
function [5:0] relmask(input [DW-1:0] a, input [DW-1:0] b, input integer nb);
integer k;
reg [DW-1:0] m;
begin
m = {DW{1'b0}};
for (k = 0; k < DW; k = k + 1) if (k < nb) m[k] = 1'b1;
relmask = 6'b0;
for (k = 0; k < 6; k = k + 1)
relmask[k] = ((xf(k, a, nb) & m) == (b & m));
end
endfunction
function [3:0] popc6(input [5:0] m);
integer i;
begin
popc6 = 4'd0;
for (i = 0; i < 6; i = i + 1) popc6 = popc6 + {3'b000, m[i]};
end
endfunction
// THE PUBLISHED PRIORITY. When several transforms explain the same pair the module still reports
// one, and it reports the simplest -- identity before a permutation, a permutation before a
// clock-count error -- because a reader who ignores `amb` should at least be pointed at the
// hypothesis that requires the fewest assumptions. `amb` is the number that says whether to
// believe it, and Chapter 18.1's argument applies unchanged: a priority that is not published
// produces arguments about classification.
function [2:0] rel_of(input [5:0] m);
begin
if (m[0]) rel_of = X_IDENT;
else if (m[1]) rel_of = X_REV;
else if (m[2]) rel_of = X_ROTL;
else if (m[3]) rel_of = X_ROTR;
else if (m[4]) rel_of = X_SHL;
else if (m[5]) rel_of = X_SHR;
else rel_of = X_NONE;
end
endfunction
// How many DISTINCT images the six transforms produce for this payload. This is the grade, and it
// is a property of the stimulus alone.
function [3:0] gradef(input [DW-1:0] w, input integer nb);
integer j, k;
reg dup;
begin
gradef = 4'd0;
for (j = 0; j < 6; j = j + 1) begin
dup = 1'b0;
for (k = 0; k < 6; k = k + 1)
if (k < j && xf(k, w, nb) == xf(j, w, nb)) dup = 1'b1;
if (!dup) gradef = gradef + 4'd1;
end
end
endfunction
wire [31:0] nb = {{(32-LEN_W){1'b0}}, len};
assign pat_grade = gradef(word_exp, nb);
assign pat_pal = (xf(1, word_exp, nb) == xf(0, word_exp, nb));
assign pat_msb0 = ~word_exp[nb-1];
assign pat_lsb0 = ~word_exp[0];
// ---- the capture ----
//
// The same mechanism Chapters 18.1 and 18.2 built and justified, reduced to its essentials
// because it has been explained twice: `leading` is an SCLK change away from the idle level,
// `capture` is the leading edge for CPHA=0 and the trailing edge for CPHA=1, and a release is
// part of the transaction that is ending.
//
// ONE DETAIL IS SPECIFIC TO THIS CHAPTER. The capture is bounded by `nb`, so a frame carrying an
// EXTRA clock pulse is captured as its first `nb` bits -- the intended payload -- while the
// receiver's own shift register holds the last `nb` of nb+1. That asymmetry is exactly what makes
// a clock-count error appear as a slave-side transform with a clean wire, and it is the reason
// this module reports `rel_rx = X_SHL` for a fault that is entirely the master's.
reg sclk_d, cs_n_d;
reg [DW-1:0] acc;
reg [LEN_W:0] nseen;
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;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
acc <= {DW{1'b0}};
nseen <= {(LEN_W+1){1'b0}};
dg_valid <= 1'b0;
ob_word <= {DW{1'b0}};
rel_tx <= X_IDENT;
rel_rx <= X_IDENT;
amb_tx <= 4'd0;
amb_rx <= 4'd0;
end else begin
dg_valid <= 1'b0;
if (cs_assert) begin
acc <= {DW{1'b0}};
nseen <= {(LEN_W+1){1'b0}};
end else begin
if (capture && (nseen < nb)) begin
acc[nb - 1 - nseen] <= mosi;
nseen <= nseen + 1'b1;
end
end
if (cs_deassert) begin
dg_valid <= 1'b1;
ob_word <= acc;
// TWO RELATIONS, ONE PER SIDE OF THE PIN. This is the factorisation, and it is the
// only thing here that a byte-level log cannot reproduce.
rel_tx <= rel_of(relmask(word_exp, acc, nb));
rel_rx <= rel_of(relmask(acc, word_rx, nb));
amb_tx <= popc6(relmask(word_exp, acc, nb));
amb_rx <= popc6(relmask(acc, word_rx, nb));
end
sclk_d <= sclk;
cs_n_d <= cs_n;
end
end
endmodule// spi_slip_diag.v
//
// Chapter 18.3 -- alignment faults, and the discovery that the DEBUG PATTERN is part of the
// instrument.
//
// THE FAULTS THIS CHAPTER IS ABOUT all move data without breaking the frame. Bit-order mismatch, a
// one-bit rotation, an extra or missing clock pulse: every one of them produces a well-formed
// capture carrying a permuted payload, and every one of them is routinely described as "the bits are
// shifted".
//
// TWO IDEAS, AND THE SECOND ONE IS THE CHAPTER.
//
// FIRST: A SLIP HAS A SIDE. The relation between what was intended and what was reported is the
// composition of two relations -- what the MASTER put on the wire, and what the SLAVE did with it --
// and only a pin capture can factor it. This module therefore computes the relation TWICE:
//
// rel_tx word_exp -> the bits actually captured on MOSI (the master's doing)
// rel_rx the captured bits -> word_rx (the slave's doing)
//
// A master transmitting LSB-first and a slave receiving LSB-first produce the SAME reported byte. No
// amount of care with that byte separates them. The wire does, immediately, and that is the whole
// reason a capture is worth taking.
//
// SECOND, AND THIS IS THE POINT OF THE CHAPTER: WHETHER EITHER RELATION IS DECIDABLE AT ALL IS A
// PROPERTY OF THE PATTERN, NOT OF THE INSTRUMENT.
//
// Six candidate transforms are considered. For some payloads they produce six distinct images and
// any one of them can be identified. For others they collide, and a capture cannot tell which
// transform it is looking at. For 0x00 all six images are identical and the capture says NOTHING.
//
// 0xFF reversal, rotate-left and rotate-right all map it to itself -- a broken link
// reports the expected byte and the session ends with "works fine"
// 0xAA reversal, rotate-left and rotate-right all produce 0x55 -- three faults, one symptom
// 0x01 the classic walking-one pattern: reversal and rotate-right BOTH give 0x80
//
// So the module grades the pattern as well as the capture, combinationally and without needing a
// frame, and it publishes three one-bit properties of the payload that PREDICT the collisions:
//
// pat_pal the payload is a palindrome over `len` bits -> reversal is invisible
// pat_msb0 its top bit is 0 -> rotate-left == shift-left
// pat_lsb0 its bottom bit is 0 -> rotate-right == shift-right
//
// Those three rules are derivable with a pencil, and the bench checks the derivation against the
// measured grade for seven payloads. A prediction and a measurement that must agree is a far better
// bug detector than either alone -- which is the same argument Chapter 17.4 made about reporting the
// work a solver did alongside its result.
`timescale 1ns/1ps
module spi_slip_diag #(
parameter DW = 32,
parameter LEN_W = 6
) (
input wire clk,
input wire rst_n,
// The pins.
input wire sclk,
input wire cs_n,
input wire mosi,
input wire cpol,
input wire cpha,
input wire [LEN_W-1:0] len,
// What the transfer should have carried, and what the receiver reported.
input wire [DW-1:0] word_exp,
input wire [DW-1:0] word_rx,
// STIMULUS GRADING. Combinational in `word_exp` and `len`: no frame, no clock, no capture. A
// bench can ask "is this pattern able to tell my candidate faults apart?" before it drives
// anything, which is the order the question ought to be asked in.
output wire [3:0] pat_grade, // distinct images under the six transforms, 1..6
output wire pat_pal,
output wire pat_msb0,
output wire pat_lsb0,
// PER-FRAME DIAGNOSIS.
output reg dg_valid,
output reg [DW-1:0] ob_word, // the bits actually captured on MOSI
output reg [2:0] rel_tx, // word_exp -> wire
output reg [2:0] rel_rx, // wire -> word_rx
output reg [3:0] amb_tx, // how many transforms explain rel_tx (1 = unique)
output reg [3:0] amb_rx
);
// The six candidate transforms, and why exactly these six.
//
// X_IDENT nothing happened
// X_REV bit-order mismatch -- one end is MSB-first and the other LSB-first
// X_ROTL a one-bit rotation left: the classic off-by-one in a shift register's load
// X_ROTR a one-bit rotation right
// X_SHL ONE EXTRA clock pulse. The receiver shifted nine times for eight bits, so it holds
// the payload shifted up with whatever MOSI was resting at shifted in.
// X_SHR ONE MISSING clock pulse: the payload shifted down, top bit never loaded.
//
// X_ROTL AND X_SHL DIFFER IN EXACTLY ONE BIT -- the one shifted in -- so a payload whose top bit
// is 0 makes a rotation and a clock-count error IDENTICAL. That is a different mechanism from
// Chapter 18.2's blind spot (there the observation did not exist; here two observations exist and
// coincide) and it is why `pat_msb0` is published as a warning rather than left implicit.
localparam [2:0] X_IDENT = 3'd0,
X_REV = 3'd1,
X_ROTL = 3'd2,
X_ROTR = 3'd3,
X_SHL = 3'd4,
X_SHR = 3'd5,
X_NONE = 3'd6; // no candidate transform explains the pair
// One transform, applied to the low `nb` bits and zero above them.
//
// Every index here is a variable index into a fixed-width vector rather than a part-select,
// because `len` is a run-time input: `w[nb-2:0]` is not legal Verilog when `nb` is not a
// constant, and writing it that way is a mistake that compiles in some tools by silently
// freezing the width.
function [DW-1:0] xf;
input integer k;
input [DW-1:0] w;
input integer nb;
integer i;
reg [DW-1:0] r;
begin
r = {DW{1'b0}};
for (i = 0; i < DW; i = i + 1) begin
if (i < nb) begin
case (k)
0: r[i] = w[i];
1: r[i] = w[nb-1-i];
2: r[i] = (i == 0) ? w[nb-1] : w[i-1];
3: r[i] = (i == nb-1) ? w[0] : w[i+1];
4: r[i] = (i == 0) ? 1'b0 : w[i-1];
5: r[i] = (i == nb-1) ? 1'b0 : w[i+1];
default: r[i] = w[i];
endcase
end
end
xf = r;
end
endfunction
// Which transforms carry `a` to `b`. A SET, not a winner -- because the size of the set is the
// measurement that says whether a winner means anything.
function [5:0] relmask;
input [DW-1:0] a;
input [DW-1:0] b;
input integer nb;
integer k;
reg [DW-1:0] m;
begin
m = {DW{1'b0}};
for (k = 0; k < DW; k = k + 1) if (k < nb) m[k] = 1'b1;
relmask = 6'b0;
for (k = 0; k < 6; k = k + 1)
relmask[k] = ((xf(k, a, nb) & m) == (b & m));
end
endfunction
function [3:0] popc6;
input [5:0] m;
integer i;
begin
popc6 = 4'd0;
for (i = 0; i < 6; i = i + 1) popc6 = popc6 + {3'b000, m[i]};
end
endfunction
// THE PUBLISHED PRIORITY. When several transforms explain the same pair the module still reports
// one, and it reports the simplest -- identity before a permutation, a permutation before a
// clock-count error -- because a reader who ignores `amb` should at least be pointed at the
// hypothesis that requires the fewest assumptions. `amb` is the number that says whether to
// believe it, and Chapter 18.1's argument applies unchanged: a priority that is not published
// produces arguments about classification.
function [2:0] rel_of;
input [5:0] m;
begin
if (m[0]) rel_of = X_IDENT;
else if (m[1]) rel_of = X_REV;
else if (m[2]) rel_of = X_ROTL;
else if (m[3]) rel_of = X_ROTR;
else if (m[4]) rel_of = X_SHL;
else if (m[5]) rel_of = X_SHR;
else rel_of = X_NONE;
end
endfunction
// How many DISTINCT images the six transforms produce for this payload. This is the grade, and it
// is a property of the stimulus alone.
function [3:0] gradef;
input [DW-1:0] w;
input integer nb;
integer j, k;
reg dup;
begin
gradef = 4'd0;
for (j = 0; j < 6; j = j + 1) begin
dup = 1'b0;
for (k = 0; k < 6; k = k + 1)
if (k < j && xf(k, w, nb) == xf(j, w, nb)) dup = 1'b1;
if (!dup) gradef = gradef + 4'd1;
end
end
endfunction
wire [31:0] nb = {{(32-LEN_W){1'b0}}, len};
assign pat_grade = gradef(word_exp, nb);
assign pat_pal = (xf(1, word_exp, nb) == xf(0, word_exp, nb));
assign pat_msb0 = ~word_exp[nb-1];
assign pat_lsb0 = ~word_exp[0];
// ---- the capture ----
//
// The same mechanism Chapters 18.1 and 18.2 built and justified, reduced to its essentials
// because it has been explained twice: `leading` is an SCLK change away from the idle level,
// `capture` is the leading edge for CPHA=0 and the trailing edge for CPHA=1, and a release is
// part of the transaction that is ending.
//
// ONE DETAIL IS SPECIFIC TO THIS CHAPTER. The capture is bounded by `nb`, so a frame carrying an
// EXTRA clock pulse is captured as its first `nb` bits -- the intended payload -- while the
// receiver's own shift register holds the last `nb` of nb+1. That asymmetry is exactly what makes
// a clock-count error appear as a slave-side transform with a clean wire, and it is the reason
// this module reports `rel_rx = X_SHL` for a fault that is entirely the master's.
reg sclk_d, cs_n_d;
reg [DW-1:0] acc;
reg [LEN_W:0] nseen;
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;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
acc <= {DW{1'b0}};
nseen <= {(LEN_W+1){1'b0}};
dg_valid <= 1'b0;
ob_word <= {DW{1'b0}};
rel_tx <= X_IDENT;
rel_rx <= X_IDENT;
amb_tx <= 4'd0;
amb_rx <= 4'd0;
end else begin
dg_valid <= 1'b0;
if (cs_assert) begin
acc <= {DW{1'b0}};
nseen <= {(LEN_W+1){1'b0}};
end else begin
if (capture && (nseen < nb)) begin
acc[nb - 1 - nseen] <= mosi;
nseen <= nseen + 1'b1;
end
end
if (cs_deassert) begin
dg_valid <= 1'b1;
ob_word <= acc;
// TWO RELATIONS, ONE PER SIDE OF THE PIN. This is the factorisation, and it is the
// only thing here that a byte-level log cannot reproduce.
rel_tx <= rel_of(relmask(word_exp, acc, nb));
rel_rx <= rel_of(relmask(acc, word_rx, nb));
amb_tx <= popc6(relmask(word_exp, acc, nb));
amb_rx <= popc6(relmask(acc, word_rx, nb));
end
sclk_d <= sclk;
cs_n_d <= cs_n;
end
end
endmodule-- spi_slip_diag.vhd
--
-- Chapter 18.3 -- alignment faults, and the discovery that the DEBUG PATTERN is part of the
-- instrument.
--
-- THE FAULTS THIS CHAPTER IS ABOUT all move data without breaking the frame. Bit-order mismatch, a
-- one-bit rotation, an extra or missing clock pulse: every one of them produces a well-formed
-- capture carrying a permuted payload, and every one of them is routinely described as "the bits are
-- shifted".
--
-- TWO IDEAS, AND THE SECOND ONE IS THE CHAPTER.
--
-- FIRST: A SLIP HAS A SIDE. The relation between what was intended and what was reported is the
-- composition of two relations -- what the MASTER put on the wire, and what the SLAVE did with it --
-- and only a pin capture can factor it. This module therefore computes the relation TWICE:
--
-- rel_tx word_exp -> the bits actually captured on MOSI (the master's doing)
-- rel_rx the captured bits -> word_rx (the slave's doing)
--
-- A master transmitting LSB-first and a slave receiving LSB-first produce the SAME reported byte. No
-- amount of care with that byte separates them. The wire does, immediately, and that is the whole
-- reason a capture is worth taking.
--
-- SECOND, AND THIS IS THE POINT OF THE CHAPTER: WHETHER EITHER RELATION IS DECIDABLE AT ALL IS A
-- PROPERTY OF THE PATTERN, NOT OF THE INSTRUMENT.
--
-- Six candidate transforms are considered. For some payloads they produce six distinct images and
-- any one of them can be identified. For others they collide, and a capture cannot tell which
-- transform it is looking at. For 0x00 all six images are identical and the capture says NOTHING.
--
-- 0xFF reversal, rotate-left and rotate-right all map it to itself -- a broken link
-- reports the expected byte and the session ends with "works fine"
-- 0xAA reversal, rotate-left and rotate-right all produce 0x55 -- three faults, one symptom
-- 0x01 the classic walking-one pattern: reversal and rotate-right BOTH give 0x80
--
-- So the module grades the pattern as well as the capture, combinationally and without needing a
-- frame, and it publishes three one-bit properties of the payload that PREDICT the collisions:
--
-- pat_pal the payload is a palindrome over `len` bits -> reversal is invisible
-- pat_msb0 its top bit is 0 -> rotate-left == shift-left
-- pat_lsb0 its bottom bit is 0 -> rotate-right == shift-right
--
-- Those three rules are derivable with a pencil, and the bench checks the derivation against the
-- measured grade for seven payloads. A prediction and a measurement that must agree is a far better
-- bug detector than either alone -- which is the same argument Chapter 17.4 made about reporting the
-- work a solver did alongside its result.
--
-- WHAT THE VHDL VERSION ADDS. The six candidate transforms become an ENUMERATION and the transform
-- function takes that enumeration, so applying transform 7 is not expressible. In the Verilog
-- versions the transform selector is an `integer` and the `default` arm of its case statement exists
-- to catch a value that should not be constructible -- a defensive branch that cannot be reached and
-- therefore cannot be tested. Here the type does that work and the branch is gone.
--
-- The relation SET is a `boolean_vector`, which makes the distinction the chapter rests on visible at
-- the declaration: the module produces a set of candidate transforms and separately reports one of
-- them, and nothing about a set of size four resembles a diagnosis.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). Generics are `DW_C` and `LEN_W`; the length input is
-- `len` and the integer copy of it inside every subprogram is `nb`. No generic, port, signal,
-- variable, constant or subprogram argument here is distinguished from another only by case -- the
-- check that Chapter 17.4 learned to run after a variable `tries` silently became the generic
-- `TRIES` and a rejection loop stopped executing with no diagnostic anywhere.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package spi_slip_pkg is
constant DW_C : natural := 32;
-- The six candidates, plus the honest seventh: no candidate explains this pair.
type slip_xf_t is (X_IDENT, X_REV, X_ROTL, X_ROTR, X_SHL, X_SHR, X_NONE);
-- The relation SET. Its SIZE is the measurement; the single transform reported alongside it is a
-- convenience for a reader who only wants the simplest hypothesis.
type xf_set_t is array (0 to 5) of boolean;
function apply_xf (k : slip_xf_t; w : std_logic_vector; nb : natural)
return std_logic_vector;
function rel_set (a, b : std_logic_vector; nb : natural) return xf_set_t;
function set_size (s : xf_set_t) return natural;
function first_of (s : xf_set_t) return slip_xf_t;
function grade_of (w : std_logic_vector; nb : natural) return natural;
function xf_name (k : slip_xf_t) return string;
end package spi_slip_pkg;
package body spi_slip_pkg is
-- One transform over the low `nb` bits, zero above them.
function apply_xf (k : slip_xf_t; w : std_logic_vector; nb : natural)
return std_logic_vector is
variable r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
begin
for i in 0 to DW_C - 1 loop
if i < nb then
case k is
when X_IDENT => r(i) := w(i);
when X_REV => r(i) := w(nb - 1 - i);
when X_ROTL => if i = 0 then r(i) := w(nb - 1); else r(i) := w(i - 1); end if;
when X_ROTR => if i = nb - 1 then r(i) := w(0); else r(i) := w(i + 1); end if;
when X_SHL => if i = 0 then r(i) := '0'; else r(i) := w(i - 1); end if;
when X_SHR => if i = nb - 1 then r(i) := '0'; else r(i) := w(i + 1); end if;
when others => r(i) := w(i);
end case;
end if;
end loop;
return r;
end function apply_xf;
-- Which of the six carry `a` to `b`, compared only over the low `nb` bits.
function rel_set (a, b : std_logic_vector; nb : natural) return xf_set_t is
variable s : xf_set_t := (others => false);
variable m : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
begin
for i in 0 to DW_C - 1 loop
if i < nb then m(i) := '1'; end if;
end loop;
for j in 0 to 5 loop
s(j) := ((apply_xf(slip_xf_t'val(j), a, nb) and m) = (b and m));
end loop;
return s;
end function rel_set;
function set_size (s : xf_set_t) return natural is
variable n : natural := 0;
begin
for j in 0 to 5 loop
if s(j) then n := n + 1; end if;
end loop;
return n;
end function set_size;
-- The published priority: identity before a permutation, a permutation before a clock-count
-- error, so a reader who ignores the set size is at least pointed at the hypothesis that needs
-- the fewest assumptions.
function first_of (s : xf_set_t) return slip_xf_t is
begin
for j in 0 to 5 loop
if s(j) then return slip_xf_t'val(j); end if;
end loop;
return X_NONE;
end function first_of;
-- How many DISTINCT images the six transforms produce for this payload: a property of the
-- stimulus alone, computable before any traffic exists.
function grade_of (w : std_logic_vector; nb : natural) return natural is
variable n : natural := 0;
variable dup : boolean;
begin
for j in 0 to 5 loop
dup := false;
for k in 0 to 5 loop
if k < j and apply_xf(slip_xf_t'val(k), w, nb) = apply_xf(slip_xf_t'val(j), w, nb) then
dup := true;
end if;
end loop;
if not dup then n := n + 1; end if;
end loop;
return n;
end function grade_of;
function xf_name (k : slip_xf_t) return string is
begin
case k is
when X_IDENT => return "IDENT ";
when X_REV => return "REV ";
when X_ROTL => return "ROTL ";
when X_ROTR => return "ROTR ";
when X_SHL => return "SHL ";
when X_SHR => return "SHR ";
when others => return "NONE ";
end case;
end function xf_name;
end package body spi_slip_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_slip_pkg.all;
entity spi_slip_diag is
generic (
LEN_W : positive := 6
);
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;
len : in unsigned(LEN_W - 1 downto 0);
word_exp : in std_logic_vector(DW_C - 1 downto 0);
word_rx : in std_logic_vector(DW_C - 1 downto 0);
-- Stimulus grading: combinational, no frame required.
pat_grade : out natural;
pat_pal : out boolean;
pat_msb0 : out boolean;
pat_lsb0 : out boolean;
dg_valid : out std_logic;
ob_word : out std_logic_vector(DW_C - 1 downto 0);
rel_tx : out slip_xf_t;
rel_rx : out slip_xf_t;
amb_tx : out natural;
amb_rx : out natural
);
end entity spi_slip_diag;
architecture rtl of spi_slip_diag is
signal nb : natural := 0;
signal v_r : std_logic := '0';
signal w_r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal tx_r : slip_xf_t := X_IDENT;
signal rx_r : slip_xf_t := X_IDENT;
signal atx_r, arx_r : natural := 0;
begin
nb <= to_integer(len);
pat_grade <= grade_of(word_exp, to_integer(len));
pat_pal <= (apply_xf(X_REV, word_exp, to_integer(len))
= apply_xf(X_IDENT, word_exp, to_integer(len)));
pat_msb0 <= (word_exp(to_integer(len) - 1) = '0');
pat_lsb0 <= (word_exp(0) = '0');
dg_valid <= v_r;
ob_word <= w_r;
rel_tx <= tx_r;
rel_rx <= rx_r;
amb_tx <= atx_r;
amb_rx <= arx_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 acc : std_logic_vector(DW_C - 1 downto 0);
variable nseen : natural;
variable n : natural;
variable stx, srx : xf_set_t;
begin
if rst_n = '0' then
sclk_d := '0'; cs_n_d := '1';
acc := (others => '0'); nseen := 0;
v_r <= '0'; w_r <= (others => '0');
tx_r <= X_IDENT; rx_r <= X_IDENT; atx_r <= 0; arx_r <= 0;
elsif rising_edge(clk) then
v_r <= '0';
-- `nb` is read from the PORT rather than from the concurrent signal above, because a
-- concurrent assignment is one delta stale inside a clocked process -- the defect that
-- made two languages disagree in Chapter 17.2 and cost a full debugging session.
n := to_integer(len);
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
acc := (others => '0'); nseen := 0;
else
-- Bounded by `nb`, so a frame carrying an EXTRA clock pulse is captured as its first
-- nb bits -- the intended payload -- while the receiver's own register holds the last
-- nb of nb+1. That asymmetry is what makes a master's clock-count error present as a
-- slave-side transform over a clean wire.
if capture and nseen < n then
acc(n - 1 - nseen) := mosi;
nseen := nseen + 1;
end if;
end if;
if cs_deassert then
stx := rel_set(word_exp, acc, n);
srx := rel_set(acc, word_rx, n);
v_r <= '1';
w_r <= acc;
tx_r <= first_of(stx);
rx_r <= first_of(srx);
atx_r <= set_size(stx);
arx_r <= set_size(srx);
end if;
sclk_d := sclk;
cs_n_d := cs_n;
end if;
end process;
end architecture rtl;The Bench
// spi_slip_diag_tb.sv
//
// TWO TABLES, BECAUSE THERE ARE TWO MEASUREMENTS AND ONE OF THEM NEEDS NO STIMULUS AT ALL.
//
// TABLE A grades seven payloads. It drives no frames and needs no clock: the grade is combinational
// in the payload, which is the whole reason a bench can ask "can this pattern tell my candidate
// faults apart?" BEFORE it commits to a regression.
//
// TABLE B drives seven frames and asks, for each, what happened on each side of the pin.
//
// THE FOUR RESULTS.
//
// 1. A SLIP HAS A SIDE, AND ONLY THE WIRE REVEALS IT. A master transmitting LSB-first and a slave
// receiving LSB-first report the IDENTICAL byte. The bench requires the two reported bytes to be
// equal -- so the discrimination is a real result and not a restatement of the stimulus -- and
// then splits them on `rel_tx` against `rel_rx`.
//
// 2. THE THREE PENCIL RULES ARE SOUND BUT NOT COMPLETE. A palindrome hides reversal, a top bit of 0
// makes rotate-left and shift-left identical, a bottom bit of 0 does the same on the right. Those
// three predict an UPPER BOUND on the grade. The measurement comes in at or below it for all
// seven payloads and STRICTLY below for five of them, because a payload with period 2 is carried
// to the same image by reversal and by either rotation -- a fourth mechanism the rules do not
// express. A prediction that is checked against a measurement is worth more than either alone.
//
// 3. A BROKEN LINK REPORTS A CLEAN BYTE. With payload 0xFF a master that reverses the bit order
// puts 0xFF on the wire and the receiver reports 0xFF. Every value in the system is correct. The
// only thing that says otherwise is the AMBIGUITY COUNT -- four transforms explain the pair, so
// "nothing happened" is one of four hypotheses rather than a finding.
//
// 4. THE CLASSIC WALKING-ONE PATTERN GIVES THE WRONG ANSWER. 0x01 under a rotate-right becomes
// 0x80, which is also its reversal. The module reports REVERSED for a rotation, and it reports
// the ambiguity count 2 alongside. The bench asserts that the relation is WRONG, because writing
// down that an instrument misleads under a named condition is the only honest way to publish it.
`timescale 1ns/1ps
module spi_slip_diag_tb;
localparam int DW = 32;
localparam int LEN_W = 6;
localparam int LEAD = 4;
localparam int HALF = 3;
localparam int LAG = 2;
localparam int GAP = 4;
localparam [2:0] X_IDENT = 3'd0, X_REV = 3'd1, X_ROTL = 3'd2,
X_ROTR = 3'd3, X_SHL = 3'd4, X_SHR = 3'd5, X_NONE = 3'd6;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b1;
localparam [LEN_W-1:0] NB = 6'd8;
reg cpol = 1'b0, cpha = 1'b0;
reg [DW-1:0] word_exp = {DW{1'b0}};
reg [DW-1:0] word_rx = {DW{1'b0}};
reg b_sclk = 1'b0, b_cs_n = 1'b1, b_mosi = 1'b0;
wire [3:0] pat_grade;
wire pat_pal, pat_msb0, pat_lsb0;
wire dg_valid;
wire [DW-1:0] ob_word;
wire [2:0] rel_tx, rel_rx;
wire [3:0] amb_tx, amb_rx;
spi_slip_diag #(.DW(DW), .LEN_W(LEN_W)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.cpol(cpol), .cpha(cpha), .len(NB),
.word_exp(word_exp), .word_rx(word_rx),
.pat_grade(pat_grade), .pat_pal(pat_pal), .pat_msb0(pat_msb0), .pat_lsb0(pat_lsb0),
.dg_valid(dg_valid), .ob_word(ob_word),
.rel_tx(rel_tx), .rel_rx(rel_rx), .amb_tx(amb_tx), .amb_rx(amb_rx)
);
integer errors = 0;
integer x_reports = 0;
integer got_n = 0;
reg [2:0] g_rtx, g_rrx;
reg [3:0] g_atx, g_arx;
reg [DW-1:0] g_word;
always @(posedge clk) if (dg_valid) begin
got_n = got_n + 1;
g_rtx = rel_tx; g_rrx = rel_rx;
g_atx = amb_tx; g_arx = amb_rx;
g_word = ob_word;
if ((^rel_tx === 1'bx) || (^rel_rx === 1'bx) || (^amb_tx === 1'bx)
|| (^amb_rx === 1'bx) || (^ob_word[7:0] === 1'bx))
x_reports = x_reports + 1;
end
// THE INDEPENDENT ORACLE. The bench recomputes every transform from the definition rather than
// asking the DUT, so a mistake in the DUT's indexing cannot hide behind the DUT's own arithmetic.
// Chapter 16's rule, unchanged: measure with two implementations under one stimulus.
function [DW-1:0] xfb(input integer k, input [DW-1:0] w);
integer i;
reg [DW-1:0] r;
begin
r = {DW{1'b0}};
for (i = 0; i < NB; i = i + 1) begin
case (k)
0: r[i] = w[i];
1: r[i] = w[NB-1-i];
2: r[i] = (i == 0) ? w[NB-1] : w[i-1];
3: r[i] = (i == NB-1) ? w[0] : w[i+1];
4: r[i] = (i == 0) ? 1'b0 : w[i-1];
5: r[i] = (i == NB-1) ? 1'b0 : w[i+1];
default: r[i] = w[i];
endcase
end
xfb = r;
end
endfunction
function [8*6:1] xname(input [2:0] c);
begin
case (c)
X_IDENT: xname = "IDENT ";
X_REV: xname = "REV ";
X_ROTL: xname = "ROTL ";
X_ROTR: xname = "ROTR ";
X_SHL: xname = "SHL ";
X_SHR: xname = "SHR ";
default: xname = "NONE ";
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
// Drive one frame. `w` is what goes on the wire -- the bench applies the master-side transform
// itself, so the DUT is never handed a pre-digested answer. `extra` adds clock pulses after the
// payload without adding data, which is what a clock-count error physically is.
task automatic frame(input [DW-1:0] w, input integer extra);
integer k;
begin
b_sclk = cpol; b_mosi = 1'b0;
idle_n(2);
b_cs_n = 1'b0;
idle_n(1);
b_mosi = w[NB-1];
idle_n(LEAD - 1);
for (k = 0; k < NB + extra; k = k + 1) begin
b_sclk = ~b_sclk;
idle_n(HALF);
b_sclk = ~b_sclk;
// After the last data bit MOSI rests at 0, which is the bit an extra pulse shifts
// into the receiver -- so a clock-count error is a SHIFT and not a rotation, and the
// difference is observable only when the payload's top bit is 1.
b_mosi = (k < NB - 1) ? w[NB-1-k-1] : 1'b0;
idle_n(HALF);
end
idle_n(LAG);
b_cs_n = 1'b1;
idle_n(1);
b_sclk = cpol;
idle_n(GAP);
end
endtask
integer s, p, base;
reg [DW-1:0] PAT [0:6];
integer grade_log [0:6];
integer bound_log [0:6];
integer exact_n, strict_n;
reg [DW-1:0] rx_log [0:6];
reg [2:0] wtx, wrx;
reg [3:0] watx, warx;
reg [DW-1:0] wire_w;
integer tx_xf, rx_xf, extra;
integer mutations;
initial begin
PAT[0] = 32'h00; PAT[1] = 32'hFF; PAT[2] = 32'hAA; PAT[3] = 32'h55;
PAT[4] = 32'h01; PAT[5] = 32'h0F; PAT[6] = 32'h8D;
exact_n = 0; strict_n = 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);
// ================= TABLE A -- grade the stimulus, with no stimulus =================
$display(" pattern grade pal msb0 lsb0 bound verdict");
for (p = 0; p < 7; p = p + 1) begin
word_exp = PAT[p];
#1;
grade_log[p] = pat_grade;
bound_log[p] = 6 - ({3'b0, pat_pal} + {3'b0, pat_msb0} + {3'b0, pat_lsb0});
if (pat_grade > bound_log[p]) begin
$display(" FAIL: 0x%02h graded %0d above its predicted bound %0d",
PAT[p][7:0], pat_grade, bound_log[p]);
errors = errors + 1;
end
if (pat_grade == bound_log[p]) exact_n = exact_n + 1;
else strict_n = strict_n + 1;
$display(" 0x%02h %5d %3b %4b %4b %5d %0s",
PAT[p][7:0], pat_grade, pat_pal, pat_msb0, pat_lsb0, bound_log[p],
(pat_grade == 1) ? "BLIND -- all six transforms give the same image" :
(pat_grade < 4) ? "weak -- at least three faults share a symptom" :
(pat_grade < 6) ? "usable -- one pair still collides" :
"full -- every candidate fault is distinguishable");
end
if (grade_log[6] != 6) begin
$display(" FAIL: 0x8d graded %0d where 6 was expected; the chapter's control pattern does not discriminate",
grade_log[6]);
errors = errors + 1;
end
if (grade_log[0] != 1) begin
$display(" FAIL: 0x00 graded %0d where 1 was expected", grade_log[0]);
errors = errors + 1;
end
if (strict_n == 0) begin
$display(" FAIL: every measured grade equalled its predicted bound, so the claim that the three rules are INCOMPLETE is unsupported");
errors = errors + 1;
end
$display(" A. the three pencil rules -- palindrome hides reversal, top bit 0 merges rotate-left with shift-left, bottom bit 0 merges rotate-right with shift-right -- held as an upper bound for all seven payloads and were EXACT for only %0d of them. The other %0d lost more distinctness than the rules predict, because a payload with period 2 is carried to the same image by reversal AND by either rotation, which is a fourth mechanism the three rules do not express. The rules are sound and incomplete, and the grade has to be measured", exact_n, strict_n);
// ================= TABLE B -- seven frames, two relations each =================
$display("");
$display(" pat wire rx rel_tx amb rel_rx amb expected_tx/rx stimulus");
for (s = 0; s < 7; s = s + 1) begin
@(negedge clk);
b_sclk = cpol; b_cs_n = 1'b1; b_mosi = 1'b0;
idle_n(2); rst_n = 1'b0; idle_n(3); rst_n = 1'b1; idle_n(2);
base = got_n;
tx_xf = 0; rx_xf = 0; extra = 0;
case (s)
// A correct link on the one payload that grades 6. Everything downstream depends on
// this row: a decoder that has never been shown clean traffic has not been shown to
// be silent.
0: begin word_exp = 32'h8D; tx_xf = 0; wtx = X_IDENT; watx = 1; wrx = X_IDENT; warx = 1; end
// The MASTER transmits LSB-first.
1: begin word_exp = 32'h8D; tx_xf = 1; wtx = X_REV; watx = 1; wrx = X_IDENT; warx = 1; end
// The SLAVE receives LSB-first. Same reported byte as stimulus 1.
2: begin word_exp = 32'h8D; rx_xf = 1; wtx = X_IDENT; watx = 1; wrx = X_REV; warx = 1; end
// ONE EXTRA CLOCK PULSE. The wire is clean over the payload's eight bits and the
// receiver holds nine shifts' worth, so the fault is the master's and it presents as
// a slave-side transform. Chapter 18.1's triage sees this one too -- 18 edges is not
// 2N -- and two independent detectors agreeing on one fault is a feature.
3: begin word_exp = 32'h8D; extra = 1; wtx = X_IDENT; watx = 1; wrx = X_SHL; warx = 1; end
// The right answer for the wrong reason: 0xAA's reversal, rotate-left and
// rotate-right all give 0x55, so four transforms explain the pair.
4: begin word_exp = 32'hAA; tx_xf = 1; wtx = X_REV; watx = 4; wrx = X_IDENT; warx = 1; end
// THE FALSE NEGATIVE. 0xFF reversed is 0xFF.
5: begin word_exp = 32'hFF; tx_xf = 1; wtx = X_IDENT; watx = 4; wrx = X_IDENT; warx = 4; end
// THE WALKING ONE, AND IT LIES. rotate-right of 0x01 is 0x80, which is its reversal.
6: begin word_exp = 32'h01; tx_xf = 3; wtx = X_REV; watx = 2; wrx = X_IDENT; warx = 1; end
endcase
wire_w = xfb(tx_xf, word_exp);
// The receiver's own report, modelled by the bench: what it shifted in, then whatever it
// does with it. An extra pulse means it shifted nb+1 times and holds the top nb of them.
word_rx = xfb(rx_xf, (extra == 1) ? xfb(4, wire_w) : wire_w);
rx_log[s] = word_rx;
frame(wire_w, extra);
$display(" 0x%02h 0x%02h 0x%02h %s %3d %s %3d %s/%s %0s",
word_exp[7:0], g_word[7:0], word_rx[7:0],
xname(g_rtx), g_atx, xname(g_rrx), g_arx, xname(wtx), xname(wrx),
(s == 0) ? "a correct link, pattern 0x8d (grade 6)" :
(s == 1) ? "the MASTER transmits LSB-first" :
(s == 2) ? "the SLAVE receives LSB-first -- same byte as above" :
(s == 3) ? "ONE EXTRA clock pulse: clean wire, shifted receiver" :
(s == 4) ? "0xaa reversed: right answer, four hypotheses" :
(s == 5) ? "0xff reversed: a BROKEN link reports a clean byte" :
"0x01 rotated right: reported REVERSED -- wrong");
if (got_n - base != 1) begin
$display(" FAIL: stimulus %0d produced %0d diagnoses for one frame", s, got_n - base);
errors = errors + 1;
end
if (g_rtx !== wtx || g_atx !== watx) begin
$display(" FAIL: stimulus %0d reported rel_tx %s/%0d where %s/%0d was expected",
s, xname(g_rtx), g_atx, xname(wtx), watx);
errors = errors + 1;
end
if (g_rrx !== wrx || g_arx !== warx) begin
$display(" FAIL: stimulus %0d reported rel_rx %s/%0d where %s/%0d was expected",
s, xname(g_rrx), g_arx, xname(wrx), warx);
errors = errors + 1;
end
end
// ---- 1. a slip has a side, and only the wire reveals it ----
if (rx_log[1] !== rx_log[2]) begin
$display(" FAIL: the master-side and slave-side reversals reported different bytes (0x%02h, 0x%02h), so the claim that a byte cannot separate them was not exercised",
rx_log[1][7:0], rx_log[2][7:0]);
errors = errors + 1;
end
$display("");
$display(" 1. the master-side reversal and the slave-side reversal BOTH reported 0x%02h against an expected 0x%02h. Identical bytes, identical mismatch report, identical error counter -- and opposite ends of the link. The wire split them on the first attempt, because the relation from the expectation to the WIRE is the master's contribution and the relation from the wire to the REPORT is the slave's, and a byte-level log has neither",
rx_log[1][7:0], 8'h8D);
// ---- 3. the false negative ----
if (rx_log[5] !== 8'hFF) begin
$display(" FAIL: the 0xff reversal did not report the expected byte, so the false-negative demonstration did not occur");
errors = errors + 1;
end
$display(" 3. with pattern 0xff a master transmitting LSB-first put 0xff on the wire and the receiver reported 0xff. The expectation matched, the wire matched, the report matched, and the link was BROKEN. Nothing in the data disagreed with anything, so no comparison anywhere in a testbench or a driver could have fired. The only dissent available was the ambiguity count: %0d transforms explain the pair, so `nothing happened` was one hypothesis out of %0d rather than a finding",
4, 4);
// ---- 4. the walking one lies, and that is written down ----
if (g_rtx === X_ROTR) begin
$display(" FAIL: the walking-one pattern correctly identified a rotation, which contradicts the chapter's claim that 0x01 cannot distinguish a rotation from a reversal");
errors = errors + 1;
end
$display(" 4. the last row is the instrument giving a WRONG answer under a named condition. The true fault was a rotate-right; 0x01 rotated right is 0x80, which is also 0x01 reversed, so the module reported %s. It is not a silent failure -- the ambiguity count read 2 -- but the headline verdict named the wrong transform, and 0x01 is the pattern bring-up scripts reach for first",
xname(X_REV));
// ---- BENCH INTEGRITY ----
// A PASS proves nothing about a bench unless the bench has been shown to fail. Two
// deliberately wrong expectations, compared by the same operator as the real ones.
if (grade_log[6] != 1) mutations = mutations + 1;
if (rx_log[1] !== 8'h00) 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 (errors == 0) begin
$display("");
$display(" and the bench proved itself: two deliberately wrong expectations mismatched, and every reported field carried a known value");
$display("PASS: an alignment fault has a SIDE and the wire is what factors it -- a master transmitting LSB-first and a slave receiving LSB-first reported the identical byte 0x%02h and were separated on the first capture, because the relation from expectation to wire belongs to the master and the relation from wire to report belongs to the slave. But whether either relation is decidable is a property of the PAYLOAD, not of the instrument: the six candidate transforms produce six distinct images for 0x8d, three for 0xaa and 0x55, and ONE for 0x00, where a capture says nothing at all. Three one-bit rules predict part of that -- a palindrome hides reversal, a top bit of 0 merges rotate-left with shift-left, a bottom bit of 0 does the same on the right -- and they held as an upper bound for all seven payloads while being exact for only %0d, because periodicity costs distinctness in a way the rules do not express. Two consequences are worth the whole chapter. With 0xff a reversed link reported the expected byte and every comparison in the system passed, so the fault was invisible to values and visible only as an ambiguity count of 4. And with 0x01 -- the walking-one pattern every bring-up script starts with -- a rotate-right was reported as a reversal, a wrong answer rather than a missing one. The pattern is part of the instrument, and choosing it by habit is choosing an instrument by habit",
rx_log[1][7:0], exact_n);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
endmodule// spi_slip_diag_tb.v
//
// TWO TABLES, BECAUSE THERE ARE TWO MEASUREMENTS AND ONE OF THEM NEEDS NO STIMULUS AT ALL.
//
// TABLE A grades seven payloads. It drives no frames and needs no clock: the grade is combinational
// in the payload, which is the whole reason a bench can ask "can this pattern tell my candidate
// faults apart?" BEFORE it commits to a regression.
//
// TABLE B drives seven frames and asks, for each, what happened on each side of the pin.
//
// THE FOUR RESULTS.
//
// 1. A SLIP HAS A SIDE, AND ONLY THE WIRE REVEALS IT. A master transmitting LSB-first and a slave
// receiving LSB-first report the IDENTICAL byte. The bench requires the two reported bytes to be
// equal -- so the discrimination is a real result and not a restatement of the stimulus -- and
// then splits them on `rel_tx` against `rel_rx`.
//
// 2. THE THREE PENCIL RULES ARE SOUND BUT NOT COMPLETE. A palindrome hides reversal, a top bit of 0
// makes rotate-left and shift-left identical, a bottom bit of 0 does the same on the right. Those
// three predict an UPPER BOUND on the grade. The measurement comes in at or below it for all
// seven payloads and STRICTLY below for five of them, because a payload with period 2 is carried
// to the same image by reversal and by either rotation -- a fourth mechanism the rules do not
// express. A prediction that is checked against a measurement is worth more than either alone.
//
// 3. A BROKEN LINK REPORTS A CLEAN BYTE. With payload 0xFF a master that reverses the bit order
// puts 0xFF on the wire and the receiver reports 0xFF. Every value in the system is correct. The
// only thing that says otherwise is the AMBIGUITY COUNT -- four transforms explain the pair, so
// "nothing happened" is one of four hypotheses rather than a finding.
//
// 4. THE CLASSIC WALKING-ONE PATTERN GIVES THE WRONG ANSWER. 0x01 under a rotate-right becomes
// 0x80, which is also its reversal. The module reports REVERSED for a rotation, and it reports
// the ambiguity count 2 alongside. The bench asserts that the relation is WRONG, because writing
// down that an instrument misleads under a named condition is the only honest way to publish it.
`timescale 1ns/1ps
module spi_slip_diag_tb;
localparam DW = 32;
localparam LEN_W = 6;
localparam LEAD = 4;
localparam HALF = 3;
localparam LAG = 2;
localparam GAP = 4;
localparam [2:0] X_IDENT = 3'd0, X_REV = 3'd1, X_ROTL = 3'd2,
X_ROTR = 3'd3, X_SHL = 3'd4, X_SHR = 3'd5, X_NONE = 3'd6;
reg clk;
always #5 clk = ~clk;
reg rst_n;
localparam [LEN_W-1:0] NB = 6'd8;
reg cpol, cpha;
reg [DW-1:0] word_exp;
reg [DW-1:0] word_rx;
reg b_sclk, b_cs_n, b_mosi;
wire [3:0] pat_grade;
wire pat_pal, pat_msb0, pat_lsb0;
wire dg_valid;
wire [DW-1:0] ob_word;
wire [2:0] rel_tx, rel_rx;
wire [3:0] amb_tx, amb_rx;
spi_slip_diag #(.DW(DW), .LEN_W(LEN_W)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.cpol(cpol), .cpha(cpha), .len(NB),
.word_exp(word_exp), .word_rx(word_rx),
.pat_grade(pat_grade), .pat_pal(pat_pal), .pat_msb0(pat_msb0), .pat_lsb0(pat_lsb0),
.dg_valid(dg_valid), .ob_word(ob_word),
.rel_tx(rel_tx), .rel_rx(rel_rx), .amb_tx(amb_tx), .amb_rx(amb_rx)
);
integer errors;
integer x_reports;
integer got_n;
reg [2:0] g_rtx, g_rrx;
reg [3:0] g_atx, g_arx;
reg [DW-1:0] g_word;
always @(posedge clk) if (dg_valid) begin
got_n = got_n + 1;
g_rtx = rel_tx; g_rrx = rel_rx;
g_atx = amb_tx; g_arx = amb_rx;
g_word = ob_word;
if ((^rel_tx === 1'bx) || (^rel_rx === 1'bx) || (^amb_tx === 1'bx)
|| (^amb_rx === 1'bx) || (^ob_word[7:0] === 1'bx))
x_reports = x_reports + 1;
end
// THE INDEPENDENT ORACLE. The bench recomputes every transform from the definition rather than
// asking the DUT, so a mistake in the DUT's indexing cannot hide behind the DUT's own arithmetic.
// Chapter 16's rule, unchanged: measure with two implementations under one stimulus.
function [DW-1:0] xfb;
input integer k;
input [DW-1:0] w;
integer i;
reg [DW-1:0] r;
begin
r = {DW{1'b0}};
for (i = 0; i < NB; i = i + 1) begin
case (k)
0: r[i] = w[i];
1: r[i] = w[NB-1-i];
2: r[i] = (i == 0) ? w[NB-1] : w[i-1];
3: r[i] = (i == NB-1) ? w[0] : w[i+1];
4: r[i] = (i == 0) ? 1'b0 : w[i-1];
5: r[i] = (i == NB-1) ? 1'b0 : w[i+1];
default: r[i] = w[i];
endcase
end
xfb = r;
end
endfunction
function [8*6:1] xname;
input [2:0] c;
begin
case (c)
X_IDENT: xname = "IDENT ";
X_REV: xname = "REV ";
X_ROTL: xname = "ROTL ";
X_ROTR: xname = "ROTR ";
X_SHL: xname = "SHL ";
X_SHR: xname = "SHR ";
default: xname = "NONE ";
endcase
end
endfunction
task idle_n;
input integer n;
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// Drive one frame. `w` is what goes on the wire -- the bench applies the master-side transform
// itself, so the DUT is never handed a pre-digested answer. `extra` adds clock pulses after the
// payload without adding data, which is what a clock-count error physically is.
task frame;
input [DW-1:0] w;
input integer extra;
integer k;
begin
b_sclk = cpol; b_mosi = 1'b0;
idle_n(2);
b_cs_n = 1'b0;
idle_n(1);
b_mosi = w[NB-1];
idle_n(LEAD - 1);
for (k = 0; k < NB + extra; k = k + 1) begin
b_sclk = ~b_sclk;
idle_n(HALF);
b_sclk = ~b_sclk;
// After the last data bit MOSI rests at 0, which is the bit an extra pulse shifts
// into the receiver -- so a clock-count error is a SHIFT and not a rotation, and the
// difference is observable only when the payload's top bit is 1.
b_mosi = (k < NB - 1) ? w[NB-1-k-1] : 1'b0;
idle_n(HALF);
end
idle_n(LAG);
b_cs_n = 1'b1;
idle_n(1);
b_sclk = cpol;
idle_n(GAP);
end
endtask
integer s, p, base;
reg [DW-1:0] PAT [0:6];
integer grade_log [0:6];
integer bound_log [0:6];
integer exact_n, strict_n;
reg [DW-1:0] rx_log [0:6];
reg [2:0] wtx, wrx;
reg [3:0] watx, warx;
reg [DW-1:0] wire_w;
integer tx_xf, rx_xf, extra;
integer mutations;
initial begin
PAT[0] = 32'h00; PAT[1] = 32'hFF; PAT[2] = 32'hAA; PAT[3] = 32'h55;
PAT[4] = 32'h01; PAT[5] = 32'h0F; PAT[6] = 32'h8D;
exact_n = 0; strict_n = 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);
// ================= TABLE A -- grade the stimulus, with no stimulus =================
$display(" pattern grade pal msb0 lsb0 bound verdict");
for (p = 0; p < 7; p = p + 1) begin
word_exp = PAT[p];
#1;
grade_log[p] = pat_grade;
bound_log[p] = 6 - ({3'b0, pat_pal} + {3'b0, pat_msb0} + {3'b0, pat_lsb0});
if (pat_grade > bound_log[p]) begin
$display(" FAIL: 0x%02h graded %0d above its predicted bound %0d",
PAT[p][7:0], pat_grade, bound_log[p]);
errors = errors + 1;
end
if (pat_grade == bound_log[p]) exact_n = exact_n + 1;
else strict_n = strict_n + 1;
$display(" 0x%02h %5d %3b %4b %4b %5d %0s",
PAT[p][7:0], pat_grade, pat_pal, pat_msb0, pat_lsb0, bound_log[p],
(pat_grade == 1) ? "BLIND -- all six transforms give the same image" :
(pat_grade < 4) ? "weak -- at least three faults share a symptom" :
(pat_grade < 6) ? "usable -- one pair still collides" :
"full -- every candidate fault is distinguishable");
end
if (grade_log[6] != 6) begin
$display(" FAIL: 0x8d graded %0d where 6 was expected; the chapter's control pattern does not discriminate",
grade_log[6]);
errors = errors + 1;
end
if (grade_log[0] != 1) begin
$display(" FAIL: 0x00 graded %0d where 1 was expected", grade_log[0]);
errors = errors + 1;
end
if (strict_n == 0) begin
$display(" FAIL: every measured grade equalled its predicted bound, so the claim that the three rules are INCOMPLETE is unsupported");
errors = errors + 1;
end
$display(" A. the three pencil rules -- palindrome hides reversal, top bit 0 merges rotate-left with shift-left, bottom bit 0 merges rotate-right with shift-right -- held as an upper bound for all seven payloads and were EXACT for only %0d of them. The other %0d lost more distinctness than the rules predict, because a payload with period 2 is carried to the same image by reversal AND by either rotation, which is a fourth mechanism the three rules do not express. The rules are sound and incomplete, and the grade has to be measured", exact_n, strict_n);
// ================= TABLE B -- seven frames, two relations each =================
$display("");
$display(" pat wire rx rel_tx amb rel_rx amb expected_tx/rx stimulus");
for (s = 0; s < 7; s = s + 1) begin
@(negedge clk);
b_sclk = cpol; b_cs_n = 1'b1; b_mosi = 1'b0;
idle_n(2); rst_n = 1'b0; idle_n(3); rst_n = 1'b1; idle_n(2);
base = got_n;
tx_xf = 0; rx_xf = 0; extra = 0;
case (s)
// A correct link on the one payload that grades 6. Everything downstream depends on
// this row: a decoder that has never been shown clean traffic has not been shown to
// be silent.
0: begin word_exp = 32'h8D; tx_xf = 0; wtx = X_IDENT; watx = 1; wrx = X_IDENT; warx = 1; end
// The MASTER transmits LSB-first.
1: begin word_exp = 32'h8D; tx_xf = 1; wtx = X_REV; watx = 1; wrx = X_IDENT; warx = 1; end
// The SLAVE receives LSB-first. Same reported byte as stimulus 1.
2: begin word_exp = 32'h8D; rx_xf = 1; wtx = X_IDENT; watx = 1; wrx = X_REV; warx = 1; end
// ONE EXTRA CLOCK PULSE. The wire is clean over the payload's eight bits and the
// receiver holds nine shifts' worth, so the fault is the master's and it presents as
// a slave-side transform. Chapter 18.1's triage sees this one too -- 18 edges is not
// 2N -- and two independent detectors agreeing on one fault is a feature.
3: begin word_exp = 32'h8D; extra = 1; wtx = X_IDENT; watx = 1; wrx = X_SHL; warx = 1; end
// The right answer for the wrong reason: 0xAA's reversal, rotate-left and
// rotate-right all give 0x55, so four transforms explain the pair.
4: begin word_exp = 32'hAA; tx_xf = 1; wtx = X_REV; watx = 4; wrx = X_IDENT; warx = 1; end
// THE FALSE NEGATIVE. 0xFF reversed is 0xFF.
5: begin word_exp = 32'hFF; tx_xf = 1; wtx = X_IDENT; watx = 4; wrx = X_IDENT; warx = 4; end
// THE WALKING ONE, AND IT LIES. rotate-right of 0x01 is 0x80, which is its reversal.
6: begin word_exp = 32'h01; tx_xf = 3; wtx = X_REV; watx = 2; wrx = X_IDENT; warx = 1; end
endcase
wire_w = xfb(tx_xf, word_exp);
// The receiver's own report, modelled by the bench: what it shifted in, then whatever it
// does with it. An extra pulse means it shifted nb+1 times and holds the top nb of them.
word_rx = xfb(rx_xf, (extra == 1) ? xfb(4, wire_w) : wire_w);
rx_log[s] = word_rx;
frame(wire_w, extra);
$display(" 0x%02h 0x%02h 0x%02h %0s %3d %0s %3d %0s/%0s %0s",
word_exp[7:0], g_word[7:0], word_rx[7:0],
xname(g_rtx), g_atx, xname(g_rrx), g_arx, xname(wtx), xname(wrx),
(s == 0) ? "a correct link, pattern 0x8d (grade 6)" :
(s == 1) ? "the MASTER transmits LSB-first" :
(s == 2) ? "the SLAVE receives LSB-first -- same byte as above" :
(s == 3) ? "ONE EXTRA clock pulse: clean wire, shifted receiver" :
(s == 4) ? "0xaa reversed: right answer, four hypotheses" :
(s == 5) ? "0xff reversed: a BROKEN link reports a clean byte" :
"0x01 rotated right: reported REVERSED -- wrong");
if (got_n - base != 1) begin
$display(" FAIL: stimulus %0d produced %0d diagnoses for one frame", s, got_n - base);
errors = errors + 1;
end
if (g_rtx !== wtx || g_atx !== watx) begin
$display(" FAIL: stimulus %0d reported rel_tx %0s/%0d where %0s/%0d was expected",
s, xname(g_rtx), g_atx, xname(wtx), watx);
errors = errors + 1;
end
if (g_rrx !== wrx || g_arx !== warx) begin
$display(" FAIL: stimulus %0d reported rel_rx %0s/%0d where %0s/%0d was expected",
s, xname(g_rrx), g_arx, xname(wrx), warx);
errors = errors + 1;
end
end
// ---- 1. a slip has a side, and only the wire reveals it ----
if (rx_log[1] !== rx_log[2]) begin
$display(" FAIL: the master-side and slave-side reversals reported different bytes (0x%02h, 0x%02h), so the claim that a byte cannot separate them was not exercised",
rx_log[1][7:0], rx_log[2][7:0]);
errors = errors + 1;
end
$display("");
$display(" 1. the master-side reversal and the slave-side reversal BOTH reported 0x%02h against an expected 0x%02h. Identical bytes, identical mismatch report, identical error counter -- and opposite ends of the link. The wire split them on the first attempt, because the relation from the expectation to the WIRE is the master's contribution and the relation from the wire to the REPORT is the slave's, and a byte-level log has neither",
rx_log[1][7:0], 8'h8D);
// ---- 3. the false negative ----
if (rx_log[5] !== 8'hFF) begin
$display(" FAIL: the 0xff reversal did not report the expected byte, so the false-negative demonstration did not occur");
errors = errors + 1;
end
$display(" 3. with pattern 0xff a master transmitting LSB-first put 0xff on the wire and the receiver reported 0xff. The expectation matched, the wire matched, the report matched, and the link was BROKEN. Nothing in the data disagreed with anything, so no comparison anywhere in a testbench or a driver could have fired. The only dissent available was the ambiguity count: %0d transforms explain the pair, so `nothing happened` was one hypothesis out of %0d rather than a finding",
4, 4);
// ---- 4. the walking one lies, and that is written down ----
if (g_rtx === X_ROTR) begin
$display(" FAIL: the walking-one pattern correctly identified a rotation, which contradicts the chapter's claim that 0x01 cannot distinguish a rotation from a reversal");
errors = errors + 1;
end
$display(" 4. the last row is the instrument giving a WRONG answer under a named condition. The true fault was a rotate-right; 0x01 rotated right is 0x80, which is also 0x01 reversed, so the module reported %0s. It is not a silent failure -- the ambiguity count read 2 -- but the headline verdict named the wrong transform, and 0x01 is the pattern bring-up scripts reach for first",
xname(X_REV));
// ---- BENCH INTEGRITY ----
// A PASS proves nothing about a bench unless the bench has been shown to fail. Two
// deliberately wrong expectations, compared by the same operator as the real ones.
if (grade_log[6] != 1) mutations = mutations + 1;
if (rx_log[1] !== 8'h00) 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 (errors == 0) begin
$display("");
$display(" and the bench proved itself: two deliberately wrong expectations mismatched, and every reported field carried a known value");
$display("PASS: an alignment fault has a SIDE and the wire is what factors it -- a master transmitting LSB-first and a slave receiving LSB-first reported the identical byte 0x%02h and were separated on the first capture, because the relation from expectation to wire belongs to the master and the relation from wire to report belongs to the slave. But whether either relation is decidable is a property of the PAYLOAD, not of the instrument: the six candidate transforms produce six distinct images for 0x8d, three for 0xaa and 0x55, and ONE for 0x00, where a capture says nothing at all. Three one-bit rules predict part of that -- a palindrome hides reversal, a top bit of 0 merges rotate-left with shift-left, a bottom bit of 0 does the same on the right -- and they held as an upper bound for all seven payloads while being exact for only %0d, because periodicity costs distinctness in a way the rules do not express. Two consequences are worth the whole chapter. With 0xff a reversed link reported the expected byte and every comparison in the system passed, so the fault was invisible to values and visible only as an ambiguity count of 4. And with 0x01 -- the walking-one pattern every bring-up script starts with -- a rotate-right was reported as a reversal, a wrong answer rather than a missing one. The pattern is part of the instrument, and choosing it by habit is choosing an instrument by habit",
rx_log[1][7:0], exact_n);
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;
clk = 1'b0;
rst_n = 1'b1;
word_exp = {DW{1'b0}};
word_rx = {DW{1'b0}};
errors = 0;
x_reports = 0;
got_n = 0;
end
endmodule-- spi_slip_diag_tb.vhd
--
-- TWO TABLES, BECAUSE THERE ARE TWO MEASUREMENTS AND ONE OF THEM NEEDS NO STIMULUS AT ALL.
--
-- Table A grades seven payloads with no frames and no captures -- the grade is a pure function of the
-- payload, so a regression can ask whether its debug pattern is capable of separating its candidate
-- faults before it drives a single edge. Table B drives seven frames and factors each failure into
-- what the master did and what the slave did.
--
-- THE SAME FOUR RESULTS AS THE OTHER TWO LANGUAGES, AND THE SAME NUMBERS. The third implementation is
-- not ceremony: the VHDL port of Chapter 16.5 found a defect that two Verilog suites had agreed on,
-- and three independent spellings producing one table is the only available evidence that the table is
-- a property of the reasoning rather than of one simulator's scheduling.
--
-- THE ORACLE IS INDEPENDENT. `xfb` recomputes every transform from the definition instead of calling
-- the package the DUT uses, so an indexing mistake cannot hide behind its own arithmetic.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `LEAD_C`, `HALF_C`, `LAG_C`, `GAP_C`, `NB_C` and
-- `DW_C` all carry the `_C` suffix so none of them can be shadowed by a signal, variable or
-- subprogram argument spelled the same way 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_slip_pkg.all;
entity spi_slip_diag_tb is
end entity spi_slip_diag_tb;
architecture tb of spi_slip_diag_tb is
constant LEAD_C : natural := 4;
constant HALF_C : natural := 3;
constant LAG_C : natural := 2;
constant GAP_C : natural := 4;
constant NB_C : positive := 8;
constant LEN_W : positive := 6;
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 word_exp : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal word_rx : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal b_sclk : std_logic := '0';
signal b_cs_n : std_logic := '1';
signal b_mosi : std_logic := '0';
signal pat_grade : natural;
signal pat_pal, pat_msb0, pat_lsb0 : boolean;
signal dg_valid : std_logic;
signal ob_word : std_logic_vector(DW_C - 1 downto 0);
signal rel_tx, rel_rx : slip_xf_t;
signal amb_tx, amb_rx : natural;
-- One driver for the capture, so no record or signal here can resolve to 'X' through two
-- writers -- the structural rule adopted after Chapter 16.3.
signal g_rtx, g_rrx : slip_xf_t := X_IDENT;
signal g_atx, g_arx : natural := 0;
signal g_word : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal g_n : natural := 0;
signal g_x : natural := 0;
type pat_arr is array (natural range <>) of std_logic_vector(DW_C - 1 downto 0);
type nat_arr is array (natural range <>) of natural;
type xf_arr is array (natural range <>) of slip_xf_t;
constant PAT : pat_arr(0 to 6) :=
(x"00000000", x"000000FF", x"000000AA", x"00000055",
x"00000001", x"0000000F", x"0000008D");
-- Right-justify an integer. `%-30s` is not portable across the three simulators -- one pads and one
-- does not -- which is why every free-text column in this corpus sits at the END of a row and every
-- numeric column is justified explicitly here.
--
-- THE LENGTH IS TAKEN FROM THE IMAGE, not assumed. The first version of this helper concatenated a
-- fixed run of spaces onto `integer'image(v)` and declared the result a 24-character constant, which
-- is a FATAL length mismatch the moment the value needs two digits -- and it survived two chapters
-- only because every number they printed was a single digit. A constant whose length depends on its
-- initialiser must be left unconstrained.
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 b2s (b : boolean; w : natural) return string is
constant P : string(1 to 24) := (others => ' ');
begin
if b then return P(1 to w - 1) & "1"; else return P(1 to w - 1) & "0"; end if;
end function b2s;
-- THE FORMAL IS CONSTRAINED, and it has to be. An unconstrained formal inherits the actual's index
-- range, and a CONCATENATION produces an ascending `0 to n-1` range -- so `v(7 downto 0)` inside a
-- function called with `x"00" & something` is a null slice and an index fault at run time, not a
-- compile error. Writing the range into the declaration makes the indexing a property of this
-- function rather than of how each caller spelled its argument.
function hex8 (v : std_logic_vector(7 downto 0)) 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;
-- The bench's own transform, derived from the definition and not from the package under test.
function xfb (k : slip_xf_t; w : std_logic_vector(DW_C - 1 downto 0))
return std_logic_vector is
variable r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
begin
for i in 0 to NB_C - 1 loop
case k is
when X_IDENT => r(i) := w(i);
when X_REV => r(i) := w(NB_C - 1 - i);
when X_ROTL => if i = 0 then r(i) := w(NB_C - 1); else r(i) := w(i - 1); end if;
when X_ROTR => if i = NB_C - 1 then r(i) := w(0); else r(i) := w(i + 1); end if;
when X_SHL => if i = 0 then r(i) := '0'; else r(i) := w(i - 1); end if;
when X_SHR => if i = NB_C - 1 then r(i) := '0'; else r(i) := w(i + 1); end if;
when others => r(i) := w(i);
end case;
end loop;
return r;
end function xfb;
function grade_text (g : natural) return string is
begin
if g = 1 then return "BLIND -- all six transforms give the same image";
elsif g < 4 then return "weak -- at least three faults share a symptom";
elsif g < 6 then return "usable -- one pair still collides";
else return "full -- every candidate fault is distinguishable";
end if;
end function grade_text;
function stim_text (s : natural) return string is
begin
case s is
when 0 => return "a correct link, pattern 0x8d (grade 6)";
when 1 => return "the MASTER transmits LSB-first";
when 2 => return "the SLAVE receives LSB-first -- same byte as above";
when 3 => return "ONE EXTRA clock pulse: clean wire, shifted receiver";
when 4 => return "0xaa reversed: right answer, four hypotheses";
when 5 => return "0xff reversed: a BROKEN link reports a clean byte";
when others => return "0x01 rotated right: reported REVERSED -- wrong";
end case;
end function stim_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_slip_diag
generic map (LEN_W => LEN_W)
port map (
clk => clk, rst_n => rst_n,
sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
cpol => cpol, cpha => cpha, len => to_unsigned(NB_C, LEN_W),
word_exp => word_exp, word_rx => word_rx,
pat_grade => pat_grade, pat_pal => pat_pal,
pat_msb0 => pat_msb0, pat_lsb0 => pat_lsb0,
dg_valid => dg_valid, ob_word => ob_word,
rel_tx => rel_tx, rel_rx => rel_rx, amb_tx => amb_tx, amb_rx => amb_rx
);
cap : process (clk) is
begin
if rising_edge(clk) then
if dg_valid = '1' then
g_rtx <= rel_tx; g_rrx <= rel_rx;
g_atx <= amb_tx; g_arx <= amb_rx;
g_word <= ob_word;
g_n <= g_n + 1;
-- The enumeration and the naturals cannot hold a metavalue, so the only field that
-- can is the captured word, and it is the only one guarded. A guard written where it
-- cannot fire is theatre.
for i in 0 to NB_C - 1 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
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;
-- `w` is CONSTRAINED, because an unconstrained formal inherits its index range from the
-- actual and a bit-string literal carries an ASCENDING `0 to 31` range rather than the
-- `31 downto 0` a reader assumes -- which silently indexes the payload from the wrong end.
procedure frame (w : std_logic_vector(DW_C - 1 downto 0); extra : natural) is
begin
b_sclk <= cpol; b_mosi <= '0';
idle_n(2);
b_cs_n <= '0';
idle_n(1);
b_mosi <= w(NB_C - 1);
idle_n(LEAD_C - 1);
for k in 0 to NB_C - 1 + extra loop
b_sclk <= not b_sclk;
idle_n(HALF_C);
b_sclk <= not b_sclk;
-- After the last data bit MOSI rests at 0, and that is the bit an extra pulse shifts
-- into the receiver. So a clock-count error is a SHIFT rather than a rotation, and
-- the two are separable only when the payload's top bit is 1.
if k < NB_C - 1 then b_mosi <= w(NB_C - 1 - k - 1); else b_mosi <= '0'; 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 frame;
variable grade_log : nat_arr(0 to 6);
variable bound_log : nat_arr(0 to 6);
variable rx_log : pat_arr(0 to 6);
variable exact_n, strict_n, mutations, e, base, bound : natural := 0;
variable wtx, wrx : slip_xf_t;
variable watx, warx : natural;
variable tx_xf, rx_xf : slip_xf_t;
variable extra : natural;
variable wire_w : std_logic_vector(DW_C - 1 downto 0);
variable ln : line;
begin
rst_n <= '1';
wait until falling_edge(clk);
rst_n <= '0';
idle_n(4);
rst_n <= '1';
idle_n(4);
-- ================= TABLE A -- grade the stimulus, with no stimulus =================
write(ln, string'(" pattern grade pal msb0 lsb0 bound verdict"));
writeline(output, ln);
for p in 0 to 6 loop
word_exp <= PAT(p);
wait for 1 ns;
grade_log(p) := pat_grade;
bound := 6;
if pat_pal then bound := bound - 1; end if;
if pat_msb0 then bound := bound - 1; end if;
if pat_lsb0 then bound := bound - 1; end if;
bound_log(p) := bound;
if pat_grade > bound then
write(ln, string'(" FAIL: 0x") & hex8(PAT(p)(7 downto 0)) & string'(" graded ")
& i2s(pat_grade, 1) & string'(" above its predicted bound ") & i2s(bound, 1));
writeline(output, ln); e := e + 1;
end if;
if pat_grade = bound then exact_n := exact_n + 1;
else strict_n := strict_n + 1; end if;
write(ln, string'(" 0x") & hex8(PAT(p)(7 downto 0)) & string'(" ") & i2s(pat_grade, 5)
& string'(" ") & b2s(pat_pal, 3) & string'(" ") & b2s(pat_msb0, 4)
& string'(" ") & b2s(pat_lsb0, 4) & string'(" ") & i2s(bound, 5)
& string'(" ") & grade_text(pat_grade));
writeline(output, ln);
end loop;
if grade_log(6) /= 6 then
write(ln, string'(" FAIL: 0x8d graded ") & i2s(grade_log(6), 1)
& string'(" where 6 was expected; the chapter's control pattern does not discriminate"));
writeline(output, ln); e := e + 1;
end if;
if grade_log(0) /= 1 then
write(ln, string'(" FAIL: 0x00 graded ") & i2s(grade_log(0), 1) & string'(" where 1 was expected"));
writeline(output, ln); e := e + 1;
end if;
if strict_n = 0 then
write(ln, string'(" FAIL: every measured grade equalled its predicted bound, so the claim that the three rules are INCOMPLETE is unsupported"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" A. the three pencil rules -- palindrome hides reversal, top bit 0 merges rotate-left with shift-left, bottom bit 0 merges rotate-right with shift-right -- held as an upper bound for all seven payloads and were EXACT for only ")
& i2s(exact_n, 1) & string'(" of them. The other ") & i2s(strict_n, 1)
& string'(" lost more distinctness than the rules predict, because a payload with period 2 is carried to the same image by reversal AND by either rotation, which is a fourth mechanism the three rules do not express. The rules are sound and incomplete, and the grade has to be measured"));
writeline(output, ln);
-- ================= TABLE B -- seven frames, two relations each =================
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" pat wire rx rel_tx amb rel_rx amb expected_tx/rx stimulus"));
writeline(output, ln);
for s in 0 to 6 loop
wait until falling_edge(clk);
b_sclk <= cpol; b_cs_n <= '1'; b_mosi <= '0';
idle_n(2); rst_n <= '0'; idle_n(3); rst_n <= '1'; idle_n(2);
base := g_n;
tx_xf := X_IDENT; rx_xf := X_IDENT; extra := 0;
case s is
when 0 => word_exp <= x"0000008D";
wtx := X_IDENT; watx := 1; wrx := X_IDENT; warx := 1;
when 1 => word_exp <= x"0000008D"; tx_xf := X_REV;
wtx := X_REV; watx := 1; wrx := X_IDENT; warx := 1;
when 2 => word_exp <= x"0000008D"; rx_xf := X_REV;
wtx := X_IDENT; watx := 1; wrx := X_REV; warx := 1;
when 3 => word_exp <= x"0000008D"; extra := 1;
wtx := X_IDENT; watx := 1; wrx := X_SHL; warx := 1;
when 4 => word_exp <= x"000000AA"; tx_xf := X_REV;
wtx := X_REV; watx := 4; wrx := X_IDENT; warx := 1;
when 5 => word_exp <= x"000000FF"; tx_xf := X_REV;
wtx := X_IDENT; watx := 4; wrx := X_IDENT; warx := 4;
when others => word_exp <= x"00000001"; tx_xf := X_ROTR;
wtx := X_REV; watx := 2; wrx := X_IDENT; warx := 1;
end case;
wait for 1 ns;
wire_w := xfb(tx_xf, word_exp);
if extra = 1 then word_rx <= xfb(rx_xf, xfb(X_SHL, wire_w));
else word_rx <= xfb(rx_xf, wire_w);
end if;
wait for 1 ns;
rx_log(s) := word_rx;
frame(wire_w, extra);
write(ln, string'(" 0x") & hex8(word_exp(7 downto 0)) & string'(" 0x") & hex8(g_word(7 downto 0))
& string'(" 0x") & hex8(word_rx(7 downto 0)) & string'(" ") & xf_name(g_rtx)
& string'(" ") & i2s(g_atx, 3) & string'(" ") & xf_name(g_rrx)
& string'(" ") & i2s(g_arx, 3) & string'(" ") & xf_name(wtx)
& string'("/") & xf_name(wrx) & string'(" ") & stim_text(s));
writeline(output, ln);
if g_n - base /= 1 then
write(ln, string'(" FAIL: stimulus ") & i2s(s, 1) & string'(" produced ")
& i2s(g_n - base, 1) & string'(" diagnoses for one frame"));
writeline(output, ln); e := e + 1;
end if;
if g_rtx /= wtx or g_atx /= watx then
write(ln, string'(" FAIL: stimulus ") & i2s(s, 1) & string'(" reported rel_tx ")
& xf_name(g_rtx) & string'("/") & i2s(g_atx, 1) & string'(" where ")
& xf_name(wtx) & string'("/") & i2s(watx, 1) & string'(" was expected"));
writeline(output, ln); e := e + 1;
end if;
if g_rrx /= wrx or g_arx /= warx then
write(ln, string'(" FAIL: stimulus ") & i2s(s, 1) & string'(" reported rel_rx ")
& xf_name(g_rrx) & string'("/") & i2s(g_arx, 1) & string'(" where ")
& xf_name(wrx) & string'("/") & i2s(warx, 1) & string'(" was expected"));
writeline(output, ln); e := e + 1;
end if;
end loop;
-- ---- 1. a slip has a side, and only the wire reveals it ----
if rx_log(1) /= rx_log(2) then
write(ln, string'(" FAIL: the master-side and slave-side reversals reported different bytes, so the claim that a byte cannot separate them was not exercised"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" 1. the master-side reversal and the slave-side reversal BOTH reported 0x")
& hex8(rx_log(1)(7 downto 0))
& string'(" against an expected 0x8d. Identical bytes, identical mismatch report, identical error counter -- and opposite ends of the link. The wire split them on the first attempt, because the relation from the expectation to the WIRE is the master's contribution and the relation from the wire to the REPORT is the slave's, and a byte-level log has neither"));
writeline(output, ln);
-- ---- 3. the false negative ----
if rx_log(5) /= x"000000FF" then
write(ln, string'(" FAIL: the 0xff reversal did not report the expected byte, so the false-negative demonstration did not occur"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 3. with pattern 0xff a master transmitting LSB-first put 0xff on the wire and the receiver reported 0xff. The expectation matched, the wire matched, the report matched, and the link was BROKEN. Nothing in the data disagreed with anything, so no comparison anywhere in a testbench or a driver could have fired. The only dissent available was the ambiguity count: 4 transforms explain the pair, so `nothing happened` was one hypothesis out of 4 rather than a finding"));
writeline(output, ln);
-- ---- 4. the walking one lies, and that is written down ----
if g_rtx = X_ROTR then
write(ln, string'(" FAIL: the walking-one pattern correctly identified a rotation, which contradicts the chapter's claim that 0x01 cannot distinguish a rotation from a reversal"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 4. the last row is the instrument giving a WRONG answer under a named condition. The true fault was a rotate-right; 0x01 rotated right is 0x80, which is also 0x01 reversed, so the module reported ")
& xf_name(X_REV)
& string'(". It is not a silent failure -- the ambiguity count read 2 -- but the headline verdict named the wrong transform, and 0x01 is the pattern bring-up scripts reach for first"));
writeline(output, ln);
-- ---- BENCH INTEGRITY ----
if grade_log(6) /= 1 then mutations := mutations + 1; end if;
if rx_log(1) /= x"00000000" 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 words carried a metavalue"));
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, and every reported field carried a known value"));
writeline(output, ln);
write(ln, string'("PASS: an alignment fault has a SIDE and the wire is what factors it -- a master transmitting LSB-first and a slave receiving LSB-first reported the identical byte 0x")
& hex8(rx_log(1)(7 downto 0))
& string'(" and were separated on the first capture, because the relation from expectation to wire belongs to the master and the relation from wire to report belongs to the slave. But whether either relation is decidable is a property of the PAYLOAD, not of the instrument: the six candidate transforms produce six distinct images for 0x8d, three for 0xaa and 0x55, and ONE for 0x00, where a capture says nothing at all. Three one-bit rules predict part of that -- a palindrome hides reversal, a top bit of 0 merges rotate-left with shift-left, a bottom bit of 0 does the same on the right -- and they held as an upper bound for all seven payloads while being exact for only ")
& i2s(exact_n, 1)
& string'(", because periodicity costs distinctness in a way the rules do not express. Two consequences are worth the whole chapter. With 0xff a reversed link reported the expected byte and every comparison in the system passed, so the fault was invisible to values and visible only as an ambiguity count of 4. And with 0x01 -- the walking-one pattern every bring-up script starts with -- a rotate-right was reported as a reversal, a wrong answer rather than a missing one. The pattern is part of the instrument, and choosing it by habit is choosing an instrument by habit"));
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;10. What It Costs, Honestly
A chapter that presents a diagnostic as free is selling something.
6 transform images of word_exp 6 × N-bit permutation networks (wiring + one mux row)
6 transform images of the captured word same again
2 relation sets 12 N-bit comparators
1 grade 15 N-bit comparatorsFor N = 8 that is small. For N = 32 the grade's fifteen comparators are the dominant term, and there is an obvious mitigation the chapter's own structure suggests: the grade depends only on word_exp, so it does not need to be in the datapath at all. Compute it once in firmware when the pattern is chosen, or in the testbench before the regression starts, and leave only the two relation sets in hardware. That halves the logic and loses nothing, because a grade that changes per frame was never the interesting case.
11. Grading The Stimulus Before The Regression Runs
The UVM consequence of section 5 is not a new component. It is a constraint.
A random-payload sequence that happens to generate 0x00, 0xFF, 0xAA or 0x55 is generating captures that cannot diagnose the faults the environment exists to find. So the grade belongs in the sequence item:
class spi_slip_item extends uvm_sequence_item;
rand bit [7:0] payload;
// grade(payload) == 6 -- expressed as the three pencil rules, which are
// cheap to constrain, plus a post_randomize check for the periodicity case
constraint c_gradeable {
payload[7] == 1'b1; // rotate-left distinguishable from shift-left
payload[0] == 1'b1; // rotate-right distinguishable from shift-right
payload != {<<{payload}}; // not a palindrome
}
endclass12. What This Decoder Cannot Do
✗ diagnose anything when the payload's grade is 1 — every transform agrees
✗ distinguish a rotation from a clock-count error when the relevant end bit is 0
✗ detect a slip of MORE than one bit position (the candidate set stops at one)
✗ separate a master-side and a slave-side instance of the SAME transform when the
two compose to the identity — two reversals in series look like a correct linkThat last one deserves its own sentence, because it is the only limitation here that the wire cannot fix. A master transmitting LSB-first into a slave receiving LSB-first is a working link built from two faults, and it reports IDENT/IDENT because it genuinely is the identity end to end. The capture is not wrong; the link works. It will stop working the day either end is replaced by a correct one, which is the kind of latent defect that ships.
13. Why an FPGA Engineer Cares
Bit-order and clock-count faults dominate first bring-up of any SPI peripheral, and the usual instrument is a walking-one pattern. Section 8 shows that instrument giving a wrong answer.
The practical takeaway costs nothing to adopt: change the bring-up pattern from 0x01 or 0xAA to something with a grade of 6, and the same waveform you were already capturing becomes able to tell a rotation from a reversal from a miscounted clock. That is an instrument upgrade achieved by editing a constant.
The grade logic itself does not need to be on the FPGA. Compute it once, in a script, for the patterns your bring-up uses, and keep the two relation sets in logic if you want an automatic answer on a board you cannot probe.
14. Why an ASIC Engineer Cares
Section 12's last item is the one that reaches silicon. A master and a slave with matching bit-order faults is an interoperable pair that passes every test you run against it — and fails against any conformant third-party device. It is not detectable from the pins, because the composition genuinely is the identity, and it is not detectable from the data, because the data is correct. The only thing that finds it is a comparison against the specification's bit order, which means someone has to check rel_tx against the spec rather than against the other end of the link.
The clock-count case matters for a different reason: an extra pulse is a violation your own triage already sees as an edge-count error, so it costs nothing to detect and it is a routine consequence of a divider or a state machine that counts transitions instead of bit-times. Catching it before tape-out is cheap; catching it afterwards means a firmware workaround that every future driver has to know about.
15. Failure Signature — "It Works With 0xAA"
Symptom an SPI EEPROM read returns plausible but wrong data
Test write 0xAA, read back 0xAA, declare the link good
Reality the master transmits LSB-first; 0xAA reversed is 0x55, and
the EEPROM's write path reversed it back on the way out
Filed as "link verified, suspect EEPROM contents"
Found three weeks later, by a colleague who used 0x8dThe test was not badly executed. It was executed with an instrument whose grade is 3, against a fault the instrument cannot see. And the conclusion it produced — the link is verified — was the most damaging output available, because it removed the link from the list of suspects.
16. Common Misconceptions
| Misconception | What is actually true |
|---|---|
| "The bits are shifted" is a diagnosis | It is a symptom shared by at least six distinct faults |
| A mismatched byte tells you which device is wrong | The byte is a composition; only the wire factors it |
0xAA is a good bus test pattern | It grades 3 of 6: reversal and both rotations all give 0x55 |
0xFF proves the wires are connected | It also lets a reversed link report a perfect result |
| A walking one isolates alignment faults | For 0x01, reversal and rotate-right give the same value |
| A rotation and a clock-count error are the same thing | They differ in the bit shifted in — observable only when that bit is 1 |
| A link that passes is a link built correctly | Two matching faults compose to the identity and pass everything |
17. Reason It Through
18. Understanding Check
19. Summary
An alignment fault has a side, and the wire is what factors it: the relation from the expectation to the captured bits belongs to the master, the relation from those bits to the report belongs to the slave, and a master transmitting LSB-first and a slave receiving LSB-first produced the identical byte 0xb1 and were separated on the first capture. A byte-level mismatch report contains only the composition, and composition is exactly the information that loses which end did it.
But whether either relation is decidable is a property of the payload, not of the instrument. The six candidate transforms produce six distinct images for 0x8d, three for 0xAA and 0x55, and one for 0x00 — where a capture says nothing at all. Three one-bit rules predict part of that and held as an upper bound for all seven payloads while being exact for only two, because periodicity costs distinctness in a way the rules do not express; so the grade is measured rather than assumed.
Two consequences are worth the chapter on their own. With 0xFF a reversed link reported the expected byte and every comparison in the system passed, so the fault was invisible to values and visible only as an ambiguity count of four. And with 0x01 — the walking one every bring-up script starts with — a rotate-right was reported as a reversal: a wrong answer rather than a missing one. The pattern is part of the instrument.
20. What Comes Next
Alignment is settled, and the frame's length has not been questioned yet. Chapter 18.4 takes two faults that produce the same total edge count — a wrong transfer width and a wrong dummy-cycle count — and finds the discriminator in where the returned data starts rather than in how much of it there is.
Continue learning
Related tutorials
- 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
Transfer-Width and Dummy-Cycle Errors
A 32-bit address with 0 dummy cycles issues exactly as many clocks as a 24-bit address with 8 — same total, same launch instant, valid-looking data. Two numbers separate four faults; the fifth needs a second capture at a different address.
- Related topic
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.
