Skip to content
VLSI Mentor

SPI · Module 16

Extracting Protocol Rules and the Verification Plan

Eight pin-observable SPI rules, each with a checker and an exercised counter, because a checker alone cannot tell never-broken from never-reached. Legal traffic violates nothing and exercises all eight; eight injected faults produce a diagonal violation matrix; and one plan row is proved to have no checker at all.

Module 15 finished the design side of SPI. This module builds the environment that decides whether a design is right, and it starts where every verification effort actually starts and almost every one starts badly: with a list.

A verification plan is usually a table of features with a column for "covered". This chapter argues that such a table is worthless until every row carries two things — a checker that can fire, and an exercised counter that proves the checker was reached — and it then builds exactly that for SPI and measures it.

A checker that has never fired and a checker that cannot fire produce the same report. What distinguishes them?

1. What a Rule Has to Be

A verification plan row is only usable if it can be decided from what an observer can actually see. That constraint is sharper than it sounds, and applying it honestly deletes rows.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   USABLE                          NOT USABLE
   ------------------------------  -------------------------------------------
   SCLK idles at CPOL while        "the slave latches on the correct edge"
   deselected                        -- not observable from the pins; it is a
                                      statement about the slave's insides

   CS falls at least LEAD before   "the master is fast enough"
   the first SCLK edge               -- not a protocol rule; a performance
                                      requirement with no pass/fail line

   every half-period is at least   "the data is correct"
   HALF cycles                       -- correct against WHAT? this is a
                                      scoreboard row, not a protocol row

   a transaction carries a whole    "bit order is MSB-first"
   number of frames                  -- see section 7. There is no pin
                                      observation that decides this, ever

The last one matters most, because it is the row that teaches what a plan is for. Bit order is a real requirement, it appears in every datasheet, and no observation of SCLK, CS and MOSI can decide it — Chapter 14.3 showed why: MSB-first and LSB-first both produce a well-formed word of the right length at the right time. A plan that lists it next to the other rows implies a checker exists. One will never be written, and the honest plan says so in the row rather than leaving the gap for somebody to find during signoff.

2. The Eight Rules

These are the rules this module's checker implements, in the form it implements them. Every one is a statement about pins and about the configuration the traffic was supposed to use, and nothing else.

#RuleDecided at
R1SCLK idles at CPOL while deselectedevery deselected cycle
R2CS leads the first SCLK edge by at least LEADthe first edge of a transaction
R3every SCLK half-period is at least HALFevery edge after the first
R4MOSI moves only on launch edges, never at a capture edgeevery MOSI change inside a transaction
R5a transaction carries a whole number of framesthe deassert
R6CS lags the last SCLK edge by at least LAGthe deassert
R7CS is high for at least GAP between transactionsan assert that follows a previous transaction
R8MISO is driven only while the slave is selectedevery deselected cycle

Four of the eight are timing intervals (R2, R3, R6, R7), two are continuous obligations (R1, R8), one is a counting property (R5) and one is a coincidence property (R4). They need four different shapes of logic, and a plan that lists them as eight similar-looking rows hides that the work is not eight equal pieces.

3. The Half Everybody Skips

Each rule gets two counters.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   exercised[r]    how many times the rule's PRECONDITION occurred
   violated[r]     how many of those times the rule was broken

Without the first counter, a report of violated == 0 has two completely different meanings and no way to tell them apart:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   violated = 0, exercised = 4127     the rule holds, and was tested 4127 times
   violated = 0, exercised = 0        the rule was never reached. The checker may
                                      be correct, wrong, or unreachable; this run
                                      contains no information about it

Both print the same green line in most suites. The second is the state a checker spends its life in after somebody renames a signal, changes a default parameter, or writes the precondition with the wrong polarity.

Legal traffic must do two things, and the second is the one that gives the first any weight.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   rule  exercised  violated  what it says
   R1           24         0  SCLK idles at CPOL while deselected
   R2            3         0  CS leads the first edge by LEAD
   R3           21         0  every half-period is at least HALF
   R4            8         0  MOSI moves only on launch edges
   R5            3         0  a whole number of frames
   R6            3         0  CS lags the last edge by LAG
   R7            2         0  CS high for at least GAP
   R8           24         0  MISO driven only while selected

Three transactions. Every rule exercised, none violated. Note R7 = 2: the gap is an obligation between transactions, so three transactions offer two gaps, and a checker that reported three would be counting something else. Note R2 = 3 and R3 = 21: three first edges, and 21 subsequent ones across three eight-bit frames of sixteen edges each minus the three that have no predecessor — 48 − 3 = 45 if every edge counted, but the bench's frames are shorter. The point of reading the numbers this closely is that a plan whose exercise counts you cannot predict is a plan you do not understand.

5. The Violation Matrix

Eight faults are injected, one at a time, and each must fire its own rule and nothing else.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   R1 wrong idle level          -> fired 00000001
   R2 short lead                -> fired 00000010
   R3 short half-period         -> fired 00000100
   R4 phase-mismatched master   -> fired 00001000
   R5 partial frame             -> fired 00010000
   R6 short lag                 -> fired 00100000
   R7 short gap                 -> fired 01000000
   R8 driven while idle         -> fired 10000000

A perfect diagonal. The diagonal is the property worth chasing, and it is a stronger requirement than "every checker fires":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a checker that fires for its own fault           half verified
   a checker that fires for its own fault AND
     for nothing else                               its report is a DIAGNOSIS

The difference shows up the first time a real design fails. A diagonal matrix means the report names the defect; a matrix with off-diagonal entries means the report names three things and an engineer has to guess which is the cause. Chapter 16.4 hits exactly this: one of its injected driver faults produces two rule violations, for a reason that turns out to be arithmetic rather than a checker defect, and the expectation has to be written down as a set rather than a single rule.

6. Building It — Three HDLs

The component is a monitor: it takes pins, the configuration, and a clock of its own, and it produces counters. It drives nothing.

One structural note that recurs through the whole module. The counter arrays stay inside the module, where all three languages have them, and the ports differ:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   SystemVerilog / Verilog   exercised_flat[NRULES*CNT_W-1:0]   a packed bus
   VHDL                      exercised : out rule_counts_t      a named array type

An unpacked array port is SystemVerilog only. Flattening is the portable spelling and it costs the bench a pair of accessor functions; VHDL has array types in ports and uses one, in a package, which is what that language actually offers. The three are the same component expressed in what each language has rather than one language's shape forced onto the others — and that is the policy for every file in this module.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_rule_monitor.sv — the eight pin-observable rules, with paired exercised and violated counters
// spi_rule_monitor.sv
//
// Chapter 16.1 -- a verification plan, made checkable.
//
// A verification plan is usually a table of sentences. This file is the argument
// that a plan is only real when every row of it maps to two things:
//
//     a CHECKER   that fires when the rule is broken
//     a COUNTER   that says the rule was EXERCISED
//
// and that those are different, because a checker alone cannot tell "this rule was
// never broken" from "this rule was never reached". A suite whose plan is all green
// on the first reading is almost always reporting the second.
//
// THE EIGHT RULES, AND WHERE EACH ONE CAME FROM.
//
// Every rule here is observable FROM THE PINS ALONE. That is deliberate and it is
// the plan's most important property: a rule that needs to look inside the DUT is a
// rule the DUT gets a vote on (Chapter 16.5 is about what that costs).
//
//   R1  SCLK idles at CPOL while deselected                         (13.5, 14.6)
//   R2  CS leads the first SCLK edge by at least LEAD               (14.4)
//   R3  every SCLK half-period is at least HALF                     (14.1, 15.3)
//   R4  MOSI changes only on LAUNCH edges, never on capture edges   (13.5, 14.1)
//   R5  a transaction carries a whole number of frames              (14.2)
//   R6  CS lags the last SCLK edge by at least LAG                  (13.7)
//   R7  CS stays high for at least GAP between transactions         (14.5)
//   R8  MISO is driven only while selected                          (14.5)
//
// WHY "EXERCISED" IS THE HARDER HALF.
//
// Take R2. Its checker fires when the lead is shorter than LEAD. A suite that never
// drives a short lead never fires it -- and a suite that never drives a lead AT ALL
// also never fires it, because there was no transaction. Those two suites produce
// identical reports and have tested completely different amounts.
//
// So each rule here publishes BOTH:
//
//     exercised[i]   the number of times the rule's precondition occurred
//     violated[i]    the number of times the rule was broken
//
// and a plan row is only closed when `exercised > 0`. That one extra counter is the
// difference between a plan and a list of hopes.
//
// AND THE RULE THIS BLOCK DELIBERATELY CANNOT CHECK.
//
// Bit ORDER. Chapter 14.3 established that MSB-first and LSB-first produce
// well-formed words of the right length at the right time, so no observation of the
// pins distinguishes them. It is in the plan as a row with no checker, marked
// "configuration, not checkable" -- because a plan that silently omits it looks
// complete and a plan that lists it honestly does not.

module spi_rule_monitor #(
    parameter int LEAD   = 4,    // recovered cycles, CS assert to first edge
    parameter int HALF   = 3,    // cycles per SCLK half-period
    parameter int LAG    = 2,    // cycles, last edge to CS deassert
    parameter int GAP    = 3,    // cycles of CS high between transactions
    parameter int LEN_W  = 6,
    parameter int CNT_W  = 16,
    parameter int NRULES = 8
) (
    input  wire              clk,      // the OBSERVER's clock, not the DUT's
    input  wire              rst_n,

    // --- the pins, and nothing else ---------------------------------------
    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,
    input  wire              miso_driven,   // a bus monitor's view: is MISO driven?

    // --- the configuration the rules are checked AGAINST -------------------
    input  wire              cpol,
    input  wire              cpha,
    input  wire [LEN_W-1:0]  len,

    // --- the plan, as two vectors ------------------------------------------
    // FLATTENED into packed buses rather than exposed as unpacked arrays, because an
    // unpacked array port is SystemVerilog only and this component exists in three
    // languages. The VHDL version uses a proper array type in its port, which is
    // idiomatic there -- so the three are structurally the same component expressed
    // in what each language actually offers, rather than one language's shape forced
    // on the others.
    output wire [NRULES*CNT_W-1:0] exercised_flat,
    output wire [NRULES*CNT_W-1:0] violated_flat,
    input  wire                    clr
);

    // Rule indices, named so that a report can be read without the source.
    localparam int R_IDLE  = 0,   // R1
                   R_LEAD  = 1,   // R2
                   R_HALF  = 2,   // R3
                   R_LAUNCH= 3,   // R4
                   R_FRAME = 4,   // R5
                   R_LAG   = 5,   // R6
                   R_GAP   = 6,   // R7
                   R_DRIVE = 7;   // R8

    // --- 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;

    // WHICH TRANSACTION A COINCIDENT EVENT BELONGS TO, which is a question a pin
    // monitor has to answer and "is the select asserted right now?" is the wrong
    // answer to. A master that deasserts on the same cycle as its final edge makes
    // `cs_n` read high on the cycle that edge is observed -- so `~cs_n` attributes
    // that edge to NO transaction, it is never counted, and the frame remainder ends
    // one short. R5 then fires on a master whose only fault was a short lag.
    //
    // An edge observed on the deassert cycle belongs to the transaction that is
    // ending, so the test includes it.
    wire in_txn = ~cs_n | cs_deassert;
    wire sclk_edge   = sclk ^ sclk_d;
    wire mosi_change = mosi ^ mosi_d;

    // A LAUNCH edge is the one that is not the capture edge. Chapter 14.6's mapping:
    // with CPOL consumed, CPHA=0 captures on the leading edge and launches on the
    // trailing one. `leading` means SCLK left its idle level.
    wire leading  = sclk_edge & (sclk != cpol);
    wire trailing = sclk_edge & (sclk == cpol);
    wire capture  = cpha ? trailing : leading;
    wire launch   = cpha ? leading  : trailing;

    // --- interval measurement ----------------------------------------------
    // One counter, reused by three rules, and reloaded with ONE for Chapter 14.1's
    // reason: the cycle an event is detected on is the first cycle of the next
    // interval.
    reg [CNT_W-1:0] since_cs_assert;
    reg [CNT_W-1:0] since_sclk_edge;
    reg [CNT_W-1:0] since_cs_deassert;
    reg             seen_edge_in_txn;
    reg [LEN_W:0]   in_frame;              // running remainder, Chapter 14.2
    reg             had_a_txn;             // so GAP is not checked before the first

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

    // An interval measured when the event that ENDS it is happening on this very
    // cycle is ZERO, not the previous interval's value. Without this, a master that
    // deasserts on the same cycle as its last edge measures the whole preceding
    // half-period as its lag and R6 never fires -- which is the worst kind of
    // checker bug, because the report says the rule is fine.
    // Each one asks "is the event that STARTED this interval happening right now?",
    // and each one therefore tests a DIFFERENT signal. Getting that wrong is easy and
    // the first version did: the lead was gated on `sclk_edge`, which is always true
    // at the moment the lead is checked -- so the lead measured zero on every
    // transaction and R2 fired on all of them, including the legal ones.
    wire [CNT_W-1:0] lead_now = cs_assert   ? {CNT_W{1'b0}} : since_cs_assert;
    wire [CNT_W-1:0] lag_now  = sclk_edge   ? {CNT_W{1'b0}} : since_sclk_edge;
    wire [CNT_W-1:0] gap_now  = cs_deassert ? {CNT_W{1'b0}} : since_cs_deassert;

    // And the remainder AS IT WILL BE once this cycle's edge is counted, because the
    // counter itself updates non-blockingly and the R5 check runs in the same cycle.
    wire [LEN_W:0] frame_now =
        (sclk_edge && in_txn)
            ? ((in_frame == (edges_per_frame - 1'b1)) ? {(LEN_W+1){1'b0}}
                                                      : in_frame + 1'b1)
            : in_frame;

    // The counters stay as arrays INSIDE the module, where every language has them.
    reg [CNT_W-1:0] exercised [0:NRULES-1];
    reg [CNT_W-1:0] violated  [0:NRULES-1];

    genvar gi;
    generate
        for (gi = 0; gi < NRULES; gi = gi + 1) begin : g_flat
            assign exercised_flat[gi*CNT_W +: CNT_W] = exercised[gi];
            assign violated_flat [gi*CNT_W +: CNT_W] = violated [gi];
        end
    endgenerate

    integer r;

    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;
            since_cs_assert   <= {CNT_W{1'b0}};
            since_sclk_edge   <= {CNT_W{1'b0}};
            since_cs_deassert <= {CNT_W{1'b0}};
            seen_edge_in_txn  <= 1'b0;
            in_frame          <= {(LEN_W+1){1'b0}};
            had_a_txn         <= 1'b0;
            for (r = 0; r < NRULES; r = r + 1) begin
                exercised[r] <= {CNT_W{1'b0}};
                violated[r]  <= {CNT_W{1'b0}};
            end
        end else begin
            sclk_d <= sclk;
            cs_n_d <= cs_n;
            mosi_d <= mosi;

            if (clr) begin
                for (r = 0; r < NRULES; r = r + 1) begin
                    exercised[r] <= {CNT_W{1'b0}};
                    violated[r]  <= {CNT_W{1'b0}};
                end
            end

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

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

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

            // ---------------- R1: SCLK idles at CPOL while deselected ------
            // EXERCISED on every deselected cycle -- which is the right choice and
            // worth arguing: the rule is a continuous obligation, so a suite that
            // was ever deselected has exercised it. Counting only the assert would
            // make the counter a transaction count wearing a rule's name.
            if (cs_n) begin
                exercised[R_IDLE] <= exercised[R_IDLE] + 1'b1;
                if (sclk != cpol)
                    violated[R_IDLE] <= violated[R_IDLE] + 1'b1;
            end

            // ---------------- R2: the lead ---------------------------------
            // EXERCISED at the FIRST edge of a transaction, because that is the
            // only moment the lead exists. A transaction with no edges never
            // exercises it, and that is correct: there was no lead to measure.
            if (sclk_edge && in_txn && !seen_edge_in_txn) begin
                seen_edge_in_txn <= 1'b1;
                exercised[R_LEAD] <= exercised[R_LEAD] + 1'b1;
                if (lead_now < LEAD)
                    violated[R_LEAD] <= violated[R_LEAD] + 1'b1;
            end

            // ---------------- R3: the half-period --------------------------
            // EXERCISED on every edge that FOLLOWS another edge inside the same
            // transaction. The first edge has no preceding one, and the interval
            // after a deassert is not a half-period.
            if (sclk_edge && in_txn && seen_edge_in_txn) begin
                exercised[R_HALF] <= exercised[R_HALF] + 1'b1;
                if (since_sclk_edge < HALF)
                    violated[R_HALF] <= violated[R_HALF] + 1'b1;
            end

            // ---------------- R4: MOSI moves only on launch edges ----------
            // EXERCISED whenever MOSI changes inside a transaction -- which makes a
            // constant data pattern exercise it ZERO times, and that is the honest
            // count. A suite sending only 0x00 has not tested this rule at all.
            if (mosi_change && in_txn) begin
                exercised[R_LAUNCH] <= exercised[R_LAUNCH] + 1'b1;
                // A change is legal on the launch edge and for a short while after
                // it. It is illegal AT a capture edge, which is what would put the
                // data in motion when the slave samples it (Chapter 14.6).
                if (capture)
                    violated[R_LAUNCH] <= violated[R_LAUNCH] + 1'b1;
            end

            // ---------------- the frame remainder -------------------------
            if (sclk_edge && in_txn) begin
                if (in_frame == (edges_per_frame - 1'b1))
                    in_frame <= {(LEN_W+1){1'b0}};
                else
                    in_frame <= in_frame + 1'b1;
            end

            // ---------------- R5, R6 at the deassert ----------------------
            if (cs_deassert) begin
                had_a_txn <= 1'b1;

                // R5 is EXERCISED by any transaction that carried an edge. An
                // empty select pulse does not exercise it -- there were no frames
                // to be whole or partial.
                if (seen_edge_in_txn || (sclk_edge && in_txn)) begin
                    exercised[R_FRAME] <= exercised[R_FRAME] + 1'b1;
                    if (frame_now != {(LEN_W+1){1'b0}})
                        violated[R_FRAME] <= violated[R_FRAME] + 1'b1;

                    // R6 likewise needs a last edge to lag from.
                    exercised[R_LAG] <= exercised[R_LAG] + 1'b1;
                    if (lag_now < LAG)
                        violated[R_LAG] <= violated[R_LAG] + 1'b1;
                end

                seen_edge_in_txn <= 1'b0;
                in_frame         <= {(LEN_W+1){1'b0}};
            end

            // ---------------- R7: the gap ---------------------------------
            // EXERCISED at an assert that FOLLOWS a previous transaction. The very
            // first assert of a run has no gap before it, and counting it would
            // make every run report one spurious exercise.
            if (cs_assert && had_a_txn) begin
                exercised[R_GAP] <= exercised[R_GAP] + 1'b1;
                if (gap_now < GAP)
                    violated[R_GAP] <= violated[R_GAP] + 1'b1;
            end

            // ---------------- R8: MISO driven only while selected ---------
            // EXERCISED on every deselected cycle, for R1's reason.
            if (cs_n) begin
                exercised[R_DRIVE] <= exercised[R_DRIVE] + 1'b1;
                if (miso_driven)
                    violated[R_DRIVE] <= violated[R_DRIVE] + 1'b1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_rule_monitor.v — the same design in Verilog-2001
// spi_rule_monitor.v
//
// Chapter 16.1 -- a verification plan, made checkable.
//
// A verification plan is usually a table of sentences. This file is the argument
// that a plan is only real when every row of it maps to two things:
//
//     a CHECKER   that fires when the rule is broken
//     a COUNTER   that says the rule was EXERCISED
//
// and that those are different, because a checker alone cannot tell "this rule was
// never broken" from "this rule was never reached". A suite whose plan is all green
// on the first reading is almost always reporting the second.
//
// THE EIGHT RULES, AND WHERE EACH ONE CAME FROM.
//
// Every rule here is observable FROM THE PINS ALONE. That is deliberate and it is
// the plan's most important property: a rule that needs to look inside the DUT is a
// rule the DUT gets a vote on (Chapter 16.5 is about what that costs).
//
//   R1  SCLK idles at CPOL while deselected                         (13.5, 14.6)
//   R2  CS leads the first SCLK edge by at least LEAD               (14.4)
//   R3  every SCLK half-period is at least HALF                     (14.1, 15.3)
//   R4  MOSI changes only on LAUNCH edges, never on capture edges   (13.5, 14.1)
//   R5  a transaction carries a whole number of frames              (14.2)
//   R6  CS lags the last SCLK edge by at least LAG                  (13.7)
//   R7  CS stays high for at least GAP between transactions         (14.5)
//   R8  MISO is driven only while selected                          (14.5)
//
// WHY "EXERCISED" IS THE HARDER HALF.
//
// Take R2. Its checker fires when the lead is shorter than LEAD. A suite that never
// drives a short lead never fires it -- and a suite that never drives a lead AT ALL
// also never fires it, because there was no transaction. Those two suites produce
// identical reports and have tested completely different amounts.
//
// So each rule here publishes BOTH:
//
//     exercised[i]   the number of times the rule's precondition occurred
//     violated[i]    the number of times the rule was broken
//
// and a plan row is only closed when `exercised > 0`. That one extra counter is the
// difference between a plan and a list of hopes.
//
// AND THE RULE THIS BLOCK DELIBERATELY CANNOT CHECK.
//
// Bit ORDER. Chapter 14.3 established that MSB-first and LSB-first produce
// well-formed words of the right length at the right time, so no observation of the
// pins distinguishes them. It is in the plan as a row with no checker, marked
// "configuration, not checkable" -- because a plan that silently omits it looks
// complete and a plan that lists it honestly does not.

module spi_rule_monitor #(
    parameter LEAD   = 4,    // recovered cycles, CS assert to first edge
    parameter HALF   = 3,    // cycles per SCLK half-period
    parameter LAG    = 2,    // cycles, last edge to CS deassert
    parameter GAP    = 3,    // cycles of CS high between transactions
    parameter LEN_W  = 6,
    parameter CNT_W  = 16,
    parameter NRULES = 8
) (
    input  wire              clk,      // the OBSERVER's clock, not the DUT's
    input  wire              rst_n,

    // --- the pins, and nothing else ---------------------------------------
    input  wire              sclk,
    input  wire              cs_n,
    input  wire              mosi,
    input  wire              miso_driven,   // a bus monitor's view: is MISO driven?

    // --- the configuration the rules are checked AGAINST -------------------
    input  wire              cpol,
    input  wire              cpha,
    input  wire [LEN_W-1:0]  len,

    // --- the plan, as two vectors ------------------------------------------
    // FLATTENED into packed buses rather than exposed as unpacked arrays, because an
    // unpacked array port is SystemVerilog only and this component exists in three
    // languages. The VHDL version uses a proper array type in its port, which is
    // idiomatic there -- so the three are structurally the same component expressed
    // in what each language actually offers, rather than one language's shape forced
    // on the others.
    output wire [NRULES*CNT_W-1:0] exercised_flat,
    output wire [NRULES*CNT_W-1:0] violated_flat,
    input  wire                    clr
);

    // Rule indices, named so that a report can be read without the source.
    localparam R_IDLE  = 0,   // R1
                   R_LEAD  = 1,   // R2
                   R_HALF  = 2,   // R3
                   R_LAUNCH= 3,   // R4
                   R_FRAME = 4,   // R5
                   R_LAG   = 5,   // R6
                   R_GAP   = 6,   // R7
                   R_DRIVE = 7;   // R8

    // --- 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;

    // WHICH TRANSACTION A COINCIDENT EVENT BELONGS TO, which is a question a pin
    // monitor has to answer and "is the select asserted right now?" is the wrong
    // answer to. A master that deasserts on the same cycle as its final edge makes
    // `cs_n` read high on the cycle that edge is observed -- so `~cs_n` attributes
    // that edge to NO transaction, it is never counted, and the frame remainder ends
    // one short. R5 then fires on a master whose only fault was a short lag.
    //
    // An edge observed on the deassert cycle belongs to the transaction that is
    // ending, so the test includes it.
    wire in_txn = ~cs_n | cs_deassert;
    wire sclk_edge   = sclk ^ sclk_d;
    wire mosi_change = mosi ^ mosi_d;

    // A LAUNCH edge is the one that is not the capture edge. Chapter 14.6's mapping:
    // with CPOL consumed, CPHA=0 captures on the leading edge and launches on the
    // trailing one. `leading` means SCLK left its idle level.
    wire leading  = sclk_edge & (sclk != cpol);
    wire trailing = sclk_edge & (sclk == cpol);
    wire capture  = cpha ? trailing : leading;
    wire launch   = cpha ? leading  : trailing;

    // --- interval measurement ----------------------------------------------
    // One counter, reused by three rules, and reloaded with ONE for Chapter 14.1's
    // reason: the cycle an event is detected on is the first cycle of the next
    // interval.
    reg [CNT_W-1:0] since_cs_assert;
    reg [CNT_W-1:0] since_sclk_edge;
    reg [CNT_W-1:0] since_cs_deassert;
    reg             seen_edge_in_txn;
    reg [LEN_W:0]   in_frame;              // running remainder, Chapter 14.2
    reg             had_a_txn;             // so GAP is not checked before the first

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

    // An interval measured when the event that ENDS it is happening on this very
    // cycle is ZERO, not the previous interval's value. Without this, a master that
    // deasserts on the same cycle as its last edge measures the whole preceding
    // half-period as its lag and R6 never fires -- which is the worst kind of
    // checker bug, because the report says the rule is fine.
    // Each one asks "is the event that STARTED this interval happening right now?",
    // and each one therefore tests a DIFFERENT signal. Getting that wrong is easy and
    // the first version did: the lead was gated on `sclk_edge`, which is always true
    // at the moment the lead is checked -- so the lead measured zero on every
    // transaction and R2 fired on all of them, including the legal ones.
    wire [CNT_W-1:0] lead_now = cs_assert   ? {CNT_W{1'b0}} : since_cs_assert;
    wire [CNT_W-1:0] lag_now  = sclk_edge   ? {CNT_W{1'b0}} : since_sclk_edge;
    wire [CNT_W-1:0] gap_now  = cs_deassert ? {CNT_W{1'b0}} : since_cs_deassert;

    // And the remainder AS IT WILL BE once this cycle's edge is counted, because the
    // counter itself updates non-blockingly and the R5 check runs in the same cycle.
    wire [LEN_W:0] frame_now =
        (sclk_edge && in_txn)
            ? ((in_frame == (edges_per_frame - 1'b1)) ? {(LEN_W+1){1'b0}}
                                                      : in_frame + 1'b1)
            : in_frame;

    // The counters stay as arrays INSIDE the module, where every language has them.
    reg [CNT_W-1:0] exercised [0:NRULES-1];
    reg [CNT_W-1:0] violated  [0:NRULES-1];

    genvar gi;
    generate
        for (gi = 0; gi < NRULES; gi = gi + 1) begin : g_flat
            assign exercised_flat[gi*CNT_W +: CNT_W] = exercised[gi];
            assign violated_flat [gi*CNT_W +: CNT_W] = violated [gi];
        end
    endgenerate

    integer r;

    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;
            since_cs_assert   <= {CNT_W{1'b0}};
            since_sclk_edge   <= {CNT_W{1'b0}};
            since_cs_deassert <= {CNT_W{1'b0}};
            seen_edge_in_txn  <= 1'b0;
            in_frame          <= {(LEN_W+1){1'b0}};
            had_a_txn         <= 1'b0;
            for (r = 0; r < NRULES; r = r + 1) begin
                exercised[r] <= {CNT_W{1'b0}};
                violated[r]  <= {CNT_W{1'b0}};
            end
        end else begin
            sclk_d <= sclk;
            cs_n_d <= cs_n;
            mosi_d <= mosi;

            if (clr) begin
                for (r = 0; r < NRULES; r = r + 1) begin
                    exercised[r] <= {CNT_W{1'b0}};
                    violated[r]  <= {CNT_W{1'b0}};
                end
            end

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

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

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

            // ---------------- R1: SCLK idles at CPOL while deselected ------
            // EXERCISED on every deselected cycle -- which is the right choice and
            // worth arguing: the rule is a continuous obligation, so a suite that
            // was ever deselected has exercised it. Counting only the assert would
            // make the counter a transaction count wearing a rule's name.
            if (cs_n) begin
                exercised[R_IDLE] <= exercised[R_IDLE] + 1'b1;
                if (sclk != cpol)
                    violated[R_IDLE] <= violated[R_IDLE] + 1'b1;
            end

            // ---------------- R2: the lead ---------------------------------
            // EXERCISED at the FIRST edge of a transaction, because that is the
            // only moment the lead exists. A transaction with no edges never
            // exercises it, and that is correct: there was no lead to measure.
            if (sclk_edge && in_txn && !seen_edge_in_txn) begin
                seen_edge_in_txn <= 1'b1;
                exercised[R_LEAD] <= exercised[R_LEAD] + 1'b1;
                if (lead_now < LEAD)
                    violated[R_LEAD] <= violated[R_LEAD] + 1'b1;
            end

            // ---------------- R3: the half-period --------------------------
            // EXERCISED on every edge that FOLLOWS another edge inside the same
            // transaction. The first edge has no preceding one, and the interval
            // after a deassert is not a half-period.
            if (sclk_edge && in_txn && seen_edge_in_txn) begin
                exercised[R_HALF] <= exercised[R_HALF] + 1'b1;
                if (since_sclk_edge < HALF)
                    violated[R_HALF] <= violated[R_HALF] + 1'b1;
            end

            // ---------------- R4: MOSI moves only on launch edges ----------
            // EXERCISED whenever MOSI changes inside a transaction -- which makes a
            // constant data pattern exercise it ZERO times, and that is the honest
            // count. A suite sending only 0x00 has not tested this rule at all.
            if (mosi_change && in_txn) begin
                exercised[R_LAUNCH] <= exercised[R_LAUNCH] + 1'b1;
                // A change is legal on the launch edge and for a short while after
                // it. It is illegal AT a capture edge, which is what would put the
                // data in motion when the slave samples it (Chapter 14.6).
                if (capture)
                    violated[R_LAUNCH] <= violated[R_LAUNCH] + 1'b1;
            end

            // ---------------- the frame remainder -------------------------
            if (sclk_edge && in_txn) begin
                if (in_frame == (edges_per_frame - 1'b1))
                    in_frame <= {(LEN_W+1){1'b0}};
                else
                    in_frame <= in_frame + 1'b1;
            end

            // ---------------- R5, R6 at the deassert ----------------------
            if (cs_deassert) begin
                had_a_txn <= 1'b1;

                // R5 is EXERCISED by any transaction that carried an edge. An
                // empty select pulse does not exercise it -- there were no frames
                // to be whole or partial.
                if (seen_edge_in_txn || (sclk_edge && in_txn)) begin
                    exercised[R_FRAME] <= exercised[R_FRAME] + 1'b1;
                    if (frame_now != {(LEN_W+1){1'b0}})
                        violated[R_FRAME] <= violated[R_FRAME] + 1'b1;

                    // R6 likewise needs a last edge to lag from.
                    exercised[R_LAG] <= exercised[R_LAG] + 1'b1;
                    if (lag_now < LAG)
                        violated[R_LAG] <= violated[R_LAG] + 1'b1;
                end

                seen_edge_in_txn <= 1'b0;
                in_frame         <= {(LEN_W+1){1'b0}};
            end

            // ---------------- R7: the gap ---------------------------------
            // EXERCISED at an assert that FOLLOWS a previous transaction. The very
            // first assert of a run has no gap before it, and counting it would
            // make every run report one spurious exercise.
            if (cs_assert && had_a_txn) begin
                exercised[R_GAP] <= exercised[R_GAP] + 1'b1;
                if (gap_now < GAP)
                    violated[R_GAP] <= violated[R_GAP] + 1'b1;
            end

            // ---------------- R8: MISO driven only while selected ---------
            // EXERCISED on every deselected cycle, for R1's reason.
            if (cs_n) begin
                exercised[R_DRIVE] <= exercised[R_DRIVE] + 1'b1;
                if (miso_driven)
                    violated[R_DRIVE] <= violated[R_DRIVE] + 1'b1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_rule_monitor.vhd — the same design in VHDL
-- spi_rule_monitor.vhd
--
-- Chapter 16.1 -- a verification plan, made checkable.
--
-- A verification plan is usually a table of sentences. This file is the argument
-- that a plan is only real when every row of it maps to two things:
--
--     a CHECKER   that fires when the rule is broken
--     a COUNTER   that says the rule was EXERCISED
--
-- and that those are different, because a checker alone cannot tell "this rule was
-- never broken" from "this rule was never reached". A suite whose plan is all green
-- on the first reading is almost always reporting the second.
--
-- THE EIGHT RULES, AND WHERE EACH ONE CAME FROM.
--
-- Every rule here is observable FROM THE PINS ALONE. That is deliberate and it is
-- the plan's most important property: a rule that needs to look inside the DUT is a
-- rule the DUT gets a vote on (Chapter 16.5 is about what that costs).
--
--   R1  SCLK idles at CPOL while deselected                         (13.5, 14.6)
--   R2  CS leads the first SCLK edge by at least LEAD               (14.4)
--   R3  every SCLK half-period is at least HALF                     (14.1, 15.3)
--   R4  MOSI changes only on LAUNCH edges, never on capture edges   (13.5, 14.1)
--   R5  a transaction carries a whole number of frames              (14.2)
--   R6  CS lags the last SCLK edge by at least LAG                  (13.7)
--   R7  CS stays high for at least GAP between transactions         (14.5)
--   R8  MISO is driven only while selected                          (14.5)
--
-- WHY "EXERCISED" IS THE HARDER HALF.
--
-- Take R2. Its checker fires when the lead is shorter than LEAD. A suite that never
-- drives a short lead never fires it -- and a suite that never drives a lead AT ALL
-- also never fires it, because there was no transaction. Those two suites produce
-- identical reports and have tested completely different amounts.
--
-- So each rule here publishes BOTH:
--
--     exercised[i]   the number of times the rule's precondition occurred
--     violated[i]    the number of times the rule was broken
--
-- and a plan row is only closed when `exercised > 0`. That one extra counter is the
-- difference between a plan and a list of hopes.
--
-- AND THE RULE THIS BLOCK DELIBERATELY CANNOT CHECK.
--
-- Bit ORDER. Chapter 14.3 established that MSB-first and LSB-first produce
-- well-formed words of the right length at the right time, so no observation of the
-- pins distinguishes them. It is in the plan as a row with no checker, marked
-- "configuration, not checkable" -- because a plan that silently omits it looks
-- complete and a plan that lists it honestly does not.

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

-- The rule vector gets a named TYPE in a package, which is what VHDL offers in place
-- of the packed-bus flattening the SystemVerilog and Verilog versions need. The
-- component is the same; the port is expressed in what the language actually has.
package spi_rule_pkg is
    constant NRULES : natural := 8;

    -- Named indices, so a report can be read without the source.
    constant R_IDLE   : natural := 0;   -- R1
    constant R_LEAD   : natural := 1;   -- R2
    constant R_HALF   : natural := 2;   -- R3
    constant R_LAUNCH : natural := 3;   -- R4
    constant R_FRAME  : natural := 4;   -- R5
    constant R_LAG    : natural := 5;   -- R6
    constant R_GAP    : natural := 6;   -- R7
    constant R_DRIVE  : natural := 7;   -- R8

    type rule_counts_t is array (0 to NRULES - 1) of natural;

    function rule_name(i : natural) return string;
end package;

package body spi_rule_pkg is
    function rule_name(i : natural) return string is
    begin
        case i is
            when R_IDLE   => return "SCLK idles at CPOL while deselected";
            when R_LEAD   => return "CS leads the first edge by LEAD";
            when R_HALF   => return "every half-period is at least HALF";
            when R_LAUNCH => return "MOSI moves only on launch edges";
            when R_FRAME  => return "a whole number of frames";
            when R_LAG    => return "CS lags the last edge by LAG";
            when R_GAP    => return "CS high for at least GAP";
            when others   => return "MISO driven only while selected";
        end case;
    end function;
end package body;

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

entity spi_rule_monitor is
    generic (
        LEAD  : natural  := 4;    -- cycles, CS assert to first edge
        HALF  : natural  := 3;    -- cycles per SCLK half-period
        LAG   : natural  := 2;    -- cycles, last edge to CS deassert
        GAP   : natural  := 3;    -- cycles of CS high between transactions
        LEN_W : positive := 6
    );
    port (
        clk         : in  std_logic;   -- the OBSERVER's clock, not the DUT's
        rst_n       : in  std_logic;

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

        -- the configuration the rules are checked AGAINST
        cpol        : in  std_logic;
        cpha        : in  std_logic;
        len         : in  unsigned(LEN_W - 1 downto 0);

        -- the plan, as two vectors
        exercised   : out rule_counts_t;
        violated    : out rule_counts_t;
        clr         : in  std_logic
    );
end entity;

architecture rtl of spi_rule_monitor is

    signal sclk_d : std_logic := '0';
    signal cs_n_d : std_logic := '1';
    signal mosi_d : std_logic := '0';

    signal cs_assert, cs_deassert, sclk_edge, mosi_change : std_logic;
    signal leading, trailing, capture, launch             : std_logic;
    signal in_txn                                         : std_logic;

    signal since_cs_assert   : natural := 0;
    signal since_sclk_edge   : natural := 0;
    signal since_cs_deassert : natural := 0;
    signal seen_edge_in_txn  : std_logic := '0';
    signal in_frame          : natural := 0;
    signal had_a_txn         : std_logic := '0';

    signal edges_per_frame : natural;
    signal lead_now, lag_now, gap_now, frame_now : natural;

    signal ex_r, vi_r : rule_counts_t := (others => 0);

    constant SAT : natural := 65535;

begin

    cs_assert   <= (not cs_n) and cs_n_d;
    cs_deassert <= cs_n and (not cs_n_d);
    sclk_edge   <= sclk xor sclk_d;
    mosi_change <= mosi xor mosi_d;

    -- WHICH TRANSACTION A COINCIDENT EVENT BELONGS TO. An edge observed on the
    -- deassert cycle belongs to the transaction that is ending, so the test includes
    -- it -- `not cs_n` alone attributes that edge to no transaction at all.
    in_txn <= (not cs_n) or cs_deassert;

    -- A LAUNCH edge is the one that is not the capture edge (Chapter 14.6's mapping,
    -- with CPOL already consumed).
    leading  <= sclk_edge when sclk /= cpol else '0';
    trailing <= sclk_edge when sclk  = cpol else '0';
    capture  <= trailing when cpha = '1' else leading;
    launch   <= leading  when cpha = '1' else trailing;

    edges_per_frame <= to_integer(len) * 2;

    -- Each of these asks "is the event that STARTED this interval happening right
    -- now?", and each therefore tests a DIFFERENT signal.
    lead_now <= 0 when cs_assert   = '1' else since_cs_assert;
    lag_now  <= 0 when sclk_edge   = '1' else since_sclk_edge;
    gap_now  <= 0 when cs_deassert = '1' else since_cs_deassert;

    -- The remainder AS IT WILL BE once this cycle's edge is counted.
    frame_now <= 0            when (sclk_edge = '1' and in_txn = '1'
                                    and in_frame = edges_per_frame - 1)
            else in_frame + 1 when (sclk_edge = '1' and in_txn = '1')
            else in_frame;

    exercised <= ex_r;
    violated  <= vi_r;

    rules : process (clk, rst_n)
        procedure bump(idx : natural; hit : boolean) is
        begin
            if ex_r(idx) < SAT then
                ex_r(idx) <= ex_r(idx) + 1;
            end if;
            if hit and vi_r(idx) < SAT then
                vi_r(idx) <= vi_r(idx) + 1;
            end if;
        end procedure;
    begin
        if rst_n = '0' then
            sclk_d            <= '0';
            cs_n_d            <= '1';
            mosi_d            <= '0';
            since_cs_assert   <= 0;
            since_sclk_edge   <= 0;
            since_cs_deassert <= 0;
            seen_edge_in_txn  <= '0';
            in_frame          <= 0;
            had_a_txn         <= '0';
            ex_r              <= (others => 0);
            vi_r              <= (others => 0);
        elsif rising_edge(clk) then
            sclk_d <= sclk;
            cs_n_d <= cs_n;
            mosi_d <= mosi;

            if clr = '1' then
                ex_r <= (others => 0);
                vi_r <= (others => 0);
            end if;

            -- intervals
            if cs_assert = '1' then since_cs_assert <= 1;
            elsif since_cs_assert < SAT then since_cs_assert <= since_cs_assert + 1;
            end if;

            if sclk_edge = '1' then since_sclk_edge <= 1;
            elsif since_sclk_edge < SAT then since_sclk_edge <= since_sclk_edge + 1;
            end if;

            if cs_deassert = '1' then since_cs_deassert <= 1;
            elsif since_cs_deassert < SAT then
                since_cs_deassert <= since_cs_deassert + 1;
            end if;

            -- R1: a continuous obligation, so every deselected cycle exercises it
            if cs_n = '1' then
                bump(R_IDLE, sclk /= cpol);
            end if;

            -- R2: the lead, at the FIRST edge of a transaction
            if sclk_edge = '1' and in_txn = '1' and seen_edge_in_txn = '0' then
                seen_edge_in_txn <= '1';
                bump(R_LEAD, lead_now < LEAD);
            end if;

            -- R3: the half-period, on every edge that follows another one
            if sclk_edge = '1' and in_txn = '1' and seen_edge_in_txn = '1' then
                bump(R_HALF, since_sclk_edge < HALF);
            end if;

            -- R4: MOSI must not move AT a capture edge. Exercised only when MOSI
            -- changes, so a constant data pattern exercises it ZERO times.
            if mosi_change = '1' and in_txn = '1' then
                bump(R_LAUNCH, capture = '1');
            end if;

            -- the frame remainder
            if sclk_edge = '1' and in_txn = '1' then
                if in_frame = edges_per_frame - 1 then
                    in_frame <= 0;
                else
                    in_frame <= in_frame + 1;
                end if;
            end if;

            -- R5 and R6 at the deassert
            if cs_deassert = '1' then
                had_a_txn <= '1';
                if seen_edge_in_txn = '1' or (sclk_edge = '1' and in_txn = '1') then
                    bump(R_FRAME, frame_now /= 0);
                    bump(R_LAG,   lag_now < LAG);
                end if;
                seen_edge_in_txn <= '0';
                in_frame         <= 0;
            end if;

            -- R7: the gap, at an assert that FOLLOWS a previous transaction
            if cs_assert = '1' and had_a_txn = '1' then
                bump(R_GAP, gap_now < GAP);
            end if;

            -- R8: a continuous obligation, like R1
            if cs_n = '1' then
                bump(R_DRIVE, miso_driven = '1');
            end if;
        end if;
    end process;

end architecture;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_rule_monitor_tb.sv — legal traffic exercises all eight rules; eight injected faults produce a diagonal violation matrix
// spi_rule_monitor_tb.sv
//
// The experiment that makes a verification plan trustworthy: drive LEGAL traffic and
// then break each rule ONE AT A TIME, and check three things each time.
//
//   1  the rule that was broken fires
//   2  no OTHER rule fires
//   3  the rule was EXERCISED -- so a zero in the violated column means "not broken"
//      rather than "never reached"
//
// The second is the one benches usually skip, and it is what separates a checker from
// a smoke alarm. A monitor whose lead check also fires on a short gap is a monitor
// that will be disabled the first time an integrator sees it fire for the wrong
// reason.
//
// The third is what the chapter is about. `violated(i) == 0` is meaningless on its
// own; `violated(i) == 0 && exercised(i) > 0` is a closed plan row.

`timescale 1ns/1ps

module spi_rule_monitor_tb;

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

    localparam int R_IDLE = 0, R_LEAD = 1, R_HALF = 2, R_LAUNCH = 3,
                   R_FRAME = 4, R_LAG = 5, R_GAP = 6, R_DRIVE = 7;

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

    reg sclk        = 1'b0;
    reg cs_n        = 1'b1;
    reg mosi        = 1'b0;
    reg miso_driven = 1'b0;
    reg cpol        = 1'b0;
    reg cpha        = 1'b0;
    reg [LEN_W-1:0] len = 6'd4;
    reg clr         = 1'b0;

    wire [NRULES*CNT_W-1:0] exercised_flat;
    wire [NRULES*CNT_W-1:0] violated_flat;

    // Named accessors, so the rest of the bench reads as if the ports were arrays.
    // The flattening is the component's portability concession (see its header) and
    // it should not leak into everything that reads it.
    function automatic [CNT_W-1:0] exercised(input integer i);
        exercised = exercised_flat[i*CNT_W +: CNT_W];
    endfunction
    function automatic [CNT_W-1:0] violated(input integer i);
        violated = violated_flat[i*CNT_W +: CNT_W];
    endfunction

    spi_rule_monitor #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
                       .LEN_W(LEN_W), .CNT_W(CNT_W), .NRULES(NRULES)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso_driven(miso_driven),
        .cpol(cpol), .cpha(cpha), .len(len),
        .exercised_flat(exercised_flat), .violated_flat(violated_flat), .clr(clr)
    );

    integer errors = 0;

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

    // --- a legal master, parameterised so each rule can be broken in turn ---
    // Every timing number is an argument, so a violation is produced by passing a
    // different number rather than by writing a different driver. That matters: two
    // drivers drift, and then a "violation" is a difference between two pieces of
    // bench code.
    // EVERY wait in the stimulus is on the NEGEDGE, and the monitor samples on the
    // posedge. The first version of this bench waited on the posedge and assigned
    // immediately afterwards, so the assignment and the monitor's sample happened at
    // the same simulation time -- and the monitor read the old value. Every rule
    // whose precondition is inside a transaction reported ZERO exercises while the
    // waveform looked perfect.
    //
    // Driving away from the sampling edge is the discipline that fixes it, and
    // Chapter 16.3 is about the language feature that encodes it declaratively
    // instead of leaving it to every task in the file to remember.
    // `drv_cpha` is the MASTER's phase and is separate from the monitor's `cpha`
    // input, which is the whole point of the R4 test: a phase mismatch is a
    // disagreement between two devices, so a bench that changes one signal changes
    // both and measures nothing. The first version did exactly that and R4 never
    // fired.
    task automatic drive_txn(input [7:0] data, input integer nbits,
                             input integer lead_c, input integer half_c,
                             input integer lag_c, input integer stop_early,
                             input bit drv_cpha, input bit drv_miso);
        integer i;
        // A `done` flag rather than `return`: Verilog-2001 has no `return` in a task
        // and no `break` in a loop, so the same source has to work without either --
        // which it does, at the cost of one flag and a guarded loop condition.
        reg     done;
        begin
            done = 1'b0;
            @(negedge clk);
            sclk = cpol;
            cs_n = 1'b0;
            // MISO is driven only AFTER the select is low, which is the obligation R8
            // states. Setting it before the select -- as the first version of this
            // bench did, outside the task -- puts a driven MISO on a deselected bus
            // for one monitor cycle and violates R8 on legal traffic.
            if (drv_miso) miso_driven = 1'b1;
            // The first bit is placed HERE, during the lead, and not alongside the
            // first edge -- placing it on the same cycle as the leading edge makes a
            // legal CPHA=0 master look like it moved MOSI at a capture instant, and
            // R4 fired once on every legal run.
            if (!drv_cpha) begin
                @(negedge clk);
                mosi = data[nbits-1];
            end
            repeat (lead_c) @(negedge clk);
            for (i = 0; i < nbits && !done; i = i + 1) begin
                if (!drv_cpha) begin
                    // CPHA=0: MOSI is launched on the TRAILING edge. The first bit
                    // was already placed during the lead, above.
                    sclk = ~cpol;                          // leading  (capture)
                    repeat (half_c) @(negedge clk);
                    sclk = cpol;                           // trailing (launch)
                    if (i < nbits-1) mosi = data[nbits-2-i];
                    // On the LAST bit the trailing wait IS the lag -- there is no
                    // following edge, so R3 does not measure this interval and
                    // shortening it is how a short lag is produced at all. The first
                    // version waited a full half-period here and then `lag_c` more,
                    // so `lag_c = 0` still gave a lag of half a period and R6 could
                    // not be made to fire.
                    if (i == nbits-1) repeat (lag_c)  @(negedge clk);
                    else              repeat (half_c) @(negedge clk);
                end else begin
                    sclk = ~cpol;                          // leading  (launch)
                    mosi = data[nbits-1-i];
                    repeat (half_c) @(negedge clk);
                    sclk = cpol;                           // trailing (capture)
                    if (i == nbits-1) repeat (lag_c)  @(negedge clk);
                    else              repeat (half_c) @(negedge clk);
                end
                if (stop_early != 0 && i == stop_early-1) begin
                    // leave the transaction mid-frame on purpose
                    repeat (lag_c) @(negedge clk);
                    miso_driven = 1'b0;
                    cs_n = 1'b1;
                    done = 1'b1;
                end
            end
            if (!done) begin
                // and released BEFORE the select rises, for the same reason.
                miso_driven = 1'b0;
                cs_n = 1'b1;
            end
        end
    endtask

    task automatic wait_gap(input integer gap_c);
        begin repeat (gap_c) @(negedge clk); end
    endtask

    task automatic restart;
        begin
            @(negedge clk);
            cs_n = 1'b1; sclk = cpol; mosi = 1'b0; miso_driven = 1'b0;
            rst_n = 1'b1;
            repeat (2) @(negedge clk);
            rst_n = 1'b0;
            repeat (4) @(negedge clk);
            rst_n = 1'b1;
            repeat (4) @(negedge clk);
            clr = 1'b1; @(negedge clk); clr = 1'b0; @(negedge clk);
        end
    endtask

    // Reports which rules fired, and checks that exactly `want` did.
    // A Verilog-2001 function must have at least one input, so this takes an unused
    // one. SystemVerilog does not require it; keeping the same signature in both
    // means the bench body is identical across the two languages.
    function automatic [NRULES-1:0] fired(input bit unused);
        integer k;
        begin
            fired = {NRULES{1'b0}};
            for (k = 0; k < NRULES; k = k + 1)
                if (violated(k) != 0) fired[k] = 1'b1;
        end
    endfunction

    task automatic expect_only(input integer which, input [8*24-1:0] label);
        integer k;
        reg [NRULES-1:0] f;
        begin
            f = fired(1'b0);
            if (which >= 0 && !f[which]) begin
                $display("  FAIL: %0s did not fire rule %0d", label, which);
                errors = errors + 1;
            end
            for (k = 0; k < NRULES; k = k + 1)
                if (k != which && f[k]) begin
                    $display("  FAIL: %0s also fired rule %0d, which is a checker that will be switched off the first time it fires for the wrong reason",
                             label, k);
                    errors = errors + 1;
                end
            $display("  %0s -> fired %b", label, f);
        end
    endtask

    integer k;
    reg [NRULES-1:0] f;
    integer unexercised;

    initial begin
        // =============================================================
        // 1. LEGAL TRAFFIC: nothing fires, and EVERY rule is exercised. The second
        //    half is the chapter's point and the half a bench usually omits.
        // =============================================================
        restart();
        miso_driven = 1'b0;
        // 0xA5 has transitions in it, which is what exercises R4 at all.
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        drive_txn(8'h3C, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        // and a transaction that legitimately drives MISO, so R8's precondition is
        // separated from its violation -- the task asserts it after the select and
        // releases it before the deselect, which is what R8 actually requires.
        drive_txn(8'h5A, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b1);
        wait_gap(GAP+2);

        $display("  rule  exercised  violated  what it says");
        $display("  R1     %8d  %8d  SCLK idles at CPOL while deselected",  exercised(R_IDLE),   violated(R_IDLE));
        $display("  R2     %8d  %8d  CS leads the first edge by LEAD",      exercised(R_LEAD),   violated(R_LEAD));
        $display("  R3     %8d  %8d  every half-period is at least HALF",   exercised(R_HALF),   violated(R_HALF));
        $display("  R4     %8d  %8d  MOSI moves only on launch edges",      exercised(R_LAUNCH), violated(R_LAUNCH));
        $display("  R5     %8d  %8d  a whole number of frames",             exercised(R_FRAME),  violated(R_FRAME));
        $display("  R6     %8d  %8d  CS lags the last edge by LAG",         exercised(R_LAG),    violated(R_LAG));
        $display("  R7     %8d  %8d  CS high for at least GAP",             exercised(R_GAP),    violated(R_GAP));
        $display("  R8     %8d  %8d  MISO driven only while selected",      exercised(R_DRIVE),  violated(R_DRIVE));

        f = fired(1'b0);
        if (f != 0) begin
            $display("  FAIL: legal traffic fired %b", f);
            errors = errors + 1;
        end

        unexercised = 0;
        for (k = 0; k < NRULES; k = k + 1)
            if (exercised(k) == 0) begin
                $display("  FAIL: rule %0d was never EXERCISED, so its zero in the violated column means nothing at all -- a plan row closed on that zero is a row nobody tested",
                         k);
                unexercised = unexercised + 1;
                errors = errors + 1;
            end
        if (unexercised == 0)
            $display("  legal traffic: no rule violated, and all %0d rules EXERCISED -- which is what makes the eight zeros above mean something",
                     NRULES);

        // =============================================================
        // 2. BREAK EACH RULE IN TURN. One violation per run, and no other rule may
        //    fire -- which is the property that keeps a checker usable.
        // =============================================================

        // R1: SCLK parked away from idle while deselected.
        restart();
        @(negedge clk);
        sclk = ~cpol;                       // wrong idle level
        repeat (10) @(negedge clk);
        sclk = cpol;
        expect_only(R_IDLE, "R1 wrong idle level");

        // R2: a lead shorter than LEAD.
        restart();
        drive_txn(8'hA5, 4, 1, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_LEAD, "R2 short lead");

        // R3: a half-period shorter than HALF.
        restart();
        drive_txn(8'hA5, 4, LEAD+2, 1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_HALF, "R3 short half-period");

        // R5: a transaction cut mid-frame. len is 4, so stopping after 3 bits
        //     leaves the remainder non-zero.
        restart();
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 3, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_FRAME, "R5 partial frame");

        // R6: CS deasserting immediately after the last edge.
        restart();
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, 0, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_LAG, "R6 short lag");

        // R7: a gap shorter than GAP between two transactions.
        restart();
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(1);
        drive_txn(8'h3C, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_GAP, "R7 short gap");

        // R8: MISO driven while deselected.
        restart();
        @(negedge clk);
        miso_driven = 1'b1;
        repeat (8) @(negedge clk);
        miso_driven = 1'b0;
        expect_only(R_DRIVE, "R8 driven while idle");

        // R4: MOSI moving at a capture edge. Produced by driving CPHA=1 timing
        //     against a monitor configured for CPHA=0 -- which is exactly the
        //     phase mismatch of Chapter 14.6, seen from the bus.
        restart();
        // The MONITOR stays configured for CPHA=0 and the MASTER drives CPHA=1
        // timing. That disagreement is the point, and it is why the phase is an
        // argument rather than a shared signal.
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b1, 1'b0);
        wait_gap(GAP+2);
        f = fired(1'b0);
        if (!f[R_LAUNCH]) begin
            $display("  FAIL: a master launching on the monitor's capture edge must fire R4 -- that is the phase mismatch of Chapter 14.6 seen from the pins");
            errors = errors + 1;
        end
        $display("  R4 phase-mismatched master -> fired %b", f);

        // =============================================================
        // 3. THE ROW WITH NO CHECKER, stated rather than omitted.
        // =============================================================
        $display("  and one plan row has NO checker and never will: bit ORDER. MSB-first and LSB-first produce a well-formed word of the right length at the right time (Chapter 14.3), so no observation of these pins distinguishes them. It stays in the plan marked 'configuration, not checkable', because a plan that silently omits it looks complete and one that lists it honestly does not");

        if (errors == 0)
            $display("PASS: a verification plan is only real when every row maps to a CHECKER and an EXERCISED counter, and the second is the harder half -- a checker alone cannot tell 'never broken' from 'never reached', and the two produce identical reports from suites that tested completely different amounts. Legal traffic violated none of the eight rules AND exercised all eight, which is what makes those zeros mean something. Each rule was then broken in turn and in every case exactly one rule fired: the wrong idle level fired only R1, a short lead only R2, a short half-period only R3, a partial frame only R5, a short lag only R6, a short gap only R7, and MISO driven while deselected only R8 -- a checker that also fires for a neighbouring fault is a checker an integrator switches off the first time it is wrong. R4 was broken by driving a phase-mismatched master, which is Chapter 14.6's fault seen from the pins rather than from inside the slave. And every rule here is observable FROM THE PINS ALONE, which is the plan's most important property, because a rule that needs to look inside the DUT is a rule the DUT gets a vote on -- with one honest exception, bit order, which has no checker and never will and stays in the plan saying so");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_rule_monitor_tb.v — the same bench in Verilog-2001
// spi_rule_monitor_tb.v
//
// The experiment that makes a verification plan trustworthy: drive LEGAL traffic and
// then break each rule ONE AT A TIME, and check three things each time.
//
//   1  the rule that was broken fires
//   2  no OTHER rule fires
//   3  the rule was EXERCISED -- so a zero in the violated column means "not broken"
//      rather than "never reached"
//
// The second is the one benches usually skip, and it is what separates a checker from
// a smoke alarm. A monitor whose lead check also fires on a short gap is a monitor
// that will be disabled the first time an integrator sees it fire for the wrong
// reason.
//
// The third is what the chapter is about. `violated(i) == 0` is meaningless on its
// own; `violated(i) == 0 && exercised(i) > 0` is a closed plan row.

`timescale 1ns/1ps

module spi_rule_monitor_tb;

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

    localparam R_IDLE = 0, R_LEAD = 1, R_HALF = 2, R_LAUNCH = 3,
                   R_FRAME = 4, R_LAG = 5, R_GAP = 6, R_DRIVE = 7;

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

    reg sclk;
    reg cs_n;
    reg mosi;
    reg miso_driven;
    reg cpol;
    reg cpha;
    reg [LEN_W-1:0] len;
    reg clr;

    wire [NRULES*CNT_W-1:0] exercised_flat;
    wire [NRULES*CNT_W-1:0] violated_flat;

    // Named accessors, so the rest of the bench reads as if the ports were arrays.
    // The flattening is the component's portability concession (see its header) and
    // it should not leak into everything that reads it.
        function [CNT_W-1:0] exercised;
        input integer i;
        exercised = exercised_flat[i*CNT_W +: CNT_W];
    endfunction
        function [CNT_W-1:0] violated;
        input integer i;
        violated = violated_flat[i*CNT_W +: CNT_W];
    endfunction

    spi_rule_monitor #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
                       .LEN_W(LEN_W), .CNT_W(CNT_W), .NRULES(NRULES)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso_driven(miso_driven),
        .cpol(cpol), .cpha(cpha), .len(len),
        .exercised_flat(exercised_flat), .violated_flat(violated_flat), .clr(clr)
    );

    integer errors;

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

    // --- a legal master, parameterised so each rule can be broken in turn ---
    // Every timing number is an argument, so a violation is produced by passing a
    // different number rather than by writing a different driver. That matters: two
    // drivers drift, and then a "violation" is a difference between two pieces of
    // bench code.
    // EVERY wait in the stimulus is on the NEGEDGE, and the monitor samples on the
    // posedge. The first version of this bench waited on the posedge and assigned
    // immediately afterwards, so the assignment and the monitor's sample happened at
    // the same simulation time -- and the monitor read the old value. Every rule
    // whose precondition is inside a transaction reported ZERO exercises while the
    // waveform looked perfect.
    //
    // Driving away from the sampling edge is the discipline that fixes it, and
    // Chapter 16.3 is about the language feature that encodes it declaratively
    // instead of leaving it to every task in the file to remember.
    // `drv_cpha` is the MASTER's phase and is separate from the monitor's `cpha`
    // input, which is the whole point of the R4 test: a phase mismatch is a
    // disagreement between two devices, so a bench that changes one signal changes
    // both and measures nothing. The first version did exactly that and R4 never
    // fired.
        task drive_txn;
        input [7:0] data;
        input integer nbits;
        input integer lead_c;
        input integer half_c;
        input integer lag_c;
        input integer stop_early;
        input drv_cpha;
        input drv_miso;
        integer i;
        // A `done` flag rather than `return`: Verilog-2001 has no `return` in a task
        // and no `break` in a loop, so the same source has to work without either --
        // which it does, at the cost of one flag and a guarded loop condition.
        reg     done;
        begin
            done = 1'b0;
            @(negedge clk);
            sclk = cpol;
            cs_n = 1'b0;
            // MISO is driven only AFTER the select is low, which is the obligation R8
            // states. Setting it before the select -- as the first version of this
            // bench did, outside the task -- puts a driven MISO on a deselected bus
            // for one monitor cycle and violates R8 on legal traffic.
            if (drv_miso) miso_driven = 1'b1;
            // The first bit is placed HERE, during the lead, and not alongside the
            // first edge -- placing it on the same cycle as the leading edge makes a
            // legal CPHA=0 master look like it moved MOSI at a capture instant, and
            // R4 fired once on every legal run.
            if (!drv_cpha) begin
                @(negedge clk);
                mosi = data[nbits-1];
            end
            repeat (lead_c) @(negedge clk);
            for (i = 0; i < nbits && !done; i = i + 1) begin
                if (!drv_cpha) begin
                    // CPHA=0: MOSI is launched on the TRAILING edge. The first bit
                    // was already placed during the lead, above.
                    sclk = ~cpol;                          // leading  (capture)
                    repeat (half_c) @(negedge clk);
                    sclk = cpol;                           // trailing (launch)
                    if (i < nbits-1) mosi = data[nbits-2-i];
                    // On the LAST bit the trailing wait IS the lag -- there is no
                    // following edge, so R3 does not measure this interval and
                    // shortening it is how a short lag is produced at all. The first
                    // version waited a full half-period here and then `lag_c` more,
                    // so `lag_c = 0` still gave a lag of half a period and R6 could
                    // not be made to fire.
                    if (i == nbits-1) repeat (lag_c)  @(negedge clk);
                    else              repeat (half_c) @(negedge clk);
                end else begin
                    sclk = ~cpol;                          // leading  (launch)
                    mosi = data[nbits-1-i];
                    repeat (half_c) @(negedge clk);
                    sclk = cpol;                           // trailing (capture)
                    if (i == nbits-1) repeat (lag_c)  @(negedge clk);
                    else              repeat (half_c) @(negedge clk);
                end
                if (stop_early != 0 && i == stop_early-1) begin
                    // leave the transaction mid-frame on purpose
                    repeat (lag_c) @(negedge clk);
                    miso_driven = 1'b0;
                    cs_n = 1'b1;
                    done = 1'b1;
                end
            end
            if (!done) begin
                // and released BEFORE the select rises, for the same reason.
                miso_driven = 1'b0;
                cs_n = 1'b1;
            end
        end
    endtask

        task wait_gap;
        input integer gap_c;
        begin repeat (gap_c) @(negedge clk); end
    endtask

    task restart;
        begin
            @(negedge clk);
            cs_n = 1'b1; sclk = cpol; mosi = 1'b0; miso_driven = 1'b0;
            rst_n = 1'b1;
            repeat (2) @(negedge clk);
            rst_n = 1'b0;
            repeat (4) @(negedge clk);
            rst_n = 1'b1;
            repeat (4) @(negedge clk);
            clr = 1'b1; @(negedge clk); clr = 1'b0; @(negedge clk);
        end
    endtask

    // Reports which rules fired, and checks that exactly `want` did.
    // A Verilog-2001 function must have at least one input, so this takes an unused
    // one. SystemVerilog does not require it; keeping the same signature in both
    // means the bench body is identical across the two languages.
        function [NRULES-1:0] fired;
        input unused;
        integer k;
        begin
            fired = {NRULES{1'b0}};
            for (k = 0; k < NRULES; k = k + 1)
                if (violated(k) != 0) fired[k] = 1'b1;
        end
    endfunction

        task expect_only;
        input integer which;
        input [8*24-1:0] label;
        integer k;
        reg [NRULES-1:0] f;
        begin
            f = fired(1'b0);
            if (which >= 0 && !f[which]) begin
                $display("  FAIL: %0s did not fire rule %0d", label, which);
                errors = errors + 1;
            end
            for (k = 0; k < NRULES; k = k + 1)
                if (k != which && f[k]) begin
                    $display("  FAIL: %0s also fired rule %0d, which is a checker that will be switched off the first time it fires for the wrong reason",
                             label, k);
                    errors = errors + 1;
                end
            $display("  %0s -> fired %b", label, f);
        end
    endtask

    integer k;
    reg [NRULES-1:0] f;
    integer unexercised;

    initial begin
        // =============================================================
        // 1. LEGAL TRAFFIC: nothing fires, and EVERY rule is exercised. The second
        //    half is the chapter's point and the half a bench usually omits.
        // =============================================================
        restart();
        miso_driven = 1'b0;
        // 0xA5 has transitions in it, which is what exercises R4 at all.
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        drive_txn(8'h3C, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        // and a transaction that legitimately drives MISO, so R8's precondition is
        // separated from its violation -- the task asserts it after the select and
        // releases it before the deselect, which is what R8 actually requires.
        drive_txn(8'h5A, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b1);
        wait_gap(GAP+2);

        $display("  rule  exercised  violated  what it says");
        $display("  R1     %8d  %8d  SCLK idles at CPOL while deselected",  exercised(R_IDLE),   violated(R_IDLE));
        $display("  R2     %8d  %8d  CS leads the first edge by LEAD",      exercised(R_LEAD),   violated(R_LEAD));
        $display("  R3     %8d  %8d  every half-period is at least HALF",   exercised(R_HALF),   violated(R_HALF));
        $display("  R4     %8d  %8d  MOSI moves only on launch edges",      exercised(R_LAUNCH), violated(R_LAUNCH));
        $display("  R5     %8d  %8d  a whole number of frames",             exercised(R_FRAME),  violated(R_FRAME));
        $display("  R6     %8d  %8d  CS lags the last edge by LAG",         exercised(R_LAG),    violated(R_LAG));
        $display("  R7     %8d  %8d  CS high for at least GAP",             exercised(R_GAP),    violated(R_GAP));
        $display("  R8     %8d  %8d  MISO driven only while selected",      exercised(R_DRIVE),  violated(R_DRIVE));

        f = fired(1'b0);
        if (f != 0) begin
            $display("  FAIL: legal traffic fired %b", f);
            errors = errors + 1;
        end

        unexercised = 0;
        for (k = 0; k < NRULES; k = k + 1)
            if (exercised(k) == 0) begin
                $display("  FAIL: rule %0d was never EXERCISED, so its zero in the violated column means nothing at all -- a plan row closed on that zero is a row nobody tested",
                         k);
                unexercised = unexercised + 1;
                errors = errors + 1;
            end
        if (unexercised == 0)
            $display("  legal traffic: no rule violated, and all %0d rules EXERCISED -- which is what makes the eight zeros above mean something",
                     NRULES);

        // =============================================================
        // 2. BREAK EACH RULE IN TURN. One violation per run, and no other rule may
        //    fire -- which is the property that keeps a checker usable.
        // =============================================================

        // R1: SCLK parked away from idle while deselected.
        restart();
        @(negedge clk);
        sclk = ~cpol;                       // wrong idle level
        repeat (10) @(negedge clk);
        sclk = cpol;
        expect_only(R_IDLE, "R1 wrong idle level");

        // R2: a lead shorter than LEAD.
        restart();
        drive_txn(8'hA5, 4, 1, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_LEAD, "R2 short lead");

        // R3: a half-period shorter than HALF.
        restart();
        drive_txn(8'hA5, 4, LEAD+2, 1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_HALF, "R3 short half-period");

        // R5: a transaction cut mid-frame. len is 4, so stopping after 3 bits
        //     leaves the remainder non-zero.
        restart();
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 3, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_FRAME, "R5 partial frame");

        // R6: CS deasserting immediately after the last edge.
        restart();
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, 0, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_LAG, "R6 short lag");

        // R7: a gap shorter than GAP between two transactions.
        restart();
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(1);
        drive_txn(8'h3C, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
        wait_gap(GAP+2);
        expect_only(R_GAP, "R7 short gap");

        // R8: MISO driven while deselected.
        restart();
        @(negedge clk);
        miso_driven = 1'b1;
        repeat (8) @(negedge clk);
        miso_driven = 1'b0;
        expect_only(R_DRIVE, "R8 driven while idle");

        // R4: MOSI moving at a capture edge. Produced by driving CPHA=1 timing
        //     against a monitor configured for CPHA=0 -- which is exactly the
        //     phase mismatch of Chapter 14.6, seen from the bus.
        restart();
        // The MONITOR stays configured for CPHA=0 and the MASTER drives CPHA=1
        // timing. That disagreement is the point, and it is why the phase is an
        // argument rather than a shared signal.
        drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b1, 1'b0);
        wait_gap(GAP+2);
        f = fired(1'b0);
        if (!f[R_LAUNCH]) begin
            $display("  FAIL: a master launching on the monitor's capture edge must fire R4 -- that is the phase mismatch of Chapter 14.6 seen from the pins");
            errors = errors + 1;
        end
        $display("  R4 phase-mismatched master -> fired %b", f);

        // =============================================================
        // 3. THE ROW WITH NO CHECKER, stated rather than omitted.
        // =============================================================
        $display("  and one plan row has NO checker and never will: bit ORDER. MSB-first and LSB-first produce a well-formed word of the right length at the right time (Chapter 14.3), so no observation of these pins distinguishes them. It stays in the plan marked 'configuration, not checkable', because a plan that silently omits it looks complete and one that lists it honestly does not");

        if (errors == 0)
            $display("PASS: a verification plan is only real when every row maps to a CHECKER and an EXERCISED counter, and the second is the harder half -- a checker alone cannot tell 'never broken' from 'never reached', and the two produce identical reports from suites that tested completely different amounts. Legal traffic violated none of the eight rules AND exercised all eight, which is what makes those zeros mean something. Each rule was then broken in turn and in every case exactly one rule fired: the wrong idle level fired only R1, a short lead only R2, a short half-period only R3, a partial frame only R5, a short lag only R6, a short gap only R7, and MISO driven while deselected only R8 -- a checker that also fires for a neighbouring fault is a checker an integrator switches off the first time it is wrong. R4 was broken by driving a phase-mismatched master, which is Chapter 14.6's fault seen from the pins rather than from inside the slave. And every rule here is observable FROM THE PINS ALONE, which is the plan's most important property, because a rule that needs to look inside the DUT is a rule the DUT gets a vote on -- with one honest exception, bit order, which has no checker and never will and stays in the plan saying so");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b1;
        sclk = 1'b0;
        cs_n = 1'b1;
        mosi = 1'b0;
        miso_driven = 1'b0;
        cpol = 1'b0;
        cpha = 1'b0;
        len = 6'd4;
        clr = 1'b0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_rule_monitor_tb.vhd — the same bench in VHDL
-- spi_rule_monitor_tb.vhd
--
-- The experiment that makes a verification plan trustworthy: drive LEGAL traffic and
-- then break each rule ONE AT A TIME, and check three things each time.
--
--   1  the rule that was broken fires
--   2  no OTHER rule fires
--   3  the rule was EXERCISED -- so a zero in the violated column means "not broken"
--      rather than "never reached"
--
-- The second is the one benches usually skip, and it is what separates a checker from
-- a smoke alarm. A monitor whose lead check also fires on a short gap is a monitor
-- that will be disabled the first time an integrator sees it fire for the wrong
-- reason.
--
-- The third is what the chapter is about. `violated(i) == 0` is meaningless on its
-- own; `violated(i) == 0 && exercised(i) > 0` is a closed plan row.

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

entity spi_rule_monitor_tb is
end entity;

architecture sim of spi_rule_monitor_tb is

    constant LEAD  : natural  := 4;
    constant HALF  : natural  := 3;
    constant LAG   : natural  := 2;
    constant GAP   : natural  := 3;
    constant LEN_W : positive := 6;

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

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

    signal exercised, violated : rule_counts_t;

begin

    clkgen : process
    begin
        while not halt loop
            clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns;
        end loop;
        wait;
    end process;

    dut : entity work.spi_rule_monitor
        generic map (LEAD => LEAD, HALF => HALF, LAG => LAG, GAP => GAP,
                     LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  sclk => sclk, cs_n => cs_n, mosi => mosi,
                  miso_driven => miso_driven,
                  cpol => cpol, cpha => cpha, len => len,
                  exercised => exercised, violated => violated, clr => clr);

    watchdog : process
    begin
        wait for 500 us;
        if not halt then
            report "FAIL: the simulation did not finish within its time limit"
                severity failure;
        end if;
        wait;
    end process;

    stim : process
        variable errs : natural := 0;
        variable f    : std_logic_vector(NRULES - 1 downto 0);
        variable unexercised : natural;

        -- Every wait is on the FALLING edge and the monitor samples on the rising
        -- one. Driving on the sampling edge makes the monitor read the old value, and
        -- every rule whose precondition is inside a transaction reports zero
        -- exercises while the waveform looks perfect.
        procedure drive_txn(data       : std_logic_vector(7 downto 0);
                            nbits      : natural;
                            lead_c     : natural;
                            half_c     : natural;
                            lag_c      : natural;
                            stop_early : natural;
                            drv_cpha   : std_logic;
                            drv_miso   : std_logic) is
            variable done : boolean := false;
        begin
            done := false;
            wait until falling_edge(clk);
            sclk <= cpol;
            cs_n <= '0';
            -- MISO is driven only AFTER the select is low, which is what R8 requires.
            if drv_miso = '1' then miso_driven <= '1'; end if;
            -- The first bit is placed during the LEAD, not alongside the first edge:
            -- placing it on the same cycle as the leading edge makes a legal CPHA=0
            -- master look like it moved MOSI at a capture instant.
            if drv_cpha = '0' then
                wait until falling_edge(clk);
                mosi <= data(nbits - 1);
            end if;
            for i in 1 to lead_c loop wait until falling_edge(clk); end loop;

            for i in 0 to nbits - 1 loop
                if not done then
                    if drv_cpha = '0' then
                        sclk <= not cpol;                       -- leading (capture)
                        for j in 1 to half_c loop wait until falling_edge(clk); end loop;
                        sclk <= cpol;                           -- trailing (launch)
                        if i < nbits - 1 then mosi <= data(nbits - 2 - i); end if;
                        -- On the LAST bit the trailing wait IS the lag: there is no
                        -- following edge, so R3 does not measure this interval and
                        -- shortening it is how a short lag is produced at all.
                        if i = nbits - 1 then
                            for j in 1 to lag_c loop wait until falling_edge(clk); end loop;
                        else
                            for j in 1 to half_c loop wait until falling_edge(clk); end loop;
                        end if;
                    else
                        sclk <= not cpol;                       -- leading (launch)
                        mosi <= data(nbits - 1 - i);
                        for j in 1 to half_c loop wait until falling_edge(clk); end loop;
                        sclk <= cpol;                           -- trailing (capture)
                        if i = nbits - 1 then
                            for j in 1 to lag_c loop wait until falling_edge(clk); end loop;
                        else
                            for j in 1 to half_c loop wait until falling_edge(clk); end loop;
                        end if;
                    end if;

                    if stop_early /= 0 and i = stop_early - 1 then
                        for j in 1 to lag_c loop wait until falling_edge(clk); end loop;
                        miso_driven <= '0';
                        cs_n <= '1';
                        done := true;
                    end if;
                end if;
            end loop;

            if not done then
                miso_driven <= '0';
                cs_n <= '1';
            end if;
        end procedure;

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

        procedure restart is
        begin
            wait until falling_edge(clk);
            cs_n <= '1'; sclk <= cpol; mosi <= '0'; miso_driven <= '0';
            rst_n <= '1';
            for i in 1 to 2 loop wait until falling_edge(clk); end loop;
            rst_n <= '0';
            for i in 1 to 4 loop wait until falling_edge(clk); end loop;
            rst_n <= '1';
            for i in 1 to 4 loop wait until falling_edge(clk); end loop;
            clr <= '1'; wait until falling_edge(clk);
            clr <= '0'; wait until falling_edge(clk);
        end procedure;

        impure function fired return std_logic_vector is
            variable r : std_logic_vector(NRULES - 1 downto 0) := (others => '0');
        begin
            for k in 0 to NRULES - 1 loop
                if violated(k) /= 0 then r(k) := '1'; end if;
            end loop;
            return r;
        end function;

        -- `label` is a VHDL RESERVED WORD, so the parameter is `tag`. The error names the
        -- token rather than the reservation, which is confusing the first time.
        procedure expect_only(which : integer; tag : string) is
            variable v : std_logic_vector(NRULES - 1 downto 0);
        begin
            v := fired;
            if which >= 0 and v(which) = '0' then
                report "  FAIL: " & tag & " did not fire rule " &
                       integer'image(which);
                errs := errs + 1;
            end if;
            for k in 0 to NRULES - 1 loop
                if k /= which and v(k) = '1' then
                    report "  FAIL: " & tag & " also fired rule " &
                           integer'image(k) &
                           ", which is a checker that will be switched off the first time it fires for the wrong reason";
                    errs := errs + 1;
                end if;
            end loop;
            report "  " & tag & " -> fired " & to_string(v);
        end procedure;
    begin
        -- 1. LEGAL TRAFFIC: nothing fires, and EVERY rule is exercised.
        restart;
        -- 0xA5 has transitions in it, which is what exercises R4 at all.
        drive_txn(x"A5", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '0');
        wait_gap(GAP + 2);
        drive_txn(x"3C", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '0');
        wait_gap(GAP + 2);
        -- and a transaction that legitimately drives MISO, so R8's precondition is
        -- separated from its violation
        drive_txn(x"5A", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '1');
        wait_gap(GAP + 2);

        report "  rule  exercised  violated  what it says";
        for k in 0 to NRULES - 1 loop
            report "  R" & integer'image(k + 1) & "     " &
                   integer'image(exercised(k)) & "  " &
                   integer'image(violated(k)) & "  " & rule_name(k);
        end loop;

        f := fired;
        if f /= (f'range => '0') then
            report "  FAIL: legal traffic fired " & to_string(f);
            errs := errs + 1;
        end if;

        unexercised := 0;
        for k in 0 to NRULES - 1 loop
            if exercised(k) = 0 then
                report "  FAIL: rule " & integer'image(k) &
                       " was never EXERCISED, so its zero in the violated column means nothing at all -- a plan row closed on that zero is a row nobody tested";
                unexercised := unexercised + 1;
                errs := errs + 1;
            end if;
        end loop;
        if unexercised = 0 then
            report "  legal traffic: no rule violated, and all " &
                   integer'image(NRULES) &
                   " rules EXERCISED -- which is what makes those zeros mean something";
        end if;

        -- 2. BREAK EACH RULE IN TURN.
        restart;
        wait until falling_edge(clk);
        sclk <= not cpol;                       -- wrong idle level
        for i in 1 to 10 loop wait until falling_edge(clk); end loop;
        sclk <= cpol;
        expect_only(R_IDLE, "R1 wrong idle level");

        restart;
        drive_txn(x"A5", 4, 1, HALF + 1, LAG + 1, 0, '0', '0');
        wait_gap(GAP + 2);
        expect_only(R_LEAD, "R2 short lead");

        restart;
        drive_txn(x"A5", 4, LEAD + 2, 1, LAG + 1, 0, '0', '0');
        wait_gap(GAP + 2);
        expect_only(R_HALF, "R3 short half-period");

        restart;
        drive_txn(x"A5", 4, LEAD + 2, HALF + 1, LAG + 1, 3, '0', '0');
        wait_gap(GAP + 2);
        expect_only(R_FRAME, "R5 partial frame");

        restart;
        drive_txn(x"A5", 4, LEAD + 2, HALF + 1, 0, 0, '0', '0');
        wait_gap(GAP + 2);
        expect_only(R_LAG, "R6 short lag");

        restart;
        drive_txn(x"A5", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '0');
        wait_gap(1);
        drive_txn(x"3C", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '0');
        wait_gap(GAP + 2);
        expect_only(R_GAP, "R7 short gap");

        restart;
        wait until falling_edge(clk);
        miso_driven <= '1';
        for i in 1 to 8 loop wait until falling_edge(clk); end loop;
        miso_driven <= '0';
        expect_only(R_DRIVE, "R8 driven while idle");

        -- R4: the MONITOR stays at CPHA=0 and the MASTER drives CPHA=1 timing. That
        -- disagreement is the point, and it is why the phase is an argument rather
        -- than a shared signal.
        restart;
        drive_txn(x"A5", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '1', '0');
        wait_gap(GAP + 2);
        f := fired;
        if f(R_LAUNCH) = '0' then
            report "  FAIL: a master launching on the monitor's capture edge must fire R4 -- that is the phase mismatch of Chapter 14.6 seen from the pins";
            errs := errs + 1;
        end if;
        report "  R4 phase-mismatched master -> fired " & to_string(f);

        report "  and one plan row has NO checker and never will: bit ORDER. MSB-first and LSB-first produce a well-formed word of the right length at the right time (Chapter 14.3), so no observation of these pins distinguishes them. It stays in the plan marked 'configuration, not checkable', because a plan that silently omits it looks complete and one that lists it honestly does not";

        if errs = 0 then
            report "PASS: a verification plan is only real when every row maps to a CHECKER and an EXERCISED counter, and the second is the harder half -- a checker alone cannot tell 'never broken' from 'never reached', and the two produce identical reports from suites that tested completely different amounts. Legal traffic violated none of the eight rules AND exercised all eight, which is what makes those zeros mean something. Each rule was then broken in turn and in every case exactly one rule fired: the wrong idle level fired only R1, a short lead only R2, a short half-period only R3, a partial frame only R5, a short lag only R6, a short gap only R7, and MISO driven while deselected only R8 -- a checker that also fires for a neighbouring fault is a checker an integrator switches off the first time it is wrong. R4 was broken by driving a phase-mismatched master, which is Chapter 14.6's fault seen from the pins rather than from inside the slave. And every rule here is observable FROM THE PINS ALONE, which is the plan's most important property, because a rule that needs to look inside the DUT is a rule the DUT gets a vote on -- with one honest exception, bit order, which has no checker and never will and stays in the plan saying so";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;

        halt <= true;
        wait;
    end process;

end architecture;

7. The Row With No Checker

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   and one plan row has NO checker and never will: bit ORDER.

The bench prints that line deliberately, because it is the most useful sentence in its log.

MSB-first and LSB-first differ in which bit of the word goes out first. Both produce N edges in N pairs, a whole number of frames, a legal lead, a legal lag, legal half-periods, and MOSI moving only at launch edges. Every one of the eight rules passes on both. There is no observation of these three pins that separates them, because the difference is not in the pins — it is in the meaning assigned to them, which is a decision made at both ends and visible at neither.

That is not a gap to be closed by a cleverer monitor. It is a rule that belongs to a different layer of the environment, and Chapter 16.6's reference model is where it finally gets checked: the model predicts the word the transaction asked to send, the monitor rebuilds the word using the bit order the specification names, and a mismatch between them is what a reversed frame looks like. Bit order becomes checkable the moment you stop asking the pins and start comparing two independent statements about them.

8. Why a Verification Engineer Cares

Because the two-column plan is what makes a regression report readable, and the second column is the one that catches the failure nobody else catches.

A suite with 4,000 passing assertions and no exercise data is a suite in which any number of those assertions may be unreachable, and the number is unknowable from the report. Every one of the eight checkers in this chapter was, at some point during its development, silently unreachable — three of them for the reasons in the callout above, and each time the symptom was a green report.

The discipline generalises past SPI and past protocol checking: every check needs a second check that the first one ran. In a constrained-random environment that second check is coverage. In a directed test it is a counter. In a formal flow it is the cover trace that witnesses the antecedent. The names differ; the failure they prevent is identical.

9. Why an FPGA or ASIC Engineer Cares

Because this table is the contract you will be handed when something does not work, and its shape decides how long the argument takes.

A bug report that says "SPI is broken" starts a week of work. A report that says "R6 violated 14 times, R1–R5 and R7–R8 clean, all eight exercised" says: the master deasserts too soon after its last edge, everything else about it is correct, and the checker that says so has been shown to fire for this and only this. The second report is actionable in an afternoon, and the difference between them is the diagonal.

10. Failure Signature — A Green Regression With a Dead Checker

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom          a regression that has been green for eleven months. A new
                    slave is integrated and fails immediately in the lab, on a
                    rule the suite has a checker for.

   What happened    six months earlier, a parameter default changed and the
                    checker's precondition stopped being reachable. The
                    assertion never fired again. Nothing in the report changed,
                    because a check that never runs and a check that always
                    passes print the same line.

   What would have  the exercised counter for that rule dropping from a few
   caught it        thousand to zero, in the same commit that changed the
                    default. A report that shows exercise counts makes this
                    visible as a diff; a report that shows pass/fail cannot
                    represent it at all.

   The tell         the failing rule is one of the ones with a narrow
                    precondition -- a gap, a lead, a coincidence. Continuous
                    obligations like R1 and R8 are exercised by any traffic at
                    all and almost never go dead. Rules with narrow
                    preconditions go dead quietly and stay dead.

11. Common Misconceptions

"A verification plan is a list of features." A list of features is the input to a plan. The plan is the mapping from each feature to the specific check that decides it and the specific evidence that the check ran. A row with no checker named is a row that has not been planned; a row with a checker and no exercise evidence is a row that has been planned and not verified.

"If the assertion passes, the rule holds." Only if the assertion was reached. An SVA property whose antecedent is never true passes vacuously, forever, with no warning — which is the same failure as an exercised counter stuck at zero, and is why cover property on antecedents is not optional.

"Zero violations across a big regression is strong evidence." It is evidence in proportion to the exercise counts, and to nothing else. Zero violations with an exercise count of zero is not weak evidence; it is no evidence.

"Every requirement in the datasheet can be checked from the pins." Bit order cannot, and it is in every SPI datasheet. The plan's job includes saying which rows are not pin-decidable and naming the layer that will decide them instead — a reference model, a register read-back, an end-to-end data check. A plan that quietly implies a pin checker for a rule that has none is worse than a plan with a gap, because the gap is at least visible.

"The checker fires on the bug, so it works." That is half the test. A checker that fires on its own bug and also on three unrelated ones produces reports that require guessing. The diagonal is the property to demand, and demanding it found three monitor bugs in this chapter alone.

12. Reason It Through

A rule reports violated = 0 and exercised = 0 after a 10-hour regression. Name three distinct causes, and the observation that separates them.

The precondition is unreachable in this configuration; the precondition is written with the wrong polarity so it is never true; or the traffic genuinely never produces the situation. The separator is a second configuration: run a directed test that forces the situation. If the counter still reads zero the checker is broken; if it counts, the original suite's stimulus is the gap.

Why does R4 count MOSI changes rather than capture edges?

Because the rule is about MOSI moving at a forbidden moment. Counting capture edges would make the exercise count large and constant — every frame has them — and would hide the fact that a suite sending constant data has not tested the rule at all. The exercise counter should count the precondition of the rule, and R4's precondition is a change on MOSI.

Three transactions produce R7 = 2. What would R7 = 3 have meant?

That the checker counted the first assert of the run as following a gap. There is no preceding transaction to have left a gap, so the measurement is undefined and including it inflates every run's exercise count by exactly one — small enough never to be noticed and large enough to make a run with a single transaction report a gap check that never happened.

Where in this environment does bit order finally become checkable, and why not sooner?

In Chapter 16.6, when a reference model states independently what word should have been sent and a monitor states what word was observed. Not sooner, because a single observer of the pins has only one statement, and one statement cannot disagree with itself. Bit order is decided by comparison, and comparison needs two independent sources.

13. Understanding Check

14. Summary

A verification plan becomes real when every row names a checker and the evidence that the checker ran. Eight pin-observable SPI rules were extracted, implemented in one monitor in three languages, and measured: legal traffic violated none of them and exercised all eight, and eight injected faults produced a perfectly diagonal violation matrix. Three off-diagonal entries along the way were monitor bugs rather than stimulus bugs, and each of them had a green report as its symptom. One plan row — bit order — was shown to have no pin-level checker and never to be able to have one, which is why the honest plan says so in the row instead of implying a check that will not be written.

15. What Comes Next

The rules exist; something has to produce traffic that reaches them. Chapter 16.2 builds the transaction object and measures the difference between stimulus that looks random and stimulus that covers the space.

Continue learning