USB · Module 24
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.
Module 23 built five blocks. This module builds the things that decide whether blocks like those are right — and the first of them is the one that sits on the bus and says "that is not legal."
1. What a Checker Is, in Two Sentences
It is a state machine that sees every packet, drives nothing, and reports violations.
1. NOTHING IT OBSERVES MAY CHANGE WHAT THE DESIGN DOES.
2. NOTHING THE DESIGN DOES MAY CHANGE WHAT IT BELIEVES.
Almost every bad checker breaks one of those two.The first is usually respected by accident — a checker has no outputs that go anywhere. The second is broken constantly, and the usual form is a checker that takes some piece of state from inside the DUT because it was convenient. A checker that reads the DUT's own idea of what it is doing cannot catch the DUT being wrong about it.
2. The Shape of a USB Transaction
TOKEN -> [DATA] -> [HANDSHAKE]
SOF token alone. Complete the moment it arrives.
OUT/SETUP token(host) -> DATA(host) -> handshake(device)
IN token(host) -> DATA(device) -> handshake(host)
...or token(host) -> NAK/STALL(device), which is a
COMPLETE, LEGAL transaction with no data in it at all.
PING token(host) -> handshake(device)Two of those lines are where checkers go wrong.
A SOF is a whole transaction. It has no data and no handshake and nothing follows it. A checker that treats every token as the opening of a three-part sequence reports a violation on every microframe — 8000 a second at high speed.
An IN answered with NAK is a complete, legal transaction. That is flow control working exactly as designed. Flag it and the checker reports thousands of violations on any busy bus.
3. Half the Rules Need to Know Who Drove It
A token always comes from the host. On an OUT the data comes from the host and the handshake from the device; on an IN it is the other way round.
A checker that looks only at PIDs and not at who drove them cannot tell a correct IN transaction from a device that has started talking out of turn — and a device talking out of turn is the single most destructive thing on a USB bus, because it collides with whatever the host was driving. Both sides see corruption; neither sees a cause.
4. The Timeout Is the Only Liveness Tool a Checker Has
Every rule so far is of the form "if X, then Y must follow." None of them says anything about what happens if nothing follows.
host sends IN ... and the device never answers.
Not one PID rule violated.
Not one direction rule violated.
The bus simply goes quiet.
A checker built only from "if X then Y" reports
nothing. For ever.And the counter must measure dead air, not elapsed time. A transaction interleaved with other traffic is slow, not dead; timing it out because the bus was busy is a violation the checker invented.
5. Two Things This Block Got Wrong First
Both were found by the testbench, and neither is a PID rule.
5.1 The timer outlived the transaction it was timing
The counter was cleared when a token arrived and when a handshake was expected — but not when a violation sent the checker back to IDLE. So it sat in IDLE with a stale age, and the next transaction started with less patience than it was entitled to.
5.2 A violation cycle also reported a completed transaction
A SOF arriving while another transaction was still open produced both violation (the abandoned transaction) and xact_done (the SOF, which is complete in itself). Both statements are true, and reporting both leaves a consumer unable to say what happened.
6. The Checker
usb_protocol_checker — five states, and every exit leads to IDLE
usb_protocol_checker #(TO_MAX = 12)
inputs outputs
------ -------
pkt_valid / pkt_pid state 5 states
from_host WHO drove it violation ONE pulse each
ep_iso vio_code 7 named causes
bus_idle xact_done ONE pulse each
open_token / wait_age
n_xacts n_violations
n_orphan_data n_orphan_hs n_direction
n_setup_data n_timeout n_overlap n_iso_hsSeven named causes, counted separately and summed — the per-cause counters are incremented from the same pulse as the total, so they add up by construction and the suite checks that they do.
7. Verilog-2005 Implementation
// usb_protocol_checker -- the block that watches the bus and says "that is
// not legal", and the two things that decide whether it is worth having.
//
// WHAT A CHECKER IS
//
// It is a state machine that sits on the bus, sees every packet, drives
// nothing, and reports violations. It is NOT part of the design: nothing it
// observes may change what the design does, and nothing the design does may
// change what it believes. Those two sentences are the whole discipline, and
// almost every bad checker breaks one of them.
//
// THE SHAPE OF A USB TRANSACTION
//
// TOKEN -> [DATA] -> [HANDSHAKE]
//
// SOF token alone. Complete when it arrives.
// OUT/SETUP token(host) -> DATA(host) -> handshake(device)
// IN token(host) -> DATA(device) -> handshake(host)
// ...or token(host) -> NAK/STALL(device), and that is
// a COMPLETE, LEGAL transaction with no data in it.
// PING token(host) -> handshake(device)
//
// ISOCHRONOUS transactions have NO handshake at all. There is nothing to
// retry with, so there is nothing to acknowledge, and a checker that demands
// a handshake will flag every isochronous transfer on the bus.
//
// THE FIRST THING THAT DECIDES WHETHER IT IS WORTH HAVING: DIRECTION
//
// A token always comes from the host. On an OUT the data comes from the host
// and the handshake from the device; on an IN it is the other way round. A
// checker that only looks at PIDs and not at WHO DROVE THEM cannot tell a
// correct IN transaction from a device that has started talking out of turn
// -- which is the single most destructive thing a USB device can do, because
// it collides with whatever the host was driving.
//
// THE SECOND: A TIMEOUT
//
// Every rule above is a rule about what happens NEXT. None of them says
// anything about what happens if nothing happens at all.
//
// host sends IN ... and the device never answers.
//
// Not one PID rule is violated. Not one direction rule. The bus simply goes
// quiet, and a checker built only from "if X then Y must follow" reports
// nothing, for ever.
//
// A CHECKER WITHOUT A TIMEOUT CANNOT SEE A TRANSACTION
// THAT NEVER ENDS -- WHICH IS THE MOST COMMON WAY REAL
// HARDWARE FAILS.
//
// The timeout is the checker's only liveness tool, and it is the reason this
// block has a counter in it at all.
//
// AND IT MUST RESYNCHRONISE
//
// After a violation the checker does not know where it is in the transaction
// any more -- that is what a violation means. So it returns to IDLE and
// waits for the next token, exactly as the packet recogniser of chapter 23.4
// does, and for exactly the same reason: there is no way to recover a
// position in a stream except by waiting for a known starting point.
module usb_protocol_checker #(
parameter integer TO_MAX = 12 // cycles a response may take before the
// transaction is declared dead
) (
input wire clk,
input wire rst_n,
input wire pkt_valid, // a packet completed on the bus this cycle
input wire [3:0] pkt_pid,
input wire from_host, // WHO drove it. Half the rules need this.
input wire ep_iso, // this endpoint is isochronous
input wire bus_idle, // the bus is idle this cycle
output wire [2:0] state,
output wire violation, // ONE pulse per violation
output wire [3:0] vio_code,
output wire xact_done, // ONE pulse per completed transaction
output wire [3:0] open_token, // the token this transaction started with
output wire [4:0] wait_age,
output reg [31:0] n_xacts,
output reg [31:0] n_violations,
output reg [31:0] n_orphan_data,
output reg [31:0] n_orphan_hs,
output reg [31:0] n_direction,
output reg [31:0] n_setup_data,
output reg [31:0] n_timeout,
output reg [31:0] n_overlap,
output reg [31:0] n_iso_hs
);
// ---- The checker's own states. Five, and one of them is the timeout. ----
localparam [2:0] C_IDLE = 3'd0, // between transactions; expect a token
C_OUT = 3'd1, // OUT/SETUP seen; expect host DATA
C_IN = 3'd2, // IN seen; expect device DATA or handshake
C_HS = 3'd3, // DATA seen; expect a handshake
C_PING = 3'd4; // PING seen; expect a device handshake
// ---- The violations, named. Every one is a different question. ----
localparam [3:0] V_NONE = 4'd0,
V_ORPHAN_DATA= 4'd1, // DATA with no token in front of it
V_ORPHAN_HS = 4'd2, // a handshake with no transaction
V_DIRECTION = 4'd3, // the wrong side drove it
V_SETUP_DATA = 4'd4, // SETUP must be followed by DATA0
V_TIMEOUT = 4'd5, // nobody answered
V_OVERLAP = 4'd6, // a token while one was still open
V_ISO_HS = 4'd7; // a handshake in an iso transaction
// ---- PID classification, from the encoding (chapter 23.4). ----
wire is_token = (pkt_pid[1:0] == 2'b01);
wire is_data = (pkt_pid[1:0] == 2'b11);
wire is_hs = (pkt_pid[1:0] == 2'b10);
wire is_ping = (pkt_pid == 4'b0100);
wire is_sof = (pkt_pid == 4'b0101);
wire is_setup = (pkt_pid == 4'b1101);
wire is_in = (pkt_pid == 4'b1001);
wire is_out = (pkt_pid == 4'b0001);
wire is_data0 = (pkt_pid == 4'b0011);
// A NAK, STALL or NYET from the device ENDS the transaction. An ACK ends
// it too. There is no such thing as a handshake that continues one, which
// is why C_HS has no successor state.
reg [2:0] st_r;
reg [3:0] tok_r;
reg [4:0] age_r;
reg [3:0] vio_r;
reg vio_pulse_r, done_r;
assign state = st_r;
assign open_token = tok_r;
assign wait_age = age_r;
assign vio_code = vio_r;
assign violation = vio_pulse_r;
assign xact_done = done_r;
reg [2:0] st_n;
reg [3:0] tok_n, vio_n;
reg [4:0] age_n;
reg vp_n, dn_n;
always @* begin
st_n = st_r;
tok_n = tok_r;
vio_n = vio_r;
age_n = age_r;
vp_n = 1'b0;
dn_n = 1'b0;
if (pkt_valid) begin
// ---- Rule 0: a token ALWAYS comes from the host. Checked before
// ---- anything else, because a device driving a token means the bus
// ---- has two masters and nothing after this point is meaningful.
if (is_token && !from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if (is_token) begin
// A token while a transaction is still open. The previous one will
// never complete, and saying so is the point -- silently starting
// the new one hides a lost response for ever.
if (st_r != C_IDLE) begin
vio_n = V_OVERLAP; vp_n = 1'b1;
end
tok_n = pkt_pid;
age_n = 5'd0;
if (is_sof) begin
// A SOF is a complete transaction all by itself.
st_n = C_IDLE;
dn_n = 1'b1;
end else if (is_in) begin
st_n = C_IN;
end else begin
st_n = C_OUT; // OUT and SETUP both expect host data next
end
end else if (is_ping) begin
if (!from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else begin
if (st_r != C_IDLE) begin vio_n = V_OVERLAP; vp_n = 1'b1; end
tok_n = pkt_pid;
age_n = 5'd0;
st_n = C_PING;
end
end else if (is_data) begin
case (st_r)
C_OUT: begin
// The host sends the data on an OUT or a SETUP.
if (!from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if ((tok_r == 4'b1101) && !is_data0) begin
// ---- A SETUP is ALWAYS DATA0. The control transfer's whole
// ---- toggle sequence is defined from that starting point,
// ---- so a SETUP carrying DATA1 desynchronises every stage
// ---- that follows it (chapter 21.1).
st_n = C_IDLE; vio_n = V_SETUP_DATA; vp_n = 1'b1;
end else if (ep_iso) begin
// Isochronous: no handshake follows, so the transaction is
// complete the moment the data has been sent.
st_n = C_IDLE; dn_n = 1'b1;
end else begin
st_n = C_HS; age_n = 5'd0;
end
end
C_IN: begin
// The DEVICE sends the data on an IN.
if (from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if (ep_iso) begin
st_n = C_IDLE; dn_n = 1'b1;
end else begin
st_n = C_HS; age_n = 5'd0;
end
end
default: begin
// ---- Data with nothing in front of it. ----
st_n = C_IDLE; vio_n = V_ORPHAN_DATA; vp_n = 1'b1;
end
endcase
end else if (is_hs) begin
case (st_r)
C_IN: begin
// NAK / STALL / NYET from the device instead of data. This is a
// COMPLETE and perfectly legal transaction, and a checker that
// treats it as an error flags every flow-controlled transfer on
// a busy bus.
if (from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if (ep_iso) begin
st_n = C_IDLE; vio_n = V_ISO_HS; vp_n = 1'b1;
end else begin
st_n = C_IDLE; dn_n = 1'b1;
end
end
C_PING: begin
if (from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else begin
st_n = C_IDLE; dn_n = 1'b1;
end
end
C_HS: begin
// The acknowledging side is whoever did NOT send the data: the
// device on an OUT, the host on an IN.
if (from_host != (tok_r == 4'b1001)) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if (ep_iso) begin
st_n = C_IDLE; vio_n = V_ISO_HS; vp_n = 1'b1;
end else begin
st_n = C_IDLE; dn_n = 1'b1;
end
end
C_OUT: begin
// A handshake where the host's data was expected. On a
// non-isochronous OUT this is the device refusing early, which
// the protocol does not provide for.
st_n = C_IDLE; vio_n = V_ORPHAN_HS; vp_n = 1'b1;
end
default: begin
st_n = C_IDLE; vio_n = V_ORPHAN_HS; vp_n = 1'b1;
end
endcase
end
// Any other PID -- SPLIT, PRE/ERR, reserved -- is outside this
// checker's remit and is deliberately ignored rather than flagged.
// A checker that reports things it does not model is a checker
// people switch off.
end else if (st_r != C_IDLE) begin
// ---- THE LIVENESS RULE. Nothing arrived. ----
//
// Every other rule in this block is of the form "if X then Y must
// follow". None of them fires when NOTHING follows, which is how a
// device that has simply stopped answering passes a checker that is
// otherwise complete.
if (age_r >= TO_MAX[4:0] - 5'd1) begin
st_n = C_IDLE; vio_n = V_TIMEOUT; vp_n = 1'b1; age_n = 5'd0;
end else if (bus_idle) begin
age_n = age_r + 5'd1;
end
end
// ---- The timer belongs to an OPEN transaction, and to nothing else.
//
// Returning to IDLE -- for ANY reason, a completion or a violation --
// ends the transaction, so the counter goes with it. Left running, a
// stale age shortens the NEXT transaction's patience by however long
// the previous one happened to wait, and the checker then times out a
// perfectly healthy transfer. That is a false positive generated by
// the checker's own bookkeeping, which is the worst kind: it is not
// wrong about the bus, it is wrong about itself.
if (st_n == C_IDLE) age_n = 5'd0;
// ---- A cycle that produced a violation never also reports a
// ---- completed transaction.
//
// The case that forces this is a SOF arriving while another transaction
// is still open: the SOF is a complete transaction all by itself, AND it
// has just abandoned the one in progress. Both statements are true, and
// reporting both leaves a consumer unable to say what happened.
//
// It matters more than it looks, because n_xacts is a DENOMINATOR --
// violations per transaction is the number anybody actually quotes.
// Counting the interrupting packet as healthy traffic makes the error
// rate look better exactly when the bus is going wrong.
if (vp_n) dn_n = 1'b0;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= C_IDLE;
tok_r <= 4'd0;
age_r <= 5'd0;
vio_r <= V_NONE;
vio_pulse_r <= 1'b0;
done_r <= 1'b0;
n_xacts <= 32'd0;
n_violations <= 32'd0;
n_orphan_data <= 32'd0;
n_orphan_hs <= 32'd0;
n_direction <= 32'd0;
n_setup_data <= 32'd0;
n_timeout <= 32'd0;
n_overlap <= 32'd0;
n_iso_hs <= 32'd0;
end else begin
st_r <= st_n;
tok_r <= tok_n;
age_r <= age_n;
vio_r <= vio_n;
vio_pulse_r <= vp_n;
done_r <= dn_n;
if (dn_n) n_xacts <= n_xacts + 32'd1;
// The per-cause counters are driven by the SAME pulse as the total,
// so they sum to it by construction (chapter 23.4).
if (vp_n) begin
n_violations <= n_violations + 32'd1;
case (vio_n)
V_ORPHAN_DATA: n_orphan_data <= n_orphan_data + 32'd1;
V_ORPHAN_HS: n_orphan_hs <= n_orphan_hs + 32'd1;
V_DIRECTION: n_direction <= n_direction + 32'd1;
V_SETUP_DATA: n_setup_data <= n_setup_data + 32'd1;
V_TIMEOUT: n_timeout <= n_timeout + 32'd1;
V_OVERLAP: n_overlap <= n_overlap + 32'd1;
V_ISO_HS: n_iso_hs <= n_iso_hs + 32'd1;
default: ;
endcase
end
end
end
endmodule8. SystemVerilog Implementation
// usb_protocol_checker -- the block that watches the bus and says "that is
// not legal", and the two things that decide whether it is worth having.
//
// WHAT A CHECKER IS
//
// It is a state machine that sits on the bus, sees every packet, drives
// nothing, and reports violations. It is NOT part of the design: nothing it
// observes may change what the design does, and nothing the design does may
// change what it believes. Those two sentences are the whole discipline, and
// almost every bad checker breaks one of them.
//
// THE SHAPE OF A USB TRANSACTION
//
// TOKEN -> [DATA] -> [HANDSHAKE]
//
// SOF token alone. Complete when it arrives.
// OUT/SETUP token(host) -> DATA(host) -> handshake(device)
// IN token(host) -> DATA(device) -> handshake(host)
// ...or token(host) -> NAK/STALL(device), and that is
// a COMPLETE, LEGAL transaction with no data in it.
// PING token(host) -> handshake(device)
//
// ISOCHRONOUS transactions have NO handshake at all. There is nothing to
// retry with, so there is nothing to acknowledge, and a checker that demands
// a handshake will flag every isochronous transfer on the bus.
//
// THE FIRST THING THAT DECIDES WHETHER IT IS WORTH HAVING: DIRECTION
//
// A token always comes from the host. On an OUT the data comes from the host
// and the handshake from the device; on an IN it is the other way round. A
// checker that only looks at PIDs and not at WHO DROVE THEM cannot tell a
// correct IN transaction from a device that has started talking out of turn
// -- which is the single most destructive thing a USB device can do, because
// it collides with whatever the host was driving.
//
// THE SECOND: A TIMEOUT
//
// Every rule above is a rule about what happens NEXT. None of them says
// anything about what happens if nothing happens at all.
//
// host sends IN ... and the device never answers.
//
// Not one PID rule is violated. Not one direction rule. The bus simply goes
// quiet, and a checker built only from "if X then Y must follow" reports
// nothing, for ever.
//
// A CHECKER WITHOUT A TIMEOUT CANNOT SEE A TRANSACTION
// THAT NEVER ENDS -- WHICH IS THE MOST COMMON WAY REAL
// HARDWARE FAILS.
//
// The timeout is the checker's only liveness tool, and it is the reason this
// block has a counter in it at all.
//
// AND IT MUST RESYNCHRONISE
//
// After a violation the checker does not know where it is in the transaction
// any more -- that is what a violation means. So it returns to IDLE and
// waits for the next token, exactly as the packet recogniser of chapter 23.4
// does, and for exactly the same reason: there is no way to recover a
// position in a stream except by waiting for a known starting point.
package usb_chk_pkg;
// The checker's own states. Five, and the presence of C_PING as a state of
// its own is the point: a PING is a complete transaction with no data in
// it, and folding it into C_IN would make a legal PING look like an IN
// that never delivered anything.
typedef enum logic [2:0] {
C_IDLE = 3'd0, // between transactions; expect a token
C_OUT = 3'd1, // OUT/SETUP seen; expect host DATA
C_IN = 3'd2, // IN seen; expect device DATA or handshake
C_HS = 3'd3, // DATA seen; expect a handshake
C_PING = 3'd4 // PING seen; expect a device handshake
} chk_state_e;
// The violations, named. Every one is a different question, and they are
// counted separately AND summed, so a mis-classification shows up as a
// total that does not add up.
typedef enum logic [3:0] {
V_NONE = 4'd0,
V_ORPHAN_DATA = 4'd1, // DATA with no token in front of it
V_ORPHAN_HS = 4'd2, // a handshake with no transaction
V_DIRECTION = 4'd3, // the wrong side drove it
V_SETUP_DATA = 4'd4, // SETUP must be followed by DATA0
V_TIMEOUT = 4'd5, // nobody answered
V_OVERLAP = 4'd6, // a token while one was still open
V_ISO_HS = 4'd7 // a handshake in an isochronous transaction
} vio_e;
endpackage
module usb_protocol_checker
import usb_chk_pkg::*;
#(
parameter int TO_MAX = 12 // cycles a response may take before the
// transaction is declared dead
) (
input logic clk,
input logic rst_n,
input logic pkt_valid, // a packet completed on the bus this cycle
input logic [3:0] pkt_pid,
input logic from_host, // WHO drove it. Half the rules need this.
input logic ep_iso, // this endpoint is isochronous
input logic bus_idle, // the bus is idle this cycle
output chk_state_e state,
output logic violation, // ONE pulse per violation
output vio_e vio_code,
output logic xact_done, // ONE pulse per completed transaction
output logic [3:0] open_token, // the token this transaction started with
output logic [4:0] wait_age,
output logic [31:0] n_xacts,
output logic [31:0] n_violations,
output logic [31:0] n_orphan_data,
output logic [31:0] n_orphan_hs,
output logic [31:0] n_direction,
output logic [31:0] n_setup_data,
output logic [31:0] n_timeout,
output logic [31:0] n_overlap,
output logic [31:0] n_iso_hs
);
// ---- PID classification, from the encoding (chapter 23.4). ----
logic is_token, is_data, is_hs, is_ping;
logic is_sof, is_setup, is_in, is_out, is_data0;
// Continuous assignments, not initialisers. `logic x = expr;` in
// SystemVerilog sets a VARIABLE once at time zero -- it is not the
// continuous assignment that `wire x = expr;` is in Verilog, and the
// difference is silent: the block elaborates, simulates, and classifies
// every packet as whatever the first one happened to be.
assign is_token = (pkt_pid[1:0] == 2'b01);
assign is_data = (pkt_pid[1:0] == 2'b11);
assign is_hs = (pkt_pid[1:0] == 2'b10);
assign is_ping = (pkt_pid == 4'b0100);
assign is_sof = (pkt_pid == 4'b0101);
assign is_setup = (pkt_pid == 4'b1101);
assign is_in = (pkt_pid == 4'b1001);
assign is_out = (pkt_pid == 4'b0001);
assign is_data0 = (pkt_pid == 4'b0011);
// A NAK, STALL or NYET from the device ENDS the transaction. An ACK ends
// it too. There is no such thing as a handshake that continues one, which
// is why C_HS has no successor state.
chk_state_e st_r;
vio_e vio_r;
logic [3:0] tok_r;
logic [4:0] age_r;
logic vio_pulse_r, done_r;
assign state = st_r;
assign open_token = tok_r;
assign wait_age = age_r;
assign vio_code = vio_r;
assign violation = vio_pulse_r;
assign xact_done = done_r;
chk_state_e st_n;
vio_e vio_n;
logic [3:0] tok_n;
logic [4:0] age_n;
logic vp_n, dn_n;
always_comb begin
st_n = st_r;
tok_n = tok_r;
vio_n = vio_r;
age_n = age_r;
vp_n = 1'b0;
dn_n = 1'b0;
if (pkt_valid) begin
// ---- Rule 0: a token ALWAYS comes from the host. Checked before
// ---- anything else, because a device driving a token means the bus
// ---- has two masters and nothing after this point is meaningful.
if (is_token && !from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if (is_token) begin
// A token while a transaction is still open. The previous one will
// never complete, and saying so is the point -- silently starting
// the new one hides a lost response for ever.
if (st_r != C_IDLE) begin
vio_n = V_OVERLAP; vp_n = 1'b1;
end
tok_n = pkt_pid;
age_n = 5'd0;
if (is_sof) begin
// A SOF is a complete transaction all by itself.
st_n = C_IDLE;
dn_n = 1'b1;
end else if (is_in) begin
st_n = C_IN;
end else begin
st_n = C_OUT; // OUT and SETUP both expect host data next
end
end else if (is_ping) begin
if (!from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else begin
if (st_r != C_IDLE) begin vio_n = V_OVERLAP; vp_n = 1'b1; end
tok_n = pkt_pid;
age_n = 5'd0;
st_n = C_PING;
end
end else if (is_data) begin
case (st_r)
C_OUT: begin
// The host sends the data on an OUT or a SETUP.
if (!from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if ((tok_r == 4'b1101) && !is_data0) begin
// ---- A SETUP is ALWAYS DATA0. The control transfer's whole
// ---- toggle sequence is defined from that starting point,
// ---- so a SETUP carrying DATA1 desynchronises every stage
// ---- that follows it (chapter 21.1).
st_n = C_IDLE; vio_n = V_SETUP_DATA; vp_n = 1'b1;
end else if (ep_iso) begin
// Isochronous: no handshake follows, so the transaction is
// complete the moment the data has been sent.
st_n = C_IDLE; dn_n = 1'b1;
end else begin
st_n = C_HS; age_n = 5'd0;
end
end
C_IN: begin
// The DEVICE sends the data on an IN.
if (from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if (ep_iso) begin
st_n = C_IDLE; dn_n = 1'b1;
end else begin
st_n = C_HS; age_n = 5'd0;
end
end
default: begin
// ---- Data with nothing in front of it. ----
st_n = C_IDLE; vio_n = V_ORPHAN_DATA; vp_n = 1'b1;
end
endcase
end else if (is_hs) begin
case (st_r)
C_IN: begin
// NAK / STALL / NYET from the device instead of data. This is a
// COMPLETE and perfectly legal transaction, and a checker that
// treats it as an error flags every flow-controlled transfer on
// a busy bus.
if (from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if (ep_iso) begin
st_n = C_IDLE; vio_n = V_ISO_HS; vp_n = 1'b1;
end else begin
st_n = C_IDLE; dn_n = 1'b1;
end
end
C_PING: begin
if (from_host) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else begin
st_n = C_IDLE; dn_n = 1'b1;
end
end
C_HS: begin
// The acknowledging side is whoever did NOT send the data: the
// device on an OUT, the host on an IN.
if (from_host != (tok_r == 4'b1001)) begin
st_n = C_IDLE; vio_n = V_DIRECTION; vp_n = 1'b1;
end else if (ep_iso) begin
st_n = C_IDLE; vio_n = V_ISO_HS; vp_n = 1'b1;
end else begin
st_n = C_IDLE; dn_n = 1'b1;
end
end
C_OUT: begin
// A handshake where the host's data was expected. On a
// non-isochronous OUT this is the device refusing early, which
// the protocol does not provide for.
st_n = C_IDLE; vio_n = V_ORPHAN_HS; vp_n = 1'b1;
end
default: begin
st_n = C_IDLE; vio_n = V_ORPHAN_HS; vp_n = 1'b1;
end
endcase
end
// Any other PID -- SPLIT, PRE/ERR, reserved -- is outside this
// checker's remit and is deliberately ignored rather than flagged.
// A checker that reports things it does not model is a checker
// people switch off.
end else if (st_r != C_IDLE) begin
// ---- THE LIVENESS RULE. Nothing arrived. ----
//
// Every other rule in this block is of the form "if X then Y must
// follow". None of them fires when NOTHING follows, which is how a
// device that has simply stopped answering passes a checker that is
// otherwise complete.
if (age_r >= 5'(TO_MAX) - 5'd1) begin
st_n = C_IDLE; vio_n = V_TIMEOUT; vp_n = 1'b1; age_n = 5'd0;
end else if (bus_idle) begin
age_n = age_r + 5'd1;
end
end
// ---- The timer belongs to an OPEN transaction, and to nothing else.
//
// Returning to IDLE -- for ANY reason, a completion or a violation --
// ends the transaction, so the counter goes with it. Left running, a
// stale age shortens the NEXT transaction's patience by however long
// the previous one happened to wait, and the checker then times out a
// perfectly healthy transfer. That is a false positive generated by
// the checker's own bookkeeping, which is the worst kind: it is not
// wrong about the bus, it is wrong about itself.
if (st_n == C_IDLE) age_n = 5'd0;
// ---- A cycle that produced a violation never also reports a
// ---- completed transaction.
//
// The case that forces this is a SOF arriving while another transaction
// is still open: the SOF is a complete transaction all by itself, AND it
// has just abandoned the one in progress. Both statements are true, and
// reporting both leaves a consumer unable to say what happened.
//
// It matters more than it looks, because n_xacts is a DENOMINATOR --
// violations per transaction is the number anybody actually quotes.
// Counting the interrupting packet as healthy traffic makes the error
// rate look better exactly when the bus is going wrong.
if (vp_n) dn_n = 1'b0;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= C_IDLE;
tok_r <= 4'd0;
age_r <= 5'd0;
vio_r <= V_NONE;
vio_pulse_r <= 1'b0;
done_r <= 1'b0;
n_xacts <= 32'd0;
n_violations <= 32'd0;
n_orphan_data <= 32'd0;
n_orphan_hs <= 32'd0;
n_direction <= 32'd0;
n_setup_data <= 32'd0;
n_timeout <= 32'd0;
n_overlap <= 32'd0;
n_iso_hs <= 32'd0;
end else begin
st_r <= st_n;
tok_r <= tok_n;
age_r <= age_n;
vio_r <= vio_n;
vio_pulse_r <= vp_n;
done_r <= dn_n;
if (dn_n) n_xacts <= n_xacts + 32'd1;
// The per-cause counters are driven by the SAME pulse as the total,
// so they sum to it by construction (chapter 23.4).
if (vp_n) begin
n_violations <= n_violations + 32'd1;
case (vio_n)
V_ORPHAN_DATA: n_orphan_data <= n_orphan_data + 32'd1;
V_ORPHAN_HS: n_orphan_hs <= n_orphan_hs + 32'd1;
V_DIRECTION: n_direction <= n_direction + 32'd1;
V_SETUP_DATA: n_setup_data <= n_setup_data + 32'd1;
V_TIMEOUT: n_timeout <= n_timeout + 32'd1;
V_OVERLAP: n_overlap <= n_overlap + 32'd1;
V_ISO_HS: n_iso_hs <= n_iso_hs + 32'd1;
default: ;
endcase
end
end
end
endmodule9. VHDL-2008 Implementation
-- usb_protocol_checker -- the block that watches the bus and says "that is
-- not legal", and the two things that decide whether it is worth having.
--
-- WHAT A CHECKER IS
--
-- It is a state machine that sits on the bus, sees every packet, drives
-- nothing, and reports violations. It is NOT part of the design: nothing it
-- observes may change what the design does, and nothing the design does may
-- change what it believes. Those two sentences are the whole discipline, and
-- almost every bad checker breaks one of them.
--
-- THE SHAPE OF A USB TRANSACTION
--
-- TOKEN -> [DATA] -> [HANDSHAKE]
--
-- SOF token alone. Complete when it arrives.
-- OUT/SETUP token(host) -> DATA(host) -> handshake(device)
-- IN token(host) -> DATA(device) -> handshake(host)
-- ...or token(host) -> NAK/STALL(device), and that is
-- a COMPLETE, LEGAL transaction with no data in it.
-- PING token(host) -> handshake(device)
--
-- ISOCHRONOUS transactions have NO handshake at all. There is nothing to
-- retry with, so there is nothing to acknowledge, and a checker that demands
-- a handshake will flag every isochronous transfer on the bus.
--
-- THE FIRST THING THAT DECIDES WHETHER IT IS WORTH HAVING: DIRECTION
--
-- A token always comes from the host. On an OUT the data comes from the host
-- and the handshake from the device; on an IN it is the other way round. A
-- checker that only looks at PIDs and not at WHO DROVE THEM cannot tell a
-- correct IN transaction from a device that has started talking out of turn
-- -- which is the single most destructive thing a USB device can do, because
-- it collides with whatever the host was driving.
--
-- THE SECOND: A TIMEOUT
--
-- Every rule above is a rule about what happens NEXT. None of them says
-- anything about what happens if nothing happens at all.
--
-- host sends IN ... and the device never answers.
--
-- Not one PID rule is violated. Not one direction rule. The bus simply goes
-- quiet, and a checker built only from "if X then Y must follow" reports
-- nothing, for ever.
--
-- A CHECKER WITHOUT A TIMEOUT CANNOT SEE A TRANSACTION
-- THAT NEVER ENDS -- WHICH IS THE MOST COMMON WAY REAL
-- HARDWARE FAILS.
--
-- The timeout is the checker's only liveness tool, and it is the reason this
-- block has a counter in it at all.
--
-- AND IT MUST RESYNCHRONISE
--
-- After a violation the checker does not know where it is in the transaction
-- any more -- that is what a violation means. So it returns to IDLE and
-- waits for the next token, exactly as the packet recogniser of chapter 23.4
-- does, and for exactly the same reason: there is no way to recover a
-- position in a stream except by waiting for a known starting point.
library ieee;
use ieee.std_logic_1164.all;
package usb_chk_pkg is
-- The checker's own states. Five, and the presence of C_PING as a state of
-- its own is the point: a PING is a complete transaction with no data in
-- it, and folding it into C_IN would make a legal PING look like an IN
-- that never delivered anything.
type chk_state_t is (C_IDLE, C_OUT, C_IN, C_HS, C_PING);
-- The violations, named. Every one is a different question, and they are
-- counted separately AND summed, so a mis-classification shows up as a
-- total that does not add up.
type vio_t is (V_NONE, V_ORPHAN_DATA, V_ORPHAN_HS, V_DIRECTION,
V_SETUP_DATA, V_TIMEOUT, V_OVERLAP, V_ISO_HS);
function cs_code (s : chk_state_t) return std_logic_vector;
function vi_code (v : vio_t) return std_logic_vector;
end package usb_chk_pkg;
package body usb_chk_pkg is
-- The encodings are written out rather than derived from position, so they
-- are pinned to the same numbers the Verilog and SystemVerilog use and a
-- reordering of either type cannot silently change the interface.
function cs_code (s : chk_state_t) return std_logic_vector is
begin
case s is
when C_IDLE => return "000";
when C_OUT => return "001";
when C_IN => return "010";
when C_HS => return "011";
when C_PING => return "100";
end case;
end function;
function vi_code (v : vio_t) return std_logic_vector is
begin
case v is
when V_NONE => return "0000";
when V_ORPHAN_DATA => return "0001";
when V_ORPHAN_HS => return "0010";
when V_DIRECTION => return "0011";
when V_SETUP_DATA => return "0100";
when V_TIMEOUT => return "0101";
when V_OVERLAP => return "0110";
when V_ISO_HS => return "0111";
end case;
end function;
end package body usb_chk_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_chk_pkg.all;
entity usb_protocol_checker is
generic (
TO_MAX : integer := 12 -- cycles a response may take before the
-- transaction is declared dead
);
port (
clk : in std_logic;
rst_n : in std_logic;
pkt_valid : in std_logic; -- a packet completed
pkt_pid : in std_logic_vector(3 downto 0);
from_host : in std_logic; -- WHO drove it
ep_iso : in std_logic; -- isochronous endpoint
bus_idle : in std_logic;
state : out std_logic_vector(2 downto 0);
violation : out std_logic; -- ONE pulse per violation
vio_code : out std_logic_vector(3 downto 0);
xact_done : out std_logic; -- ONE pulse per xact
open_token : out std_logic_vector(3 downto 0);
wait_age : out std_logic_vector(4 downto 0);
n_xacts : out std_logic_vector(31 downto 0);
n_violations : out std_logic_vector(31 downto 0);
n_orphan_data : out std_logic_vector(31 downto 0);
n_orphan_hs : out std_logic_vector(31 downto 0);
n_direction : out std_logic_vector(31 downto 0);
n_setup_data : out std_logic_vector(31 downto 0);
n_timeout : out std_logic_vector(31 downto 0);
n_overlap : out std_logic_vector(31 downto 0);
n_iso_hs : out std_logic_vector(31 downto 0)
);
end entity usb_protocol_checker;
architecture rtl of usb_protocol_checker is
constant P_IN : std_logic_vector(3 downto 0) := "1001";
constant P_SETUP : std_logic_vector(3 downto 0) := "1101";
signal st_r : chk_state_t := C_IDLE;
signal vio_r : vio_t := V_NONE;
signal tok_r : std_logic_vector(3 downto 0) := (others => '0');
signal age_r : unsigned(4 downto 0) := (others => '0');
signal vio_pulse_r, done_r : std_logic := '0';
-- PID classification, from the encoding (chapter 23.4).
signal is_token, is_data, is_hs, is_ping : std_logic;
signal is_sof, is_setup, is_in, is_data0 : std_logic;
-- Accumulators are held as unsigned rather than as range-constrained
-- integers: a constrained integer aborts simulation on overflow, which
-- turns a mutation into a crash instead of a measured kill.
signal c_x, c_v, c_od, c_oh : unsigned(31 downto 0) := (others => '0');
signal c_dir, c_sd, c_to, c_ov, c_ih : unsigned(31 downto 0) := (others => '0');
begin
is_token <= '1' when pkt_pid(1 downto 0) = "01" else '0';
is_data <= '1' when pkt_pid(1 downto 0) = "11" else '0';
is_hs <= '1' when pkt_pid(1 downto 0) = "10" else '0';
is_ping <= '1' when pkt_pid = "0100" else '0';
is_sof <= '1' when pkt_pid = "0101" else '0';
is_setup <= '1' when pkt_pid = P_SETUP else '0';
is_in <= '1' when pkt_pid = P_IN else '0';
is_data0 <= '1' when pkt_pid = "0011" else '0';
state <= cs_code(st_r);
vio_code <= vi_code(vio_r);
open_token <= tok_r;
wait_age <= std_logic_vector(age_r);
violation <= vio_pulse_r;
xact_done <= done_r;
n_xacts <= std_logic_vector(c_x);
n_violations <= std_logic_vector(c_v);
n_orphan_data <= std_logic_vector(c_od);
n_orphan_hs <= std_logic_vector(c_oh);
n_direction <= std_logic_vector(c_dir);
n_setup_data <= std_logic_vector(c_sd);
n_timeout <= std_logic_vector(c_to);
n_overlap <= std_logic_vector(c_ov);
n_iso_hs <= std_logic_vector(c_ih);
process (clk, rst_n)
variable ns : chk_state_t;
variable nv : vio_t;
variable nt : std_logic_vector(3 downto 0);
variable na : unsigned(4 downto 0);
variable vp, dn : std_logic;
begin
if rst_n = '0' then
st_r <= C_IDLE;
vio_r <= V_NONE;
tok_r <= (others => '0');
age_r <= (others => '0');
vio_pulse_r <= '0';
done_r <= '0';
c_x <= (others => '0');
c_v <= (others => '0');
c_od <= (others => '0');
c_oh <= (others => '0');
c_dir <= (others => '0');
c_sd <= (others => '0');
c_to <= (others => '0');
c_ov <= (others => '0');
c_ih <= (others => '0');
elsif rising_edge(clk) then
ns := st_r; nt := tok_r; nv := vio_r; na := age_r;
vp := '0'; dn := '0';
if pkt_valid = '1' then
-- ---- Rule 0: a token ALWAYS comes from the host. Checked before
-- ---- anything else, because a device driving a token means the bus
-- ---- has two masters and nothing after this point is meaningful.
if is_token = '1' and from_host = '0' then
ns := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif is_token = '1' then
-- A token while a transaction is still open. The previous one will
-- never complete, and saying so is the point -- silently starting
-- the new one hides a lost response for ever.
if st_r /= C_IDLE then
nv := V_OVERLAP; vp := '1';
end if;
nt := pkt_pid;
na := (others => '0');
if is_sof = '1' then
-- A SOF is a complete transaction all by itself.
ns := C_IDLE; dn := '1';
elsif is_in = '1' then
ns := C_IN;
else
ns := C_OUT; -- OUT and SETUP both expect host data next
end if;
elsif is_ping = '1' then
if from_host = '0' then
ns := C_IDLE; nv := V_DIRECTION; vp := '1';
else
if st_r /= C_IDLE then nv := V_OVERLAP; vp := '1'; end if;
nt := pkt_pid;
na := (others => '0');
ns := C_PING;
end if;
elsif is_data = '1' then
case st_r is
when C_OUT =>
-- The host sends the data on an OUT or a SETUP.
if from_host = '0' then
ns := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif tok_r = P_SETUP and is_data0 = '0' then
-- ---- A SETUP is ALWAYS DATA0. The control transfer's whole
-- ---- toggle sequence is defined from that starting point,
-- ---- so a SETUP carrying DATA1 desynchronises every stage
-- ---- that follows it (chapter 21.1).
ns := C_IDLE; nv := V_SETUP_DATA; vp := '1';
elsif ep_iso = '1' then
-- Isochronous: no handshake follows, so the transaction is
-- complete the moment the data has been sent.
ns := C_IDLE; dn := '1';
else
ns := C_HS; na := (others => '0');
end if;
when C_IN =>
-- The DEVICE sends the data on an IN.
if from_host = '1' then
ns := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif ep_iso = '1' then
ns := C_IDLE; dn := '1';
else
ns := C_HS; na := (others => '0');
end if;
when others =>
-- ---- Data with nothing in front of it. ----
ns := C_IDLE; nv := V_ORPHAN_DATA; vp := '1';
end case;
elsif is_hs = '1' then
case st_r is
when C_IN =>
-- NAK / STALL / NYET from the device instead of data. This is a
-- COMPLETE and perfectly legal transaction, and a checker that
-- treats it as an error flags every flow-controlled transfer on
-- a busy bus.
if from_host = '1' then
ns := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif ep_iso = '1' then
ns := C_IDLE; nv := V_ISO_HS; vp := '1';
else
ns := C_IDLE; dn := '1';
end if;
when C_PING =>
if from_host = '1' then
ns := C_IDLE; nv := V_DIRECTION; vp := '1';
else
ns := C_IDLE; dn := '1';
end if;
when C_HS =>
-- The acknowledging side is whoever did NOT send the data: the
-- device on an OUT, the host on an IN.
if (from_host = '1') /= (tok_r = P_IN) then
ns := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif ep_iso = '1' then
ns := C_IDLE; nv := V_ISO_HS; vp := '1';
else
ns := C_IDLE; dn := '1';
end if;
when C_OUT =>
-- A handshake where the host's data was expected. On a
-- non-isochronous OUT this is the device refusing early, which
-- the protocol does not provide for.
ns := C_IDLE; nv := V_ORPHAN_HS; vp := '1';
when others =>
ns := C_IDLE; nv := V_ORPHAN_HS; vp := '1';
end case;
end if;
-- Any other PID -- SPLIT, PRE/ERR, reserved -- is outside this
-- checker's remit and is deliberately ignored rather than flagged.
-- A checker that reports things it does not model is a checker
-- people switch off.
elsif st_r /= C_IDLE then
-- ---- THE LIVENESS RULE. Nothing arrived. ----
--
-- Every other rule in this block is of the form "if X then Y must
-- follow". None of them fires when NOTHING follows, which is how a
-- device that has simply stopped answering passes a checker that is
-- otherwise complete.
if age_r >= to_unsigned(TO_MAX - 1, 5) then
ns := C_IDLE; nv := V_TIMEOUT; vp := '1'; na := (others => '0');
elsif bus_idle = '1' then
na := age_r + 1;
end if;
end if;
-- ---- The timer belongs to an OPEN transaction, and to nothing else.
--
-- Returning to IDLE -- for ANY reason, a completion or a violation --
-- ends the transaction, so the counter goes with it. Left running, a
-- stale age shortens the NEXT transaction's patience by however long
-- the previous one happened to wait, and the checker then times out a
-- perfectly healthy transfer. That is a false positive generated by
-- the checker's own bookkeeping, which is the worst kind: it is not
-- wrong about the bus, it is wrong about itself.
if ns = C_IDLE then na := (others => '0'); end if;
-- ---- A cycle that produced a violation never also reports a
-- ---- completed transaction.
--
-- The case that forces this is a SOF arriving while another transaction
-- is still open: the SOF is a complete transaction all by itself, AND it
-- has just abandoned the one in progress. Both statements are true, and
-- reporting both leaves a consumer unable to say what happened.
--
-- It matters more than it looks, because n_xacts is a DENOMINATOR --
-- violations per transaction is the number anybody actually quotes.
-- Counting the interrupting packet as healthy traffic makes the error
-- rate look better exactly when the bus is going wrong.
if vp = '1' then dn := '0'; end if;
st_r <= ns;
tok_r <= nt;
age_r <= na;
vio_r <= nv;
vio_pulse_r <= vp;
done_r <= dn;
if dn = '1' then c_x <= c_x + 1; end if;
-- The per-cause counters are driven by the SAME pulse as the total,
-- so they sum to it by construction (chapter 23.4).
if vp = '1' then
c_v <= c_v + 1;
case nv is
when V_ORPHAN_DATA => c_od <= c_od + 1;
when V_ORPHAN_HS => c_oh <= c_oh + 1;
when V_DIRECTION => c_dir <= c_dir + 1;
when V_SETUP_DATA => c_sd <= c_sd + 1;
when V_TIMEOUT => c_to <= c_to + 1;
when V_OVERLAP => c_ov <= c_ov + 1;
when V_ISO_HS => c_ih <= c_ih + 1;
when others => null;
end case;
end if;
end if;
end process;
end architecture rtl;10. Seeing It Work, and Seeing It Catch Something
A legal IN transaction, a direction violation, and a SOF
usb_protocol_checker — a good transaction and a bad one
10 cyclesCompare cycle 1 with cycle 5. The PID is identical. The state is identical. Only from_host differs, and one of them is a bus with two devices driving it.
11. The Testbenches, and Why Most of Their Cycles Look Wasted
Verifying a checker is not like verifying a design. A design has one failure mode that matters. A checker has two, and they pull in opposite directions:
| What it costs | |
|---|---|
| false negative — it misses a real violation | the checker is useless |
| false positive — it flags legal traffic | the checker gets switched off, and then it is worse than useless, because everybody believes the bus was checked |
The second is the one that kills checkers in practice, and it is the one a violation-injection suite never tests. So this suite spends most of its cycles doing something that looks pointless:
1760 PERFECTLY LEGAL TRANSACTIONS, of all eight shapes:
SOF OUT (x4 handshake PIDs)
SETUP IN with data
IN with NAK PING
isochronous OUT isochronous IN
...each with both data toggles, and every one required to
produce EXACTLY ONE completion and ZERO violations. check(n_violations == before_v,
"a PERFECTLY LEGAL transaction was flagged as a violation -- a checker with false positives gets switched off, and then nobody is checking anything");The exhaustive part is the sweep: every checker state × all 16 PIDs × both directions × isochronous and not = 320 combinations, each reached by real packets.
And one property that cannot be expressed as a packet sequence at all:
// ---- Phase D: THE TIMEOUT. No packet sequence can express "and then
// ---- nothing happened", so it is driven directly.
for (k = 0; k < 4; k = k + 1) begin
...
// one cycle short of the bound: still waiting, patiently
quiet(TO_MAX - 1, 1'b0);
check(n_timeout == before_v,
"the checker gave up before its own timeout bound");
check(state !== C_IDLE,
"the checker abandoned the transaction early");
quiet(3, 1'b0);
check(n_timeout == before_v + 1,
"the checker did not notice a transaction that never completed -- without this it is blind to the most common way real hardware fails");
endNote that it checks both edges: one cycle short of the bound the checker must still be waiting, and past it, it must have given up. Checking only the second half passes a checker that gives up immediately.
11.1 Verilog testbench
// Testbench for usb_protocol_checker (Verilog-2005).
//
// VERIFYING A CHECKER IS NOT LIKE VERIFYING A DESIGN
//
// A design has one failure mode that matters: doing the wrong thing. A
// checker has TWO, and they pull in opposite directions.
//
// FALSE NEGATIVE it misses a real violation. The checker is useless.
// FALSE POSITIVE it flags legal traffic. The checker is SWITCHED
// OFF, and then it is worse
// than useless, because
// everyone believes the bus
// was checked.
//
// The second is the one that kills checkers in practice, and it is the one
// a violation-injection suite never tests. So this suite spends most of its
// cycles doing something that looks pointless: driving THOUSANDS OF PERFECTLY
// LEGAL TRANSACTIONS of every shape the protocol allows -- SOF, OUT, SETUP,
// IN-with-data, IN-with-NAK, PING, isochronous OUT, isochronous IN -- and
// demanding ZERO violations.
//
// WHAT IS EXHAUSTIVE HERE
//
// Every checker state x every one of the 16 PIDs x both directions x
// isochronous and not = 320 combinations, each reached by real packets.
//
// AND THE PROPERTY THAT IS NOT A SWEEP
//
// The timeout. No packet sequence can express "and then nothing happened",
// so it is driven directly: open a transaction, go quiet, and require the
// checker to notice.
`timescale 1ns/1ps
module tb_pc_v;
localparam integer TO_MAX = 12;
localparam [2:0] C_IDLE=3'd0, C_OUT=3'd1, C_IN=3'd2, C_HS=3'd3, C_PING=3'd4;
localparam [3:0] V_NONE=4'd0, V_ORPHAN_DATA=4'd1, V_ORPHAN_HS=4'd2,
V_DIRECTION=4'd3, V_SETUP_DATA=4'd4, V_TIMEOUT=4'd5,
V_OVERLAP=4'd6, V_ISO_HS=4'd7;
localparam [3:0] P_OUT=4'b0001, P_IN=4'b1001, P_SOF=4'b0101, P_SETUP=4'b1101,
P_DATA0=4'b0011, P_DATA1=4'b1011, P_DATA2=4'b0111, P_MDATA=4'b1111,
P_ACK=4'b0010, P_NAK=4'b1010, P_STALL=4'b1110, P_NYET=4'b0110,
P_PING=4'b0100, P_SPLIT=4'b1000, P_PRE=4'b1100, P_RSVD=4'b0000;
reg clk = 1'b0, rst_n = 1'b0;
reg pkt_valid = 1'b0, from_host = 1'b1, ep_iso = 1'b0, bus_idle = 1'b1;
reg [3:0] pkt_pid = 4'd0;
wire [2:0] state;
wire [3:0] vio_code, open_token;
wire [4:0] wait_age;
wire violation, xact_done;
wire [31:0] n_xacts, n_violations, n_orphan_data, n_orphan_hs,
n_direction, n_setup_data, n_timeout, n_overlap, n_iso_hs;
usb_protocol_checker #(.TO_MAX(TO_MAX)) dut (
.clk(clk), .rst_n(rst_n),
.pkt_valid(pkt_valid), .pkt_pid(pkt_pid), .from_host(from_host),
.ep_iso(ep_iso), .bus_idle(bus_idle),
.state(state), .violation(violation), .vio_code(vio_code),
.xact_done(xact_done), .open_token(open_token), .wait_age(wait_age),
.n_xacts(n_xacts), .n_violations(n_violations),
.n_orphan_data(n_orphan_data), .n_orphan_hs(n_orphan_hs),
.n_direction(n_direction), .n_setup_data(n_setup_data),
.n_timeout(n_timeout), .n_overlap(n_overlap), .n_iso_hs(n_iso_hs)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0;
task check(input cond, input [1023:0] msg);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 25)
$display("FAIL @%0t: %0s | st=%0d tok=%h vio=%0d age=%0d",
$time, msg, state, open_token, vio_code, wait_age);
end
end
endtask
// ------------------------------------------------------------------
// The shadow checker. Written from the protocol, not from the design.
// ------------------------------------------------------------------
reg [2:0] m_st;
reg [3:0] m_tok, m_vio;
reg [4:0] m_age;
reg m_vp, m_dn;
integer m_x, m_v, m_od, m_oh, m_dir, m_sd, m_to, m_ov, m_ih;
integer seen [0:319]; // 5 states x 16 PIDs x 2 dirs x 2 iso
integer n_seen, n_steps;
task model_reset;
integer i;
begin
m_st = C_IDLE; m_tok = 4'd0; m_vio = V_NONE; m_age = 5'd0;
m_vp = 1'b0; m_dn = 1'b0;
m_x = 0; m_v = 0; m_od = 0; m_oh = 0; m_dir = 0;
m_sd = 0; m_to = 0; m_ov = 0; m_ih = 0;
for (i = 0; i < 320; i = i + 1) seen[i] = 0;
n_seen = 0; n_steps = 0;
end
endtask
integer idx;
task step(input pv, input [3:0] pid, input fh, input iso, input bi);
reg [2:0] ns;
reg [3:0] nt, nv;
reg [4:0] na;
reg vp, dn;
reg tok, dat, hs, png, sof, setup, inn, d0;
begin
pkt_valid = pv; pkt_pid = pid; from_host = fh;
ep_iso = iso; bus_idle = bi;
#1;
check(state === m_st, "state disagrees with the shadow checker");
check(open_token === m_tok, "open_token disagrees");
check(wait_age === m_age, "wait_age disagrees -- the timeout does not run the way the model says");
check(vio_code === m_vio, "vio_code disagrees");
check(violation === m_vp, "the violation pulse disagrees");
check(xact_done === m_dn, "xact_done disagrees");
check(!(violation && xact_done),
"a transaction was reported complete and illegal in the same cycle");
check(wait_age <= TO_MAX[4:0],
"the timeout counter ran past its bound");
check(!((m_st == C_IDLE) && (m_age != 5'd0)),
"the timeout counter is running with no transaction open");
if (pv) begin
idx = m_st * 64 + pid * 4 + (fh ? 2 : 0) + (iso ? 1 : 0);
if (seen[idx] == 0) begin seen[idx] = 1; n_seen = n_seen + 1; end
end
n_steps = n_steps + 1;
// ---- advance the shadow checker ----
ns = m_st; nt = m_tok; nv = m_vio; na = m_age; vp = 1'b0; dn = 1'b0;
tok = (pid[1:0] == 2'b01);
dat = (pid[1:0] == 2'b11);
hs = (pid[1:0] == 2'b10);
png = (pid == P_PING);
sof = (pid == P_SOF);
setup = (pid == P_SETUP);
inn = (pid == P_IN);
d0 = (pid == P_DATA0);
if (pv) begin
if (tok && !fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else if (tok) begin
if (m_st != C_IDLE) begin nv = V_OVERLAP; vp = 1'b1; end
nt = pid; na = 5'd0;
if (sof) begin ns = C_IDLE; dn = 1'b1; end
else if (inn) ns = C_IN;
else ns = C_OUT;
end else if (png) begin
if (!fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else begin
if (m_st != C_IDLE) begin nv = V_OVERLAP; vp = 1'b1; end
nt = pid; na = 5'd0; ns = C_PING;
end
end else if (dat) begin
if (m_st == C_OUT) begin
if (!fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else if ((m_tok == P_SETUP) && !d0) begin
ns = C_IDLE; nv = V_SETUP_DATA; vp = 1'b1;
end else if (iso) begin ns = C_IDLE; dn = 1'b1; end
else begin ns = C_HS; na = 5'd0; end
end else if (m_st == C_IN) begin
if (fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else if (iso) begin ns = C_IDLE; dn = 1'b1; end
else begin ns = C_HS; na = 5'd0; end
end else begin
ns = C_IDLE; nv = V_ORPHAN_DATA; vp = 1'b1;
end
end else if (hs) begin
if (m_st == C_IN) begin
if (fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else if (iso) begin ns = C_IDLE; nv = V_ISO_HS; vp = 1'b1; end
else begin ns = C_IDLE; dn = 1'b1; end
end else if (m_st == C_PING) begin
if (fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else begin ns = C_IDLE; dn = 1'b1; end
end else if (m_st == C_HS) begin
if (fh != (m_tok == P_IN)) begin
ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1;
end else if (iso) begin ns = C_IDLE; nv = V_ISO_HS; vp = 1'b1; end
else begin ns = C_IDLE; dn = 1'b1; end
end else begin
ns = C_IDLE; nv = V_ORPHAN_HS; vp = 1'b1;
end
end
end else if (m_st != C_IDLE) begin
if (m_age >= TO_MAX[4:0] - 5'd1) begin
ns = C_IDLE; nv = V_TIMEOUT; vp = 1'b1; na = 5'd0;
end else if (bi) na = m_age + 5'd1;
end
// The timer belongs to an open transaction and to nothing else.
if (ns == C_IDLE) na = 5'd0;
// And a cycle that produced a violation never also reports a
// completed transaction -- n_xacts is a denominator.
if (vp) dn = 1'b0;
m_st = ns; m_tok = nt; m_vio = nv; m_age = na; m_vp = vp; m_dn = dn;
if (dn) m_x = m_x + 1;
if (vp) begin
m_v = m_v + 1;
case (nv)
V_ORPHAN_DATA: m_od = m_od + 1;
V_ORPHAN_HS: m_oh = m_oh + 1;
V_DIRECTION: m_dir = m_dir + 1;
V_SETUP_DATA: m_sd = m_sd + 1;
V_TIMEOUT: m_to = m_to + 1;
V_OVERLAP: m_ov = m_ov + 1;
V_ISO_HS: m_ih = m_ih + 1;
default: ;
endcase
end
@(posedge clk); #1;
pkt_valid = 1'b0;
end
endtask
task pkt(input [3:0] pid, input fh, input iso);
begin step(1'b1, pid, fh, iso, 1'b0); end
endtask
task quiet(input integer n, input iso);
integer i;
begin for (i = 0; i < n; i = i + 1) step(1'b0, 4'd0, 1'b1, iso, 1'b1); end
endtask
// Put the checker back in IDLE the way the protocol allows -- by letting
// the transaction time out -- never by forcing the state.
task settle(input iso);
begin
quiet(TO_MAX + 2, iso);
check(state === C_IDLE, "the checker did not return to IDLE after a quiet bus");
end
endtask
// ------------------------------------------------------------------
// The eight LEGAL transaction shapes. Each must complete with exactly
// one xact_done and ZERO violations.
// ------------------------------------------------------------------
integer before_v, before_x;
task legal(input integer shape, input [3:0] dpid, input [3:0] hpid);
begin
before_v = n_violations;
before_x = n_xacts;
case (shape)
0: pkt(P_SOF, 1'b1, 1'b0); // SOF
1: begin pkt(P_OUT, 1'b1,1'b0); pkt(dpid,1'b1,1'b0);
pkt(hpid,1'b0,1'b0); end // OUT
2: begin pkt(P_SETUP,1'b1,1'b0); pkt(P_DATA0,1'b1,1'b0);
pkt(P_ACK,1'b0,1'b0); end // SETUP
3: begin pkt(P_IN, 1'b1,1'b0); pkt(dpid,1'b0,1'b0);
pkt(P_ACK,1'b1,1'b0); end // IN+data
4: begin pkt(P_IN, 1'b1,1'b0); pkt(hpid,1'b0,1'b0); end // IN+NAK
5: begin pkt(P_PING, 1'b1,1'b0); pkt(hpid,1'b0,1'b0); end // PING
6: begin pkt(P_OUT, 1'b1,1'b1); pkt(dpid,1'b1,1'b1); end // iso OUT
7: begin pkt(P_IN, 1'b1,1'b1); pkt(dpid,1'b0,1'b1); end // iso IN
endcase
check(n_violations == before_v,
"a PERFECTLY LEGAL transaction was flagged as a violation -- a checker with false positives gets switched off, and then nobody is checking anything");
check(n_xacts == before_x + 1,
"a legal transaction did not complete exactly once");
check(state === C_IDLE, "the checker did not return to IDLE after a legal transaction");
end
endtask
integer s, p, f, o, i, k;
reg [3:0] dsel, hsel;
initial begin
model_reset;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(state === C_IDLE, "reset did not land in IDLE");
check(violation === 1'b0, "reset asserted a violation");
check(n_violations === 32'd0, "reset left the violation counter non-zero");
// ---- Phase B: NO FALSE POSITIVES. Every legal shape, many times, with
// ---- every data PID and every handshake PID the shape permits.
for (k = 0; k < 220; k = k + 1) begin
dsel = (k % 2) ? P_DATA1 : P_DATA0;
case (k % 4)
0: hsel = P_ACK; 1: hsel = P_NAK; 2: hsel = P_STALL;
default: hsel = P_NYET;
endcase
legal(0, dsel, hsel);
legal(1, dsel, hsel);
legal(2, dsel, hsel);
legal(3, dsel, P_ACK);
legal(4, dsel, hsel);
legal(5, dsel, (k % 2) ? P_NAK : P_ACK);
legal(6, dsel, hsel);
legal(7, dsel, hsel);
end
// ---- Phase C: EXHAUSTIVE. Every state x PID x direction x iso. ----
for (s = 0; s < 5; s = s + 1) begin
for (p = 0; p < 16; p = p + 1) begin
for (f = 0; f < 2; f = f + 1) begin
for (o = 0; o < 2; o = o + 1) begin
settle(o[0]);
// reach the state with real packets, never by forcing
case (s)
0: ;
1: pkt(P_OUT, 1'b1, o[0]);
2: pkt(P_IN, 1'b1, o[0]);
3: begin pkt(P_OUT, 1'b1, 1'b0); pkt(P_DATA0, 1'b1, 1'b0); end
4: pkt(P_PING, 1'b1, o[0]);
endcase
check(state === s[2:0],
"the sweep could not reach the state it meant to reach");
step(1'b1, p[3:0], f[0], o[0], 1'b0);
step(1'b1, p[3:0], f[0], o[0], 1'b0);
end
end
end
end
// ---- Phase D: THE TIMEOUT. No packet sequence can express "and then
// ---- nothing happened", so it is driven directly.
for (k = 0; k < 4; k = k + 1) begin
before_v = n_timeout;
case (k)
0: pkt(P_OUT, 1'b1, 1'b0);
1: pkt(P_IN, 1'b1, 1'b0);
2: pkt(P_PING, 1'b1, 1'b0);
default: begin pkt(P_OUT, 1'b1, 1'b0); pkt(P_DATA0, 1'b1, 1'b0); end
endcase
// one cycle short of the bound: still waiting, patiently
quiet(TO_MAX - 1, 1'b0);
check(n_timeout == before_v,
"the checker gave up before its own timeout bound");
check(state !== C_IDLE,
"the checker abandoned the transaction early");
quiet(3, 1'b0);
check(n_timeout == before_v + 1,
"the checker did not notice a transaction that never completed -- without this it is blind to the most common way real hardware fails");
check(state === C_IDLE, "the checker did not resynchronise after a timeout");
end
// ---- A busy bus must NOT time out. The counter advances on dead air
// ---- only, so a transaction interleaved with other traffic is not
// ---- abandoned merely for taking a while.
settle(1'b0);
pkt(P_IN, 1'b1, 1'b0);
before_v = n_timeout;
for (k = 0; k < 3 * TO_MAX; k = k + 1) step(1'b0, 4'd0, 1'b1, 1'b0, 1'b0);
check(n_timeout == before_v,
"the checker timed out a transaction while the bus was busy -- the counter must measure dead air, not elapsed time");
pkt(P_DATA0, 1'b0, 1'b0);
pkt(P_ACK, 1'b1, 1'b0);
// ---- Phase E: every violation kind, deliberately, one at a time ----
settle(1'b0); pkt(P_DATA0, 1'b1, 1'b0); // orphan data
settle(1'b0); pkt(P_ACK, 1'b0, 1'b0); // orphan hs
settle(1'b0); pkt(P_OUT, 1'b0, 1'b0); // direction
settle(1'b0); pkt(P_SETUP, 1'b1, 1'b0); pkt(P_DATA1,1'b1,1'b0); // setup/DATA1
settle(1'b0); pkt(P_OUT, 1'b1, 1'b0); pkt(P_IN, 1'b1,1'b0); // overlap
settle(1'b0); pkt(P_IN, 1'b1, 1'b1); pkt(P_NAK, 1'b0,1'b1); // iso hs
// ---- Phase F: random traffic, mostly legal, with corruption injected ----
for (i = 0; i < 30000; i = i + 1)
step(($unsigned($random) % 100) < 62,
$random,
($unsigned($random) % 100) < 55,
($unsigned($random) % 100) < 25,
($unsigned($random) % 100) < 70);
// ---- Phase G: and legal traffic again AFTERWARDS, so the checker is
// ---- shown to still work rather than merely to have stopped.
settle(1'b0);
for (k = 0; k < 120; k = k + 1) begin
legal(1, P_DATA0, P_ACK);
legal(3, P_DATA1, P_ACK);
legal(7, P_DATA0, P_ACK);
end
// ---- Final agreement ----
check(n_xacts === m_x[31:0], "n_xacts disagrees with the model");
check(n_violations === m_v[31:0], "n_violations disagrees with the model");
check(n_orphan_data === m_od[31:0], "n_orphan_data disagrees");
check(n_orphan_hs === m_oh[31:0], "n_orphan_hs disagrees");
check(n_direction === m_dir[31:0], "n_direction disagrees");
check(n_setup_data === m_sd[31:0], "n_setup_data disagrees");
check(n_timeout === m_to[31:0], "n_timeout disagrees");
check(n_overlap === m_ov[31:0], "n_overlap disagrees");
check(n_iso_hs === m_ih[31:0], "n_iso_hs disagrees");
check(n_violations === n_orphan_data + n_orphan_hs + n_direction +
n_setup_data + n_timeout + n_overlap + n_iso_hs,
"the violation causes do not sum to the total -- a violation was mis-classified");
check(n_seen == 320, "not every state was crossed with every PID, direction and endpoint kind");
check(n_orphan_data > 32'd0, "orphan data was never seen");
check(n_orphan_hs > 32'd0, "an orphan handshake was never seen");
check(n_direction > 32'd0, "a direction violation was never seen");
check(n_setup_data > 32'd0, "a SETUP carrying DATA1 was never seen");
check(n_timeout > 32'd0, "the timeout was never exercised");
check(n_overlap > 32'd0, "an overlapping token was never seen");
check(n_iso_hs > 32'd0, "a handshake on an isochronous endpoint was never seen");
check(n_xacts > 32'd2000, "too few legal transactions to have tested for false positives");
$display("REACH state-x-pid-x-dir-x-iso=%0d/320 steps=%0d", n_seen, n_steps);
$display("COUNTERS xacts=%0d violations=%0d orphan-data=%0d orphan-hs=%0d dir=%0d setup=%0d timeout=%0d overlap=%0d iso-hs=%0d",
n_xacts, n_violations, n_orphan_data, n_orphan_hs, n_direction,
n_setup_data, n_timeout, n_overlap, n_iso_hs);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule11.2 SystemVerilog testbench
// Testbench for usb_protocol_checker (SystemVerilog).
//
// VERIFYING A CHECKER IS NOT LIKE VERIFYING A DESIGN
//
// A design has one failure mode that matters: doing the wrong thing. A
// checker has TWO, and they pull in opposite directions.
//
// FALSE NEGATIVE it misses a real violation. The checker is useless.
// FALSE POSITIVE it flags legal traffic. The checker is SWITCHED
// OFF, and then it is worse
// than useless, because
// everyone believes the bus
// was checked.
//
// The second is the one that kills checkers in practice, and it is the one
// a violation-injection suite never tests. So this suite spends most of its
// cycles doing something that looks pointless: driving THOUSANDS OF PERFECTLY
// LEGAL TRANSACTIONS of every shape the protocol allows -- SOF, OUT, SETUP,
// IN-with-data, IN-with-NAK, PING, isochronous OUT, isochronous IN -- and
// demanding ZERO violations.
//
// WHAT IS EXHAUSTIVE HERE
//
// Every checker state x every one of the 16 PIDs x both directions x
// isochronous and not = 320 combinations, each reached by real packets.
//
// AND THE PROPERTY THAT IS NOT A SWEEP
//
// The timeout. No packet sequence can express "and then nothing happened",
// so it is driven directly: open a transaction, go quiet, and require the
// checker to notice.
`timescale 1ns/1ps
module tb_pc_sv;
import usb_chk_pkg::*;
localparam int TO_MAX = 12;
localparam [3:0] P_OUT=4'b0001, P_IN=4'b1001, P_SOF=4'b0101, P_SETUP=4'b1101,
P_DATA0=4'b0011, P_DATA1=4'b1011, P_DATA2=4'b0111, P_MDATA=4'b1111,
P_ACK=4'b0010, P_NAK=4'b1010, P_STALL=4'b1110, P_NYET=4'b0110,
P_PING=4'b0100, P_SPLIT=4'b1000, P_PRE=4'b1100, P_RSVD=4'b0000;
logic clk = 1'b0, rst_n = 1'b0;
logic pkt_valid = 1'b0, from_host = 1'b1, ep_iso = 1'b0, bus_idle = 1'b1;
logic [3:0] pkt_pid = 4'd0;
chk_state_e state;
vio_e vio_code;
logic [3:0] open_token;
logic [4:0] wait_age;
logic violation, xact_done;
logic [31:0] n_xacts, n_violations, n_orphan_data, n_orphan_hs,
n_direction, n_setup_data, n_timeout, n_overlap, n_iso_hs;
usb_protocol_checker #(.TO_MAX(TO_MAX)) dut (.*);
always #5 clk = ~clk;
int errors = 0, checks = 0;
task automatic check(input logic cond, input string msg);
checks++;
if (!cond) begin
errors++;
if (errors <= 25)
$display("FAIL @%0t: %0s | st=%0d tok=%h vio=%0d age=%0d",
$time, msg, state, open_token, vio_code, wait_age);
end
endtask
// ------------------------------------------------------------------
// The shadow checker. Written from the protocol, not from the design.
// ------------------------------------------------------------------
chk_state_e m_st;
vio_e m_vio;
logic [3:0] m_tok;
logic [4:0] m_age;
logic m_vp, m_dn;
int m_x, m_v, m_od, m_oh, m_dir, m_sd, m_to, m_ov, m_ih;
int seen [320]; // 5 states x 16 PIDs x 2 dirs x 2 iso
int n_seen, n_steps;
task automatic model_reset();
m_st = C_IDLE; m_tok = '0; m_vio = V_NONE; m_age = '0;
m_vp = 1'b0; m_dn = 1'b0;
m_x = 0; m_v = 0; m_od = 0; m_oh = 0; m_dir = 0;
m_sd = 0; m_to = 0; m_ov = 0; m_ih = 0;
foreach (seen[i]) seen[i] = 0;
n_seen = 0; n_steps = 0;
endtask
int idx;
task automatic step(input logic pv, input logic [3:0] pid,
input logic fh, input logic iso, input logic bi);
chk_state_e ns;
vio_e nv;
logic [3:0] nt;
logic [4:0] na;
logic vp, dn;
logic tok, dat, hs, png, sof, setup, inn, d0;
begin
pkt_valid = pv; pkt_pid = pid; from_host = fh;
ep_iso = iso; bus_idle = bi;
#1;
check(state === m_st, "state disagrees with the shadow checker");
check(open_token === m_tok, "open_token disagrees");
check(wait_age === m_age, "wait_age disagrees -- the timeout does not run the way the model says");
check(vio_code === m_vio, "vio_code disagrees");
check(violation === m_vp, "the violation pulse disagrees");
check(xact_done === m_dn, "xact_done disagrees");
check(!(violation && xact_done),
"a transaction was reported complete and illegal in the same cycle");
check(wait_age <= 5'(TO_MAX),
"the timeout counter ran past its bound");
check(!((m_st == C_IDLE) && (m_age != 5'd0)),
"the timeout counter is running with no transaction open");
if (pv) begin
idx = int'(m_st) * 64 + int'(pid) * 4 + (fh ? 2 : 0) + (iso ? 1 : 0);
if (seen[idx] == 0) begin seen[idx] = 1; n_seen = n_seen + 1; end
end
n_steps = n_steps + 1;
// ---- advance the shadow checker ----
ns = m_st; nt = m_tok; nv = m_vio; na = m_age; vp = 1'b0; dn = 1'b0;
tok = (pid[1:0] == 2'b01);
dat = (pid[1:0] == 2'b11);
hs = (pid[1:0] == 2'b10);
png = (pid == P_PING);
sof = (pid == P_SOF);
setup = (pid == P_SETUP);
inn = (pid == P_IN);
d0 = (pid == P_DATA0);
if (pv) begin
if (tok && !fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else if (tok) begin
if (m_st != C_IDLE) begin nv = V_OVERLAP; vp = 1'b1; end
nt = pid; na = 5'd0;
if (sof) begin ns = C_IDLE; dn = 1'b1; end
else if (inn) ns = C_IN;
else ns = C_OUT;
end else if (png) begin
if (!fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else begin
if (m_st != C_IDLE) begin nv = V_OVERLAP; vp = 1'b1; end
nt = pid; na = 5'd0; ns = C_PING;
end
end else if (dat) begin
if (m_st == C_OUT) begin
if (!fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else if ((m_tok == P_SETUP) && !d0) begin
ns = C_IDLE; nv = V_SETUP_DATA; vp = 1'b1;
end else if (iso) begin ns = C_IDLE; dn = 1'b1; end
else begin ns = C_HS; na = 5'd0; end
end else if (m_st == C_IN) begin
if (fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else if (iso) begin ns = C_IDLE; dn = 1'b1; end
else begin ns = C_HS; na = 5'd0; end
end else begin
ns = C_IDLE; nv = V_ORPHAN_DATA; vp = 1'b1;
end
end else if (hs) begin
if (m_st == C_IN) begin
if (fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else if (iso) begin ns = C_IDLE; nv = V_ISO_HS; vp = 1'b1; end
else begin ns = C_IDLE; dn = 1'b1; end
end else if (m_st == C_PING) begin
if (fh) begin ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1; end
else begin ns = C_IDLE; dn = 1'b1; end
end else if (m_st == C_HS) begin
if (fh != (m_tok == P_IN)) begin
ns = C_IDLE; nv = V_DIRECTION; vp = 1'b1;
end else if (iso) begin ns = C_IDLE; nv = V_ISO_HS; vp = 1'b1; end
else begin ns = C_IDLE; dn = 1'b1; end
end else begin
ns = C_IDLE; nv = V_ORPHAN_HS; vp = 1'b1;
end
end
end else if (m_st != C_IDLE) begin
if (m_age >= 5'(TO_MAX) - 5'd1) begin
ns = C_IDLE; nv = V_TIMEOUT; vp = 1'b1; na = 5'd0;
end else if (bi) na = m_age + 5'd1;
end
// The timer belongs to an open transaction and to nothing else.
if (ns == C_IDLE) na = 5'd0;
// And a cycle that produced a violation never also reports a
// completed transaction -- n_xacts is a denominator.
if (vp) dn = 1'b0;
m_st = ns; m_tok = nt; m_vio = nv; m_age = na; m_vp = vp; m_dn = dn;
if (dn) m_x++;
if (vp) begin
m_v++;
case (nv)
V_ORPHAN_DATA: m_od++;
V_ORPHAN_HS: m_oh++;
V_DIRECTION: m_dir++;
V_SETUP_DATA: m_sd++;
V_TIMEOUT: m_to++;
V_OVERLAP: m_ov++;
V_ISO_HS: m_ih++;
default: ;
endcase
end
@(posedge clk); #1;
pkt_valid = 1'b0;
end
endtask
task automatic pkt(input logic [3:0] pid, input logic fh, input logic iso);
step(1'b1, pid, fh, iso, 1'b0);
endtask
task automatic quiet(input int n, input logic iso);
repeat (n) step(1'b0, 4'd0, 1'b1, iso, 1'b1);
endtask
// Put the checker back in IDLE the way the protocol allows -- by letting
// the transaction time out -- never by forcing the state.
task automatic settle(input logic iso);
quiet(TO_MAX + 2, iso);
check(state === C_IDLE, "the checker did not return to IDLE after a quiet bus");
endtask
// ------------------------------------------------------------------
// The eight LEGAL transaction shapes. Each must complete with exactly
// one xact_done and ZERO violations.
// ------------------------------------------------------------------
int before_v, before_x;
task automatic legal(input int shape, input logic [3:0] dpid,
input logic [3:0] hpid);
begin
before_v = n_violations;
before_x = n_xacts;
case (shape)
0: pkt(P_SOF, 1'b1, 1'b0); // SOF
1: begin pkt(P_OUT, 1'b1,1'b0); pkt(dpid,1'b1,1'b0);
pkt(hpid,1'b0,1'b0); end // OUT
2: begin pkt(P_SETUP,1'b1,1'b0); pkt(P_DATA0,1'b1,1'b0);
pkt(P_ACK,1'b0,1'b0); end // SETUP
3: begin pkt(P_IN, 1'b1,1'b0); pkt(dpid,1'b0,1'b0);
pkt(P_ACK,1'b1,1'b0); end // IN+data
4: begin pkt(P_IN, 1'b1,1'b0); pkt(hpid,1'b0,1'b0); end // IN+NAK
5: begin pkt(P_PING, 1'b1,1'b0); pkt(hpid,1'b0,1'b0); end // PING
6: begin pkt(P_OUT, 1'b1,1'b1); pkt(dpid,1'b1,1'b1); end // iso OUT
7: begin pkt(P_IN, 1'b1,1'b1); pkt(dpid,1'b0,1'b1); end // iso IN
endcase
check(n_violations == before_v,
"a PERFECTLY LEGAL transaction was flagged as a violation -- a checker with false positives gets switched off, and then nobody is checking anything");
check(n_xacts == before_x + 1,
"a legal transaction did not complete exactly once");
check(state === C_IDLE, "the checker did not return to IDLE after a legal transaction");
end
endtask
int s, p, f, o, i, k;
logic [3:0] dsel, hsel;
initial begin
model_reset();
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(state === C_IDLE, "reset did not land in IDLE");
check(violation === 1'b0, "reset asserted a violation");
check(n_violations === 32'd0, "reset left the violation counter non-zero");
// ---- Phase B: NO FALSE POSITIVES. Every legal shape, many times, with
// ---- every data PID and every handshake PID the shape permits.
for (k = 0; k < 220; k++) begin
dsel = (k % 2) ? P_DATA1 : P_DATA0;
case (k % 4)
0: hsel = P_ACK; 1: hsel = P_NAK; 2: hsel = P_STALL;
default: hsel = P_NYET;
endcase
legal(0, dsel, hsel);
legal(1, dsel, hsel);
legal(2, dsel, hsel);
legal(3, dsel, P_ACK);
legal(4, dsel, hsel);
legal(5, dsel, (k % 2) ? P_NAK : P_ACK);
legal(6, dsel, hsel);
legal(7, dsel, hsel);
end
// ---- Phase C: EXHAUSTIVE. Every state x PID x direction x iso. ----
for (s = 0; s < 5; s++) begin
for (p = 0; p < 16; p++) begin
for (f = 0; f < 2; f++) begin
for (o = 0; o < 2; o++) begin
settle(o[0]);
// reach the state with real packets, never by forcing
case (s)
0: ;
1: pkt(P_OUT, 1'b1, o[0]);
2: pkt(P_IN, 1'b1, o[0]);
3: begin pkt(P_OUT, 1'b1, 1'b0); pkt(P_DATA0, 1'b1, 1'b0); end
4: pkt(P_PING, 1'b1, o[0]);
endcase
check(state === chk_state_e'(s),
"the sweep could not reach the state it meant to reach");
step(1'b1, 4'(p), f[0], o[0], 1'b0);
step(1'b1, 4'(p), f[0], o[0], 1'b0);
end
end
end
end
// ---- Phase D: THE TIMEOUT. No packet sequence can express "and then
// ---- nothing happened", so it is driven directly.
for (k = 0; k < 4; k++) begin
before_v = n_timeout;
case (k)
0: pkt(P_OUT, 1'b1, 1'b0);
1: pkt(P_IN, 1'b1, 1'b0);
2: pkt(P_PING, 1'b1, 1'b0);
default: begin pkt(P_OUT, 1'b1, 1'b0); pkt(P_DATA0, 1'b1, 1'b0); end
endcase
// one cycle short of the bound: still waiting, patiently
quiet(TO_MAX - 1, 1'b0);
check(n_timeout == before_v,
"the checker gave up before its own timeout bound");
check(state !== C_IDLE,
"the checker abandoned the transaction early");
quiet(3, 1'b0);
check(n_timeout == before_v + 1,
"the checker did not notice a transaction that never completed -- without this it is blind to the most common way real hardware fails");
check(state === C_IDLE, "the checker did not resynchronise after a timeout");
end
// ---- A busy bus must NOT time out. The counter advances on dead air
// ---- only, so a transaction interleaved with other traffic is not
// ---- abandoned merely for taking a while.
settle(1'b0);
pkt(P_IN, 1'b1, 1'b0);
before_v = n_timeout;
repeat (3 * TO_MAX) step(1'b0, 4'd0, 1'b1, 1'b0, 1'b0);
check(n_timeout == before_v,
"the checker timed out a transaction while the bus was busy -- the counter must measure dead air, not elapsed time");
pkt(P_DATA0, 1'b0, 1'b0);
pkt(P_ACK, 1'b1, 1'b0);
// ---- Phase E: every violation kind, deliberately, one at a time ----
settle(1'b0); pkt(P_DATA0, 1'b1, 1'b0); // orphan data
settle(1'b0); pkt(P_ACK, 1'b0, 1'b0); // orphan hs
settle(1'b0); pkt(P_OUT, 1'b0, 1'b0); // direction
settle(1'b0); pkt(P_SETUP, 1'b1, 1'b0); pkt(P_DATA1,1'b1,1'b0); // setup/DATA1
settle(1'b0); pkt(P_OUT, 1'b1, 1'b0); pkt(P_IN, 1'b1,1'b0); // overlap
settle(1'b0); pkt(P_IN, 1'b1, 1'b1); pkt(P_NAK, 1'b0,1'b1); // iso hs
// ---- Phase F: random traffic, mostly legal, with corruption injected ----
for (i = 0; i < 30000; i++)
step($urandom_range(0,99) < 62,
4'($urandom()),
$urandom_range(0,99) < 55,
$urandom_range(0,99) < 25,
$urandom_range(0,99) < 70);
// ---- Phase G: and legal traffic again AFTERWARDS, so the checker is
// ---- shown to still work rather than merely to have stopped.
settle(1'b0);
for (k = 0; k < 120; k++) begin
legal(1, P_DATA0, P_ACK);
legal(3, P_DATA1, P_ACK);
legal(7, P_DATA0, P_ACK);
end
// ---- Final agreement ----
check(n_xacts === 32'(m_x), "n_xacts disagrees with the model");
check(n_violations === 32'(m_v), "n_violations disagrees with the model");
check(n_orphan_data === 32'(m_od), "n_orphan_data disagrees");
check(n_orphan_hs === 32'(m_oh), "n_orphan_hs disagrees");
check(n_direction === 32'(m_dir), "n_direction disagrees");
check(n_setup_data === 32'(m_sd), "n_setup_data disagrees");
check(n_timeout === 32'(m_to), "n_timeout disagrees");
check(n_overlap === 32'(m_ov), "n_overlap disagrees");
check(n_iso_hs === 32'(m_ih), "n_iso_hs disagrees");
check(n_violations === n_orphan_data + n_orphan_hs + n_direction +
n_setup_data + n_timeout + n_overlap + n_iso_hs,
"the violation causes do not sum to the total -- a violation was mis-classified");
check(n_seen == 320, "not every state was crossed with every PID, direction and endpoint kind");
check(n_orphan_data > 32'd0, "orphan data was never seen");
check(n_orphan_hs > 32'd0, "an orphan handshake was never seen");
check(n_direction > 32'd0, "a direction violation was never seen");
check(n_setup_data > 32'd0, "a SETUP carrying DATA1 was never seen");
check(n_timeout > 32'd0, "the timeout was never exercised");
check(n_overlap > 32'd0, "an overlapping token was never seen");
check(n_iso_hs > 32'd0, "a handshake on an isochronous endpoint was never seen");
check(n_xacts > 32'd2000, "too few legal transactions to have tested for false positives");
$display("REACH state-x-pid-x-dir-x-iso=%0d/320 steps=%0d", n_seen, n_steps);
$display("COUNTERS xacts=%0d violations=%0d orphan-data=%0d orphan-hs=%0d dir=%0d setup=%0d timeout=%0d overlap=%0d iso-hs=%0d",
n_xacts, n_violations, n_orphan_data, n_orphan_hs, n_direction,
n_setup_data, n_timeout, n_overlap, n_iso_hs);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule11.3 VHDL testbench
-- Testbench for usb_protocol_checker (VHDL-2008).
--
-- VERIFYING A CHECKER IS NOT LIKE VERIFYING A DESIGN
--
-- A design has one failure mode that matters: doing the wrong thing. A
-- checker has TWO, and they pull in opposite directions.
--
-- FALSE NEGATIVE it misses a real violation. The checker is useless.
-- FALSE POSITIVE it flags legal traffic. The checker is SWITCHED
-- OFF, and then it is worse
-- than useless, because
-- everyone believes the bus
-- was checked.
--
-- The second is the one that kills checkers in practice, and it is the one
-- a violation-injection suite never tests. So this suite spends most of its
-- cycles doing something that looks pointless: driving THOUSANDS OF PERFECTLY
-- LEGAL TRANSACTIONS of every shape the protocol allows -- SOF, OUT, SETUP,
-- IN-with-data, IN-with-NAK, PING, isochronous OUT, isochronous IN -- and
-- demanding ZERO violations.
--
-- WHAT IS EXHAUSTIVE HERE
--
-- Every checker state x every one of the 16 PIDs x both directions x
-- isochronous and not = 320 combinations, each reached by real packets.
--
-- AND THE PROPERTY THAT IS NOT A SWEEP
--
-- The timeout. No packet sequence can express "and then nothing happened",
-- so it is driven directly: open a transaction, go quiet, and require the
-- checker to notice.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_chk_pkg.all;
entity tb_pc_vhdl is
end entity tb_pc_vhdl;
architecture sim of tb_pc_vhdl is
constant TO_MAX : integer := 12;
constant P_OUT : std_logic_vector(3 downto 0) := "0001";
constant P_IN : std_logic_vector(3 downto 0) := "1001";
constant P_SOF : std_logic_vector(3 downto 0) := "0101";
constant P_SETUP : std_logic_vector(3 downto 0) := "1101";
constant P_DATA0 : std_logic_vector(3 downto 0) := "0011";
constant P_DATA1 : std_logic_vector(3 downto 0) := "1011";
constant P_ACK : std_logic_vector(3 downto 0) := "0010";
constant P_NAK : std_logic_vector(3 downto 0) := "1010";
constant P_STALL : std_logic_vector(3 downto 0) := "1110";
constant P_NYET : std_logic_vector(3 downto 0) := "0110";
constant P_PING : std_logic_vector(3 downto 0) := "0100";
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal pkt_valid : std_logic := '0';
signal from_host : std_logic := '1';
signal ep_iso : std_logic := '0';
signal bus_idle : std_logic := '1';
signal pkt_pid : std_logic_vector(3 downto 0) := (others => '0');
signal state : std_logic_vector(2 downto 0);
signal vio_code : std_logic_vector(3 downto 0);
signal open_token : std_logic_vector(3 downto 0);
signal wait_age : std_logic_vector(4 downto 0);
signal violation, xact_done : std_logic;
signal n_xacts, n_violations, n_orphan_data, n_orphan_hs : std_logic_vector(31 downto 0);
signal n_direction, n_setup_data, n_timeout, n_overlap, n_iso_hs : std_logic_vector(31 downto 0);
begin
dut : entity work.usb_protocol_checker
generic map (TO_MAX => TO_MAX)
port map (
clk => clk, rst_n => rst_n,
pkt_valid => pkt_valid, pkt_pid => pkt_pid, from_host => from_host,
ep_iso => ep_iso, bus_idle => bus_idle,
state => state, violation => violation, vio_code => vio_code,
xact_done => xact_done, open_token => open_token, wait_age => wait_age,
n_xacts => n_xacts, n_violations => n_violations,
n_orphan_data => n_orphan_data, n_orphan_hs => n_orphan_hs,
n_direction => n_direction, n_setup_data => n_setup_data,
n_timeout => n_timeout, n_overlap => n_overlap, n_iso_hs => n_iso_hs
);
clk <= (not clk) after 5 ns when not done else '0';
stim : process
type seen_arr is array (0 to 319) of integer;
variable errors, checks : integer := 0;
-- ---- The shadow checker. Written from the protocol, not the design. ----
variable m_st : chk_state_t := C_IDLE;
variable m_vio : vio_t := V_NONE;
variable m_tok : std_logic_vector(3 downto 0) := (others => '0');
variable m_age : unsigned(4 downto 0) := (others => '0');
variable m_vp, m_dn : std_logic := '0';
variable m_x, m_v, m_od, m_oh : integer := 0;
variable m_dir, m_sd, m_to, m_ov, m_ih : 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"5CA1AB1E";
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_nib return std_logic_vector is
variable u : unsigned(31 downto 0);
begin
u := rnd32;
return std_logic_vector(u(3 downto 0));
end function;
impure function rnd_lt (pct : integer) return std_logic is
begin
if (rnd_nat mod 100) < pct then return '1'; else return '0'; end if;
end function;
procedure chk (cond : boolean; msg : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 25 then
report "FAIL: " & msg &
" | st=" & integer'image(chk_state_t'pos(m_st)) &
" vio=" & integer'image(vio_t'pos(m_vio)) &
" age=" & integer'image(to_integer(m_age))
severity note;
end if;
end if;
end procedure;
procedure step (pv : std_logic; pid : std_logic_vector(3 downto 0);
fh, iso, bi : std_logic) is
variable nst : chk_state_t;
variable nv : vio_t;
variable nt : std_logic_vector(3 downto 0);
variable na : unsigned(4 downto 0);
variable vp, dn : std_logic;
variable tok, dat, hs, png, sof, inn, d0 : boolean;
variable idx : integer;
begin
pkt_valid <= pv; pkt_pid <= pid; from_host <= fh;
ep_iso <= iso; bus_idle <= bi;
wait for 1 ns;
chk(state = cs_code(m_st), "state disagrees with the shadow checker");
chk(open_token = m_tok, "open_token disagrees");
chk(wait_age = std_logic_vector(m_age),
"wait_age disagrees -- the timeout does not run the way the model says");
chk(vio_code = vi_code(m_vio), "vio_code disagrees");
chk(violation = m_vp, "the violation pulse disagrees");
chk(xact_done = m_dn, "xact_done disagrees");
chk(not (violation = '1' and xact_done = '1'),
"a transaction was reported complete and illegal in the same cycle");
chk(unsigned(wait_age) <= to_unsigned(TO_MAX, 5),
"the timeout counter ran past its bound");
chk(not (m_st = C_IDLE and m_age /= 0),
"the timeout counter is running with no transaction open");
if pv = '1' then
idx := chk_state_t'pos(m_st) * 64 + to_integer(unsigned(pid)) * 4;
if fh = '1' then idx := idx + 2; end if;
if iso = '1' then idx := idx + 1; end if;
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 checker ----
nst := m_st; nt := m_tok; nv := m_vio; na := m_age;
vp := '0'; dn := '0';
tok := pid(1 downto 0) = "01";
dat := pid(1 downto 0) = "11";
hs := pid(1 downto 0) = "10";
png := pid = P_PING;
sof := pid = P_SOF;
inn := pid = P_IN;
d0 := pid = P_DATA0;
if pv = '1' then
if tok and fh = '0' then
nst := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif tok then
if m_st /= C_IDLE then nv := V_OVERLAP; vp := '1'; end if;
nt := pid; na := (others => '0');
if sof then nst := C_IDLE; dn := '1';
elsif inn then nst := C_IN;
else nst := C_OUT;
end if;
elsif png then
if fh = '0' then
nst := C_IDLE; nv := V_DIRECTION; vp := '1';
else
if m_st /= C_IDLE then nv := V_OVERLAP; vp := '1'; end if;
nt := pid; na := (others => '0'); nst := C_PING;
end if;
elsif dat then
if m_st = C_OUT then
if fh = '0' then nst := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif m_tok = P_SETUP and not d0 then
nst := C_IDLE; nv := V_SETUP_DATA; vp := '1';
elsif iso = '1' then nst := C_IDLE; dn := '1';
else nst := C_HS; na := (others => '0');
end if;
elsif m_st = C_IN then
if fh = '1' then nst := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif iso = '1' then nst := C_IDLE; dn := '1';
else nst := C_HS; na := (others => '0');
end if;
else
nst := C_IDLE; nv := V_ORPHAN_DATA; vp := '1';
end if;
elsif hs then
if m_st = C_IN then
if fh = '1' then nst := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif iso = '1' then nst := C_IDLE; nv := V_ISO_HS; vp := '1';
else nst := C_IDLE; dn := '1';
end if;
elsif m_st = C_PING then
if fh = '1' then nst := C_IDLE; nv := V_DIRECTION; vp := '1';
else nst := C_IDLE; dn := '1';
end if;
elsif m_st = C_HS then
if (fh = '1') /= (m_tok = P_IN) then
nst := C_IDLE; nv := V_DIRECTION; vp := '1';
elsif iso = '1' then nst := C_IDLE; nv := V_ISO_HS; vp := '1';
else nst := C_IDLE; dn := '1';
end if;
else
nst := C_IDLE; nv := V_ORPHAN_HS; vp := '1';
end if;
end if;
elsif m_st /= C_IDLE then
if m_age >= to_unsigned(TO_MAX - 1, 5) then
nst := C_IDLE; nv := V_TIMEOUT; vp := '1'; na := (others => '0');
elsif bi = '1' then
na := m_age + 1;
end if;
end if;
-- The timer belongs to an open transaction and to nothing else.
if nst = C_IDLE then na := (others => '0'); end if;
-- And a cycle that produced a violation never also reports a
-- completed transaction -- n_xacts is a denominator.
if vp = '1' then dn := '0'; end if;
m_st := nst; m_tok := nt; m_vio := nv; m_age := na;
m_vp := vp; m_dn := dn;
if dn = '1' then m_x := m_x + 1; end if;
if vp = '1' then
m_v := m_v + 1;
case nv is
when V_ORPHAN_DATA => m_od := m_od + 1;
when V_ORPHAN_HS => m_oh := m_oh + 1;
when V_DIRECTION => m_dir := m_dir + 1;
when V_SETUP_DATA => m_sd := m_sd + 1;
when V_TIMEOUT => m_to := m_to + 1;
when V_OVERLAP => m_ov := m_ov + 1;
when V_ISO_HS => m_ih := m_ih + 1;
when others => null;
end case;
end if;
wait until rising_edge(clk);
wait for 1 ns;
pkt_valid <= '0';
end procedure;
procedure pkt (pid : std_logic_vector(3 downto 0); fh, iso : std_logic) is
begin
step('1', pid, fh, iso, '0');
end procedure;
procedure quiet (n : integer; iso : std_logic) is
begin
for i in 1 to n loop
step('0', "0000", '1', iso, '1');
end loop;
end procedure;
-- Put the checker back in IDLE the way the protocol allows -- by letting
-- the transaction time out -- never by forcing the state.
procedure settle (iso : std_logic) is
begin
quiet(TO_MAX + 2, iso);
chk(state = cs_code(C_IDLE),
"the checker did not return to IDLE after a quiet bus");
end procedure;
variable before_v, before_x : integer := 0;
-- ----------------------------------------------------------------
-- The eight LEGAL transaction shapes. Each must complete with exactly
-- one xact_done and ZERO violations.
-- ----------------------------------------------------------------
procedure legal (shape : integer; dpid, hpid : std_logic_vector(3 downto 0)) is
begin
before_v := to_integer(unsigned(n_violations));
before_x := to_integer(unsigned(n_xacts));
case shape is
when 0 => pkt(P_SOF, '1', '0'); -- SOF
when 1 => pkt(P_OUT, '1', '0'); pkt(dpid, '1', '0');
pkt(hpid, '0', '0'); -- OUT
when 2 => pkt(P_SETUP, '1', '0'); pkt(P_DATA0, '1', '0');
pkt(P_ACK, '0', '0'); -- SETUP
when 3 => pkt(P_IN, '1', '0'); pkt(dpid, '0', '0');
pkt(P_ACK, '1', '0'); -- IN+data
when 4 => pkt(P_IN, '1', '0'); pkt(hpid, '0', '0'); -- IN+NAK
when 5 => pkt(P_PING, '1', '0'); pkt(hpid, '0', '0'); -- PING
when 6 => pkt(P_OUT, '1', '1'); pkt(dpid, '1', '1'); -- iso OUT
when others => pkt(P_IN, '1', '1'); pkt(dpid, '0', '1'); -- iso IN
end case;
chk(to_integer(unsigned(n_violations)) = before_v,
"a PERFECTLY LEGAL transaction was flagged as a violation -- a checker with false positives gets switched off, and then nobody is checking anything");
chk(to_integer(unsigned(n_xacts)) = before_x + 1,
"a legal transaction did not complete exactly once");
chk(state = cs_code(C_IDLE),
"the checker did not return to IDLE after a legal transaction");
end procedure;
variable dsel, hsel : std_logic_vector(3 downto 0);
variable iso_v, fh_v : std_logic;
variable ln : line;
begin
wait for 33 ns;
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
-- ---- Phase A: the state after reset ----
chk(state = cs_code(C_IDLE), "reset did not land in IDLE");
chk(violation = '0', "reset asserted a violation");
chk(unsigned(n_violations) = 0, "reset left the violation counter non-zero");
-- ---- Phase B: NO FALSE POSITIVES. Every legal shape, many times, with
-- ---- every data PID and every handshake PID the shape permits.
for k in 0 to 219 loop
if (k mod 2) = 1 then dsel := P_DATA1; else dsel := P_DATA0; end if;
case k mod 4 is
when 0 => hsel := P_ACK;
when 1 => hsel := P_NAK;
when 2 => hsel := P_STALL;
when others => hsel := P_NYET;
end case;
legal(0, dsel, hsel);
legal(1, dsel, hsel);
legal(2, dsel, hsel);
legal(3, dsel, P_ACK);
legal(4, dsel, hsel);
if (k mod 2) = 1 then legal(5, dsel, P_NAK); else legal(5, dsel, P_ACK); end if;
legal(6, dsel, hsel);
legal(7, dsel, hsel);
end loop;
-- ---- Phase C: EXHAUSTIVE. Every state x PID x direction x iso. ----
for s in 0 to 4 loop
for p in 0 to 15 loop
for f in 0 to 1 loop
for o in 0 to 1 loop
if o = 1 then iso_v := '1'; else iso_v := '0'; end if;
if f = 1 then fh_v := '1'; else fh_v := '0'; end if;
settle(iso_v);
-- reach the state with real packets, never by forcing
case s is
when 0 => null;
when 1 => pkt(P_OUT, '1', iso_v);
when 2 => pkt(P_IN, '1', iso_v);
when 3 => pkt(P_OUT, '1', '0'); pkt(P_DATA0, '1', '0');
when others => pkt(P_PING, '1', iso_v);
end case;
chk(state = cs_code(chk_state_t'val(s)),
"the sweep could not reach the state it meant to reach");
step('1', std_logic_vector(to_unsigned(p, 4)), fh_v, iso_v, '0');
step('1', std_logic_vector(to_unsigned(p, 4)), fh_v, iso_v, '0');
end loop;
end loop;
end loop;
end loop;
-- ---- Phase D: THE TIMEOUT. No packet sequence can express "and then
-- ---- nothing happened", so it is driven directly.
for k in 0 to 3 loop
before_v := to_integer(unsigned(n_timeout));
case k is
when 0 => pkt(P_OUT, '1', '0');
when 1 => pkt(P_IN, '1', '0');
when 2 => pkt(P_PING, '1', '0');
when others => pkt(P_OUT, '1', '0'); pkt(P_DATA0, '1', '0');
end case;
-- one cycle short of the bound: still waiting, patiently
quiet(TO_MAX - 1, '0');
chk(to_integer(unsigned(n_timeout)) = before_v,
"the checker gave up before its own timeout bound");
chk(state /= cs_code(C_IDLE),
"the checker abandoned the transaction early");
quiet(3, '0');
chk(to_integer(unsigned(n_timeout)) = before_v + 1,
"the checker did not notice a transaction that never completed -- without this it is blind to the most common way real hardware fails");
chk(state = cs_code(C_IDLE),
"the checker did not resynchronise after a timeout");
end loop;
-- ---- A busy bus must NOT time out. The counter advances on dead air
-- ---- only, so a transaction interleaved with other traffic is not
-- ---- abandoned merely for taking a while.
settle('0');
pkt(P_IN, '1', '0');
before_v := to_integer(unsigned(n_timeout));
for k in 1 to 3 * TO_MAX loop
step('0', "0000", '1', '0', '0');
end loop;
chk(to_integer(unsigned(n_timeout)) = before_v,
"the checker timed out a transaction while the bus was busy -- the counter must measure dead air, not elapsed time");
pkt(P_DATA0, '0', '0');
pkt(P_ACK, '1', '0');
-- ---- Phase E: every violation kind, deliberately, one at a time ----
settle('0'); pkt(P_DATA0, '1', '0'); -- orphan data
settle('0'); pkt(P_ACK, '0', '0'); -- orphan hs
settle('0'); pkt(P_OUT, '0', '0'); -- direction
settle('0'); pkt(P_SETUP, '1', '0'); pkt(P_DATA1, '1', '0'); -- setup/DATA1
settle('0'); pkt(P_OUT, '1', '0'); pkt(P_IN, '1', '0'); -- overlap
settle('0'); pkt(P_IN, '1', '1'); pkt(P_NAK, '0', '1'); -- iso hs
-- ---- Phase F: random traffic, mostly legal, with corruption injected ----
for i in 0 to 29999 loop
step(rnd_lt(62), rnd_nib, rnd_lt(55), rnd_lt(25), rnd_lt(70));
end loop;
-- ---- Phase G: and legal traffic again AFTERWARDS, so the checker is
-- ---- shown to still work rather than merely to have stopped.
settle('0');
for k in 0 to 119 loop
legal(1, P_DATA0, P_ACK);
legal(3, P_DATA1, P_ACK);
legal(7, P_DATA0, P_ACK);
end loop;
-- ---- Final agreement ----
chk(to_integer(unsigned(n_xacts)) = m_x, "n_xacts disagrees with the model");
chk(to_integer(unsigned(n_violations)) = m_v, "n_violations disagrees with the model");
chk(to_integer(unsigned(n_orphan_data)) = m_od, "n_orphan_data disagrees");
chk(to_integer(unsigned(n_orphan_hs)) = m_oh, "n_orphan_hs disagrees");
chk(to_integer(unsigned(n_direction)) = m_dir, "n_direction disagrees");
chk(to_integer(unsigned(n_setup_data)) = m_sd, "n_setup_data disagrees");
chk(to_integer(unsigned(n_timeout)) = m_to, "n_timeout disagrees");
chk(to_integer(unsigned(n_overlap)) = m_ov, "n_overlap disagrees");
chk(to_integer(unsigned(n_iso_hs)) = m_ih, "n_iso_hs disagrees");
chk(to_integer(unsigned(n_violations)) =
to_integer(unsigned(n_orphan_data)) + to_integer(unsigned(n_orphan_hs)) +
to_integer(unsigned(n_direction)) + to_integer(unsigned(n_setup_data)) +
to_integer(unsigned(n_timeout)) + to_integer(unsigned(n_overlap)) +
to_integer(unsigned(n_iso_hs)),
"the violation causes do not sum to the total -- a violation was mis-classified");
chk(n_seen = 320, "not every state was crossed with every PID, direction and endpoint kind");
chk(to_integer(unsigned(n_orphan_data)) > 0, "orphan data was never seen");
chk(to_integer(unsigned(n_orphan_hs)) > 0, "an orphan handshake was never seen");
chk(to_integer(unsigned(n_direction)) > 0, "a direction violation was never seen");
chk(to_integer(unsigned(n_setup_data)) > 0, "a SETUP carrying DATA1 was never seen");
chk(to_integer(unsigned(n_timeout)) > 0, "the timeout was never exercised");
chk(to_integer(unsigned(n_overlap)) > 0, "an overlapping token was never seen");
chk(to_integer(unsigned(n_iso_hs)) > 0, "a handshake on an isochronous endpoint was never seen");
chk(to_integer(unsigned(n_xacts)) > 2000,
"too few legal transactions to have tested for false positives");
write(ln, string'("REACH state-x-pid-x-dir-x-iso=") & integer'image(n_seen) &
"/320 steps=" & integer'image(n_steps));
writeline(output, ln);
write(ln, string'("COUNTERS xacts=") & integer'image(to_integer(unsigned(n_xacts))) &
" violations=" & integer'image(to_integer(unsigned(n_violations))) &
" orphan-data=" & integer'image(to_integer(unsigned(n_orphan_data))) &
" orphan-hs=" & integer'image(to_integer(unsigned(n_orphan_hs))) &
" dir=" & integer'image(to_integer(unsigned(n_direction))) &
" setup=" & integer'image(to_integer(unsigned(n_setup_data))) &
" timeout=" & integer'image(to_integer(unsigned(n_timeout))) &
" overlap=" & integer'image(to_integer(unsigned(n_overlap))) &
" iso-hs=" & integer'image(to_integer(unsigned(n_iso_hs))));
writeline(output, ln);
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks) & " checks");
else
write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
integer'image(checks) & " checks");
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture sim;12. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| state x PID x direction x iso | 320 / 320 | 320 / 320 | 320 / 320 |
| Steps | 40581 | 40581 | 40581 |
| Checks executed | 372276 | 372276 | 372276 |
| legal transactions completed | 2878 | 2882 | 2883 |
| false positives on them | 0 | 0 | 0 |
| violations reported | 12612 | 12716 | 12617 |
| — orphan data | 4082 | 4165 | 4168 |
| — orphan handshake | 4346 | 4328 | 4184 |
| — wrong direction | 3298 | 3385 | 3383 |
| — SETUP not DATA0 | 88 | 70 | 78 |
| — timeout | 94 | 93 | 93 |
| — overlapping token | 676 | 633 | 674 |
| — handshake on isochronous | 28 | 42 | 37 |
| Result | PASS | PASS | PASS |
Two rows carry the chapter. 320 of 320 means there is no packet, from either side, on either kind of endpoint, in any checker state, that the suite has not applied. 2878 legal transactions with zero false positives is the other half, and it is the half that decides whether anybody leaves the checker switched on.
The seven cause rows sum to the violation total in all three columns, checked programmatically.
13. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| K2 | a token is accepted from either side | 25629 | 26026 | 25961 |
| K5 | data with no token in front of it is ignored | 15722 | 16710 | 15895 |
| K7 | the isochronous exemption is dropped — FALSE POSITIVES | 9067 | 9145 | 9024 |
| K4 | the SETUP rule is inverted (DATA1 required) | 6134 | 6014 | 6132 |
| K1 | the timeout is removed | 3022 | 2048 | 2048 |
| K6 | an overlapping token is accepted silently | 2881 | 2768 | 2877 |
| K3 | the DATA direction is not checked | 1595 | 1641 | 1552 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct.
K7 is the false-positive mutation, and it is the one the §11 legal-traffic phase exists for. It misses nothing at all — it invents a timeout on every isochronous transfer. A suite built only from violation injection would score it zero and conclude the checker was fine.
K3 is the smallest at ~1600, which is worth noticing: not checking the direction of a DATA packet is a serious hole, and it scores low simply because a direction error has to be injected to be seen — random traffic drives the correct side most of the time by construction, since the suite's legal phases do.
14. Debugging Walkthrough: The Checker That Was Switched Off
The report. A verification team has a USB protocol checker. It is disabled in every regression except one, with a comment reading # too noisy. A silicon bug then escapes that the checker would have caught.
Step 1 — why was it disabled? Turn it on. 40 000 violations in a ten-microsecond simulation.
Step 2 — what kind? They are not distributed across the seven causes. 97% are timeouts.
Step 3 — on what? Every one of them is on an isochronous endpoint. The audio stream.
Step 4 — so the checker is waiting for a handshake. An isochronous transaction has none, so the checker waits, times out, reports, and does it again on the next packet — 8000 times a second per endpoint.
Step 5 — one wrong assumption, made once. Whoever wrote the transaction rules wrote them from the control-transfer case and never revisited them for isochronous. The checker was correct about six of its seven rules.
Step 6 — but that is not the interesting part. The interesting part is what happened next: the team did not fix it. They disabled the checker, and then ran eighteen months of regressions with a comment in a Makefile standing in for protocol checking. The escape was not caused by the missing isochronous rule. It was caused by the checker being off.
15. UVM and Assertions
15.1 Where a checker lives in a UVM environment
// A protocol checker is NOT a scoreboard and does not belong in one.
//
// the checker says "that packet sequence is not legal"
// -- a statement about the BUS, needing no knowledge
// of what the test intended
//
// the scoreboard says "that is not the data I sent"
// -- a statement about the TRANSFER, needing the
// stimulus to compare against
//
// Keeping them apart matters because the checker must be enabled in EVERY
// test, including tests that deliberately break the data path, and a
// scoreboard cannot be. Fold them together and the checker inherits the
// scoreboard's disable switch -- see the walkthrough in section 14.
class usb_protocol_checker_c extends uvm_component;
`uvm_component_utils(usb_protocol_checker_c)
uvm_analysis_imp #(usb_bus_pkt, usb_protocol_checker_c) ap;
// The checker's OWN state. Never read from the DUT, never from the
// sequencer, never from a config object the test can edit mid-run.
typedef enum { C_IDLE, C_OUT, C_IN, C_HS, C_PING } chk_state_e;
chk_state_e st;
bit [3:0] open_tok;
int unsigned quiet_cycles;
int unsigned n_xacts;
int unsigned n_vio[string];
// The timeout in bus cycles. A checker's bound must be configurable --
// it is the one number that depends on the platform rather than on the
// protocol -- but it must NOT be settable to zero or to infinity, which
// are the two values that quietly disable it.
int unsigned to_max = 12;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db #(int unsigned)::get(this, "", "to_max", to_max));
if (to_max < 2 || to_max > 1024)
`uvm_fatal("CFG",
$sformatf("to_max=%0d is outside the range a bus timeout can sensibly take; 0 and very large values disable the only liveness check this component has",
to_max))
endfunction
function void flag(string cause, string detail);
n_vio[cause]++;
`uvm_error("PROTO", $sformatf("%s: %s", cause, detail))
// ---- Resynchronise. The checker has just admitted it does not know
// ---- where it is in the stream; carrying on from a guessed position
// ---- produces one violation per packet for the rest of the run.
st = C_IDLE;
quiet_cycles = 0;
endfunction
function void write(usb_bus_pkt p);
// ---- The liveness rule, first, because it is the one that fires when
// ---- nothing else can.
if (!p.valid) begin
if (st != C_IDLE && p.bus_idle) begin
quiet_cycles++;
if (quiet_cycles >= to_max)
flag("TIMEOUT",
$sformatf("no response for %0d idle cycles after a %s token -- a transaction that never ends violates no ordering rule, and this is the only check that sees it",
quiet_cycles, p.pid_name(open_tok)));
end
return;
end
// ---- A token always comes from the host. ----
if (p.is_token() && !p.from_host) begin
flag("DIRECTION",
"a device drove a token -- the bus now has two masters and nothing after this point is meaningful");
return;
end
case (st)
C_IDLE: begin
if (p.is_data()) flag("ORPHAN_DATA", "DATA with no token in front of it");
else if (p.is_hs()) flag("ORPHAN_HS", "a handshake with no transaction open");
else if (p.is_sof()) n_xacts++; // complete in itself
else if (p.is_in()) begin st = C_IN; open_tok = p.pid; quiet_cycles = 0; end
else if (p.is_ping()) begin st = C_PING; open_tok = p.pid; quiet_cycles = 0; end
else if (p.is_token())begin st = C_OUT; open_tok = p.pid; quiet_cycles = 0; end
end
C_OUT: begin
if (p.is_token()) flag("OVERLAP",
"a token arrived while a transaction was still open -- the previous one will never complete, and not saying so hides a lost response for ever");
else if (p.is_hs()) flag("ORPHAN_HS", "a handshake where host data was expected");
else if (!p.from_host) flag("DIRECTION", "the device drove the data of an OUT");
else if (open_tok == 4'b1101 && !p.is_data0())
flag("SETUP_DATA",
"a SETUP carrying DATA1 -- every stage of the control transfer that follows is now toggle-desynchronised");
else if (p.ep_iso) begin st = C_IDLE; n_xacts++; end
else begin st = C_HS; quiet_cycles = 0; end
end
C_IN: begin
if (p.is_token()) flag("OVERLAP", "a token arrived while an IN was still open");
else if (p.from_host) flag("DIRECTION", "the host drove the data of an IN");
else if (p.is_hs() && p.ep_iso)
flag("ISO_HS", "a handshake on an isochronous endpoint");
else if (p.is_hs()) begin st = C_IDLE; n_xacts++; end // NAK/STALL: LEGAL
else if (p.ep_iso) begin st = C_IDLE; n_xacts++; end
else begin st = C_HS; quiet_cycles = 0; end
end
C_HS: begin
if (p.is_token()) flag("OVERLAP", "a token arrived while a handshake was awaited");
else if (!p.is_hs()) flag("ORPHAN_DATA", "more data where a handshake was expected");
else if (p.from_host != (open_tok == 4'b1001))
flag("DIRECTION", "the side that sent the data also acknowledged it");
else if (p.ep_iso) flag("ISO_HS", "a handshake on an isochronous endpoint");
else begin st = C_IDLE; n_xacts++; end
end
C_PING: begin
if (p.is_token()) flag("OVERLAP", "a token arrived while a PING was open");
else if (!p.is_hs()) flag("ORPHAN_DATA", "a PING answered with data");
else if (p.from_host) flag("DIRECTION", "the host answered its own PING");
else begin st = C_IDLE; n_xacts++; end
end
endcase
endfunction
function void report_phase(uvm_phase phase);
int unsigned total = 0;
foreach (n_vio[c]) total += n_vio[c];
`uvm_info("CHK", $sformatf("transactions=%0d violations=%0d %p",
n_xacts, total, n_vio), UVM_LOW)
// ---- A checker that saw no traffic is not a passing checker. ----
if (n_xacts == 0)
`uvm_error("COVERAGE",
"the checker completed no transactions at all -- it was either not connected or the test drove nothing, and either way its silence means nothing")
endfunction
endclass15.2 Assertions
// Assertions are the right tool for the ordering rules, and NOT the right
// tool for the timeout -- see the note after this module.
module usb_protocol_checker_sva
import usb_chk_pkg::*;
#(
parameter int TO_MAX = 12
) (
input logic clk,
input logic rst_n,
input logic pkt_valid,
input logic [3:0] pkt_pid,
input logic from_host,
input logic ep_iso,
input logic bus_idle,
input chk_state_e state,
input logic violation,
input vio_e vio_code,
input logic xact_done,
input logic [3:0] open_token,
input logic [4:0] wait_age
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
function automatic bit is_token(logic [3:0] p); return p[1:0] == 2'b01; endfunction
function automatic bit is_data (logic [3:0] p); return p[1:0] == 2'b11; endfunction
function automatic bit is_hs (logic [3:0] p); return p[1:0] == 2'b10; endfunction
// ---- 1. A token always comes from the host. ----
property p_token_from_host;
(pkt_valid && is_token(pkt_pid) && !from_host)
|=> (violation && vio_code == V_DIRECTION);
endproperty
a_token_from_host : assert property (p_token_from_host)
else $error("a device drove a token and the checker did not object");
// ---- 2. DATA in IDLE is orphaned. ----
property p_orphan_data;
(state == C_IDLE && pkt_valid && is_data(pkt_pid))
|=> (violation && vio_code == V_ORPHAN_DATA);
endproperty
a_orphan_data : assert property (p_orphan_data);
// ---- 3. A handshake in IDLE is orphaned. ----
property p_orphan_hs;
(state == C_IDLE && pkt_valid && is_hs(pkt_pid))
|=> (violation && vio_code == V_ORPHAN_HS);
endproperty
a_orphan_hs : assert property (p_orphan_hs);
// ---- 4. A SETUP is always followed by DATA0. ----
property p_setup_is_data0;
(state == C_OUT && open_token == 4'b1101 && pkt_valid
&& is_data(pkt_pid) && from_host && pkt_pid != 4'b0011)
|=> (violation && vio_code == V_SETUP_DATA);
endproperty
a_setup_is_data0 : assert property (p_setup_is_data0)
else $error("a SETUP carrying DATA1 was accepted");
// ---- 5. An isochronous transaction has NO handshake. And the converse:
// ---- a NAK on a NON-isochronous IN is perfectly legal and must NOT
// ---- be flagged. Both directions, because only checking one of them
// ---- is how a checker acquires a false positive.
property p_iso_has_no_handshake;
(state == C_IN && ep_iso && pkt_valid && is_hs(pkt_pid) && !from_host)
|=> (violation && vio_code == V_ISO_HS);
endproperty
a_iso_has_no_handshake : assert property (p_iso_has_no_handshake);
property p_nak_on_in_is_legal;
(state == C_IN && !ep_iso && pkt_valid && is_hs(pkt_pid) && !from_host)
|=> (xact_done && !violation);
endproperty
a_nak_on_in_is_legal : assert property (p_nak_on_in_is_legal)
else $error("a NAK on an IN was flagged -- that is flow control working, and flagging it makes the checker unusable on a busy bus");
// ---- 6. A SOF is a complete transaction all by itself. ----
property p_sof_completes;
(state == C_IDLE && pkt_valid && pkt_pid == 4'b0101 && from_host)
|=> (xact_done && !violation);
endproperty
a_sof_completes : assert property (p_sof_completes)
else $error("a SOF was not treated as a complete transaction -- this fires 8000 times a second at high speed");
// ---- 7. Never both. n_xacts is a denominator. ----
a_not_both : assert property (!(violation && xact_done))
else $error("a cycle reported a completion and a violation, inflating the denominator of the error rate");
// ---- 8. The timer belongs to an open transaction and to nothing else. ----
a_timer_scoped : assert property ((state == C_IDLE) |-> (wait_age == '0))
else $error("the timeout counter is running with no transaction open -- the next transaction will have its patience shortened by this one");
a_timer_bounded : assert property (wait_age <= 5'(TO_MAX));
// ---- 9. Every violation returns the checker to IDLE. ----
a_resync : assert property (violation |-> (state == C_IDLE))
else $error("a violation left the checker mid-transaction, from which it cannot recover its position");
// ---- Cover: every cause, and the two legal shapes most often flagged. ----
c_timeout : cover property ((violation && vio_code == V_TIMEOUT));
c_overlap : cover property ((violation && vio_code == V_OVERLAP));
c_iso_hs : cover property ((violation && vio_code == V_ISO_HS));
c_legal_nak : cover property ((state == C_IN && pkt_valid
&& is_hs(pkt_pid) && !from_host
&& !ep_iso ##1 xact_done));
c_legal_iso : cover property ((ep_iso && xact_done));
endmodule
bind usb_protocol_checker
usb_protocol_checker_sva #(.TO_MAX(TO_MAX)) u_sva (.*);16. Common Misconceptions
"A checker can read the DUT's state to simplify itself." Then it cannot catch the DUT being wrong about its own state, which is most of what goes wrong.
"Every token opens a three-part transaction." A SOF is complete on arrival — 8000 false violations a second at high speed.
"A NAK means something failed." A NAK is flow control succeeding. Flagging it makes the checker unusable on any busy bus.
"Isochronous endpoints just don't retry." They also do not acknowledge. Demanding a handshake flags every audio and video transfer on the device.
"PID rules are enough." Half the rules are about who drove it. A device talking out of turn violates no PID rule and destroys the bus.
"Ordering rules are enough." None of them fires when nothing happens, and stopping is how real hardware usually fails.
"The timeout can just count cycles." It must count dead air. Counting elapsed time flags a transaction for being interleaved with other traffic.
"A noisy checker is a quality-of-life problem." It is a reliability problem. A checker that gets switched off has a false-negative rate of 100%.
"Violations per transaction is a safe metric." Only if a violation cycle cannot also increment the denominator.
17. Exercises
1. K1 removes the timeout and scores 2048 with the random phase deleted entirely. Explain that from the probability of twelve consecutive quiet cycles, and say what it implies about trusting a coverage report that shows the timeout branch hit.
2. K7 invents violations rather than missing them. Work out what it would score against a suite built only from violation injection, and then write the smallest legal-traffic test that kills it.
3. The timer is cleared whenever the checker returns to IDLE. Construct the two-transaction sequence that fails if it is not, and say which transaction reports the violation.
4. xact_done is suppressed on a violation cycle. Find the packet sequence that makes this matter, and compute the violations-per-transaction figure with and without the suppression.
5. Add a rule that a data toggle must alternate within a transfer. Say why it cannot live in this block, and what the checker would need to be given in order to host it.
6. The SystemVerilog port classified every packet as the first one because logic x = expr; is an initialiser. Write the lint rule that catches it, and explain why neither compilation nor the shadow model's first cycle would.
18. Summary
| Idea | Why it matters |
|---|---|
| A checker observes and drives nothing | and never reads the DUT's own state |
| A SOF is a complete transaction | or the checker fires 8000 times a second |
| A NAK on an IN is legal | flow control succeeding, not a failure |
| Isochronous has no handshake | demanding one flags every audio transfer |
| Half the rules need who drove it | a device talking out of turn breaks no PID rule |
| Ordering rules never fire on silence | and stopping is how hardware usually fails |
| The timeout is the only liveness tool | and it must count dead air, not elapsed time |
| A timer must not outlive its transaction | or the next one is timed out for free |
| A violation cycle reports no completion | n_xacts is a denominator |
| False positives get the checker switched off | which is a 100% false-negative rate |
| A liveness bug needs deliberate silence | random stimulus does not produce silence |
logic x = expr; is not wire x = expr; | a silent, clean-compiling translation bug |
| 320 of 320 combinations, 2878 legal transactions, 0 false positives | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o pc_v.out pc_v.v pc_v_tb.v && ./pc_v.out |
| SystemVerilog | iverilog -g2012 -o pc_sv.out pc_sv.sv pc_sv_tb.sv && ./pc_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a pc_vhdl.vhd pc_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_pc_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_pc_vhdl |
| One mutation | iverilog -g2005 -DMUT_K1 -o mm pc_v_mut.v pc_v_tb.v && ./mm |
All three implementations pass with 0 errors: 320 of 320 state-by-PID-by-direction-by-endpoint-kind combinations, 2878 legal transactions of all eight shapes with zero false positives, and both edges of the timeout bound checked.
Chapter 24.2 — USB Assertions takes the other half of the checking problem. This chapter's rules were all safety properties — "this must never happen" — and SVA is excellent at those. The rules that remain are liveness properties, and the honest statement about them is short: eventually is not implementable, not synthesisable, and not checkable in a finite simulation. Every real liveness check is a bounded one, and choosing the bound is the whole job.
Continue learning
Related tutorials
- Related topic
Senior Verification Strategy
The UVM component list is the easy half — the answer that gets hired is a checker that fails when the stimulus was too weak to prove anything, and counts the opportunities to prove it.
- 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.
