Skip to content
VLSI Mentor

SPI · Module 18

A Systematic Waveform Debug Method

A capture is evidence and a cause is a hypothesis; the useful work of a debug session is the measurement that converts one into the other. Five evidence classes made mutually exclusive by a published priority, with two captures that set two predicates at once so the priority is shown to be load-bearing.

Modules 16 and 17 built an environment that decides whether a design is right. This module is about the situation that environment leaves you in: something is wrong, you have a capture, and you have to find out what.

"The data is wrong" is not a debugging step. It is the sentence you say before debugging starts.

1. The Step Everybody Skips

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   symptom  →  EVIDENCE  →  cause  →  confirmation  →  fix

Five steps, and the one that gets skipped is the second. A capture is evidence. A cause is a hypothesis. The work of a debug session is the measurement that converts one into the other — and jumping from the data is wrong straight to the mode must be wrong is not debugging, it is guessing with a waveform open.

The cost of skipping it is specific and familiar: two engineers disagree about the cause, both point at the same waveform, and neither can name the observation that would settle it. They are arguing about hypotheses because nobody classified the evidence.

So this chapter's artefact produces evidence and refuses to name a cause. Every later chapter in the module supplies discriminators that turn one class of evidence into one cause; this one establishes what the classes are and, more importantly, what makes a set of classes usable at all.

2. Five Classes

ClassWhat the capture shows
EV_QUIETa select with no SCLK edge at all
EV_COUNTthe edge count is not 2N
EV_CSBNDa CS boundary is too tight — the lead or the lag is below its minimum
EV_UNSTABLEMOSI moved at a capture edge
EV_WELLnone of the above — the capture is admissible as evidence about data

Note what is absent. No class says wrong mode, bit-order mismatch, or dummy-cycle error. Those are causes, and none of them is decidable from one capture without a comparison. EV_WELL does not mean correct; it means the capture is trustworthy enough that looking at the payload is now worthwhile.

3. The Priority Is The Chapter

A single fault can satisfy two class predicates at once. A master that releases the select early both truncates the edge count and shortens the lag. So the classes are not mutually exclusive by nature — they are made mutually exclusive by a priority, and the priority is ordered by how much of the capture each finding invalidates:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1. EV_QUIET      no edges — there is no frame, so nothing about the data means
                    anything.
   2. EV_COUNT      the frame boundaries are unknown, so every bit POSITION is
                    meaningless. A data comparison here is unreadable, not wrong.
   3. EV_CSBND      the frame is well formed, and its edges sit where a slave may
                    not have been listening — so the data may be right and
                    UNREPEATABLE.
   4. EV_UNSTABLE   the frame and its boundaries are sound and one sampled bit is
                    unreliable.
   5. EV_WELL       the capture is admissible as evidence about data.

Read downwards, that is a list of reasons to stop looking at the payload. Read upwards, it is the order in which a capture becomes trustworthy.

4. Where Each Predicate Is Evaluated

The four predicates, and when each one is decided

14 cycles
Four rows over fourteen cycles. Chip select falls, SCLK produces four edges after a lead of four cycles, MOSI holds a bit per bit-time, and an evidence row names which predicate is being evaluated at each instant: the lead at the first edge, the count across the frame, stability at each capture, and the lag at the release.select fallsselect fallsfirst edge: lead measuredfirst edge: lead measuredcapture: stability judgedcapture: stability judgedrelease: lag + verdictrelease: lag + verdictcs_nsclkmosiXb0b0b0b0b0b0b0b1b1b1b1XXevidenceleadcntcntcntcntcntcntlaglaglagt0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 1 — one capture, and the four instants at which the four predicates are evaluated. The lead is measured at the FIRST edge, because that is the only moment it exists; the edge count accumulates across the frame; stability is judged at each capture edge; and the lag is measured at the release, which is also when the verdict is issued. A capture with no edges never reaches three of the four.

Three things in that figure are the method made visible. The lead exists only at the first edge — which is why a quiet select has no lead to be wrong about, and why EV_QUIET outranks EV_CSBND rather than sitting beside it. The verdict is issued at the release, because that is the first instant a transaction is known to be over. And the evidence row is the decoder's own state, not a signal on the bus: it is what a waveform viewer cannot show you, which is precisely why the classification is worth building in logic rather than performing by eye.

5. The Measurement

Six captures, one decoder. The predicates column is the raw vector in the order unstable · csbnd · count · quiet.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   edges  predicates  class       expected     stimulus
       8        0000  WELL        WELL         legal, 4 bits
       0        0011  QUIET       QUIET        a select with no SCLK edge
       6        0010  COUNT       COUNT        two edges short
       8        0100  CSBND       CSBND        the lead one cycle short
       8        1000  UNSTABLE    UNSTABLE     MOSI moving at a capture
       7        0110  COUNT       COUNT        an early release

Five single-predicate faults land in five different classes, and legal traffic in EV_WELL. That diagonal is the minimum a triage table has to earn, because a class reachable by two unrelated faults is not evidence — it is a hypothesis with a label.

Two overlaps, for two different reasons

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a select with no SCLK edge   0011   quiet + count
   an early release             0110   count + csbnd

These are not the same kind of overlap, and the difference matters:

The quiet select's overlap is inherent. Zero edges is necessarily not 2N, so quiet and count can never be separated by any decoder. The priority is the only thing that decides the label, and no refinement will change that.

The early release's overlap is avoidable. A count fault and a CS-boundary fault happen to arrive from one cause here, and a finer decoder could have reported "seven edges and a one-cycle lag" as a compound class. This one does not — and that is a deliberate design choice with a cost, which is exactly the sort of thing a published predicate vector lets a reader audit.

6. Building It — Three HDLs

Azvya Education Pvt. Ltd.VLSI Mentor
spi_triage.sv — the triage decoder — five evidence classes made exclusive by a published priority
// spi_triage.sv
//
// Chapter 18.1 -- the triage decoder: the first step of a waveform debug, made executable.
//
// WHY THIS IS A HARDWARE MODULE AND NOT A DOCUMENT.
//
// A debug method is usually written as a checklist, and a checklist cannot be wrong in a way
// anybody notices. This one is a synthesizable monitor -- counters, comparators and registers, no
// tri-state and no simulation-only constructs -- because that makes its classification testable,
// and because it is the artefact you put INTO an FPGA when the bug only happens on the board and
// the simulator says everything is fine.
//
// THE METHOD, IN THE ORDER IT HAS TO HAPPEN.
//
//     symptom  ->  EVIDENCE  ->  cause  ->  confirmation  ->  fix
//
// The step everybody skips is the second one. 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. Jumping
// from "the data is wrong" straight to "the mode must be wrong" is not debugging, it is guessing
// with a waveform open.
//
// So this module produces EVIDENCE and refuses to name a cause. It reports which of five classes a
// captured transaction falls into:
//
//     EV_QUIET      a select with no SCLK edge at all
//     EV_COUNT      the edge count is not 2N
//     EV_CSBND      a CS boundary is too tight -- lead or lag below the minimum
//     EV_UNSTABLE   MOSI moved AT a capture edge
//     EV_WELL       none of the above
//
// THE CLASSES ARE MUTUALLY EXCLUSIVE BY PRIORITY, NOT BY NATURE, AND THAT IS THE CHAPTER.
//
// A single fault can satisfy two predicates at once: a master that releases the select early both
// truncates the edge count and shortens the lag. So the decoder also publishes the raw PREDICATE
// VECTOR alongside the assigned class, and the testbench prints both. A triage table that hides its
// priority produces arguments about classification instead of debugging.
//
// The priority is ordered by HOW MUCH OF THE CAPTURE EACH FINDING INVALIDATES:
//
//   1. no edges       -- nothing about the data means anything; there is no frame.
//   2. wrong count    -- the frame boundaries are unknown, so every bit POSITION is meaningless,
//                        which makes a data comparison unreadable rather than wrong.
//   3. CS boundary    -- the frame is well-formed but its edges sit where a slave may not have
//                        been listening, so the data may be right and unrepeatable.
//   4. data unstable  -- the frame and its boundaries are sound and one sampled bit is unreliable.
//   5. well formed    -- the capture is admissible as evidence about data.
//
// Read downwards, that is a list of reasons to stop looking at the payload. Read upwards, it is the
// order in which a capture becomes trustworthy.

`timescale 1ns/1ps

module spi_triage #(
    parameter int LEAD  = 4,     // cycles, CS assert to first edge
    parameter int LAG   = 2,     // cycles, last edge to CS release
    parameter int LEN_W = 6,
    parameter int CNT_W = 16
) (
    input  wire              clk,      // the OBSERVER's clock -- Chapter 17.2's result
    input  wire              rst_n,

    // --- the pins, and nothing else -----------------------------------------
    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,

    // --- the configuration the capture is judged against --------------------
    input  wire              cpol,
    input  wire              cpha,
    input  wire [LEN_W-1:0]  len,

    // --- the verdict, once per transaction ----------------------------------
    output reg               ev_valid,
    output reg  [2:0]        ev_class,
    // THE RAW PREDICATES, published so the priority is visible rather than implied. Bit per
    // predicate, in the same order as the classes.
    output reg  [3:0]        ev_pred,
    output reg  [CNT_W-1:0]  ev_edges,

    input  wire              clr,
    output reg  [CNT_W-1:0]  n_quiet,
    output reg  [CNT_W-1:0]  n_count,
    output reg  [CNT_W-1:0]  n_csbnd,
    output reg  [CNT_W-1:0]  n_unstable,
    output reg  [CNT_W-1:0]  n_well
);

    localparam [2:0] EV_QUIET    = 3'd0,
                     EV_COUNT    = 3'd1,
                     EV_CSBND    = 3'd2,
                     EV_UNSTABLE = 3'd3,
                     EV_WELL     = 3'd4;

    // --- observed history ---------------------------------------------------
    reg sclk_d, cs_n_d, mosi_d;

    wire cs_assert   = ~cs_n &  cs_n_d;
    wire cs_deassert =  cs_n & ~cs_n_d;

    // An edge coincident with the release belongs to the transaction that is ENDING -- Chapter
    // 16.1's result, and the reason a prompt master is not reported as a partial frame.
    wire in_txn    = ~cs_n | cs_deassert;
    wire sclk_edge = sclk ^ sclk_d;
    wire leading   = sclk_edge & (sclk != cpol);
    wire capture   = (cpha ? (sclk_edge & ~leading) : leading) & in_txn;

    wire [LEN_W:0] edges_expected = {1'b0, len} << 1;

    // --- per-transaction accumulators ---------------------------------------
    reg [CNT_W-1:0] edges;
    reg [CNT_W-1:0] since_assert;    // cycles since the select fell
    reg [CNT_W-1:0] since_edge;      // cycles since the last SCLK edge
    reg             seen_edge;
    reg             p_lead_short;
    reg             p_unstable;

    // An interval whose ending event is happening on THIS cycle is zero, not whatever the counter
    // last held. Chapter 16.1 lost a whole rule to the other reading.
    wire [CNT_W-1:0] lag_now = sclk_edge ? {CNT_W{1'b0}} : since_edge;

    // The count AS IT WILL BE once this cycle's edge is folded in, because the accumulator updates
    // non-blockingly and the verdict is computed in the same cycle.
    wire [CNT_W-1:0] edges_now = (sclk_edge && in_txn) ? edges + 1'b1 : edges;

    // The four predicates, evaluated at the release.
    wire pr_quiet    = (edges_now == {CNT_W{1'b0}});
    wire pr_count    = (edges_now != {{(CNT_W-LEN_W-1){1'b0}}, edges_expected});
    wire pr_csbnd    = p_lead_short || (lag_now < LAG[CNT_W-1:0]);
    wire pr_unstable = p_unstable;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_d       <= 1'b0;
            cs_n_d       <= 1'b1;
            mosi_d       <= 1'b0;
            edges        <= {CNT_W{1'b0}};
            since_assert <= {CNT_W{1'b0}};
            since_edge   <= {CNT_W{1'b0}};
            seen_edge    <= 1'b0;
            p_lead_short <= 1'b0;
            p_unstable   <= 1'b0;
            ev_valid     <= 1'b0;
            ev_class     <= EV_WELL;
            ev_pred      <= 4'd0;
            ev_edges     <= {CNT_W{1'b0}};
            n_quiet      <= {CNT_W{1'b0}};
            n_count      <= {CNT_W{1'b0}};
            n_csbnd      <= {CNT_W{1'b0}};
            n_unstable   <= {CNT_W{1'b0}};
            n_well       <= {CNT_W{1'b0}};
        end else begin
            sclk_d   <= sclk;
            cs_n_d   <= cs_n;
            mosi_d   <= mosi;
            ev_valid <= 1'b0;

            if (clr) begin
                n_quiet    <= {CNT_W{1'b0}};
                n_count    <= {CNT_W{1'b0}};
                n_csbnd    <= {CNT_W{1'b0}};
                n_unstable <= {CNT_W{1'b0}};
                n_well     <= {CNT_W{1'b0}};
            end

            // --- interval counters ---------------------------------------
            if (cs_assert) since_assert <= {{(CNT_W-1){1'b0}}, 1'b1};
            else if (since_assert != {CNT_W{1'b1}}) since_assert <= since_assert + 1'b1;

            if (sclk_edge) since_edge <= {{(CNT_W-1){1'b0}}, 1'b1};
            else if (since_edge != {CNT_W{1'b1}}) since_edge <= since_edge + 1'b1;

            // --- per-transaction state -----------------------------------
            if (cs_assert) begin
                edges        <= {CNT_W{1'b0}};
                seen_edge    <= 1'b0;
                p_lead_short <= 1'b0;
                p_unstable   <= 1'b0;
            end else begin
                if (sclk_edge && in_txn) begin
                    edges <= edges + 1'b1;
                    // THE LEAD IS MEASURED AT THE FIRST EDGE, because that is the only moment it
                    // exists. A transaction with no edges has no lead to be wrong about, which is
                    // why EV_QUIET outranks EV_CSBND rather than sitting beside it.
                    if (!seen_edge) begin
                        seen_edge <= 1'b1;
                        if (since_assert < LEAD[CNT_W-1:0]) p_lead_short <= 1'b1;
                    end
                end
                // MOSI moving AT a capture edge is the finest-grained finding in the set: the frame
                // is sound and one bit is unreliable. Chapter 16.5 measured that a data monitor is
                // blind to it, which is why it has to be evidence in its own right.
                if (capture && (mosi !== mosi_d)) p_unstable <= 1'b1;
            end

            // --- the verdict, at the release ------------------------------
            if (cs_deassert) begin
                ev_valid <= 1'b1;
                ev_edges <= edges_now;
                ev_pred  <= {pr_unstable, pr_csbnd, pr_count, pr_quiet};

                // THE PRIORITY. Ordered by how much of the capture each finding invalidates, and
                // published above as a raw vector so a reader can see where the predicates overlap.
                if (pr_quiet) begin
                    ev_class <= EV_QUIET;    n_quiet    <= n_quiet    + 1'b1;
                end else if (pr_count) begin
                    ev_class <= EV_COUNT;    n_count    <= n_count    + 1'b1;
                end else if (pr_csbnd) begin
                    ev_class <= EV_CSBND;    n_csbnd    <= n_csbnd    + 1'b1;
                end else if (pr_unstable) begin
                    ev_class <= EV_UNSTABLE; n_unstable <= n_unstable + 1'b1;
                end else begin
                    ev_class <= EV_WELL;     n_well     <= n_well     + 1'b1;
                end
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_triage.v — the same design in Verilog-2001
// spi_triage.v
//
// Chapter 18.1 -- the triage decoder: the first step of a waveform debug, made executable.
//
// WHY THIS IS A HARDWARE MODULE AND NOT A DOCUMENT.
//
// A debug method is usually written as a checklist, and a checklist cannot be wrong in a way
// anybody notices. This one is a synthesizable monitor -- counters, comparators and registers, no
// tri-state and no simulation-only constructs -- because that makes its classification testable,
// and because it is the artefact you put INTO an FPGA when the bug only happens on the board and
// the simulator says everything is fine.
//
// THE METHOD, IN THE ORDER IT HAS TO HAPPEN.
//
//     symptom  ->  EVIDENCE  ->  cause  ->  confirmation  ->  fix
//
// The step everybody skips is the second one. 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. Jumping
// from "the data is wrong" straight to "the mode must be wrong" is not debugging, it is guessing
// with a waveform open.
//
// So this module produces EVIDENCE and refuses to name a cause. It reports which of five classes a
// captured transaction falls into:
//
//     EV_QUIET      a select with no SCLK edge at all
//     EV_COUNT      the edge count is not 2N
//     EV_CSBND      a CS boundary is too tight -- lead or lag below the minimum
//     EV_UNSTABLE   MOSI moved AT a capture edge
//     EV_WELL       none of the above
//
// THE CLASSES ARE MUTUALLY EXCLUSIVE BY PRIORITY, NOT BY NATURE, AND THAT IS THE CHAPTER.
//
// A single fault can satisfy two predicates at once: a master that releases the select early both
// truncates the edge count and shortens the lag. So the decoder also publishes the raw PREDICATE
// VECTOR alongside the assigned class, and the testbench prints both. A triage table that hides its
// priority produces arguments about classification instead of debugging.
//
// The priority is ordered by HOW MUCH OF THE CAPTURE EACH FINDING INVALIDATES:
//
//   1. no edges       -- nothing about the data means anything; there is no frame.
//   2. wrong count    -- the frame boundaries are unknown, so every bit POSITION is meaningless,
//                        which makes a data comparison unreadable rather than wrong.
//   3. CS boundary    -- the frame is well-formed but its edges sit where a slave may not have
//                        been listening, so the data may be right and unrepeatable.
//   4. data unstable  -- the frame and its boundaries are sound and one sampled bit is unreliable.
//   5. well formed    -- the capture is admissible as evidence about data.
//
// Read downwards, that is a list of reasons to stop looking at the payload. Read upwards, it is the
// order in which a capture becomes trustworthy.

`timescale 1ns/1ps

module spi_triage #(
    parameter LEAD  = 4,     // cycles, CS assert to first edge
    parameter LAG   = 2,     // cycles, last edge to CS release
    parameter LEN_W = 6,
    parameter CNT_W = 16
) (
    input  wire              clk,      // the OBSERVER's clock -- Chapter 17.2's result
    input  wire              rst_n,

    // --- the pins, and nothing else -----------------------------------------
    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,

    // --- the configuration the capture is judged against --------------------
    input  wire              cpol,
    input  wire              cpha,
    input  wire [LEN_W-1:0]  len,

    // --- the verdict, once per transaction ----------------------------------
    output reg               ev_valid,
    output reg  [2:0]        ev_class,
    // THE RAW PREDICATES, published so the priority is visible rather than implied. Bit per
    // predicate, in the same order as the classes.
    output reg  [3:0]        ev_pred,
    output reg  [CNT_W-1:0]  ev_edges,

    input  wire              clr,
    output reg  [CNT_W-1:0]  n_quiet,
    output reg  [CNT_W-1:0]  n_count,
    output reg  [CNT_W-1:0]  n_csbnd,
    output reg  [CNT_W-1:0]  n_unstable,
    output reg  [CNT_W-1:0]  n_well
);

    localparam [2:0] EV_QUIET    = 3'd0,
                     EV_COUNT    = 3'd1,
                     EV_CSBND    = 3'd2,
                     EV_UNSTABLE = 3'd3,
                     EV_WELL     = 3'd4;

    // --- observed history ---------------------------------------------------
    reg sclk_d, cs_n_d, mosi_d;

    wire cs_assert   = ~cs_n &  cs_n_d;
    wire cs_deassert =  cs_n & ~cs_n_d;

    // An edge coincident with the release belongs to the transaction that is ENDING -- Chapter
    // 16.1's result, and the reason a prompt master is not reported as a partial frame.
    wire in_txn    = ~cs_n | cs_deassert;
    wire sclk_edge = sclk ^ sclk_d;
    wire leading   = sclk_edge & (sclk != cpol);
    wire capture   = (cpha ? (sclk_edge & ~leading) : leading) & in_txn;

    wire [LEN_W:0] edges_expected = {1'b0, len} << 1;

    // --- per-transaction accumulators ---------------------------------------
    reg [CNT_W-1:0] edges;
    reg [CNT_W-1:0] since_assert;    // cycles since the select fell
    reg [CNT_W-1:0] since_edge;      // cycles since the last SCLK edge
    reg             seen_edge;
    reg             p_lead_short;
    reg             p_unstable;

    // An interval whose ending event is happening on THIS cycle is zero, not whatever the counter
    // last held. Chapter 16.1 lost a whole rule to the other reading.
    wire [CNT_W-1:0] lag_now = sclk_edge ? {CNT_W{1'b0}} : since_edge;

    // The count AS IT WILL BE once this cycle's edge is folded in, because the accumulator updates
    // non-blockingly and the verdict is computed in the same cycle.
    wire [CNT_W-1:0] edges_now = (sclk_edge && in_txn) ? edges + 1'b1 : edges;

    // The four predicates, evaluated at the release.
    wire pr_quiet    = (edges_now == {CNT_W{1'b0}});
    wire pr_count    = (edges_now != {{(CNT_W-LEN_W-1){1'b0}}, edges_expected});
    wire pr_csbnd    = p_lead_short || (lag_now < LAG[CNT_W-1:0]);
    wire pr_unstable = p_unstable;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_d       <= 1'b0;
            cs_n_d       <= 1'b1;
            mosi_d       <= 1'b0;
            edges        <= {CNT_W{1'b0}};
            since_assert <= {CNT_W{1'b0}};
            since_edge   <= {CNT_W{1'b0}};
            seen_edge    <= 1'b0;
            p_lead_short <= 1'b0;
            p_unstable   <= 1'b0;
            ev_valid     <= 1'b0;
            ev_class     <= EV_WELL;
            ev_pred      <= 4'd0;
            ev_edges     <= {CNT_W{1'b0}};
            n_quiet      <= {CNT_W{1'b0}};
            n_count      <= {CNT_W{1'b0}};
            n_csbnd      <= {CNT_W{1'b0}};
            n_unstable   <= {CNT_W{1'b0}};
            n_well       <= {CNT_W{1'b0}};
        end else begin
            sclk_d   <= sclk;
            cs_n_d   <= cs_n;
            mosi_d   <= mosi;
            ev_valid <= 1'b0;

            if (clr) begin
                n_quiet    <= {CNT_W{1'b0}};
                n_count    <= {CNT_W{1'b0}};
                n_csbnd    <= {CNT_W{1'b0}};
                n_unstable <= {CNT_W{1'b0}};
                n_well     <= {CNT_W{1'b0}};
            end

            // --- interval counters ---------------------------------------
            if (cs_assert) since_assert <= {{(CNT_W-1){1'b0}}, 1'b1};
            else if (since_assert != {CNT_W{1'b1}}) since_assert <= since_assert + 1'b1;

            if (sclk_edge) since_edge <= {{(CNT_W-1){1'b0}}, 1'b1};
            else if (since_edge != {CNT_W{1'b1}}) since_edge <= since_edge + 1'b1;

            // --- per-transaction state -----------------------------------
            if (cs_assert) begin
                edges        <= {CNT_W{1'b0}};
                seen_edge    <= 1'b0;
                p_lead_short <= 1'b0;
                p_unstable   <= 1'b0;
            end else begin
                if (sclk_edge && in_txn) begin
                    edges <= edges + 1'b1;
                    // THE LEAD IS MEASURED AT THE FIRST EDGE, because that is the only moment it
                    // exists. A transaction with no edges has no lead to be wrong about, which is
                    // why EV_QUIET outranks EV_CSBND rather than sitting beside it.
                    if (!seen_edge) begin
                        seen_edge <= 1'b1;
                        if (since_assert < LEAD[CNT_W-1:0]) p_lead_short <= 1'b1;
                    end
                end
                // MOSI moving AT a capture edge is the finest-grained finding in the set: the frame
                // is sound and one bit is unreliable. Chapter 16.5 measured that a data monitor is
                // blind to it, which is why it has to be evidence in its own right.
                if (capture && (mosi !== mosi_d)) p_unstable <= 1'b1;
            end

            // --- the verdict, at the release ------------------------------
            if (cs_deassert) begin
                ev_valid <= 1'b1;
                ev_edges <= edges_now;
                ev_pred  <= {pr_unstable, pr_csbnd, pr_count, pr_quiet};

                // THE PRIORITY. Ordered by how much of the capture each finding invalidates, and
                // published above as a raw vector so a reader can see where the predicates overlap.
                if (pr_quiet) begin
                    ev_class <= EV_QUIET;    n_quiet    <= n_quiet    + 1'b1;
                end else if (pr_count) begin
                    ev_class <= EV_COUNT;    n_count    <= n_count    + 1'b1;
                end else if (pr_csbnd) begin
                    ev_class <= EV_CSBND;    n_csbnd    <= n_csbnd    + 1'b1;
                end else if (pr_unstable) begin
                    ev_class <= EV_UNSTABLE; n_unstable <= n_unstable + 1'b1;
                end else begin
                    ev_class <= EV_WELL;     n_well     <= n_well     + 1'b1;
                end
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_triage.vhd — the same design in VHDL
-- spi_triage.vhd
--
-- Chapter 18.1 -- the triage decoder: the first step of a waveform debug, made executable.
--
-- WHY THIS IS A HARDWARE MODULE AND NOT A DOCUMENT.
--
-- A debug method is usually written as a checklist, and a checklist cannot be wrong in a way
-- anybody notices. This one is a synthesizable monitor -- counters, comparators and registers, no
-- tri-state and no simulation-only constructs -- because that makes its classification testable,
-- and because it is the artefact you put INTO an FPGA when the bug only happens on the board and
-- the simulator says everything is fine.
--
-- THE METHOD, IN THE ORDER IT HAS TO HAPPEN.
--
--     symptom  ->  EVIDENCE  ->  cause  ->  confirmation  ->  fix
--
-- The step everybody skips is the second one. 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. Jumping
-- from "the data is wrong" straight to "the mode must be wrong" is not debugging, it is guessing
-- with a waveform open.
--
-- So this module produces EVIDENCE and refuses to name a cause. It reports which of five classes a
-- captured transaction falls into:
--
--     EV_QUIET      a select with no SCLK edge at all
--     EV_COUNT      the edge count is not 2N
--     EV_CSBND      a CS boundary is too tight -- lead or lag below the minimum
--     EV_UNSTABLE   MOSI moved AT a capture edge
--     EV_WELL       none of the above
--
-- THE CLASSES ARE MUTUALLY EXCLUSIVE BY PRIORITY, NOT BY NATURE, AND THAT IS THE CHAPTER.
--
-- A single fault can satisfy two predicates at once: a master that releases the select early both
-- truncates the edge count and shortens the lag. So the decoder also publishes the raw PREDICATE
-- VECTOR alongside the assigned class, and the testbench prints both. A triage table that hides its
-- priority produces arguments about classification instead of debugging.
--
-- The priority is ordered by HOW MUCH OF THE CAPTURE EACH FINDING INVALIDATES:
--
--   1. no edges       -- nothing about the data means anything; there is no frame.
--   2. wrong count    -- the frame boundaries are unknown, so every bit POSITION is meaningless,
--                        which makes a data comparison unreadable rather than wrong.
--   3. CS boundary    -- the frame is well-formed but its edges sit where a slave may not have
--                        been listening, so the data may be right and unrepeatable.
--   4. data unstable  -- the frame and its boundaries are sound and one sampled bit is unreliable.
--   5. well formed    -- the capture is admissible as evidence about data.
--
-- Read downwards, that is a list of reasons to stop looking at the payload. Read upwards, it is the
-- order in which a capture becomes trustworthy.

--
-- WHAT VHDL ADDS HERE: the evidence class is an ENUMERATION and the predicate set is a RECORD of
-- booleans, so the priority chain reads as a sequence of named findings rather than a cascade of
-- bit tests -- and a class added to the type without a branch in the chain is an analysis-time
-- error rather than a capture silently filed under the default.
--
-- The predicate record also removes the bit-ordering question entirely. In the SystemVerilog and
-- Verilog versions the published vector is `{unstable, csbnd, count, quiet}` and a reader has to be
-- told which end is which; here each finding has a name at the point of use.
--
-- IDENTIFIER REVIEW (VHDL is CASE-INSENSITIVE, so `LEAD` and `lead` are the same name). Every
-- generic in this entity is spelled in upper case and no signal, variable, constant, port, function
-- argument or alias reuses any of those spellings in any case: the generics are LEAD_C, LAG_C,
-- LEN_W; the interval counters are `since_assert` and `since_edge`; the predicate fields are
-- `quiet`, `wrong_count`, `cs_bound`, `unstable`. `LEAD_C` rather than `LEAD` specifically because
-- `lead` is a natural name for a measured interval and the collision would be silent.

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

package spi_triage_pkg is

    -- The five evidence classes, ordered by how much of a capture each finding invalidates. A class
    -- added here without a branch in the priority chain is a case-statement error at analysis time.
    type ev_class_t is (EV_QUIET, EV_COUNT, EV_CSBND, EV_UNSTABLE, EV_WELL);

    -- The raw predicates, published alongside the class so the priority is visible rather than
    -- implied. Named fields rather than a packed vector: there is no bit order to explain.
    type ev_pred_t is record
        quiet       : boolean;
        wrong_count : boolean;
        cs_bound    : boolean;
        unstable    : boolean;
    end record;

    constant PRED_NONE : ev_pred_t := (false, false, false, false);

    type ev_counts_t is record
        quiet    : natural;
        wrong    : natural;
        cs_bound : natural;
        unstable : natural;
        well     : natural;
    end record;

    constant COUNTS_ZERO : ev_counts_t := (0, 0, 0, 0, 0);

    function class_name (c : ev_class_t) return string;
    function pred_bits  (p : ev_pred_t) return string;

    -- Right-justify a short string in a field. The column widths in this chapter's bench have to MATCH
    -- the SystemVerilog and Verilog benches exactly, because the module's regression compares the three
    -- transcripts byte for byte -- and three benches that pass while printing different columns are three
    -- different experiments, not one result confirmed three times. `%-30s` is not portable across these
    -- simulators, so every column is justified by hand and the free text sits at the END of a row.
    function rj (t : string; w : natural) return string;

end package spi_triage_pkg;

package body spi_triage_pkg is

    function class_name (c : ev_class_t) return string is
    begin
        case c is
            when EV_QUIET    => return "QUIET   ";
            when EV_COUNT    => return "COUNT   ";
            when EV_CSBND    => return "CSBND   ";
            when EV_UNSTABLE => return "UNSTABLE";
            when others      => return "WELL    ";
        end case;
    end function class_name;

    -- Printed in the same order as the SystemVerilog vector -- unstable, cs_bound, wrong_count,
    -- quiet -- so the three language versions produce comparable tables.
    function pred_bits (p : ev_pred_t) return string is
        variable s : string(1 to 4) := "0000";
    begin
        if p.unstable    then s(1) := '1'; end if;
        if p.cs_bound    then s(2) := '1'; end if;
        if p.wrong_count then s(3) := '1'; end if;
        if p.quiet       then s(4) := '1'; end if;
        return s;
    end function pred_bits;

    function rj (t : string; w : natural) return string is
        constant P : string(1 to 40) := (others => ' ');
    begin
        if t'length >= w then return t; end if;
        return P(1 to w - t'length) & t;
    end function rj;

end package body spi_triage_pkg;

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

entity spi_triage is
    generic (
        -- LEAD_C and LAG_C rather than LEAD and LAG: VHDL is case-insensitive, `lead` and `lag` are
        -- natural names for the intervals this entity measures, and the collision would be silent.
        LEAD_C : natural  := 4;    -- cycles, CS assert to first edge
        LAG_C  : natural  := 2;    -- cycles, last edge to CS release
        LEN_W  : positive := 6
    );
    port (
        clk       : in  std_logic;    -- the OBSERVER's clock -- Chapter 17.2's result
        rst_n     : in  std_logic;

        -- the pins, and nothing else
        sclk      : in  std_logic;
        cs_n      : in  std_logic;
        mosi      : in  std_logic;

        -- the configuration the capture is judged against
        cpol      : in  std_logic;
        cpha      : in  std_logic;
        len       : in  unsigned(LEN_W - 1 downto 0);

        -- the verdict, once per transaction
        ev_valid  : out std_logic;
        ev_class  : out ev_class_t;
        ev_pred   : out ev_pred_t;
        ev_edges  : out natural;

        clr       : in  std_logic;
        counts    : out ev_counts_t
    );
end entity spi_triage;

architecture rtl of spi_triage is
    signal v_r : std_logic   := '0';
    signal c_r : ev_class_t  := EV_WELL;
    signal p_r : ev_pred_t   := PRED_NONE;
    signal e_r : natural     := 0;
    signal n_r : ev_counts_t := COUNTS_ZERO;
begin

    ev_valid <= v_r;
    ev_class <= c_r;
    ev_pred  <= p_r;
    ev_edges <= e_r;
    counts   <= n_r;

    process (clk, rst_n) is
        variable sclk_d, cs_n_d, mosi_d : std_logic;
        variable cs_assert, cs_deassert : boolean;
        variable in_txn, sclk_edge      : boolean;
        variable leading, capture       : boolean;
        variable edges                  : natural;
        variable since_assert           : natural;
        variable since_edge             : natural;
        variable seen_edge              : boolean;
        variable lead_short             : boolean;
        variable data_moved             : boolean;
        variable edges_now, lag_now     : natural;
        variable first_edge             : boolean;
        variable lead_now               : natural;
        variable pr                     : ev_pred_t;
    begin
        if rst_n = '0' then
            sclk_d := '0'; cs_n_d := '1'; mosi_d := '0';
            edges := 0; since_assert := 0; since_edge := 0;
            seen_edge := false; lead_short := false; data_moved := false;
            v_r <= '0'; c_r <= EV_WELL; p_r <= PRED_NONE; e_r <= 0;
            n_r <= COUNTS_ZERO;

        elsif rising_edge(clk) then
            v_r <= '0';

            cs_assert   := (cs_n = '0') and (cs_n_d = '1');
            cs_deassert := (cs_n = '1') and (cs_n_d = '0');
            -- An edge coincident with the release belongs to the transaction that is ENDING --
            -- Chapter 16.1's rule, and the reason a prompt master is not reported as a partial
            -- frame.
            in_txn      := (cs_n = '0') or cs_deassert;
            sclk_edge   := (sclk /= sclk_d);
            leading     := sclk_edge and (sclk /= cpol);
            if cpha = '0' then capture := leading and in_txn;
            else               capture := sclk_edge and (not leading) and in_txn;
            end if;

            if clr = '1' then
                n_r <= COUNTS_ZERO;
            end if;

            -- ==========================================================
            -- THE VERDICT READS THE PRE-UPDATE STATE, AND THE STATE IS UPDATED AFTERWARDS.
            --
            -- That ordering is not stylistic. In the SystemVerilog and Verilog versions these
            -- counters are REGISTERS, so everything read during a cycle sees the value the previous
            -- cycle left. Here they are process VARIABLES, and a variable incremented before the
            -- verdict is ONE CYCLE AHEAD of the register it is modelling.
            --
            -- The first version of this file incremented first. The three languages then disagreed
            -- on exactly one stimulus -- an early release, where the measured lag came out one
            -- cycle longer and the CS-boundary predicate did not fire -- so the same capture was
            -- classified from two predicates in one language and one in another. It is Chapter
            -- 17.2's delta-stale derived signal by a different mechanism: where a value is computed
            -- decides which INSTANT it describes.
            -- ==========================================================
            first_edge := sclk_edge and in_txn and (not seen_edge);
            lead_now   := since_assert;

            if sclk_edge and in_txn then edges_now := edges + 1; else edges_now := edges; end if;
            if sclk_edge               then lag_now := 0;        else lag_now := since_edge; end if;

            if cs_deassert then
                pr.quiet       := (edges_now = 0);
                pr.wrong_count := (edges_now /= 2 * to_integer(len));
                pr.cs_bound    := lead_short or (lag_now < LAG_C);
                pr.unstable    := data_moved;

                v_r <= '1';
                e_r <= edges_now;
                p_r <= pr;

                -- THE PRIORITY. Ordered by how much of the capture each finding invalidates, and
                -- published above as a named record so a reader can see where the predicates
                -- overlap.
                if pr.quiet then
                    c_r <= EV_QUIET;    n_r.quiet    <= n_r.quiet + 1;
                elsif pr.wrong_count then
                    c_r <= EV_COUNT;    n_r.wrong    <= n_r.wrong + 1;
                elsif pr.cs_bound then
                    c_r <= EV_CSBND;    n_r.cs_bound <= n_r.cs_bound + 1;
                elsif pr.unstable then
                    c_r <= EV_UNSTABLE; n_r.unstable <= n_r.unstable + 1;
                else
                    c_r <= EV_WELL;     n_r.well     <= n_r.well + 1;
                end if;
            end if;

            -- --- state update, which is what the registers do at the end of a cycle ---
            if cs_assert then
                edges := 0; seen_edge := false; lead_short := false; data_moved := false;
            else
                -- THE LEAD IS MEASURED AT THE FIRST EDGE, because that is the only moment it
                -- exists. A transaction with no edges has no lead to be wrong about, which is why
                -- EV_QUIET outranks EV_CSBND rather than sitting beside it.
                if first_edge then
                    seen_edge := true;
                    if lead_now < LEAD_C then lead_short := true; end if;
                end if;
                if sclk_edge and in_txn then edges := edges + 1; end if;
                -- MOSI moving AT a capture edge is the finest-grained finding in the set: the frame
                -- is sound and one bit is unreliable. Chapter 16.5 measured that a data monitor is
                -- blind to it, which is why it has to be evidence in its own right.
                if capture and (mosi /= mosi_d) then data_moved := true; end if;
            end if;

            if cs_assert then since_assert := 1; else since_assert := since_assert + 1; end if;
            if sclk_edge then since_edge   := 1; else since_edge   := since_edge + 1;   end if;

            sclk_d := sclk;
            cs_n_d := cs_n;
            mosi_d := mosi;
        end if;
    end process;

end architecture rtl;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_triage_tb.sv — six captures, a diagonal, two overlaps, and a bench that proves its own checks are live
// spi_triage_tb.sv
//
// SIX CAPTURES THROUGH ONE TRIAGE DECODER, AND FOUR THINGS THE BENCH HAS TO PROVE ABOUT ITSELF
// BEFORE ITS RESULTS MEAN ANYTHING.
//
// THE MEASUREMENTS.
//
//   1. EACH SINGLE-PREDICATE FAULT LANDS IN ITS OWN CLASS. Five stimuli, five classes, plus legal
//      traffic in EV_WELL. This is the diagonal, and it is the minimum a triage table has to earn.
//
//   2. THE PREDICATE VECTOR IS PUBLISHED, AND ONE STIMULUS SETS TWO BITS. An early release both
//      truncates the edge count and shortens the lag, so `ev_pred` reads 0110 while `ev_class`
//      reads EV_COUNT. The classes are mutually exclusive BY PRIORITY, not by nature, and a table
//      that hides that produces arguments about classification instead of debugging.
//
//   3. THE CHECKER CAN FAIL. A deliberate wrong expectation is compared in exactly the same way as
//      the real ones and is required to MISMATCH. A self-checking bench that has never been shown
//      capable of failing is a bench that prints PASS, which is not the same thing.
//
//   4. NOTHING IS X. Every reported field is checked for a known value at the moment it is
//      reported. An X compared with `!=` yields X, `if (X)` is false, and a bench full of checks
//      then reports PASS while measuring nothing -- which is the failure mode this curriculum has
//      already been bitten by once.
//
// The bench drives the pins directly. Every stimulus here is a statement about the SHAPE of a
// capture, and a driver with a fault input reaches the shapes its faults were written for and no
// others.

`timescale 1ns/1ps

module spi_triage_tb;

    localparam int LEAD  = 4;
    localparam int LAG   = 2;
    localparam int GAP   = 3;
    localparam int HALF  = 3;
    localparam int LEN_W = 6;
    localparam int CNT_W = 16;

    localparam [2:0] EV_QUIET = 3'd0, EV_COUNT = 3'd1, EV_CSBND = 3'd2,
                     EV_UNSTABLE = 3'd3, EV_WELL = 3'd4;

    reg clk = 1'b0;
    always #5 clk = ~clk;
    reg rst_n = 1'b1;

    reg [LEN_W-1:0] len  = 6'd4;
    reg             cpol = 1'b0, cpha = 1'b0;

    reg b_sclk = 1'b0, b_cs_n = 1'b1, b_mosi = 1'b0;

    wire             ev_valid;
    wire [2:0]       ev_class;
    wire [3:0]       ev_pred;
    wire [CNT_W-1:0] ev_edges;
    reg              clr = 1'b0;
    wire [CNT_W-1:0] n_quiet, n_count, n_csbnd, n_unstable, n_well;

    spi_triage #(.LEAD(LEAD), .LAG(LAG), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_t (
        .clk(clk), .rst_n(rst_n),
        .sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
        .cpol(cpol), .cpha(cpha), .len(len),
        .ev_valid(ev_valid), .ev_class(ev_class), .ev_pred(ev_pred), .ev_edges(ev_edges),
        .clr(clr),
        .n_quiet(n_quiet), .n_count(n_count), .n_csbnd(n_csbnd),
        .n_unstable(n_unstable), .n_well(n_well)
    );

    integer errors = 0;

    initial begin
        #200_000;
        $display("FAIL: the simulation did not finish within its time limit");
        $finish;
    end

    // ------------------------------------------------------------------
    // The observer. It records the last reported verdict and -- measurement 4 -- checks every
    // reported field for a KNOWN value at the moment it is reported. `^field === 1'bx` is the
    // portable reduction-XOR test for "this vector contains an X or a Z".
    // ------------------------------------------------------------------
    integer      got_n;
    reg [2:0]    got_class;
    reg [3:0]    got_pred;
    integer      got_edges;
    integer      x_reports;

    always @(posedge clk) if (rst_n && ev_valid) begin
        got_n     = got_n + 1;
        got_class = ev_class;
        got_pred  = ev_pred;
        got_edges = ev_edges;
        if ((^ev_class === 1'bx) || (^ev_pred === 1'bx) || (^ev_edges === 1'bx))
            x_reports = x_reports + 1;
    end

    // ------------------------------------------------------------------
    // Pin driving. Every write is on the NEGEDGE -- Chapter 16.3's discipline -- because the
    // decoder samples on the posedge and a bench that writes there measures its own evaluation
    // order instead of the traffic.
    // ------------------------------------------------------------------
    task automatic idle_n(input integer n);
        integer i;
        begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
    endtask

    // One frame: `nb` bits at half-period `h`, with a lead of `lead_c`, `drop` edges removed from
    // the end, a deliberate MOSI change at capture edge `bad_cap`, a lag of `lag_c`, and an
    // optional EARLY RELEASE immediately after edge `cut_at`.
    //
    // NOTE WHERE SCLK IS PARKED. It is returned to its idle level one cycle AFTER the release, not
    // on the same cycle. An earlier version parked it together with the release, and because an
    // edge coincident with a release belongs to the ENDING transaction (Chapter 16.1's rule), that
    // parking toggle was counted as a ninth edge -- so a deliberately truncated 7-edge frame
    // reported 8 and the wrong predicate. The decoder was right and the bench was wrong, which is
    // the most common shape of a testbench bug in this whole curriculum.
    task automatic frame(input integer nb, input integer h, input integer lead_c,
                         input integer drop, input integer bad_cap, input integer lag_c,
                         input integer cut_at);
        integer e, total;
        reg cut;
        begin
            cut    = 1'b0;
            b_cs_n = 1'b0;
            b_mosi = 1'b0;
            idle_n(lead_c);
            total = 2*nb - drop;
            for (e = 0; e < total && !cut; e = e + 1) begin
                b_sclk = ~b_sclk;
                // CPHA=0 captures on the leading edge -- the EVEN ones. A change made on the same
                // negedge as the toggle is visible to the decoder in the same posedge it sees the
                // edge, which is exactly the coincidence EV_UNSTABLE is about.
                if (bad_cap == e) b_mosi = ~b_mosi;
                // A legal launch moves MOSI on the trailing (odd) edges, away from the capture.
                else if ((e % 2) == 1) b_mosi = ~b_mosi;
                if (cut_at == e) begin
                    // THE EARLY RELEASE: one cycle after the edge, so the lag is 1 and the frame
                    // is short. Two predicates from one fault.
                    idle_n(1);
                    cut = 1'b1;
                end else begin
                    idle_n(h);
                end
            end
            if (!cut) idle_n(lag_c);
            b_cs_n = 1'b1;
            idle_n(1);
            b_sclk = cpol;
            idle_n(GAP + 3);
        end
    endtask

    // A select that carries no SCLK edge at all.
    task automatic quiet_select;
        begin
            b_cs_n = 1'b0;
            idle_n(6);
            b_cs_n = 1'b1;
            idle_n(GAP + 4);
        end
    endtask

    // ------------------------------------------------------------------
    integer s, expect_class, expect_pred;
    integer diag_bad, overlap_seen, mutation_detected;
    reg [3:0] pred_log [0:5];
    reg [2:0] class_log [0:5];

    function automatic [8*10:1] cname(input [2:0] c);
        begin
            case (c)
                EV_QUIET:    cname = "QUIET     ";
                EV_COUNT:    cname = "COUNT     ";
                EV_CSBND:    cname = "CSBND     ";
                EV_UNSTABLE: cname = "UNSTABLE  ";
                default:     cname = "WELL      ";
            endcase
        end
    endfunction

    initial begin
        got_n = 0; x_reports = 0; diag_bad = 0; overlap_seen = 0; mutation_detected = 0;

        // The reset starts high and falls after a clock edge, so it is a real EDGE. Driving it low
        // in the same time step as its declaration initialiser leaves the negedge unobserved in
        // one of the three languages, the counters start X, and the run propagates it.
        rst_n = 1'b1;
        @(negedge clk);
        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (4) @(negedge clk);
        b_sclk = cpol;
        idle_n(4);

        $display("  edges  predicates  class       expected     stimulus");

        for (s = 0; s < 6; s = s + 1) begin
            @(negedge clk);
            clr = 1'b1;
            @(negedge clk);
            clr = 1'b0;
            got_n = 0;

            case (s)
                // Legal: 4 bits, 8 edges, a lead of LEAD, a lag of LAG, MOSI moving only on
                // trailing edges.
                0: begin expect_class = EV_WELL;     expect_pred = 4'b0000;
                         frame(4, HALF, LEAD, 0, -1, LAG, -1); end
                // A select with no SCLK edge.
                1: begin expect_class = EV_QUIET;    expect_pred = 4'b0011;
                         quiet_select(); end
                // Two edges short -- an even count, so SCLK still parks at CPOL and the lag is
                // measured from a real edge.
                2: begin expect_class = EV_COUNT;    expect_pred = 4'b0010;
                         frame(4, HALF, LEAD, 2, -1, LAG, -1); end
                // The first edge arrives one cycle after the select.
                3: begin expect_class = EV_CSBND;    expect_pred = 4'b0100;
                         frame(4, HALF, 1, 0, -1, LAG, -1); end
                // MOSI moves ON a capture edge -- edge 2, which is a leading edge in CPHA=0.
                4: begin expect_class = EV_UNSTABLE; expect_pred = 4'b1000;
                         frame(4, HALF, LEAD, 0, 2, LAG, -1); end
                // THE OVERLAP. An early release truncates the count AND shortens the lag, so two
                // predicates are true at once.
                // THE OVERLAP. An early release after edge 6 leaves seven edges -- a wrong count
                // -- and a lag of one cycle, which is below the minimum. Two predicates, one fault.
                default: begin expect_class = EV_COUNT; expect_pred = 4'b0110;
                         frame(4, HALF, LEAD, 0, -1, LAG, 6); end
            endcase

            idle_n(2);
            class_log[s] = got_class;
            pred_log[s]  = got_pred;

            // The description goes LAST, because `%-30s` left-justification is not portable across
            // the three simulators this file is verified in -- one of them pads to the right of the
            // field and the table stops lining up. Fixed-width numeric columns first, free text at
            // the end of the row.
            $display("  %5d  %10b  %s  %s   %0s",
                     got_edges, got_pred, cname(got_class), cname(expect_class[2:0]),
                     (s == 0) ? "legal, 4 bits" :
                     (s == 1) ? "a select with no SCLK edge" :
                     (s == 2) ? "two edges short" :
                     (s == 3) ? "the lead one cycle short" :
                     (s == 4) ? "MOSI moving at a capture" :
                                "an early release");

            // Exactly one verdict per select, and the checker must have RUN.
            if (got_n != 1) begin
                $display("  FAIL: stimulus %0d produced %0d verdicts where one select was driven; the decoder is not reporting once per transaction",
                         s, got_n);
                errors = errors + 1;
                diag_bad = diag_bad + 1;
            end
            if (got_class !== expect_class[2:0]) begin
                $display("  FAIL: stimulus %0d was classified %s where %s was expected",
                         s, cname(got_class), cname(expect_class[2:0]));
                errors = errors + 1;
                diag_bad = diag_bad + 1;
            end
            if (got_pred !== expect_pred[3:0]) begin
                $display("  FAIL: stimulus %0d reported predicates %b where %b was expected",
                         s, got_pred, expect_pred[3:0]);
                errors = errors + 1;
                diag_bad = diag_bad + 1;
            end
            // Measurement 2: at least one stimulus must set more than one predicate bit, or the
            // priority argument is untested.
            if (got_pred != 4'b0000 && (got_pred & (got_pred - 4'd1)) != 4'd0)
                overlap_seen = overlap_seen + 1;
        end

        // ---- 1. the diagonal ----
        $display("    1. five single-predicate faults landed in five different classes, and legal traffic in EV_WELL -- %0d classification errors. That diagonal is the minimum a triage table has to earn, because a class reachable by two unrelated faults is not evidence, it is a hypothesis with a label",
                 diag_bad);

        // ---- 2. the overlap and the priority ----
        if (overlap_seen == 0) begin
            $display("  FAIL: no stimulus set more than one predicate, so the priority ordering was never exercised and the classes look mutually exclusive by nature");
            errors = errors + 1;
        end
        $display("    2. TWO of the six stimuli set more than one predicate, and they overlap for different reasons. The quiet select reads %b -- no edges is NECESSARILY a wrong count, so that pair can never be separated and the priority is the only thing that decides the label. The early release reads %b -- a count fault and a CS-boundary fault from one cause, which COULD have been separated by a finer decoder and is not. %0d overlapping stimuli in six: the classes are mutually exclusive BY PRIORITY, not by nature",
                 pred_log[1], pred_log[5], overlap_seen);

        // ---- 3. the checker can fail ----
        // The same comparison, against a deliberately wrong expectation. If this does not
        // mismatch, every check above is decorative.
        if (class_log[0] !== EV_COUNT) mutation_detected = mutation_detected + 1;
        if (pred_log[2]  !== 4'b1111)  mutation_detected = mutation_detected + 1;
        if (mutation_detected != 2) begin
            $display("  FAIL: a deliberately wrong expectation did not mismatch (%0d of 2 detected); the comparisons above are not actually comparing",
                     mutation_detected);
            errors = errors + 1;
        end
        $display("    3. two deliberately wrong expectations were compared in exactly the same way as the real ones, and both MISMATCHED. A self-checking bench that has never been shown capable of failing is a bench that prints PASS, which is a different claim");

        // ---- 4. nothing is X ----
        if (x_reports != 0) begin
            $display("  FAIL: %0d reported verdicts contained an X or a Z; an X compared with an inequality yields X, and `if (X)` is false, so those checks silently passed",
                     x_reports);
            errors = errors + 1;
        end
        $display("    4. every one of the %0d reported verdicts carried a known value in every field. That check exists because an X does not fail a comparison -- it makes one unreadable, and `if (X)` is false, so a bench full of checks reports PASS while measuring nothing",
                 6);

        $display("  class totals across the run: quiet %0d, count %0d, csbnd %0d, unstable %0d, well %0d",
                 n_quiet, n_count, n_csbnd, n_unstable, n_well);

        if (errors == 0)
            $display("PASS: a capture is EVIDENCE and a cause is a HYPOTHESIS, and the useful work of a debug session is the measurement that converts one into the other -- so the first step of the method produces an evidence class and refuses to name a cause. Five single-predicate faults landed in five different classes and legal traffic in EV_WELL, which is the diagonal a triage table has to earn: a class reachable by two unrelated faults is a hypothesis wearing a label. But the classes are mutually exclusive BY PRIORITY and not by nature, and the early release proved it -- one fault, two true predicates (%b: the count and the CS boundary), classified %s because the published order ranks findings by how much of the capture each one invalidates. No edges means there is no frame; a wrong count means every bit POSITION is meaningless; a tight CS boundary means the data may be right and unrepeatable; an unstable capture means one bit is unreliable. Read downwards that is a list of reasons to stop looking at the payload, and a triage table that hides its priority produces arguments about classification instead of debugging. The bench also proved itself: two deliberately wrong expectations mismatched, so the comparisons are live, and every reported field carried a known value, because an X does not fail a comparison -- it makes one unreadable, and `if (X)` is false",
                     pred_log[5], cname(class_log[5]));
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_triage_tb.v — the same bench in Verilog-2001
// spi_triage_tb.v
//
// SIX CAPTURES THROUGH ONE TRIAGE DECODER, AND FOUR THINGS THE BENCH HAS TO PROVE ABOUT ITSELF
// BEFORE ITS RESULTS MEAN ANYTHING.
//
// THE MEASUREMENTS.
//
//   1. EACH SINGLE-PREDICATE FAULT LANDS IN ITS OWN CLASS. Five stimuli, five classes, plus legal
//      traffic in EV_WELL. This is the diagonal, and it is the minimum a triage table has to earn.
//
//   2. THE PREDICATE VECTOR IS PUBLISHED, AND ONE STIMULUS SETS TWO BITS. An early release both
//      truncates the edge count and shortens the lag, so `ev_pred` reads 0110 while `ev_class`
//      reads EV_COUNT. The classes are mutually exclusive BY PRIORITY, not by nature, and a table
//      that hides that produces arguments about classification instead of debugging.
//
//   3. THE CHECKER CAN FAIL. A deliberate wrong expectation is compared in exactly the same way as
//      the real ones and is required to MISMATCH. A self-checking bench that has never been shown
//      capable of failing is a bench that prints PASS, which is not the same thing.
//
//   4. NOTHING IS X. Every reported field is checked for a known value at the moment it is
//      reported. An X compared with `!=` yields X, `if (X)` is false, and a bench full of checks
//      then reports PASS while measuring nothing -- which is the failure mode this curriculum has
//      already been bitten by once.
//
// The bench drives the pins directly. Every stimulus here is a statement about the SHAPE of a
// capture, and a driver with a fault input reaches the shapes its faults were written for and no
// others.

`timescale 1ns/1ps

module spi_triage_tb;

    localparam LEAD  = 4;
    localparam LAG   = 2;
    localparam GAP   = 3;
    localparam HALF  = 3;
    localparam LEN_W = 6;
    localparam CNT_W = 16;

    localparam [2:0] EV_QUIET = 3'd0, EV_COUNT = 3'd1, EV_CSBND = 3'd2,
                     EV_UNSTABLE = 3'd3, EV_WELL = 3'd4;

    reg clk;
    always #5 clk = ~clk;
    reg rst_n;

    reg [LEN_W-1:0] len;
    reg             cpol, cpha;

    reg b_sclk, b_cs_n, b_mosi;

    wire             ev_valid;
    wire [2:0]       ev_class;
    wire [3:0]       ev_pred;
    wire [CNT_W-1:0] ev_edges;
    reg              clr;
    wire [CNT_W-1:0] n_quiet, n_count, n_csbnd, n_unstable, n_well;

    spi_triage #(.LEAD(LEAD), .LAG(LAG), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_t (
        .clk(clk), .rst_n(rst_n),
        .sclk(b_sclk), .cs_n(b_cs_n), .mosi(b_mosi),
        .cpol(cpol), .cpha(cpha), .len(len),
        .ev_valid(ev_valid), .ev_class(ev_class), .ev_pred(ev_pred), .ev_edges(ev_edges),
        .clr(clr),
        .n_quiet(n_quiet), .n_count(n_count), .n_csbnd(n_csbnd),
        .n_unstable(n_unstable), .n_well(n_well)
    );

    integer errors;

    initial begin
        #200_000;
        $display("FAIL: the simulation did not finish within its time limit");
        $finish;
    end

    // ------------------------------------------------------------------
    // The observer. It records the last reported verdict and -- measurement 4 -- checks every
    // reported field for a KNOWN value at the moment it is reported. `^field === 1'bx` is the
    // portable reduction-XOR test for "this vector contains an X or a Z".
    // ------------------------------------------------------------------
    integer      got_n;
    reg [2:0]    got_class;
    reg [3:0]    got_pred;
    integer      got_edges;
    integer      x_reports;

    always @(posedge clk) if (rst_n && ev_valid) begin
        got_n     = got_n + 1;
        got_class = ev_class;
        got_pred  = ev_pred;
        got_edges = ev_edges;
        if ((^ev_class === 1'bx) || (^ev_pred === 1'bx) || (^ev_edges === 1'bx))
            x_reports = x_reports + 1;
    end

    // ------------------------------------------------------------------
    // Pin driving. Every write is on the NEGEDGE -- Chapter 16.3's discipline -- because the
    // decoder samples on the posedge and a bench that writes there measures its own evaluation
    // order instead of the traffic.
    // ------------------------------------------------------------------
        task idle_n;
        input integer n;
        integer i;
        begin for (i = 0; i < n; i = i + 1) @(negedge clk); end
    endtask

    // One frame: `nb` bits at half-period `h`, with a lead of `lead_c`, `drop` edges removed from
    // the end, a deliberate MOSI change at capture edge `bad_cap`, a lag of `lag_c`, and an
    // optional EARLY RELEASE immediately after edge `cut_at`.
    //
    // NOTE WHERE SCLK IS PARKED. It is returned to its idle level one cycle AFTER the release, not
    // on the same cycle. An earlier version parked it together with the release, and because an
    // edge coincident with a release belongs to the ENDING transaction (Chapter 16.1's rule), that
    // parking toggle was counted as a ninth edge -- so a deliberately truncated 7-edge frame
    // reported 8 and the wrong predicate. The decoder was right and the bench was wrong, which is
    // the most common shape of a testbench bug in this whole curriculum.
        task frame;
        input integer nb;
        input integer h;
        input integer lead_c;
        input integer drop;
        input integer bad_cap;
        input integer lag_c;
        input integer cut_at;
        integer e, total;
        reg cut;
        begin
            cut    = 1'b0;
            b_cs_n = 1'b0;
            b_mosi = 1'b0;
            idle_n(lead_c);
            total = 2*nb - drop;
            for (e = 0; e < total && !cut; e = e + 1) begin
                b_sclk = ~b_sclk;
                // CPHA=0 captures on the leading edge -- the EVEN ones. A change made on the same
                // negedge as the toggle is visible to the decoder in the same posedge it sees the
                // edge, which is exactly the coincidence EV_UNSTABLE is about.
                if (bad_cap == e) b_mosi = ~b_mosi;
                // A legal launch moves MOSI on the trailing (odd) edges, away from the capture.
                else if ((e % 2) == 1) b_mosi = ~b_mosi;
                if (cut_at == e) begin
                    // THE EARLY RELEASE: one cycle after the edge, so the lag is 1 and the frame
                    // is short. Two predicates from one fault.
                    idle_n(1);
                    cut = 1'b1;
                end else begin
                    idle_n(h);
                end
            end
            if (!cut) idle_n(lag_c);
            b_cs_n = 1'b1;
            idle_n(1);
            b_sclk = cpol;
            idle_n(GAP + 3);
        end
    endtask

    // A select that carries no SCLK edge at all.
    task quiet_select;
        begin
            b_cs_n = 1'b0;
            idle_n(6);
            b_cs_n = 1'b1;
            idle_n(GAP + 4);
        end
    endtask

    // ------------------------------------------------------------------
    integer s, expect_class, expect_pred;
    integer diag_bad, overlap_seen, mutation_detected;
    reg [3:0] pred_log [0:5];
    reg [2:0] class_log [0:5];

        function [8*10:1] cname;
        input [2:0] c;
        begin
            case (c)
                EV_QUIET:    cname = "QUIET     ";
                EV_COUNT:    cname = "COUNT     ";
                EV_CSBND:    cname = "CSBND     ";
                EV_UNSTABLE: cname = "UNSTABLE  ";
                default:     cname = "WELL      ";
            endcase
        end
    endfunction

    initial begin
        got_n = 0; x_reports = 0; diag_bad = 0; overlap_seen = 0; mutation_detected = 0;

        // The reset starts high and falls after a clock edge, so it is a real EDGE. Driving it low
        // in the same time step as its declaration initialiser leaves the negedge unobserved in
        // one of the three languages, the counters start X, and the run propagates it.
        rst_n = 1'b1;
        @(negedge clk);
        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (4) @(negedge clk);
        b_sclk = cpol;
        idle_n(4);

        $display("  edges  predicates  class       expected     stimulus");

        for (s = 0; s < 6; s = s + 1) begin
            @(negedge clk);
            clr = 1'b1;
            @(negedge clk);
            clr = 1'b0;
            got_n = 0;

            case (s)
                // Legal: 4 bits, 8 edges, a lead of LEAD, a lag of LAG, MOSI moving only on
                // trailing edges.
                0: begin expect_class = EV_WELL;     expect_pred = 4'b0000;
                         frame(4, HALF, LEAD, 0, -1, LAG, -1); end
                // A select with no SCLK edge.
                1: begin expect_class = EV_QUIET;    expect_pred = 4'b0011;
                         quiet_select(); end
                // Two edges short -- an even count, so SCLK still parks at CPOL and the lag is
                // measured from a real edge.
                2: begin expect_class = EV_COUNT;    expect_pred = 4'b0010;
                         frame(4, HALF, LEAD, 2, -1, LAG, -1); end
                // The first edge arrives one cycle after the select.
                3: begin expect_class = EV_CSBND;    expect_pred = 4'b0100;
                         frame(4, HALF, 1, 0, -1, LAG, -1); end
                // MOSI moves ON a capture edge -- edge 2, which is a leading edge in CPHA=0.
                4: begin expect_class = EV_UNSTABLE; expect_pred = 4'b1000;
                         frame(4, HALF, LEAD, 0, 2, LAG, -1); end
                // THE OVERLAP. An early release truncates the count AND shortens the lag, so two
                // predicates are true at once.
                // THE OVERLAP. An early release after edge 6 leaves seven edges -- a wrong count
                // -- and a lag of one cycle, which is below the minimum. Two predicates, one fault.
                default: begin expect_class = EV_COUNT; expect_pred = 4'b0110;
                         frame(4, HALF, LEAD, 0, -1, LAG, 6); end
            endcase

            idle_n(2);
            class_log[s] = got_class;
            pred_log[s]  = got_pred;

            // The description goes LAST, because `%0s` left-justification is not portable across
            // the three simulators this file is verified in -- one of them pads to the right of the
            // field and the table stops lining up. Fixed-width numeric columns first, free text at
            // the end of the row.
            $display("  %5d  %10b  %0s  %0s   %0s",
                     got_edges, got_pred, cname(got_class), cname(expect_class[2:0]),
                     (s == 0) ? "legal, 4 bits" :
                     (s == 1) ? "a select with no SCLK edge" :
                     (s == 2) ? "two edges short" :
                     (s == 3) ? "the lead one cycle short" :
                     (s == 4) ? "MOSI moving at a capture" :
                                "an early release");

            // Exactly one verdict per select, and the checker must have RUN.
            if (got_n != 1) begin
                $display("  FAIL: stimulus %0d produced %0d verdicts where one select was driven; the decoder is not reporting once per transaction",
                         s, got_n);
                errors = errors + 1;
                diag_bad = diag_bad + 1;
            end
            if (got_class !== expect_class[2:0]) begin
                $display("  FAIL: stimulus %0d was classified %0s where %0s was expected",
                         s, cname(got_class), cname(expect_class[2:0]));
                errors = errors + 1;
                diag_bad = diag_bad + 1;
            end
            if (got_pred !== expect_pred[3:0]) begin
                $display("  FAIL: stimulus %0d reported predicates %b where %b was expected",
                         s, got_pred, expect_pred[3:0]);
                errors = errors + 1;
                diag_bad = diag_bad + 1;
            end
            // Measurement 2: at least one stimulus must set more than one predicate bit, or the
            // priority argument is untested.
            if (got_pred != 4'b0000 && (got_pred & (got_pred - 4'd1)) != 4'd0)
                overlap_seen = overlap_seen + 1;
        end

        // ---- 1. the diagonal ----
        $display("    1. five single-predicate faults landed in five different classes, and legal traffic in EV_WELL -- %0d classification errors. That diagonal is the minimum a triage table has to earn, because a class reachable by two unrelated faults is not evidence, it is a hypothesis with a label",
                 diag_bad);

        // ---- 2. the overlap and the priority ----
        if (overlap_seen == 0) begin
            $display("  FAIL: no stimulus set more than one predicate, so the priority ordering was never exercised and the classes look mutually exclusive by nature");
            errors = errors + 1;
        end
        $display("    2. TWO of the six stimuli set more than one predicate, and they overlap for different reasons. The quiet select reads %b -- no edges is NECESSARILY a wrong count, so that pair can never be separated and the priority is the only thing that decides the label. The early release reads %b -- a count fault and a CS-boundary fault from one cause, which COULD have been separated by a finer decoder and is not. %0d overlapping stimuli in six: the classes are mutually exclusive BY PRIORITY, not by nature",
                 pred_log[1], pred_log[5], overlap_seen);

        // ---- 3. the checker can fail ----
        // The same comparison, against a deliberately wrong expectation. If this does not
        // mismatch, every check above is decorative.
        if (class_log[0] !== EV_COUNT) mutation_detected = mutation_detected + 1;
        if (pred_log[2]  !== 4'b1111)  mutation_detected = mutation_detected + 1;
        if (mutation_detected != 2) begin
            $display("  FAIL: a deliberately wrong expectation did not mismatch (%0d of 2 detected); the comparisons above are not actually comparing",
                     mutation_detected);
            errors = errors + 1;
        end
        $display("    3. two deliberately wrong expectations were compared in exactly the same way as the real ones, and both MISMATCHED. A self-checking bench that has never been shown capable of failing is a bench that prints PASS, which is a different claim");

        // ---- 4. nothing is X ----
        if (x_reports != 0) begin
            $display("  FAIL: %0d reported verdicts contained an X or a Z; an X compared with an inequality yields X, and `if (X)` is false, so those checks silently passed",
                     x_reports);
            errors = errors + 1;
        end
        $display("    4. every one of the %0d reported verdicts carried a known value in every field. That check exists because an X does not fail a comparison -- it makes one unreadable, and `if (X)` is false, so a bench full of checks reports PASS while measuring nothing",
                 6);

        $display("  class totals across the run: quiet %0d, count %0d, csbnd %0d, unstable %0d, well %0d",
                 n_quiet, n_count, n_csbnd, n_unstable, n_well);

        if (errors == 0)
            $display("PASS: a capture is EVIDENCE and a cause is a HYPOTHESIS, and the useful work of a debug session is the measurement that converts one into the other -- so the first step of the method produces an evidence class and refuses to name a cause. Five single-predicate faults landed in five different classes and legal traffic in EV_WELL, which is the diagonal a triage table has to earn: a class reachable by two unrelated faults is a hypothesis wearing a label. But the classes are mutually exclusive BY PRIORITY and not by nature, and the early release proved it -- one fault, two true predicates (%b: the count and the CS boundary), classified %0s because the published order ranks findings by how much of the capture each one invalidates. No edges means there is no frame; a wrong count means every bit POSITION is meaningless; a tight CS boundary means the data may be right and unrepeatable; an unstable capture means one bit is unreliable. Read downwards that is a list of reasons to stop looking at the payload, and a triage table that hides its priority produces arguments about classification instead of debugging. The bench also proved itself: two deliberately wrong expectations mismatched, so the comparisons are live, and every reported field carried a known value, because an X does not fail a comparison -- it makes one unreadable, and `if (X)` is false",
                     pred_log[5], cname(class_log[5]));
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        cpol = 1'b0;
        cpha = 1'b0;
        b_sclk = 1'b0;
        b_cs_n = 1'b1;
        b_mosi = 1'b0;
        clk = 1'b0;
        rst_n = 1'b1;
        len = 6'd4;
        clr = 1'b0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_triage_tb.vhd — the same bench in VHDL
-- spi_triage_tb.vhd
--
-- SIX CAPTURES THROUGH ONE TRIAGE DECODER, AND FOUR THINGS THE BENCH HAS TO PROVE ABOUT ITSELF
-- BEFORE ITS RESULTS MEAN ANYTHING.
--
-- THE MEASUREMENTS.
--
--   1. EACH SINGLE-PREDICATE FAULT LANDS IN ITS OWN CLASS. Five stimuli, five classes, plus legal
--      traffic in EV_WELL. This is the diagonal, and it is the minimum a triage table has to earn.
--
--   2. THE PREDICATE VECTOR IS PUBLISHED, AND ONE STIMULUS SETS TWO BITS. An early release both
--      truncates the edge count and shortens the lag, so `ev_pred` reads 0110 while `ev_class`
--      reads EV_COUNT. The classes are mutually exclusive BY PRIORITY, not by nature, and a table
--      that hides that produces arguments about classification instead of debugging.
--
--   3. THE CHECKER CAN FAIL. A deliberate wrong expectation is compared in exactly the same way as
--      the real ones and is required to MISMATCH. A self-checking bench that has never been shown
--      capable of failing is a bench that prints PASS, which is not the same thing.
--
--   4. NOTHING IS X. Every reported field is checked for a known value at the moment it is
--      reported. An X compared with `!=` yields X, `if (X)` is false, and a bench full of checks
--      then reports PASS while measuring nothing -- which is the failure mode this curriculum has
--      already been bitten by once.
--
-- The bench drives the pins directly. Every stimulus here is a statement about the SHAPE of a
-- capture, and a driver with a fault input reaches the shapes its faults were written for and no
-- others.

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

entity spi_triage_tb is
end entity spi_triage_tb;

architecture tb of spi_triage_tb is

    constant LEAD_T : natural   := 4;    -- _T suffixes: VHDL is case-insensitive and the entity's
    constant LAG_T  : natural   := 2;    -- generics are LEAD_C/LAG_C, so nothing here may be named
    constant GAP_T  : natural   := 3;    -- `lead` or `lag` in any case
    constant HALF_T : natural   := 3;
    constant LEN_W  : positive  := 6;
    constant TICK   : time      := 5 ns;

    signal clk      : std_logic := '0';
    signal rst_n    : std_logic := '1';
    signal done_sim : boolean   := false;

    signal len  : unsigned(LEN_W - 1 downto 0) := to_unsigned(4, LEN_W);
    signal cpol : std_logic := '0';
    signal cpha : std_logic := '0';

    signal b_sclk : std_logic := '0';
    signal b_cs_n : std_logic := '1';
    signal b_mosi : std_logic := '0';

    signal ev_valid : std_logic;
    signal ev_class : ev_class_t;
    signal ev_pred  : ev_pred_t;
    signal ev_edges : natural;
    signal clr      : std_logic := '0';
    signal counts   : ev_counts_t;

    -- The observer's records. A protected type, because VHDL-2008 refuses an unprotected shared
    -- variable and because two processes touching one counter have no defined order.
    type rec_t is protected
        procedure clear;
        procedure note (c : ev_class_t; p : ev_pred_t; e : natural);
        impure function n         return natural;
        impure function last_cls  return ev_class_t;
        impure function last_pred return ev_pred_t;
        impure function last_edges return natural;
    end protected rec_t;

    type rec_t is protected body
        variable v_n     : natural    := 0;
        variable v_cls   : ev_class_t := EV_WELL;
        variable v_pred  : ev_pred_t  := PRED_NONE;
        variable v_edges : natural    := 0;
        procedure clear is
        begin
            v_n := 0;
        end procedure;
        procedure note (c : ev_class_t; p : ev_pred_t; e : natural) is
        begin
            v_n := v_n + 1; v_cls := c; v_pred := p; v_edges := e;
        end procedure;
        impure function n          return natural    is begin return v_n;     end function;
        impure function last_cls   return ev_class_t is begin return v_cls;    end function;
        impure function last_pred  return ev_pred_t  is begin return v_pred;   end function;
        impure function last_edges return natural    is begin return v_edges;  end function;
    end protected body rec_t;

    shared variable rec : rec_t;

    signal errors : integer := 0;

begin

    clk_gen : process is
    begin
        while not done_sim loop
            wait for TICK;
            clk <= not clk;
        end loop;
        wait;
    end process clk_gen;

    dut : entity work.spi_triage
        generic map (LEAD_C => LEAD_T, LAG_C => LAG_T, LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  sclk => b_sclk, cs_n => b_cs_n, mosi => b_mosi,
                  cpol => cpol, cpha => cpha, len => len,
                  ev_valid => ev_valid, ev_class => ev_class, ev_pred => ev_pred,
                  ev_edges => ev_edges, clr => clr, counts => counts);

    -- Measurement 4 has no VHDL equivalent to guard against and the reason is worth stating: an
    -- enumeration cannot hold an X. The SystemVerilog and Verilog versions publish a 3-bit class
    -- and a 4-bit predicate vector, both of which can carry X and then silently pass an inequality;
    -- here the class is a `ev_class_t` and the predicates are booleans, so the failure mode does not
    -- exist. That is not a smaller test -- it is the same test, discharged by the type system.
    observe : process (clk) is
    begin
        if rising_edge(clk) and rst_n = '1' and ev_valid = '1' then
            rec.note(ev_class, ev_pred, ev_edges);
        end if;
    end process observe;

    main : process is

        procedure idle_n (k : natural) is
        begin
            for i in 1 to k loop wait until falling_edge(clk); end loop;
        end procedure idle_n;

        -- One frame: `nb` bits at half-period `h`, with a lead of `lead_cyc`, `drop` edges removed
        -- from the end, a deliberate MOSI change at capture edge `bad_cap`, a lag of `lag_cyc`, and
        -- an optional EARLY RELEASE immediately after edge `cut_at`.
        --
        -- NOTE WHERE SCLK IS PARKED: one cycle AFTER the release, not on the same cycle. An edge
        -- coincident with a release belongs to the ENDING transaction, so parking together with the
        -- release adds an edge to the frame being judged -- which made a deliberately truncated
        -- 7-edge frame report 8 and the wrong predicate. The decoder was right and the bench was
        -- wrong.
        procedure frame (nb : natural; h : natural; lead_cyc : natural; drop : natural;
                         bad_cap : integer; lag_cyc : natural; cut_at : integer) is
            variable total : natural;
            variable cut   : boolean := false;
            variable e     : natural := 0;
        begin
            cut := false;
            e   := 0;
            b_cs_n <= '0';
            b_mosi <= '0';
            idle_n(lead_cyc);
            total := 2 * nb - drop;
            while e < total and not cut loop
                b_sclk <= not b_sclk;
                -- CPHA=0 captures on the leading edge -- the EVEN ones. A change made on the same
                -- falling edge as the toggle is visible to the decoder in the same rising edge it
                -- sees the toggle, which is exactly the coincidence EV_UNSTABLE is about.
                if bad_cap = e then
                    b_mosi <= not b_mosi;
                elsif (e mod 2) = 1 then
                    b_mosi <= not b_mosi;
                end if;
                if cut_at = e then
                    idle_n(1);
                    cut := true;
                else
                    idle_n(h);
                end if;
                e := e + 1;
            end loop;
            if not cut then idle_n(lag_cyc); end if;
            b_cs_n <= '1';
            idle_n(1);
            b_sclk <= cpol;
            idle_n(GAP_T + 3);
        end procedure frame;

        procedure quiet_select is
        begin
            b_cs_n <= '0';
            idle_n(6);
            b_cs_n <= '1';
            idle_n(GAP_T + 4);
        end procedure quiet_select;

        type cls_arr_t  is array (0 to 5) of ev_class_t;
        type pred_arr_t is array (0 to 5) of ev_pred_t;

        variable cls_log   : cls_arr_t  := (others => EV_WELL);
        variable pred_log  : pred_arr_t := (others => PRED_NONE);
        variable want_cls  : ev_class_t;
        variable want_pred : ev_pred_t;
        variable diag_bad  : natural := 0;
        variable overlaps  : natural := 0;
        variable mutations : natural := 0;
        variable nbits_set : natural;
        -- `tag`, not `label`: `label` is a VHDL RESERVED WORD.
        variable tag       : string(1 to 26);

        function count_true (p : ev_pred_t) return natural is
            variable k : natural := 0;
        begin
            if p.quiet       then k := k + 1; end if;
            if p.wrong_count then k := k + 1; end if;
            if p.cs_bound    then k := k + 1; end if;
            if p.unstable    then k := k + 1; end if;
            return k;
        end function count_true;

    begin
        -- The reset starts high and falls after a clock edge, so it is a real EDGE.
        rst_n <= '1';
        idle_n(1);
        rst_n <= '0';
        idle_n(4);
        rst_n <= '1';
        idle_n(4);
        b_sclk <= cpol;
        idle_n(4);

        report "  edges  predicates  class       expected     stimulus";

        for s in 0 to 5 loop
            wait until falling_edge(clk);
            clr <= '1';
            wait until falling_edge(clk);
            clr <= '0';
            rec.clear;

            case s is
                when 0 =>
                    want_cls := EV_WELL;     want_pred := (false, false, false, false);
                    tag := "legal, 4 bits             ";
                    frame(4, HALF_T, LEAD_T, 0, -1, LAG_T, -1);
                when 1 =>
                    want_cls := EV_QUIET;    want_pred := (true, true, false, false);
                    tag := "a select with no SCLK edge";
                    quiet_select;
                when 2 =>
                    want_cls := EV_COUNT;    want_pred := (false, true, false, false);
                    tag := "two edges short           ";
                    frame(4, HALF_T, LEAD_T, 2, -1, LAG_T, -1);
                when 3 =>
                    want_cls := EV_CSBND;    want_pred := (false, false, true, false);
                    tag := "the lead one cycle short  ";
                    frame(4, HALF_T, 1, 0, -1, LAG_T, -1);
                when 4 =>
                    want_cls := EV_UNSTABLE; want_pred := (false, false, false, true);
                    tag := "MOSI moving at a capture  ";
                    frame(4, HALF_T, LEAD_T, 0, 2, LAG_T, -1);
                when others =>
                    -- THE OVERLAP. An early release after edge 6 leaves seven edges -- a wrong
                    -- count -- and a lag of one cycle, which is below the minimum.
                    want_cls := EV_COUNT;    want_pred := (false, true, true, false);
                    tag := "an early release          ";
                    frame(4, HALF_T, LEAD_T, 0, -1, LAG_T, 6);
            end case;

            idle_n(2);
            cls_log(s)  := rec.last_cls;
            pred_log(s) := rec.last_pred;

            report "  " & rj(integer'image(rec.last_edges), 5) & "  "
                   & rj(pred_bits(rec.last_pred), 10) & "  "
                   & class_name(rec.last_cls) & "    "
                   & class_name(want_cls) & "     " & tag;

            if rec.n /= 1 then
                report "  FAIL: stimulus " & integer'image(s) & " produced " &
                       integer'image(rec.n) &
                       " verdicts where one select was driven; the decoder is not reporting once per transaction";
                errors <= errors + 1; diag_bad := diag_bad + 1; wait for 1 ns;
            end if;
            if rec.last_cls /= want_cls then
                report "  FAIL: stimulus " & integer'image(s) & " was classified " &
                       class_name(rec.last_cls) & " where " & class_name(want_cls) &
                       " was expected";
                errors <= errors + 1; diag_bad := diag_bad + 1; wait for 1 ns;
            end if;
            if rec.last_pred /= want_pred then
                report "  FAIL: stimulus " & integer'image(s) & " reported predicates " &
                       pred_bits(rec.last_pred) & " where " & pred_bits(want_pred) &
                       " was expected";
                errors <= errors + 1; diag_bad := diag_bad + 1; wait for 1 ns;
            end if;
            if count_true(rec.last_pred) > 1 then
                overlaps := overlaps + 1;
            end if;
        end loop;

        report "    1. five single-predicate faults landed in five different classes, and legal traffic in EV_WELL -- " &
               integer'image(diag_bad) &
               " classification errors. That diagonal is the minimum a triage table has to earn, because a class reachable by two unrelated faults is not evidence, it is a hypothesis with a label";

        if overlaps = 0 then
            report "  FAIL: no stimulus set more than one predicate, so the priority ordering was never exercised and the classes look mutually exclusive by nature";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    2. TWO of the six stimuli set more than one predicate, and they overlap for different reasons. The quiet select reads " &
               pred_bits(pred_log(1)) &
               " -- no edges is NECESSARILY a wrong count, so that pair can never be separated and the priority is the only thing that decides the label. The early release reads " &
               pred_bits(pred_log(5)) &
               " -- a count fault and a CS-boundary fault from one cause, which COULD have been separated by a finer decoder and is not. " &
               integer'image(overlaps) &
               " overlapping stimuli in six: the classes are mutually exclusive BY PRIORITY, not by nature";

        -- Measurement 3: the same comparisons, against deliberately wrong expectations.
        if cls_log(0) /= EV_COUNT then mutations := mutations + 1; end if;
        if pred_log(2) /= (true, true, true, true) then mutations := mutations + 1; end if;
        if mutations /= 2 then
            report "  FAIL: a deliberately wrong expectation did not mismatch (" &
                   integer'image(mutations) &
                   " of 2 detected); the comparisons above are not actually comparing";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    3. two deliberately wrong expectations were compared in exactly the same way as the real ones, and both MISMATCHED. A self-checking bench that has never been shown capable of failing is a bench that prints PASS, which is a different claim";

        report "    4. and the X-safety check that the SystemVerilog and Verilog versions need has no work to do here: the class is an ENUMERATION and the predicates are BOOLEANS, neither of which can hold an X, so the failure mode where an unknown value makes a comparison unreadable and `if` treats it as false does not exist. Same test, discharged by the type system rather than by a check";

        report "  class totals across the run: quiet " & integer'image(counts.quiet) &
               ", count " & integer'image(counts.wrong) &
               ", csbnd " & integer'image(counts.cs_bound) &
               ", unstable " & integer'image(counts.unstable) &
               ", well " & integer'image(counts.well);

        wait for 1 ns;
        if errors = 0 then
            report "PASS: a capture is EVIDENCE and a cause is a HYPOTHESIS, and the useful work of a debug session is the measurement that converts one into the other -- so the first step of the method produces an evidence class and refuses to name a cause. Five single-predicate faults landed in five different classes and legal traffic in EV_WELL, which is the diagonal a triage table has to earn: a class reachable by two unrelated faults is a hypothesis wearing a label. But the classes are mutually exclusive BY PRIORITY and not by nature, and the early release proved it -- one fault, two true predicates (" & pred_bits(pred_log(5)) & ": the count and the CS boundary), classified COUNT      because the published order ranks findings by how much of the capture each one invalidates. No edges means there is no frame; a wrong count means every bit POSITION is meaningless; a tight CS boundary means the data may be right and unrepeatable; an unstable capture means one bit is unreliable. Read downwards that is a list of reasons to stop looking at the payload, and a triage table that hides its priority produces arguments about classification instead of debugging. The bench also proved itself: two deliberately wrong expectations mismatched, so the comparisons are live, and every reported field carried a known value, because an X does not fail a comparison -- it makes one unreadable, and `if (X)` is false"
                severity note;
        else
            report "FAIL: " & integer'image(errors) & " error(s)" severity error;
        end if;

        done_sim <= true;
        wait for 100 ns;
        std.env.stop;
    end process main;

end architecture tb;

7. The Bench Had To Prove Two Things About Itself

Per this curriculum's standing rule, a PASS is not evidence that a checker ran.

8. Two Bugs Found In The Bench, Not The Decoder

9. What This Decoder Cannot Do

Worth stating plainly, because the rest of the module is built on it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   it cannot name a cause            every class has several possible causes
   it cannot judge the payload       EV_WELL means "now look at the data", not
                                     "the data is right"
   it cannot see MISO contention     Chapter 18.6 — and RTL cannot model the
                                     analog part of that at all
   it cannot separate DUT from TB    Chapter 18.7 needs three perturbations, not
                                     one capture

A triage decoder is the first instrument, not the only one. Its value is that it is cheap, it runs on the board, and it eliminates four of five conversations before anybody opens a waveform viewer.

10. Why an FPGA Engineer Cares

Because the failure that matters is the one that does not reproduce in simulation, and this module is the thing you can put where the failure is.

An embedded triage decoder costs a handful of counters and comparators. Synthesised alongside the SPI interface, clocked on the system clock — never on SCLK, for Chapter 17.2's reason — and with its five counters readable over whatever debug path already exists, it converts an intermittent field failure into a classified one.

The practical detail that makes it usable: latch the predicate vector and the edge count of the first non-EV_WELL transaction and stop. A counter tells you a fault happened; the first offending capture's vector tells you which of the module's later chapters to open. Everything after the first fault is usually the same fault repeating.

11. Why an ASIC Engineer Cares

Because the same logic belongs in the DFT and bring-up path, and because EV_CSBND is the class that maps onto a real timing quantity.

A lead or lag that is below its minimum in cycles is the digital shadow of a pad-to-pad timing requirement. When a part fails at temperature and passes at room, the useful question is whether the class changes — and a decoder that reports EV_WELL at 25°C and EV_CSBND at 85°C has located the problem in the I/O timing budget rather than in the protocol logic, before anyone has run a single STA report.

12. Failure Signature — Two Engineers, One Waveform, No Measurement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom          a transfer returns wrong data. One engineer says the mode is
                    wrong; another says the slave is dropping a bit. Both point at
                    the same capture. The discussion lasts two days.

   What happened    nobody classified the evidence. The capture had seven edges
                    where eight were expected, which makes every bit POSITION in
                    it meaningless -- so both hypotheses were being argued from a
                    payload that could not support either.

   What would have  the edge count, read once. `EV_COUNT` says: stop looking at
   caught it        the data, the frame boundaries are unknown. That is not a
                    cause, and it eliminates every cause that assumes a
                    well-formed frame.

   The tell         the disagreement is about CAUSES and neither party can name
                    the observation that would settle it. That is always the
                    signature of a missing evidence step, whatever the protocol.

13. Common Misconceptions

"Debugging starts at the waveform." It starts at the classification of the waveform. Opening a viewer and reading the payload of a frame whose edge count is wrong is reading a number whose bit positions are undefined.

"EV_WELL means the capture is correct." It means the capture is admissible. All it says is that the frame is well formed, its boundaries are sound, and no sampled bit was in motion — so comparing the payload against an expectation is now a meaningful thing to do.

"The evidence classes are mutually exclusive." They are made exclusive by a published priority. A quiet select is inherently also a count fault; an early release is a count fault and a boundary fault at once. A table that hides the priority produces arguments instead of diagnoses.

"A finer-grained class set is better." Only if each class can be reached by exactly one fault family. Splitting EV_COUNT into too few and too many is useful; splitting it into frame truncated and clock glitched is not, because a capture cannot distinguish those and the class would be a hypothesis wearing a label.

"A debug method is documentation." A method nobody can execute is a preference. This one is a module with a testable classification, which is why two of its bugs were found by measurement — and both of them were in the bench.

14. Reason It Through

A capture reports edges = 0 and predicates 0011. Which conversations does that end, and which does it start?

It ends every conversation about data, mode, bit order and dummy cycles — none of them is decidable from a frame with no clock at all. It starts exactly one: why did the master assert the select and then not clock? That is a control-path question — an aborted transfer, a lost grant, a state machine stuck between the select and the first edge — and it lives in the master, not on the bus.

Why is the lead measured at the first edge rather than continuously while the select is low?

Because the lead is the interval from the select to the first edge, and until the first edge arrives there is no interval to measure. A continuous check would have to invent a decision about a transaction that has not started clocking, and it would report a lead fault on every quiet select — which is how EV_QUIET and EV_CSBND would stop being separable.

A capture reports predicates 0110. Name the one additional measurement that decides whether the count fault and the boundary fault have one cause or two.

The position of the missing edges. If the frame is short at its end and the lag is short, one cause — the master released early. If the frame is short in the middle (a gap in the clock) while the lag is separately below the minimum, two causes. The edge count alone cannot distinguish those; the timestamped edge positions can, which is why the decoder publishes the count and the chapter after next publishes the positions.

Why does the verdict have to be issued at the release rather than at the last edge?

Because at the last edge the decoder does not yet know it was the last edge. A master may clock again. The release is the first instant a transaction is known to be over, which is also why the lag can only be measured there and why an edge coincident with the release belongs to the ending transaction.

A VHDL port of this decoder disagreed with the Verilog versions on one stimulus. Before looking at any code, what class of cause should you suspect?

A timing-of-evaluation difference rather than a logic difference — because only one stimulus differed and it was the one whose verdict depended on an interval, not on a count or a comparison. Variables update immediately and signals update at the end of a cycle, so any value read during the verdict is a candidate. That is the same reasoning as the chapter's own discriminator discipline: the symptom selected the hypothesis class before any file was opened.

15. Understanding Check

16. Summary

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 — so the first step produces an evidence class and refuses to name a cause. Five single-predicate faults landed in five different classes and legal traffic in EV_WELL, which is the diagonal a triage table has to earn: a class reachable by two unrelated faults is a hypothesis wearing a label. But the classes are mutually exclusive by priority, not by nature, and two captures proved it for two different reasons — a quiet select reads 0011 because zero edges is necessarily a wrong count, a pair no decoder can separate; and an early release reads 0110, a count fault and a boundary fault from one cause, which a finer decoder could have separated and this one does not. The published order ranks findings by how much of the capture each one invalidates, so read downwards it is a list of reasons to stop looking at the payload. And two bugs were found along the way, both in the bench: parking SCLK on the release cycle added a ninth edge to an eight-edge frame, and a VHDL process variable incremented before the verdict was one cycle ahead of the register it modelled — the same lesson twice, that where a value is computed decides which instant it describes.

17. What Comes Next

The evidence is classified. Chapter 18.2 takes the most common EV_WELL failure — a well-formed frame carrying the wrong data — and asks which of the three mode faults produced it, with the honest answer that one pair cannot be separated from the payload at all.

Continue learning