Skip to content
VLSI Mentor

USB · Module 21

Protocol Engine

The CRC is the last thing on the wire, so a device must write the payload before it knows whether the payload is good — provisional writes, commit and rollback, and why you cannot change your mind mid-packet.

Three chapters of this module have decided whether to accept a packet. 21.1 decided it from the toggle, 21.2 from buffer occupancy, 21.3 from lengths.

This chapter is about the fact that the decision arrives too late.

1. The CRC Is the Last Thing on the Wire

A USB DATA packet is a PID, a payload, and a 16-bit CRC covering that payload:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ DATA0 ][ b0 b1 b2 b3 ......... bN ][ CRC16 ]
                                           ^
                                           the first moment the device
                                           knows whether ANY of those
                                           bytes are good

The bytes arrived one at a time at the bus's rate — 480 Mb/s on a high-speed link — and the device had to put each one somewhere as it came. It cannot hold a 1024-byte payload in flip-flops while it waits: that is what the endpoint buffer is, and there is only one of it.

So the device must start writing before it knows whether the data is good. There is no version of this design in which it does not.

2. Two Pointers, and Only One of Them Is Real

The answer is to make the write provisional.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   committed ---->|                          firmware reads up to here
                  |###### provisional ######>|
                   \_ bytes written, but not yet real _/

The committed pointer is what firmware can see. The provisional pointer runs ahead of it during reception. At the end of the packet exactly one of two things happens:

what happenscost
COMMITcommitted <= provisional — the bytes become visibleone pointer assignment
ROLLBACKprovisional <= committed — the bytes are unwrittenone pointer assignment

The payload was physically written to the buffer either way. What makes it "not written" is that nothing points at it, and the next packet overwrites it. A rollback costs no cycles and no bus time — which matters, because the device has to be ready for the next transaction immediately.

The provisional write, and the two ways a packet can end

A provisional write path. Bytes arrive from the bus and are written at the provisional pointer, which runs ahead of the committed pointer. At the end of the packet, either the CRC passed and the committed pointer jumps forward to the provisional one, or something failed and the provisional pointer falls back to the committed one.Bytes from the busone per bus clockProvisional ptrruns ahead, writes hereEndpoint bufferthe bytes are physically in itPacket endsthe CRC is finally knownCOMMITcommitted ← provisionalFirmware sees themup to the committed ptrROLLBACKprovisional ← committedNever pointed atoverwritten next packetwriteCRC okanything wrong12
Bytes go into the buffer as they arrive, at the provisional pointer. At the end of the packet the CRC result decides which pointer moves: the committed one forward, or the provisional one back.

3. You Cannot Change Your Mind Mid-Packet

The second constraint, and the one that surprises people.

Suppose the buffer fills up halfway through reception. The device now knows it cannot accept this packet. So why keep receiving?

Because there is nowhere to put the refusal.

A USB transaction has exactly one place a handshake may be sent: after the packet ends. There is no mid-packet abort. There is no flow control inside a packet. There is no way to tell the host to stop talking. The host will transmit every byte it intended to and then wait for an answer.

So the engine:

  1. keeps receiving,
  2. discards the bytes that do not fit,
  3. records that an overflow happened,
  4. and NAKs at the end.

4. The Decision, and Its Order

Every rule in this chapter's decision comes from an earlier one, which is what makes the block a composition rather than a fourth set of rules:

ConditionHandshakeBufferFrom
endpoint haltedSTALLrollback21.1 — a halt outranks everything but SETUP
CRC failedsilencerollback21.1 — a NAK would claim knowledge the device lacks
toggle mismatchACKrollback21.1 — a retransmission: re-ACK, store nothing
overflowedNAKrollback21.2 — no room; ask again
otherwiseACKCOMMIT

Four different causes, three different handshakes, and from the host's side they are nearly indistinguishable — which is why the design also emits a drop_reason. DROP_CRC means fix your cable; DROP_NO_ROOM means firmware is too slow; DROP_TOGGLE means an ACK went missing; DROP_HALTED means software has not cleared a halt. Four different engineers, four different afternoons.

The engine's three states, and the one-cycle decision that ends a packet

A three-state machine. From IDLE, rx_start moves to RECEIVING, where each valid byte advances the provisional pointer and a full buffer sets the overflow flag without leaving the state. At rx_end the machine moves to DECIDE, which either commits or rolls back and returns to IDLE.IDLERECEIVINGDECIDErx_startrx_startbyte: advance, or mark overflowbyte: advance, or mark overflowbyte: advance,or mark…rx_endrx_endcommit or rollbackcommit or rollback
Reception is a single state that no condition can leave early. Everything that can go wrong is recorded and acted on at the end, because the end is the only place a handshake may be sent.

5. What We Are Building

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  usb_packet_engine  #(CAP = 16, PW = 5)

  from the bus                  to the buffer
  ------------                  -------------
  rx_start                      prov_wr_en
  rx_byte_valid                 prov_wr_addr
  rx_byte      [7:0]            prov_wr_data
  rx_end
  rx_crc_ok     } valid only    wr_ptr    COMMITTED: firmware sees this
  toggle_match  } at rx_end     prov_ptr  PROVISIONAL: we write here
  ep_halted
  flush                         receiving / overflowed
                                commit / rollback
                                handshake    NONE / ACK / NAK / STALL
                                drop_reason  NONE / HALTED / CRC /
                                             TOGGLE / NO_ROOM

  n_commit / n_crc_drop / n_dup_drop / n_nak_space / n_stall / n_bytes

6. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_packet_engine -- the block that actually moves the bytes, and the
// constraint that shapes it: THE HANDSHAKE IS DUE BEFORE THE CRC IS KNOWN.
//
// THE ORDERING PROBLEM
//
// A USB DATA packet is a PID, a payload, and a 16-bit CRC covering that
// payload. The CRC is the LAST thing on the wire:
//
//     [ DATA0 ][ b0 b1 b2 ... bN ][ CRC16 ]
//                                  ^
//                                  the first moment the device knows
//                                  whether any of those bytes are good
//
// But the bytes arrived one at a time, at the bus's rate, and the device has
// nowhere to put them except the endpoint buffer. It cannot hold a 1024-byte
// payload in flip-flops while it waits for the CRC -- that is the buffer,
// and there is only one of it.
//
// So the device MUST start writing before it knows whether the data is good.
//
// THE ANSWER IS A PROVISIONAL WRITE
//
// Two pointers. The COMMITTED pointer is what firmware can see. The
// PROVISIONAL pointer runs ahead of it during reception:
//
//     committed ---->|                          firmware reads up to here
//                    |###### provisional ######>|
//                     \_ bytes written, but not yet real _/
//
// At the end of the packet, exactly one of two things happens:
//
//     COMMIT    the CRC passed and the packet is wanted:
//               committed <= provisional.  The bytes become visible.
//
//     ROLLBACK  anything went wrong:
//               provisional <= committed.  The bytes are unwritten by the
//               simple expedient of never having been pointed at.
//
// The payload was physically written to the buffer either way. What makes it
// "not written" is that nothing points at it, and the next packet overwrites
// it. A rollback costs one pointer assignment and no cycles.
//
// YOU CANNOT CHANGE YOUR MIND MID-PACKET
//
// The second constraint, and the one that surprises people. Suppose the
// buffer fills up halfway through reception. The device now knows it cannot
// accept this packet -- so why keep receiving?
//
// Because THERE IS NOWHERE TO PUT THE REFUSAL. A USB transaction has exactly
// one place a handshake may be sent: after the packet ends. There is no
// mid-packet abort, no flow control inside a packet, no way to tell the host
// to stop talking. The host will transmit every byte it intended to and then
// wait for an answer.
//
// So the engine keeps receiving, DISCARDS the bytes that do not fit, records
// that an overflow happened, and NAKs at the end. Stopping the byte counter
// early would lose the length; stopping the state machine early would leave
// it out of step with the bus for the next packet.
//
// THE DECISION, AND ITS ORDER
//
//     halted        -> STALL,   rollback   (chapter 21.1: halt outranks all)
//     CRC failed    -> SILENCE, rollback   (21.1: a NAK would claim knowledge)
//     toggle wrong  -> ACK,     rollback   (21.1: a retransmission, re-ACKed
//                                           and NOT stored)
//     overflowed    -> NAK,     rollback   (no room; ask again)
//     otherwise     -> ACK,     COMMIT
module usb_packet_engine #(
  parameter CAP = 16,            // endpoint buffer capacity in bytes
  parameter PW  = 5              // pointer width: must hold 0 .. CAP
) (
  input  wire          clk,
  input  wire          rst_n,

  input  wire          rx_start,       // a DATA packet has begun
  input  wire          rx_byte_valid,  // a payload byte is on rx_byte
  input  wire [7:0]    rx_byte,
  input  wire          rx_end,         // the packet ended; the two signals
  input  wire          rx_crc_ok,      // below are valid only in this cycle
  input  wire          toggle_match,
  input  wire          ep_halted,
  input  wire          flush,

  output wire [PW-1:0] wr_ptr,         // COMMITTED: what firmware can see
  output wire [PW-1:0] prov_ptr,       // PROVISIONAL: where we are writing
  output wire          prov_wr_en,
  output wire [PW-1:0] prov_wr_addr,
  output wire [7:0]    prov_wr_data,

  output wire          receiving,
  output wire          overflowed,     // this packet did not fit
  output wire          commit,
  output wire          rollback,
  output wire [2:0]    handshake,
  output wire [2:0]    drop_reason,    // WHY, when it was rolled back

  output reg  [31:0]   n_commit,
  output reg  [31:0]   n_crc_drop,
  output reg  [31:0]   n_dup_drop,
  output reg  [31:0]   n_nak_space,
  output reg  [31:0]   n_stall,
  output reg  [31:0]   n_bytes
);
  localparam [2:0] H_NONE  = 3'd0,   // no handshake leaves the device
                   H_ACK   = 3'd1,
                   H_NAK   = 3'd2,
                   H_STALL = 3'd3;

  reg [PW-1:0] wptr_r;
  reg [PW-1:0] prov_r;
  reg          rx_r;
  reg          ovf_r;

  assign wr_ptr     = wptr_r;
  assign prov_ptr   = prov_r;
  assign receiving  = rx_r;
  assign overflowed = ovf_r;

  // A byte is written provisionally only while there is room. Once there is
  // not, the packet is doomed -- but reception CONTINUES, because there is
  // nowhere to put a refusal until the packet ends.
  wire have_room = (prov_r < CAP[PW-1:0]);
  wire accepting = rx_r && rx_byte_valid;

  assign prov_wr_en   = accepting && have_room;
  assign prov_wr_addr = prov_r;
  assign prov_wr_data = rx_byte;

  // A packet carrying the toggle we are NOT expecting is a retransmission of
  // one already accepted (chapter 21.1). Its bytes are already in the buffer.
  wire dup = !toggle_match;

  // THE END-OF-PACKET DECISION. Nothing before rx_end can produce a
  // handshake, because nothing before rx_end is known.
  assign handshake = !rx_end      ? H_NONE
                   : ep_halted    ? H_STALL
                   : !rx_crc_ok   ? H_NONE   // silence, never a NAK
                   : dup          ? H_ACK    // re-ACK, store nothing
                   : ovf_r        ? H_NAK
                                  : H_ACK;

  // The same priority order, reported as a REASON. Four different causes
  // produce three different handshakes, and from the host's side they are
  // nearly indistinguishable -- so the device says which one it was.
  localparam [2:0] DROP_NONE    = 3'd0,  // nothing was dropped
                   DROP_HALTED  = 3'd1,  // the endpoint is halted
                   DROP_CRC     = 3'd2,  // the payload was corrupt
                   DROP_TOGGLE  = 3'd3,  // a retransmission: already have it
                   DROP_NO_ROOM = 3'd4;  // it did not fit

  assign drop_reason = !rx_end    ? DROP_NONE
                     : ep_halted  ? DROP_HALTED
                     : !rx_crc_ok ? DROP_CRC
                     : dup        ? DROP_TOGGLE
                     : ovf_r      ? DROP_NO_ROOM
                                  : DROP_NONE;

  // Exactly one of commit and rollback happens at the end of every packet.
  assign commit   = rx_end && !ep_halted && rx_crc_ok && !dup && !ovf_r;
  assign rollback = rx_end && !commit;

  // How many bytes this packet actually contributed, if it is committed.
  wire [PW-1:0] gained = prov_r - wptr_r;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wptr_r      <= {PW{1'b0}};
      prov_r      <= {PW{1'b0}};
      rx_r        <= 1'b0;
      ovf_r       <= 1'b0;
      n_commit    <= 32'd0;
      n_crc_drop  <= 32'd0;
      n_dup_drop  <= 32'd0;
      n_nak_space <= 32'd0;
      n_stall     <= 32'd0;
      n_bytes     <= 32'd0;
    end else if (flush) begin
      // A flush discards everything, provisional and committed alike.
      wptr_r <= {PW{1'b0}};
      prov_r <= {PW{1'b0}};
      rx_r   <= 1'b0;
      ovf_r  <= 1'b0;
    end else if (rx_start) begin
      // The provisional pointer starts from the committed one, every packet.
      // Anything left over from a rolled-back packet is discarded here as
      // well as at the rollback -- belt and braces, because starting from a
      // stale provisional pointer is the bug that corrupts the NEXT packet.
      rx_r   <= 1'b1;
      prov_r <= wptr_r;
      ovf_r  <= 1'b0;
    end else if (rx_end) begin
      rx_r <= 1'b0;
      if (commit) begin
        wptr_r  <= prov_r;
        n_commit <= n_commit + 32'd1;
        n_bytes  <= n_bytes + {{(32-PW){1'b0}}, gained};
      end else begin
        // ROLLBACK. The bytes are still physically in the buffer; what makes
        // them not-written is that nothing points at them any more.
        prov_r <= wptr_r;
        if (ep_halted)       n_stall     <= n_stall + 32'd1;
        else if (!rx_crc_ok) n_crc_drop  <= n_crc_drop + 32'd1;
        else if (dup)        n_dup_drop  <= n_dup_drop + 32'd1;
        else                 n_nak_space <= n_nak_space + 32'd1;
      end
    end else if (accepting) begin
      if (have_room) prov_r <= prov_r + {{(PW-1){1'b0}}, 1'b1};
      else           ovf_r  <= 1'b1;   // doomed, but keep receiving
    end
  end
endmodule

Three details are load-bearing.

prov_r <= wptr_r on rx_start, not only on rollback. Belt and braces: the provisional pointer is re-anchored at the start of every packet as well as at the end of a failed one. Starting a packet from a stale provisional pointer is the bug that corrupts the next packet rather than the current one, which makes it roughly four times harder to find.

have_room is prov_r < CAP, strictly. A packet that fills the buffer exactly is not an overflow — it fits. Mutation H4 makes this < CAP-1 and costs 254 000 failures, because it makes the last byte of the buffer permanently unreachable.

ovf_r is set in the else of if (have_room) — so an overflow is recorded only when a byte actually had nowhere to go. This is what makes the first version of mutation H6 provably equivalent; see §11.

7. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_packet_engine -- the block that actually moves the bytes, and the
// constraint that shapes it: THE HANDSHAKE IS DUE BEFORE THE CRC IS KNOWN.
//
// THE ORDERING PROBLEM
//
// A USB DATA packet is a PID, a payload, and a 16-bit CRC covering that
// payload. The CRC is the LAST thing on the wire:
//
//     [ DATA0 ][ b0 b1 b2 ... bN ][ CRC16 ]
//                                  ^
//                                  the first moment the device knows
//                                  whether any of those bytes are good
//
// But the bytes arrived one at a time, at the bus's rate, and the device has
// nowhere to put them except the endpoint buffer. It cannot hold a 1024-byte
// payload in flip-flops while it waits for the CRC -- that is the buffer,
// and there is only one of it.
//
// So the device MUST start writing before it knows whether the data is good.
//
// THE ANSWER IS A PROVISIONAL WRITE
//
// Two pointers. The COMMITTED pointer is what firmware can see. The
// PROVISIONAL pointer runs ahead of it during reception:
//
//     committed ---->|                          firmware reads up to here
//                    |###### provisional ######>|
//                     \_ bytes written, but not yet real _/
//
// At the end of the packet, exactly one of two things happens:
//
//     COMMIT    the CRC passed and the packet is wanted:
//               committed <= provisional.  The bytes become visible.
//
//     ROLLBACK  anything went wrong:
//               provisional <= committed.  The bytes are unwritten by the
//               simple expedient of never having been pointed at.
//
// The payload was physically written to the buffer either way. What makes it
// "not written" is that nothing points at it, and the next packet overwrites
// it. A rollback costs one pointer assignment and no cycles.
//
// YOU CANNOT CHANGE YOUR MIND MID-PACKET
//
// The second constraint, and the one that surprises people. Suppose the
// buffer fills up halfway through reception. The device now knows it cannot
// accept this packet -- so why keep receiving?
//
// Because THERE IS NOWHERE TO PUT THE REFUSAL. A USB transaction has exactly
// one place a handshake may be sent: after the packet ends. There is no
// mid-packet abort, no flow control inside a packet, no way to tell the host
// to stop talking. The host will transmit every byte it intended to and then
// wait for an answer.
//
// So the engine keeps receiving, DISCARDS the bytes that do not fit, records
// that an overflow happened, and NAKs at the end. Stopping the byte counter
// early would lose the length; stopping the state machine early would leave
// it out of step with the bus for the next packet.
//
// THE DECISION, AND ITS ORDER
//
//     halted        -> STALL,   rollback   (chapter 21.1: halt outranks all)
//     CRC failed    -> SILENCE, rollback   (21.1: a NAK would claim knowledge)
//     toggle wrong  -> ACK,     rollback   (21.1: a retransmission, re-ACKed
//                                           and NOT stored)
//     overflowed    -> NAK,     rollback   (no room; ask again)
//     otherwise     -> ACK,     COMMIT
package usb_pktengine_pkg;
  typedef enum logic [2:0] {
    H_NONE  = 3'd0,   // no handshake leaves the device at all
    H_ACK   = 3'd1,
    H_NAK   = 3'd2,
    H_STALL = 3'd3
  } handshake_e;

  // WHY a packet was rolled back. Four different reasons that all look like
  // "the data did not arrive" from the host's side, and all need different
  // action from whoever is debugging the device.
  typedef enum logic [2:0] {
    DROP_NONE    = 3'd0,   // nothing was dropped
    DROP_HALTED  = 3'd1,   // the endpoint is halted
    DROP_CRC     = 3'd2,   // the payload was corrupt
    DROP_TOGGLE  = 3'd3,   // a retransmission: we already have these bytes
    DROP_NO_ROOM = 3'd4    // it did not fit
  } drop_reason_e;
endpackage

module usb_packet_engine
  import usb_pktengine_pkg::*;
#(
  parameter int CAP = 16,        // endpoint buffer capacity in bytes
  parameter int PW  = 5          // pointer width: must hold 0 .. CAP
) (
  input  logic          clk,
  input  logic          rst_n,

  input  logic          rx_start,      // a DATA packet has begun
  input  logic          rx_byte_valid, // a payload byte is on rx_byte
  input  logic [7:0]    rx_byte,
  input  logic          rx_end,        // the packet ended; the two signals
  input  logic          rx_crc_ok,     // below are valid only in this cycle
  input  logic          toggle_match,
  input  logic          ep_halted,
  input  logic          flush,

  output logic [PW-1:0] wr_ptr,        // COMMITTED: what firmware can see
  output logic [PW-1:0] prov_ptr,      // PROVISIONAL: where we are writing
  output logic          prov_wr_en,
  output logic [PW-1:0] prov_wr_addr,
  output logic [7:0]    prov_wr_data,

  output logic          receiving,
  output logic          overflowed,    // this packet did not fit
  output logic          commit,
  output logic          rollback,
  output handshake_e    handshake,
  output drop_reason_e  drop_reason,   // WHY, when it was rolled back

  output logic [31:0]   n_commit,
  output logic [31:0]   n_crc_drop,
  output logic [31:0]   n_dup_drop,
  output logic [31:0]   n_nak_space,
  output logic [31:0]   n_stall,
  output logic [31:0]   n_bytes
);

  logic [PW-1:0] wptr_r;
  logic [PW-1:0] prov_r;
  logic          rx_r;
  logic          ovf_r;

  assign wr_ptr     = wptr_r;
  assign prov_ptr   = prov_r;
  assign receiving  = rx_r;
  assign overflowed = ovf_r;

  // A byte is written provisionally only while there is room. Once there is
  // not, the packet is doomed -- but reception CONTINUES, because there is
  // nowhere to put a refusal until the packet ends.
  logic have_room, accepting;
  assign have_room = (prov_r < PW'(CAP));
  assign accepting = rx_r && rx_byte_valid;

  assign prov_wr_en   = accepting && have_room;
  assign prov_wr_addr = prov_r;
  assign prov_wr_data = rx_byte;

  // A packet carrying the toggle we are NOT expecting is a retransmission of
  // one already accepted (chapter 21.1). Its bytes are already in the buffer.
  logic dup;
  assign dup = !toggle_match;

  // THE END-OF-PACKET DECISION. Nothing before rx_end can produce a
  // handshake, because nothing before rx_end is known.
  always_comb begin
    if      (!rx_end)    handshake = H_NONE;
    else if (ep_halted)  handshake = H_STALL;
    else if (!rx_crc_ok) handshake = H_NONE;  // silence, never a NAK
    else if (dup)        handshake = H_ACK;   // re-ACK, store nothing
    else if (ovf_r)      handshake = H_NAK;
    else                 handshake = H_ACK;
  end

  // The same priority order, reported as a REASON. Four different causes
  // produce three different handshakes, and from the host's side they are
  // nearly indistinguishable -- so the device says which one it was.
  always_comb begin
    if      (!rx_end)    drop_reason = DROP_NONE;
    else if (ep_halted)  drop_reason = DROP_HALTED;
    else if (!rx_crc_ok) drop_reason = DROP_CRC;
    else if (dup)        drop_reason = DROP_TOGGLE;
    else if (ovf_r)      drop_reason = DROP_NO_ROOM;
    else                 drop_reason = DROP_NONE;
  end

  // Exactly one of commit and rollback happens at the end of every packet.
  assign commit   = rx_end && !ep_halted && rx_crc_ok && !dup && !ovf_r;
  assign rollback = rx_end && !commit;

  // How many bytes this packet actually contributed, if it is committed.
  logic [PW-1:0] gained;
  assign gained = prov_r - wptr_r;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wptr_r      <= '0;
      prov_r      <= '0;
      rx_r        <= 1'b0;
      ovf_r       <= 1'b0;
      n_commit    <= '0;
      n_crc_drop  <= '0;
      n_dup_drop  <= '0;
      n_nak_space <= '0;
      n_stall     <= '0;
      n_bytes     <= '0;
    end else if (flush) begin
      // A flush discards everything, provisional and committed alike.
      wptr_r <= '0;
      prov_r <= '0;
      rx_r   <= 1'b0;
      ovf_r  <= 1'b0;
    end else if (rx_start) begin
      // The provisional pointer starts from the committed one, every packet.
      // Anything left over from a rolled-back packet is discarded here as
      // well as at the rollback -- belt and braces, because starting from a
      // stale provisional pointer is the bug that corrupts the NEXT packet.
      rx_r   <= 1'b1;
      prov_r <= wptr_r;
      ovf_r  <= 1'b0;
    end else if (rx_end) begin
      rx_r <= 1'b0;
      if (commit) begin
        wptr_r  <= prov_r;
        n_commit <= n_commit + 1;
        n_bytes  <= n_bytes + 32'(gained);
      end else begin
        // ROLLBACK. The bytes are still physically in the buffer; what makes
        // them not-written is that nothing points at them any more.
        prov_r <= wptr_r;
        if (ep_halted)       n_stall     <= n_stall + 1;
        else if (!rx_crc_ok) n_crc_drop  <= n_crc_drop + 1;
        else if (dup)        n_dup_drop  <= n_dup_drop + 1;
        else                 n_nak_space <= n_nak_space + 1;
      end
    end else if (accepting) begin
      if (have_room) prov_r <= prov_r + 1'b1;
      else           ovf_r  <= 1'b1;   // doomed, but keep receiving
    end
  end
endmodule

8. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- usb_packet_engine -- the block that actually moves the bytes, and the
-- constraint that shapes it: THE HANDSHAKE IS DUE BEFORE THE CRC IS KNOWN.
--
-- THE ORDERING PROBLEM
--
-- A USB DATA packet is a PID, a payload, and a 16-bit CRC covering that
-- payload. The CRC is the LAST thing on the wire:
--
--     [ DATA0 ][ b0 b1 b2 ... bN ][ CRC16 ]
--                                  ^
--                                  the first moment the device knows
--                                  whether any of those bytes are good
--
-- But the bytes arrived one at a time, at the bus's rate, and the device has
-- nowhere to put them except the endpoint buffer. It cannot hold a 1024-byte
-- payload in flip-flops while it waits for the CRC.
--
-- So the device MUST start writing before it knows whether the data is good.
--
-- THE ANSWER IS A PROVISIONAL WRITE
--
-- Two pointers. The COMMITTED pointer is what firmware can see; the
-- PROVISIONAL pointer runs ahead of it during reception. At the end of the
-- packet, exactly one of two things happens:
--
--     COMMIT    the CRC passed and the packet is wanted:
--               committed <= provisional.  The bytes become visible.
--
--     ROLLBACK  anything went wrong:
--               provisional <= committed.  The bytes are unwritten by the
--               simple expedient of never having been pointed at.
--
-- The payload was physically written either way. What makes it "not written"
-- is that nothing points at it, and the next packet overwrites it.
--
-- YOU CANNOT CHANGE YOUR MIND MID-PACKET
--
-- Suppose the buffer fills halfway through. The device now knows it cannot
-- accept this packet -- so why keep receiving?
--
-- Because THERE IS NOWHERE TO PUT THE REFUSAL. A USB transaction has exactly
-- one place a handshake may be sent: after the packet ends. There is no
-- mid-packet abort and no flow control inside a packet. The host will
-- transmit every byte it intended to and then wait for an answer.
--
-- So the engine keeps receiving, discards the bytes that do not fit, records
-- that an overflow happened, and NAKs at the end.
--
-- THE DECISION, AND ITS ORDER
--
--     halted        -> STALL,   rollback   (chapter 21.1: halt outranks all)
--     CRC failed    -> SILENCE, rollback   (21.1: a NAK would claim knowledge)
--     toggle wrong  -> ACK,     rollback   (21.1: a retransmission)
--     overflowed    -> NAK,     rollback   (no room; ask again)
--     otherwise     -> ACK,     COMMIT
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package usb_pktengine_pkg is
  type handshake_t is (H_NONE, H_ACK, H_NAK, H_STALL);

  -- WHY a packet was rolled back. Four causes that all look like "the data
  -- did not arrive" from the host's side, and all need different action from
  -- whoever is debugging the device.
  type drop_reason_t is (
    DROP_NONE,     -- nothing was dropped
    DROP_HALTED,   -- the endpoint is halted
    DROP_CRC,      -- the payload was corrupt
    DROP_TOGGLE,   -- a retransmission: we already have these bytes
    DROP_NO_ROOM   -- it did not fit
  );

  function hs_code(h : handshake_t) return std_logic_vector;
  function dr_code(d : drop_reason_t) return std_logic_vector;
end package;

package body usb_pktengine_pkg is
  function hs_code(h : handshake_t) return std_logic_vector is
  begin
    return std_logic_vector(to_unsigned(handshake_t'pos(h), 3));
  end function;

  function dr_code(d : drop_reason_t) return std_logic_vector is
  begin
    return std_logic_vector(to_unsigned(drop_reason_t'pos(d), 3));
  end function;
end package body;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_pktengine_pkg.all;

entity usb_packet_engine is
  generic (
    CAP : natural := 16;          -- endpoint buffer capacity in bytes
    PW  : natural := 5            -- pointer width: must hold 0 .. CAP
  );
  port (
    clk           : in  std_logic;
    rst_n         : in  std_logic;

    rx_start      : in  std_logic;   -- a DATA packet has begun
    rx_byte_valid : in  std_logic;   -- a payload byte is on rx_byte
    rx_byte       : in  std_logic_vector(7 downto 0);
    rx_end        : in  std_logic;   -- the packet ended; the two signals
    rx_crc_ok     : in  std_logic;   -- below are valid only in this cycle
    toggle_match  : in  std_logic;
    ep_halted     : in  std_logic;
    flush         : in  std_logic;

    wr_ptr        : out std_logic_vector(PW-1 downto 0); -- COMMITTED
    prov_ptr      : out std_logic_vector(PW-1 downto 0); -- PROVISIONAL
    prov_wr_en    : out std_logic;
    prov_wr_addr  : out std_logic_vector(PW-1 downto 0);
    prov_wr_data  : out std_logic_vector(7 downto 0);

    receiving     : out std_logic;
    overflowed    : out std_logic;
    commit        : out std_logic;
    rollback      : out std_logic;
    handshake     : out std_logic_vector(2 downto 0);
    drop_reason   : out std_logic_vector(2 downto 0);

    n_commit      : out std_logic_vector(31 downto 0);
    n_crc_drop    : out std_logic_vector(31 downto 0);
    n_dup_drop    : out std_logic_vector(31 downto 0);
    n_nak_space   : out std_logic_vector(31 downto 0);
    n_stall       : out std_logic_vector(31 downto 0);
    n_bytes       : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_packet_engine is
  signal wptr_r : natural range 0 to CAP := 0;
  signal prov_r : natural range 0 to CAP := 0;
  signal rx_r   : std_logic := '0';
  signal ovf_r  : std_logic := '0';

  signal have_room, accepting, dup : std_logic;
  signal hs   : handshake_t;
  signal dr   : drop_reason_t;
  signal cmt, rbk : std_logic;

  signal cmt_r, crc_r, dup_r, nak_r, stl_r, byt_r
       : unsigned(31 downto 0) := (others => '0');
begin
  wr_ptr     <= std_logic_vector(to_unsigned(wptr_r, PW));
  prov_ptr   <= std_logic_vector(to_unsigned(prov_r, PW));
  receiving  <= rx_r;
  overflowed <= ovf_r;

  -- A byte is written provisionally only while there is room. Once there is
  -- not, the packet is doomed -- but reception CONTINUES, because there is
  -- nowhere to put a refusal until the packet ends.
  have_room <= '1' when prov_r < CAP else '0';
  accepting <= rx_r and rx_byte_valid;

  prov_wr_en   <= accepting and have_room;
  prov_wr_addr <= std_logic_vector(to_unsigned(prov_r, PW));
  prov_wr_data <= rx_byte;

  -- A packet carrying the toggle we are NOT expecting is a retransmission of
  -- one already accepted (chapter 21.1). Its bytes are already in the buffer.
  dup <= not toggle_match;

  -- THE END-OF-PACKET DECISION. Nothing before rx_end can produce a
  -- handshake, because nothing before rx_end is known.
  decide : process (rx_end, ep_halted, rx_crc_ok, dup, ovf_r)
  begin
    if rx_end = '0' then
      hs <= H_NONE;
      dr <= DROP_NONE;
    elsif ep_halted = '1' then
      hs <= H_STALL;
      dr <= DROP_HALTED;
    elsif rx_crc_ok = '0' then
      hs <= H_NONE;                  -- silence, never a NAK
      dr <= DROP_CRC;
    elsif dup = '1' then
      hs <= H_ACK;                   -- re-ACK, store nothing
      dr <= DROP_TOGGLE;
    elsif ovf_r = '1' then
      hs <= H_NAK;
      dr <= DROP_NO_ROOM;
    else
      hs <= H_ACK;
      dr <= DROP_NONE;
    end if;
  end process;

  handshake   <= hs_code(hs);
  drop_reason <= dr_code(dr);

  -- Exactly one of commit and rollback happens at the end of every packet.
  cmt <= '1' when (rx_end = '1' and ep_halted = '0' and rx_crc_ok = '1'
                   and dup = '0' and ovf_r = '0') else '0';
  rbk <= '1' when (rx_end = '1' and cmt = '0') else '0';

  commit   <= cmt;
  rollback <= rbk;

  regs : process (clk, rst_n)
  begin
    if rst_n = '0' then
      wptr_r <= 0;
      prov_r <= 0;
      rx_r   <= '0';
      ovf_r  <= '0';
      cmt_r  <= (others => '0');
      crc_r  <= (others => '0');
      dup_r  <= (others => '0');
      nak_r  <= (others => '0');
      stl_r  <= (others => '0');
      byt_r  <= (others => '0');
    elsif rising_edge(clk) then
      if flush = '1' then
        -- A flush discards everything, provisional and committed alike.
        wptr_r <= 0;
        prov_r <= 0;
        rx_r   <= '0';
        ovf_r  <= '0';
      elsif rx_start = '1' then
        -- The provisional pointer starts from the committed one, every
        -- packet. Starting from a stale provisional pointer is the bug that
        -- corrupts the NEXT packet.
        rx_r   <= '1';
        prov_r <= wptr_r;
        ovf_r  <= '0';
      elsif rx_end = '1' then
        rx_r <= '0';
        if cmt = '1' then
          wptr_r <= prov_r;
          cmt_r  <= cmt_r + 1;
          byt_r  <= byt_r + to_unsigned(prov_r - wptr_r, 32);
        else
          -- ROLLBACK. The bytes are still physically in the buffer; what
          -- makes them not-written is that nothing points at them any more.
          prov_r <= wptr_r;
          if ep_halted = '1' then
            stl_r <= stl_r + 1;
          elsif rx_crc_ok = '0' then
            crc_r <= crc_r + 1;
          elsif dup = '1' then
            dup_r <= dup_r + 1;
          else
            nak_r <= nak_r + 1;
          end if;
        end if;
      elsif accepting = '1' then
        if have_room = '1' then
          prov_r <= prov_r + 1;
        else
          ovf_r <= '1';              -- doomed, but keep receiving
        end if;
      end if;
    end if;
  end process;

  n_commit    <= std_logic_vector(cmt_r);
  n_crc_drop  <= std_logic_vector(crc_r);
  n_dup_drop  <= std_logic_vector(dup_r);
  n_nak_space <= std_logic_vector(nak_r);
  n_stall     <= std_logic_vector(stl_r);
  n_bytes     <= std_logic_vector(byt_r);
end architecture;

9. Seeing the Rollback

A packet is written provisionally, its CRC fails, and firmware never sees a byte of it

usb_packet_engine — provisional write and rollback

10 cycles
A ten-cycle waveform. The committed pointer starts at 4. During cycles 1 to 4 four payload bytes arrive and the provisional pointer advances from 4 to 8 while the committed pointer stays at 4. At cycle 6 rx_end is asserted with rx_crc_ok low: rollback rises, drop_reason reads CRC, the committed pointer stays at 4 and the provisional pointer returns to 4.provisional writes beginprovisional writes begin4 bytes in the buffer, invisible4 bytes in the buffer,invisibleCRC fails: rollbackCRC fails: rollbackclkrx_byte_validrx_endrx_crc_okprov_wr_enprov_ptr4456784444wr_ptr4444444444rollbackdrop_reasonNONENONENONENONENONECRCNONENONENONENONEt0t1t2t3t4t5t6t7t8t9
Cycles 1–4: four bytes arrive and prov_ptr runs from 4 to 8 while wr_ptr stays at 4 — the bytes are physically in the buffer and invisible. Cycle 6: the CRC fails. wr_ptr does not move, prov_ptr falls back to 4, and the four bytes are unwritten by never having been pointed at.

wr_ptr is flat across the entire waveform. That row is the whole design: whatever happens during a packet, firmware's view of the buffer does not change until the packet is known to be good.

10. The Testbenches

The unit of stimulus here is a complete packet — start, N bytes, end, with a chosen CRC / toggle / halt outcome — so each testbench has a packet() task and drives everything through it. The exhaustive domain is over packets rather than cycles:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  17 starting committed-pointer positions (0 .. CAP)
    x  18 packet lengths (0 .. CAP+1, past capacity)
    x  2 (crc)  x  2 (toggle)  x  2 (halted)

    =  2448 complete packets, each driven byte by byte from a
       known buffer occupancy set up by a prior committed packet

Each bench also keeps a shadow model — its own buffer contents, its own two pointers, its own overflow flag — and a second memory mirroring the DUT's actual writes, so that a rollback can be checked for what it really means: firmware must never see those bytes, and the next packet must overwrite them.

10.1 Verilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_pe_v;
  localparam CAP = 16, PW = 5;
  reg clk=0, rst_n=0;
  reg rx_start=0, rx_byte_valid=0, rx_end=0;
  reg rx_crc_ok=1, toggle_match=1, ep_halted=0, flush=0;
  reg [7:0] rx_byte=0;
  wire [PW-1:0] wr_ptr, prov_ptr, prov_wr_addr;
  wire [7:0] prov_wr_data;
  wire prov_wr_en, receiving, overflowed, commit, rollback;
  wire [2:0] handshake, drop_reason;
  wire [31:0] n_commit, n_crc_drop, n_dup_drop, n_nak_space, n_stall, n_bytes;
  always #5 clk=~clk;

  usb_packet_engine #(.CAP(CAP), .PW(PW)) dut (
    .clk(clk), .rst_n(rst_n), .rx_start(rx_start),
    .rx_byte_valid(rx_byte_valid), .rx_byte(rx_byte), .rx_end(rx_end),
    .rx_crc_ok(rx_crc_ok), .toggle_match(toggle_match),
    .ep_halted(ep_halted), .flush(flush), .wr_ptr(wr_ptr),
    .prov_ptr(prov_ptr), .prov_wr_en(prov_wr_en),
    .prov_wr_addr(prov_wr_addr), .prov_wr_data(prov_wr_data),
    .receiving(receiving), .overflowed(overflowed), .commit(commit),
    .rollback(rollback), .handshake(handshake), .drop_reason(drop_reason),
    .n_commit(n_commit),
    .n_crc_drop(n_crc_drop), .n_dup_drop(n_dup_drop),
    .n_nak_space(n_nak_space), .n_stall(n_stall), .n_bytes(n_bytes));

  localparam [2:0] H_NONE=0, H_ACK=1, H_NAK=2, H_STALL=3;
  localparam [2:0] DROP_NONE=0, DROP_HALTED=1, DROP_CRC=2, DROP_TOGGLE=3,
                   DROP_NO_ROOM=4;

  // ---- SHADOW MODEL: an independent copy of the buffer and both pointers.
  // ---- The buffer contents are modelled too, so that a rollback can be
  // ---- checked for what it really means: firmware must never SEE the bytes.
  reg [7:0] s_mem [0:CAP-1];
  reg [7:0] d_mem [0:CAP-1];      // what the DUT's writes actually produced
  integer s_wptr, s_prov, s_ovf, s_rx;
  integer m_commit, m_crc, m_dup, m_nak, m_stall, m_bytes;

  integer errors=0, i, j, k, rlen;
  integer n_dom=0, n_rand=0;
  integer c_commit=0, c_crc=0, c_dup=0, c_ovf=0, c_stall=0, c_exact=0;

  task check(input cond, input [639:0] msg);
    begin if (!cond) begin errors=errors+1;
      if (errors <= 25)
        $display("  FAIL: %0s (rx=%b start=%b vld=%b end=%b crc=%b tog=%b halt=%b | wptr=%0d prov=%0d ovf=%b hs=%0d || model w=%0d p=%0d o=%0d, t=%0t)",
                 msg, receiving, rx_start, rx_byte_valid, rx_end, rx_crc_ok,
                 toggle_match, ep_halted, wr_ptr, prov_ptr, overflowed,
                 handshake, s_wptr, s_prov, s_ovf, $time);
    end end
  endtask

  task check_comb;
    reg [2:0] e_hs, e_drop;
    reg e_commit, e_rollback, e_dup;
    begin
      e_dup = !toggle_match;
      // The model decides with a case-over-conditions tree where the design
      // uses a ternary chain -- a different route to the same answer.
      case (1'b1)
        (!rx_end):    e_hs = H_NONE;
        ep_halted:    e_hs = H_STALL;
        (!rx_crc_ok): e_hs = H_NONE;
        e_dup:        e_hs = H_ACK;
        (s_ovf != 0): e_hs = H_NAK;
        default:      e_hs = H_ACK;
      endcase
      e_commit   = rx_end && !ep_halted && rx_crc_ok && !e_dup && (s_ovf == 0);
      e_rollback = rx_end && !e_commit;

      case (1'b1)
        (!rx_end):    e_drop = DROP_NONE;
        ep_halted:    e_drop = DROP_HALTED;
        (!rx_crc_ok): e_drop = DROP_CRC;
        e_dup:        e_drop = DROP_TOGGLE;
        (s_ovf != 0): e_drop = DROP_NO_ROOM;
        default:      e_drop = DROP_NONE;
      endcase

      check(handshake   === e_hs,   "handshake matches the model");
      check(drop_reason === e_drop, "drop_reason matches the model");
      // A rollback always has a reason, and a commit never does. A summary
      // that can disagree with the thing it summarises is worse than none.
      check((drop_reason !== DROP_NONE) === rollback,
            "drop_reason disagrees with rollback");
      check(commit    === e_commit,   "commit matches the model");
      check(rollback  === e_rollback, "rollback matches the model");
      check(wr_ptr    === s_wptr[PW-1:0],  "the COMMITTED pointer matches");
      check(prov_ptr  === s_prov[PW-1:0],  "the PROVISIONAL pointer matches");
      check(overflowed === (s_ovf != 0),   "overflowed matches the model");
      check(receiving  === (s_rx != 0),    "receiving matches the model");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE property. The committed pointer NEVER moves except on a
      //    commit. Everything else is provisional by construction.
      // (checked across the clock edge in step)
      // 2. The provisional pointer never falls behind the committed one --
      //    that would expose bytes the packet has not written yet.
      check(prov_ptr >= wr_ptr,
            "the provisional pointer fell behind the committed one");
      // 3. Neither pointer ever exceeds the buffer.
      check(wr_ptr   <= CAP[PW-1:0], "the committed pointer left the buffer");
      check(prov_ptr <= CAP[PW-1:0], "the provisional pointer left the buffer");
      // 4. Commit and rollback are mutually exclusive, and every packet end
      //    produces exactly one of them.
      check(!(commit && rollback), "commit and rollback both asserted");
      if (rx_end) check(commit || rollback,
                        "a packet ended with neither a commit nor a rollback");
      if (!rx_end) check(!commit && !rollback,
                         "a commit or rollback outside a packet end");
      // 5. A write only ever happens while receiving and with room.
      if (prov_wr_en)
        check(receiving && (prov_ptr < CAP[PW-1:0]),
              "a provisional write outside reception or past the buffer");
      // 6. A handshake is only ever produced at the end of a packet.
      if (!rx_end) check(handshake === H_NONE,
                         "a handshake was produced mid-packet");
      // 7. A failed CRC is answered with SILENCE, never a NAK (chapter 21.1).
      if (rx_end && !ep_halted && !rx_crc_ok)
        check(handshake === H_NONE,
              "a corrupt packet was answered -- the device claimed knowledge it lacks");
      // 8. A retransmission is ACKed and NOT committed (chapter 21.1).
      if (rx_end && !ep_halted && rx_crc_ok && e_dup) begin
        check(handshake === H_ACK, "a retransmission was not ACKed");
        check(!commit, "a retransmission was COMMITTED -- duplicated data");
      end
      // 9. An overflowed packet is never committed, whatever else is true.
      if (overflowed) check(!commit,
                            "a packet that did not fit was committed anyway");
    end
  endtask

  task model_step;
    begin
      if (flush) begin
        s_wptr=0; s_prov=0; s_rx=0; s_ovf=0;
      end else if (rx_start) begin
        s_rx=1; s_prov=s_wptr; s_ovf=0;
      end else if (rx_end) begin
        s_rx=0;
        if (!ep_halted && rx_crc_ok && toggle_match && (s_ovf==0)) begin
          m_bytes = m_bytes + (s_prov - s_wptr);
          s_wptr = s_prov;
          m_commit = m_commit + 1;
        end else begin
          s_prov = s_wptr;
          if (ep_halted)       m_stall = m_stall + 1;
          else if (!rx_crc_ok) m_crc   = m_crc + 1;
          else if (!toggle_match) m_dup = m_dup + 1;
          else                 m_nak   = m_nak + 1;
        end
      end else if (s_rx != 0 && rx_byte_valid) begin
        if (s_prov < CAP) begin
          s_mem[s_prov] = rx_byte;
          s_prov = s_prov + 1;
        end else s_ovf = 1;
      end
    end
  endtask

  task step;
    begin
      #1;
      check_comb;
      // mirror the DUT's own write into a separate memory, so the bench can
      // check what firmware would actually READ after a rollback
      if (prov_wr_en) d_mem[prov_wr_addr] = prov_wr_data;
      model_step;
      @(posedge clk); #1;
      check(wr_ptr   === s_wptr[PW-1:0], "the committed pointer tracked the model");
      check(prov_ptr === s_prov[PW-1:0], "the provisional pointer tracked the model");
      check(n_commit    === m_commit[31:0], "n_commit matches the model");
      check(n_crc_drop  === m_crc[31:0],    "n_crc_drop matches the model");
      check(n_dup_drop  === m_dup[31:0],    "n_dup_drop matches the model");
      check(n_nak_space === m_nak[31:0],    "n_nak_space matches the model");
      check(n_stall     === m_stall[31:0],  "n_stall matches the model");
      check(n_bytes     === m_bytes[31:0],  "n_bytes matches the model");
    end
  endtask

  task idle; begin rx_start=0; rx_byte_valid=0; rx_end=0; flush=0; end endtask

  task hard_reset;
    begin
      rst_n=0; idle; rx_crc_ok=1; toggle_match=1; ep_halted=0;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      s_wptr=0; s_prov=0; s_rx=0; s_ovf=0;
      m_commit=0; m_crc=0; m_dup=0; m_nak=0; m_stall=0; m_bytes=0;
    end
  endtask

  // Drive one complete packet of `len` bytes and end it with the given
  // conditions. This is the unit the whole block is organised around.
  task packet(input integer len, input crc, input tog, input halt);
    integer b;
    begin
      idle; rx_start=1; step; idle;
      for (b=0; b<len; b=b+1) begin
        rx_byte_valid=1; rx_byte = (8'hA0 + b[7:0]); step; idle;
      end
      rx_crc_ok=crc; toggle_match=tog; ep_halted=halt;
      rx_end=1; step; idle;
      rx_crc_ok=1; toggle_match=1; ep_halted=0;
    end
  endtask

  initial begin
    for (i=0;i<CAP;i=i+1) begin s_mem[i]=0; d_mem[i]=0; end
    hard_reset;
    check(wr_ptr === 5'd0, "reset leaves the committed pointer at zero");
    check(!receiving, "and not receiving");

    // ===== A. EXHAUSTIVE packet-outcome domain =====
    // 17 starting committed-pointer positions (0..16) x 18 packet lengths
    // (0..17, past the 16-byte capacity) x crc ok/bad x toggle match/mismatch
    // x halted/running = 17 x 18 x 8 = 2448 complete packets, each driven
    // byte by byte from a known buffer occupancy.
    for (i=0; i<=CAP; i=i+1)           // starting committed pointer
     for (j=0; j<=CAP+1; j=j+1)        // packet length, past capacity
      for (k=0; k<8; k=k+1) begin      // crc x toggle x halted
        hard_reset;
        // fill the buffer to `i` with a committed packet of length i
        if (i > 0) packet(i, 1'b1, 1'b1, 1'b0);
        check(wr_ptr === i[PW-1:0], "the prefill committed exactly i bytes");
        packet(j, k[2], k[1], k[0]);
        n_dom = n_dom + 1;
        if (k[0])            c_stall = c_stall + 1;
        else if (!k[2])      c_crc   = c_crc + 1;
        else if (!k[1])      c_dup   = c_dup + 1;
        else if (i + j > CAP) c_ovf  = c_ovf + 1;
        else begin
          c_commit = c_commit + 1;
          if (i + j == CAP) c_exact = c_exact + 1;
        end
      end
    $display("  exhaustive packet-outcome sweep: %0d of %0d packets verified",
             n_dom, 17*18*8);

    // ===== B. directed: the provisional-write story =====
    hard_reset;

    // 1. A good packet commits and the bytes become visible.
    packet(4, 1'b1, 1'b1, 1'b0);
    #1;
    check(!commit,
          "commit is a one-cycle pulse at the packet end and is gone by now");
    check(wr_ptr === 5'd4, "four bytes are committed");
    check(n_bytes === 32'd4, "and counted");

    // 2. THE case. A packet arrives, is written provisionally, and its CRC
    //    fails. The committed pointer must not have moved.
    rx_start=1; step; idle;
    for (i=0;i<6;i=i+1) begin rx_byte_valid=1; rx_byte=8'hFF; step; idle; end
    #1;
    check(prov_ptr === 5'd10,
          "the provisional pointer ran ahead to 10 -- the bytes ARE in the buffer");
    check(wr_ptr === 5'd4,
          "but the committed pointer has not moved: firmware still sees 4");
    rx_crc_ok=0; rx_end=1; step; idle; rx_crc_ok=1;
    check(wr_ptr === 5'd4, "after the CRC failure it is STILL 4");
    #1;
    check(prov_ptr === 5'd4,
          "and the provisional pointer rolled back to meet it");
    check(n_crc_drop === 32'd1, "one packet dropped for a failed CRC");
    check(n_bytes === 32'd4, "and no bytes were added");

    // 3. The next packet reuses the same buffer space. The 0xFF bytes from
    //    the dropped packet are physically still there and are overwritten.
    packet(3, 1'b1, 1'b1, 1'b0);
    check(wr_ptr === 5'd7, "three more bytes committed, from 4 to 7");
    check(d_mem[4] === 8'hA0,
          "the byte at offset 4 is from the NEW packet, not the dropped one");

    // 4. A retransmission: good CRC, wrong toggle. ACKed, not committed.
    packet(3, 1'b1, 1'b0, 1'b0);
    check(wr_ptr === 5'd7, "the committed pointer did not move");
    check(n_dup_drop === 32'd1, "and it was counted as a duplicate");

    // 5. Overflow. The buffer holds 7 of 16; a 12-byte packet does not fit.
    //    Reception continues to the end, and the NAK comes at the end.
    rx_start=1; step; idle;
    for (i=0;i<12;i=i+1) begin
      rx_byte_valid=1; rx_byte=8'h5A; step; idle;
      #1;
      if (i >= 9) check(overflowed,
                        "once the buffer filled, the packet is marked doomed");
      check(receiving,
            "but reception CONTINUES -- there is nowhere to put a refusal yet");
    end
    #1; check(prov_ptr === 5'd16, "the provisional pointer stopped at capacity");
    rx_end=1; step; idle;
    check(wr_ptr === 5'd7, "nothing was committed");
    check(n_nak_space === 32'd1, "and the packet was NAKed for want of room");

    // 6. A packet that fits EXACTLY fills the buffer and commits.
    hard_reset;
    packet(CAP, 1'b1, 1'b1, 1'b0);
    check(wr_ptr === CAP[PW-1:0], "a packet filling the buffer exactly commits");
    check(!overflowed, "and is NOT an overflow -- it fit");
    check(n_commit === 32'd1, "one commit");

    // 7. One more byte than fits does not.
    hard_reset;
    packet(CAP+1, 1'b1, 1'b1, 1'b0);
    check(wr_ptr === 5'd0, "one byte too many commits nothing at all");
    check(n_nak_space === 32'd1, "and is NAKed");

    // 8. A halted endpoint STALLs whatever else is true.
    hard_reset;
    packet(4, 1'b1, 1'b1, 1'b1);
    check(wr_ptr === 5'd0, "a halted endpoint commits nothing");
    check(n_stall === 32'd1, "and STALLs");

    // 9. A zero-length packet commits, and commits zero bytes.
    hard_reset;
    packet(0, 1'b1, 1'b1, 1'b0);
    check(n_commit === 32'd1, "a zero-length packet is a packet and commits");
    check(wr_ptr === 5'd0, "adding no bytes");
    check(n_bytes === 32'd0, "and counting none");

    // ===== C. randomised: many packets against one buffer =====
    hard_reset;
    for (i=0;i<4000;i=i+1) begin
      rlen = {$random}%19;
      if (({$random}%16)==0) begin flush=1; step; idle; end
      packet(rlen, ({$random}%8)!=0, ({$random}%8)!=0, ({$random}%32)==0);
      n_rand = n_rand + 1;
    end

    check(c_commit > 100, "commits happened often");
    check(c_crc    > 100, "CRC failures happened often");
    check(c_dup    > 100, "retransmissions happened often");
    check(c_ovf    > 100, "overflows happened often");
    check(c_stall  > 100, "halted packets happened often");
    check(c_exact  > 10,  "packets that exactly filled the buffer were seen");

    $display("");
    $display("  REACH: exhaustive-packets=%0d random-packets=%0d",
             n_dom, n_rand);
    $display("  OUTCOMES in the sweep: commit=%0d exact-fit=%0d crc-drop=%0d dup-drop=%0d overflow=%0d stall=%0d",
             c_commit, c_exact, c_crc, c_dup, c_ovf, c_stall);
    $display("  COUNTERS: commits=%0d bytes=%0d crc=%0d dup=%0d nak=%0d stall=%0d",
             n_commit, n_bytes, n_crc_drop, n_dup_drop, n_nak_space, n_stall);
    $display("  [Verilog] usb_packet_engine: %0d errors", errors);
    $display("  [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

10.2 SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_pe_sv;
  import usb_pktengine_pkg::*;

  localparam CAP = 16, PW = 5;
  logic clk=0, rst_n=0;
  logic rx_start=0, rx_byte_valid=0, rx_end=0;
  logic rx_crc_ok=1, toggle_match=1, ep_halted=0, flush=0;
  logic [7:0] rx_byte=0;
  logic [PW-1:0] wr_ptr, prov_ptr, prov_wr_addr;
  logic [7:0] prov_wr_data;
  logic prov_wr_en, receiving, overflowed, commit, rollback;
  handshake_e   handshake;
  drop_reason_e drop_reason;

  // Icarus seeds $random and $urandom identically, so an unseeded run would
  // replay the Verilog suite's stimulus exactly. See chapter 20.5 section 9.2.
  int urandom_seed = 21404;
  wire [31:0] n_commit, n_crc_drop, n_dup_drop, n_nak_space, n_stall, n_bytes;
  always #5 clk=~clk;

  usb_packet_engine #(.CAP(CAP), .PW(PW)) dut (
    .clk, .rst_n, .rx_start, .rx_byte_valid, .rx_byte, .rx_end, .rx_crc_ok,
    .toggle_match, .ep_halted, .flush, .wr_ptr, .prov_ptr, .prov_wr_en,
    .prov_wr_addr, .prov_wr_data, .receiving, .overflowed, .commit,
    .rollback, .handshake, .drop_reason, .n_commit, .n_crc_drop,
    .n_dup_drop, .n_nak_space, .n_stall, .n_bytes);

  // ---- SHADOW MODEL: an independent copy of the buffer and both pointers.
  // ---- The buffer contents are modelled too, so that a rollback can be
  // ---- checked for what it really means: firmware must never SEE the bytes.
  logic [7:0] s_mem [0:CAP-1];
  logic [7:0] d_mem [0:CAP-1];    // what the DUT's writes actually produced
  int s_wptr, s_prov, s_ovf, s_rx;
  int m_commit, m_crc, m_dup, m_nak, m_stall, m_bytes;

  int errors=0, i, j, k, rlen;
  int n_dom=0, n_rand=0;
  int c_commit=0, c_crc=0, c_dup=0, c_ovf=0, c_stall=0, c_exact=0;

  task automatic check(input bit cond, input string msg);
    // Icarus will not call .name() on a net, so the enum outputs are copied
    // into variables of the same type before being printed.
    handshake_e hs_v; drop_reason_e dr_v;
    if (!cond) begin
      errors++;
      hs_v = handshake; dr_v = drop_reason;
      if (errors <= 25)
        $display("  FAIL: %0s (rx=%b start=%b vld=%b end=%b crc=%b tog=%b halt=%b | wptr=%0d prov=%0d ovf=%b hs=%s why=%s || model w=%0d p=%0d o=%0d, t=%0t)",
                 msg, receiving, rx_start, rx_byte_valid, rx_end, rx_crc_ok,
                 toggle_match, ep_halted, wr_ptr, prov_ptr, overflowed,
                 hs_v.name(), dr_v.name(), s_wptr, s_prov, s_ovf, $time);
    end
  endtask

  task automatic check_comb;
    handshake_e e_hs; drop_reason_e e_drop;
    bit e_commit, e_rollback, e_dup;
    begin
      e_dup = !toggle_match;
      // The model decides with a case-over-conditions tree where the design
      // uses a ternary chain -- a different route to the same answer.
      case (1'b1)
        (!rx_end):    e_hs = H_NONE;
        ep_halted:    e_hs = H_STALL;
        (!rx_crc_ok): e_hs = H_NONE;
        e_dup:        e_hs = H_ACK;
        (s_ovf != 0): e_hs = H_NAK;
        default:      e_hs = H_ACK;
      endcase
      e_commit   = rx_end && !ep_halted && rx_crc_ok && !e_dup && (s_ovf == 0);
      e_rollback = rx_end && !e_commit;

      case (1'b1)
        (!rx_end):    e_drop = DROP_NONE;
        ep_halted:    e_drop = DROP_HALTED;
        (!rx_crc_ok): e_drop = DROP_CRC;
        e_dup:        e_drop = DROP_TOGGLE;
        (s_ovf != 0): e_drop = DROP_NO_ROOM;
        default:      e_drop = DROP_NONE;
      endcase

      check(handshake   === e_hs,   "handshake matches the model");
      check(drop_reason === e_drop, "drop_reason matches the model");
      // A rollback always has a reason, and a commit never does. A summary
      // that can disagree with the thing it summarises is worse than none.
      check((drop_reason !== DROP_NONE) === rollback,
            "drop_reason disagrees with rollback");
      check(commit    === e_commit,   "commit matches the model");
      check(rollback  === e_rollback, "rollback matches the model");
      check(wr_ptr    === PW'(s_wptr),  "the COMMITTED pointer matches");
      check(prov_ptr  === PW'(s_prov),  "the PROVISIONAL pointer matches");
      check(overflowed === (s_ovf != 0),   "overflowed matches the model");
      check(receiving  === (s_rx != 0),    "receiving matches the model");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE property. The committed pointer NEVER moves except on a
      //    commit. Everything else is provisional by construction.
      // (checked across the clock edge in step)
      // 2. The provisional pointer never falls behind the committed one --
      //    that would expose bytes the packet has not written yet.
      check(prov_ptr >= wr_ptr,
            "the provisional pointer fell behind the committed one");
      // 3. Neither pointer ever exceeds the buffer.
      check(wr_ptr   <= PW'(CAP), "the committed pointer left the buffer");
      check(prov_ptr <= PW'(CAP), "the provisional pointer left the buffer");
      // 4. Commit and rollback are mutually exclusive, and every packet end
      //    produces exactly one of them.
      check(!(commit && rollback), "commit and rollback both asserted");
      if (rx_end) check(commit || rollback,
                        "a packet ended with neither a commit nor a rollback");
      if (!rx_end) check(!commit && !rollback,
                         "a commit or rollback outside a packet end");
      // 5. A write only ever happens while receiving and with room.
      if (prov_wr_en)
        check(receiving && (prov_ptr < PW'(CAP)),
              "a provisional write outside reception or past the buffer");
      // 6. A handshake is only ever produced at the end of a packet.
      if (!rx_end) check(handshake === H_NONE,
                         "a handshake was produced mid-packet");
      // 7. A failed CRC is answered with SILENCE, never a NAK (chapter 21.1).
      if (rx_end && !ep_halted && !rx_crc_ok)
        check(handshake === H_NONE,
              "a corrupt packet was answered -- the device claimed knowledge it lacks");
      // 8. A retransmission is ACKed and NOT committed (chapter 21.1).
      if (rx_end && !ep_halted && rx_crc_ok && e_dup) begin
        check(handshake === H_ACK, "a retransmission was not ACKed");
        check(!commit, "a retransmission was COMMITTED -- duplicated data");
      end
      // 9. An overflowed packet is never committed, whatever else is true.
      if (overflowed) check(!commit,
                            "a packet that did not fit was committed anyway");
    end
  endtask

  task automatic model_step;
    begin
      if (flush) begin
        s_wptr=0; s_prov=0; s_rx=0; s_ovf=0;
      end else if (rx_start) begin
        s_rx=1; s_prov=s_wptr; s_ovf=0;
      end else if (rx_end) begin
        s_rx=0;
        if (!ep_halted && rx_crc_ok && toggle_match && (s_ovf==0)) begin
          m_bytes = m_bytes + (s_prov - s_wptr);
          s_wptr = s_prov;
          m_commit = m_commit + 1;
        end else begin
          s_prov = s_wptr;
          if (ep_halted)       m_stall = m_stall + 1;
          else if (!rx_crc_ok) m_crc   = m_crc + 1;
          else if (!toggle_match) m_dup = m_dup + 1;
          else                 m_nak   = m_nak + 1;
        end
      end else if (s_rx != 0 && rx_byte_valid) begin
        if (s_prov < CAP) begin
          s_mem[s_prov] = rx_byte;
          s_prov = s_prov + 1;
        end else s_ovf = 1;
      end
    end
  endtask

  task automatic step;
    begin
      #1;
      check_comb;
      // mirror the DUT's own write into a separate memory, so the bench can
      // check what firmware would actually READ after a rollback
      if (prov_wr_en) d_mem[prov_wr_addr] = prov_wr_data;
      model_step;
      @(posedge clk); #1;
      check(wr_ptr   === PW'(s_wptr), "the committed pointer tracked the model");
      check(prov_ptr === PW'(s_prov), "the provisional pointer tracked the model");
      check(n_commit    === 32'(m_commit), "n_commit matches the model");
      check(n_crc_drop  === 32'(m_crc),    "n_crc_drop matches the model");
      check(n_dup_drop  === 32'(m_dup),    "n_dup_drop matches the model");
      check(n_nak_space === 32'(m_nak),    "n_nak_space matches the model");
      check(n_stall     === 32'(m_stall),  "n_stall matches the model");
      check(n_bytes     === 32'(m_bytes),  "n_bytes matches the model");
    end
  endtask

  task automatic idle; begin rx_start=0; rx_byte_valid=0; rx_end=0; flush=0; end endtask

  task automatic hard_reset;
    begin
      rst_n=0; idle; rx_crc_ok=1; toggle_match=1; ep_halted=0;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      s_wptr=0; s_prov=0; s_rx=0; s_ovf=0;
      m_commit=0; m_crc=0; m_dup=0; m_nak=0; m_stall=0; m_bytes=0;
    end
  endtask

  // Drive one complete packet of `len` bytes and end it with the given
  // conditions. This is the unit the whole block is organised around.
  task automatic packet(input int len, input bit crc, input bit tog,
                        input bit halt);
    int b;
    begin
      idle; rx_start=1; step; idle;
      for (b=0; b<len; b=b+1) begin
        rx_byte_valid=1; rx_byte = 8'(8'hA0 + b); step; idle;
      end
      rx_crc_ok=crc; toggle_match=tog; ep_halted=halt;
      rx_end=1; step; idle;
      rx_crc_ok=1; toggle_match=1; ep_halted=0;
    end
  endtask

  initial begin
    void'($urandom(urandom_seed));
    for (i=0;i<CAP;i++) begin s_mem[i]=0; d_mem[i]=0; end
    hard_reset;
    check(wr_ptr === 5'd0, "reset leaves the committed pointer at zero");
    check(!receiving, "and not receiving");

    // ===== A. EXHAUSTIVE packet-outcome domain =====
    // 17 starting committed-pointer positions (0..16) x 18 packet lengths
    // (0..17, past the 16-byte capacity) x crc ok/bad x toggle match/mismatch
    // x halted/running = 17 x 18 x 8 = 2448 complete packets, each driven
    // byte by byte from a known buffer occupancy.
    for (i=0; i<=CAP; i=i+1)           // starting committed pointer
     for (j=0; j<=CAP+1; j=j+1)        // packet length, past capacity
      for (k=0; k<8; k=k+1) begin      // crc x toggle x halted
        hard_reset;
        // fill the buffer to `i` with a committed packet of length i
        if (i > 0) packet(i, 1'b1, 1'b1, 1'b0);
        check(wr_ptr === PW'(i), "the prefill committed exactly i bytes");
        packet(j, k[2], k[1], k[0]);
        n_dom = n_dom + 1;
        if (k[0])            c_stall = c_stall + 1;
        else if (!k[2])      c_crc   = c_crc + 1;
        else if (!k[1])      c_dup   = c_dup + 1;
        else if (i + j > CAP) c_ovf  = c_ovf + 1;
        else begin
          c_commit = c_commit + 1;
          if (i + j == CAP) c_exact = c_exact + 1;
        end
      end
    $display("  exhaustive packet-outcome sweep: %0d of %0d packets verified",
             n_dom, 17*18*8);

    // ===== B. directed: the provisional-write story =====
    hard_reset;

    // 1. A good packet commits and the bytes become visible.
    packet(4, 1'b1, 1'b1, 1'b0);
    #1;
    check(!commit,
          "commit is a one-cycle pulse at the packet end and is gone by now");
    check(wr_ptr === 5'd4, "four bytes are committed");
    check(n_bytes === 32'd4, "and counted");

    // 2. THE case. A packet arrives, is written provisionally, and its CRC
    //    fails. The committed pointer must not have moved.
    rx_start=1; step; idle;
    for (i=0;i<6;i=i+1) begin rx_byte_valid=1; rx_byte=8'hFF; step; idle; end
    #1;
    check(prov_ptr === 5'd10,
          "the provisional pointer ran ahead to 10 -- the bytes ARE in the buffer");
    check(wr_ptr === 5'd4,
          "but the committed pointer has not moved: firmware still sees 4");
    rx_crc_ok=0; rx_end=1; step; idle; rx_crc_ok=1;
    check(wr_ptr === 5'd4, "after the CRC failure it is STILL 4");
    #1;
    check(prov_ptr === 5'd4,
          "and the provisional pointer rolled back to meet it");
    check(n_crc_drop === 32'd1, "one packet dropped for a failed CRC");
    check(n_bytes === 32'd4, "and no bytes were added");

    // 3. The next packet reuses the same buffer space. The 0xFF bytes from
    //    the dropped packet are physically still there and are overwritten.
    packet(3, 1'b1, 1'b1, 1'b0);
    check(wr_ptr === 5'd7, "three more bytes committed, from 4 to 7");
    check(d_mem[4] === 8'hA0,
          "the byte at offset 4 is from the NEW packet, not the dropped one");

    // 4. A retransmission: good CRC, wrong toggle. ACKed, not committed.
    packet(3, 1'b1, 1'b0, 1'b0);
    check(wr_ptr === 5'd7, "the committed pointer did not move");
    check(n_dup_drop === 32'd1, "and it was counted as a duplicate");

    // 5. Overflow. The buffer holds 7 of 16; a 12-byte packet does not fit.
    //    Reception continues to the end, and the NAK comes at the end.
    rx_start=1; step; idle;
    for (i=0;i<12;i=i+1) begin
      rx_byte_valid=1; rx_byte=8'h5A; step; idle;
      #1;
      if (i >= 9) check(overflowed,
                        "once the buffer filled, the packet is marked doomed");
      check(receiving,
            "but reception CONTINUES -- there is nowhere to put a refusal yet");
    end
    #1; check(prov_ptr === 5'd16, "the provisional pointer stopped at capacity");
    rx_end=1; step; idle;
    check(wr_ptr === 5'd7, "nothing was committed");
    check(n_nak_space === 32'd1, "and the packet was NAKed for want of room");

    // 6. A packet that fits EXACTLY fills the buffer and commits.
    hard_reset;
    packet(CAP, 1'b1, 1'b1, 1'b0);
    check(wr_ptr === PW'(CAP), "a packet filling the buffer exactly commits");
    check(!overflowed, "and is NOT an overflow -- it fit");
    check(n_commit === 32'd1, "one commit");

    // 7. One more byte than fits does not.
    hard_reset;
    packet(CAP+1, 1'b1, 1'b1, 1'b0);
    check(wr_ptr === 5'd0, "one byte too many commits nothing at all");
    check(n_nak_space === 32'd1, "and is NAKed");

    // 8. A halted endpoint STALLs whatever else is true.
    hard_reset;
    packet(4, 1'b1, 1'b1, 1'b1);
    check(wr_ptr === 5'd0, "a halted endpoint commits nothing");
    check(n_stall === 32'd1, "and STALLs");

    // 9. A zero-length packet commits, and commits zero bytes.
    hard_reset;
    packet(0, 1'b1, 1'b1, 1'b0);
    check(n_commit === 32'd1, "a zero-length packet is a packet and commits");
    check(wr_ptr === 5'd0, "adding no bytes");
    check(n_bytes === 32'd0, "and counting none");

    // ===== C. randomised: many packets against one buffer =====
    hard_reset;
    for (i=0;i<4000;i=i+1) begin
      rlen = $urandom%19;
      if (($urandom%16)==0) begin flush=1; step; idle; end
      packet(rlen, ($urandom%8)!=0, ($urandom%8)!=0, ($urandom%32)==0);
      n_rand = n_rand + 1;
    end

    check(c_commit > 100, "commits happened often");
    check(c_crc    > 100, "CRC failures happened often");
    check(c_dup    > 100, "retransmissions happened often");
    check(c_ovf    > 100, "overflows happened often");
    check(c_stall  > 100, "halted packets happened often");
    check(c_exact  > 10,  "packets that exactly filled the buffer were seen");

    $display("");
    $display("  REACH: exhaustive-packets=%0d random-packets=%0d",
             n_dom, n_rand);
    $display("  OUTCOMES in the sweep: commit=%0d exact-fit=%0d crc-drop=%0d dup-drop=%0d overflow=%0d stall=%0d",
             c_commit, c_exact, c_crc, c_dup, c_ovf, c_stall);
    $display("  COUNTERS: commits=%0d bytes=%0d crc=%0d dup=%0d nak=%0d stall=%0d",
             n_commit, n_bytes, n_crc_drop, n_dup_drop, n_nak_space, n_stall);
    $display("  [SystemVerilog] usb_packet_engine: %0d errors", errors);
    $display("  [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

10.3 VHDL testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.usb_pktengine_pkg.all;

entity tb_pe_vhdl is
end entity;

architecture sim of tb_pe_vhdl is
  constant CAP : natural := 16;
  constant PW  : natural := 5;

  signal clk   : std_logic := '0';
  signal rst_n : std_logic := '0';
  signal rx_start, rx_byte_valid, rx_end, flush : std_logic := '0';
  signal rx_crc_ok, toggle_match : std_logic := '1';
  signal ep_halted : std_logic := '0';
  signal rx_byte : std_logic_vector(7 downto 0) := (others => '0');

  signal wr_ptr, prov_ptr, prov_wr_addr : std_logic_vector(PW-1 downto 0);
  signal prov_wr_data : std_logic_vector(7 downto 0);
  signal prov_wr_en, receiving, overflowed, commit, rollback : std_logic;
  signal handshake, drop_reason : std_logic_vector(2 downto 0);
  signal n_commit, n_crc_drop, n_dup_drop, n_nak_space, n_stall, n_bytes
       : std_logic_vector(31 downto 0);

  signal running : boolean := true;

  type mem_t is array (0 to CAP-1) of integer;
begin
  clk <= not clk after 5 ns when running else '0';

  dut : entity work.usb_packet_engine
    generic map (CAP => CAP, PW => PW)
    port map (clk => clk, rst_n => rst_n, rx_start => rx_start,
              rx_byte_valid => rx_byte_valid, rx_byte => rx_byte,
              rx_end => rx_end, rx_crc_ok => rx_crc_ok,
              toggle_match => toggle_match, ep_halted => ep_halted,
              flush => flush, wr_ptr => wr_ptr, prov_ptr => prov_ptr,
              prov_wr_en => prov_wr_en, prov_wr_addr => prov_wr_addr,
              prov_wr_data => prov_wr_data, receiving => receiving,
              overflowed => overflowed, commit => commit,
              rollback => rollback, handshake => handshake,
              drop_reason => drop_reason, n_commit => n_commit,
              n_crc_drop => n_crc_drop, n_dup_drop => n_dup_drop,
              n_nak_space => n_nak_space, n_stall => n_stall,
              n_bytes => n_bytes);

  stim : process
    variable seed1 : positive := 3313;
    variable seed2 : positive := 7717;
    variable r1    : real;

    -- VHDL-2008 requires a shared variable to have a protected type, so the
    -- bookkeeping lives inside the single stimulus process instead.
    variable errors : integer := 0;

    -- SHADOW MODEL: an independent copy of the buffer and both pointers.
    variable s_mem, d_mem : mem_t := (others => 0);
    variable s_wptr, s_prov, s_rx, s_ovf : integer := 0;
    variable m_commit, m_crc, m_dup, m_nak, m_stall, m_bytes : integer := 0;

    variable n_dom, n_rand : integer := 0;
    variable c_commit, c_crc, c_dup, c_ovf, c_stall, c_exact : integer := 0;

    procedure check(cond : boolean; msg : string) is
    begin
      if not cond then
        errors := errors + 1;
        if errors <= 25 then
          report "  FAIL: " & msg
               & " (rx=" & std_logic'image(receiving)(2)
               & " start=" & std_logic'image(rx_start)(2)
               & " vld=" & std_logic'image(rx_byte_valid)(2)
               & " end=" & std_logic'image(rx_end)(2)
               & " crc=" & std_logic'image(rx_crc_ok)(2)
               & " tog=" & std_logic'image(toggle_match)(2)
               & " halt=" & std_logic'image(ep_halted)(2)
               & " | wptr=" & integer'image(to_integer(unsigned(wr_ptr)))
               & " prov=" & integer'image(to_integer(unsigned(prov_ptr)))
               & " ovf=" & std_logic'image(overflowed)(2)
               & " hs=" & integer'image(to_integer(unsigned(handshake)))
               & " || model w=" & integer'image(s_wptr)
               & " p=" & integer'image(s_prov)
               & " o=" & integer'image(s_ovf)
               & ")" severity note;
        end if;
      end if;
    end procedure;

    procedure rnd(variable v : out integer; m : integer) is
    begin
      uniform(seed1, seed2, r1);
      v := integer(floor(r1 * real(m)));
    end procedure;

    procedure check_comb is
      variable e_hs   : handshake_t;
      variable e_drop : drop_reason_t;
      variable e_commit, e_rollback, e_dup : boolean;
    begin
      e_dup := toggle_match = '0';
      -- The model decides with a flat if-chain over booleans where the design
      -- uses signals and an enumerated process -- a different route.
      if rx_end = '0' then        e_hs := H_NONE;  e_drop := DROP_NONE;
      elsif ep_halted = '1' then  e_hs := H_STALL; e_drop := DROP_HALTED;
      elsif rx_crc_ok = '0' then  e_hs := H_NONE;  e_drop := DROP_CRC;
      elsif e_dup then            e_hs := H_ACK;   e_drop := DROP_TOGGLE;
      elsif s_ovf /= 0 then       e_hs := H_NAK;   e_drop := DROP_NO_ROOM;
      else                        e_hs := H_ACK;   e_drop := DROP_NONE;
      end if;
      e_commit := rx_end = '1' and ep_halted = '0' and rx_crc_ok = '1'
                  and not e_dup and s_ovf = 0;
      e_rollback := rx_end = '1' and not e_commit;

      check(handshake = hs_code(e_hs),     "handshake matches the model");
      check(drop_reason = dr_code(e_drop), "drop_reason matches the model");
      -- A rollback always has a reason, and a commit never does.
      check((drop_reason /= dr_code(DROP_NONE)) = (rollback = '1'),
            "drop_reason disagrees with rollback");
      check((commit = '1') = e_commit,     "commit matches the model");
      check((rollback = '1') = e_rollback, "rollback matches the model");
      check(to_integer(unsigned(wr_ptr)) = s_wptr,
            "the COMMITTED pointer matches");
      check(to_integer(unsigned(prov_ptr)) = s_prov,
            "the PROVISIONAL pointer matches");
      check((overflowed = '1') = (s_ovf /= 0), "overflowed matches the model");
      check((receiving = '1') = (s_rx /= 0),   "receiving matches the model");

      -- ---- SAFETY PROPERTIES, independent of the model ----
      -- 2. The provisional pointer never falls behind the committed one.
      check(to_integer(unsigned(prov_ptr)) >= to_integer(unsigned(wr_ptr)),
            "the provisional pointer fell behind the committed one");
      -- 3. Neither pointer ever exceeds the buffer.
      check(to_integer(unsigned(wr_ptr)) <= CAP,
            "the committed pointer left the buffer");
      check(to_integer(unsigned(prov_ptr)) <= CAP,
            "the provisional pointer left the buffer");
      -- 4. Commit and rollback are mutually exclusive, and every packet end
      --    produces exactly one of them.
      check(not (commit = '1' and rollback = '1'),
            "commit and rollback both asserted");
      if rx_end = '1' then
        check(commit = '1' or rollback = '1',
              "a packet ended with neither a commit nor a rollback");
      else
        check(commit = '0' and rollback = '0',
              "a commit or rollback outside a packet end");
      end if;
      -- 5. A write only ever happens while receiving and with room.
      if prov_wr_en = '1' then
        check(receiving = '1' and to_integer(unsigned(prov_ptr)) < CAP,
              "a provisional write outside reception or past the buffer");
      end if;
      -- 6. A handshake is only ever produced at the end of a packet.
      if rx_end = '0' then
        check(handshake = hs_code(H_NONE),
              "a handshake was produced mid-packet");
      end if;
      -- 7. A failed CRC is answered with SILENCE, never a NAK.
      if rx_end = '1' and ep_halted = '0' and rx_crc_ok = '0' then
        check(handshake = hs_code(H_NONE),
              "a corrupt packet was answered -- the device claimed knowledge it lacks");
      end if;
      -- 8. A retransmission is ACKed and NOT committed.
      if rx_end = '1' and ep_halted = '0' and rx_crc_ok = '1' and e_dup then
        check(handshake = hs_code(H_ACK), "a retransmission was not ACKed");
        check(commit = '0',
              "a retransmission was COMMITTED -- duplicated data");
      end if;
      -- 9. An overflowed packet is never committed.
      if overflowed = '1' then
        check(commit = '0', "a packet that did not fit was committed anyway");
      end if;
    end procedure;

    procedure model_step is
    begin
      if flush = '1' then
        s_wptr := 0; s_prov := 0; s_rx := 0; s_ovf := 0;
      elsif rx_start = '1' then
        s_rx := 1; s_prov := s_wptr; s_ovf := 0;
      elsif rx_end = '1' then
        s_rx := 0;
        if ep_halted = '0' and rx_crc_ok = '1' and toggle_match = '1'
           and s_ovf = 0 then
          m_bytes  := m_bytes + (s_prov - s_wptr);
          s_wptr   := s_prov;
          m_commit := m_commit + 1;
        else
          s_prov := s_wptr;
          if ep_halted = '1' then       m_stall := m_stall + 1;
          elsif rx_crc_ok = '0' then    m_crc   := m_crc + 1;
          elsif toggle_match = '0' then m_dup   := m_dup + 1;
          else                          m_nak   := m_nak + 1;
          end if;
        end if;
      elsif s_rx /= 0 and rx_byte_valid = '1' then
        if s_prov < CAP then
          s_mem(s_prov) := to_integer(unsigned(rx_byte));
          s_prov := s_prov + 1;
        else
          s_ovf := 1;
        end if;
      end if;
    end procedure;

    procedure step is
    begin
      wait for 1 ns;
      check_comb;
      -- mirror the DUT's own write into a separate memory, so the bench can
      -- check what firmware would actually READ after a rollback
      if prov_wr_en = '1' then
        d_mem(to_integer(unsigned(prov_wr_addr)))
          := to_integer(unsigned(prov_wr_data));
      end if;
      model_step;
      wait until rising_edge(clk);
      wait for 1 ns;
      check(to_integer(unsigned(wr_ptr)) = s_wptr,
            "the committed pointer tracked the model");
      check(to_integer(unsigned(prov_ptr)) = s_prov,
            "the provisional pointer tracked the model");
      check(n_commit = std_logic_vector(to_unsigned(m_commit, 32)),
            "n_commit matches the model");
      check(n_crc_drop = std_logic_vector(to_unsigned(m_crc, 32)),
            "n_crc_drop matches the model");
      check(n_dup_drop = std_logic_vector(to_unsigned(m_dup, 32)),
            "n_dup_drop matches the model");
      check(n_nak_space = std_logic_vector(to_unsigned(m_nak, 32)),
            "n_nak_space matches the model");
      check(n_stall = std_logic_vector(to_unsigned(m_stall, 32)),
            "n_stall matches the model");
      check(n_bytes = std_logic_vector(to_unsigned(m_bytes, 32)),
            "n_bytes matches the model");
    end procedure;

    procedure idle is
    begin
      rx_start <= '0'; rx_byte_valid <= '0'; rx_end <= '0'; flush <= '0';
    end procedure;

    procedure hard_reset is
    begin
      rst_n <= '0'; idle;
      rx_crc_ok <= '1'; toggle_match <= '1'; ep_halted <= '0';
      wait until rising_edge(clk); wait for 1 ns;
      wait until rising_edge(clk); wait for 1 ns;
      rst_n <= '1'; wait for 1 ns;
      s_wptr := 0; s_prov := 0; s_rx := 0; s_ovf := 0;
      m_commit := 0; m_crc := 0; m_dup := 0; m_nak := 0;
      m_stall := 0; m_bytes := 0;
    end procedure;

    -- Drive one complete packet of `len` bytes and end it with the given
    -- conditions. This is the unit the whole block is organised around.
    procedure packet(len : integer; crc : std_logic; tog : std_logic;
                     halt : std_logic) is
    begin
      idle; rx_start <= '1'; step; idle;
      for b in 0 to len - 1 loop
        rx_byte_valid <= '1';
        rx_byte <= std_logic_vector(to_unsigned((16#A0# + b) mod 256, 8));
        step; idle;
      end loop;
      rx_crc_ok <= crc; toggle_match <= tog; ep_halted <= halt;
      rx_end <= '1'; step; idle;
      rx_crc_ok <= '1'; toggle_match <= '1'; ep_halted <= '0';
    end procedure;

    variable iv, rlen : integer;
    variable kc, kt, kh : std_logic;
  begin
    hard_reset;
    check(to_integer(unsigned(wr_ptr)) = 0,
          "reset leaves the committed pointer at zero");
    check(receiving = '0', "and not receiving");

    -- ===== A. EXHAUSTIVE packet-outcome domain =====
    -- 17 starting committed-pointer positions (0..16) x 18 packet lengths
    -- (0..17, past the 16-byte capacity) x crc ok/bad x toggle match/mismatch
    -- x halted/running = 17 x 18 x 8 = 2448 complete packets.
    for i in 0 to CAP loop
      for j in 0 to CAP + 1 loop
        for k in 0 to 7 loop
          if (k / 4) mod 2 = 1 then kc := '1'; else kc := '0'; end if;
          if (k / 2) mod 2 = 1 then kt := '1'; else kt := '0'; end if;
          if k mod 2 = 1       then kh := '1'; else kh := '0'; end if;
          hard_reset;
          if i > 0 then packet(i, '1', '1', '0'); end if;
          check(to_integer(unsigned(wr_ptr)) = i,
                "the prefill committed exactly i bytes");
          packet(j, kc, kt, kh);
          n_dom := n_dom + 1;
          if kh = '1' then          c_stall := c_stall + 1;
          elsif kc = '0' then       c_crc := c_crc + 1;
          elsif kt = '0' then       c_dup := c_dup + 1;
          elsif i + j > CAP then    c_ovf := c_ovf + 1;
          else
            c_commit := c_commit + 1;
            if i + j = CAP then c_exact := c_exact + 1; end if;
          end if;
        end loop;
      end loop;
    end loop;
    report "  exhaustive packet-outcome sweep: " & integer'image(n_dom)
         & " of 2448 packets verified" severity note;

    -- ===== B. directed: the provisional-write story =====
    hard_reset;

    -- 1. A good packet commits and the bytes become visible.
    packet(4, '1', '1', '0');
    wait for 1 ns;
    check(commit = '0',
          "commit is a one-cycle pulse at the packet end and is gone by now");
    check(to_integer(unsigned(wr_ptr)) = 4, "four bytes are committed");
    check(n_bytes = std_logic_vector(to_unsigned(4, 32)), "and counted");

    -- 2. THE case. A packet arrives, is written provisionally, and its CRC
    --    fails. The committed pointer must not have moved.
    rx_start <= '1'; step; idle;
    for i in 0 to 5 loop
      rx_byte_valid <= '1'; rx_byte <= x"FF"; step; idle;
    end loop;
    wait for 1 ns;
    check(to_integer(unsigned(prov_ptr)) = 10,
          "the provisional pointer ran ahead to 10 -- the bytes ARE in the buffer");
    check(to_integer(unsigned(wr_ptr)) = 4,
          "but the committed pointer has not moved: firmware still sees 4");
    rx_crc_ok <= '0'; rx_end <= '1'; step; idle; rx_crc_ok <= '1';
    check(to_integer(unsigned(wr_ptr)) = 4,
          "after the CRC failure it is STILL 4");
    wait for 1 ns;
    check(to_integer(unsigned(prov_ptr)) = 4,
          "and the provisional pointer rolled back to meet it");
    check(n_crc_drop = std_logic_vector(to_unsigned(1, 32)),
          "one packet dropped for a failed CRC");
    check(n_bytes = std_logic_vector(to_unsigned(4, 32)),
          "and no bytes were added");

    -- 3. The next packet reuses the same buffer space. The 0xFF bytes from
    --    the dropped packet are physically still there and are overwritten.
    packet(3, '1', '1', '0');
    check(to_integer(unsigned(wr_ptr)) = 7,
          "three more bytes committed, from 4 to 7");
    check(d_mem(4) = 16#A0#,
          "the byte at offset 4 is from the NEW packet, not the dropped one");

    -- 4. A retransmission: good CRC, wrong toggle. ACKed, not committed.
    packet(3, '1', '0', '0');
    check(to_integer(unsigned(wr_ptr)) = 7,
          "the committed pointer did not move");
    check(n_dup_drop = std_logic_vector(to_unsigned(1, 32)),
          "and it was counted as a duplicate");

    -- 5. Overflow. The buffer holds 7 of 16; a 12-byte packet does not fit.
    --    Reception continues to the end, and the NAK comes at the end.
    rx_start <= '1'; step; idle;
    for i in 0 to 11 loop
      rx_byte_valid <= '1'; rx_byte <= x"5A"; step; idle;
      wait for 1 ns;
      if i >= 9 then
        check(overflowed = '1',
              "once the buffer filled, the packet is marked doomed");
      end if;
      check(receiving = '1',
            "but reception CONTINUES -- there is nowhere to put a refusal yet");
    end loop;
    wait for 1 ns;
    check(to_integer(unsigned(prov_ptr)) = 16,
          "the provisional pointer stopped at capacity");
    rx_end <= '1'; step; idle;
    check(to_integer(unsigned(wr_ptr)) = 7, "nothing was committed");
    check(n_nak_space = std_logic_vector(to_unsigned(1, 32)),
          "and the packet was NAKed for want of room");

    -- 6. A packet that fits EXACTLY fills the buffer and commits.
    hard_reset;
    packet(CAP, '1', '1', '0');
    check(to_integer(unsigned(wr_ptr)) = CAP,
          "a packet filling the buffer exactly commits");
    check(overflowed = '0', "and is NOT an overflow -- it fit");
    check(n_commit = std_logic_vector(to_unsigned(1, 32)), "one commit");

    -- 7. One more byte than fits does not.
    hard_reset;
    packet(CAP + 1, '1', '1', '0');
    check(to_integer(unsigned(wr_ptr)) = 0,
          "one byte too many commits nothing at all");
    check(n_nak_space = std_logic_vector(to_unsigned(1, 32)), "and is NAKed");

    -- 8. A halted endpoint STALLs whatever else is true.
    hard_reset;
    packet(4, '1', '1', '1');
    check(to_integer(unsigned(wr_ptr)) = 0,
          "a halted endpoint commits nothing");
    check(n_stall = std_logic_vector(to_unsigned(1, 32)), "and STALLs");

    -- 9. A zero-length packet commits, and commits zero bytes.
    hard_reset;
    packet(0, '1', '1', '0');
    check(n_commit = std_logic_vector(to_unsigned(1, 32)),
          "a zero-length packet is a packet and commits");
    check(to_integer(unsigned(wr_ptr)) = 0, "adding no bytes");
    check(n_bytes = std_logic_vector(to_unsigned(0, 32)), "and counting none");

    -- ===== C. randomised: many packets against one buffer =====
    -- ieee.math_real.uniform is a genuinely different generator from either
    -- Verilog builtin, which is what makes this column independent evidence.
    hard_reset;
    for i in 0 to 3999 loop
      rnd(rlen, 19);
      rnd(iv, 16);
      if iv = 0 then flush <= '1'; step; idle; end if;
      rnd(iv, 8);  if iv /= 0 then kc := '1'; else kc := '0'; end if;
      rnd(iv, 8);  if iv /= 0 then kt := '1'; else kt := '0'; end if;
      rnd(iv, 32); if iv = 0 then kh := '1'; else kh := '0'; end if;
      packet(rlen, kc, kt, kh);
      n_rand := n_rand + 1;
    end loop;

    check(c_commit > 100, "commits happened often");
    check(c_crc    > 100, "CRC failures happened often");
    check(c_dup    > 100, "retransmissions happened often");
    check(c_ovf    > 100, "overflows happened often");
    check(c_stall  > 100, "halted packets happened often");
    check(c_exact  > 10,  "packets that exactly filled the buffer were seen");

    report "  REACH: exhaustive-packets=" & integer'image(n_dom)
         & " random-packets=" & integer'image(n_rand) severity note;
    report "  OUTCOMES in the sweep: commit=" & integer'image(c_commit)
         & " exact-fit=" & integer'image(c_exact)
         & " crc-drop=" & integer'image(c_crc)
         & " dup-drop=" & integer'image(c_dup)
         & " overflow=" & integer'image(c_ovf)
         & " stall=" & integer'image(c_stall) severity note;
    report "  COUNTERS: commits="
         & integer'image(to_integer(unsigned(n_commit)))
         & " bytes=" & integer'image(to_integer(unsigned(n_bytes)))
         & " crc=" & integer'image(to_integer(unsigned(n_crc_drop)))
         & " dup=" & integer'image(to_integer(unsigned(n_dup_drop)))
         & " nak=" & integer'image(to_integer(unsigned(n_nak_space)))
         & " stall=" & integer'image(to_integer(unsigned(n_stall)))
         severity note;
    report "  [VHDL] usb_packet_engine: " & integer'image(errors) & " errors"
         severity note;
    if errors = 0 then
      report "  [VHDL] PASS" severity note;
    else
      report "  [VHDL] FAIL" severity failure;
    end if;
    running <= false;
    wait;
  end process;
end architecture;

11. Exhaustive Verification and a Provably Equivalent Mutation

MeasureVerilogSystemVerilogVHDL
Exhaustive packets2448 / 24482448 / 24482448 / 2448
Random packets400040004000
sweep: commits153153153
sweep: exact-fit commits171717
sweep: CRC drops612612612
sweep: retransmissions306306306
sweep: overflows153153153
sweep: halted122412241224
commits (total)771733734
bytes committed367934543408
CRC / dup / NAK / STALL481 / 459 / 2171 / 118511 / 440 / 2193 / 123496 / 469 / 2195 / 106
ResultPASSPASSPASS

The sweep columns are identical across all three languages because the sweep is deterministic — 2448 packets in a fixed order. The totals differ because the 4000 randomised packets come from three independent generators.

Now the interesting part.

This is the second provably-equivalent mutation in this curriculum, and both had the same shape: a term added to a condition that another term already implied. It is worth recognising on sight, because the alternative is hours spent strengthening a suite that was never weak.

12. Mutation Testing

#MutationVerilogSysVerVHDL
H1commit-as-you-go — no provisional stage at all319189331479321278
H2a rollback does not restore the provisional pointer558860795616
H3an overflowed packet is committed anyway198145210324200370
H4the room test is off by one (< CAP-1)254711258725261336
H5a retransmission is NAKed instead of ACKed153214941552
H6the engine gives up mid-packet on overflow331583383833451
H7a failed CRC is answered with NAK, not silence218822482218
—unmutated baseline000

All seven die, all counts distinct, columns within 4%.

H1 is the mutation this chapter exists for. Removing the provisional stage means the committed pointer advances with every byte — so a packet whose CRC fails leaves a partial, corrupt packet visible to firmware, and the n_bytes count is wrong for every dropped packet as well. At 319 000 failures it is the largest in the module, because every single byte of every failed packet is a violation.

H2 is the quietest at ~5600, and it is the most dangerous in the field. The rollback happens — the committed pointer does not move — but the provisional pointer is left where the failed packet stopped. The current packet is handled correctly. The next one starts writing in the wrong place, and its data lands at an offset that depends on the length of a packet that was discarded. That is a corruption whose position varies with traffic history, which is about the least reproducible thing a bug can be.

Note also that the design defends against H2 twice — the rollback restores the pointer and rx_start re-anchors it. Mutation H2 removes only the first, and the second is why the count is 5600 rather than 300 000: most packets are re-anchored before the damage shows. A redundant defence makes a bug rarer without making it less severe, which is exactly the combination that gets something shipped.

H5 and H7 are the two handshake mutations inherited from Chapter 21.1, at ~1500 and ~2200. They are the smallest because each changes exactly one output on one narrow condition, and they are worth keeping in this block's matrix precisely to confirm that composing the rules did not lose them.

13. Debugging Walkthrough: Corruption at an Offset That Moves

The report. A device occasionally delivers a buffer with a block of bytes in the wrong place — not corrupted values, but correct data at the wrong offset, as if part of a transfer had been pasted in shifted. Once every few hours under load. The offset is different every time.

Step 1 — corrupted or displaced? Displaced. The bytes are valid payload; they are in the wrong position. That rules out the CRC: a CRC failure gives you noise, not correct data in the wrong place. Something is writing to the right buffer at the wrong address.

Step 2 — is it the host or the device? Check whether the host ever sees a NAK or a retry around the event. It does: each corruption event is preceded by a transaction the host retried.

Step 3 — which retries? Instrument n_crc_drop, n_dup_drop, n_nak_space and n_stall separately — which is why drop_reason exists. The corruptions follow DROP_CRC events specifically, not the other three.

Step 4 — what is special about a CRC drop? It is the one outcome where bytes were written to the buffer and then disowned. A DROP_TOGGLE packet also writes bytes, but it is a retransmission of data already committed, so the same bytes land in the same place. A DROP_NO_ROOM packet writes nothing new past the cap. Only a CRC drop writes a fresh run of bytes that must then vanish.

Step 5 — the decisive trace. Capture wr_ptr and prov_ptr across a CRC-dropped packet. wr_ptr is correct: it does not move. prov_ptr does not come back. The next packet starts writing from where the failed one ended.

Step 6 — why the offset varies. The displacement equals the length of the discarded packet, which varies with traffic. And it only happens after a CRC failure, which only happens under electrical stress. Mutation H2, in production.

14. UVM: Building Packets, Not Cycles

The natural transaction here is a whole packet, not a bus cycle. A driver that randomises rx_start, rx_byte_valid and rx_end independently spends most of its time producing sequences that no bus could ever generate, and almost none producing the four-way conjunctions that matter.

14.1 The transaction

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class usb_pkt_item extends uvm_sequence_item;
  `uvm_object_utils(usb_pkt_item)

  // ONE PACKET, not one cycle. The driver expands this into rx_start, a run
  // of rx_byte_valid cycles, and rx_end -- a sequence the bus can actually
  // produce, which randomising the three signals independently would not.
  rand int unsigned length;
  rand bit          crc_ok;
  rand bit          toggle_match;
  rand bit          halted;
  rand bit          flush_before;

  // Lengths up to one past the buffer capacity, so overflow is reachable.
  constraint c_length { length inside {[0:17]}; }

  // A healthy bus. The error sequences below override these.
  constraint c_mostly_good {
    crc_ok       dist {1 := 88, 0 := 12};
    toggle_match dist {1 := 88, 0 := 12};
    halted       dist {0 := 97, 1 := 3};
    flush_before dist {0 := 94, 1 := 6};
  }

  // The boundary lengths get extra weight: 0 (a ZLP is a packet), exactly
  // the capacity, and one past it. A uniform draw over 0..17 visits the
  // exact-fit case 6% of the time and the suite needs it far more often.
  constraint c_boundaries {
    length dist { 0 := 12, 16 := 12, 17 := 12, [1:15] := 64 };
  }

  function new(string name = "usb_pkt_item"); super.new(name); endfunction

  function string convert2string();
    return $sformatf("len=%0d crc=%0b tog=%0b halt=%0b flush=%0b",
                     length, crc_ok, toggle_match, halted, flush_before);
  endfunction
endclass

14.2 Sequences

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// THE sequence for this chapter: a long packet that is written provisionally
// and then rejected, immediately followed by a good one. The property under
// test is that the good packet lands where the bad one STARTED, not where it
// ended -- which is mutation H2, the displaced-data bug from section 13.
class reject_then_accept_seq extends uvm_sequence #(usb_pkt_item);
  `uvm_object_utils(reject_then_accept_seq)
  function new(string name = "reject_then_accept_seq"); super.new(name); endfunction

  task body();
    repeat (400) begin
      usb_pkt_item it;

      // A packet that will be rolled back, long enough that a stale
      // provisional pointer is unmistakable.
      it = usb_pkt_item::type_id::create("it");
      start_item(it);
      it.c_mostly_good.constraint_mode(0);
      if (!it.randomize() with { crc_ok == 0; halted == 0;
                                 toggle_match == 1;
                                 length inside {[4:8]};
                                 flush_before == 0; })
        `uvm_error("RAND", "reject randomize failed")
      finish_item(it);

      // ...and a good one right behind it.
      it = usb_pkt_item::type_id::create("it");
      start_item(it);
      it.c_mostly_good.constraint_mode(0);
      if (!it.randomize() with { crc_ok == 1; halted == 0;
                                 toggle_match == 1;
                                 length inside {[1:4]};
                                 flush_before == 0; })
        `uvm_error("RAND", "accept randomize failed")
      finish_item(it);
    end
  endtask
endclass

// Fill the buffer to the brim, then present packets that do and do not fit.
// This is where the off-by-one in the room test lives: exactly CAP must
// commit and exactly CAP+1 must not.
class exact_fit_seq extends uvm_sequence #(usb_pkt_item);
  `uvm_object_utils(exact_fit_seq)
  function new(string name = "exact_fit_seq"); super.new(name); endfunction

  task body();
    repeat (300) begin
      usb_pkt_item it;
      int unsigned prefill = $urandom_range(0, 16);

      it = usb_pkt_item::type_id::create("it");
      start_item(it);
      it.c_mostly_good.constraint_mode(0);
      it.c_boundaries.constraint_mode(0);
      if (!it.randomize() with { flush_before == 1; crc_ok == 1;
                                 toggle_match == 1; halted == 0;
                                 length == prefill; })
        `uvm_error("RAND", "prefill randomize failed")
      finish_item(it);

      // Exactly the remaining space, or one more than it.
      it = usb_pkt_item::type_id::create("it");
      start_item(it);
      it.c_mostly_good.constraint_mode(0);
      it.c_boundaries.constraint_mode(0);
      if (!it.randomize() with { flush_before == 0; crc_ok == 1;
                                 toggle_match == 1; halted == 0;
                                 length inside {16 - prefill,
                                                17 - prefill}; })
        `uvm_error("RAND", "fit randomize failed")
      finish_item(it);
    end
  endtask
endclass

// A packet that overflows halfway. The property under test is that the
// engine keeps receiving to the end -- section 3's rule, and mutation H6.
class overflow_midpacket_seq extends uvm_sequence #(usb_pkt_item);
  `uvm_object_utils(overflow_midpacket_seq)
  function new(string name = "overflow_midpacket_seq"); super.new(name); endfunction

  task body();
    repeat (300) begin
      usb_pkt_item it;
      // fill most of the buffer...
      it = usb_pkt_item::type_id::create("it");
      start_item(it);
      it.c_mostly_good.constraint_mode(0);
      it.c_boundaries.constraint_mode(0);
      if (!it.randomize() with { flush_before == 1; crc_ok == 1;
                                 toggle_match == 1; halted == 0;
                                 length inside {[10:14]}; })
        `uvm_error("RAND", "prefill randomize failed")
      finish_item(it);
      // ...then send a packet far too long for what is left
      it = usb_pkt_item::type_id::create("it");
      start_item(it);
      it.c_mostly_good.constraint_mode(0);
      it.c_boundaries.constraint_mode(0);
      if (!it.randomize() with { flush_before == 0; crc_ok == 1;
                                 toggle_match == 1; halted == 0;
                                 length inside {[12:17]}; })
        `uvm_error("RAND", "overflow randomize failed")
      finish_item(it);
    end
  endtask
endclass

14.3 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;

  localparam int CAP = 16;

  // The scoreboard's own committed pointer, and its own idea of what
  // firmware can see. Comparing the DUT's wr_ptr against the DUT's prov_ptr
  // would be circular.
  int unsigned sb_committed;
  int unsigned n_commit, n_rollback, n_overflow, n_exact_fit;
  int unsigned drop_by[5];

  function new(string name, uvm_component parent);
    super.new(name, parent);
    ap = new("ap", this);
  endfunction

  // Called once per COMPLETE packet by the monitor.
  function void write(usb_pkt_mon_item t);
    bit will_overflow = (sb_committed + t.length) > CAP;
    bit should_commit = !t.halted && t.crc_ok && t.toggle_match
                        && !will_overflow;

    // ---- THE property. The committed pointer moves ONLY on a commit. ----
    if (should_commit) begin
      if (t.wr_ptr_after != sb_committed + t.length)
        `uvm_error("COMMIT",
          $sformatf("committed pointer is %0d, expected %0d",
                    t.wr_ptr_after, sb_committed + t.length))
      sb_committed += t.length;
      n_commit++;
      if (sb_committed == CAP) n_exact_fit++;
    end else begin
      if (t.wr_ptr_after != sb_committed)
        `uvm_error("ROLLBACK",
          $sformatf("committed pointer MOVED on a rejected packet: %0d, expected %0d -- firmware can see bytes that were never accepted",
                    t.wr_ptr_after, sb_committed))
      n_rollback++;
    end

    // ---- ...and the provisional pointer comes BACK. This is the one that
    // ---- displaces the NEXT packet rather than corrupting this one. ----
    if (t.prov_ptr_after != sb_committed)
      `uvm_error("STALE_PROV",
        $sformatf("provisional pointer left at %0d, expected %0d -- the next packet will be written at the wrong offset",
                  t.prov_ptr_after, sb_committed))

    // ---- Reception runs to the END of every packet, whatever happens ----
    if (t.bytes_seen != t.length)
      `uvm_error("EARLY_ABORT",
        $sformatf("the engine saw %0d of %0d bytes -- it stopped listening mid-packet, and there is nowhere to put a refusal until the packet ends",
                  t.bytes_seen, t.length))

    // ---- A packet that fits EXACTLY is not an overflow ----
    if (!will_overflow && t.overflowed)
      `uvm_error("OFF_BY_ONE",
        "a packet that fits was reported as an overflow -- the last buffer byte is unreachable")
    if (will_overflow && !t.overflowed)
      `uvm_error("OVERFLOW", "a packet that did not fit was not reported")
    if (will_overflow) n_overflow++;

    // ---- The reason must match the priority order ----
    begin
      drop_reason_e expect_why;
      if      (!t.halted && t.crc_ok && t.toggle_match && !will_overflow)
                                   expect_why = DROP_NONE;
      else if (t.halted)           expect_why = DROP_HALTED;
      else if (!t.crc_ok)          expect_why = DROP_CRC;
      else if (!t.toggle_match)    expect_why = DROP_TOGGLE;
      else                         expect_why = DROP_NO_ROOM;
      if (t.drop_reason != expect_why)
        `uvm_error("REASON",
          $sformatf("drop_reason=%s, expected %s -- four causes that need four different fixes",
                    t.drop_reason.name(), expect_why.name()))
      drop_by[int'(expect_why)]++;
    end

    if (t.flush_before) sb_committed = 0;
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf(
      "commits=%0d rollbacks=%0d overflows=%0d exact-fits=%0d",
      n_commit, n_rollback, n_overflow, n_exact_fit), UVM_LOW)

    // Every drop reason must have been exercised: they are four different
    // bugs wearing the same symptom.
    foreach (drop_by[i])
      if (drop_by[i] == 0)
        `uvm_error("COVERAGE",
          $sformatf("drop reason %0d never occurred", i))
    if (n_exact_fit == 0) `uvm_error("COVERAGE",
      "no packet ever filled the buffer exactly -- the off-by-one is untested")
  endfunction
endclass

14.4 Functional coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup pkt_engine_cg with function sample(
    int unsigned start_ptr, int unsigned length, bit crc, bit tog, bit halt,
    bit ovf, bit committed, drop_reason_e why);

  // Buffer occupancy BEFORE the packet, at the boundaries that matter.
  cp_start : coverpoint start_ptr {
    bins empty    = {0};
    bins partial  = {[1:15]};
    bins full     = {16};
  }

  // Packet length relative to the capacity, not in absolute bytes.
  cp_len : coverpoint length {
    bins zero     = {0};        // a ZLP is a packet and commits
    bins small    = {[1:15]};
    bins exact    = {16};       // exactly the capacity
    bins too_long = {17};       // one past it
  }

  cp_why : coverpoint why {
    bins none    = {DROP_NONE};
    bins halted  = {DROP_HALTED};
    bins crc     = {DROP_CRC};
    bins toggle  = {DROP_TOGGLE};
    bins no_room = {DROP_NO_ROOM};
  }

  cp_ovf : coverpoint ovf { bins fitted = {0}; bins overflowed = {1}; }

  // THE cross. Occupancy against length: the bin (start=partial, len=exact)
  // is where the room test's off-by-one lives, and (start=full, len=zero)
  // is the zero-length packet into a full buffer, which must still commit.
  x_fit : cross cp_start, cp_len;

  // Every drop reason from every starting occupancy -- a rollback from an
  // empty buffer and one from a nearly-full buffer exercise different
  // pointer arithmetic.
  x_why_start : cross cp_why, cp_start;

  // The four-way outcome conjunction. Crossing the three error inputs pins
  // the PRIORITY: halted beats CRC beats toggle beats room.
  cp_crc  : coverpoint crc  { bins ok = {1}; bins bad = {0}; }
  cp_tog  : coverpoint tog  { bins match = {1}; bins mismatch = {0}; }
  cp_halt : coverpoint halt { bins running = {0}; bins halted = {1}; }
  x_priority : cross cp_crc, cp_tog, cp_halt, cp_ovf;
endgroup

x_priority is a 16-bin cross whose only purpose is to pin the order of §4's decision. Any single error condition tells you the design reacts to it; only presenting two or three at once tells you which one wins — and the priority is the part a reimplementation gets wrong, because each individual rule looks independently correct.

15. SystemVerilog Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module usb_packet_engine_sva
  import usb_pktengine_pkg::*;
#(
  parameter int CAP = 16,
  parameter int PW  = 5
) (
  input logic          clk,
  input logic          rst_n,
  input logic          rx_start,
  input logic          rx_byte_valid,
  input logic          rx_end,
  input logic          rx_crc_ok,
  input logic          toggle_match,
  input logic          ep_halted,
  input logic          flush,
  input logic [PW-1:0] wr_ptr,
  input logic [PW-1:0] prov_ptr,
  input logic          prov_wr_en,
  input logic [PW-1:0] prov_wr_addr,
  input logic          receiving,
  input logic          overflowed,
  input logic          commit,
  input logic          rollback,
  input handshake_e    handshake,
  input drop_reason_e  drop_reason
);
  default clocking cb @(posedge clk); endclocking
  default disable iff (!rst_n);

  // ---- 1. THE property. The committed pointer moves ONLY on a commit or
  // ----    a flush. Everything else the engine does is provisional.
  property p_committed_moves_only_on_commit;
    (!$stable(wr_ptr)) |-> $past(commit || flush);
  endproperty
  a_committed_moves_only_on_commit :
    assert property (p_committed_moves_only_on_commit)
    else $error("the committed pointer moved without a commit -- firmware can see unaccepted bytes");

  // ---- 2. A rollback restores the provisional pointer. ----
  property p_rollback_restores_prov;
    (rollback && !flush) |=> (prov_ptr == $past(wr_ptr));
  endproperty
  a_rollback_restores_prov : assert property (p_rollback_restores_prov)
    else $error("a rollback left the provisional pointer stale -- the NEXT packet lands at the wrong offset");

  // ---- 3. Every packet starts from the committed pointer. ----
  property p_packet_starts_anchored;
    (rx_start && !flush) |=> (prov_ptr == $past(wr_ptr));
  endproperty
  a_packet_starts_anchored : assert property (p_packet_starts_anchored);

  // ---- 4. The provisional pointer never falls behind the committed one. ----
  property p_prov_never_behind;
    prov_ptr >= wr_ptr;
  endproperty
  a_prov_never_behind : assert property (p_prov_never_behind);

  // ---- 5. Neither pointer leaves the buffer. ----
  property p_pointers_bounded;
    (wr_ptr <= PW'(CAP)) && (prov_ptr <= PW'(CAP));
  endproperty
  a_pointers_bounded : assert property (p_pointers_bounded);

  // ---- 6. Exactly one of commit and rollback at every packet end, and
  // ----    neither at any other time.
  property p_one_outcome_per_packet;
    rx_end |-> (commit ^ rollback);
  endproperty
  a_one_outcome_per_packet : assert property (p_one_outcome_per_packet);

  property p_no_outcome_outside_end;
    !rx_end |-> (!commit && !rollback);
  endproperty
  a_no_outcome_outside_end : assert property (p_no_outcome_outside_end);

  // ---- 7. A handshake exists only at a packet end. ----
  property p_handshake_only_at_end;
    !rx_end |-> (handshake == H_NONE);
  endproperty
  a_handshake_only_at_end : assert property (p_handshake_only_at_end)
    else $error("a handshake was produced mid-packet -- there is nowhere on the bus to put it");

  // ---- 8. THE second property. Reception is never abandoned early: while
  // ----    a packet is in progress, only rx_end or a flush ends it.
  property p_no_early_abort;
    (receiving && !rx_end && !flush) |=> receiving;
  endproperty
  a_no_early_abort : assert property (p_no_early_abort)
    else $error("the engine stopped receiving mid-packet -- the bus is still sending and it is no longer listening");

  // ---- 9. An overflowed packet is never committed. ----
  property p_overflow_never_commits;
    overflowed |-> !commit;
  endproperty
  a_overflow_never_commits : assert property (p_overflow_never_commits);

  // ---- 10. A write only happens while receiving, with room. ----
  property p_write_needs_room;
    prov_wr_en |-> (receiving && (prov_ptr < PW'(CAP))
                    && (prov_wr_addr == prov_ptr));
  endproperty
  a_write_needs_room : assert property (p_write_needs_room);

  // ---- 11. A rollback always has a reason, a commit never does. ----
  property p_reason_matches_outcome;
    (drop_reason != DROP_NONE) == rollback;
  endproperty
  a_reason_matches_outcome : assert property (p_reason_matches_outcome);

  // ---- Cover: every outcome, and the boundary cases. ----
  c_commit     : cover property ((commit));
  c_crc_drop   : cover property ((rollback && (drop_reason == DROP_CRC)));
  c_dup_drop   : cover property ((rollback && (drop_reason == DROP_TOGGLE)));
  c_no_room    : cover property ((rollback && (drop_reason == DROP_NO_ROOM)));
  c_halted     : cover property ((rollback && (drop_reason == DROP_HALTED)));
  c_exact_fit  : cover property ((commit && (prov_ptr == PW'(CAP))));
  c_zlp_commit : cover property ((commit && (prov_ptr == wr_ptr)));
endmodule

bind usb_packet_engine usb_packet_engine_sva #(.CAP(CAP), .PW(PW)) u_sva (.*);

16. Common Misconceptions

"Wait for the CRC before writing anything." There is nowhere to wait. The payload can be 1024 bytes and the only storage is the buffer you would be writing into.

"A rollback has to erase the bytes." It does not. The bytes are only meaningful because a pointer says so; move the pointer back and they are gone. Erasing would cost cycles the device does not have.

"If the buffer fills mid-packet, stop receiving." There is nowhere to put the refusal until the packet ends. Stopping leaves the engine out of step with the bus (mutation H6, 33 000 failures).

"A packet that exactly fills the buffer is an overflow." It fits. have_room is prov_ptr < CAP, strictly — and the off-by-one makes the last byte of the buffer permanently unreachable (mutation H4).

"A CRC failure and a full buffer are both just 'packet dropped'." They need opposite responses from whoever is debugging: one is electrical, one is a firmware latency problem. That is why drop_reason exists.

"The rollback is enough — the packet start does not need to re-anchor the pointer." Two defences against the same bug is not waste here; it is what turns a 300 000-failure bug into a 5600-failure one, which is the difference between "caught in the first simulation" and "shipped". See H2.

"A zero-length packet has nothing to commit, so it can be skipped." It commits zero bytes and it is still a commit — the handshake, the counter and the toggle all depend on it. See Chapter 21.2.

17. Exercises

1. Make the committed pointer advance per byte (mutation H1) and predict, before running it, whether n_bytes or wr_ptr fails first. Explain why the count is 319 000 rather than the ~3500 bytes the suite commits.

2. The design re-anchors prov_ptr on rx_start and on rollback. Remove the rx_start anchor instead of the rollback one and measure the new count. Which of the two defences is worth more, and does that match which one you would have written first?

3. H6's first version was provably equivalent. Construct a third mutation of the overflow path that is also equivalent, prove it, and then construct one that looks equivalent and is not.

4. Add a rx_abort input modelling a bus reset arriving mid-packet. Which of the eleven SVA properties need changing? Property 8 says reception is never abandoned early — restate it so that it remains true and still forbids H6.

5. Property 1 uses $past(commit || flush). Write the version that does not use $past, using an auxiliary flop, and say which you would prefer in a formal run and which in simulation.

6. The scoreboard in §14.3 checks t.bytes_seen != t.length. Work out what the monitor has to do to produce bytes_seen, and why counting prov_wr_en pulses would give the wrong answer for an overflowing packet.

18. Summary

IdeaWhy it matters
The CRC is the last thing on the wirethe device must write before it knows
Provisional writes, one committed pointerfirmware sees only what was accepted
COMMIT: committed ← provisionalone assignment, no cycles
ROLLBACK: provisional ← committedthe bytes are unwritten by being unpointed-at
Re-anchor on rx_start as welltwo defences; H2 shows what the second is worth
You cannot change your mind mid-packetthere is nowhere to put a refusal until the end
So an overflow keeps receiving and NAKs at the endgiving up loses the bus
have_room is < CAP, strictlyan exact fit is a fit
halted > CRC > toggle > roomfour causes, three handshakes
drop_reason names whichfour different afternoons for four engineers
2448-packet exhaustive verificationevery occupancy × every length × every outcome
7 mutations, all killed in 3 languagesafter one was found provably equivalent

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o pe_v.out pe_v.v pe_v_tb.v && ./pe_v.out
SystemVerilogiverilog -g2012 -o pe_sv.out pe_sv.sv pe_sv_tb.sv && ./pe_sv.out
VHDL-2008 analysenvc --std=2008 -a pe_vhdl.vhd pe_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_pe_vhdl
VHDL-2008 runnvc --std=2008 -r tb_pe_vhdl
One mutationiverilog -g2005 -DMUT_H1 -o mm pe_v_mut.v pe_v_tb.v && ./mm

All three implementations pass with 0 errors: 2448 of 2448 exhaustive packets, 4000 randomised packets, every outcome reached and asserted reached.


Chapter 21.5 — Controller FSMs closes the module with the state machine that sits above all four of these blocks — Attached, Powered, Default, Address, Configured, Suspended. Its defining property is that a bus reset can arrive in any state and always returns to Default, and the part that gets missed is that returning to Default is not enough: every endpoint's toggle and halt must be flushed with it, or a re-enumerated device carries stale per-endpoint state into a fresh session.

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.