USB · Module 27
The Transfer-Types Question
Guaranteed bandwidth and guaranteed delivery are the same resource spent two different ways — isochronous has no retries because every slot is already promised, not because somebody skimped.
Chapter 27.4 established what an endpoint is. This chapter is about what it is for, and it contains the single most commonly inverted statement in USB interviews.
1. The Question
"You are designing a device that streams audio. Which transfer type, and why?"
The type is the easy half. Everyone says isochronous. The why is where the answer is won or lost, and the usual attempt goes:
"Isochronous, because it is fast. The downside is that it is unreliable — there are no retries."
That is two claims and both are wrong. Isochronous is not particularly fast; bulk is faster on an idle bus. And the lack of retries is not a downside of the guarantee — it is the guarantee, seen from the other side.
2. The Four Types, Derived Rather Than Listed
Start from the schedule. The host has a 1 ms frame, and it must divide it. Everything else follows from two decisions: does this endpoint get a reservation, and is it allowed to retry.
| Reservation | Retries | Therefore | Used for | |
|---|---|---|---|---|
| Isochronous | yes, every frame | no | bounded latency, lossy | audio, video |
| Interrupt | yes, every N frames | yes | bounded latency, reliable, small | keyboards, mice |
| Control | its own 10% | yes | always available | enumeration, configuration |
| Bulk | none | yes | reliable, unbounded latency | disks, printers, networks |
Interrupt looks like a contradiction — reserved and retryable — and it is not. It buys the retry by being small: an interrupt endpoint is capped at 64 bytes at full speed, and its reservation is per N frames rather than per frame. A small reservation leaves room in the frame for the retry to land in. That is the whole trick, and it is why "interrupt" cannot be used for bulk data no matter how much you want its latency.
The one question that generates the table:
reservation? --no--> BULK (retry freely, no deadline)
|
yes
|
big enough to fill a frame?
| |
yes no
| |
ISOCHRONOUS INTERRUPT
(no room to retry) (small enough that a retry fits)
CONTROL is the special case: its own separate 10%, so that a
full periodic schedule can never stop the host talking.3. Why Control Has Its Own Reservation
This is the detail that separates a good answer from a complete one.
Control could have been given the periodic remainder — whatever is left after isochronous and interrupt. It is not. It gets its own 10% minimum, and the periodic types are capped at 90%, as two separate rules.
The reason is a deadlock that would otherwise be reachable: if control shared the periodic budget, a fully committed schedule would leave no room for control transfers — and control transfers are how the host changes the schedule. A device could not be configured, an endpoint could not be un-halted, and nothing could be removed to make room. The bus would be full and unable to be emptied.
4. What We Are Building
usb_xfer_admit is the host's admission control: the block that decides whether a new endpoint can be accepted into the schedule. It is where the trade stops being a discussion and becomes arithmetic.
One frame, three budgets, and what each buys
The budget filling, and the refusal that follows
used_per never moves when the bulk endpoint is admitted. That is the arithmetic statement of "bulk has no reservation", and it is mutation E1.
5. Two Details That Are Easy to Get Wrong
The cost of a transaction is not its payload. A token, a data PID, a CRC and an inter-packet gap ride along with every transaction — conventionally 13 bytes at full speed. An 8-byte interrupt endpoint costs 21, not 8, and for small endpoints the overhead dominates. A budget that charges the payload alone is optimistic by 13 bytes per endpoint, admits one endpoint too many, and the schedule then misses deadlines it was told it would meet. That is mutation E4.
Charge the worst frame, not the average. An endpoint with maxp = 64 and an interval of 8 frames moves 64 bytes every eighth frame — 8 bytes per frame on average. Charging the average is wrong, because the schedule has to survive the frame in which the transaction actually happens. Charge the full 77.
6. Seven Properties
| # | Property |
|---|---|
| 1 | The periodic commitment never exceeds 90% of the frame. |
| 2 | The control commitment never exceeds its own reservation. |
| 3 | Bulk is charged nothing and can always be admitted. |
| 4 | A packet larger than the type allows is refused, with the reason. |
| 5 | A periodic interval that is not a power of two is refused. |
| 6 | A release returns exactly what was charged. |
| 7 | A release and a request in the same cycle both succeed. |
Property 7 is the one nobody writes down. Section 12 is about why it matters and why the obvious implementation gets it wrong.
7. Verilog-2005 RTL
// =====================================================================
// usb_xfer_admit -- "Which transfer type, and why?" answered in hardware.
//
// The four types are easy to list and the trade is easy to state
// backwards. What candidates say: "isochronous is fast but unreliable."
// What is actually true:
//
// Guaranteed bandwidth and guaranteed delivery are the SAME
// resource spent two different ways.
//
// An isochronous endpoint is promised a slot in every frame. A retry
// needs a slot. Every slot is already allocated -- that is what the
// promise means -- so there is nowhere to put the retry. Isochronous
// is not unreliable because somebody skimped on error handling; it is
// unreliable BECAUSE it is guaranteed.
//
// Bulk is the mirror image: no reservation at all, therefore retries
// are free, therefore delivery is certain and timing is not.
//
// This module is the host's admission control: it decides whether a new
// endpoint can be accepted into the schedule, and it is the place where
// the trade becomes arithmetic.
// =====================================================================
module usb_xfer_admit #(
// A USB 2.0 full-speed frame is 1500 bytes at 12 Mbit/s. The
// specification reserves at most 90% for periodic traffic and at least
// 10% for control, and the two limits are separate rules.
parameter integer FRAME_BYTES = 1500,
parameter integer PERIODIC_MAX = 1350, // 90% of the frame
parameter integer CONTROL_MIN = 150, // 10%, reserved for control
parameter integer N_EP = 8
) (
input wire clk,
input wire rst_n,
// ---- a request to admit an endpoint into the schedule ----
input wire req_valid,
input wire [1:0] req_type, // X_CTRL / X_ISO / X_INT / X_BULK
input wire [10:0] req_maxp, // bytes per transaction
input wire [7:0] req_interval, // frames between polls (periodic only)
// ---- withdraw a previously admitted endpoint ----
input wire rel_valid,
input wire [3:0] rel_slot,
// ---- the decision, one cycle later ----
output wire adm_done,
output wire adm_ok,
output wire [2:0] adm_reason,
output wire [3:0] adm_slot,
// ---- the schedule as it stands ----
output wire [15:0] used_periodic,
output wire [15:0] used_control,
output wire [15:0] n_admitted,
// ---- observability ----
output wire [31:0] n_req,
output wire [31:0] n_reject_bw,
output wire [31:0] n_reject_interval,
output wire [31:0] n_reject_maxp,
output wire [31:0] n_reject_full,
output wire [31:0] n_overcommit // must always read 0
);
localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;
localparam [2:0] R_OK = 3'd0,
R_BW = 3'd1, // no periodic bandwidth left
R_INTERVAL = 3'd2, // interval not a legal value
R_MAXP = 3'd3, // packet too large for the type
R_FULL = 3'd4, // no free slot
R_CTRL_ROOM = 3'd5; // would eat the control reservation
// ---- the largest packet each type may use at full speed ----
//
// These are not stylistic limits. Isochronous gets the biggest packet
// because it is the type that cannot retry, so a lost packet must cost
// as little as possible in transactions -- one big one beats eight small
// ones when none of them can be repeated.
function [10:0] max_packet;
input [1:0] t;
begin
case (t)
X_CTRL: max_packet = 11'd64;
X_ISO: max_packet = 11'd1023;
X_INT: max_packet = 11'd64;
default: max_packet = 11'd64; // bulk
endcase
end
endfunction
// ---- a periodic endpoint's cost, per frame ----
//
// maxp bytes every `interval` frames is maxp/interval bytes per frame on
// average -- but the schedule must survive the WORST frame, not the
// average one, so the cost charged is the full maxp. Charging the average
// is how a schedule that passes admission control then misses deadlines.
function [15:0] frame_cost;
input [10:0] mp;
begin
// Bytes on the wire are more than the payload: token, data PID, CRC
// and inter-packet gap. 13 bytes of overhead per transaction at full
// speed is the conventional figure and it is not negligible for small
// packets -- an 8-byte interrupt endpoint costs 21, not 8.
frame_cost = {5'd0, mp} + 16'd13;
end
endfunction
// ---- a legal interval is a power of two, in frames ----
function is_pow2;
input [7:0] v;
begin
is_pow2 = (v != 8'd0) && ((v & (v - 8'd1)) == 8'd0);
end
endfunction
reg [15:0] per_r; // periodic bytes committed per frame
reg [15:0] ctl_r; // control bytes committed per frame
reg [15:0] cnt_r; // how many endpoints are admitted
reg [10:0] slot_mp [0:N_EP-1];
reg [1:0] slot_type [0:N_EP-1];
// A packed vector rather than an array of regs, so that a same-cycle
// release can be MASKED OUT of it combinationally. See used_now below.
reg [N_EP-1:0] used_r;
reg done_r;
reg ok_r;
reg [2:0] why_r;
reg [3:0] slot_r;
reg [31:0] req_c, bw_c, iv_c, mp_c, full_c, over_c;
assign adm_done = done_r;
assign adm_ok = ok_r;
assign adm_reason = why_r;
assign adm_slot = slot_r;
assign used_periodic = per_r;
assign used_control = ctl_r;
assign n_admitted = cnt_r;
assign n_req = req_c;
assign n_reject_bw = bw_c;
assign n_reject_interval = iv_c;
assign n_reject_maxp = mp_c;
assign n_reject_full = full_c;
assign n_overcommit = over_c;
// =================================================================
// THE POST-RELEASE VIEW.
//
// A release written "before" the request in source order is NOT applied
// before it in hardware: every read below sees the registered value, so
// a driver that closes one endpoint and opens another in the same cycle
// would be refused for want of a slot it just freed.
//
// Being refused is not a functional failure -- the driver retries and
// succeeds -- which is exactly why this is worth getting right. The
// symptom is a bandwidth limit that appears only under load, and it is
// unreproducible because it depends on whether two operations happened
// to land in one cycle.
//
// So the post-release state is computed explicitly and everything that
// decides the request reads THAT.
// =================================================================
wire rel_hit = rel_valid && (rel_slot < N_EP) && used_r[rel_slot];
wire [15:0] rel_cost = frame_cost(slot_mp[rel_slot]);
wire rel_per = rel_hit && ((slot_type[rel_slot] == X_ISO) ||
(slot_type[rel_slot] == X_INT));
wire rel_ctl = rel_hit && (slot_type[rel_slot] == X_CTRL);
wire [N_EP-1:0] used_now = rel_hit ? (used_r & ~({{(N_EP-1){1'b0}}, 1'b1} << rel_slot))
: used_r;
wire [15:0] per_now = rel_per ? (per_r - rel_cost) : per_r;
wire [15:0] ctl_now = rel_ctl ? (ctl_r - rel_cost) : ctl_r;
// ---- the first free slot, in the POST-RELEASE view ----
integer j;
reg [3:0] free_slot;
reg have_free;
always @* begin
free_slot = 4'd0;
have_free = 1'b0;
for (j = N_EP - 1; j >= 0; j = j - 1)
if (!used_now[j]) begin
free_slot = j[3:0];
have_free = 1'b1;
end
end
wire is_periodic = (req_type == X_ISO) || (req_type == X_INT);
wire [15:0] cost = frame_cost(req_maxp);
integer k;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
per_r <= 16'd0;
ctl_r <= 16'd0;
cnt_r <= 16'd0;
done_r <= 1'b0;
ok_r <= 1'b0;
why_r <= R_OK;
slot_r <= 4'd0;
req_c <= 32'd0;
bw_c <= 32'd0;
iv_c <= 32'd0;
mp_c <= 32'd0;
full_c <= 32'd0;
over_c <= 32'd0;
used_r <= {N_EP{1'b0}};
for (k = 0; k < N_EP; k = k + 1) begin
slot_mp[k] <= 11'd0;
slot_type[k] <= X_CTRL;
end
end else begin
done_r <= 1'b0;
ok_r <= 1'b0;
why_r <= R_OK;
// ---- release first, so a slot freed this cycle is available ----
//
// Applied before the request below, which means a driver that closes
// one endpoint and opens another in the same cycle succeeds. Ordering
// it the other way rejects the request and the driver retries -- which
// works, and looks like a bandwidth bug under load.
if (rel_hit) begin
cnt_r <= cnt_r - 16'd1;
if (rel_per) per_r <= per_now;
else if (rel_ctl) ctl_r <= ctl_now;
end
used_r <= used_now;
if (req_valid) begin
req_c <= req_c + 32'd1;
done_r <= 1'b1;
slot_r <= free_slot;
// ---- the checks, in the order that makes the reason useful ----
//
// Cheapest and most specific first. A request that is wrong in two
// ways should report the one the driver can act on.
if (req_maxp > max_packet(req_type)) begin
ok_r <= 1'b0;
why_r <= R_MAXP;
mp_c <= mp_c + 32'd1;
end else if (is_periodic && !is_pow2(req_interval)) begin
// A periodic interval must be a power of two so the host can
// place the endpoint in a binary tree of frames. 3 frames is not
// schedulable however much bandwidth is free.
ok_r <= 1'b0;
why_r <= R_INTERVAL;
iv_c <= iv_c + 32'd1;
end else if (!have_free) begin
ok_r <= 1'b0;
why_r <= R_FULL;
full_c <= full_c + 32'd1;
end else if (is_periodic && ((per_now + cost) > PERIODIC_MAX)) begin
// The 90% rule. This is the one that makes isochronous what it
// is: once the periodic budget is spoken for, nothing more gets
// in -- and nothing already in can be retried either.
ok_r <= 1'b0;
why_r <= R_BW;
bw_c <= bw_c + 32'd1;
end else if ((req_type == X_CTRL) &&
((ctl_now + cost) > CONTROL_MIN)) begin
// The 10% rule, and it is a SEPARATE limit rather than the
// remainder of the periodic one. Control traffic has its own
// reservation precisely so that a fully committed periodic
// schedule cannot stop the host talking to its devices.
ok_r <= 1'b0;
why_r <= R_CTRL_ROOM;
bw_c <= bw_c + 32'd1;
end else begin
ok_r <= 1'b1;
why_r <= R_OK;
used_r <= used_now | ({{(N_EP-1){1'b0}}, 1'b1} << free_slot);
slot_mp[free_slot] <= req_maxp;
slot_type[free_slot] <= req_type;
cnt_r <= rel_hit ? cnt_r : (cnt_r + 16'd1);
if (is_periodic) per_r <= per_now + cost;
else if (req_type == X_CTRL) ctl_r <= ctl_now + cost;
// Bulk is charged NOTHING. It has no reservation, which is
// exactly why it can retry: it uses whatever the frame has left
// after everything with a promise has been served.
end
end
// ---- the self-check that makes the whole thing measurable ----
//
// The periodic commitment must never exceed its ceiling, and the
// control commitment must never exceed its reservation. Unreachable
// on a correct design, counted so a run can publish zero.
if ((per_r > PERIODIC_MAX) || (ctl_r > CONTROL_MIN))
over_c <= over_c + 32'd1;
end
end
endmodule8. SystemVerilog RTL
// =====================================================================
// usb_xfer_admit -- SystemVerilog.
//
// The transfer types and the rejection reasons become named enumerations,
// which matters here for a specific reason: this module's job is to REPORT
// WHY, and a rejection reason that arrives as the integer 5 is a reason
// nobody reads. `R_CTRL_ROOM` in a waveform is a diagnosis.
//
// The four types are easy to list and the trade is easy to state
// backwards. What candidates say: "isochronous is fast but unreliable."
// What is actually true:
//
// Guaranteed bandwidth and guaranteed delivery are the SAME
// resource spent two different ways.
//
// An isochronous endpoint is promised a slot in every frame. A retry
// needs a slot. Every slot is already allocated -- that is what the
// promise means -- so there is nowhere to put the retry. Isochronous
// is not unreliable because somebody skimped on error handling; it is
// unreliable BECAUSE it is guaranteed.
//
// Bulk is the mirror image: no reservation at all, therefore retries
// are free, therefore delivery is certain and timing is not.
//
// This module is the host's admission control: it decides whether a new
// endpoint can be accepted into the schedule, and it is the place where
// the trade becomes arithmetic.
// =====================================================================
module usb_xfer_admit #(
// A USB 2.0 full-speed frame is 1500 bytes at 12 Mbit/s. The
// specification reserves at most 90% for periodic traffic and at least
// 10% for control, and the two limits are separate rules.
parameter int FRAME_BYTES = 1500,
parameter int PERIODIC_MAX = 1350, // 90% of the frame
parameter int CONTROL_MIN = 150, // 10%, reserved for control
parameter int N_EP = 8
) (
input logic clk,
input logic rst_n,
// ---- a request to admit an endpoint into the schedule ----
input logic req_valid,
input logic [1:0] req_type, // X_CTRL / X_ISO / X_INT / X_BULK
input logic [10:0]req_maxp, // bytes per transaction
input logic [7:0] req_interval, // frames between polls (periodic only)
// ---- withdraw a previously admitted endpoint ----
input logic rel_valid,
input logic [3:0] rel_slot,
// ---- the decision, one cycle later ----
output logic adm_done,
output logic adm_ok,
output logic [2:0] adm_reason,
output logic [3:0] adm_slot,
// ---- the schedule as it stands ----
output logic [15:0]used_periodic,
output logic [15:0]used_control,
output logic [15:0]n_admitted,
// ---- observability ----
output logic [31:0]n_req,
output logic [31:0]n_reject_bw,
output logic [31:0]n_reject_interval,
output logic [31:0]n_reject_maxp,
output logic [31:0]n_reject_full,
output logic [31:0]n_overcommit // must always read 0
);
typedef enum logic [1:0] { X_CTRL = 2'd0, X_ISO = 2'd1,
X_INT = 2'd2, X_BULK = 2'd3 } xfer_e;
// A rejection reason that arrives as the integer 5 is a reason nobody
// reads. This is the output the driver author actually consumes.
typedef enum logic [2:0] {
R_OK = 3'd0,
R_BW = 3'd1, // no periodic bandwidth left
R_INTERVAL = 3'd2, // interval not a legal value
R_MAXP = 3'd3, // packet too large for the type
R_FULL = 3'd4, // no free slot
R_CTRL_ROOM = 3'd5 // would eat the control reservation
} reason_e;
// ---- the largest packet each type may use at full speed ----
//
// These are not stylistic limits. Isochronous gets the biggest packet
// because it is the type that cannot retry, so a lost packet must cost
// as little as possible in transactions -- one big one beats eight small
// ones when none of them can be repeated.
function automatic logic [10:0] max_packet(logic [1:0] t);
begin
case (t)
X_CTRL: max_packet = 11'd64;
X_ISO: max_packet = 11'd1023;
X_INT: max_packet = 11'd64;
default: max_packet = 11'd64; // bulk
endcase
end
endfunction
// ---- a periodic endpoint's cost, per frame ----
//
// maxp bytes every `interval` frames is maxp/interval bytes per frame on
// average -- but the schedule must survive the WORST frame, not the
// average one, so the cost charged is the full maxp. Charging the average
// is how a schedule that passes admission control then misses deadlines.
function automatic logic [15:0] frame_cost(logic [10:0] mp);
begin
// Bytes on the wire are more than the payload: token, data PID, CRC
// and inter-packet gap. 13 bytes of overhead per transaction at full
// speed is the conventional figure and it is not negligible for small
// packets -- an 8-byte interrupt endpoint costs 21, not 8.
frame_cost = {5'd0, mp} + 16'd13;
end
endfunction
// ---- a legal interval is a power of two, in frames ----
function automatic logic is_pow2(logic [7:0] v);
begin
is_pow2 = (v != 8'd0) && ((v & (v - 8'd1)) == 8'd0);
end
endfunction
logic [15:0] per_r; // periodic bytes committed per frame
logic [15:0] ctl_r; // control bytes committed per frame
logic [15:0] cnt_r; // how many endpoints are admitted
logic [10:0] slot_mp [N_EP];
xfer_e slot_type [N_EP];
// A packed vector rather than an array of regs, so that a same-cycle
// release can be MASKED OUT of it combinationally. See used_now below.
logic [N_EP-1:0] used_r;
logic done_r;
logic ok_r;
reason_e why_r;
logic [3:0] slot_r;
logic [31:0] req_c, bw_c, iv_c, mp_c, full_c, over_c;
assign adm_done = done_r;
assign adm_ok = ok_r;
assign adm_reason = why_r;
assign adm_slot = slot_r;
assign used_periodic = per_r;
assign used_control = ctl_r;
assign n_admitted = cnt_r;
assign n_req = req_c;
assign n_reject_bw = bw_c;
assign n_reject_interval = iv_c;
assign n_reject_maxp = mp_c;
assign n_reject_full = full_c;
assign n_overcommit = over_c;
// =================================================================
// THE POST-RELEASE VIEW.
//
// A release written "before" the request in source order is NOT applied
// before it in hardware: every read below sees the registered value, so
// a driver that closes one endpoint and opens another in the same cycle
// would be refused for want of a slot it just freed.
//
// Being refused is not a functional failure -- the driver retries and
// succeeds -- which is exactly why this is worth getting right. The
// symptom is a bandwidth limit that appears only under load, and it is
// unreproducible because it depends on whether two operations happened
// to land in one cycle.
//
// So the post-release state is computed explicitly and everything that
// decides the request reads THAT.
// =================================================================
wire rel_hit = rel_valid && (rel_slot < N_EP) && used_r[rel_slot];
wire [15:0] rel_cost = frame_cost(slot_mp[rel_slot]);
wire rel_per = rel_hit && ((slot_type[rel_slot] == X_ISO) ||
(slot_type[rel_slot] == X_INT));
wire rel_ctl = rel_hit && (slot_type[rel_slot] == X_CTRL);
wire [N_EP-1:0] used_now = rel_hit ? (used_r & ~({{(N_EP-1){1'b0}}, 1'b1} << rel_slot))
: used_r;
wire [15:0] per_now = rel_per ? (per_r - rel_cost) : per_r;
wire [15:0] ctl_now = rel_ctl ? (ctl_r - rel_cost) : ctl_r;
// ---- the first free slot, in the POST-RELEASE view ----
logic [3:0] free_slot;
logic have_free;
always_comb begin
free_slot = 4'd0;
have_free = 1'b0;
for (int j = N_EP - 1; j >= 0; j--)
if (!used_now[j]) begin
free_slot = j[3:0];
have_free = 1'b1;
end
end
wire is_periodic = (req_type == X_ISO) || (req_type == X_INT);
wire [15:0] cost = frame_cost(req_maxp);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
per_r <= 16'd0;
ctl_r <= 16'd0;
cnt_r <= 16'd0;
done_r <= 1'b0;
ok_r <= 1'b0;
why_r <= R_OK;
slot_r <= 4'd0;
req_c <= 32'd0;
bw_c <= 32'd0;
iv_c <= 32'd0;
mp_c <= 32'd0;
full_c <= 32'd0;
over_c <= 32'd0;
used_r <= {N_EP{1'b0}};
for (int k = 0; k < N_EP; k++) begin
slot_mp[k] <= 11'd0;
slot_type[k] <= X_CTRL;
end
end else begin
done_r <= 1'b0;
ok_r <= 1'b0;
why_r <= R_OK;
// ---- release first, so a slot freed this cycle is available ----
//
// Applied before the request below, which means a driver that closes
// one endpoint and opens another in the same cycle succeeds. Ordering
// it the other way rejects the request and the driver retries -- which
// works, and looks like a bandwidth bug under load.
if (rel_hit) begin
cnt_r <= cnt_r - 16'd1;
if (rel_per) per_r <= per_now;
else if (rel_ctl) ctl_r <= ctl_now;
end
used_r <= used_now;
if (req_valid) begin
req_c <= req_c + 32'd1;
done_r <= 1'b1;
slot_r <= free_slot;
// ---- the checks, in the order that makes the reason useful ----
//
// Cheapest and most specific first. A request that is wrong in two
// ways should report the one the driver can act on.
if (req_maxp > max_packet(req_type)) begin
ok_r <= 1'b0;
why_r <= R_MAXP;
mp_c <= mp_c + 32'd1;
end else if (is_periodic && !is_pow2(req_interval)) begin
// A periodic interval must be a power of two so the host can
// place the endpoint in a binary tree of frames. 3 frames is not
// schedulable however much bandwidth is free.
ok_r <= 1'b0;
why_r <= R_INTERVAL;
iv_c <= iv_c + 32'd1;
end else if (!have_free) begin
ok_r <= 1'b0;
why_r <= R_FULL;
full_c <= full_c + 32'd1;
end else if (is_periodic && ((per_now + cost) > PERIODIC_MAX)) begin
// The 90% rule. This is the one that makes isochronous what it
// is: once the periodic budget is spoken for, nothing more gets
// in -- and nothing already in can be retried either.
ok_r <= 1'b0;
why_r <= R_BW;
bw_c <= bw_c + 32'd1;
end else if ((req_type == X_CTRL) &&
((ctl_now + cost) > CONTROL_MIN)) begin
// The 10% rule, and it is a SEPARATE limit rather than the
// remainder of the periodic one. Control traffic has its own
// reservation precisely so that a fully committed periodic
// schedule cannot stop the host talking to its devices.
ok_r <= 1'b0;
why_r <= R_CTRL_ROOM;
bw_c <= bw_c + 32'd1;
end else begin
ok_r <= 1'b1;
why_r <= R_OK;
used_r <= used_now | ({{(N_EP-1){1'b0}}, 1'b1} << free_slot);
slot_mp[free_slot] <= req_maxp;
slot_type[free_slot] <= xfer_e'(req_type);
cnt_r <= rel_hit ? cnt_r : (cnt_r + 16'd1);
if (is_periodic) per_r <= per_now + cost;
else if (req_type == X_CTRL) ctl_r <= ctl_now + cost;
// Bulk is charged NOTHING. It has no reservation, which is
// exactly why it can retry: it uses whatever the frame has left
// after everything with a promise has been served.
end
end
// ---- the self-check that makes the whole thing measurable ----
//
// The periodic commitment must never exceed its ceiling, and the
// control commitment must never exceed its reservation. Unreachable
// on a correct design, counted so a run can publish zero.
if ((per_r > PERIODIC_MAX) || (ctl_r > CONTROL_MIN))
over_c <= over_c + 32'd1;
end
end
endmodule9. VHDL-2008 RTL
-- =====================================================================
-- usb_xfer_admit -- VHDL-2008.
--
-- The transfer types and the rejection reasons are enumerations of
-- genuinely distinct types, so the compiler refuses to confuse a reason
-- with a type. That matters here because this module's whole output is a
-- DIAGNOSIS: "refused" is useless, "refused because the control
-- reservation is full" tells a driver author what to change.
--
-- Constants are prefixed because VHDL identifiers are case-insensitive:
-- a package constant and a same-named port are one name and the port
-- wins, silently.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package xt_pkg is
type xfer_t is (XT_CTRL, XT_ISO, XT_INT, XT_BULK);
type reason_t is (RS_OK,
RS_BW, -- no periodic bandwidth left
RS_INTERVAL, -- interval not a legal value
RS_MAXP, -- packet too large for the type
RS_FULL, -- no free slot
RS_CTRL_ROOM); -- would eat the control reservation
function xfer_of(v : std_logic_vector(1 downto 0)) return xfer_t;
function code_of(r : reason_t) return std_logic_vector;
function is_periodic_t(t : xfer_t) return boolean;
-- A transaction costs more than its payload: token, data PID, CRC and
-- inter-packet gap. 13 bytes at full speed is the conventional figure,
-- and it is not negligible for small packets -- an 8-byte interrupt
-- endpoint costs 21, not 8.
constant TXN_OVERHEAD : natural := 13;
type mp_array is array (natural range <>) of unsigned(10 downto 0);
type ty_array is array (natural range <>) of xfer_t;
end package;
package body xt_pkg is
function xfer_of(v : std_logic_vector(1 downto 0)) return xfer_t is
begin
case v is
when "00" => return XT_CTRL;
when "01" => return XT_ISO;
when "10" => return XT_INT;
when others => return XT_BULK;
end case;
end function;
function code_of(r : reason_t) return std_logic_vector is
begin
case r is
when RS_OK => return "000";
when RS_BW => return "001";
when RS_INTERVAL => return "010";
when RS_MAXP => return "011";
when RS_FULL => return "100";
when RS_CTRL_ROOM => return "101";
end case;
end function;
function is_periodic_t(t : xfer_t) return boolean is
begin
return t = XT_ISO or t = XT_INT;
end function;
end package body;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.xt_pkg.all;
entity usb_xfer_admit is
generic (
FRAME_BYTES : natural := 1500;
PERIODIC_MAX : natural := 1350; -- 90% of the frame
CONTROL_MIN : natural := 150; -- 10%, reserved for control
N_EP : natural := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
req_valid : in std_logic;
req_type : in std_logic_vector(1 downto 0);
req_maxp : in std_logic_vector(10 downto 0);
req_interval : in std_logic_vector(7 downto 0);
rel_valid : in std_logic;
rel_slot : in std_logic_vector(3 downto 0);
adm_done : out std_logic;
adm_ok : out std_logic;
adm_reason : out std_logic_vector(2 downto 0);
adm_slot : out std_logic_vector(3 downto 0);
used_periodic : out std_logic_vector(15 downto 0);
used_control : out std_logic_vector(15 downto 0);
n_admitted : out std_logic_vector(15 downto 0);
n_req : out std_logic_vector(31 downto 0);
n_reject_bw : out std_logic_vector(31 downto 0);
n_reject_interval : out std_logic_vector(31 downto 0);
n_reject_maxp : out std_logic_vector(31 downto 0);
n_reject_full : out std_logic_vector(31 downto 0);
n_overcommit : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_xfer_admit is
signal per_r, ctl_r, cnt_r : unsigned(15 downto 0) := (others => '0');
signal used_r : std_logic_vector(N_EP-1 downto 0) := (others => '0');
signal slot_mp : mp_array(0 to N_EP-1) := (others => (others => '0'));
signal slot_ty : ty_array(0 to N_EP-1) := (others => XT_CTRL);
signal done_r : std_logic := '0';
signal ok_r : std_logic := '0';
signal why_r : reason_t := RS_OK;
signal slot_r : unsigned(3 downto 0) := (others => '0');
signal req_c, bw_c, iv_c, mp_c, full_c, over_c : unsigned(31 downto 0)
:= (others => '0');
-- ---- the largest packet each type may use at full speed ----
--
-- Isochronous gets the biggest packet because it is the type that cannot
-- retry: a lost packet must cost as few transactions as possible, and one
-- big one beats eight small ones when none of them can be repeated.
-- A case over all four types rather than "ISO or not", so that the
-- per-type limits can be changed independently. Collapsing it to a single
-- comparison also collapses three mutations into one: a mutation of the
-- control limit then silently changes interrupt and bulk too, and the
-- VHDL column reads 116236 against Verilog's 64357.
function max_packet(t : xfer_t) return unsigned is
begin
case t is
when XT_CTRL => return to_unsigned(64, 11);
when XT_ISO => return to_unsigned(1023, 11);
when XT_INT => return to_unsigned(64, 11);
when XT_BULK => return to_unsigned(64, 11);
end case;
end function;
-- The cost charged is the FULL packet, not maxp/interval. The schedule
-- must survive the worst frame, not the average one; charging the average
-- is how a schedule passes admission control and then misses deadlines.
function frame_cost(mp : unsigned(10 downto 0)) return unsigned is
begin
return resize(mp, 16) + to_unsigned(TXN_OVERHEAD, 16);
end function;
-- A legal periodic interval is a power of two, in frames, so the host can
-- place the endpoint in a binary tree of frames. 3 frames is not
-- schedulable however much bandwidth is free.
function is_pow2(v : unsigned(7 downto 0)) return boolean is
begin
return v /= 0 and (v and (v - 1)) = 0;
end function;
begin
adm_done <= done_r;
adm_ok <= ok_r;
adm_reason <= code_of(why_r);
adm_slot <= std_logic_vector(slot_r);
used_periodic <= std_logic_vector(per_r);
used_control <= std_logic_vector(ctl_r);
n_admitted <= std_logic_vector(cnt_r);
n_req <= std_logic_vector(req_c);
n_reject_bw <= std_logic_vector(bw_c);
n_reject_interval <= std_logic_vector(iv_c);
n_reject_maxp <= std_logic_vector(mp_c);
n_reject_full <= std_logic_vector(full_c);
n_overcommit <= std_logic_vector(over_c);
main : process(clk, rst_n)
-- The POST-RELEASE view. A release written before the request in source
-- order is not applied before it in hardware: every read sees the
-- registered value, so a driver that closes one endpoint and opens
-- another in one cycle would be refused for a slot it just freed.
--
-- Being refused is not a functional failure -- the driver retries -- which
-- is why it is worth getting right: the symptom is a bandwidth limit that
-- appears only under load and cannot be reproduced.
variable rs_i : natural;
variable rel_hit : boolean;
variable rel_cost : unsigned(15 downto 0);
variable used_now : std_logic_vector(N_EP-1 downto 0);
variable per_now, ctl_now : unsigned(15 downto 0);
variable free_slot : natural;
variable have_free : boolean;
variable t : xfer_t;
variable cost : unsigned(15 downto 0);
variable per_req : boolean;
begin
if rst_n = '0' then
per_r <= (others => '0');
ctl_r <= (others => '0');
cnt_r <= (others => '0');
used_r <= (others => '0');
done_r <= '0';
ok_r <= '0';
why_r <= RS_OK;
slot_r <= (others => '0');
req_c <= (others => '0');
bw_c <= (others => '0');
iv_c <= (others => '0');
mp_c <= (others => '0');
full_c <= (others => '0');
over_c <= (others => '0');
slot_mp <= (others => (others => '0'));
slot_ty <= (others => XT_CTRL);
elsif rising_edge(clk) then
done_r <= '0';
ok_r <= '0';
why_r <= RS_OK;
-- ---- the post-release view, computed before anything decides ----
rs_i := to_integer(unsigned(rel_slot));
rel_hit := rel_valid = '1' and rs_i < N_EP and used_r(rs_i) = '1';
used_now := used_r;
per_now := per_r;
ctl_now := ctl_r;
if rel_hit then
rel_cost := frame_cost(slot_mp(rs_i));
used_now(rs_i) := '0';
if is_periodic_t(slot_ty(rs_i)) then
per_now := per_now - rel_cost;
elsif slot_ty(rs_i) = XT_CTRL then
ctl_now := ctl_now - rel_cost;
end if;
cnt_r <= cnt_r - 1;
end if;
per_r <= per_now;
ctl_r <= ctl_now;
used_r <= used_now;
-- ---- the first free slot, in the post-release view ----
free_slot := 0;
have_free := false;
for j in N_EP-1 downto 0 loop
if used_now(j) = '0' then
free_slot := j;
have_free := true;
end if;
end loop;
if req_valid = '1' then
t := xfer_of(req_type);
cost := frame_cost(unsigned(req_maxp));
per_req := is_periodic_t(t);
req_c <= req_c + 1;
done_r <= '1';
slot_r <= to_unsigned(free_slot, 4);
-- ---- the checks, cheapest and most specific first ----
--
-- A request that is wrong in two ways should report the one the
-- driver can act on.
if unsigned(req_maxp) > max_packet(t) then
ok_r <= '0';
why_r <= RS_MAXP;
mp_c <= mp_c + 1;
elsif per_req and not is_pow2(unsigned(req_interval)) then
ok_r <= '0';
why_r <= RS_INTERVAL;
iv_c <= iv_c + 1;
elsif not have_free then
ok_r <= '0';
why_r <= RS_FULL;
full_c <= full_c + 1;
elsif per_req and (per_now + cost) > to_unsigned(PERIODIC_MAX, 16) then
-- The 90% rule, and the reason isochronous is what it is: once the
-- periodic budget is spoken for nothing more gets in -- and nothing
-- already in can be retried either.
ok_r <= '0';
why_r <= RS_BW;
bw_c <= bw_c + 1;
elsif t = XT_CTRL and (ctl_now + cost) > to_unsigned(CONTROL_MIN, 16) then
-- The 10% rule, a SEPARATE limit rather than the remainder of the
-- periodic one: control has its own reservation precisely so that a
-- fully committed periodic schedule cannot stop the host talking to
-- its devices.
ok_r <= '0';
why_r <= RS_CTRL_ROOM;
bw_c <= bw_c + 1;
else
ok_r <= '1';
why_r <= RS_OK;
used_now(free_slot) := '1';
used_r <= used_now;
slot_mp(free_slot) <= unsigned(req_maxp);
slot_ty(free_slot) <= t;
if rel_hit then cnt_r <= cnt_r;
else cnt_r <= cnt_r + 1;
end if;
if per_req then
per_r <= per_now + cost;
elsif t = XT_CTRL then
ctl_r <= ctl_now + cost;
end if;
-- Bulk is charged NOTHING. It has no reservation, which is exactly
-- why it can retry: it uses whatever the frame has left after
-- everything with a promise has been served.
end if;
end if;
-- ---- the self-check that makes the whole thing measurable ----
if per_r > to_unsigned(PERIODIC_MAX, 16)
or ctl_r > to_unsigned(CONTROL_MIN, 16) then
over_c <= over_c + 1;
end if;
end if;
end process;
end architecture;10. The Testbench: A Model That Re-Sums
The shadow does not track the schedule incrementally. It keeps the set of admitted endpoints and re-sums the whole commitment from scratch every time it needs a total.
Design: per_r <= per_r + cost (incremental)
per_r <= per_r - released_cost
Shadow: sum over every admitted slot (re-summed)
A release that subtracts the wrong amount leaves the
design's total SELF-CONSISTENT and wrong. A model that
re-sums cannot make that mistake, so the two disagree
the moment the arithmetic drifts.That is the whole reason the model is written the expensive way. An incremental model would drift in exactly the same direction as an incremental design and agree with it perfectly.
Verilog-2005 testbench
// =====================================================================
// Testbench for usb_xfer_admit.
//
// The shadow model is written from the RULES rather than from the
// design's control flow: it recomputes, from scratch, what the total
// periodic and control commitment OUGHT to be given the set of
// endpoints currently admitted -- by summing over the slots rather than
// by tracking increments.
//
// That distinction matters. The design maintains running totals, so a
// release that subtracts the wrong amount leaves a total that is
// self-consistent and wrong. A model that re-sums cannot make that
// mistake, so the two disagree the moment the design's arithmetic drifts.
// =====================================================================
`timescale 1ns/1ps
module tb_xt_v;
localparam integer FRAME_BYTES = 1500;
localparam integer PERIODIC_MAX = 1350;
localparam integer CONTROL_MIN = 150;
localparam integer N_EP = 8;
localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;
localparam [2:0] R_OK = 3'd0, R_BW = 3'd1, R_INTERVAL = 3'd2,
R_MAXP = 3'd3, R_FULL = 3'd4, R_CTRL_ROOM = 3'd5;
reg clk = 1'b0, rst_n = 1'b0;
reg req_valid = 1'b0;
reg [1:0] req_type = X_BULK;
reg [10:0] req_maxp = 11'd0;
reg [7:0] req_interval = 8'd1;
reg rel_valid = 1'b0;
reg [3:0] rel_slot = 4'd0;
wire adm_done, adm_ok;
wire [2:0] adm_reason;
wire [3:0] adm_slot;
wire [15:0] used_periodic, used_control, n_admitted;
wire [31:0] n_req, n_reject_bw, n_reject_interval, n_reject_maxp,
n_reject_full, n_overcommit;
usb_xfer_admit #(.FRAME_BYTES(FRAME_BYTES), .PERIODIC_MAX(PERIODIC_MAX),
.CONTROL_MIN(CONTROL_MIN), .N_EP(N_EP)) dut (
.clk(clk), .rst_n(rst_n),
.req_valid(req_valid), .req_type(req_type),
.req_maxp(req_maxp), .req_interval(req_interval),
.rel_valid(rel_valid), .rel_slot(rel_slot),
.adm_done(adm_done), .adm_ok(adm_ok),
.adm_reason(adm_reason), .adm_slot(adm_slot),
.used_periodic(used_periodic), .used_control(used_control),
.n_admitted(n_admitted),
.n_req(n_req), .n_reject_bw(n_reject_bw),
.n_reject_interval(n_reject_interval), .n_reject_maxp(n_reject_maxp),
.n_reject_full(n_reject_full), .n_overcommit(n_overcommit)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0;
integer seed;
function [31:0] urand;
input dummy;
begin urand = $random(seed) & 32'h3FFF_FFFF; end
endfunction
// ---- the shadow: a SET of admitted endpoints, re-summed each time ----
reg s_used [0:N_EP-1];
reg [10:0] s_mp [0:N_EP-1];
reg [1:0] s_ty [0:N_EP-1];
reg [31:0] x_req, x_bw, x_iv, x_mp, x_full;
integer n_over = 0; // the headline: any overcommitted cycle
// Run-wide totals: the DUT's counters are cleared by every reset, so a
// summary printed from them describes the last window, not the run.
integer g_req = 0, g_bw = 0, g_iv = 0, g_mp = 0, g_full = 0, g_ok = 0;
reg reach [0:143];
integer ri, n_reach;
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
function [15:0] fcost;
input [10:0] mp;
begin fcost = {5'd0, mp} + 16'd13; end
endfunction
function [10:0] maxp_of;
input [1:0] t;
begin
if (t == X_ISO) maxp_of = 11'd1023;
else maxp_of = 11'd64;
end
endfunction
function pow2;
input [7:0] v;
begin pow2 = (v != 8'd0) && ((v & (v - 8'd1)) == 8'd0); end
endfunction
// ---- re-sum the whole schedule from the admitted set ----
//
// Never incremental. A design that subtracts the wrong amount on release
// stays self-consistent; a model that re-sums does not.
function [15:0] sum_periodic;
input dummy;
integer a;
begin
sum_periodic = 16'd0;
for (a = 0; a < N_EP; a = a + 1)
if (s_used[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT)))
sum_periodic = sum_periodic + fcost(s_mp[a]);
end
endfunction
function [15:0] sum_control;
input dummy;
integer a;
begin
sum_control = 16'd0;
for (a = 0; a < N_EP; a = a + 1)
if (s_used[a] && (s_ty[a] == X_CTRL))
sum_control = sum_control + fcost(s_mp[a]);
end
endfunction
function [15:0] count_used;
input dummy;
integer a;
begin
count_used = 16'd0;
for (a = 0; a < N_EP; a = a + 1) if (s_used[a]) count_used = count_used + 16'd1;
end
endfunction
function [3:0] first_free;
input dummy;
integer a;
begin
first_free = 4'd0;
for (a = N_EP - 1; a >= 0; a = a - 1) if (!s_used[a]) first_free = a[3:0];
end
endfunction
function have_free_f;
input dummy;
integer a;
begin
have_free_f = 1'b0;
for (a = 0; a < N_EP; a = a + 1) if (!s_used[a]) have_free_f = 1'b1;
end
endfunction
// ---------------------------------------------------------------
// One cycle: a release and/or a request, then check the decision.
// ---------------------------------------------------------------
task step(input rv, input [3:0] rs,
input qv, input [1:0] qt, input [10:0] qm, input [7:0] qi);
reg e_ok;
reg [2:0] e_why;
reg [3:0] e_slot;
reg e_per;
reg [15:0] e_cost;
begin
rel_valid = rv; rel_slot = rs;
req_valid = qv; req_type = qt; req_maxp = qm; req_interval = qi;
// ---- release first, exactly as the design orders it ----
if (rv && (rs < N_EP) && s_used[rs]) s_used[rs] = 1'b0;
e_per = (qt == X_ISO) || (qt == X_INT);
e_cost = fcost(qm);
e_ok = 1'b0;
e_why = R_OK;
e_slot = first_free(0);
if (qv) begin
x_req = x_req + 1; g_req = g_req + 1;
if (qm > maxp_of(qt)) begin
e_why = R_MAXP; x_mp = x_mp + 1; g_mp = g_mp + 1;
end else if (e_per && !pow2(qi)) begin
e_why = R_INTERVAL; x_iv = x_iv + 1; g_iv = g_iv + 1;
end else if (!have_free_f(0)) begin
e_why = R_FULL; x_full = x_full + 1; g_full = g_full + 1;
end else if (e_per && ((sum_periodic(0) + e_cost) > PERIODIC_MAX)) begin
e_why = R_BW; x_bw = x_bw + 1; g_bw = g_bw + 1;
end else if ((qt == X_CTRL) && ((sum_control(0) + e_cost) > CONTROL_MIN)) begin
e_why = R_CTRL_ROOM; x_bw = x_bw + 1; g_bw = g_bw + 1;
end else begin
e_ok = 1'b1; e_why = R_OK; g_ok = g_ok + 1;
s_used[e_slot] = 1'b1;
s_mp[e_slot] = qm;
s_ty[e_slot] = qt;
end
end
@(posedge clk);
#1;
steps = steps + 1;
rel_valid = 1'b0; req_valid = 1'b0;
// ---- PROPERTY 1: the decision and its reason ----
ck(adm_done === qv, "adm_done disagrees with whether a request was made");
if (qv) begin
ck(adm_ok === e_ok, "admission decision disagrees");
ck(adm_reason === e_why, "rejection reason disagrees");
if (e_ok) ck(adm_slot === e_slot, "admitted into the wrong slot");
end
// ---- PROPERTY 2: the schedule totals, RE-SUMMED ----
ck(used_periodic === sum_periodic(0), "periodic commitment disagrees");
ck(used_control === sum_control(0), "control commitment disagrees");
ck(n_admitted === count_used(0), "admitted count disagrees");
// ---- PROPERTY 3: the ceilings are never exceeded ----
if (used_periodic > PERIODIC_MAX) n_over = n_over + 1;
if (used_control > CONTROL_MIN) n_over = n_over + 1;
ck(used_periodic <= PERIODIC_MAX, "periodic budget overcommitted");
ck(used_control <= CONTROL_MIN, "control reservation overcommitted");
ck(n_overcommit === 32'd0, "the design detected its own overcommitment");
ck(n_over == 0, "the schedule was overcommitted");
// ---- PROPERTY 4: the counters agree ----
ck(n_req === x_req, "request count disagrees");
ck(n_reject_bw === x_bw, "bandwidth-rejection count disagrees");
ck(n_reject_interval === x_iv, "interval-rejection count disagrees");
ck(n_reject_maxp === x_mp, "maxp-rejection count disagrees");
ck(n_reject_full === x_full, "slot-full-rejection count disagrees");
end
endtask
task reset_dut;
integer a;
begin
rst_n = 1'b0;
req_valid = 0; rel_valid = 0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
for (a = 0; a < N_EP; a = a + 1) begin
s_used[a] = 1'b0; s_mp[a] = 11'd0; s_ty[a] = X_CTRL;
end
x_req = 0; x_bw = 0; x_iv = 0; x_mp = 0; x_full = 0;
@(posedge clk); #1;
end
endtask
integer ti, mi, ii, k, a3;
integer expect_n;
reg [10:0] mps [0:5];
reg [7:0] ivs [0:5];
initial begin
for (ri = 0; ri < 144; ri = ri + 1) reach[ri] = 1'b0;
// Boundary values on purpose: 64 and 65 straddle the control/bulk
// limit, 1023 and 1024 straddle the isochronous one.
mps[0] = 11'd8; mps[1] = 11'd64; mps[2] = 11'd65;
mps[3] = 11'd512; mps[4] = 11'd1023; mps[5] = 11'd1024;
// 1,2,4,8 are legal; 3 and 255 are not powers of two.
ivs[0] = 8'd1; ivs[1] = 8'd2; ivs[2] = 8'd3;
ivs[3] = 8'd4; ivs[4] = 8'd8; ivs[5] = 8'd255;
seed = 32'd27005;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every type against every
// packet size against every interval. 4 x 6 x 6 = 144.
// =============================================================
for (ti = 0; ti < 4; ti = ti + 1)
for (mi = 0; mi < 6; mi = mi + 1)
for (ii = 0; ii < 6; ii = ii + 1) begin
reset_dut;
step(1'b0, 4'd0, 1'b1, ti[1:0], mps[mi], ivs[ii]);
ri = (ti * 36) + (mi * 6) + ii;
reach[ri] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED) -- fill the periodic budget exactly, then
// ask for one more byte.
//
// This is the trade made arithmetic: the last endpoint that fits
// is admitted, the next is refused, and NOTHING already admitted
// can be retried -- because every slot is spoken for. That is
// what "guaranteed bandwidth" costs.
// =============================================================
reset_dut;
// 1023 + 13 = 1036 per iso endpoint; the ceiling is 1350
step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);
ck(adm_ok === 1'b1, "the first isochronous endpoint was refused");
// a second one would be 2072 > 1350
step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);
ck(adm_ok === 1'b0, "a second full-size isochronous endpoint was admitted");
ck(adm_reason === R_BW, "the refusal was not attributed to bandwidth");
// but a small interrupt endpoint still fits: 1036 + 8 + 13 = 1057
step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd8);
ck(adm_ok === 1'b1, "a small interrupt endpoint did not fit in the remainder");
// and bulk ALWAYS fits, because it is charged nothing at all
for (k = 0; k < 5; k = k + 1) begin
step(1'b0, 4'd0, 1'b1, X_BULK, 11'd64, 8'd0);
ck(adm_ok === 1'b1, "a bulk endpoint was refused for bandwidth");
ck(used_periodic === 16'd1057, "bulk was charged periodic bandwidth");
end
// =============================================================
// PHASE 3 (DIRECTED) -- the control reservation is SEPARATE.
//
// A fully committed periodic schedule must not stop the host
// talking to its devices, which is why control has its own 10%
// rather than sharing the periodic remainder.
// =============================================================
reset_dut;
step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1); // 1036 periodic
step(1'b0, 4'd0, 1'b1, X_CTRL, 11'd64, 8'd0); // 77 control
ck(adm_ok === 1'b1, "control was refused while its reservation was free");
ck(used_control === 16'd77, "control was charged to the wrong budget");
step(1'b0, 4'd0, 1'b1, X_CTRL, 11'd64, 8'd0); // 154 > 150
ck(adm_ok === 1'b0, "control exceeded its own reservation");
ck(adm_reason === R_CTRL_ROOM, "the refusal was not attributed to control room");
// =============================================================
// PHASE 4 (DIRECTED, EXHAUSTIVE) -- release returns EXACTLY
// what was charged, from every slot.
// =============================================================
for (k = 0; k < N_EP; k = k + 1) begin
reset_dut;
// fill every slot with a small interrupt endpoint
for (a3 = 0; a3 < N_EP; a3 = a3 + 1)
step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
ck(used_periodic === 16'd168, "eight 8-byte interrupt endpoints cost the wrong amount");
// the ninth is refused for want of a slot, not bandwidth
step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
ck(adm_ok === 1'b0, "a ninth endpoint was admitted into eight slots");
ck(adm_reason === R_FULL, "the refusal was not attributed to slots");
// release slot k and the commitment drops by exactly one endpoint
step(1'b1, k[3:0], 1'b0, X_BULK, 11'd0, 8'd0);
ck(used_periodic === 16'd147, "release returned the wrong amount");
// and a release in the SAME cycle as a request frees the slot for it
step(1'b1, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
end
// =============================================================
// PHASE 4b (DIRECTED, EXHAUSTIVE) -- walk the periodic budget from
// empty to refusal, for every packet size.
//
// The trade this chapter is about is arithmetic, so it deserves
// arithmetic stimulus: admit identical isochronous endpoints one at a
// time until the budget refuses one, checking at every step that the
// commitment is exactly the re-summed total and that the refusal
// arrives at precisely the right count.
//
// This is what catches a cost function that forgets the
// per-transaction overhead, or charges the average instead of the
// worst frame: both are off by a few bytes per endpoint and both show
// up as the budget refusing one endpoint too late.
// =============================================================
for (mi = 0; mi < 6; mi = mi + 1) begin
if (mps[mi] <= 11'd1023) begin
reset_dut;
expect_n = 0;
// how many of this size fit: floor(PERIODIC_MAX / (maxp + 13)),
// capped by the number of slots
while (((expect_n + 1) * (mps[mi] + 13) <= PERIODIC_MAX)
&& (expect_n < N_EP))
expect_n = expect_n + 1;
for (k = 0; k < expect_n; k = k + 1) begin
step(1'b0, 4'd0, 1'b1, X_ISO, mps[mi], 8'd1);
ck(adm_ok === 1'b1, "an endpoint that fits the budget was refused");
ck(used_periodic === ((k + 1) * (mps[mi] + 13)),
"the running commitment is not the sum of the admitted costs");
end
// the next one must be refused -- for bandwidth if the budget is
// what binds, for slots if we ran out of those first
step(1'b0, 4'd0, 1'b1, X_ISO, mps[mi], 8'd1);
ck(adm_ok === 1'b0, "one endpoint too many was admitted");
if (expect_n < N_EP)
ck(adm_reason === R_BW, "the refusal was not attributed to bandwidth");
else
ck(adm_reason === R_FULL, "the refusal was not attributed to slots");
ck(used_periodic === (expect_n * (mps[mi] + 13)),
"a refused endpoint changed the commitment");
end
end
// =============================================================
// PHASE 5 (RANDOM) -- a driver opening and closing endpoints.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 20000; k = k + 1) begin
step((urand(0) % 4) == 0, urand(0) % 16,
(urand(0) % 2) == 0, urand(0) % 4,
mps[urand(0) % 6], ivs[urand(0) % 6]);
if ((k % 256) == 255) reset_dut;
end
`endif
n_reach = 0;
for (ri = 0; ri < 144; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d reach=%0d/144 errors=%0d",
steps, checks, n_reach, errors);
$display("[sched] requests=%0d admitted=%0d bw=%0d interval=%0d maxp=%0d full=%0d",
g_req, g_ok, g_bw, g_iv, g_mp, g_full);
$display("[the whole point] overcommitted cycles = %0d", n_over);
if (n_reach != 144) 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
endmoduleSystemVerilog testbench
// =====================================================================
// Testbench for usb_xfer_admit.
//
// The shadow model is written from the RULES rather than from the
// design's control flow: it recomputes, from scratch, what the total
// periodic and control commitment OUGHT to be given the set of
// endpoints currently admitted -- by summing over the slots rather than
// by tracking increments.
//
// That distinction matters. The design maintains running totals, so a
// release that subtracts the wrong amount leaves a total that is
// self-consistent and wrong. A model that re-sums cannot make that
// mistake, so the two disagree the moment the design's arithmetic drifts.
// =====================================================================
`timescale 1ns/1ps
module tb_xt_sv;
localparam integer FRAME_BYTES = 1500;
localparam integer PERIODIC_MAX = 1350;
localparam integer CONTROL_MIN = 150;
localparam integer N_EP = 8;
localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;
localparam [2:0] R_OK = 3'd0, R_BW = 3'd1, R_INTERVAL = 3'd2,
R_MAXP = 3'd3, R_FULL = 3'd4, R_CTRL_ROOM = 3'd5;
logic clk = 1'b0, rst_n = 1'b0;
logic req_valid = 1'b0;
logic [1:0] req_type = X_BULK;
logic [10:0] req_maxp = 11'd0;
logic [7:0] req_interval = 8'd1;
logic rel_valid = 1'b0;
logic [3:0] rel_slot = 4'd0;
logic adm_done, adm_ok;
logic [2:0] adm_reason;
logic [3:0] adm_slot;
logic [15:0] used_periodic, used_control, n_admitted;
logic [31:0] n_req, n_reject_bw, n_reject_interval, n_reject_maxp,
n_reject_full, n_overcommit;
usb_xfer_admit #(.FRAME_BYTES(FRAME_BYTES), .PERIODIC_MAX(PERIODIC_MAX),
.CONTROL_MIN(CONTROL_MIN), .N_EP(N_EP)) dut (
.clk(clk), .rst_n(rst_n),
.req_valid(req_valid), .req_type(req_type),
.req_maxp(req_maxp), .req_interval(req_interval),
.rel_valid(rel_valid), .rel_slot(rel_slot),
.adm_done(adm_done), .adm_ok(adm_ok),
.adm_reason(adm_reason), .adm_slot(adm_slot),
.used_periodic(used_periodic), .used_control(used_control),
.n_admitted(n_admitted),
.n_req(n_req), .n_reject_bw(n_reject_bw),
.n_reject_interval(n_reject_interval), .n_reject_maxp(n_reject_maxp),
.n_reject_full(n_reject_full), .n_overcommit(n_overcommit)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0;
integer seed;
function automatic logic [31:0] urand(bit dummy);
return $random(seed) & 32'h3FFF_FFFF;
endfunction
// ---- the shadow: a SET of admitted endpoints, re-summed each time ----
logic s_used [0:N_EP-1];
logic [10:0] s_mp [0:N_EP-1];
logic [1:0] s_ty [0:N_EP-1];
logic [31:0] x_req, x_bw, x_iv, x_mp, x_full;
integer n_over = 0; // the headline: any overcommitted cycle
// Run-wide totals: the DUT's counters are cleared by every reset, so a
// summary printed from them describes the last window, not the run.
integer g_req = 0, g_bw = 0, g_iv = 0, g_mp = 0, g_full = 0, g_ok = 0;
logic reach [0:143];
integer ri, n_reach;
task ck(input logic cond, input logic [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
function automatic logic [15:0] fcost(logic [10:0] mp);
return {5'd0, mp} + 16'd13;
endfunction
function automatic logic [10:0] maxp_of(logic [1:0] t);
if (t == X_ISO) return 11'd1023;
return 11'd64;
endfunction
function automatic logic pow2(logic [7:0] v);
return (v != 8'd0) && ((v & (v - 8'd1)) == 8'd0);
endfunction
// ---- re-sum the whole schedule from the admitted set ----
//
// Never incremental. A design that subtracts the wrong amount on release
// stays self-consistent; a model that re-sums does not.
function automatic logic [15:0] sum_periodic(bit dummy);
integer a;
begin
sum_periodic = 16'd0;
for (a = 0; a < N_EP; a = a + 1)
if (s_used[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT)))
sum_periodic = sum_periodic + fcost(s_mp[a]);
end
endfunction
function automatic logic [15:0] sum_control(bit dummy);
integer a;
begin
sum_control = 16'd0;
for (a = 0; a < N_EP; a = a + 1)
if (s_used[a] && (s_ty[a] == X_CTRL))
sum_control = sum_control + fcost(s_mp[a]);
end
endfunction
function automatic logic [15:0] count_used(bit dummy);
integer a;
begin
count_used = 16'd0;
for (a = 0; a < N_EP; a = a + 1) if (s_used[a]) count_used = count_used + 16'd1;
end
endfunction
function automatic logic [3:0] first_free(bit dummy);
integer a;
begin
first_free = 4'd0;
for (a = N_EP - 1; a >= 0; a = a - 1) if (!s_used[a]) first_free = a[3:0];
end
endfunction
function automatic logic have_free_f(bit dummy);
integer a;
begin
have_free_f = 1'b0;
for (a = 0; a < N_EP; a = a + 1) if (!s_used[a]) have_free_f = 1'b1;
end
endfunction
// ---------------------------------------------------------------
// One cycle: a release and/or a request, then check the decision.
// ---------------------------------------------------------------
task step(input logic rv, input logic [3:0] rs,
input logic qv, input logic [1:0] qt,
input logic [10:0] qm, input logic [7:0] qi);
logic e_ok;
logic [2:0] e_why;
logic [3:0] e_slot;
logic e_per;
logic [15:0] e_cost;
begin
rel_valid = rv; rel_slot = rs;
req_valid = qv; req_type = qt; req_maxp = qm; req_interval = qi;
// ---- release first, exactly as the design orders it ----
if (rv && (rs < N_EP) && s_used[rs]) s_used[rs] = 1'b0;
e_per = (qt == X_ISO) || (qt == X_INT);
e_cost = fcost(qm);
e_ok = 1'b0;
e_why = R_OK;
e_slot = first_free(0);
if (qv) begin
x_req = x_req + 1; g_req = g_req + 1;
if (qm > maxp_of(qt)) begin
e_why = R_MAXP; x_mp = x_mp + 1; g_mp = g_mp + 1;
end else if (e_per && !pow2(qi)) begin
e_why = R_INTERVAL; x_iv = x_iv + 1; g_iv = g_iv + 1;
end else if (!have_free_f(0)) begin
e_why = R_FULL; x_full = x_full + 1; g_full = g_full + 1;
end else if (e_per && ((sum_periodic(0) + e_cost) > PERIODIC_MAX)) begin
e_why = R_BW; x_bw = x_bw + 1; g_bw = g_bw + 1;
end else if ((qt == X_CTRL) && ((sum_control(0) + e_cost) > CONTROL_MIN)) begin
e_why = R_CTRL_ROOM; x_bw = x_bw + 1; g_bw = g_bw + 1;
end else begin
e_ok = 1'b1; e_why = R_OK; g_ok = g_ok + 1;
s_used[e_slot] = 1'b1;
s_mp[e_slot] = qm;
s_ty[e_slot] = qt;
end
end
@(posedge clk);
#1;
steps = steps + 1;
rel_valid = 1'b0; req_valid = 1'b0;
// ---- PROPERTY 1: the decision and its reason ----
ck(adm_done === qv, "adm_done disagrees with whether a request was made");
if (qv) begin
ck(adm_ok === e_ok, "admission decision disagrees");
ck(adm_reason === e_why, "rejection reason disagrees");
if (e_ok) ck(adm_slot === e_slot, "admitted into the wrong slot");
end
// ---- PROPERTY 2: the schedule totals, RE-SUMMED ----
ck(used_periodic === sum_periodic(0), "periodic commitment disagrees");
ck(used_control === sum_control(0), "control commitment disagrees");
ck(n_admitted === count_used(0), "admitted count disagrees");
// ---- PROPERTY 3: the ceilings are never exceeded ----
if (used_periodic > PERIODIC_MAX) n_over = n_over + 1;
if (used_control > CONTROL_MIN) n_over = n_over + 1;
ck(used_periodic <= PERIODIC_MAX, "periodic budget overcommitted");
ck(used_control <= CONTROL_MIN, "control reservation overcommitted");
ck(n_overcommit === 32'd0, "the design detected its own overcommitment");
ck(n_over == 0, "the schedule was overcommitted");
// ---- PROPERTY 4: the counters agree ----
ck(n_req === x_req, "request count disagrees");
ck(n_reject_bw === x_bw, "bandwidth-rejection count disagrees");
ck(n_reject_interval === x_iv, "interval-rejection count disagrees");
ck(n_reject_maxp === x_mp, "maxp-rejection count disagrees");
ck(n_reject_full === x_full, "slot-full-rejection count disagrees");
end
endtask
task reset_dut;
integer a;
begin
rst_n = 1'b0;
req_valid = 0; rel_valid = 0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
for (a = 0; a < N_EP; a = a + 1) begin
s_used[a] = 1'b0; s_mp[a] = 11'd0; s_ty[a] = X_CTRL;
end
x_req = 0; x_bw = 0; x_iv = 0; x_mp = 0; x_full = 0;
@(posedge clk); #1;
end
endtask
integer ti, mi, ii, k, a3;
integer expect_n;
logic [10:0] mps [0:5];
logic [7:0] ivs [0:5];
initial begin
for (ri = 0; ri < 144; ri = ri + 1) reach[ri] = 1'b0;
// Boundary values on purpose: 64 and 65 straddle the control/bulk
// limit, 1023 and 1024 straddle the isochronous one.
mps[0] = 11'd8; mps[1] = 11'd64; mps[2] = 11'd65;
mps[3] = 11'd512; mps[4] = 11'd1023; mps[5] = 11'd1024;
// 1,2,4,8 are legal; 3 and 255 are not powers of two.
ivs[0] = 8'd1; ivs[1] = 8'd2; ivs[2] = 8'd3;
ivs[3] = 8'd4; ivs[4] = 8'd8; ivs[5] = 8'd255;
seed = 32'd27005;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every type against every
// packet size against every interval. 4 x 6 x 6 = 144.
// =============================================================
for (ti = 0; ti < 4; ti = ti + 1)
for (mi = 0; mi < 6; mi = mi + 1)
for (ii = 0; ii < 6; ii = ii + 1) begin
reset_dut;
step(1'b0, 4'd0, 1'b1, ti[1:0], mps[mi], ivs[ii]);
ri = (ti * 36) + (mi * 6) + ii;
reach[ri] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED) -- fill the periodic budget exactly, then
// ask for one more byte.
//
// This is the trade made arithmetic: the last endpoint that fits
// is admitted, the next is refused, and NOTHING already admitted
// can be retried -- because every slot is spoken for. That is
// what "guaranteed bandwidth" costs.
// =============================================================
reset_dut;
// 1023 + 13 = 1036 per iso endpoint; the ceiling is 1350
step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);
ck(adm_ok === 1'b1, "the first isochronous endpoint was refused");
// a second one would be 2072 > 1350
step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);
ck(adm_ok === 1'b0, "a second full-size isochronous endpoint was admitted");
ck(adm_reason === R_BW, "the refusal was not attributed to bandwidth");
// but a small interrupt endpoint still fits: 1036 + 8 + 13 = 1057
step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd8);
ck(adm_ok === 1'b1, "a small interrupt endpoint did not fit in the remainder");
// and bulk ALWAYS fits, because it is charged nothing at all
for (k = 0; k < 5; k = k + 1) begin
step(1'b0, 4'd0, 1'b1, X_BULK, 11'd64, 8'd0);
ck(adm_ok === 1'b1, "a bulk endpoint was refused for bandwidth");
ck(used_periodic === 16'd1057, "bulk was charged periodic bandwidth");
end
// =============================================================
// PHASE 3 (DIRECTED) -- the control reservation is SEPARATE.
//
// A fully committed periodic schedule must not stop the host
// talking to its devices, which is why control has its own 10%
// rather than sharing the periodic remainder.
// =============================================================
reset_dut;
step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1); // 1036 periodic
step(1'b0, 4'd0, 1'b1, X_CTRL, 11'd64, 8'd0); // 77 control
ck(adm_ok === 1'b1, "control was refused while its reservation was free");
ck(used_control === 16'd77, "control was charged to the wrong budget");
step(1'b0, 4'd0, 1'b1, X_CTRL, 11'd64, 8'd0); // 154 > 150
ck(adm_ok === 1'b0, "control exceeded its own reservation");
ck(adm_reason === R_CTRL_ROOM, "the refusal was not attributed to control room");
// =============================================================
// PHASE 4 (DIRECTED, EXHAUSTIVE) -- release returns EXACTLY
// what was charged, from every slot.
// =============================================================
for (k = 0; k < N_EP; k = k + 1) begin
reset_dut;
// fill every slot with a small interrupt endpoint
for (a3 = 0; a3 < N_EP; a3 = a3 + 1)
step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
ck(used_periodic === 16'd168, "eight 8-byte interrupt endpoints cost the wrong amount");
// the ninth is refused for want of a slot, not bandwidth
step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
ck(adm_ok === 1'b0, "a ninth endpoint was admitted into eight slots");
ck(adm_reason === R_FULL, "the refusal was not attributed to slots");
// release slot k and the commitment drops by exactly one endpoint
step(1'b1, k[3:0], 1'b0, X_BULK, 11'd0, 8'd0);
ck(used_periodic === 16'd147, "release returned the wrong amount");
// and a release in the SAME cycle as a request frees the slot for it
step(1'b1, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
end
// =============================================================
// PHASE 4b (DIRECTED, EXHAUSTIVE) -- walk the periodic budget from
// empty to refusal, for every packet size.
//
// The trade this chapter is about is arithmetic, so it deserves
// arithmetic stimulus: admit identical isochronous endpoints one at a
// time until the budget refuses one, checking at every step that the
// commitment is exactly the re-summed total and that the refusal
// arrives at precisely the right count.
//
// This is what catches a cost function that forgets the
// per-transaction overhead, or charges the average instead of the
// worst frame: both are off by a few bytes per endpoint and both show
// up as the budget refusing one endpoint too late.
// =============================================================
for (mi = 0; mi < 6; mi = mi + 1) begin
if (mps[mi] <= 11'd1023) begin
reset_dut;
expect_n = 0;
// how many of this size fit: floor(PERIODIC_MAX / (maxp + 13)),
// capped by the number of slots
while (((expect_n + 1) * (mps[mi] + 13) <= PERIODIC_MAX)
&& (expect_n < N_EP))
expect_n = expect_n + 1;
for (k = 0; k < expect_n; k = k + 1) begin
step(1'b0, 4'd0, 1'b1, X_ISO, mps[mi], 8'd1);
ck(adm_ok === 1'b1, "an endpoint that fits the budget was refused");
ck(used_periodic === ((k + 1) * (mps[mi] + 13)),
"the running commitment is not the sum of the admitted costs");
end
// the next one must be refused -- for bandwidth if the budget is
// what binds, for slots if we ran out of those first
step(1'b0, 4'd0, 1'b1, X_ISO, mps[mi], 8'd1);
ck(adm_ok === 1'b0, "one endpoint too many was admitted");
if (expect_n < N_EP)
ck(adm_reason === R_BW, "the refusal was not attributed to bandwidth");
else
ck(adm_reason === R_FULL, "the refusal was not attributed to slots");
ck(used_periodic === (expect_n * (mps[mi] + 13)),
"a refused endpoint changed the commitment");
end
end
// =============================================================
// PHASE 5 (RANDOM) -- a driver opening and closing endpoints.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 20000; k = k + 1) begin
step((urand(0) % 4) == 0, urand(0) % 16,
(urand(0) % 2) == 0, urand(0) % 4,
mps[urand(0) % 6], ivs[urand(0) % 6]);
if ((k % 256) == 255) reset_dut;
end
`endif
n_reach = 0;
for (ri = 0; ri < 144; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d reach=%0d/144 errors=%0d",
steps, checks, n_reach, errors);
$display("[sched] requests=%0d admitted=%0d bw=%0d interval=%0d maxp=%0d full=%0d",
g_req, g_ok, g_bw, g_iv, g_mp, g_full);
$display("[the whole point] overcommitted cycles = %0d", n_over);
if (n_reach != 144) 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
endmoduleVHDL-2008 testbench
-- =====================================================================
-- Testbench for usb_xfer_admit (VHDL-2008).
--
-- The shadow re-sums the whole schedule from the set of admitted
-- endpoints rather than tracking increments. That distinction is the
-- point: the design maintains running totals, so a release that
-- subtracts the wrong amount leaves a total that is self-consistent and
-- wrong. A model that re-sums cannot make that mistake.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.xt_pkg.all;
entity tb_xt_vhdl is
generic (DIRECTED_ONLY : boolean := false);
end entity;
architecture sim of tb_xt_vhdl is
constant PERIODIC_MAX : natural := 1350;
constant CONTROL_MIN : natural := 150;
constant N_EP : natural := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal req_valid : std_logic := '0';
signal req_type : std_logic_vector(1 downto 0) := "11";
signal req_maxp : std_logic_vector(10 downto 0) := (others => '0');
signal req_interval : std_logic_vector(7 downto 0) := x"01";
signal rel_valid : std_logic := '0';
signal rel_slot : std_logic_vector(3 downto 0) := (others => '0');
signal adm_done, adm_ok : std_logic;
signal adm_reason : std_logic_vector(2 downto 0);
signal adm_slot : std_logic_vector(3 downto 0);
signal used_periodic, used_control, n_admitted : std_logic_vector(15 downto 0);
signal n_req, n_reject_bw, n_reject_interval, n_reject_maxp,
n_reject_full, n_overcommit : std_logic_vector(31 downto 0);
signal done : boolean := false;
begin
dut : entity work.usb_xfer_admit
generic map (PERIODIC_MAX => PERIODIC_MAX, CONTROL_MIN => CONTROL_MIN,
N_EP => N_EP)
port map (
clk => clk, rst_n => rst_n,
req_valid => req_valid, req_type => req_type,
req_maxp => req_maxp, req_interval => req_interval,
rel_valid => rel_valid, rel_slot => rel_slot,
adm_done => adm_done, adm_ok => adm_ok,
adm_reason => adm_reason, adm_slot => adm_slot,
used_periodic => used_periodic, used_control => used_control,
n_admitted => n_admitted,
n_req => n_req, n_reject_bw => n_reject_bw,
n_reject_interval => n_reject_interval, n_reject_maxp => n_reject_maxp,
n_reject_full => n_reject_full, n_overcommit => n_overcommit);
clk <= not clk after 5 ns when not done else '0';
stim : process
variable errors : natural := 0;
variable checks : natural := 0;
variable steps : natural := 0;
variable n_over : natural := 0;
type used_arr is array (0 to N_EP-1) of boolean;
variable s_used : used_arr := (others => false);
variable s_mp : mp_array(0 to N_EP-1) := (others => (others => '0'));
variable s_ty : ty_array(0 to N_EP-1) := (others => XT_CTRL);
variable x_req, x_bw, x_iv, x_mp, x_full : natural := 0;
variable g_req, g_bw, g_iv, g_mp, g_full, g_ok : natural := 0;
variable reach : std_logic_vector(0 to 143) := (others => '0');
variable n_reach : natural := 0;
variable rnd : unsigned(31 downto 0) := x"00051D7B";
variable ln : line;
procedure ck(cond : boolean; what : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 20 then
write(ln, string'(" ERROR step=") & integer'image(steps)
& string'(": ") & what);
writeline(output, ln);
end if;
end if;
end procedure;
-- boolean to std_logic: VHDL has no implicit conversion, and a
-- conditional expression in argument position is VHDL-2019, not 2008.
function sl_of(b : boolean) return std_logic is
begin
if b then return '1'; else return '0'; end if;
end function;
impure function nxt return natural is
begin
rnd := rnd xor (rnd sll 13);
rnd := rnd xor (rnd srl 17);
rnd := rnd xor (rnd sll 5);
return to_integer(rnd(14 downto 0));
end function;
function fcost(mp : unsigned(10 downto 0)) return natural is
begin
return to_integer(mp) + TXN_OVERHEAD;
end function;
function maxp_of(t : xfer_t) return natural is
begin
if t = XT_ISO then return 1023; else return 64; end if;
end function;
-- VHDL has no bitwise AND on NATURAL, so the trick the design uses on
-- an `unsigned` is not available here. Enumerating the eight legal
-- 8-bit values is clearer than converting back and forth, and it is an
-- independent formulation rather than a copy of the design's.
function pow2(v : natural) return boolean is
begin
return v = 1 or v = 2 or v = 4 or v = 8
or v = 16 or v = 32 or v = 64 or v = 128;
end function;
-- Re-sum the whole schedule from the admitted set. NEVER incremental.
impure function sum_periodic return natural is
variable t : natural := 0;
begin
for a in 0 to N_EP-1 loop
if s_used(a) and is_periodic_t(s_ty(a)) then
t := t + fcost(s_mp(a));
end if;
end loop;
return t;
end function;
impure function sum_control return natural is
variable t : natural := 0;
begin
for a in 0 to N_EP-1 loop
if s_used(a) and s_ty(a) = XT_CTRL then
t := t + fcost(s_mp(a));
end if;
end loop;
return t;
end function;
impure function count_used return natural is
variable t : natural := 0;
begin
for a in 0 to N_EP-1 loop
if s_used(a) then t := t + 1; end if;
end loop;
return t;
end function;
impure function first_free return natural is
variable f : natural := 0;
begin
for a in N_EP-1 downto 0 loop
if not s_used(a) then f := a; end if;
end loop;
return f;
end function;
impure function have_free return boolean is
begin
for a in 0 to N_EP-1 loop
if not s_used(a) then return true; end if;
end loop;
return false;
end function;
procedure step(rv : std_logic; rs : natural;
qv : std_logic; qt : xfer_t;
qm : natural; qi : natural) is
variable e_ok : boolean;
variable e_why : reason_t;
variable e_slot : natural;
variable e_per : boolean;
variable e_cost : natural;
begin
rel_valid <= rv; rel_slot <= std_logic_vector(to_unsigned(rs, 4));
req_valid <= qv;
case qt is
when XT_CTRL => req_type <= "00";
when XT_ISO => req_type <= "01";
when XT_INT => req_type <= "10";
when XT_BULK => req_type <= "11";
end case;
req_maxp <= std_logic_vector(to_unsigned(qm, 11));
req_interval <= std_logic_vector(to_unsigned(qi, 8));
-- release first, exactly as the design orders it
if rv = '1' and rs < N_EP and s_used(rs) then s_used(rs) := false; end if;
e_per := is_periodic_t(qt);
e_cost := qm + TXN_OVERHEAD;
e_ok := false;
e_why := RS_OK;
e_slot := first_free;
if qv = '1' then
x_req := x_req + 1; g_req := g_req + 1;
if qm > maxp_of(qt) then
e_why := RS_MAXP; x_mp := x_mp + 1; g_mp := g_mp + 1;
elsif e_per and not pow2(qi) then
e_why := RS_INTERVAL; x_iv := x_iv + 1; g_iv := g_iv + 1;
elsif not have_free then
e_why := RS_FULL; x_full := x_full + 1; g_full := g_full + 1;
elsif e_per and (sum_periodic + e_cost) > PERIODIC_MAX then
e_why := RS_BW; x_bw := x_bw + 1; g_bw := g_bw + 1;
elsif qt = XT_CTRL and (sum_control + e_cost) > CONTROL_MIN then
e_why := RS_CTRL_ROOM; x_bw := x_bw + 1; g_bw := g_bw + 1;
else
e_ok := true; e_why := RS_OK; g_ok := g_ok + 1;
s_used(e_slot) := true;
s_mp(e_slot) := to_unsigned(qm, 11);
s_ty(e_slot) := qt;
end if;
end if;
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
rel_valid <= '0'; req_valid <= '0';
ck((adm_done = '1') = (qv = '1'),
"adm_done disagrees with whether a request was made");
if qv = '1' then
ck((adm_ok = '1') = e_ok, "admission decision disagrees");
ck(adm_reason = code_of(e_why), "rejection reason disagrees");
if e_ok then
ck(to_integer(unsigned(adm_slot)) = e_slot, "admitted into the wrong slot");
end if;
end if;
ck(to_integer(unsigned(used_periodic)) = sum_periodic,
"periodic commitment disagrees");
ck(to_integer(unsigned(used_control)) = sum_control,
"control commitment disagrees");
ck(to_integer(unsigned(n_admitted)) = count_used,
"admitted count disagrees");
if to_integer(unsigned(used_periodic)) > PERIODIC_MAX then
n_over := n_over + 1;
end if;
if to_integer(unsigned(used_control)) > CONTROL_MIN then
n_over := n_over + 1;
end if;
ck(to_integer(unsigned(used_periodic)) <= PERIODIC_MAX,
"periodic budget overcommitted");
ck(to_integer(unsigned(used_control)) <= CONTROL_MIN,
"control reservation overcommitted");
ck(to_integer(unsigned(n_overcommit)) = 0,
"the design detected its own overcommitment");
ck(n_over = 0, "the schedule was overcommitted");
ck(to_integer(unsigned(n_req)) = x_req, "request count disagrees");
ck(to_integer(unsigned(n_reject_bw)) = x_bw, "bandwidth-rejection count disagrees");
ck(to_integer(unsigned(n_reject_interval)) = x_iv, "interval-rejection count disagrees");
ck(to_integer(unsigned(n_reject_maxp)) = x_mp, "maxp-rejection count disagrees");
ck(to_integer(unsigned(n_reject_full)) = x_full, "slot-full-rejection count disagrees");
end procedure;
procedure reset_dut is
begin
rst_n <= '0';
req_valid <= '0'; rel_valid <= '0';
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
s_used := (others => false);
s_mp := (others => (others => '0'));
s_ty := (others => XT_CTRL);
x_req := 0; x_bw := 0; x_iv := 0; x_mp := 0; x_full := 0;
wait until rising_edge(clk);
wait for 1 ns;
end procedure;
-- Boundary values on purpose: 64/65 straddle the control and bulk limit,
-- 1023/1024 straddle the isochronous one. 3 and 255 are not powers of two.
type nat6 is array (0 to 5) of natural;
constant mps : nat6 := (8, 64, 65, 512, 1023, 1024);
constant ivs : nat6 := (1, 2, 3, 4, 8, 255);
type ty4 is array (0 to 3) of xfer_t;
constant tys : ty4 := (XT_CTRL, XT_ISO, XT_INT, XT_BULK);
variable ri : natural;
variable expect_n : natural;
begin
-- PHASE 1 (DIRECTED, EXHAUSTIVE) -- 4 types x 6 sizes x 6 intervals
for ti in 0 to 3 loop
for mi in 0 to 5 loop
for ii in 0 to 5 loop
reset_dut;
step('0', 0, '1', tys(ti), mps(mi), ivs(ii));
ri := ti*36 + mi*6 + ii;
reach(ri) := '1';
end loop;
end loop;
end loop;
-- PHASE 2 (DIRECTED) -- fill the periodic budget, then ask for one more
reset_dut;
step('0', 0, '1', XT_ISO, 1023, 1);
ck(adm_ok = '1', "the first isochronous endpoint was refused");
step('0', 0, '1', XT_ISO, 1023, 1);
ck(adm_ok = '0', "a second full-size isochronous endpoint was admitted");
ck(adm_reason = code_of(RS_BW), "the refusal was not attributed to bandwidth");
step('0', 0, '1', XT_INT, 8, 8);
ck(adm_ok = '1', "a small interrupt endpoint did not fit in the remainder");
for k in 0 to 4 loop
step('0', 0, '1', XT_BULK, 64, 0);
ck(adm_ok = '1', "a bulk endpoint was refused for bandwidth");
ck(to_integer(unsigned(used_periodic)) = 1057,
"bulk was charged periodic bandwidth");
end loop;
-- PHASE 3 (DIRECTED) -- the control reservation is SEPARATE
reset_dut;
step('0', 0, '1', XT_ISO, 1023, 1);
step('0', 0, '1', XT_CTRL, 64, 0);
ck(adm_ok = '1', "control was refused while its reservation was free");
ck(to_integer(unsigned(used_control)) = 77,
"control was charged to the wrong budget");
step('0', 0, '1', XT_CTRL, 64, 0);
ck(adm_ok = '0', "control exceeded its own reservation");
ck(adm_reason = code_of(RS_CTRL_ROOM),
"the refusal was not attributed to control room");
-- PHASE 4 (DIRECTED, EXHAUSTIVE) -- release returns exactly what was charged
for k in 0 to N_EP-1 loop
reset_dut;
for a in 0 to N_EP-1 loop
step('0', 0, '1', XT_INT, 8, 4);
end loop;
ck(to_integer(unsigned(used_periodic)) = 168,
"eight 8-byte interrupt endpoints cost the wrong amount");
step('0', 0, '1', XT_INT, 8, 4);
ck(adm_ok = '0', "a ninth endpoint was admitted into eight slots");
ck(adm_reason = code_of(RS_FULL), "the refusal was not attributed to slots");
step('1', k, '0', XT_BULK, 0, 0);
ck(to_integer(unsigned(used_periodic)) = 147, "release returned the wrong amount");
step('1', 0, '1', XT_INT, 8, 4);
end loop;
-- PHASE 4b (DIRECTED, EXHAUSTIVE) -- walk the periodic budget from empty
-- to refusal, for every packet size.
--
-- The trade this chapter is about is arithmetic, so it deserves
-- arithmetic stimulus: admit identical isochronous endpoints one at a
-- time until the budget refuses one, checking at every step that the
-- commitment is exactly the re-summed total and that the refusal arrives
-- at precisely the right count. This is what catches a cost function that
-- forgets the per-transaction overhead or charges the average frame.
for mi in 0 to 5 loop
if mps(mi) <= 1023 then
reset_dut;
expect_n := 0;
while ((expect_n + 1) * (mps(mi) + TXN_OVERHEAD) <= PERIODIC_MAX)
and expect_n < N_EP loop
expect_n := expect_n + 1;
end loop;
for k in 0 to expect_n - 1 loop
step('0', 0, '1', XT_ISO, mps(mi), 1);
ck(adm_ok = '1', "an endpoint that fits the budget was refused");
ck(to_integer(unsigned(used_periodic))
= (k + 1) * (mps(mi) + TXN_OVERHEAD),
"the running commitment is not the sum of the admitted costs");
end loop;
step('0', 0, '1', XT_ISO, mps(mi), 1);
ck(adm_ok = '0', "one endpoint too many was admitted");
if expect_n < N_EP then
ck(adm_reason = code_of(RS_BW), "the refusal was not attributed to bandwidth");
else
ck(adm_reason = code_of(RS_FULL), "the refusal was not attributed to slots");
end if;
ck(to_integer(unsigned(used_periodic)) = expect_n * (mps(mi) + TXN_OVERHEAD),
"a refused endpoint changed the commitment");
end if;
end loop;
-- PHASE 5 (RANDOM)
if not DIRECTED_ONLY then
reset_dut;
for k in 0 to 19999 loop
step(sl_of(nxt mod 4 = 0), nxt mod 16,
sl_of(nxt mod 2 = 0), tys(nxt mod 4),
mps(nxt mod 6), ivs(nxt mod 6));
if (k mod 256) = 255 then reset_dut; end if;
end loop;
end if;
n_reach := 0;
for i in 0 to 143 loop
if reach(i) = '1' then n_reach := n_reach + 1; end if;
end loop;
write(ln, string'("steps=") & integer'image(steps)
& string'(" checks=") & integer'image(checks)
& string'(" reach=") & integer'image(n_reach) & string'("/144")
& string'(" errors=") & integer'image(errors));
writeline(output, ln);
write(ln, string'("[sched] requests=") & integer'image(g_req)
& string'(" admitted=") & integer'image(g_ok)
& string'(" bw=") & integer'image(g_bw)
& string'(" interval=") & integer'image(g_iv)
& string'(" maxp=") & integer'image(g_mp)
& string'(" full=") & integer'image(g_full));
writeline(output, ln);
write(ln, string'("[the whole point] overcommitted cycles = ")
& integer'image(n_over));
writeline(output, ln);
if n_reach /= 144 then
write(ln, string'("FAIL: exhaustive sweep incomplete"));
writeline(output, ln);
errors := errors + 1;
end if;
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
& string'(" checks"));
else
write(ln, string'("FAIL: ") & integer'image(errors)
& string'(" errors in ") & integer'image(checks) & string'(" checks"));
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture;11. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| (type × packet size × interval) reached | 144 / 144 | 144 / 144 | 144 / 144 |
| budget walks (empty to refusal) | 5 / 5 | 5 / 5 | 5 / 5 |
| release-from-every-slot sweeps | 8 / 8 | 8 / 8 | 8 / 8 |
| Steps | 20275 | 20275 | 20275 |
| Checks executed | 286696 | 286696 | 286778 |
| admission requests | 10235 | 10235 | 10299 |
| admitted | 2532 | 2532 | 2486 |
| refused — bandwidth or control room | 359 | 359 | 346 |
| refused — illegal interval | 1002 | 1002 | 993 |
| refused — packet too large | 5444 | 5444 | 5547 |
| refused — no slot | 898 | 898 | 927 |
| overcommitted cycles | 0 | 0 | 0 |
| Result | PASS | PASS | PASS |
The exhaustive sweep uses boundary values rather than convenient ones: 64 and 65 straddle the control and bulk limit, 1023 and 1024 straddle the isochronous one, and 3 and 255 are the non-powers-of-two among the intervals. A sweep over 8, 64, 512 would have missed every off-by-one in the file.
12. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| E7 | the average frame is charged instead of the worst | 146104 | 146104 | 129021 |
| E5 | a periodic interval need not be a power of two | 76866 | 76866 | 77752 |
| E2 | the 90% periodic ceiling is dropped | 74066 | 74066 | 73656 |
| E6 | every type gets the isochronous packet limit | 64357 | 64357 | 65648 |
| E1 | bulk is charged periodic bandwidth | 60608 | 60608 | 56323 |
| E3 | control shares the periodic remainder | 57459 | 57459 | 59472 |
| E4 | the per-transaction overhead is ignored | 52439 | 52439 | 53074 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages.
E7 scores highest, and it is the most seductive error in the design. Dividing the cost by the interval looks exactly like the right thing to do — the endpoint really does move fewer bytes per frame on average — and the resulting schedule really does fit, on average. It then misses deadlines in the frames where the transactions actually land.
E1 is the mutation that inverts the chapter's thesis. Charging bulk for periodic bandwidth makes best-effort traffic compete with guaranteed traffic for the same budget, which defeats the purpose of having two categories: a disk copy would then be able to starve an audio stream, and an audio stream would be able to refuse a disk.
Directed against random
| # | V all | V directed | V random | VHDL all | VHDL directed | VHDL random |
|---|---|---|---|---|---|---|
| E1 | 60608 | 150 | 60458 | 56323 | 150 | 56173 |
| E2 | 74066 | 197 | 73869 | 73656 | 197 | 73459 |
| E3 | 57459 | 129 | 57330 | 59472 | 129 | 59343 |
| E4 | 52439 | 233 | 52206 | 53074 | 233 | 52841 |
| E5 | 76866 | 70 | 76796 | 77752 | 70 | 77682 |
| E6 | 64357 | 66 | 64291 | 65648 | 66 | 65582 |
| E7 | 146104 | 146 | 145958 | 129021 | 146 | 128875 |
Every directed column is identical across Verilog and VHDL.
The directed share here is about 0.2% — the smallest of any chapter in this module. That is not a weakness and it is worth being explicit about why: this design's state space is small and dense. Four types, a handful of sizes, one budget. Random stimulus walks into every interesting situation constantly, and the four arithmetic mutations (E1, E2, E4, E7) are wrong on almost every request. Compare chapter 26.3's burst splitter, where the directed share was 40% because its space was 8192 points that random stimulus visited sparsely.
The directed column's job is not to be large. It is to be non-zero for reasons you can name — and here every one of them is: the budget walk in phase 4b accounts for E1, E2, E4 and E7; the boundary sizes account for E6; the illegal intervals account for E5; the separate control reservation accounts for E3.
13. E6 Read 116,236 in VHDL Against 64,357 Elsewhere
The fifth out-of-line column in this module, and the fifth time it was the mutation rather than the design.
E6 gives every transfer type the isochronous packet limit. In the Verilog, max_packet is a case over all four types and the mutation changes one arm:
Verilog max_packet: VHDL max_packet (before):
case (t) if t = XT_ISO then 1023
X_CTRL: 64 <- mutated else 64
X_ISO: 1023 end if
X_INT: 64
default: 64 E6 replaced the whole body,
endcase so CTRL, INT and BULK all
became 1023 -- three
One arm changes. mutations wearing one label.The VHDL function had been written the compact way — one comparison instead of four cases — and the compact version has no separate arm to mutate. Rewriting it as a case over all four types brought E6 to 65,648 against 64,357, a 2% spread consistent with everything else in the table.
14. Property 7: A Release and a Request in the Same Cycle
The design releases a slot before it considers a request, and in source order that is exactly what it does. In hardware it did not, and the testbench caught it: 33,777 errors, admitted into the wrong slot.
if (rel_valid && slot_used[rel_slot]) <- registered read
slot_used[rel_slot] <= 1'b0; <- takes effect NEXT cycle
... free_slot scans slot_used ... <- still the OLD value
So a driver that closes endpoint 3 and opens a new one in the
same cycle is refused for want of a slot it just freed.The fix is to compute the post-release state explicitly — used_now, per_now, ctl_now — and have everything that decides the request read those rather than the registers.
15. Choosing a Type: The Answer to the Actual Question
| Use case | Type | The reason, in one line |
|---|---|---|
| Audio / video stream | Isochronous | a late packet is worthless, so guarantee the slot and drop the retry |
| Keyboard / mouse | Interrupt | bounded latency and reliability, bought by being small |
| Enumeration / configuration | Control | must always be possible, hence its own reservation |
| Disk / printer / network | Bulk | every byte must arrive; nobody promised when |
| Firmware update | Bulk | correctness over speed, absolutely |
| Sensor polled at 10 ms | Interrupt, interval 8 | reserved, retryable, and 64 bytes is plenty |
| Camera at 30 fps | Isochronous | frame 31 is useless if frame 30 is late |
| Command / response to a device | Bulk pair, or Control | control if it changes device state, bulk if it moves data |
16. Follow-Ups the Interviewer Will Ask
"Why does isochronous have no retries?" Because every slot is reserved. A retry needs a slot, and there is not one that is not already promised to somebody.
"Is isochronous faster than bulk?" No. Bulk is faster on an idle bus — it can use the whole remainder. Isochronous is predictable, which is a different property.
"What happens to an isochronous packet with a CRC error?" It is discarded. The device is told nothing and the host does not retry. Applications are expected to conceal it — audio interpolates, video repeats a frame.
"Why is there a 90% cap rather than 100%?" So that control transfers, and bulk, are always possible. A bus that cannot be reconfigured cannot be recovered.
"Why must the interval be a power of two?" So the host can place the endpoint in a binary tree of frames. An interval of 3 is not schedulable however much bandwidth is free — mutation E5.
"Can an endpoint change type after configuration?" No. The type is in the endpoint descriptor, fixed for that configuration. A device that needs both offers two configurations, or two endpoints.
"What is different at high speed?" Microframes are 125 µs, so periodic endpoints can be polled eight times as often; the periodic cap becomes 80%; and high-speed bulk gains NYET so the host can avoid wasting a slot on a device that is not ready.
17. UVM: Checking a Budget Rather Than a Transaction
// A bandwidth budget is a PROPERTY OF A SET, not of a transaction, so this
// scoreboard does not have a queue of expected items. It has a model of the
// schedule, and its central check re-derives the whole commitment from the
// admitted set rather than tracking it incrementally -- because an
// incremental model drifts in the same direction as an incremental design
// and agrees with it perfectly.
typedef enum bit [1:0] { XT_CTRL, XT_ISO, XT_INT, XT_BULK } xfer_e;
class admit_req extends uvm_sequence_item;
`uvm_object_utils(admit_req)
rand xfer_e xtype;
rand bit [10:0] maxp;
rand bit [7:0] interval;
rand bit is_release;
rand bit [3:0] rel_slot;
function new(string name = "admit_req"); super.new(name); endfunction
// Boundary values, not convenient ones. 64/65 straddle the control and
// bulk limit; 1023/1024 straddle the isochronous one. A distribution over
// 8, 64 and 512 would miss every off-by-one in the design.
constraint c_boundaries { maxp inside {8, 64, 65, 512, 1023, 1024}; }
// Illegal intervals on purpose: 3 and 255 are not powers of two, and the
// rejection path is this block's most important output.
constraint c_intervals { interval inside {1, 2, 3, 4, 8, 255}; }
endclass
class sched_scoreboard extends uvm_scoreboard;
`uvm_component_utils(sched_scoreboard)
uvm_analysis_imp #(admit_req, sched_scoreboard) ap;
localparam int PERIODIC_MAX = 1350; // 90% of a full-speed frame
localparam int CONTROL_MIN = 150; // 10%, reserved
localparam int TXN_OVERHEAD = 13; // token + PID + CRC + gap
localparam int N_SLOT = 8;
// The admitted SET. Everything else is derived from it.
bit used [N_SLOT];
bit [10:0] mp [N_SLOT];
xfer_e ty [N_SLOT];
int unsigned n_admit, n_refuse_bw, n_refuse_iv, n_refuse_mp, n_refuse_full;
int unsigned n_at_ceiling; // admissions that landed within 5% of the cap
int unsigned n_bulk_admitted;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
// The cost of one transaction: the payload PLUS the per-transaction
// overhead, charged at the FULL packet size rather than maxp/interval.
// The schedule must survive the frame the transaction lands in, not the
// average frame -- charging the average is how a schedule passes
// admission control and then misses deadlines.
function int cost_of(bit [10:0] m); return int'(m) + TXN_OVERHEAD; endfunction
function bit periodic(xfer_e t); return (t == XT_ISO) || (t == XT_INT); endfunction
// Re-summed, never incremental.
function int sum_periodic();
int s = 0;
foreach (used[i]) if (used[i] && periodic(ty[i])) s += cost_of(mp[i]);
return s;
endfunction
function int sum_control();
int s = 0;
foreach (used[i]) if (used[i] && ty[i] == XT_CTRL) s += cost_of(mp[i]);
return s;
endfunction
function bit legal_interval(bit [7:0] v);
return (v != 0) && ((v & (v - 1)) == 0);
endfunction
function int max_packet(xfer_e t);
// A case over all four, not "ISO or not": the limits are independent in
// the specification, and a design that expresses four as two cannot be
// tested for the difference.
case (t)
XT_CTRL: return 64;
XT_ISO: return 1023;
XT_INT: return 64;
XT_BULK: return 64;
endcase
endfunction
function void write(admit_req t);
int slot = -1;
if (t.is_release) begin
if (t.rel_slot < N_SLOT) used[t.rel_slot] = 1'b0;
return;
end
foreach (used[i]) if (!used[i] && slot < 0) slot = i;
// ---- the checks, most specific first, so the REASON is useful ----
if (int'(t.maxp) > max_packet(t.xtype)) begin
n_refuse_mp++; return;
end
if (periodic(t.xtype) && !legal_interval(t.interval)) begin
n_refuse_iv++; return;
end
if (slot < 0) begin
n_refuse_full++; return;
end
if (periodic(t.xtype) && (sum_periodic() + cost_of(t.maxp)) > PERIODIC_MAX) begin
n_refuse_bw++; return;
end
// Control's reservation is a SEPARATE limit, not the periodic
// remainder: a fully committed periodic schedule must never be able to
// stop the host talking to its devices, because control transfers are
// how the schedule gets changed.
if (t.xtype == XT_CTRL && (sum_control() + cost_of(t.maxp)) > CONTROL_MIN) begin
n_refuse_bw++; return;
end
used[slot] = 1'b1;
mp[slot] = t.maxp;
ty[slot] = t.xtype;
n_admit++;
if (t.xtype == XT_BULK) n_bulk_admitted++;
if (sum_periodic() * 20 >= PERIODIC_MAX * 19) n_at_ceiling++;
// ---- the invariants, asserted after every admission ----
if (sum_periodic() > PERIODIC_MAX)
`uvm_error("SCHED/BW",
$sformatf("periodic commitment %0d exceeds the %0d-byte ceiling: every isochronous endpoint in this schedule can now miss its deadline",
sum_periodic(), PERIODIC_MAX))
if (sum_control() > CONTROL_MIN)
`uvm_error("SCHED/CTRL",
$sformatf("control commitment %0d exceeds its %0d-byte reservation: the host may lose the ability to reconfigure the bus",
sum_control(), CONTROL_MIN))
endfunction
function void check_phase(uvm_phase phase);
super.check_phase(phase);
`uvm_info("SCHED",
$sformatf("%0d admitted (%0d bulk) | refused: %0d bw, %0d interval, %0d maxp, %0d full",
n_admit, n_bulk_admitted, n_refuse_bw, n_refuse_iv,
n_refuse_mp, n_refuse_full), UVM_LOW)
// A budget that is never approached is a budget that was never tested.
if (n_at_ceiling == 0)
`uvm_error("SCHED/COV",
"the periodic commitment never came within 5% of its ceiling: the budget was never under pressure")
if (n_refuse_bw == 0)
`uvm_error("SCHED/COV",
"nothing was ever refused for bandwidth: the 90% rule was never exercised")
if (n_bulk_admitted == 0)
`uvm_error("SCHED/COV",
"no bulk endpoint was ever admitted: the claim that bulk is charged nothing was never tested")
endfunction
endclass18. Common Misconceptions
"Isochronous is fast." It is predictable. Bulk is faster on an idle bus.
"Isochronous is unreliable because of poor error handling." It is unreliable because it is guaranteed. A retry needs a slot, and every slot is promised.
"Bulk is slow." Bulk is unbounded, which is different. It gets the whole remainder of every frame and can be the fastest thing on the bus.
"Interrupt transfers interrupt." They are polled. The host promises to ask at least every N frames.
"Interrupt is just small bulk." It has a reservation, which bulk does not. That reservation is why it is capped at 64 bytes.
"Control shares the periodic budget." It has its own 10%, so that a full schedule can still be changed.
"An endpoint's cost is its packet size." It is the packet size plus about 13 bytes. For an 8-byte endpoint the overhead is 62% of the cost.
"Charge the average bytes per frame." Charge the worst frame. A schedule sized on averages misses deadlines — the highest-scoring mutation here.
"Any polling interval works." It must be a power of two, so the host can place it in a binary tree of frames.
"A refused request that succeeds on retry is not a bug." It is a bandwidth limit that appears only under load and cannot be reproduced.
19. Exercises
1. A device streams 48 kHz 16-bit stereo audio. Compute the bytes per millisecond, choose a transfer type, and show the arithmetic that rules out the other three.
2. Derive the 90%/10% split from first principles: construct the deadlock that a shared budget permits, and show the split makes it unreachable.
3. E7 charges cost/interval for a periodic endpoint. Construct a schedule that passes admission control under E7 and then misses a deadline, and give the frame in which it happens.
4. E4 ignores the 13-byte overhead. Compute the oversubscription factor for a schedule of 8-byte interrupt endpoints, and for one of 1023-byte isochronous endpoints. Explain the difference.
5. Property 7 requires a same-cycle release and request to both succeed. Explain why failing it is not a functional bug, and why that makes it harder to find than one that is.
6. The VHDL max_packet was rewritten from one comparison to a four-arm case purely for mutability. Argue the general principle, and give a case where you would not apply it.
7. Add high-speed support: 125 µs microframes, an 80% periodic cap, and NYET. Which of the seven properties change, which do not, and what new one is needed?
20. Summary
| Idea | Why it matters |
|---|---|
| Guaranteed bandwidth is no retries | the same resource, spent two ways |
| Isochronous is predictable, not fast | bulk is faster on an idle bus |
| Interrupt buys its retry by being small | 64 bytes, and a reservation per N frames |
| Bulk is charged nothing | which is exactly why it can retry |
| Control has its own 10% | so a full schedule can still be changed |
| The cap is 90%, not 100% | a bus that cannot be reconfigured cannot recover |
| A transaction costs payload + ~13 bytes | 62% overhead on an 8-byte endpoint |
| Charge the worst frame, not the average | the highest-scoring mutation in the chapter |
| Intervals are powers of two | so the host can place them in a frame tree |
| The rejection reason is the real output | "refused" is useless to a driver author |
| Compute the post-release state explicitly | or a same-cycle release/request is refused |
| A retry that hides a bug is worse than a failure | unreproducible, and closed as cannot-reproduce |
| Compact code is not mutable code | four independent limits need four arms |
| Sweep boundary values, not convenient ones | 64/65 and 1023/1024, not 8/64/512 |
| 144 states, 7 mutations, 3 languages | 0 overcommitted cycles in 286,696 checks |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o xt_v.out xt_v.v xt_v_tb.v && ./xt_v.out |
| SystemVerilog | iverilog -g2012 -o xt_sv.out xt_sv.sv xt_sv_tb.sv && ./xt_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a xt_vhdl.vhd xt_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_xt_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_xt_vhdl |
| One mutation | iverilog -g2005 -DMUT_E7 -o mm xt_v_mut.v xt_v_tb.v && ./mm |
| Directed only (Verilog) | iverilog -g2005 -DDIRECTED_ONLY -o mm xt_v_mut.v xt_v_tb.v && ./mm |
| Directed only (VHDL) | nvc --std=2008 -e -gDIRECTED_ONLY=true tb_xt_vhdl |
All three implementations pass with 0 errors: all 144 combinations of transfer type, packet size and interval, using boundary values throughout; the periodic budget walked from empty to refusal for every packet size; a release from every slot; zero overcommitted cycles in 286,696 checks; and every one of the seven mutations killed by directed stimulus alone, with all seven directed scores identical across languages.
Chapter 27.6 — The Scheduling Question takes this budget and turns it into a timeline. Admission control answers can these endpoints coexist; scheduling answers in what order, in this frame — and its central rule is that periodic traffic is placed first, before anything that merely wants to go fast.
Continue learning
Related tutorials
- Related topic
Host-Side Scheduling
A USB transaction cannot be stopped once its token goes out, so the scheduler must ask whether it will finish before it starts — and the periodic reserve exists to protect bulk traffic, not to limit isochronous.
- Related topic
What Is USB?
The opening interview question answered with one load-bearing idea instead of a list — USB is host-scheduled, and polling, NAK, the frame and the missing interrupt line are all consequences of it.
- Related topic
Host / Device / Hub Identification
Host and device take fifteen seconds; the hub is where the interview is decided — a hub is a repeater, not a switch, and no downstream port can ever reach another one.
- Related topic
The Enumeration Question
Attach to configured, with the one step almost everybody gets backwards — SET_ADDRESS takes effect after the status stage, and a device that switches early is invisible to the host.
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.
