USB · Module 31
“Bulk Transfers Are Always Fastest”
Usually true, which is what makes it dangerous. Bulk has the largest packets and no rate limit, and on a quiet bus it wins every benchmark. A frame-budget allocator shows whose property throughput actually is.
1. The Belief
"Bulk is the high-throughput transfer type. If you want speed, use bulk — it has the biggest packets and nothing throttles it. Isochronous is for audio and interrupt is for tiny status reads."
2. Why An Intelligent Engineer Believes It
Because on the bus this engineer is measuring, it is true. Every part of the reasoning is sound.
THE NUMBERS SUPPORT IT
bulk maximum packet, high speed 512 bytes
interrupt maximum packet, high speed 64 bytes typically
isochronous per (micro)frame capped at what was reserved
THE RULES SUPPORT IT
bulk has NO reserved bandwidth and NO interval limit, so nothing
stops the host from issuing bulk transactions back to back as
fast as the bus can carry them
THE MEASUREMENT SUPPORTS IT
benchmark a bulk endpoint on a bus with nothing else on it and
you will measure the highest number USB can produce. Mass
storage, network adaptors and every high-rate device use bulk,
and they are right to.
THE DOCUMENTATION SUPPORTS IT
"bulk: large, non-time-critical transfers" is in every summary
table ever printed, next to "isochronous: guaranteed bandwidth",
and the reader takes "guaranteed" to mean "limited".3. The Prediction It Makes
IF BULK WERE ALWAYS FASTEST, THEN:
1 bulk throughput would be a property of the DEVICE and its
packet size -- measurable once, valid thereafter
2 adding an unrelated device to the bus could not change a bulk
endpoint's rate very much
3 isochronous could never beat bulk, since it is capped per frame
4 a bulk transfer would have a worst-case latency you could
compute
5 "use bulk for speed" would be advice that needs no conditionsPrediction 1 is the one that costs money, because it is the one that makes it into a datasheet.
4. The Counterexample
One bus, one measurement, taken twice.
STEP 1 a high-speed bulk IN endpoint, alone on the bus.
Measure. Call it 100%.
STEP 2 plug in a USB audio interface or a webcam and START IT.
It claims isochronous bandwidth at configuration time --
an amount the host reserved before allowing the setting.
STEP 3 measure the bulk endpoint again, unchanged.
OBSERVED
bulk throughput drops, and it drops by roughly the bandwidth the
isochronous device reserved. Nothing is broken. Nothing NAKed
incorrectly. No error counter moved. The host simply has less
frame left to give away, and bulk is what it gives away LAST.
PUSH IT FURTHER
reserve enough periodic bandwidth -- the specification permits
periodic transfers up to a large fraction of the frame -- and
bulk approaches ZERO while remaining entirely correct.Prediction 2 fails immediately. Prediction 3 fails in the same measurement: the isochronous stream's rate did not move at all, because it was reserved. The "capped" transfer type held its number and the "unlimited" one collapsed.
Prediction 4 fails structurally, not experimentally: bulk has no worst-case latency. A guarantee is exactly the thing it does not have. Isochronous and interrupt have bounded service intervals; bulk has a promise of delivery with no promise of when.
THE FRAME, AND WHY THE ORDER IS THE WHOLE ANSWER
|<--------------------- one frame's budget --------------------->|
| |
| ISOCHRONOUS reserved | INTERRUPT reserved | ...what is left... |
|<---- claimed 1st ---->|<--- claimed 2nd -->|<-- BULK gets this -|
o the first two regions are sized at CONFIGURATION time, by the
host, when it agreed to the alternate setting
o the third region is whatever remains, and it is not negotiated,
not reserved and not guaranteed
o a device cannot enlarge the third region by asking, by being
fast, or by having 512-byte packets
o if the first two fill the frame, the third is empty and every
component is working correctly5. The Corrected Model
THROUGHPUT IS NOT A PROPERTY OF A TRANSFER TYPE.
It is the outcome of an ALLOCATION, and the host allocates.
WHAT EACH TYPE ACTUALLY BUYS
ISOCHRONOUS a reservation, and no retries. You get your bytes
every frame or the frame is lost -- there is no
second chance and no handshake. Bounded rate,
bounded latency, NO delivery guarantee.
INTERRUPT a reservation of a service INTERVAL. Small
payloads, bounded latency, retried on error.
Bounded latency, low throughput.
BULK no reservation, no interval, largest packets,
retried on error, delivery guaranteed EVENTUALLY.
Unbounded latency, highest throughput AVAILABLE.
CONTROL a reserved minimum share, because enumeration must
always be possible. Never for throughput.
SO THE CORRECT SENTENCE IS:
bulk has the highest throughput CEILING and the weakest
guarantee; isochronous has a fixed floor and no retries. Which
is "fastest" depends entirely on what else is on the bus, and on
whether you mean peak rate or worst case.6. The Hardware Contract
The allocator is where the belief becomes checkable, because allocation order is structural: you can read it off the RTL.
PURPOSE given one frame's capacity and this frame's demands,
divide the capacity between three classes of traffic
INPUTS clk, rst_n
frame_start one cycle: a new frame begins
iso_req [15:0] isochronous bytes wanted
intr_req [15:0] interrupt bytes wanted
bulk_req [15:0] bulk bytes offered
budget [15:0] the frame's total capacity
periodic_cap [15:0] the most of it periodic traffic
may claim
OUTPUTS iso_grant, intr_grant, bulk_grant [15:0]
periodic_over periodic asked beyond its
reservation
bulk_starved bulk asked and received nothing
n_frames, n_bulk_starved
tot_iso, tot_intr, tot_bulk [31:0]
AUTHORITATIVE STATE
the registered grants and the running totals. The
division itself is combinational and is sampled on
frame_start.
DERIVED THE ORDER, and it is the chapter:
per_limit = min(periodic_cap, budget)
iso_g = min(iso_req, per_limit)
per_left = per_limit - iso_g
intr_g = min(intr_req, per_left)
used = iso_g + intr_g
frm_left = budget > used ? budget - used : 0
bulk_g = min(bulk_req, frm_left)
RESET all grants and totals zero.
PRIORITY isochronous, then interrupt, then bulk. Bulk sees only
what the FRAME has left after the reservations -- note
which number frm_left subtracts from -- and that can
legitimately be zero.
WHY periodic_cap IS AN INPUT
the fraction of a frame periodic traffic may claim
differs between full-speed frames and high-speed
microframes. The chapter states both numbers; the
hardware takes one.
LATENCY one frame_start to one allocation, registered.
BOUNDARY periodic_cap > budget is CLAMPED, so grants never
exceed the frame -- see the assumption below, which was
found by a check rather than by inspection.
bulk_req > 0 with frm_left == 0 raises bulk_starved,
which is a REPORT and not an error.
periodic asking beyond its reservation raises
periodic_over, likewise an observation: a real host
would have refused the configuration.
ASSUMPTIONS periodic_cap is a fraction OF THE FRAME, so
periodic_cap <= budget. The module CLAMPS rather than
trusts, because a reservation larger than the frame
would otherwise be granted -- bytes the frame does not
contain.
OMISSIONS the transaction list, endpoint descriptors, retries,
split transactions, NAK handling, frame numbers,
microframes, packet overhead, bit stuffing, and the
fact that a real host schedules TRANSACTIONS rather
than bytes.
MISCONCEPTION DEMONSTRATED
"bulk is fastest, so serve it first" and
"a reservation is a suggestion"usb_frame_budget.v — the design, Verilog-2005
// =====================================================================
// usb_frame_budget -- where a frame's bytes actually go.
//
// CLASSIFICATION: simplified synthesisable teaching RTL / scheduling
// MODEL. It is NOT a host controller. There is no transaction list, no
// endpoint descriptors, no retry, no split transaction, no NAK
// handling and no notion of packets at all. One frame's worth of
// capacity is divided between three classes of traffic, once per
// frame_start, and that is the whole module.
//
// It exists because "which transfer type is fastest" is a question
// about ALLOCATION, and allocation is the one part of the answer that
// can be written down as arithmetic.
//
// THE ORDER, WHICH IS THE ARCHITECTURE
// ------------------------------------
// 1 periodic traffic (isochronous, then interrupt) is served
// first, up to a RESERVATION CAP
// 2 bulk receives whatever the frame has left
//
// Two consequences follow from those two lines, and they are the whole
// of chapter 31.4:
//
// on an EMPTY frame, bulk receives the entire budget and is
// indeed the fastest thing available
//
// on a LOADED frame, isochronous receives its reservation
// REGARDLESS of how much bulk wants, and bulk receives the
// remainder -- which can be nothing
//
// Neither class is "faster". One has a guarantee and the other has
// whatever is left, and which of those is better depends entirely on
// what you are measuring.
//
// SCOPE OF THE CAP: the reservation limit is an INPUT here rather than
// a constant, because the fraction of a frame that periodic traffic
// may claim differs between full-speed frames and high-speed
// microframes. The chapter states both; the hardware takes a number.
//
// ASSUMPTION, and it is CHECKED rather than assumed: periodic_cap is a
// fraction OF THE FRAME, so periodic_cap <= budget. The module clamps
// it rather than trusting it, because a reservation larger than the
// frame would otherwise be granted -- bytes the frame does not
// contain. See 31.4's account of how that clamp came to be written.
// =====================================================================
module usb_frame_budget (
input wire clk,
input wire rst_n,
// One cycle: a new frame begins and the budget is redivided.
input wire frame_start,
// How many bytes each class would like this frame.
input wire [15:0] iso_req,
input wire [15:0] intr_req,
input wire [15:0] bulk_req,
// The frame's total capacity, and the most of it periodic traffic
// may claim.
input wire [15:0] budget,
input wire [15:0] periodic_cap,
// What each class got.
output wire [15:0] iso_grant,
output wire [15:0] intr_grant,
output wire [15:0] bulk_grant,
// Periodic traffic asked for more than its reservation allows. The
// host would have refused the endpoint at configuration time; here it
// is an observable fact rather than an error.
output wire periodic_over,
// Bulk wanted bytes and received none. NOT an error: it is the
// defined behaviour of a class with no reservation, and it is the
// measurement the misconception cannot survive.
output wire bulk_starved,
output wire [15:0] n_frames,
output wire [15:0] n_bulk_starved,
output wire [31:0] tot_iso,
output wire [31:0] tot_intr,
output wire [31:0] tot_bulk
);
reg [15:0] iso_r, intr_r, bulk_r;
reg over_r;
reg [15:0] c_frames, c_starved;
reg [31:0] t_iso, t_intr, t_bulk;
// ---- the division, in the order the architecture requires --------
// The FRAME is the hard limit and the reservation is a sub-limit
// inside it. A reservation larger than the frame is a nonsensical
// configuration, and the clamp below is what stops it becoming a
// grant of bytes the frame does not contain. The intent check in the
// testbench -- "grants never exceed the budget" -- is what found
// this; the reference model computed the same over-grant and agreed.
wire [15:0] per_limit = (periodic_cap > budget) ? budget : periodic_cap;
// Isochronous first, capped by the reservation.
wire [15:0] iso_g = (iso_req > per_limit) ? per_limit : iso_req;
// Interrupt next, from whatever the reservation has left.
wire [15:0] per_left = per_limit - iso_g;
wire [15:0] intr_g = (intr_req > per_left) ? per_left : intr_req;
// Bulk last, from whatever the FRAME has left. Note which number
// this subtracts from: the frame budget, not the reservation.
wire [15:0] used = iso_g + intr_g;
wire [15:0] frm_left = (budget > used) ? (budget - used) : 16'd0;
wire [15:0] bulk_g = (bulk_req > frm_left) ? frm_left : bulk_req;
wire over = ((iso_req + intr_req) > per_limit);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
iso_r <= 16'd0; intr_r <= 16'd0; bulk_r <= 16'd0;
over_r <= 1'b0;
c_frames <= 16'd0; c_starved <= 16'd0;
t_iso <= 32'd0; t_intr <= 32'd0; t_bulk <= 32'd0;
end else if (frame_start) begin
iso_r <= iso_g;
intr_r <= intr_g;
bulk_r <= bulk_g;
over_r <= over;
c_frames <= c_frames + 16'd1;
t_iso <= t_iso + {16'd0, iso_g};
t_intr <= t_intr + {16'd0, intr_g};
t_bulk <= t_bulk + {16'd0, bulk_g};
if ((bulk_req != 16'd0) && (bulk_g == 16'd0))
c_starved <= c_starved + 16'd1;
end
end
assign iso_grant = iso_r;
assign intr_grant = intr_r;
assign bulk_grant = bulk_r;
assign periodic_over = over_r;
assign bulk_starved = (bulk_r == 16'd0);
assign n_frames = c_frames;
assign n_bulk_starved = c_starved;
assign tot_iso = t_iso;
assign tot_intr = t_intr;
assign tot_bulk = t_bulk;
endmoduleThe clamp, and how it was found
per_limit = min(periodic_cap, budget) was not in the first version of this
block, and the story is the reason this section exists.
BEFORE periodic_cap was used directly. With a cap larger than
the budget, iso_g + intr_g could exceed the frame.
THE DUT AND THE REFERENCE MODEL BOTH DID THIS, AND AGREED.
Thousands of checks. Zero mismatches. A clean report.
WHAT DISAGREED the intent check, which says, in its own terms and
without consulting the model:
the three grants must never SUM to more
than the budget
That one line is the only thing in the entire bench that knew the
design was wrong.This is exactly the failure 30.4 documented, where a set/clear expression was backwards in the design and in the model, and the two agreed for 39,108 checks under a comment saying what the design should do. Two artefacts written by the same person from the same sentence are one artefact. A reference model measures consistency; only an independently stated intent measures correctness.
7. The Testbench
The frame's capacity is 1,500 bytes and the reservation cap 1,350 — full speed at 90% — unless a phase changes them.
PHASE 1 INTENT -- two claims, stated in the architecture's terms
and consulting no model:
C1 AN EMPTY FRAME: bulk receives the whole 1,500 and
isochronous receives nothing. THE MISCONCEPTION IS CORRECT
HERE, and saying so is the point -- it is why the belief
survives contact with a benchmark.
C2 THE RELATIONAL CLAIM, run eight times. Take one periodic
load. Run the frame with bulk_req = 0, then run the SAME
periodic load with bulk_req = 9000, and require:
o iso_grant IDENTICAL in both runs
o bulk_grant STRICTLY LARGER in the second
The first half says bulk demand cannot move a reservation.
The second says the bench is actually varying something --
without it, a design that granted bulk nothing at all would
satisfy the first half perfectly.
and running throughout every phase, the unconditional check:
total grants never exceed the budget
PHASE 2 the load grid -- see the axes below
PHASE 3 SCENARIOS -- including a backlog that persists across
frames, which phase 2 structurally cannot containC2 is the counterexample from section 4 written as a test, and note what it asserts: not a number, but a relation. "Isochronous is unmoved" and "bulk moved" are both claims about a pair of runs, which is the only shape in which "throughput is not a property of the device" can be stated at all.
tb_usb_frame_budget.v — the testbench, Verilog-2005
// =====================================================================
// tb_usb_frame_budget -- Verilog-2005 testbench for usb_frame_budget.
//
// PHASE 1 is the intent phase. It does not compare against a model; it
// states the two architectural claims that together refute "bulk is
// fastest" and requires them:
//
// C1 on an EMPTY frame, bulk receives the entire budget
// C2 on a LOADED frame, the isochronous grant does NOT depend on
// how much bulk wants
//
// C2 is the one that matters and it is a RELATIONAL property: it
// compares two runs that differ only in bulk_req. No single-run check
// can express it, and no reference model that computes the grants the
// same way the design does can fail it independently.
//
// PHASES
// 1 INTENT C1 and C2, the second as a paired-run comparison
// 2 EXHAUSTIVE the load grid: 5 iso x 4 bulk x 2 caps
// 3 SCENARIO the named boundaries
// 4 RANDOM supplementary, audited
// =====================================================================
`timescale 1ns/1ps
module tb_usb_frame_budget;
reg clk = 1'b0;
reg rst_n;
reg frame_start;
reg [15:0] iso_req, intr_req, bulk_req, budget, periodic_cap;
wire [15:0] iso_grant, intr_grant, bulk_grant;
wire periodic_over, bulk_starved;
wire [15:0] n_frames, n_bulk_starved;
wire [31:0] tot_iso, tot_intr, tot_bulk;
usb_frame_budget dut (
.clk(clk), .rst_n(rst_n), .frame_start(frame_start),
.iso_req(iso_req), .intr_req(intr_req), .bulk_req(bulk_req),
.budget(budget), .periodic_cap(periodic_cap),
.iso_grant(iso_grant), .intr_grant(intr_grant), .bulk_grant(bulk_grant),
.periodic_over(periodic_over), .bulk_starved(bulk_starved),
.n_frames(n_frames), .n_bulk_starved(n_bulk_starved),
.tot_iso(tot_iso), .tot_intr(tot_intr), .tot_bulk(tot_bulk)
);
always #5 clk = ~clk;
// ---- the independent reference model ---------------------------
// Written as the RULES, in words, then arithmetic:
// R1 periodic traffic is served first, within its reservation
// R2 isochronous takes precedence inside the reservation
// R3 bulk receives what the FRAME has left, not what the
// reservation has left
reg [15:0] rm_iso, rm_intr, rm_bulk;
reg [15:0] rm_frames, rm_starved;
reg [31:0] rm_tiso, rm_tintr, rm_tbulk;
integer chk_dir, chk_rnd, err, in_random;
integer m_frames, m_empty, m_loaded, m_starved, m_over, m_bulk_full,
m_setupfail;
integer i, j, a, b, c;
task bump; begin
if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
end endtask
task ck;
input [255:0] what;
input [31:0] got;
input [31:0] exp;
begin
bump;
if (got !== exp) begin
err = err + 1;
if (!in_random && err <= 40)
$display(" ** %0s: got %0d expected %0d (t=%0t)", what, got, exp, $time);
end
end
endtask
function [15:0] mn;
input [15:0] x;
input [15:0] y;
begin mn = (x < y) ? x : y; end
endfunction
task ref_step;
reg [15:0] ig, il, tg, fl, bg, pl;
begin
if (!rst_n) begin
rm_iso = 0; rm_intr = 0; rm_bulk = 0;
rm_frames = 0; rm_starved = 0;
rm_tiso = 0; rm_tintr = 0; rm_tbulk = 0;
end else if (frame_start) begin
// R0 the frame is the hard limit; the reservation lives inside
// it. This line was absent from both the design and this
// model, and they agreed with each other about the result.
pl = mn(periodic_cap, budget);
ig = mn(iso_req, pl); // R2
il = pl - ig;
tg = mn(intr_req, il); // R1
fl = (budget > (ig + tg)) ? (budget - ig - tg) : 16'd0;
bg = mn(bulk_req, fl); // R3
rm_iso = ig; rm_intr = tg; rm_bulk = bg;
rm_frames = rm_frames + 1;
rm_tiso = rm_tiso + ig;
rm_tintr = rm_tintr + tg;
rm_tbulk = rm_tbulk + bg;
if (bulk_req != 0 && bg == 0) rm_starved = rm_starved + 1;
// tallies
m_frames = m_frames + 1;
if (iso_req == 0 && intr_req == 0) m_empty = m_empty + 1;
else m_loaded = m_loaded + 1;
if (bulk_req != 0 && bg == 0) m_starved = m_starved + 1;
if ((iso_req + intr_req) > pl) m_over = m_over + 1;
if (bulk_req != 0 && bg == bulk_req) m_bulk_full = m_bulk_full + 1;
end
end
endtask
task cmp; begin
ck("iso_grant", {16'd0, iso_grant}, {16'd0, rm_iso});
ck("intr_grant", {16'd0, intr_grant}, {16'd0, rm_intr});
ck("bulk_grant", {16'd0, bulk_grant}, {16'd0, rm_bulk});
ck("n_frames", {16'd0, n_frames}, {16'd0, rm_frames});
ck("n_starved", {16'd0, n_bulk_starved}, {16'd0, rm_starved});
ck("tot_iso", tot_iso, rm_tiso);
ck("tot_bulk", tot_bulk, rm_tbulk);
end endtask
// The claim, asserted directly: the three grants never exceed the
// frame, and periodic never exceeds its reservation.
task intent_check; begin
bump;
if ((iso_grant + intr_grant + bulk_grant) > budget) begin
err = err + 1;
$display(" ** INTENT VIOLATED: grants exceed the frame budget (t=%0t)", $time);
end
bump;
if ((iso_grant + intr_grant) > periodic_cap) begin
err = err + 1;
$display(" ** INTENT VIOLATED: periodic exceeded its reservation (t=%0t)", $time);
end
end endtask
task step; begin
#1;
@(posedge clk);
ref_step;
#1;
cmp;
intent_check;
frame_start = 0;
end endtask
task hard_reset; begin
rst_n = 0; frame_start = 0;
iso_req = 0; intr_req = 0; bulk_req = 0;
budget = 16'd1500; periodic_cap = 16'd1350;
repeat (3) begin @(posedge clk); ref_step; end
#1; rst_n = 1;
@(posedge clk); ref_step; #1; cmp;
end endtask
task frame;
input [15:0] iso;
input [15:0] intr;
input [15:0] blk;
begin
frame_start = 1; iso_req = iso; intr_req = intr; bulk_req = blk;
step;
end
endtask
// -----------------------------------------------------------------
// PHASE 1 -- THE TWO CLAIMS.
// -----------------------------------------------------------------
integer paired_runs;
reg [15:0] iso_a, iso_b, bulk_a, bulk_b;
task phase_intent;
begin
hard_reset;
paired_runs = 0;
// C1 an empty frame: bulk gets everything. The misconception is
// CORRECT here, and saying so is the point -- it is why the
// belief survives.
frame(16'd0, 16'd0, 16'd2000);
ck("C1 bulk got the whole frame", {16'd0, bulk_grant}, 32'd1500);
ck("C1 iso got nothing", {16'd0, iso_grant}, 32'd0);
// C2 the relational claim. Run the SAME periodic load twice,
// changing only what bulk asks for, and require the
// isochronous grant to be identical. If bulk could take
// priority by wanting more, this is where it would show.
for (i = 0; i < 8; i = i + 1) begin
hard_reset;
frame(16'd100 + i[15:0] * 16'd100, 16'd50, 16'd0);
iso_a = iso_grant; bulk_a = bulk_grant;
hard_reset;
frame(16'd100 + i[15:0] * 16'd100, 16'd50, 16'd9000);
iso_b = iso_grant; bulk_b = bulk_grant;
ck("C2 iso is unmoved by bulk demand", {16'd0, iso_a}, {16'd0, iso_b});
bump;
if (bulk_b <= bulk_a) begin
err = err + 1;
$display(" ** C2: bulk asking for more did not get more (%0d vs %0d)",
bulk_a, bulk_b);
end
paired_runs = paired_runs + 1;
end
bump;
if (paired_runs != 8) begin
err = err + 1;
$display(" ** intent: only %0d paired runs", paired_runs);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 2 -- the load grid.
//
// AXES:
// isochronous demand 0, 300, 700, 1200, 1600 bytes 5
// bulk demand 0, 200, 1500, 9000 bytes 4
// reservation cap 1350 (full-speed 90%), 1200 (80%) 2
// ---------------------------------------------------------------
// 5 x 4 x 2 = 40
//
// Interrupt demand is held at 50 rather than swept: within the
// reservation it behaves exactly as isochronous does, and sweeping
// it would multiply the grid without adding a case. That is a claim
// a reviewer can disagree with, which is why it is written here --
// 30.2 §6.
//
// WHAT IS ABSENT: time. Every frame here is independent. A backlog
// that persists across frames is phase 3.
// -----------------------------------------------------------------
reg [15:0] isos [0:4];
reg [15:0] blks [0:3];
reg [15:0] caps [0:1];
task phase_sweep;
begin
isos[0]=16'd0; isos[1]=16'd300; isos[2]=16'd700;
isos[3]=16'd1200; isos[4]=16'd1600;
blks[0]=16'd0; blks[1]=16'd200; blks[2]=16'd1500; blks[3]=16'd9000;
caps[0]=16'd1350; caps[1]=16'd1200;
for (a = 0; a < 5; a = a + 1)
for (b = 0; b < 4; b = b + 1)
for (c = 0; c < 2; c = c + 1) begin
hard_reset;
periodic_cap = caps[c];
bump;
if (periodic_cap !== caps[c]) begin
err = err + 1; m_setupfail = m_setupfail + 1;
$display(" ** setup: cap not applied");
end
frame(isos[a], 16'd50, blks[b]);
frame(16'd0, 16'd0, 16'd0);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 3 -- the named boundaries.
// -----------------------------------------------------------------
task phase_scenarios;
begin
// S1 periodic exactly at the reservation: bulk gets the rest.
hard_reset;
frame(16'd1300, 16'd50, 16'd9000);
ck("S1 iso got all it asked", {16'd0, iso_grant}, 32'd1300);
ck("S1 interrupt too", {16'd0, intr_grant}, 32'd50);
ck("S1 bulk got the remainder",{16'd0, bulk_grant}, 32'd150);
ck("S1 not flagged over", {31'd0, periodic_over}, 32'd0);
// S2 periodic one byte over: the reservation is a CAP, not a
// suggestion, and the excess is refused rather than taken
// from bulk.
hard_reset;
frame(16'd1301, 16'd50, 16'd9000);
ck("S2 flagged over", {31'd0, periodic_over}, 32'd1);
ck("S2 periodic capped", {16'd0, iso_grant + intr_grant}, 32'd1350);
ck("S2 bulk still gets 150", {16'd0, bulk_grant}, 32'd150);
// S3 THE COUNTEREXAMPLE. Periodic fills the reservation
// completely and the frame has nothing left. Bulk asks for
// nine thousand bytes and receives zero -- for as many frames
// as this lasts.
hard_reset;
periodic_cap = 16'd1500;
for (i = 0; i < 10; i = i + 1) frame(16'd1500, 16'd0, 16'd9000);
ck("S3 bulk got nothing, ten frames running", {16'd0, n_bulk_starved}, 32'd10);
ck("S3 while iso got everything", tot_iso, 32'd15000);
ck("S3 and bulk got zero bytes", tot_bulk, 32'd0);
// S4 the mirror: an idle periodic load, ten frames, bulk takes
// the lot. Same design, same parameters, opposite verdict.
hard_reset;
for (i = 0; i < 10; i = i + 1) frame(16'd0, 16'd0, 16'd9000);
ck("S4 bulk never starved", {16'd0, n_bulk_starved}, 32'd0);
ck("S4 and took every byte", tot_bulk, 32'd15000);
// S5 a tiny bulk request always fits when there is room, and
// never fits when there is not. "Fastest" is not a property
// of the request.
hard_reset;
frame(16'd0, 16'd0, 16'd1);
ck("S5 one byte, empty frame", {16'd0, bulk_grant}, 32'd1);
hard_reset;
periodic_cap = 16'd1500;
frame(16'd1500, 16'd0, 16'd1);
ck("S5 one byte, full frame", {16'd0, bulk_grant}, 32'd0);
end
endtask
// -----------------------------------------------------------------
// PHASE 4 -- random, audited.
// -----------------------------------------------------------------
task phase_random;
integer r;
begin
in_random = 1;
hard_reset;
for (j = 0; j < 3000; j = j + 1) begin
r = {$random} % 100;
if (r < 20) periodic_cap = ({$random} % 1600);
if (r < 30) budget = 16'd1000 + ({$random} % 800);
frame(({$random} % 1700), ({$random} % 300), ({$random} % 4000));
end
in_random = 0;
end
endtask
initial begin
chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
m_frames=0; m_empty=0; m_loaded=0; m_starved=0; m_over=0;
m_bulk_full=0; m_setupfail=0;
phase_intent;
$display(" phase 1 intent : %0d checks, %0d errors (%0d paired runs)",
chk_dir, err, paired_runs);
phase_sweep;
$display(" phase 2 exhaustive : %0d checks, %0d errors (40 combinations)", chk_dir, err);
phase_scenarios;
$display(" phase 3 scenarios : %0d checks, %0d errors", chk_dir, err);
$display(" ---- DIRECTED-ONLY : %0d checks, %0d errors ----", chk_dir, err);
phase_random;
$display("");
$display(" measured reachability (all phases)");
$display(" frames scheduled ....... %0d", m_frames);
$display(" with no periodic ..... %0d", m_empty);
$display(" with periodic load ... %0d", m_loaded);
$display(" frames starving bulk ... %0d", m_starved);
$display(" bulk fully satisfied ... %0d", m_bulk_full);
$display(" periodic over its cap .. %0d", m_over);
$display(" setup failures ......... %0d", m_setupfail);
$display("");
$display(" directed checks ........ %0d", chk_dir);
$display(" random checks .......... %0d", chk_rnd);
$display(" TOTAL checks ........... %0d", chk_dir + chk_rnd);
$display(" ERRORS ................. %0d", err);
if (err == 0) $display(" PASS"); else $display(" FAIL");
$finish;
end
endmodule VERILOG SYSTEMVERILOG VHDL-2008
phase 1 intent 291 291 291
phase 2 load grid 1,331 1,331 1,331
phase 3 scenarios 1,603 1,603 1,603
---- DIRECTED 1,603 1,603 1,603
errors 0 0 0
TOTAL 28,610 28,610 28,610
measured reachability, Verilog run
frames scheduled .................... 3,121
with no periodic demand ............... 52
with periodic load ................. 3,069
frames in which bulk was STARVED ....... 191
frames in which bulk was fully satisfied 587
periodic demand over its reservation . 1,880
paired intent runs ....................... 8Three rows carry the argument.
52 EMPTY FRAMES vs 3,069 LOADED ONES
On the 52, bulk takes the entire budget and the belief is right.
On the 3,069 it is not, and the belief was formed on a bus that
looked like the 52.
191 STARVATIONS, 0 ERRORS
A state the belief calls a malfunction and the architecture calls
correct operation, reached 191 times with nothing wrong.
1,880 FRAMES WITH PERIODIC OVER ITS RESERVATION
The case the clamp exists for. Without periodic_over as an
OUTPUT this would be invisible; with it, "the configuration was
refusable" is an observation rather than an inference.The load grid, and its named axes
AXES
isochronous demand 0, 300, 700, 1200, 1600 bytes 5
bulk demand 0, 200, 1500, 9000 bytes 4
reservation cap 1350 (full-speed 90%), 1200 (80%) 2
---------------------------------------------------------------
5 x 4 x 2 = 40All 40 reached. Three of the axis values are chosen against the boundary rather than spread across a range, and that is the whole design of the grid:
iso 1200 at the 1200 cap and BELOW the 1350 cap
iso 1600 ABOVE both caps -- the over-demand case
bulk 9000 far larger than any frame, so bulk is always truncated
bulk 0 so "bulk got nothing" can be distinguished from
"bulk asked for nothing"8. SystemVerilog
usb_frame_budget_sv.sv — the design, SystemVerilog
// =====================================================================
// usb_frame_budget_sv -- the same contract in SystemVerilog. Same
// ports, same order of allocation, same reset, same latency.
//
// The SVA block at the bottom is why this version matters to the
// chapter: the claim that isochronous is unaffected by bulk demand
// is written as a property in which bulk demand does not appear.
//
// ------------------------------------------------------------------
// Where a frame's bytes actually go.
//
// CLASSIFICATION: simplified synthesisable teaching RTL / scheduling
// MODEL. It is NOT a host controller. There is no transaction list, no
// endpoint descriptors, no retry, no split transaction, no NAK
// handling and no notion of packets at all. One frame's worth of
// capacity is divided between three classes of traffic, once per
// frame_start, and that is the whole module.
//
// It exists because "which transfer type is fastest" is a question
// about ALLOCATION, and allocation is the one part of the answer that
// can be written down as arithmetic.
//
// THE ORDER, WHICH IS THE ARCHITECTURE
// ------------------------------------
// 1 periodic traffic (isochronous, then interrupt) is served
// first, up to a RESERVATION CAP
// 2 bulk receives whatever the frame has left
//
// Two consequences follow from those two lines, and they are the whole
// of chapter 31.4:
//
// on an EMPTY frame, bulk receives the entire budget and is
// indeed the fastest thing available
//
// on a LOADED frame, isochronous receives its reservation
// REGARDLESS of how much bulk wants, and bulk receives the
// remainder -- which can be nothing
//
// Neither class is "faster". One has a guarantee and the other has
// whatever is left, and which of those is better depends entirely on
// what you are measuring.
//
// SCOPE OF THE CAP: the reservation limit is an INPUT here rather than
// a constant, because the fraction of a frame that periodic traffic
// may claim differs between full-speed frames and high-speed
// microframes. The chapter states both; the hardware takes a number.
//
// ASSUMPTION, and it is CHECKED rather than assumed: periodic_cap is a
// fraction OF THE FRAME, so periodic_cap <= budget. The module clamps
// it rather than trusting it, because a reservation larger than the
// frame would otherwise be granted -- bytes the frame does not
// contain. See 31.4's account of how that clamp came to be written.
// =====================================================================
module usb_frame_budget_sv (
input logic clk,
input logic rst_n,
// One cycle: a new frame begins and the budget is redivided.
input logic frame_start,
// How many bytes each class would like this frame.
input logic [15:0] iso_req,
input logic [15:0] intr_req,
input logic [15:0] bulk_req,
// The frame's total capacity, and the most of it periodic traffic
// may claim.
input logic [15:0] budget,
input logic [15:0] periodic_cap,
// What each class got.
output logic [15:0] iso_grant,
output logic [15:0] intr_grant,
output logic [15:0] bulk_grant,
// Periodic traffic asked for more than its reservation allows. The
// host would have refused the endpoint at configuration time; here it
// is an observable fact rather than an error.
output logic periodic_over,
// Bulk wanted bytes and received none. NOT an error: it is the
// defined behaviour of a class with no reservation, and it is the
// measurement the misconception cannot survive.
output logic bulk_starved,
output logic [15:0] n_frames,
output logic [15:0] n_bulk_starved,
output logic [31:0] tot_iso,
output logic [31:0] tot_intr,
output logic [31:0] tot_bulk
);
logic [15:0] iso_r, intr_r, bulk_r;
logic over_r;
logic [15:0] c_frames, c_starved;
logic [31:0] t_iso, t_intr, t_bulk;
// ---- the division, in the order the architecture requires --------
// The FRAME is the hard limit and the reservation is a sub-limit
// inside it. A reservation larger than the frame is a nonsensical
// configuration, and the clamp below is what stops it becoming a
// grant of bytes the frame does not contain. The intent check in the
// testbench -- "grants never exceed the budget" -- is what found
// this; the reference model computed the same over-grant and agreed.
logic [15:0] per_limit;
assign per_limit = (periodic_cap > budget) ? budget : periodic_cap;
// Isochronous first, capped by the reservation.
logic [15:0] iso_g;
assign iso_g = (iso_req > per_limit) ? per_limit : iso_req;
// Interrupt next, from whatever the reservation has left.
logic [15:0] per_left;
assign per_left = per_limit - iso_g;
logic [15:0] intr_g;
assign intr_g = (intr_req > per_left) ? per_left : intr_req;
// Bulk last, from whatever the FRAME has left. Note which number
// this subtracts from: the frame budget, not the reservation.
logic [15:0] used;
assign used = iso_g + intr_g;
logic [15:0] frm_left;
assign frm_left = (budget > used) ? (budget - used) : 16'd0;
logic [15:0] bulk_g;
assign bulk_g = (bulk_req > frm_left) ? frm_left : bulk_req;
logic over;
assign over = ((iso_req + intr_req) > per_limit);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
iso_r <= 16'd0; intr_r <= 16'd0; bulk_r <= 16'd0;
over_r <= 1'b0;
c_frames <= 16'd0; c_starved <= 16'd0;
t_iso <= 32'd0; t_intr <= 32'd0; t_bulk <= 32'd0;
end else if (frame_start) begin
iso_r <= iso_g;
intr_r <= intr_g;
bulk_r <= bulk_g;
over_r <= over;
c_frames <= c_frames + 16'd1;
t_iso <= t_iso + {16'd0, iso_g};
t_intr <= t_intr + {16'd0, intr_g};
t_bulk <= t_bulk + {16'd0, bulk_g};
if ((bulk_req != 16'd0) && (bulk_g == 16'd0))
c_starved <= c_starved + 16'd1;
end
end
assign iso_grant = iso_r;
assign intr_grant = intr_r;
assign bulk_grant = bulk_r;
assign periodic_over = over_r;
assign bulk_starved = (bulk_r == 16'd0);
assign n_frames = c_frames;
assign n_bulk_starved = c_starved;
assign tot_iso = t_iso;
assign tot_intr = t_intr;
assign tot_bulk = t_bulk;
`ifdef SVA_ON
// The allocation rules as properties. Icarus rejects SVA; under
// Icarus the intent checks in the testbench enforce each of these.
// SAFETY. The frame is the hard limit. This is the property that the
// over-grant defect described in 31.4 violated, and that the reference
// model agreed with the design about.
property p_within_budget;
@(posedge clk) disable iff (!rst_n)
(iso_grant + intr_grant + bulk_grant) <= $past(budget);
endproperty
a_within_budget: assert property (p_within_budget);
// SAFETY. Periodic never exceeds its reservation.
property p_within_reservation;
@(posedge clk) disable iff (!rst_n)
(iso_grant + intr_grant) <= $past(periodic_cap);
endproperty
a_within_reservation: assert property (p_within_reservation);
// THE ONE THE CHAPTER IS ABOUT. Isochronous gets what it asked for
// whenever the reservation allows it -- whatever bulk wants. Bulk
// demand does not appear in the antecedent OR the consequent, which
// is the point.
property p_iso_is_independent_of_bulk;
@(posedge clk) disable iff (!rst_n)
frame_start && (iso_req <= periodic_cap) && (periodic_cap <= budget)
|=> (iso_grant == $past(iso_req));
endproperty
a_iso_is_independent_of_bulk: assert property (p_iso_is_independent_of_bulk);
// AND ITS MIRROR. On an empty frame bulk gets everything it asked
// for, up to the budget -- so the misconception is right, there.
property p_bulk_takes_an_empty_frame;
@(posedge clk) disable iff (!rst_n)
frame_start && (iso_req == '0) && (intr_req == '0) && (bulk_req >= budget)
|=> (bulk_grant == $past(budget));
endproperty
a_bulk_takes_an_empty_frame: assert property (p_bulk_takes_an_empty_frame);
c_empty: cover property (@(posedge clk) frame_start && iso_req == '0 && intr_req == '0);
c_starved: cover property (@(posedge clk) frame_start && bulk_req != '0 &&
(iso_req + intr_req) >= budget);
c_over: cover property (@(posedge clk) frame_start &&
(iso_req + intr_req) > periodic_cap);
c_exact: cover property (@(posedge clk) frame_start &&
(iso_req + intr_req) == periodic_cap);
`endif
endmoduletb_usb_frame_budget_sv.sv — the testbench, SystemVerilog
// =====================================================================
// tb_usb_frame_budget -- Verilog-2005 testbench for usb_frame_budget.
//
// PHASE 1 is the intent phase. It does not compare against a model; it
// states the two architectural claims that together refute "bulk is
// fastest" and requires them:
//
// C1 on an EMPTY frame, bulk receives the entire budget
// C2 on a LOADED frame, the isochronous grant does NOT depend on
// how much bulk wants
//
// C2 is the one that matters and it is a RELATIONAL property: it
// compares two runs that differ only in bulk_req. No single-run check
// can express it, and no reference model that computes the grants the
// same way the design does can fail it independently.
//
// PHASES
// 1 INTENT C1 and C2, the second as a paired-run comparison
// 2 EXHAUSTIVE the load grid: 5 iso x 4 bulk x 2 caps
// 3 SCENARIO the named boundaries
// 4 RANDOM supplementary, audited
// =====================================================================
`timescale 1ns/1ps
module tb_usb_frame_budget_sv;
logic clk = 1'b0;
logic rst_n;
logic frame_start;
logic [15:0] iso_req, intr_req, bulk_req, budget, periodic_cap;
wire [15:0] iso_grant, intr_grant, bulk_grant;
wire periodic_over, bulk_starved;
wire [15:0] n_frames, n_bulk_starved;
wire [31:0] tot_iso, tot_intr, tot_bulk;
usb_frame_budget_sv dut (
.clk(clk), .rst_n(rst_n), .frame_start(frame_start),
.iso_req(iso_req), .intr_req(intr_req), .bulk_req(bulk_req),
.budget(budget), .periodic_cap(periodic_cap),
.iso_grant(iso_grant), .intr_grant(intr_grant), .bulk_grant(bulk_grant),
.periodic_over(periodic_over), .bulk_starved(bulk_starved),
.n_frames(n_frames), .n_bulk_starved(n_bulk_starved),
.tot_iso(tot_iso), .tot_intr(tot_intr), .tot_bulk(tot_bulk)
);
always #5 clk = ~clk;
// ---- the independent reference model ---------------------------
// Written as the RULES, in words, then arithmetic:
// R1 periodic traffic is served first, within its reservation
// R2 isochronous takes precedence inside the reservation
// R3 bulk receives what the FRAME has left, not what the
// reservation has left
logic [15:0] rm_iso, rm_intr, rm_bulk;
logic [15:0] rm_frames, rm_starved;
logic [31:0] rm_tiso, rm_tintr, rm_tbulk;
int chk_dir, chk_rnd, err;
bit in_random;
int m_frames, m_empty, m_loaded, m_starved, m_over, m_bulk_full,
m_setupfail;
int i, j, a, b, c;
task bump; begin
if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
end endtask
task ck(string what, logic [31:0] got, logic [31:0] exp);
begin
bump;
if (got !== exp) begin
err = err + 1;
if (!in_random && err <= 40)
$display(" ** %s: got %0d expected %0d (t=%0t)", what, got, exp, $time);
end
end
endtask
function logic [15:0] mn(logic [15:0] x, logic [15:0] y);
return (x < y) ? x : y;
endfunction
task ref_step;
logic [15:0] ig, il, tg, fl, bg, pl;
begin
if (!rst_n) begin
rm_iso = 0; rm_intr = 0; rm_bulk = 0;
rm_frames = 0; rm_starved = 0;
rm_tiso = 0; rm_tintr = 0; rm_tbulk = 0;
end else if (frame_start) begin
// R0 the frame is the hard limit; the reservation lives inside
// it. This line was absent from both the design and this
// model, and they agreed with each other about the result.
pl = mn(periodic_cap, budget);
ig = mn(iso_req, pl); // R2
il = pl - ig;
tg = mn(intr_req, il); // R1
fl = (budget > (ig + tg)) ? (budget - ig - tg) : 16'd0;
bg = mn(bulk_req, fl); // R3
rm_iso = ig; rm_intr = tg; rm_bulk = bg;
rm_frames = rm_frames + 1;
rm_tiso = rm_tiso + ig;
rm_tintr = rm_tintr + tg;
rm_tbulk = rm_tbulk + bg;
if (bulk_req != 0 && bg == 0) rm_starved = rm_starved + 1;
// tallies
m_frames = m_frames + 1;
if (iso_req == 0 && intr_req == 0) m_empty = m_empty + 1;
else m_loaded = m_loaded + 1;
if (bulk_req != 0 && bg == 0) m_starved = m_starved + 1;
if ((iso_req + intr_req) > pl) m_over = m_over + 1;
if (bulk_req != 0 && bg == bulk_req) m_bulk_full = m_bulk_full + 1;
end
end
endtask
task cmp; begin
ck("iso_grant", {16'd0, iso_grant}, {16'd0, rm_iso});
ck("intr_grant", {16'd0, intr_grant}, {16'd0, rm_intr});
ck("bulk_grant", {16'd0, bulk_grant}, {16'd0, rm_bulk});
ck("n_frames", {16'd0, n_frames}, {16'd0, rm_frames});
ck("n_starved", {16'd0, n_bulk_starved}, {16'd0, rm_starved});
ck("tot_iso", tot_iso, rm_tiso);
ck("tot_bulk", tot_bulk, rm_tbulk);
end endtask
// The claim, asserted directly: the three grants never exceed the
// frame, and periodic never exceeds its reservation.
task intent_check; begin
bump;
if ((iso_grant + intr_grant + bulk_grant) > budget) begin
err = err + 1;
$display(" ** INTENT VIOLATED: grants exceed the frame budget (t=%0t)", $time);
end
bump;
if ((iso_grant + intr_grant) > periodic_cap) begin
err = err + 1;
$display(" ** INTENT VIOLATED: periodic exceeded its reservation (t=%0t)", $time);
end
end endtask
task step; begin
#1;
@(posedge clk);
ref_step;
#1;
cmp;
intent_check;
frame_start = 0;
end endtask
task hard_reset; begin
rst_n = 0; frame_start = 0;
iso_req = 0; intr_req = 0; bulk_req = 0;
budget = 16'd1500; periodic_cap = 16'd1350;
repeat (3) begin @(posedge clk); ref_step; end
#1; rst_n = 1;
@(posedge clk); ref_step; #1; cmp;
end endtask
task frame(logic [15:0] iso, logic [15:0] intr, logic [15:0] blk);
begin
frame_start = 1; iso_req = iso; intr_req = intr; bulk_req = blk;
step;
end
endtask
// -----------------------------------------------------------------
// PHASE 1 -- THE TWO CLAIMS.
// -----------------------------------------------------------------
int paired_runs;
logic [15:0] iso_a, iso_b, bulk_a, bulk_b;
task phase_intent;
begin
hard_reset;
paired_runs = 0;
// C1 an empty frame: bulk gets everything. The misconception is
// CORRECT here, and saying so is the point -- it is why the
// belief survives.
frame(16'd0, 16'd0, 16'd2000);
ck("C1 bulk got the whole frame", {16'd0, bulk_grant}, 32'd1500);
ck("C1 iso got nothing", {16'd0, iso_grant}, 32'd0);
// C2 the relational claim. Run the SAME periodic load twice,
// changing only what bulk asks for, and require the
// isochronous grant to be identical. If bulk could take
// priority by wanting more, this is where it would show.
for (i = 0; i < 8; i = i + 1) begin
hard_reset;
frame(16'd100 + 16'(i) * 16'd100, 16'd50, 16'd0);
iso_a = iso_grant; bulk_a = bulk_grant;
hard_reset;
frame(16'd100 + 16'(i) * 16'd100, 16'd50, 16'd9000);
iso_b = iso_grant; bulk_b = bulk_grant;
ck("C2 iso is unmoved by bulk demand", {16'd0, iso_a}, {16'd0, iso_b});
bump;
if (bulk_b <= bulk_a) begin
err = err + 1;
$display(" ** C2: bulk asking for more did not get more (%0d vs %0d)",
bulk_a, bulk_b);
end
paired_runs = paired_runs + 1;
end
bump;
if (paired_runs != 8) begin
err = err + 1;
$display(" ** intent: only %0d paired runs", paired_runs);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 2 -- the load grid.
//
// AXES:
// isochronous demand 0, 300, 700, 1200, 1600 bytes 5
// bulk demand 0, 200, 1500, 9000 bytes 4
// reservation cap 1350 (full-speed 90%), 1200 (80%) 2
// ---------------------------------------------------------------
// 5 x 4 x 2 = 40
//
// Interrupt demand is held at 50 rather than swept: within the
// reservation it behaves exactly as isochronous does, and sweeping
// it would multiply the grid without adding a case. That is a claim
// a reviewer can disagree with, which is why it is written here --
// 30.2 §6.
//
// WHAT IS ABSENT: time. Every frame here is independent. A backlog
// that persists across frames is phase 3.
// -----------------------------------------------------------------
logic [15:0] isos [5];
logic [15:0] blks [4];
logic [15:0] caps [2];
task phase_sweep;
begin
isos[0]=16'd0; isos[1]=16'd300; isos[2]=16'd700;
isos[3]=16'd1200; isos[4]=16'd1600;
blks[0]=16'd0; blks[1]=16'd200; blks[2]=16'd1500; blks[3]=16'd9000;
caps[0]=16'd1350; caps[1]=16'd1200;
for (a = 0; a < 5; a = a + 1)
for (b = 0; b < 4; b = b + 1)
for (c = 0; c < 2; c = c + 1) begin
hard_reset;
periodic_cap = caps[c];
bump;
if (periodic_cap !== caps[c]) begin
err = err + 1; m_setupfail = m_setupfail + 1;
$display(" ** setup: cap not applied");
end
frame(isos[a], 16'd50, blks[b]);
frame(16'd0, 16'd0, 16'd0);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 3 -- the named boundaries.
// -----------------------------------------------------------------
task phase_scenarios;
begin
// S1 periodic exactly at the reservation: bulk gets the rest.
hard_reset;
frame(16'd1300, 16'd50, 16'd9000);
ck("S1 iso got all it asked", {16'd0, iso_grant}, 32'd1300);
ck("S1 interrupt too", {16'd0, intr_grant}, 32'd50);
ck("S1 bulk got the remainder",{16'd0, bulk_grant}, 32'd150);
ck("S1 not flagged over", {31'd0, periodic_over}, 32'd0);
// S2 periodic one byte over: the reservation is a CAP, not a
// suggestion, and the excess is refused rather than taken
// from bulk.
hard_reset;
frame(16'd1301, 16'd50, 16'd9000);
ck("S2 flagged over", {31'd0, periodic_over}, 32'd1);
ck("S2 periodic capped", {16'd0, iso_grant + intr_grant}, 32'd1350);
ck("S2 bulk still gets 150", {16'd0, bulk_grant}, 32'd150);
// S3 THE COUNTEREXAMPLE. Periodic fills the reservation
// completely and the frame has nothing left. Bulk asks for
// nine thousand bytes and receives zero -- for as many frames
// as this lasts.
hard_reset;
periodic_cap = 16'd1500;
for (i = 0; i < 10; i = i + 1) frame(16'd1500, 16'd0, 16'd9000);
ck("S3 bulk got nothing, ten frames running", {16'd0, n_bulk_starved}, 32'd10);
ck("S3 while iso got everything", tot_iso, 32'd15000);
ck("S3 and bulk got zero bytes", tot_bulk, 32'd0);
// S4 the mirror: an idle periodic load, ten frames, bulk takes
// the lot. Same design, same parameters, opposite verdict.
hard_reset;
for (i = 0; i < 10; i = i + 1) frame(16'd0, 16'd0, 16'd9000);
ck("S4 bulk never starved", {16'd0, n_bulk_starved}, 32'd0);
ck("S4 and took every byte", tot_bulk, 32'd15000);
// S5 a tiny bulk request always fits when there is room, and
// never fits when there is not. "Fastest" is not a property
// of the request.
hard_reset;
frame(16'd0, 16'd0, 16'd1);
ck("S5 one byte, empty frame", {16'd0, bulk_grant}, 32'd1);
hard_reset;
periodic_cap = 16'd1500;
frame(16'd1500, 16'd0, 16'd1);
ck("S5 one byte, full frame", {16'd0, bulk_grant}, 32'd0);
end
endtask
// -----------------------------------------------------------------
// PHASE 4 -- random, audited.
// -----------------------------------------------------------------
task phase_random;
int r;
begin
in_random = 1;
hard_reset;
for (j = 0; j < 3000; j = j + 1) begin
r = $urandom_range(99);
if (r < 20) periodic_cap = 16'($urandom_range(1599));
if (r < 30) budget = 16'd1000 + 16'($urandom_range(799));
frame(16'($urandom_range(1699)), 16'($urandom_range(299)), 16'($urandom_range(3999)));
end
in_random = 0;
end
endtask
initial begin
chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
m_frames=0; m_empty=0; m_loaded=0; m_starved=0; m_over=0;
m_bulk_full=0; m_setupfail=0;
phase_intent;
$display(" phase 1 intent : %0d checks, %0d errors (%0d paired runs)",
chk_dir, err, paired_runs);
phase_sweep;
$display(" phase 2 exhaustive : %0d checks, %0d errors (40 combinations)", chk_dir, err);
phase_scenarios;
$display(" phase 3 scenarios : %0d checks, %0d errors", chk_dir, err);
$display(" ---- DIRECTED-ONLY : %0d checks, %0d errors ----", chk_dir, err);
phase_random;
$display("");
$display(" measured reachability (all phases)");
$display(" frames scheduled ....... %0d", m_frames);
$display(" with no periodic ..... %0d", m_empty);
$display(" with periodic load ... %0d", m_loaded);
$display(" frames starving bulk ... %0d", m_starved);
$display(" bulk fully satisfied ... %0d", m_bulk_full);
$display(" periodic over its cap .. %0d", m_over);
$display(" setup failures ......... %0d", m_setupfail);
$display("");
$display(" directed checks ........ %0d", chk_dir);
$display(" random checks .......... %0d", chk_rnd);
$display(" TOTAL checks ........... %0d", chk_dir + chk_rnd);
$display(" ERRORS ................. %0d", err);
if (err == 0) $display(" PASS"); else $display(" FAIL");
$finish;
end
endmoduleThe reservation guarantee becomes one property, and it is the most quotable line in the module:
property p_iso_is_independent_of_bulk;
@(posedge clk) disable iff (!rst_n)
frame_start && (iso_req <= periodic_cap) && (periodic_cap <= budget)
|=> (iso_grant == $past(iso_req));
endpropertyBulk demand appears in neither the antecedent nor the consequent, and that absence is the property. Beside it, the safety pair and the mirror:
property p_within_budget; // the frame is the hard limit
property p_within_reservation; // periodic never exceeds its cap
property p_bulk_takes_an_empty_frame; // and here the belief is RIGHTp_within_budget is the one the missing clamp violated while the design and its
reference model agreed with each other. p_bulk_takes_an_empty_frame is the
belief's true half, written down as an obligation so that a "fix" which starved bulk
on an idle frame would fail too.
And the belief becomes a property that a correct design fails, which is worth writing down in a comment and never enabling:
// p_bulk_always_gets_what_it_asks_for -- bulk_req |=> bulk_grant == bulk_req
// This is the misconception as an assertion. A correct allocator
// violates it in 191 frames of this bench, and is right to.A property that a correct design violates is the most precise possible statement of a misconception. It is also the reason 30.2 §9 insists a checker be tested for false positives: somebody will write that assertion in good faith, watch it fail, and "fix" the allocator.
9. VHDL-2008
usb_frame_budget.vhd — the design, VHDL-2008
-- =====================================================================
-- usb_frame_budget (VHDL-2008) -- the same contract. Same ports, same
-- order of allocation, same reset, same latency.
--
-- The clamp that makes the frame the outer bound is written with the
-- same explicitness here as in the other two: a reservation larger
-- than the frame is a configuration error, and the design refuses it
-- rather than granting bytes the frame does not contain. See 31.4
-- 31.4 for how that line came to be written.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity usb_frame_budget is
port (
clk : in std_logic;
rst_n : in std_logic;
frame_start : in std_logic;
iso_req : in unsigned(15 downto 0);
intr_req : in unsigned(15 downto 0);
bulk_req : in unsigned(15 downto 0);
budget : in unsigned(15 downto 0);
periodic_cap : in unsigned(15 downto 0);
iso_grant : out unsigned(15 downto 0);
intr_grant : out unsigned(15 downto 0);
bulk_grant : out unsigned(15 downto 0);
periodic_over : out std_logic;
bulk_starved : out std_logic;
n_frames : out unsigned(15 downto 0);
n_bulk_starved : out unsigned(15 downto 0);
tot_iso : out unsigned(31 downto 0);
tot_intr : out unsigned(31 downto 0);
tot_bulk : out unsigned(31 downto 0)
);
end entity usb_frame_budget;
architecture rtl of usb_frame_budget is
signal iso_r, intr_r, bulk_r : unsigned(15 downto 0) := (others => '0');
signal over_r : std_logic := '0';
signal c_frames, c_starved : unsigned(15 downto 0) := (others => '0');
signal t_iso, t_intr, t_bulk : unsigned(31 downto 0) := (others => '0');
signal per_limit, iso_g, per_left, intr_g : unsigned(15 downto 0);
signal used, frm_left, bulk_g : unsigned(15 downto 0);
signal over : std_logic;
function mn (x, y : unsigned(15 downto 0)) return unsigned is
begin
if x < y then return x; else return y; end if;
end function mn;
begin
-- The frame is the hard limit; the reservation lives inside it.
per_limit <= mn(periodic_cap, budget);
iso_g <= mn(iso_req, per_limit);
per_left <= per_limit - iso_g;
intr_g <= mn(intr_req, per_left);
used <= iso_g + intr_g;
frm_left <= budget - used when budget > used else (others => '0');
bulk_g <= mn(bulk_req, frm_left);
over <= '1' when (iso_req + intr_req) > per_limit else '0';
seq : process (clk, rst_n)
begin
if rst_n = '0' then
iso_r <= (others => '0'); intr_r <= (others => '0');
bulk_r <= (others => '0'); over_r <= '0';
c_frames <= (others => '0'); c_starved <= (others => '0');
t_iso <= (others => '0'); t_intr <= (others => '0');
t_bulk <= (others => '0');
elsif rising_edge(clk) then
if frame_start = '1' then
iso_r <= iso_g;
intr_r <= intr_g;
bulk_r <= bulk_g;
over_r <= over;
c_frames <= c_frames + 1;
t_iso <= t_iso + resize(iso_g, 32);
t_intr <= t_intr + resize(intr_g, 32);
t_bulk <= t_bulk + resize(bulk_g, 32);
if bulk_req /= 0 and bulk_g = 0 then
c_starved <= c_starved + 1;
end if;
end if;
end if;
end process seq;
iso_grant <= iso_r;
intr_grant <= intr_r;
bulk_grant <= bulk_r;
periodic_over <= over_r;
bulk_starved <= '1' when bulk_r = 0 else '0';
n_frames <= c_frames;
n_bulk_starved <= c_starved;
tot_iso <= t_iso;
tot_intr <= t_intr;
tot_bulk <= t_bulk;
end architecture rtl;tb_usb_frame_budget.vhd — the testbench, VHDL-2008
-- =====================================================================
-- tb_usb_frame_budget -- VHDL-2008 testbench for usb_frame_budget.
-- Phases 1-3 present the SAME directed stimulus as the other two
-- benches, so their directed counts must agree.
--
-- Phase 1 carries the RELATIONAL claim: two runs differing only in
-- bulk demand must produce the same isochronous grant. No single-run
-- check can express it.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
entity tb_usb_frame_budget is
end entity tb_usb_frame_budget;
architecture sim of tb_usb_frame_budget is
constant HALF : time := 10 ns;
function b2i_over (s : std_logic) return integer is
begin
if s = '1' then return 1; else return 0; end if;
end function b2i_over;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal frame_start : std_logic := '0';
signal iso_req, intr_req, bulk_req : unsigned(15 downto 0) := (others => '0');
signal budget : unsigned(15 downto 0) := to_unsigned(1500, 16);
signal periodic_cap : unsigned(15 downto 0) := to_unsigned(1350, 16);
signal iso_grant, intr_grant, bulk_grant : unsigned(15 downto 0);
signal periodic_over, bulk_starved : std_logic;
signal n_frames, n_bulk_starved : unsigned(15 downto 0);
signal tot_iso, tot_intr, tot_bulk : unsigned(31 downto 0);
signal done_flag : boolean := false;
begin
dut : entity work.usb_frame_budget
port map (clk => clk, rst_n => rst_n, frame_start => frame_start,
iso_req => iso_req, intr_req => intr_req, bulk_req => bulk_req,
budget => budget, periodic_cap => periodic_cap,
iso_grant => iso_grant, intr_grant => intr_grant,
bulk_grant => bulk_grant, periodic_over => periodic_over,
bulk_starved => bulk_starved, n_frames => n_frames,
n_bulk_starved => n_bulk_starved, tot_iso => tot_iso,
tot_intr => tot_intr, tot_bulk => tot_bulk);
clkgen : process
begin
while not done_flag loop
clk <= '0'; wait for HALF;
clk <= '1'; wait for HALF;
end loop;
wait;
end process clkgen;
stim : process
variable rm_iso, rm_intr, rm_bulk : natural := 0;
variable rm_frames, rm_starved : natural := 0;
variable rm_tiso, rm_tintr, rm_tbulk : natural := 0;
variable chk_dir, chk_rnd, errs, shown : natural := 0;
variable in_random : boolean := false;
variable m_frames, m_empty, m_loaded, m_starved : natural := 0;
variable m_over, m_bulk_full, m_setupfail, paired_runs : natural := 0;
variable iso_a, iso_b, bulk_a, bulk_b : natural := 0;
variable seed1 : positive := 901_337; variable seed2 : positive := 47_111;
type nat5 is array (0 to 4) of natural;
type nat4 is array (0 to 3) of natural;
type nat2 is array (0 to 1) of natural;
constant isos : nat5 := (0, 300, 700, 1200, 1600);
constant blks : nat4 := (0, 200, 1500, 9000);
constant caps : nat2 := (1350, 1200);
procedure bump is
begin
if in_random then chk_rnd := chk_rnd + 1; else chk_dir := chk_dir + 1; end if;
end procedure bump;
procedure ck (what : string; got : integer; exp : integer) is
begin
bump;
if got /= exp then
errs := errs + 1;
if (not in_random) and shown < 40 then
shown := shown + 1;
report " ** " & what & ": got " & integer'image(got) &
" expected " & integer'image(exp) severity warning;
end if;
end if;
end procedure ck;
function mn (x, y : natural) return natural is
begin
if x < y then return x; else return y; end if;
end function mn;
procedure ref_step is
variable ig, il, tg, fl, bg, pl : natural;
begin
if rst_n = '0' then
rm_iso := 0; rm_intr := 0; rm_bulk := 0;
rm_frames := 0; rm_starved := 0;
rm_tiso := 0; rm_tintr := 0; rm_tbulk := 0;
elsif frame_start = '1' then
pl := mn(to_integer(periodic_cap), to_integer(budget));
ig := mn(to_integer(iso_req), pl);
il := pl - ig;
tg := mn(to_integer(intr_req), il);
if to_integer(budget) > (ig + tg) then fl := to_integer(budget) - ig - tg;
else fl := 0; end if;
bg := mn(to_integer(bulk_req), fl);
rm_iso := ig; rm_intr := tg; rm_bulk := bg;
rm_frames := rm_frames + 1;
rm_tiso := rm_tiso + ig; rm_tintr := rm_tintr + tg;
rm_tbulk := rm_tbulk + bg;
if to_integer(bulk_req) /= 0 and bg = 0 then
rm_starved := rm_starved + 1;
m_starved := m_starved + 1;
end if;
m_frames := m_frames + 1;
if iso_req = 0 and intr_req = 0 then m_empty := m_empty + 1;
else m_loaded := m_loaded + 1; end if;
if (to_integer(iso_req) + to_integer(intr_req)) > pl then
m_over := m_over + 1;
end if;
if to_integer(bulk_req) /= 0 and bg = to_integer(bulk_req) then
m_bulk_full := m_bulk_full + 1;
end if;
end if;
end procedure ref_step;
procedure cmp is
begin
ck("iso_grant", to_integer(iso_grant), rm_iso);
ck("intr_grant", to_integer(intr_grant), rm_intr);
ck("bulk_grant", to_integer(bulk_grant), rm_bulk);
ck("n_frames", to_integer(n_frames), rm_frames);
ck("n_starved", to_integer(n_bulk_starved), rm_starved);
ck("tot_iso", to_integer(tot_iso), rm_tiso);
ck("tot_bulk", to_integer(tot_bulk), rm_tbulk);
end procedure cmp;
procedure intent_check is
begin
bump;
if (to_integer(iso_grant) + to_integer(intr_grant) +
to_integer(bulk_grant)) > to_integer(budget) then
errs := errs + 1;
report " ** INTENT VIOLATED: grants exceed the frame budget" severity warning;
end if;
bump;
if (to_integer(iso_grant) + to_integer(intr_grant)) >
to_integer(periodic_cap) then
errs := errs + 1;
report " ** INTENT VIOLATED: periodic exceeded its reservation" severity warning;
end if;
end procedure intent_check;
procedure step is
begin
wait for 1 ns;
wait until rising_edge(clk);
ref_step;
wait for 1 ns;
cmp;
intent_check;
frame_start <= '0';
end procedure step;
procedure hard_reset is
begin
rst_n <= '0'; frame_start <= '0';
iso_req <= (others => '0'); intr_req <= (others => '0');
bulk_req <= (others => '0');
budget <= to_unsigned(1500, 16); periodic_cap <= to_unsigned(1350, 16);
for i in 0 to 2 loop wait until rising_edge(clk); ref_step; end loop;
wait for 1 ns; rst_n <= '1';
wait until rising_edge(clk); ref_step; wait for 1 ns; cmp;
end procedure hard_reset;
procedure frame (iso, intr, blk : natural) is
begin
frame_start <= '1';
iso_req <= to_unsigned(iso, 16);
intr_req <= to_unsigned(intr, 16);
bulk_req <= to_unsigned(blk, 16);
step;
end procedure frame;
impure function rnd (n : positive) return natural is
variable x : real;
begin
uniform(seed1, seed2, x);
return natural(real(n - 1) * x);
end function rnd;
variable r : natural;
begin
-- ---- PHASE 1 : the two claims ----
hard_reset;
paired_runs := 0;
frame(0, 0, 2000);
ck("C1 bulk got the whole frame", to_integer(bulk_grant), 1500);
ck("C1 iso got nothing", to_integer(iso_grant), 0);
for i in 0 to 7 loop
hard_reset;
frame(100 + i * 100, 50, 0);
iso_a := to_integer(iso_grant); bulk_a := to_integer(bulk_grant);
hard_reset;
frame(100 + i * 100, 50, 9000);
iso_b := to_integer(iso_grant); bulk_b := to_integer(bulk_grant);
ck("C2 iso is unmoved by bulk demand", iso_a, iso_b);
bump;
if bulk_b <= bulk_a then
errs := errs + 1;
report " ** C2: bulk asking for more did not get more" severity warning;
end if;
paired_runs := paired_runs + 1;
end loop;
bump;
if paired_runs /= 8 then
errs := errs + 1;
report " ** intent: too few paired runs" severity warning;
end if;
report " phase 1 intent : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors (" &
integer'image(paired_runs) & " paired runs)";
-- ---- PHASE 2 : 5 x 4 x 2 = 40 ----
for a in 0 to 4 loop
for b in 0 to 3 loop
for c in 0 to 1 loop
hard_reset;
periodic_cap <= to_unsigned(caps(c), 16);
-- A VHDL signal assignment is not visible on the next line.
-- Without this wait the check below reads the PREVIOUS cap and
-- reports a setup failure that did not happen. The same hazard
-- cost Module 30 a bench defect; it is the commonest way a
-- Verilog engineer's first VHDL testbench is wrong.
wait for 1 ns;
bump;
if to_integer(periodic_cap) /= caps(c) then
errs := errs + 1; m_setupfail := m_setupfail + 1;
report " ** setup: cap not applied" severity warning;
end if;
frame(isos(a), 50, blks(b));
frame(0, 0, 0);
end loop;
end loop;
end loop;
report " phase 2 exhaustive : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors (40 combinations)";
-- ---- PHASE 3 : the named boundaries ----
hard_reset;
frame(1300, 50, 9000);
ck("S1 iso got all it asked", to_integer(iso_grant), 1300);
ck("S1 interrupt too", to_integer(intr_grant), 50);
ck("S1 bulk got the remainder", to_integer(bulk_grant), 150);
ck("S1 not flagged over", b2i_over(periodic_over), 0);
hard_reset;
frame(1301, 50, 9000);
ck("S2 flagged over", b2i_over(periodic_over), 1);
ck("S2 periodic capped", to_integer(iso_grant) + to_integer(intr_grant), 1350);
ck("S2 bulk still gets 150", to_integer(bulk_grant), 150);
hard_reset;
periodic_cap <= to_unsigned(1500, 16);
for i in 0 to 9 loop frame(1500, 0, 9000); end loop;
ck("S3 bulk got nothing, ten frames running", to_integer(n_bulk_starved), 10);
ck("S3 while iso got everything", to_integer(tot_iso), 15000);
ck("S3 and bulk got zero bytes", to_integer(tot_bulk), 0);
hard_reset;
for i in 0 to 9 loop frame(0, 0, 9000); end loop;
ck("S4 bulk never starved", to_integer(n_bulk_starved), 0);
ck("S4 and took every byte", to_integer(tot_bulk), 15000);
hard_reset;
frame(0, 0, 1);
ck("S5 one byte, empty frame", to_integer(bulk_grant), 1);
hard_reset;
periodic_cap <= to_unsigned(1500, 16);
frame(1500, 0, 1);
ck("S5 one byte, full frame", to_integer(bulk_grant), 0);
report " phase 3 scenarios : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors";
report " ---- DIRECTED-ONLY : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors ----";
-- ---- PHASE 4 : random ----
in_random := true;
hard_reset;
for j in 0 to 2999 loop
r := rnd(100);
if r < 20 then periodic_cap <= to_unsigned(rnd(1600), 16); end if;
if r < 30 then budget <= to_unsigned(1000 + rnd(800), 16); end if;
frame(rnd(1700), rnd(300), rnd(4000));
end loop;
in_random := false;
report " measured reachability (all phases)";
report " frames scheduled ....... " & integer'image(m_frames);
report " with no periodic ..... " & integer'image(m_empty);
report " with periodic load ... " & integer'image(m_loaded);
report " frames starving bulk ... " & integer'image(m_starved);
report " bulk fully satisfied ... " & integer'image(m_bulk_full);
report " periodic over its cap .. " & integer'image(m_over);
report " setup failures ......... " & integer'image(m_setupfail);
report " directed checks ........ " & integer'image(chk_dir);
report " random checks .......... " & integer'image(chk_rnd);
report " TOTAL checks ........... " & integer'image(chk_dir + chk_rnd);
report " ERRORS ................. " & integer'image(errs);
if errs = 0 then report " PASS"; else report " FAIL" severity failure; end if;
done_flag <= true;
wait;
end process stim;
end architecture sim;The VHDL version earns its place here because the allocation is a sequence of signal assignments whose order of dependency is visible in the code, and because integer subtraction with a range constraint would have crashed rather than wrapped had the clamp been missing — a different way to find the same bug.
10. The Misconception As Hardware
MUT THE BELIEF ENCODED V-DIR SV-DIR VH-DIR
S-M1 bulk is fastest, so serve it first
(bulk allocated before the
reservations) 218 218 218
S-M2 a reservation is a suggestion
(periodic grants clamped by what
remains after bulk) 78 78 78BASE zero in all six columns; directed columns identical across the languages.
S-M1 is the belief promoted to policy. It allocates bulk from the full budget
first, then gives the reservations whatever is left. Every number it produces is a
legal-looking byte count and the total never exceeds the frame — so it is not
caught by the total-grant check. It is caught 218 times by the checks that
say isochronous gets what it reserved regardless of bulk — C2 and
p_iso_is_independent_of_bulk — which are the only ones that encode the priority
rather than the arithmetic.
WHAT S-M1 WOULD LOOK LIKE IN A REAL SYSTEM
bulk benchmarks beautifully -- better than the correct design
audio clicks and video drops frames, intermittently, under load
no error counter anywhere moves
the bug is reported as an AUDIO problemS-M2 scores 78 and is the subtler inversion: it keeps the order but makes the reservations yield. A device that reserved bandwidth and did not receive it, with no error raised, is the hardest class of USB bug to attribute, because the symptom appears on a different device from the defect.
11. What The Wrong Model Does To Debugging
SYMPTOM "our bulk throughput halved in the field and we cannot
reproduce it"
WRONG MODEL bulk throughput is a property of our device
WRONG QUESTION what changed in our device / driver / firmware?
WASTED ON firmware versions, buffer sizes, packet alignment,
the host driver, a suspected silicon revision
CORRECT MODEL bulk receives what the frame has left
THE FIRST QUESTION what ELSE is on that bus, and is any of it
periodic?
AND THE SECOND what does the host's bandwidth allocation look
like -- how much periodic bandwidth is
committed on this controller?
Typical resolution: a webcam, a headset, or a hub sharing the
controller. Nothing in the device changed. Nothing was broken. SYMPTOM "our isochronous stream glitches when the disk is busy"
WRONG MODEL bulk and isochronous compete on equal terms
WRONG QUESTION how do we give isochronous more priority?
THE REAL QUESTION is the reservation actually being honoured --
i.e. does the system behave like the design or
like S-M1?
Because if reservations are honoured, bulk activity CANNOT affect
an isochronous stream. If it does, something in the path is
allocating in the wrong order, and that is a scheduling defect with
a specific location -- not a priority to be tuned.That second one is the payoff of having built S-M1. An engineer who has seen the mutation recognises its signature in the field: bulk unusually good, periodic intermittently bad, no errors anywhere.
12. Interview Reasoning
"Which USB transfer type gives you the best performance?"
I would want to split the question first, because "performance" is two different requirements and no transfer type wins both.
Bulk has the highest throughput ceiling: the largest packets, no rate limit, and the host will issue bulk transactions back to back with whatever frame time is available. That is why mass storage and network adaptors use it. But that last clause is the whole answer — bulk gets the bandwidth that nothing else reserved. It has no reservation and no bounded latency, so on a bus with a committed isochronous stream its throughput drops by roughly the reserved amount, and if periodic traffic fills the frame, bulk can legitimately go to zero with no error raised anywhere.
Isochronous is the opposite trade: a fixed reservation agreed at configuration time, bounded latency, and no retries — a corrupted frame is simply lost. So for worst-case guaranteed rate, isochronous wins and bulk does not compete, because bulk offers no guarantee at all. Interrupt buys a bounded service interval with small payloads.
So the answer I would give is: bulk for the highest average rate when you can tolerate an unbounded worst case; isochronous when you need a floor and can tolerate loss; interrupt when you need bounded latency on small data. And the practical warning — a bulk throughput number measured on an otherwise idle bus is not a specification, it is a best case, and it will be quoted back at you as a specification if you put it in a datasheet.
13. Exercises
1 THE ORDER
Write the allocation order from memory and say which type's
grant depends on which other grants.
2 MEASUREMENT
Design the experiment from section 4 as a repeatable test. What
do you record, and what makes the result a RELATION rather than
a number?
3 LATENCY
Bulk has no bounded latency. Construct a legal traffic pattern
under which a single bulk transfer never completes, and say what
(if anything) in the specification forbids it.
4 VERILOG
Add a fourth request type, control, with a reserved MINIMUM
share. Where does it go in the order, and why must it be
reserved at all?
5 SYSTEMVERILOG
Write the property that the control reservation in exercise 4
is always honoured, and say what would break if it were not.
6 VHDL
Implement exercise 4. Say what a range-constrained integer would
have done to the missing-clamp bug in section 6.
7 TESTBENCH
The second axis has values "below / at / above". Add a fourth
value that is worth having, and justify it.
8 MUTATION
S-M1's signature is "bulk unusually good, periodic
intermittently bad, no errors". Write a THIRD mutation whose
signature is "everything slightly slow, nothing ever wrong", and
predict its score.
9 DEBUG
A customer reports that your bulk device is slow only on one
laptop. List the observations in order, and say which of them
are about your device.
10 REVIEW
Write the design-review question from 30.4 that would have
caught a datasheet claiming an idle-bus bulk number as a
specification.14. What Carries Forward
THE CORRECTION
o "always" is the wrong word. Bulk is the fastest type for the
bandwidth NOBODY ELSE RESERVED
o throughput is not a property of a transfer type or a device; it
is the outcome of an ALLOCATION the host performs each frame
o the allocation order is isochronous, interrupt, then bulk -- and
bulk's grant is a function of the OTHER traffic
o bulk starved to zero, with no error anywhere, is correct
operation
o "guaranteed bandwidth" means claimed FIRST, not limited
o FAST is two requirements: throughput and worst-case latency.
Bulk maximises one and abandons the other; isochronous fixes
both and abandons reliability
THE HARDWARE
o bulk_req is compared against what REMAINS, and the remainder is
computed without reference to bulk at all
o a starvation OUTPUT, because the state is legal and needs to be
observable rather than prevented
o min(periodic_cap, budget): a clamp added because a check
disagreed with both the design and its model
THE METHOD
o a design and a reference model written from the same sentence
are ONE artefact. Only an independently stated intent found the
missing clamp
o an axis of RELATIONSHIPS (below / at / above) is worth more than
an axis of numbers, and "at the bound" is not "past the bound"
o arithmetic properties are blind to priority inversion: three of
the four obvious checks pass on both mutants
o the misconception written as an SVA property is a property a
CORRECT design violates -- in 191 frames here
THE DEBUG CONSEQUENCE
o "why did our throughput drop" is answered by looking at the rest
of the bus, not at the device
o if bulk traffic affects an isochronous stream at all, the
reservation is not being honoured, and that is a located defect
rather than a priority to tuneThe next belief is the one that wastes the most engineering days per year, and it is held mostly by people who have never watched a device fail to be recognised.
Continue learning
Related tutorials
- Related topic
Audio over Isochronous
Isochronous gives up the retry and the NAK, so a device cannot slow the host down — it can only report the rate it needs. The feedback accumulator in three HDLs, and the clamp that hid a defect from eight of nine chances to catch it.
- Related topic
Bandwidth Reservation
An isochronous endpoint reserves time on the wire, not bytes — and worst-case bit stuffing inflates every payload by a sixth. The host's admission arithmetic reproduced exactly, verified over its entire input domain.
- Related topic
Frame Scheduling (1 ms)
The 1 ms frame is the unit of scheduling opportunity, and its number is an 11-bit protocol field living inside a counter of some other width — with the mutation that was equivalent in one language only.
- Related topic
Bandwidth Allocation
A scheduler has two numbers for every transaction — what it expects the work to cost and what it actually cost — and using one for both keeps a wrong ledger while making correct decisions.
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.
