USB · Module 26
USB Controllers on SoC
DWC2, MUSB and xHCI differ in a hundred mechanical ways that do not matter and one architectural way that does — whether endpoints own their packet buffers or share a pool.
Module 25 was about debugging a USB link. This module is about the other side of the controller: the SoC it is bolted into, the DMA that feeds it, the fabric it masters, the interrupt it raises and the registers a driver drives it through.
1. The Register Maps Are All Different and It Does Not Matter
The three controllers you will actually meet — Synopsys DWC2 (and DWC3), Mentor/Analog Devices MUSB, and the xHCI host controllers — have completely different register layouts. Different names, different bit positions, different offsets, different reset values.
Porting a driver between them is a week of tedious, mechanical work. It is also work a careful engineer gets right, because every discrepancy produces an immediate and obvious failure.
2. A Shared Pool Fails by Latency, Not by Error
Nothing is lost. No packet is corrupted, no transfer fails, no error bit is set anywhere in the device. Some packets are simply late — and the late ones are specifically the ones queued behind an endpoint that is holding a buffer and not releasing it.
That is why it is diagnosed so slowly. Every error counter in the chip reads zero.
The same four endpoints, two architectures
3. And the Failure Is Not Proportional to Load
Below the point where the pool is exhausted, a shared pool behaves exactly like a dedicated one. Same latency, same throughput, bit for bit. Above it, blocking appears suddenly and the tail grows fast.
The testbench measures precisely this. Identical stimulus, 20,000 operations, one architecture:
PHASE7 SHARED : grants=7976 blocks=6589 worst_wait=29
PHASE7 DEDICATED: grants=8348 blocks=0 worst_wait=0and below the knee — three endpoints, three buffers — the shared pool blocks zero times across 40 rounds.
4. The Blocked Endpoint Is Not the Endpoint at Fault
This is the part that sends people to the wrong place, and it is worth stating on its own.
When endpoint 3 cannot get a buffer, the controller reports a problem on endpoint 3. Endpoint 3 is behaving perfectly. The bug is in whatever is holding a buffer — usually an endpoint whose firmware forgot to hand one back, or one whose host has stopped polling it.
block_ep who asked and could not have one
holder_ep who has been sitting on one LONGEST
Report the second. The first is a victim.5. What We Are Building
usb_fifo_pool implements both architectures behind one mode input, so they can be driven with identical stimulus and compared:
| Output | What it is for |
|---|---|
grant / grant_ep | a buffer was handed out |
block_pulse / block_ep | who could not have one |
holder_ep | who has held one longest — the culprit |
pending | who is waiting, and will be served automatically |
ram_bufs | the architectural cost, in buffers |
worst_wait | the tail, in cycles |
Plus two reports that are firmware bugs rather than architecture: a request from an endpoint that already holds a buffer, and a release from one that does not.
6. Verilog-2005 Implementation
// usb_fifo_pool -- the one architectural difference between USB device
// controllers that actually changes the software you write.
//
// THE REGISTER MAPS ARE DIFFERENT AND IT DOES NOT MATTER
//
// DWC2, MUSB and xHCI have completely different register layouts, different
// naming, different bit positions. Porting between them is a week of
// tedious, mechanical work that a careful engineer gets right.
//
// The thing that is NOT mechanical, and that people get wrong for months,
// is how the controller allocates PACKET BUFFERS to endpoints:
//
// DEDICATED every endpoint owns a fixed slice of RAM.
// MUSB-style. Deterministic. Wastes memory.
//
// SHARED endpoints allocate from a common pool.
// DWC2-style. Better utilisation. Introduces
// HEAD-OF-LINE BLOCKING.
//
// A SHARED POOL FAILS BY LATENCY, NOT BY ERROR
//
// Nothing is lost. No packet is corrupted, no transfer fails, no error bit
// is set anywhere. Some packets are simply LATE -- and the late ones are
// specifically the ones queued behind an endpoint that is holding a buffer
// and not releasing it.
//
// That is why it is diagnosed so slowly. Every error counter in the device
// reads zero, and the symptom is a latency distribution with a tail.
//
// AND THE FAILURE IS NOT PROPORTIONAL TO LOAD
//
// Below the point where the pool is exhausted, a shared pool behaves
// EXACTLY like a dedicated one -- same latency, same throughput, bit for
// bit. Above it, blocking appears suddenly and the tail grows fast.
//
// There is no gradual degradation to notice in testing, which is the same
// shape as chapter 25.4's retry cliff and has the same consequence: every
// measurement taken below the knee says the design is fine.
//
// THE BLOCKED ENDPOINT IS NOT THE ENDPOINT AT FAULT
//
// This is the part that sends people to the wrong place. When endpoint 2
// cannot get a buffer, the controller reports a problem on endpoint 2. The
// bug is in whatever is holding the buffer -- usually an endpoint whose
// firmware forgot to release one, or whose host stopped polling it.
//
// Report the HOLDER, not the requester.
//
// So this block names the endpoint that has held a buffer longest, which is
// the one to go and look at.
module usb_fifo_pool #(
parameter integer N_EP = 4, // hardware endpoints
parameter integer N_BUF = 2 // packet buffers in the SHARED pool
) (
input wire clk,
input wire rst_n,
input wire mode, // 0 = DEDICATED, 1 = SHARED
input wire req_valid, // this endpoint wants a buffer
input wire [1:0] req_ep,
input wire rel_valid, // this endpoint is done with its buffer
input wire [1:0] rel_ep,
input wire eot,
output wire grant, // a buffer was handed out this cycle
output wire [1:0] grant_ep,
output wire block_pulse,
output wire [1:0] block_ep, // who could not get one
output wire [1:0] holder_ep, // who has held one LONGEST -- the culprit
output wire [N_EP-1:0] held,
output wire [N_EP-1:0] pending,
output wire [3:0] free_bufs,
output wire [3:0] ram_bufs, // the architectural cost, in buffers
output wire [15:0] worst_wait,
output wire err_pulse,
output wire [1:0] err_code,
output reg [31:0] n_req,
output reg [31:0] n_grant,
output reg [31:0] n_block,
output reg [31:0] n_release,
output reg [31:0] n_double, // requested while already holding
output reg [31:0] n_unheld // released without holding
);
localparam [1:0] E_NONE = 2'd0,
E_DOUBLE = 2'd1, // a request from an endpoint that holds
E_UNHELD = 2'd2; // a release from one that does not
// Widths derived from the parameters' own type, never from the width the
// DEFAULT value happens to need -- chapter 25.4's retry budget is what
// that rule came from.
localparam [3:0] NBUF4 = N_BUF;
localparam [3:0] NEP4 = N_EP;
reg [N_EP-1:0] held_r, pend_r;
reg [15:0] age_r [0:N_EP-1]; // cycles this endpoint has HELD
reg [15:0] wait_r [0:N_EP-1]; // cycles this endpoint has WAITED
reg [15:0] worst_r;
reg gr_r, bl_r, er_r;
reg [1:0] gep_r, bep_r, hep_r;
reg [1:0] ec_r;
assign grant = gr_r;
assign grant_ep = gep_r;
assign block_pulse = bl_r;
assign block_ep = bep_r;
assign holder_ep = hep_r;
assign held = held_r;
assign pending = pend_r;
assign worst_wait = worst_r;
assign err_pulse = er_r;
assign err_code = ec_r;
// ---- How many buffers exist, and how many are left. ----
//
// In DEDICATED mode each endpoint has its own slice, so the capacity is
// N_EP and an endpoint can never be short of a buffer it already owns.
// In SHARED mode the capacity is N_BUF, which is the whole point: fewer
// buffers than endpoints is the saving, and it is also the blocking.
function [3:0] popc;
input [N_EP-1:0] v;
integer i;
begin
popc = 4'd0;
for (i = 0; i < N_EP; i = i + 1)
if (v[i]) popc = popc + 4'd1;
end
endfunction
// The capacity is computed in exactly ONE place and used everywhere. It
// appears in four expressions; writing the conditional out four times is
// four chances for the architecture to be half-changed, and a pool that is
// dedicated for its capacity check and shared for its report is a design
// nobody would write on purpose and everybody writes by editing.
wire [3:0] cap;
assign cap = mode ? NBUF4 : NEP4;
assign ram_bufs = cap;
assign free_bufs = cap - popc(held_r);
// ---- The oldest holder. ----
//
// Not the largest endpoint number, not the most recent: the one that has
// been sitting on a buffer longest. On a real device that is almost
// always an endpoint whose firmware forgot to hand one back, and it is
// the only endpoint in the system worth looking at when another one
// blocks.
function [1:0] oldest_holder;
input dummy;
integer i;
reg [15:0] best;
begin
oldest_holder = 2'd0;
best = 16'd0;
for (i = 0; i < N_EP; i = i + 1)
if (held_r[i] && (age_r[i] >= best)) begin
best = age_r[i];
oldest_holder = i[1:0];
end
end
endfunction
// ---- Among waiting endpoints, the one that has waited longest. ----
//
// A fairness policy, and a deliberate one: granting to the lowest
// endpoint number instead is one line shorter and starves the high ones
// for as long as the low ones keep asking.
function [1:0] longest_waiter;
input dummy;
integer i;
reg [15:0] best;
reg any;
begin
longest_waiter = 2'd0;
best = 16'd0;
any = 1'b0;
for (i = 0; i < N_EP; i = i + 1)
if (pend_r[i] && (!any || (wait_r[i] > best))) begin
best = wait_r[i];
longest_waiter = i[1:0];
any = 1'b1;
end
end
endfunction
integer j;
reg [N_EP-1:0] held_n, pend_n;
reg [15:0] age_n [0:N_EP-1];
reg [15:0] wait_n [0:N_EP-1];
reg [15:0] worst_n;
reg gr_n, bl_n, er_n;
reg [1:0] gep_n, bep_n, hep_n, ec_n;
reg req_n, rls_n, dbl_n, unh_n;
reg [3:0] free_n;
reg [1:0] wnr;
always @* begin
held_n = held_r;
pend_n = pend_r;
worst_n = worst_r;
gr_n = 1'b0; bl_n = 1'b0; er_n = 1'b0;
gep_n = 2'd0; bep_n = 2'd0; hep_n = 2'd0; ec_n = E_NONE;
req_n = 1'b0; rls_n = 1'b0; dbl_n = 1'b0; unh_n = 1'b0;
for (j = 0; j < N_EP; j = j + 1) begin
age_n[j] = age_r[j];
wait_n[j] = wait_r[j];
end
if (eot) begin
// Nothing: the counters are the report.
end else begin
// ---- A release happens FIRST. ----
//
// Ordering, not style. A release and a request in the same cycle is
// the common case on a busy controller, and doing the request first
// blocks an endpoint against a buffer that is being handed back on
// the very same cycle. It costs one cycle of latency per collision
// and it is invisible unless somebody counts blocks.
if (rel_valid) begin
if (held_r[rel_ep]) begin
held_n[rel_ep] = 1'b0;
age_n[rel_ep] = 16'd0;
rls_n = 1'b1;
end else begin
// Firmware handed back a buffer it does not have. Harmless to the
// hardware and a real bug in the driver, so it is named rather
// than ignored.
er_n = 1'b1; ec_n = E_UNHELD;
unh_n = 1'b1;
end
end
free_n = cap - popc(held_n);
if (req_valid) begin
req_n = 1'b1;
if (held_r[req_ep] && !(rel_valid && (rel_ep == req_ep))) begin
// Asking for a second buffer while still holding the first.
er_n = 1'b1; ec_n = E_DOUBLE;
dbl_n = 1'b1;
end else if (free_n != 4'd0) begin
held_n[req_ep] = 1'b1;
age_n[req_ep] = 16'd0;
pend_n[req_ep] = 1'b0;
wait_n[req_ep] = 16'd0;
gr_n = 1'b1;
gep_n = req_ep;
end else begin
// ---- BLOCKED. And the report names the HOLDER. ----
//
// `block_ep` is the endpoint that asked; `holder_ep` is the one
// that has been sitting on a buffer longest. Reporting only the
// first sends an engineer to read the firmware of an endpoint
// that is behaving perfectly.
pend_n[req_ep] = 1'b1;
bl_n = 1'b1;
bep_n = req_ep;
hep_n = oldest_holder(1'b0);
end
end else if (|pend_r) begin
// ---- A waiting endpoint retries automatically. ----
//
// A real endpoint with data does not stop wanting a buffer because
// it was refused once. Modelling the request as a one-shot makes
// the pool look far better than it is, because every refusal then
// costs the testbench a retry it has to remember to issue.
free_n = cap - popc(held_n);
if (free_n != 4'd0) begin
wnr = longest_waiter(1'b0);
held_n[wnr] = 1'b1;
age_n[wnr] = 16'd0;
pend_n[wnr] = 1'b0;
if (wait_r[wnr] > worst_r) worst_n = wait_r[wnr];
wait_n[wnr] = 16'd0;
gr_n = 1'b1;
gep_n = wnr;
end
end
// ---- Ages and waits advance for everyone still in that state. ----
for (j = 0; j < N_EP; j = j + 1) begin
if (held_n[j] && held_r[j] && (age_r[j] != 16'hFFFF))
age_n[j] = age_r[j] + 16'd1;
if (pend_n[j] && (wait_r[j] != 16'hFFFF))
wait_n[j] = wait_r[j] + 16'd1;
if (pend_n[j] && (wait_n[j] > worst_n)) worst_n = wait_n[j];
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
held_r <= {N_EP{1'b0}};
pend_r <= {N_EP{1'b0}};
worst_r <= 16'd0;
gr_r <= 1'b0; bl_r <= 1'b0; er_r <= 1'b0;
gep_r <= 2'd0; bep_r <= 2'd0; hep_r <= 2'd0; ec_r <= E_NONE;
for (j = 0; j < N_EP; j = j + 1) begin
age_r[j] <= 16'd0;
wait_r[j] <= 16'd0;
end
n_req <= 32'd0;
n_grant <= 32'd0;
n_block <= 32'd0;
n_release <= 32'd0;
n_double <= 32'd0;
n_unheld <= 32'd0;
end else begin
held_r <= held_n;
pend_r <= pend_n;
worst_r <= worst_n;
gr_r <= gr_n; bl_r <= bl_n; er_r <= er_n;
gep_r <= gep_n; bep_r <= bep_n; hep_r <= hep_n; ec_r <= ec_n;
for (j = 0; j < N_EP; j = j + 1) begin
age_r[j] <= age_n[j];
wait_r[j] <= wait_n[j];
end
if (req_n) n_req <= n_req + 32'd1;
if (gr_n) n_grant <= n_grant + 32'd1;
if (bl_n) n_block <= n_block + 32'd1;
if (rls_n) n_release <= n_release + 32'd1;
if (dbl_n) n_double <= n_double + 32'd1;
if (unh_n) n_unheld <= n_unheld + 32'd1;
end
end
endmodule7. SystemVerilog Implementation
// usb_fifo_pool -- the one architectural difference between USB device
// controllers that actually changes the software you write.
//
// THE REGISTER MAPS ARE DIFFERENT AND IT DOES NOT MATTER
//
// DWC2, MUSB and xHCI have completely different register layouts, different
// naming, different bit positions. Porting between them is a week of
// tedious, mechanical work that a careful engineer gets right.
//
// The thing that is NOT mechanical, and that people get wrong for months,
// is how the controller allocates PACKET BUFFERS to endpoints:
//
// DEDICATED every endpoint owns a fixed slice of RAM.
// MUSB-style. Deterministic. Wastes memory.
//
// SHARED endpoints allocate from a common pool.
// DWC2-style. Better utilisation. Introduces
// HEAD-OF-LINE BLOCKING.
//
// A SHARED POOL FAILS BY LATENCY, NOT BY ERROR
//
// Nothing is lost. No packet is corrupted, no transfer fails, no error bit
// is set anywhere. Some packets are simply LATE -- and the late ones are
// specifically the ones queued behind an endpoint that is holding a buffer
// and not releasing it.
//
// That is why it is diagnosed so slowly. Every error counter in the device
// reads zero, and the symptom is a latency distribution with a tail.
//
// AND THE FAILURE IS NOT PROPORTIONAL TO LOAD
//
// Below the point where the pool is exhausted, a shared pool behaves
// EXACTLY like a dedicated one -- same latency, same throughput, bit for
// bit. Above it, blocking appears suddenly and the tail grows fast.
//
// There is no gradual degradation to notice in testing, which is the same
// shape as chapter 25.4's retry cliff and has the same consequence: every
// measurement taken below the knee says the design is fine.
//
// THE BLOCKED ENDPOINT IS NOT THE ENDPOINT AT FAULT
//
// This is the part that sends people to the wrong place. When endpoint 2
// cannot get a buffer, the controller reports a problem on endpoint 2. The
// bug is in whatever is holding the buffer -- usually an endpoint whose
// firmware forgot to release one, or whose host stopped polling it.
//
// Report the HOLDER, not the requester.
//
// So this block names the endpoint that has held a buffer longest, which is
// the one to go and look at.
package usb_pool_pkg;
// The architecture is a TYPE, not a bit. A one-bit port called `mode` is
// a number somebody has to look up in a comment; this prints DEDICATED
// and SHARED in the waveform viewer and in every log line.
typedef enum logic {
M_DEDICATED = 1'b0, // every endpoint owns a fixed slice of RAM
M_SHARED = 1'b1 // endpoints allocate from a common pool
} pool_mode_e;
typedef enum logic [1:0] {
E_NONE = 2'd0,
E_DOUBLE = 2'd1, // a request from an endpoint that already holds
E_UNHELD = 2'd2 // a release from one that does not
} pool_err_e;
endpackage
module usb_fifo_pool
import usb_pool_pkg::*;
#(
parameter int N_EP = 4, // hardware endpoints
parameter int N_BUF = 2 // packet buffers in the SHARED pool
) (
input logic clk,
input logic rst_n,
input pool_mode_e mode,
input logic req_valid, // this endpoint wants a buffer
input logic [1:0] req_ep,
input logic rel_valid, // this endpoint is done with its buffer
input logic [1:0] rel_ep,
input logic eot,
output logic grant, // a buffer was handed out this cycle
output logic [1:0] grant_ep,
output logic block_pulse,
output logic [1:0] block_ep, // who could not get one
output logic [1:0] holder_ep, // who has held one LONGEST -- the culprit
output logic [N_EP-1:0] held,
output logic [N_EP-1:0] pending,
output logic [3:0] free_bufs,
output logic [3:0] ram_bufs, // the architectural cost, in buffers
output logic [15:0] worst_wait,
output logic err_pulse,
output pool_err_e err_code,
output logic [31:0] n_req,
output logic [31:0] n_grant,
output logic [31:0] n_block,
output logic [31:0] n_release,
output logic [31:0] n_double, // requested while already holding
output logic [31:0] n_unheld // released without holding
);
// Widths derived from the parameters' own type, never from the width the
// DEFAULT value happens to need -- chapter 25.4's retry budget is what
// that rule came from.
localparam logic [3:0] NBUF4 = 4'(N_BUF);
localparam logic [3:0] NEP4 = 4'(N_EP);
logic [N_EP-1:0] held_r, pend_r;
logic [15:0] age_r [N_EP-1:0]; // cycles this endpoint has HELD
logic [15:0] wait_r [N_EP-1:0]; // cycles this endpoint has WAITED
logic [15:0] worst_r;
logic gr_r, bl_r, er_r;
logic [1:0] gep_r, bep_r, hep_r;
pool_err_e ec_r;
assign grant = gr_r;
assign grant_ep = gep_r;
assign block_pulse = bl_r;
assign block_ep = bep_r;
assign holder_ep = hep_r;
assign held = held_r;
assign pending = pend_r;
assign worst_wait = worst_r;
assign err_pulse = er_r;
assign err_code = ec_r;
// ---- How many buffers exist, and how many are left. ----
//
// In DEDICATED mode each endpoint has its own slice, so the capacity is
// N_EP and an endpoint can never be short of a buffer it already owns.
// In SHARED mode the capacity is N_BUF, which is the whole point: fewer
// buffers than endpoints is the saving, and it is also the blocking.
function automatic logic [3:0] popc(input logic [N_EP-1:0] v);
int i;
begin
popc = 4'd0;
for (i = 0; i < N_EP; i = i + 1)
if (v[i]) popc = popc + 4'd1;
end
endfunction
// The capacity is computed in exactly ONE place and used everywhere. It
// appears in four expressions; writing the conditional out four times is
// four chances for the architecture to be half-changed, and a pool that is
// dedicated for its capacity check and shared for its report is a design
// nobody would write on purpose and everybody writes by editing.
logic [3:0] cap;
assign cap = (mode == M_SHARED) ? NBUF4 : NEP4;
assign ram_bufs = cap;
assign free_bufs = cap - popc(held_r);
// ---- The oldest holder. ----
//
// Not the largest endpoint number, not the most recent: the one that has
// been sitting on a buffer longest. On a real device that is almost
// always an endpoint whose firmware forgot to hand one back, and it is
// the only endpoint in the system worth looking at when another one
// blocks.
function automatic logic [1:0] oldest_holder();
int i;
logic [15:0] best;
begin
oldest_holder = 2'd0;
best = 16'd0;
for (i = 0; i < N_EP; i = i + 1)
if (held_r[i] && (age_r[i] >= best)) begin
best = age_r[i];
oldest_holder = i[1:0];
end
end
endfunction
// ---- Among waiting endpoints, the one that has waited longest. ----
//
// A fairness policy, and a deliberate one: granting to the lowest
// endpoint number instead is one line shorter and starves the high ones
// for as long as the low ones keep asking.
function automatic logic [1:0] longest_waiter();
int i;
logic [15:0] best;
logic any;
begin
longest_waiter = 2'd0;
best = 16'd0;
any = 1'b0;
for (i = 0; i < N_EP; i = i + 1)
if (pend_r[i] && (!any || (wait_r[i] > best))) begin
best = wait_r[i];
longest_waiter = i[1:0];
any = 1'b1;
end
end
endfunction
int j;
logic [N_EP-1:0] held_n, pend_n;
logic [15:0] age_n [N_EP-1:0];
logic [15:0] wait_n [N_EP-1:0];
logic [15:0] worst_n;
logic gr_n, bl_n, er_n;
logic [1:0] gep_n, bep_n, hep_n;
pool_err_e ec_n;
logic req_n, rls_n, dbl_n, unh_n;
logic [3:0] free_n;
logic [1:0] wnr;
always_comb begin
held_n = held_r;
pend_n = pend_r;
worst_n = worst_r;
gr_n = 1'b0; bl_n = 1'b0; er_n = 1'b0;
gep_n = 2'd0; bep_n = 2'd0; hep_n = 2'd0; ec_n = E_NONE;
req_n = 1'b0; rls_n = 1'b0; dbl_n = 1'b0; unh_n = 1'b0;
for (j = 0; j < N_EP; j = j + 1) begin
age_n[j] = age_r[j];
wait_n[j] = wait_r[j];
end
if (eot) begin
// Nothing: the counters are the report.
end else begin
// ---- A release happens FIRST. ----
//
// Ordering, not style. A release and a request in the same cycle is
// the common case on a busy controller, and doing the request first
// blocks an endpoint against a buffer that is being handed back on
// the very same cycle. It costs one cycle of latency per collision
// and it is invisible unless somebody counts blocks.
if (rel_valid) begin
if (held_r[rel_ep]) begin
held_n[rel_ep] = 1'b0;
age_n[rel_ep] = 16'd0;
rls_n = 1'b1;
end else begin
// Firmware handed back a buffer it does not have. Harmless to the
// hardware and a real bug in the driver, so it is named rather
// than ignored.
er_n = 1'b1; ec_n = E_UNHELD;
unh_n = 1'b1;
end
end
free_n = cap - popc(held_n);
if (req_valid) begin
req_n = 1'b1;
if (held_r[req_ep] && !(rel_valid && (rel_ep == req_ep))) begin
// Asking for a second buffer while still holding the first.
er_n = 1'b1; ec_n = E_DOUBLE;
dbl_n = 1'b1;
end else if (free_n != 4'd0) begin
held_n[req_ep] = 1'b1;
age_n[req_ep] = 16'd0;
pend_n[req_ep] = 1'b0;
wait_n[req_ep] = 16'd0;
gr_n = 1'b1;
gep_n = req_ep;
end else begin
// ---- BLOCKED. And the report names the HOLDER. ----
//
// `block_ep` is the endpoint that asked; `holder_ep` is the one
// that has been sitting on a buffer longest. Reporting only the
// first sends an engineer to read the firmware of an endpoint
// that is behaving perfectly.
pend_n[req_ep] = 1'b1;
bl_n = 1'b1;
bep_n = req_ep;
hep_n = oldest_holder();
end
end else if (|pend_r) begin
// ---- A waiting endpoint retries automatically. ----
//
// A real endpoint with data does not stop wanting a buffer because
// it was refused once. Modelling the request as a one-shot makes
// the pool look far better than it is, because every refusal then
// costs the testbench a retry it has to remember to issue.
free_n = cap - popc(held_n);
if (free_n != 4'd0) begin
wnr = longest_waiter();
held_n[wnr] = 1'b1;
age_n[wnr] = 16'd0;
pend_n[wnr] = 1'b0;
if (wait_r[wnr] > worst_r) worst_n = wait_r[wnr];
wait_n[wnr] = 16'd0;
gr_n = 1'b1;
gep_n = wnr;
end
end
// ---- Ages and waits advance for everyone still in that state. ----
for (j = 0; j < N_EP; j = j + 1) begin
if (held_n[j] && held_r[j] && (age_r[j] != 16'hFFFF))
age_n[j] = age_r[j] + 16'd1;
if (pend_n[j] && (wait_r[j] != 16'hFFFF))
wait_n[j] = wait_r[j] + 16'd1;
if (pend_n[j] && (wait_n[j] > worst_n)) worst_n = wait_n[j];
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
held_r <= '0;
pend_r <= '0;
worst_r <= 16'd0;
gr_r <= 1'b0; bl_r <= 1'b0; er_r <= 1'b0;
gep_r <= 2'd0; bep_r <= 2'd0; hep_r <= 2'd0; ec_r <= E_NONE;
for (j = 0; j < N_EP; j = j + 1) begin
age_r[j] <= 16'd0;
wait_r[j] <= 16'd0;
end
n_req <= 32'd0;
n_grant <= 32'd0;
n_block <= 32'd0;
n_release <= 32'd0;
n_double <= 32'd0;
n_unheld <= 32'd0;
end else begin
held_r <= held_n;
pend_r <= pend_n;
worst_r <= worst_n;
gr_r <= gr_n; bl_r <= bl_n; er_r <= er_n;
gep_r <= gep_n; bep_r <= bep_n; hep_r <= hep_n; ec_r <= ec_n;
for (j = 0; j < N_EP; j = j + 1) begin
age_r[j] <= age_n[j];
wait_r[j] <= wait_n[j];
end
if (req_n) n_req <= n_req + 32'd1;
if (gr_n) n_grant <= n_grant + 32'd1;
if (bl_n) n_block <= n_block + 32'd1;
if (rls_n) n_release <= n_release + 32'd1;
if (dbl_n) n_double <= n_double + 32'd1;
if (unh_n) n_unheld <= n_unheld + 32'd1;
end
end
endmodule8. VHDL-2008 Implementation
-- usb_fifo_pool -- the one architectural difference between USB device
-- controllers that actually changes the software you write.
--
-- THE REGISTER MAPS ARE DIFFERENT AND IT DOES NOT MATTER
--
-- DWC2, MUSB and xHCI have completely different register layouts, different
-- naming, different bit positions. Porting between them is a week of
-- tedious, mechanical work that a careful engineer gets right.
--
-- The thing that is NOT mechanical, and that people get wrong for months, is
-- how the controller allocates PACKET BUFFERS to endpoints:
--
-- DEDICATED every endpoint owns a fixed slice of RAM.
-- MUSB-style. Deterministic. Wastes memory.
--
-- SHARED endpoints allocate from a common pool.
-- DWC2-style. Better utilisation. Introduces
-- HEAD-OF-LINE BLOCKING.
--
-- A SHARED POOL FAILS BY LATENCY, NOT BY ERROR
--
-- Nothing is lost. No packet is corrupted, no transfer fails, no error bit
-- is set anywhere. Some packets are simply LATE -- and the late ones are
-- specifically the ones queued behind an endpoint that is holding a buffer
-- and not releasing it.
--
-- That is why it is diagnosed so slowly. Every error counter in the device
-- reads zero, and the symptom is a latency distribution with a tail.
--
-- AND THE FAILURE IS NOT PROPORTIONAL TO LOAD
--
-- Below the point where the pool is exhausted, a shared pool behaves EXACTLY
-- like a dedicated one -- same latency, same throughput, bit for bit. Above
-- it, blocking appears suddenly and the tail grows fast.
--
-- There is no gradual degradation to notice in testing, which is the same
-- shape as chapter 25.4's retry cliff and has the same consequence: every
-- measurement taken below the knee says the design is fine.
--
-- THE BLOCKED ENDPOINT IS NOT THE ENDPOINT AT FAULT
--
-- When endpoint 2 cannot get a buffer, the controller reports a problem on
-- endpoint 2. The bug is in whatever is holding the buffer -- usually an
-- endpoint whose firmware forgot to release one, or whose host stopped
-- polling it.
--
-- Report the HOLDER, not the requester.
--
-- So this block names the endpoint that has held a buffer longest, which is
-- the one to go and look at.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_pool_pkg is
constant M_DEDICATED : std_logic := '0';
constant M_SHARED : std_logic := '1';
constant E_NONE : std_logic_vector(1 downto 0) := "00";
constant E_DOUBLE : std_logic_vector(1 downto 0) := "01";
constant E_UNHELD : std_logic_vector(1 downto 0) := "10";
type age_array is array (natural range <>) of unsigned(15 downto 0);
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_pool_pkg.all;
entity usb_fifo_pool is
generic (
N_EP : integer := 4; -- hardware endpoints
N_BUF : integer := 2 -- packet buffers in the SHARED pool
);
port (
clk : in std_logic;
rst_n : in std_logic;
mode : in std_logic; -- '0' DEDICATED, '1' SHARED
req_valid : in std_logic;
req_ep : in std_logic_vector(1 downto 0);
rel_valid : in std_logic;
rel_ep : in std_logic_vector(1 downto 0);
eot : in std_logic;
grant : out std_logic;
grant_ep : out std_logic_vector(1 downto 0);
block_pulse : out std_logic;
block_ep : out std_logic_vector(1 downto 0);
holder_ep : out std_logic_vector(1 downto 0);
held : out std_logic_vector(N_EP-1 downto 0);
pending : out std_logic_vector(N_EP-1 downto 0);
free_bufs : out unsigned(3 downto 0);
ram_bufs : out unsigned(3 downto 0);
worst_wait : out unsigned(15 downto 0);
err_pulse : out std_logic;
err_code : out std_logic_vector(1 downto 0);
n_req : out unsigned(31 downto 0);
n_grant : out unsigned(31 downto 0);
n_block : out unsigned(31 downto 0);
n_release : out unsigned(31 downto 0);
n_double : out unsigned(31 downto 0);
n_unheld : out unsigned(31 downto 0)
);
end entity;
architecture rtl of usb_fifo_pool is
signal held_r, pend_r : std_logic_vector(N_EP-1 downto 0)
:= (others => '0');
signal age_r, wait_r : age_array(0 to N_EP-1)
:= (others => (others => '0'));
signal worst_r : unsigned(15 downto 0) := (others => '0');
signal gr_r, bl_r, er_r : std_logic := '0';
signal gep_r, bep_r, hep_r : std_logic_vector(1 downto 0) := "00";
signal ec_r : std_logic_vector(1 downto 0) := E_NONE;
signal req_c, grant_c, block_c : unsigned(31 downto 0) := (others => '0');
signal rel_c, dbl_c, unh_c : unsigned(31 downto 0) := (others => '0');
function popc (v : std_logic_vector) return unsigned is
variable c : unsigned(3 downto 0) := (others => '0');
begin
for i in v'range loop
if v(i) = '1' then c := c + 1; end if;
end loop;
return c;
end function;
function capacity (m : std_logic) return unsigned is
begin
if m = M_SHARED then
return to_unsigned(N_BUF, 4);
else
return to_unsigned(N_EP, 4);
end if;
end function;
begin
held <= held_r;
pending <= pend_r;
ram_bufs <= capacity(mode);
free_bufs <= capacity(mode) - popc(held_r);
grant <= gr_r;
grant_ep <= gep_r;
block_pulse <= bl_r;
block_ep <= bep_r;
holder_ep <= hep_r;
worst_wait <= worst_r;
err_pulse <= er_r;
err_code <= ec_r;
n_req <= req_c;
n_grant <= grant_c;
n_block <= block_c;
n_release <= rel_c;
n_double <= dbl_c;
n_unheld <= unh_c;
process (clk, rst_n)
variable held_v, pend_v : std_logic_vector(N_EP-1 downto 0);
variable age_v, wait_v : age_array(0 to N_EP-1);
variable held_pre : std_logic_vector(N_EP-1 downto 0);
variable worst_v : unsigned(15 downto 0);
variable gr_v, bl_v, er_v : std_logic;
variable gep_v, bep_v, hep_v, ec_v : std_logic_vector(1 downto 0);
variable free_v : unsigned(3 downto 0);
variable e, r : integer range 0 to N_EP-1;
variable best : unsigned(15 downto 0);
variable any : boolean;
variable wnr : integer range 0 to N_EP-1;
begin
if rst_n = '0' then
held_r <= (others => '0');
pend_r <= (others => '0');
age_r <= (others => (others => '0'));
wait_r <= (others => (others => '0'));
worst_r <= (others => '0');
gr_r <= '0'; bl_r <= '0'; er_r <= '0';
gep_r <= "00"; bep_r <= "00"; hep_r <= "00"; ec_r <= E_NONE;
req_c <= (others => '0');
grant_c <= (others => '0');
block_c <= (others => '0');
rel_c <= (others => '0');
dbl_c <= (others => '0');
unh_c <= (others => '0');
elsif rising_edge(clk) then
held_v := held_r;
pend_v := pend_r;
held_pre := held_r;
age_v := age_r;
wait_v := wait_r;
worst_v := worst_r;
gr_v := '0'; bl_v := '0'; er_v := '0';
gep_v := "00"; bep_v := "00"; hep_v := "00"; ec_v := E_NONE;
e := to_integer(unsigned(req_ep));
r := to_integer(unsigned(rel_ep));
if eot = '1' then
-- Nothing: the counters are the report.
null;
else
-- ---- A release happens FIRST. ----
--
-- Ordering, not style. A release and a request in the same cycle is
-- the common case on a busy controller, and doing the request first
-- blocks an endpoint against a buffer that is being handed back on
-- the very same cycle. It costs one cycle of latency per collision
-- and it is invisible unless somebody counts blocks.
if rel_valid = '1' then
if held_v(r) = '1' then
held_v(r) := '0';
age_v(r) := (others => '0');
rel_c <= rel_c + 1;
else
-- Firmware handed back a buffer it does not have. Harmless to
-- the hardware and a real bug in the driver, so it is named
-- rather than ignored.
er_v := '1';
ec_v := E_UNHELD;
unh_c <= unh_c + 1;
end if;
end if;
free_v := capacity(mode) - popc(held_v);
if req_valid = '1' then
req_c <= req_c + 1;
if (held_r(e) = '1')
and not (rel_valid = '1' and rel_ep = req_ep) then
-- Asking for a second buffer while still holding the first.
er_v := '1';
ec_v := E_DOUBLE;
dbl_c <= dbl_c + 1;
elsif free_v /= x"0" then
held_v(e) := '1';
age_v(e) := (others => '0');
pend_v(e) := '0';
wait_v(e) := (others => '0');
gr_v := '1';
gep_v := req_ep;
grant_c <= grant_c + 1;
else
-- ---- BLOCKED. And the report names the HOLDER. ----
--
-- block_ep is the endpoint that asked; holder_ep is the one
-- that has been sitting on a buffer longest. Reporting only the
-- first sends an engineer to read the firmware of an endpoint
-- that is behaving perfectly.
pend_v(e) := '1';
bl_v := '1';
bep_v := req_ep;
best := (others => '0');
hep_v := "00";
for i in 0 to N_EP-1 loop
if held_r(i) = '1' and age_r(i) >= best then
best := age_r(i);
hep_v := std_logic_vector(to_unsigned(i, 2));
end if;
end loop;
block_c <= block_c + 1;
end if;
elsif pend_v /= (pend_v'range => '0') then
-- ---- A waiting endpoint retries automatically. ----
--
-- A real endpoint with data does not stop wanting a buffer
-- because it was refused once. Modelling the request as a
-- one-shot makes the pool look far better than it is.
free_v := capacity(mode) - popc(held_v);
if free_v /= x"0" then
-- Among waiting endpoints, the one that has waited LONGEST.
-- Granting to the lowest endpoint number instead is one line
-- shorter and starves the high ones for as long as the low ones
-- keep asking.
best := (others => '0');
any := false;
wnr := 0;
for i in 0 to N_EP-1 loop
if pend_v(i) = '1' and ((not any) or wait_v(i) > best) then
best := wait_v(i);
wnr := i;
any := true;
end if;
end loop;
held_v(wnr) := '1';
age_v(wnr) := (others => '0');
pend_v(wnr) := '0';
if wait_v(wnr) > worst_v then worst_v := wait_v(wnr); end if;
wait_v(wnr) := (others => '0');
gr_v := '1';
gep_v := std_logic_vector(to_unsigned(wnr, 2));
grant_c <= grant_c + 1;
end if;
end if;
-- ---- Ages and waits advance for everyone still in that state. ----
for i in 0 to N_EP-1 loop
if held_v(i) = '1' and held_pre(i) = '1' and age_v(i) /= x"FFFF" then
age_v(i) := age_v(i) + 1;
end if;
if pend_v(i) = '1' and wait_v(i) /= x"FFFF" then
wait_v(i) := wait_v(i) + 1;
end if;
if pend_v(i) = '1' and wait_v(i) > worst_v then
worst_v := wait_v(i);
end if;
end loop;
end if;
held_r <= held_v;
pend_r <= pend_v;
age_r <= age_v;
wait_r <= wait_v;
worst_r <= worst_v;
gr_r <= gr_v; bl_r <= bl_v; er_r <= er_v;
gep_r <= gep_v; bep_r <= bep_v; hep_r <= hep_v; ec_r <= ec_v;
end if;
end process;
end architecture;9. Seeing the Architectures Diverge
A shared pool of two, and the fourth endpoint that has to wait
usb_fifo_pool — SHARED: blocked, then served on a release
10 cyclesAnd the same stimulus in the other architecture, where none of it happens:
DEDICATED: the same four requests, no blocking at all
usb_fifo_pool — DEDICATED: every request granted
10 cycles10. The Testbenches
The oracle is a shadow model written from sections 2 to 4 rather than from the RTL, re-derived every cycle and compared against every output — nineteen checks per cycle, including the structural one that the pool can never be oversubscribed.
The exhaustive claim is over three things at once, because all three change the answer:
2 architectures x 16 holding states x 9 operations = 288
Of those, the states with MORE HOLDERS THAN BUFFERS in shared
mode are UNREACHABLE by construction: five states, nine
operations each, 45 combinations.
243 required. 45 declared unreachable and proven never to occur.Each holding state is reached by requesting, never by forcing a register: a state forced into the design is a state the design never proved it can reach.
Verilog-2005 testbench
`timescale 1ns/1ps
// Testbench for usb_fifo_pool.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS OVER (ARCHITECTURE, POOL STATE, OPERATION)
//
// A buffer allocator's answer depends on three things at once: which
// architecture it is configured as, exactly which endpoints are currently
// holding, and what is being asked. Sweeping any one of them alone proves
// nothing about the other two -- and the interesting behaviour, blocking,
// exists only at particular combinations.
//
// 2 modes x 16 holding states x 9 operations = 288
//
// Of those, the nine with all four endpoints holding in SHARED mode are
// UNREACHABLE by construction: the pool has three buffers. That is a claim
// about the design, so the testbench makes it explicitly and requires the
// other 279 -- rather than quietly reporting 279/288 and leaving a reader
// to wonder which nine were missed and why. Chapter 24.4's argument, in the
// place it actually bites.
module tb_fp_v;
localparam integer N_EP = 4;
localparam integer N_BUF = 2;
localparam [1:0] E_NONE = 2'd0, E_DOUBLE = 2'd1, E_UNHELD = 2'd2;
reg clk = 1'b0, rst_n = 1'b0;
reg mode = 1'b0, req_valid = 1'b0, rel_valid = 1'b0, eot = 1'b0;
reg [1:0] req_ep = 2'd0, rel_ep = 2'd0;
wire grant, block_pulse, err_pulse;
wire [1:0] grant_ep, block_ep, holder_ep, err_code;
wire [N_EP-1:0] held, pending;
wire [3:0] free_bufs, ram_bufs;
wire [15:0] worst_wait;
wire [31:0] n_req, n_grant, n_block, n_release, n_double, n_unheld;
usb_fifo_pool #(.N_EP(N_EP), .N_BUF(N_BUF)) dut (
.clk(clk), .rst_n(rst_n),
.mode(mode), .req_valid(req_valid), .req_ep(req_ep),
.rel_valid(rel_valid), .rel_ep(rel_ep), .eot(eot),
.grant(grant), .grant_ep(grant_ep),
.block_pulse(block_pulse), .block_ep(block_ep), .holder_ep(holder_ep),
.held(held), .pending(pending),
.free_bufs(free_bufs), .ram_bufs(ram_bufs), .worst_wait(worst_wait),
.err_pulse(err_pulse), .err_code(err_code),
.n_req(n_req), .n_grant(n_grant), .n_block(n_block),
.n_release(n_release), .n_double(n_double), .n_unheld(n_unheld)
);
always #5 clk = ~clk;
// ---------------- the shadow model ----------------
reg [N_EP-1:0] m_held, m_pend;
reg [15:0] m_age [0:N_EP-1];
reg [15:0] m_wait [0:N_EP-1];
reg [15:0] m_worst;
reg m_gr, m_bl, m_er;
reg [1:0] m_gep, m_bep, m_hep, m_ec;
reg [31:0] c_req, c_grant, c_block, c_rel, c_dbl, c_unh;
integer errors = 0, checks = 0, steps = 0;
integer k;
// reach: mode (2) x holding state (16) x operation (9)
reg [0:0] reach [0:287];
integer n_reach, n_unreach;
task ck;
input [255:0] nm;
input [31:0] got, exp;
begin
checks = checks + 1;
if (got !== exp) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
$time, steps, nm, got, exp);
end
end
endtask
function [3:0] popc;
input [N_EP-1:0] v;
integer i;
begin
popc = 4'd0;
for (i = 0; i < N_EP; i = i + 1) if (v[i]) popc = popc + 4'd1;
end
endfunction
// The oldest holder, from the model's own ages. The tie-break matches the
// design's documented one: on equal age the highest endpoint index wins,
// because the scan uses >= and runs upwards.
function [1:0] m_oldest;
input dummy;
integer i;
reg [15:0] best;
begin
m_oldest = 2'd0; best = 16'd0;
for (i = 0; i < N_EP; i = i + 1)
if (m_held[i] && (m_age[i] >= best)) begin
best = m_age[i]; m_oldest = i[1:0];
end
end
endfunction
function [1:0] m_waiter;
input dummy;
integer i;
reg [15:0] best;
reg any;
begin
m_waiter = 2'd0; best = 16'd0; any = 1'b0;
for (i = 0; i < N_EP; i = i + 1)
if (m_pend[i] && (!any || (m_wait[i] > best))) begin
best = m_wait[i]; m_waiter = i[1:0]; any = 1'b1;
end
end
endfunction
reg [N_EP-1:0] m_held_pre;
task model_step;
integer j;
reg [3:0] free_m;
reg [1:0] w;
begin
m_gr = 1'b0; m_bl = 1'b0; m_er = 1'b0;
m_gep = 2'd0; m_bep = 2'd0; m_hep = 2'd0; m_ec = E_NONE;
if (eot) begin
// nothing
end else begin
// A release is applied FIRST: a release and a request in the same
// cycle is the common case, and doing the request first blocks an
// endpoint against a buffer being handed back on that very cycle.
if (rel_valid) begin
if (m_held[rel_ep]) begin
m_held[rel_ep] = 1'b0;
m_age[rel_ep] = 16'd0;
c_rel = c_rel + 1;
end else begin
m_er = 1'b1; m_ec = E_UNHELD;
c_unh = c_unh + 1;
end
end
free_m = (mode ? N_BUF[3:0] : N_EP[3:0]) - popc(m_held);
if (req_valid) begin
c_req = c_req + 1;
if (m_held[req_ep] && !(rel_valid && (rel_ep == req_ep))) begin
m_er = 1'b1; m_ec = E_DOUBLE;
c_dbl = c_dbl + 1;
end else if (free_m != 4'd0) begin
m_held[req_ep] = 1'b1;
m_age[req_ep] = 16'd0;
m_pend[req_ep] = 1'b0;
m_wait[req_ep] = 16'd0;
m_gr = 1'b1; m_gep = req_ep;
c_grant = c_grant + 1;
end else begin
m_pend[req_ep] = 1'b1;
m_bl = 1'b1; m_bep = req_ep;
m_hep = m_oldest(1'b0);
c_block = c_block + 1;
end
end else if (|m_pend) begin
free_m = (mode ? N_BUF[3:0] : N_EP[3:0]) - popc(m_held);
if (free_m != 4'd0) begin
w = m_waiter(1'b0);
m_held[w] = 1'b1;
m_age[w] = 16'd0;
m_pend[w] = 1'b0;
if (m_wait[w] > m_worst) m_worst = m_wait[w];
m_wait[w] = 16'd0;
m_gr = 1'b1; m_gep = w;
c_grant = c_grant + 1;
end
end
for (j = 0; j < N_EP; j = j + 1) begin
if (m_held[j] && m_held_pre[j] && (m_age[j] != 16'hFFFF))
m_age[j] = m_age[j] + 16'd1;
if (m_pend[j] && (m_wait[j] != 16'hFFFF))
m_wait[j] = m_wait[j] + 16'd1;
if (m_pend[j] && (m_wait[j] > m_worst)) m_worst = m_wait[j];
end
end
end
endtask
task check_out;
begin
ck("held", {28'd0, held}, {28'd0, m_held});
ck("pending", {28'd0, pending}, {28'd0, m_pend});
ck("free_bufs", {28'd0, free_bufs},
{28'd0, (mode ? N_BUF[3:0] : N_EP[3:0]) - popc(m_held)});
ck("ram_bufs", {28'd0, ram_bufs}, {28'd0, mode ? N_BUF[3:0] : N_EP[3:0]});
ck("grant", {31'd0, grant}, {31'd0, m_gr});
ck("grant_ep", {30'd0, grant_ep}, {30'd0, m_gep});
ck("block_pulse",{31'd0, block_pulse}, {31'd0, m_bl});
ck("block_ep", {30'd0, block_ep}, {30'd0, m_bep});
ck("holder_ep", {30'd0, holder_ep}, {30'd0, m_hep});
ck("worst_wait", {16'd0, worst_wait}, {16'd0, m_worst});
ck("err_pulse", {31'd0, err_pulse}, {31'd0, m_er});
ck("err_code", {30'd0, err_code}, {30'd0, m_ec});
ck("n_req", n_req, c_req);
ck("n_grant", n_grant, c_grant);
ck("n_block", n_block, c_block);
ck("n_release", n_release, c_rel);
ck("n_double", n_double, c_dbl);
ck("n_unheld", n_unheld, c_unh);
// ---- the structural invariant ----
//
// A buffer is held by exactly one endpoint and the pool cannot be
// oversubscribed. If this ever fails the allocator has handed the
// same RAM to two endpoints, which is a data-corruption bug rather
// than a latency one.
ck("capacity", {28'd0, popc(held)},
{28'd0, (mode ? N_BUF[3:0] : N_EP[3:0]) - free_bufs});
if (popc(held) > (mode ? N_BUF[3:0] : N_EP[3:0])) begin
errors = errors + 1;
$display("FAIL pool oversubscribed: %0d held of %0d", popc(held),
mode ? N_BUF : N_EP);
end
end
endtask
task step;
integer idx;
begin
m_held_pre = m_held;
if (!eot) begin
idx = (mode ? 144 : 0) + {28'd0, m_held} * 9 +
(req_valid ? {30'd0, req_ep} :
rel_valid ? (4 + {30'd0, rel_ep}) : 8);
reach[idx] = 1'b1;
end
model_step;
@(posedge clk);
#1;
steps = steps + 1;
check_out;
end
endtask
task req; input [1:0] e;
begin
req_valid = 1'b1; rel_valid = 1'b0; eot = 1'b0; req_ep = e;
step;
req_valid = 1'b0;
end
endtask
task rel; input [1:0] e;
begin
req_valid = 1'b0; rel_valid = 1'b1; eot = 1'b0; rel_ep = e;
step;
rel_valid = 1'b0;
end
endtask
task nop;
begin
req_valid = 1'b0; rel_valid = 1'b0; eot = 1'b0;
step;
end
endtask
// ---- A release and a request in the SAME cycle. ----
//
// The common case on a busy controller, and the one that distinguishes an
// allocator which applies the release first from one which does not. The
// first version of this testbench drove it exactly once, in a directed
// phase, and the mutation that reverses the order scored 11 -- correct,
// and one reordered phase from zero.
task reqrel; input [1:0] e_req; input [1:0] e_rel;
begin
req_valid = 1'b1; rel_valid = 1'b1; eot = 1'b0;
req_ep = e_req; rel_ep = e_rel;
step;
req_valid = 1'b0; rel_valid = 1'b0;
end
endtask
task reset_all; input m;
integer j;
begin
req_valid = 1'b0; rel_valid = 1'b0; eot = 1'b0;
rst_n = 1'b0;
@(posedge clk); #1;
rst_n = 1'b1;
mode = m;
m_held = {N_EP{1'b0}}; m_pend = {N_EP{1'b0}}; m_worst = 16'd0;
m_gr = 1'b0; m_bl = 1'b0; m_er = 1'b0;
m_gep = 2'd0; m_bep = 2'd0; m_hep = 2'd0; m_ec = E_NONE;
for (j = 0; j < N_EP; j = j + 1) begin
m_age[j] = 16'd0; m_wait[j] = 16'd0;
end
c_req=0; c_grant=0; c_block=0; c_rel=0; c_dbl=0; c_unh=0;
@(negedge clk);
end
endtask
integer i, md, st, op, b, w, h0, h1, w0, w1;
integer base_block, base_grant, n_skipped, n_fair, n_same;
integer lat_ded, lat_shr;
initial begin
for (k = 0; k < 288; k = k + 1) reach[k] = 1'b0;
n_skipped = 0;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(negedge clk);
// ================= PHASE 1 -- the exhaustive (mode, state, op) sweep ==
//
// Each holding state is reached BY REQUESTING, never by forcing: a
// state forced into the design is a state the design never proved it
// can reach (chapter 22.4).
for (md = 0; md < 2; md = md + 1)
for (st = 0; st < 16; st = st + 1)
for (op = 0; op < 9; op = op + 1) begin
if ((md == 1) && (popc(st[3:0]) > N_BUF[3:0])) begin
// UNREACHABLE BY CONSTRUCTION, and declared rather than
// silently missing: the shared pool has N_BUF buffers, so any
// state with more holders than that cannot occur. Five of the
// sixteen states, nine operations each.
n_skipped = n_skipped + 1;
end else begin
reset_all(md[0]);
for (b = 0; b < N_EP; b = b + 1)
if (st[b]) req(b[1:0]);
if (op < 4) req(op[1:0]);
else if (op < 8) rel(op[1:0] - 2'd0);
else nop;
end
end
if (n_skipped != 45) begin
errors = errors + 1;
$display("FAIL skipped %0d unreachable combinations, expected 45",
n_skipped);
end
// ================= PHASE 2 -- DEDICATED never blocks ==================
//
// Every endpoint holding at once, repeatedly, with no pool to exhaust.
// The whole architectural claim in one assertion.
reset_all(1'b0);
base_block = c_block;
for (i = 0; i < 40; i = i + 1) begin
for (b = 0; b < N_EP; b = b + 1) req(b[1:0]);
for (b = 0; b < N_EP; b = b + 1) rel(b[1:0]);
end
if (c_block != base_block) begin
errors = errors + 1;
$display("FAIL DEDICATED blocked %0d times", c_block - base_block);
end
if (ram_bufs != N_EP) begin
errors = errors + 1;
$display("FAIL DEDICATED reports %0d buffers, expected %0d",
ram_bufs, N_EP);
end
// ================= PHASE 3 -- SHARED is IDENTICAL below the knee ======
//
// Three endpoints and three buffers: the pool is never exhausted, so a
// shared pool behaves exactly like a dedicated one. Bit for bit, no
// blocking, same grants. This is why the problem is never seen in
// testing -- every measurement below the knee says the design is fine.
reset_all(1'b1);
base_block = c_block; base_grant = c_grant;
for (i = 0; i < 40; i = i + 1) begin
for (b = 0; b < N_BUF; b = b + 1) req(b[1:0]);
for (b = 0; b < N_BUF; b = b + 1) rel(b[1:0]);
end
lat_shr = c_block - base_block;
if (lat_shr != 0) begin
errors = errors + 1;
$display("FAIL SHARED blocked %0d times below the knee", lat_shr);
end
if (c_grant - base_grant != 40 * N_BUF) begin
errors = errors + 1;
$display("FAIL SHARED granted %0d below the knee, expected %0d",
c_grant - base_grant, 40 * N_BUF);
end
// ================= PHASE 4 -- one endpoint past the knee ==============
//
// The fourth endpoint asks while three are held. Nothing is lost and
// nothing errors: it waits. And the report names the HOLDER.
reset_all(1'b1);
req(2'd0); // ep0 takes a buffer and keeps it
nop; nop; nop; // ...and ages
req(2'd1);
base_block = c_block;
req(2'd3); // the pool is empty: ep3 blocks
if (c_block != base_block + 1) begin
errors = errors + 1;
$display("FAIL the fourth request did not block");
end
if (block_ep !== 2'd3) begin
errors = errors + 1;
$display("FAIL block_ep is %0d, expected 3", block_ep);
end
// ---- THE point of the block. ----
if (holder_ep !== 2'd0) begin
errors = errors + 1;
$display("FAIL holder_ep is %0d, expected 0 (the oldest holder)",
holder_ep);
end
if (err_pulse !== 1'b0) begin
errors = errors + 1;
$display("FAIL blocking raised an error: it is latency, not an error");
end
// ...and it is granted the moment a buffer comes back, without asking
// again.
rel(2'd1);
if (!grant || (grant_ep !== 2'd3)) begin
errors = errors + 1;
$display("FAIL ep3 was not granted on release (grant=%0b ep=%0d)",
grant, grant_ep);
end
// ================= PHASE 5 -- fairness, over EVERY pair ===============
//
// Two endpoints hold the pool; the other two wait. The one that blocked
// FIRST must be served first, whichever two they are and whichever order
// they blocked in.
//
// The first version of this phase drove exactly one scenario -- endpoint
// 1 blocking before endpoint 0 -- and the mutation that serves the lowest
// pending index instead of the longest waiter died on FIVE checks. A
// property with one instance is a property that is nearly untested, so
// every choice of two holders and both orders of the two waiters is
// driven: 6 x 2 = 12 scenarios.
n_fair = 0;
for (h0 = 0; h0 < N_EP; h0 = h0 + 1)
for (h1 = h0 + 1; h1 < N_EP; h1 = h1 + 1)
for (op = 0; op < 2; op = op + 1) begin
// the two endpoints that are NOT holding are the two waiters
w0 = -1; w1 = -1;
for (b = 0; b < N_EP; b = b + 1)
if ((b != h0) && (b != h1)) begin
if (w0 < 0) w0 = b; else w1 = b;
end
// `op` picks which of them blocks first, so the longest waiter is
// sometimes the lower index and sometimes the higher one. An
// arbiter that serves the lowest index looks correct in half of
// these and only in half.
if (op == 1) begin i = w0; w0 = w1; w1 = i; end
reset_all(1'b1);
req(h0[1:0]); req(h1[1:0]); // the pool is full
req(w0[1:0]); // this one blocks FIRST
nop; nop; nop;
req(w1[1:0]); // and this one SECOND
nop;
if (pending !== ((4'd1 << w0) | (4'd1 << w1))) begin
errors = errors + 1;
$display("FAIL holders %0d,%0d: expected %0d and %0d waiting, pending=%b",
h0, h1, w0, w1, pending);
end
rel(h0[1:0]);
if (!grant || (grant_ep !== w0[1:0])) begin
errors = errors + 1;
$display("FAIL holders %0d,%0d: served ep%0d, the longest waiter is %0d",
h0, h1, grant_ep, w0);
end
// ...and the second waiter is served on the next release, not
// before it.
rel(h1[1:0]);
if (!grant || (grant_ep !== w1[1:0])) begin
errors = errors + 1;
$display("FAIL holders %0d,%0d: the second waiter was not served",
h0, h1);
end
n_fair = n_fair + 1;
end
if (n_fair != 12) begin
errors = errors + 1;
$display("FAIL fairness scenarios %0d, expected 12", n_fair);
end
// ================= PHASE 5b -- release + request, EVERY pair =========
//
// At capacity, in one cycle, on different endpoints. This is the common
// case on a busy controller and it is the one that distinguishes an
// allocator which applies the release first from one which does not: the
// buffer being handed back this cycle is the buffer the requester needs.
//
// Get the order wrong and nothing breaks -- the requester waits one cycle
// longer, every single time, and the only symptom is a block count
// nobody is looking at. Driven for every (holder released, requester)
// pair rather than once, for the same reason as phase 5.
n_same = 0;
for (h0 = 0; h0 < N_EP; h0 = h0 + 1)
for (h1 = h0 + 1; h1 < N_EP; h1 = h1 + 1)
for (op = 0; op < 2; op = op + 1)
for (i = 0; i < 2; i = i + 1) begin
w0 = -1; w1 = -1;
for (b = 0; b < N_EP; b = b + 1)
if ((b != h0) && (b != h1)) begin
if (w0 < 0) w0 = b; else w1 = b;
end
reset_all(1'b1);
req(h0[1:0]); req(h1[1:0]); // the pool is full
base_block = c_block;
// release one holder and have one non-holder ask, same cycle
if (op == 0) reqrel((i == 0) ? w0[1:0] : w1[1:0], h0[1:0]);
else reqrel((i == 0) ? w0[1:0] : w1[1:0], h1[1:0]);
if (c_block != base_block) begin
errors = errors + 1;
$display("FAIL same-cycle release+request blocked (holders %0d,%0d)",
h0, h1);
end
if (!grant) begin
errors = errors + 1;
$display("FAIL same-cycle release+request was not granted");
end
n_same = n_same + 1;
end
if (n_same != 24) begin
errors = errors + 1;
$display("FAIL same-cycle scenarios %0d, expected 24", n_same);
end
// ================= PHASE 6 -- the two firmware bugs ===================
reset_all(1'b1);
req(2'd0);
b = c_dbl;
req(2'd0); // asking again while holding
if (c_dbl != b + 1) begin
errors = errors + 1;
$display("FAIL a double request was not reported");
end
b = c_unh;
rel(2'd2); // releasing without holding
if (c_unh != b + 1) begin
errors = errors + 1;
$display("FAIL an unheld release was not reported");
end
// ...and a release-then-request on the SAME endpoint in one cycle is
// legal, not a double request.
reset_all(1'b1);
req(2'd1);
b = c_dbl;
req_valid = 1'b1; rel_valid = 1'b1; req_ep = 2'd1; rel_ep = 2'd1;
eot = 1'b0;
step;
req_valid = 1'b0; rel_valid = 1'b0;
if (c_dbl != b) begin
errors = errors + 1;
$display("FAIL release+request on one endpoint reported as a double");
end
// The random phase is switchable, because a mutation score is only
// interesting once it is DECOMPOSED. The directed phases already reach
// every situation the exhaustiveness proof requires, so nothing in that
// claim depends on it.
`ifndef DIRECTED_ONLY
// ================= PHASE 7 -- random, in both architectures ===========
//
// The stimulus is BIASED, and it has to be. Drawing an endpoint
// uniformly for a request hits one that is already holding about half
// the time, so a uniform run spends its cycles on double-request and
// unheld-release reports and almost never fills the pool -- the first
// version of this phase produced 4159 double requests and ZERO blocks.
//
// Requesting from an endpoint that does NOT hold, and releasing from
// one that does, is what a working driver does. The misuse cases still
// appear at a few percent, which is roughly their real rate and enough
// to keep those two checks under load.
for (md = 1; md >= 0; md = md - 1) begin
reset_all(md[0]);
base_block = c_block; base_grant = c_grant;
for (i = 0; i < 20000; i = i + 1) begin
w = $unsigned($random) % 100;
if (w < 48) begin
// a request, preferably from an endpoint with no buffer
b = $unsigned($random) % N_EP;
if ((w >= 45) || !m_held[b[1:0]]) req(b[1:0]);
else begin
for (k = 0; k < N_EP; k = k + 1)
if (!m_held[k[1:0]]) b = k;
req(b[1:0]);
end
end else if (w < 82) begin
// a release, preferably from one that has a buffer
b = $unsigned($random) % N_EP;
if ((w >= 79) || m_held[b[1:0]]) rel(b[1:0]);
else begin
for (k = 0; k < N_EP; k = k + 1)
if (m_held[k[1:0]]) b = k;
rel(b[1:0]);
end
end else if (w < 94) begin
// BOTH in one cycle: a holder hands a buffer back while a
// non-holder asks for one.
b = 0; op = 1;
for (k = 0; k < N_EP; k = k + 1) begin
if (m_held[k[1:0]]) b = k; // someone to release
if (!m_held[k[1:0]]) op = k; // someone to ask
end
reqrel(op[1:0], b[1:0]);
end else nop;
end
$display("PHASE7 %0s: grants=%0d blocks=%0d worst_wait=%0d",
md ? "SHARED " : "DEDICATED",
c_grant - base_grant, c_block - base_block, worst_wait);
end
`endif
// ================= the exhaustiveness proof ==========================
n_reach = 0; n_unreach = 0;
for (k = 0; k < 288; k = k + 1) begin
st = (k % 144) / 9;
if ((k >= 144) && (popc(st[3:0]) > N_BUF[3:0])) begin
n_unreach = n_unreach + 1;
// ...and a declared-unreachable combination must actually never
// have occurred. A declaration is a claim, and a claim that is
// falsified must fire (chapter 24.4).
if (reach[k]) begin
errors = errors + 1;
$display("FAIL a declared-unreachable combination OCCURRED: held=%0d op=%0d",
st, k % 9);
end
end else n_reach = n_reach + reach[k];
end
if (n_reach != 243) begin
errors = errors + 1;
$display("FAIL reachable (mode,state,op) %0d/243", n_reach);
for (k = 0; k < 288; k = k + 1) begin
st = (k % 144) / 9;
if (!reach[k] && !((k >= 144) && (popc(st[3:0]) > N_BUF[3:0])))
$display(" unreached mode=%0d held=%0d op=%0d", k / 144, st, k % 9);
end
end
$display("steps=%0d checks=%0d reach=%0d/243 (+%0d declared unreachable) errors=%0d",
steps, checks, n_reach, n_unreach, errors);
$display("req=%0d grant=%0d block=%0d release=%0d double=%0d unheld=%0d",
n_req, n_grant, n_block, n_release, n_double, n_unheld);
$display("worst_wait=%0d", worst_wait);
$display("%0s: %0d errors in %0d checks",
(errors == 0) ? "PASS" : "FAIL", errors, checks);
$finish;
end
endmoduleSystemVerilog testbench
`timescale 1ns/1ps
// Testbench for usb_fifo_pool.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS OVER (ARCHITECTURE, POOL STATE, OPERATION)
//
// A buffer allocator's answer depends on three things at once: which
// architecture it is configured as, exactly which endpoints are currently
// holding, and what is being asked. Sweeping any one of them alone proves
// nothing about the other two -- and the interesting behaviour, blocking,
// exists only at particular combinations.
//
// 2 modes x 16 holding states x 9 operations = 288
//
// Of those, the nine with all four endpoints holding in SHARED mode are
// UNREACHABLE by construction: the pool has three buffers. That is a claim
// about the design, so the testbench makes it explicitly and requires the
// other 279 -- rather than quietly reporting 279/288 and leaving a reader
// to wonder which nine were missed and why. Chapter 24.4's argument, in the
// place it actually bites.
module tb_fp_sv;
import usb_pool_pkg::*;
localparam int N_EP = 4;
localparam int N_BUF = 2;
logic clk = 1'b0, rst_n = 1'b0;
pool_mode_e mode = M_DEDICATED;
logic req_valid = 1'b0, rel_valid = 1'b0, eot = 1'b0;
logic [1:0] req_ep = 2'd0, rel_ep = 2'd0;
logic grant, block_pulse, err_pulse;
logic [1:0] grant_ep, block_ep, holder_ep;
pool_err_e err_code;
logic [N_EP-1:0] held, pending;
logic [3:0] free_bufs, ram_bufs;
logic [15:0] worst_wait;
logic [31:0] n_req, n_grant, n_block, n_release, n_double, n_unheld;
usb_fifo_pool #(.N_EP(N_EP), .N_BUF(N_BUF)) dut (
.clk(clk), .rst_n(rst_n),
.mode(mode), .req_valid(req_valid), .req_ep(req_ep),
.rel_valid(rel_valid), .rel_ep(rel_ep), .eot(eot),
.grant(grant), .grant_ep(grant_ep),
.block_pulse(block_pulse), .block_ep(block_ep), .holder_ep(holder_ep),
.held(held), .pending(pending),
.free_bufs(free_bufs), .ram_bufs(ram_bufs), .worst_wait(worst_wait),
.err_pulse(err_pulse), .err_code(err_code),
.n_req(n_req), .n_grant(n_grant), .n_block(n_block),
.n_release(n_release), .n_double(n_double), .n_unheld(n_unheld)
);
always #5 clk = ~clk;
// ---------------- the shadow model ----------------
logic [N_EP-1:0] m_held, m_pend;
logic [15:0] m_age [N_EP-1:0];
logic [15:0] m_wait [N_EP-1:0];
logic [15:0] m_worst;
logic m_gr, m_bl, m_er;
logic [1:0] m_gep, m_bep, m_hep;
pool_err_e m_ec;
int unsigned c_req, c_grant, c_block, c_rel, c_dbl, c_unh;
int errors = 0, checks = 0, steps = 0;
int k;
// reach: mode (2) x holding state (16) x operation (9)
bit reach [288];
int n_reach, n_unreach;
task automatic ck(string nm, int unsigned got, int unsigned exp);
checks++;
if (got !== exp) begin
errors++;
if (errors < 25)
$display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
$time, steps, nm, got, exp);
end
endtask
function automatic logic [3:0] popc(input logic [N_EP-1:0] v);
int i;
begin
popc = 4'd0;
for (i = 0; i < N_EP; i = i + 1) if (v[i]) popc = popc + 4'd1;
end
endfunction
// The oldest holder, from the model's own ages. The tie-break matches the
// design's documented one: on equal age the highest endpoint index wins,
// because the scan uses >= and runs upwards.
function automatic logic [1:0] m_oldest();
int i;
logic [15:0] best;
begin
m_oldest = 2'd0; best = 16'd0;
for (i = 0; i < N_EP; i = i + 1)
if (m_held[i] && (m_age[i] >= best)) begin
best = m_age[i]; m_oldest = i[1:0];
end
end
endfunction
function automatic logic [1:0] m_waiter();
int i;
logic [15:0] best;
logic any;
begin
m_waiter = 2'd0; best = 16'd0; any = 1'b0;
for (i = 0; i < N_EP; i = i + 1)
if (m_pend[i] && (!any || (m_wait[i] > best))) begin
best = m_wait[i]; m_waiter = i[1:0]; any = 1'b1;
end
end
endfunction
logic [N_EP-1:0] m_held_pre;
task automatic model_step;
int j;
logic [3:0] free_m;
logic [1:0] w;
begin
m_gr = 1'b0; m_bl = 1'b0; m_er = 1'b0;
m_gep = 2'd0; m_bep = 2'd0; m_hep = 2'd0; m_ec = E_NONE;
if (eot) begin
// nothing
end else begin
// A release is applied FIRST: a release and a request in the same
// cycle is the common case, and doing the request first blocks an
// endpoint against a buffer being handed back on that very cycle.
if (rel_valid) begin
if (m_held[rel_ep]) begin
m_held[rel_ep] = 1'b0;
m_age[rel_ep] = 16'd0;
c_rel = c_rel + 1;
end else begin
m_er = 1'b1; m_ec = E_UNHELD;
c_unh = c_unh + 1;
end
end
free_m = ((mode == M_SHARED) ? 4'(N_BUF) : 4'(N_EP)) - popc(m_held);
if (req_valid) begin
c_req = c_req + 1;
if (m_held[req_ep] && !(rel_valid && (rel_ep == req_ep))) begin
m_er = 1'b1; m_ec = E_DOUBLE;
c_dbl = c_dbl + 1;
end else if (free_m != 4'd0) begin
m_held[req_ep] = 1'b1;
m_age[req_ep] = 16'd0;
m_pend[req_ep] = 1'b0;
m_wait[req_ep] = 16'd0;
m_gr = 1'b1; m_gep = req_ep;
c_grant = c_grant + 1;
end else begin
m_pend[req_ep] = 1'b1;
m_bl = 1'b1; m_bep = req_ep;
m_hep = m_oldest();
c_block = c_block + 1;
end
end else if (|m_pend) begin
free_m = ((mode == M_SHARED) ? 4'(N_BUF) : 4'(N_EP)) - popc(m_held);
if (free_m != 4'd0) begin
w = m_waiter();
m_held[w] = 1'b1;
m_age[w] = 16'd0;
m_pend[w] = 1'b0;
if (m_wait[w] > m_worst) m_worst = m_wait[w];
m_wait[w] = 16'd0;
m_gr = 1'b1; m_gep = w;
c_grant = c_grant + 1;
end
end
for (j = 0; j < N_EP; j = j + 1) begin
if (m_held[j] && m_held_pre[j] && (m_age[j] != 16'hFFFF))
m_age[j] = m_age[j] + 16'd1;
if (m_pend[j] && (m_wait[j] != 16'hFFFF))
m_wait[j] = m_wait[j] + 16'd1;
if (m_pend[j] && (m_wait[j] > m_worst)) m_worst = m_wait[j];
end
end
end
endtask
task automatic check_out;
begin
ck("held", {28'd0, held}, {28'd0, m_held});
ck("pending", {28'd0, pending}, {28'd0, m_pend});
ck("free_bufs", {28'd0, free_bufs},
{28'd0, ((mode == M_SHARED) ? 4'(N_BUF) : 4'(N_EP)) - popc(m_held)});
ck("ram_bufs", ram_bufs, ((mode == M_SHARED) ? 4'(N_BUF) : 4'(N_EP)));
ck("grant", {31'd0, grant}, {31'd0, m_gr});
ck("grant_ep", {30'd0, grant_ep}, {30'd0, m_gep});
ck("block_pulse",{31'd0, block_pulse}, {31'd0, m_bl});
ck("block_ep", {30'd0, block_ep}, {30'd0, m_bep});
ck("holder_ep", {30'd0, holder_ep}, {30'd0, m_hep});
ck("worst_wait", {16'd0, worst_wait}, {16'd0, m_worst});
ck("err_pulse", {31'd0, err_pulse}, {31'd0, m_er});
ck("err_code", err_code, m_ec);
ck("n_req", n_req, c_req);
ck("n_grant", n_grant, c_grant);
ck("n_block", n_block, c_block);
ck("n_release", n_release, c_rel);
ck("n_double", n_double, c_dbl);
ck("n_unheld", n_unheld, c_unh);
// ---- the structural invariant ----
//
// A buffer is held by exactly one endpoint and the pool cannot be
// oversubscribed. If this ever fails the allocator has handed the
// same RAM to two endpoints, which is a data-corruption bug rather
// than a latency one.
ck("capacity", {28'd0, popc(held)},
{28'd0, ((mode == M_SHARED) ? 4'(N_BUF) : 4'(N_EP)) - free_bufs});
if (popc(held) > ((mode == M_SHARED) ? 4'(N_BUF) : 4'(N_EP))) begin
errors = errors + 1;
$display("FAIL pool oversubscribed: %0d held of %0d", popc(held),
(mode == M_SHARED) ? N_BUF : N_EP);
end
end
endtask
task automatic step;
integer idx;
begin
m_held_pre = m_held;
if (!eot) begin
idx = ((mode == M_SHARED) ? 144 : 0) + int'(m_held) * 9 +
(req_valid ? int'(req_ep) :
rel_valid ? (4 + int'(rel_ep)) : 8);
reach[idx] = 1'b1;
end
model_step;
@(posedge clk);
#1;
steps = steps + 1;
check_out;
end
endtask
task automatic req(logic [1:0] e);
begin
req_valid = 1'b1; rel_valid = 1'b0; eot = 1'b0; req_ep = e;
step;
req_valid = 1'b0;
end
endtask
task automatic rel(logic [1:0] e);
begin
req_valid = 1'b0; rel_valid = 1'b1; eot = 1'b0; rel_ep = e;
step;
rel_valid = 1'b0;
end
endtask
task automatic nop;
begin
req_valid = 1'b0; rel_valid = 1'b0; eot = 1'b0;
step;
end
endtask
// ---- A release and a request in the SAME cycle. ----
//
// The common case on a busy controller, and the one that distinguishes an
// allocator which applies the release first from one which does not. The
// first version of this testbench drove it exactly once, in a directed
// phase, and the mutation that reverses the order scored 11 -- correct,
// and one reordered phase from zero.
task automatic reqrel(logic [1:0] e_req, logic [1:0] e_rel);
begin
req_valid = 1'b1; rel_valid = 1'b1; eot = 1'b0;
req_ep = e_req; rel_ep = e_rel;
step;
req_valid = 1'b0; rel_valid = 1'b0;
end
endtask
task automatic reset_all(pool_mode_e m);
int j;
begin
req_valid = 1'b0; rel_valid = 1'b0; eot = 1'b0;
rst_n = 1'b0;
@(posedge clk); #1;
rst_n = 1'b1;
mode = m;
m_held = {N_EP{1'b0}}; m_pend = {N_EP{1'b0}}; m_worst = 16'd0;
m_gr = 1'b0; m_bl = 1'b0; m_er = 1'b0;
m_gep = 2'd0; m_bep = 2'd0; m_hep = 2'd0; m_ec = E_NONE;
for (j = 0; j < N_EP; j = j + 1) begin
m_age[j] = 16'd0; m_wait[j] = 16'd0;
end
c_req=0; c_grant=0; c_block=0; c_rel=0; c_dbl=0; c_unh=0;
@(negedge clk);
end
endtask
integer i, md, st, op, b, w, h0, h1, w0, w1;
integer base_block, base_grant, n_skipped, n_fair, n_same;
integer lat_ded, lat_shr;
initial begin
foreach (reach[q]) reach[q] = 1'b0;
n_skipped = 0;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(negedge clk);
// ================= PHASE 1 -- the exhaustive (mode, state, op) sweep ==
//
// Each holding state is reached BY REQUESTING, never by forcing: a
// state forced into the design is a state the design never proved it
// can reach (chapter 22.4).
for (md = 0; md < 2; md = md + 1)
for (st = 0; st < 16; st = st + 1)
for (op = 0; op < 9; op = op + 1) begin
if ((md == 1) && (popc(4'(st)) > 4'(N_BUF))) begin
// UNREACHABLE BY CONSTRUCTION, and declared rather than
// silently missing: the shared pool has N_BUF buffers, so any
// state with more holders than that cannot occur. Five of the
// sixteen states, nine operations each.
n_skipped = n_skipped + 1;
end else begin
reset_all(pool_mode_e'(md[0]));
for (b = 0; b < N_EP; b = b + 1)
if (st[b]) req(b[1:0]);
if (op < 4) req(op[1:0]);
else if (op < 8) rel(op[1:0]);
else nop;
end
end
if (n_skipped != 45) begin
errors = errors + 1;
$display("FAIL skipped %0d unreachable combinations, expected 45",
n_skipped);
end
// ================= PHASE 2 -- DEDICATED never blocks ==================
//
// Every endpoint holding at once, repeatedly, with no pool to exhaust.
// The whole architectural claim in one assertion.
reset_all(M_DEDICATED);
base_block = c_block;
for (i = 0; i < 40; i = i + 1) begin
for (b = 0; b < N_EP; b = b + 1) req(b[1:0]);
for (b = 0; b < N_EP; b = b + 1) rel(b[1:0]);
end
if (c_block != base_block) begin
errors = errors + 1;
$display("FAIL DEDICATED blocked %0d times", c_block - base_block);
end
if (ram_bufs != N_EP) begin
errors = errors + 1;
$display("FAIL DEDICATED reports %0d buffers, expected %0d",
ram_bufs, N_EP);
end
// ================= PHASE 3 -- SHARED is IDENTICAL below the knee ======
//
// Three endpoints and three buffers: the pool is never exhausted, so a
// shared pool behaves exactly like a dedicated one. Bit for bit, no
// blocking, same grants. This is why the problem is never seen in
// testing -- every measurement below the knee says the design is fine.
reset_all(M_SHARED);
base_block = c_block; base_grant = c_grant;
for (i = 0; i < 40; i = i + 1) begin
for (b = 0; b < N_BUF; b = b + 1) req(2'(b));
for (b = 0; b < N_BUF; b = b + 1) rel(2'(b));
end
lat_shr = c_block - base_block;
if (lat_shr != 0) begin
errors = errors + 1;
$display("FAIL SHARED blocked %0d times below the knee", lat_shr);
end
if (c_grant - base_grant != 40 * N_BUF) begin
errors = errors + 1;
$display("FAIL SHARED granted %0d below the knee, expected %0d",
c_grant - base_grant, 40 * N_BUF);
end
// ================= PHASE 4 -- one endpoint past the knee ==============
//
// The fourth endpoint asks while three are held. Nothing is lost and
// nothing errors: it waits. And the report names the HOLDER.
reset_all(M_SHARED);
req(2'd0); // ep0 takes a buffer and keeps it
nop; nop; nop; // ...and ages
req(2'd1);
base_block = c_block;
req(2'd3); // the pool is empty: ep3 blocks
if (c_block != base_block + 1) begin
errors = errors + 1;
$display("FAIL the fourth request did not block");
end
if (block_ep !== 2'd3) begin
errors = errors + 1;
$display("FAIL block_ep is %0d, expected 3", block_ep);
end
// ---- THE point of the block. ----
if (holder_ep !== 2'd0) begin
errors = errors + 1;
$display("FAIL holder_ep is %0d, expected 0 (the oldest holder)",
holder_ep);
end
if (err_pulse !== 1'b0) begin
errors = errors + 1;
$display("FAIL blocking raised an error: it is latency, not an error");
end
// ...and it is granted the moment a buffer comes back, without asking
// again.
rel(2'd1);
if (!grant || (grant_ep !== 2'd3)) begin
errors = errors + 1;
$display("FAIL ep3 was not granted on release (grant=%0b ep=%0d)",
grant, grant_ep);
end
// ================= PHASE 5 -- fairness, over EVERY pair ===============
//
// Two endpoints hold the pool; the other two wait. The one that blocked
// FIRST must be served first, whichever two they are and whichever order
// they blocked in.
//
// The first version of this phase drove exactly one scenario -- endpoint
// 1 blocking before endpoint 0 -- and the mutation that serves the lowest
// pending index instead of the longest waiter died on FIVE checks. A
// property with one instance is a property that is nearly untested, so
// every choice of two holders and both orders of the two waiters is
// driven: 6 x 2 = 12 scenarios.
n_fair = 0;
for (h0 = 0; h0 < N_EP; h0 = h0 + 1)
for (h1 = h0 + 1; h1 < N_EP; h1 = h1 + 1)
for (op = 0; op < 2; op = op + 1) begin
// the two endpoints that are NOT holding are the two waiters
w0 = -1; w1 = -1;
for (b = 0; b < N_EP; b = b + 1)
if ((b != h0) && (b != h1)) begin
if (w0 < 0) w0 = b; else w1 = b;
end
// `op` picks which of them blocks first, so the longest waiter is
// sometimes the lower index and sometimes the higher one. An
// arbiter that serves the lowest index looks correct in half of
// these and only in half.
if (op == 1) begin i = w0; w0 = w1; w1 = i; end
reset_all(M_SHARED);
req(2'(h0)); req(2'(h1)); // the pool is full
req(2'(w0)); // this one blocks FIRST
nop; nop; nop;
req(2'(w1)); // and this one SECOND
nop;
if (pending !== (4'((4'd1 << w0) | (4'd1 << w1)))) begin
errors = errors + 1;
$display("FAIL holders %0d,%0d: expected %0d and %0d waiting, pending=%b",
h0, h1, w0, w1, pending);
end
rel(2'(h0));
if (!grant || (grant_ep !== 2'(w0))) begin
errors = errors + 1;
$display("FAIL holders %0d,%0d: served ep%0d, the longest waiter is %0d",
h0, h1, grant_ep, w0);
end
// ...and the second waiter is served on the next release, not
// before it.
rel(2'(h1));
if (!grant || (grant_ep !== 2'(w1))) begin
errors = errors + 1;
$display("FAIL holders %0d,%0d: the second waiter was not served",
h0, h1);
end
n_fair = n_fair + 1;
end
if (n_fair != 12) begin
errors = errors + 1;
$display("FAIL fairness scenarios %0d, expected 12", n_fair);
end
// ================= PHASE 5b -- release + request, EVERY pair =========
//
// At capacity, in one cycle, on different endpoints. This is the common
// case on a busy controller and it is the one that distinguishes an
// allocator which applies the release first from one which does not: the
// buffer being handed back this cycle is the buffer the requester needs.
//
// Get the order wrong and nothing breaks -- the requester waits one cycle
// longer, every single time, and the only symptom is a block count
// nobody is looking at. Driven for every (holder released, requester)
// pair rather than once, for the same reason as phase 5.
n_same = 0;
for (h0 = 0; h0 < N_EP; h0 = h0 + 1)
for (h1 = h0 + 1; h1 < N_EP; h1 = h1 + 1)
for (op = 0; op < 2; op = op + 1)
for (i = 0; i < 2; i = i + 1) begin
w0 = -1; w1 = -1;
for (b = 0; b < N_EP; b = b + 1)
if ((b != h0) && (b != h1)) begin
if (w0 < 0) w0 = b; else w1 = b;
end
reset_all(M_SHARED);
req(2'(h0)); req(2'(h1)); // the pool is full
base_block = int'(c_block);
// release one holder and have one non-holder ask, same cycle
if (op == 0) reqrel((i == 0) ? 2'(w0) : 2'(w1), 2'(h0));
else reqrel((i == 0) ? 2'(w0) : 2'(w1), 2'(h1));
if (int'(c_block) != base_block) begin
errors = errors + 1;
$display("FAIL same-cycle release+request blocked (holders %0d,%0d)",
h0, h1);
end
if (!grant) begin
errors = errors + 1;
$display("FAIL same-cycle release+request was not granted");
end
n_same = n_same + 1;
end
if (n_same != 24) begin
errors = errors + 1;
$display("FAIL same-cycle scenarios %0d, expected 24", n_same);
end
// ================= PHASE 6 -- the two firmware bugs ===================
reset_all(M_SHARED);
req(2'd0);
b = c_dbl;
req(2'd0); // asking again while holding
if (c_dbl != b + 1) begin
errors = errors + 1;
$display("FAIL a double request was not reported");
end
b = c_unh;
rel(2'd2); // releasing without holding
if (c_unh != b + 1) begin
errors = errors + 1;
$display("FAIL an unheld release was not reported");
end
// ...and a release-then-request on the SAME endpoint in one cycle is
// legal, not a double request.
reset_all(M_SHARED);
req(2'd1);
b = c_dbl;
req_valid = 1'b1; rel_valid = 1'b1; req_ep = 2'd1; rel_ep = 2'd1;
eot = 1'b0;
step;
req_valid = 1'b0; rel_valid = 1'b0;
if (c_dbl != b) begin
errors = errors + 1;
$display("FAIL release+request on one endpoint reported as a double");
end
// The random phase is switchable, because a mutation score is only
// interesting once it is DECOMPOSED. The directed phases already reach
// every situation the exhaustiveness proof requires, so nothing in that
// claim depends on it.
`ifndef DIRECTED_ONLY
// ================= PHASE 7 -- random, in both architectures ===========
//
// The stimulus is BIASED, and it has to be. Drawing an endpoint
// uniformly for a request hits one that is already holding about half
// the time, so a uniform run spends its cycles on double-request and
// unheld-release reports and almost never fills the pool -- the first
// version of this phase produced 4159 double requests and ZERO blocks.
//
// Requesting from an endpoint that does NOT hold, and releasing from
// one that does, is what a working driver does. The misuse cases still
// appear at a few percent, which is roughly their real rate and enough
// to keep those two checks under load.
for (md = 1; md >= 0; md = md - 1) begin
reset_all(pool_mode_e'(md[0]));
base_block = c_block; base_grant = c_grant;
for (i = 0; i < 20000; i = i + 1) begin
w = $unsigned($random) % 100;
if (w < 48) begin
// a request, preferably from an endpoint with no buffer
b = $unsigned($random) % N_EP;
if ((w >= 45) || !m_held[b[1:0]]) req(b[1:0]);
else begin
for (k = 0; k < N_EP; k = k + 1)
if (!m_held[k[1:0]]) b = k;
req(b[1:0]);
end
end else if (w < 82) begin
// a release, preferably from one that has a buffer
b = $unsigned($random) % N_EP;
if ((w >= 79) || m_held[b[1:0]]) rel(b[1:0]);
else begin
for (k = 0; k < N_EP; k = k + 1)
if (m_held[k[1:0]]) b = k;
rel(b[1:0]);
end
end else if (w < 94) begin
// BOTH in one cycle: a holder hands a buffer back while a
// non-holder asks for one.
b = 0; op = 1;
for (k = 0; k < N_EP; k = k + 1) begin
if (m_held[k[1:0]]) b = k; // someone to release
if (!m_held[k[1:0]]) op = k; // someone to ask
end
reqrel(2'(op), 2'(b));
end else nop;
end
$display("PHASE7 %0s: grants=%0d blocks=%0d worst_wait=%0d",
(md == 1) ? "SHARED " : "DEDICATED",
c_grant - base_grant, c_block - base_block, worst_wait);
end
`endif
// ================= the exhaustiveness proof ==========================
n_reach = 0; n_unreach = 0;
for (k = 0; k < 288; k = k + 1) begin
st = (k % 144) / 9;
if ((k >= 144) && (popc(4'(st)) > 4'(N_BUF))) begin
n_unreach = n_unreach + 1;
// ...and a declared-unreachable combination must actually never
// have occurred. A declaration is a claim, and a claim that is
// falsified must fire (chapter 24.4).
if (reach[k]) begin
errors = errors + 1;
$display("FAIL a declared-unreachable combination OCCURRED: held=%0d op=%0d",
st, k % 9);
end
end else n_reach = n_reach + reach[k];
end
if (n_reach != 243) begin
errors = errors + 1;
$display("FAIL reachable (mode,state,op) %0d/243", n_reach);
for (k = 0; k < 288; k = k + 1) begin
st = (k % 144) / 9;
if (!reach[k] && !((k >= 144) && (popc(4'(st)) > 4'(N_BUF))))
$display(" unreached mode=%0d held=%0d op=%0d", k / 144, st, k % 9);
end
end
$display("steps=%0d checks=%0d reach=%0d/243 (+%0d declared unreachable) errors=%0d",
steps, checks, n_reach, n_unreach, errors);
$display("req=%0d grant=%0d block=%0d release=%0d double=%0d unheld=%0d",
n_req, n_grant, n_block, n_release, n_double, n_unheld);
$display("worst_wait=%0d", worst_wait);
$display("%0s: %0d errors in %0d checks",
(errors == 0) ? "PASS" : "FAIL", errors, checks);
$finish;
end
endmoduleVHDL-2008 testbench
-- Testbench for usb_fifo_pool (VHDL-2008).
--
-- The oracle is a shadow model held in process variables and written from
-- the chapter's rules rather than from the RTL, re-derived every cycle and
-- compared against every output.
--
-- THE EXHAUSTIVE CLAIM IS OVER (ARCHITECTURE, POOL STATE, OPERATION)
--
-- A buffer allocator's answer depends on three things at once: which
-- architecture it is configured as, exactly which endpoints are currently
-- holding, and what is being asked. Sweeping any one of them alone proves
-- nothing about the other two -- and the interesting behaviour, blocking,
-- exists only at particular combinations.
--
-- 2 modes x 16 holding states x 9 operations = 288
--
-- Of those, the nine with all four endpoints holding in SHARED mode are
-- UNREACHABLE by construction: the pool has three buffers. That is a claim
-- about the design, so the testbench makes it explicitly and requires the
-- other 279 -- rather than quietly reporting 279/288 and leaving a reader to
-- wonder which nine were missed and why.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_pool_pkg.all;
entity tb_fp_vhdl is
end entity;
architecture sim of tb_fp_vhdl is
constant N_EP : integer := 4;
constant N_BUF : integer := 2;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal mode : std_logic := M_DEDICATED;
signal req_valid : std_logic := '0';
signal rel_valid : std_logic := '0';
signal eot : std_logic := '0';
signal req_ep : std_logic_vector(1 downto 0) := "00";
signal rel_ep : std_logic_vector(1 downto 0) := "00";
signal grant_s : std_logic;
signal grant_ep_s : std_logic_vector(1 downto 0);
signal block_pulse_s : std_logic;
signal block_ep_s : std_logic_vector(1 downto 0);
signal holder_ep_s : std_logic_vector(1 downto 0);
signal held_s : std_logic_vector(N_EP-1 downto 0);
signal pending_s : std_logic_vector(N_EP-1 downto 0);
signal free_bufs_s : unsigned(3 downto 0);
signal ram_bufs_s : unsigned(3 downto 0);
signal worst_wait_s : unsigned(15 downto 0);
signal err_pulse_s : std_logic;
signal err_code_s : std_logic_vector(1 downto 0);
signal n_req_s, n_grant_s, n_block_s : unsigned(31 downto 0);
signal n_rel_s, n_dbl_s, n_unh_s : unsigned(31 downto 0);
signal done : boolean := false;
type int_array is array (natural range <>) of integer;
begin
clk <= not clk after 5 ns when not done else '0';
dut : entity work.usb_fifo_pool
generic map (N_EP => N_EP, N_BUF => N_BUF)
port map (
clk => clk, rst_n => rst_n,
mode => mode, req_valid => req_valid, req_ep => req_ep,
rel_valid => rel_valid, rel_ep => rel_ep, eot => eot,
grant => grant_s, grant_ep => grant_ep_s,
block_pulse => block_pulse_s, block_ep => block_ep_s,
holder_ep => holder_ep_s,
held => held_s, pending => pending_s,
free_bufs => free_bufs_s, ram_bufs => ram_bufs_s,
worst_wait => worst_wait_s,
err_pulse => err_pulse_s, err_code => err_code_s,
n_req => n_req_s, n_grant => n_grant_s, n_block => n_block_s,
n_release => n_rel_s, n_double => n_dbl_s, n_unheld => n_unh_s
);
stim : process
-- ---------------- the shadow model ----------------
variable m_held, m_pend : std_logic_vector(N_EP-1 downto 0)
:= (others => '0');
variable m_age, m_wait : age_array(0 to N_EP-1)
:= (others => (others => '0'));
variable m_held_pre : std_logic_vector(N_EP-1 downto 0) := (others => '0');
variable m_worst : unsigned(15 downto 0) := (others => '0');
variable m_gr, m_bl, m_er : std_logic := '0';
variable m_gep, m_bep, m_hep : std_logic_vector(1 downto 0) := "00";
variable m_ec : std_logic_vector(1 downto 0) := E_NONE;
variable c_req, c_grant, c_block : integer := 0;
variable c_rel, c_dbl, c_unh : integer := 0;
variable errors, checks, steps : integer := 0;
variable reach : int_array(0 to 287) := (others => 0);
variable n_reach, n_unreach, n_skipped : integer := 0;
variable base_block, base_grant : integer := 0;
variable bb, hb, w_v : integer := 0;
-- h0/h1 are FOR-loop variables below and implicitly declared there;
-- declaring them here as well shadows them and warns on every analysis.
variable w0, w1 : integer := 0;
variable n_fair, n_same : integer := 0;
variable ln : line;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"BEEF1234";
impure function rnd_nat return integer is
begin
lfsr := lfsr(30 downto 0) &
(lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
-- Only the low 30 bits: a full 32-bit unsigned does not fit in VHDL's
-- INTEGER, and to_integer aborts the run rather than wrapping.
return to_integer(lfsr(29 downto 0));
end function;
procedure ck (nm : string; got, exp : integer) is
begin
checks := checks + 1;
if got /= exp then
errors := errors + 1;
if errors < 25 then
write(ln, string'("FAIL step=") & integer'image(steps) & " " & nm
& " got=" & integer'image(got)
& " exp=" & integer'image(exp));
writeline(output, ln);
end if;
end if;
end procedure;
function sl2i (s : std_logic) return integer is
begin
if s = '1' then return 1; else return 0; end if;
end function;
function popc_i (x : integer) return integer is
variable c, t : integer := 0;
begin
c := 0; t := x;
for i in 0 to 3 loop
if (t mod 2) = 1 then c := c + 1; end if;
t := t / 2;
end loop;
return c;
end function;
function popc_m (v : std_logic_vector) return integer is
variable c : integer := 0;
begin
for i in v'range loop
if v(i) = '1' then c := c + 1; end if;
end loop;
return c;
end function;
impure function cap return integer is
begin
if mode = M_SHARED then return N_BUF; else return N_EP; end if;
end function;
procedure model_step is
variable free_m : integer;
variable best : unsigned(15 downto 0);
variable any : boolean;
variable wnr : integer;
variable e, r : integer;
begin
m_gr := '0'; m_bl := '0'; m_er := '0';
m_gep := "00"; m_bep := "00"; m_hep := "00"; m_ec := E_NONE;
e := to_integer(unsigned(req_ep));
r := to_integer(unsigned(rel_ep));
if eot = '1' then
null;
else
-- A release is applied FIRST: a release and a request in the same
-- cycle is the common case, and doing the request first blocks an
-- endpoint against a buffer being handed back on that very cycle.
if rel_valid = '1' then
if m_held(r) = '1' then
m_held(r) := '0';
m_age(r) := (others => '0');
c_rel := c_rel + 1;
else
m_er := '1'; m_ec := E_UNHELD;
c_unh := c_unh + 1;
end if;
end if;
free_m := cap - popc_m(m_held);
if req_valid = '1' then
c_req := c_req + 1;
if (m_held_pre(e) = '1')
and not (rel_valid = '1' and rel_ep = req_ep) then
m_er := '1'; m_ec := E_DOUBLE;
c_dbl := c_dbl + 1;
elsif free_m /= 0 then
m_held(e) := '1';
m_age(e) := (others => '0');
m_pend(e) := '0';
m_wait(e) := (others => '0');
m_gr := '1'; m_gep := req_ep;
c_grant := c_grant + 1;
else
m_pend(e) := '1';
m_bl := '1'; m_bep := req_ep;
best := (others => '0');
m_hep := "00";
for i in 0 to N_EP-1 loop
if m_held_pre(i) = '1' and m_age(i) >= best then
best := m_age(i);
m_hep := std_logic_vector(to_unsigned(i, 2));
end if;
end loop;
c_block := c_block + 1;
end if;
elsif m_pend /= (m_pend'range => '0') then
free_m := cap - popc_m(m_held);
if free_m /= 0 then
best := (others => '0');
any := false;
wnr := 0;
for i in 0 to N_EP-1 loop
if m_pend(i) = '1' and ((not any) or m_wait(i) > best) then
best := m_wait(i); wnr := i; any := true;
end if;
end loop;
m_held(wnr) := '1';
m_age(wnr) := (others => '0');
m_pend(wnr) := '0';
if m_wait(wnr) > m_worst then m_worst := m_wait(wnr); end if;
m_wait(wnr) := (others => '0');
m_gr := '1';
m_gep := std_logic_vector(to_unsigned(wnr, 2));
c_grant := c_grant + 1;
end if;
end if;
for i in 0 to N_EP-1 loop
if m_held(i) = '1' and m_held_pre(i) = '1' and m_age(i) /= x"FFFF" then
m_age(i) := m_age(i) + 1;
end if;
if m_pend(i) = '1' and m_wait(i) /= x"FFFF" then
m_wait(i) := m_wait(i) + 1;
end if;
if m_pend(i) = '1' and m_wait(i) > m_worst then
m_worst := m_wait(i);
end if;
end loop;
end if;
end procedure;
procedure check_out is
begin
ck("held", to_integer(unsigned(held_s)),
to_integer(unsigned(m_held)));
ck("pending", to_integer(unsigned(pending_s)),
to_integer(unsigned(m_pend)));
ck("free_bufs", to_integer(free_bufs_s), cap - popc_m(m_held));
ck("ram_bufs", to_integer(ram_bufs_s), cap);
ck("grant", sl2i(grant_s), sl2i(m_gr));
ck("grant_ep", to_integer(unsigned(grant_ep_s)),
to_integer(unsigned(m_gep)));
ck("block_pulse",sl2i(block_pulse_s), sl2i(m_bl));
ck("block_ep", to_integer(unsigned(block_ep_s)),
to_integer(unsigned(m_bep)));
ck("holder_ep", to_integer(unsigned(holder_ep_s)),
to_integer(unsigned(m_hep)));
ck("worst_wait", to_integer(worst_wait_s), to_integer(m_worst));
ck("err_pulse", sl2i(err_pulse_s), sl2i(m_er));
ck("err_code", to_integer(unsigned(err_code_s)),
to_integer(unsigned(m_ec)));
ck("n_req", to_integer(n_req_s), c_req);
ck("n_grant", to_integer(n_grant_s), c_grant);
ck("n_block", to_integer(n_block_s), c_block);
ck("n_release", to_integer(n_rel_s), c_rel);
ck("n_double", to_integer(n_dbl_s), c_dbl);
ck("n_unheld", to_integer(n_unh_s), c_unh);
-- ---- the structural invariant ----
--
-- A buffer is held by exactly one endpoint and the pool cannot be
-- oversubscribed. If this ever fails the allocator has handed the same
-- RAM to two endpoints, which is a data-corruption bug rather than a
-- latency one.
ck("capacity", popc_m(held_s), cap - to_integer(free_bufs_s));
if popc_m(held_s) > cap then
errors := errors + 1;
write(ln, string'("FAIL pool oversubscribed"));
writeline(output, ln);
end if;
end procedure;
procedure step is
variable idx : integer;
begin
m_held_pre := m_held;
if eot = '0' then
idx := to_integer(unsigned(m_held)) * 9;
if mode = M_SHARED then idx := idx + 144; end if;
if req_valid = '1' then
idx := idx + to_integer(unsigned(req_ep));
elsif rel_valid = '1' then
idx := idx + 4 + to_integer(unsigned(rel_ep));
else
idx := idx + 8;
end if;
reach(idx) := 1;
end if;
model_step;
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
check_out;
end procedure;
procedure req (e : integer) is
begin
req_valid <= '1'; rel_valid <= '0'; eot <= '0';
req_ep <= std_logic_vector(to_unsigned(e, 2));
wait for 0 ns;
step;
req_valid <= '0';
end procedure;
procedure rel (e : integer) is
begin
req_valid <= '0'; rel_valid <= '1'; eot <= '0';
rel_ep <= std_logic_vector(to_unsigned(e, 2));
wait for 0 ns;
step;
rel_valid <= '0';
end procedure;
procedure nop is
begin
req_valid <= '0'; rel_valid <= '0'; eot <= '0';
wait for 0 ns;
step;
end procedure;
-- ---- A release and a request in the SAME cycle. ----
--
-- The common case on a busy controller, and the one that distinguishes
-- an allocator which applies the release first from one which does not.
-- The first version of this testbench drove it exactly once, in a
-- directed phase, and the mutation that reverses the order scored 11 --
-- correct, and one reordered phase from zero.
procedure reqrel (e_req, e_rel : integer) is
begin
req_valid <= '1'; rel_valid <= '1'; eot <= '0';
req_ep <= std_logic_vector(to_unsigned(e_req, 2));
rel_ep <= std_logic_vector(to_unsigned(e_rel, 2));
wait for 0 ns;
step;
req_valid <= '0'; rel_valid <= '0';
end procedure;
procedure reset_all (m : std_logic) is
begin
req_valid <= '0'; rel_valid <= '0'; eot <= '0';
rst_n <= '0';
wait until rising_edge(clk);
wait for 1 ns;
rst_n <= '1';
mode <= m;
m_held := (others => '0'); m_pend := (others => '0');
m_age := (others => (others => '0'));
m_wait := (others => (others => '0'));
m_worst := (others => '0');
m_gr := '0'; m_bl := '0'; m_er := '0';
m_gep := "00"; m_bep := "00"; m_hep := "00"; m_ec := E_NONE;
c_req := 0; c_grant := 0; c_block := 0;
c_rel := 0; c_dbl := 0; c_unh := 0;
wait for 1 ns;
end procedure;
begin
wait until rising_edge(clk);
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait for 1 ns;
-- ================= PHASE 1 -- the exhaustive (mode, state, op) sweep ==
--
-- Each holding state is reached BY REQUESTING, never by forcing: a state
-- forced into the design is a state the design never proved it can reach
-- (chapter 22.4).
for md in 0 to 1 loop
for st in 0 to 15 loop
for op in 0 to 8 loop
if md = 1 and popc_i(st) > N_BUF then
-- UNREACHABLE BY CONSTRUCTION, and declared rather than silently
-- missing: the shared pool has N_BUF buffers, so any state with
-- more holders than that cannot occur. Five of the sixteen
-- states, nine operations each.
n_skipped := n_skipped + 1;
else
if md = 1 then reset_all(M_SHARED); else reset_all(M_DEDICATED); end if;
for b in 0 to N_EP-1 loop
if ((st / (2**b)) mod 2) = 1 then req(b); end if;
end loop;
if op < 4 then req(op);
elsif op < 8 then rel(op - 4);
else nop;
end if;
end if;
end loop;
end loop;
end loop;
if n_skipped /= 45 then
errors := errors + 1;
write(ln, string'("FAIL skipped ") & integer'image(n_skipped)
& " unreachable combinations, expected 45");
writeline(output, ln);
end if;
-- ================= PHASE 2 -- DEDICATED never blocks ==================
reset_all(M_DEDICATED);
base_block := c_block;
for i in 0 to 39 loop
for b in 0 to N_EP-1 loop req(b); end loop;
for b in 0 to N_EP-1 loop rel(b); end loop;
end loop;
if c_block /= base_block then
errors := errors + 1;
write(ln, string'("FAIL DEDICATED blocked")); writeline(output, ln);
end if;
if to_integer(ram_bufs_s) /= N_EP then
errors := errors + 1;
write(ln, string'("FAIL DEDICATED buffer count")); writeline(output, ln);
end if;
-- ================= PHASE 3 -- SHARED is IDENTICAL below the knee ======
--
-- Three endpoints and three buffers: the pool is never exhausted, so a
-- shared pool behaves exactly like a dedicated one. This is why the
-- problem is never seen in testing.
reset_all(M_SHARED);
base_block := c_block; base_grant := c_grant;
for i in 0 to 39 loop
for b in 0 to N_BUF-1 loop req(b); end loop;
for b in 0 to N_BUF-1 loop rel(b); end loop;
end loop;
if c_block /= base_block then
errors := errors + 1;
write(ln, string'("FAIL SHARED blocked below the knee"));
writeline(output, ln);
end if;
if c_grant - base_grant /= 40 * N_BUF then
errors := errors + 1;
write(ln, string'("FAIL SHARED grants below the knee ")
& integer'image(c_grant - base_grant));
writeline(output, ln);
end if;
-- ================= PHASE 4 -- one endpoint past the knee ==============
reset_all(M_SHARED);
req(0);
nop; nop; nop;
req(1);
base_block := c_block;
req(3);
if c_block /= base_block + 1 then
errors := errors + 1;
write(ln, string'("FAIL the fourth request did not block"));
writeline(output, ln);
end if;
if block_ep_s /= "11" then
errors := errors + 1;
write(ln, string'("FAIL block_ep")); writeline(output, ln);
end if;
-- ---- THE point of the block. ----
if holder_ep_s /= "00" then
errors := errors + 1;
write(ln, string'("FAIL holder_ep is ")
& integer'image(to_integer(unsigned(holder_ep_s)))
& ", expected 0 (the oldest holder)");
writeline(output, ln);
end if;
if err_pulse_s /= '0' then
errors := errors + 1;
write(ln, string'("FAIL blocking raised an error: it is latency"));
writeline(output, ln);
end if;
rel(1);
if grant_s /= '1' or grant_ep_s /= "11" then
errors := errors + 1;
write(ln, string'("FAIL ep3 was not granted on release"));
writeline(output, ln);
end if;
-- ================= PHASE 5 -- fairness, over EVERY pair ===============
--
-- Two endpoints hold the pool; the other two wait. The one that blocked
-- FIRST must be served first, whichever two they are and whichever order
-- they blocked in.
--
-- The first version of this phase drove exactly one scenario and the
-- mutation that serves the lowest pending index instead of the longest
-- waiter died on FIVE checks. A property with one instance is a property
-- that is nearly untested, so every choice of two holders and both orders
-- of the two waiters is driven: 6 x 2 = 12 scenarios.
n_fair := 0;
for h0 in 0 to N_EP-1 loop
for h1 in h0+1 to N_EP-1 loop
for op in 0 to 1 loop
w0 := -1; w1 := -1;
for b in 0 to N_EP-1 loop
if b /= h0 and b /= h1 then
if w0 < 0 then w0 := b; else w1 := b; end if;
end if;
end loop;
-- `op` picks which of them blocks first, so the longest waiter is
-- sometimes the lower index and sometimes the higher one. An
-- arbiter that serves the lowest index looks correct in half of
-- these and only in half.
if op = 1 then hb := w0; w0 := w1; w1 := hb; end if;
reset_all(M_SHARED);
req(h0); req(h1);
req(w0);
nop; nop; nop;
req(w1);
nop;
if to_integer(unsigned(pending_s)) /= (2**w0 + 2**w1) then
errors := errors + 1;
write(ln, string'("FAIL wrong endpoints waiting"));
writeline(output, ln);
end if;
rel(h0);
if grant_s /= '1'
or grant_ep_s /= std_logic_vector(to_unsigned(w0, 2)) then
errors := errors + 1;
write(ln, string'("FAIL the longest waiter was not served"));
writeline(output, ln);
end if;
rel(h1);
if grant_s /= '1'
or grant_ep_s /= std_logic_vector(to_unsigned(w1, 2)) then
errors := errors + 1;
write(ln, string'("FAIL the second waiter was not served"));
writeline(output, ln);
end if;
n_fair := n_fair + 1;
end loop;
end loop;
end loop;
if n_fair /= 12 then
errors := errors + 1;
write(ln, string'("FAIL fairness scenarios ") & integer'image(n_fair));
writeline(output, ln);
end if;
-- ================= PHASE 5b -- release + request, EVERY pair =========
--
-- At capacity, in one cycle, on different endpoints. This is the common
-- case on a busy controller and it is the one that distinguishes an
-- allocator which applies the release first from one which does not.
n_same := 0;
for h0 in 0 to N_EP-1 loop
for h1 in h0+1 to N_EP-1 loop
for op in 0 to 1 loop
for iq in 0 to 1 loop
w0 := -1; w1 := -1;
for b in 0 to N_EP-1 loop
if b /= h0 and b /= h1 then
if w0 < 0 then w0 := b; else w1 := b; end if;
end if;
end loop;
reset_all(M_SHARED);
req(h0); req(h1);
base_block := c_block;
if iq = 0 then hb := w0; else hb := w1; end if;
if op = 0 then reqrel(hb, h0); else reqrel(hb, h1); end if;
if c_block /= base_block then
errors := errors + 1;
write(ln, string'("FAIL same-cycle release+request blocked"));
writeline(output, ln);
end if;
if grant_s /= '1' then
errors := errors + 1;
write(ln, string'("FAIL same-cycle release+request not granted"));
writeline(output, ln);
end if;
n_same := n_same + 1;
end loop;
end loop;
end loop;
end loop;
if n_same /= 24 then
errors := errors + 1;
write(ln, string'("FAIL same-cycle scenarios ") & integer'image(n_same));
writeline(output, ln);
end if;
-- ================= PHASE 6 -- the two firmware bugs ===================
reset_all(M_SHARED);
req(0);
bb := c_dbl;
req(0);
if c_dbl /= bb + 1 then
errors := errors + 1;
write(ln, string'("FAIL a double request was not reported"));
writeline(output, ln);
end if;
bb := c_unh;
rel(2);
if c_unh /= bb + 1 then
errors := errors + 1;
write(ln, string'("FAIL an unheld release was not reported"));
writeline(output, ln);
end if;
-- ...and a release-then-request on the SAME endpoint in one cycle is
-- legal, not a double request.
reset_all(M_SHARED);
req(1);
bb := c_dbl;
req_valid <= '1'; rel_valid <= '1'; eot <= '0';
req_ep <= "01"; rel_ep <= "01";
wait for 0 ns;
step;
req_valid <= '0'; rel_valid <= '0';
if c_dbl /= bb then
errors := errors + 1;
write(ln, string'("FAIL release+request on one endpoint was a double"));
writeline(output, ln);
end if;
-- ================= PHASE 7 -- random, in both architectures ===========
--
-- The stimulus is BIASED, and it has to be. Drawing an endpoint
-- uniformly for a request hits one that is already holding about half
-- the time, so a uniform run spends its cycles on double-request and
-- unheld-release reports and almost never fills the pool.
for md in 1 downto 0 loop
if md = 1 then reset_all(M_SHARED); else reset_all(M_DEDICATED); end if;
base_block := c_block; base_grant := c_grant;
for i in 0 to 19999 loop
w_v := rnd_nat mod 100;
if w_v < 48 then
bb := rnd_nat mod N_EP;
if w_v < 45 and m_held(bb) = '1' then
for k in 0 to N_EP-1 loop
if m_held(k) = '0' then bb := k; end if;
end loop;
end if;
req(bb);
elsif w_v < 82 then
bb := rnd_nat mod N_EP;
if w_v < 79 and m_held(bb) = '0' then
for k in 0 to N_EP-1 loop
if m_held(k) = '1' then bb := k; end if;
end loop;
end if;
rel(bb);
elsif w_v < 94 then
-- BOTH in one cycle: a holder hands a buffer back while a
-- non-holder asks for one.
bb := 0; hb := 1;
for k in 0 to N_EP-1 loop
if m_held(k) = '1' then bb := k; end if;
if m_held(k) = '0' then hb := k; end if;
end loop;
reqrel(hb, bb);
else
nop;
end if;
end loop;
if md = 1 then
write(ln, string'("PHASE7 SHARED : grants=")
& integer'image(c_grant - base_grant)
& " blocks=" & integer'image(c_block - base_block)
& " worst_wait=" & integer'image(to_integer(worst_wait_s)));
else
write(ln, string'("PHASE7 DEDICATED: grants=")
& integer'image(c_grant - base_grant)
& " blocks=" & integer'image(c_block - base_block)
& " worst_wait=" & integer'image(to_integer(worst_wait_s)));
end if;
writeline(output, ln);
end loop;
-- ================= the exhaustiveness proof ==========================
n_reach := 0; n_unreach := 0;
for k in 0 to 287 loop
if k >= 144 and popc_i((k mod 144) / 9) > N_BUF then
n_unreach := n_unreach + 1;
-- A declaration is a claim, and a claim that is falsified must fire.
if reach(k) = 1 then
errors := errors + 1;
write(ln, string'("FAIL a declared-unreachable combination OCCURRED"));
writeline(output, ln);
end if;
else
n_reach := n_reach + reach(k);
end if;
end loop;
if n_reach /= 243 then
errors := errors + 1;
write(ln, string'("FAIL reachable (mode,state,op) ")
& integer'image(n_reach) & "/243");
writeline(output, ln);
for k in 0 to 287 loop
if reach(k) = 0
and not (k >= 144 and popc_i((k mod 144) / 9) > N_BUF) then
write(ln, string'(" unreached mode=") & integer'image(k / 144)
& " held=" & integer'image((k mod 144) / 9)
& " op=" & integer'image(k mod 9));
writeline(output, ln);
end if;
end loop;
end if;
write(ln, string'("steps=") & integer'image(steps)
& " checks=" & integer'image(checks)
& " reach=" & integer'image(n_reach) & "/243 (+"
& integer'image(n_unreach) & " declared unreachable) errors="
& integer'image(errors));
writeline(output, ln);
write(ln, string'("req=") & integer'image(to_integer(n_req_s))
& " grant=" & integer'image(to_integer(n_grant_s))
& " block=" & integer'image(to_integer(n_block_s))
& " release=" & integer'image(to_integer(n_rel_s))
& " double=" & integer'image(to_integer(n_dbl_s))
& " unheld=" & integer'image(to_integer(n_unh_s)));
writeline(output, ln);
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
& " checks");
else
write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
integer'image(checks) & " checks");
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture;11. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| (mode × state × op) reached | 243 / 243 | 243 / 243 | 243 / 243 |
| declared unreachable, proven absent | 45 | 45 | 45 |
| fairness scenarios | 12 / 12 | 12 / 12 | 12 / 12 |
| same-cycle release+request scenarios | 24 / 24 | 24 / 24 | 24 / 24 |
| Steps | 41359 | 41359 | 41359 |
| Checks executed | 785821 | 785821 | 785821 |
| SHARED: grants / blocks | 7976 / 6589 | 7976 / 6589 | 8339 / 3957 |
| DEDICATED: grants / blocks | 8348 / 0 | 8348 / 0 | 8671 / 0 |
| worst wait, shared (cycles) | 29 | 29 | 17 |
| double requests / unheld releases | 3652 / 845 | 3652 / 845 | 2067 / 1879 |
| Result | PASS | PASS | PASS |
The two rows in bold are the chapter, and they are the same stimulus: one architecture blocks thousands of times and the other never does.
12. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| V6 | the double-request check is dropped | 269652 | 269652 | 262930 |
| V1 | the architecture collapses: DEDICATED uses the pool | 247164 | 247164 | 231102 |
| V7 | the capacity check itself is dropped | 193156 | 193156 | 164118 |
| V4 | a blocked request is not remembered | 162700 | 162700 | 145997 |
| V3 | the free count is read before the release applies | 158899 | 158899 | 148823 |
| V5 | the arbiter serves the lowest index, not the longest waiter | 127885 | 127885 | 124077 |
| V2 | the block report names the requester, not the holder | 6627 | 6627 | 3995 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages.
V2 is two orders of magnitude below the rest, and that is structural rather than weak. holder_ep is a one-cycle output that only means anything on a blocking cycle, so the number of opportunities to catch a wrong value is the number of blocks — not the number of cycles. Divide V2 by the block count and it is about one check per block, which is exactly right. This is chapter 24.2's rule: divide a suspicious column by the count of the events it depends on before concluding anything about it.
V7 is the only one of the seven that is not a latency bug. Dropping the capacity check hands the same RAM to two endpoints, which is data corruption. It is caught by the structural invariant rather than by any behavioural check, and that invariant — popcount(held) ≤ capacity — is three lines in the testbench and the only thing standing between this block and silent corruption.
Directed against random
| # | All phases | Directed only | Random |
|---|---|---|---|
| V1 | 247164 | 4565 | 242599 |
| V2 | 6627 | 38 | 6589 |
| V3 | 158899 | 264 | 158635 |
| V4 | 162700 | 363 | 162337 |
| V5 | 127885 | 48 | 127837 |
| V6 | 269652 | 321 | 269331 |
| V7 | 193156 | 935 | 192221 |
Every mutation is killed by directed stimulus alone, and the balance here is the opposite of Module 25's: there, directed stimulus did most of the killing; here the random phase does. That is a property of the block rather than of the testbench — an allocator's interesting behaviour is a history, and 20,000 biased operations generate far more distinct histories than any hand-written phase.
13. Two Survivors, and What Each One Was
V3 and V5 both scored 0 in all three languages in the first version of this suite. Neither was a check gap and neither had the same cause.
V5 was unreachable by parameterisation. The pool had three buffers and there are four endpoints, so an endpoint can only wait while all three buffers are held — which leaves exactly one endpoint free to wait. With one waiter, "serve the longest waiter" and "serve the lowest index" are the same policy, and no stimulus can tell them apart.
V3 was a stimulus gap. The mutation reads the free count before the release applies, so it only matters on a cycle carrying both a release and a request, at capacity. The testbench drove that combination exactly once, in a directed phase — and the reqrel task did not exist, so the random phase never produced it either.
And after both were fixed, the directed contributions were still thin: V5 died on 5 directed checks and V3 on 11. A property with one directed instance is a property that is nearly untested, so both scenarios were made exhaustive over endpoint pairs — every choice of two holders and both orders of the two waiters, 12 scenarios for fairness and 24 for the same-cycle case. V5 went from 5 to 48, V3 from 11 to 264.
14. Debugging Walkthrough: The Endpoint That Is Slow Because Another One Is Idle
The report. A composite device — mass storage plus a CDC serial port — has a bulk IN endpoint whose latency is fine at 99th percentile and terrible at 99.9th. No errors anywhere. The customer's application occasionally times out.
Step 1 — is it the link? No. Attempt-level error rate, per chapter 25.4, is 0.02%. The retry budget is barely used.
Step 2 — is it the host? No. The host is polling on schedule; the analyser shows the tokens arriving when expected and the device NAKing.
Step 3 — so the device has no data ready. Why? The firmware says it queued the data. Instrument the controller's buffer allocator, and the queued transfer is waiting for a buffer, not for data.
Step 4 — who has the buffers? The CDC serial port's IN endpoint. It is completely idle — the host is not polling it, because nothing has the serial port open — and it is holding a buffer it claimed at configuration time and has never released.
Step 5 — the controller reported the problem on the storage endpoint. Which is the endpoint that is behaving perfectly. Six weeks were spent reading the storage firmware.
Step 6 — why the 99.9th percentile and not the 99th? Because the pool has enough buffers for the common case. Blocking only happens when the third and fourth endpoints want one at the same moment, which is rare — and this is the cliff from section 3: below it there is nothing to see, and above it the tail is unbounded.
The fix. One line of CDC firmware: release the buffer when the interface is not in use. The storage endpoint's tail disappeared.
15. UVM: Modelling the Architecture, Not the Register Map
// The point of a UVM model of a controller is NOT to reproduce its register
// map. A register model is generated from the same spreadsheet the RTL came
// from, so it agrees with the design by construction and proves nothing.
//
// What is worth modelling is the thing the register map does not describe:
// which architecture the controller is, because that is what changes the
// behaviour a driver has to cope with.
class usb_ctrl_cfg extends uvm_object;
`uvm_object_utils(usb_ctrl_cfg)
typedef enum { CTRL_MUSB, CTRL_DWC2, CTRL_XHCI } family_e;
rand family_e family;
rand int unsigned n_ep;
rand int unsigned n_buf; // MEANINGFUL only for a shared pool
function new(string name = "usb_ctrl_cfg"); super.new(name); endfunction
// ---- The architecture follows from the family, and the pool size from
// ---- the architecture.
//
// Writing `n_buf` as an independent random field would generate
// configurations that do not exist: a dedicated controller with three
// buffers for four endpoints is not a thing you can buy.
constraint c_family {
n_ep inside {[2:8]};
(family == CTRL_MUSB) -> n_buf == n_ep; // dedicated
(family != CTRL_MUSB) -> n_buf inside {[2:n_ep]};
}
function bit is_shared(); return family != CTRL_MUSB; endfunction
endclass
// ---------------------------------------------------------------------
// The buffer-pool scoreboard. A SUBSCRIBER on the allocator's analysis
// port, and the one check it exists for is the one no error counter in the
// device will ever produce: that a transfer was late because of a DIFFERENT
// endpoint.
// ---------------------------------------------------------------------
class usb_pool_sb extends uvm_subscriber #(usb_pool_item);
`uvm_component_utils(usb_pool_sb)
usb_ctrl_cfg cfg;
// Per-endpoint state, in an associative array. Same structural argument as
// chapter 25.3's per-endpoint NAK runs: the only place state lives is
// behind an endpoint key, so "one counter for the whole device" is not
// something you can write here by accident.
typedef struct {
bit held;
time since; // when it took the buffer
int unsigned blocks; // how often IT was blocked
int unsigned blamed; // how often it was the HOLDER when someone else
} ep_pool_t; // blocked -- the number that matters
ep_pool_t p [int];
int unsigned n_grant, n_block, held_now;
time worst_wait;
function new(string name, uvm_component parent); super.new(name, parent);
endfunction
function void write(usb_pool_item t);
if (!p.exists(t.ep)) p[t.ep] = '{0, 0, 0, 0};
case (t.kind)
POOL_GRANT: begin
// The structural invariant, checked here rather than trusted: the
// pool cannot hand out more buffers than it has. A latency bug is
// annoying; this one is two endpoints writing the same RAM.
held_now++;
if (held_now > cfg.n_buf)
`uvm_fatal("POOL/OVERSUB",
$sformatf("%0d buffers held from a pool of %0d", held_now, cfg.n_buf))
p[t.ep].held = 1;
p[t.ep].since = $time;
n_grant++;
end
POOL_RELEASE: begin
if (!p[t.ep].held)
`uvm_error("POOL/UNHELD",
$sformatf("ep %0d released a buffer it does not hold", t.ep))
else held_now--;
p[t.ep].held = 0;
end
POOL_BLOCK: begin
p[t.ep].blocks++;
n_block++;
// ---- THE check. ----
//
// Blame the HOLDER, not the endpoint that asked. The oldest holder is
// computed here rather than taken from the item, so a design that
// reports the requester is caught rather than believed.
begin
int oldest = -1;
time best = 0;
foreach (p[e])
if (p[e].held && (oldest < 0 || p[e].since < best)) begin
best = p[e].since; oldest = e;
end
if (oldest >= 0) begin
p[oldest].blamed++;
if (t.holder != oldest)
`uvm_error("POOL/WRONG_BLAME",
$sformatf("ep %0d blocked and the design blamed ep %0d; the oldest holder is ep %0d",
t.ep, t.holder, oldest))
end
end
end
endcase
endfunction
// ---- A DEDICATED controller that blocks is not a dedicated controller. ----
//
// Checked at the end rather than per transaction, because it is a claim
// about the whole run: with a buffer per endpoint there is no arrangement
// of requests that can block.
function void check_phase(uvm_phase phase);
super.check_phase(phase);
if (!cfg.is_shared() && n_block > 0)
`uvm_error("POOL/DEDICATED_BLOCKED",
$sformatf("a dedicated controller blocked %0d times: the architecture is not what the configuration says",
n_block))
endfunction
function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("POOL",
$sformatf("%s: %0d buffers for %0d endpoints | %0d grants, %0d blocks",
cfg.family.name(), cfg.n_buf, cfg.n_ep, n_grant, n_block),
UVM_LOW)
// The list that ends the investigation in section 14: who was blamed,
// ranked. The endpoint at the top is the one to go and read.
foreach (p[e])
if (p[e].blamed > 0)
`uvm_info("POOL/BLAME",
$sformatf("ep %0d was the oldest holder for %0d of the %0d blocks",
e, p[e].blamed, n_block), UVM_LOW)
endfunction
endclass16. Common Misconceptions
"Porting between controllers is about the register map." The register map is a week of mechanical work. The buffer architecture is the part that changes the behaviour.
"A shared pool is strictly better; it uses less RAM." It uses less RAM and introduces head-of-line blocking. Both are true, and only one of them is in the datasheet.
"If there were a problem, an error counter would show it." Nothing is lost and nothing errors. The symptom is a latency tail.
"The problem scales with load." Below the knee the two architectures are bit-for-bit identical. Above it the tail is unbounded. There is no ramp.
"The controller reported endpoint 3, so endpoint 3 is broken." Endpoint 3 is the one that could not proceed. The bug belongs to whoever is holding a buffer.
"Testing on the simple product covers the complex one." A device with fewer endpoints than buffers can never block. The architecture ships untested.
"Buffers get released eventually." Only if firmware releases them. An idle endpoint holding one holds it for ever.
"Fairness among waiters is an optimisation." Serving the lowest index starves the high endpoints for as long as the low ones keep asking.
"A mutation that scores zero means the testbench missed it." V5 scored zero because the pool size made the property unfalsifiable, and V3 because the stimulus never produced the cycle it needs.
17. Exercises
1. A controller has 6 endpoints and a pool of 4. Work out the smallest number of simultaneously-active endpoints that can cause blocking, and say what that implies about which products expose it.
2. V2 scores 6627 against V6's 269652. Explain the ratio in terms of what holder_ep is, without referring to the strength of either check.
3. The exhaustive sweep declares 45 of 288 combinations unreachable. Derive the 45 from N_EP and N_BUF, then give the number for a pool of three.
4. With N_BUF = 3 and N_EP = 4, prove that at most one endpoint can be waiting — and hence that the fairness rule is unfalsifiable.
5. The design applies a release before a request in the same cycle. Construct the sequence where the other order costs a cycle, and say why the block counter is the only place it shows.
6. An endpoint holds a buffer for 4,000 cycles. Say which of the block's outputs makes that visible and which do not, and add the one you would want.
7. Extend the block to a reservation scheme: every endpoint is guaranteed one buffer and the rest are shared. Say what it costs in RAM and which of this chapter's failures it removes.
18. Summary
| Idea | Why it matters |
|---|---|
| Register maps differ mechanically | a week of work, and a careful engineer gets it right |
| Buffer architecture differs architecturally | it changes the behaviour and is not in the map |
| DEDICATED wastes RAM | and the waste is bounded and known |
| SHARED saves RAM | and introduces head-of-line blocking |
| A shared pool fails by latency | nothing is lost, nothing errors, every counter reads zero |
| Below the knee the two are identical | so every measurement in testing says it is fine |
| It scales the wrong way | more endpoints hide the fault better |
| The blocked endpoint is not at fault | report the holder, or the investigation starts in the wrong file |
| Apply a release before a request | or every collision costs a cycle nobody counts |
| A refused endpoint stays pending | a one-shot request makes the pool look better than it is |
| Serve the longest waiter | lowest-index starves the high endpoints |
| Declare the unreachable set | 243/243 with 45 declared beats 243/288 with a footnote |
| One instance of a property is nearly untested | V5 died on 5 checks until the scenario was made exhaustive |
| A parameter can make a property unfalsifiable | N_BUF = 3 with 4 endpoints permits only one waiter |
| 243/243 situations, 12 + 24 scenarios | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o fp_v.out fp_v.v fp_v_tb.v && ./fp_v.out |
| SystemVerilog | iverilog -g2012 -o fp_sv.out fp_sv.sv fp_sv_tb.sv && ./fp_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a fp_vhdl.vhd fp_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_fp_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_fp_vhdl |
| One mutation | iverilog -g2005 -DMUT_V5 -o mm fp_v_mut.v fp_v_tb.v && ./mm |
| Directed only | iverilog -g2005 -DDIRECTED_ONLY -o mm fp_v_mut.v fp_v_tb.v && ./mm |
All three implementations pass with 0 errors: all 243 reachable (architecture × holding state × operation) combinations driven on the wire, 45 declared unreachable and proven never to occur, fairness checked over all 12 waiter orderings and the same-cycle case over all 24, 785821 checks against an independently written shadow model, and every one of the seven mutations killed by directed stimulus alone.
Chapter 26.2 — DMA Integration follows the data instead of the buffer. Its central problem is arithmetic rather than architecture: a descriptor has a byte count and the wire has packets, and the rule that converts one into the other is not ceil(length / packet_size). The version that is has a single failure mode — it hangs on exactly the buffer sizes everybody uses, because everybody rounds buffers up to a power of two.
Continue learning
Related tutorials
- Related topic
xHCI Overview
xHCI shares a ring between software and hardware using one Cycle bit per TRB and no pointer exchange at all — and an unowned entry holds the previous lap's complete, plausible descriptor.
- Related topic
Host Resource Management
Software cannot edit a context hardware owns, so xHCI has two of them — and inside a Configure Endpoint command the Drop flags are applied before the Add flags, making drop+add an atomic re-initialisation.
- Related topic
DMA Integration
A descriptor has a byte count and the wire has packets, and the rule that converts one to the other is not ceil(length / packet size) — the version that is hangs on exactly the buffer sizes everybody uses.
- Related topic
AXI Interfaces to USB
AXI forbids a burst from crossing a 4 KB address boundary, and the reason this bug reaches production is that many interconnects quietly split the burst for you — so the engine that breaks the rule works on the board you develop on and corrupts memory on the board you ship.
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.
