USB · Module 29
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.
The first case study. Modules 1 through 28 built USB from first principles and compared it with everything else; this module looks at what that machinery turns into in products people actually buy.
A flash drive is the simplest of them, and it is not simple.
1. A Flash Drive Is Three Bulk Transfers In A Loop
Strip away the enclosure, the NAND and the wear levelling and what is left on the wire is a loop of three transfers:
CBW 31 bytes OUT "here is a command, and here is how many bytes
I expect to move, in this direction"
DATA 0..N bytes the payload, if there is one
CSW 13 bytes IN "it worked / it did not, and I moved this many
fewer bytes than you asked for"That is Bulk-Only Transport. Two bulk endpoints, no interrupt endpoint, no class-specific control requests beyond two, and a state machine small enough to fit on a page. It is the most widely deployed USB class protocol in existence by a wide margin.
That makes it an unusually good thing to build: a small, completely enumerable decision table where a single wrong cell is silent in normal use and unrecoverable when it fires.
2. The Thirteen Cases
Write the host's expectation as Hn (no data), Hi (device-to-host) or Ho (host-to-device), and the device's intent as Dn, Di or Do. Then every situation is one of these:
| # | host | device | length | outcome |
|---|---|---|---|---|
| 1 | Hn | Dn | — | normal, residue 0 |
| 2 | Hn | Di | — | phase error |
| 3 | Hn | Do | — | phase error |
| 4 | Hi | Dn | — | normal, residue = full, stall IN |
| 5 | Hi | Di | Hi > Di | normal, residue = Hi − Di, stall IN |
| 6 | Hi | Di | Hi = Di | normal, residue 0 |
| 7 | Hi | Di | Hi < Di | phase error |
| 8 | Hi | Do | — | phase error |
| 9 | Ho | Dn | — | normal, residue = full, stall OUT |
| 10 | Ho | Do | Ho > Do | normal, residue = Ho − Do, stall OUT |
| 11 | Ho | Do | Ho = Do | normal, residue 0 |
| 12 | Ho | Do | Ho < Do | phase error |
| 13 | Ho | Di | — | phase error |
Six of the thirteen are fatal, and they fall into exactly two families:
Direction disagreement is always fatal — cases 8 and 13. The host is waiting on one endpoint and the device wants to fill the other. Nothing the transport can do makes those meet.
The host under-allocating is always fatal — cases 2, 3, 7 and 12. There is no way to move more bytes than the host set aside, and no way to tell it so except by declaring the phase lost. The residue field can only report a shortfall, never an excess.
Everything else is survivable, and the residue carries the difference.
3. The Design (Verilog-2005)
// =====================================================================
// msc_bot_fsm -- Bulk-Only Transport, the protocol every USB flash
// drive on earth actually speaks.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// This is NOT a mass-storage device. There is no SCSI command decoder,
// no media, no FIFO and no error recovery beyond what the transport
// itself defines. It is the TRANSPORT: the wrapper around every
// command, and the arithmetic that decides what the host is told.
//
// WHY THIS MECHANISM IS WORTH RTL
// -------------------------------
// A flash drive is three bulk transfers in a loop:
//
// CBW 31 bytes OUT "here is a command, and here is how many
// bytes I expect to move, in this direction"
// DATA 0..N bytes the payload, if any
// CSW 13 bytes IN "it worked / it did not, and I moved this
// many fewer bytes than you asked for"
//
// The interesting part is the third line. The host states an
// expectation in the CBW -- dCBWDataTransferLength and the direction
// bit -- BEFORE the device has looked at the command. The device then
// discovers what it actually wants to do. Those two can disagree, in
// both length and direction, and the transport has to resolve every
// combination without ever leaving the host and device out of step.
//
// The BOT specification enumerates exactly THIRTEEN cases of that
// disagreement. They are not a style guide; they are the contract, and
// an implementation that gets case 7 wrong works perfectly against
// every well-behaved host until somebody issues a command whose real
// length exceeds what the host allocated -- and then the two ends
// disagree about how many bytes are on the wire, which no amount of
// retrying fixes.
//
// That is why this is the mechanism worth building: it is a small,
// completely enumerable decision table where a single wrong cell is
// invisible in normal use and unrecoverable when it fires.
// =====================================================================
module msc_bot_fsm (
input wire clk,
input wire rst_n,
// ---- the Command Block Wrapper, 31 bytes OUT ----
input wire cbw_valid,
input wire [31:0] cbw_tag, // dCBWTag: MUST come back in the CSW
input wire [31:0] cbw_len, // dCBWDataTransferLength: the host's expectation
input wire cbw_dir_in, // bmCBWFlags bit 7: 1 = device-to-host
input wire cbw_sig_ok, // dCBWSignature == 'USBC'
input wire cbw_cb_ok, // bCBWCBLength in 1..16
// ---- what the command layer says it will actually do ----
//
// Available only AFTER the CBW has been parsed, which is exactly why
// the disagreement below is possible at all.
input wire dev_none, // this command moves no data
input wire dev_dir_in, // direction the device wants
input wire [31:0] dev_len, // bytes the device will actually move
input wire dev_fail, // the command itself failed
// ---- the Command Status Wrapper, 13 bytes IN ----
output wire csw_valid,
output wire [31:0] csw_tag, // echoed, always
output wire [31:0] csw_residue, // dCSWDataResidue = expected - actual
output wire [1:0] csw_status, // 00 pass, 01 fail, 10 phase error
// ---- what the endpoints are told to do ----
output wire stall_in,
output wire stall_out,
output wire need_reset, // invalid CBW: wait for Reset Recovery
output wire [2:0] state,
output wire [31:0] n_cbw,
output wire [31:0] n_pass,
output wire [31:0] n_fail,
output wire [31:0] n_phase,
output wire [31:0] n_stall,
output wire [31:0] n_invalid
);
localparam [1:0] ST_PASS = 2'd0,
ST_FAIL = 2'd1,
ST_PHASE = 2'd2;
localparam [2:0] S_IDLE = 3'd0, // waiting for a CBW
S_DATA = 3'd1, // moving the data phase
S_CSW = 3'd2, // presenting the status
S_WEDGE = 3'd3; // invalid CBW: both endpoints stalled
// -------------------------------------------------------------------
// The host's stated expectation, as the spec's three kinds.
// -------------------------------------------------------------------
wire h_none = (cbw_len == 32'd0);
wire h_in = !h_none && cbw_dir_in;
wire h_out = !h_none && !cbw_dir_in;
// The device's intent. dev_dir_in and dev_len are IGNORED when the
// command moves no data -- a device that let a stale length leak into
// a no-data command would report a residue for bytes that were never
// going to move.
wire d_none = dev_none;
wire d_in = !d_none && dev_dir_in;
wire d_out = !d_none && !dev_dir_in;
// dev_len is read only through d_in / d_out below, both of which are
// false when the command moves no data -- so no zeroing guard is needed
// here, and adding one would be dead code. The bench asserts that
// invariant rather than the design defending against it.
wire [31:0] d_len = dev_len;
// -------------------------------------------------------------------
// THE THIRTEEN CASES.
//
// Direction disagreement is always fatal: cases 8 and 13. So is the
// host under-allocating, cases 2, 3, 7 and 12 -- there is no way to
// move more bytes than the host set aside, and no way to tell it so
// except by declaring the phase lost.
//
// Everything else is survivable, and the residue carries the
// shortfall.
// -------------------------------------------------------------------
wire dir_clash = (h_in && d_out) || (h_out && d_in); // 8, 13
wire host_short = (h_none && !d_none) // 2, 3
|| (h_in && d_in && (cbw_len < d_len)) // 7
|| (h_out && d_out && (cbw_len < d_len)); // 12
wire phase_err = dir_clash || host_short;
// Bytes the transport will actually move.
//
// NO CLAMP TO cbw_len IS NEEDED, and that is worth stating because the
// clamp is the obvious thing to write. Every situation in which the
// device wants more bytes than the host allocated is ALREADY a phase
// error: same-direction under-allocation is caught by host_short
// (cases 7 and 12) and opposite-direction by dir_clash (cases 8 and 13).
// So on any path that reaches this expression, d_len <= cbw_len holds.
//
// A clamp here would be unreachable code -- it was in the first version
// of this file, and the mutation that broke it scored exactly ZERO,
// which is how it was found. The invariant is asserted in the bench
// instead, where a violation would be visible.
//
// There is no phase-error guard here either, for the same reason: both
// consumers below (`residue` and the two short-data tests) already
// exclude the phase-error case themselves. Guarding it a second time
// scored zero as well. Two dead guards, both found by mutations that
// refused to die, and both deleted rather than explained away.
wire [31:0] moved = d_none ? 32'd0 : d_len;
// dCSWDataResidue. On a phase error the field is defined but
// meaningless to the host, which is about to reset the interface
// anyway; it is reported as the full expectation so a trace reads
// sensibly.
wire [31:0] residue = phase_err ? cbw_len : (cbw_len - moved);
// Cases 4 and 9: the host allocated a data phase and the device has
// nothing to put in it. The transport must terminate that phase
// rather than leave the host waiting, and a STALL is how BOT says so.
wire short_in = h_in && !phase_err && (moved < cbw_len);
wire short_out = h_out && !phase_err && (moved < cbw_len);
// -------------------------------------------------------------------
// CBW validity. A CBW that is not 31 bytes, lacks the signature, or
// carries an out-of-range command length is not a CBW at all: the
// device stalls BOTH endpoints and waits for Reset Recovery. It must
// NOT answer with a CSW, because a CSW would imply it understood a
// command it did not.
// -------------------------------------------------------------------
wire cbw_ok = cbw_sig_ok && cbw_cb_ok;
reg [2:0] st;
reg [31:0] tag_r, res_r;
reg [1:0] sts_r;
reg csw_r, sin_r, sout_r, wedge_r;
reg [31:0] cbw_c, pass_c, fail_c, phase_c, stall_c, inval_c;
assign state = st;
assign csw_valid = csw_r;
assign csw_tag = tag_r;
assign csw_residue = res_r;
assign csw_status = sts_r;
assign stall_in = sin_r;
assign stall_out = sout_r;
assign need_reset = wedge_r;
assign n_cbw = cbw_c;
assign n_pass = pass_c;
assign n_fail = fail_c;
assign n_phase = phase_c;
assign n_stall = stall_c;
assign n_invalid = inval_c;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st <= S_IDLE;
tag_r <= 32'd0;
res_r <= 32'd0;
sts_r <= ST_PASS;
csw_r <= 1'b0;
sin_r <= 1'b0;
sout_r <= 1'b0;
wedge_r <= 1'b0;
cbw_c <= 32'd0;
pass_c <= 32'd0;
fail_c <= 32'd0;
phase_c <= 32'd0;
stall_c <= 32'd0;
inval_c <= 32'd0;
end else begin
csw_r <= 1'b0;
sin_r <= 1'b0;
sout_r <= 1'b0;
case (st)
S_IDLE: begin
if (cbw_valid) begin
cbw_c <= cbw_c + 32'd1;
if (!cbw_ok) begin
// Not a CBW. Stall both ways and wedge until Reset
// Recovery -- answering anything else would be inventing
// a reply to a command that was never received.
st <= S_WEDGE;
wedge_r <= 1'b1;
sin_r <= 1'b1;
sout_r <= 1'b1;
inval_c <= inval_c + 32'd1;
end else begin
// THE TAG IS CAPTURED HERE, from the CBW, and echoed
// unchanged. A device that regenerates it, or echoes the
// previous one, breaks every host's command queue.
tag_r <= cbw_tag;
res_r <= residue;
sts_r <= phase_err ? ST_PHASE : (dev_fail ? ST_FAIL : ST_PASS);
if (phase_err) begin
phase_c <= phase_c + 32'd1;
// A phase error stalls the direction the host was
// waiting on, so the host stops waiting and comes to
// read the CSW.
if (h_in) sin_r <= 1'b1;
if (h_out) sout_r <= 1'b1;
if (h_in || h_out) stall_c <= stall_c + 32'd1;
st <= S_CSW;
end else begin
if (dev_fail) fail_c <= fail_c + 32'd1;
else pass_c <= pass_c + 32'd1;
// Cases 4 and 9: terminate a data phase the device
// cannot fill.
if (short_in) begin sin_r <= 1'b1; stall_c <= stall_c + 32'd1; end
if (short_out) begin sout_r <= 1'b1; stall_c <= stall_c + 32'd1; end
st <= (h_none && d_none) ? S_CSW : S_DATA;
end
end
end
end
// The data phase itself is not modelled byte by byte; what the
// transport owes the host is the LENGTH decision above, and that
// is already fixed.
S_DATA: st <= S_CSW;
S_CSW: begin
csw_r <= 1'b1;
st <= S_IDLE;
end
// Wedged. Only a reset gets out, which is what Reset Recovery
// means: the host has to clear both stalls and start again.
S_WEDGE: begin
sin_r <= 1'b1;
sout_r <= 1'b1;
end
default: st <= S_IDLE;
endcase
end
end
endmoduleTwo guards that are not there, and why
The file carries no clamp on moved and no phase-error guard on it either.
Both are the obvious things to write, and both would be dead code:
- every case where the device wants more bytes than the host allocated is
already a phase error, by
host_short(cases 7, 12) ordir_clash(cases 8, 13), sod_len <= cbw_lenholds everywheremovedis evaluated; - both consumers of
moved— the residue and the two short-data tests — already exclude the phase-error case themselves.
Neither of those was reasoned out in advance. Each was a guard in the first version of this file, and each was found by a mutation that refused to die: breaking an unreachable expression changes nothing, so the score came back exactly zero. Section 9 tells that story properly, because the reasoning generalises further than the guards do.
The invariants the design now relies on are asserted in the testbench instead, where a violation would be visible.
One command, and the decision that resolves it
4. The Same Command, Agreeing And Not
Case 6 and case 7 differ by one byte of expectation
The two differ by one number, and the difference between them is the difference between a working drive and a wedged one.
5. The Measurement
The directed phase is exhaustive over the whole decision space:
| dimension | values |
|---|---|
dCBWDataTransferLength | 0, 8, 16 |
| direction bit | IN, OUT |
| device intent | none, IN, OUT |
| device length | 0, 8, 16 |
| command result | pass, fail |
3 × 2 × 3 × 3 × 2 = 108 points, every one reachable. Device length and direction are swept even when the command declares no data, because ignoring them in that case is itself a property — a device that let a stale length leak into a no-data command would report a residue for bytes that were never going to move.
Directed phases only, identical in Verilog, SystemVerilog and VHDL:
| steps | checks | reach | errors | |
|---|---|---|---|---|
| directed only | 209 | 2,206 | 108 / 108 | 0 |
And every one of the thirteen cases is exercised, which the bench asserts rather than assumes:
| case | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| times hit | 13 | 15 | 13 | 13 | 8 | 70 | 3 | 13 | 13 | 7 | 5 | 3 | 15 |
6. The Testbench (Verilog)
The model does not share the design's derivation. The design decides with two boolean expressions and derives everything from them; the model classifies into one of the thirteen named cases and looks the answer up in a table written straight from the specification. A boolean the design got subtly wrong cannot be got wrong the same way by a lookup keyed on a case number.
// =====================================================================
// Testbench for msc_bot_fsm.
//
// THE MODEL IS THE SPEC'S TABLE, NOT THE DESIGN'S BOOLEANS.
//
// The design decides with two expressions -- `dir_clash` and
// `host_short` -- and derives everything from them. The model does the
// opposite: it classifies the situation into one of the thirteen NAMED
// cases the Bulk-Only Transport specification enumerates, and then
// looks the answer up in a table written straight from that document.
//
// Two different derivations of the same contract. A boolean the design
// got subtly wrong cannot be got wrong the same way by a lookup keyed
// on a case number, which is the entire reason for writing it this way
// rather than more carefully.
//
// Every valid-gated output is captured at a DEFINED instant. The stall
// decision is only meaningful in the cycle the CBW is consumed; reading
// it later reads a scheduling artefact.
// =====================================================================
`timescale 1ns/1ps
module tb_bt_v;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg cbw_valid = 1'b0;
reg [31:0] cbw_tag = 32'd0, cbw_len = 32'd0;
reg cbw_dir_in = 1'b0, cbw_sig_ok = 1'b1, cbw_cb_ok = 1'b1;
reg dev_none = 1'b0, dev_dir_in = 1'b0, dev_fail = 1'b0;
reg [31:0] dev_len = 32'd0;
wire csw_valid, stall_in, stall_out, need_reset;
wire [31:0] csw_tag, csw_residue;
wire [1:0] csw_status;
wire [2:0] state;
wire [31:0] n_cbw, n_pass, n_fail, n_phase, n_stall, n_invalid;
msc_bot_fsm dut (
.clk(clk), .rst_n(rst_n),
.cbw_valid(cbw_valid), .cbw_tag(cbw_tag), .cbw_len(cbw_len),
.cbw_dir_in(cbw_dir_in), .cbw_sig_ok(cbw_sig_ok), .cbw_cb_ok(cbw_cb_ok),
.dev_none(dev_none), .dev_dir_in(dev_dir_in), .dev_len(dev_len),
.dev_fail(dev_fail),
.csw_valid(csw_valid), .csw_tag(csw_tag), .csw_residue(csw_residue),
.csw_status(csw_status),
.stall_in(stall_in), .stall_out(stall_out), .need_reset(need_reset),
.state(state),
.n_cbw(n_cbw), .n_pass(n_pass), .n_fail(n_fail), .n_phase(n_phase),
.n_stall(n_stall), .n_invalid(n_invalid)
);
localparam [1:0] ST_PASS = 2'd0, ST_FAIL = 2'd1, ST_PHASE = 2'd2;
integer errors = 0, checks = 0, steps = 0;
integer seed;
// ---- cumulative across resets ----
//
// Every reset_dut zeroes the DUT's own counters, so a summary that read
// them directly would report only whatever happened after the last one.
// Per-step checks still use the DUT counters; these are for the totals.
integer c_cbw = 0, c_pass = 0, c_fail = 0, c_phase = 0,
c_stall = 0, c_invalid = 0;
// $random is SIGNED: mask the sign bit before any modulo.
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
// ---- what the design said, sampled at a DEFINED instant ----
reg obs_sin, obs_sout, obs_wedge, obs_csw;
reg [31:0] obs_tag, obs_res;
reg [1:0] obs_sts;
// ---- how many times each of the thirteen cases was exercised ----
integer case_hits [1:13];
// -------------------------------------------------------------------
// THE MODEL: classify into the spec's thirteen cases.
//
// h: 0 = Hn (no data), 1 = Hi (device-to-host), 2 = Ho (host-to-device)
// d: 0 = Dn (no data), 1 = Di (device-to-host), 2 = Do (host-to-device)
// -------------------------------------------------------------------
function [4:0] spec_case(input [1:0] h, input [1:0] d,
input [31:0] hl, input [31:0] dl);
begin
if (h == 2'd0 && d == 2'd0) spec_case = 5'd1;
else if (h == 2'd0 && d == 2'd1) spec_case = 5'd2;
else if (h == 2'd0 && d == 2'd2) spec_case = 5'd3;
else if (h == 2'd1 && d == 2'd0) spec_case = 5'd4;
else if (h == 2'd1 && d == 2'd1 && hl > dl) spec_case = 5'd5;
else if (h == 2'd1 && d == 2'd1 && hl == dl) spec_case = 5'd6;
else if (h == 2'd1 && d == 2'd1 && hl < dl) spec_case = 5'd7;
else if (h == 2'd1 && d == 2'd2) spec_case = 5'd8;
else if (h == 2'd2 && d == 2'd0) spec_case = 5'd9;
else if (h == 2'd2 && d == 2'd2 && hl > dl) spec_case = 5'd10;
else if (h == 2'd2 && d == 2'd2 && hl == dl) spec_case = 5'd11;
else if (h == 2'd2 && d == 2'd2 && hl < dl) spec_case = 5'd12;
else spec_case = 5'd13;
end
endfunction
// Which cases are a phase error, straight from the specification.
// Written as a set membership rather than as a condition, so it cannot
// share an algebraic mistake with the design.
function is_phase(input [4:0] c);
begin
is_phase = (c == 5'd2) || (c == 5'd3) || (c == 5'd7) ||
(c == 5'd8) || (c == 5'd12) || (c == 5'd13);
end
endfunction
// -------------------------------------------------------------------
// Drive one CBW and check everything the transport owes the host.
// -------------------------------------------------------------------
task run_cbw(input [31:0] tag, input [31:0] hlen, input hdir,
input dnone, input ddir, input [31:0] dlen,
input dfail, input sig_ok, input cb_ok);
reg [1:0] h, d;
reg [4:0] c;
reg e_phase, e_sin, e_sout;
reg [31:0] e_moved, e_res;
reg [1:0] e_sts;
reg [31:0] p0, f0, ph0, i0;
integer g;
begin
// classify, from the inputs only
h = (hlen == 32'd0) ? 2'd0 : (hdir ? 2'd1 : 2'd2);
d = dnone ? 2'd0 : (ddir ? 2'd1 : 2'd2);
c = spec_case(h, d, hlen, dnone ? 32'd0 : dlen);
e_phase = is_phase(c);
// the expected answer, from the case number
e_moved = e_phase ? 32'd0
: (d == 2'd0) ? 32'd0
: ((dlen > hlen) ? hlen : dlen);
e_res = e_phase ? hlen : (hlen - e_moved);
e_sts = e_phase ? ST_PHASE : (dfail ? ST_FAIL : ST_PASS);
// A short or absent data phase must be terminated on the side the
// host is waiting on; a phase error stalls that side too.
e_sin = e_phase ? (h == 2'd1) : ((h == 2'd1) && (e_moved < hlen));
e_sout = e_phase ? (h == 2'd2) : ((h == 2'd2) && (e_moved < hlen));
p0 = n_pass; f0 = n_fail; ph0 = n_phase; i0 = n_invalid;
cbw_valid = 1'b1; cbw_tag = tag; cbw_len = hlen; cbw_dir_in = hdir;
cbw_sig_ok = sig_ok; cbw_cb_ok = cb_ok;
dev_none = dnone; dev_dir_in = ddir; dev_len = dlen; dev_fail = dfail;
@(posedge clk); #1;
// ---- captured at the instant the CBW is consumed ----
obs_sin = stall_in;
obs_sout = stall_out;
obs_wedge = need_reset;
cbw_valid = 1'b0;
if (!(sig_ok && cb_ok)) begin
// ---- PROPERTY 1: an invalid CBW is not answered ----
//
// The device must stall both endpoints and wait for Reset
// Recovery. Replying with a CSW would tell the host the command
// was understood, and the host would believe it.
ck(obs_sin && obs_sout,
"an invalid CBW did not stall both endpoints");
ck(obs_wedge, "an invalid CBW did not request reset recovery");
ck(n_invalid == i0 + 32'd1, "an invalid CBW was not counted");
c_cbw = c_cbw + 1; c_invalid = c_invalid + 1;
// and no CSW, ever -- checked over a bounded window
obs_csw = 1'b0;
for (g = 0; g < 8; g = g + 1) begin
@(posedge clk); #1;
if (csw_valid) obs_csw = 1'b1;
end
ck(!obs_csw, "an invalid CBW was answered with a CSW");
ck(n_pass == p0 && n_fail == f0 && n_phase == ph0,
"an invalid CBW moved a status counter");
// reset out of the wedge, which is what Reset Recovery does
rst_n = 1'b0; @(posedge clk); @(posedge clk); rst_n = 1'b1;
@(posedge clk); #1;
steps = steps + 1;
end else begin
case_hits[c] = case_hits[c] + 1;
c_cbw = c_cbw + 1;
if (e_sts == ST_PHASE) c_phase = c_phase + 1;
else if (e_sts == ST_FAIL) c_fail = c_fail + 1;
else c_pass = c_pass + 1;
if (e_sin) c_stall = c_stall + 1;
if (e_sout) c_stall = c_stall + 1;
// ---- PROPERTY 2a: the invariants the design RELIES on ----
//
// The design carries no clamp and no zeroing guard, because two
// invariants make both unnecessary. Relying on an invariant is
// fine; relying on one nobody checks is not, so they are checked
// here -- and a violation would be a bench bug before it was a
// design bug.
if (!e_phase && d != 2'd0)
ck(dlen <= hlen,
"INVARIANT: a non-phase-error transfer wants more than the host allocated");
if (dnone)
ck(e_moved == 32'd0,
"INVARIANT: a no-data command moved a non-zero number of bytes");
// ---- PROPERTY 2: the stall decision matches the case ----
ck(obs_sin === e_sin, "stall_in disagrees with the spec case");
ck(obs_sout === e_sout, "stall_out disagrees with the spec case");
ck(!obs_wedge, "a valid CBW requested reset recovery");
// ---- wait, bounded, for the CSW ----
obs_csw = 1'b0;
for (g = 0; g < 8 && !obs_csw; g = g + 1) begin
@(posedge clk); #1;
if (csw_valid) begin
obs_csw = 1'b1;
obs_tag = csw_tag;
obs_res = csw_residue;
obs_sts = csw_status;
end
end
// ---- PROPERTY 3: exactly one CSW per valid CBW ----
//
// Silence would hang the host forever: it is waiting on an IN
// endpoint that will never produce anything.
ck(obs_csw, "a valid CBW produced no CSW");
if (obs_csw) begin
// ---- PROPERTY 4: THE TAG IS ECHOED UNCHANGED ----
//
// dCSWTag must equal dCBWTag. A device that regenerates it, or
// returns the previous one, breaks every host that has more
// than one command in flight -- and the corruption is silent,
// because both values are plausible 32-bit numbers.
ck(obs_tag === tag, "dCSWTag does not echo dCBWTag");
// ---- PROPERTY 5: the status matches the case ----
ck(obs_sts === e_sts, "csw_status disagrees with the spec case");
// ---- PROPERTY 6: the residue is expected minus actual ----
ck(obs_res === e_res, "dCSWDataResidue disagrees with the spec case");
// ---- PROPERTY 7: a residue never exceeds the expectation ----
//
// A residue larger than dCBWDataTransferLength is arithmetically
// impossible and would be read by the host as an enormous
// negative transfer.
ck(obs_res <= hlen, "the residue exceeds what the host asked for");
end
// ---- PROPERTY 8: exactly one status counter moved ----
if (e_sts == ST_PHASE)
ck(n_phase == ph0 + 32'd1 && n_pass == p0 && n_fail == f0,
"counters wrong for a phase error");
else if (e_sts == ST_FAIL)
ck(n_fail == f0 + 32'd1 && n_pass == p0 && n_phase == ph0,
"counters wrong for a failed command");
else
ck(n_pass == p0 + 32'd1 && n_fail == f0 && n_phase == ph0,
"counters wrong for a passing command");
ck(state === 3'd0, "the transport did not return to IDLE after a CSW");
steps = steps + 1;
end
end
endtask
task reset_dut;
begin
rst_n = 1'b0; cbw_valid = 1'b0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
end
endtask
// ---- exhaustive reach ----
//
// hlen(3) x hdir(2) x dkind(3) x dlen(3) x dfail(2) = 108, every
// combination an independent input. dlen and ddir are deliberately
// swept even when the device declares no data, because IGNORING them
// in that case is itself a property -- a device that let a stale
// length leak into a no-data command would report a residue for bytes
// that were never going to move.
reg reach [0:107];
integer nr, ri;
integer hi_, hd, dk, dl_, df, k, idx;
integer HLEN [0:2];
integer DLEN [0:2];
initial begin
for (ri = 0; ri < 108; ri = ri + 1) reach[ri] = 1'b0;
for (k = 1; k <= 13; k = k + 1) case_hits[k] = 0;
HLEN[0] = 0; HLEN[1] = 8; HLEN[2] = 16;
DLEN[0] = 0; DLEN[1] = 8; DLEN[2] = 16;
seed = 32'd29001;
reset_dut;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- the whole decision space.
// =============================================================
for (hi_ = 0; hi_ < 3; hi_ = hi_ + 1)
for (hd = 0; hd < 2; hd = hd + 1)
for (dk = 0; dk < 3; dk = dk + 1)
for (dl_ = 0; dl_ < 3; dl_ = dl_ + 1)
for (df = 0; df < 2; df = df + 1) begin
run_cbw(32'hA5A5_0000 + steps[31:0], HLEN[hi_][31:0], hd[0],
(dk == 0), (dk == 1), DLEN[dl_][31:0], df[0], 1'b1, 1'b1);
idx = ((((hi_ * 2 + hd) * 3 + dk) * 3 + dl_) * 2 + df);
reach[idx] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED) -- every one of the thirteen cases, by name.
//
// Phase 1 already reaches them all. This phase exists so the
// chapter can state that each NAMED case was exercised, and so a
// reader can find the one line that produces case 7.
// =============================================================
reset_dut;
run_cbw(32'h0000_0001, 32'd0, 1'b0, 1'b1, 1'b0, 32'd0, 1'b0, 1'b1, 1'b1); // 1 Hn = Dn
run_cbw(32'h0000_0002, 32'd0, 1'b0, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1); // 2 Hn < Di
run_cbw(32'h0000_0003, 32'd0, 1'b0, 1'b0, 1'b0, 32'd8, 1'b0, 1'b1, 1'b1); // 3 Hn < Do
run_cbw(32'h0000_0004, 32'd16, 1'b1, 1'b1, 1'b0, 32'd0, 1'b0, 1'b1, 1'b1); // 4 Hi > Dn
run_cbw(32'h0000_0005, 32'd16, 1'b1, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1); // 5 Hi > Di
run_cbw(32'h0000_0006, 32'd8, 1'b1, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1); // 6 Hi = Di
run_cbw(32'h0000_0007, 32'd8, 1'b1, 1'b0, 1'b1, 32'd16, 1'b0, 1'b1, 1'b1); // 7 Hi < Di
run_cbw(32'h0000_0008, 32'd8, 1'b1, 1'b0, 1'b0, 32'd8, 1'b0, 1'b1, 1'b1); // 8 Hi <> Do
run_cbw(32'h0000_0009, 32'd16, 1'b0, 1'b1, 1'b0, 32'd0, 1'b0, 1'b1, 1'b1); // 9 Ho > Dn
run_cbw(32'h0000_000A, 32'd16, 1'b0, 1'b0, 1'b0, 32'd8, 1'b0, 1'b1, 1'b1); // 10 Ho > Do
run_cbw(32'h0000_000B, 32'd8, 1'b0, 1'b0, 1'b0, 32'd8, 1'b0, 1'b1, 1'b1); // 11 Ho = Do
run_cbw(32'h0000_000C, 32'd8, 1'b0, 1'b0, 1'b0, 32'd16, 1'b0, 1'b1, 1'b1); // 12 Ho < Do
run_cbw(32'h0000_000D, 32'd8, 1'b0, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1); // 13 Ho <> Di
// =============================================================
// PHASE 3 (DIRECTED) -- the tag, over many distinct values.
//
// The tag is the one field with no arithmetic in it, which makes it
// the easiest to get wrong in a way nothing else notices. Sixty-four
// distinct tags, including the ends of the range and values that a
// truncating implementation would alias together.
// =============================================================
reset_dut;
for (k = 0; k < 64; k = k + 1) begin : tags
reg [31:0] t;
case (k % 4)
0: t = 32'h0000_0000 + k;
1: t = 32'hFFFF_FFFF - k;
2: t = 32'h0000_FFFF + (k << 16);
default: t = {k[7:0], ~k[7:0], k[7:0], ~k[7:0]};
endcase
run_cbw(t, 32'd8, 1'b1, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1);
ck(obs_tag === t, "a distinct tag was not echoed exactly");
end
// =============================================================
// PHASE 4 (DIRECTED, EXHAUSTIVE) -- CBW validity.
//
// Signature and command length, all four combinations. Three of
// them are not a CBW at all, and the device must wedge rather than
// guess.
// =============================================================
// Swept across every host length and direction as well, because the
// wedge must happen REGARDLESS of what the rest of the CBW claimed --
// the device has not understood the command and must not act on any
// part of it. 4 validity combinations x 3 lengths x 2 directions = 24,
// of which 18 are invalid.
//
// The first version tested the four validity combinations once each,
// which gave the mutation that answers an invalid CBW a domain of
// three and a score of nine. Widening a property costs nothing and
// turns an uninformative number into one that means something.
reset_dut;
for (k = 0; k < 4; k = k + 1)
for (hi_ = 0; hi_ < 3; hi_ = hi_ + 1)
for (hd = 0; hd < 2; hd = hd + 1)
run_cbw(32'hDEAD_0000 + k * 8 + hi_ * 2 + hd, HLEN[hi_][31:0], hd[0],
1'b0, 1'b1, 32'd8, 1'b0, k[1], k[0]);
// =============================================================
// PHASE 5 (RANDOM)
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 600; k = k + 1)
run_cbw(urand(0), (urand(0) % 3) * 8, (urand(0) % 2),
(urand(0) % 3) == 0, (urand(0) % 2), (urand(0) % 3) * 8,
(urand(0) % 4) == 0, 1'b1, 1'b1);
`endif
nr = 0; for (ri = 0; ri < 108; ri = ri + 1) if (reach[ri]) nr = nr + 1;
$display("steps=%0d checks=%0d reach=%0d/108 errors=%0d",
steps, checks, nr, errors);
$display("[bot] cbws=%0d pass=%0d fail=%0d phase_error=%0d stalls=%0d invalid=%0d",
c_cbw, c_pass, c_fail, c_phase, c_stall, c_invalid);
$display("--- the thirteen Bulk-Only Transport cases, times exercised ---");
$display(" case: 1 2 3 4 5 6 7 8 9 10 11 12 13");
$write(" hits:");
for (k = 1; k <= 13; k = k + 1) $write("%5d", case_hits[k]);
$display("");
for (k = 1; k <= 13; k = k + 1)
ck(case_hits[k] > 0, "a Bulk-Only Transport case was never exercised");
if (nr != 108) 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
endmodule7. SystemVerilog
// =====================================================================
// msc_bot_fsm -- SystemVerilog.
//
// Same hardware contract as the Verilog file: same ports, same widths,
// same reset values, same cycle-by-cycle behaviour. The status and state
// encodings become named types, and every continuous assignment is
// written as `logic` + `assign` on separate lines.
//
// THAT LAST POINT IS NOT COSMETIC. `logic x = expr;` is a one-shot
// VARIABLE INITIALISER in SystemVerilog -- evaluated once at time zero
// and never again -- while the Verilog `wire x = expr;` it came from is
// a continuous assignment. Earlier in this track the mechanical
// translation of five such lines produced 29,580 phantom failures
// against a design that was entirely correct.
//
// msc_bot_fsm -- Bulk-Only Transport, the protocol every USB flash
// drive on earth actually speaks.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// This is NOT a mass-storage device. There is no SCSI command decoder,
// no media, no FIFO and no error recovery beyond what the transport
// itself defines. It is the TRANSPORT: the wrapper around every
// command, and the arithmetic that decides what the host is told.
//
// WHY THIS MECHANISM IS WORTH RTL
// -------------------------------
// A flash drive is three bulk transfers in a loop:
//
// CBW 31 bytes OUT "here is a command, and here is how many
// bytes I expect to move, in this direction"
// DATA 0..N bytes the payload, if any
// CSW 13 bytes IN "it worked / it did not, and I moved this
// many fewer bytes than you asked for"
//
// The interesting part is the third line. The host states an
// expectation in the CBW -- dCBWDataTransferLength and the direction
// bit -- BEFORE the device has looked at the command. The device then
// discovers what it actually wants to do. Those two can disagree, in
// both length and direction, and the transport has to resolve every
// combination without ever leaving the host and device out of step.
//
// The BOT specification enumerates exactly THIRTEEN cases of that
// disagreement. They are not a style guide; they are the contract, and
// an implementation that gets case 7 wrong works perfectly against
// every well-behaved host until somebody issues a command whose real
// length exceeds what the host allocated -- and then the two ends
// disagree about how many bytes are on the wire, which no amount of
// retrying fixes.
//
// That is why this is the mechanism worth building: it is a small,
// completely enumerable decision table where a single wrong cell is
// invisible in normal use and unrecoverable when it fires.
// =====================================================================
module msc_bot_fsm (
input logic clk,
input logic rst_n,
// ---- the Command Block Wrapper, 31 bytes OUT ----
input logic cbw_valid,
input logic [31:0] cbw_tag, // dCBWTag: MUST come back in the CSW
input logic [31:0] cbw_len, // dCBWDataTransferLength: the host's expectation
input logic cbw_dir_in, // bmCBWFlags bit 7: 1 = device-to-host
input logic cbw_sig_ok, // dCBWSignature == 'USBC'
input logic cbw_cb_ok, // bCBWCBLength in 1..16
// ---- what the command layer says it will actually do ----
//
// Available only AFTER the CBW has been parsed, which is exactly why
// the disagreement below is possible at all.
input logic dev_none, // this command moves no data
input logic dev_dir_in, // direction the device wants
input logic [31:0] dev_len, // bytes the device will actually move
input logic dev_fail, // the command itself failed
// ---- the Command Status Wrapper, 13 bytes IN ----
output logic csw_valid,
output logic [31:0] csw_tag, // echoed, always
output logic [31:0] csw_residue, // dCSWDataResidue = expected - actual
output logic [1:0] csw_status, // 00 pass, 01 fail, 10 phase error
// ---- what the endpoints are told to do ----
output logic stall_in,
output logic stall_out,
output logic need_reset, // invalid CBW: wait for Reset Recovery
output logic [2:0] state,
output logic [31:0] n_cbw,
output logic [31:0] n_pass,
output logic [31:0] n_fail,
output logic [31:0] n_phase,
output logic [31:0] n_stall,
output logic [31:0] n_invalid
);
localparam [1:0] ST_PASS = 2'd0,
ST_FAIL = 2'd1,
ST_PHASE = 2'd2;
localparam [2:0] S_IDLE = 3'd0, // waiting for a CBW
S_DATA = 3'd1, // moving the data phase
S_CSW = 3'd2, // presenting the status
S_WEDGE = 3'd3; // invalid CBW: both endpoints stalled
// -------------------------------------------------------------------
// The host's stated expectation, as the spec's three kinds.
// -------------------------------------------------------------------
logic h_none;
assign h_none = (cbw_len == 32'd0);
logic h_in;
assign h_in = !h_none && cbw_dir_in;
logic h_out;
assign h_out = !h_none && !cbw_dir_in;
// The device's intent. dev_dir_in and dev_len are IGNORED when the
// command moves no data -- a device that let a stale length leak into
// a no-data command would report a residue for bytes that were never
// going to move.
logic d_none;
assign d_none = dev_none;
logic d_in;
assign d_in = !d_none && dev_dir_in;
logic d_out;
assign d_out = !d_none && !dev_dir_in;
// dev_len is read only through d_in / d_out below, both of which are
// false when the command moves no data -- so no zeroing guard is needed
// here, and adding one would be dead code. The bench asserts that
// invariant rather than the design defending against it.
logic [31:0] d_len;
assign d_len = dev_len;
// -------------------------------------------------------------------
// THE THIRTEEN CASES.
//
// Direction disagreement is always fatal: cases 8 and 13. So is the
// host under-allocating, cases 2, 3, 7 and 12 -- there is no way to
// move more bytes than the host set aside, and no way to tell it so
// except by declaring the phase lost.
//
// Everything else is survivable, and the residue carries the
// shortfall.
// -------------------------------------------------------------------
logic dir_clash;
assign dir_clash = (h_in && d_out) || (h_out && d_in); // 8, 13
logic host_short;
assign host_short = (h_none && !d_none) // 2, 3
|| (h_in && d_in && (cbw_len < d_len)) // 7
|| (h_out && d_out && (cbw_len < d_len)); // 12
logic phase_err;
assign phase_err = dir_clash || host_short;
// Bytes the transport will actually move.
//
// NO CLAMP TO cbw_len IS NEEDED, and that is worth stating because the
// clamp is the obvious thing to write. Every situation in which the
// device wants more bytes than the host allocated is ALREADY a phase
// error: same-direction under-allocation is caught by host_short
// (cases 7 and 12) and opposite-direction by dir_clash (cases 8 and 13).
// So on any path that reaches this expression, d_len <= cbw_len holds.
//
// A clamp here would be unreachable code -- it was in the first version
// of this file, and the mutation that broke it scored exactly ZERO,
// which is how it was found. The invariant is asserted in the bench
// instead, where a violation would be visible.
//
// There is no phase-error guard here either, for the same reason: both
// consumers below (`residue` and the two short-data tests) already
// exclude the phase-error case themselves. Guarding it a second time
// scored zero as well. Two dead guards, both found by mutations that
// refused to die, and both deleted rather than explained away.
logic [31:0] moved;
assign moved = d_none ? 32'd0 : d_len;
// dCSWDataResidue. On a phase error the field is defined but
// meaningless to the host, which is about to reset the interface
// anyway; it is reported as the full expectation so a trace reads
// sensibly.
logic [31:0] residue;
assign residue = phase_err ? cbw_len : (cbw_len - moved);
// Cases 4 and 9: the host allocated a data phase and the device has
// nothing to put in it. The transport must terminate that phase
// rather than leave the host waiting, and a STALL is how BOT says so.
logic short_in;
assign short_in = h_in && !phase_err && (moved < cbw_len);
logic short_out;
assign short_out = h_out && !phase_err && (moved < cbw_len);
// -------------------------------------------------------------------
// CBW validity. A CBW that is not 31 bytes, lacks the signature, or
// carries an out-of-range command length is not a CBW at all: the
// device stalls BOTH endpoints and waits for Reset Recovery. It must
// NOT answer with a CSW, because a CSW would imply it understood a
// command it did not.
// -------------------------------------------------------------------
logic cbw_ok;
assign cbw_ok = cbw_sig_ok && cbw_cb_ok;
logic [2:0] st;
logic [31:0] tag_r, res_r;
logic [1:0] sts_r;
logic csw_r, sin_r, sout_r, wedge_r;
logic [31:0] cbw_c, pass_c, fail_c, phase_c, stall_c, inval_c;
assign state = st;
assign csw_valid = csw_r;
assign csw_tag = tag_r;
assign csw_residue = res_r;
assign csw_status = sts_r;
assign stall_in = sin_r;
assign stall_out = sout_r;
assign need_reset = wedge_r;
assign n_cbw = cbw_c;
assign n_pass = pass_c;
assign n_fail = fail_c;
assign n_phase = phase_c;
assign n_stall = stall_c;
assign n_invalid = inval_c;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st <= S_IDLE;
tag_r <= 32'd0;
res_r <= 32'd0;
sts_r <= ST_PASS;
csw_r <= 1'b0;
sin_r <= 1'b0;
sout_r <= 1'b0;
wedge_r <= 1'b0;
cbw_c <= 32'd0;
pass_c <= 32'd0;
fail_c <= 32'd0;
phase_c <= 32'd0;
stall_c <= 32'd0;
inval_c <= 32'd0;
end else begin
csw_r <= 1'b0;
sin_r <= 1'b0;
sout_r <= 1'b0;
case (st)
S_IDLE: begin
if (cbw_valid) begin
cbw_c <= cbw_c + 32'd1;
if (!cbw_ok) begin
// Not a CBW. Stall both ways and wedge until Reset
// Recovery -- answering anything else would be inventing
// a reply to a command that was never received.
st <= S_WEDGE;
wedge_r <= 1'b1;
sin_r <= 1'b1;
sout_r <= 1'b1;
inval_c <= inval_c + 32'd1;
end else begin
// THE TAG IS CAPTURED HERE, from the CBW, and echoed
// unchanged. A device that regenerates it, or echoes the
// previous one, breaks every host's command queue.
tag_r <= cbw_tag;
res_r <= residue;
sts_r <= phase_err ? ST_PHASE : (dev_fail ? ST_FAIL : ST_PASS);
if (phase_err) begin
phase_c <= phase_c + 32'd1;
// A phase error stalls the direction the host was
// waiting on, so the host stops waiting and comes to
// read the CSW.
if (h_in) sin_r <= 1'b1;
if (h_out) sout_r <= 1'b1;
if (h_in || h_out) stall_c <= stall_c + 32'd1;
st <= S_CSW;
end else begin
if (dev_fail) fail_c <= fail_c + 32'd1;
else pass_c <= pass_c + 32'd1;
// Cases 4 and 9: terminate a data phase the device
// cannot fill.
if (short_in) begin sin_r <= 1'b1; stall_c <= stall_c + 32'd1; end
if (short_out) begin sout_r <= 1'b1; stall_c <= stall_c + 32'd1; end
st <= (h_none && d_none) ? S_CSW : S_DATA;
end
end
end
end
// The data phase itself is not modelled byte by byte; what the
// transport owes the host is the LENGTH decision above, and that
// is already fixed.
S_DATA: st <= S_CSW;
S_CSW: begin
csw_r <= 1'b1;
st <= S_IDLE;
end
// Wedged. Only a reset gets out, which is what Reset Recovery
// means: the host has to clear both stalls and start again.
S_WEDGE: begin
sin_r <= 1'b1;
sout_r <= 1'b1;
end
default: st <= S_IDLE;
endcase
end
end
endmoduleThe SystemVerilog testbench
// =====================================================================
// Testbench for msc_bot_fsm -- SystemVerilog.
//
// 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.
//
// THE MODEL IS THE SPEC'S TABLE, NOT THE DESIGN'S BOOLEANS.
//
// The design decides with two expressions -- `dir_clash` and
// `host_short` -- and derives everything from them. The model does the
// opposite: it classifies the situation into one of the thirteen NAMED
// cases the Bulk-Only Transport specification enumerates, and then
// looks the answer up in a table written straight from that document.
//
// Two different derivations of the same contract. A boolean the design
// got subtly wrong cannot be got wrong the same way by a lookup keyed
// on a case number, which is the entire reason for writing it this way
// rather than more carefully.
//
// Every valid-gated output is captured at a DEFINED instant. The stall
// decision is only meaningful in the cycle the CBW is consumed; reading
// it later reads a scheduling artefact.
// =====================================================================
`timescale 1ns/1ps
module tb_bt_sv;
logic clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
logic cbw_valid = 1'b0;
logic [31:0] cbw_tag = 32'd0, cbw_len = 32'd0;
logic cbw_dir_in = 1'b0, cbw_sig_ok = 1'b1, cbw_cb_ok = 1'b1;
logic dev_none = 1'b0, dev_dir_in = 1'b0, dev_fail = 1'b0;
logic [31:0] dev_len = 32'd0;
logic csw_valid, stall_in, stall_out, need_reset;
logic [31:0] csw_tag, csw_residue;
logic [1:0] csw_status;
logic [2:0] state;
logic [31:0] n_cbw, n_pass, n_fail, n_phase, n_stall, n_invalid;
msc_bot_fsm dut (
.clk(clk), .rst_n(rst_n),
.cbw_valid(cbw_valid), .cbw_tag(cbw_tag), .cbw_len(cbw_len),
.cbw_dir_in(cbw_dir_in), .cbw_sig_ok(cbw_sig_ok), .cbw_cb_ok(cbw_cb_ok),
.dev_none(dev_none), .dev_dir_in(dev_dir_in), .dev_len(dev_len),
.dev_fail(dev_fail),
.csw_valid(csw_valid), .csw_tag(csw_tag), .csw_residue(csw_residue),
.csw_status(csw_status),
.stall_in(stall_in), .stall_out(stall_out), .need_reset(need_reset),
.state(state),
.n_cbw(n_cbw), .n_pass(n_pass), .n_fail(n_fail), .n_phase(n_phase),
.n_stall(n_stall), .n_invalid(n_invalid)
);
localparam [1:0] ST_PASS = 2'd0, ST_FAIL = 2'd1, ST_PHASE = 2'd2;
int errors = 0, checks = 0, steps = 0;
int seed;
// ---- cumulative across resets ----
//
// Every reset_dut zeroes the DUT's own counters, so a summary that read
// them directly would report only whatever happened after the last one.
// Per-step checks still use the DUT counters; these are for the totals.
int c_cbw = 0, c_pass = 0, c_fail = 0, c_phase = 0,
c_stall = 0, c_invalid = 0;
// $random is SIGNED: mask the sign bit before any modulo.
function automatic logic [31:0] urand();
return $random(seed) & 32'h3FFF_FFFF;
endfunction
task automatic ck(input logic cond, input string what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t step#%0d: %s", $time, steps, what);
end
end
endtask
// ---- what the design said, sampled at a DEFINED instant ----
logic obs_sin, obs_sout, obs_wedge, obs_csw;
logic [31:0] obs_tag, obs_res;
logic [1:0] obs_sts;
// ---- how many times each of the thirteen cases was exercised ----
int case_hits [1:13];
// -------------------------------------------------------------------
// THE MODEL: classify into the spec's thirteen cases.
//
// h: 0 = Hn (no data), 1 = Hi (device-to-host), 2 = Ho (host-to-device)
// d: 0 = Dn (no data), 1 = Di (device-to-host), 2 = Do (host-to-device)
// -------------------------------------------------------------------
function automatic logic [4:0] spec_case(input [1:0] h, input [1:0] d,
input [31:0] hl, input [31:0] dl);
begin
if (h == 2'd0 && d == 2'd0) spec_case = 5'd1;
else if (h == 2'd0 && d == 2'd1) spec_case = 5'd2;
else if (h == 2'd0 && d == 2'd2) spec_case = 5'd3;
else if (h == 2'd1 && d == 2'd0) spec_case = 5'd4;
else if (h == 2'd1 && d == 2'd1 && hl > dl) spec_case = 5'd5;
else if (h == 2'd1 && d == 2'd1 && hl == dl) spec_case = 5'd6;
else if (h == 2'd1 && d == 2'd1 && hl < dl) spec_case = 5'd7;
else if (h == 2'd1 && d == 2'd2) spec_case = 5'd8;
else if (h == 2'd2 && d == 2'd0) spec_case = 5'd9;
else if (h == 2'd2 && d == 2'd2 && hl > dl) spec_case = 5'd10;
else if (h == 2'd2 && d == 2'd2 && hl == dl) spec_case = 5'd11;
else if (h == 2'd2 && d == 2'd2 && hl < dl) spec_case = 5'd12;
else spec_case = 5'd13;
end
endfunction
// Which cases are a phase error, straight from the specification.
// Written as a set membership rather than as a condition, so it cannot
// share an algebraic mistake with the design.
function automatic logic is_phase(input [4:0] c);
begin
is_phase = (c == 5'd2) || (c == 5'd3) || (c == 5'd7) ||
(c == 5'd8) || (c == 5'd12) || (c == 5'd13);
end
endfunction
// -------------------------------------------------------------------
// Drive one CBW and check everything the transport owes the host.
// -------------------------------------------------------------------
task automatic run_cbw(input [31:0] tag, input [31:0] hlen, input hdir,
input dnone, input ddir, input [31:0] dlen,
input dfail, input sig_ok, input cb_ok);
logic [1:0] h, d;
logic [4:0] c;
logic e_phase, e_sin, e_sout;
logic [31:0] e_moved, e_res;
logic [1:0] e_sts;
logic [31:0] p0, f0, ph0, i0;
int g;
begin
// classify, from the inputs only
h = (hlen == 32'd0) ? 2'd0 : (hdir ? 2'd1 : 2'd2);
d = dnone ? 2'd0 : (ddir ? 2'd1 : 2'd2);
c = spec_case(h, d, hlen, dnone ? 32'd0 : dlen);
e_phase = is_phase(c);
// the expected answer, from the case number
e_moved = e_phase ? 32'd0
: (d == 2'd0) ? 32'd0
: ((dlen > hlen) ? hlen : dlen);
e_res = e_phase ? hlen : (hlen - e_moved);
e_sts = e_phase ? ST_PHASE : (dfail ? ST_FAIL : ST_PASS);
// A short or absent data phase must be terminated on the side the
// host is waiting on; a phase error stalls that side too.
e_sin = e_phase ? (h == 2'd1) : ((h == 2'd1) && (e_moved < hlen));
e_sout = e_phase ? (h == 2'd2) : ((h == 2'd2) && (e_moved < hlen));
p0 = n_pass; f0 = n_fail; ph0 = n_phase; i0 = n_invalid;
cbw_valid = 1'b1; cbw_tag = tag; cbw_len = hlen; cbw_dir_in = hdir;
cbw_sig_ok = sig_ok; cbw_cb_ok = cb_ok;
dev_none = dnone; dev_dir_in = ddir; dev_len = dlen; dev_fail = dfail;
@(posedge clk); #1;
// ---- captured at the instant the CBW is consumed ----
obs_sin = stall_in;
obs_sout = stall_out;
obs_wedge = need_reset;
cbw_valid = 1'b0;
if (!(sig_ok && cb_ok)) begin
// ---- PROPERTY 1: an invalid CBW is not answered ----
//
// The device must stall both endpoints and wait for Reset
// Recovery. Replying with a CSW would tell the host the command
// was understood, and the host would believe it.
ck(obs_sin && obs_sout,
"an invalid CBW did not stall both endpoints");
ck(obs_wedge, "an invalid CBW did not request reset recovery");
ck(n_invalid == i0 + 32'd1, "an invalid CBW was not counted");
c_cbw = c_cbw + 1; c_invalid = c_invalid + 1;
// and no CSW, ever -- checked over a bounded window
obs_csw = 1'b0;
for (g = 0; g < 8; g = g + 1) begin
@(posedge clk); #1;
if (csw_valid) obs_csw = 1'b1;
end
ck(!obs_csw, "an invalid CBW was answered with a CSW");
ck(n_pass == p0 && n_fail == f0 && n_phase == ph0,
"an invalid CBW moved a status counter");
// reset out of the wedge, which is what Reset Recovery does
rst_n = 1'b0; @(posedge clk); @(posedge clk); rst_n = 1'b1;
@(posedge clk); #1;
steps = steps + 1;
end else begin
case_hits[c] = case_hits[c] + 1;
c_cbw = c_cbw + 1;
if (e_sts == ST_PHASE) c_phase = c_phase + 1;
else if (e_sts == ST_FAIL) c_fail = c_fail + 1;
else c_pass = c_pass + 1;
if (e_sin) c_stall = c_stall + 1;
if (e_sout) c_stall = c_stall + 1;
// ---- PROPERTY 2a: the invariants the design RELIES on ----
//
// The design carries no clamp and no zeroing guard, because two
// invariants make both unnecessary. Relying on an invariant is
// fine; relying on one nobody checks is not, so they are checked
// here -- and a violation would be a bench bug before it was a
// design bug.
if (!e_phase && d != 2'd0)
ck(dlen <= hlen,
"INVARIANT: a non-phase-error transfer wants more than the host allocated");
if (dnone)
ck(e_moved == 32'd0,
"INVARIANT: a no-data command moved a non-zero number of bytes");
// ---- PROPERTY 2: the stall decision matches the case ----
ck(obs_sin === e_sin, "stall_in disagrees with the spec case");
ck(obs_sout === e_sout, "stall_out disagrees with the spec case");
ck(!obs_wedge, "a valid CBW requested reset recovery");
// ---- wait, bounded, for the CSW ----
obs_csw = 1'b0;
for (g = 0; g < 8 && !obs_csw; g = g + 1) begin
@(posedge clk); #1;
if (csw_valid) begin
obs_csw = 1'b1;
obs_tag = csw_tag;
obs_res = csw_residue;
obs_sts = csw_status;
end
end
// ---- PROPERTY 3: exactly one CSW per valid CBW ----
//
// Silence would hang the host forever: it is waiting on an IN
// endpoint that will never produce anything.
ck(obs_csw, "a valid CBW produced no CSW");
if (obs_csw) begin
// ---- PROPERTY 4: THE TAG IS ECHOED UNCHANGED ----
//
// dCSWTag must equal dCBWTag. A device that regenerates it, or
// returns the previous one, breaks every host that has more
// than one command in flight -- and the corruption is silent,
// because both values are plausible 32-bit numbers.
ck(obs_tag === tag, "dCSWTag does not echo dCBWTag");
// ---- PROPERTY 5: the status matches the case ----
ck(obs_sts === e_sts, "csw_status disagrees with the spec case");
// ---- PROPERTY 6: the residue is expected minus actual ----
ck(obs_res === e_res, "dCSWDataResidue disagrees with the spec case");
// ---- PROPERTY 7: a residue never exceeds the expectation ----
//
// A residue larger than dCBWDataTransferLength is arithmetically
// impossible and would be read by the host as an enormous
// negative transfer.
ck(obs_res <= hlen, "the residue exceeds what the host asked for");
end
// ---- PROPERTY 8: exactly one status counter moved ----
if (e_sts == ST_PHASE)
ck(n_phase == ph0 + 32'd1 && n_pass == p0 && n_fail == f0,
"counters wrong for a phase error");
else if (e_sts == ST_FAIL)
ck(n_fail == f0 + 32'd1 && n_pass == p0 && n_phase == ph0,
"counters wrong for a failed command");
else
ck(n_pass == p0 + 32'd1 && n_fail == f0 && n_phase == ph0,
"counters wrong for a passing command");
ck(state === 3'd0, "the transport did not return to IDLE after a CSW");
steps = steps + 1;
end
end
endtask
task automatic reset_dut;
begin
rst_n = 1'b0; cbw_valid = 1'b0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
end
endtask
// ---- exhaustive reach ----
//
// hlen(3) x hdir(2) x dkind(3) x dlen(3) x dfail(2) = 108, every
// combination an independent input. dlen and ddir are deliberately
// swept even when the device declares no data, because IGNORING them
// in that case is itself a property -- a device that let a stale
// length leak into a no-data command would report a residue for bytes
// that were never going to move.
logic reach [0:107];
int nr, ri;
int hi_, hd, dk, dl_, df, k, idx;
int HLEN [0:2];
int DLEN [0:2];
initial begin
for (ri = 0; ri < 108; ri = ri + 1) reach[ri] = 1'b0;
for (k = 1; k <= 13; k = k + 1) case_hits[k] = 0;
HLEN[0] = 0; HLEN[1] = 8; HLEN[2] = 16;
DLEN[0] = 0; DLEN[1] = 8; DLEN[2] = 16;
seed = 32'd29001;
reset_dut;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- the whole decision space.
// =============================================================
for (hi_ = 0; hi_ < 3; hi_ = hi_ + 1)
for (hd = 0; hd < 2; hd = hd + 1)
for (dk = 0; dk < 3; dk = dk + 1)
for (dl_ = 0; dl_ < 3; dl_ = dl_ + 1)
for (df = 0; df < 2; df = df + 1) begin
run_cbw(32'hA5A5_0000 + steps[31:0], HLEN[hi_][31:0], hd[0],
(dk == 0), (dk == 1), DLEN[dl_][31:0], df[0], 1'b1, 1'b1);
idx = ((((hi_ * 2 + hd) * 3 + dk) * 3 + dl_) * 2 + df);
reach[idx] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED) -- every one of the thirteen cases, by name.
//
// Phase 1 already reaches them all. This phase exists so the
// chapter can state that each NAMED case was exercised, and so a
// reader can find the one line that produces case 7.
// =============================================================
reset_dut;
run_cbw(32'h0000_0001, 32'd0, 1'b0, 1'b1, 1'b0, 32'd0, 1'b0, 1'b1, 1'b1); // 1 Hn = Dn
run_cbw(32'h0000_0002, 32'd0, 1'b0, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1); // 2 Hn < Di
run_cbw(32'h0000_0003, 32'd0, 1'b0, 1'b0, 1'b0, 32'd8, 1'b0, 1'b1, 1'b1); // 3 Hn < Do
run_cbw(32'h0000_0004, 32'd16, 1'b1, 1'b1, 1'b0, 32'd0, 1'b0, 1'b1, 1'b1); // 4 Hi > Dn
run_cbw(32'h0000_0005, 32'd16, 1'b1, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1); // 5 Hi > Di
run_cbw(32'h0000_0006, 32'd8, 1'b1, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1); // 6 Hi = Di
run_cbw(32'h0000_0007, 32'd8, 1'b1, 1'b0, 1'b1, 32'd16, 1'b0, 1'b1, 1'b1); // 7 Hi < Di
run_cbw(32'h0000_0008, 32'd8, 1'b1, 1'b0, 1'b0, 32'd8, 1'b0, 1'b1, 1'b1); // 8 Hi <> Do
run_cbw(32'h0000_0009, 32'd16, 1'b0, 1'b1, 1'b0, 32'd0, 1'b0, 1'b1, 1'b1); // 9 Ho > Dn
run_cbw(32'h0000_000A, 32'd16, 1'b0, 1'b0, 1'b0, 32'd8, 1'b0, 1'b1, 1'b1); // 10 Ho > Do
run_cbw(32'h0000_000B, 32'd8, 1'b0, 1'b0, 1'b0, 32'd8, 1'b0, 1'b1, 1'b1); // 11 Ho = Do
run_cbw(32'h0000_000C, 32'd8, 1'b0, 1'b0, 1'b0, 32'd16, 1'b0, 1'b1, 1'b1); // 12 Ho < Do
run_cbw(32'h0000_000D, 32'd8, 1'b0, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1); // 13 Ho <> Di
// =============================================================
// PHASE 3 (DIRECTED) -- the tag, over many distinct values.
//
// The tag is the one field with no arithmetic in it, which makes it
// the easiest to get wrong in a way nothing else notices. Sixty-four
// distinct tags, including the ends of the range and values that a
// truncating implementation would alias together.
// =============================================================
reset_dut;
for (k = 0; k < 64; k = k + 1) begin : tags
logic [31:0] t;
case (k % 4)
0: t = 32'h0000_0000 + k;
1: t = 32'hFFFF_FFFF - k;
2: t = 32'h0000_FFFF + (k << 16);
default: t = {k[7:0], ~k[7:0], k[7:0], ~k[7:0]};
endcase
run_cbw(t, 32'd8, 1'b1, 1'b0, 1'b1, 32'd8, 1'b0, 1'b1, 1'b1);
ck(obs_tag === t, "a distinct tag was not echoed exactly");
end
// =============================================================
// PHASE 4 (DIRECTED, EXHAUSTIVE) -- CBW validity.
//
// Signature and command length, all four combinations. Three of
// them are not a CBW at all, and the device must wedge rather than
// guess.
// =============================================================
// Swept across every host length and direction as well, because the
// wedge must happen REGARDLESS of what the rest of the CBW claimed --
// the device has not understood the command and must not act on any
// part of it. 4 validity combinations x 3 lengths x 2 directions = 24,
// of which 18 are invalid.
//
// The first version tested the four validity combinations once each,
// which gave the mutation that answers an invalid CBW a domain of
// three and a score of nine. Widening a property costs nothing and
// turns an uninformative number into one that means something.
reset_dut;
for (k = 0; k < 4; k = k + 1)
for (hi_ = 0; hi_ < 3; hi_ = hi_ + 1)
for (hd = 0; hd < 2; hd = hd + 1)
run_cbw(32'hDEAD_0000 + k * 8 + hi_ * 2 + hd, HLEN[hi_][31:0], hd[0],
1'b0, 1'b1, 32'd8, 1'b0, k[1], k[0]);
// =============================================================
// PHASE 5 (RANDOM)
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 600; k = k + 1)
run_cbw(urand(), (urand() % 3) * 8, (urand() % 2),
(urand() % 3) == 0, (urand() % 2), (urand() % 3) * 8,
(urand() % 4) == 0, 1'b1, 1'b1);
`endif
nr = 0; for (ri = 0; ri < 108; ri = ri + 1) if (reach[ri]) nr = nr + 1;
$display("steps=%0d checks=%0d reach=%0d/108 errors=%0d",
steps, checks, nr, errors);
$display("[bot] cbws=%0d pass=%0d fail=%0d phase_error=%0d stalls=%0d invalid=%0d",
c_cbw, c_pass, c_fail, c_phase, c_stall, c_invalid);
$display("--- the thirteen Bulk-Only Transport cases, times exercised ---");
$display(" case: 1 2 3 4 5 6 7 8 9 10 11 12 13");
$write(" hits:");
for (k = 1; k <= 13; k = k + 1) $write("%5d", case_hits[k]);
$display("");
for (k = 1; k <= 13; k = k + 1)
ck(case_hits[k] > 0, "a Bulk-Only Transport case was never exercised");
if (nr != 108) 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
endmoduleSame seed and phase order as the Verilog bench, so any difference between those two mutation columns is a real difference between the designs. Their outputs are byte-identical, including the case-hit distribution.
8. VHDL-2008
-- =====================================================================
-- msc_bot_fsm -- Bulk-Only Transport, 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.
--
-- This is NOT a mass-storage device. There is no SCSI decoder, no media
-- and no FIFO. It is the TRANSPORT: the wrapper around every command,
-- and the arithmetic that decides what the host is told.
--
-- A flash drive is three bulk transfers in a loop -- CBW out, data,
-- CSW in -- and the interesting part is the third. The host states an
-- expectation in the CBW BEFORE the device has looked at the command;
-- the device then discovers what it actually wants to do. Those two can
-- disagree in length and in direction, and the specification enumerates
-- exactly THIRTEEN cases of that disagreement.
--
-- Getting one cell of that table wrong is invisible against every
-- well-behaved host and unrecoverable when it fires, because the two
-- ends then disagree about how many bytes are on the wire and no amount
-- of retrying fixes it.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity msc_bot_fsm is
port (
clk : in std_logic;
rst_n : in std_logic;
-- the Command Block Wrapper, 31 bytes OUT
cbw_valid : in std_logic;
cbw_tag : in std_logic_vector(31 downto 0); -- MUST come back in the CSW
cbw_len : in std_logic_vector(31 downto 0); -- the host's expectation
cbw_dir_in : in std_logic; -- 1 = device-to-host
cbw_sig_ok : in std_logic; -- dCBWSignature == 'USBC'
cbw_cb_ok : in std_logic; -- bCBWCBLength in 1..16
-- what the command layer says it will actually do, available only
-- AFTER the CBW has been parsed -- which is why disagreement is
-- possible at all
dev_none : in std_logic;
dev_dir_in : in std_logic;
dev_len : in std_logic_vector(31 downto 0);
dev_fail : in std_logic;
-- the Command Status Wrapper, 13 bytes IN
csw_valid : out std_logic;
csw_tag : out std_logic_vector(31 downto 0);
csw_residue : out std_logic_vector(31 downto 0);
csw_status : out std_logic_vector(1 downto 0);
stall_in : out std_logic;
stall_out : out std_logic;
need_reset : out std_logic;
state : out std_logic_vector(2 downto 0);
n_cbw : out std_logic_vector(31 downto 0);
n_pass : out std_logic_vector(31 downto 0);
n_fail : out std_logic_vector(31 downto 0);
n_phase : out std_logic_vector(31 downto 0);
n_stall : out std_logic_vector(31 downto 0);
n_invalid : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of msc_bot_fsm is
constant ST_PASS : std_logic_vector(1 downto 0) := "00";
constant ST_FAIL : std_logic_vector(1 downto 0) := "01";
constant ST_PHASE : std_logic_vector(1 downto 0) := "10";
type bot_state_t is (S_IDLE, S_DATA, S_CSW, S_WEDGE);
-- Exported as a 3-bit code so all three languages present one identical
-- observable contract to their benches.
function st_code (s : bot_state_t) return std_logic_vector is
begin
case s is
when S_IDLE => return "000";
when S_DATA => return "001";
when S_CSW => return "010";
when S_WEDGE => return "011";
end case;
end function;
signal st : bot_state_t := S_IDLE;
-- the host's stated expectation, as the spec's three kinds
signal h_none, h_in, h_out : std_logic;
-- the device's intent
signal d_none, d_in, d_out : std_logic;
signal d_len : unsigned(31 downto 0);
signal dir_clash, host_short, phase_err : std_logic;
signal moved, residue : unsigned(31 downto 0);
signal short_in, short_out : std_logic;
signal cbw_ok : std_logic;
signal tag_r : std_logic_vector(31 downto 0) := (others => '0');
signal res_r : unsigned(31 downto 0) := (others => '0');
signal sts_r : std_logic_vector(1 downto 0) := ST_PASS;
signal csw_r, sin_r, sout_r, wedge_r : std_logic := '0';
signal cbw_c, pass_c, fail_c, phase_c, stall_c, inval_c
: unsigned(31 downto 0) := (others => '0');
-- How many stalls this CBW raises, computed combinationally so the
-- counter takes one write rather than one per direction.
signal n_stall_now : unsigned(31 downto 0);
begin
h_none <= '1' when unsigned(cbw_len) = 0 else '0';
h_in <= '1' when (h_none = '0' and cbw_dir_in = '1') else '0';
h_out <= '1' when (h_none = '0' and cbw_dir_in = '0') else '0';
d_none <= dev_none;
d_in <= '1' when (d_none = '0' and dev_dir_in = '1') else '0';
d_out <= '1' when (d_none = '0' and dev_dir_in = '0') else '0';
-- dev_len is read only through d_in / d_out below, both false when the
-- command moves no data, so no zeroing guard is needed here.
d_len <= unsigned(dev_len);
-- -------------------------------------------------------------------
-- THE THIRTEEN CASES. Direction disagreement is always fatal (8, 13);
-- so is the host under-allocating (2, 3, 7, 12) -- there is no way to
-- move more bytes than the host set aside and no way to say so except
-- by declaring the phase lost. Everything else is survivable and the
-- residue carries the shortfall.
-- -------------------------------------------------------------------
dir_clash <= '1' when ((h_in = '1' and d_out = '1') or
(h_out = '1' and d_in = '1')) else '0'; -- 8, 13
host_short <= '1' when ((h_none = '1' and d_none = '0') or -- 2, 3
(h_in = '1' and d_in = '1' and unsigned(cbw_len) < d_len) or -- 7
(h_out = '1' and d_out = '1' and unsigned(cbw_len) < d_len)) -- 12
else '0';
phase_err <= dir_clash or host_short;
-- No clamp to cbw_len, and no phase-error guard. Every case where the
-- device wants more than the host allocated is ALREADY a phase error,
-- and both consumers below exclude the phase-error case themselves.
-- Both guards were present in the first version of this design and both
-- were found to be unreachable by mutations that scored exactly zero.
moved <= (others => '0') when d_none = '1' else d_len;
residue <= unsigned(cbw_len) when phase_err = '1'
else unsigned(cbw_len) - moved;
-- Cases 4 and 9: the host allocated a data phase the device cannot
-- fill. The transport must terminate it rather than leave the host
-- waiting, and a STALL is how BOT says so.
short_in <= '1' when (h_in = '1' and phase_err = '0' and moved < unsigned(cbw_len))
else '0';
short_out <= '1' when (h_out = '1' and phase_err = '0' and moved < unsigned(cbw_len))
else '0';
cbw_ok <= cbw_sig_ok and cbw_cb_ok;
stalls : process (all)
variable c : unsigned(31 downto 0);
begin
c := (others => '0');
if phase_err = '1' then
if h_in = '1' or h_out = '1' then c := to_unsigned(1, 32); end if;
else
if short_in = '1' then c := c + 1; end if;
if short_out = '1' then c := c + 1; end if;
end if;
n_stall_now <= c;
end process;
state <= st_code(st);
csw_valid <= csw_r;
csw_tag <= tag_r;
csw_residue <= std_logic_vector(res_r);
csw_status <= sts_r;
stall_in <= sin_r;
stall_out <= sout_r;
need_reset <= wedge_r;
n_cbw <= std_logic_vector(cbw_c);
n_pass <= std_logic_vector(pass_c);
n_fail <= std_logic_vector(fail_c);
n_phase <= std_logic_vector(phase_c);
n_stall <= std_logic_vector(stall_c);
n_invalid <= std_logic_vector(inval_c);
process (clk, rst_n)
begin
if rst_n = '0' then
st <= S_IDLE;
tag_r <= (others => '0');
res_r <= (others => '0');
sts_r <= ST_PASS;
csw_r <= '0';
sin_r <= '0';
sout_r <= '0';
wedge_r <= '0';
cbw_c <= (others => '0');
pass_c <= (others => '0');
fail_c <= (others => '0');
phase_c <= (others => '0');
stall_c <= (others => '0');
inval_c <= (others => '0');
elsif rising_edge(clk) then
csw_r <= '0';
sin_r <= '0';
sout_r <= '0';
case st is
when S_IDLE =>
if cbw_valid = '1' then
cbw_c <= cbw_c + 1;
if cbw_ok = '0' then
-- Not a CBW. Stall both ways and wedge until Reset Recovery;
-- answering anything else would invent a reply to a command
-- that was never received.
st <= S_WEDGE;
wedge_r <= '1';
sin_r <= '1';
sout_r <= '1';
inval_c <= inval_c + 1;
else
-- THE TAG IS CAPTURED HERE and echoed unchanged. A device
-- that regenerates it, or echoes the previous one, breaks
-- every host with more than one command in flight.
tag_r <= cbw_tag;
res_r <= residue;
if phase_err = '1' then
sts_r <= ST_PHASE;
phase_c <= phase_c + 1;
if h_in = '1' then sin_r <= '1'; end if;
if h_out = '1' then sout_r <= '1'; end if;
stall_c <= stall_c + n_stall_now;
st <= S_CSW;
else
if dev_fail = '1' then
sts_r <= ST_FAIL;
fail_c <= fail_c + 1;
else
sts_r <= ST_PASS;
pass_c <= pass_c + 1;
end if;
if short_in = '1' then sin_r <= '1'; end if;
if short_out = '1' then sout_r <= '1'; end if;
stall_c <= stall_c + n_stall_now;
if h_none = '1' and d_none = '1' then
st <= S_CSW;
else
st <= S_DATA;
end if;
end if;
end if;
end if;
-- The data phase is not modelled byte by byte; what the transport
-- owes the host is the LENGTH decision above, already fixed.
when S_DATA =>
st <= S_CSW;
when S_CSW =>
csw_r <= '1';
st <= S_IDLE;
-- Wedged. Only a reset gets out, which is what Reset Recovery
-- means: the host clears both stalls and starts again.
when S_WEDGE =>
sin_r <= '1';
sout_r <= '1';
end case;
end if;
end process;
end architecture;The VHDL testbench
-- =====================================================================
-- Testbench for msc_bot_fsm -- VHDL-2008.
--
-- THE MODEL IS THE SPEC'S TABLE, NOT THE DESIGN'S BOOLEANS.
--
-- The design decides with two expressions -- dir_clash and host_short --
-- and derives everything from them. The model does the opposite: it
-- classifies into one of the thirteen NAMED cases the Bulk-Only
-- Transport specification enumerates, then looks the answer up in a
-- table written straight from that document. Two derivations of one
-- contract.
--
-- 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 and any disagreement is a real
-- finding. The random phase uses a VHDL-native generator.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
entity tb_bt_vhdl is
generic (
DIRECTED_ONLY : boolean := false
);
end entity;
architecture sim of tb_bt_vhdl is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal cbw_valid : std_logic := '0';
signal cbw_tag : std_logic_vector(31 downto 0) := (others => '0');
signal cbw_len : std_logic_vector(31 downto 0) := (others => '0');
signal cbw_dir_in : std_logic := '0';
signal cbw_sig_ok : std_logic := '1';
signal cbw_cb_ok : std_logic := '1';
signal dev_none : std_logic := '0';
signal dev_dir_in : std_logic := '0';
signal dev_len : std_logic_vector(31 downto 0) := (others => '0');
signal dev_fail : std_logic := '0';
signal csw_valid : std_logic;
signal csw_tag : std_logic_vector(31 downto 0);
signal csw_residue : std_logic_vector(31 downto 0);
signal csw_status : std_logic_vector(1 downto 0);
signal stall_in : std_logic;
signal stall_out : std_logic;
signal need_reset : std_logic;
signal state : std_logic_vector(2 downto 0);
signal n_cbw, n_pass, n_fail, n_phase, n_stall, n_invalid
: std_logic_vector(31 downto 0);
constant ST_PASS : std_logic_vector(1 downto 0) := "00";
constant ST_FAIL : std_logic_vector(1 downto 0) := "01";
constant ST_PHASE : std_logic_vector(1 downto 0) := "10";
begin
dut : entity work.msc_bot_fsm
port map (
clk => clk, rst_n => rst_n,
cbw_valid => cbw_valid, cbw_tag => cbw_tag, cbw_len => cbw_len,
cbw_dir_in => cbw_dir_in, cbw_sig_ok => cbw_sig_ok, cbw_cb_ok => cbw_cb_ok,
dev_none => dev_none, dev_dir_in => dev_dir_in, dev_len => dev_len,
dev_fail => dev_fail,
csw_valid => csw_valid, csw_tag => csw_tag, csw_residue => csw_residue,
csw_status => csw_status,
stall_in => stall_in, stall_out => stall_out, need_reset => need_reset,
state => state,
n_cbw => n_cbw, n_pass => n_pass, n_fail => n_fail, n_phase => n_phase,
n_stall => n_stall, n_invalid => n_invalid
);
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;
-- cumulative across resets: every reset_dut zeroes the DUT's counters
variable c_cbw, c_pass, c_fail, c_phase, c_stall, c_invalid : integer := 0;
-- what the design said, sampled at a DEFINED instant
variable obs_sin, obs_sout, obs_wedge, obs_csw : boolean := false;
variable obs_tag : std_logic_vector(31 downto 0) := (others => '0');
variable obs_res : unsigned(31 downto 0) := (others => '0');
variable obs_sts : std_logic_vector(1 downto 0) := "00";
type hits_t is array (1 to 13) of integer;
variable case_hits : hits_t := (others => 0);
type reach_t is array (0 to 107) of boolean;
variable reach : reach_t := (others => false);
variable nr : integer := 0;
type len3_t is array (0 to 2) of integer;
constant HLEN : len3_t := (0, 8, 16);
constant DLEN : len3_t := (0, 8, 16);
variable rnd_state : unsigned(31 downto 0) := x"000F0701";
impure function urand return integer is
begin
rnd_state := resize(rnd_state * to_unsigned(1103515245, 32), 32)
+ to_unsigned(12345, 32);
-- The HIGH bits. In an LCG with a power-of-two modulus bit i has
-- period 2**(i+1), so `urand mod 4` off the low bits cycles
-- 3,0,1,2,... in lockstep while its histogram stays perfectly
-- uniform -- a defect that once emptied a whole random phase.
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;
-- -----------------------------------------------------------------
-- THE MODEL: classify into the spec's thirteen cases.
-- h: 0 = Hn, 1 = Hi, 2 = Ho d: 0 = Dn, 1 = Di, 2 = Do
-- -----------------------------------------------------------------
function spec_case (h, d, hl, dl : integer) return integer is
begin
if h = 0 and d = 0 then return 1;
elsif h = 0 and d = 1 then return 2;
elsif h = 0 and d = 2 then return 3;
elsif h = 1 and d = 0 then return 4;
elsif h = 1 and d = 1 and hl > dl then return 5;
elsif h = 1 and d = 1 and hl = dl then return 6;
elsif h = 1 and d = 1 and hl < dl then return 7;
elsif h = 1 and d = 2 then return 8;
elsif h = 2 and d = 0 then return 9;
elsif h = 2 and d = 2 and hl > dl then return 10;
elsif h = 2 and d = 2 and hl = dl then return 11;
elsif h = 2 and d = 2 and hl < dl then return 12;
else return 13;
end if;
end function;
-- Which cases are a phase error, straight from the specification.
-- Written as set membership so it cannot share an algebraic mistake
-- with the design.
function is_phase (c : integer) return boolean is
begin
return c = 2 or c = 3 or c = 7 or c = 8 or c = 12 or c = 13;
end function;
procedure reset_dut is
begin
rst_n <= '0';
cbw_valid <= '0';
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
end procedure;
-- -----------------------------------------------------------------
-- Drive one CBW and check everything the transport owes the host.
-- -----------------------------------------------------------------
procedure run_cbw (tag : std_logic_vector(31 downto 0);
hlen : integer; hdir : boolean;
dnone : boolean; ddir : boolean; dlen : integer;
dfail : boolean; sig_ok : boolean; cb_ok : boolean) is
variable h, d, c : integer;
variable e_phase, e_sin, e_sout : boolean;
variable e_moved, e_res : integer;
variable e_sts : std_logic_vector(1 downto 0);
variable p0, f0, ph0, i0 : integer;
begin
if hlen = 0 then h := 0; elsif hdir then h := 1; else h := 2; end if;
if dnone then d := 0; elsif ddir then d := 1; else d := 2; end if;
if dnone then c := spec_case(h, d, hlen, 0);
else c := spec_case(h, d, hlen, dlen); end if;
e_phase := is_phase(c);
if e_phase then e_moved := 0;
elsif d = 0 then e_moved := 0;
elsif dlen > hlen then e_moved := hlen;
else e_moved := dlen; end if;
if e_phase then e_res := hlen; else e_res := hlen - e_moved; end if;
if e_phase then e_sts := ST_PHASE;
elsif dfail then e_sts := ST_FAIL;
else e_sts := ST_PASS; end if;
if e_phase then e_sin := (h = 1); else e_sin := (h = 1) and (e_moved < hlen); end if;
if e_phase then e_sout := (h = 2); else e_sout := (h = 2) and (e_moved < hlen); end if;
p0 := to_integer(unsigned(n_pass));
f0 := to_integer(unsigned(n_fail));
ph0 := to_integer(unsigned(n_phase));
i0 := to_integer(unsigned(n_invalid));
cbw_valid <= '1';
cbw_tag <= tag;
cbw_len <= std_logic_vector(to_unsigned(hlen, 32));
if hdir then cbw_dir_in <= '1'; else cbw_dir_in <= '0'; end if;
if sig_ok then cbw_sig_ok <= '1'; else cbw_sig_ok <= '0'; end if;
if cb_ok then cbw_cb_ok <= '1'; else cbw_cb_ok <= '0'; end if;
if dnone then dev_none <= '1'; else dev_none <= '0'; end if;
if ddir then dev_dir_in <= '1'; else dev_dir_in <= '0'; end if;
dev_len <= std_logic_vector(to_unsigned(dlen, 32));
if dfail then dev_fail <= '1'; else dev_fail <= '0'; end if;
wait until rising_edge(clk);
wait for 1 ns;
-- captured at the instant the CBW is consumed
obs_sin := (stall_in = '1');
obs_sout := (stall_out = '1');
obs_wedge := (need_reset = '1');
cbw_valid <= '0';
if not (sig_ok and cb_ok) then
-- ---- PROPERTY 1: an invalid CBW is not answered ----
ck(obs_sin and obs_sout, "an invalid CBW did not stall both endpoints");
ck(obs_wedge, "an invalid CBW did not request reset recovery");
ck(to_integer(unsigned(n_invalid)) = i0 + 1, "an invalid CBW was not counted");
c_cbw := c_cbw + 1; c_invalid := c_invalid + 1;
obs_csw := false;
for g in 0 to 7 loop
wait until rising_edge(clk);
wait for 1 ns;
if csw_valid = '1' then obs_csw := true; end if;
end loop;
ck(not obs_csw, "an invalid CBW was answered with a CSW");
ck(to_integer(unsigned(n_pass)) = p0 and
to_integer(unsigned(n_fail)) = f0 and
to_integer(unsigned(n_phase)) = ph0,
"an invalid CBW moved a status counter");
rst_n <= '0';
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
else
case_hits(c) := case_hits(c) + 1;
c_cbw := c_cbw + 1;
if e_sts = ST_PHASE then c_phase := c_phase + 1;
elsif e_sts = ST_FAIL then c_fail := c_fail + 1;
else c_pass := c_pass + 1; end if;
if e_sin then c_stall := c_stall + 1; end if;
if e_sout then c_stall := c_stall + 1; end if;
-- ---- PROPERTY 2a: the invariants the design RELIES on ----
--
-- The design carries no clamp and no phase-error guard on `moved`,
-- because two invariants make both unnecessary. Relying on an
-- invariant is fine; relying on one nobody checks is not.
if (not e_phase) and d /= 0 then
ck(dlen <= hlen,
"INVARIANT: a non-phase-error transfer wants more than the host allocated");
end if;
if dnone then
ck(e_moved = 0,
"INVARIANT: a no-data command moved a non-zero number of bytes");
end if;
-- ---- PROPERTY 2: the stall decision matches the case ----
ck(obs_sin = e_sin, "stall_in disagrees with the spec case");
ck(obs_sout = e_sout, "stall_out disagrees with the spec case");
ck(not obs_wedge, "a valid CBW requested reset recovery");
obs_csw := false;
for g in 0 to 7 loop
if not obs_csw then
wait until rising_edge(clk);
wait for 1 ns;
if csw_valid = '1' then
obs_csw := true;
obs_tag := csw_tag;
obs_res := unsigned(csw_residue);
obs_sts := csw_status;
end if;
end if;
end loop;
-- ---- PROPERTY 3: exactly one CSW per valid CBW ----
ck(obs_csw, "a valid CBW produced no CSW");
if obs_csw then
-- ---- PROPERTY 4: THE TAG IS ECHOED UNCHANGED ----
ck(obs_tag = tag, "dCSWTag does not echo dCBWTag");
-- ---- PROPERTY 5: the status matches the case ----
ck(obs_sts = e_sts, "csw_status disagrees with the spec case");
-- ---- PROPERTY 6: the residue is expected minus actual ----
--
-- Compared AS UNSIGNED, never via to_integer. A mutant that
-- breaks the phase-error detection lets the residue subtraction
-- underflow to nearly 2**32, and converting that to VHDL's
-- 32-bit signed INTEGER is a FATAL RUNTIME ERROR -- the mutant
-- aborts the simulation instead of failing it, and the matrix
-- reports a crash where it should report a score.
--
-- A mutant must fail, not abort. Comparing in the DUT's own type
-- is what guarantees that.
ck(obs_res = to_unsigned(e_res, 32),
"dCSWDataResidue disagrees with the spec case");
-- ---- PROPERTY 7: a residue never exceeds the expectation ----
ck(obs_res <= to_unsigned(hlen, 32),
"the residue exceeds what the host asked for");
end if;
-- ---- PROPERTY 8: exactly one status counter moved ----
if e_sts = ST_PHASE then
ck(to_integer(unsigned(n_phase)) = ph0 + 1 and
to_integer(unsigned(n_pass)) = p0 and
to_integer(unsigned(n_fail)) = f0, "counters wrong for a phase error");
elsif e_sts = ST_FAIL then
ck(to_integer(unsigned(n_fail)) = f0 + 1 and
to_integer(unsigned(n_pass)) = p0 and
to_integer(unsigned(n_phase)) = ph0, "counters wrong for a failed command");
else
ck(to_integer(unsigned(n_pass)) = p0 + 1 and
to_integer(unsigned(n_fail)) = f0 and
to_integer(unsigned(n_phase)) = ph0, "counters wrong for a passing command");
end if;
ck(state = "000", "the transport did not return to IDLE after a CSW");
steps := steps + 1;
end if;
end procedure;
variable idx : integer;
variable tv : std_logic_vector(31 downto 0);
begin
reset_dut;
-- ===============================================================
-- PHASE 1 (DIRECTED, EXHAUSTIVE) -- the whole decision space.
-- hlen(3) x hdir(2) x dkind(3) x dlen(3) x dfail(2) = 108.
-- ===============================================================
for hix in 0 to 2 loop
for hd in 0 to 1 loop
for dk in 0 to 2 loop
for dlx in 0 to 2 loop
for df in 0 to 1 loop
run_cbw(std_logic_vector(unsigned'(x"A5A50000") + to_unsigned(steps, 32)),
HLEN(hix), hd = 1, dk = 0, dk = 1, DLEN(dlx), df = 1,
true, true);
idx := ((((hix * 2 + hd) * 3 + dk) * 3 + dlx) * 2 + df);
reach(idx) := true;
end loop;
end loop;
end loop;
end loop;
end loop;
-- ===============================================================
-- PHASE 2 (DIRECTED) -- every one of the thirteen cases, by name.
-- ===============================================================
reset_dut;
run_cbw(x"00000001", 0, false, true, false, 0, false, true, true); -- 1 Hn = Dn
run_cbw(x"00000002", 0, false, false, true, 8, false, true, true); -- 2 Hn < Di
run_cbw(x"00000003", 0, false, false, false, 8, false, true, true); -- 3 Hn < Do
run_cbw(x"00000004", 16, true, true, false, 0, false, true, true); -- 4 Hi > Dn
run_cbw(x"00000005", 16, true, false, true, 8, false, true, true); -- 5 Hi > Di
run_cbw(x"00000006", 8, true, false, true, 8, false, true, true); -- 6 Hi = Di
run_cbw(x"00000007", 8, true, false, true, 16, false, true, true); -- 7 Hi < Di
run_cbw(x"00000008", 8, true, false, false, 8, false, true, true); -- 8 Hi <> Do
run_cbw(x"00000009", 16, false, true, false, 0, false, true, true); -- 9 Ho > Dn
run_cbw(x"0000000A", 16, false, false, false, 8, false, true, true); -- 10 Ho > Do
run_cbw(x"0000000B", 8, false, false, false, 8, false, true, true); -- 11 Ho = Do
run_cbw(x"0000000C", 8, false, false, false, 16, false, true, true); -- 12 Ho < Do
run_cbw(x"0000000D", 8, false, false, true, 8, false, true, true); -- 13 Ho <> Di
-- ===============================================================
-- PHASE 3 (DIRECTED) -- the tag, over many distinct values.
--
-- The tag is the one field with no arithmetic in it, which makes it
-- the easiest to get wrong in a way nothing else notices.
-- ===============================================================
reset_dut;
for k in 0 to 63 loop
case k mod 4 is
when 0 => tv := std_logic_vector(to_unsigned(k, 32));
when 1 => tv := std_logic_vector(unsigned'(x"FFFFFFFF") - to_unsigned(k, 32));
when 2 => tv := std_logic_vector(unsigned'(x"0000FFFF") +
shift_left(to_unsigned(k, 32), 16));
when others =>
tv := std_logic_vector(to_unsigned(k mod 256, 8)) &
std_logic_vector(not to_unsigned(k mod 256, 8)) &
std_logic_vector(to_unsigned(k mod 256, 8)) &
std_logic_vector(not to_unsigned(k mod 256, 8));
end case;
run_cbw(tv, 8, true, false, true, 8, false, true, true);
ck(obs_tag = tv, "a distinct tag was not echoed exactly");
end loop;
-- ===============================================================
-- PHASE 4 (DIRECTED, EXHAUSTIVE) -- CBW validity.
--
-- Swept across every host length and direction as well, because the
-- wedge must happen REGARDLESS of what the rest of the CBW claimed.
-- 4 validity combinations x 3 lengths x 2 directions = 24, of which
-- 18 are invalid.
-- ===============================================================
reset_dut;
for k in 0 to 3 loop
for hix in 0 to 2 loop
for hd in 0 to 1 loop
run_cbw(std_logic_vector(unsigned'(x"DEAD0000") +
to_unsigned(k * 8 + hix * 2 + hd, 32)),
HLEN(hix), hd = 1, false, true, 8, false,
(k / 2) = 1, (k mod 2) = 1);
end loop;
end loop;
end loop;
-- ===============================================================
-- PHASE 5 (RANDOM)
-- ===============================================================
if not DIRECTED_ONLY then
reset_dut;
for k in 0 to 599 loop
run_cbw(std_logic_vector(to_unsigned(urand mod 65536, 32)),
(urand mod 3) * 8, (urand mod 2) = 1,
(urand mod 3) = 0, (urand mod 2) = 1, (urand mod 3) * 8,
(urand mod 4) = 0, true, true);
end loop;
end if;
nr := 0;
for i in 0 to 107 loop
if reach(i) then nr := nr + 1; end if;
end loop;
write(lo, string'("steps=") & integer'image(steps) &
string'(" checks=") & integer'image(checks) &
string'(" reach=") & integer'image(nr) &
string'("/108 errors=") & integer'image(errors));
writeline(output, lo);
write(lo, string'("[bot] cbws=") & integer'image(c_cbw) &
string'(" pass=") & integer'image(c_pass) &
string'(" fail=") & integer'image(c_fail) &
string'(" phase_error=") & integer'image(c_phase) &
string'(" stalls=") & integer'image(c_stall) &
string'(" invalid=") & integer'image(c_invalid));
writeline(output, lo);
write(lo, string'("--- the thirteen Bulk-Only Transport cases, times exercised ---"));
writeline(output, lo);
write(lo, string'(" case: 1 2 3 4 5 6 7 8 9 10 11 12 13"));
writeline(output, lo);
write(lo, string'(" hits:"));
for k in 1 to 13 loop
write(lo, case_hits(k), right, 5);
end loop;
writeline(output, lo);
for k in 1 to 13 loop
ck(case_hits(k) > 0, "a Bulk-Only Transport case was never exercised");
end loop;
if nr /= 108 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;9. Assertions
// ---------------------------------------------------------------------
// Properties for msc_bot_fsm.
//
// 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.
// ---------------------------------------------------------------------
module msc_bot_sva (
input logic clk,
input logic rst_n,
input logic cbw_valid,
input logic [31:0] cbw_tag,
input logic [31:0] cbw_len,
input logic cbw_sig_ok,
input logic cbw_cb_ok,
input logic csw_valid,
input logic [31:0] csw_tag,
input logic [31:0] csw_residue,
input logic [1:0] csw_status,
input logic stall_in,
input logic stall_out,
input logic need_reset,
input logic [2:0] state
);
localparam logic [1:0] ST_PASS = 2'd0, ST_FAIL = 2'd1, ST_PHASE = 2'd2;
localparam logic [2:0] S_IDLE = 3'd0, S_DATA = 3'd1,
S_CSW = 3'd2, S_WEDGE = 3'd3;
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. the CSW is a single-cycle pulse ----
// A status held for two cycles is counted twice by any consumer that
// simply samples the flag, and the host would see two completions for
// one command.
a_csw_pulse : assert property (csw_valid |=> !csw_valid);
// ---- 2. THE TAG IS ECHOED ----
//
// dCSWTag must equal the dCBWTag of the command being answered. This is
// the property with no arithmetic in it, which makes it the easiest to
// break in a way nothing else notices: every value is a plausible 32-bit
// number.
property p_tag_echo;
(cbw_valid && cbw_sig_ok && cbw_cb_ok)
|-> ##[1:4] (csw_valid && (csw_tag == $past(cbw_tag, 1)));
endproperty
// (The $past depth is written for the concrete pipeline; a formal tool
// would carry the tag in an auxiliary variable instead.)
// ---- 3. the residue never exceeds the expectation ----
//
// A residue larger than dCBWDataTransferLength is arithmetically
// impossible, and a host reading one interprets it as an enormous
// transfer. This is the property that catches an underflowed
// subtraction, which is exactly what a missed phase error produces.
a_residue_bound : assert property
(csw_valid |-> (csw_residue <= $past(cbw_len, 2)));
// ---- 4. the status is one of the three defined codes ----
// 2'b11 is reserved; emitting it would be a value the host has no
// interpretation for.
a_status_legal : assert property
(csw_valid |-> (csw_status inside {ST_PASS, ST_FAIL, ST_PHASE}));
// ---- 5. an invalid CBW is NEVER answered ----
//
// THE property of this design. A CSW after an unparseable CBW tells the
// host the device understood a command it never received, and the host
// believes it. The device must wedge instead and wait for Reset
// Recovery.
property p_invalid_wedges;
(cbw_valid && !(cbw_sig_ok && cbw_cb_ok))
|=> (need_reset && stall_in && stall_out && !csw_valid);
endproperty
a_invalid_wedges : assert property (p_invalid_wedges);
// ---- 6. the wedge is sticky until reset ----
// Reset Recovery means exactly that: nothing short of a reset clears it.
a_wedge_sticky : assert property
((state == S_WEDGE) |=> (state == S_WEDGE));
// ---- 7. a CSW only ever follows a CBW ----
// The transport never speaks unprompted.
a_csw_prompted : assert property
(csw_valid |-> $past(state) == S_CSW);
// ---- 8. both endpoints are stalled only when wedged ----
//
// A single command stalls at most ONE direction -- the one the host is
// waiting on. Stalling both is reserved for "this was not a CBW", and
// conflating the two would make Reset Recovery fire on ordinary short
// reads.
a_double_stall : assert property
((stall_in && stall_out) |-> need_reset);
// ---- COVER: the fatal cases are actually reached ----
// A suite of passing assertions over stimulus that never produces a
// phase error has tested the happy path and called it the protocol.
c_phase : cover property (csw_valid && (csw_status == ST_PHASE));
c_fail : cover property (csw_valid && (csw_status == ST_FAIL));
c_residue : cover property (csw_valid && (csw_residue != 0));
c_wedge : cover property (need_reset);
c_stall_i : cover property (stall_in && !need_reset);
c_stall_o : cover property (stall_out && !need_reset);
endmodule10. Where UVM Fits
// ---------------------------------------------------------------------
// UVM structure for Bulk-Only Transport.
//
// NOT SIMULATED IN THIS CHAPTER. Icarus cannot compile UVM -- it breaks
// on virtual method dispatch -- so every number comes from the procedural
// benches. This is the structure a production environment would use.
//
// THE DESIGN DECISION: the sequence item carries the HOST's claim and the
// DEVICE's intent as two independent fields, and the constraint block is
// written so they can DISAGREE. An environment whose stimulus derives one
// from the other can never reach cases 2, 3, 7, 8, 12 or 13 -- which is
// to say it can never reach any of the fatal ones.
// ---------------------------------------------------------------------
class bot_command extends uvm_sequence_item;
`uvm_object_utils(bot_command)
// what the host puts in the CBW
rand bit [31:0] tag;
rand int host_len;
rand bit host_dir_in;
// what the device turns out to want -- INDEPENDENT of the above
rand bit dev_none;
rand bit dev_dir_in;
rand int dev_len;
rand bit dev_fail;
// CBW validity, so the wedge path is reachable
rand bit sig_ok;
rand bit cb_ok;
constraint c_sane {
host_len inside {0, 8, 16, 512};
dev_len inside {0, 8, 16, 512};
}
// Malformed CBWs are rare but must not be impossible: the wedge is the
// one path with no recovery except a reset, and a regression that never
// reaches it has not tested the worst outcome the transport has.
constraint c_validity { sig_ok dist {1 := 95, 0 := 5};
cb_ok dist {1 := 95, 0 := 5}; }
// THE IMPORTANT ONE. Nothing ties dev_* to host_*, and that is
// deliberate. A tempting constraint like
// dev_len <= host_len;
// would make the environment well-behaved and delete six of the thirteen
// cases -- including both families of fatal disagreement.
constraint c_reach_the_fatal_cases {
// bias TOWARDS disagreement, because a real host rarely produces it
// and the rare cases are the ones that ship broken
dev_dir_in dist { host_dir_in := 70, (!host_dir_in) := 30 };
}
function new(string name = "bot_command");
super.new(name);
endfunction
endclass
// ---- the scoreboard IS the thirteen-case table ----
//
// Identical classification to the procedural benches, and for the same
// reason: it must not share a derivation with the design.
class bot_scoreboard extends uvm_scoreboard;
`uvm_component_utils(bot_scoreboard)
int unsigned case_hits[14];
int unsigned n_tag_mismatch, n_bad_residue;
function int classify(bot_command c);
int h = (c.host_len == 0) ? 0 : (c.host_dir_in ? 1 : 2);
int d = c.dev_none ? 0 : (c.dev_dir_in ? 1 : 2);
int dl = c.dev_none ? 0 : c.dev_len;
if (h == 0 && d == 0) return 1;
if (h == 0 && d == 1) return 2;
if (h == 0 && d == 2) return 3;
if (h == 1 && d == 0) return 4;
if (h == 1 && d == 1) return (c.host_len > dl) ? 5 : (c.host_len == dl) ? 6 : 7;
if (h == 1 && d == 2) return 8;
if (h == 2 && d == 0) return 9;
if (h == 2 && d == 2) return (c.host_len > dl) ? 10 : (c.host_len == dl) ? 11 : 12;
return 13;
endfunction
function void report_phase(uvm_phase phase);
// Every case must have been reached. A regression that missed one has
// not tested the transport, however many commands it ran.
for (int i = 1; i <= 13; i++)
if (case_hits[i] == 0)
`uvm_error("BOT", $sformatf("spec case %0d was never exercised", i))
if (n_tag_mismatch)
`uvm_error("BOT", $sformatf("%0d CSWs did not echo their CBW tag", n_tag_mismatch))
endfunction
endclass
// ---- coverage: the CASES, not the commands ----
class bot_coverage extends uvm_subscriber #(bot_command);
`uvm_component_utils(bot_coverage)
int spec_case;
covergroup cg with function sample(bot_command c, int sc);
// THE coverpoint. Line coverage on this design reaches 100% long
// before case 7 has ever happened, which is precisely the gap.
cp_case : coverpoint sc { bins b[] = {[1:13]}; }
cp_fail : coverpoint c.dev_fail;
cp_sig : coverpoint c.sig_ok;
// A failing command crossed with each case, because "the command
// failed" and "the phase was impossible" are different statuses and a
// device can confuse them.
x_fail : cross cp_case, cp_fail;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg = new();
endfunction
function void write(bot_command t);
cg.sample(t, spec_case);
endfunction
endclass11. Mutation Testing
Nine defects, injected one at a time into all three languages. Every replacement asserted; each mutation generated as its own file.
| # | the injected defect | V-all | V-dir | SV-all | SV-dir | VHDL-all | VHDL-dir |
|---|---|---|---|---|---|---|---|
| BASE | unmodified design | 0 | 0 | 0 | 0 | 0 | 0 |
| P1 | dCSWTag is regenerated, not echoed | 855 | 255 | 855 | 255 | 855 | 255 |
| P2 | residue is bytes moved, not bytes missing | 324 | 109 | 324 | 109 | 318 | 109 |
| P3 | direction disagreement not detected (8, 13) | 554 | 95 | 554 | 95 | 491 | 95 |
| P4 | host under-allocation not detected (2, 3, 7, 12) | 677 | 126 | 677 | 126 | 710 | 126 |
| P5 | a short data phase is never terminated | 250 | 41 | 250 | 41 | 234 | 41 |
| P6 | an invalid CBW is answered with a CSW | 54 | 54 | 54 | 54 | 54 | 54 |
| P7 | the direction bit is read with inverted polarity | 1805 | 540 | 1805 | 540 | 1776 | 540 |
| P8 | off-by-one: an exact-length transfer stalls | 110 | 75 | 110 | 75 | 116 | 75 |
| P9 | a phase error is reported as an ordinary failure | 349 | 62 | 349 | 62 | 359 | 62 |
Every mutation is killed, and every one by directed stimulus alone. The directed column is identical across all three languages at all nine rows.
Two mutations that refused to die, and what they were telling me
P7 and P8 are not the defects they started as. The originals scored exactly zero, twice in a row, and each time the answer was the same: the mutation was breaking code that could never execute.
| attempt | what it broke | score | what that meant |
|---|---|---|---|
| 1 | a clamp of moved to cbw_len | 0 | unreachable: d_len > cbw_len is already a phase error |
| 2 | a phase-error guard on moved | 0 | unreachable: both consumers of moved exclude phase errors themselves |
| 3 | the direction-bit polarity | 540 | a real defect |
P6's 54 came from widening a property, not from finding a bug
P6 — answering an invalid CBW — first scored 9. That was exactly its domain: the CBW-validity phase tested the four signature/length combinations once each, three of which are invalid, and each invalid one runs three checks.
Sweeping the same property across every host length and direction as well — because the wedge must happen regardless of what the rest of the CBW claimed — took it to 54. No bug was found. An uninformative number became one that means something.
Run totals
| steps | checks | reach | errors | |
|---|---|---|---|---|
| Verilog, full | 809 | 8,519 | 108 / 108 | 0 |
| Verilog, directed only | 209 | 2,206 | 108 / 108 | 0 |
| SystemVerilog, full | 809 | 8,519 | 108 / 108 | 0 |
| SystemVerilog, directed only | 209 | 2,206 | 108 / 108 | 0 |
| VHDL, full | 809 | 8,509 | 108 / 108 | 0 |
| VHDL, directed only | 209 | 2,206 | 108 / 108 | 0 |
The directed-only rows are identical across all three languages in every column, including the case-hit distribution. The full rows differ only in VHDL's check count, by 10, from its independent random stream.
12. What This Does Not Cover
No SCSI. The command layer is an input to this design, not part of it. READ(10), WRITE(10), INQUIRY, REQUEST SENSE and the rest are a separate and much larger subject; what the transport needs from them is three values — direction, length, pass/fail — and those are ports.
No media, no FIFO, no wear levelling. A real drive spends most of its silicon on flash management. None of it changes the transport.
The data phase is not modelled byte by byte. What the transport owes the host is the length decision, and that is fixed before the first byte moves. Modelling the bytes would add volume without adding a case.
No Get Max LUN or Bulk-Only Mass Storage Reset. Both are class-specific control requests, and both matter — the reset is half of Reset Recovery. They belong to the control endpoint, which is module 13's subject.
Reset Recovery is modelled as a reset. The real sequence is a class reset followed by clearing both endpoint halts, and a host that skips a step leaves the device wedged. The design's obligation — stay wedged until a reset — is what is checked here.
Lengths are 0, 8 and 16 bytes. The field is 32 bits. Those three values reach every structural case; the remaining four billion add points, not cases.
13. The Interview Answer
"How does a USB flash drive work?" is usually answered with NAND and wear levelling, which is the storage answer. The USB answer is better:
1. Name the loop. "Bulk-Only Transport: a 31-byte command wrapper out, an optional data phase, a 13-byte status wrapper in. Two bulk endpoints, and that is the whole protocol."
2. Name the hard part. "The host declares how many bytes it expects and in which direction before the device has parsed the command. Those two can disagree, and the spec enumerates thirteen cases of disagreement — six of them fatal, in two families: wrong direction, and the host allocating less than the device needs."
3. Name the field that does the work. "dCSWDataResidue — expected minus
actual. It is how a short read reports success rather than failure, and a
device that returns an error for a short read makes every end-of-data look like
a fault."
If there is time, the detail worth offering is the tag: dCSWTag must echo
dCBWTag, and a device that regenerates it works perfectly against a host with
one command in flight and corrupts completions against a host with two. It is
the field with no arithmetic in it, which is what makes it easy to break
silently.
14. What Carries Forward
This chapter's mechanism was a decision table with fatal cells — small, completely enumerable, and dangerous precisely because the dangerous cells are rare.
The next chapter changes that shape entirely. A webcam does not have a decision table; it has a firehose. Isochronous transfers have no retries at all, so there is no error to handle and no status to return — a packet that goes missing is simply gone, and the device's only job is to make sure the next frame is not damaged by it too.
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 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).
- 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.
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.
