Skip to content
VLSI Mentor

USB · Module 20

SuperSpeed Concepts

USB 3 kept the single master and deleted the polling — credit-based flow control, announced readiness, and a sender bounded by the smallest of three limits.

Nineteen modules have rested on one fact: the host asks, and nothing happens until it does. Chapter 2.6 established it; 18.3 built an entire polled reporting mechanism because of it; 19.5 treated the single exception as a large enough concession to need two fences.

USB 3 keeps the single master and deletes the polling. Those turn out to be separable, and separating them is most of what SuperSpeed is.

1. The Cost of Asking

A USB 2 bulk endpoint with nothing to say NAKs, and the host asks again.

The asking costs bus time whether or not there is ever an answer. Every poll is a token packet, a turnaround, and a handshake — a complete transaction that moves no data. On a busy bus with several idle endpoints, a significant fraction of every frame is spent discovering that nothing has changed.

And the host cannot know when to stop, because the only way to find out whether a device is ready is to ask it.

USB 2USB 3
How readiness is learnedby trying — poll, NAK, repeatby being told — the device sends ERDY
Cost of an idle endpointa transaction per poll intervalnothing
Who tracks itthe host, by failingthe device, by announcing

USB 3's inversion is not that devices may initiate. They still may not — 19.5's exception is still the only one. It is that a device may decline once and be believed, and say later when that changes.

2. ERDY Is Not a Credit

This is the confusion the chapter exists to prevent.

ERDY — endpoint ready — tells the host resume asking me. It grants no permission to send anything.

The permission is credits, and credits come from an ACK transaction packet, which carries a field called NumP: the number of packets the receiver can still accept.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ERDY   "I have data now."            → the host resumes scheduling
   ACK    "I can take N more packets."  → the sender may have N outstanding

A design that treats ERDY as a credit transmits into a receiver that has room for nothing, which is a receive-buffer overflow rather than a retryable protocol error. Mutation C4 does exactly that and dies 38 457 times — after §12, which is about why it did not at first.

3. Three Limits, and the Smallest Wins

A SuperSpeed sender is bounded by three separate things:

LimitWhere it comes from
1creditsthe receiver's NumP, in an ACK
2burst sizebMaxBurst + 1, from the endpoint companion descriptor
3datahow many packets it actually has

Conflating any two of them produces a design that works until the third becomes binding.

  • min(credits, pending) — ignores the burst limit, and overruns a receiver PHY that cannot absorb packets back to back. Mutation C2, 3765 errors.
  • min(credits, burst) — claims it may send packets it does not have. Mutation C3, 41 723 errors.

4. bMaxBurst Is Burst Size Minus One

The SuperSpeed endpoint companion descriptor stores it that way:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   /* USB_DT_SS_ENDPOINT_COMP: SuperSpeed Endpoint Companion descriptor */
   struct usb_ss_ep_comp_descriptor {
           __u8  bLength;
           __u8  bDescriptorType;
           __u8  bMaxBurst;
           __u8  bmAttributes;
           __le16 wBytesPerInterval;
   } __attribute__ ((packed));

bMaxBurst = 0 means a burst of ONE packet. bMaxBurst = 15 means sixteen.

A design that treats the field as the burst size itself forbids bursts entirely on every endpoint that declared 0 — which is most of them, and which is a permanent stall rather than a slowdown, because a burst allowance of zero never permits anything.

Mutation C1 does that and dies 46 038 times. The design saturates at 255 rather than wrapping, for the same reason: bMaxBurst = 255 incrementing to zero would stall the highest-throughput endpoints on the bus.

5. The Flow, Drawn

A block diagram of SuperSpeed credit-based flow control, arranged in four rows. At the top is the receiver, which sends an acknowledgement transaction packet carrying a field called NumP, the number of packets it can still accept. In the second row on the left, the endpoint may separately send an ERDY, meaning it has data and the host should resume scheduling it; this path grants no permission to transmit and deliberately does not touch the credit count. On the right of that row is the endpoint companion descriptor's bMaxBurst field, which is the burst size minus one, so that a declared value of zero means a burst of one packet and fifteen means sixteen. In the third row, three independent limits converge on a single decision block: the credits the receiver advertised, the burst allowance remaining after packets already in flight, and the number of packets the sender actually has. The decision block takes the minimum of all three, not any two of them. The bottom row holds the two outputs of that decision: how many packets may be sent right now, and which of the three limits was the binding one, the latter being the more useful answer because a slow link has three completely different fixes depending on which limit is tight.Receiver → ACK (NumP)how many packets it can still takeERDY"resume asking me" — grants NOTHINGbMaxBurstburst size MINUS ONEMINIMUM of threecredits · burst room · packetspendingmay_sendhow many may go right nowbinding_limitWHICH limit — three different fixescreditsno creditburst+1countreason12
Figure 1 — what a SuperSpeed sender may transmit. The shaded block is §3: three independent limits, and the smallest of them decides. The ERDY path on the left grants nothing — it only tells the host to resume scheduling, which is §2.

The dashed ERDY edge is drawn deliberately. It reaches the decision and contributes nothing to it, which is exactly what §2 says and what mutation C4 breaks.

6. The Hardware, Before Any Language

One combinational decision and a small amount of state.

burst_size is bMaxBurst + 1, saturating (§4).

burst_room is what is left of the burst after packets already in flight, floored at zero — a sender that has already used its allowance may send nothing more until an ACK retires it.

may_send is the minimum of three (§3), and binding_limit names which.

An ACK does two things: it sets the credits, and it retires the outstanding count. Those move together — a design that decremented credits without tracking outstanding would permit a whole new burst the instant an ACK arrived, before the previous one had drained. Mutation C6, 20 267 errors.

A link reset discards the credit state entirely. Credits describe room in a receiver that has just been reset; carrying them across would let the sender transmit into a buffer that no longer exists. Mutation C5, 14 794 errors.

And flow_blocked is separate from "nothing to send" — the difference between a link that is idle and one that is stalled, which look identical from outside.

7. Verilog-2005

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb3_credit_flow -- what USB 3 replaced polling with, and why the bus got
// quiet.
//
// EVERYTHING in Modules 1 to 19 rested on the host asking. A device with no
// data to give NAKs, and the host asks again -- and again -- and the asking
// costs bus time whether or not there is ever an answer. Chapter 12.4
// measured that cost; a bulk endpoint with nothing to say can consume a
// large fraction of a frame doing nothing.
//
// USB 3 keeps the single master and removes the polling, by inverting who
// tracks readiness:
//
//   USB 2   the HOST asks, the device answers NAK, repeat.
//           Readiness is discovered by trying.
//
//   USB 3   the DEVICE says when it is ready (ERDY), and until then the
//           host does not ask at all. Readiness is ANNOUNCED.
//
// and by giving the sender a budget instead of a permission slip:
//
//   CREDITS  the receiver tells the sender how many packets it can still
//            accept (NumP, carried in an ACK transaction packet). The
//            sender may have that many packets outstanding at once, and
//            must stop when the budget is spent.
//
// THREE LIMITS, NOT ONE
//
// A sender is bounded by three separate things and the smallest wins:
//
//   1. CREDITS       how many packets the receiver has room for now.
//   2. BURST SIZE    bMaxBurst + 1 -- the most it may send back to back.
//   3. DATA          how many packets it actually has.
//
// Conflating any two of them produces a design that works until the third
// becomes the binding one.
//
// bMaxBurst IS BURST SIZE MINUS ONE
//
// The SuperSpeed endpoint companion descriptor stores it that way, so a
// bMaxBurst of 0 means a burst of ONE packet, and 15 means 16. A design
// that treats the field as the burst size itself silently halves the
// throughput of every endpoint that declared 1 and forbids bursts entirely
// on every endpoint that declared 0.
//
//     /* USB_DT_SS_ENDPOINT_COMP: SuperSpeed Endpoint Companion descriptor */
//     struct usb_ss_ep_comp_descriptor {
//             __u8  bLength;
//             __u8  bDescriptorType;
//             __u8  bMaxBurst;
//             __u8  bmAttributes;
//             __le16 wBytesPerInterval;
//     } __attribute__ ((packed));
module usb3_credit_flow #(
  parameter integer MAX_CREDITS = 31   // NumP is a bounded field
) (
  input  wire        clk,
  input  wire        rst_n,

  // --- from the receiver, in an ACK transaction packet ---
  input  wire        ack_valid,
  input  wire [7:0]  ack_nump,        // credits the receiver is advertising
  input  wire        erdy,            // "I am ready now" -- replaces polling

  // --- the endpoint's own situation ---
  input  wire [7:0]  b_max_burst,     // the DESCRIPTOR field: burst MINUS one
  input  wire [7:0]  packets_pending, // how many it actually has to send
  input  wire        send,            // one packet leaves the sender

  input  wire        link_reset,

  output wire [7:0]  burst_size,      // bMaxBurst + 1
  output reg  [7:0]  credits,         // what the receiver said it can take
  output reg  [7:0]  outstanding,     // sent but not yet acknowledged
  output wire [7:0]  may_send,        // how many it may send RIGHT NOW
  output wire        can_send,
  output wire [1:0]  binding_limit,   // WHICH of the three is binding
  output reg         ready_announced, // ERDY sent, host has not returned
  output reg         flow_blocked,    // sticky: it wanted to send and could not
  output reg  [31:0] blocked_cycles,
  output reg  [31:0] packets_sent,
  output reg         credit_violation // sticky: a send with no credit
);
  // THE OFF-BY-ONE THAT MATTERS. bMaxBurst is burst size minus one.
  // Saturating at 255 so a descriptor declaring the maximum does not wrap
  // to a burst of zero -- which would stall the endpoint permanently.
  assign burst_size = (b_max_burst == 8'hFF) ? 8'hFF : (b_max_burst + 8'd1);

  // How much of the burst allowance is left after what is already in flight.
  wire [8:0] burst_room_w = {1'b0, burst_size} - {1'b0, outstanding};
  wire [7:0] burst_room   = (outstanding >= burst_size) ? 8'd0
                                                        : burst_room_w[7:0];

  // THE MINIMUM OF THREE. Not two: a design that takes min(credits, burst)
  // will happily claim it may send packets it does not have, and one that
  // takes min(credits, pending) will overrun a burst the receiver's PHY
  // cannot absorb back to back.
  wire [7:0] min_cb = (credits < burst_room) ? credits : burst_room;
  assign may_send   = (min_cb < packets_pending) ? min_cb : packets_pending;
  assign can_send   = (may_send != 8'd0);

  // Which limit is actually binding. This is not decoration: "the link is
  // slow" has three completely different fixes depending on the answer --
  // a bigger receive buffer, a larger bMaxBurst, or more data to send --
  // and from outside the three are indistinguishable.
  //
  // Verilog-2005 has no enumerated type, so the encoding is localparams.
  // Chapters 19.2 and 19.3 measured what happens when one language's
  // design exposes less than the others, so all three expose this.
  localparam [1:0] LIM_NONE    = 2'd0,  // everything pending can go now
                   LIM_CREDITS = 2'd1,  // the receiver has no room
                   LIM_BURST   = 2'd2,  // the burst allowance is spent
                   LIM_PENDING = 2'd3;  // there is simply nothing to send
  assign binding_limit =
      (packets_pending == 8'd0)          ? LIM_PENDING
    : (credits    <= burst_room &&
       credits    <  packets_pending)    ? LIM_CREDITS
    : (burst_room <  packets_pending)    ? LIM_BURST
    :                                      LIM_NONE;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      credits          <= 8'd0;
      outstanding      <= 8'd0;
      flow_blocked     <= 1'b0;
      blocked_cycles   <= 32'd0;
      packets_sent     <= 32'd0;
      credit_violation <= 1'b0;
      ready_announced  <= 1'b0;
    end else if (link_reset) begin
      // A link reset discards the credit state entirely. Credits describe
      // room in a receiver that has just been reset; carrying them across
      // would let the sender transmit into a buffer that no longer exists.
      credits          <= 8'd0;
      outstanding      <= 8'd0;
      flow_blocked     <= 1'b0;
      ready_announced  <= 1'b0;
    end else begin
      // An ERDY is a readiness ANNOUNCEMENT, not a credit grant. It tells
      // the host to resume asking; the credits still come from an ACK.
      // Treating ERDY as a credit is how a design ends up transmitting
      // into a receiver that has room for nothing -- so it sets a flag and
      // touches `credits` nowhere.
      //
      // The flag clears when the host comes back, which is what makes it
      // measurable: the interval between the two is the latency USB 3
      // trades polling bandwidth for, and a device that keeps re-announcing
      // is a device whose ERDYs are not arriving.
      if (erdy && !ack_valid) ready_announced <= 1'b1;
      else if (ack_valid)     ready_announced <= 1'b0;

      if (ack_valid) begin
        credits <= (ack_nump > MAX_CREDITS[7:0]) ? MAX_CREDITS[7:0] : ack_nump;
        // An acknowledgement also retires what it acknowledges.
        outstanding <= 8'd0;
      end

      if (send) begin
        if (can_send) begin
          // Spend one credit and add one to the outstanding count. The two
          // move together: a design that decremented credits without
          // tracking outstanding would allow a whole new burst the instant
          // an ACK arrived, before the previous one had drained.
          if (!ack_valid) begin
            credits     <= (credits == 8'd0) ? 8'd0 : (credits - 8'd1);
            outstanding <= outstanding + 8'd1;
          end
          packets_sent <= packets_sent + 32'd1;
        end else begin
          // A send attempt with nothing to spend it on. This is a design
          // error in the layer above, and it is RECORDED rather than
          // silently dropped -- an unlogged violation is indistinguishable
          // from a packet that was never offered.
          credit_violation <= 1'b1;
        end
      end

      // Wanting to send and being unable to is the condition worth
      // measuring: it is the difference between a link that is idle and a
      // link that is stalled, which look identical from outside.
      if ((packets_pending != 8'd0) && !can_send) begin
        flow_blocked   <= 1'b1;
        blocked_cycles <= blocked_cycles + 32'd1;
      end
    end
  end
endmodule

min_cb then may_send is two comparisons, not one three-way expression, and the intermediate is named because §3's whole point is that there are three limits rather than two. A single nested ternary would hide which pair was being compared.

8. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package usb3_flow_pkg;
  // WHICH of the three limits is binding. This is not decoration: "the link
  // is slow" has three completely different fixes depending on the answer --
  // a bigger receive buffer, a larger bMaxBurst, or more data to send -- and
  // from outside the three are indistinguishable.
  typedef enum logic [1:0] {
    LIM_NONE,      // everything pending can go now
    LIM_CREDITS,   // the receiver has no room
    LIM_BURST,     // the burst allowance is spent
    LIM_PENDING    // there is simply nothing to send
  } flow_limit_e;
endpackage

// usb3_credit_flow_sv -- what USB 3 replaced polling with, and why the bus got
// quiet.
//
// EVERYTHING in Modules 1 to 19 rested on the host asking. A device with no
// data to give NAKs, and the host asks again -- and again -- and the asking
// costs bus time whether or not there is ever an answer. Chapter 12.4
// measured that cost; a bulk endpoint with nothing to say can consume a
// large fraction of a frame doing nothing.
//
// USB 3 keeps the single master and removes the polling, by inverting who
// tracks readiness:
//
//   USB 2   the HOST asks, the device answers NAK, repeat.
//           Readiness is discovered by trying.
//
//   USB 3   the DEVICE says when it is ready (ERDY), and until then the
//           host does not ask at all. Readiness is ANNOUNCED.
//
// and by giving the sender a budget instead of a permission slip:
//
//   CREDITS  the receiver tells the sender how many packets it can still
//            accept (NumP, carried in an ACK transaction packet). The
//            sender may have that many packets outstanding at once, and
//            must stop when the budget is spent.
//
// THREE LIMITS, NOT ONE
//
// A sender is bounded by three separate things and the smallest wins:
//
//   1. CREDITS       how many packets the receiver has room for now.
//   2. BURST SIZE    bMaxBurst + 1 -- the most it may send back to back.
//   3. DATA          how many packets it actually has.
//
// Conflating any two of them produces a design that works until the third
// becomes the binding one.
//
// bMaxBurst IS BURST SIZE MINUS ONE
//
// The SuperSpeed endpoint companion descriptor stores it that way, so a
// bMaxBurst of 0 means a burst of ONE packet, and 15 means 16. A design
// that treats the field as the burst size itself silently halves the
// throughput of every endpoint that declared 1 and forbids bursts entirely
// on every endpoint that declared 0.
//
//     /* USB_DT_SS_ENDPOINT_COMP: SuperSpeed Endpoint Companion descriptor */
//     struct usb_ss_ep_comp_descriptor {
//             __u8  bLength;
//             __u8  bDescriptorType;
//             __u8  bMaxBurst;
//             __u8  bmAttributes;
//             __le16 wBytesPerInterval;
//     } __attribute__ ((packed));
module usb3_credit_flow_sv
  import usb3_flow_pkg::*;
#(
  parameter int unsigned MAX_CREDITS = 31  // NumP is a bounded field
) (
  input  logic       clk,
  input  logic       rst_n,

  // --- from the receiver, in an ACK transaction packet ---
  input  logic       ack_valid,
  input  logic [7:0] ack_nump,        // credits the receiver is advertising
  input  logic       erdy,            // "I am ready now" -- replaces polling

  // --- the endpoint's own situation ---
  input  logic [7:0] b_max_burst,     // the DESCRIPTOR field: burst MINUS one
  input  logic [7:0] packets_pending, // how many it actually has to send
  input  logic       send,            // one packet leaves the sender

  input  logic       link_reset,

  output logic [7:0] burst_size,      // bMaxBurst + 1
  output logic [7:0] credits,         // what the receiver said it can take
  output logic [7:0] outstanding,     // sent but not yet acknowledged
  output logic [7:0] may_send,        // how many it may send RIGHT NOW
  output logic       can_send,
  output flow_limit_e binding_limit,   // WHICH of the three is binding
  output logic       ready_announced, // ERDY sent, host has not returned
  output logic       flow_blocked,    // sticky: it wanted to send and could not
  output logic [31:0] blocked_cycles,
  output logic [31:0] packets_sent,
  output logic       credit_violation // sticky: a send with no credit
);
  initial begin
    if (MAX_CREDITS < 1)
      $fatal(1, "MAX_CREDITS=%0d: a link that can never grant a credit can never carry data",
             MAX_CREDITS);
    if (MAX_CREDITS > 255)
      $fatal(1, "MAX_CREDITS=%0d exceeds what the NumP field can express",
             MAX_CREDITS);
  end

  // THE OFF-BY-ONE THAT MATTERS. bMaxBurst is burst size minus one.
  // Saturating at 255 so a descriptor declaring the maximum does not wrap
  // to a burst of zero -- which would stall the endpoint permanently.
  assign burst_size = (b_max_burst == 8'hFF) ? 8'hFF : (b_max_burst + 8'd1);

  // How much of the burst allowance is left after what is already in flight.
  wire [8:0] burst_room_w = {1'b0, burst_size} - {1'b0, outstanding};
  wire [7:0] burst_room   = (outstanding >= burst_size) ? 8'd0
                                                        : burst_room_w[7:0];

  // THE MINIMUM OF THREE. Not two: a design that takes min(credits, burst)
  // will happily claim it may send packets it does not have, and one that
  // takes min(credits, pending) will overrun a burst the receiver's PHY
  // cannot absorb back to back.
  wire [7:0] min_cb = (credits < burst_room) ? credits : burst_room;
  assign may_send   = (min_cb < packets_pending) ? min_cb : packets_pending;
  assign can_send   = (may_send != 8'd0);

  // Which limit is actually binding. This is not decoration: "the link is
  // slow" has three completely different fixes depending on the answer --
  // a bigger receive buffer, a larger bMaxBurst, or more data to send --
  // and from outside the three are indistinguishable.
  //
  // Written as branches rather than nested ternaries: a conditional
  // expression yielding an enum needs an explicit cast in some tools.
  always_comb begin
    if      (packets_pending == 8'd0)      binding_limit = LIM_PENDING;
    else if (credits <= burst_room
             && credits < packets_pending) binding_limit = LIM_CREDITS;
    else if (burst_room < packets_pending) binding_limit = LIM_BURST;
    else                                   binding_limit = LIM_NONE;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      credits          <= 8'd0;
      outstanding      <= 8'd0;
      flow_blocked     <= 1'b0;
      blocked_cycles   <= 32'd0;
      packets_sent     <= 32'd0;
      credit_violation <= 1'b0;
      ready_announced  <= 1'b0;
    end else if (link_reset) begin
      // A link reset discards the credit state entirely. Credits describe
      // room in a receiver that has just been reset; carrying them across
      // would let the sender transmit into a buffer that no longer exists.
      credits          <= 8'd0;
      outstanding      <= 8'd0;
      flow_blocked     <= 1'b0;
      ready_announced  <= 1'b0;
    end else begin
      // An ERDY is a readiness ANNOUNCEMENT, not a credit grant. It tells
      // the host to resume asking; the credits still come from an ACK.
      // Treating ERDY as a credit is how a design ends up transmitting
      // into a receiver that has room for nothing -- so it sets a flag and
      // touches `credits` nowhere.
      //
      // The flag clears when the host comes back, which is what makes it
      // measurable: the interval between the two is the latency USB 3
      // trades polling bandwidth for, and a device that keeps re-announcing
      // is a device whose ERDYs are not arriving.
      if (erdy && !ack_valid) ready_announced <= 1'b1;
      else if (ack_valid)     ready_announced <= 1'b0;

      if (ack_valid) begin
        credits <= (ack_nump > 8'(MAX_CREDITS)) ? 8'(MAX_CREDITS) : ack_nump;
        // An acknowledgement also retires what it acknowledges.
        outstanding <= 8'd0;
      end

      if (send) begin
        if (can_send) begin
          // Spend one credit and add one to the outstanding count. The two
          // move together: a design that decremented credits without
          // tracking outstanding would allow a whole new burst the instant
          // an ACK arrived, before the previous one had drained.
          if (!ack_valid) begin
            credits     <= (credits == 8'd0) ? 8'd0 : (credits - 8'd1);
            outstanding <= outstanding + 8'd1;
          end
          packets_sent <= packets_sent + 32'd1;
        end else begin
          // A send attempt with nothing to spend it on. This is a design
          // error in the layer above, and it is RECORDED rather than
          // silently dropped -- an unlogged violation is indistinguishable
          // from a packet that was never offered.
          credit_violation <= 1'b1;
        end
      end

      // Wanting to send and being unable to is the condition worth
      // measuring: it is the difference between a link that is idle and a
      // link that is stalled, which look identical from outside.
      if ((packets_pending != 8'd0) && !can_send) begin
        flow_blocked   <= 1'b1;
        blocked_cycles <= blocked_cycles + 32'd1;
      end
    end
  end
endmodule

flow_limit_e has four values because there are four answers, and LIM_PENDING is the one people leave out. An endpoint with nothing to send is not flow-controlled — it is idle and working correctly — and a design that reported LIM_CREDITS for it would send a debugging effort after a receive buffer that is perfectly fine.

9. VHDL-2008

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

package usb3_flow_pkg is
  -- WHICH of the three limits is binding. This is not decoration: "the link
  -- is slow" has three completely different fixes depending on the answer --
  -- a bigger receive buffer, a larger bMaxBurst, or more data to send -- and
  -- from outside the three are indistinguishable.
  type flow_limit_t is (
    LIM_NONE,      -- everything pending can go now
    LIM_CREDITS,   -- the receiver has no room
    LIM_BURST,     -- the burst allowance is spent
    LIM_PENDING    -- there is simply nothing to send
  );
end package;

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

-- usb3_credit_flow_vhdl -- what USB 3 replaced polling with, and why the bus
-- got quiet.
--
-- EVERYTHING in Modules 1 to 19 rested on the host asking. A device with no
-- data to give NAKs, and the host asks again -- and again -- and the asking
-- costs bus time whether or not there is ever an answer.
--
-- USB 3 keeps the single master and removes the polling, by inverting who
-- tracks readiness:
--
--   USB 2   the HOST asks, the device answers NAK, repeat.
--           Readiness is discovered by trying.
--
--   USB 3   the DEVICE says when it is ready (ERDY), and until then the
--           host does not ask at all. Readiness is ANNOUNCED.
--
-- and by giving the sender a budget instead of a permission slip:
--
--   CREDITS  the receiver tells the sender how many packets it can still
--            accept (NumP, carried in an ACK transaction packet).
--
-- THREE LIMITS, NOT ONE
--
-- A sender is bounded by three separate things and the smallest wins:
--
--   1. CREDITS       how many packets the receiver has room for now.
--   2. BURST SIZE    bMaxBurst + 1 -- the most it may send back to back.
--   3. DATA          how many packets it actually has.
--
-- bMaxBurst IS BURST SIZE MINUS ONE. The SuperSpeed endpoint companion
-- descriptor stores it that way, so a bMaxBurst of 0 means a burst of ONE
-- packet and 15 means 16. A design that treats the field as the burst size
-- itself forbids bursts entirely on every endpoint that declared 0.
entity usb3_credit_flow_vhdl is
  generic (
    MAX_CREDITS : positive := 31   -- NumP is a bounded field
  );
  port (
    clk              : in  std_logic;
    rst_n            : in  std_logic;

    ack_valid        : in  std_logic;
    ack_nump         : in  unsigned(7 downto 0);
    erdy             : in  std_logic;

    b_max_burst      : in  unsigned(7 downto 0);  -- the DESCRIPTOR field
    packets_pending  : in  unsigned(7 downto 0);
    send             : in  std_logic;

    link_reset       : in  std_logic;

    burst_size       : out unsigned(7 downto 0);  -- bMaxBurst + 1
    credits          : out unsigned(7 downto 0);
    outstanding      : out unsigned(7 downto 0);
    may_send         : out unsigned(7 downto 0);
    can_send         : out std_logic;
    binding_limit    : out flow_limit_t;
    ready_announced  : out std_logic;
    flow_blocked     : out std_logic;
    blocked_cycles   : out unsigned(31 downto 0);
    packets_sent     : out unsigned(31 downto 0);
    credit_violation : out std_logic
  );
end entity;

architecture rtl of usb3_credit_flow_vhdl is
  signal cred_r  : unsigned(7 downto 0) := (others => '0');
  signal out_r   : unsigned(7 downto 0) := (others => '0');
  signal rdy_r   : std_logic := '0';
  signal blk_r   : std_logic := '0';
  signal blkc_r  : unsigned(31 downto 0) := (others => '0');
  signal sent_r  : unsigned(31 downto 0) := (others => '0');
  signal viol_r  : std_logic := '0';

  signal bsz_i   : unsigned(7 downto 0);
  signal room_i  : unsigned(7 downto 0);
  signal may_i   : unsigned(7 downto 0);
  signal mincb_i : unsigned(7 downto 0);
begin
  assert MAX_CREDITS <= 255
    report "MAX_CREDITS exceeds what the NumP field can express"
    severity failure;

  -- THE OFF-BY-ONE THAT MATTERS. bMaxBurst is burst size minus one.
  -- Saturating at 255 so a descriptor declaring the maximum does not wrap
  -- to a burst of zero -- which would stall the endpoint permanently.
  bsz_i <= to_unsigned(255, 8) when b_max_burst = to_unsigned(255, 8)
           else b_max_burst + 1;

  -- How much of the burst allowance is left after what is already in flight.
  room_i <= to_unsigned(0, 8) when out_r >= bsz_i else (bsz_i - out_r);

  -- THE MINIMUM OF THREE. Not two: a design that takes min(credits, burst)
  -- will happily claim it may send packets it does not have, and one that
  -- takes min(credits, pending) will overrun a burst the receiver's PHY
  -- cannot absorb back to back.
  mincb_i <= cred_r when cred_r < room_i else room_i;
  may_i   <= mincb_i when mincb_i < packets_pending else packets_pending;

  burst_size       <= bsz_i;
  credits          <= cred_r;
  outstanding      <= out_r;
  may_send         <= may_i;
  can_send         <= '1' when may_i /= to_unsigned(0, 8) else '0';
  ready_announced  <= rdy_r;
  flow_blocked     <= blk_r;
  blocked_cycles   <= blkc_r;
  packets_sent     <= sent_r;
  credit_violation <= viol_r;

  binding_limit <= LIM_PENDING when packets_pending = to_unsigned(0, 8) else
                   LIM_CREDITS when (cred_r <= room_i
                                     and cred_r < packets_pending) else
                   LIM_BURST   when room_i < packets_pending else
                   LIM_NONE;

  process (clk, rst_n)
    variable nc, no : unsigned(7 downto 0);
  begin
    if rst_n = '0' then
      cred_r <= (others => '0'); out_r <= (others => '0');
      rdy_r <= '0'; blk_r <= '0';
      blkc_r <= (others => '0'); sent_r <= (others => '0');
      viol_r <= '0';
    elsif rising_edge(clk) then
      if link_reset = '1' then
        -- A link reset discards the credit state entirely. Credits describe
        -- room in a receiver that has just been reset; carrying them across
        -- would let the sender transmit into a buffer that no longer exists.
        cred_r <= (others => '0'); out_r <= (others => '0');
        blk_r <= '0'; rdy_r <= '0';
      else
        -- An ERDY is a readiness ANNOUNCEMENT, not a credit grant. It tells
        -- the host to resume asking; the credits still come from an ACK.
        -- Treating ERDY as a credit is how a design ends up transmitting
        -- into a receiver that has room for nothing -- so it sets a flag
        -- and touches the credit count nowhere.
        if erdy = '1' and ack_valid = '0' then rdy_r <= '1';
        elsif ack_valid = '1' then rdy_r <= '0'; end if;

        nc := cred_r; no := out_r;
        if ack_valid = '1' then
          if ack_nump > to_unsigned(MAX_CREDITS, 8) then
            nc := to_unsigned(MAX_CREDITS, 8);
          else
            nc := ack_nump;
          end if;
          -- An acknowledgement also retires what it acknowledges.
          no := (others => '0');
        end if;

        if send = '1' then
          if may_i /= to_unsigned(0, 8) then
            -- Spend one credit and add one to the outstanding count. The
            -- two move together: a design that decremented credits without
            -- tracking outstanding would allow a whole new burst the
            -- instant an ACK arrived, before the previous one had drained.
            if ack_valid = '0' then
              if nc /= to_unsigned(0, 8) then nc := nc - 1; end if;
              no := no + 1;
            end if;
            sent_r <= sent_r + 1;
          else
            -- A send attempt with nothing to spend it on. Recorded rather
            -- than silently dropped: an unlogged violation is
            -- indistinguishable from a packet that was never offered.
            viol_r <= '1';
          end if;
        end if;

        cred_r <= nc; out_r <= no;

        -- Wanting to send and being unable to is the condition worth
        -- measuring: it is the difference between a link that is idle and
        -- a link that is stalled, which look identical from outside.
        if packets_pending /= to_unsigned(0, 8)
           and may_i = to_unsigned(0, 8) then
          blk_r  <= '1';
          blkc_r <= blkc_r + 1;
        end if;
      end if;
    end if;
  end process;
end architecture;

The process computes nc and no as variables and assigns them to the signals once, at the end. That is idiomatic VHDL and it reads clearly — and §12 is about the fact that it also silently neutralised a mutation written against the signals.

10. The Testbench: 4096 Points and 64 Burst Walks

The decision is a pure function of three numbers, so the sweep enumerates them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // 16 bMaxBurst values x 16 credit grants x 16 pending counts = 4096
    // points with nothing outstanding: the whole decision surface.
    for (b=0; b<16; b=b+1)
     for (c=0; c<16; c=c+1)
      for (a=0; a<16; a=a+1) begin

And a second sweep drives the burst allowance down by actually sending, because burst_room is state rather than an input:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // For each bMaxBurst 0..7, send k packets 0..7 and check the room left,
    // with credits held high so the BURST limit is the one under test.

Four safety properties are checked against no model, and the first is the one with consequences:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE one that matters: never claim to be able to send more than
      //    the receiver said it has room for. Overrunning a credit is a
      //    receive-buffer overflow, not a retryable protocol error.
      check(may_send <= credits,
            "may_send exceeded the credits the receiver advertised");
      // 2. Nor more than the burst allowance permits.
      check(outstanding + may_send <= burst_size || outstanding >= burst_size,
            "a burst would exceed bMaxBurst + 1");
      // 3. Nor more packets than actually exist.
      check(may_send <= pend, "may_send exceeded the packets pending");
      // 4. burst_size is never zero: a zero burst stalls the endpoint for
      //    ever, and bMaxBurst = 0 legitimately means a burst of ONE.
      check(burst_size !== 8'd0, "burst_size collapsed to zero");

Properties 1, 2 and 3 are the three limits restated as inequalities — which is a different statement from "may_send equals the minimum", and a stronger one: it holds whatever the design computed, including if it computed something the model also got wrong.

And the model takes the minimum by sorting three values where the design nests two comparisons. Same answer, different route.

Measured reach:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  exhaustive min-of-three sweep: 4096 of 4096 points verified
  exhaustive burst-consumption sweep: 64 of 64 verified

  Verilog / SystemVerilog:
  REACH: binding: none=18313 credits=22702 burst=1608 pending=1120
  [Verilog] usb3_credit_flow: 0 errors — PASS

  VHDL:
  REACH: binding: none=18206 credits=22528 burst=1596 pending=1457
  [VHDL] usb3_credit_flow_vhdl: 0 errors — PASS

All four binding cases are reached thousands of times, which is the sweep's justification: a run that never reaches LIM_BURST has not tested §4's off-by-one at all, however many credits it varied.

10.1 The complete Verilog testbench

The excerpts above are the parts worth arguing about. Here is the whole thing — the sweeps, the reference model, the safety properties and the reach assertions, exactly as simulated against the usb3_credit_flow listing published in this chapter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_cf_v;
  localparam MAXC = 31;
  reg clk=0, rst_n=0, ack_valid=0, erdy=0, send=0, link_reset=0;
  reg [7:0] ack_nump=0, bmb=0, pend=0;
  wire [7:0] burst_size, credits, outstanding, may_send;
  wire can_send, flow_blocked, credit_violation, ready_announced;
  wire [1:0] binding_limit;
  wire [31:0] blocked_cycles, packets_sent;
  always #5 clk=~clk;

  usb3_credit_flow #(.MAX_CREDITS(MAXC)) dut (
    .clk(clk), .rst_n(rst_n), .ack_valid(ack_valid), .ack_nump(ack_nump),
    .erdy(erdy), .b_max_burst(bmb), .packets_pending(pend), .send(send),
    .link_reset(link_reset), .burst_size(burst_size), .credits(credits),
    .outstanding(outstanding), .may_send(may_send), .can_send(can_send),
    .binding_limit(binding_limit), .ready_announced(ready_announced),
    .flow_blocked(flow_blocked), .blocked_cycles(blocked_cycles),
    .packets_sent(packets_sent), .credit_violation(credit_violation));

  localparam [1:0] L_NONE=0, L_CRED=1, L_BURST=2, L_PEND=3;

  integer errors=0, i, a, b, c, k;
  integer n_exh=0, n_burst_exh=0;
  integer n_lim [0:3];
  // ---- INDEPENDENT MODEL: its own credits, outstanding and flags ----
  integer m_cred, m_out, m_sent;
  reg m_ready, m_viol;

  task check(input cond, input [639:0] msg);
    begin if (!cond) begin errors=errors+1;
      if (errors <= 25)
        $display("  FAIL: %0s (bmb=%0d cred=%0d out=%0d pend=%0d | bsz=%0d may=%0d lim=%0d, t=%0t)",
                 msg, bmb, credits, outstanding, pend, burst_size, may_send,
                 binding_limit, $time);
    end end
  endtask

  // The model computes the minimum by sorting three values rather than by
  // two nested comparisons. Same answer, different route.
  function [7:0] min3(input [15:0] x, input [15:0] y, input [15:0] z);
    integer lo;
    begin
      lo = x; if (y < lo) lo = y; if (z < lo) lo = z;
      min3 = lo[7:0];
    end
  endfunction

  task check_comb;
    integer e_bsz, e_room, e_may, e_lim;
    begin
      e_bsz  = (bmb == 255) ? 255 : bmb + 1;
      e_room = (m_out >= e_bsz) ? 0 : (e_bsz - m_out);
      e_may  = min3(m_cred, e_room, pend);
      if (pend == 0)                              e_lim = L_PEND;
      else if (m_cred <= e_room && m_cred < pend) e_lim = L_CRED;
      else if (e_room < pend)                     e_lim = L_BURST;
      else                                        e_lim = L_NONE;

      check(burst_size    === e_bsz[7:0],  "burst_size is bMaxBurst PLUS ONE");
      check(credits       === m_cred[7:0], "credits matches the model");
      check(outstanding   === m_out[7:0],  "outstanding matches the model");
      check(may_send      === e_may,       "may_send is the MINIMUM of three");
      check(can_send      === (e_may != 0), "can_send tracks may_send");
      check(binding_limit === e_lim[1:0],  "binding_limit names the tightest");
      check(ready_announced === m_ready,   "ready_announced matches the model");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE one that matters: never claim to be able to send more than
      //    the receiver said it has room for. Overrunning a credit is a
      //    receive-buffer overflow, not a retryable protocol error.
      check(may_send <= credits,
            "may_send exceeded the credits the receiver advertised");
      // 2. Nor more than the burst allowance permits.
      check(outstanding + may_send <= burst_size || outstanding >= burst_size,
            "a burst would exceed bMaxBurst + 1");
      // 3. Nor more packets than actually exist.
      check(may_send <= pend, "may_send exceeded the packets pending");
      // 4. burst_size is never zero: a zero burst stalls the endpoint for
      //    ever, and bMaxBurst = 0 legitimately means a burst of ONE.
      check(burst_size !== 8'd0, "burst_size collapsed to zero");
      if (e_lim >= 0 && e_lim <= 3) n_lim[e_lim] = n_lim[e_lim] + 1;
    end
  endtask

  task step(input av, input [7:0] nump, input er, input sd, input lr);
    integer nc, no, e_may, e_bsz, e_room;
    begin
      ack_valid=av; ack_nump=nump; erdy=er; send=sd; link_reset=lr; #1;
      check_comb;

      e_bsz  = (bmb == 255) ? 255 : bmb + 1;
      e_room = (m_out >= e_bsz) ? 0 : (e_bsz - m_out);
      e_may  = min3(m_cred, e_room, pend);

      @(posedge clk); #1;
      // advance the model in the design's precedence order
      if (lr) begin m_cred=0; m_out=0; m_ready=0; end
      else begin
        if (er && !av) m_ready = 1; else if (av) m_ready = 0;
        nc = m_cred; no = m_out;
        if (av) begin nc = (nump > MAXC) ? MAXC : nump; no = 0; end
        if (sd) begin
          if (e_may != 0) begin
            if (!av) begin
              nc = (nc == 0) ? 0 : nc - 1;
              no = no + 1;
            end
            m_sent = m_sent + 1;
          end else m_viol = 1;
        end
        m_cred = nc; m_out = no;
      end
      #1;
      check(packets_sent     === m_sent[31:0], "packets_sent matches the model");
      check(credit_violation === m_viol,       "credit_violation matches the model");
    end
  endtask

  task hard_reset;
    begin
      rst_n=0; ack_valid=0; ack_nump=0; erdy=0; send=0; link_reset=0;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      m_cred=0; m_out=0; m_sent=0; m_ready=0; m_viol=0;
    end
  endtask

  initial begin
    for (i=0;i<4;i=i+1) n_lim[i]=0;
    hard_reset;
    check(credits === 8'd0, "a link starts with no credits");
    check(!can_send, "and can send nothing");

    // ===== A. EXHAUSTIVE over the min-of-three decision =====
    // 16 bMaxBurst values x 16 credit grants x 16 pending counts = 4096
    // points with nothing outstanding: the whole decision surface.
    for (b=0; b<16; b=b+1)
     for (c=0; c<16; c=c+1)
      for (a=0; a<16; a=a+1) begin
        hard_reset;
        bmb = b[7:0]; pend = a[7:0];
        step(1'b1, c[7:0], 1'b0, 1'b0, 1'b0);   // grant c credits
        n_exh = n_exh + 1;
      end
    $display("  exhaustive min-of-three sweep: %0d of %0d points verified",
             n_exh, 16*16*16);

    // ===== B. EXHAUSTIVE over the burst allowance being consumed =====
    // For each bMaxBurst 0..7, send k packets 0..7 and check the room left,
    // with credits held high so the BURST limit is the one under test.
    for (b=0; b<8; b=b+1)
     for (k=0; k<8; k=k+1) begin
       hard_reset;
       bmb = b[7:0]; pend = 8'd15;
       step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);     // plenty of credits
       for (i=0;i<k;i=i+1)
         if (can_send) step(1'b0, 8'd0, 1'b0, 1'b1, 1'b0);
         else          step(1'b0, 8'd0, 1'b0, 1'b0, 1'b0);
       n_burst_exh = n_burst_exh + 1;
     end
    $display("  exhaustive burst-consumption sweep: %0d of %0d verified",
             n_burst_exh, 8*8);

    // ===== C. directed: the off-by-one, and the three limits =====
    hard_reset; bmb = 8'd0; pend = 8'd8;
    step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
    check(burst_size === 8'd1,
          "bMaxBurst = 0 means a burst of ONE packet, not zero");
    check(may_send === 8'd1, "so exactly one packet may go");
    check(binding_limit === L_BURST, "and the BURST is what binds");

    hard_reset; bmb = 8'd15; pend = 8'd8;
    step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
    check(burst_size === 8'd16, "bMaxBurst = 15 means a burst of SIXTEEN");
    check(may_send === 8'd8, "so all eight pending packets may go");
    check(binding_limit === L_NONE, "and nothing binds");

    hard_reset; bmb = 8'd15; pend = 8'd8;
    step(1'b1, 8'd3, 1'b0, 1'b0, 1'b0);
    check(may_send === 8'd3, "three credits caps it at three");
    check(binding_limit === L_CRED, "and the CREDITS are what bind");

    hard_reset; bmb = 8'd15; pend = 8'd0;
    step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
    check(may_send === 8'd0, "nothing pending, nothing to send");
    check(binding_limit === L_PEND, "and that is a DIFFERENT reason");
    check(!flow_blocked, "which is not the same as being blocked");

    // ERDY is not a credit
    hard_reset; bmb = 8'd15; pend = 8'd8;
    step(1'b0, 8'd0, 1'b1, 1'b0, 1'b0);
    check(credits === 8'd0, "an ERDY grants NO credits");
    check(ready_announced, "it announces readiness and nothing else");
    check(!can_send, "so nothing may be sent on the strength of it");
    step(1'b1, 8'd4, 1'b0, 1'b0, 1'b0);
    check(credits === 8'd4, "the credits come from the ACK");
    check(!ready_announced, "and the announcement is retired");

    // a link reset discards the credit state
    hard_reset; bmb = 8'd15; pend = 8'd8;
    step(1'b1, 8'd8, 1'b0, 1'b0, 1'b0);
    check(credits === 8'd8, "credits granted");
    step(1'b0, 8'd0, 1'b0, 1'b0, 1'b1);
    check(credits === 8'd0,
          "a link reset discards them: the receiver's buffer is gone");

    // ===== D. randomised =====
    for (i=0;i<40000;i=i+1) begin
      if (({$random}%64)==0) begin
        hard_reset; bmb={$random}%256; pend={$random}%32;
      end else begin
        if (({$random}%16)==0) pend = {$random}%32;
        step(({$random}%5)==0, {$random}%40, ({$random}%8)==0,
             ({$random}%3)==0, ({$random}%128)==0);
      end
    end

    check(n_lim[0]>0 && n_lim[1]>0 && n_lim[2]>0 && n_lim[3]>0,
          "the run reached ALL FOUR binding-limit cases");

    $display("");
    $display("  REACH: min3=%0d burst=%0d | binding: none=%0d credits=%0d burst=%0d pending=%0d",
             n_exh, n_burst_exh, n_lim[0], n_lim[1], n_lim[2], n_lim[3]);
    $display("  [Verilog] usb3_credit_flow: %0d errors", errors);
    $display("  [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

10.2 The complete SystemVerilog testbench

Same structure, with the enumerated types doing the work that localparams do in the Verilog build — which is what makes a failure message name a state instead of printing a number.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_cf_sv;
  import usb3_flow_pkg::*;
  localparam MAXC = 31;
  reg clk=0, rst_n=0, ack_valid=0, erdy=0, send=0, link_reset=0;
  reg [7:0] ack_nump=0, bmb=0, pend=0;
  wire [7:0] burst_size, credits, outstanding, may_send;
  wire can_send, flow_blocked, credit_violation, ready_announced;
  flow_limit_e binding_limit;
  wire [31:0] blocked_cycles, packets_sent;
  always #5 clk=~clk;

  usb3_credit_flow_sv #(.MAX_CREDITS(MAXC)) dut (
    .clk(clk), .rst_n(rst_n), .ack_valid(ack_valid), .ack_nump(ack_nump),
    .erdy(erdy), .b_max_burst(bmb), .packets_pending(pend), .send(send),
    .link_reset(link_reset), .burst_size(burst_size), .credits(credits),
    .outstanding(outstanding), .may_send(may_send), .can_send(can_send),
    .binding_limit(binding_limit), .ready_announced(ready_announced),
    .flow_blocked(flow_blocked), .blocked_cycles(blocked_cycles),
    .packets_sent(packets_sent), .credit_violation(credit_violation));



  localparam flow_limit_e L_NONE=LIM_NONE, L_CRED=LIM_CREDITS,
                          L_BURST=LIM_BURST, L_PEND=LIM_PENDING;
  integer errors=0, i, a, b, c, k;
  integer n_exh=0, n_burst_exh=0;
  integer n_lim [0:3];
  // ---- INDEPENDENT MODEL: its own credits, outstanding and flags ----
  integer m_cred, m_out, m_sent;
  reg m_ready, m_viol;

  task check(input cond, input [639:0] msg);
    begin if (!cond) begin errors=errors+1;
      if (errors <= 25)
        $display("  FAIL: %0s (bmb=%0d cred=%0d out=%0d pend=%0d | bsz=%0d may=%0d lim=%0d, t=%0t)",
                 msg, bmb, credits, outstanding, pend, burst_size, may_send,
                 binding_limit, $time);
    end end
  endtask

  // The model computes the minimum by sorting three values rather than by
  // two nested comparisons. Same answer, different route.
  function [7:0] min3(input [15:0] x, input [15:0] y, input [15:0] z);
    integer lo;
    begin
      lo = x; if (y < lo) lo = y; if (z < lo) lo = z;
      min3 = lo[7:0];
    end
  endfunction

  task check_comb;
    integer e_bsz, e_room, e_may, e_lim;
    begin
      e_bsz  = (bmb == 255) ? 255 : bmb + 1;
      e_room = (m_out >= e_bsz) ? 0 : (e_bsz - m_out);
      e_may  = min3(m_cred, e_room, pend);
      if (pend == 0)                              e_lim = L_PEND;
      else if (m_cred <= e_room && m_cred < pend) e_lim = L_CRED;
      else if (e_room < pend)                     e_lim = L_BURST;
      else                                        e_lim = L_NONE;

      check(burst_size    === e_bsz[7:0],  "burst_size is bMaxBurst PLUS ONE");
      check(credits       === m_cred[7:0], "credits matches the model");
      check(outstanding   === m_out[7:0],  "outstanding matches the model");
      check(may_send      === e_may,       "may_send is the MINIMUM of three");
      check(can_send      === (e_may != 0), "can_send tracks may_send");
      check(binding_limit === flow_limit_e'(e_lim[1:0]),
            "binding_limit names the tightest");
      check(ready_announced === m_ready,   "ready_announced matches the model");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE one that matters: never claim to be able to send more than
      //    the receiver said it has room for. Overrunning a credit is a
      //    receive-buffer overflow, not a retryable protocol error.
      check(may_send <= credits,
            "may_send exceeded the credits the receiver advertised");
      // 2. Nor more than the burst allowance permits.
      check(outstanding + may_send <= burst_size || outstanding >= burst_size,
            "a burst would exceed bMaxBurst + 1");
      // 3. Nor more packets than actually exist.
      check(may_send <= pend, "may_send exceeded the packets pending");
      // 4. burst_size is never zero: a zero burst stalls the endpoint for
      //    ever, and bMaxBurst = 0 legitimately means a burst of ONE.
      check(burst_size !== 8'd0, "burst_size collapsed to zero");
      if (e_lim >= 0 && e_lim <= 3) n_lim[e_lim] = n_lim[e_lim] + 1;
    end
  endtask

  task step(input av, input [7:0] nump, input er, input sd, input lr);
    integer nc, no, e_may, e_bsz, e_room;
    begin
      ack_valid=av; ack_nump=nump; erdy=er; send=sd; link_reset=lr; #1;
      check_comb;

      e_bsz  = (bmb == 255) ? 255 : bmb + 1;
      e_room = (m_out >= e_bsz) ? 0 : (e_bsz - m_out);
      e_may  = min3(m_cred, e_room, pend);

      @(posedge clk); #1;
      // advance the model in the design's precedence order
      if (lr) begin m_cred=0; m_out=0; m_ready=0; end
      else begin
        if (er && !av) m_ready = 1; else if (av) m_ready = 0;
        nc = m_cred; no = m_out;
        if (av) begin nc = (nump > MAXC) ? MAXC : nump; no = 0; end
        if (sd) begin
          if (e_may != 0) begin
            if (!av) begin
              nc = (nc == 0) ? 0 : nc - 1;
              no = no + 1;
            end
            m_sent = m_sent + 1;
          end else m_viol = 1;
        end
        m_cred = nc; m_out = no;
      end
      #1;
      check(packets_sent     === m_sent[31:0], "packets_sent matches the model");
      check(credit_violation === m_viol,       "credit_violation matches the model");
    end
  endtask

  task hard_reset;
    begin
      rst_n=0; ack_valid=0; ack_nump=0; erdy=0; send=0; link_reset=0;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      m_cred=0; m_out=0; m_sent=0; m_ready=0; m_viol=0;
    end
  endtask

  initial begin
    for (i=0;i<4;i=i+1) n_lim[i]=0;
    hard_reset;
    check(credits === 8'd0, "a link starts with no credits");
    check(!can_send, "and can send nothing");

    // ===== A. EXHAUSTIVE over the min-of-three decision =====
    // 16 bMaxBurst values x 16 credit grants x 16 pending counts = 4096
    // points with nothing outstanding: the whole decision surface.
    for (b=0; b<16; b=b+1)
     for (c=0; c<16; c=c+1)
      for (a=0; a<16; a=a+1) begin
        hard_reset;
        bmb = b[7:0]; pend = a[7:0];
        step(1'b1, c[7:0], 1'b0, 1'b0, 1'b0);   // grant c credits
        n_exh = n_exh + 1;
      end
    $display("  exhaustive min-of-three sweep: %0d of %0d points verified",
             n_exh, 16*16*16);

    // ===== B. EXHAUSTIVE over the burst allowance being consumed =====
    // For each bMaxBurst 0..7, send k packets 0..7 and check the room left,
    // with credits held high so the BURST limit is the one under test.
    for (b=0; b<8; b=b+1)
     for (k=0; k<8; k=k+1) begin
       hard_reset;
       bmb = b[7:0]; pend = 8'd15;
       step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);     // plenty of credits
       for (i=0;i<k;i=i+1)
         if (can_send) step(1'b0, 8'd0, 1'b0, 1'b1, 1'b0);
         else          step(1'b0, 8'd0, 1'b0, 1'b0, 1'b0);
       n_burst_exh = n_burst_exh + 1;
     end
    $display("  exhaustive burst-consumption sweep: %0d of %0d verified",
             n_burst_exh, 8*8);

    // ===== C. directed: the off-by-one, and the three limits =====
    hard_reset; bmb = 8'd0; pend = 8'd8;
    step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
    check(burst_size === 8'd1,
          "bMaxBurst = 0 means a burst of ONE packet, not zero");
    check(may_send === 8'd1, "so exactly one packet may go");
    check(binding_limit === L_BURST, "and the BURST is what binds");

    hard_reset; bmb = 8'd15; pend = 8'd8;
    step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
    check(burst_size === 8'd16, "bMaxBurst = 15 means a burst of SIXTEEN");
    check(may_send === 8'd8, "so all eight pending packets may go");
    check(binding_limit === L_NONE, "and nothing binds");

    hard_reset; bmb = 8'd15; pend = 8'd8;
    step(1'b1, 8'd3, 1'b0, 1'b0, 1'b0);
    check(may_send === 8'd3, "three credits caps it at three");
    check(binding_limit === L_CRED, "and the CREDITS are what bind");

    hard_reset; bmb = 8'd15; pend = 8'd0;
    step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
    check(may_send === 8'd0, "nothing pending, nothing to send");
    check(binding_limit === L_PEND, "and that is a DIFFERENT reason");
    check(!flow_blocked, "which is not the same as being blocked");

    // ERDY is not a credit
    hard_reset; bmb = 8'd15; pend = 8'd8;
    step(1'b0, 8'd0, 1'b1, 1'b0, 1'b0);
    check(credits === 8'd0, "an ERDY grants NO credits");
    check(ready_announced, "it announces readiness and nothing else");
    check(!can_send, "so nothing may be sent on the strength of it");
    step(1'b1, 8'd4, 1'b0, 1'b0, 1'b0);
    check(credits === 8'd4, "the credits come from the ACK");
    check(!ready_announced, "and the announcement is retired");

    // a link reset discards the credit state
    hard_reset; bmb = 8'd15; pend = 8'd8;
    step(1'b1, 8'd8, 1'b0, 1'b0, 1'b0);
    check(credits === 8'd8, "credits granted");
    step(1'b0, 8'd0, 1'b0, 1'b0, 1'b1);
    check(credits === 8'd0,
          "a link reset discards them: the receiver's buffer is gone");

    // ===== D. randomised =====
    for (i=0;i<40000;i=i+1) begin
      if (({$random}%64)==0) begin
        hard_reset; bmb={$random}%256; pend={$random}%32;
      end else begin
        if (({$random}%16)==0) pend = {$random}%32;
        step(({$random}%5)==0, {$random}%40, ({$random}%8)==0,
             ({$random}%3)==0, ({$random}%128)==0);
      end
    end

    check(n_lim[0]>0 && n_lim[1]>0 && n_lim[2]>0 && n_lim[3]>0,
          "the run reached ALL FOUR binding-limit cases");

    $display("");
    $display("  REACH: min3=%0d burst=%0d | binding: none=%0d credits=%0d burst=%0d pending=%0d",
             n_exh, n_burst_exh, n_lim[0], n_lim[1], n_lim[2], n_lim[3]);
    $display("  [SystemVerilog] usb3_credit_flow_sv: %0d errors", errors);
    $display("  [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

10.3 The complete VHDL testbench

VHDL-2008 requires a shared variable to have a protected type, so all the bookkeeping lives in process variables inside the single stimulus process. The randomisation uses ieee.math_real.uniform, which is a genuinely different generator from either Verilog builtin — see Chapter 20.5 §9.2 for why that distinction turned out to matter across this whole module.

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.usb3_flow_pkg.all;

entity tb_cf_vhdl is end entity;

architecture sim of tb_cf_vhdl is
  constant MAXC : positive := 31;

  signal clk, rst_n : std_logic := '0';
  signal ack_valid, erdy, send, link_reset : std_logic := '0';
  signal ack_nump, bmb, pend : unsigned(7 downto 0) := (others => '0');
  signal burst_size, credits, outstanding, may_send : unsigned(7 downto 0);
  signal can_send, flow_blocked, credit_violation, ready_announced
       : std_logic;
  signal binding_limit : flow_limit_t;
  signal blocked_cycles, packets_sent : unsigned(31 downto 0);
  signal done : boolean := false;
begin
  clk <= not clk after 5 ns when not done else '0';

  dut : entity work.usb3_credit_flow_vhdl
    generic map (MAX_CREDITS => MAXC)
    port map (clk, rst_n, ack_valid, ack_nump, erdy, bmb, pend, send,
              link_reset, burst_size, credits, outstanding, may_send,
              can_send, binding_limit, ready_announced, flow_blocked,
              blocked_cycles, packets_sent, credit_violation);

  stim : process
    variable errors : natural := 0;
    variable n_exh, n_burst_exh : natural := 0;
    type lim_count_t is array (flow_limit_t) of natural;
    variable n_lim : lim_count_t := (others => 0);
    -- INDEPENDENT MODEL: its own credits, outstanding and flags.
    variable m_cred, m_out : natural := 0;
    variable m_sent : natural := 0;
    variable m_ready, m_viol : boolean := false;
    variable seed1 : positive := 83; variable seed2 : positive := 29;
    variable r1, r2, r3, r4, r5, r6 : real;

    function sl (b : boolean) return std_logic is
    begin
      if b then return '1'; else return '0'; end if;
    end function;

    procedure chk (c : boolean; m : string) is
    begin
      if not c then
        errors := errors + 1;
        if errors <= 25 then report "FAIL: " & m severity warning; end if;
      end if;
    end procedure;

    -- The model computes the minimum by sorting three values rather than by
    -- two nested comparisons. Same answer, different route.
    function min3 (x, y, z : natural) return natural is
      variable lo : natural := x;
    begin
      if y < lo then lo := y; end if;
      if z < lo then lo := z; end if;
      return lo;
    end function;

    procedure check_comb is
      variable e_bsz, e_room, e_may : natural;
      variable e_lim : flow_limit_t;
    begin
      if bmb = to_unsigned(255, 8) then e_bsz := 255;
      else e_bsz := to_integer(bmb) + 1; end if;
      if m_out >= e_bsz then e_room := 0; else e_room := e_bsz - m_out; end if;
      e_may := min3(m_cred, e_room, to_integer(pend));

      if pend = to_unsigned(0, 8) then e_lim := LIM_PENDING;
      elsif m_cred <= e_room and m_cred < to_integer(pend) then
        e_lim := LIM_CREDITS;
      elsif e_room < to_integer(pend) then e_lim := LIM_BURST;
      else e_lim := LIM_NONE; end if;

      chk(burst_size = to_unsigned(e_bsz, 8),
          "burst_size is bMaxBurst PLUS ONE");
      chk(credits = to_unsigned(m_cred, 8), "credits matches the model");
      chk(outstanding = to_unsigned(m_out, 8),
          "outstanding matches the model");
      chk(may_send = to_unsigned(e_may, 8),
          "may_send is the MINIMUM of three");
      chk((can_send = '1') = (e_may /= 0), "can_send tracks may_send");
      chk(binding_limit = e_lim, "binding_limit names the tightest");
      chk((ready_announced = '1') = m_ready,
          "ready_announced matches the model");

      -- ---- SAFETY PROPERTIES, independent of the model ----
      -- 1. THE one that matters: never claim to be able to send more than
      --    the receiver said it has room for.
      chk(may_send <= credits,
          "may_send exceeded the credits the receiver advertised");
      -- 2. Nor more than the burst allowance permits.
      chk(outstanding >= burst_size
          or (outstanding + may_send) <= burst_size,
          "a burst would exceed bMaxBurst + 1");
      -- 3. Nor more packets than actually exist.
      chk(may_send <= pend, "may_send exceeded the packets pending");
      -- 4. burst_size is never zero.
      chk(burst_size /= to_unsigned(0, 8), "burst_size collapsed to zero");
      n_lim(e_lim) := n_lim(e_lim) + 1;
    end procedure;

    procedure step (av : std_logic; nump : natural; er, sd, lr : std_logic) is
      variable nc, no, e_bsz, e_room, e_may : natural;
    begin
      ack_valid <= av; ack_nump <= to_unsigned(nump, 8);
      erdy <= er; send <= sd; link_reset <= lr;
      wait for 1 ns;
      check_comb;

      if bmb = to_unsigned(255, 8) then e_bsz := 255;
      else e_bsz := to_integer(bmb) + 1; end if;
      if m_out >= e_bsz then e_room := 0; else e_room := e_bsz - m_out; end if;
      e_may := min3(m_cred, e_room, to_integer(pend));

      wait until rising_edge(clk); wait for 1 ns;
      if lr = '1' then
        m_cred := 0; m_out := 0; m_ready := false;
      else
        if er = '1' and av = '0' then m_ready := true;
        elsif av = '1' then m_ready := false; end if;
        nc := m_cred; no := m_out;
        if av = '1' then
          if nump > MAXC then nc := MAXC; else nc := nump; end if;
          no := 0;
        end if;
        if sd = '1' then
          if e_may /= 0 then
            if av = '0' then
              if nc /= 0 then nc := nc - 1; end if;
              no := no + 1;
            end if;
            m_sent := m_sent + 1;
          else
            m_viol := true;
          end if;
        end if;
        m_cred := nc; m_out := no;
      end if;
      wait for 1 ns;
      chk(packets_sent = to_unsigned(m_sent, 32),
          "packets_sent matches the model");
      chk((credit_violation = '1') = m_viol,
          "credit_violation matches the model");
    end procedure;

    procedure hard_reset is
    begin
      rst_n <= '0'; ack_valid <= '0'; ack_nump <= (others => '0');
      erdy <= '0'; send <= '0'; link_reset <= '0';
      wait until rising_edge(clk); wait for 1 ns;
      wait until rising_edge(clk); wait for 1 ns;
      rst_n <= '1'; wait for 1 ns;
      m_cred := 0; m_out := 0; m_sent := 0;
      m_ready := false; m_viol := false;
    end procedure;
  begin
    hard_reset;
    chk(credits = to_unsigned(0, 8), "a link starts with no credits");
    chk(can_send = '0', "and can send nothing");

    -- ===== A. EXHAUSTIVE over the min-of-three decision =====
    -- 16 bMaxBurst values x 16 credit grants x 16 pending counts = 4096.
    for b in 0 to 15 loop
     for c in 0 to 15 loop
      for a in 0 to 15 loop
        hard_reset;
        bmb <= to_unsigned(b, 8); pend <= to_unsigned(a, 8);
        wait for 1 ns;
        step('1', c, '0', '0', '0');
        n_exh := n_exh + 1;
      end loop;
     end loop;
    end loop;
    report "exhaustive min-of-three sweep: " & integer'image(n_exh)
         & " of 4096 points verified";

    -- ===== B. EXHAUSTIVE over the burst allowance being consumed =====
    for b in 0 to 7 loop
     for k in 0 to 7 loop
       hard_reset;
       bmb <= to_unsigned(b, 8); pend <= to_unsigned(15, 8);
       wait for 1 ns;
       step('1', 31, '0', '0', '0');
       for i in 1 to k loop
         if can_send = '1' then step('0', 0, '0', '1', '0');
         else step('0', 0, '0', '0', '0'); end if;
       end loop;
       n_burst_exh := n_burst_exh + 1;
     end loop;
    end loop;
    report "exhaustive burst-consumption sweep: "
         & integer'image(n_burst_exh) & " of 64 verified";

    -- ===== C. directed: the off-by-one, and the three limits =====
    hard_reset; bmb <= to_unsigned(0, 8); pend <= to_unsigned(8, 8);
    wait for 1 ns; step('1', 31, '0', '0', '0');
    chk(burst_size = to_unsigned(1, 8),
        "bMaxBurst = 0 means a burst of ONE packet, not zero");
    chk(may_send = to_unsigned(1, 8), "so exactly one packet may go");
    chk(binding_limit = LIM_BURST, "and the BURST is what binds");

    hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(8, 8);
    wait for 1 ns; step('1', 31, '0', '0', '0');
    chk(burst_size = to_unsigned(16, 8),
        "bMaxBurst = 15 means a burst of SIXTEEN");
    chk(may_send = to_unsigned(8, 8),
        "so all eight pending packets may go");
    chk(binding_limit = LIM_NONE, "and nothing binds");

    hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(8, 8);
    wait for 1 ns; step('1', 3, '0', '0', '0');
    chk(may_send = to_unsigned(3, 8), "three credits caps it at three");
    chk(binding_limit = LIM_CREDITS, "and the CREDITS are what bind");

    hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(0, 8);
    wait for 1 ns; step('1', 31, '0', '0', '0');
    chk(may_send = to_unsigned(0, 8), "nothing pending, nothing to send");
    chk(binding_limit = LIM_PENDING, "and that is a DIFFERENT reason");
    chk(flow_blocked = '0', "which is not the same as being blocked");

    hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(8, 8);
    wait for 1 ns; step('0', 0, '1', '0', '0');
    chk(credits = to_unsigned(0, 8), "an ERDY grants NO credits");
    chk(ready_announced = '1', "it announces readiness and nothing else");
    chk(can_send = '0', "so nothing may be sent on the strength of it");
    step('1', 4, '0', '0', '0');
    chk(credits = to_unsigned(4, 8), "the credits come from the ACK");
    chk(ready_announced = '0', "and the announcement is retired");

    hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(8, 8);
    wait for 1 ns; step('1', 8, '0', '0', '0');
    chk(credits = to_unsigned(8, 8), "credits granted");
    step('0', 0, '0', '0', '1');
    chk(credits = to_unsigned(0, 8),
        "a link reset discards them: the receiver's buffer is gone");

    -- ===== D. randomised =====
    for i in 1 to 40000 loop
      uniform(seed1, seed2, r1);
      if r1 < 0.015625 then
        hard_reset;
        uniform(seed1, seed2, r2); uniform(seed1, seed2, r3);
        bmb  <= to_unsigned(integer(floor(r2*256.0)), 8);
        pend <= to_unsigned(integer(floor(r3*32.0)), 8);
        wait for 1 ns;
      else
        uniform(seed1, seed2, r2); uniform(seed1, seed2, r3);
        uniform(seed1, seed2, r4); uniform(seed1, seed2, r5);
        uniform(seed1, seed2, r6);
        if r6 < 0.0625 then
          pend <= to_unsigned(integer(floor(r6*32.0*16.0)) mod 32, 8);
          wait for 1 ns;
        end if;
        step(sl(r2 < 0.2), integer(floor(r3*40.0)), sl(r4 < 0.125),
             sl(r5 < 0.3333), sl(r6 < 0.0078125));
      end if;
    end loop;

    chk(n_lim(LIM_NONE) > 0 and n_lim(LIM_CREDITS) > 0
        and n_lim(LIM_BURST) > 0 and n_lim(LIM_PENDING) > 0,
        "the run reached ALL FOUR binding-limit cases");

    report "REACH: min3=" & integer'image(n_exh)
         & " burst=" & integer'image(n_burst_exh)
         & " | binding: none=" & integer'image(n_lim(LIM_NONE))
         & " credits=" & integer'image(n_lim(LIM_CREDITS))
         & " burst=" & integer'image(n_lim(LIM_BURST))
         & " pending=" & integer'image(n_lim(LIM_PENDING));
    report "[VHDL] usb3_credit_flow_vhdl: " & integer'image(errors)
         & " errors";
    if errors = 0 then report "[VHDL] PASS";
    else report "[VHDL] FAIL" severity error; end if;
    done <= true;
    wait;
  end process;
end architecture;

Run it with:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nvc --std=2008 -a cf_vhdl.vhd cf_vhdl_tb.vhd
nvc --std=2008 -e tb_cf_vhdl
nvc --std=2008 -r tb_cf_vhdl

11. Mutation Testing — Across All Three Languages

MutationVerilogSystemVerilogVHDL
C1bMaxBurst treated as the burst size (no +1)460384603847050
C2the burst limit ignored — min(credits, pending)376537654158
C3the data limit ignored — min(credits, burst)417234172341377
C4ERDY grants credits384573845743987
C5a link reset preserves the credit state147941479413177
C6sending does not add to the outstanding count202672026720625
C7the binding-limit priority is swapped265726572783

C2 scores lowest of the three min-of-three mutations at 3765, and the reason is reachability rather than importance: it differs from the correct design only when the burst is the tightest limit, which §10 measured at 1608 of the exhaustive points. C3 scores 41 723 because "more data than credits" is the common case.

C7 changes no transfer at all — may_send is identical — and still dies 2657 times, because binding_limit is checked. A mutation that only affects a diagnostic output is only detectable if the diagnostic is checked, which is the argument for §3's output existing.

12. The Mutation VHDL's Semantics Made Equivalent

The first run of this matrix had C4 surviving outright in VHDL:

MutationVerilogSystemVerilogVHDL
C4 — ERDY grants credits38457384570

Zero errors, against a design the other two languages proved the mutation breaks.

The VHDL mutation had been written against the signal:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
        if erdy = '1' and ack_valid = '0' then
          rdy_r <= '1'; cred_r <= to_unsigned(MAX_CREDITS, 8);   -- MUT C4
        elsif ack_valid = '1' then rdy_r <= '0'; end if;

— and the process assigns cred_r again, unconditionally, forty lines later:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
        cred_r <= nc; out_r <= no;

In VHDL the last signal assignment in a process wins. The mutation's write was overwritten before the process ended, so C4 was not a weak mutation — it was genuinely equivalent, and no amount of extra stimulus would have killed it.

The fix was to retarget the mutation at the variable that actually reaches the signal:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 # C4 must target the VARIABLE nc, not the signal cred_r: the process
 # assigns `cred_r <= nc` unconditionally at its end, and VHDL's
 # last-assignment-wins would silently override a mutation written against
 # the signal -- making it equivalent rather than merely different.

C4 in VHDL went from 0 to 43 987.

13. A UVM Environment for Flow Control

Credit flow is a protocol between two agents, which is what makes it a natural UVM target rather than a directed-test one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The receiver is an agent, not a stimulus file. Its credit advertisements
// are a policy, and different policies expose different bugs: a receiver
// that always advertises its maximum never exercises the credit limit at
// all, which is section 10's LIM_CREDITS bin reading zero.
class receiver_agent extends uvm_agent;
  `uvm_component_utils(receiver_agent)
  rand int unsigned buffer_depth;
  rand int unsigned drain_rate;
  constraint c_realistic {
    buffer_depth inside {[1:31]};
    drain_rate   inside {[0:4]};
  }
endclass

class credit_item extends uvm_sequence_item;
  `uvm_object_utils(credit_item)
  rand bit [7:0] nump;
  rand bit       ack, erdy, send, link_reset;

  // A receiver advertising zero is the interesting case and the one a
  // naive generator produces least often.
  constraint c_starve { nump dist { 0 := 3, [1:3] := 5, [4:31] := 4 }; }
  constraint c_rare   { link_reset dist { 1 := 1, 0 := 200 }; }
endclass

// The adversarial sequence: offer a send in every state, including the
// states where there is nothing to spend it on. A well-behaved layer above
// would never do this -- which is exactly why the DUT must not rely on it.
class overrun_attack_seq extends uvm_sequence #(credit_item);
  `uvm_object_utils(overrun_attack_seq)
  task body();
    credit_item it;
    `uvm_do_with(it, { ack == 1; nump == 0; })        // no room at all
    repeat (8)
      `uvm_do_with(it, { send == 1; ack == 0; })      // try anyway
  endtask
endclass

class flow_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(flow_scoreboard)
  local int unsigned m_credits, m_outstanding;

  function void write(flow_txn t);
    int unsigned burst_size = t.b_max_burst + 1;      // SECTION 4
    int unsigned room = (m_outstanding >= burst_size)
                      ? 0 : burst_size - m_outstanding;

    // THE property. Not "may_send matched" -- an inequality, because its
    // violation is a receive-buffer overflow in real silicon and there is
    // no expected value that makes overrunning a credit acceptable.
    if (t.may_send > m_credits)
      `uvm_fatal("FLOW/OVERRUN", $sformatf(
        "claimed %0d packets against %0d credits", t.may_send, m_credits))
    if (t.may_send > room)
      `uvm_error("FLOW/BURST", "a burst would exceed bMaxBurst + 1")
    if (t.may_send > t.packets_pending)
      `uvm_error("FLOW/PHANTOM", "claimed more packets than exist")

    // ERDY grants nothing. Section 2, and mutation C4.
    if (t.erdy && !t.ack && t.credits != m_credits)
      `uvm_error("FLOW/ERDY", "an ERDY changed the credit count")

    if (t.ack) begin m_credits = t.nump; m_outstanding = 0; end
    else if (t.send && t.may_send > 0) begin
      m_credits--; m_outstanding++;
    end
  endfunction
endclass

covergroup flow_cg with function sample(
    flow_limit_e lim, int unsigned burst_size, bit erdy_no_ack);
  // ALL FOUR must be reached. A run missing LIM_BURST has not tested
  // section 4's off-by-one however many credits it varied.
  cp_limit : coverpoint lim {
    bins none = {LIM_NONE}; bins credits = {LIM_CREDITS};
    bins burst = {LIM_BURST}; bins pending = {LIM_PENDING};
  }
  // bMaxBurst = 0 is the value the off-by-one destroys, and it is the
  // most common value in real descriptors.
  cp_burst1 : coverpoint burst_size { bins single = {1}; bins multi = {[2:$]}; }
  // An ERDY with no ACK behind it -- the case mutation C4 corrupts.
  cp_erdy : coverpoint erdy_no_ack { bins announced = {1}; }
  x_limit_burst : cross cp_limit, cp_burst1;
endgroup

14. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // THE property: never claim more than the receiver has room for.
  property p_within_credits;
    @(posedge clk) disable iff (!rst_n)
      may_send <= credits;
  endproperty
  a_within_credits : assert property (p_within_credits)
    else $fatal(1, "may_send exceeded the advertised credits");

  // Nor more than the burst allowance. Mutation C2.
  property p_within_burst;
    @(posedge clk) disable iff (!rst_n)
      (outstanding < burst_size) |-> (outstanding + may_send <= burst_size);
  endproperty
  a_within_burst : assert property (p_within_burst);

  // Nor more packets than exist. Mutation C3.
  property p_within_pending;
    @(posedge clk) disable iff (!rst_n)
      may_send <= packets_pending;
  endproperty
  a_within_pending : assert property (p_within_pending);

  // An ERDY with no ACK changes no credits. Mutation C4, stated as the
  // absence of an effect rather than the presence of one.
  property p_erdy_grants_nothing;
    @(posedge clk) disable iff (!rst_n)
      (erdy && !ack_valid && !link_reset) |=> $stable(credits) || $past(send);
  endproperty
  a_erdy_grants_nothing : assert property (p_erdy_grants_nothing)
    else $error("an ERDY changed the credit count");

  // burst_size is never zero. Mutation C1's consequence.
  property p_burst_never_zero;
    @(posedge clk) disable iff (!rst_n) burst_size != '0;
  endproperty
  a_burst_never_zero : assert property (p_burst_never_zero);

The first three are the three limits as three separate assertions, deliberately not combined into one. A single may_send == min(a,b,c) would pass a design that got the minimum right by accident from two wrong inputs; three inequalities each fail independently.

These were written but not simulated; Icarus supports no concurrent assertions.

The report: a SuperSpeed device transfers at roughly a third of the rate the same silicon achieves on a reference host. No errors, no retries, no resets — it is simply slower.

The procedure:

1. Establish which limit is binding, and recognise that this is the question. Throughput alone cannot distinguish a small receive buffer from a small burst from an endpoint with nothing to say — and those have no fixes in common.

2. Read bMaxBurst from the endpoint companion descriptor, and add one (§4). A device declaring 0 gets bursts of one packet, which on a 5 Gb/s link with per-packet overhead is roughly the throughput being reported.

3. Check whether the host is advertising credits at all. A receiver that always returns NumP = 1 serialises the link no matter how large the burst allowance is — the sender may never have more than one packet outstanding.

4. Distinguish "blocked" from "idle". An endpoint reporting LIM_PENDING is working correctly and has nothing to send; the bottleneck is upstream of USB entirely. This is the case most often misdiagnosed, because the throughput graph looks identical.

5. Look for ERDY storms. A device that repeatedly announces readiness and is not scheduled is telling you its ERDYs are not arriving, or that the host is not acting on them. ready_announced staying high is that condition, and it is invisible from a bandwidth measurement.

16. Common Misconceptions

"USB 3 devices can initiate transfers." They cannot (§1) — 19.5's wakeup remains the only exception. They may announce readiness, which is different.

"ERDY means the device is sending." It means resume scheduling me (§2). The permission comes from credits in an ACK. Mutation C4, 38 457 errors.

"bMaxBurst is the burst size." It is the burst size minus one (§4) — 0 means one packet. Mutation C1, 46 038 errors.

"Credits are the only limit." Three limits, smallest wins (§3). Mutations C2 and C3.

"Credits survive a link reset." They describe a buffer that has just been reset (§6). Mutation C5, 14 794 errors.

"Knowing the throughput tells you what to fix." It tells you nothing — the three limits produce identical throughput graphs and have no fixes in common (§3, §15).

"A mutation that changes no data transfer is not worth having." C7 changes may_send not at all and dies 2657 times, because the diagnostic it corrupts is checked (§11).

17. Exercises

1. §12 found a mutation VHDL's last-assignment-wins made equivalent. Audit the remaining VHDL designs in Modules 18–20 for signals assigned more than once in one process, and say which mutations against them would be neutralised.

2. An endpoint declares bMaxBurst = 15 and the receiver advertises NumP = 2. Compute the maximum sustained throughput as a fraction of the burst-limited rate, and say which limit binding_limit reports.

3. Write the SVA property that catches C6 — sending without incrementing outstanding — without referring to outstanding.

4. §3 argues the three limits must not be conflated. Construct a workload for which min(credits, pending) and the correct min-of-three agree on every cycle, and say what that implies about C2's count.

5. usb_ss_max_streams reads five bits of bmAttributes and returns 1 << n. Determine the largest number of streams an endpoint can declare, and what happens to this design if streams are added.

6. A device sends ERDY and the host never returns. Determine what the design reports, what a USB 2 device would have done in the same situation, and which is easier to diagnose.

18. Summary

USB 3 kept the single master and deleted the polling (§1). A USB 2 endpoint with nothing to say costs a transaction per poll; a USB 3 endpoint costs nothing until it announces readiness.

ERDY is not a credit (§2). It says resume asking me; the permission to transmit is NumP, carried in an ACK. Mutation C4, 38 457 errors.

Three limits bound a sender and the smallest wins (§3): credits, burst size, and data on hand. Conflating any two works until the third becomes binding — C2 at 3765 and C3 at 41 723 — and which limit binds is the more useful output, because the three have no fixes in common.

bMaxBurst is burst size minus one (§4). Zero means one packet, and a design reading it literally stalls every endpoint that declared zero. Mutation C1, 46 038 errors.

All three HDL implementations were simulated (§19) and seven mutations died in all three (§11), with the decision verified exhaustively over 4096 points plus 64 burst-consumption walks (§10), reaching all four binding cases thousands of times each.

And C4 survived outright in VHDL on the first run (§12) — zero errors against 38 457 elsewhere. The mutation had been written against a signal that the process assigns again, unconditionally, later; VHDL's last-assignment-wins overwrote it, making it genuinely equivalent rather than weak. Retargeting it at the variable that actually reaches the signal took it to 43 987.

That is the fourth distinct way in three modules that a mutation has failed to mean the same thing in all three languages — after a duplicate, a ternary with identical branches, and a duplicated design condition. The detector has been the same every time: a column badly out of line with the other two.

19. Tooling, Honestly

LanguageDesignTestbenchAnalysed / compiledSimulatedMutations
Verilog-2005usb3_credit_flowcf_v_tb.v✅ Icarus -g2005✅ 0 errors, 4096 + 64✅ all seven
SystemVerilogusb3_credit_flow_svcf_sv_tb.sv✅ Icarus -g2012✅ 0 errors, 4096 + 64✅ all seven
VHDL-2008usb3_credit_flow_vhdlcf_vhdl_tb.vhd✅ nvc 1.23.0✅ 0 errors, 4096 + 64✅ all seven
UVM (§13)——❌ no UVM-capable simulator here❌—
SVA (§14)——❌ unsupported by Icarus❌—

binding_limit was added to all three designs at once, not to SystemVerilog and VHDL first. Chapters 19.2 §11 and 19.3 §13 each cost a round of re-measurement to discover that one design exposing less makes the comparison measure the benches instead.

VHDL's randomised tail differs (pending reached 1457 times against 1120) because the three benches draw from different generators. The 4096 exhaustive points and 64 burst walks are identical by construction.

20. What Comes Next

This chapter described a link without saying anything about the wires it runs on — and those are not the wires Modules 1–19 have been about.

Chapter 20.2 — Dual-Bus Architecture is about what is physically in a USB 3 cable, which is two complete buses: the USB 2 D+/D− pair, unchanged and still present, and two additional differential pairs carrying SuperSpeed in each direction.

They coexist physically and barely interact logically, and the rule that connects them is sharper than it looks: a device operates at SuperSpeed or at USB 2, never both — so the interesting hardware is not either bus, it is the decision between them, and what happens when SuperSpeed training fails.

Browse the full path on the USB tutorials index.

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.