SPI · Module 18
Read Corruption and MISO Contention
Contention shows up as a level that is neither level. A marginal link and a structurally-early sampling instant share one verdict — and the only thing that separates them is slowing the clock down.
Every chapter so far has diagnosed by observing: a predicate, a transition time, a relation, a displacement, a count of assertions. This one cannot.
The discriminator here is not an observation. It is a change you make to the system.
1. Two Suspects, And One Of Them Is Not Digital
| Cause | Nature |
|---|---|
| Contention | two devices driving MISO at once — a select decode that overlaps, or a slave that does not release the pin |
| Sampling margin | the slave's data is not valid at the instant the master samples it |
The first is digital and countable. The second is where this chapter has to be careful, because most of it is not digital at all.
2. What RTL Can And Cannot Model — Said Up Front
This belongs here rather than in a footnote, because everything downstream depends on it.
CAN be modelled a propagation delay measured against a sampling instant
that moves with the clock period. That is the arithmetic
of a setup margin: a bit is in time exactly when its
delay is no greater than the half period.
CANNOT be modelled intermittency. Temperature and voltage dependence.
Rise times, reflections, crosstalk, ground bounce.
A flip-flop resolving metastably either way.The bench below models the first faithfully and the second not at all. So the error counts it produces are deterministic, and the thing they stand for is not: a real marginal link fails some of the time, more often when it is warm, and differently on each board.
3. Contention's Signature Is A Level, Not A Value
A contested net is not carrying the wrong bit. It is carrying neither bit — a mid-rail voltage that reads unpredictably. So the decoder counts indeterminate sampled bits separately from wrong ones, and that separation is what makes contention a finding rather than a guess.
ob_err sampled bits that differ from the expectation
ob_xbits sampled bits that were neither 0 nor 1 ← contention only
ob_overlap system-clock cycles with more than one select asserted4. The Pair That Shares A Signature
Here is the chapter's real content, and it is not contention.
A bit that arrives late makes the master sample the previous bit — a stale sample. So does a master whose sampling instant is wrong by one bit by design. Two faults, in different places, needing different fixes, producing the same evidence:
MARGINAL the data arrives late relative to a sampling instant that is
too close to it → fix the board, or slow down
STRUCTURAL the master samples at the wrong instant regardless of rate
→ fix the RTLEach bit arrives one cycle after the edge that samples it
14 cyclesNow redraw that figure with a five-cycle half period instead of two. Every arrival lands comfortably before its edge and the errors vanish. Nothing about the figure's shape changes for the structural fault, because a slave that is one bit behind is one bit behind at any rate.
5. The First Bit Is The One Bit That Says Nothing
The measurement found something worth keeping. At the fastest rate the two faults report the same verdict and the same signature — every error a stale sample — and their error counts differ by exactly four in thirty-two. That difference is entirely the first bit of each frame.
bit 0 has the whole select-to-first-edge LEAD to settle in
bit 1..7 have only a half periodSo a structurally-early slave gets bit 0 wrong too, and a marginal one does not. Which makes bit 0 the one bit in a frame that carries no information about marginal timing — and a four-in-thirty-two difference is not what anybody reads a verdict for.
6. The Discriminator Is An Intervention
There is no observation available at a single clock rate that separates the pair. The experiment has to change the system:
slow the clock down marginal → errors fall, then vanish
structural → nothing moves7. The Measurement
Four conditions at three clock rates, four frames each, identical output from all three languages:
half bits errs x stale 1st overlap verdict condition
2 32 0 0 0 0 0 CLEAN a correct link
3 32 0 0 0 0 0 CLEAN a correct link
5 32 0 0 0 0 0 CLEAN a correct link
2 32 16 16 0 0 64 CONTENTION CONTENTION -- a second select overlaps
3 32 16 16 0 0 96 CONTENTION CONTENTION -- a second select overlaps
5 32 16 16 0 0 160 CONTENTION CONTENTION -- a second select overlaps
2 32 28 0 28 0 0 STALE MARGINAL -- per-bit delays of 3, 4, 5
3 32 20 0 20 0 0 STALE MARGINAL -- per-bit delays of 3, 4, 5
5 32 0 0 0 0 0 CLEAN MARGINAL -- per-bit delays of 3, 4, 5
2 32 32 0 32 4 0 STALE STRUCTURAL -- the slave is one bit behind
3 32 32 0 32 4 0 STALE STRUCTURAL -- the slave is one bit behind
5 32 32 0 32 4 0 STALE STRUCTURAL -- the slave is one bit behindThe x column is exclusive to contention and the bench asserts it in both directions — non-zero for every contention run and zero for every other one. So indeterminate means contention is measured, not assumed.
Rows 7 and 10 are the pair. Same verdict, same all-stale signature, and the only visible difference is the 1st column: 0 against 4.
Rows 7–9 against rows 10–12 are the diagnosis. The marginal link goes 28, 20, 0 as the half-period goes 2, 3, 5. The structural fault sits at 32 throughout. Note that half is measured by the decoder from the pins rather than passed in — an error count without the rate it was taken at is not evidence of a trend, and the trend is the entire diagnosis.
The overlap column rises with the period for the contention runs — 64, 96, 160 — for the mundane reason that a slower clock makes every bit-time longer, so the same four-bit overlap window spans more system-clock cycles. That is a reminder to read accumulated counts against their denominator rather than against each other.
8. The Pattern, The Other Way Round
A stale sample returns the previous bit, so it is only visible when adjacent bits differ. Same physical fault, same clock rate, two payloads:
0xaa 7 of 7 adjacent pairs differ → 28 errors
0xf0 1 of 7 differ → 4 errors9. Building It — Three HDLs
// spi_miso_diag.sv
//
// Chapter 18.6 -- read corruption on MISO, and the first diagnosis in this module that requires
// CHANGING the system rather than observing it.
//
// THE TWO CAUSES EVERYBODY SUSPECTS, and one of them is not digital.
//
// CONTENTION two devices driving MISO at once, because a select decode overlaps or because a
// slave does not release the pin when deselected. Digital, countable, and it has a
// signature no other fault has: the sampled bits are INDETERMINATE rather than
// wrong.
//
// SAMPLING MARGIN the slave's data is not valid at the instant the master samples it. This is where
// honesty is required, and section 11 of the chapter says it in full: RTL CANNOT
// MODEL THE PHYSICAL HALF OF THIS. What can be modelled is the TIMING half -- a
// propagation delay measured against a sampling instant that moves with the clock
// period -- and that is what the bench below does. What cannot be modelled is a
// real setup violation's intermittency, its temperature and voltage dependence,
// reflections, crosstalk, or a metastable flip-flop resolving either way. A
// simulation of marginal timing is deterministic; the thing it stands for is not.
//
// AND THE TWO THAT ACTUALLY NEED SEPARATING, which is the chapter's real content.
//
// A late-arriving bit makes the master sample the PREVIOUS bit -- a stale sample. So does a master
// whose sampling instant is structurally wrong by one bit. At any single clock rate the two produce
// IDENTICAL observations: the same error count, the same positions, the same stale signature.
//
// MARGINAL the data arrives late relative to a sampling instant that is too close to it
// -> SLOW THE CLOCK DOWN and the errors go away
// STRUCTURAL the master samples at the wrong instant by design
// -> slow the clock down and NOTHING CHANGES
//
// The discriminator is therefore not an observation at all. It is an INTERVENTION: change the clock
// period and measure the trend. This module supplies the per-run numbers; the trend across runs is the
// diagnosis, and it lives in whatever drives the experiment.
//
// That is the arc of this whole module arriving somewhere. Chapter 18.1 diagnosed from one capture.
// 18.4 needed two captures with different stimulus. This one needs two captures with a different
// SYSTEM, and there is no version of "look at the waveform more carefully" that substitutes for it.
//
// WHAT THIS MODULE PUBLISHES, per run of frames:
//
// ob_frames, ob_bits how much traffic the numbers are drawn from -- a rate needs a denominator
// ob_err sampled bits differing from the expectation
// ob_xbits sampled bits that were neither 0 nor 1: the contention signature
// ob_stale errors whose sampled value equals the PREVIOUS expected bit
// ob_err_first errors in a frame's FIRST bit position: the output-enable signature
// ob_overlap system-clock cycles with more than one select asserted
// ob_half the smallest SCLK half-period observed, in system-clock cycles
//
// `ob_half` exists so that a run's numbers carry the clock rate they were measured at. A table of
// error counts without the period beside each one is not evidence of a trend, and the trend is the
// entire diagnosis.
`timescale 1ns/1ps
module spi_miso_diag #(
parameter int DW = 32,
parameter int NB = 8,
parameter int CNT_W = 16
) (
input wire clk,
input wire rst_n,
input wire sclk,
input wire [1:0] cs_n, // two selects, so overlap is expressible
input wire miso,
input wire cpol,
input wire cpha,
input wire [DW-1:0] word_exp,
input wire clr, // start a new run of frames
output reg [CNT_W-1:0] ob_frames,
output reg [CNT_W-1:0] ob_bits,
output reg [CNT_W-1:0] ob_err,
output reg [CNT_W-1:0] ob_xbits,
output reg [CNT_W-1:0] ob_stale,
output reg [CNT_W-1:0] ob_err_first,
output reg [CNT_W-1:0] ob_overlap,
output reg [CNT_W-1:0] ob_half,
output reg [2:0] dg_code
);
localparam [2:0] D_CLEAN = 3'd0,
// Indeterminate sampled bits with overlapping selects. The only fault here whose
// evidence is a LEVEL that is neither level.
D_CONTENTION = 3'd1,
// Every error is a stale sample. This verdict deliberately does NOT name a cause,
// because two causes produce it and one clock rate cannot separate them.
D_STALE = 3'd2,
// Every error is in a frame's first bit: the slave's output driver turned on late.
D_ENABLE = 3'd3,
D_OTHER = 3'd4;
reg sclk_d;
reg [1:0] cs_n_d;
reg miso_rest;
reg [CNT_W-1:0] nseen;
reg [CNT_W-1:0] since_edge;
wire cs0_assert = cs_n_d[0] & ~cs_n[0];
wire cs0_deassert = ~cs_n_d[0] & cs_n[0];
wire in_txn = ~cs_n[0] | cs0_deassert;
wire sclk_edge = (sclk !== sclk_d);
wire leading = sclk_edge && (sclk !== cpol);
wire capture = (cpha ? (sclk_edge && !leading) : leading) && in_txn;
// More than one select asserted. A one-line observation that no amount of staring at MISO
// substitutes for, because contention's effect on MISO is a voltage and its CAUSE is on other pins.
wire overlap_now = (cs_n == 2'b00);
// The bit the expectation says should be here, and the one before it. `prev` is what a stale sample
// returns, and for the first bit of a frame there is no previous bit -- so the resting level of the
// bus takes its place, which is exactly what a driver that has not turned on yet leaves behind.
wire [CNT_W-1:0] idx = nseen;
wire exp_now = word_exp[NB - 1 - idx];
wire exp_prev = (idx == {CNT_W{1'b0}}) ? miso_rest : word_exp[NB - idx];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 2'b11;
miso_rest <= 1'b0;
nseen <= {CNT_W{1'b0}};
since_edge <= {CNT_W{1'b0}};
ob_frames <= {CNT_W{1'b0}};
ob_bits <= {CNT_W{1'b0}};
ob_err <= {CNT_W{1'b0}};
ob_xbits <= {CNT_W{1'b0}};
ob_stale <= {CNT_W{1'b0}};
ob_err_first <= {CNT_W{1'b0}};
ob_overlap <= {CNT_W{1'b0}};
ob_half <= {CNT_W{1'b1}};
dg_code <= D_CLEAN;
end else begin
if (clr) begin
ob_frames <= {CNT_W{1'b0}};
ob_bits <= {CNT_W{1'b0}};
ob_err <= {CNT_W{1'b0}};
ob_xbits <= {CNT_W{1'b0}};
ob_stale <= {CNT_W{1'b0}};
ob_err_first <= {CNT_W{1'b0}};
ob_overlap <= {CNT_W{1'b0}};
ob_half <= {CNT_W{1'b1}};
nseen <= {CNT_W{1'b0}};
end else begin
if (overlap_now) ob_overlap <= ob_overlap + 1'b1;
// THE PERIOD MEASUREMENT. The smallest interval between SCLK edges seen in this run,
// in system-clock cycles -- so every error count below carries the rate it was taken
// at. Chapter 18.5's last exercise asked for exactly this observation and could not
// supply it; a counting decoder has no notion of an interval.
if (sclk_edge) begin
if (since_edge < ob_half) ob_half <= since_edge;
// Restarted at ONE, not zero. The interval between two edges H ticks apart
// contains H ticks, and a counter cleared to zero at the first of them reads
// H-1 at the second -- so every period in the report came back one short, and a
// half-period of 2 printed as 1. An off-by-one in a number that labels an
// experiment is worse than an off-by-one in the experiment.
since_edge <= {{(CNT_W-1){1'b0}}, 1'b1};
end else begin
since_edge <= since_edge + 1'b1;
end
if (cs0_assert) begin
miso_rest <= miso;
nseen <= {CNT_W{1'b0}};
end else if (capture && (nseen < NB[CNT_W-1:0])) begin
ob_bits <= ob_bits + 1'b1;
if (miso !== exp_now) begin
ob_err <= ob_err + 1'b1;
// AN INDETERMINATE BIT IS NOT A WRONG BIT, and keeping them in separate
// counters is the difference between naming contention and guessing at it.
if ((miso !== 1'b0) && (miso !== 1'b1))
ob_xbits <= ob_xbits + 1'b1;
else if (miso === exp_prev)
ob_stale <= ob_stale + 1'b1;
if (idx == {CNT_W{1'b0}})
ob_err_first <= ob_err_first + 1'b1;
end
nseen <= nseen + 1'b1;
end
if (cs0_deassert) ob_frames <= ob_frames + 1'b1;
// THE VERDICT IS FOR A RUN, NOT A FRAME, and it names what the evidence supports and
// nothing more. `D_STALE` in particular is a deliberate refusal: two causes -- data
// arriving late, and a sampling instant that is wrong by design -- produce identical
// numbers at one clock rate, and the module does not pretend to choose between them.
if (ob_overlap != {CNT_W{1'b0}})
dg_code <= D_CONTENTION;
else if (ob_err == {CNT_W{1'b0}})
dg_code <= D_CLEAN;
else if (ob_err == ob_err_first)
dg_code <= D_ENABLE;
else if (ob_err == ob_stale)
// NOT `ob_stale + ob_err_first`. A first-bit error IS a stale sample -- the value
// returned is the bus's resting level, which is what `exp_prev` means at index 0 --
// so it is counted in BOTH `ob_stale` and `ob_err_first`, and adding them
// double-counts. The first version of this line did, and a structurally-early
// slave whose every error was stale fell through to D_OTHER: a verdict of
// `I cannot explain this` for the case the chapter is about.
dg_code <= D_STALE;
else
dg_code <= D_OTHER;
end
sclk_d <= sclk;
cs_n_d <= cs_n;
end
end
endmodule// spi_miso_diag.v
//
// Chapter 18.6 -- read corruption on MISO, and the first diagnosis in this module that requires
// CHANGING the system rather than observing it.
//
// THE TWO CAUSES EVERYBODY SUSPECTS, and one of them is not digital.
//
// CONTENTION two devices driving MISO at once, because a select decode overlaps or because a
// slave does not release the pin when deselected. Digital, countable, and it has a
// signature no other fault has: the sampled bits are INDETERMINATE rather than
// wrong.
//
// SAMPLING MARGIN the slave's data is not valid at the instant the master samples it. This is where
// honesty is required, and section 11 of the chapter says it in full: RTL CANNOT
// MODEL THE PHYSICAL HALF OF THIS. What can be modelled is the TIMING half -- a
// propagation delay measured against a sampling instant that moves with the clock
// period -- and that is what the bench below does. What cannot be modelled is a
// real setup violation's intermittency, its temperature and voltage dependence,
// reflections, crosstalk, or a metastable flip-flop resolving either way. A
// simulation of marginal timing is deterministic; the thing it stands for is not.
//
// AND THE TWO THAT ACTUALLY NEED SEPARATING, which is the chapter's real content.
//
// A late-arriving bit makes the master sample the PREVIOUS bit -- a stale sample. So does a master
// whose sampling instant is structurally wrong by one bit. At any single clock rate the two produce
// IDENTICAL observations: the same error count, the same positions, the same stale signature.
//
// MARGINAL the data arrives late relative to a sampling instant that is too close to it
// -> SLOW THE CLOCK DOWN and the errors go away
// STRUCTURAL the master samples at the wrong instant by design
// -> slow the clock down and NOTHING CHANGES
//
// The discriminator is therefore not an observation at all. It is an INTERVENTION: change the clock
// period and measure the trend. This module supplies the per-run numbers; the trend across runs is the
// diagnosis, and it lives in whatever drives the experiment.
//
// That is the arc of this whole module arriving somewhere. Chapter 18.1 diagnosed from one capture.
// 18.4 needed two captures with different stimulus. This one needs two captures with a different
// SYSTEM, and there is no version of "look at the waveform more carefully" that substitutes for it.
//
// WHAT THIS MODULE PUBLISHES, per run of frames:
//
// ob_frames, ob_bits how much traffic the numbers are drawn from -- a rate needs a denominator
// ob_err sampled bits differing from the expectation
// ob_xbits sampled bits that were neither 0 nor 1: the contention signature
// ob_stale errors whose sampled value equals the PREVIOUS expected bit
// ob_err_first errors in a frame's FIRST bit position: the output-enable signature
// ob_overlap system-clock cycles with more than one select asserted
// ob_half the smallest SCLK half-period observed, in system-clock cycles
//
// `ob_half` exists so that a run's numbers carry the clock rate they were measured at. A table of
// error counts without the period beside each one is not evidence of a trend, and the trend is the
// entire diagnosis.
`timescale 1ns/1ps
module spi_miso_diag #(
parameter DW = 32,
parameter NB = 8,
parameter CNT_W = 16
) (
input wire clk,
input wire rst_n,
input wire sclk,
input wire [1:0] cs_n, // two selects, so overlap is expressible
input wire miso,
input wire cpol,
input wire cpha,
input wire [DW-1:0] word_exp,
input wire clr, // start a new run of frames
output reg [CNT_W-1:0] ob_frames,
output reg [CNT_W-1:0] ob_bits,
output reg [CNT_W-1:0] ob_err,
output reg [CNT_W-1:0] ob_xbits,
output reg [CNT_W-1:0] ob_stale,
output reg [CNT_W-1:0] ob_err_first,
output reg [CNT_W-1:0] ob_overlap,
output reg [CNT_W-1:0] ob_half,
output reg [2:0] dg_code
);
localparam [2:0] D_CLEAN = 3'd0,
// Indeterminate sampled bits with overlapping selects. The only fault here whose
// evidence is a LEVEL that is neither level.
D_CONTENTION = 3'd1,
// Every error is a stale sample. This verdict deliberately does NOT name a cause,
// because two causes produce it and one clock rate cannot separate them.
D_STALE = 3'd2,
// Every error is in a frame's first bit: the slave's output driver turned on late.
D_ENABLE = 3'd3,
D_OTHER = 3'd4;
reg sclk_d;
reg [1:0] cs_n_d;
reg miso_rest;
reg [CNT_W-1:0] nseen;
reg [CNT_W-1:0] since_edge;
wire cs0_assert = cs_n_d[0] & ~cs_n[0];
wire cs0_deassert = ~cs_n_d[0] & cs_n[0];
wire in_txn = ~cs_n[0] | cs0_deassert;
wire sclk_edge = (sclk !== sclk_d);
wire leading = sclk_edge && (sclk !== cpol);
wire capture = (cpha ? (sclk_edge && !leading) : leading) && in_txn;
// More than one select asserted. A one-line observation that no amount of staring at MISO
// substitutes for, because contention's effect on MISO is a voltage and its CAUSE is on other pins.
wire overlap_now = (cs_n == 2'b00);
// The bit the expectation says should be here, and the one before it. `prev` is what a stale sample
// returns, and for the first bit of a frame there is no previous bit -- so the resting level of the
// bus takes its place, which is exactly what a driver that has not turned on yet leaves behind.
wire [CNT_W-1:0] idx = nseen;
wire exp_now = word_exp[NB - 1 - idx];
wire exp_prev = (idx == {CNT_W{1'b0}}) ? miso_rest : word_exp[NB - idx];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 2'b11;
miso_rest <= 1'b0;
nseen <= {CNT_W{1'b0}};
since_edge <= {CNT_W{1'b0}};
ob_frames <= {CNT_W{1'b0}};
ob_bits <= {CNT_W{1'b0}};
ob_err <= {CNT_W{1'b0}};
ob_xbits <= {CNT_W{1'b0}};
ob_stale <= {CNT_W{1'b0}};
ob_err_first <= {CNT_W{1'b0}};
ob_overlap <= {CNT_W{1'b0}};
ob_half <= {CNT_W{1'b1}};
dg_code <= D_CLEAN;
end else begin
if (clr) begin
ob_frames <= {CNT_W{1'b0}};
ob_bits <= {CNT_W{1'b0}};
ob_err <= {CNT_W{1'b0}};
ob_xbits <= {CNT_W{1'b0}};
ob_stale <= {CNT_W{1'b0}};
ob_err_first <= {CNT_W{1'b0}};
ob_overlap <= {CNT_W{1'b0}};
ob_half <= {CNT_W{1'b1}};
nseen <= {CNT_W{1'b0}};
end else begin
if (overlap_now) ob_overlap <= ob_overlap + 1'b1;
// THE PERIOD MEASUREMENT. The smallest interval between SCLK edges seen in this run,
// in system-clock cycles -- so every error count below carries the rate it was taken
// at. Chapter 18.5's last exercise asked for exactly this observation and could not
// supply it; a counting decoder has no notion of an interval.
if (sclk_edge) begin
if (since_edge < ob_half) ob_half <= since_edge;
// Restarted at ONE, not zero. The interval between two edges H ticks apart
// contains H ticks, and a counter cleared to zero at the first of them reads
// H-1 at the second -- so every period in the report came back one short, and a
// half-period of 2 printed as 1. An off-by-one in a number that labels an
// experiment is worse than an off-by-one in the experiment.
since_edge <= {{(CNT_W-1){1'b0}}, 1'b1};
end else begin
since_edge <= since_edge + 1'b1;
end
if (cs0_assert) begin
miso_rest <= miso;
nseen <= {CNT_W{1'b0}};
end else if (capture && (nseen < NB[CNT_W-1:0])) begin
ob_bits <= ob_bits + 1'b1;
if (miso !== exp_now) begin
ob_err <= ob_err + 1'b1;
// AN INDETERMINATE BIT IS NOT A WRONG BIT, and keeping them in separate
// counters is the difference between naming contention and guessing at it.
if ((miso !== 1'b0) && (miso !== 1'b1))
ob_xbits <= ob_xbits + 1'b1;
else if (miso === exp_prev)
ob_stale <= ob_stale + 1'b1;
if (idx == {CNT_W{1'b0}})
ob_err_first <= ob_err_first + 1'b1;
end
nseen <= nseen + 1'b1;
end
if (cs0_deassert) ob_frames <= ob_frames + 1'b1;
// THE VERDICT IS FOR A RUN, NOT A FRAME, and it names what the evidence supports and
// nothing more. `D_STALE` in particular is a deliberate refusal: two causes -- data
// arriving late, and a sampling instant that is wrong by design -- produce identical
// numbers at one clock rate, and the module does not pretend to choose between them.
if (ob_overlap != {CNT_W{1'b0}})
dg_code <= D_CONTENTION;
else if (ob_err == {CNT_W{1'b0}})
dg_code <= D_CLEAN;
else if (ob_err == ob_err_first)
dg_code <= D_ENABLE;
else if (ob_err == ob_stale)
// NOT `ob_stale + ob_err_first`. A first-bit error IS a stale sample -- the value
// returned is the bus's resting level, which is what `exp_prev` means at index 0 --
// so it is counted in BOTH `ob_stale` and `ob_err_first`, and adding them
// double-counts. The first version of this line did, and a structurally-early
// slave whose every error was stale fell through to D_OTHER: a verdict of
// `I cannot explain this` for the case the chapter is about.
dg_code <= D_STALE;
else
dg_code <= D_OTHER;
end
sclk_d <= sclk;
cs_n_d <= cs_n;
end
end
endmodule-- spi_miso_diag.vhd
--
-- Chapter 18.6 -- read corruption on MISO, and the first diagnosis in this module that requires
-- CHANGING the system rather than observing it.
--
-- THE TWO CAUSES EVERYBODY SUSPECTS, and one of them is not digital.
--
-- CONTENTION two devices driving MISO at once, because a select decode overlaps or because a
-- slave does not release the pin when deselected. Digital, countable, and it has a
-- signature no other fault has: the sampled bits are INDETERMINATE rather than
-- wrong.
--
-- SAMPLING MARGIN the slave's data is not valid at the instant the master samples it. This is where
-- honesty is required, and section 11 of the chapter says it in full: RTL CANNOT
-- MODEL THE PHYSICAL HALF OF THIS. What can be modelled is the TIMING half -- a
-- propagation delay measured against a sampling instant that moves with the clock
-- period -- and that is what the bench below does. What cannot be modelled is a
-- real setup violation's intermittency, its temperature and voltage dependence,
-- reflections, crosstalk, or a metastable flip-flop resolving either way. A
-- simulation of marginal timing is deterministic; the thing it stands for is not.
--
-- AND THE TWO THAT ACTUALLY NEED SEPARATING, which is the chapter's real content.
--
-- A late-arriving bit makes the master sample the PREVIOUS bit -- a stale sample. So does a master
-- whose sampling instant is structurally wrong by one bit. At any single clock rate the two produce
-- IDENTICAL observations: the same error count, the same positions, the same stale signature.
--
-- MARGINAL the data arrives late relative to a sampling instant that is too close to it
-- -> SLOW THE CLOCK DOWN and the errors go away
-- STRUCTURAL the master samples at the wrong instant by design
-- -> slow the clock down and NOTHING CHANGES
--
-- The discriminator is therefore not an observation at all. It is an INTERVENTION: change the clock
-- period and measure the trend. This module supplies the per-run numbers; the trend across runs is the
-- diagnosis, and it lives in whatever drives the experiment.
--
-- That is the arc of this whole module arriving somewhere. Chapter 18.1 diagnosed from one capture.
-- 18.4 needed two captures with different stimulus. This one needs two captures with a different
-- SYSTEM, and there is no version of "look at the waveform more carefully" that substitutes for it.
--
-- WHAT THIS MODULE PUBLISHES, per run of frames:
--
-- ob_frames, ob_bits how much traffic the numbers are drawn from -- a rate needs a denominator
-- ob_err sampled bits differing from the expectation
-- ob_xbits sampled bits that were neither 0 nor 1: the contention signature
-- ob_stale errors whose sampled value equals the PREVIOUS expected bit
-- ob_err_first errors in a frame's FIRST bit position: the output-enable signature
-- ob_overlap system-clock cycles with more than one select asserted
-- ob_half the smallest SCLK half-period observed, in system-clock cycles
--
-- `ob_half` exists so that a run's numbers carry the clock rate they were measured at. A table of
-- error counts without the period beside each one is not evidence of a trend, and the trend is the
-- entire diagnosis.
--
-- WHAT THE VHDL VERSION ADDS HERE IS UNUSUALLY CONCRETE, and it is the reason this chapter's third
-- language is more than a formality.
--
-- `std_logic` is a RESOLVED type. Two processes driving one signal do not produce an error; they produce
-- 'X' through the resolution function -- which is exactly what a contested net does. So in the VHDL
-- bench, contention is not injected by writing an 'X': it is created by CONNECTING A SECOND DRIVER, and
-- the 'X' that appears on MISO is the language's own resolution of two devices driving at once.
--
-- That is a materially better model than the other two languages get. The Verilog benches assign 1'bx
-- during the overlap window, which is a HAND-PLACED value standing in for a physical effect; the VHDL
-- bench places two drivers and lets the effect happen. If the overlap window were computed wrongly in
-- the Verilog benches the 'x' would still appear exactly where it was told to; here it appears only
-- where two drivers genuinely coincide.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). Generics are `NB_C` and `CNT_W`; the counters are
-- `n_frames`, `n_bits`, `n_err`, `n_x`, `n_stale`, `n_first`, `n_ovl`, `n_half`. None of them is
-- `NB`/`nb` or a case variant of any generic, port or subprogram argument -- the check Chapter 17.4
-- learned to run after a variable `tries` silently became the generic `TRIES`.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package spi_miso_pkg is
constant DW_C : natural := 32;
-- Five verdicts for a RUN of frames, not for a frame.
--
-- D_CONTENTION indeterminate sampled bits with overlapping selects
-- D_STALE every error is a stale sample. A DELIBERATE REFUSAL to name a cause: two causes
-- produce this and one clock rate cannot separate them.
-- D_ENABLE every error is in a frame's first bit -- the output driver turned on late
type miso_diag_t is (D_CLEAN, D_CONTENTION, D_STALE, D_ENABLE, D_OTHER);
function diag_name (d : miso_diag_t) return string;
end package spi_miso_pkg;
package body spi_miso_pkg is
function diag_name (d : miso_diag_t) return string is
begin
case d is
when D_CLEAN => return "CLEAN ";
when D_CONTENTION => return "CONTENTION ";
when D_STALE => return "STALE ";
when D_ENABLE => return "ENABLE ";
when others => return "OTHER ";
end case;
end function diag_name;
end package body spi_miso_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_miso_pkg.all;
entity spi_miso_diag is
generic (
NB_C : positive := 8;
CNT_W : positive := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
sclk : in std_logic;
cs_n : in std_logic_vector(1 downto 0); -- two selects, so overlap is expressible
miso : in std_logic;
cpol : in std_logic;
cpha : in std_logic;
word_exp : in std_logic_vector(DW_C - 1 downto 0);
clr : in std_logic; -- start a new run of frames
ob_frames : out natural;
ob_bits : out natural;
ob_err : out natural;
ob_xbits : out natural;
ob_stale : out natural;
ob_err_first : out natural;
ob_overlap : out natural;
ob_half : out natural;
dg_code : out miso_diag_t
);
end entity spi_miso_diag;
architecture rtl of spi_miso_diag is
constant BIG_C : natural := 2 ** CNT_W - 1;
signal f_r, b_r, e_r, x_r, s_r, fb_r, o_r, h_r : natural := 0;
signal d_r : miso_diag_t := D_CLEAN;
begin
ob_frames <= f_r;
ob_bits <= b_r;
ob_err <= e_r;
ob_xbits <= x_r;
ob_stale <= s_r;
ob_err_first <= fb_r;
ob_overlap <= o_r;
ob_half <= h_r;
dg_code <= d_r;
process (clk, rst_n) is
variable sclk_d : std_logic;
variable cs_n_d : std_logic_vector(1 downto 0);
variable miso_rest : std_logic;
variable nseen : natural;
variable since : natural;
variable cs0_assert, cs0_deassert : boolean;
variable in_txn, sclk_edge : boolean;
variable leading, capture : boolean;
variable exp_now, exp_prev : std_logic;
variable n_frames, n_bits, n_err : natural;
variable n_x, n_stale, n_first : natural;
variable n_ovl, n_half : natural;
begin
if rst_n = '0' then
sclk_d := '0'; cs_n_d := "11"; miso_rest := '0';
nseen := 0; since := 0;
n_frames := 0; n_bits := 0; n_err := 0; n_x := 0;
n_stale := 0; n_first := 0; n_ovl := 0; n_half := BIG_C;
f_r <= 0; b_r <= 0; e_r <= 0; x_r <= 0; s_r <= 0; fb_r <= 0; o_r <= 0;
h_r <= BIG_C; d_r <= D_CLEAN;
elsif rising_edge(clk) then
if clr = '1' then
n_frames := 0; n_bits := 0; n_err := 0; n_x := 0;
n_stale := 0; n_first := 0; n_ovl := 0; n_half := BIG_C;
nseen := 0;
else
-- More than one select asserted. A one-line observation that no amount of staring at
-- MISO substitutes for, because contention's effect is on MISO and its CAUSE is on
-- other pins entirely.
if cs_n = "00" then n_ovl := n_ovl + 1; end if;
cs0_assert := (cs_n(0) = '0') and (cs_n_d(0) = '1');
cs0_deassert := (cs_n(0) = '1') and (cs_n_d(0) = '0');
in_txn := (cs_n(0) = '0') or cs0_deassert;
sclk_edge := (sclk /= sclk_d);
leading := sclk_edge and (sclk /= cpol);
if cpha = '0' then capture := leading and in_txn;
else capture := sclk_edge and (not leading) and in_txn;
end if;
-- THE PERIOD MEASUREMENT, restarted at ONE rather than zero: the interval between two
-- edges H ticks apart contains H ticks, and a counter cleared to zero at the first of
-- them reads H-1 at the second. Every error count in the report carries the rate it was
-- taken at, and an off-by-one in the number that LABELS an experiment is worse than one
-- inside it.
if sclk_edge then
if since < n_half then n_half := since; end if;
since := 1;
else
since := since + 1;
end if;
if cs0_assert then
miso_rest := miso;
nseen := 0;
elsif capture and nseen < NB_C then
exp_now := word_exp(NB_C - 1 - nseen);
-- For the first bit of a frame there is no previous bit, so the bus's resting level
-- takes its place -- which is exactly what a driver that has not turned on yet
-- leaves behind.
if nseen = 0 then exp_prev := miso_rest;
else exp_prev := word_exp(NB_C - nseen);
end if;
n_bits := n_bits + 1;
if miso /= exp_now then
n_err := n_err + 1;
-- AN INDETERMINATE BIT IS NOT A WRONG BIT, and separate counters are the
-- difference between naming contention and guessing at it.
if miso /= '0' and miso /= '1' then
n_x := n_x + 1;
elsif miso = exp_prev then
n_stale := n_stale + 1;
end if;
if nseen = 0 then n_first := n_first + 1; end if;
end if;
nseen := nseen + 1;
end if;
if cs0_deassert then n_frames := n_frames + 1; end if;
end if;
f_r <= n_frames; b_r <= n_bits; e_r <= n_err; x_r <= n_x;
s_r <= n_stale; fb_r <= n_first; o_r <= n_ovl; h_r <= n_half;
-- THE VERDICT IS FOR A RUN, and it names what the evidence supports and nothing more.
if n_ovl /= 0 then
d_r <= D_CONTENTION;
elsif n_err = 0 then
d_r <= D_CLEAN;
elsif n_err = n_first then
d_r <= D_ENABLE;
elsif n_err = n_stale then
-- NOT `n_stale + n_first`. A first-bit error IS a stale sample -- the value returned is
-- the bus's resting level, which is what `exp_prev` means at index 0 -- so it is counted
-- in BOTH, and adding them double-counts. The first version of this line did, and a
-- structurally-early slave whose every error was stale fell through to D_OTHER: a
-- verdict of `I cannot explain this` for the case the chapter is about.
d_r <= D_STALE;
else
d_r <= D_OTHER;
end if;
sclk_d := sclk;
cs_n_d := cs_n;
end if;
end process;
end architecture rtl;The Bench
The bench builds the MISO waveform as a table of per-tick values before driving anything, so the timing model is something a reader can check rather than behaviour emerging from the order of statements in a driver.
// spi_miso_diag_tb.sv
//
// FOUR CONDITIONS AT THREE CLOCK RATES, AND TWO FAULTS THAT ONE RATE CANNOT TELL APART.
//
// HOW THE TIMING IS MODELLED, stated before any result so that no reader has to guess how much of this
// is simulation and how much is physics.
//
// The bench BUILDS THE MISO WAVEFORM AS AN ARRAY OF PER-TICK VALUES before driving anything. Each bit
// becomes valid on the pin at
//
// (the trailing edge that launched it) + d
//
// where `d` is a propagation delay in system-clock ticks. The master samples at the leading edge, one
// half-period later. So whether a bit is valid in time is a comparison between a FIXED delay and a
// half-period that the experiment varies -- which is exactly the arithmetic of a setup margin, and the
// reason slowing the clock fixes a marginal link.
//
// Building the waveform first rather than emitting it from a loop is deliberate: the timing model is
// then a table somebody can read and check, instead of behaviour that emerges from the order of
// statements in a driver.
//
// WHAT THIS DOES NOT MODEL, and the chapter repeats it where a reader cannot miss it: a real setup
// violation is intermittent, temperature- and voltage-dependent, and can leave a flip-flop metastable.
// This one is deterministic. The model is faithful about WHICH bits fail and why; it is silent about
// how often, and anything a reader concludes here about failure RATES is a property of the model.
//
// THE FOUR RESULTS.
//
// 1. CONTENTION HAS A SIGNATURE NO OTHER FAULT HAS: sampled bits that are neither 0 nor 1, together
// with overlapping selects. The bench requires the indeterminate-bit count to be non-zero for
// contention and ZERO for every other condition, so "indeterminate means contention" is measured
// rather than assumed.
//
// 2. THE HEADLINE. A marginal link and a structurally-early sampling instant produce the SAME verdict
// and the SAME signature -- every error a stale sample -- at any single rate. The bench requires
// that, which is an assertion that a measurement FAILS to discriminate. Their error counts differ
// by exactly the FIRST BIT of each frame, and the bench asserts that too: bit 0 has the whole
// select-to-first-edge lead to settle in, so it is the one bit in a frame that says nothing about
// marginal timing. Four counts in thirty-two is not what anybody reads a verdict for.
//
// 3. THE DISCRIMINATOR IS AN INTERVENTION. There is no observation at a single rate that separates
// result 2's pair. The experiment has to change the system, which is a different kind of
// diagnostic step from everything earlier in this module.
//
// 4. THE BEST DEBUG PATTERN IS THE OPPOSITE OF CHAPTER 18.3'S. A stale sample is only VISIBLE when
// adjacent bits differ, so the same physical fault produces a large error count with 0xAA and a
// small one with 0xF0. Chapter 18.3 measured 0xAA as one of the worst patterns for alignment
// faults; here it is the best available. The pattern is part of the instrument in both chapters
// and the right choice is the opposite one.
`timescale 1ns/1ps
module spi_miso_diag_tb;
localparam int DW = 32;
localparam int NB = 8;
localparam int CNT_W = 16;
localparam int LEAD = 3;
localparam int LAG = 2;
localparam int GAP = 3;
localparam int MAXT = 512;
localparam [2:0] D_CLEAN = 3'd0, D_CONTENTION = 3'd1, D_STALE = 3'd2,
D_ENABLE = 3'd3, D_OTHER = 3'd4;
// The four conditions.
localparam int C_CLEAN = 0;
localparam int C_CONT = 1; // a second select overlaps -- two drivers on MISO
localparam int C_MARG = 2; // per-bit propagation delays of 3, 4 and 5 ticks
localparam int C_EARLY = 3; // the slave is structurally one bit behind, at every rate
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b1;
reg cpol = 1'b0, cpha = 1'b0;
reg [DW-1:0] word_exp = {DW{1'b0}};
reg clr = 1'b0;
reg b_sclk = 1'b0;
reg [1:0] b_cs_n = 2'b11;
reg b_miso = 1'b0;
wire [CNT_W-1:0] ob_frames, ob_bits, ob_err, ob_xbits, ob_stale, ob_err_first,
ob_overlap, ob_half;
wire [2:0] dg_code;
spi_miso_diag #(.DW(DW), .NB(NB), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .miso(b_miso),
.cpol(cpol), .cpha(cpha), .word_exp(word_exp), .clr(clr),
.ob_frames(ob_frames), .ob_bits(ob_bits), .ob_err(ob_err), .ob_xbits(ob_xbits),
.ob_stale(ob_stale), .ob_err_first(ob_err_first), .ob_overlap(ob_overlap),
.ob_half(ob_half), .dg_code(dg_code)
);
integer errors = 0;
function [8*12:1] dname(input [2:0] c);
begin
case (c)
D_CLEAN: dname = "CLEAN ";
D_CONTENTION: dname = "CONTENTION ";
D_STALE: dname = "STALE ";
D_ENABLE: dname = "ENABLE ";
default: dname = "OTHER ";
endcase
end
endfunction
task automatic idle_n(input integer n);
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// ---- the waveform tables, built before anything is driven ----
reg w_sclk [0:MAXT-1];
reg [1:0] w_cs [0:MAXT-1];
reg w_miso [0:MAXT-1];
integer w_len;
// Build one frame. Returns through the tables above.
//
// half the SCLK half-period in system-clock ticks -- the experiment's variable
// cond which condition to inject
// w the payload the slave intends to return
task automatic build(input [DW-1:0] w, input integer half, input integer cond);
integer t, j, i, d, vt, shift, le, te;
reg cur;
begin
w_len = LEAD + NB*2*half + LAG + 1 + GAP;
shift = (cond == C_EARLY) ? 1 : 0;
for (t = 0; t < MAXT; t = t + 1) begin
w_sclk[t] = cpol;
w_cs[t] = 2'b11;
w_miso[t] = 1'b0;
end
// SCLK and the select. The select falls at tick 0 and rises after the lag.
for (t = 0; t < w_len; t = t + 1) begin
if (t < LEAD + NB*2*half + LAG) w_cs[t] = 2'b10; // only device 0 selected
cur = cpol;
for (i = 0; i < NB; i = i + 1) begin
le = LEAD + i*2*half;
te = le + half;
if (t >= le && t < te) cur = ~cpol;
end
w_sclk[t] = cur;
end
// MISO, one bit at a time, placed at the tick it actually becomes valid.
//
// Bit j is launched at the trailing edge of bit-time (j-1+shift) and becomes valid `d`
// ticks later. `shift` of 1 makes the slave permanently one bit behind, which is the
// STRUCTURAL fault -- and note that it does not reference `half` at all, which is precisely
// why slowing the clock cannot cure it.
//
// Bit 0 with no shift is placed right after the select, because a correct slave has its
// first bit on the pin before the first edge.
for (j = 0; j < NB; j = j + 1) begin
// The per-bit delay. Varying it across bits is what makes the MARGINAL condition look
// like a real one: some bits make their deadline at a given period and others do not,
// so the error count falls gradually rather than all at once.
// Delays of 3, 4 and 5 ticks, cycling by bit index. The values are chosen so that at
// the FASTEST rate every bit misses its deadline -- which is what makes the marginal
// link indistinguishable from the structural fault there, and the indistinguishability
// is the result the chapter is built on. At the middle rate some bits make it and
// others do not; at the slowest every bit does. A bit is in time exactly when d <= half.
d = (cond == C_MARG) ? (3 + (j % 3)) : 1;
if ((j == 0) && (shift == 0)) begin
vt = 1;
end else begin
te = LEAD + (j - 1 + shift)*2*half + half;
vt = te + d;
end
if (vt < 0) vt = 0;
for (t = vt; t < MAXT; t = t + 1)
w_miso[t] = w[NB-1-j];
end
// CONTENTION. A second select overlaps bit-times 2 to 5, and while two devices drive the
// pin its level is INDETERMINATE -- which is the honest value for a contested net and is
// what makes this fault's signature a level rather than a value.
if (cond == C_CONT) begin
for (t = LEAD + 2*2*half; t < LEAD + 6*2*half; t = t + 1) begin
w_cs[t] = 2'b00;
w_miso[t] = 1'bx;
end
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_miso = w_miso[t];
@(negedge clk);
end
b_sclk = cpol; b_cs_n = 2'b11; b_miso = 1'b0;
idle_n(2);
end
endtask
// ---- one run: four frames at one clock rate under one condition ----
// Sized for FOURTEEN runs, not twelve. The first version stopped at twelve -- the size of the
// 4x3 sweep -- and the two extra payload runs at the end wrote past the end of every array. Icarus
// returns X for an out-of-range read rather than faulting, so the conclusion printed `x errors`
// instead of a number and the comparison that guards it evaluated to false, which `if` treats as
// not-a-failure. A silent out-of-range read is exactly the shape of bug this module is about.
integer r_err [0:15], r_x[0:15], r_stale[0:15], r_first[0:15], r_ovl[0:15], r_half[0:15];
reg [2:0] r_code [0:15];
integer r_bits [0:15];
integer run_i, ci, hi, k;
integer HALVES [0:2];
task automatic run(input integer cond, input integer half, input [DW-1:0] w,
input [8*44:1] label);
begin
clr = 1'b1; @(negedge clk); clr = 1'b0; @(negedge clk);
word_exp = w;
for (k = 0; k < 4; k = k + 1) begin
build(w, half, cond);
drive;
end
@(negedge clk);
r_err[run_i] = ob_err; r_x[run_i] = ob_xbits;
r_stale[run_i] = ob_stale; r_first[run_i] = ob_err_first;
r_ovl[run_i] = ob_overlap; r_half[run_i] = ob_half;
r_code[run_i] = dg_code; r_bits[run_i] = ob_bits;
$display(" %6d %5d %5d %4d %5d %5d %7d %s %0s",
half, ob_bits, ob_err, ob_xbits, ob_stale, ob_err_first, ob_overlap,
dname(dg_code), label);
if (run_i > 15) begin
$display(" FAIL: run index %0d exceeds the log arrays; every conclusion below would read X",
run_i);
errors = errors + 1;
end
if (ob_frames != 16'd4) begin
$display(" FAIL: run %0d saw %0d frames where 4 were driven", run_i, ob_frames);
errors = errors + 1;
end
run_i = run_i + 1;
end
endtask
localparam [7:0] PAT_AA = 8'hAA; // every adjacent pair differs: a stale sample always shows
localparam [7:0] PAT_F0 = 8'hF0; // one transition in eight: a stale sample almost never shows
integer mutations, p_aa, p_f0;
initial begin
HALVES[0] = 2; HALVES[1] = 3; HALVES[2] = 5;
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(" half bits errs x stale 1st overlap verdict condition");
for (ci = 0; ci < 4; ci = ci + 1)
for (hi = 0; hi < 3; hi = hi + 1)
run(ci, HALVES[hi], {24'b0, PAT_AA},
(ci == C_CLEAN) ? "a correct link" :
(ci == C_CONT) ? "CONTENTION -- a second select overlaps" :
(ci == C_MARG) ? "MARGINAL -- per-bit delays of 3, 4, 5" :
"STRUCTURAL -- the slave is one bit behind");
// ================= 1. contention's signature is a level, not a value =================
for (k = 0; k < 12; k = k + 1) begin
if ((k >= 3) && (k < 6)) begin
if (r_x[k] == 0) begin
$display(" FAIL: run %0d is a contention run and reported 0 indeterminate bits", k);
errors = errors + 1;
end
if (r_ovl[k] == 0) begin
$display(" FAIL: run %0d is a contention run and reported no select overlap", k);
errors = errors + 1;
end
end else begin
if (r_x[k] != 0) begin
$display(" FAIL: run %0d is not a contention run and reported %0d indeterminate bits",
k, r_x[k]);
errors = errors + 1;
end
if (r_ovl[k] != 0) begin
$display(" FAIL: run %0d is not a contention run and reported %0d overlap cycles",
k, r_ovl[k]);
errors = errors + 1;
end
end
end
$display("");
$display(" 1. indeterminate sampled bits appeared in the three contention runs and in NO other run, and select overlap did the same. That is a signature no other fault here produces, and it is a LEVEL rather than a value -- a contested net is not carrying the wrong bit, it is carrying neither bit. It also means the evidence for contention is on the SELECT pins while the symptom is on MISO, which is why a capture of MISO alone can only ever say `something is wrong here`");
// ================= 2. the pair that one VERDICT cannot separate =================
// Runs 6,7,8 are MARGINAL at halves 2,3,5. Runs 9,10,11 are STRUCTURAL at the same halves.
if (r_code[6] !== r_code[9]) begin
$display(" FAIL: at the fastest rate the marginal and structural faults received different verdicts (%s against %s); the chapter's claim is that a verdict cannot separate them",
dname(r_code[6]), dname(r_code[9]));
errors = errors + 1;
end
if (!(r_stale[6] == r_err[6] && r_stale[9] == r_err[9])) begin
$display(" FAIL: one of the two faults produced errors that were not stale samples (%0d of %0d, %0d of %0d), so they do not share a signature",
r_stale[6], r_err[6], r_stale[9], r_err[9]);
errors = errors + 1;
end
// THE WHOLE DIFFERENCE IS THE FIRST BIT OF EACH FRAME, and asserting that precisely is worth
// more than asserting the counts are equal -- which they are not.
if ((r_err[9] - r_err[6]) != (r_first[9] - r_first[6])) begin
$display(" FAIL: the two faults' error counts differ by %0d and their first-bit errors by %0d; the claim is that the whole difference is the first bit",
r_err[9] - r_err[6], r_first[9] - r_first[6]);
errors = errors + 1;
end
$display(" 2. at half-period %0d the MARGINAL link and the STRUCTURALLY EARLY sampling instant both reported %s, and in both every single error was a stale sample -- %0d of %0d and %0d of %0d. The verdict cannot separate them and neither can the signature. Their error counts differ by %0d, and that difference is ENTIRELY the first bit of each frame: a structurally-early slave gets bit 0 wrong too, while a marginal one does not, because bit 0 has the whole select-to-first-edge lead to settle in and every later bit has only a half period. So the first bit is the one bit in a frame that carries no information about marginal timing -- and a four-in-thirty-two difference is not what anybody reads a verdict for",
r_half[6], dname(r_code[6]), r_stale[6], r_err[6], r_stale[9], r_err[9],
r_err[9] - r_err[6]);
// ================= 3. the trend separates them, and the trend is an intervention =======
if (!(r_err[6] > r_err[7] && r_err[7] > r_err[8] && r_err[8] == 0)) begin
$display(" FAIL: the marginal fault's error count did not fall monotonically to zero (%0d, %0d, %0d)",
r_err[6], r_err[7], r_err[8]);
errors = errors + 1;
end
if (!(r_err[9] == r_err[10] && r_err[10] == r_err[11] && r_err[9] > 0)) begin
$display(" FAIL: the structural fault's error count was not constant across rates (%0d, %0d, %0d)",
r_err[9], r_err[10], r_err[11]);
errors = errors + 1;
end
$display(" 3. slowing the clock down separated them completely. The marginal link went %0d, %0d, %0d errors as the half-period went %0d, %0d, %0d -- monotonically to zero. The structural fault went %0d, %0d, %0d: it does not move, because the model of it never references the period at all and neither does the real fault. So the discriminator is not an observation, it is an INTERVENTION, and that is a different kind of debugging step from anything earlier in this module: there is no way to look harder at one capture and get this answer",
r_err[6], r_err[7], r_err[8], r_half[6], r_half[7], r_half[8],
r_err[9], r_err[10], r_err[11]);
// ================= 4. the pattern is part of the instrument, the other way round ========
run(C_MARG, HALVES[0], {24'b0, PAT_AA}, "MARGINAL with 0xaa -- 7 transitions");
p_aa = r_err[run_i-1];
run(C_MARG, HALVES[0], {24'b0, PAT_F0}, "MARGINAL with 0xf0 -- 1 transition");
p_f0 = r_err[run_i-1];
// And the two numbers must be REAL before they are compared. An X compared with `>` is false,
// which `if` reads as a pass -- so the guard has to be for a metavalue first.
if ((^p_aa === 1'bx) || (^p_f0 === 1'bx)) begin
$display(" FAIL: a payload run's error count came back X, so result 4 measured nothing");
errors = errors + 1;
end
if (!(p_aa > p_f0)) begin
$display(" FAIL: the high-transition pattern did not reveal more errors than the low-transition one (%0d against %0d)",
p_aa, p_f0);
errors = errors + 1;
end
$display(" 4. the SAME physical fault at the SAME clock rate reported %0d errors with payload 0xaa and %0d with 0xf0. A stale sample returns the PREVIOUS bit, so it is only visible when adjacent bits differ -- 0xaa differs on all seven boundaries and 0xf0 on one. Chapter 18.3 measured 0xaa as one of the worst patterns available, because reversal and both rotations all map it to the same value; here it is the best one available. The pattern is part of the instrument in both chapters and the right choice is the OPPOSITE one, which is why `use a good test pattern` is not advice anybody can follow without knowing what they are looking for",
p_aa, p_f0);
// ================= BENCH INTEGRITY =================
if (r_code[0] !== D_CLEAN) mutations = mutations + 1; // a clean run must be CLEAN
if (r_err[8] != 0) mutations = mutations + 1; // the slow marginal run must be clean
if (mutations != 0) begin
$display(" FAIL: %0d baseline expectation(s) did not hold, so nothing below is evidence", mutations);
errors = errors + 1;
end
// And the checks must be able to fail: two deliberately wrong expectations.
mutations = 0;
if (r_code[3] !== D_CLEAN) mutations = mutations + 1;
if (r_err[6] != 0) mutations = mutations + 1;
if (mutations != 2) begin
$display(" FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
errors = errors + 1;
end
if (r_bits[0] != 32) begin
$display(" FAIL: a run sampled %0d bits where 4 frames of %0d bits were driven",
r_bits[0], NB);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: the clean run was CLEAN and the slowest marginal run had zero errors, so the baselines hold; two deliberately wrong expectations mismatched; and every run's sampled-bit count matched the %0d bits driven",
r_bits[0]);
$display("PASS: read corruption on MISO has one cause with a signature of its own and two that share one. CONTENTION produces sampled bits that are neither 0 nor 1, and it produced them in all three contention runs and in no other run -- but the evidence is select OVERLAP, which is on different pins from the symptom, so a capture of MISO alone can only say that something is wrong. The pair that matters is a MARGINAL link against a sampling instant that is structurally early: at half-period %0d both reported %s, and in both every error was a stale sample -- %0d of %0d against %0d of %0d -- so neither the verdict nor the signature separates them, and the faults are in different places needing different fixes. Slowing the clock separated them completely -- the marginal link went %0d, %0d, %0d errors across half-periods %0d, %0d, %0d while the structural fault stayed at %0d throughout -- which makes the discriminator an INTERVENTION rather than an observation, the first in this module that cannot be had by looking harder at a capture. And the instrument's own sensitivity is a property of the payload: the same fault reported %0d errors with 0xaa and %0d with 0xf0, because a stale sample is only visible when adjacent bits differ -- the exact reverse of Chapter 18.3, where 0xaa was among the worst patterns available. Finally, and this is a limit rather than a result: the timing half of a marginal link is modelled faithfully here and the PHYSICAL half is not modelled at all. Intermittency, temperature and voltage dependence, reflections, crosstalk and metastability are absent, so every error count above is deterministic where the thing it stands for is not",
r_half[6], dname(r_code[6]), r_stale[6], r_err[6], r_stale[9], r_err[9],
r_err[6], r_err[7], r_err[8], r_half[6], r_half[7], r_half[8],
r_err[9], p_aa, p_f0);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
endmodule// spi_miso_diag_tb.v
//
// FOUR CONDITIONS AT THREE CLOCK RATES, AND TWO FAULTS THAT ONE RATE CANNOT TELL APART.
//
// HOW THE TIMING IS MODELLED, stated before any result so that no reader has to guess how much of this
// is simulation and how much is physics.
//
// The bench BUILDS THE MISO WAVEFORM AS AN ARRAY OF PER-TICK VALUES before driving anything. Each bit
// becomes valid on the pin at
//
// (the trailing edge that launched it) + d
//
// where `d` is a propagation delay in system-clock ticks. The master samples at the leading edge, one
// half-period later. So whether a bit is valid in time is a comparison between a FIXED delay and a
// half-period that the experiment varies -- which is exactly the arithmetic of a setup margin, and the
// reason slowing the clock fixes a marginal link.
//
// Building the waveform first rather than emitting it from a loop is deliberate: the timing model is
// then a table somebody can read and check, instead of behaviour that emerges from the order of
// statements in a driver.
//
// WHAT THIS DOES NOT MODEL, and the chapter repeats it where a reader cannot miss it: a real setup
// violation is intermittent, temperature- and voltage-dependent, and can leave a flip-flop metastable.
// This one is deterministic. The model is faithful about WHICH bits fail and why; it is silent about
// how often, and anything a reader concludes here about failure RATES is a property of the model.
//
// THE FOUR RESULTS.
//
// 1. CONTENTION HAS A SIGNATURE NO OTHER FAULT HAS: sampled bits that are neither 0 nor 1, together
// with overlapping selects. The bench requires the indeterminate-bit count to be non-zero for
// contention and ZERO for every other condition, so "indeterminate means contention" is measured
// rather than assumed.
//
// 2. THE HEADLINE. A marginal link and a structurally-early sampling instant produce the SAME verdict
// and the SAME signature -- every error a stale sample -- at any single rate. The bench requires
// that, which is an assertion that a measurement FAILS to discriminate. Their error counts differ
// by exactly the FIRST BIT of each frame, and the bench asserts that too: bit 0 has the whole
// select-to-first-edge lead to settle in, so it is the one bit in a frame that says nothing about
// marginal timing. Four counts in thirty-two is not what anybody reads a verdict for.
//
// 3. THE DISCRIMINATOR IS AN INTERVENTION. There is no observation at a single rate that separates
// result 2's pair. The experiment has to change the system, which is a different kind of
// diagnostic step from everything earlier in this module.
//
// 4. THE BEST DEBUG PATTERN IS THE OPPOSITE OF CHAPTER 18.3'S. A stale sample is only VISIBLE when
// adjacent bits differ, so the same physical fault produces a large error count with 0xAA and a
// small one with 0xF0. Chapter 18.3 measured 0xAA as one of the worst patterns for alignment
// faults; here it is the best available. The pattern is part of the instrument in both chapters
// and the right choice is the opposite one.
`timescale 1ns/1ps
module spi_miso_diag_tb;
localparam DW = 32;
localparam NB = 8;
localparam CNT_W = 16;
localparam LEAD = 3;
localparam LAG = 2;
localparam GAP = 3;
localparam MAXT = 512;
localparam [2:0] D_CLEAN = 3'd0, D_CONTENTION = 3'd1, D_STALE = 3'd2,
D_ENABLE = 3'd3, D_OTHER = 3'd4;
// The four conditions.
localparam C_CLEAN = 0;
localparam C_CONT = 1; // a second select overlaps -- two drivers on MISO
localparam C_MARG = 2; // per-bit propagation delays of 3, 4 and 5 ticks
localparam C_EARLY = 3; // the slave is structurally one bit behind, at every rate
reg clk;
always #5 clk = ~clk;
reg rst_n;
reg cpol, cpha;
reg [DW-1:0] word_exp;
reg clr;
reg b_sclk;
reg [1:0] b_cs_n;
reg b_miso;
wire [CNT_W-1:0] ob_frames, ob_bits, ob_err, ob_xbits, ob_stale, ob_err_first,
ob_overlap, ob_half;
wire [2:0] dg_code;
spi_miso_diag #(.DW(DW), .NB(NB), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .miso(b_miso),
.cpol(cpol), .cpha(cpha), .word_exp(word_exp), .clr(clr),
.ob_frames(ob_frames), .ob_bits(ob_bits), .ob_err(ob_err), .ob_xbits(ob_xbits),
.ob_stale(ob_stale), .ob_err_first(ob_err_first), .ob_overlap(ob_overlap),
.ob_half(ob_half), .dg_code(dg_code)
);
integer errors;
function [8*12:1] dname;
input [2:0] c;
begin
case (c)
D_CLEAN: dname = "CLEAN ";
D_CONTENTION: dname = "CONTENTION ";
D_STALE: dname = "STALE ";
D_ENABLE: dname = "ENABLE ";
default: dname = "OTHER ";
endcase
end
endfunction
task idle_n;
input integer n;
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// ---- the waveform tables, built before anything is driven ----
reg w_sclk [0:MAXT-1];
reg [1:0] w_cs [0:MAXT-1];
reg w_miso [0:MAXT-1];
integer w_len;
// Build one frame. Returns through the tables above.
//
// half the SCLK half-period in system-clock ticks -- the experiment's variable
// cond which condition to inject
// w the payload the slave intends to return
task build;
input [DW-1:0] w;
input integer half;
input integer cond;
integer t, j, i, d, vt, shift, le, te;
reg cur;
begin
w_len = LEAD + NB*2*half + LAG + 1 + GAP;
shift = (cond == C_EARLY) ? 1 : 0;
for (t = 0; t < MAXT; t = t + 1) begin
w_sclk[t] = cpol;
w_cs[t] = 2'b11;
w_miso[t] = 1'b0;
end
// SCLK and the select. The select falls at tick 0 and rises after the lag.
for (t = 0; t < w_len; t = t + 1) begin
if (t < LEAD + NB*2*half + LAG) w_cs[t] = 2'b10; // only device 0 selected
cur = cpol;
for (i = 0; i < NB; i = i + 1) begin
le = LEAD + i*2*half;
te = le + half;
if (t >= le && t < te) cur = ~cpol;
end
w_sclk[t] = cur;
end
// MISO, one bit at a time, placed at the tick it actually becomes valid.
//
// Bit j is launched at the trailing edge of bit-time (j-1+shift) and becomes valid `d`
// ticks later. `shift` of 1 makes the slave permanently one bit behind, which is the
// STRUCTURAL fault -- and note that it does not reference `half` at all, which is precisely
// why slowing the clock cannot cure it.
//
// Bit 0 with no shift is placed right after the select, because a correct slave has its
// first bit on the pin before the first edge.
for (j = 0; j < NB; j = j + 1) begin
// The per-bit delay. Varying it across bits is what makes the MARGINAL condition look
// like a real one: some bits make their deadline at a given period and others do not,
// so the error count falls gradually rather than all at once.
// Delays of 3, 4 and 5 ticks, cycling by bit index. The values are chosen so that at
// the FASTEST rate every bit misses its deadline -- which is what makes the marginal
// link indistinguishable from the structural fault there, and the indistinguishability
// is the result the chapter is built on. At the middle rate some bits make it and
// others do not; at the slowest every bit does. A bit is in time exactly when d <= half.
d = (cond == C_MARG) ? (3 + (j % 3)) : 1;
if ((j == 0) && (shift == 0)) begin
vt = 1;
end else begin
te = LEAD + (j - 1 + shift)*2*half + half;
vt = te + d;
end
if (vt < 0) vt = 0;
for (t = vt; t < MAXT; t = t + 1)
w_miso[t] = w[NB-1-j];
end
// CONTENTION. A second select overlaps bit-times 2 to 5, and while two devices drive the
// pin its level is INDETERMINATE -- which is the honest value for a contested net and is
// what makes this fault's signature a level rather than a value.
if (cond == C_CONT) begin
for (t = LEAD + 2*2*half; t < LEAD + 6*2*half; t = t + 1) begin
w_cs[t] = 2'b00;
w_miso[t] = 1'bx;
end
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_miso = w_miso[t];
@(negedge clk);
end
b_sclk = cpol; b_cs_n = 2'b11; b_miso = 1'b0;
idle_n(2);
end
endtask
// ---- one run: four frames at one clock rate under one condition ----
// Sized for FOURTEEN runs, not twelve. The first version stopped at twelve -- the size of the
// 4x3 sweep -- and the two extra payload runs at the end wrote past the end of every array. Icarus
// returns X for an out-of-range read rather than faulting, so the conclusion printed `x errors`
// instead of a number and the comparison that guards it evaluated to false, which `if` treats as
// not-a-failure. A silent out-of-range read is exactly the shape of bug this module is about.
integer r_err [0:15], r_x[0:15], r_stale[0:15], r_first[0:15], r_ovl[0:15], r_half[0:15];
reg [2:0] r_code [0:15];
integer r_bits [0:15];
integer run_i, ci, hi, k;
integer HALVES [0:2];
task run;
input integer cond;
input integer half;
input [DW-1:0] w;
input [8*44:1] label;
begin
clr = 1'b1; @(negedge clk); clr = 1'b0; @(negedge clk);
word_exp = w;
for (k = 0; k < 4; k = k + 1) begin
build(w, half, cond);
drive;
end
@(negedge clk);
r_err[run_i] = ob_err; r_x[run_i] = ob_xbits;
r_stale[run_i] = ob_stale; r_first[run_i] = ob_err_first;
r_ovl[run_i] = ob_overlap; r_half[run_i] = ob_half;
r_code[run_i] = dg_code; r_bits[run_i] = ob_bits;
$display(" %6d %5d %5d %4d %5d %5d %7d %0s %0s",
half, ob_bits, ob_err, ob_xbits, ob_stale, ob_err_first, ob_overlap,
dname(dg_code), label);
if (run_i > 15) begin
$display(" FAIL: run index %0d exceeds the log arrays; every conclusion below would read X",
run_i);
errors = errors + 1;
end
if (ob_frames != 16'd4) begin
$display(" FAIL: run %0d saw %0d frames where 4 were driven", run_i, ob_frames);
errors = errors + 1;
end
run_i = run_i + 1;
end
endtask
localparam [7:0] PAT_AA = 8'hAA; // every adjacent pair differs: a stale sample always shows
localparam [7:0] PAT_F0 = 8'hF0; // one transition in eight: a stale sample almost never shows
integer mutations, p_aa, p_f0;
initial begin
HALVES[0] = 2; HALVES[1] = 3; HALVES[2] = 5;
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(" half bits errs x stale 1st overlap verdict condition");
for (ci = 0; ci < 4; ci = ci + 1)
for (hi = 0; hi < 3; hi = hi + 1)
run(ci, HALVES[hi], {24'b0, PAT_AA},
(ci == C_CLEAN) ? "a correct link" :
(ci == C_CONT) ? "CONTENTION -- a second select overlaps" :
(ci == C_MARG) ? "MARGINAL -- per-bit delays of 3, 4, 5" :
"STRUCTURAL -- the slave is one bit behind");
// ================= 1. contention's signature is a level, not a value =================
for (k = 0; k < 12; k = k + 1) begin
if ((k >= 3) && (k < 6)) begin
if (r_x[k] == 0) begin
$display(" FAIL: run %0d is a contention run and reported 0 indeterminate bits", k);
errors = errors + 1;
end
if (r_ovl[k] == 0) begin
$display(" FAIL: run %0d is a contention run and reported no select overlap", k);
errors = errors + 1;
end
end else begin
if (r_x[k] != 0) begin
$display(" FAIL: run %0d is not a contention run and reported %0d indeterminate bits",
k, r_x[k]);
errors = errors + 1;
end
if (r_ovl[k] != 0) begin
$display(" FAIL: run %0d is not a contention run and reported %0d overlap cycles",
k, r_ovl[k]);
errors = errors + 1;
end
end
end
$display("");
$display(" 1. indeterminate sampled bits appeared in the three contention runs and in NO other run, and select overlap did the same. That is a signature no other fault here produces, and it is a LEVEL rather than a value -- a contested net is not carrying the wrong bit, it is carrying neither bit. It also means the evidence for contention is on the SELECT pins while the symptom is on MISO, which is why a capture of MISO alone can only ever say `something is wrong here`");
// ================= 2. the pair that one VERDICT cannot separate =================
// Runs 6,7,8 are MARGINAL at halves 2,3,5. Runs 9,10,11 are STRUCTURAL at the same halves.
if (r_code[6] !== r_code[9]) begin
$display(" FAIL: at the fastest rate the marginal and structural faults received different verdicts (%0s against %0s); the chapter's claim is that a verdict cannot separate them",
dname(r_code[6]), dname(r_code[9]));
errors = errors + 1;
end
if (!(r_stale[6] == r_err[6] && r_stale[9] == r_err[9])) begin
$display(" FAIL: one of the two faults produced errors that were not stale samples (%0d of %0d, %0d of %0d), so they do not share a signature",
r_stale[6], r_err[6], r_stale[9], r_err[9]);
errors = errors + 1;
end
// THE WHOLE DIFFERENCE IS THE FIRST BIT OF EACH FRAME, and asserting that precisely is worth
// more than asserting the counts are equal -- which they are not.
if ((r_err[9] - r_err[6]) != (r_first[9] - r_first[6])) begin
$display(" FAIL: the two faults' error counts differ by %0d and their first-bit errors by %0d; the claim is that the whole difference is the first bit",
r_err[9] - r_err[6], r_first[9] - r_first[6]);
errors = errors + 1;
end
$display(" 2. at half-period %0d the MARGINAL link and the STRUCTURALLY EARLY sampling instant both reported %0s, and in both every single error was a stale sample -- %0d of %0d and %0d of %0d. The verdict cannot separate them and neither can the signature. Their error counts differ by %0d, and that difference is ENTIRELY the first bit of each frame: a structurally-early slave gets bit 0 wrong too, while a marginal one does not, because bit 0 has the whole select-to-first-edge lead to settle in and every later bit has only a half period. So the first bit is the one bit in a frame that carries no information about marginal timing -- and a four-in-thirty-two difference is not what anybody reads a verdict for",
r_half[6], dname(r_code[6]), r_stale[6], r_err[6], r_stale[9], r_err[9],
r_err[9] - r_err[6]);
// ================= 3. the trend separates them, and the trend is an intervention =======
if (!(r_err[6] > r_err[7] && r_err[7] > r_err[8] && r_err[8] == 0)) begin
$display(" FAIL: the marginal fault's error count did not fall monotonically to zero (%0d, %0d, %0d)",
r_err[6], r_err[7], r_err[8]);
errors = errors + 1;
end
if (!(r_err[9] == r_err[10] && r_err[10] == r_err[11] && r_err[9] > 0)) begin
$display(" FAIL: the structural fault's error count was not constant across rates (%0d, %0d, %0d)",
r_err[9], r_err[10], r_err[11]);
errors = errors + 1;
end
$display(" 3. slowing the clock down separated them completely. The marginal link went %0d, %0d, %0d errors as the half-period went %0d, %0d, %0d -- monotonically to zero. The structural fault went %0d, %0d, %0d: it does not move, because the model of it never references the period at all and neither does the real fault. So the discriminator is not an observation, it is an INTERVENTION, and that is a different kind of debugging step from anything earlier in this module: there is no way to look harder at one capture and get this answer",
r_err[6], r_err[7], r_err[8], r_half[6], r_half[7], r_half[8],
r_err[9], r_err[10], r_err[11]);
// ================= 4. the pattern is part of the instrument, the other way round ========
run(C_MARG, HALVES[0], {24'b0, PAT_AA}, "MARGINAL with 0xaa -- 7 transitions");
p_aa = r_err[run_i-1];
run(C_MARG, HALVES[0], {24'b0, PAT_F0}, "MARGINAL with 0xf0 -- 1 transition");
p_f0 = r_err[run_i-1];
// And the two numbers must be REAL before they are compared. An X compared with `>` is false,
// which `if` reads as a pass -- so the guard has to be for a metavalue first.
if ((^p_aa === 1'bx) || (^p_f0 === 1'bx)) begin
$display(" FAIL: a payload run's error count came back X, so result 4 measured nothing");
errors = errors + 1;
end
if (!(p_aa > p_f0)) begin
$display(" FAIL: the high-transition pattern did not reveal more errors than the low-transition one (%0d against %0d)",
p_aa, p_f0);
errors = errors + 1;
end
$display(" 4. the SAME physical fault at the SAME clock rate reported %0d errors with payload 0xaa and %0d with 0xf0. A stale sample returns the PREVIOUS bit, so it is only visible when adjacent bits differ -- 0xaa differs on all seven boundaries and 0xf0 on one. Chapter 18.3 measured 0xaa as one of the worst patterns available, because reversal and both rotations all map it to the same value; here it is the best one available. The pattern is part of the instrument in both chapters and the right choice is the OPPOSITE one, which is why `use a good test pattern` is not advice anybody can follow without knowing what they are looking for",
p_aa, p_f0);
// ================= BENCH INTEGRITY =================
if (r_code[0] !== D_CLEAN) mutations = mutations + 1; // a clean run must be CLEAN
if (r_err[8] != 0) mutations = mutations + 1; // the slow marginal run must be clean
if (mutations != 0) begin
$display(" FAIL: %0d baseline expectation(s) did not hold, so nothing below is evidence", mutations);
errors = errors + 1;
end
// And the checks must be able to fail: two deliberately wrong expectations.
mutations = 0;
if (r_code[3] !== D_CLEAN) mutations = mutations + 1;
if (r_err[6] != 0) mutations = mutations + 1;
if (mutations != 2) begin
$display(" FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
errors = errors + 1;
end
if (r_bits[0] != 32) begin
$display(" FAIL: a run sampled %0d bits where 4 frames of %0d bits were driven",
r_bits[0], NB);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: the clean run was CLEAN and the slowest marginal run had zero errors, so the baselines hold; two deliberately wrong expectations mismatched; and every run's sampled-bit count matched the %0d bits driven",
r_bits[0]);
$display("PASS: read corruption on MISO has one cause with a signature of its own and two that share one. CONTENTION produces sampled bits that are neither 0 nor 1, and it produced them in all three contention runs and in no other run -- but the evidence is select OVERLAP, which is on different pins from the symptom, so a capture of MISO alone can only say that something is wrong. The pair that matters is a MARGINAL link against a sampling instant that is structurally early: at half-period %0d both reported %0s, and in both every error was a stale sample -- %0d of %0d against %0d of %0d -- so neither the verdict nor the signature separates them, and the faults are in different places needing different fixes. Slowing the clock separated them completely -- the marginal link went %0d, %0d, %0d errors across half-periods %0d, %0d, %0d while the structural fault stayed at %0d throughout -- which makes the discriminator an INTERVENTION rather than an observation, the first in this module that cannot be had by looking harder at a capture. And the instrument's own sensitivity is a property of the payload: the same fault reported %0d errors with 0xaa and %0d with 0xf0, because a stale sample is only visible when adjacent bits differ -- the exact reverse of Chapter 18.3, where 0xaa was among the worst patterns available. Finally, and this is a limit rather than a result: the timing half of a marginal link is modelled faithfully here and the PHYSICAL half is not modelled at all. Intermittency, temperature and voltage dependence, reflections, crosstalk and metastability are absent, so every error count above is deterministic where the thing it stands for is not",
r_half[6], dname(r_code[6]), r_stale[6], r_err[6], r_stale[9], r_err[9],
r_err[6], r_err[7], r_err[8], r_half[6], r_half[7], r_half[8],
r_err[9], p_aa, p_f0);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
initial begin
cpol = 1'b0;
cpha = 1'b0;
clk = 1'b0;
rst_n = 1'b1;
word_exp = {DW{1'b0}};
clr = 1'b0;
b_sclk = 1'b0;
b_cs_n = 2'b11;
b_miso = 1'b0;
errors = 0;
end
endmodule-- spi_miso_diag_tb.vhd
--
-- FOUR CONDITIONS AT THREE CLOCK RATES, AND TWO FAULTS THAT ONE VERDICT CANNOT TELL APART.
--
-- HOW THE TIMING IS MODELLED, stated before any result so that no reader has to guess how much of this
-- is simulation and how much is physics. Each bit becomes valid on MISO at
--
-- (the trailing edge that launched it) + d
--
-- where `d` is a propagation delay in system-clock ticks. The master samples at the leading edge, one
-- half-period later, so a bit is in time exactly when d <= half -- a comparison between a FIXED delay
-- and a half-period the experiment varies, which is the arithmetic of a setup margin and the reason
-- slowing the clock fixes a marginal link. The waveform is BUILT AS A TABLE before anything is driven,
-- so the timing model is something a reader can check rather than behaviour emerging from a loop.
--
-- WHAT THIS DOES NOT MODEL: a real setup violation is intermittent, temperature- and voltage-dependent,
-- and can leave a flip-flop metastable. This one is deterministic. The model is faithful about WHICH
-- bits fail and why; it is silent about how often.
--
-- WHERE THIS VERSION IS BETTER THAN THE OTHER TWO. `std_logic` is a RESOLVED type, so contention here is
-- created by CONNECTING A SECOND DRIVER and the 'X' on MISO is the language resolving two devices
-- driving at once. The Verilog benches assign 1'bx during a computed window -- a hand-placed value
-- standing in for a physical effect, which would still appear exactly where it was told to even if the
-- window were computed wrongly. Here it appears only where two drivers genuinely coincide.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `LEAD_C`, `LAG_C`, `GAP_C`, `NB_C`, `MAXT_C`, `PAT_AA_C`
-- and `PAT_F0_C` all carry suffixes so nothing can shadow them in another case; the condition selectors
-- are an enumeration rather than integers, so an out-of-range condition is not expressible.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.spi_miso_pkg.all;
entity spi_miso_diag_tb is
end entity spi_miso_diag_tb;
architecture tb of spi_miso_diag_tb is
constant LEAD_C : natural := 3;
constant LAG_C : natural := 2;
constant GAP_C : natural := 3;
constant NB_C : positive := 8;
constant CNT_W : positive := 16;
constant MAXT_C : natural := 512;
subtype byte_t is std_logic_vector(7 downto 0);
constant PAT_AA_C : byte_t := x"AA"; -- every adjacent pair differs: a stale sample always shows
constant PAT_F0_C : byte_t := x"F0"; -- one transition in eight: a stale sample almost never shows
-- The four conditions, as a type rather than as integers.
type cond_t is (C_CLEAN, C_CONT, C_MARG, C_EARLY);
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal run : boolean := true;
signal cpol : std_logic := '0';
signal cpha : std_logic := '0';
signal word_exp : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal clr : std_logic := '0';
signal b_sclk : std_logic := '0';
signal b_cs_n : std_logic_vector(1 downto 0) := "11";
-- TWO DRIVERS ON ONE RESOLVED SIGNAL. `b_miso` is what the decoder sees; the primary slave drives
-- `miso_a` and the intruder drives `miso_b`, and when both drive, resolution produces 'X'. That is
-- the contention model, and it is the language doing the work rather than the bench asserting a
-- result.
signal miso_a : std_logic := '0';
signal miso_b : std_logic := 'Z';
signal b_miso : std_logic;
signal ob_frames, ob_bits, ob_err, ob_xbits : natural;
signal ob_stale, ob_err_first, ob_overlap, ob_half : natural;
signal dg_code : miso_diag_t;
type nat_arr is array (natural range <>) of natural;
type dg_arr is array (natural range <>) of miso_diag_t;
function i2s (v : integer; w : natural) return string is
constant S : string := integer'image(v);
constant P : string(1 to 40) := (others => ' ');
begin
if S'length >= w then return S; end if;
return P(1 to w - S'length) & S;
end function i2s;
function cond_text (c : cond_t) return string is
begin
case c is
when C_CLEAN => return "a correct link";
when C_CONT => return "CONTENTION -- a second select overlaps";
when C_MARG => return "MARGINAL -- per-bit delays of 3, 4, 5";
when others => return "STRUCTURAL -- the slave is one bit behind";
end case;
end function cond_text;
begin
b_miso <= miso_a;
b_miso <= miso_b;
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_miso_diag
generic map (NB_C => NB_C, CNT_W => CNT_W)
port map (
clk => clk, rst_n => rst_n,
sclk => b_sclk, cs_n => b_cs_n, miso => b_miso,
cpol => cpol, cpha => cpha, word_exp => word_exp, clr => clr,
ob_frames => ob_frames, ob_bits => ob_bits, ob_err => ob_err, ob_xbits => ob_xbits,
ob_stale => ob_stale, ob_err_first => ob_err_first, ob_overlap => ob_overlap,
ob_half => ob_half, dg_code => dg_code
);
stim : process is
type tick_arr is array (0 to MAXT_C - 1) of std_logic;
type cs_arr is array (0 to MAXT_C - 1) of std_logic_vector(1 downto 0);
variable w_sclk : tick_arr;
variable w_cs : cs_arr;
variable w_a : tick_arr; -- the primary slave's MISO
variable w_b : tick_arr; -- the intruder's MISO ('Z' except during contention)
variable w_len : natural;
variable r_err, r_x, r_stale, r_first, r_ovl, r_half, r_bits : nat_arr(0 to 15);
variable r_code : dg_arr(0 to 15);
variable run_i, e, mutations, p_aa, p_f0 : natural := 0;
variable ln : line;
constant HALVES_C : nat_arr(0 to 2) := (2, 3, 5);
procedure idle_n (n : natural) is
begin
for i in 1 to n loop
wait until falling_edge(clk);
end loop;
end procedure idle_n;
-- Build one frame into the tables above.
procedure build (w : byte_t; half : natural; cond : cond_t) is
variable shift : natural;
variable d, vt, le, te : integer;
variable cur : std_logic;
begin
w_len := LEAD_C + NB_C*2*half + LAG_C + 1 + GAP_C;
if cond = C_EARLY then shift := 1; else shift := 0; end if;
for t in 0 to MAXT_C - 1 loop
w_sclk(t) := cpol;
w_cs(t) := "11";
w_a(t) := '0';
w_b(t) := 'Z';
end loop;
for t in 0 to w_len - 1 loop
if t < LEAD_C + NB_C*2*half + LAG_C then w_cs(t) := "10"; end if;
cur := cpol;
for i in 0 to NB_C - 1 loop
le := LEAD_C + i*2*half;
te := le + half;
if t >= le and t < te then cur := not cpol; end if;
end loop;
w_sclk(t) := cur;
end loop;
-- Bit j is launched at the trailing edge of bit-time (j-1+shift) and becomes valid `d` ticks
-- later. `shift` of 1 makes the slave permanently one bit behind -- the STRUCTURAL fault --
-- and note that it does not reference `half` at all, which is precisely why slowing the
-- clock cannot cure it. Bit 0 with no shift sits on the pin right after the select, because
-- a correct slave has its first bit valid before the first edge.
for j in 0 to NB_C - 1 loop
-- Delays of 3, 4 and 5, cycling by bit index. Chosen so that at the FASTEST rate every
-- bit misses its deadline: a bit is in time exactly when d <= half.
if cond = C_MARG then d := 3 + (j mod 3); else d := 1; end if;
if j = 0 and shift = 0 then
vt := 1;
else
te := LEAD_C + (j - 1 + shift)*2*half + half;
vt := te + d;
end if;
if vt < 0 then vt := 0; end if;
for t in vt to MAXT_C - 1 loop
w_a(t) := w(NB_C - 1 - j);
end loop;
end loop;
-- CONTENTION: a second select overlaps bit-times 2 to 5, and the intruder DRIVES the pin.
-- No 'X' is written anywhere -- resolution of two drivers produces it.
if cond = C_CONT then
for t in LEAD_C + 2*2*half to LEAD_C + 6*2*half - 1 loop
w_cs(t) := "00";
w_b(t) := not w_a(t);
end loop;
end if;
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);
miso_a <= w_a(t);
miso_b <= w_b(t);
wait until falling_edge(clk);
end loop;
b_sclk <= cpol; b_cs_n <= "11"; miso_a <= '0'; miso_b <= 'Z';
idle_n(2);
end procedure drive;
-- `caption`, NOT `label`. `label` is a VHDL RESERVED WORD, and using it as a parameter name is
-- a parse error whose message points at the interface list rather than at the word -- the same
-- trap Chapter 17.1 hit and renamed away from.
procedure do_run (cond : cond_t; half : natural; w : byte_t; caption : string) is
begin
clr <= '1'; wait until falling_edge(clk); clr <= '0'; wait until falling_edge(clk);
word_exp <= x"000000" & w;
wait until falling_edge(clk);
for k in 0 to 3 loop
build(w, half, cond);
drive;
end loop;
wait until falling_edge(clk);
r_err(run_i) := ob_err; r_x(run_i) := ob_xbits;
r_stale(run_i) := ob_stale; r_first(run_i) := ob_err_first;
r_ovl(run_i) := ob_overlap; r_half(run_i) := ob_half;
r_code(run_i) := dg_code; r_bits(run_i) := ob_bits;
write(ln, string'(" ") & i2s(half, 6) & string'(" ") & i2s(ob_bits, 5)
& string'(" ") & i2s(ob_err, 5) & string'(" ") & i2s(ob_xbits, 4)
& string'(" ") & i2s(ob_stale, 5) & string'(" ") & i2s(ob_err_first, 5)
& string'(" ") & i2s(ob_overlap, 7) & string'(" ") & diag_name(dg_code)
& string'(" ") & caption);
writeline(output, ln);
if ob_frames /= 4 then
write(ln, string'(" FAIL: run ") & i2s(run_i, 1) & string'(" saw ")
& i2s(ob_frames, 1) & string'(" frames where 4 were driven"));
writeline(output, ln); e := e + 1;
end if;
run_i := run_i + 1;
end procedure do_run;
begin
rst_n <= '1';
wait until falling_edge(clk);
rst_n <= '0';
idle_n(4);
rst_n <= '1';
idle_n(4);
write(ln, string'(" half bits errs x stale 1st overlap verdict condition"));
writeline(output, ln);
for ci in cond_t'pos(C_CLEAN) to cond_t'pos(C_EARLY) loop
for hi in 0 to 2 loop
do_run(cond_t'val(ci), HALVES_C(hi), PAT_AA_C, cond_text(cond_t'val(ci)));
end loop;
end loop;
-- ================= 1. contention's signature is a level, not a value =================
for k in 0 to 11 loop
if k >= 3 and k < 6 then
if r_x(k) = 0 then
write(ln, string'(" FAIL: run ") & i2s(k, 1)
& string'(" is a contention run and reported 0 indeterminate bits"));
writeline(output, ln); e := e + 1;
end if;
if r_ovl(k) = 0 then
write(ln, string'(" FAIL: run ") & i2s(k, 1)
& string'(" is a contention run and reported no select overlap"));
writeline(output, ln); e := e + 1;
end if;
else
if r_x(k) /= 0 then
write(ln, string'(" FAIL: run ") & i2s(k, 1)
& string'(" is not a contention run and reported ") & i2s(r_x(k), 1)
& string'(" indeterminate bits"));
writeline(output, ln); e := e + 1;
end if;
if r_ovl(k) /= 0 then
write(ln, string'(" FAIL: run ") & i2s(k, 1)
& string'(" is not a contention run and reported ") & i2s(r_ovl(k), 1)
& string'(" overlap cycles"));
writeline(output, ln); e := e + 1;
end if;
end if;
end loop;
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" 1. indeterminate sampled bits appeared in the three contention runs and in NO other run, and select overlap did the same. That is a signature no other fault here produces, and it is a LEVEL rather than a value -- a contested net is not carrying the wrong bit, it is carrying neither bit. It also means the evidence for contention is on the SELECT pins while the symptom is on MISO, which is why a capture of MISO alone can only ever say `something is wrong here`"));
writeline(output, ln);
-- ================= 2. the pair that one VERDICT cannot separate =================
if r_code(6) /= r_code(9) then
write(ln, string'(" FAIL: at the fastest rate the marginal and structural faults received different verdicts (")
& diag_name(r_code(6)) & string'(" against ") & diag_name(r_code(9))
& string'("); the chapter's claim is that a verdict cannot separate them"));
writeline(output, ln); e := e + 1;
end if;
if not (r_stale(6) = r_err(6) and r_stale(9) = r_err(9)) then
write(ln, string'(" FAIL: one of the two faults produced errors that were not stale samples, so they do not share a signature"));
writeline(output, ln); e := e + 1;
end if;
-- THE WHOLE DIFFERENCE IS THE FIRST BIT OF EACH FRAME, and asserting that precisely is worth
-- more than asserting the counts are equal -- which they are not.
if (r_err(9) - r_err(6)) /= (r_first(9) - r_first(6)) then
write(ln, string'(" FAIL: the two faults' error counts differ by ")
& i2s(r_err(9) - r_err(6), 1) & string'(" and their first-bit errors by ")
& i2s(r_first(9) - r_first(6), 1)
& string'("; the claim is that the whole difference is the first bit"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 2. at half-period ") & i2s(r_half(6), 1)
& string'(" the MARGINAL link and the STRUCTURALLY EARLY sampling instant both reported ")
& diag_name(r_code(6))
& string'(", and in both every single error was a stale sample -- ")
& i2s(r_stale(6), 1) & string'(" of ") & i2s(r_err(6), 1) & string'(" and ")
& i2s(r_stale(9), 1) & string'(" of ") & i2s(r_err(9), 1)
& string'(". The verdict cannot separate them and neither can the signature. Their error counts differ by ")
& i2s(r_err(9) - r_err(6), 1)
& string'(", and that difference is ENTIRELY the first bit of each frame: a structurally-early slave gets bit 0 wrong too, while a marginal one does not, because bit 0 has the whole select-to-first-edge lead to settle in and every later bit has only a half period. So the first bit is the one bit in a frame that carries no information about marginal timing -- and a four-in-thirty-two difference is not what anybody reads a verdict for"));
writeline(output, ln);
-- ================= 3. the trend separates them, and the trend is an intervention =======
if not (r_err(6) > r_err(7) and r_err(7) > r_err(8) and r_err(8) = 0) then
write(ln, string'(" FAIL: the marginal fault's error count did not fall monotonically to zero"));
writeline(output, ln); e := e + 1;
end if;
if not (r_err(9) = r_err(10) and r_err(10) = r_err(11) and r_err(9) > 0) then
write(ln, string'(" FAIL: the structural fault's error count was not constant across rates"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 3. slowing the clock down separated them completely. The marginal link went ")
& i2s(r_err(6), 1) & string'(", ") & i2s(r_err(7), 1) & string'(", ")
& i2s(r_err(8), 1) & string'(" errors as the half-period went ")
& i2s(r_half(6), 1) & string'(", ") & i2s(r_half(7), 1) & string'(", ")
& i2s(r_half(8), 1) & string'(" -- monotonically to zero. The structural fault went ")
& i2s(r_err(9), 1) & string'(", ") & i2s(r_err(10), 1) & string'(", ")
& i2s(r_err(11), 1)
& string'(": it does not move, because the model of it never references the period at all and neither does the real fault. So the discriminator is not an observation, it is an INTERVENTION, and that is a different kind of debugging step from anything earlier in this module: there is no way to look harder at one capture and get this answer"));
writeline(output, ln);
-- ================= 4. the pattern is part of the instrument, the other way round ========
do_run(C_MARG, HALVES_C(0), PAT_AA_C, "MARGINAL with 0xaa -- 7 transitions");
p_aa := r_err(run_i - 1);
do_run(C_MARG, HALVES_C(0), PAT_F0_C, "MARGINAL with 0xf0 -- 1 transition");
p_f0 := r_err(run_i - 1);
if not (p_aa > p_f0) then
write(ln, string'(" FAIL: the high-transition pattern did not reveal more errors than the low-transition one (")
& i2s(p_aa, 1) & string'(" against ") & i2s(p_f0, 1) & string'(")"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 4. the SAME physical fault at the SAME clock rate reported ") & i2s(p_aa, 1)
& string'(" errors with payload 0xaa and ") & i2s(p_f0, 1)
& string'(" with 0xf0. A stale sample returns the PREVIOUS bit, so it is only visible when adjacent bits differ -- 0xaa differs on all seven boundaries and 0xf0 on one. Chapter 18.3 measured 0xaa as one of the worst patterns available, because reversal and both rotations all map it to the same value; here it is the best one available. The pattern is part of the instrument in both chapters and the right choice is the OPPOSITE one, which is why `use a good test pattern` is not advice anybody can follow without knowing what they are looking for"));
writeline(output, ln);
-- ================= BENCH INTEGRITY =================
mutations := 0;
if r_code(0) /= D_CLEAN then mutations := mutations + 1; end if;
if r_err(8) /= 0 then mutations := mutations + 1; end if;
if mutations /= 0 then
write(ln, string'(" FAIL: ") & i2s(mutations, 1)
& string'(" baseline expectation(s) did not hold, so nothing above is evidence"));
writeline(output, ln); e := e + 1;
end if;
mutations := 0;
if r_code(3) /= D_CLEAN then mutations := mutations + 1; end if;
if r_err(6) /= 0 then mutations := mutations + 1; end if;
if mutations /= 2 then
write(ln, string'(" FAIL: a deliberately wrong expectation did not mismatch (")
& i2s(mutations, 1) & string'(" of 2)"));
writeline(output, ln); e := e + 1;
end if;
if r_bits(0) /= 32 then
write(ln, string'(" FAIL: a run sampled ") & i2s(r_bits(0), 1)
& string'(" bits where 4 frames of ") & i2s(NB_C, 1)
& string'(" bits were driven"));
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: the clean run was CLEAN and the slowest marginal run had zero errors, so the baselines hold; two deliberately wrong expectations mismatched; and every run's sampled-bit count matched the ")
& i2s(r_bits(0), 1) & string'(" bits driven"));
writeline(output, ln);
write(ln, string'("PASS: read corruption on MISO has one cause with a signature of its own and two that share one. CONTENTION produces sampled bits that are neither 0 nor 1, and it produced them in all three contention runs and in no other run -- but the evidence is select OVERLAP, which is on different pins from the symptom, so a capture of MISO alone can only say that something is wrong. The pair that matters is a MARGINAL link against a sampling instant that is structurally early: at half-period ")
& i2s(r_half(6), 1) & string'(" both reported ") & diag_name(r_code(6))
& string'(", and in both every error was a stale sample -- ")
& i2s(r_stale(6), 1) & string'(" of ") & i2s(r_err(6), 1)
& string'(" against ") & i2s(r_stale(9), 1) & string'(" of ")
& i2s(r_err(9), 1)
& string'(" -- so neither the verdict nor the signature separates them, and the faults are in different places needing different fixes. Slowing the clock separated them completely -- the marginal link went ")
& i2s(r_err(6), 1) & string'(", ") & i2s(r_err(7), 1) & string'(", ")
& i2s(r_err(8), 1) & string'(" errors across half-periods ")
& i2s(r_half(6), 1) & string'(", ") & i2s(r_half(7), 1) & string'(", ")
& i2s(r_half(8), 1) & string'(" while the structural fault stayed at ")
& i2s(r_err(9), 1)
& string'(" throughout -- which makes the discriminator an INTERVENTION rather than an observation, the first in this module that cannot be had by looking harder at a capture. And the instrument's own sensitivity is a property of the payload: the same fault reported ")
& i2s(p_aa, 1) & string'(" errors with 0xaa and ") & i2s(p_f0, 1)
& string'(" with 0xf0, because a stale sample is only visible when adjacent bits differ -- the exact reverse of Chapter 18.3, where 0xaa was among the worst patterns available. Finally, and this is a limit rather than a result: the timing half of a marginal link is modelled faithfully here and the PHYSICAL half is not modelled at all. Intermittency, temperature and voltage dependence, reflections, crosstalk and metastability are absent, so every error count above is deterministic where the thing it stands for is not"));
writeline(output, ln);
else
write(ln, string'("FAIL: ") & i2s(e, 1) & string'(" error(s)"));
writeline(output, ln);
end if;
run <= false;
wait;
end process stim;
end architecture tb;10. What A Simulation Of Marginal Timing Is Worth
Not nothing, and much less than it looks.
✓ it establishes WHICH bits fail and why -- the arithmetic of delay against
half period, and the fact that bit 0 is exempt because of the lead
✓ it establishes that the marginal and structural faults share a verdict
✓ it establishes that the trend separates them
✗ it says nothing about failure RATES
✗ it says nothing about margin -- there is no picosecond in the model
✗ a passing run is not evidence that a real link has marginEverything in the first group is an argument about structure, and structure is exactly what a deterministic model can settle. Everything in the second requires a physical model the simulation does not have. Reading a number from the first group as if it belonged to the second is the mistake, and it is easy to make because the numbers look alike.
11. Where This Verification Actually Belongs
The useful UVM answer here is mostly about what does not belong in an RTL environment.
the STRUCTURAL fault an RTL assertion: the master samples on the
edge the mode specifies. Fully checkable.
the marginal INTERNAL path static timing analysis. Not simulation.
the marginal BOARD path signal-integrity simulation and lab measurement.
the failure RATE lab, over temperature and voltage.Putting a check for marginal timing into a UVM regression is a false comfort: it passes for a design that will fail on a board, and its passing tells you only that your delay constants were smaller than your half period.
12. What This Decoder Cannot Do
✗ separate a marginal link from a structurally-early sampling instant at any
single clock rate (that is the chapter's premise, not an oversight)
✗ say anything about failure rate, margin, or temperature dependence
✗ distinguish contention between two slaves from a slave that fails to release
MISO on deselect -- both show overlap and indeterminate bits
✗ detect contention that does not overlap a sampling edge
✗ tell a stale sample from a correct sample when adjacent bits are equalThe third is worth a sentence because the fix differs. Two slaves selected at once is a decode fault in the master or the board; a slave that holds MISO after deselect is a fault in that slave. Separating them needs a capture where only one select is asserted and MISO is still contested — which is a stimulus change, and therefore Chapter 18.4's kind of discriminator applied to this chapter's fault.
13. Why an FPGA Engineer Cares
Two of these have FPGA-specific causes worth knowing. Contention on MISO is often a select decoder that is combinational on an address that glitches — the decode passes through an illegal state for a cycle and two selects go low together. Registering the decode output fixes it and costs one flip-flop per select. The overlap counter finds it in one run.
For the marginal case, the honest procedure is the intervention: halve the SCLK rate and re-run. If the errors vanish, the fault is timing and the next questions are about trace length, load capacitance, and whether the slave's output-valid time at your rate is actually within its datasheet. If they do not vanish, stop looking at the board — the sampling instant is wrong in your logic, and that is a fix in RTL.
That single experiment is worth more than any amount of scope work, and it takes one register write.
14. Why an ASIC Engineer Cares
The structural fault is yours and it is provable absent before tape-out: a rate-invariance test plus an assertion that the sampling edge matches the configured mode. Doing that work has a payoff that is easy to undervalue — it means a stale-sample failure in the lab can be attributed to the physical path without argument, because the other candidate has already been excluded.
The marginal fault is not an RTL question at all. The internal path from pad to capture flop is static timing analysis, the external path is a signal-integrity problem, and the failure rate is a lab measurement over corners. What the RTL owes is a sampling instant the STA can be run against and an input timing constraint that matches what the datasheet will promise — which is Chapter 15's material, and this chapter is the reason it matters.
And contention is a top-level integration question: if two selects can ever be low together, in any state including reset and scan, MISO is contested and the pads are fighting. That is checkable formally on the decode logic and is worth checking that way, because a simulation only finds the overlaps a stimulus happens to produce.
15. Failure Signature — "It Only Works At 1 MHz"
Symptom reads are corrupt at 8 MHz and clean at 1 MHz
Concluded the slave cannot keep up; the datasheet's 10 MHz is optimistic
Action the driver is locked to 1 MHz and shipped
Cost a throughput requirement missed by 8x, permanently, and a
workaround nobody revisits because it works
Actual a 30 cm ribbon cable with no ground return next to it; the
slave's output-valid time was well within spec and the
interconnect was not
Found two years later, when the cable was shortened for mechanical
reasons and somebody noticed 8 MHz had started workingThe clock-rate experiment was performed and it gave the right answer to the wrong question. Slowing down fixes it correctly identifies the fault as marginal timing — and the conclusion drawn was about the slave rather than about the path to it. The missing step is the one section 13 names: once the fault is known to be marginal, the next question is which part of the path has no margin, and the answer is rarely the part with a datasheet.
16. Common Misconceptions
| Misconception | What is actually true |
|---|---|
| Corrupt reads mean the slave is faulty | Contention, marginal timing, and a wrong sampling edge are all master-or-board faults |
| An RTL regression can verify timing margin | It has no model of the physics that decides; a pass is not evidence |
| A stale sample means a marginal link | It equally means a sampling instant that is wrong by design |
| Contention is visible on MISO | Its effect is; its cause is on the select pins |
| An X in simulation is a modelling artefact | For a contested net it is the physically honest value |
| "Slowing down fixes it" identifies the culprit | It identifies the fault class; the culprit is usually the interconnect |
| One good test pattern serves all debugging | 0xAA is the best pattern here and among the worst in 18.3 |
| The first bit is as good as any other for timing | It has the whole lead to settle in, so it is the one bit exempt |
17. Reason It Through
18. Understanding Check
19. Summary
Read corruption on MISO has one cause with a signature of its own and two that share one. Contention produces sampled bits that are neither 0 nor 1 — a level rather than a value, because a contested net is not carrying the wrong bit but neither bit — and it produced them in all three contention runs and in no other run. Its evidence, though, is select overlap, which is on different pins from the symptom, so a capture of MISO alone can only say that something is wrong.
The pair that matters is a marginal link against a sampling instant that is structurally early. At the fastest rate both reported the same verdict with every error a stale sample, 28 of 28 against 32 of 32, so neither the verdict nor the signature separates them — and the faults are in different places needing different fixes. Their counts differ by four in thirty-two, entirely the first bit of each frame, because bit 0 has the whole lead to settle in and is therefore the one bit exempt from marginal timing.
Slowing the clock separated them completely: the marginal link went 28, 20, 0 errors across half-periods 2, 3, 5, while the structural fault stayed at 32 throughout. That makes the discriminator an intervention rather than an observation — the first in this module that cannot be had by looking harder at a capture. And the instrument's own sensitivity is a property of the payload: the same fault reported 28 errors with 0xAA and 4 with 0xF0, the exact reverse of Chapter 18.3, where 0xAA was among the worst patterns available.
Finally, and this is a limit rather than a result: the timing half of a marginal link is modelled faithfully here and the physical half is not modelled at all. Intermittency, temperature and voltage dependence, reflections, crosstalk and metastability are absent, so every error count above is deterministic where the thing it stands for is not. A passing RTL regression is not evidence that a link has margin. What RTL can prove is rate invariance — and proving it eliminates the structural half of the pair, so that a field failure can be attributed to the physical half without argument.
20. What Comes Next
Six chapters have each taken one fault family and found its discriminator. Chapter 18.7 asks the question that comes first in practice and last in this module: given a failure, is it in the RTL, in a clock-domain crossing, in a constraint, or in the testbench? Three orthogonal perturbations — clock rate, the bench's sampling instant, and the random seed — map those four causes to four distinct signatures, and the chapter closes the module by making the choice of which discriminator to reach for into a measurement of its own.
Continue learning
Related tutorials
- Related topic
MISO Contention and Multiple Slaves Selected
What two active drivers do to a shared net: why the level is undefined rather than wrong, how much current flows, why damage ranges from a corrupted bit to a destroyed output stage, and why contention can be detected but never implemented.
- Related topic
Master, Slave, and Signal Ownership
Which device is allowed to drive SCLK, MOSI, MISO and CS at each instant, why a shared return line can carry only one driver, and why signal ownership is an electrical question separate from what the transferred bits mean.
- 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.
- Related topic
Slave Output Valid Timing
How long after a clock edge a peripheral may take before MISO is trustworthy. What clock-to-output includes, why the datasheet number is conditional on a load your board probably exceeds, and why it dominates the return-path budget.
