SPI · Module 18
RTL, CDC, Constraint, or Testbench?
Three perturbations — the clock ratio, the bench's data placement, and the payload — map four failure layers to four signatures. And one of the four is provably invisible to any RTL regression.
Six chapters have each taken a fault family and found its discriminator. This one asks the question that actually comes first: which layer is this failure in? Because the answer decides who owns it, what tool applies, and whether any of the previous six chapters is even relevant.
Three perturbations. Four layers. And one layer that a passing regression cannot say anything about.
1. Four Layers, Not Four Faults
| Layer | Where the fix is | What tool applies |
|---|---|---|
| RTL | the design's logic | simulation, formal |
| CDC | a clock-domain crossing | simulation partly, CDC analysis |
| Constraint | an SDC file, an I/O budget | static timing analysis — not simulation |
| Testbench | the environment | nothing but review |
These are not four kinds of bug in one place. They are four places, and the mistake that costs weeks is debugging in the wrong one — most often debugging the design when the bench is wrong, or debugging the design when the design is fine and a constraint is not.
2. Three Perturbations, And Why These Three
RATE change the SCLK period; leave the system clock alone.
Only the PHASE relationship between the two domains moves.
INSTANT change when the bench presents MOSI relative to the edge
that samples it. The design's function does not depend on
this as long as the data is there in time.
SEED change the payload sequence. A bug with a data-dependent
trigger responds; a systematic one does not.Each perturbation is chosen to be orthogonal to the others and to leave a correct design alone. That second property is what makes the whole method work, and section 4 is about it.
3. The Signature Matrix
layer base rate instant seed
RTL bug a a a b only the payload moved it
CDC c d c c only the phase moved it
TESTBENCH e e f e only the bench moved it
CONSTRAINT 0 0 0 0 nothing moved it, and nothing failedThree perturbations, four layers, and the fourth row is the point of the chapter rather than a gap in the method. A constraint error has no representation in an RTL simulation — RTL has no delays, so a violated input-delay budget is not something a simulator can express. The row is identical to a correct design's, and this chapter measures that rather than asserting it.
4. The Perturbations Must Not Disturb A Working Design
This is the load-bearing row and it is easy to skip.
If a perturbation makes a correct design fail, then a non-zero response to it means nothing — you are measuring the knob rather than the design. So the first requirement on the whole method is that all three perturbations leave the correct design at zero failures, and the bench asserts exactly that before any other conclusion is drawn.
5. The Crossing Defect: A Pulse That Outlives Nothing
The injected CDC fault is the most common real crossing defect there is, and it is worth understanding precisely because the correct alternative costs nothing.
a TOGGLE flips once per word and PERSISTS until the next one
→ no clock ratio can miss it
a PULSE is asserted at the last SCLK edge and cleared when the frame
ends, so its width is a property of the SPI timing
→ caught only if a receiving clock edge falls inside itA pulse narrower than the receiving clock period is caught only by luck
14 cyclesPhase A's rising edges fall at columns 0 and 10 — both outside the window — so the transfer is lost. Phase B's edge at column 3 is inside it, so the transfer arrives. Nothing about either design changed; the phase did.
6. The Measurement
Five conditions, four runs each, twenty frames per run, identical output from all three languages:
condition base rate instant seed verdict expected what it means
none 0 0 0 0 NONE_SEEN NONE_SEEN nothing responds to anything
rtl 7 7 7 5 RTL RTL only the PAYLOAD moved it
cdc 8 0 8 8 CDC CDC only the clock RATIO moved it
testbench 20 20 0 20 TESTBENCH TESTBENCH only the bench's INSTANT moved it
constraint 0 0 0 0 NONE_SEEN NONE_SEEN NOTHING moved it -- and nothing failedThe matrix is diagonal by measurement. Each fault responds to exactly one perturbation and to neither of the others, and the bench requires that per row rather than eyeballing the table.
Two rows deserve a second look.
The testbench row fails 20 of 20 at baseline. A bench that presents data after the edge that samples it fails every transfer, which in practice is what makes a bench fault findable: a 100% failure rate on a design that was working last week is a strong hint that the change was not in the design.
The cdc row's failures are not corrupted words. They are transfers that never arrived — and section 8 is about what that does to a scoreboard.
7. The Loss Rate Is Predicted, Not Observed
This is the strongest check in the chapter, and it is worth more than the matrix.
a pulse of width W crossing into a clock of period T
is caught with probability W / T
so the expected loss over N frames is N · (1 − W/T)
here: W = 6 ns T = 10 ns N = 20
20 · (1 − 6/10) = 8Eight transfers were lost. The bench computes that number from the periods and requires the measured loss to equal it.
8. What A Crossing Bug Looks Like To A Scoreboard
Nothing. That is the problem.
words compared: 12
words mismatched: 0
verdict: PASSEight transfers were lost and every word that arrived was correct. A scoreboard that compares received words against expected words has nothing to compare for a transfer that never arrived, so it reports a clean run.
9. The Fourth Row: The Regression Is Blind
The constraint condition injects a MOSI delay that is late but still inside the bit time. RTL tolerates it completely, because RTL has no delays — data that arrives before the sampling edge is simply data that arrived.
All four of its runs report zero, and the bench requires the constraint row to be identical to the correct design's row. That is an assertion that the regression is blind, which is the only honest way to publish the fact.
Two consequences, and the second is the more useful one:
a PASSING RTL regression is NOT evidence that a constraint is right
a failure that does NOT reproduce in simulation is EVIDENCE FOR
this class, rather than an absence of information10. Building It — Three HDLs
Two faults are injected into the design and two are not, and the asymmetry is the lesson: the testbench fault lives in the bench by definition, and the constraint fault is a delay the design has no way to represent.
// spi_localise.sv
//
// Chapter 18.7 -- given a failure, is it in the RTL, in a clock-domain crossing, in a CONSTRAINT, or in
// the TESTBENCH? Four causes, three perturbations, and one cause that an RTL regression cannot see at
// all.
//
// THIS IS THE QUESTION THAT COMES FIRST IN PRACTICE AND LAST IN THIS MODULE. Chapters 18.1 to 18.6 each
// took a fault family and found its discriminator. This one asks which family -- or rather which LAYER
// -- a failure belongs to, because the answer decides who owns it and which of the previous six chapters
// is even applicable.
//
// THE THREE PERTURBATIONS, and why these three.
//
// RATE change the SCLK period against an unchanged system clock. This changes the PHASE
// relationship between the two domains and nothing else. A correct design is insensitive
// to it; a clock-domain crossing is not.
//
// INSTANT change when the TESTBENCH places MOSI relative to the SCLK edge that samples it. A
// correct design is insensitive as long as the data is there in time; a bench that drives
// on the sampling edge is measuring its own race. Chapter 16.3 built that race deliberately
// and this chapter uses it as an instrument.
//
// SEED change the payload sequence. A logic bug with a data-dependent trigger responds; a
// systematic one does not.
//
// THE SIGNATURES THE THREE PRODUCE, and the fourth row is the point of the chapter.
//
// cause base rate instant seed
// RTL bug a a a b only the data changed it
// CDC c d c c only the phase changed it
// TESTBENCH e e f e only the bench changed it
// CONSTRAINT 0 0 0 0 NOTHING changed it, and nothing failed
//
// The fourth row is IDENTICAL to a correct design's. That is not a gap in the method; it is the method
// telling the truth. A constraint error has no representation in an RTL simulation -- RTL has no
// delays, so a violated input-delay budget is simply not expressible -- and therefore a passing
// regression is not evidence that a constraint is right. The chapter measures that blindness rather
// than asserting it.
//
// TWO FAULTS ARE INJECTED INTO THIS MODULE AND TWO ARE NOT, and the asymmetry is the lesson.
//
// f_rtl a data-dependent last-bit error: when the word being assembled has bit 1 set, the final bit
// latched is the PREVIOUS MOSI value. A genuine logic bug, local to one word, and it fires
// only for some payloads.
//
// f_cdc the request crosses as a PULSE instead of a TOGGLE, and the pulse lives only until the frame
// ends. A pulse narrower than the receiving clock's period is caught only if a receiving edge
// happens to fall inside it -- so whether a transfer is seen at all depends on the phase
// between the two clocks, and the fraction lost depends on the SCLK period. This is the single
// most common real crossing defect there is, and unlike a shallow synchroniser it is fully
// visible in zero-delay RTL.
//
// the TESTBENCH fault is in the bench, by definition, and injecting it here would be a category error.
//
// the CONSTRAINT fault is injected by the bench as a MOSI delay that is still comfortably inside the
// bit time. RTL tolerates it completely. That is the whole demonstration.
//
// WHAT AN RTL SIMULATION CANNOT SEE ABOUT A CDC, said here because the f_cdc model would otherwise
// overclaim: METASTABILITY IS NOT MODELLED. A single-flop synchroniser is a real defect whose mechanism
// is a flip-flop resolving slowly, and in zero-delay RTL it works perfectly. What IS modelled here is
// the DATA-STABILITY half -- sampling a multi-bit bus that is mid-update -- and that half is real,
// common, and phase-dependent. A regression that finds no CDC bug has established nothing about
// synchroniser depth.
`timescale 1ns/1ps
module spi_localise #(
parameter int NB = 8,
parameter int DW = 32
) (
input wire clk,
input wire rst_n,
input wire sclk, // an INDEPENDENT clock -- the whole reason CDC exists in SPI
input wire cs_n,
input wire mosi,
input wire f_rtl,
input wire f_cdc,
input wire [DW-1:0] word_exp,
input wire clr,
output reg sys_valid,
output reg [DW-1:0] word_sys,
output reg [15:0] ob_frames,
output reg [15:0] ob_err,
output reg [DW-1:0] ob_last
);
// ---------------- the SCLK domain ----------------
//
// A slave's receive path: shift on the leading edge, latch a word every NB bits, toggle a flag for
// the other domain. The counter is cleared asynchronously by deselect, which is how a real slave
// guarantees frame alignment without a length field.
// THE SHIFT REGISTER IS NB BITS WIDE, NOT DW. A DW-wide register accumulates the previous frames'
// bits above bit NB-1, so the latched word compares unequal against a zero-extended expectation from
// the second frame onwards -- which the first version of this module did, and the symptom was a
// baseline run reporting an error on every frame but the first. A width is part of a design's
// meaning, not a convenience.
reg [NB-1:0] sh;
reg [7:0] cnt;
reg req; // a TOGGLE: it persists, so it cannot be missed at any clock ratio
reg req_p; // a PULSE that lives only until the frame ends -- the injected crossing bug
reg [DW-1:0] w_flag;
reg mosi_d;
// THE RESET IS ASYNCHRONOUS AND THE DESELECT IS SYNCHRONOUS TO SCLK, and getting that the wrong way
// round cost the first version of this module a debugging session: with no reset at all, `req`
// started X, both synchroniser chains carried X, `fire` was X, and `if (X)` is false -- so the
// system domain saw almost no frames and every reported word was X. A bench full of comparisons
// measured nothing and printed a plausible table of zeros.
always @(posedge sclk or negedge rst_n) begin
if (!rst_n) begin
sh <= {NB{1'b0}};
cnt <= 8'd0;
req <= 1'b0;
w_flag <= {DW{1'b0}};
mosi_d <= 1'b0;
end else if (cs_n) begin
cnt <= 8'd0;
end else begin
mosi_d <= mosi;
sh <= {sh[NB-2:0], mosi};
if (cnt == NB[7:0] - 8'd1) begin
cnt <= 8'd0;
// THE INJECTED LOGIC BUG. When the assembling word has bit 1 set, the final bit latched
// is the PREVIOUS MOSI value instead of the current one. It is local to one word, it
// does not corrupt the frames after it, and it fires only for some payloads -- which is
// what makes its error count depend on the payload SEQUENCE and on nothing else.
w_flag <= {{(DW-NB){1'b0}}, sh[NB-2:0], (f_rtl && sh[1]) ? mosi_d : mosi};
req <= ~req;
end else begin
cnt <= cnt + 8'd1;
end
end
end
// THE PULSE, and the reason it is a bug rather than a style choice. It is asserted at the SCLK edge
// that completes a word and cleared when the frame ends -- so its width is a property of the SPI
// TIMING, not of the receiving clock. A toggle carries the same information and cannot be missed,
// because it persists until the next one.
//
// Cleared asynchronously by deselect because there is no SCLK edge left to clear it on: between
// frames the SCLK domain has no clock at all, which is exactly why a pulse generated there cannot be
// given a guaranteed minimum width in the receiving domain.
always @(posedge sclk or posedge cs_n) begin
if (cs_n) req_p <= 1'b0;
else req_p <= (cnt == NB[7:0] - 8'd1);
end
// ---------------- the crossing ----------------
//
// Two flops on the flag, then an edge detect, then capture the LATCHED word. The latched word is
// stable for a whole SCLK period before the flag is honoured, so the capture is safe.
reg req_s1, req_s2, req_s3;
reg p_s1, p_s2, p_s3;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
req_s1 <= 1'b0; req_s2 <= 1'b0; req_s3 <= 1'b0;
p_s1 <= 1'b0; p_s2 <= 1'b0; p_s3 <= 1'b0;
end else begin
req_s1 <= req;
req_s2 <= req_s1;
req_s3 <= req_s2;
p_s1 <= req_p;
p_s2 <= p_s1;
p_s3 <= p_s2;
end
end
// The correct trigger is a CHANGE in the synchronised toggle -- caught at any clock ratio, because a
// toggle persists until the next word. The faulty trigger is a RISING EDGE of the synchronised
// pulse, which exists only if a receiving clock edge happened to fall inside the pulse.
//
// Note what is NOT modelled: both paths here are two flops deep. A shallow synchroniser is a real
// defect and it is INVISIBLE in zero-delay RTL, because metastability is not representable. The bug
// injected here is the other half of the same subject -- pulse width against the receiving clock
// period -- and that half is fully visible. A regression that finds no crossing bug has established
// nothing about synchroniser depth.
wire fire_ok = (req_s2 !== req_s3);
wire fire_bad = p_s2 && !p_s3;
wire fire = f_cdc ? fire_bad : fire_ok;
// ---------------- the system domain ----------------
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sys_valid <= 1'b0;
word_sys <= {DW{1'b0}};
ob_frames <= 16'd0;
ob_err <= 16'd0;
ob_last <= {DW{1'b0}};
end else if (clr) begin
sys_valid <= 1'b0;
ob_frames <= 16'd0;
ob_err <= 16'd0;
end else begin
sys_valid <= 1'b0;
if (fire) begin
// Both paths read the LATCHED word, which is stable for far longer than either
// synchroniser takes. The difference between them is only whether the capture happens
// at all -- so the crossing bug's signature is a LOST TRANSFER rather than a wrong one,
// and the bench's failure count has to include frames that never arrived.
word_sys <= w_flag;
sys_valid <= 1'b1;
ob_frames <= ob_frames + 16'd1;
ob_last <= w_flag;
if (w_flag !== word_exp) ob_err <= ob_err + 16'd1;
end
end
end
endmodule// spi_localise.v
//
// Chapter 18.7 -- given a failure, is it in the RTL, in a clock-domain crossing, in a CONSTRAINT, or in
// the TESTBENCH? Four causes, three perturbations, and one cause that an RTL regression cannot see at
// all.
//
// THIS IS THE QUESTION THAT COMES FIRST IN PRACTICE AND LAST IN THIS MODULE. Chapters 18.1 to 18.6 each
// took a fault family and found its discriminator. This one asks which family -- or rather which LAYER
// -- a failure belongs to, because the answer decides who owns it and which of the previous six chapters
// is even applicable.
//
// THE THREE PERTURBATIONS, and why these three.
//
// RATE change the SCLK period against an unchanged system clock. This changes the PHASE
// relationship between the two domains and nothing else. A correct design is insensitive
// to it; a clock-domain crossing is not.
//
// INSTANT change when the TESTBENCH places MOSI relative to the SCLK edge that samples it. A
// correct design is insensitive as long as the data is there in time; a bench that drives
// on the sampling edge is measuring its own race. Chapter 16.3 built that race deliberately
// and this chapter uses it as an instrument.
//
// SEED change the payload sequence. A logic bug with a data-dependent trigger responds; a
// systematic one does not.
//
// THE SIGNATURES THE THREE PRODUCE, and the fourth row is the point of the chapter.
//
// cause base rate instant seed
// RTL bug a a a b only the data changed it
// CDC c d c c only the phase changed it
// TESTBENCH e e f e only the bench changed it
// CONSTRAINT 0 0 0 0 NOTHING changed it, and nothing failed
//
// The fourth row is IDENTICAL to a correct design's. That is not a gap in the method; it is the method
// telling the truth. A constraint error has no representation in an RTL simulation -- RTL has no
// delays, so a violated input-delay budget is simply not expressible -- and therefore a passing
// regression is not evidence that a constraint is right. The chapter measures that blindness rather
// than asserting it.
//
// TWO FAULTS ARE INJECTED INTO THIS MODULE AND TWO ARE NOT, and the asymmetry is the lesson.
//
// f_rtl a data-dependent last-bit error: when the word being assembled has bit 1 set, the final bit
// latched is the PREVIOUS MOSI value. A genuine logic bug, local to one word, and it fires
// only for some payloads.
//
// f_cdc the request crosses as a PULSE instead of a TOGGLE, and the pulse lives only until the frame
// ends. A pulse narrower than the receiving clock's period is caught only if a receiving edge
// happens to fall inside it -- so whether a transfer is seen at all depends on the phase
// between the two clocks, and the fraction lost depends on the SCLK period. This is the single
// most common real crossing defect there is, and unlike a shallow synchroniser it is fully
// visible in zero-delay RTL.
//
// the TESTBENCH fault is in the bench, by definition, and injecting it here would be a category error.
//
// the CONSTRAINT fault is injected by the bench as a MOSI delay that is still comfortably inside the
// bit time. RTL tolerates it completely. That is the whole demonstration.
//
// WHAT AN RTL SIMULATION CANNOT SEE ABOUT A CDC, said here because the f_cdc model would otherwise
// overclaim: METASTABILITY IS NOT MODELLED. A single-flop synchroniser is a real defect whose mechanism
// is a flip-flop resolving slowly, and in zero-delay RTL it works perfectly. What IS modelled here is
// the DATA-STABILITY half -- sampling a multi-bit bus that is mid-update -- and that half is real,
// common, and phase-dependent. A regression that finds no CDC bug has established nothing about
// synchroniser depth.
`timescale 1ns/1ps
module spi_localise #(
parameter NB = 8,
parameter DW = 32
) (
input wire clk,
input wire rst_n,
input wire sclk, // an INDEPENDENT clock -- the whole reason CDC exists in SPI
input wire cs_n,
input wire mosi,
input wire f_rtl,
input wire f_cdc,
input wire [DW-1:0] word_exp,
input wire clr,
output reg sys_valid,
output reg [DW-1:0] word_sys,
output reg [15:0] ob_frames,
output reg [15:0] ob_err,
output reg [DW-1:0] ob_last
);
// ---------------- the SCLK domain ----------------
//
// A slave's receive path: shift on the leading edge, latch a word every NB bits, toggle a flag for
// the other domain. The counter is cleared asynchronously by deselect, which is how a real slave
// guarantees frame alignment without a length field.
// THE SHIFT REGISTER IS NB BITS WIDE, NOT DW. A DW-wide register accumulates the previous frames'
// bits above bit NB-1, so the latched word compares unequal against a zero-extended expectation from
// the second frame onwards -- which the first version of this module did, and the symptom was a
// baseline run reporting an error on every frame but the first. A width is part of a design's
// meaning, not a convenience.
reg [NB-1:0] sh;
reg [7:0] cnt;
reg req; // a TOGGLE: it persists, so it cannot be missed at any clock ratio
reg req_p; // a PULSE that lives only until the frame ends -- the injected crossing bug
reg [DW-1:0] w_flag;
reg mosi_d;
// THE RESET IS ASYNCHRONOUS AND THE DESELECT IS SYNCHRONOUS TO SCLK, and getting that the wrong way
// round cost the first version of this module a debugging session: with no reset at all, `req`
// started X, both synchroniser chains carried X, `fire` was X, and `if (X)` is false -- so the
// system domain saw almost no frames and every reported word was X. A bench full of comparisons
// measured nothing and printed a plausible table of zeros.
always @(posedge sclk or negedge rst_n) begin
if (!rst_n) begin
sh <= {NB{1'b0}};
cnt <= 8'd0;
req <= 1'b0;
w_flag <= {DW{1'b0}};
mosi_d <= 1'b0;
end else if (cs_n) begin
cnt <= 8'd0;
end else begin
mosi_d <= mosi;
sh <= {sh[NB-2:0], mosi};
if (cnt == NB[7:0] - 8'd1) begin
cnt <= 8'd0;
// THE INJECTED LOGIC BUG. When the assembling word has bit 1 set, the final bit latched
// is the PREVIOUS MOSI value instead of the current one. It is local to one word, it
// does not corrupt the frames after it, and it fires only for some payloads -- which is
// what makes its error count depend on the payload SEQUENCE and on nothing else.
w_flag <= {{(DW-NB){1'b0}}, sh[NB-2:0], (f_rtl && sh[1]) ? mosi_d : mosi};
req <= ~req;
end else begin
cnt <= cnt + 8'd1;
end
end
end
// THE PULSE, and the reason it is a bug rather than a style choice. It is asserted at the SCLK edge
// that completes a word and cleared when the frame ends -- so its width is a property of the SPI
// TIMING, not of the receiving clock. A toggle carries the same information and cannot be missed,
// because it persists until the next one.
//
// Cleared asynchronously by deselect because there is no SCLK edge left to clear it on: between
// frames the SCLK domain has no clock at all, which is exactly why a pulse generated there cannot be
// given a guaranteed minimum width in the receiving domain.
always @(posedge sclk or posedge cs_n) begin
if (cs_n) req_p <= 1'b0;
else req_p <= (cnt == NB[7:0] - 8'd1);
end
// ---------------- the crossing ----------------
//
// Two flops on the flag, then an edge detect, then capture the LATCHED word. The latched word is
// stable for a whole SCLK period before the flag is honoured, so the capture is safe.
reg req_s1, req_s2, req_s3;
reg p_s1, p_s2, p_s3;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
req_s1 <= 1'b0; req_s2 <= 1'b0; req_s3 <= 1'b0;
p_s1 <= 1'b0; p_s2 <= 1'b0; p_s3 <= 1'b0;
end else begin
req_s1 <= req;
req_s2 <= req_s1;
req_s3 <= req_s2;
p_s1 <= req_p;
p_s2 <= p_s1;
p_s3 <= p_s2;
end
end
// The correct trigger is a CHANGE in the synchronised toggle -- caught at any clock ratio, because a
// toggle persists until the next word. The faulty trigger is a RISING EDGE of the synchronised
// pulse, which exists only if a receiving clock edge happened to fall inside the pulse.
//
// Note what is NOT modelled: both paths here are two flops deep. A shallow synchroniser is a real
// defect and it is INVISIBLE in zero-delay RTL, because metastability is not representable. The bug
// injected here is the other half of the same subject -- pulse width against the receiving clock
// period -- and that half is fully visible. A regression that finds no crossing bug has established
// nothing about synchroniser depth.
wire fire_ok = (req_s2 !== req_s3);
wire fire_bad = p_s2 && !p_s3;
wire fire = f_cdc ? fire_bad : fire_ok;
// ---------------- the system domain ----------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sys_valid <= 1'b0;
word_sys <= {DW{1'b0}};
ob_frames <= 16'd0;
ob_err <= 16'd0;
ob_last <= {DW{1'b0}};
end else if (clr) begin
sys_valid <= 1'b0;
ob_frames <= 16'd0;
ob_err <= 16'd0;
end else begin
sys_valid <= 1'b0;
if (fire) begin
// Both paths read the LATCHED word, which is stable for far longer than either
// synchroniser takes. The difference between them is only whether the capture happens
// at all -- so the crossing bug's signature is a LOST TRANSFER rather than a wrong one,
// and the bench's failure count has to include frames that never arrived.
word_sys <= w_flag;
sys_valid <= 1'b1;
ob_frames <= ob_frames + 16'd1;
ob_last <= w_flag;
if (w_flag !== word_exp) ob_err <= ob_err + 16'd1;
end
end
end
endmodule-- spi_localise.vhd
--
-- Chapter 18.7 -- given a failure, is it in the RTL, in a clock-domain crossing, in a CONSTRAINT, or in
-- the TESTBENCH? Four causes, three perturbations, and one cause that an RTL regression cannot see at
-- all.
--
-- THIS IS THE QUESTION THAT COMES FIRST IN PRACTICE AND LAST IN THIS MODULE. Chapters 18.1 to 18.6 each
-- took a fault family and found its discriminator. This one asks which family -- or rather which LAYER
-- -- a failure belongs to, because the answer decides who owns it and which of the previous six chapters
-- is even applicable.
--
-- THE THREE PERTURBATIONS, and why these three.
--
-- RATE change the SCLK period against an unchanged system clock. This changes the PHASE
-- relationship between the two domains and nothing else. A correct design is insensitive
-- to it; a clock-domain crossing is not.
--
-- INSTANT change when the TESTBENCH places MOSI relative to the SCLK edge that samples it. A
-- correct design is insensitive as long as the data is there in time; a bench that drives
-- on the sampling edge is measuring its own race. Chapter 16.3 built that race deliberately
-- and this chapter uses it as an instrument.
--
-- SEED change the payload sequence. A logic bug with a data-dependent trigger responds; a
-- systematic one does not.
--
-- THE SIGNATURES THE THREE PRODUCE, and the fourth row is the point of the chapter.
--
-- cause base rate instant seed
-- RTL bug a a a b only the data changed it
-- CDC c d c c only the phase changed it
-- TESTBENCH e e f e only the bench changed it
-- CONSTRAINT 0 0 0 0 NOTHING changed it, and nothing failed
--
-- The fourth row is IDENTICAL to a correct design's. That is not a gap in the method; it is the method
-- telling the truth. A constraint error has no representation in an RTL simulation -- RTL has no
-- delays, so a violated input-delay budget is simply not expressible -- and therefore a passing
-- regression is not evidence that a constraint is right. The chapter measures that blindness rather
-- than asserting it.
--
-- TWO FAULTS ARE INJECTED INTO THIS MODULE AND TWO ARE NOT, and the asymmetry is the lesson.
--
-- f_rtl a data-dependent last-bit error: when the word being assembled has bit 1 set, the final bit
-- latched is the PREVIOUS MOSI value. A genuine logic bug, local to one word, and it fires
-- only for some payloads.
--
-- f_cdc the request crosses as a PULSE instead of a TOGGLE, and the pulse lives only until the frame
-- ends. A pulse narrower than the receiving clock's period is caught only if a receiving edge
-- happens to fall inside it -- so whether a transfer is seen at all depends on the phase
-- between the two clocks, and the fraction lost depends on the SCLK period. This is the single
-- most common real crossing defect there is, and unlike a shallow synchroniser it is fully
-- visible in zero-delay RTL.
--
-- the TESTBENCH fault is in the bench, by definition, and injecting it here would be a category error.
--
-- the CONSTRAINT fault is injected by the bench as a MOSI delay that is still comfortably inside the
-- bit time. RTL tolerates it completely. That is the whole demonstration.
--
-- WHAT AN RTL SIMULATION CANNOT SEE ABOUT A CDC, said here because the f_cdc model would otherwise
-- overclaim: METASTABILITY IS NOT MODELLED. A single-flop synchroniser is a real defect whose mechanism
-- is a flip-flop resolving slowly, and in zero-delay RTL it works perfectly. What IS modelled here is
-- the DATA-STABILITY half -- sampling a multi-bit bus that is mid-update -- and that half is real,
-- common, and phase-dependent. A regression that finds no CDC bug has established nothing about
-- synchroniser depth.
--
-- WHAT THE VHDL VERSION ADDS. The two synchroniser chains are `std_logic_vector` shift registers rather
-- than three named flops each, which makes the DEPTH a number in one place instead of a pattern a reader
-- has to recognise -- and depth is the parameter that a reviewer of a crossing actually wants to see.
--
-- The injected faults are booleans rather than `std_logic`, so `f_cdc` cannot be driven to 'X' and silently
-- disable the fault it selects. That matters more than it sounds: an 'X' on a fault-select input turns a
-- mux into an X-producer, every comparison downstream evaluates to false, and the run reports a clean
-- table.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). Generics are `NB_C` and `DW_C`; the counters are `n_frm`
-- and `n_err`; the synchroniser chains are `req_sr` and `pls_sr`. No declaration here differs from another
-- only by case -- the check 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;
entity spi_localise is
generic (
NB_C : positive := 8;
DW_C : positive := 32
);
port (
clk : in std_logic;
rst_n : in std_logic;
sclk : in std_logic; -- an INDEPENDENT clock: the whole reason CDC exists in SPI
cs_n : in std_logic;
mosi : in std_logic;
f_rtl : in boolean;
f_cdc : in boolean;
word_exp : in std_logic_vector(DW_C - 1 downto 0);
clr : in std_logic;
sys_valid : out std_logic;
word_sys : out std_logic_vector(DW_C - 1 downto 0);
ob_frames : out natural;
ob_err : out natural;
ob_last : out std_logic_vector(DW_C - 1 downto 0)
);
end entity spi_localise;
architecture rtl of spi_localise is
-- ---------------- the SCLK domain ----------------
signal sh : std_logic_vector(NB_C - 1 downto 0) := (others => '0');
signal cnt : natural := 0;
signal req : std_logic := '0'; -- a TOGGLE: it persists, so no clock ratio can miss it
signal req_p : std_logic := '0'; -- a PULSE that lives only until the frame ends: the injected bug
signal w_flag : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal mosi_d : std_logic := '0';
-- ---------------- the crossing ----------------
-- The DEPTH is the number in the range, in one place. A reviewer of a crossing wants to read a depth,
-- not count named flops.
signal req_sr : std_logic_vector(2 downto 0) := (others => '0');
signal pls_sr : std_logic_vector(2 downto 0) := (others => '0');
signal v_r : std_logic := '0';
signal ws_r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal wl_r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal n_frm, n_err : natural := 0;
begin
sys_valid <= v_r;
word_sys <= ws_r;
ob_last <= wl_r;
ob_frames <= n_frm;
ob_err <= n_err;
-- THE RESET IS ASYNCHRONOUS AND THE DESELECT IS SYNCHRONOUS TO SCLK, and getting that the wrong way
-- round cost the first version of this design a debugging session: with no reset at all, `req` started
-- as a metavalue, both synchroniser chains carried it, the capture trigger was undefined, and an
-- undefined condition is treated as FALSE -- so the receiving domain saw almost no frames and every
-- reported word was a metavalue. A bench full of comparisons measured nothing and printed zeros.
sclk_dom : process (sclk, rst_n) is
begin
if rst_n = '0' then
sh <= (others => '0'); cnt <= 0; req <= '0';
w_flag <= (others => '0'); mosi_d <= '0';
elsif rising_edge(sclk) then
if cs_n = '1' then
cnt <= 0;
else
mosi_d <= mosi;
sh <= sh(NB_C - 2 downto 0) & mosi;
if cnt = NB_C - 1 then
cnt <= 0;
-- THE INJECTED LOGIC BUG. When the assembling word has bit 1 set, the final bit latched
-- is the PREVIOUS MOSI value instead of the current one. Local to one word, it does not
-- corrupt the frames after it, and it fires only for some payloads -- which is what
-- makes its failure count depend on the payload SEQUENCE and on nothing else.
if f_rtl and sh(1) = '1' then
w_flag <= (DW_C - 1 downto NB_C => '0')
& sh(NB_C - 2 downto 0) & mosi_d;
else
w_flag <= (DW_C - 1 downto NB_C => '0')
& sh(NB_C - 2 downto 0) & mosi;
end if;
req <= not req;
else
cnt <= cnt + 1;
end if;
end if;
end if;
end process sclk_dom;
-- THE PULSE, and the reason it is a bug rather than a style choice. It is asserted at the SCLK edge
-- that completes a word and cleared when the frame ends -- so its width is a property of the SPI
-- TIMING rather than of the receiving clock. A toggle carries the same information and cannot be
-- missed, because it persists until the next one.
--
-- Cleared asynchronously by deselect because there is no SCLK edge left to clear it on: between frames
-- the SCLK domain has no clock at all, which is exactly why a pulse generated there cannot be given a
-- guaranteed minimum width in the receiving domain.
pulse_gen : process (sclk, cs_n) is
begin
if cs_n = '1' then
req_p <= '0';
elsif rising_edge(sclk) then
if cnt = NB_C - 1 then req_p <= '1'; else req_p <= '0'; end if;
end if;
end process pulse_gen;
sync : process (clk, rst_n) is
begin
if rst_n = '0' then
req_sr <= (others => '0');
pls_sr <= (others => '0');
elsif rising_edge(clk) then
req_sr <= req_sr(1 downto 0) & req;
pls_sr <= pls_sr(1 downto 0) & req_p;
end if;
end process sync;
-- ---------------- the system domain ----------------
sys_dom : process (clk, rst_n) is
variable fire : boolean;
begin
if rst_n = '0' then
v_r <= '0'; ws_r <= (others => '0'); wl_r <= (others => '0');
n_frm <= 0; n_err <= 0;
elsif rising_edge(clk) then
-- The correct trigger is a CHANGE in the synchronised toggle -- caught at any clock ratio,
-- because a toggle persists until the next word. The faulty trigger is a RISING EDGE of the
-- synchronised pulse, which exists only if a receiving clock edge happened to fall inside it.
--
-- Note what is NOT modelled: both chains here are two flops deep. A shallow synchroniser is a
-- real defect and it is INVISIBLE in zero-delay simulation, because metastability is not
-- representable. The bug injected here is the other half of the same subject -- pulse width
-- against the receiving clock period -- and that half is fully visible.
if f_cdc then fire := (pls_sr(1) = '1') and (pls_sr(2) = '0');
else fire := (req_sr(1) /= req_sr(2));
end if;
if clr = '1' then
v_r <= '0';
n_frm <= 0;
n_err <= 0;
else
v_r <= '0';
if fire then
-- Both paths read the LATCHED word, which is stable for far longer than either
-- synchroniser takes. The difference between them is only whether the capture happens
-- AT ALL -- so the crossing bug's signature is a LOST transfer rather than a wrong
-- one, and any failure metric that counts only wrong words is blind to it.
ws_r <= w_flag;
wl_r <= w_flag;
v_r <= '1';
n_frm <= n_frm + 1;
if w_flag /= word_exp then n_err <= n_err + 1; end if;
end if;
end if;
end if;
end process sys_dom;
end architecture rtl;The Bench
// spi_localise_tb.sv
//
// FIVE CONDITIONS, FOUR RUNS EACH, AND A SIGNATURE MATRIX THAT LOCALISES A FAILURE TO A LAYER.
//
// Each run drives twenty well-separated frames and reports a FAILURE COUNT that includes both wrong
// words and transfers that never arrived -- because a crossing defect loses transfers rather than
// corrupting them, and a metric that only counts wrong data is blind to half of what this chapter is
// about.
//
// THE THREE PERTURBATIONS ARE APPLIED ONE AT A TIME TO AN UNCHANGED CONDITION.
//
// RATE the SCLK period changes; the system clock does not. Only the PHASE relationship moves.
// INSTANT the bench places MOSI two nanoseconds earlier relative to the sampling edge.
// SEED a different payload permutation.
//
// WHY THE WAVEFORM IS BUILT AS A TABLE. The first version of this bench emitted SCLK and MOSI from a
// loop of `#` delays, and placing MOSI later than the half period silently LENGTHENED the half period
// instead of making the data late -- so the testbench fault it was trying to inject did not exist, and
// the run reported zero failures. Building the waveform as a table of per-nanosecond values makes the
// timing a thing that can be read and checked rather than a consequence of statement order.
//
// THE FOUR RESULTS.
//
// 1. THE THREE PERTURBATIONS ARE ORTHOGONAL ON A CORRECT DESIGN. Every one of them leaves the failure
// count at zero, which is what makes a NON-zero response to one of them informative. A perturbation
// that disturbs a working design is not an instrument.
//
// 2. EACH FAULT RESPONDS TO EXACTLY ONE PERTURBATION. The bench requires each row to differ from its
// baseline in one column and to match it in the other two -- so the signature matrix is diagonal by
// measurement rather than by assertion.
//
// 3. THE CROSSING DEFECT'S LOSS RATE IS PREDICTABLE, AND THAT IS THE STRONGEST CHECK HERE. A pulse of
// width W crossing into a clock of period T is caught with probability W/T, so the expected loss
// over N frames is N(1 - W/T). The bench computes that number from the periods and requires the
// measured loss to equal it. A checker that predicts a value is worth more than one that compares
// against whatever it saw last time.
//
// 4. THE CONSTRAINT-CLASS FAULT IS INDISTINGUISHABLE FROM A CORRECT DESIGN. All four of its runs report
// zero, exactly as the correct design does, and the bench requires the two rows to be IDENTICAL.
// That is an assertion that the regression is BLIND -- and it is the only honest way to publish the
// fact that a passing RTL run says nothing about whether an input-delay budget is met.
`timescale 1ns/1ps
module spi_localise_tb;
localparam int NB = 8;
localparam int DW = 32;
localparam int MAXT = 1024;
localparam int NFR = 20; // frames per run
localparam int GAP = 205; // ns of deselected time -- chosen so the frame period is NOT a
// multiple of the system clock period, so the phase DRIFTS frame to
// frame. With a period that is a multiple, every frame sees the same
// phase and a phase-dependent fault is all-or-nothing.
// The five conditions.
localparam int K_NONE = 0;
localparam int K_RTL = 1;
localparam int K_CDC = 2;
localparam int K_TB = 3;
localparam int K_CON = 4;
reg clk = 1'b0;
always #5 clk = ~clk; // a 10 ns system clock
reg rst_n = 1'b1;
reg f_rtl = 1'b0, f_cdc = 1'b0, clr = 1'b0;
reg b_sclk = 1'b0, b_cs_n = 1'b1, b_mosi = 1'b0;
reg [DW-1:0] word_exp = {DW{1'b0}};
wire sys_valid;
wire [DW-1:0] word_sys, ob_last;
wire [15:0] ob_frames, ob_err;
spi_localise #(.NB(NB), .DW(DW)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.f_rtl(f_rtl), .f_cdc(f_cdc),
.word_exp(word_exp), .clr(clr),
.sys_valid(sys_valid), .word_sys(word_sys),
.ob_frames(ob_frames), .ob_err(ob_err), .ob_last(ob_last)
);
integer errors = 0;
// Two payload permutations. No two CONSECUTIVE entries are equal in either, which matters: the
// crossing defect loses a transfer rather than corrupting one, and a lost transfer is only
// detectable as a missing frame -- so the payload sequence must not be able to mask it.
reg [7:0] PAT_A [0:7];
reg [7:0] PAT_B [0:7];
// ---- the waveform tables ----
reg w_sclk [0:MAXT-1];
reg w_cs [0:MAXT-1];
reg w_mosi [0:MAXT-1];
integer w_len;
// Build one frame at half-period `ph`, with MOSI placed `toff` ns after each trailing edge.
//
// The bit time is ALWAYS 2*ph regardless of `toff`. That is the property the first version of this
// bench broke: if placing MOSI late is allowed to stretch the bit time, then "late data" becomes
// "a slower clock" and the fault being injected does not exist.
task automatic build(input [7:0] w, input integer ph, input integer toff);
integer t, i, j, le, te, vt;
reg cur;
begin
w_len = ph + NB*2*ph + ph + GAP;
for (t = 0; t < MAXT; t = t + 1) begin
w_sclk[t] = 1'b0;
w_cs[t] = 1'b1;
w_mosi[t] = 1'b0;
end
for (t = 0; t < ph + NB*2*ph + ph; t = t + 1) w_cs[t] = 1'b0;
for (t = 0; t < w_len; t = t + 1) begin
cur = 1'b0;
for (i = 0; i < NB; i = i + 1) begin
le = ph + i*2*ph;
te = le + ph;
if (t >= le && t < te) cur = 1'b1;
end
w_sclk[t] = cur;
end
// Bit 0 is on the pin from the first nanosecond of the frame. Bit j is placed `toff` ns after
// the trailing edge of bit-time j-1, which for toff > ph lands AFTER the edge that samples it.
for (j = 0; j < NB; j = j + 1) begin
if (j == 0) vt = 1;
else vt = ph + (j-1)*2*ph + ph + toff;
if (vt < 0) vt = 0;
for (t = vt; t < MAXT; t = t + 1) w_mosi[t] = w[NB-1-j];
end
end
endtask
task automatic drive;
integer t;
begin
for (t = 0; t < w_len; t = t + 1) begin
b_sclk = w_sclk[t];
b_cs_n = w_cs[t];
b_mosi = w_mosi[t];
#1;
end
end
endtask
// ---- one run ----
integer r_fail [0:23];
integer r_frm [0:23];
integer r_err [0:23];
integer run_i;
task automatic run(input integer kind, input integer ph, input integer toff, input integer pat);
integer i;
reg [7:0] w;
begin
f_rtl = (kind == K_RTL);
f_cdc = (kind == K_CDC);
// THE CLEAR IS PULSED ON THE NEGEDGE, and the first version of this bench pulsed it on the
// posedge -- the same edge the design samples it on. Whether the design saw the pulse then
// depended on which process the simulator happened to evaluate first, and the counters were
// not cleared between runs. Chapter 16.3's race, in the bench that measures it.
@(negedge clk); clr = 1'b1;
@(negedge clk); clr = 1'b0;
@(negedge clk);
for (i = 0; i < NFR; i = i + 1) begin
w = (pat == 0) ? PAT_A[i % 8] : PAT_B[i % 8];
word_exp = {24'b0, w};
build(w, ph, toff);
drive;
end
#40;
// THE FAILURE COUNT INCLUDES TRANSFERS THAT NEVER ARRIVED. A crossing defect loses frames; a
// metric that only counts wrong words reports it as a perfect run.
r_frm[run_i] = ob_frames;
r_err[run_i] = ob_err;
r_fail[run_i] = ob_err + (NFR - ob_frames);
run_i = run_i + 1;
end
endtask
function [8*12:1] kname(input integer k);
begin
case (k)
K_NONE: kname = "none ";
K_RTL: kname = "rtl ";
K_CDC: kname = "cdc ";
K_TB: kname = "testbench ";
default:kname = "constraint ";
endcase
end
endfunction
function [8*12:1] vname(input integer v);
begin
case (v)
0: vname = "NONE_SEEN ";
1: vname = "RTL ";
2: vname = "CDC ";
3: vname = "TESTBENCH ";
default: vname = "MIXED ";
endcase
end
endfunction
// THE CLASSIFIER, as a pure function of the four failure counts. This is the chapter's thesis
// expressed as logic: a diagnosis is a pattern of RESPONSES to perturbations, not a property of any
// one measurement.
function integer classify(input integer e0, input integer e1, input integer e2, input integer e3);
integer n;
begin
if ((e0 == 0) && (e1 == 0) && (e2 == 0) && (e3 == 0)) classify = 0;
else begin
n = 0;
if (e1 != e0) n = n + 1;
if (e2 != e0) n = n + 1;
if (e3 != e0) n = n + 1;
if (n != 1) classify = 4; // more than one axis moved it, or none did
else if (e1 != e0) classify = 2; // only the clock ratio -> a crossing
else if (e2 != e0) classify = 3; // only the bench's instant -> the bench
else classify = 1; // only the payload -> logic
end
end
endfunction
// The base configuration, and the three perturbations of it.
localparam int PH0 = 2; // system clock 10 ns, so SCLK period 4 ns
localparam int PH1 = 14; // SCLK period 28 ns -- a different PHASE relationship, nothing else
integer ci, base_off, v, want, mutations;
integer b0, b1, b2, b3;
integer exp_loss;
initial begin
PAT_A[0]=8'h8D; PAT_A[1]=8'hC3; PAT_A[2]=8'h5A; PAT_A[3]=8'h3C;
PAT_A[4]=8'hF0; PAT_A[5]=8'h96; PAT_A[6]=8'h69; PAT_A[7]=8'hA5;
PAT_B[0]=8'h71; PAT_B[1]=8'h2E; PAT_B[2]=8'hB4; PAT_B[3]=8'h4B;
PAT_B[4]=8'h0F; PAT_B[5]=8'hE1; PAT_B[6]=8'h1E; PAT_B[7]=8'hD2;
run_i = 0; mutations = 0;
rst_n = 1'b1; @(negedge clk); rst_n = 1'b0;
repeat (4) @(negedge clk); rst_n = 1'b1; repeat (4) @(negedge clk);
$display(" condition base rate instant seed verdict expected what it means");
for (ci = 0; ci < 5; ci = ci + 1) begin
// The bench's MOSI offset for this condition. The TESTBENCH fault places the data AFTER the
// edge that samples it; the CONSTRAINT fault places it late but still inside the bit time,
// which RTL tolerates completely and a timing budget might not.
base_off = (ci == K_TB) ? PH0 + 1 :
(ci == K_CON) ? PH0 - 1 : 1;
run(ci, PH0, base_off, 0); // base
run(ci, PH1, (ci == K_TB) ? PH1 + 1 :
(ci == K_CON) ? PH1 - 1 : 1, 0); // RATE
run(ci, PH0, (base_off - 2 < 0) ? 0 : base_off - 2, 0); // INSTANT
run(ci, PH0, base_off, 1); // SEED
b0 = r_fail[run_i-4]; b1 = r_fail[run_i-3];
b2 = r_fail[run_i-2]; b3 = r_fail[run_i-1];
v = classify(b0, b1, b2, b3);
want = (ci == K_NONE) ? 0 : (ci == K_RTL) ? 1 : (ci == K_CDC) ? 2 :
(ci == K_TB) ? 3 : 0;
$display(" %s %5d %5d %7d %5d %s %s %0s",
kname(ci), b0, b1, b2, b3, vname(v), vname(want),
(ci == K_NONE) ? "nothing responds to anything" :
(ci == K_RTL) ? "only the PAYLOAD moved it" :
(ci == K_CDC) ? "only the clock RATIO moved it" :
(ci == K_TB) ? "only the bench's INSTANT moved it" :
"NOTHING moved it -- and nothing failed");
if (v != want) begin
$display(" FAIL: condition %s classified %s where %s was expected",
kname(ci), vname(v), vname(want));
errors = errors + 1;
end
end
// ================= 1. the perturbations do not disturb a correct design =================
if (!(r_fail[0] == 0 && r_fail[1] == 0 && r_fail[2] == 0 && r_fail[3] == 0)) begin
$display(" FAIL: a perturbation disturbed the correct design (%0d, %0d, %0d, %0d); a perturbation that breaks a working design is not an instrument",
r_fail[0], r_fail[1], r_fail[2], r_fail[3]);
errors = errors + 1;
end
$display("");
$display(" 1. all three perturbations left the correct design at zero failures. That is what makes a non-zero response informative at all -- a knob that disturbs a working design measures the knob rather than the design, and every conclusion below rests on this row");
// ================= 2. each fault responds to exactly one perturbation =================
// rtl: only the seed. cdc: only the rate. tb: only the instant.
if (!(r_fail[5] == r_fail[4] && r_fail[6] == r_fail[4] && r_fail[7] != r_fail[4])) begin
$display(" FAIL: the logic bug did not respond to the payload alone (%0d | %0d %0d %0d)",
r_fail[4], r_fail[5], r_fail[6], r_fail[7]);
errors = errors + 1;
end
if (!(r_fail[9] != r_fail[8] && r_fail[10] == r_fail[8] && r_fail[11] == r_fail[8])) begin
$display(" FAIL: the crossing defect did not respond to the clock ratio alone (%0d | %0d %0d %0d)",
r_fail[8], r_fail[9], r_fail[10], r_fail[11]);
errors = errors + 1;
end
if (!(r_fail[13] == r_fail[12] && r_fail[14] != r_fail[12] && r_fail[15] == r_fail[12])) begin
$display(" FAIL: the bench fault did not respond to the sampling instant alone (%0d | %0d %0d %0d)",
r_fail[12], r_fail[13], r_fail[14], r_fail[15]);
errors = errors + 1;
end
$display(" 2. each fault responded to exactly ONE perturbation and to neither of the others. The logic bug moved with the payload (%0d against %0d) and not with the rate or the instant; the crossing defect moved with the rate (%0d against %0d) and not with the payload; the bench fault moved with the instant (%0d against %0d) and not with either of the others. The matrix is diagonal by measurement, which is the only way a signature table is worth anything",
r_fail[4], r_fail[7], r_fail[8], r_fail[9], r_fail[12], r_fail[14]);
// ================= 3. the crossing defect's loss rate is PREDICTED =================
// A pulse of width W crossing into a clock of period T is caught with probability W/T, so the
// expected loss over N frames is N*(1 - W/T). Here W = 3*ph (the request is asserted at the last
// leading edge and cleared when the frame ends) and T = 10 ns.
exp_loss = (NFR * (10 - 3*PH0)) / 10;
if ((NFR - r_frm[8]) != exp_loss) begin
$display(" FAIL: the crossing defect lost %0d frames where the pulse-width arithmetic predicts %0d; a predicted number that does not match is either a wrong model or a wrong design",
NFR - r_frm[8], exp_loss);
errors = errors + 1;
end
if (r_err[8] != 0) begin
$display(" FAIL: the crossing defect corrupted %0d words; its signature should be LOST transfers, not wrong ones",
r_err[8]);
errors = errors + 1;
end
$display(" 3. the crossing defect lost %0d of %0d transfers, and the pulse-width arithmetic predicted exactly that: the request is asserted at the last SCLK edge and cleared when the frame ends, so it is %0d ns wide against a %0d ns receiving clock, caught with probability %0d/10, and %0d*(1 - %0d/10) = %0d. Not one word was corrupted -- the signature of a crossing defect is a transfer that never arrives, which a scoreboard comparing words reports as a perfect run",
NFR - r_frm[8], NFR, 3*PH0, 10, 3*PH0, NFR, 3*PH0, exp_loss);
// ================= 4. the regression is BLIND to the constraint class =================
if (!(r_fail[16] == r_fail[0] && r_fail[17] == r_fail[1]
&& r_fail[18] == r_fail[2] && r_fail[19] == r_fail[3])) begin
$display(" FAIL: the constraint-class fault differed from the correct design somewhere (%0d %0d %0d %0d against %0d %0d %0d %0d); if it is visible, the chapter's central claim is wrong",
r_fail[16], r_fail[17], r_fail[18], r_fail[19],
r_fail[0], r_fail[1], r_fail[2], r_fail[3]);
errors = errors + 1;
end
$display(" 4. the constraint-class fault produced the IDENTICAL signature to a correct design -- zero in all four columns -- and the bench requires that rather than hoping for it. RTL has no delays, so a violated input-delay budget is not expressible in it: MOSI arriving late but inside the bit time is simply data that arrived. The consequence is the one sentence in this chapter worth memorising: a PASSING RTL REGRESSION IS NOT EVIDENCE THAT A CONSTRAINT IS RIGHT, and a failure that does not reproduce in simulation is EVIDENCE FOR this class rather than an absence of information");
// ================= BENCH INTEGRITY =================
// COUNTED IN THE POSITIVE SENSE. The first version of this counted the faults that did NOT fire
// and then required the count to be 2 -- so a run in which both injected faults were silent would
// have passed, and a run in which both fired reported that neither had. A polarity error in a
// self-check is the one bug that makes every other check in a bench meaningless.
if (r_fail[4] != 0) mutations = mutations + 1; // the logic bug must actually fail
if (r_frm[8] != NFR) mutations = mutations + 1; // the crossing defect must actually lose frames
if (mutations != 2) begin
$display(" FAIL: an injected fault produced no failures at all (%0d of 2 fired), so the matrix above is a table of zeros",
mutations);
errors = errors + 1;
end
mutations = 0;
if (classify(0, 0, 0, 0) != 0) mutations = mutations + 1;
if (classify(5, 9, 5, 5) != 2) mutations = mutations + 1;
if (classify(5, 9, 2, 5) != 4) mutations = mutations + 1;
if (mutations != 0) begin
$display(" FAIL: the classifier misclassified a hand-constructed signature");
errors = errors + 1;
end
if (run_i != 20) begin
$display(" FAIL: %0d runs were driven where 20 were expected", run_i);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: both injected faults actually failed, the classifier was checked against three hand-constructed signatures including a MIXED one, and all %0d runs were driven",
run_i);
$display("PASS: a failure can be localised to a LAYER by how it responds to perturbations rather than by anything visible in one capture. Three perturbations -- the clock ratio, the instant the bench presents data, and the payload sequence -- leave a correct design at zero failures, which is what makes any response to them informative. A logic bug with a data-dependent trigger moved with the payload alone, %0d failures against %0d. A crossing defect moved with the clock ratio alone, %0d against %0d, and its signature was %0d LOST transfers with not one corrupted word -- a scoreboard comparing words would have called that run perfect. A bench fault moved with the sampling instant alone, %0d against %0d. The loss rate was PREDICTED rather than observed: a request pulse %0d ns wide crossing into a %0d ns clock is caught with probability %0d/10, so %0d frames should be lost and %0d were. And the fourth row is the one to carry away: a CONSTRAINT-class fault produced the identical signature to a correct design, zero in every column, because RTL has no delays and a violated input-delay budget is not expressible in it. A passing RTL regression is not evidence that a constraint is right -- and a failure that does not reproduce in simulation is evidence FOR that class rather than an absence of information",
r_fail[4], r_fail[7], r_fail[8], r_fail[9], NFR - r_frm[8],
r_fail[12], r_fail[14], 3*PH0, 10, 3*PH0, exp_loss, NFR - r_frm[8]);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
endmodule// spi_localise_tb.v
//
// FIVE CONDITIONS, FOUR RUNS EACH, AND A SIGNATURE MATRIX THAT LOCALISES A FAILURE TO A LAYER.
//
// Each run drives twenty well-separated frames and reports a FAILURE COUNT that includes both wrong
// words and transfers that never arrived -- because a crossing defect loses transfers rather than
// corrupting them, and a metric that only counts wrong data is blind to half of what this chapter is
// about.
//
// THE THREE PERTURBATIONS ARE APPLIED ONE AT A TIME TO AN UNCHANGED CONDITION.
//
// RATE the SCLK period changes; the system clock does not. Only the PHASE relationship moves.
// INSTANT the bench places MOSI two nanoseconds earlier relative to the sampling edge.
// SEED a different payload permutation.
//
// WHY THE WAVEFORM IS BUILT AS A TABLE. The first version of this bench emitted SCLK and MOSI from a
// loop of `#` delays, and placing MOSI later than the half period silently LENGTHENED the half period
// instead of making the data late -- so the testbench fault it was trying to inject did not exist, and
// the run reported zero failures. Building the waveform as a table of per-nanosecond values makes the
// timing a thing that can be read and checked rather than a consequence of statement order.
//
// THE FOUR RESULTS.
//
// 1. THE THREE PERTURBATIONS ARE ORTHOGONAL ON A CORRECT DESIGN. Every one of them leaves the failure
// count at zero, which is what makes a NON-zero response to one of them informative. A perturbation
// that disturbs a working design is not an instrument.
//
// 2. EACH FAULT RESPONDS TO EXACTLY ONE PERTURBATION. The bench requires each row to differ from its
// baseline in one column and to match it in the other two -- so the signature matrix is diagonal by
// measurement rather than by assertion.
//
// 3. THE CROSSING DEFECT'S LOSS RATE IS PREDICTABLE, AND THAT IS THE STRONGEST CHECK HERE. A pulse of
// width W crossing into a clock of period T is caught with probability W/T, so the expected loss
// over N frames is N(1 - W/T). The bench computes that number from the periods and requires the
// measured loss to equal it. A checker that predicts a value is worth more than one that compares
// against whatever it saw last time.
//
// 4. THE CONSTRAINT-CLASS FAULT IS INDISTINGUISHABLE FROM A CORRECT DESIGN. All four of its runs report
// zero, exactly as the correct design does, and the bench requires the two rows to be IDENTICAL.
// That is an assertion that the regression is BLIND -- and it is the only honest way to publish the
// fact that a passing RTL run says nothing about whether an input-delay budget is met.
`timescale 1ns/1ps
module spi_localise_tb;
localparam NB = 8;
localparam DW = 32;
localparam MAXT = 1024;
localparam NFR = 20; // frames per run
localparam GAP = 205; // ns of deselected time -- chosen so the frame period is NOT a
// multiple of the system clock period, so the phase DRIFTS frame to
// frame. With a period that is a multiple, every frame sees the same
// phase and a phase-dependent fault is all-or-nothing.
// The five conditions.
localparam K_NONE = 0;
localparam K_RTL = 1;
localparam K_CDC = 2;
localparam K_TB = 3;
localparam K_CON = 4;
reg clk;
always #5 clk = ~clk; // a 10 ns system clock
reg rst_n;
reg f_rtl, f_cdc, clr;
reg b_sclk, b_cs_n, b_mosi;
reg [DW-1:0] word_exp;
wire sys_valid;
wire [DW-1:0] word_sys, ob_last;
wire [15:0] ob_frames, ob_err;
spi_localise #(.NB(NB), .DW(DW)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
.f_rtl(f_rtl), .f_cdc(f_cdc),
.word_exp(word_exp), .clr(clr),
.sys_valid(sys_valid), .word_sys(word_sys),
.ob_frames(ob_frames), .ob_err(ob_err), .ob_last(ob_last)
);
integer errors;
// Two payload permutations. No two CONSECUTIVE entries are equal in either, which matters: the
// crossing defect loses a transfer rather than corrupting one, and a lost transfer is only
// detectable as a missing frame -- so the payload sequence must not be able to mask it.
reg [7:0] PAT_A [0:7];
reg [7:0] PAT_B [0:7];
// ---- the waveform tables ----
reg w_sclk [0:MAXT-1];
reg w_cs [0:MAXT-1];
reg w_mosi [0:MAXT-1];
integer w_len;
// Build one frame at half-period `ph`, with MOSI placed `toff` ns after each trailing edge.
//
// The bit time is ALWAYS 2*ph regardless of `toff`. That is the property the first version of this
// bench broke: if placing MOSI late is allowed to stretch the bit time, then "late data" becomes
// "a slower clock" and the fault being injected does not exist.
task build;
input [7:0] w;
input integer ph;
input integer toff;
integer t, i, j, le, te, vt;
reg cur;
begin
w_len = ph + NB*2*ph + ph + GAP;
for (t = 0; t < MAXT; t = t + 1) begin
w_sclk[t] = 1'b0;
w_cs[t] = 1'b1;
w_mosi[t] = 1'b0;
end
for (t = 0; t < ph + NB*2*ph + ph; t = t + 1) w_cs[t] = 1'b0;
for (t = 0; t < w_len; t = t + 1) begin
cur = 1'b0;
for (i = 0; i < NB; i = i + 1) begin
le = ph + i*2*ph;
te = le + ph;
if (t >= le && t < te) cur = 1'b1;
end
w_sclk[t] = cur;
end
// Bit 0 is on the pin from the first nanosecond of the frame. Bit j is placed `toff` ns after
// the trailing edge of bit-time j-1, which for toff > ph lands AFTER the edge that samples it.
for (j = 0; j < NB; j = j + 1) begin
if (j == 0) vt = 1;
else vt = ph + (j-1)*2*ph + ph + toff;
if (vt < 0) vt = 0;
for (t = vt; t < MAXT; t = t + 1) w_mosi[t] = w[NB-1-j];
end
end
endtask
task drive;
integer t;
begin
for (t = 0; t < w_len; t = t + 1) begin
b_sclk = w_sclk[t];
b_cs_n = w_cs[t];
b_mosi = w_mosi[t];
#1;
end
end
endtask
// ---- one run ----
integer r_fail [0:23];
integer r_frm [0:23];
integer r_err [0:23];
integer run_i;
task run;
input integer kind;
input integer ph;
input integer toff;
input integer pat;
integer i;
reg [7:0] w;
begin
f_rtl = (kind == K_RTL);
f_cdc = (kind == K_CDC);
// THE CLEAR IS PULSED ON THE NEGEDGE, and the first version of this bench pulsed it on the
// posedge -- the same edge the design samples it on. Whether the design saw the pulse then
// depended on which process the simulator happened to evaluate first, and the counters were
// not cleared between runs. Chapter 16.3's race, in the bench that measures it.
@(negedge clk); clr = 1'b1;
@(negedge clk); clr = 1'b0;
@(negedge clk);
for (i = 0; i < NFR; i = i + 1) begin
w = (pat == 0) ? PAT_A[i % 8] : PAT_B[i % 8];
word_exp = {24'b0, w};
build(w, ph, toff);
drive;
end
#40;
// THE FAILURE COUNT INCLUDES TRANSFERS THAT NEVER ARRIVED. A crossing defect loses frames; a
// metric that only counts wrong words reports it as a perfect run.
r_frm[run_i] = ob_frames;
r_err[run_i] = ob_err;
r_fail[run_i] = ob_err + (NFR - ob_frames);
run_i = run_i + 1;
end
endtask
function [8*12:1] kname;
input integer k;
begin
case (k)
K_NONE: kname = "none ";
K_RTL: kname = "rtl ";
K_CDC: kname = "cdc ";
K_TB: kname = "testbench ";
default:kname = "constraint ";
endcase
end
endfunction
function [8*12:1] vname;
input integer v;
begin
case (v)
0: vname = "NONE_SEEN ";
1: vname = "RTL ";
2: vname = "CDC ";
3: vname = "TESTBENCH ";
default: vname = "MIXED ";
endcase
end
endfunction
// THE CLASSIFIER, as a pure function of the four failure counts. This is the chapter's thesis
// expressed as logic: a diagnosis is a pattern of RESPONSES to perturbations, not a property of any
// one measurement.
function integer classify;
input integer e0;
input integer e1;
input integer e2;
input integer e3;
integer n;
begin
if ((e0 == 0) && (e1 == 0) && (e2 == 0) && (e3 == 0)) classify = 0;
else begin
n = 0;
if (e1 != e0) n = n + 1;
if (e2 != e0) n = n + 1;
if (e3 != e0) n = n + 1;
if (n != 1) classify = 4; // more than one axis moved it, or none did
else if (e1 != e0) classify = 2; // only the clock ratio -> a crossing
else if (e2 != e0) classify = 3; // only the bench's instant -> the bench
else classify = 1; // only the payload -> logic
end
end
endfunction
// The base configuration, and the three perturbations of it.
localparam PH0 = 2; // system clock 10 ns, so SCLK period 4 ns
localparam PH1 = 14; // SCLK period 28 ns -- a different PHASE relationship, nothing else
integer ci, base_off, v, want, mutations;
integer b0, b1, b2, b3;
integer exp_loss;
initial begin
PAT_A[0]=8'h8D; PAT_A[1]=8'hC3; PAT_A[2]=8'h5A; PAT_A[3]=8'h3C;
PAT_A[4]=8'hF0; PAT_A[5]=8'h96; PAT_A[6]=8'h69; PAT_A[7]=8'hA5;
PAT_B[0]=8'h71; PAT_B[1]=8'h2E; PAT_B[2]=8'hB4; PAT_B[3]=8'h4B;
PAT_B[4]=8'h0F; PAT_B[5]=8'hE1; PAT_B[6]=8'h1E; PAT_B[7]=8'hD2;
run_i = 0; mutations = 0;
rst_n = 1'b1; @(negedge clk); rst_n = 1'b0;
repeat (4) @(negedge clk); rst_n = 1'b1; repeat (4) @(negedge clk);
$display(" condition base rate instant seed verdict expected what it means");
for (ci = 0; ci < 5; ci = ci + 1) begin
// The bench's MOSI offset for this condition. The TESTBENCH fault places the data AFTER the
// edge that samples it; the CONSTRAINT fault places it late but still inside the bit time,
// which RTL tolerates completely and a timing budget might not.
base_off = (ci == K_TB) ? PH0 + 1 :
(ci == K_CON) ? PH0 - 1 : 1;
run(ci, PH0, base_off, 0); // base
run(ci, PH1, (ci == K_TB) ? PH1 + 1 :
(ci == K_CON) ? PH1 - 1 : 1, 0); // RATE
run(ci, PH0, (base_off - 2 < 0) ? 0 : base_off - 2, 0); // INSTANT
run(ci, PH0, base_off, 1); // SEED
b0 = r_fail[run_i-4]; b1 = r_fail[run_i-3];
b2 = r_fail[run_i-2]; b3 = r_fail[run_i-1];
v = classify(b0, b1, b2, b3);
want = (ci == K_NONE) ? 0 : (ci == K_RTL) ? 1 : (ci == K_CDC) ? 2 :
(ci == K_TB) ? 3 : 0;
$display(" %0s %5d %5d %7d %5d %0s %0s %0s",
kname(ci), b0, b1, b2, b3, vname(v), vname(want),
(ci == K_NONE) ? "nothing responds to anything" :
(ci == K_RTL) ? "only the PAYLOAD moved it" :
(ci == K_CDC) ? "only the clock RATIO moved it" :
(ci == K_TB) ? "only the bench's INSTANT moved it" :
"NOTHING moved it -- and nothing failed");
if (v != want) begin
$display(" FAIL: condition %0s classified %0s where %0s was expected",
kname(ci), vname(v), vname(want));
errors = errors + 1;
end
end
// ================= 1. the perturbations do not disturb a correct design =================
if (!(r_fail[0] == 0 && r_fail[1] == 0 && r_fail[2] == 0 && r_fail[3] == 0)) begin
$display(" FAIL: a perturbation disturbed the correct design (%0d, %0d, %0d, %0d); a perturbation that breaks a working design is not an instrument",
r_fail[0], r_fail[1], r_fail[2], r_fail[3]);
errors = errors + 1;
end
$display("");
$display(" 1. all three perturbations left the correct design at zero failures. That is what makes a non-zero response informative at all -- a knob that disturbs a working design measures the knob rather than the design, and every conclusion below rests on this row");
// ================= 2. each fault responds to exactly one perturbation =================
// rtl: only the seed. cdc: only the rate. tb: only the instant.
if (!(r_fail[5] == r_fail[4] && r_fail[6] == r_fail[4] && r_fail[7] != r_fail[4])) begin
$display(" FAIL: the logic bug did not respond to the payload alone (%0d | %0d %0d %0d)",
r_fail[4], r_fail[5], r_fail[6], r_fail[7]);
errors = errors + 1;
end
if (!(r_fail[9] != r_fail[8] && r_fail[10] == r_fail[8] && r_fail[11] == r_fail[8])) begin
$display(" FAIL: the crossing defect did not respond to the clock ratio alone (%0d | %0d %0d %0d)",
r_fail[8], r_fail[9], r_fail[10], r_fail[11]);
errors = errors + 1;
end
if (!(r_fail[13] == r_fail[12] && r_fail[14] != r_fail[12] && r_fail[15] == r_fail[12])) begin
$display(" FAIL: the bench fault did not respond to the sampling instant alone (%0d | %0d %0d %0d)",
r_fail[12], r_fail[13], r_fail[14], r_fail[15]);
errors = errors + 1;
end
$display(" 2. each fault responded to exactly ONE perturbation and to neither of the others. The logic bug moved with the payload (%0d against %0d) and not with the rate or the instant; the crossing defect moved with the rate (%0d against %0d) and not with the payload; the bench fault moved with the instant (%0d against %0d) and not with either of the others. The matrix is diagonal by measurement, which is the only way a signature table is worth anything",
r_fail[4], r_fail[7], r_fail[8], r_fail[9], r_fail[12], r_fail[14]);
// ================= 3. the crossing defect's loss rate is PREDICTED =================
// A pulse of width W crossing into a clock of period T is caught with probability W/T, so the
// expected loss over N frames is N*(1 - W/T). Here W = 3*ph (the request is asserted at the last
// leading edge and cleared when the frame ends) and T = 10 ns.
exp_loss = (NFR * (10 - 3*PH0)) / 10;
if ((NFR - r_frm[8]) != exp_loss) begin
$display(" FAIL: the crossing defect lost %0d frames where the pulse-width arithmetic predicts %0d; a predicted number that does not match is either a wrong model or a wrong design",
NFR - r_frm[8], exp_loss);
errors = errors + 1;
end
if (r_err[8] != 0) begin
$display(" FAIL: the crossing defect corrupted %0d words; its signature should be LOST transfers, not wrong ones",
r_err[8]);
errors = errors + 1;
end
$display(" 3. the crossing defect lost %0d of %0d transfers, and the pulse-width arithmetic predicted exactly that: the request is asserted at the last SCLK edge and cleared when the frame ends, so it is %0d ns wide against a %0d ns receiving clock, caught with probability %0d/10, and %0d*(1 - %0d/10) = %0d. Not one word was corrupted -- the signature of a crossing defect is a transfer that never arrives, which a scoreboard comparing words reports as a perfect run",
NFR - r_frm[8], NFR, 3*PH0, 10, 3*PH0, NFR, 3*PH0, exp_loss);
// ================= 4. the regression is BLIND to the constraint class =================
if (!(r_fail[16] == r_fail[0] && r_fail[17] == r_fail[1]
&& r_fail[18] == r_fail[2] && r_fail[19] == r_fail[3])) begin
$display(" FAIL: the constraint-class fault differed from the correct design somewhere (%0d %0d %0d %0d against %0d %0d %0d %0d); if it is visible, the chapter's central claim is wrong",
r_fail[16], r_fail[17], r_fail[18], r_fail[19],
r_fail[0], r_fail[1], r_fail[2], r_fail[3]);
errors = errors + 1;
end
$display(" 4. the constraint-class fault produced the IDENTICAL signature to a correct design -- zero in all four columns -- and the bench requires that rather than hoping for it. RTL has no delays, so a violated input-delay budget is not expressible in it: MOSI arriving late but inside the bit time is simply data that arrived. The consequence is the one sentence in this chapter worth memorising: a PASSING RTL REGRESSION IS NOT EVIDENCE THAT A CONSTRAINT IS RIGHT, and a failure that does not reproduce in simulation is EVIDENCE FOR this class rather than an absence of information");
// ================= BENCH INTEGRITY =================
// COUNTED IN THE POSITIVE SENSE. The first version of this counted the faults that did NOT fire
// and then required the count to be 2 -- so a run in which both injected faults were silent would
// have passed, and a run in which both fired reported that neither had. A polarity error in a
// self-check is the one bug that makes every other check in a bench meaningless.
if (r_fail[4] != 0) mutations = mutations + 1; // the logic bug must actually fail
if (r_frm[8] != NFR) mutations = mutations + 1; // the crossing defect must actually lose frames
if (mutations != 2) begin
$display(" FAIL: an injected fault produced no failures at all (%0d of 2 fired), so the matrix above is a table of zeros",
mutations);
errors = errors + 1;
end
mutations = 0;
if (classify(0, 0, 0, 0) != 0) mutations = mutations + 1;
if (classify(5, 9, 5, 5) != 2) mutations = mutations + 1;
if (classify(5, 9, 2, 5) != 4) mutations = mutations + 1;
if (mutations != 0) begin
$display(" FAIL: the classifier misclassified a hand-constructed signature");
errors = errors + 1;
end
if (run_i != 20) begin
$display(" FAIL: %0d runs were driven where 20 were expected", run_i);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: both injected faults actually failed, the classifier was checked against three hand-constructed signatures including a MIXED one, and all %0d runs were driven",
run_i);
$display("PASS: a failure can be localised to a LAYER by how it responds to perturbations rather than by anything visible in one capture. Three perturbations -- the clock ratio, the instant the bench presents data, and the payload sequence -- leave a correct design at zero failures, which is what makes any response to them informative. A logic bug with a data-dependent trigger moved with the payload alone, %0d failures against %0d. A crossing defect moved with the clock ratio alone, %0d against %0d, and its signature was %0d LOST transfers with not one corrupted word -- a scoreboard comparing words would have called that run perfect. A bench fault moved with the sampling instant alone, %0d against %0d. The loss rate was PREDICTED rather than observed: a request pulse %0d ns wide crossing into a %0d ns clock is caught with probability %0d/10, so %0d frames should be lost and %0d were. And the fourth row is the one to carry away: a CONSTRAINT-class fault produced the identical signature to a correct design, zero in every column, because RTL has no delays and a violated input-delay budget is not expressible in it. A passing RTL regression is not evidence that a constraint is right -- and a failure that does not reproduce in simulation is evidence FOR that class rather than an absence of information",
r_fail[4], r_fail[7], r_fail[8], r_fail[9], NFR - r_frm[8],
r_fail[12], r_fail[14], 3*PH0, 10, 3*PH0, exp_loss, NFR - r_frm[8]);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
initial begin
f_rtl = 1'b0;
f_cdc = 1'b0;
clr = 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}};
errors = 0;
end
endmodule-- spi_localise_tb.vhd
--
-- FIVE CONDITIONS, FOUR RUNS EACH, AND A SIGNATURE MATRIX THAT LOCALISES A FAILURE TO A LAYER.
--
-- Each run drives twenty well-separated frames and reports a FAILURE COUNT that includes both wrong words
-- and transfers that never arrived -- because a crossing defect loses transfers rather than corrupting
-- them, and a metric that only counts wrong data is blind to half of what this chapter is about.
--
-- The same four results as the other two languages, with the same numbers. The third implementation earns
-- its place here in a specific way: the CLASSIFIER is written against an enumeration rather than against
-- integers, so a verdict outside the defined set is not expressible, and the `MIXED` case -- more than one
-- perturbation moved the count -- is a named value rather than a fall-through.
--
-- WHY THE WAVEFORM IS BUILT AS A TABLE. The first version of this bench emitted SCLK and MOSI from a loop
-- of waits, and placing MOSI later than the half period silently LENGTHENED the half period instead of
-- making the data late -- so the testbench fault it was trying to inject did not exist and the run reported
-- zero failures. A table of per-nanosecond values makes the timing something a reader can check.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `NB_C`, `DW_C`, `MAXT_C`, `NFR_C`, `GAP_C`, `PH0_C` and
-- `PH1_C` all carry suffixes so nothing can shadow them in another case; the condition and verdict
-- selectors are enumerations rather than integers.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
entity spi_localise_tb is
end entity spi_localise_tb;
architecture tb of spi_localise_tb is
constant NB_C : positive := 8;
constant DW_C : positive := 32;
constant MAXT_C : natural := 1024;
constant NFR_C : natural := 20; -- frames per run
-- Chosen so the frame period is NOT a multiple of the system clock period, so the phase DRIFTS frame
-- to frame. With a period that is a multiple, every frame sees the same phase and a phase-dependent
-- fault is all-or-nothing rather than a fraction.
constant GAP_C : natural := 205;
constant PH0_C : natural := 2; -- system clock 10 ns, so SCLK period 4 ns
constant PH1_C : natural := 14; -- SCLK period 28 ns: a different PHASE relationship, nothing else
subtype byte_t is std_logic_vector(7 downto 0);
type cond_t is (K_NONE, K_RTL, K_CDC, K_TB, K_CON);
type verdict_t is (V_NONE_SEEN, V_RTL, V_CDC, V_TESTBENCH, V_MIXED);
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal run : boolean := true;
signal f_rtl, f_cdc : boolean := false;
signal clr : std_logic := '0';
signal b_sclk : std_logic := '0';
signal b_cs_n : std_logic := '1';
signal b_mosi : std_logic := '0';
signal word_exp : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal sys_valid : std_logic;
signal word_sys, ob_last : std_logic_vector(DW_C - 1 downto 0);
signal ob_frames, ob_err : natural;
type nat_arr is array (natural range <>) of natural;
type byte_arr is array (natural range <>) of byte_t;
-- Two payload permutations. No two CONSECUTIVE entries are equal in either, which matters: the crossing
-- defect loses a transfer rather than corrupting one, so the payload sequence must not be able to mask
-- a missing frame.
constant PAT_A_C : byte_arr(0 to 7) :=
(x"8D", x"C3", x"5A", x"3C", x"F0", x"96", x"69", x"A5");
constant PAT_B_C : byte_arr(0 to 7) :=
(x"71", x"2E", x"B4", x"4B", x"0F", x"E1", x"1E", x"D2");
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 kname (k : cond_t) return string is
begin
case k is
when K_NONE => return "none ";
when K_RTL => return "rtl ";
when K_CDC => return "cdc ";
when K_TB => return "testbench ";
when others => return "constraint ";
end case;
end function kname;
function vname (v : verdict_t) return string is
begin
case v is
when V_NONE_SEEN => return "NONE_SEEN ";
when V_RTL => return "RTL ";
when V_CDC => return "CDC ";
when V_TESTBENCH => return "TESTBENCH ";
when others => return "MIXED ";
end case;
end function vname;
-- THE CLASSIFIER, a pure function of the four failure counts: the chapter's thesis as logic. A diagnosis
-- is a pattern of RESPONSES to perturbations, not a property of any one measurement.
function classify (e0, e1, e2, e3 : natural) return verdict_t is
variable n : natural := 0;
begin
if e0 = 0 and e1 = 0 and e2 = 0 and e3 = 0 then
return V_NONE_SEEN;
end if;
if e1 /= e0 then n := n + 1; end if;
if e2 /= e0 then n := n + 1; end if;
if e3 /= e0 then n := n + 1; end if;
if n /= 1 then return V_MIXED; -- more than one axis moved it, or none did
elsif e1 /= e0 then return V_CDC; -- only the clock ratio -> a crossing
elsif e2 /= e0 then return V_TESTBENCH; -- only the bench's instant -> the bench
else return V_RTL; -- only the payload -> logic
end if;
end function classify;
function means_text (k : cond_t) return string is
begin
case k is
when K_NONE => return "nothing responds to anything";
when K_RTL => return "only the PAYLOAD moved it";
when K_CDC => return "only the clock RATIO moved it";
when K_TB => return "only the bench's INSTANT moved it";
when others => return "NOTHING moved it -- and nothing failed";
end case;
end function means_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_localise
generic map (NB_C => NB_C, DW_C => DW_C)
port map (
clk => clk, rst_n => rst_n,
sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
f_rtl => f_rtl, f_cdc => f_cdc,
word_exp => word_exp, clr => clr,
sys_valid => sys_valid, word_sys => word_sys,
ob_frames => ob_frames, ob_err => ob_err, ob_last => ob_last
);
stim : process is
type tick_arr is array (0 to MAXT_C - 1) of std_logic;
variable w_sclk, w_cs, w_mosi : tick_arr;
variable w_len : natural;
variable r_fail, r_frm, r_err : nat_arr(0 to 23);
variable run_i, e, fired, bad_cls : natural := 0;
variable b0, b1, b2, b3, exp_loss, base_off : natural;
-- An INTEGER, because `base_off - 2` is legitimately negative for the baseline offset of 1 and a
-- natural-typed variable would take a range fault rather than clamping. `integer'max` is a
-- VHDL-2019 attribute and is not available here.
variable inst_off : integer;
variable v, want : verdict_t;
variable ln : line;
-- Build one frame at half-period `ph`, with MOSI placed `toff` ns after each trailing edge.
--
-- The bit time is ALWAYS 2*ph regardless of `toff`. That is the property the first version of this
-- bench broke: if placing MOSI late is allowed to stretch the bit time, then "late data" becomes
-- "a slower clock" and the fault being injected does not exist.
procedure build (w : byte_t; ph : natural; toff : integer) is
variable le, te, vt : integer;
variable cur : std_logic;
begin
w_len := ph + NB_C*2*ph + ph + GAP_C;
for t in 0 to MAXT_C - 1 loop
w_sclk(t) := '0'; w_cs(t) := '1'; w_mosi(t) := '0';
end loop;
for t in 0 to ph + NB_C*2*ph + ph - 1 loop
w_cs(t) := '0';
end loop;
for t in 0 to w_len - 1 loop
cur := '0';
for i in 0 to NB_C - 1 loop
le := ph + i*2*ph;
te := le + ph;
if t >= le and t < te then cur := '1'; end if;
end loop;
w_sclk(t) := cur;
end loop;
-- Bit 0 is on the pin from the first nanosecond. Bit j is placed `toff` ns after the trailing
-- edge of bit-time j-1, which for toff > ph lands AFTER the edge that samples it.
for j in 0 to NB_C - 1 loop
if j = 0 then vt := 1;
else vt := ph + (j-1)*2*ph + ph + toff;
end if;
if vt < 0 then vt := 0; end if;
for t in vt to MAXT_C - 1 loop
w_mosi(t) := w(NB_C - 1 - j);
end loop;
end loop;
end procedure build;
procedure drive is
begin
for t in 0 to w_len - 1 loop
b_sclk <= w_sclk(t);
b_cs_n <= w_cs(t);
b_mosi <= w_mosi(t);
wait for 1 ns;
end loop;
end procedure drive;
procedure do_run (kind : cond_t; ph : natural; toff : integer; pat : natural) is
variable w : byte_t;
begin
f_rtl <= (kind = K_RTL);
f_cdc <= (kind = K_CDC);
-- THE CLEAR IS PULSED ON THE FALLING EDGE, and the first version of this bench pulsed it on the
-- rising edge -- the same edge the design samples it on. Whether the design saw the pulse then
-- depended on which process the simulator happened to evaluate first, and the counters were not
-- cleared between runs. Chapter 16.3's race, in the bench that measures it.
wait until falling_edge(clk); clr <= '1';
wait until falling_edge(clk); clr <= '0';
wait until falling_edge(clk);
for i in 0 to NFR_C - 1 loop
if pat = 0 then w := PAT_A_C(i mod 8); else w := PAT_B_C(i mod 8); end if;
word_exp <= x"000000" & w;
wait for 1 ns;
build(w, ph, toff);
drive;
end loop;
wait for 40 ns;
-- THE FAILURE COUNT INCLUDES TRANSFERS THAT NEVER ARRIVED.
r_frm(run_i) := ob_frames;
r_err(run_i) := ob_err;
r_fail(run_i) := ob_err + (NFR_C - ob_frames);
run_i := run_i + 1;
end procedure do_run;
begin
rst_n <= '1';
wait until falling_edge(clk);
rst_n <= '0';
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
rst_n <= '1';
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
write(ln, string'(" condition base rate instant seed verdict expected what it means"));
writeline(output, ln);
for ci in cond_t'pos(K_NONE) to cond_t'pos(K_CON) loop
-- The bench's MOSI offset for this condition. The TESTBENCH fault places the data AFTER the edge
-- that samples it; the CONSTRAINT fault places it late but still inside the bit time, which RTL
-- tolerates completely and a timing budget might not.
if cond_t'val(ci) = K_TB then base_off := PH0_C + 1;
elsif cond_t'val(ci) = K_CON then base_off := PH0_C - 1;
else base_off := 1;
end if;
do_run(cond_t'val(ci), PH0_C, base_off, 0);
if cond_t'val(ci) = K_TB then do_run(cond_t'val(ci), PH1_C, PH1_C + 1, 0);
elsif cond_t'val(ci) = K_CON then do_run(cond_t'val(ci), PH1_C, PH1_C - 1, 0);
else do_run(cond_t'val(ci), PH1_C, 1, 0);
end if;
inst_off := base_off - 2;
if inst_off < 0 then inst_off := 0; end if;
do_run(cond_t'val(ci), PH0_C, inst_off, 0);
do_run(cond_t'val(ci), PH0_C, base_off, 1);
b0 := r_fail(run_i-4); b1 := r_fail(run_i-3);
b2 := r_fail(run_i-2); b3 := r_fail(run_i-1);
v := classify(b0, b1, b2, b3);
case cond_t'val(ci) is
when K_NONE => want := V_NONE_SEEN;
when K_RTL => want := V_RTL;
when K_CDC => want := V_CDC;
when K_TB => want := V_TESTBENCH;
when others => want := V_NONE_SEEN;
end case;
write(ln, string'(" ") & kname(cond_t'val(ci)) & string'(" ") & i2s(b0, 5)
& string'(" ") & i2s(b1, 5) & string'(" ") & i2s(b2, 7)
& string'(" ") & i2s(b3, 5) & string'(" ") & vname(v)
& string'(" ") & vname(want) & string'(" ") & means_text(cond_t'val(ci)));
writeline(output, ln);
if v /= want then
write(ln, string'(" FAIL: condition ") & kname(cond_t'val(ci))
& string'(" classified ") & vname(v) & string'(" where ") & vname(want)
& string'(" was expected"));
writeline(output, ln); e := e + 1;
end if;
end loop;
-- ================= 1. the perturbations do not disturb a correct design =================
if not (r_fail(0) = 0 and r_fail(1) = 0 and r_fail(2) = 0 and r_fail(3) = 0) then
write(ln, string'(" FAIL: a perturbation disturbed the correct design; a perturbation that breaks a working design is not an instrument"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" 1. all three perturbations left the correct design at zero failures. That is what makes a non-zero response informative at all -- a knob that disturbs a working design measures the knob rather than the design, and every conclusion below rests on this row"));
writeline(output, ln);
-- ================= 2. each fault responds to exactly one perturbation =================
if not (r_fail(5) = r_fail(4) and r_fail(6) = r_fail(4) and r_fail(7) /= r_fail(4)) then
write(ln, string'(" FAIL: the logic bug did not respond to the payload alone"));
writeline(output, ln); e := e + 1;
end if;
if not (r_fail(9) /= r_fail(8) and r_fail(10) = r_fail(8) and r_fail(11) = r_fail(8)) then
write(ln, string'(" FAIL: the crossing defect did not respond to the clock ratio alone"));
writeline(output, ln); e := e + 1;
end if;
if not (r_fail(13) = r_fail(12) and r_fail(14) /= r_fail(12) and r_fail(15) = r_fail(12)) then
write(ln, string'(" FAIL: the bench fault did not respond to the sampling instant alone"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 2. each fault responded to exactly ONE perturbation and to neither of the others. The logic bug moved with the payload (")
& i2s(r_fail(4), 1) & string'(" against ") & i2s(r_fail(7), 1)
& string'(") and not with the rate or the instant; the crossing defect moved with the rate (")
& i2s(r_fail(8), 1) & string'(" against ") & i2s(r_fail(9), 1)
& string'(") and not with the payload; the bench fault moved with the instant (")
& i2s(r_fail(12), 1) & string'(" against ") & i2s(r_fail(14), 1)
& string'(") and not with either of the others. The matrix is diagonal by measurement, which is the only way a signature table is worth anything"));
writeline(output, ln);
-- ================= 3. the crossing defect's loss rate is PREDICTED =================
-- A pulse of width W crossing into a clock of period T is caught with probability W/T, so the
-- expected loss over N frames is N*(1 - W/T). Here W = 3*ph and T = 10 ns.
exp_loss := (NFR_C * (10 - 3*PH0_C)) / 10;
if (NFR_C - r_frm(8)) /= exp_loss then
write(ln, string'(" FAIL: the crossing defect lost ") & i2s(NFR_C - r_frm(8), 1)
& string'(" frames where the pulse-width arithmetic predicts ") & i2s(exp_loss, 1)
& string'("; a predicted number that does not match is either a wrong model or a wrong design"));
writeline(output, ln); e := e + 1;
end if;
if r_err(8) /= 0 then
write(ln, string'(" FAIL: the crossing defect corrupted ") & i2s(r_err(8), 1)
& string'(" words; its signature should be LOST transfers, not wrong ones"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 3. the crossing defect lost ") & i2s(NFR_C - r_frm(8), 1)
& string'(" of ") & i2s(NFR_C, 1)
& string'(" transfers, and the pulse-width arithmetic predicted exactly that: the request is asserted at the last SCLK edge and cleared when the frame ends, so it is ")
& i2s(3*PH0_C, 1) & string'(" ns wide against a 10 ns receiving clock, caught with probability ")
& i2s(3*PH0_C, 1) & string'("/10, and ") & i2s(NFR_C, 1) & string'("*(1 - ")
& i2s(3*PH0_C, 1) & string'("/10) = ") & i2s(exp_loss, 1)
& string'(". Not one word was corrupted -- the signature of a crossing defect is a transfer that never arrives, which a scoreboard comparing words reports as a perfect run"));
writeline(output, ln);
-- ================= 4. the regression is BLIND to the constraint class =================
if not (r_fail(16) = r_fail(0) and r_fail(17) = r_fail(1)
and r_fail(18) = r_fail(2) and r_fail(19) = r_fail(3)) then
write(ln, string'(" FAIL: the constraint-class fault differed from the correct design somewhere; if it is visible, the chapter's central claim is wrong"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 4. the constraint-class fault produced the IDENTICAL signature to a correct design -- zero in all four columns -- and the bench requires that rather than hoping for it. RTL has no delays, so a violated input-delay budget is not expressible in it: MOSI arriving late but inside the bit time is simply data that arrived. The consequence is the one sentence in this chapter worth memorising: a PASSING RTL REGRESSION IS NOT EVIDENCE THAT A CONSTRAINT IS RIGHT, and a failure that does not reproduce in simulation is EVIDENCE FOR this class rather than an absence of information"));
writeline(output, ln);
-- ================= BENCH INTEGRITY =================
-- COUNTED IN THE POSITIVE SENSE. Counting the faults that did NOT fire and requiring the count to be
-- two would pass a run in which both injected faults were silent -- a polarity error in a self-check
-- is the one bug that makes every other check in a bench meaningless.
fired := 0;
if r_fail(4) /= 0 then fired := fired + 1; end if;
if r_frm(8) /= NFR_C then fired := fired + 1; end if;
if fired /= 2 then
write(ln, string'(" FAIL: an injected fault produced no failures at all (")
& i2s(fired, 1) & string'(" of 2 fired), so the matrix above is a table of zeros"));
writeline(output, ln); e := e + 1;
end if;
bad_cls := 0;
if classify(0, 0, 0, 0) /= V_NONE_SEEN then bad_cls := bad_cls + 1; end if;
if classify(5, 9, 5, 5) /= V_CDC then bad_cls := bad_cls + 1; end if;
if classify(5, 9, 2, 5) /= V_MIXED then bad_cls := bad_cls + 1; end if;
if bad_cls /= 0 then
write(ln, string'(" FAIL: the classifier misclassified a hand-constructed signature"));
writeline(output, ln); e := e + 1;
end if;
if run_i /= 20 then
write(ln, string'(" FAIL: ") & i2s(run_i, 1) & string'(" runs were driven where 20 were expected"));
writeline(output, ln); e := e + 1;
end if;
if e = 0 then
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" and the bench proved itself: both injected faults actually failed, the classifier was checked against three hand-constructed signatures including a MIXED one, and all ")
& i2s(run_i, 1) & string'(" runs were driven"));
writeline(output, ln);
write(ln, string'("PASS: a failure can be localised to a LAYER by how it responds to perturbations rather than by anything visible in one capture. Three perturbations -- the clock ratio, the instant the bench presents data, and the payload sequence -- leave a correct design at zero failures, which is what makes any response to them informative. A logic bug with a data-dependent trigger moved with the payload alone, ")
& i2s(r_fail(4), 1) & string'(" failures against ") & i2s(r_fail(7), 1)
& string'(". A crossing defect moved with the clock ratio alone, ") & i2s(r_fail(8), 1)
& string'(" against ") & i2s(r_fail(9), 1) & string'(", and its signature was ")
& i2s(NFR_C - r_frm(8), 1)
& string'(" LOST transfers with not one corrupted word -- a scoreboard comparing words would have called that run perfect. A bench fault moved with the sampling instant alone, ")
& i2s(r_fail(12), 1) & string'(" against ") & i2s(r_fail(14), 1)
& string'(". The loss rate was PREDICTED rather than observed: a request pulse ")
& i2s(3*PH0_C, 1) & string'(" ns wide crossing into a 10 ns clock is caught with probability ")
& i2s(3*PH0_C, 1) & string'("/10, so ") & i2s(exp_loss, 1)
& string'(" frames should be lost and ") & i2s(NFR_C - r_frm(8), 1)
& string'(" were. And the fourth row is the one to carry away: a CONSTRAINT-class fault produced the identical signature to a correct design, zero in every column, because RTL has no delays and a violated input-delay budget is not expressible in it. A passing RTL regression is not evidence that a constraint is right -- and a failure that does not reproduce in simulation is evidence FOR that class rather than an absence of information"));
writeline(output, ln);
else
write(ln, string'("FAIL: ") & i2s(e, 1) & string'(" error(s)"));
writeline(output, ln);
end if;
run <= false;
wait;
end process stim;
end architecture tb;11. Perturbation As A Test Axis
The UVM consequence is structural: the three perturbations have to be configuration, not constants.
spi_env_cfg
rand int sclk_period_ns; // the RATE axis
rand int drive_offset_ns; // the INSTANT axis
// the SEED axis is already a UVM first-class citizenA regression that runs one clock ratio has one column of the matrix and cannot produce a diagonal. A regression that runs three ratios costs three times the wall clock and can localise a failure to a layer without anybody opening a waveform.
12. What This Method Cannot Do
✗ localise anything that does not reproduce (by construction — that IS the
fourth row, and it is a finding rather than a failure of the method)
✗ separate two simultaneous faults: the classifier reports MIXED when more
than one perturbation moves the count, and MIXED is honest, not a diagnosis
✗ detect a synchroniser that is too shallow — invisible in zero-delay RTL
✗ distinguish a logic bug whose trigger happens to be phase-dependent from a
crossing defect; nothing in real hardware makes logic phase-dependent, so
the method assumes that and the assumption is worth knowing
✗ tell you WHICH crossing, which line of RTL, or which constraintThe last one matters most in practice. This method narrows a failure to a layer and stops there. What it hands over is the applicable tool — and the six chapters before it are what you reach for once the layer is known.
13. Why an FPGA Engineer Cares
All three perturbations are things you can change from a script on real hardware. The clock divider is a register write. The payload is a constant. The bench's data placement has no hardware analogue, which is itself informative: on a board, a failure that moves with nothing but the clock divider is a crossing or a timing problem and cannot be a bench artefact.
The pulse-versus-toggle lesson is the most directly actionable thing here. If you have a signal crossing from an SPI clock domain to a system clock domain, check whether it is a pulse, and check whether its width is guaranteed to exceed the receiving clock's period. If either answer is no, the bug is already present and is waiting for a clock-frequency change to expose it — which is exactly what happens when somebody raises the SPI rate for throughput and the design "suddenly" breaks.
And the counting lesson: instrument transfer counts at both ends, not just data. A lost transfer is silent in every data check you will write.
14. Why an ASIC Engineer Cares
The four layers map onto four different sign-offs and four different owners, and getting the layer wrong wastes the most expensive weeks in a project.
The rate-invariance test belongs in the regression because it is the only one of the four perturbations that a simulation can apply to silicon behaviour by proxy. The synchroniser-depth question belongs to a CDC tool and cannot be answered by any amount of simulation. The constraint question belongs to STA, and the reason this chapter labours the fourth row is that a green regression is routinely presented as evidence that the constraints are fine — it is not evidence of anything about them.
The most valuable habit is the two-way inference in section 9. When a lab failure does not reproduce, that is a result: three of four layers are eliminated and the remaining work is timing, physical, or CDC analysis. Treating it as cannot reproduce, closing is how a silicon bug survives to the next revision.
15. Failure Signature — Six Weeks In The Wrong Layer
Symptom an SPI link drops occasional transfers at 20 MHz; clean at 5 MHz
Assumed marginal timing -- it gets better when you slow down
Actioned board respin with shorter traces and series termination
Result unchanged
Then the SPI rate was lowered permanently and the product shipped
Actual a request PULSE crossing into the system clock domain, narrower
than the receiving clock period at 20 MHz and wider at 5 MHz
Fix one line: a toggle instead of a pulse
Found by an engineer who ran the RTL regression at two SCLK periods
and saw the transfer COUNT change while no word was ever wrongIt gets better when you slow down is consistent with marginal timing and with a pulse-width crossing bug, and the two are in different layers with different owners. The perturbation that separates them is the same one in both cases — change the rate — but the observation differs: marginal timing corrupts words, and a narrow-pulse crossing loses transfers. The team never looked at the transfer count, so the distinguishing observation was never made.
The board respin was a reasonable action taken on an incomplete diagnosis, and the permanent rate reduction was a workaround that worked — which, as Chapter 18.2 and Chapter 18.6 both found, is the most durable way to lose a bug.
16. Common Misconceptions
| Misconception | What is actually true |
|---|---|
| A passing regression means the design is right | It means nothing about constraints, synchroniser depth, or physical timing |
| "Cannot reproduce in simulation" is a dead end | It eliminates three of four layers; it is a positive result |
| A scoreboard comparing words catches everything | It is blind to a transfer that never arrived |
| Slowing the clock down implies marginal timing | It equally implies a pulse-width crossing defect |
| A CDC bug needs a CDC tool to find | Pulse-width defects are fully visible in RTL; depth defects are not |
| Any clock ratio will do for a rate sweep | A ratio that is an exact multiple gives every frame the same phase |
| The bench is the last thing to suspect | It fails 100% of transfers, which is the easiest signature to recognise |
| Localising the layer is most of the work | It is the first of the work; the layer selects which tool applies |
17. Reason It Through
18. Understanding Check
19. Summary
A failure can be localised to a layer by how it responds to perturbations rather than by anything visible in one capture. Three perturbations — the clock ratio, the instant the bench presents data, and the payload sequence — leave a correct design at zero failures, and that row is what makes any response to them informative at all.
The matrix came out diagonal by measurement. A logic bug with a data-dependent trigger moved with the payload alone, 7 failures against 5. A crossing defect moved with the clock ratio alone, 8 against 0, and its signature was 8 lost transfers with not one corrupted word — a scoreboard comparing payloads would have called that run perfect, which is why the failure metric has to include transfers that never arrived. A bench fault moved with the sampling instant alone, and failed 20 of 20, which is the easiest signature in the whole module to recognise.
The loss rate was predicted rather than observed: a request pulse 6 ns wide crossing into a 10 ns clock is caught with probability 6/10, so 8 of 20 frames should be lost and 8 were. That check required an honest experiment — a frame period that is not a multiple of the receiving clock period, so the phase actually drifts.
And the fourth row is the one to carry away. A constraint-class fault produced the identical signature to a correct design, zero in every column, because RTL has no delays and a violated input-delay budget is not expressible in it. A passing RTL regression is not evidence that a constraint is right — and a failure that does not reproduce in simulation is evidence for that class rather than an absence of information.
20. The Module, Closed
Seven chapters, one method. A capture is evidence; a cause is a hypothesis; and the useful work of a debug session is the measurement that converts one into the other. Each chapter found the measurement for one fault family, and each one also measured its own blind spot — which is what separates a diagnostic from a guess with a label.
The discriminators got progressively more expensive, and that ladder is the module's real content:
18.1 one capture, classified a predicate
18.2 one capture, plus the receiver's report a transition TIME
18.3 one capture, factored into two sides a relation, and a graded PATTERN
18.4 TWO captures with different stimulus a displacement, then a second address
18.5 a SEQUENCE of frames a count of assertions, one frame late
18.6 two runs with a different SYSTEM an INTERVENTION: slow the clock
18.7 four runs across three axes a SIGNATURE, and one invisible layerKnowing which rung a problem sits on is most of the skill, and the cost of getting it wrong is always the same: effort spent at a lower rung than the fault requires. Each rung also costs more than the one below — a second capture is a line of script, a change of clock rate may mean re-qualifying a link — so the order matters in both directions. Start too high and you spend a day proving something a single capture would have shown; stay too low and you spend a week proving nothing at all.
Module 19 turns from finding faults to shipping designs: SPI as it actually appears in a system — a streaming converter with a sample deadline, a register-mapped sensor, a controller behind a register bus, and a multi-slave arbiter — each one reusing the timing, crossing and RTL reasoning this track has already built, and each one a place where the faults in this module are committed.
Continue learning
Related tutorials
- Related topic
CS Glitches and False Selection
How a noise event becomes a transaction: what a glitch does to a device's state, why the half-selected device it leaves is worse than a broken one, why a synchroniser cannot help, and the glitch filter that can.
- Related topic
Timing, Constraints, and CDC Considerations
The MISO round trip decides the maximum SCLK rate and static timing analysis never checks it, plus why this architecture has no internal clock-domain crossing and what simulation cannot establish about reset release.
- Related topic
Electrical and Board-Level Limits
Why an SPI link with identical logic runs at one clock rate and fails at a higher one. Push-pull drivers into real capacitance, the four delays inside a bit time, the round trip a returned bit must complete, and why usable SCLK is a property of the board.
- Related topic
Leading and Trailing Edges
Why rising and falling are the wrong words for an SPI transfer. How the clock's resting level decides which physical edge comes first, and the vocabulary every later timing chapter depends on.
