SPI · Module 18
Transfer-Width and Dummy-Cycle Errors
A 32-bit address with 0 dummy cycles issues exactly as many clocks as a 24-bit address with 8 — same total, same launch instant, valid-looking data. Two numbers separate four faults; the fifth needs a second capture at a different address.
Chapter 18.3 settled alignment: the payload's bits are in the right order and the right positions. This chapter questions the frame's structure — how many clocks belong to each field.
A 32-bit address with 0 dummy cycles issues exactly as many clocks as a 24-bit address with 8. Same total. Same launch instant. Valid-looking data. Every timing observation agrees with a correct transfer.
1. Four Numbers From One Datasheet Table
A flash fast-read is four fields:
command 8 clocks address 24 clocks dummy 8 clocks data 8 clocks
────────────────────────────────── 48 clocks total ──────────────────────────────────Every one of those numbers is something somebody copied out of a table. Several can be wrong, the symptom is identical — a read returning garbage — and the fix is in a different place for each:
| What is wrong | Where the fix is |
|---|---|
| the master's clock accounting | a driver constant |
| the device's dummy-cycle count | a configuration register never written, or a part that isn't the part in the schematic |
| the address width | 3-byte against 4-byte addressing |
2. Why A Length Check Separates Nothing
intended: 8 + 24 + 8 + 8 = 48 clocks
traded: 8 + 32 + 0 + 8 = 48 clocksThe same total. Chapter 18.1's edge-count predicate compares against 2N for a known N — and here N is the thing in question, so a length checker configured from the same table inherits the same error.
Worse: the device begins driving at its own instant either way, so the data is not displaced. And the byte it returns is the correct response for the address it actually latched — the top three of the four bytes the master sent. It is not corrupt data; it is valid data for a different address, which is why it survives a plausibility check and why the failure gets filed against the memory contents instead of the transfer.
3. The Observation That Does Not Work
The obvious measurement is when the device began driving MISO: time the first moment MISO leaves the level the bus rests at. The first version of this module did exactly that, and it is worth keeping on the record because it failed in the most expensive way available.
MISO's first visible change is not the launch. It is an upper bound on the launch, because a device that starts driving with a bit equal to the resting level produces no transition to see.
first change EARLIER than intended → the device definitely drove early
first change LATER than intended → it drove late, OR it drove on time with a
leading bit that matched the resting busSo a late launch is not provable from a level.
4. The Observation That Does
Instead of timing one transition, use the whole byte. Sample MISO over the window the transfer was supposed to put data in, then ask: for which displacement d does the expected byte, shifted by d and padded with the resting level, equal what was sampled?
d > 0 the device drove LATE → the window opens with resting-level bits
d = 0 on time
d < 0 it drove EARLY → the window closes with themA displacement is decidable where a launch instant is not, because it uses eight bits of evidence instead of one transition.
The intended data window, and a device that drives two edges late
14 cyclesRead the two MISO rows against the sampled row. The decoder reads the same three instants in both cases. In the first it collects d7 d6 d5; in the second it collects two resting-level bits and then d7 — which is the expected byte displaced by exactly two, and the displacement is the dummy-cycle deficit.
5. The Two Observations, And The 2×2
len_err disp
correct 0 0
master's clock accounting wrong +/- 0 → the MASTER, not WHICH field
device wants more dummy cycles 0 +2 → the DEVICE's register
device wants fewer 0 -2 → the DEVICE's register
both +/- +/- → reported as a compound
address width traded for dummy cycles 0 0 → a FIELD WIDTH; needs capture #2Two numbers separate four faults, and the sign of the displacement names the direction of the register somebody has to write.
6. The Measurement
Ten captures, one flash read configuration, identical output from all three languages:
leads len_err disp nd moved 1st cmd data exp verdict expected stimulus
48 0 0 1 1 40 0b 8d 8d OK OK a correct read at address A
48 0 0 0 1 41 0b 7c 8d FIELD_WIDTH FIELD_WIDTH 32-bit address, 0 dummies -- same total, no displacement
48 0 0 0 1 41 0b 74 c3 FIELD_WIDTH FIELD_WIDTH the same trade at address B -- the answer CHANGED
48 0 0 0 0 0 0c 00 8d QUIET QUIET command 0x0c: the device drove nothing
48 0 0 0 0 0 0c 00 c3 QUIET QUIET the same bad command at B -- the answer did NOT change
50 2 0 1 1 40 0b 8d 8d MASTER_LEN MASTER_LEN the MASTER inserts two dummy cycles too many
56 8 0 1 1 40 0b 8d 8d MASTER_LEN MASTER_LEN the MASTER clocks eight extra DATA bits -- same verdict
48 0 2 1 1 42 0b 23 8d DEVICE_PHASE DEVICE_PHASE the DEVICE wants two dummy cycles more (late)
48 0 -2 1 1 38 0b 34 8d DEVICE_PHASE DEVICE_PHASE the DEVICE wants two fewer (early)
48 0 0 15 0 0 0b 00 00 QUIET QUIET response 0x00 on a bus resting at 0 -- looks silentFour columns repay attention.
1st — the failed observation, printed deliberately. Rows 2 and 3 read 41 against the correct read's 40, and there is no fault in the device's timing. That column is the trap from section 3, left in the output so the reader can see it rather than be told about it.
disp and nd together. nd is the number of displacements that explain the window. One is a finding. Zero means the expected byte is nowhere in the window at any displacement — so this is not a timing fault at all. Fifteen means it is everywhere.
Rows 6 and 7 — the same verdict for different faults. Two extra dummy cycles and eight extra data bits are different constants in different lines of a driver, and both are MASTER_LEN with only the magnitude to separate them. That is not a decoder weakness; it is arithmetic, and section 8 takes it seriously.
Row 10 — a correct answer that looks like silence. The device answered 0x00 on a bus resting at 0, so MISO never moved. Same waveform as row 4, where the device ignored the command entirely.
7. Two Captures Separate What One Cannot
Rows 2 and 3 are the same fault at two addresses; rows 4 and 5 are the same fault at two addresses. Look at what changes.
width fault address A → 7c address B → 74 the answer DEPENDS on the address
unrecognised command address A → 00 address B → 00 it does NOTA response that changes with the address means the address field is being parsed, and only its width is wrong. A response that does not means the address was never parsed at all — the device rejected the command before it got there.
Neither statement is available from one capture. Both follow immediately from two, which makes re-run it at a different address the cheapest next measurement in the whole chapter — and a measurement that costs one line of a bring-up script.
8. Two Limits, Measured Rather Than Claimed
9. Building It — Three HDLs
// spi_len_diag.sv
//
// Chapter 18.4 -- transfer width against dummy cycles, and two observations that separate four faults
// while a fifth stays undecidable from one capture.
//
// THE FAULT FAMILY. A flash read is a command, an address, some dummy cycles and then data, and every
// one of those lengths is a number somebody copied out of a datasheet table. Get one wrong and the
// read returns garbage. Several different numbers can be wrong, the symptom is the same, and the fix
// is in a different place for each:
//
// the MASTER's clock accounting a driver constant
// the DEVICE's dummy-cycle count a configuration register never written, or a part that is
// not the part in the schematic
// the ADDRESS WIDTH 3-byte against 4-byte addressing
//
// WHY A LENGTH CHECK SEPARATES NONE OF THEM, and this is what the chapter is built on.
//
// A master configured for a 32-bit address with 0 dummy cycles issues EXACTLY as many clocks as one
// configured for a 24-bit address with 8 dummy cycles: 8 + 32 + 0 + 8 = 8 + 24 + 8 + 8 = 48. The total
// is identical, so Chapter 18.1's edge-count predicate is silent. And the device begins driving at its
// own instant either way, so the data is not displaced either. EVERY TIMING OBSERVATION AGREES WITH A
// CORRECT TRANSFER. Only the data disagrees, and it disagrees into a byte that is a perfectly valid
// response for a different address -- so it does not look like corruption either.
//
// That trade is not contrived. It is the mistake the datasheet invites, because address width and
// dummy count are adjacent entries in one table and a 4-byte-address mode conventionally carries a
// different dummy count.
//
// -----------------------------------------------------------------------------------------------
// THE OBSERVATION THAT DOES NOT WORK, AND WHY IT IS DOCUMENTED HERE RATHER THAN DELETED
// -----------------------------------------------------------------------------------------------
//
// The obvious measurement is WHEN the device began driving MISO: time the first moment MISO leaves the
// level the bus rests at while nobody drives it. The first version of this module did exactly that,
// and it is wrong in a way worth keeping on the record.
//
// MISO's first VISIBLE change is not the launch. It is an upper bound on the launch, because a device
// that starts driving with a bit equal to the resting level produces no transition to see. So:
//
// first change EARLIER than expected -> the device definitely launched early
// first change LATER than expected -> the device launched late, OR launched on time with a
// leading bit that happened to match the resting bus
//
// A late launch is therefore NOT PROVABLE from a level. And the failure is not a missing number: with
// a 32-bit address traded against 0 dummy cycles the device launched exactly on time and its first bit
// matched the resting bus, so the module reported a launch one cycle late and diagnosed a device-side
// dummy fault. A confident wrong answer, pointing at the wrong device, from an observation that looked
// obviously correct.
//
// -----------------------------------------------------------------------------------------------
// THE TWO OBSERVATIONS THAT DO WORK
// -----------------------------------------------------------------------------------------------
//
// ob_len_err the total leading-edge count, against the intended configuration.
//
// ob_disp the DISPLACEMENT of the expected data within the intended data window. The module
// samples MISO over the window the transfer was supposed to put data in, then asks
// for which displacement d the expected byte -- shifted by d and padded with the
// resting level -- equals what was sampled. A displacement is decidable where a
// launch instant is not, because it uses the whole byte instead of one transition.
//
// len_err disp
// correct 0 0
// master's clock accounting wrong +/- 0 -> the MASTER, but not WHICH field
// device wants more dummy cycles 0 +2 -> the DEVICE's register
// device wants fewer 0 -2 -> the DEVICE's register
// both +/- +/- -> reported as a compound
// address width traded for dummy cycles 0 0 -> a FIELD WIDTH; needs capture #2
//
// Four faults separated by two numbers. The trade agrees with a correct transfer on both, and nothing
// in ONE capture separates it from a correct transfer that returned unexpected contents. It takes a
// second capture at a different address -- a change of stimulus, not a sharper look at one waveform.
//
// TWO HONEST LIMITS, BOTH MEASURED RATHER THAN CLAIMED.
//
// * `ob_len_err` says the master's clock count is wrong and CANNOT say which of its fields is wrong,
// because three field lengths feed one total. An over-long address, an over-long dummy run and an
// over-long data phase are the same number.
//
// * A RESPONSE EQUAL TO THE BUS'S RESTING LEVEL IS INDISTINGUISHABLE FROM NO RESPONSE AT ALL. If the
// device answers 0x00 on a bus resting at 0, MISO never changes -- and neither does it change when
// the device ignored the command entirely. One verdict, `D_QUIET`, covers both, and `ob_ndisp`
// separates them: zero matching displacements means the device really was silent, and a full set
// means it may have answered perfectly and this bus cannot show it.
//
// That is a stronger statement than it first looks. It says a debug read must never target an
// address whose contents equal the resting level, which is a constraint on the DEBUG PROCEDURE
// rather than on the design. Chapter 18.3 reached the same two payloads -- 0x00 and 0xFF -- for an
// unrelated reason: there they were invariant under PERMUTATION, here they are invisible against
// the bus's own idle state.
//
// Chapter 18.1 met an overlap it chose not to separate and argued a finer decoder could report the
// compound. This is that finer decoder: when both observations fail it reports BOTH rather than
// ranking one above the other, because the two numbers are independent and a priority would discard
// one of them.
`timescale 1ns/1ps
module spi_len_diag #(
parameter int DW = 32,
parameter int CNT_W = 8
) (
input wire clk,
input wire rst_n,
input wire sclk,
input wire cs_n,
input wire mosi,
input wire miso,
input wire cpol,
input wire cpha,
// THE INTENDED CONFIGURATION -- what the datasheet says this transfer should be. Every number the
// module reports is relative to these, which is the honest shape for a diagnostic: it does not
// know what is correct, it knows what was intended.
input wire [CNT_W-1:0] cmd_w,
input wire [CNT_W-1:0] addr_w,
input wire [CNT_W-1:0] dummy_n,
input wire [CNT_W-1:0] data_w,
input wire [DW-1:0] word_exp,
output reg dg_valid,
output reg [2:0] dg_code,
output reg [CNT_W-1:0] ob_leads, // leading edges in the frame
output reg signed [CNT_W:0] ob_len_err, // against cmd_w + addr_w + dummy_n + data_w
output reg ob_moved, // did MISO ever leave the resting level?
output reg [CNT_W-1:0] ob_first_move, // and when -- kept because it is an EARLY-launch proof
output reg signed [CNT_W:0] ob_disp, // the displacement of the expected data, when unique
output reg [3:0] ob_ndisp, // how many displacements match: 1 is decidable, more is not
output reg [DW-1:0] ob_cmd, // the command byte, so NOLAUNCH names what was ignored
output reg [DW-1:0] ob_data // MISO over the INTENDED data window
);
localparam [2:0] D_OK = 3'd0,
D_MASTER_LEN = 3'd1, // the master's own clock count is wrong; not which field
D_DEVICE_PHASE = 3'd2, // the data is present and displaced: the device's count
D_FIELD_WIDTH = 3'd3, // both numbers agree with a good transfer; data absent
D_BOTH = 3'd4, // both wrong -- reported, not ranked
// MISO never left the resting level. This does NOT mean "the device was silent":
// it means silence and a response equal to the resting level are the same
// observation. `ob_ndisp` says which reading is available.
D_QUIET = 3'd5;
reg sclk_d, cs_n_d;
reg miso_rest;
reg [CNT_W-1:0] leads, first_move;
reg moved;
reg [DW-1:0] cmd_acc, data_acc;
wire cs_assert = cs_n_d & ~cs_n;
wire cs_deassert = ~cs_n_d & cs_n;
wire in_txn = ~cs_n | cs_deassert;
wire sclk_edge = (sclk !== sclk_d);
wire leading = sclk_edge && (sclk !== cpol);
wire capture = (cpha ? (sclk_edge && !leading) : leading) && in_txn;
// The intended instants, derived from the configuration rather than hard-coded, so one module
// serves a 3-byte and a 4-byte addressing device.
wire [CNT_W-1:0] launch_exp = cmd_w + addr_w + dummy_n;
wire [CNT_W-1:0] leads_exp = cmd_w + addr_w + dummy_n + data_w;
// The expected byte displaced by `d` inside the data window and padded with the resting level.
// d > 0 means the device drove LATE, so the window opens with resting-level bits; d < 0 means it
// drove EARLY and the window closes with them.
//
// Every index is a variable index into a fixed-width vector, never a part-select, because
// `data_w` is a run-time input.
function [DW-1:0] displaced(input [DW-1:0] w, input integer d, input integer nb, input rest);
integer i, src;
reg [DW-1:0] r;
begin
r = {DW{1'b0}};
for (i = 0; i < DW; i = i + 1) begin
if (i < nb) begin
// Window bit i is sampled at launch_exp + (nb-1-i); the device is then `d` cycles
// into its own transmission offset by -d.
src = (nb - 1 - i) - d;
if ((src >= 0) && (src < nb)) r[i] = w[nb - 1 - src];
else r[i] = rest;
end
end
displaced = r;
end
endfunction
// How many displacements explain the window, and which. A COUNT of one is what makes the
// displacement a finding; anything else is a set, and a set of size fifteen is silence.
function [3:0] ndisp(input [DW-1:0] w, input [DW-1:0] obs, input integer nb, input rest);
integer d;
reg [DW-1:0] m;
integer i;
begin
m = {DW{1'b0}};
for (i = 0; i < DW; i = i + 1) if (i < nb) m[i] = 1'b1;
ndisp = 4'd0;
for (d = -(DW-1); d <= (DW-1); d = d + 1)
if ((d > -nb) && (d < nb) && ((displaced(w, d, nb, rest) & m) == (obs & m))
&& (ndisp < 4'd15))
ndisp = ndisp + 4'd1;
end
endfunction
function signed [CNT_W:0] firstdisp(input [DW-1:0] w, input [DW-1:0] obs, input integer nb, input rest);
integer d;
reg [DW-1:0] m;
integer i;
reg done;
begin
m = {DW{1'b0}};
for (i = 0; i < DW; i = i + 1) if (i < nb) m[i] = 1'b1;
firstdisp = {(CNT_W+1){1'b0}};
done = 1'b0;
// Searched outwards from zero so that the smallest displacement is reported first: when
// several match, the one needing the least explanation is the one worth printing.
for (d = 0; d < DW; d = d + 1) begin
if (!done && (d < nb) && ((displaced(w, d, nb, rest) & m) == (obs & m))) begin
firstdisp = d; done = 1'b1;
end
if (!done && (d != 0) && (d < nb)
&& ((displaced(w, -d, nb, rest) & m) == (obs & m))) begin
firstdisp = -d; done = 1'b1;
end
end
end
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
miso_rest <= 1'b0;
leads <= {CNT_W{1'b0}};
first_move <= {CNT_W{1'b0}};
moved <= 1'b0;
cmd_acc <= {DW{1'b0}};
data_acc <= {DW{1'b0}};
dg_valid <= 1'b0;
dg_code <= D_OK;
ob_leads <= {CNT_W{1'b0}};
ob_len_err <= {(CNT_W+1){1'b0}};
ob_moved <= 1'b0;
ob_first_move <= {CNT_W{1'b0}};
ob_disp <= {(CNT_W+1){1'b0}};
ob_ndisp <= 4'd0;
ob_cmd <= {DW{1'b0}};
ob_data <= {DW{1'b0}};
end else begin
dg_valid <= 1'b0;
if (cs_assert) begin
// The resting level is sampled AT the select, before any driver can have started --
// the only instant at which "what the bus does when nobody drives it" is on show. On a
// real board that level is set by a pull resistor, not by a device.
miso_rest <= miso;
leads <= {CNT_W{1'b0}};
first_move <= {CNT_W{1'b0}};
moved <= 1'b0;
cmd_acc <= {DW{1'b0}};
data_acc <= {DW{1'b0}};
end else if (capture) begin
if (!moved && (miso !== miso_rest)) begin
moved <= 1'b1;
first_move <= leads;
end
if (leads < cmd_w)
cmd_acc[cmd_w - 1'b1 - leads] <= mosi;
// The data window is the INTENDED one. Sampling where the transfer was supposed to put
// the data -- rather than where the device happened to put it -- is what makes the
// window comparable against a displaced expectation at all.
if ((leads >= launch_exp) && (leads < leads_exp))
data_acc[data_w - 1'b1 - (leads - launch_exp)] <= miso;
leads <= leads + 1'b1;
end
if (cs_deassert) begin
dg_valid <= 1'b1;
ob_leads <= leads;
ob_len_err <= $signed({1'b0, leads}) - $signed({1'b0, leads_exp});
ob_moved <= moved;
ob_first_move <= first_move;
ob_cmd <= cmd_acc;
ob_data <= data_acc;
ob_ndisp <= ndisp(word_exp, data_acc, data_w, miso_rest);
ob_disp <= firstdisp(word_exp, data_acc, data_w, miso_rest);
if (!moved)
// MISO never left the resting level, and that is all this says. The device may have
// ignored the command -- `ob_cmd` names which command -- or it may have answered
// with a byte equal to the resting level, in which case a correct response and no
// response are the same waveform. `ob_ndisp` is the disambiguator: 0 means the
// expected data is nowhere in the window at any displacement, so the device really
// was silent; a full set means the expected data is EVERYWHERE and the bus cannot
// distinguish a perfect answer from none.
dg_code <= D_QUIET;
else if (ndisp(word_exp, data_acc, data_w, miso_rest) == 4'd0)
// The expected data is not in the window at ANY displacement, so this is not a
// timing fault: the device answered a different question. With the total also
// correct, a traded field width is what remains -- and localising which field
// needs a second capture.
dg_code <= (leads == leads_exp) ? D_FIELD_WIDTH : D_BOTH;
else if ((leads != leads_exp) && (firstdisp(word_exp, data_acc, data_w, miso_rest) != 0))
dg_code <= D_BOTH;
else if (leads != leads_exp)
dg_code <= D_MASTER_LEN;
else if (firstdisp(word_exp, data_acc, data_w, miso_rest) != 0)
// More than one matching displacement requires a window equal to the resting level
// throughout, which is the `!moved` case already taken above -- so there is no arm
// here for an ambiguous displacement, and deliberately no untestable branch
// pretending to handle one. `ob_ndisp` is still published, because a reader should
// be able to see that the count was 1 rather than take it on trust.
dg_code <= D_DEVICE_PHASE;
else
dg_code <= D_OK;
end
sclk_d <= sclk;
cs_n_d <= cs_n;
end
end
endmodule// spi_len_diag.v
//
// Chapter 18.4 -- transfer width against dummy cycles, and two observations that separate four faults
// while a fifth stays undecidable from one capture.
//
// THE FAULT FAMILY. A flash read is a command, an address, some dummy cycles and then data, and every
// one of those lengths is a number somebody copied out of a datasheet table. Get one wrong and the
// read returns garbage. Several different numbers can be wrong, the symptom is the same, and the fix
// is in a different place for each:
//
// the MASTER's clock accounting a driver constant
// the DEVICE's dummy-cycle count a configuration register never written, or a part that is
// not the part in the schematic
// the ADDRESS WIDTH 3-byte against 4-byte addressing
//
// WHY A LENGTH CHECK SEPARATES NONE OF THEM, and this is what the chapter is built on.
//
// A master configured for a 32-bit address with 0 dummy cycles issues EXACTLY as many clocks as one
// configured for a 24-bit address with 8 dummy cycles: 8 + 32 + 0 + 8 = 8 + 24 + 8 + 8 = 48. The total
// is identical, so Chapter 18.1's edge-count predicate is silent. And the device begins driving at its
// own instant either way, so the data is not displaced either. EVERY TIMING OBSERVATION AGREES WITH A
// CORRECT TRANSFER. Only the data disagrees, and it disagrees into a byte that is a perfectly valid
// response for a different address -- so it does not look like corruption either.
//
// That trade is not contrived. It is the mistake the datasheet invites, because address width and
// dummy count are adjacent entries in one table and a 4-byte-address mode conventionally carries a
// different dummy count.
//
// -----------------------------------------------------------------------------------------------
// THE OBSERVATION THAT DOES NOT WORK, AND WHY IT IS DOCUMENTED HERE RATHER THAN DELETED
// -----------------------------------------------------------------------------------------------
//
// The obvious measurement is WHEN the device began driving MISO: time the first moment MISO leaves the
// level the bus rests at while nobody drives it. The first version of this module did exactly that,
// and it is wrong in a way worth keeping on the record.
//
// MISO's first VISIBLE change is not the launch. It is an upper bound on the launch, because a device
// that starts driving with a bit equal to the resting level produces no transition to see. So:
//
// first change EARLIER than expected -> the device definitely launched early
// first change LATER than expected -> the device launched late, OR launched on time with a
// leading bit that happened to match the resting bus
//
// A late launch is therefore NOT PROVABLE from a level. And the failure is not a missing number: with
// a 32-bit address traded against 0 dummy cycles the device launched exactly on time and its first bit
// matched the resting bus, so the module reported a launch one cycle late and diagnosed a device-side
// dummy fault. A confident wrong answer, pointing at the wrong device, from an observation that looked
// obviously correct.
//
// -----------------------------------------------------------------------------------------------
// THE TWO OBSERVATIONS THAT DO WORK
// -----------------------------------------------------------------------------------------------
//
// ob_len_err the total leading-edge count, against the intended configuration.
//
// ob_disp the DISPLACEMENT of the expected data within the intended data window. The module
// samples MISO over the window the transfer was supposed to put data in, then asks
// for which displacement d the expected byte -- shifted by d and padded with the
// resting level -- equals what was sampled. A displacement is decidable where a
// launch instant is not, because it uses the whole byte instead of one transition.
//
// len_err disp
// correct 0 0
// master's clock accounting wrong +/- 0 -> the MASTER, but not WHICH field
// device wants more dummy cycles 0 +2 -> the DEVICE's register
// device wants fewer 0 -2 -> the DEVICE's register
// both +/- +/- -> reported as a compound
// address width traded for dummy cycles 0 0 -> a FIELD WIDTH; needs capture #2
//
// Four faults separated by two numbers. The trade agrees with a correct transfer on both, and nothing
// in ONE capture separates it from a correct transfer that returned unexpected contents. It takes a
// second capture at a different address -- a change of stimulus, not a sharper look at one waveform.
//
// TWO HONEST LIMITS, BOTH MEASURED RATHER THAN CLAIMED.
//
// * `ob_len_err` says the master's clock count is wrong and CANNOT say which of its fields is wrong,
// because three field lengths feed one total. An over-long address, an over-long dummy run and an
// over-long data phase are the same number.
//
// * A RESPONSE EQUAL TO THE BUS'S RESTING LEVEL IS INDISTINGUISHABLE FROM NO RESPONSE AT ALL. If the
// device answers 0x00 on a bus resting at 0, MISO never changes -- and neither does it change when
// the device ignored the command entirely. One verdict, `D_QUIET`, covers both, and `ob_ndisp`
// separates them: zero matching displacements means the device really was silent, and a full set
// means it may have answered perfectly and this bus cannot show it.
//
// That is a stronger statement than it first looks. It says a debug read must never target an
// address whose contents equal the resting level, which is a constraint on the DEBUG PROCEDURE
// rather than on the design. Chapter 18.3 reached the same two payloads -- 0x00 and 0xFF -- for an
// unrelated reason: there they were invariant under PERMUTATION, here they are invisible against
// the bus's own idle state.
//
// Chapter 18.1 met an overlap it chose not to separate and argued a finer decoder could report the
// compound. This is that finer decoder: when both observations fail it reports BOTH rather than
// ranking one above the other, because the two numbers are independent and a priority would discard
// one of them.
`timescale 1ns/1ps
module spi_len_diag #(
parameter DW = 32,
parameter CNT_W = 8
) (
input wire clk,
input wire rst_n,
input wire sclk,
input wire cs_n,
input wire mosi,
input wire miso,
input wire cpol,
input wire cpha,
// THE INTENDED CONFIGURATION -- what the datasheet says this transfer should be. Every number the
// module reports is relative to these, which is the honest shape for a diagnostic: it does not
// know what is correct, it knows what was intended.
input wire [CNT_W-1:0] cmd_w,
input wire [CNT_W-1:0] addr_w,
input wire [CNT_W-1:0] dummy_n,
input wire [CNT_W-1:0] data_w,
input wire [DW-1:0] word_exp,
output reg dg_valid,
output reg [2:0] dg_code,
output reg [CNT_W-1:0] ob_leads, // leading edges in the frame
output reg signed [CNT_W:0] ob_len_err, // against cmd_w + addr_w + dummy_n + data_w
output reg ob_moved, // did MISO ever leave the resting level?
output reg [CNT_W-1:0] ob_first_move, // and when -- kept because it is an EARLY-launch proof
output reg signed [CNT_W:0] ob_disp, // the displacement of the expected data, when unique
output reg [3:0] ob_ndisp, // how many displacements match: 1 is decidable, more is not
output reg [DW-1:0] ob_cmd, // the command byte, so NOLAUNCH names what was ignored
output reg [DW-1:0] ob_data // MISO over the INTENDED data window
);
localparam [2:0] D_OK = 3'd0,
D_MASTER_LEN = 3'd1, // the master's own clock count is wrong; not which field
D_DEVICE_PHASE = 3'd2, // the data is present and displaced: the device's count
D_FIELD_WIDTH = 3'd3, // both numbers agree with a good transfer; data absent
D_BOTH = 3'd4, // both wrong -- reported, not ranked
// MISO never left the resting level. This does NOT mean "the device was silent":
// it means silence and a response equal to the resting level are the same
// observation. `ob_ndisp` says which reading is available.
D_QUIET = 3'd5;
reg sclk_d, cs_n_d;
reg miso_rest;
reg [CNT_W-1:0] leads, first_move;
reg moved;
reg [DW-1:0] cmd_acc, data_acc;
wire cs_assert = cs_n_d & ~cs_n;
wire cs_deassert = ~cs_n_d & cs_n;
wire in_txn = ~cs_n | cs_deassert;
wire sclk_edge = (sclk !== sclk_d);
wire leading = sclk_edge && (sclk !== cpol);
wire capture = (cpha ? (sclk_edge && !leading) : leading) && in_txn;
// The intended instants, derived from the configuration rather than hard-coded, so one module
// serves a 3-byte and a 4-byte addressing device.
wire [CNT_W-1:0] launch_exp = cmd_w + addr_w + dummy_n;
wire [CNT_W-1:0] leads_exp = cmd_w + addr_w + dummy_n + data_w;
// The expected byte displaced by `d` inside the data window and padded with the resting level.
// d > 0 means the device drove LATE, so the window opens with resting-level bits; d < 0 means it
// drove EARLY and the window closes with them.
//
// Every index is a variable index into a fixed-width vector, never a part-select, because
// `data_w` is a run-time input.
function [DW-1:0] displaced;
input [DW-1:0] w;
input integer d;
input integer nb;
input rest;
integer i, src;
reg [DW-1:0] r;
begin
r = {DW{1'b0}};
for (i = 0; i < DW; i = i + 1) begin
if (i < nb) begin
// Window bit i is sampled at launch_exp + (nb-1-i); the device is then `d` cycles
// into its own transmission offset by -d.
src = (nb - 1 - i) - d;
if ((src >= 0) && (src < nb)) r[i] = w[nb - 1 - src];
else r[i] = rest;
end
end
displaced = r;
end
endfunction
// How many displacements explain the window, and which. A COUNT of one is what makes the
// displacement a finding; anything else is a set, and a set of size fifteen is silence.
function [3:0] ndisp;
input [DW-1:0] w;
input [DW-1:0] obs;
input integer nb;
input rest;
integer d;
reg [DW-1:0] m;
integer i;
begin
m = {DW{1'b0}};
for (i = 0; i < DW; i = i + 1) if (i < nb) m[i] = 1'b1;
ndisp = 4'd0;
for (d = -(DW-1); d <= (DW-1); d = d + 1)
if ((d > -nb) && (d < nb) && ((displaced(w, d, nb, rest) & m) == (obs & m))
&& (ndisp < 4'd15))
ndisp = ndisp + 4'd1;
end
endfunction
function signed [CNT_W:0] firstdisp;
input [DW-1:0] w;
input [DW-1:0] obs;
input integer nb;
input rest;
integer d;
reg [DW-1:0] m;
integer i;
reg done;
begin
m = {DW{1'b0}};
for (i = 0; i < DW; i = i + 1) if (i < nb) m[i] = 1'b1;
firstdisp = {(CNT_W+1){1'b0}};
done = 1'b0;
// Searched outwards from zero so that the smallest displacement is reported first: when
// several match, the one needing the least explanation is the one worth printing.
for (d = 0; d < DW; d = d + 1) begin
if (!done && (d < nb) && ((displaced(w, d, nb, rest) & m) == (obs & m))) begin
firstdisp = d; done = 1'b1;
end
if (!done && (d != 0) && (d < nb)
&& ((displaced(w, -d, nb, rest) & m) == (obs & m))) begin
firstdisp = -d; done = 1'b1;
end
end
end
endfunction
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
miso_rest <= 1'b0;
leads <= {CNT_W{1'b0}};
first_move <= {CNT_W{1'b0}};
moved <= 1'b0;
cmd_acc <= {DW{1'b0}};
data_acc <= {DW{1'b0}};
dg_valid <= 1'b0;
dg_code <= D_OK;
ob_leads <= {CNT_W{1'b0}};
ob_len_err <= {(CNT_W+1){1'b0}};
ob_moved <= 1'b0;
ob_first_move <= {CNT_W{1'b0}};
ob_disp <= {(CNT_W+1){1'b0}};
ob_ndisp <= 4'd0;
ob_cmd <= {DW{1'b0}};
ob_data <= {DW{1'b0}};
end else begin
dg_valid <= 1'b0;
if (cs_assert) begin
// The resting level is sampled AT the select, before any driver can have started --
// the only instant at which "what the bus does when nobody drives it" is on show. On a
// real board that level is set by a pull resistor, not by a device.
miso_rest <= miso;
leads <= {CNT_W{1'b0}};
first_move <= {CNT_W{1'b0}};
moved <= 1'b0;
cmd_acc <= {DW{1'b0}};
data_acc <= {DW{1'b0}};
end else if (capture) begin
if (!moved && (miso !== miso_rest)) begin
moved <= 1'b1;
first_move <= leads;
end
if (leads < cmd_w)
cmd_acc[cmd_w - 1'b1 - leads] <= mosi;
// The data window is the INTENDED one. Sampling where the transfer was supposed to put
// the data -- rather than where the device happened to put it -- is what makes the
// window comparable against a displaced expectation at all.
if ((leads >= launch_exp) && (leads < leads_exp))
data_acc[data_w - 1'b1 - (leads - launch_exp)] <= miso;
leads <= leads + 1'b1;
end
if (cs_deassert) begin
dg_valid <= 1'b1;
ob_leads <= leads;
ob_len_err <= $signed({1'b0, leads}) - $signed({1'b0, leads_exp});
ob_moved <= moved;
ob_first_move <= first_move;
ob_cmd <= cmd_acc;
ob_data <= data_acc;
ob_ndisp <= ndisp(word_exp, data_acc, data_w, miso_rest);
ob_disp <= firstdisp(word_exp, data_acc, data_w, miso_rest);
if (!moved)
// MISO never left the resting level, and that is all this says. The device may have
// ignored the command -- `ob_cmd` names which command -- or it may have answered
// with a byte equal to the resting level, in which case a correct response and no
// response are the same waveform. `ob_ndisp` is the disambiguator: 0 means the
// expected data is nowhere in the window at any displacement, so the device really
// was silent; a full set means the expected data is EVERYWHERE and the bus cannot
// distinguish a perfect answer from none.
dg_code <= D_QUIET;
else if (ndisp(word_exp, data_acc, data_w, miso_rest) == 4'd0)
// The expected data is not in the window at ANY displacement, so this is not a
// timing fault: the device answered a different question. With the total also
// correct, a traded field width is what remains -- and localising which field
// needs a second capture.
dg_code <= (leads == leads_exp) ? D_FIELD_WIDTH : D_BOTH;
else if ((leads != leads_exp) && (firstdisp(word_exp, data_acc, data_w, miso_rest) != 0))
dg_code <= D_BOTH;
else if (leads != leads_exp)
dg_code <= D_MASTER_LEN;
else if (firstdisp(word_exp, data_acc, data_w, miso_rest) != 0)
// More than one matching displacement requires a window equal to the resting level
// throughout, which is the `!moved` case already taken above -- so there is no arm
// here for an ambiguous displacement, and deliberately no untestable branch
// pretending to handle one. `ob_ndisp` is still published, because a reader should
// be able to see that the count was 1 rather than take it on trust.
dg_code <= D_DEVICE_PHASE;
else
dg_code <= D_OK;
end
sclk_d <= sclk;
cs_n_d <= cs_n;
end
end
endmodule-- spi_len_diag.vhd
--
-- Chapter 18.4 -- transfer width against dummy cycles, and two observations that separate four faults
-- while a fifth stays undecidable from one capture.
--
-- THE FAULT FAMILY. A flash read is a command, an address, some dummy cycles and then data, and every
-- one of those lengths is a number somebody copied out of a datasheet table. Get one wrong and the
-- read returns garbage. Several different numbers can be wrong, the symptom is the same, and the fix
-- is in a different place for each:
--
-- the MASTER's clock accounting a driver constant
-- the DEVICE's dummy-cycle count a configuration register never written, or a part that is
-- not the part in the schematic
-- the ADDRESS WIDTH 3-byte against 4-byte addressing
--
-- WHY A LENGTH CHECK SEPARATES NONE OF THEM, and this is what the chapter is built on.
--
-- A master configured for a 32-bit address with 0 dummy cycles issues EXACTLY as many clocks as one
-- configured for a 24-bit address with 8 dummy cycles: 8 + 32 + 0 + 8 = 8 + 24 + 8 + 8 = 48. The total
-- is identical, so Chapter 18.1's edge-count predicate is silent. And the device begins driving at its
-- own instant either way, so the data is not displaced either. EVERY TIMING OBSERVATION AGREES WITH A
-- CORRECT TRANSFER. Only the data disagrees, and it disagrees into a byte that is a perfectly valid
-- response for a different address -- so it does not look like corruption either.
--
-- That trade is not contrived. It is the mistake the datasheet invites, because address width and
-- dummy count are adjacent entries in one table and a 4-byte-address mode conventionally carries a
-- different dummy count.
--
-- -----------------------------------------------------------------------------------------------
-- THE OBSERVATION THAT DOES NOT WORK, AND WHY IT IS DOCUMENTED HERE RATHER THAN DELETED
-- -----------------------------------------------------------------------------------------------
--
-- The obvious measurement is WHEN the device began driving MISO: time the first moment MISO leaves the
-- level the bus rests at while nobody drives it. The first version of this module did exactly that,
-- and it is wrong in a way worth keeping on the record.
--
-- MISO's first VISIBLE change is not the launch. It is an upper bound on the launch, because a device
-- that starts driving with a bit equal to the resting level produces no transition to see. So:
--
-- first change EARLIER than expected -> the device definitely launched early
-- first change LATER than expected -> the device launched late, OR launched on time with a
-- leading bit that happened to match the resting bus
--
-- A late launch is therefore NOT PROVABLE from a level. And the failure is not a missing number: with
-- a 32-bit address traded against 0 dummy cycles the device launched exactly on time and its first bit
-- matched the resting bus, so the module reported a launch one cycle late and diagnosed a device-side
-- dummy fault. A confident wrong answer, pointing at the wrong device, from an observation that looked
-- obviously correct.
--
-- -----------------------------------------------------------------------------------------------
-- THE TWO OBSERVATIONS THAT DO WORK
-- -----------------------------------------------------------------------------------------------
--
-- ob_len_err the total leading-edge count, against the intended configuration.
--
-- ob_disp the DISPLACEMENT of the expected data within the intended data window. The module
-- samples MISO over the window the transfer was supposed to put data in, then asks
-- for which displacement d the expected byte -- shifted by d and padded with the
-- resting level -- equals what was sampled. A displacement is decidable where a
-- launch instant is not, because it uses the whole byte instead of one transition.
--
-- len_err disp
-- correct 0 0
-- master's clock accounting wrong +/- 0 -> the MASTER, but not WHICH field
-- device wants more dummy cycles 0 +2 -> the DEVICE's register
-- device wants fewer 0 -2 -> the DEVICE's register
-- both +/- +/- -> reported as a compound
-- address width traded for dummy cycles 0 0 -> a FIELD WIDTH; needs capture #2
--
-- Four faults separated by two numbers. The trade agrees with a correct transfer on both, and nothing
-- in ONE capture separates it from a correct transfer that returned unexpected contents. It takes a
-- second capture at a different address -- a change of stimulus, not a sharper look at one waveform.
--
-- TWO HONEST LIMITS, BOTH MEASURED RATHER THAN CLAIMED.
--
-- * `ob_len_err` says the master's clock count is wrong and CANNOT say which of its fields is wrong,
-- because three field lengths feed one total. An over-long address, an over-long dummy run and an
-- over-long data phase are the same number.
--
-- * A RESPONSE EQUAL TO THE BUS'S RESTING LEVEL IS INDISTINGUISHABLE FROM NO RESPONSE AT ALL. If the
-- device answers 0x00 on a bus resting at 0, MISO never changes -- and neither does it change when
-- the device ignored the command entirely. One verdict, `D_QUIET`, covers both, and `ob_ndisp`
-- separates them: zero matching displacements means the device really was silent, and a full set
-- means it may have answered perfectly and this bus cannot show it.
--
-- That is a stronger statement than it first looks. It says a debug read must never target an
-- address whose contents equal the resting level, which is a constraint on the DEBUG PROCEDURE
-- rather than on the design. Chapter 18.3 reached the same two payloads -- 0x00 and 0xFF -- for an
-- unrelated reason: there they were invariant under PERMUTATION, here they are invisible against
-- the bus's own idle state.
--
-- Chapter 18.1 met an overlap it chose not to separate and argued a finer decoder could report the
-- compound. This is that finer decoder: when both observations fail it reports BOTH rather than
-- ranking one above the other, because the two numbers are independent and a priority would discard
-- one of them.
--
-- WHAT THE VHDL VERSION ADDS. The displacement is an INTEGER with an explicit range rather than a
-- hand-rolled signed vector, so the negative case -- a device driving EARLY -- cannot be silently
-- reinterpreted as a large positive number by an unsigned comparison. The Verilog versions spell this
-- as `signed [CNT_W:0]` and rely on every comparison against it staying signed; here the type carries
-- the obligation.
--
-- The verdict is an enumeration, which matters for one specific reason in this chapter: `D_QUIET` is
-- not a failure code and it is not a success code, and having it sit in a named type next to the other
-- five makes the reader ask what it means instead of assuming.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). The generics are `DW_C` and `CNT_W`. The configuration
-- ports are `cmd_w`, `addr_w`, `dummy_n`, `data_w`; the derived constants inside the process are
-- `n_cmd`, `n_addr`, `n_dummy`, `n_data` -- deliberately NOT `CMD_W`/`cmd_w` pairs, because a variable
-- `cmd_w` and the port `cmd_w` would be one identifier and the assignment would drive the port. That is
-- the failure Chapter 17.4 spent an afternoon on, so every declaration below was re-read against it.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package spi_len_pkg is
constant DW_C : natural := 32;
-- Six verdicts. `D_QUIET` is the interesting one: it names an OBSERVATION -- MISO never left the
-- resting level -- rather than a cause, because two different causes produce that observation and
-- the wire does not distinguish them.
type len_diag_t is (D_OK, D_MASTER_LEN, D_DEVICE_PHASE, D_FIELD_WIDTH, D_BOTH, D_QUIET);
function diag_name (d : len_diag_t) return string;
-- The expected byte displaced by `d` inside the data window, padded with the resting level.
-- d > 0: the device drove LATE, so the window opens with resting-level bits.
-- d < 0: it drove EARLY, so the window closes with them.
function displaced (w : std_logic_vector; d : integer; nb : natural; rest : std_logic)
return std_logic_vector;
-- How many displacements explain the window. A count of one is what makes a displacement a
-- finding; more than one is a set, and a full set is silence.
function n_disp (w, obs : std_logic_vector; nb : natural; rest : std_logic) return natural;
-- The matching displacement nearest to zero, searched outwards, so that when several match the
-- one needing the least explanation is the one reported.
function first_disp (w, obs : std_logic_vector; nb : natural; rest : std_logic) return integer;
end package spi_len_pkg;
package body spi_len_pkg is
function diag_name (d : len_diag_t) return string is
begin
case d is
when D_OK => return "OK ";
when D_MASTER_LEN => return "MASTER_LEN ";
when D_DEVICE_PHASE => return "DEVICE_PHASE ";
when D_FIELD_WIDTH => return "FIELD_WIDTH ";
when D_BOTH => return "BOTH ";
when others => return "QUIET ";
end case;
end function diag_name;
function displaced (w : std_logic_vector; d : integer; nb : natural; rest : std_logic)
return std_logic_vector is
variable r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
variable src : integer;
begin
for i in 0 to DW_C - 1 loop
if i < nb then
-- Window bit i is sampled at launch_exp + (nb-1-i); the device is then `d` cycles into
-- its own transmission offset by -d.
src := (nb - 1 - i) - d;
if src >= 0 and src < nb then r(i) := w(nb - 1 - src);
else r(i) := rest;
end if;
end if;
end loop;
return r;
end function displaced;
function n_disp (w, obs : std_logic_vector; nb : natural; rest : std_logic) return natural is
variable m : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
variable n : natural := 0;
begin
for i in 0 to DW_C - 1 loop
if i < nb then m(i) := '1'; end if;
end loop;
for d in -(DW_C - 1) to DW_C - 1 loop
if d > -nb and d < nb
and ((displaced(w, d, nb, rest) and m) = (obs and m)) then
n := n + 1;
end if;
end loop;
return n;
end function n_disp;
function first_disp (w, obs : std_logic_vector; nb : natural; rest : std_logic) return integer is
variable m : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
begin
for i in 0 to DW_C - 1 loop
if i < nb then m(i) := '1'; end if;
end loop;
for d in 0 to DW_C - 1 loop
if d < nb and ((displaced(w, d, nb, rest) and m) = (obs and m)) then
return d;
end if;
if d /= 0 and d < nb and ((displaced(w, -d, nb, rest) and m) = (obs and m)) then
return -d;
end if;
end loop;
return 0;
end function first_disp;
end package body spi_len_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_len_pkg.all;
entity spi_len_diag is
generic (
CNT_W : positive := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
sclk : in std_logic;
cs_n : in std_logic;
mosi : in std_logic;
miso : in std_logic;
cpol : in std_logic;
cpha : in std_logic;
-- The intended configuration.
cmd_w : in unsigned(CNT_W - 1 downto 0);
addr_w : in unsigned(CNT_W - 1 downto 0);
dummy_n : in unsigned(CNT_W - 1 downto 0);
data_w : in unsigned(CNT_W - 1 downto 0);
word_exp : in std_logic_vector(DW_C - 1 downto 0);
dg_valid : out std_logic;
dg_code : out len_diag_t;
ob_leads : out natural;
ob_len_err : out integer;
ob_moved : out boolean;
ob_first_move : out natural; -- kept as an EARLY-launch proof, never as the launch itself
ob_disp : out integer;
ob_ndisp : out natural;
ob_cmd : out std_logic_vector(DW_C - 1 downto 0);
ob_data : out std_logic_vector(DW_C - 1 downto 0)
);
end entity spi_len_diag;
architecture rtl of spi_len_diag is
signal v_r : std_logic := '0';
signal d_r : len_diag_t := D_OK;
signal lead_r : natural := 0;
signal lerr_r : integer := 0;
signal mov_r : boolean := false;
signal fm_r : natural := 0;
signal dsp_r : integer := 0;
signal nd_r : natural := 0;
signal cmd_r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal dat_r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
begin
dg_valid <= v_r;
dg_code <= d_r;
ob_leads <= lead_r;
ob_len_err <= lerr_r;
ob_moved <= mov_r;
ob_first_move <= fm_r;
ob_disp <= dsp_r;
ob_ndisp <= nd_r;
ob_cmd <= cmd_r;
ob_data <= dat_r;
process (clk, rst_n) is
variable sclk_d, cs_n_d : std_logic;
variable miso_rest : std_logic;
variable cs_assert, cs_deassert : boolean;
variable in_txn, sclk_edge : boolean;
variable leading, capture : boolean;
variable leads, first_move : natural;
variable moved : boolean;
variable cmd_acc, data_acc : std_logic_vector(DW_C - 1 downto 0);
variable n_cmd, n_addr : natural;
variable n_dummy, n_data : natural;
variable launch_exp, leads_exp : natural;
variable nd : natural;
variable dd : integer;
begin
if rst_n = '0' then
sclk_d := '0'; cs_n_d := '1'; miso_rest := '0';
leads := 0; first_move := 0; moved := false;
cmd_acc := (others => '0'); data_acc := (others => '0');
v_r <= '0'; d_r <= D_OK; lead_r <= 0; lerr_r <= 0;
mov_r <= false; fm_r <= 0; dsp_r <= 0; nd_r <= 0;
cmd_r <= (others => '0'); dat_r <= (others => '0');
elsif rising_edge(clk) then
v_r <= '0';
-- Read from the PORTS, not from a concurrent signal: a concurrent assignment is one delta
-- stale inside a clocked process, the defect that made two languages disagree in 17.2.
n_cmd := to_integer(cmd_w);
n_addr := to_integer(addr_w);
n_dummy := to_integer(dummy_n);
n_data := to_integer(data_w);
launch_exp := n_cmd + n_addr + n_dummy;
leads_exp := launch_exp + n_data;
cs_assert := (cs_n = '0') and (cs_n_d = '1');
cs_deassert := (cs_n = '1') and (cs_n_d = '0');
in_txn := (cs_n = '0') or cs_deassert;
sclk_edge := (sclk /= sclk_d);
leading := sclk_edge and (sclk /= cpol);
if cpha = '0' then capture := leading and in_txn;
else capture := sclk_edge and (not leading) and in_txn;
end if;
if cs_assert then
-- The resting level, sampled AT the select -- the only instant at which what the bus
-- does when nobody drives it is on show. On a board that level is a pull resistor.
miso_rest := miso;
leads := 0;
first_move := 0;
moved := false;
cmd_acc := (others => '0');
data_acc := (others => '0');
elsif capture then
if (not moved) and (miso /= miso_rest) then
moved := true;
first_move := leads;
end if;
if leads < n_cmd then
cmd_acc(n_cmd - 1 - leads) := mosi;
end if;
-- The INTENDED window. Sampling where the transfer was supposed to put the data is
-- what makes the window comparable against a displaced expectation at all.
if leads >= launch_exp and leads < leads_exp then
data_acc(n_data - 1 - (leads - launch_exp)) := miso;
end if;
leads := leads + 1;
end if;
if cs_deassert then
nd := n_disp(word_exp, data_acc, n_data, miso_rest);
dd := first_disp(word_exp, data_acc, n_data, miso_rest);
v_r <= '1';
lead_r <= leads;
lerr_r <= leads - leads_exp;
mov_r <= moved;
fm_r <= first_move;
dsp_r <= dd;
nd_r <= nd;
cmd_r <= cmd_acc;
dat_r <= data_acc;
if not moved then
-- MISO never left the resting level, and that is ALL this says. The device may have
-- ignored the command -- `ob_cmd` names which -- or it may have answered with a
-- byte equal to the resting level, in which case a correct response and no response
-- are the same waveform. `ob_ndisp` is the disambiguator.
d_r <= D_QUIET;
elsif nd = 0 then
-- The expected data is nowhere in the window at any displacement, so this is not a
-- timing fault: the device answered a different question.
if leads = leads_exp then d_r <= D_FIELD_WIDTH;
else d_r <= D_BOTH;
end if;
elsif leads /= leads_exp and dd /= 0 then
d_r <= D_BOTH;
elsif leads /= leads_exp then
d_r <= D_MASTER_LEN;
elsif dd /= 0 then
-- More than one matching displacement requires a window equal to the resting level
-- throughout, which is the `not moved` case already taken above -- so there is no
-- arm here for an ambiguous displacement, and deliberately no untestable branch
-- pretending to handle one.
d_r <= D_DEVICE_PHASE;
else
d_r <= D_OK;
end if;
end if;
sclk_d := sclk;
cs_n_d := cs_n;
end if;
end process;
end architecture rtl;The Bench
The device is modelled in the bench, deliberately. The chapter's artefact is the instrument, and a diagnostic tested against a device model that shares its assumptions has been tested against itself. The slave procedure is written from the datasheet's description — latch a command, latch three address bytes, wait its own dummy count, then drive — and it never reads the decoder's configuration inputs. The two disagree about the transfer's shape on seven of ten frames, and that disagreement is the measurement.
// spi_len_diag_tb.sv
//
// TEN CAPTURES OF A FLASH READ, AND THE ONE FAULT THAT TWO NUMBERS CANNOT REACH.
//
// THE DEVICE IS MODELLED IN THE BENCH, deliberately. The chapter's artefact is the INSTRUMENT, and a
// diagnostic tested against a device model that shares its assumptions has been tested against itself.
// So the slave here is written from the datasheet's description -- latch a command, latch three address
// bytes, wait its OWN dummy count, then drive -- and it never reads the decoder's configuration inputs.
// The two disagree about the transfer's shape on seven of ten frames, and that disagreement is the
// measurement.
//
// THE FIVE RESULTS.
//
// 1. THE TRADE IS INVISIBLE TO BOTH OBSERVATIONS. A 32-bit address with 0 dummy cycles and a 24-bit
// address with 8 produce the same total edge count AND leave the data undisplaced. The bench
// requires both numbers to EQUAL the correct frame's, so the claim is measured, not asserted.
//
// 2. THE DATA IT RETURNS IS VALID, NOT CORRUPT. The device latched a different address and answered
// correctly for it. The bench checks the byte against the device's response function evaluated at
// the WRONGLY LATCHED address, so "this looks like real data" is a computed fact.
//
// 3. TWO CAPTURES SEPARATE WHAT ONE CANNOT. Every fault is run at two addresses. A width fault
// changes its answer with the address; an unrecognised command does not. That is the difference
// between a field parsed at the wrong width and a field never parsed, and it is a property of a
// PAIR of captures rather than of either one.
//
// 4. THE TOTAL CANNOT LOCALISE A FIELD, AND THE BENCH PROVES IT BY COLLISION. A master inserting two
// extra dummy cycles and a master reading eight extra data bits are different faults in different
// driver constants, and both are reported as MASTER_LEN with only the magnitude to tell them
// apart. Three field lengths feed one total; a total cannot un-sum itself.
//
// 5. A CORRECT ANSWER CAN BE INDISTINGUISHABLE FROM SILENCE. The last frame reads an address whose
// response is 0x00 on a bus resting at 0, so MISO never moves -- exactly the waveform an ignored
// command produces. The bench requires the two to receive the SAME verdict and then separates them
// on the displacement count: zero matches means the device was silent, a full set means it may have
// answered perfectly. Chapter 18.3 rejected the same payload for an unrelated reason -- invariance
// under permutation rather than invisibility against the idle bus.
`timescale 1ns/1ps
module spi_len_diag_tb;
localparam int DW = 32;
localparam int CNT_W = 8;
localparam int LEAD = 3;
localparam int HALF = 2;
localparam int LAG = 2;
localparam int GAP = 4;
localparam [2:0] D_OK = 3'd0, D_MASTER_LEN = 3'd1, D_DEVICE_PHASE = 3'd2,
D_FIELD_WIDTH = 3'd3, D_BOTH = 3'd4, D_QUIET = 3'd5;
// The transfer the datasheet describes.
localparam [CNT_W-1:0] CMD_W = 8'd8, ADDR_W = 8'd24, DUMMY_N = 8'd8, DATA_W = 8'd8;
localparam [7:0] CMD_READ = 8'h0B;
// Two addresses, chosen so that the device's response is measurable against a pull-down (top bit
// of the expected byte is 1) and so that the wrongly-latched addresses differ from each other --
// without which result 3 could not be demonstrated.
localparam [23:0] ADDR_A = 24'h1234F1; // response 0x8d
localparam [23:0] ADDR_B = 24'h5678B7; // response 0xc3
// And one whose response is 0x00 -- the payload that cannot locate itself on a bus resting at 0.
localparam [23:0] ADDR_C = 24'h12347C; // response 0x00
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 b_sclk = 1'b0, b_cs_n = 1'b1, b_mosi = 1'b0, b_miso = 1'b0;
wire dg_valid, ob_moved;
wire [2:0] dg_code;
wire [CNT_W-1:0] ob_leads, ob_first_move;
wire signed [CNT_W:0] ob_len_err, ob_disp;
wire [3:0] ob_ndisp;
wire [DW-1:0] ob_cmd, ob_data;
spi_len_diag #(.DW(DW), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi), .miso(b_miso),
.cpol(cpol), .cpha(cpha),
.cmd_w(CMD_W), .addr_w(ADDR_W), .dummy_n(DUMMY_N), .data_w(DATA_W),
.word_exp(word_exp),
.dg_valid(dg_valid), .dg_code(dg_code),
.ob_leads(ob_leads), .ob_len_err(ob_len_err), .ob_moved(ob_moved),
.ob_first_move(ob_first_move), .ob_disp(ob_disp), .ob_ndisp(ob_ndisp),
.ob_cmd(ob_cmd), .ob_data(ob_data)
);
integer errors = 0, x_reports = 0, got_n = 0;
reg [2:0] g_code;
reg [CNT_W-1:0] g_leads, g_fmove;
reg signed [CNT_W:0] g_len, g_disp;
reg [3:0] g_nd;
reg [DW-1:0] g_data, g_cmd;
reg g_moved;
always @(posedge clk) if (dg_valid) begin
got_n = got_n + 1;
g_code = dg_code; g_leads = ob_leads; g_fmove = ob_first_move;
g_len = ob_len_err; g_disp = ob_disp; g_nd = ob_ndisp;
g_data = ob_data; g_cmd = ob_cmd; g_moved = ob_moved;
if ((^dg_code === 1'bx) || (^ob_leads === 1'bx) || (^ob_first_move === 1'bx)
|| (^ob_data[7:0] === 1'bx) || (^ob_cmd[7:0] === 1'bx) || (^ob_ndisp === 1'bx)
|| (ob_moved === 1'bx) || (^ob_len_err === 1'bx) || (^ob_disp === 1'bx))
x_reports = x_reports + 1;
end
// THE DEVICE'S RESPONSE FUNCTION, written from its description. A response that mixes all three
// address bytes is what makes a mis-latched address produce a DIFFERENT but equally valid byte --
// which is the whole reason a width fault does not look like corruption.
function [7:0] dev_data(input [23:0] a);
begin dev_data = (a[7:0] ^ a[15:8] ^ a[23:16]) ^ 8'h5A; end
endfunction
function [8*14:1] dname(input [2:0] c);
begin
case (c)
D_OK: dname = "OK ";
D_MASTER_LEN: dname = "MASTER_LEN ";
D_DEVICE_PHASE: dname = "DEVICE_PHASE ";
D_FIELD_WIDTH: dname = "FIELD_WIDTH ";
D_BOTH: dname = "BOTH ";
default: dname = "QUIET ";
endcase
end
endfunction
task automatic idle_n(input integer n);
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// ---- one flash read ----
//
// cmd the command byte the master issues
// addr the address, as a 32-bit value
// m_addr_w how many address bits the MASTER sends (24 or 32)
// m_dummy how many dummy cycles the MASTER inserts
// m_data_w how many data bits the MASTER clocks out
// dev_dummy how many dummy cycles the DEVICE waits for
// rest_lvl what MISO rests at while nobody drives it -- a pull resistor, not a signal
//
// The device model is inline and reads only `cmd`, the first 24 address bits and `dev_dummy`, so it
// cannot inherit the master's misconfiguration. That separation is the whole reason the bench is
// evidence about the decoder rather than a restatement of it.
task automatic flash_read(input [7:0] cmd,
input [31:0] addr,
input integer m_addr_w,
input integer m_dummy,
input integer m_data_w,
input integer dev_dummy,
input rest_lvl);
integer k, total, dev_launch;
reg [7:0] payload;
reg [23:0] dev_addr;
begin
// What the device latches: the FIRST 24 address bits the master sends. With a 32-bit master
// and a 24-bit device that is the top three bytes of four -- the address shifted by a byte,
// which is why the returned data is valid and wrong.
if (m_addr_w == 32) dev_addr = addr[31:8];
else dev_addr = addr[23:0];
payload = dev_data(dev_addr);
dev_launch = 8 + 24 + dev_dummy;
total = 8 + m_addr_w + m_dummy + m_data_w;
b_sclk = cpol; b_mosi = 1'b0; b_miso = rest_lvl; b_cs_n = 1'b1;
idle_n(2);
b_cs_n = 1'b0;
idle_n(1);
b_mosi = cmd[7];
idle_n(LEAD - 1);
for (k = 0; k < total; k = k + 1) begin
b_sclk = ~b_sclk; // leading edge, index k
idle_n(HALF);
b_sclk = ~b_sclk; // trailing edge -- both ends move here
if (k + 1 < 8)
b_mosi = cmd[7 - (k+1)];
else if ((k + 1 >= 8) && (k + 1 < 8 + m_addr_w))
b_mosi = addr[m_addr_w - 1 - (k + 1 - 8)];
else
b_mosi = 1'b0;
// MISO: the device drives only if it recognised the command, and only from its OWN
// launch instant. Before and after that the bus rests where the resistor puts it.
if ((cmd == CMD_READ) && (k + 1 >= dev_launch) && (k + 1 < dev_launch + 8))
b_miso = payload[7 - (k + 1 - dev_launch)];
else
b_miso = rest_lvl;
idle_n(HALF);
end
idle_n(LAG);
b_cs_n = 1'b1;
idle_n(1);
b_sclk = cpol;
b_miso = rest_lvl;
idle_n(GAP);
end
endtask
integer s, base;
reg [2:0] code_log [0:9];
reg [DW-1:0] data_log [0:9];
integer leads_log[0:9], disp_log[0:9], nd_log[0:9];
reg [2:0] want;
integer w_leads, w_disp, mutations;
initial begin
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(" leads len_err disp nd moved 1st cmd data exp verdict expected stimulus");
for (s = 0; s < 10; s = s + 1) begin
@(negedge clk);
b_cs_n = 1'b1; b_sclk = cpol; b_mosi = 1'b0;
idle_n(2); rst_n = 1'b0; idle_n(3); rst_n = 1'b1; idle_n(2);
base = got_n;
word_exp = {24'b0, dev_data(ADDR_A)};
case (s)
// A correct read at address A. A decoder never shown clean traffic has not been shown
// to be silent.
0: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 8, 8, 8, 1'b0);
want = D_OK; w_leads = 48; w_disp = 0; end
// THE TRADE at address A: 32 address bits, 0 dummy cycles. Same total, no displacement.
1: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 32, 0, 8, 8, 1'b0);
want = D_FIELD_WIDTH; w_leads = 48; w_disp = 0; end
// THE TRADE at address B: the answer CHANGES with the address, so the address field is
// being parsed -- at the wrong width.
2: begin word_exp = {24'b0, dev_data(ADDR_B)};
flash_read(CMD_READ, {8'h00, ADDR_B}, 32, 0, 8, 8, 1'b0);
want = D_FIELD_WIDTH; w_leads = 48; w_disp = 0; end
// An unrecognised command at address A: nothing is ever driven.
3: begin flash_read(8'h0C, {8'h00, ADDR_A}, 24, 8, 8, 8, 1'b0);
want = D_QUIET; w_leads = 48; w_disp = 0; end
// The same unrecognised command at address B: the answer does NOT change, so the
// address was never parsed at all.
4: begin word_exp = {24'b0, dev_data(ADDR_B)};
flash_read(8'h0C, {8'h00, ADDR_B}, 24, 8, 8, 8, 1'b0);
want = D_QUIET; w_leads = 48; w_disp = 0; end
// The MASTER inserts two dummy cycles too many. The total moves; the data does not.
5: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 10, 8, 8, 1'b0);
want = D_MASTER_LEN; w_leads = 50; w_disp = 0; end
// The MASTER clocks eight extra DATA bits. A different constant in a different line of
// the driver, and the SAME verdict -- because a total cannot un-sum itself.
6: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 8, 16, 8, 1'b0);
want = D_MASTER_LEN; w_leads = 56; w_disp = 0; end
// The DEVICE wants two more dummy cycles than it is given: the data is present and LATE.
7: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 8, 8, 10, 1'b0);
want = D_DEVICE_PHASE; w_leads = 48; w_disp = 2; end
// The DEVICE wants two fewer: present and EARLY. The sign of one number names the
// direction of a register somebody has to write.
8: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 8, 8, 6, 1'b0);
want = D_DEVICE_PHASE; w_leads = 48; w_disp = -2; end
// A CORRECT ANSWER THAT LOOKS LIKE SILENCE. Address C answers 0x00 on a bus resting at
// 0, so MISO never moves -- the same waveform an ignored command produces. The verdict
// is therefore the SAME code as stimuli 3 and 4, and the disambiguator is `ob_ndisp`.
9: begin word_exp = {24'b0, dev_data(ADDR_C)};
flash_read(CMD_READ, {8'h00, ADDR_C}, 24, 8, 8, 10, 1'b0);
want = D_QUIET; w_leads = 48; w_disp = 0; end
endcase
code_log[s] = g_code;
data_log[s] = g_data;
leads_log[s] = g_leads;
disp_log[s] = g_disp;
nd_log[s] = g_nd;
$display(" %5d %7d %4d %2d %5b %3d %02h %02h %02h %s %s %0s",
g_leads, g_len, g_disp, g_nd, g_moved, g_fmove,
g_cmd[7:0], g_data[7:0], word_exp[7:0], dname(g_code), dname(want),
(s == 0) ? "a correct read at address A" :
(s == 1) ? "32-bit address, 0 dummies -- same total, no displacement" :
(s == 2) ? "the same trade at address B -- the answer CHANGED" :
(s == 3) ? "command 0x0c: the device drove nothing" :
(s == 4) ? "the same bad command at B -- the answer did NOT change" :
(s == 5) ? "the MASTER inserts two dummy cycles too many" :
(s == 6) ? "the MASTER clocks eight extra DATA bits -- same verdict" :
(s == 7) ? "the DEVICE wants two dummy cycles more (late)" :
(s == 8) ? "the DEVICE wants two fewer (early)" :
"response 0x00 on a bus resting at 0 -- looks silent");
if (got_n - base != 1) begin
$display(" FAIL: stimulus %0d produced %0d diagnoses for one frame", s, got_n - base);
errors = errors + 1;
end
if (g_code !== want) begin
$display(" FAIL: stimulus %0d diagnosed %s where %s was expected",
s, dname(g_code), dname(want));
errors = errors + 1;
end
if (g_leads != w_leads[CNT_W-1:0]) begin
$display(" FAIL: stimulus %0d counted %0d leading edges where %0d were driven",
s, g_leads, w_leads);
errors = errors + 1;
end
if ((g_nd == 1) && (g_disp != w_disp)) begin
$display(" FAIL: stimulus %0d reported displacement %0d where %0d was expected",
s, g_disp, w_disp);
errors = errors + 1;
end
end
// ---- 1. the trade is invisible to both observations ----
if (!(leads_log[1] == leads_log[0] && disp_log[1] == disp_log[0] && nd_log[1] == 0)) begin
$display(" FAIL: the traded configuration differed from the correct one in an observation (%0d/%0d/%0d against %0d/%0d), so the chapter's central claim was not exercised",
leads_log[1], disp_log[1], nd_log[1], leads_log[0], disp_log[0]);
errors = errors + 1;
end
$display("");
$display(" 1. the traded configuration -- a 32-bit address with 0 dummy cycles against a 24-bit address with 8 -- produced %0d leading edges, exactly as the CORRECT read did, and left the data window undisplaced. An edge-count predicate is silent and a displacement search finds nothing to displace. Both of this module's observations agree with a good transfer, and the only thing that disagrees anywhere in the system is the byte itself",
leads_log[0]);
// ---- 2. the returned data is a VALID response, not corruption ----
if (data_log[1][7:0] !== dev_data({8'h00, ADDR_A[23:8]})) begin
$display(" FAIL: the traded read did not return the device's response for the wrongly-latched address (got %02h, the device would answer %02h)",
data_log[1][7:0], dev_data({8'h00, ADDR_A[23:8]}));
errors = errors + 1;
end
$display(" 2. the traded read returned %02h where %02h was expected -- and %02h is exactly what this device answers for address %06h, the value it latched when it took the first three of four address bytes. The byte is not corrupt; it is CORRECT for a different address. That is why it survives a plausibility check, why a firmware log looks reasonable, and why the failure gets attributed to the memory contents rather than to the transfer",
data_log[1][7:0], dev_data(ADDR_A), data_log[1][7:0], {8'h00, ADDR_A[23:8]});
// ---- 3. two captures separate what one cannot ----
if (data_log[1][7:0] === data_log[2][7:0]) begin
$display(" FAIL: the width fault returned the same byte at both addresses, so the pair of captures could not show that the address field is parsed");
errors = errors + 1;
end
if (data_log[3][7:0] !== data_log[4][7:0]) begin
$display(" FAIL: the unrecognised command returned different bytes at the two addresses (%02h, %02h), which it cannot do if the address was never parsed",
data_log[3][7:0], data_log[4][7:0]);
errors = errors + 1;
end
$display(" 3. the width fault answered %02h at address A and %02h at address B -- the response DEPENDS on the address, so the address field is being parsed and only its width is wrong. The unrecognised command answered %02h at both -- the response does not depend on the address at all, so the address was never parsed. Neither statement is available from one capture and both follow immediately from two, which makes `re-run it at a different address` the cheapest next measurement in this whole chapter",
data_log[1][7:0], data_log[2][7:0], data_log[3][7:0]);
// ---- 4. the total cannot localise a field, demonstrated by collision ----
if (code_log[5] !== code_log[6]) begin
$display(" FAIL: the two master-side faults produced different verdicts, so the claim that a total cannot localise a field was not exercised");
errors = errors + 1;
end
if (leads_log[5] == leads_log[6]) begin
$display(" FAIL: the two master-side faults produced the same total, so they were not actually different faults");
errors = errors + 1;
end
$display(" 4. two DIFFERENT master-side faults -- two extra dummy cycles, and eight extra data bits -- were both reported %s, with only the magnitude (%0d against %0d) to tell them apart. That is not a weakness of this decoder, it is arithmetic: three field lengths are summed into one total and a total cannot un-sum itself. The actionable part is still there -- the master's own clock accounting is wrong, so the fault is in the driver and not in the device -- and the field has to come from reading the driver's constants against the datasheet",
dname(code_log[5]), leads_log[5] - 48, leads_log[6] - 48);
// ---- 5. a correct answer that is indistinguishable from silence ----
if (code_log[9] !== code_log[3]) begin
$display(" FAIL: the 0x00 response and the ignored command produced different verdicts, so the claim that they are the same observation was not exercised");
errors = errors + 1;
end
if (nd_log[3] != 0) begin
$display(" FAIL: the ignored command matched %0d displacement(s) where 0 was expected", nd_log[3]);
errors = errors + 1;
end
if (nd_log[9] <= 1) begin
$display(" FAIL: the 0x00 response matched %0d displacement(s); the claim is that it matches every one", nd_log[9]);
errors = errors + 1;
end
$display(" 5. a device that IGNORED the command and a device that answered 0x00 on a bus resting at 0 produced the SAME verdict, %s, because they produce the same waveform: MISO never moves in either case. That is not a decoder limitation, it is what the wire carries. The disambiguator is the displacement count -- %0d for the ignored command, meaning the expected data is nowhere in the window, against %0d for the 0x00 response, meaning it is everywhere. So the verdict names an observation and a second number says which of two physical situations produced it, which is the honest decomposition. The procedural consequence is a rule about debugging rather than about design: never issue a debug read against an address whose contents equal the bus's resting level. Chapter 18.3 rejected the same two payloads for an unrelated reason -- invariance under permutation rather than invisibility against the idle bus",
dname(code_log[9]), nd_log[3], nd_log[9]);
// ---- BENCH INTEGRITY ----
// Two deliberately wrong expectations, compared by the same operator as the real ones.
if (code_log[1] !== D_OK) mutations = mutations + 1;
if (leads_log[5] != 48) mutations = mutations + 1;
if (mutations != 2) begin
$display(" FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
errors = errors + 1;
end
if (x_reports != 0) begin
$display(" FAIL: %0d reported fields carried X", x_reports);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: two deliberately wrong expectations mismatched, and every reported field carried a known value");
$display("PASS: several field-length faults share one symptom, and TWO numbers separate four of them -- the total leading-edge count against the intended configuration, and the DISPLACEMENT of the expected data inside the intended data window. A master whose clock accounting is wrong moves the total and not the displacement; a device wanting more or fewer dummy cycles moves the displacement and not the total, and its SIGN names the direction of the register somebody has to write. The displacement is used rather than the launch instant for a reason worth keeping: MISO's first visible change is only an UPPER BOUND on the launch, because a device whose first bit matches the resting bus produces no transition -- and the earlier version of this module, which timed that transition, reported a device-side dummy fault for a master-side width fault. A confident answer naming the wrong device. The fault that defeats both numbers is the trade: 32 address bits against 0 dummy cycles gives the same total (%0d) and no displacement, so every timing observation agrees with a correct read and the data comes back as %02h -- not corrupt but CORRECT for the address the device actually latched. Two captures settle it where one cannot, because the width fault's answer CHANGED with the address and the unrecognised command's did not. And two limits are measured rather than claimed: a total cannot say WHICH of three summed fields is wrong, shown by two different master-side faults colliding on one verdict; and a response consisting entirely of the resting level matches every displacement, so the module declines instead of picking one",
leads_log[0], data_log[1][7:0]);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
endmodule// spi_len_diag_tb.v
//
// TEN CAPTURES OF A FLASH READ, AND THE ONE FAULT THAT TWO NUMBERS CANNOT REACH.
//
// THE DEVICE IS MODELLED IN THE BENCH, deliberately. The chapter's artefact is the INSTRUMENT, and a
// diagnostic tested against a device model that shares its assumptions has been tested against itself.
// So the slave here is written from the datasheet's description -- latch a command, latch three address
// bytes, wait its OWN dummy count, then drive -- and it never reads the decoder's configuration inputs.
// The two disagree about the transfer's shape on seven of ten frames, and that disagreement is the
// measurement.
//
// THE FIVE RESULTS.
//
// 1. THE TRADE IS INVISIBLE TO BOTH OBSERVATIONS. A 32-bit address with 0 dummy cycles and a 24-bit
// address with 8 produce the same total edge count AND leave the data undisplaced. The bench
// requires both numbers to EQUAL the correct frame's, so the claim is measured, not asserted.
//
// 2. THE DATA IT RETURNS IS VALID, NOT CORRUPT. The device latched a different address and answered
// correctly for it. The bench checks the byte against the device's response function evaluated at
// the WRONGLY LATCHED address, so "this looks like real data" is a computed fact.
//
// 3. TWO CAPTURES SEPARATE WHAT ONE CANNOT. Every fault is run at two addresses. A width fault
// changes its answer with the address; an unrecognised command does not. That is the difference
// between a field parsed at the wrong width and a field never parsed, and it is a property of a
// PAIR of captures rather than of either one.
//
// 4. THE TOTAL CANNOT LOCALISE A FIELD, AND THE BENCH PROVES IT BY COLLISION. A master inserting two
// extra dummy cycles and a master reading eight extra data bits are different faults in different
// driver constants, and both are reported as MASTER_LEN with only the magnitude to tell them
// apart. Three field lengths feed one total; a total cannot un-sum itself.
//
// 5. A CORRECT ANSWER CAN BE INDISTINGUISHABLE FROM SILENCE. The last frame reads an address whose
// response is 0x00 on a bus resting at 0, so MISO never moves -- exactly the waveform an ignored
// command produces. The bench requires the two to receive the SAME verdict and then separates them
// on the displacement count: zero matches means the device was silent, a full set means it may have
// answered perfectly. Chapter 18.3 rejected the same payload for an unrelated reason -- invariance
// under permutation rather than invisibility against the idle bus.
`timescale 1ns/1ps
module spi_len_diag_tb;
localparam DW = 32;
localparam CNT_W = 8;
localparam LEAD = 3;
localparam HALF = 2;
localparam LAG = 2;
localparam GAP = 4;
localparam [2:0] D_OK = 3'd0, D_MASTER_LEN = 3'd1, D_DEVICE_PHASE = 3'd2,
D_FIELD_WIDTH = 3'd3, D_BOTH = 3'd4, D_QUIET = 3'd5;
// The transfer the datasheet describes.
localparam [CNT_W-1:0] CMD_W = 8'd8, ADDR_W = 8'd24, DUMMY_N = 8'd8, DATA_W = 8'd8;
localparam [7:0] CMD_READ = 8'h0B;
// Two addresses, chosen so that the device's response is measurable against a pull-down (top bit
// of the expected byte is 1) and so that the wrongly-latched addresses differ from each other --
// without which result 3 could not be demonstrated.
localparam [23:0] ADDR_A = 24'h1234F1; // response 0x8d
localparam [23:0] ADDR_B = 24'h5678B7; // response 0xc3
// And one whose response is 0x00 -- the payload that cannot locate itself on a bus resting at 0.
localparam [23:0] ADDR_C = 24'h12347C; // response 0x00
reg clk;
always #5 clk = ~clk;
reg rst_n;
reg cpol, cpha;
reg [DW-1:0] word_exp;
reg b_sclk, b_cs_n, b_mosi, b_miso;
wire dg_valid, ob_moved;
wire [2:0] dg_code;
wire [CNT_W-1:0] ob_leads, ob_first_move;
wire signed [CNT_W:0] ob_len_err, ob_disp;
wire [3:0] ob_ndisp;
wire [DW-1:0] ob_cmd, ob_data;
spi_len_diag #(.DW(DW), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi), .miso(b_miso),
.cpol(cpol), .cpha(cpha),
.cmd_w(CMD_W), .addr_w(ADDR_W), .dummy_n(DUMMY_N), .data_w(DATA_W),
.word_exp(word_exp),
.dg_valid(dg_valid), .dg_code(dg_code),
.ob_leads(ob_leads), .ob_len_err(ob_len_err), .ob_moved(ob_moved),
.ob_first_move(ob_first_move), .ob_disp(ob_disp), .ob_ndisp(ob_ndisp),
.ob_cmd(ob_cmd), .ob_data(ob_data)
);
integer errors, x_reports, got_n;
reg [2:0] g_code;
reg [CNT_W-1:0] g_leads, g_fmove;
reg signed [CNT_W:0] g_len, g_disp;
reg [3:0] g_nd;
reg [DW-1:0] g_data, g_cmd;
reg g_moved;
always @(posedge clk) if (dg_valid) begin
got_n = got_n + 1;
g_code = dg_code; g_leads = ob_leads; g_fmove = ob_first_move;
g_len = ob_len_err; g_disp = ob_disp; g_nd = ob_ndisp;
g_data = ob_data; g_cmd = ob_cmd; g_moved = ob_moved;
if ((^dg_code === 1'bx) || (^ob_leads === 1'bx) || (^ob_first_move === 1'bx)
|| (^ob_data[7:0] === 1'bx) || (^ob_cmd[7:0] === 1'bx) || (^ob_ndisp === 1'bx)
|| (ob_moved === 1'bx) || (^ob_len_err === 1'bx) || (^ob_disp === 1'bx))
x_reports = x_reports + 1;
end
// THE DEVICE'S RESPONSE FUNCTION, written from its description. A response that mixes all three
// address bytes is what makes a mis-latched address produce a DIFFERENT but equally valid byte --
// which is the whole reason a width fault does not look like corruption.
function [7:0] dev_data;
input [23:0] a;
begin dev_data = (a[7:0] ^ a[15:8] ^ a[23:16]) ^ 8'h5A; end
endfunction
function [8*14:1] dname;
input [2:0] c;
begin
case (c)
D_OK: dname = "OK ";
D_MASTER_LEN: dname = "MASTER_LEN ";
D_DEVICE_PHASE: dname = "DEVICE_PHASE ";
D_FIELD_WIDTH: dname = "FIELD_WIDTH ";
D_BOTH: dname = "BOTH ";
default: dname = "QUIET ";
endcase
end
endfunction
task idle_n;
input integer n;
integer i;
begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
endtask
// ---- one flash read ----
//
// cmd the command byte the master issues
// addr the address, as a 32-bit value
// m_addr_w how many address bits the MASTER sends (24 or 32)
// m_dummy how many dummy cycles the MASTER inserts
// m_data_w how many data bits the MASTER clocks out
// dev_dummy how many dummy cycles the DEVICE waits for
// rest_lvl what MISO rests at while nobody drives it -- a pull resistor, not a signal
//
// The device model is inline and reads only `cmd`, the first 24 address bits and `dev_dummy`, so it
// cannot inherit the master's misconfiguration. That separation is the whole reason the bench is
// evidence about the decoder rather than a restatement of it.
task flash_read;
input [7:0] cmd;
input [31:0] addr;
input integer m_addr_w;
input integer m_dummy;
input integer m_data_w;
input integer dev_dummy;
input rest_lvl;
integer k, total, dev_launch;
reg [7:0] payload;
reg [23:0] dev_addr;
begin
// What the device latches: the FIRST 24 address bits the master sends. With a 32-bit master
// and a 24-bit device that is the top three bytes of four -- the address shifted by a byte,
// which is why the returned data is valid and wrong.
if (m_addr_w == 32) dev_addr = addr[31:8];
else dev_addr = addr[23:0];
payload = dev_data(dev_addr);
dev_launch = 8 + 24 + dev_dummy;
total = 8 + m_addr_w + m_dummy + m_data_w;
b_sclk = cpol; b_mosi = 1'b0; b_miso = rest_lvl; b_cs_n = 1'b1;
idle_n(2);
b_cs_n = 1'b0;
idle_n(1);
b_mosi = cmd[7];
idle_n(LEAD - 1);
for (k = 0; k < total; k = k + 1) begin
b_sclk = ~b_sclk; // leading edge, index k
idle_n(HALF);
b_sclk = ~b_sclk; // trailing edge -- both ends move here
if (k + 1 < 8)
b_mosi = cmd[7 - (k+1)];
else if ((k + 1 >= 8) && (k + 1 < 8 + m_addr_w))
b_mosi = addr[m_addr_w - 1 - (k + 1 - 8)];
else
b_mosi = 1'b0;
// MISO: the device drives only if it recognised the command, and only from its OWN
// launch instant. Before and after that the bus rests where the resistor puts it.
if ((cmd == CMD_READ) && (k + 1 >= dev_launch) && (k + 1 < dev_launch + 8))
b_miso = payload[7 - (k + 1 - dev_launch)];
else
b_miso = rest_lvl;
idle_n(HALF);
end
idle_n(LAG);
b_cs_n = 1'b1;
idle_n(1);
b_sclk = cpol;
b_miso = rest_lvl;
idle_n(GAP);
end
endtask
integer s, base;
reg [2:0] code_log [0:9];
reg [DW-1:0] data_log [0:9];
integer leads_log[0:9], disp_log[0:9], nd_log[0:9];
reg [2:0] want;
integer w_leads, w_disp, mutations;
initial begin
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(" leads len_err disp nd moved 1st cmd data exp verdict expected stimulus");
for (s = 0; s < 10; s = s + 1) begin
@(negedge clk);
b_cs_n = 1'b1; b_sclk = cpol; b_mosi = 1'b0;
idle_n(2); rst_n = 1'b0; idle_n(3); rst_n = 1'b1; idle_n(2);
base = got_n;
word_exp = {24'b0, dev_data(ADDR_A)};
case (s)
// A correct read at address A. A decoder never shown clean traffic has not been shown
// to be silent.
0: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 8, 8, 8, 1'b0);
want = D_OK; w_leads = 48; w_disp = 0; end
// THE TRADE at address A: 32 address bits, 0 dummy cycles. Same total, no displacement.
1: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 32, 0, 8, 8, 1'b0);
want = D_FIELD_WIDTH; w_leads = 48; w_disp = 0; end
// THE TRADE at address B: the answer CHANGES with the address, so the address field is
// being parsed -- at the wrong width.
2: begin word_exp = {24'b0, dev_data(ADDR_B)};
flash_read(CMD_READ, {8'h00, ADDR_B}, 32, 0, 8, 8, 1'b0);
want = D_FIELD_WIDTH; w_leads = 48; w_disp = 0; end
// An unrecognised command at address A: nothing is ever driven.
3: begin flash_read(8'h0C, {8'h00, ADDR_A}, 24, 8, 8, 8, 1'b0);
want = D_QUIET; w_leads = 48; w_disp = 0; end
// The same unrecognised command at address B: the answer does NOT change, so the
// address was never parsed at all.
4: begin word_exp = {24'b0, dev_data(ADDR_B)};
flash_read(8'h0C, {8'h00, ADDR_B}, 24, 8, 8, 8, 1'b0);
want = D_QUIET; w_leads = 48; w_disp = 0; end
// The MASTER inserts two dummy cycles too many. The total moves; the data does not.
5: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 10, 8, 8, 1'b0);
want = D_MASTER_LEN; w_leads = 50; w_disp = 0; end
// The MASTER clocks eight extra DATA bits. A different constant in a different line of
// the driver, and the SAME verdict -- because a total cannot un-sum itself.
6: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 8, 16, 8, 1'b0);
want = D_MASTER_LEN; w_leads = 56; w_disp = 0; end
// The DEVICE wants two more dummy cycles than it is given: the data is present and LATE.
7: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 8, 8, 10, 1'b0);
want = D_DEVICE_PHASE; w_leads = 48; w_disp = 2; end
// The DEVICE wants two fewer: present and EARLY. The sign of one number names the
// direction of a register somebody has to write.
8: begin flash_read(CMD_READ, {8'h00, ADDR_A}, 24, 8, 8, 6, 1'b0);
want = D_DEVICE_PHASE; w_leads = 48; w_disp = -2; end
// A CORRECT ANSWER THAT LOOKS LIKE SILENCE. Address C answers 0x00 on a bus resting at
// 0, so MISO never moves -- the same waveform an ignored command produces. The verdict
// is therefore the SAME code as stimuli 3 and 4, and the disambiguator is `ob_ndisp`.
9: begin word_exp = {24'b0, dev_data(ADDR_C)};
flash_read(CMD_READ, {8'h00, ADDR_C}, 24, 8, 8, 10, 1'b0);
want = D_QUIET; w_leads = 48; w_disp = 0; end
endcase
code_log[s] = g_code;
data_log[s] = g_data;
leads_log[s] = g_leads;
disp_log[s] = g_disp;
nd_log[s] = g_nd;
$display(" %5d %7d %4d %2d %5b %3d %02h %02h %02h %0s %0s %0s",
g_leads, g_len, g_disp, g_nd, g_moved, g_fmove,
g_cmd[7:0], g_data[7:0], word_exp[7:0], dname(g_code), dname(want),
(s == 0) ? "a correct read at address A" :
(s == 1) ? "32-bit address, 0 dummies -- same total, no displacement" :
(s == 2) ? "the same trade at address B -- the answer CHANGED" :
(s == 3) ? "command 0x0c: the device drove nothing" :
(s == 4) ? "the same bad command at B -- the answer did NOT change" :
(s == 5) ? "the MASTER inserts two dummy cycles too many" :
(s == 6) ? "the MASTER clocks eight extra DATA bits -- same verdict" :
(s == 7) ? "the DEVICE wants two dummy cycles more (late)" :
(s == 8) ? "the DEVICE wants two fewer (early)" :
"response 0x00 on a bus resting at 0 -- looks silent");
if (got_n - base != 1) begin
$display(" FAIL: stimulus %0d produced %0d diagnoses for one frame", s, got_n - base);
errors = errors + 1;
end
if (g_code !== want) begin
$display(" FAIL: stimulus %0d diagnosed %0s where %0s was expected",
s, dname(g_code), dname(want));
errors = errors + 1;
end
if (g_leads != w_leads[CNT_W-1:0]) begin
$display(" FAIL: stimulus %0d counted %0d leading edges where %0d were driven",
s, g_leads, w_leads);
errors = errors + 1;
end
if ((g_nd == 1) && (g_disp != w_disp)) begin
$display(" FAIL: stimulus %0d reported displacement %0d where %0d was expected",
s, g_disp, w_disp);
errors = errors + 1;
end
end
// ---- 1. the trade is invisible to both observations ----
if (!(leads_log[1] == leads_log[0] && disp_log[1] == disp_log[0] && nd_log[1] == 0)) begin
$display(" FAIL: the traded configuration differed from the correct one in an observation (%0d/%0d/%0d against %0d/%0d), so the chapter's central claim was not exercised",
leads_log[1], disp_log[1], nd_log[1], leads_log[0], disp_log[0]);
errors = errors + 1;
end
$display("");
$display(" 1. the traded configuration -- a 32-bit address with 0 dummy cycles against a 24-bit address with 8 -- produced %0d leading edges, exactly as the CORRECT read did, and left the data window undisplaced. An edge-count predicate is silent and a displacement search finds nothing to displace. Both of this module's observations agree with a good transfer, and the only thing that disagrees anywhere in the system is the byte itself",
leads_log[0]);
// ---- 2. the returned data is a VALID response, not corruption ----
if (data_log[1][7:0] !== dev_data({8'h00, ADDR_A[23:8]})) begin
$display(" FAIL: the traded read did not return the device's response for the wrongly-latched address (got %02h, the device would answer %02h)",
data_log[1][7:0], dev_data({8'h00, ADDR_A[23:8]}));
errors = errors + 1;
end
$display(" 2. the traded read returned %02h where %02h was expected -- and %02h is exactly what this device answers for address %06h, the value it latched when it took the first three of four address bytes. The byte is not corrupt; it is CORRECT for a different address. That is why it survives a plausibility check, why a firmware log looks reasonable, and why the failure gets attributed to the memory contents rather than to the transfer",
data_log[1][7:0], dev_data(ADDR_A), data_log[1][7:0], {8'h00, ADDR_A[23:8]});
// ---- 3. two captures separate what one cannot ----
if (data_log[1][7:0] === data_log[2][7:0]) begin
$display(" FAIL: the width fault returned the same byte at both addresses, so the pair of captures could not show that the address field is parsed");
errors = errors + 1;
end
if (data_log[3][7:0] !== data_log[4][7:0]) begin
$display(" FAIL: the unrecognised command returned different bytes at the two addresses (%02h, %02h), which it cannot do if the address was never parsed",
data_log[3][7:0], data_log[4][7:0]);
errors = errors + 1;
end
$display(" 3. the width fault answered %02h at address A and %02h at address B -- the response DEPENDS on the address, so the address field is being parsed and only its width is wrong. The unrecognised command answered %02h at both -- the response does not depend on the address at all, so the address was never parsed. Neither statement is available from one capture and both follow immediately from two, which makes `re-run it at a different address` the cheapest next measurement in this whole chapter",
data_log[1][7:0], data_log[2][7:0], data_log[3][7:0]);
// ---- 4. the total cannot localise a field, demonstrated by collision ----
if (code_log[5] !== code_log[6]) begin
$display(" FAIL: the two master-side faults produced different verdicts, so the claim that a total cannot localise a field was not exercised");
errors = errors + 1;
end
if (leads_log[5] == leads_log[6]) begin
$display(" FAIL: the two master-side faults produced the same total, so they were not actually different faults");
errors = errors + 1;
end
$display(" 4. two DIFFERENT master-side faults -- two extra dummy cycles, and eight extra data bits -- were both reported %0s, with only the magnitude (%0d against %0d) to tell them apart. That is not a weakness of this decoder, it is arithmetic: three field lengths are summed into one total and a total cannot un-sum itself. The actionable part is still there -- the master's own clock accounting is wrong, so the fault is in the driver and not in the device -- and the field has to come from reading the driver's constants against the datasheet",
dname(code_log[5]), leads_log[5] - 48, leads_log[6] - 48);
// ---- 5. a correct answer that is indistinguishable from silence ----
if (code_log[9] !== code_log[3]) begin
$display(" FAIL: the 0x00 response and the ignored command produced different verdicts, so the claim that they are the same observation was not exercised");
errors = errors + 1;
end
if (nd_log[3] != 0) begin
$display(" FAIL: the ignored command matched %0d displacement(s) where 0 was expected", nd_log[3]);
errors = errors + 1;
end
if (nd_log[9] <= 1) begin
$display(" FAIL: the 0x00 response matched %0d displacement(s); the claim is that it matches every one", nd_log[9]);
errors = errors + 1;
end
$display(" 5. a device that IGNORED the command and a device that answered 0x00 on a bus resting at 0 produced the SAME verdict, %0s, because they produce the same waveform: MISO never moves in either case. That is not a decoder limitation, it is what the wire carries. The disambiguator is the displacement count -- %0d for the ignored command, meaning the expected data is nowhere in the window, against %0d for the 0x00 response, meaning it is everywhere. So the verdict names an observation and a second number says which of two physical situations produced it, which is the honest decomposition. The procedural consequence is a rule about debugging rather than about design: never issue a debug read against an address whose contents equal the bus's resting level. Chapter 18.3 rejected the same two payloads for an unrelated reason -- invariance under permutation rather than invisibility against the idle bus",
dname(code_log[9]), nd_log[3], nd_log[9]);
// ---- BENCH INTEGRITY ----
// Two deliberately wrong expectations, compared by the same operator as the real ones.
if (code_log[1] !== D_OK) mutations = mutations + 1;
if (leads_log[5] != 48) mutations = mutations + 1;
if (mutations != 2) begin
$display(" FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
errors = errors + 1;
end
if (x_reports != 0) begin
$display(" FAIL: %0d reported fields carried X", x_reports);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: two deliberately wrong expectations mismatched, and every reported field carried a known value");
$display("PASS: several field-length faults share one symptom, and TWO numbers separate four of them -- the total leading-edge count against the intended configuration, and the DISPLACEMENT of the expected data inside the intended data window. A master whose clock accounting is wrong moves the total and not the displacement; a device wanting more or fewer dummy cycles moves the displacement and not the total, and its SIGN names the direction of the register somebody has to write. The displacement is used rather than the launch instant for a reason worth keeping: MISO's first visible change is only an UPPER BOUND on the launch, because a device whose first bit matches the resting bus produces no transition -- and the earlier version of this module, which timed that transition, reported a device-side dummy fault for a master-side width fault. A confident answer naming the wrong device. The fault that defeats both numbers is the trade: 32 address bits against 0 dummy cycles gives the same total (%0d) and no displacement, so every timing observation agrees with a correct read and the data comes back as %02h -- not corrupt but CORRECT for the address the device actually latched. Two captures settle it where one cannot, because the width fault's answer CHANGED with the address and the unrecognised command's did not. And two limits are measured rather than claimed: a total cannot say WHICH of three summed fields is wrong, shown by two different master-side faults colliding on one verdict; and a response consisting entirely of the resting level matches every displacement, so the module declines instead of picking one",
leads_log[0], data_log[1][7:0]);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
initial begin
cpol = 1'b0;
cpha = 1'b0;
b_sclk = 1'b0;
b_cs_n = 1'b1;
b_mosi = 1'b0;
b_miso = 1'b0;
errors = 0;
x_reports = 0;
got_n = 0;
clk = 1'b0;
rst_n = 1'b1;
word_exp = {DW{1'b0}};
end
endmodule-- spi_len_diag_tb.vhd
--
-- TEN CAPTURES OF A FLASH READ, AND THE ONE FAULT THAT TWO NUMBERS CANNOT REACH.
--
-- THE DEVICE IS MODELLED IN THE BENCH, deliberately. The chapter's artefact is the INSTRUMENT, and a
-- diagnostic tested against a device model that shares its assumptions has been tested against itself.
-- The slave procedure below is written from the datasheet's description -- latch a command, latch three
-- address bytes, wait its OWN dummy count, then drive -- and it never reads the decoder's configuration
-- inputs. The two disagree about the transfer's shape on seven of ten frames, and that disagreement is
-- the measurement.
--
-- The five results are the ones the other two languages report, in the same order and with the same
-- numbers. Running the argument a third time is not ceremony: the VHDL port of Chapter 16.5 found a
-- defect two Verilog suites had agreed on, and three independent spellings producing one table is the
-- only evidence available that the table is a property of the reasoning rather than of one scheduler.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `LEAD_C`, `HALF_C`, `LAG_C`, `GAP_C` and `CMD_READ_C`
-- carry suffixes so that no signal, variable or subprogram argument can shadow them in another case.
-- The procedure arguments are `m_addr_w`, `m_dummy`, `m_data_w`, `dev_dummy` -- prefixed by which side
-- of the link owns them, which is also what makes the stimulus table readable.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.spi_len_pkg.all;
entity spi_len_diag_tb is
end entity spi_len_diag_tb;
architecture tb of spi_len_diag_tb is
constant LEAD_C : natural := 3;
constant HALF_C : natural := 2;
constant LAG_C : natural := 2;
constant GAP_C : natural := 4;
constant CNT_W : positive := 8;
constant CMD_READ_C : std_logic_vector(7 downto 0) := x"0B";
-- Two addresses chosen so the device's response is non-trivial and so the WRONGLY LATCHED addresses
-- differ from each other -- without which result 3 could not be demonstrated. And one whose
-- response is 0x00, which on a bus resting at 0 is invisible.
constant ADDR_A : std_logic_vector(23 downto 0) := x"1234F1"; -- answers 0x8d
constant ADDR_B : std_logic_vector(23 downto 0) := x"5678B7"; -- answers 0xc3
constant ADDR_C : std_logic_vector(23 downto 0) := x"12347C"; -- answers 0x00
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 b_sclk : std_logic := '0';
signal b_cs_n : std_logic := '1';
signal b_mosi : std_logic := '0';
signal b_miso : std_logic := '0';
signal dg_valid : std_logic;
signal dg_code : len_diag_t;
signal ob_leads, ob_first_move, ob_ndisp : natural;
signal ob_len_err, ob_disp : integer;
signal ob_moved : boolean;
signal ob_cmd, ob_data : std_logic_vector(DW_C - 1 downto 0);
-- One driver each, so nothing here can resolve to 'X' through two writers.
signal g_code : len_diag_t := D_OK;
signal g_leads, g_fmove, g_nd : natural := 0;
signal g_len, g_disp : integer := 0;
signal g_moved : boolean := false;
signal g_cmd, g_data : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
signal g_n, g_x : natural := 0;
type nat_arr is array (natural range <>) of natural;
type int_arr is array (natural range <>) of integer;
type dg_arr is array (natural range <>) of len_diag_t;
type word_arr is array (natural range <>) of std_logic_vector(DW_C - 1 downto 0);
-- Right-justify an integer. `%-30s` is not portable across the three simulators -- one pads and one
-- does not -- which is why every free-text column in this corpus sits at the END of a row and every
-- numeric column is justified explicitly here.
--
-- THE LENGTH IS TAKEN FROM THE IMAGE, not assumed. The first version of this helper concatenated a
-- fixed run of spaces onto `integer'image(v)` and declared the result a 24-character constant, which
-- is a FATAL length mismatch the moment the value needs two digits -- and it survived two chapters
-- only because every number they printed was a single digit. A constant whose length depends on its
-- initialiser must be left unconstrained.
function i2s (v : integer; w : natural) return string is
constant S : string := integer'image(v);
constant P : string(1 to 40) := (others => ' ');
begin
if S'length >= w then return S; end if;
return P(1 to w - S'length) & S;
end function i2s;
function b2s (b : boolean; w : natural) return string is
constant P : string(1 to 24) := (others => ' ');
begin
if b then return P(1 to w - 1) & "1"; else return P(1 to w - 1) & "0"; end if;
end function b2s;
-- THE FORMAL IS CONSTRAINED, and it has to be. An unconstrained formal inherits the actual's index
-- range, and a CONCATENATION produces an ascending `0 to n-1` range -- so `v(7 downto 0)` inside a
-- function called with `x"00" & something` is a null slice and an index fault at run time, not a
-- compile error. Writing the range into the declaration makes the indexing a property of this
-- function rather than of how each caller spelled its argument.
subtype byte_t is std_logic_vector(7 downto 0);
function hex8 (v : byte_t) return string is
constant D : string := "0123456789abcdef";
variable r : string(1 to 2);
variable u : natural := to_integer(unsigned(v));
begin
r(1) := D(u / 16 + 1);
r(2) := D(u mod 16 + 1);
return r;
end function hex8;
function hex24 (v : std_logic_vector(23 downto 0)) return string is
begin
return hex8(v(23 downto 16)) & hex8(v(15 downto 8)) & hex8(v(7 downto 0));
end function hex24;
-- THE DEVICE'S RESPONSE FUNCTION, from its description. Mixing all three address bytes is what makes
-- a mis-latched address produce a DIFFERENT but equally valid byte, which is the whole reason a
-- width fault does not look like corruption.
-- THE RETURN TYPE IS A CONSTRAINED SUBTYPE, and the unconstrained version of this function is a
-- run-time fault waiting for a caller.
--
-- The index range of an array produced by a logical operator is not guaranteed to be the left
-- operand's `downto` range -- a simulator may normalise it to an ascending `1 to 8`. An
-- unconstrained `return std_logic_vector` then hands the caller a vector whose `(7 downto 0)` slice
-- is a null range, and the failure appears as an index fault inside an unrelated formatting helper
-- two hundred lines away. Assigning through a variable of a constrained subtype normalises the
-- range once, here, where the reason is visible.
function dev_data (a : std_logic_vector(23 downto 0)) return byte_t is
variable r : byte_t;
begin
r := (a(7 downto 0) xor a(15 downto 8) xor a(23 downto 16)) xor x"5A";
return r;
end function dev_data;
function stim_text (s : natural) return string is
begin
case s is
when 0 => return "a correct read at address A";
when 1 => return "32-bit address, 0 dummies -- same total, no displacement";
when 2 => return "the same trade at address B -- the answer CHANGED";
when 3 => return "command 0x0c: the device drove nothing";
when 4 => return "the same bad command at B -- the answer did NOT change";
when 5 => return "the MASTER inserts two dummy cycles too many";
when 6 => return "the MASTER clocks eight extra DATA bits -- same verdict";
when 7 => return "the DEVICE wants two dummy cycles more (late)";
when 8 => return "the DEVICE wants two fewer (early)";
when others => return "response 0x00 on a bus resting at 0 -- looks silent";
end case;
end function stim_text;
begin
clk_gen : process is
begin
while run loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process clk_gen;
dut : entity work.spi_len_diag
generic map (CNT_W => CNT_W)
port map (
clk => clk, rst_n => rst_n,
sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi, miso => b_miso,
cpol => cpol, cpha => cpha,
cmd_w => to_unsigned(8, CNT_W),
addr_w => to_unsigned(24, CNT_W),
dummy_n => to_unsigned(8, CNT_W),
data_w => to_unsigned(8, CNT_W),
word_exp => word_exp,
dg_valid => dg_valid, dg_code => dg_code,
ob_leads => ob_leads, ob_len_err => ob_len_err, ob_moved => ob_moved,
ob_first_move => ob_first_move, ob_disp => ob_disp, ob_ndisp => ob_ndisp,
ob_cmd => ob_cmd, ob_data => ob_data
);
cap : process (clk) is
begin
if rising_edge(clk) then
if dg_valid = '1' then
g_code <= dg_code; g_leads <= ob_leads; g_fmove <= ob_first_move;
g_len <= ob_len_err; g_disp <= ob_disp; g_nd <= ob_ndisp;
g_data <= ob_data; g_cmd <= ob_cmd; g_moved <= ob_moved;
g_n <= g_n + 1;
-- The enumeration, the naturals, the integers and the boolean cannot hold a metavalue,
-- so the two vectors are the only fields guarded -- and they are the only ones guarded.
for i in 0 to 7 loop
if ob_data(i) /= '0' and ob_data(i) /= '1' then g_x <= g_x + 1; end if;
if ob_cmd(i) /= '0' and ob_cmd(i) /= '1' then g_x <= g_x + 1; end if;
end loop;
end if;
end if;
end process cap;
stim : process is
procedure idle_n (n : natural) is
begin
for i in 1 to n loop
wait until falling_edge(clk);
end loop;
end procedure idle_n;
-- One flash read. Every formal is CONSTRAINED, because an unconstrained formal inherits its
-- index range from the actual and a bit-string literal carries an ASCENDING `0 to n` range
-- rather than the `n downto 0` a reader assumes.
procedure flash_read (cmd : std_logic_vector(7 downto 0);
addr : std_logic_vector(31 downto 0);
m_addr_w : natural;
m_dummy : natural;
m_data_w : natural;
dev_dummy : natural;
rest_lvl : std_logic) is
variable total, dev_launch : natural;
variable payload : std_logic_vector(7 downto 0);
variable dev_addr : std_logic_vector(23 downto 0);
begin
-- What the device latches: the FIRST 24 address bits the master sends. With a 32-bit master
-- and a 24-bit device that is the top three bytes of four -- the address shifted by a byte,
-- which is why the returned data is valid and wrong.
if m_addr_w = 32 then dev_addr := addr(31 downto 8);
else dev_addr := addr(23 downto 0);
end if;
payload := dev_data(dev_addr);
dev_launch := 8 + 24 + dev_dummy;
total := 8 + m_addr_w + m_dummy + m_data_w;
b_sclk <= cpol; b_mosi <= '0'; b_miso <= rest_lvl; b_cs_n <= '1';
idle_n(2);
b_cs_n <= '0';
idle_n(1);
b_mosi <= cmd(7);
idle_n(LEAD_C - 1);
for k in 0 to total - 1 loop
b_sclk <= not b_sclk; -- leading edge, index k
idle_n(HALF_C);
b_sclk <= not b_sclk; -- trailing edge -- both ends move here
if k + 1 < 8 then
b_mosi <= cmd(7 - (k + 1));
elsif k + 1 >= 8 and k + 1 < 8 + m_addr_w then
b_mosi <= addr(m_addr_w - 1 - (k + 1 - 8));
else
b_mosi <= '0';
end if;
-- MISO: the device drives only if it recognised the command, and only from its OWN
-- launch instant. Before and after, the bus rests where the resistor puts it.
if cmd = CMD_READ_C and k + 1 >= dev_launch and k + 1 < dev_launch + 8 then
b_miso <= payload(7 - (k + 1 - dev_launch));
else
b_miso <= rest_lvl;
end if;
idle_n(HALF_C);
end loop;
idle_n(LAG_C);
b_cs_n <= '1';
idle_n(1);
b_sclk <= cpol;
b_miso <= rest_lvl;
idle_n(GAP_C);
end procedure flash_read;
variable code_log : dg_arr(0 to 9);
variable data_log : word_arr(0 to 9);
variable leads_log : nat_arr(0 to 9);
variable nd_log : nat_arr(0 to 9);
variable disp_log : int_arr(0 to 9);
variable want : len_diag_t;
variable w_leads : natural;
variable w_disp : integer;
variable mutations, e, base : natural := 0;
variable ln : line;
begin
rst_n <= '1';
wait until falling_edge(clk);
rst_n <= '0';
idle_n(4);
rst_n <= '1';
idle_n(4);
write(ln, string'(" leads len_err disp nd moved 1st cmd data exp verdict expected stimulus"));
writeline(output, ln);
for s in 0 to 9 loop
wait until falling_edge(clk);
b_cs_n <= '1'; b_sclk <= cpol; b_mosi <= '0';
idle_n(2); rst_n <= '0'; idle_n(3); rst_n <= '1'; idle_n(2);
base := g_n;
word_exp <= x"000000" & dev_data(ADDR_A);
w_leads := 48; w_disp := 0;
wait for 1 ns;
case s is
when 0 =>
flash_read(CMD_READ_C, x"00" & ADDR_A, 24, 8, 8, 8, '0');
want := D_OK;
when 1 =>
flash_read(CMD_READ_C, x"00" & ADDR_A, 32, 0, 8, 8, '0');
want := D_FIELD_WIDTH;
when 2 =>
word_exp <= x"000000" & dev_data(ADDR_B); wait for 1 ns;
flash_read(CMD_READ_C, x"00" & ADDR_B, 32, 0, 8, 8, '0');
want := D_FIELD_WIDTH;
when 3 =>
flash_read(x"0C", x"00" & ADDR_A, 24, 8, 8, 8, '0');
want := D_QUIET;
when 4 =>
word_exp <= x"000000" & dev_data(ADDR_B); wait for 1 ns;
flash_read(x"0C", x"00" & ADDR_B, 24, 8, 8, 8, '0');
want := D_QUIET;
when 5 =>
flash_read(CMD_READ_C, x"00" & ADDR_A, 24, 10, 8, 8, '0');
want := D_MASTER_LEN; w_leads := 50;
when 6 =>
flash_read(CMD_READ_C, x"00" & ADDR_A, 24, 8, 16, 8, '0');
want := D_MASTER_LEN; w_leads := 56;
when 7 =>
flash_read(CMD_READ_C, x"00" & ADDR_A, 24, 8, 8, 10, '0');
want := D_DEVICE_PHASE; w_disp := 2;
when 8 =>
flash_read(CMD_READ_C, x"00" & ADDR_A, 24, 8, 8, 6, '0');
want := D_DEVICE_PHASE; w_disp := -2;
when others =>
word_exp <= x"000000" & dev_data(ADDR_C); wait for 1 ns;
flash_read(CMD_READ_C, x"00" & ADDR_C, 24, 8, 8, 10, '0');
want := D_QUIET;
end case;
code_log(s) := g_code;
data_log(s) := g_data;
leads_log(s) := g_leads;
disp_log(s) := g_disp;
nd_log(s) := g_nd;
write(ln, string'(" ") & i2s(g_leads, 5) & string'(" ") & i2s(g_len, 7)
& string'(" ") & i2s(g_disp, 4) & string'(" ") & i2s(g_nd, 2)
& string'(" ") & b2s(g_moved, 5) & string'(" ") & i2s(g_fmove, 3)
& string'(" ") & hex8(g_cmd(7 downto 0)) & string'(" ") & hex8(g_data(7 downto 0))
& string'(" ") & hex8(word_exp(7 downto 0)) & string'(" ") & diag_name(g_code)
& string'(" ") & diag_name(want) & string'(" ") & stim_text(s));
writeline(output, ln);
if g_n - base /= 1 then
write(ln, string'(" FAIL: stimulus ") & i2s(s, 1) & string'(" produced ")
& i2s(g_n - base, 1) & string'(" diagnoses for one frame"));
writeline(output, ln); e := e + 1;
end if;
if g_code /= want then
write(ln, string'(" FAIL: stimulus ") & i2s(s, 1) & string'(" diagnosed ")
& diag_name(g_code) & string'(" where ") & diag_name(want)
& string'(" was expected"));
writeline(output, ln); e := e + 1;
end if;
if g_leads /= w_leads then
write(ln, string'(" FAIL: stimulus ") & i2s(s, 1) & string'(" counted ")
& i2s(g_leads, 1) & string'(" leading edges where ") & i2s(w_leads, 1)
& string'(" were driven"));
writeline(output, ln); e := e + 1;
end if;
if g_nd = 1 and g_disp /= w_disp then
write(ln, string'(" FAIL: stimulus ") & i2s(s, 1)
& string'(" reported displacement ") & i2s(g_disp, 1) & string'(" where ")
& i2s(w_disp, 1) & string'(" was expected"));
writeline(output, ln); e := e + 1;
end if;
end loop;
-- ---- 1. the trade is invisible to both observations ----
if not (leads_log(1) = leads_log(0) and disp_log(1) = disp_log(0) and nd_log(1) = 0) then
write(ln, string'(" FAIL: the traded configuration differed from the correct one in an observation, so the chapter's central claim was not exercised"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" 1. the traded configuration -- a 32-bit address with 0 dummy cycles against a 24-bit address with 8 -- produced ")
& i2s(leads_log(0), 1)
& string'(" leading edges, exactly as the CORRECT read did, and left the data window undisplaced. An edge-count predicate is silent and a displacement search finds nothing to displace. Both of this module's observations agree with a good transfer, and the only thing that disagrees anywhere in the system is the byte itself"));
writeline(output, ln);
-- ---- 2. the returned data is a VALID response, not corruption ----
if data_log(1)(7 downto 0) /= dev_data(x"00" & ADDR_A(23 downto 8)) then
write(ln, string'(" FAIL: the traded read did not return the device's response for the wrongly-latched address"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 2. the traded read returned ") & hex8(data_log(1)(7 downto 0))
& string'(" where ") & hex8(dev_data(ADDR_A)) & string'(" was expected -- and ")
& hex8(data_log(1)(7 downto 0)) & string'(" is exactly what this device answers for address ")
& hex24(x"00" & ADDR_A(23 downto 8))
& string'(", the value it latched when it took the first three of four address bytes. The byte is not corrupt; it is CORRECT for a different address. That is why it survives a plausibility check, why a firmware log looks reasonable, and why the failure gets attributed to the memory contents rather than to the transfer"));
writeline(output, ln);
-- ---- 3. two captures separate what one cannot ----
if data_log(1)(7 downto 0) = data_log(2)(7 downto 0) then
write(ln, string'(" FAIL: the width fault returned the same byte at both addresses, so the pair of captures could not show that the address field is parsed"));
writeline(output, ln); e := e + 1;
end if;
if data_log(3)(7 downto 0) /= data_log(4)(7 downto 0) then
write(ln, string'(" FAIL: the unrecognised command returned different bytes at the two addresses, which it cannot do if the address was never parsed"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 3. the width fault answered ") & hex8(data_log(1)(7 downto 0))
& string'(" at address A and ") & hex8(data_log(2)(7 downto 0))
& string'(" at address B -- the response DEPENDS on the address, so the address field is being parsed and only its width is wrong. The unrecognised command answered ")
& hex8(data_log(3)(7 downto 0))
& string'(" at both -- the response does not depend on the address at all, so the address was never parsed. Neither statement is available from one capture and both follow immediately from two, which makes `re-run it at a different address` the cheapest next measurement in this whole chapter"));
writeline(output, ln);
-- ---- 4. the total cannot localise a field, shown by collision ----
if code_log(5) /= code_log(6) then
write(ln, string'(" FAIL: the two master-side faults produced different verdicts, so the claim that a total cannot localise a field was not exercised"));
writeline(output, ln); e := e + 1;
end if;
if leads_log(5) = leads_log(6) then
write(ln, string'(" FAIL: the two master-side faults produced the same total, so they were not actually different faults"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 4. two DIFFERENT master-side faults -- two extra dummy cycles, and eight extra data bits -- were both reported ")
& diag_name(code_log(5)) & string'(", with only the magnitude (")
& i2s(leads_log(5) - 48, 1) & string'(" against ") & i2s(leads_log(6) - 48, 1)
& string'(") to tell them apart. That is not a weakness of this decoder, it is arithmetic: three field lengths are summed into one total and a total cannot un-sum itself. The actionable part is still there -- the master's own clock accounting is wrong, so the fault is in the driver and not in the device -- and the field has to come from reading the driver's constants against the datasheet"));
writeline(output, ln);
-- ---- 5. a correct answer that is indistinguishable from silence ----
if code_log(9) /= code_log(3) then
write(ln, string'(" FAIL: the 0x00 response and the ignored command produced different verdicts, so the claim that they are the same observation was not exercised"));
writeline(output, ln); e := e + 1;
end if;
if nd_log(3) /= 0 then
write(ln, string'(" FAIL: the ignored command matched ") & i2s(nd_log(3), 1)
& string'(" displacement(s) where 0 was expected"));
writeline(output, ln); e := e + 1;
end if;
if nd_log(9) <= 1 then
write(ln, string'(" FAIL: the 0x00 response matched ") & i2s(nd_log(9), 1)
& string'(" displacement(s); the claim is that it matches every one"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" 5. a device that IGNORED the command and a device that answered 0x00 on a bus resting at 0 produced the SAME verdict, ")
& diag_name(code_log(9))
& string'(", because they produce the same waveform: MISO never moves in either case. That is not a decoder limitation, it is what the wire carries. The disambiguator is the displacement count -- ")
& i2s(nd_log(3), 1)
& string'(" for the ignored command, meaning the expected data is nowhere in the window, against ")
& i2s(nd_log(9), 1)
& string'(" for the 0x00 response, meaning it is everywhere. So the verdict names an observation and a second number says which of two physical situations produced it, which is the honest decomposition. The procedural consequence is a rule about debugging rather than about design: never issue a debug read against an address whose contents equal the bus's resting level. Chapter 18.3 rejected the same two payloads for an unrelated reason -- invariance under permutation rather than invisibility against the idle bus"));
writeline(output, ln);
-- ---- BENCH INTEGRITY ----
if code_log(1) /= D_OK then mutations := mutations + 1; end if;
if leads_log(5) /= 48 then mutations := mutations + 1; end if;
if mutations /= 2 then
write(ln, string'(" FAIL: a deliberately wrong expectation did not mismatch (")
& i2s(mutations, 1) & string'(" of 2)"));
writeline(output, ln); e := e + 1;
end if;
if g_x /= 0 then
write(ln, string'(" FAIL: ") & i2s(g_x, 1) & string'(" reported fields carried a metavalue"));
writeline(output, ln); e := e + 1;
end if;
if e = 0 then
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" and the bench proved itself: two deliberately wrong expectations mismatched, and every reported field carried a known value"));
writeline(output, ln);
write(ln, string'("PASS: several field-length faults share one symptom, and TWO numbers separate four of them -- the total leading-edge count against the intended configuration, and the DISPLACEMENT of the expected data inside the intended data window. A master whose clock accounting is wrong moves the total and not the displacement; a device wanting more or fewer dummy cycles moves the displacement and not the total, and its SIGN names the direction of the register somebody has to write. The displacement is used rather than the launch instant for a reason worth keeping: MISO's first visible change is only an UPPER BOUND on the launch, because a device whose first bit matches the resting bus produces no transition -- and the earlier version of this module, which timed that transition, reported a device-side dummy fault for a master-side width fault. A confident answer naming the wrong device. The fault that defeats both numbers is the trade: 32 address bits against 0 dummy cycles gives the same total (")
& i2s(leads_log(0), 1) & string'(") and no displacement, so every timing observation agrees with a correct read and the data comes back as ")
& hex8(data_log(1)(7 downto 0))
& string'(" -- not corrupt but CORRECT for the address the device actually latched. Two captures settle it where one cannot, because the width fault's answer CHANGED with the address and the unrecognised command's did not. And two limits are measured rather than claimed: a total cannot say WHICH of three summed fields is wrong, shown by two different master-side faults colliding on one verdict; and a response consisting entirely of the resting level matches every displacement, so the module declines instead of picking one"));
writeline(output, ln);
else
write(ln, string'("FAIL: ") & i2s(e, 1) & string'(" error(s)"));
writeline(output, ln);
end if;
run <= false;
wait;
end process stim;
end architecture tb;10. What It Costs
the displacement search 2·data_w − 1 candidate displacements, each a data_w-wide compare
for data_w = 8 15 comparators
for data_w = 32 63 comparatorsThat grows linearly with the data width, and there is a bound worth knowing: you only need to search as far as the largest dummy-count error you are willing to diagnose. Restricting the search to |d| ≤ 4 covers every realistic misconfiguration — dummy counts differ between modes by single digits — and cuts the logic by more than half at 32 bits. The honest trade is that a search window smaller than the real error reports nd = 0, which is FIELD_WIDTH: a wrong answer rather than no answer. So the window is a parameter that has to be documented, not a constant that can be quietly tuned.
11. Where A Two-Capture Diagnostic Lives
Section 7's discriminator needs two different stimuli. An analysis component cannot produce that — it observes what the sequence chose to drive. So this diagnosis is partly a sequence, and that is a structural point rather than a coding detail.
spi_probe_seq (a virtual sequence)
1. read address A → capture, diagnose
2. if the verdict is FIELD_WIDTH:
read address B → capture, diagnose
compare the two returned bytes
different → the address field is parsed at the wrong width
identical → the address is not being parsed at all
3. report the pair as ONE finding12. What This Decoder Cannot Do
✗ say WHICH master-side field is wrong — a total cannot un-sum itself
✗ distinguish a correct all-resting-level response from a silent device
✗ diagnose a displacement larger than its search window (reports FIELD_WIDTH)
✗ localise the traded field from ONE capture, at all
✗ tell a device that wants more dummy cycles from one that is simply slow to
turn its output driver on — both drive late, and no capture separates themThat last one is the boundary this chapter shares with the next. A late launch and a slow output enable produce the same displacement, and the discriminator is not in the digital domain at all: a slow driver's launch instant moves with clock rate and a wrong dummy count does not. Chapter 18.6 takes that up, and its discriminator is deliberately a change to the clock rather than anything visible in one capture.
13. Why an FPGA Engineer Cares
Bringing up a flash controller means getting four numbers right, and the standard procedure is to vary them until reads work. That procedure does find a working combination, and it produces no evidence — the same failure mode Chapter 18.2 identified in a four-way mode sweep, and for the same reason.
Both observations here are cheap on an FPGA. The leading-edge count is one counter. The displacement search is comparators against a constant you already know, and if you restrict it to |d| ≤ 4 it is nine of them. That turns vary the numbers until it works into the master's clock accounting is off by two — which is a line of code, not a search.
And the single most valuable thing in this chapter costs nothing to adopt: read a second address. One extra line in a bring-up script converts an undiagnosable FIELD_WIDTH into a statement about whether the device parses your address at all.
14. Why an ASIC Engineer Cares
The address-width trade is an integration defect that passes every test written against your own model, because your model and your driver were written from the same reading of the table. It appears when the design meets a real device — at bring-up, on a schedule where the answer needs to be defensible by the end of the day.
The MASTER_LEN versus DEVICE_PHASE split is the one that decides ownership. MASTER_LEN means your driver's clock count disagrees with the intended configuration, which is yours. DEVICE_PHASE means the device is driving at a different instant from the one the configuration implies — either its register was never written, or it is not the part in the schematic. A finding that points out of your design has to survive a meeting with the other vendor, and the displacement number is what makes it survive: your device drove two edges later than your datasheet's default is a statement with a measurement behind it.
15. Failure Signature — "The Flash Is Corrupt"
Symptom a flash read returns a byte that is plausible but wrong
Checked the byte is not 0x00, not 0xff, and changes with the address
Concluded the transfer works; the flash contents must be wrong
Action reflash the part; the problem persists
Actual the driver was configured for 4-byte addressing against a
3-byte-addressing device, with the dummy count adjusted to
match — so the clock count was right and the device latched
the top three of four address bytes
Found by reading a second address and noticing the returned byte
tracked the address SHIFTED BY ONE BYTEEvery check performed was a sound check, and each one made the conclusion more confident. The byte was plausible because it was a valid response. It changed with the address because the address was being parsed. The missing question was not "is this data valid" but "valid for which address" — and that question needs two captures.
16. Common Misconceptions
| Misconception | What is actually true |
|---|---|
| A correct total clock count means the field lengths are right | Address bits and dummy cycles trade against each other at a fixed total |
| Garbage data means corrupt data | It may be the correct response for the address the device latched |
| Timing the first MISO transition finds a late launch | That transition is an upper bound; a late launch is not provable from a level |
| Data that changes with the address proves the transfer is right | It proves only that the address is reaching the device somehow |
| A wrong total tells you which field is wrong | Three lengths are summed; a total cannot un-sum itself |
| No response on MISO means the device is dead | It may have answered with a byte equal to the bus's resting level |
| One capture is enough if you look hard enough | The traded-width fault is invisible to every observation in one capture |
17. Reason It Through
18. Understanding Check
19. Summary
Several field-length faults share one symptom, and two numbers separate four of them: the total leading-edge count against the intended configuration, and the displacement of the expected data inside the intended data window. A master whose clock accounting is wrong moves the total and not the displacement; a device wanting more or fewer dummy cycles moves the displacement and not the total, and the sign names the direction of the register somebody has to write.
The displacement is used rather than the launch instant for a reason worth carrying forward: MISO's first visible change is only an upper bound on the launch, because a device whose first bit matches the resting bus produces no transition — and the version of this module that timed that transition reported a device-side dummy fault for a master-side width fault. A confident answer naming the wrong device, from an observation that looks obviously correct.
The fault that defeats both numbers is the trade. Thirty-two address bits against zero dummy cycles gives the same total, 48, and no displacement, so every timing observation agrees with a correct read — and the data comes back as 0x7c, which is not corrupt but correct for the address the device actually latched. Two captures settle it where one cannot: the width fault's answer changed with the address and the unrecognised command's did not, which is the difference between a field parsed at the wrong width and a field never parsed. And two limits are measured rather than claimed — a total cannot say which of three summed fields is wrong, shown by two different master-side faults colliding on one verdict; and a response consisting entirely of the bus's resting level is indistinguishable from silence, so the verdict names the observation and a second number says which situation produced it.
20. What Comes Next
The frame's fields are settled. Chapter 18.5 turns to the boundary that contains them — chip select — where two faults are separated by the number of assertions rather than by anything inside a frame, and where an early deassert's symptom appears one frame late.
Continue learning
Related tutorials
- Related topic
A Systematic Waveform Debug Method
A capture is evidence and a cause is a hypothesis; the useful work of a debug session is the measurement that converts one into the other. Five evidence classes made mutually exclusive by a published priority, with two captures that set two predicates at once so the priority is shown to be load-bearing.
- Related topic
Wrong CPOL, Wrong CPHA, Wrong Sampling Edge
Three mode faults share one symptom and two share the identical wrong byte. Separating them needs three different kinds of observation — a static level, a transition time, and an elimination — plus the discipline to decline when the payload makes the measurement impossible.
- Related topic
Bit Shifts, Bit-Order Mismatch, and Clock-Count Errors
A slip has a side, and only the wire factors it. But whether it is decidable at all is a property of the debug pattern — 0xFF lets a reversed link report a clean byte, and 0x01, the walking one, reports a rotation as a reversal.
- Related topic
CS Timing Faults and Partial Frames
The only fault family in this module whose evidence lives in a different frame from its symptom. A frame cut short leaves orphan bits, and the NEXT frame is structurally flawless and carries the wrong byte.
