SPI · Module 20
Constrained Random and Coverage Closure
Reviewing the random generator before trusting it, where one trap makes 45 percent of draws illegal and another yields a perfectly uniform histogram over a period-four sequence, then closing 17 holes by classifying them.
Chapter 20.5 built checkers and fed them twelve hand-written transactions. This chapter replaces the table with a generator, and starts by reviewing the generator — because a constrained random test can be wrong because its constraints are wrong, and then every number it produces is confident and meaningless.
One of the two traps below yields a perfectly uniform histogram over a sequence whose period is four. A distribution review that plots histograms passes it.
1. The Generator Is Hand-Built, For Two Reasons
$urandom, randomize() with constraints, and covergroup are all SystemVerilog, and the simulator these examples run in rejects covergroup outright:
cg.sv:3: syntax error
cg.sv:3: error: Invalid module item.That is the first reason. The second is more interesting: a hand-written linear congruential generator produces the same stream in SystemVerilog, Verilog-2001 and VHDL. That turns the three-language comparison from all three covered roughly the same ground into all three saw the identical 160 transactions and agreed on every bin. A randomised bench whose languages disagree cannot be compared at all — the coverage tables are of different tests.
s = s * 1664525 + 1013904223 (mod 2^32)Verilog truncates the product to 32 bits by context; VHDL's multiplication of two 32-bit unsigneds returns 64 bits, so the truncation is written out. Those agree because (a·b mod 2^32 + c) mod 2^32 = (a·b + c) mod 2^32, and the evidence that they agree in practice is that all three transcripts — every coverage count and both trap measurements — come out identical.
2. Trap One: Signed Modulo
value % 13 is the obvious way to get a width in the range 4 to 16. If value is signed — and $random is signed, which is what makes this a real bug rather than a curiosity — the remainder takes the sign of the dividend, so the result ranges −12 to +12.
Measured over 2000 draws:
signed 4 + (s % 13) : min -8 max 16 illegal 891 of 2000
unsigned 4 + (hi % 13) : min 4 max 16 illegal 0 of 2000891 of 2000 draws — 44.6% — are outside the legal range, and the constraint reads as though it says 4 to 16. Those draws do not vanish: they become requests the controller refuses, so a suite built on this generator spends nearly half its cycles exercising REQ-ERR-001 and believing it is exercising the width range. The coverage report would show the width bins thinly populated and the explanation would look like bad luck.
3. Trap Two: A Uniform Histogram Is Not Randomness
The low bits of a power-of-two-modulus LCG have short periods. Drawing two bits from the bottom gives this:
low bits[1:0] histogram 500 500 500 500
high bits[17:16] histogram 500 511 489 500The low-bit histogram is perfect — exactly 25% in each bin over 2000 draws. The high-bit one is slightly ragged, which is what a real random source looks like. A distribution review that stops at the histogram would prefer the low bits.
Now look at the sequences:
low bits[1:0] sequence 3 2 1 0 3 2 1 0 3 2 1 0 3 2 1 0
high bits[17:16] sequence 3 0 3 0 2 2 3 0 0 3 1 0 0 0 2 2The low bits have a period of four. They are not random at all; they are a counter. Any dimension drawn from them cycles deterministically, so a cross between two such dimensions can only ever reach the combinations that the two periods happen to align on — and the per-dimension histograms remain flawless while the cross is systematically starved.
4. The Coverage Model
Plain arrays of hit counts. Every bin answers the question what plausible bug survives if this is never exercised? — and a bin that cannot answer it is not in the model.
| Coverpoint | Bins | The bug it would let through |
|---|---|---|
cp_mode | 4 — one per CPOL/CPHA pair | a mode-decode fault in an unvisited mode |
cp_order | 2 — MSB, LSB | the alignment path is separate per order |
cp_width | 4 — exactly 4, 5–8, 9–12, 13–16 | boundary shifts and the 16-bit mask |
cp_div | 3 — 0, 1–7, 8 and above | cfg_div = 0 is the one-cycle half-period edge |
cp_dev | 4 — one per decoder output | a decoder that drops its last output |
| Cross | Bins | Why this cross and not another |
|---|---|---|
x_mode_order | 8 | mode and bit order are handled by different logic that meets in the datapath |
x_mode_width | 16 | the alignment shift depends on width; the launch edge depends on mode |
x_width_order | 8 | the reversal is width-dependent, and only in one order |
Three crosses, 32 bins, and no full Cartesian product — mode × order × width × div × dev would be 1536 bins whose occupancy would say nothing about any specific fault.
Two bins are excluded from the denominator, each with a reason, and each asserted to stay at zero:
illegal a width below 4 that produced a frame REQ-ERR-001 forbids it
unreach two chip selects low at once one index, one decoder5. Phase A — A Deliberately Narrow Constraint Set
40 transactions with cfg_width drawn from 4 to 8 and cfg_div from 1 to 3.
C2 phase A: 40 transactions, width 4..8, div 1..3
scored 40 transactions, 0 failures, 17 coverage holes
C3 coverage after phase A
cp_mode 4/ 4 holes 0
cp_order 2/ 2 holes 0
cp_width 2/ 4 holes 2
cp_div 1/ 3 holes 2
cp_dev 4/ 4 holes 0
x_mode_order 8/ 8 holes 0
x_mode_width 7/16 holes 9
x_width_order 4/ 8 holes 4Zero functional failures and 17 holes. The single-dimension coverpoints look healthy — mode, order and device are all complete — and the damage is concentrated in the crosses, which is the normal shape of a narrow constraint set.
6. Classification, Which Is The Step That Matters
C4 hole classification
CONSTRAINT BUG width range 4..8 cannot reach bins 9-12, 13-16
CONSTRAINT BUG div range 1..3 cannot reach bin 0 or bin 8+
DEPENDENT 13 cross holes follow from the 2 above, not from
separate stimulus gaps -- fixing the ranges fixes themSeventeen holes, two root causes. The thirteen cross holes are not independent gaps; they are the arithmetic consequence of two unreachable ranges, and every one of them closes when the ranges widen.
That distinction decides what to do next. Thirteen separate targeted sequences would be thirteen pieces of throwaway code that a single constraint change makes redundant. The classification is what turns a coverage report into a work list.
| Class | Meaning | Correct action |
|---|---|---|
| constraint bug | the generator cannot produce the value | widen or fix the constraint |
| dependent | follows from another hole | fix the cause, re-measure |
| stimulus gap | reachable, but the scenario is never assembled | write a targeted sequence |
| monitor bug | it happened and was not sampled | fix the sampling, not the stimulus |
| coverage-model bug | the bin is mis-specified | fix the bin |
| unreachable | the design makes it impossible | exclude, with the reason recorded |
7. Phase B, And The Hole Random Does Not Close
Widen the width to 4–16 and run 45 more. The divider is deliberately left at 0–7, so bin 8-and-above stays out of reach.
C5 phase B: 45 transactions, width 4..16, div 0..7
widened random leaves 2 holes
C6 targeted closure
2 targeted transactions added
C7 final coverage
cp_mode 4/ 4
cp_order 2/ 2
cp_width 4/ 4
cp_div 3/ 3
cp_dev 4/ 4
x_mode_order 8/ 8
x_mode_width 16/16
x_width_order 8/ 8
remaining holes 0That shape is the normal end state of a random campaign: it closes the bulk cheaply and then stops improving, and the last bins have to be constructed. The targeted transactions are built for the specific bin — pick the mode and the width the empty cross bin names, and fire exactly that — which is the only way to close a hole deterministically rather than by running more cycles and hoping.
8. The Two Excluded Bins
excluded, not chased:
illegal width < 4 accepted hits 0 (REQ-ERR-001 forbids it)
unreach two selects low hits 0 (one index, one decoder)
the illegal bin was offered a width of 3 and stayed 0The last line is the part that makes the exclusions worth anything. A counter that stays at zero is indistinguishable from a counter that is not wired to anything — so the bench offers an illegal width of 3 at the end of the run and confirms the controller refuses it and the counter stays zero. Without that, both excluded bins would be two integers that were never going to change.
The unreachable one is excluded rather than removed, and the difference is documentation. Deleting it loses the record that somebody considered two-selects-low, established that the architecture makes it impossible, and decided not to chase it. Keeping it as an excluded bin with a stated reason means the next person to add a broadcast mode finds the note.
9. Code Coverage, Which Measures Something Else Entirely
nvc collects statement and branch coverage, so this is a measured result rather than a methodological aside. On the VHDL controller under the directed suite alone:
statement total 12 hit 11 UNHIT 1
branch total 4 hit 3 UNHIT 1— that from a deliberately tiny probe design, used here to show the tool reports holes and not only hits, which is the only property that makes a coverage tool useful.
The distinction worth being precise about:
| Functional coverage | Code coverage | |
|---|---|---|
| Asks | was this scenario exercised? | was this line executed? |
| Written by | a person, from the specification | the tool, from the source |
| 100% means | every scenario I thought of happened | every line ran at least once |
| Blind to | a scenario nobody modelled | a scenario that shares lines with another |
| Cannot detect | a missing requirement | a missing requirement |
Neither is verification. High code coverage does not prove protocol completeness: this controller's mode decoder is three lines, and a suite that only ever ran mode 0 would execute all three of them and report 100% statement coverage on the part of the design most likely to be wrong. High functional coverage does not prove structural exploration: the functional model has no bin for the default arm of the state machine, so a suite could close every functional bin without ever entering it.
And neither replaces the assertions, the scoreboard, or the review. A single percentage is not a verification status, and a report that leads with one is hiding what it did not measure.
10. The Constrained Random Bench
// spi_capstone_crv_tb.sv
//
// Chapter 20.6 -- constrained random stimulus, and what closing coverage actually is.
//
// WHY THIS BENCH BUILDS ITS OWN RANDOM GENERATOR
//
// Two reasons, and the second is the interesting one.
//
// (1) `$urandom`, `randomize()` with constraints, and `covergroup` are all
// SystemVerilog, and the simulator these examples run in rejects covergroup
// outright:
//
// cg.sv:3: syntax error
// cg.sv:3: error: Invalid module item.
//
// (2) A hand-written linear congruential generator produces the SAME STREAM in
// SystemVerilog, Verilog-2001 and VHDL. That turns the three-language comparison
// from "all three covered roughly the same ground" into "all three saw the
// identical 160 transactions and agreed on every bin". A randomised bench whose
// languages disagree cannot be compared at all.
//
// The generator is deliberately the textbook one:
//
// s = s * 1664525 + 1013904223 (mod 2^32)
//
// and it carries two traps this chapter measures rather than warns about.
//
// TRAP 1 -- SIGNED MODULO
//
// `value % 13` is the obvious way to get a width. If `value` is SIGNED -- and
// `$random` is signed, which is what makes this a real bug rather than a curiosity --
// the result ranges -12..+12, so `4 + (value % 13)` produces widths from -8 to 16.
// Every negative one is an illegal configuration the generator was never meant to
// produce, and the constraint LOOKS like it says 4..16.
//
// TRAP 2 -- A UNIFORM HISTOGRAM IS NOT EVIDENCE OF RANDOMNESS
//
// The low bits of this LCG have period 4. Drawing 2 bits from the bottom gives a
// PERFECTLY uniform histogram -- exactly 25% each over any multiple of four draws --
// and a completely deterministic repeating sequence. A distribution review that only
// plots histograms passes it. Printing the sequence fails it immediately.
//
// WHAT COVERAGE CLOSURE MEANS HERE
//
// Phase A runs a deliberately NARROW constraint set and reports the holes. Phase B
// classifies every hole, fixes the constraints that caused them, re-runs, and closes
// whatever the widened random stimulus still misses with TARGETED transactions
// constructed for the specific bin.
//
// Two bins are excluded from the denominator rather than chased, each with a reason,
// and both are asserted to stay at zero. "100% coverage" with an unreachable bin
// quietly deleted is not closure, it is arithmetic.
`timescale 1ns/1ps
module spi_capstone_crv_tb;
reg clk, rst_n;
reg cfg_cpol, cfg_cpha, cfg_lsb;
reg [4:0] cfg_width;
reg [7:0] cfg_div;
reg [1:0] cfg_dev;
reg [3:0] cfg_lead, cfg_lag, cfg_idle;
reg start, abort;
reg [15:0] tx_data;
wire busy, done, cfg_err;
wire [15:0] rx_data;
wire [4:0] bits_done;
wire sclk, mosi;
wire [3:0] cs_n;
wire miso;
integer n_chk, n_err, n_neg;
spi_capstone_ctrl #(.DATA_W(16), .MIN_WIDTH(4), .NDEV(4)) dut (
.clk(clk), .rst_n(rst_n),
.cfg_cpol(cfg_cpol), .cfg_cpha(cfg_cpha), .cfg_lsb_first(cfg_lsb),
.cfg_width(cfg_width), .cfg_div(cfg_div), .cfg_dev(cfg_dev),
.cfg_lead(cfg_lead), .cfg_lag(cfg_lag), .cfg_idle(cfg_idle),
.start(start), .tx_data(tx_data), .abort(abort),
.busy(busy), .done(done), .cfg_err(cfg_err),
.rx_data(rx_data), .bits_done(bits_done),
.sclk(sclk), .mosi(mosi), .cs_n(cs_n), .miso(miso)
);
always #5 clk = ~clk;
// ---------------- oracle (same arithmetic as 20.5) -------------------------
function [15:0] mask;
input [4:0] w;
reg [16:0] one;
begin one = 17'd1; mask = ((one << w) - 17'd1); end
endfunction
function [15:0] revw;
input [15:0] v; input [4:0] w;
integer b;
begin
revw = 16'd0;
for (b = 0; b < 16; b = b + 1) if (b < w) revw[w-1-b] = v[b];
end
endfunction
function [15:0] ref_rx;
input [15:0] sw; input [4:0] w; input lsb;
begin ref_rx = lsb ? revw(sw & mask(w), w) : (sw & mask(w)); end
endfunction
function [15:0] ref_slave_rx;
input [15:0] tx; input [4:0] w; input lsb;
begin ref_slave_rx = lsb ? revw(tx & mask(w), w) : (tx & mask(w)); end
endfunction
// ---------------- the generator -------------------------------------------
reg [31:0] lcg;
task step_lcg;
begin
lcg = lcg * 32'd1664525 + 32'd1013904223;
end
endtask
// Draw from the HIGH half. Bits [30:16] are used, never the bottom ones -- see
// trap 2 and the measurement in group C1.
function [14:0] draw;
input [31:0] s;
begin draw = s[30:16]; end
endfunction
// ---------------- pin-level device model ----------------------------------
reg slv_cpol, slv_cpha;
reg [15:0] slv_word, slv_sr, slv_rx;
reg [4:0] slv_w, slv_idx, slv_nrx;
reg slv_miso, lead_s;
wire cs_any = ~(&cs_n);
assign miso = slv_miso;
always @(posedge cs_any) begin
slv_sr = slv_word << (16 - slv_w);
slv_rx = 16'd0;
slv_nrx = 5'd0;
if (!slv_cpha) begin
slv_miso = slv_sr[15]; slv_sr = slv_sr << 1; slv_idx = 5'd1;
end else begin
slv_miso = 1'b0; slv_idx = 5'd0;
end
end
always @(sclk) begin
if (cs_any === 1'b1) begin
lead_s = (sclk !== slv_cpol);
if (slv_cpha ? !lead_s : lead_s) begin
if (slv_nrx < slv_w) begin
slv_rx = {slv_rx[14:0], mosi}; slv_nrx = slv_nrx + 5'd1;
end
end
if (slv_cpha ? lead_s : !lead_s) begin
if (slv_idx < slv_w) begin
slv_miso = slv_sr[15]; slv_sr = slv_sr << 1;
slv_idx = slv_idx + 5'd1;
end
end
end
end
// ---------------- coverage model ------------------------------------------
// Plain arrays of hit counts. Every bin below answers a question of the form
// "what plausible bug survives if this is never exercised?" -- a bin that cannot
// answer it is not in the model.
integer cp_mode [0:3]; // CPOL/CPHA -- a mode-decode bug hides in an unvisited mode
integer cp_order [0:1]; // MSB/LSB -- the alignment path is separate per order
integer cp_width [0:3]; // 4 / 5-8 / 9-12 / 13-16 -- boundary and mid widths
integer cp_div [0:2]; // 0 / 1-7 / 8+ -- div=0 is the one-cycle half-period edge
integer cp_dev [0:3]; // which decoder output
integer x_mode_order [0:7];
integer x_mode_width [0:15];
integer x_width_order[0:7];
// EXCLUDED from the denominator, each for a stated reason, each asserted to be 0.
integer ex_illegal_accepted; // a width < 4 that produced a frame: REQ-ERR-001 says no
integer ex_two_selects; // two selects low: unreachable, one index -> one decoder
function integer wbin;
input [4:0] w;
begin
if (w == 5'd4) wbin = 0;
else if (w <= 5'd8) wbin = 1;
else if (w <= 5'd12) wbin = 2;
else wbin = 3;
end
endfunction
function integer dbin;
input [7:0] d;
begin
if (d == 8'd0) dbin = 0;
else if (d <= 8'd7) dbin = 1;
else dbin = 2;
end
endfunction
task sample_cov;
input cpol_i; input cpha_i; input lsb_i; input [4:0] w;
input [7:0] d; input [1:0] dv;
integer m, o, wb;
begin
m = {cpol_i, cpha_i};
o = lsb_i ? 1 : 0;
wb = wbin(w);
cp_mode[m] = cp_mode[m] + 1;
cp_order[o] = cp_order[o] + 1;
cp_width[wb] = cp_width[wb] + 1;
cp_div[dbin(d)] = cp_div[dbin(d)] + 1;
cp_dev[dv] = cp_dev[dv] + 1;
x_mode_order[m*2 + o] = x_mode_order[m*2 + o] + 1;
x_mode_width[m*4 + wb] = x_mode_width[m*4 + wb] + 1;
x_width_order[wb*2 + o] = x_width_order[wb*2 + o] + 1;
end
endtask
function integer holes_in;
input integer which; // 0..7 selects the coverpoint / cross
integer i, h;
begin
h = 0;
if (which == 0) for (i=0;i<4;i=i+1) if (cp_mode[i]==0) h=h+1;
if (which == 1) for (i=0;i<2;i=i+1) if (cp_order[i]==0) h=h+1;
if (which == 2) for (i=0;i<4;i=i+1) if (cp_width[i]==0) h=h+1;
if (which == 3) for (i=0;i<3;i=i+1) if (cp_div[i]==0) h=h+1;
if (which == 4) for (i=0;i<4;i=i+1) if (cp_dev[i]==0) h=h+1;
if (which == 5) for (i=0;i<8;i=i+1) if (x_mode_order[i]==0) h=h+1;
if (which == 6) for (i=0;i<16;i=i+1) if (x_mode_width[i]==0) h=h+1;
if (which == 7) for (i=0;i<8;i=i+1) if (x_width_order[i]==0) h=h+1;
holes_in = h;
end
endfunction
// A TASK, not a function, and the reason is a Verilog-2001 rule that catches
// everyone once: a FUNCTION MUST HAVE AT LEAST ONE INPUT. Written as
// `function integer total_holes;` with no arguments it compiles under
// SystemVerilog and fails under -g2001 with
//
// error: Function total_holes has no ports.
// : Functions must have at least one input port.
//
// A task may have outputs only, so the total comes back through one.
task total_holes;
output integer h;
integer i;
begin
h = 0;
for (i = 0; i < 8; i = i + 1) h = h + holes_in(i);
end
endtask
// ---------------- monitors -------------------------------------------------
integer n_edge, n_frames, n_low;
reg sclk_d, cs_d;
always @(posedge clk) begin
if (!rst_n) begin
n_edge <= 0; n_frames <= 0; sclk_d <= 1'b0; cs_d <= 1'b0;
end else begin
n_low = (cs_n[0]?0:1)+(cs_n[1]?0:1)+(cs_n[2]?0:1)+(cs_n[3]?0:1);
if (n_low > 1) ex_two_selects = ex_two_selects + 1;
if (cs_any && !cs_d) n_edge <= 0;
if (!cs_any && cs_d) n_frames <= n_frames + 1;
// `cs_d` as well as `cs_any`: an edge is only a FRAME edge if a device
// was ALREADY selected last cycle. A transition in the very cycle the
// select falls is SCLK reaching its new idle level, not a clocking
// edge -- the controller parks SCLK and asserts CS together, so when
// the previous idle level differed the two coincide.
//
// Measured cost of omitting `cs_d`: the first transaction after reset
// with CPOL=1 counted 17 edges instead of 16 in VHDL and 16 in
// SystemVerilog, because a one-cycle difference in reset-release
// timing decided whether the re-park landed inside the window. The
// received data was correct in both. With the gate the measurement no
// longer depends on that phase at all.
if (cs_any && cs_d && (sclk !== sclk_d)) n_edge <= n_edge + 1;
cs_d <= cs_any; sclk_d <= sclk;
end
end
// ---------------- stimulus -------------------------------------------------
task set_cfg;
input cpol_i; input cpha_i; input lsb_i; input [4:0] w;
input [7:0] dv; input [1:0] dv_n;
begin
cfg_cpol=cpol_i; cfg_cpha=cpha_i; cfg_lsb=lsb_i;
cfg_width=w; cfg_div=dv; cfg_dev=dv_n;
cfg_lead=4'd2; cfg_lag=4'd2; cfg_idle=4'd1;
slv_cpol=cpol_i; slv_cpha=cpha_i; slv_w=w;
end
endtask
task fire;
input [15:0] d;
begin
@(negedge clk); tx_data = d; start = 1'b1;
@(negedge clk); start = 1'b0;
end
endtask
task wait_idle;
input integer maxc; output gotd;
integer g; reg seen;
begin
g=0; seen=1'b0;
while (g < maxc) begin
@(negedge clk); g=g+1;
if (done) seen=1'b1;
if (!busy && seen) g=maxc;
else if (!busy && g>4) g=maxc;
end
gotd = seen;
end
endtask
// One transaction: build it, run it, score it, sample coverage.
task do_txn;
input cpol_i; input cpha_i; input lsb_i; input [4:0] w;
input [7:0] dv; input [1:0] dv_n; input [15:0] txd; input [15:0] swd;
reg gd;
begin
set_cfg(cpol_i, cpha_i, lsb_i, w, dv, dv_n);
slv_word = swd & mask(w);
fire(txd);
wait_idle(40000, gd);
n_chk = n_chk + 1;
if (!gd) begin
n_err = n_err + 1;
$display(" FAIL no done for w=%0d div=%0d", w, dv);
end
n_chk = n_chk + 1;
if (rx_data !== ref_rx(swd & mask(w), w, lsb_i)) begin
n_err = n_err + 1;
$display(" FAIL rx w=%0d mode=%0d lsb=%0d got %04h exp %04h",
w, {cpol_i,cpha_i}, lsb_i, rx_data,
ref_rx(swd & mask(w), w, lsb_i));
end
n_chk = n_chk + 1;
if ((slv_rx & mask(w)) !== ref_slave_rx(txd, w, lsb_i)) begin
n_err = n_err + 1;
$display(" FAIL device rx w=%0d got %04h exp %04h",
w, slv_rx & mask(w), ref_slave_rx(txd, w, lsb_i));
end
n_chk = n_chk + 1;
if (n_edge !== 2 * w) begin
n_err = n_err + 1;
$display(" FAIL edges w=%0d got %0d exp %0d", w, n_edge, 2*w);
end
sample_cov(cpol_i, cpha_i, lsb_i, w, dv, dv_n);
end
endtask
integer i, j, k, m, o, wb;
integer h_before, h_after, root_causes, targeted;
integer lo_hist [0:3];
integer hi_hist [0:3];
integer smin, smax, sbad;
integer v;
reg gdx;
initial begin
clk=1'b0; rst_n=1'b0; start=1'b0; abort=1'b0; tx_data=16'd0;
cfg_cpol=1'b0; cfg_cpha=1'b0; cfg_lsb=1'b0; cfg_width=5'd8;
cfg_div=8'd1; cfg_dev=2'd0; cfg_lead=4'd2; cfg_lag=4'd2; cfg_idle=4'd1;
slv_cpol=1'b0; slv_cpha=1'b0; slv_w=5'd8; slv_word=16'd0;
slv_miso=1'b0; slv_sr=16'd0; slv_rx=16'd0; slv_idx=5'd0; slv_nrx=5'd0;
n_chk=0; n_err=0; n_neg=0; n_edge=0; n_frames=0;
sclk_d=1'b0; cs_d=1'b0;
ex_illegal_accepted=0; ex_two_selects=0;
for (i=0;i<4;i=i+1) cp_mode[i]=0;
for (i=0;i<2;i=i+1) cp_order[i]=0;
for (i=0;i<4;i=i+1) cp_width[i]=0;
for (i=0;i<3;i=i+1) cp_div[i]=0;
for (i=0;i<4;i=i+1) cp_dev[i]=0;
for (i=0;i<8;i=i+1) x_mode_order[i]=0;
for (i=0;i<16;i=i+1) x_mode_width[i]=0;
for (i=0;i<8;i=i+1) x_width_order[i]=0;
for (i=0;i<4;i=i+1) begin lo_hist[i]=0; hi_hist[i]=0; end
$display("=== Chapter 20.6 -- constrained random and coverage closure ===");
// -----------------------------------------------------------------
// C1 -- review the generator BEFORE trusting anything it produces.
// -----------------------------------------------------------------
$display(" C1 generator review, 2000 draws");
lcg = 32'h1234_5678;
smin = 999; smax = -999; sbad = 0;
for (i = 0; i < 2000; i = i + 1) begin
step_lcg;
// The trap: treat the draw as SIGNED, exactly as `$random` is.
v = 4 + ($signed(lcg) % 13);
if (v < smin) smin = v;
if (v > smax) smax = v;
if (v < 4 || v > 16) sbad = sbad + 1;
end
$display(" signed 4 + (s %% 13) : min %4d max %4d illegal %4d of 2000",
smin, smax, sbad);
lcg = 32'h1234_5678;
smin = 999; smax = -999; sbad = 0;
for (i = 0; i < 2000; i = i + 1) begin
step_lcg;
v = 4 + (draw(lcg) % 13);
if (v < smin) smin = v;
if (v > smax) smax = v;
if (v < 4 || v > 16) sbad = sbad + 1;
end
$display(" unsigned 4 + (hi %% 13) : min %4d max %4d illegal %4d of 2000",
smin, smax, sbad);
// Trap 2: low bits vs high bits.
lcg = 32'h1234_5678;
for (i = 0; i < 2000; i = i + 1) begin
step_lcg;
lo_hist[lcg[1:0]] = lo_hist[lcg[1:0]] + 1;
hi_hist[lcg[17:16]] = hi_hist[lcg[17:16]] + 1;
end
$display(" low bits[1:0] histogram %4d %4d %4d %4d",
lo_hist[0], lo_hist[1], lo_hist[2], lo_hist[3]);
$display(" high bits[17:16] histogram %4d %4d %4d %4d",
hi_hist[0], hi_hist[1], hi_hist[2], hi_hist[3]);
lcg = 32'h1234_5678;
$write(" low bits[1:0] sequence ");
for (i = 0; i < 16; i = i + 1) begin step_lcg; $write("%0d ", lcg[1:0]); end
$display("");
lcg = 32'h1234_5678;
$write(" high bits[17:16] sequence ");
for (i = 0; i < 16; i = i + 1) begin step_lcg; $write("%0d ", lcg[17:16]); end
$display("");
// A uniform low-bit histogram must NOT be accepted as evidence: the sequence
// has period 4. This is checked, not merely narrated.
n_chk = n_chk + 1;
if (lo_hist[0] == 500 && lo_hist[1] == 500 &&
lo_hist[2] == 500 && lo_hist[3] == 500) begin
n_neg = n_neg + 1;
$display(" low bits are PERFECTLY uniform and have period 4 -- rejected");
end else begin
n_err = n_err + 1;
$display(" FAIL expected a perfectly uniform low-bit histogram");
end
// Reset is RELEASED ON A NEGEDGE, for the same reason `start` is driven on one.
// Releasing it on a posedge puts the assignment in the same region as every
// clocked block that tests it, and the order is undefined: the monitor may see
// the old value or the new one. Measured cost of getting this wrong -- the
// monitor held its reset one cycle longer in VHDL than in SystemVerilog, so the
// idle re-park of SCLK to CPOL=1 was counted as a frame edge in one language
// and not the other, and the first transaction of the run reported 17 edges
// instead of 16 in exactly one of the three.
repeat (4) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (2) @(posedge clk);
// -----------------------------------------------------------------
// C2 -- PHASE A: 40 transactions under a deliberately NARROW constraint.
// -----------------------------------------------------------------
$display(" C2 phase A: 40 transactions, width 4..8, div 1..3");
lcg = 32'hACE1_0F0F;
for (i = 0; i < 40; i = i + 1) begin
step_lcg; m = draw(lcg) % 4;
step_lcg; o = draw(lcg) % 2;
step_lcg; wb = 4 + (draw(lcg) % 5); // 4..8 only
step_lcg; k = 1 + (draw(lcg) % 3); // 1..3 only
step_lcg; j = draw(lcg) % 4;
step_lcg; v = draw(lcg);
do_txn(m[1], m[0], o[0], wb[4:0], k[7:0], j[1:0],
{v[7:0], v[14:7]}, {v[3:0], v[14:3]});
end
total_holes(h_before);
$display(" scored %0d transactions, %0d failures, %0d coverage holes",
40, n_err, h_before);
// -----------------------------------------------------------------
// C3 -- report the holes per coverpoint.
// -----------------------------------------------------------------
$display(" C3 coverage after phase A");
$display(" cp_mode %2d/%2d holes %2d", 4-holes_in(0), 4, holes_in(0));
$display(" cp_order %2d/%2d holes %2d", 2-holes_in(1), 2, holes_in(1));
$display(" cp_width %2d/%2d holes %2d", 4-holes_in(2), 4, holes_in(2));
$display(" cp_div %2d/%2d holes %2d", 3-holes_in(3), 3, holes_in(3));
$display(" cp_dev %2d/%2d holes %2d", 4-holes_in(4), 4, holes_in(4));
$display(" x_mode_order %2d/%2d holes %2d", 8-holes_in(5), 8, holes_in(5));
$display(" x_mode_width %2d/%2d holes %2d", 16-holes_in(6),16, holes_in(6));
$display(" x_width_order %2d/%2d holes %2d", 8-holes_in(7), 8, holes_in(7));
// -----------------------------------------------------------------
// C4 -- classify. This is the step that gets skipped, and it is the step
// that decides whether the next run is useful.
// -----------------------------------------------------------------
$display(" C4 hole classification");
root_causes = 0;
if (cp_width[2] == 0 || cp_width[3] == 0) begin
root_causes = root_causes + 1;
$display(" CONSTRAINT BUG width range 4..8 cannot reach bins 9-12, 13-16");
end
if (cp_div[0] == 0 || cp_div[2] == 0) begin
root_causes = root_causes + 1;
$display(" CONSTRAINT BUG div range 1..3 cannot reach bin 0 or bin 8+");
end
$display(" DEPENDENT %0d cross holes follow from the %0d above, not from",
holes_in(6) + holes_in(7), root_causes);
$display(" separate stimulus gaps -- fixing the ranges fixes them");
n_chk = n_chk + 1;
if (root_causes != 2) begin
n_err = n_err + 1;
$display(" FAIL expected 2 constraint root causes, found %0d", root_causes);
end
// -----------------------------------------------------------------
// C5 -- PHASE B: widened constraints.
// -----------------------------------------------------------------
// Phase B widens WIDTH but deliberately leaves DIV at 0..7, so bin 8+ is
// still unreachable by random draw. That is not an oversight in the example:
// it is the normal end state of a random campaign. Random stimulus closes the
// bulk cheaply and then stops improving, and the last few bins have to be
// constructed. A closure loop whose targeted stage never runs has not been
// shown to work -- and with 120 transactions and div 0..11 it did not run,
// which is how this batch size was chosen.
$display(" C5 phase B: 45 transactions, width 4..16, div 0..7");
lcg = 32'h5EED_1111;
for (i = 0; i < 45; i = i + 1) begin
step_lcg; m = draw(lcg) % 4;
step_lcg; o = draw(lcg) % 2;
step_lcg; wb = 4 + (draw(lcg) % 13); // 4..16
step_lcg; k = draw(lcg) % 8; // 0..7 -- bin 8+ left open
step_lcg; j = draw(lcg) % 4;
step_lcg; v = draw(lcg);
do_txn(m[1], m[0], o[0], wb[4:0], k[7:0], j[1:0],
{v[7:0], v[14:7]}, {v[3:0], v[14:3]});
end
total_holes(h_after);
$display(" widened random leaves %0d holes", h_after);
// -----------------------------------------------------------------
// C6 -- TARGETED closure for whatever random did not reach. Constructed for
// the specific bin, which is the only way to close a hole deterministically.
// -----------------------------------------------------------------
$display(" C6 targeted closure");
targeted = 0;
for (i = 0; i < 16; i = i + 1) begin
if (x_mode_width[i] == 0) begin
m = i / 4;
wb = i % 4;
if (wb == 0) k = 4;
else if (wb == 1) k = 7;
else if (wb == 2) k = 11;
else k = 15;
do_txn(m[1], m[0], 1'b0, k[4:0], 8'd1, 2'd0, 16'h9D5A, 16'h3CB9);
targeted = targeted + 1;
end
end
for (i = 0; i < 8; i = i + 1) begin
if (x_width_order[i] == 0) begin
wb = i / 2;
o = i % 2;
if (wb == 0) k = 4;
else if (wb == 1) k = 6;
else if (wb == 2) k = 10;
else k = 14;
do_txn(1'b0, 1'b0, o[0], k[4:0], 8'd1, 2'd0, 16'h9D5A, 16'h3CB9);
targeted = targeted + 1;
end
end
for (i = 0; i < 3; i = i + 1) begin
if (cp_div[i] == 0) begin
if (i == 0) k = 0;
else if (i == 1) k = 4;
else k = 9;
do_txn(1'b0, 1'b0, 1'b0, 5'd8, k[7:0], 2'd0, 16'h9D5A, 16'h3CB9);
targeted = targeted + 1;
end
end
$display(" %0d targeted transactions added", targeted);
// -----------------------------------------------------------------
// C7 -- final coverage, and the two bins that are EXCLUDED with reasons.
// -----------------------------------------------------------------
$display(" C7 final coverage");
$display(" cp_mode %2d/%2d", 4-holes_in(0), 4);
$display(" cp_order %2d/%2d", 2-holes_in(1), 2);
$display(" cp_width %2d/%2d", 4-holes_in(2), 4);
$display(" cp_div %2d/%2d", 3-holes_in(3), 3);
$display(" cp_dev %2d/%2d", 4-holes_in(4), 4);
$display(" x_mode_order %2d/%2d", 8-holes_in(5), 8);
$display(" x_mode_width %2d/%2d", 16-holes_in(6), 16);
$display(" x_width_order %2d/%2d", 8-holes_in(7), 8);
total_holes(h_after);
$display(" remaining holes %0d", h_after);
n_chk = n_chk + 1;
if (h_after != 0) begin
n_err = n_err + 1;
$display(" FAIL %0d holes remain after targeted closure", h_after);
end
$display(" excluded, not chased:");
$display(" illegal width < 4 accepted hits %0d (REQ-ERR-001 forbids it)",
ex_illegal_accepted);
$display(" unreach two selects low hits %0d (one index, one decoder)",
ex_two_selects);
n_chk = n_chk + 1;
if (ex_illegal_accepted != 0 || ex_two_selects != 0) begin
n_err = n_err + 1;
$display(" FAIL an excluded bin was hit -- the exclusion was wrong");
end
// An illegal width is offered once, to prove the illegal bin's counter is
// wired to something and is not simply a variable that stays at zero.
set_cfg(1'b0, 1'b0, 1'b0, 5'd3, 8'd1, 2'd0);
fire(16'h1234);
if (busy) ex_illegal_accepted = ex_illegal_accepted + 1;
n_chk = n_chk + 1;
if (ex_illegal_accepted != 0) begin
n_err = n_err + 1;
$display(" FAIL an illegal width started a frame");
end else begin
n_neg = n_neg + 1;
$display(" the illegal bin was offered a width of 3 and stayed 0");
end
$display("=== SUMMARY checks=%0d negatives=%0d failures=%0d : %0s ===",
n_chk, n_neg, n_err, (n_err == 0) ? "PASS" : "FAIL");
$finish;
end
initial begin
#40000000;
$display(" FATAL global timeout");
$display("=== SUMMARY checks=%0d negatives=%0d failures=%0d : FAIL ===",
n_chk, n_neg, n_err + 1);
$finish;
end
endmodule// spi_capstone_crv_tb.sv
//
// Chapter 20.6 -- constrained random stimulus, and what closing coverage actually is.
//
// WHY THIS BENCH BUILDS ITS OWN RANDOM GENERATOR
//
// Two reasons, and the second is the interesting one.
//
// (1) `$urandom`, `randomize()` with constraints, and `covergroup` are all
// SystemVerilog, and the simulator these examples run in rejects covergroup
// outright:
//
// cg.sv:3: syntax error
// cg.sv:3: error: Invalid module item.
//
// (2) A hand-written linear congruential generator produces the SAME STREAM in
// SystemVerilog, Verilog-2001 and VHDL. That turns the three-language comparison
// from "all three covered roughly the same ground" into "all three saw the
// identical 160 transactions and agreed on every bin". A randomised bench whose
// languages disagree cannot be compared at all.
//
// The generator is deliberately the textbook one:
//
// s = s * 1664525 + 1013904223 (mod 2^32)
//
// and it carries two traps this chapter measures rather than warns about.
//
// TRAP 1 -- SIGNED MODULO
//
// `value % 13` is the obvious way to get a width. If `value` is SIGNED -- and
// `$random` is signed, which is what makes this a real bug rather than a curiosity --
// the result ranges -12..+12, so `4 + (value % 13)` produces widths from -8 to 16.
// Every negative one is an illegal configuration the generator was never meant to
// produce, and the constraint LOOKS like it says 4..16.
//
// TRAP 2 -- A UNIFORM HISTOGRAM IS NOT EVIDENCE OF RANDOMNESS
//
// The low bits of this LCG have period 4. Drawing 2 bits from the bottom gives a
// PERFECTLY uniform histogram -- exactly 25% each over any multiple of four draws --
// and a completely deterministic repeating sequence. A distribution review that only
// plots histograms passes it. Printing the sequence fails it immediately.
//
// WHAT COVERAGE CLOSURE MEANS HERE
//
// Phase A runs a deliberately NARROW constraint set and reports the holes. Phase B
// classifies every hole, fixes the constraints that caused them, re-runs, and closes
// whatever the widened random stimulus still misses with TARGETED transactions
// constructed for the specific bin.
//
// Two bins are excluded from the denominator rather than chased, each with a reason,
// and both are asserted to stay at zero. "100% coverage" with an unreachable bin
// quietly deleted is not closure, it is arithmetic.
`timescale 1ns/1ps
module spi_capstone_crv_tb;
reg clk, rst_n;
reg cfg_cpol, cfg_cpha, cfg_lsb;
reg [4:0] cfg_width;
reg [7:0] cfg_div;
reg [1:0] cfg_dev;
reg [3:0] cfg_lead, cfg_lag, cfg_idle;
reg start, abort;
reg [15:0] tx_data;
wire busy, done, cfg_err;
wire [15:0] rx_data;
wire [4:0] bits_done;
wire sclk, mosi;
wire [3:0] cs_n;
wire miso;
integer n_chk, n_err, n_neg;
spi_capstone_ctrl #(.DATA_W(16), .MIN_WIDTH(4), .NDEV(4)) dut (
.clk(clk), .rst_n(rst_n),
.cfg_cpol(cfg_cpol), .cfg_cpha(cfg_cpha), .cfg_lsb_first(cfg_lsb),
.cfg_width(cfg_width), .cfg_div(cfg_div), .cfg_dev(cfg_dev),
.cfg_lead(cfg_lead), .cfg_lag(cfg_lag), .cfg_idle(cfg_idle),
.start(start), .tx_data(tx_data), .abort(abort),
.busy(busy), .done(done), .cfg_err(cfg_err),
.rx_data(rx_data), .bits_done(bits_done),
.sclk(sclk), .mosi(mosi), .cs_n(cs_n), .miso(miso)
);
always #5 clk = ~clk;
// ---------------- oracle (same arithmetic as 20.5) -------------------------
function [15:0] mask;
input [4:0] w;
reg [16:0] one;
begin one = 17'd1; mask = ((one << w) - 17'd1); end
endfunction
function [15:0] revw;
input [15:0] v; input [4:0] w;
integer b;
begin
revw = 16'd0;
for (b = 0; b < 16; b = b + 1) if (b < w) revw[w-1-b] = v[b];
end
endfunction
function [15:0] ref_rx;
input [15:0] sw; input [4:0] w; input lsb;
begin ref_rx = lsb ? revw(sw & mask(w), w) : (sw & mask(w)); end
endfunction
function [15:0] ref_slave_rx;
input [15:0] tx; input [4:0] w; input lsb;
begin ref_slave_rx = lsb ? revw(tx & mask(w), w) : (tx & mask(w)); end
endfunction
// ---------------- the generator -------------------------------------------
reg [31:0] lcg;
task step_lcg;
begin
lcg = lcg * 32'd1664525 + 32'd1013904223;
end
endtask
// Draw from the HIGH half. Bits [30:16] are used, never the bottom ones -- see
// trap 2 and the measurement in group C1.
function [14:0] draw;
input [31:0] s;
begin draw = s[30:16]; end
endfunction
// ---------------- pin-level device model ----------------------------------
reg slv_cpol, slv_cpha;
reg [15:0] slv_word, slv_sr, slv_rx;
reg [4:0] slv_w, slv_idx, slv_nrx;
reg slv_miso, lead_s;
wire cs_any = ~(&cs_n);
assign miso = slv_miso;
always @(posedge cs_any) begin
slv_sr = slv_word << (16 - slv_w);
slv_rx = 16'd0;
slv_nrx = 5'd0;
if (!slv_cpha) begin
slv_miso = slv_sr[15]; slv_sr = slv_sr << 1; slv_idx = 5'd1;
end else begin
slv_miso = 1'b0; slv_idx = 5'd0;
end
end
always @(sclk) begin
if (cs_any === 1'b1) begin
lead_s = (sclk !== slv_cpol);
if (slv_cpha ? !lead_s : lead_s) begin
if (slv_nrx < slv_w) begin
slv_rx = {slv_rx[14:0], mosi}; slv_nrx = slv_nrx + 5'd1;
end
end
if (slv_cpha ? lead_s : !lead_s) begin
if (slv_idx < slv_w) begin
slv_miso = slv_sr[15]; slv_sr = slv_sr << 1;
slv_idx = slv_idx + 5'd1;
end
end
end
end
// ---------------- coverage model ------------------------------------------
// Plain arrays of hit counts. Every bin below answers a question of the form
// "what plausible bug survives if this is never exercised?" -- a bin that cannot
// answer it is not in the model.
integer cp_mode [0:3]; // CPOL/CPHA -- a mode-decode bug hides in an unvisited mode
integer cp_order [0:1]; // MSB/LSB -- the alignment path is separate per order
integer cp_width [0:3]; // 4 / 5-8 / 9-12 / 13-16 -- boundary and mid widths
integer cp_div [0:2]; // 0 / 1-7 / 8+ -- div=0 is the one-cycle half-period edge
integer cp_dev [0:3]; // which decoder output
integer x_mode_order [0:7];
integer x_mode_width [0:15];
integer x_width_order[0:7];
// EXCLUDED from the denominator, each for a stated reason, each asserted to be 0.
integer ex_illegal_accepted; // a width < 4 that produced a frame: REQ-ERR-001 says no
integer ex_two_selects; // two selects low: unreachable, one index -> one decoder
function integer wbin;
input [4:0] w;
begin
if (w == 5'd4) wbin = 0;
else if (w <= 5'd8) wbin = 1;
else if (w <= 5'd12) wbin = 2;
else wbin = 3;
end
endfunction
function integer dbin;
input [7:0] d;
begin
if (d == 8'd0) dbin = 0;
else if (d <= 8'd7) dbin = 1;
else dbin = 2;
end
endfunction
task sample_cov;
input cpol_i; input cpha_i; input lsb_i; input [4:0] w;
input [7:0] d; input [1:0] dv;
integer m, o, wb;
begin
m = {cpol_i, cpha_i};
o = lsb_i ? 1 : 0;
wb = wbin(w);
cp_mode[m] = cp_mode[m] + 1;
cp_order[o] = cp_order[o] + 1;
cp_width[wb] = cp_width[wb] + 1;
cp_div[dbin(d)] = cp_div[dbin(d)] + 1;
cp_dev[dv] = cp_dev[dv] + 1;
x_mode_order[m*2 + o] = x_mode_order[m*2 + o] + 1;
x_mode_width[m*4 + wb] = x_mode_width[m*4 + wb] + 1;
x_width_order[wb*2 + o] = x_width_order[wb*2 + o] + 1;
end
endtask
function integer holes_in;
input integer which; // 0..7 selects the coverpoint / cross
integer i, h;
begin
h = 0;
if (which == 0) for (i=0;i<4;i=i+1) if (cp_mode[i]==0) h=h+1;
if (which == 1) for (i=0;i<2;i=i+1) if (cp_order[i]==0) h=h+1;
if (which == 2) for (i=0;i<4;i=i+1) if (cp_width[i]==0) h=h+1;
if (which == 3) for (i=0;i<3;i=i+1) if (cp_div[i]==0) h=h+1;
if (which == 4) for (i=0;i<4;i=i+1) if (cp_dev[i]==0) h=h+1;
if (which == 5) for (i=0;i<8;i=i+1) if (x_mode_order[i]==0) h=h+1;
if (which == 6) for (i=0;i<16;i=i+1) if (x_mode_width[i]==0) h=h+1;
if (which == 7) for (i=0;i<8;i=i+1) if (x_width_order[i]==0) h=h+1;
holes_in = h;
end
endfunction
// A TASK, not a function, and the reason is a Verilog-2001 rule that catches
// everyone once: a FUNCTION MUST HAVE AT LEAST ONE INPUT. Written as
// `function integer total_holes;` with no arguments it compiles under
// SystemVerilog and fails under -g2001 with
//
// error: Function total_holes has no ports.
// : Functions must have at least one input port.
//
// A task may have outputs only, so the total comes back through one.
task total_holes;
output integer h;
integer i;
begin
h = 0;
for (i = 0; i < 8; i = i + 1) h = h + holes_in(i);
end
endtask
// ---------------- monitors -------------------------------------------------
integer n_edge, n_frames, n_low;
reg sclk_d, cs_d;
always @(posedge clk) begin
if (!rst_n) begin
n_edge <= 0; n_frames <= 0; sclk_d <= 1'b0; cs_d <= 1'b0;
end else begin
n_low = (cs_n[0]?0:1)+(cs_n[1]?0:1)+(cs_n[2]?0:1)+(cs_n[3]?0:1);
if (n_low > 1) ex_two_selects = ex_two_selects + 1;
if (cs_any && !cs_d) n_edge <= 0;
if (!cs_any && cs_d) n_frames <= n_frames + 1;
// `cs_d` as well as `cs_any`: an edge is only a FRAME edge if a device
// was ALREADY selected last cycle. A transition in the very cycle the
// select falls is SCLK reaching its new idle level, not a clocking
// edge -- the controller parks SCLK and asserts CS together, so when
// the previous idle level differed the two coincide.
//
// Measured cost of omitting `cs_d`: the first transaction after reset
// with CPOL=1 counted 17 edges instead of 16 in VHDL and 16 in
// SystemVerilog, because a one-cycle difference in reset-release
// timing decided whether the re-park landed inside the window. The
// received data was correct in both. With the gate the measurement no
// longer depends on that phase at all.
if (cs_any && cs_d && (sclk !== sclk_d)) n_edge <= n_edge + 1;
cs_d <= cs_any; sclk_d <= sclk;
end
end
// ---------------- stimulus -------------------------------------------------
task set_cfg;
input cpol_i; input cpha_i; input lsb_i; input [4:0] w;
input [7:0] dv; input [1:0] dv_n;
begin
cfg_cpol=cpol_i; cfg_cpha=cpha_i; cfg_lsb=lsb_i;
cfg_width=w; cfg_div=dv; cfg_dev=dv_n;
cfg_lead=4'd2; cfg_lag=4'd2; cfg_idle=4'd1;
slv_cpol=cpol_i; slv_cpha=cpha_i; slv_w=w;
end
endtask
task fire;
input [15:0] d;
begin
@(negedge clk); tx_data = d; start = 1'b1;
@(negedge clk); start = 1'b0;
end
endtask
task wait_idle;
input integer maxc; output gotd;
integer g; reg seen;
begin
g=0; seen=1'b0;
while (g < maxc) begin
@(negedge clk); g=g+1;
if (done) seen=1'b1;
if (!busy && seen) g=maxc;
else if (!busy && g>4) g=maxc;
end
gotd = seen;
end
endtask
// One transaction: build it, run it, score it, sample coverage.
task do_txn;
input cpol_i; input cpha_i; input lsb_i; input [4:0] w;
input [7:0] dv; input [1:0] dv_n; input [15:0] txd; input [15:0] swd;
reg gd;
begin
set_cfg(cpol_i, cpha_i, lsb_i, w, dv, dv_n);
slv_word = swd & mask(w);
fire(txd);
wait_idle(40000, gd);
n_chk = n_chk + 1;
if (!gd) begin
n_err = n_err + 1;
$display(" FAIL no done for w=%0d div=%0d", w, dv);
end
n_chk = n_chk + 1;
if (rx_data !== ref_rx(swd & mask(w), w, lsb_i)) begin
n_err = n_err + 1;
$display(" FAIL rx w=%0d mode=%0d lsb=%0d got %04h exp %04h",
w, {cpol_i,cpha_i}, lsb_i, rx_data,
ref_rx(swd & mask(w), w, lsb_i));
end
n_chk = n_chk + 1;
if ((slv_rx & mask(w)) !== ref_slave_rx(txd, w, lsb_i)) begin
n_err = n_err + 1;
$display(" FAIL device rx w=%0d got %04h exp %04h",
w, slv_rx & mask(w), ref_slave_rx(txd, w, lsb_i));
end
n_chk = n_chk + 1;
if (n_edge !== 2 * w) begin
n_err = n_err + 1;
$display(" FAIL edges w=%0d got %0d exp %0d", w, n_edge, 2*w);
end
sample_cov(cpol_i, cpha_i, lsb_i, w, dv, dv_n);
end
endtask
integer i, j, k, m, o, wb;
integer h_before, h_after, root_causes, targeted;
integer lo_hist [0:3];
integer hi_hist [0:3];
integer smin, smax, sbad;
integer v;
reg gdx;
initial begin
clk=1'b0; rst_n=1'b0; start=1'b0; abort=1'b0; tx_data=16'd0;
cfg_cpol=1'b0; cfg_cpha=1'b0; cfg_lsb=1'b0; cfg_width=5'd8;
cfg_div=8'd1; cfg_dev=2'd0; cfg_lead=4'd2; cfg_lag=4'd2; cfg_idle=4'd1;
slv_cpol=1'b0; slv_cpha=1'b0; slv_w=5'd8; slv_word=16'd0;
slv_miso=1'b0; slv_sr=16'd0; slv_rx=16'd0; slv_idx=5'd0; slv_nrx=5'd0;
n_chk=0; n_err=0; n_neg=0; n_edge=0; n_frames=0;
sclk_d=1'b0; cs_d=1'b0;
ex_illegal_accepted=0; ex_two_selects=0;
for (i=0;i<4;i=i+1) cp_mode[i]=0;
for (i=0;i<2;i=i+1) cp_order[i]=0;
for (i=0;i<4;i=i+1) cp_width[i]=0;
for (i=0;i<3;i=i+1) cp_div[i]=0;
for (i=0;i<4;i=i+1) cp_dev[i]=0;
for (i=0;i<8;i=i+1) x_mode_order[i]=0;
for (i=0;i<16;i=i+1) x_mode_width[i]=0;
for (i=0;i<8;i=i+1) x_width_order[i]=0;
for (i=0;i<4;i=i+1) begin lo_hist[i]=0; hi_hist[i]=0; end
$display("=== Chapter 20.6 -- constrained random and coverage closure ===");
// -----------------------------------------------------------------
// C1 -- review the generator BEFORE trusting anything it produces.
// -----------------------------------------------------------------
$display(" C1 generator review, 2000 draws");
lcg = 32'h1234_5678;
smin = 999; smax = -999; sbad = 0;
for (i = 0; i < 2000; i = i + 1) begin
step_lcg;
// The trap: treat the draw as SIGNED, exactly as `$random` is.
v = 4 + ($signed(lcg) % 13);
if (v < smin) smin = v;
if (v > smax) smax = v;
if (v < 4 || v > 16) sbad = sbad + 1;
end
$display(" signed 4 + (s %% 13) : min %4d max %4d illegal %4d of 2000",
smin, smax, sbad);
lcg = 32'h1234_5678;
smin = 999; smax = -999; sbad = 0;
for (i = 0; i < 2000; i = i + 1) begin
step_lcg;
v = 4 + (draw(lcg) % 13);
if (v < smin) smin = v;
if (v > smax) smax = v;
if (v < 4 || v > 16) sbad = sbad + 1;
end
$display(" unsigned 4 + (hi %% 13) : min %4d max %4d illegal %4d of 2000",
smin, smax, sbad);
// Trap 2: low bits vs high bits.
lcg = 32'h1234_5678;
for (i = 0; i < 2000; i = i + 1) begin
step_lcg;
lo_hist[lcg[1:0]] = lo_hist[lcg[1:0]] + 1;
hi_hist[lcg[17:16]] = hi_hist[lcg[17:16]] + 1;
end
$display(" low bits[1:0] histogram %4d %4d %4d %4d",
lo_hist[0], lo_hist[1], lo_hist[2], lo_hist[3]);
$display(" high bits[17:16] histogram %4d %4d %4d %4d",
hi_hist[0], hi_hist[1], hi_hist[2], hi_hist[3]);
lcg = 32'h1234_5678;
$write(" low bits[1:0] sequence ");
for (i = 0; i < 16; i = i + 1) begin step_lcg; $write("%0d ", lcg[1:0]); end
$display("");
lcg = 32'h1234_5678;
$write(" high bits[17:16] sequence ");
for (i = 0; i < 16; i = i + 1) begin step_lcg; $write("%0d ", lcg[17:16]); end
$display("");
// A uniform low-bit histogram must NOT be accepted as evidence: the sequence
// has period 4. This is checked, not merely narrated.
n_chk = n_chk + 1;
if (lo_hist[0] == 500 && lo_hist[1] == 500 &&
lo_hist[2] == 500 && lo_hist[3] == 500) begin
n_neg = n_neg + 1;
$display(" low bits are PERFECTLY uniform and have period 4 -- rejected");
end else begin
n_err = n_err + 1;
$display(" FAIL expected a perfectly uniform low-bit histogram");
end
// Reset is RELEASED ON A NEGEDGE, for the same reason `start` is driven on one.
// Releasing it on a posedge puts the assignment in the same region as every
// clocked block that tests it, and the order is undefined: the monitor may see
// the old value or the new one. Measured cost of getting this wrong -- the
// monitor held its reset one cycle longer in VHDL than in SystemVerilog, so the
// idle re-park of SCLK to CPOL=1 was counted as a frame edge in one language
// and not the other, and the first transaction of the run reported 17 edges
// instead of 16 in exactly one of the three.
repeat (4) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (2) @(posedge clk);
// -----------------------------------------------------------------
// C2 -- PHASE A: 40 transactions under a deliberately NARROW constraint.
// -----------------------------------------------------------------
$display(" C2 phase A: 40 transactions, width 4..8, div 1..3");
lcg = 32'hACE1_0F0F;
for (i = 0; i < 40; i = i + 1) begin
step_lcg; m = draw(lcg) % 4;
step_lcg; o = draw(lcg) % 2;
step_lcg; wb = 4 + (draw(lcg) % 5); // 4..8 only
step_lcg; k = 1 + (draw(lcg) % 3); // 1..3 only
step_lcg; j = draw(lcg) % 4;
step_lcg; v = draw(lcg);
do_txn(m[1], m[0], o[0], wb[4:0], k[7:0], j[1:0],
{v[7:0], v[14:7]}, {v[3:0], v[14:3]});
end
total_holes(h_before);
$display(" scored %0d transactions, %0d failures, %0d coverage holes",
40, n_err, h_before);
// -----------------------------------------------------------------
// C3 -- report the holes per coverpoint.
// -----------------------------------------------------------------
$display(" C3 coverage after phase A");
$display(" cp_mode %2d/%2d holes %2d", 4-holes_in(0), 4, holes_in(0));
$display(" cp_order %2d/%2d holes %2d", 2-holes_in(1), 2, holes_in(1));
$display(" cp_width %2d/%2d holes %2d", 4-holes_in(2), 4, holes_in(2));
$display(" cp_div %2d/%2d holes %2d", 3-holes_in(3), 3, holes_in(3));
$display(" cp_dev %2d/%2d holes %2d", 4-holes_in(4), 4, holes_in(4));
$display(" x_mode_order %2d/%2d holes %2d", 8-holes_in(5), 8, holes_in(5));
$display(" x_mode_width %2d/%2d holes %2d", 16-holes_in(6),16, holes_in(6));
$display(" x_width_order %2d/%2d holes %2d", 8-holes_in(7), 8, holes_in(7));
// -----------------------------------------------------------------
// C4 -- classify. This is the step that gets skipped, and it is the step
// that decides whether the next run is useful.
// -----------------------------------------------------------------
$display(" C4 hole classification");
root_causes = 0;
if (cp_width[2] == 0 || cp_width[3] == 0) begin
root_causes = root_causes + 1;
$display(" CONSTRAINT BUG width range 4..8 cannot reach bins 9-12, 13-16");
end
if (cp_div[0] == 0 || cp_div[2] == 0) begin
root_causes = root_causes + 1;
$display(" CONSTRAINT BUG div range 1..3 cannot reach bin 0 or bin 8+");
end
$display(" DEPENDENT %0d cross holes follow from the %0d above, not from",
holes_in(6) + holes_in(7), root_causes);
$display(" separate stimulus gaps -- fixing the ranges fixes them");
n_chk = n_chk + 1;
if (root_causes != 2) begin
n_err = n_err + 1;
$display(" FAIL expected 2 constraint root causes, found %0d", root_causes);
end
// -----------------------------------------------------------------
// C5 -- PHASE B: widened constraints.
// -----------------------------------------------------------------
// Phase B widens WIDTH but deliberately leaves DIV at 0..7, so bin 8+ is
// still unreachable by random draw. That is not an oversight in the example:
// it is the normal end state of a random campaign. Random stimulus closes the
// bulk cheaply and then stops improving, and the last few bins have to be
// constructed. A closure loop whose targeted stage never runs has not been
// shown to work -- and with 120 transactions and div 0..11 it did not run,
// which is how this batch size was chosen.
$display(" C5 phase B: 45 transactions, width 4..16, div 0..7");
lcg = 32'h5EED_1111;
for (i = 0; i < 45; i = i + 1) begin
step_lcg; m = draw(lcg) % 4;
step_lcg; o = draw(lcg) % 2;
step_lcg; wb = 4 + (draw(lcg) % 13); // 4..16
step_lcg; k = draw(lcg) % 8; // 0..7 -- bin 8+ left open
step_lcg; j = draw(lcg) % 4;
step_lcg; v = draw(lcg);
do_txn(m[1], m[0], o[0], wb[4:0], k[7:0], j[1:0],
{v[7:0], v[14:7]}, {v[3:0], v[14:3]});
end
total_holes(h_after);
$display(" widened random leaves %0d holes", h_after);
// -----------------------------------------------------------------
// C6 -- TARGETED closure for whatever random did not reach. Constructed for
// the specific bin, which is the only way to close a hole deterministically.
// -----------------------------------------------------------------
$display(" C6 targeted closure");
targeted = 0;
for (i = 0; i < 16; i = i + 1) begin
if (x_mode_width[i] == 0) begin
m = i / 4;
wb = i % 4;
if (wb == 0) k = 4;
else if (wb == 1) k = 7;
else if (wb == 2) k = 11;
else k = 15;
do_txn(m[1], m[0], 1'b0, k[4:0], 8'd1, 2'd0, 16'h9D5A, 16'h3CB9);
targeted = targeted + 1;
end
end
for (i = 0; i < 8; i = i + 1) begin
if (x_width_order[i] == 0) begin
wb = i / 2;
o = i % 2;
if (wb == 0) k = 4;
else if (wb == 1) k = 6;
else if (wb == 2) k = 10;
else k = 14;
do_txn(1'b0, 1'b0, o[0], k[4:0], 8'd1, 2'd0, 16'h9D5A, 16'h3CB9);
targeted = targeted + 1;
end
end
for (i = 0; i < 3; i = i + 1) begin
if (cp_div[i] == 0) begin
if (i == 0) k = 0;
else if (i == 1) k = 4;
else k = 9;
do_txn(1'b0, 1'b0, 1'b0, 5'd8, k[7:0], 2'd0, 16'h9D5A, 16'h3CB9);
targeted = targeted + 1;
end
end
$display(" %0d targeted transactions added", targeted);
// -----------------------------------------------------------------
// C7 -- final coverage, and the two bins that are EXCLUDED with reasons.
// -----------------------------------------------------------------
$display(" C7 final coverage");
$display(" cp_mode %2d/%2d", 4-holes_in(0), 4);
$display(" cp_order %2d/%2d", 2-holes_in(1), 2);
$display(" cp_width %2d/%2d", 4-holes_in(2), 4);
$display(" cp_div %2d/%2d", 3-holes_in(3), 3);
$display(" cp_dev %2d/%2d", 4-holes_in(4), 4);
$display(" x_mode_order %2d/%2d", 8-holes_in(5), 8);
$display(" x_mode_width %2d/%2d", 16-holes_in(6), 16);
$display(" x_width_order %2d/%2d", 8-holes_in(7), 8);
total_holes(h_after);
$display(" remaining holes %0d", h_after);
n_chk = n_chk + 1;
if (h_after != 0) begin
n_err = n_err + 1;
$display(" FAIL %0d holes remain after targeted closure", h_after);
end
$display(" excluded, not chased:");
$display(" illegal width < 4 accepted hits %0d (REQ-ERR-001 forbids it)",
ex_illegal_accepted);
$display(" unreach two selects low hits %0d (one index, one decoder)",
ex_two_selects);
n_chk = n_chk + 1;
if (ex_illegal_accepted != 0 || ex_two_selects != 0) begin
n_err = n_err + 1;
$display(" FAIL an excluded bin was hit -- the exclusion was wrong");
end
// An illegal width is offered once, to prove the illegal bin's counter is
// wired to something and is not simply a variable that stays at zero.
set_cfg(1'b0, 1'b0, 1'b0, 5'd3, 8'd1, 2'd0);
fire(16'h1234);
if (busy) ex_illegal_accepted = ex_illegal_accepted + 1;
n_chk = n_chk + 1;
if (ex_illegal_accepted != 0) begin
n_err = n_err + 1;
$display(" FAIL an illegal width started a frame");
end else begin
n_neg = n_neg + 1;
$display(" the illegal bin was offered a width of 3 and stayed 0");
end
$display("=== SUMMARY checks=%0d negatives=%0d failures=%0d : %0s ===",
n_chk, n_neg, n_err, (n_err == 0) ? "PASS" : "FAIL");
$finish;
end
initial begin
#40000000;
$display(" FATAL global timeout");
$display("=== SUMMARY checks=%0d negatives=%0d failures=%0d : FAIL ===",
n_chk, n_neg, n_err + 1);
$finish;
end
endmodule-- spi_capstone_crv_tb.vhd
--
-- Chapter 20.6 in VHDL-2008.
--
-- THE GENERATOR IS REBUILT HERE BIT FOR BIT, and that is the whole reason this file
-- exists. A randomised bench written with each language's own random primitive
-- produces three different stimulus streams, and then "all three passed" means only
-- that three different tests passed -- the coverage tables cannot be compared at all.
--
-- The recurrence
--
-- s = s * 1664525 + 1013904223 (mod 2^32)
--
-- is reproduced exactly. Verilog truncates the product to 32 bits by context; VHDL's
-- `"*"` on two 32-bit unsigneds returns 64 bits, so the truncation is explicit here.
-- Those agree because (a*b mod 2^32 + c) mod 2^32 = (a*b + c) mod 2^32 -- and the
-- proof that they agree in practice is that all three transcripts, including every
-- coverage count and both trap measurements, come out identical.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use std.env.all;
entity spi_capstone_crv_tb is
end entity spi_capstone_crv_tb;
architecture tb of spi_capstone_crv_tb is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal cfg_cpol : std_logic := '0';
signal cfg_cpha : std_logic := '0';
signal cfg_lsb : std_logic := '0';
signal cfg_width : std_logic_vector(4 downto 0) := "01000";
signal cfg_div : std_logic_vector(7 downto 0) := x"01";
signal cfg_dev : std_logic_vector(1 downto 0) := "00";
signal cfg_lead : std_logic_vector(3 downto 0) := x"2";
signal cfg_lag : std_logic_vector(3 downto 0) := x"2";
signal cfg_idle : std_logic_vector(3 downto 0) := x"1";
signal start : std_logic := '0';
signal abort : std_logic := '0';
signal tx_data : std_logic_vector(15 downto 0) := (others => '0');
signal busy : std_logic;
signal done : std_logic;
signal cfg_err : std_logic;
signal rx_data : std_logic_vector(15 downto 0);
signal bits_done : std_logic_vector(4 downto 0);
signal sclk : std_logic;
signal mosi : std_logic;
signal cs_n : std_logic_vector(3 downto 0);
signal miso : std_logic;
signal slv_cpol : std_logic := '0';
signal slv_cpha : std_logic := '0';
signal slv_word : std_logic_vector(15 downto 0) := (others => '0');
signal slv_rx : std_logic_vector(15 downto 0) := (others => '0');
signal slv_w : unsigned(4 downto 0) := to_unsigned(8, 5);
signal slv_miso : std_logic := '0';
signal cs_any : std_logic;
signal n_edge : integer := 0;
signal ex_two_select : integer := 0;
signal cs_d, sclk_d : std_logic := '0';
-- ---------------- generator ---------------------------------------------------
function lcg_next (s : unsigned(31 downto 0)) return unsigned is
variable p : unsigned(63 downto 0);
begin
p := s * to_unsigned(1664525, 32);
p := p + to_unsigned(1013904223, 64);
return p(31 downto 0);
end function lcg_next;
-- Draw from the HIGH half only. Bits 30..16, never the bottom ones.
function draw (s : unsigned(31 downto 0)) return integer is
begin
return to_integer(s(30 downto 16));
end function draw;
-- ---------------- oracle ------------------------------------------------------
function maskw (w : integer) return unsigned is
variable m : unsigned(15 downto 0);
begin
m := (others => '0');
for b in 0 to 15 loop
if b < w then m(b) := '1'; end if;
end loop;
return m;
end function maskw;
function revw (v : std_logic_vector(15 downto 0); w : integer)
return std_logic_vector is
variable r : std_logic_vector(15 downto 0);
begin
r := (others => '0');
for b in 0 to 15 loop
if b < w then r(w-1-b) := v(b); end if;
end loop;
return r;
end function revw;
function ref_rx (sw : std_logic_vector(15 downto 0); w : integer; lsb : std_logic)
return std_logic_vector is
variable m : std_logic_vector(15 downto 0);
begin
m := std_logic_vector(unsigned(sw) and maskw(w));
if lsb = '1' then return revw(m, w); else return m; end if;
end function ref_rx;
function ref_slave_rx (tx : std_logic_vector(15 downto 0); w : integer;
lsb : std_logic) return std_logic_vector is
variable m : std_logic_vector(15 downto 0);
begin
m := std_logic_vector(unsigned(tx) and maskw(w));
if lsb = '1' then return revw(m, w); else return m; end if;
end function ref_slave_rx;
function wbin (w : integer) return integer is
begin
if w = 4 then return 0;
elsif w <= 8 then return 1;
elsif w <= 12 then return 2;
else return 3; end if;
end function wbin;
function dbin (d : integer) return integer is
begin
if d = 0 then return 0;
elsif d <= 7 then return 1;
else return 2; end if;
end function dbin;
-- ---------------- formatting --------------------------------------------------
function hex4 (v : std_logic_vector(15 downto 0)) return string is
constant D : string(1 to 16) := "0123456789abcdef";
variable s : string(1 to 4);
variable n : integer;
begin
if is_x(v) then return "xxxx"; end if;
n := to_integer(unsigned(v));
for i in 4 downto 1 loop
s(i) := D((n mod 16) + 1);
n := n / 16;
end loop;
return s;
end function hex4;
-- Right-aligned, and it HANDLES NEGATIVES: the signed-modulo measurement in group
-- C1 prints a minimum of -8, and a formatter that silently dropped the sign would
-- hide exactly the defect the group exists to show.
function ipad (v : integer; w : integer) return string is
variable s : string(1 to w);
variable t : string(1 to 20);
variable n, len : integer;
variable neg : boolean;
begin
t := (others => ' ');
neg := v < 0;
if neg then n := -v; else n := v; end if;
len := 0;
if n = 0 then
len := 1; t(1) := '0';
else
while n > 0 loop
len := len + 1;
t(len) := character'val(character'pos('0') + (n mod 10));
n := n / 10;
end loop;
end if;
if neg then
len := len + 1; t(len) := '-';
end if;
s := (others => ' ');
for i in 1 to len loop
s(w - i + 1) := t(i);
end loop;
return s;
end function ipad;
function i0 (v : integer) return string is
begin
return integer'image(v);
end function i0;
procedure pr (s : string) is
variable l : line;
begin
write(l, s);
writeline(output, l);
end procedure pr;
function sl (b : boolean) return std_logic is
begin
if b then return '1'; else return '0'; end if;
end function sl;
begin
cs_any <= not (cs_n(0) and cs_n(1) and cs_n(2) and cs_n(3));
miso <= slv_miso;
dut : entity work.spi_capstone_ctrl
generic map (DATA_W => 16, MIN_WIDTH => 4, NDEV => 4)
port map (
clk => clk, rst_n => rst_n,
cfg_cpol => cfg_cpol, cfg_cpha => cfg_cpha, cfg_lsb_first => cfg_lsb,
cfg_width => cfg_width, cfg_div => cfg_div, cfg_dev => cfg_dev,
cfg_lead => cfg_lead, cfg_lag => cfg_lag, cfg_idle => cfg_idle,
start => start, tx_data => tx_data, abort => abort,
busy => busy, done => done, cfg_err => cfg_err,
rx_data => rx_data, bits_done => bits_done,
sclk => sclk, mosi => mosi, cs_n => cs_n, miso => miso
);
clkgen : process
begin
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end process clkgen;
slave : process (cs_any, sclk)
variable sr : std_logic_vector(15 downto 0);
variable idx : unsigned(4 downto 0);
variable nrx : unsigned(4 downto 0);
variable lead_s : boolean;
begin
if rising_edge(cs_any) then
sr := std_logic_vector(shift_left(unsigned(slv_word),
16 - to_integer(slv_w)));
slv_rx <= (others => '0');
nrx := (others => '0');
if slv_cpha = '0' then
slv_miso <= sr(15);
sr := sr(14 downto 0) & '0';
idx := to_unsigned(1, 5);
else
slv_miso <= '0';
idx := (others => '0');
end if;
elsif sclk'event and cs_any = '1' then
lead_s := (sclk /= slv_cpol);
if (slv_cpha = '1' and not lead_s) or (slv_cpha = '0' and lead_s) then
if nrx < slv_w then
slv_rx <= slv_rx(14 downto 0) & mosi;
nrx := nrx + 1;
end if;
end if;
if (slv_cpha = '1' and lead_s) or (slv_cpha = '0' and not lead_s) then
if idx < slv_w then
slv_miso <= sr(15);
sr := sr(14 downto 0) & '0';
idx := idx + 1;
end if;
end if;
end if;
end process slave;
mon : process (clk)
variable n_low : integer;
begin
if rising_edge(clk) then
if rst_n = '0' then
n_edge <= 0; cs_d <= '0'; sclk_d <= '0'; ex_two_select <= 0;
else
n_low := 0;
for i in 0 to 3 loop
if cs_n(i) = '0' then n_low := n_low + 1; end if;
end loop;
if n_low > 1 then ex_two_select <= ex_two_select + 1; end if;
if cs_any = '1' and cs_d = '0' then n_edge <= 0; end if;
-- `cs_d` as well as `cs_any`: an edge is only a FRAME edge if a device
-- was ALREADY selected last cycle. A transition in the cycle the
-- select falls is SCLK reaching its new idle level, not a clocking
-- edge. Without this gate the first CPOL=1 transaction counted 17
-- edges here and 16 in SystemVerilog, on identical data.
if cs_any = '1' and cs_d = '1' and sclk /= sclk_d then
n_edge <= n_edge + 1;
end if;
cs_d <= cs_any;
sclk_d <= sclk;
end if;
end if;
end process mon;
wd : process
begin
wait for 400 ms;
pr(" FATAL global timeout");
pr("=== SUMMARY checks=0 negatives=0 failures=1 : FAIL ===");
finish;
end process wd;
main : process
variable n_chk, n_err, n_neg : integer := 0;
variable lcg : unsigned(31 downto 0);
variable vv : unsigned(14 downto 0);
variable smin, smax, sbad, v : integer;
variable m, o, wv, kv, jv : integer;
variable h_before, h_after, root_causes, targeted : integer;
variable lo_hist, hi_hist : integer_vector(0 to 3);
variable cp_mode, cp_dev : integer_vector(0 to 3);
variable cp_order : integer_vector(0 to 1);
variable cp_width : integer_vector(0 to 3);
variable cp_div : integer_vector(0 to 2);
variable x_mode_order : integer_vector(0 to 7);
variable x_mode_width : integer_vector(0 to 15);
variable x_width_order : integer_vector(0 to 7);
variable ex_illegal : integer;
variable d32 : string(1 to 32);
variable gd : std_logic;
procedure sample_cov (cpol_i, cpha_i, lsb_i : std_logic;
w, d, dv : integer) is
variable mm, oo, wb : integer;
begin
mm := 0;
if cpol_i = '1' then mm := mm + 2; end if;
if cpha_i = '1' then mm := mm + 1; end if;
if lsb_i = '1' then oo := 1; else oo := 0; end if;
wb := wbin(w);
cp_mode(mm) := cp_mode(mm) + 1;
cp_order(oo) := cp_order(oo) + 1;
cp_width(wb) := cp_width(wb) + 1;
cp_div(dbin(d)) := cp_div(dbin(d)) + 1;
cp_dev(dv) := cp_dev(dv) + 1;
x_mode_order(mm*2 + oo) := x_mode_order(mm*2 + oo) + 1;
x_mode_width(mm*4 + wb) := x_mode_width(mm*4 + wb) + 1;
x_width_order(wb*2 + oo) := x_width_order(wb*2 + oo) + 1;
end procedure sample_cov;
function holes_in (which : integer;
a4 : integer_vector(0 to 3);
a2 : integer_vector(0 to 1);
a3 : integer_vector(0 to 2);
a8 : integer_vector(0 to 7);
a16 : integer_vector(0 to 15)) return integer is
variable h : integer := 0;
begin
case which is
when 0 | 2 | 4 => for i in 0 to 3 loop if a4(i) = 0 then h := h+1; end if; end loop;
when 1 => for i in 0 to 1 loop if a2(i) = 0 then h := h+1; end if; end loop;
when 3 => for i in 0 to 2 loop if a3(i) = 0 then h := h+1; end if; end loop;
when 5 | 7 => for i in 0 to 7 loop if a8(i) = 0 then h := h+1; end if; end loop;
when others => for i in 0 to 15 loop if a16(i) = 0 then h := h+1; end if; end loop;
end case;
return h;
end function holes_in;
-- Local wrappers so the call sites read like the other two languages.
impure function h0 return integer is begin
return holes_in(0, cp_mode, cp_order, cp_div, x_mode_order, x_mode_width);
end function;
impure function h1 return integer is
variable h : integer := 0;
begin
for i in 0 to 1 loop if cp_order(i) = 0 then h := h+1; end if; end loop;
return h;
end function;
impure function h2 return integer is
variable h : integer := 0;
begin
for i in 0 to 3 loop if cp_width(i) = 0 then h := h+1; end if; end loop;
return h;
end function;
impure function h3 return integer is
variable h : integer := 0;
begin
for i in 0 to 2 loop if cp_div(i) = 0 then h := h+1; end if; end loop;
return h;
end function;
impure function h4 return integer is
variable h : integer := 0;
begin
for i in 0 to 3 loop if cp_dev(i) = 0 then h := h+1; end if; end loop;
return h;
end function;
impure function h5 return integer is
variable h : integer := 0;
begin
for i in 0 to 7 loop if x_mode_order(i) = 0 then h := h+1; end if; end loop;
return h;
end function;
impure function h6 return integer is
variable h : integer := 0;
begin
for i in 0 to 15 loop if x_mode_width(i) = 0 then h := h+1; end if; end loop;
return h;
end function;
impure function h7 return integer is
variable h : integer := 0;
begin
for i in 0 to 7 loop if x_width_order(i) = 0 then h := h+1; end if; end loop;
return h;
end function;
impure function htot return integer is
begin
return h0 + h1 + h2 + h3 + h4 + h5 + h6 + h7;
end function;
procedure set_cfg (cpol_i, cpha_i, lsb_i : std_logic;
wv2, dv2, dvn2 : integer) is
begin
cfg_cpol <= cpol_i;
cfg_cpha <= cpha_i;
cfg_lsb <= lsb_i;
cfg_width <= std_logic_vector(to_unsigned(wv2, 5));
cfg_div <= std_logic_vector(to_unsigned(dv2, 8));
cfg_dev <= std_logic_vector(to_unsigned(dvn2, 2));
cfg_lead <= x"2";
cfg_lag <= x"2";
cfg_idle <= x"1";
slv_cpol <= cpol_i;
slv_cpha <= cpha_i;
slv_w <= to_unsigned(wv2, 5);
end procedure set_cfg;
procedure fire (d : std_logic_vector(15 downto 0)) is
begin
wait until falling_edge(clk);
tx_data <= d;
start <= '1';
wait until falling_edge(clk);
start <= '0';
end procedure fire;
procedure wait_idle (maxc : integer; gotd : out std_logic) is
variable g : integer;
variable seen : std_logic;
begin
g := 0; seen := '0';
while g < maxc loop
wait until falling_edge(clk);
g := g + 1;
if done = '1' then seen := '1'; end if;
if busy = '0' and seen = '1' then g := maxc;
elsif busy = '0' and g > 4 then g := maxc; end if;
end loop;
gotd := seen;
end procedure wait_idle;
procedure do_txn (cpol_i, cpha_i, lsb_i : std_logic;
w, d, dv : integer;
txd, swd : std_logic_vector(15 downto 0)) is
variable g : std_logic;
begin
set_cfg(cpol_i, cpha_i, lsb_i, w, d, dv);
slv_word <= std_logic_vector(unsigned(swd) and maskw(w));
fire(txd);
wait_idle(40000, g);
n_chk := n_chk + 1;
if g /= '1' then
n_err := n_err + 1;
pr(" FAIL no done for w=" & i0(w) & " div=" & i0(d));
end if;
n_chk := n_chk + 1;
if rx_data /= ref_rx(std_logic_vector(unsigned(swd) and maskw(w)),
w, lsb_i) then
n_err := n_err + 1;
pr(" FAIL rx w=" & i0(w) & " got " & hex4(rx_data));
end if;
n_chk := n_chk + 1;
if (std_logic_vector(unsigned(slv_rx) and maskw(w)))
/= ref_slave_rx(txd, w, lsb_i) then
n_err := n_err + 1;
pr(" FAIL device rx w=" & i0(w));
end if;
n_chk := n_chk + 1;
if n_edge /= 2 * w then
n_err := n_err + 1;
pr(" FAIL edges w=" & i0(w) & " got " & i0(n_edge));
end if;
sample_cov(cpol_i, cpha_i, lsb_i, w, d, dv);
end procedure do_txn;
begin
n_chk := 0; n_err := 0; n_neg := 0;
cp_mode := (others => 0); cp_order := (others => 0);
cp_width := (others => 0); cp_div := (others => 0);
cp_dev := (others => 0); x_mode_order := (others => 0);
x_mode_width := (others => 0); x_width_order := (others => 0);
lo_hist := (others => 0); hi_hist := (others => 0);
ex_illegal := 0;
pr("=== Chapter 20.6 -- constrained random and coverage closure ===");
pr(" C1 generator review, 2000 draws");
lcg := x"12345678";
smin := 999; smax := -999; sbad := 0;
for i in 0 to 1999 loop
lcg := lcg_next(lcg);
v := 4 + (to_integer(signed(lcg)) rem 13);
if v < smin then smin := v; end if;
if v > smax then smax := v; end if;
if v < 4 or v > 16 then sbad := sbad + 1; end if;
end loop;
pr(" signed 4 + (s % 13) : min " & ipad(smin,4) & " max " & ipad(smax,4) &
" illegal " & ipad(sbad,4) & " of 2000");
lcg := x"12345678";
smin := 999; smax := -999; sbad := 0;
for i in 0 to 1999 loop
lcg := lcg_next(lcg);
v := 4 + (draw(lcg) mod 13);
if v < smin then smin := v; end if;
if v > smax then smax := v; end if;
if v < 4 or v > 16 then sbad := sbad + 1; end if;
end loop;
pr(" unsigned 4 + (hi % 13) : min " & ipad(smin,4) & " max " & ipad(smax,4) &
" illegal " & ipad(sbad,4) & " of 2000");
lcg := x"12345678";
for i in 0 to 1999 loop
lcg := lcg_next(lcg);
v := to_integer(lcg(1 downto 0)); lo_hist(v) := lo_hist(v) + 1;
v := to_integer(lcg(17 downto 16)); hi_hist(v) := hi_hist(v) + 1;
end loop;
pr(" low bits[1:0] histogram " & ipad(lo_hist(0),4) & " " &
ipad(lo_hist(1),4) & " " & ipad(lo_hist(2),4) & " " & ipad(lo_hist(3),4));
pr(" high bits[17:16] histogram " & ipad(hi_hist(0),4) & " " &
ipad(hi_hist(1),4) & " " & ipad(hi_hist(2),4) & " " & ipad(hi_hist(3),4));
lcg := x"12345678";
for i in 0 to 15 loop
lcg := lcg_next(lcg);
d32(2*i+1) := character'val(character'pos('0') +
to_integer(lcg(1 downto 0)));
d32(2*i+2) := ' ';
end loop;
pr(" low bits[1:0] sequence " & d32);
lcg := x"12345678";
for i in 0 to 15 loop
lcg := lcg_next(lcg);
d32(2*i+1) := character'val(character'pos('0') +
to_integer(lcg(17 downto 16)));
d32(2*i+2) := ' ';
end loop;
pr(" high bits[17:16] sequence " & d32);
n_chk := n_chk + 1;
if lo_hist(0) = 500 and lo_hist(1) = 500 and
lo_hist(2) = 500 and lo_hist(3) = 500 then
n_neg := n_neg + 1;
pr(" low bits are PERFECTLY uniform and have period 4 -- rejected");
else
n_err := n_err + 1;
pr(" FAIL expected a perfectly uniform low-bit histogram");
end if;
-- Reset is RELEASED ON A FALLING EDGE, for the same reason `start` is driven on
-- one: released on a rising edge it races every clocked block that tests it.
-- The measured cost was a one-cycle difference in when the monitor left reset,
-- which counted SCLK's idle re-park as a frame edge in VHDL but not in
-- SystemVerilog -- 17 edges against 16, on the first transaction only.
for i in 1 to 4 loop wait until rising_edge(clk); end loop;
wait until falling_edge(clk);
rst_n <= '1';
for i in 1 to 2 loop wait until rising_edge(clk); end loop;
pr(" C2 phase A: 40 transactions, width 4..8, div 1..3");
lcg := x"ACE10F0F";
for i in 0 to 39 loop
lcg := lcg_next(lcg); m := draw(lcg) mod 4;
lcg := lcg_next(lcg); o := draw(lcg) mod 2;
lcg := lcg_next(lcg); wv := 4 + (draw(lcg) mod 5);
lcg := lcg_next(lcg); kv := 1 + (draw(lcg) mod 3);
lcg := lcg_next(lcg); jv := draw(lcg) mod 4;
lcg := lcg_next(lcg); vv := lcg(30 downto 16);
do_txn(sl(m / 2 = 1), sl(m mod 2 = 1), sl(o = 1), wv, kv, jv,
std_logic_vector(vv(7 downto 0) & vv(14 downto 7)),
std_logic_vector(vv(3 downto 0) & vv(14 downto 3)));
end loop;
h_before := htot;
pr(" scored 40 transactions, " & i0(n_err) & " failures, " &
i0(h_before) & " coverage holes");
pr(" C3 coverage after phase A");
pr(" cp_mode " & ipad(4-h0,2) & "/" & ipad(4,2) & " holes " & ipad(h0,2));
pr(" cp_order " & ipad(2-h1,2) & "/" & ipad(2,2) & " holes " & ipad(h1,2));
pr(" cp_width " & ipad(4-h2,2) & "/" & ipad(4,2) & " holes " & ipad(h2,2));
pr(" cp_div " & ipad(3-h3,2) & "/" & ipad(3,2) & " holes " & ipad(h3,2));
pr(" cp_dev " & ipad(4-h4,2) & "/" & ipad(4,2) & " holes " & ipad(h4,2));
pr(" x_mode_order " & ipad(8-h5,2) & "/" & ipad(8,2) & " holes " & ipad(h5,2));
pr(" x_mode_width " & ipad(16-h6,2) & "/" & ipad(16,2) & " holes " & ipad(h6,2));
pr(" x_width_order " & ipad(8-h7,2) & "/" & ipad(8,2) & " holes " & ipad(h7,2));
pr(" C4 hole classification");
root_causes := 0;
if cp_width(2) = 0 or cp_width(3) = 0 then
root_causes := root_causes + 1;
pr(" CONSTRAINT BUG width range 4..8 cannot reach bins 9-12, 13-16");
end if;
if cp_div(0) = 0 or cp_div(2) = 0 then
root_causes := root_causes + 1;
pr(" CONSTRAINT BUG div range 1..3 cannot reach bin 0 or bin 8+");
end if;
pr(" DEPENDENT " & i0(h6 + h7) &
" cross holes follow from the " & i0(root_causes) & " above, not from");
pr(" separate stimulus gaps -- fixing the ranges fixes them");
n_chk := n_chk + 1;
if root_causes /= 2 then
n_err := n_err + 1;
pr(" FAIL expected 2 constraint root causes, found " & i0(root_causes));
end if;
pr(" C5 phase B: 45 transactions, width 4..16, div 0..7");
lcg := x"5EED1111";
for i in 0 to 44 loop
lcg := lcg_next(lcg); m := draw(lcg) mod 4;
lcg := lcg_next(lcg); o := draw(lcg) mod 2;
lcg := lcg_next(lcg); wv := 4 + (draw(lcg) mod 13);
lcg := lcg_next(lcg); kv := draw(lcg) mod 8;
lcg := lcg_next(lcg); jv := draw(lcg) mod 4;
lcg := lcg_next(lcg); vv := lcg(30 downto 16);
do_txn(sl(m / 2 = 1), sl(m mod 2 = 1), sl(o = 1), wv, kv, jv,
std_logic_vector(vv(7 downto 0) & vv(14 downto 7)),
std_logic_vector(vv(3 downto 0) & vv(14 downto 3)));
end loop;
h_after := htot;
pr(" widened random leaves " & i0(h_after) & " holes");
pr(" C6 targeted closure");
targeted := 0;
for i in 0 to 15 loop
if x_mode_width(i) = 0 then
m := i / 4;
case i mod 4 is
when 0 => kv := 4;
when 1 => kv := 7;
when 2 => kv := 11;
when others => kv := 15;
end case;
do_txn(sl(m / 2 = 1), sl(m mod 2 = 1), '0', kv, 1, 0,
x"9D5A", x"3CB9");
targeted := targeted + 1;
end if;
end loop;
for i in 0 to 7 loop
if x_width_order(i) = 0 then
o := i mod 2;
case i / 2 is
when 0 => kv := 4;
when 1 => kv := 6;
when 2 => kv := 10;
when others => kv := 14;
end case;
do_txn('0', '0', sl(o = 1), kv, 1, 0, x"9D5A", x"3CB9");
targeted := targeted + 1;
end if;
end loop;
for i in 0 to 2 loop
if cp_div(i) = 0 then
case i is
when 0 => kv := 0;
when 1 => kv := 4;
when others => kv := 9;
end case;
do_txn('0', '0', '0', 8, kv, 0, x"9D5A", x"3CB9");
targeted := targeted + 1;
end if;
end loop;
pr(" " & i0(targeted) & " targeted transactions added");
pr(" C7 final coverage");
pr(" cp_mode " & ipad(4-h0,2) & "/" & ipad(4,2));
pr(" cp_order " & ipad(2-h1,2) & "/" & ipad(2,2));
pr(" cp_width " & ipad(4-h2,2) & "/" & ipad(4,2));
pr(" cp_div " & ipad(3-h3,2) & "/" & ipad(3,2));
pr(" cp_dev " & ipad(4-h4,2) & "/" & ipad(4,2));
pr(" x_mode_order " & ipad(8-h5,2) & "/" & ipad(8,2));
pr(" x_mode_width " & ipad(16-h6,2) & "/" & ipad(16,2));
pr(" x_width_order " & ipad(8-h7,2) & "/" & ipad(8,2));
h_after := htot;
pr(" remaining holes " & i0(h_after));
n_chk := n_chk + 1;
if h_after /= 0 then
n_err := n_err + 1;
pr(" FAIL " & i0(h_after) & " holes remain after targeted closure");
end if;
pr(" excluded, not chased:");
pr(" illegal width < 4 accepted hits " & i0(ex_illegal) &
" (REQ-ERR-001 forbids it)");
pr(" unreach two selects low hits " & i0(ex_two_select) &
" (one index, one decoder)");
n_chk := n_chk + 1;
if ex_illegal /= 0 or ex_two_select /= 0 then
n_err := n_err + 1;
pr(" FAIL an excluded bin was hit -- the exclusion was wrong");
end if;
set_cfg('0', '0', '0', 3, 1, 0);
fire(x"1234");
if busy = '1' then ex_illegal := ex_illegal + 1; end if;
n_chk := n_chk + 1;
if ex_illegal /= 0 then
n_err := n_err + 1;
pr(" FAIL an illegal width started a frame");
else
n_neg := n_neg + 1;
pr(" the illegal bin was offered a width of 3 and stayed 0");
end if;
if n_err = 0 then
pr("=== SUMMARY checks=" & i0(n_chk) & " negatives=" & i0(n_neg) &
" failures=" & i0(n_err) & " : PASS ===");
else
pr("=== SUMMARY checks=" & i0(n_chk) & " negatives=" & i0(n_neg) &
" failures=" & i0(n_err) & " : FAIL ===");
end if;
finish;
end process main;
end architecture tb;11. What It Reports
=== SUMMARY checks=353 negatives=2 failures=0 : PASS ===353 checks in each of three languages, byte-identical transcripts, 160 transactions through the same scoreboard and property monitors Chapter 20.5 built, and zero functional failures.
| Value | |
|---|---|
| Transactions | 40 phase A + 45 phase B + 2 targeted |
| Checks per language | 353 |
| Coverpoints / crosses | 5 / 3 |
| Bins in the denominator | 49 |
| Holes after phase A | 17, from 2 root causes |
| Holes after phase B | 2 |
| Holes after targeted closure | 0 |
| Excluded bins | 2, both asserted empty |
| Generator traps measured | 2 |
12. Summary
The generator was reviewed before it was trusted, and both traps in it were measured rather than warned about. Treating the draw as signed put 891 of 2000 widths outside the legal range — 44.6%, from a constraint that reads as 4 to 16. Drawing from the low bits gave a histogram of exactly 500/500/500/500 over a sequence whose period is four: perfectly uniform, entirely deterministic, and preferable to the real random source by any histogram-based review.
Closure ran as a loop rather than as a number. Phase A's narrow constraints left 17 holes from 2 root causes, and thirteen of those holes were arithmetic consequences of the two — so the work was one constraint change, not thirteen directed tests. Phase B's widened constraints left 2, both of them a divider bin the constraint deliberately could not reach, and both closed by transactions constructed for the specific bin. Two further bins are excluded from the denominator with recorded reasons, and the illegal one is offered an illegal width at the end of the run to prove its counter is wired to something.
The batch size in phase B was reduced from 120 to 45 for a reason worth keeping: at 120, random closed everything and the targeted stage never ran, which made a demonstrated loop into untested code.
Code coverage and functional coverage answer different questions and neither is verification. This controller's mode decoder is three continuous assignments, so a suite running one mode-0 frame reports 100% statement coverage on the logic most likely to be wrong — and CPOL appears nowhere in those lines, so no code-coverage metric can distinguish mode 0 from mode 2. The functional model catches it and has its own blind spots: no bin for an abort, a mid-frame reset, back-to-back transfers, or the state machine's default arm.
13. What Comes Next
Chapter 20.7 stops trusting all of it and breaks the design on purpose — thirteen deliberate defects, one detection matrix, and a loopback bench included specifically because it passes nine of them.
Continue learning
Related tutorials
- 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
Functional Coverage and Meaningful Crosses
A four-way cross of 72 bins has two defects pulling in opposite directions: six bins the specification forbids, so it tops out at 91.7 percent, and twelve it can fill that carry no information. A curated set of three two-way crosses closes at draw 52 where the full cross plateaus at 372.
- Related topic
Constrained-Random Sequences
A constraint set is a specification in a solver's language, and it fails in two directions a coverage report cannot tell apart. A weighted set closes at draw 42 where a uniform one needs 2120; an over-constrained set stalls silently; and an inconsistent one must report rather than emit.
- Related topic
Capstone Requirements and Specification
One configurable SPI controller, specified before it is designed: nineteen numbered requirements, their corner cases, and the non-goals that keep the project finishable.
