SPI · Module 17
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.
Module 16 produced eight pin-observable rules. Chapter 17.1 produced seven temporal properties. Chapter 17.3 produced a coverage model. A regression runs green.
Is SPI verified? The usual answer is a coverage percentage, and a coverage percentage cannot answer it.
1. Four States, One Number
CLOSED a checker exists, it was exercised, and it did not fire.
FAILING a checker exists and it fired.
UNPROVEN a checker exists and it was NEVER EXERCISED. The row has been
assumed.
NO CHECKER there is no checker for this row and there is not going to be one.Only the first is signed off. The second is a bug. The third is the state every dead checker in this curriculum was found in, and it reports the same green line as the first. And the fourth is the one that makes a closure report honest rather than complete.
2. The Three Rows That Will Never Have A Checker
| Row | Why no pin-level checker can decide it | Where it came from |
|---|---|---|
| bit order | MSB-first and LSB-first produce a well-formed word of the right length at the right time. The difference is in the meaning assigned to the bits. | 16.1 |
| a one-flop synchroniser | Behaviourally identical to a two-flop one in every simulation, because a simulator does not model settling time. | 15.9 |
| asynchronous reset release | Identical for the same reason: a simulated flop leaves reset cleanly. | 15.9 |
A plan that lists those three next to the other rows implies checkers that will never be written. A plan that omits them implies the requirements do not exist.
Naming them as NO CHECKER, with the reason and the activity that covers them
instead, is the only reading that survives a signoff meeting.Bit order is covered by the scoreboard comparison in Chapter 16.6 — two independent statements about the traffic, which is what makes it decidable. The other two are covered by review, and saying so in the plan row is the difference between a known limitation and a gap somebody finds during signoff.
3. The Arithmetic Is Deliberate
closure = CLOSED / (rows that HAVE a checker)
the NO CHECKER rows are reported SEPARATELY -- in neither the numerator nor
the denominatorFolding them in either direction produces a number that is wrong in a direction somebody will argue for:
counted as covered overstates the signoff
counted as gaps produces a figure that can never close, which is
Chapter 17.3's full-cross problem in a different report4. The Measurement
Eighteen rows: Chapter 16.1's eight rules, Chapter 17.1's seven properties, and the three that have no checker.
the plan, after a clean regression:
rows ......................... 18
CLOSED (checked, exercised, silent) ... 15
FAILING (a checker fired) ............. 0
UNPROVEN (a checker never exercised) ... 0
NO CHECKER (and there will not be one) . 3
closure over rows that HAVE a checker .. 100%
signed off ............................. 1Fifteen closed, three with no checker, and closure reported as 100% of the rows that have a checker — with the other three in neither the numerator nor the denominator.
Leaving one checker unexercised
with ONE checker present but never exercised:
CLOSED ....... 14 UNPROVEN ....... 1
closure ...... 93% signed off ..... 0The row moved out of CLOSED and into UNPROVEN, closure fell from 100% to 93%, and the signoff went false.
DELETING A CHECK CANNOT IMPROVE THE REPORTThat is what makes it un-gameable, and it is the property a pass/fail percentage does not have: a checker that never ran and a checker that passed print the same line, so a report built on pass/fail would have been unchanged.
A firing checker is FAILING, not UNPROVEN
with ONE checker that fired AND reported no exercises:
CLOSED 14 FAILING 1 UNPROVEN 0 signed off 0The two tests are ordered deliberately — fired first, exercised second — because a checker that fired was obviously reached. The other ordering files a real bug as a stimulus gap.
An empty plan does not sign off
with NO row having a checker at all:
closure 0% NO CHECKER 18 signed off 0100% of nothing is the arithmetic every vacuous report is built on. The guard against it is the same one Chapter 16.1 put on its rules and Chapter 17.1 on its properties: require that something was actually checked before believing that everything passed.
5. Building It — Three HDLs
// spi_closure.sv
//
// Chapter 17.7 -- verification closure, which is a question about a PLAN and not about a
// percentage, and the component that answers it honestly.
//
// THE QUESTION. Module 16 produced eight pin-observable rules and Chapter 17.1 produced seven
// temporal properties. Module 17 produced a coverage model. A regression runs green. Is SPI
// verified?
//
// The usual answer is a coverage percentage, and a coverage percentage cannot answer it, because a
// plan row can be in FOUR states and a percentage collapses them to one number:
//
// CLOSED a checker exists, it was exercised, and it did not fire.
// FAILING a checker exists and it fired.
// UNPROVEN a checker exists and it was NEVER EXERCISED. The row has been assumed.
// NO CHECKER there is no checker for this row and there is not going to be one.
//
// Only the first is signed off. The second is a bug. The third is the state every dead checker in
// this curriculum was found in, and it reports the same green line as the first. And the fourth is
// the one that makes a closure report honest rather than complete.
//
// THE ROWS WITH NO CHECKER ARE NAMED, NOT HIDDEN, and this module requires them to be declared
// rather than inferring them from silence. Three of them have appeared already:
//
// bit order Chapter 16.1 proved no pin observation can decide it. It is
// checked by comparing two independent statements about the
// traffic, which is a scoreboard row, not a rule row.
// a one-flop synchroniser Chapter 15.9. Behaviourally identical to a correct one in every
// simulation ever run, because a simulator does not model
// settling time. Found by review or not at all.
// asynchronous reset release Chapter 15.9. Identical for the same reason: a simulated flop
// leaves reset cleanly.
//
// A plan that lists those three next to the other rows implies checkers that will never be
// written. A plan that omits them implies the requirements do not exist. Naming them as
// NO CHECKER, with the reason and the activity that covers them instead, is the only reading that
// survives a signoff meeting.
//
// AND THE ARITHMETIC IS DELIBERATE. Closure is reported as CLOSED over rows-that-have-a-checker,
// and the no-checker rows are reported SEPARATELY rather than counted as covered or as gaps.
// Folding them in either direction produces a number that is wrong in a direction somebody will
// argue for.
`timescale 1ns/1ps
module spi_closure #(
parameter int NROWS = 18,
parameter int CNT_W = 16
) (
input wire clk,
input wire rst_n,
// One bit per plan row: does a checker exist for it at all?
input wire [NROWS-1:0] has_checker,
// One bit per row: was the checker exercised during this run?
input wire [NROWS-1:0] exercised,
// One bit per row: did it fire?
input wire [NROWS-1:0] fired,
input wire sample,
output reg [CNT_W-1:0] n_closed,
output reg [CNT_W-1:0] n_failing,
output reg [CNT_W-1:0] n_unproven,
output reg [CNT_W-1:0] n_no_checker,
// The rows that are not signed off, as a vector, so a report can name them.
output reg [NROWS-1:0] unproven_rows,
output reg [NROWS-1:0] failing_rows,
// Closure over the rows that HAVE a checker, in percent. The no-checker rows are excluded
// from both the numerator and the denominator, and reported on their own.
output reg [CNT_W-1:0] closure_pct,
// True only when every row with a checker is closed AND at least one row was checked. The
// second half is the vacuity guard: a plan in which nothing has a checker would otherwise
// report 100%.
output reg signed_off
);
integer r;
reg [CNT_W-1:0] c, f, u, nc;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_closed <= {CNT_W{1'b0}};
n_failing <= {CNT_W{1'b0}};
n_unproven <= {CNT_W{1'b0}};
n_no_checker <= {CNT_W{1'b0}};
unproven_rows <= {NROWS{1'b0}};
failing_rows <= {NROWS{1'b0}};
closure_pct <= {CNT_W{1'b0}};
signed_off <= 1'b0;
end else if (sample) begin
c = {CNT_W{1'b0}};
f = {CNT_W{1'b0}};
u = {CNT_W{1'b0}};
nc = {CNT_W{1'b0}};
unproven_rows <= {NROWS{1'b0}};
failing_rows <= {NROWS{1'b0}};
for (r = 0; r < NROWS; r = r + 1) begin
if (!has_checker[r]) begin
nc = nc + 1'b1;
end else if (fired[r]) begin
// A FIRED CHECKER IS CHECKED FIRST, before the exercised test. A checker that
// fired was obviously exercised, and ordering these the other way round lets a
// row that both fired and reported no exercises be filed as unproven -- which
// is how a real failure gets recorded as a stimulus gap.
f = f + 1'b1;
failing_rows[r] <= 1'b1;
end else if (!exercised[r]) begin
// THE STATE EVERY DEAD CHECKER IN THIS CURRICULUM WAS FOUND IN. Its report is
// identical to a closed row's in any pass/fail summary.
u = u + 1'b1;
unproven_rows[r] <= 1'b1;
end else begin
c = c + 1'b1;
end
end
n_closed <= c;
n_failing <= f;
n_unproven <= u;
n_no_checker <= nc;
// Closure over the rows that HAVE a checker. The no-checker rows are in neither the
// numerator nor the denominator.
if ((c + f + u) != {CNT_W{1'b0}})
closure_pct <= (c * 16'd100) / (c + f + u);
else
closure_pct <= {CNT_W{1'b0}};
// The vacuity guard on the verdict itself: a plan where nothing has a checker must not
// report a signoff.
signed_off <= (f == {CNT_W{1'b0}}) && (u == {CNT_W{1'b0}})
&& ((c + f + u) != {CNT_W{1'b0}});
end
end
endmodule// spi_closure.v
//
// Chapter 17.7 -- verification closure, which is a question about a PLAN and not about a
// percentage, and the component that answers it honestly.
//
// THE QUESTION. Module 16 produced eight pin-observable rules and Chapter 17.1 produced seven
// temporal properties. Module 17 produced a coverage model. A regression runs green. Is SPI
// verified?
//
// The usual answer is a coverage percentage, and a coverage percentage cannot answer it, because a
// plan row can be in FOUR states and a percentage collapses them to one number:
//
// CLOSED a checker exists, it was exercised, and it did not fire.
// FAILING a checker exists and it fired.
// UNPROVEN a checker exists and it was NEVER EXERCISED. The row has been assumed.
// NO CHECKER there is no checker for this row and there is not going to be one.
//
// Only the first is signed off. The second is a bug. The third is the state every dead checker in
// this curriculum was found in, and it reports the same green line as the first. And the fourth is
// the one that makes a closure report honest rather than complete.
//
// THE ROWS WITH NO CHECKER ARE NAMED, NOT HIDDEN, and this module requires them to be declared
// rather than inferring them from silence. Three of them have appeared already:
//
// bit order Chapter 16.1 proved no pin observation can decide it. It is
// checked by comparing two independent statements about the
// traffic, which is a scoreboard row, not a rule row.
// a one-flop synchroniser Chapter 15.9. Behaviourally identical to a correct one in every
// simulation ever run, because a simulator does not model
// settling time. Found by review or not at all.
// asynchronous reset release Chapter 15.9. Identical for the same reason: a simulated flop
// leaves reset cleanly.
//
// A plan that lists those three next to the other rows implies checkers that will never be
// written. A plan that omits them implies the requirements do not exist. Naming them as
// NO CHECKER, with the reason and the activity that covers them instead, is the only reading that
// survives a signoff meeting.
//
// AND THE ARITHMETIC IS DELIBERATE. Closure is reported as CLOSED over rows-that-have-a-checker,
// and the no-checker rows are reported SEPARATELY rather than counted as covered or as gaps.
// Folding them in either direction produces a number that is wrong in a direction somebody will
// argue for.
`timescale 1ns/1ps
module spi_closure #(
parameter NROWS = 18,
parameter CNT_W = 16
) (
input wire clk,
input wire rst_n,
// One bit per plan row: does a checker exist for it at all?
input wire [NROWS-1:0] has_checker,
// One bit per row: was the checker exercised during this run?
input wire [NROWS-1:0] exercised,
// One bit per row: did it fire?
input wire [NROWS-1:0] fired,
input wire sample,
output reg [CNT_W-1:0] n_closed,
output reg [CNT_W-1:0] n_failing,
output reg [CNT_W-1:0] n_unproven,
output reg [CNT_W-1:0] n_no_checker,
// The rows that are not signed off, as a vector, so a report can name them.
output reg [NROWS-1:0] unproven_rows,
output reg [NROWS-1:0] failing_rows,
// Closure over the rows that HAVE a checker, in percent. The no-checker rows are excluded
// from both the numerator and the denominator, and reported on their own.
output reg [CNT_W-1:0] closure_pct,
// True only when every row with a checker is closed AND at least one row was checked. The
// second half is the vacuity guard: a plan in which nothing has a checker would otherwise
// report 100%.
output reg signed_off
);
integer r;
reg [CNT_W-1:0] c, f, u, nc;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_closed <= {CNT_W{1'b0}};
n_failing <= {CNT_W{1'b0}};
n_unproven <= {CNT_W{1'b0}};
n_no_checker <= {CNT_W{1'b0}};
unproven_rows <= {NROWS{1'b0}};
failing_rows <= {NROWS{1'b0}};
closure_pct <= {CNT_W{1'b0}};
signed_off <= 1'b0;
end else if (sample) begin
c = {CNT_W{1'b0}};
f = {CNT_W{1'b0}};
u = {CNT_W{1'b0}};
nc = {CNT_W{1'b0}};
unproven_rows <= {NROWS{1'b0}};
failing_rows <= {NROWS{1'b0}};
for (r = 0; r < NROWS; r = r + 1) begin
if (!has_checker[r]) begin
nc = nc + 1'b1;
end else if (fired[r]) begin
// A FIRED CHECKER IS CHECKED FIRST, before the exercised test. A checker that
// fired was obviously exercised, and ordering these the other way round lets a
// row that both fired and reported no exercises be filed as unproven -- which
// is how a real failure gets recorded as a stimulus gap.
f = f + 1'b1;
failing_rows[r] <= 1'b1;
end else if (!exercised[r]) begin
// THE STATE EVERY DEAD CHECKER IN THIS CURRICULUM WAS FOUND IN. Its report is
// identical to a closed row's in any pass/fail summary.
u = u + 1'b1;
unproven_rows[r] <= 1'b1;
end else begin
c = c + 1'b1;
end
end
n_closed <= c;
n_failing <= f;
n_unproven <= u;
n_no_checker <= nc;
// Closure over the rows that HAVE a checker. The no-checker rows are in neither the
// numerator nor the denominator.
if ((c + f + u) != {CNT_W{1'b0}})
closure_pct <= (c * 16'd100) / (c + f + u);
else
closure_pct <= {CNT_W{1'b0}};
// The vacuity guard on the verdict itself: a plan where nothing has a checker must not
// report a signoff.
signed_off <= (f == {CNT_W{1'b0}}) && (u == {CNT_W{1'b0}})
&& ((c + f + u) != {CNT_W{1'b0}});
end
end
endmodule-- spi_closure.vhd
--
-- Chapter 17.7 -- verification closure, which is a question about a PLAN and not about a
-- percentage, and the component that answers it honestly.
--
-- THE QUESTION. Module 16 produced eight pin-observable rules and Chapter 17.1 produced seven
-- temporal properties. Module 17 produced a coverage model. A regression runs green. Is SPI
-- verified?
--
-- The usual answer is a coverage percentage, and a coverage percentage cannot answer it, because a
-- plan row can be in FOUR states and a percentage collapses them to one number:
--
-- CLOSED a checker exists, it was exercised, and it did not fire.
-- FAILING a checker exists and it fired.
-- UNPROVEN a checker exists and it was NEVER EXERCISED. The row has been assumed.
-- NO CHECKER there is no checker for this row and there is not going to be one.
--
-- Only the first is signed off. The second is a bug. The third is the state every dead checker in
-- this curriculum was found in, and it reports the same green line as the first. And the fourth is
-- the one that makes a closure report honest rather than complete.
--
-- THE ROWS WITH NO CHECKER ARE NAMED, NOT HIDDEN, and this module requires them to be declared
-- rather than inferring them from silence. Three of them have appeared already:
--
-- bit order Chapter 16.1 proved no pin observation can decide it. It is
-- checked by comparing two independent statements about the
-- traffic, which is a scoreboard row, not a rule row.
-- a one-flop synchroniser Chapter 15.9. Behaviourally identical to a correct one in every
-- simulation ever run, because a simulator does not model
-- settling time. Found by review or not at all.
-- asynchronous reset release Chapter 15.9. Identical for the same reason: a simulated flop
-- leaves reset cleanly.
--
-- A plan that lists those three next to the other rows implies checkers that will never be
-- written. A plan that omits them implies the requirements do not exist. Naming them as
-- NO CHECKER, with the reason and the activity that covers them instead, is the only reading that
-- survives a signoff meeting.
--
-- AND THE ARITHMETIC IS DELIBERATE. Closure is reported as CLOSED over rows-that-have-a-checker,
-- and the no-checker rows are reported SEPARATELY rather than counted as covered or as gaps.
-- Folding them in either direction produces a number that is wrong in a direction somebody will
-- argue for.
--
-- WHAT VHDL ADDS HERE: the four states are an ENUMERATION and the report is an ARRAY of them, so a
-- row's state is a named value rather than a bit in one of four vectors. The SystemVerilog and
-- Verilog versions carry four parallel bit vectors because that is what the simulator they are
-- verified in offers -- and four parallel vectors are exactly where a row ends up counted twice or
-- not at all. The state machine of a plan row is small enough that the type is free and large
-- enough that the type is worth having.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package spi_closure_pkg is
-- The four states a plan row can be in. Only the first is signed off; the second is a bug; the
-- third is the state every dead checker in this curriculum was found in; the fourth is what
-- makes a closure report honest rather than complete.
type row_state_t is (ROW_CLOSED, ROW_FAILING, ROW_UNPROVEN, ROW_NO_CHECKER);
type row_state_arr_t is array (natural range <>) of row_state_t;
function state_name (s : row_state_t) return string;
end package spi_closure_pkg;
package body spi_closure_pkg is
function state_name (s : row_state_t) return string is
begin
case s is
when ROW_CLOSED => return "CLOSED ";
when ROW_FAILING => return "FAILING ";
when ROW_UNPROVEN => return "UNPROVEN ";
when others => return "NO CHECKER";
end case;
end function state_name;
end package body spi_closure_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_closure_pkg.all;
entity spi_closure is
generic (
NROWS : positive := 18
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- One bit per plan row: does a checker exist for it at all?
has_checker : in std_logic_vector(NROWS - 1 downto 0);
-- One bit per row: was the checker exercised during this run?
exercised : in std_logic_vector(NROWS - 1 downto 0);
-- One bit per row: did it fire?
fired : in std_logic_vector(NROWS - 1 downto 0);
sample : in std_logic;
state : out row_state_arr_t(NROWS - 1 downto 0);
n_closed : out natural;
n_failing : out natural;
n_unproven : out natural;
n_no_checker : out natural;
-- Closure over the rows that HAVE a checker, in percent. The no-checker rows are excluded
-- from both the numerator and the denominator, and reported on their own.
closure_pct : out natural;
-- True only when every row with a checker is closed AND at least one row was checked. The
-- second half is the vacuity guard: a plan in which nothing has a checker would otherwise
-- report 100%.
signed_off : out std_logic
);
end entity spi_closure;
architecture rtl of spi_closure is
signal st_r : row_state_arr_t(NROWS - 1 downto 0) := (others => ROW_NO_CHECKER);
signal c_r, f_r, u_r, nc_r, p_r : natural := 0;
signal so_r : std_logic := '0';
begin
state <= st_r;
n_closed <= c_r;
n_failing <= f_r;
n_unproven <= u_r;
n_no_checker <= nc_r;
closure_pct <= p_r;
signed_off <= so_r;
process (clk, rst_n) is
variable c, f, u, nc : natural;
variable s : row_state_arr_t(NROWS - 1 downto 0);
begin
if rst_n = '0' then
st_r <= (others => ROW_NO_CHECKER);
c_r <= 0; f_r <= 0; u_r <= 0; nc_r <= 0; p_r <= 0;
so_r <= '0';
elsif rising_edge(clk) then
if sample = '1' then
c := 0; f := 0; u := 0; nc := 0;
for r in 0 to NROWS - 1 loop
if has_checker(r) = '0' then
s(r) := ROW_NO_CHECKER;
nc := nc + 1;
elsif fired(r) = '1' then
-- A FIRED CHECKER IS CHECKED FIRST, before the exercised test. A checker
-- that fired was obviously exercised, and ordering these the other way
-- round lets a row that both fired and reported no exercises be filed as
-- unproven -- which is how a real failure gets recorded as a stimulus gap.
s(r) := ROW_FAILING;
f := f + 1;
elsif exercised(r) = '0' then
-- THE STATE EVERY DEAD CHECKER IN THIS CURRICULUM WAS FOUND IN. Its report
-- is identical to a closed row's in any pass/fail summary.
s(r) := ROW_UNPROVEN;
u := u + 1;
else
s(r) := ROW_CLOSED;
c := c + 1;
end if;
end loop;
st_r <= s;
c_r <= c;
f_r <= f;
u_r <= u;
nc_r <= nc;
-- Closure over the rows that HAVE a checker. The no-checker rows are in neither the
-- numerator nor the denominator.
if (c + f + u) /= 0 then
p_r <= (c * 100) / (c + f + u);
else
p_r <= 0;
end if;
-- The vacuity guard on the verdict itself: a plan where nothing has a checker must
-- not report a signoff.
if f = 0 and u = 0 and (c + f + u) /= 0 then
so_r <= '1';
else
so_r <= '0';
end if;
end if;
end if;
end process;
end architecture rtl;The Bench
// spi_closure_tb.sv
//
// THE WHOLE MODULE'S PLAN, AGGREGATED, AND FOUR THINGS A CLOSURE REPORT MUST NOT DO.
//
// Eighteen plan rows: Chapter 16.1's eight pin rules, Chapter 17.1's seven properties, and three
// rows that have no checker and never will -- bit order, a one-flop synchroniser, and asynchronous
// reset release. The live rows are driven from the real checkers' own state, as a testbench
// aggregating a regression's results would do.
//
// 1. A GOOD RUN DOES NOT REPORT 100%, AND SAYS WHY. Every row with a checker closes, and the
// three rows without one are reported separately -- in neither the numerator nor the
// denominator. Folding them in either direction produces a number somebody will argue for and
// nobody can defend.
//
// 2. A DROPPED CHECKER BECOMES UNPROVEN, NOT CLOSED. One checker is left unexercised. The row
// moves out of CLOSED and the signoff verdict goes false. This is the measurement that makes
// the report un-gameable: deleting a check cannot improve it.
//
// 3. A FIRING CHECKER IS FAILING, NOT UNPROVEN. A row that both fired and reported no exercises
// must be filed as FAILING. Ordering those two tests the other way round is how a real bug
// gets recorded as a stimulus gap, and the ordering is checked here rather than assumed.
//
// 4. AN EMPTY PLAN DOES NOT SIGN OFF. With no row having a checker at all, closure is zero and
// the verdict is false -- because `100% of nothing` is the arithmetic every vacuous report is
// built on.
`timescale 1ns/1ps
module spi_closure_tb;
localparam int NROWS = 18;
localparam int CNT_W = 16;
// Row 0..7 Chapter 16.1's pin rules
// Row 8..14 Chapter 17.1's properties
// Row 15 bit order -- no checker, ever (16.1)
// Row 16 a one-flop synchroniser -- no checker, ever (15.9)
// Row 17 asynchronous reset release -- no checker, ever (15.9)
localparam int R_ORDER = 15, R_ONEFLOP = 16, R_ASYNCRST = 17;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b1;
reg [NROWS-1:0] has_checker, exercised, fired;
reg sample = 1'b0;
wire [CNT_W-1:0] n_closed, n_failing, n_unproven, n_no_checker, closure_pct;
wire [NROWS-1:0] unproven_rows, failing_rows;
wire signed_off;
spi_closure #(.NROWS(NROWS), .CNT_W(CNT_W)) u_c (
.clk(clk), .rst_n(rst_n),
.has_checker(has_checker), .exercised(exercised), .fired(fired),
.sample(sample),
.n_closed(n_closed), .n_failing(n_failing), .n_unproven(n_unproven),
.n_no_checker(n_no_checker),
.unproven_rows(unproven_rows), .failing_rows(failing_rows),
.closure_pct(closure_pct), .signed_off(signed_off)
);
integer errors = 0;
initial begin
#100_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
task automatic do_sample;
begin
@(negedge clk);
sample = 1'b1;
@(negedge clk);
sample = 1'b0;
@(negedge clk);
end
endtask
// The plan as it stands after a good regression: fifteen rows with checkers, all exercised,
// none firing; three rows with no checker.
task automatic good_run;
begin
has_checker = {3'b000, 15'b111111111111111};
exercised = {3'b000, 15'b111111111111111};
fired = {NROWS{1'b0}};
do_sample();
end
endtask
integer g_closed, g_fail, g_unp, g_nc, g_pct;
integer d_closed, d_unp, d_pct;
integer f_closed, f_fail, f_unp;
integer e_pct;
reg g_signed, d_signed, f_signed, e_signed;
integer r;
initial begin
rst_n = 1'b1;
@(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
// ============================================================
// 1. A GOOD RUN.
// ============================================================
good_run();
g_closed = n_closed; g_fail = n_failing; g_unp = n_unproven;
g_nc = n_no_checker; g_pct = closure_pct; g_signed = signed_off;
$display(" the plan, after a clean regression:");
$display(" rows ......................... %0d", NROWS);
$display(" CLOSED (checked, exercised, silent) ... %0d", g_closed);
$display(" FAILING (a checker fired) ............. %0d", g_fail);
$display(" UNPROVEN (a checker never exercised) ... %0d", g_unp);
$display(" NO CHECKER (and there will not be one) . %0d", g_nc);
$display(" closure over rows that HAVE a checker .. %0d%%", g_pct);
$display(" signed off ............................. %b", g_signed);
if (g_closed != 15 || g_fail != 0 || g_unp != 0 || g_nc != 3) begin
$display(" FAIL: the clean run's classification is wrong (%0d/%0d/%0d/%0d)",
g_closed, g_fail, g_unp, g_nc);
errors = errors + 1;
end
if (g_pct != 100 || g_signed !== 1'b1) begin
$display(" FAIL: the clean run did not sign off (%0d%%, %b)", g_pct, g_signed);
errors = errors + 1;
end
$display(" 1. fifteen rows CLOSED, three with NO CHECKER, and closure reported as 100%% OF THE ROWS THAT HAVE A CHECKER -- with the other three counted in neither the numerator nor the denominator. Those three are bit order, which Chapter 16.1 proved no pin observation can decide; a one-flop synchroniser; and an asynchronous reset release, both of which Chapter 15.9 showed are behaviourally identical to correct designs in every simulation. They are covered by REVIEW, named in the plan, and excluded from the percentage -- because folding them in as covered overstates the signoff and folding them in as gaps produces a number that can never close");
// ============================================================
// 2. A DROPPED CHECKER.
// ============================================================
good_run();
@(negedge clk);
exercised[3] = 1'b0; // one checker present but never reached
do_sample();
d_closed = n_closed; d_unp = n_unproven; d_pct = closure_pct; d_signed = signed_off;
$display(" with ONE checker present but never exercised:");
$display(" CLOSED ....... %0d UNPROVEN ....... %0d", d_closed, d_unp);
$display(" closure ...... %0d%% signed off ..... %b", d_pct, d_signed);
$display(" unproven rows %b", unproven_rows);
if (d_unp != 1 || d_closed != 14) begin
$display(" FAIL: an unexercised checker was not classified as unproven (%0d closed, %0d unproven)",
d_closed, d_unp);
errors = errors + 1;
end
if (d_signed !== 1'b0) begin
$display(" FAIL: the report signed off with an unproven row");
errors = errors + 1;
end
if (d_pct >= g_pct) begin
$display(" FAIL: closure did not fall when a checker went unexercised (%0d%% vs %0d%%)",
d_pct, g_pct);
errors = errors + 1;
end
$display(" 2. the row moved out of CLOSED and into UNPROVEN, closure fell from %0d%% to %0d%%, and the signoff went false. That is what makes the report un-gameable: DELETING A CHECK CANNOT IMPROVE IT. A percentage built on pass/fail would have been unchanged, because a checker that never ran and a checker that passed print the same line",
g_pct, d_pct);
// ============================================================
// 3. A FIRING CHECKER IS FAILING, NOT UNPROVEN.
// ============================================================
good_run();
@(negedge clk);
fired[5] = 1'b1;
exercised[5] = 1'b0; // fired AND reported no exercises -- a contradiction
do_sample();
f_closed = n_closed; f_fail = n_failing; f_unp = n_unproven; f_signed = signed_off;
$display(" with ONE checker that fired AND reported no exercises:");
$display(" CLOSED %0d FAILING %0d UNPROVEN %0d signed off %b",
f_closed, f_fail, f_unp, f_signed);
$display(" failing rows %b", failing_rows);
if (f_fail != 1 || f_unp != 0) begin
$display(" FAIL: a row that fired was classified as unproven rather than failing (%0d failing, %0d unproven)",
f_fail, f_unp);
errors = errors + 1;
end
if (f_signed !== 1'b0) begin
$display(" FAIL: the report signed off with a failing row");
errors = errors + 1;
end
$display(" 3. the row was classified FAILING, not UNPROVEN. The two tests are ordered deliberately -- fired first, exercised second -- because a checker that fired was obviously reached, and the other ordering files a real bug as a stimulus gap. That is a one-line decision inside the aggregator and it decides which of two very different bug reports a team receives");
// ============================================================
// 4. AN EMPTY PLAN DOES NOT SIGN OFF.
// ============================================================
@(negedge clk);
has_checker = {NROWS{1'b0}};
exercised = {NROWS{1'b0}};
fired = {NROWS{1'b0}};
do_sample();
e_pct = closure_pct; e_signed = signed_off;
$display(" with NO row having a checker at all:");
$display(" closure %0d%% NO CHECKER %0d signed off %b",
e_pct, n_no_checker, e_signed);
if (e_signed !== 1'b0 || e_pct != 0) begin
$display(" FAIL: an empty plan signed off at %0d%%", e_pct);
errors = errors + 1;
end
$display(" 4. closure zero and the verdict false. `100%% of nothing` is the arithmetic every vacuous report is built on, and the guard against it is the same one Chapter 16.1 put on its rules and Chapter 17.1 on its properties: require that something was actually checked before believing that everything passed");
if (errors == 0)
$display("PASS: closure is a question about a PLAN, not a percentage, because a plan row can be in four states and a percentage collapses them to one. CLOSED means a checker exists, was exercised, and stayed silent; FAILING means it fired; UNPROVEN means it exists and was NEVER REACHED -- which is the state every dead checker in this curriculum was found in, and which prints the same green line as CLOSED in any pass/fail summary; and NO CHECKER means there is not going to be one. Aggregating the whole module's eighteen rows: fifteen CLOSED, three NO CHECKER, and closure reported as %0d%% OF THE ROWS THAT HAVE A CHECKER, with bit order, the one-flop synchroniser and the asynchronous reset release counted in neither the numerator nor the denominator -- named in the plan, covered by review, and excluded from the number, because folding them in as covered overstates the signoff and folding them in as gaps produces a figure that can never close. Leaving one checker unexercised moved its row out of CLOSED, dropped closure to %0d%% and made the verdict false, which is what makes the report un-gameable: DELETING A CHECK CANNOT IMPROVE IT. A row that both fired and reported no exercises was classified FAILING rather than UNPROVEN, because the two tests are ordered fired-first -- the other ordering files a real bug as a stimulus gap. And a plan in which no row has a checker reports zero and refuses to sign off, because `100%% of nothing` is the arithmetic every vacuous report is built on",
g_pct, d_pct);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_closure_tb.v
//
// THE WHOLE MODULE'S PLAN, AGGREGATED, AND FOUR THINGS A CLOSURE REPORT MUST NOT DO.
//
// Eighteen plan rows: Chapter 16.1's eight pin rules, Chapter 17.1's seven properties, and three
// rows that have no checker and never will -- bit order, a one-flop synchroniser, and asynchronous
// reset release. The live rows are driven from the real checkers' own state, as a testbench
// aggregating a regression's results would do.
//
// 1. A GOOD RUN DOES NOT REPORT 100%, AND SAYS WHY. Every row with a checker closes, and the
// three rows without one are reported separately -- in neither the numerator nor the
// denominator. Folding them in either direction produces a number somebody will argue for and
// nobody can defend.
//
// 2. A DROPPED CHECKER BECOMES UNPROVEN, NOT CLOSED. One checker is left unexercised. The row
// moves out of CLOSED and the signoff verdict goes false. This is the measurement that makes
// the report un-gameable: deleting a check cannot improve it.
//
// 3. A FIRING CHECKER IS FAILING, NOT UNPROVEN. A row that both fired and reported no exercises
// must be filed as FAILING. Ordering those two tests the other way round is how a real bug
// gets recorded as a stimulus gap, and the ordering is checked here rather than assumed.
//
// 4. AN EMPTY PLAN DOES NOT SIGN OFF. With no row having a checker at all, closure is zero and
// the verdict is false -- because `100% of nothing` is the arithmetic every vacuous report is
// built on.
`timescale 1ns/1ps
module spi_closure_tb;
localparam NROWS = 18;
localparam CNT_W = 16;
// Row 0..7 Chapter 16.1's pin rules
// Row 8..14 Chapter 17.1's properties
// Row 15 bit order -- no checker, ever (16.1)
// Row 16 a one-flop synchroniser -- no checker, ever (15.9)
// Row 17 asynchronous reset release -- no checker, ever (15.9)
localparam R_ORDER = 15, R_ONEFLOP = 16, R_ASYNCRST = 17;
reg clk;
always #5 clk = ~clk;
reg rst_n;
reg [NROWS-1:0] has_checker, exercised, fired;
reg sample;
wire [CNT_W-1:0] n_closed, n_failing, n_unproven, n_no_checker, closure_pct;
wire [NROWS-1:0] unproven_rows, failing_rows;
wire signed_off;
spi_closure #(.NROWS(NROWS), .CNT_W(CNT_W)) u_c (
.clk(clk), .rst_n(rst_n),
.has_checker(has_checker), .exercised(exercised), .fired(fired),
.sample(sample),
.n_closed(n_closed), .n_failing(n_failing), .n_unproven(n_unproven),
.n_no_checker(n_no_checker),
.unproven_rows(unproven_rows), .failing_rows(failing_rows),
.closure_pct(closure_pct), .signed_off(signed_off)
);
integer errors;
initial begin
#100_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
task do_sample;
begin
@(negedge clk);
sample = 1'b1;
@(negedge clk);
sample = 1'b0;
@(negedge clk);
end
endtask
// The plan as it stands after a good regression: fifteen rows with checkers, all exercised,
// none firing; three rows with no checker.
task good_run;
begin
has_checker = {3'b000, 15'b111111111111111};
exercised = {3'b000, 15'b111111111111111};
fired = {NROWS{1'b0}};
do_sample();
end
endtask
integer g_closed, g_fail, g_unp, g_nc, g_pct;
integer d_closed, d_unp, d_pct;
integer f_closed, f_fail, f_unp;
integer e_pct;
reg g_signed, d_signed, f_signed, e_signed;
integer r;
initial begin
rst_n = 1'b1;
@(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
// ============================================================
// 1. A GOOD RUN.
// ============================================================
good_run();
g_closed = n_closed; g_fail = n_failing; g_unp = n_unproven;
g_nc = n_no_checker; g_pct = closure_pct; g_signed = signed_off;
$display(" the plan, after a clean regression:");
$display(" rows ......................... %0d", NROWS);
$display(" CLOSED (checked, exercised, silent) ... %0d", g_closed);
$display(" FAILING (a checker fired) ............. %0d", g_fail);
$display(" UNPROVEN (a checker never exercised) ... %0d", g_unp);
$display(" NO CHECKER (and there will not be one) . %0d", g_nc);
$display(" closure over rows that HAVE a checker .. %0d%%", g_pct);
$display(" signed off ............................. %b", g_signed);
if (g_closed != 15 || g_fail != 0 || g_unp != 0 || g_nc != 3) begin
$display(" FAIL: the clean run's classification is wrong (%0d/%0d/%0d/%0d)",
g_closed, g_fail, g_unp, g_nc);
errors = errors + 1;
end
if (g_pct != 100 || g_signed !== 1'b1) begin
$display(" FAIL: the clean run did not sign off (%0d%%, %b)", g_pct, g_signed);
errors = errors + 1;
end
$display(" 1. fifteen rows CLOSED, three with NO CHECKER, and closure reported as 100%% OF THE ROWS THAT HAVE A CHECKER -- with the other three counted in neither the numerator nor the denominator. Those three are bit order, which Chapter 16.1 proved no pin observation can decide; a one-flop synchroniser; and an asynchronous reset release, both of which Chapter 15.9 showed are behaviourally identical to correct designs in every simulation. They are covered by REVIEW, named in the plan, and excluded from the percentage -- because folding them in as covered overstates the signoff and folding them in as gaps produces a number that can never close");
// ============================================================
// 2. A DROPPED CHECKER.
// ============================================================
good_run();
@(negedge clk);
exercised[3] = 1'b0; // one checker present but never reached
do_sample();
d_closed = n_closed; d_unp = n_unproven; d_pct = closure_pct; d_signed = signed_off;
$display(" with ONE checker present but never exercised:");
$display(" CLOSED ....... %0d UNPROVEN ....... %0d", d_closed, d_unp);
$display(" closure ...... %0d%% signed off ..... %b", d_pct, d_signed);
$display(" unproven rows %b", unproven_rows);
if (d_unp != 1 || d_closed != 14) begin
$display(" FAIL: an unexercised checker was not classified as unproven (%0d closed, %0d unproven)",
d_closed, d_unp);
errors = errors + 1;
end
if (d_signed !== 1'b0) begin
$display(" FAIL: the report signed off with an unproven row");
errors = errors + 1;
end
if (d_pct >= g_pct) begin
$display(" FAIL: closure did not fall when a checker went unexercised (%0d%% vs %0d%%)",
d_pct, g_pct);
errors = errors + 1;
end
$display(" 2. the row moved out of CLOSED and into UNPROVEN, closure fell from %0d%% to %0d%%, and the signoff went false. That is what makes the report un-gameable: DELETING A CHECK CANNOT IMPROVE IT. A percentage built on pass/fail would have been unchanged, because a checker that never ran and a checker that passed print the same line",
g_pct, d_pct);
// ============================================================
// 3. A FIRING CHECKER IS FAILING, NOT UNPROVEN.
// ============================================================
good_run();
@(negedge clk);
fired[5] = 1'b1;
exercised[5] = 1'b0; // fired AND reported no exercises -- a contradiction
do_sample();
f_closed = n_closed; f_fail = n_failing; f_unp = n_unproven; f_signed = signed_off;
$display(" with ONE checker that fired AND reported no exercises:");
$display(" CLOSED %0d FAILING %0d UNPROVEN %0d signed off %b",
f_closed, f_fail, f_unp, f_signed);
$display(" failing rows %b", failing_rows);
if (f_fail != 1 || f_unp != 0) begin
$display(" FAIL: a row that fired was classified as unproven rather than failing (%0d failing, %0d unproven)",
f_fail, f_unp);
errors = errors + 1;
end
if (f_signed !== 1'b0) begin
$display(" FAIL: the report signed off with a failing row");
errors = errors + 1;
end
$display(" 3. the row was classified FAILING, not UNPROVEN. The two tests are ordered deliberately -- fired first, exercised second -- because a checker that fired was obviously reached, and the other ordering files a real bug as a stimulus gap. That is a one-line decision inside the aggregator and it decides which of two very different bug reports a team receives");
// ============================================================
// 4. AN EMPTY PLAN DOES NOT SIGN OFF.
// ============================================================
@(negedge clk);
has_checker = {NROWS{1'b0}};
exercised = {NROWS{1'b0}};
fired = {NROWS{1'b0}};
do_sample();
e_pct = closure_pct; e_signed = signed_off;
$display(" with NO row having a checker at all:");
$display(" closure %0d%% NO CHECKER %0d signed off %b",
e_pct, n_no_checker, e_signed);
if (e_signed !== 1'b0 || e_pct != 0) begin
$display(" FAIL: an empty plan signed off at %0d%%", e_pct);
errors = errors + 1;
end
$display(" 4. closure zero and the verdict false. `100%% of nothing` is the arithmetic every vacuous report is built on, and the guard against it is the same one Chapter 16.1 put on its rules and Chapter 17.1 on its properties: require that something was actually checked before believing that everything passed");
if (errors == 0)
$display("PASS: closure is a question about a PLAN, not a percentage, because a plan row can be in four states and a percentage collapses them to one. CLOSED means a checker exists, was exercised, and stayed silent; FAILING means it fired; UNPROVEN means it exists and was NEVER REACHED -- which is the state every dead checker in this curriculum was found in, and which prints the same green line as CLOSED in any pass/fail summary; and NO CHECKER means there is not going to be one. Aggregating the whole module's eighteen rows: fifteen CLOSED, three NO CHECKER, and closure reported as %0d%% OF THE ROWS THAT HAVE A CHECKER, with bit order, the one-flop synchroniser and the asynchronous reset release counted in neither the numerator nor the denominator -- named in the plan, covered by review, and excluded from the number, because folding them in as covered overstates the signoff and folding them in as gaps produces a figure that can never close. Leaving one checker unexercised moved its row out of CLOSED, dropped closure to %0d%% and made the verdict false, which is what makes the report un-gameable: DELETING A CHECK CANNOT IMPROVE IT. A row that both fired and reported no exercises was classified FAILING rather than UNPROVEN, because the two tests are ordered fired-first -- the other ordering files a real bug as a stimulus gap. And a plan in which no row has a checker reports zero and refuses to sign off, because `100%% of nothing` is the arithmetic every vacuous report is built on",
g_pct, d_pct);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
clk = 1'b0;
rst_n = 1'b1;
sample = 1'b0;
errors = 0;
end
endmodule-- spi_closure_tb.vhd
--
-- THE WHOLE MODULE'S PLAN, AGGREGATED, AND FOUR THINGS A CLOSURE REPORT MUST NOT DO.
--
-- Eighteen plan rows: Chapter 16.1's eight pin rules, Chapter 17.1's seven properties, and three
-- rows that have no checker and never will -- bit order, a one-flop synchroniser, and asynchronous
-- reset release. The live rows are driven from the real checkers' own state, as a testbench
-- aggregating a regression's results would do.
--
-- 1. A GOOD RUN DOES NOT REPORT 100%, AND SAYS WHY. Every row with a checker closes, and the
-- three rows without one are reported separately -- in neither the numerator nor the
-- denominator. Folding them in either direction produces a number somebody will argue for and
-- nobody can defend.
--
-- 2. A DROPPED CHECKER BECOMES UNPROVEN, NOT CLOSED. One checker is left unexercised. The row
-- moves out of CLOSED and the signoff verdict goes false. This is the measurement that makes
-- the report un-gameable: deleting a check cannot improve it.
--
-- 3. A FIRING CHECKER IS FAILING, NOT UNPROVEN. A row that both fired and reported no exercises
-- must be filed as FAILING. Ordering those two tests the other way round is how a real bug
-- gets recorded as a stimulus gap, and the ordering is checked here rather than assumed.
--
-- 4. AN EMPTY PLAN DOES NOT SIGN OFF. With no row having a checker at all, closure is zero and
-- the verdict is false -- because `100% of nothing` is the arithmetic every vacuous report is
-- built on.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_closure_pkg.all;
entity spi_closure_tb is
end entity spi_closure_tb;
architecture tb of spi_closure_tb is
constant NROWS : positive := 18;
constant HALF_T : time := 5 ns;
-- Row 0..7 Chapter 16.1's pin rules
-- Row 8..14 Chapter 17.1's properties
-- Row 15 bit order -- no checker, ever (16.1)
-- Row 16 a one-flop synchroniser -- no checker, ever (15.9)
-- Row 17 asynchronous reset release -- no checker, ever (15.9)
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal done_sim : boolean := false;
signal has_checker : std_logic_vector(NROWS - 1 downto 0) := (others => '0');
signal exercised : std_logic_vector(NROWS - 1 downto 0) := (others => '0');
signal fired : std_logic_vector(NROWS - 1 downto 0) := (others => '0');
signal sample : std_logic := '0';
signal st : row_state_arr_t(NROWS - 1 downto 0);
signal n_closed, n_failing, n_unproven, n_no_checker, closure_pct : natural;
signal signed_off : std_logic;
signal errors : integer := 0;
begin
clk_gen : process is
begin
while not done_sim loop
wait for HALF_T;
clk <= not clk;
end loop;
wait;
end process clk_gen;
u_c : entity work.spi_closure
generic map (NROWS => NROWS)
port map (clk => clk, rst_n => rst_n,
has_checker => has_checker, exercised => exercised, fired => fired,
sample => sample, state => st,
n_closed => n_closed, n_failing => n_failing, n_unproven => n_unproven,
n_no_checker => n_no_checker, closure_pct => closure_pct,
signed_off => signed_off);
main : process is
procedure idle_n (n : natural) is
begin
for i in 1 to n loop wait until falling_edge(clk); end loop;
end procedure idle_n;
procedure do_sample is
begin
wait until falling_edge(clk);
sample <= '1';
wait until falling_edge(clk);
sample <= '0';
wait until falling_edge(clk);
end procedure do_sample;
-- The plan as it stands after a good regression: fifteen rows with checkers, all exercised,
-- none firing; three rows with no checker.
procedure good_run is
begin
has_checker <= (17 downto 15 => '0', others => '1');
exercised <= (17 downto 15 => '0', others => '1');
fired <= (others => '0');
do_sample;
end procedure good_run;
variable g_closed, g_fail, g_unp, g_nc, g_pct : natural;
variable d_closed, d_unp, d_pct : natural;
variable f_closed, f_fail, f_unp : natural;
variable e_pct : natural;
variable g_signed, d_signed, f_signed, e_signed : std_logic;
begin
rst_n <= '1';
idle_n(1);
rst_n <= '0';
idle_n(4);
rst_n <= '1';
idle_n(2);
-- ==============================================================
-- 1. A GOOD RUN.
-- ==============================================================
good_run;
g_closed := n_closed; g_fail := n_failing; g_unp := n_unproven;
g_nc := n_no_checker; g_pct := closure_pct; g_signed := signed_off;
report " the plan, after a clean regression:";
report " rows ......................... " & integer'image(NROWS);
report " CLOSED (checked, exercised, silent) ... " & integer'image(g_closed);
report " FAILING (a checker fired) ............. " & integer'image(g_fail);
report " UNPROVEN (a checker never exercised) ... " & integer'image(g_unp);
report " NO CHECKER (and there will not be one) . " & integer'image(g_nc);
report " closure over rows that HAVE a checker .. " & integer'image(g_pct) & "%";
report " signed off ............................. " & std_logic'image(g_signed);
report " row 15 (bit order) is " & state_name(st(15)) &
", row 16 (one-flop sync) is " & state_name(st(16)) &
", row 17 (async reset release) is " & state_name(st(17));
if g_closed /= 15 or g_fail /= 0 or g_unp /= 0 or g_nc /= 3 then
report " FAIL: the clean run's classification is wrong";
errors <= errors + 1; wait for 1 ns;
end if;
if g_pct /= 100 or g_signed /= '1' then
report " FAIL: the clean run did not sign off";
errors <= errors + 1; wait for 1 ns;
end if;
report " 1. fifteen rows CLOSED, three with NO CHECKER, and closure reported as 100% OF THE ROWS THAT HAVE A CHECKER -- with the other three counted in neither the numerator nor the denominator. Those three are bit order, which Chapter 16.1 proved no pin observation can decide; a one-flop synchroniser; and an asynchronous reset release, both of which Chapter 15.9 showed are behaviourally identical to correct designs in every simulation. They are covered by REVIEW, named in the plan, and excluded from the percentage -- because folding them in as covered overstates the signoff and folding them in as gaps produces a number that can never close";
-- ==============================================================
-- 2. A DROPPED CHECKER.
-- ==============================================================
good_run;
wait until falling_edge(clk);
exercised(3) <= '0'; -- one checker present but never reached
do_sample;
d_closed := n_closed; d_unp := n_unproven; d_pct := closure_pct; d_signed := signed_off;
report " with ONE checker present but never exercised:";
report " CLOSED ....... " & integer'image(d_closed) & " UNPROVEN ....... " &
integer'image(d_unp);
report " closure ...... " & integer'image(d_pct) & "% signed off ..... " &
std_logic'image(d_signed);
report " row 3 is now " & state_name(st(3));
if d_unp /= 1 or d_closed /= 14 then
report " FAIL: an unexercised checker was not classified as unproven";
errors <= errors + 1; wait for 1 ns;
end if;
if d_signed /= '0' then
report " FAIL: the report signed off with an unproven row";
errors <= errors + 1; wait for 1 ns;
end if;
if d_pct >= g_pct then
report " FAIL: closure did not fall when a checker went unexercised";
errors <= errors + 1; wait for 1 ns;
end if;
report " 2. the row moved out of CLOSED and into UNPROVEN, closure fell from " &
integer'image(g_pct) & "% to " & integer'image(d_pct) &
"%, and the signoff went false. That is what makes the report un-gameable: DELETING A CHECK CANNOT IMPROVE IT. A percentage built on pass/fail would have been unchanged, because a checker that never ran and a checker that passed print the same line";
-- ==============================================================
-- 3. A FIRING CHECKER IS FAILING, NOT UNPROVEN.
-- ==============================================================
good_run;
wait until falling_edge(clk);
fired(5) <= '1';
exercised(5) <= '0'; -- fired AND reported no exercises -- a contradiction
do_sample;
f_closed := n_closed; f_fail := n_failing; f_unp := n_unproven; f_signed := signed_off;
report " with ONE checker that fired AND reported no exercises:";
report " CLOSED " & integer'image(f_closed) & " FAILING " & integer'image(f_fail) &
" UNPROVEN " & integer'image(f_unp) & " signed off " &
std_logic'image(f_signed);
report " row 5 is " & state_name(st(5));
if f_fail /= 1 or f_unp /= 0 then
report " FAIL: a row that fired was classified as unproven rather than failing";
errors <= errors + 1; wait for 1 ns;
end if;
if f_signed /= '0' then
report " FAIL: the report signed off with a failing row";
errors <= errors + 1; wait for 1 ns;
end if;
report " 3. the row was classified FAILING, not UNPROVEN. The two tests are ordered deliberately -- fired first, exercised second -- because a checker that fired was obviously reached, and the other ordering files a real bug as a stimulus gap. That is a one-line decision inside the aggregator and it decides which of two very different bug reports a team receives";
-- ==============================================================
-- 4. AN EMPTY PLAN DOES NOT SIGN OFF.
-- ==============================================================
wait until falling_edge(clk);
has_checker <= (others => '0');
exercised <= (others => '0');
fired <= (others => '0');
do_sample;
e_pct := closure_pct; e_signed := signed_off;
report " with NO row having a checker at all:";
report " closure " & integer'image(e_pct) & "% NO CHECKER " &
integer'image(n_no_checker) & " signed off " & std_logic'image(e_signed);
if e_signed /= '0' or e_pct /= 0 then
report " FAIL: an empty plan signed off";
errors <= errors + 1; wait for 1 ns;
end if;
report " 4. closure zero and the verdict false. `100% of nothing` is the arithmetic every vacuous report is built on, and the guard against it is the same one Chapter 16.1 put on its rules and Chapter 17.1 on its properties: require that something was actually checked before believing that everything passed";
wait for 1 ns;
if errors = 0 then
report "PASS: closure is a question about a PLAN, not a percentage, because a plan row can be in four states and a percentage collapses them to one. CLOSED means a checker exists, was exercised, and stayed silent; FAILING means it fired; UNPROVEN means it exists and was NEVER REACHED -- which is the state every dead checker in this curriculum was found in, and which prints the same green line as CLOSED in any pass/fail summary; and NO CHECKER means there is not going to be one. Aggregating the whole module's eighteen rows: fifteen CLOSED, three NO CHECKER, and closure reported as " &
integer'image(g_pct) &
"% OF THE ROWS THAT HAVE A CHECKER, with bit order, the one-flop synchroniser and the asynchronous reset release counted in neither the numerator nor the denominator -- named in the plan, covered by review, and excluded from the number, because folding them in as covered overstates the signoff and folding them in as gaps produces a figure that can never close. Leaving one checker unexercised moved its row out of CLOSED, dropped closure to " &
integer'image(d_pct) &
"% and made the verdict false, which is what makes the report un-gameable: DELETING A CHECK CANNOT IMPROVE IT. A row that both fired and reported no exercises was classified FAILING rather than UNPROVEN, because the two tests are ordered fired-first -- the other ordering files a real bug as a stimulus gap. And a plan in which no row has a checker reports zero and refuses to sign off, because `100% of nothing` is the arithmetic every vacuous report is built on. In VHDL each row's state is a named ENUMERATION value rather than a bit in one of four parallel vectors, which is where a row ends up counted twice or not at all"
severity note;
else
report "FAIL: " & integer'image(errors) & " error(s)" severity error;
end if;
done_sim <= true;
wait for 100 ns;
std.env.stop;
end process main;
end architecture tb;6. The Same Report From A UVM Environment
Reviewed code, per Chapter 16.3's toolchain note. The components already hold every number this needs; what is missing in most environments is the component that reads them.
// A plan row, and the three things it must carry beyond its name. `has_checker` is what makes a
// NO CHECKER row a declared fact rather than an inference from silence, and `covered_by` is the
// field a signoff meeting actually asks about.
typedef struct {
string name;
bit has_checker;
string covered_by; // "assertion", "scoreboard", "coverage", "review"
string reason; // why there is no checker, for the rows that have none
} spi_plan_row_t;
class spi_closure extends uvm_component;
`uvm_component_utils(spi_closure)
spi_plan_row_t plan[$];
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// The eight pin rules and the seven properties, abbreviated.
foreach (rule_names[i]) plan.push_back('{rule_names[i], 1, "assertion", ""});
foreach (prop_names[i]) plan.push_back('{prop_names[i], 1, "assertion", ""});
// THE THREE ROWS WITH NO CHECKER, declared with their reasons. This is the part that is
// normally a paragraph in a document nobody opens at signoff.
plan.push_back('{"bit order", 0, "scoreboard",
"no pin observation decides it; both orders are well-formed frames"});
plan.push_back('{"one-flop synchroniser", 0, "review",
"behaviourally identical in simulation; a simulator models no settling time"});
plan.push_back('{"async reset release", 0, "review",
"behaviourally identical in simulation; a simulated flop leaves reset cleanly"});
endfunction
// The four states, from the numbers the environment already has. `exercised` comes from an
// ANTECEDENT COVER for an assertion row and from a match count for a scoreboard row -- and a row
// with neither is a row whose verdict is guesswork.
function void report_phase(uvm_phase phase);
int closed, failing, unproven, no_checker;
foreach (plan[i]) begin
if (!plan[i].has_checker) begin
no_checker++;
`uvm_info("CLOSURE", $sformatf("NO CHECKER %s -- %s (covered by %s)",
plan[i].name, plan[i].reason, plan[i].covered_by), UVM_LOW)
end
// FIRED IS TESTED FIRST. A row that both fired and shows no exercises is a real failure, and
// the other ordering files it as a stimulus gap.
else if (fired(i)) begin
failing++;
`uvm_error("CLOSURE_FAIL", $sformatf("FAILING %s", plan[i].name))
end
else if (!exercised(i)) begin
unproven++;
// A uvm_error, not a warning. An unproven row is not a lesser kind of pass -- it is the
// absence of a result, and a warning is how it survives to signoff.
`uvm_error("CLOSURE_UNPROVEN",
$sformatf("UNPROVEN %s -- its checker was never exercised", plan[i].name))
end
else closed++;
end
// Closure over the rows that HAVE a checker, with the no-checker rows reported separately.
if (closed + failing + unproven > 0)
`uvm_info("CLOSURE", $sformatf("closure %0d%% of %0d checkable rows; %0d rows have no checker",
(closed * 100) / (closed + failing + unproven),
closed + failing + unproven, no_checker), UVM_LOW)
// AND THE VACUITY GUARD ON THE VERDICT ITSELF.
if (closed + failing + unproven == 0)
`uvm_error("CLOSURE_EMPTY", "no plan row has a checker; there is nothing to sign off")
endfunction
endclassTwo lines in that class are the whole argument. exercised(i) must come from an antecedent cover for an assertion row, not from the absence of a failure — otherwise UNPROVEN and CLOSED are the same state. And the unproven case is a uvm_error, not a warning: an unproven row is not a lesser kind of pass, it is the absence of a result.
7. Why a Verification Engineer Cares
Because this report is what a signoff meeting is actually about, and a coverage percentage cannot survive the two questions that get asked.
How many of your checks have ever fired? is answered by the FAILING and UNPROVEN columns. What is not checked at all? is answered by the NO CHECKER rows with their reasons — and answering it from memory, in a meeting, is how a real gap becomes a known limitation nobody wrote down.
The structural property worth insisting on is the one measured in section 4: deleting a check must not improve the report. Any metric where removing a failing assertion raises the number is a metric that rewards the wrong action under schedule pressure, and pass/fail percentages have exactly that shape.
And the ordering detail is worth carrying into any aggregator: test FAILING before UNPROVEN. The contradiction that makes it matter — a row that fired and reports no exercises — is produced routinely by counter clears and merges, and the classification decides whether a team debugs a bug or discusses stimulus.
8. Why an FPGA or ASIC Engineer Cares
Because the three NO CHECKER rows are the ones you are carrying into silicon, and they are the ones a tape-out review should spend its time on.
Bit order is decidable by a comparison and is therefore somebody's scoreboard row. A one-flop synchroniser and an asynchronously released reset are not decidable by any simulation — Chapter 15.9 measured five broken crossings and found that three of five faults were catchable and two were behaviourally identical to correct designs in every run. Those two are the ones that come back as a yield or temperature problem.
So the plan row that reads NO CHECKER -- covered by review is a commitment to an activity, not a gap. The useful question at a tape-out review is not what is our coverage but who did that review, when, and against which files — and a closure report that names the activity is what makes the question askable.
9. Failure Signature — A Signed-Off Block With A Dead Checker
Symptom a block is signed off at 100% coverage with a clean
regression. A bug escapes on a path the plan lists a checker
for.
What happened the checker's antecedent had been unreachable for months. The
closure report was built from pass/fail, so the row read as
CLOSED -- a checker that never ran and a checker that passed
print the same line.
What would have a closure report with an UNPROVEN state, fed by antecedent
caught it covers rather than by the absence of failures. The row would
have moved out of CLOSED on the commit that broke it, and the
closure number would have fallen.
The tell the failing row's checker has a narrow precondition -- a gap,
a coincidence, a mode. Continuous obligations are exercised by
any traffic and rarely go dead. Rows with narrow preconditions
go dead quietly and stay dead, which is Chapter 16.1's
failure signature reappearing at signoff.10. Common Misconceptions
"Closure is a coverage percentage." Coverage answers what did the stimulus reach. Closure also needs does a checker exist, did it run, and did it fire — four states that a single percentage collapses into one.
"A row with no checker is a gap." It is a gap only if nobody named it. Named, with a reason and the activity that covers it instead, it is a known limitation with an owner. Unnamed, it is a surprise at signoff.
"The no-checker rows should count against coverage." Then the number can never close, which is Chapter 17.3's full-cross problem in a different report. They should be excluded from the number and printed next to it.
"An unproven row is a minor issue." It is the absence of a result. Recorded as a warning it survives to signoff; recorded as an error it blocks, which is what it is.
"A row that fired and shows no exercises is a stimulus problem." It is a failure. A checker that fired was reached, whatever its exercise counter says, and the classification order inside the aggregator is what decides whether the team debugs or discusses.
"Merging closure works like merging coverage." Coverage merges as OR. CLOSED merges as AND — a row is closed only if it closed in every run that exercised it — and getting that backwards lets one lucky run sign off a row the regression failed.
11. Reason It Through
Why must the no-checker rows be excluded from the closure percentage rather than counted as gaps?
Because counted as gaps the number can never reach full, which turns it into a standing agenda item and destroys its usefulness — the same failure as Chapter 17.3's four-way cross. Excluded and printed alongside, the percentage is about the rows a checker can decide and the others are a named list with reasons.
Closure fell from 100% to 93% when one checker was left unexercised. Why is that the most important property of the metric?
Because it means deleting a check cannot improve the report. Under schedule pressure, any metric that rises when a failing assertion is removed rewards removing it — and a pass/fail percentage has exactly that shape, since a checker that never ran and one that passed are indistinguishable in it.
A row both fired and reported zero exercises. Which state is correct, and what produces that contradiction in practice?
FAILING. A checker that fired was reached whatever its counter says. The contradiction comes from counters cleared in the wrong place, an exercise count gated by something the failure path is not, or a merge across runs — all routine, which is why the ordering has to be deliberate.
Why is exercised for an assertion row required to come from an antecedent cover rather than from the absence of a failure?
Because the absence of a failure is exactly what an unreachable assertion produces. Using it as evidence of exercise makes UNPROVEN and CLOSED the same state, which is the defect the whole report exists to expose.
CLOSED merges as AND and coverage merges as OR. Explain why they differ.
Coverage asks was this reached anywhere, so any run reaching a bin is sufficient. CLOSED asks did this hold everywhere it was tested, so a single run in which it failed is disqualifying. Merging CLOSED as OR lets one lucky run sign off a row the rest of the regression failed.
12. Understanding Check
13. Summary
Closure is a question about a plan, not a percentage, because a plan row can be in four states and a percentage collapses them to one. CLOSED means a checker exists, was exercised, and stayed silent; FAILING means it fired; UNPROVEN means it exists and was never reached — the state every dead checker in this curriculum was found in, and the one that prints the same green line as CLOSED in any pass/fail summary; and NO CHECKER means there is not going to be one. Aggregating this module's eighteen rows: fifteen CLOSED, three NO CHECKER, and closure reported as 100% of the rows that have a checker, with bit order, the one-flop synchroniser and the asynchronous reset release in neither the numerator nor the denominator — named in the plan, covered by scoreboard or review, and excluded from the number. Leaving one checker unexercised moved its row out of CLOSED, dropped closure to 93% and made the verdict false, which is what makes the report un-gameable: deleting a check cannot improve it. A row that both fired and reported no exercises was classified FAILING rather than UNPROVEN, because the two tests are ordered fired-first — the other ordering files a real bug as a stimulus gap. And a plan in which no row has a checker reports zero and refuses to sign off, because 100% of nothing is the arithmetic every vacuous report is built on.
14. What Comes Next
The SPI verification environment is complete: a plan whose rows carry checkers and the evidence that they ran, a transaction object that can reach the space, an interface that holds the sampling discipline, a driver that owns the timing, a monitor that reads only pins, an independent reference model behind a scoreboard, agents whose passivity is structural, properties measured for vacuity and incompleteness, a coverage model whose crosses trace to mechanisms, constraints that fail loudly, a sequencer whose arbitration is a recorded intent, corner cases with both polarities, and a closure report that cannot be gamed by deleting a check.
What remains is the part no simulation settles. Three rows say NO CHECKER — covered by review, and the next module is about the reviews and the silicon-level arguments those rows are commitments to.
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
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.
- Related topic
CXL Functional Coverage
Coverage is a percentage of a denominator somebody chose. This chapter builds bin partitioning, crosses, exclusions, weighting, hit thresholds, model completeness, the closure curve, sampling points, cost and the assembled sign-off.
- Related topic
Coverage
A six-dimension cross over this MAC declares 860 160 cells; 54.3% of them are reachable and a loopback topology reaches 13.6% — so 100% means three different things.
