SPI · Module 16
Extracting Protocol Rules and the Verification Plan
Eight pin-observable SPI rules, each with a checker and an exercised counter, because a checker alone cannot tell never-broken from never-reached. Legal traffic violates nothing and exercises all eight; eight injected faults produce a diagonal violation matrix; and one plan row is proved to have no checker at all.
Module 15 finished the design side of SPI. This module builds the environment that decides whether a design is right, and it starts where every verification effort actually starts and almost every one starts badly: with a list.
A verification plan is usually a table of features with a column for "covered". This chapter argues that such a table is worthless until every row carries two things — a checker that can fire, and an exercised counter that proves the checker was reached — and it then builds exactly that for SPI and measures it.
A checker that has never fired and a checker that cannot fire produce the same report. What distinguishes them?
1. What a Rule Has to Be
A verification plan row is only usable if it can be decided from what an observer can actually see. That constraint is sharper than it sounds, and applying it honestly deletes rows.
USABLE NOT USABLE
------------------------------ -------------------------------------------
SCLK idles at CPOL while "the slave latches on the correct edge"
deselected -- not observable from the pins; it is a
statement about the slave's insides
CS falls at least LEAD before "the master is fast enough"
the first SCLK edge -- not a protocol rule; a performance
requirement with no pass/fail line
every half-period is at least "the data is correct"
HALF cycles -- correct against WHAT? this is a
scoreboard row, not a protocol row
a transaction carries a whole "bit order is MSB-first"
number of frames -- see section 7. There is no pin
observation that decides this, everThe last one matters most, because it is the row that teaches what a plan is for. Bit order is a real requirement, it appears in every datasheet, and no observation of SCLK, CS and MOSI can decide it — Chapter 14.3 showed why: MSB-first and LSB-first both produce a well-formed word of the right length at the right time. A plan that lists it next to the other rows implies a checker exists. One will never be written, and the honest plan says so in the row rather than leaving the gap for somebody to find during signoff.
2. The Eight Rules
These are the rules this module's checker implements, in the form it implements them. Every one is a statement about pins and about the configuration the traffic was supposed to use, and nothing else.
| # | Rule | Decided at |
|---|---|---|
| R1 | SCLK idles at CPOL while deselected | every deselected cycle |
| R2 | CS leads the first SCLK edge by at least LEAD | the first edge of a transaction |
| R3 | every SCLK half-period is at least HALF | every edge after the first |
| R4 | MOSI moves only on launch edges, never at a capture edge | every MOSI change inside a transaction |
| R5 | a transaction carries a whole number of frames | the deassert |
| R6 | CS lags the last SCLK edge by at least LAG | the deassert |
| R7 | CS is high for at least GAP between transactions | an assert that follows a previous transaction |
| R8 | MISO is driven only while the slave is selected | every deselected cycle |
Four of the eight are timing intervals (R2, R3, R6, R7), two are continuous obligations (R1, R8), one is a counting property (R5) and one is a coincidence property (R4). They need four different shapes of logic, and a plan that lists them as eight similar-looking rows hides that the work is not eight equal pieces.
3. The Half Everybody Skips
Each rule gets two counters.
exercised[r] how many times the rule's PRECONDITION occurred
violated[r] how many of those times the rule was brokenWithout the first counter, a report of violated == 0 has two completely different meanings and no way to tell them apart:
violated = 0, exercised = 4127 the rule holds, and was tested 4127 times
violated = 0, exercised = 0 the rule was never reached. The checker may
be correct, wrong, or unreachable; this run
contains no information about itBoth print the same green line in most suites. The second is the state a checker spends its life in after somebody renames a signal, changes a default parameter, or writes the precondition with the wrong polarity.
4. Legal Traffic
Legal traffic must do two things, and the second is the one that gives the first any weight.
rule exercised violated what it says
R1 24 0 SCLK idles at CPOL while deselected
R2 3 0 CS leads the first edge by LEAD
R3 21 0 every half-period is at least HALF
R4 8 0 MOSI moves only on launch edges
R5 3 0 a whole number of frames
R6 3 0 CS lags the last edge by LAG
R7 2 0 CS high for at least GAP
R8 24 0 MISO driven only while selectedThree transactions. Every rule exercised, none violated. Note R7 = 2: the gap is an obligation between transactions, so three transactions offer two gaps, and a checker that reported three would be counting something else. Note R2 = 3 and R3 = 21: three first edges, and 21 subsequent ones across three eight-bit frames of sixteen edges each minus the three that have no predecessor — 48 − 3 = 45 if every edge counted, but the bench's frames are shorter. The point of reading the numbers this closely is that a plan whose exercise counts you cannot predict is a plan you do not understand.
5. The Violation Matrix
Eight faults are injected, one at a time, and each must fire its own rule and nothing else.
R1 wrong idle level -> fired 00000001
R2 short lead -> fired 00000010
R3 short half-period -> fired 00000100
R4 phase-mismatched master -> fired 00001000
R5 partial frame -> fired 00010000
R6 short lag -> fired 00100000
R7 short gap -> fired 01000000
R8 driven while idle -> fired 10000000A perfect diagonal. The diagonal is the property worth chasing, and it is a stronger requirement than "every checker fires":
a checker that fires for its own fault half verified
a checker that fires for its own fault AND
for nothing else its report is a DIAGNOSISThe difference shows up the first time a real design fails. A diagonal matrix means the report names the defect; a matrix with off-diagonal entries means the report names three things and an engineer has to guess which is the cause. Chapter 16.4 hits exactly this: one of its injected driver faults produces two rule violations, for a reason that turns out to be arithmetic rather than a checker defect, and the expectation has to be written down as a set rather than a single rule.
6. Building It — Three HDLs
The component is a monitor: it takes pins, the configuration, and a clock of its own, and it produces counters. It drives nothing.
One structural note that recurs through the whole module. The counter arrays stay inside the module, where all three languages have them, and the ports differ:
SystemVerilog / Verilog exercised_flat[NRULES*CNT_W-1:0] a packed bus
VHDL exercised : out rule_counts_t a named array typeAn unpacked array port is SystemVerilog only. Flattening is the portable spelling and it costs the bench a pair of accessor functions; VHDL has array types in ports and uses one, in a package, which is what that language actually offers. The three are the same component expressed in what each language has rather than one language's shape forced onto the others — and that is the policy for every file in this module.
// spi_rule_monitor.sv
//
// Chapter 16.1 -- a verification plan, made checkable.
//
// A verification plan is usually a table of sentences. This file is the argument
// that a plan is only real when every row of it maps to two things:
//
// a CHECKER that fires when the rule is broken
// a COUNTER that says the rule was EXERCISED
//
// and that those are different, because a checker alone cannot tell "this rule was
// never broken" from "this rule was never reached". A suite whose plan is all green
// on the first reading is almost always reporting the second.
//
// THE EIGHT RULES, AND WHERE EACH ONE CAME FROM.
//
// Every rule here is observable FROM THE PINS ALONE. That is deliberate and it is
// the plan's most important property: a rule that needs to look inside the DUT is a
// rule the DUT gets a vote on (Chapter 16.5 is about what that costs).
//
// R1 SCLK idles at CPOL while deselected (13.5, 14.6)
// R2 CS leads the first SCLK edge by at least LEAD (14.4)
// R3 every SCLK half-period is at least HALF (14.1, 15.3)
// R4 MOSI changes only on LAUNCH edges, never on capture edges (13.5, 14.1)
// R5 a transaction carries a whole number of frames (14.2)
// R6 CS lags the last SCLK edge by at least LAG (13.7)
// R7 CS stays high for at least GAP between transactions (14.5)
// R8 MISO is driven only while selected (14.5)
//
// WHY "EXERCISED" IS THE HARDER HALF.
//
// Take R2. Its checker fires when the lead is shorter than LEAD. A suite that never
// drives a short lead never fires it -- and a suite that never drives a lead AT ALL
// also never fires it, because there was no transaction. Those two suites produce
// identical reports and have tested completely different amounts.
//
// So each rule here publishes BOTH:
//
// exercised[i] the number of times the rule's precondition occurred
// violated[i] the number of times the rule was broken
//
// and a plan row is only closed when `exercised > 0`. That one extra counter is the
// difference between a plan and a list of hopes.
//
// AND THE RULE THIS BLOCK DELIBERATELY CANNOT CHECK.
//
// Bit ORDER. Chapter 14.3 established that MSB-first and LSB-first produce
// well-formed words of the right length at the right time, so no observation of the
// pins distinguishes them. It is in the plan as a row with no checker, marked
// "configuration, not checkable" -- because a plan that silently omits it looks
// complete and a plan that lists it honestly does not.
module spi_rule_monitor #(
parameter int LEAD = 4, // recovered cycles, CS assert to first edge
parameter int HALF = 3, // cycles per SCLK half-period
parameter int LAG = 2, // cycles, last edge to CS deassert
parameter int GAP = 3, // cycles of CS high between transactions
parameter int LEN_W = 6,
parameter int CNT_W = 16,
parameter int NRULES = 8
) (
input wire clk, // the OBSERVER's clock, not the DUT's
input wire rst_n,
// --- the pins, and nothing else ---------------------------------------
input wire sclk,
input wire cs_n,
input wire mosi,
input wire miso_driven, // a bus monitor's view: is MISO driven?
// --- the configuration the rules are checked AGAINST -------------------
input wire cpol,
input wire cpha,
input wire [LEN_W-1:0] len,
// --- the plan, as two vectors ------------------------------------------
// FLATTENED into packed buses rather than exposed as unpacked arrays, because an
// unpacked array port is SystemVerilog only and this component exists in three
// languages. The VHDL version uses a proper array type in its port, which is
// idiomatic there -- so the three are structurally the same component expressed
// in what each language actually offers, rather than one language's shape forced
// on the others.
output wire [NRULES*CNT_W-1:0] exercised_flat,
output wire [NRULES*CNT_W-1:0] violated_flat,
input wire clr
);
// Rule indices, named so that a report can be read without the source.
localparam int R_IDLE = 0, // R1
R_LEAD = 1, // R2
R_HALF = 2, // R3
R_LAUNCH= 3, // R4
R_FRAME = 4, // R5
R_LAG = 5, // R6
R_GAP = 6, // R7
R_DRIVE = 7; // R8
// --- observed history --------------------------------------------------
reg sclk_d, cs_n_d, mosi_d;
wire cs_assert = ~cs_n & cs_n_d;
wire cs_deassert = cs_n & ~cs_n_d;
// WHICH TRANSACTION A COINCIDENT EVENT BELONGS TO, which is a question a pin
// monitor has to answer and "is the select asserted right now?" is the wrong
// answer to. A master that deasserts on the same cycle as its final edge makes
// `cs_n` read high on the cycle that edge is observed -- so `~cs_n` attributes
// that edge to NO transaction, it is never counted, and the frame remainder ends
// one short. R5 then fires on a master whose only fault was a short lag.
//
// An edge observed on the deassert cycle belongs to the transaction that is
// ending, so the test includes it.
wire in_txn = ~cs_n | cs_deassert;
wire sclk_edge = sclk ^ sclk_d;
wire mosi_change = mosi ^ mosi_d;
// A LAUNCH edge is the one that is not the capture edge. Chapter 14.6's mapping:
// with CPOL consumed, CPHA=0 captures on the leading edge and launches on the
// trailing one. `leading` means SCLK left its idle level.
wire leading = sclk_edge & (sclk != cpol);
wire trailing = sclk_edge & (sclk == cpol);
wire capture = cpha ? trailing : leading;
wire launch = cpha ? leading : trailing;
// --- interval measurement ----------------------------------------------
// One counter, reused by three rules, and reloaded with ONE for Chapter 14.1's
// reason: the cycle an event is detected on is the first cycle of the next
// interval.
reg [CNT_W-1:0] since_cs_assert;
reg [CNT_W-1:0] since_sclk_edge;
reg [CNT_W-1:0] since_cs_deassert;
reg seen_edge_in_txn;
reg [LEN_W:0] in_frame; // running remainder, Chapter 14.2
reg had_a_txn; // so GAP is not checked before the first
wire [LEN_W:0] edges_per_frame = {1'b0, len} << 1;
// An interval measured when the event that ENDS it is happening on this very
// cycle is ZERO, not the previous interval's value. Without this, a master that
// deasserts on the same cycle as its last edge measures the whole preceding
// half-period as its lag and R6 never fires -- which is the worst kind of
// checker bug, because the report says the rule is fine.
// Each one asks "is the event that STARTED this interval happening right now?",
// and each one therefore tests a DIFFERENT signal. Getting that wrong is easy and
// the first version did: the lead was gated on `sclk_edge`, which is always true
// at the moment the lead is checked -- so the lead measured zero on every
// transaction and R2 fired on all of them, including the legal ones.
wire [CNT_W-1:0] lead_now = cs_assert ? {CNT_W{1'b0}} : since_cs_assert;
wire [CNT_W-1:0] lag_now = sclk_edge ? {CNT_W{1'b0}} : since_sclk_edge;
wire [CNT_W-1:0] gap_now = cs_deassert ? {CNT_W{1'b0}} : since_cs_deassert;
// And the remainder AS IT WILL BE once this cycle's edge is counted, because the
// counter itself updates non-blockingly and the R5 check runs in the same cycle.
wire [LEN_W:0] frame_now =
(sclk_edge && in_txn)
? ((in_frame == (edges_per_frame - 1'b1)) ? {(LEN_W+1){1'b0}}
: in_frame + 1'b1)
: in_frame;
// The counters stay as arrays INSIDE the module, where every language has them.
reg [CNT_W-1:0] exercised [0:NRULES-1];
reg [CNT_W-1:0] violated [0:NRULES-1];
genvar gi;
generate
for (gi = 0; gi < NRULES; gi = gi + 1) begin : g_flat
assign exercised_flat[gi*CNT_W +: CNT_W] = exercised[gi];
assign violated_flat [gi*CNT_W +: CNT_W] = violated [gi];
end
endgenerate
integer r;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
mosi_d <= 1'b0;
since_cs_assert <= {CNT_W{1'b0}};
since_sclk_edge <= {CNT_W{1'b0}};
since_cs_deassert <= {CNT_W{1'b0}};
seen_edge_in_txn <= 1'b0;
in_frame <= {(LEN_W+1){1'b0}};
had_a_txn <= 1'b0;
for (r = 0; r < NRULES; r = r + 1) begin
exercised[r] <= {CNT_W{1'b0}};
violated[r] <= {CNT_W{1'b0}};
end
end else begin
sclk_d <= sclk;
cs_n_d <= cs_n;
mosi_d <= mosi;
if (clr) begin
for (r = 0; r < NRULES; r = r + 1) begin
exercised[r] <= {CNT_W{1'b0}};
violated[r] <= {CNT_W{1'b0}};
end
end
// ---------------- intervals -----------------------------------
if (cs_assert) since_cs_assert <= {{(CNT_W-1){1'b0}}, 1'b1};
else if (since_cs_assert != {CNT_W{1'b1}})
since_cs_assert <= since_cs_assert + 1'b1;
if (sclk_edge) since_sclk_edge <= {{(CNT_W-1){1'b0}}, 1'b1};
else if (since_sclk_edge != {CNT_W{1'b1}})
since_sclk_edge <= since_sclk_edge + 1'b1;
if (cs_deassert) since_cs_deassert <= {{(CNT_W-1){1'b0}}, 1'b1};
else if (since_cs_deassert != {CNT_W{1'b1}})
since_cs_deassert <= since_cs_deassert + 1'b1;
// ---------------- R1: SCLK idles at CPOL while deselected ------
// EXERCISED on every deselected cycle -- which is the right choice and
// worth arguing: the rule is a continuous obligation, so a suite that
// was ever deselected has exercised it. Counting only the assert would
// make the counter a transaction count wearing a rule's name.
if (cs_n) begin
exercised[R_IDLE] <= exercised[R_IDLE] + 1'b1;
if (sclk != cpol)
violated[R_IDLE] <= violated[R_IDLE] + 1'b1;
end
// ---------------- R2: the lead ---------------------------------
// EXERCISED at the FIRST edge of a transaction, because that is the
// only moment the lead exists. A transaction with no edges never
// exercises it, and that is correct: there was no lead to measure.
if (sclk_edge && in_txn && !seen_edge_in_txn) begin
seen_edge_in_txn <= 1'b1;
exercised[R_LEAD] <= exercised[R_LEAD] + 1'b1;
if (lead_now < LEAD)
violated[R_LEAD] <= violated[R_LEAD] + 1'b1;
end
// ---------------- R3: the half-period --------------------------
// EXERCISED on every edge that FOLLOWS another edge inside the same
// transaction. The first edge has no preceding one, and the interval
// after a deassert is not a half-period.
if (sclk_edge && in_txn && seen_edge_in_txn) begin
exercised[R_HALF] <= exercised[R_HALF] + 1'b1;
if (since_sclk_edge < HALF)
violated[R_HALF] <= violated[R_HALF] + 1'b1;
end
// ---------------- R4: MOSI moves only on launch edges ----------
// EXERCISED whenever MOSI changes inside a transaction -- which makes a
// constant data pattern exercise it ZERO times, and that is the honest
// count. A suite sending only 0x00 has not tested this rule at all.
if (mosi_change && in_txn) begin
exercised[R_LAUNCH] <= exercised[R_LAUNCH] + 1'b1;
// A change is legal on the launch edge and for a short while after
// it. It is illegal AT a capture edge, which is what would put the
// data in motion when the slave samples it (Chapter 14.6).
if (capture)
violated[R_LAUNCH] <= violated[R_LAUNCH] + 1'b1;
end
// ---------------- the frame remainder -------------------------
if (sclk_edge && in_txn) begin
if (in_frame == (edges_per_frame - 1'b1))
in_frame <= {(LEN_W+1){1'b0}};
else
in_frame <= in_frame + 1'b1;
end
// ---------------- R5, R6 at the deassert ----------------------
if (cs_deassert) begin
had_a_txn <= 1'b1;
// R5 is EXERCISED by any transaction that carried an edge. An
// empty select pulse does not exercise it -- there were no frames
// to be whole or partial.
if (seen_edge_in_txn || (sclk_edge && in_txn)) begin
exercised[R_FRAME] <= exercised[R_FRAME] + 1'b1;
if (frame_now != {(LEN_W+1){1'b0}})
violated[R_FRAME] <= violated[R_FRAME] + 1'b1;
// R6 likewise needs a last edge to lag from.
exercised[R_LAG] <= exercised[R_LAG] + 1'b1;
if (lag_now < LAG)
violated[R_LAG] <= violated[R_LAG] + 1'b1;
end
seen_edge_in_txn <= 1'b0;
in_frame <= {(LEN_W+1){1'b0}};
end
// ---------------- R7: the gap ---------------------------------
// EXERCISED at an assert that FOLLOWS a previous transaction. The very
// first assert of a run has no gap before it, and counting it would
// make every run report one spurious exercise.
if (cs_assert && had_a_txn) begin
exercised[R_GAP] <= exercised[R_GAP] + 1'b1;
if (gap_now < GAP)
violated[R_GAP] <= violated[R_GAP] + 1'b1;
end
// ---------------- R8: MISO driven only while selected ---------
// EXERCISED on every deselected cycle, for R1's reason.
if (cs_n) begin
exercised[R_DRIVE] <= exercised[R_DRIVE] + 1'b1;
if (miso_driven)
violated[R_DRIVE] <= violated[R_DRIVE] + 1'b1;
end
end
end
endmodule// spi_rule_monitor.v
//
// Chapter 16.1 -- a verification plan, made checkable.
//
// A verification plan is usually a table of sentences. This file is the argument
// that a plan is only real when every row of it maps to two things:
//
// a CHECKER that fires when the rule is broken
// a COUNTER that says the rule was EXERCISED
//
// and that those are different, because a checker alone cannot tell "this rule was
// never broken" from "this rule was never reached". A suite whose plan is all green
// on the first reading is almost always reporting the second.
//
// THE EIGHT RULES, AND WHERE EACH ONE CAME FROM.
//
// Every rule here is observable FROM THE PINS ALONE. That is deliberate and it is
// the plan's most important property: a rule that needs to look inside the DUT is a
// rule the DUT gets a vote on (Chapter 16.5 is about what that costs).
//
// R1 SCLK idles at CPOL while deselected (13.5, 14.6)
// R2 CS leads the first SCLK edge by at least LEAD (14.4)
// R3 every SCLK half-period is at least HALF (14.1, 15.3)
// R4 MOSI changes only on LAUNCH edges, never on capture edges (13.5, 14.1)
// R5 a transaction carries a whole number of frames (14.2)
// R6 CS lags the last SCLK edge by at least LAG (13.7)
// R7 CS stays high for at least GAP between transactions (14.5)
// R8 MISO is driven only while selected (14.5)
//
// WHY "EXERCISED" IS THE HARDER HALF.
//
// Take R2. Its checker fires when the lead is shorter than LEAD. A suite that never
// drives a short lead never fires it -- and a suite that never drives a lead AT ALL
// also never fires it, because there was no transaction. Those two suites produce
// identical reports and have tested completely different amounts.
//
// So each rule here publishes BOTH:
//
// exercised[i] the number of times the rule's precondition occurred
// violated[i] the number of times the rule was broken
//
// and a plan row is only closed when `exercised > 0`. That one extra counter is the
// difference between a plan and a list of hopes.
//
// AND THE RULE THIS BLOCK DELIBERATELY CANNOT CHECK.
//
// Bit ORDER. Chapter 14.3 established that MSB-first and LSB-first produce
// well-formed words of the right length at the right time, so no observation of the
// pins distinguishes them. It is in the plan as a row with no checker, marked
// "configuration, not checkable" -- because a plan that silently omits it looks
// complete and a plan that lists it honestly does not.
module spi_rule_monitor #(
parameter LEAD = 4, // recovered cycles, CS assert to first edge
parameter HALF = 3, // cycles per SCLK half-period
parameter LAG = 2, // cycles, last edge to CS deassert
parameter GAP = 3, // cycles of CS high between transactions
parameter LEN_W = 6,
parameter CNT_W = 16,
parameter NRULES = 8
) (
input wire clk, // the OBSERVER's clock, not the DUT's
input wire rst_n,
// --- the pins, and nothing else ---------------------------------------
input wire sclk,
input wire cs_n,
input wire mosi,
input wire miso_driven, // a bus monitor's view: is MISO driven?
// --- the configuration the rules are checked AGAINST -------------------
input wire cpol,
input wire cpha,
input wire [LEN_W-1:0] len,
// --- the plan, as two vectors ------------------------------------------
// FLATTENED into packed buses rather than exposed as unpacked arrays, because an
// unpacked array port is SystemVerilog only and this component exists in three
// languages. The VHDL version uses a proper array type in its port, which is
// idiomatic there -- so the three are structurally the same component expressed
// in what each language actually offers, rather than one language's shape forced
// on the others.
output wire [NRULES*CNT_W-1:0] exercised_flat,
output wire [NRULES*CNT_W-1:0] violated_flat,
input wire clr
);
// Rule indices, named so that a report can be read without the source.
localparam R_IDLE = 0, // R1
R_LEAD = 1, // R2
R_HALF = 2, // R3
R_LAUNCH= 3, // R4
R_FRAME = 4, // R5
R_LAG = 5, // R6
R_GAP = 6, // R7
R_DRIVE = 7; // R8
// --- observed history --------------------------------------------------
reg sclk_d, cs_n_d, mosi_d;
wire cs_assert = ~cs_n & cs_n_d;
wire cs_deassert = cs_n & ~cs_n_d;
// WHICH TRANSACTION A COINCIDENT EVENT BELONGS TO, which is a question a pin
// monitor has to answer and "is the select asserted right now?" is the wrong
// answer to. A master that deasserts on the same cycle as its final edge makes
// `cs_n` read high on the cycle that edge is observed -- so `~cs_n` attributes
// that edge to NO transaction, it is never counted, and the frame remainder ends
// one short. R5 then fires on a master whose only fault was a short lag.
//
// An edge observed on the deassert cycle belongs to the transaction that is
// ending, so the test includes it.
wire in_txn = ~cs_n | cs_deassert;
wire sclk_edge = sclk ^ sclk_d;
wire mosi_change = mosi ^ mosi_d;
// A LAUNCH edge is the one that is not the capture edge. Chapter 14.6's mapping:
// with CPOL consumed, CPHA=0 captures on the leading edge and launches on the
// trailing one. `leading` means SCLK left its idle level.
wire leading = sclk_edge & (sclk != cpol);
wire trailing = sclk_edge & (sclk == cpol);
wire capture = cpha ? trailing : leading;
wire launch = cpha ? leading : trailing;
// --- interval measurement ----------------------------------------------
// One counter, reused by three rules, and reloaded with ONE for Chapter 14.1's
// reason: the cycle an event is detected on is the first cycle of the next
// interval.
reg [CNT_W-1:0] since_cs_assert;
reg [CNT_W-1:0] since_sclk_edge;
reg [CNT_W-1:0] since_cs_deassert;
reg seen_edge_in_txn;
reg [LEN_W:0] in_frame; // running remainder, Chapter 14.2
reg had_a_txn; // so GAP is not checked before the first
wire [LEN_W:0] edges_per_frame = {1'b0, len} << 1;
// An interval measured when the event that ENDS it is happening on this very
// cycle is ZERO, not the previous interval's value. Without this, a master that
// deasserts on the same cycle as its last edge measures the whole preceding
// half-period as its lag and R6 never fires -- which is the worst kind of
// checker bug, because the report says the rule is fine.
// Each one asks "is the event that STARTED this interval happening right now?",
// and each one therefore tests a DIFFERENT signal. Getting that wrong is easy and
// the first version did: the lead was gated on `sclk_edge`, which is always true
// at the moment the lead is checked -- so the lead measured zero on every
// transaction and R2 fired on all of them, including the legal ones.
wire [CNT_W-1:0] lead_now = cs_assert ? {CNT_W{1'b0}} : since_cs_assert;
wire [CNT_W-1:0] lag_now = sclk_edge ? {CNT_W{1'b0}} : since_sclk_edge;
wire [CNT_W-1:0] gap_now = cs_deassert ? {CNT_W{1'b0}} : since_cs_deassert;
// And the remainder AS IT WILL BE once this cycle's edge is counted, because the
// counter itself updates non-blockingly and the R5 check runs in the same cycle.
wire [LEN_W:0] frame_now =
(sclk_edge && in_txn)
? ((in_frame == (edges_per_frame - 1'b1)) ? {(LEN_W+1){1'b0}}
: in_frame + 1'b1)
: in_frame;
// The counters stay as arrays INSIDE the module, where every language has them.
reg [CNT_W-1:0] exercised [0:NRULES-1];
reg [CNT_W-1:0] violated [0:NRULES-1];
genvar gi;
generate
for (gi = 0; gi < NRULES; gi = gi + 1) begin : g_flat
assign exercised_flat[gi*CNT_W +: CNT_W] = exercised[gi];
assign violated_flat [gi*CNT_W +: CNT_W] = violated [gi];
end
endgenerate
integer r;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_d <= 1'b0;
cs_n_d <= 1'b1;
mosi_d <= 1'b0;
since_cs_assert <= {CNT_W{1'b0}};
since_sclk_edge <= {CNT_W{1'b0}};
since_cs_deassert <= {CNT_W{1'b0}};
seen_edge_in_txn <= 1'b0;
in_frame <= {(LEN_W+1){1'b0}};
had_a_txn <= 1'b0;
for (r = 0; r < NRULES; r = r + 1) begin
exercised[r] <= {CNT_W{1'b0}};
violated[r] <= {CNT_W{1'b0}};
end
end else begin
sclk_d <= sclk;
cs_n_d <= cs_n;
mosi_d <= mosi;
if (clr) begin
for (r = 0; r < NRULES; r = r + 1) begin
exercised[r] <= {CNT_W{1'b0}};
violated[r] <= {CNT_W{1'b0}};
end
end
// ---------------- intervals -----------------------------------
if (cs_assert) since_cs_assert <= {{(CNT_W-1){1'b0}}, 1'b1};
else if (since_cs_assert != {CNT_W{1'b1}})
since_cs_assert <= since_cs_assert + 1'b1;
if (sclk_edge) since_sclk_edge <= {{(CNT_W-1){1'b0}}, 1'b1};
else if (since_sclk_edge != {CNT_W{1'b1}})
since_sclk_edge <= since_sclk_edge + 1'b1;
if (cs_deassert) since_cs_deassert <= {{(CNT_W-1){1'b0}}, 1'b1};
else if (since_cs_deassert != {CNT_W{1'b1}})
since_cs_deassert <= since_cs_deassert + 1'b1;
// ---------------- R1: SCLK idles at CPOL while deselected ------
// EXERCISED on every deselected cycle -- which is the right choice and
// worth arguing: the rule is a continuous obligation, so a suite that
// was ever deselected has exercised it. Counting only the assert would
// make the counter a transaction count wearing a rule's name.
if (cs_n) begin
exercised[R_IDLE] <= exercised[R_IDLE] + 1'b1;
if (sclk != cpol)
violated[R_IDLE] <= violated[R_IDLE] + 1'b1;
end
// ---------------- R2: the lead ---------------------------------
// EXERCISED at the FIRST edge of a transaction, because that is the
// only moment the lead exists. A transaction with no edges never
// exercises it, and that is correct: there was no lead to measure.
if (sclk_edge && in_txn && !seen_edge_in_txn) begin
seen_edge_in_txn <= 1'b1;
exercised[R_LEAD] <= exercised[R_LEAD] + 1'b1;
if (lead_now < LEAD)
violated[R_LEAD] <= violated[R_LEAD] + 1'b1;
end
// ---------------- R3: the half-period --------------------------
// EXERCISED on every edge that FOLLOWS another edge inside the same
// transaction. The first edge has no preceding one, and the interval
// after a deassert is not a half-period.
if (sclk_edge && in_txn && seen_edge_in_txn) begin
exercised[R_HALF] <= exercised[R_HALF] + 1'b1;
if (since_sclk_edge < HALF)
violated[R_HALF] <= violated[R_HALF] + 1'b1;
end
// ---------------- R4: MOSI moves only on launch edges ----------
// EXERCISED whenever MOSI changes inside a transaction -- which makes a
// constant data pattern exercise it ZERO times, and that is the honest
// count. A suite sending only 0x00 has not tested this rule at all.
if (mosi_change && in_txn) begin
exercised[R_LAUNCH] <= exercised[R_LAUNCH] + 1'b1;
// A change is legal on the launch edge and for a short while after
// it. It is illegal AT a capture edge, which is what would put the
// data in motion when the slave samples it (Chapter 14.6).
if (capture)
violated[R_LAUNCH] <= violated[R_LAUNCH] + 1'b1;
end
// ---------------- the frame remainder -------------------------
if (sclk_edge && in_txn) begin
if (in_frame == (edges_per_frame - 1'b1))
in_frame <= {(LEN_W+1){1'b0}};
else
in_frame <= in_frame + 1'b1;
end
// ---------------- R5, R6 at the deassert ----------------------
if (cs_deassert) begin
had_a_txn <= 1'b1;
// R5 is EXERCISED by any transaction that carried an edge. An
// empty select pulse does not exercise it -- there were no frames
// to be whole or partial.
if (seen_edge_in_txn || (sclk_edge && in_txn)) begin
exercised[R_FRAME] <= exercised[R_FRAME] + 1'b1;
if (frame_now != {(LEN_W+1){1'b0}})
violated[R_FRAME] <= violated[R_FRAME] + 1'b1;
// R6 likewise needs a last edge to lag from.
exercised[R_LAG] <= exercised[R_LAG] + 1'b1;
if (lag_now < LAG)
violated[R_LAG] <= violated[R_LAG] + 1'b1;
end
seen_edge_in_txn <= 1'b0;
in_frame <= {(LEN_W+1){1'b0}};
end
// ---------------- R7: the gap ---------------------------------
// EXERCISED at an assert that FOLLOWS a previous transaction. The very
// first assert of a run has no gap before it, and counting it would
// make every run report one spurious exercise.
if (cs_assert && had_a_txn) begin
exercised[R_GAP] <= exercised[R_GAP] + 1'b1;
if (gap_now < GAP)
violated[R_GAP] <= violated[R_GAP] + 1'b1;
end
// ---------------- R8: MISO driven only while selected ---------
// EXERCISED on every deselected cycle, for R1's reason.
if (cs_n) begin
exercised[R_DRIVE] <= exercised[R_DRIVE] + 1'b1;
if (miso_driven)
violated[R_DRIVE] <= violated[R_DRIVE] + 1'b1;
end
end
end
endmodule-- spi_rule_monitor.vhd
--
-- Chapter 16.1 -- a verification plan, made checkable.
--
-- A verification plan is usually a table of sentences. This file is the argument
-- that a plan is only real when every row of it maps to two things:
--
-- a CHECKER that fires when the rule is broken
-- a COUNTER that says the rule was EXERCISED
--
-- and that those are different, because a checker alone cannot tell "this rule was
-- never broken" from "this rule was never reached". A suite whose plan is all green
-- on the first reading is almost always reporting the second.
--
-- THE EIGHT RULES, AND WHERE EACH ONE CAME FROM.
--
-- Every rule here is observable FROM THE PINS ALONE. That is deliberate and it is
-- the plan's most important property: a rule that needs to look inside the DUT is a
-- rule the DUT gets a vote on (Chapter 16.5 is about what that costs).
--
-- R1 SCLK idles at CPOL while deselected (13.5, 14.6)
-- R2 CS leads the first SCLK edge by at least LEAD (14.4)
-- R3 every SCLK half-period is at least HALF (14.1, 15.3)
-- R4 MOSI changes only on LAUNCH edges, never on capture edges (13.5, 14.1)
-- R5 a transaction carries a whole number of frames (14.2)
-- R6 CS lags the last SCLK edge by at least LAG (13.7)
-- R7 CS stays high for at least GAP between transactions (14.5)
-- R8 MISO is driven only while selected (14.5)
--
-- WHY "EXERCISED" IS THE HARDER HALF.
--
-- Take R2. Its checker fires when the lead is shorter than LEAD. A suite that never
-- drives a short lead never fires it -- and a suite that never drives a lead AT ALL
-- also never fires it, because there was no transaction. Those two suites produce
-- identical reports and have tested completely different amounts.
--
-- So each rule here publishes BOTH:
--
-- exercised[i] the number of times the rule's precondition occurred
-- violated[i] the number of times the rule was broken
--
-- and a plan row is only closed when `exercised > 0`. That one extra counter is the
-- difference between a plan and a list of hopes.
--
-- AND THE RULE THIS BLOCK DELIBERATELY CANNOT CHECK.
--
-- Bit ORDER. Chapter 14.3 established that MSB-first and LSB-first produce
-- well-formed words of the right length at the right time, so no observation of the
-- pins distinguishes them. It is in the plan as a row with no checker, marked
-- "configuration, not checkable" -- because a plan that silently omits it looks
-- complete and a plan that lists it honestly does not.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- The rule vector gets a named TYPE in a package, which is what VHDL offers in place
-- of the packed-bus flattening the SystemVerilog and Verilog versions need. The
-- component is the same; the port is expressed in what the language actually has.
package spi_rule_pkg is
constant NRULES : natural := 8;
-- Named indices, so a report can be read without the source.
constant R_IDLE : natural := 0; -- R1
constant R_LEAD : natural := 1; -- R2
constant R_HALF : natural := 2; -- R3
constant R_LAUNCH : natural := 3; -- R4
constant R_FRAME : natural := 4; -- R5
constant R_LAG : natural := 5; -- R6
constant R_GAP : natural := 6; -- R7
constant R_DRIVE : natural := 7; -- R8
type rule_counts_t is array (0 to NRULES - 1) of natural;
function rule_name(i : natural) return string;
end package;
package body spi_rule_pkg is
function rule_name(i : natural) return string is
begin
case i is
when R_IDLE => return "SCLK idles at CPOL while deselected";
when R_LEAD => return "CS leads the first edge by LEAD";
when R_HALF => return "every half-period is at least HALF";
when R_LAUNCH => return "MOSI moves only on launch edges";
when R_FRAME => return "a whole number of frames";
when R_LAG => return "CS lags the last edge by LAG";
when R_GAP => return "CS high for at least GAP";
when others => return "MISO driven only while selected";
end case;
end function;
end package body;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_rule_pkg.all;
entity spi_rule_monitor is
generic (
LEAD : natural := 4; -- cycles, CS assert to first edge
HALF : natural := 3; -- cycles per SCLK half-period
LAG : natural := 2; -- cycles, last edge to CS deassert
GAP : natural := 3; -- cycles of CS high between transactions
LEN_W : positive := 6
);
port (
clk : in std_logic; -- the OBSERVER's clock, not the DUT's
rst_n : in std_logic;
-- the pins, and nothing else
sclk : in std_logic;
cs_n : in std_logic;
mosi : in std_logic;
miso_driven : in std_logic;
-- the configuration the rules are checked AGAINST
cpol : in std_logic;
cpha : in std_logic;
len : in unsigned(LEN_W - 1 downto 0);
-- the plan, as two vectors
exercised : out rule_counts_t;
violated : out rule_counts_t;
clr : in std_logic
);
end entity;
architecture rtl of spi_rule_monitor is
signal sclk_d : std_logic := '0';
signal cs_n_d : std_logic := '1';
signal mosi_d : std_logic := '0';
signal cs_assert, cs_deassert, sclk_edge, mosi_change : std_logic;
signal leading, trailing, capture, launch : std_logic;
signal in_txn : std_logic;
signal since_cs_assert : natural := 0;
signal since_sclk_edge : natural := 0;
signal since_cs_deassert : natural := 0;
signal seen_edge_in_txn : std_logic := '0';
signal in_frame : natural := 0;
signal had_a_txn : std_logic := '0';
signal edges_per_frame : natural;
signal lead_now, lag_now, gap_now, frame_now : natural;
signal ex_r, vi_r : rule_counts_t := (others => 0);
constant SAT : natural := 65535;
begin
cs_assert <= (not cs_n) and cs_n_d;
cs_deassert <= cs_n and (not cs_n_d);
sclk_edge <= sclk xor sclk_d;
mosi_change <= mosi xor mosi_d;
-- WHICH TRANSACTION A COINCIDENT EVENT BELONGS TO. An edge observed on the
-- deassert cycle belongs to the transaction that is ending, so the test includes
-- it -- `not cs_n` alone attributes that edge to no transaction at all.
in_txn <= (not cs_n) or cs_deassert;
-- A LAUNCH edge is the one that is not the capture edge (Chapter 14.6's mapping,
-- with CPOL already consumed).
leading <= sclk_edge when sclk /= cpol else '0';
trailing <= sclk_edge when sclk = cpol else '0';
capture <= trailing when cpha = '1' else leading;
launch <= leading when cpha = '1' else trailing;
edges_per_frame <= to_integer(len) * 2;
-- Each of these asks "is the event that STARTED this interval happening right
-- now?", and each therefore tests a DIFFERENT signal.
lead_now <= 0 when cs_assert = '1' else since_cs_assert;
lag_now <= 0 when sclk_edge = '1' else since_sclk_edge;
gap_now <= 0 when cs_deassert = '1' else since_cs_deassert;
-- The remainder AS IT WILL BE once this cycle's edge is counted.
frame_now <= 0 when (sclk_edge = '1' and in_txn = '1'
and in_frame = edges_per_frame - 1)
else in_frame + 1 when (sclk_edge = '1' and in_txn = '1')
else in_frame;
exercised <= ex_r;
violated <= vi_r;
rules : process (clk, rst_n)
procedure bump(idx : natural; hit : boolean) is
begin
if ex_r(idx) < SAT then
ex_r(idx) <= ex_r(idx) + 1;
end if;
if hit and vi_r(idx) < SAT then
vi_r(idx) <= vi_r(idx) + 1;
end if;
end procedure;
begin
if rst_n = '0' then
sclk_d <= '0';
cs_n_d <= '1';
mosi_d <= '0';
since_cs_assert <= 0;
since_sclk_edge <= 0;
since_cs_deassert <= 0;
seen_edge_in_txn <= '0';
in_frame <= 0;
had_a_txn <= '0';
ex_r <= (others => 0);
vi_r <= (others => 0);
elsif rising_edge(clk) then
sclk_d <= sclk;
cs_n_d <= cs_n;
mosi_d <= mosi;
if clr = '1' then
ex_r <= (others => 0);
vi_r <= (others => 0);
end if;
-- intervals
if cs_assert = '1' then since_cs_assert <= 1;
elsif since_cs_assert < SAT then since_cs_assert <= since_cs_assert + 1;
end if;
if sclk_edge = '1' then since_sclk_edge <= 1;
elsif since_sclk_edge < SAT then since_sclk_edge <= since_sclk_edge + 1;
end if;
if cs_deassert = '1' then since_cs_deassert <= 1;
elsif since_cs_deassert < SAT then
since_cs_deassert <= since_cs_deassert + 1;
end if;
-- R1: a continuous obligation, so every deselected cycle exercises it
if cs_n = '1' then
bump(R_IDLE, sclk /= cpol);
end if;
-- R2: the lead, at the FIRST edge of a transaction
if sclk_edge = '1' and in_txn = '1' and seen_edge_in_txn = '0' then
seen_edge_in_txn <= '1';
bump(R_LEAD, lead_now < LEAD);
end if;
-- R3: the half-period, on every edge that follows another one
if sclk_edge = '1' and in_txn = '1' and seen_edge_in_txn = '1' then
bump(R_HALF, since_sclk_edge < HALF);
end if;
-- R4: MOSI must not move AT a capture edge. Exercised only when MOSI
-- changes, so a constant data pattern exercises it ZERO times.
if mosi_change = '1' and in_txn = '1' then
bump(R_LAUNCH, capture = '1');
end if;
-- the frame remainder
if sclk_edge = '1' and in_txn = '1' then
if in_frame = edges_per_frame - 1 then
in_frame <= 0;
else
in_frame <= in_frame + 1;
end if;
end if;
-- R5 and R6 at the deassert
if cs_deassert = '1' then
had_a_txn <= '1';
if seen_edge_in_txn = '1' or (sclk_edge = '1' and in_txn = '1') then
bump(R_FRAME, frame_now /= 0);
bump(R_LAG, lag_now < LAG);
end if;
seen_edge_in_txn <= '0';
in_frame <= 0;
end if;
-- R7: the gap, at an assert that FOLLOWS a previous transaction
if cs_assert = '1' and had_a_txn = '1' then
bump(R_GAP, gap_now < GAP);
end if;
-- R8: a continuous obligation, like R1
if cs_n = '1' then
bump(R_DRIVE, miso_driven = '1');
end if;
end if;
end process;
end architecture;The Bench
// spi_rule_monitor_tb.sv
//
// The experiment that makes a verification plan trustworthy: drive LEGAL traffic and
// then break each rule ONE AT A TIME, and check three things each time.
//
// 1 the rule that was broken fires
// 2 no OTHER rule fires
// 3 the rule was EXERCISED -- so a zero in the violated column means "not broken"
// rather than "never reached"
//
// The second is the one benches usually skip, and it is what separates a checker from
// a smoke alarm. A monitor whose lead check also fires on a short gap is a monitor
// that will be disabled the first time an integrator sees it fire for the wrong
// reason.
//
// The third is what the chapter is about. `violated(i) == 0` is meaningless on its
// own; `violated(i) == 0 && exercised(i) > 0` is a closed plan row.
`timescale 1ns/1ps
module spi_rule_monitor_tb;
localparam int LEAD = 4;
localparam int HALF = 3;
localparam int LAG = 2;
localparam int GAP = 3;
localparam int LEN_W = 6;
localparam int CNT_W = 16;
localparam int NRULES = 8;
localparam int R_IDLE = 0, R_LEAD = 1, R_HALF = 2, R_LAUNCH = 3,
R_FRAME = 4, R_LAG = 5, R_GAP = 6, R_DRIVE = 7;
reg clk = 1'b0;
reg rst_n = 1'b1;
always #5 clk = ~clk;
reg sclk = 1'b0;
reg cs_n = 1'b1;
reg mosi = 1'b0;
reg miso_driven = 1'b0;
reg cpol = 1'b0;
reg cpha = 1'b0;
reg [LEN_W-1:0] len = 6'd4;
reg clr = 1'b0;
wire [NRULES*CNT_W-1:0] exercised_flat;
wire [NRULES*CNT_W-1:0] violated_flat;
// Named accessors, so the rest of the bench reads as if the ports were arrays.
// The flattening is the component's portability concession (see its header) and
// it should not leak into everything that reads it.
function automatic [CNT_W-1:0] exercised(input integer i);
exercised = exercised_flat[i*CNT_W +: CNT_W];
endfunction
function automatic [CNT_W-1:0] violated(input integer i);
violated = violated_flat[i*CNT_W +: CNT_W];
endfunction
spi_rule_monitor #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.LEN_W(LEN_W), .CNT_W(CNT_W), .NRULES(NRULES)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso_driven(miso_driven),
.cpol(cpol), .cpha(cpha), .len(len),
.exercised_flat(exercised_flat), .violated_flat(violated_flat), .clr(clr)
);
integer errors = 0;
initial begin
#500_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
// --- a legal master, parameterised so each rule can be broken in turn ---
// Every timing number is an argument, so a violation is produced by passing a
// different number rather than by writing a different driver. That matters: two
// drivers drift, and then a "violation" is a difference between two pieces of
// bench code.
// EVERY wait in the stimulus is on the NEGEDGE, and the monitor samples on the
// posedge. The first version of this bench waited on the posedge and assigned
// immediately afterwards, so the assignment and the monitor's sample happened at
// the same simulation time -- and the monitor read the old value. Every rule
// whose precondition is inside a transaction reported ZERO exercises while the
// waveform looked perfect.
//
// Driving away from the sampling edge is the discipline that fixes it, and
// Chapter 16.3 is about the language feature that encodes it declaratively
// instead of leaving it to every task in the file to remember.
// `drv_cpha` is the MASTER's phase and is separate from the monitor's `cpha`
// input, which is the whole point of the R4 test: a phase mismatch is a
// disagreement between two devices, so a bench that changes one signal changes
// both and measures nothing. The first version did exactly that and R4 never
// fired.
task automatic drive_txn(input [7:0] data, input integer nbits,
input integer lead_c, input integer half_c,
input integer lag_c, input integer stop_early,
input bit drv_cpha, input bit drv_miso);
integer i;
// A `done` flag rather than `return`: Verilog-2001 has no `return` in a task
// and no `break` in a loop, so the same source has to work without either --
// which it does, at the cost of one flag and a guarded loop condition.
reg done;
begin
done = 1'b0;
@(negedge clk);
sclk = cpol;
cs_n = 1'b0;
// MISO is driven only AFTER the select is low, which is the obligation R8
// states. Setting it before the select -- as the first version of this
// bench did, outside the task -- puts a driven MISO on a deselected bus
// for one monitor cycle and violates R8 on legal traffic.
if (drv_miso) miso_driven = 1'b1;
// The first bit is placed HERE, during the lead, and not alongside the
// first edge -- placing it on the same cycle as the leading edge makes a
// legal CPHA=0 master look like it moved MOSI at a capture instant, and
// R4 fired once on every legal run.
if (!drv_cpha) begin
@(negedge clk);
mosi = data[nbits-1];
end
repeat (lead_c) @(negedge clk);
for (i = 0; i < nbits && !done; i = i + 1) begin
if (!drv_cpha) begin
// CPHA=0: MOSI is launched on the TRAILING edge. The first bit
// was already placed during the lead, above.
sclk = ~cpol; // leading (capture)
repeat (half_c) @(negedge clk);
sclk = cpol; // trailing (launch)
if (i < nbits-1) mosi = data[nbits-2-i];
// On the LAST bit the trailing wait IS the lag -- there is no
// following edge, so R3 does not measure this interval and
// shortening it is how a short lag is produced at all. The first
// version waited a full half-period here and then `lag_c` more,
// so `lag_c = 0` still gave a lag of half a period and R6 could
// not be made to fire.
if (i == nbits-1) repeat (lag_c) @(negedge clk);
else repeat (half_c) @(negedge clk);
end else begin
sclk = ~cpol; // leading (launch)
mosi = data[nbits-1-i];
repeat (half_c) @(negedge clk);
sclk = cpol; // trailing (capture)
if (i == nbits-1) repeat (lag_c) @(negedge clk);
else repeat (half_c) @(negedge clk);
end
if (stop_early != 0 && i == stop_early-1) begin
// leave the transaction mid-frame on purpose
repeat (lag_c) @(negedge clk);
miso_driven = 1'b0;
cs_n = 1'b1;
done = 1'b1;
end
end
if (!done) begin
// and released BEFORE the select rises, for the same reason.
miso_driven = 1'b0;
cs_n = 1'b1;
end
end
endtask
task automatic wait_gap(input integer gap_c);
begin repeat (gap_c) @(negedge clk); end
endtask
task automatic restart;
begin
@(negedge clk);
cs_n = 1'b1; sclk = cpol; mosi = 1'b0; miso_driven = 1'b0;
rst_n = 1'b1;
repeat (2) @(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (4) @(negedge clk);
clr = 1'b1; @(negedge clk); clr = 1'b0; @(negedge clk);
end
endtask
// Reports which rules fired, and checks that exactly `want` did.
// A Verilog-2001 function must have at least one input, so this takes an unused
// one. SystemVerilog does not require it; keeping the same signature in both
// means the bench body is identical across the two languages.
function automatic [NRULES-1:0] fired(input bit unused);
integer k;
begin
fired = {NRULES{1'b0}};
for (k = 0; k < NRULES; k = k + 1)
if (violated(k) != 0) fired[k] = 1'b1;
end
endfunction
task automatic expect_only(input integer which, input [8*24-1:0] label);
integer k;
reg [NRULES-1:0] f;
begin
f = fired(1'b0);
if (which >= 0 && !f[which]) begin
$display(" FAIL: %0s did not fire rule %0d", label, which);
errors = errors + 1;
end
for (k = 0; k < NRULES; k = k + 1)
if (k != which && f[k]) begin
$display(" FAIL: %0s also fired rule %0d, which is a checker that will be switched off the first time it fires for the wrong reason",
label, k);
errors = errors + 1;
end
$display(" %0s -> fired %b", label, f);
end
endtask
integer k;
reg [NRULES-1:0] f;
integer unexercised;
initial begin
// =============================================================
// 1. LEGAL TRAFFIC: nothing fires, and EVERY rule is exercised. The second
// half is the chapter's point and the half a bench usually omits.
// =============================================================
restart();
miso_driven = 1'b0;
// 0xA5 has transitions in it, which is what exercises R4 at all.
drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
drive_txn(8'h3C, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
// and a transaction that legitimately drives MISO, so R8's precondition is
// separated from its violation -- the task asserts it after the select and
// releases it before the deselect, which is what R8 actually requires.
drive_txn(8'h5A, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b1);
wait_gap(GAP+2);
$display(" rule exercised violated what it says");
$display(" R1 %8d %8d SCLK idles at CPOL while deselected", exercised(R_IDLE), violated(R_IDLE));
$display(" R2 %8d %8d CS leads the first edge by LEAD", exercised(R_LEAD), violated(R_LEAD));
$display(" R3 %8d %8d every half-period is at least HALF", exercised(R_HALF), violated(R_HALF));
$display(" R4 %8d %8d MOSI moves only on launch edges", exercised(R_LAUNCH), violated(R_LAUNCH));
$display(" R5 %8d %8d a whole number of frames", exercised(R_FRAME), violated(R_FRAME));
$display(" R6 %8d %8d CS lags the last edge by LAG", exercised(R_LAG), violated(R_LAG));
$display(" R7 %8d %8d CS high for at least GAP", exercised(R_GAP), violated(R_GAP));
$display(" R8 %8d %8d MISO driven only while selected", exercised(R_DRIVE), violated(R_DRIVE));
f = fired(1'b0);
if (f != 0) begin
$display(" FAIL: legal traffic fired %b", f);
errors = errors + 1;
end
unexercised = 0;
for (k = 0; k < NRULES; k = k + 1)
if (exercised(k) == 0) begin
$display(" FAIL: rule %0d was never EXERCISED, so its zero in the violated column means nothing at all -- a plan row closed on that zero is a row nobody tested",
k);
unexercised = unexercised + 1;
errors = errors + 1;
end
if (unexercised == 0)
$display(" legal traffic: no rule violated, and all %0d rules EXERCISED -- which is what makes the eight zeros above mean something",
NRULES);
// =============================================================
// 2. BREAK EACH RULE IN TURN. One violation per run, and no other rule may
// fire -- which is the property that keeps a checker usable.
// =============================================================
// R1: SCLK parked away from idle while deselected.
restart();
@(negedge clk);
sclk = ~cpol; // wrong idle level
repeat (10) @(negedge clk);
sclk = cpol;
expect_only(R_IDLE, "R1 wrong idle level");
// R2: a lead shorter than LEAD.
restart();
drive_txn(8'hA5, 4, 1, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_LEAD, "R2 short lead");
// R3: a half-period shorter than HALF.
restart();
drive_txn(8'hA5, 4, LEAD+2, 1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_HALF, "R3 short half-period");
// R5: a transaction cut mid-frame. len is 4, so stopping after 3 bits
// leaves the remainder non-zero.
restart();
drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 3, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_FRAME, "R5 partial frame");
// R6: CS deasserting immediately after the last edge.
restart();
drive_txn(8'hA5, 4, LEAD+2, HALF+1, 0, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_LAG, "R6 short lag");
// R7: a gap shorter than GAP between two transactions.
restart();
drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(1);
drive_txn(8'h3C, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_GAP, "R7 short gap");
// R8: MISO driven while deselected.
restart();
@(negedge clk);
miso_driven = 1'b1;
repeat (8) @(negedge clk);
miso_driven = 1'b0;
expect_only(R_DRIVE, "R8 driven while idle");
// R4: MOSI moving at a capture edge. Produced by driving CPHA=1 timing
// against a monitor configured for CPHA=0 -- which is exactly the
// phase mismatch of Chapter 14.6, seen from the bus.
restart();
// The MONITOR stays configured for CPHA=0 and the MASTER drives CPHA=1
// timing. That disagreement is the point, and it is why the phase is an
// argument rather than a shared signal.
drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b1, 1'b0);
wait_gap(GAP+2);
f = fired(1'b0);
if (!f[R_LAUNCH]) begin
$display(" FAIL: a master launching on the monitor's capture edge must fire R4 -- that is the phase mismatch of Chapter 14.6 seen from the pins");
errors = errors + 1;
end
$display(" R4 phase-mismatched master -> fired %b", f);
// =============================================================
// 3. THE ROW WITH NO CHECKER, stated rather than omitted.
// =============================================================
$display(" and one plan row has NO checker and never will: bit ORDER. MSB-first and LSB-first produce a well-formed word of the right length at the right time (Chapter 14.3), so no observation of these pins distinguishes them. It stays in the plan marked 'configuration, not checkable', because a plan that silently omits it looks complete and one that lists it honestly does not");
if (errors == 0)
$display("PASS: a verification plan is only real when every row maps to a CHECKER and an EXERCISED counter, and the second is the harder half -- a checker alone cannot tell 'never broken' from 'never reached', and the two produce identical reports from suites that tested completely different amounts. Legal traffic violated none of the eight rules AND exercised all eight, which is what makes those zeros mean something. Each rule was then broken in turn and in every case exactly one rule fired: the wrong idle level fired only R1, a short lead only R2, a short half-period only R3, a partial frame only R5, a short lag only R6, a short gap only R7, and MISO driven while deselected only R8 -- a checker that also fires for a neighbouring fault is a checker an integrator switches off the first time it is wrong. R4 was broken by driving a phase-mismatched master, which is Chapter 14.6's fault seen from the pins rather than from inside the slave. And every rule here is observable FROM THE PINS ALONE, which is the plan's most important property, because a rule that needs to look inside the DUT is a rule the DUT gets a vote on -- with one honest exception, bit order, which has no checker and never will and stays in the plan saying so");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_rule_monitor_tb.v
//
// The experiment that makes a verification plan trustworthy: drive LEGAL traffic and
// then break each rule ONE AT A TIME, and check three things each time.
//
// 1 the rule that was broken fires
// 2 no OTHER rule fires
// 3 the rule was EXERCISED -- so a zero in the violated column means "not broken"
// rather than "never reached"
//
// The second is the one benches usually skip, and it is what separates a checker from
// a smoke alarm. A monitor whose lead check also fires on a short gap is a monitor
// that will be disabled the first time an integrator sees it fire for the wrong
// reason.
//
// The third is what the chapter is about. `violated(i) == 0` is meaningless on its
// own; `violated(i) == 0 && exercised(i) > 0` is a closed plan row.
`timescale 1ns/1ps
module spi_rule_monitor_tb;
localparam LEAD = 4;
localparam HALF = 3;
localparam LAG = 2;
localparam GAP = 3;
localparam LEN_W = 6;
localparam CNT_W = 16;
localparam NRULES = 8;
localparam R_IDLE = 0, R_LEAD = 1, R_HALF = 2, R_LAUNCH = 3,
R_FRAME = 4, R_LAG = 5, R_GAP = 6, R_DRIVE = 7;
reg clk;
reg rst_n;
always #5 clk = ~clk;
reg sclk;
reg cs_n;
reg mosi;
reg miso_driven;
reg cpol;
reg cpha;
reg [LEN_W-1:0] len;
reg clr;
wire [NRULES*CNT_W-1:0] exercised_flat;
wire [NRULES*CNT_W-1:0] violated_flat;
// Named accessors, so the rest of the bench reads as if the ports were arrays.
// The flattening is the component's portability concession (see its header) and
// it should not leak into everything that reads it.
function [CNT_W-1:0] exercised;
input integer i;
exercised = exercised_flat[i*CNT_W +: CNT_W];
endfunction
function [CNT_W-1:0] violated;
input integer i;
violated = violated_flat[i*CNT_W +: CNT_W];
endfunction
spi_rule_monitor #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.LEN_W(LEN_W), .CNT_W(CNT_W), .NRULES(NRULES)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso_driven(miso_driven),
.cpol(cpol), .cpha(cpha), .len(len),
.exercised_flat(exercised_flat), .violated_flat(violated_flat), .clr(clr)
);
integer errors;
initial begin
#500_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
// --- a legal master, parameterised so each rule can be broken in turn ---
// Every timing number is an argument, so a violation is produced by passing a
// different number rather than by writing a different driver. That matters: two
// drivers drift, and then a "violation" is a difference between two pieces of
// bench code.
// EVERY wait in the stimulus is on the NEGEDGE, and the monitor samples on the
// posedge. The first version of this bench waited on the posedge and assigned
// immediately afterwards, so the assignment and the monitor's sample happened at
// the same simulation time -- and the monitor read the old value. Every rule
// whose precondition is inside a transaction reported ZERO exercises while the
// waveform looked perfect.
//
// Driving away from the sampling edge is the discipline that fixes it, and
// Chapter 16.3 is about the language feature that encodes it declaratively
// instead of leaving it to every task in the file to remember.
// `drv_cpha` is the MASTER's phase and is separate from the monitor's `cpha`
// input, which is the whole point of the R4 test: a phase mismatch is a
// disagreement between two devices, so a bench that changes one signal changes
// both and measures nothing. The first version did exactly that and R4 never
// fired.
task drive_txn;
input [7:0] data;
input integer nbits;
input integer lead_c;
input integer half_c;
input integer lag_c;
input integer stop_early;
input drv_cpha;
input drv_miso;
integer i;
// A `done` flag rather than `return`: Verilog-2001 has no `return` in a task
// and no `break` in a loop, so the same source has to work without either --
// which it does, at the cost of one flag and a guarded loop condition.
reg done;
begin
done = 1'b0;
@(negedge clk);
sclk = cpol;
cs_n = 1'b0;
// MISO is driven only AFTER the select is low, which is the obligation R8
// states. Setting it before the select -- as the first version of this
// bench did, outside the task -- puts a driven MISO on a deselected bus
// for one monitor cycle and violates R8 on legal traffic.
if (drv_miso) miso_driven = 1'b1;
// The first bit is placed HERE, during the lead, and not alongside the
// first edge -- placing it on the same cycle as the leading edge makes a
// legal CPHA=0 master look like it moved MOSI at a capture instant, and
// R4 fired once on every legal run.
if (!drv_cpha) begin
@(negedge clk);
mosi = data[nbits-1];
end
repeat (lead_c) @(negedge clk);
for (i = 0; i < nbits && !done; i = i + 1) begin
if (!drv_cpha) begin
// CPHA=0: MOSI is launched on the TRAILING edge. The first bit
// was already placed during the lead, above.
sclk = ~cpol; // leading (capture)
repeat (half_c) @(negedge clk);
sclk = cpol; // trailing (launch)
if (i < nbits-1) mosi = data[nbits-2-i];
// On the LAST bit the trailing wait IS the lag -- there is no
// following edge, so R3 does not measure this interval and
// shortening it is how a short lag is produced at all. The first
// version waited a full half-period here and then `lag_c` more,
// so `lag_c = 0` still gave a lag of half a period and R6 could
// not be made to fire.
if (i == nbits-1) repeat (lag_c) @(negedge clk);
else repeat (half_c) @(negedge clk);
end else begin
sclk = ~cpol; // leading (launch)
mosi = data[nbits-1-i];
repeat (half_c) @(negedge clk);
sclk = cpol; // trailing (capture)
if (i == nbits-1) repeat (lag_c) @(negedge clk);
else repeat (half_c) @(negedge clk);
end
if (stop_early != 0 && i == stop_early-1) begin
// leave the transaction mid-frame on purpose
repeat (lag_c) @(negedge clk);
miso_driven = 1'b0;
cs_n = 1'b1;
done = 1'b1;
end
end
if (!done) begin
// and released BEFORE the select rises, for the same reason.
miso_driven = 1'b0;
cs_n = 1'b1;
end
end
endtask
task wait_gap;
input integer gap_c;
begin repeat (gap_c) @(negedge clk); end
endtask
task restart;
begin
@(negedge clk);
cs_n = 1'b1; sclk = cpol; mosi = 1'b0; miso_driven = 1'b0;
rst_n = 1'b1;
repeat (2) @(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (4) @(negedge clk);
clr = 1'b1; @(negedge clk); clr = 1'b0; @(negedge clk);
end
endtask
// Reports which rules fired, and checks that exactly `want` did.
// A Verilog-2001 function must have at least one input, so this takes an unused
// one. SystemVerilog does not require it; keeping the same signature in both
// means the bench body is identical across the two languages.
function [NRULES-1:0] fired;
input unused;
integer k;
begin
fired = {NRULES{1'b0}};
for (k = 0; k < NRULES; k = k + 1)
if (violated(k) != 0) fired[k] = 1'b1;
end
endfunction
task expect_only;
input integer which;
input [8*24-1:0] label;
integer k;
reg [NRULES-1:0] f;
begin
f = fired(1'b0);
if (which >= 0 && !f[which]) begin
$display(" FAIL: %0s did not fire rule %0d", label, which);
errors = errors + 1;
end
for (k = 0; k < NRULES; k = k + 1)
if (k != which && f[k]) begin
$display(" FAIL: %0s also fired rule %0d, which is a checker that will be switched off the first time it fires for the wrong reason",
label, k);
errors = errors + 1;
end
$display(" %0s -> fired %b", label, f);
end
endtask
integer k;
reg [NRULES-1:0] f;
integer unexercised;
initial begin
// =============================================================
// 1. LEGAL TRAFFIC: nothing fires, and EVERY rule is exercised. The second
// half is the chapter's point and the half a bench usually omits.
// =============================================================
restart();
miso_driven = 1'b0;
// 0xA5 has transitions in it, which is what exercises R4 at all.
drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
drive_txn(8'h3C, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
// and a transaction that legitimately drives MISO, so R8's precondition is
// separated from its violation -- the task asserts it after the select and
// releases it before the deselect, which is what R8 actually requires.
drive_txn(8'h5A, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b1);
wait_gap(GAP+2);
$display(" rule exercised violated what it says");
$display(" R1 %8d %8d SCLK idles at CPOL while deselected", exercised(R_IDLE), violated(R_IDLE));
$display(" R2 %8d %8d CS leads the first edge by LEAD", exercised(R_LEAD), violated(R_LEAD));
$display(" R3 %8d %8d every half-period is at least HALF", exercised(R_HALF), violated(R_HALF));
$display(" R4 %8d %8d MOSI moves only on launch edges", exercised(R_LAUNCH), violated(R_LAUNCH));
$display(" R5 %8d %8d a whole number of frames", exercised(R_FRAME), violated(R_FRAME));
$display(" R6 %8d %8d CS lags the last edge by LAG", exercised(R_LAG), violated(R_LAG));
$display(" R7 %8d %8d CS high for at least GAP", exercised(R_GAP), violated(R_GAP));
$display(" R8 %8d %8d MISO driven only while selected", exercised(R_DRIVE), violated(R_DRIVE));
f = fired(1'b0);
if (f != 0) begin
$display(" FAIL: legal traffic fired %b", f);
errors = errors + 1;
end
unexercised = 0;
for (k = 0; k < NRULES; k = k + 1)
if (exercised(k) == 0) begin
$display(" FAIL: rule %0d was never EXERCISED, so its zero in the violated column means nothing at all -- a plan row closed on that zero is a row nobody tested",
k);
unexercised = unexercised + 1;
errors = errors + 1;
end
if (unexercised == 0)
$display(" legal traffic: no rule violated, and all %0d rules EXERCISED -- which is what makes the eight zeros above mean something",
NRULES);
// =============================================================
// 2. BREAK EACH RULE IN TURN. One violation per run, and no other rule may
// fire -- which is the property that keeps a checker usable.
// =============================================================
// R1: SCLK parked away from idle while deselected.
restart();
@(negedge clk);
sclk = ~cpol; // wrong idle level
repeat (10) @(negedge clk);
sclk = cpol;
expect_only(R_IDLE, "R1 wrong idle level");
// R2: a lead shorter than LEAD.
restart();
drive_txn(8'hA5, 4, 1, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_LEAD, "R2 short lead");
// R3: a half-period shorter than HALF.
restart();
drive_txn(8'hA5, 4, LEAD+2, 1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_HALF, "R3 short half-period");
// R5: a transaction cut mid-frame. len is 4, so stopping after 3 bits
// leaves the remainder non-zero.
restart();
drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 3, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_FRAME, "R5 partial frame");
// R6: CS deasserting immediately after the last edge.
restart();
drive_txn(8'hA5, 4, LEAD+2, HALF+1, 0, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_LAG, "R6 short lag");
// R7: a gap shorter than GAP between two transactions.
restart();
drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(1);
drive_txn(8'h3C, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b0, 1'b0);
wait_gap(GAP+2);
expect_only(R_GAP, "R7 short gap");
// R8: MISO driven while deselected.
restart();
@(negedge clk);
miso_driven = 1'b1;
repeat (8) @(negedge clk);
miso_driven = 1'b0;
expect_only(R_DRIVE, "R8 driven while idle");
// R4: MOSI moving at a capture edge. Produced by driving CPHA=1 timing
// against a monitor configured for CPHA=0 -- which is exactly the
// phase mismatch of Chapter 14.6, seen from the bus.
restart();
// The MONITOR stays configured for CPHA=0 and the MASTER drives CPHA=1
// timing. That disagreement is the point, and it is why the phase is an
// argument rather than a shared signal.
drive_txn(8'hA5, 4, LEAD+2, HALF+1, LAG+1, 0, 1'b1, 1'b0);
wait_gap(GAP+2);
f = fired(1'b0);
if (!f[R_LAUNCH]) begin
$display(" FAIL: a master launching on the monitor's capture edge must fire R4 -- that is the phase mismatch of Chapter 14.6 seen from the pins");
errors = errors + 1;
end
$display(" R4 phase-mismatched master -> fired %b", f);
// =============================================================
// 3. THE ROW WITH NO CHECKER, stated rather than omitted.
// =============================================================
$display(" and one plan row has NO checker and never will: bit ORDER. MSB-first and LSB-first produce a well-formed word of the right length at the right time (Chapter 14.3), so no observation of these pins distinguishes them. It stays in the plan marked 'configuration, not checkable', because a plan that silently omits it looks complete and one that lists it honestly does not");
if (errors == 0)
$display("PASS: a verification plan is only real when every row maps to a CHECKER and an EXERCISED counter, and the second is the harder half -- a checker alone cannot tell 'never broken' from 'never reached', and the two produce identical reports from suites that tested completely different amounts. Legal traffic violated none of the eight rules AND exercised all eight, which is what makes those zeros mean something. Each rule was then broken in turn and in every case exactly one rule fired: the wrong idle level fired only R1, a short lead only R2, a short half-period only R3, a partial frame only R5, a short lag only R6, a short gap only R7, and MISO driven while deselected only R8 -- a checker that also fires for a neighbouring fault is a checker an integrator switches off the first time it is wrong. R4 was broken by driving a phase-mismatched master, which is Chapter 14.6's fault seen from the pins rather than from inside the slave. And every rule here is observable FROM THE PINS ALONE, which is the plan's most important property, because a rule that needs to look inside the DUT is a rule the DUT gets a vote on -- with one honest exception, bit order, which has no checker and never will and stays in the plan saying so");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
clk = 1'b0;
rst_n = 1'b1;
sclk = 1'b0;
cs_n = 1'b1;
mosi = 1'b0;
miso_driven = 1'b0;
cpol = 1'b0;
cpha = 1'b0;
len = 6'd4;
clr = 1'b0;
errors = 0;
end
endmodule-- spi_rule_monitor_tb.vhd
--
-- The experiment that makes a verification plan trustworthy: drive LEGAL traffic and
-- then break each rule ONE AT A TIME, and check three things each time.
--
-- 1 the rule that was broken fires
-- 2 no OTHER rule fires
-- 3 the rule was EXERCISED -- so a zero in the violated column means "not broken"
-- rather than "never reached"
--
-- The second is the one benches usually skip, and it is what separates a checker from
-- a smoke alarm. A monitor whose lead check also fires on a short gap is a monitor
-- that will be disabled the first time an integrator sees it fire for the wrong
-- reason.
--
-- The third is what the chapter is about. `violated(i) == 0` is meaningless on its
-- own; `violated(i) == 0 && exercised(i) > 0` is a closed plan row.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_rule_pkg.all;
entity spi_rule_monitor_tb is
end entity;
architecture sim of spi_rule_monitor_tb is
constant LEAD : natural := 4;
constant HALF : natural := 3;
constant LAG : natural := 2;
constant GAP : natural := 3;
constant LEN_W : positive := 6;
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal halt : boolean := false;
signal sclk : std_logic := '0';
signal cs_n : std_logic := '1';
signal mosi : std_logic := '0';
signal miso_driven : std_logic := '0';
signal cpol : std_logic := '0';
signal cpha : std_logic := '0';
signal len : unsigned(LEN_W - 1 downto 0) := to_unsigned(4, LEN_W);
signal clr : std_logic := '0';
signal exercised, violated : rule_counts_t;
begin
clkgen : process
begin
while not halt loop
clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
dut : entity work.spi_rule_monitor
generic map (LEAD => LEAD, HALF => HALF, LAG => LAG, GAP => GAP,
LEN_W => LEN_W)
port map (clk => clk, rst_n => rst_n,
sclk => sclk, cs_n => cs_n, mosi => mosi,
miso_driven => miso_driven,
cpol => cpol, cpha => cpha, len => len,
exercised => exercised, violated => violated, clr => clr);
watchdog : process
begin
wait for 500 us;
if not halt then
report "FAIL: the simulation did not finish within its time limit"
severity failure;
end if;
wait;
end process;
stim : process
variable errs : natural := 0;
variable f : std_logic_vector(NRULES - 1 downto 0);
variable unexercised : natural;
-- Every wait is on the FALLING edge and the monitor samples on the rising
-- one. Driving on the sampling edge makes the monitor read the old value, and
-- every rule whose precondition is inside a transaction reports zero
-- exercises while the waveform looks perfect.
procedure drive_txn(data : std_logic_vector(7 downto 0);
nbits : natural;
lead_c : natural;
half_c : natural;
lag_c : natural;
stop_early : natural;
drv_cpha : std_logic;
drv_miso : std_logic) is
variable done : boolean := false;
begin
done := false;
wait until falling_edge(clk);
sclk <= cpol;
cs_n <= '0';
-- MISO is driven only AFTER the select is low, which is what R8 requires.
if drv_miso = '1' then miso_driven <= '1'; end if;
-- The first bit is placed during the LEAD, not alongside the first edge:
-- placing it on the same cycle as the leading edge makes a legal CPHA=0
-- master look like it moved MOSI at a capture instant.
if drv_cpha = '0' then
wait until falling_edge(clk);
mosi <= data(nbits - 1);
end if;
for i in 1 to lead_c loop wait until falling_edge(clk); end loop;
for i in 0 to nbits - 1 loop
if not done then
if drv_cpha = '0' then
sclk <= not cpol; -- leading (capture)
for j in 1 to half_c loop wait until falling_edge(clk); end loop;
sclk <= cpol; -- trailing (launch)
if i < nbits - 1 then mosi <= data(nbits - 2 - i); end if;
-- On the LAST bit the trailing wait IS the lag: there is no
-- following edge, so R3 does not measure this interval and
-- shortening it is how a short lag is produced at all.
if i = nbits - 1 then
for j in 1 to lag_c loop wait until falling_edge(clk); end loop;
else
for j in 1 to half_c loop wait until falling_edge(clk); end loop;
end if;
else
sclk <= not cpol; -- leading (launch)
mosi <= data(nbits - 1 - i);
for j in 1 to half_c loop wait until falling_edge(clk); end loop;
sclk <= cpol; -- trailing (capture)
if i = nbits - 1 then
for j in 1 to lag_c loop wait until falling_edge(clk); end loop;
else
for j in 1 to half_c loop wait until falling_edge(clk); end loop;
end if;
end if;
if stop_early /= 0 and i = stop_early - 1 then
for j in 1 to lag_c loop wait until falling_edge(clk); end loop;
miso_driven <= '0';
cs_n <= '1';
done := true;
end if;
end if;
end loop;
if not done then
miso_driven <= '0';
cs_n <= '1';
end if;
end procedure;
procedure wait_gap(gap_c : natural) is
begin
for i in 1 to gap_c loop wait until falling_edge(clk); end loop;
end procedure;
procedure restart is
begin
wait until falling_edge(clk);
cs_n <= '1'; sclk <= cpol; mosi <= '0'; miso_driven <= '0';
rst_n <= '1';
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
rst_n <= '0';
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
rst_n <= '1';
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
clr <= '1'; wait until falling_edge(clk);
clr <= '0'; wait until falling_edge(clk);
end procedure;
impure function fired return std_logic_vector is
variable r : std_logic_vector(NRULES - 1 downto 0) := (others => '0');
begin
for k in 0 to NRULES - 1 loop
if violated(k) /= 0 then r(k) := '1'; end if;
end loop;
return r;
end function;
-- `label` is a VHDL RESERVED WORD, so the parameter is `tag`. The error names the
-- token rather than the reservation, which is confusing the first time.
procedure expect_only(which : integer; tag : string) is
variable v : std_logic_vector(NRULES - 1 downto 0);
begin
v := fired;
if which >= 0 and v(which) = '0' then
report " FAIL: " & tag & " did not fire rule " &
integer'image(which);
errs := errs + 1;
end if;
for k in 0 to NRULES - 1 loop
if k /= which and v(k) = '1' then
report " FAIL: " & tag & " also fired rule " &
integer'image(k) &
", which is a checker that will be switched off the first time it fires for the wrong reason";
errs := errs + 1;
end if;
end loop;
report " " & tag & " -> fired " & to_string(v);
end procedure;
begin
-- 1. LEGAL TRAFFIC: nothing fires, and EVERY rule is exercised.
restart;
-- 0xA5 has transitions in it, which is what exercises R4 at all.
drive_txn(x"A5", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '0');
wait_gap(GAP + 2);
drive_txn(x"3C", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '0');
wait_gap(GAP + 2);
-- and a transaction that legitimately drives MISO, so R8's precondition is
-- separated from its violation
drive_txn(x"5A", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '1');
wait_gap(GAP + 2);
report " rule exercised violated what it says";
for k in 0 to NRULES - 1 loop
report " R" & integer'image(k + 1) & " " &
integer'image(exercised(k)) & " " &
integer'image(violated(k)) & " " & rule_name(k);
end loop;
f := fired;
if f /= (f'range => '0') then
report " FAIL: legal traffic fired " & to_string(f);
errs := errs + 1;
end if;
unexercised := 0;
for k in 0 to NRULES - 1 loop
if exercised(k) = 0 then
report " FAIL: rule " & integer'image(k) &
" was never EXERCISED, so its zero in the violated column means nothing at all -- a plan row closed on that zero is a row nobody tested";
unexercised := unexercised + 1;
errs := errs + 1;
end if;
end loop;
if unexercised = 0 then
report " legal traffic: no rule violated, and all " &
integer'image(NRULES) &
" rules EXERCISED -- which is what makes those zeros mean something";
end if;
-- 2. BREAK EACH RULE IN TURN.
restart;
wait until falling_edge(clk);
sclk <= not cpol; -- wrong idle level
for i in 1 to 10 loop wait until falling_edge(clk); end loop;
sclk <= cpol;
expect_only(R_IDLE, "R1 wrong idle level");
restart;
drive_txn(x"A5", 4, 1, HALF + 1, LAG + 1, 0, '0', '0');
wait_gap(GAP + 2);
expect_only(R_LEAD, "R2 short lead");
restart;
drive_txn(x"A5", 4, LEAD + 2, 1, LAG + 1, 0, '0', '0');
wait_gap(GAP + 2);
expect_only(R_HALF, "R3 short half-period");
restart;
drive_txn(x"A5", 4, LEAD + 2, HALF + 1, LAG + 1, 3, '0', '0');
wait_gap(GAP + 2);
expect_only(R_FRAME, "R5 partial frame");
restart;
drive_txn(x"A5", 4, LEAD + 2, HALF + 1, 0, 0, '0', '0');
wait_gap(GAP + 2);
expect_only(R_LAG, "R6 short lag");
restart;
drive_txn(x"A5", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '0');
wait_gap(1);
drive_txn(x"3C", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '0', '0');
wait_gap(GAP + 2);
expect_only(R_GAP, "R7 short gap");
restart;
wait until falling_edge(clk);
miso_driven <= '1';
for i in 1 to 8 loop wait until falling_edge(clk); end loop;
miso_driven <= '0';
expect_only(R_DRIVE, "R8 driven while idle");
-- R4: the MONITOR stays at CPHA=0 and the MASTER drives CPHA=1 timing. That
-- disagreement is the point, and it is why the phase is an argument rather
-- than a shared signal.
restart;
drive_txn(x"A5", 4, LEAD + 2, HALF + 1, LAG + 1, 0, '1', '0');
wait_gap(GAP + 2);
f := fired;
if f(R_LAUNCH) = '0' then
report " FAIL: a master launching on the monitor's capture edge must fire R4 -- that is the phase mismatch of Chapter 14.6 seen from the pins";
errs := errs + 1;
end if;
report " R4 phase-mismatched master -> fired " & to_string(f);
report " and one plan row has NO checker and never will: bit ORDER. MSB-first and LSB-first produce a well-formed word of the right length at the right time (Chapter 14.3), so no observation of these pins distinguishes them. It stays in the plan marked 'configuration, not checkable', because a plan that silently omits it looks complete and one that lists it honestly does not";
if errs = 0 then
report "PASS: a verification plan is only real when every row maps to a CHECKER and an EXERCISED counter, and the second is the harder half -- a checker alone cannot tell 'never broken' from 'never reached', and the two produce identical reports from suites that tested completely different amounts. Legal traffic violated none of the eight rules AND exercised all eight, which is what makes those zeros mean something. Each rule was then broken in turn and in every case exactly one rule fired: the wrong idle level fired only R1, a short lead only R2, a short half-period only R3, a partial frame only R5, a short lag only R6, a short gap only R7, and MISO driven while deselected only R8 -- a checker that also fires for a neighbouring fault is a checker an integrator switches off the first time it is wrong. R4 was broken by driving a phase-mismatched master, which is Chapter 14.6's fault seen from the pins rather than from inside the slave. And every rule here is observable FROM THE PINS ALONE, which is the plan's most important property, because a rule that needs to look inside the DUT is a rule the DUT gets a vote on -- with one honest exception, bit order, which has no checker and never will and stays in the plan saying so";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;7. The Row With No Checker
and one plan row has NO checker and never will: bit ORDER.The bench prints that line deliberately, because it is the most useful sentence in its log.
MSB-first and LSB-first differ in which bit of the word goes out first. Both produce N edges in N pairs, a whole number of frames, a legal lead, a legal lag, legal half-periods, and MOSI moving only at launch edges. Every one of the eight rules passes on both. There is no observation of these three pins that separates them, because the difference is not in the pins — it is in the meaning assigned to them, which is a decision made at both ends and visible at neither.
That is not a gap to be closed by a cleverer monitor. It is a rule that belongs to a different layer of the environment, and Chapter 16.6's reference model is where it finally gets checked: the model predicts the word the transaction asked to send, the monitor rebuilds the word using the bit order the specification names, and a mismatch between them is what a reversed frame looks like. Bit order becomes checkable the moment you stop asking the pins and start comparing two independent statements about them.
8. Why a Verification Engineer Cares
Because the two-column plan is what makes a regression report readable, and the second column is the one that catches the failure nobody else catches.
A suite with 4,000 passing assertions and no exercise data is a suite in which any number of those assertions may be unreachable, and the number is unknowable from the report. Every one of the eight checkers in this chapter was, at some point during its development, silently unreachable — three of them for the reasons in the callout above, and each time the symptom was a green report.
The discipline generalises past SPI and past protocol checking: every check needs a second check that the first one ran. In a constrained-random environment that second check is coverage. In a directed test it is a counter. In a formal flow it is the cover trace that witnesses the antecedent. The names differ; the failure they prevent is identical.
9. Why an FPGA or ASIC Engineer Cares
Because this table is the contract you will be handed when something does not work, and its shape decides how long the argument takes.
A bug report that says "SPI is broken" starts a week of work. A report that says "R6 violated 14 times, R1–R5 and R7–R8 clean, all eight exercised" says: the master deasserts too soon after its last edge, everything else about it is correct, and the checker that says so has been shown to fire for this and only this. The second report is actionable in an afternoon, and the difference between them is the diagonal.
10. Failure Signature — A Green Regression With a Dead Checker
Symptom a regression that has been green for eleven months. A new
slave is integrated and fails immediately in the lab, on a
rule the suite has a checker for.
What happened six months earlier, a parameter default changed and the
checker's precondition stopped being reachable. The
assertion never fired again. Nothing in the report changed,
because a check that never runs and a check that always
passes print the same line.
What would have the exercised counter for that rule dropping from a few
caught it thousand to zero, in the same commit that changed the
default. A report that shows exercise counts makes this
visible as a diff; a report that shows pass/fail cannot
represent it at all.
The tell the failing rule is one of the ones with a narrow
precondition -- a gap, a lead, a coincidence. Continuous
obligations like R1 and R8 are exercised by any traffic at
all and almost never go dead. Rules with narrow
preconditions go dead quietly and stay dead.11. Common Misconceptions
"A verification plan is a list of features." A list of features is the input to a plan. The plan is the mapping from each feature to the specific check that decides it and the specific evidence that the check ran. A row with no checker named is a row that has not been planned; a row with a checker and no exercise evidence is a row that has been planned and not verified.
"If the assertion passes, the rule holds." Only if the assertion was reached. An SVA property whose antecedent is never true passes vacuously, forever, with no warning — which is the same failure as an exercised counter stuck at zero, and is why cover property on antecedents is not optional.
"Zero violations across a big regression is strong evidence." It is evidence in proportion to the exercise counts, and to nothing else. Zero violations with an exercise count of zero is not weak evidence; it is no evidence.
"Every requirement in the datasheet can be checked from the pins." Bit order cannot, and it is in every SPI datasheet. The plan's job includes saying which rows are not pin-decidable and naming the layer that will decide them instead — a reference model, a register read-back, an end-to-end data check. A plan that quietly implies a pin checker for a rule that has none is worse than a plan with a gap, because the gap is at least visible.
"The checker fires on the bug, so it works." That is half the test. A checker that fires on its own bug and also on three unrelated ones produces reports that require guessing. The diagonal is the property to demand, and demanding it found three monitor bugs in this chapter alone.
12. Reason It Through
A rule reports violated = 0 and exercised = 0 after a 10-hour regression. Name three distinct causes, and the observation that separates them.
The precondition is unreachable in this configuration; the precondition is written with the wrong polarity so it is never true; or the traffic genuinely never produces the situation. The separator is a second configuration: run a directed test that forces the situation. If the counter still reads zero the checker is broken; if it counts, the original suite's stimulus is the gap.
Why does R4 count MOSI changes rather than capture edges?
Because the rule is about MOSI moving at a forbidden moment. Counting capture edges would make the exercise count large and constant — every frame has them — and would hide the fact that a suite sending constant data has not tested the rule at all. The exercise counter should count the precondition of the rule, and R4's precondition is a change on MOSI.
Three transactions produce R7 = 2. What would R7 = 3 have meant?
That the checker counted the first assert of the run as following a gap. There is no preceding transaction to have left a gap, so the measurement is undefined and including it inflates every run's exercise count by exactly one — small enough never to be noticed and large enough to make a run with a single transaction report a gap check that never happened.
Where in this environment does bit order finally become checkable, and why not sooner?
In Chapter 16.6, when a reference model states independently what word should have been sent and a monitor states what word was observed. Not sooner, because a single observer of the pins has only one statement, and one statement cannot disagree with itself. Bit order is decided by comparison, and comparison needs two independent sources.
13. Understanding Check
14. Summary
A verification plan becomes real when every row names a checker and the evidence that the checker ran. Eight pin-observable SPI rules were extracted, implemented in one monitor in three languages, and measured: legal traffic violated none of them and exercised all eight, and eight injected faults produced a perfectly diagonal violation matrix. Three off-diagonal entries along the way were monitor bugs rather than stimulus bugs, and each of them had a green report as its symptom. One plan row — bit order — was shown to have no pin-level checker and never to be able to have one, which is why the honest plan says so in the row instead of implying a check that will not be written.
15. What Comes Next
The rules exist; something has to produce traffic that reaches them. Chapter 16.2 builds the transaction object and measures the difference between stimulus that looks random and stimulus that covers the space.
Continue learning
Related tutorials
- Related topic
Full-Duplex Exchange
Every SPI transfer moves a bit in both directions on every edge, whether the software wanted it to or not. Where dummy bytes come from, why bytes received during a command phase exist but mean nothing, and why read and write are interpretations rather than modes.
- Related topic
Mode Mismatch and Its Failure Signature
What happens when the two ends disagree about the mode. The distinct signature each mismatch produces, how to tell polarity from phase disagreement from the data alone, and the monitor and coverage work that catches it.
- Related topic
Write Waveform Analysis
Reconstruct a transaction from four unlabelled signals: recover the mode from the idle level and transitions, find the frame, segment the bytes, and see where decoding stops and the datasheet takes over.
- Related topic
Read Waveform Analysis
Decode an unlabelled read capture including the dummy phase: why the MISO tri-state transition is the most informative event on the bus, how to measure read latency without a datasheet, and checking versus discovery.
