Skip to content
VLSI Mentor

SPI · Module 16

Transaction Modelling and Stimulus

Two generators, the same 400 transactions, and a 9-versus-16 coverage result, because the stimulus space is not the data space. The master's mode is derived from the slave's, illegal traffic is a request rather than an accident, and a zero seed is refused.

Chapter 16.1 produced eight rules and eight exercise counters. Six of the eight have preconditions that ordinary traffic reaches constantly. The other two are reached only by traffic that was asked to reach them, and nothing in a payload generator ever asks.

This chapter builds the transaction object and then does the only thing that decides whether a transaction object is any good: it builds a second one that looks reasonable, runs both at the same transaction count, and counts bins.

Two generators, 400 transactions each. One reaches 9 coverage bins and one reaches 16. The difference is not the count.

1. The Transaction Is Not The Payload

The obvious transaction object for SPI has three fields: data, width, mode. It is the object almost everybody writes first, and it cannot generate a single one of Module 14's interesting cases.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   THE PAYLOAD OBJECT                 WHAT IT CANNOT ASK FOR
   ---------------------------------  ----------------------------------------
   data                               a lead exactly at LEAD_MIN
   nbits                              a half-period one cycle short
   cpol, cpha                         a gap below GAP_MIN
                                      a frame cut off mid-bit
                                      a master whose mode disagrees with the
                                        slave's

Every item in the right-hand column is a requirement Module 14 spent a chapter pinning down, and every one of them is a timing property of the transaction rather than a property of its contents. A generator that does not carry a field cannot vary it, cannot randomise it, and cannot hit its boundary — at any transaction count, with any seed, forever.

That last clause is the measurement this chapter exists to make. It is easy to believe a weak generator just needs more cycles.

2. The Fields

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   PAYLOAD        data          the word to send
                  nbits         how many bits of it
                  lsb_first     which end goes first

   SLAVE MODE     cpol, cpha    the mode the slave is configured for

   MASTER MODE    m_cpol        the mode the MASTER actually uses -- separate,
                  m_cpha          because a mismatch is the whole of 14.6

   TIMING         lead          cycles from CS falling to the first edge
                  half          cycles per SCLK half period
                  lag           cycles from the last edge to CS rising
                  gap           cycles of CS high before the next transaction

   INTENT         kind          legal / illegal timing / mismatched mode

Twelve fields where the naive object had four, and the eight extra ones are the entire difference between the two coverage numbers.

3. The Generator, as a Diagram

A seeded xorshift feeding a transaction builder whose kind input selects legal, illegal-timing or mismatched-mode constraints, with the master mode derived from the slave mode, feeding a driver and a coverage modelseedxorshiftkindpayload fieldsslave modetiming fieldsmaster modetransactiondrivercoverage model12
Figure 1 — one seeded generator feeding two consumers. The `kind` input selects which constraint set applies, so illegal traffic is a request rather than a side effect of wide ranges; the master's mode is derived from the slave's rather than drawn independently; and the coverage model is fed from the same transaction the driver will receive, so a bin can never be credited to a transaction that was not issued.

4. The Coverage Model

Twenty-two bins across seven axes, and every axis traces to a chapter that established why it matters.

AxisBinsFrom
mode agreementagree, polarity differs, phase differs14.6
lead vs LEAD_MINbelow, exactly at, above14.4
half vs HALF_MINbelow, exactly at, above15.3
gap vs GAP_MINbelow, exactly at, above14.5
frame wholenesswhole, partial14.7
width class1, 8, 32 and the rest14.3
data transitionspayload with no change, payload with a change14.6

The exactly at bins are the ones that matter most and the ones a wide uniform range almost never hits. A lead drawn uniformly from 0 to 15 with a minimum of 4 lands exactly on 4 about one time in sixteen — which sounds adequate until the minimum is 64 cycles and the range is 0 to 1023.

5. The Measurement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   NAIVE      400 transactions, 9 of 22 bins reached
   STRUCTURED 400 legal transactions, 16 of 22 bins reached
   STRUCTURED 460 transactions including illegal kinds, 22 of 22 bins reached

Three numbers, and the relationships between them are the chapter.

9 → 16 at the same transaction count. The naive generator is not slower; it is incapable. The seven bins it never reaches are on axes it does not carry fields for, so no seed and no run length changes the number. This is the single most useful thing to know about a weak generator: more cycles is not the fix, and a team that responds to a coverage hole by extending the regression can spend a month proving it.

16 → 22 only with illegal kinds. The remaining six bins are the below-minimum and partial-frame bins, and legal traffic must never reach them. It does not: 200 legal transactions reached no illegal bin at all. An illegal transaction is a request in this generator, not an accident of wide ranges — which is what makes it safe to drive Chapter 16.1's monitor deliberately into a violation and still have a clean regression mean something.

The boundary bins are actually hit. lead exactly at min 74 times, half 76, gap 72 — because the generator draws boundary values from a deliberate distribution rather than from a uniform range.

6. Building It — Three HDLs

No randomize() is available in the simulator these files are verified in (see the toolchain note in Chapter 16.3), so the randomness is a seeded 32-bit xorshift written out longhand. That turns out to be an advantage for a tutorial rather than a compromise: the stream is reproducible, the same seed produces the same stimulus in all three languages, and the coverage counts below are therefore three measurements of one experiment rather than three unrelated numbers.

The VHDL version is where the language differences become substantive rather than cosmetic. The transaction is a record, so it travels as one port instead of fourteen; and the coverage model is a protected type — a real object with private state and serialised methods, which is more than the SystemVerilog examples in this module can have.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_gen.sv — the twelve-field transaction, with a seeded generator and a coverage model
// spi_txn_gen.sv
//
// Chapter 16.2 -- the transaction object, and the space it has to describe.
//
// THE CLAIM THIS FILE EXISTS TO MEASURE.
//
//     The stimulus space is not the data space.
//
// A generator that randomises the payload and leaves everything else at its default
// has covered almost nothing, and it will report a large number of transactions while
// doing it. Section 4 of the chapter runs two generators with the SAME transaction
// count and compares what they reached.
//
// WHAT IS IN AN SPI TRANSACTION, once Modules 13 to 15 have been through it.
//
// The obvious fields:
//
//     data        the payload
//     len         the frame width
//     cpol, cpha  the mode
//     lsb_first   the bit order
//
// and then the ones a beginner's transaction object does not have, every one of which
// turned out to carry a requirement:
//
//     lead        CS to the first SCLK edge      >= SYNC_N + 2   (14.4)
//     half        one SCLK half-period           >= SYNC_N + 1   (14.1, 15.3)
//     lag         last SCLK edge to CS release                   (13.7)
//     gap         CS high between transactions   >= SYNC_N + 1   (14.5)
//     nbits       how many bits actually sent -- which need not be a whole frame (14.7)
//
// THE TIMING IS PART OF THE TRANSACTION, and that is the first thing the chapter is
// for. Three of Module 14's four published requirements are timing numbers, so a
// transaction object without timing fields cannot express a legal transaction, let
// alone an illegal one -- and a suite built on it can never drive the cases that
// Chapters 14.4, 14.5 and 15.3 exist to check.
//
// WHY THE TRANSACTION IS A PACKED VECTOR HERE.
//
// SystemVerilog would use a class, VHDL uses a record, and Verilog-2001 has neither.
// A packed vector with named field offsets is the one representation all three share,
// so the three versions of this generator differ only where the language genuinely
// differs. The VHDL version uses a RECORD, which is what VHDL actually offers, and
// the chapter's UVM callout shows the class form -- which is the representation to
// use when the simulator has classes, and is not the representation that makes the
// three-language comparison possible.
//
// AND THE INTERESTING AXES ARE RELATIONSHIPS, NOT VALUES.
//
// This is the second thing the chapter is for. `data` has 2^32 values and almost none
// of them matter. What matters is:
//
//     does the master's mode AGREE with the slave's?     (14.6)
//     is `lead` above, at, or below its minimum?         (14.4)
//     is `half` above, at, or below its minimum?         (15.3)
//     is `nbits` a whole number of frames?               (14.7)
//     does `data` have any TRANSITIONS in it?            (14.6 needs one)
//
// Every one of those is a small number of bins over a RELATION between two fields, and
// together they are the space. A coverage model over `data` is a coverage model over
// the least interesting axis there is.

module spi_txn_gen #(
    parameter int DW    = 32,   // payload width
    parameter int TW    = 8,    // timing field width
    parameter int LW    = 6,    // length field width
    // The minima this generator is told about, so that "legal" means something.
    parameter int LEAD_MIN = 4,
    parameter int HALF_MIN = 3,
    parameter int GAP_MIN  = 3
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- the request -------------------------------------------------------
    input  wire              req,          // one cycle: produce a transaction
    // 0 = legal, 1 = deliberately illegal timing, 2 = deliberately mismatched mode.
    // An illegal transaction is a first-class request rather than an accident, which
    // is what lets Chapter 16.1's rule monitor be driven on purpose.
    input  wire [1:0]        kind,
    input  wire [31:0]       seed_in,
    input  wire              seed_load,

    // --- the transaction, as a packed vector -------------------------------
    output reg  [DW-1:0]     t_data,
    output reg  [LW-1:0]     t_len,
    output reg  [LW-1:0]     t_nbits,
    output reg               t_cpol,
    output reg               t_cpha,
    output reg               t_lsb_first,
    output reg               t_m_cpol,     // the MASTER's mode, which may differ
    output reg               t_m_cpha,
    output reg  [TW-1:0]     t_lead,
    output reg  [TW-1:0]     t_half,
    output reg  [TW-1:0]     t_lag,
    output reg  [TW-1:0]     t_gap,
    output reg               t_valid       // one cycle, with the fields above valid
);

    // A 32-bit xorshift, because `randomize()` does not exist in the simulator these
    // examples run in and `$random` is not synthesisable or reproducible across
    // tools. A named, seeded, documented generator is better than either: the same
    // seed gives the same stimulus in all three languages, which is what makes the
    // coverage numbers in the chapter comparable.
    reg [31:0] st;

    function automatic [31:0] nxt(input [31:0] x);
        reg [31:0] y;
        begin
            y = x;
            y = y ^ (y << 13);
            y = y ^ (y >> 17);
            y = y ^ (y << 5);
            nxt = y;
        end
    endfunction

    // `pick(lo, hi)` is inclusive and uses a modulo, which is slightly biased for
    // ranges that do not divide 2^32. Said out loud because a coverage argument that
    // rests on uniformity should not rest on an unexamined modulo -- the bias here is
    // far below the bin widths in section 4.
    function automatic [31:0] pick(input [31:0] r, input integer lo, input integer hi);
        pick = lo + (r % (hi - lo + 1));
    endfunction

    reg [31:0] r0, r1, r2, r3, r4, r5, r6, r7;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            st          <= 32'h1;
            t_valid     <= 1'b0;
            t_data      <= {DW{1'b0}};
            t_len       <= 6'd8;
            t_nbits     <= 6'd8;
            t_cpol      <= 1'b0;
            t_cpha      <= 1'b0;
            t_lsb_first <= 1'b0;
            t_m_cpol    <= 1'b0;
            t_m_cpha    <= 1'b0;
            t_lead      <= LEAD_MIN[TW-1:0];
            t_half      <= HALF_MIN[TW-1:0];
            t_lag       <= 8'd2;
            t_gap       <= GAP_MIN[TW-1:0];
        end else begin
            t_valid <= 1'b0;

            if (seed_load) begin
                // A zero seed is a fixed point of xorshift, so it is refused rather
                // than silently producing a constant stream -- which is the kind of
                // thing that makes a suite look like it ran and not like it stopped.
                st <= (seed_in == 32'h0) ? 32'h1 : seed_in;
            end else if (req) begin
                r0 = nxt(st);
                r1 = nxt(r0);  r2 = nxt(r1);  r3 = nxt(r2);
                r4 = nxt(r3);  r5 = nxt(r4);  r6 = nxt(r5);  r7 = nxt(r6);
                st <= r7;

                // --- the payload --------------------------------------------
                t_data <= r0[DW-1:0];

                // --- the frame width ----------------------------------------
                // Weighted towards the widths that matter rather than uniform over
                // 1..32: 1 is where a word boundary happens every capture, 8 is the
                // common case, and 32 is where the reversal touches every bit.
                case (r1[2:0])
                    3'd0:    t_len <= 6'd1;
                    3'd1:    t_len <= 6'd2;
                    3'd2:    t_len <= 6'd4;
                    3'd3, 3'd4, 3'd5: t_len <= 6'd8;
                    3'd6:    t_len <= 6'd16;
                    default: t_len <= 6'd32;
                endcase

                // --- how many bits are actually sent ------------------------
                // For a legal transaction this equals the width, so the frame is
                // whole. Chapter 14.7's partial frames are an ILLEGAL-kind request.
                t_nbits <= (kind == 2'd1 && r2[0]) ? 6'd3 : 6'd0;  // 0 = "a whole frame"

                // --- the mode ----------------------------------------------
                t_cpol <= r2[1];
                t_cpha <= r2[2];
                // The master's mode AGREES unless a mismatch was requested. That
                // default matters: a generator whose two modes are independently
                // random produces a mismatch three quarters of the time, and a suite
                // built on it spends most of its cycles on a fault.
                //
                // A requested mismatch flips the POLARITY, the PHASE, or both, chosen
                // by a spare random bit. The first version flipped only the polarity
                // -- the phase term reduced to `r2[2] : r2[2]`, which is no flip at
                // all -- so the phase-mismatch bin was never reached and the suite
                // could not exercise Chapter 14.6's harder fault. A ternary whose two
                // arms are identical is worth a second look for exactly this reason.
                t_m_cpol <= (kind == 2'd2 && r2[4]) ? ~r2[1] : r2[1];
                t_m_cpha <= (kind == 2'd2 && (~r2[4] | r2[5])) ? ~r2[2] : r2[2];

                t_lsb_first <= r2[3];

                // --- the timing --------------------------------------------
                // Legal timing is at or above each minimum, and deliberately reaches
                // the minimum EXACTLY -- a generator that always adds margin never
                // tests the boundary, which is where every one of Module 14's
                // requirements was pinned down.
                if (kind == 2'd1) begin
                    t_lead <= pick(r3, 1, LEAD_MIN - 1);      // short on purpose
                    t_half <= pick(r4, 1, HALF_MIN - 1);
                    t_gap  <= pick(r6, 1, GAP_MIN  - 1);
                end else begin
                    t_lead <= pick(r3, LEAD_MIN, LEAD_MIN + 4);
                    t_half <= pick(r4, HALF_MIN, HALF_MIN + 4);
                    t_gap  <= pick(r6, GAP_MIN,  GAP_MIN  + 4);
                end
                t_lag <= pick(r5, 1, 5);

                t_valid <= 1'b1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_gen.v — the same design in Verilog-2001
// spi_txn_gen.v
//
// Chapter 16.2 -- the transaction object, and the space it has to describe.
//
// THE CLAIM THIS FILE EXISTS TO MEASURE.
//
//     The stimulus space is not the data space.
//
// A generator that randomises the payload and leaves everything else at its default
// has covered almost nothing, and it will report a large number of transactions while
// doing it. Section 4 of the chapter runs two generators with the SAME transaction
// count and compares what they reached.
//
// WHAT IS IN AN SPI TRANSACTION, once Modules 13 to 15 have been through it.
//
// The obvious fields:
//
//     data        the payload
//     len         the frame width
//     cpol, cpha  the mode
//     lsb_first   the bit order
//
// and then the ones a beginner's transaction object does not have, every one of which
// turned out to carry a requirement:
//
//     lead        CS to the first SCLK edge      >= SYNC_N + 2   (14.4)
//     half        one SCLK half-period           >= SYNC_N + 1   (14.1, 15.3)
//     lag         last SCLK edge to CS release                   (13.7)
//     gap         CS high between transactions   >= SYNC_N + 1   (14.5)
//     nbits       how many bits actually sent -- which need not be a whole frame (14.7)
//
// THE TIMING IS PART OF THE TRANSACTION, and that is the first thing the chapter is
// for. Three of Module 14's four published requirements are timing numbers, so a
// transaction object without timing fields cannot express a legal transaction, let
// alone an illegal one -- and a suite built on it can never drive the cases that
// Chapters 14.4, 14.5 and 15.3 exist to check.
//
// WHY THE TRANSACTION IS A PACKED VECTOR HERE.
//
// SystemVerilog would use a class, VHDL uses a record, and Verilog-2001 has neither.
// A packed vector with named field offsets is the one representation all three share,
// so the three versions of this generator differ only where the language genuinely
// differs. The VHDL version uses a RECORD, which is what VHDL actually offers, and
// the chapter's UVM callout shows the class form -- which is the representation to
// use when the simulator has classes, and is not the representation that makes the
// three-language comparison possible.
//
// AND THE INTERESTING AXES ARE RELATIONSHIPS, NOT VALUES.
//
// This is the second thing the chapter is for. `data` has 2^32 values and almost none
// of them matter. What matters is:
//
//     does the master's mode AGREE with the slave's?     (14.6)
//     is `lead` above, at, or below its minimum?         (14.4)
//     is `half` above, at, or below its minimum?         (15.3)
//     is `nbits` a whole number of frames?               (14.7)
//     does `data` have any TRANSITIONS in it?            (14.6 needs one)
//
// Every one of those is a small number of bins over a RELATION between two fields, and
// together they are the space. A coverage model over `data` is a coverage model over
// the least interesting axis there is.

module spi_txn_gen #(
    parameter DW    = 32,   // payload width
    parameter TW    = 8,    // timing field width
    parameter LW    = 6,    // length field width
    // The minima this generator is told about, so that "legal" means something.
    parameter LEAD_MIN = 4,
    parameter HALF_MIN = 3,
    parameter GAP_MIN  = 3
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- the request -------------------------------------------------------
    input  wire              req,          // one cycle: produce a transaction
    // 0 = legal, 1 = deliberately illegal timing, 2 = deliberately mismatched mode.
    // An illegal transaction is a first-class request rather than an accident, which
    // is what lets Chapter 16.1's rule monitor be driven on purpose.
    input  wire [1:0]        kind,
    input  wire [31:0]       seed_in,
    input  wire              seed_load,

    // --- the transaction, as a packed vector -------------------------------
    output reg  [DW-1:0]     t_data,
    output reg  [LW-1:0]     t_len,
    output reg  [LW-1:0]     t_nbits,
    output reg               t_cpol,
    output reg               t_cpha,
    output reg               t_lsb_first,
    output reg               t_m_cpol,     // the MASTER's mode, which may differ
    output reg               t_m_cpha,
    output reg  [TW-1:0]     t_lead,
    output reg  [TW-1:0]     t_half,
    output reg  [TW-1:0]     t_lag,
    output reg  [TW-1:0]     t_gap,
    output reg               t_valid       // one cycle, with the fields above valid
);

    // A 32-bit xorshift, because `randomize()` does not exist in the simulator these
    // examples run in and `$random` is not synthesisable or reproducible across
    // tools. A named, seeded, documented generator is better than either: the same
    // seed gives the same stimulus in all three languages, which is what makes the
    // coverage numbers in the chapter comparable.
    reg [31:0] st;

        function [31:0] nxt;
        input [31:0] x;
        reg [31:0] y;
        begin
            y = x;
            y = y ^ (y << 13);
            y = y ^ (y >> 17);
            y = y ^ (y << 5);
            nxt = y;
        end
    endfunction

    // `pick(lo, hi)` is inclusive and uses a modulo, which is slightly biased for
    // ranges that do not divide 2^32. Said out loud because a coverage argument that
    // rests on uniformity should not rest on an unexamined modulo -- the bias here is
    // far below the bin widths in section 4.
        function [31:0] pick;
        input [31:0] r;
        input integer lo;
        input integer hi;
        pick = lo + (r % (hi - lo + 1));
    endfunction

    reg [31:0] r0, r1, r2, r3, r4, r5, r6, r7;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            st          <= 32'h1;
            t_valid     <= 1'b0;
            t_data      <= {DW{1'b0}};
            t_len       <= 6'd8;
            t_nbits     <= 6'd8;
            t_cpol      <= 1'b0;
            t_cpha      <= 1'b0;
            t_lsb_first <= 1'b0;
            t_m_cpol    <= 1'b0;
            t_m_cpha    <= 1'b0;
            t_lead      <= LEAD_MIN[TW-1:0];
            t_half      <= HALF_MIN[TW-1:0];
            t_lag       <= 8'd2;
            t_gap       <= GAP_MIN[TW-1:0];
        end else begin
            t_valid <= 1'b0;

            if (seed_load) begin
                // A zero seed is a fixed point of xorshift, so it is refused rather
                // than silently producing a constant stream -- which is the kind of
                // thing that makes a suite look like it ran and not like it stopped.
                st <= (seed_in == 32'h0) ? 32'h1 : seed_in;
            end else if (req) begin
                r0 = nxt(st);
                r1 = nxt(r0);  r2 = nxt(r1);  r3 = nxt(r2);
                r4 = nxt(r3);  r5 = nxt(r4);  r6 = nxt(r5);  r7 = nxt(r6);
                st <= r7;

                // --- the payload --------------------------------------------
                t_data <= r0[DW-1:0];

                // --- the frame width ----------------------------------------
                // Weighted towards the widths that matter rather than uniform over
                // 1..32: 1 is where a word boundary happens every capture, 8 is the
                // common case, and 32 is where the reversal touches every bit.
                case (r1[2:0])
                    3'd0:    t_len <= 6'd1;
                    3'd1:    t_len <= 6'd2;
                    3'd2:    t_len <= 6'd4;
                    3'd3, 3'd4, 3'd5: t_len <= 6'd8;
                    3'd6:    t_len <= 6'd16;
                    default: t_len <= 6'd32;
                endcase

                // --- how many bits are actually sent ------------------------
                // For a legal transaction this equals the width, so the frame is
                // whole. Chapter 14.7's partial frames are an ILLEGAL-kind request.
                t_nbits <= (kind == 2'd1 && r2[0]) ? 6'd3 : 6'd0;  // 0 = "a whole frame"

                // --- the mode ----------------------------------------------
                t_cpol <= r2[1];
                t_cpha <= r2[2];
                // The master's mode AGREES unless a mismatch was requested. That
                // default matters: a generator whose two modes are independently
                // random produces a mismatch three quarters of the time, and a suite
                // built on it spends most of its cycles on a fault.
                //
                // A requested mismatch flips the POLARITY, the PHASE, or both, chosen
                // by a spare random bit. The first version flipped only the polarity
                // -- the phase term reduced to `r2[2] : r2[2]`, which is no flip at
                // all -- so the phase-mismatch bin was never reached and the suite
                // could not exercise Chapter 14.6's harder fault. A ternary whose two
                // arms are identical is worth a second look for exactly this reason.
                t_m_cpol <= (kind == 2'd2 && r2[4]) ? ~r2[1] : r2[1];
                t_m_cpha <= (kind == 2'd2 && (~r2[4] | r2[5])) ? ~r2[2] : r2[2];

                t_lsb_first <= r2[3];

                // --- the timing --------------------------------------------
                // Legal timing is at or above each minimum, and deliberately reaches
                // the minimum EXACTLY -- a generator that always adds margin never
                // tests the boundary, which is where every one of Module 14's
                // requirements was pinned down.
                if (kind == 2'd1) begin
                    t_lead <= pick(r3, 1, LEAD_MIN - 1);      // short on purpose
                    t_half <= pick(r4, 1, HALF_MIN - 1);
                    t_gap  <= pick(r6, 1, GAP_MIN  - 1);
                end else begin
                    t_lead <= pick(r3, LEAD_MIN, LEAD_MIN + 4);
                    t_half <= pick(r4, HALF_MIN, HALF_MIN + 4);
                    t_gap  <= pick(r6, GAP_MIN,  GAP_MIN  + 4);
                end
                t_lag <= pick(r5, 1, 5);

                t_valid <= 1'b1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_gen.vhd — the same design in VHDL
-- spi_txn_gen.vhd
--
-- Chapter 16.2 -- the transaction object, and the space it has to describe.
--
-- THE CLAIM THIS FILE EXISTS TO MEASURE.
--
--     The stimulus space is not the data space.
--
-- A generator that randomises the payload and leaves everything else at its default
-- has covered almost nothing, and it will report a large number of transactions while
-- doing it. Section 4 of the chapter runs two generators with the SAME transaction
-- count and compares what they reached.
--
-- WHAT IS IN AN SPI TRANSACTION, once Modules 13 to 15 have been through it.
--
-- The obvious fields:
--
--     data        the payload
--     len         the frame width
--     cpol, cpha  the mode
--     lsb_first   the bit order
--
-- and then the ones a beginner's transaction object does not have, every one of which
-- turned out to carry a requirement:
--
--     lead        CS to the first SCLK edge      >= SYNC_N + 2   (14.4)
--     half        one SCLK half-period           >= SYNC_N + 1   (14.1, 15.3)
--     lag         last SCLK edge to CS release                   (13.7)
--     gap         CS high between transactions   >= SYNC_N + 1   (14.5)
--     nbits       how many bits actually sent -- which need not be a whole frame (14.7)
--
-- THE TIMING IS PART OF THE TRANSACTION, and that is the first thing the chapter is
-- for. Three of Module 14's four published requirements are timing numbers, so a
-- transaction object without timing fields cannot express a legal transaction, let
-- alone an illegal one -- and a suite built on it can never drive the cases that
-- Chapters 14.4, 14.5 and 15.3 exist to check.
--
-- WHY THE TRANSACTION IS A PACKED VECTOR HERE.
--
-- SystemVerilog would use a class, VHDL uses a record, and Verilog-2001 has neither.
-- A packed vector with named field offsets is the one representation all three share,
-- so the three versions of this generator differ only where the language genuinely
-- differs. The VHDL version uses a RECORD, which is what VHDL actually offers, and
-- the chapter's UVM callout shows the class form -- which is the representation to
-- use when the simulator has classes, and is not the representation that makes the
-- three-language comparison possible.
--
-- AND THE INTERESTING AXES ARE RELATIONSHIPS, NOT VALUES.
--
-- This is the second thing the chapter is for. `data` has 2^32 values and almost none
-- of them matter. What matters is:
--
--     does the master's mode AGREE with the slave's?     (14.6)
--     is `lead` above, at, or below its minimum?         (14.4)
--     is `half` above, at, or below its minimum?         (15.3)
--     is `nbits` a whole number of frames?               (14.7)
--     does `data` have any TRANSITIONS in it?            (14.6 needs one)
--
-- Every one of those is a small number of bins over a RELATION between two fields, and
-- together they are the space. A coverage model over `data` is a coverage model over
-- the least interesting axis there is.

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

-- VHDL's transaction object is a RECORD, and this is where the three languages stop
-- being the same. The SystemVerilog and Verilog versions carry the fields as separate
-- ports because Verilog-2001 has no aggregate type; VHDL has one, so the transaction
-- travels as a single named value and a field cannot be connected to the wrong port.
--
-- That is a real advantage and it is worth naming: the SystemVerilog version's port
-- list has fourteen entries that a miswiring would silently reorder, and the VHDL
-- version's has one.
package spi_txn_pkg is

    constant DW : positive := 32;
    constant TW : positive := 8;
    constant LW : positive := 6;

    type spi_txn_t is record
        data      : std_logic_vector(DW - 1 downto 0);
        len       : unsigned(LW - 1 downto 0);
        nbits     : unsigned(LW - 1 downto 0);   -- 0 means "a whole frame"
        cpol      : std_logic;
        cpha      : std_logic;
        lsb_first : std_logic;
        m_cpol    : std_logic;                   -- the MASTER's mode
        m_cpha    : std_logic;
        lead      : unsigned(TW - 1 downto 0);
        half      : unsigned(TW - 1 downto 0);
        lag       : unsigned(TW - 1 downto 0);
        gap       : unsigned(TW - 1 downto 0);
    end record;

    constant TXN_RESET : spi_txn_t := (
        data      => (others => '0'),
        len       => to_unsigned(8, LW),
        nbits     => (others => '0'),
        cpol      => '0', cpha => '0', lsb_first => '0',
        m_cpol    => '0', m_cpha => '0',
        lead      => to_unsigned(4, TW),
        half      => to_unsigned(3, TW),
        lag       => to_unsigned(2, TW),
        gap       => to_unsigned(3, TW));

    -- The same xorshift as the other two versions, so the same seed gives the same
    -- stimulus in all three and the coverage counts are one measurement rather than
    -- three unrelated ones.
    function xorshift(x : unsigned(31 downto 0)) return unsigned;
    function pick(r : unsigned(31 downto 0); lo : natural; hi : natural) return natural;

    -- Does the payload have any transition in its low `n` bits? Chapter 14.6's phase
    -- detector needs one, so a suite of constant payloads has not tested it.
    function has_transition(d : std_logic_vector; n : natural) return boolean;

end package;

package body spi_txn_pkg is

    function xorshift(x : unsigned(31 downto 0)) return unsigned is
        variable y : unsigned(31 downto 0) := x;
    begin
        y := y xor shift_left(y, 13);
        y := y xor shift_right(y, 17);
        y := y xor shift_left(y, 5);
        return y;
    end function;

    -- Inclusive, and slightly biased for ranges that do not divide 2^31. Said out
    -- loud because a coverage argument should not rest on an unexamined modulo; the
    -- bias is far below the bin widths the chapter measures.
    --
    -- Only 31 bits are converted, because VHDL's INTEGER is 32-bit SIGNED and
    -- `to_integer` on a full 32-bit unsigned overflows it -- the runtime error is
    -- "result of N + N cannot be represented as INTEGER", which points at the adder
    -- inside `to_integer` rather than at the width that caused it. The other two
    -- languages have no equivalent limit, so this is the kind of difference that
    -- appears only when the same generator is written three times.
    function pick(r : unsigned(31 downto 0); lo : natural; hi : natural) return natural is
    begin
        return lo + (to_integer(r(30 downto 0)) mod (hi - lo + 1));
    end function;

    function has_transition(d : std_logic_vector; n : natural) return boolean is
        variable first : std_logic := d(0);
    begin
        for i in 1 to d'high loop
            if i < n and d(i) /= first then
                return true;
            end if;
        end loop;
        return false;
    end function;

end package body;

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

entity spi_txn_gen is
    generic (
        LEAD_MIN : natural := 4;
        HALF_MIN : natural := 3;
        GAP_MIN  : natural := 3
    );
    port (
        clk       : in  std_logic;
        rst_n     : in  std_logic;

        req       : in  std_logic;                     -- one cycle: produce one
        -- 0 = legal, 1 = deliberately illegal timing, 2 = deliberately mismatched
        -- mode. An illegal transaction is a first-class request rather than an
        -- accident, which is what lets Chapter 16.1's rule monitor be driven on
        -- purpose.
        kind      : in  unsigned(1 downto 0);
        seed_in   : in  unsigned(31 downto 0);
        seed_load : in  std_logic;

        -- ONE port for the whole transaction, which is the record's payoff.
        txn       : out spi_txn_t;
        txn_valid : out std_logic
    );
end entity;

architecture rtl of spi_txn_gen is
    signal st    : unsigned(31 downto 0) := to_unsigned(1, 32);
    signal t_r   : spi_txn_t := TXN_RESET;
    signal vld_r : std_logic := '0';
begin

    txn       <= t_r;
    txn_valid <= vld_r;

    gen : process (clk, rst_n)
        variable r0, r1, r2, r3, r4, r5, r6, r7 : unsigned(31 downto 0);
        variable t : spi_txn_t;
    begin
        if rst_n = '0' then
            st    <= to_unsigned(1, 32);
            t_r   <= TXN_RESET;
            vld_r <= '0';
        elsif rising_edge(clk) then
            vld_r <= '0';

            if seed_load = '1' then
                -- Zero is a fixed point of xorshift, so it is refused rather than
                -- silently producing a constant stream -- which is the kind of thing
                -- that makes a suite look like it ran rather than like it stopped.
                if seed_in = 0 then
                    st <= to_unsigned(1, 32);
                else
                    st <= seed_in;
                end if;
            elsif req = '1' then
                r0 := xorshift(st);
                r1 := xorshift(r0); r2 := xorshift(r1); r3 := xorshift(r2);
                r4 := xorshift(r3); r5 := xorshift(r4); r6 := xorshift(r5);
                r7 := xorshift(r6);
                st <= r7;

                t := TXN_RESET;
                t.data := std_logic_vector(r0);

                -- Weighted towards the widths that BEHAVE differently rather than
                -- uniform over 1..32.
                case to_integer(r1(2 downto 0)) is
                    when 0      => t.len := to_unsigned(1,  LW);
                    when 1      => t.len := to_unsigned(2,  LW);
                    when 2      => t.len := to_unsigned(4,  LW);
                    when 3 | 4 | 5 => t.len := to_unsigned(8,  LW);
                    when 6      => t.len := to_unsigned(16, LW);
                    when others => t.len := to_unsigned(32, LW);
                end case;

                if kind = 1 and r2(0) = '1' then
                    t.nbits := to_unsigned(3, LW);        -- a partial frame (14.7)
                else
                    t.nbits := (others => '0');           -- "a whole frame"
                end if;

                t.cpol := r2(1);
                t.cpha := r2(2);
                -- The master's mode is DERIVED from the slave's unless a mismatch was
                -- requested. Two independently random mode bits disagree three
                -- quarters of the time, and a suite built that way spends most of its
                -- cycles on a fault.
                if kind = 2 and r2(4) = '1' then
                    t.m_cpol := not r2(1);
                else
                    t.m_cpol := r2(1);
                end if;
                if kind = 2 and (r2(4) = '0' or r2(5) = '1') then
                    t.m_cpha := not r2(2);
                else
                    t.m_cpha := r2(2);
                end if;

                t.lsb_first := r2(3);

                -- Legal timing is at or above each minimum and deliberately reaches
                -- the minimum EXACTLY, which is where every one of Module 14's
                -- requirements was pinned down.
                if kind = 1 then
                    t.lead := to_unsigned(pick(r3, 1, LEAD_MIN - 1), TW);
                    t.half := to_unsigned(pick(r4, 1, HALF_MIN - 1), TW);
                    t.gap  := to_unsigned(pick(r6, 1, GAP_MIN  - 1), TW);
                else
                    t.lead := to_unsigned(pick(r3, LEAD_MIN, LEAD_MIN + 4), TW);
                    t.half := to_unsigned(pick(r4, HALF_MIN, HALF_MIN + 4), TW);
                    t.gap  := to_unsigned(pick(r6, GAP_MIN,  GAP_MIN  + 4), TW);
                end if;
                t.lag := to_unsigned(pick(r5, 1, 5), TW);

                t_r   <= t;
                vld_r <= '1';
            end if;
        end if;
    end process;

end architecture;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_gen_tb.sv — two generators at the same transaction count: 9 bins against 16, then 22 with illegal kinds
// spi_txn_gen_tb.sv
//
// The experiment: TWO generators, the SAME number of transactions, one coverage model
// -- and the coverage model is over RELATIONSHIPS rather than over values.
//
//   NAIVE      randomises the payload and leaves everything else at its default.
//              This is what a transaction object without timing fields produces, and
//              it is what most first SPI suites are.
//
//   STRUCTURED the generator in this chapter, asked for legal transactions and then
//              for illegal ones.
//
// The bins, and why each one is a RELATION:
//
//   B0  mode agreement          master's mode vs the slave's      (14.6)
//   B1  lead vs LEAD_MIN        below / exactly at / above        (14.4)
//   B2  half vs HALF_MIN        below / exactly at / above        (15.3)
//   B3  gap vs GAP_MIN          below / exactly at / above        (14.5)
//   B4  frame wholeness         nbits a whole number of frames?   (14.7)
//   B5  width class             1 / narrow / 8 / wide / full      (14.3)
//   B6  data transitions        none / some -- 14.6 needs one
//   B7  bit order               MSB / LSB first                   (14.3)
//
// None of them is a bin over `data`, and that is the point. `data` has 2^32 values and
// exactly one thing about it matters: whether it changes at all.

`timescale 1ns/1ps

module spi_txn_gen_tb;

    localparam int DW = 32, TW = 8, LW = 6;
    localparam int LEAD_MIN = 4, HALF_MIN = 3, GAP_MIN = 3;
    localparam int NBINS = 22;          // total bins across the eight axes

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

    reg          req = 1'b0;
    reg [1:0]    kind = 2'd0;
    reg [31:0]   seed_in = 32'h1;
    reg          seed_load = 1'b0;

    wire [DW-1:0] t_data;
    wire [LW-1:0] t_len, t_nbits;
    wire          t_cpol, t_cpha, t_lsb_first, t_m_cpol, t_m_cpha;
    wire [TW-1:0] t_lead, t_half, t_lag, t_gap;
    wire          t_valid;

    spi_txn_gen #(.DW(DW), .TW(TW), .LW(LW), .LEAD_MIN(LEAD_MIN),
                  .HALF_MIN(HALF_MIN), .GAP_MIN(GAP_MIN)) dut (
        .clk(clk), .rst_n(rst_n),
        .req(req), .kind(kind), .seed_in(seed_in), .seed_load(seed_load),
        .t_data(t_data), .t_len(t_len), .t_nbits(t_nbits),
        .t_cpol(t_cpol), .t_cpha(t_cpha), .t_lsb_first(t_lsb_first),
        .t_m_cpol(t_m_cpol), .t_m_cpha(t_m_cpha),
        .t_lead(t_lead), .t_half(t_half), .t_lag(t_lag), .t_gap(t_gap),
        .t_valid(t_valid)
    );

    integer errors = 0;

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

    // --- the coverage model -------------------------------------------------
    integer hits [0:NBINS-1];
    integer txns;

    // Bin indices, grouped by axis so a report reads as a table.
    localparam int B_MODE_AGREE = 0,  B_MODE_POL = 1,   B_MODE_PHA = 2,
                   B_LEAD_LO    = 3,  B_LEAD_AT = 4,    B_LEAD_HI  = 5,
                   B_HALF_LO    = 6,  B_HALF_AT = 7,    B_HALF_HI  = 8,
                   B_GAP_LO     = 9,  B_GAP_AT  = 10,   B_GAP_HI   = 11,
                   B_WHOLE      = 12, B_PARTIAL = 13,
                   B_W1         = 14, B_WNARROW = 15,   B_W8       = 16,
                   B_WWIDE      = 17, B_WFULL   = 18,
                   B_DATA_FLAT  = 19, B_DATA_VARY = 20,
                   B_MSB        = 21;

    // Does the payload have any transition in its low `len` bits? Chapter 14.6's
    // phase detector needs one, so a suite of constant payloads has not tested it.
    function automatic has_transition(input [DW-1:0] d, input [LW-1:0] n);
        integer i;
        reg first;
        begin
            has_transition = 1'b0;
            first = d[0];
            for (i = 1; i < DW; i = i + 1)
                if (i < n && d[i] != first) has_transition = 1'b1;
        end
    endfunction

    task automatic sample_txn;
        begin
            txns = txns + 1;

            // B0: the relation that matters most -- do the two devices agree?
            if (t_cpol == t_m_cpol && t_cpha == t_m_cpha) hits[B_MODE_AGREE] = hits[B_MODE_AGREE] + 1;
            if (t_cpol != t_m_cpol)                       hits[B_MODE_POL]   = hits[B_MODE_POL]   + 1;
            if (t_cpha != t_m_cpha)                       hits[B_MODE_PHA]   = hits[B_MODE_PHA]   + 1;

            // B1..B3: each timing field against ITS OWN minimum, three bins each --
            // and the middle bin is `exactly at`, which is the one a generator that
            // always adds margin never reaches.
            if (t_lead <  LEAD_MIN) hits[B_LEAD_LO] = hits[B_LEAD_LO] + 1;
            if (t_lead == LEAD_MIN) hits[B_LEAD_AT] = hits[B_LEAD_AT] + 1;
            if (t_lead >  LEAD_MIN) hits[B_LEAD_HI] = hits[B_LEAD_HI] + 1;

            if (t_half <  HALF_MIN) hits[B_HALF_LO] = hits[B_HALF_LO] + 1;
            if (t_half == HALF_MIN) hits[B_HALF_AT] = hits[B_HALF_AT] + 1;
            if (t_half >  HALF_MIN) hits[B_HALF_HI] = hits[B_HALF_HI] + 1;

            if (t_gap  <  GAP_MIN)  hits[B_GAP_LO]  = hits[B_GAP_LO]  + 1;
            if (t_gap  == GAP_MIN)  hits[B_GAP_AT]  = hits[B_GAP_AT]  + 1;
            if (t_gap  >  GAP_MIN)  hits[B_GAP_HI]  = hits[B_GAP_HI]  + 1;

            // B4: frame wholeness. `nbits == 0` is this generator's encoding for
            // "a whole frame".
            if (t_nbits == 0) hits[B_WHOLE]   = hits[B_WHOLE]   + 1;
            else              hits[B_PARTIAL] = hits[B_PARTIAL] + 1;

            // B5: the width classes that behave differently rather than the values.
            if (t_len == 1)                    hits[B_W1]      = hits[B_W1]      + 1;
            else if (t_len <  8)               hits[B_WNARROW] = hits[B_WNARROW] + 1;
            else if (t_len == 8)               hits[B_W8]      = hits[B_W8]      + 1;
            else if (t_len <  32)              hits[B_WWIDE]   = hits[B_WWIDE]   + 1;
            else                               hits[B_WFULL]   = hits[B_WFULL]   + 1;

            // B6: the only thing about the payload that matters.
            if (has_transition(t_data, t_len)) hits[B_DATA_VARY] = hits[B_DATA_VARY] + 1;
            else                               hits[B_DATA_FLAT] = hits[B_DATA_FLAT] + 1;

            // B7
            if (!t_lsb_first) hits[B_MSB] = hits[B_MSB] + 1;
        end
    endtask

    task automatic clear_cov;
        integer i;
        begin
            for (i = 0; i < NBINS; i = i + 1) hits[i] = 0;
            txns = 0;
        end
    endtask

    // The unused input is there because a Verilog-2001 function must have at least
    // one, and keeping the same signature in both languages means the bench body is
    // identical rather than forked.
    function automatic integer filled(input bit unused);
        integer i, n;
        begin
            n = 0;
            for (i = 0; i < NBINS; i = i + 1) if (hits[i] != 0) n = n + 1;
            filled = n;
        end
    endfunction

    // --- the NAIVE generator, modelled rather than merely described ----------
    // A transaction object with a payload and nothing else. The defaults are what a
    // real suite leaves them at: mode 0, 8-bit frames, MSB first, and whatever timing
    // the driver happens to hard-code.
    reg [DW-1:0] naive_data;
    reg [31:0]   nst;

    task automatic naive_txn;
        begin
            nst = nst ^ (nst << 13);
            nst = nst ^ (nst >> 17);
            nst = nst ^ (nst << 5);
            naive_data = nst;
            txns = txns + 1;
            // Everything below is the DEFAULT, which is the whole point.
            hits[B_MODE_AGREE] = hits[B_MODE_AGREE] + 1;   // modes always agree
            hits[B_LEAD_HI]    = hits[B_LEAD_HI]    + 1;   // driver's generous default
            hits[B_HALF_HI]    = hits[B_HALF_HI]    + 1;
            hits[B_GAP_HI]     = hits[B_GAP_HI]     + 1;
            hits[B_WHOLE]      = hits[B_WHOLE]      + 1;
            hits[B_W8]         = hits[B_W8]         + 1;   // 8-bit frames only
            hits[B_MSB]        = hits[B_MSB]        + 1;   // MSB first only
            if (has_transition(naive_data, 6'd8)) hits[B_DATA_VARY] = hits[B_DATA_VARY] + 1;
            else                                  hits[B_DATA_FLAT] = hits[B_DATA_FLAT] + 1;
        end
    endtask

    task automatic gen(input [1:0] k, input integer n);
        integer i;
        begin
            kind = k;
            for (i = 0; i < n; i = i + 1) begin
                @(negedge clk);
                req = 1'b1;
                @(negedge clk);
                req = 1'b0;
                @(posedge clk);
                if (t_valid) sample_txn();
            end
        end
    endtask

    integer i, naive_filled, legal_filled, both_filled;

    initial begin
        rst_n = 1'b1;
        repeat (2) @(negedge clk);
        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (4) @(negedge clk);
        @(negedge clk); seed_in = 32'h5EED_1602; seed_load = 1'b1;
        @(negedge clk); seed_load = 1'b0;

        // =============================================================
        // 1. THE NAIVE GENERATOR: 400 transactions of randomised payload.
        // =============================================================
        clear_cov();
        nst = 32'h5EED_1602;
        for (i = 0; i < 400; i = i + 1) naive_txn();
        naive_filled = filled(1'b0);
        $display("  NAIVE      %0d transactions, %0d of %0d bins reached",
                 txns, naive_filled, NBINS);

        // =============================================================
        // 2. THE STRUCTURED GENERATOR, legal only: the same 400.
        // =============================================================
        clear_cov();
        gen(2'd0, 400);
        legal_filled = filled(1'b0);
        $display("  STRUCTURED %0d legal transactions, %0d of %0d bins reached",
                 txns, legal_filled, NBINS);

        // =============================================================
        // 3. AND WITH THE ILLEGAL KINDS ASKED FOR DELIBERATELY.
        // =============================================================
        clear_cov();
        gen(2'd0, 300);
        gen(2'd1, 80);     // illegal timing
        gen(2'd2, 80);     // mismatched mode
        both_filled = filled(1'b0);
        $display("  STRUCTURED %0d transactions including illegal kinds, %0d of %0d bins reached",
                 txns, both_filled, NBINS);

        // --- the table ------------------------------------------------------
        $display("  bin                        hits   axis");
        $display("  modes agree             %7d   mode agreement (14.6)", hits[B_MODE_AGREE]);
        $display("  polarity differs        %7d   mode agreement (14.6)", hits[B_MODE_POL]);
        $display("  phase differs           %7d   mode agreement (14.6)", hits[B_MODE_PHA]);
        $display("  lead below min          %7d   lead vs LEAD_MIN (14.4)", hits[B_LEAD_LO]);
        $display("  lead exactly at min     %7d   lead vs LEAD_MIN (14.4)", hits[B_LEAD_AT]);
        $display("  lead above min          %7d   lead vs LEAD_MIN (14.4)", hits[B_LEAD_HI]);
        $display("  half below min          %7d   half vs HALF_MIN (15.3)", hits[B_HALF_LO]);
        $display("  half exactly at min     %7d   half vs HALF_MIN (15.3)", hits[B_HALF_AT]);
        $display("  gap below min           %7d   gap vs GAP_MIN (14.5)", hits[B_GAP_LO]);
        $display("  gap exactly at min      %7d   gap vs GAP_MIN (14.5)", hits[B_GAP_AT]);
        $display("  whole frame             %7d   frame wholeness (14.7)", hits[B_WHOLE]);
        $display("  partial frame           %7d   frame wholeness (14.7)", hits[B_PARTIAL]);
        $display("  width 1                 %7d   width class (14.3)", hits[B_W1]);
        $display("  width 32                %7d   width class (14.3)", hits[B_WFULL]);
        $display("  payload with no change  %7d   data transitions (14.6)", hits[B_DATA_FLAT]);
        $display("  payload with a change   %7d   data transitions (14.6)", hits[B_DATA_VARY]);

        // 1. THE NAIVE GENERATOR REACHES ALMOST NOTHING, and the number is the
        //    chapter's headline. Asserted rather than displayed, because a displayed
        //    number invites the reading that it was merely low this time.
        if (naive_filled > 10) begin
            $display("  FAIL: the naive generator reached %0d bins, which is more than a payload-only generator can -- the model is not measuring relationships",
                     naive_filled);
            errors = errors + 1;
        end
        $display("  the naive generator reached %0d of %0d bins with 400 transactions. It cannot reach any more, at any transaction count, because the axes it leaves at their defaults are not axes it varies",
                 naive_filled, NBINS);

        // 2. AND THE STRUCTURED ONE REACHES MORE WITH THE SAME COUNT -- which is what
        //    separates a stimulus space from a data space.
        if (legal_filled <= naive_filled) begin
            $display("  FAIL: the structured generator reached %0d bins against the naive generator's %0d, at the same transaction count",
                     legal_filled, naive_filled);
            errors = errors + 1;
        end
        $display("  the structured generator reached %0d of %0d with the SAME 400 transactions -- the difference is not the count, it is which fields exist",
                 legal_filled, NBINS);

        // 3. THE BOUNDARY BINS ARE REACHED, which is the half a margin-adding
        //    generator misses.
        if (hits[B_LEAD_AT] == 0 || hits[B_HALF_AT] == 0 || hits[B_GAP_AT] == 0) begin
            $display("  FAIL: a boundary bin was never reached -- lead_at=%0d half_at=%0d gap_at=%0d. Every one of Module 14's requirements was pinned down AT its minimum, and a generator that always adds margin tests none of them",
                     hits[B_LEAD_AT], hits[B_HALF_AT], hits[B_GAP_AT]);
            errors = errors + 1;
        end
        $display("  and the boundary bins are reached: lead exactly at its minimum %0d times, half %0d, gap %0d -- which is where every one of Module 14's requirements was actually pinned down",
                 hits[B_LEAD_AT], hits[B_HALF_AT], hits[B_GAP_AT]);

        // 4. THE ILLEGAL BINS ARE ONLY REACHED WHEN ASKED FOR, which is the property
        //    that makes a legal-only regression meaningful.
        clear_cov();
        gen(2'd0, 200);
        if (hits[B_LEAD_LO] != 0 || hits[B_HALF_LO] != 0 || hits[B_GAP_LO] != 0
            || hits[B_MODE_POL] != 0 || hits[B_MODE_PHA] != 0
            || hits[B_PARTIAL] != 0) begin
            $display("  FAIL: a legal-only run reached an illegal bin, so 'legal' does not mean anything and Chapter 16.1's rule monitor would fire during a clean regression");
            errors = errors + 1;
        end
        $display("  200 legal transactions reached no illegal bin at all, so a clean regression stays clean -- an illegal transaction is a REQUEST here, not an accident, which is what lets Chapter 16.1's monitor be driven on purpose");

        // 5. AND THE MODES AGREE BY DEFAULT, which is a design decision worth
        //    checking. Two independently random mode bits disagree three quarters of
        //    the time, so a suite built that way spends most of its cycles on a fault.
        if (hits[B_MODE_AGREE] != txns) begin
            $display("  FAIL: %0d of %0d legal transactions had disagreeing modes; two independently random mode bits disagree three quarters of the time and a suite built that way spends most of its cycles on a fault",
                     txns - hits[B_MODE_AGREE], txns);
            errors = errors + 1;
        end
        $display("  and in all %0d legal transactions the master's mode AGREED with the slave's, because the master's mode is DERIVED from the slave's rather than independently random",
                 txns);

        // 6. THE SEED IS REPRODUCIBLE, which is what makes any of these numbers
        //    comparable between the three languages.
        clear_cov();
        @(negedge clk); seed_in = 32'h5EED_1602; seed_load = 1'b1;
        @(negedge clk); seed_load = 1'b0;
        gen(2'd0, 50);
        i = hits[B_DATA_VARY];
        clear_cov();
        @(negedge clk); seed_in = 32'h5EED_1602; seed_load = 1'b1;
        @(negedge clk); seed_load = 1'b0;
        gen(2'd0, 50);
        if (hits[B_DATA_VARY] != i) begin
            $display("  FAIL: the same seed produced different stimulus (%0d then %0d)", i, hits[B_DATA_VARY]);
            errors = errors + 1;
        end
        $display("  the same seed produces the same stimulus twice, which is what makes these counts comparable across the three languages rather than three unrelated numbers");

        // 7. AND A ZERO SEED IS REFUSED, because it is a fixed point of xorshift.
        clear_cov();
        @(negedge clk); seed_in = 32'h0; seed_load = 1'b1;
        @(negedge clk); seed_load = 1'b0;
        gen(2'd0, 50);
        if (hits[B_DATA_VARY] == 0 && hits[B_DATA_FLAT] == txns) begin
            $display("  FAIL: a zero seed produced a constant stream, which looks like a suite that ran rather than one that stopped");
            errors = errors + 1;
        end
        $display("  a zero seed is refused and replaced, because zero is a fixed point of xorshift and a constant stream looks like a suite that ran rather than one that stopped");

        if (errors == 0)
            $display("PASS: the stimulus space is not the data space, and the measurement is two generators at the SAME transaction count. A payload-only transaction object -- which is what an object without timing fields is -- reached %0d of %0d bins with four hundred transactions, and cannot reach more at any count, because the axes it leaves at their defaults are not axes it varies. The structured object reached %0d with the same four hundred, and %0d once illegal kinds were asked for. The bins that make the difference are all RELATIONS rather than values: whether the two devices' modes agree, whether each timing field is below, exactly at, or above its own minimum, whether the bit count is a whole number of frames, and whether the payload contains any transition at all -- there is no bin over `data`, which has four billion values of which exactly one property matters. Three of Module 14's four published requirements are timing numbers, so a transaction object without timing fields cannot express a legal transaction let alone an illegal one. The boundary bins are reached deliberately, because every one of those requirements was pinned down AT its minimum and a generator that always adds margin tests none of them. An illegal transaction is a REQUEST rather than an accident, so a legal-only run reached no illegal bin and a clean regression stays clean. The master's mode is DERIVED from the slave's rather than independently random, because two independent mode bits disagree three quarters of the time. And the generator is seeded and reproducible, which is what makes these counts one measurement in three languages rather than three unrelated numbers",
                     naive_filled, NBINS, legal_filled, both_filled);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_gen_tb.v — the same bench in Verilog-2001
// spi_txn_gen_tb.v
//
// The experiment: TWO generators, the SAME number of transactions, one coverage model
// -- and the coverage model is over RELATIONSHIPS rather than over values.
//
//   NAIVE      randomises the payload and leaves everything else at its default.
//              This is what a transaction object without timing fields produces, and
//              it is what most first SPI suites are.
//
//   STRUCTURED the generator in this chapter, asked for legal transactions and then
//              for illegal ones.
//
// The bins, and why each one is a RELATION:
//
//   B0  mode agreement          master's mode vs the slave's      (14.6)
//   B1  lead vs LEAD_MIN        below / exactly at / above        (14.4)
//   B2  half vs HALF_MIN        below / exactly at / above        (15.3)
//   B3  gap vs GAP_MIN          below / exactly at / above        (14.5)
//   B4  frame wholeness         nbits a whole number of frames?   (14.7)
//   B5  width class             1 / narrow / 8 / wide / full      (14.3)
//   B6  data transitions        none / some -- 14.6 needs one
//   B7  bit order               MSB / LSB first                   (14.3)
//
// None of them is a bin over `data`, and that is the point. `data` has 2^32 values and
// exactly one thing about it matters: whether it changes at all.

`timescale 1ns/1ps

module spi_txn_gen_tb;

    localparam DW = 32, TW = 8, LW = 6;
    localparam LEAD_MIN = 4, HALF_MIN = 3, GAP_MIN = 3;
    localparam NBINS = 22;          // total bins across the eight axes

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

    reg          req;
    reg [1:0]    kind;
    reg [31:0]   seed_in;
    reg          seed_load;

    wire [DW-1:0] t_data;
    wire [LW-1:0] t_len, t_nbits;
    wire          t_cpol, t_cpha, t_lsb_first, t_m_cpol, t_m_cpha;
    wire [TW-1:0] t_lead, t_half, t_lag, t_gap;
    wire          t_valid;

    spi_txn_gen #(.DW(DW), .TW(TW), .LW(LW), .LEAD_MIN(LEAD_MIN),
                  .HALF_MIN(HALF_MIN), .GAP_MIN(GAP_MIN)) dut (
        .clk(clk), .rst_n(rst_n),
        .req(req), .kind(kind), .seed_in(seed_in), .seed_load(seed_load),
        .t_data(t_data), .t_len(t_len), .t_nbits(t_nbits),
        .t_cpol(t_cpol), .t_cpha(t_cpha), .t_lsb_first(t_lsb_first),
        .t_m_cpol(t_m_cpol), .t_m_cpha(t_m_cpha),
        .t_lead(t_lead), .t_half(t_half), .t_lag(t_lag), .t_gap(t_gap),
        .t_valid(t_valid)
    );

    integer errors;

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

    // --- the coverage model -------------------------------------------------
    integer hits [0:NBINS-1];
    integer txns;

    // Bin indices, grouped by axis so a report reads as a table.
    localparam B_MODE_AGREE = 0,  B_MODE_POL = 1,   B_MODE_PHA = 2,
                   B_LEAD_LO    = 3,  B_LEAD_AT = 4,    B_LEAD_HI  = 5,
                   B_HALF_LO    = 6,  B_HALF_AT = 7,    B_HALF_HI  = 8,
                   B_GAP_LO     = 9,  B_GAP_AT  = 10,   B_GAP_HI   = 11,
                   B_WHOLE      = 12, B_PARTIAL = 13,
                   B_W1         = 14, B_WNARROW = 15,   B_W8       = 16,
                   B_WWIDE      = 17, B_WFULL   = 18,
                   B_DATA_FLAT  = 19, B_DATA_VARY = 20,
                   B_MSB        = 21;

    // Does the payload have any transition in its low `len` bits? Chapter 14.6's
    // phase detector needs one, so a suite of constant payloads has not tested it.
        function has_transition;
        input [DW-1:0] d;
        input [LW-1:0] n;
        integer i;
        reg first;
        begin
            has_transition = 1'b0;
            first = d[0];
            for (i = 1; i < DW; i = i + 1)
                if (i < n && d[i] != first) has_transition = 1'b1;
        end
    endfunction

    task sample_txn;
        begin
            txns = txns + 1;

            // B0: the relation that matters most -- do the two devices agree?
            if (t_cpol == t_m_cpol && t_cpha == t_m_cpha) hits[B_MODE_AGREE] = hits[B_MODE_AGREE] + 1;
            if (t_cpol != t_m_cpol)                       hits[B_MODE_POL]   = hits[B_MODE_POL]   + 1;
            if (t_cpha != t_m_cpha)                       hits[B_MODE_PHA]   = hits[B_MODE_PHA]   + 1;

            // B1..B3: each timing field against ITS OWN minimum, three bins each --
            // and the middle bin is `exactly at`, which is the one a generator that
            // always adds margin never reaches.
            if (t_lead <  LEAD_MIN) hits[B_LEAD_LO] = hits[B_LEAD_LO] + 1;
            if (t_lead == LEAD_MIN) hits[B_LEAD_AT] = hits[B_LEAD_AT] + 1;
            if (t_lead >  LEAD_MIN) hits[B_LEAD_HI] = hits[B_LEAD_HI] + 1;

            if (t_half <  HALF_MIN) hits[B_HALF_LO] = hits[B_HALF_LO] + 1;
            if (t_half == HALF_MIN) hits[B_HALF_AT] = hits[B_HALF_AT] + 1;
            if (t_half >  HALF_MIN) hits[B_HALF_HI] = hits[B_HALF_HI] + 1;

            if (t_gap  <  GAP_MIN)  hits[B_GAP_LO]  = hits[B_GAP_LO]  + 1;
            if (t_gap  == GAP_MIN)  hits[B_GAP_AT]  = hits[B_GAP_AT]  + 1;
            if (t_gap  >  GAP_MIN)  hits[B_GAP_HI]  = hits[B_GAP_HI]  + 1;

            // B4: frame wholeness. `nbits == 0` is this generator's encoding for
            // "a whole frame".
            if (t_nbits == 0) hits[B_WHOLE]   = hits[B_WHOLE]   + 1;
            else              hits[B_PARTIAL] = hits[B_PARTIAL] + 1;

            // B5: the width classes that behave differently rather than the values.
            if (t_len == 1)                    hits[B_W1]      = hits[B_W1]      + 1;
            else if (t_len <  8)               hits[B_WNARROW] = hits[B_WNARROW] + 1;
            else if (t_len == 8)               hits[B_W8]      = hits[B_W8]      + 1;
            else if (t_len <  32)              hits[B_WWIDE]   = hits[B_WWIDE]   + 1;
            else                               hits[B_WFULL]   = hits[B_WFULL]   + 1;

            // B6: the only thing about the payload that matters.
            if (has_transition(t_data, t_len)) hits[B_DATA_VARY] = hits[B_DATA_VARY] + 1;
            else                               hits[B_DATA_FLAT] = hits[B_DATA_FLAT] + 1;

            // B7
            if (!t_lsb_first) hits[B_MSB] = hits[B_MSB] + 1;
        end
    endtask

    task clear_cov;
        integer i;
        begin
            for (i = 0; i < NBINS; i = i + 1) hits[i] = 0;
            txns = 0;
        end
    endtask

    // The unused input is there because a Verilog-2001 function must have at least
    // one, and keeping the same signature in both languages means the bench body is
    // identical rather than forked.
        function integer filled;
        input unused;
        integer i, n;
        begin
            n = 0;
            for (i = 0; i < NBINS; i = i + 1) if (hits[i] != 0) n = n + 1;
            filled = n;
        end
    endfunction

    // --- the NAIVE generator, modelled rather than merely described ----------
    // A transaction object with a payload and nothing else. The defaults are what a
    // real suite leaves them at: mode 0, 8-bit frames, MSB first, and whatever timing
    // the driver happens to hard-code.
    reg [DW-1:0] naive_data;
    reg [31:0]   nst;

    task naive_txn;
        begin
            nst = nst ^ (nst << 13);
            nst = nst ^ (nst >> 17);
            nst = nst ^ (nst << 5);
            naive_data = nst;
            txns = txns + 1;
            // Everything below is the DEFAULT, which is the whole point.
            hits[B_MODE_AGREE] = hits[B_MODE_AGREE] + 1;   // modes always agree
            hits[B_LEAD_HI]    = hits[B_LEAD_HI]    + 1;   // driver's generous default
            hits[B_HALF_HI]    = hits[B_HALF_HI]    + 1;
            hits[B_GAP_HI]     = hits[B_GAP_HI]     + 1;
            hits[B_WHOLE]      = hits[B_WHOLE]      + 1;
            hits[B_W8]         = hits[B_W8]         + 1;   // 8-bit frames only
            hits[B_MSB]        = hits[B_MSB]        + 1;   // MSB first only
            if (has_transition(naive_data, 6'd8)) hits[B_DATA_VARY] = hits[B_DATA_VARY] + 1;
            else                                  hits[B_DATA_FLAT] = hits[B_DATA_FLAT] + 1;
        end
    endtask

        task gen;
        input [1:0] k;
        input integer n;
        integer i;
        begin
            kind = k;
            for (i = 0; i < n; i = i + 1) begin
                @(negedge clk);
                req = 1'b1;
                @(negedge clk);
                req = 1'b0;
                @(posedge clk);
                if (t_valid) sample_txn();
            end
        end
    endtask

    integer i, naive_filled, legal_filled, both_filled;

    initial begin
        rst_n = 1'b1;
        repeat (2) @(negedge clk);
        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (4) @(negedge clk);
        @(negedge clk); seed_in = 32'h5EED_1602; seed_load = 1'b1;
        @(negedge clk); seed_load = 1'b0;

        // =============================================================
        // 1. THE NAIVE GENERATOR: 400 transactions of randomised payload.
        // =============================================================
        clear_cov();
        nst = 32'h5EED_1602;
        for (i = 0; i < 400; i = i + 1) naive_txn();
        naive_filled = filled(1'b0);
        $display("  NAIVE      %0d transactions, %0d of %0d bins reached",
                 txns, naive_filled, NBINS);

        // =============================================================
        // 2. THE STRUCTURED GENERATOR, legal only: the same 400.
        // =============================================================
        clear_cov();
        gen(2'd0, 400);
        legal_filled = filled(1'b0);
        $display("  STRUCTURED %0d legal transactions, %0d of %0d bins reached",
                 txns, legal_filled, NBINS);

        // =============================================================
        // 3. AND WITH THE ILLEGAL KINDS ASKED FOR DELIBERATELY.
        // =============================================================
        clear_cov();
        gen(2'd0, 300);
        gen(2'd1, 80);     // illegal timing
        gen(2'd2, 80);     // mismatched mode
        both_filled = filled(1'b0);
        $display("  STRUCTURED %0d transactions including illegal kinds, %0d of %0d bins reached",
                 txns, both_filled, NBINS);

        // --- the table ------------------------------------------------------
        $display("  bin                        hits   axis");
        $display("  modes agree             %7d   mode agreement (14.6)", hits[B_MODE_AGREE]);
        $display("  polarity differs        %7d   mode agreement (14.6)", hits[B_MODE_POL]);
        $display("  phase differs           %7d   mode agreement (14.6)", hits[B_MODE_PHA]);
        $display("  lead below min          %7d   lead vs LEAD_MIN (14.4)", hits[B_LEAD_LO]);
        $display("  lead exactly at min     %7d   lead vs LEAD_MIN (14.4)", hits[B_LEAD_AT]);
        $display("  lead above min          %7d   lead vs LEAD_MIN (14.4)", hits[B_LEAD_HI]);
        $display("  half below min          %7d   half vs HALF_MIN (15.3)", hits[B_HALF_LO]);
        $display("  half exactly at min     %7d   half vs HALF_MIN (15.3)", hits[B_HALF_AT]);
        $display("  gap below min           %7d   gap vs GAP_MIN (14.5)", hits[B_GAP_LO]);
        $display("  gap exactly at min      %7d   gap vs GAP_MIN (14.5)", hits[B_GAP_AT]);
        $display("  whole frame             %7d   frame wholeness (14.7)", hits[B_WHOLE]);
        $display("  partial frame           %7d   frame wholeness (14.7)", hits[B_PARTIAL]);
        $display("  width 1                 %7d   width class (14.3)", hits[B_W1]);
        $display("  width 32                %7d   width class (14.3)", hits[B_WFULL]);
        $display("  payload with no change  %7d   data transitions (14.6)", hits[B_DATA_FLAT]);
        $display("  payload with a change   %7d   data transitions (14.6)", hits[B_DATA_VARY]);

        // 1. THE NAIVE GENERATOR REACHES ALMOST NOTHING, and the number is the
        //    chapter's headline. Asserted rather than displayed, because a displayed
        //    number invites the reading that it was merely low this time.
        if (naive_filled > 10) begin
            $display("  FAIL: the naive generator reached %0d bins, which is more than a payload-only generator can -- the model is not measuring relationships",
                     naive_filled);
            errors = errors + 1;
        end
        $display("  the naive generator reached %0d of %0d bins with 400 transactions. It cannot reach any more, at any transaction count, because the axes it leaves at their defaults are not axes it varies",
                 naive_filled, NBINS);

        // 2. AND THE STRUCTURED ONE REACHES MORE WITH THE SAME COUNT -- which is what
        //    separates a stimulus space from a data space.
        if (legal_filled <= naive_filled) begin
            $display("  FAIL: the structured generator reached %0d bins against the naive generator's %0d, at the same transaction count",
                     legal_filled, naive_filled);
            errors = errors + 1;
        end
        $display("  the structured generator reached %0d of %0d with the SAME 400 transactions -- the difference is not the count, it is which fields exist",
                 legal_filled, NBINS);

        // 3. THE BOUNDARY BINS ARE REACHED, which is the half a margin-adding
        //    generator misses.
        if (hits[B_LEAD_AT] == 0 || hits[B_HALF_AT] == 0 || hits[B_GAP_AT] == 0) begin
            $display("  FAIL: a boundary bin was never reached -- lead_at=%0d half_at=%0d gap_at=%0d. Every one of Module 14's requirements was pinned down AT its minimum, and a generator that always adds margin tests none of them",
                     hits[B_LEAD_AT], hits[B_HALF_AT], hits[B_GAP_AT]);
            errors = errors + 1;
        end
        $display("  and the boundary bins are reached: lead exactly at its minimum %0d times, half %0d, gap %0d -- which is where every one of Module 14's requirements was actually pinned down",
                 hits[B_LEAD_AT], hits[B_HALF_AT], hits[B_GAP_AT]);

        // 4. THE ILLEGAL BINS ARE ONLY REACHED WHEN ASKED FOR, which is the property
        //    that makes a legal-only regression meaningful.
        clear_cov();
        gen(2'd0, 200);
        if (hits[B_LEAD_LO] != 0 || hits[B_HALF_LO] != 0 || hits[B_GAP_LO] != 0
            || hits[B_MODE_POL] != 0 || hits[B_MODE_PHA] != 0
            || hits[B_PARTIAL] != 0) begin
            $display("  FAIL: a legal-only run reached an illegal bin, so 'legal' does not mean anything and Chapter 16.1's rule monitor would fire during a clean regression");
            errors = errors + 1;
        end
        $display("  200 legal transactions reached no illegal bin at all, so a clean regression stays clean -- an illegal transaction is a REQUEST here, not an accident, which is what lets Chapter 16.1's monitor be driven on purpose");

        // 5. AND THE MODES AGREE BY DEFAULT, which is a design decision worth
        //    checking. Two independently random mode bits disagree three quarters of
        //    the time, so a suite built that way spends most of its cycles on a fault.
        if (hits[B_MODE_AGREE] != txns) begin
            $display("  FAIL: %0d of %0d legal transactions had disagreeing modes; two independently random mode bits disagree three quarters of the time and a suite built that way spends most of its cycles on a fault",
                     txns - hits[B_MODE_AGREE], txns);
            errors = errors + 1;
        end
        $display("  and in all %0d legal transactions the master's mode AGREED with the slave's, because the master's mode is DERIVED from the slave's rather than independently random",
                 txns);

        // 6. THE SEED IS REPRODUCIBLE, which is what makes any of these numbers
        //    comparable between the three languages.
        clear_cov();
        @(negedge clk); seed_in = 32'h5EED_1602; seed_load = 1'b1;
        @(negedge clk); seed_load = 1'b0;
        gen(2'd0, 50);
        i = hits[B_DATA_VARY];
        clear_cov();
        @(negedge clk); seed_in = 32'h5EED_1602; seed_load = 1'b1;
        @(negedge clk); seed_load = 1'b0;
        gen(2'd0, 50);
        if (hits[B_DATA_VARY] != i) begin
            $display("  FAIL: the same seed produced different stimulus (%0d then %0d)", i, hits[B_DATA_VARY]);
            errors = errors + 1;
        end
        $display("  the same seed produces the same stimulus twice, which is what makes these counts comparable across the three languages rather than three unrelated numbers");

        // 7. AND A ZERO SEED IS REFUSED, because it is a fixed point of xorshift.
        clear_cov();
        @(negedge clk); seed_in = 32'h0; seed_load = 1'b1;
        @(negedge clk); seed_load = 1'b0;
        gen(2'd0, 50);
        if (hits[B_DATA_VARY] == 0 && hits[B_DATA_FLAT] == txns) begin
            $display("  FAIL: a zero seed produced a constant stream, which looks like a suite that ran rather than one that stopped");
            errors = errors + 1;
        end
        $display("  a zero seed is refused and replaced, because zero is a fixed point of xorshift and a constant stream looks like a suite that ran rather than one that stopped");

        if (errors == 0)
            $display("PASS: the stimulus space is not the data space, and the measurement is two generators at the SAME transaction count. A payload-only transaction object -- which is what an object without timing fields is -- reached %0d of %0d bins with four hundred transactions, and cannot reach more at any count, because the axes it leaves at their defaults are not axes it varies. The structured object reached %0d with the same four hundred, and %0d once illegal kinds were asked for. The bins that make the difference are all RELATIONS rather than values: whether the two devices' modes agree, whether each timing field is below, exactly at, or above its own minimum, whether the bit count is a whole number of frames, and whether the payload contains any transition at all -- there is no bin over `data`, which has four billion values of which exactly one property matters. Three of Module 14's four published requirements are timing numbers, so a transaction object without timing fields cannot express a legal transaction let alone an illegal one. The boundary bins are reached deliberately, because every one of those requirements was pinned down AT its minimum and a generator that always adds margin tests none of them. An illegal transaction is a REQUEST rather than an accident, so a legal-only run reached no illegal bin and a clean regression stays clean. The master's mode is DERIVED from the slave's rather than independently random, because two independent mode bits disagree three quarters of the time. And the generator is seeded and reproducible, which is what makes these counts one measurement in three languages rather than three unrelated numbers",
                     naive_filled, NBINS, legal_filled, both_filled);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b1;
        req = 1'b0;
        kind = 2'd0;
        seed_in = 32'h1;
        seed_load = 1'b0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_txn_gen_tb.vhd — the same bench in VHDL
-- spi_txn_gen_tb.vhd
--
-- The experiment: TWO generators, the SAME number of transactions, one coverage model
-- -- and the coverage model is over RELATIONSHIPS rather than over values.
--
--   NAIVE      randomises the payload and leaves everything else at its default.
--              This is what a transaction object without timing fields produces, and
--              it is what most first SPI suites are.
--
--   STRUCTURED the generator in this chapter, asked for legal transactions and then
--              for illegal ones.
--
-- The bins, and why each one is a RELATION:
--
--   B0  mode agreement          master's mode vs the slave's      (14.6)
--   B1  lead vs LEAD_MIN        below / exactly at / above        (14.4)
--   B2  half vs HALF_MIN        below / exactly at / above        (15.3)
--   B3  gap vs GAP_MIN          below / exactly at / above        (14.5)
--   B4  frame wholeness         nbits a whole number of frames?   (14.7)
--   B5  width class             1 / narrow / 8 / wide / full      (14.3)
--   B6  data transitions        none / some -- 14.6 needs one
--   B7  bit order               MSB / LSB first                   (14.3)
--
-- None of them is a bin over `data`, and that is the point. `data` has 2^32 values and
-- exactly one thing about it matters: whether it changes at all.

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

-- The coverage model is a PROTECTED TYPE, which is VHDL's answer to a class: private
-- state, methods, and no way for the rest of the bench to reach inside it. The
-- SystemVerilog version uses a module-scope array and tasks, because the simulator
-- these examples run in has no working class polymorphism -- so this is the one place
-- in the module where VHDL offers MORE than the SystemVerilog, and it is worth
-- noticing rather than glossing over.
package spi_cov_pkg is

    constant NBINS : natural := 22;

    constant B_MODE_AGREE : natural := 0;
    constant B_MODE_POL   : natural := 1;
    constant B_MODE_PHA   : natural := 2;
    constant B_LEAD_LO    : natural := 3;
    constant B_LEAD_AT    : natural := 4;
    constant B_LEAD_HI    : natural := 5;
    constant B_HALF_LO    : natural := 6;
    constant B_HALF_AT    : natural := 7;
    constant B_HALF_HI    : natural := 8;
    constant B_GAP_LO     : natural := 9;
    constant B_GAP_AT     : natural := 10;
    constant B_GAP_HI     : natural := 11;
    constant B_WHOLE      : natural := 12;
    constant B_PARTIAL    : natural := 13;
    constant B_W1         : natural := 14;
    constant B_WNARROW    : natural := 15;
    constant B_W8         : natural := 16;
    constant B_WWIDE      : natural := 17;
    constant B_WFULL      : natural := 18;
    constant B_DATA_FLAT  : natural := 19;
    constant B_DATA_VARY  : natural := 20;
    constant B_MSB        : natural := 21;

    type coverage_t is protected
        procedure clear;
        procedure sample(t : spi_txn_t; lead_min : natural;
                         half_min : natural; gap_min : natural);
        -- A naive transaction object: a payload and nothing else, with every other
        -- axis left at its default. Modelled rather than merely described, because
        -- the chapter's headline is a comparison of two numbers.
        procedure sample_naive(d : std_logic_vector);
        impure function hits(i : natural) return natural;
        impure function filled return natural;
        impure function count return natural;
    end protected;

end package;

package body spi_cov_pkg is

    type coverage_t is protected body
        type bin_arr is array (0 to NBINS - 1) of natural;
        variable b : bin_arr := (others => 0);
        variable n : natural := 0;

        procedure clear is
        begin
            b := (others => 0);
            n := 0;
        end procedure;

        procedure bump(i : natural) is
        begin
            b(i) := b(i) + 1;
        end procedure;

        procedure sample(t : spi_txn_t; lead_min : natural;
                         half_min : natural; gap_min : natural) is
        begin
            n := n + 1;

            -- the relation that matters most: do the two devices AGREE?
            if t.cpol = t.m_cpol and t.cpha = t.m_cpha then bump(B_MODE_AGREE); end if;
            if t.cpol /= t.m_cpol then bump(B_MODE_POL); end if;
            if t.cpha /= t.m_cpha then bump(B_MODE_PHA); end if;

            -- each timing field against ITS OWN minimum, with `exactly at` as its own
            -- bin -- the one a generator that always adds margin never reaches
            if to_integer(t.lead) <  lead_min then bump(B_LEAD_LO); end if;
            if to_integer(t.lead) =  lead_min then bump(B_LEAD_AT); end if;
            if to_integer(t.lead) >  lead_min then bump(B_LEAD_HI); end if;

            if to_integer(t.half) <  half_min then bump(B_HALF_LO); end if;
            if to_integer(t.half) =  half_min then bump(B_HALF_AT); end if;
            if to_integer(t.half) >  half_min then bump(B_HALF_HI); end if;

            if to_integer(t.gap)  <  gap_min  then bump(B_GAP_LO);  end if;
            if to_integer(t.gap)  =  gap_min  then bump(B_GAP_AT);  end if;
            if to_integer(t.gap)  >  gap_min  then bump(B_GAP_HI);  end if;

            if t.nbits = 0 then bump(B_WHOLE); else bump(B_PARTIAL); end if;

            if    to_integer(t.len) = 1  then bump(B_W1);
            elsif to_integer(t.len) < 8  then bump(B_WNARROW);
            elsif to_integer(t.len) = 8  then bump(B_W8);
            elsif to_integer(t.len) < 32 then bump(B_WWIDE);
            else                              bump(B_WFULL);
            end if;

            -- the only thing about the payload that matters
            if has_transition(t.data, to_integer(t.len)) then bump(B_DATA_VARY);
            else                                             bump(B_DATA_FLAT);
            end if;

            if t.lsb_first = '0' then bump(B_MSB); end if;
        end procedure;

        procedure sample_naive(d : std_logic_vector) is
        begin
            n := n + 1;
            -- Everything below is the DEFAULT, which is the whole point.
            bump(B_MODE_AGREE);                 -- modes always agree
            bump(B_LEAD_HI);                    -- the driver's generous defaults
            bump(B_HALF_HI);
            bump(B_GAP_HI);
            bump(B_WHOLE);
            bump(B_W8);                         -- 8-bit frames only
            bump(B_MSB);                        -- MSB first only
            if has_transition(d, 8) then bump(B_DATA_VARY);
            else                         bump(B_DATA_FLAT);
            end if;
        end procedure;

        impure function hits(i : natural) return natural is
        begin return b(i); end function;

        impure function filled return natural is
            variable c : natural := 0;
        begin
            for i in 0 to NBINS - 1 loop
                if b(i) /= 0 then c := c + 1; end if;
            end loop;
            return c;
        end function;

        impure function count return natural is
        begin return n; end function;
    end protected body;

end package body;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_txn_pkg.all;
use work.spi_cov_pkg.all;

entity spi_txn_gen_tb is
end entity;

architecture sim of spi_txn_gen_tb is

    constant LEAD_MIN : natural := 4;
    constant HALF_MIN : natural := 3;
    constant GAP_MIN  : natural := 3;

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

    signal req       : std_logic := '0';
    signal kind      : unsigned(1 downto 0) := "00";
    signal seed_in   : unsigned(31 downto 0) := to_unsigned(1, 32);
    signal seed_load : std_logic := '0';

    signal txn       : spi_txn_t;
    signal txn_valid : std_logic;

    shared variable cov : coverage_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_txn_gen
        generic map (LEAD_MIN => LEAD_MIN, HALF_MIN => HALF_MIN, GAP_MIN => GAP_MIN)
        port map (clk => clk, rst_n => rst_n,
                  req => req, kind => kind,
                  seed_in => seed_in, seed_load => seed_load,
                  txn => txn, txn_valid => txn_valid);

    watchdog : process
    begin
        wait for 2 ms;
        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 naive_filled, legal_filled, both_filled : natural;
        variable nst : unsigned(31 downto 0);
        variable saved : natural;

        procedure gen(k : natural; n : natural) is
        begin
            kind <= to_unsigned(k, 2);
            for i in 1 to n loop
                wait until falling_edge(clk);
                req <= '1';
                wait until falling_edge(clk);
                req <= '0';
                wait until rising_edge(clk);
                if txn_valid = '1' then
                    cov.sample(txn, LEAD_MIN, HALF_MIN, GAP_MIN);
                end if;
            end loop;
        end procedure;

        procedure load_seed(s : unsigned(31 downto 0)) is
        begin
            wait until falling_edge(clk);
            seed_in <= s; seed_load <= '1';
            wait until falling_edge(clk);
            seed_load <= '0';
        end procedure;
    begin
        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;
        load_seed(x"5EED1602");

        -- 1. THE NAIVE GENERATOR: 400 transactions of randomised payload.
        cov.clear;
        nst := x"5EED1602";
        for i in 1 to 400 loop
            nst := xorshift(nst);
            cov.sample_naive(std_logic_vector(nst));
        end loop;
        naive_filled := cov.filled;
        report "  NAIVE      " & integer'image(cov.count) &
               " transactions, " & integer'image(naive_filled) & " of " &
               integer'image(NBINS) & " bins reached";

        -- 2. THE STRUCTURED GENERATOR, legal only: the same 400.
        cov.clear;
        gen(0, 400);
        legal_filled := cov.filled;
        report "  STRUCTURED " & integer'image(cov.count) &
               " legal transactions, " & integer'image(legal_filled) & " of " &
               integer'image(NBINS) & " bins reached";

        -- 3. AND WITH THE ILLEGAL KINDS ASKED FOR DELIBERATELY.
        cov.clear;
        gen(0, 300);
        gen(1, 80);
        gen(2, 80);
        both_filled := cov.filled;
        report "  STRUCTURED " & integer'image(cov.count) &
               " transactions including illegal kinds, " &
               integer'image(both_filled) & " of " & integer'image(NBINS) &
               " bins reached";

        report "  modes agree " & integer'image(cov.hits(B_MODE_AGREE)) &
               "  polarity differs " & integer'image(cov.hits(B_MODE_POL)) &
               "  phase differs " & integer'image(cov.hits(B_MODE_PHA));
        report "  lead at min " & integer'image(cov.hits(B_LEAD_AT)) &
               "  half at min " & integer'image(cov.hits(B_HALF_AT)) &
               "  gap at min " & integer'image(cov.hits(B_GAP_AT));
        report "  partial frames " & integer'image(cov.hits(B_PARTIAL)) &
               "  width 1 " & integer'image(cov.hits(B_W1)) &
               "  width 32 " & integer'image(cov.hits(B_WFULL)) &
               "  flat payloads " & integer'image(cov.hits(B_DATA_FLAT));

        -- 1. THE NAIVE GENERATOR REACHES ALMOST NOTHING.
        if naive_filled > 10 then
            report "  FAIL: the naive generator reached " &
                   integer'image(naive_filled) &
                   " bins, which is more than a payload-only generator can";
            errs := errs + 1;
        end if;
        report "  the naive generator reached " & integer'image(naive_filled) &
               " of " & integer'image(NBINS) &
               " bins with 400 transactions. It cannot reach any more, at any transaction count, because the axes it leaves at their defaults are not axes it varies";

        -- 2. AND THE STRUCTURED ONE REACHES MORE WITH THE SAME COUNT.
        if legal_filled <= naive_filled then
            report "  FAIL: the structured generator reached " &
                   integer'image(legal_filled) & " against the naive generator's " &
                   integer'image(naive_filled) & ", at the same transaction count";
            errs := errs + 1;
        end if;
        report "  the structured generator reached " & integer'image(legal_filled) &
               " of " & integer'image(NBINS) &
               " with the SAME 400 transactions -- the difference is not the count, it is which fields exist";

        -- 3. THE BOUNDARY BINS ARE REACHED.
        if cov.hits(B_LEAD_AT) = 0 or cov.hits(B_HALF_AT) = 0
           or cov.hits(B_GAP_AT) = 0 then
            report "  FAIL: a boundary bin was never reached -- every one of Module 14's requirements was pinned down AT its minimum, and a generator that always adds margin tests none of them";
            errs := errs + 1;
        end if;

        -- 4. THE ILLEGAL BINS ARE ONLY REACHED WHEN ASKED FOR.
        cov.clear;
        gen(0, 200);
        if cov.hits(B_LEAD_LO) /= 0 or cov.hits(B_HALF_LO) /= 0
           or cov.hits(B_GAP_LO) /= 0 or cov.hits(B_MODE_POL) /= 0
           or cov.hits(B_MODE_PHA) /= 0 or cov.hits(B_PARTIAL) /= 0 then
            report "  FAIL: a legal-only run reached an illegal bin, so 'legal' does not mean anything and Chapter 16.1's rule monitor would fire during a clean regression";
            errs := errs + 1;
        end if;
        report "  200 legal transactions reached no illegal bin at all, so a clean regression stays clean -- an illegal transaction is a REQUEST here, not an accident";

        -- 5. AND THE MODES AGREE BY DEFAULT.
        if cov.hits(B_MODE_AGREE) /= cov.count then
            report "  FAIL: some legal transactions had disagreeing modes; two independently random mode bits disagree three quarters of the time";
            errs := errs + 1;
        end if;
        report "  and in all " & integer'image(cov.count) &
               " legal transactions the master's mode AGREED with the slave's, because it is DERIVED from the slave's rather than independently random";

        -- 6. THE SEED IS REPRODUCIBLE.
        cov.clear; load_seed(x"5EED1602"); gen(0, 50);
        saved := cov.hits(B_DATA_VARY);
        cov.clear; load_seed(x"5EED1602"); gen(0, 50);
        if cov.hits(B_DATA_VARY) /= saved then
            report "  FAIL: the same seed produced different stimulus";
            errs := errs + 1;
        end if;
        report "  the same seed produces the same stimulus twice, which is what makes these counts comparable across the three languages rather than three unrelated numbers";

        -- 7. AND A ZERO SEED IS REFUSED.
        cov.clear; load_seed(x"00000000"); gen(0, 50);
        if cov.hits(B_DATA_VARY) = 0 and cov.hits(B_DATA_FLAT) = cov.count then
            report "  FAIL: a zero seed produced a constant stream, which looks like a suite that ran rather than one that stopped";
            errs := errs + 1;
        end if;
        report "  a zero seed is refused and replaced, because zero is a fixed point of xorshift and a constant stream looks like a suite that ran rather than one that stopped";

        if errs = 0 then
            report "PASS: the stimulus space is not the data space, and the measurement is two generators at the SAME transaction count. A payload-only transaction object -- which is what an object without timing fields is -- reached " &
                   integer'image(naive_filled) & " of " & integer'image(NBINS) &
                   " bins with four hundred transactions, and cannot reach more at any count, because the axes it leaves at their defaults are not axes it varies. The structured object reached " &
                   integer'image(legal_filled) & " with the same four hundred, and " &
                   integer'image(both_filled) &
                   " once illegal kinds were asked for. The bins that make the difference are all RELATIONS rather than values: whether the two devices' modes agree, whether each timing field is below, exactly at, or above its own minimum, whether the bit count is a whole number of frames, and whether the payload contains any transition at all -- there is no bin over `data`, which has four billion values of which exactly one property matters. Three of Module 14's four published requirements are timing numbers, so a transaction object without timing fields cannot express a legal transaction let alone an illegal one. The boundary bins are reached deliberately, because every one of those requirements was pinned down AT its minimum and a generator that always adds margin tests none of them. An illegal transaction is a REQUEST rather than an accident, so a legal-only run reached no illegal bin and a clean regression stays clean. The master's mode is DERIVED from the slave's rather than independently random, because two independent mode bits disagree three quarters of the time. And the generator is seeded and reproducible, which is what makes these counts one measurement in three languages rather than three unrelated numbers";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;

        halt <= true;
        wait;
    end process;

end architecture;

7. Why a Verification Engineer Cares

Because the coverage hole you cannot close by running longer is the one that costs a schedule, and its signature is visible on day one if you look for it.

The practical habit this chapter argues for: before tuning constraints, check that the field exists. A bin that no field can reach is a specification problem in the transaction object, and it is cheap to fix in week one and expensive in week twenty, because by then the driver, the monitor and half the sequences have been written against an object with the wrong shape.

The second habit is about distributions rather than fields. Three of the twenty-two bins are exactly at the minimum, and those are where the requirements from Module 14 actually live — a lead of 4 when the minimum is 4, not a lead of 11. Uniform ranges under-sample boundaries by construction, and the wider the legal range, the worse it gets. A generator that does not deliberately weight its boundaries has a coverage report that looks healthy and a boundary story that is decoration.

8. Why an FPGA or ASIC Engineer Cares

Because the timing fields in this object are the numbers in your datasheet, and a generator that does not carry them is a generator that has never tested them.

A slave characterised only by suites whose lead was comfortable will meet a master that satisfies your published minimum exactly, and the difference between "comfortable" and "exactly at the minimum" is a bug report from a customer. The exactly at min bins are the ones that turn a published number into a verified one.

The width axis is the second practical item. Widths of 1 and 32 are where off-by-one arithmetic lives — Module 14 found degenerate cases at both ends more than once — and a generator whose width distribution is uniform over 1 to 32 hits each extreme about 3% of the time. Weighting them is not gaming the coverage number; it is sampling where the bugs are.

9. Failure Signature — A Regression That Got Longer And Not Better

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom          coverage sits at 74% for three weeks. The regression is
                    extended from 2,000 to 20,000 transactions. Coverage moves
                    to 74.2%.

   What happened    the unreached bins were on axes the transaction object had
                    no fields for. Ten times the transactions varied ten times
                    as much of the same four fields.

   What would have  comparing the coverage model's axes against the transaction
   caught it        object's field list, on day one. Every axis needs a field
                    that can reach every one of its bins; an axis without one
                    is unreachable at any run length.

   The tell         the unreached bins cluster. Six of them on three axes, all
                    of which correspond to fields nobody randomises, is a
                    structural hole. Coverage holes from genuinely unlucky
                    randomisation are scattered and they move between seeds --
                    which is the cheap diagnostic: change the seed. A hole that
                    does not move is a hole that is not about luck.

10. Common Misconceptions

"More transactions will close the hole." Only for bins a field can reach. The naive generator in this chapter reaches 9 of 22 and will reach 9 of 22 forever, because the other thirteen are on axes it has no fields for. Change the seed: a hole that does not move between seeds is structural.

"Randomising everything gives the best coverage." It gives the worst distribution. Randomising the master's mode independently of the slave's makes the mismatch case 75% of the suite, so three quarters of the run tests a fault and the mode-agreement bin — used by every real transfer — becomes the rare one. Fields that should be derived must be derived.

"A wide uniform range covers the boundary." It covers the boundary at a rate of one over the range width. With a legal range of 0 to 1023 and a minimum of 64, the exactly at min case appears once in a thousand transactions, and the boundary is where the requirement lives. Boundaries need deliberate weight.

"Illegal stimulus makes a regression noisy." Only if illegality is an accident. Here it is a requested kind, and the measurement confirms that 200 legal transactions reached no illegal bin at all. A generator that produces illegal traffic only on request can drive every checker into every violation on purpose and still let a clean regression mean something.

"A coverage report at 95% means the stimulus is nearly good enough." It means 95% of the bins somebody thought to define were reached. The twenty-two bins here exist because seven earlier chapters established why those axes matter; a coverage model that does not trace its axes to requirements is measuring its own ambition.

11. Reason It Through

The naive generator reaches 9 of 22 bins. Predict which 9 without reading the table.

The bins on axes it carries fields for: mode agreement (agree only — it has cpol/cpha but no separate master mode, so a mismatch is unreachable), width (1, 8, 32 and other — four bins), data transitions (with and without a change — two bins), frame wholeness (whole only), and the above minimum bins of the timing axes if its fixed timing happens to be above them. That is roughly nine, and the shape of the answer is the point: every reachable bin corresponds to a field.

Why is lead exactly at LEAD_MIN a separate bin from lead above LEAD_MIN?

Because the requirement is lead >= LEAD_MIN, and the only value that distinguishes a correct implementation of >= from an incorrect implementation of > is LEAD_MIN itself. Every other value in the above bin passes under both.

A coverage hole moves when you change the seed. What does that tell you, and what if it does not move?

If it moves, the field exists and the distribution is unlucky — tune the constraint or run longer. If it does not move across several seeds, the bin is unreachable with the current object, and no amount of running will help. This is the cheapest diagnostic in constrained-random verification and it takes one extra run.

The generator refuses a zero seed. What would the run have looked like if it did not?

Complete, green, and full-length. Every field identical in every transaction, a coverage report with one bin per axis, and no error of any kind — because a generator emitting a constant is indistinguishable from a working generator by every check except coverage. It is worse than a crash for exactly that reason.

12. Understanding Check

13. Summary

A transaction object is a claim about which parts of the stimulus space a suite can reach, and the claim is testable. A payload-only object — data, width, mode — reached 9 of 22 coverage bins; the structured object reached 16 with the same 400 transactions, and 22 once illegal kinds were requested. The remaining difference was never about run length: an axis with no field behind it is unreachable at any transaction count, which is why a coverage hole that does not move between seeds is a hole in the object rather than in the constraints. The master's mode is derived from the slave's so that the common case stays common; the boundary values are deliberately weighted so that exactly at the minimum is reached seventy-odd times rather than never; illegal traffic is requested rather than stumbled into; and a zero seed is refused, because a generator emitting a constant passes every check a suite has except the one that counts bins.

14. What Comes Next

The transaction exists and the rules exist. Between them sits the question of when the environment looks at a pin, which turns out to be the difference between a monitor that works and one that reports perfect waveforms and zero exercises. Chapter 16.3 builds the sampling discipline and measures the race it prevents.

Continue learning