USB · Module 24
UVM Architecture for USB
A USB environment has two agents on one wire, and the turnaround between them is the structural problem the whole architecture is built around — model it, or debug your own testbench for weeks.
This chapter assembles the module. The five components built so far all assumed something was feeding them; this one is about what feeds them, and about the single structural fact that shapes the whole environment.
1. Two Agents, One Wire
A USB environment has a host agent and a device agent, and they drive the same differential pair. Not two interfaces that happen to be connected — one wire, half duplex, with the two sides taking turns.
That is not an implementation detail of the driver. It is the shape of the architecture, and almost every question about a USB environment — where the monitor sits, what the sequencer may do, why the scoreboard sees what it sees — comes back to it.
2. The Turnaround Is Not Optional and It Is Not Zero
When the host finishes a token that requires a response, it stops driving and the device starts. Between those two events there is a gap:
the host's drivers must actually release the line
the line must settle
the device must have decoded the token and decided to answer
start EARLY -> both sides driving at once: contention,
the line at an undefined level, both sides
seeing corruption, and NEITHER seeing a cause
start LATE -> the host has already timed out, and reports
it as "the device did not answer"3. Ownership Is Derived, Not Negotiated
There is no arbitration on a USB bus. The host owns it by default and hands it over implicitly, by sending a packet that requires a response. The device never initiates anything.
So a device driving an idle bus is not "an agent that won arbitration early". It is an agent doing something no real device can do — and letting it through produces a stimulus the design will never see in silicon, which is worse than useless because it generates work.
4. The Machine
usb_bus_ownership — five states, and two of them are the gap
usb_bus_ownership #(TA_MIN = 2, TA_MAX = 8, HOLD_MAX = 16)
inputs outputs
------ -------
host_drive / dev_drive state 5 states
handover this packet owner NONE / HOST / DEV
needs a reply ta_age / hold_age
rel_bus finished grant_pulse / err_pulse
err_code CONTENTION / EARLY /
LATE / OVERHOLD / STOLEN
n_grants n_handovers n_contention
n_early n_late n_overhold n_stolen
`release` is a Verilog keyword, hence rel_bus.5. Contention Is Checked First, in Every State
Two agents driving one wire is not a state-machine transition. It is a physical fault, and once it has happened nothing else observed that cycle means anything.
// ---- CONTENTION is checked FIRST, in every state, before anything else.
//
// Two agents driving one wire is not a state-machine transition; it is a
// physical fault, and once it has happened nothing else observed that
// cycle means anything. Checking it inside the per-state logic -- which is
// the natural way to write this block -- means every state has to remember
// to check it, and one of them will not.
wire both_drive = host_drive && dev_drive;The natural way to write this block puts the check inside each state's logic, because that is where the drive signals are already being examined. Five states, five places to remember, and one of them will not. Hoisting it above the case statement makes it structurally impossible to miss — and the suite drives contention from all five states specifically to catch the version that did not.
6. Verilog-2005 Implementation
// usb_bus_ownership -- the structural problem a USB verification environment
// is built around, and the thing an environment that ignores it spends weeks
// debugging.
//
// TWO AGENTS, ONE WIRE
//
// A USB environment has a host agent and a device agent, and they drive the
// SAME differential pair. Not two interfaces that happen to be connected --
// one wire, half duplex, with the two sides taking turns.
//
// That is not an implementation detail of the driver. It is the shape of the
// whole architecture, and almost every question about a USB environment --
// where the monitor sits, what the sequencer may do, why the scoreboard sees
// what it sees -- comes back to it.
//
// THE TURNAROUND IS NOT OPTIONAL AND IT IS NOT ZERO
//
// When the host finishes a token that requires a response, it stops driving
// and the device starts. Between those two events there is a gap:
//
// the host's drivers must actually release the line
// the line must settle
// the device must have decoded the token and decided to answer
//
// Drive too EARLY and both sides are driving at once, which is contention:
// the line is at an undefined level, both sides see corruption, and NEITHER
// sees a cause. Drive too LATE and the host has already timed out.
//
// start early -> contention, and no symptom that points at it
// start late -> a timeout the host reports as "no response"
//
// AN ENVIRONMENT THAT DOES NOT MODEL IT DEBUGS ITSELF
//
// This is the practical consequence and it is worth stating bluntly. A
// two-agent environment whose drivers do not respect a turnaround produces
// contention that the real bus never would, on a schedule determined by
// testbench scheduling rather than by the design. The failures are
// intermittent, they move when you add a $display, and they look exactly
// like a design bug.
//
// Weeks go into that. The fix is a block like this one: ownership is a
// MODELLED, CHECKED property of the environment, and a driver that drives
// when it does not own the bus is a testbench error reported as such --
// immediately, with a name, and distinguishable from the design's faults.
//
// OWNERSHIP IS DERIVED, NOT NEGOTIATED
//
// There is no arbitration on a USB bus. The host owns it by default and
// hands it over IMPLICITLY, by sending a packet that requires a response.
// The device never initiates anything.
//
// So a device driving an idle bus is not "an agent that won arbitration
// early". It is an agent doing something no real device can do, and the
// environment must say so rather than letting it through and producing a
// stimulus the design will never see in silicon.
module usb_bus_ownership #(
parameter integer TA_MIN = 2, // cycles before the new owner may drive
parameter integer TA_MAX = 8, // cycles before the turnaround is dead
parameter integer HOLD_MAX = 16 // cycles an owner may drive continuously
) (
input wire clk,
input wire rst_n,
input wire host_drive, // the host agent is driving the wire
input wire dev_drive, // the device agent is driving the wire
input wire handover, // this packet requires a response
input wire rel_bus, // the current owner has finished
// (`release` is a Verilog keyword)
input wire eot,
output wire [2:0] state,
output wire [1:0] owner,
output wire [4:0] ta_age, // cycles spent in the turnaround
output wire [4:0] hold_age, // cycles the owner has been driving
output wire grant_pulse,
output wire err_pulse,
output wire [2:0] err_code,
output reg [31:0] n_grants,
output reg [31:0] n_handovers,
output reg [31:0] n_contention,
output reg [31:0] n_early,
output reg [31:0] n_late,
output reg [31:0] n_overhold,
output reg [31:0] n_stolen
);
localparam [2:0] B_IDLE = 3'd0, // nobody driving; the host may start
B_HOST = 3'd1, // the host owns the wire
B_TURN_DEV = 3'd2, // handed over; the device may start
// once TA_MIN cycles have passed
B_DEV = 3'd3, // the device owns the wire
B_TURN_HST = 3'd4; // returning; the host may start again
localparam [1:0] O_NONE = 2'd0, O_HOST = 2'd1, O_DEV = 2'd2;
localparam [2:0] E_NONE = 3'd0,
E_CONTENTION = 3'd1, // BOTH agents driving at once
E_EARLY = 3'd2, // drove before the turnaround ended
E_LATE = 3'd3, // the turnaround expired unanswered
E_OVERHOLD = 3'd4, // drove past HOLD_MAX
E_STOLEN = 3'd5; // drove without owning the wire
reg [2:0] st_r;
reg [4:0] ta_r, hd_r;
reg [2:0] ec_r;
reg gr_r, er_r;
assign state = st_r;
assign owner = (st_r == B_HOST) ? O_HOST
: (st_r == B_DEV) ? O_DEV
: O_NONE;
assign ta_age = ta_r;
assign hold_age = hd_r;
assign grant_pulse = gr_r;
assign err_pulse = er_r;
assign err_code = ec_r;
// ---- CONTENTION is checked FIRST, in every state, before anything else.
//
// Two agents driving one wire is not a state-machine transition; it is a
// physical fault, and once it has happened nothing else observed that
// cycle means anything. Checking it inside the per-state logic -- which is
// the natural way to write this block -- means every state has to remember
// to check it, and one of them will not.
wire both_drive = host_drive && dev_drive;
reg [2:0] st_n, ec_n;
reg [4:0] ta_n, hd_n;
reg gr_n, er_n, hv_n;
always @* begin
st_n = st_r;
ta_n = ta_r;
hd_n = hd_r;
ec_n = E_NONE;
gr_n = 1'b0;
er_n = 1'b0;
hv_n = 1'b0;
if (eot) begin
// End of test returns the wire to nobody. An agent still driving here
// is not reported as an error: the run is over, and the interesting
// report at this point is the counters.
st_n = B_IDLE;
ta_n = 5'd0;
hd_n = 5'd0;
end else if (both_drive) begin
// ---- CONTENTION. Both agents on the wire. ----
//
// The line is at an undefined level, both sides will see corruption,
// and neither will see a cause. Reported here and nowhere else, so it
// cannot be missed by a state that forgot to look.
er_n = 1'b1; ec_n = E_CONTENTION;
st_n = B_IDLE; ta_n = 5'd0; hd_n = 5'd0;
end else begin
case (st_r)
// ------------------------------------------------------------------
B_IDLE: begin
hd_n = 5'd0;
ta_n = 5'd0;
if (dev_drive) begin
// ---- A device driving an idle bus. ----
//
// There is no arbitration on this bus. The host owns it by
// default and the device never initiates, so this is an agent
// doing something no real device can do -- and letting it
// through produces a stimulus the design will never see.
er_n = 1'b1; ec_n = E_STOLEN;
end else if (host_drive) begin
st_n = B_HOST;
gr_n = 1'b1;
hd_n = 5'd1;
end
end
// ------------------------------------------------------------------
B_HOST: begin
if (dev_drive) begin
er_n = 1'b1; ec_n = E_STOLEN;
st_n = B_IDLE; hd_n = 5'd0;
end else if (hd_r >= HOLD_MAX[4:0]) begin
// ---- Driving past the hold bound. ----
//
// An agent that never stops is the worst of the failures here,
// because the other agent's response collides with it and the
// contention is attributed to whoever spoke second.
er_n = 1'b1; ec_n = E_OVERHOLD;
st_n = B_IDLE; hd_n = 5'd0;
end else if (handover) begin
// The packet requires a response: ownership passes, through a
// turnaround rather than directly.
st_n = B_TURN_DEV;
ta_n = 5'd0;
hd_n = 5'd0;
hv_n = 1'b1;
end else if (rel_bus) begin
st_n = B_IDLE;
hd_n = 5'd0;
end else if (host_drive) begin
hd_n = hd_r + 5'd1;
end
end
// ------------------------------------------------------------------
B_TURN_DEV: begin
if (host_drive) begin
// The host has handed over. Driving again now is contention
// waiting to happen.
er_n = 1'b1; ec_n = E_STOLEN;
st_n = B_IDLE; ta_n = 5'd0;
end else if (dev_drive && (ta_r < TA_MIN[4:0])) begin
// ---- TOO EARLY. ----
//
// The host's drivers have not released the line yet. This is
// the failure with no symptom that points at it: both sides see
// corruption and neither sees a cause.
er_n = 1'b1; ec_n = E_EARLY;
st_n = B_IDLE; ta_n = 5'd0;
end else if (dev_drive) begin
st_n = B_DEV;
gr_n = 1'b1;
hd_n = 5'd1;
ta_n = 5'd0;
end else if (ta_r >= TA_MAX[4:0]) begin
// ---- TOO LATE. The device never answered. ----
er_n = 1'b1; ec_n = E_LATE;
st_n = B_IDLE; ta_n = 5'd0;
end else begin
ta_n = ta_r + 5'd1;
end
end
// ------------------------------------------------------------------
B_DEV: begin
if (host_drive) begin
er_n = 1'b1; ec_n = E_STOLEN;
st_n = B_IDLE; hd_n = 5'd0;
end else if (hd_r >= HOLD_MAX[4:0]) begin
er_n = 1'b1; ec_n = E_OVERHOLD;
st_n = B_IDLE; hd_n = 5'd0;
end else if (rel_bus || handover) begin
// The device has finished. The wire goes back to the host, and
// it goes back through a turnaround for exactly the same reason
// it came the other way through one.
st_n = B_TURN_HST;
ta_n = 5'd0;
hd_n = 5'd0;
end else if (dev_drive) begin
hd_n = hd_r + 5'd1;
end
end
// ------------------------------------------------------------------
B_TURN_HST: begin
if (dev_drive) begin
er_n = 1'b1; ec_n = E_STOLEN;
st_n = B_IDLE; ta_n = 5'd0;
end else if (host_drive && (ta_r < TA_MIN[4:0])) begin
er_n = 1'b1; ec_n = E_EARLY;
st_n = B_IDLE; ta_n = 5'd0;
end else if (host_drive) begin
st_n = B_HOST;
gr_n = 1'b1;
hd_n = 5'd1;
ta_n = 5'd0;
end else if (ta_r >= TA_MAX[4:0]) begin
// The host not taking the wire back is not an error on the
// device's part, but it is a stalled bus, and it is reported
// for the same reason chapter 24.1's timeout is: nothing else
// in this block fires when NOTHING happens.
er_n = 1'b1; ec_n = E_LATE;
st_n = B_IDLE; ta_n = 5'd0;
end else begin
ta_n = ta_r + 5'd1;
end
end
default: st_n = B_IDLE;
endcase
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= B_IDLE;
ta_r <= 5'd0;
hd_r <= 5'd0;
ec_r <= E_NONE;
gr_r <= 1'b0;
er_r <= 1'b0;
n_grants <= 32'd0;
n_handovers <= 32'd0;
n_contention <= 32'd0;
n_early <= 32'd0;
n_late <= 32'd0;
n_overhold <= 32'd0;
n_stolen <= 32'd0;
end else begin
st_r <= st_n;
ta_r <= ta_n;
hd_r <= hd_n;
ec_r <= ec_n;
gr_r <= gr_n;
er_r <= er_n;
if (gr_n) n_grants <= n_grants + 32'd1;
if (hv_n) n_handovers <= n_handovers + 32'd1;
// The per-cause counters are driven by the SAME pulse as the error,
// so they sum to the error total by construction (chapter 23.4).
if (er_n) begin
case (ec_n)
E_CONTENTION: n_contention <= n_contention + 32'd1;
E_EARLY: n_early <= n_early + 32'd1;
E_LATE: n_late <= n_late + 32'd1;
E_OVERHOLD: n_overhold <= n_overhold + 32'd1;
E_STOLEN: n_stolen <= n_stolen + 32'd1;
default: ;
endcase
end
end
end
endmodule7. SystemVerilog Implementation
// usb_bus_ownership -- the structural problem a USB verification environment
// is built around, and the thing an environment that ignores it spends weeks
// debugging.
//
// TWO AGENTS, ONE WIRE
//
// A USB environment has a host agent and a device agent, and they drive the
// SAME differential pair. Not two interfaces that happen to be connected --
// one wire, half duplex, with the two sides taking turns.
//
// That is not an implementation detail of the driver. It is the shape of the
// whole architecture, and almost every question about a USB environment --
// where the monitor sits, what the sequencer may do, why the scoreboard sees
// what it sees -- comes back to it.
//
// THE TURNAROUND IS NOT OPTIONAL AND IT IS NOT ZERO
//
// When the host finishes a token that requires a response, it stops driving
// and the device starts. Between those two events there is a gap:
//
// the host's drivers must actually release the line
// the line must settle
// the device must have decoded the token and decided to answer
//
// Drive too EARLY and both sides are driving at once, which is contention:
// the line is at an undefined level, both sides see corruption, and NEITHER
// sees a cause. Drive too LATE and the host has already timed out.
//
// start early -> contention, and no symptom that points at it
// start late -> a timeout the host reports as "no response"
//
// AN ENVIRONMENT THAT DOES NOT MODEL IT DEBUGS ITSELF
//
// This is the practical consequence and it is worth stating bluntly. A
// two-agent environment whose drivers do not respect a turnaround produces
// contention that the real bus never would, on a schedule determined by
// testbench scheduling rather than by the design. The failures are
// intermittent, they move when you add a $display, and they look exactly
// like a design bug.
//
// Weeks go into that. The fix is a block like this one: ownership is a
// MODELLED, CHECKED property of the environment, and a driver that drives
// when it does not own the bus is a testbench error reported as such --
// immediately, with a name, and distinguishable from the design's faults.
//
// OWNERSHIP IS DERIVED, NOT NEGOTIATED
//
// There is no arbitration on a USB bus. The host owns it by default and
// hands it over IMPLICITLY, by sending a packet that requires a response.
// The device never initiates anything.
//
// So a device driving an idle bus is not "an agent that won arbitration
// early". It is an agent doing something no real device can do, and the
// environment must say so rather than letting it through and producing a
// stimulus the design will never see in silicon.
package usb_bus_pkg;
// The five bus states. The two turnaround states are not decoration: they
// are the states in which NOBODY owns the wire, and an environment whose
// ownership model has no such state will let an agent drive into the gap.
typedef enum logic [2:0] {
B_IDLE = 3'd0, // nobody driving; the host may start
B_HOST = 3'd1, // the host owns the wire
B_TURN_DEV = 3'd2, // handed over; the device may start once TA_MIN
// cycles have passed
B_DEV = 3'd3, // the device owns the wire
B_TURN_HST = 3'd4 // returning; the host may start again
} bus_state_e;
typedef enum logic [1:0] {
O_NONE = 2'd0, O_HOST = 2'd1, O_DEV = 2'd2
} owner_e;
typedef enum logic [2:0] {
E_NONE = 3'd0,
E_CONTENTION = 3'd1, // BOTH agents driving at once
E_EARLY = 3'd2, // drove before the turnaround ended
E_LATE = 3'd3, // the turnaround expired unanswered
E_OVERHOLD = 3'd4, // drove past HOLD_MAX
E_STOLEN = 3'd5 // drove without owning the wire
} bus_err_e;
endpackage
module usb_bus_ownership
import usb_bus_pkg::*;
#(
parameter int TA_MIN = 2, // cycles before the new owner may drive
parameter int TA_MAX = 8, // cycles before the turnaround is dead
parameter int HOLD_MAX = 16 // cycles an owner may drive continuously
) (
input logic clk,
input logic rst_n,
input logic host_drive, // the host agent is driving the wire
input logic dev_drive, // the device agent is driving the wire
input logic handover, // this packet requires a response
input logic rel_bus, // the current owner has finished
// (`release` is a Verilog keyword)
input logic eot,
output bus_state_e state,
output owner_e owner,
output logic [4:0] ta_age, // cycles spent in the turnaround
output logic [4:0] hold_age, // cycles the owner has been driving
output logic grant_pulse,
output logic err_pulse,
output bus_err_e err_code,
output logic [31:0] n_grants,
output logic [31:0] n_handovers,
output logic [31:0] n_contention,
output logic [31:0] n_early,
output logic [31:0] n_late,
output logic [31:0] n_overhold,
output logic [31:0] n_stolen
);
bus_state_e st_r;
bus_err_e ec_r;
logic [4:0] ta_r, hd_r;
logic gr_r, er_r;
assign state = st_r;
// Written as if/else rather than a ternary chain: an enum-valued ternary
// needs an explicit cast in Icarus, and the cast would turn a type error
// into a silent truncation.
always_comb begin
if (st_r == B_HOST) owner = O_HOST;
else if (st_r == B_DEV) owner = O_DEV;
else owner = O_NONE;
end
assign ta_age = ta_r;
assign hold_age = hd_r;
assign grant_pulse = gr_r;
assign err_pulse = er_r;
assign err_code = ec_r;
// ---- CONTENTION is checked FIRST, in every state, before anything else.
//
// Two agents driving one wire is not a state-machine transition; it is a
// physical fault, and once it has happened nothing else observed that
// cycle means anything. Checking it inside the per-state logic -- which is
// the natural way to write this block -- means every state has to remember
// to check it, and one of them will not.
logic both_drive;
assign both_drive = host_drive && dev_drive;
bus_state_e st_n;
bus_err_e ec_n;
logic [4:0] ta_n, hd_n;
logic gr_n, er_n, hv_n;
always_comb begin
st_n = st_r;
ta_n = ta_r;
hd_n = hd_r;
ec_n = E_NONE;
gr_n = 1'b0;
er_n = 1'b0;
hv_n = 1'b0;
if (eot) begin
// End of test returns the wire to nobody. An agent still driving here
// is not reported as an error: the run is over, and the interesting
// report at this point is the counters.
st_n = B_IDLE;
ta_n = 5'd0;
hd_n = 5'd0;
end else if (both_drive) begin
// ---- CONTENTION. Both agents on the wire. ----
//
// The line is at an undefined level, both sides will see corruption,
// and neither will see a cause. Reported here and nowhere else, so it
// cannot be missed by a state that forgot to look.
er_n = 1'b1; ec_n = E_CONTENTION;
st_n = B_IDLE; ta_n = 5'd0; hd_n = 5'd0;
end else begin
case (st_r)
// ------------------------------------------------------------------
B_IDLE: begin
hd_n = 5'd0;
ta_n = 5'd0;
if (dev_drive) begin
// ---- A device driving an idle bus. ----
//
// There is no arbitration on this bus. The host owns it by
// default and the device never initiates, so this is an agent
// doing something no real device can do -- and letting it
// through produces a stimulus the design will never see.
er_n = 1'b1; ec_n = E_STOLEN;
end else if (host_drive) begin
st_n = B_HOST;
gr_n = 1'b1;
hd_n = 5'd1;
end
end
// ------------------------------------------------------------------
B_HOST: begin
if (dev_drive) begin
er_n = 1'b1; ec_n = E_STOLEN;
st_n = B_IDLE; hd_n = 5'd0;
end else if (hd_r >= 5'(HOLD_MAX)) begin
// ---- Driving past the hold bound. ----
//
// An agent that never stops is the worst of the failures here,
// because the other agent's response collides with it and the
// contention is attributed to whoever spoke second.
er_n = 1'b1; ec_n = E_OVERHOLD;
st_n = B_IDLE; hd_n = 5'd0;
end else if (handover) begin
// The packet requires a response: ownership passes, through a
// turnaround rather than directly.
st_n = B_TURN_DEV;
ta_n = 5'd0;
hd_n = 5'd0;
hv_n = 1'b1;
end else if (rel_bus) begin
st_n = B_IDLE;
hd_n = 5'd0;
end else if (host_drive) begin
hd_n = hd_r + 5'd1;
end
end
// ------------------------------------------------------------------
B_TURN_DEV: begin
if (host_drive) begin
// The host has handed over. Driving again now is contention
// waiting to happen.
er_n = 1'b1; ec_n = E_STOLEN;
st_n = B_IDLE; ta_n = 5'd0;
end else if (dev_drive && (ta_r < 5'(TA_MIN))) begin
// ---- TOO EARLY. ----
//
// The host's drivers have not released the line yet. This is
// the failure with no symptom that points at it: both sides see
// corruption and neither sees a cause.
er_n = 1'b1; ec_n = E_EARLY;
st_n = B_IDLE; ta_n = 5'd0;
end else if (dev_drive) begin
st_n = B_DEV;
gr_n = 1'b1;
hd_n = 5'd1;
ta_n = 5'd0;
end else if (ta_r >= 5'(TA_MAX)) begin
// ---- TOO LATE. The device never answered. ----
er_n = 1'b1; ec_n = E_LATE;
st_n = B_IDLE; ta_n = 5'd0;
end else begin
ta_n = ta_r + 5'd1;
end
end
// ------------------------------------------------------------------
B_DEV: begin
if (host_drive) begin
er_n = 1'b1; ec_n = E_STOLEN;
st_n = B_IDLE; hd_n = 5'd0;
end else if (hd_r >= 5'(HOLD_MAX)) begin
er_n = 1'b1; ec_n = E_OVERHOLD;
st_n = B_IDLE; hd_n = 5'd0;
end else if (rel_bus || handover) begin
// The device has finished. The wire goes back to the host, and
// it goes back through a turnaround for exactly the same reason
// it came the other way through one.
st_n = B_TURN_HST;
ta_n = 5'd0;
hd_n = 5'd0;
end else if (dev_drive) begin
hd_n = hd_r + 5'd1;
end
end
// ------------------------------------------------------------------
B_TURN_HST: begin
if (dev_drive) begin
er_n = 1'b1; ec_n = E_STOLEN;
st_n = B_IDLE; ta_n = 5'd0;
end else if (host_drive && (ta_r < 5'(TA_MIN))) begin
er_n = 1'b1; ec_n = E_EARLY;
st_n = B_IDLE; ta_n = 5'd0;
end else if (host_drive) begin
st_n = B_HOST;
gr_n = 1'b1;
hd_n = 5'd1;
ta_n = 5'd0;
end else if (ta_r >= 5'(TA_MAX)) begin
// The host not taking the wire back is not an error on the
// device's part, but it is a stalled bus, and it is reported
// for the same reason chapter 24.1's timeout is: nothing else
// in this block fires when NOTHING happens.
er_n = 1'b1; ec_n = E_LATE;
st_n = B_IDLE; ta_n = 5'd0;
end else begin
ta_n = ta_r + 5'd1;
end
end
default: st_n = B_IDLE;
endcase
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= B_IDLE;
ta_r <= 5'd0;
hd_r <= 5'd0;
ec_r <= E_NONE;
gr_r <= 1'b0;
er_r <= 1'b0;
n_grants <= 32'd0;
n_handovers <= 32'd0;
n_contention <= 32'd0;
n_early <= 32'd0;
n_late <= 32'd0;
n_overhold <= 32'd0;
n_stolen <= 32'd0;
end else begin
st_r <= st_n;
ta_r <= ta_n;
hd_r <= hd_n;
ec_r <= ec_n;
gr_r <= gr_n;
er_r <= er_n;
if (gr_n) n_grants <= n_grants + 32'd1;
if (hv_n) n_handovers <= n_handovers + 32'd1;
// The per-cause counters are driven by the SAME pulse as the error,
// so they sum to the error total by construction (chapter 23.4).
if (er_n) begin
case (ec_n)
E_CONTENTION: n_contention <= n_contention + 32'd1;
E_EARLY: n_early <= n_early + 32'd1;
E_LATE: n_late <= n_late + 32'd1;
E_OVERHOLD: n_overhold <= n_overhold + 32'd1;
E_STOLEN: n_stolen <= n_stolen + 32'd1;
default: ;
endcase
end
end
end
endmodule8. VHDL-2008 Implementation
-- usb_bus_ownership -- the structural problem a USB verification environment
-- is built around, and the thing an environment that ignores it spends weeks
-- debugging.
--
-- TWO AGENTS, ONE WIRE
--
-- A USB environment has a host agent and a device agent, and they drive the
-- SAME differential pair. Not two interfaces that happen to be connected --
-- one wire, half duplex, with the two sides taking turns.
--
-- That is not an implementation detail of the driver. It is the shape of the
-- whole architecture, and almost every question about a USB environment --
-- where the monitor sits, what the sequencer may do, why the scoreboard sees
-- what it sees -- comes back to it.
--
-- THE TURNAROUND IS NOT OPTIONAL AND IT IS NOT ZERO
--
-- When the host finishes a token that requires a response, it stops driving
-- and the device starts. Between those two events there is a gap:
--
-- the host's drivers must actually release the line
-- the line must settle
-- the device must have decoded the token and decided to answer
--
-- Drive too EARLY and both sides are driving at once, which is contention:
-- the line is at an undefined level, both sides see corruption, and NEITHER
-- sees a cause. Drive too LATE and the host has already timed out.
--
-- start early -> contention, and no symptom that points at it
-- start late -> a timeout the host reports as "no response"
--
-- AN ENVIRONMENT THAT DOES NOT MODEL IT DEBUGS ITSELF
--
-- This is the practical consequence and it is worth stating bluntly. A
-- two-agent environment whose drivers do not respect a turnaround produces
-- contention that the real bus never would, on a schedule determined by
-- testbench scheduling rather than by the design. The failures are
-- intermittent, they move when you add a $display, and they look exactly
-- like a design bug.
--
-- Weeks go into that. The fix is a block like this one: ownership is a
-- MODELLED, CHECKED property of the environment, and a driver that drives
-- when it does not own the bus is a testbench error reported as such --
-- immediately, with a name, and distinguishable from the design's faults.
--
-- OWNERSHIP IS DERIVED, NOT NEGOTIATED
--
-- There is no arbitration on a USB bus. The host owns it by default and
-- hands it over IMPLICITLY, by sending a packet that requires a response.
-- The device never initiates anything.
--
-- So a device driving an idle bus is not "an agent that won arbitration
-- early". It is an agent doing something no real device can do, and the
-- environment must say so rather than letting it through and producing a
-- stimulus the design will never see in silicon.
library ieee;
use ieee.std_logic_1164.all;
package usb_bus_pkg is
-- The five bus states. The two turnaround states are not decoration: they
-- are the states in which NOBODY owns the wire, and an environment whose
-- ownership model has no such state will let an agent drive into the gap.
type bus_state_t is (B_IDLE, B_HOST, B_TURN_DEV, B_DEV, B_TURN_HST);
type owner_t is (O_NONE, O_HOST, O_DEV);
type bus_err_t is (E_NONE, E_CONTENTION, E_EARLY, E_LATE,
E_OVERHOLD, E_STOLEN);
function bs_code (s : bus_state_t) return std_logic_vector;
function ow_code (o : owner_t) return std_logic_vector;
function be_code (e : bus_err_t) return std_logic_vector;
end package usb_bus_pkg;
package body usb_bus_pkg is
function bs_code (s : bus_state_t) return std_logic_vector is
begin
case s is
when B_IDLE => return "000";
when B_HOST => return "001";
when B_TURN_DEV => return "010";
when B_DEV => return "011";
when B_TURN_HST => return "100";
end case;
end function;
function ow_code (o : owner_t) return std_logic_vector is
begin
case o is
when O_NONE => return "00";
when O_HOST => return "01";
when O_DEV => return "10";
end case;
end function;
function be_code (e : bus_err_t) return std_logic_vector is
begin
case e is
when E_NONE => return "000";
when E_CONTENTION => return "001";
when E_EARLY => return "010";
when E_LATE => return "011";
when E_OVERHOLD => return "100";
when E_STOLEN => return "101";
end case;
end function;
end package body usb_bus_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_bus_pkg.all;
entity usb_bus_ownership is
generic (
TA_MIN : integer := 2; -- cycles before the new owner may drive
TA_MAX : integer := 8; -- cycles before the turnaround is dead
HOLD_MAX : integer := 16 -- cycles an owner may drive continuously
);
port (
clk : in std_logic;
rst_n : in std_logic;
host_drive : in std_logic; -- the host agent is driving the wire
dev_drive : in std_logic; -- the device agent is driving the wire
handover : in std_logic; -- this packet requires a response
rel_bus : in std_logic; -- the current owner has finished
eot : in std_logic;
state : out std_logic_vector(2 downto 0);
owner : out std_logic_vector(1 downto 0);
ta_age : out std_logic_vector(4 downto 0);
hold_age : out std_logic_vector(4 downto 0);
grant_pulse : out std_logic;
err_pulse : out std_logic;
err_code : out std_logic_vector(2 downto 0);
n_grants : out std_logic_vector(31 downto 0);
n_handovers : out std_logic_vector(31 downto 0);
n_contention : out std_logic_vector(31 downto 0);
n_early : out std_logic_vector(31 downto 0);
n_late : out std_logic_vector(31 downto 0);
n_overhold : out std_logic_vector(31 downto 0);
n_stolen : out std_logic_vector(31 downto 0)
);
end entity usb_bus_ownership;
architecture rtl of usb_bus_ownership is
signal st_r : bus_state_t := B_IDLE;
signal ec_r : bus_err_t := E_NONE;
signal ta_r, hd_r : unsigned(4 downto 0) := (others => '0');
signal gr_r, er_r : std_logic := '0';
-- ---- CONTENTION is checked FIRST, in every state, before anything else.
--
-- Two agents driving one wire is not a state-machine transition; it is a
-- physical fault, and once it has happened nothing else observed that
-- cycle means anything. Checking it inside the per-state logic -- which is
-- the natural way to write this block -- means every state has to remember
-- to check it, and one of them will not.
signal both_drive : std_logic;
-- 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_g, c_h, c_c : unsigned(31 downto 0) := (others => '0');
signal c_e, c_l, c_o, c_s : unsigned(31 downto 0) := (others => '0');
begin
both_drive <= host_drive and dev_drive;
state <= bs_code(st_r);
owner <= ow_code(O_HOST) when st_r = B_HOST
else ow_code(O_DEV) when st_r = B_DEV
else ow_code(O_NONE);
ta_age <= std_logic_vector(ta_r);
hold_age <= std_logic_vector(hd_r);
grant_pulse <= gr_r;
err_pulse <= er_r;
err_code <= be_code(ec_r);
n_grants <= std_logic_vector(c_g);
n_handovers <= std_logic_vector(c_h);
n_contention <= std_logic_vector(c_c);
n_early <= std_logic_vector(c_e);
n_late <= std_logic_vector(c_l);
n_overhold <= std_logic_vector(c_o);
n_stolen <= std_logic_vector(c_s);
process (clk, rst_n)
variable ns : bus_state_t;
variable nec : bus_err_t;
variable nta, nhd : unsigned(4 downto 0);
variable ngr, ner, nhv : std_logic;
begin
if rst_n = '0' then
st_r <= B_IDLE;
ec_r <= E_NONE;
ta_r <= (others => '0');
hd_r <= (others => '0');
gr_r <= '0'; er_r <= '0';
c_g <= (others => '0'); c_h <= (others => '0');
c_c <= (others => '0'); c_e <= (others => '0');
c_l <= (others => '0'); c_o <= (others => '0');
c_s <= (others => '0');
elsif rising_edge(clk) then
ns := st_r; nta := ta_r; nhd := hd_r; nec := E_NONE;
ngr := '0'; ner := '0'; nhv := '0';
if eot = '1' then
-- End of test returns the wire to nobody. An agent still driving
-- here is not reported as an error: the run is over, and the
-- interesting report at this point is the counters.
ns := B_IDLE; nta := (others => '0'); nhd := (others => '0');
elsif both_drive = '1' then
-- ---- CONTENTION. Both agents on the wire. ----
--
-- The line is at an undefined level, both sides will see corruption,
-- and neither will see a cause. Reported here and nowhere else, so
-- it cannot be missed by a state that forgot to look.
ner := '1'; nec := E_CONTENTION;
ns := B_IDLE; nta := (others => '0'); nhd := (others => '0');
else
case st_r is
when B_IDLE =>
nhd := (others => '0');
nta := (others => '0');
if dev_drive = '1' then
-- ---- A device driving an idle bus. ----
--
-- There is no arbitration on this bus. The host owns it by
-- default and the device never initiates, so this is an agent
-- doing something no real device can do -- and letting it
-- through produces a stimulus the design will never see.
ner := '1'; nec := E_STOLEN;
elsif host_drive = '1' then
ns := B_HOST; ngr := '1'; nhd := to_unsigned(1, 5);
end if;
when B_HOST =>
if dev_drive = '1' then
ner := '1'; nec := E_STOLEN;
ns := B_IDLE; nhd := (others => '0');
elsif hd_r >= to_unsigned(HOLD_MAX, 5) then
-- ---- Driving past the hold bound. ----
--
-- An agent that never stops is the worst of the failures here,
-- because the other agent's response collides with it and the
-- contention is attributed to whoever spoke second.
ner := '1'; nec := E_OVERHOLD;
ns := B_IDLE; nhd := (others => '0');
elsif handover = '1' then
-- The packet requires a response: ownership passes, through a
-- turnaround rather than directly.
ns := B_TURN_DEV;
nta := (others => '0'); nhd := (others => '0'); nhv := '1';
elsif rel_bus = '1' then
ns := B_IDLE; nhd := (others => '0');
elsif host_drive = '1' then
nhd := hd_r + 1;
end if;
when B_TURN_DEV =>
if host_drive = '1' then
-- The host has handed over. Driving again now is contention
-- waiting to happen.
ner := '1'; nec := E_STOLEN;
ns := B_IDLE; nta := (others => '0');
elsif dev_drive = '1' and ta_r < to_unsigned(TA_MIN, 5) then
-- ---- TOO EARLY. ----
--
-- The host's drivers have not released the line yet. This is
-- the failure with no symptom that points at it: both sides
-- see corruption and neither sees a cause.
ner := '1'; nec := E_EARLY;
ns := B_IDLE; nta := (others => '0');
elsif dev_drive = '1' then
ns := B_DEV; ngr := '1';
nhd := to_unsigned(1, 5); nta := (others => '0');
elsif ta_r >= to_unsigned(TA_MAX, 5) then
-- ---- TOO LATE. The device never answered. ----
ner := '1'; nec := E_LATE;
ns := B_IDLE; nta := (others => '0');
else
nta := ta_r + 1;
end if;
when B_DEV =>
if host_drive = '1' then
ner := '1'; nec := E_STOLEN;
ns := B_IDLE; nhd := (others => '0');
elsif hd_r >= to_unsigned(HOLD_MAX, 5) then
ner := '1'; nec := E_OVERHOLD;
ns := B_IDLE; nhd := (others => '0');
elsif rel_bus = '1' or handover = '1' then
-- The device has finished. The wire goes back to the host, and
-- it goes back through a turnaround for exactly the same
-- reason it came the other way through one.
ns := B_TURN_HST;
nta := (others => '0'); nhd := (others => '0');
elsif dev_drive = '1' then
nhd := hd_r + 1;
end if;
when B_TURN_HST =>
if dev_drive = '1' then
ner := '1'; nec := E_STOLEN;
ns := B_IDLE; nta := (others => '0');
elsif host_drive = '1' and ta_r < to_unsigned(TA_MIN, 5) then
ner := '1'; nec := E_EARLY;
ns := B_IDLE; nta := (others => '0');
elsif host_drive = '1' then
ns := B_HOST; ngr := '1';
nhd := to_unsigned(1, 5); nta := (others => '0');
elsif ta_r >= to_unsigned(TA_MAX, 5) then
-- The host not taking the wire back is not an error on the
-- device's part, but it is a stalled bus, and it is reported
-- for the same reason chapter 24.1's timeout is: nothing else
-- in this block fires when NOTHING happens.
ner := '1'; nec := E_LATE;
ns := B_IDLE; nta := (others => '0');
else
nta := ta_r + 1;
end if;
end case;
end if;
st_r <= ns;
ta_r <= nta;
hd_r <= nhd;
ec_r <= nec;
gr_r <= ngr;
er_r <= ner;
if ngr = '1' then c_g <= c_g + 1; end if;
if nhv = '1' then c_h <= c_h + 1; end if;
-- The per-cause counters are driven by the SAME pulse as the error,
-- so they sum to the error total by construction (chapter 23.4).
if ner = '1' then
case nec is
when E_CONTENTION => c_c <= c_c + 1;
when E_EARLY => c_e <= c_e + 1;
when E_LATE => c_l <= c_l + 1;
when E_OVERHOLD => c_o <= c_o + 1;
when E_STOLEN => c_s <= c_s + 1;
when others => null;
end case;
end if;
end if;
end process;
end architecture rtl;9. A Complete Ownership Cycle, and One Drive Into the Gap
Handover, turnaround, response, return — and an early drive that is not one
usb_bus_ownership — one full cycle, then a drive into the turnaround
10 cyclesCycles 2 to 4 are the whole point of the block. Nobody owns the wire, ta_age is counting, and both agents must wait. An environment with no representation of that interval has nowhere to put it, and therefore does not have one.
10. The Testbenches
Two exhaustive sweeps — every bus state crossed with all 16 input combinations (80 pairs), and every turnaround age crossed with "the new owner drove" and "it did not" (18 pairs) — plus the checks that carry the chapter.
Both edges of the turnaround, at every age in the window:
if (a < TA_MIN) begin
check(n_early == b_e + 1,
"the device drove before the host's drivers could have released the line, and the adapter allowed it -- this is the failure with no symptom that points at it, because both sides see corruption and neither sees a cause");
check(n_grants == b_g, "an early drive was granted the wire");
end else begin
check(n_grants == b_g + 1,
"the device drove after the full turnaround and was not granted the wire");
check(n_early == b_e, "a legal handover was reported as early");
endContention from every state, because the version of this block that checks it per-state will have missed one:
check(state === st2[2:0],
"the sweep could not reach the bus state it meant to reach");
b_c = n_contention;
step(1'b1,1'b1,1'b0,1'b0,1'b0);
check(n_contention == b_c + 1,
"both agents drove the wire at once and it was not reported -- the line is at an undefined level, both sides see corruption, and neither sees a cause");And the invariant that makes the turnaround a real thing rather than a name:
// ---- Nobody owns the wire during a turnaround. That is what a
// ---- turnaround IS, and an environment whose ownership model says
// ---- otherwise will let an agent drive into it.
check(!(((m_st == B_TURN_DEV) || (m_st == B_TURN_HST))
&& (owner !== O_NONE)),
"the wire is owned during a turnaround -- the whole point of the gap is that nobody owns it");10.1 Verilog testbench
// Testbench for usb_bus_ownership (Verilog-2005).
//
// WHAT IS EXHAUSTIVE HERE
//
// Every one of the five bus states crossed with all 16 combinations of
// {host_drive, dev_drive, handover, rel_bus} = 80 pairs, each state
// reached by real ownership transfers rather than forced.
//
// And every turnaround age from 0 to TA_MAX crossed with "the new owner
// drove" and "it did not" = 18 pairs, which is what puts a drive attempt
// on every cycle of the window including both of its edges.
//
// THE TWO CHECKS THAT CARRY THE CHAPTER
//
// BOTH EDGES OF THE TURNAROUND. Driving at TA_MIN-1 must fail as EARLY
// and driving at TA_MIN must be granted. Checking only the first accepts
// a bus that never hands over at all; checking only the second accepts a
// bus with no turnaround.
//
// CONTENTION IS CHECKED IN EVERY STATE. Both agents driving at once is a
// physical fault, not a transition, and the sweep drives it from all five
// states -- because the natural way to write this block is to check it
// inside the per-state logic, where one state will forget.
`timescale 1ns/1ps
module tb_bo_v;
localparam integer TA_MIN = 2;
localparam integer TA_MAX = 8;
localparam integer HOLD_MAX = 16;
localparam [2:0] B_IDLE=3'd0, B_HOST=3'd1, B_TURN_DEV=3'd2,
B_DEV=3'd3, B_TURN_HST=3'd4;
localparam [1:0] O_NONE=2'd0, O_HOST=2'd1, O_DEV=2'd2;
localparam [2:0] E_NONE=3'd0, E_CONTENTION=3'd1, E_EARLY=3'd2,
E_LATE=3'd3, E_OVERHOLD=3'd4, E_STOLEN=3'd5;
reg clk = 1'b0, rst_n = 1'b0;
reg host_drive = 1'b0, dev_drive = 1'b0;
reg handover = 1'b0, rel_bus = 1'b0, eot = 1'b0;
wire [2:0] state, err_code;
wire [1:0] owner;
wire [4:0] ta_age, hold_age;
wire grant_pulse, err_pulse;
wire [31:0] n_grants, n_handovers, n_contention, n_early, n_late,
n_overhold, n_stolen;
usb_bus_ownership #(.TA_MIN(TA_MIN), .TA_MAX(TA_MAX), .HOLD_MAX(HOLD_MAX)) dut (
.clk(clk), .rst_n(rst_n),
.host_drive(host_drive), .dev_drive(dev_drive),
.handover(handover), .rel_bus(rel_bus), .eot(eot),
.state(state), .owner(owner), .ta_age(ta_age), .hold_age(hold_age),
.grant_pulse(grant_pulse), .err_pulse(err_pulse), .err_code(err_code),
.n_grants(n_grants), .n_handovers(n_handovers),
.n_contention(n_contention), .n_early(n_early), .n_late(n_late),
.n_overhold(n_overhold), .n_stolen(n_stolen)
);
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 | st=%0d own=%0d ta=%0d hd=%0d g=%b e=%b(%0d)",
$time, msg, state, owner, ta_age, hold_age,
grant_pulse, err_pulse, err_code);
end
end
endtask
// ------------------------------------------------------------------
// The shadow bus. Written from the protocol, not copied from the design.
// ------------------------------------------------------------------
reg [2:0] m_st, m_ec;
reg [4:0] m_ta, m_hd;
reg m_gr, m_er;
integer m_g, m_h, m_c, m_e, m_l, m_o, m_s;
integer seen_si [0:79]; // 5 states x 16 input combinations
integer seen_ta [0:17]; // (TA_MAX+1) x {the new owner drove, or not}
integer n_si, n_ta_s, n_steps;
task model_reset;
integer j;
begin
m_st = B_IDLE; m_ec = E_NONE; m_ta = 5'd0; m_hd = 5'd0;
m_gr = 1'b0; m_er = 1'b0;
m_g = 0; m_h = 0; m_c = 0; m_e = 0; m_l = 0; m_o = 0; m_s = 0;
for (j = 0; j < 80; j = j + 1) seen_si[j] = 0;
for (j = 0; j < 18; j = j + 1) seen_ta[j] = 0;
n_si = 0; n_ta_s = 0; n_steps = 0;
end
endtask
integer idx;
task step(input hd_i, input dd_i, input hv_i, input rb_i, input eo_i);
reg [2:0] ns, nec;
reg [4:0] nta, nhd;
reg ngr, ner, nhv, bothd, newdrv;
begin
host_drive = hd_i; dev_drive = dd_i;
handover = hv_i; rel_bus = rb_i; eot = eo_i;
#1;
check(state === m_st, "state disagrees with the shadow bus");
check(owner === ((m_st == B_HOST) ? O_HOST
: (m_st == B_DEV) ? O_DEV : O_NONE),
"owner disagrees with the state it is derived from");
check(ta_age === m_ta, "ta_age disagrees -- the turnaround is not the length the model says");
check(hold_age === m_hd, "hold_age disagrees");
check(err_code === m_ec, "err_code disagrees");
check(grant_pulse === m_gr, "the grant pulse disagrees");
check(err_pulse === m_er, "the error pulse disagrees");
check(!(grant_pulse && err_pulse),
"the wire was granted and reported faulty in the same cycle");
check(ta_age <= TA_MAX[4:0],
"the turnaround ran past its bound -- a turnaround that never expires is a bus that can stall for ever");
check(hold_age <= HOLD_MAX[4:0],
"an owner drove past the hold bound without being reported");
// ---- Nobody owns the wire during a turnaround. That is what a
// ---- turnaround IS, and an environment whose ownership model says
// ---- otherwise will let an agent drive into it.
check(!(((m_st == B_TURN_DEV) || (m_st == B_TURN_HST))
&& (owner !== O_NONE)),
"the wire is owned during a turnaround -- the whole point of the gap is that nobody owns it");
idx = m_st * 16 + (hd_i ? 8 : 0) + (dd_i ? 4 : 0) + (hv_i ? 2 : 0) + (rb_i ? 1 : 0);
if (idx < 80) begin
if (seen_si[idx] == 0) begin seen_si[idx] = 1; n_si = n_si + 1; end
end
if ((m_st == B_TURN_DEV) || (m_st == B_TURN_HST)) begin
newdrv = (m_st == B_TURN_DEV) ? dd_i : hd_i;
idx = m_ta * 2 + (newdrv ? 1 : 0);
if (idx < 18) begin
if (seen_ta[idx] == 0) begin seen_ta[idx] = 1; n_ta_s = n_ta_s + 1; end
end
end
n_steps = n_steps + 1;
// ---- advance the shadow bus ----
ns = m_st; nta = m_ta; nhd = m_hd; nec = E_NONE;
ngr = 1'b0; ner = 1'b0; nhv = 1'b0;
bothd = hd_i && dd_i;
if (eo_i) begin
ns = B_IDLE; nta = 5'd0; nhd = 5'd0;
end else if (bothd) begin
ner = 1'b1; nec = E_CONTENTION;
ns = B_IDLE; nta = 5'd0; nhd = 5'd0;
end else begin
case (m_st)
B_IDLE: begin
nhd = 5'd0; nta = 5'd0;
if (dd_i) begin ner = 1'b1; nec = E_STOLEN; end
else if (hd_i) begin ns = B_HOST; ngr = 1'b1; nhd = 5'd1; end
end
B_HOST: begin
if (dd_i) begin
ner = 1'b1; nec = E_STOLEN; ns = B_IDLE; nhd = 5'd0;
end else if (m_hd >= HOLD_MAX[4:0]) begin
ner = 1'b1; nec = E_OVERHOLD; ns = B_IDLE; nhd = 5'd0;
end else if (hv_i) begin
ns = B_TURN_DEV; nta = 5'd0; nhd = 5'd0; nhv = 1'b1;
end else if (rb_i) begin
ns = B_IDLE; nhd = 5'd0;
end else if (hd_i) begin
nhd = m_hd + 5'd1;
end
end
B_TURN_DEV: begin
if (hd_i) begin
ner = 1'b1; nec = E_STOLEN; ns = B_IDLE; nta = 5'd0;
end else if (dd_i && (m_ta < TA_MIN[4:0])) begin
ner = 1'b1; nec = E_EARLY; ns = B_IDLE; nta = 5'd0;
end else if (dd_i) begin
ns = B_DEV; ngr = 1'b1; nhd = 5'd1; nta = 5'd0;
end else if (m_ta >= TA_MAX[4:0]) begin
ner = 1'b1; nec = E_LATE; ns = B_IDLE; nta = 5'd0;
end else begin
nta = m_ta + 5'd1;
end
end
B_DEV: begin
if (hd_i) begin
ner = 1'b1; nec = E_STOLEN; ns = B_IDLE; nhd = 5'd0;
end else if (m_hd >= HOLD_MAX[4:0]) begin
ner = 1'b1; nec = E_OVERHOLD; ns = B_IDLE; nhd = 5'd0;
end else if (rb_i || hv_i) begin
ns = B_TURN_HST; nta = 5'd0; nhd = 5'd0;
end else if (dd_i) begin
nhd = m_hd + 5'd1;
end
end
default: begin // B_TURN_HST
if (dd_i) begin
ner = 1'b1; nec = E_STOLEN; ns = B_IDLE; nta = 5'd0;
end else if (hd_i && (m_ta < TA_MIN[4:0])) begin
ner = 1'b1; nec = E_EARLY; ns = B_IDLE; nta = 5'd0;
end else if (hd_i) begin
ns = B_HOST; ngr = 1'b1; nhd = 5'd1; nta = 5'd0;
end else if (m_ta >= TA_MAX[4:0]) begin
ner = 1'b1; nec = E_LATE; ns = B_IDLE; nta = 5'd0;
end else begin
nta = m_ta + 5'd1;
end
end
endcase
end
m_st = ns; m_ta = nta; m_hd = nhd; m_ec = nec;
m_gr = ngr; m_er = ner;
if (ngr) m_g = m_g + 1;
if (nhv) m_h = m_h + 1;
if (ner) begin
case (nec)
E_CONTENTION: m_c = m_c + 1;
E_EARLY: m_e = m_e + 1;
E_LATE: m_l = m_l + 1;
E_OVERHOLD: m_o = m_o + 1;
E_STOLEN: m_s = m_s + 1;
default: ;
endcase
end
@(posedge clk); #1;
host_drive = 1'b0; dev_drive = 1'b0;
handover = 1'b0; rel_bus = 1'b0; eot = 1'b0;
end
endtask
task idle(input integer n);
integer j;
begin for (j = 0; j < n; j = j + 1) step(1'b0,1'b0,1'b0,1'b0,1'b0); end
endtask
// Return the wire to nobody the way the design provides for.
task quiesce;
begin
step(1'b0,1'b0,1'b0,1'b0,1'b1);
idle(1);
check(state === B_IDLE, "the bus did not return to idle");
check(owner === O_NONE, "somebody still owns an idle wire");
end
endtask
integer k, b_c, b_e, b_l, b_o, b_s, b_g, b_h, st2, cb, a;
initial begin
model_reset;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(state === B_IDLE, "reset did not land on an idle bus");
check(owner === O_NONE, "reset left the wire owned");
check(err_pulse === 1'b0, "reset reported a bus fault");
// ---- Phase B: a complete, legal ownership cycle, repeatedly. ----
//
// host drives -> hands over -> turnaround -> device drives ->
// releases -> turnaround -> host drives again. ZERO errors.
for (k = 0; k < 60; k = k + 1) begin
b_g = n_grants; b_h = n_handovers;
b_c = n_contention + n_early + n_late + n_overhold + n_stolen;
step(1'b1,1'b0,1'b0,1'b0,1'b0); // host takes the wire
check(owner === O_HOST, "the host did not get an idle wire");
step(1'b1,1'b0,1'b1,1'b0,1'b0); // ...and hands over
check(state === B_TURN_DEV, "handover did not enter the turnaround");
idle(TA_MIN); // the gap
step(1'b0,1'b1,1'b0,1'b0,1'b0); // the device answers
check(owner === O_DEV, "the device did not get the wire after the turnaround");
step(1'b0,1'b1,1'b0,1'b1,1'b0); // ...and releases
check(state === B_TURN_HST, "release did not enter the return turnaround");
idle(TA_MIN);
step(1'b1,1'b0,1'b0,1'b0,1'b0); // the host takes it back
check(owner === O_HOST, "the host did not get the wire back");
step(1'b1,1'b0,1'b0,1'b1,1'b0); // and finishes
check(n_grants == b_g + 3, "a legal ownership cycle did not produce three grants");
check(n_handovers == b_h + 1, "the handover was not counted");
check(n_contention + n_early + n_late + n_overhold + n_stolen == b_c,
"a completely legal ownership cycle produced a bus fault -- an environment that flags legal traffic is an environment whose ownership model gets switched off");
quiesce;
end
// ---- Phase C: BOTH EDGES OF THE TURNAROUND. ----
//
// Driving at TA_MIN-1 is contention waiting to happen; driving at
// TA_MIN is the handover working. Checking only one of the two accepts
// either a bus with no turnaround or a bus that never hands over.
for (a = 0; a <= TA_MAX; a = a + 1) begin
quiesce;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); // into the turnaround
idle(a); // wait `a` cycles
b_e = n_early; b_g = n_grants; b_l = n_late;
if (a < TA_MAX) begin
step(1'b0,1'b1,1'b0,1'b0,1'b0); // the device drives
if (a < TA_MIN) begin
check(n_early == b_e + 1,
"the device drove before the host's drivers could have released the line, and the adapter allowed it -- this is the failure with no symptom that points at it, because both sides see corruption and neither sees a cause");
check(n_grants == b_g, "an early drive was granted the wire");
end else begin
check(n_grants == b_g + 1,
"the device drove after the full turnaround and was not granted the wire");
check(n_early == b_e, "a legal handover was reported as early");
end
end else begin
idle(1);
check(n_late == b_l + 1,
"the turnaround expired with nobody taking the wire and nothing was reported -- a turnaround that never expires is a bus that can stall for ever");
end
end
// ---- Phase D: CONTENTION, from every state. ----
//
// Two agents on one wire is a physical fault rather than a transition,
// so it is driven from all five states -- because the natural way to
// write this block is to check it inside the per-state logic, and one
// state will forget.
for (st2 = 0; st2 < 5; st2 = st2 + 1) begin
quiesce;
case (st2)
0: ;
1: step(1'b1,1'b0,1'b0,1'b0,1'b0);
2: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); end
3: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0); end
4: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0);
step(1'b0,1'b1,1'b0,1'b1,1'b0); end
endcase
check(state === st2[2:0],
"the sweep could not reach the bus state it meant to reach");
b_c = n_contention;
step(1'b1,1'b1,1'b0,1'b0,1'b0);
check(n_contention == b_c + 1,
"both agents drove the wire at once and it was not reported -- the line is at an undefined level, both sides see corruption, and neither sees a cause");
check(state === B_IDLE, "contention did not return the wire to nobody");
end
// ---- Phase E: a device driving an idle bus. ----
quiesce;
b_s = n_stolen;
step(1'b0,1'b1,1'b0,1'b0,1'b0);
check(n_stolen == b_s + 1,
"a device drove an idle wire and it was allowed -- there is no arbitration on this bus and no real device can do this, so the stimulus is one the design will never see");
// ---- Phase F: driving past the hold bound, on BOTH agents. ----
for (k = 0; k < 40; k = k + 1) begin
quiesce;
b_o = n_overhold;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
for (a = 0; a < HOLD_MAX + 2; a = a + 1) step(1'b1,1'b0,1'b0,1'b0,1'b0);
check(n_overhold == b_o + 1,
"an agent drove continuously past the hold bound and nothing was reported -- the other agent's response will collide with it and the contention will be blamed on whoever spoke second");
// ...and the device side, which is a separate branch of the design
// and therefore a separate thing to get wrong.
quiesce;
b_o = n_overhold;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0);
idle(TA_MIN);
for (a = 0; a < HOLD_MAX + 3; a = a + 1) step(1'b0,1'b1,1'b0,1'b0,1'b0);
check(n_overhold == b_o + 1,
"the device drove continuously past the hold bound and nothing was reported");
end
// ---- Phase F2: the turnaround expires, in BOTH directions. ----
//
// Nothing else in this block fires when NOTHING happens, which is the
// same argument as chapter 24.1's timeout: the bound on the turnaround
// is the only rule here that can notice a bus that has simply stalled.
for (k = 0; k < 40; k = k + 1) begin
quiesce;
b_l = n_late;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); // handed over to the device
idle(TA_MAX + 2); // ...which never answers
check(n_late == b_l + 1,
"the device never answered and the turnaround never expired -- a turnaround with no bound is a bus that can stall for ever, and nothing else in this block fires when nothing happens");
quiesce;
b_l = n_late;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0);
idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0);
step(1'b0,1'b1,1'b0,1'b1,1'b0); // returning to the host
idle(TA_MAX + 2); // ...which never takes it
check(n_late == b_l + 1,
"the host never took the wire back and the return turnaround never expired");
end
// ---- Phase G: EXHAUSTIVE. Every state x every input combination. ----
for (st2 = 0; st2 < 5; st2 = st2 + 1) begin
for (cb = 0; cb < 16; cb = cb + 1) begin
quiesce;
case (st2)
0: ;
1: step(1'b1,1'b0,1'b0,1'b0,1'b0);
2: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); end
3: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0); end
4: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0);
step(1'b0,1'b1,1'b0,1'b1,1'b0); end
endcase
check(state === st2[2:0],
"the sweep could not reach the bus state it meant to reach");
step(cb[3], cb[2], cb[1], cb[0], 1'b0);
step(cb[3], cb[2], cb[1], cb[0], 1'b0);
end
end
// ---- Phase H: every turnaround age, with and without the new owner
// ---- driving, so the window is swept rather than sampled.
for (a = 0; a <= TA_MAX; a = a + 1) begin
for (k = 0; k < 2; k = k + 1) begin
quiesce;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0);
idle(a);
step(1'b0, k[0], 1'b0, 1'b0, 1'b0);
idle(1);
end
end
// ---- Phase I: random ----
for (k = 0; k < 34000; k = k + 1)
step(($unsigned($random) % 100) < 34,
($unsigned($random) % 100) < 30,
($unsigned($random) % 100) < 18,
($unsigned($random) % 100) < 18,
($unsigned($random) % 1000) < 8);
// ---- Phase J: and a clean ownership cycle afterwards, so the model is
// ---- shown to still work rather than merely to have stopped.
quiesce;
b_g = n_grants;
b_c = n_contention + n_early + n_late + n_overhold + n_stolen;
for (k = 0; k < 80; k = k + 1) begin
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0);
idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0);
step(1'b0,1'b1,1'b0,1'b1,1'b0);
idle(TA_MIN);
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b0,1'b1,1'b0);
quiesce;
end
check(n_grants == b_g + 240, "the ownership model stopped granting the wire");
check(n_contention + n_early + n_late + n_overhold + n_stolen == b_c,
"clean ownership cycles after the random phase produced bus faults");
// ---- Final agreement ----
check(n_grants === m_g[31:0], "n_grants disagrees with the model");
check(n_handovers === m_h[31:0], "n_handovers disagrees");
check(n_contention === m_c[31:0], "n_contention disagrees");
check(n_early === m_e[31:0], "n_early disagrees");
check(n_late === m_l[31:0], "n_late disagrees");
check(n_overhold === m_o[31:0], "n_overhold disagrees");
check(n_stolen === m_s[31:0], "n_stolen disagrees");
check(n_si == 80, "not every bus state was crossed with every input combination");
check(n_ta_s == 18, "not every turnaround age was seen with and without the new owner driving");
check(n_grants > 32'd0, "the wire was never granted to anybody");
check(n_handovers > 32'd0, "ownership was never handed over");
check(n_contention > 32'd0, "contention was never exercised");
check(n_early > 32'd0, "an early drive was never exercised");
check(n_late > 32'd0, "a turnaround never expired");
check(n_overhold > 32'd0, "the hold bound was never exceeded");
check(n_stolen > 32'd0, "a non-owner never drove the wire");
$display("REACH state-x-input=%0d/80 turnaround-age=%0d/18 steps=%0d",
n_si, n_ta_s, n_steps);
$display("COUNTERS grants=%0d handovers=%0d contention=%0d early=%0d late=%0d overhold=%0d stolen=%0d",
n_grants, n_handovers, n_contention, n_early, n_late,
n_overhold, n_stolen);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule10.2 SystemVerilog testbench
// Testbench for usb_bus_ownership (SystemVerilog).
//
// WHAT IS EXHAUSTIVE HERE
//
// Every one of the five bus states crossed with all 16 combinations of
// {host_drive, dev_drive, handover, rel_bus} = 80 pairs, each state
// reached by real ownership transfers rather than forced.
//
// And every turnaround age from 0 to TA_MAX crossed with "the new owner
// drove" and "it did not" = 18 pairs, which is what puts a drive attempt
// on every cycle of the window including both of its edges.
//
// THE TWO CHECKS THAT CARRY THE CHAPTER
//
// BOTH EDGES OF THE TURNAROUND. Driving at TA_MIN-1 must fail as EARLY
// and driving at TA_MIN must be granted. Checking only the first accepts
// a bus that never hands over at all; checking only the second accepts a
// bus with no turnaround.
//
// CONTENTION IS CHECKED IN EVERY STATE. Both agents driving at once is a
// physical fault, not a transition, and the sweep drives it from all five
// states -- because the natural way to write this block is to check it
// inside the per-state logic, where one state will forget.
`timescale 1ns/1ps
module tb_bo_sv;
import usb_bus_pkg::*;
localparam int TA_MIN = 2;
localparam int TA_MAX = 8;
localparam int HOLD_MAX = 16;
logic clk = 1'b0, rst_n = 1'b0;
logic host_drive = 1'b0, dev_drive = 1'b0;
logic handover = 1'b0, rel_bus = 1'b0, eot = 1'b0;
bus_state_e state;
bus_err_e err_code;
owner_e owner;
logic [4:0] ta_age, hold_age;
logic grant_pulse, err_pulse;
logic [31:0] n_grants, n_handovers, n_contention, n_early, n_late,
n_overhold, n_stolen;
usb_bus_ownership #(.TA_MIN(TA_MIN), .TA_MAX(TA_MAX), .HOLD_MAX(HOLD_MAX))
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 | st=%0d own=%0d ta=%0d hd=%0d g=%b e=%b(%0d)",
$time, msg, state, owner, ta_age, hold_age,
grant_pulse, err_pulse, err_code);
end
end
endtask
// ------------------------------------------------------------------
// The shadow bus. Written from the protocol, not copied from the design.
// ------------------------------------------------------------------
bus_state_e m_st;
bus_err_e m_ec;
logic [4:0] m_ta, m_hd;
logic m_gr, m_er;
int m_g, m_h, m_c, m_e, m_l, m_o, m_s;
int seen_si [80]; // 5 states x 16 input combinations
int seen_ta [18]; // (TA_MAX+1) x {the new owner drove, or not}
int n_si, n_ta_s, n_steps;
task automatic model_reset();
int j;
begin
m_st = B_IDLE; m_ec = E_NONE; m_ta = 5'd0; m_hd = 5'd0;
m_gr = 1'b0; m_er = 1'b0;
m_g = 0; m_h = 0; m_c = 0; m_e = 0; m_l = 0; m_o = 0; m_s = 0;
for (j = 0; j < 80; j++) seen_si[j] = 0;
for (j = 0; j < 18; j++) seen_ta[j] = 0;
n_si = 0; n_ta_s = 0; n_steps = 0;
end
endtask
int idx;
task automatic step(input logic hd_i, input logic dd_i, input logic hv_i,
input logic rb_i, input logic eo_i);
bus_state_e ns;
bus_err_e nec;
logic [4:0] nta, nhd;
logic ngr, ner, nhv, bothd, newdrv;
begin
host_drive = hd_i; dev_drive = dd_i;
handover = hv_i; rel_bus = rb_i; eot = eo_i;
#1;
check(state === m_st, "state disagrees with the shadow bus");
check(owner === ((m_st == B_HOST) ? O_HOST
: (m_st == B_DEV) ? O_DEV : O_NONE),
"owner disagrees with the state it is derived from");
check(ta_age === m_ta, "ta_age disagrees -- the turnaround is not the length the model says");
check(hold_age === m_hd, "hold_age disagrees");
check(err_code === m_ec, "err_code disagrees");
check(grant_pulse === m_gr, "the grant pulse disagrees");
check(err_pulse === m_er, "the error pulse disagrees");
check(!(grant_pulse && err_pulse),
"the wire was granted and reported faulty in the same cycle");
check(ta_age <= 5'(TA_MAX),
"the turnaround ran past its bound -- a turnaround that never expires is a bus that can stall for ever");
check(hold_age <= 5'(HOLD_MAX),
"an owner drove past the hold bound without being reported");
// ---- Nobody owns the wire during a turnaround. That is what a
// ---- turnaround IS, and an environment whose ownership model says
// ---- otherwise will let an agent drive into it.
check(!(((m_st == B_TURN_DEV) || (m_st == B_TURN_HST))
&& (owner !== O_NONE)),
"the wire is owned during a turnaround -- the whole point of the gap is that nobody owns it");
idx = int'(m_st) * 16 + (hd_i ? 8 : 0) + (dd_i ? 4 : 0) + (hv_i ? 2 : 0) + (rb_i ? 1 : 0);
if (idx < 80) begin
if (seen_si[idx] == 0) begin seen_si[idx] = 1; n_si = n_si + 1; end
end
if ((m_st == B_TURN_DEV) || (m_st == B_TURN_HST)) begin
newdrv = (m_st == B_TURN_DEV) ? dd_i : hd_i;
idx = int'(m_ta) * 2 + (newdrv ? 1 : 0);
if (idx < 18) begin
if (seen_ta[idx] == 0) begin seen_ta[idx] = 1; n_ta_s = n_ta_s + 1; end
end
end
n_steps++;
// ---- advance the shadow bus ----
ns = m_st; nta = m_ta; nhd = m_hd; nec = E_NONE;
ngr = 1'b0; ner = 1'b0; nhv = 1'b0;
bothd = hd_i && dd_i;
if (eo_i) begin
ns = B_IDLE; nta = 5'd0; nhd = 5'd0;
end else if (bothd) begin
ner = 1'b1; nec = E_CONTENTION;
ns = B_IDLE; nta = 5'd0; nhd = 5'd0;
end else begin
case (m_st)
B_IDLE: begin
nhd = 5'd0; nta = 5'd0;
if (dd_i) begin ner = 1'b1; nec = E_STOLEN; end
else if (hd_i) begin ns = B_HOST; ngr = 1'b1; nhd = 5'd1; end
end
B_HOST: begin
if (dd_i) begin
ner = 1'b1; nec = E_STOLEN; ns = B_IDLE; nhd = 5'd0;
end else if (m_hd >= 5'(HOLD_MAX)) begin
ner = 1'b1; nec = E_OVERHOLD; ns = B_IDLE; nhd = 5'd0;
end else if (hv_i) begin
ns = B_TURN_DEV; nta = 5'd0; nhd = 5'd0; nhv = 1'b1;
end else if (rb_i) begin
ns = B_IDLE; nhd = 5'd0;
end else if (hd_i) begin
nhd = m_hd + 5'd1;
end
end
B_TURN_DEV: begin
if (hd_i) begin
ner = 1'b1; nec = E_STOLEN; ns = B_IDLE; nta = 5'd0;
end else if (dd_i && (m_ta < 5'(TA_MIN))) begin
ner = 1'b1; nec = E_EARLY; ns = B_IDLE; nta = 5'd0;
end else if (dd_i) begin
ns = B_DEV; ngr = 1'b1; nhd = 5'd1; nta = 5'd0;
end else if (m_ta >= 5'(TA_MAX)) begin
ner = 1'b1; nec = E_LATE; ns = B_IDLE; nta = 5'd0;
end else begin
nta = m_ta + 5'd1;
end
end
B_DEV: begin
if (hd_i) begin
ner = 1'b1; nec = E_STOLEN; ns = B_IDLE; nhd = 5'd0;
end else if (m_hd >= 5'(HOLD_MAX)) begin
ner = 1'b1; nec = E_OVERHOLD; ns = B_IDLE; nhd = 5'd0;
end else if (rb_i || hv_i) begin
ns = B_TURN_HST; nta = 5'd0; nhd = 5'd0;
end else if (dd_i) begin
nhd = m_hd + 5'd1;
end
end
default: begin // B_TURN_HST
if (dd_i) begin
ner = 1'b1; nec = E_STOLEN; ns = B_IDLE; nta = 5'd0;
end else if (hd_i && (m_ta < 5'(TA_MIN))) begin
ner = 1'b1; nec = E_EARLY; ns = B_IDLE; nta = 5'd0;
end else if (hd_i) begin
ns = B_HOST; ngr = 1'b1; nhd = 5'd1; nta = 5'd0;
end else if (m_ta >= 5'(TA_MAX)) begin
ner = 1'b1; nec = E_LATE; ns = B_IDLE; nta = 5'd0;
end else begin
nta = m_ta + 5'd1;
end
end
endcase
end
m_st = ns; m_ta = nta; m_hd = nhd; m_ec = nec;
m_gr = ngr; m_er = ner;
if (ngr) m_g = m_g + 1;
if (nhv) m_h = m_h + 1;
if (ner) begin
case (nec)
E_CONTENTION: m_c = m_c + 1;
E_EARLY: m_e = m_e + 1;
E_LATE: m_l = m_l + 1;
E_OVERHOLD: m_o = m_o + 1;
E_STOLEN: m_s = m_s + 1;
default: ;
endcase
end
@(posedge clk); #1;
host_drive = 1'b0; dev_drive = 1'b0;
handover = 1'b0; rel_bus = 1'b0; eot = 1'b0;
end
endtask
task automatic idle(input int n);
repeat (n) step(1'b0,1'b0,1'b0,1'b0,1'b0);
endtask
// Return the wire to nobody the way the design provides for.
task automatic quiesce();
begin
step(1'b0,1'b0,1'b0,1'b0,1'b1);
idle(1);
check(state === B_IDLE, "the bus did not return to idle");
check(owner === O_NONE, "somebody still owns an idle wire");
end
endtask
int k, b_c, b_e, b_l, b_o, b_s, b_g, b_h, st2, cb, a;
initial begin
model_reset();
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(state === B_IDLE, "reset did not land on an idle bus");
check(owner === O_NONE, "reset left the wire owned");
check(err_pulse === 1'b0, "reset reported a bus fault");
// ---- Phase B: a complete, legal ownership cycle, repeatedly. ----
//
// host drives -> hands over -> turnaround -> device drives ->
// releases -> turnaround -> host drives again. ZERO errors.
for (k = 0; k < 60; k++) begin
b_g = n_grants; b_h = n_handovers;
b_c = n_contention + n_early + n_late + n_overhold + n_stolen;
step(1'b1,1'b0,1'b0,1'b0,1'b0); // host takes the wire
check(owner === O_HOST, "the host did not get an idle wire");
step(1'b1,1'b0,1'b1,1'b0,1'b0); // ...and hands over
check(state === B_TURN_DEV, "handover did not enter the turnaround");
idle(TA_MIN); // the gap
step(1'b0,1'b1,1'b0,1'b0,1'b0); // the device answers
check(owner === O_DEV, "the device did not get the wire after the turnaround");
step(1'b0,1'b1,1'b0,1'b1,1'b0); // ...and releases
check(state === B_TURN_HST, "release did not enter the return turnaround");
idle(TA_MIN);
step(1'b1,1'b0,1'b0,1'b0,1'b0); // the host takes it back
check(owner === O_HOST, "the host did not get the wire back");
step(1'b1,1'b0,1'b0,1'b1,1'b0); // and finishes
check(n_grants == b_g + 3, "a legal ownership cycle did not produce three grants");
check(n_handovers == b_h + 1, "the handover was not counted");
check(n_contention + n_early + n_late + n_overhold + n_stolen == b_c,
"a completely legal ownership cycle produced a bus fault -- an environment that flags legal traffic is an environment whose ownership model gets switched off");
quiesce();
end
// ---- Phase C: BOTH EDGES OF THE TURNAROUND. ----
//
// Driving at TA_MIN-1 is contention waiting to happen; driving at
// TA_MIN is the handover working. Checking only one of the two accepts
// either a bus with no turnaround or a bus that never hands over.
for (a = 0; a <= TA_MAX; a++) begin
quiesce();
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); // into the turnaround
idle(a); // wait `a` cycles
b_e = n_early; b_g = n_grants; b_l = n_late;
if (a < TA_MAX) begin
step(1'b0,1'b1,1'b0,1'b0,1'b0); // the device drives
if (a < TA_MIN) begin
check(n_early == b_e + 1,
"the device drove before the host's drivers could have released the line, and the adapter allowed it -- this is the failure with no symptom that points at it, because both sides see corruption and neither sees a cause");
check(n_grants == b_g, "an early drive was granted the wire");
end else begin
check(n_grants == b_g + 1,
"the device drove after the full turnaround and was not granted the wire");
check(n_early == b_e, "a legal handover was reported as early");
end
end else begin
idle(1);
check(n_late == b_l + 1,
"the turnaround expired with nobody taking the wire and nothing was reported -- a turnaround that never expires is a bus that can stall for ever");
end
end
// ---- Phase D: CONTENTION, from every state. ----
//
// Two agents on one wire is a physical fault rather than a transition,
// so it is driven from all five states -- because the natural way to
// write this block is to check it inside the per-state logic, and one
// state will forget.
for (st2 = 0; st2 < 5; st2++) begin
quiesce();
case (st2)
0: ;
1: step(1'b1,1'b0,1'b0,1'b0,1'b0);
2: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); end
3: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0); end
4: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0);
step(1'b0,1'b1,1'b0,1'b1,1'b0); end
endcase
check(state === bus_state_e'(st2),
"the sweep could not reach the bus state it meant to reach");
b_c = n_contention;
step(1'b1,1'b1,1'b0,1'b0,1'b0);
check(n_contention == b_c + 1,
"both agents drove the wire at once and it was not reported -- the line is at an undefined level, both sides see corruption, and neither sees a cause");
check(state === B_IDLE, "contention did not return the wire to nobody");
end
// ---- Phase E: a device driving an idle bus. ----
quiesce();
b_s = n_stolen;
step(1'b0,1'b1,1'b0,1'b0,1'b0);
check(n_stolen == b_s + 1,
"a device drove an idle wire and it was allowed -- there is no arbitration on this bus and no real device can do this, so the stimulus is one the design will never see");
// ---- Phase F: driving past the hold bound, on BOTH agents. ----
for (k = 0; k < 40; k++) begin
quiesce();
b_o = n_overhold;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
for (a = 0; a < HOLD_MAX + 2; a++) step(1'b1,1'b0,1'b0,1'b0,1'b0);
check(n_overhold == b_o + 1,
"an agent drove continuously past the hold bound and nothing was reported -- the other agent's response will collide with it and the contention will be blamed on whoever spoke second");
// ...and the device side, which is a separate branch of the design
// and therefore a separate thing to get wrong.
quiesce();
b_o = n_overhold;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0);
idle(TA_MIN);
for (a = 0; a < HOLD_MAX + 3; a++) step(1'b0,1'b1,1'b0,1'b0,1'b0);
check(n_overhold == b_o + 1,
"the device drove continuously past the hold bound and nothing was reported");
end
// ---- Phase F2: the turnaround expires, in BOTH directions. ----
//
// Nothing else in this block fires when NOTHING happens, which is the
// same argument as chapter 24.1's timeout: the bound on the turnaround
// is the only rule here that can notice a bus that has simply stalled.
for (k = 0; k < 40; k++) begin
quiesce();
b_l = n_late;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); // handed over to the device
idle(TA_MAX + 2); // ...which never answers
check(n_late == b_l + 1,
"the device never answered and the turnaround never expired -- a turnaround with no bound is a bus that can stall for ever, and nothing else in this block fires when nothing happens");
quiesce();
b_l = n_late;
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0);
idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0);
step(1'b0,1'b1,1'b0,1'b1,1'b0); // returning to the host
idle(TA_MAX + 2); // ...which never takes it
check(n_late == b_l + 1,
"the host never took the wire back and the return turnaround never expired");
end
// ---- Phase G: EXHAUSTIVE. Every state x every input combination. ----
for (st2 = 0; st2 < 5; st2++) begin
for (cb = 0; cb < 16; cb++) begin
quiesce();
case (st2)
0: ;
1: step(1'b1,1'b0,1'b0,1'b0,1'b0);
2: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); end
3: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0); end
4: begin step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0); idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0);
step(1'b0,1'b1,1'b0,1'b1,1'b0); end
endcase
check(state === bus_state_e'(st2),
"the sweep could not reach the bus state it meant to reach");
step(1'(cb[3]), 1'(cb[2]), 1'(cb[1]), 1'(cb[0]), 1'b0);
step(1'(cb[3]), 1'(cb[2]), 1'(cb[1]), 1'(cb[0]), 1'b0);
end
end
// ---- Phase H: every turnaround age, with and without the new owner
// ---- driving, so the window is swept rather than sampled.
for (a = 0; a <= TA_MAX; a++) begin
for (k = 0; k < 2; k++) begin
quiesce();
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0);
idle(a);
step(1'b0, 1'(k[0]), 1'b0, 1'b0, 1'b0);
idle(1);
end
end
// ---- Phase I: random ----
for (k = 0; k < 34000; k++)
step($urandom_range(0,99) < 34,
$urandom_range(0,99) < 30,
$urandom_range(0,99) < 18,
$urandom_range(0,99) < 18,
$urandom_range(0,999) < 8);
// ---- Phase J: and a clean ownership cycle afterwards, so the model is
// ---- shown to still work rather than merely to have stopped.
quiesce();
b_g = n_grants;
b_c = n_contention + n_early + n_late + n_overhold + n_stolen;
for (k = 0; k < 80; k++) begin
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b1,1'b0,1'b0);
idle(TA_MIN);
step(1'b0,1'b1,1'b0,1'b0,1'b0);
step(1'b0,1'b1,1'b0,1'b1,1'b0);
idle(TA_MIN);
step(1'b1,1'b0,1'b0,1'b0,1'b0);
step(1'b1,1'b0,1'b0,1'b1,1'b0);
quiesce();
end
check(n_grants == b_g + 240, "the ownership model stopped granting the wire");
check(n_contention + n_early + n_late + n_overhold + n_stolen == b_c,
"clean ownership cycles after the random phase produced bus faults");
// ---- Final agreement ----
check(n_grants === 32'(m_g), "n_grants disagrees with the model");
check(n_handovers === 32'(m_h), "n_handovers disagrees");
check(n_contention === 32'(m_c), "n_contention disagrees");
check(n_early === 32'(m_e), "n_early disagrees");
check(n_late === 32'(m_l), "n_late disagrees");
check(n_overhold === 32'(m_o), "n_overhold disagrees");
check(n_stolen === 32'(m_s), "n_stolen disagrees");
check(n_si == 80, "not every bus state was crossed with every input combination");
check(n_ta_s == 18, "not every turnaround age was seen with and without the new owner driving");
check(n_grants > 32'd0, "the wire was never granted to anybody");
check(n_handovers > 32'd0, "ownership was never handed over");
check(n_contention > 32'd0, "contention was never exercised");
check(n_early > 32'd0, "an early drive was never exercised");
check(n_late > 32'd0, "a turnaround never expired");
check(n_overhold > 32'd0, "the hold bound was never exceeded");
check(n_stolen > 32'd0, "a non-owner never drove the wire");
$display("REACH state-x-input=%0d/80 turnaround-age=%0d/18 steps=%0d",
n_si, n_ta_s, n_steps);
$display("COUNTERS grants=%0d handovers=%0d contention=%0d early=%0d late=%0d overhold=%0d stolen=%0d",
n_grants, n_handovers, n_contention, n_early, n_late,
n_overhold, n_stolen);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule10.3 VHDL testbench
-- Testbench for usb_bus_ownership (VHDL-2008).
--
-- WHAT IS EXHAUSTIVE HERE
--
-- Every one of the five bus states crossed with all 16 combinations of
-- (host_drive, dev_drive, handover, rel_bus) = 80 pairs, each state
-- reached by real ownership transfers rather than forced.
--
-- And every turnaround age from 0 to TA_MAX crossed with "the new owner
-- drove" and "it did not" = 18 pairs, which is what puts a drive attempt
-- on every cycle of the window including both of its edges.
--
-- THE TWO CHECKS THAT CARRY THE CHAPTER
--
-- BOTH EDGES OF THE TURNAROUND. Driving at TA_MIN-1 must fail as EARLY
-- and driving at TA_MIN must be granted. Checking only the first accepts
-- a bus that never hands over at all; checking only the second accepts a
-- bus with no turnaround.
--
-- CONTENTION IS CHECKED IN EVERY STATE. Both agents driving at once is a
-- physical fault, not a transition, and the sweep drives it from all five
-- states -- because the natural way to write this block is to check it
-- inside the per-state logic, where one state will forget.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_bus_pkg.all;
entity tb_bo_vhdl is
end entity tb_bo_vhdl;
architecture sim of tb_bo_vhdl is
constant TA_MIN : integer := 2;
constant TA_MAX : integer := 8;
constant HOLD_MAX : integer := 16;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal host_drive, dev_drive : std_logic := '0';
signal handover, rel_bus, eot : std_logic := '0';
signal state, err_code : std_logic_vector(2 downto 0);
signal owner : std_logic_vector(1 downto 0);
signal ta_age, hold_age : std_logic_vector(4 downto 0);
signal grant_pulse, err_pulse : std_logic;
signal n_grants, n_handovers, n_contention : std_logic_vector(31 downto 0);
signal n_early, n_late, n_overhold, n_stolen : std_logic_vector(31 downto 0);
begin
dut : entity work.usb_bus_ownership
generic map (TA_MIN => TA_MIN, TA_MAX => TA_MAX, HOLD_MAX => HOLD_MAX)
port map (
clk => clk, rst_n => rst_n,
host_drive => host_drive, dev_drive => dev_drive,
handover => handover, rel_bus => rel_bus, eot => eot,
state => state, owner => owner, ta_age => ta_age, hold_age => hold_age,
grant_pulse => grant_pulse, err_pulse => err_pulse, err_code => err_code,
n_grants => n_grants, n_handovers => n_handovers,
n_contention => n_contention, n_early => n_early, n_late => n_late,
n_overhold => n_overhold, n_stolen => n_stolen
);
clk <= (not clk) after 5 ns when not done else '0';
stim : process
type si_arr is array (0 to 79) of integer;
type ta_arr is array (0 to 17) of integer;
variable errors, checks : integer := 0;
-- ---- The shadow bus. Written from the protocol, not the design. ----
variable m_st : bus_state_t := B_IDLE;
variable m_ec : bus_err_t := E_NONE;
variable m_ta, m_hd : unsigned(4 downto 0) := (others => '0');
variable m_gr, m_er : std_logic := '0';
variable m_g, m_h, m_c, m_e, m_l, m_o, m_s : integer := 0;
variable seen_si : si_arr := (others => 0);
variable seen_ta : ta_arr := (others => 0);
variable n_si, n_ta_s, n_steps : integer := 0;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"B0BACAFE";
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 &
" | st=" & integer'image(bus_state_t'pos(m_st)) &
" ta=" & integer'image(to_integer(m_ta)) &
" hd=" & integer'image(to_integer(m_hd)) &
" ec=" & integer'image(bus_err_t'pos(m_ec))
severity note;
end if;
end if;
end procedure;
function own_of (s : bus_state_t) return owner_t is
begin
if s = B_HOST then return O_HOST;
elsif s = B_DEV then return O_DEV;
else return O_NONE;
end if;
end function;
procedure step (hd_i, dd_i, hv_i, rb_i, eo_i : std_logic) is
variable nst : bus_state_t;
variable nec : bus_err_t;
variable nta, nhd : unsigned(4 downto 0);
variable ngr, ner, nhv, bothd, newdrv : std_logic;
variable idx : integer;
begin
host_drive <= hd_i; dev_drive <= dd_i;
handover <= hv_i; rel_bus <= rb_i; eot <= eo_i;
wait for 1 ns;
chk(state = bs_code(m_st), "state disagrees with the shadow bus");
chk(owner = ow_code(own_of(m_st)),
"owner disagrees with the state it is derived from");
chk(unsigned(ta_age) = m_ta,
"ta_age disagrees -- the turnaround is not the length the model says");
chk(unsigned(hold_age) = m_hd, "hold_age disagrees");
chk(err_code = be_code(m_ec), "err_code disagrees");
chk(grant_pulse = m_gr, "the grant pulse disagrees");
chk(err_pulse = m_er, "the error pulse disagrees");
chk(not (grant_pulse = '1' and err_pulse = '1'),
"the wire was granted and reported faulty in the same cycle");
chk(unsigned(ta_age) <= to_unsigned(TA_MAX, 5),
"the turnaround ran past its bound -- a turnaround that never expires is a bus that can stall for ever");
chk(unsigned(hold_age) <= to_unsigned(HOLD_MAX, 5),
"an owner drove past the hold bound without being reported");
-- ---- Nobody owns the wire during a turnaround. That is what a
-- ---- turnaround IS, and an environment whose ownership model says
-- ---- otherwise will let an agent drive into it.
chk(not ((m_st = B_TURN_DEV or m_st = B_TURN_HST)
and own_of(m_st) /= O_NONE),
"the wire is owned during a turnaround -- the whole point of the gap is that nobody owns it");
idx := bus_state_t'pos(m_st) * 16;
if hd_i = '1' then idx := idx + 8; end if;
if dd_i = '1' then idx := idx + 4; end if;
if hv_i = '1' then idx := idx + 2; end if;
if rb_i = '1' then idx := idx + 1; end if;
if idx < 80 then
if seen_si(idx) = 0 then seen_si(idx) := 1; n_si := n_si + 1; end if;
end if;
if m_st = B_TURN_DEV or m_st = B_TURN_HST then
if m_st = B_TURN_DEV then newdrv := dd_i; else newdrv := hd_i; end if;
idx := to_integer(m_ta) * 2;
if newdrv = '1' then idx := idx + 1; end if;
if idx < 18 then
if seen_ta(idx) = 0 then seen_ta(idx) := 1; n_ta_s := n_ta_s + 1; end if;
end if;
end if;
n_steps := n_steps + 1;
-- ---- advance the shadow bus ----
nst := m_st; nta := m_ta; nhd := m_hd; nec := E_NONE;
ngr := '0'; ner := '0'; nhv := '0';
bothd := hd_i and dd_i;
if eo_i = '1' then
nst := B_IDLE; nta := (others => '0'); nhd := (others => '0');
elsif bothd = '1' then
ner := '1'; nec := E_CONTENTION;
nst := B_IDLE; nta := (others => '0'); nhd := (others => '0');
else
case m_st is
when B_IDLE =>
nhd := (others => '0'); nta := (others => '0');
if dd_i = '1' then
ner := '1'; nec := E_STOLEN;
elsif hd_i = '1' then
nst := B_HOST; ngr := '1'; nhd := to_unsigned(1, 5);
end if;
when B_HOST =>
if dd_i = '1' then
ner := '1'; nec := E_STOLEN; nst := B_IDLE; nhd := (others => '0');
elsif m_hd >= to_unsigned(HOLD_MAX, 5) then
ner := '1'; nec := E_OVERHOLD; nst := B_IDLE; nhd := (others => '0');
elsif hv_i = '1' then
nst := B_TURN_DEV; nta := (others => '0');
nhd := (others => '0'); nhv := '1';
elsif rb_i = '1' then
nst := B_IDLE; nhd := (others => '0');
elsif hd_i = '1' then
nhd := m_hd + 1;
end if;
when B_TURN_DEV =>
if hd_i = '1' then
ner := '1'; nec := E_STOLEN; nst := B_IDLE; nta := (others => '0');
elsif dd_i = '1' and m_ta < to_unsigned(TA_MIN, 5) then
ner := '1'; nec := E_EARLY; nst := B_IDLE; nta := (others => '0');
elsif dd_i = '1' then
nst := B_DEV; ngr := '1';
nhd := to_unsigned(1, 5); nta := (others => '0');
elsif m_ta >= to_unsigned(TA_MAX, 5) then
ner := '1'; nec := E_LATE; nst := B_IDLE; nta := (others => '0');
else
nta := m_ta + 1;
end if;
when B_DEV =>
if hd_i = '1' then
ner := '1'; nec := E_STOLEN; nst := B_IDLE; nhd := (others => '0');
elsif m_hd >= to_unsigned(HOLD_MAX, 5) then
ner := '1'; nec := E_OVERHOLD; nst := B_IDLE; nhd := (others => '0');
elsif rb_i = '1' or hv_i = '1' then
nst := B_TURN_HST; nta := (others => '0'); nhd := (others => '0');
elsif dd_i = '1' then
nhd := m_hd + 1;
end if;
when B_TURN_HST =>
if dd_i = '1' then
ner := '1'; nec := E_STOLEN; nst := B_IDLE; nta := (others => '0');
elsif hd_i = '1' and m_ta < to_unsigned(TA_MIN, 5) then
ner := '1'; nec := E_EARLY; nst := B_IDLE; nta := (others => '0');
elsif hd_i = '1' then
nst := B_HOST; ngr := '1';
nhd := to_unsigned(1, 5); nta := (others => '0');
elsif m_ta >= to_unsigned(TA_MAX, 5) then
ner := '1'; nec := E_LATE; nst := B_IDLE; nta := (others => '0');
else
nta := m_ta + 1;
end if;
end case;
end if;
m_st := nst; m_ta := nta; m_hd := nhd; m_ec := nec;
m_gr := ngr; m_er := ner;
if ngr = '1' then m_g := m_g + 1; end if;
if nhv = '1' then m_h := m_h + 1; end if;
if ner = '1' then
case nec is
when E_CONTENTION => m_c := m_c + 1;
when E_EARLY => m_e := m_e + 1;
when E_LATE => m_l := m_l + 1;
when E_OVERHOLD => m_o := m_o + 1;
when E_STOLEN => m_s := m_s + 1;
when others => null;
end case;
end if;
wait until rising_edge(clk);
wait for 1 ns;
host_drive <= '0'; dev_drive <= '0';
handover <= '0'; rel_bus <= '0'; eot <= '0';
end procedure;
procedure idle (n : integer) is
begin
for j in 1 to n loop
step('0','0','0','0','0');
end loop;
end procedure;
-- Return the wire to nobody the way the design provides for.
procedure quiesce is
begin
step('0','0','0','0','1');
idle(1);
chk(state = bs_code(B_IDLE), "the bus did not return to idle");
chk(owner = ow_code(O_NONE), "somebody still owns an idle wire");
end procedure;
-- Reach a bus state using real ownership transfers, never by forcing.
procedure reach (s : integer) is
begin
case s is
when 0 => null;
when 1 => step('1','0','0','0','0');
when 2 => step('1','0','0','0','0'); step('1','0','1','0','0');
when 3 => step('1','0','0','0','0'); step('1','0','1','0','0');
idle(TA_MIN); step('0','1','0','0','0');
when others => step('1','0','0','0','0'); step('1','0','1','0','0');
idle(TA_MIN); step('0','1','0','0','0');
step('0','1','0','1','0');
end case;
end procedure;
variable b_c, b_e, b_l, b_o, b_s, b_g, b_h : integer := 0;
variable hd_v, dd_v, hv_v, rb_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(state = bs_code(B_IDLE), "reset did not land on an idle bus");
chk(owner = ow_code(O_NONE), "reset left the wire owned");
chk(err_pulse = '0', "reset reported a bus fault");
-- ---- Phase B: a complete, legal ownership cycle, repeatedly. ----
for k in 0 to 59 loop
b_g := to_integer(unsigned(n_grants));
b_h := to_integer(unsigned(n_handovers));
b_c := to_integer(unsigned(n_contention)) + to_integer(unsigned(n_early))
+ to_integer(unsigned(n_late)) + to_integer(unsigned(n_overhold))
+ to_integer(unsigned(n_stolen));
step('1','0','0','0','0'); -- host takes the wire
chk(owner = ow_code(O_HOST), "the host did not get an idle wire");
step('1','0','1','0','0'); -- ...and hands over
chk(state = bs_code(B_TURN_DEV), "handover did not enter the turnaround");
idle(TA_MIN); -- the gap
step('0','1','0','0','0'); -- the device answers
chk(owner = ow_code(O_DEV),
"the device did not get the wire after the turnaround");
step('0','1','0','1','0'); -- ...and releases
chk(state = bs_code(B_TURN_HST),
"release did not enter the return turnaround");
idle(TA_MIN);
step('1','0','0','0','0'); -- the host takes it back
chk(owner = ow_code(O_HOST), "the host did not get the wire back");
step('1','0','0','1','0'); -- and finishes
chk(to_integer(unsigned(n_grants)) = b_g + 3,
"a legal ownership cycle did not produce three grants");
chk(to_integer(unsigned(n_handovers)) = b_h + 1,
"the handover was not counted");
chk(to_integer(unsigned(n_contention)) + to_integer(unsigned(n_early))
+ to_integer(unsigned(n_late)) + to_integer(unsigned(n_overhold))
+ to_integer(unsigned(n_stolen)) = b_c,
"a completely legal ownership cycle produced a bus fault -- an environment that flags legal traffic is an environment whose ownership model gets switched off");
quiesce;
end loop;
-- ---- Phase C: BOTH EDGES OF THE TURNAROUND. ----
for a in 0 to TA_MAX loop
quiesce;
step('1','0','0','0','0');
step('1','0','1','0','0'); -- into the turnaround
idle(a); -- wait `a` cycles
b_e := to_integer(unsigned(n_early));
b_g := to_integer(unsigned(n_grants));
b_l := to_integer(unsigned(n_late));
if a < TA_MAX then
step('0','1','0','0','0'); -- the device drives
if a < TA_MIN then
chk(to_integer(unsigned(n_early)) = b_e + 1,
"the device drove before the host's drivers could have released the line, and the adapter allowed it -- this is the failure with no symptom that points at it, because both sides see corruption and neither sees a cause");
chk(to_integer(unsigned(n_grants)) = b_g,
"an early drive was granted the wire");
else
chk(to_integer(unsigned(n_grants)) = b_g + 1,
"the device drove after the full turnaround and was not granted the wire");
chk(to_integer(unsigned(n_early)) = b_e,
"a legal handover was reported as early");
end if;
else
idle(1);
chk(to_integer(unsigned(n_late)) = b_l + 1,
"the turnaround expired with nobody taking the wire and nothing was reported -- a turnaround that never expires is a bus that can stall for ever");
end if;
end loop;
-- ---- Phase D: CONTENTION, from every state. ----
for st2 in 0 to 4 loop
quiesce;
reach(st2);
chk(state = bs_code(bus_state_t'val(st2)),
"the sweep could not reach the bus state it meant to reach");
b_c := to_integer(unsigned(n_contention));
step('1','1','0','0','0');
chk(to_integer(unsigned(n_contention)) = b_c + 1,
"both agents drove the wire at once and it was not reported -- the line is at an undefined level, both sides see corruption, and neither sees a cause");
chk(state = bs_code(B_IDLE), "contention did not return the wire to nobody");
end loop;
-- ---- Phase E: a device driving an idle bus. ----
quiesce;
b_s := to_integer(unsigned(n_stolen));
step('0','1','0','0','0');
chk(to_integer(unsigned(n_stolen)) = b_s + 1,
"a device drove an idle wire and it was allowed -- there is no arbitration on this bus and no real device can do this, so the stimulus is one the design will never see");
-- ---- Phase F: driving past the hold bound, on BOTH agents. ----
for k in 0 to 39 loop
quiesce;
b_o := to_integer(unsigned(n_overhold));
step('1','0','0','0','0');
for a in 0 to HOLD_MAX + 1 loop
step('1','0','0','0','0');
end loop;
chk(to_integer(unsigned(n_overhold)) = b_o + 1,
"an agent drove continuously past the hold bound and nothing was reported -- the other agent's response will collide with it and the contention will be blamed on whoever spoke second");
quiesce;
b_o := to_integer(unsigned(n_overhold));
step('1','0','0','0','0');
step('1','0','1','0','0');
idle(TA_MIN);
for a in 0 to HOLD_MAX + 2 loop
step('0','1','0','0','0');
end loop;
chk(to_integer(unsigned(n_overhold)) = b_o + 1,
"the device drove continuously past the hold bound and nothing was reported");
end loop;
-- ---- Phase F2: the turnaround expires, in BOTH directions. ----
for k in 0 to 39 loop
quiesce;
b_l := to_integer(unsigned(n_late));
step('1','0','0','0','0');
step('1','0','1','0','0'); -- handed over to the device
idle(TA_MAX + 2); -- ...which never answers
chk(to_integer(unsigned(n_late)) = b_l + 1,
"the device never answered and the turnaround never expired -- a turnaround with no bound is a bus that can stall for ever, and nothing else in this block fires when nothing happens");
quiesce;
b_l := to_integer(unsigned(n_late));
step('1','0','0','0','0');
step('1','0','1','0','0');
idle(TA_MIN);
step('0','1','0','0','0');
step('0','1','0','1','0'); -- returning to the host
idle(TA_MAX + 2); -- ...which never takes it
chk(to_integer(unsigned(n_late)) = b_l + 1,
"the host never took the wire back and the return turnaround never expired");
end loop;
-- ---- Phase G: EXHAUSTIVE. Every state x every input combination. ----
for st2 in 0 to 4 loop
for cb in 0 to 15 loop
quiesce;
reach(st2);
chk(state = bs_code(bus_state_t'val(st2)),
"the sweep could not reach the bus state it meant to reach");
if (cb / 8) mod 2 = 1 then hd_v := '1'; else hd_v := '0'; end if;
if (cb / 4) mod 2 = 1 then dd_v := '1'; else dd_v := '0'; end if;
if (cb / 2) mod 2 = 1 then hv_v := '1'; else hv_v := '0'; end if;
if cb mod 2 = 1 then rb_v := '1'; else rb_v := '0'; end if;
step(hd_v, dd_v, hv_v, rb_v, '0');
step(hd_v, dd_v, hv_v, rb_v, '0');
end loop;
end loop;
-- ---- Phase H: every turnaround age, with and without the new owner
-- ---- driving, so the window is swept rather than sampled.
for a in 0 to TA_MAX loop
for k in 0 to 1 loop
quiesce;
step('1','0','0','0','0');
step('1','0','1','0','0');
idle(a);
if k = 1 then step('0','1','0','0','0'); else step('0','0','0','0','0'); end if;
idle(1);
end loop;
end loop;
-- ---- Phase I: random ----
for k in 0 to 33999 loop
hd_v := rnd_lt(34, 100);
dd_v := rnd_lt(30, 100);
hv_v := rnd_lt(18, 100);
rb_v := rnd_lt(18, 100);
step(hd_v, dd_v, hv_v, rb_v, rnd_lt(8, 1000));
end loop;
-- ---- Phase J: and a clean ownership cycle afterwards. ----
quiesce;
b_g := to_integer(unsigned(n_grants));
b_c := to_integer(unsigned(n_contention)) + to_integer(unsigned(n_early))
+ to_integer(unsigned(n_late)) + to_integer(unsigned(n_overhold))
+ to_integer(unsigned(n_stolen));
for k in 0 to 79 loop
step('1','0','0','0','0');
step('1','0','1','0','0');
idle(TA_MIN);
step('0','1','0','0','0');
step('0','1','0','1','0');
idle(TA_MIN);
step('1','0','0','0','0');
step('1','0','0','1','0');
quiesce;
end loop;
chk(to_integer(unsigned(n_grants)) = b_g + 240,
"the ownership model stopped granting the wire");
chk(to_integer(unsigned(n_contention)) + to_integer(unsigned(n_early))
+ to_integer(unsigned(n_late)) + to_integer(unsigned(n_overhold))
+ to_integer(unsigned(n_stolen)) = b_c,
"clean ownership cycles after the random phase produced bus faults");
-- ---- Final agreement ----
chk(to_integer(unsigned(n_grants)) = m_g, "n_grants disagrees with the model");
chk(to_integer(unsigned(n_handovers)) = m_h, "n_handovers disagrees");
chk(to_integer(unsigned(n_contention)) = m_c, "n_contention disagrees");
chk(to_integer(unsigned(n_early)) = m_e, "n_early disagrees");
chk(to_integer(unsigned(n_late)) = m_l, "n_late disagrees");
chk(to_integer(unsigned(n_overhold)) = m_o, "n_overhold disagrees");
chk(to_integer(unsigned(n_stolen)) = m_s, "n_stolen disagrees");
chk(n_si = 80, "not every bus state was crossed with every input combination");
chk(n_ta_s = 18, "not every turnaround age was seen with and without the new owner driving");
chk(to_integer(unsigned(n_grants)) > 0, "the wire was never granted to anybody");
chk(to_integer(unsigned(n_handovers)) > 0, "ownership was never handed over");
chk(to_integer(unsigned(n_contention)) > 0, "contention was never exercised");
chk(to_integer(unsigned(n_early)) > 0, "an early drive was never exercised");
chk(to_integer(unsigned(n_late)) > 0, "a turnaround never expired");
chk(to_integer(unsigned(n_overhold)) > 0, "the hold bound was never exceeded");
chk(to_integer(unsigned(n_stolen)) > 0, "a non-owner never drove the wire");
write(ln, string'("REACH state-x-input=") & integer'image(n_si) &
"/80 turnaround-age=" & integer'image(n_ta_s) &
"/18 steps=" & integer'image(n_steps));
writeline(output, ln);
write(ln, string'("COUNTERS grants=") & integer'image(to_integer(unsigned(n_grants))) &
" handovers=" & integer'image(to_integer(unsigned(n_handovers))) &
" contention=" & integer'image(to_integer(unsigned(n_contention))) &
" early=" & integer'image(to_integer(unsigned(n_early))) &
" late=" & integer'image(to_integer(unsigned(n_late))) &
" overhold=" & integer'image(to_integer(unsigned(n_overhold))) &
" stolen=" & integer'image(to_integer(unsigned(n_stolen))));
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;11. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| bus state x input | 80 / 80 | 80 / 80 | 80 / 80 |
| turnaround age x new owner drove | 18 / 18 | 18 / 18 | 18 / 18 |
| Steps | 39639 | 39639 | 39639 |
| Checks executed | 437631 | 437631 | 437631 |
| grants | 6088 | 6135 | 5219 |
| handovers | 1529 | 1544 | 1149 |
| contention | 3546 | 3554 | 5095 |
| early drives (into the gap) | 352 | 376 | 191 |
| turnarounds expired | 83 | 83 | 87 |
| hold bound exceeded | 80 | 80 | 80 |
| non-owner drove | 6841 | 6788 | 5225 |
| Result | PASS | PASS | PASS |
The 18-of-18 turnaround sweep is what puts a drive attempt on every cycle of the window, including both of its edges — which is the only way to distinguish a correct turnaround from one that is one cycle too short or one cycle too long.
12. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| Y6 | the turnaround is skipped — ownership passes directly | 17276 | 17831 | 10745 |
| Y5 | a device driving an idle bus is allowed | 8666 | 8628 | 7002 |
| Y7 | releasing the wire does not clear ownership | 7698 | 8388 | 6223 |
| Y2 | the lower edge of the turnaround is not enforced | 5013 | 5266 | 2707 |
| Y1 | contention is not detected | 3554 | 3562 | 5103 |
| Y4 | an owner may drive for ever | 1244 | 1244 | 1244 |
| Y3 | the turnaround never expires | 741 | 741 | 808 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct.
Y6 is the largest, and it is the mutation that describes an environment with no turnaround model at all: ownership passes straight from host to device with no gap. Every subsequent handover in the run is then wrong, which is why it dominates.
Y1 — contention undetected — scores only 3554, which is lower than several mutations that matter far less. That is not a measure of its importance; it is a measure of how often the suite drives contention, which is deliberately controlled because contention returns the bus to idle and disrupts everything after it. The directed phase that drives it from all five states is what actually kills it, and the score would be the same if the random phase were deleted.
13. Debugging Walkthrough: Three Weeks on a Testbench Bug
The report. A new USB device environment produces intermittent CRC failures. They appear in roughly one run in six, on different transfers each time, and they move when anything at all is changed — a $display, a different seed, a different simulator version.
Step 1 — is it the design? Dump the waveform. The device's transmitted packet is corrupt on the wire. The device's internal data is correct. So the corruption happens at the pin.
Step 2 — is it the PHY model? Replace it with a pass-through. No change.
Step 3 — look at the pin. Both drivers are enabled, for two cycles, at the start of the device's response. The host agent is still driving.
Step 4 — why? The host driver deasserts its enable in the clocking block's output skew after sending the last bit. The device driver asserts its enable as soon as its sequencer hands it an item. Those two events are ordered by the simulator's scheduler, not by anything in the protocol — which is why the failure moves when anything changes.
Step 5 — the environment had no turnaround. The host agent and the device agent each knew when they wanted to drive. Neither knew when the other had stopped, and nothing in the environment represented the interval between.
Step 6 — three weeks. Because every symptom pointed at the design: the packet on the wire was corrupt, the CRC failure was real, and the device's own logic was demonstrably correct. The one thing that would have pointed at the environment — "both agents are driving" — was not being checked by anything, because it is not a protocol violation. It is a physical impossibility, and physically impossible things are exactly what a testbench can do and a chip cannot.
14. The Environment This Block Belongs To
// The whole of Module 24, assembled. Five of these components are the ones
// built in chapters 24.1 to 24.5; the sixth is this chapter's ownership
// model, and it is the one that makes the other five trustworthy -- because
// a scoreboard fed by an environment that creates its own contention is a
// scoreboard reporting on the testbench.
class usb_env extends uvm_env;
`uvm_component_utils(usb_env)
// ---- TWO AGENTS, ONE WIRE. ----
//
// They are separate agents because they have separate sequencers, separate
// stimulus and separate coverage. They are NOT separate interfaces: both
// drive the same virtual interface, and the ownership model below is the
// only thing that stops them doing it at the same time.
usb_host_agent host_agt;
usb_device_agent dev_agt;
// ---- ONE monitor, not two. ----
//
// There is one wire, so there is one thing to observe. A monitor per agent
// sees what that agent DROVE, which is its intention -- and chapter 24.4
// makes the same point about sampling coverage in the driver. What the
// checkers need is what APPEARED, which only a bus monitor can give them.
usb_bus_monitor bus_mon;
// ---- the ownership model: this chapter ----
usb_bus_ownership_c ownership;
// ---- and the five components from the rest of the module ----
usb_protocol_checker_c checker; // 24.1 was that legal?
usb_liveness_c liveness; // 24.2 did it happen in time?
usb_xfer_scoreboard scoreboard; // 24.3 was that mine?
usb_coverage_report coverage; // 24.4 what did we cover?
usb_vip_comparator vip_cmp; // 24.5 does the VIP agree?
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
host_agt = usb_host_agent::type_id::create("host_agt", this);
dev_agt = usb_device_agent::type_id::create("dev_agt", this);
bus_mon = usb_bus_monitor::type_id::create("bus_mon", this);
ownership = usb_bus_ownership_c::type_id::create("ownership", this);
checker = usb_protocol_checker_c::type_id::create("checker", this);
liveness = usb_liveness_c::type_id::create("liveness", this);
scoreboard = usb_xfer_scoreboard::type_id::create("scoreboard", this);
coverage = usb_coverage_report::type_id::create("coverage", this);
vip_cmp = usb_vip_comparator::type_id::create("vip_cmp", this);
endfunction
function void connect_phase(uvm_phase phase);
// ---- Everything that CHECKS hangs off the ONE bus monitor. ----
//
// Not off the drivers. A checker fed by a driver is checking what the
// testbench meant to do, which is the mistake chapter 24.1 forbids for
// the protocol checker and chapter 24.4 forbids for coverage, for the
// same reason in both cases.
bus_mon.ap.connect(checker.ap);
bus_mon.ap.connect(liveness.ap);
bus_mon.ap.connect(coverage.ap);
bus_mon.ap.connect(vip_cmp.dut_ap);
// ---- The ownership model watches the DRIVE ENABLES, which is the one
// ---- thing the bus monitor cannot see.
//
// A monitor sees the wire. It cannot distinguish "nobody is driving"
// from "both are driving and the result happens to look like a 1". The
// enables are testbench state, and contention is a testbench fault, so
// this is the one connection in the environment that goes to the
// drivers on purpose.
host_agt.driver.drive_en_ap.connect(ownership.host_ap);
dev_agt.driver.drive_en_ap.connect(ownership.dev_ap);
// ---- The scoreboard needs both what was SENT and what was SEEN. ----
host_agt.driver.issued_ap.connect(scoreboard.exp_ap);
bus_mon.xfer_ap.connect(scoreboard.act_ap);
endfunction
// ---- The ownership model gates the drivers. ----
//
// This is the part that turns the model from a checker into a mechanism:
// a driver asks before it drives, and is refused if it does not own the
// wire. Without the gate the model reports contention; with it, the
// contention does not happen and the report says which driver tried.
function void end_of_elaboration_phase(uvm_phase phase);
host_agt.driver.set_ownership_model(ownership);
dev_agt.driver.set_ownership_model(ownership);
endfunction
endclass14.1 The driver side of the gate
class usb_device_driver extends uvm_driver #(usb_pkt_item);
`uvm_component_utils(usb_device_driver)
virtual usb_if vif;
usb_bus_ownership_c own;
uvm_analysis_port #(bit) drive_en_ap;
function void set_ownership_model(usb_bus_ownership_c m); own = m; endfunction
task run_phase(uvm_phase phase);
forever begin
usb_pkt_item it;
seq_item_port.get_next_item(it);
// ---- ASK BEFORE DRIVING. ----
//
// Not "wait for the bus to look idle". Looking idle is exactly what a
// wire does during the gap the host has not finished, and it is what
// the three weeks in section 13 were spent on.
own.request(OWNER_DEV);
if (!own.granted(OWNER_DEV)) begin
// A refusal is a TESTBENCH error and is reported as one. It is not
// a design failure and must never be counted as one -- the whole
// value of modelling ownership is that these two populations stop
// being confused with each other.
`uvm_error("BUS_OWN",
$sformatf("the device driver tried to drive while the bus was %s -- this is a testbench fault, not a design failure, and letting it through produces contention that no real bus can create",
own.state_name()))
seq_item_port.item_done();
continue;
end
drive_en_ap.write(1'b1);
drive_packet(it);
drive_en_ap.write(1'b0);
// ---- RELEASE, and do not drive again until the model says so. ----
own.release_bus(OWNER_DEV);
seq_item_port.item_done();
end
endtask
task drive_packet(usb_pkt_item it);
// ...the usual bit-level driving, unchanged by any of this.
endtask
endclass14.2 What the two agents may and may not randomise
// A device sequence cannot decide WHEN to speak. That is the protocol's
// decision, not the test's, and a sequence that randomises it is generating
// stimulus the design will never see.
class usb_device_response_seq extends uvm_sequence #(usb_pkt_item);
`uvm_object_utils(usb_device_response_seq)
function new(string name = "usb_device_response_seq"); super.new(name); endfunction
task body();
forever begin
usb_pkt_item it = usb_pkt_item::type_id::create("it");
// The device agent randomises WHAT it answers -- ACK, NAK, STALL,
// data, a short packet -- and it randomises HOW LONG it takes within
// the turnaround window. It does not randomise WHETHER to answer at
// an arbitrary moment, because a real device cannot.
start_item(it);
if (!it.randomize() with {
resp inside {RESP_ACK, RESP_NAK, RESP_STALL, RESP_DATA};
resp dist {RESP_ACK := 70, RESP_NAK := 20,
RESP_STALL := 5, RESP_DATA := 5};
// Within the window, including both of its edges, which is
// where the interesting bugs are.
turnaround_delay inside {[TA_MIN : TA_MAX-1]};
})
`uvm_error("RAND", "device response randomize failed")
finish_item(it);
end
endtask
endclass
// ...and the error-injection sequence, which is allowed to break the rule --
// ONCE, deliberately, with the expectation written down.
//
// This is the sequence that must exist and must be separate. Folding
// turnaround violations into the normal response sequence means every test
// occasionally drives an illegal handover, and the environment's own
// ownership errors become background noise nobody reads.
class usb_turnaround_violation_seq extends uvm_sequence #(usb_pkt_item);
`uvm_object_utils(usb_turnaround_violation_seq)
function new(string name = "usb_turnaround_violation_seq"); super.new(name); endfunction
task body();
usb_pkt_item it = usb_pkt_item::type_id::create("it");
start_item(it);
// Deliberately inside the gap. The test that runs this sequence EXPECTS
// an ownership error and fails if it does not get one -- which is the
// only way to know the ownership model is still connected.
if (!it.randomize() with { turnaround_delay == 0; })
`uvm_error("RAND", "violation randomize failed")
finish_item(it);
endtask
endclass15. Common Misconceptions
"Two agents means two interfaces." It means two drivers on one wire, and everything else follows from that.
"The turnaround is a timing detail." It is a state in which nobody owns the wire. An architecture with no such state cannot represent it.
"Wait for the bus to look idle." A wire in the middle of a turnaround looks idle. That is what makes this bug take three weeks.
"Contention is a protocol violation." No specification forbids it, because no real bus can produce it. It is a testbench fault, and nothing checks for it unless you decide to.
"Each agent needs its own monitor." A monitor per agent sees what that agent intended. The checkers need what appeared.
"The device agent randomises when it speaks." It randomises what it answers and how long it takes within the window. When is the protocol's decision.
"Error injection can live in the normal sequences." Then the environment's own ownership errors become background noise.
"Zero contention means the model is working." It also means the model is not instantiated. One test has to expect a violation.
"A failure that moves when you add a $display is a race in the design." The design does not know about your $display.
16. Exercises
1. Y4 scores 1244 in all three languages. Identify the directed phase responsible, then compute the probability that 34 000 cycles of the random phase produce seventeen consecutive drive cycles from one agent.
2. Contention is hoisted above the case statement. Write the per-state version, omit the check from exactly one state, and find which of the suite's checks catches it.
3. The turnaround sweep drives at every age 0 to TA_MAX. Show that checking only TA_MIN-1 and TA_MIN would pass a design whose window is [TA_MIN, TA_MIN] — granted at exactly one age and refused at every other.
4. own.request() gates the driver. Work out what the ownership model reports if the gate is removed but the model is left connected, and why that is still better than not having it.
5. Add a second device agent, as a hub would need. What changes in the ownership model, and which of the five error codes acquires a new meaning?
6. Write the test from §14.2 that fails if no ownership error is reported, and say why uvm_report_catcher is the wrong tool for it.
17. Summary
| Idea | Why it matters |
|---|---|
| Two agents, one wire | the shape of the whole architecture |
| The turnaround is a state | nobody owns the wire, and it must be representable |
| Drive early → contention | undefined level, both sides corrupt, no cause visible |
| Drive late → a timeout the host blames on the device | |
| Ownership is derived, not negotiated | the host owns by default; the device never initiates |
| Contention is checked first, everywhere | five states means five places to forget |
| A driver asks before driving | "the bus looks idle" is exactly the bug |
| A refusal is a testbench error | and must never be counted as a design failure |
| One bus monitor, not one per agent | a driver's monitor sees intentions |
| Error injection is a separate sequence | or the environment's own errors become noise |
| One test must expect a violation | zero contention also means "not instantiated" |
release is a Verilog keyword | as is illegal_bins in SystemVerilog |
| 80/80 state x input, 18/18 turnaround ages | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o bo_v.out bo_v.v bo_v_tb.v && ./bo_v.out |
| SystemVerilog | iverilog -g2012 -o bo_sv.out bo_sv.sv bo_sv_tb.sv && ./bo_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a bo_vhdl.vhd bo_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_bo_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_bo_vhdl |
| One mutation | iverilog -g2005 -DMUT_Y1 -o mm bo_v_mut.v bo_v_tb.v && ./mm |
All three implementations pass with 0 errors: every bus state crossed with every input combination, every turnaround age swept with and without the new owner driving, contention driven from all five states, and both edges of the turnaround window checked.
18. Module 24 in One Page
Six components, each answering a different question about the same bus:
| Chapter | Component | The question | The failure that defines it |
|---|---|---|---|
| 24.1 | protocol checker | was that legal? | no timeout — silence violates no ordering rule |
| 24.2 | assertion engine | did it happen in time? | "eventually" has no failing case |
| 24.3 | scoreboard | was that mine? | matching on value lets two bugs cancel |
| 24.4 | coverage model | what did we cover? | unreachable and untested both read 0% |
| 24.5 | VIP adapter | does the second opinion agree? | the shim that resolves the disagreement |
| 24.6 | ownership model | whose wire is it? | no turnaround, so the env creates its own contention |
Four habits run through all six, and they are worth more than any of the components:
Check the false-positive side. Every one of these components gets switched off if it cries wolf, and a component that is switched off has a false-negative rate of 100%. Three of the six chapters spend most of their stimulus proving the thing is quiet on legal traffic.
Name the cause, and make the causes sum. A single error counter is a number; per-cause counters driven by the same pulse are a diagnosis, and the fact that they add up is checkable.
Drain at the end of test. An unfinished obligation, an unmatched transfer, an uncovered bin and an unpaired event are all findings, and all four are reported as nothing by the default behaviour of the tool.
Know which half of your suite is doing the work. A mutation whose score is identical across three independently written testbenches is caught entirely by directed stimulus; one whose score varies is caught by the random half. Both are legitimate. Not knowing which is not.
Continue learning
Related tutorials
- Related topic
USB Protocol Checkers
Every ordering rule says what must happen next, and none of them fires when nothing happens at all — the timeout is a checker's only liveness tool, and a checker with false positives gets switched off.
- Related topic
USB Assertions
“Eventually” has no failing case, so it cannot be checked in a finite run — every real liveness check is bounded, a window has two edges, and an obligation still outstanding at end of test is a failure, not an unknown.
- Related topic
USB Scoreboards
Two transfers carrying the same bytes are indistinguishable to a scoreboard that matches on value, so a duplicate delivery and a lost transfer cancel out — identity finds the partner, value checks it.
- Related topic
USB Functional Coverage
An unreachable bin and an untested bin both read 0% and demand opposite responses — and an exclusion is a claim about the design, so a bin that is excluded and then hit must fail.
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.
