Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 wrongWhere the fix is
the master's clock accountinga driver constant
the device's dummy-cycle counta configuration register never written, or a part that isn't the part in the schematic
the address width3-byte against 4-byte addressing

2. Why A Length Check Separates Nothing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   intended:   8 + 24 + 8 + 8  = 48 clocks
   traded:     8 + 32 + 0 + 8  = 48 clocks

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 bus

So 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?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 them

A 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 cycles
Five rows over fourteen cycles, covering leading edges thirty-six to forty-two of a flash read. A phase band marks the dummy cycles and the intended data window. One MISO row drives data bit seven from edge forty; a second MISO row stays at the resting level until edge forty-two. A sample row marks the three instants the decoder reads.dummy cycles, edges 36-39dummy cycles, edges 36-39intended data windowintended data windowedge 40 — the intended launchedge 40 — the intendedlaunchedge 42 — where a late device droveedge 42 — where a latedevice drovesclkmosimiso ok000000000d7d7d6d6d5miso late0000000000000d7sampledbit 7bit 7bit 6bit 6bit 5t0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 1 — the end of an 8-bit flash read: the last of the dummy cycles and the intended data window. A correct device drives from leading edge 40, so the window's first sample is data bit 7. A device wanting two more dummy cycles drives from edge 42, so the window opens with two resting-level bits and the expected byte appears displaced by +2. Horizontal scrolling is expected on a narrow screen; the figure is a timeline.

Read 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                                            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 #2

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 silent

Four 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   width fault          address A → 7c      address B → 74     the answer DEPENDS on the address
   unrecognised command address A → 00      address B → 00     it does NOT

A 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

Azvya Education Pvt. Ltd.VLSI Mentor
spi_len_diag.sv — the field-length decoder — two numbers, a 2×2, and a verdict that names an observation rather than a cause
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_len_diag.v — the same design in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_len_diag.vhd — the same design in VHDL
-- 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.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_len_diag_tb.sv — ten captures of a flash read, and the one fault that two numbers cannot reach
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_len_diag_tb.v — the same bench in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_len_diag_tb.vhd — the same bench in VHDL
-- 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 comparators

That 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 finding

12. What This Decoder Cannot Do

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ✗ 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 them

That 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"

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 BYTE

Every 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

MisconceptionWhat is actually true
A correct total clock count means the field lengths are rightAddress bits and dummy cycles trade against each other at a fixed total
Garbage data means corrupt dataIt may be the correct response for the address the device latched
Timing the first MISO transition finds a late launchThat transition is an upper bound; a late launch is not provable from a level
Data that changes with the address proves the transfer is rightIt proves only that the address is reaching the device somehow
A wrong total tells you which field is wrongThree lengths are summed; a total cannot un-sum itself
No response on MISO means the device is deadIt may have answered with a byte equal to the bus's resting level
One capture is enough if you look hard enoughThe 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