USB · Module 25
Enumeration Failures
A failing enumeration retries from the beginning, so the current step is always ATTACH and tells you nothing — the furthest step ever reached is the diagnosis, and a bus reset must not clear it.
Module 24 built the components that decide whether something is wrong. This module is about the harder question that follows: what, specifically, is wrong — starting with the failure that accounts for more USB debugging time than any other.
1. Enumeration Is a Sequence
1 ATTACH VBUS appears
2 RESET the host drives SE0
3 GET_DESC_8 GET_DESCRIPTOR(Device, 8 bytes) at address 0
4 RESET_2 the host resets again
5 SET_ADDRESS the device gets a unique address
6 GET_DESC_DEV the full device descriptor
7 GET_DESC_C9 GET_DESCRIPTOR(Config, 9 bytes)
8 GET_DESC_CFG the full configuration descriptor
9 SET_CONFIG SET_CONFIGURATION
10 ENUMERATEDEvery one of those steps fails for a different reason, and knowing which one is most of the debug.
"The device does not enumerate" is not a bug report. "It never gets past SET_ADDRESS" is.
The enumeration handshake, step by step
2. And Here Is Why the Obvious Instrumentation Tells You Nothing
When enumeration fails, the host does not stop. It resets the bus and tries again, from step 1. It does that three times, and then it gives up and reports "device not recognised".
So a tracker that records the current step — which is the obvious thing to build, and what a state machine naturally gives you — reads ATTACH at the end of every failed enumeration, no matter where the failure was.
3. Retries Are Per Step, Not Global
A device that fails at step 5 three times has a different bug from one that fails at 5, then at 9, then at 5 again.
| Pattern | What it means | What you reach for next |
|---|---|---|
| fails at step 5, every time | deterministic — firmware or descriptor | a debugger and the source |
| fails at 5, then 9, then 5 | marginal — timing, power, signal integrity | a scope and a power supply |
A single global retry counter loses that distinction entirely, and it is the distinction that decides which instrument you pick up.
4. A Skipped Step Is as Diagnostic as a Failed One
Hosts do not skip steps by accident. If step 7 arrives when step 5 was the last one seen, the host has abandoned something — usually because a descriptor it read earlier told it not to bother.
That is a finding, and it points upstream of the step that appears to be missing.
5. And a Step That Never Completes Is Not a Step That Failed
a STEP FAILURE the device answered, and said no
-> firmware is running and has an opinion
a TIMEOUT the device did not answer at all
-> firmware is not running, or is stuck
Nothing else in the tracker fires when NOTHING happens.This is the same argument Chapter 24.1 makes about its own timeout, and it reappears in every diagnostic block in this module: a failure is an answer; silence is not, and they have different causes.
6. What We Are Building
usb_enum_tracker #(N_STEP = 10, RETRY_MAX = 3, STEP_TO = 12)
inputs outputs
------ -------
step_valid / step_id cur_step where it is NOW
step_ok max_step the furthest EVER -- the one
bus_reset a retry number that survives the retries
detach a new device fail_step / fail_reason
bus_idle retries_at_max
complete
n_steps n_resets n_detach n_step_fail n_timeout
n_skip n_regress n_exhausted n_badstep n_complete7. Verilog-2005 Implementation
// usb_enum_tracker -- why a failing enumeration tells you nothing, and the
// one change to the instrumentation that makes it tell you everything.
//
// ENUMERATION IS A SEQUENCE, AND THE FAILING STEP IS THE DIAGNOSIS
//
// 1 ATTACH VBUS appears
// 2 RESET the host drives SE0
// 3 GET_DESC_8 GET_DESCRIPTOR(Device, 8 bytes) at address 0
// 4 RESET_2 the host resets again
// 5 SET_ADDRESS the device gets a unique address
// 6 GET_DESC_DEV the full device descriptor
// 7 GET_DESC_C9 GET_DESCRIPTOR(Config, 9 bytes)
// 8 GET_DESC_CFG the full configuration descriptor
// 9 SET_CONFIG SET_CONFIGURATION
// 10 ENUMERATED
//
// Every one of those steps fails for a different reason, and knowing WHICH
// one is most of the debug. "The device does not enumerate" is not a bug
// report; "it never gets past SET_ADDRESS" is.
//
// AND HERE IS WHY THE OBVIOUS INSTRUMENTATION TELLS YOU NOTHING
//
// When enumeration fails, the host does not stop. It RESETS THE BUS and
// tries again, from step 1. It does that three times, and then it gives up
// and reports "device not recognised".
//
// So a tracker that records the CURRENT step -- which is the obvious thing
// to build, and what a state machine naturally gives you -- reads ATTACH at
// the end of every failed enumeration, no matter where the failure was. The
// state at the end is not the failure; it is the third retry's first step.
//
// RECORD THE FURTHEST STEP EVER REACHED, AND DO NOT
// LET A BUS RESET CLEAR IT.
//
// That one register is the difference between "the device does not work"
// and "it fails at step 6, three times, and step 6 is the full device
// descriptor". Those are the same bug and two completely different days.
//
// RETRIES ARE PER STEP, NOT GLOBAL
//
// A device that fails at step 5 three times has a different bug from one
// that fails at 5, then at 9, then at 5 again. The first is deterministic
// and cheap to find; the second is marginal, and probably timing or power.
// A single global retry counter loses that distinction entirely, and it is
// the distinction that decides which instrument you pick up next.
//
// A SKIPPED STEP IS AS DIAGNOSTIC AS A FAILED ONE
//
// The host does not skip steps by accident. If step 7 arrives when step 5
// was the last one seen, the host has abandoned something -- usually
// because a descriptor it read earlier told it not to bother. That is a
// finding, and it points at the descriptor rather than at the step that
// appears to be missing.
//
// AND A STEP THAT NEVER COMPLETES IS NOT A STEP THAT FAILED
//
// A failure is an answer. Silence is not. They have different causes --
// a STALL is firmware saying no, a timeout is firmware not running -- and
// nothing else in this block fires when nothing happens, which is the same
// argument the protocol checker of chapter 24.1 makes about its own
// timeout.
module usb_enum_tracker #(
parameter integer N_STEP = 10, // steps in a complete enumeration
parameter integer RETRY_MAX = 3, // attempts at one step before giving up
parameter integer STEP_TO = 12 // idle cycles before a step is dead
) (
input wire clk,
input wire rst_n,
input wire step_valid, // a step completed, one way or the other
input wire [3:0] step_id, // 1..N_STEP
input wire step_ok, // it succeeded
input wire bus_reset, // the host reset the bus: back to step 1
input wire detach, // VBUS gone: a different device next time
input wire bus_idle, // nothing is happening on the wire
input wire eot,
output wire [3:0] cur_step, // where enumeration is NOW
output wire [3:0] max_step, // the furthest step EVER reached -- the
// one number that survives the retries
output wire [3:0] fail_step, // where it went wrong
output wire [2:0] fail_reason,
output wire [3:0] retries_at_max,
output wire [4:0] step_age,
output wire complete,
output wire event_pulse,
output reg [31:0] n_steps,
output reg [31:0] n_resets,
output reg [31:0] n_detach,
output reg [31:0] n_step_fail,
output reg [31:0] n_timeout,
output reg [31:0] n_skip,
output reg [31:0] n_regress,
output reg [31:0] n_exhausted,
output reg [31:0] n_badstep,
output reg [31:0] n_complete
);
localparam [2:0] F_NONE = 3'd0,
F_STEP_FAIL = 3'd1, // the step returned an error
F_TIMEOUT = 3'd2, // the step never returned at all
F_SKIP = 3'd3, // the host jumped past a step
F_REGRESS = 3'd4, // a step went backwards, no reset
F_EXHAUSTED = 3'd5, // RETRY_MAX attempts at one step
F_BADSTEP = 3'd6; // a step number the sequence has no
// room for -- the decoder is wrong,
// and letting it through corrupts
// the diagnosis rather than adding
// to it
reg [3:0] cur_r, max_r, fail_r;
reg [2:0] reason_r;
reg [4:0] age_r;
reg done_r, ev_r;
// Retries are counted PER STEP. One counter per step is N_STEP registers
// and it is the whole difference between "fails at 5, three times" and
// "fails three times, somewhere".
reg [3:0] retry_r [1:N_STEP];
assign cur_step = cur_r;
assign max_step = max_r;
assign fail_step = fail_r;
assign fail_reason = reason_r;
assign retries_at_max = (max_r == 4'd0) ? 4'd0 : retry_r[max_r];
assign step_age = age_r;
assign complete = done_r;
assign event_pulse = ev_r;
integer i;
reg [3:0] cur_n, max_n, fail_n;
reg [2:0] reason_n;
reg [4:0] age_n;
reg done_n, ev_n;
reg bump_r_n; // this step's retry counter advances
reg [3:0] bump_idx;
always @* begin
cur_n = cur_r;
max_n = max_r;
fail_n = fail_r;
reason_n = F_NONE;
age_n = age_r;
// ---- `complete` is STICKY, not a pulse. ----
//
// A completed enumeration must stop the timeout below from firing, and
// a one-cycle pulse cannot: the cycle after it, the tracker is sitting
// at the last step with nothing outstanding and starts counting dead
// air towards a step that does not exist.
done_n = done_r;
ev_n = 1'b0;
bump_r_n = 1'b0;
bump_idx = 4'd0;
if (eot) begin
age_n = 5'd0;
end else if (detach) begin
// ---- A DIFFERENT DEVICE. Everything goes, including the diagnosis.
//
// This is the one event that must clear max_step. Carrying a previous
// device's furthest step into the next device's enumeration produces
// a report that names a step the new device never attempted.
cur_n = 4'd0;
max_n = 4'd0;
fail_n = 4'd0;
reason_n = F_NONE;
age_n = 5'd0;
done_n = 1'b0;
end else if (bus_reset) begin
// ---- A RETRY. The sequence restarts; the DIAGNOSIS does not. ----
//
// max_step deliberately survives. It is the only thing in this block
// that does, and it is the reason the block exists.
cur_n = 4'd1;
age_n = 5'd0;
done_n = 1'b0;
// Step 1 HAS been reached -- the device is attached -- so the
// high-water mark advances to it. Without this the mark can sit
// behind the current step, which is not a state that means anything.
if (max_r < 4'd1) max_n = 4'd1;
end else if (step_valid) begin
age_n = 5'd0;
if ((step_id < 4'd1) || (step_id > N_STEP[3:0])) begin
// ---- NOT A STEP AT ALL. ----
//
// The sequence has ten steps. A trace that reports an eleventh is a
// decoder that is wrong, and the important thing is that it must
// not ADVANCE anything: a bad step number accepted as progress
// makes max_step name a step nobody ever attempted, which is worse
// than no diagnosis because it is a confident one.
ev_n = 1'b1;
reason_n = F_BADSTEP;
fail_n = step_id;
end else if (step_id > cur_r + 4'd1) begin
// ---- The host jumped past a step. ----
//
// Hosts do not do this by accident. Something the host read earlier
// told it not to bother, so the finding is upstream of the step
// that appears to be missing.
ev_n = 1'b1;
reason_n = F_SKIP;
fail_n = step_id;
cur_n = step_id;
if (step_id > max_r) max_n = step_id;
end else if (step_id <= cur_r) begin
// ---- Backwards, with no reset in between. ----
ev_n = 1'b1;
reason_n = F_REGRESS;
fail_n = step_id;
cur_n = step_id;
end else if (step_ok) begin
cur_n = step_id;
if (step_id > max_r) max_n = step_id;
if (step_id == N_STEP[3:0]) begin
done_n = 1'b1;
ev_n = 1'b1;
end
end else begin
// ---- The step returned an error. ----
//
// The step is still the furthest REACHED -- it was attempted, and
// naming it is the entire point -- so max_step advances even though
// the step failed.
ev_n = 1'b1;
reason_n = F_STEP_FAIL;
fail_n = step_id;
if (step_id > max_r) max_n = step_id;
bump_r_n = 1'b1;
bump_idx = step_id;
if (retry_r[step_id] + 4'd1 >= RETRY_MAX[3:0]) reason_n = F_EXHAUSTED;
end
end else if ((cur_r != 4'd0) && !done_r && (cur_r < N_STEP[3:0])) begin
// ---- THE LIVENESS RULE. The step never returned at all. ----
//
// A failure is an answer; silence is not, and they have different
// causes. Nothing else in this block fires when nothing happens.
if (age_r >= STEP_TO[4:0] - 5'd1) begin
ev_n = 1'b1;
reason_n = F_TIMEOUT;
fail_n = cur_r + 4'd1; // the step that was awaited
age_n = 5'd0;
bump_r_n = 1'b1;
bump_idx = cur_r + 4'd1;
if (bump_idx > max_r) max_n = bump_idx;
end else if (bus_idle) begin
age_n = age_r + 5'd1;
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cur_r <= 4'd0;
max_r <= 4'd0;
fail_r <= 4'd0;
reason_r <= F_NONE;
age_r <= 5'd0;
done_r <= 1'b0;
ev_r <= 1'b0;
for (i = 1; i <= N_STEP; i = i + 1) retry_r[i] <= 4'd0;
n_steps <= 32'd0;
n_resets <= 32'd0;
n_detach <= 32'd0;
n_step_fail <= 32'd0;
n_timeout <= 32'd0;
n_skip <= 32'd0;
n_regress <= 32'd0;
n_exhausted <= 32'd0;
n_badstep <= 32'd0;
n_complete <= 32'd0;
end else begin
cur_r <= cur_n;
max_r <= max_n;
fail_r <= fail_n;
reason_r <= reason_n;
age_r <= age_n;
done_r <= done_n;
ev_r <= ev_n;
if (detach) begin
for (i = 1; i <= N_STEP; i = i + 1) retry_r[i] <= 4'd0;
n_detach <= n_detach + 32'd1;
end else if (bump_r_n && (bump_idx >= 4'd1) && (bump_idx <= N_STEP[3:0])) begin
if (retry_r[bump_idx] != 4'hF)
retry_r[bump_idx] <= retry_r[bump_idx] + 4'd1;
end
if (step_valid && !detach && !bus_reset && !eot)
n_steps <= n_steps + 32'd1;
if (bus_reset && !detach && !eot)
n_resets <= n_resets + 32'd1;
// The RISING edge of a sticky flag, not the flag itself.
if (done_n && !done_r) n_complete <= n_complete + 32'd1;
// The per-cause counters are driven by the SAME pulse as the event,
// so they sum to it by construction (chapter 23.4).
if (ev_n) begin
case (reason_n)
F_STEP_FAIL: n_step_fail <= n_step_fail + 32'd1;
F_TIMEOUT: n_timeout <= n_timeout + 32'd1;
F_SKIP: n_skip <= n_skip + 32'd1;
F_REGRESS: n_regress <= n_regress + 32'd1;
F_EXHAUSTED: n_exhausted <= n_exhausted + 32'd1;
F_BADSTEP: n_badstep <= n_badstep + 32'd1;
default: ;
endcase
end
end
end
endmodule8. SystemVerilog Implementation
// usb_enum_tracker -- why a failing enumeration tells you nothing, and the
// one change to the instrumentation that makes it tell you everything.
//
// ENUMERATION IS A SEQUENCE, AND THE FAILING STEP IS THE DIAGNOSIS
//
// 1 ATTACH VBUS appears
// 2 RESET the host drives SE0
// 3 GET_DESC_8 GET_DESCRIPTOR(Device, 8 bytes) at address 0
// 4 RESET_2 the host resets again
// 5 SET_ADDRESS the device gets a unique address
// 6 GET_DESC_DEV the full device descriptor
// 7 GET_DESC_C9 GET_DESCRIPTOR(Config, 9 bytes)
// 8 GET_DESC_CFG the full configuration descriptor
// 9 SET_CONFIG SET_CONFIGURATION
// 10 ENUMERATED
//
// Every one of those steps fails for a different reason, and knowing WHICH
// one is most of the debug. "The device does not enumerate" is not a bug
// report; "it never gets past SET_ADDRESS" is.
//
// AND HERE IS WHY THE OBVIOUS INSTRUMENTATION TELLS YOU NOTHING
//
// When enumeration fails, the host does not stop. It RESETS THE BUS and
// tries again, from step 1. It does that three times, and then it gives up
// and reports "device not recognised".
//
// So a tracker that records the CURRENT step -- which is the obvious thing
// to build, and what a state machine naturally gives you -- reads ATTACH at
// the end of every failed enumeration, no matter where the failure was. The
// state at the end is not the failure; it is the third retry's first step.
//
// RECORD THE FURTHEST STEP EVER REACHED, AND DO NOT
// LET A BUS RESET CLEAR IT.
//
// That one register is the difference between "the device does not work"
// and "it fails at step 6, three times, and step 6 is the full device
// descriptor". Those are the same bug and two completely different days.
//
// RETRIES ARE PER STEP, NOT GLOBAL
//
// A device that fails at step 5 three times has a different bug from one
// that fails at 5, then at 9, then at 5 again. The first is deterministic
// and cheap to find; the second is marginal, and probably timing or power.
// A single global retry counter loses that distinction entirely, and it is
// the distinction that decides which instrument you pick up next.
//
// A SKIPPED STEP IS AS DIAGNOSTIC AS A FAILED ONE
//
// The host does not skip steps by accident. If step 7 arrives when step 5
// was the last one seen, the host has abandoned something -- usually
// because a descriptor it read earlier told it not to bother. That is a
// finding, and it points at the descriptor rather than at the step that
// appears to be missing.
//
// AND A STEP THAT NEVER COMPLETES IS NOT A STEP THAT FAILED
//
// A failure is an answer. Silence is not. They have different causes --
// a STALL is firmware saying no, a timeout is firmware not running -- and
// nothing else in this block fires when nothing happens, which is the same
// argument the protocol checker of chapter 24.1 makes about its own
// timeout.
package usb_enum_pkg;
// The reasons an enumeration stops making progress. They are separate
// because they send you to different places: a STEP_FAIL is firmware
// saying no, a TIMEOUT is firmware not running, a SKIP is the host having
// read something earlier that told it not to bother, and a BADSTEP is the
// decoder being wrong about the trace.
typedef enum logic [2:0] {
F_NONE = 3'd0,
F_STEP_FAIL = 3'd1, // the step returned an error
F_TIMEOUT = 3'd2, // the step never returned at all
F_SKIP = 3'd3, // the host jumped past a step
F_REGRESS = 3'd4, // a step went backwards, with no reset
F_EXHAUSTED = 3'd5, // RETRY_MAX attempts at one step
F_BADSTEP = 3'd6 // a step number the sequence has no room for
} fail_e;
endpackage
module usb_enum_tracker
import usb_enum_pkg::*;
#(
parameter int N_STEP = 10, // steps in a complete enumeration
parameter int RETRY_MAX = 3, // attempts at one step before giving up
parameter int STEP_TO = 12 // idle cycles before a step is dead
) (
input logic clk,
input logic rst_n,
input logic step_valid, // a step completed, one way or the other
input logic [3:0] step_id, // 1..N_STEP
input logic step_ok, // it succeeded
input logic bus_reset, // the host reset the bus: back to step 1
input logic detach, // VBUS gone: a different device next time
input logic bus_idle, // nothing is happening on the wire
input logic eot,
output logic [3:0] cur_step, // where enumeration is NOW
output logic [3:0] max_step, // the furthest step EVER reached -- the
// one number that survives the retries
output logic [3:0] fail_step, // where it went wrong
output fail_e fail_reason,
output logic [3:0] retries_at_max,
output logic [4:0] step_age,
output logic complete,
output logic event_pulse,
output logic [31:0] n_steps,
output logic [31:0] n_resets,
output logic [31:0] n_detach,
output logic [31:0] n_step_fail,
output logic [31:0] n_timeout,
output logic [31:0] n_skip,
output logic [31:0] n_regress,
output logic [31:0] n_exhausted,
output logic [31:0] n_badstep,
output logic [31:0] n_complete
);
logic [3:0] cur_r, max_r, fail_r;
fail_e reason_r;
logic [4:0] age_r;
logic done_r, ev_r;
// Retries are counted PER STEP. One counter per step is N_STEP registers
// and it is the whole difference between "fails at 5, three times" and
// "fails three times, somewhere".
logic [3:0] retry_r [1:N_STEP];
assign cur_step = cur_r;
assign max_step = max_r;
assign fail_step = fail_r;
assign fail_reason = reason_r;
assign retries_at_max = (max_r == 4'd0) ? 4'd0 : retry_r[max_r];
assign step_age = age_r;
assign complete = done_r;
assign event_pulse = ev_r;
int i;
logic [3:0] cur_n, max_n, fail_n;
fail_e reason_n;
logic [4:0] age_n;
logic done_n, ev_n;
logic bump_r_n; // this step's retry counter advances
logic [3:0] bump_idx;
always_comb begin
cur_n = cur_r;
max_n = max_r;
fail_n = fail_r;
reason_n = F_NONE;
age_n = age_r;
// ---- `complete` is STICKY, not a pulse. ----
//
// A completed enumeration must stop the timeout below from firing, and
// a one-cycle pulse cannot: the cycle after it, the tracker is sitting
// at the last step with nothing outstanding and starts counting dead
// air towards a step that does not exist.
done_n = done_r;
ev_n = 1'b0;
bump_r_n = 1'b0;
bump_idx = 4'd0;
if (eot) begin
age_n = 5'd0;
end else if (detach) begin
// ---- A DIFFERENT DEVICE. Everything goes, including the diagnosis.
//
// This is the one event that must clear max_step. Carrying a previous
// device's furthest step into the next device's enumeration produces
// a report that names a step the new device never attempted.
cur_n = 4'd0;
max_n = 4'd0;
fail_n = 4'd0;
reason_n = F_NONE;
age_n = 5'd0;
done_n = 1'b0;
end else if (bus_reset) begin
// ---- A RETRY. The sequence restarts; the DIAGNOSIS does not. ----
//
// max_step deliberately survives. It is the only thing in this block
// that does, and it is the reason the block exists.
cur_n = 4'd1;
age_n = 5'd0;
done_n = 1'b0;
// Step 1 HAS been reached -- the device is attached -- so the
// high-water mark advances to it. Without this the mark can sit
// behind the current step, which is not a state that means anything.
if (max_r < 4'd1) max_n = 4'd1;
end else if (step_valid) begin
age_n = 5'd0;
if ((step_id < 4'd1) || (step_id > 4'(N_STEP))) begin
// ---- NOT A STEP AT ALL. ----
//
// The sequence has ten steps. A trace that reports an eleventh is a
// decoder that is wrong, and the important thing is that it must
// not ADVANCE anything: a bad step number accepted as progress
// makes max_step name a step nobody ever attempted, which is worse
// than no diagnosis because it is a confident one.
ev_n = 1'b1;
reason_n = F_BADSTEP;
fail_n = step_id;
end else if (step_id > cur_r + 4'd1) begin
// ---- The host jumped past a step. ----
//
// Hosts do not do this by accident. Something the host read earlier
// told it not to bother, so the finding is upstream of the step
// that appears to be missing.
ev_n = 1'b1;
reason_n = F_SKIP;
fail_n = step_id;
cur_n = step_id;
if (step_id > max_r) max_n = step_id;
end else if (step_id <= cur_r) begin
// ---- Backwards, with no reset in between. ----
ev_n = 1'b1;
reason_n = F_REGRESS;
fail_n = step_id;
cur_n = step_id;
end else if (step_ok) begin
cur_n = step_id;
if (step_id > max_r) max_n = step_id;
if (step_id == 4'(N_STEP)) begin
done_n = 1'b1;
ev_n = 1'b1;
end
end else begin
// ---- The step returned an error. ----
//
// The step is still the furthest REACHED -- it was attempted, and
// naming it is the entire point -- so max_step advances even though
// the step failed.
ev_n = 1'b1;
reason_n = F_STEP_FAIL;
fail_n = step_id;
if (step_id > max_r) max_n = step_id;
bump_r_n = 1'b1;
bump_idx = step_id;
if (retry_r[step_id] + 4'd1 >= 4'(RETRY_MAX)) reason_n = F_EXHAUSTED;
end
end else if ((cur_r != 4'd0) && !done_r && (cur_r < 4'(N_STEP))) begin
// ---- THE LIVENESS RULE. The step never returned at all. ----
//
// A failure is an answer; silence is not, and they have different
// causes. Nothing else in this block fires when nothing happens.
if (age_r >= 5'(STEP_TO) - 5'd1) begin
ev_n = 1'b1;
reason_n = F_TIMEOUT;
fail_n = cur_r + 4'd1; // the step that was awaited
age_n = 5'd0;
bump_r_n = 1'b1;
bump_idx = cur_r + 4'd1;
if (bump_idx > max_r) max_n = bump_idx;
end else if (bus_idle) begin
age_n = age_r + 5'd1;
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cur_r <= 4'd0;
max_r <= 4'd0;
fail_r <= 4'd0;
reason_r <= F_NONE;
age_r <= 5'd0;
done_r <= 1'b0;
ev_r <= 1'b0;
for (i = 1; i <= N_STEP; i++) retry_r[i] <= 4'd0;
n_steps <= 32'd0;
n_resets <= 32'd0;
n_detach <= 32'd0;
n_step_fail <= 32'd0;
n_timeout <= 32'd0;
n_skip <= 32'd0;
n_regress <= 32'd0;
n_exhausted <= 32'd0;
n_badstep <= 32'd0;
n_complete <= 32'd0;
end else begin
cur_r <= cur_n;
max_r <= max_n;
fail_r <= fail_n;
reason_r <= reason_n;
age_r <= age_n;
done_r <= done_n;
ev_r <= ev_n;
if (detach) begin
for (i = 1; i <= N_STEP; i++) retry_r[i] <= 4'd0;
n_detach <= n_detach + 32'd1;
end else if (bump_r_n && (bump_idx >= 4'd1) && (bump_idx <= 4'(N_STEP))) begin
if (retry_r[bump_idx] != 4'hF)
retry_r[bump_idx] <= retry_r[bump_idx] + 4'd1;
end
if (step_valid && !detach && !bus_reset && !eot)
n_steps <= n_steps + 32'd1;
if (bus_reset && !detach && !eot)
n_resets <= n_resets + 32'd1;
// The RISING edge of a sticky flag, not the flag itself.
if (done_n && !done_r) n_complete <= n_complete + 32'd1;
// The per-cause counters are driven by the SAME pulse as the event,
// so they sum to it by construction (chapter 23.4).
if (ev_n) begin
case (reason_n)
F_STEP_FAIL: n_step_fail <= n_step_fail + 32'd1;
F_TIMEOUT: n_timeout <= n_timeout + 32'd1;
F_SKIP: n_skip <= n_skip + 32'd1;
F_REGRESS: n_regress <= n_regress + 32'd1;
F_EXHAUSTED: n_exhausted <= n_exhausted + 32'd1;
F_BADSTEP: n_badstep <= n_badstep + 32'd1;
default: ;
endcase
end
end
end
endmodule9. VHDL-2008 Implementation
-- usb_enum_tracker -- why a failing enumeration tells you nothing, and the
-- one change to the instrumentation that makes it tell you everything.
--
-- ENUMERATION IS A SEQUENCE, AND THE FAILING STEP IS THE DIAGNOSIS
--
-- 1 ATTACH VBUS appears
-- 2 RESET the host drives SE0
-- 3 GET_DESC_8 GET_DESCRIPTOR(Device, 8 bytes) at address 0
-- 4 RESET_2 the host resets again
-- 5 SET_ADDRESS the device gets a unique address
-- 6 GET_DESC_DEV the full device descriptor
-- 7 GET_DESC_C9 GET_DESCRIPTOR(Config, 9 bytes)
-- 8 GET_DESC_CFG the full configuration descriptor
-- 9 SET_CONFIG SET_CONFIGURATION
-- 10 ENUMERATED
--
-- Every one of those steps fails for a different reason, and knowing WHICH
-- one is most of the debug. "The device does not enumerate" is not a bug
-- report; "it never gets past SET_ADDRESS" is.
--
-- AND HERE IS WHY THE OBVIOUS INSTRUMENTATION TELLS YOU NOTHING
--
-- When enumeration fails, the host does not stop. It RESETS THE BUS and
-- tries again, from step 1. It does that three times, and then it gives up
-- and reports "device not recognised".
--
-- So a tracker that records the CURRENT step -- which is the obvious thing
-- to build, and what a state machine naturally gives you -- reads ATTACH at
-- the end of every failed enumeration, no matter where the failure was. The
-- state at the end is not the failure; it is the third retry's first step.
--
-- RECORD THE FURTHEST STEP EVER REACHED, AND DO NOT
-- LET A BUS RESET CLEAR IT.
--
-- That one register is the difference between "the device does not work"
-- and "it fails at step 6, three times, and step 6 is the full device
-- descriptor". Those are the same bug and two completely different days.
--
-- RETRIES ARE PER STEP, NOT GLOBAL
--
-- A device that fails at step 5 three times has a different bug from one
-- that fails at 5, then at 9, then at 5 again. The first is deterministic
-- and cheap to find; the second is marginal, and probably timing or power.
-- A single global retry counter loses that distinction entirely, and it is
-- the distinction that decides which instrument you pick up next.
--
-- A SKIPPED STEP IS AS DIAGNOSTIC AS A FAILED ONE
--
-- The host does not skip steps by accident. If step 7 arrives when step 5
-- was the last one seen, the host has abandoned something -- usually
-- because a descriptor it read earlier told it not to bother. That is a
-- finding, and it points at the descriptor rather than at the step that
-- appears to be missing.
--
-- AND A STEP THAT NEVER COMPLETES IS NOT A STEP THAT FAILED
--
-- A failure is an answer. Silence is not. They have different causes --
-- a STALL is firmware saying no, a timeout is firmware not running -- and
-- nothing else in this block fires when nothing happens, which is the same
-- argument the protocol checker of chapter 24.1 makes about its own
-- timeout.
library ieee;
use ieee.std_logic_1164.all;
package usb_enum_pkg is
-- The reasons an enumeration stops making progress. They are separate
-- because they send you to different places: a STEP_FAIL is firmware
-- saying no, a TIMEOUT is firmware not running, a SKIP is the host having
-- read something earlier that told it not to bother, and a BADSTEP is the
-- decoder being wrong about the trace.
type fail_t is (F_NONE, F_STEP_FAIL, F_TIMEOUT, F_SKIP, F_REGRESS,
F_EXHAUSTED, F_BADSTEP);
function f_code (f : fail_t) return std_logic_vector;
end package usb_enum_pkg;
package body usb_enum_pkg is
function f_code (f : fail_t) return std_logic_vector is
begin
case f is
when F_NONE => return "000";
when F_STEP_FAIL => return "001";
when F_TIMEOUT => return "010";
when F_SKIP => return "011";
when F_REGRESS => return "100";
when F_EXHAUSTED => return "101";
when F_BADSTEP => return "110";
end case;
end function;
end package body usb_enum_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_enum_pkg.all;
entity usb_enum_tracker is
generic (
N_STEP : integer := 10; -- steps in a complete enumeration
RETRY_MAX : integer := 3; -- attempts at one step before giving up
STEP_TO : integer := 12 -- idle cycles before a step is dead
);
port (
clk : in std_logic;
rst_n : in std_logic;
step_valid : in std_logic; -- a step completed
step_id : in std_logic_vector(3 downto 0); -- 1..N_STEP
step_ok : in std_logic; -- it succeeded
bus_reset : in std_logic; -- back to step 1
detach : in std_logic; -- a different device
bus_idle : in std_logic;
eot : in std_logic;
cur_step : out std_logic_vector(3 downto 0);
max_step : out std_logic_vector(3 downto 0); -- THE diagnosis
fail_step : out std_logic_vector(3 downto 0);
fail_reason : out std_logic_vector(2 downto 0);
retries_at_max : out std_logic_vector(3 downto 0);
step_age : out std_logic_vector(4 downto 0);
complete : out std_logic;
event_pulse : out std_logic;
n_steps : out std_logic_vector(31 downto 0);
n_resets : out std_logic_vector(31 downto 0);
n_detach : out std_logic_vector(31 downto 0);
n_step_fail : out std_logic_vector(31 downto 0);
n_timeout : out std_logic_vector(31 downto 0);
n_skip : out std_logic_vector(31 downto 0);
n_regress : out std_logic_vector(31 downto 0);
n_exhausted : out std_logic_vector(31 downto 0);
n_badstep : out std_logic_vector(31 downto 0);
n_complete : out std_logic_vector(31 downto 0)
);
end entity usb_enum_tracker;
architecture rtl of usb_enum_tracker is
type retry_arr is array (1 to N_STEP) of unsigned(3 downto 0);
signal cur_r, max_r, fail_r : unsigned(3 downto 0) := (others => '0');
signal reason_r : fail_t := F_NONE;
signal age_r : unsigned(4 downto 0) := (others => '0');
signal done_r, ev_r : std_logic := '0';
-- Retries are counted PER STEP. One counter per step is N_STEP registers
-- and it is the whole difference between "fails at 5, three times" and
-- "fails three times, somewhere".
signal retry_r : retry_arr := (others => (others => '0'));
-- Accumulators are held as unsigned rather than as range-constrained
-- integers: a constrained integer aborts simulation on overflow, which
-- turns a mutation into a crash instead of a measured kill.
signal c_st, c_rs, c_dt, c_sf, c_to : unsigned(31 downto 0) := (others => '0');
signal c_sk, c_rg, c_ex, c_bs, c_cp : unsigned(31 downto 0) := (others => '0');
begin
cur_step <= std_logic_vector(cur_r);
max_step <= std_logic_vector(max_r);
fail_step <= std_logic_vector(fail_r);
fail_reason <= f_code(reason_r);
retries_at_max <= (others => '0') when max_r = 0
else std_logic_vector(retry_r(to_integer(max_r)));
step_age <= std_logic_vector(age_r);
complete <= done_r;
event_pulse <= ev_r;
n_steps <= std_logic_vector(c_st);
n_resets <= std_logic_vector(c_rs);
n_detach <= std_logic_vector(c_dt);
n_step_fail <= std_logic_vector(c_sf);
n_timeout <= std_logic_vector(c_to);
n_skip <= std_logic_vector(c_sk);
n_regress <= std_logic_vector(c_rg);
n_exhausted <= std_logic_vector(c_ex);
n_badstep <= std_logic_vector(c_bs);
n_complete <= std_logic_vector(c_cp);
process (clk, rst_n)
variable ncur, nmax, nfail, bidx : unsigned(3 downto 0);
variable nrsn : fail_t;
variable nage : unsigned(4 downto 0);
variable ndone, nev, nbump : std_logic;
variable sid : unsigned(3 downto 0);
begin
if rst_n = '0' then
cur_r <= (others => '0');
max_r <= (others => '0');
fail_r <= (others => '0');
reason_r <= F_NONE;
age_r <= (others => '0');
done_r <= '0';
ev_r <= '0';
retry_r <= (others => (others => '0'));
c_st <= (others => '0'); c_rs <= (others => '0');
c_dt <= (others => '0'); c_sf <= (others => '0');
c_to <= (others => '0'); c_sk <= (others => '0');
c_rg <= (others => '0'); c_ex <= (others => '0');
c_bs <= (others => '0'); c_cp <= (others => '0');
elsif rising_edge(clk) then
sid := unsigned(step_id);
ncur := cur_r; nmax := max_r; nfail := fail_r;
nrsn := F_NONE; nage := age_r;
-- `complete` is STICKY, not a pulse: a completed enumeration must stop
-- the timeout below from firing, and a one-cycle pulse cannot.
ndone := done_r;
nev := '0'; nbump := '0'; bidx := (others => '0');
if eot = '1' then
nage := (others => '0');
elsif detach = '1' then
-- ---- A DIFFERENT DEVICE. Everything goes, including the diagnosis.
--
-- This is the one event that must clear max_step. Carrying a
-- previous device's furthest step into the next device's
-- enumeration produces a report that names a step the new device
-- never attempted.
ncur := (others => '0'); nmax := (others => '0');
nfail := (others => '0'); nrsn := F_NONE;
nage := (others => '0'); ndone := '0';
elsif bus_reset = '1' then
-- ---- A RETRY. The sequence restarts; the DIAGNOSIS does not. ----
ncur := to_unsigned(1, 4);
nage := (others => '0');
ndone := '0';
-- Step 1 HAS been reached -- the device is attached -- so the
-- high-water mark advances to it.
if max_r < 1 then nmax := to_unsigned(1, 4); end if;
elsif step_valid = '1' then
nage := (others => '0');
if sid < 1 or sid > to_unsigned(N_STEP, 4) then
-- ---- NOT A STEP AT ALL. ----
--
-- The sequence has N_STEP steps. A trace that reports one beyond
-- them is a decoder that is wrong, and the important thing is
-- that it must not ADVANCE anything: a bad step number accepted
-- as progress makes max_step name a step nobody ever attempted,
-- which is worse than no diagnosis because it is a confident one.
nev := '1'; nrsn := F_BADSTEP; nfail := sid;
elsif sid > cur_r + 1 then
-- ---- The host jumped past a step. ----
nev := '1'; nrsn := F_SKIP; nfail := sid; ncur := sid;
if sid > max_r then nmax := sid; end if;
elsif sid <= cur_r then
-- ---- Backwards, with no reset in between. ----
nev := '1'; nrsn := F_REGRESS; nfail := sid; ncur := sid;
elsif step_ok = '1' then
ncur := sid;
if sid > max_r then nmax := sid; end if;
if sid = to_unsigned(N_STEP, 4) then
ndone := '1'; nev := '1';
end if;
else
-- ---- The step returned an error. ----
--
-- The step is still the furthest REACHED -- it was attempted, and
-- naming it is the entire point -- so max_step advances even
-- though the step failed.
nev := '1'; nrsn := F_STEP_FAIL; nfail := sid;
if sid > max_r then nmax := sid; end if;
nbump := '1'; bidx := sid;
if retry_r(to_integer(sid)) + 1 >= to_unsigned(RETRY_MAX, 4) then
nrsn := F_EXHAUSTED;
end if;
end if;
elsif cur_r /= 0 and done_r = '0' and cur_r < to_unsigned(N_STEP, 4) then
-- ---- THE LIVENESS RULE. The step never returned at all. ----
--
-- A failure is an answer; silence is not, and they have different
-- causes. Nothing else in this block fires when nothing happens.
if age_r >= to_unsigned(STEP_TO - 1, 5) then
nev := '1'; nrsn := F_TIMEOUT; nfail := cur_r + 1;
nage := (others => '0');
nbump := '1'; bidx := cur_r + 1;
if bidx > max_r then nmax := bidx; end if;
elsif bus_idle = '1' then
nage := age_r + 1;
end if;
end if;
cur_r <= ncur;
max_r <= nmax;
fail_r <= nfail;
reason_r <= nrsn;
age_r <= nage;
ev_r <= nev;
if detach = '1' then
retry_r <= (others => (others => '0'));
c_dt <= c_dt + 1;
elsif nbump = '1' and bidx >= 1 and bidx <= to_unsigned(N_STEP, 4) then
if retry_r(to_integer(bidx)) /= x"F" then
retry_r(to_integer(bidx)) <= retry_r(to_integer(bidx)) + 1;
end if;
end if;
if step_valid = '1' and detach = '0' and bus_reset = '0' and eot = '0' then
c_st <= c_st + 1;
end if;
if bus_reset = '1' and detach = '0' and eot = '0' then
c_rs <= c_rs + 1;
end if;
-- The RISING edge of a sticky flag, not the flag itself.
if ndone = '1' and done_r = '0' then c_cp <= c_cp + 1; end if;
done_r <= ndone;
-- The per-cause counters are driven by the SAME pulse as the event,
-- so they sum to it by construction (chapter 23.4).
if nev = '1' then
case nrsn is
when F_STEP_FAIL => c_sf <= c_sf + 1;
when F_TIMEOUT => c_to <= c_to + 1;
when F_SKIP => c_sk <= c_sk + 1;
when F_REGRESS => c_rg <= c_rg + 1;
when F_EXHAUSTED => c_ex <= c_ex + 1;
when F_BADSTEP => c_bs <= c_bs + 1;
when others => null;
end case;
end if;
end if;
end process;
end architecture rtl;10. Seeing the Diagnosis Survive a Retry
Step 6 fails, the host resets, and the two numbers separate
usb_enum_tracker — cur_step is noise, max_step is the answer
10 cycles11. The Testbenches
The phase the chapter exists for drives every step as the failing step, with the full retry-and-reset sequence around it, and reads the diagnosis off max_step each time:
for (s = 2; s <= N_STEP; s = s + 1) begin
do_detach;
for (k = 0; k < RETRY_MAX; k = k + 1) begin
do_reset;
for (a = 2; a < s; a = a + 1) ok_step(a[3:0]);
bad_step(s[3:0]);
end
// The host gives up and resets once more before reporting.
do_reset;
check(cur_step === 4'd1,
"the tracker is not sitting at step 1 after the final reset");
check(max_step === s[3:0],
"max_step is not the step that failed -- a tracker whose furthest step is cleared by a bus reset reports step 1 for every failure, and 'the device does not get past ATTACH' sends somebody to look at VBUS");
check(retries_at_max === RETRY_MAX[3:0],
"the retry count at the failing step is wrong -- retries are counted PER STEP, and 'fails at 5 three times' is a different bug from 'fails three times, somewhere'");
endTen failures, ten different diagnoses, and the tracker must produce the right one for each — plus the invariant that makes the whole thing meaningful:
// ---- max_step is a HIGH-WATER MARK. It never goes backwards except
// ---- on a detach, and that invariant is the whole block.
check(m_max >= m_cur,
"the furthest step reached is behind the current step, which is impossible");11.1 Verilog testbench
// Testbench for usb_enum_tracker (Verilog-2005).
//
// THE TEST THAT DEFINES THE CHAPTER
//
// Enumeration fails at step 6 and the host retries the whole sequence three
// times. At the end:
//
// cur_step reads 1 -- the third retry's first step
// max_step reads 6 -- THE DIAGNOSIS
//
// The suite drives exactly that and checks both numbers, because the whole
// argument of the chapter is that one of them is useless and the other is
// the answer. A tracker whose max_step is cleared by a bus reset reports 1
// as well, and a report of "the device does not get past ATTACH" sends
// somebody to look at VBUS.
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. Every step 1..N_STEP as the FAILING step, with the full retry-and-
// reset sequence around it, checking max_step each time. Ten failures,
// ten different diagnoses, and the tracker must produce the right one
// for each.
//
// 2. Every current step 0..N_STEP crossed with all 16 combinations of
// {step_valid, step_ok, bus_reset, detach} = 176 pairs, each state
// reached by real steps.
`timescale 1ns/1ps
module tb_et_v;
localparam integer N_STEP = 10;
localparam integer RETRY_MAX = 3;
localparam integer STEP_TO = 12;
localparam [2:0] F_NONE=3'd0, F_STEP_FAIL=3'd1, F_TIMEOUT=3'd2,
F_SKIP=3'd3, F_REGRESS=3'd4, F_EXHAUSTED=3'd5,
F_BADSTEP=3'd6;
reg clk = 1'b0, rst_n = 1'b0;
reg step_valid = 1'b0, step_ok = 1'b0;
reg bus_reset = 1'b0, detach = 1'b0, bus_idle = 1'b1, eot = 1'b0;
reg [3:0] step_id = 4'd0;
wire [3:0] cur_step, max_step, fail_step, retries_at_max;
wire [2:0] fail_reason;
wire [4:0] step_age;
wire complete, event_pulse;
wire [31:0] n_steps, n_resets, n_detach, n_step_fail, n_timeout,
n_skip, n_regress, n_exhausted, n_badstep, n_complete;
usb_enum_tracker #(.N_STEP(N_STEP), .RETRY_MAX(RETRY_MAX),
.STEP_TO(STEP_TO)) dut (
.clk(clk), .rst_n(rst_n),
.step_valid(step_valid), .step_id(step_id), .step_ok(step_ok),
.bus_reset(bus_reset), .detach(detach), .bus_idle(bus_idle), .eot(eot),
.cur_step(cur_step), .max_step(max_step), .fail_step(fail_step),
.fail_reason(fail_reason), .retries_at_max(retries_at_max),
.step_age(step_age), .complete(complete), .event_pulse(event_pulse),
.n_steps(n_steps), .n_resets(n_resets), .n_detach(n_detach),
.n_step_fail(n_step_fail), .n_timeout(n_timeout), .n_skip(n_skip),
.n_regress(n_regress), .n_exhausted(n_exhausted),
.n_badstep(n_badstep), .n_complete(n_complete)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0;
task check(input cond, input [1023:0] msg);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 25)
$display("FAIL @%0t: %0s | cur=%0d max=%0d fail=%0d(%0d) rty=%0d age=%0d",
$time, msg, cur_step, max_step, fail_step, fail_reason,
retries_at_max, step_age);
end
end
endtask
// ------------------------------------------------------------------
// The shadow tracker.
// ------------------------------------------------------------------
reg [3:0] m_cur, m_max, m_fail;
reg [2:0] m_rsn;
reg [4:0] m_age;
reg m_done, m_ev;
reg [3:0] m_retry [1:N_STEP];
integer m_st, m_rs, m_dt, m_sf, m_to, m_sk, m_rg, m_ex, m_bs, m_cp;
integer seen [0:175]; // (N_STEP+1) x 16 input combinations
integer n_seen, n_steps_t;
task model_reset;
integer j;
begin
m_cur = 4'd0; m_max = 4'd0; m_fail = 4'd0; m_rsn = F_NONE;
m_age = 5'd0; m_done = 1'b0; m_ev = 1'b0;
for (j = 1; j <= N_STEP; j = j + 1) m_retry[j] = 4'd0;
m_st = 0; m_rs = 0; m_dt = 0; m_sf = 0; m_to = 0;
m_sk = 0; m_rg = 0; m_ex = 0; m_bs = 0; m_cp = 0;
for (j = 0; j < 176; j = j + 1) seen[j] = 0;
n_seen = 0; n_steps_t = 0;
end
endtask
integer idx;
task step(input sv, input [3:0] sid, input sok,
input brst, input det, input bidle, input eo);
reg [3:0] ncur, nmax, nfail, bidx;
reg [2:0] nrsn;
reg [4:0] nage;
reg ndone, nev, nbump;
begin
step_valid = sv; step_id = sid; step_ok = sok;
bus_reset = brst; detach = det; bus_idle = bidle; eot = eo;
#1;
check(cur_step === m_cur, "cur_step disagrees with the shadow tracker");
check(max_step === m_max, "max_step disagrees -- and max_step IS the diagnosis");
check(fail_step === m_fail, "fail_step disagrees");
check(fail_reason === m_rsn, "fail_reason disagrees");
check(step_age === m_age, "step_age disagrees");
check(complete === m_done, "complete disagrees");
check(event_pulse === m_ev, "the event pulse disagrees");
check(retries_at_max === ((m_max == 4'd0) ? 4'd0 : m_retry[m_max]),
"retries_at_max disagrees -- retries are counted PER STEP, and a global counter cannot produce this number");
// ---- max_step is a HIGH-WATER MARK. It never goes backwards except
// ---- on a detach, and that invariant is the whole block.
check(m_max >= m_cur,
"the furthest step reached is behind the current step, which is impossible");
check(max_step <= N_STEP[3:0],
"max_step names a step that does not exist");
check(step_age <= STEP_TO[4:0],
"the step timer ran past its bound");
check(!((m_cur == 4'd0) && (step_age != 5'd0)),
"the step timer is running with no enumeration in progress");
idx = m_cur * 16 + (sv ? 8 : 0) + (sok ? 4 : 0) + (brst ? 2 : 0) + (det ? 1 : 0);
if (idx < 176) begin
if (seen[idx] == 0) begin seen[idx] = 1; n_seen = n_seen + 1; end
end
n_steps_t = n_steps_t + 1;
// ---- advance the shadow tracker ----
ncur = m_cur; nmax = m_max; nfail = m_fail; nrsn = F_NONE;
nage = m_age; ndone = m_done; nev = 1'b0; nbump = 1'b0; bidx = 4'd0;
if (eo) begin
nage = 5'd0;
end else if (det) begin
ncur = 4'd0; nmax = 4'd0; nfail = 4'd0; nrsn = F_NONE;
nage = 5'd0; ndone = 1'b0;
end else if (brst) begin
ncur = 4'd1; nage = 5'd0; ndone = 1'b0;
if (m_max < 4'd1) nmax = 4'd1;
end else if (sv) begin
nage = 5'd0;
if ((sid < 4'd1) || (sid > N_STEP[3:0])) begin
nev = 1'b1; nrsn = F_BADSTEP; nfail = sid;
end else if (sid > m_cur + 4'd1) begin
nev = 1'b1; nrsn = F_SKIP; nfail = sid; ncur = sid;
if (sid > m_max) nmax = sid;
end else if (sid <= m_cur) begin
nev = 1'b1; nrsn = F_REGRESS; nfail = sid; ncur = sid;
end else if (sok) begin
ncur = sid;
if (sid > m_max) nmax = sid;
if (sid == N_STEP[3:0]) begin ndone = 1'b1; nev = 1'b1; end
end else begin
nev = 1'b1; nrsn = F_STEP_FAIL; nfail = sid;
if (sid > m_max) nmax = sid;
nbump = 1'b1; bidx = sid;
if (m_retry[sid] + 4'd1 >= RETRY_MAX[3:0]) nrsn = F_EXHAUSTED;
end
end else if ((m_cur != 4'd0) && !m_done && (m_cur < N_STEP[3:0])) begin
if (m_age >= STEP_TO[4:0] - 5'd1) begin
nev = 1'b1; nrsn = F_TIMEOUT; nfail = m_cur + 4'd1; nage = 5'd0;
nbump = 1'b1; bidx = m_cur + 4'd1;
if (bidx > m_max) nmax = bidx;
end else if (bidle) begin
nage = m_age + 5'd1;
end
end
m_cur = ncur; m_max = nmax; m_fail = nfail; m_rsn = nrsn;
m_age = nage; m_ev = nev;
if (det) begin
for (idx = 1; idx <= N_STEP; idx = idx + 1) m_retry[idx] = 4'd0;
m_dt = m_dt + 1;
end else if (nbump && (bidx >= 4'd1) && (bidx <= N_STEP[3:0])) begin
if (m_retry[bidx] != 4'hF) m_retry[bidx] = m_retry[bidx] + 4'd1;
end
if (ndone && !m_done) m_cp = m_cp + 1;
m_done = ndone;
if (sv && !det && !brst && !eo) m_st = m_st + 1;
if (brst && !det && !eo) m_rs = m_rs + 1;
if (nev) begin
case (nrsn)
F_STEP_FAIL: m_sf = m_sf + 1;
F_TIMEOUT: m_to = m_to + 1;
F_SKIP: m_sk = m_sk + 1;
F_REGRESS: m_rg = m_rg + 1;
F_EXHAUSTED: m_ex = m_ex + 1;
F_BADSTEP: m_bs = m_bs + 1;
default: ;
endcase
end
@(posedge clk); #1;
step_valid = 1'b0; bus_reset = 1'b0; detach = 1'b0; eot = 1'b0;
end
endtask
// A quiet bus. `bus_idle` must be HIGH here: the step timer counts DEAD
// AIR, not elapsed time, so a "nop" that leaves the bus marked busy never
// ages anything and the timeout phase below silently tests nothing.
task nop(input integer n);
integer j;
begin
for (j = 0; j < n; j = j + 1)
step(1'b0,4'd0,1'b0, 1'b0,1'b0,1'b1,1'b0);
end
endtask
task ok_step(input [3:0] s);
begin step(1'b1,s,1'b1, 1'b0,1'b0,1'b0,1'b0); end
endtask
task bad_step(input [3:0] s);
begin step(1'b1,s,1'b0, 1'b0,1'b0,1'b0,1'b0); end
endtask
task do_reset;
begin step(1'b0,4'd0,1'b0, 1'b1,1'b0,1'b0,1'b0); end
endtask
// A new device. This is the ONLY thing that clears the diagnosis.
task do_detach;
begin
step(1'b0,4'd0,1'b0, 1'b0,1'b1,1'b0,1'b0);
nop(1);
check(max_step === 4'd0,
"a detach did not clear the furthest step -- the next device's report would name a step it never attempted");
check(retries_at_max === 4'd0, "a detach did not clear the retry counts");
end
endtask
integer k, s, cb, b_e, b_sf, b_c, a;
initial begin
model_reset;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(cur_step === 4'd0 && max_step === 4'd0, "reset left a step recorded");
check(complete === 1'b0, "reset reported a complete enumeration");
// ---- Phase B: a clean enumeration, end to end. ----
for (k = 0; k < 40; k = k + 1) begin
do_detach;
b_c = n_complete;
do_reset;
for (s = 2; s <= N_STEP; s = s + 1) ok_step(s[3:0]);
check(complete === 1'b1, "a complete enumeration was not reported complete");
check(max_step === N_STEP[3:0], "a complete enumeration did not reach the last step");
check(n_complete == b_c + 1, "the completion was not counted");
// ...and a completed enumeration must NOT then time out.
b_e = n_timeout;
nop(STEP_TO + 4);
check(n_timeout == b_e,
"a COMPLETED enumeration timed out -- `complete` must be a sticky state, because one cycle later the tracker is sitting at the last step counting dead air toward a step that does not exist");
end
// ---- Phase C: THE CHAPTER. Fail at every step, retry three times, and
// ---- read the diagnosis off max_step.
for (s = 2; s <= N_STEP; s = s + 1) begin
do_detach;
for (k = 0; k < RETRY_MAX; k = k + 1) begin
do_reset;
for (a = 2; a < s; a = a + 1) ok_step(a[3:0]);
bad_step(s[3:0]);
end
// The host gives up and resets once more before reporting.
do_reset;
check(cur_step === 4'd1,
"the tracker is not sitting at step 1 after the final reset");
check(max_step === s[3:0],
"max_step is not the step that failed -- a tracker whose furthest step is cleared by a bus reset reports step 1 for every failure, and 'the device does not get past ATTACH' sends somebody to look at VBUS");
check(retries_at_max === RETRY_MAX[3:0],
"the retry count at the failing step is wrong -- retries are counted PER STEP, and 'fails at 5 three times' is a different bug from 'fails three times, somewhere'");
check(fail_step === s[3:0], "fail_step is not the failing step");
end
// ---- Phase D: retries are PER STEP. Fail at two different steps and
// ---- check the counts are kept apart.
do_detach;
do_reset;
ok_step(4'd2); ok_step(4'd3); ok_step(4'd4);
bad_step(4'd5); bad_step(4'd5);
do_reset;
ok_step(4'd2); ok_step(4'd3); ok_step(4'd4); ok_step(4'd5);
ok_step(4'd6); ok_step(4'd7); ok_step(4'd8);
bad_step(4'd9);
check(max_step === 4'd9, "max_step did not follow the furthest step");
check(retries_at_max === 4'd1,
"the retry count at step 9 has picked up step 5's failures -- a single global counter cannot tell a deterministic bug from a marginal one");
// ---- Phase E: RETRY_MAX is reported as its own reason. ----
do_detach;
b_e = n_exhausted;
for (k = 0; k < RETRY_MAX; k = k + 1) begin
do_reset;
ok_step(4'd2); ok_step(4'd3); ok_step(4'd4);
bad_step(4'd5);
end
check(n_exhausted == b_e + 1,
"the host ran out of retries at one step and it was not reported as exhaustion -- which is the difference between 'it sometimes fails' and 'the host has given up'");
// ---- Phase F: a step that never answers is not a step that failed. --
for (k = 0; k < 20; k = k + 1) begin
do_detach;
do_reset;
ok_step(4'd2); ok_step(4'd3);
b_e = n_timeout; b_sf = n_step_fail;
nop(STEP_TO - 1);
check(n_timeout == b_e, "the tracker gave up before its own bound");
nop(3);
check(n_timeout == b_e + 1,
"a step that never returned at all was not reported -- a failure is an answer and silence is not, and they have different causes");
check(n_step_fail == b_sf,
"a timeout was classified as a step failure");
check(max_step === 4'd4,
"the awaited step was not recorded as the furthest reached -- it was attempted, and naming it is the point");
end
// ---- Phase G: a SKIPPED step. ----
do_detach;
do_reset;
ok_step(4'd2); ok_step(4'd3);
b_e = n_skip;
ok_step(4'd7);
check(n_skip == b_e + 1,
"the host jumped past three steps and it was not reported -- hosts do not skip by accident, and the finding is upstream of the step that appears to be missing");
// ---- Phase H: a REGRESSING step, with no reset in between. ----
do_detach;
do_reset;
ok_step(4'd2); ok_step(4'd3); ok_step(4'd4);
b_e = n_regress;
ok_step(4'd3);
check(n_regress == b_e + 1,
"a step went backwards with no bus reset in between and it was not reported");
// ---- Phase H2: a step number the sequence has no room for. ----
do_detach;
do_reset;
ok_step(4'd2); ok_step(4'd3);
b_e = n_badstep;
step(1'b1, 4'd13, 1'b1, 1'b0, 1'b0, 1'b0, 1'b0);
check(n_badstep == b_e + 1,
"a step number outside the sequence was accepted -- a bad step number taken as progress makes max_step name a step nobody attempted, which is worse than no diagnosis because it is a confident one");
check(max_step === 4'd3,
"an out-of-range step number advanced the furthest step reached");
step(1'b1, 4'd0, 1'b1, 1'b0, 1'b0, 1'b0, 1'b0);
check(n_badstep == b_e + 2, "step number zero was accepted");
// ---- Phase I: EXHAUSTIVE. Every current step x every input. ----
for (s = 0; s <= N_STEP; s = s + 1) begin
for (cb = 0; cb < 16; cb = cb + 1) begin
do_detach;
if (s > 0) begin
do_reset;
for (a = 2; a <= s; a = a + 1) ok_step(a[3:0]);
end
check(cur_step === s[3:0],
"the sweep could not reach the step it meant to reach");
step(cb[3], (s[3:0] + 4'd1), cb[2], cb[1], cb[0], 1'b1, 1'b0);
step(cb[3], (s[3:0] + 4'd1), cb[2], cb[1], cb[0], 1'b1, 1'b0);
end
end
// ---- Phase J: random ----
for (k = 0; k < 30000; k = k + 1)
step(($unsigned($random) % 100) < 36,
($unsigned($random) % 12),
($unsigned($random) % 100) < 62,
($unsigned($random) % 100) < 12,
($unsigned($random) % 1000) < 7,
($unsigned($random) % 100) < 70,
1'b0);
// ---- Phase K: and a clean enumeration afterwards. ----
do_detach;
b_c = n_complete;
for (k = 0; k < 40; k = k + 1) begin
do_detach;
do_reset;
for (s = 2; s <= N_STEP; s = s + 1) ok_step(s[3:0]);
check(complete === 1'b1, "the tracker stopped recognising a complete enumeration");
end
check(n_complete == b_c + 40, "clean enumerations after the random phase were not counted");
// ---- Final agreement ----
check(n_steps === m_st[31:0], "n_steps disagrees with the model");
check(n_resets === m_rs[31:0], "n_resets disagrees");
check(n_detach === m_dt[31:0], "n_detach disagrees");
check(n_step_fail === m_sf[31:0], "n_step_fail disagrees");
check(n_timeout === m_to[31:0], "n_timeout disagrees");
check(n_skip === m_sk[31:0], "n_skip disagrees");
check(n_regress === m_rg[31:0], "n_regress disagrees");
check(n_exhausted === m_ex[31:0], "n_exhausted disagrees");
check(n_badstep === m_bs[31:0], "n_badstep disagrees");
check(n_complete === m_cp[31:0], "n_complete disagrees");
check(n_seen == 176, "not every current step was crossed with every input combination");
check(n_step_fail > 32'd0, "no step ever failed");
check(n_timeout > 32'd0, "no step ever timed out");
check(n_skip > 32'd0, "no step was ever skipped");
check(n_regress > 32'd0, "no step ever went backwards");
check(n_exhausted > 32'd0, "the retry limit was never reached");
check(n_badstep > 32'd0, "a step number outside the sequence was never seen");
check(n_complete > 32'd0, "enumeration never completed");
check(n_detach > 32'd0, "no device was ever detached");
$display("REACH step-x-input=%0d/176 steps=%0d", n_seen, n_steps_t);
$display("COUNTERS steps=%0d resets=%0d detach=%0d fail=%0d timeout=%0d skip=%0d regress=%0d exhausted=%0d complete=%0d",
n_steps, n_resets, n_detach, n_step_fail, n_timeout, n_skip,
n_regress, n_exhausted, n_complete);
$display(" badstep=%0d", n_badstep);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule11.2 SystemVerilog testbench
// Testbench for usb_enum_tracker (SystemVerilog).
//
// THE TEST THAT DEFINES THE CHAPTER
//
// Enumeration fails at step 6 and the host retries the whole sequence three
// times. At the end:
//
// cur_step reads 1 -- the third retry's first step
// max_step reads 6 -- THE DIAGNOSIS
//
// The suite drives exactly that and checks both numbers, because the whole
// argument of the chapter is that one of them is useless and the other is
// the answer. A tracker whose max_step is cleared by a bus reset reports 1
// as well, and a report of "the device does not get past ATTACH" sends
// somebody to look at VBUS.
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. Every step 1..N_STEP as the FAILING step, with the full retry-and-
// reset sequence around it, checking max_step each time. Ten failures,
// ten different diagnoses, and the tracker must produce the right one
// for each.
//
// 2. Every current step 0..N_STEP crossed with all 16 combinations of
// {step_valid, step_ok, bus_reset, detach} = 176 pairs, each state
// reached by real steps.
`timescale 1ns/1ps
module tb_et_sv;
import usb_enum_pkg::*;
localparam int N_STEP = 10;
localparam int RETRY_MAX = 3;
localparam int STEP_TO = 12;
logic clk = 1'b0, rst_n = 1'b0;
logic step_valid = 1'b0, step_ok = 1'b0;
logic bus_reset = 1'b0, detach = 1'b0, bus_idle = 1'b1, eot = 1'b0;
logic [3:0] step_id = 4'd0;
logic [3:0] cur_step, max_step, fail_step, retries_at_max;
fail_e fail_reason;
logic [4:0] step_age;
logic complete, event_pulse;
logic [31:0] n_steps, n_resets, n_detach, n_step_fail, n_timeout,
n_skip, n_regress, n_exhausted, n_badstep, n_complete;
usb_enum_tracker #(.N_STEP(N_STEP), .RETRY_MAX(RETRY_MAX),
.STEP_TO(STEP_TO)) dut (.*);
always #5 clk = ~clk;
int errors = 0, checks = 0;
task automatic check(input logic cond, input string msg);
begin
checks++;
if (!cond) begin
errors++;
if (errors <= 25)
$display("FAIL @%0t: %0s | cur=%0d max=%0d fail=%0d(%0d) rty=%0d age=%0d",
$time, msg, cur_step, max_step, fail_step, fail_reason,
retries_at_max, step_age);
end
end
endtask
// ------------------------------------------------------------------
// The shadow tracker.
// ------------------------------------------------------------------
logic [3:0] m_cur, m_max, m_fail;
fail_e m_rsn;
logic [4:0] m_age;
logic m_done, m_ev;
logic [3:0] m_retry [1:N_STEP];
int m_st, m_rs, m_dt, m_sf, m_to, m_sk, m_rg, m_ex, m_bs, m_cp;
int seen [176]; // (N_STEP+1) x 16 input combinations
int n_seen, n_steps_t;
task automatic model_reset();
int j;
begin
m_cur = 4'd0; m_max = 4'd0; m_fail = 4'd0; m_rsn = F_NONE;
m_age = 5'd0; m_done = 1'b0; m_ev = 1'b0;
for (j = 1; j <= N_STEP; j++) m_retry[j] = 4'd0;
m_st = 0; m_rs = 0; m_dt = 0; m_sf = 0; m_to = 0;
m_sk = 0; m_rg = 0; m_ex = 0; m_bs = 0; m_cp = 0;
for (j = 0; j < 176; j++) seen[j] = 0;
n_seen = 0; n_steps_t = 0;
end
endtask
int idx;
task automatic step(input logic sv, input logic [3:0] sid, input logic sok,
input logic brst, input logic det, input logic bidle,
input logic eo);
logic [3:0] ncur, nmax, nfail, bidx;
fail_e nrsn;
logic [4:0] nage;
logic ndone, nev, nbump;
begin
step_valid = sv; step_id = sid; step_ok = sok;
bus_reset = brst; detach = det; bus_idle = bidle; eot = eo;
#1;
check(cur_step === m_cur, "cur_step disagrees with the shadow tracker");
check(max_step === m_max, "max_step disagrees -- and max_step IS the diagnosis");
check(fail_step === m_fail, "fail_step disagrees");
check(fail_reason === m_rsn, "fail_reason disagrees");
check(step_age === m_age, "step_age disagrees");
check(complete === m_done, "complete disagrees");
check(event_pulse === m_ev, "the event pulse disagrees");
check(retries_at_max === ((m_max == 4'd0) ? 4'd0 : m_retry[m_max]),
"retries_at_max disagrees -- retries are counted PER STEP, and a global counter cannot produce this number");
// ---- max_step is a HIGH-WATER MARK. It never goes backwards except
// ---- on a detach, and that invariant is the whole block.
check(m_max >= m_cur,
"the furthest step reached is behind the current step, which is impossible");
check(max_step <= 4'(N_STEP),
"max_step names a step that does not exist");
check(step_age <= 5'(STEP_TO),
"the step timer ran past its bound");
check(!((m_cur == 4'd0) && (step_age != 5'd0)),
"the step timer is running with no enumeration in progress");
idx = int'(m_cur) * 16 + (sv ? 8 : 0) + (sok ? 4 : 0) + (brst ? 2 : 0) + (det ? 1 : 0);
if (idx < 176) begin
if (seen[idx] == 0) begin seen[idx] = 1; n_seen = n_seen + 1; end
end
n_steps_t++;
// ---- advance the shadow tracker ----
ncur = m_cur; nmax = m_max; nfail = m_fail; nrsn = F_NONE;
nage = m_age; ndone = m_done; nev = 1'b0; nbump = 1'b0; bidx = 4'd0;
if (eo) begin
nage = 5'd0;
end else if (det) begin
ncur = 4'd0; nmax = 4'd0; nfail = 4'd0; nrsn = F_NONE;
nage = 5'd0; ndone = 1'b0;
end else if (brst) begin
ncur = 4'd1; nage = 5'd0; ndone = 1'b0;
if (m_max < 4'd1) nmax = 4'd1;
end else if (sv) begin
nage = 5'd0;
if ((sid < 4'd1) || (sid > 4'(N_STEP))) begin
nev = 1'b1; nrsn = F_BADSTEP; nfail = sid;
end else if (sid > m_cur + 4'd1) begin
nev = 1'b1; nrsn = F_SKIP; nfail = sid; ncur = sid;
if (sid > m_max) nmax = sid;
end else if (sid <= m_cur) begin
nev = 1'b1; nrsn = F_REGRESS; nfail = sid; ncur = sid;
end else if (sok) begin
ncur = sid;
if (sid > m_max) nmax = sid;
if (sid == 4'(N_STEP)) begin ndone = 1'b1; nev = 1'b1; end
end else begin
nev = 1'b1; nrsn = F_STEP_FAIL; nfail = sid;
if (sid > m_max) nmax = sid;
nbump = 1'b1; bidx = sid;
if (m_retry[sid] + 4'd1 >= 4'(RETRY_MAX)) nrsn = F_EXHAUSTED;
end
end else if ((m_cur != 4'd0) && !m_done && (m_cur < 4'(N_STEP))) begin
if (m_age >= 5'(STEP_TO) - 5'd1) begin
nev = 1'b1; nrsn = F_TIMEOUT; nfail = m_cur + 4'd1; nage = 5'd0;
nbump = 1'b1; bidx = m_cur + 4'd1;
if (bidx > m_max) nmax = bidx;
end else if (bidle) begin
nage = m_age + 5'd1;
end
end
m_cur = ncur; m_max = nmax; m_fail = nfail; m_rsn = nrsn;
m_age = nage; m_ev = nev;
if (det) begin
for (idx = 1; idx <= N_STEP; idx++) m_retry[idx] = 4'd0;
m_dt++;
end else if (nbump && (bidx >= 4'd1) && (bidx <= 4'(N_STEP))) begin
if (m_retry[bidx] != 4'hF) m_retry[bidx] = m_retry[bidx] + 4'd1;
end
if (ndone && !m_done) m_cp = m_cp + 1;
m_done = ndone;
if (sv && !det && !brst && !eo) m_st = m_st + 1;
if (brst && !det && !eo) m_rs = m_rs + 1;
if (nev) begin
case (nrsn)
F_STEP_FAIL: m_sf = m_sf + 1;
F_TIMEOUT: m_to = m_to + 1;
F_SKIP: m_sk = m_sk + 1;
F_REGRESS: m_rg = m_rg + 1;
F_EXHAUSTED: m_ex = m_ex + 1;
F_BADSTEP: m_bs = m_bs + 1;
default: ;
endcase
end
@(posedge clk); #1;
step_valid = 1'b0; bus_reset = 1'b0; detach = 1'b0; eot = 1'b0;
end
endtask
// A quiet bus. `bus_idle` must be HIGH here: the step timer counts DEAD
// AIR, not elapsed time, so a "nop" that leaves the bus marked busy never
// ages anything and the timeout phase below silently tests nothing.
task automatic nop(input int n);
repeat (n) step(1'b0,4'd0,1'b0, 1'b0,1'b0,1'b1,1'b0);
endtask
task automatic ok_step(input logic [3:0] s);
step(1'b1,s,1'b1, 1'b0,1'b0,1'b0,1'b0);
endtask
task automatic bad_step(input logic [3:0] s);
step(1'b1,s,1'b0, 1'b0,1'b0,1'b0,1'b0);
endtask
task automatic do_reset();
step(1'b0,4'd0,1'b0, 1'b1,1'b0,1'b0,1'b0);
endtask
// A new device. This is the ONLY thing that clears the diagnosis.
task automatic do_detach();
begin
step(1'b0,4'd0,1'b0, 1'b0,1'b1,1'b0,1'b0);
nop(1);
check(max_step === 4'd0,
"a detach did not clear the furthest step -- the next device's report would name a step it never attempted");
check(retries_at_max === 4'd0, "a detach did not clear the retry counts");
end
endtask
int k, s, cb, b_e, b_sf, b_c, a;
initial begin
model_reset();
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(cur_step === 4'd0 && max_step === 4'd0, "reset left a step recorded");
check(complete === 1'b0, "reset reported a complete enumeration");
// ---- Phase B: a clean enumeration, end to end. ----
for (k = 0; k < 40; k++) begin
do_detach();
b_c = n_complete;
do_reset();
for (s = 2; s <= N_STEP; s++) ok_step(4'(s));
check(complete === 1'b1, "a complete enumeration was not reported complete");
check(max_step === 4'(N_STEP), "a complete enumeration did not reach the last step");
check(n_complete == b_c + 1, "the completion was not counted");
// ...and a completed enumeration must NOT then time out.
b_e = n_timeout;
nop(STEP_TO + 4);
check(n_timeout == b_e,
"a COMPLETED enumeration timed out -- `complete` must be a sticky state, because one cycle later the tracker is sitting at the last step counting dead air toward a step that does not exist");
end
// ---- Phase C: THE CHAPTER. Fail at every step, retry three times, and
// ---- read the diagnosis off max_step.
for (s = 2; s <= N_STEP; s++) begin
do_detach();
for (k = 0; k < RETRY_MAX; k++) begin
do_reset();
for (a = 2; a < s; a++) ok_step(4'(a));
bad_step(4'(s));
end
// The host gives up and resets once more before reporting.
do_reset();
check(cur_step === 4'd1,
"the tracker is not sitting at step 1 after the final reset");
check(max_step === 4'(s),
"max_step is not the step that failed -- a tracker whose furthest step is cleared by a bus reset reports step 1 for every failure, and 'the device does not get past ATTACH' sends somebody to look at VBUS");
check(retries_at_max === 4'(RETRY_MAX),
"the retry count at the failing step is wrong -- retries are counted PER STEP, and 'fails at 5 three times' is a different bug from 'fails three times, somewhere'");
check(fail_step === 4'(s), "fail_step is not the failing step");
end
// ---- Phase D: retries are PER STEP. Fail at two different steps and
// ---- check the counts are kept apart.
do_detach();
do_reset();
ok_step(4'd2); ok_step(4'd3); ok_step(4'd4);
bad_step(4'd5); bad_step(4'd5);
do_reset();
ok_step(4'd2); ok_step(4'd3); ok_step(4'd4); ok_step(4'd5);
ok_step(4'd6); ok_step(4'd7); ok_step(4'd8);
bad_step(4'd9);
check(max_step === 4'd9, "max_step did not follow the furthest step");
check(retries_at_max === 4'd1,
"the retry count at step 9 has picked up step 5's failures -- a single global counter cannot tell a deterministic bug from a marginal one");
// ---- Phase E: RETRY_MAX is reported as its own reason. ----
do_detach();
b_e = n_exhausted;
for (k = 0; k < RETRY_MAX; k++) begin
do_reset();
ok_step(4'd2); ok_step(4'd3); ok_step(4'd4);
bad_step(4'd5);
end
check(n_exhausted == b_e + 1,
"the host ran out of retries at one step and it was not reported as exhaustion -- which is the difference between 'it sometimes fails' and 'the host has given up'");
// ---- Phase F: a step that never answers is not a step that failed. --
for (k = 0; k < 20; k++) begin
do_detach();
do_reset();
ok_step(4'd2); ok_step(4'd3);
b_e = n_timeout; b_sf = n_step_fail;
nop(STEP_TO - 1);
check(n_timeout == b_e, "the tracker gave up before its own bound");
nop(3);
check(n_timeout == b_e + 1,
"a step that never returned at all was not reported -- a failure is an answer and silence is not, and they have different causes");
check(n_step_fail == b_sf,
"a timeout was classified as a step failure");
check(max_step === 4'd4,
"the awaited step was not recorded as the furthest reached -- it was attempted, and naming it is the point");
end
// ---- Phase G: a SKIPPED step. ----
do_detach();
do_reset();
ok_step(4'd2); ok_step(4'd3);
b_e = n_skip;
ok_step(4'd7);
check(n_skip == b_e + 1,
"the host jumped past three steps and it was not reported -- hosts do not skip by accident, and the finding is upstream of the step that appears to be missing");
// ---- Phase H: a REGRESSING step, with no reset in between. ----
do_detach();
do_reset();
ok_step(4'd2); ok_step(4'd3); ok_step(4'd4);
b_e = n_regress;
ok_step(4'd3);
check(n_regress == b_e + 1,
"a step went backwards with no bus reset in between and it was not reported");
// ---- Phase H2: a step number the sequence has no room for. ----
do_detach();
do_reset();
ok_step(4'd2); ok_step(4'd3);
b_e = n_badstep;
step(1'b1, 4'd13, 1'b1, 1'b0, 1'b0, 1'b0, 1'b0);
check(n_badstep == b_e + 1,
"a step number outside the sequence was accepted -- a bad step number taken as progress makes max_step name a step nobody attempted, which is worse than no diagnosis because it is a confident one");
check(max_step === 4'd3,
"an out-of-range step number advanced the furthest step reached");
step(1'b1, 4'd0, 1'b1, 1'b0, 1'b0, 1'b0, 1'b0);
check(n_badstep == b_e + 2, "step number zero was accepted");
// ---- Phase I: EXHAUSTIVE. Every current step x every input. ----
for (s = 0; s <= N_STEP; s++) begin
for (cb = 0; cb < 16; cb++) begin
do_detach();
if (s > 0) begin
do_reset();
for (a = 2; a <= s; a++) ok_step(4'(a));
end
check(cur_step === 4'(s),
"the sweep could not reach the step it meant to reach");
step(1'(cb[3]), 4'(s + 1), 1'(cb[2]), 1'(cb[1]), 1'(cb[0]), 1'b1, 1'b0);
step(1'(cb[3]), 4'(s + 1), 1'(cb[2]), 1'(cb[1]), 1'(cb[0]), 1'b1, 1'b0);
end
end
// ---- Phase J: random ----
for (k = 0; k < 30000; k++)
step($urandom_range(0,99) < 36,
4'($urandom_range(0,11)),
$urandom_range(0,99) < 62,
$urandom_range(0,99) < 12,
$urandom_range(0,999) < 7,
$urandom_range(0,99) < 70,
1'b0);
// ---- Phase K: and a clean enumeration afterwards. ----
do_detach();
b_c = n_complete;
for (k = 0; k < 40; k++) begin
do_detach();
do_reset();
for (s = 2; s <= N_STEP; s++) ok_step(4'(s));
check(complete === 1'b1, "the tracker stopped recognising a complete enumeration");
end
check(n_complete == b_c + 40, "clean enumerations after the random phase were not counted");
// ---- Final agreement ----
check(n_steps === 32'(m_st), "n_steps disagrees with the model");
check(n_resets === 32'(m_rs), "n_resets disagrees");
check(n_detach === 32'(m_dt), "n_detach disagrees");
check(n_step_fail === 32'(m_sf), "n_step_fail disagrees");
check(n_timeout === 32'(m_to), "n_timeout disagrees");
check(n_skip === 32'(m_sk), "n_skip disagrees");
check(n_regress === 32'(m_rg), "n_regress disagrees");
check(n_exhausted === 32'(m_ex), "n_exhausted disagrees");
check(n_badstep === 32'(m_bs), "n_badstep disagrees");
check(n_complete === 32'(m_cp), "n_complete disagrees");
check(n_seen == 176, "not every current step was crossed with every input combination");
check(n_step_fail > 32'd0, "no step ever failed");
check(n_timeout > 32'd0, "no step ever timed out");
check(n_skip > 32'd0, "no step was ever skipped");
check(n_regress > 32'd0, "no step ever went backwards");
check(n_exhausted > 32'd0, "the retry limit was never reached");
check(n_badstep > 32'd0, "a step number outside the sequence was never seen");
check(n_complete > 32'd0, "enumeration never completed");
check(n_detach > 32'd0, "no device was ever detached");
$display("REACH step-x-input=%0d/176 steps=%0d", n_seen, n_steps_t);
$display("COUNTERS steps=%0d resets=%0d detach=%0d fail=%0d timeout=%0d skip=%0d regress=%0d exhausted=%0d complete=%0d",
n_steps, n_resets, n_detach, n_step_fail, n_timeout, n_skip,
n_regress, n_exhausted, n_complete);
$display(" badstep=%0d", n_badstep);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule11.3 VHDL testbench
-- Testbench for usb_enum_tracker (VHDL-2008).
--
-- THE TEST THAT DEFINES THE CHAPTER
--
-- Enumeration fails at step 6 and the host retries the whole sequence three
-- times. At the end:
--
-- cur_step reads 1 -- the third retry's first step
-- max_step reads 6 -- THE DIAGNOSIS
--
-- The suite drives exactly that and checks both numbers, because the whole
-- argument of the chapter is that one of them is useless and the other is
-- the answer. A tracker whose max_step is cleared by a bus reset reports 1
-- as well, and a report of "the device does not get past ATTACH" sends
-- somebody to look at VBUS.
--
-- WHAT IS EXHAUSTIVE HERE
--
-- 1. Every step 1..N_STEP as the FAILING step, with the full retry-and-
-- reset sequence around it, checking max_step each time. Ten failures,
-- ten different diagnoses, and the tracker must produce the right one
-- for each.
--
-- 2. Every current step 0..N_STEP crossed with all 16 combinations of
-- (step_valid, step_ok, bus_reset, detach) = 176 pairs, each state
-- reached by real steps.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_enum_pkg.all;
entity tb_et_vhdl is
end entity tb_et_vhdl;
architecture sim of tb_et_vhdl is
constant N_STEP : integer := 10;
constant RETRY_MAX : integer := 3;
constant STEP_TO : integer := 12;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal step_valid, step_ok, bus_reset, detach, eot : std_logic := '0';
signal bus_idle : std_logic := '1';
signal step_id : std_logic_vector(3 downto 0) := (others => '0');
signal cur_step, max_step, fail_step, retries_at_max : std_logic_vector(3 downto 0);
signal fail_reason : std_logic_vector(2 downto 0);
signal step_age : std_logic_vector(4 downto 0);
signal complete, event_pulse : std_logic;
signal n_steps, n_resets, n_detach, n_step_fail : std_logic_vector(31 downto 0);
signal n_timeout, n_skip, n_regress : std_logic_vector(31 downto 0);
signal n_exhausted, n_badstep, n_complete : std_logic_vector(31 downto 0);
begin
dut : entity work.usb_enum_tracker
generic map (N_STEP => N_STEP, RETRY_MAX => RETRY_MAX, STEP_TO => STEP_TO)
port map (
clk => clk, rst_n => rst_n,
step_valid => step_valid, step_id => step_id, step_ok => step_ok,
bus_reset => bus_reset, detach => detach, bus_idle => bus_idle, eot => eot,
cur_step => cur_step, max_step => max_step, fail_step => fail_step,
fail_reason => fail_reason, retries_at_max => retries_at_max,
step_age => step_age, complete => complete, event_pulse => event_pulse,
n_steps => n_steps, n_resets => n_resets, n_detach => n_detach,
n_step_fail => n_step_fail, n_timeout => n_timeout, n_skip => n_skip,
n_regress => n_regress, n_exhausted => n_exhausted,
n_badstep => n_badstep, n_complete => n_complete
);
clk <= (not clk) after 5 ns when not done else '0';
stim : process
type retry_arr is array (1 to N_STEP) of unsigned(3 downto 0);
type seen_arr is array (0 to 175) of integer;
variable errors, checks : integer := 0;
-- ---- The shadow tracker. ----
variable m_cur, m_max, m_fail : unsigned(3 downto 0) := (others => '0');
variable m_rsn : fail_t := F_NONE;
variable m_age : unsigned(4 downto 0) := (others => '0');
variable m_done, m_ev : std_logic := '0';
variable m_retry : retry_arr := (others => (others => '0'));
variable m_st, m_rs, m_dt, m_sf, m_to : integer := 0;
variable m_sk, m_rg, m_ex, m_bs, m_cp : integer := 0;
variable seen : seen_arr := (others => 0);
variable n_seen, n_steps_t : integer := 0;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"E17A0B3C";
impure function rnd32 return unsigned is
begin
lfsr := lfsr(30 downto 0) &
(lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
return lfsr;
end function;
-- Only the low 30 bits are converted: a full 32-bit unsigned does not
-- fit in VHDL's INTEGER, and to_integer aborts the run rather than
-- wrapping.
impure function rnd_nat return integer is
variable u : unsigned(31 downto 0);
begin
u := rnd32;
return to_integer(u(29 downto 0));
end function;
impure function rnd_lt (pct, base : integer) return std_logic is
begin
if (rnd_nat mod base) < pct then return '1'; else return '0'; end if;
end function;
procedure chk (cond : boolean; msg : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 25 then
report "FAIL: " & msg &
" | cur=" & integer'image(to_integer(m_cur)) &
" max=" & integer'image(to_integer(m_max)) &
" rsn=" & integer'image(fail_t'pos(m_rsn))
severity note;
end if;
end if;
end procedure;
procedure step (sv : std_logic; sid_v : std_logic_vector(3 downto 0);
sok, brst, det, bidle, eo : std_logic) is
variable ncur, nmax, nfail, bidx : unsigned(3 downto 0);
variable nrsn : fail_t;
variable nage : unsigned(4 downto 0);
variable ndone, nev, nbump : std_logic;
variable sid : unsigned(3 downto 0);
variable idx : integer;
begin
step_valid <= sv; step_id <= sid_v; step_ok <= sok;
bus_reset <= brst; detach <= det; bus_idle <= bidle; eot <= eo;
wait for 1 ns;
chk(unsigned(cur_step) = m_cur, "cur_step disagrees with the shadow tracker");
chk(unsigned(max_step) = m_max, "max_step disagrees -- and max_step IS the diagnosis");
chk(unsigned(fail_step) = m_fail, "fail_step disagrees");
chk(fail_reason = f_code(m_rsn), "fail_reason disagrees");
chk(unsigned(step_age) = m_age, "step_age disagrees");
chk(complete = m_done, "complete disagrees");
chk(event_pulse = m_ev, "the event pulse disagrees");
if m_max = 0 then
chk(unsigned(retries_at_max) = 0,
"retries_at_max disagrees -- retries are counted PER STEP, and a global counter cannot produce this number");
else
chk(unsigned(retries_at_max) = m_retry(to_integer(m_max)),
"retries_at_max disagrees -- retries are counted PER STEP, and a global counter cannot produce this number");
end if;
-- ---- max_step is a HIGH-WATER MARK. It never goes backwards except
-- ---- on a detach, and that invariant is the whole block.
chk(m_max >= m_cur,
"the furthest step reached is behind the current step, which is impossible");
chk(unsigned(max_step) <= to_unsigned(N_STEP, 4),
"max_step names a step that does not exist");
chk(unsigned(step_age) <= to_unsigned(STEP_TO, 5),
"the step timer ran past its bound");
chk(not (m_cur = 0 and unsigned(step_age) /= 0),
"the step timer is running with no enumeration in progress");
sid := unsigned(sid_v);
idx := to_integer(m_cur) * 16;
if sv = '1' then idx := idx + 8; end if;
if sok = '1' then idx := idx + 4; end if;
if brst = '1' then idx := idx + 2; end if;
if det = '1' then idx := idx + 1; end if;
if idx < 176 then
if seen(idx) = 0 then seen(idx) := 1; n_seen := n_seen + 1; end if;
end if;
n_steps_t := n_steps_t + 1;
-- ---- advance the shadow tracker ----
ncur := m_cur; nmax := m_max; nfail := m_fail; nrsn := F_NONE;
nage := m_age; ndone := m_done; nev := '0'; nbump := '0';
bidx := (others => '0');
if eo = '1' then
nage := (others => '0');
elsif det = '1' then
ncur := (others => '0'); nmax := (others => '0');
nfail := (others => '0'); nrsn := F_NONE;
nage := (others => '0'); ndone := '0';
elsif brst = '1' then
ncur := to_unsigned(1, 4); nage := (others => '0'); ndone := '0';
if m_max < 1 then nmax := to_unsigned(1, 4); end if;
elsif sv = '1' then
nage := (others => '0');
if sid < 1 or sid > to_unsigned(N_STEP, 4) then
nev := '1'; nrsn := F_BADSTEP; nfail := sid;
elsif sid > m_cur + 1 then
nev := '1'; nrsn := F_SKIP; nfail := sid; ncur := sid;
if sid > m_max then nmax := sid; end if;
elsif sid <= m_cur then
nev := '1'; nrsn := F_REGRESS; nfail := sid; ncur := sid;
elsif sok = '1' then
ncur := sid;
if sid > m_max then nmax := sid; end if;
if sid = to_unsigned(N_STEP, 4) then ndone := '1'; nev := '1'; end if;
else
nev := '1'; nrsn := F_STEP_FAIL; nfail := sid;
if sid > m_max then nmax := sid; end if;
nbump := '1'; bidx := sid;
if m_retry(to_integer(sid)) + 1 >= to_unsigned(RETRY_MAX, 4) then
nrsn := F_EXHAUSTED;
end if;
end if;
elsif m_cur /= 0 and m_done = '0' and m_cur < to_unsigned(N_STEP, 4) then
if m_age >= to_unsigned(STEP_TO - 1, 5) then
nev := '1'; nrsn := F_TIMEOUT; nfail := m_cur + 1;
nage := (others => '0');
nbump := '1'; bidx := m_cur + 1;
if bidx > m_max then nmax := bidx; end if;
elsif bidle = '1' then
nage := m_age + 1;
end if;
end if;
m_cur := ncur; m_max := nmax; m_fail := nfail; m_rsn := nrsn;
m_age := nage; m_ev := nev;
if det = '1' then
m_retry := (others => (others => '0'));
m_dt := m_dt + 1;
elsif nbump = '1' and bidx >= 1 and bidx <= to_unsigned(N_STEP, 4) then
if m_retry(to_integer(bidx)) /= x"F" then
m_retry(to_integer(bidx)) := m_retry(to_integer(bidx)) + 1;
end if;
end if;
if ndone = '1' and m_done = '0' then m_cp := m_cp + 1; end if;
m_done := ndone;
if sv = '1' and det = '0' and brst = '0' and eo = '0' then
m_st := m_st + 1;
end if;
if brst = '1' and det = '0' and eo = '0' then m_rs := m_rs + 1; end if;
if nev = '1' then
case nrsn is
when F_STEP_FAIL => m_sf := m_sf + 1;
when F_TIMEOUT => m_to := m_to + 1;
when F_SKIP => m_sk := m_sk + 1;
when F_REGRESS => m_rg := m_rg + 1;
when F_EXHAUSTED => m_ex := m_ex + 1;
when F_BADSTEP => m_bs := m_bs + 1;
when others => null;
end case;
end if;
wait until rising_edge(clk);
wait for 1 ns;
step_valid <= '0'; bus_reset <= '0'; detach <= '0'; eot <= '0';
end procedure;
constant Z4 : std_logic_vector(3 downto 0) := (others => '0');
function s4 (n : integer) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(n, 4));
end function;
-- A quiet bus. `bus_idle` must be HIGH here: the step timer counts DEAD
-- AIR, not elapsed time, so a "nop" that leaves the bus marked busy
-- never ages anything and the timeout phase below silently tests
-- nothing.
procedure nop (n : integer) is
begin
for j in 1 to n loop
step('0', Z4, '0', '0', '0', '1', '0');
end loop;
end procedure;
procedure ok_step (s : integer) is
begin
step('1', s4(s), '1', '0', '0', '0', '0');
end procedure;
procedure bad_step (s : integer) is
begin
step('1', s4(s), '0', '0', '0', '0', '0');
end procedure;
procedure do_reset is
begin
step('0', Z4, '0', '1', '0', '0', '0');
end procedure;
-- A new device. This is the ONLY thing that clears the diagnosis.
procedure do_detach is
begin
step('0', Z4, '0', '0', '1', '0', '0');
nop(1);
chk(unsigned(max_step) = 0,
"a detach did not clear the furthest step -- the next device's report would name a step it never attempted");
chk(unsigned(retries_at_max) = 0, "a detach did not clear the retry counts");
end procedure;
variable b_e, b_sf, b_c : integer := 0;
variable sv_v, sok_v, brst_v, det_v : std_logic;
variable ln : line;
begin
wait for 33 ns;
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
-- ---- Phase A: the state after reset ----
chk(unsigned(cur_step) = 0 and unsigned(max_step) = 0,
"reset left a step recorded");
chk(complete = '0', "reset reported a complete enumeration");
-- ---- Phase B: a clean enumeration, end to end. ----
for k in 0 to 39 loop
do_detach;
b_c := to_integer(unsigned(n_complete));
do_reset;
for s in 2 to N_STEP loop ok_step(s); end loop;
chk(complete = '1', "a complete enumeration was not reported complete");
chk(unsigned(max_step) = to_unsigned(N_STEP, 4),
"a complete enumeration did not reach the last step");
chk(to_integer(unsigned(n_complete)) = b_c + 1,
"the completion was not counted");
b_e := to_integer(unsigned(n_timeout));
nop(STEP_TO + 4);
chk(to_integer(unsigned(n_timeout)) = b_e,
"a COMPLETED enumeration timed out -- `complete` must be a sticky state, because one cycle later the tracker is sitting at the last step counting dead air toward a step that does not exist");
end loop;
-- ---- Phase C: THE CHAPTER. Fail at every step, retry three times, and
-- ---- read the diagnosis off max_step.
for s in 2 to N_STEP loop
do_detach;
for k in 1 to RETRY_MAX loop
do_reset;
for a in 2 to s - 1 loop ok_step(a); end loop;
bad_step(s);
end loop;
do_reset;
chk(unsigned(cur_step) = 1,
"the tracker is not sitting at step 1 after the final reset");
chk(unsigned(max_step) = to_unsigned(s, 4),
"max_step is not the step that failed -- a tracker whose furthest step is cleared by a bus reset reports step 1 for every failure, and 'the device does not get past ATTACH' sends somebody to look at VBUS");
chk(unsigned(retries_at_max) = to_unsigned(RETRY_MAX, 4),
"the retry count at the failing step is wrong -- retries are counted PER STEP, and 'fails at 5 three times' is a different bug from 'fails three times, somewhere'");
chk(unsigned(fail_step) = to_unsigned(s, 4), "fail_step is not the failing step");
end loop;
-- ---- Phase D: retries are PER STEP. ----
do_detach;
do_reset;
ok_step(2); ok_step(3); ok_step(4);
bad_step(5); bad_step(5);
do_reset;
ok_step(2); ok_step(3); ok_step(4); ok_step(5);
ok_step(6); ok_step(7); ok_step(8);
bad_step(9);
chk(unsigned(max_step) = 9, "max_step did not follow the furthest step");
chk(unsigned(retries_at_max) = 1,
"the retry count at step 9 has picked up step 5's failures -- a single global counter cannot tell a deterministic bug from a marginal one");
-- ---- Phase E: RETRY_MAX is reported as its own reason. ----
do_detach;
b_e := to_integer(unsigned(n_exhausted));
for k in 1 to RETRY_MAX loop
do_reset;
ok_step(2); ok_step(3); ok_step(4);
bad_step(5);
end loop;
chk(to_integer(unsigned(n_exhausted)) = b_e + 1,
"the host ran out of retries at one step and it was not reported as exhaustion -- which is the difference between 'it sometimes fails' and 'the host has given up'");
-- ---- Phase F: a step that never answers is not a step that failed. --
for k in 1 to 20 loop
do_detach;
do_reset;
ok_step(2); ok_step(3);
b_e := to_integer(unsigned(n_timeout));
b_sf := to_integer(unsigned(n_step_fail));
nop(STEP_TO - 1);
chk(to_integer(unsigned(n_timeout)) = b_e,
"the tracker gave up before its own bound");
nop(3);
chk(to_integer(unsigned(n_timeout)) = b_e + 1,
"a step that never returned at all was not reported -- a failure is an answer and silence is not, and they have different causes");
chk(to_integer(unsigned(n_step_fail)) = b_sf,
"a timeout was classified as a step failure");
chk(unsigned(max_step) = 4,
"the awaited step was not recorded as the furthest reached -- it was attempted, and naming it is the point");
end loop;
-- ---- Phase G: a SKIPPED step. ----
do_detach;
do_reset;
ok_step(2); ok_step(3);
b_e := to_integer(unsigned(n_skip));
ok_step(7);
chk(to_integer(unsigned(n_skip)) = b_e + 1,
"the host jumped past three steps and it was not reported -- hosts do not skip by accident, and the finding is upstream of the step that appears to be missing");
-- ---- Phase H: a REGRESSING step, with no reset in between. ----
do_detach;
do_reset;
ok_step(2); ok_step(3); ok_step(4);
b_e := to_integer(unsigned(n_regress));
ok_step(3);
chk(to_integer(unsigned(n_regress)) = b_e + 1,
"a step went backwards with no bus reset in between and it was not reported");
-- ---- Phase H2: a step number the sequence has no room for. ----
do_detach;
do_reset;
ok_step(2); ok_step(3);
b_e := to_integer(unsigned(n_badstep));
step('1', s4(13), '1', '0', '0', '0', '0');
chk(to_integer(unsigned(n_badstep)) = b_e + 1,
"a step number outside the sequence was accepted -- a bad step number taken as progress makes max_step name a step nobody attempted, which is worse than no diagnosis because it is a confident one");
chk(unsigned(max_step) = 3,
"an out-of-range step number advanced the furthest step reached");
step('1', s4(0), '1', '0', '0', '0', '0');
chk(to_integer(unsigned(n_badstep)) = b_e + 2, "step number zero was accepted");
-- ---- Phase I: EXHAUSTIVE. Every current step x every input. ----
for s in 0 to N_STEP loop
for cb in 0 to 15 loop
do_detach;
if s > 0 then
do_reset;
for a in 2 to s loop ok_step(a); end loop;
end if;
chk(unsigned(cur_step) = to_unsigned(s, 4),
"the sweep could not reach the step it meant to reach");
if (cb / 8) mod 2 = 1 then sv_v := '1'; else sv_v := '0'; end if;
if (cb / 4) mod 2 = 1 then sok_v := '1'; else sok_v := '0'; end if;
if (cb / 2) mod 2 = 1 then brst_v := '1'; else brst_v := '0'; end if;
if cb mod 2 = 1 then det_v := '1'; else det_v := '0'; end if;
step(sv_v, s4((s + 1) mod 16), sok_v, brst_v, det_v, '1', '0');
step(sv_v, s4((s + 1) mod 16), sok_v, brst_v, det_v, '1', '0');
end loop;
end loop;
-- ---- Phase J: random ----
for k in 0 to 29999 loop
sv_v := rnd_lt(36, 100);
sok_v := rnd_lt(62, 100);
brst_v := rnd_lt(12, 100);
det_v := rnd_lt(7, 1000);
step(sv_v, s4(rnd_nat mod 12), sok_v, brst_v, det_v, rnd_lt(70, 100), '0');
end loop;
-- ---- Phase K: and a clean enumeration afterwards. ----
do_detach;
b_c := to_integer(unsigned(n_complete));
for k in 1 to 40 loop
do_detach;
do_reset;
for s in 2 to N_STEP loop ok_step(s); end loop;
chk(complete = '1', "the tracker stopped recognising a complete enumeration");
end loop;
chk(to_integer(unsigned(n_complete)) = b_c + 40,
"clean enumerations after the random phase were not counted");
-- ---- Final agreement ----
chk(to_integer(unsigned(n_steps)) = m_st, "n_steps disagrees with the model");
chk(to_integer(unsigned(n_resets)) = m_rs, "n_resets disagrees");
chk(to_integer(unsigned(n_detach)) = m_dt, "n_detach disagrees");
chk(to_integer(unsigned(n_step_fail)) = m_sf, "n_step_fail disagrees");
chk(to_integer(unsigned(n_timeout)) = m_to, "n_timeout disagrees");
chk(to_integer(unsigned(n_skip)) = m_sk, "n_skip disagrees");
chk(to_integer(unsigned(n_regress)) = m_rg, "n_regress disagrees");
chk(to_integer(unsigned(n_exhausted)) = m_ex, "n_exhausted disagrees");
chk(to_integer(unsigned(n_badstep)) = m_bs, "n_badstep disagrees");
chk(to_integer(unsigned(n_complete)) = m_cp, "n_complete disagrees");
chk(n_seen = 176, "not every current step was crossed with every input combination");
chk(to_integer(unsigned(n_step_fail)) > 0, "no step ever failed");
chk(to_integer(unsigned(n_timeout)) > 0, "no step ever timed out");
chk(to_integer(unsigned(n_skip)) > 0, "no step was ever skipped");
chk(to_integer(unsigned(n_regress)) > 0, "no step ever went backwards");
chk(to_integer(unsigned(n_exhausted)) > 0, "the retry limit was never reached");
chk(to_integer(unsigned(n_badstep)) > 0, "a step number outside the sequence was never seen");
chk(to_integer(unsigned(n_complete)) > 0, "enumeration never completed");
chk(to_integer(unsigned(n_detach)) > 0, "no device was ever detached");
write(ln, string'("REACH step-x-input=") & integer'image(n_seen) &
"/176 steps=" & integer'image(n_steps_t));
writeline(output, ln);
write(ln, string'("COUNTERS steps=") & integer'image(to_integer(unsigned(n_steps))) &
" resets=" & integer'image(to_integer(unsigned(n_resets))) &
" detach=" & integer'image(to_integer(unsigned(n_detach))) &
" fail=" & integer'image(to_integer(unsigned(n_step_fail))) &
" timeout=" & integer'image(to_integer(unsigned(n_timeout))) &
" skip=" & integer'image(to_integer(unsigned(n_skip))) &
" regress=" & integer'image(to_integer(unsigned(n_regress))) &
" exhausted=" & integer'image(to_integer(unsigned(n_exhausted))) &
" complete=" & integer'image(to_integer(unsigned(n_complete))));
writeline(output, ln);
write(ln, string'(" badstep=") & integer'image(to_integer(unsigned(n_badstep))));
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 sim;12. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| current step x input | 176 / 176 | 176 / 176 | 176 / 176 |
| Steps | 33809 | 33809 | 33809 |
| Checks executed | 406812 | 406812 | 406812 |
| steps observed | 11100 | 11009 | 11147 |
| bus resets | 3996 | 4015 | 3883 |
| detaches | 656 | 649 | 686 |
| step failures | 293 | 294 | 211 |
| timeouts | 23 | 20 | 21 |
| skipped steps | 3998 | 3978 | 3948 |
| regressions | 3215 | 3086 | 3189 |
| retry limits reached | 23 | 41 | 13 |
| out-of-range step ids | 1528 | 1531 | 1623 |
| enumerations completed | 127 | 130 | 131 |
| Result | PASS | PASS | PASS |
The timeout row is small in every column for the reason 24.1 §13 established: random stimulus essentially never produces the long silence a liveness rule needs. Those twenty-odd timeouts are the directed phase, and if the directed phase were deleted the number would be close to zero.
13. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| N3 | a skipped step is taken as ordinary progress | 44672 | 44774 | 40403 |
| N1 | a bus reset clears the furthest step | 26080 | 26394 | 23914 |
| N4 | a step going backwards is taken as progress | 20238 | 17546 | 17302 |
| N6 | a detach does not clear the diagnosis | 9327 | 8766 | 9986 |
| N5 | one global retry counter instead of one per step | 3092 | 2982 | 1139 |
| N7 | a step that FAILED does not advance the mark | 612 | 548 | 538 |
| N2 | no timeout at all | 377 | 362 | 369 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct.
N1 is the headline and scores 26 000 — a bus reset clearing the furthest step. Every retry in the run then destroys the diagnosis, which is why the count is so large.
N7 is the same bug in its subtle form and scores 612, forty times lower. It does not clear the mark; it simply fails to advance it when a step fails, so a step that was attempted and rejected is never named. The reported diagnosis is then the last step that succeeded — which is off by one, plausible, and points at the wrong place.
N2 — no timeout — scores 377, and per §12 essentially all of that is the directed phase.
14. Debugging Walkthrough: "The Device Does Not Get Past ATTACH"
The report. A new USB device does not enumerate on one particular host. The team's own analyser script reports: enumeration state at failure = ATTACH. Three engineers spend two days on VBUS, the connector, the pull-up resistor and the cable.
Step 1 — the pull-up is fine. The host clearly sees the device: it issues a bus reset, which it would not do for a device it had not detected. So ATTACH cannot be where it stopped.
Step 2 — read the trace instead of the summary. The host resets, reads eight bytes of the device descriptor, resets again, sets the address, requests the full device descriptor — and then resets again. Three times, and then nothing.
Step 3 — so it fails at step 6. The summary said ATTACH because the script recorded the current state, and the current state after the third retry's reset is, correctly and uselessly, ATTACH.
Step 4 — why step 6? The full device descriptor is 18 bytes. The device returns 18 bytes. The host asks for 18 and gets 18. That looks right.
Step 5 — compare the two reads. The 8-byte read at step 3 reports bMaxPacketSize0 = 64. The full read at step 6 reports bMaxPacketSize0 = 8. The device changed its answer.
Step 6 — and that is why only one host fails. The first host had used 64 throughout and never re-read the field. This one re-reads it, believes the second answer, and every subsequent control transfer is fragmented at 8 bytes — which the device's endpoint-0 handler, written for 64, does not reassemble.
15. UVM: Making the Tracker Part of the Environment
// The tracker is not a scoreboard and not a checker. It does not decide
// whether anything is WRONG -- chapter 24.1's checker does that. Its whole
// job is to make a failure LEGIBLE after the fact, which means it must
// survive everything the checkers react to.
class usb_enum_tracker_c extends uvm_component;
`uvm_component_utils(usb_enum_tracker_c)
uvm_analysis_imp #(usb_bus_pkt, usb_enum_tracker_c) ap;
typedef enum { S_NONE, S_ATTACH, S_RESET, S_GET_DESC_8, S_RESET_2,
S_SET_ADDRESS, S_GET_DESC_DEV, S_GET_DESC_C9,
S_GET_DESC_CFG, S_SET_CONFIG, S_ENUMERATED } enum_step_e;
enum_step_e cur_step = S_NONE;
// ---- THE register. It survives a bus reset; only a detach clears it. ----
enum_step_e max_step = S_NONE;
// Retries PER STEP, because "fails at SET_ADDRESS three times" and "fails
// three times, somewhere" send you to different instruments.
int unsigned retries[enum_step_e];
enum_step_e fail_step = S_NONE;
string fail_reason = "";
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void note(enum_step_e s);
cur_step = s;
// `>` on an enum compares its position, which is exactly the ordering
// the sequence has. That is the only reason these are an enum in
// declaration order rather than a set of strings.
if (s > max_step) max_step = s;
endfunction
function void write(usb_bus_pkt p);
if (p.is_detach()) begin
// A DIFFERENT DEVICE. This is the only event that clears the
// diagnosis; carrying a previous device's furthest step into the next
// device's enumeration produces a report naming a step it never
// attempted.
cur_step = S_NONE; max_step = S_NONE;
fail_step = S_NONE; fail_reason = "";
retries.delete();
return;
end
if (p.is_bus_reset()) begin
// A RETRY. The sequence restarts; the DIAGNOSIS does not.
cur_step = S_ATTACH;
if (max_step < S_ATTACH) max_step = S_ATTACH;
return;
end
begin
enum_step_e s = classify(p);
if (s == S_NONE) return;
if (s > cur_step.next()) begin
// The host jumped past a step. It does not do that by accident:
// something it read earlier told it not to bother, so the finding
// is UPSTREAM of the step that appears to be missing.
fail_step = s;
fail_reason = $sformatf("the host skipped from %s to %s -- look at what it read BEFORE %s, not at %s itself",
cur_step.name(), s.name(), cur_step.name(), s.name());
`uvm_warning("ENUM_SKIP", fail_reason)
note(s);
end else if (!p.ok) begin
// A step that FAILED is still a step that was REACHED. Naming it is
// the entire point, so the mark advances even though the step did
// not succeed -- see mutation N7.
note(s);
retries[s]++;
fail_step = s;
fail_reason = $sformatf("%s failed, attempt %0d", s.name(), retries[s]);
`uvm_info("ENUM_FAIL", fail_reason, UVM_MEDIUM)
end else begin
note(s);
end
end
endfunction
function enum_step_e classify(usb_bus_pkt p);
// ...maps a bus packet onto a step. Deliberately returns S_NONE for
// anything it does not recognise rather than guessing: a step number
// the sequence has no room for, accepted as progress, makes max_step
// name a step nobody attempted, which is worse than no diagnosis
// because it is a confident one.
return S_NONE;
endfunction
// ---- The report. This is the whole product of the component. ----
function void report_phase(uvm_phase phase);
if (max_step == S_ENUMERATED) begin
`uvm_info("ENUM", "enumeration completed", UVM_LOW)
return;
end
// NOT cur_step. At the end of a failed enumeration cur_step is
// S_ATTACH, every single time, and reporting it sends people to look
// at VBUS.
`uvm_error("ENUM",
$sformatf("enumeration never got past %s (attempted %0d time(s)); the current step reads %s, which is only the last retry's first step",
max_step.name(), retries.exists(max_step) ? retries[max_step] : 0,
cur_step.name()))
// And the pattern, which decides what you pick up next.
if (retries.size() == 1)
`uvm_info("ENUM",
"every failure was at the same step: this is deterministic -- a debugger and the firmware source",
UVM_LOW)
else if (retries.size() > 1)
`uvm_info("ENUM",
$sformatf("failures at %0d different steps: this is marginal -- a scope and a power supply",
retries.size()), UVM_LOW)
endfunction
endclass16. Common Misconceptions
"The state at the end of a failed enumeration is where it failed." It is the last retry's first step, and that is ATTACH every time.
"A bus reset should reset the tracker." It resets the sequence. The diagnosis must survive it, or there is no diagnosis.
"A detach is just another reset." It is a different device. It is the only event that may clear the furthest step.
"One retry counter is enough." Then "deterministic" and "marginal" look identical, and they need different instruments.
"A step that failed did not really happen." It was attempted, and naming it is the whole point. Not advancing the mark on a failure reports the previous step — plausible, and wrong.
"A skipped step means the trace is incomplete." Hosts do not skip by accident. Look upstream.
"A timeout is a kind of failure." A failure is an answer. Silence means firmware is not running.
"An unrecognised step id is harmless — just ignore it." Taken as progress it makes the report name a step nobody attempted, which is worse than silence because it is confident.
17. Exercises
1. N1 scores 26 000 and N7 scores 612, though N7 produces the more dangerous report. Explain the ratio from what each does to the visibility of the wrong answer, and say which you would rather ship.
2. Derive the number of bus resets a host performs before reporting "device not recognised", given RETRY_MAX, and say what cur_step reads at that moment for a failure at any step.
3. The step timer counts dead air rather than elapsed time. Construct the enumeration that a wall-clock timer would wrongly abandon, and say why it is common on a busy hub.
4. complete had to become sticky. Trace the exact cycle sequence that made a pulse report a timeout on a successful enumeration.
5. Add an eleventh step — reading the BOS descriptor — between steps 6 and 7. Say what changes in the SystemVerilog enum, and what breaks if the numbers are also in a log format.
6. The walkthrough's device reported bMaxPacketSize0 as 64 and then as 8. Which step does the tracker name, and what extra field would the tracker need to carry in order to name the inconsistency rather than the step?
18. Summary
| Idea | Why it matters |
|---|---|
| Enumeration is a sequence | and the failing step is most of the debug |
| A failure retries from the beginning | so the current step is always ATTACH |
| Record the furthest step, not the current one | that register is the diagnosis |
| A bus reset must not clear it | a retry restarts the sequence, not the finding |
| A detach is the only thing that may | it is a different device |
| A failed step still advances the mark | or the report is one step early and plausible |
| Retries are per step | deterministic and marginal need different instruments |
| A skipped step points upstream | hosts do not skip by accident |
| A timeout is not a failure | a failure is an answer; silence is not |
| An out-of-range step id is not progress | a confident wrong answer beats no answer, badly |
complete is a sticky state | or a successful enumeration reports a timeout |
| 176/176 step-by-input pairs | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o et_v.out et_v.v et_v_tb.v && ./et_v.out |
| SystemVerilog | iverilog -g2012 -o et_sv.out et_sv.sv et_sv_tb.sv && ./et_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a et_vhdl.vhd et_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_et_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_et_vhdl |
| One mutation | iverilog -g2005 -DMUT_N1 -o mm et_v_mut.v et_v_tb.v && ./mm |
All three implementations pass with 0 errors: every step driven as the failing step with a full retry-and-reset sequence around it, every current step crossed with every input combination, and both edges of the step timeout checked.
Chapter 25.2 — Descriptor Issues takes the failure the walkthrough above ended on. A descriptor is a self-describing tree, and its three length fields — bLength, wTotalLength, and the sum of the children — must agree. The host trusts wTotalLength and walks the buffer by bLength, so when they disagree it reads past the end of one descriptor and into the middle of the next, which produces a report about a field that is perfectly correct.
Continue learning
Related tutorials
- Related topic
Downstream Device Discovery
A hub cannot interrupt the host, so every port event waits to be asked for — and the window between the poll and the acknowledgement is where devices are silently lost.
- Related topic
Bus Power
A device's current allowance changes exactly once during enumeration — and bMaxPower is counted in 2 mA units, not milliamps.
- Related topic
Descriptor Engine
wLength is the size of the host's buffer, not a preference — and whether a zero-length packet must follow depends on comparing what was sent against what was asked for, not against what exists.
- Related topic
Descriptor Issues
A descriptor set is described three times by three different fields, and the host walks it by one while reading it by another — so when they disagree the error lands on a field that is perfectly correct.
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.
