Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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 detectedThe rest of the packetWhere to go
mid-packet — bad PID, over-long body, PHY failure, a byte with no SYNCstill arrivingS_ABORT: swallow it
at the EOP — a packet that ended before it was wholealready goneS_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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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

A five-state packet recogniser. IDLE moves to PID on SYNC. PID moves to BODY when the identifier is valid and needs a body, or to HSHK when it is a handshake, or to ABORT when the identifier fails its check. BODY returns to IDLE on a complete end of packet and goes to ABORT when there are too many bytes or the PHY fails. HSHK returns to IDLE on the end of packet. ABORT returns to IDLE on the end of packet or a timeout.IDLEPIDBODYHSHKABORTSYNCSYNCPID okPID okhandshakehandshakeEOP: completeEOP: completeEOP: acceptedEOP: acceptedbad PIDbadPIDover-long / PHYover-long / PHYEOP or timeoutEOP ortimeout
Every error path leads either to S_ABORT (when the rest of the packet is still coming) or straight back to IDLE (when it has already gone). Nothing loops back into the middle of a packet, which is the property that makes the machine recoverable.

Three transitions are deliberately left off the picture because they would clutter it, and they are exactly the ones §3 is about:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

8. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

9. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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 cycles
A ten-cycle waveform. A SYNC at cycle 0 moves the machine to PID. The byte 0x00 at cycle 1 fails the complement check, so at cycle 2 pkt_error pulses once, err_code becomes PID and the state is ABORT. A junk byte at cycle 2 produces no second error. The EOP at cycle 3 returns the machine to IDLE, and a SYNC, an OUT identifier and two body bytes then produce a valid packet at cycle 9.0x00 fails the complement0x00 fails the complementone error; junk ignoredone error; junk ignored0xE1 is a valid OUT token0xE1 is a valid OUT tokenclksyncbyte_validbyte_data0007A7A7AE112343434eopstateIDLEPIDABORTABORTIDLEPIDBODYBODYBODYIDLEbyte_count0000000120err_codeNONENONEPIDPIDPIDNONENONENONENONENONEpkt_errorpkt_validt0t1t2t3t4t5t6t7t8t9
0x00 fails the complement check at cycle 1. One error is reported at cycle 2 and the machine enters ABORT, where the trailing junk byte produces nothing at all. The EOP at cycle 3 returns it to IDLE, and the well-formed OUT token that follows is accepted normally.

Look 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // ---- 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

That 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

11.2 SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

11.3 VHDL testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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

MeasureVerilogSystemVerilogVHDL
state x input pairs80 / 8080 / 8080 / 80
legal PID bytes found15 / 25615 / 25615 / 256
Transitions349243485734808
Checks executed525076524071488528
packets accepted568569565
errors reported364236863578
— bad PID881863868
— short254240238
— over-long136127129
— stray byte233224132302
— PHY error394341
abort timeouts258259266
ResultPASSPASSPASS

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

#MutationVerilogSysVerVHDL
P3every error returns straight to IDLE — the error cascade98318103949102204
P2the top nibble must repeat the bottom, not invert it935299417697462
P1the complement check is dropped; the PID is taken on trust822528837086700
P6the abort leaves on any byte, not only on EOP748227411467481
P7the abort timeout is removed — a lost EOP hangs the receiver15838825810710
P4the over-long check is off by one140412171150
P5a packet that ended too early is accepted anyway270264270
—unmutated baseline000

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endclass

15.2 The scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endclass

15.3 Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

IdeaWhy it matters
The PID validates itselfthe identifier must be checkable before what it identifies
Exactly 15 of 256 bytes are legalthe encoding, measured, not a list to maintain
Every error resynchronisesthere is no way to find the next packet but to wait for one
Mid-packet errors go to S_ABORTthe rest of the packet is still arriving
EOP errors go straight to IDLEit has already gone, and waiting swallows the next packet
...and under a retry, the retry is eatenan error, then a silent gap, is the signature
One error per packetnot one per byte, or the log is useless
S_ABORT interprets nothingno decode, no count, no second error
Resynchronisation is boundeda lost EOP must be an error, never a hang
Causes are counted from the same pulseso they sum to the total by construction
80 of 80 state x input pairs7 mutations, all killed in 3 languages

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o pf_v.out pf_v.v pf_v_tb.v && ./pf_v.out
SystemVerilogiverilog -g2012 -o pf_sv.out pf_sv.sv pf_sv_tb.sv && ./pf_sv.out
VHDL-2008 analysenvc --std=2008 -a pf_vhdl.vhd pf_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_pf_vhdl
VHDL-2008 runnvc --std=2008 -r tb_pf_vhdl
One mutationiverilog -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

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.