USB · Module 28
USB vs SPI
SPI selects a peripheral with a wire routed at layout time and USB with an address the host assigned — so a chip-select contention is invisible to every slave (0 of 11) while a duplicate USB address is detected every time (274 of 274).
The second of four comparisons. Chapter 28.1 compared USB with a protocol that has no clock wire. This one compares it with a protocol that does — so timing is not the difference, and the difference turns out to be something more interesting.
1. The Comparison That Is Not A Wire Count
The usual answer is arithmetic: SPI needs four wires plus one chip select per device, USB needs two wires for up to 127 devices, therefore USB wins on pin count. The arithmetic is right. It is also the least interesting thing about the difference, and it leads people to the wrong conclusion — because if pin count were the axis, nobody would still be putting SPI flash on boards, and everybody does.
SPI has a clock wire. It has no tolerance budget worth measuring, no framing to guess, no synchronisation to recover. On the axis that chapter 28.1 was about, SPI is better than UART and arguably simpler than USB.
What SPI does not have is any way to ask a device who it is.
That last sentence is a measurable claim, so this chapter measures it. The result is 0 out of 11 against 274 out of 274, and section 6 explains what those numbers count.
2. Two Designs, One Question
Not one module with a mode switch. Two separate designs, because they are two separate designs, and forcing them into one would hide the thing being compared. Both are driven by the same testbench from the same stimulus source, so the comparison happens inside the verification rather than in prose afterwards.
spi_cs_select | usb_addr_select | |
|---|---|---|
| what selects | one of N wires | an address field |
| who knows the mapping | the board | the host, at run time |
| where the mapping lives in RTL | a parameter | a register file |
| can it change after power-on | no | yes |
| configuration port | none | cfg_valid / cfg_slot / cfg_addr |
| wires for N devices | N + 3 | 2 |
The row that matters is the third. In one design the mapping is an elaboration-time constant; in the other it is state. That is not a stylistic difference between the two files — it is the protocol difference, expressed in the only way hardware can express it.
3. The Designs (Verilog-2005)
// =====================================================================
// TWO SELECTION DISCIPLINES, SIDE BY SIDE.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// Two separate modules, because they ARE two separate designs -- that
// is the comparison. Neither is a complete controller: there is no SPI
// shift register here and no USB packet decoder, only the part that
// answers one question.
//
// "Which peripheral should respond to this transaction?"
//
// SPI answers it with a WIRE. A board designer routes one chip-select
// line per device, and the master pulls one of them low. The answer is
// fixed at layout time and is not visible to anybody except the master
// and the one slave being addressed.
//
// USB answers it with a FIELD. Every token on the bus carries an
// address and an endpoint number, every device sees every token, and
// the address was ASSIGNED BY THE HOST at enumeration -- so the answer
// is run-time state that can be changed, queried and checked.
//
// The difference that matters is not the wire count. It is this:
//
// SPI's selection information is DISTRIBUTED (each slave sees only
// its own select line). USB's is BROADCAST (every device sees the
// whole address field).
//
// A distributed selector cannot detect its own worst failure. Section
// 7 of the chapter measures exactly that, and the RTL below is built
// so the measurement is possible rather than assumed.
// =====================================================================
// ---------------------------------------------------------------------
// spi_cs_select -- selection by wire.
//
// IMPORTANT AND EASY TO MISS: this module is given ALL N chip-select
// lines. No real SPI slave has that. A real slave receives exactly one
// CS pin and has no way to know whether any other slave is also
// selected. So `contention` below is a BUS MONITOR output -- it is
// observable only from a vantage point that no SPI device on a real
// board occupies.
//
// That is not a limitation of this model. It is the finding.
// ---------------------------------------------------------------------
module spi_cs_select #(
parameter integer N_DEV = 4
) (
input wire clk,
input wire rst_n,
// Active-low chip selects, one per device, routed on the board.
input wire [N_DEV-1:0] cs_n,
// Resolved selection.
output wire sel_valid,
output wire [$clog2(N_DEV)-1:0] sel_idx,
// The failure a real slave cannot see.
output wire contention,
output wire [31:0] n_sel,
output wire [31:0] n_idle,
output wire [31:0] n_contend
);
localparam integer IW = $clog2(N_DEV);
// How many selects are asserted. On a correct board this is 0 or 1;
// two at once is a firmware or routing fault, and on real hardware it
// means two slaves drive MISO simultaneously.
reg [IW:0] n_low;
reg [IW-1:0] first_low;
integer i;
always @(*) begin
n_low = {(IW+1){1'b0}};
first_low = {IW{1'b0}};
// Walk downwards so the LOWEST asserted index wins the tie. The
// direction is arbitrary but it must be DEFINED: an undefined
// winner makes the contention case unverifiable.
for (i = N_DEV - 1; i >= 0; i = i - 1) begin
if (!cs_n[i]) begin
n_low = n_low + 1'b1;
first_low = i[IW-1:0];
end
end
end
assign contention = (n_low > 1);
assign sel_valid = (n_low == 1);
assign sel_idx = first_low;
reg [31:0] sel_c, idle_c, con_c;
assign n_sel = sel_c;
assign n_idle = idle_c;
assign n_contend = con_c;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sel_c <= 32'd0;
idle_c <= 32'd0;
con_c <= 32'd0;
end else begin
if (n_low == 0) idle_c <= idle_c + 32'd1;
else if (n_low == 1) sel_c <= sel_c + 32'd1;
else con_c <= con_c + 32'd1;
end
end
endmodule
// ---------------------------------------------------------------------
// usb_addr_select -- selection by assigned name.
//
// A device slot holds an address the HOST gave it, plus a mask of
// which endpoints are configured. A token matches a slot only if the
// address matches AND that endpoint is configured.
//
// Two real USB rules are enforced here because both are places where
// an implementation can be plausibly wrong:
//
// * Address 0 is the DEFAULT address. A device sitting at address 0
// is mid-enumeration and has only endpoint 0. Honouring any other
// endpoint at address 0 would let a half-enumerated device answer
// traffic it has no configuration for.
//
// * An UNASSIGNED slot matches nothing at all, including address 0.
// A slot that answered before being assigned would respond to the
// enumeration of a different device.
//
// Unlike the SPI decoder, this one CAN see its own worst failure: two
// slots holding the same address is directly detectable, because the
// address is a field every slot compares against.
// ---------------------------------------------------------------------
module usb_addr_select #(
parameter integer N_SLOT = 4
) (
input wire clk,
input wire rst_n,
// ---- the assignment interface: this is the whole difference ----
//
// The SPI decoder above has no equivalent port. Its mapping is fixed
// at elaboration. This one's mapping is a register file, writable at
// run time, which is what "the host assigns an address" means in
// hardware.
input wire cfg_valid,
input wire [$clog2(N_SLOT)-1:0] cfg_slot,
input wire cfg_assigned,
input wire [6:0] cfg_addr,
input wire [15:0] cfg_ep_mask,
// ---- a token ----
input wire req_valid,
input wire [6:0] req_addr,
input wire [3:0] req_ep,
// ---- the answer ----
output wire sel_valid,
output wire [$clog2(N_SLOT)-1:0] sel_slot,
output wire [2:0] sel_reason,
// The failure this discipline CAN see.
output wire conflict,
output wire [31:0] n_match,
output wire [31:0] n_no_addr,
output wire [31:0] n_no_ep,
output wire [31:0] n_conflict
);
localparam integer SW = $clog2(N_SLOT);
localparam [2:0] R_NONE = 3'd0, // no token this cycle
R_MATCH = 3'd1, // addressed and configured
R_NO_ADDR = 3'd2, // nobody holds that address
R_NO_EP = 3'd3; // address matched, endpoint did not
// The assignment register file -- per-field arrays rather than an
// array of structs, because a variable field-select into an unpacked
// array of packed structs aborts the Icarus elaborator.
reg sl_asg [0:N_SLOT-1];
reg [6:0] sl_addr [0:N_SLOT-1];
reg [15:0] sl_epm [0:N_SLOT-1];
integer j;
// ---- the match, combinational over the registered table ----
reg m_any, m_ep_any;
reg [SW-1:0] m_slot;
reg [SW:0] m_addr_count;
always @(*) begin
m_any = 1'b0;
m_ep_any = 1'b0;
m_slot = {SW{1'b0}};
m_addr_count = {(SW+1){1'b0}};
// Walk downwards so the LOWEST matching slot wins. Defined, not
// arbitrary: the conflict case must have a predictable winner or it
// cannot be checked.
for (j = N_SLOT - 1; j >= 0; j = j - 1) begin
if (sl_asg[j] && (sl_addr[j] == req_addr)) begin
m_addr_count = m_addr_count + 1'b1;
m_any = 1'b1;
m_slot = j[SW-1:0];
// Address 0 is the default address: endpoint 0 only. A device
// there has no configuration yet, so honouring any other
// endpoint would answer traffic it cannot service.
if (sl_addr[j] == 7'd0) begin
if (req_ep == 4'd0) m_ep_any = 1'b1;
end else begin
if (sl_epm[j][req_ep]) m_ep_any = 1'b1;
end
end
end
end
assign conflict = (m_addr_count > 1);
assign sel_valid = req_valid && m_any && m_ep_any;
assign sel_slot = m_slot;
assign sel_reason = !req_valid ? R_NONE :
!m_any ? R_NO_ADDR :
!m_ep_any ? R_NO_EP : R_MATCH;
reg [31:0] match_c, noaddr_c, noep_c, conf_c;
assign n_match = match_c;
assign n_no_addr = noaddr_c;
assign n_no_ep = noep_c;
assign n_conflict = conf_c;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (j = 0; j < N_SLOT; j = j + 1) begin
sl_asg[j] <= 1'b0;
sl_addr[j] <= 7'd0;
sl_epm[j] <= 16'd0;
end
match_c <= 32'd0;
noaddr_c <= 32'd0;
noep_c <= 32'd0;
conf_c <= 32'd0;
end else begin
if (cfg_valid) begin
sl_asg[cfg_slot] <= cfg_assigned;
sl_addr[cfg_slot] <= cfg_addr;
sl_epm[cfg_slot] <= cfg_ep_mask;
end
if (req_valid) begin
case (sel_reason)
R_MATCH: match_c <= match_c + 32'd1;
R_NO_ADDR: noaddr_c <= noaddr_c + 32'd1;
R_NO_EP: noep_c <= noep_c + 32'd1;
default: ;
endcase
if (conflict) conf_c <= conf_c + 32'd1;
end
end
end
endmoduleThe one comment in that file worth reading twice
spi_cs_select is handed all N chip-select lines. No real SPI slave has
that.
A slave on a real board receives exactly one CS pin. It cannot see the other N−1 lines, it has no port through which it could, and adding one would mean routing every select line to every device — which is both a different bus and a worse one.
So the contention output is a bus monitor: it is observable only from a
vantage point that nothing on a real SPI board occupies. That is not a
shortcoming of the model. It is the finding, and section 6 measures it.
4. The Two Real USB Rules In The Matcher
usb_addr_select enforces two rules that are easy to state and easy to get
wrong, and both are places a plausible implementation goes astray.
Address 0 is the default address, and it exposes endpoint 0 only. A device sitting at address 0 is mid-enumeration: it has been reset and given the default address, but it has not yet been configured. Honouring any other endpoint there would let a half-enumerated device answer traffic it has no configuration to service. This is mutation L5.
An unassigned slot matches nothing at all, including address 0. A slot that answered before being assigned would respond during another device's enumeration — which is the single most destructive thing a USB device can do on a shared bus, because it corrupts the enumeration of a device that was behaving correctly. This is mutation L4, and it is the highest-scoring single-condition mutation in the chapter.
Tie resolution is defined, not arbitrary
Both designs walk their index space downwards so the lowest index wins. Either direction would be defensible; what is not defensible is leaving it unspecified, because then the conflict case has no predictable outcome and cannot be checked at all. Mutation L3 flips the direction, and the testbench catches it on every one of the 11 patterns where it makes a difference.
Both shadow models walk upwards and stop at the first hit. Same answer, different derivation — which is the only way a model can fail to inherit the design's mistake.
5. Distributed Versus Broadcast
The same question, answered from two different amounts of information
The asymmetry is not about effort or cleverness. It is about how much information reaches the place where the decision is made. A slave that receives one bit cannot compute a property of four bits, whatever logic you put behind that bit. A device that receives the whole address field gets the count for free.
6. The Measurement
For each failure the two disciplines admit, could a real device detect it from what that device actually receives?
This is not a pass/fail. It is a distinguishability test, and it is constructed rather than asserted — the bench builds each observer's view and compares it against the view that observer would have had in the clean case. If the two views are equal, no logic inside that observer can tell them apart, whatever it does.
The SPI side
With four devices there are 2⁴ = 16 chip-select patterns, of which 11 assert two or more selects. For each of those, and for each selected slave k:
slave k's entire view of the world = cs_n[k]
slave k's view if it alone were selected = 0 (its own line low)
the two are EQUAL for every k and every contention patternSo the result is 0 detectable out of 11, and it is not a measurement of a weak design. It is a proof: the two situations are represented by identical inputs at the only place the slave can observe, so no slave can distinguish them. On real hardware both selected slaves drive MISO, the master reads the wired-AND of two answers, and every participant believes the transaction succeeded.
The bench also confirms the bus monitor sees all 11 — which is the point.
The detector exists only because spi_cs_select was handed every wire.
The USB side
Measured on every one of the 2560 exhaustive tokens, not on a directed handful, so the denominator is every duplicate-address token the sweep produces: 274 detected out of 274.
| failures admitted | detectable by a real device | |
|---|---|---|
| SPI chip-select contention | 11 patterns | 0 |
| USB duplicate address | 274 tokens | 274 |
Where the 2560 comes from
| dimension | values | why |
|---|---|---|
| assigned mask | 16 | which of the 4 slots have been given an address |
| address mode | 4 | all distinct · all identical · all at address 0 · pairwise duplicates |
| endpoint mode | 2 | all four endpoints configured · only endpoints 0 and 2 |
| request address | 5 | 0 through 4 |
| request endpoint | 4 | 0 through 3 |
16 × 4 × 2 × 5 × 4 = 2560, every point reachable, because the five dimensions are independent inputs with no forbidden combinations.
The address-mode dimension is the one that earns its place. Without modes 1 and 3 every slot would hold a distinct address, the conflict logic would never fire, and the sweep would report complete coverage of a design whose conflict detector had never been exercised. An exhaustive sweep that cannot reach a mechanism is not exhaustive over anything that matters.
7. What Run-Time Addressing Actually Buys
A device being addressed, re-addressed, and switched off — all without touching the board
Three capabilities in that trace, none of which spi_cs_select can express:
A device can be given an address it did not have. That is enumeration. On SPI the equivalent operation is soldering.
A device can be given a different address later. USB does this on every bus reset: every device drops back to the default address and is re-enumerated. A bus that can renumber its participants can also recover from a participant disappearing, which is what hot-plug is.
A device can be switched off logically. An unassigned slot answers nothing, including the default address. On SPI, deselecting a device means not asserting its wire — which is not the same thing, because the wire is still there and still capable of selecting it.
8. When A Configuration Takes Effect
The design registers its table, so a token presented in the same cycle as a configuration write sees the old table. The write lands on the next edge.
This sounds pedantic and is not. It is the single most common shadow-model error in this whole track: the bench applies the write immediately, the design applies it a cycle later, and the resulting disagreement gets mistaken for a design bug. So the bench mirrors each write only after the edge, and phase 5 checks the boundary explicitly:
cfg_valid = 1'b1; cfg_slot = 2'd1; cfg_assigned = 1'b1;
cfg_addr = 7'd8; cfg_ep_mask = 16'h000F;
req_valid = 1'b1; req_addr = 7'd7; req_ep = 4'd0;
#1;
ck(usb_sel_valid === 1'b1,
"a configuration write took effect in the same cycle as the token");A design that applied the write early would disagree here and nowhere else in the entire run — 2560 exhaustive tokens would all still pass, because none of them writes and reads in the same cycle. That is what a one-cycle boundary check is for.
9. The Testbench (Verilog)
// =====================================================================
// Testbench for spi_cs_select and usb_addr_select.
//
// BOTH DISCIPLINES, ONE BENCH, ONE STIMULUS SOURCE. The point of the
// chapter is a comparison, so the comparison happens inside the
// verification rather than in prose afterwards.
//
// THE SHADOW MODELS ARE FORMULATED IN THE OPPOSITE DIRECTION.
// Both designs resolve ties by walking their index space DOWNWARDS so
// the lowest index wins. Both models walk UPWARDS and stop at the
// first hit. Same answer, different derivation -- which is the only
// way a model can fail to inherit the design's mistake.
//
// THE HEADLINE MEASUREMENT is not a pass/fail. It is:
//
// of the selection failures each discipline admits, how many are
// DETECTABLE FROM THE VANTAGE POINT A REAL DEVICE OCCUPIES?
//
// An SPI slave receives exactly one chip-select pin. A USB device sees
// every token's whole address field. That asymmetry is measured in
// phase 3, and it is measured by CONSTRUCTING each observer's view and
// comparing it against the view it would have had in the clean case --
// not by asserting the conclusion.
// =====================================================================
`timescale 1ns/1ps
module tb_sl_v;
localparam integer N_DEV = 4;
localparam integer N_SLOT = 4;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
// ---- SPI side ----
reg [N_DEV-1:0] cs_n = {N_DEV{1'b1}};
wire spi_sel_valid, spi_contention;
wire [1:0] spi_sel_idx;
wire [31:0] spi_n_sel, spi_n_idle, spi_n_contend;
spi_cs_select #(.N_DEV(N_DEV)) dut_spi (
.clk(clk), .rst_n(rst_n), .cs_n(cs_n),
.sel_valid(spi_sel_valid), .sel_idx(spi_sel_idx),
.contention(spi_contention),
.n_sel(spi_n_sel), .n_idle(spi_n_idle), .n_contend(spi_n_contend)
);
// ---- USB side ----
reg cfg_valid = 1'b0;
reg [1:0] cfg_slot = 2'd0;
reg cfg_assigned = 1'b0;
reg [6:0] cfg_addr = 7'd0;
reg [15:0] cfg_ep_mask = 16'd0;
reg req_valid = 1'b0;
reg [6:0] req_addr = 7'd0;
reg [3:0] req_ep = 4'd0;
wire usb_sel_valid, usb_conflict;
wire [1:0] usb_sel_slot;
wire [2:0] usb_sel_reason;
wire [31:0] usb_n_match, usb_n_no_addr, usb_n_no_ep, usb_n_conflict;
usb_addr_select #(.N_SLOT(N_SLOT)) dut_usb (
.clk(clk), .rst_n(rst_n),
.cfg_valid(cfg_valid), .cfg_slot(cfg_slot),
.cfg_assigned(cfg_assigned), .cfg_addr(cfg_addr),
.cfg_ep_mask(cfg_ep_mask),
.req_valid(req_valid), .req_addr(req_addr), .req_ep(req_ep),
.sel_valid(usb_sel_valid), .sel_slot(usb_sel_slot),
.sel_reason(usb_sel_reason), .conflict(usb_conflict),
.n_match(usb_n_match), .n_no_addr(usb_n_no_addr),
.n_no_ep(usb_n_no_ep), .n_conflict(usb_n_conflict)
);
localparam [2:0] R_NONE = 3'd0, R_MATCH = 3'd1,
R_NO_ADDR = 3'd2, R_NO_EP = 3'd3;
integer errors = 0, checks = 0, steps = 0;
// ---- the headline measurement ----
integer spi_contend_patterns = 0; // patterns with >= 2 selects asserted
integer spi_contend_visible = 0; // ... distinguishable by SOME slave
integer usb_conflict_cases = 0; // tokens hitting a duplicate address
integer usb_conflict_visible = 0; // ... detected by the device logic
integer seed;
// $random is SIGNED: mask the sign bit before any modulo, or every
// `% N` is negative about half the time and the sweep silently
// collapses onto a subset of its range.
function [31:0] urand;
input dummy;
begin urand = $random(seed) & 32'h3FFF_FFFF; end
endfunction
task ck(input cond, input [255:0] what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t step#%0d: %0s", $time, steps, what);
end
end
endtask
// ---- the shadow copy of the assignment table ----
//
// Maintained by the BENCH from the writes it issued, never read back
// from the design. A model that reads the design's own table cannot
// detect a design that writes the wrong entry.
reg s_asg [0:N_SLOT-1];
reg [6:0] s_addr [0:N_SLOT-1];
reg [15:0] s_epm [0:N_SLOT-1];
// ---------------------------------------------------------------
// SPI model -- counts upward, unlike the design's downward walk.
// ---------------------------------------------------------------
function [2:0] spi_popcount(input [N_DEV-1:0] p);
integer i; reg [2:0] c;
begin
c = 3'd0;
for (i = 0; i < N_DEV; i = i + 1) if (!p[i]) c = c + 3'd1;
spi_popcount = c;
end
endfunction
function [1:0] spi_first_low(input [N_DEV-1:0] p);
integer i; reg found; reg [1:0] r;
begin
found = 1'b0; r = 2'd0;
// upward, stop at the first. The design walks downward and
// overwrites, so the two agree only if both are right.
for (i = 0; i < N_DEV; i = i + 1)
if (!p[i] && !found) begin r = i[1:0]; found = 1'b1; end
spi_first_low = r;
end
endfunction
// ---------------------------------------------------------------
// USB model -- also upward-and-stop, over the bench's own table.
// ---------------------------------------------------------------
function [2:0] usb_addr_count(input [6:0] a);
integer j; reg [2:0] c;
begin
c = 3'd0;
for (j = 0; j < N_SLOT; j = j + 1)
if (s_asg[j] && (s_addr[j] == a)) c = c + 3'd1;
usb_addr_count = c;
end
endfunction
function [1:0] usb_first_slot(input [6:0] a);
integer j; reg found; reg [1:0] r;
begin
found = 1'b0; r = 2'd0;
for (j = 0; j < N_SLOT; j = j + 1)
if (s_asg[j] && (s_addr[j] == a) && !found) begin r = j[1:0]; found = 1'b1; end
usb_first_slot = r;
end
endfunction
// Does ANY assigned slot at address `a` expose endpoint `e`?
//
// The default-address rule lives here, stated independently of the
// design: a slot at address 0 is mid-enumeration and has endpoint 0
// only, whatever its ep_mask happens to contain.
function usb_ep_ok(input [6:0] a, input [3:0] e);
integer j; reg ok;
begin
ok = 1'b0;
for (j = 0; j < N_SLOT; j = j + 1) begin
if (s_asg[j] && (s_addr[j] == a)) begin
if (s_addr[j] == 7'd0) begin
if (e == 4'd0) ok = 1'b1;
end else begin
if (s_epm[j][e]) ok = 1'b1;
end
end
end
usb_ep_ok = ok;
end
endfunction
// ---------------------------------------------------------------
// Drive one SPI pattern and check the resolution.
// ---------------------------------------------------------------
task spi_step(input [N_DEV-1:0] p);
reg [2:0] nlow;
reg [1:0] efirst;
reg [31:0] s0, i0, c0;
begin
nlow = spi_popcount(p);
efirst = spi_first_low(p);
s0 = spi_n_sel; i0 = spi_n_idle; c0 = spi_n_contend;
cs_n = p;
#1;
// ---- PROPERTY 1: exactly one low is a selection ----
ck(spi_sel_valid === (nlow == 3'd1),
"spi sel_valid does not mean exactly one chip select is asserted");
// ---- PROPERTY 2: two or more low is contention ----
ck(spi_contention === (nlow > 3'd1),
"spi contention does not mean two or more chip selects are asserted");
// ---- PROPERTY 3: a selection and a contention are exclusive ----
ck(!(spi_sel_valid && spi_contention),
"spi reported a selection and a contention at the same time");
// ---- PROPERTY 4: the winner is the lowest asserted index ----
if (nlow >= 3'd1)
ck(spi_sel_idx === efirst,
"spi did not resolve the tie to the lowest asserted index");
@(posedge clk); #1;
// ---- PROPERTY 5: exactly one counter moved ----
if (nlow == 3'd0) begin
ck(spi_n_idle == i0 + 32'd1 && spi_n_sel == s0 && spi_n_contend == c0,
"spi counters wrong for an idle bus");
end else if (nlow == 3'd1) begin
ck(spi_n_sel == s0 + 32'd1 && spi_n_idle == i0 && spi_n_contend == c0,
"spi counters wrong for a clean selection");
end else begin
ck(spi_n_contend == c0 + 32'd1 && spi_n_sel == s0 && spi_n_idle == i0,
"spi counters wrong for a contention");
end
steps = steps + 1;
end
endtask
// ---------------------------------------------------------------
// Write one assignment slot, mirroring it into the bench's table.
// The mirror happens AFTER the edge on purpose -- see phase 5.
// ---------------------------------------------------------------
task do_cfg(input [1:0] slot, input asg, input [6:0] a, input [15:0] epm);
begin
cfg_valid = 1'b1; cfg_slot = slot; cfg_assigned = asg;
cfg_addr = a; cfg_ep_mask = epm;
@(posedge clk); #1;
cfg_valid = 1'b0;
s_asg[slot] = asg;
s_addr[slot] = a;
s_epm[slot] = epm;
end
endtask
// ---------------------------------------------------------------
// Present one token and check the resolution.
// ---------------------------------------------------------------
task usb_step(input [6:0] a, input [3:0] e);
reg [2:0] nmatch, ereason;
reg [1:0] efirst;
reg eep, eany;
reg [31:0] m0, na0, ne0, cf0;
begin
nmatch = usb_addr_count(a);
efirst = usb_first_slot(a);
eany = (nmatch > 3'd0);
eep = usb_ep_ok(a, e);
ereason = !eany ? R_NO_ADDR : (!eep ? R_NO_EP : R_MATCH);
m0 = usb_n_match; na0 = usb_n_no_addr;
ne0 = usb_n_no_ep; cf0 = usb_n_conflict;
req_valid = 1'b1; req_addr = a; req_ep = e;
#1;
// ---- PROPERTY 6: selection means addressed AND configured ----
ck(usb_sel_valid === (eany && eep),
"usb sel_valid does not mean the address matched and the endpoint was configured");
// ---- PROPERTY 7: the reason code is exact ----
ck(usb_sel_reason === ereason, "usb sel_reason disagrees with the model");
// ---- PROPERTY 8: the winner is the lowest matching slot ----
if (eany)
ck(usb_sel_slot === efirst,
"usb did not resolve a duplicate address to the lowest slot");
// ---- PROPERTY 9: a duplicate address is DETECTED ----
//
// The property the SPI decoder has no equivalent of, and the
// reason is structural rather than a matter of effort: the address
// is a field every slot compares against, so the count is
// available on the spot. A chip select is a wire only one slave
// ever receives.
ck(usb_conflict === (nmatch > 3'd1),
"usb conflict does not mean two assigned slots share the address");
// The measurement, taken on EVERY token rather than on a directed
// handful. The denominator is therefore every duplicate-address
// token the exhaustive sweep produces, which makes it comparable
// in weight to the SPI enumeration in phase 3.
if (nmatch > 3'd1) begin
usb_conflict_cases = usb_conflict_cases + 1;
if (usb_conflict === 1'b1)
usb_conflict_visible = usb_conflict_visible + 1;
end
@(posedge clk); #1;
req_valid = 1'b0;
// ---- PROPERTY 10: exactly one reason counter moved ----
case (ereason)
R_MATCH: ck(usb_n_match == m0 + 32'd1 && usb_n_no_addr == na0 && usb_n_no_ep == ne0,
"usb counters wrong for a match");
R_NO_ADDR: ck(usb_n_no_addr == na0 + 32'd1 && usb_n_match == m0 && usb_n_no_ep == ne0,
"usb counters wrong for an unknown address");
R_NO_EP: ck(usb_n_no_ep == ne0 + 32'd1 && usb_n_match == m0 && usb_n_no_addr == na0,
"usb counters wrong for an unconfigured endpoint");
default: ;
endcase
ck(usb_n_conflict == cf0 + ((nmatch > 3'd1) ? 32'd1 : 32'd0),
"usb conflict counter disagrees with the model");
steps = steps + 1;
end
endtask
task reset_dut;
integer j;
begin
rst_n = 1'b0;
cs_n = {N_DEV{1'b1}};
cfg_valid = 1'b0; req_valid = 1'b0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
for (j = 0; j < N_SLOT; j = j + 1) begin
s_asg[j] = 1'b0; s_addr[j] = 7'd0; s_epm[j] = 16'd0;
end
end
endtask
// ---- exhaustive reach ----
//
// SPI: 2**N_DEV chip-select patterns = 16.
// USB: asg_mask(16) x addr_mode(4) x ep_mode(2) x req_addr(5) x req_ep(4)
// = 2560. Every dimension is an independent input with no
// forbidden combinations, so the denominator is exactly 2560 and a
// sweep reporting less is broken rather than constrained.
reg reach_spi [0:15];
reg reach_usb [0:2559];
integer nrs, nru, ri;
integer pat, am, em, ra, re, asg, j2, k;
reg [6:0] addr_of;
reg [15:0] epm_of;
reg any_slave_knows, view_here, view_clean;
initial begin
for (ri = 0; ri < 16; ri = ri + 1) reach_spi[ri] = 1'b0;
for (ri = 0; ri < 2560; ri = ri + 1) reach_usb[ri] = 1'b0;
seed = 32'd28002;
reset_dut;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every chip-select pattern.
// 16 of 16, nothing unreachable.
// =============================================================
for (pat = 0; pat < 16; pat = pat + 1) begin
spi_step(pat[N_DEV-1:0]);
reach_spi[pat] = 1'b1;
end
cs_n = {N_DEV{1'b1}};
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE) -- the USB selection space.
//
// The table is reconfigured once per (asg_mask, addr_mode,
// ep_mode) and then every (req_addr, req_ep) is presented against
// it: 128 configurations x 20 tokens.
//
// addr_mode exists so duplicate addresses are REACHED rather than
// assumed away. Mode 1 gives every slot the same address, mode 3
// gives pairwise duplicates, mode 2 puts everything at the default
// address. A sweep with distinct addresses only would leave the
// conflict logic at zero coverage while reporting completeness.
// =============================================================
for (asg = 0; asg < 16; asg = asg + 1)
for (am = 0; am < 4; am = am + 1)
for (em = 0; em < 2; em = em + 1) begin
for (j2 = 0; j2 < N_SLOT; j2 = j2 + 1) begin
case (am)
0: addr_of = j2[6:0] + 7'd1; // all distinct
1: addr_of = 7'd1; // all identical
2: addr_of = 7'd0; // all at default
default: addr_of = (j2[6:0] % 7'd2) + 7'd1; // pairwise duplicates
endcase
// ep_mode 1 leaves endpoints 1 and 3 unconfigured, so R_NO_EP is
// reachable at a NON-zero address and not only through the
// default-address rule.
epm_of = (em == 0) ? 16'h000F : 16'h0005;
do_cfg(j2[1:0], asg[j2], addr_of, epm_of);
end
for (ra = 0; ra < 5; ra = ra + 1)
for (re = 0; re < 4; re = re + 1) begin
usb_step(ra[6:0], re[3:0]);
ri = ((((asg * 4 + am) * 2 + em) * 5 + ra) * 4 + re);
reach_usb[ri] = 1'b1;
end
end
// =============================================================
// PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE MEASUREMENT.
//
// Not a pass/fail. For every failure each discipline admits, could
// a real device detect it from what that device actually receives?
//
// An SPI slave receives ONE chip-select pin, so slave k's entire
// view of the world is cs_n[k]. The test is a DISTINGUISHABILITY
// test, constructed rather than asserted: compare slave k's view
// under the contention pattern against its view under the clean
// pattern in which only k is selected. If the two views are equal,
// no logic inside slave k can tell them apart, whatever it does.
// =============================================================
for (pat = 0; pat < 16; pat = pat + 1) begin
if (spi_popcount(pat[N_DEV-1:0]) > 3'd1) begin
spi_contend_patterns = spi_contend_patterns + 1;
any_slave_knows = 1'b0;
for (k = 0; k < N_DEV; k = k + 1) begin
if (!pat[k]) begin
// what slave k sees now
view_here = pat[k];
// what slave k would see if it alone were selected: its own
// line low. Every other line is invisible to it.
view_clean = 1'b0;
if (view_here !== view_clean) any_slave_knows = 1'b1;
end
end
if (any_slave_knows) spi_contend_visible = spi_contend_visible + 1;
// And confirm the BUS MONITOR does see it -- the detector exists
// only because this module was handed all N lines, which no slave
// on a real board is.
cs_n = pat[N_DEV-1:0]; #1;
ck(spi_contention === 1'b1,
"the bus monitor failed to see a contention it was given every wire for");
@(posedge clk); #1;
end
end
cs_n = {N_DEV{1'b1}};
// The USB side of this question is measured inside usb_step, on
// every one of the 2560 exhaustive tokens, so there is nothing to
// add here -- the numbers are reported at the end of the run.
// =============================================================
// PHASE 4 (DIRECTED) -- RE-ADDRESSING AT RUN TIME.
//
// The capability SPI does not have and cannot be given without
// changing the board. Assign a slot, verify it answers, re-assign
// it, verify the OLD address has gone silent and the new one
// answers, then unassign it entirely.
//
// The absence of a comparable phase on the SPI side is not an
// omission in this bench. spi_cs_select HAS NO CONFIGURATION PORT
// -- its mapping is fixed at elaboration -- and that missing port
// is the finding.
// =============================================================
reset_dut;
do_cfg(2'd0, 1'b1, 7'd5, 16'h000F);
usb_step(7'd5, 4'd0); // answers at 5
do_cfg(2'd0, 1'b1, 7'd9, 16'h000F); // re-addressed to 9
usb_step(7'd5, 4'd0); // must now be silent at 5
usb_step(7'd9, 4'd0); // and answer at 9
do_cfg(2'd0, 1'b0, 7'd9, 16'h000F); // unassigned entirely
usb_step(7'd9, 4'd0); // silent again
// An unassigned slot must not answer even at the default address, or
// it would respond during another device's enumeration.
usb_step(7'd0, 4'd0);
// =============================================================
// PHASE 5 (DIRECTED) -- WHEN A CONFIGURATION TAKES EFFECT.
//
// The design registers the table, so a token presented in the SAME
// cycle as a write must see the OLD table. Getting this backwards
// is a classic shadow-model error, and it earns an explicit phase:
// do_cfg mirrors the write only AFTER the edge, so a design that
// applied it early would disagree here and nowhere else in the run.
// =============================================================
reset_dut;
do_cfg(2'd1, 1'b1, 7'd7, 16'h000F);
// A same-cycle write moving the slot to a different address, plus a
// token at the OLD one: the token must still match.
cfg_valid = 1'b1; cfg_slot = 2'd1; cfg_assigned = 1'b1;
cfg_addr = 7'd8; cfg_ep_mask = 16'h000F;
req_valid = 1'b1; req_addr = 7'd7; req_ep = 4'd0;
#1;
ck(usb_sel_valid === 1'b1,
"a configuration write took effect in the same cycle as the token");
ck(usb_sel_reason === R_MATCH,
"same-cycle reason code should still be MATCH at the old address");
@(posedge clk); #1;
cfg_valid = 1'b0; req_valid = 1'b0;
s_asg[1] = 1'b1; s_addr[1] = 7'd8; s_epm[1] = 16'h000F;
// and from the next cycle the new address is the live one
usb_step(7'd8, 4'd0);
usb_step(7'd7, 4'd0);
// =============================================================
// PHASE 6 (RANDOM)
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 400; k = k + 1) begin
if ((urand(0) % 3) == 0)
do_cfg((urand(0) % 4), (urand(0) % 4) != 0,
(urand(0) % 6), (urand(0) % 2) ? 16'h000F : 16'h0005);
usb_step((urand(0) % 6), (urand(0) % 4));
spi_step((urand(0) % 16));
end
`endif
nrs = 0; for (ri = 0; ri < 16; ri = ri + 1) if (reach_spi[ri]) nrs = nrs + 1;
nru = 0; for (ri = 0; ri < 2560; ri = ri + 1) if (reach_usb[ri]) nru = nru + 1;
$display("steps=%0d checks=%0d reach_spi=%0d/16 reach_usb=%0d/2560 errors=%0d",
steps, checks, nrs, nru, errors);
$display("[spi] selections=%0d idle=%0d CONTENTIONS=%0d",
spi_n_sel, spi_n_idle, spi_n_contend);
$display("[usb] matches=%0d no_addr=%0d no_ep=%0d CONFLICTS=%0d",
usb_n_match, usb_n_no_addr, usb_n_no_ep, usb_n_conflict);
$display("--- detectability from the vantage point a real device occupies ---");
$display("[spi] contention patterns=%0d distinguishable by any slave=%0d",
spi_contend_patterns, spi_contend_visible);
$display("[usb] duplicate-address tokens=%0d detected by the device=%0d",
usb_conflict_cases, usb_conflict_visible);
if (nrs != 16 || nru != 2560) begin
$display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmodule10. SystemVerilog
// =====================================================================
// TWO SELECTION DISCIPLINES, SIDE BY SIDE -- SystemVerilog.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// Same hardware contract as the Verilog file: same ports, same widths,
// same reset values, same cycle-by-cycle behaviour. What changes is
// that the reason code becomes a named type, so a reason that does not
// exist cannot be produced.
//
// The question both modules answer:
//
// "Which peripheral should respond to this transaction?"
//
// SPI answers it with a WIRE routed at layout time. USB answers it with
// a FIELD every device sees, holding an address the HOST assigned.
//
// The difference that matters is not the wire count:
//
// SPI's selection information is DISTRIBUTED -- each slave sees
// only its own select line. USB's is BROADCAST -- every device
// sees the whole address field.
//
// A distributed selector cannot detect its own worst failure.
// =====================================================================
// ---------------------------------------------------------------------
// spi_cs_select -- selection by wire.
//
// IMPORTANT: this module is given ALL N chip-select lines. No real SPI
// slave has that. A real slave receives exactly one CS pin and cannot
// know whether another slave is also selected. So `contention` is a
// BUS MONITOR output, observable only from a vantage point that no SPI
// device on a real board occupies.
//
// That is not a limitation of the model. It is the finding.
// ---------------------------------------------------------------------
module spi_cs_select #(
parameter int N_DEV = 4
) (
input logic clk,
input logic rst_n,
// Active-low chip selects, one per device, routed on the board.
input logic [N_DEV-1:0] cs_n,
output logic sel_valid,
output logic [$clog2(N_DEV)-1:0] sel_idx,
// The failure a real slave cannot see.
output logic contention,
output logic [31:0] n_sel,
output logic [31:0] n_idle,
output logic [31:0] n_contend
);
localparam int IW = $clog2(N_DEV);
// How many selects are asserted. On a correct board 0 or 1; two at
// once is a firmware or routing fault, and on real hardware it means
// two slaves drive MISO simultaneously.
logic [IW:0] n_low;
logic [IW-1:0] first_low;
always_comb begin
n_low = '0;
first_low = '0;
// Downwards, so the LOWEST asserted index wins the tie. The
// direction is arbitrary but it must be DEFINED: an undefined winner
// makes the contention case unverifiable.
for (int i = N_DEV - 1; i >= 0; i--) begin
if (!cs_n[i]) begin
n_low = n_low + 1'b1;
first_low = IW'(i);
end
end
end
assign contention = (n_low > 1);
assign sel_valid = (n_low == 1);
assign sel_idx = first_low;
logic [31:0] sel_c, idle_c, con_c;
assign n_sel = sel_c;
assign n_idle = idle_c;
assign n_contend = con_c;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sel_c <= 32'd0;
idle_c <= 32'd0;
con_c <= 32'd0;
end else begin
if (n_low == 0) idle_c <= idle_c + 32'd1;
else if (n_low == 1) sel_c <= sel_c + 32'd1;
else con_c <= con_c + 32'd1;
end
end
endmodule
// ---------------------------------------------------------------------
// usb_addr_select -- selection by assigned name.
//
// Two real USB rules are enforced, because both are places an
// implementation can be plausibly wrong:
//
// * Address 0 is the DEFAULT address. A device there is
// mid-enumeration and has endpoint 0 only. Honouring any other
// endpoint would let a half-enumerated device answer traffic it has
// no configuration for.
//
// * An UNASSIGNED slot matches nothing, including address 0. A slot
// that answered before being assigned would respond during another
// device's enumeration.
//
// Unlike the SPI decoder, this one CAN see its own worst failure: two
// slots holding the same address is directly detectable, because the
// address is a field every slot compares against.
// ---------------------------------------------------------------------
module usb_addr_select #(
parameter int N_SLOT = 4
) (
input logic clk,
input logic rst_n,
// ---- the assignment interface: this is the whole difference ----
//
// spi_cs_select has no equivalent port. Its mapping is fixed at
// elaboration. This one's mapping is a register file, writable at run
// time, which is what "the host assigns an address" means in hardware.
input logic cfg_valid,
input logic [$clog2(N_SLOT)-1:0] cfg_slot,
input logic cfg_assigned,
input logic [6:0] cfg_addr,
input logic [15:0] cfg_ep_mask,
input logic req_valid,
input logic [6:0] req_addr,
input logic [3:0] req_ep,
output logic sel_valid,
output logic [$clog2(N_SLOT)-1:0] sel_slot,
output logic [2:0] sel_reason,
// The failure this discipline CAN see.
output logic conflict,
output logic [31:0] n_match,
output logic [31:0] n_no_addr,
output logic [31:0] n_no_ep,
output logic [31:0] n_conflict
);
localparam int SW = $clog2(N_SLOT);
// A named type, exported as 3 bits so all three languages present one
// identical observable contract.
typedef enum logic [2:0] {
R_NONE = 3'd0, // no token this cycle
R_MATCH = 3'd1, // addressed and configured
R_NO_ADDR = 3'd2, // nobody holds that address
R_NO_EP = 3'd3 // address matched, endpoint did not
} reason_e;
// Per-field arrays rather than an array of structs: a variable
// field-select into an unpacked array of packed structs aborts the
// Icarus elaborator.
logic sl_asg [N_SLOT];
logic [6:0] sl_addr [N_SLOT];
logic [15:0] sl_epm [N_SLOT];
logic m_any, m_ep_any;
logic [SW-1:0] m_slot;
logic [SW:0] m_addr_count;
reason_e reason_q;
always_comb begin
m_any = 1'b0;
m_ep_any = 1'b0;
m_slot = '0;
m_addr_count = '0;
// Downwards, so the LOWEST matching slot wins. Defined, not
// arbitrary: the conflict case must have a predictable winner or it
// cannot be checked.
for (int j = N_SLOT - 1; j >= 0; j--) begin
if (sl_asg[j] && (sl_addr[j] == req_addr)) begin
m_addr_count = m_addr_count + 1'b1;
m_any = 1'b1;
m_slot = SW'(j);
// Address 0 is the default address: endpoint 0 only. A device
// there has no configuration yet, so honouring any other
// endpoint would answer traffic it cannot service.
if (sl_addr[j] == 7'd0) begin
if (req_ep == 4'd0) m_ep_any = 1'b1;
end else begin
if (sl_epm[j][req_ep]) m_ep_any = 1'b1;
end
end
end
end
always_comb begin
if (!req_valid) reason_q = R_NONE;
else if (!m_any) reason_q = R_NO_ADDR;
else if (!m_ep_any) reason_q = R_NO_EP;
else reason_q = R_MATCH;
end
assign conflict = (m_addr_count > 1);
assign sel_valid = req_valid && m_any && m_ep_any;
assign sel_slot = m_slot;
assign sel_reason = reason_q;
logic [31:0] match_c, noaddr_c, noep_c, conf_c;
assign n_match = match_c;
assign n_no_addr = noaddr_c;
assign n_no_ep = noep_c;
assign n_conflict = conf_c;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int j = 0; j < N_SLOT; j++) begin
sl_asg[j] <= 1'b0;
sl_addr[j] <= 7'd0;
sl_epm[j] <= 16'd0;
end
match_c <= 32'd0;
noaddr_c <= 32'd0;
noep_c <= 32'd0;
conf_c <= 32'd0;
end else begin
if (cfg_valid) begin
sl_asg[cfg_slot] <= cfg_assigned;
sl_addr[cfg_slot] <= cfg_addr;
sl_epm[cfg_slot] <= cfg_ep_mask;
end
if (req_valid) begin
case (reason_q)
R_MATCH: match_c <= match_c + 32'd1;
R_NO_ADDR: noaddr_c <= noaddr_c + 32'd1;
R_NO_EP: noep_c <= noep_c + 32'd1;
default: ;
endcase
if (conflict) conf_c <= conf_c + 32'd1;
end
end
end
endmoduleThe reason code becomes a named type, so a reason that does not exist cannot be produced. It is still exported as three bits, so all three languages present one identical observable contract to their benches.
The SystemVerilog testbench
// =====================================================================
// Testbench for spi_cs_select and usb_addr_select -- SystemVerilog.
//
// BOTH DISCIPLINES, ONE BENCH, ONE STIMULUS SOURCE. The chapter's point
// is a comparison, so the comparison happens inside the verification
// rather than in prose afterwards.
//
// THE SHADOW MODELS ARE FORMULATED IN THE OPPOSITE DIRECTION. Both
// designs resolve ties by walking their index space DOWNWARDS so the
// lowest index wins; both models walk UPWARDS and stop at the first
// hit. Same answer, different derivation -- the only way a model can
// fail to inherit the design's mistake.
//
// SAME SEED AND SAME PHASE ORDER AS THE VERILOG BENCH, deliberately.
// Icarus seeds $random identically, so both drive identical stimulus and
// any difference between the two mutation columns is a real difference
// between the two DESIGNS. The independent-stimulus role is VHDL's.
// =====================================================================
`timescale 1ns/1ps
module tb_sl_sv;
localparam int N_DEV = 4;
localparam int N_SLOT = 4;
logic clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
// ---- SPI side ----
logic [N_DEV-1:0] cs_n = '1;
logic spi_sel_valid, spi_contention;
logic [1:0] spi_sel_idx;
logic [31:0] spi_n_sel, spi_n_idle, spi_n_contend;
spi_cs_select #(.N_DEV(N_DEV)) dut_spi (
.clk(clk), .rst_n(rst_n), .cs_n(cs_n),
.sel_valid(spi_sel_valid), .sel_idx(spi_sel_idx),
.contention(spi_contention),
.n_sel(spi_n_sel), .n_idle(spi_n_idle), .n_contend(spi_n_contend)
);
// ---- USB side ----
logic cfg_valid = 1'b0;
logic [1:0] cfg_slot = 2'd0;
logic cfg_assigned = 1'b0;
logic [6:0] cfg_addr = 7'd0;
logic [15:0] cfg_ep_mask = 16'd0;
logic req_valid = 1'b0;
logic [6:0] req_addr = 7'd0;
logic [3:0] req_ep = 4'd0;
logic usb_sel_valid, usb_conflict;
logic [1:0] usb_sel_slot;
logic [2:0] usb_sel_reason;
logic [31:0] usb_n_match, usb_n_no_addr, usb_n_no_ep, usb_n_conflict;
usb_addr_select #(.N_SLOT(N_SLOT)) dut_usb (
.clk(clk), .rst_n(rst_n),
.cfg_valid(cfg_valid), .cfg_slot(cfg_slot),
.cfg_assigned(cfg_assigned), .cfg_addr(cfg_addr),
.cfg_ep_mask(cfg_ep_mask),
.req_valid(req_valid), .req_addr(req_addr), .req_ep(req_ep),
.sel_valid(usb_sel_valid), .sel_slot(usb_sel_slot),
.sel_reason(usb_sel_reason), .conflict(usb_conflict),
.n_match(usb_n_match), .n_no_addr(usb_n_no_addr),
.n_no_ep(usb_n_no_ep), .n_conflict(usb_n_conflict)
);
localparam logic [2:0] R_NONE = 3'd0, R_MATCH = 3'd1,
R_NO_ADDR = 3'd2, R_NO_EP = 3'd3;
int errors = 0, checks = 0, steps = 0;
int seed;
// ---- the headline measurement ----
int spi_contend_patterns = 0; // patterns with >= 2 selects asserted
int spi_contend_visible = 0; // ... distinguishable by SOME slave
int usb_conflict_cases = 0; // tokens hitting a duplicate address
int usb_conflict_visible = 0; // ... detected by the device logic
// $random is SIGNED: mask the sign bit before any modulo, or every
// `% N` is negative about half the time and the sweep silently
// collapses onto a subset of its range.
function automatic logic [31:0] urand();
return $random(seed) & 32'h3FFF_FFFF;
endfunction
task automatic ck(input logic cond, input string what);
checks++;
if (!cond) begin
errors++;
if (errors <= 20)
$display(" ERROR @%0t step#%0d: %s", $time, steps, what);
end
endtask
// ---- the shadow copy of the assignment table ----
//
// Maintained by the BENCH from the writes it issued, never read back
// from the design. A model that reads the design's own table cannot
// detect a design that writes the wrong entry.
logic s_asg [N_SLOT];
logic [6:0] s_addr [N_SLOT];
logic [15:0] s_epm [N_SLOT];
// ---- SPI model: counts upward, unlike the design's downward walk ----
function automatic logic [2:0] spi_popcount(input logic [N_DEV-1:0] p);
logic [2:0] c = 3'd0;
for (int i = 0; i < N_DEV; i++) if (!p[i]) c = c + 3'd1;
return c;
endfunction
function automatic logic [1:0] spi_first_low(input logic [N_DEV-1:0] p);
logic found = 1'b0;
logic [1:0] r = 2'd0;
// upward, stop at the first. The design walks downward and
// overwrites, so the two agree only if both are right.
for (int i = 0; i < N_DEV; i++)
if (!p[i] && !found) begin r = 2'(i); found = 1'b1; end
return r;
endfunction
// ---- USB model: also upward-and-stop, over the bench's own table ----
function automatic logic [2:0] usb_addr_count(input logic [6:0] a);
logic [2:0] c = 3'd0;
for (int j = 0; j < N_SLOT; j++)
if (s_asg[j] && (s_addr[j] == a)) c = c + 3'd1;
return c;
endfunction
function automatic logic [1:0] usb_first_slot(input logic [6:0] a);
logic found = 1'b0;
logic [1:0] r = 2'd0;
for (int j = 0; j < N_SLOT; j++)
if (s_asg[j] && (s_addr[j] == a) && !found) begin r = 2'(j); found = 1'b1; end
return r;
endfunction
// Does ANY assigned slot at address `a` expose endpoint `e`?
//
// The default-address rule lives here, stated independently of the
// design: a slot at address 0 is mid-enumeration and has endpoint 0
// only, whatever its ep_mask happens to contain.
function automatic logic usb_ep_ok(input logic [6:0] a, input logic [3:0] e);
logic ok = 1'b0;
for (int j = 0; j < N_SLOT; j++) begin
if (s_asg[j] && (s_addr[j] == a)) begin
if (s_addr[j] == 7'd0) begin
if (e == 4'd0) ok = 1'b1;
end else begin
if (s_epm[j][e]) ok = 1'b1;
end
end
end
return ok;
endfunction
// ---------------------------------------------------------------
// Drive one SPI pattern and check the resolution.
// ---------------------------------------------------------------
task automatic spi_step(input logic [N_DEV-1:0] p);
logic [2:0] nlow;
logic [1:0] efirst;
logic [31:0] s0, i0, c0;
begin
nlow = spi_popcount(p);
efirst = spi_first_low(p);
s0 = spi_n_sel; i0 = spi_n_idle; c0 = spi_n_contend;
cs_n = p;
#1;
// ---- PROPERTY 1: exactly one low is a selection ----
ck(spi_sel_valid === (nlow == 3'd1),
"spi sel_valid does not mean exactly one chip select is asserted");
// ---- PROPERTY 2: two or more low is contention ----
ck(spi_contention === (nlow > 3'd1),
"spi contention does not mean two or more chip selects are asserted");
// ---- PROPERTY 3: a selection and a contention are exclusive ----
ck(!(spi_sel_valid && spi_contention),
"spi reported a selection and a contention at the same time");
// ---- PROPERTY 4: the winner is the lowest asserted index ----
if (nlow >= 3'd1)
ck(spi_sel_idx === efirst,
"spi did not resolve the tie to the lowest asserted index");
@(posedge clk); #1;
// ---- PROPERTY 5: exactly one counter moved ----
if (nlow == 3'd0) begin
ck(spi_n_idle == i0 + 32'd1 && spi_n_sel == s0 && spi_n_contend == c0,
"spi counters wrong for an idle bus");
end else if (nlow == 3'd1) begin
ck(spi_n_sel == s0 + 32'd1 && spi_n_idle == i0 && spi_n_contend == c0,
"spi counters wrong for a clean selection");
end else begin
ck(spi_n_contend == c0 + 32'd1 && spi_n_sel == s0 && spi_n_idle == i0,
"spi counters wrong for a contention");
end
steps++;
end
endtask
// ---------------------------------------------------------------
// Write one assignment slot, mirroring it into the bench's table.
// The mirror happens AFTER the edge on purpose -- see phase 5.
// ---------------------------------------------------------------
task automatic do_cfg(input logic [1:0] slot, input logic asg,
input logic [6:0] a, input logic [15:0] epm);
cfg_valid = 1'b1; cfg_slot = slot; cfg_assigned = asg;
cfg_addr = a; cfg_ep_mask = epm;
@(posedge clk); #1;
cfg_valid = 1'b0;
s_asg[slot] = asg;
s_addr[slot] = a;
s_epm[slot] = epm;
endtask
// ---------------------------------------------------------------
// Present one token and check the resolution.
// ---------------------------------------------------------------
task automatic usb_step(input logic [6:0] a, input logic [3:0] e);
logic [2:0] nmatch, ereason;
logic [1:0] efirst;
logic eep, eany;
logic [31:0] m0, na0, ne0, cf0;
begin
nmatch = usb_addr_count(a);
efirst = usb_first_slot(a);
eany = (nmatch > 3'd0);
eep = usb_ep_ok(a, e);
ereason = !eany ? R_NO_ADDR : (!eep ? R_NO_EP : R_MATCH);
m0 = usb_n_match; na0 = usb_n_no_addr;
ne0 = usb_n_no_ep; cf0 = usb_n_conflict;
req_valid = 1'b1; req_addr = a; req_ep = e;
#1;
// ---- PROPERTY 6: selection means addressed AND configured ----
ck(usb_sel_valid === (eany && eep),
"usb sel_valid does not mean the address matched and the endpoint was configured");
// ---- PROPERTY 7: the reason code is exact ----
ck(usb_sel_reason === ereason, "usb sel_reason disagrees with the model");
// ---- PROPERTY 8: the winner is the lowest matching slot ----
if (eany)
ck(usb_sel_slot === efirst,
"usb did not resolve a duplicate address to the lowest slot");
// ---- PROPERTY 9: a duplicate address is DETECTED ----
//
// The property the SPI decoder has no equivalent of, and the
// reason is structural rather than a matter of effort: the address
// is a field every slot compares against, so the count is
// available on the spot. A chip select is a wire only one slave
// ever receives.
ck(usb_conflict === (nmatch > 3'd1),
"usb conflict does not mean two assigned slots share the address");
// The measurement, taken on EVERY token rather than on a directed
// handful, so its denominator is every duplicate-address token the
// exhaustive sweep produces.
if (nmatch > 3'd1) begin
usb_conflict_cases++;
if (usb_conflict === 1'b1) usb_conflict_visible++;
end
@(posedge clk); #1;
req_valid = 1'b0;
// ---- PROPERTY 10: exactly one reason counter moved ----
case (ereason)
R_MATCH: ck(usb_n_match == m0 + 32'd1 && usb_n_no_addr == na0 && usb_n_no_ep == ne0,
"usb counters wrong for a match");
R_NO_ADDR: ck(usb_n_no_addr == na0 + 32'd1 && usb_n_match == m0 && usb_n_no_ep == ne0,
"usb counters wrong for an unknown address");
R_NO_EP: ck(usb_n_no_ep == ne0 + 32'd1 && usb_n_match == m0 && usb_n_no_addr == na0,
"usb counters wrong for an unconfigured endpoint");
default: ;
endcase
ck(usb_n_conflict == cf0 + ((nmatch > 3'd1) ? 32'd1 : 32'd0),
"usb conflict counter disagrees with the model");
steps++;
end
endtask
task automatic reset_dut();
rst_n = 1'b0;
cs_n = '1;
cfg_valid = 1'b0; req_valid = 1'b0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
for (int j = 0; j < N_SLOT; j++) begin
s_asg[j] = 1'b0; s_addr[j] = 7'd0; s_epm[j] = 16'd0;
end
endtask
// ---- exhaustive reach ----
//
// SPI: 2**N_DEV chip-select patterns = 16.
// USB: asg_mask(16) x addr_mode(4) x ep_mode(2) x req_addr(5) x req_ep(4)
// = 2560. Every dimension is an independent input with no
// forbidden combinations, so the denominator is exactly 2560.
bit reach_spi [0:15];
bit reach_usb [0:2559];
int nrs, nru, ri;
int pat, am, em, ra, re, asg, j2, k;
logic [6:0] addr_of;
logic [15:0] epm_of;
logic any_slave_knows, view_here, view_clean;
initial begin
for (ri = 0; ri < 16; ri++) reach_spi[ri] = 1'b0;
for (ri = 0; ri < 2560; ri++) reach_usb[ri] = 1'b0;
seed = 32'd28002;
reset_dut();
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every chip-select pattern.
// 16 of 16, nothing unreachable.
// =============================================================
for (pat = 0; pat < 16; pat++) begin
spi_step(pat[N_DEV-1:0]);
reach_spi[pat] = 1'b1;
end
cs_n = '1;
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE) -- the USB selection space.
//
// The table is reconfigured once per (asg_mask, addr_mode, ep_mode)
// and every (req_addr, req_ep) is presented against it: 128
// configurations x 20 tokens.
//
// addr_mode exists so duplicate addresses are REACHED rather than
// assumed away. A sweep with distinct addresses only would leave the
// conflict logic at zero coverage while reporting completeness.
// =============================================================
for (asg = 0; asg < 16; asg++)
for (am = 0; am < 4; am++)
for (em = 0; em < 2; em++) begin
for (j2 = 0; j2 < N_SLOT; j2++) begin
case (am)
0: addr_of = 7'(j2) + 7'd1; // all distinct
1: addr_of = 7'd1; // all identical
2: addr_of = 7'd0; // all at default
default: addr_of = (7'(j2) % 7'd2) + 7'd1; // pairwise duplicates
endcase
// ep_mode 1 leaves endpoints 1 and 3 unconfigured, so R_NO_EP is
// reachable at a NON-zero address and not only through the
// default-address rule.
epm_of = (em == 0) ? 16'h000F : 16'h0005;
do_cfg(2'(j2), asg[j2], addr_of, epm_of);
end
for (ra = 0; ra < 5; ra++)
for (re = 0; re < 4; re++) begin
usb_step(7'(ra), 4'(re));
ri = ((((asg * 4 + am) * 2 + em) * 5 + ra) * 4 + re);
reach_usb[ri] = 1'b1;
end
end
// =============================================================
// PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE MEASUREMENT.
//
// An SPI slave receives ONE chip-select pin, so slave k's entire
// view of the world is cs_n[k]. The test is a DISTINGUISHABILITY
// test, constructed rather than asserted: compare slave k's view
// under the contention pattern against its view under the clean
// pattern in which only k is selected. If the two views are equal,
// no logic inside slave k can tell them apart, whatever it does.
// =============================================================
for (pat = 0; pat < 16; pat++) begin
if (spi_popcount(pat[N_DEV-1:0]) > 3'd1) begin
spi_contend_patterns++;
any_slave_knows = 1'b0;
for (k = 0; k < N_DEV; k++) begin
if (!pat[k]) begin
view_here = pat[k]; // what slave k sees now
view_clean = 1'b0; // what it would see if it alone were selected
if (view_here !== view_clean) any_slave_knows = 1'b1;
end
end
if (any_slave_knows) spi_contend_visible++;
// And confirm the BUS MONITOR does see it -- the detector exists
// only because this module was handed every wire, which no slave
// on a real board is.
cs_n = pat[N_DEV-1:0]; #1;
ck(spi_contention === 1'b1,
"the bus monitor failed to see a contention it was given every wire for");
@(posedge clk); #1;
end
end
cs_n = '1;
// The USB side of this question is measured inside usb_step, on every
// one of the 2560 exhaustive tokens, so there is nothing to add here.
// =============================================================
// PHASE 4 (DIRECTED) -- RE-ADDRESSING AT RUN TIME.
//
// The capability SPI does not have and cannot be given without
// changing the board.
//
// The absence of a comparable phase on the SPI side is not an
// omission. spi_cs_select HAS NO CONFIGURATION PORT -- its mapping
// is fixed at elaboration -- and that missing port is the finding.
// =============================================================
reset_dut();
do_cfg(2'd0, 1'b1, 7'd5, 16'h000F);
usb_step(7'd5, 4'd0); // answers at 5
do_cfg(2'd0, 1'b1, 7'd9, 16'h000F); // re-addressed to 9
usb_step(7'd5, 4'd0); // must now be silent at 5
usb_step(7'd9, 4'd0); // and answer at 9
do_cfg(2'd0, 1'b0, 7'd9, 16'h000F); // unassigned entirely
usb_step(7'd9, 4'd0); // silent again
// An unassigned slot must not answer even at the default address, or
// it would respond during another device's enumeration.
usb_step(7'd0, 4'd0);
// =============================================================
// PHASE 5 (DIRECTED) -- WHEN A CONFIGURATION TAKES EFFECT.
//
// The design registers the table, so a token presented in the SAME
// cycle as a write must see the OLD table. do_cfg mirrors the write
// only AFTER the edge, so a design that applied it early would
// disagree here and nowhere else in the run.
// =============================================================
reset_dut();
do_cfg(2'd1, 1'b1, 7'd7, 16'h000F);
cfg_valid = 1'b1; cfg_slot = 2'd1; cfg_assigned = 1'b1;
cfg_addr = 7'd8; cfg_ep_mask = 16'h000F;
req_valid = 1'b1; req_addr = 7'd7; req_ep = 4'd0;
#1;
ck(usb_sel_valid === 1'b1,
"a configuration write took effect in the same cycle as the token");
ck(usb_sel_reason === R_MATCH,
"same-cycle reason code should still be MATCH at the old address");
@(posedge clk); #1;
cfg_valid = 1'b0; req_valid = 1'b0;
s_asg[1] = 1'b1; s_addr[1] = 7'd8; s_epm[1] = 16'h000F;
usb_step(7'd8, 4'd0);
usb_step(7'd7, 4'd0);
// =============================================================
// PHASE 6 (RANDOM)
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut();
for (k = 0; k < 400; k++) begin
if ((urand() % 3) == 0)
do_cfg(2'(urand() % 4), (urand() % 4) != 0,
7'(urand() % 6), (urand() % 2) ? 16'h000F : 16'h0005);
usb_step(7'(urand() % 6), 4'(urand() % 4));
spi_step(4'(urand() % 16));
end
`endif
nrs = 0; for (ri = 0; ri < 16; ri++) if (reach_spi[ri]) nrs++;
nru = 0; for (ri = 0; ri < 2560; ri++) if (reach_usb[ri]) nru++;
$display("steps=%0d checks=%0d reach_spi=%0d/16 reach_usb=%0d/2560 errors=%0d",
steps, checks, nrs, nru, errors);
$display("[spi] selections=%0d idle=%0d CONTENTIONS=%0d",
spi_n_sel, spi_n_idle, spi_n_contend);
$display("[usb] matches=%0d no_addr=%0d no_ep=%0d CONFLICTS=%0d",
usb_n_match, usb_n_no_addr, usb_n_no_ep, usb_n_conflict);
$display("--- detectability from the vantage point a real device occupies ---");
$display("[spi] contention patterns=%0d distinguishable by any slave=%0d",
spi_contend_patterns, spi_contend_visible);
$display("[usb] duplicate-address tokens=%0d detected by the device=%0d",
usb_conflict_cases, usb_conflict_visible);
if (nrs != 16 || nru != 2560) begin
$display("FAIL: exhaustive sweep incomplete"); errors++;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmoduleSame seed and same phase order as the Verilog bench, deliberately — so any difference between the two mutation columns is a real difference between the two designs. The independent-stimulus role belongs to VHDL.
11. VHDL-2008
-- =====================================================================
-- TWO SELECTION DISCIPLINES, SIDE BY SIDE -- VHDL-2008.
--
-- CLASSIFICATION: simplified synthesisable teaching RTL.
-- Same hardware contract as the Verilog and SystemVerilog files: same
-- ports, same widths, same reset values, same cycle-by-cycle behaviour.
--
-- The question both entities answer:
--
-- "Which peripheral should respond to this transaction?"
--
-- SPI answers it with a WIRE routed at layout time. USB answers it with
-- a FIELD every device sees, holding an address the HOST assigned.
--
-- The difference that matters is not the wire count:
--
-- SPI's selection information is DISTRIBUTED -- each slave sees
-- only its own select line. USB's is BROADCAST -- every device
-- sees the whole address field.
--
-- A distributed selector cannot detect its own worst failure.
--
-- Both combinational processes use `process (all)`. That is a
-- deliberate choice for a file that gets mutated: a mutation which adds
-- a branch reading a new signal would otherwise need the sensitivity
-- list extended by hand, and forgetting to do so produces a mutant that
-- fails for the wrong reason.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package sel_pkg is
-- Ceiling log2, for index widths. Named so it cannot collide with a
-- port: a VHDL port shadows a same-named package object, and VHDL is
-- case-insensitive, so the collision would be silent.
function sel_clog2 (n : natural) return natural;
end package;
package body sel_pkg is
function sel_clog2 (n : natural) return natural is
variable r : natural := 0;
variable v : natural := 1;
begin
while v < n loop
v := v * 2;
r := r + 1;
end loop;
if r = 0 then
return 1;
else
return r;
end if;
end function;
end package body;
-- ---------------------------------------------------------------------
-- spi_cs_select -- selection by wire.
--
-- IMPORTANT: this entity is given ALL N chip-select lines. No real SPI
-- slave has that. A real slave receives exactly one CS pin and cannot
-- know whether another slave is also selected. So `contention` is a BUS
-- MONITOR output, observable only from a vantage point that no SPI
-- device on a real board occupies.
--
-- That is not a limitation of the model. It is the finding.
-- ---------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.sel_pkg.all;
entity spi_cs_select is
generic (
N_DEV : natural := 4
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- Active-low chip selects, one per device, routed on the board.
cs_n : in std_logic_vector(N_DEV - 1 downto 0);
sel_valid : out std_logic;
sel_idx : out std_logic_vector(sel_clog2(N_DEV) - 1 downto 0);
-- The failure a real slave cannot see.
contention : out std_logic;
n_sel : out std_logic_vector(31 downto 0);
n_idle : out std_logic_vector(31 downto 0);
n_contend : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of spi_cs_select is
constant IW : natural := sel_clog2(N_DEV);
-- How many selects are asserted. On a correct board 0 or 1; two at
-- once is a firmware or routing fault, and on real hardware it means
-- two slaves drive MISO simultaneously.
signal n_low : unsigned(IW downto 0) := (others => '0');
signal first_low : unsigned(IW - 1 downto 0) := (others => '0');
signal sel_c : unsigned(31 downto 0) := (others => '0');
signal idle_c : unsigned(31 downto 0) := (others => '0');
signal con_c : unsigned(31 downto 0) := (others => '0');
begin
count : process (all)
variable c : unsigned(IW downto 0);
variable f : unsigned(IW - 1 downto 0);
begin
c := (others => '0');
f := (others => '0');
-- Downwards, so the LOWEST asserted index wins the tie. The
-- direction is arbitrary but it must be DEFINED: an undefined winner
-- makes the contention case unverifiable.
for i in N_DEV - 1 downto 0 loop
if cs_n(i) = '0' then
c := c + 1;
f := to_unsigned(i, IW);
end if;
end loop;
n_low <= c;
first_low <= f;
end process;
contention <= '1' when n_low > 1 else '0';
sel_valid <= '1' when n_low = 1 else '0';
sel_idx <= std_logic_vector(first_low);
n_sel <= std_logic_vector(sel_c);
n_idle <= std_logic_vector(idle_c);
n_contend <= std_logic_vector(con_c);
process (clk, rst_n)
begin
if rst_n = '0' then
sel_c <= (others => '0');
idle_c <= (others => '0');
con_c <= (others => '0');
elsif rising_edge(clk) then
if n_low = 0 then
idle_c <= idle_c + 1;
elsif n_low = 1 then
sel_c <= sel_c + 1;
else
con_c <= con_c + 1;
end if;
end if;
end process;
end architecture;
-- ---------------------------------------------------------------------
-- usb_addr_select -- selection by assigned name.
--
-- Two real USB rules are enforced, because both are places an
-- implementation can be plausibly wrong:
--
-- * Address 0 is the DEFAULT address. A device there is
-- mid-enumeration and has endpoint 0 only. Honouring any other
-- endpoint would let a half-enumerated device answer traffic it has
-- no configuration for.
--
-- * An UNASSIGNED slot matches nothing, including address 0. A slot
-- that answered before being assigned would respond during another
-- device's enumeration.
--
-- Unlike the SPI decoder, this one CAN see its own worst failure: two
-- slots holding the same address is directly detectable, because the
-- address is a field every slot compares against.
-- ---------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.sel_pkg.all;
entity usb_addr_select is
generic (
N_SLOT : natural := 4
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- ---- the assignment interface: this is the whole difference ----
--
-- spi_cs_select has no equivalent port. Its mapping is fixed at
-- elaboration. This one's mapping is a register file, writable at run
-- time, which is what "the host assigns an address" means in
-- hardware.
cfg_valid : in std_logic;
cfg_slot : in std_logic_vector(sel_clog2(N_SLOT) - 1 downto 0);
cfg_assigned : in std_logic;
cfg_addr : in std_logic_vector(6 downto 0);
cfg_ep_mask : in std_logic_vector(15 downto 0);
req_valid : in std_logic;
req_addr : in std_logic_vector(6 downto 0);
req_ep : in std_logic_vector(3 downto 0);
sel_valid : out std_logic;
sel_slot : out std_logic_vector(sel_clog2(N_SLOT) - 1 downto 0);
sel_reason : out std_logic_vector(2 downto 0);
-- The failure this discipline CAN see.
conflict : out std_logic;
n_match : out std_logic_vector(31 downto 0);
n_no_addr : out std_logic_vector(31 downto 0);
n_no_ep : out std_logic_vector(31 downto 0);
n_conflict : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_addr_select is
constant SW : natural := sel_clog2(N_SLOT);
constant R_NONE : std_logic_vector(2 downto 0) := "000"; -- no token
constant R_MATCH : std_logic_vector(2 downto 0) := "001"; -- addressed + configured
constant R_NO_ADDR : std_logic_vector(2 downto 0) := "010"; -- nobody holds it
constant R_NO_EP : std_logic_vector(2 downto 0) := "011"; -- endpoint not configured
type asg_arr_t is array (0 to N_SLOT - 1) of std_logic;
type addr_arr_t is array (0 to N_SLOT - 1) of std_logic_vector(6 downto 0);
type epm_arr_t is array (0 to N_SLOT - 1) of std_logic_vector(15 downto 0);
signal sl_asg : asg_arr_t := (others => '0');
signal sl_addr : addr_arr_t := (others => (others => '0'));
signal sl_epm : epm_arr_t := (others => (others => '0'));
-- Initialised so the concurrent comparisons below are never
-- evaluated against 'U' in the first delta cycle, which numeric_std
-- reports as a metavalue warning on every run.
signal m_any : std_logic := '0';
signal m_ep_any : std_logic := '0';
signal m_slot : unsigned(SW - 1 downto 0) := (others => '0');
signal m_addr_count : unsigned(SW downto 0) := (others => '0');
signal reason_q : std_logic_vector(2 downto 0) := "000";
signal match_c : unsigned(31 downto 0) := (others => '0');
signal noaddr_c : unsigned(31 downto 0) := (others => '0');
signal noep_c : unsigned(31 downto 0) := (others => '0');
signal conf_c : unsigned(31 downto 0) := (others => '0');
begin
match : process (all)
variable any_v : std_logic;
variable ep_v : std_logic;
variable slot_v : unsigned(SW - 1 downto 0);
variable count_v : unsigned(SW downto 0);
begin
any_v := '0';
ep_v := '0';
slot_v := (others => '0');
count_v := (others => '0');
-- Downwards, so the LOWEST matching slot wins. Defined, not
-- arbitrary: the conflict case must have a predictable winner or it
-- cannot be checked.
for j in N_SLOT - 1 downto 0 loop
if sl_asg(j) = '1' and sl_addr(j) = req_addr then
count_v := count_v + 1;
any_v := '1';
slot_v := to_unsigned(j, SW);
-- Address 0 is the default address: endpoint 0 only. A device
-- there has no configuration yet, so honouring any other
-- endpoint would answer traffic it cannot service.
if sl_addr(j) = "0000000" then
if req_ep = "0000" then
ep_v := '1';
end if;
else
if sl_epm(j)(to_integer(unsigned(req_ep))) = '1' then
ep_v := '1';
end if;
end if;
end if;
end loop;
m_any <= any_v;
m_ep_any <= ep_v;
m_slot <= slot_v;
m_addr_count <= count_v;
end process;
reason : process (all)
begin
if req_valid = '0' then
reason_q <= R_NONE;
elsif m_any = '0' then
reason_q <= R_NO_ADDR;
elsif m_ep_any = '0' then
reason_q <= R_NO_EP;
else
reason_q <= R_MATCH;
end if;
end process;
conflict <= '1' when m_addr_count > 1 else '0';
sel_valid <= '1' when (req_valid = '1' and m_any = '1' and m_ep_any = '1') else '0';
sel_slot <= std_logic_vector(m_slot);
sel_reason <= reason_q;
n_match <= std_logic_vector(match_c);
n_no_addr <= std_logic_vector(noaddr_c);
n_no_ep <= std_logic_vector(noep_c);
n_conflict <= std_logic_vector(conf_c);
process (clk, rst_n)
begin
if rst_n = '0' then
sl_asg <= (others => '0');
sl_addr <= (others => (others => '0'));
sl_epm <= (others => (others => '0'));
match_c <= (others => '0');
noaddr_c <= (others => '0');
noep_c <= (others => '0');
conf_c <= (others => '0');
elsif rising_edge(clk) then
if cfg_valid = '1' then
sl_asg(to_integer(unsigned(cfg_slot))) <= cfg_assigned;
sl_addr(to_integer(unsigned(cfg_slot))) <= cfg_addr;
sl_epm(to_integer(unsigned(cfg_slot))) <= cfg_ep_mask;
end if;
if req_valid = '1' then
case reason_q is
when R_MATCH => match_c <= match_c + 1;
when R_NO_ADDR => noaddr_c <= noaddr_c + 1;
when R_NO_EP => noep_c <= noep_c + 1;
when others => null;
end case;
if m_addr_count > 1 then
conf_c <= conf_c + 1;
end if;
end if;
end if;
end process;
end architecture;The VHDL testbench
-- =====================================================================
-- Testbench for spi_cs_select and usb_addr_select -- VHDL-2008.
--
-- BOTH DISCIPLINES, ONE BENCH, ONE STIMULUS SOURCE. The chapter's point
-- is a comparison, so the comparison happens inside the verification
-- rather than in prose afterwards.
--
-- THE SHADOW MODELS ARE FORMULATED IN THE OPPOSITE DIRECTION. Both
-- designs resolve ties by walking their index space DOWNWARDS so the
-- lowest index wins; both models walk UPWARDS and stop at the first hit.
-- Same answer, different derivation -- the only way a model can fail to
-- inherit the design's mistake.
--
-- THIS IS THE INDEPENDENT BENCH. The directed phases are structurally
-- identical to the Verilog and SystemVerilog benches, so the DIRECTED
-- mutation columns must agree EXACTLY across all three languages and any
-- disagreement is a real finding. The random phase uses a VHDL-native
-- generator and therefore a different stream, so the ALL columns are
-- expected to differ.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
entity tb_sl_vhdl is
generic (
-- Set true (nvc -e -gDIRECTED_ONLY=true) to run the directed phases
-- alone. They must pass and must kill every mutation by themselves; a
-- suite that needs its random phase to find a defect has not
-- characterised the defect.
DIRECTED_ONLY : boolean := false
);
end entity;
architecture sim of tb_sl_vhdl is
constant N_DEV : natural := 4;
constant N_SLOT : natural := 4;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
-- SPI side
signal cs_n : std_logic_vector(N_DEV - 1 downto 0) := (others => '1');
signal spi_sel_valid : std_logic;
signal spi_sel_idx : std_logic_vector(1 downto 0);
signal spi_contention : std_logic;
signal spi_n_sel : std_logic_vector(31 downto 0);
signal spi_n_idle : std_logic_vector(31 downto 0);
signal spi_n_contend : std_logic_vector(31 downto 0);
-- USB side
signal cfg_valid : std_logic := '0';
signal cfg_slot : std_logic_vector(1 downto 0) := "00";
signal cfg_assigned : std_logic := '0';
signal cfg_addr : std_logic_vector(6 downto 0) := (others => '0');
signal cfg_ep_mask : std_logic_vector(15 downto 0) := (others => '0');
signal req_valid : std_logic := '0';
signal req_addr : std_logic_vector(6 downto 0) := (others => '0');
signal req_ep : std_logic_vector(3 downto 0) := (others => '0');
signal usb_sel_valid : std_logic;
signal usb_sel_slot : std_logic_vector(1 downto 0);
signal usb_sel_reason : std_logic_vector(2 downto 0);
signal usb_conflict : std_logic;
signal usb_n_match : std_logic_vector(31 downto 0);
signal usb_n_no_addr : std_logic_vector(31 downto 0);
signal usb_n_no_ep : std_logic_vector(31 downto 0);
signal usb_n_conflict : std_logic_vector(31 downto 0);
constant R_NONE : std_logic_vector(2 downto 0) := "000";
constant R_MATCH : std_logic_vector(2 downto 0) := "001";
constant R_NO_ADDR : std_logic_vector(2 downto 0) := "010";
constant R_NO_EP : std_logic_vector(2 downto 0) := "011";
begin
dut_spi : entity work.spi_cs_select
generic map (N_DEV => N_DEV)
port map (
clk => clk, rst_n => rst_n, cs_n => cs_n,
sel_valid => spi_sel_valid, sel_idx => spi_sel_idx,
contention => spi_contention,
n_sel => spi_n_sel, n_idle => spi_n_idle, n_contend => spi_n_contend
);
dut_usb : entity work.usb_addr_select
generic map (N_SLOT => N_SLOT)
port map (
clk => clk, rst_n => rst_n,
cfg_valid => cfg_valid, cfg_slot => cfg_slot,
cfg_assigned => cfg_assigned, cfg_addr => cfg_addr,
cfg_ep_mask => cfg_ep_mask,
req_valid => req_valid, req_addr => req_addr, req_ep => req_ep,
sel_valid => usb_sel_valid, sel_slot => usb_sel_slot,
sel_reason => usb_sel_reason, conflict => usb_conflict,
n_match => usb_n_match, n_no_addr => usb_n_no_addr,
n_no_ep => usb_n_no_ep, n_conflict => usb_n_conflict
);
clkgen : process
begin
while not done loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
main : process
variable errors : integer := 0;
variable checks : integer := 0;
variable steps : integer := 0;
variable lo : line;
-- ---- the headline measurement ----
variable spi_contend_patterns : integer := 0;
variable spi_contend_visible : integer := 0;
variable usb_conflict_cases : integer := 0;
variable usb_conflict_visible : integer := 0;
-- ---- the shadow copy of the assignment table ----
--
-- Maintained by the BENCH from the writes it issued, never read back
-- from the design. A model that reads the design's own table cannot
-- detect a design that writes the wrong entry.
type asg_arr_t is array (0 to N_SLOT - 1) of std_logic;
type addr_arr_t is array (0 to N_SLOT - 1) of std_logic_vector(6 downto 0);
type epm_arr_t is array (0 to N_SLOT - 1) of std_logic_vector(15 downto 0);
variable s_asg : asg_arr_t := (others => '0');
variable s_addr : addr_arr_t := (others => (others => '0'));
variable s_epm : epm_arr_t := (others => (others => '0'));
-- exhaustive reach
type rspi_t is array (0 to 15) of boolean;
type rusb_t is array (0 to 2559) of boolean;
variable reach_spi : rspi_t := (others => false);
variable reach_usb : rusb_t := (others => false);
variable nrs, nru : integer := 0;
-- A VHDL-native LCG. Deliberately NOT the same stream as the Verilog
-- bench, so the random phases are genuinely independent.
variable rnd_state : unsigned(31 downto 0) := x"000606D2";
impure function urand return integer is
begin
-- resize() back to 32 bits: `unsigned * unsigned` widens to 64 in
-- VHDL, and truncating to 32 is exactly the LCG's mod 2**32.
rnd_state := resize(rnd_state * to_unsigned(1103515245, 32), 32)
+ to_unsigned(12345, 32);
-- Return the HIGH bits, not the low ones. In an LCG with a
-- power-of-two modulus, bit i has period 2**(i+1): bit 0 alternates,
-- bit 1 cycles in four. So `urand mod 4` taken from the low bits
-- returns 3,0,1,2,3,0,1,2,... in perfect lockstep -- while its
-- histogram is exactly uniform, which is why the defect survives
-- every distribution check anyone would think to run.
return to_integer(rnd_state(30 downto 15));
end function;
procedure ck (cond : boolean; what : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 20 then
write(lo, string'(" ERROR @") & time'image(now) &
string'(" step#") & integer'image(steps) &
string'(": ") & what);
writeline(output, lo);
end if;
end if;
end procedure;
-- ---- SPI model: counts upward, unlike the design's downward walk ----
function spi_popcount (p : std_logic_vector) return integer is
variable c : integer := 0;
begin
for i in 0 to N_DEV - 1 loop
if p(i) = '0' then c := c + 1; end if;
end loop;
return c;
end function;
function spi_first_low (p : std_logic_vector) return integer is
variable found : boolean := false;
variable r : integer := 0;
begin
-- upward, stop at the first. The design walks downward and
-- overwrites, so the two agree only if both are right.
for i in 0 to N_DEV - 1 loop
if p(i) = '0' and not found then
r := i; found := true;
end if;
end loop;
return r;
end function;
-- ---- USB model: also upward-and-stop, over the bench's own table ----
impure function usb_addr_count (a : std_logic_vector(6 downto 0)) return integer is
variable c : integer := 0;
begin
for j in 0 to N_SLOT - 1 loop
if s_asg(j) = '1' and s_addr(j) = a then c := c + 1; end if;
end loop;
return c;
end function;
impure function usb_first_slot (a : std_logic_vector(6 downto 0)) return integer is
variable found : boolean := false;
variable r : integer := 0;
begin
for j in 0 to N_SLOT - 1 loop
if s_asg(j) = '1' and s_addr(j) = a and not found then
r := j; found := true;
end if;
end loop;
return r;
end function;
-- Does ANY assigned slot at address `a` expose endpoint `e`?
--
-- The default-address rule lives here, stated independently of the
-- design: a slot at address 0 is mid-enumeration and has endpoint 0
-- only, whatever its ep_mask happens to contain.
impure function usb_ep_ok (a : std_logic_vector(6 downto 0);
e : std_logic_vector(3 downto 0)) return boolean is
variable ok : boolean := false;
begin
for j in 0 to N_SLOT - 1 loop
if s_asg(j) = '1' and s_addr(j) = a then
if s_addr(j) = "0000000" then
if e = "0000" then ok := true; end if;
else
if s_epm(j)(to_integer(unsigned(e))) = '1' then ok := true; end if;
end if;
end if;
end loop;
return ok;
end function;
-- ---------------------------------------------------------------
-- Drive one SPI pattern and check the resolution.
-- ---------------------------------------------------------------
procedure spi_step (p : std_logic_vector(N_DEV - 1 downto 0)) is
variable nlow, efirst : integer;
variable s0, i0, c0 : integer;
begin
nlow := spi_popcount(p);
efirst := spi_first_low(p);
s0 := to_integer(unsigned(spi_n_sel));
i0 := to_integer(unsigned(spi_n_idle));
c0 := to_integer(unsigned(spi_n_contend));
cs_n <= p;
wait for 1 ns;
-- ---- PROPERTY 1: exactly one low is a selection ----
ck((spi_sel_valid = '1') = (nlow = 1),
"spi sel_valid does not mean exactly one chip select is asserted");
-- ---- PROPERTY 2: two or more low is contention ----
ck((spi_contention = '1') = (nlow > 1),
"spi contention does not mean two or more chip selects are asserted");
-- ---- PROPERTY 3: a selection and a contention are exclusive ----
ck(not (spi_sel_valid = '1' and spi_contention = '1'),
"spi reported a selection and a contention at the same time");
-- ---- PROPERTY 4: the winner is the lowest asserted index ----
if nlow >= 1 then
ck(to_integer(unsigned(spi_sel_idx)) = efirst,
"spi did not resolve the tie to the lowest asserted index");
end if;
wait until rising_edge(clk);
wait for 1 ns;
-- ---- PROPERTY 5: exactly one counter moved ----
if nlow = 0 then
ck(to_integer(unsigned(spi_n_idle)) = i0 + 1 and
to_integer(unsigned(spi_n_sel)) = s0 and
to_integer(unsigned(spi_n_contend)) = c0,
"spi counters wrong for an idle bus");
elsif nlow = 1 then
ck(to_integer(unsigned(spi_n_sel)) = s0 + 1 and
to_integer(unsigned(spi_n_idle)) = i0 and
to_integer(unsigned(spi_n_contend)) = c0,
"spi counters wrong for a clean selection");
else
ck(to_integer(unsigned(spi_n_contend)) = c0 + 1 and
to_integer(unsigned(spi_n_sel)) = s0 and
to_integer(unsigned(spi_n_idle)) = i0,
"spi counters wrong for a contention");
end if;
steps := steps + 1;
end procedure;
-- ---------------------------------------------------------------
-- Write one assignment slot, mirroring it into the bench's table.
-- The mirror happens AFTER the edge on purpose -- see phase 5.
-- ---------------------------------------------------------------
procedure do_cfg (slot : integer; asg : std_logic;
a : std_logic_vector(6 downto 0);
epm : std_logic_vector(15 downto 0)) is
begin
cfg_valid <= '1';
cfg_slot <= std_logic_vector(to_unsigned(slot, 2));
cfg_assigned <= asg;
cfg_addr <= a;
cfg_ep_mask <= epm;
wait until rising_edge(clk);
wait for 1 ns;
cfg_valid <= '0';
s_asg(slot) := asg;
s_addr(slot) := a;
s_epm(slot) := epm;
end procedure;
-- ---------------------------------------------------------------
-- Present one token and check the resolution.
-- ---------------------------------------------------------------
procedure usb_step (a : std_logic_vector(6 downto 0);
e : std_logic_vector(3 downto 0)) is
variable nmatch, efirst : integer;
variable eep, eany : boolean;
variable ereason : std_logic_vector(2 downto 0);
variable m0, na0, ne0, cf0 : integer;
begin
nmatch := usb_addr_count(a);
efirst := usb_first_slot(a);
eany := nmatch > 0;
eep := usb_ep_ok(a, e);
if not eany then
ereason := R_NO_ADDR;
elsif not eep then
ereason := R_NO_EP;
else
ereason := R_MATCH;
end if;
m0 := to_integer(unsigned(usb_n_match));
na0 := to_integer(unsigned(usb_n_no_addr));
ne0 := to_integer(unsigned(usb_n_no_ep));
cf0 := to_integer(unsigned(usb_n_conflict));
req_valid <= '1';
req_addr <= a;
req_ep <= e;
wait for 1 ns;
-- ---- PROPERTY 6: selection means addressed AND configured ----
ck((usb_sel_valid = '1') = (eany and eep),
"usb sel_valid does not mean the address matched and the endpoint was configured");
-- ---- PROPERTY 7: the reason code is exact ----
ck(usb_sel_reason = ereason, "usb sel_reason disagrees with the model");
-- ---- PROPERTY 8: the winner is the lowest matching slot ----
if eany then
ck(to_integer(unsigned(usb_sel_slot)) = efirst,
"usb did not resolve a duplicate address to the lowest slot");
end if;
-- ---- PROPERTY 9: a duplicate address is DETECTED ----
--
-- The property the SPI decoder has no equivalent of, and the reason
-- is structural rather than a matter of effort: the address is a
-- field every slot compares against, so the count is available on
-- the spot. A chip select is a wire only one slave ever receives.
ck((usb_conflict = '1') = (nmatch > 1),
"usb conflict does not mean two assigned slots share the address");
-- The measurement, taken on EVERY token rather than on a directed
-- handful, so its denominator is every duplicate-address token the
-- exhaustive sweep produces.
if nmatch > 1 then
usb_conflict_cases := usb_conflict_cases + 1;
if usb_conflict = '1' then
usb_conflict_visible := usb_conflict_visible + 1;
end if;
end if;
wait until rising_edge(clk);
wait for 1 ns;
req_valid <= '0';
-- ---- PROPERTY 10: exactly one reason counter moved ----
if ereason = R_MATCH then
ck(to_integer(unsigned(usb_n_match)) = m0 + 1 and
to_integer(unsigned(usb_n_no_addr)) = na0 and
to_integer(unsigned(usb_n_no_ep)) = ne0,
"usb counters wrong for a match");
elsif ereason = R_NO_ADDR then
ck(to_integer(unsigned(usb_n_no_addr)) = na0 + 1 and
to_integer(unsigned(usb_n_match)) = m0 and
to_integer(unsigned(usb_n_no_ep)) = ne0,
"usb counters wrong for an unknown address");
elsif ereason = R_NO_EP then
ck(to_integer(unsigned(usb_n_no_ep)) = ne0 + 1 and
to_integer(unsigned(usb_n_match)) = m0 and
to_integer(unsigned(usb_n_no_addr)) = na0,
"usb counters wrong for an unconfigured endpoint");
end if;
if nmatch > 1 then
ck(to_integer(unsigned(usb_n_conflict)) = cf0 + 1,
"usb conflict counter disagrees with the model");
else
ck(to_integer(unsigned(usb_n_conflict)) = cf0,
"usb conflict counter disagrees with the model");
end if;
steps := steps + 1;
end procedure;
procedure reset_dut is
begin
rst_n <= '0';
cs_n <= (others => '1');
cfg_valid <= '0';
req_valid <= '0';
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
s_asg := (others => '0');
s_addr := (others => (others => '0'));
s_epm := (others => (others => '0'));
end procedure;
variable addr_of : std_logic_vector(6 downto 0);
variable epm_of : std_logic_vector(15 downto 0);
variable ri : integer;
variable pv : std_logic_vector(N_DEV - 1 downto 0);
variable asgv : std_logic_vector(3 downto 0);
variable any_slave_knows : boolean;
variable view_here, view_clean : std_logic;
begin
reset_dut;
-- ===============================================================
-- PHASE 1 (DIRECTED, EXHAUSTIVE) -- every chip-select pattern.
-- 16 of 16, nothing unreachable.
-- ===============================================================
for pat in 0 to 15 loop
pv := std_logic_vector(to_unsigned(pat, N_DEV));
spi_step(pv);
reach_spi(pat) := true;
end loop;
cs_n <= (others => '1');
-- ===============================================================
-- PHASE 2 (DIRECTED, EXHAUSTIVE) -- the USB selection space.
--
-- The table is reconfigured once per (asg_mask, addr_mode, ep_mode)
-- and every (req_addr, req_ep) is presented against it: 128
-- configurations x 20 tokens.
--
-- addr_mode exists so duplicate addresses are REACHED rather than
-- assumed away. A sweep with distinct addresses only would leave the
-- conflict logic at zero coverage while reporting completeness.
-- ===============================================================
for asg in 0 to 15 loop
asgv := std_logic_vector(to_unsigned(asg, 4));
for am in 0 to 3 loop
for em in 0 to 1 loop
for j2 in 0 to N_SLOT - 1 loop
case am is
when 0 => addr_of := std_logic_vector(to_unsigned(j2 + 1, 7));
when 1 => addr_of := std_logic_vector(to_unsigned(1, 7));
when 2 => addr_of := (others => '0');
when others => addr_of := std_logic_vector(to_unsigned((j2 mod 2) + 1, 7));
end case;
-- ep_mode 1 leaves endpoints 1 and 3 unconfigured, so R_NO_EP
-- is reachable at a NON-zero address and not only through the
-- default-address rule.
if em = 0 then epm_of := x"000F"; else epm_of := x"0005"; end if;
do_cfg(j2, asgv(j2), addr_of, epm_of);
end loop;
for ra in 0 to 4 loop
for re in 0 to 3 loop
usb_step(std_logic_vector(to_unsigned(ra, 7)),
std_logic_vector(to_unsigned(re, 4)));
ri := ((((asg * 4 + am) * 2 + em) * 5 + ra) * 4 + re);
reach_usb(ri) := true;
end loop;
end loop;
end loop;
end loop;
end loop;
-- ===============================================================
-- PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE MEASUREMENT.
--
-- An SPI slave receives ONE chip-select pin, so slave k's entire
-- view of the world is cs_n(k). The test is a DISTINGUISHABILITY
-- test, constructed rather than asserted: compare slave k's view
-- under the contention pattern against its view under the clean
-- pattern in which only k is selected. If the two views are equal,
-- no logic inside slave k can tell them apart, whatever it does.
-- ===============================================================
for pat in 0 to 15 loop
pv := std_logic_vector(to_unsigned(pat, N_DEV));
if spi_popcount(pv) > 1 then
spi_contend_patterns := spi_contend_patterns + 1;
any_slave_knows := false;
for k in 0 to N_DEV - 1 loop
if pv(k) = '0' then
view_here := pv(k); -- what slave k sees now
view_clean := '0'; -- what it would see if it alone were selected
if view_here /= view_clean then any_slave_knows := true; end if;
end if;
end loop;
if any_slave_knows then
spi_contend_visible := spi_contend_visible + 1;
end if;
-- And confirm the BUS MONITOR does see it -- the detector exists
-- only because this entity was handed every wire, which no slave
-- on a real board is.
cs_n <= pv;
wait for 1 ns;
ck(spi_contention = '1',
"the bus monitor failed to see a contention it was given every wire for");
wait until rising_edge(clk);
wait for 1 ns;
end if;
end loop;
cs_n <= (others => '1');
-- The USB side of this question is measured inside usb_step, on every
-- one of the 2560 exhaustive tokens, so there is nothing to add here.
-- ===============================================================
-- PHASE 4 (DIRECTED) -- RE-ADDRESSING AT RUN TIME.
--
-- The capability SPI does not have and cannot be given without
-- changing the board.
--
-- The absence of a comparable phase on the SPI side is not an
-- omission. spi_cs_select HAS NO CONFIGURATION PORT -- its mapping
-- is fixed at elaboration -- and that missing port is the finding.
-- ===============================================================
reset_dut;
do_cfg(0, '1', std_logic_vector(to_unsigned(5, 7)), x"000F");
usb_step(std_logic_vector(to_unsigned(5, 7)), "0000"); -- answers at 5
do_cfg(0, '1', std_logic_vector(to_unsigned(9, 7)), x"000F");
usb_step(std_logic_vector(to_unsigned(5, 7)), "0000"); -- silent at 5
usb_step(std_logic_vector(to_unsigned(9, 7)), "0000"); -- answers at 9
do_cfg(0, '0', std_logic_vector(to_unsigned(9, 7)), x"000F");
usb_step(std_logic_vector(to_unsigned(9, 7)), "0000"); -- silent again
-- An unassigned slot must not answer even at the default address, or
-- it would respond during another device's enumeration.
usb_step((others => '0'), "0000");
-- ===============================================================
-- PHASE 5 (DIRECTED) -- WHEN A CONFIGURATION TAKES EFFECT.
--
-- The design registers the table, so a token presented in the SAME
-- cycle as a write must see the OLD table. do_cfg mirrors the write
-- only AFTER the edge, so a design that applied it early would
-- disagree here and nowhere else in the run.
-- ===============================================================
reset_dut;
do_cfg(1, '1', std_logic_vector(to_unsigned(7, 7)), x"000F");
cfg_valid <= '1';
cfg_slot <= "01";
cfg_assigned <= '1';
cfg_addr <= std_logic_vector(to_unsigned(8, 7));
cfg_ep_mask <= x"000F";
req_valid <= '1';
req_addr <= std_logic_vector(to_unsigned(7, 7));
req_ep <= "0000";
wait for 1 ns;
ck(usb_sel_valid = '1',
"a configuration write took effect in the same cycle as the token");
ck(usb_sel_reason = R_MATCH,
"same-cycle reason code should still be MATCH at the old address");
wait until rising_edge(clk);
wait for 1 ns;
cfg_valid <= '0';
req_valid <= '0';
s_asg(1) := '1';
s_addr(1) := std_logic_vector(to_unsigned(8, 7));
s_epm(1) := x"000F";
usb_step(std_logic_vector(to_unsigned(8, 7)), "0000");
usb_step(std_logic_vector(to_unsigned(7, 7)), "0000");
-- ===============================================================
-- PHASE 6 (RANDOM)
-- ===============================================================
if not DIRECTED_ONLY then
reset_dut;
for k in 0 to 399 loop
if (urand mod 3) = 0 then
if (urand mod 2) = 0 then epm_of := x"000F"; else epm_of := x"0005"; end if;
if (urand mod 4) /= 0 then
do_cfg(urand mod 4, '1', std_logic_vector(to_unsigned(urand mod 6, 7)), epm_of);
else
do_cfg(urand mod 4, '0', std_logic_vector(to_unsigned(urand mod 6, 7)), epm_of);
end if;
end if;
usb_step(std_logic_vector(to_unsigned(urand mod 6, 7)),
std_logic_vector(to_unsigned(urand mod 4, 4)));
spi_step(std_logic_vector(to_unsigned(urand mod 16, N_DEV)));
end loop;
end if;
nrs := 0;
for i in 0 to 15 loop
if reach_spi(i) then nrs := nrs + 1; end if;
end loop;
nru := 0;
for i in 0 to 2559 loop
if reach_usb(i) then nru := nru + 1; end if;
end loop;
write(lo, string'("steps=") & integer'image(steps) &
string'(" checks=") & integer'image(checks) &
string'(" reach_spi=") & integer'image(nrs) &
string'("/16 reach_usb=") & integer'image(nru) &
string'("/2560 errors=") & integer'image(errors));
writeline(output, lo);
write(lo, string'("[spi] selections=") & integer'image(to_integer(unsigned(spi_n_sel))) &
string'(" idle=") & integer'image(to_integer(unsigned(spi_n_idle))) &
string'(" CONTENTIONS=") & integer'image(to_integer(unsigned(spi_n_contend))));
writeline(output, lo);
write(lo, string'("[usb] matches=") & integer'image(to_integer(unsigned(usb_n_match))) &
string'(" no_addr=") & integer'image(to_integer(unsigned(usb_n_no_addr))) &
string'(" no_ep=") & integer'image(to_integer(unsigned(usb_n_no_ep))) &
string'(" CONFLICTS=") & integer'image(to_integer(unsigned(usb_n_conflict))));
writeline(output, lo);
write(lo, string'("--- detectability from the vantage point a real device occupies ---"));
writeline(output, lo);
write(lo, string'("[spi] contention patterns=") & integer'image(spi_contend_patterns) &
string'(" distinguishable by any slave=") & integer'image(spi_contend_visible));
writeline(output, lo);
write(lo, string'("[usb] duplicate-address tokens=") & integer'image(usb_conflict_cases) &
string'(" detected by the device=") & integer'image(usb_conflict_visible));
writeline(output, lo);
if nrs /= 16 or nru /= 2560 then
write(lo, string'("FAIL: exhaustive sweep incomplete")); writeline(output, lo);
errors := errors + 1;
end if;
if errors = 0 then
write(lo, string'("PASS: 0 errors in ") & integer'image(checks) & string'(" checks"));
else
write(lo, string'("FAIL: ") & integer'image(errors) &
string'(" errors in ") & integer'image(checks) & string'(" checks"));
end if;
writeline(output, lo);
done <= true;
wait;
end process;
end architecture;What the third language found here
The generator defect described in chapter 28.1 was found on this chapter.
The VHDL random phase reported matches=0 no_addr=400 — four hundred tokens
and not a single address match — while the Verilog stream on identical directed
stimulus reported 127 matches.
The cause was that urand returned the low bits of a linear congruential
generator, where bit i has period 2 to the power (i+1). So urand mod 4,
used to pick which slot to configure, returned
3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, ...in perfect lockstep with the loop — phase-locked against the address picked by a later call, so the configured slot and the requested address never coincided. Its histogram over 2000 draws is exactly uniform, 500 of each, which is why no distribution check would have caught it.
12. Assertions
// ---------------------------------------------------------------------
// Properties for the two selection disciplines.
//
// NOT SIMULATED IN THIS CHAPTER. Icarus Verilog does not support
// concurrent assertions, so every number published here comes from the
// procedural checks in the testbenches. These are the same obligations
// in the form a commercial simulator or a formal tool would take.
//
// Note which module gets which properties. The SPI module's properties
// are all about ITS OWN OUTPUTS, because that is all it can promise. The
// USB module gets properties about the RELATIONSHIP between a token and
// the table, because it has the information to support them.
// ---------------------------------------------------------------------
module sel_sva #(parameter int N_DEV = 4, parameter int N_SLOT = 4) (
input logic clk,
input logic rst_n,
// spi
input logic [N_DEV-1:0] cs_n,
input logic spi_sel_valid,
input logic [1:0] spi_sel_idx,
input logic spi_contention,
// usb
input logic cfg_valid,
input logic [1:0] cfg_slot,
input logic req_valid,
input logic [6:0] req_addr,
input logic [3:0] req_ep,
input logic usb_sel_valid,
input logic [2:0] usb_sel_reason,
input logic usb_conflict,
input logic [31:0] usb_n_match
);
localparam logic [2:0] R_NONE = 3'd0, R_MATCH = 3'd1,
R_NO_ADDR = 3'd2, R_NO_EP = 3'd3;
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. a selection and a contention are mutually exclusive ----
// If both could be true the master would have no way to interpret the
// pair, and the monitor's whole output would be ambiguous.
a_spi_excl : assert property (!(spi_sel_valid && spi_contention));
// ---- 2. an idle bus selects nothing ----
a_spi_idle : assert property ((&cs_n) |-> !spi_sel_valid);
// ---- 3. the selected line is actually asserted ----
// Weaker than "it is the lowest asserted line", which needs a count and
// is left to the procedural bench. This is the part expressible locally.
a_spi_low : assert property (spi_sel_valid |-> !cs_n[spi_sel_idx]);
// ---- 4. no token, no selection ----
a_usb_gated : assert property (!req_valid |-> !usb_sel_valid);
// ---- 5. the reason code and the valid bit agree ----
// Two outputs encoding overlapping information must never disagree, or
// a consumer that trusts one and a consumer that trusts the other will
// behave differently on the same cycle.
a_usb_reason : assert property (usb_sel_valid == (usb_sel_reason == R_MATCH));
// ---- 6. NONE is reserved for the no-token case ----
a_usb_none : assert property ((usb_sel_reason == R_NONE) |-> !req_valid);
// ---- 7. the match counter moves ONLY on a match ----
// Stated in this direction on purpose. "A match implies the counter
// incremented" is the easy half; a counter that also advanced on some
// other condition would satisfy it while making every published total
// unfalsifiable, because the totals are read from these counters.
a_usb_count : assert property
((usb_n_match != $past(usb_n_match)) |-> $past(usb_sel_valid));
// ---- 8. a configuration write cannot change the CURRENT answer ----
//
// THE boundary property, and the one section 8 exists for. The table is
// registered, so a write and a token in the same cycle must resolve
// against the pre-write table. 2560 exhaustive tokens cannot reach this,
// because none of them writes and reads simultaneously.
property p_cfg_next_cycle;
(cfg_valid && req_valid) |-> (usb_sel_reason == $past(usb_sel_reason, 0));
endproperty
a_cfg_next : assert property (p_cfg_next_cycle);
// ---- COVER: the failure modes are actually reached ----
// Assertions over stimulus that never produces a contention or a
// duplicate address prove nothing. These covers are the denominator.
c_contend : cover property (spi_contention);
c_conflict : cover property (usb_conflict);
c_no_ep : cover property (usb_sel_reason == R_NO_EP);
c_no_addr : cover property (usb_sel_reason == R_NO_ADDR);
c_cfg_race : cover property (cfg_valid && req_valid);
endmodule13. Where UVM Fits
The verification problem here has a shape worth naming, because it recurs whenever two implementations of one idea are compared:
// ---------------------------------------------------------------------
// UVM structure for the two selection disciplines.
//
// NOT SIMULATED IN THIS CHAPTER. Icarus cannot compile UVM -- it breaks
// on virtual method dispatch -- so every number published comes from the
// procedural benches. This is the structure a production environment
// would use, and the mapping from the procedural bench is exact.
//
// THE DESIGN DECISION: one abstract selection request, TWO agents.
// The sequence library describes what is being asked for; each agent
// knows how its own protocol asks it. That is what makes the two
// disciplines comparable at all -- if each agent had its own transaction
// type, the scoreboard would be comparing two things it could not line
// up.
// ---------------------------------------------------------------------
// ---- one abstract request, protocol-independent ----
class select_request extends uvm_sequence_item;
`uvm_object_utils(select_request)
// What the transaction WANTS, expressed without reference to either bus.
rand int target; // which peripheral is intended
rand int endpoint; // which endpoint (ignored by SPI: it has none)
// The failure to inject. Modelled as a first-class field rather than as
// an error hook, because both failure modes are reachable only on
// purpose and the covergroup has to be able to see them.
rand bit inject_double; // SPI: assert a second chip select
rand bit inject_dup; // USB: give two slots the same address
constraint c_sane {
target inside {[0:3]};
endpoint inside {[0:3]};
}
// Failures stay rare so the clean path keeps most of the cycles; making
// them 50/50 would measure a broken bus rather than a working one.
constraint c_rare { inject_double dist {0 := 9, 1 := 1};
inject_dup dist {0 := 9, 1 := 1}; }
function new(string name = "select_request");
super.new(name);
endfunction
endclass
// ---- the SPI agent translates a request into WIRES ----
class spi_select_driver extends uvm_driver #(select_request);
`uvm_component_utils(spi_select_driver)
virtual spi_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
select_request tr;
logic [3:0] mask;
seq_item_port.get_next_item(tr);
mask = '1;
mask[tr.target] = 1'b0;
// The injected failure is a second line, which is exactly how the
// real fault arrives: one extra GPIO write.
if (tr.inject_double) mask[(tr.target + 1) % 4] = 1'b0;
vif.cs_n <= mask;
@(posedge vif.clk);
seq_item_port.item_done();
end
endtask
endclass
// ---- the USB agent translates the SAME request into a TOKEN ----
//
// Note what this driver has that the SPI driver does not: a configuration
// phase. It must assign an address before it can use one. That asymmetry
// between the two drivers is the protocol difference, and it is the reason
// the two agents are not interchangeable even though their sequence item
// is.
class usb_select_driver extends uvm_driver #(select_request);
`uvm_component_utils(usb_select_driver)
virtual usb_if vif;
task run_phase(uvm_phase phase);
forever begin
select_request tr;
seq_item_port.get_next_item(tr);
assign_address(tr.target, tr.target + 1, tr.inject_dup);
send_token(tr.target + 1, tr.endpoint);
seq_item_port.item_done();
end
endtask
task assign_address(int slot, int addr, bit dup);
vif.cfg_valid <= 1'b1;
vif.cfg_slot <= slot[1:0];
vif.cfg_assigned <= 1'b1;
vif.cfg_addr <= addr[6:0];
vif.cfg_ep_mask <= 16'h000F;
@(posedge vif.clk);
if (dup) begin
// A second slot at the same address -- the failure this discipline
// CAN see, which is the whole point of injecting it.
vif.cfg_slot <= ((slot + 1) % 4);
@(posedge vif.clk);
end
vif.cfg_valid <= 1'b0;
endtask
task send_token(int addr, int ep);
vif.req_valid <= 1'b1;
vif.req_addr <= addr[6:0];
vif.req_ep <= ep[3:0];
@(posedge vif.clk);
vif.req_valid <= 1'b0;
endtask
endclass
// ---- the scoreboard scores DETECTABILITY, not just correctness ----
//
// This is the part that differs from a conventional environment. Both
// designs are expected to resolve selection correctly; the measurement is
// whether the injected failure was REPORTED. So the scoreboard counts two
// things per discipline: failures injected, and failures observed.
class detectability_scoreboard extends uvm_scoreboard;
`uvm_component_utils(detectability_scoreboard)
int spi_injected, spi_observed;
int usb_injected, usb_observed;
function void report_phase(uvm_phase phase);
`uvm_info("DETECT", $sformatf(
"spi: %0d/%0d injected contentions observable by a slave; usb: %0d/%0d duplicate addresses reported",
spi_observed, spi_injected, usb_observed, usb_injected), UVM_LOW)
// The SPI figure is expected to be ZERO, and a non-zero value is the
// bug -- it would mean the environment gave a slave information no
// slave has. An environment that models a slave with all N select
// lines measures a bus nobody builds.
if (spi_observed != 0)
`uvm_error("DETECT", "a modelled SPI slave saw a contention: the slave model has too many pins")
endfunction
endclass
// ---- coverage is the cross, as always ----
class select_coverage extends uvm_subscriber #(select_request);
`uvm_component_utils(select_coverage)
covergroup cg with function sample(select_request tr);
cp_target : coverpoint tr.target { bins t[] = {[0:3]}; }
cp_endpoint : coverpoint tr.endpoint { bins e[] = {[0:3]}; }
cp_double : coverpoint tr.inject_double { bins b[] = {0, 1}; }
cp_dup : coverpoint tr.inject_dup { bins b[] = {0, 1}; }
// The failure injection crossed with the target, because a contention
// between slots 0 and 1 is a different circuit path from one between
// 3 and 0, and the tie-resolution direction only shows up in the
// cross.
x_fail : cross cp_target, cp_double;
x_dup : cross cp_target, cp_dup;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg = new();
endfunction
function void write(select_request t);
cg.sample(t);
endfunction
endclass14. Mutation Testing
Eight defects, injected one at a time into all three languages. Every
replacement is asserted by the generator, and each mutation is generated as
its own file rather than through nested ifdefs — because L5 and L7 touch
overlapping regions of the same if/else, and a nested guard makes it easy to
neutralise a different statement set in one language than in another.
| # | the injected defect | V-all | V-dir | SV-all | SV-dir | VHDL-all | VHDL-dir |
|---|---|---|---|---|---|---|---|
| BASE | unmodified designs | 0 | 0 | 0 | 0 | 0 | 0 |
| L1 | a contention reported as a clean selection | 572 | 22 | 572 | 22 | 548 | 22 |
| L2 | only three-way contention flagged | 160 | 12 | 160 | 12 | 167 | 12 |
| L3 | the tie resolves to the highest index | 286 | 11 | 286 | 11 | 274 | 11 |
| L4 | an unassigned slot matches | 1842 | 1648 | 1842 | 1648 | 1796 | 1648 |
| L5 | the default-address rule is dropped | 216 | 180 | 216 | 180 | 273 | 180 |
| L6 | SPI's blindness, injected into USB | 548 | 480 | 548 | 480 | 548 | 480 |
| L7 | the endpoint mask is ignored | 507 | 426 | 507 | 426 | 501 | 426 |
| L8 | every config write lands in slot 0 | 2600 | 2073 | 2600 | 2073 | 2652 | 2073 |
Every mutation is killed, and every one is killed by directed stimulus alone. The directed column is identical across all three languages at all eight rows — 22, 12, 11, 1648, 180, 480, 426, 2073.
L6 is the mutation this chapter was built for
L6 clamps m_addr_count at one, so usb_addr_select can never report a
duplicate address. Nothing else changes: the selection still resolves to the
correct slot, the reason code is still right, every byte of the data path is
untouched. The only thing removed is the ability to know.
In other words, L6 injects SPI's structural blindness into the USB matcher.
It scores 480 in the directed phase, identically in all three languages. That matters because it makes the chapter's central claim falsifiable rather than rhetorical:
Why the SPI scores are small, and why that is correct
L1, L2 and L3 score 22, 12 and 11 in the directed phase. Those look weak next to L8's 2073 and they are not weak at all — they are exactly the size of the domain:
| # | reachable instances of the defect | checks it breaks each time | predicted | measured |
|---|---|---|---|---|
| L1 | 11 contention patterns | 2 (sel_valid, exclusivity) | 22 | 22 |
| L2 | 6 exactly-two patterns | 2 (phase 1, phase 3 monitor) | 12 | 12 |
| L3 | 11 contention patterns | 1 (lowest-index) | 11 | 11 |
With four devices there are only 16 chip-select patterns and only 11 of them contain a contention. A score of 22 against a domain of 11 means every reachable instance is caught, twice over. There is no stimulus that could raise it, because there is no seventeenth pattern.
Run totals
| steps | checks | SPI reach | USB reach | errors | |
|---|---|---|---|---|---|
| Verilog, full | 3383 | 17,763 | 16 / 16 | 2560 / 2560 | 0 |
| Verilog, directed only | 2583 | 13,618 | 16 / 16 | 2560 / 2560 | 0 |
| SystemVerilog, full | 3383 | 17,763 | 16 / 16 | 2560 / 2560 | 0 |
| SystemVerilog, directed only | 2583 | 13,618 | 16 / 16 | 2560 / 2560 | 0 |
| VHDL, full | 3383 | 17,776 | 16 / 16 | 2560 / 2560 | 0 |
| VHDL, directed only | 2583 | 13,618 | 16 / 16 | 2560 / 2560 | 0 |
The directed-only rows are identical across all three languages in every column, including both exhaustive denominators. The full rows differ only in VHDL's check count, by 13, from its independent random stream.
15. What This Does Not Cover
Neither module moves data. There is no SPI shift register and no USB packet decoder. The comparison is about selection, and adding the data path would add volume without adding a difference.
The SPI slave model has one pin, by construction. That is the point of section 6, but it is worth stating as a limit: this chapter proves that a slave with one select pin cannot detect contention. A bus that routed every select line to every device could detect it — and would be a different bus, with N² routing instead of N.
No electrical modelling. On real hardware two selected slaves drive MISO simultaneously and the result depends on the drivers, the pull-ups and the timing. RTL simulation cannot model that contention; it models the logical consequence, which is that no participant knows.
Four devices and four slots. The SPI sweep is exhaustive at N = 4 (16 patterns). The detectability result generalises trivially — a slave with one pin cannot see N−1 others for any N — but the numbers in section 6 are for N = 4.
USB address space truncated to 0–4 in the sweep. The real field is 7 bits. Five values with four slots is enough to reach every structural case (no match, one match, duplicate, default address); the remaining 123 addresses add points, not cases.
Enumeration is not modelled, only its output. The host's SET_ADDRESS
sequence, the control transfer that carries it, and the reset that precedes it
are chapter 27.3's subject. Here the assignment is a register write, which is
what that sequence eventually becomes in hardware.
16. The Interview Answer
"USB versus SPI" is usually answered with pin counts, and the pin count is the weakest true thing you can say. Three sentences:
1. Name the mechanism. "SPI selects a peripheral with a wire that a board designer routed. USB selects one with an address the host assigned at enumeration. So SPI's selection mapping is structure and USB's is state."
2. Name the consequence that is not pin count. "Because a chip select is a wire only one slave receives, an SPI slave cannot tell a correct selection from a bus contention — both look like its own line going low. A USB device sees the whole address field, so it can detect a duplicate address in the cycle it arrives."
3. Say what that buys and what it costs. "That is why USB gets enumeration, hot-plug and re-numbering after a reset, and why SPI bring-up failures are symptoms rather than signatures. It is also why SPI needs no host, no driver stack and no protocol engine — which is exactly why SPI flash is still on your board."
17. What Carries Forward
Two comparisons, two mechanisms, two costs measured:
| chapter | the mechanism the other protocol lacks | the measured cost |
|---|---|---|
| 28.1, UART | synchronisation from a single edge | a tolerance budget shrinking as 1/N |
| 28.2, SPI | asking a device who it is | 0 of 11 failures detectable |
Both costs turned out to be about information: how much of it reaches the place where a decision is made. UART's receiver decides where a bit is from one edge. SPI's slave decides whether it is selected from one wire. In both cases the protocol that spends more on delivering information gets a capability that cannot be retrofitted.
The next chapter changes the question. UART and SPI are both local buses with exactly one master, and every difference so far has been about how that single master addresses its peripherals. Ethernet has no master at all.
Continue learning
Related tutorials
- Related topic
USB vs UART
UART spends zero wires on synchronisation and pays a tolerance budget that shrinks as the frame grows; USB spends a SYNC field, an encoding rule and a PLL to buy that budget away — measured across 5376 exhaustive points, not quoted.
- Related topic
USB vs Ethernet
USB has one authority that assigns every address; Ethernet has none, so a switch infers the topology from traffic — and an inferred table is wrong 294 times out of 1065 where an assigned one is wrong 0 times out of 130.
- Related topic
USB vs PCIe
USB holds one transaction outstanding per endpoint so its throughput is exactly 1/(latency+1) whatever the wire carries; PCIe tags many at once and needs exactly latency+1 tags to saturate — both measured as closed forms over 64 points.
- Related topic
USB Flash Drives
Every flash drive speaks Bulk-Only Transport — CBW out, data, CSW in. The spec enumerates thirteen cases of host-versus-device disagreement, six of them fatal, and the two rarest are the ones that ship broken.
Standards & specifications
- Governing standard
- USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)
Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the USB curriculum.
