USB · Module 27
The Scheduling Question
Periodic traffic is placed first because that is how admission control's promise is kept — and bulk is round-robin because a rotating pointer is the only fairness mechanism in the entire schedule.
Chapter 27.5 answered can these endpoints coexist. This chapter answers the next question, which is the one that gets asked at senior level: in what order, in this frame.
1. The Question
"Walk me through how a USB 2.0 host schedules a frame across the four transfer types."
The answer has two halves. Almost everybody gets the first and almost nobody volunteers the second.
2. Why That Order, Specifically
Each position in the sequence is forced by a property from chapter 27.5.
Isochronous first, because it cannot retry. An isochronous transaction that does not happen in its frame does not happen. Every other type can be deferred to a later frame; this one cannot, so it is placed while the frame is empty.
Interrupt second, because it can be deferred — within its interval. It has a reservation, so it must be placed before best-effort traffic; it can retry, so it does not need to be placed before the type that cannot. It goes exactly between them.
Control third, before bulk rather than competing with it. Control transfers are how the host changes the schedule. A frame packed with bulk that leaves no room for control is a bus that cannot be reconfigured — the same deadlock argument that gives control its own 10% reservation, applied to placement instead of budget.
Bulk last, and round-robin. It has no deadline, so it goes wherever there is room. It has no reservation, so nothing protects one bulk endpoint from another, so the placement order among them must rotate.
The frame, in order:
ISO -- cannot retry -> place while the frame is empty
INT -- can retry, reserved -> after ISO, before best-effort
CTRL -- must always fit -> before bulk, so the bus stays
reconfigurable
BULK -- no deadline at all -> the remainder, ROUND-ROBIN
Each line's position is forced by a property, not chosen.3. What We Are Building
usb_frame_sched walks that sequence as a phase machine and emits one schedule entry per cycle. Eight endpoint slots, a 1500-byte full-speed frame, and a rotating bulk pointer that survives across frames.
The four phases of a frame
One frame, in order, with bulk taking what is left
frame_used only ever rises, and it stops at 1500. The order in which it rises is the answer to the question.
4. Seven Properties
| # | Property |
|---|---|
| 1 | Within a frame, no entry precedes one of higher priority. |
| 2 | Each entry is charged its packet size plus the per-transaction overhead. |
| 3 | A frame never exceeds its byte budget. |
| 4 | Every periodic endpoint that is due and fits is served in that frame. |
| 5 | A periodic endpoint is served only when due. |
| 6 | No periodic endpoint ever misses a deadline. |
| 7 | A bulk endpoint's wait is bounded while at least one fits per frame. |
Properties 4 and 5 are a pair and both are necessary. A scheduler that places nothing satisfies 1, 2, 3 and 6 perfectly. A scheduler that places everything every frame satisfies 1, 2, 4 and 6 and blows the budget it was admitted under. Section 12 is about what happened when only one of the pair existed.
5. Verilog-2005 RTL
// =====================================================================
// usb_frame_sched -- "Describe USB 2.0 host scheduling" in hardware.
//
// Chapter 27.5's admission control answered "can these endpoints
// coexist". This answers the next question: "in what order, in THIS
// frame" -- and the order is the whole answer:
//
// Periodic traffic is placed FIRST, before anything that merely
// wants to go fast. Bulk gets the remainder, and only the
// remainder.
//
// That is not a fairness policy. It is the mechanism by which the
// guarantee made at admission time is actually kept: an endpoint
// promised a slot in every frame gets it because it is placed before
// the traffic that would otherwise fill the frame.
//
// The second half of the answer is the one candidates omit: bulk is
// served ROUND-ROBIN. Bulk has no reservation, so nothing stops one
// bulk endpoint from taking the whole remainder of every frame forever
// -- nothing except a rotating pointer, which is the only fairness
// mechanism in the entire schedule.
// =====================================================================
module usb_frame_sched #(
parameter integer FRAME_BYTES = 1500,
parameter integer N_SLOT = 8,
parameter integer TXN_OH = 13 // per-transaction overhead
) (
input wire clk,
input wire rst_n,
// ---- a new frame begins ----
input wire frame_tick,
// ---- the endpoint table ----
input wire cfg_wr,
input wire [2:0] cfg_slot,
input wire [1:0] cfg_type, // X_CTRL / X_ISO / X_INT / X_BULK
input wire [10:0] cfg_maxp,
input wire [7:0] cfg_interval, // frames between polls (periodic)
input wire cfg_enable,
// ---- the emitted schedule, one entry per cycle ----
output wire ent_valid,
output wire [2:0] ent_slot,
output wire [1:0] ent_type,
output wire [15:0] ent_bytes,
output wire frame_busy,
output wire [15:0] frame_used,
output wire [15:0] frame_num,
// ---- observability ----
output wire [31:0] n_frames,
output wire [31:0] n_entries,
output wire [31:0] n_periodic,
output wire [31:0] n_bulk,
output wire [31:0] n_missed, // must always read 0
output wire [31:0] n_overrun, // must always read 0
output wire [31:0] n_starved // bulk slots passed over while due
);
localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;
// ---- the phases of a frame, in the order that keeps the promise ----
localparam [2:0] P_IDLE = 3'd0,
P_ISO = 3'd1,
P_INT = 3'd2,
P_CTRL = 3'd3,
P_BULK = 3'd4,
P_END = 3'd5;
reg [1:0] ty [0:N_SLOT-1];
reg [10:0] mp [0:N_SLOT-1];
reg [7:0] iv [0:N_SLOT-1];
reg en [0:N_SLOT-1];
// Frames since this endpoint was last served. Compared against its
// interval to detect a missed deadline, which is the only thing in this
// design that a schedule can get wrong without producing a wrong byte.
reg [15:0] age [0:N_SLOT-1];
reg [2:0] ph;
reg [2:0] scan; // which slot the current phase is examining
reg [15:0] used_r;
reg [15:0] fnum_r;
// ---- the round-robin pointer, and why it needs TWO registers ----
//
// A single pointer that advances once per examined slot advances N_SLOT
// times per frame and therefore returns to exactly where it started --
// it never rotates at all, and the same bulk endpoint wins every frame
// forever. The bug is invisible in one frame and total over many.
//
// So rr_r is where the NEXT frame starts, updated only when an endpoint
// is actually served, and bidx is the index the current frame is walking.
reg [2:0] rr_r;
reg [2:0] bidx;
reg [2:0] rr_seen;
reg ev_r;
reg [2:0] es_r;
reg [1:0] et_r;
reg [15:0] eb_r;
reg [31:0] fr_c, ent_c, per_c, blk_c, miss_c, over_c, starv_c;
assign ent_valid = ev_r;
assign ent_slot = es_r;
assign ent_type = et_r;
assign ent_bytes = eb_r;
assign frame_busy = (ph != P_IDLE);
assign frame_used = used_r;
assign frame_num = fnum_r;
assign n_frames = fr_c;
assign n_entries = ent_c;
assign n_periodic = per_c;
assign n_bulk = blk_c;
assign n_missed = miss_c;
assign n_overrun = over_c;
assign n_starved = starv_c;
// ---- is a periodic endpoint due in this frame? ----
//
// The interval is a power of two, so "every Nth frame" is a mask test
// rather than a division. An endpoint with interval 1 is due every
// frame; one with interval 8 is due when the low three bits are zero.
function due_now;
input [7:0] interval;
input [15:0] f;
begin
if (interval <= 8'd1) due_now = 1'b1;
else due_now = ((f & {8'd0, (interval - 8'd1)}) == 16'd0);
end
endfunction
function [15:0] cost_of;
input [10:0] m;
begin cost_of = {5'd0, m} + TXN_OH[15:0]; end
endfunction
integer k;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
ph <= P_IDLE;
scan <= 3'd0;
used_r <= 16'd0;
fnum_r <= 16'd0;
rr_r <= 3'd0;
bidx <= 3'd0;
rr_seen <= 3'd0;
ev_r <= 1'b0;
es_r <= 3'd0;
et_r <= X_CTRL;
eb_r <= 16'd0;
fr_c <= 32'd0;
ent_c <= 32'd0;
per_c <= 32'd0;
blk_c <= 32'd0;
miss_c <= 32'd0;
over_c <= 32'd0;
starv_c <= 32'd0;
for (k = 0; k < N_SLOT; k = k + 1) begin
ty[k] <= X_CTRL;
mp[k] <= 11'd0;
iv[k] <= 8'd1;
en[k] <= 1'b0;
age[k] <= 16'd0;
end
end else begin
ev_r <= 1'b0;
// ---- table writes are only accepted between frames ----
//
// Reconfiguring mid-frame would change the schedule the host is
// already executing, and the endpoint that had already been placed
// would be served under its old parameters. The host does this
// between frames, so the design refuses to do it any other way.
if (cfg_wr && (ph == P_IDLE)) begin
ty[cfg_slot] <= cfg_type;
mp[cfg_slot] <= cfg_maxp;
iv[cfg_slot] <= cfg_interval;
en[cfg_slot] <= cfg_enable;
age[cfg_slot] <= 16'd0;
end
case (ph)
// =============================================================
P_IDLE: begin
if (frame_tick) begin
ph <= P_ISO;
scan <= 3'd0;
used_r <= 16'd0;
rr_seen <= 3'd0;
fr_c <= fr_c + 32'd1;
// Every enabled periodic endpoint ages by one frame. An
// endpoint served this frame will have its age cleared below.
for (k = 0; k < N_SLOT; k = k + 1)
if (en[k] && ((ty[k] == X_ISO) || (ty[k] == X_INT)))
age[k] <= age[k] + 16'd1;
end
end
// =============================================================
// ISOCHRONOUS FIRST. It cannot retry, so it cannot be deferred:
// an isochronous transaction that does not happen in its frame
// does not happen at all.
// =============================================================
P_ISO: begin
if (scan == N_SLOT - 1) ph <= P_INT;
scan <= scan + 3'd1;
if (en[scan] && (ty[scan] == X_ISO) && due_now(iv[scan], fnum_r)
&& ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
ev_r <= 1'b1;
es_r <= scan;
et_r <= X_ISO;
eb_r <= cost_of(mp[scan]);
used_r <= used_r + cost_of(mp[scan]);
age[scan] <= 16'd0;
ent_c <= ent_c + 32'd1;
per_c <= per_c + 32'd1;
end
end
// =============================================================
// INTERRUPT SECOND. Reserved, but retryable, so it may be
// deferred within its interval -- which is exactly why it is
// placed after isochronous rather than before.
// =============================================================
P_INT: begin
if (scan == N_SLOT - 1) ph <= P_CTRL;
scan <= scan + 3'd1;
if (en[scan] && (ty[scan] == X_INT) && due_now(iv[scan], fnum_r)
&& ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
ev_r <= 1'b1;
es_r <= scan;
et_r <= X_INT;
eb_r <= cost_of(mp[scan]);
used_r <= used_r + cost_of(mp[scan]);
age[scan] <= 16'd0;
ent_c <= ent_c + 32'd1;
per_c <= per_c + 32'd1;
end
end
// =============================================================
// CONTROL THIRD, and it is placed before bulk rather than
// competing with it. Control is how the host changes the
// schedule; a frame full of bulk that leaves no room for
// control is a bus that cannot be reconfigured.
// =============================================================
P_CTRL: begin
if (scan == N_SLOT - 1) begin
ph <= P_BULK;
bidx <= rr_r; // resume where the last frame left off
end
scan <= scan + 3'd1;
if (en[scan] && (ty[scan] == X_CTRL)
&& ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
ev_r <= 1'b1;
es_r <= scan;
et_r <= X_CTRL;
eb_r <= cost_of(mp[scan]);
used_r <= used_r + cost_of(mp[scan]);
ent_c <= ent_c + 32'd1;
end
end
// =============================================================
// BULK LAST, ROUND-ROBIN, until the frame is full.
//
// The rotating pointer is the ONLY fairness mechanism in the
// whole schedule. Bulk has no reservation, so without it one
// bulk endpoint would take the entire remainder of every frame
// forever and the others would never move a byte -- with no
// error reported anywhere, because nothing was promised.
// =============================================================
P_BULK: begin
if (rr_seen == N_SLOT - 1) ph <= P_END;
rr_seen <= rr_seen + 3'd1;
bidx <= bidx + 3'd1;
if (en[bidx] && (ty[bidx] == X_BULK)) begin
if ((used_r + cost_of(mp[bidx])) <= FRAME_BYTES) begin
ev_r <= 1'b1;
es_r <= bidx;
et_r <= X_BULK;
eb_r <= cost_of(mp[bidx]);
used_r <= used_r + cost_of(mp[bidx]);
ent_c <= ent_c + 32'd1;
blk_c <= blk_c + 32'd1;
// The next frame starts AFTER the last one served, which is
// what makes the rotation real.
rr_r <= bidx + 3'd1;
end else begin
// Passed over for want of room. Counted, because "the frame
// was full" and "this endpoint is being starved" look the
// same in one frame and are different over many.
starv_c <= starv_c + 32'd1;
end
end
end
// =============================================================
P_END: begin
ph <= P_IDLE;
fnum_r <= fnum_r + 16'd1;
// ---- the deadline check, once per frame ----
//
// An endpoint whose age has passed its interval has missed a
// deadline it was promised. Unreachable on a correct schedule,
// counted so a run can publish zero.
for (k = 0; k < N_SLOT; k = k + 1)
if (en[k] && ((ty[k] == X_ISO) || (ty[k] == X_INT))
&& (age[k] > {8'd0, iv[k]}))
miss_c <= miss_c + 32'd1;
if (used_r > FRAME_BYTES) over_c <= over_c + 32'd1;
end
default: ph <= P_IDLE;
endcase
end
end
endmodule6. SystemVerilog RTL
// =====================================================================
// usb_frame_sched -- SystemVerilog.
//
// The frame phases become a named enumeration, which is worth more here
// than in most designs: the ORDER of those phases IS the answer to the
// interview question, and `P_ISO -> P_INT -> P_CTRL -> P_BULK` in a
// waveform says the whole thing at a glance.
//
// Chapter 27.5's admission control answered "can these endpoints
// coexist". This answers the next question: "in what order, in THIS
// frame" -- and the order is the whole answer:
//
// Periodic traffic is placed FIRST, before anything that merely
// wants to go fast. Bulk gets the remainder, and only the
// remainder.
//
// That is not a fairness policy. It is the mechanism by which the
// guarantee made at admission time is actually kept: an endpoint
// promised a slot in every frame gets it because it is placed before
// the traffic that would otherwise fill the frame.
//
// The second half of the answer is the one candidates omit: bulk is
// served ROUND-ROBIN. Bulk has no reservation, so nothing stops one
// bulk endpoint from taking the whole remainder of every frame forever
// -- nothing except a rotating pointer, which is the only fairness
// mechanism in the entire schedule.
// =====================================================================
module usb_frame_sched #(
parameter int FRAME_BYTES = 1500,
parameter int N_SLOT = 8,
parameter int TXN_OH = 13 // per-transaction overhead
) (
input logic clk,
input logic rst_n,
// ---- a new frame begins ----
input logic frame_tick,
// ---- the endpoint table ----
input logic cfg_wr,
input logic [2:0] cfg_slot,
input logic [1:0] cfg_type, // X_CTRL / X_ISO / X_INT / X_BULK
input logic [10:0]cfg_maxp,
input logic [7:0] cfg_interval, // frames between polls (periodic)
input logic cfg_enable,
// ---- the emitted schedule, one entry per cycle ----
output logic ent_valid,
output logic [2:0] ent_slot,
output logic [1:0] ent_type,
output logic [15:0]ent_bytes,
output logic frame_busy,
output logic [15:0]frame_used,
output logic [15:0]frame_num,
// ---- observability ----
output logic [31:0]n_frames,
output logic [31:0]n_entries,
output logic [31:0]n_periodic,
output logic [31:0]n_bulk,
output logic [31:0]n_missed, // must always read 0
output logic [31:0]n_overrun, // must always read 0
output logic [31:0]n_starved // bulk slots passed over while due
);
typedef enum logic [1:0] { X_CTRL = 2'd0, X_ISO = 2'd1,
X_INT = 2'd2, X_BULK = 2'd3 } xfer_e;
// ---- the phases of a frame, in the order that keeps the promise ----
// The ORDER of these is the answer to the interview question, and a
// waveform showing P_ISO -> P_INT -> P_CTRL -> P_BULK says it at a glance.
typedef enum logic [2:0] {
P_IDLE = 3'd0,
P_ISO = 3'd1,
P_INT = 3'd2,
P_CTRL = 3'd3,
P_BULK = 3'd4,
P_END = 3'd5
} phase_e;
xfer_e ty [N_SLOT];
logic [10:0] mp [N_SLOT];
logic [7:0] iv [N_SLOT];
logic en [N_SLOT];
// Frames since this endpoint was last served. Compared against its
// interval to detect a missed deadline, which is the only thing in this
// design that a schedule can get wrong without producing a wrong byte.
logic [15:0] age [N_SLOT];
phase_e ph;
logic [2:0] scan; // which slot the current phase is examining
logic [15:0] used_r;
logic [15:0] fnum_r;
// ---- the round-robin pointer, and why it needs TWO registers ----
//
// A single pointer that advances once per examined slot advances N_SLOT
// times per frame and therefore returns to exactly where it started --
// it never rotates at all, and the same bulk endpoint wins every frame
// forever. The bug is invisible in one frame and total over many.
//
// So rr_r is where the NEXT frame starts, updated only when an endpoint
// is actually served, and bidx is the index the current frame is walking.
logic [2:0] rr_r;
logic [2:0] bidx;
logic [2:0] rr_seen;
logic ev_r;
logic [2:0] es_r;
xfer_e et_r;
logic [15:0] eb_r;
logic [31:0] fr_c, ent_c, per_c, blk_c, miss_c, over_c, starv_c;
assign ent_valid = ev_r;
assign ent_slot = es_r;
assign ent_type = et_r;
assign ent_bytes = eb_r;
assign frame_busy = (ph != P_IDLE);
assign frame_used = used_r;
assign frame_num = fnum_r;
assign n_frames = fr_c;
assign n_entries = ent_c;
assign n_periodic = per_c;
assign n_bulk = blk_c;
assign n_missed = miss_c;
assign n_overrun = over_c;
assign n_starved = starv_c;
// ---- is a periodic endpoint due in this frame? ----
//
// The interval is a power of two, so "every Nth frame" is a mask test
// rather than a division. An endpoint with interval 1 is due every
// frame; one with interval 8 is due when the low three bits are zero.
function automatic logic due_now(logic [7:0] interval, logic [15:0] f);
begin
if (interval <= 8'd1) due_now = 1'b1;
else due_now = ((f & {8'd0, (interval - 8'd1)}) == 16'd0);
end
endfunction
function automatic logic [15:0] cost_of(logic [10:0] m);
return {5'd0, m} + 16'(TXN_OH);
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
ph <= P_IDLE;
scan <= 3'd0;
used_r <= 16'd0;
fnum_r <= 16'd0;
rr_r <= 3'd0;
bidx <= 3'd0;
rr_seen <= 3'd0;
ev_r <= 1'b0;
es_r <= 3'd0;
et_r <= X_CTRL;
eb_r <= 16'd0;
fr_c <= 32'd0;
ent_c <= 32'd0;
per_c <= 32'd0;
blk_c <= 32'd0;
miss_c <= 32'd0;
over_c <= 32'd0;
starv_c <= 32'd0;
for (int k = 0; k < N_SLOT; k++) begin
ty[k] <= X_CTRL;
mp[k] <= 11'd0;
iv[k] <= 8'd1;
en[k] <= 1'b0;
age[k] <= 16'd0;
end
end else begin
ev_r <= 1'b0;
// ---- table writes are only accepted between frames ----
//
// Reconfiguring mid-frame would change the schedule the host is
// already executing, and the endpoint that had already been placed
// would be served under its old parameters. The host does this
// between frames, so the design refuses to do it any other way.
if (cfg_wr && (ph == P_IDLE)) begin
ty[cfg_slot] <= xfer_e'(cfg_type);
mp[cfg_slot] <= cfg_maxp;
iv[cfg_slot] <= cfg_interval;
en[cfg_slot] <= cfg_enable;
age[cfg_slot] <= 16'd0;
end
case (ph)
// =============================================================
P_IDLE: begin
if (frame_tick) begin
ph <= P_ISO;
scan <= 3'd0;
used_r <= 16'd0;
rr_seen <= 3'd0;
fr_c <= fr_c + 32'd1;
// Every enabled periodic endpoint ages by one frame. An
// endpoint served this frame will have its age cleared below.
for (int k = 0; k < N_SLOT; k++)
if (en[k] && ((ty[k] == X_ISO) || (ty[k] == X_INT)))
age[k] <= age[k] + 16'd1;
end
end
// =============================================================
// ISOCHRONOUS FIRST. It cannot retry, so it cannot be deferred:
// an isochronous transaction that does not happen in its frame
// does not happen at all.
// =============================================================
P_ISO: begin
if (scan == N_SLOT - 1) ph <= P_INT;
scan <= scan + 3'd1;
if (en[scan] && (ty[scan] == X_ISO) && due_now(iv[scan], fnum_r)
&& ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
ev_r <= 1'b1;
es_r <= scan;
et_r <= X_ISO;
eb_r <= cost_of(mp[scan]);
used_r <= used_r + cost_of(mp[scan]);
age[scan] <= 16'd0;
ent_c <= ent_c + 32'd1;
per_c <= per_c + 32'd1;
end
end
// =============================================================
// INTERRUPT SECOND. Reserved, but retryable, so it may be
// deferred within its interval -- which is exactly why it is
// placed after isochronous rather than before.
// =============================================================
P_INT: begin
if (scan == N_SLOT - 1) ph <= P_CTRL;
scan <= scan + 3'd1;
if (en[scan] && (ty[scan] == X_INT) && due_now(iv[scan], fnum_r)
&& ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
ev_r <= 1'b1;
es_r <= scan;
et_r <= X_INT;
eb_r <= cost_of(mp[scan]);
used_r <= used_r + cost_of(mp[scan]);
age[scan] <= 16'd0;
ent_c <= ent_c + 32'd1;
per_c <= per_c + 32'd1;
end
end
// =============================================================
// CONTROL THIRD, and it is placed before bulk rather than
// competing with it. Control is how the host changes the
// schedule; a frame full of bulk that leaves no room for
// control is a bus that cannot be reconfigured.
// =============================================================
P_CTRL: begin
if (scan == N_SLOT - 1) begin
ph <= P_BULK;
bidx <= rr_r; // resume where the last frame left off
end
scan <= scan + 3'd1;
if (en[scan] && (ty[scan] == X_CTRL)
&& ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
ev_r <= 1'b1;
es_r <= scan;
et_r <= X_CTRL;
eb_r <= cost_of(mp[scan]);
used_r <= used_r + cost_of(mp[scan]);
ent_c <= ent_c + 32'd1;
end
end
// =============================================================
// BULK LAST, ROUND-ROBIN, until the frame is full.
//
// The rotating pointer is the ONLY fairness mechanism in the
// whole schedule. Bulk has no reservation, so without it one
// bulk endpoint would take the entire remainder of every frame
// forever and the others would never move a byte -- with no
// error reported anywhere, because nothing was promised.
// =============================================================
P_BULK: begin
if (rr_seen == N_SLOT - 1) ph <= P_END;
rr_seen <= rr_seen + 3'd1;
bidx <= bidx + 3'd1;
if (en[bidx] && (ty[bidx] == X_BULK)) begin
if ((used_r + cost_of(mp[bidx])) <= FRAME_BYTES) begin
ev_r <= 1'b1;
es_r <= bidx;
et_r <= X_BULK;
eb_r <= cost_of(mp[bidx]);
used_r <= used_r + cost_of(mp[bidx]);
ent_c <= ent_c + 32'd1;
blk_c <= blk_c + 32'd1;
// The next frame starts AFTER the last one served, which is
// what makes the rotation real.
rr_r <= bidx + 3'd1;
end else begin
// Passed over for want of room. Counted, because "the frame
// was full" and "this endpoint is being starved" look the
// same in one frame and are different over many.
starv_c <= starv_c + 32'd1;
end
end
end
// =============================================================
P_END: begin
ph <= P_IDLE;
fnum_r <= fnum_r + 16'd1;
// ---- the deadline check, once per frame ----
//
// An endpoint whose age has passed its interval has missed a
// deadline it was promised. Unreachable on a correct schedule,
// counted so a run can publish zero.
for (int k = 0; k < N_SLOT; k++)
if (en[k] && ((ty[k] == X_ISO) || (ty[k] == X_INT))
&& (age[k] > {8'd0, iv[k]}))
miss_c <= miss_c + 32'd1;
if (used_r > FRAME_BYTES) over_c <= over_c + 32'd1;
end
default: ph <= P_IDLE;
endcase
end
end
endmodule7. VHDL-2008 RTL
-- =====================================================================
-- usb_frame_sched -- VHDL-2008.
--
-- The frame phases are a real enumeration type, so the ORDER that is the
-- answer to this chapter's question is declared once, in one place, and
-- the compiler refuses to confuse a phase with a transfer type.
--
-- Constants and types are prefixed because VHDL identifiers are
-- case-insensitive: a package name 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 fs_pkg is
type xfer_t is (XT_CTRL, XT_ISO, XT_INT, XT_BULK);
-- The order of these IS the answer to the interview question.
type phase_t is (PH_IDLE, PH_ISO, PH_INT, PH_CTRL, PH_BULK, PH_END);
constant TXN_OH_C : natural := 13;
function xfer_of(v : std_logic_vector(1 downto 0)) return xfer_t;
function code_of(t : xfer_t) return std_logic_vector;
function periodic_t(t : xfer_t) return boolean;
type mp_arr is array (natural range <>) of unsigned(10 downto 0);
type iv_arr is array (natural range <>) of unsigned(7 downto 0);
type ag_arr is array (natural range <>) of unsigned(15 downto 0);
type ty_arr is array (natural range <>) of xfer_t;
end package;
package body fs_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(t : xfer_t) return std_logic_vector is
begin
case t is
when XT_CTRL => return "00";
when XT_ISO => return "01";
when XT_INT => return "10";
when XT_BULK => return "11";
end case;
end function;
function 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.fs_pkg.all;
entity usb_frame_sched is
generic (
FRAME_BYTES : natural := 1500;
N_SLOT : natural := 8;
TXN_OH : natural := 13
);
port (
clk : in std_logic;
rst_n : in std_logic;
frame_tick : in std_logic;
cfg_wr : in std_logic;
cfg_slot : in std_logic_vector(2 downto 0);
cfg_type : in std_logic_vector(1 downto 0);
cfg_maxp : in std_logic_vector(10 downto 0);
cfg_interval : in std_logic_vector(7 downto 0);
cfg_enable : in std_logic;
ent_valid : out std_logic;
ent_slot : out std_logic_vector(2 downto 0);
ent_type : out std_logic_vector(1 downto 0);
ent_bytes : out std_logic_vector(15 downto 0);
frame_busy : out std_logic;
frame_used : out std_logic_vector(15 downto 0);
frame_num : out std_logic_vector(15 downto 0);
n_frames : out std_logic_vector(31 downto 0);
n_entries : out std_logic_vector(31 downto 0);
n_periodic : out std_logic_vector(31 downto 0);
n_bulk : out std_logic_vector(31 downto 0);
n_missed : out std_logic_vector(31 downto 0);
n_overrun : out std_logic_vector(31 downto 0);
n_starved : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_frame_sched is
signal ty : ty_arr(0 to N_SLOT-1) := (others => XT_CTRL);
signal mp : mp_arr(0 to N_SLOT-1) := (others => (others => '0'));
signal iv : iv_arr(0 to N_SLOT-1) := (others => to_unsigned(1, 8));
signal en : std_logic_vector(N_SLOT-1 downto 0) := (others => '0');
-- Frames since this endpoint was last served, compared against its
-- interval. A missed deadline is the only thing a schedule can get wrong
-- without producing a single wrong byte.
signal age : ag_arr(0 to N_SLOT-1) := (others => (others => '0'));
signal ph : phase_t := PH_IDLE;
signal scan : natural range 0 to 7 := 0;
signal used_r : unsigned(15 downto 0) := (others => '0');
signal fnum_r : unsigned(15 downto 0) := (others => '0');
-- TWO pointers, not one. A single pointer advanced once per examined slot
-- advances N_SLOT times per frame and therefore returns to exactly where
-- it started -- it never rotates, and the same bulk endpoint wins every
-- frame forever. rr_r is where the NEXT frame starts and is updated only
-- when an endpoint is actually served; bidx walks the current frame.
signal rr_r : natural range 0 to 7 := 0;
signal bidx : natural range 0 to 7 := 0;
signal rr_seen : natural range 0 to 7 := 0;
signal ev_r : std_logic := '0';
signal es_r : natural range 0 to 7 := 0;
signal et_r : xfer_t := XT_CTRL;
signal eb_r : unsigned(15 downto 0) := (others => '0');
signal fr_c, ent_c, per_c, blk_c, miss_c, over_c, starv_c
: unsigned(31 downto 0) := (others => '0');
-- The interval is a power of two, so "every Nth frame" is a mask test.
function due_now(interval : unsigned(7 downto 0);
f : unsigned(15 downto 0)) return boolean is
begin
if interval <= 1 then
return true;
else
return (f and resize(interval - 1, 16)) = 0;
end if;
end function;
function cost_of(m : unsigned(10 downto 0)) return unsigned is
begin
return resize(m, 16) + to_unsigned(TXN_OH_C, 16);
end function;
begin
ent_valid <= ev_r;
ent_slot <= std_logic_vector(to_unsigned(es_r, 3));
ent_type <= code_of(et_r);
ent_bytes <= std_logic_vector(eb_r);
frame_busy <= '0' when ph = PH_IDLE else '1';
frame_used <= std_logic_vector(used_r);
frame_num <= std_logic_vector(fnum_r);
n_frames <= std_logic_vector(fr_c);
n_entries <= std_logic_vector(ent_c);
n_periodic <= std_logic_vector(per_c);
n_bulk <= std_logic_vector(blk_c);
n_missed <= std_logic_vector(miss_c);
n_overrun <= std_logic_vector(over_c);
n_starved <= std_logic_vector(starv_c);
main : process(clk, rst_n)
variable cs : natural;
begin
if rst_n = '0' then
ph <= PH_IDLE;
scan <= 0;
used_r <= (others => '0');
fnum_r <= (others => '0');
rr_r <= 0;
bidx <= 0;
rr_seen <= 0;
ev_r <= '0';
es_r <= 0;
et_r <= XT_CTRL;
eb_r <= (others => '0');
fr_c <= (others => '0');
ent_c <= (others => '0');
per_c <= (others => '0');
blk_c <= (others => '0');
miss_c <= (others => '0');
over_c <= (others => '0');
starv_c <= (others => '0');
ty <= (others => XT_CTRL);
mp <= (others => (others => '0'));
iv <= (others => to_unsigned(1, 8));
en <= (others => '0');
age <= (others => (others => '0'));
elsif rising_edge(clk) then
ev_r <= '0';
-- Table writes are accepted only BETWEEN frames. Reconfiguring
-- mid-frame would change a schedule the host is already executing,
-- and an endpoint already placed would be served under its old
-- parameters.
if cfg_wr = '1' and ph = PH_IDLE then
cs := to_integer(unsigned(cfg_slot));
ty(cs) <= xfer_of(cfg_type);
mp(cs) <= unsigned(cfg_maxp);
iv(cs) <= unsigned(cfg_interval);
en(cs) <= cfg_enable;
age(cs) <= (others => '0');
end if;
case ph is
when PH_IDLE =>
if frame_tick = '1' then
ph <= PH_ISO;
scan <= 0;
used_r <= (others => '0');
rr_seen <= 0;
fr_c <= fr_c + 1;
-- Every enabled periodic endpoint ages by one frame; one served
-- this frame has its age cleared below.
for k in 0 to N_SLOT-1 loop
if en(k) = '1' and periodic_t(ty(k)) then
age(k) <= age(k) + 1;
end if;
end loop;
end if;
-- ISOCHRONOUS FIRST. It cannot retry, so it cannot be deferred: a
-- transaction that does not happen in its frame does not happen.
when PH_ISO =>
if scan = N_SLOT - 1 then ph <= PH_INT; end if;
if scan = N_SLOT - 1 then scan <= 0; else scan <= scan + 1; end if;
if en(scan) = '1' and ty(scan) = XT_ISO
and due_now(iv(scan), fnum_r)
and (used_r + cost_of(mp(scan))) <= to_unsigned(FRAME_BYTES, 16) then
ev_r <= '1';
es_r <= scan;
et_r <= XT_ISO;
eb_r <= cost_of(mp(scan));
used_r <= used_r + cost_of(mp(scan));
age(scan) <= (others => '0');
ent_c <= ent_c + 1;
per_c <= per_c + 1;
end if;
-- INTERRUPT SECOND. Reserved but retryable, so it may be deferred
-- within its interval -- which is why it goes after isochronous.
when PH_INT =>
if scan = N_SLOT - 1 then ph <= PH_CTRL; end if;
if scan = N_SLOT - 1 then scan <= 0; else scan <= scan + 1; end if;
if en(scan) = '1' and ty(scan) = XT_INT
and due_now(iv(scan), fnum_r)
and (used_r + cost_of(mp(scan))) <= to_unsigned(FRAME_BYTES, 16) then
ev_r <= '1';
es_r <= scan;
et_r <= XT_INT;
eb_r <= cost_of(mp(scan));
used_r <= used_r + cost_of(mp(scan));
age(scan) <= (others => '0');
ent_c <= ent_c + 1;
per_c <= per_c + 1;
end if;
-- CONTROL THIRD, before bulk rather than competing with it.
-- Control is how the host changes the schedule; a frame full of
-- bulk that leaves no room for control is a bus that cannot be
-- reconfigured.
when PH_CTRL =>
if scan = N_SLOT - 1 then
ph <= PH_BULK;
bidx <= rr_r; -- resume where the last frame left off
end if;
if scan = N_SLOT - 1 then scan <= 0; else scan <= scan + 1; end if;
if en(scan) = '1' and ty(scan) = XT_CTRL
and (used_r + cost_of(mp(scan))) <= to_unsigned(FRAME_BYTES, 16) then
ev_r <= '1';
es_r <= scan;
et_r <= XT_CTRL;
eb_r <= cost_of(mp(scan));
used_r <= used_r + cost_of(mp(scan));
ent_c <= ent_c + 1;
end if;
-- BULK LAST, ROUND-ROBIN, until the frame is full. The rotating
-- pointer is the ONLY fairness mechanism in the whole schedule:
-- bulk has no reservation, so without it one endpoint would take
-- the entire remainder of every frame forever, with no error
-- reported anywhere because nothing was promised.
when PH_BULK =>
if rr_seen = N_SLOT - 1 then ph <= PH_END; end if;
if rr_seen = N_SLOT - 1 then rr_seen <= 0; else rr_seen <= rr_seen + 1; end if;
if bidx = N_SLOT - 1 then bidx <= 0; else bidx <= bidx + 1; end if;
if en(bidx) = '1' and ty(bidx) = XT_BULK then
if (used_r + cost_of(mp(bidx))) <= to_unsigned(FRAME_BYTES, 16) then
ev_r <= '1';
es_r <= bidx;
et_r <= XT_BULK;
eb_r <= cost_of(mp(bidx));
used_r <= used_r + cost_of(mp(bidx));
ent_c <= ent_c + 1;
blk_c <= blk_c + 1;
-- The next frame starts AFTER the last one served, which is
-- what makes the rotation real.
if bidx = N_SLOT - 1 then rr_r <= 0; else rr_r <= bidx + 1; end if;
else
-- Passed over for want of room. Counted, because "the frame
-- was full" and "this endpoint is starved" look the same in
-- one frame and are different over many.
starv_c <= starv_c + 1;
end if;
end if;
when PH_END =>
ph <= PH_IDLE;
fnum_r <= fnum_r + 1;
-- The deadline check, once per frame. Unreachable on a correct
-- schedule, counted so a run can publish zero.
for k in 0 to N_SLOT-1 loop
if en(k) = '1' and periodic_t(ty(k))
and age(k) > resize(iv(k), 16) then
miss_c <= miss_c + 1;
end if;
end loop;
if used_r > to_unsigned(FRAME_BYTES, 16) then
over_c <= over_c + 1;
end if;
end case;
end if;
end process;
end architecture;8. The Testbench: Two Properties That Are Not About Transactions
Order is a property of a whole frame. No single entry is wrong; the sequence is. So the bench collects a frame's entries into an array and checks that priority never decreases across it — and it maps type to priority explicitly, because the numeric type encoding is arbitrary and the placement order is the thesis. Checking ent_type monotonicity would test the encoding by accident.
Fairness is a property of many frames and cannot be observed in one at all. So the bench counts, per bulk endpoint, how many consecutive frames it has gone unserved, and bounds it.
What one frame can tell you about fairness: NOTHING.
frame 41: bulk slot 3 served, slots 0,1,2,4..7 passed over
Correct rotation and total starvation produce the
IDENTICAL frame. Only the sequence of frames differs.There is also a per-cycle check: the design's running frame_used must equal the bench's accumulation at every point inside the frame, not merely at the end. A scheduler that overshoots and then corrects passes an end-of-frame comparison.
Verilog-2005 testbench
// =====================================================================
// Testbench for usb_frame_sched.
//
// Two properties here cannot be checked one transaction at a time.
//
// ORDER is a property of a whole frame: every entry must belong to a
// type of priority at least as low as the one before it. So the bench
// collects a frame's entries and checks the sequence, not the items.
//
// FAIRNESS is a property of many frames: a round-robin pointer cannot
// be observed in one frame at all. So the bench counts how many frames
// each bulk endpoint waits, and asserts a bound over the whole run.
// =====================================================================
`timescale 1ns/1ps
module tb_fs_v;
localparam integer FRAME_BYTES = 1500;
localparam integer N_SLOT = 8;
localparam integer TXN_OH = 13;
localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;
reg clk = 1'b0, rst_n = 1'b0;
reg frame_tick = 1'b0;
reg cfg_wr = 1'b0;
reg [2:0] cfg_slot = 3'd0;
reg [1:0] cfg_type = X_CTRL;
reg [10:0] cfg_maxp = 11'd0;
reg [7:0] cfg_interval = 8'd1;
reg cfg_enable = 1'b0;
wire ent_valid, frame_busy;
wire [2:0] ent_slot;
wire [1:0] ent_type;
wire [15:0] ent_bytes, frame_used, frame_num;
wire [31:0] n_frames, n_entries, n_periodic, n_bulk,
n_missed, n_overrun, n_starved;
usb_frame_sched #(.FRAME_BYTES(FRAME_BYTES), .N_SLOT(N_SLOT),
.TXN_OH(TXN_OH)) dut (
.clk(clk), .rst_n(rst_n), .frame_tick(frame_tick),
.cfg_wr(cfg_wr), .cfg_slot(cfg_slot), .cfg_type(cfg_type),
.cfg_maxp(cfg_maxp), .cfg_interval(cfg_interval), .cfg_enable(cfg_enable),
.ent_valid(ent_valid), .ent_slot(ent_slot), .ent_type(ent_type),
.ent_bytes(ent_bytes), .frame_busy(frame_busy),
.frame_used(frame_used), .frame_num(frame_num),
.n_frames(n_frames), .n_entries(n_entries), .n_periodic(n_periodic),
.n_bulk(n_bulk), .n_missed(n_missed), .n_overrun(n_overrun),
.n_starved(n_starved)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0, frames = 0;
integer seed;
function [31:0] urand;
input dummy;
begin urand = $random(seed) & 32'h3FFF_FFFF; end
endfunction
// ---- the shadow table ----
reg [1:0] s_ty [0:N_SLOT-1];
reg [10:0] s_mp [0:N_SLOT-1];
reg [7:0] s_iv [0:N_SLOT-1];
reg s_en [0:N_SLOT-1];
// ---- per-frame collection ----
reg [2:0] f_slot [0:63];
reg [1:0] f_type [0:63];
reg [15:0] f_byte [0:63];
integer f_n;
reg [15:0] f_sum;
// ---- the two headline counters ----
integer n_order = 0; // an entry out of priority order
integer n_over = 0; // a frame that exceeded its byte budget
// ---- fairness: frames since each bulk slot was last served ----
integer bulk_wait [0:N_SLOT-1];
integer worst_wait = 0;
integer ph3_worst = 0;
reg fair_check = 1'b0;
integer g_frames = 0, g_entries = 0, g_periodic = 0, g_bulk = 0, g_starved = 0;
// ---- exhaustive reach over (type, interval, frame phase) ----
reg reach [0:127];
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 frame=%0d: %0s", $time, frames, what);
end
end
endtask
// ---- priority, which is NOT the numeric type code ----
//
// The type encoding is arbitrary; the placement order is the design's
// whole thesis. Mapping one to the other explicitly is how the bench
// avoids checking the encoding by accident.
function [1:0] prio;
input [1:0] t;
begin
case (t)
X_ISO: prio = 2'd0;
X_INT: prio = 2'd1;
X_CTRL: prio = 2'd2;
default: prio = 2'd3; // bulk
endcase
end
endfunction
function due_f;
input [7:0] interval;
input [15:0] f;
begin
if (interval <= 8'd1) due_f = 1'b1;
else due_f = ((f & {8'd0, (interval - 8'd1)}) == 16'd0);
end
endfunction
// ---------------------------------------------------------------
// Configure one slot. Only legal between frames.
// ---------------------------------------------------------------
task cfg(input [2:0] sl, input [1:0] t, input [10:0] m,
input [7:0] interval, input e);
begin
cfg_wr = 1'b1; cfg_slot = sl; cfg_type = t;
cfg_maxp = m; cfg_interval = interval; cfg_enable = e;
s_ty[sl] = t; s_mp[sl] = m; s_iv[sl] = interval; s_en[sl] = e;
@(posedge clk); #1;
cfg_wr = 1'b0;
steps = steps + 1;
end
endtask
// ---------------------------------------------------------------
// Run one complete frame and check everything about it.
// ---------------------------------------------------------------
task run_frame;
integer guard, a, p;
reg [15:0] fnum_at_start;
begin
fnum_at_start = frame_num;
f_n = 0;
f_sum = 16'd0;
frame_tick = 1'b1;
@(posedge clk); #1;
frame_tick = 1'b0;
steps = steps + 1;
// ---- collect until the frame ends ----
//
// Bounded. An unbounded wait here is an infinite loop the moment a
// mutation stops the phase machine advancing, and the run would
// never reach the summary that says which mutation did it.
guard = 0;
while (frame_busy && (guard < 8 * N_SLOT + 32)) begin
if (ent_valid) begin
if (f_n < 64) begin
f_slot[f_n] = ent_slot;
f_type[f_n] = ent_type;
f_byte[f_n] = ent_bytes;
end
f_sum = f_sum + ent_bytes;
f_n = f_n + 1;
end
// ---- checked EVERY cycle, not once per frame ----
//
// The design's running byte total must equal the bench's
// accumulation at every point inside the frame, not merely at the
// end. A scheduler that overshoots and then corrects would pass an
// end-of-frame check and fail this one.
ck(frame_used === f_sum[15:0],
"the running byte total disagrees mid-frame");
ck(!(ent_valid && !frame_busy),
"an entry was emitted outside a frame");
@(posedge clk); #1;
steps = steps + 1;
guard = guard + 1;
end
ck(guard < 8 * N_SLOT + 32, "the frame never ended");
// one more cycle to catch an entry emitted on the last phase cycle
if (ent_valid) begin
if (f_n < 64) begin
f_slot[f_n] = ent_slot; f_type[f_n] = ent_type; f_byte[f_n] = ent_bytes;
end
f_sum = f_sum + ent_bytes;
f_n = f_n + 1;
end
frames = frames + 1;
g_frames = g_frames + 1;
// ---- PROPERTY 1: priority order across the whole frame ----
for (a = 1; a < f_n && a < 64; a = a + 1)
if (prio(f_type[a]) < prio(f_type[a-1])) begin
n_order = n_order + 1;
ck(1'b0, "an entry was placed before a higher-priority one");
end
ck(n_order == 0, "the frame was not in priority order");
// ---- PROPERTY 2: each entry's cost is maxp + overhead ----
for (a = 0; a < f_n && a < 64; a = a + 1)
ck(f_byte[a] === ({5'd0, s_mp[f_slot[a]]} + TXN_OH),
"an entry was charged the wrong number of bytes");
// ---- PROPERTY 3: the frame never overruns ----
if (f_sum > FRAME_BYTES) n_over = n_over + 1;
ck(f_sum <= FRAME_BYTES, "the frame exceeded its byte budget");
ck(n_over == 0, "a frame overran");
ck(n_overrun === 32'd0, "the design detected its own overrun");
// ---- PROPERTY 4: every DUE periodic endpoint that fits was served ----
//
// The liveness half. A schedule that places nothing is in perfect
// priority order and perfectly within budget.
for (a = 0; a < N_SLOT; a = a + 1)
if (s_en[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT))
&& due_f(s_iv[a], fnum_at_start)
&& (({5'd0, s_mp[a]} + TXN_OH) <= FRAME_BYTES)) begin
p = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == a[2:0]) p = 1;
ck(p == 1, "a due periodic endpoint was not served in its frame");
end
// ---- PROPERTY 5: served ONLY when due ----
//
// The other half of property 4, and the half that is easy to forget:
// a scheduler that serves every periodic endpoint every frame misses
// no deadlines and violates nothing the liveness check looks at. It
// is still wrong -- it spends bandwidth that was reserved for
// somebody else, and for an isochronous endpoint it delivers data the
// application has not produced yet.
for (a = 0; a < N_SLOT; a = a + 1)
if (s_en[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT))
&& !due_f(s_iv[a], fnum_at_start)) begin
p = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == a[2:0]) p = 1;
ck(p == 0, "a periodic endpoint was served in a frame it was not due");
end
// ---- PROPERTY 6: no missed deadlines, ever ----
ck(n_missed === 32'd0, "the design reported a missed deadline");
// ---- PROPERTY 7: the fairness bound, EVERY frame ----
//
// Checked per frame rather than once at the end. A single
// end-of-phase check kills a non-rotating pointer exactly once, and
// one kill is indistinguishable from luck.
if (fair_check)
for (a = 0; a < N_SLOT; a = a + 1)
if (s_en[a] && (s_ty[a] == X_BULK))
ck(bulk_wait[a] <= N_SLOT,
"a bulk endpoint waited longer than the round-robin bound");
// ---- fairness bookkeeping, checked over the whole run ----
for (a = 0; a < N_SLOT; a = a + 1) begin
if (s_en[a] && (s_ty[a] == X_BULK)) begin
p = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == a[2:0]) p = 1;
if (p) bulk_wait[a] = 0;
else begin
bulk_wait[a] = bulk_wait[a] + 1;
if (bulk_wait[a] > worst_wait) worst_wait = bulk_wait[a];
end
end else begin
bulk_wait[a] = 0;
end
end
g_entries = g_entries + f_n;
g_periodic = n_periodic;
g_bulk = n_bulk;
g_starved = n_starved;
end
endtask
task reset_dut;
integer a;
begin
rst_n = 1'b0;
frame_tick = 0; cfg_wr = 0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
for (a = 0; a < N_SLOT; a = a + 1) begin
s_ty[a] = X_CTRL; s_mp[a] = 11'd0; s_iv[a] = 8'd1; s_en[a] = 1'b0;
bulk_wait[a] = 0;
end
@(posedge clk); #1;
end
endtask
integer ti, vi, fi, k, a4, pj;
integer per_budget;
reg [1:0] rt;
reg [10:0] rm;
reg [7:0] rv;
reg re;
reg [7:0] ivs [0:3];
reg [1:0] tys [0:3];
initial begin
for (ri = 0; ri < 128; ri = ri + 1) reach[ri] = 1'b0;
ivs[0] = 8'd1; ivs[1] = 8'd2; ivs[2] = 8'd4; ivs[3] = 8'd8;
tys[0] = X_CTRL; tys[1] = X_ISO; tys[2] = X_INT; tys[3] = X_BULK;
seed = 32'd27006;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- one endpoint of every
// (type, interval), observed through all 8 frame phases.
// 4 x 4 x 8 = 128.
// =============================================================
for (ti = 0; ti < 4; ti = ti + 1)
for (vi = 0; vi < 4; vi = vi + 1) begin
reset_dut;
cfg(3'd0, tys[ti], 11'd64, ivs[vi], 1'b1);
for (fi = 0; fi < 8; fi = fi + 1) begin
run_frame;
ri = (ti * 32) + (vi * 8) + fi;
reach[ri] = 1'b1;
end
end
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE) -- ORDER, over every
// assignment of the four types to four slots.
//
// The types are placed in slots in a DIFFERENT order from the
// order they must be emitted in, so a scheduler that simply
// walked its table would produce the wrong sequence. 24
// permutations, and the emitted order must be identical in all.
// =============================================================
for (pj = 0; pj < 24; pj = pj + 1) begin
reset_dut;
// a simple permutation generator: rotate and swap
cfg(3'd0, tys[(pj) % 4], 11'd64, 8'd1, 1'b1);
cfg(3'd1, tys[(pj / 4 + 1) % 4], 11'd64, 8'd1, 1'b1);
cfg(3'd2, tys[(pj / 8 + 2) % 4], 11'd64, 8'd1, 1'b1);
cfg(3'd3, tys[(pj / 12 + 3) % 4], 11'd64, 8'd1, 1'b1);
run_frame;
run_frame;
end
// =============================================================
// PHASE 3 (DIRECTED) -- FAIRNESS, which one frame cannot show.
//
// EIGHT bulk endpoints of 800 bytes each. 813 bytes apiece means
// exactly ONE fits in a 1500-byte frame, so seven are passed over
// every single frame -- and the question is whether it is always
// the same seven.
//
// The sizing matters. An earlier version used 400-byte endpoints,
// three of which fit, and the worst wait was 1 frame: a fixed
// priority would have looked almost as good as a rotation. Making
// only ONE fit forces the wait to N_SLOT-1 under a correct
// rotation and to UNBOUNDED under a fixed one.
// =============================================================
reset_dut;
for (k = 0; k < N_SLOT; k = k + 1)
cfg(k[2:0], X_BULK, 11'd800, 8'd0, 1'b1);
worst_wait = 0;
fair_check = 1'b1;
for (k = 0; k < 64; k = k + 1) run_frame;
fair_check = 1'b0;
// ---- the bound is only PROVABLE for this configuration ----
//
// The pointer advances when an endpoint is served, so the wait is
// bounded by N_SLOT only while at least one bulk endpoint fits in every
// frame -- which is true here by construction (413 bytes into 1500) and
// NOT true in general. In a frame whose periodic traffic fills the
// budget no bulk is served, the pointer does not move, and every bulk
// endpoint waits one frame longer.
//
// So the bound is asserted here, where it holds, and the run-wide worst
// case is reported as an observation rather than checked against a
// number it is not required to meet.
ph3_worst = worst_wait;
ck(ph3_worst <= N_SLOT,
"a bulk endpoint waited longer than the round-robin bound");
ck(ph3_worst > 0,
"no bulk endpoint was ever passed over: fairness was not exercised");
// =============================================================
// PHASE 4 (DIRECTED) -- periodic traffic must NOT be displaced
// by bulk, however much bulk there is.
//
// One isochronous endpoint plus eight slots' worth of bulk. The
// isochronous endpoint must be served in every single frame.
// =============================================================
reset_dut;
cfg(3'd0, X_ISO, 11'd1023, 8'd1, 1'b1);
for (k = 1; k < N_SLOT; k = k + 1)
cfg(k[2:0], X_BULK, 11'd400, 8'd0, 1'b1);
for (k = 0; k < 32; k = k + 1) begin
run_frame;
a4 = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == 3'd0) a4 = 1;
ck(a4 == 1, "bulk traffic displaced an isochronous endpoint");
end
// =============================================================
// PHASE 5 (DIRECTED) -- a configuration write mid-frame is
// ignored, because it would change a schedule already running.
// =============================================================
// Swept over every slot and over every cycle of the frame at which
// the write could land, because one attempt kills the mutation once
// and one kill is indistinguishable from luck.
for (pj = 0; pj < N_SLOT; pj = pj + 1)
for (vi = 1; vi < 6; vi = vi + 1) begin
reset_dut;
cfg(pj[2:0], X_ISO, 11'd64, 8'd1, 1'b1);
frame_tick = 1'b1;
@(posedge clk); #1;
frame_tick = 1'b0;
steps = steps + 1;
// advance into the frame by a varying number of cycles
for (k = 0; k < vi; k = k + 1) begin @(posedge clk); #1; steps = steps + 1; end
// mid-frame: try to disable the endpoint
cfg_wr = 1'b1; cfg_slot = pj[2:0]; cfg_type = X_ISO;
cfg_maxp = 11'd64; cfg_interval = 8'd1; cfg_enable = 1'b0;
@(posedge clk); #1;
cfg_wr = 1'b0;
steps = steps + 1;
// drain the frame
k = 0;
while (frame_busy && (k < 64)) begin @(posedge clk); #1; k = k + 1; end
// the endpoint must still be enabled: the write was refused
run_frame;
a4 = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == pj[2:0]) a4 = 1;
ck(a4 == 1, "a mid-frame configuration write took effect");
end
// =============================================================
// PHASE 6 (RANDOM) -- an arbitrary endpoint mix.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 3000; k = k + 1) begin
if ((k % 8) == 0) begin
// ---- the random phase applies ADMISSION CONTROL ----
//
// A scheduler cannot keep a promise the schedule never had room
// for. Configuring an arbitrary mix overcommits the frame, the
// periodic endpoints that do not fit miss their deadlines, and the
// design correctly reports it -- 386 "missed deadline" errors in
// the first run of this bench, none of them the scheduler's fault.
//
// Chapter 27.5's admission control is a PRECONDITION for this
// chapter's guarantees, so the stimulus has to respect it.
per_budget = 0;
for (a4 = 0; a4 < N_SLOT; a4 = a4 + 1) begin
rt = tys[urand(0) % 4];
rm = (urand(0) % 2) ? 11'd64 : 11'd400;
rv = ivs[urand(0) % 4];
re = ((urand(0) % 4) != 0);
if (re && ((rt == X_ISO) || (rt == X_INT))) begin
if ((per_budget + rm + TXN_OH) > 1350) re = 1'b0;
else per_budget = per_budget + rm + TXN_OH;
end
cfg(a4[2:0], rt, rm, rv, re);
end
end
run_frame;
end
`endif
n_reach = 0;
for (ri = 0; ri < 128; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d frames=%0d reach=%0d/128 errors=%0d",
steps, checks, g_frames, n_reach, errors);
$display("[sched] entries=%0d periodic=%0d bulk=%0d starved=%0d",
g_entries, g_periodic, g_bulk, g_starved);
$display("[fairness] round-robin bound phase=%0d (<= %0d required), run-wide worst=%0d",
ph3_worst, N_SLOT, worst_wait);
$display("[the whole point] out-of-order entries = %0d, frame overruns = %0d",
n_order, n_over);
if (n_reach != 128) 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_frame_sched.
//
// Two properties here cannot be checked one transaction at a time.
//
// ORDER is a property of a whole frame: every entry must belong to a
// type of priority at least as low as the one before it. So the bench
// collects a frame's entries and checks the sequence, not the items.
//
// FAIRNESS is a property of many frames: a round-robin pointer cannot
// be observed in one frame at all. So the bench counts how many frames
// each bulk endpoint waits, and asserts a bound over the whole run.
// =====================================================================
`timescale 1ns/1ps
module tb_fs_sv;
localparam integer FRAME_BYTES = 1500;
localparam integer N_SLOT = 8;
localparam integer TXN_OH = 13;
localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;
logic clk = 1'b0, rst_n = 1'b0;
logic frame_tick = 1'b0;
logic cfg_wr = 1'b0;
logic [2:0] cfg_slot = 3'd0;
logic [1:0] cfg_type = X_CTRL;
logic [10:0] cfg_maxp = 11'd0;
logic [7:0] cfg_interval = 8'd1;
logic cfg_enable = 1'b0;
logic ent_valid, frame_busy;
logic [2:0] ent_slot;
logic [1:0] ent_type;
logic [15:0] ent_bytes, frame_used, frame_num;
logic [31:0] n_frames, n_entries, n_periodic, n_bulk,
n_missed, n_overrun, n_starved;
usb_frame_sched #(.FRAME_BYTES(FRAME_BYTES), .N_SLOT(N_SLOT),
.TXN_OH(TXN_OH)) dut (
.clk(clk), .rst_n(rst_n), .frame_tick(frame_tick),
.cfg_wr(cfg_wr), .cfg_slot(cfg_slot), .cfg_type(cfg_type),
.cfg_maxp(cfg_maxp), .cfg_interval(cfg_interval), .cfg_enable(cfg_enable),
.ent_valid(ent_valid), .ent_slot(ent_slot), .ent_type(ent_type),
.ent_bytes(ent_bytes), .frame_busy(frame_busy),
.frame_used(frame_used), .frame_num(frame_num),
.n_frames(n_frames), .n_entries(n_entries), .n_periodic(n_periodic),
.n_bulk(n_bulk), .n_missed(n_missed), .n_overrun(n_overrun),
.n_starved(n_starved)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0, frames = 0;
integer seed;
function automatic logic [31:0] urand(bit dummy);
return $random(seed) & 32'h3FFF_FFFF;
endfunction
// ---- the shadow table ----
logic [1:0] s_ty [0:N_SLOT-1];
logic [10:0] s_mp [0:N_SLOT-1];
logic [7:0] s_iv [0:N_SLOT-1];
logic s_en [0:N_SLOT-1];
// ---- per-frame collection ----
logic [2:0] f_slot [0:63];
logic [1:0] f_type [0:63];
logic [15:0] f_byte [0:63];
integer f_n;
logic [15:0] f_sum;
// ---- the two headline counters ----
integer n_order = 0; // an entry out of priority order
integer n_over = 0; // a frame that exceeded its byte budget
// ---- fairness: frames since each bulk slot was last served ----
integer bulk_wait [0:N_SLOT-1];
integer worst_wait = 0;
integer ph3_worst = 0;
logic fair_check = 1'b0;
integer g_frames = 0, g_entries = 0, g_periodic = 0, g_bulk = 0, g_starved = 0;
// ---- exhaustive reach over (type, interval, frame phase) ----
logic reach [0:127];
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 frame=%0d: %0s", $time, frames, what);
end
end
endtask
// ---- priority, which is NOT the numeric type code ----
//
// The type encoding is arbitrary; the placement order is the design's
// whole thesis. Mapping one to the other explicitly is how the bench
// avoids checking the encoding by accident.
function automatic logic [1:0] prio(logic [1:0] t);
begin
case (t)
X_ISO: prio = 2'd0;
X_INT: prio = 2'd1;
X_CTRL: prio = 2'd2;
default: prio = 2'd3; // bulk
endcase
end
endfunction
function automatic logic due_f(logic [7:0] interval, logic [15:0] f);
begin
if (interval <= 8'd1) due_f = 1'b1;
else due_f = ((f & {8'd0, (interval - 8'd1)}) == 16'd0);
end
endfunction
// ---------------------------------------------------------------
// Configure one slot. Only legal between frames.
// ---------------------------------------------------------------
task cfg(input logic [2:0] sl, input logic [1:0] t, input logic [10:0] m,
input logic [7:0] interval, input logic e);
begin
cfg_wr = 1'b1; cfg_slot = sl; cfg_type = t;
cfg_maxp = m; cfg_interval = interval; cfg_enable = e;
s_ty[sl] = t; s_mp[sl] = m; s_iv[sl] = interval; s_en[sl] = e;
@(posedge clk); #1;
cfg_wr = 1'b0;
steps = steps + 1;
end
endtask
// ---------------------------------------------------------------
// Run one complete frame and check everything about it.
// ---------------------------------------------------------------
task run_frame;
integer guard, a, p;
logic [15:0] fnum_at_start;
begin
fnum_at_start = frame_num;
f_n = 0;
f_sum = 16'd0;
frame_tick = 1'b1;
@(posedge clk); #1;
frame_tick = 1'b0;
steps = steps + 1;
// ---- collect until the frame ends ----
//
// Bounded. An unbounded wait here is an infinite loop the moment a
// mutation stops the phase machine advancing, and the run would
// never reach the summary that says which mutation did it.
guard = 0;
while (frame_busy && (guard < 8 * N_SLOT + 32)) begin
if (ent_valid) begin
if (f_n < 64) begin
f_slot[f_n] = ent_slot;
f_type[f_n] = ent_type;
f_byte[f_n] = ent_bytes;
end
f_sum = f_sum + ent_bytes;
f_n = f_n + 1;
end
// ---- checked EVERY cycle, not once per frame ----
//
// The design's running byte total must equal the bench's
// accumulation at every point inside the frame, not merely at the
// end. A scheduler that overshoots and then corrects would pass an
// end-of-frame check and fail this one.
ck(frame_used === f_sum[15:0],
"the running byte total disagrees mid-frame");
ck(!(ent_valid && !frame_busy),
"an entry was emitted outside a frame");
@(posedge clk); #1;
steps = steps + 1;
guard = guard + 1;
end
ck(guard < 8 * N_SLOT + 32, "the frame never ended");
// one more cycle to catch an entry emitted on the last phase cycle
if (ent_valid) begin
if (f_n < 64) begin
f_slot[f_n] = ent_slot; f_type[f_n] = ent_type; f_byte[f_n] = ent_bytes;
end
f_sum = f_sum + ent_bytes;
f_n = f_n + 1;
end
frames = frames + 1;
g_frames = g_frames + 1;
// ---- PROPERTY 1: priority order across the whole frame ----
for (a = 1; a < f_n && a < 64; a = a + 1)
if (prio(f_type[a]) < prio(f_type[a-1])) begin
n_order = n_order + 1;
ck(1'b0, "an entry was placed before a higher-priority one");
end
ck(n_order == 0, "the frame was not in priority order");
// ---- PROPERTY 2: each entry's cost is maxp + overhead ----
for (a = 0; a < f_n && a < 64; a = a + 1)
ck(f_byte[a] === ({5'd0, s_mp[f_slot[a]]} + TXN_OH),
"an entry was charged the wrong number of bytes");
// ---- PROPERTY 3: the frame never overruns ----
if (f_sum > FRAME_BYTES) n_over = n_over + 1;
ck(f_sum <= FRAME_BYTES, "the frame exceeded its byte budget");
ck(n_over == 0, "a frame overran");
ck(n_overrun === 32'd0, "the design detected its own overrun");
// ---- PROPERTY 4: every DUE periodic endpoint that fits was served ----
//
// The liveness half. A schedule that places nothing is in perfect
// priority order and perfectly within budget.
for (a = 0; a < N_SLOT; a = a + 1)
if (s_en[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT))
&& due_f(s_iv[a], fnum_at_start)
&& (({5'd0, s_mp[a]} + TXN_OH) <= FRAME_BYTES)) begin
p = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == a[2:0]) p = 1;
ck(p == 1, "a due periodic endpoint was not served in its frame");
end
// ---- PROPERTY 5: served ONLY when due ----
//
// The other half of property 4, and the half that is easy to forget:
// a scheduler that serves every periodic endpoint every frame misses
// no deadlines and violates nothing the liveness check looks at. It
// is still wrong -- it spends bandwidth that was reserved for
// somebody else, and for an isochronous endpoint it delivers data the
// application has not produced yet.
for (a = 0; a < N_SLOT; a = a + 1)
if (s_en[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT))
&& !due_f(s_iv[a], fnum_at_start)) begin
p = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == a[2:0]) p = 1;
ck(p == 0, "a periodic endpoint was served in a frame it was not due");
end
// ---- PROPERTY 6: no missed deadlines, ever ----
ck(n_missed === 32'd0, "the design reported a missed deadline");
// ---- PROPERTY 7: the fairness bound, EVERY frame ----
//
// Checked per frame rather than once at the end. A single
// end-of-phase check kills a non-rotating pointer exactly once, and
// one kill is indistinguishable from luck.
if (fair_check)
for (a = 0; a < N_SLOT; a = a + 1)
if (s_en[a] && (s_ty[a] == X_BULK))
ck(bulk_wait[a] <= N_SLOT,
"a bulk endpoint waited longer than the round-robin bound");
// ---- fairness bookkeeping, checked over the whole run ----
for (a = 0; a < N_SLOT; a = a + 1) begin
if (s_en[a] && (s_ty[a] == X_BULK)) begin
p = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == a[2:0]) p = 1;
if (p) bulk_wait[a] = 0;
else begin
bulk_wait[a] = bulk_wait[a] + 1;
if (bulk_wait[a] > worst_wait) worst_wait = bulk_wait[a];
end
end else begin
bulk_wait[a] = 0;
end
end
g_entries = g_entries + f_n;
g_periodic = n_periodic;
g_bulk = n_bulk;
g_starved = n_starved;
end
endtask
task reset_dut;
integer a;
begin
rst_n = 1'b0;
frame_tick = 0; cfg_wr = 0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
for (a = 0; a < N_SLOT; a = a + 1) begin
s_ty[a] = X_CTRL; s_mp[a] = 11'd0; s_iv[a] = 8'd1; s_en[a] = 1'b0;
bulk_wait[a] = 0;
end
@(posedge clk); #1;
end
endtask
integer ti, vi, fi, k, a4, pj;
integer per_budget;
logic [1:0] rt;
logic [10:0] rm;
logic [7:0] rv;
logic re;
logic [7:0] ivs [0:3];
logic [1:0] tys [0:3];
initial begin
for (ri = 0; ri < 128; ri = ri + 1) reach[ri] = 1'b0;
ivs[0] = 8'd1; ivs[1] = 8'd2; ivs[2] = 8'd4; ivs[3] = 8'd8;
tys[0] = X_CTRL; tys[1] = X_ISO; tys[2] = X_INT; tys[3] = X_BULK;
seed = 32'd27006;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- one endpoint of every
// (type, interval), observed through all 8 frame phases.
// 4 x 4 x 8 = 128.
// =============================================================
for (ti = 0; ti < 4; ti = ti + 1)
for (vi = 0; vi < 4; vi = vi + 1) begin
reset_dut;
cfg(3'd0, tys[ti], 11'd64, ivs[vi], 1'b1);
for (fi = 0; fi < 8; fi = fi + 1) begin
run_frame;
ri = (ti * 32) + (vi * 8) + fi;
reach[ri] = 1'b1;
end
end
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE) -- ORDER, over every
// assignment of the four types to four slots.
//
// The types are placed in slots in a DIFFERENT order from the
// order they must be emitted in, so a scheduler that simply
// walked its table would produce the wrong sequence. 24
// permutations, and the emitted order must be identical in all.
// =============================================================
for (pj = 0; pj < 24; pj = pj + 1) begin
reset_dut;
// a simple permutation generator: rotate and swap
cfg(3'd0, tys[(pj) % 4], 11'd64, 8'd1, 1'b1);
cfg(3'd1, tys[(pj / 4 + 1) % 4], 11'd64, 8'd1, 1'b1);
cfg(3'd2, tys[(pj / 8 + 2) % 4], 11'd64, 8'd1, 1'b1);
cfg(3'd3, tys[(pj / 12 + 3) % 4], 11'd64, 8'd1, 1'b1);
run_frame;
run_frame;
end
// =============================================================
// PHASE 3 (DIRECTED) -- FAIRNESS, which one frame cannot show.
//
// EIGHT bulk endpoints of 800 bytes each. 813 bytes apiece means
// exactly ONE fits in a 1500-byte frame, so seven are passed over
// every single frame -- and the question is whether it is always
// the same seven.
//
// The sizing matters. An earlier version used 400-byte endpoints,
// three of which fit, and the worst wait was 1 frame: a fixed
// priority would have looked almost as good as a rotation. Making
// only ONE fit forces the wait to N_SLOT-1 under a correct
// rotation and to UNBOUNDED under a fixed one.
// =============================================================
reset_dut;
for (k = 0; k < N_SLOT; k = k + 1)
cfg(k[2:0], X_BULK, 11'd800, 8'd0, 1'b1);
worst_wait = 0;
fair_check = 1'b1;
for (k = 0; k < 64; k = k + 1) run_frame;
fair_check = 1'b0;
// ---- the bound is only PROVABLE for this configuration ----
//
// The pointer advances when an endpoint is served, so the wait is
// bounded by N_SLOT only while at least one bulk endpoint fits in every
// frame -- which is true here by construction (413 bytes into 1500) and
// NOT true in general. In a frame whose periodic traffic fills the
// budget no bulk is served, the pointer does not move, and every bulk
// endpoint waits one frame longer.
//
// So the bound is asserted here, where it holds, and the run-wide worst
// case is reported as an observation rather than checked against a
// number it is not required to meet.
ph3_worst = worst_wait;
ck(ph3_worst <= N_SLOT,
"a bulk endpoint waited longer than the round-robin bound");
ck(ph3_worst > 0,
"no bulk endpoint was ever passed over: fairness was not exercised");
// =============================================================
// PHASE 4 (DIRECTED) -- periodic traffic must NOT be displaced
// by bulk, however much bulk there is.
//
// One isochronous endpoint plus eight slots' worth of bulk. The
// isochronous endpoint must be served in every single frame.
// =============================================================
reset_dut;
cfg(3'd0, X_ISO, 11'd1023, 8'd1, 1'b1);
for (k = 1; k < N_SLOT; k = k + 1)
cfg(k[2:0], X_BULK, 11'd400, 8'd0, 1'b1);
for (k = 0; k < 32; k = k + 1) begin
run_frame;
a4 = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == 3'd0) a4 = 1;
ck(a4 == 1, "bulk traffic displaced an isochronous endpoint");
end
// =============================================================
// PHASE 5 (DIRECTED) -- a configuration write mid-frame is
// ignored, because it would change a schedule already running.
// =============================================================
// Swept over every slot and over every cycle of the frame at which
// the write could land, because one attempt kills the mutation once
// and one kill is indistinguishable from luck.
for (pj = 0; pj < N_SLOT; pj = pj + 1)
for (vi = 1; vi < 6; vi = vi + 1) begin
reset_dut;
cfg(pj[2:0], X_ISO, 11'd64, 8'd1, 1'b1);
frame_tick = 1'b1;
@(posedge clk); #1;
frame_tick = 1'b0;
steps = steps + 1;
// advance into the frame by a varying number of cycles
for (k = 0; k < vi; k = k + 1) begin @(posedge clk); #1; steps = steps + 1; end
// mid-frame: try to disable the endpoint
cfg_wr = 1'b1; cfg_slot = pj[2:0]; cfg_type = X_ISO;
cfg_maxp = 11'd64; cfg_interval = 8'd1; cfg_enable = 1'b0;
@(posedge clk); #1;
cfg_wr = 1'b0;
steps = steps + 1;
// drain the frame
k = 0;
while (frame_busy && (k < 64)) begin @(posedge clk); #1; k = k + 1; end
// the endpoint must still be enabled: the write was refused
run_frame;
a4 = 0;
for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
if (f_slot[ri] == pj[2:0]) a4 = 1;
ck(a4 == 1, "a mid-frame configuration write took effect");
end
// =============================================================
// PHASE 6 (RANDOM) -- an arbitrary endpoint mix.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 3000; k = k + 1) begin
if ((k % 8) == 0) begin
// ---- the random phase applies ADMISSION CONTROL ----
//
// A scheduler cannot keep a promise the schedule never had room
// for. Configuring an arbitrary mix overcommits the frame, the
// periodic endpoints that do not fit miss their deadlines, and the
// design correctly reports it -- 386 "missed deadline" errors in
// the first run of this bench, none of them the scheduler's fault.
//
// Chapter 27.5's admission control is a PRECONDITION for this
// chapter's guarantees, so the stimulus has to respect it.
per_budget = 0;
for (a4 = 0; a4 < N_SLOT; a4 = a4 + 1) begin
rt = tys[urand(0) % 4];
rm = (urand(0) % 2) ? 11'd64 : 11'd400;
rv = ivs[urand(0) % 4];
re = ((urand(0) % 4) != 0);
if (re && ((rt == X_ISO) || (rt == X_INT))) begin
if ((per_budget + rm + TXN_OH) > 1350) re = 1'b0;
else per_budget = per_budget + rm + TXN_OH;
end
cfg(a4[2:0], rt, rm, rv, re);
end
end
run_frame;
end
`endif
n_reach = 0;
for (ri = 0; ri < 128; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d frames=%0d reach=%0d/128 errors=%0d",
steps, checks, g_frames, n_reach, errors);
$display("[sched] entries=%0d periodic=%0d bulk=%0d starved=%0d",
g_entries, g_periodic, g_bulk, g_starved);
$display("[fairness] round-robin bound phase=%0d (<= %0d required), run-wide worst=%0d",
ph3_worst, N_SLOT, worst_wait);
$display("[the whole point] out-of-order entries = %0d, frame overruns = %0d",
n_order, n_over);
if (n_reach != 128) 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_frame_sched (VHDL-2008).
--
-- Two properties here cannot be checked one transaction at a time.
-- ORDER is a property of a whole frame; FAIRNESS is a property of many
-- frames and cannot be observed in one at all. So the bench collects a
-- frame's entries and checks the sequence, and counts how many frames
-- each bulk endpoint waits and bounds it over the run.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.fs_pkg.all;
entity tb_fs_vhdl is
generic (DIRECTED_ONLY : boolean := false);
end entity;
architecture sim of tb_fs_vhdl is
constant FRAME_BYTES : natural := 1500;
constant N_SLOT : natural := 8;
constant TXN_OH : natural := 13;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal frame_tick : std_logic := '0';
signal cfg_wr : std_logic := '0';
signal cfg_slot : std_logic_vector(2 downto 0) := (others => '0');
signal cfg_type : std_logic_vector(1 downto 0) := "00";
signal cfg_maxp : std_logic_vector(10 downto 0) := (others => '0');
signal cfg_interval : std_logic_vector(7 downto 0) := x"01";
signal cfg_enable : std_logic := '0';
signal ent_valid, frame_busy : std_logic;
signal ent_slot : std_logic_vector(2 downto 0);
signal ent_type : std_logic_vector(1 downto 0);
signal ent_bytes, frame_used, frame_num : std_logic_vector(15 downto 0);
signal n_frames, n_entries, n_periodic, n_bulk,
n_missed, n_overrun, n_starved : std_logic_vector(31 downto 0);
signal done : boolean := false;
begin
dut : entity work.usb_frame_sched
generic map (FRAME_BYTES => FRAME_BYTES, N_SLOT => N_SLOT, TXN_OH => TXN_OH)
port map (
clk => clk, rst_n => rst_n, frame_tick => frame_tick,
cfg_wr => cfg_wr, cfg_slot => cfg_slot, cfg_type => cfg_type,
cfg_maxp => cfg_maxp, cfg_interval => cfg_interval,
cfg_enable => cfg_enable,
ent_valid => ent_valid, ent_slot => ent_slot, ent_type => ent_type,
ent_bytes => ent_bytes, frame_busy => frame_busy,
frame_used => frame_used, frame_num => frame_num,
n_frames => n_frames, n_entries => n_entries, n_periodic => n_periodic,
n_bulk => n_bulk, n_missed => n_missed, n_overrun => n_overrun,
n_starved => n_starved);
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 frames : natural := 0;
type sl_arr is array (0 to N_SLOT-1) of std_logic;
type nm_arr is array (0 to N_SLOT-1) of natural;
variable s_ty : ty_arr(0 to N_SLOT-1) := (others => XT_CTRL);
variable s_mp : nm_arr := (others => 0);
variable s_iv : nm_arr := (others => 1);
variable s_en : sl_arr := (others => '0');
type f_sl is array (0 to 63) of natural;
type f_ty is array (0 to 63) of xfer_t;
variable f_slot : f_sl := (others => 0);
variable f_type : f_ty := (others => XT_CTRL);
variable f_byte : f_sl := (others => 0);
variable f_n : natural := 0;
variable f_sum : natural := 0;
variable n_order : natural := 0;
variable n_over : natural := 0;
variable bulk_wait : nm_arr := (others => 0);
variable worst_wait, ph3_worst : natural := 0;
variable fair_check : boolean := false;
variable g_frames, g_entries, g_periodic, g_bulk, g_starved : natural := 0;
variable reach : std_logic_vector(0 to 127) := (others => '0');
variable n_reach : natural := 0;
variable rnd : unsigned(31 downto 0) := x"0006E4A7";
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 frame=") & integer'image(frames)
& string'(": ") & what);
writeline(output, ln);
end if;
end if;
end procedure;
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;
-- Priority, which is NOT the numeric type code. The encoding is
-- arbitrary; the placement order is the design's whole thesis, and
-- mapping one to the other explicitly is how the bench avoids checking
-- the encoding by accident.
function prio(t : xfer_t) return natural is
begin
case t is
when XT_ISO => return 0;
when XT_INT => return 1;
when XT_CTRL => return 2;
when XT_BULK => return 3;
end case;
end function;
function due_f(interval : natural; f : natural) return boolean is
begin
if interval <= 1 then return true; end if;
return (f mod interval) = 0;
end function;
procedure cfg(sl : natural; t : xfer_t; m : natural;
interval : natural; e : std_logic) is
begin
cfg_wr <= '1';
cfg_slot <= std_logic_vector(to_unsigned(sl, 3));
cfg_type <= code_of(t);
cfg_maxp <= std_logic_vector(to_unsigned(m, 11));
cfg_interval <= std_logic_vector(to_unsigned(interval, 8));
cfg_enable <= e;
s_ty(sl) := t; s_mp(sl) := m; s_iv(sl) := interval; s_en(sl) := e;
wait until rising_edge(clk);
wait for 1 ns;
cfg_wr <= '0';
steps := steps + 1;
end procedure;
procedure run_frame is
variable guard, p : natural;
variable fnum_at_start : natural;
begin
fnum_at_start := to_integer(unsigned(frame_num));
f_n := 0;
f_sum := 0;
frame_tick <= '1';
wait until rising_edge(clk);
wait for 1 ns;
frame_tick <= '0';
steps := steps + 1;
-- Bounded. An unbounded wait is an infinite loop the moment a
-- mutation stops the phase machine advancing, and the run would never
-- reach the summary that says which mutation did it.
guard := 0;
while frame_busy = '1' and guard < 8 * N_SLOT + 32 loop
if ent_valid = '1' then
if f_n < 64 then
f_slot(f_n) := to_integer(unsigned(ent_slot));
f_type(f_n) := xfer_of(ent_type);
f_byte(f_n) := to_integer(unsigned(ent_bytes));
end if;
f_sum := f_sum + to_integer(unsigned(ent_bytes));
f_n := f_n + 1;
end if;
-- checked EVERY cycle, not once per frame: a scheduler that
-- overshoots and then corrects passes an end-of-frame check.
ck(to_integer(unsigned(frame_used)) = f_sum,
"the running byte total disagrees mid-frame");
ck(not (ent_valid = '1' and frame_busy = '0'),
"an entry was emitted outside a frame");
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
guard := guard + 1;
end loop;
ck(guard < 8 * N_SLOT + 32, "the frame never ended");
if ent_valid = '1' then
if f_n < 64 then
f_slot(f_n) := to_integer(unsigned(ent_slot));
f_type(f_n) := xfer_of(ent_type);
f_byte(f_n) := to_integer(unsigned(ent_bytes));
end if;
f_sum := f_sum + to_integer(unsigned(ent_bytes));
f_n := f_n + 1;
end if;
frames := frames + 1;
g_frames := g_frames + 1;
-- PROPERTY 1: priority order across the whole frame
for a in 1 to 63 loop
if a < f_n then
if prio(f_type(a)) < prio(f_type(a-1)) then
n_order := n_order + 1;
ck(false, "an entry was placed before a higher-priority one");
end if;
end if;
end loop;
ck(n_order = 0, "the frame was not in priority order");
-- PROPERTY 2: each entry's cost is maxp + overhead
for a in 0 to 63 loop
if a < f_n then
ck(f_byte(a) = s_mp(f_slot(a)) + TXN_OH,
"an entry was charged the wrong number of bytes");
end if;
end loop;
-- PROPERTY 3: the frame never overruns
if f_sum > FRAME_BYTES then n_over := n_over + 1; end if;
ck(f_sum <= FRAME_BYTES, "the frame exceeded its byte budget");
ck(n_over = 0, "a frame overran");
ck(to_integer(unsigned(n_overrun)) = 0,
"the design detected its own overrun");
-- PROPERTY 4: every DUE periodic endpoint that fits was served
for a in 0 to N_SLOT-1 loop
if s_en(a) = '1' and periodic_t(s_ty(a))
and due_f(s_iv(a), fnum_at_start)
and (s_mp(a) + TXN_OH) <= FRAME_BYTES then
p := 0;
for i in 0 to 63 loop
if i < f_n and f_slot(i) = a then p := 1; end if;
end loop;
ck(p = 1, "a due periodic endpoint was not served in its frame");
end if;
end loop;
-- PROPERTY 5: served ONLY when due. A scheduler that serves every
-- endpoint every frame misses no deadlines and is still wrong: it
-- spends bandwidth reserved for somebody else.
for a in 0 to N_SLOT-1 loop
if s_en(a) = '1' and periodic_t(s_ty(a))
and not due_f(s_iv(a), fnum_at_start) then
p := 0;
for i in 0 to 63 loop
if i < f_n and f_slot(i) = a then p := 1; end if;
end loop;
ck(p = 0, "a periodic endpoint was served in a frame it was not due");
end if;
end loop;
-- PROPERTY 6: no missed deadlines, ever
ck(to_integer(unsigned(n_missed)) = 0,
"the design reported a missed deadline");
-- fairness bookkeeping
for a in 0 to N_SLOT-1 loop
if s_en(a) = '1' and s_ty(a) = XT_BULK then
p := 0;
for i in 0 to 63 loop
if i < f_n and f_slot(i) = a then p := 1; end if;
end loop;
if p = 1 then
bulk_wait(a) := 0;
else
bulk_wait(a) := bulk_wait(a) + 1;
if bulk_wait(a) > worst_wait then worst_wait := bulk_wait(a); end if;
end if;
else
bulk_wait(a) := 0;
end if;
end loop;
-- PROPERTY 7: the fairness bound, EVERY frame
if fair_check then
for a in 0 to N_SLOT-1 loop
if s_en(a) = '1' and s_ty(a) = XT_BULK then
ck(bulk_wait(a) <= N_SLOT,
"a bulk endpoint waited longer than the round-robin bound");
end if;
end loop;
end if;
g_entries := g_entries + f_n;
g_periodic := to_integer(unsigned(n_periodic));
g_bulk := to_integer(unsigned(n_bulk));
g_starved := to_integer(unsigned(n_starved));
end procedure;
procedure reset_dut is
begin
rst_n <= '0';
frame_tick <= '0'; cfg_wr <= '0';
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
s_ty := (others => XT_CTRL);
s_mp := (others => 0);
s_iv := (others => 1);
s_en := (others => '0');
bulk_wait := (others => 0);
wait until rising_edge(clk);
wait for 1 ns;
end procedure;
type nat4 is array (0 to 3) of natural;
constant ivs : nat4 := (1, 2, 4, 8);
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 per_budget : natural;
variable rt : xfer_t;
variable rm, rv : natural;
variable re : std_logic;
variable a4, p2 : natural;
begin
-- PHASE 1 (DIRECTED, EXHAUSTIVE) -- one endpoint of every
-- (type, interval), observed through all 8 frame phases. 4 x 4 x 8 = 128
for ti in 0 to 3 loop
for vi in 0 to 3 loop
reset_dut;
cfg(0, tys(ti), 64, ivs(vi), '1');
for fi in 0 to 7 loop
run_frame;
ri := ti*32 + vi*8 + fi;
reach(ri) := '1';
end loop;
end loop;
end loop;
-- PHASE 2 (DIRECTED, EXHAUSTIVE) -- ORDER, over 24 assignments of the
-- four types to four slots. The types sit in slots in a DIFFERENT order
-- from the one they must be emitted in, so a scheduler that simply
-- walked its table would produce the wrong sequence.
for pj in 0 to 23 loop
reset_dut;
cfg(0, tys(pj mod 4), 64, 1, '1');
cfg(1, tys((pj / 4 + 1) mod 4), 64, 1, '1');
cfg(2, tys((pj / 8 + 2) mod 4), 64, 1, '1');
cfg(3, tys((pj / 12 + 3) mod 4), 64, 1, '1');
run_frame;
run_frame;
end loop;
-- PHASE 3 (DIRECTED) -- FAIRNESS, which one frame cannot show.
--
-- EIGHT bulk endpoints of 800 bytes. 813 apiece means exactly ONE fits
-- in a 1500-byte frame, so seven are passed over every frame -- and the
-- question is whether it is always the same seven. The sizing matters:
-- with three fitting, the worst wait is 1 frame and a fixed priority
-- looks almost as good as a rotation.
reset_dut;
for k in 0 to N_SLOT-1 loop
cfg(k, XT_BULK, 800, 0, '1');
end loop;
worst_wait := 0;
fair_check := true;
for k in 0 to 63 loop run_frame; end loop;
fair_check := false;
ph3_worst := worst_wait;
ck(ph3_worst <= N_SLOT,
"a bulk endpoint waited longer than the round-robin bound");
ck(ph3_worst > 0,
"no bulk endpoint was ever passed over: fairness was not exercised");
-- PHASE 4 (DIRECTED) -- periodic must NOT be displaced by bulk
reset_dut;
cfg(0, XT_ISO, 1023, 1, '1');
for k in 1 to N_SLOT-1 loop
cfg(k, XT_BULK, 400, 0, '1');
end loop;
for k in 0 to 31 loop
run_frame;
a4 := 0;
for i in 0 to 63 loop
if i < f_n and f_slot(i) = 0 then a4 := 1; end if;
end loop;
ck(a4 = 1, "bulk traffic displaced an isochronous endpoint");
end loop;
-- PHASE 5 (DIRECTED) -- a mid-frame configuration write is ignored,
-- swept over every slot and over every cycle at which it could land.
for pj in 0 to N_SLOT-1 loop
for vi in 1 to 5 loop
reset_dut;
cfg(pj, XT_ISO, 64, 1, '1');
frame_tick <= '1';
wait until rising_edge(clk);
wait for 1 ns;
frame_tick <= '0';
steps := steps + 1;
for k in 1 to vi loop
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
end loop;
cfg_wr <= '1';
cfg_slot <= std_logic_vector(to_unsigned(pj, 3));
cfg_type <= code_of(XT_ISO);
cfg_maxp <= std_logic_vector(to_unsigned(64, 11));
cfg_interval <= x"01";
cfg_enable <= '0';
wait until rising_edge(clk);
wait for 1 ns;
cfg_wr <= '0';
steps := steps + 1;
p2 := 0;
while frame_busy = '1' and p2 < 64 loop
wait until rising_edge(clk);
wait for 1 ns;
p2 := p2 + 1;
end loop;
run_frame;
a4 := 0;
for i in 0 to 63 loop
if i < f_n and f_slot(i) = pj then a4 := 1; end if;
end loop;
ck(a4 = 1, "a mid-frame configuration write took effect");
end loop;
end loop;
-- PHASE 6 (RANDOM) -- an arbitrary but ADMISSIBLE endpoint mix.
--
-- A scheduler cannot keep a promise the schedule never had room for, so
-- the stimulus applies chapter 27.5's admission control before
-- configuring: an arbitrary mix overcommits the frame and the periodic
-- endpoints that do not fit miss deadlines that were never affordable.
if not DIRECTED_ONLY then
reset_dut;
for k in 0 to 2999 loop
if (k mod 8) = 0 then
per_budget := 0;
for a in 0 to N_SLOT-1 loop
rt := tys(nxt mod 4);
if (nxt mod 2) = 1 then rm := 64; else rm := 400; end if;
rv := ivs(nxt mod 4);
if (nxt mod 4) /= 0 then re := '1'; else re := '0'; end if;
if re = '1' and periodic_t(rt) then
if (per_budget + rm + TXN_OH) > 1350 then
re := '0';
else
per_budget := per_budget + rm + TXN_OH;
end if;
end if;
cfg(a, rt, rm, rv, re);
end loop;
end if;
run_frame;
end loop;
end if;
n_reach := 0;
for i in 0 to 127 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'(" frames=") & integer'image(g_frames)
& string'(" reach=") & integer'image(n_reach) & string'("/128")
& string'(" errors=") & integer'image(errors));
writeline(output, ln);
write(ln, string'("[sched] entries=") & integer'image(g_entries)
& string'(" periodic=") & integer'image(g_periodic)
& string'(" bulk=") & integer'image(g_bulk)
& string'(" starved=") & integer'image(g_starved));
writeline(output, ln);
write(ln, string'("[fairness] round-robin bound phase=")
& integer'image(ph3_worst) & string'(" (<= ")
& integer'image(N_SLOT) & string'(" required), run-wide worst=")
& integer'image(worst_wait));
writeline(output, ln);
write(ln, string'("[the whole point] out-of-order entries = ")
& integer'image(n_order) & string'(", frame overruns = ")
& integer'image(n_over));
writeline(output, ln);
if n_reach /= 128 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;9. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| (type × interval × frame phase) reached | 128 / 128 | 128 / 128 | 128 / 128 |
| type-to-slot assignments swept | 24 / 24 | 24 / 24 | 24 / 24 |
| mid-frame write attempts swept | 40 / 40 | 40 / 40 | 40 / 40 |
| Steps | 115976 | 115976 | 115976 |
| Frames run | 3312 | 3312 | 3312 |
| Checks executed | 260898 | 260898 | 260799 |
| schedule entries emitted | 12832 | 12832 | 12685 |
| periodic entries | 4074 | 4074 | 4103 |
| bulk entries | 4048 | 4048 | 3781 |
| bulk slots passed over for room | 512 | 512 | 451 |
| round-robin worst wait (bounded phase) | 7 ≤ 8 | 7 ≤ 8 | 7 ≤ 8 |
| out-of-order entries | 0 | 0 | 0 |
| frame overruns | 0 | 0 | 0 |
| Result | PASS | PASS | PASS |
The ordering sweep is worth a word. Phase 2 assigns the four transfer types to four slots in 24 different arrangements, so the order the types sit in the table is almost never the order they must be emitted in. A scheduler that simply walked its table would pass a test where slot 0 happened to hold the isochronous endpoint and fail 23 of the 24.
10. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| F1 | bulk is placed BEFORE periodic | 8799 | 8799 | 8246 |
| F4 | no frame-budget check before placing a bulk entry | 6762 | 6762 | 6695 |
| F5 | the due test ignores the interval | 4768 | 4768 | 4787 |
| F3 | interrupt is placed before isochronous | 3698 | 3698 | 3657 |
| F6 | a served endpoint's age is not cleared | 3090 | 3090 | 3098 |
| F2 | the round-robin pointer never rotates | 386 | 386 | 393 |
| F7 | a configuration write is accepted mid-frame | 80 | 80 | 80 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages.
F1 is the mutation this chapter exists for and it scores highest. Placing bulk first does not corrupt a byte: every entry is still charged correctly, the frame still fits, and the order is still internally consistent. What breaks is the guarantee — the isochronous endpoint that was admitted on the promise of a slot in every frame now gets whatever bulk left behind, which on a busy bus is nothing.
Directed against random
| # | V all | V directed | V random | VHDL all | VHDL directed | VHDL random |
|---|---|---|---|---|---|---|
| F1 | 8799 | 313 | 8486 | 8246 | 313 | 7933 |
| F2 | 386 | 386 | 0 | 393 | 393 | 0 |
| F3 | 3698 | 184 | 3514 | 3657 | 184 | 3473 |
| F4 | 6762 | 329 | 6433 | 6695 | 329 | 6366 |
| F5 | 4768 | 34 | 4734 | 4787 | 34 | 4753 |
| F6 | 3090 | 100 | 2990 | 3098 | 100 | 2998 |
| F7 | 80 | 80 | 0 | 80 | 80 | 0 |
Every directed column is identical across Verilog and VHDL.
F2 and F7 have a random contribution of exactly zero, and that is the most informative pair of numbers in the chapter. Neither the round-robin bound nor the mid-frame-write refusal can be reached by stimulus that does not deliberately set them up: fairness needs a configuration in which the resource is scarce enough to force a choice, and the mid-frame write needs a write issued at a cycle a driver would never choose.
Random stimulus does not construct either situation, however long it runs. Those two properties exist in this suite only because somebody wrote a phase for them.
11. F2 Scored 1, and 1 Is Indistinguishable From Luck
The first run of this matrix gave the round-robin mutation a score of one.
One error, in 250,000 checks, for a defect that starves seven endpoints out of eight indefinitely. The reason was that the fairness bound was asserted once, at the end of the fairness phase, against the worst wait observed across it. One assertion, one failure.
Before: run 64 frames; then once:
assert worst_wait <= N_SLOT -> 1 error
After: every frame, for every bulk slot:
assert bulk_wait[slot] <= N_SLOT -> 386 errors
Same property. Same stimulus. 386x the evidence.The fix was not more stimulus. It was to check the property where it holds — at every frame, for every endpoint — rather than summarising it into a single number and checking that.
12. F4 and F5 Both Scored Zero, For Opposite Reasons
Two mutations survived the first run entirely, and the two diagnoses are worth contrasting because the fixes are nothing alike.
F4 was unreachable, not unchecked
F4 originally removed the frame-budget check from the isochronous placement. It scored zero because the check can never bind there: chapter 27.5's admission control caps periodic traffic at 1350 bytes of a 1500-byte frame, so isochronous placement has room by construction and the test it was guarded by is never false.
The fix was to move the mutation to where the check actually does work: bulk. Bulk is the unbounded type, the frame budget is the only thing that stops it overrunning, and F4 went from 0 to 6762.
F5 was genuinely unchecked
F5 makes every periodic endpoint due in every frame. It scored zero for a completely different reason: nothing in the suite said an endpoint may not be served early.
Property 4 asserts that a due endpoint is served. Serving one that is not due violates nothing it says. And the deadline tracker cannot object either — serving more often than required can only make deadlines easier to meet.
So the suite gained property 5: a periodic endpoint is served only when due. F5 went from 0 to 4768.
13. Two Design Bugs the Bench Found
The round-robin pointer never rotated. Covered above — a single pointer advanced once per examined slot advances N_SLOT times per frame and lands back where it began. Found because the fairness phase measured a worst wait of 63 frames where 7 was expected.
The random phase was asking for the impossible. The first random phase configured an arbitrary endpoint mix and the design reported 386 missed deadlines. The scheduler was right: the stimulus had overcommitted the frame, and a periodic endpoint that does not fit misses a deadline nobody could have honoured.
Chapter 27.5 answers: can these endpoints coexist?
Chapter 27.6 answers: in what order, this frame?
The second only has an answer if the first said YES.
So the random phase applies admission control before
configuring -- it is a PRECONDITION of the guarantees
this chapter's design makes, not an optional extra.14. Follow-Ups the Interviewer Will Ask
"What happens if the periodic traffic does not all fit?" It cannot happen, because admission control refused the endpoint that would not fit. If it does happen, the scheduler is being run outside its contract and some endpoint misses a deadline — which is exactly why the 90% cap exists.
"How does the host know a periodic endpoint is due?" The interval is a power of two, so "every Nth frame" is a mask test on the frame number — no division, no per-endpoint counter. That is why the interval must be a power of two.
"What stops one bulk endpoint starving the others?" A rotating pointer, and nothing else. There is no reservation to appeal to.
"Is the bulk wait bounded?" Only while at least one bulk endpoint fits in every frame. In a frame whose periodic traffic fills the budget, no bulk is served, the pointer does not advance, and every bulk endpoint waits one frame longer. The bound is N_SLOT plus the number of such frames — which is why this suite asserts it where it is provable and reports the run-wide worst case rather than checking it against a number it is not required to meet.
"Can the schedule be changed while a frame is running?" No. A write mid-frame would change a schedule the host is already executing, and an endpoint already placed would be served under its old parameters. The host reconfigures between frames.
"What is different at high speed?" Microframes of 125 µs, so a periodic endpoint can be polled eight times as often; the periodic cap drops to 80%; and split transactions appear, where a high-speed hub's transaction translator holds a full-speed transaction across several microframes.
"Where does this go wrong in real controllers?" Almost always the same two places: the ordering (bulk somewhere it should not be) and the rotation (a pointer that does not survive the frame boundary). Both produce a device that works on a quiet bus.
15. UVM: Checking a Sequence and a Trend
// Two of this design's properties are not about transactions at all, and a
// scoreboard built around a queue of expected items cannot express either.
//
// ORDER is a property of one frame's SEQUENCE.
// FAIRNESS is a property of MANY frames and is invisible in one.
//
// So this component buffers a frame, checks the sequence when the frame
// closes, and keeps a per-endpoint wait history across the whole run.
typedef enum bit [1:0] { XT_CTRL, XT_ISO, XT_INT, XT_BULK } xfer_e;
class sched_entry extends uvm_sequence_item;
`uvm_object_utils(sched_entry)
rand bit [2:0] slot;
rand xfer_e xtype;
rand bit [15:0] bytes;
rand bit frame_end;
function new(string name = "sched_entry"); super.new(name); endfunction
endclass
class frame_monitor extends uvm_subscriber #(sched_entry);
`uvm_component_utils(frame_monitor)
localparam int N_SLOT = 8;
localparam int FRAME_BYTES = 1500;
// ---- the frame being collected ----
xfer_e seq_type [$];
bit [2:0] seq_slot [$];
int frame_bytes;
// ---- run-wide ----
int unsigned n_frames, n_entries, n_order_violations;
int unsigned n_overrun;
// ---- fairness history, which one frame cannot supply ----
int bulk_wait [N_SLOT];
bit is_bulk [N_SLOT];
int unsigned worst_wait;
int unsigned n_bulk_passed_over;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// Priority is NOT the numeric type code. The encoding is arbitrary; the
// placement order is the design's thesis. Mapping one to the other
// explicitly is how this avoids testing the encoding by accident.
function int prio(xfer_e t);
case (t)
XT_ISO: return 0;
XT_INT: return 1;
XT_CTRL: return 2;
XT_BULK: return 3;
endcase
endfunction
function void write(sched_entry t);
if (!t.frame_end) begin
seq_type.push_back(t.xtype);
seq_slot.push_back(t.slot);
frame_bytes += int'(t.bytes);
n_entries++;
return;
end
// ---- the frame has closed: check the SEQUENCE ----
for (int i = 1; i < seq_type.size(); i++)
if (prio(seq_type[i]) < prio(seq_type[i-1])) begin
n_order_violations++;
`uvm_error("SCHED/ORDER",
$sformatf("frame %0d: %s placed after %s -- reserved traffic must be scheduled before best-effort",
n_frames, seq_type[i].name(), seq_type[i-1].name()))
end
if (frame_bytes > FRAME_BYTES) begin
n_overrun++;
`uvm_error("SCHED/OVERRUN",
$sformatf("frame %0d carried %0d bytes into a %0d-byte frame",
n_frames, frame_bytes, FRAME_BYTES))
end
// ---- fairness: updated per frame, checked per frame ----
//
// Checked HERE rather than summarised and checked once at the end. A
// single end-of-run assertion kills a non-rotating pointer exactly once,
// and one kill is indistinguishable from luck.
for (int s = 0; s < N_SLOT; s++) begin
bit served = 0;
foreach (seq_slot[i]) if (seq_slot[i] == s[2:0]) served = 1;
if (!is_bulk[s]) begin
bulk_wait[s] = 0;
end else if (served) begin
bulk_wait[s] = 0;
end else begin
bulk_wait[s]++;
n_bulk_passed_over++;
if (bulk_wait[s] > worst_wait) worst_wait = bulk_wait[s];
if (bulk_wait[s] > N_SLOT)
`uvm_error("SCHED/FAIR",
$sformatf("bulk slot %0d has gone %0d frames unserved: the round-robin pointer is not rotating",
s, bulk_wait[s]))
end
end
n_frames++;
seq_type.delete();
seq_slot.delete();
frame_bytes = 0;
endfunction
function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("SCHED",
$sformatf("%0d frames | %0d entries | worst bulk wait %0d | %0d order violations",
n_frames, n_entries, worst_wait, n_order_violations), UVM_LOW)
// A run in which bulk was never passed over says nothing about
// fairness: correct rotation and total starvation produce identical
// frames when there is room for everybody.
if (n_bulk_passed_over == 0)
`uvm_error("SCHED/COV",
"no bulk endpoint was ever passed over: fairness was never at risk, so zero violations means nothing")
if (worst_wait == 0)
`uvm_error("SCHED/COV",
"no bulk endpoint ever waited a frame: the rotation was never exercised")
endfunction
endclass16. Common Misconceptions
"The host schedules whatever is ready." It places reserved traffic first, by type, in a fixed order. Readiness decides nothing about position.
"Bulk and interrupt compete." Interrupt has a reservation and is placed first. Bulk gets the remainder.
"Round-robin is a fairness nicety." It is the only fairness mechanism bulk has. Without it one endpoint takes every frame and no error is reported.
"A broken rotation would be obvious." Every individual frame is perfect. Only the sequence of frames shows it, and F2 scored 1 until the property was checked per frame.
"Bulk starvation is bounded by the protocol." Nothing in the protocol bounds it. The bound comes from the host's implementation, and only while at least one bulk endpoint fits per frame.
"Serving a periodic endpoint early is harmless." It spends bandwidth reserved for somebody else, and for isochronous it delivers data the application has not produced.
"The schedule can be updated any time." Between frames. A mid-frame write changes a schedule already executing.
"A missed deadline means the scheduler is wrong." Not if the schedule was overcommitted. Admission control is a precondition, and violating it produced 386 "missed deadline" errors that were entirely the stimulus's fault.
"An entry's cost is its packet size." Plus the per-transaction overhead, every time.
17. Exercises
1. Derive the four-phase order from the properties in chapter 27.5 alone. Each position should be forced, not chosen.
2. F2 makes the pointer advance once per examined slot. Show that with N_SLOT slots it returns to its starting value every frame, and give the one change to N_SLOT that would accidentally hide the bug.
3. The fairness phase was changed from four 400-byte endpoints to eight 800-byte ones. Compute the worst wait in each case and explain why only the second discriminates.
4. F4 scored zero because the check it mutated cannot fail. Distinguish a dead mutation from a surviving one, and say what each tells you to do.
5. F5 scored zero against a suite that checked "every due endpoint is served". Write the missing property and explain why serving early is a real defect rather than a harmless one.
6. The bulk wait bound is N_SLOT only while one bulk endpoint fits per frame. Derive the general bound, and design a scheduler change that restores a fixed one.
7. Add high-speed split transactions: a full-speed transaction held by a hub across several microframes. Which of the seven properties change, and what new one is needed?
18. Summary
| Idea | Why it matters |
|---|---|
| Periodic traffic is placed first | it is how admission control's promise is kept |
| ISO before INT because it cannot retry | a missed isochronous frame is gone |
| CTRL before BULK | so the bus stays reconfigurable |
| Bulk is round-robin | the only fairness mechanism in the schedule |
| A broken rotation is invisible per frame | every frame is perfect; the sequence is not |
| The rotation needs two pointers | one advanced per slot never rotates at all |
| "Due" is a mask test on the frame number | which is why intervals are powers of two |
| Served when due, and only when due | liveness and safety are two properties |
| Serving early spends someone else's bandwidth | and delivers data that does not exist yet |
| No configuration writes mid-frame | it would change a schedule already running |
| Admission control is a precondition | a scheduler cannot honour an unaffordable promise |
| Map type to priority explicitly | or you test the encoding by accident |
| A summarised property is checked once | F2 scored 1 until it was checked per frame |
| A dead mutation ≠ a surviving one | one means unreachable, the other means unchecked |
| Make the resource scarce to test fairness | 3-of-4 fitting gives a worst wait of 1 |
| 128 states, 24 orderings, 7 mutations | 0 out-of-order entries in 260,898 checks |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o fs_v.out fs_v.v fs_v_tb.v && ./fs_v.out |
| SystemVerilog | iverilog -g2012 -o fs_sv.out fs_sv.sv fs_sv_tb.sv && ./fs_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a fs_vhdl.vhd fs_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_fs_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_fs_vhdl |
| One mutation | iverilog -g2005 -DMUT_F1 -o mm fs_v_mut.v fs_v_tb.v && ./mm |
| Directed only (Verilog) | iverilog -g2005 -DDIRECTED_ONLY -o mm fs_v_mut.v fs_v_tb.v && ./mm |
| Directed only (VHDL) | nvc --std=2008 -e -gDIRECTED_ONLY=true tb_fs_vhdl |
All three implementations pass with 0 errors: 3312 frames across all 128 combinations of type, interval and frame phase; 24 assignments of the four types to slots, so the table order is almost never the emission order; a fairness phase sized so that exactly one bulk endpoint fits per frame, giving a worst wait of exactly 7 against a bound of 8; zero out-of-order entries and zero frame overruns in 260,898 checks; and every one of the seven mutations killed by directed stimulus alone, with all seven directed scores identical across languages.
Chapter 27.7 — Senior Host-Controller Architecture is the first of the four senior questions, and it is a whiteboard exercise: draw an xHCI host controller. Its central mechanism is the one that makes the whole thing work without a lock — a cycle bit that lets producer and consumer share a ring buffer and always agree on who owns each entry.
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
Host Scheduling Algorithm
Pending, eligible and granted are three different things. An arbiter verified exhaustively over 262144 points — and the reference-model coupling that made four invariant mutations die by a single check each.
- Related topic
Endpoint RTL
Fixed priority starves the interrupt endpoint exactly under the load its deadline was specified for — round robin replaces fairness-as-a-feeling with bounded waiting, a number you can put in a latency budget.
- 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.
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.
