USB · Module 24
USB VIP Usage
A VIP is a second implementation of the specification, so where it and the design disagree one of them is wrong — and the adapter's job is to make that visible, never to resolve it in a shim.
Every checking component so far has been one you wrote. This chapter is about the one you did not.
1. A VIP Is a Second Implementation of the Specification
That is the whole value of it. Two independent readings of the same document, watching the same wires, and where they disagree one of them is wrong.
Neither can tell you which, because each is reasoning from its own model — and that is fine. "These two disagree, here, on this packet" is already the most useful thing anybody will learn that day.
2. The Job the Adapter Must Not Do
An adapter sits between the VIP and the design because their interfaces never match exactly: different field widths, different event granularity, a cycle of pipelining here, a differently-named signal there. Writing that translation is legitimate work.
What is not legitimate is the step that always follows:
"the VIP flags this as an error but our device is allowed to do it, so the adapter filters it out"
So this block compares the two views and reports the difference. It has a permissive mode — because sometimes you genuinely must get through the afternoon — and in permissive mode it counts what it masked. A permissive run cannot come back clean; it comes back with a number, and the number is how many specification arguments nobody has had yet.
3. A VIP's Coverage Is the VIP's Model's Coverage
This is worth saying plainly because VIP coverage reports get quoted as if they were project coverage.
A VIP at 100% has exercised every bin IT declares:
the bins ITS authors thought of
for the specification revision THEY targeted
with the configurations THEY support
It says nothing about the model in chapter 24.4, and in
particular nothing about the combinations your device
supports and its authors had never heard of.Version skew is the usual shape of this. The VIP implements revision X; the device targets revision Y; the disagreements cluster on the delta — which is exactly the region with the least review behind it on both sides.
4. The Two Sides Do Not Speak at the Same Time
A behavioural VIP reports a packet when it has decoded it. A pipelined design reports the same packet some cycles later. So the adapter aligns them with a small queue on each side rather than comparing cycle by cycle.
And the alignment window is bounded.
5. What We Are Building
usb_vip_adapter — two views, aligned and compared, never reconciled
usb_vip_adapter #(QD = 8, QW = 3, SKEW_MAX = 4)
the VIP's decoded view the design's decoded view
---------------------- -------------------------
vip_valid / vip_pid dut_valid / dut_pid
vip_view dut_view
strict 1: a disagreement FAILS
0: it is MASKED -- and COUNTED
outputs agree_pulse / dis_pulse / masked_pulse
dis_code PID / VIEW / SKEW / UNPAIRED
skew how far apart the two sides are
n_agree n_pid_dis n_view_dis n_skew n_masked n_unpaireddis_code separates two things that get conflated. A PID disagreement means the two sides decoded different packets — usually a decode bug or a lost event. A VIEW disagreement means they decoded the same packet and read it differently, and that is always a specification reading. Those go to different people.
6. Verilog-2005 Implementation
// usb_vip_adapter -- integrating verification IP you did not write, and the
// one job the adapter must not do.
//
// A VIP IS A SECOND IMPLEMENTATION OF THE SPECIFICATION
//
// That is the whole value of it. Two independent readings of the same
// document, watching the same wires, and where they disagree ONE OF THEM IS
// WRONG. Neither can tell you which, because each is reasoning from its own
// model -- and that is fine, because "these two disagree, here, on this
// packet" is already the most useful thing anybody will learn that day.
//
// THE JOB THE ADAPTER MUST NOT DO
//
// An adapter sits between the VIP and the design because their interfaces
// never match exactly: different field widths, different event granularity,
// a cycle of pipelining here, a differently-named signal there. Writing that
// translation is legitimate work.
//
// What is not legitimate is the step that always follows:
//
// "the VIP flags this as an error but our device is
// allowed to do it, so the adapter filters it out"
//
// That sentence is sometimes true. It is a specification argument, and it
// has to be WON -- written down, cited, and reviewed -- not implemented in a
// shim by whoever was unblocking the regression that afternoon. Once it is
// in the shim, the disagreement is invisible, and so is every future
// disagreement of the same shape.
//
// AN ADAPTER MAKES A DISAGREEMENT VISIBLE.
// IT DOES NOT RESOLVE ONE.
//
// So this block compares the two views and reports the difference. It has a
// permissive mode -- because sometimes you genuinely must get through the
// afternoon -- and in permissive mode it COUNTS what it masked. A permissive
// run cannot come back clean; it comes back with a number.
//
// A VIP'S COVERAGE IS THE VIP'S MODEL'S COVERAGE
//
// This is worth saying plainly because VIP coverage reports are quoted as if
// they were project coverage. A VIP at 100% has exercised every bin IT
// declares, which are the bins ITS authors thought of, for the specification
// revision THEY targeted. It says nothing about the bins in chapter 24.4's
// model, and in particular it says nothing about the combinations your
// device supports and the VIP's authors had never heard of.
//
// THE TWO SIDES DO NOT SPEAK AT THE SAME TIME
//
// A behavioural VIP reports a packet when it has decoded it; a pipelined
// design reports the same packet some cycles later. So the adapter aligns
// them with a small queue on each side rather than comparing cycle by cycle.
//
// And the alignment window is BOUNDED. When one side runs SKEW_MAX events
// ahead of the other, they are not skewed any more -- they have lost
// alignment, and every comparison after that point is between unrelated
// events. Widening the window is the reflex and it is wrong: it converts a
// loud failure into a quiet stream of nonsense comparisons.
module usb_vip_adapter #(
parameter integer QD = 8, // alignment queue depth
parameter integer QW = 3, // log2(QD)
parameter integer SKEW_MAX = 4 // events one side may run ahead
) (
input wire clk,
input wire rst_n,
// ---- the VIP's decoded view of the bus ----
input wire vip_valid,
input wire [3:0] vip_pid,
input wire [1:0] vip_view,
// ---- the design's decoded view of the SAME bus ----
input wire dut_valid,
input wire [3:0] dut_pid,
input wire [1:0] dut_view,
input wire strict, // 1: a disagreement fails. 0: it is COUNTED.
input wire eot,
output wire [QW:0] vip_depth,
output wire [QW:0] dut_depth,
output wire [QW:0] skew,
output wire agree_pulse,
output wire dis_pulse,
output wire [2:0] dis_code,
output wire masked_pulse,
output reg [31:0] n_agree,
output reg [31:0] n_pid_dis,
output reg [31:0] n_view_dis,
output reg [31:0] n_skew,
output reg [31:0] n_masked,
output reg [31:0] n_unpaired
);
localparam [2:0] D_NONE = 3'd0,
D_PID = 3'd1, // the two sides decoded a different PID
D_VIEW = 3'd2, // same PID, different interpretation
D_SKEW = 3'd3, // alignment lost
D_UNPAIRED = 3'd4; // an event only one side ever saw
reg [3:0] vp_pid [0:QD-1];
reg [1:0] vp_vw [0:QD-1];
reg [3:0] dt_pid [0:QD-1];
reg [1:0] dt_vw [0:QD-1];
reg [QW:0] vp_n, dt_n;
reg [2:0] code_r;
reg agr_r, dis_r, msk_r;
assign vip_depth = vp_n;
assign dut_depth = dt_n;
// Only one queue can be non-empty after a comparison, so the skew is
// simply whichever one still holds events.
assign skew = (vp_n > dt_n) ? vp_n : dt_n;
assign agree_pulse = agr_r;
assign dis_pulse = dis_r;
assign dis_code = code_r;
assign masked_pulse = msk_r;
integer i;
reg [3:0] nvp_pid [0:QD-1];
reg [1:0] nvp_vw [0:QD-1];
reg [3:0] ndt_pid [0:QD-1];
reg [1:0] ndt_vw [0:QD-1];
reg [QW:0] nvp_n, ndt_n;
reg [2:0] ncode;
reg nagr, ndis, nmsk, nunp;
reg differ;
always @* begin
for (i = 0; i < QD; i = i + 1) begin
nvp_pid[i] = vp_pid[i]; nvp_vw[i] = vp_vw[i];
ndt_pid[i] = dt_pid[i]; ndt_vw[i] = dt_vw[i];
end
nvp_n = vp_n; ndt_n = dt_n;
ncode = D_NONE;
nagr = 1'b0; ndis = 1'b0; nmsk = 1'b0; nunp = 1'b0;
differ = 1'b0;
if (eot) begin
// ---- THE DRAIN. An event only one side ever reported. ----
//
// Not "skew that had not settled": the run is over. One of the two
// implementations saw something the other never did, which is exactly
// the class of finding a second implementation exists to produce.
if (vp_n != 0) begin
for (i = 0; i < QD - 1; i = i + 1) begin
nvp_pid[i] = nvp_pid[i+1]; nvp_vw[i] = nvp_vw[i+1];
end
nvp_n = vp_n - 1'b1;
ndis = 1'b1; ncode = D_UNPAIRED; nunp = 1'b1;
end else if (dt_n != 0) begin
for (i = 0; i < QD - 1; i = i + 1) begin
ndt_pid[i] = ndt_pid[i+1]; ndt_vw[i] = ndt_vw[i+1];
end
ndt_n = dt_n - 1'b1;
ndis = 1'b1; ncode = D_UNPAIRED; nunp = 1'b1;
end
end else begin
// ---- 1. Both sides have an event: compare the OLDEST pair. ----
if ((vp_n != 0) && (dt_n != 0)) begin
if (vp_pid[0] !== dt_pid[0]) begin
differ = 1'b1; ncode = D_PID;
end else if (vp_vw[0] !== dt_vw[0]) begin
// ---- Same PID, different INTERPRETATION. ----
//
// This is the disagreement worth having a VIP for. Both sides
// agree on what arrived and disagree on what it MEANS -- which is
// always a specification reading, and is the case an adapter is
// most often asked to paper over.
differ = 1'b1; ncode = D_VIEW;
end else begin
nagr = 1'b1;
end
if (differ) begin
if (strict) begin
ndis = 1'b1;
end else begin
// ---- PERMISSIVE. Masked, and COUNTED. ----
//
// The count is the point. A permissive run does not come back
// clean; it comes back with a number, and the number is how
// many specification arguments nobody has had yet.
nmsk = 1'b1;
end
end
for (i = 0; i < QD - 1; i = i + 1) begin
nvp_pid[i] = nvp_pid[i+1]; nvp_vw[i] = nvp_vw[i+1];
ndt_pid[i] = ndt_pid[i+1]; ndt_vw[i] = ndt_vw[i+1];
end
nvp_n = nvp_n - 1'b1;
ndt_n = ndt_n - 1'b1;
end
// ---- 2. Enqueue whatever arrived this cycle. ----
if (vip_valid && (nvp_n < QD[QW:0])) begin
nvp_pid[nvp_n] = vip_pid;
nvp_vw[nvp_n] = vip_view;
nvp_n = nvp_n + 1'b1;
end
if (dut_valid && (ndt_n < QD[QW:0])) begin
ndt_pid[ndt_n] = dut_pid;
ndt_vw[ndt_n] = dut_view;
ndt_n = ndt_n + 1'b1;
end
// ---- 3. THE ALIGNMENT BOUND. ----
//
// One side running SKEW_MAX events ahead of the other is not skew any
// more: alignment is lost, and every comparison after this point is
// between unrelated events. The queues are flushed so that the loss
// is reported ONCE rather than as a stream of nonsense PID
// disagreements.
//
// Widening SKEW_MAX is the reflex and it is the wrong move: it
// converts a loud failure into a quiet stream of garbage.
if ((nvp_n > SKEW_MAX[QW:0]) || (ndt_n > SKEW_MAX[QW:0])) begin
nvp_n = {(QW+1){1'b0}};
ndt_n = {(QW+1){1'b0}};
ndis = 1'b1;
nmsk = 1'b0;
ncode = D_SKEW;
nagr = 1'b0;
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < QD; i = i + 1) begin
vp_pid[i] <= 4'd0; vp_vw[i] <= 2'd0;
dt_pid[i] <= 4'd0; dt_vw[i] <= 2'd0;
end
vp_n <= {(QW+1){1'b0}};
dt_n <= {(QW+1){1'b0}};
code_r <= D_NONE;
agr_r <= 1'b0;
dis_r <= 1'b0;
msk_r <= 1'b0;
n_agree <= 32'd0;
n_pid_dis <= 32'd0;
n_view_dis <= 32'd0;
n_skew <= 32'd0;
n_masked <= 32'd0;
n_unpaired <= 32'd0;
end else begin
for (i = 0; i < QD; i = i + 1) begin
vp_pid[i] <= nvp_pid[i]; vp_vw[i] <= nvp_vw[i];
dt_pid[i] <= ndt_pid[i]; dt_vw[i] <= ndt_vw[i];
end
vp_n <= nvp_n;
dt_n <= ndt_n;
code_r <= ncode;
agr_r <= nagr;
dis_r <= ndis;
msk_r <= nmsk;
if (nagr) n_agree <= n_agree + 32'd1;
if (nmsk) n_masked <= n_masked + 32'd1;
// A masked disagreement still increments its CAUSE counter. The
// permissive switch decides whether the run fails; it does not decide
// whether the disagreement happened.
if (ndis || nmsk) begin
case (ncode)
D_PID: n_pid_dis <= n_pid_dis + 32'd1;
D_VIEW: n_view_dis <= n_view_dis + 32'd1;
D_SKEW: n_skew <= n_skew + 32'd1;
D_UNPAIRED: n_unpaired <= n_unpaired + 32'd1;
default: ;
endcase
end
end
end
endmodule7. SystemVerilog Implementation
// usb_vip_adapter -- integrating verification IP you did not write, and the
// one job the adapter must not do.
//
// A VIP IS A SECOND IMPLEMENTATION OF THE SPECIFICATION
//
// That is the whole value of it. Two independent readings of the same
// document, watching the same wires, and where they disagree ONE OF THEM IS
// WRONG. Neither can tell you which, because each is reasoning from its own
// model -- and that is fine, because "these two disagree, here, on this
// packet" is already the most useful thing anybody will learn that day.
//
// THE JOB THE ADAPTER MUST NOT DO
//
// An adapter sits between the VIP and the design because their interfaces
// never match exactly: different field widths, different event granularity,
// a cycle of pipelining here, a differently-named signal there. Writing that
// translation is legitimate work.
//
// What is not legitimate is the step that always follows:
//
// "the VIP flags this as an error but our device is
// allowed to do it, so the adapter filters it out"
//
// That sentence is sometimes true. It is a specification argument, and it
// has to be WON -- written down, cited, and reviewed -- not implemented in a
// shim by whoever was unblocking the regression that afternoon. Once it is
// in the shim, the disagreement is invisible, and so is every future
// disagreement of the same shape.
//
// AN ADAPTER MAKES A DISAGREEMENT VISIBLE.
// IT DOES NOT RESOLVE ONE.
//
// So this block compares the two views and reports the difference. It has a
// permissive mode -- because sometimes you genuinely must get through the
// afternoon -- and in permissive mode it COUNTS what it masked. A permissive
// run cannot come back clean; it comes back with a number.
//
// A VIP'S COVERAGE IS THE VIP'S MODEL'S COVERAGE
//
// This is worth saying plainly because VIP coverage reports are quoted as if
// they were project coverage. A VIP at 100% has exercised every bin IT
// declares, which are the bins ITS authors thought of, for the specification
// revision THEY targeted. It says nothing about the bins in chapter 24.4's
// model, and in particular it says nothing about the combinations your
// device supports and the VIP's authors had never heard of.
//
// THE TWO SIDES DO NOT SPEAK AT THE SAME TIME
//
// A behavioural VIP reports a packet when it has decoded it; a pipelined
// design reports the same packet some cycles later. So the adapter aligns
// them with a small queue on each side rather than comparing cycle by cycle.
//
// And the alignment window is BOUNDED. When one side runs SKEW_MAX events
// ahead of the other, they are not skewed any more -- they have lost
// alignment, and every comparison after that point is between unrelated
// events. Widening the window is the reflex and it is wrong: it converts a
// loud failure into a quiet stream of nonsense comparisons.
package usb_vip_pkg;
// The four ways two independent readings of the same bus can differ.
// D_VIEW is the one worth owning a VIP for: both sides agree on what
// arrived and disagree on what it MEANS, which is always a specification
// reading rather than a decode bug.
typedef enum logic [2:0] {
D_NONE = 3'd0,
D_PID = 3'd1, // the two sides decoded a different PID
D_VIEW = 3'd2, // same PID, different interpretation
D_SKEW = 3'd3, // alignment lost
D_UNPAIRED = 3'd4 // an event only one side ever saw
} dis_e;
endpackage
module usb_vip_adapter
import usb_vip_pkg::*;
#(
parameter int QD = 8, // alignment queue depth
parameter int QW = 3, // log2(QD)
parameter int SKEW_MAX = 4 // events one side may run ahead
) (
input logic clk,
input logic rst_n,
// ---- the VIP's decoded view of the bus ----
input logic vip_valid,
input logic [3:0] vip_pid,
input logic [1:0] vip_view,
// ---- the design's decoded view of the SAME bus ----
input logic dut_valid,
input logic [3:0] dut_pid,
input logic [1:0] dut_view,
input logic strict, // 1: a disagreement fails. 0: it is COUNTED.
input logic eot,
output logic [QW:0] vip_depth,
output logic [QW:0] dut_depth,
output logic [QW:0] skew,
output logic agree_pulse,
output logic dis_pulse,
output dis_e dis_code,
output logic masked_pulse,
output logic [31:0] n_agree,
output logic [31:0] n_pid_dis,
output logic [31:0] n_view_dis,
output logic [31:0] n_skew,
output logic [31:0] n_masked,
output logic [31:0] n_unpaired
);
logic [3:0] vp_pid [QD];
logic [1:0] vp_vw [QD];
logic [3:0] dt_pid [QD];
logic [1:0] dt_vw [QD];
logic [QW:0] vp_n, dt_n;
dis_e code_r;
logic agr_r, dis_r, msk_r;
assign vip_depth = vp_n;
assign dut_depth = dt_n;
// Only one queue can be non-empty after a comparison, so the skew is
// simply whichever one still holds events.
assign skew = (vp_n > dt_n) ? vp_n : dt_n;
assign agree_pulse = agr_r;
assign dis_pulse = dis_r;
assign dis_code = code_r;
assign masked_pulse = msk_r;
int i;
logic [3:0] nvp_pid [QD];
logic [1:0] nvp_vw [QD];
logic [3:0] ndt_pid [QD];
logic [1:0] ndt_vw [QD];
logic [QW:0] nvp_n, ndt_n;
dis_e ncode;
logic nagr, ndis, nmsk, nunp;
logic differ;
always_comb begin
for (i = 0; i < QD; i++) begin
nvp_pid[i] = vp_pid[i]; nvp_vw[i] = vp_vw[i];
ndt_pid[i] = dt_pid[i]; ndt_vw[i] = dt_vw[i];
end
nvp_n = vp_n; ndt_n = dt_n;
ncode = D_NONE;
nagr = 1'b0; ndis = 1'b0; nmsk = 1'b0; nunp = 1'b0;
differ = 1'b0;
if (eot) begin
// ---- THE DRAIN. An event only one side ever reported. ----
//
// Not "skew that had not settled": the run is over. One of the two
// implementations saw something the other never did, which is exactly
// the class of finding a second implementation exists to produce.
if (vp_n != 0) begin
for (i = 0; i < QD - 1; i++) begin
nvp_pid[i] = nvp_pid[i+1]; nvp_vw[i] = nvp_vw[i+1];
end
nvp_n = vp_n - 1'b1;
ndis = 1'b1; ncode = D_UNPAIRED; nunp = 1'b1;
end else if (dt_n != 0) begin
for (i = 0; i < QD - 1; i++) begin
ndt_pid[i] = ndt_pid[i+1]; ndt_vw[i] = ndt_vw[i+1];
end
ndt_n = dt_n - 1'b1;
ndis = 1'b1; ncode = D_UNPAIRED; nunp = 1'b1;
end
end else begin
// ---- 1. Both sides have an event: compare the OLDEST pair. ----
if ((vp_n != 0) && (dt_n != 0)) begin
if (vp_pid[0] !== dt_pid[0]) begin
differ = 1'b1; ncode = D_PID;
end else if (vp_vw[0] !== dt_vw[0]) begin
// ---- Same PID, different INTERPRETATION. ----
//
// This is the disagreement worth having a VIP for. Both sides
// agree on what arrived and disagree on what it MEANS -- which is
// always a specification reading, and is the case an adapter is
// most often asked to paper over.
differ = 1'b1; ncode = D_VIEW;
end else begin
nagr = 1'b1;
end
if (differ) begin
if (strict) begin
ndis = 1'b1;
end else begin
// ---- PERMISSIVE. Masked, and COUNTED. ----
//
// The count is the point. A permissive run does not come back
// clean; it comes back with a number, and the number is how
// many specification arguments nobody has had yet.
nmsk = 1'b1;
end
end
for (i = 0; i < QD - 1; i++) begin
nvp_pid[i] = nvp_pid[i+1]; nvp_vw[i] = nvp_vw[i+1];
ndt_pid[i] = ndt_pid[i+1]; ndt_vw[i] = ndt_vw[i+1];
end
nvp_n = nvp_n - 1'b1;
ndt_n = ndt_n - 1'b1;
end
// ---- 2. Enqueue whatever arrived this cycle. ----
if (vip_valid && (nvp_n < (QW+1)'(QD))) begin
nvp_pid[nvp_n] = vip_pid;
nvp_vw[nvp_n] = vip_view;
nvp_n = nvp_n + 1'b1;
end
if (dut_valid && (ndt_n < (QW+1)'(QD))) begin
ndt_pid[ndt_n] = dut_pid;
ndt_vw[ndt_n] = dut_view;
ndt_n = ndt_n + 1'b1;
end
// ---- 3. THE ALIGNMENT BOUND. ----
//
// One side running SKEW_MAX events ahead of the other is not skew any
// more: alignment is lost, and every comparison after this point is
// between unrelated events. The queues are flushed so that the loss
// is reported ONCE rather than as a stream of nonsense PID
// disagreements.
//
// Widening SKEW_MAX is the reflex and it is the wrong move: it
// converts a loud failure into a quiet stream of garbage.
if ((nvp_n > (QW+1)'(SKEW_MAX)) || (ndt_n > (QW+1)'(SKEW_MAX))) begin
nvp_n = '0;
ndt_n = '0;
ndis = 1'b1;
nmsk = 1'b0;
ncode = D_SKEW;
nagr = 1'b0;
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < QD; i++) begin
vp_pid[i] <= 4'd0; vp_vw[i] <= 2'd0;
dt_pid[i] <= 4'd0; dt_vw[i] <= 2'd0;
end
vp_n <= '0;
dt_n <= '0;
code_r <= D_NONE;
agr_r <= 1'b0;
dis_r <= 1'b0;
msk_r <= 1'b0;
n_agree <= 32'd0;
n_pid_dis <= 32'd0;
n_view_dis <= 32'd0;
n_skew <= 32'd0;
n_masked <= 32'd0;
n_unpaired <= 32'd0;
end else begin
for (i = 0; i < QD; i++) begin
vp_pid[i] <= nvp_pid[i]; vp_vw[i] <= nvp_vw[i];
dt_pid[i] <= ndt_pid[i]; dt_vw[i] <= ndt_vw[i];
end
vp_n <= nvp_n;
dt_n <= ndt_n;
code_r <= ncode;
agr_r <= nagr;
dis_r <= ndis;
msk_r <= nmsk;
if (nagr) n_agree <= n_agree + 32'd1;
if (nmsk) n_masked <= n_masked + 32'd1;
// A masked disagreement still increments its CAUSE counter. The
// permissive switch decides whether the run fails; it does not decide
// whether the disagreement happened.
if (ndis || nmsk) begin
case (ncode)
D_PID: n_pid_dis <= n_pid_dis + 32'd1;
D_VIEW: n_view_dis <= n_view_dis + 32'd1;
D_SKEW: n_skew <= n_skew + 32'd1;
D_UNPAIRED: n_unpaired <= n_unpaired + 32'd1;
default: ;
endcase
end
end
end
endmodule8. VHDL-2008 Implementation
-- usb_vip_adapter -- integrating verification IP you did not write, and the
-- one job the adapter must not do.
--
-- A VIP IS A SECOND IMPLEMENTATION OF THE SPECIFICATION
--
-- That is the whole value of it. Two independent readings of the same
-- document, watching the same wires, and where they disagree ONE OF THEM IS
-- WRONG. Neither can tell you which, because each is reasoning from its own
-- model -- and that is fine, because "these two disagree, here, on this
-- packet" is already the most useful thing anybody will learn that day.
--
-- THE JOB THE ADAPTER MUST NOT DO
--
-- An adapter sits between the VIP and the design because their interfaces
-- never match exactly: different field widths, different event granularity,
-- a cycle of pipelining here, a differently-named signal there. Writing that
-- translation is legitimate work.
--
-- What is not legitimate is the step that always follows:
--
-- "the VIP flags this as an error but our device is
-- allowed to do it, so the adapter filters it out"
--
-- That sentence is sometimes true. It is a specification argument, and it
-- has to be WON -- written down, cited, and reviewed -- not implemented in a
-- shim by whoever was unblocking the regression that afternoon. Once it is
-- in the shim, the disagreement is invisible, and so is every future
-- disagreement of the same shape.
--
-- AN ADAPTER MAKES A DISAGREEMENT VISIBLE.
-- IT DOES NOT RESOLVE ONE.
--
-- So this block compares the two views and reports the difference. It has a
-- permissive mode -- because sometimes you genuinely must get through the
-- afternoon -- and in permissive mode it COUNTS what it masked. A permissive
-- run cannot come back clean; it comes back with a number.
--
-- A VIP'S COVERAGE IS THE VIP'S MODEL'S COVERAGE
--
-- This is worth saying plainly because VIP coverage reports are quoted as if
-- they were project coverage. A VIP at 100% has exercised every bin IT
-- declares, which are the bins ITS authors thought of, for the specification
-- revision THEY targeted. It says nothing about the bins in chapter 24.4's
-- model, and in particular it says nothing about the combinations your
-- device supports and the VIP's authors had never heard of.
--
-- THE TWO SIDES DO NOT SPEAK AT THE SAME TIME
--
-- A behavioural VIP reports a packet when it has decoded it; a pipelined
-- design reports the same packet some cycles later. So the adapter aligns
-- them with a small queue on each side rather than comparing cycle by cycle.
--
-- And the alignment window is BOUNDED. When one side runs SKEW_MAX events
-- ahead of the other, they are not skewed any more -- they have lost
-- alignment, and every comparison after that point is between unrelated
-- events. Widening the window is the reflex and it is wrong: it converts a
-- loud failure into a quiet stream of nonsense comparisons.
library ieee;
use ieee.std_logic_1164.all;
package usb_vip_pkg is
-- The four ways two independent readings of the same bus can differ.
-- D_VIEW is the one worth owning a VIP for: both sides agree on what
-- arrived and disagree on what it MEANS, which is always a specification
-- reading rather than a decode bug.
type dis_t is (D_NONE, D_PID, D_VIEW, D_SKEW, D_UNPAIRED);
function d_code (d : dis_t) return std_logic_vector;
end package usb_vip_pkg;
package body usb_vip_pkg is
function d_code (d : dis_t) return std_logic_vector is
begin
case d is
when D_NONE => return "000";
when D_PID => return "001";
when D_VIEW => return "010";
when D_SKEW => return "011";
when D_UNPAIRED => return "100";
end case;
end function;
end package body usb_vip_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_vip_pkg.all;
entity usb_vip_adapter is
generic (
QD : integer := 8; -- alignment queue depth
QW : integer := 3; -- log2(QD)
SKEW_MAX : integer := 4 -- events one side may run ahead
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- ---- the VIP's decoded view of the bus ----
vip_valid : in std_logic;
vip_pid : in std_logic_vector(3 downto 0);
vip_view : in std_logic_vector(1 downto 0);
-- ---- the design's decoded view of the SAME bus ----
dut_valid : in std_logic;
dut_pid : in std_logic_vector(3 downto 0);
dut_view : in std_logic_vector(1 downto 0);
strict : in std_logic; -- 1: fails. 0: the disagreement is COUNTED.
eot : in std_logic;
vip_depth : out std_logic_vector(QW downto 0);
dut_depth : out std_logic_vector(QW downto 0);
skew : out std_logic_vector(QW downto 0);
agree_pulse : out std_logic;
dis_pulse : out std_logic;
dis_code : out std_logic_vector(2 downto 0);
masked_pulse : out std_logic;
n_agree : out std_logic_vector(31 downto 0);
n_pid_dis : out std_logic_vector(31 downto 0);
n_view_dis : out std_logic_vector(31 downto 0);
n_skew : out std_logic_vector(31 downto 0);
n_masked : out std_logic_vector(31 downto 0);
n_unpaired : out std_logic_vector(31 downto 0)
);
end entity usb_vip_adapter;
architecture rtl of usb_vip_adapter is
type pid_arr is array (0 to QD-1) of std_logic_vector(3 downto 0);
type vw_arr is array (0 to QD-1) of std_logic_vector(1 downto 0);
signal vp_pid : pid_arr := (others => (others => '0'));
signal vp_vw : vw_arr := (others => (others => '0'));
signal dt_pid : pid_arr := (others => (others => '0'));
signal dt_vw : vw_arr := (others => (others => '0'));
signal vp_n, dt_n : unsigned(QW downto 0) := (others => '0');
signal code_r : dis_t := D_NONE;
signal agr_r, dis_r, msk_r : std_logic := '0';
-- Accumulators are held as unsigned rather than as range-constrained
-- integers: a constrained integer aborts simulation on overflow, which
-- turns a mutation into a crash instead of a measured kill.
signal c_ag, c_pd, c_vd : unsigned(31 downto 0) := (others => '0');
signal c_sk, c_mk, c_up : unsigned(31 downto 0) := (others => '0');
begin
vip_depth <= std_logic_vector(vp_n);
dut_depth <= std_logic_vector(dt_n);
-- Only one queue can be non-empty after a comparison, so the skew is
-- simply whichever one still holds events.
skew <= std_logic_vector(vp_n) when vp_n > dt_n else std_logic_vector(dt_n);
agree_pulse <= agr_r;
dis_pulse <= dis_r;
dis_code <= d_code(code_r);
masked_pulse <= msk_r;
n_agree <= std_logic_vector(c_ag);
n_pid_dis <= std_logic_vector(c_pd);
n_view_dis <= std_logic_vector(c_vd);
n_skew <= std_logic_vector(c_sk);
n_masked <= std_logic_vector(c_mk);
n_unpaired <= std_logic_vector(c_up);
process (clk, rst_n)
variable nvp_pid, ndt_pid : pid_arr;
variable nvp_vw, ndt_vw : vw_arr;
variable nvp_n, ndt_n : unsigned(QW downto 0);
variable ncode : dis_t;
variable nagr, ndis, nmsk, dif : std_logic;
begin
if rst_n = '0' then
vp_pid <= (others => (others => '0'));
vp_vw <= (others => (others => '0'));
dt_pid <= (others => (others => '0'));
dt_vw <= (others => (others => '0'));
vp_n <= (others => '0');
dt_n <= (others => '0');
code_r <= D_NONE;
agr_r <= '0'; dis_r <= '0'; msk_r <= '0';
c_ag <= (others => '0'); c_pd <= (others => '0');
c_vd <= (others => '0'); c_sk <= (others => '0');
c_mk <= (others => '0'); c_up <= (others => '0');
elsif rising_edge(clk) then
nvp_pid := vp_pid; nvp_vw := vp_vw;
ndt_pid := dt_pid; ndt_vw := dt_vw;
nvp_n := vp_n; ndt_n := dt_n;
ncode := D_NONE;
nagr := '0'; ndis := '0'; nmsk := '0'; dif := '0';
if eot = '1' then
-- ---- THE DRAIN. An event only one side ever reported. ----
--
-- Not "skew that had not settled": the run is over. One of the two
-- implementations saw something the other never did, which is
-- exactly the class of finding a second implementation exists to
-- produce.
if nvp_n /= 0 then
for k in 0 to QD - 2 loop
nvp_pid(k) := nvp_pid(k+1); nvp_vw(k) := nvp_vw(k+1);
end loop;
nvp_n := nvp_n - 1;
ndis := '1'; ncode := D_UNPAIRED;
elsif ndt_n /= 0 then
for k in 0 to QD - 2 loop
ndt_pid(k) := ndt_pid(k+1); ndt_vw(k) := ndt_vw(k+1);
end loop;
ndt_n := ndt_n - 1;
ndis := '1'; ncode := D_UNPAIRED;
end if;
else
-- ---- 1. Both sides have an event: compare the OLDEST pair. ----
if nvp_n /= 0 and ndt_n /= 0 then
if vp_pid(0) /= dt_pid(0) then
dif := '1'; ncode := D_PID;
elsif vp_vw(0) /= dt_vw(0) then
-- ---- Same PID, different INTERPRETATION. ----
--
-- This is the disagreement worth having a VIP for. Both sides
-- agree on what arrived and disagree on what it MEANS -- which
-- is always a specification reading, and is the case an adapter
-- is most often asked to paper over.
dif := '1'; ncode := D_VIEW;
else
nagr := '1';
end if;
if dif = '1' then
if strict = '1' then
ndis := '1';
else
-- ---- PERMISSIVE. Masked, and COUNTED. ----
--
-- The count is the point. A permissive run does not come back
-- clean; it comes back with a number, and the number is how
-- many specification arguments nobody has had yet.
nmsk := '1';
end if;
end if;
for k in 0 to QD - 2 loop
nvp_pid(k) := nvp_pid(k+1); nvp_vw(k) := nvp_vw(k+1);
ndt_pid(k) := ndt_pid(k+1); ndt_vw(k) := ndt_vw(k+1);
end loop;
nvp_n := nvp_n - 1;
ndt_n := ndt_n - 1;
end if;
-- ---- 2. Enqueue whatever arrived this cycle. ----
if vip_valid = '1' and nvp_n < to_unsigned(QD, QW+1) then
nvp_pid(to_integer(nvp_n)) := vip_pid;
nvp_vw(to_integer(nvp_n)) := vip_view;
nvp_n := nvp_n + 1;
end if;
if dut_valid = '1' and ndt_n < to_unsigned(QD, QW+1) then
ndt_pid(to_integer(ndt_n)) := dut_pid;
ndt_vw(to_integer(ndt_n)) := dut_view;
ndt_n := ndt_n + 1;
end if;
-- ---- 3. THE ALIGNMENT BOUND. ----
--
-- One side running SKEW_MAX events ahead of the other is not skew
-- any more: alignment is lost, and every comparison after this
-- point is between unrelated events. The queues are flushed so that
-- the loss is reported ONCE rather than as a stream of nonsense PID
-- disagreements.
--
-- Widening SKEW_MAX is the reflex and it is the wrong move: it
-- converts a loud failure into a quiet stream of garbage.
if nvp_n > to_unsigned(SKEW_MAX, QW+1)
or ndt_n > to_unsigned(SKEW_MAX, QW+1) then
nvp_n := (others => '0');
ndt_n := (others => '0');
ndis := '1';
nmsk := '0';
ncode := D_SKEW;
nagr := '0';
end if;
end if;
vp_pid <= nvp_pid; vp_vw <= nvp_vw;
dt_pid <= ndt_pid; dt_vw <= ndt_vw;
vp_n <= nvp_n;
dt_n <= ndt_n;
code_r <= ncode;
agr_r <= nagr;
dis_r <= ndis;
msk_r <= nmsk;
if nagr = '1' then c_ag <= c_ag + 1; end if;
if nmsk = '1' then c_mk <= c_mk + 1; end if;
-- A masked disagreement still increments its CAUSE counter. The
-- permissive switch decides whether the run fails; it does not decide
-- whether the disagreement happened.
if ndis = '1' or nmsk = '1' then
case ncode is
when D_PID => c_pd <= c_pd + 1;
when D_VIEW => c_vd <= c_vd + 1;
when D_SKEW => c_sk <= c_sk + 1;
when D_UNPAIRED => c_up <= c_up + 1;
when others => null;
end case;
end if;
end if;
end process;
end architecture rtl;9. Seeing Skew, Agreement, and Both Kinds of Masking
The VIP runs ahead, the pair matches, then the same disagreement in both modes
usb_vip_adapter — alignment, agreement, strict and permissive
10 cyclesCompare cycles 5 and 7. The disagreement is identical. dis_code reads PID in both, and n_pid_dis increments in both. The only thing strict changes is which pulse carries it — which is exactly the separation the chapter argues for: the switch decides whether the run fails, not whether the disagreement happened.
10. The Testbenches
Three exhaustive sweeps:
1. ALL 256 (vip_pid, dut_pid) pairs, views agreeing.
Agreement on exactly the 16 diagonal pairs and
disagreement on the other 240 -- which is a far
stronger statement than "it noticed the one we
injected".
2. ALL 16 (vip_view, dut_view) pairs, PIDs agreeing:
both sides saw the same packet and read it
differently.
3. Every alignment skew 0..SKEW_MAX x all 16
combinations of {vip_valid, dut_valid, strict, eot}
= 80 pairs.And the phase the chapter exists for:
// strict: it fails
b_pd = n_pid_dis; b_mk = n_masked;
pair(4'h1, 2'd0, 4'h2, 2'd0, 1'b1);
check(n_pid_dis == b_pd + 1, "a strict disagreement was not counted");
check(n_masked == b_mk, "a strict disagreement was masked");
// permissive: it is masked -- and STILL COUNTED
b_pd = n_pid_dis; b_mk = n_masked;
pair(4'h1, 2'd0, 4'h2, 2'd0, 1'b0);
check(n_masked == b_mk + 1,
"a permissive run came back clean -- masking without counting is the shim that makes a disagreement invisible, and every future disagreement of the same shape with it");
check(n_pid_dis == b_pd + 1,
"the permissive switch decided whether the disagreement HAPPENED, rather than whether the run fails");The alignment bound is checked on both edges — exactly SKEW_MAX ahead is still aligned, one more is not — and the flush is checked too, because an adapter that reports the loss without flushing reports it again on every subsequent event.
10.1 Verilog testbench
// Testbench for usb_vip_adapter (Verilog-2005).
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. ALL 256 (vip_pid, dut_pid) PAIRS, with the two views agreeing. The
// adapter must agree on exactly the 16 diagonal pairs and disagree on
// the other 240 -- which is a stronger statement than "it noticed the
// disagreement we injected".
//
// 2. ALL 16 (vip_view, dut_view) pairs with the PIDs agreeing, for the
// case that matters most: both sides saw the same packet and read it
// differently.
//
// 3. Every alignment skew from 0 to SKEW_MAX crossed with all sixteen
// combinations of {vip_valid, dut_valid, strict, eot} = 80 pairs.
//
// AND THE PROPERTY THE CHAPTER EXISTS FOR
//
// PERMISSIVE MODE STILL COUNTS. The same disagreement is driven twice, once
// strict and once permissive, and the checks are:
//
// strict dis_pulse fires cause counter +1
// permissive masked_pulse fires cause counter +1 AND n_masked +1
//
// The cause counter increments in BOTH. The permissive switch decides
// whether the run fails; it does not decide whether the disagreement
// happened, and an adapter that forgets that is the shim this chapter is
// about.
`timescale 1ns/1ps
module tb_va_v;
localparam integer QD = 8;
localparam integer QW = 3;
localparam integer SKEW_MAX = 4;
localparam [2:0] D_NONE=3'd0, D_PID=3'd1, D_VIEW=3'd2, D_SKEW=3'd3,
D_UNPAIRED=3'd4;
reg clk = 1'b0, rst_n = 1'b0;
reg vip_valid = 1'b0, dut_valid = 1'b0, strict = 1'b1, eot = 1'b0;
reg [3:0] vip_pid = 4'd0, dut_pid = 4'd0;
reg [1:0] vip_view = 2'd0, dut_view = 2'd0;
wire [QW:0] vip_depth, dut_depth, skew;
wire [2:0] dis_code;
wire agree_pulse, dis_pulse, masked_pulse;
wire [31:0] n_agree, n_pid_dis, n_view_dis, n_skew, n_masked, n_unpaired;
usb_vip_adapter #(.QD(QD), .QW(QW), .SKEW_MAX(SKEW_MAX)) dut (
.clk(clk), .rst_n(rst_n),
.vip_valid(vip_valid), .vip_pid(vip_pid), .vip_view(vip_view),
.dut_valid(dut_valid), .dut_pid(dut_pid), .dut_view(dut_view),
.strict(strict), .eot(eot),
.vip_depth(vip_depth), .dut_depth(dut_depth), .skew(skew),
.agree_pulse(agree_pulse), .dis_pulse(dis_pulse),
.dis_code(dis_code), .masked_pulse(masked_pulse),
.n_agree(n_agree), .n_pid_dis(n_pid_dis), .n_view_dis(n_view_dis),
.n_skew(n_skew), .n_masked(n_masked), .n_unpaired(n_unpaired)
);
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 | vd=%0d dd=%0d sk=%0d a=%b d=%b(%0d) m=%b",
$time, msg, vip_depth, dut_depth, skew, agree_pulse,
dis_pulse, dis_code, masked_pulse);
end
end
endtask
// ------------------------------------------------------------------
// The shadow adapter. Its own queues.
// ------------------------------------------------------------------
reg [3:0] mvp_pid [0:QD-1];
reg [1:0] mvp_vw [0:QD-1];
reg [3:0] mdt_pid [0:QD-1];
reg [1:0] mdt_vw [0:QD-1];
reg [QW:0] mvp_n, mdt_n;
reg [2:0] m_code;
reg m_agr, m_dis, m_msk;
integer m_ag, m_pd, m_vd, m_sk, m_mk, m_up;
integer seen [0:79]; // (SKEW_MAX+1) x 16 input combinations
integer n_seen, n_steps;
task model_reset;
integer j;
begin
for (j = 0; j < QD; j = j + 1) begin
mvp_pid[j] = 4'd0; mvp_vw[j] = 2'd0;
mdt_pid[j] = 4'd0; mdt_vw[j] = 2'd0;
end
mvp_n = 0; mdt_n = 0; m_code = D_NONE;
m_agr = 1'b0; m_dis = 1'b0; m_msk = 1'b0;
m_ag = 0; m_pd = 0; m_vd = 0; m_sk = 0; m_mk = 0; m_up = 0;
for (j = 0; j < 80; j = j + 1) seen[j] = 0;
n_seen = 0; n_steps = 0;
end
endtask
integer q, idx;
task step(input vv, input [3:0] vp, input [1:0] vw,
input dv, input [3:0] dp, input [1:0] dw,
input st, input eo);
reg [2:0] ncode;
reg nagr, ndis, nmsk, dif;
reg [QW:0] msk_now;
begin
vip_valid = vv; vip_pid = vp; vip_view = vw;
dut_valid = dv; dut_pid = dp; dut_view = dw;
strict = st; eot = eo;
#1;
check(vip_depth === mvp_n, "vip_depth disagrees with the shadow adapter");
check(dut_depth === mdt_n, "dut_depth disagrees");
check(skew === ((mvp_n > mdt_n) ? mvp_n : mdt_n), "skew disagrees");
check(agree_pulse === m_agr, "the agree pulse disagrees");
check(dis_pulse === m_dis, "the disagreement pulse disagrees");
check(dis_code === m_code, "dis_code disagrees");
check(masked_pulse === m_msk, "the masked pulse disagrees");
check(!(agree_pulse && dis_pulse),
"a comparison was reported as both agreement and disagreement");
check(!(dis_pulse && masked_pulse),
"a disagreement was reported as both failed and masked");
check(skew <= SKEW_MAX[QW:0] + 1,
"the alignment queues grew past the bound -- every comparison after that point is between unrelated events");
msk_now = (mvp_n > mdt_n) ? mvp_n : mdt_n;
idx = msk_now * 16 + (vv ? 8 : 0) + (dv ? 4 : 0) + (st ? 2 : 0) + (eo ? 1 : 0);
if (idx < 80) begin
if (seen[idx] == 0) begin seen[idx] = 1; n_seen = n_seen + 1; end
end
n_steps = n_steps + 1;
// ---- advance the shadow adapter ----
ncode = D_NONE; nagr = 1'b0; ndis = 1'b0; nmsk = 1'b0; dif = 1'b0;
if (eo) begin
if (mvp_n != 0) begin
for (q = 0; q < QD - 1; q = q + 1) begin
mvp_pid[q] = mvp_pid[q+1]; mvp_vw[q] = mvp_vw[q+1];
end
mvp_n = mvp_n - 1'b1;
ndis = 1'b1; ncode = D_UNPAIRED;
end else if (mdt_n != 0) begin
for (q = 0; q < QD - 1; q = q + 1) begin
mdt_pid[q] = mdt_pid[q+1]; mdt_vw[q] = mdt_vw[q+1];
end
mdt_n = mdt_n - 1'b1;
ndis = 1'b1; ncode = D_UNPAIRED;
end
end else begin
if ((mvp_n != 0) && (mdt_n != 0)) begin
if (mvp_pid[0] !== mdt_pid[0]) begin dif = 1'b1; ncode = D_PID; end
else if (mvp_vw[0] !== mdt_vw[0]) begin dif = 1'b1; ncode = D_VIEW; end
else nagr = 1'b1;
if (dif) begin
if (st) ndis = 1'b1; else nmsk = 1'b1;
end
for (q = 0; q < QD - 1; q = q + 1) begin
mvp_pid[q] = mvp_pid[q+1]; mvp_vw[q] = mvp_vw[q+1];
mdt_pid[q] = mdt_pid[q+1]; mdt_vw[q] = mdt_vw[q+1];
end
mvp_n = mvp_n - 1'b1;
mdt_n = mdt_n - 1'b1;
end
if (vv && (mvp_n < QD[QW:0])) begin
mvp_pid[mvp_n] = vp; mvp_vw[mvp_n] = vw; mvp_n = mvp_n + 1'b1;
end
if (dv && (mdt_n < QD[QW:0])) begin
mdt_pid[mdt_n] = dp; mdt_vw[mdt_n] = dw; mdt_n = mdt_n + 1'b1;
end
if ((mvp_n > SKEW_MAX[QW:0]) || (mdt_n > SKEW_MAX[QW:0])) begin
mvp_n = 0; mdt_n = 0;
ndis = 1'b1; nmsk = 1'b0; ncode = D_SKEW; nagr = 1'b0;
end
end
m_code = ncode; m_agr = nagr; m_dis = ndis; m_msk = nmsk;
if (nagr) m_ag = m_ag + 1;
if (nmsk) m_mk = m_mk + 1;
if (ndis || nmsk) begin
case (ncode)
D_PID: m_pd = m_pd + 1;
D_VIEW: m_vd = m_vd + 1;
D_SKEW: m_sk = m_sk + 1;
D_UNPAIRED: m_up = m_up + 1;
default: ;
endcase
end
@(posedge clk); #1;
vip_valid = 1'b0; dut_valid = 1'b0; eot = 1'b0;
end
endtask
task nop(input integer n);
integer j;
begin
for (j = 0; j < n; j = j + 1)
step(1'b0,4'd0,2'd0, 1'b0,4'd0,2'd0, strict, 1'b0);
end
endtask
// Feed both sides one event each and let the comparison happen.
task pair(input [3:0] vp, input [1:0] vw, input [3:0] dp, input [1:0] dw,
input st);
begin
step(1'b1,vp,vw, 1'b1,dp,dw, st, 1'b0);
step(1'b0,4'd0,2'd0, 1'b0,4'd0,2'd0, st, 1'b0);
end
endtask
// Drain the alignment queues the way the design provides for.
task drain;
integer j;
begin
for (j = 0; j < 2*QD + 2; j = j + 1)
step(1'b0,4'd0,2'd0, 1'b0,4'd0,2'd0, strict, 1'b1);
check(vip_depth === 4'd0 && dut_depth === 4'd0,
"the drain did not empty the alignment queues");
nop(1);
end
endtask
integer a, b, k, b_ag, b_pd, b_vd, b_mk, b_up, b_sk, sk2, cb;
initial begin
model_reset;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(vip_depth === 4'd0 && dut_depth === 4'd0, "reset left events queued");
check(agree_pulse === 1'b0 && dis_pulse === 1'b0,
"reset asserted a comparison result");
// ---- Phase B: ALL 256 PID PAIRS. Agreement on exactly the diagonal. --
for (a = 0; a < 16; a = a + 1) begin
for (b = 0; b < 16; b = b + 1) begin
b_ag = n_agree; b_pd = n_pid_dis;
pair(a[3:0], 2'd1, b[3:0], 2'd1, 1'b1);
if (a == b) begin
check(n_agree == b_ag + 1,
"the two sides reported the same PID and the adapter did not agree");
check(n_pid_dis == b_pd,
"identical PIDs were reported as a disagreement");
end else begin
check(n_pid_dis == b_pd + 1,
"the two sides decoded DIFFERENT PIDs and the adapter did not say so -- an adapter that resolves a disagreement has deleted the only finding a second implementation can produce");
check(n_agree == b_ag,
"a PID disagreement was counted as agreement");
end
end
end
// ---- Phase C: ALL 16 VIEW PAIRS, with the PIDs agreeing. ----
//
// Both sides saw the same packet and read it differently. This is the
// disagreement worth owning a VIP for, and the one an adapter is most
// often asked to paper over.
for (a = 0; a < 4; a = a + 1) begin
for (b = 0; b < 4; b = b + 1) begin
b_ag = n_agree; b_vd = n_view_dis;
pair(4'hC, a[1:0], 4'hC, b[1:0], 1'b1);
if (a == b) begin
check(n_agree == b_ag + 1, "identical views were not agreed");
check(n_view_dis == b_vd, "identical views were reported as a disagreement");
end else begin
check(n_view_dis == b_vd + 1,
"both sides saw the same packet and read it differently, and the adapter said nothing");
check(n_agree == b_ag, "a view disagreement was counted as agreement");
end
end
end
// ---- Phase D: PERMISSIVE STILL COUNTS. ----
for (k = 0; k < 30; k = k + 1) begin
// strict: it fails
b_pd = n_pid_dis; b_mk = n_masked;
pair(4'h1, 2'd0, 4'h2, 2'd0, 1'b1);
check(n_pid_dis == b_pd + 1, "a strict disagreement was not counted");
check(n_masked == b_mk, "a strict disagreement was masked");
// permissive: it is masked -- and STILL COUNTED
b_pd = n_pid_dis; b_mk = n_masked;
pair(4'h1, 2'd0, 4'h2, 2'd0, 1'b0);
check(n_masked == b_mk + 1,
"a permissive run came back clean -- masking without counting is the shim that makes a disagreement invisible, and every future disagreement of the same shape with it");
check(n_pid_dis == b_pd + 1,
"the permissive switch decided whether the disagreement HAPPENED, rather than whether the run fails");
end
// ---- Phase E: THE ALIGNMENT BOUND, both edges. ----
drain;
// exactly SKEW_MAX ahead: still aligned, no failure
b_sk = n_skew;
for (k = 0; k < SKEW_MAX; k = k + 1)
step(1'b1, 4'h5, 2'd0, 1'b0, 4'd0, 2'd0, 1'b1, 1'b0);
check(skew === SKEW_MAX[QW:0],
"the queue did not reach the alignment bound");
check(n_skew == b_sk,
"alignment was declared lost AT the bound rather than past it");
// one more: alignment is lost
step(1'b1, 4'h5, 2'd0, 1'b0, 4'd0, 2'd0, 1'b1, 1'b0);
nop(1);
check(n_skew == b_sk + 1,
"one side ran more than SKEW_MAX events ahead and the adapter carried on comparing unrelated events");
check(skew === 4'd0,
"the queues were not flushed after alignment was lost, so the loss will be reported again on every subsequent event");
// ---- Phase F: UNPAIRED at end of test. ----
drain;
for (k = 1; k <= SKEW_MAX; k = k + 1) begin
b_up = n_unpaired;
for (a = 0; a < k; a = a + 1)
step(1'b1, 4'h7, 2'd0, 1'b0, 4'd0, 2'd0, 1'b1, 1'b0);
drain;
check(n_unpaired == b_up + k,
"an event that only one side ever reported was not named at end of test -- that is exactly the class of finding a second implementation exists to produce");
end
// ---- Phase G: EXHAUSTIVE. Every skew x every input combination. ----
for (sk2 = 0; sk2 <= SKEW_MAX; sk2 = sk2 + 1) begin
for (cb = 0; cb < 16; cb = cb + 1) begin
drain;
for (a = 0; a < sk2; a = a + 1)
step(1'b1, 4'h9, 2'd0, 1'b0, 4'd0, 2'd0, 1'b1, 1'b0);
check(skew === sk2[QW:0],
"the sweep could not reach the skew it meant to reach");
step(cb[3], 4'h9, 2'd0, cb[2], 4'h9, 2'd0, cb[1], cb[0]);
step(cb[3], 4'h9, 2'd0, cb[2], 4'h9, 2'd0, cb[1], cb[0]);
end
end
// ---- Phase H: random ----
for (k = 0; k < 32000; k = k + 1)
step(($unsigned($random) % 100) < 42, $random, $random,
($unsigned($random) % 100) < 42, $random, $random,
($unsigned($random) % 100) < 70,
($unsigned($random) % 1000) < 8);
// ---- Phase I: clean agreement afterwards, so the adapter is shown to
// ---- still work rather than merely to have stopped.
drain;
b_ag = n_agree;
for (k = 0; k < 200; k = k + 1) pair(4'hE, 2'd2, 4'hE, 2'd2, 1'b1);
check(n_agree == b_ag + 200,
"the adapter stopped agreeing on identical views after the random phase");
// ---- Final agreement ----
check(n_agree === m_ag[31:0], "n_agree disagrees with the model");
check(n_pid_dis === m_pd[31:0], "n_pid_dis disagrees");
check(n_view_dis === m_vd[31:0], "n_view_dis disagrees");
check(n_skew === m_sk[31:0], "n_skew disagrees");
check(n_masked === m_mk[31:0], "n_masked disagrees");
check(n_unpaired === m_up[31:0], "n_unpaired disagrees");
check(n_seen == 80, "not every alignment skew was crossed with every input combination");
check(n_agree > 32'd0, "the two sides never agreed about anything");
check(n_pid_dis > 32'd0, "a PID disagreement was never seen");
check(n_view_dis > 32'd0, "a view disagreement was never seen");
check(n_skew > 32'd0, "alignment was never lost");
check(n_masked > 32'd0, "permissive mode was never exercised");
check(n_unpaired > 32'd0, "an unpaired event was never seen");
$display("REACH skew-x-input=%0d/80 steps=%0d", n_seen, n_steps);
$display("COUNTERS agree=%0d pid-dis=%0d view-dis=%0d skew=%0d masked=%0d unpaired=%0d",
n_agree, n_pid_dis, n_view_dis, n_skew, n_masked, n_unpaired);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule10.2 SystemVerilog testbench
// Testbench for usb_vip_adapter (SystemVerilog).
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. ALL 256 (vip_pid, dut_pid) PAIRS, with the two views agreeing. The
// adapter must agree on exactly the 16 diagonal pairs and disagree on
// the other 240 -- which is a stronger statement than "it noticed the
// disagreement we injected".
//
// 2. ALL 16 (vip_view, dut_view) pairs with the PIDs agreeing, for the
// case that matters most: both sides saw the same packet and read it
// differently.
//
// 3. Every alignment skew from 0 to SKEW_MAX crossed with all sixteen
// combinations of {vip_valid, dut_valid, strict, eot} = 80 pairs.
//
// AND THE PROPERTY THE CHAPTER EXISTS FOR
//
// PERMISSIVE MODE STILL COUNTS. The same disagreement is driven twice, once
// strict and once permissive, and the checks are:
//
// strict dis_pulse fires cause counter +1
// permissive masked_pulse fires cause counter +1 AND n_masked +1
//
// The cause counter increments in BOTH. The permissive switch decides
// whether the run fails; it does not decide whether the disagreement
// happened, and an adapter that forgets that is the shim this chapter is
// about.
`timescale 1ns/1ps
module tb_va_sv;
import usb_vip_pkg::*;
localparam int QD = 8;
localparam int QW = 3;
localparam int SKEW_MAX = 4;
logic clk = 1'b0, rst_n = 1'b0;
logic vip_valid = 1'b0, dut_valid = 1'b0, strict = 1'b1, eot = 1'b0;
logic [3:0] vip_pid = '0, dut_pid = '0;
logic [1:0] vip_view = '0, dut_view = '0;
logic [QW:0] vip_depth, dut_depth, skew;
dis_e dis_code;
logic agree_pulse, dis_pulse, masked_pulse;
logic [31:0] n_agree, n_pid_dis, n_view_dis, n_skew, n_masked, n_unpaired;
usb_vip_adapter #(.QD(QD), .QW(QW), .SKEW_MAX(SKEW_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 | vd=%0d dd=%0d sk=%0d a=%b d=%b(%0d) m=%b",
$time, msg, vip_depth, dut_depth, skew, agree_pulse,
dis_pulse, dis_code, masked_pulse);
end
end
endtask
// ------------------------------------------------------------------
// The shadow adapter. Its own queues.
// ------------------------------------------------------------------
logic [3:0] mvp_pid [QD];
logic [1:0] mvp_vw [QD];
logic [3:0] mdt_pid [QD];
logic [1:0] mdt_vw [QD];
logic [QW:0] mvp_n, mdt_n;
dis_e m_code;
logic m_agr, m_dis, m_msk;
int m_ag, m_pd, m_vd, m_sk, m_mk, m_up;
int seen [80]; // (SKEW_MAX+1) x 16 input combinations
int n_seen, n_steps;
task automatic model_reset();
int j;
begin
for (j = 0; j < QD; j++) begin
mvp_pid[j] = 4'd0; mvp_vw[j] = 2'd0;
mdt_pid[j] = 4'd0; mdt_vw[j] = 2'd0;
end
mvp_n = 0; mdt_n = 0; m_code = D_NONE;
m_agr = 1'b0; m_dis = 1'b0; m_msk = 1'b0;
m_ag = 0; m_pd = 0; m_vd = 0; m_sk = 0; m_mk = 0; m_up = 0;
for (j = 0; j < 80; j++) seen[j] = 0;
n_seen = 0; n_steps = 0;
end
endtask
int q, idx;
task automatic step(input logic vv, input logic [3:0] vp, input logic [1:0] vw,
input logic dv, input logic [3:0] dp, input logic [1:0] dw,
input logic st, input logic eo);
dis_e ncode;
logic nagr, ndis, nmsk, dif;
logic [QW:0] msk_now;
begin
vip_valid = vv; vip_pid = vp; vip_view = vw;
dut_valid = dv; dut_pid = dp; dut_view = dw;
strict = st; eot = eo;
#1;
check(vip_depth === mvp_n, "vip_depth disagrees with the shadow adapter");
check(dut_depth === mdt_n, "dut_depth disagrees");
check(skew === ((mvp_n > mdt_n) ? mvp_n : mdt_n), "skew disagrees");
check(agree_pulse === m_agr, "the agree pulse disagrees");
check(dis_pulse === m_dis, "the disagreement pulse disagrees");
check(dis_code === m_code, "dis_code disagrees");
check(masked_pulse === m_msk, "the masked pulse disagrees");
check(!(agree_pulse && dis_pulse),
"a comparison was reported as both agreement and disagreement");
check(!(dis_pulse && masked_pulse),
"a disagreement was reported as both failed and masked");
check(skew <= (QW+1)'(SKEW_MAX) + 1,
"the alignment queues grew past the bound -- every comparison after that point is between unrelated events");
msk_now = (mvp_n > mdt_n) ? mvp_n : mdt_n;
idx = int'(msk_now) * 16 + (vv ? 8 : 0) + (dv ? 4 : 0) + (st ? 2 : 0) + (eo ? 1 : 0);
if (idx < 80) begin
if (seen[idx] == 0) begin seen[idx] = 1; n_seen = n_seen + 1; end
end
n_steps++;
// ---- advance the shadow adapter ----
ncode = D_NONE; nagr = 1'b0; ndis = 1'b0; nmsk = 1'b0; dif = 1'b0;
if (eo) begin
if (mvp_n != 0) begin
for (q = 0; q < QD - 1; q++) begin
mvp_pid[q] = mvp_pid[q+1]; mvp_vw[q] = mvp_vw[q+1];
end
mvp_n = mvp_n - 1'b1;
ndis = 1'b1; ncode = D_UNPAIRED;
end else if (mdt_n != 0) begin
for (q = 0; q < QD - 1; q++) begin
mdt_pid[q] = mdt_pid[q+1]; mdt_vw[q] = mdt_vw[q+1];
end
mdt_n = mdt_n - 1'b1;
ndis = 1'b1; ncode = D_UNPAIRED;
end
end else begin
if ((mvp_n != 0) && (mdt_n != 0)) begin
if (mvp_pid[0] !== mdt_pid[0]) begin dif = 1'b1; ncode = D_PID; end
else if (mvp_vw[0] !== mdt_vw[0]) begin dif = 1'b1; ncode = D_VIEW; end
else nagr = 1'b1;
if (dif) begin
if (st) ndis = 1'b1; else nmsk = 1'b1;
end
for (q = 0; q < QD - 1; q++) begin
mvp_pid[q] = mvp_pid[q+1]; mvp_vw[q] = mvp_vw[q+1];
mdt_pid[q] = mdt_pid[q+1]; mdt_vw[q] = mdt_vw[q+1];
end
mvp_n = mvp_n - 1'b1;
mdt_n = mdt_n - 1'b1;
end
if (vv && (mvp_n < (QW+1)'(QD))) begin
mvp_pid[mvp_n] = vp; mvp_vw[mvp_n] = vw; mvp_n = mvp_n + 1'b1;
end
if (dv && (mdt_n < (QW+1)'(QD))) begin
mdt_pid[mdt_n] = dp; mdt_vw[mdt_n] = dw; mdt_n = mdt_n + 1'b1;
end
if ((mvp_n > (QW+1)'(SKEW_MAX)) || (mdt_n > (QW+1)'(SKEW_MAX))) begin
mvp_n = 0; mdt_n = 0;
ndis = 1'b1; nmsk = 1'b0; ncode = D_SKEW; nagr = 1'b0;
end
end
m_code = ncode; m_agr = nagr; m_dis = ndis; m_msk = nmsk;
if (nagr) m_ag = m_ag + 1;
if (nmsk) m_mk = m_mk + 1;
if (ndis || nmsk) begin
case (ncode)
D_PID: m_pd = m_pd + 1;
D_VIEW: m_vd = m_vd + 1;
D_SKEW: m_sk = m_sk + 1;
D_UNPAIRED: m_up = m_up + 1;
default: ;
endcase
end
@(posedge clk); #1;
vip_valid = 1'b0; dut_valid = 1'b0; eot = 1'b0;
end
endtask
task automatic nop(input int n);
repeat (n) step(1'b0,4'd0,2'd0, 1'b0,4'd0,2'd0, strict, 1'b0);
endtask
// Feed both sides one event each and let the comparison happen.
task automatic pair(input logic [3:0] vp, input logic [1:0] vw,
input logic [3:0] dp, input logic [1:0] dw,
input logic st);
begin
step(1'b1,vp,vw, 1'b1,dp,dw, st, 1'b0);
step(1'b0,4'd0,2'd0, 1'b0,4'd0,2'd0, st, 1'b0);
end
endtask
// Drain the alignment queues the way the design provides for.
task automatic drain();
repeat (2*QD + 2) step(1'b0,4'd0,2'd0, 1'b0,4'd0,2'd0, strict, 1'b1);
check(vip_depth === '0 && dut_depth === '0,
"the drain did not empty the alignment queues");
nop(1);
endtask
int a, b, k, b_ag, b_pd, b_vd, b_mk, b_up, b_sk, sk2, cb;
initial begin
model_reset();
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(vip_depth === 4'd0 && dut_depth === 4'd0, "reset left events queued");
check(agree_pulse === 1'b0 && dis_pulse === 1'b0,
"reset asserted a comparison result");
// ---- Phase B: ALL 256 PID PAIRS. Agreement on exactly the diagonal. --
for (a = 0; a < 16; a++) begin
for (b = 0; b < 16; b++) begin
b_ag = n_agree; b_pd = n_pid_dis;
pair(4'(a), 2'd1, 4'(b), 2'd1, 1'b1);
if (a == b) begin
check(n_agree == b_ag + 1,
"the two sides reported the same PID and the adapter did not agree");
check(n_pid_dis == b_pd,
"identical PIDs were reported as a disagreement");
end else begin
check(n_pid_dis == b_pd + 1,
"the two sides decoded DIFFERENT PIDs and the adapter did not say so -- an adapter that resolves a disagreement has deleted the only finding a second implementation can produce");
check(n_agree == b_ag,
"a PID disagreement was counted as agreement");
end
end
end
// ---- Phase C: ALL 16 VIEW PAIRS, with the PIDs agreeing. ----
//
// Both sides saw the same packet and read it differently. This is the
// disagreement worth owning a VIP for, and the one an adapter is most
// often asked to paper over.
for (a = 0; a < 4; a++) begin
for (b = 0; b < 4; b++) begin
b_ag = n_agree; b_vd = n_view_dis;
pair(4'hC, 2'(a), 4'hC, 2'(b), 1'b1);
if (a == b) begin
check(n_agree == b_ag + 1, "identical views were not agreed");
check(n_view_dis == b_vd, "identical views were reported as a disagreement");
end else begin
check(n_view_dis == b_vd + 1,
"both sides saw the same packet and read it differently, and the adapter said nothing");
check(n_agree == b_ag, "a view disagreement was counted as agreement");
end
end
end
// ---- Phase D: PERMISSIVE STILL COUNTS. ----
for (k = 0; k < 30; k++) begin
// strict: it fails
b_pd = n_pid_dis; b_mk = n_masked;
pair(4'h1, 2'd0, 4'h2, 2'd0, 1'b1);
check(n_pid_dis == b_pd + 1, "a strict disagreement was not counted");
check(n_masked == b_mk, "a strict disagreement was masked");
// permissive: it is masked -- and STILL COUNTED
b_pd = n_pid_dis; b_mk = n_masked;
pair(4'h1, 2'd0, 4'h2, 2'd0, 1'b0);
check(n_masked == b_mk + 1,
"a permissive run came back clean -- masking without counting is the shim that makes a disagreement invisible, and every future disagreement of the same shape with it");
check(n_pid_dis == b_pd + 1,
"the permissive switch decided whether the disagreement HAPPENED, rather than whether the run fails");
end
// ---- Phase E: THE ALIGNMENT BOUND, both edges. ----
drain();
// exactly SKEW_MAX ahead: still aligned, no failure
b_sk = n_skew;
for (k = 0; k < SKEW_MAX; k++)
step(1'b1, 4'h5, 2'd0, 1'b0, 4'd0, 2'd0, 1'b1, 1'b0);
check(skew === (QW+1)'(SKEW_MAX),
"the queue did not reach the alignment bound");
check(n_skew == b_sk,
"alignment was declared lost AT the bound rather than past it");
// one more: alignment is lost
step(1'b1, 4'h5, 2'd0, 1'b0, 4'd0, 2'd0, 1'b1, 1'b0);
nop(1);
check(n_skew == b_sk + 1,
"one side ran more than SKEW_MAX events ahead and the adapter carried on comparing unrelated events");
check(skew === '0,
"the queues were not flushed after alignment was lost, so the loss will be reported again on every subsequent event");
// ---- Phase F: UNPAIRED at end of test. ----
drain();
for (k = 1; k <= SKEW_MAX; k++) begin
b_up = n_unpaired;
for (a = 0; a < k; a++)
step(1'b1, 4'h7, 2'd0, 1'b0, 4'd0, 2'd0, 1'b1, 1'b0);
drain();
check(n_unpaired == b_up + k,
"an event that only one side ever reported was not named at end of test -- that is exactly the class of finding a second implementation exists to produce");
end
// ---- Phase G: EXHAUSTIVE. Every skew x every input combination. ----
for (sk2 = 0; sk2 <= SKEW_MAX; sk2++) begin
for (cb = 0; cb < 16; cb++) begin
drain();
for (a = 0; a < sk2; a++)
step(1'b1, 4'h9, 2'd0, 1'b0, 4'd0, 2'd0, 1'b1, 1'b0);
check(skew === (QW+1)'(sk2),
"the sweep could not reach the skew it meant to reach");
step(1'(cb[3]), 4'h9, 2'd0, 1'(cb[2]), 4'h9, 2'd0, 1'(cb[1]), 1'(cb[0]));
step(1'(cb[3]), 4'h9, 2'd0, 1'(cb[2]), 4'h9, 2'd0, 1'(cb[1]), 1'(cb[0]));
end
end
// ---- Phase H: random ----
for (k = 0; k < 32000; k++)
step($urandom_range(0,99) < 42, 4'($urandom()), 2'($urandom()),
$urandom_range(0,99) < 42, 4'($urandom()), 2'($urandom()),
$urandom_range(0,99) < 70,
$urandom_range(0,999) < 8);
// ---- Phase I: clean agreement afterwards, so the adapter is shown to
// ---- still work rather than merely to have stopped.
drain();
b_ag = n_agree;
for (k = 0; k < 200; k++) pair(4'hE, 2'd2, 4'hE, 2'd2, 1'b1);
check(n_agree == b_ag + 200,
"the adapter stopped agreeing on identical views after the random phase");
// ---- Final agreement ----
check(n_agree === 32'(m_ag), "n_agree disagrees with the model");
check(n_pid_dis === 32'(m_pd), "n_pid_dis disagrees");
check(n_view_dis === 32'(m_vd), "n_view_dis disagrees");
check(n_skew === 32'(m_sk), "n_skew disagrees");
check(n_masked === 32'(m_mk), "n_masked disagrees");
check(n_unpaired === 32'(m_up), "n_unpaired disagrees");
check(n_seen == 80, "not every alignment skew was crossed with every input combination");
check(n_agree > 32'd0, "the two sides never agreed about anything");
check(n_pid_dis > 32'd0, "a PID disagreement was never seen");
check(n_view_dis > 32'd0, "a view disagreement was never seen");
check(n_skew > 32'd0, "alignment was never lost");
check(n_masked > 32'd0, "permissive mode was never exercised");
check(n_unpaired > 32'd0, "an unpaired event was never seen");
$display("REACH skew-x-input=%0d/80 steps=%0d", n_seen, n_steps);
$display("COUNTERS agree=%0d pid-dis=%0d view-dis=%0d skew=%0d masked=%0d unpaired=%0d",
n_agree, n_pid_dis, n_view_dis, n_skew, n_masked, n_unpaired);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule10.3 VHDL testbench
-- Testbench for usb_vip_adapter (VHDL-2008).
--
-- WHAT IS EXHAUSTIVE HERE
--
-- 1. ALL 256 (vip_pid, dut_pid) PAIRS, with the two views agreeing. The
-- adapter must agree on exactly the 16 diagonal pairs and disagree on
-- the other 240 -- which is a stronger statement than "it noticed the
-- disagreement we injected".
--
-- 2. ALL 16 (vip_view, dut_view) pairs with the PIDs agreeing, for the
-- case that matters most: both sides saw the same packet and read it
-- differently.
--
-- 3. Every alignment skew from 0 to SKEW_MAX crossed with all sixteen
-- combinations of (vip_valid, dut_valid, strict, eot) = 80 pairs.
--
-- AND THE PROPERTY THE CHAPTER EXISTS FOR
--
-- PERMISSIVE MODE STILL COUNTS. The same disagreement is driven twice, once
-- strict and once permissive, and the checks are:
--
-- strict dis_pulse fires cause counter +1
-- permissive masked_pulse fires cause counter +1 AND n_masked +1
--
-- The cause counter increments in BOTH. The permissive switch decides
-- whether the run fails; it does not decide whether the disagreement
-- happened, and an adapter that forgets that is the shim this chapter is
-- about.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_vip_pkg.all;
entity tb_va_vhdl is
end entity tb_va_vhdl;
architecture sim of tb_va_vhdl is
constant QD : integer := 8;
constant QW : integer := 3;
constant SKEW_MAX : integer := 4;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal vip_valid, dut_valid, eot : std_logic := '0';
signal strict : std_logic := '1';
signal vip_pid, dut_pid : std_logic_vector(3 downto 0) := (others => '0');
signal vip_view, dut_view : std_logic_vector(1 downto 0) := (others => '0');
signal vip_depth, dut_depth, skew : std_logic_vector(QW downto 0);
signal dis_code : std_logic_vector(2 downto 0);
signal agree_pulse, dis_pulse, masked_pulse : std_logic;
signal n_agree, n_pid_dis, n_view_dis : std_logic_vector(31 downto 0);
signal n_skew, n_masked, n_unpaired : std_logic_vector(31 downto 0);
begin
dut : entity work.usb_vip_adapter
generic map (QD => QD, QW => QW, SKEW_MAX => SKEW_MAX)
port map (
clk => clk, rst_n => rst_n,
vip_valid => vip_valid, vip_pid => vip_pid, vip_view => vip_view,
dut_valid => dut_valid, dut_pid => dut_pid, dut_view => dut_view,
strict => strict, eot => eot,
vip_depth => vip_depth, dut_depth => dut_depth, skew => skew,
agree_pulse => agree_pulse, dis_pulse => dis_pulse,
dis_code => dis_code, masked_pulse => masked_pulse,
n_agree => n_agree, n_pid_dis => n_pid_dis, n_view_dis => n_view_dis,
n_skew => n_skew, n_masked => n_masked, n_unpaired => n_unpaired
);
clk <= (not clk) after 5 ns when not done else '0';
stim : process
type pid_arr is array (0 to QD-1) of std_logic_vector(3 downto 0);
type vw_arr is array (0 to QD-1) of std_logic_vector(1 downto 0);
type seen_arr is array (0 to 79) of integer;
variable errors, checks : integer := 0;
-- ---- The shadow adapter. Its own queues. ----
variable mvp_pid, mdt_pid : pid_arr := (others => (others => '0'));
variable mvp_vw, mdt_vw : vw_arr := (others => (others => '0'));
variable mvp_n, mdt_n : unsigned(QW downto 0) := (others => '0');
variable m_code : dis_t := D_NONE;
variable m_agr, m_dis, m_msk : std_logic := '0';
variable m_ag, m_pd, m_vd, m_sk, m_mk, m_up : integer := 0;
variable seen : seen_arr := (others => 0);
variable n_seen, n_steps : integer := 0;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"7A5B3C1D";
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_slv (w : integer) return std_logic_vector is
variable u : unsigned(31 downto 0);
begin
u := rnd32;
return std_logic_vector(u(w-1 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 &
" | vd=" & integer'image(to_integer(mvp_n)) &
" dd=" & integer'image(to_integer(mdt_n)) &
" code=" & integer'image(dis_t'pos(m_code))
severity note;
end if;
end if;
end procedure;
procedure step (vv : std_logic; vp : std_logic_vector(3 downto 0);
vw : std_logic_vector(1 downto 0);
dv : std_logic; dp : std_logic_vector(3 downto 0);
dw : std_logic_vector(1 downto 0);
st, eo : std_logic) is
variable ncode : dis_t;
variable nagr, ndis, nmsk, dif : std_logic;
variable msk_now : unsigned(QW downto 0);
variable idx : integer;
begin
vip_valid <= vv; vip_pid <= vp; vip_view <= vw;
dut_valid <= dv; dut_pid <= dp; dut_view <= dw;
strict <= st; eot <= eo;
wait for 1 ns;
chk(unsigned(vip_depth) = mvp_n, "vip_depth disagrees with the shadow adapter");
chk(unsigned(dut_depth) = mdt_n, "dut_depth disagrees");
if mvp_n > mdt_n then
chk(unsigned(skew) = mvp_n, "skew disagrees");
else
chk(unsigned(skew) = mdt_n, "skew disagrees");
end if;
chk(agree_pulse = m_agr, "the agree pulse disagrees");
chk(dis_pulse = m_dis, "the disagreement pulse disagrees");
chk(dis_code = d_code(m_code), "dis_code disagrees");
chk(masked_pulse = m_msk, "the masked pulse disagrees");
chk(not (agree_pulse = '1' and dis_pulse = '1'),
"a comparison was reported as both agreement and disagreement");
chk(not (dis_pulse = '1' and masked_pulse = '1'),
"a disagreement was reported as both failed and masked");
chk(unsigned(skew) <= to_unsigned(SKEW_MAX + 1, QW+1),
"the alignment queues grew past the bound -- every comparison after that point is between unrelated events");
if mvp_n > mdt_n then msk_now := mvp_n; else msk_now := mdt_n; end if;
idx := to_integer(msk_now) * 16;
if vv = '1' then idx := idx + 8; end if;
if dv = '1' then idx := idx + 4; end if;
if st = '1' then idx := idx + 2; end if;
if eo = '1' then idx := idx + 1; end if;
if idx < 80 then
if seen(idx) = 0 then seen(idx) := 1; n_seen := n_seen + 1; end if;
end if;
n_steps := n_steps + 1;
-- ---- advance the shadow adapter ----
ncode := D_NONE; nagr := '0'; ndis := '0'; nmsk := '0'; dif := '0';
if eo = '1' then
if mvp_n /= 0 then
for k in 0 to QD - 2 loop
mvp_pid(k) := mvp_pid(k+1); mvp_vw(k) := mvp_vw(k+1);
end loop;
mvp_n := mvp_n - 1;
ndis := '1'; ncode := D_UNPAIRED;
elsif mdt_n /= 0 then
for k in 0 to QD - 2 loop
mdt_pid(k) := mdt_pid(k+1); mdt_vw(k) := mdt_vw(k+1);
end loop;
mdt_n := mdt_n - 1;
ndis := '1'; ncode := D_UNPAIRED;
end if;
else
if mvp_n /= 0 and mdt_n /= 0 then
if mvp_pid(0) /= mdt_pid(0) then
dif := '1'; ncode := D_PID;
elsif mvp_vw(0) /= mdt_vw(0) then
dif := '1'; ncode := D_VIEW;
else
nagr := '1';
end if;
if dif = '1' then
if st = '1' then ndis := '1'; else nmsk := '1'; end if;
end if;
for k in 0 to QD - 2 loop
mvp_pid(k) := mvp_pid(k+1); mvp_vw(k) := mvp_vw(k+1);
mdt_pid(k) := mdt_pid(k+1); mdt_vw(k) := mdt_vw(k+1);
end loop;
mvp_n := mvp_n - 1;
mdt_n := mdt_n - 1;
end if;
if vv = '1' and mvp_n < to_unsigned(QD, QW+1) then
mvp_pid(to_integer(mvp_n)) := vp;
mvp_vw(to_integer(mvp_n)) := vw;
mvp_n := mvp_n + 1;
end if;
if dv = '1' and mdt_n < to_unsigned(QD, QW+1) then
mdt_pid(to_integer(mdt_n)) := dp;
mdt_vw(to_integer(mdt_n)) := dw;
mdt_n := mdt_n + 1;
end if;
if mvp_n > to_unsigned(SKEW_MAX, QW+1)
or mdt_n > to_unsigned(SKEW_MAX, QW+1) then
mvp_n := (others => '0');
mdt_n := (others => '0');
ndis := '1'; nmsk := '0'; ncode := D_SKEW; nagr := '0';
end if;
end if;
m_code := ncode; m_agr := nagr; m_dis := ndis; m_msk := nmsk;
if nagr = '1' then m_ag := m_ag + 1; end if;
if nmsk = '1' then m_mk := m_mk + 1; end if;
if ndis = '1' or nmsk = '1' then
case ncode is
when D_PID => m_pd := m_pd + 1;
when D_VIEW => m_vd := m_vd + 1;
when D_SKEW => m_sk := m_sk + 1;
when D_UNPAIRED => m_up := m_up + 1;
when others => null;
end case;
end if;
wait until rising_edge(clk);
wait for 1 ns;
vip_valid <= '0'; dut_valid <= '0'; eot <= '0';
end procedure;
constant Z4 : std_logic_vector(3 downto 0) := (others => '0');
constant Z2 : std_logic_vector(1 downto 0) := (others => '0');
procedure nop (n : integer) is
begin
for j in 1 to n loop
step('0',Z4,Z2, '0',Z4,Z2, strict, '0');
end loop;
end procedure;
-- Feed both sides one event each and let the comparison happen.
procedure pair (vp : std_logic_vector(3 downto 0);
vw : std_logic_vector(1 downto 0);
dp : std_logic_vector(3 downto 0);
dw : std_logic_vector(1 downto 0);
st : std_logic) is
begin
step('1',vp,vw, '1',dp,dw, st, '0');
step('0',Z4,Z2, '0',Z4,Z2, st, '0');
end procedure;
-- Drain the alignment queues the way the design provides for.
procedure drain is
begin
for j in 1 to 2*QD + 2 loop
step('0',Z4,Z2, '0',Z4,Z2, strict, '1');
end loop;
chk(unsigned(vip_depth) = 0 and unsigned(dut_depth) = 0,
"the drain did not empty the alignment queues");
nop(1);
end procedure;
function p4 (n : integer) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(n, 4));
end function;
function p2 (n : integer) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(n, 2));
end function;
variable b_ag, b_pd, b_vd, b_mk, b_up, b_sk : integer := 0;
variable vv_v, dv_v, st_v, eo_v : std_logic;
variable ln : line;
begin
wait for 33 ns;
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
-- ---- Phase A: the state after reset ----
chk(unsigned(vip_depth) = 0 and unsigned(dut_depth) = 0,
"reset left events queued");
chk(agree_pulse = '0' and dis_pulse = '0',
"reset asserted a comparison result");
-- ---- Phase B: ALL 256 PID PAIRS. Agreement on exactly the diagonal. --
for a in 0 to 15 loop
for b in 0 to 15 loop
b_ag := to_integer(unsigned(n_agree));
b_pd := to_integer(unsigned(n_pid_dis));
pair(p4(a), "01", p4(b), "01", '1');
if a = b then
chk(to_integer(unsigned(n_agree)) = b_ag + 1,
"the two sides reported the same PID and the adapter did not agree");
chk(to_integer(unsigned(n_pid_dis)) = b_pd,
"identical PIDs were reported as a disagreement");
else
chk(to_integer(unsigned(n_pid_dis)) = b_pd + 1,
"the two sides decoded DIFFERENT PIDs and the adapter did not say so -- an adapter that resolves a disagreement has deleted the only finding a second implementation can produce");
chk(to_integer(unsigned(n_agree)) = b_ag,
"a PID disagreement was counted as agreement");
end if;
end loop;
end loop;
-- ---- Phase C: ALL 16 VIEW PAIRS, with the PIDs agreeing. ----
for a in 0 to 3 loop
for b in 0 to 3 loop
b_ag := to_integer(unsigned(n_agree));
b_vd := to_integer(unsigned(n_view_dis));
pair(x"C", p2(a), x"C", p2(b), '1');
if a = b then
chk(to_integer(unsigned(n_agree)) = b_ag + 1,
"identical views were not agreed");
chk(to_integer(unsigned(n_view_dis)) = b_vd,
"identical views were reported as a disagreement");
else
chk(to_integer(unsigned(n_view_dis)) = b_vd + 1,
"both sides saw the same packet and read it differently, and the adapter said nothing");
chk(to_integer(unsigned(n_agree)) = b_ag,
"a view disagreement was counted as agreement");
end if;
end loop;
end loop;
-- ---- Phase D: PERMISSIVE STILL COUNTS. ----
for k in 0 to 29 loop
b_pd := to_integer(unsigned(n_pid_dis));
b_mk := to_integer(unsigned(n_masked));
pair(x"1", "00", x"2", "00", '1');
chk(to_integer(unsigned(n_pid_dis)) = b_pd + 1,
"a strict disagreement was not counted");
chk(to_integer(unsigned(n_masked)) = b_mk,
"a strict disagreement was masked");
b_pd := to_integer(unsigned(n_pid_dis));
b_mk := to_integer(unsigned(n_masked));
pair(x"1", "00", x"2", "00", '0');
chk(to_integer(unsigned(n_masked)) = b_mk + 1,
"a permissive run came back clean -- masking without counting is the shim that makes a disagreement invisible, and every future disagreement of the same shape with it");
chk(to_integer(unsigned(n_pid_dis)) = b_pd + 1,
"the permissive switch decided whether the disagreement HAPPENED, rather than whether the run fails");
end loop;
-- ---- Phase E: THE ALIGNMENT BOUND, both edges. ----
drain;
b_sk := to_integer(unsigned(n_skew));
for k in 1 to SKEW_MAX loop
step('1', x"5", "00", '0', Z4, Z2, '1', '0');
end loop;
chk(unsigned(skew) = to_unsigned(SKEW_MAX, QW+1),
"the queue did not reach the alignment bound");
chk(to_integer(unsigned(n_skew)) = b_sk,
"alignment was declared lost AT the bound rather than past it");
step('1', x"5", "00", '0', Z4, Z2, '1', '0');
nop(1);
chk(to_integer(unsigned(n_skew)) = b_sk + 1,
"one side ran more than SKEW_MAX events ahead and the adapter carried on comparing unrelated events");
chk(unsigned(skew) = 0,
"the queues were not flushed after alignment was lost, so the loss will be reported again on every subsequent event");
-- ---- Phase F: UNPAIRED at end of test. ----
drain;
for k in 1 to SKEW_MAX loop
b_up := to_integer(unsigned(n_unpaired));
for a in 1 to k loop
step('1', x"7", "00", '0', Z4, Z2, '1', '0');
end loop;
drain;
chk(to_integer(unsigned(n_unpaired)) = b_up + k,
"an event that only one side ever reported was not named at end of test -- that is exactly the class of finding a second implementation exists to produce");
end loop;
-- ---- Phase G: EXHAUSTIVE. Every skew x every input combination. ----
for sk2 in 0 to SKEW_MAX loop
for cb in 0 to 15 loop
drain;
for a in 1 to sk2 loop
step('1', x"9", "00", '0', Z4, Z2, '1', '0');
end loop;
chk(unsigned(skew) = to_unsigned(sk2, QW+1),
"the sweep could not reach the skew it meant to reach");
if (cb / 8) mod 2 = 1 then vv_v := '1'; else vv_v := '0'; end if;
if (cb / 4) mod 2 = 1 then dv_v := '1'; else dv_v := '0'; end if;
if (cb / 2) mod 2 = 1 then st_v := '1'; else st_v := '0'; end if;
if cb mod 2 = 1 then eo_v := '1'; else eo_v := '0'; end if;
step(vv_v, x"9", "00", dv_v, x"9", "00", st_v, eo_v);
step(vv_v, x"9", "00", dv_v, x"9", "00", st_v, eo_v);
end loop;
end loop;
-- ---- Phase H: random ----
for k in 0 to 31999 loop
vv_v := rnd_lt(42, 100);
dv_v := rnd_lt(42, 100);
st_v := rnd_lt(70, 100);
eo_v := rnd_lt(8, 1000);
step(vv_v, rnd_slv(4), rnd_slv(2), dv_v, rnd_slv(4), rnd_slv(2), st_v, eo_v);
end loop;
-- ---- Phase I: clean agreement afterwards. ----
drain;
b_ag := to_integer(unsigned(n_agree));
for k in 1 to 200 loop
pair(x"E", "10", x"E", "10", '1');
end loop;
chk(to_integer(unsigned(n_agree)) = b_ag + 200,
"the adapter stopped agreeing on identical views after the random phase");
-- ---- Final agreement ----
chk(to_integer(unsigned(n_agree)) = m_ag, "n_agree disagrees with the model");
chk(to_integer(unsigned(n_pid_dis)) = m_pd, "n_pid_dis disagrees");
chk(to_integer(unsigned(n_view_dis)) = m_vd, "n_view_dis disagrees");
chk(to_integer(unsigned(n_skew)) = m_sk, "n_skew disagrees");
chk(to_integer(unsigned(n_masked)) = m_mk, "n_masked disagrees");
chk(to_integer(unsigned(n_unpaired)) = m_up, "n_unpaired disagrees");
chk(n_seen = 80, "not every alignment skew was crossed with every input combination");
chk(to_integer(unsigned(n_agree)) > 0, "the two sides never agreed about anything");
chk(to_integer(unsigned(n_pid_dis)) > 0, "a PID disagreement was never seen");
chk(to_integer(unsigned(n_view_dis)) > 0, "a view disagreement was never seen");
chk(to_integer(unsigned(n_skew)) > 0, "alignment was never lost");
chk(to_integer(unsigned(n_masked)) > 0, "permissive mode was never exercised");
chk(to_integer(unsigned(n_unpaired)) > 0, "an unpaired event was never seen");
write(ln, string'("REACH skew-x-input=") & integer'image(n_seen) &
"/80 steps=" & integer'image(n_steps));
writeline(output, ln);
write(ln, string'("COUNTERS agree=") & integer'image(to_integer(unsigned(n_agree))) &
" pid-dis=" & integer'image(to_integer(unsigned(n_pid_dis))) &
" view-dis=" & integer'image(to_integer(unsigned(n_view_dis))) &
" skew=" & integer'image(to_integer(unsigned(n_skew))) &
" masked=" & integer'image(to_integer(unsigned(n_masked))) &
" unpaired=" & integer'image(to_integer(unsigned(n_unpaired))));
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 |
|---|---|---|---|
| skew x input | 80 / 80 | 80 / 80 | 80 / 80 |
| PID pairs compared | 256 / 256 | 256 / 256 | 256 / 256 |
| view pairs compared | 16 / 16 | 16 / 16 | 16 / 16 |
| Steps | 35053 | 35053 | 35053 |
| Checks executed | 351385 | 351385 | 351385 |
| agreements | 397 | 433 | 583 |
| PID disagreements | 10924 | 11017 | 11025 |
| view disagreements | 533 | 536 | 369 |
| alignment losses | 694 | 711 | 657 |
| masked (permissive) | 3364 | 3380 | 3331 |
| unpaired at drain | 417 | 412 | 411 |
| Result | PASS | PASS | PASS |
The 256-pair sweep is the row worth dwelling on. Agreement on exactly the diagonal is a much stronger claim than "the injected disagreement was caught", because it also rules out an adapter that agrees too readily — which is the mutation this chapter is about.
12. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| X6 | the compared pair is not removed from the queues | 111865 | 112406 | 113217 |
| X4 | the alignment bound is removed | 88128 | 94172 | 93650 |
| X3 | a PID disagreement is reported as agreement | 17281 | 17518 | 17166 |
| X2 | permissive masks WITHOUT counting | 3428 | 3444 | 3395 |
| X1 | only PIDs are compared — "read differently" is invisible | 1627 | 1636 | 1135 |
| X7 | end of test discards what only one side saw | 840 | 830 | 828 |
| X5 | alignment is lost and flushed silently | 697 | 714 | 660 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct.
X3 is the shim. It is the mutation that implements "our device is allowed to do that" in code, and it scores 17 000 — not because a single disagreement is catastrophic, but because the 256-pair sweep compares every combination and 240 of them should disagree.
X2 is the one this chapter is named for. Permissive mode masks the failure and forgets to count it, so a permissive run comes back clean. It scores only 3400 — an order of magnitude below X3 — and that ratio is the point: the mutation that makes a run silently clean is far harder to detect than the one that makes it wrong, because the only evidence is a number that did not change.
X1 is the smallest of the three headline mutations at ~1600, and it is the most insidious. It compares PIDs and ignores meaning, so both sides agree on what arrived and disagree on what it means — and the adapter says nothing. That is precisely the disagreement worth owning a VIP for.
13. Debugging Walkthrough: The Filter That Hid Its Own Reason
The report. A USB device passes a commercial VIP's compliance suite. It fails interoperability testing against a particular host chipset, on enumeration, reproducibly.
Step 1 — what does the VIP say? Nothing. Clean run, 100% of its compliance checks.
Step 2 — what does the host do differently? It issues a GET_DESCRIPTOR with a wLength larger than the descriptor. The device returns the descriptor and then a zero-length packet. The host times out.
Step 3 — is that legal? Yes: a short transfer terminates when the data is shorter than requested, and the zero-length packet is only required when the transfer is an exact multiple of the maximum packet size. The device is sending one when it must not, and the host is waiting for the status stage.
Step 4 — so why did the VIP not flag it? Grep the adapter. There is a filter:
// added 2023-04-11, VIP flags spurious ZLP, our device is fine
if (vip_err == VIP_UNEXPECTED_ZLP) return; // <-- three years oldStep 5 — was the device fine? No. The comment records somebody's conclusion and none of their reasoning, and the person who wrote it left. The VIP had been reporting the actual interoperability bug on every single run for three years, and the adapter had been deleting it.
Step 6 — what the permissive counter would have done. Nothing, on its own — the filter would still have masked it. But n_masked would have read a few thousand rather than zero, on every run, and "we mask three thousand VIP disagreements per regression" is a sentence somebody eventually asks about. Zero is a sentence nobody asks about.
14. UVM: Wrapping a Foreign VIP
// A VIP arrives as a UVM agent with its own sequence item, its own monitor,
// and its own idea of what an "event" is. The wrapper's job is to bring its
// stream into your environment WITHOUT deciding anything.
//
// Note what this class does not contain: no filtering, no severity
// downgrades, no `if (err == X) return`. Every transformation in it is a
// FORMAT change, and format changes are the only kind that are safe to make
// silently.
class usb_vip_wrapper extends uvm_component;
`uvm_component_utils(usb_vip_wrapper)
// The foreign agent's analysis port comes in here...
uvm_analysis_imp #(vendor_usb_item, usb_vip_wrapper) vip_ap;
// ...and a normalised view goes out here.
uvm_analysis_port #(usb_view_item) out_ap;
// Counted, not filtered: every item the wrapper could not map onto our
// own view. A VIP that reports something we have no representation for is
// itself a finding -- it usually means a protocol feature nobody on this
// side has modelled.
int unsigned n_unmapped;
int unsigned n_unmapped_kind[string];
function new(string name, uvm_component parent);
super.new(name, parent);
vip_ap = new("vip_ap", this);
out_ap = new("out_ap", this);
endfunction
function void write(vendor_usb_item t);
usb_view_item v = usb_view_item::type_id::create("v");
// ---- FORMAT normalisation only. ----
v.pid = t.packet_id[3:0];
v.view = map_view(t.transaction_kind);
if (v.view == VIEW_UNKNOWN) begin
// NOT dropped. A VIP event we cannot represent is counted by kind,
// so "the VIP keeps telling us about something we do not model" is
// visible as a number rather than as silence.
n_unmapped++;
n_unmapped_kind[t.transaction_kind.name()]++;
end
out_ap.write(v);
endfunction
function usb_view_e map_view(vendor_kind_e k);
case (k)
VENDOR_TOKEN_OUT, VENDOR_TOKEN_IN,
VENDOR_TOKEN_SETUP, VENDOR_TOKEN_SOF : return VIEW_TOKEN;
VENDOR_DATA0, VENDOR_DATA1 : return VIEW_DATA;
VENDOR_ACK, VENDOR_NAK, VENDOR_STALL : return VIEW_HANDSHAKE;
default : return VIEW_UNKNOWN;
endcase
endfunction
function void report_phase(uvm_phase phase);
if (n_unmapped > 0)
`uvm_warning("VIP_UNMAPPED",
$sformatf("%0d VIP events had no representation in our view model: %p -- this is usually a protocol feature nobody on this side has modelled, not a VIP defect",
n_unmapped, n_unmapped_kind))
endfunction
endclass
// ---- And the comparator, which is the hardware block above. ----
class usb_vip_comparator extends uvm_scoreboard;
`uvm_component_utils(usb_vip_comparator)
uvm_analysis_imp_vip #(usb_view_item, usb_vip_comparator) vip_ap;
uvm_analysis_imp_dut #(usb_view_item, usb_vip_comparator) dut_ap;
usb_view_item vip_q[$];
usb_view_item dut_q[$];
// Configurable, and deliberately NOT settable to zero or to something
// enormous: a bound that can be disabled by configuration is not a bound.
int unsigned skew_max = 4;
// strict is a CONFIGURATION, and the default is strict. A permissive
// default is how an environment acquires a hundred masked disagreements
// that nobody ever decided to mask.
bit strict = 1;
int unsigned n_agree, n_masked;
int unsigned n_dis[string];
function new(string name, uvm_component parent);
super.new(name, parent);
vip_ap = new("vip_ap", this);
dut_ap = new("dut_ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db #(int unsigned)::get(this, "", "skew_max", skew_max));
void'(uvm_config_db #(bit)::get(this, "", "strict", strict));
if (skew_max < 1 || skew_max > 64)
`uvm_fatal("CFG",
$sformatf("skew_max=%0d -- an alignment bound that can be set to zero or to a very large number is not a bound",
skew_max))
endfunction
function void write_vip(usb_view_item t); vip_q.push_back(t); compare(); endfunction
function void write_dut(usb_view_item t); dut_q.push_back(t); compare(); endfunction
function void compare();
while (vip_q.size() > 0 && dut_q.size() > 0) begin
usb_view_item v = vip_q.pop_front();
usb_view_item d = dut_q.pop_front();
string cause = "";
if (v.pid != d.pid)
cause = "PID";
else if (v.view != d.view)
// The disagreement worth owning a VIP for: both sides saw the same
// packet and read it differently, which is always a specification
// reading rather than a decode bug.
cause = "VIEW";
if (cause == "") begin
n_agree++;
end else begin
// The CAUSE is counted either way. `strict` decides whether the run
// fails; it does not decide whether the disagreement happened.
n_dis[cause]++;
if (strict)
`uvm_error("VIP_DISAGREE",
$sformatf("%s: VIP read pid=0x%01h view=%s, design read pid=0x%01h view=%s -- one of these two readings of the specification is wrong and the adapter cannot tell you which",
cause, v.pid, v.view.name(), d.pid, d.view.name()))
else begin
n_masked++;
`uvm_info("VIP_MASKED",
$sformatf("%s disagreement masked by permissive mode", cause), UVM_MEDIUM)
end
end
end
// ---- THE ALIGNMENT BOUND. ----
if (vip_q.size() > skew_max || dut_q.size() > skew_max) begin
n_dis["SKEW"]++;
`uvm_error("VIP_SKEW",
$sformatf("one side is %0d events ahead of the other -- they are no longer skewed, they have lost alignment, and every comparison after this point would be between unrelated events",
(vip_q.size() > dut_q.size()) ? vip_q.size() : dut_q.size()))
vip_q.delete();
dut_q.delete();
end
endfunction
// ---- THE DRAIN. An event only one side ever reported. ----
function void check_phase(uvm_phase phase);
foreach (vip_q[i])
`uvm_error("VIP_UNPAIRED",
$sformatf("the VIP reported pid=0x%01h and the design never did", vip_q[i].pid))
foreach (dut_q[i])
`uvm_error("VIP_UNPAIRED",
$sformatf("the design reported pid=0x%01h and the VIP never did", dut_q[i].pid))
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("VIP", $sformatf("agreed=%0d masked=%0d disagreements=%p",
n_agree, n_masked, n_dis), UVM_LOW)
// ---- A permissive run does not come back clean. ----
if (n_masked > 0)
`uvm_warning("VIP_MASKED",
$sformatf("%0d disagreements were masked by permissive mode -- that is %0d specification arguments nobody has had yet, and the number is the only record that they exist",
n_masked, n_masked))
// ---- A comparator that compared nothing is not a passing comparator. --
if (n_agree == 0 && n_masked == 0)
`uvm_error("VIP",
"the two views were never compared at all -- either the VIP's port is not connected or it produced no events, and its silence means nothing")
endfunction
endclass15. Common Misconceptions
"The VIP is the golden reference." It is a second implementation. It can be wrong, and on the specification's newer corners it frequently is.
"The VIP's coverage is our coverage." It is the coverage of the bins its authors declared for the revision they targeted.
"The adapter should make the interfaces line up." Format, yes. Meaning, never.
"This one disagreement is a known false positive." Then it is a specification argument, and it has to be won in writing rather than filtered in a shim.
"A filter with an explanatory comment is documented." The comment records a conclusion and none of the reasoning, and it never expires.
"Permissive mode is for getting through the afternoon." It is — and it must cost a visible number, or the afternoon becomes three years.
"The skew window is too small; widen it." Past the bound the two sides are not skewed, they are unaligned, and widening converts one loud failure into a stream of nonsense.
"An event only one side saw is skew that had not settled." At end of test there is no more traffic. One implementation saw something the other never did.
"Strict or permissive decides whether it is a bug." It decides whether the run fails. The disagreement happened either way.
16. Exercises
1. X3 scores ~17 000 and X2 ~3 400, though X2 is the more dangerous bug. Explain the ratio from what each does to the evidence, and say which you would rather have in a regression you do not read every day.
2. X1's score is almost exactly three times the baseline view-disagreement count in all three languages. Derive the three, then predict X7's score from n_unpaired and check it.
3. The skew check cannot fire in the same cycle as a comparison. Prove it from the order of the three steps, then show which reordering breaks it.
4. Write the three artefacts §13 says would make a filter an owned decision, for the ZLP case specifically.
5. A VIP reports an event your view model has no representation for. Argue whether that should be an error, a warning or a counted statistic, and what changes your answer.
6. Add a third observer — a bus analyser trace replayed from silicon. What changes in the comparator, and what does a 2-against-1 disagreement now let you conclude that a 1-against-1 did not?
17. Summary
| Idea | Why it matters |
|---|---|
| A VIP is a second implementation | where it and the design differ, one is wrong |
| The adapter makes a disagreement visible | it does not resolve one |
| A filter is a specification argument | it has to be won, not implemented in a shim |
| Permissive mode must count | or a masked run comes back clean |
| Strict is the default | a permissive default is how masks accumulate unnoticed |
| PID vs VIEW disagreements go to different people | a decode bug and a reading of the spec |
| The alignment window is bounded | past it they are unaligned, not skewed |
| Widening the window is the wrong reflex | it buries one loud failure in quiet nonsense |
| The loss is reported once and flushed | or every later event repeats it |
| An unpaired event at end of test is a finding | one implementation saw what the other never did |
| A VIP's coverage is its model's coverage | its bins, its revision, its configurations |
| 80/80 skew x input, 256/256 PID pairs, 16/16 view pairs | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o va_v.out va_v.v va_v_tb.v && ./va_v.out |
| SystemVerilog | iverilog -g2012 -o va_sv.out va_sv.sv va_sv_tb.sv && ./va_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a va_vhdl.vhd va_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_va_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_va_vhdl |
| One mutation | iverilog -g2005 -DMUT_X3 -o mm va_v_mut.v va_v_tb.v && ./mm |
All three implementations pass with 0 errors: all 256 PID pairs compared with agreement on exactly the diagonal, all 16 view pairs compared, every alignment skew crossed with every input combination, and the same disagreement driven in both strict and permissive mode with the cause counted in both.
Chapter 24.6 — UVM Architecture for USB assembles all of it. A USB environment has two agents on one wire, and that is the structural problem the whole architecture is built around: host and device both drive the same differential pair, at different times, with a turnaround between them. An environment that does not model the turnaround creates contention the real bus never would — and then spends weeks debugging its own testbench.
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.
