USB · Module 23
Protocol FSMs
The packet identifier validates itself — exactly 15 of the 256 possible bytes are legal — and every error resynchronises, because there is no way to find the next packet except by waiting for one to begin.
Chapter 23.3 built the buffer. This chapter builds the machine that decides what the bytes in it mean — and its organising rule is stricter than it first sounds.
1. The First Byte Validates Itself
A USB packet identifier is one byte carrying four bits of information:
bit 7 6 5 4 3 2 1 0
~PID PID
OUT 0001 -> 1110 0001 = 0xE1
IN 1001 -> 0110 1001 = 0x69
DATA0 0011 -> 1100 0011 = 0xC3
ACK 0010 -> 1101 0010 = 0xD2
The top nibble is the BITWISE COMPLEMENT of the bottom.That is not a checksum bolted on afterwards. It is the encoding, and it means the very first byte of every packet validates itself before anything is done with it — before a state is entered, before a counter is set, before a byte of payload is stored anywhere.
Of the 256 possible bytes, exactly 16 satisfy the complement rule, and one of those (PID 0000) is reserved. So exactly 15 bytes in 256 — under 6% — are a legal packet identifier, and a corrupted line hits one by accident about one time in seventeen.
2. Every Error Resynchronises
The second rule is the one that decides whether a receiver survives a corrupt packet or spends the rest of the session out of step.
There is no way to know where the next packet begins except by waiting for one to begin. So on any error the machine stops interpreting the stream entirely, waits for the packet to end, and starts again from nothing.
A machine that tries to be clever — skipping the bad byte, guessing the length, assuming the next byte is a PID — is not more robust. It is a machine that is now reading payload as headers, and it stays wrong until something resets it.
3. Two Kinds of Error, and They Do Not Resynchronise the Same Way
This is the distinction that gets missed, and missing it costs a whole extra packet every time:
| Error detected | The rest of the packet | Where to go |
|---|---|---|
| mid-packet — bad PID, over-long body, PHY failure, a byte with no SYNC | still arriving | S_ABORT: swallow it |
| at the EOP — a packet that ended before it was whole | already gone | S_IDLE: immediately |
Sending a short packet to S_ABORT looks harmless — it is one error either way. But S_ABORT leaves only on an EOP, and the EOP has already been consumed.
4. One Error Per Packet, Not One Per Byte
When a PID fails its check, the bytes behind it are still coming. A machine that returns straight to IDLE sees each of them as a stray byte with no SYNC, and reports an error for every one.
ONE corrupted PID, followed by 40 bytes of payload:
with S_ABORT 1 error
straight to IDLE 41 errors
The log shows forty-one events where one packet was
corrupted. The per-cause counters are useless for
diagnosis, and any firmware that reacts to errors
reacts forty-one times.S_ABORT's entire job is to interpret nothing. It does not decode, it does not count, and above all it does not raise a second error for a packet that is already known to be bad.
5. And Resynchronisation Is Bounded
S_ABORT leaves on the end of the packet — or, if the EOP itself was lost, after ABORT_MAX cycles.
Without that timeout, a single missing EOP wedges the receiver permanently. That is a hang rather than an error, and a hang is the one failure a protocol block must not have: an error is reported, retried, and survived; a hang requires somebody to power-cycle the device.
6. The Machine
usb_packet_fsm — the shape of a recogniser that resynchronises
Three transitions are deliberately left off the picture because they would clutter it, and they are exactly the ones §3 is about:
IDLE --byte with no SYNC--> ABORT the stream is lost
PID --EOP--------------> IDLE already over: short
BODY --EOP, too few-----> IDLE already over: short
HSHK --byte-------------> ABORT a handshake has no body
Note which two go to IDLE and which two go to ABORT.
The rule is not "errors go to ABORT". It is:
is the rest of the packet still coming?7. Verilog-2005 Implementation
// usb_packet_fsm -- the packet recogniser, and the one rule that decides
// whether a USB receiver survives a corrupt packet or spends the rest of the
// session out of step.
//
// THE RULE
//
// EVERY ERROR RESYNCHRONISES.
//
// Not "recovers". Not "skips the bad byte and carries on". There is no way
// to know where the next packet begins except by waiting for one to begin --
// so on any error the machine stops interpreting the stream entirely, waits
// for the end of the packet, and starts again from nothing.
//
// A receiver that tries to be clever -- guessing the length, resuming after
// the bad byte, assuming the next byte is a PID -- is not more robust. It is
// a machine that is now interpreting payload as headers, and it will stay
// wrong until something resets it.
//
// THE PID IS SELF-CHECKING
//
// A USB packet identifier is one byte carrying four bits of information:
//
// bit 7 6 5 4 3 2 1 0
// ~PID PID
//
// The top nibble is the BITWISE COMPLEMENT of the bottom one. That is not a
// checksum bolted on afterwards -- it is the encoding itself, and it means
// the very first byte of every packet validates itself before anything is
// done with it.
//
// Of the 256 possible bytes, exactly 16 satisfy the complement rule, and one
// of those 16 (PID 0000) is reserved. So EXACTLY 15 BYTES IN 256 ARE A LEGAL
// PACKET IDENTIFIER, and the testbench checks all 256 of them.
//
// ONE ERROR PER PACKET, NOT ONE PER BYTE
//
// This is the part that is easy to get wrong and hard to notice. When a PID
// fails its check, the bytes behind it are still coming. A machine that
// returns straight to IDLE will see each of them as a stray byte with no
// SYNC and report an error for every one.
//
// The log then shows forty errors where one packet was corrupted, the error
// counters are useless for diagnosis, and any firmware that reacts to errors
// reacts forty times. The fix is a state whose entire job is to interpret
// NOTHING until the packet is over: S_ABORT.
//
// TWO KINDS OF ERROR, AND THEY DO NOT RESYNCHRONISE THE SAME WAY
//
// This distinction is the one that is missed, and missing it costs a whole
// extra packet every time:
//
// detected MID-PACKET a bad PID, an over-long body, a PHY failure, a
// byte with no SYNC. The rest of the packet is
// still arriving. -> S_ABORT, swallow it
//
// detected AT THE EOP a packet that ended before it was whole. The
// packet is ALREADY OVER. -> S_IDLE, immediately
//
// Sending a short packet to S_ABORT looks harmless -- it is one error
// either way. But S_ABORT leaves only on an EOP, and the EOP has already
// been consumed. So the machine sits in S_ABORT through the whole of the
// NEXT packet, swallows it, and leaves on ITS end-of-packet.
//
// The symptom is that every short packet costs two: the corrupt one and
// the good one behind it. Under a retry, the retry is the packet that gets
// eaten -- so the transfer never completes and nothing in the error log
// says why.
//
// RESYNCHRONISATION IS BOUNDED
//
// S_ABORT leaves on the end of the packet -- or, if the EOP itself was lost,
// after ABORT_MAX cycles. Without the timeout a single missing EOP wedges
// the receiver permanently, which is a hang rather than an error, and a hang
// is the one failure a protocol block must not have.
module usb_packet_fsm #(
parameter integer MAXLEN = 8, // largest payload this recogniser accepts
parameter integer ABORT_MAX = 16 // cycles before an abort gives up waiting
) (
input wire clk,
input wire rst_n,
input wire sync, // a SYNC pattern was detected on the line
input wire byte_valid, // a decoded byte is present
input wire [7:0] byte_data,
input wire eop, // end of packet
input wire bus_error, // PHY-level failure (bit-stuff / NRZI)
output wire [2:0] state,
output wire [3:0] pid,
output wire [1:0] pid_class,
output wire [3:0] exp_min, // bytes this PID requires
output wire [3:0] exp_max, // bytes this PID permits
output wire [3:0] byte_count,
output wire in_packet,
output wire pkt_valid, // ONE pulse per well-formed packet
output wire pkt_error, // ONE pulse per corrupt packet
output wire [2:0] err_code,
output wire [4:0] abort_age,
output reg [31:0] n_packets,
output reg [31:0] n_errors,
output reg [31:0] n_pid_err,
output reg [31:0] n_short,
output reg [31:0] n_long,
output reg [31:0] n_stray,
output reg [31:0] n_bus_err,
output reg [31:0] n_timeouts
);
localparam [2:0] S_IDLE = 3'd0, // nothing is happening; wait for SYNC
S_PID = 3'd1, // SYNC seen; the next byte IS the PID
S_BODY = 3'd2, // collecting a token / data payload
S_HSHK = 3'd3, // a handshake: EOP and nothing else
S_ABORT = 3'd4; // interpret NOTHING until the packet ends
localparam [2:0] E_NONE = 3'd0,
E_PID = 3'd1, // the complement check failed, or reserved
E_SHORT = 3'd2, // EOP arrived before the packet was whole
E_LONG = 3'd3, // more bytes than this PID permits
E_STRAY = 3'd4, // a byte with no SYNC in front of it
E_BUS = 3'd5; // the PHY failed mid-packet
// ---- The length rules, from the PID and nothing else. ----
//
// Returned as {reserved, max, min} so a single lookup answers both "is
// this PID legal at all" and "how long is its body".
function [8:0] len_of;
input [3:0] p;
begin
case (p[1:0])
2'b01: len_of = {1'b0, 4'd2, 4'd2}; // token: ADDR+ENDP+CRC5
2'b11: len_of = {1'b0, MAXLEN[3:0], 4'd2}; // data: payload + CRC16
2'b10: len_of = {1'b0, 4'd0, 4'd0}; // handshake: nothing
default:
case (p)
4'b0100: len_of = {1'b0, 4'd2, 4'd2}; // PING
4'b1000: len_of = {1'b0, 4'd3, 4'd3}; // SPLIT
4'b1100: len_of = {1'b0, 4'd0, 4'd0}; // PRE / ERR
default: len_of = {1'b1, 4'd0, 4'd0}; // 0000 is RESERVED
endcase
endcase
end
endfunction
reg [2:0] st_r;
reg [3:0] pid_r;
reg [3:0] cnt_r;
reg [4:0] age_r;
reg [2:0] err_r;
reg pkt_ok_r, pkt_err_r;
wire [8:0] lv_r = len_of(pid_r);
assign state = st_r;
assign pid = pid_r;
assign pid_class = pid_r[1:0];
assign exp_min = lv_r[3:0];
assign exp_max = lv_r[7:4];
assign byte_count = cnt_r;
assign abort_age = age_r;
assign err_code = err_r;
assign pkt_valid = pkt_ok_r;
assign pkt_error = pkt_err_r;
assign in_packet = (st_r == S_PID) || (st_r == S_BODY) || (st_r == S_HSHK);
// ---- The PID byte checks itself. ----
//
// The complement is the encoding, so this is not an optional integrity
// check that could be moved elsewhere -- a byte that fails it is not a
// damaged PID, it is not a PID.
wire [3:0] in_pid = byte_data[3:0];
wire pid_cmpl = (byte_data[7:4] == ~byte_data[3:0]);
wire [8:0] lv_in = len_of(in_pid);
wire pid_bad = !pid_cmpl || lv_in[8];
reg [2:0] st_n;
reg [3:0] pid_n, cnt_n;
reg [4:0] age_n;
reg [2:0] err_n;
reg ok_n, err_pulse_n, to_n;
always @* begin
st_n = st_r;
pid_n = pid_r;
cnt_n = cnt_r;
age_n = 5'd0;
err_n = err_r;
ok_n = 1'b0;
err_pulse_n = 1'b0;
to_n = 1'b0;
case (st_r)
// ------------------------------------------------------------------
S_IDLE: begin
// A bus error on an idle line is noise, not a packet error. There is
// no packet to be wrong about, and counting it would fill the log
// with events that mean nothing.
if (sync) begin
st_n = S_PID;
cnt_n = 4'd0;
err_n = E_NONE;
end else if (byte_valid) begin
// A byte with no SYNC in front of it. The stream is not where we
// think it is, so stop interpreting it.
st_n = S_ABORT;
err_n = E_STRAY;
err_pulse_n = 1'b1;
end
end
// ------------------------------------------------------------------
S_PID: begin
if (bus_error) begin
st_n = S_ABORT; err_n = E_BUS; err_pulse_n = 1'b1;
end else if (eop) begin
// A packet that ended before its own identifier arrived. The
// packet is already OVER, so there is nothing to swallow: go
// straight back to IDLE, ready for the next SYNC.
st_n = S_IDLE; err_n = E_SHORT; err_pulse_n = 1'b1;
end else if (byte_valid) begin
if (pid_bad) begin
st_n = S_ABORT; err_n = E_PID; err_pulse_n = 1'b1;
end else begin
pid_n = in_pid;
cnt_n = 4'd0;
// A handshake has no body at all, so it goes to a state that
// accepts nothing but the end of the packet.
if (lv_in[7:4] == 4'd0) st_n = S_HSHK;
else st_n = S_BODY;
end
end
end
// ------------------------------------------------------------------
S_BODY: begin
if (bus_error) begin
st_n = S_ABORT; err_n = E_BUS; err_pulse_n = 1'b1;
end else if (byte_valid) begin
// Compared against the maximum BEFORE the increment: the byte
// that would take the count past the limit is the one that is
// too many, not the one after it.
if (cnt_r >= lv_r[7:4]) begin
st_n = S_ABORT; err_n = E_LONG; err_pulse_n = 1'b1;
end else begin
cnt_n = cnt_r + 4'd1;
end
end else if (eop) begin
if (cnt_r >= lv_r[3:0]) begin
st_n = S_IDLE;
ok_n = 1'b1;
err_n = E_NONE;
end else begin
// Short. And, again, already over -- waiting in S_ABORT for an
// EOP that has already been consumed would swallow the next
// packet as well.
st_n = S_IDLE; err_n = E_SHORT; err_pulse_n = 1'b1;
end
end
end
// ------------------------------------------------------------------
S_HSHK: begin
if (bus_error) begin
st_n = S_ABORT; err_n = E_BUS; err_pulse_n = 1'b1;
end else if (byte_valid) begin
st_n = S_ABORT; err_n = E_LONG; err_pulse_n = 1'b1;
end else if (eop) begin
st_n = S_IDLE;
ok_n = 1'b1;
err_n = E_NONE;
end
end
// ------------------------------------------------------------------
S_ABORT: begin
// The whole point of this state is that it does NOTHING. It does not
// decode, it does not count, and above all it does not raise a
// second error for the rest of a packet that is already known to be
// bad. One corrupt packet, one error.
if (eop) begin
st_n = S_IDLE;
end else if (age_r >= ABORT_MAX[4:0] - 5'd1) begin
// The EOP itself was lost. Give up waiting rather than wedge:
// resynchronisation must be BOUNDED, or a single missing EOP is
// a permanent hang.
st_n = S_IDLE;
to_n = 1'b1;
end else begin
age_n = age_r + 5'd1;
end
end
default: st_n = S_IDLE;
endcase
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= S_IDLE;
pid_r <= 4'd0;
cnt_r <= 4'd0;
age_r <= 5'd0;
err_r <= E_NONE;
pkt_ok_r <= 1'b0;
pkt_err_r <= 1'b0;
n_packets <= 32'd0;
n_errors <= 32'd0;
n_pid_err <= 32'd0;
n_short <= 32'd0;
n_long <= 32'd0;
n_stray <= 32'd0;
n_bus_err <= 32'd0;
n_timeouts <= 32'd0;
end else begin
st_r <= st_n;
pid_r <= pid_n;
cnt_r <= cnt_n;
age_r <= age_n;
err_r <= err_n;
pkt_ok_r <= ok_n;
pkt_err_r <= err_pulse_n;
if (ok_n) n_packets <= n_packets + 32'd1;
if (err_pulse_n) n_errors <= n_errors + 32'd1;
if (to_n) n_timeouts <= n_timeouts + 32'd1;
// The per-cause counters are driven by the SAME pulse as n_errors, so
// they sum to it by construction. A cause counted independently can
// drift from the total, and then neither number can be trusted.
if (err_pulse_n) begin
case (err_n)
E_PID: n_pid_err <= n_pid_err + 32'd1;
E_SHORT: n_short <= n_short + 32'd1;
E_LONG: n_long <= n_long + 32'd1;
E_STRAY: n_stray <= n_stray + 32'd1;
E_BUS: n_bus_err <= n_bus_err + 32'd1;
default: ;
endcase
end
end
end
endmodule8. SystemVerilog Implementation
// usb_packet_fsm -- the packet recogniser, and the one rule that decides
// whether a USB receiver survives a corrupt packet or spends the rest of the
// session out of step.
//
// THE RULE
//
// EVERY ERROR RESYNCHRONISES.
//
// Not "recovers". Not "skips the bad byte and carries on". There is no way
// to know where the next packet begins except by waiting for one to begin --
// so on any error the machine stops interpreting the stream entirely, waits
// for the end of the packet, and starts again from nothing.
//
// A receiver that tries to be clever -- guessing the length, resuming after
// the bad byte, assuming the next byte is a PID -- is not more robust. It is
// a machine that is now interpreting payload as headers, and it will stay
// wrong until something resets it.
//
// THE PID IS SELF-CHECKING
//
// A USB packet identifier is one byte carrying four bits of information:
//
// bit 7 6 5 4 3 2 1 0
// ~PID PID
//
// The top nibble is the BITWISE COMPLEMENT of the bottom one. That is not a
// checksum bolted on afterwards -- it is the encoding itself, and it means
// the very first byte of every packet validates itself before anything is
// done with it.
//
// Of the 256 possible bytes, exactly 16 satisfy the complement rule, and one
// of those 16 (PID 0000) is reserved. So EXACTLY 15 BYTES IN 256 ARE A LEGAL
// PACKET IDENTIFIER, and the testbench checks all 256 of them.
//
// ONE ERROR PER PACKET, NOT ONE PER BYTE
//
// This is the part that is easy to get wrong and hard to notice. When a PID
// fails its check, the bytes behind it are still coming. A machine that
// returns straight to IDLE will see each of them as a stray byte with no
// SYNC and report an error for every one.
//
// The log then shows forty errors where one packet was corrupted, the error
// counters are useless for diagnosis, and any firmware that reacts to errors
// reacts forty times. The fix is a state whose entire job is to interpret
// NOTHING until the packet is over: S_ABORT.
//
// TWO KINDS OF ERROR, AND THEY DO NOT RESYNCHRONISE THE SAME WAY
//
// This distinction is the one that is missed, and missing it costs a whole
// extra packet every time:
//
// detected MID-PACKET a bad PID, an over-long body, a PHY failure, a
// byte with no SYNC. The rest of the packet is
// still arriving. -> S_ABORT, swallow it
//
// detected AT THE EOP a packet that ended before it was whole. The
// packet is ALREADY OVER. -> S_IDLE, immediately
//
// Sending a short packet to S_ABORT looks harmless -- it is one error
// either way. But S_ABORT leaves only on an EOP, and the EOP has already
// been consumed. So the machine sits in S_ABORT through the whole of the
// NEXT packet, swallows it, and leaves on ITS end-of-packet.
//
// The symptom is that every short packet costs two: the corrupt one and
// the good one behind it. Under a retry, the retry is the packet that gets
// eaten -- so the transfer never completes and nothing in the error log
// says why.
//
// RESYNCHRONISATION IS BOUNDED
//
// S_ABORT leaves on the end of the packet -- or, if the EOP itself was lost,
// after ABORT_MAX cycles. Without the timeout a single missing EOP wedges
// the receiver permanently, which is a hang rather than an error, and a hang
// is the one failure a protocol block must not have.
package usb_pktfsm_pkg;
// The five states. They are an enumeration and not an encoding because
// S_ABORT is not a step in the sequence -- it is the place the machine
// goes when the sequence has stopped meaning anything.
typedef enum logic [2:0] {
S_IDLE = 3'd0, // nothing is happening; wait for SYNC
S_PID = 3'd1, // SYNC seen; the next byte IS the PID
S_BODY = 3'd2, // collecting a token / data payload
S_HSHK = 3'd3, // a handshake: EOP and nothing else
S_ABORT = 3'd4 // interpret NOTHING until the packet ends
} pkt_state_e;
// The causes, named. They are counted separately AND summed, so a
// mis-classification shows up as a total that does not add up.
typedef enum logic [2:0] {
E_NONE = 3'd0,
E_PID = 3'd1, // the complement check failed, or the PID is reserved
E_SHORT = 3'd2, // EOP arrived before the packet was whole
E_LONG = 3'd3, // more bytes than this PID permits
E_STRAY = 3'd4, // a byte with no SYNC in front of it
E_BUS = 3'd5 // the PHY failed mid-packet
} pkt_err_e;
endpackage
module usb_packet_fsm
import usb_pktfsm_pkg::*;
#(
parameter int MAXLEN = 8, // largest payload this recogniser accepts
parameter int ABORT_MAX = 16 // cycles before an abort gives up waiting
) (
input logic clk,
input logic rst_n,
input logic sync, // a SYNC pattern was detected on the line
input logic byte_valid, // a decoded byte is present
input logic [7:0] byte_data,
input logic eop, // end of packet
input logic bus_error, // PHY-level failure (bit-stuff / NRZI)
output pkt_state_e state,
output logic [3:0] pid,
output logic [1:0] pid_class,
output logic [3:0] exp_min, // bytes this PID requires
output logic [3:0] exp_max, // bytes this PID permits
output logic [3:0] byte_count,
output logic in_packet,
output logic pkt_valid, // ONE pulse per well-formed packet
output logic pkt_error, // ONE pulse per corrupt packet
output pkt_err_e err_code,
output logic [4:0] abort_age,
output logic [31:0] n_packets,
output logic [31:0] n_errors,
output logic [31:0] n_pid_err,
output logic [31:0] n_short,
output logic [31:0] n_long,
output logic [31:0] n_stray,
output logic [31:0] n_bus_err,
output logic [31:0] n_timeouts
);
// ---- The length rules, from the PID and nothing else. ----
//
// Returned as {reserved, max, min} so a single lookup answers both "is
// this PID legal at all" and "how long is its body".
function automatic logic [8:0] len_of(input logic [3:0] p);
unique case (p[1:0])
2'b01: return {1'b0, 4'd2, 4'd2}; // token: ADDR+ENDP+CRC5
2'b11: return {1'b0, 4'(MAXLEN), 4'd2}; // data: payload + CRC16
2'b10: return {1'b0, 4'd0, 4'd0}; // handshake: nothing
default: begin
case (p)
4'b0100: return {1'b0, 4'd2, 4'd2}; // PING
4'b1000: return {1'b0, 4'd3, 4'd3}; // SPLIT
4'b1100: return {1'b0, 4'd0, 4'd0}; // PRE / ERR
default: return {1'b1, 4'd0, 4'd0}; // 0000 is RESERVED
endcase
end
endcase
endfunction
pkt_state_e st_r;
pkt_err_e err_r;
logic [3:0] pid_r, cnt_r;
logic [4:0] age_r;
logic pkt_ok_r, pkt_err_r;
logic [8:0] lv_r;
assign lv_r = len_of(pid_r);
assign state = st_r;
assign pid = pid_r;
assign pid_class = pid_r[1:0];
assign exp_min = lv_r[3:0];
assign exp_max = lv_r[7:4];
assign byte_count = cnt_r;
assign abort_age = age_r;
assign err_code = err_r;
assign pkt_valid = pkt_ok_r;
assign pkt_error = pkt_err_r;
assign in_packet = (st_r == S_PID) || (st_r == S_BODY) || (st_r == S_HSHK);
// ---- The PID byte checks itself. ----
//
// The complement is the encoding, so this is not an optional integrity
// check that could be moved elsewhere -- a byte that fails it is not a
// damaged PID, it is not a PID.
logic [3:0] in_pid;
logic pid_cmpl, pid_bad;
logic [8:0] lv_in;
assign in_pid = byte_data[3:0];
assign pid_cmpl = (byte_data[7:4] == ~byte_data[3:0]);
assign lv_in = len_of(in_pid);
assign pid_bad = !pid_cmpl || lv_in[8];
pkt_state_e st_n;
pkt_err_e err_n;
logic [3:0] pid_n, cnt_n;
logic [4:0] age_n;
logic ok_n, err_pulse_n, to_n;
always_comb begin
st_n = st_r;
err_n = err_r;
pid_n = pid_r;
cnt_n = cnt_r;
age_n = '0;
ok_n = 1'b0;
err_pulse_n = 1'b0;
to_n = 1'b0;
unique case (st_r)
// ------------------------------------------------------------------
S_IDLE: begin
// A bus error on an idle line is noise, not a packet error. There is
// no packet to be wrong about, and counting it would fill the log
// with events that mean nothing.
if (sync) begin
st_n = S_PID;
cnt_n = '0;
err_n = E_NONE;
end else if (byte_valid) begin
// A byte with no SYNC in front of it. The stream is not where we
// think it is, so stop interpreting it.
st_n = S_ABORT;
err_n = E_STRAY;
err_pulse_n = 1'b1;
end
end
// ------------------------------------------------------------------
S_PID: begin
if (bus_error) begin
st_n = S_ABORT; err_n = E_BUS; err_pulse_n = 1'b1;
end else if (eop) begin
// A packet that ended before its own identifier arrived. The
// packet is already OVER, so there is nothing to swallow: go
// straight back to IDLE, ready for the next SYNC.
st_n = S_IDLE; err_n = E_SHORT; err_pulse_n = 1'b1;
end else if (byte_valid) begin
if (pid_bad) begin
st_n = S_ABORT; err_n = E_PID; err_pulse_n = 1'b1;
end else begin
pid_n = in_pid;
cnt_n = '0;
// A handshake has no body at all, so it goes to a state that
// accepts nothing but the end of the packet.
if (lv_in[7:4] == 4'd0) st_n = S_HSHK;
else st_n = S_BODY;
end
end
end
// ------------------------------------------------------------------
S_BODY: begin
if (bus_error) begin
st_n = S_ABORT; err_n = E_BUS; err_pulse_n = 1'b1;
end else if (byte_valid) begin
// Compared against the maximum BEFORE the increment: the byte
// that would take the count past the limit is the one that is
// too many, not the one after it.
if (cnt_r >= lv_r[7:4]) begin
st_n = S_ABORT; err_n = E_LONG; err_pulse_n = 1'b1;
end else begin
cnt_n = cnt_r + 4'd1;
end
end else if (eop) begin
if (cnt_r >= lv_r[3:0]) begin
st_n = S_IDLE;
ok_n = 1'b1;
err_n = E_NONE;
end else begin
// Short. And, again, already over -- waiting in S_ABORT for an
// EOP that has already been consumed would swallow the next
// packet as well.
st_n = S_IDLE; err_n = E_SHORT; err_pulse_n = 1'b1;
end
end
end
// ------------------------------------------------------------------
S_HSHK: begin
if (bus_error) begin
st_n = S_ABORT; err_n = E_BUS; err_pulse_n = 1'b1;
end else if (byte_valid) begin
st_n = S_ABORT; err_n = E_LONG; err_pulse_n = 1'b1;
end else if (eop) begin
st_n = S_IDLE;
ok_n = 1'b1;
err_n = E_NONE;
end
end
// ------------------------------------------------------------------
S_ABORT: begin
// The whole point of this state is that it does NOTHING. It does not
// decode, it does not count, and above all it does not raise a
// second error for the rest of a packet that is already known to be
// bad. One corrupt packet, one error.
if (eop) begin
st_n = S_IDLE;
end else if (age_r >= 5'(ABORT_MAX) - 5'd1) begin
// The EOP itself was lost. Give up waiting rather than wedge:
// resynchronisation must be BOUNDED, or a single missing EOP is
// a permanent hang.
st_n = S_IDLE;
to_n = 1'b1;
end else begin
age_n = age_r + 5'd1;
end
end
default: st_n = S_IDLE;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= S_IDLE;
pid_r <= '0;
cnt_r <= '0;
age_r <= '0;
err_r <= E_NONE;
pkt_ok_r <= 1'b0;
pkt_err_r <= 1'b0;
n_packets <= '0;
n_errors <= '0;
n_pid_err <= '0;
n_short <= '0;
n_long <= '0;
n_stray <= '0;
n_bus_err <= '0;
n_timeouts <= '0;
end else begin
st_r <= st_n;
pid_r <= pid_n;
cnt_r <= cnt_n;
age_r <= age_n;
err_r <= err_n;
pkt_ok_r <= ok_n;
pkt_err_r <= err_pulse_n;
if (ok_n) n_packets <= n_packets + 32'd1;
if (err_pulse_n) n_errors <= n_errors + 32'd1;
if (to_n) n_timeouts <= n_timeouts + 32'd1;
// The per-cause counters are driven by the SAME pulse as n_errors, so
// they sum to it by construction. A cause counted independently can
// drift from the total, and then neither number can be trusted.
if (err_pulse_n) begin
case (err_n)
E_PID: n_pid_err <= n_pid_err + 32'd1;
E_SHORT: n_short <= n_short + 32'd1;
E_LONG: n_long <= n_long + 32'd1;
E_STRAY: n_stray <= n_stray + 32'd1;
E_BUS: n_bus_err <= n_bus_err + 32'd1;
default: ;
endcase
end
end
end
endmodule9. VHDL-2008 Implementation
-- usb_packet_fsm -- the packet recogniser, and the one rule that decides
-- whether a USB receiver survives a corrupt packet or spends the rest of the
-- session out of step.
--
-- THE RULE
--
-- EVERY ERROR RESYNCHRONISES.
--
-- Not "recovers". Not "skips the bad byte and carries on". There is no way
-- to know where the next packet begins except by waiting for one to begin --
-- so on any error the machine stops interpreting the stream entirely, waits
-- for the end of the packet, and starts again from nothing.
--
-- A receiver that tries to be clever -- guessing the length, resuming after
-- the bad byte, assuming the next byte is a PID -- is not more robust. It is
-- a machine that is now interpreting payload as headers, and it will stay
-- wrong until something resets it.
--
-- THE PID IS SELF-CHECKING
--
-- A USB packet identifier is one byte carrying four bits of information:
--
-- bit 7 6 5 4 3 2 1 0
-- ~PID PID
--
-- The top nibble is the BITWISE COMPLEMENT of the bottom one. That is not a
-- checksum bolted on afterwards -- it is the encoding itself, and it means
-- the very first byte of every packet validates itself before anything is
-- done with it.
--
-- Of the 256 possible bytes, exactly 16 satisfy the complement rule, and one
-- of those 16 (PID 0000) is reserved. So EXACTLY 15 BYTES IN 256 ARE A LEGAL
-- PACKET IDENTIFIER, and the testbench checks all 256 of them.
--
-- ONE ERROR PER PACKET, NOT ONE PER BYTE
--
-- This is the part that is easy to get wrong and hard to notice. When a PID
-- fails its check, the bytes behind it are still coming. A machine that
-- returns straight to IDLE will see each of them as a stray byte with no
-- SYNC and report an error for every one.
--
-- The log then shows forty errors where one packet was corrupted, the error
-- counters are useless for diagnosis, and any firmware that reacts to errors
-- reacts forty times. The fix is a state whose entire job is to interpret
-- NOTHING until the packet is over: S_ABORT.
--
-- TWO KINDS OF ERROR, AND THEY DO NOT RESYNCHRONISE THE SAME WAY
--
-- This distinction is the one that is missed, and missing it costs a whole
-- extra packet every time:
--
-- detected MID-PACKET a bad PID, an over-long body, a PHY failure, a
-- byte with no SYNC. The rest of the packet is
-- still arriving. -> S_ABORT, swallow it
--
-- detected AT THE EOP a packet that ended before it was whole. The
-- packet is ALREADY OVER. -> S_IDLE, immediately
--
-- Sending a short packet to S_ABORT looks harmless -- it is one error
-- either way. But S_ABORT leaves only on an EOP, and the EOP has already
-- been consumed. So the machine sits in S_ABORT through the whole of the
-- NEXT packet, swallows it, and leaves on ITS end-of-packet.
--
-- The symptom is that every short packet costs two: the corrupt one and
-- the good one behind it. Under a retry, the retry is the packet that gets
-- eaten -- so the transfer never completes and nothing in the error log
-- says why.
--
-- RESYNCHRONISATION IS BOUNDED
--
-- S_ABORT leaves on the end of the packet -- or, if the EOP itself was lost,
-- after ABORT_MAX cycles. Without the timeout a single missing EOP wedges
-- the receiver permanently, which is a hang rather than an error, and a hang
-- is the one failure a protocol block must not have.
library ieee;
use ieee.std_logic_1164.all;
package usb_pktfsm_pkg is
-- The five states. They are an enumeration and not an encoding because
-- S_ABORT is not a step in the sequence -- it is the place the machine
-- goes when the sequence has stopped meaning anything.
type pkt_state_t is (S_IDLE, S_PID, S_BODY, S_HSHK, S_ABORT);
-- The causes, named. They are counted separately AND summed, so a
-- mis-classification shows up as a total that does not add up.
type pkt_err_t is (E_NONE, E_PID, E_SHORT, E_LONG, E_STRAY, E_BUS);
function st_code (s : pkt_state_t) return std_logic_vector;
function er_code (e : pkt_err_t) return std_logic_vector;
end package usb_pktfsm_pkg;
package body usb_pktfsm_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 st_code (s : pkt_state_t) return std_logic_vector is
begin
case s is
when S_IDLE => return "000";
when S_PID => return "001";
when S_BODY => return "010";
when S_HSHK => return "011";
when S_ABORT => return "100";
end case;
end function;
function er_code (e : pkt_err_t) return std_logic_vector is
begin
case e is
when E_NONE => return "000";
when E_PID => return "001";
when E_SHORT => return "010";
when E_LONG => return "011";
when E_STRAY => return "100";
when E_BUS => return "101";
end case;
end function;
end package body usb_pktfsm_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_pktfsm_pkg.all;
entity usb_packet_fsm is
generic (
MAXLEN : integer := 8; -- largest payload this recogniser accepts
ABORT_MAX : integer := 16 -- cycles before an abort gives up waiting
);
port (
clk : in std_logic;
rst_n : in std_logic;
sync : in std_logic; -- a SYNC was detected
byte_valid : in std_logic; -- a decoded byte is here
byte_data : in std_logic_vector(7 downto 0);
eop : in std_logic; -- end of packet
bus_error : in std_logic; -- PHY-level failure
state : out std_logic_vector(2 downto 0);
pid : out std_logic_vector(3 downto 0);
pid_class : out std_logic_vector(1 downto 0);
exp_min : out std_logic_vector(3 downto 0); -- bytes this PID requires
exp_max : out std_logic_vector(3 downto 0); -- bytes this PID permits
byte_count : out std_logic_vector(3 downto 0);
in_packet : out std_logic;
pkt_valid : out std_logic; -- ONE pulse per well-formed packet
pkt_error : out std_logic; -- ONE pulse per corrupt packet
err_code : out std_logic_vector(2 downto 0);
abort_age : out std_logic_vector(4 downto 0);
n_packets : out std_logic_vector(31 downto 0);
n_errors : out std_logic_vector(31 downto 0);
n_pid_err : out std_logic_vector(31 downto 0);
n_short : out std_logic_vector(31 downto 0);
n_long : out std_logic_vector(31 downto 0);
n_stray : out std_logic_vector(31 downto 0);
n_bus_err : out std_logic_vector(31 downto 0);
n_timeouts : out std_logic_vector(31 downto 0)
);
end entity usb_packet_fsm;
architecture rtl of usb_packet_fsm is
-- ---- The length rules, from the PID and nothing else. ----
--
-- Returned as (reserved, max, min) so a single lookup answers both "is
-- this PID legal at all" and "how long is its body".
function len_of (p : std_logic_vector(3 downto 0)) return std_logic_vector is
begin
case p(1 downto 0) is
when "01" => return '0' & x"2" & x"2"; -- token
when "11" => return '0' & std_logic_vector(to_unsigned(MAXLEN, 4)) & x"2";
when "10" => return '0' & x"0" & x"0"; -- handshake
when others =>
case p is
when "0100" => return '0' & x"2" & x"2"; -- PING
when "1000" => return '0' & x"3" & x"3"; -- SPLIT
when "1100" => return '0' & x"0" & x"0"; -- PRE / ERR
when others => return '1' & x"0" & x"0"; -- RESERVED
end case;
end case;
end function;
signal st_r : pkt_state_t := S_IDLE;
signal err_r : pkt_err_t := E_NONE;
signal pid_r : std_logic_vector(3 downto 0) := (others => '0');
signal cnt_r : unsigned(3 downto 0) := (others => '0');
signal age_r : unsigned(4 downto 0) := (others => '0');
signal pkt_ok_r : std_logic := '0';
signal pkt_err_r : std_logic := '0';
signal lv_r, lv_in : std_logic_vector(8 downto 0);
signal in_pid : std_logic_vector(3 downto 0);
signal pid_cmpl : std_logic;
signal pid_bad : 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_pkts, c_errs, c_pid_e : unsigned(31 downto 0) := (others => '0');
signal c_short, c_long, c_stray : unsigned(31 downto 0) := (others => '0');
signal c_bus, c_to : unsigned(31 downto 0) := (others => '0');
begin
lv_r <= len_of(pid_r);
state <= st_code(st_r);
err_code <= er_code(err_r);
pid <= pid_r;
pid_class <= pid_r(1 downto 0);
exp_min <= lv_r(3 downto 0);
exp_max <= lv_r(7 downto 4);
byte_count <= std_logic_vector(cnt_r);
abort_age <= std_logic_vector(age_r);
pkt_valid <= pkt_ok_r;
pkt_error <= pkt_err_r;
in_packet <= '1' when (st_r = S_PID or st_r = S_BODY or st_r = S_HSHK)
else '0';
-- ---- The PID byte checks itself. ----
--
-- The complement is the encoding, so this is not an optional integrity
-- check that could be moved elsewhere -- a byte that fails it is not a
-- damaged PID, it is not a PID.
in_pid <= byte_data(3 downto 0);
pid_cmpl <= '1' when byte_data(7 downto 4) = (not byte_data(3 downto 0))
else '0';
lv_in <= len_of(in_pid);
pid_bad <= '1' when (pid_cmpl = '0' or lv_in(8) = '1') else '0';
n_packets <= std_logic_vector(c_pkts);
n_errors <= std_logic_vector(c_errs);
n_pid_err <= std_logic_vector(c_pid_e);
n_short <= std_logic_vector(c_short);
n_long <= std_logic_vector(c_long);
n_stray <= std_logic_vector(c_stray);
n_bus_err <= std_logic_vector(c_bus);
n_timeouts <= std_logic_vector(c_to);
process (clk, rst_n)
variable ns : pkt_state_t;
variable ne : pkt_err_t;
variable np : std_logic_vector(3 downto 0);
variable nc : unsigned(3 downto 0);
variable na : unsigned(4 downto 0);
variable no, nep, nto : std_logic;
begin
if rst_n = '0' then
st_r <= S_IDLE;
err_r <= E_NONE;
pid_r <= (others => '0');
cnt_r <= (others => '0');
age_r <= (others => '0');
pkt_ok_r <= '0';
pkt_err_r <= '0';
c_pkts <= (others => '0');
c_errs <= (others => '0');
c_pid_e <= (others => '0');
c_short <= (others => '0');
c_long <= (others => '0');
c_stray <= (others => '0');
c_bus <= (others => '0');
c_to <= (others => '0');
elsif rising_edge(clk) then
ns := st_r; ne := err_r; np := pid_r; nc := cnt_r;
na := (others => '0');
no := '0'; nep := '0'; nto := '0';
case st_r is
-- ----------------------------------------------------------------
when S_IDLE =>
-- A bus error on an idle line is noise, not a packet error. There
-- is no packet to be wrong about, and counting it would fill the
-- log with events that mean nothing.
if sync = '1' then
ns := S_PID; nc := (others => '0'); ne := E_NONE;
elsif byte_valid = '1' then
-- A byte with no SYNC in front of it. The stream is not where
-- we think it is, so stop interpreting it.
ns := S_ABORT; ne := E_STRAY; nep := '1';
end if;
-- ----------------------------------------------------------------
when S_PID =>
if bus_error = '1' then
ns := S_ABORT; ne := E_BUS; nep := '1';
elsif eop = '1' then
-- A packet that ended before its own identifier arrived. The
-- packet is already OVER, so there is nothing to swallow: go
-- straight back to IDLE, ready for the next SYNC.
ns := S_IDLE; ne := E_SHORT; nep := '1';
elsif byte_valid = '1' then
if pid_bad = '1' then
ns := S_ABORT; ne := E_PID; nep := '1';
else
np := in_pid; nc := (others => '0');
-- A handshake has no body at all, so it goes to a state that
-- accepts nothing but the end of the packet.
if lv_in(7 downto 4) = x"0" then ns := S_HSHK;
else ns := S_BODY;
end if;
end if;
end if;
-- ----------------------------------------------------------------
when S_BODY =>
if bus_error = '1' then
ns := S_ABORT; ne := E_BUS; nep := '1';
elsif byte_valid = '1' then
-- Compared against the maximum BEFORE the increment: the byte
-- that would take the count past the limit is the one that is
-- too many, not the one after it.
if cnt_r >= unsigned(lv_r(7 downto 4)) then
ns := S_ABORT; ne := E_LONG; nep := '1';
else
nc := cnt_r + 1;
end if;
elsif eop = '1' then
if cnt_r >= unsigned(lv_r(3 downto 0)) then
ns := S_IDLE; no := '1'; ne := E_NONE;
else
-- Short. And, again, already over -- waiting in S_ABORT for
-- an EOP that has already been consumed would swallow the
-- next packet as well.
ns := S_IDLE; ne := E_SHORT; nep := '1';
end if;
end if;
-- ----------------------------------------------------------------
when S_HSHK =>
if bus_error = '1' then
ns := S_ABORT; ne := E_BUS; nep := '1';
elsif byte_valid = '1' then
ns := S_ABORT; ne := E_LONG; nep := '1';
elsif eop = '1' then
ns := S_IDLE; no := '1'; ne := E_NONE;
end if;
-- ----------------------------------------------------------------
when S_ABORT =>
-- The whole point of this state is that it does NOTHING. It does
-- not decode, it does not count, and above all it does not raise
-- a second error for the rest of a packet that is already known
-- to be bad. One corrupt packet, one error.
if eop = '1' then
ns := S_IDLE;
elsif age_r >= to_unsigned(ABORT_MAX - 1, 5) then
-- The EOP itself was lost. Give up waiting rather than wedge:
-- resynchronisation must be BOUNDED, or a single missing EOP
-- is a permanent hang.
ns := S_IDLE; nto := '1';
else
na := age_r + 1;
end if;
end case;
st_r <= ns;
err_r <= ne;
pid_r <= np;
cnt_r <= nc;
age_r <= na;
pkt_ok_r <= no;
pkt_err_r <= nep;
if no = '1' then c_pkts <= c_pkts + 1; end if;
if nep = '1' then c_errs <= c_errs + 1; end if;
if nto = '1' then c_to <= c_to + 1; end if;
-- The per-cause counters are driven by the SAME pulse as n_errors, so
-- they sum to it by construction. A cause counted independently can
-- drift from the total, and then neither number can be trusted.
if nep = '1' then
case ne is
when E_PID => c_pid_e <= c_pid_e + 1;
when E_SHORT => c_short <= c_short + 1;
when E_LONG => c_long <= c_long + 1;
when E_STRAY => c_stray <= c_stray + 1;
when E_BUS => c_bus <= c_bus + 1;
when others => null;
end case;
end if;
end if;
end process;
end architecture rtl;10. Seeing the Resynchronisation
A corrupt PID, the rest of the packet swallowed, and the next packet accepted
usb_packet_fsm — one error, then a clean restart
10 cyclesLook at cycle 2. byte_valid is high with a junk byte, and pkt_error is low. That single cycle is the whole of §4: the machine has already reported this packet, and it will not report it again.
11. The Testbenches
Three things are swept exhaustively:
1. ALL 256 POSSIBLE PID BYTES -- not the 16 legal ones and
a few neighbours, but every value a corrupted line can
produce. Exactly 15 must be accepted, and the suite
counts them.
2. Every legal PID x every body length 0 .. MAXLEN+2, so
each length rule is checked at both boundaries and one
past each of them.
3. Every state x all 16 combinations of
{sync, byte_valid, eop, bus_error} = 80 pairs,
with each state REACHED by a real packet.And one property that is not a sweep at all:
// ---- Phase D: ONE ERROR PER CORRUPT PACKET, NOT ONE PER BYTE. ----
//
// The PID is corrupt, and then k more bytes arrive. A machine that
// returns straight to IDLE reports an error for each of them. The delta
// must be 1 for every k.
for (k = 0; k <= 12; k = k + 1) begin
before_e = n_errors;
step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0);
step(1'b0, 1'b1, 8'h00, 1'b0, 1'b0); // 0x00 fails the complement
for (it = 0; it < k; it = it + 1)
step(1'b0, 1'b1, (it*8'd29 + 8'd3), 1'b0, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0);
check(n_errors == before_e + 1,
"a single corrupt packet produced more than one error -- the machine returned to IDLE instead of swallowing the rest of the packet");
endThat check is a delta across a whole packet with a varying number of trailing bytes. No single-cycle comparison can express it, which is why the property survives in designs whose per-cycle checking is otherwise complete.
Phase E is its companion and is the "resynchronises" claim made concrete: after each of the five error causes in turn, a perfectly ordinary OUT token is sent, and it must be accepted. A machine that merely stops passes every safety property in this file; only phase E notices that it never started again.
11.1 Verilog testbench
// Testbench for usb_packet_fsm (Verilog-2005).
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. ALL 256 POSSIBLE PID BYTES. Not the 16 legal ones and a few
// neighbours -- every value a corrupted line can produce. Exactly 15
// of them must be accepted as an identifier, and the suite counts.
//
// 2. Every legal PID crossed with every body length from 0 to MAXLEN+2,
// so each PID's length rule is checked at both its boundaries and one
// past each of them.
//
// 3. Every state crossed with all 16 combinations of
// {sync, byte_valid, eop, bus_error}, with each state REACHED by a
// real packet rather than forced.
//
// AND THE PROPERTY THAT IS NOT A SWEEP
//
// ONE ERROR PER CORRUPT PACKET, NOT ONE PER BYTE. A machine that returns
// straight to IDLE after an error reports an error for every remaining byte
// of the packet, and the check for that is a DELTA across a whole packet
// with a varying number of trailing bytes -- which no single-cycle
// comparison can express.
`timescale 1ns/1ps
module tb_pf_v;
localparam integer MAXLEN = 8;
localparam integer ABORT_MAX = 16;
localparam [2:0] S_IDLE=3'd0, S_PID=3'd1, S_BODY=3'd2, S_HSHK=3'd3, S_ABORT=3'd4;
localparam [2:0] E_NONE=3'd0, E_PID=3'd1, E_SHORT=3'd2, E_LONG=3'd3,
E_STRAY=3'd4, E_BUS=3'd5;
reg clk = 1'b0, rst_n = 1'b0;
reg sync = 1'b0, byte_valid = 1'b0, eop = 1'b0, bus_error = 1'b0;
reg [7:0] byte_data = 8'd0;
wire [2:0] state, err_code;
wire [3:0] pid, exp_min, exp_max, byte_count;
wire [1:0] pid_class;
wire [4:0] abort_age;
wire in_packet, pkt_valid, pkt_error;
wire [31:0] n_packets, n_errors, n_pid_err, n_short, n_long, n_stray,
n_bus_err, n_timeouts;
usb_packet_fsm #(.MAXLEN(MAXLEN), .ABORT_MAX(ABORT_MAX)) dut (
.clk(clk), .rst_n(rst_n),
.sync(sync), .byte_valid(byte_valid), .byte_data(byte_data),
.eop(eop), .bus_error(bus_error),
.state(state), .pid(pid), .pid_class(pid_class),
.exp_min(exp_min), .exp_max(exp_max), .byte_count(byte_count),
.in_packet(in_packet), .pkt_valid(pkt_valid), .pkt_error(pkt_error),
.err_code(err_code), .abort_age(abort_age),
.n_packets(n_packets), .n_errors(n_errors), .n_pid_err(n_pid_err),
.n_short(n_short), .n_long(n_long), .n_stray(n_stray),
.n_bus_err(n_bus_err), .n_timeouts(n_timeouts)
);
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 pid=%h cnt=%0d err=%0d age=%0d",
$time, msg, state, pid, byte_count, err_code, abort_age);
end
end
endtask
// ------------------------------------------------------------------
// The length rules, written INDEPENDENTLY of the design's function.
// ------------------------------------------------------------------
function [3:0] lmin_of; input [3:0] p; begin
if (p[1:0] == 2'b01) lmin_of = 4'd2; // token
else if (p[1:0] == 2'b11) lmin_of = 4'd2; // data: CRC16 at minimum
else if (p[1:0] == 2'b10) lmin_of = 4'd0; // handshake
else if (p == 4'b0100) lmin_of = 4'd2; // PING
else if (p == 4'b1000) lmin_of = 4'd3; // SPLIT
else lmin_of = 4'd0; // PRE/ERR, and reserved
end endfunction
function [3:0] lmax_of; input [3:0] p; begin
if (p[1:0] == 2'b01) lmax_of = 4'd2;
else if (p[1:0] == 2'b11) lmax_of = MAXLEN[3:0];
else if (p[1:0] == 2'b10) lmax_of = 4'd0;
else if (p == 4'b0100) lmax_of = 4'd2;
else if (p == 4'b1000) lmax_of = 4'd3;
else lmax_of = 4'd0;
end endfunction
function pid_legal; input [7:0] b; begin
pid_legal = (b[7:4] == ~b[3:0]) && (b[3:0] != 4'd0);
end endfunction
// ------------------------------------------------------------------
// The shadow machine.
// ------------------------------------------------------------------
reg [2:0] m_st, m_err;
reg [3:0] m_pid, m_cnt;
reg [4:0] m_age;
reg m_ok, m_errp;
integer m_pkts, m_errs, m_pid_e, m_short, m_long, m_stray, m_bus, m_to;
integer seen [0:79]; // 5 states x 16 input combinations
integer n_seen, n_transitions;
task model_reset;
integer i;
begin
m_st = S_IDLE; m_err = E_NONE; m_pid = 4'd0; m_cnt = 4'd0; m_age = 5'd0;
m_ok = 1'b0; m_errp = 1'b0;
m_pkts = 0; m_errs = 0; m_pid_e = 0; m_short = 0;
m_long = 0; m_stray = 0; m_bus = 0; m_to = 0;
for (i = 0; i < 80; i = i + 1) seen[i] = 0;
n_seen = 0; n_transitions = 0;
end
endtask
integer combo_idx;
task step(input sy, input bv, input [7:0] dat, input ep, input be);
reg [2:0] ns, ne;
reg [3:0] np, nc;
reg [4:0] na;
reg no, nep, nto;
reg [3:0] ib;
begin
sync = sy; byte_valid = bv; byte_data = dat; eop = ep; bus_error = be;
#1;
// ---- Everything the DUT holds must match the model, every cycle. ----
check(state === m_st, "state disagrees with the shadow machine");
check(pid === m_pid, "pid disagrees");
check(byte_count === m_cnt, "byte_count disagrees");
check(abort_age === m_age, "abort_age disagrees -- resynchronisation is not bounded the way the model says");
check(err_code === m_err, "err_code disagrees");
check(pkt_valid === m_ok, "pkt_valid disagrees");
check(pkt_error === m_errp, "pkt_error disagrees");
check(in_packet === ((m_st==S_PID)||(m_st==S_BODY)||(m_st==S_HSHK)),
"in_packet disagrees with the state it summarises");
check(exp_min === lmin_of(m_pid), "exp_min disagrees with the PID's length rule");
check(exp_max === lmax_of(m_pid), "exp_max disagrees with the PID's length rule");
// ---- Invariants that hold regardless of stimulus ----
check(!(pkt_valid && pkt_error),
"a packet was reported both good and bad in the same cycle");
check(m_st <= S_ABORT, "the machine left its own state space");
check(!((m_st == S_BODY) && (m_cnt > lmax_of(m_pid))),
"more bytes were collected than this PID permits");
check(abort_age <= ABORT_MAX[4:0],
"the abort timer ran past its bound -- resynchronisation is unbounded");
check(!((m_st != S_ABORT) && (abort_age != 5'd0)),
"the abort timer is running outside S_ABORT");
combo_idx = m_st * 16 + {sy, bv, ep, be};
if (seen[combo_idx] == 0) begin seen[combo_idx] = 1; n_seen = n_seen + 1; end
n_transitions = n_transitions + 1;
// ---- advance the shadow machine ----
ns = m_st; ne = m_err; np = m_pid; nc = m_cnt; na = 5'd0;
no = 1'b0; nep = 1'b0; nto = 1'b0;
ib = dat[3:0];
if (m_st == S_IDLE) begin
if (sy) begin ns = S_PID; nc = 4'd0; ne = E_NONE; end
else if (bv) begin ns = S_ABORT; ne = E_STRAY; nep = 1'b1; end
end else if (m_st == S_PID) begin
if (be) begin ns = S_ABORT; ne = E_BUS; nep = 1'b1; end
else if (ep) begin ns = S_IDLE; ne = E_SHORT; nep = 1'b1; end
else if (bv) begin
if (!pid_legal(dat)) begin ns = S_ABORT; ne = E_PID; nep = 1'b1; end
else begin
np = ib; nc = 4'd0;
if (lmax_of(ib) == 4'd0) ns = S_HSHK; else ns = S_BODY;
end
end
end else if (m_st == S_BODY) begin
if (be) begin ns = S_ABORT; ne = E_BUS; nep = 1'b1; end
else if (bv) begin
if (m_cnt >= lmax_of(m_pid)) begin ns = S_ABORT; ne = E_LONG; nep = 1'b1; end
else nc = m_cnt + 4'd1;
end else if (ep) begin
if (m_cnt >= lmin_of(m_pid)) begin ns = S_IDLE; no = 1'b1; ne = E_NONE; end
else begin ns = S_IDLE; ne = E_SHORT; nep = 1'b1; end
end
end else if (m_st == S_HSHK) begin
if (be) begin ns = S_ABORT; ne = E_BUS; nep = 1'b1; end
else if (bv) begin ns = S_ABORT; ne = E_LONG; nep = 1'b1; end
else if (ep) begin ns = S_IDLE; no = 1'b1; ne = E_NONE; end
end else begin // S_ABORT: interpret nothing
if (ep) ns = S_IDLE;
else if (m_age >= ABORT_MAX[4:0] - 5'd1) begin ns = S_IDLE; nto = 1'b1; end
else na = m_age + 5'd1;
end
m_st = ns; m_err = ne; m_pid = np; m_cnt = nc; m_age = na;
m_ok = no; m_errp = nep;
if (no) m_pkts = m_pkts + 1;
if (nto) m_to = m_to + 1;
if (nep) begin
m_errs = m_errs + 1;
case (ne)
E_PID: m_pid_e = m_pid_e + 1;
E_SHORT: m_short = m_short + 1;
E_LONG: m_long = m_long + 1;
E_STRAY: m_stray = m_stray + 1;
E_BUS: m_bus = m_bus + 1;
default: ;
endcase
end
@(posedge clk); #1;
sync = 1'b0; byte_valid = 1'b0; eop = 1'b0; bus_error = 1'b0;
end
endtask
task quiet(input integer n);
integer i;
begin for (i = 0; i < n; i = i + 1) step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0); end
endtask
// Send SYNC, an arbitrary PID byte, nb body bytes, then EOP -- and one
// quiet cycle so the registered pkt_valid / pkt_error pulse is observed.
task send_raw(input [7:0] pb, input integer nb);
integer i;
begin
step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0);
step(1'b0, 1'b1, pb, 1'b0, 1'b0);
for (i = 0; i < nb; i = i + 1)
step(1'b0, 1'b1, (i*8'd13 + 8'd7), 1'b0, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0);
end
endtask
task send_pid(input [3:0] nib, input integer nb);
begin send_raw({~nib, nib}, nb); end
endtask
integer b, nb, k, it, n_pid_ok, exp_pid_ok;
integer before_p, before_e;
reg [3:0] nib;
reg exp_accept;
initial begin
model_reset;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(state === S_IDLE, "reset did not land in IDLE");
check(in_packet === 1'b0, "reset asserted in_packet");
check(err_code === E_NONE, "reset left an error code set");
check(n_errors === 32'd0, "reset left the error counter non-zero");
// ---- Phase B: ALL 256 PID BYTES. ----
//
// Each is sent with no body at all, so the outcome separates cleanly:
// a byte that fails the complement check is E_PID; a byte that passes
// it but needs a body is E_SHORT; a handshake is a complete packet.
n_pid_ok = 0; exp_pid_ok = 0;
for (b = 0; b < 256; b = b + 1) begin
before_e = n_pid_err;
send_raw(b[7:0], 0);
if (n_pid_err == before_e) n_pid_ok = n_pid_ok + 1;
if (pid_legal(b[7:0])) exp_pid_ok = exp_pid_ok + 1;
check((n_pid_err == before_e) == pid_legal(b[7:0]),
"a PID byte was accepted or rejected against the complement rule");
end
check(n_pid_ok == exp_pid_ok, "the set of accepted PID bytes is not the legal set");
check(n_pid_ok == 15,
"exactly 15 of the 256 possible bytes are a legal packet identifier: 16 satisfy the complement and one of those is reserved");
// ---- Phase C: every legal PID x every body length 0..MAXLEN+2 ----
for (b = 1; b < 16; b = b + 1) begin
nib = b[3:0];
for (nb = 0; nb <= MAXLEN + 2; nb = nb + 1) begin
before_p = n_packets;
before_e = n_errors;
send_pid(nib, nb);
exp_accept = (nb >= lmin_of(nib)) && (nb <= lmax_of(nib));
check((n_packets == before_p + 1) == exp_accept,
"a packet was accepted or rejected against this PID's length rule");
check((n_errors == before_e + 1) == !exp_accept,
"a rejected packet did not produce exactly one error");
end
end
// ---- Phase D: ONE ERROR PER CORRUPT PACKET, NOT ONE PER BYTE. ----
//
// The PID is corrupt, and then k more bytes arrive. A machine that
// returns straight to IDLE reports an error for each of them. The delta
// must be 1 for every k.
for (k = 0; k <= 12; k = k + 1) begin
before_e = n_errors;
step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0);
step(1'b0, 1'b1, 8'h00, 1'b0, 1'b0); // 0x00 fails the complement
for (it = 0; it < k; it = it + 1)
step(1'b0, 1'b1, (it*8'd29 + 8'd3), 1'b0, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0);
check(n_errors == before_e + 1,
"a single corrupt packet produced more than one error -- the machine returned to IDLE instead of swallowing the rest of the packet");
end
// ---- Phase E: RESYNCHRONISATION. After each error, the very next
// ---- well-formed packet must be accepted.
for (k = 0; k < 5; k = k + 1) begin
case (k)
0: begin // bad PID
step(1'b1,1'b0,8'd0,1'b0,1'b0); step(1'b0,1'b1,8'h00,1'b0,1'b0);
step(1'b0,1'b0,8'd0,1'b1,1'b0); step(1'b0,1'b0,8'd0,1'b0,1'b0);
end
1: send_pid(4'b0001, 1); // token, too short
2: send_pid(4'b0010, 3); // handshake with a body
3: begin // stray byte with no SYNC
step(1'b0,1'b1,8'h5a,1'b0,1'b0);
step(1'b0,1'b0,8'd0,1'b1,1'b0); step(1'b0,1'b0,8'd0,1'b0,1'b0);
end
4: begin // PHY error mid-packet
step(1'b1,1'b0,8'd0,1'b0,1'b0);
step(1'b0,1'b1,8'hc3,1'b0,1'b0); // DATA0
step(1'b0,1'b1,8'h11,1'b0,1'b0);
step(1'b0,1'b0,8'd0,1'b0,1'b1); // bus_error
step(1'b0,1'b0,8'd0,1'b1,1'b0); step(1'b0,1'b0,8'd0,1'b0,1'b0);
end
endcase
before_p = n_packets;
send_pid(4'b0001, 2); // a perfectly good OUT token
check(n_packets == before_p + 1,
"the next well-formed packet after an error was not accepted -- the machine did not resynchronise");
check(state === S_IDLE, "the machine did not return to IDLE");
end
// ---- Phase F: BOUNDED resynchronisation. The EOP itself is lost. ----
before_e = n_timeouts;
step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0);
step(1'b0, 1'b1, 8'h00, 1'b0, 1'b0); // corrupt PID, no EOP will follow
quiet(ABORT_MAX + 3);
check(state === S_IDLE,
"the machine did not resynchronise after the abort timeout -- a lost EOP is a permanent hang");
check(n_timeouts == before_e + 1, "the abort timeout was not counted");
before_p = n_packets;
send_pid(4'b1001, 2); // an IN token
check(n_packets == before_p + 1,
"the machine did not accept a packet after timing out of an abort");
// ---- Phase G: EXHAUSTIVE. Every state x all 16 input combinations. ----
for (k = 0; k < 5; k = k + 1) begin
for (b = 0; b < 16; b = b + 1) begin
// An end of packet returns the machine to IDLE from ANY state --
// that is what "every error resynchronises" buys -- so each sweep
// entry starts from the same place without any state being forced.
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
quiet(ABORT_MAX + 3);
check(state === S_IDLE, "an end of packet did not return the machine to IDLE");
case (k)
0: ; // IDLE
1: step(1'b1,1'b0,8'd0,1'b0,1'b0); // PID
2: begin step(1'b1,1'b0,8'd0,1'b0,1'b0);
step(1'b0,1'b1,8'he1,1'b0,1'b0); end // BODY (OUT)
3: begin step(1'b1,1'b0,8'd0,1'b0,1'b0);
step(1'b0,1'b1,8'hd2,1'b0,1'b0); end // HSHK (ACK)
4: begin step(1'b1,1'b0,8'd0,1'b0,1'b0);
step(1'b0,1'b1,8'h00,1'b0,1'b0); end // ABORT
endcase
step(b[0], b[1], (b*8'd37 + 8'd5), b[2], b[3]);
step(b[0], b[1], (b*8'd37 + 8'd6), b[2], b[3]);
quiet(2);
end
end
// ---- Phase H: a random stream with corruption injected ----
for (it = 0; it < 26000; it = it + 1)
step(($unsigned($random) % 100) < 12,
($unsigned($random) % 100) < 46,
$random,
($unsigned($random) % 100) < 14,
($unsigned($random) % 1000) < 9);
// ---- Phase H left the machine wherever the random stream happened to
// ---- stop -- very likely in the middle of a packet. Put it back in
// ---- IDLE the way a real receiver does, with an end of packet and a
// ---- quiet line, rather than by forcing the state. A suite that
// ---- forces it here would be hiding the very property under test.
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
quiet(ABORT_MAX + 3);
check(state === S_IDLE, "the machine did not return to IDLE after the random phase");
// ---- Phase I: long runs of well-formed traffic, so the machine is
// ---- shown to keep working and not merely to fail safely
for (it = 0; it < 500; it = it + 1) begin
nib = ($unsigned($random) % 15) + 1;
nb = lmin_of(nib) + ($unsigned($random) % (lmax_of(nib) - lmin_of(nib) + 1));
before_p = n_packets;
send_pid(nib, nb);
check(n_packets == before_p + 1,
"a well-formed packet was rejected");
if (($unsigned($random) % 100) < 25) begin
before_e = n_errors;
send_raw(($unsigned($random) % 256), 2);
quiet(2);
end
end
// ---- Final agreement, reach, and the numbers that matter ----
check(n_packets === m_pkts[31:0], "n_packets disagrees with the model");
check(n_errors === m_errs[31:0], "n_errors disagrees with the model");
check(n_pid_err === m_pid_e[31:0], "n_pid_err disagrees with the model");
check(n_short === m_short[31:0], "n_short disagrees with the model");
check(n_long === m_long[31:0], "n_long disagrees with the model");
check(n_stray === m_stray[31:0], "n_stray disagrees with the model");
check(n_bus_err === m_bus[31:0], "n_bus_err disagrees with the model");
check(n_timeouts === m_to[31:0], "n_timeouts disagrees with the model");
// The per-cause counters must sum to the total. They are driven by the
// same pulse, so a discrepancy means a cause was mis-classified.
check(n_errors === n_pid_err + n_short + n_long + n_stray + n_bus_err,
"the error causes do not sum to the error total -- an error was mis-classified");
check(n_seen == 80, "not every state was crossed with every input combination");
check(n_packets > 32'd0, "no packet was ever accepted");
check(n_pid_err > 32'd0, "no PID was ever rejected");
check(n_short > 32'd0, "no short packet was ever seen");
check(n_long > 32'd0, "no over-long packet was ever seen");
check(n_stray > 32'd0, "no stray byte was ever seen");
check(n_bus_err > 32'd0, "no PHY error was ever seen");
check(n_timeouts > 32'd0, "the abort timeout was never exercised");
$display("REACH state-x-input=%0d/80 legal-PIDs=%0d/256 transitions=%0d",
n_seen, n_pid_ok, n_transitions);
$display("COUNTERS packets=%0d errors=%0d pid=%0d short=%0d long=%0d stray=%0d bus=%0d timeouts=%0d",
n_packets, n_errors, n_pid_err, n_short, n_long, n_stray,
n_bus_err, n_timeouts);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule11.2 SystemVerilog testbench
// Testbench for usb_packet_fsm (SystemVerilog).
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. ALL 256 POSSIBLE PID BYTES. Not the 16 legal ones and a few
// neighbours -- every value a corrupted line can produce. Exactly 15
// of them must be accepted as an identifier, and the suite counts.
//
// 2. Every legal PID crossed with every body length from 0 to MAXLEN+2,
// so each PID's length rule is checked at both its boundaries and one
// past each of them.
//
// 3. Every state crossed with all 16 combinations of
// {sync, byte_valid, eop, bus_error}, with each state REACHED by a
// real packet rather than forced.
//
// AND THE PROPERTY THAT IS NOT A SWEEP
//
// ONE ERROR PER CORRUPT PACKET, NOT ONE PER BYTE. A machine that returns
// straight to IDLE after an error reports an error for every remaining byte
// of the packet, and the check for that is a DELTA across a whole packet
// with a varying number of trailing bytes -- which no single-cycle
// comparison can express.
`timescale 1ns/1ps
module tb_pf_sv;
import usb_pktfsm_pkg::*;
localparam int MAXLEN = 8;
localparam int ABORT_MAX = 16;
logic clk = 1'b0, rst_n = 1'b0;
logic sync = 1'b0, byte_valid = 1'b0, eop = 1'b0, bus_error = 1'b0;
logic [7:0] byte_data = 8'd0;
pkt_state_e state;
pkt_err_e err_code;
logic [3:0] pid, exp_min, exp_max, byte_count;
logic [1:0] pid_class;
logic [4:0] abort_age;
logic in_packet, pkt_valid, pkt_error;
logic [31:0] n_packets, n_errors, n_pid_err, n_short, n_long, n_stray,
n_bus_err, n_timeouts;
usb_packet_fsm #(.MAXLEN(MAXLEN), .ABORT_MAX(ABORT_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 pid=%h cnt=%0d err=%0d age=%0d",
$time, msg, state, pid, byte_count, err_code, abort_age);
end
endtask
// ------------------------------------------------------------------
// The length rules, written INDEPENDENTLY of the design's function.
// ------------------------------------------------------------------
function automatic logic [3:0] lmin_of(input logic [3:0] p);
if (p[1:0] == 2'b01) return 4'd2; // token
else if (p[1:0] == 2'b11) return 4'd2; // data: CRC16 at minimum
else if (p[1:0] == 2'b10) return 4'd0; // handshake
else if (p == 4'b0100) return 4'd2; // PING
else if (p == 4'b1000) return 4'd3; // SPLIT
else return 4'd0; // PRE/ERR, and reserved
endfunction
function automatic logic [3:0] lmax_of(input logic [3:0] p);
if (p[1:0] == 2'b01) return 4'd2;
else if (p[1:0] == 2'b11) return 4'(MAXLEN);
else if (p[1:0] == 2'b10) return 4'd0;
else if (p == 4'b0100) return 4'd2;
else if (p == 4'b1000) return 4'd3;
else return 4'd0;
endfunction
function automatic logic pid_legal(input logic [7:0] b);
return (b[7:4] == ~b[3:0]) && (b[3:0] != 4'd0);
endfunction
// ------------------------------------------------------------------
// The shadow machine.
// ------------------------------------------------------------------
pkt_state_e m_st;
pkt_err_e m_err;
logic [3:0] m_pid, m_cnt;
logic [4:0] m_age;
logic m_ok, m_errp;
int m_pkts, m_errs, m_pid_e, m_short, m_long, m_stray, m_bus, m_to;
int seen [80]; // 5 states x 16 input combinations
int n_seen, n_transitions;
task automatic model_reset();
m_st = S_IDLE; m_err = E_NONE; m_pid = '0; m_cnt = '0; m_age = '0;
m_ok = 1'b0; m_errp = 1'b0;
m_pkts = 0; m_errs = 0; m_pid_e = 0; m_short = 0;
m_long = 0; m_stray = 0; m_bus = 0; m_to = 0;
foreach (seen[i]) seen[i] = 0;
n_seen = 0; n_transitions = 0;
endtask
int combo_idx;
task automatic step(input logic sy, input logic bv, input logic [7:0] dat,
input logic ep, input logic be);
pkt_state_e ns;
pkt_err_e ne;
logic [3:0] np, nc, ib;
logic [4:0] na;
logic no, nep, nto;
begin
sync = sy; byte_valid = bv; byte_data = dat; eop = ep; bus_error = be;
#1;
// ---- Everything the DUT holds must match the model, every cycle. ----
check(state === m_st, "state disagrees with the shadow machine");
check(pid === m_pid, "pid disagrees");
check(byte_count === m_cnt, "byte_count disagrees");
check(abort_age === m_age, "abort_age disagrees -- resynchronisation is not bounded the way the model says");
check(err_code === m_err, "err_code disagrees");
check(pkt_valid === m_ok, "pkt_valid disagrees");
check(pkt_error === m_errp, "pkt_error disagrees");
check(in_packet === ((m_st==S_PID)||(m_st==S_BODY)||(m_st==S_HSHK)),
"in_packet disagrees with the state it summarises");
check(exp_min === lmin_of(m_pid), "exp_min disagrees with the PID's length rule");
check(exp_max === lmax_of(m_pid), "exp_max disagrees with the PID's length rule");
// ---- Invariants that hold regardless of stimulus ----
check(!(pkt_valid && pkt_error),
"a packet was reported both good and bad in the same cycle");
check((m_st == S_IDLE) || (m_st == S_PID) || (m_st == S_BODY) ||
(m_st == S_HSHK) || (m_st == S_ABORT),
"the machine left its own state space");
check(!((m_st == S_BODY) && (m_cnt > lmax_of(m_pid))),
"more bytes were collected than this PID permits");
check(abort_age <= ABORT_MAX[4:0],
"the abort timer ran past its bound -- resynchronisation is unbounded");
check(!((m_st != S_ABORT) && (abort_age != 5'd0)),
"the abort timer is running outside S_ABORT");
combo_idx = int'(m_st) * 16 + int'({sy, bv, ep, be});
if (seen[combo_idx] == 0) begin seen[combo_idx] = 1; n_seen = n_seen + 1; end
n_transitions = n_transitions + 1;
// ---- advance the shadow machine ----
ns = m_st; ne = m_err; np = m_pid; nc = m_cnt; na = '0;
no = 1'b0; nep = 1'b0; nto = 1'b0;
ib = dat[3:0];
if (m_st == S_IDLE) begin
if (sy) begin ns = S_PID; nc = '0; ne = E_NONE; end
else if (bv) begin ns = S_ABORT; ne = E_STRAY; nep = 1'b1; end
end else if (m_st == S_PID) begin
if (be) begin ns = S_ABORT; ne = E_BUS; nep = 1'b1; end
else if (ep) begin ns = S_IDLE; ne = E_SHORT; nep = 1'b1; end
else if (bv) begin
if (!pid_legal(dat)) begin ns = S_ABORT; ne = E_PID; nep = 1'b1; end
else begin
np = ib; nc = '0;
if (lmax_of(ib) == 4'd0) ns = S_HSHK; else ns = S_BODY;
end
end
end else if (m_st == S_BODY) begin
if (be) begin ns = S_ABORT; ne = E_BUS; nep = 1'b1; end
else if (bv) begin
if (m_cnt >= lmax_of(m_pid)) begin ns = S_ABORT; ne = E_LONG; nep = 1'b1; end
else nc = m_cnt + 4'd1;
end else if (ep) begin
if (m_cnt >= lmin_of(m_pid)) begin ns = S_IDLE; no = 1'b1; ne = E_NONE; end
else begin ns = S_IDLE; ne = E_SHORT; nep = 1'b1; end
end
end else if (m_st == S_HSHK) begin
if (be) begin ns = S_ABORT; ne = E_BUS; nep = 1'b1; end
else if (bv) begin ns = S_ABORT; ne = E_LONG; nep = 1'b1; end
else if (ep) begin ns = S_IDLE; no = 1'b1; ne = E_NONE; end
end else begin // S_ABORT: interpret nothing
if (ep) ns = S_IDLE;
else if (m_age >= ABORT_MAX[4:0] - 5'd1) begin ns = S_IDLE; nto = 1'b1; end
else na = m_age + 5'd1;
end
m_st = ns; m_err = ne; m_pid = np; m_cnt = nc; m_age = na;
m_ok = no; m_errp = nep;
if (no) m_pkts++;
if (nto) m_to++;
if (nep) begin
m_errs++;
case (ne)
E_PID: m_pid_e++;
E_SHORT: m_short++;
E_LONG: m_long++;
E_STRAY: m_stray++;
E_BUS: m_bus++;
default: ;
endcase
end
@(posedge clk); #1;
sync = 1'b0; byte_valid = 1'b0; eop = 1'b0; bus_error = 1'b0;
end
endtask
task automatic quiet(input int n);
repeat (n) step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0);
endtask
// Send SYNC, an arbitrary PID byte, nb body bytes, then EOP -- and one
// quiet cycle so the registered pkt_valid / pkt_error pulse is observed.
task automatic send_raw(input logic [7:0] pb, input int nb);
step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0);
step(1'b0, 1'b1, pb, 1'b0, 1'b0);
for (int i = 0; i < nb; i++)
step(1'b0, 1'b1, 8'(i*13 + 7), 1'b0, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0);
endtask
task automatic send_pid(input logic [3:0] nib, input int nb);
send_raw({~nib, nib}, nb);
endtask
int b, nb, k, it, n_pid_ok, exp_pid_ok;
int before_p, before_e;
logic [3:0] nib;
logic exp_accept;
initial begin
model_reset();
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(state === S_IDLE, "reset did not land in IDLE");
check(in_packet === 1'b0, "reset asserted in_packet");
check(err_code === E_NONE, "reset left an error code set");
check(n_errors === 32'd0, "reset left the error counter non-zero");
// ---- Phase B: ALL 256 PID BYTES. ----
//
// Each is sent with no body at all, so the outcome separates cleanly:
// a byte that fails the complement check is E_PID; a byte that passes
// it but needs a body is E_SHORT; a handshake is a complete packet.
n_pid_ok = 0; exp_pid_ok = 0;
for (b = 0; b < 256; b++) begin
before_e = n_pid_err;
send_raw(8'(b), 0);
if (n_pid_err == before_e) n_pid_ok++;
if (pid_legal(8'(b))) exp_pid_ok++;
check((n_pid_err == before_e) == pid_legal(8'(b)),
"a PID byte was accepted or rejected against the complement rule");
end
check(n_pid_ok == exp_pid_ok, "the set of accepted PID bytes is not the legal set");
check(n_pid_ok == 15,
"exactly 15 of the 256 possible bytes are a legal packet identifier: 16 satisfy the complement and one of those is reserved");
// ---- Phase C: every legal PID x every body length 0..MAXLEN+2 ----
for (b = 1; b < 16; b++) begin
nib = 4'(b);
for (nb = 0; nb <= MAXLEN + 2; nb++) begin
before_p = n_packets;
before_e = n_errors;
send_pid(nib, nb);
exp_accept = (nb >= lmin_of(nib)) && (nb <= lmax_of(nib));
check((n_packets == before_p + 1) == exp_accept,
"a packet was accepted or rejected against this PID's length rule");
check((n_errors == before_e + 1) == !exp_accept,
"a rejected packet did not produce exactly one error");
end
end
// ---- Phase D: ONE ERROR PER CORRUPT PACKET, NOT ONE PER BYTE. ----
//
// The PID is corrupt, and then k more bytes arrive. A machine that
// returns straight to IDLE reports an error for each of them. The delta
// must be 1 for every k.
for (k = 0; k <= 12; k++) begin
before_e = n_errors;
step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0);
step(1'b0, 1'b1, 8'h00, 1'b0, 1'b0); // 0x00 fails the complement
for (it = 0; it < k; it++)
step(1'b0, 1'b1, 8'(it*29 + 3), 1'b0, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0);
check(n_errors == before_e + 1,
"a single corrupt packet produced more than one error -- the machine returned to IDLE instead of swallowing the rest of the packet");
end
// ---- Phase E: RESYNCHRONISATION. After each error, the very next
// ---- well-formed packet must be accepted.
for (k = 0; k < 5; k++) begin
case (k)
0: begin // bad PID
step(1'b1,1'b0,8'd0,1'b0,1'b0); step(1'b0,1'b1,8'h00,1'b0,1'b0);
step(1'b0,1'b0,8'd0,1'b1,1'b0); step(1'b0,1'b0,8'd0,1'b0,1'b0);
end
1: send_pid(4'b0001, 1); // token, too short
2: send_pid(4'b0010, 3); // handshake with a body
3: begin // stray byte with no SYNC
step(1'b0,1'b1,8'h5a,1'b0,1'b0);
step(1'b0,1'b0,8'd0,1'b1,1'b0); step(1'b0,1'b0,8'd0,1'b0,1'b0);
end
4: begin // PHY error mid-packet
step(1'b1,1'b0,8'd0,1'b0,1'b0);
step(1'b0,1'b1,8'hc3,1'b0,1'b0); // DATA0
step(1'b0,1'b1,8'h11,1'b0,1'b0);
step(1'b0,1'b0,8'd0,1'b0,1'b1); // bus_error
step(1'b0,1'b0,8'd0,1'b1,1'b0); step(1'b0,1'b0,8'd0,1'b0,1'b0);
end
endcase
before_p = n_packets;
send_pid(4'b0001, 2); // a perfectly good OUT token
check(n_packets == before_p + 1,
"the next well-formed packet after an error was not accepted -- the machine did not resynchronise");
check(state === S_IDLE, "the machine did not return to IDLE");
end
// ---- Phase F: BOUNDED resynchronisation. The EOP itself is lost. ----
before_e = n_timeouts;
step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0);
step(1'b0, 1'b1, 8'h00, 1'b0, 1'b0); // corrupt PID, no EOP will follow
quiet(ABORT_MAX + 3);
check(state === S_IDLE,
"the machine did not resynchronise after the abort timeout -- a lost EOP is a permanent hang");
check(n_timeouts == before_e + 1, "the abort timeout was not counted");
before_p = n_packets;
send_pid(4'b1001, 2); // an IN token
check(n_packets == before_p + 1,
"the machine did not accept a packet after timing out of an abort");
// ---- Phase G: EXHAUSTIVE. Every state x all 16 input combinations. ----
for (k = 0; k < 5; k++) begin
for (b = 0; b < 16; b++) begin
// An end of packet returns the machine to IDLE from ANY state --
// that is what "every error resynchronises" buys -- so each sweep
// entry starts from the same place without any state being forced.
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
quiet(ABORT_MAX + 3);
check(state === S_IDLE, "an end of packet did not return the machine to IDLE");
case (k)
0: ; // IDLE
1: step(1'b1,1'b0,8'd0,1'b0,1'b0); // PID
2: begin step(1'b1,1'b0,8'd0,1'b0,1'b0);
step(1'b0,1'b1,8'he1,1'b0,1'b0); end // BODY (OUT)
3: begin step(1'b1,1'b0,8'd0,1'b0,1'b0);
step(1'b0,1'b1,8'hd2,1'b0,1'b0); end // HSHK (ACK)
4: begin step(1'b1,1'b0,8'd0,1'b0,1'b0);
step(1'b0,1'b1,8'h00,1'b0,1'b0); end // ABORT
endcase
step(b[0], b[1], 8'(b*37 + 5), b[2], b[3]);
step(b[0], b[1], 8'(b*37 + 6), b[2], b[3]);
quiet(2);
end
end
// ---- Phase H: a random stream with corruption injected ----
for (it = 0; it < 26000; it++)
step($urandom_range(0,99) < 12,
$urandom_range(0,99) < 46,
8'($urandom()),
$urandom_range(0,99) < 14,
$urandom_range(0,999) < 9);
// ---- Phase H left the machine wherever the random stream happened to
// ---- stop -- very likely in the middle of a packet. Put it back in
// ---- IDLE the way a real receiver does, with an end of packet and a
// ---- quiet line, rather than by forcing the state. A suite that
// ---- forces it here would be hiding the very property under test.
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
quiet(ABORT_MAX + 3);
check(state === S_IDLE, "the machine did not return to IDLE after the random phase");
// ---- Phase I: long runs of well-formed traffic, so the machine is
// ---- shown to keep working and not merely to fail safely
for (it = 0; it < 500; it++) begin
nib = 4'($urandom_range(1, 15));
nb = int'(lmin_of(nib)) + $urandom_range(0, int'(lmax_of(nib)) - int'(lmin_of(nib)));
before_p = n_packets;
send_pid(nib, nb);
check(n_packets == before_p + 1,
"a well-formed packet was rejected");
if ($urandom_range(0,99) < 25) begin
before_e = n_errors;
send_raw(8'($urandom()), 2);
quiet(2);
end
end
// ---- Final agreement, reach, and the numbers that matter ----
check(n_packets === 32'(m_pkts), "n_packets disagrees with the model");
check(n_errors === 32'(m_errs), "n_errors disagrees with the model");
check(n_pid_err === 32'(m_pid_e), "n_pid_err disagrees with the model");
check(n_short === 32'(m_short), "n_short disagrees with the model");
check(n_long === 32'(m_long), "n_long disagrees with the model");
check(n_stray === 32'(m_stray), "n_stray disagrees with the model");
check(n_bus_err === 32'(m_bus), "n_bus_err disagrees with the model");
check(n_timeouts === 32'(m_to), "n_timeouts disagrees with the model");
// The per-cause counters must sum to the total. They are driven by the
// same pulse, so a discrepancy means a cause was mis-classified.
check(n_errors === n_pid_err + n_short + n_long + n_stray + n_bus_err,
"the error causes do not sum to the error total -- an error was mis-classified");
check(n_seen == 80, "not every state was crossed with every input combination");
check(n_packets > 32'd0, "no packet was ever accepted");
check(n_pid_err > 32'd0, "no PID was ever rejected");
check(n_short > 32'd0, "no short packet was ever seen");
check(n_long > 32'd0, "no over-long packet was ever seen");
check(n_stray > 32'd0, "no stray byte was ever seen");
check(n_bus_err > 32'd0, "no PHY error was ever seen");
check(n_timeouts > 32'd0, "the abort timeout was never exercised");
$display("REACH state-x-input=%0d/80 legal-PIDs=%0d/256 transitions=%0d",
n_seen, n_pid_ok, n_transitions);
$display("COUNTERS packets=%0d errors=%0d pid=%0d short=%0d long=%0d stray=%0d bus=%0d timeouts=%0d",
n_packets, n_errors, n_pid_err, n_short, n_long, n_stray,
n_bus_err, n_timeouts);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule11.3 VHDL testbench
-- Testbench for usb_packet_fsm (VHDL-2008).
--
-- WHAT IS EXHAUSTIVE HERE
--
-- 1. ALL 256 POSSIBLE PID BYTES. Not the 16 legal ones and a few
-- neighbours -- every value a corrupted line can produce. Exactly 15
-- of them must be accepted as an identifier, and the suite counts.
--
-- 2. Every legal PID crossed with every body length from 0 to MAXLEN+2,
-- so each PID's length rule is checked at both its boundaries and one
-- past each of them.
--
-- 3. Every state crossed with all 16 combinations of
-- (sync, byte_valid, eop, bus_error), with each state REACHED by a
-- real packet rather than forced.
--
-- AND THE PROPERTY THAT IS NOT A SWEEP
--
-- ONE ERROR PER CORRUPT PACKET, NOT ONE PER BYTE. A machine that returns
-- straight to IDLE after an error reports an error for every remaining byte
-- of the packet, and the check for that is a DELTA across a whole packet
-- with a varying number of trailing bytes -- which no single-cycle
-- comparison can express.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_pktfsm_pkg.all;
entity tb_pf_vhdl is
end entity tb_pf_vhdl;
architecture sim of tb_pf_vhdl is
constant MAXLEN : integer := 8;
constant ABORT_MAX : integer := 16;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal sync, byte_valid, eop, bus_error : std_logic := '0';
signal byte_data : std_logic_vector(7 downto 0) := (others => '0');
signal state, err_code : std_logic_vector(2 downto 0);
signal pid, exp_min, exp_max, byte_count : std_logic_vector(3 downto 0);
signal pid_class : std_logic_vector(1 downto 0);
signal abort_age : std_logic_vector(4 downto 0);
signal in_packet, pkt_valid, pkt_error : std_logic;
signal n_packets, n_errors, n_pid_err, n_short : std_logic_vector(31 downto 0);
signal n_long, n_stray, n_bus_err, n_timeouts : std_logic_vector(31 downto 0);
begin
dut : entity work.usb_packet_fsm
generic map (MAXLEN => MAXLEN, ABORT_MAX => ABORT_MAX)
port map (
clk => clk, rst_n => rst_n,
sync => sync, byte_valid => byte_valid, byte_data => byte_data,
eop => eop, bus_error => bus_error,
state => state, pid => pid, pid_class => pid_class,
exp_min => exp_min, exp_max => exp_max, byte_count => byte_count,
in_packet => in_packet, pkt_valid => pkt_valid, pkt_error => pkt_error,
err_code => err_code, abort_age => abort_age,
n_packets => n_packets, n_errors => n_errors, n_pid_err => n_pid_err,
n_short => n_short, n_long => n_long, n_stray => n_stray,
n_bus_err => n_bus_err, n_timeouts => n_timeouts
);
clk <= (not clk) after 5 ns when not done else '0';
stim : process
type seen_arr is array (0 to 79) of integer;
variable errors, checks : integer := 0;
-- ---- The shadow machine ----
variable m_st : pkt_state_t := S_IDLE;
variable m_err : pkt_err_t := E_NONE;
variable m_pid : std_logic_vector(3 downto 0) := (others => '0');
variable m_cnt : unsigned(3 downto 0) := (others => '0');
variable m_age : unsigned(4 downto 0) := (others => '0');
variable m_ok, m_errp : std_logic := '0';
variable m_pkts, m_errs, m_pid_e, m_short : integer := 0;
variable m_long, m_stray, m_bus, m_to : integer := 0;
variable seen : seen_arr := (others => 0);
variable n_seen, n_transitions : integer := 0;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"2468BDF1";
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 v : unsigned(31 downto 0);
begin
v := rnd32;
return to_integer(v(29 downto 0));
end function;
impure function rnd_byte return std_logic_vector is
variable v : unsigned(31 downto 0);
begin
v := rnd32;
return std_logic_vector(v(7 downto 0));
end function;
impure function rnd_lt (pct, base : integer) return std_logic is
begin
if (rnd_nat mod base) < pct then return '1'; else return '0'; end if;
end function;
-- ---- The length rules, written INDEPENDENTLY of the design's function.
function lmin_of (p : std_logic_vector(3 downto 0)) return integer is
begin
if p(1 downto 0) = "01" then return 2; -- token
elsif p(1 downto 0) = "11" then return 2; -- data: CRC16 at minimum
elsif p(1 downto 0) = "10" then return 0; -- handshake
elsif p = "0100" then return 2; -- PING
elsif p = "1000" then return 3; -- SPLIT
else return 0; -- PRE/ERR, and reserved
end if;
end function;
function lmax_of (p : std_logic_vector(3 downto 0)) return integer is
begin
if p(1 downto 0) = "01" then return 2;
elsif p(1 downto 0) = "11" then return MAXLEN;
elsif p(1 downto 0) = "10" then return 0;
elsif p = "0100" then return 2;
elsif p = "1000" then return 3;
else return 0;
end if;
end function;
function pid_legal (b : std_logic_vector(7 downto 0)) return boolean is
begin
return (b(7 downto 4) = (not b(3 downto 0))) and (b(3 downto 0) /= "0000");
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(pkt_state_t'pos(m_st)) &
" cnt=" & integer'image(to_integer(m_cnt)) &
" err=" & integer'image(pkt_err_t'pos(m_err)) &
" age=" & integer'image(to_integer(m_age))
severity note;
end if;
end if;
end procedure;
procedure step (sy, bv : std_logic; dat : std_logic_vector(7 downto 0);
ep, be : std_logic) is
variable nst : pkt_state_t;
variable ne : pkt_err_t;
variable np : std_logic_vector(3 downto 0);
variable nc : unsigned(3 downto 0);
variable na : unsigned(4 downto 0);
variable no, nep, nto : std_logic;
variable ci : integer;
variable cb : integer;
begin
sync <= sy; byte_valid <= bv; byte_data <= dat;
eop <= ep; bus_error <= be;
wait for 1 ns;
-- ---- Everything the DUT holds must match the model, every cycle. ----
chk(state = st_code(m_st), "state disagrees with the shadow machine");
chk(pid = m_pid, "pid disagrees");
chk(byte_count = std_logic_vector(m_cnt), "byte_count disagrees");
chk(abort_age = std_logic_vector(m_age),
"abort_age disagrees -- resynchronisation is not bounded the way the model says");
chk(err_code = er_code(m_err), "err_code disagrees");
chk(pkt_valid = m_ok, "pkt_valid disagrees");
chk(pkt_error = m_errp, "pkt_error disagrees");
chk((in_packet = '1') = (m_st = S_PID or m_st = S_BODY or m_st = S_HSHK),
"in_packet disagrees with the state it summarises");
chk(to_integer(unsigned(exp_min)) = lmin_of(m_pid),
"exp_min disagrees with the PID's length rule");
chk(to_integer(unsigned(exp_max)) = lmax_of(m_pid),
"exp_max disagrees with the PID's length rule");
-- ---- Invariants that hold regardless of stimulus ----
chk(not (pkt_valid = '1' and pkt_error = '1'),
"a packet was reported both good and bad in the same cycle");
chk(not (m_st = S_BODY and to_integer(m_cnt) > lmax_of(m_pid)),
"more bytes were collected than this PID permits");
chk(to_integer(unsigned(abort_age)) <= ABORT_MAX,
"the abort timer ran past its bound -- resynchronisation is unbounded");
chk(not (m_st /= S_ABORT and unsigned(abort_age) /= 0),
"the abort timer is running outside S_ABORT");
cb := 0;
if sy = '1' then cb := cb + 8; end if;
if bv = '1' then cb := cb + 4; end if;
if ep = '1' then cb := cb + 2; end if;
if be = '1' then cb := cb + 1; end if;
ci := pkt_state_t'pos(m_st) * 16 + cb;
if seen(ci) = 0 then seen(ci) := 1; n_seen := n_seen + 1; end if;
n_transitions := n_transitions + 1;
-- ---- advance the shadow machine ----
nst := m_st; ne := m_err; np := m_pid; nc := m_cnt;
na := (others => '0');
no := '0'; nep := '0'; nto := '0';
case m_st is
when S_IDLE =>
if sy = '1' then
nst := S_PID; nc := (others => '0'); ne := E_NONE;
elsif bv = '1' then
nst := S_ABORT; ne := E_STRAY; nep := '1';
end if;
when S_PID =>
if be = '1' then
nst := S_ABORT; ne := E_BUS; nep := '1';
elsif ep = '1' then
nst := S_IDLE; ne := E_SHORT; nep := '1';
elsif bv = '1' then
if not pid_legal(dat) then
nst := S_ABORT; ne := E_PID; nep := '1';
else
np := dat(3 downto 0); nc := (others => '0');
if lmax_of(dat(3 downto 0)) = 0 then nst := S_HSHK;
else nst := S_BODY;
end if;
end if;
end if;
when S_BODY =>
if be = '1' then
nst := S_ABORT; ne := E_BUS; nep := '1';
elsif bv = '1' then
if to_integer(m_cnt) >= lmax_of(m_pid) then
nst := S_ABORT; ne := E_LONG; nep := '1';
else
nc := m_cnt + 1;
end if;
elsif ep = '1' then
if to_integer(m_cnt) >= lmin_of(m_pid) then
nst := S_IDLE; no := '1'; ne := E_NONE;
else
nst := S_IDLE; ne := E_SHORT; nep := '1';
end if;
end if;
when S_HSHK =>
if be = '1' then
nst := S_ABORT; ne := E_BUS; nep := '1';
elsif bv = '1' then
nst := S_ABORT; ne := E_LONG; nep := '1';
elsif ep = '1' then
nst := S_IDLE; no := '1'; ne := E_NONE;
end if;
when S_ABORT => -- interpret nothing
if ep = '1' then
nst := S_IDLE;
elsif to_integer(m_age) >= ABORT_MAX - 1 then
nst := S_IDLE; nto := '1';
else
na := m_age + 1;
end if;
end case;
m_st := nst; m_err := ne; m_pid := np; m_cnt := nc; m_age := na;
m_ok := no; m_errp := nep;
if no = '1' then m_pkts := m_pkts + 1; end if;
if nto = '1' then m_to := m_to + 1; end if;
if nep = '1' then
m_errs := m_errs + 1;
case ne is
when E_PID => m_pid_e := m_pid_e + 1;
when E_SHORT => m_short := m_short + 1;
when E_LONG => m_long := m_long + 1;
when E_STRAY => m_stray := m_stray + 1;
when E_BUS => m_bus := m_bus + 1;
when others => null;
end case;
end if;
wait until rising_edge(clk);
wait for 1 ns;
sync <= '0'; byte_valid <= '0'; eop <= '0'; bus_error <= '0';
end procedure;
procedure quiet (n : integer) is
begin
for i in 1 to n loop
step('0', '0', x"00", '0', '0');
end loop;
end procedure;
-- Send SYNC, an arbitrary PID byte, nb body bytes, then EOP -- and one
-- quiet cycle so the registered pkt_valid / pkt_error pulse is observed.
procedure send_raw (pb : std_logic_vector(7 downto 0); nb : integer) is
begin
step('1', '0', x"00", '0', '0');
step('0', '1', pb, '0', '0');
for i in 0 to nb - 1 loop
step('0', '1', std_logic_vector(to_unsigned((i*13 + 7) mod 256, 8)), '0', '0');
end loop;
step('0', '0', x"00", '1', '0');
step('0', '0', x"00", '0', '0');
end procedure;
procedure send_pid (nib : std_logic_vector(3 downto 0); nb : integer) is
begin
send_raw((not nib) & nib, nb);
end procedure;
variable n_pid_ok, exp_pid_ok : integer := 0;
variable before_p, before_e : integer := 0;
variable nib : std_logic_vector(3 downto 0);
variable nb : integer;
variable exp_accept : boolean;
variable sy_v, bv_v, ep_v, be_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 = st_code(S_IDLE), "reset did not land in IDLE");
chk(in_packet = '0', "reset asserted in_packet");
chk(err_code = er_code(E_NONE), "reset left an error code set");
chk(unsigned(n_errors) = 0, "reset left the error counter non-zero");
-- ---- Phase B: ALL 256 PID BYTES. ----
--
-- Each is sent with no body at all, so the outcome separates cleanly:
-- a byte that fails the complement check is E_PID; a byte that passes
-- it but needs a body is E_SHORT; a handshake is a complete packet.
for b in 0 to 255 loop
before_e := to_integer(unsigned(n_pid_err));
send_raw(std_logic_vector(to_unsigned(b, 8)), 0);
if to_integer(unsigned(n_pid_err)) = before_e then
n_pid_ok := n_pid_ok + 1;
end if;
if pid_legal(std_logic_vector(to_unsigned(b, 8))) then
exp_pid_ok := exp_pid_ok + 1;
end if;
chk((to_integer(unsigned(n_pid_err)) = before_e)
= pid_legal(std_logic_vector(to_unsigned(b, 8))),
"a PID byte was accepted or rejected against the complement rule");
end loop;
chk(n_pid_ok = exp_pid_ok, "the set of accepted PID bytes is not the legal set");
chk(n_pid_ok = 15,
"exactly 15 of the 256 possible bytes are a legal packet identifier: 16 satisfy the complement and one of those is reserved");
-- ---- Phase C: every legal PID x every body length 0..MAXLEN+2 ----
for b in 1 to 15 loop
nib := std_logic_vector(to_unsigned(b, 4));
for k in 0 to MAXLEN + 2 loop
before_p := to_integer(unsigned(n_packets));
before_e := to_integer(unsigned(n_errors));
send_pid(nib, k);
exp_accept := (k >= lmin_of(nib)) and (k <= lmax_of(nib));
chk((to_integer(unsigned(n_packets)) = before_p + 1) = exp_accept,
"a packet was accepted or rejected against this PID's length rule");
chk((to_integer(unsigned(n_errors)) = before_e + 1) = (not exp_accept),
"a rejected packet did not produce exactly one error");
end loop;
end loop;
-- ---- Phase D: ONE ERROR PER CORRUPT PACKET, NOT ONE PER BYTE. ----
--
-- The PID is corrupt, and then k more bytes arrive. A machine that
-- returns straight to IDLE reports an error for each of them. The delta
-- must be 1 for every k.
for k in 0 to 12 loop
before_e := to_integer(unsigned(n_errors));
step('1', '0', x"00", '0', '0');
step('0', '1', x"00", '0', '0'); -- 0x00 fails the complement
for i in 0 to k - 1 loop
step('0', '1', std_logic_vector(to_unsigned((i*29 + 3) mod 256, 8)), '0', '0');
end loop;
step('0', '0', x"00", '1', '0');
step('0', '0', x"00", '0', '0');
chk(to_integer(unsigned(n_errors)) = before_e + 1,
"a single corrupt packet produced more than one error -- the machine returned to IDLE instead of swallowing the rest of the packet");
end loop;
-- ---- Phase E: RESYNCHRONISATION. After each error, the very next
-- ---- well-formed packet must be accepted.
for k in 0 to 4 loop
case k is
when 0 => -- bad PID
step('1', '0', x"00", '0', '0');
step('0', '1', x"00", '0', '0');
step('0', '0', x"00", '1', '0');
step('0', '0', x"00", '0', '0');
when 1 => send_pid("0001", 1); -- token, too short
when 2 => send_pid("0010", 3); -- handshake with a body
when 3 => -- stray byte with no SYNC
step('0', '1', x"5a", '0', '0');
step('0', '0', x"00", '1', '0');
step('0', '0', x"00", '0', '0');
when others => -- PHY error mid-packet
step('1', '0', x"00", '0', '0');
step('0', '1', x"c3", '0', '0'); -- DATA0
step('0', '1', x"11", '0', '0');
step('0', '0', x"00", '0', '1'); -- bus_error
step('0', '0', x"00", '1', '0');
step('0', '0', x"00", '0', '0');
end case;
before_p := to_integer(unsigned(n_packets));
send_pid("0001", 2); -- a perfectly good OUT token
chk(to_integer(unsigned(n_packets)) = before_p + 1,
"the next well-formed packet after an error was not accepted -- the machine did not resynchronise");
chk(state = st_code(S_IDLE), "the machine did not return to IDLE");
end loop;
-- ---- Phase F: BOUNDED resynchronisation. The EOP itself is lost. ----
before_e := to_integer(unsigned(n_timeouts));
step('1', '0', x"00", '0', '0');
step('0', '1', x"00", '0', '0'); -- corrupt PID, no EOP follows
quiet(ABORT_MAX + 3);
chk(state = st_code(S_IDLE),
"the machine did not resynchronise after the abort timeout -- a lost EOP is a permanent hang");
chk(to_integer(unsigned(n_timeouts)) = before_e + 1,
"the abort timeout was not counted");
before_p := to_integer(unsigned(n_packets));
send_pid("1001", 2); -- an IN token
chk(to_integer(unsigned(n_packets)) = before_p + 1,
"the machine did not accept a packet after timing out of an abort");
-- ---- Phase G: EXHAUSTIVE. Every state x all 16 input combinations. ----
for k in 0 to 4 loop
for b in 0 to 15 loop
-- An end of packet returns the machine to IDLE from ANY state --
-- that is what "every error resynchronises" buys -- so each sweep
-- entry starts from the same place without any state being forced.
step('0', '0', x"00", '1', '0');
quiet(ABORT_MAX + 3);
chk(state = st_code(S_IDLE),
"an end of packet did not return the machine to IDLE");
case k is
when 0 => null; -- IDLE
when 1 => step('1', '0', x"00", '0', '0'); -- PID
when 2 => step('1', '0', x"00", '0', '0');
step('0', '1', x"e1", '0', '0'); -- BODY (OUT)
when 3 => step('1', '0', x"00", '0', '0');
step('0', '1', x"d2", '0', '0'); -- HSHK (ACK)
when others => step('1', '0', x"00", '0', '0');
step('0', '1', x"00", '0', '0'); -- ABORT
end case;
if (b / 8) mod 2 = 1 then sy_v := '1'; else sy_v := '0'; end if;
if (b / 4) mod 2 = 1 then bv_v := '1'; else bv_v := '0'; end if;
if (b / 2) mod 2 = 1 then ep_v := '1'; else ep_v := '0'; end if;
if b mod 2 = 1 then be_v := '1'; else be_v := '0'; end if;
step(sy_v, bv_v, std_logic_vector(to_unsigned((b*37 + 5) mod 256, 8)), ep_v, be_v);
step(sy_v, bv_v, std_logic_vector(to_unsigned((b*37 + 6) mod 256, 8)), ep_v, be_v);
quiet(2);
end loop;
end loop;
-- ---- Phase H: a random stream with corruption injected ----
for i in 0 to 25999 loop
sy_v := rnd_lt(12, 100);
bv_v := rnd_lt(46, 100);
ep_v := rnd_lt(14, 100);
be_v := rnd_lt(9, 1000);
step(sy_v, bv_v, rnd_byte, ep_v, be_v);
end loop;
-- ---- Phase H left the machine wherever the random stream happened to
-- ---- stop -- very likely in the middle of a packet. Put it back in
-- ---- IDLE the way a real receiver does, with an end of packet and a
-- ---- quiet line, rather than by forcing the state. A suite that
-- ---- forced it here would be hiding the very property under test.
step('0', '0', x"00", '1', '0');
quiet(ABORT_MAX + 3);
chk(state = st_code(S_IDLE),
"the machine did not return to IDLE after the random phase");
-- ---- Phase I: long runs of well-formed traffic, so the machine is
-- ---- shown to keep working and not merely to fail safely
for i in 0 to 499 loop
nib := std_logic_vector(to_unsigned((rnd_nat mod 15) + 1, 4));
nb := lmin_of(nib) + (rnd_nat mod (lmax_of(nib) - lmin_of(nib) + 1));
before_p := to_integer(unsigned(n_packets));
send_pid(nib, nb);
chk(to_integer(unsigned(n_packets)) = before_p + 1,
"a well-formed packet was rejected");
if rnd_lt(25, 100) = '1' then
send_raw(rnd_byte, 2);
quiet(2);
end if;
end loop;
-- ---- Final agreement, reach, and the numbers that matter ----
chk(to_integer(unsigned(n_packets)) = m_pkts, "n_packets disagrees with the model");
chk(to_integer(unsigned(n_errors)) = m_errs, "n_errors disagrees with the model");
chk(to_integer(unsigned(n_pid_err)) = m_pid_e, "n_pid_err disagrees with the model");
chk(to_integer(unsigned(n_short)) = m_short, "n_short disagrees with the model");
chk(to_integer(unsigned(n_long)) = m_long, "n_long disagrees with the model");
chk(to_integer(unsigned(n_stray)) = m_stray, "n_stray disagrees with the model");
chk(to_integer(unsigned(n_bus_err)) = m_bus, "n_bus_err disagrees with the model");
chk(to_integer(unsigned(n_timeouts)) = m_to, "n_timeouts disagrees with the model");
-- The per-cause counters must sum to the total. They are driven by the
-- same pulse, so a discrepancy means a cause was mis-classified.
chk(to_integer(unsigned(n_errors)) =
to_integer(unsigned(n_pid_err)) + to_integer(unsigned(n_short)) +
to_integer(unsigned(n_long)) + to_integer(unsigned(n_stray)) +
to_integer(unsigned(n_bus_err)),
"the error causes do not sum to the error total -- an error was mis-classified");
chk(n_seen = 80, "not every state was crossed with every input combination");
chk(to_integer(unsigned(n_packets)) > 0, "no packet was ever accepted");
chk(to_integer(unsigned(n_pid_err)) > 0, "no PID was ever rejected");
chk(to_integer(unsigned(n_short)) > 0, "no short packet was ever seen");
chk(to_integer(unsigned(n_long)) > 0, "no over-long packet was ever seen");
chk(to_integer(unsigned(n_stray)) > 0, "no stray byte was ever seen");
chk(to_integer(unsigned(n_bus_err)) > 0, "no PHY error was ever seen");
chk(to_integer(unsigned(n_timeouts)) > 0, "the abort timeout was never exercised");
write(ln, string'("REACH state-x-input=") & integer'image(n_seen) &
"/80 legal-PIDs=" & integer'image(n_pid_ok) &
"/256 transitions=" & integer'image(n_transitions));
writeline(output, ln);
write(ln, string'("COUNTERS packets=") & integer'image(to_integer(unsigned(n_packets))) &
" errors=" & integer'image(to_integer(unsigned(n_errors))) &
" pid=" & integer'image(to_integer(unsigned(n_pid_err))) &
" short=" & integer'image(to_integer(unsigned(n_short))) &
" long=" & integer'image(to_integer(unsigned(n_long))) &
" stray=" & integer'image(to_integer(unsigned(n_stray))) &
" bus=" & integer'image(to_integer(unsigned(n_bus_err))) &
" timeouts=" & integer'image(to_integer(unsigned(n_timeouts))));
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 input pairs | 80 / 80 | 80 / 80 | 80 / 80 |
| legal PID bytes found | 15 / 256 | 15 / 256 | 15 / 256 |
| Transitions | 34924 | 34857 | 34808 |
| Checks executed | 525076 | 524071 | 488528 |
| packets accepted | 568 | 569 | 565 |
| errors reported | 3642 | 3686 | 3578 |
| — bad PID | 881 | 863 | 868 |
| — short | 254 | 240 | 238 |
| — over-long | 136 | 127 | 129 |
| — stray byte | 2332 | 2413 | 2302 |
| — PHY error | 39 | 43 | 41 |
| abort timeouts | 258 | 259 | 266 |
| Result | PASS | PASS | PASS |
Two rows are the headline. 80 of 80 means there is no combination of inputs in any state that the suite has not applied. 15 of 256 is the self-checking property measured rather than asserted: the suite tried every byte a damaged line can produce and found exactly the fifteen that are legal — the sixteen that satisfy the complement, minus the reserved one.
The five cause rows sum to the error total in all three columns (881 + 254 + 136 + 2332 + 39 = 3642), which is checked programmatically, not by eye.
13. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| P3 | every error returns straight to IDLE — the error cascade | 98318 | 103949 | 102204 |
| P2 | the top nibble must repeat the bottom, not invert it | 93529 | 94176 | 97462 |
| P1 | the complement check is dropped; the PID is taken on trust | 82252 | 88370 | 86700 |
| P6 | the abort leaves on any byte, not only on EOP | 74822 | 74114 | 67481 |
| P7 | the abort timeout is removed — a lost EOP hangs the receiver | 15838 | 8258 | 10710 |
| P4 | the over-long check is off by one | 1404 | 1217 | 1150 |
| P5 | a packet that ended too early is accepted anyway | 270 | 264 | 270 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct.
P3 is the largest, which is the right shape for the error cascade: it does not fail once, it fails on every byte of every corrupt packet for the whole run. P1 and P2 are close behind and are the PID check — P2 scoring slightly higher than P1 because inverting the rule rejects the real PIDs as well as accepting the wrong ones, so it breaks good traffic and bad.
P5 is the smallest at ~270, and its smallness is informative rather than reassuring. Accepting a short packet fires only when a packet is actually short, which in a well-behaved stream is rare — so a suite that did not deliberately send under-length packets at every PID would score it near zero and conclude nothing was wrong. It is the length sweep in phase C, not the random traffic, that catches it.
P7's spread across languages is the widest in the table (15838 / 8258 / 10710). That is stimulus variance, not a broken mutation: removing the abort timeout wedges the receiver until the next EOP, so the score depends entirely on how long the three independent random streams happen to go without one. A mutation whose damage is unbounded per cycle produces scores that differ by how early it triggers, and comparing those columns tells you about the stimulus rather than about the design.
14. Debugging Walkthrough: The Transfer That Never Completes
The report. A USB device works perfectly on a short cable and fails to enumerate on a long one. The failure is not random: enumeration gets to the same step every time and stops. The host's log says the device did not answer a GET_DESCRIPTOR.
Step 1 — the long cable is a hint, not the cause. A longer cable means more bit errors. More bit errors means more corrupt packets. That is expected; USB is designed to retry them. What is not expected is that the retries do not work either.
Step 2 — trace the bus. The host sends GET_DESCRIPTOR. The device answers with a DATA0 packet that is truncated — the EOP arrives early. The host times out and retries. The retry goes out on the wire, complete and well-formed. The device does not answer.
Step 3 — the retry was well-formed. Confirm it byte by byte on the analyser: correct SYNC, correct PID, correct length, correct CRC. A good packet went in and nothing came out.
Step 4 — instrument the receiver. n_errors increments once for the truncated packet. It does not increment at all for the retry. The retry produced neither an error nor a packet.
Step 5 — what state was it in? S_ABORT, and it had been since the truncated packet. It entered S_ABORT because the packet was short, and it was waiting for an EOP — the EOP it had already consumed.
Step 6 — so the retry was swallowed. It sat in S_ABORT through the whole retry and left on the retry's own end-of-packet, by which time the host had timed out again. Every retry is eaten by the packet before it, and enumeration can never progress.
Step 7 — the fix. An error detected at the EOP goes straight to IDLE, because the packet is already over. Two lines.
15. UVM and Assertions
15.1 The sequences
class usb_pkt_item extends uvm_sequence_item;
`uvm_object_utils(usb_pkt_item)
rand bit sync;
rand bit byte_valid;
rand bit [7:0] byte_data;
rand bit eop;
rand bit bus_error;
constraint c_one_event { $onehot0({sync, byte_valid, eop}); }
constraint c_phy_rare { bus_error dist {0 := 990, 1 := 10}; }
function new(string name = "usb_pkt_item"); super.new(name); endfunction
endclass
// THE sequence for this chapter. Every one of the 256 possible PID bytes,
// sent as a real packet. A constrained-random sequence over "legal PIDs"
// tests the 15 the designer already thought about; this one tests the 241
// that a damaged line actually produces.
class all_pid_bytes_seq extends uvm_sequence #(usb_pkt_item);
`uvm_object_utils(all_pid_bytes_seq)
function new(string name = "all_pid_bytes_seq"); super.new(name); endfunction
task body();
for (int b = 0; b < 256; b++) begin
usb_pkt_item it;
it = usb_pkt_item::type_id::create("sync_it");
start_item(it);
if (!it.randomize() with { sync == 1; byte_valid == 0; eop == 0;
bus_error == 0; })
`uvm_error("RAND", "sync randomize failed")
finish_item(it);
it = usb_pkt_item::type_id::create("pid_it");
start_item(it);
if (!it.randomize() with { sync == 0; byte_valid == 1; eop == 0;
bus_error == 0; byte_data == 8'(b); })
`uvm_error("RAND", "pid randomize failed")
finish_item(it);
it = usb_pkt_item::type_id::create("eop_it");
start_item(it);
if (!it.randomize() with { sync == 0; byte_valid == 0; eop == 1;
bus_error == 0; })
`uvm_error("RAND", "eop randomize failed")
finish_item(it);
end
endtask
endclass
// A corrupt PID followed by a VARYING number of trailing bytes. The number
// of trailing bytes is the whole point: the scoreboard must see exactly one
// error regardless of it.
class corrupt_then_trailing_seq extends uvm_sequence #(usb_pkt_item);
`uvm_object_utils(corrupt_then_trailing_seq)
function new(string name = "corrupt_then_trailing_seq"); super.new(name); endfunction
task body();
for (int trail = 0; trail <= 20; trail++) begin
usb_pkt_item it;
it = usb_pkt_item::type_id::create("s");
start_item(it);
if (!it.randomize() with { sync == 1; byte_valid == 0; eop == 0;
bus_error == 0; })
`uvm_error("RAND", "sync randomize failed")
finish_item(it);
// 0x00 has 0000 on top and 0000 below: the complement fails.
it = usb_pkt_item::type_id::create("bad");
start_item(it);
if (!it.randomize() with { sync == 0; byte_valid == 1; eop == 0;
bus_error == 0; byte_data == 8'h00; })
`uvm_error("RAND", "bad-pid randomize failed")
finish_item(it);
repeat (trail) begin
it = usb_pkt_item::type_id::create("junk");
start_item(it);
if (!it.randomize() with { sync == 0; byte_valid == 1; eop == 0;
bus_error == 0; })
`uvm_error("RAND", "junk randomize failed")
finish_item(it);
end
it = usb_pkt_item::type_id::create("e");
start_item(it);
if (!it.randomize() with { sync == 0; byte_valid == 0; eop == 1;
bus_error == 0; })
`uvm_error("RAND", "eop randomize failed")
finish_item(it);
end
endtask
endclass
// THE sequence that catches the bug in section 3: a SHORT packet, and then
// a perfectly good one. The good one must be accepted. A receiver that
// waits in S_ABORT for an EOP it has already consumed swallows it.
class short_then_good_seq extends uvm_sequence #(usb_pkt_item);
`uvm_object_utils(short_then_good_seq)
function new(string name = "short_then_good_seq"); super.new(name); endfunction
task send_packet(bit [7:0] pid_byte, int body_len);
usb_pkt_item it;
it = usb_pkt_item::type_id::create("s");
start_item(it);
if (!it.randomize() with { sync == 1; byte_valid == 0; eop == 0;
bus_error == 0; })
`uvm_error("RAND", "sync randomize failed")
finish_item(it);
it = usb_pkt_item::type_id::create("p");
start_item(it);
if (!it.randomize() with { sync == 0; byte_valid == 1; eop == 0;
bus_error == 0; byte_data == pid_byte; })
`uvm_error("RAND", "pid randomize failed")
finish_item(it);
repeat (body_len) begin
it = usb_pkt_item::type_id::create("d");
start_item(it);
if (!it.randomize() with { sync == 0; byte_valid == 1; eop == 0;
bus_error == 0; })
`uvm_error("RAND", "body randomize failed")
finish_item(it);
end
it = usb_pkt_item::type_id::create("e");
start_item(it);
if (!it.randomize() with { sync == 0; byte_valid == 0; eop == 1;
bus_error == 0; })
`uvm_error("RAND", "eop randomize failed")
finish_item(it);
endtask
task body();
repeat (200) begin
send_packet(8'hE1, 1); // OUT token with one byte: SHORT
send_packet(8'hE1, 2); // and a perfectly good one behind it
end
endtask
endclass15.2 The scoreboard
class usb_pkt_scoreboard extends uvm_scoreboard;
`uvm_component_utils(usb_pkt_scoreboard)
uvm_analysis_imp #(usb_pkt_mon_item, usb_pkt_scoreboard) ap;
// Packet-level bookkeeping. The interesting properties of this block are
// all statements about a WHOLE packet, so the scoreboard is organised
// around packets and not around cycles.
bit pkt_open; // a SYNC has been seen and no EOP yet
int unsigned errors_this_pkt;
int unsigned n_packets, n_errors, n_cascades, n_swallowed;
int unsigned pid_bytes_accepted[256];
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function automatic bit pid_is_legal(bit [7:0] b);
return (b[7:4] == ~b[3:0]) && (b[3:0] != 4'd0);
endfunction
function void write(usb_pkt_mon_item t);
// ---- A packet is never both good and bad. ----
if (t.pkt_valid && t.pkt_error)
`uvm_error("BOTH", "a packet was reported good and bad in the same cycle")
// ---- THE self-checking property, checked against the ENCODING and
// ---- not against a list of PIDs somebody typed in.
if (t.prev_state == S_PID && t.byte_valid) begin
bit legal = pid_is_legal(t.byte_data);
if (legal && t.pkt_error && t.err_code == E_PID)
`uvm_error("PID",
$sformatf("0x%02h satisfies the complement rule and was rejected", t.byte_data))
if (!legal && !(t.pkt_error && t.err_code == E_PID))
`uvm_error("PID",
$sformatf("0x%02h fails the complement rule and was accepted as an identifier", t.byte_data))
if (legal) pid_bytes_accepted[t.byte_data]++;
end
// ---- ONE ERROR PER PACKET. Counted per packet, checked at its end. ----
if (t.sync) begin pkt_open = 1; errors_this_pkt = 0; end
if (t.pkt_error) begin
errors_this_pkt++;
n_errors++;
if (errors_this_pkt > 1) begin
n_cascades++;
`uvm_error("CASCADE",
$sformatf("error %0d within a single packet -- the machine returned to IDLE instead of swallowing the rest of it, so every remaining byte is reported separately",
errors_this_pkt))
end
end
if (t.pkt_valid) n_packets++;
// ---- THE section-3 property. A packet that produced NEITHER an error
// ---- NOR an acceptance was swallowed, and that is a silent loss.
if (t.eop) begin
if (pkt_open && (errors_this_pkt == 0) && !t.pkt_valid_next) begin
n_swallowed++;
`uvm_error("SWALLOWED",
"a packet ended with neither an error nor an acceptance -- the receiver was waiting in S_ABORT for an EOP it had already consumed")
end
pkt_open = 0;
end
// ---- Resynchronisation is BOUNDED. ----
if (t.abort_age > ABORT_MAX)
`uvm_error("UNBOUNDED",
$sformatf("the abort timer reached %0d, past its bound of %0d -- a lost EOP would hang the receiver",
t.abort_age, ABORT_MAX))
endfunction
function void report_phase(uvm_phase phase);
int unsigned distinct = 0;
foreach (pid_bytes_accepted[i]) if (pid_bytes_accepted[i] > 0) distinct++;
`uvm_info("SB", $sformatf("packets=%0d errors=%0d cascades=%0d swallowed=%0d distinct-PIDs=%0d",
n_packets, n_errors, n_cascades, n_swallowed, distinct), UVM_LOW)
// Exactly 15 of the 256 bytes are a legal identifier: 16 satisfy the
// complement rule and one of those is reserved. A run that saw fewer
// did not try them all.
if (distinct != 15)
`uvm_error("COVERAGE",
$sformatf("%0d distinct PID bytes were accepted; the encoding admits exactly 15", distinct))
if (n_packets == 0) `uvm_error("COVERAGE", "no packet was ever accepted")
if (n_errors == 0) `uvm_error("COVERAGE", "no error was ever exercised")
endfunction
endclass15.3 Assertions
module usb_packet_fsm_sva
import usb_pktfsm_pkg::*;
#(
parameter int MAXLEN = 8,
parameter int ABORT_MAX = 16
) (
input logic clk,
input logic rst_n,
input logic sync,
input logic byte_valid,
input logic [7:0] byte_data,
input logic eop,
input logic bus_error,
input pkt_state_e state,
input logic [3:0] pid,
input logic [3:0] exp_min,
input logic [3:0] exp_max,
input logic [3:0] byte_count,
input logic in_packet,
input logic pkt_valid,
input logic pkt_error,
input pkt_err_e err_code,
input logic [4:0] abort_age
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. Never both. ----
a_not_both : assert property (!(pkt_valid && pkt_error))
else $error("a packet was reported good and bad in the same cycle");
// ---- 2. THE self-checking property, stated as the ENCODING. ----
property p_pid_complement;
(state == S_PID && byte_valid && !eop && !bus_error)
|=> (pkt_error && err_code == E_PID)
== !($past(byte_data[7:4]) == ~$past(byte_data[3:0])
&& $past(byte_data[3:0]) != 4'd0);
endproperty
a_pid_complement : assert property (p_pid_complement)
else $error("a PID byte was accepted or rejected against the complement rule");
// ---- 3. ONE ERROR AT A TIME, and none at all from inside S_ABORT.
// ---- This is the cascade property in its per-cycle form: S_ABORT
// ---- interprets nothing, so it cannot generate an event.
property p_abort_is_silent;
(state == S_ABORT) |-> !pkt_error && !pkt_valid;
endproperty
a_abort_is_silent : assert property (p_abort_is_silent)
else $error("S_ABORT raised an event -- it is supposed to interpret nothing");
// ---- 4. Every error leads OUT of the packet: either to S_ABORT (the
// ---- rest is still coming) or to S_IDLE (it has already gone).
// ---- Never back into the middle of one.
property p_error_leaves_the_packet;
pkt_error |-> (state == S_ABORT) || (state == S_IDLE);
endproperty
a_error_leaves_the_packet : assert property (p_error_leaves_the_packet)
else $error("an error left the machine inside a packet -- it is now reading payload as headers");
// ---- 5. BOUNDED resynchronisation. From any state, ABORT_MAX+1 cycles
// ---- with no input returns the machine to IDLE. A lost EOP must
// ---- never be a hang.
property p_resync_is_bounded;
(!sync && !byte_valid && !eop && !bus_error)[*ABORT_MAX+1]
|-> (state == S_IDLE);
endproperty
a_resync_is_bounded : assert property (p_resync_is_bounded)
else $error("a quiet line did not return the machine to IDLE within the abort bound");
// ---- 6. The abort timer exists only inside S_ABORT, and is bounded. ----
a_age_bounded : assert property (abort_age <= 5'(ABORT_MAX));
a_age_scoped : assert property ((state != S_ABORT) |-> (abort_age == '0));
// ---- 7. The body length is never exceeded. ----
property p_length_respected;
(state == S_BODY) |-> (byte_count <= exp_max);
endproperty
a_length_respected : assert property (p_length_respected)
else $error("more bytes were collected than this PID permits");
// ---- 8. A packet is only ever ACCEPTED at a length the PID allows. ----
property p_accept_only_legal_length;
pkt_valid |-> ($past(byte_count) >= $past(exp_min))
&& ($past(byte_count) <= $past(exp_max));
endproperty
a_accept_only_legal_length : assert property (p_accept_only_legal_length)
else $error("a packet was accepted at a length its PID does not allow");
// ---- 9. in_packet agrees with the states it summarises. ----
a_in_packet : assert property
(in_packet == ((state == S_PID) || (state == S_BODY) || (state == S_HSHK)));
// ---- Cover: every error cause, and the two shapes of recovery. ----
c_err_pid : cover property ((pkt_error && err_code == E_PID));
c_err_short : cover property ((pkt_error && err_code == E_SHORT));
c_err_long : cover property ((pkt_error && err_code == E_LONG));
c_err_stray : cover property ((pkt_error && err_code == E_STRAY));
c_err_bus : cover property ((pkt_error && err_code == E_BUS));
c_abort_eop : cover property (((state == S_ABORT) ##1 (state == S_IDLE)));
c_abort_tmout : cover property (((abort_age == 5'(ABORT_MAX) - 5'd1)
##1 (state == S_IDLE)));
// The section-3 recovery: an error at the EOP, and the very next packet
// accepted rather than swallowed.
c_short_then_good : cover property
(((pkt_error && err_code == E_SHORT) ##[1:40] pkt_valid));
endmodule
bind usb_packet_fsm
usb_packet_fsm_sva #(.MAXLEN(MAXLEN), .ABORT_MAX(ABORT_MAX)) u_sva (.*);16. Common Misconceptions
"The CRC will catch a corrupt PID." The PID decides how long the packet is, so a corrupt PID means the CRC is computed over the wrong window. The identifier has to be verifiable before the thing it identifies.
"There are 16 legal PIDs." Sixteen satisfy the complement rule; one is reserved. Fifteen are legal, and a suite that finds 16 has accepted a reserved one.
"Recovering in place is more robust than resynchronising." It is a machine reading payload as headers. There is no way to find the next packet except by waiting for one.
"An error is an error; they all go to the abort state." Only the ones where the rest of the packet is still coming. An error detected at the EOP must go straight to IDLE, or the receiver swallows the next packet — and under a retry, the retry is what gets swallowed.
"One extra error report is harmless." One corrupt packet becomes forty-one events. The per-cause counters stop being usable for diagnosis, which is exactly when you need them.
"The abort will always see an EOP eventually." Not if the EOP was the thing that was corrupted. Without a timeout that is a permanent hang, and a hang is worse than any error.
"A low mutation score means the mutation is unimportant." P5 scores 270 because short packets are rare in good traffic. It is caught only because the length sweep deliberately sends them.
17. Exercises
1. Work out from the encoding alone why exactly 16 bytes satisfy the complement rule, and why the answer does not depend on which four bits carry the PID.
2. Apply P3 and count the errors produced by one corrupt PID followed by a 64-byte payload. Then explain why P3 scores higher than P1 even though P1 breaks the PID check entirely.
3. Revert §3 — send a short packet to S_ABORT — and write the smallest stimulus that demonstrates the next packet being swallowed. How many packets does it take to notice?
4. P7 scores 15838 / 8258 / 10710 across three languages. Argue from the mutation's behaviour why the spread is this wide, and why that is a statement about the stimulus rather than about the design.
5. SVA property 5 says a quiet line returns the machine to IDLE within ABORT_MAX+1 cycles. Show it is false for P7 and find the shortest counterexample.
6. S_ABORT waits for an EOP. Design an alternative that waits for the next SYNC instead, and say what breaks — specifically, what happens when the corrupt payload contains a byte pattern that looks like a SYNC.
18. Summary
| Idea | Why it matters |
|---|---|
| The PID validates itself | the identifier must be checkable before what it identifies |
| Exactly 15 of 256 bytes are legal | the encoding, measured, not a list to maintain |
| Every error resynchronises | there is no way to find the next packet but to wait for one |
| Mid-packet errors go to S_ABORT | the rest of the packet is still arriving |
| EOP errors go straight to IDLE | it has already gone, and waiting swallows the next packet |
| ...and under a retry, the retry is eaten | an error, then a silent gap, is the signature |
| One error per packet | not one per byte, or the log is useless |
S_ABORT interprets nothing | no decode, no count, no second error |
| Resynchronisation is bounded | a lost EOP must be an error, never a hang |
| Causes are counted from the same pulse | so they sum to the total by construction |
| 80 of 80 state x input pairs | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o pf_v.out pf_v.v pf_v_tb.v && ./pf_v.out |
| SystemVerilog | iverilog -g2012 -o pf_sv.out pf_sv.sv pf_sv_tb.sv && ./pf_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a pf_vhdl.vhd pf_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_pf_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_pf_vhdl |
| One mutation | iverilog -g2005 -DMUT_P3 -o mm pf_v_mut.v pf_v_tb.v && ./mm |
All three implementations pass with 0 errors: 80 of 80 state-by-input pairs, all 256 possible PID bytes tried with exactly 15 accepted, one error per corrupt packet at every trailing length, and bounded resynchronisation after every error cause.
Chapter 23.5 — Timing Considerations is the one that undoes an assumption running through all four chapters so far: that there is one clock. The PHY runs at the bus rate and the controller does not, and the moment a value crosses between them the rules change — most sharply for anything wider than a bit, because a multi-bit value cannot be bit-synchronised at all.
Continue learning
Related tutorials
- Related topic
Protocol Engine
The CRC is the last thing on the wire, so a device must write the payload before it knows whether the payload is good — provisional writes, commit and rollback, and why you cannot change your mind mid-packet.
- Related topic
Video over Isochronous
A video frame is many isochronous packets, and losing the one carrying End-of-Frame would merge frames forever. The single toggling bit that prevents it — and the mutation that exposed a real defect in the RTL.
- Related topic
Hub Architecture
A hub is three devices in one package, and its repeater is deliberately asymmetric: downstream is a broadcast, upstream is a select of exactly one — and two talkers connects neither.
- Related topic
Port Management
Three uncoordinated sources move a hub port and collide in the same cycle — and the host, whose information is stale by construction, is the one that loses.
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.
