Skip to content
VLSI Mentor

USB · Module 21

Descriptor Engine

wLength is the size of the host's buffer, not a preference — and whether a zero-length packet must follow depends on comparing what was sent against what was asked for, not against what exists.

Chapter 21.2 established that a short packet terminates a transfer and that a zero-length packet is the terminator when the data ends on a boundary. This chapter is where that rule meets a number the host chose.

1. wLength Is a Promise About a Buffer

Every GET_DESCRIPTOR request carries a wLength field. It is easy to read it as "how much of the descriptor I want", which makes it sound like a preference. It is not a preference.

wLength is the size of the buffer the host has allocated. Sending more than that is a buffer overflow in the host's driver — in kernel memory, on the machine your device is plugged into.

And the host genuinely does ask for less than the whole thing, routinely, as part of ordinary enumeration:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   GET_DESCRIPTOR(CONFIGURATION, wLength = 9)
        the host does not yet know how long a configuration
        descriptor is. It asks for the 9-byte header.

        ... reads bLength and wTotalLength out of those 9 bytes ...

   GET_DESCRIPTOR(CONFIGURATION, wLength = 64)
        now it knows, and asks for all of it.

That first request is a 9-byte buffer. A device that ignores wLength and always sends its full 32-byte configuration descriptor writes 32 bytes into it.

So the first half of this block is one line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   send_len = min(wLength, descriptor length)

2. Knowing When to Stop

Now the harder half. A control read's data stage ends when either of two things happens:

ConditionWho observes it
1the device has sent exactly wLength bytesboth ends, by counting
2the device sends a short packet — fewer than wMaxPacketSize, and zero countsthe host, in-band

Either one terminates the transfer. The device must make sure at least one of them occurs, and that is not automatic.

Consider: the host asks for 64 bytes. The descriptor is 32 bytes. The endpoint's wMaxPacketSize is 32.

  • The device sends 32 bytes. That is one full packet — not short, so condition 2 does not fire.
  • 32 is not 64, so condition 1 does not fire either.
  • Nothing has terminated the transfer. The host is still waiting.

The device has to append a zero-length packet, which is short by definition, and the transfer ends.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   zlp_required = (send_len < wLength) && (send_len % mps == 0)

Both terms are load-bearing, and dropping either produces a real, shipped bug:

Drop this termWhat happens
send_len < wLengtha ZLP after a transfer that already ended by count — an extra packet the host did not ask for, which it sees at the start of the next transfer
send_len % mps == 0a ZLP after a short packet, which already terminated the transfer — same symptom
boththe multiple-of-mps truncated read hangs, exactly as in §2's example

Four requests for the same 32-byte descriptor, and the four different endings

Four request cases against a 32-byte configuration descriptor. wLength 9 sends 9 bytes and ends by short packet. wLength 32 sends 32 bytes and ends by count. wLength 64 with a 32-byte packet size sends 32 bytes, which is one whole packet and fewer than asked for, so a zero-length packet is appended. wLength 64 with a 64-byte packet size sends 32 bytes, which is already short, so no ZLP is needed.wLength 9mps 64 · sends 99 < 32the descriptor IS truncatedends: SHORT9 is not a whole packetwLength 32mps 32 · sends 32sent = askednothing was truncatedends: COUNTwLength bytes arrivedwLength 64mps 32 · sends 32whole packetand fewer than askedends: ZLPan empty packet is appendedwLength 64mps 64 · sends 3232 under 64already a short packetends: SHORTno ZLP needed12
Only the third needs a zero-length packet. The first ends because the byte count reached wLength; the second because the last packet was short; the fourth because both happened at once. The fifth outcome — none of them applying — is the hang.

3. An Unknown Descriptor Is Not an Empty One

The third structural decision. A host asks for descriptors the device may not have — a DEVICE_QUALIFIER on a full-speed-only device, a BOS descriptor on a USB 2 device, string index 7 when there are three strings.

That is not an error. It is how a host discovers what a device supports.

The correct answer is a STALL, which the host reads as "this device does not have that". The wrong answer — and it is a tempting one, because it makes the RTL simpler — is to return a descriptor of length zero:

STALLlength-0 descriptor
what the host concludes"not supported""supported, and empty"
what the host does nextmoves onmay retry, or mis-configure
distinguishable from a real empty descriptor?yesno

A device that answers every request with an empty descriptor enumerates successfully and then behaves oddly in ways that depend entirely on which host is asking. Mutation G4 in §12.

4. What We Are Building

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  usb_descriptor_engine

  request                       answer
  -------                       ------
  req_valid                     found        we have it
  desc_type   [2:0]             stall_req    we do not
  w_length    [7:0]             desc_len     how long it really is
  mps_sel     [1:0]             send_len     how much we will send
                                truncated    send_len < desc_len
                                zlp_required
                                n_packets    including the ZLP
                                mps
                                terminator   NONE / COUNT / SHORT / ZLP

  n_requests / n_stalls / n_truncated / n_zlp

terminator is worth a note. It names which of the three endings applies — and, crucially, it has a value for none of them:

terminatormeaning
END_BY_COUNTthe byte count reached wLength
END_BY_SHORTthe last packet was shorter than mps
END_BY_ZLPan empty packet was appended
END_NONEnothing ends this transfer — the host will hang

END_NONE is the whole reason the output exists. A hang is the absence of an event, and absences do not show up on a waveform. Giving the absence a name and an encoding turns "the transfer never ended" from something you infer after three hours into something the cursor tells you.

5. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_descriptor_engine -- serving a descriptor against a length the HOST
// chose, and the one-line rule that decides whether a zero-length packet has
// to follow it.
//
// THE HOST SAYS HOW MUCH IT WANTS
//
// Every GET_DESCRIPTOR request carries a wLength field: "send me at most this
// many bytes". It is not a request for the whole descriptor. It is a promise
// about the size of the buffer the host has allocated, and exceeding it is a
// buffer overflow in the host's driver.
//
// Enumeration depends on this. A host does not know how long a configuration
// descriptor is until it has read one, so it asks TWICE:
//
//     GET_DESCRIPTOR(CONFIG, wLength = 9)    <- just the header, please
//        ... reads bLength and wTotalLength from the 9 bytes ...
//     GET_DESCRIPTOR(CONFIG, wLength = 64)   <- now the whole thing
//
// So a device that ignores wLength and always sends the full descriptor does
// not merely waste bandwidth. On the first of those two requests it sends 64
// bytes into a 9-byte buffer.
//
//     send_len = min(wLength, descriptor length)
//
// THE HARD PART IS KNOWING WHEN TO STOP
//
// A control read's data stage ends when EITHER of two things happens:
//
//     1. the device has sent exactly wLength bytes, or
//     2. the device sends a SHORT packet -- fewer than wMaxPacketSize bytes,
//        and a zero-length packet counts (chapter 21.2)
//
// Condition 1 is a count both ends can do. Condition 2 is the in-band marker.
// Either one terminates the transfer, and the device must make sure at least
// one of them occurs -- otherwise the host waits for data that will never
// come, which is the enumeration hang every USB developer meets once.
//
// So a ZLP is required exactly when the device sent LESS than the host asked
// for AND the amount it sent was a whole number of packets:
//
//     zlp_required = (send_len < wLength) && (send_len % mps == 0)
//
// Both terms are load-bearing, and dropping either produces a real bug:
//
//   drop (send_len < wLength)   -> a ZLP after a transfer that already ended
//                                  by count. The host sees an extra packet it
//                                  did not ask for, on the next transfer.
//
//   drop (send_len % mps == 0)  -> a ZLP after a SHORT packet, which already
//                                  terminated the transfer. Same symptom.
//
//   drop the whole thing        -> the multiple-of-mps truncated read hangs.
//
// Note the boundary that catches people: send_len = 0 with wLength > 0 is
// aligned (0 % mps == 0) and less than wLength, so it DOES need a ZLP -- the
// device sends a single empty packet. But send_len = 0 with wLength = 0 needs
// nothing at all, because there is no data stage to terminate.
module usb_descriptor_engine (
  input  wire       clk,
  input  wire       rst_n,

  input  wire       req_valid,     // a GET_DESCRIPTOR arrived
  input  wire [2:0] desc_type,     // which descriptor
  input  wire [7:0] w_length,      // how much the host is willing to take
  input  wire [1:0] mps_sel,       // 0..3 -> wMaxPacketSize 8 / 16 / 32 / 64

  output wire       found,         // we have that descriptor
  output wire       stall_req,     // we do not: the request must be STALLed
  output wire [7:0] desc_len,      // how long it actually is
  output wire [7:0] send_len,      // how much we will send
  output wire       truncated,     // send_len < desc_len
  output wire       zlp_required,
  output wire [7:0] n_packets,     // including the ZLP if there is one
  output wire [7:0] mps,
  output wire [1:0] terminator,    // WHICH of the three ends this transfer

  output reg [31:0] n_requests,
  output reg [31:0] n_stalls,
  output reg [31:0] n_truncated,
  output reg [31:0] n_zlp
);
  // A tiny descriptor ROM. Types 0 and 6 are not descriptors this device
  // has, which is a THIRD outcome distinct from "found and empty".
  // Lengths chosen so that several land on exact packet boundaries.
  function [7:0] len_of;
    input [2:0] t;
    begin
      case (t)
        3'd1:    len_of = 8'd18;  // DEVICE
        3'd2:    len_of = 8'd32;  // CONFIGURATION  (exactly 32)
        3'd3:    len_of = 8'd4;   // STRING 0 (language IDs)
        3'd4:    len_of = 8'd16;  // STRING 1       (exactly 16)
        3'd5:    len_of = 8'd10;  // DEVICE_QUALIFIER
        3'd7:    len_of = 8'd64;  // BOS            (exactly 64)
        default: len_of = 8'd0;   // not a descriptor we have
      endcase
    end
  endfunction

  function have;
    input [2:0] t;
    begin
      have = (t != 3'd0) && (t != 3'd6);
    end
  endfunction

  assign found     = req_valid && have(desc_type);
  // An unknown descriptor is NOT "a descriptor of length zero". It is a
  // request the device cannot answer, and the protocol's way of saying so is
  // a STALL -- which the host uses to discover optional features.
  assign stall_req = req_valid && !have(desc_type);

  assign desc_len = found ? len_of(desc_type) : 8'd0;

  // wMaxPacketSize is always a power of two, so the alignment test is a mask
  // and the packet count is a shift. Real controllers rely on this; a design
  // that needs a divider here has chosen the wrong representation.
  assign mps = 8'd8 << mps_sel;
  wire [7:0] mps_mask = mps - 8'd1;

  // THE truncation. Send the smaller of what was asked for and what exists.
  assign send_len  = !found                  ? 8'd0
                   : (w_length < desc_len)   ? w_length
                                             : desc_len;
  assign truncated = found && (send_len < desc_len);

  // Was the amount we sent a whole number of packets? A zero-length send is
  // aligned, which is exactly why a truncated-to-nothing read still needs a
  // ZLP to terminate it.
  wire aligned = ((send_len & mps_mask) == 8'd0);

  // THE RULE. Both terms matter -- see the header comment.
  assign zlp_required = found && (send_len < w_length) && aligned;

  // Whole packets, plus a partial one if the send did not end on a boundary,
  // plus the terminating ZLP if one is required.
  wire [7:0] whole_packets = send_len >> (3 + mps_sel);
  assign n_packets = whole_packets
                   + (aligned       ? 8'd0 : 8'd1)
                   + (zlp_required  ? 8'd1 : 8'd0);

  // WHICH of the three terminators applies. Exactly one must, whenever a
  // descriptor was found and anything is going to be sent -- and naming the
  // "none of them applies" case is what turns a silent host hang into
  // something a waveform shows you.
  localparam [1:0] END_NONE     = 2'd0,  // no data stage at all
                   END_BY_COUNT = 2'd1,  // wLength bytes were sent
                   END_BY_SHORT = 2'd2,  // the last packet was short
                   END_BY_ZLP   = 2'd3;  // a zero-length packet was appended

  assign terminator = !found                   ? END_NONE
                    : zlp_required             ? END_BY_ZLP
                    : !aligned                 ? END_BY_SHORT
                    : (send_len == w_length)   ? END_BY_COUNT
                                               : END_NONE;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_requests  <= 32'd0;
      n_stalls    <= 32'd0;
      n_truncated <= 32'd0;
      n_zlp       <= 32'd0;
    end else if (req_valid) begin
      n_requests <= n_requests + 32'd1;
      if (stall_req)    n_stalls    <= n_stalls + 32'd1;
      if (truncated)    n_truncated <= n_truncated + 32'd1;
      if (zlp_required) n_zlp       <= n_zlp + 32'd1;
    end
  end
endmodule

6. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_descriptor_engine -- serving a descriptor against a length the HOST
// chose, and the one-line rule that decides whether a zero-length packet has
// to follow it.
//
// The SystemVerilog build names the three ways a data stage can end. See the
// comment above `terminator` near the bottom: making END_NONE a value the
// design can produce is what turns "nothing terminates this transfer" from a
// silent hang into something a waveform shows you.
//
// THE HOST SAYS HOW MUCH IT WANTS
//
// Every GET_DESCRIPTOR request carries a wLength field: "send me at most this
// many bytes". It is not a request for the whole descriptor. It is a promise
// about the size of the buffer the host has allocated, and exceeding it is a
// buffer overflow in the host's driver.
//
// Enumeration depends on this. A host does not know how long a configuration
// descriptor is until it has read one, so it asks TWICE:
//
//     GET_DESCRIPTOR(CONFIG, wLength = 9)    <- just the header, please
//        ... reads bLength and wTotalLength from the 9 bytes ...
//     GET_DESCRIPTOR(CONFIG, wLength = 64)   <- now the whole thing
//
// So a device that ignores wLength and always sends the full descriptor does
// not merely waste bandwidth. On the first of those two requests it sends 64
// bytes into a 9-byte buffer.
//
//     send_len = min(wLength, descriptor length)
//
// THE HARD PART IS KNOWING WHEN TO STOP
//
// A control read's data stage ends when EITHER of two things happens:
//
//     1. the device has sent exactly wLength bytes, or
//     2. the device sends a SHORT packet -- fewer than wMaxPacketSize bytes,
//        and a zero-length packet counts (chapter 21.2)
//
// Condition 1 is a count both ends can do. Condition 2 is the in-band marker.
// Either one terminates the transfer, and the device must make sure at least
// one of them occurs -- otherwise the host waits for data that will never
// come, which is the enumeration hang every USB developer meets once.
//
// So a ZLP is required exactly when the device sent LESS than the host asked
// for AND the amount it sent was a whole number of packets:
//
//     zlp_required = (send_len < wLength) && (send_len % mps == 0)
//
// Both terms are load-bearing, and dropping either produces a real bug:
//
//   drop (send_len < wLength)   -> a ZLP after a transfer that already ended
//                                  by count. The host sees an extra packet it
//                                  did not ask for, on the next transfer.
//
//   drop (send_len % mps == 0)  -> a ZLP after a SHORT packet, which already
//                                  terminated the transfer. Same symptom.
//
//   drop the whole thing        -> the multiple-of-mps truncated read hangs.
//
// Note the boundary that catches people: send_len = 0 with wLength > 0 is
// aligned (0 % mps == 0) and less than wLength, so it DOES need a ZLP -- the
// device sends a single empty packet. But send_len = 0 with wLength = 0 needs
// nothing at all, because there is no data stage to terminate.
package usb_desc_pkg;
  // The descriptor types this device serves. DESC_NONE and DESC_RSVD are not
  // gaps in the table -- they are requests the device answers with a STALL,
  // which is a THIRD outcome distinct from "found, and empty".
  typedef enum logic [2:0] {
    DESC_NONE   = 3'd0,   // not a descriptor: STALL
    DESC_DEVICE = 3'd1,
    DESC_CONFIG = 3'd2,
    DESC_STR0   = 3'd3,
    DESC_STR1   = 3'd4,
    DESC_QUAL   = 3'd5,
    DESC_RSVD   = 3'd6,   // not a descriptor: STALL
    DESC_BOS    = 3'd7
  } desc_type_e;

  // How a control read's data stage ENDS. Naming these is the point of the
  // chapter: exactly one of them must apply, or the host hangs.
  typedef enum logic [1:0] {
    END_NONE     = 2'd0,  // no data stage at all
    END_BY_COUNT = 2'd1,  // wLength bytes were sent
    END_BY_SHORT = 2'd2,  // the last packet was shorter than mps
    END_BY_ZLP   = 2'd3   // a zero-length packet was appended
  } term_e;
endpackage

module usb_descriptor_engine
  import usb_desc_pkg::*;
(
  input  logic       clk,
  input  logic       rst_n,

  input  logic       req_valid,   // a GET_DESCRIPTOR arrived
  input  desc_type_e desc_type,   // which descriptor
  input  logic [7:0] w_length,    // how much the host is willing to take
  input  logic [1:0] mps_sel,     // 0..3 -> wMaxPacketSize 8 / 16 / 32 / 64

  output logic       found,       // we have that descriptor
  output logic       stall_req,   // we do not: the request must be STALLed
  output logic [7:0] desc_len,    // how long it actually is
  output logic [7:0] send_len,    // how much we will send
  output logic       truncated,   // send_len < desc_len
  output logic       zlp_required,
  output logic [7:0] n_packets,   // including the ZLP if there is one
  output logic [7:0] mps,
  output term_e      terminator,  // WHICH of the three ends this transfer

  output logic [31:0] n_requests,
  output logic [31:0] n_stalls,
  output logic [31:0] n_truncated,
  output logic [31:0] n_zlp
);
  // A tiny descriptor ROM. Types 0 and 6 are not descriptors this device
  // has, which is a THIRD outcome distinct from "found and empty".
  // Lengths chosen so that several land on exact packet boundaries.
  function automatic logic [7:0] len_of(desc_type_e t);
    case (t)
      DESC_DEVICE: return 8'd18;
      DESC_CONFIG: return 8'd32;   // exactly 32
      DESC_STR0:   return 8'd4;
      DESC_STR1:   return 8'd16;   // exactly 16
      DESC_QUAL:   return 8'd10;
      DESC_BOS:    return 8'd64;   // exactly 64
      default:     return 8'd0;    // not a descriptor we have
    endcase
  endfunction

  function automatic bit have(desc_type_e t);
    return (t != DESC_NONE) && (t != DESC_RSVD);
  endfunction

  assign found     = req_valid && have(desc_type);
  // An unknown descriptor is NOT "a descriptor of length zero". It is a
  // request the device cannot answer, and the protocol's way of saying so is
  // a STALL -- which the host uses to discover optional features.
  assign stall_req = req_valid && !have(desc_type);

  assign desc_len = found ? len_of(desc_type) : 8'd0;

  // wMaxPacketSize is always a power of two, so the alignment test is a mask
  // and the packet count is a shift. Real controllers rely on this; a design
  // that needs a divider here has chosen the wrong representation.
  assign mps = 8'd8 << mps_sel;
  logic [7:0] mps_mask;
  assign mps_mask = mps - 8'd1;

  // THE truncation. Send the smaller of what was asked for and what exists.
  assign send_len  = !found                  ? 8'd0
                   : (w_length < desc_len)   ? w_length
                                             : desc_len;
  assign truncated = found && (send_len < desc_len);

  // Was the amount we sent a whole number of packets? A zero-length send is
  // aligned, which is exactly why a truncated-to-nothing read still needs a
  // ZLP to terminate it.
  logic aligned;
  assign aligned = ((send_len & mps_mask) == 8'd0);

  // THE RULE. Both terms matter -- see the header comment.
  assign zlp_required = found && (send_len < w_length) && aligned;

  // Whole packets, plus a partial one if the send did not end on a boundary,
  // plus the terminating ZLP if one is required.
  logic [7:0] whole_packets;
  assign whole_packets = send_len >> (3 + mps_sel);
  assign n_packets = whole_packets
                   + (aligned       ? 8'd0 : 8'd1)
                   + (zlp_required  ? 8'd1 : 8'd0);

  // WHICH of the three terminators applies. Exactly one must, whenever a
  // descriptor was found and anything is going to be sent -- and stating it
  // as an enumeration rather than as three booleans is what makes "none of
  // them applies" a visible value instead of a silent hang.
  always_comb begin
    if (!found)                       terminator = END_NONE;
    else if (zlp_required)            terminator = END_BY_ZLP;
    else if (!aligned)                terminator = END_BY_SHORT;
    else if (send_len == w_length)    terminator = END_BY_COUNT;
    else                              terminator = END_NONE;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_requests  <= '0;
      n_stalls    <= '0;
      n_truncated <= '0;
      n_zlp       <= '0;
    end else if (req_valid) begin
      n_requests <= n_requests + 1;
      if (stall_req)    n_stalls    <= n_stalls + 1;
      if (truncated)    n_truncated <= n_truncated + 1;
      if (zlp_required) n_zlp       <= n_zlp + 1;
    end
  end
endmodule

7. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- usb_descriptor_engine -- serving a descriptor against a length the HOST
-- chose, and the one-line rule that decides whether a zero-length packet has
-- to follow it.
--
-- THE HOST SAYS HOW MUCH IT WANTS
--
-- Every GET_DESCRIPTOR request carries a wLength field: "send me at most this
-- many bytes". It is not a request for the whole descriptor. It is a promise
-- about the size of the buffer the host has allocated, and exceeding it is a
-- buffer overflow in the host's driver.
--
-- Enumeration depends on this. A host does not know how long a configuration
-- descriptor is until it has read one, so it asks TWICE:
--
--     GET_DESCRIPTOR(CONFIG, wLength = 9)    <- just the header, please
--        ... reads bLength and wTotalLength from the 9 bytes ...
--     GET_DESCRIPTOR(CONFIG, wLength = 64)   <- now the whole thing
--
-- A device that ignores wLength and always sends the full descriptor does not
-- merely waste bandwidth: on the first of those two requests it sends 64
-- bytes into a 9-byte buffer.
--
--     send_len = min(wLength, descriptor length)
--
-- THE HARD PART IS KNOWING WHEN TO STOP
--
-- A control read's data stage ends when EITHER of two things happens:
--
--     1. the device has sent exactly wLength bytes, or
--     2. the device sends a SHORT packet -- fewer than wMaxPacketSize bytes,
--        and a zero-length packet counts (chapter 21.2)
--
-- Either one terminates the transfer, and the device must ensure at least one
-- occurs -- otherwise the host waits for data that never comes, which is the
-- enumeration hang every USB developer meets once.
--
--     zlp_required = (send_len < wLength) and (send_len mod mps = 0)
--
-- Both terms are load-bearing:
--
--   drop (send_len < wLength)   -> a ZLP after a transfer that already ended
--                                  by count: an extra packet the host did
--                                  not ask for, seen on the NEXT transfer.
--
--   drop (send_len mod mps = 0) -> a ZLP after a SHORT packet, which already
--                                  terminated the transfer. Same symptom.
--
--   drop the whole thing        -> the multiple-of-mps truncated read hangs.
--
-- Note the boundary that catches people: send_len = 0 with wLength > 0 is
-- aligned and less than wLength, so it DOES need a ZLP -- a single empty
-- packet. But send_len = 0 with wLength = 0 needs nothing at all, because
-- there is no data stage to terminate.
--
-- VHDL names both the descriptor set and the three ways a data stage can end,
-- which is what makes END_NONE -- "nothing terminates this transfer" -- a
-- value the design can be caught producing rather than a silent hang.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package usb_desc_pkg is
  type desc_type_t is (
    DESC_NONE,    -- not a descriptor: STALL
    DESC_DEVICE,
    DESC_CONFIG,
    DESC_STR0,
    DESC_STR1,
    DESC_QUAL,
    DESC_RSVD,    -- not a descriptor: STALL
    DESC_BOS
  );

  type term_t is (
    END_NONE,      -- no data stage at all
    END_BY_COUNT,  -- wLength bytes were sent
    END_BY_SHORT,  -- the last packet was shorter than mps
    END_BY_ZLP     -- a zero-length packet was appended
  );

  function desc_decode(t : std_logic_vector(2 downto 0)) return desc_type_t;
  function term_code(t : term_t) return std_logic_vector;
  function len_of(t : desc_type_t) return natural;
  function have(t : desc_type_t) return boolean;
end package;

package body usb_desc_pkg is
  function desc_decode(t : std_logic_vector(2 downto 0)) return desc_type_t is
  begin
    return desc_type_t'val(to_integer(unsigned(t)));
  end function;

  function term_code(t : term_t) return std_logic_vector is
  begin
    return std_logic_vector(to_unsigned(term_t'pos(t), 2));
  end function;

  -- Lengths chosen so that several land on exact packet boundaries.
  function len_of(t : desc_type_t) return natural is
  begin
    case t is
      when DESC_DEVICE => return 18;
      when DESC_CONFIG => return 32;   -- exactly 32
      when DESC_STR0   => return 4;
      when DESC_STR1   => return 16;   -- exactly 16
      when DESC_QUAL   => return 10;
      when DESC_BOS    => return 64;   -- exactly 64
      when others      => return 0;    -- not a descriptor we have
    end case;
  end function;

  function have(t : desc_type_t) return boolean is
  begin
    return (t /= DESC_NONE) and (t /= DESC_RSVD);
  end function;
end package body;

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

entity usb_descriptor_engine is
  port (
    clk          : in  std_logic;
    rst_n        : in  std_logic;

    req_valid    : in  std_logic;                     -- a GET_DESCRIPTOR
    desc_type    : in  std_logic_vector(2 downto 0);
    w_length     : in  std_logic_vector(7 downto 0);  -- what the host will take
    mps_sel      : in  std_logic_vector(1 downto 0);  -- mps = 8 / 16 / 32 / 64

    found        : out std_logic;
    stall_req    : out std_logic;
    desc_len     : out std_logic_vector(7 downto 0);
    send_len     : out std_logic_vector(7 downto 0);
    truncated    : out std_logic;
    zlp_required : out std_logic;
    n_packets    : out std_logic_vector(7 downto 0);
    mps          : out std_logic_vector(7 downto 0);
    terminator   : out std_logic_vector(1 downto 0);

    n_requests   : out std_logic_vector(31 downto 0);
    n_stalls     : out std_logic_vector(31 downto 0);
    n_truncated  : out std_logic_vector(31 downto 0);
    n_zlp        : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_descriptor_engine is
  signal dt      : desc_type_t;
  signal fnd     : std_logic;
  signal stl     : std_logic;
  signal dlen    : natural range 0 to 255 := 0;
  signal slen    : natural range 0 to 255 := 0;
  signal wlen    : natural range 0 to 255 := 0;
  signal mps_i   : natural range 0 to 255 := 8;
  signal aligned : boolean := true;
  signal zlp     : std_logic;
  signal trunc   : std_logic;
  signal npkt    : natural range 0 to 255 := 0;
  signal term    : term_t;

  signal req_r, stl_r, tr_r, zlp_r : unsigned(31 downto 0) := (others => '0');
begin
  dt   <= desc_decode(desc_type);
  wlen <= to_integer(unsigned(w_length));

  fnd <= '1' when (req_valid = '1' and have(dt)) else '0';
  -- An unknown descriptor is NOT "a descriptor of length zero". It is a
  -- request the device cannot answer, and the protocol's way of saying so is
  -- a STALL -- which is how a host discovers optional features.
  stl <= '1' when (req_valid = '1' and not have(dt)) else '0';

  dlen <= len_of(dt) when fnd = '1' else 0;

  -- wMaxPacketSize is always a power of two, so the alignment test is a mask
  -- and the packet count is a shift. A design that needs a divider here has
  -- chosen the wrong representation.
  mps_i <= 8 * (2 ** to_integer(unsigned(mps_sel)));

  -- THE truncation. Send the smaller of what was asked for and what exists.
  minimum : process (fnd, wlen, dlen)
  begin
    if fnd = '0' then
      slen <= 0;
    elsif wlen < dlen then
      slen <= wlen;
    else
      slen <= dlen;
    end if;
  end process;

  trunc <= '1' when (fnd = '1' and slen < dlen) else '0';

  -- Was the amount we sent a whole number of packets? A zero-length send is
  -- aligned, which is exactly why a truncated-to-nothing read still needs a
  -- ZLP to terminate it.
  aligned <= (slen mod mps_i) = 0;

  -- THE RULE. Both terms matter -- see the header comment.
  zlp <= '1' when (fnd = '1' and slen < wlen and aligned) else '0';

  -- Whole packets, plus a partial one if the send did not end on a boundary,
  -- plus the terminating ZLP if one is required.
  packets : process (slen, mps_i, aligned, zlp)
    variable p : natural range 0 to 255;
  begin
    p := slen / mps_i;
    if not aligned then
      p := p + 1;
    end if;
    if zlp = '1' then
      p := p + 1;
    end if;
    npkt <= p;
  end process;

  -- WHICH of the three terminators applies. Exactly one must, whenever a
  -- descriptor was found -- and naming the "none of them applies" case is
  -- what turns a silent host hang into something a waveform shows you.
  which_end : process (fnd, zlp, aligned, slen, wlen)
  begin
    if fnd = '0' then
      term <= END_NONE;
    elsif zlp = '1' then
      term <= END_BY_ZLP;
    elsif not aligned then
      term <= END_BY_SHORT;
    elsif slen = wlen then
      term <= END_BY_COUNT;
    else
      term <= END_NONE;
    end if;
  end process;

  found        <= fnd;
  stall_req    <= stl;
  desc_len     <= std_logic_vector(to_unsigned(dlen, 8));
  send_len     <= std_logic_vector(to_unsigned(slen, 8));
  truncated    <= trunc;
  zlp_required <= zlp;
  n_packets    <= std_logic_vector(to_unsigned(npkt, 8));
  mps          <= std_logic_vector(to_unsigned(mps_i, 8));
  terminator   <= term_code(term);

  regs : process (clk, rst_n)
  begin
    if rst_n = '0' then
      req_r <= (others => '0');
      stl_r <= (others => '0');
      tr_r  <= (others => '0');
      zlp_r <= (others => '0');
    elsif rising_edge(clk) then
      if req_valid = '1' then
        req_r <= req_r + 1;
        if stl = '1' then
          stl_r <= stl_r + 1;
        end if;
        if trunc = '1' then
          tr_r <= tr_r + 1;
        end if;
        if zlp = '1' then
          zlp_r <= zlp_r + 1;
        end if;
      end if;
    end if;
  end process;

  n_requests  <= std_logic_vector(req_r);
  n_stalls    <= std_logic_vector(stl_r);
  n_truncated <= std_logic_vector(tr_r);
  n_zlp       <= std_logic_vector(zlp_r);
end architecture;

8. Seeing the Rule Fire

Four requests, four endings

usb_descriptor_engine — the four ways a data stage ends

10 cycles
A ten-cycle waveform. At cycle 1 a 9-byte request for a 32-byte descriptor sends 9 bytes and the terminator reads SHORT. At cycle 3 a 32-byte request sends 32 bytes and the terminator reads COUNT. At cycle 5 a 64-byte request on a 32-byte endpoint sends 32 bytes, zlp_required rises and the terminator reads ZLP with two packets. At cycle 7 an unknown descriptor type asserts stall_req with send_len zero.9-byte probe: short packet9-byte probe: short packetwhole packet, more asked: ZLPwhole packet, more asked:ZLPunknown descriptor: STALLunknown descriptor: STALLclkreq_validdesc_type0CONFIGCONFIGCONFIGCONFIGCONFIGCONFIGRSVDRSVDRSVDw_length09932326464646464mps64646432323232646464send_len090320320000zlp_requiredterminatorNONESHORTNONECOUNTNONEZLPNONENONENONENONEn_packets0101020000t0t1t2t3t4t5t6t7t8t9
Cycle 1: the host's 9-byte probe — a short packet ends it. Cycle 3: the full 32 bytes — the count ends it. Cycle 5: 32 bytes into a 64-byte request on a 32-byte endpoint — nothing has ended it, so zlp_required rises and n_packets becomes 2. Cycle 7: an unknown descriptor is STALLed and offers nothing.

9. The Testbenches

Each suite sweeps the entire decision surface — 8 descriptor types × 80 wLength values (0…79, past the longest descriptor) × 4 packet sizes = 2560 points — then runs the eight-step enumeration sequence a real host performs, then 40 000 randomised requests against a reference model that counts packets in a loop where the design shifts, and takes the minimum with an if where the design uses a ternary.

9.1 Verilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_de_v;
  reg clk=0, rst_n=0;
  reg req_valid=0;
  reg [2:0] desc_type=0;
  reg [7:0] w_length=0;
  reg [1:0] mps_sel=3;
  wire found, stall_req, truncated, zlp_required;
  wire [7:0] desc_len, send_len, n_packets, mps;
  wire [1:0] terminator;
  wire [31:0] n_requests, n_stalls, n_truncated, n_zlp;
  always #5 clk=~clk;

  usb_descriptor_engine dut (
    .clk(clk), .rst_n(rst_n), .req_valid(req_valid), .desc_type(desc_type),
    .w_length(w_length), .mps_sel(mps_sel), .found(found),
    .stall_req(stall_req), .desc_len(desc_len), .send_len(send_len),
    .truncated(truncated), .zlp_required(zlp_required),
    .n_packets(n_packets), .mps(mps), .terminator(terminator),
    .n_requests(n_requests),
    .n_stalls(n_stalls), .n_truncated(n_truncated), .n_zlp(n_zlp));

  integer errors=0, i, t, w, m;
  integer n_exh=0;
  integer n_found=0, n_stall=0, n_trunc=0, n_zlpreq=0, n_exact=0, n_partial=0;
  integer n_bytype [0:7];
  integer m_req, m_stall, m_trunc, m_zlp;

  task check(input cond, input [639:0] msg);
    begin if (!cond) begin errors=errors+1;
      if (errors <= 25)
        $display("  FAIL: %0s (type=%0d wLen=%0d mps=%0d | found=%b dlen=%0d slen=%0d trunc=%b zlp=%b npkt=%0d, t=%0t)",
                 msg, desc_type, w_length, mps, found, desc_len, send_len,
                 truncated, zlp_required, n_packets, $time);
    end end
  endtask

  // ---- independent reference model. It computes the packet count by
  // ---- COUNTING packets in a loop where the design uses a shift, and takes
  // ---- the minimum with an if where the design uses a ternary.
  task model(output e_found, output e_stall, output [7:0] e_dlen,
             output [7:0] e_slen, output e_trunc, output e_zlp,
             output [7:0] e_npkt);
    integer real_len, mps_i, sent, pkts;
    begin
      case (desc_type)
        3'd1: real_len = 18;
        3'd2: real_len = 32;
        3'd3: real_len = 4;
        3'd4: real_len = 16;
        3'd5: real_len = 10;
        3'd7: real_len = 64;
        default: real_len = -1;      // not a descriptor we have
      endcase

      mps_i  = 8 << mps_sel;
      e_found = req_valid && (real_len >= 0);
      e_stall = req_valid && (real_len < 0);
      e_dlen  = e_found ? real_len[7:0] : 8'd0;

      if (!e_found) sent = 0;
      else if (w_length < real_len) sent = w_length;
      else sent = real_len;
      e_slen  = sent[7:0];
      e_trunc = e_found && (sent < real_len);

      // The rule, restated: a ZLP is needed when we sent less than was asked
      // for and what we sent was a whole number of packets.
      e_zlp = e_found && (sent < w_length) && ((sent % mps_i) == 0);

      // Count the packets one at a time -- a different route from the
      // design's shift-and-add.
      pkts = 0;
      begin : countloop
        integer left;
        left = sent;
        while (left >= mps_i) begin
          pkts = pkts + 1;
          left = left - mps_i;
        end
        if (left > 0) pkts = pkts + 1;
      end
      if (e_zlp) pkts = pkts + 1;
      e_npkt = pkts[7:0];
    end
  endtask

  localparam [1:0] END_NONE=0, END_BY_COUNT=1, END_BY_SHORT=2, END_BY_ZLP=3;

  task check_comb;
    reg e_found, e_stall, e_trunc, e_zlp;
    reg [7:0] e_dlen, e_slen, e_npkt;
    reg [1:0] e_term;
    integer mps_i;
    begin
      model(e_found, e_stall, e_dlen, e_slen, e_trunc, e_zlp, e_npkt);
      mps_i = 8 << mps_sel;

      check(found        === e_found, "found matches the model");
      check(stall_req    === e_stall, "stall_req matches the model");
      check(desc_len     === e_dlen,  "desc_len matches the model");
      check(send_len     === e_slen,  "send_len matches the model");
      check(truncated    === e_trunc, "truncated matches the model");
      check(zlp_required === e_zlp,   "zlp_required matches the model");
      check(n_packets    === e_npkt,  "n_packets matches the model");
      check(mps          === mps_i[7:0], "mps decodes from mps_sel");

      // The terminator, computed by the model as a flat if-chain.
      if      (!e_found)                 e_term = END_NONE;
      else if (e_zlp)                    e_term = END_BY_ZLP;
      else if ((e_slen % mps_i) != 0)    e_term = END_BY_SHORT;
      else if (e_slen == w_length)       e_term = END_BY_COUNT;
      else                               e_term = END_NONE;
      check(terminator === e_term, "terminator matches the model");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE property. Never send the host more than it asked for. This is
      //    a buffer overflow in the host driver, not a protocol nicety.
      check(send_len <= w_length,
            "more bytes were offered than the host asked for -- host buffer overflow");
      // 2. And never more than the descriptor actually has.
      check(send_len <= desc_len,
            "more bytes were offered than the descriptor contains");
      // 3. found and stall_req are mutually exclusive, and one of them holds
      //    whenever a request is present.
      check(!(found && stall_req), "found and stall_req both asserted");
      if (req_valid) check(found || stall_req,
                           "a request produced neither a descriptor nor a STALL");
      if (!req_valid) check(!found && !stall_req,
                            "an output asserted with no request");
      // 4. A transfer must terminate: either we sent exactly what was asked
      //    for, or the last packet is short, or a ZLP follows. If none of the
      //    three holds, the host waits for ever.
      if (found)
        check((send_len == w_length)
              || ((send_len % mps_i) != 0)
              || zlp_required,
              "nothing terminates this transfer -- the host will hang");
      // 4b. ...restated through the named terminator: a found descriptor
      //     always has one, and it agrees with the booleans.
      if (found)
        check(terminator !== END_NONE,
              "a descriptor was found with no way to end its data stage");
      check((terminator === END_BY_ZLP) === zlp_required,
            "terminator disagrees with zlp_required");
      // 5. A ZLP is never sent when the byte count already ended the
      //    transfer: that would be an extra packet the host did not expect.
      if (send_len == w_length)
        check(!zlp_required,
              "a ZLP was added after a transfer that already ended by count");
      // 6. Nor after a short packet, which already terminated it.
      if (found && ((send_len % mps_i) != 0))
        check(!zlp_required,
              "a ZLP was added after a short packet -- two terminators");
      // 7. An unknown descriptor offers nothing at all.
      if (stall_req)
        check((send_len == 8'd0) && (n_packets == 8'd0) && !zlp_required,
              "a STALLed request still offered data");
      // 8. The packet count must be able to carry the bytes.
      check(n_packets * mps_i >= send_len,
            "n_packets cannot carry send_len bytes");

      if (e_found) begin
        n_found = n_found + 1;
        n_bytype[desc_type] = n_bytype[desc_type] + 1;
        if (e_trunc) n_trunc = n_trunc + 1;
        if (e_zlp)   n_zlpreq = n_zlpreq + 1;
        if ((e_slen % mps_i) == 0) n_exact = n_exact + 1;
        else n_partial = n_partial + 1;
      end
      if (e_stall) n_stall = n_stall + 1;
    end
  endtask

  task step;
    begin
      #1;
      check_comb;
      if (req_valid) begin
        m_req = m_req + 1;
        if (stall_req)    m_stall = m_stall + 1;
        if (truncated)    m_trunc = m_trunc + 1;
        if (zlp_required) m_zlp   = m_zlp + 1;
      end
      @(posedge clk); #1;
      check(n_requests  === m_req[31:0],   "n_requests matches the model");
      check(n_stalls    === m_stall[31:0], "n_stalls matches the model");
      check(n_truncated === m_trunc[31:0], "n_truncated matches the model");
      check(n_zlp       === m_zlp[31:0],   "n_zlp matches the model");
    end
  endtask

  task hard_reset;
    begin
      rst_n=0; req_valid=0; desc_type=0; w_length=0; mps_sel=3;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      m_req=0; m_stall=0; m_trunc=0; m_zlp=0;
    end
  endtask

  initial begin
    for (i=0;i<8;i=i+1) n_bytype[i]=0;
    hard_reset;
    check(!found && !stall_req, "no request, no answer");

    // ===== A. EXHAUSTIVE sweep of the whole decision =====
    // 8 descriptor types x 80 wLength values (0..79, past the longest
    // descriptor) x 4 max-packet-sizes = 2560 points. Every truncation
    // boundary and every packet-size alignment is visited by construction.
    req_valid = 1;
    for (t=0; t<8; t=t+1)
     for (w=0; w<80; w=w+1)
      for (m=0; m<4; m=m+1) begin
        desc_type=t[2:0]; w_length=w[7:0]; mps_sel=m[1:0];
        step;
        n_exh = n_exh + 1;
      end
    req_valid = 0;
    $display("  exhaustive descriptor sweep: %0d of %0d points verified",
             n_exh, 8*80*4);

    // ===== B. directed: the enumeration sequence a real host performs =====
    hard_reset;

    // 1. The host asks for the first 9 bytes of the configuration
    //    descriptor, because it does not yet know how long the whole thing
    //    is. wLength = 9, descriptor = 32.
    req_valid=1; desc_type=3'd2; w_length=8'd9; mps_sel=2'd3; step;
    check(found, "the configuration descriptor exists");
    check(desc_len === 8'd32, "and it is 32 bytes long");
    check(send_len === 8'd9,
          "but only 9 bytes are sent -- the host asked for 9");
    check(truncated, "so the descriptor was truncated");
    check(!zlp_required,
          "and no ZLP is needed: 9 bytes is exactly what was asked for");
    check(n_packets === 8'd1, "one short packet carries it");

    // 2. Now the host knows the length and asks for all of it.
    req_valid=1; desc_type=3'd2; w_length=8'd32; mps_sel=2'd3; step;
    check(send_len === 8'd32, "all 32 bytes are sent");
    check(!truncated, "nothing was truncated");
    check(!zlp_required,
          "and no ZLP: the byte count reached wLength exactly");

    // 3. THE case. The host asks for MORE than exists, and what exists is an
    //    exact multiple of the packet size.
    req_valid=1; desc_type=3'd2; w_length=8'd64; mps_sel=2'd2; step;  // mps=32
    check(mps === 8'd32, "a 32-byte max packet size");
    check(send_len === 8'd32, "32 bytes are sent -- all the descriptor has");
    check(!truncated,
          "the DESCRIPTOR was not truncated: the host asked for more than exists");
    check(zlp_required,
          "so a ZLP IS required: 32 bytes is one whole packet and the host wants more");
    check(n_packets === 8'd2, "one full packet plus the terminating ZLP");

    // 4. The same request with a packet size that does NOT divide it.
    req_valid=1; desc_type=3'd1; w_length=8'd64; mps_sel=2'd3; step;  // 18, mps 64
    check(send_len === 8'd18, "18 bytes are sent");
    check(!truncated,
          "again nothing was truncated -- 18 is the whole descriptor");
    check(!zlp_required,
          "but NO ZLP: 18 is a short packet and already terminates the transfer");
    check(n_packets === 8'd1, "one short packet");

    // 5. The boundary that catches people: nothing to send, but the host
    //    asked for something.
    req_valid=1; desc_type=3'd3; w_length=8'd0; mps_sel=2'd3; step;
    check(send_len === 8'd0, "zero bytes sent");
    check(!zlp_required,
          "and NO ZLP: wLength was 0, so there is no data stage to terminate");
    check(n_packets === 8'd0, "no packets at all");

    // 6. An unknown descriptor is STALLed, not answered with an empty one.
    req_valid=1; desc_type=3'd6; w_length=8'd64; mps_sel=2'd3; step;
    check(!found, "type 6 is not a descriptor this device has");
    check(stall_req, "so the request is STALLed");
    check(send_len === 8'd0, "and nothing is offered");
    check(n_packets === 8'd0, "and no packets are counted");

    // 7. A descriptor exactly one max-packet-size long, asked for in full.
    req_valid=1; desc_type=3'd7; w_length=8'd64; mps_sel=2'd3; step;  // 64, mps 64
    check(send_len === 8'd64, "all 64 bytes are sent");
    check(!zlp_required,
          "no ZLP: the count reached wLength, even though it is one whole packet");
    check(n_packets === 8'd1, "exactly one full packet");

    // 8. ...and the same descriptor when the host asked for more.
    req_valid=1; desc_type=3'd7; w_length=8'd80; mps_sel=2'd3; step;
    check(send_len === 8'd64, "64 bytes are sent");
    check(zlp_required, "and NOW a ZLP is required");
    check(n_packets === 8'd2, "one full packet plus the ZLP");
    req_valid=0;

    // ===== C. randomised =====
    hard_reset;
    for (i=0;i<40000;i=i+1) begin
      req_valid = ({$random}%8)!=0;
      desc_type = {$random}%8;
      mps_sel   = {$random}%4;
      // bias toward the descriptor lengths and the boundaries around them
      case ({$random}%4)
        0: w_length = {$random}%80;
        1: w_length = 8'd0;
        2: w_length = (8'd8 << (mps_sel));
        default: begin
          case ({$random}%6)
            0: w_length = 8'd18; 1: w_length = 8'd32; 2: w_length = 8'd4;
            3: w_length = 8'd16; 4: w_length = 8'd10; default: w_length = 8'd64;
          endcase
        end
      endcase
      step;
    end

    for (i=0;i<8;i=i+1)
      if (i != 0 && i != 6)
        check(n_bytype[i] > 0, "every real descriptor type was requested");
    check(n_stall   > 1000, "unknown descriptors were requested often");
    check(n_trunc   > 1000, "truncation happened often");
    check(n_zlpreq  > 1000, "the ZLP rule fired often");
    check(n_exact   > 1000, "aligned sends happened often");
    check(n_partial > 1000, "unaligned sends happened often");

    $display("");
    $display("  REACH: exhaustive=%0d | found=%0d stalled=%0d truncated=%0d zlp-required=%0d",
             n_exh, n_found, n_stall, n_trunc, n_zlpreq);
    $display("  SHAPE: aligned-sends=%0d partial-sends=%0d | by type: dev=%0d cfg=%0d str0=%0d str1=%0d qual=%0d bos=%0d",
             n_exact, n_partial, n_bytype[1], n_bytype[2], n_bytype[3],
             n_bytype[4], n_bytype[5], n_bytype[7]);
    $display("  COUNTERS: requests=%0d stalls=%0d truncated=%0d zlp=%0d",
             n_requests, n_stalls, n_truncated, n_zlp);
    $display("  [Verilog] usb_descriptor_engine: %0d errors", errors);
    $display("  [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

9.2 SystemVerilog testbench

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

  logic clk=0, rst_n=0;
  logic req_valid=0;
  desc_type_e desc_type = DESC_NONE;
  logic [7:0] w_length=0;
  logic [1:0] mps_sel=3;
  logic found, stall_req, truncated, zlp_required;
  logic [7:0] desc_len, send_len, n_packets, mps;
  term_e terminator;
  logic [31:0] n_requests, n_stalls, n_truncated, n_zlp;

  // 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 = 21303;
  always #5 clk=~clk;

  usb_descriptor_engine dut (
    .clk, .rst_n, .req_valid, .desc_type, .w_length, .mps_sel, .found,
    .stall_req, .desc_len, .send_len, .truncated, .zlp_required,
    .n_packets, .mps, .terminator, .n_requests, .n_stalls, .n_truncated,
    .n_zlp);

  int errors=0, i, t, w, m;
  int n_exh=0;
  int n_found=0, n_stall=0, n_trunc=0, n_zlpreq=0, n_exact=0, n_partial=0;
  int n_bytype [8];
  int m_req, m_stall, m_trunc, m_zlp;

  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.
    desc_type_e dt_v; term_e tm_v;
    if (!cond) begin
      errors++;
      dt_v = desc_type; tm_v = terminator;
      if (errors <= 25)
        $display("  FAIL: %0s (type=%s wLen=%0d mps=%0d | found=%b dlen=%0d slen=%0d trunc=%b zlp=%b npkt=%0d term=%s, t=%0t)",
                 msg, dt_v.name(), w_length, mps, found, desc_len, send_len,
                 truncated, zlp_required, n_packets, tm_v.name(), $time);
    end
  endtask

  // ---- independent reference model. It computes the packet count by
  // ---- COUNTING packets in a loop where the design uses a shift, and takes
  // ---- the minimum with an if where the design uses a ternary.
  task automatic model(output bit e_found, output bit e_stall,
                       output logic [7:0] e_dlen, output logic [7:0] e_slen,
                       output bit e_trunc, output bit e_zlp,
                       output logic [7:0] e_npkt);
    int real_len, mps_i, sent, pkts;
    begin
      case (desc_type)
        DESC_DEVICE: real_len = 18;
        DESC_CONFIG: real_len = 32;
        DESC_STR0:   real_len = 4;
        DESC_STR1:   real_len = 16;
        DESC_QUAL:   real_len = 10;
        DESC_BOS:    real_len = 64;
        default:     real_len = -1;  // not a descriptor we have
      endcase

      mps_i  = 8 << mps_sel;
      e_found = req_valid && (real_len >= 0);
      e_stall = req_valid && (real_len < 0);
      e_dlen  = e_found ? real_len[7:0] : 8'd0;

      if (!e_found) sent = 0;
      else if (w_length < real_len) sent = w_length;
      else sent = real_len;
      e_slen  = sent[7:0];
      e_trunc = e_found && (sent < real_len);

      // The rule, restated: a ZLP is needed when we sent less than was asked
      // for and what we sent was a whole number of packets.
      e_zlp = e_found && (sent < w_length) && ((sent % mps_i) == 0);

      // Count the packets one at a time -- a different route from the
      // design's shift-and-add.
      pkts = 0;
      begin : countloop
        int left;
        left = sent;
        while (left >= mps_i) begin
          pkts = pkts + 1;
          left = left - mps_i;
        end
        if (left > 0) pkts = pkts + 1;
      end
      if (e_zlp) pkts = pkts + 1;
      e_npkt = pkts[7:0];
    end
  endtask

  task automatic check_comb;
    bit e_found, e_stall, e_trunc, e_zlp;
    logic [7:0] e_dlen, e_slen, e_npkt;
    term_e e_term;
    int mps_i;
    begin
      model(e_found, e_stall, e_dlen, e_slen, e_trunc, e_zlp, e_npkt);
      mps_i = 8 << mps_sel;

      check(found        === e_found, "found matches the model");
      check(stall_req    === e_stall, "stall_req matches the model");
      check(desc_len     === e_dlen,  "desc_len matches the model");
      check(send_len     === e_slen,  "send_len matches the model");
      check(truncated    === e_trunc, "truncated matches the model");
      check(zlp_required === e_zlp,   "zlp_required matches the model");
      check(n_packets    === e_npkt,  "n_packets matches the model");
      check(mps          === mps_i[7:0], "mps decodes from mps_sel");

      // The terminator, computed by the model as a flat if-chain.
      if      (!e_found)                 e_term = END_NONE;
      else if (e_zlp)                    e_term = END_BY_ZLP;
      else if ((e_slen % mps_i) != 0)    e_term = END_BY_SHORT;
      else if (e_slen == w_length)       e_term = END_BY_COUNT;
      else                               e_term = END_NONE;
      check(terminator === e_term, "terminator matches the model");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE property. Never send the host more than it asked for. This is
      //    a buffer overflow in the host driver, not a protocol nicety.
      check(send_len <= w_length,
            "more bytes were offered than the host asked for -- host buffer overflow");
      // 2. And never more than the descriptor actually has.
      check(send_len <= desc_len,
            "more bytes were offered than the descriptor contains");
      // 3. found and stall_req are mutually exclusive, and one of them holds
      //    whenever a request is present.
      check(!(found && stall_req), "found and stall_req both asserted");
      if (req_valid) check(found || stall_req,
                           "a request produced neither a descriptor nor a STALL");
      if (!req_valid) check(!found && !stall_req,
                            "an output asserted with no request");
      // 4. A transfer must terminate: either we sent exactly what was asked
      //    for, or the last packet is short, or a ZLP follows. If none of the
      //    three holds, the host waits for ever.
      if (found)
        check((send_len == w_length)
              || ((send_len % mps_i) != 0)
              || zlp_required,
              "nothing terminates this transfer -- the host will hang");
      // 4b. ...restated through the named terminator: a found descriptor
      //     always has one, and it agrees with the booleans.
      if (found)
        check(terminator !== END_NONE,
              "a descriptor was found with no way to end its data stage");
      check((terminator === END_BY_ZLP) === zlp_required,
            "terminator disagrees with zlp_required");
      // 5. A ZLP is never sent when the byte count already ended the
      //    transfer: that would be an extra packet the host did not expect.
      if (send_len == w_length)
        check(!zlp_required,
              "a ZLP was added after a transfer that already ended by count");
      // 6. Nor after a short packet, which already terminated it.
      if (found && ((send_len % mps_i) != 0))
        check(!zlp_required,
              "a ZLP was added after a short packet -- two terminators");
      // 7. An unknown descriptor offers nothing at all.
      if (stall_req)
        check((send_len == 8'd0) && (n_packets == 8'd0) && !zlp_required,
              "a STALLed request still offered data");
      // 8. The packet count must be able to carry the bytes.
      check(n_packets * mps_i >= send_len,
            "n_packets cannot carry send_len bytes");

      if (e_found) begin
        n_found = n_found + 1;
        n_bytype[int'(desc_type)] = n_bytype[int'(desc_type)] + 1;
        if (e_trunc) n_trunc = n_trunc + 1;
        if (e_zlp)   n_zlpreq = n_zlpreq + 1;
        if ((e_slen % mps_i) == 0) n_exact = n_exact + 1;
        else n_partial = n_partial + 1;
      end
      if (e_stall) n_stall = n_stall + 1;
    end
  endtask

  task automatic step;
    begin
      #1;
      check_comb;
      if (req_valid) begin
        m_req = m_req + 1;
        if (stall_req)    m_stall = m_stall + 1;
        if (truncated)    m_trunc = m_trunc + 1;
        if (zlp_required) m_zlp   = m_zlp + 1;
      end
      @(posedge clk); #1;
      check(n_requests  === 32'(m_req),   "n_requests matches the model");
      check(n_stalls    === 32'(m_stall), "n_stalls matches the model");
      check(n_truncated === 32'(m_trunc), "n_truncated matches the model");
      check(n_zlp       === 32'(m_zlp),   "n_zlp matches the model");
    end
  endtask

  task automatic hard_reset;
    begin
      rst_n=0; req_valid=0; desc_type=DESC_NONE; w_length=0; mps_sel=3;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      m_req=0; m_stall=0; m_trunc=0; m_zlp=0;
    end
  endtask

  initial begin
    void'($urandom(urandom_seed));
    foreach (n_bytype[i]) n_bytype[i]=0;
    hard_reset;
    check(!found && !stall_req, "no request, no answer");

    // ===== A. EXHAUSTIVE sweep of the whole decision =====
    // 8 descriptor types x 80 wLength values (0..79, past the longest
    // descriptor) x 4 max-packet-sizes = 2560 points. Every truncation
    // boundary and every packet-size alignment is visited by construction.
    req_valid = 1;
    for (t=0; t<8; t=t+1)
     for (w=0; w<80; w=w+1)
      for (m=0; m<4; m=m+1) begin
        desc_type=desc_type_e'(t[2:0]); w_length=w[7:0]; mps_sel=m[1:0];
        step;
        n_exh = n_exh + 1;
      end
    req_valid = 0;
    $display("  exhaustive descriptor sweep: %0d of %0d points verified",
             n_exh, 8*80*4);

    // ===== B. directed: the enumeration sequence a real host performs =====
    hard_reset;

    // 1. The host asks for the first 9 bytes of the configuration
    //    descriptor, because it does not yet know how long the whole thing
    //    is. wLength = 9, descriptor = 32.
    req_valid=1; desc_type=DESC_CONFIG; w_length=8'd9; mps_sel=2'd3; step;
    check(found, "the configuration descriptor exists");
    check(desc_len === 8'd32, "and it is 32 bytes long");
    check(send_len === 8'd9,
          "but only 9 bytes are sent -- the host asked for 9");
    check(truncated, "so the descriptor was truncated");
    check(!zlp_required,
          "and no ZLP is needed: 9 bytes is exactly what was asked for");
    check(n_packets === 8'd1, "one short packet carries it");

    // 2. Now the host knows the length and asks for all of it.
    req_valid=1; desc_type=DESC_CONFIG; w_length=8'd32; mps_sel=2'd3; step;
    check(send_len === 8'd32, "all 32 bytes are sent");
    check(!truncated, "nothing was truncated");
    check(!zlp_required,
          "and no ZLP: the byte count reached wLength exactly");

    // 3. THE case. The host asks for MORE than exists, and what exists is an
    //    exact multiple of the packet size.
    req_valid=1; desc_type=DESC_CONFIG; w_length=8'd64; mps_sel=2'd2; step;  // mps=32
    check(mps === 8'd32, "a 32-byte max packet size");
    check(send_len === 8'd32, "32 bytes are sent -- all the descriptor has");
    check(!truncated,
          "the DESCRIPTOR was not truncated: the host asked for more than exists");
    check(zlp_required,
          "so a ZLP IS required: 32 bytes is one whole packet and the host wants more");
    check(n_packets === 8'd2, "one full packet plus the terminating ZLP");

    // 4. The same request with a packet size that does NOT divide it.
    req_valid=1; desc_type=DESC_DEVICE; w_length=8'd64; mps_sel=2'd3; step;  // 18, mps 64
    check(send_len === 8'd18, "18 bytes are sent");
    check(!truncated,
          "again nothing was truncated -- 18 is the whole descriptor");
    check(!zlp_required,
          "but NO ZLP: 18 is a short packet and already terminates the transfer");
    check(n_packets === 8'd1, "one short packet");

    // 5. The boundary that catches people: nothing to send, but the host
    //    asked for something.
    req_valid=1; desc_type=DESC_STR0; w_length=8'd0; mps_sel=2'd3; step;
    check(send_len === 8'd0, "zero bytes sent");
    check(!zlp_required,
          "and NO ZLP: wLength was 0, so there is no data stage to terminate");
    check(n_packets === 8'd0, "no packets at all");

    // 6. An unknown descriptor is STALLed, not answered with an empty one.
    req_valid=1; desc_type=DESC_RSVD; w_length=8'd64; mps_sel=2'd3; step;
    check(!found, "type 6 is not a descriptor this device has");
    check(stall_req, "so the request is STALLed");
    check(send_len === 8'd0, "and nothing is offered");
    check(n_packets === 8'd0, "and no packets are counted");

    // 7. A descriptor exactly one max-packet-size long, asked for in full.
    req_valid=1; desc_type=DESC_BOS; w_length=8'd64; mps_sel=2'd3; step;  // 64, mps 64
    check(send_len === 8'd64, "all 64 bytes are sent");
    check(!zlp_required,
          "no ZLP: the count reached wLength, even though it is one whole packet");
    check(n_packets === 8'd1, "exactly one full packet");

    // 8. ...and the same descriptor when the host asked for more.
    req_valid=1; desc_type=DESC_BOS; w_length=8'd80; mps_sel=2'd3; step;
    check(send_len === 8'd64, "64 bytes are sent");
    check(zlp_required, "and NOW a ZLP is required");
    check(n_packets === 8'd2, "one full packet plus the ZLP");
    req_valid=0;

    // ===== C. randomised =====
    hard_reset;
    for (i=0;i<40000;i=i+1) begin
      req_valid = ($urandom%8)!=0;
      desc_type = desc_type_e'($urandom%8);
      mps_sel   = $urandom%4;
      // bias toward the descriptor lengths and the boundaries around them
      case ($urandom%4)
        0: w_length = $urandom%80;
        1: w_length = 8'd0;
        2: w_length = (8'd8 << (mps_sel));
        default: begin
          case ($urandom%6)
            0: w_length = 8'd18; 1: w_length = 8'd32; 2: w_length = 8'd4;
            3: w_length = 8'd16; 4: w_length = 8'd10; default: w_length = 8'd64;
          endcase
        end
      endcase
      step;
    end

    foreach (n_bytype[i])
      if (i != 0 && i != 6)
        check(n_bytype[i] > 0, "every real descriptor type was requested");
    check(n_stall   > 1000, "unknown descriptors were requested often");
    check(n_trunc   > 1000, "truncation happened often");
    check(n_zlpreq  > 1000, "the ZLP rule fired often");
    check(n_exact   > 1000, "aligned sends happened often");
    check(n_partial > 1000, "unaligned sends happened often");

    $display("");
    $display("  REACH: exhaustive=%0d | found=%0d stalled=%0d truncated=%0d zlp-required=%0d",
             n_exh, n_found, n_stall, n_trunc, n_zlpreq);
    $display("  SHAPE: aligned-sends=%0d partial-sends=%0d | by type: dev=%0d cfg=%0d str0=%0d str1=%0d qual=%0d bos=%0d",
             n_exact, n_partial, n_bytype[1], n_bytype[2], n_bytype[3],
             n_bytype[4], n_bytype[5], n_bytype[7]);
    $display("  COUNTERS: requests=%0d stalls=%0d truncated=%0d zlp=%0d",
             n_requests, n_stalls, n_truncated, n_zlp);
    $display("  [SystemVerilog] usb_descriptor_engine: %0d errors", errors);
    $display("  [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

9.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_desc_pkg.all;

entity tb_de_vhdl is
end entity;

architecture sim of tb_de_vhdl is
  signal clk   : std_logic := '0';
  signal rst_n : std_logic := '0';
  signal req_valid : std_logic := '0';
  signal desc_type : std_logic_vector(2 downto 0) := "000";
  signal w_length  : std_logic_vector(7 downto 0) := (others => '0');
  signal mps_sel   : std_logic_vector(1 downto 0) := "11";

  signal found, stall_req, truncated, zlp_required : std_logic;
  signal desc_len, send_len, n_packets, mps : std_logic_vector(7 downto 0);
  signal terminator : std_logic_vector(1 downto 0);
  signal n_requests, n_stalls, n_truncated, n_zlp
       : std_logic_vector(31 downto 0);

  signal running : boolean := true;

  type cnt8_t is array (0 to 7) of integer;
begin
  clk <= not clk after 5 ns when running else '0';

  dut : entity work.usb_descriptor_engine
    port map (clk => clk, rst_n => rst_n, req_valid => req_valid,
              desc_type => desc_type, w_length => w_length,
              mps_sel => mps_sel, found => found, stall_req => stall_req,
              desc_len => desc_len, send_len => send_len,
              truncated => truncated, zlp_required => zlp_required,
              n_packets => n_packets, mps => mps, terminator => terminator,
              n_requests => n_requests, n_stalls => n_stalls,
              n_truncated => n_truncated, n_zlp => n_zlp);

  stim : process
    variable seed1 : positive := 6619;
    variable seed2 : positive := 1237;
    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;
    variable n_exh   : integer := 0;
    variable n_found, n_stall, n_trunc, n_zlpreq : integer := 0;
    variable n_exact, n_partial : integer := 0;
    variable n_bytype : cnt8_t := (others => 0);
    variable m_req, m_stall, m_trunc, m_zlp : integer := 0;

    procedure check(cond : boolean; msg : string) is
    begin
      if not cond then
        errors := errors + 1;
        if errors <= 25 then
          report "  FAIL: " & msg
               & " (type=" & integer'image(to_integer(unsigned(desc_type)))
               & " wLen=" & integer'image(to_integer(unsigned(w_length)))
               & " mps=" & integer'image(to_integer(unsigned(mps)))
               & " | found=" & std_logic'image(found)(2)
               & " dlen=" & integer'image(to_integer(unsigned(desc_len)))
               & " slen=" & integer'image(to_integer(unsigned(send_len)))
               & " trunc=" & std_logic'image(truncated)(2)
               & " zlp=" & std_logic'image(zlp_required)(2)
               & " npkt=" & integer'image(to_integer(unsigned(n_packets)))
               & " term=" & integer'image(to_integer(unsigned(terminator)))
               & ")" 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 dt      : desc_type_t;
      variable real_len, mps_iv, sent, pkts, wl, left : integer;
      variable e_found, e_stall, e_trunc, e_zlp : boolean;
      variable e_term  : term_t;
    begin
      dt := desc_decode(desc_type);
      wl := to_integer(unsigned(w_length));
      case dt is
        when DESC_DEVICE => real_len := 18;
        when DESC_CONFIG => real_len := 32;
        when DESC_STR0   => real_len := 4;
        when DESC_STR1   => real_len := 16;
        when DESC_QUAL   => real_len := 10;
        when DESC_BOS    => real_len := 64;
        when others      => real_len := -1;   -- not a descriptor we have
      end case;

      mps_iv  := 8 * (2 ** to_integer(unsigned(mps_sel)));
      e_found := req_valid = '1' and real_len >= 0;
      e_stall := req_valid = '1' and real_len < 0;

      if not e_found then      sent := 0;
      elsif wl < real_len then sent := wl;
      else                     sent := real_len;
      end if;
      e_trunc := e_found and sent < real_len;

      -- The rule, restated: a ZLP is needed when we sent less than was asked
      -- for and what we sent was a whole number of packets.
      e_zlp := e_found and sent < wl and (sent mod mps_iv) = 0;

      -- Count the packets one at a time -- a different route from the
      -- design's divide-and-add.
      pkts := 0;
      left := sent;
      while left >= mps_iv loop
        pkts := pkts + 1;
        left := left - mps_iv;
      end loop;
      if left > 0 then pkts := pkts + 1; end if;
      if e_zlp then pkts := pkts + 1; end if;

      if not e_found then              e_term := END_NONE;
      elsif e_zlp then                 e_term := END_BY_ZLP;
      elsif (sent mod mps_iv) /= 0 then e_term := END_BY_SHORT;
      elsif sent = wl then             e_term := END_BY_COUNT;
      else                             e_term := END_NONE;
      end if;

      check((found = '1') = e_found,          "found matches the model");
      check((stall_req = '1') = e_stall,      "stall_req matches the model");
      if e_found then
        check(to_integer(unsigned(desc_len)) = real_len,
              "desc_len matches the model");
      else
        check(to_integer(unsigned(desc_len)) = 0,
              "desc_len is zero when there is no descriptor");
      end if;
      check(to_integer(unsigned(send_len)) = sent, "send_len matches the model");
      check((truncated = '1') = e_trunc,      "truncated matches the model");
      check((zlp_required = '1') = e_zlp,     "zlp_required matches the model");
      check(to_integer(unsigned(n_packets)) = pkts,
            "n_packets matches the model");
      check(to_integer(unsigned(mps)) = mps_iv, "mps decodes from mps_sel");
      check(terminator = term_code(e_term),   "terminator matches the model");

      -- ---- SAFETY PROPERTIES, independent of the model ----
      -- 1. THE property. Never send the host more than it asked for.
      check(to_integer(unsigned(send_len)) <= wl,
            "more bytes were offered than the host asked for -- host buffer overflow");
      -- 2. And never more than the descriptor actually has.
      check(to_integer(unsigned(send_len))
            <= to_integer(unsigned(desc_len)),
            "more bytes were offered than the descriptor contains");
      -- 3. found and stall_req are mutually exclusive, and one holds whenever
      --    a request is present.
      check(not (found = '1' and stall_req = '1'),
            "found and stall_req both asserted");
      if req_valid = '1' then
        check(found = '1' or stall_req = '1',
              "a request produced neither a descriptor nor a STALL");
      else
        check(found = '0' and stall_req = '0',
              "an output asserted with no request");
      end if;
      -- 4. A transfer must terminate: exactly what was asked for, a short
      --    last packet, or a ZLP. Otherwise the host waits for ever.
      if found = '1' then
        check(to_integer(unsigned(send_len)) = wl
              or (to_integer(unsigned(send_len)) mod mps_iv) /= 0
              or zlp_required = '1',
              "nothing terminates this transfer -- the host will hang");
        -- 4b. ...restated through the named terminator.
        check(terminator /= term_code(END_NONE),
              "a descriptor was found with no way to end its data stage");
      end if;
      check((terminator = term_code(END_BY_ZLP)) = (zlp_required = '1'),
            "terminator disagrees with zlp_required");
      -- 5. A ZLP is never sent when the byte count already ended the transfer.
      if to_integer(unsigned(send_len)) = wl then
        check(zlp_required = '0',
              "a ZLP was added after a transfer that already ended by count");
      end if;
      -- 6. Nor after a short packet, which already terminated it.
      if found = '1' and (to_integer(unsigned(send_len)) mod mps_iv) /= 0 then
        check(zlp_required = '0',
              "a ZLP was added after a short packet -- two terminators");
      end if;
      -- 7. An unknown descriptor offers nothing at all.
      if stall_req = '1' then
        check(to_integer(unsigned(send_len)) = 0
              and to_integer(unsigned(n_packets)) = 0
              and zlp_required = '0',
              "a STALLed request still offered data");
      end if;
      -- 8. The packet count must be able to carry the bytes.
      check(to_integer(unsigned(n_packets)) * mps_iv
            >= to_integer(unsigned(send_len)),
            "n_packets cannot carry send_len bytes");

      if e_found then
        n_found := n_found + 1;
        n_bytype(desc_type_t'pos(dt)) := n_bytype(desc_type_t'pos(dt)) + 1;
        if e_trunc then n_trunc := n_trunc + 1; end if;
        if e_zlp then n_zlpreq := n_zlpreq + 1; end if;
        if (sent mod mps_iv) = 0 then n_exact := n_exact + 1;
        else n_partial := n_partial + 1; end if;
      end if;
      if e_stall then n_stall := n_stall + 1; end if;
    end procedure;

    procedure step is
    begin
      wait for 1 ns;
      check_comb;
      if req_valid = '1' then
        m_req := m_req + 1;
        if stall_req = '1'    then m_stall := m_stall + 1; end if;
        if truncated = '1'    then m_trunc := m_trunc + 1; end if;
        if zlp_required = '1' then m_zlp   := m_zlp + 1;   end if;
      end if;
      wait until rising_edge(clk);
      wait for 1 ns;
      check(n_requests = std_logic_vector(to_unsigned(m_req, 32)),
            "n_requests matches the model");
      check(n_stalls = std_logic_vector(to_unsigned(m_stall, 32)),
            "n_stalls matches the model");
      check(n_truncated = std_logic_vector(to_unsigned(m_trunc, 32)),
            "n_truncated matches the model");
      check(n_zlp = std_logic_vector(to_unsigned(m_zlp, 32)),
            "n_zlp matches the model");
    end procedure;

    procedure setreq(t : integer; w : integer; m : integer) is
    begin
      desc_type <= std_logic_vector(to_unsigned(t, 3));
      w_length  <= std_logic_vector(to_unsigned(w, 8));
      mps_sel   <= std_logic_vector(to_unsigned(m, 2));
    end procedure;

    procedure hard_reset is
    begin
      rst_n <= '0'; req_valid <= '0'; setreq(0, 0, 3);
      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;
      m_req := 0; m_stall := 0; m_trunc := 0; m_zlp := 0;
    end procedure;

    variable iv, msel : integer;
  begin
    hard_reset;
    check(found = '0' and stall_req = '0', "no request, no answer");

    -- ===== A. EXHAUSTIVE sweep of the whole decision =====
    -- 8 descriptor types x 80 wLength values (0..79, past the longest
    -- descriptor) x 4 max-packet-sizes = 2560 points. Every truncation
    -- boundary and every packet-size alignment is visited by construction.
    req_valid <= '1';
    for t in 0 to 7 loop
      for w in 0 to 79 loop
        for m in 0 to 3 loop
          setreq(t, w, m);
          step;
          n_exh := n_exh + 1;
        end loop;
      end loop;
    end loop;
    req_valid <= '0';
    report "  exhaustive descriptor sweep: " & integer'image(n_exh)
         & " of 2560 points verified" severity note;

    -- ===== B. directed: the enumeration sequence a real host performs =====
    hard_reset;

    -- 1. The host asks for the first 9 bytes of the configuration
    --    descriptor, because it does not yet know how long the whole thing
    --    is. wLength = 9, descriptor = 32.
    req_valid <= '1'; setreq(2, 9, 3); step;
    check(found = '1', "the configuration descriptor exists");
    check(to_integer(unsigned(desc_len)) = 32, "and it is 32 bytes long");
    check(to_integer(unsigned(send_len)) = 9,
          "but only 9 bytes are sent -- the host asked for 9");
    check(truncated = '1', "so the descriptor was truncated");
    check(zlp_required = '0',
          "and no ZLP is needed: 9 bytes is exactly what was asked for");
    check(to_integer(unsigned(n_packets)) = 1,
          "one short packet carries it");

    -- 2. Now the host knows the length and asks for all of it.
    setreq(2, 32, 3); step;
    check(to_integer(unsigned(send_len)) = 32, "all 32 bytes are sent");
    check(truncated = '0', "nothing was truncated");
    check(zlp_required = '0',
          "and no ZLP: the byte count reached wLength exactly");

    -- 3. THE case. The host asks for MORE than exists, and what exists is an
    --    exact multiple of the packet size.
    setreq(2, 64, 2); step;                    -- mps = 32
    check(to_integer(unsigned(mps)) = 32, "a 32-byte max packet size");
    check(to_integer(unsigned(send_len)) = 32,
          "32 bytes are sent -- all the descriptor has");
    check(truncated = '0',
          "the DESCRIPTOR was not truncated: the host asked for more than exists");
    check(zlp_required = '1',
          "so a ZLP IS required: 32 bytes is one whole packet and the host wants more");
    check(to_integer(unsigned(n_packets)) = 2,
          "one full packet plus the terminating ZLP");

    -- 4. The same request with a packet size that does NOT divide it.
    setreq(1, 64, 3); step;                    -- 18 bytes, mps 64
    check(to_integer(unsigned(send_len)) = 18, "18 bytes are sent");
    check(truncated = '0',
          "again nothing was truncated -- 18 is the whole descriptor");
    check(zlp_required = '0',
          "but NO ZLP: 18 is a short packet and already terminates the transfer");
    check(to_integer(unsigned(n_packets)) = 1, "one short packet");

    -- 5. The boundary that catches people: nothing to send, and the host
    --    asked for nothing either.
    setreq(3, 0, 3); step;
    check(to_integer(unsigned(send_len)) = 0, "zero bytes sent");
    check(zlp_required = '0',
          "and NO ZLP: wLength was 0, so there is no data stage to terminate");
    check(to_integer(unsigned(n_packets)) = 0, "no packets at all");

    -- 6. An unknown descriptor is STALLed, not answered with an empty one.
    setreq(6, 64, 3); step;
    check(found = '0', "type 6 is not a descriptor this device has");
    check(stall_req = '1', "so the request is STALLed");
    check(to_integer(unsigned(send_len)) = 0, "and nothing is offered");
    check(to_integer(unsigned(n_packets)) = 0, "and no packets are counted");

    -- 7. A descriptor exactly one max-packet-size long, asked for in full.
    setreq(7, 64, 3); step;                    -- 64 bytes, mps 64
    check(to_integer(unsigned(send_len)) = 64, "all 64 bytes are sent");
    check(zlp_required = '0',
          "no ZLP: the count reached wLength, even though it is one whole packet");
    check(to_integer(unsigned(n_packets)) = 1, "exactly one full packet");

    -- 8. ...and the same descriptor when the host asked for more.
    setreq(7, 80, 3); step;
    check(to_integer(unsigned(send_len)) = 64, "64 bytes are sent");
    check(zlp_required = '1', "and NOW a ZLP is required");
    check(to_integer(unsigned(n_packets)) = 2, "one full packet plus the ZLP");
    req_valid <= '0';

    -- ===== C. randomised =====
    -- 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 39999 loop
      rnd(iv, 8); if iv /= 0 then req_valid <= '1'; else req_valid <= '0'; end if;
      rnd(iv, 8); desc_type <= std_logic_vector(to_unsigned(iv, 3));
      -- The packet-size selector is kept in a VARIABLE as well as driven onto
      -- the signal, because the wLength bias below needs the size chosen THIS
      -- iteration. Reading mps_sel back would read the previous value: a
      -- signal assignment does not take effect until the next wait.
      rnd(msel, 4); mps_sel <= std_logic_vector(to_unsigned(msel, 2));
      -- bias toward the descriptor lengths and the boundaries around them
      rnd(iv, 4);
      case iv is
        when 0 => rnd(iv, 80);
                  w_length <= std_logic_vector(to_unsigned(iv, 8));
        when 1 => w_length <= (others => '0');
        when 2 => w_length <= std_logic_vector(
                                to_unsigned(8 * (2 ** msel), 8));
        when others =>
          rnd(iv, 6);
          case iv is
            when 0      => w_length <= std_logic_vector(to_unsigned(18, 8));
            when 1      => w_length <= std_logic_vector(to_unsigned(32, 8));
            when 2      => w_length <= std_logic_vector(to_unsigned(4, 8));
            when 3      => w_length <= std_logic_vector(to_unsigned(16, 8));
            when 4      => w_length <= std_logic_vector(to_unsigned(10, 8));
            when others => w_length <= std_logic_vector(to_unsigned(64, 8));
          end case;
      end case;
      step;
    end loop;

    for i in 0 to 7 loop
      if i /= 0 and i /= 6 then
        check(n_bytype(i) > 0, "every real descriptor type was requested");
      end if;
    end loop;
    check(n_stall   > 1000, "unknown descriptors were requested often");
    check(n_trunc   > 1000, "truncation happened often");
    check(n_zlpreq  > 1000, "the ZLP rule fired often");
    check(n_exact   > 1000, "aligned sends happened often");
    check(n_partial > 1000, "unaligned sends happened often");

    report "  REACH: exhaustive=" & integer'image(n_exh)
         & " | found=" & integer'image(n_found)
         & " stalled=" & integer'image(n_stall)
         & " truncated=" & integer'image(n_trunc)
         & " zlp-required=" & integer'image(n_zlpreq) severity note;
    report "  SHAPE: aligned-sends=" & integer'image(n_exact)
         & " partial-sends=" & integer'image(n_partial)
         & " | by type: dev=" & integer'image(n_bytype(1))
         & " cfg=" & integer'image(n_bytype(2))
         & " str0=" & integer'image(n_bytype(3))
         & " str1=" & integer'image(n_bytype(4))
         & " qual=" & integer'image(n_bytype(5))
         & " bos=" & integer'image(n_bytype(7)) severity note;
    report "  COUNTERS: requests="
         & integer'image(to_integer(unsigned(n_requests)))
         & " stalls=" & integer'image(to_integer(unsigned(n_stalls)))
         & " truncated=" & integer'image(to_integer(unsigned(n_truncated)))
         & " zlp=" & integer'image(to_integer(unsigned(n_zlp)))
         severity note;
    report "  [VHDL] usb_descriptor_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;

10. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
Exhaustive points2560 / 25602560 / 25602560 / 2560
descriptors found281152826928149
requests STALLed943093689412
descriptors truncated142151425114445
ZLP required181218541861
aligned sends126411283912797
partial sends154741543015352
DEVICE requests483047744678
CONFIGURATION requests473347654707
STRING 0 / STRING 14608 / 46314789 / 46884663 / 4671
DEVICE_QUALIFIER / BOS4730 / 45834631 / 46224595 / 4835
ResultPASSPASSPASS

Every real descriptor type was requested thousands of times, and the testbenches assert that. The row worth reading is ZLP required ≈ 1850 — the condition is a conjunction of two narrow terms, so it fires on about 6.5% of found requests, and the biased randomiser is what lifts it from "occasionally" to "1850 times".

11. Mutation Testing

#MutationVerilogSysVerVHDL
G1wLength ignored — the full descriptor is always sent812298148782768
G2the ZLP rule drops send_len < wLength433204394443748
G3the ZLP rule drops the alignment term403144049839358
G4an unknown descriptor is answered, not STALLed425854248942619
G5alignment tested against a fixed 64, not mps120941241712232
G6the terminating ZLP is not counted in n_packets181418561863
G7alignment tested on desc_len, not send_len187101881219219
—unmutated baseline000

All seven die in all three languages, all counts distinct, all columns within 3%.

G1 is the host-buffer-overflow bug, and at ~81 000 it is the largest because send_len feeds truncated, zlp_required, n_packets and terminator — one wrong line corrupting four outputs plus two counters.

G2 and G3 are the two halves of the ZLP rule, and they score almost identically (43 000 and 40 000) while being opposite errors. That symmetry is informative: it says the suite is equally sensitive to a ZLP that should not be there and one that should. A suite that caught only one of them would have a blind spot shaped exactly like the bug that hangs transfers.

G6 is the smallest at ~1850, and it is the most interesting. It affects only n_packets, and only on the ~1850 requests where a ZLP is required — so its count is almost exactly the number of times the ZLP rule fires. A mutation whose failure count equals a reach counter is a mutation on a path that nothing else observes, which is precisely what a single-output error looks like when the suite is tight.

G5 and G7 are the two "right idea, wrong operand" mutations, and the gap between them (12 000 vs 18 700) says something useful: testing alignment against a fixed packet size (G5) is wrong only when mps ≠ 64, which is three quarters of the sweep; testing it on the descriptor length instead of the sent length (G7) is wrong whenever the two differ, which is more often.

12. Debugging Walkthrough: The Device That Enumerates on Linux and Not on Windows

The report. A device enumerates perfectly on one operating system and fails on another, at the same point every time: the host reads the device descriptor, then hangs.

Step 1 — what is different about the two hosts? Capture both. The working host asks GET_DESCRIPTOR(DEVICE, wLength = 18). The failing host asks GET_DESCRIPTOR(DEVICE, wLength = 64) — a common pattern, because 64 is the control endpoint's maximum packet size and the host will take whatever arrives.

Step 2 — what does the device send? 18 bytes in both cases. Correct: the descriptor is 18 bytes long.

Step 3 — so why does one hang? On the working host, 18 == wLength, so the transfer ended by count. On the failing host, 18 < 64 — but 18 is also not a multiple of 64, so the packet is short and the transfer ends anyway. Both should work.

Step 4 — look again at the endpoint. bMaxPacketSize0 is 8, not 64. This is a low-speed device. And 18 bytes on an 8-byte endpoint is two full packets and a 2-byte remainder — still short, still fine.

Step 5 — the actual failing request. Further up the capture: GET_DESCRIPTOR(CONFIGURATION, wLength = 64). The configuration descriptor is 32 bytes. On an 8-byte endpoint that is four full packets and nothing left over, and 32 < 64. Nothing terminates the transfer. The host waits.

Step 6 — why the other host was fine. It asked for wLength = 9 first, then wLength = 32 — exact, so the count terminated it. The failing host asked for 64 in one shot. Both hosts are correct; only one exercises the ZLP path.

Step 7 — the fix and the diagnostic. zlp_required was implemented as send_len < desc_len — the truncation test rather than the under-request test. The two coincide on most requests and differ on exactly this one. The terminator output would have read END_NONE on that transaction, and END_NONE is not a value a correct design ever produces.

13. A Test That Was Wrong About a Correct Design

While this chapter was being written, two directed checks failed against an implementation that was right. They are worth showing, because the mistake is the one §1's callout warns about.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // The host asks for MORE than exists: 64 bytes of a 32-byte descriptor.
    req_valid=1; desc_type=3'd2; w_length=8'd64; mps_sel=2'd2; step;
    check(send_len === 8'd32, "32 bytes are sent -- all the descriptor has");
    check(truncated, "which is less than the 64 asked for");   // <-- WRONG

truncated is defined as send_len < desc_len — the descriptor was cut short. Here send_len is 32 and desc_len is 32: nothing was truncated. The host asked for more than exists, which is a different fact, and the one that drives the ZLP rule.

The corrected check:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    check(!truncated,
          "the DESCRIPTOR was not truncated: the host asked for more than exists");

This is the sixth time in this curriculum that a directed test has encoded a wrong expectation against a correct design, and the pattern is always the same: a name that reads like plain English gets used with its plain-English meaning rather than its defined one. "Truncated" sounds like it should cover both cases. It does not, and the design is right to distinguish them — the whole ZLP rule depends on the distinction.

The defence is the one this series keeps arriving at: when a directed check fails, work out which of the two is wrong before changing either. A test that is "fixed" by loosening it has removed a check; a design that is "fixed" to satisfy a wrong test has acquired a bug.

14. UVM: Driving the Host's Two-Phase Descriptor Read

14.1 The transaction

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

  rand bit         req_valid;
  rand desc_type_e desc_type;
  rand bit [7:0]   w_length;
  rand bit [1:0]   mps_sel;

  // A real host mostly asks for descriptors that exist. The unknown-type
  // path is exercised by its own sequence rather than by flooding here.
  constraint c_mostly_real {
    desc_type dist { DESC_NONE := 5,  DESC_RSVD := 5,
                     DESC_DEVICE := 18, DESC_CONFIG := 18,
                     DESC_STR0 := 18, DESC_STR1 := 18,
                     DESC_QUAL := 9,  DESC_BOS := 9 };
  }

  // THE bias. The interesting wLength values are the descriptor lengths
  // themselves and the packet-size boundaries -- a uniform draw over 0..79
  // lands on one about 15% of the time and on the ZLP condition far less.
  constraint c_boundary_heavy {
    w_length dist { 0 := 10,                       // no data stage at all
                    [1:7] := 10,
                    18 := 10, 32 := 10, 4 := 10,   // exact descriptor lengths
                    16 := 10, 10 := 10, 64 := 10,
                    [65:79] := 15,                 // more than anything exists
                    [8:63] := 15 };
  }

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

  function string convert2string();
    return $sformatf("%s wLength=%0d mps=%0d", desc_type.name(), w_length,
                     8 << mps_sel);
  endfunction
endclass

14.2 Sequences

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// THE sequence for this chapter. It reproduces the two-phase read a real
// host performs during enumeration: a short probe to learn the length, then
// a full read. The probe is the request that overflows a host buffer if
// wLength is ignored, and it is not a request random stimulus emphasises.
class two_phase_config_read_seq extends uvm_sequence #(usb_desc_item);
  `uvm_object_utils(two_phase_config_read_seq)
  function new(string name = "two_phase_config_read_seq"); super.new(name); endfunction

  task body();
    repeat (300) begin
      usb_desc_item it;

      // Phase 1: the 9-byte header probe.
      it = usb_desc_item::type_id::create("it");
      start_item(it);
      if (!it.randomize() with { req_valid == 1; desc_type == DESC_CONFIG;
                                 w_length  == 9; })
        `uvm_error("RAND", "probe randomize failed")
      finish_item(it);

      // Phase 2: the full read, now that the host knows the length.
      it = usb_desc_item::type_id::create("it");
      start_item(it);
      if (!it.randomize() with { req_valid == 1; desc_type == DESC_CONFIG;
                                 w_length  == 32; })
        `uvm_error("RAND", "full-read randomize failed")
      finish_item(it);
    end
  endtask
endclass

// The ZLP population: requests where the host asks for MORE than exists and
// what exists is a whole number of packets. Constructed rather than sampled,
// because the conjunction is narrow enough that random traffic under-visits
// it by an order of magnitude.
class zlp_required_seq extends uvm_sequence #(usb_desc_item);
  `uvm_object_utils(zlp_required_seq)
  function new(string name = "zlp_required_seq"); super.new(name); endfunction

  task body();
    // (descriptor length, mps_sel) pairs where the length divides the packet
    // size exactly: CONFIG/32 on mps 32, STR1/16 on mps 16 and 8, BOS/64 on
    // every size.
    desc_type_e types[] = '{DESC_CONFIG, DESC_STR1, DESC_BOS, DESC_BOS};
    bit [1:0]   sels[]  = '{2'd2,        2'd1,      2'd3,     2'd2};

    foreach (types[i]) begin
      repeat (150) begin
        usb_desc_item it = usb_desc_item::type_id::create("it");
        start_item(it);
        it.c_boundary_heavy.constraint_mode(0);
        // wLength strictly greater than the descriptor: this is what makes a
        // ZLP necessary rather than merely possible.
        if (!it.randomize() with { req_valid == 1;
                                   desc_type == types[i];
                                   mps_sel   == sels[i];
                                   w_length  > 64; })
          `uvm_error("RAND", "zlp randomize failed")
        finish_item(it);
      end
    end
  endtask
endclass

// Feature discovery: a host probing for descriptors the device may not have.
// The property under test is that each one is STALLed rather than answered
// with an empty descriptor.
class feature_probe_seq extends uvm_sequence #(usb_desc_item);
  `uvm_object_utils(feature_probe_seq)
  function new(string name = "feature_probe_seq"); super.new(name); endfunction

  task body();
    repeat (400) begin
      usb_desc_item it = usb_desc_item::type_id::create("it");
      start_item(it);
      it.c_mostly_real.constraint_mode(0);
      if (!it.randomize() with { req_valid == 1;
                                 desc_type inside {DESC_NONE, DESC_RSVD}; })
        `uvm_error("RAND", "probe randomize failed")
      finish_item(it);
    end
  endtask
endclass

14.3 The scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class usb_desc_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(usb_desc_scoreboard)

  uvm_analysis_imp #(usb_desc_mon_item, usb_desc_scoreboard) ap;

  int unsigned n_zlp, n_stall, n_truncated, n_overrun_attempts;

  // The scoreboard's own descriptor table. Sharing a table with the DUT
  // would make a wrong length agree with itself.
  function automatic int len_of(desc_type_e t);
    case (t)
      DESC_DEVICE: return 18;
      DESC_CONFIG: return 32;
      DESC_STR0:   return 4;
      DESC_STR1:   return 16;
      DESC_QUAL:   return 10;
      DESC_BOS:    return 64;
      default:     return -1;     // not a descriptor this device has
    endcase
  endfunction

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

  function void write(usb_desc_mon_item t);
    int dlen  = len_of(t.desc_type);
    int mps_i = 8 << t.mps_sel;
    int sent;

    if (!t.req_valid) return;

    // ---- An unknown descriptor is STALLed, never answered ----
    if (dlen < 0) begin
      if (!t.stall_req)
        `uvm_error("DISCOVERY",
          "an unsupported descriptor was not STALLed -- the host cannot tell it is absent")
      if (t.found || (t.send_len != 0))
        `uvm_error("DISCOVERY",
          "an unsupported descriptor was answered with data")
      n_stall++;
      return;
    end

    sent = (t.w_length < dlen) ? t.w_length : dlen;

    // ---- THE property. Never more than the host's buffer holds. ----
    if (t.send_len > t.w_length)
      `uvm_error("OVERFLOW",
        $sformatf("offered %0d bytes into a %0d-byte host buffer",
                  t.send_len, t.w_length))
    if (t.send_len != sent)
      `uvm_error("LENGTH",
        $sformatf("send_len=%0d, expected min(%0d, %0d)=%0d",
                  t.send_len, t.w_length, dlen, sent))
    if (t.w_length < dlen) n_overrun_attempts++;

    // ---- The transfer must end. This is the hang, stated positively. ----
    begin
      bit ends_by_count = (sent == t.w_length);
      bit ends_by_short = ((sent % mps_i) != 0);
      bit ends_by_zlp   = t.zlp_required;
      if (!(ends_by_count || ends_by_short || ends_by_zlp))
        `uvm_error("HANG",
          $sformatf("nothing terminates this transfer: sent=%0d wLength=%0d mps=%0d",
                    sent, t.w_length, mps_i))
      // ...and exactly the right one of the three.
      if (t.zlp_required != ((sent < t.w_length) && ((sent % mps_i) == 0)))
        `uvm_error("ZLP",
          $sformatf("zlp_required=%0b but sent=%0d wLength=%0d mps=%0d",
                    t.zlp_required, sent, t.w_length, mps_i))
      // A ZLP after a transfer that already ended is an EXTRA packet, and the
      // host sees it at the start of the next transfer.
      if (t.zlp_required && ends_by_count)
        `uvm_error("EXTRA", "a ZLP was appended to a transfer that ended by count")
      if (t.zlp_required && ends_by_short)
        `uvm_error("EXTRA", "a ZLP was appended after a short packet")
      if (t.zlp_required) n_zlp++;
    end

    // ---- terminator must never read END_NONE on a descriptor we have ----
    if (t.terminator == END_NONE)
      `uvm_error("HANG",
        "terminator reads END_NONE for a descriptor that was found -- this transfer cannot end")

    if (t.truncated) n_truncated++;
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf(
      "zlps=%0d stalls=%0d truncations=%0d over-requests=%0d",
      n_zlp, n_stall, n_truncated, n_overrun_attempts), UVM_LOW)

    if (n_zlp == 0) `uvm_error("COVERAGE",
      "the ZLP rule never fired -- the condition this block exists for is untested")
    if (n_stall == 0) `uvm_error("COVERAGE",
      "no unsupported descriptor was ever requested")
    if (n_truncated == 0) `uvm_error("COVERAGE",
      "no descriptor was ever truncated -- the host-buffer property is untested")
  endfunction
endclass

14.4 Functional coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup desc_engine_cg with function sample(
    desc_type_e dt, bit [7:0] wlen, bit [1:0] msel, bit [7:0] slen,
    bit zlp, term_e term);

  cp_type : coverpoint dt {
    bins real_descs[] = {DESC_DEVICE, DESC_CONFIG, DESC_STR0,
                         DESC_STR1, DESC_QUAL, DESC_BOS};
    bins unsupported  = {DESC_NONE, DESC_RSVD};
  }

  cp_mps : coverpoint msel { bins sizes[] = {0, 1, 2, 3}; }

  // THE terminator coverpoint. END_NONE gets a bin on purpose: it should be
  // impossible for a found descriptor, and a bin that CANNOT be hit is a
  // statement, not an oversight. An illegal_bins would abort the run; a
  // normal bin lets the coverage report show it empty, which is the evidence.
  cp_term : coverpoint term {
    bins by_count = {END_BY_COUNT};
    bins by_short = {END_BY_SHORT};
    bins by_zlp   = {END_BY_ZLP};
    bins none     = {END_NONE};
  }

  // The request expressed RELATIVE to what exists -- an absolute coverpoint
  // on wLength closes while never once asking for exactly the descriptor
  // length of the descriptor actually requested.
  cp_ask : coverpoint (wlen == 0 ? 0 : 1) {
    bins zero = {0};
    bins some = {1};
  }

  // Was what we sent a whole number of packets? This, crossed with whether
  // the host asked for more, IS the ZLP rule.
  cp_aligned : coverpoint ((slen & ((8 << msel) - 1)) == 0) {
    bins aligned = {1};
    bins partial = {0};
  }
  cp_under : coverpoint (slen < wlen) {
    bins sent_less  = {1};
    bins sent_all   = {0};
  }

  // THE cross. Four bins, and exactly one of them requires a ZLP. Closing it
  // is the statement that both halves of the rule were exercised in both
  // directions -- which is what separates a suite that would catch G2 from
  // one that would catch G3.
  x_zlp_rule : cross cp_aligned, cp_under;

  // Every terminator at every packet size: a ZLP on an 8-byte endpoint and
  // on a 64-byte one are the same rule, and a design that special-cases the
  // largest size should be caught.
  x_term_mps : cross cp_term, cp_mps;
endgroup

x_zlp_rule is a four-bin cross that is worth more than its size suggests. The two coverpoints are exactly the two terms of the rule, so the cross enumerates every combination of the two conditions:

cp_alignedcp_underZLP?which mutation lives here
alignedsent lessyesdropping the rule entirely — the hang
alignedsent allnoG2 — a ZLP after an ended transfer
partialsent lessnoG3 — a ZLP after a short packet
partialsent allno—

A regression that closes three of those four bins has, by construction, not tested one of G2 or G3.

15. SystemVerilog Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module usb_descriptor_engine_sva
  import usb_desc_pkg::*;
(
  input logic       clk,
  input logic       rst_n,
  input logic       req_valid,
  input desc_type_e desc_type,
  input logic [7:0] w_length,
  input logic [1:0] mps_sel,
  input logic       found,
  input logic       stall_req,
  input logic [7:0] desc_len,
  input logic [7:0] send_len,
  input logic       truncated,
  input logic       zlp_required,
  input logic [7:0] n_packets,
  input logic [7:0] mps,
  input term_e      terminator
);
  default clocking cb @(posedge clk); endclocking
  default disable iff (!rst_n);

  // ---- 1. THE property. Never offer the host more than it asked for. ----
  property p_never_exceed_wlength;
    send_len <= w_length;
  endproperty
  a_never_exceed_wlength : assert property (p_never_exceed_wlength)
    else $error("offered %0d bytes into a %0d-byte host buffer",
                send_len, w_length);

  // ---- 2. Nor more than the descriptor contains. ----
  property p_never_exceed_descriptor;
    send_len <= desc_len;
  endproperty
  a_never_exceed_descriptor : assert property (p_never_exceed_descriptor);

  // ---- 3. THE other property. Every transfer must have an ending. ----
  property p_transfer_always_terminates;
    found |-> ((send_len == w_length)
               || ((send_len & (mps - 8'd1)) != 8'd0)
               || zlp_required);
  endproperty
  a_transfer_always_terminates :
    assert property (p_transfer_always_terminates)
    else $error("nothing terminates this transfer: sent=%0d wLength=%0d mps=%0d",
                send_len, w_length, mps);

  // ---- 4. ...restated through the named terminator. ----
  property p_terminator_never_none_when_found;
    found |-> (terminator != END_NONE);
  endproperty
  a_terminator_never_none_when_found :
    assert property (p_terminator_never_none_when_found)
    else $error("terminator is END_NONE for a descriptor that was found");

  // ---- 5. A ZLP is never appended to a transfer that already ended. ----
  property p_no_zlp_after_count;
    (send_len == w_length) |-> !zlp_required;
  endproperty
  a_no_zlp_after_count : assert property (p_no_zlp_after_count)
    else $error("a ZLP followed a transfer that ended by count -- the host will see it next transfer");

  // ---- 6. Nor after a short packet. ----
  property p_no_zlp_after_short;
    (found && ((send_len & (mps - 8'd1)) != 8'd0)) |-> !zlp_required;
  endproperty
  a_no_zlp_after_short : assert property (p_no_zlp_after_short);

  // ---- 7. found and stall_req partition the request. ----
  property p_found_xor_stall;
    req_valid |-> (found ^ stall_req);
  endproperty
  a_found_xor_stall : assert property (p_found_xor_stall)
    else $error("a request produced both or neither of found and stall_req");

  // ---- 8. A STALLed request offers nothing at all. ----
  property p_stall_offers_nothing;
    stall_req |-> ((send_len == 8'd0) && (n_packets == 8'd0)
                   && !zlp_required);
  endproperty
  a_stall_offers_nothing : assert property (p_stall_offers_nothing);

  // ---- 9. The packet count can carry the bytes. ----
  property p_packets_carry_bytes;
    (n_packets * mps) >= send_len;
  endproperty
  a_packets_carry_bytes : assert property (p_packets_carry_bytes);

  // ---- 10. A required ZLP is counted as a packet. ----
  property p_zlp_is_counted;
    zlp_required |-> (n_packets == (send_len >> (3 + mps_sel)) + 8'd1);
  endproperty
  a_zlp_is_counted : assert property (p_zlp_is_counted)
    else $error("a required ZLP was not counted in n_packets");

  // ---- Cover: the narrow conditions were actually reached. ----
  c_zlp        : cover property ((zlp_required));
  c_stall      : cover property ((stall_req));
  c_truncated  : cover property ((truncated));
  c_zero_ask   : cover property ((req_valid && (w_length == 8'd0)));
  c_zlp_small  : cover property ((zlp_required && (mps_sel == 2'd0)));
endmodule

bind usb_descriptor_engine usb_descriptor_engine_sva u_sva (.*);

16. Common Misconceptions

"wLength is how much the host wants, so sending more is generous." It is the size of the buffer the host allocated. Sending more is a kernel buffer overflow.

"A short packet and a ZLP are different mechanisms." A ZLP is a short packet — the limiting case. The rule is one rule.

"A ZLP is needed whenever the descriptor is truncated." No: a ZLP is needed when the device sent less than the host asked for and the amount sent was a whole number of packets. Truncation is send_len < desc_len, which is a different comparison, and using it is the bug in §12.

"If there is nothing to send, there is nothing to do." send_len = 0 with wLength > 0 requires a ZLP. send_len = 0 with wLength = 0 requires nothing. Same send_len, opposite answers.

"An unknown descriptor should return an empty one — it is the polite answer." It tells the host the feature exists and is empty, which is a different fact from "absent". STALL is how a device says "I do not have that", and hosts rely on it for feature discovery.

"The alignment test can use 64 — it is the biggest packet size." Only if every endpoint is 64 bytes. A low-speed control endpoint is 8. Mutation G5.

"n_packets is a diagnostic, so an off-by-one is harmless." It is off by one exactly when a ZLP is required, which is exactly when a scheduler using it would under-allocate bus time for the packet that terminates the transfer.

17. Exercises

1. Implement zlp_required as truncated && aligned — the bug from §12 — and predict which of the eight safety properties fires first. Then run it and explain why the count is not the same as G2's or G3's.

2. Extend w_length to its real 16 bits and add a descriptor longer than 255 bytes. Which parts of the design need to change, and which do not? Re-run the exhaustive sweep at its new size and confirm all seven mutations still die.

3. Property 10 checks that a required ZLP is counted. Write the complementary property — that a ZLP is not counted when it is not required — and find the mutation that only the complement catches.

4. The x_zlp_rule cross has four bins and one of them requires a ZLP. Build a regression that closes all four, then remove the zlp_required_seq sequence and measure which bin empties. Use the result to argue whether constructed sequences or biased randomisation is the better way to reach a narrow conjunction.

5. cp_term in §14.4 gives END_NONE an ordinary bin rather than an illegal_bins. Argue both sides, then decide which you would ship — considering what each choice does to a nightly regression that hits the condition once.

6. The VHDL testbench trap in §9 came from reading a signal back in the same process iteration that drove it. Construct a second, subtler instance of the same class of bug somewhere in these testbenches, and say what kind of evidence would reveal it.

18. Summary

IdeaWhy it matters
wLength is the host's buffer sizeexceeding it is a kernel buffer overflow
send_len = min(wLength, desc_len)and the host really does ask for less
A data stage ends by count or by short packetthere is no length field in the protocol
zlp_required = (send_len < wLength) && alignedboth terms load-bearing
drop the first terma ZLP after a transfer that already ended
drop the seconda ZLP after a short packet
drop boththe multiple-of-mps read hangs
send_len = 0, wLength > 0 → ZLPbut wLength = 0 → nothing
An unknown descriptor → STALLnot a length-0 descriptor
terminator names the ending, including END_NONEa hang is an absence; give the absence a name
mps is a power of twoso alignment is a mask and division is a shift
2560-point exhaustive verification8 types × 80 lengths × 4 packet sizes
7 mutations, all killed in 3 languagesafter a stimulus mismatch skewed four columns

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o de_v.out de_v.v de_v_tb.v && ./de_v.out
SystemVerilogiverilog -g2012 -o de_sv.out de_sv.sv de_sv_tb.sv && ./de_sv.out
VHDL-2008 analysenvc --std=2008 -a de_vhdl.vhd de_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_de_vhdl
VHDL-2008 runnvc --std=2008 -r tb_de_vhdl
One mutationiverilog -g2005 -DMUT_G1 -o mm de_v_mut.v de_v_tb.v && ./mm

All three implementations pass with 0 errors: 2560 of 2560 exhaustive points, 40 000 randomised requests, every descriptor type reached and asserted reached.


Chapter 21.4 — Protocol Engine is the block that actually moves the bytes, and it has a constraint none of the previous three had: the handshake is due before the CRC is known. A packet's CRC covers the whole packet, so it is only verified at the end — yet the device must already have been writing the payload somewhere. The answer is to write it provisionally and commit or discard on the CRC result, which makes the endpoint FIFO's write pointer something that can move backwards.

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.