Skip to content
VLSI Mentor

USB · Module 27

The Transfer-Types Question

Guaranteed bandwidth and guaranteed delivery are the same resource spent two different ways — isochronous has no retries because every slot is already promised, not because somebody skimped.

Chapter 27.4 established what an endpoint is. This chapter is about what it is for, and it contains the single most commonly inverted statement in USB interviews.

1. The Question

"You are designing a device that streams audio. Which transfer type, and why?"

The type is the easy half. Everyone says isochronous. The why is where the answer is won or lost, and the usual attempt goes:

"Isochronous, because it is fast. The downside is that it is unreliable — there are no retries."

That is two claims and both are wrong. Isochronous is not particularly fast; bulk is faster on an idle bus. And the lack of retries is not a downside of the guarantee — it is the guarantee, seen from the other side.

2. The Four Types, Derived Rather Than Listed

Start from the schedule. The host has a 1 ms frame, and it must divide it. Everything else follows from two decisions: does this endpoint get a reservation, and is it allowed to retry.

ReservationRetriesThereforeUsed for
Isochronousyes, every framenobounded latency, lossyaudio, video
Interruptyes, every N framesyesbounded latency, reliable, smallkeyboards, mice
Controlits own 10%yesalways availableenumeration, configuration
Bulknoneyesreliable, unbounded latencydisks, printers, networks

Interrupt looks like a contradiction — reserved and retryable — and it is not. It buys the retry by being small: an interrupt endpoint is capped at 64 bytes at full speed, and its reservation is per N frames rather than per frame. A small reservation leaves room in the frame for the retry to land in. That is the whole trick, and it is why "interrupt" cannot be used for bulk data no matter how much you want its latency.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   The one question that generates the table:

     reservation?  --no-->  BULK      (retry freely, no deadline)
          |
         yes
          |
     big enough to fill a frame?
          |                    |
         yes                   no
          |                    |
     ISOCHRONOUS           INTERRUPT
     (no room to retry)    (small enough that a retry fits)

   CONTROL is the special case: its own separate 10%, so that a
   full periodic schedule can never stop the host talking.

3. Why Control Has Its Own Reservation

This is the detail that separates a good answer from a complete one.

Control could have been given the periodic remainder — whatever is left after isochronous and interrupt. It is not. It gets its own 10% minimum, and the periodic types are capped at 90%, as two separate rules.

The reason is a deadlock that would otherwise be reachable: if control shared the periodic budget, a fully committed schedule would leave no room for control transfers — and control transfers are how the host changes the schedule. A device could not be configured, an endpoint could not be un-halted, and nothing could be removed to make room. The bus would be full and unable to be emptied.

4. What We Are Building

usb_xfer_admit is the host's admission control: the block that decides whether a new endpoint can be accepted into the schedule. It is where the trade stops being a discussion and becomes arithmetic.

One frame, three budgets, and what each buys

A USB frame divided into a 90 percent periodic budget, a separate 10 percent control reservation, and bulk traffic which has no reservation and uses the remainder1 ms framePeriodicControlIsochronousInterruptBulk90% cap10% floorevery frameevery N framesthe remainder12
Periodic traffic is capped at 90%; control holds its own 10%; bulk is charged nothing and uses whatever is left. Bulk can always be admitted — and can always be delayed.

The budget filling, and the refusal that follows

A first isochronous endpoint is admitted, a second is refused for bandwidth, a small interrupt endpoint fits in the remainder, and a bulk endpoint is admitted without being charged any periodic bandwidthperiodic budget fillsperiodic budget fillsremainder, then bulkremainder, then bulkadmitted: 1036 of 1350admitted: 1036 of 1350refused: no bandwidthrefused: no bandwidth8-byte interrupt fits8-byte interrupt fitsbulk: charged nothingbulk: charged nothingclkreq_validreq_typeISOISOISOISOINTINTBULKBULKBULKreq_maxp102310231023102388646464adm_okadm_reason0OKOKBWBWOKOKOKOKused_per010361036103610361057105710571057t0t1t2t3t4t5t6t7t8
Two 1023-byte isochronous endpoints do not fit in 1350 bytes. The second is refused for bandwidth; a small interrupt endpoint then fits in the remainder, and bulk is admitted regardless.

used_per never moves when the bulk endpoint is admitted. That is the arithmetic statement of "bulk has no reservation", and it is mutation E1.

5. Two Details That Are Easy to Get Wrong

The cost of a transaction is not its payload. A token, a data PID, a CRC and an inter-packet gap ride along with every transaction — conventionally 13 bytes at full speed. An 8-byte interrupt endpoint costs 21, not 8, and for small endpoints the overhead dominates. A budget that charges the payload alone is optimistic by 13 bytes per endpoint, admits one endpoint too many, and the schedule then misses deadlines it was told it would meet. That is mutation E4.

Charge the worst frame, not the average. An endpoint with maxp = 64 and an interval of 8 frames moves 64 bytes every eighth frame — 8 bytes per frame on average. Charging the average is wrong, because the schedule has to survive the frame in which the transaction actually happens. Charge the full 77.

6. Seven Properties

#Property
1The periodic commitment never exceeds 90% of the frame.
2The control commitment never exceeds its own reservation.
3Bulk is charged nothing and can always be admitted.
4A packet larger than the type allows is refused, with the reason.
5A periodic interval that is not a power of two is refused.
6A release returns exactly what was charged.
7A release and a request in the same cycle both succeed.

Property 7 is the one nobody writes down. Section 12 is about why it matters and why the obvious implementation gets it wrong.

7. Verilog-2005 RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_xfer_admit -- "Which transfer type, and why?" answered in hardware.
//
//  The four types are easy to list and the trade is easy to state
//  backwards. What candidates say: "isochronous is fast but unreliable."
//  What is actually true:
//
//      Guaranteed bandwidth and guaranteed delivery are the SAME
//      resource spent two different ways.
//
//  An isochronous endpoint is promised a slot in every frame. A retry
//  needs a slot. Every slot is already allocated -- that is what the
//  promise means -- so there is nowhere to put the retry. Isochronous
//  is not unreliable because somebody skimped on error handling; it is
//  unreliable BECAUSE it is guaranteed.
//
//  Bulk is the mirror image: no reservation at all, therefore retries
//  are free, therefore delivery is certain and timing is not.
//
//  This module is the host's admission control: it decides whether a new
//  endpoint can be accepted into the schedule, and it is the place where
//  the trade becomes arithmetic.
// =====================================================================
module usb_xfer_admit #(
  // A USB 2.0 full-speed frame is 1500 bytes at 12 Mbit/s. The
  // specification reserves at most 90% for periodic traffic and at least
  // 10% for control, and the two limits are separate rules.
  parameter integer FRAME_BYTES   = 1500,
  parameter integer PERIODIC_MAX  = 1350,   // 90% of the frame
  parameter integer CONTROL_MIN   = 150,    // 10%, reserved for control
  parameter integer N_EP          = 8
) (
  input  wire        clk,
  input  wire        rst_n,

  // ---- a request to admit an endpoint into the schedule ----
  input  wire        req_valid,
  input  wire [1:0]  req_type,     // X_CTRL / X_ISO / X_INT / X_BULK
  input  wire [10:0] req_maxp,     // bytes per transaction
  input  wire [7:0]  req_interval, // frames between polls (periodic only)

  // ---- withdraw a previously admitted endpoint ----
  input  wire        rel_valid,
  input  wire [3:0]  rel_slot,

  // ---- the decision, one cycle later ----
  output wire        adm_done,
  output wire        adm_ok,
  output wire [2:0]  adm_reason,
  output wire [3:0]  adm_slot,

  // ---- the schedule as it stands ----
  output wire [15:0] used_periodic,
  output wire [15:0] used_control,
  output wire [15:0] n_admitted,

  // ---- observability ----
  output wire [31:0] n_req,
  output wire [31:0] n_reject_bw,
  output wire [31:0] n_reject_interval,
  output wire [31:0] n_reject_maxp,
  output wire [31:0] n_reject_full,
  output wire [31:0] n_overcommit     // must always read 0
);

  localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;

  localparam [2:0] R_OK        = 3'd0,
                   R_BW        = 3'd1,   // no periodic bandwidth left
                   R_INTERVAL  = 3'd2,   // interval not a legal value
                   R_MAXP      = 3'd3,   // packet too large for the type
                   R_FULL      = 3'd4,   // no free slot
                   R_CTRL_ROOM = 3'd5;   // would eat the control reservation

  // ---- the largest packet each type may use at full speed ----
  //
  // These are not stylistic limits. Isochronous gets the biggest packet
  // because it is the type that cannot retry, so a lost packet must cost
  // as little as possible in transactions -- one big one beats eight small
  // ones when none of them can be repeated.
  function [10:0] max_packet;
    input [1:0] t;
    begin
      case (t)
        X_CTRL: max_packet = 11'd64;
        X_ISO:  max_packet = 11'd1023;
        X_INT:  max_packet = 11'd64;
        default: max_packet = 11'd64;      // bulk
      endcase
    end
  endfunction

  // ---- a periodic endpoint's cost, per frame ----
  //
  // maxp bytes every `interval` frames is maxp/interval bytes per frame on
  // average -- but the schedule must survive the WORST frame, not the
  // average one, so the cost charged is the full maxp. Charging the average
  // is how a schedule that passes admission control then misses deadlines.
  function [15:0] frame_cost;
    input [10:0] mp;
    begin
      // Bytes on the wire are more than the payload: token, data PID, CRC
      // and inter-packet gap. 13 bytes of overhead per transaction at full
      // speed is the conventional figure and it is not negligible for small
      // packets -- an 8-byte interrupt endpoint costs 21, not 8.
      frame_cost = {5'd0, mp} + 16'd13;
    end
  endfunction

  // ---- a legal interval is a power of two, in frames ----
  function is_pow2;
    input [7:0] v;
    begin
      is_pow2 = (v != 8'd0) && ((v & (v - 8'd1)) == 8'd0);
    end
  endfunction

  reg [15:0] per_r;      // periodic bytes committed per frame
  reg [15:0] ctl_r;      // control bytes committed per frame
  reg [15:0] cnt_r;      // how many endpoints are admitted

  reg [10:0] slot_mp   [0:N_EP-1];
  reg [1:0]  slot_type [0:N_EP-1];
  // A packed vector rather than an array of regs, so that a same-cycle
  // release can be MASKED OUT of it combinationally. See used_now below.
  reg [N_EP-1:0] used_r;

  reg        done_r;
  reg        ok_r;
  reg [2:0]  why_r;
  reg [3:0]  slot_r;

  reg [31:0] req_c, bw_c, iv_c, mp_c, full_c, over_c;

  assign adm_done          = done_r;
  assign adm_ok            = ok_r;
  assign adm_reason        = why_r;
  assign adm_slot          = slot_r;
  assign used_periodic     = per_r;
  assign used_control      = ctl_r;
  assign n_admitted        = cnt_r;
  assign n_req             = req_c;
  assign n_reject_bw       = bw_c;
  assign n_reject_interval = iv_c;
  assign n_reject_maxp     = mp_c;
  assign n_reject_full     = full_c;
  assign n_overcommit      = over_c;

  // =================================================================
  //  THE POST-RELEASE VIEW.
  //
  //  A release written "before" the request in source order is NOT applied
  //  before it in hardware: every read below sees the registered value, so
  //  a driver that closes one endpoint and opens another in the same cycle
  //  would be refused for want of a slot it just freed.
  //
  //  Being refused is not a functional failure -- the driver retries and
  //  succeeds -- which is exactly why this is worth getting right. The
  //  symptom is a bandwidth limit that appears only under load, and it is
  //  unreproducible because it depends on whether two operations happened
  //  to land in one cycle.
  //
  //  So the post-release state is computed explicitly and everything that
  //  decides the request reads THAT.
  // =================================================================
  wire rel_hit = rel_valid && (rel_slot < N_EP) && used_r[rel_slot];

  wire [15:0] rel_cost = frame_cost(slot_mp[rel_slot]);
  wire        rel_per  = rel_hit && ((slot_type[rel_slot] == X_ISO) ||
                                     (slot_type[rel_slot] == X_INT));
  wire        rel_ctl  = rel_hit && (slot_type[rel_slot] == X_CTRL);

  wire [N_EP-1:0] used_now = rel_hit ? (used_r & ~({{(N_EP-1){1'b0}}, 1'b1} << rel_slot))
                                     : used_r;
  wire [15:0]     per_now  = rel_per ? (per_r - rel_cost) : per_r;
  wire [15:0]     ctl_now  = rel_ctl ? (ctl_r - rel_cost) : ctl_r;

  // ---- the first free slot, in the POST-RELEASE view ----
  integer j;
  reg [3:0]  free_slot;
  reg        have_free;

  always @* begin
    free_slot = 4'd0;
    have_free = 1'b0;
    for (j = N_EP - 1; j >= 0; j = j - 1)
      if (!used_now[j]) begin
        free_slot = j[3:0];
        have_free = 1'b1;
      end
  end

  wire is_periodic = (req_type == X_ISO) || (req_type == X_INT);
  wire [15:0] cost = frame_cost(req_maxp);

  integer k;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      per_r  <= 16'd0;
      ctl_r  <= 16'd0;
      cnt_r  <= 16'd0;
      done_r <= 1'b0;
      ok_r   <= 1'b0;
      why_r  <= R_OK;
      slot_r <= 4'd0;
      req_c  <= 32'd0;
      bw_c   <= 32'd0;
      iv_c   <= 32'd0;
      mp_c   <= 32'd0;
      full_c <= 32'd0;
      over_c <= 32'd0;
      used_r <= {N_EP{1'b0}};
      for (k = 0; k < N_EP; k = k + 1) begin
        slot_mp[k]   <= 11'd0;
        slot_type[k] <= X_CTRL;
      end
    end else begin
      done_r <= 1'b0;
      ok_r   <= 1'b0;
      why_r  <= R_OK;

      // ---- release first, so a slot freed this cycle is available ----
      //
      // Applied before the request below, which means a driver that closes
      // one endpoint and opens another in the same cycle succeeds. Ordering
      // it the other way rejects the request and the driver retries -- which
      // works, and looks like a bandwidth bug under load.
      if (rel_hit) begin
        cnt_r <= cnt_r - 16'd1;
        if      (rel_per) per_r <= per_now;
        else if (rel_ctl) ctl_r <= ctl_now;
      end
      used_r <= used_now;

      if (req_valid) begin
        req_c  <= req_c + 32'd1;
        done_r <= 1'b1;
        slot_r <= free_slot;

        // ---- the checks, in the order that makes the reason useful ----
        //
        // Cheapest and most specific first. A request that is wrong in two
        // ways should report the one the driver can act on.
        if (req_maxp > max_packet(req_type)) begin
          ok_r  <= 1'b0;
          why_r <= R_MAXP;
          mp_c  <= mp_c + 32'd1;

        end else if (is_periodic && !is_pow2(req_interval)) begin
          // A periodic interval must be a power of two so the host can
          // place the endpoint in a binary tree of frames. 3 frames is not
          // schedulable however much bandwidth is free.
          ok_r  <= 1'b0;
          why_r <= R_INTERVAL;
          iv_c  <= iv_c + 32'd1;

        end else if (!have_free) begin
          ok_r   <= 1'b0;
          why_r  <= R_FULL;
          full_c <= full_c + 32'd1;

        end else if (is_periodic && ((per_now + cost) > PERIODIC_MAX)) begin
          // The 90% rule. This is the one that makes isochronous what it
          // is: once the periodic budget is spoken for, nothing more gets
          // in -- and nothing already in can be retried either.
          ok_r  <= 1'b0;
          why_r <= R_BW;
          bw_c  <= bw_c + 32'd1;

        end else if ((req_type == X_CTRL) &&
                     ((ctl_now + cost) > CONTROL_MIN)) begin
          // The 10% rule, and it is a SEPARATE limit rather than the
          // remainder of the periodic one. Control traffic has its own
          // reservation precisely so that a fully committed periodic
          // schedule cannot stop the host talking to its devices.
          ok_r  <= 1'b0;
          why_r <= R_CTRL_ROOM;
          bw_c  <= bw_c + 32'd1;

        end else begin
          ok_r  <= 1'b1;
          why_r <= R_OK;
          used_r               <= used_now | ({{(N_EP-1){1'b0}}, 1'b1} << free_slot);
          slot_mp[free_slot]   <= req_maxp;
          slot_type[free_slot] <= req_type;
          cnt_r <= rel_hit ? cnt_r : (cnt_r + 16'd1);
          if (is_periodic)              per_r <= per_now + cost;
          else if (req_type == X_CTRL)  ctl_r <= ctl_now + cost;
          // Bulk is charged NOTHING. It has no reservation, which is
          // exactly why it can retry: it uses whatever the frame has left
          // after everything with a promise has been served.
        end
      end

      // ---- the self-check that makes the whole thing measurable ----
      //
      // The periodic commitment must never exceed its ceiling, and the
      // control commitment must never exceed its reservation. Unreachable
      // on a correct design, counted so a run can publish zero.
      if ((per_r > PERIODIC_MAX) || (ctl_r > CONTROL_MIN))
        over_c <= over_c + 32'd1;
    end
  end

endmodule

8. SystemVerilog RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_xfer_admit -- SystemVerilog.
//
//  The transfer types and the rejection reasons become named enumerations,
//  which matters here for a specific reason: this module's job is to REPORT
//  WHY, and a rejection reason that arrives as the integer 5 is a reason
//  nobody reads. `R_CTRL_ROOM` in a waveform is a diagnosis.
//
//  The four types are easy to list and the trade is easy to state
//  backwards. What candidates say: "isochronous is fast but unreliable."
//  What is actually true:
//
//      Guaranteed bandwidth and guaranteed delivery are the SAME
//      resource spent two different ways.
//
//  An isochronous endpoint is promised a slot in every frame. A retry
//  needs a slot. Every slot is already allocated -- that is what the
//  promise means -- so there is nowhere to put the retry. Isochronous
//  is not unreliable because somebody skimped on error handling; it is
//  unreliable BECAUSE it is guaranteed.
//
//  Bulk is the mirror image: no reservation at all, therefore retries
//  are free, therefore delivery is certain and timing is not.
//
//  This module is the host's admission control: it decides whether a new
//  endpoint can be accepted into the schedule, and it is the place where
//  the trade becomes arithmetic.
// =====================================================================
module usb_xfer_admit #(
  // A USB 2.0 full-speed frame is 1500 bytes at 12 Mbit/s. The
  // specification reserves at most 90% for periodic traffic and at least
  // 10% for control, and the two limits are separate rules.
  parameter int FRAME_BYTES   = 1500,
  parameter int PERIODIC_MAX  = 1350,   // 90% of the frame
  parameter int CONTROL_MIN   = 150,    // 10%, reserved for control
  parameter int N_EP          = 8
) (
  input  logic       clk,
  input  logic       rst_n,

  // ---- a request to admit an endpoint into the schedule ----
  input  logic       req_valid,
  input  logic [1:0] req_type,     // X_CTRL / X_ISO / X_INT / X_BULK
  input  logic [10:0]req_maxp,     // bytes per transaction
  input  logic [7:0] req_interval, // frames between polls (periodic only)

  // ---- withdraw a previously admitted endpoint ----
  input  logic       rel_valid,
  input  logic [3:0] rel_slot,

  // ---- the decision, one cycle later ----
  output logic       adm_done,
  output logic       adm_ok,
  output logic [2:0] adm_reason,
  output logic [3:0] adm_slot,

  // ---- the schedule as it stands ----
  output logic [15:0]used_periodic,
  output logic [15:0]used_control,
  output logic [15:0]n_admitted,

  // ---- observability ----
  output logic [31:0]n_req,
  output logic [31:0]n_reject_bw,
  output logic [31:0]n_reject_interval,
  output logic [31:0]n_reject_maxp,
  output logic [31:0]n_reject_full,
  output logic [31:0]n_overcommit     // must always read 0
);

  typedef enum logic [1:0] { X_CTRL = 2'd0, X_ISO = 2'd1,
                             X_INT  = 2'd2, X_BULK = 2'd3 } xfer_e;

  // A rejection reason that arrives as the integer 5 is a reason nobody
  // reads. This is the output the driver author actually consumes.
  typedef enum logic [2:0] {
    R_OK        = 3'd0,
    R_BW        = 3'd1,   // no periodic bandwidth left
    R_INTERVAL  = 3'd2,   // interval not a legal value
    R_MAXP      = 3'd3,   // packet too large for the type
    R_FULL      = 3'd4,   // no free slot
    R_CTRL_ROOM = 3'd5    // would eat the control reservation
  } reason_e;

  // ---- the largest packet each type may use at full speed ----
  //
  // These are not stylistic limits. Isochronous gets the biggest packet
  // because it is the type that cannot retry, so a lost packet must cost
  // as little as possible in transactions -- one big one beats eight small
  // ones when none of them can be repeated.
  function automatic logic [10:0] max_packet(logic [1:0] t);
    begin
      case (t)
        X_CTRL: max_packet = 11'd64;
        X_ISO:  max_packet = 11'd1023;
        X_INT:  max_packet = 11'd64;
        default: max_packet = 11'd64;      // bulk
      endcase
    end
  endfunction

  // ---- a periodic endpoint's cost, per frame ----
  //
  // maxp bytes every `interval` frames is maxp/interval bytes per frame on
  // average -- but the schedule must survive the WORST frame, not the
  // average one, so the cost charged is the full maxp. Charging the average
  // is how a schedule that passes admission control then misses deadlines.
  function automatic logic [15:0] frame_cost(logic [10:0] mp);
    begin
      // Bytes on the wire are more than the payload: token, data PID, CRC
      // and inter-packet gap. 13 bytes of overhead per transaction at full
      // speed is the conventional figure and it is not negligible for small
      // packets -- an 8-byte interrupt endpoint costs 21, not 8.
      frame_cost = {5'd0, mp} + 16'd13;
    end
  endfunction

  // ---- a legal interval is a power of two, in frames ----
  function automatic logic is_pow2(logic [7:0] v);
    begin
      is_pow2 = (v != 8'd0) && ((v & (v - 8'd1)) == 8'd0);
    end
  endfunction

  logic [15:0] per_r;      // periodic bytes committed per frame
  logic [15:0] ctl_r;      // control bytes committed per frame
  logic [15:0] cnt_r;      // how many endpoints are admitted

  logic [10:0] slot_mp   [N_EP];
  xfer_e       slot_type [N_EP];
  // A packed vector rather than an array of regs, so that a same-cycle
  // release can be MASKED OUT of it combinationally. See used_now below.
  logic [N_EP-1:0] used_r;

  logic      done_r;
  logic      ok_r;
  reason_e   why_r;
  logic [3:0] slot_r;

  logic [31:0] req_c, bw_c, iv_c, mp_c, full_c, over_c;

  assign adm_done          = done_r;
  assign adm_ok            = ok_r;
  assign adm_reason        = why_r;
  assign adm_slot          = slot_r;
  assign used_periodic     = per_r;
  assign used_control      = ctl_r;
  assign n_admitted        = cnt_r;
  assign n_req             = req_c;
  assign n_reject_bw       = bw_c;
  assign n_reject_interval = iv_c;
  assign n_reject_maxp     = mp_c;
  assign n_reject_full     = full_c;
  assign n_overcommit      = over_c;

  // =================================================================
  //  THE POST-RELEASE VIEW.
  //
  //  A release written "before" the request in source order is NOT applied
  //  before it in hardware: every read below sees the registered value, so
  //  a driver that closes one endpoint and opens another in the same cycle
  //  would be refused for want of a slot it just freed.
  //
  //  Being refused is not a functional failure -- the driver retries and
  //  succeeds -- which is exactly why this is worth getting right. The
  //  symptom is a bandwidth limit that appears only under load, and it is
  //  unreproducible because it depends on whether two operations happened
  //  to land in one cycle.
  //
  //  So the post-release state is computed explicitly and everything that
  //  decides the request reads THAT.
  // =================================================================
  wire rel_hit = rel_valid && (rel_slot < N_EP) && used_r[rel_slot];

  wire [15:0] rel_cost = frame_cost(slot_mp[rel_slot]);
  wire        rel_per  = rel_hit && ((slot_type[rel_slot] == X_ISO) ||
                                     (slot_type[rel_slot] == X_INT));
  wire        rel_ctl  = rel_hit && (slot_type[rel_slot] == X_CTRL);

  wire [N_EP-1:0] used_now = rel_hit ? (used_r & ~({{(N_EP-1){1'b0}}, 1'b1} << rel_slot))
                                     : used_r;
  wire [15:0]     per_now  = rel_per ? (per_r - rel_cost) : per_r;
  wire [15:0]     ctl_now  = rel_ctl ? (ctl_r - rel_cost) : ctl_r;

  // ---- the first free slot, in the POST-RELEASE view ----
  logic [3:0] free_slot;
  logic       have_free;

  always_comb begin
    free_slot = 4'd0;
    have_free = 1'b0;
    for (int j = N_EP - 1; j >= 0; j--)
      if (!used_now[j]) begin
        free_slot = j[3:0];
        have_free = 1'b1;
      end
  end

  wire is_periodic = (req_type == X_ISO) || (req_type == X_INT);
  wire [15:0] cost = frame_cost(req_maxp);


  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      per_r  <= 16'd0;
      ctl_r  <= 16'd0;
      cnt_r  <= 16'd0;
      done_r <= 1'b0;
      ok_r   <= 1'b0;
      why_r  <= R_OK;
      slot_r <= 4'd0;
      req_c  <= 32'd0;
      bw_c   <= 32'd0;
      iv_c   <= 32'd0;
      mp_c   <= 32'd0;
      full_c <= 32'd0;
      over_c <= 32'd0;
      used_r <= {N_EP{1'b0}};
      for (int k = 0; k < N_EP; k++) begin
        slot_mp[k]   <= 11'd0;
        slot_type[k] <= X_CTRL;
      end
    end else begin
      done_r <= 1'b0;
      ok_r   <= 1'b0;
      why_r  <= R_OK;

      // ---- release first, so a slot freed this cycle is available ----
      //
      // Applied before the request below, which means a driver that closes
      // one endpoint and opens another in the same cycle succeeds. Ordering
      // it the other way rejects the request and the driver retries -- which
      // works, and looks like a bandwidth bug under load.
      if (rel_hit) begin
        cnt_r <= cnt_r - 16'd1;
        if      (rel_per) per_r <= per_now;
        else if (rel_ctl) ctl_r <= ctl_now;
      end
      used_r <= used_now;

      if (req_valid) begin
        req_c  <= req_c + 32'd1;
        done_r <= 1'b1;
        slot_r <= free_slot;

        // ---- the checks, in the order that makes the reason useful ----
        //
        // Cheapest and most specific first. A request that is wrong in two
        // ways should report the one the driver can act on.
        if (req_maxp > max_packet(req_type)) begin
          ok_r  <= 1'b0;
          why_r <= R_MAXP;
          mp_c  <= mp_c + 32'd1;

        end else if (is_periodic && !is_pow2(req_interval)) begin
          // A periodic interval must be a power of two so the host can
          // place the endpoint in a binary tree of frames. 3 frames is not
          // schedulable however much bandwidth is free.
          ok_r  <= 1'b0;
          why_r <= R_INTERVAL;
          iv_c  <= iv_c + 32'd1;

        end else if (!have_free) begin
          ok_r   <= 1'b0;
          why_r  <= R_FULL;
          full_c <= full_c + 32'd1;

        end else if (is_periodic && ((per_now + cost) > PERIODIC_MAX)) begin
          // The 90% rule. This is the one that makes isochronous what it
          // is: once the periodic budget is spoken for, nothing more gets
          // in -- and nothing already in can be retried either.
          ok_r  <= 1'b0;
          why_r <= R_BW;
          bw_c  <= bw_c + 32'd1;

        end else if ((req_type == X_CTRL) &&
                     ((ctl_now + cost) > CONTROL_MIN)) begin
          // The 10% rule, and it is a SEPARATE limit rather than the
          // remainder of the periodic one. Control traffic has its own
          // reservation precisely so that a fully committed periodic
          // schedule cannot stop the host talking to its devices.
          ok_r  <= 1'b0;
          why_r <= R_CTRL_ROOM;
          bw_c  <= bw_c + 32'd1;

        end else begin
          ok_r  <= 1'b1;
          why_r <= R_OK;
          used_r               <= used_now | ({{(N_EP-1){1'b0}}, 1'b1} << free_slot);
          slot_mp[free_slot]   <= req_maxp;
          slot_type[free_slot] <= xfer_e'(req_type);
          cnt_r <= rel_hit ? cnt_r : (cnt_r + 16'd1);
          if (is_periodic)              per_r <= per_now + cost;
          else if (req_type == X_CTRL)  ctl_r <= ctl_now + cost;
          // Bulk is charged NOTHING. It has no reservation, which is
          // exactly why it can retry: it uses whatever the frame has left
          // after everything with a promise has been served.
        end
      end

      // ---- the self-check that makes the whole thing measurable ----
      //
      // The periodic commitment must never exceed its ceiling, and the
      // control commitment must never exceed its reservation. Unreachable
      // on a correct design, counted so a run can publish zero.
      if ((per_r > PERIODIC_MAX) || (ctl_r > CONTROL_MIN))
        over_c <= over_c + 32'd1;
    end
  end

endmodule

9. VHDL-2008 RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  usb_xfer_admit -- VHDL-2008.
--
--  The transfer types and the rejection reasons are enumerations of
--  genuinely distinct types, so the compiler refuses to confuse a reason
--  with a type. That matters here because this module's whole output is a
--  DIAGNOSIS: "refused" is useless, "refused because the control
--  reservation is full" tells a driver author what to change.
--
--  Constants are prefixed because VHDL identifiers are case-insensitive:
--  a package constant and a same-named port are one name and the port
--  wins, silently.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package xt_pkg is
  type xfer_t is (XT_CTRL, XT_ISO, XT_INT, XT_BULK);

  type reason_t is (RS_OK,
                    RS_BW,         -- no periodic bandwidth left
                    RS_INTERVAL,   -- interval not a legal value
                    RS_MAXP,       -- packet too large for the type
                    RS_FULL,       -- no free slot
                    RS_CTRL_ROOM); -- would eat the control reservation

  function xfer_of(v : std_logic_vector(1 downto 0)) return xfer_t;
  function code_of(r : reason_t) return std_logic_vector;
  function is_periodic_t(t : xfer_t) return boolean;

  -- A transaction costs more than its payload: token, data PID, CRC and
  -- inter-packet gap. 13 bytes at full speed is the conventional figure,
  -- and it is not negligible for small packets -- an 8-byte interrupt
  -- endpoint costs 21, not 8.
  constant TXN_OVERHEAD : natural := 13;

  type mp_array is array (natural range <>) of unsigned(10 downto 0);
  type ty_array is array (natural range <>) of xfer_t;
end package;

package body xt_pkg is
  function xfer_of(v : std_logic_vector(1 downto 0)) return xfer_t is
  begin
    case v is
      when "00"   => return XT_CTRL;
      when "01"   => return XT_ISO;
      when "10"   => return XT_INT;
      when others => return XT_BULK;
    end case;
  end function;

  function code_of(r : reason_t) return std_logic_vector is
  begin
    case r is
      when RS_OK        => return "000";
      when RS_BW        => return "001";
      when RS_INTERVAL  => return "010";
      when RS_MAXP      => return "011";
      when RS_FULL      => return "100";
      when RS_CTRL_ROOM => return "101";
    end case;
  end function;

  function is_periodic_t(t : xfer_t) return boolean is
  begin
    return t = XT_ISO or t = XT_INT;
  end function;
end package body;

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

entity usb_xfer_admit is
  generic (
    FRAME_BYTES  : natural := 1500;
    PERIODIC_MAX : natural := 1350;   -- 90% of the frame
    CONTROL_MIN  : natural := 150;    -- 10%, reserved for control
    N_EP         : natural := 8
  );
  port (
    clk          : in  std_logic;
    rst_n        : in  std_logic;

    req_valid    : in  std_logic;
    req_type     : in  std_logic_vector(1 downto 0);
    req_maxp     : in  std_logic_vector(10 downto 0);
    req_interval : in  std_logic_vector(7 downto 0);

    rel_valid    : in  std_logic;
    rel_slot     : in  std_logic_vector(3 downto 0);

    adm_done     : out std_logic;
    adm_ok       : out std_logic;
    adm_reason   : out std_logic_vector(2 downto 0);
    adm_slot     : out std_logic_vector(3 downto 0);

    used_periodic : out std_logic_vector(15 downto 0);
    used_control  : out std_logic_vector(15 downto 0);
    n_admitted    : out std_logic_vector(15 downto 0);

    n_req             : out std_logic_vector(31 downto 0);
    n_reject_bw       : out std_logic_vector(31 downto 0);
    n_reject_interval : out std_logic_vector(31 downto 0);
    n_reject_maxp     : out std_logic_vector(31 downto 0);
    n_reject_full     : out std_logic_vector(31 downto 0);
    n_overcommit      : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_xfer_admit is
  signal per_r, ctl_r, cnt_r : unsigned(15 downto 0) := (others => '0');
  signal used_r  : std_logic_vector(N_EP-1 downto 0) := (others => '0');
  signal slot_mp : mp_array(0 to N_EP-1) := (others => (others => '0'));
  signal slot_ty : ty_array(0 to N_EP-1) := (others => XT_CTRL);

  signal done_r : std_logic := '0';
  signal ok_r   : std_logic := '0';
  signal why_r  : reason_t  := RS_OK;
  signal slot_r : unsigned(3 downto 0) := (others => '0');

  signal req_c, bw_c, iv_c, mp_c, full_c, over_c : unsigned(31 downto 0)
    := (others => '0');

  -- ---- the largest packet each type may use at full speed ----
  --
  -- Isochronous gets the biggest packet because it is the type that cannot
  -- retry: a lost packet must cost as few transactions as possible, and one
  -- big one beats eight small ones when none of them can be repeated.
  -- A case over all four types rather than "ISO or not", so that the
  -- per-type limits can be changed independently. Collapsing it to a single
  -- comparison also collapses three mutations into one: a mutation of the
  -- control limit then silently changes interrupt and bulk too, and the
  -- VHDL column reads 116236 against Verilog's 64357.
  function max_packet(t : xfer_t) return unsigned is
  begin
    case t is
      when XT_CTRL => return to_unsigned(64, 11);
      when XT_ISO  => return to_unsigned(1023, 11);
      when XT_INT  => return to_unsigned(64, 11);
      when XT_BULK => return to_unsigned(64, 11);
    end case;
  end function;

  -- The cost charged is the FULL packet, not maxp/interval. The schedule
  -- must survive the worst frame, not the average one; charging the average
  -- is how a schedule passes admission control and then misses deadlines.
  function frame_cost(mp : unsigned(10 downto 0)) return unsigned is
  begin
    return resize(mp, 16) + to_unsigned(TXN_OVERHEAD, 16);
  end function;

  -- A legal periodic interval is a power of two, in frames, so the host can
  -- place the endpoint in a binary tree of frames. 3 frames is not
  -- schedulable however much bandwidth is free.
  function is_pow2(v : unsigned(7 downto 0)) return boolean is
  begin
    return v /= 0 and (v and (v - 1)) = 0;
  end function;
begin

  adm_done      <= done_r;
  adm_ok        <= ok_r;
  adm_reason    <= code_of(why_r);
  adm_slot      <= std_logic_vector(slot_r);
  used_periodic <= std_logic_vector(per_r);
  used_control  <= std_logic_vector(ctl_r);
  n_admitted    <= std_logic_vector(cnt_r);

  n_req             <= std_logic_vector(req_c);
  n_reject_bw       <= std_logic_vector(bw_c);
  n_reject_interval <= std_logic_vector(iv_c);
  n_reject_maxp     <= std_logic_vector(mp_c);
  n_reject_full     <= std_logic_vector(full_c);
  n_overcommit      <= std_logic_vector(over_c);

  main : process(clk, rst_n)
    -- The POST-RELEASE view. A release written before the request in source
    -- order is not applied before it in hardware: every read sees the
    -- registered value, so a driver that closes one endpoint and opens
    -- another in one cycle would be refused for a slot it just freed.
    --
    -- Being refused is not a functional failure -- the driver retries -- which
    -- is why it is worth getting right: the symptom is a bandwidth limit that
    -- appears only under load and cannot be reproduced.
    variable rs_i     : natural;
    variable rel_hit  : boolean;
    variable rel_cost : unsigned(15 downto 0);
    variable used_now : std_logic_vector(N_EP-1 downto 0);
    variable per_now, ctl_now : unsigned(15 downto 0);
    variable free_slot : natural;
    variable have_free : boolean;
    variable t         : xfer_t;
    variable cost      : unsigned(15 downto 0);
    variable per_req   : boolean;
  begin
    if rst_n = '0' then
      per_r  <= (others => '0');
      ctl_r  <= (others => '0');
      cnt_r  <= (others => '0');
      used_r <= (others => '0');
      done_r <= '0';
      ok_r   <= '0';
      why_r  <= RS_OK;
      slot_r <= (others => '0');
      req_c  <= (others => '0');
      bw_c   <= (others => '0');
      iv_c   <= (others => '0');
      mp_c   <= (others => '0');
      full_c <= (others => '0');
      over_c <= (others => '0');
      slot_mp <= (others => (others => '0'));
      slot_ty <= (others => XT_CTRL);

    elsif rising_edge(clk) then
      done_r <= '0';
      ok_r   <= '0';
      why_r  <= RS_OK;

      -- ---- the post-release view, computed before anything decides ----
      rs_i     := to_integer(unsigned(rel_slot));
      rel_hit  := rel_valid = '1' and rs_i < N_EP and used_r(rs_i) = '1';
      used_now := used_r;
      per_now  := per_r;
      ctl_now  := ctl_r;
      if rel_hit then
        rel_cost    := frame_cost(slot_mp(rs_i));
        used_now(rs_i) := '0';
        if is_periodic_t(slot_ty(rs_i)) then
          per_now := per_now - rel_cost;
        elsif slot_ty(rs_i) = XT_CTRL then
          ctl_now := ctl_now - rel_cost;
        end if;
        cnt_r <= cnt_r - 1;
      end if;
      per_r  <= per_now;
      ctl_r  <= ctl_now;
      used_r <= used_now;

      -- ---- the first free slot, in the post-release view ----
      free_slot := 0;
      have_free := false;
      for j in N_EP-1 downto 0 loop
        if used_now(j) = '0' then
          free_slot := j;
          have_free := true;
        end if;
      end loop;

      if req_valid = '1' then
        t       := xfer_of(req_type);
        cost    := frame_cost(unsigned(req_maxp));
        per_req := is_periodic_t(t);

        req_c  <= req_c + 1;
        done_r <= '1';
        slot_r <= to_unsigned(free_slot, 4);

        -- ---- the checks, cheapest and most specific first ----
        --
        -- A request that is wrong in two ways should report the one the
        -- driver can act on.
        if unsigned(req_maxp) > max_packet(t) then
          ok_r  <= '0';
          why_r <= RS_MAXP;
          mp_c  <= mp_c + 1;

        elsif per_req and not is_pow2(unsigned(req_interval)) then
          ok_r  <= '0';
          why_r <= RS_INTERVAL;
          iv_c  <= iv_c + 1;

        elsif not have_free then
          ok_r   <= '0';
          why_r  <= RS_FULL;
          full_c <= full_c + 1;

        elsif per_req and (per_now + cost) > to_unsigned(PERIODIC_MAX, 16) then
          -- The 90% rule, and the reason isochronous is what it is: once the
          -- periodic budget is spoken for nothing more gets in -- and nothing
          -- already in can be retried either.
          ok_r  <= '0';
          why_r <= RS_BW;
          bw_c  <= bw_c + 1;

        elsif t = XT_CTRL and (ctl_now + cost) > to_unsigned(CONTROL_MIN, 16) then
          -- The 10% rule, a SEPARATE limit rather than the remainder of the
          -- periodic one: control has its own reservation precisely so that a
          -- fully committed periodic schedule cannot stop the host talking to
          -- its devices.
          ok_r  <= '0';
          why_r <= RS_CTRL_ROOM;
          bw_c  <= bw_c + 1;

        else
          ok_r  <= '1';
          why_r <= RS_OK;
          used_now(free_slot) := '1';
          used_r              <= used_now;
          slot_mp(free_slot)  <= unsigned(req_maxp);
          slot_ty(free_slot)  <= t;
          if rel_hit then cnt_r <= cnt_r;
          else            cnt_r <= cnt_r + 1;
          end if;
          if per_req then
            per_r <= per_now + cost;
          elsif t = XT_CTRL then
            ctl_r <= ctl_now + cost;
          end if;
          -- Bulk is charged NOTHING. It has no reservation, which is exactly
          -- why it can retry: it uses whatever the frame has left after
          -- everything with a promise has been served.
        end if;
      end if;

      -- ---- the self-check that makes the whole thing measurable ----
      if per_r > to_unsigned(PERIODIC_MAX, 16)
         or ctl_r > to_unsigned(CONTROL_MIN, 16) then
        over_c <= over_c + 1;
      end if;
    end if;
  end process;

end architecture;

10. The Testbench: A Model That Re-Sums

The shadow does not track the schedule incrementally. It keeps the set of admitted endpoints and re-sums the whole commitment from scratch every time it needs a total.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Design:  per_r <= per_r + cost          (incremental)
            per_r <= per_r - released_cost

   Shadow:  sum over every admitted slot     (re-summed)

   A release that subtracts the wrong amount leaves the
   design's total SELF-CONSISTENT and wrong. A model that
   re-sums cannot make that mistake, so the two disagree
   the moment the arithmetic drifts.

That is the whole reason the model is written the expensive way. An incremental model would drift in exactly the same direction as an incremental design and agree with it perfectly.

Verilog-2005 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for usb_xfer_admit.
//
//  The shadow model is written from the RULES rather than from the
//  design's control flow: it recomputes, from scratch, what the total
//  periodic and control commitment OUGHT to be given the set of
//  endpoints currently admitted -- by summing over the slots rather than
//  by tracking increments.
//
//  That distinction matters. The design maintains running totals, so a
//  release that subtracts the wrong amount leaves a total that is
//  self-consistent and wrong. A model that re-sums cannot make that
//  mistake, so the two disagree the moment the design's arithmetic drifts.
// =====================================================================
`timescale 1ns/1ps
module tb_xt_v;

  localparam integer FRAME_BYTES  = 1500;
  localparam integer PERIODIC_MAX = 1350;
  localparam integer CONTROL_MIN  = 150;
  localparam integer N_EP         = 8;

  localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;
  localparam [2:0] R_OK = 3'd0, R_BW = 3'd1, R_INTERVAL = 3'd2,
                   R_MAXP = 3'd3, R_FULL = 3'd4, R_CTRL_ROOM = 3'd5;

  reg         clk = 1'b0, rst_n = 1'b0;
  reg         req_valid = 1'b0;
  reg  [1:0]  req_type = X_BULK;
  reg  [10:0] req_maxp = 11'd0;
  reg  [7:0]  req_interval = 8'd1;
  reg         rel_valid = 1'b0;
  reg  [3:0]  rel_slot = 4'd0;

  wire        adm_done, adm_ok;
  wire [2:0]  adm_reason;
  wire [3:0]  adm_slot;
  wire [15:0] used_periodic, used_control, n_admitted;
  wire [31:0] n_req, n_reject_bw, n_reject_interval, n_reject_maxp,
              n_reject_full, n_overcommit;

  usb_xfer_admit #(.FRAME_BYTES(FRAME_BYTES), .PERIODIC_MAX(PERIODIC_MAX),
                   .CONTROL_MIN(CONTROL_MIN), .N_EP(N_EP)) dut (
    .clk(clk), .rst_n(rst_n),
    .req_valid(req_valid), .req_type(req_type),
    .req_maxp(req_maxp), .req_interval(req_interval),
    .rel_valid(rel_valid), .rel_slot(rel_slot),
    .adm_done(adm_done), .adm_ok(adm_ok),
    .adm_reason(adm_reason), .adm_slot(adm_slot),
    .used_periodic(used_periodic), .used_control(used_control),
    .n_admitted(n_admitted),
    .n_req(n_req), .n_reject_bw(n_reject_bw),
    .n_reject_interval(n_reject_interval), .n_reject_maxp(n_reject_maxp),
    .n_reject_full(n_reject_full), .n_overcommit(n_overcommit)
  );

  always #5 clk = ~clk;

  integer errors = 0, checks = 0, steps = 0;
  integer seed;

  function [31:0] urand;
    input dummy;
    begin urand = $random(seed) & 32'h3FFF_FFFF; end
  endfunction

  // ---- the shadow: a SET of admitted endpoints, re-summed each time ----
  reg        s_used [0:N_EP-1];
  reg [10:0] s_mp   [0:N_EP-1];
  reg [1:0]  s_ty   [0:N_EP-1];
  reg [31:0] x_req, x_bw, x_iv, x_mp, x_full;

  integer n_over = 0;      // the headline: any overcommitted cycle
  // Run-wide totals: the DUT's counters are cleared by every reset, so a
  // summary printed from them describes the last window, not the run.
  integer g_req = 0, g_bw = 0, g_iv = 0, g_mp = 0, g_full = 0, g_ok = 0;

  reg reach [0:143];
  integer ri, n_reach;

  task ck(input cond, input [255:0] what);
    begin
      checks = checks + 1;
      if (!cond) begin
        errors = errors + 1;
        if (errors <= 20)
          $display("  ERROR @%0t step=%0d: %0s", $time, steps, what);
      end
    end
  endtask

  function [15:0] fcost;
    input [10:0] mp;
    begin fcost = {5'd0, mp} + 16'd13; end
  endfunction

  function [10:0] maxp_of;
    input [1:0] t;
    begin
      if (t == X_ISO) maxp_of = 11'd1023;
      else            maxp_of = 11'd64;
    end
  endfunction

  function pow2;
    input [7:0] v;
    begin pow2 = (v != 8'd0) && ((v & (v - 8'd1)) == 8'd0); end
  endfunction

  // ---- re-sum the whole schedule from the admitted set ----
  //
  // Never incremental. A design that subtracts the wrong amount on release
  // stays self-consistent; a model that re-sums does not.
  function [15:0] sum_periodic;
    input dummy;
    integer a;
    begin
      sum_periodic = 16'd0;
      for (a = 0; a < N_EP; a = a + 1)
        if (s_used[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT)))
          sum_periodic = sum_periodic + fcost(s_mp[a]);
    end
  endfunction

  function [15:0] sum_control;
    input dummy;
    integer a;
    begin
      sum_control = 16'd0;
      for (a = 0; a < N_EP; a = a + 1)
        if (s_used[a] && (s_ty[a] == X_CTRL))
          sum_control = sum_control + fcost(s_mp[a]);
    end
  endfunction

  function [15:0] count_used;
    input dummy;
    integer a;
    begin
      count_used = 16'd0;
      for (a = 0; a < N_EP; a = a + 1) if (s_used[a]) count_used = count_used + 16'd1;
    end
  endfunction

  function [3:0] first_free;
    input dummy;
    integer a;
    begin
      first_free = 4'd0;
      for (a = N_EP - 1; a >= 0; a = a - 1) if (!s_used[a]) first_free = a[3:0];
    end
  endfunction

  function have_free_f;
    input dummy;
    integer a;
    begin
      have_free_f = 1'b0;
      for (a = 0; a < N_EP; a = a + 1) if (!s_used[a]) have_free_f = 1'b1;
    end
  endfunction

  // ---------------------------------------------------------------
  //  One cycle: a release and/or a request, then check the decision.
  // ---------------------------------------------------------------
  task step(input rv, input [3:0] rs,
            input qv, input [1:0] qt, input [10:0] qm, input [7:0] qi);
    reg        e_ok;
    reg [2:0]  e_why;
    reg [3:0]  e_slot;
    reg        e_per;
    reg [15:0] e_cost;
    begin
      rel_valid = rv;  rel_slot = rs;
      req_valid = qv;  req_type = qt;  req_maxp = qm;  req_interval = qi;

      // ---- release first, exactly as the design orders it ----
      if (rv && (rs < N_EP) && s_used[rs]) s_used[rs] = 1'b0;

      e_per  = (qt == X_ISO) || (qt == X_INT);
      e_cost = fcost(qm);
      e_ok   = 1'b0;
      e_why  = R_OK;
      e_slot = first_free(0);

      if (qv) begin
        x_req = x_req + 1;  g_req = g_req + 1;
        if (qm > maxp_of(qt)) begin
          e_why = R_MAXP;  x_mp = x_mp + 1;  g_mp = g_mp + 1;
        end else if (e_per && !pow2(qi)) begin
          e_why = R_INTERVAL;  x_iv = x_iv + 1;  g_iv = g_iv + 1;
        end else if (!have_free_f(0)) begin
          e_why = R_FULL;  x_full = x_full + 1;  g_full = g_full + 1;
        end else if (e_per && ((sum_periodic(0) + e_cost) > PERIODIC_MAX)) begin
          e_why = R_BW;  x_bw = x_bw + 1;  g_bw = g_bw + 1;
        end else if ((qt == X_CTRL) && ((sum_control(0) + e_cost) > CONTROL_MIN)) begin
          e_why = R_CTRL_ROOM;  x_bw = x_bw + 1;  g_bw = g_bw + 1;
        end else begin
          e_ok = 1'b1;  e_why = R_OK;  g_ok = g_ok + 1;
          s_used[e_slot] = 1'b1;
          s_mp[e_slot]   = qm;
          s_ty[e_slot]   = qt;
        end
      end

      @(posedge clk);
      #1;
      steps = steps + 1;
      rel_valid = 1'b0;  req_valid = 1'b0;

      // ---- PROPERTY 1: the decision and its reason ----
      ck(adm_done === qv, "adm_done disagrees with whether a request was made");
      if (qv) begin
        ck(adm_ok     === e_ok,   "admission decision disagrees");
        ck(adm_reason === e_why,  "rejection reason disagrees");
        if (e_ok) ck(adm_slot === e_slot, "admitted into the wrong slot");
      end

      // ---- PROPERTY 2: the schedule totals, RE-SUMMED ----
      ck(used_periodic === sum_periodic(0), "periodic commitment disagrees");
      ck(used_control  === sum_control(0),  "control commitment disagrees");
      ck(n_admitted    === count_used(0),   "admitted count disagrees");

      // ---- PROPERTY 3: the ceilings are never exceeded ----
      if (used_periodic > PERIODIC_MAX) n_over = n_over + 1;
      if (used_control  > CONTROL_MIN)  n_over = n_over + 1;
      ck(used_periodic <= PERIODIC_MAX, "periodic budget overcommitted");
      ck(used_control  <= CONTROL_MIN,  "control reservation overcommitted");
      ck(n_overcommit === 32'd0, "the design detected its own overcommitment");
      ck(n_over == 0, "the schedule was overcommitted");

      // ---- PROPERTY 4: the counters agree ----
      ck(n_req             === x_req,  "request count disagrees");
      ck(n_reject_bw       === x_bw,   "bandwidth-rejection count disagrees");
      ck(n_reject_interval === x_iv,   "interval-rejection count disagrees");
      ck(n_reject_maxp     === x_mp,   "maxp-rejection count disagrees");
      ck(n_reject_full     === x_full, "slot-full-rejection count disagrees");
    end
  endtask

  task reset_dut;
    integer a;
    begin
      rst_n = 1'b0;
      req_valid = 0; rel_valid = 0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      for (a = 0; a < N_EP; a = a + 1) begin
        s_used[a] = 1'b0; s_mp[a] = 11'd0; s_ty[a] = X_CTRL;
      end
      x_req = 0; x_bw = 0; x_iv = 0; x_mp = 0; x_full = 0;
      @(posedge clk); #1;
    end
  endtask

  integer ti, mi, ii, k, a3;
  integer expect_n;
  reg [10:0] mps [0:5];
  reg [7:0]  ivs [0:5];

  initial begin
    for (ri = 0; ri < 144; ri = ri + 1) reach[ri] = 1'b0;
    // Boundary values on purpose: 64 and 65 straddle the control/bulk
    // limit, 1023 and 1024 straddle the isochronous one.
    mps[0] = 11'd8;   mps[1] = 11'd64;   mps[2] = 11'd65;
    mps[3] = 11'd512; mps[4] = 11'd1023; mps[5] = 11'd1024;
    // 1,2,4,8 are legal; 3 and 255 are not powers of two.
    ivs[0] = 8'd1; ivs[1] = 8'd2; ivs[2] = 8'd3;
    ivs[3] = 8'd4; ivs[4] = 8'd8; ivs[5] = 8'd255;
    seed = 32'd27005;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every type against every
    //  packet size against every interval. 4 x 6 x 6 = 144.
    // =============================================================
    for (ti = 0; ti < 4; ti = ti + 1)
    for (mi = 0; mi < 6; mi = mi + 1)
    for (ii = 0; ii < 6; ii = ii + 1) begin
      reset_dut;
      step(1'b0, 4'd0, 1'b1, ti[1:0], mps[mi], ivs[ii]);
      ri = (ti * 36) + (mi * 6) + ii;
      reach[ri] = 1'b1;
    end

    // =============================================================
    //  PHASE 2 (DIRECTED) -- fill the periodic budget exactly, then
    //  ask for one more byte.
    //
    //  This is the trade made arithmetic: the last endpoint that fits
    //  is admitted, the next is refused, and NOTHING already admitted
    //  can be retried -- because every slot is spoken for. That is
    //  what "guaranteed bandwidth" costs.
    // =============================================================
    reset_dut;
    // 1023 + 13 = 1036 per iso endpoint; the ceiling is 1350
    step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);
    ck(adm_ok === 1'b1, "the first isochronous endpoint was refused");
    // a second one would be 2072 > 1350
    step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);
    ck(adm_ok === 1'b0, "a second full-size isochronous endpoint was admitted");
    ck(adm_reason === R_BW, "the refusal was not attributed to bandwidth");
    // but a small interrupt endpoint still fits: 1036 + 8 + 13 = 1057
    step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd8);
    ck(adm_ok === 1'b1, "a small interrupt endpoint did not fit in the remainder");
    // and bulk ALWAYS fits, because it is charged nothing at all
    for (k = 0; k < 5; k = k + 1) begin
      step(1'b0, 4'd0, 1'b1, X_BULK, 11'd64, 8'd0);
      ck(adm_ok === 1'b1, "a bulk endpoint was refused for bandwidth");
      ck(used_periodic === 16'd1057, "bulk was charged periodic bandwidth");
    end

    // =============================================================
    //  PHASE 3 (DIRECTED) -- the control reservation is SEPARATE.
    //
    //  A fully committed periodic schedule must not stop the host
    //  talking to its devices, which is why control has its own 10%
    //  rather than sharing the periodic remainder.
    // =============================================================
    reset_dut;
    step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);    // 1036 periodic
    step(1'b0, 4'd0, 1'b1, X_CTRL, 11'd64, 8'd0);     // 77 control
    ck(adm_ok === 1'b1, "control was refused while its reservation was free");
    ck(used_control === 16'd77, "control was charged to the wrong budget");
    step(1'b0, 4'd0, 1'b1, X_CTRL, 11'd64, 8'd0);     // 154 > 150
    ck(adm_ok === 1'b0, "control exceeded its own reservation");
    ck(adm_reason === R_CTRL_ROOM, "the refusal was not attributed to control room");

    // =============================================================
    //  PHASE 4 (DIRECTED, EXHAUSTIVE) -- release returns EXACTLY
    //  what was charged, from every slot.
    // =============================================================
    for (k = 0; k < N_EP; k = k + 1) begin
      reset_dut;
      // fill every slot with a small interrupt endpoint
      for (a3 = 0; a3 < N_EP; a3 = a3 + 1)
        step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
      ck(used_periodic === 16'd168, "eight 8-byte interrupt endpoints cost the wrong amount");
      // the ninth is refused for want of a slot, not bandwidth
      step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
      ck(adm_ok === 1'b0, "a ninth endpoint was admitted into eight slots");
      ck(adm_reason === R_FULL, "the refusal was not attributed to slots");
      // release slot k and the commitment drops by exactly one endpoint
      step(1'b1, k[3:0], 1'b0, X_BULK, 11'd0, 8'd0);
      ck(used_periodic === 16'd147, "release returned the wrong amount");
      // and a release in the SAME cycle as a request frees the slot for it
      step(1'b1, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
    end

    // =============================================================
    //  PHASE 4b (DIRECTED, EXHAUSTIVE) -- walk the periodic budget from
    //  empty to refusal, for every packet size.
    //
    //  The trade this chapter is about is arithmetic, so it deserves
    //  arithmetic stimulus: admit identical isochronous endpoints one at a
    //  time until the budget refuses one, checking at every step that the
    //  commitment is exactly the re-summed total and that the refusal
    //  arrives at precisely the right count.
    //
    //  This is what catches a cost function that forgets the
    //  per-transaction overhead, or charges the average instead of the
    //  worst frame: both are off by a few bytes per endpoint and both show
    //  up as the budget refusing one endpoint too late.
    // =============================================================
    for (mi = 0; mi < 6; mi = mi + 1) begin
      if (mps[mi] <= 11'd1023) begin
        reset_dut;
        expect_n = 0;
        // how many of this size fit: floor(PERIODIC_MAX / (maxp + 13)),
        // capped by the number of slots
        while (((expect_n + 1) * (mps[mi] + 13) <= PERIODIC_MAX)
               && (expect_n < N_EP))
          expect_n = expect_n + 1;

        for (k = 0; k < expect_n; k = k + 1) begin
          step(1'b0, 4'd0, 1'b1, X_ISO, mps[mi], 8'd1);
          ck(adm_ok === 1'b1, "an endpoint that fits the budget was refused");
          ck(used_periodic === ((k + 1) * (mps[mi] + 13)),
             "the running commitment is not the sum of the admitted costs");
        end
        // the next one must be refused -- for bandwidth if the budget is
        // what binds, for slots if we ran out of those first
        step(1'b0, 4'd0, 1'b1, X_ISO, mps[mi], 8'd1);
        ck(adm_ok === 1'b0, "one endpoint too many was admitted");
        if (expect_n < N_EP)
          ck(adm_reason === R_BW, "the refusal was not attributed to bandwidth");
        else
          ck(adm_reason === R_FULL, "the refusal was not attributed to slots");
        ck(used_periodic === (expect_n * (mps[mi] + 13)),
           "a refused endpoint changed the commitment");
      end
    end

    // =============================================================
    //  PHASE 5 (RANDOM) -- a driver opening and closing endpoints.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 20000; k = k + 1) begin
      step((urand(0) % 4) == 0, urand(0) % 16,
           (urand(0) % 2) == 0, urand(0) % 4,
           mps[urand(0) % 6], ivs[urand(0) % 6]);
      if ((k % 256) == 255) reset_dut;
    end
`endif

    n_reach = 0;
    for (ri = 0; ri < 144; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;

    $display("steps=%0d checks=%0d reach=%0d/144 errors=%0d",
             steps, checks, n_reach, errors);
    $display("[sched] requests=%0d admitted=%0d bw=%0d interval=%0d maxp=%0d full=%0d",
             g_req, g_ok, g_bw, g_iv, g_mp, g_full);
    $display("[the whole point] overcommitted cycles = %0d", n_over);
    if (n_reach != 144) begin
      $display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
    end
    if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
    else             $display("FAIL: %0d errors in %0d checks", errors, checks);
    $finish;
  end

endmodule

SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for usb_xfer_admit.
//
//  The shadow model is written from the RULES rather than from the
//  design's control flow: it recomputes, from scratch, what the total
//  periodic and control commitment OUGHT to be given the set of
//  endpoints currently admitted -- by summing over the slots rather than
//  by tracking increments.
//
//  That distinction matters. The design maintains running totals, so a
//  release that subtracts the wrong amount leaves a total that is
//  self-consistent and wrong. A model that re-sums cannot make that
//  mistake, so the two disagree the moment the design's arithmetic drifts.
// =====================================================================
`timescale 1ns/1ps
module tb_xt_sv;

  localparam integer FRAME_BYTES  = 1500;
  localparam integer PERIODIC_MAX = 1350;
  localparam integer CONTROL_MIN  = 150;
  localparam integer N_EP         = 8;

  localparam [1:0] X_CTRL = 2'd0, X_ISO = 2'd1, X_INT = 2'd2, X_BULK = 2'd3;
  localparam [2:0] R_OK = 3'd0, R_BW = 3'd1, R_INTERVAL = 3'd2,
                   R_MAXP = 3'd3, R_FULL = 3'd4, R_CTRL_ROOM = 3'd5;

  logic       clk = 1'b0, rst_n = 1'b0;
  logic       req_valid = 1'b0;
  logic [1:0] req_type = X_BULK;
  logic [10:0] req_maxp = 11'd0;
  logic [7:0] req_interval = 8'd1;
  logic       rel_valid = 1'b0;
  logic [3:0] rel_slot = 4'd0;

  logic       adm_done, adm_ok;
  logic [2:0] adm_reason;
  logic [3:0] adm_slot;
  logic [15:0] used_periodic, used_control, n_admitted;
  logic [31:0] n_req, n_reject_bw, n_reject_interval, n_reject_maxp,
               n_reject_full, n_overcommit;

  usb_xfer_admit #(.FRAME_BYTES(FRAME_BYTES), .PERIODIC_MAX(PERIODIC_MAX),
                   .CONTROL_MIN(CONTROL_MIN), .N_EP(N_EP)) dut (
    .clk(clk), .rst_n(rst_n),
    .req_valid(req_valid), .req_type(req_type),
    .req_maxp(req_maxp), .req_interval(req_interval),
    .rel_valid(rel_valid), .rel_slot(rel_slot),
    .adm_done(adm_done), .adm_ok(adm_ok),
    .adm_reason(adm_reason), .adm_slot(adm_slot),
    .used_periodic(used_periodic), .used_control(used_control),
    .n_admitted(n_admitted),
    .n_req(n_req), .n_reject_bw(n_reject_bw),
    .n_reject_interval(n_reject_interval), .n_reject_maxp(n_reject_maxp),
    .n_reject_full(n_reject_full), .n_overcommit(n_overcommit)
  );

  always #5 clk = ~clk;

  integer errors = 0, checks = 0, steps = 0;
  integer seed;

  function automatic logic [31:0] urand(bit dummy);
    return $random(seed) & 32'h3FFF_FFFF;
  endfunction

  // ---- the shadow: a SET of admitted endpoints, re-summed each time ----
  logic      s_used [0:N_EP-1];
  logic [10:0] s_mp   [0:N_EP-1];
  logic [1:0] s_ty   [0:N_EP-1];
  logic [31:0] x_req, x_bw, x_iv, x_mp, x_full;

  integer n_over = 0;      // the headline: any overcommitted cycle
  // Run-wide totals: the DUT's counters are cleared by every reset, so a
  // summary printed from them describes the last window, not the run.
  integer g_req = 0, g_bw = 0, g_iv = 0, g_mp = 0, g_full = 0, g_ok = 0;

  logic reach [0:143];
  integer ri, n_reach;

  task ck(input logic cond, input logic [255:0] what);
    begin
      checks = checks + 1;
      if (!cond) begin
        errors = errors + 1;
        if (errors <= 20)
          $display("  ERROR @%0t step=%0d: %0s", $time, steps, what);
      end
    end
  endtask

  function automatic logic [15:0] fcost(logic [10:0] mp);
    return {5'd0, mp} + 16'd13;
  endfunction

  function automatic logic [10:0] maxp_of(logic [1:0] t);
    if (t == X_ISO) return 11'd1023;
    return 11'd64;
  endfunction

  function automatic logic pow2(logic [7:0] v);
    return (v != 8'd0) && ((v & (v - 8'd1)) == 8'd0);
  endfunction

  // ---- re-sum the whole schedule from the admitted set ----
  //
  // Never incremental. A design that subtracts the wrong amount on release
  // stays self-consistent; a model that re-sums does not.
  function automatic logic [15:0] sum_periodic(bit dummy);
    integer a;
    begin
      sum_periodic = 16'd0;
      for (a = 0; a < N_EP; a = a + 1)
        if (s_used[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT)))
          sum_periodic = sum_periodic + fcost(s_mp[a]);
    end
  endfunction

  function automatic logic [15:0] sum_control(bit dummy);
    integer a;
    begin
      sum_control = 16'd0;
      for (a = 0; a < N_EP; a = a + 1)
        if (s_used[a] && (s_ty[a] == X_CTRL))
          sum_control = sum_control + fcost(s_mp[a]);
    end
  endfunction

  function automatic logic [15:0] count_used(bit dummy);
    integer a;
    begin
      count_used = 16'd0;
      for (a = 0; a < N_EP; a = a + 1) if (s_used[a]) count_used = count_used + 16'd1;
    end
  endfunction

  function automatic logic [3:0] first_free(bit dummy);
    integer a;
    begin
      first_free = 4'd0;
      for (a = N_EP - 1; a >= 0; a = a - 1) if (!s_used[a]) first_free = a[3:0];
    end
  endfunction

  function automatic logic have_free_f(bit dummy);
    integer a;
    begin
      have_free_f = 1'b0;
      for (a = 0; a < N_EP; a = a + 1) if (!s_used[a]) have_free_f = 1'b1;
    end
  endfunction

  // ---------------------------------------------------------------
  //  One cycle: a release and/or a request, then check the decision.
  // ---------------------------------------------------------------
  task step(input logic rv, input logic [3:0] rs,
            input logic qv, input logic [1:0] qt,
            input logic [10:0] qm, input logic [7:0] qi);
    logic      e_ok;
    logic [2:0] e_why;
    logic [3:0] e_slot;
    logic      e_per;
    logic [15:0] e_cost;
    begin
      rel_valid = rv;  rel_slot = rs;
      req_valid = qv;  req_type = qt;  req_maxp = qm;  req_interval = qi;

      // ---- release first, exactly as the design orders it ----
      if (rv && (rs < N_EP) && s_used[rs]) s_used[rs] = 1'b0;

      e_per  = (qt == X_ISO) || (qt == X_INT);
      e_cost = fcost(qm);
      e_ok   = 1'b0;
      e_why  = R_OK;
      e_slot = first_free(0);

      if (qv) begin
        x_req = x_req + 1;  g_req = g_req + 1;
        if (qm > maxp_of(qt)) begin
          e_why = R_MAXP;  x_mp = x_mp + 1;  g_mp = g_mp + 1;
        end else if (e_per && !pow2(qi)) begin
          e_why = R_INTERVAL;  x_iv = x_iv + 1;  g_iv = g_iv + 1;
        end else if (!have_free_f(0)) begin
          e_why = R_FULL;  x_full = x_full + 1;  g_full = g_full + 1;
        end else if (e_per && ((sum_periodic(0) + e_cost) > PERIODIC_MAX)) begin
          e_why = R_BW;  x_bw = x_bw + 1;  g_bw = g_bw + 1;
        end else if ((qt == X_CTRL) && ((sum_control(0) + e_cost) > CONTROL_MIN)) begin
          e_why = R_CTRL_ROOM;  x_bw = x_bw + 1;  g_bw = g_bw + 1;
        end else begin
          e_ok = 1'b1;  e_why = R_OK;  g_ok = g_ok + 1;
          s_used[e_slot] = 1'b1;
          s_mp[e_slot]   = qm;
          s_ty[e_slot]   = qt;
        end
      end

      @(posedge clk);
      #1;
      steps = steps + 1;
      rel_valid = 1'b0;  req_valid = 1'b0;

      // ---- PROPERTY 1: the decision and its reason ----
      ck(adm_done === qv, "adm_done disagrees with whether a request was made");
      if (qv) begin
        ck(adm_ok     === e_ok,   "admission decision disagrees");
        ck(adm_reason === e_why,  "rejection reason disagrees");
        if (e_ok) ck(adm_slot === e_slot, "admitted into the wrong slot");
      end

      // ---- PROPERTY 2: the schedule totals, RE-SUMMED ----
      ck(used_periodic === sum_periodic(0), "periodic commitment disagrees");
      ck(used_control  === sum_control(0),  "control commitment disagrees");
      ck(n_admitted    === count_used(0),   "admitted count disagrees");

      // ---- PROPERTY 3: the ceilings are never exceeded ----
      if (used_periodic > PERIODIC_MAX) n_over = n_over + 1;
      if (used_control  > CONTROL_MIN)  n_over = n_over + 1;
      ck(used_periodic <= PERIODIC_MAX, "periodic budget overcommitted");
      ck(used_control  <= CONTROL_MIN,  "control reservation overcommitted");
      ck(n_overcommit === 32'd0, "the design detected its own overcommitment");
      ck(n_over == 0, "the schedule was overcommitted");

      // ---- PROPERTY 4: the counters agree ----
      ck(n_req             === x_req,  "request count disagrees");
      ck(n_reject_bw       === x_bw,   "bandwidth-rejection count disagrees");
      ck(n_reject_interval === x_iv,   "interval-rejection count disagrees");
      ck(n_reject_maxp     === x_mp,   "maxp-rejection count disagrees");
      ck(n_reject_full     === x_full, "slot-full-rejection count disagrees");
    end
  endtask

  task reset_dut;
    integer a;
    begin
      rst_n = 1'b0;
      req_valid = 0; rel_valid = 0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      for (a = 0; a < N_EP; a = a + 1) begin
        s_used[a] = 1'b0; s_mp[a] = 11'd0; s_ty[a] = X_CTRL;
      end
      x_req = 0; x_bw = 0; x_iv = 0; x_mp = 0; x_full = 0;
      @(posedge clk); #1;
    end
  endtask

  integer ti, mi, ii, k, a3;
  integer expect_n;
  logic [10:0] mps [0:5];
  logic [7:0] ivs [0:5];

  initial begin
    for (ri = 0; ri < 144; ri = ri + 1) reach[ri] = 1'b0;
    // Boundary values on purpose: 64 and 65 straddle the control/bulk
    // limit, 1023 and 1024 straddle the isochronous one.
    mps[0] = 11'd8;   mps[1] = 11'd64;   mps[2] = 11'd65;
    mps[3] = 11'd512; mps[4] = 11'd1023; mps[5] = 11'd1024;
    // 1,2,4,8 are legal; 3 and 255 are not powers of two.
    ivs[0] = 8'd1; ivs[1] = 8'd2; ivs[2] = 8'd3;
    ivs[3] = 8'd4; ivs[4] = 8'd8; ivs[5] = 8'd255;
    seed = 32'd27005;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every type against every
    //  packet size against every interval. 4 x 6 x 6 = 144.
    // =============================================================
    for (ti = 0; ti < 4; ti = ti + 1)
    for (mi = 0; mi < 6; mi = mi + 1)
    for (ii = 0; ii < 6; ii = ii + 1) begin
      reset_dut;
      step(1'b0, 4'd0, 1'b1, ti[1:0], mps[mi], ivs[ii]);
      ri = (ti * 36) + (mi * 6) + ii;
      reach[ri] = 1'b1;
    end

    // =============================================================
    //  PHASE 2 (DIRECTED) -- fill the periodic budget exactly, then
    //  ask for one more byte.
    //
    //  This is the trade made arithmetic: the last endpoint that fits
    //  is admitted, the next is refused, and NOTHING already admitted
    //  can be retried -- because every slot is spoken for. That is
    //  what "guaranteed bandwidth" costs.
    // =============================================================
    reset_dut;
    // 1023 + 13 = 1036 per iso endpoint; the ceiling is 1350
    step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);
    ck(adm_ok === 1'b1, "the first isochronous endpoint was refused");
    // a second one would be 2072 > 1350
    step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);
    ck(adm_ok === 1'b0, "a second full-size isochronous endpoint was admitted");
    ck(adm_reason === R_BW, "the refusal was not attributed to bandwidth");
    // but a small interrupt endpoint still fits: 1036 + 8 + 13 = 1057
    step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd8);
    ck(adm_ok === 1'b1, "a small interrupt endpoint did not fit in the remainder");
    // and bulk ALWAYS fits, because it is charged nothing at all
    for (k = 0; k < 5; k = k + 1) begin
      step(1'b0, 4'd0, 1'b1, X_BULK, 11'd64, 8'd0);
      ck(adm_ok === 1'b1, "a bulk endpoint was refused for bandwidth");
      ck(used_periodic === 16'd1057, "bulk was charged periodic bandwidth");
    end

    // =============================================================
    //  PHASE 3 (DIRECTED) -- the control reservation is SEPARATE.
    //
    //  A fully committed periodic schedule must not stop the host
    //  talking to its devices, which is why control has its own 10%
    //  rather than sharing the periodic remainder.
    // =============================================================
    reset_dut;
    step(1'b0, 4'd0, 1'b1, X_ISO, 11'd1023, 8'd1);    // 1036 periodic
    step(1'b0, 4'd0, 1'b1, X_CTRL, 11'd64, 8'd0);     // 77 control
    ck(adm_ok === 1'b1, "control was refused while its reservation was free");
    ck(used_control === 16'd77, "control was charged to the wrong budget");
    step(1'b0, 4'd0, 1'b1, X_CTRL, 11'd64, 8'd0);     // 154 > 150
    ck(adm_ok === 1'b0, "control exceeded its own reservation");
    ck(adm_reason === R_CTRL_ROOM, "the refusal was not attributed to control room");

    // =============================================================
    //  PHASE 4 (DIRECTED, EXHAUSTIVE) -- release returns EXACTLY
    //  what was charged, from every slot.
    // =============================================================
    for (k = 0; k < N_EP; k = k + 1) begin
      reset_dut;
      // fill every slot with a small interrupt endpoint
      for (a3 = 0; a3 < N_EP; a3 = a3 + 1)
        step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
      ck(used_periodic === 16'd168, "eight 8-byte interrupt endpoints cost the wrong amount");
      // the ninth is refused for want of a slot, not bandwidth
      step(1'b0, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
      ck(adm_ok === 1'b0, "a ninth endpoint was admitted into eight slots");
      ck(adm_reason === R_FULL, "the refusal was not attributed to slots");
      // release slot k and the commitment drops by exactly one endpoint
      step(1'b1, k[3:0], 1'b0, X_BULK, 11'd0, 8'd0);
      ck(used_periodic === 16'd147, "release returned the wrong amount");
      // and a release in the SAME cycle as a request frees the slot for it
      step(1'b1, 4'd0, 1'b1, X_INT, 11'd8, 8'd4);
    end

    // =============================================================
    //  PHASE 4b (DIRECTED, EXHAUSTIVE) -- walk the periodic budget from
    //  empty to refusal, for every packet size.
    //
    //  The trade this chapter is about is arithmetic, so it deserves
    //  arithmetic stimulus: admit identical isochronous endpoints one at a
    //  time until the budget refuses one, checking at every step that the
    //  commitment is exactly the re-summed total and that the refusal
    //  arrives at precisely the right count.
    //
    //  This is what catches a cost function that forgets the
    //  per-transaction overhead, or charges the average instead of the
    //  worst frame: both are off by a few bytes per endpoint and both show
    //  up as the budget refusing one endpoint too late.
    // =============================================================
    for (mi = 0; mi < 6; mi = mi + 1) begin
      if (mps[mi] <= 11'd1023) begin
        reset_dut;
        expect_n = 0;
        // how many of this size fit: floor(PERIODIC_MAX / (maxp + 13)),
        // capped by the number of slots
        while (((expect_n + 1) * (mps[mi] + 13) <= PERIODIC_MAX)
               && (expect_n < N_EP))
          expect_n = expect_n + 1;

        for (k = 0; k < expect_n; k = k + 1) begin
          step(1'b0, 4'd0, 1'b1, X_ISO, mps[mi], 8'd1);
          ck(adm_ok === 1'b1, "an endpoint that fits the budget was refused");
          ck(used_periodic === ((k + 1) * (mps[mi] + 13)),
             "the running commitment is not the sum of the admitted costs");
        end
        // the next one must be refused -- for bandwidth if the budget is
        // what binds, for slots if we ran out of those first
        step(1'b0, 4'd0, 1'b1, X_ISO, mps[mi], 8'd1);
        ck(adm_ok === 1'b0, "one endpoint too many was admitted");
        if (expect_n < N_EP)
          ck(adm_reason === R_BW, "the refusal was not attributed to bandwidth");
        else
          ck(adm_reason === R_FULL, "the refusal was not attributed to slots");
        ck(used_periodic === (expect_n * (mps[mi] + 13)),
           "a refused endpoint changed the commitment");
      end
    end

    // =============================================================
    //  PHASE 5 (RANDOM) -- a driver opening and closing endpoints.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 20000; k = k + 1) begin
      step((urand(0) % 4) == 0, urand(0) % 16,
           (urand(0) % 2) == 0, urand(0) % 4,
           mps[urand(0) % 6], ivs[urand(0) % 6]);
      if ((k % 256) == 255) reset_dut;
    end
`endif

    n_reach = 0;
    for (ri = 0; ri < 144; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;

    $display("steps=%0d checks=%0d reach=%0d/144 errors=%0d",
             steps, checks, n_reach, errors);
    $display("[sched] requests=%0d admitted=%0d bw=%0d interval=%0d maxp=%0d full=%0d",
             g_req, g_ok, g_bw, g_iv, g_mp, g_full);
    $display("[the whole point] overcommitted cycles = %0d", n_over);
    if (n_reach != 144) begin
      $display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
    end
    if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
    else             $display("FAIL: %0d errors in %0d checks", errors, checks);
    $finish;
  end

endmodule

VHDL-2008 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  Testbench for usb_xfer_admit (VHDL-2008).
--
--  The shadow re-sums the whole schedule from the set of admitted
--  endpoints rather than tracking increments. That distinction is the
--  point: the design maintains running totals, so a release that
--  subtracts the wrong amount leaves a total that is self-consistent and
--  wrong. A model that re-sums cannot make that mistake.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.xt_pkg.all;

entity tb_xt_vhdl is
  generic (DIRECTED_ONLY : boolean := false);
end entity;

architecture sim of tb_xt_vhdl is
  constant PERIODIC_MAX : natural := 1350;
  constant CONTROL_MIN  : natural := 150;
  constant N_EP         : natural := 8;

  signal clk          : std_logic := '0';
  signal rst_n        : std_logic := '0';
  signal req_valid    : std_logic := '0';
  signal req_type     : std_logic_vector(1 downto 0) := "11";
  signal req_maxp     : std_logic_vector(10 downto 0) := (others => '0');
  signal req_interval : std_logic_vector(7 downto 0) := x"01";
  signal rel_valid    : std_logic := '0';
  signal rel_slot     : std_logic_vector(3 downto 0) := (others => '0');

  signal adm_done, adm_ok : std_logic;
  signal adm_reason : std_logic_vector(2 downto 0);
  signal adm_slot   : std_logic_vector(3 downto 0);
  signal used_periodic, used_control, n_admitted : std_logic_vector(15 downto 0);
  signal n_req, n_reject_bw, n_reject_interval, n_reject_maxp,
         n_reject_full, n_overcommit : std_logic_vector(31 downto 0);

  signal done : boolean := false;
begin

  dut : entity work.usb_xfer_admit
    generic map (PERIODIC_MAX => PERIODIC_MAX, CONTROL_MIN => CONTROL_MIN,
                 N_EP => N_EP)
    port map (
      clk => clk, rst_n => rst_n,
      req_valid => req_valid, req_type => req_type,
      req_maxp => req_maxp, req_interval => req_interval,
      rel_valid => rel_valid, rel_slot => rel_slot,
      adm_done => adm_done, adm_ok => adm_ok,
      adm_reason => adm_reason, adm_slot => adm_slot,
      used_periodic => used_periodic, used_control => used_control,
      n_admitted => n_admitted,
      n_req => n_req, n_reject_bw => n_reject_bw,
      n_reject_interval => n_reject_interval, n_reject_maxp => n_reject_maxp,
      n_reject_full => n_reject_full, n_overcommit => n_overcommit);

  clk <= not clk after 5 ns when not done else '0';

  stim : process
    variable errors : natural := 0;
    variable checks : natural := 0;
    variable steps  : natural := 0;
    variable n_over : natural := 0;

    type used_arr is array (0 to N_EP-1) of boolean;
    variable s_used : used_arr := (others => false);
    variable s_mp   : mp_array(0 to N_EP-1) := (others => (others => '0'));
    variable s_ty   : ty_array(0 to N_EP-1) := (others => XT_CTRL);

    variable x_req, x_bw, x_iv, x_mp, x_full : natural := 0;
    variable g_req, g_bw, g_iv, g_mp, g_full, g_ok : natural := 0;

    variable reach   : std_logic_vector(0 to 143) := (others => '0');
    variable n_reach : natural := 0;

    variable rnd : unsigned(31 downto 0) := x"00051D7B";
    variable ln  : line;

    procedure ck(cond : boolean; what : string) is
    begin
      checks := checks + 1;
      if not cond then
        errors := errors + 1;
        if errors <= 20 then
          write(ln, string'("  ERROR step=") & integer'image(steps)
                & string'(": ") & what);
          writeline(output, ln);
        end if;
      end if;
    end procedure;

    -- boolean to std_logic: VHDL has no implicit conversion, and a
    -- conditional expression in argument position is VHDL-2019, not 2008.
    function sl_of(b : boolean) return std_logic is
    begin
      if b then return '1'; else return '0'; end if;
    end function;

    impure function nxt return natural is
    begin
      rnd := rnd xor (rnd sll 13);
      rnd := rnd xor (rnd srl 17);
      rnd := rnd xor (rnd sll 5);
      return to_integer(rnd(14 downto 0));
    end function;

    function fcost(mp : unsigned(10 downto 0)) return natural is
    begin
      return to_integer(mp) + TXN_OVERHEAD;
    end function;

    function maxp_of(t : xfer_t) return natural is
    begin
      if t = XT_ISO then return 1023; else return 64; end if;
    end function;

    -- VHDL has no bitwise AND on NATURAL, so the trick the design uses on
    -- an `unsigned` is not available here. Enumerating the eight legal
    -- 8-bit values is clearer than converting back and forth, and it is an
    -- independent formulation rather than a copy of the design's.
    function pow2(v : natural) return boolean is
    begin
      return v = 1 or v = 2 or v = 4 or v = 8
          or v = 16 or v = 32 or v = 64 or v = 128;
    end function;

    -- Re-sum the whole schedule from the admitted set. NEVER incremental.
    impure function sum_periodic return natural is
      variable t : natural := 0;
    begin
      for a in 0 to N_EP-1 loop
        if s_used(a) and is_periodic_t(s_ty(a)) then
          t := t + fcost(s_mp(a));
        end if;
      end loop;
      return t;
    end function;

    impure function sum_control return natural is
      variable t : natural := 0;
    begin
      for a in 0 to N_EP-1 loop
        if s_used(a) and s_ty(a) = XT_CTRL then
          t := t + fcost(s_mp(a));
        end if;
      end loop;
      return t;
    end function;

    impure function count_used return natural is
      variable t : natural := 0;
    begin
      for a in 0 to N_EP-1 loop
        if s_used(a) then t := t + 1; end if;
      end loop;
      return t;
    end function;

    impure function first_free return natural is
      variable f : natural := 0;
    begin
      for a in N_EP-1 downto 0 loop
        if not s_used(a) then f := a; end if;
      end loop;
      return f;
    end function;

    impure function have_free return boolean is
    begin
      for a in 0 to N_EP-1 loop
        if not s_used(a) then return true; end if;
      end loop;
      return false;
    end function;

    procedure step(rv : std_logic; rs : natural;
                   qv : std_logic; qt : xfer_t;
                   qm : natural; qi : natural) is
      variable e_ok   : boolean;
      variable e_why  : reason_t;
      variable e_slot : natural;
      variable e_per  : boolean;
      variable e_cost : natural;
    begin
      rel_valid <= rv;  rel_slot <= std_logic_vector(to_unsigned(rs, 4));
      req_valid <= qv;
      case qt is
        when XT_CTRL => req_type <= "00";
        when XT_ISO  => req_type <= "01";
        when XT_INT  => req_type <= "10";
        when XT_BULK => req_type <= "11";
      end case;
      req_maxp     <= std_logic_vector(to_unsigned(qm, 11));
      req_interval <= std_logic_vector(to_unsigned(qi, 8));

      -- release first, exactly as the design orders it
      if rv = '1' and rs < N_EP and s_used(rs) then s_used(rs) := false; end if;

      e_per  := is_periodic_t(qt);
      e_cost := qm + TXN_OVERHEAD;
      e_ok   := false;
      e_why  := RS_OK;
      e_slot := first_free;

      if qv = '1' then
        x_req := x_req + 1;  g_req := g_req + 1;
        if qm > maxp_of(qt) then
          e_why := RS_MAXP;  x_mp := x_mp + 1;  g_mp := g_mp + 1;
        elsif e_per and not pow2(qi) then
          e_why := RS_INTERVAL;  x_iv := x_iv + 1;  g_iv := g_iv + 1;
        elsif not have_free then
          e_why := RS_FULL;  x_full := x_full + 1;  g_full := g_full + 1;
        elsif e_per and (sum_periodic + e_cost) > PERIODIC_MAX then
          e_why := RS_BW;  x_bw := x_bw + 1;  g_bw := g_bw + 1;
        elsif qt = XT_CTRL and (sum_control + e_cost) > CONTROL_MIN then
          e_why := RS_CTRL_ROOM;  x_bw := x_bw + 1;  g_bw := g_bw + 1;
        else
          e_ok := true;  e_why := RS_OK;  g_ok := g_ok + 1;
          s_used(e_slot) := true;
          s_mp(e_slot)   := to_unsigned(qm, 11);
          s_ty(e_slot)   := qt;
        end if;
      end if;

      wait until rising_edge(clk);
      wait for 1 ns;
      steps := steps + 1;
      rel_valid <= '0';  req_valid <= '0';

      ck((adm_done = '1') = (qv = '1'),
         "adm_done disagrees with whether a request was made");
      if qv = '1' then
        ck((adm_ok = '1') = e_ok, "admission decision disagrees");
        ck(adm_reason = code_of(e_why), "rejection reason disagrees");
        if e_ok then
          ck(to_integer(unsigned(adm_slot)) = e_slot, "admitted into the wrong slot");
        end if;
      end if;

      ck(to_integer(unsigned(used_periodic)) = sum_periodic,
         "periodic commitment disagrees");
      ck(to_integer(unsigned(used_control))  = sum_control,
         "control commitment disagrees");
      ck(to_integer(unsigned(n_admitted))    = count_used,
         "admitted count disagrees");

      if to_integer(unsigned(used_periodic)) > PERIODIC_MAX then
        n_over := n_over + 1;
      end if;
      if to_integer(unsigned(used_control)) > CONTROL_MIN then
        n_over := n_over + 1;
      end if;
      ck(to_integer(unsigned(used_periodic)) <= PERIODIC_MAX,
         "periodic budget overcommitted");
      ck(to_integer(unsigned(used_control))  <= CONTROL_MIN,
         "control reservation overcommitted");
      ck(to_integer(unsigned(n_overcommit)) = 0,
         "the design detected its own overcommitment");
      ck(n_over = 0, "the schedule was overcommitted");

      ck(to_integer(unsigned(n_req))             = x_req,  "request count disagrees");
      ck(to_integer(unsigned(n_reject_bw))       = x_bw,   "bandwidth-rejection count disagrees");
      ck(to_integer(unsigned(n_reject_interval)) = x_iv,   "interval-rejection count disagrees");
      ck(to_integer(unsigned(n_reject_maxp))     = x_mp,   "maxp-rejection count disagrees");
      ck(to_integer(unsigned(n_reject_full))     = x_full, "slot-full-rejection count disagrees");
    end procedure;

    procedure reset_dut is
    begin
      rst_n <= '0';
      req_valid <= '0';  rel_valid <= '0';
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      s_used := (others => false);
      s_mp   := (others => (others => '0'));
      s_ty   := (others => XT_CTRL);
      x_req := 0; x_bw := 0; x_iv := 0; x_mp := 0; x_full := 0;
      wait until rising_edge(clk);
      wait for 1 ns;
    end procedure;

    -- Boundary values on purpose: 64/65 straddle the control and bulk limit,
    -- 1023/1024 straddle the isochronous one. 3 and 255 are not powers of two.
    type nat6 is array (0 to 5) of natural;
    constant mps  : nat6 := (8, 64, 65, 512, 1023, 1024);
    constant ivs  : nat6 := (1, 2, 3, 4, 8, 255);
    type ty4 is array (0 to 3) of xfer_t;
    constant tys : ty4 := (XT_CTRL, XT_ISO, XT_INT, XT_BULK);
    variable ri : natural;
    variable expect_n : natural;
  begin
    -- PHASE 1 (DIRECTED, EXHAUSTIVE) -- 4 types x 6 sizes x 6 intervals
    for ti in 0 to 3 loop
      for mi in 0 to 5 loop
        for ii in 0 to 5 loop
          reset_dut;
          step('0', 0, '1', tys(ti), mps(mi), ivs(ii));
          ri := ti*36 + mi*6 + ii;
          reach(ri) := '1';
        end loop;
      end loop;
    end loop;

    -- PHASE 2 (DIRECTED) -- fill the periodic budget, then ask for one more
    reset_dut;
    step('0', 0, '1', XT_ISO, 1023, 1);
    ck(adm_ok = '1', "the first isochronous endpoint was refused");
    step('0', 0, '1', XT_ISO, 1023, 1);
    ck(adm_ok = '0', "a second full-size isochronous endpoint was admitted");
    ck(adm_reason = code_of(RS_BW), "the refusal was not attributed to bandwidth");
    step('0', 0, '1', XT_INT, 8, 8);
    ck(adm_ok = '1', "a small interrupt endpoint did not fit in the remainder");
    for k in 0 to 4 loop
      step('0', 0, '1', XT_BULK, 64, 0);
      ck(adm_ok = '1', "a bulk endpoint was refused for bandwidth");
      ck(to_integer(unsigned(used_periodic)) = 1057,
         "bulk was charged periodic bandwidth");
    end loop;

    -- PHASE 3 (DIRECTED) -- the control reservation is SEPARATE
    reset_dut;
    step('0', 0, '1', XT_ISO, 1023, 1);
    step('0', 0, '1', XT_CTRL, 64, 0);
    ck(adm_ok = '1', "control was refused while its reservation was free");
    ck(to_integer(unsigned(used_control)) = 77,
       "control was charged to the wrong budget");
    step('0', 0, '1', XT_CTRL, 64, 0);
    ck(adm_ok = '0', "control exceeded its own reservation");
    ck(adm_reason = code_of(RS_CTRL_ROOM),
       "the refusal was not attributed to control room");

    -- PHASE 4 (DIRECTED, EXHAUSTIVE) -- release returns exactly what was charged
    for k in 0 to N_EP-1 loop
      reset_dut;
      for a in 0 to N_EP-1 loop
        step('0', 0, '1', XT_INT, 8, 4);
      end loop;
      ck(to_integer(unsigned(used_periodic)) = 168,
         "eight 8-byte interrupt endpoints cost the wrong amount");
      step('0', 0, '1', XT_INT, 8, 4);
      ck(adm_ok = '0', "a ninth endpoint was admitted into eight slots");
      ck(adm_reason = code_of(RS_FULL), "the refusal was not attributed to slots");
      step('1', k, '0', XT_BULK, 0, 0);
      ck(to_integer(unsigned(used_periodic)) = 147, "release returned the wrong amount");
      step('1', 0, '1', XT_INT, 8, 4);
    end loop;

    -- PHASE 4b (DIRECTED, EXHAUSTIVE) -- walk the periodic budget from empty
    -- to refusal, for every packet size.
    --
    -- The trade this chapter is about is arithmetic, so it deserves
    -- arithmetic stimulus: admit identical isochronous endpoints one at a
    -- time until the budget refuses one, checking at every step that the
    -- commitment is exactly the re-summed total and that the refusal arrives
    -- at precisely the right count. This is what catches a cost function that
    -- forgets the per-transaction overhead or charges the average frame.
    for mi in 0 to 5 loop
      if mps(mi) <= 1023 then
        reset_dut;
        expect_n := 0;
        while ((expect_n + 1) * (mps(mi) + TXN_OVERHEAD) <= PERIODIC_MAX)
              and expect_n < N_EP loop
          expect_n := expect_n + 1;
        end loop;

        for k in 0 to expect_n - 1 loop
          step('0', 0, '1', XT_ISO, mps(mi), 1);
          ck(adm_ok = '1', "an endpoint that fits the budget was refused");
          ck(to_integer(unsigned(used_periodic))
               = (k + 1) * (mps(mi) + TXN_OVERHEAD),
             "the running commitment is not the sum of the admitted costs");
        end loop;
        step('0', 0, '1', XT_ISO, mps(mi), 1);
        ck(adm_ok = '0', "one endpoint too many was admitted");
        if expect_n < N_EP then
          ck(adm_reason = code_of(RS_BW), "the refusal was not attributed to bandwidth");
        else
          ck(adm_reason = code_of(RS_FULL), "the refusal was not attributed to slots");
        end if;
        ck(to_integer(unsigned(used_periodic)) = expect_n * (mps(mi) + TXN_OVERHEAD),
           "a refused endpoint changed the commitment");
      end if;
    end loop;

    -- PHASE 5 (RANDOM)
    if not DIRECTED_ONLY then
      reset_dut;
      for k in 0 to 19999 loop
        step(sl_of(nxt mod 4 = 0), nxt mod 16,
             sl_of(nxt mod 2 = 0), tys(nxt mod 4),
             mps(nxt mod 6), ivs(nxt mod 6));
        if (k mod 256) = 255 then reset_dut; end if;
      end loop;
    end if;

    n_reach := 0;
    for i in 0 to 143 loop
      if reach(i) = '1' then n_reach := n_reach + 1; end if;
    end loop;

    write(ln, string'("steps=") & integer'image(steps)
          & string'(" checks=") & integer'image(checks)
          & string'(" reach=") & integer'image(n_reach) & string'("/144")
          & string'(" errors=") & integer'image(errors));
    writeline(output, ln);
    write(ln, string'("[sched] requests=") & integer'image(g_req)
          & string'(" admitted=") & integer'image(g_ok)
          & string'(" bw=") & integer'image(g_bw)
          & string'(" interval=") & integer'image(g_iv)
          & string'(" maxp=") & integer'image(g_mp)
          & string'(" full=") & integer'image(g_full));
    writeline(output, ln);
    write(ln, string'("[the whole point] overcommitted cycles = ")
          & integer'image(n_over));
    writeline(output, ln);
    if n_reach /= 144 then
      write(ln, string'("FAIL: exhaustive sweep incomplete"));
      writeline(output, ln);
      errors := errors + 1;
    end if;
    if errors = 0 then
      write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
            & string'(" checks"));
    else
      write(ln, string'("FAIL: ") & integer'image(errors)
            & string'(" errors in ") & integer'image(checks) & string'(" checks"));
    end if;
    writeline(output, ln);

    done <= true;
    wait;
  end process;

end architecture;

11. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
(type × packet size × interval) reached144 / 144144 / 144144 / 144
budget walks (empty to refusal)5 / 55 / 55 / 5
release-from-every-slot sweeps8 / 88 / 88 / 8
Steps202752027520275
Checks executed286696286696286778
admission requests102351023510299
admitted253225322486
refused — bandwidth or control room359359346
refused — illegal interval10021002993
refused — packet too large544454445547
refused — no slot898898927
overcommitted cycles000
ResultPASSPASSPASS

The exhaustive sweep uses boundary values rather than convenient ones: 64 and 65 straddle the control and bulk limit, 1023 and 1024 straddle the isochronous one, and 3 and 255 are the non-powers-of-two among the intervals. A sweep over 8, 64, 512 would have missed every off-by-one in the file.

12. Mutation Testing

#MutationVerilogSysVerVHDL
E7the average frame is charged instead of the worst146104146104129021
E5a periodic interval need not be a power of two768667686677752
E2the 90% periodic ceiling is dropped740667406673656
E6every type gets the isochronous packet limit643576435765648
E1bulk is charged periodic bandwidth606086060856323
E3control shares the periodic remainder574595745959472
E4the per-transaction overhead is ignored524395243953074
—unmutated baseline000

All seven die in all three languages.

E7 scores highest, and it is the most seductive error in the design. Dividing the cost by the interval looks exactly like the right thing to do — the endpoint really does move fewer bytes per frame on average — and the resulting schedule really does fit, on average. It then misses deadlines in the frames where the transactions actually land.

E1 is the mutation that inverts the chapter's thesis. Charging bulk for periodic bandwidth makes best-effort traffic compete with guaranteed traffic for the same budget, which defeats the purpose of having two categories: a disk copy would then be able to starve an audio stream, and an audio stream would be able to refuse a disk.

Directed against random

#V allV directedV randomVHDL allVHDL directedVHDL random
E160608150604585632315056173
E274066197738697365619773459
E357459129573305947212959343
E452439233522065307423352841
E5768667076796777527077682
E6643576664291656486665582
E7146104146145958129021146128875

Every directed column is identical across Verilog and VHDL.

The directed share here is about 0.2% — the smallest of any chapter in this module. That is not a weakness and it is worth being explicit about why: this design's state space is small and dense. Four types, a handful of sizes, one budget. Random stimulus walks into every interesting situation constantly, and the four arithmetic mutations (E1, E2, E4, E7) are wrong on almost every request. Compare chapter 26.3's burst splitter, where the directed share was 40% because its space was 8192 points that random stimulus visited sparsely.

The directed column's job is not to be large. It is to be non-zero for reasons you can name — and here every one of them is: the budget walk in phase 4b accounts for E1, E2, E4 and E7; the boundary sizes account for E6; the illegal intervals account for E5; the separate control reservation accounts for E3.

13. E6 Read 116,236 in VHDL Against 64,357 Elsewhere

The fifth out-of-line column in this module, and the fifth time it was the mutation rather than the design.

E6 gives every transfer type the isochronous packet limit. In the Verilog, max_packet is a case over all four types and the mutation changes one arm:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Verilog max_packet:          VHDL max_packet (before):

     case (t)                     if t = XT_ISO then 1023
       X_CTRL:  64    <- mutated   else               64
       X_ISO:   1023               end if
       X_INT:   64
       default: 64                E6 replaced the whole body,
     endcase                      so CTRL, INT and BULK all
                                  became 1023 -- three
     One arm changes.             mutations wearing one label.

The VHDL function had been written the compact way — one comparison instead of four cases — and the compact version has no separate arm to mutate. Rewriting it as a case over all four types brought E6 to 65,648 against 64,357, a 2% spread consistent with everything else in the table.

14. Property 7: A Release and a Request in the Same Cycle

The design releases a slot before it considers a request, and in source order that is exactly what it does. In hardware it did not, and the testbench caught it: 33,777 errors, admitted into the wrong slot.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   if (rel_valid && slot_used[rel_slot])   <- registered read
     slot_used[rel_slot] <= 1'b0;          <- takes effect NEXT cycle

   ... free_slot scans slot_used ...        <- still the OLD value

   So a driver that closes endpoint 3 and opens a new one in the
   same cycle is refused for want of a slot it just freed.

The fix is to compute the post-release state explicitly — used_now, per_now, ctl_now — and have everything that decides the request read those rather than the registers.

15. Choosing a Type: The Answer to the Actual Question

Use caseTypeThe reason, in one line
Audio / video streamIsochronousa late packet is worthless, so guarantee the slot and drop the retry
Keyboard / mouseInterruptbounded latency and reliability, bought by being small
Enumeration / configurationControlmust always be possible, hence its own reservation
Disk / printer / networkBulkevery byte must arrive; nobody promised when
Firmware updateBulkcorrectness over speed, absolutely
Sensor polled at 10 msInterrupt, interval 8reserved, retryable, and 64 bytes is plenty
Camera at 30 fpsIsochronousframe 31 is useless if frame 30 is late
Command / response to a deviceBulk pair, or Controlcontrol if it changes device state, bulk if it moves data

16. Follow-Ups the Interviewer Will Ask

"Why does isochronous have no retries?" Because every slot is reserved. A retry needs a slot, and there is not one that is not already promised to somebody.

"Is isochronous faster than bulk?" No. Bulk is faster on an idle bus — it can use the whole remainder. Isochronous is predictable, which is a different property.

"What happens to an isochronous packet with a CRC error?" It is discarded. The device is told nothing and the host does not retry. Applications are expected to conceal it — audio interpolates, video repeats a frame.

"Why is there a 90% cap rather than 100%?" So that control transfers, and bulk, are always possible. A bus that cannot be reconfigured cannot be recovered.

"Why must the interval be a power of two?" So the host can place the endpoint in a binary tree of frames. An interval of 3 is not schedulable however much bandwidth is free — mutation E5.

"Can an endpoint change type after configuration?" No. The type is in the endpoint descriptor, fixed for that configuration. A device that needs both offers two configurations, or two endpoints.

"What is different at high speed?" Microframes are 125 µs, so periodic endpoints can be polled eight times as often; the periodic cap becomes 80%; and high-speed bulk gains NYET so the host can avoid wasting a slot on a device that is not ready.

17. UVM: Checking a Budget Rather Than a Transaction

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A bandwidth budget is a PROPERTY OF A SET, not of a transaction, so this
// scoreboard does not have a queue of expected items. It has a model of the
// schedule, and its central check re-derives the whole commitment from the
// admitted set rather than tracking it incrementally -- because an
// incremental model drifts in the same direction as an incremental design
// and agrees with it perfectly.
typedef enum bit [1:0] { XT_CTRL, XT_ISO, XT_INT, XT_BULK } xfer_e;

class admit_req extends uvm_sequence_item;
  `uvm_object_utils(admit_req)

  rand xfer_e     xtype;
  rand bit [10:0] maxp;
  rand bit [7:0]  interval;
  rand bit        is_release;
  rand bit [3:0]  rel_slot;

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

  // Boundary values, not convenient ones. 64/65 straddle the control and
  // bulk limit; 1023/1024 straddle the isochronous one. A distribution over
  // 8, 64 and 512 would miss every off-by-one in the design.
  constraint c_boundaries { maxp inside {8, 64, 65, 512, 1023, 1024}; }

  // Illegal intervals on purpose: 3 and 255 are not powers of two, and the
  // rejection path is this block's most important output.
  constraint c_intervals { interval inside {1, 2, 3, 4, 8, 255}; }
endclass


class sched_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(sched_scoreboard)

  uvm_analysis_imp #(admit_req, sched_scoreboard) ap;

  localparam int PERIODIC_MAX  = 1350;   // 90% of a full-speed frame
  localparam int CONTROL_MIN   = 150;    // 10%, reserved
  localparam int TXN_OVERHEAD  = 13;     // token + PID + CRC + gap
  localparam int N_SLOT        = 8;

  // The admitted SET. Everything else is derived from it.
  bit        used   [N_SLOT];
  bit [10:0] mp     [N_SLOT];
  xfer_e     ty     [N_SLOT];

  int unsigned n_admit, n_refuse_bw, n_refuse_iv, n_refuse_mp, n_refuse_full;
  int unsigned n_at_ceiling;     // admissions that landed within 5% of the cap
  int unsigned n_bulk_admitted;

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

  // The cost of one transaction: the payload PLUS the per-transaction
  // overhead, charged at the FULL packet size rather than maxp/interval.
  // The schedule must survive the frame the transaction lands in, not the
  // average frame -- charging the average is how a schedule passes
  // admission control and then misses deadlines.
  function int cost_of(bit [10:0] m); return int'(m) + TXN_OVERHEAD; endfunction

  function bit periodic(xfer_e t); return (t == XT_ISO) || (t == XT_INT); endfunction

  // Re-summed, never incremental.
  function int sum_periodic();
    int s = 0;
    foreach (used[i]) if (used[i] && periodic(ty[i])) s += cost_of(mp[i]);
    return s;
  endfunction

  function int sum_control();
    int s = 0;
    foreach (used[i]) if (used[i] && ty[i] == XT_CTRL) s += cost_of(mp[i]);
    return s;
  endfunction

  function bit legal_interval(bit [7:0] v);
    return (v != 0) && ((v & (v - 1)) == 0);
  endfunction

  function int max_packet(xfer_e t);
    // A case over all four, not "ISO or not": the limits are independent in
    // the specification, and a design that expresses four as two cannot be
    // tested for the difference.
    case (t)
      XT_CTRL: return 64;
      XT_ISO:  return 1023;
      XT_INT:  return 64;
      XT_BULK: return 64;
    endcase
  endfunction

  function void write(admit_req t);
    int slot = -1;

    if (t.is_release) begin
      if (t.rel_slot < N_SLOT) used[t.rel_slot] = 1'b0;
      return;
    end

    foreach (used[i]) if (!used[i] && slot < 0) slot = i;

    // ---- the checks, most specific first, so the REASON is useful ----
    if (int'(t.maxp) > max_packet(t.xtype)) begin
      n_refuse_mp++;   return;
    end
    if (periodic(t.xtype) && !legal_interval(t.interval)) begin
      n_refuse_iv++;   return;
    end
    if (slot < 0) begin
      n_refuse_full++; return;
    end
    if (periodic(t.xtype) && (sum_periodic() + cost_of(t.maxp)) > PERIODIC_MAX) begin
      n_refuse_bw++;   return;
    end
    // Control's reservation is a SEPARATE limit, not the periodic
    // remainder: a fully committed periodic schedule must never be able to
    // stop the host talking to its devices, because control transfers are
    // how the schedule gets changed.
    if (t.xtype == XT_CTRL && (sum_control() + cost_of(t.maxp)) > CONTROL_MIN) begin
      n_refuse_bw++;   return;
    end

    used[slot] = 1'b1;
    mp[slot]   = t.maxp;
    ty[slot]   = t.xtype;
    n_admit++;
    if (t.xtype == XT_BULK) n_bulk_admitted++;
    if (sum_periodic() * 20 >= PERIODIC_MAX * 19) n_at_ceiling++;

    // ---- the invariants, asserted after every admission ----
    if (sum_periodic() > PERIODIC_MAX)
      `uvm_error("SCHED/BW",
        $sformatf("periodic commitment %0d exceeds the %0d-byte ceiling: every isochronous endpoint in this schedule can now miss its deadline",
                  sum_periodic(), PERIODIC_MAX))
    if (sum_control() > CONTROL_MIN)
      `uvm_error("SCHED/CTRL",
        $sformatf("control commitment %0d exceeds its %0d-byte reservation: the host may lose the ability to reconfigure the bus",
                  sum_control(), CONTROL_MIN))
  endfunction

  function void check_phase(uvm_phase phase);
    super.check_phase(phase);

    `uvm_info("SCHED",
      $sformatf("%0d admitted (%0d bulk) | refused: %0d bw, %0d interval, %0d maxp, %0d full",
                n_admit, n_bulk_admitted, n_refuse_bw, n_refuse_iv,
                n_refuse_mp, n_refuse_full), UVM_LOW)

    // A budget that is never approached is a budget that was never tested.
    if (n_at_ceiling == 0)
      `uvm_error("SCHED/COV",
        "the periodic commitment never came within 5% of its ceiling: the budget was never under pressure")
    if (n_refuse_bw == 0)
      `uvm_error("SCHED/COV",
        "nothing was ever refused for bandwidth: the 90% rule was never exercised")
    if (n_bulk_admitted == 0)
      `uvm_error("SCHED/COV",
        "no bulk endpoint was ever admitted: the claim that bulk is charged nothing was never tested")
  endfunction
endclass

18. Common Misconceptions

"Isochronous is fast." It is predictable. Bulk is faster on an idle bus.

"Isochronous is unreliable because of poor error handling." It is unreliable because it is guaranteed. A retry needs a slot, and every slot is promised.

"Bulk is slow." Bulk is unbounded, which is different. It gets the whole remainder of every frame and can be the fastest thing on the bus.

"Interrupt transfers interrupt." They are polled. The host promises to ask at least every N frames.

"Interrupt is just small bulk." It has a reservation, which bulk does not. That reservation is why it is capped at 64 bytes.

"Control shares the periodic budget." It has its own 10%, so that a full schedule can still be changed.

"An endpoint's cost is its packet size." It is the packet size plus about 13 bytes. For an 8-byte endpoint the overhead is 62% of the cost.

"Charge the average bytes per frame." Charge the worst frame. A schedule sized on averages misses deadlines — the highest-scoring mutation here.

"Any polling interval works." It must be a power of two, so the host can place it in a binary tree of frames.

"A refused request that succeeds on retry is not a bug." It is a bandwidth limit that appears only under load and cannot be reproduced.

19. Exercises

1. A device streams 48 kHz 16-bit stereo audio. Compute the bytes per millisecond, choose a transfer type, and show the arithmetic that rules out the other three.

2. Derive the 90%/10% split from first principles: construct the deadlock that a shared budget permits, and show the split makes it unreachable.

3. E7 charges cost/interval for a periodic endpoint. Construct a schedule that passes admission control under E7 and then misses a deadline, and give the frame in which it happens.

4. E4 ignores the 13-byte overhead. Compute the oversubscription factor for a schedule of 8-byte interrupt endpoints, and for one of 1023-byte isochronous endpoints. Explain the difference.

5. Property 7 requires a same-cycle release and request to both succeed. Explain why failing it is not a functional bug, and why that makes it harder to find than one that is.

6. The VHDL max_packet was rewritten from one comparison to a four-arm case purely for mutability. Argue the general principle, and give a case where you would not apply it.

7. Add high-speed support: 125 µs microframes, an 80% periodic cap, and NYET. Which of the seven properties change, which do not, and what new one is needed?

20. Summary

IdeaWhy it matters
Guaranteed bandwidth is no retriesthe same resource, spent two ways
Isochronous is predictable, not fastbulk is faster on an idle bus
Interrupt buys its retry by being small64 bytes, and a reservation per N frames
Bulk is charged nothingwhich is exactly why it can retry
Control has its own 10%so a full schedule can still be changed
The cap is 90%, not 100%a bus that cannot be reconfigured cannot recover
A transaction costs payload + ~13 bytes62% overhead on an 8-byte endpoint
Charge the worst frame, not the averagethe highest-scoring mutation in the chapter
Intervals are powers of twoso the host can place them in a frame tree
The rejection reason is the real output"refused" is useless to a driver author
Compute the post-release state explicitlyor a same-cycle release/request is refused
A retry that hides a bug is worse than a failureunreproducible, and closed as cannot-reproduce
Compact code is not mutable codefour independent limits need four arms
Sweep boundary values, not convenient ones64/65 and 1023/1024, not 8/64/512
144 states, 7 mutations, 3 languages0 overcommitted cycles in 286,696 checks

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o xt_v.out xt_v.v xt_v_tb.v && ./xt_v.out
SystemVerilogiverilog -g2012 -o xt_sv.out xt_sv.sv xt_sv_tb.sv && ./xt_sv.out
VHDL-2008 analysenvc --std=2008 -a xt_vhdl.vhd xt_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_xt_vhdl
VHDL-2008 runnvc --std=2008 -r tb_xt_vhdl
One mutationiverilog -g2005 -DMUT_E7 -o mm xt_v_mut.v xt_v_tb.v && ./mm
Directed only (Verilog)iverilog -g2005 -DDIRECTED_ONLY -o mm xt_v_mut.v xt_v_tb.v && ./mm
Directed only (VHDL)nvc --std=2008 -e -gDIRECTED_ONLY=true tb_xt_vhdl

All three implementations pass with 0 errors: all 144 combinations of transfer type, packet size and interval, using boundary values throughout; the periodic budget walked from empty to refusal for every packet size; a release from every slot; zero overcommitted cycles in 286,696 checks; and every one of the seven mutations killed by directed stimulus alone, with all seven directed scores identical across languages.


Chapter 27.6 — The Scheduling Question takes this budget and turns it into a timeline. Admission control answers can these endpoints coexist; scheduling answers in what order, in this frame — and its central rule is that periodic traffic is placed first, before anything that merely wants to go fast.

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.