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.
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'sEvery 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
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 modeTwelve 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
4. The Coverage Model
Twenty-two bins across seven axes, and every axis traces to a chapter that established why it matters.
| Axis | Bins | From |
|---|---|---|
| mode agreement | agree, polarity differs, phase differs | 14.6 |
| lead vs LEAD_MIN | below, exactly at, above | 14.4 |
| half vs HALF_MIN | below, exactly at, above | 15.3 |
| gap vs GAP_MIN | below, exactly at, above | 14.5 |
| frame wholeness | whole, partial | 14.7 |
| width class | 1, 8, 32 and the rest | 14.3 |
| data transitions | payload with no change, payload with a change | 14.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
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 reachedThree 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.
// 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// 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-- 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
// 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// 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-- 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
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
Related tutorials
- Related topic
Extracting Protocol Rules and the Verification Plan
Eight pin-observable SPI rules, each with a checker and an exercised counter, because a checker alone cannot tell never-broken from never-reached. Legal traffic violates nothing and exercises all eight; eight injected faults produce a diagonal violation matrix; and one plan row is proved to have no checker at all.
- Related topic
Constrained-Random Sequences
A constraint set is a specification in a solver's language, and it fails in two directions a coverage report cannot tell apart. A weighted set closes at draw 42 where a uniform one needs 2120; an over-constrained set stalls silently; and an inconsistent one must report rather than emit.
- Related topic
Verification Closure
A plan row can be in four states and a percentage collapses them to one. Eighteen rows aggregated: fifteen closed, three with no checker and never going to have one, and leaving one check unexercised drops the number, which is what makes the report un-gameable.
- Related topic
Packet Generation
A weight is a per-frame marginal, so it reaches a frame's own properties and nothing else — and 48 of the parser's 64 alignment offsets are unreachable from the transmit side at any weight.
