Skip to content
VLSI Mentor

SPI · Module 18

RTL, CDC, Constraint, or Testbench?

Three perturbations — the clock ratio, the bench's data placement, and the payload — map four failure layers to four signatures. And one of the four is provably invisible to any RTL regression.

Six chapters have each taken a fault family and found its discriminator. This one asks the question that actually comes first: which layer is this failure in? Because the answer decides who owns it, what tool applies, and whether any of the previous six chapters is even relevant.

Three perturbations. Four layers. And one layer that a passing regression cannot say anything about.

1. Four Layers, Not Four Faults

LayerWhere the fix isWhat tool applies
RTLthe design's logicsimulation, formal
CDCa clock-domain crossingsimulation partly, CDC analysis
Constraintan SDC file, an I/O budgetstatic timing analysis — not simulation
Testbenchthe environmentnothing but review

These are not four kinds of bug in one place. They are four places, and the mistake that costs weeks is debugging in the wrong one — most often debugging the design when the bench is wrong, or debugging the design when the design is fine and a constraint is not.

2. Three Perturbations, And Why These Three

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   RATE      change the SCLK period; leave the system clock alone.
             Only the PHASE relationship between the two domains moves.

   INSTANT   change when the bench presents MOSI relative to the edge
             that samples it. The design's function does not depend on
             this as long as the data is there in time.

   SEED      change the payload sequence. A bug with a data-dependent
             trigger responds; a systematic one does not.

Each perturbation is chosen to be orthogonal to the others and to leave a correct design alone. That second property is what makes the whole method work, and section 4 is about it.

3. The Signature Matrix

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   layer          base   rate   instant   seed
   RTL bug          a      a       a        b     only the payload moved it
   CDC              c      d       c        c     only the phase moved it
   TESTBENCH        e      e       f        e     only the bench moved it
   CONSTRAINT       0      0       0        0     nothing moved it, and nothing failed

Three perturbations, four layers, and the fourth row is the point of the chapter rather than a gap in the method. A constraint error has no representation in an RTL simulation — RTL has no delays, so a violated input-delay budget is not something a simulator can express. The row is identical to a correct design's, and this chapter measures that rather than asserting it.

4. The Perturbations Must Not Disturb A Working Design

This is the load-bearing row and it is easy to skip.

If a perturbation makes a correct design fail, then a non-zero response to it means nothing — you are measuring the knob rather than the design. So the first requirement on the whole method is that all three perturbations leave the correct design at zero failures, and the bench asserts exactly that before any other conclusion is drawn.

5. The Crossing Defect: A Pulse That Outlives Nothing

The injected CDC fault is the most common real crossing defect there is, and it is worth understanding precisely because the correct alternative costs nothing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a TOGGLE   flips once per word and PERSISTS until the next one
              → no clock ratio can miss it

   a PULSE    is asserted at the last SCLK edge and cleared when the frame
              ends, so its width is a property of the SPI timing
              → caught only if a receiving clock edge falls inside it

A pulse narrower than the receiving clock period is caught only by luck

14 cycles
Six rows over fourteen one-nanosecond columns. SCLK has its final rising edge at column two. Chip select rises at column eight. A request-pulse row is high only from column two to seven. A request-toggle row goes high at column two and stays high. Two system-clock rows are shown with different phases: one rises at columns zero and ten, outside the pulse; the other rises at column three, inside it.the pulse exists only herethe pulse exists only hereframe over — pulse goneframe over — pulse gonepulse asserted; toggle flipspulse asserted; toggleflipsframe ends — the pulse is clearedframe ends — the pulse isclearedthe toggle is still there to be seenthe toggle is still thereto be seensclkcs_nreq pulsereq toggleclk ph Aclk ph Bt0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 1 — the end of a frame, at one nanosecond per column. The request pulse exists only between the last SCLK edge and the end of the frame. Two candidate system-clock phases are shown: one whose rising edges fall outside that window and misses the transfer entirely, and one whose rising edge falls inside it and catches it. The toggle below is still asserted long afterwards, which is why a toggle cannot be missed. Horizontal scrolling is expected on a narrow screen.

Phase A's rising edges fall at columns 0 and 10 — both outside the window — so the transfer is lost. Phase B's edge at column 3 is inside it, so the transfer arrives. Nothing about either design changed; the phase did.

6. The Measurement

Five conditions, four runs each, twenty frames per run, identical output from all three languages:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  condition     base   rate  instant   seed   verdict       expected      what it means
  none             0      0        0      0   NONE_SEEN     NONE_SEEN     nothing responds to anything
  rtl              7      7        7      5   RTL           RTL           only the PAYLOAD moved it
  cdc              8      0        8      8   CDC           CDC           only the clock RATIO moved it
  testbench       20     20        0     20   TESTBENCH     TESTBENCH     only the bench's INSTANT moved it
  constraint       0      0        0      0   NONE_SEEN     NONE_SEEN     NOTHING moved it -- and nothing failed

The matrix is diagonal by measurement. Each fault responds to exactly one perturbation and to neither of the others, and the bench requires that per row rather than eyeballing the table.

Two rows deserve a second look.

The testbench row fails 20 of 20 at baseline. A bench that presents data after the edge that samples it fails every transfer, which in practice is what makes a bench fault findable: a 100% failure rate on a design that was working last week is a strong hint that the change was not in the design.

The cdc row's failures are not corrupted words. They are transfers that never arrived — and section 8 is about what that does to a scoreboard.

7. The Loss Rate Is Predicted, Not Observed

This is the strongest check in the chapter, and it is worth more than the matrix.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a pulse of width W crossing into a clock of period T
   is caught with probability W / T

   so the expected loss over N frames is  N · (1 − W/T)

   here:  W = 6 ns    T = 10 ns    N = 20
          20 · (1 − 6/10) = 8

Eight transfers were lost. The bench computes that number from the periods and requires the measured loss to equal it.

8. What A Crossing Bug Looks Like To A Scoreboard

Nothing. That is the problem.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   words compared:    12
   words mismatched:   0
   verdict:            PASS

Eight transfers were lost and every word that arrived was correct. A scoreboard that compares received words against expected words has nothing to compare for a transfer that never arrived, so it reports a clean run.

9. The Fourth Row: The Regression Is Blind

The constraint condition injects a MOSI delay that is late but still inside the bit time. RTL tolerates it completely, because RTL has no delays — data that arrives before the sampling edge is simply data that arrived.

All four of its runs report zero, and the bench requires the constraint row to be identical to the correct design's row. That is an assertion that the regression is blind, which is the only honest way to publish the fact.

Two consequences, and the second is the more useful one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a PASSING RTL regression is NOT evidence that a constraint is right

   a failure that does NOT reproduce in simulation is EVIDENCE FOR
   this class, rather than an absence of information

10. Building It — Three HDLs

Two faults are injected into the design and two are not, and the asymmetry is the lesson: the testbench fault lives in the bench by definition, and the constraint fault is a delay the design has no way to represent.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_localise.sv — the layered design — a real asynchronous crossing, two injected faults, and two faults it cannot represent
// spi_localise.sv
//
// Chapter 18.7 -- given a failure, is it in the RTL, in a clock-domain crossing, in a CONSTRAINT, or in
// the TESTBENCH? Four causes, three perturbations, and one cause that an RTL regression cannot see at
// all.
//
// THIS IS THE QUESTION THAT COMES FIRST IN PRACTICE AND LAST IN THIS MODULE. Chapters 18.1 to 18.6 each
// took a fault family and found its discriminator. This one asks which family -- or rather which LAYER
// -- a failure belongs to, because the answer decides who owns it and which of the previous six chapters
// is even applicable.
//
// THE THREE PERTURBATIONS, and why these three.
//
//   RATE      change the SCLK period against an unchanged system clock. This changes the PHASE
//             relationship between the two domains and nothing else. A correct design is insensitive
//             to it; a clock-domain crossing is not.
//
//   INSTANT   change when the TESTBENCH places MOSI relative to the SCLK edge that samples it. A
//             correct design is insensitive as long as the data is there in time; a bench that drives
//             on the sampling edge is measuring its own race. Chapter 16.3 built that race deliberately
//             and this chapter uses it as an instrument.
//
//   SEED      change the payload sequence. A logic bug with a data-dependent trigger responds; a
//             systematic one does not.
//
// THE SIGNATURES THE THREE PRODUCE, and the fourth row is the point of the chapter.
//
//     cause           base    rate   instant   seed
//     RTL bug           a      a        a        b      only the data changed it
//     CDC               c      d        c        c      only the phase changed it
//     TESTBENCH         e      e        f        e      only the bench changed it
//     CONSTRAINT        0      0        0        0      NOTHING changed it, and nothing failed
//
// The fourth row is IDENTICAL to a correct design's. That is not a gap in the method; it is the method
// telling the truth. A constraint error has no representation in an RTL simulation -- RTL has no
// delays, so a violated input-delay budget is simply not expressible -- and therefore a passing
// regression is not evidence that a constraint is right. The chapter measures that blindness rather
// than asserting it.
//
// TWO FAULTS ARE INJECTED INTO THIS MODULE AND TWO ARE NOT, and the asymmetry is the lesson.
//
//   f_rtl   a data-dependent last-bit error: when the word being assembled has bit 1 set, the final bit
//           latched is the PREVIOUS MOSI value. A genuine logic bug, local to one word, and it fires
//           only for some payloads.
//
//   f_cdc   the request crosses as a PULSE instead of a TOGGLE, and the pulse lives only until the frame
//           ends. A pulse narrower than the receiving clock's period is caught only if a receiving edge
//           happens to fall inside it -- so whether a transfer is seen at all depends on the phase
//           between the two clocks, and the fraction lost depends on the SCLK period. This is the single
//           most common real crossing defect there is, and unlike a shallow synchroniser it is fully
//           visible in zero-delay RTL.
//
//   the TESTBENCH fault is in the bench, by definition, and injecting it here would be a category error.
//
//   the CONSTRAINT fault is injected by the bench as a MOSI delay that is still comfortably inside the
//           bit time. RTL tolerates it completely. That is the whole demonstration.
//
// WHAT AN RTL SIMULATION CANNOT SEE ABOUT A CDC, said here because the f_cdc model would otherwise
// overclaim: METASTABILITY IS NOT MODELLED. A single-flop synchroniser is a real defect whose mechanism
// is a flip-flop resolving slowly, and in zero-delay RTL it works perfectly. What IS modelled here is
// the DATA-STABILITY half -- sampling a multi-bit bus that is mid-update -- and that half is real,
// common, and phase-dependent. A regression that finds no CDC bug has established nothing about
// synchroniser depth.

`timescale 1ns/1ps

module spi_localise #(
    parameter int NB = 8,
    parameter int DW = 32
) (
    input  wire              clk,
    input  wire              rst_n,

    input  wire              sclk,     // an INDEPENDENT clock -- the whole reason CDC exists in SPI
    input  wire              cs_n,
    input  wire              mosi,

    input  wire              f_rtl,
    input  wire              f_cdc,

    input  wire [DW-1:0]     word_exp,
    input  wire              clr,

    output reg               sys_valid,
    output reg  [DW-1:0]     word_sys,
    output reg  [15:0]       ob_frames,
    output reg  [15:0]       ob_err,
    output reg  [DW-1:0]     ob_last
);

    // ---------------- the SCLK domain ----------------
    //
    // A slave's receive path: shift on the leading edge, latch a word every NB bits, toggle a flag for
    // the other domain. The counter is cleared asynchronously by deselect, which is how a real slave
    // guarantees frame alignment without a length field.
    // THE SHIFT REGISTER IS NB BITS WIDE, NOT DW. A DW-wide register accumulates the previous frames'
    // bits above bit NB-1, so the latched word compares unequal against a zero-extended expectation from
    // the second frame onwards -- which the first version of this module did, and the symptom was a
    // baseline run reporting an error on every frame but the first. A width is part of a design's
    // meaning, not a convenience.
    reg [NB-1:0] sh;
    reg [7:0]    cnt;
    reg          req;      // a TOGGLE: it persists, so it cannot be missed at any clock ratio
    reg          req_p;    // a PULSE that lives only until the frame ends -- the injected crossing bug
    reg [DW-1:0] w_flag;
    reg          mosi_d;

    // THE RESET IS ASYNCHRONOUS AND THE DESELECT IS SYNCHRONOUS TO SCLK, and getting that the wrong way
    // round cost the first version of this module a debugging session: with no reset at all, `req`
    // started X, both synchroniser chains carried X, `fire` was X, and `if (X)` is false -- so the
    // system domain saw almost no frames and every reported word was X. A bench full of comparisons
    // measured nothing and printed a plausible table of zeros.
    always @(posedge sclk or negedge rst_n) begin
        if (!rst_n) begin
            sh     <= {NB{1'b0}};
            cnt    <= 8'd0;
            req    <= 1'b0;
            w_flag <= {DW{1'b0}};
            mosi_d <= 1'b0;
        end else if (cs_n) begin
            cnt <= 8'd0;
        end else begin
            mosi_d <= mosi;
            sh     <= {sh[NB-2:0], mosi};
            if (cnt == NB[7:0] - 8'd1) begin
                cnt <= 8'd0;
                // THE INJECTED LOGIC BUG. When the assembling word has bit 1 set, the final bit latched
                // is the PREVIOUS MOSI value instead of the current one. It is local to one word, it
                // does not corrupt the frames after it, and it fires only for some payloads -- which is
                // what makes its error count depend on the payload SEQUENCE and on nothing else.
                w_flag <= {{(DW-NB){1'b0}}, sh[NB-2:0], (f_rtl && sh[1]) ? mosi_d : mosi};
                req    <= ~req;
            end else begin
                cnt <= cnt + 8'd1;
            end
        end
    end

    // THE PULSE, and the reason it is a bug rather than a style choice. It is asserted at the SCLK edge
    // that completes a word and cleared when the frame ends -- so its width is a property of the SPI
    // TIMING, not of the receiving clock. A toggle carries the same information and cannot be missed,
    // because it persists until the next one.
    //
    // Cleared asynchronously by deselect because there is no SCLK edge left to clear it on: between
    // frames the SCLK domain has no clock at all, which is exactly why a pulse generated there cannot be
    // given a guaranteed minimum width in the receiving domain.
    always @(posedge sclk or posedge cs_n) begin
        if (cs_n) req_p <= 1'b0;
        else      req_p <= (cnt == NB[7:0] - 8'd1);
    end

    // ---------------- the crossing ----------------
    //
    // Two flops on the flag, then an edge detect, then capture the LATCHED word. The latched word is
    // stable for a whole SCLK period before the flag is honoured, so the capture is safe.
    reg req_s1, req_s2, req_s3;
    reg p_s1, p_s2, p_s3;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            req_s1 <= 1'b0; req_s2 <= 1'b0; req_s3 <= 1'b0;
            p_s1   <= 1'b0; p_s2   <= 1'b0; p_s3   <= 1'b0;
        end else begin
            req_s1 <= req;
            req_s2 <= req_s1;
            req_s3 <= req_s2;
            p_s1   <= req_p;
            p_s2   <= p_s1;
            p_s3   <= p_s2;
        end
    end

    // The correct trigger is a CHANGE in the synchronised toggle -- caught at any clock ratio, because a
    // toggle persists until the next word. The faulty trigger is a RISING EDGE of the synchronised
    // pulse, which exists only if a receiving clock edge happened to fall inside the pulse.
    //
    // Note what is NOT modelled: both paths here are two flops deep. A shallow synchroniser is a real
    // defect and it is INVISIBLE in zero-delay RTL, because metastability is not representable. The bug
    // injected here is the other half of the same subject -- pulse width against the receiving clock
    // period -- and that half is fully visible. A regression that finds no crossing bug has established
    // nothing about synchroniser depth.
    wire fire_ok  = (req_s2 !== req_s3);
    wire fire_bad =  p_s2 && !p_s3;
    wire fire     = f_cdc ? fire_bad : fire_ok;

    // ---------------- the system domain ----------------
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sys_valid <= 1'b0;
            word_sys  <= {DW{1'b0}};
            ob_frames <= 16'd0;
            ob_err    <= 16'd0;
            ob_last   <= {DW{1'b0}};
        end else if (clr) begin
            sys_valid <= 1'b0;
            ob_frames <= 16'd0;
            ob_err    <= 16'd0;
        end else begin
            sys_valid <= 1'b0;
            if (fire) begin
                // Both paths read the LATCHED word, which is stable for far longer than either
                // synchroniser takes. The difference between them is only whether the capture happens
                // at all -- so the crossing bug's signature is a LOST TRANSFER rather than a wrong one,
                // and the bench's failure count has to include frames that never arrived.
                word_sys  <= w_flag;
                sys_valid <= 1'b1;
                ob_frames <= ob_frames + 16'd1;
                ob_last   <= w_flag;
                if (w_flag !== word_exp) ob_err <= ob_err + 16'd1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_localise.v — the same design in Verilog-2001
// spi_localise.v
//
// Chapter 18.7 -- given a failure, is it in the RTL, in a clock-domain crossing, in a CONSTRAINT, or in
// the TESTBENCH? Four causes, three perturbations, and one cause that an RTL regression cannot see at
// all.
//
// THIS IS THE QUESTION THAT COMES FIRST IN PRACTICE AND LAST IN THIS MODULE. Chapters 18.1 to 18.6 each
// took a fault family and found its discriminator. This one asks which family -- or rather which LAYER
// -- a failure belongs to, because the answer decides who owns it and which of the previous six chapters
// is even applicable.
//
// THE THREE PERTURBATIONS, and why these three.
//
//   RATE      change the SCLK period against an unchanged system clock. This changes the PHASE
//             relationship between the two domains and nothing else. A correct design is insensitive
//             to it; a clock-domain crossing is not.
//
//   INSTANT   change when the TESTBENCH places MOSI relative to the SCLK edge that samples it. A
//             correct design is insensitive as long as the data is there in time; a bench that drives
//             on the sampling edge is measuring its own race. Chapter 16.3 built that race deliberately
//             and this chapter uses it as an instrument.
//
//   SEED      change the payload sequence. A logic bug with a data-dependent trigger responds; a
//             systematic one does not.
//
// THE SIGNATURES THE THREE PRODUCE, and the fourth row is the point of the chapter.
//
//     cause           base    rate   instant   seed
//     RTL bug           a      a        a        b      only the data changed it
//     CDC               c      d        c        c      only the phase changed it
//     TESTBENCH         e      e        f        e      only the bench changed it
//     CONSTRAINT        0      0        0        0      NOTHING changed it, and nothing failed
//
// The fourth row is IDENTICAL to a correct design's. That is not a gap in the method; it is the method
// telling the truth. A constraint error has no representation in an RTL simulation -- RTL has no
// delays, so a violated input-delay budget is simply not expressible -- and therefore a passing
// regression is not evidence that a constraint is right. The chapter measures that blindness rather
// than asserting it.
//
// TWO FAULTS ARE INJECTED INTO THIS MODULE AND TWO ARE NOT, and the asymmetry is the lesson.
//
//   f_rtl   a data-dependent last-bit error: when the word being assembled has bit 1 set, the final bit
//           latched is the PREVIOUS MOSI value. A genuine logic bug, local to one word, and it fires
//           only for some payloads.
//
//   f_cdc   the request crosses as a PULSE instead of a TOGGLE, and the pulse lives only until the frame
//           ends. A pulse narrower than the receiving clock's period is caught only if a receiving edge
//           happens to fall inside it -- so whether a transfer is seen at all depends on the phase
//           between the two clocks, and the fraction lost depends on the SCLK period. This is the single
//           most common real crossing defect there is, and unlike a shallow synchroniser it is fully
//           visible in zero-delay RTL.
//
//   the TESTBENCH fault is in the bench, by definition, and injecting it here would be a category error.
//
//   the CONSTRAINT fault is injected by the bench as a MOSI delay that is still comfortably inside the
//           bit time. RTL tolerates it completely. That is the whole demonstration.
//
// WHAT AN RTL SIMULATION CANNOT SEE ABOUT A CDC, said here because the f_cdc model would otherwise
// overclaim: METASTABILITY IS NOT MODELLED. A single-flop synchroniser is a real defect whose mechanism
// is a flip-flop resolving slowly, and in zero-delay RTL it works perfectly. What IS modelled here is
// the DATA-STABILITY half -- sampling a multi-bit bus that is mid-update -- and that half is real,
// common, and phase-dependent. A regression that finds no CDC bug has established nothing about
// synchroniser depth.

`timescale 1ns/1ps

module spi_localise #(
    parameter NB = 8,
    parameter DW = 32
) (
    input  wire              clk,
    input  wire              rst_n,

    input  wire              sclk,     // an INDEPENDENT clock -- the whole reason CDC exists in SPI
    input  wire              cs_n,
    input  wire              mosi,

    input  wire              f_rtl,
    input  wire              f_cdc,

    input  wire [DW-1:0]     word_exp,
    input  wire              clr,

    output reg               sys_valid,
    output reg  [DW-1:0]     word_sys,
    output reg  [15:0]       ob_frames,
    output reg  [15:0]       ob_err,
    output reg  [DW-1:0]     ob_last
);

    // ---------------- the SCLK domain ----------------
    //
    // A slave's receive path: shift on the leading edge, latch a word every NB bits, toggle a flag for
    // the other domain. The counter is cleared asynchronously by deselect, which is how a real slave
    // guarantees frame alignment without a length field.
    // THE SHIFT REGISTER IS NB BITS WIDE, NOT DW. A DW-wide register accumulates the previous frames'
    // bits above bit NB-1, so the latched word compares unequal against a zero-extended expectation from
    // the second frame onwards -- which the first version of this module did, and the symptom was a
    // baseline run reporting an error on every frame but the first. A width is part of a design's
    // meaning, not a convenience.
    reg [NB-1:0] sh;
    reg [7:0]    cnt;
    reg          req;      // a TOGGLE: it persists, so it cannot be missed at any clock ratio
    reg          req_p;    // a PULSE that lives only until the frame ends -- the injected crossing bug
    reg [DW-1:0] w_flag;
    reg          mosi_d;

    // THE RESET IS ASYNCHRONOUS AND THE DESELECT IS SYNCHRONOUS TO SCLK, and getting that the wrong way
    // round cost the first version of this module a debugging session: with no reset at all, `req`
    // started X, both synchroniser chains carried X, `fire` was X, and `if (X)` is false -- so the
    // system domain saw almost no frames and every reported word was X. A bench full of comparisons
    // measured nothing and printed a plausible table of zeros.
    always @(posedge sclk or negedge rst_n) begin
        if (!rst_n) begin
            sh     <= {NB{1'b0}};
            cnt    <= 8'd0;
            req    <= 1'b0;
            w_flag <= {DW{1'b0}};
            mosi_d <= 1'b0;
        end else if (cs_n) begin
            cnt <= 8'd0;
        end else begin
            mosi_d <= mosi;
            sh     <= {sh[NB-2:0], mosi};
            if (cnt == NB[7:0] - 8'd1) begin
                cnt <= 8'd0;
                // THE INJECTED LOGIC BUG. When the assembling word has bit 1 set, the final bit latched
                // is the PREVIOUS MOSI value instead of the current one. It is local to one word, it
                // does not corrupt the frames after it, and it fires only for some payloads -- which is
                // what makes its error count depend on the payload SEQUENCE and on nothing else.
                w_flag <= {{(DW-NB){1'b0}}, sh[NB-2:0], (f_rtl && sh[1]) ? mosi_d : mosi};
                req    <= ~req;
            end else begin
                cnt <= cnt + 8'd1;
            end
        end
    end

    // THE PULSE, and the reason it is a bug rather than a style choice. It is asserted at the SCLK edge
    // that completes a word and cleared when the frame ends -- so its width is a property of the SPI
    // TIMING, not of the receiving clock. A toggle carries the same information and cannot be missed,
    // because it persists until the next one.
    //
    // Cleared asynchronously by deselect because there is no SCLK edge left to clear it on: between
    // frames the SCLK domain has no clock at all, which is exactly why a pulse generated there cannot be
    // given a guaranteed minimum width in the receiving domain.
    always @(posedge sclk or posedge cs_n) begin
        if (cs_n) req_p <= 1'b0;
        else      req_p <= (cnt == NB[7:0] - 8'd1);
    end

    // ---------------- the crossing ----------------
    //
    // Two flops on the flag, then an edge detect, then capture the LATCHED word. The latched word is
    // stable for a whole SCLK period before the flag is honoured, so the capture is safe.
    reg req_s1, req_s2, req_s3;
    reg p_s1, p_s2, p_s3;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            req_s1 <= 1'b0; req_s2 <= 1'b0; req_s3 <= 1'b0;
            p_s1   <= 1'b0; p_s2   <= 1'b0; p_s3   <= 1'b0;
        end else begin
            req_s1 <= req;
            req_s2 <= req_s1;
            req_s3 <= req_s2;
            p_s1   <= req_p;
            p_s2   <= p_s1;
            p_s3   <= p_s2;
        end
    end

    // The correct trigger is a CHANGE in the synchronised toggle -- caught at any clock ratio, because a
    // toggle persists until the next word. The faulty trigger is a RISING EDGE of the synchronised
    // pulse, which exists only if a receiving clock edge happened to fall inside the pulse.
    //
    // Note what is NOT modelled: both paths here are two flops deep. A shallow synchroniser is a real
    // defect and it is INVISIBLE in zero-delay RTL, because metastability is not representable. The bug
    // injected here is the other half of the same subject -- pulse width against the receiving clock
    // period -- and that half is fully visible. A regression that finds no crossing bug has established
    // nothing about synchroniser depth.
    wire fire_ok  = (req_s2 !== req_s3);
    wire fire_bad =  p_s2 && !p_s3;
    wire fire     = f_cdc ? fire_bad : fire_ok;

    // ---------------- the system domain ----------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sys_valid <= 1'b0;
            word_sys  <= {DW{1'b0}};
            ob_frames <= 16'd0;
            ob_err    <= 16'd0;
            ob_last   <= {DW{1'b0}};
        end else if (clr) begin
            sys_valid <= 1'b0;
            ob_frames <= 16'd0;
            ob_err    <= 16'd0;
        end else begin
            sys_valid <= 1'b0;
            if (fire) begin
                // Both paths read the LATCHED word, which is stable for far longer than either
                // synchroniser takes. The difference between them is only whether the capture happens
                // at all -- so the crossing bug's signature is a LOST TRANSFER rather than a wrong one,
                // and the bench's failure count has to include frames that never arrived.
                word_sys  <= w_flag;
                sys_valid <= 1'b1;
                ob_frames <= ob_frames + 16'd1;
                ob_last   <= w_flag;
                if (w_flag !== word_exp) ob_err <= ob_err + 16'd1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_localise.vhd — the same design in VHDL
-- spi_localise.vhd
--
-- Chapter 18.7 -- given a failure, is it in the RTL, in a clock-domain crossing, in a CONSTRAINT, or in
-- the TESTBENCH? Four causes, three perturbations, and one cause that an RTL regression cannot see at
-- all.
--
-- THIS IS THE QUESTION THAT COMES FIRST IN PRACTICE AND LAST IN THIS MODULE. Chapters 18.1 to 18.6 each
-- took a fault family and found its discriminator. This one asks which family -- or rather which LAYER
-- -- a failure belongs to, because the answer decides who owns it and which of the previous six chapters
-- is even applicable.
--
-- THE THREE PERTURBATIONS, and why these three.
--
--   RATE      change the SCLK period against an unchanged system clock. This changes the PHASE
--             relationship between the two domains and nothing else. A correct design is insensitive
--             to it; a clock-domain crossing is not.
--
--   INSTANT   change when the TESTBENCH places MOSI relative to the SCLK edge that samples it. A
--             correct design is insensitive as long as the data is there in time; a bench that drives
--             on the sampling edge is measuring its own race. Chapter 16.3 built that race deliberately
--             and this chapter uses it as an instrument.
--
--   SEED      change the payload sequence. A logic bug with a data-dependent trigger responds; a
--             systematic one does not.
--
-- THE SIGNATURES THE THREE PRODUCE, and the fourth row is the point of the chapter.
--
--     cause           base    rate   instant   seed
--     RTL bug           a      a        a        b      only the data changed it
--     CDC               c      d        c        c      only the phase changed it
--     TESTBENCH         e      e        f        e      only the bench changed it
--     CONSTRAINT        0      0        0        0      NOTHING changed it, and nothing failed
--
-- The fourth row is IDENTICAL to a correct design's. That is not a gap in the method; it is the method
-- telling the truth. A constraint error has no representation in an RTL simulation -- RTL has no
-- delays, so a violated input-delay budget is simply not expressible -- and therefore a passing
-- regression is not evidence that a constraint is right. The chapter measures that blindness rather
-- than asserting it.
--
-- TWO FAULTS ARE INJECTED INTO THIS MODULE AND TWO ARE NOT, and the asymmetry is the lesson.
--
--   f_rtl   a data-dependent last-bit error: when the word being assembled has bit 1 set, the final bit
--           latched is the PREVIOUS MOSI value. A genuine logic bug, local to one word, and it fires
--           only for some payloads.
--
--   f_cdc   the request crosses as a PULSE instead of a TOGGLE, and the pulse lives only until the frame
--           ends. A pulse narrower than the receiving clock's period is caught only if a receiving edge
--           happens to fall inside it -- so whether a transfer is seen at all depends on the phase
--           between the two clocks, and the fraction lost depends on the SCLK period. This is the single
--           most common real crossing defect there is, and unlike a shallow synchroniser it is fully
--           visible in zero-delay RTL.
--
--   the TESTBENCH fault is in the bench, by definition, and injecting it here would be a category error.
--
--   the CONSTRAINT fault is injected by the bench as a MOSI delay that is still comfortably inside the
--           bit time. RTL tolerates it completely. That is the whole demonstration.
--
-- WHAT AN RTL SIMULATION CANNOT SEE ABOUT A CDC, said here because the f_cdc model would otherwise
-- overclaim: METASTABILITY IS NOT MODELLED. A single-flop synchroniser is a real defect whose mechanism
-- is a flip-flop resolving slowly, and in zero-delay RTL it works perfectly. What IS modelled here is
-- the DATA-STABILITY half -- sampling a multi-bit bus that is mid-update -- and that half is real,
-- common, and phase-dependent. A regression that finds no CDC bug has established nothing about
-- synchroniser depth.

--
-- WHAT THE VHDL VERSION ADDS. The two synchroniser chains are `std_logic_vector` shift registers rather
-- than three named flops each, which makes the DEPTH a number in one place instead of a pattern a reader
-- has to recognise -- and depth is the parameter that a reviewer of a crossing actually wants to see.
--
-- The injected faults are booleans rather than `std_logic`, so `f_cdc` cannot be driven to 'X' and silently
-- disable the fault it selects. That matters more than it sounds: an 'X' on a fault-select input turns a
-- mux into an X-producer, every comparison downstream evaluates to false, and the run reports a clean
-- table.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). Generics are `NB_C` and `DW_C`; the counters are `n_frm`
-- and `n_err`; the synchroniser chains are `req_sr` and `pls_sr`. No declaration here differs from another
-- only by case -- the check Chapter 17.4 learned to run after a variable `tries` silently became the
-- generic `TRIES` and a rejection loop stopped executing with no diagnostic anywhere.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_localise is
    generic (
        NB_C : positive := 8;
        DW_C : positive := 32
    );
    port (
        clk      : in  std_logic;
        rst_n    : in  std_logic;

        sclk     : in  std_logic;   -- an INDEPENDENT clock: the whole reason CDC exists in SPI
        cs_n     : in  std_logic;
        mosi     : in  std_logic;

        f_rtl    : in  boolean;
        f_cdc    : in  boolean;

        word_exp : in  std_logic_vector(DW_C - 1 downto 0);
        clr      : in  std_logic;

        sys_valid : out std_logic;
        word_sys  : out std_logic_vector(DW_C - 1 downto 0);
        ob_frames : out natural;
        ob_err    : out natural;
        ob_last   : out std_logic_vector(DW_C - 1 downto 0)
    );
end entity spi_localise;

architecture rtl of spi_localise is

    -- ---------------- the SCLK domain ----------------
    signal sh     : std_logic_vector(NB_C - 1 downto 0) := (others => '0');
    signal cnt    : natural := 0;
    signal req    : std_logic := '0';   -- a TOGGLE: it persists, so no clock ratio can miss it
    signal req_p  : std_logic := '0';   -- a PULSE that lives only until the frame ends: the injected bug
    signal w_flag : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
    signal mosi_d : std_logic := '0';

    -- ---------------- the crossing ----------------
    -- The DEPTH is the number in the range, in one place. A reviewer of a crossing wants to read a depth,
    -- not count named flops.
    signal req_sr : std_logic_vector(2 downto 0) := (others => '0');
    signal pls_sr : std_logic_vector(2 downto 0) := (others => '0');

    signal v_r  : std_logic := '0';
    signal ws_r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
    signal wl_r : std_logic_vector(DW_C - 1 downto 0) := (others => '0');
    signal n_frm, n_err : natural := 0;

begin

    sys_valid <= v_r;
    word_sys  <= ws_r;
    ob_last   <= wl_r;
    ob_frames <= n_frm;
    ob_err    <= n_err;

    -- THE RESET IS ASYNCHRONOUS AND THE DESELECT IS SYNCHRONOUS TO SCLK, and getting that the wrong way
    -- round cost the first version of this design a debugging session: with no reset at all, `req` started
    -- as a metavalue, both synchroniser chains carried it, the capture trigger was undefined, and an
    -- undefined condition is treated as FALSE -- so the receiving domain saw almost no frames and every
    -- reported word was a metavalue. A bench full of comparisons measured nothing and printed zeros.
    sclk_dom : process (sclk, rst_n) is
    begin
        if rst_n = '0' then
            sh <= (others => '0'); cnt <= 0; req <= '0';
            w_flag <= (others => '0'); mosi_d <= '0';
        elsif rising_edge(sclk) then
            if cs_n = '1' then
                cnt <= 0;
            else
                mosi_d <= mosi;
                sh     <= sh(NB_C - 2 downto 0) & mosi;
                if cnt = NB_C - 1 then
                    cnt <= 0;
                    -- THE INJECTED LOGIC BUG. When the assembling word has bit 1 set, the final bit latched
                    -- is the PREVIOUS MOSI value instead of the current one. Local to one word, it does not
                    -- corrupt the frames after it, and it fires only for some payloads -- which is what
                    -- makes its failure count depend on the payload SEQUENCE and on nothing else.
                    if f_rtl and sh(1) = '1' then
                        w_flag <= (DW_C - 1 downto NB_C => '0')
                                  & sh(NB_C - 2 downto 0) & mosi_d;
                    else
                        w_flag <= (DW_C - 1 downto NB_C => '0')
                                  & sh(NB_C - 2 downto 0) & mosi;
                    end if;
                    req <= not req;
                else
                    cnt <= cnt + 1;
                end if;
            end if;
        end if;
    end process sclk_dom;

    -- THE PULSE, and the reason it is a bug rather than a style choice. It is asserted at the SCLK edge
    -- that completes a word and cleared when the frame ends -- so its width is a property of the SPI
    -- TIMING rather than of the receiving clock. A toggle carries the same information and cannot be
    -- missed, because it persists until the next one.
    --
    -- Cleared asynchronously by deselect because there is no SCLK edge left to clear it on: between frames
    -- the SCLK domain has no clock at all, which is exactly why a pulse generated there cannot be given a
    -- guaranteed minimum width in the receiving domain.
    pulse_gen : process (sclk, cs_n) is
    begin
        if cs_n = '1' then
            req_p <= '0';
        elsif rising_edge(sclk) then
            if cnt = NB_C - 1 then req_p <= '1'; else req_p <= '0'; end if;
        end if;
    end process pulse_gen;

    sync : process (clk, rst_n) is
    begin
        if rst_n = '0' then
            req_sr <= (others => '0');
            pls_sr <= (others => '0');
        elsif rising_edge(clk) then
            req_sr <= req_sr(1 downto 0) & req;
            pls_sr <= pls_sr(1 downto 0) & req_p;
        end if;
    end process sync;

    -- ---------------- the system domain ----------------
    sys_dom : process (clk, rst_n) is
        variable fire : boolean;
    begin
        if rst_n = '0' then
            v_r <= '0'; ws_r <= (others => '0'); wl_r <= (others => '0');
            n_frm <= 0; n_err <= 0;
        elsif rising_edge(clk) then
            -- The correct trigger is a CHANGE in the synchronised toggle -- caught at any clock ratio,
            -- because a toggle persists until the next word. The faulty trigger is a RISING EDGE of the
            -- synchronised pulse, which exists only if a receiving clock edge happened to fall inside it.
            --
            -- Note what is NOT modelled: both chains here are two flops deep. A shallow synchroniser is a
            -- real defect and it is INVISIBLE in zero-delay simulation, because metastability is not
            -- representable. The bug injected here is the other half of the same subject -- pulse width
            -- against the receiving clock period -- and that half is fully visible.
            if f_cdc then fire := (pls_sr(1) = '1') and (pls_sr(2) = '0');
            else          fire := (req_sr(1) /= req_sr(2));
            end if;

            if clr = '1' then
                v_r   <= '0';
                n_frm <= 0;
                n_err <= 0;
            else
                v_r <= '0';
                if fire then
                    -- Both paths read the LATCHED word, which is stable for far longer than either
                    -- synchroniser takes. The difference between them is only whether the capture happens
                    -- AT ALL -- so the crossing bug's signature is a LOST transfer rather than a wrong
                    -- one, and any failure metric that counts only wrong words is blind to it.
                    ws_r  <= w_flag;
                    wl_r  <= w_flag;
                    v_r   <= '1';
                    n_frm <= n_frm + 1;
                    if w_flag /= word_exp then n_err <= n_err + 1; end if;
                end if;
            end if;
        end if;
    end process sys_dom;

end architecture rtl;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_localise_tb.sv — five conditions, four runs each, and a signature matrix that is diagonal by measurement
// spi_localise_tb.sv
//
// FIVE CONDITIONS, FOUR RUNS EACH, AND A SIGNATURE MATRIX THAT LOCALISES A FAILURE TO A LAYER.
//
// Each run drives twenty well-separated frames and reports a FAILURE COUNT that includes both wrong
// words and transfers that never arrived -- because a crossing defect loses transfers rather than
// corrupting them, and a metric that only counts wrong data is blind to half of what this chapter is
// about.
//
// THE THREE PERTURBATIONS ARE APPLIED ONE AT A TIME TO AN UNCHANGED CONDITION.
//
//   RATE      the SCLK period changes; the system clock does not. Only the PHASE relationship moves.
//   INSTANT   the bench places MOSI two nanoseconds earlier relative to the sampling edge.
//   SEED      a different payload permutation.
//
// WHY THE WAVEFORM IS BUILT AS A TABLE. The first version of this bench emitted SCLK and MOSI from a
// loop of `#` delays, and placing MOSI later than the half period silently LENGTHENED the half period
// instead of making the data late -- so the testbench fault it was trying to inject did not exist, and
// the run reported zero failures. Building the waveform as a table of per-nanosecond values makes the
// timing a thing that can be read and checked rather than a consequence of statement order.
//
// THE FOUR RESULTS.
//
//   1. THE THREE PERTURBATIONS ARE ORTHOGONAL ON A CORRECT DESIGN. Every one of them leaves the failure
//      count at zero, which is what makes a NON-zero response to one of them informative. A perturbation
//      that disturbs a working design is not an instrument.
//
//   2. EACH FAULT RESPONDS TO EXACTLY ONE PERTURBATION. The bench requires each row to differ from its
//      baseline in one column and to match it in the other two -- so the signature matrix is diagonal by
//      measurement rather than by assertion.
//
//   3. THE CROSSING DEFECT'S LOSS RATE IS PREDICTABLE, AND THAT IS THE STRONGEST CHECK HERE. A pulse of
//      width W crossing into a clock of period T is caught with probability W/T, so the expected loss
//      over N frames is N(1 - W/T). The bench computes that number from the periods and requires the
//      measured loss to equal it. A checker that predicts a value is worth more than one that compares
//      against whatever it saw last time.
//
//   4. THE CONSTRAINT-CLASS FAULT IS INDISTINGUISHABLE FROM A CORRECT DESIGN. All four of its runs report
//      zero, exactly as the correct design does, and the bench requires the two rows to be IDENTICAL.
//      That is an assertion that the regression is BLIND -- and it is the only honest way to publish the
//      fact that a passing RTL run says nothing about whether an input-delay budget is met.

`timescale 1ns/1ps

module spi_localise_tb;

    localparam int NB    = 8;
    localparam int DW    = 32;
    localparam int MAXT  = 1024;
    localparam int NFR   = 20;    // frames per run
    localparam int GAP   = 205;   // ns of deselected time -- chosen so the frame period is NOT a
                                  // multiple of the system clock period, so the phase DRIFTS frame to
                                  // frame. With a period that is a multiple, every frame sees the same
                                  // phase and a phase-dependent fault is all-or-nothing.

    // The five conditions.
    localparam int K_NONE = 0;
    localparam int K_RTL  = 1;
    localparam int K_CDC  = 2;
    localparam int K_TB   = 3;
    localparam int K_CON  = 4;

    reg clk = 1'b0;
    always #5 clk = ~clk;               // a 10 ns system clock
    reg rst_n = 1'b1;

    reg          f_rtl = 1'b0, f_cdc = 1'b0, clr = 1'b0;
    reg          b_sclk = 1'b0, b_cs_n = 1'b1, b_mosi = 1'b0;
    reg [DW-1:0] word_exp = {DW{1'b0}};

    wire             sys_valid;
    wire [DW-1:0]    word_sys, ob_last;
    wire [15:0]      ob_frames, ob_err;

    spi_localise #(.NB(NB), .DW(DW)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
        .f_rtl(f_rtl), .f_cdc(f_cdc),
        .word_exp(word_exp), .clr(clr),
        .sys_valid(sys_valid), .word_sys(word_sys),
        .ob_frames(ob_frames), .ob_err(ob_err), .ob_last(ob_last)
    );

    integer errors = 0;

    // Two payload permutations. No two CONSECUTIVE entries are equal in either, which matters: the
    // crossing defect loses a transfer rather than corrupting one, and a lost transfer is only
    // detectable as a missing frame -- so the payload sequence must not be able to mask it.
    reg [7:0] PAT_A [0:7];
    reg [7:0] PAT_B [0:7];

    // ---- the waveform tables ----
    reg       w_sclk [0:MAXT-1];
    reg       w_cs   [0:MAXT-1];
    reg       w_mosi [0:MAXT-1];
    integer   w_len;

    // Build one frame at half-period `ph`, with MOSI placed `toff` ns after each trailing edge.
    //
    // The bit time is ALWAYS 2*ph regardless of `toff`. That is the property the first version of this
    // bench broke: if placing MOSI late is allowed to stretch the bit time, then "late data" becomes
    // "a slower clock" and the fault being injected does not exist.
    task automatic build(input [7:0] w, input integer ph, input integer toff);
        integer t, i, j, le, te, vt;
        reg     cur;
        begin
            w_len = ph + NB*2*ph + ph + GAP;

            for (t = 0; t < MAXT; t = t + 1) begin
                w_sclk[t] = 1'b0;
                w_cs[t]   = 1'b1;
                w_mosi[t] = 1'b0;
            end

            for (t = 0; t < ph + NB*2*ph + ph; t = t + 1) w_cs[t] = 1'b0;

            for (t = 0; t < w_len; t = t + 1) begin
                cur = 1'b0;
                for (i = 0; i < NB; i = i + 1) begin
                    le = ph + i*2*ph;
                    te = le + ph;
                    if (t >= le && t < te) cur = 1'b1;
                end
                w_sclk[t] = cur;
            end

            // Bit 0 is on the pin from the first nanosecond of the frame. Bit j is placed `toff` ns after
            // the trailing edge of bit-time j-1, which for toff > ph lands AFTER the edge that samples it.
            for (j = 0; j < NB; j = j + 1) begin
                if (j == 0) vt = 1;
                else        vt = ph + (j-1)*2*ph + ph + toff;
                if (vt < 0) vt = 0;
                for (t = vt; t < MAXT; t = t + 1) w_mosi[t] = w[NB-1-j];
            end
        end
    endtask

    task automatic drive;
        integer t;
        begin
            for (t = 0; t < w_len; t = t + 1) begin
                b_sclk = w_sclk[t];
                b_cs_n = w_cs[t];
                b_mosi = w_mosi[t];
                #1;
            end
        end
    endtask

    // ---- one run ----
    integer r_fail [0:23];
    integer r_frm  [0:23];
    integer r_err  [0:23];
    integer run_i;

    task automatic run(input integer kind, input integer ph, input integer toff, input integer pat);
        integer i;
        reg [7:0] w;
        begin
            f_rtl = (kind == K_RTL);
            f_cdc = (kind == K_CDC);
            // THE CLEAR IS PULSED ON THE NEGEDGE, and the first version of this bench pulsed it on the
            // posedge -- the same edge the design samples it on. Whether the design saw the pulse then
            // depended on which process the simulator happened to evaluate first, and the counters were
            // not cleared between runs. Chapter 16.3's race, in the bench that measures it.
            @(negedge clk); clr = 1'b1;
            @(negedge clk); clr = 1'b0;
            @(negedge clk);
            for (i = 0; i < NFR; i = i + 1) begin
                w = (pat == 0) ? PAT_A[i % 8] : PAT_B[i % 8];
                word_exp = {24'b0, w};
                build(w, ph, toff);
                drive;
            end
            #40;
            // THE FAILURE COUNT INCLUDES TRANSFERS THAT NEVER ARRIVED. A crossing defect loses frames; a
            // metric that only counts wrong words reports it as a perfect run.
            r_frm[run_i]  = ob_frames;
            r_err[run_i]  = ob_err;
            r_fail[run_i] = ob_err + (NFR - ob_frames);
            run_i = run_i + 1;
        end
    endtask

    function [8*12:1] kname(input integer k);
        begin
            case (k)
                K_NONE: kname = "none        ";
                K_RTL:  kname = "rtl         ";
                K_CDC:  kname = "cdc         ";
                K_TB:   kname = "testbench   ";
                default:kname = "constraint  ";
            endcase
        end
    endfunction

    function [8*12:1] vname(input integer v);
        begin
            case (v)
                0: vname = "NONE_SEEN   ";
                1: vname = "RTL         ";
                2: vname = "CDC         ";
                3: vname = "TESTBENCH   ";
                default: vname = "MIXED       ";
            endcase
        end
    endfunction

    // THE CLASSIFIER, as a pure function of the four failure counts. This is the chapter's thesis
    // expressed as logic: a diagnosis is a pattern of RESPONSES to perturbations, not a property of any
    // one measurement.
    function integer classify(input integer e0, input integer e1, input integer e2, input integer e3);
        integer n;
        begin
            if ((e0 == 0) && (e1 == 0) && (e2 == 0) && (e3 == 0)) classify = 0;
            else begin
                n = 0;
                if (e1 != e0) n = n + 1;
                if (e2 != e0) n = n + 1;
                if (e3 != e0) n = n + 1;
                if      (n != 1)     classify = 4;   // more than one axis moved it, or none did
                else if (e1 != e0)   classify = 2;   // only the clock ratio     -> a crossing
                else if (e2 != e0)   classify = 3;   // only the bench's instant -> the bench
                else                 classify = 1;   // only the payload         -> logic
            end
        end
    endfunction

    // The base configuration, and the three perturbations of it.
    localparam int PH0 = 2;     // system clock 10 ns, so SCLK period 4 ns
    localparam int PH1 = 14;    // SCLK period 28 ns -- a different PHASE relationship, nothing else

    integer ci, base_off, v, want, mutations;
    integer b0, b1, b2, b3;
    integer exp_loss;

    initial begin
        PAT_A[0]=8'h8D; PAT_A[1]=8'hC3; PAT_A[2]=8'h5A; PAT_A[3]=8'h3C;
        PAT_A[4]=8'hF0; PAT_A[5]=8'h96; PAT_A[6]=8'h69; PAT_A[7]=8'hA5;
        PAT_B[0]=8'h71; PAT_B[1]=8'h2E; PAT_B[2]=8'hB4; PAT_B[3]=8'h4B;
        PAT_B[4]=8'h0F; PAT_B[5]=8'hE1; PAT_B[6]=8'h1E; PAT_B[7]=8'hD2;
        run_i = 0; mutations = 0;

        rst_n = 1'b1; @(negedge clk); rst_n = 1'b0;
        repeat (4) @(negedge clk); rst_n = 1'b1; repeat (4) @(negedge clk);

        $display("  condition     base   rate  instant   seed   verdict       expected      what it means");

        for (ci = 0; ci < 5; ci = ci + 1) begin
            // The bench's MOSI offset for this condition. The TESTBENCH fault places the data AFTER the
            // edge that samples it; the CONSTRAINT fault places it late but still inside the bit time,
            // which RTL tolerates completely and a timing budget might not.
            base_off = (ci == K_TB)  ? PH0 + 1 :
                       (ci == K_CON) ? PH0 - 1 : 1;

            run(ci, PH0, base_off, 0);                                     // base
            run(ci, PH1, (ci == K_TB) ? PH1 + 1 :
                         (ci == K_CON) ? PH1 - 1 : 1, 0);                  // RATE
            run(ci, PH0, (base_off - 2 < 0) ? 0 : base_off - 2, 0);        // INSTANT
            run(ci, PH0, base_off, 1);                                     // SEED

            b0 = r_fail[run_i-4]; b1 = r_fail[run_i-3];
            b2 = r_fail[run_i-2]; b3 = r_fail[run_i-1];
            v  = classify(b0, b1, b2, b3);
            want = (ci == K_NONE) ? 0 : (ci == K_RTL) ? 1 : (ci == K_CDC) ? 2 :
                   (ci == K_TB)   ? 3 : 0;

            $display("  %s %5d  %5d  %7d  %5d   %s  %s  %0s",
                     kname(ci), b0, b1, b2, b3, vname(v), vname(want),
                     (ci == K_NONE) ? "nothing responds to anything" :
                     (ci == K_RTL)  ? "only the PAYLOAD moved it" :
                     (ci == K_CDC)  ? "only the clock RATIO moved it" :
                     (ci == K_TB)   ? "only the bench's INSTANT moved it" :
                                      "NOTHING moved it -- and nothing failed");

            if (v != want) begin
                $display("  FAIL: condition %s classified %s where %s was expected",
                         kname(ci), vname(v), vname(want));
                errors = errors + 1;
            end
        end

        // ================= 1. the perturbations do not disturb a correct design =================
        if (!(r_fail[0] == 0 && r_fail[1] == 0 && r_fail[2] == 0 && r_fail[3] == 0)) begin
            $display("  FAIL: a perturbation disturbed the correct design (%0d, %0d, %0d, %0d); a perturbation that breaks a working design is not an instrument",
                     r_fail[0], r_fail[1], r_fail[2], r_fail[3]);
            errors = errors + 1;
        end
        $display("");
        $display("    1. all three perturbations left the correct design at zero failures. That is what makes a non-zero response informative at all -- a knob that disturbs a working design measures the knob rather than the design, and every conclusion below rests on this row");

        // ================= 2. each fault responds to exactly one perturbation =================
        // rtl: only the seed. cdc: only the rate. tb: only the instant.
        if (!(r_fail[5] == r_fail[4] && r_fail[6] == r_fail[4] && r_fail[7] != r_fail[4])) begin
            $display("  FAIL: the logic bug did not respond to the payload alone (%0d | %0d %0d %0d)",
                     r_fail[4], r_fail[5], r_fail[6], r_fail[7]);
            errors = errors + 1;
        end
        if (!(r_fail[9] != r_fail[8] && r_fail[10] == r_fail[8] && r_fail[11] == r_fail[8])) begin
            $display("  FAIL: the crossing defect did not respond to the clock ratio alone (%0d | %0d %0d %0d)",
                     r_fail[8], r_fail[9], r_fail[10], r_fail[11]);
            errors = errors + 1;
        end
        if (!(r_fail[13] == r_fail[12] && r_fail[14] != r_fail[12] && r_fail[15] == r_fail[12])) begin
            $display("  FAIL: the bench fault did not respond to the sampling instant alone (%0d | %0d %0d %0d)",
                     r_fail[12], r_fail[13], r_fail[14], r_fail[15]);
            errors = errors + 1;
        end
        $display("    2. each fault responded to exactly ONE perturbation and to neither of the others. The logic bug moved with the payload (%0d against %0d) and not with the rate or the instant; the crossing defect moved with the rate (%0d against %0d) and not with the payload; the bench fault moved with the instant (%0d against %0d) and not with either of the others. The matrix is diagonal by measurement, which is the only way a signature table is worth anything",
                 r_fail[4], r_fail[7], r_fail[8], r_fail[9], r_fail[12], r_fail[14]);

        // ================= 3. the crossing defect's loss rate is PREDICTED =================
        // A pulse of width W crossing into a clock of period T is caught with probability W/T, so the
        // expected loss over N frames is N*(1 - W/T). Here W = 3*ph (the request is asserted at the last
        // leading edge and cleared when the frame ends) and T = 10 ns.
        exp_loss = (NFR * (10 - 3*PH0)) / 10;
        if ((NFR - r_frm[8]) != exp_loss) begin
            $display("  FAIL: the crossing defect lost %0d frames where the pulse-width arithmetic predicts %0d; a predicted number that does not match is either a wrong model or a wrong design",
                     NFR - r_frm[8], exp_loss);
            errors = errors + 1;
        end
        if (r_err[8] != 0) begin
            $display("  FAIL: the crossing defect corrupted %0d words; its signature should be LOST transfers, not wrong ones",
                     r_err[8]);
            errors = errors + 1;
        end
        $display("    3. the crossing defect lost %0d of %0d transfers, and the pulse-width arithmetic predicted exactly that: the request is asserted at the last SCLK edge and cleared when the frame ends, so it is %0d ns wide against a %0d ns receiving clock, caught with probability %0d/10, and %0d*(1 - %0d/10) = %0d. Not one word was corrupted -- the signature of a crossing defect is a transfer that never arrives, which a scoreboard comparing words reports as a perfect run",
                 NFR - r_frm[8], NFR, 3*PH0, 10, 3*PH0, NFR, 3*PH0, exp_loss);

        // ================= 4. the regression is BLIND to the constraint class =================
        if (!(r_fail[16] == r_fail[0] && r_fail[17] == r_fail[1]
              && r_fail[18] == r_fail[2] && r_fail[19] == r_fail[3])) begin
            $display("  FAIL: the constraint-class fault differed from the correct design somewhere (%0d %0d %0d %0d against %0d %0d %0d %0d); if it is visible, the chapter's central claim is wrong",
                     r_fail[16], r_fail[17], r_fail[18], r_fail[19],
                     r_fail[0], r_fail[1], r_fail[2], r_fail[3]);
            errors = errors + 1;
        end
        $display("    4. the constraint-class fault produced the IDENTICAL signature to a correct design -- zero in all four columns -- and the bench requires that rather than hoping for it. RTL has no delays, so a violated input-delay budget is not expressible in it: MOSI arriving late but inside the bit time is simply data that arrived. The consequence is the one sentence in this chapter worth memorising: a PASSING RTL REGRESSION IS NOT EVIDENCE THAT A CONSTRAINT IS RIGHT, and a failure that does not reproduce in simulation is EVIDENCE FOR this class rather than an absence of information");

        // ================= BENCH INTEGRITY =================
        // COUNTED IN THE POSITIVE SENSE. The first version of this counted the faults that did NOT fire
        // and then required the count to be 2 -- so a run in which both injected faults were silent would
        // have passed, and a run in which both fired reported that neither had. A polarity error in a
        // self-check is the one bug that makes every other check in a bench meaningless.
        if (r_fail[4] != 0)  mutations = mutations + 1;   // the logic bug must actually fail
        if (r_frm[8] != NFR) mutations = mutations + 1;   // the crossing defect must actually lose frames
        if (mutations != 2) begin
            $display("  FAIL: an injected fault produced no failures at all (%0d of 2 fired), so the matrix above is a table of zeros",
                     mutations);
            errors = errors + 1;
        end
        mutations = 0;
        if (classify(0, 0, 0, 0) != 0) mutations = mutations + 1;
        if (classify(5, 9, 5, 5) != 2) mutations = mutations + 1;
        if (classify(5, 9, 2, 5) != 4) mutations = mutations + 1;
        if (mutations != 0) begin
            $display("  FAIL: the classifier misclassified a hand-constructed signature");
            errors = errors + 1;
        end
        if (run_i != 20) begin
            $display("  FAIL: %0d runs were driven where 20 were expected", run_i);
            errors = errors + 1;
        end

        if (errors == 0) begin
            $display("");
            $display("    and the bench proved itself: both injected faults actually failed, the classifier was checked against three hand-constructed signatures including a MIXED one, and all %0d runs were driven",
                     run_i);
            $display("PASS: a failure can be localised to a LAYER by how it responds to perturbations rather than by anything visible in one capture. Three perturbations -- the clock ratio, the instant the bench presents data, and the payload sequence -- leave a correct design at zero failures, which is what makes any response to them informative. A logic bug with a data-dependent trigger moved with the payload alone, %0d failures against %0d. A crossing defect moved with the clock ratio alone, %0d against %0d, and its signature was %0d LOST transfers with not one corrupted word -- a scoreboard comparing words would have called that run perfect. A bench fault moved with the sampling instant alone, %0d against %0d. The loss rate was PREDICTED rather than observed: a request pulse %0d ns wide crossing into a %0d ns clock is caught with probability %0d/10, so %0d frames should be lost and %0d were. And the fourth row is the one to carry away: a CONSTRAINT-class fault produced the identical signature to a correct design, zero in every column, because RTL has no delays and a violated input-delay budget is not expressible in it. A passing RTL regression is not evidence that a constraint is right -- and a failure that does not reproduce in simulation is evidence FOR that class rather than an absence of information",
                     r_fail[4], r_fail[7], r_fail[8], r_fail[9], NFR - r_frm[8],
                     r_fail[12], r_fail[14], 3*PH0, 10, 3*PH0, exp_loss, NFR - r_frm[8]);
        end else begin
            $display("FAIL: %0d error(s)", errors);
        end
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_localise_tb.v — the same bench in Verilog-2001
// spi_localise_tb.v
//
// FIVE CONDITIONS, FOUR RUNS EACH, AND A SIGNATURE MATRIX THAT LOCALISES A FAILURE TO A LAYER.
//
// Each run drives twenty well-separated frames and reports a FAILURE COUNT that includes both wrong
// words and transfers that never arrived -- because a crossing defect loses transfers rather than
// corrupting them, and a metric that only counts wrong data is blind to half of what this chapter is
// about.
//
// THE THREE PERTURBATIONS ARE APPLIED ONE AT A TIME TO AN UNCHANGED CONDITION.
//
//   RATE      the SCLK period changes; the system clock does not. Only the PHASE relationship moves.
//   INSTANT   the bench places MOSI two nanoseconds earlier relative to the sampling edge.
//   SEED      a different payload permutation.
//
// WHY THE WAVEFORM IS BUILT AS A TABLE. The first version of this bench emitted SCLK and MOSI from a
// loop of `#` delays, and placing MOSI later than the half period silently LENGTHENED the half period
// instead of making the data late -- so the testbench fault it was trying to inject did not exist, and
// the run reported zero failures. Building the waveform as a table of per-nanosecond values makes the
// timing a thing that can be read and checked rather than a consequence of statement order.
//
// THE FOUR RESULTS.
//
//   1. THE THREE PERTURBATIONS ARE ORTHOGONAL ON A CORRECT DESIGN. Every one of them leaves the failure
//      count at zero, which is what makes a NON-zero response to one of them informative. A perturbation
//      that disturbs a working design is not an instrument.
//
//   2. EACH FAULT RESPONDS TO EXACTLY ONE PERTURBATION. The bench requires each row to differ from its
//      baseline in one column and to match it in the other two -- so the signature matrix is diagonal by
//      measurement rather than by assertion.
//
//   3. THE CROSSING DEFECT'S LOSS RATE IS PREDICTABLE, AND THAT IS THE STRONGEST CHECK HERE. A pulse of
//      width W crossing into a clock of period T is caught with probability W/T, so the expected loss
//      over N frames is N(1 - W/T). The bench computes that number from the periods and requires the
//      measured loss to equal it. A checker that predicts a value is worth more than one that compares
//      against whatever it saw last time.
//
//   4. THE CONSTRAINT-CLASS FAULT IS INDISTINGUISHABLE FROM A CORRECT DESIGN. All four of its runs report
//      zero, exactly as the correct design does, and the bench requires the two rows to be IDENTICAL.
//      That is an assertion that the regression is BLIND -- and it is the only honest way to publish the
//      fact that a passing RTL run says nothing about whether an input-delay budget is met.

`timescale 1ns/1ps

module spi_localise_tb;

    localparam NB    = 8;
    localparam DW    = 32;
    localparam MAXT  = 1024;
    localparam NFR   = 20;    // frames per run
    localparam GAP   = 205;   // ns of deselected time -- chosen so the frame period is NOT a
                                  // multiple of the system clock period, so the phase DRIFTS frame to
                                  // frame. With a period that is a multiple, every frame sees the same
                                  // phase and a phase-dependent fault is all-or-nothing.

    // The five conditions.
    localparam K_NONE = 0;
    localparam K_RTL  = 1;
    localparam K_CDC  = 2;
    localparam K_TB   = 3;
    localparam K_CON  = 4;

    reg clk;
    always #5 clk = ~clk;               // a 10 ns system clock
    reg rst_n;

    reg          f_rtl, f_cdc, clr;
    reg          b_sclk, b_cs_n, b_mosi;
    reg [DW-1:0] word_exp;

    wire             sys_valid;
    wire [DW-1:0]    word_sys, ob_last;
    wire [15:0]      ob_frames, ob_err;

    spi_localise #(.NB(NB), .DW(DW)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
        .f_rtl(f_rtl), .f_cdc(f_cdc),
        .word_exp(word_exp), .clr(clr),
        .sys_valid(sys_valid), .word_sys(word_sys),
        .ob_frames(ob_frames), .ob_err(ob_err), .ob_last(ob_last)
    );

    integer errors;

    // Two payload permutations. No two CONSECUTIVE entries are equal in either, which matters: the
    // crossing defect loses a transfer rather than corrupting one, and a lost transfer is only
    // detectable as a missing frame -- so the payload sequence must not be able to mask it.
    reg [7:0] PAT_A [0:7];
    reg [7:0] PAT_B [0:7];

    // ---- the waveform tables ----
    reg       w_sclk [0:MAXT-1];
    reg       w_cs   [0:MAXT-1];
    reg       w_mosi [0:MAXT-1];
    integer   w_len;

    // Build one frame at half-period `ph`, with MOSI placed `toff` ns after each trailing edge.
    //
    // The bit time is ALWAYS 2*ph regardless of `toff`. That is the property the first version of this
    // bench broke: if placing MOSI late is allowed to stretch the bit time, then "late data" becomes
    // "a slower clock" and the fault being injected does not exist.
        task build;
        input [7:0] w;
        input integer ph;
        input integer toff;
        integer t, i, j, le, te, vt;
        reg     cur;
        begin
            w_len = ph + NB*2*ph + ph + GAP;

            for (t = 0; t < MAXT; t = t + 1) begin
                w_sclk[t] = 1'b0;
                w_cs[t]   = 1'b1;
                w_mosi[t] = 1'b0;
            end

            for (t = 0; t < ph + NB*2*ph + ph; t = t + 1) w_cs[t] = 1'b0;

            for (t = 0; t < w_len; t = t + 1) begin
                cur = 1'b0;
                for (i = 0; i < NB; i = i + 1) begin
                    le = ph + i*2*ph;
                    te = le + ph;
                    if (t >= le && t < te) cur = 1'b1;
                end
                w_sclk[t] = cur;
            end

            // Bit 0 is on the pin from the first nanosecond of the frame. Bit j is placed `toff` ns after
            // the trailing edge of bit-time j-1, which for toff > ph lands AFTER the edge that samples it.
            for (j = 0; j < NB; j = j + 1) begin
                if (j == 0) vt = 1;
                else        vt = ph + (j-1)*2*ph + ph + toff;
                if (vt < 0) vt = 0;
                for (t = vt; t < MAXT; t = t + 1) w_mosi[t] = w[NB-1-j];
            end
        end
    endtask

    task drive;
        integer t;
        begin
            for (t = 0; t < w_len; t = t + 1) begin
                b_sclk = w_sclk[t];
                b_cs_n = w_cs[t];
                b_mosi = w_mosi[t];
                #1;
            end
        end
    endtask

    // ---- one run ----
    integer r_fail [0:23];
    integer r_frm  [0:23];
    integer r_err  [0:23];
    integer run_i;

        task run;
        input integer kind;
        input integer ph;
        input integer toff;
        input integer pat;
        integer i;
        reg [7:0] w;
        begin
            f_rtl = (kind == K_RTL);
            f_cdc = (kind == K_CDC);
            // THE CLEAR IS PULSED ON THE NEGEDGE, and the first version of this bench pulsed it on the
            // posedge -- the same edge the design samples it on. Whether the design saw the pulse then
            // depended on which process the simulator happened to evaluate first, and the counters were
            // not cleared between runs. Chapter 16.3's race, in the bench that measures it.
            @(negedge clk); clr = 1'b1;
            @(negedge clk); clr = 1'b0;
            @(negedge clk);
            for (i = 0; i < NFR; i = i + 1) begin
                w = (pat == 0) ? PAT_A[i % 8] : PAT_B[i % 8];
                word_exp = {24'b0, w};
                build(w, ph, toff);
                drive;
            end
            #40;
            // THE FAILURE COUNT INCLUDES TRANSFERS THAT NEVER ARRIVED. A crossing defect loses frames; a
            // metric that only counts wrong words reports it as a perfect run.
            r_frm[run_i]  = ob_frames;
            r_err[run_i]  = ob_err;
            r_fail[run_i] = ob_err + (NFR - ob_frames);
            run_i = run_i + 1;
        end
    endtask

        function [8*12:1] kname;
        input integer k;
        begin
            case (k)
                K_NONE: kname = "none        ";
                K_RTL:  kname = "rtl         ";
                K_CDC:  kname = "cdc         ";
                K_TB:   kname = "testbench   ";
                default:kname = "constraint  ";
            endcase
        end
    endfunction

        function [8*12:1] vname;
        input integer v;
        begin
            case (v)
                0: vname = "NONE_SEEN   ";
                1: vname = "RTL         ";
                2: vname = "CDC         ";
                3: vname = "TESTBENCH   ";
                default: vname = "MIXED       ";
            endcase
        end
    endfunction

    // THE CLASSIFIER, as a pure function of the four failure counts. This is the chapter's thesis
    // expressed as logic: a diagnosis is a pattern of RESPONSES to perturbations, not a property of any
    // one measurement.
        function integer classify;
        input integer e0;
        input integer e1;
        input integer e2;
        input integer e3;
        integer n;
        begin
            if ((e0 == 0) && (e1 == 0) && (e2 == 0) && (e3 == 0)) classify = 0;
            else begin
                n = 0;
                if (e1 != e0) n = n + 1;
                if (e2 != e0) n = n + 1;
                if (e3 != e0) n = n + 1;
                if      (n != 1)     classify = 4;   // more than one axis moved it, or none did
                else if (e1 != e0)   classify = 2;   // only the clock ratio     -> a crossing
                else if (e2 != e0)   classify = 3;   // only the bench's instant -> the bench
                else                 classify = 1;   // only the payload         -> logic
            end
        end
    endfunction

    // The base configuration, and the three perturbations of it.
    localparam PH0 = 2;     // system clock 10 ns, so SCLK period 4 ns
    localparam PH1 = 14;    // SCLK period 28 ns -- a different PHASE relationship, nothing else

    integer ci, base_off, v, want, mutations;
    integer b0, b1, b2, b3;
    integer exp_loss;

    initial begin
        PAT_A[0]=8'h8D; PAT_A[1]=8'hC3; PAT_A[2]=8'h5A; PAT_A[3]=8'h3C;
        PAT_A[4]=8'hF0; PAT_A[5]=8'h96; PAT_A[6]=8'h69; PAT_A[7]=8'hA5;
        PAT_B[0]=8'h71; PAT_B[1]=8'h2E; PAT_B[2]=8'hB4; PAT_B[3]=8'h4B;
        PAT_B[4]=8'h0F; PAT_B[5]=8'hE1; PAT_B[6]=8'h1E; PAT_B[7]=8'hD2;
        run_i = 0; mutations = 0;

        rst_n = 1'b1; @(negedge clk); rst_n = 1'b0;
        repeat (4) @(negedge clk); rst_n = 1'b1; repeat (4) @(negedge clk);

        $display("  condition     base   rate  instant   seed   verdict       expected      what it means");

        for (ci = 0; ci < 5; ci = ci + 1) begin
            // The bench's MOSI offset for this condition. The TESTBENCH fault places the data AFTER the
            // edge that samples it; the CONSTRAINT fault places it late but still inside the bit time,
            // which RTL tolerates completely and a timing budget might not.
            base_off = (ci == K_TB)  ? PH0 + 1 :
                       (ci == K_CON) ? PH0 - 1 : 1;

            run(ci, PH0, base_off, 0);                                     // base
            run(ci, PH1, (ci == K_TB) ? PH1 + 1 :
                         (ci == K_CON) ? PH1 - 1 : 1, 0);                  // RATE
            run(ci, PH0, (base_off - 2 < 0) ? 0 : base_off - 2, 0);        // INSTANT
            run(ci, PH0, base_off, 1);                                     // SEED

            b0 = r_fail[run_i-4]; b1 = r_fail[run_i-3];
            b2 = r_fail[run_i-2]; b3 = r_fail[run_i-1];
            v  = classify(b0, b1, b2, b3);
            want = (ci == K_NONE) ? 0 : (ci == K_RTL) ? 1 : (ci == K_CDC) ? 2 :
                   (ci == K_TB)   ? 3 : 0;

            $display("  %0s %5d  %5d  %7d  %5d   %0s  %0s  %0s",
                     kname(ci), b0, b1, b2, b3, vname(v), vname(want),
                     (ci == K_NONE) ? "nothing responds to anything" :
                     (ci == K_RTL)  ? "only the PAYLOAD moved it" :
                     (ci == K_CDC)  ? "only the clock RATIO moved it" :
                     (ci == K_TB)   ? "only the bench's INSTANT moved it" :
                                      "NOTHING moved it -- and nothing failed");

            if (v != want) begin
                $display("  FAIL: condition %0s classified %0s where %0s was expected",
                         kname(ci), vname(v), vname(want));
                errors = errors + 1;
            end
        end

        // ================= 1. the perturbations do not disturb a correct design =================
        if (!(r_fail[0] == 0 && r_fail[1] == 0 && r_fail[2] == 0 && r_fail[3] == 0)) begin
            $display("  FAIL: a perturbation disturbed the correct design (%0d, %0d, %0d, %0d); a perturbation that breaks a working design is not an instrument",
                     r_fail[0], r_fail[1], r_fail[2], r_fail[3]);
            errors = errors + 1;
        end
        $display("");
        $display("    1. all three perturbations left the correct design at zero failures. That is what makes a non-zero response informative at all -- a knob that disturbs a working design measures the knob rather than the design, and every conclusion below rests on this row");

        // ================= 2. each fault responds to exactly one perturbation =================
        // rtl: only the seed. cdc: only the rate. tb: only the instant.
        if (!(r_fail[5] == r_fail[4] && r_fail[6] == r_fail[4] && r_fail[7] != r_fail[4])) begin
            $display("  FAIL: the logic bug did not respond to the payload alone (%0d | %0d %0d %0d)",
                     r_fail[4], r_fail[5], r_fail[6], r_fail[7]);
            errors = errors + 1;
        end
        if (!(r_fail[9] != r_fail[8] && r_fail[10] == r_fail[8] && r_fail[11] == r_fail[8])) begin
            $display("  FAIL: the crossing defect did not respond to the clock ratio alone (%0d | %0d %0d %0d)",
                     r_fail[8], r_fail[9], r_fail[10], r_fail[11]);
            errors = errors + 1;
        end
        if (!(r_fail[13] == r_fail[12] && r_fail[14] != r_fail[12] && r_fail[15] == r_fail[12])) begin
            $display("  FAIL: the bench fault did not respond to the sampling instant alone (%0d | %0d %0d %0d)",
                     r_fail[12], r_fail[13], r_fail[14], r_fail[15]);
            errors = errors + 1;
        end
        $display("    2. each fault responded to exactly ONE perturbation and to neither of the others. The logic bug moved with the payload (%0d against %0d) and not with the rate or the instant; the crossing defect moved with the rate (%0d against %0d) and not with the payload; the bench fault moved with the instant (%0d against %0d) and not with either of the others. The matrix is diagonal by measurement, which is the only way a signature table is worth anything",
                 r_fail[4], r_fail[7], r_fail[8], r_fail[9], r_fail[12], r_fail[14]);

        // ================= 3. the crossing defect's loss rate is PREDICTED =================
        // A pulse of width W crossing into a clock of period T is caught with probability W/T, so the
        // expected loss over N frames is N*(1 - W/T). Here W = 3*ph (the request is asserted at the last
        // leading edge and cleared when the frame ends) and T = 10 ns.
        exp_loss = (NFR * (10 - 3*PH0)) / 10;
        if ((NFR - r_frm[8]) != exp_loss) begin
            $display("  FAIL: the crossing defect lost %0d frames where the pulse-width arithmetic predicts %0d; a predicted number that does not match is either a wrong model or a wrong design",
                     NFR - r_frm[8], exp_loss);
            errors = errors + 1;
        end
        if (r_err[8] != 0) begin
            $display("  FAIL: the crossing defect corrupted %0d words; its signature should be LOST transfers, not wrong ones",
                     r_err[8]);
            errors = errors + 1;
        end
        $display("    3. the crossing defect lost %0d of %0d transfers, and the pulse-width arithmetic predicted exactly that: the request is asserted at the last SCLK edge and cleared when the frame ends, so it is %0d ns wide against a %0d ns receiving clock, caught with probability %0d/10, and %0d*(1 - %0d/10) = %0d. Not one word was corrupted -- the signature of a crossing defect is a transfer that never arrives, which a scoreboard comparing words reports as a perfect run",
                 NFR - r_frm[8], NFR, 3*PH0, 10, 3*PH0, NFR, 3*PH0, exp_loss);

        // ================= 4. the regression is BLIND to the constraint class =================
        if (!(r_fail[16] == r_fail[0] && r_fail[17] == r_fail[1]
              && r_fail[18] == r_fail[2] && r_fail[19] == r_fail[3])) begin
            $display("  FAIL: the constraint-class fault differed from the correct design somewhere (%0d %0d %0d %0d against %0d %0d %0d %0d); if it is visible, the chapter's central claim is wrong",
                     r_fail[16], r_fail[17], r_fail[18], r_fail[19],
                     r_fail[0], r_fail[1], r_fail[2], r_fail[3]);
            errors = errors + 1;
        end
        $display("    4. the constraint-class fault produced the IDENTICAL signature to a correct design -- zero in all four columns -- and the bench requires that rather than hoping for it. RTL has no delays, so a violated input-delay budget is not expressible in it: MOSI arriving late but inside the bit time is simply data that arrived. The consequence is the one sentence in this chapter worth memorising: a PASSING RTL REGRESSION IS NOT EVIDENCE THAT A CONSTRAINT IS RIGHT, and a failure that does not reproduce in simulation is EVIDENCE FOR this class rather than an absence of information");

        // ================= BENCH INTEGRITY =================
        // COUNTED IN THE POSITIVE SENSE. The first version of this counted the faults that did NOT fire
        // and then required the count to be 2 -- so a run in which both injected faults were silent would
        // have passed, and a run in which both fired reported that neither had. A polarity error in a
        // self-check is the one bug that makes every other check in a bench meaningless.
        if (r_fail[4] != 0)  mutations = mutations + 1;   // the logic bug must actually fail
        if (r_frm[8] != NFR) mutations = mutations + 1;   // the crossing defect must actually lose frames
        if (mutations != 2) begin
            $display("  FAIL: an injected fault produced no failures at all (%0d of 2 fired), so the matrix above is a table of zeros",
                     mutations);
            errors = errors + 1;
        end
        mutations = 0;
        if (classify(0, 0, 0, 0) != 0) mutations = mutations + 1;
        if (classify(5, 9, 5, 5) != 2) mutations = mutations + 1;
        if (classify(5, 9, 2, 5) != 4) mutations = mutations + 1;
        if (mutations != 0) begin
            $display("  FAIL: the classifier misclassified a hand-constructed signature");
            errors = errors + 1;
        end
        if (run_i != 20) begin
            $display("  FAIL: %0d runs were driven where 20 were expected", run_i);
            errors = errors + 1;
        end

        if (errors == 0) begin
            $display("");
            $display("    and the bench proved itself: both injected faults actually failed, the classifier was checked against three hand-constructed signatures including a MIXED one, and all %0d runs were driven",
                     run_i);
            $display("PASS: a failure can be localised to a LAYER by how it responds to perturbations rather than by anything visible in one capture. Three perturbations -- the clock ratio, the instant the bench presents data, and the payload sequence -- leave a correct design at zero failures, which is what makes any response to them informative. A logic bug with a data-dependent trigger moved with the payload alone, %0d failures against %0d. A crossing defect moved with the clock ratio alone, %0d against %0d, and its signature was %0d LOST transfers with not one corrupted word -- a scoreboard comparing words would have called that run perfect. A bench fault moved with the sampling instant alone, %0d against %0d. The loss rate was PREDICTED rather than observed: a request pulse %0d ns wide crossing into a %0d ns clock is caught with probability %0d/10, so %0d frames should be lost and %0d were. And the fourth row is the one to carry away: a CONSTRAINT-class fault produced the identical signature to a correct design, zero in every column, because RTL has no delays and a violated input-delay budget is not expressible in it. A passing RTL regression is not evidence that a constraint is right -- and a failure that does not reproduce in simulation is evidence FOR that class rather than an absence of information",
                     r_fail[4], r_fail[7], r_fail[8], r_fail[9], NFR - r_frm[8],
                     r_fail[12], r_fail[14], 3*PH0, 10, 3*PH0, exp_loss, NFR - r_frm[8]);
        end else begin
            $display("FAIL: %0d error(s)", errors);
        end
        $finish;
    end


    initial begin
        f_rtl = 1'b0;
        f_cdc = 1'b0;
        clr = 1'b0;
        b_sclk = 1'b0;
        b_cs_n = 1'b1;
        b_mosi = 1'b0;
        clk = 1'b0;
        rst_n = 1'b1;
        word_exp = {DW{1'b0}};
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_localise_tb.vhd — the same bench in VHDL
-- spi_localise_tb.vhd
--
-- FIVE CONDITIONS, FOUR RUNS EACH, AND A SIGNATURE MATRIX THAT LOCALISES A FAILURE TO A LAYER.
--
-- Each run drives twenty well-separated frames and reports a FAILURE COUNT that includes both wrong words
-- and transfers that never arrived -- because a crossing defect loses transfers rather than corrupting
-- them, and a metric that only counts wrong data is blind to half of what this chapter is about.
--
-- The same four results as the other two languages, with the same numbers. The third implementation earns
-- its place here in a specific way: the CLASSIFIER is written against an enumeration rather than against
-- integers, so a verdict outside the defined set is not expressible, and the `MIXED` case -- more than one
-- perturbation moved the count -- is a named value rather than a fall-through.
--
-- WHY THE WAVEFORM IS BUILT AS A TABLE. The first version of this bench emitted SCLK and MOSI from a loop
-- of waits, and placing MOSI later than the half period silently LENGTHENED the half period instead of
-- making the data late -- so the testbench fault it was trying to inject did not exist and the run reported
-- zero failures. A table of per-nanosecond values makes the timing something a reader can check.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `NB_C`, `DW_C`, `MAXT_C`, `NFR_C`, `GAP_C`, `PH0_C` and
-- `PH1_C` all carry suffixes so nothing can shadow them in another case; the condition and verdict
-- selectors are enumerations rather than integers.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;

entity spi_localise_tb is
end entity spi_localise_tb;

architecture tb of spi_localise_tb is

    constant NB_C   : positive := 8;
    constant DW_C   : positive := 32;
    constant MAXT_C : natural  := 1024;
    constant NFR_C  : natural  := 20;   -- frames per run
    -- Chosen so the frame period is NOT a multiple of the system clock period, so the phase DRIFTS frame
    -- to frame. With a period that is a multiple, every frame sees the same phase and a phase-dependent
    -- fault is all-or-nothing rather than a fraction.
    constant GAP_C  : natural  := 205;
    constant PH0_C  : natural  := 2;    -- system clock 10 ns, so SCLK period 4 ns
    constant PH1_C  : natural  := 14;   -- SCLK period 28 ns: a different PHASE relationship, nothing else

    subtype byte_t is std_logic_vector(7 downto 0);

    type cond_t is (K_NONE, K_RTL, K_CDC, K_TB, K_CON);
    type verdict_t is (V_NONE_SEEN, V_RTL, V_CDC, V_TESTBENCH, V_MIXED);

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '1';
    signal run   : boolean   := true;

    signal f_rtl, f_cdc : boolean := false;
    signal clr : std_logic := '0';
    signal b_sclk : std_logic := '0';
    signal b_cs_n : std_logic := '1';
    signal b_mosi : std_logic := '0';
    signal word_exp : std_logic_vector(DW_C - 1 downto 0) := (others => '0');

    signal sys_valid : std_logic;
    signal word_sys, ob_last : std_logic_vector(DW_C - 1 downto 0);
    signal ob_frames, ob_err : natural;

    type nat_arr  is array (natural range <>) of natural;
    type byte_arr is array (natural range <>) of byte_t;

    -- Two payload permutations. No two CONSECUTIVE entries are equal in either, which matters: the crossing
    -- defect loses a transfer rather than corrupting one, so the payload sequence must not be able to mask
    -- a missing frame.
    constant PAT_A_C : byte_arr(0 to 7) :=
        (x"8D", x"C3", x"5A", x"3C", x"F0", x"96", x"69", x"A5");
    constant PAT_B_C : byte_arr(0 to 7) :=
        (x"71", x"2E", x"B4", x"4B", x"0F", x"E1", x"1E", x"D2");

    function i2s (v : integer; w : natural) return string is
        constant S : string          := integer'image(v);
        constant P : string(1 to 40) := (others => ' ');
    begin
        if S'length >= w then return S; end if;
        return P(1 to w - S'length) & S;
    end function i2s;

    function kname (k : cond_t) return string is
    begin
        case k is
            when K_NONE => return "none        ";
            when K_RTL  => return "rtl         ";
            when K_CDC  => return "cdc         ";
            when K_TB   => return "testbench   ";
            when others => return "constraint  ";
        end case;
    end function kname;

    function vname (v : verdict_t) return string is
    begin
        case v is
            when V_NONE_SEEN => return "NONE_SEEN   ";
            when V_RTL       => return "RTL         ";
            when V_CDC       => return "CDC         ";
            when V_TESTBENCH => return "TESTBENCH   ";
            when others      => return "MIXED       ";
        end case;
    end function vname;

    -- THE CLASSIFIER, a pure function of the four failure counts: the chapter's thesis as logic. A diagnosis
    -- is a pattern of RESPONSES to perturbations, not a property of any one measurement.
    function classify (e0, e1, e2, e3 : natural) return verdict_t is
        variable n : natural := 0;
    begin
        if e0 = 0 and e1 = 0 and e2 = 0 and e3 = 0 then
            return V_NONE_SEEN;
        end if;
        if e1 /= e0 then n := n + 1; end if;
        if e2 /= e0 then n := n + 1; end if;
        if e3 /= e0 then n := n + 1; end if;
        if    n /= 1     then return V_MIXED;       -- more than one axis moved it, or none did
        elsif e1 /= e0   then return V_CDC;         -- only the clock ratio     -> a crossing
        elsif e2 /= e0   then return V_TESTBENCH;   -- only the bench's instant -> the bench
        else                  return V_RTL;         -- only the payload         -> logic
        end if;
    end function classify;

    function means_text (k : cond_t) return string is
    begin
        case k is
            when K_NONE => return "nothing responds to anything";
            when K_RTL  => return "only the PAYLOAD moved it";
            when K_CDC  => return "only the clock RATIO moved it";
            when K_TB   => return "only the bench's INSTANT moved it";
            when others => return "NOTHING moved it -- and nothing failed";
        end case;
    end function means_text;

begin

    clk_gen : process is
    begin
        while run loop
            clk <= '0'; wait for 5 ns;
            clk <= '1'; wait for 5 ns;
        end loop;
        wait;
    end process clk_gen;

    dut : entity work.spi_localise
        generic map (NB_C => NB_C, DW_C => DW_C)
        port map (
            clk => clk, rst_n => rst_n,
            sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
            f_rtl => f_rtl, f_cdc => f_cdc,
            word_exp => word_exp, clr => clr,
            sys_valid => sys_valid, word_sys => word_sys,
            ob_frames => ob_frames, ob_err => ob_err, ob_last => ob_last
        );

    stim : process is

        type tick_arr is array (0 to MAXT_C - 1) of std_logic;

        variable w_sclk, w_cs, w_mosi : tick_arr;
        variable w_len : natural;

        variable r_fail, r_frm, r_err : nat_arr(0 to 23);
        variable run_i, e, fired, bad_cls : natural := 0;
        variable b0, b1, b2, b3, exp_loss, base_off : natural;
        -- An INTEGER, because `base_off - 2` is legitimately negative for the baseline offset of 1 and a
        -- natural-typed variable would take a range fault rather than clamping. `integer'max` is a
        -- VHDL-2019 attribute and is not available here.
        variable inst_off : integer;
        variable v, want : verdict_t;
        variable ln : line;

        -- Build one frame at half-period `ph`, with MOSI placed `toff` ns after each trailing edge.
        --
        -- The bit time is ALWAYS 2*ph regardless of `toff`. That is the property the first version of this
        -- bench broke: if placing MOSI late is allowed to stretch the bit time, then "late data" becomes
        -- "a slower clock" and the fault being injected does not exist.
        procedure build (w : byte_t; ph : natural; toff : integer) is
            variable le, te, vt : integer;
            variable cur : std_logic;
        begin
            w_len := ph + NB_C*2*ph + ph + GAP_C;

            for t in 0 to MAXT_C - 1 loop
                w_sclk(t) := '0'; w_cs(t) := '1'; w_mosi(t) := '0';
            end loop;

            for t in 0 to ph + NB_C*2*ph + ph - 1 loop
                w_cs(t) := '0';
            end loop;

            for t in 0 to w_len - 1 loop
                cur := '0';
                for i in 0 to NB_C - 1 loop
                    le := ph + i*2*ph;
                    te := le + ph;
                    if t >= le and t < te then cur := '1'; end if;
                end loop;
                w_sclk(t) := cur;
            end loop;

            -- Bit 0 is on the pin from the first nanosecond. Bit j is placed `toff` ns after the trailing
            -- edge of bit-time j-1, which for toff > ph lands AFTER the edge that samples it.
            for j in 0 to NB_C - 1 loop
                if j = 0 then vt := 1;
                else          vt := ph + (j-1)*2*ph + ph + toff;
                end if;
                if vt < 0 then vt := 0; end if;
                for t in vt to MAXT_C - 1 loop
                    w_mosi(t) := w(NB_C - 1 - j);
                end loop;
            end loop;
        end procedure build;

        procedure drive is
        begin
            for t in 0 to w_len - 1 loop
                b_sclk <= w_sclk(t);
                b_cs_n <= w_cs(t);
                b_mosi <= w_mosi(t);
                wait for 1 ns;
            end loop;
        end procedure drive;

        procedure do_run (kind : cond_t; ph : natural; toff : integer; pat : natural) is
            variable w : byte_t;
        begin
            f_rtl <= (kind = K_RTL);
            f_cdc <= (kind = K_CDC);
            -- THE CLEAR IS PULSED ON THE FALLING EDGE, and the first version of this bench pulsed it on the
            -- rising edge -- the same edge the design samples it on. Whether the design saw the pulse then
            -- depended on which process the simulator happened to evaluate first, and the counters were not
            -- cleared between runs. Chapter 16.3's race, in the bench that measures it.
            wait until falling_edge(clk); clr <= '1';
            wait until falling_edge(clk); clr <= '0';
            wait until falling_edge(clk);
            for i in 0 to NFR_C - 1 loop
                if pat = 0 then w := PAT_A_C(i mod 8); else w := PAT_B_C(i mod 8); end if;
                word_exp <= x"000000" & w;
                wait for 1 ns;
                build(w, ph, toff);
                drive;
            end loop;
            wait for 40 ns;
            -- THE FAILURE COUNT INCLUDES TRANSFERS THAT NEVER ARRIVED.
            r_frm(run_i)  := ob_frames;
            r_err(run_i)  := ob_err;
            r_fail(run_i) := ob_err + (NFR_C - ob_frames);
            run_i := run_i + 1;
        end procedure do_run;

    begin
        rst_n <= '1';
        wait until falling_edge(clk);
        rst_n <= '0';
        for i in 1 to 4 loop wait until falling_edge(clk); end loop;
        rst_n <= '1';
        for i in 1 to 4 loop wait until falling_edge(clk); end loop;

        write(ln, string'("  condition     base   rate  instant   seed   verdict       expected      what it means"));
        writeline(output, ln);

        for ci in cond_t'pos(K_NONE) to cond_t'pos(K_CON) loop
            -- The bench's MOSI offset for this condition. The TESTBENCH fault places the data AFTER the edge
            -- that samples it; the CONSTRAINT fault places it late but still inside the bit time, which RTL
            -- tolerates completely and a timing budget might not.
            if    cond_t'val(ci) = K_TB  then base_off := PH0_C + 1;
            elsif cond_t'val(ci) = K_CON then base_off := PH0_C - 1;
            else                              base_off := 1;
            end if;

            do_run(cond_t'val(ci), PH0_C, base_off, 0);
            if    cond_t'val(ci) = K_TB  then do_run(cond_t'val(ci), PH1_C, PH1_C + 1, 0);
            elsif cond_t'val(ci) = K_CON then do_run(cond_t'val(ci), PH1_C, PH1_C - 1, 0);
            else                              do_run(cond_t'val(ci), PH1_C, 1, 0);
            end if;
            inst_off := base_off - 2;
            if inst_off < 0 then inst_off := 0; end if;
            do_run(cond_t'val(ci), PH0_C, inst_off, 0);
            do_run(cond_t'val(ci), PH0_C, base_off, 1);

            b0 := r_fail(run_i-4); b1 := r_fail(run_i-3);
            b2 := r_fail(run_i-2); b3 := r_fail(run_i-1);
            v  := classify(b0, b1, b2, b3);
            case cond_t'val(ci) is
                when K_NONE => want := V_NONE_SEEN;
                when K_RTL  => want := V_RTL;
                when K_CDC  => want := V_CDC;
                when K_TB   => want := V_TESTBENCH;
                when others => want := V_NONE_SEEN;
            end case;

            write(ln, string'("  ") & kname(cond_t'val(ci)) & string'(" ") & i2s(b0, 5)
                      & string'("  ") & i2s(b1, 5) & string'("  ") & i2s(b2, 7)
                      & string'("  ") & i2s(b3, 5) & string'("   ") & vname(v)
                      & string'("  ") & vname(want) & string'("  ") & means_text(cond_t'val(ci)));
            writeline(output, ln);

            if v /= want then
                write(ln, string'("  FAIL: condition ") & kname(cond_t'val(ci))
                          & string'(" classified ") & vname(v) & string'(" where ") & vname(want)
                          & string'(" was expected"));
                writeline(output, ln); e := e + 1;
            end if;
        end loop;

        -- ================= 1. the perturbations do not disturb a correct design =================
        if not (r_fail(0) = 0 and r_fail(1) = 0 and r_fail(2) = 0 and r_fail(3) = 0) then
            write(ln, string'("  FAIL: a perturbation disturbed the correct design; a perturbation that breaks a working design is not an instrument"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'(""));
        writeline(output, ln);
        write(ln, string'("    1. all three perturbations left the correct design at zero failures. That is what makes a non-zero response informative at all -- a knob that disturbs a working design measures the knob rather than the design, and every conclusion below rests on this row"));
        writeline(output, ln);

        -- ================= 2. each fault responds to exactly one perturbation =================
        if not (r_fail(5) = r_fail(4) and r_fail(6) = r_fail(4) and r_fail(7) /= r_fail(4)) then
            write(ln, string'("  FAIL: the logic bug did not respond to the payload alone"));
            writeline(output, ln); e := e + 1;
        end if;
        if not (r_fail(9) /= r_fail(8) and r_fail(10) = r_fail(8) and r_fail(11) = r_fail(8)) then
            write(ln, string'("  FAIL: the crossing defect did not respond to the clock ratio alone"));
            writeline(output, ln); e := e + 1;
        end if;
        if not (r_fail(13) = r_fail(12) and r_fail(14) /= r_fail(12) and r_fail(15) = r_fail(12)) then
            write(ln, string'("  FAIL: the bench fault did not respond to the sampling instant alone"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    2. each fault responded to exactly ONE perturbation and to neither of the others. The logic bug moved with the payload (")
                  & i2s(r_fail(4), 1) & string'(" against ") & i2s(r_fail(7), 1)
                  & string'(") and not with the rate or the instant; the crossing defect moved with the rate (")
                  & i2s(r_fail(8), 1) & string'(" against ") & i2s(r_fail(9), 1)
                  & string'(") and not with the payload; the bench fault moved with the instant (")
                  & i2s(r_fail(12), 1) & string'(" against ") & i2s(r_fail(14), 1)
                  & string'(") and not with either of the others. The matrix is diagonal by measurement, which is the only way a signature table is worth anything"));
        writeline(output, ln);

        -- ================= 3. the crossing defect's loss rate is PREDICTED =================
        -- A pulse of width W crossing into a clock of period T is caught with probability W/T, so the
        -- expected loss over N frames is N*(1 - W/T). Here W = 3*ph and T = 10 ns.
        exp_loss := (NFR_C * (10 - 3*PH0_C)) / 10;
        if (NFR_C - r_frm(8)) /= exp_loss then
            write(ln, string'("  FAIL: the crossing defect lost ") & i2s(NFR_C - r_frm(8), 1)
                      & string'(" frames where the pulse-width arithmetic predicts ") & i2s(exp_loss, 1)
                      & string'("; a predicted number that does not match is either a wrong model or a wrong design"));
            writeline(output, ln); e := e + 1;
        end if;
        if r_err(8) /= 0 then
            write(ln, string'("  FAIL: the crossing defect corrupted ") & i2s(r_err(8), 1)
                      & string'(" words; its signature should be LOST transfers, not wrong ones"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    3. the crossing defect lost ") & i2s(NFR_C - r_frm(8), 1)
                  & string'(" of ") & i2s(NFR_C, 1)
                  & string'(" transfers, and the pulse-width arithmetic predicted exactly that: the request is asserted at the last SCLK edge and cleared when the frame ends, so it is ")
                  & i2s(3*PH0_C, 1) & string'(" ns wide against a 10 ns receiving clock, caught with probability ")
                  & i2s(3*PH0_C, 1) & string'("/10, and ") & i2s(NFR_C, 1) & string'("*(1 - ")
                  & i2s(3*PH0_C, 1) & string'("/10) = ") & i2s(exp_loss, 1)
                  & string'(". Not one word was corrupted -- the signature of a crossing defect is a transfer that never arrives, which a scoreboard comparing words reports as a perfect run"));
        writeline(output, ln);

        -- ================= 4. the regression is BLIND to the constraint class =================
        if not (r_fail(16) = r_fail(0) and r_fail(17) = r_fail(1)
                and r_fail(18) = r_fail(2) and r_fail(19) = r_fail(3)) then
            write(ln, string'("  FAIL: the constraint-class fault differed from the correct design somewhere; if it is visible, the chapter's central claim is wrong"));
            writeline(output, ln); e := e + 1;
        end if;
        write(ln, string'("    4. the constraint-class fault produced the IDENTICAL signature to a correct design -- zero in all four columns -- and the bench requires that rather than hoping for it. RTL has no delays, so a violated input-delay budget is not expressible in it: MOSI arriving late but inside the bit time is simply data that arrived. The consequence is the one sentence in this chapter worth memorising: a PASSING RTL REGRESSION IS NOT EVIDENCE THAT A CONSTRAINT IS RIGHT, and a failure that does not reproduce in simulation is EVIDENCE FOR this class rather than an absence of information"));
        writeline(output, ln);

        -- ================= BENCH INTEGRITY =================
        -- COUNTED IN THE POSITIVE SENSE. Counting the faults that did NOT fire and requiring the count to be
        -- two would pass a run in which both injected faults were silent -- a polarity error in a self-check
        -- is the one bug that makes every other check in a bench meaningless.
        fired := 0;
        if r_fail(4) /= 0     then fired := fired + 1; end if;
        if r_frm(8)  /= NFR_C then fired := fired + 1; end if;
        if fired /= 2 then
            write(ln, string'("  FAIL: an injected fault produced no failures at all (")
                      & i2s(fired, 1) & string'(" of 2 fired), so the matrix above is a table of zeros"));
            writeline(output, ln); e := e + 1;
        end if;
        bad_cls := 0;
        if classify(0, 0, 0, 0) /= V_NONE_SEEN then bad_cls := bad_cls + 1; end if;
        if classify(5, 9, 5, 5) /= V_CDC       then bad_cls := bad_cls + 1; end if;
        if classify(5, 9, 2, 5) /= V_MIXED     then bad_cls := bad_cls + 1; end if;
        if bad_cls /= 0 then
            write(ln, string'("  FAIL: the classifier misclassified a hand-constructed signature"));
            writeline(output, ln); e := e + 1;
        end if;
        if run_i /= 20 then
            write(ln, string'("  FAIL: ") & i2s(run_i, 1) & string'(" runs were driven where 20 were expected"));
            writeline(output, ln); e := e + 1;
        end if;

        if e = 0 then
            write(ln, string'(""));
            writeline(output, ln);
            write(ln, string'("    and the bench proved itself: both injected faults actually failed, the classifier was checked against three hand-constructed signatures including a MIXED one, and all ")
                      & i2s(run_i, 1) & string'(" runs were driven"));
            writeline(output, ln);
            write(ln, string'("PASS: a failure can be localised to a LAYER by how it responds to perturbations rather than by anything visible in one capture. Three perturbations -- the clock ratio, the instant the bench presents data, and the payload sequence -- leave a correct design at zero failures, which is what makes any response to them informative. A logic bug with a data-dependent trigger moved with the payload alone, ")
                      & i2s(r_fail(4), 1) & string'(" failures against ") & i2s(r_fail(7), 1)
                      & string'(". A crossing defect moved with the clock ratio alone, ") & i2s(r_fail(8), 1)
                      & string'(" against ") & i2s(r_fail(9), 1) & string'(", and its signature was ")
                      & i2s(NFR_C - r_frm(8), 1)
                      & string'(" LOST transfers with not one corrupted word -- a scoreboard comparing words would have called that run perfect. A bench fault moved with the sampling instant alone, ")
                      & i2s(r_fail(12), 1) & string'(" against ") & i2s(r_fail(14), 1)
                      & string'(". The loss rate was PREDICTED rather than observed: a request pulse ")
                      & i2s(3*PH0_C, 1) & string'(" ns wide crossing into a 10 ns clock is caught with probability ")
                      & i2s(3*PH0_C, 1) & string'("/10, so ") & i2s(exp_loss, 1)
                      & string'(" frames should be lost and ") & i2s(NFR_C - r_frm(8), 1)
                      & string'(" were. And the fourth row is the one to carry away: a CONSTRAINT-class fault produced the identical signature to a correct design, zero in every column, because RTL has no delays and a violated input-delay budget is not expressible in it. A passing RTL regression is not evidence that a constraint is right -- and a failure that does not reproduce in simulation is evidence FOR that class rather than an absence of information"));
            writeline(output, ln);
        else
            write(ln, string'("FAIL: ") & i2s(e, 1) & string'(" error(s)"));
            writeline(output, ln);
        end if;

        run <= false;
        wait;
    end process stim;

end architecture tb;

11. Perturbation As A Test Axis

The UVM consequence is structural: the three perturbations have to be configuration, not constants.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   spi_env_cfg
     rand int sclk_period_ns;      // the RATE axis
     rand int drive_offset_ns;     // the INSTANT axis
     // the SEED axis is already a UVM first-class citizen

A regression that runs one clock ratio has one column of the matrix and cannot produce a diagonal. A regression that runs three ratios costs three times the wall clock and can localise a failure to a layer without anybody opening a waveform.

12. What This Method Cannot Do

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ✗ localise anything that does not reproduce (by construction — that IS the
     fourth row, and it is a finding rather than a failure of the method)
   ✗ separate two simultaneous faults: the classifier reports MIXED when more
     than one perturbation moves the count, and MIXED is honest, not a diagnosis
   ✗ detect a synchroniser that is too shallow — invisible in zero-delay RTL
   ✗ distinguish a logic bug whose trigger happens to be phase-dependent from a
     crossing defect; nothing in real hardware makes logic phase-dependent, so
     the method assumes that and the assumption is worth knowing
   ✗ tell you WHICH crossing, which line of RTL, or which constraint

The last one matters most in practice. This method narrows a failure to a layer and stops there. What it hands over is the applicable tool — and the six chapters before it are what you reach for once the layer is known.

13. Why an FPGA Engineer Cares

All three perturbations are things you can change from a script on real hardware. The clock divider is a register write. The payload is a constant. The bench's data placement has no hardware analogue, which is itself informative: on a board, a failure that moves with nothing but the clock divider is a crossing or a timing problem and cannot be a bench artefact.

The pulse-versus-toggle lesson is the most directly actionable thing here. If you have a signal crossing from an SPI clock domain to a system clock domain, check whether it is a pulse, and check whether its width is guaranteed to exceed the receiving clock's period. If either answer is no, the bug is already present and is waiting for a clock-frequency change to expose it — which is exactly what happens when somebody raises the SPI rate for throughput and the design "suddenly" breaks.

And the counting lesson: instrument transfer counts at both ends, not just data. A lost transfer is silent in every data check you will write.

14. Why an ASIC Engineer Cares

The four layers map onto four different sign-offs and four different owners, and getting the layer wrong wastes the most expensive weeks in a project.

The rate-invariance test belongs in the regression because it is the only one of the four perturbations that a simulation can apply to silicon behaviour by proxy. The synchroniser-depth question belongs to a CDC tool and cannot be answered by any amount of simulation. The constraint question belongs to STA, and the reason this chapter labours the fourth row is that a green regression is routinely presented as evidence that the constraints are fine — it is not evidence of anything about them.

The most valuable habit is the two-way inference in section 9. When a lab failure does not reproduce, that is a result: three of four layers are eliminated and the remaining work is timing, physical, or CDC analysis. Treating it as cannot reproduce, closing is how a silicon bug survives to the next revision.

15. Failure Signature — Six Weeks In The Wrong Layer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom     an SPI link drops occasional transfers at 20 MHz; clean at 5 MHz
   Assumed     marginal timing -- it gets better when you slow down
   Actioned    board respin with shorter traces and series termination
   Result      unchanged
   Then        the SPI rate was lowered permanently and the product shipped
   Actual      a request PULSE crossing into the system clock domain, narrower
               than the receiving clock period at 20 MHz and wider at 5 MHz
   Fix         one line: a toggle instead of a pulse
   Found       by an engineer who ran the RTL regression at two SCLK periods
               and saw the transfer COUNT change while no word was ever wrong

It gets better when you slow down is consistent with marginal timing and with a pulse-width crossing bug, and the two are in different layers with different owners. The perturbation that separates them is the same one in both cases — change the rate — but the observation differs: marginal timing corrupts words, and a narrow-pulse crossing loses transfers. The team never looked at the transfer count, so the distinguishing observation was never made.

The board respin was a reasonable action taken on an incomplete diagnosis, and the permanent rate reduction was a workaround that worked — which, as Chapter 18.2 and Chapter 18.6 both found, is the most durable way to lose a bug.

16. Common Misconceptions

MisconceptionWhat is actually true
A passing regression means the design is rightIt means nothing about constraints, synchroniser depth, or physical timing
"Cannot reproduce in simulation" is a dead endIt eliminates three of four layers; it is a positive result
A scoreboard comparing words catches everythingIt is blind to a transfer that never arrived
Slowing the clock down implies marginal timingIt equally implies a pulse-width crossing defect
A CDC bug needs a CDC tool to findPulse-width defects are fully visible in RTL; depth defects are not
Any clock ratio will do for a rate sweepA ratio that is an exact multiple gives every frame the same phase
The bench is the last thing to suspectIt fails 100% of transfers, which is the easiest signature to recognise
Localising the layer is most of the workIt is the first of the work; the layer selects which tool applies

17. Reason It Through

18. Understanding Check

19. Summary

A failure can be localised to a layer by how it responds to perturbations rather than by anything visible in one capture. Three perturbations — the clock ratio, the instant the bench presents data, and the payload sequence — leave a correct design at zero failures, and that row is what makes any response to them informative at all.

The matrix came out diagonal by measurement. A logic bug with a data-dependent trigger moved with the payload alone, 7 failures against 5. A crossing defect moved with the clock ratio alone, 8 against 0, and its signature was 8 lost transfers with not one corrupted word — a scoreboard comparing payloads would have called that run perfect, which is why the failure metric has to include transfers that never arrived. A bench fault moved with the sampling instant alone, and failed 20 of 20, which is the easiest signature in the whole module to recognise.

The loss rate was predicted rather than observed: a request pulse 6 ns wide crossing into a 10 ns clock is caught with probability 6/10, so 8 of 20 frames should be lost and 8 were. That check required an honest experiment — a frame period that is not a multiple of the receiving clock period, so the phase actually drifts.

And the fourth row is the one to carry away. A constraint-class fault produced the identical signature to a correct design, zero in every column, because RTL has no delays and a violated input-delay budget is not expressible in it. A passing RTL regression is not evidence that a constraint is right — and a failure that does not reproduce in simulation is evidence for that class rather than an absence of information.

20. The Module, Closed

Seven chapters, one method. A capture is evidence; a cause is a hypothesis; and the useful work of a debug session is the measurement that converts one into the other. Each chapter found the measurement for one fault family, and each one also measured its own blind spot — which is what separates a diagnostic from a guess with a label.

The discriminators got progressively more expensive, and that ladder is the module's real content:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   18.1  one capture, classified                 a predicate
   18.2  one capture, plus the receiver's report  a transition TIME
   18.3  one capture, factored into two sides     a relation, and a graded PATTERN
   18.4  TWO captures with different stimulus     a displacement, then a second address
   18.5  a SEQUENCE of frames                     a count of assertions, one frame late
   18.6  two runs with a different SYSTEM         an INTERVENTION: slow the clock
   18.7  four runs across three axes              a SIGNATURE, and one invisible layer

Knowing which rung a problem sits on is most of the skill, and the cost of getting it wrong is always the same: effort spent at a lower rung than the fault requires. Each rung also costs more than the one below — a second capture is a line of script, a change of clock rate may mean re-qualifying a link — so the order matters in both directions. Start too high and you spend a day proving something a single capture would have shown; stay too low and you spend a week proving nothing at all.

Module 19 turns from finding faults to shipping designs: SPI as it actually appears in a system — a streaming converter with a sample deadline, a register-mapped sensor, a controller behind a register bus, and a multi-slave arbiter — each one reusing the timing, crossing and RTL reasoning this track has already built, and each one a place where the faults in this module are committed.

Continue learning