Skip to content
VLSI Mentor

USB · Module 27

The Scheduling Question

Periodic traffic is placed first because that is how admission control's promise is kept — and bulk is round-robin because a rotating pointer is the only fairness mechanism in the entire schedule.

Chapter 27.5 answered can these endpoints coexist. This chapter answers the next question, which is the one that gets asked at senior level: in what order, in this frame.

1. The Question

"Walk me through how a USB 2.0 host schedules a frame across the four transfer types."

The answer has two halves. Almost everybody gets the first and almost nobody volunteers the second.

2. Why That Order, Specifically

Each position in the sequence is forced by a property from chapter 27.5.

Isochronous first, because it cannot retry. An isochronous transaction that does not happen in its frame does not happen. Every other type can be deferred to a later frame; this one cannot, so it is placed while the frame is empty.

Interrupt second, because it can be deferred — within its interval. It has a reservation, so it must be placed before best-effort traffic; it can retry, so it does not need to be placed before the type that cannot. It goes exactly between them.

Control third, before bulk rather than competing with it. Control transfers are how the host changes the schedule. A frame packed with bulk that leaves no room for control is a bus that cannot be reconfigured — the same deadlock argument that gives control its own 10% reservation, applied to placement instead of budget.

Bulk last, and round-robin. It has no deadline, so it goes wherever there is room. It has no reservation, so nothing protects one bulk endpoint from another, so the placement order among them must rotate.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   The frame, in order:

     ISO   -- cannot retry        -> place while the frame is empty
     INT   -- can retry, reserved -> after ISO, before best-effort
     CTRL  -- must always fit     -> before bulk, so the bus stays
                                     reconfigurable
     BULK  -- no deadline at all  -> the remainder, ROUND-ROBIN

   Each line's position is forced by a property, not chosen.

3. What We Are Building

usb_frame_sched walks that sequence as a phase machine and emits one schedule entry per cycle. Eight endpoint slots, a 1500-byte full-speed frame, and a rotating bulk pointer that survives across frames.

The four phases of a frame

A frame scheduler phase machine that places isochronous endpoints first, then interrupt, then control, then bulk using a rotating pointer, before ending the frameframe_tickP_ISOP_INTP_CTRLP_BULKrr pointerP_ENDstartthenthenremainderadvances on servenext frame12
The phase order is the answer to the question. Bulk is reached only after everything with a reservation has been placed, and it walks its slots from a pointer that carries over between frames.

One frame, in order, with bulk taking what is left

Within one frame the scheduler emits an isochronous entry, then an interrupt entry, then a control entry, then a single bulk entry, with the running byte total rising to the frame budgetreserved traffic firstreserved traffic firstbulk on what is leftbulk on what is leftISO placed into an empty frameISO placed into an emptyframeinterrupt nextinterrupt nextcontrol before bulkcontrol before bulkbulk takes the remainderbulk takes the remainderclkframe_tickphaseIDLEISOISOINTINTCTRLCTRLBULKBULKENDent_valident_type00ISOISOINTINTCTRLCTRLBULKBULKent_bytes001036103677777777310310frame_used0010361036111311131190119015001500t0t1t2t3t4t5t6t7t8t9
The isochronous endpoint is placed while the frame is empty. Bulk is reached last and fits once; the 1500-byte budget then refuses the next one.

frame_used only ever rises, and it stops at 1500. The order in which it rises is the answer to the question.

4. Seven Properties

#Property
1Within a frame, no entry precedes one of higher priority.
2Each entry is charged its packet size plus the per-transaction overhead.
3A frame never exceeds its byte budget.
4Every periodic endpoint that is due and fits is served in that frame.
5A periodic endpoint is served only when due.
6No periodic endpoint ever misses a deadline.
7A bulk endpoint's wait is bounded while at least one fits per frame.

Properties 4 and 5 are a pair and both are necessary. A scheduler that places nothing satisfies 1, 2, 3 and 6 perfectly. A scheduler that places everything every frame satisfies 1, 2, 4 and 6 and blows the budget it was admitted under. Section 12 is about what happened when only one of the pair existed.

5. Verilog-2005 RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_frame_sched -- "Describe USB 2.0 host scheduling" in hardware.
//
//  Chapter 27.5's admission control answered "can these endpoints
//  coexist". This answers the next question: "in what order, in THIS
//  frame" -- and the order is the whole answer:
//
//      Periodic traffic is placed FIRST, before anything that merely
//      wants to go fast. Bulk gets the remainder, and only the
//      remainder.
//
//  That is not a fairness policy. It is the mechanism by which the
//  guarantee made at admission time is actually kept: an endpoint
//  promised a slot in every frame gets it because it is placed before
//  the traffic that would otherwise fill the frame.
//
//  The second half of the answer is the one candidates omit: bulk is
//  served ROUND-ROBIN. Bulk has no reservation, so nothing stops one
//  bulk endpoint from taking the whole remainder of every frame forever
//  -- nothing except a rotating pointer, which is the only fairness
//  mechanism in the entire schedule.
// =====================================================================
module usb_frame_sched #(
  parameter integer FRAME_BYTES = 1500,
  parameter integer N_SLOT      = 8,
  parameter integer TXN_OH      = 13     // per-transaction overhead
) (
  input  wire        clk,
  input  wire        rst_n,

  // ---- a new frame begins ----
  input  wire        frame_tick,

  // ---- the endpoint table ----
  input  wire        cfg_wr,
  input  wire [2:0]  cfg_slot,
  input  wire [1:0]  cfg_type,     // X_CTRL / X_ISO / X_INT / X_BULK
  input  wire [10:0] cfg_maxp,
  input  wire [7:0]  cfg_interval, // frames between polls (periodic)
  input  wire        cfg_enable,

  // ---- the emitted schedule, one entry per cycle ----
  output wire        ent_valid,
  output wire [2:0]  ent_slot,
  output wire [1:0]  ent_type,
  output wire [15:0] ent_bytes,
  output wire        frame_busy,
  output wire [15:0] frame_used,
  output wire [15:0] frame_num,

  // ---- observability ----
  output wire [31:0] n_frames,
  output wire [31:0] n_entries,
  output wire [31:0] n_periodic,
  output wire [31:0] n_bulk,
  output wire [31:0] n_missed,      // must always read 0
  output wire [31:0] n_overrun,     // must always read 0
  output wire [31:0] n_starved      // bulk slots passed over while due
);

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

  // ---- the phases of a frame, in the order that keeps the promise ----
  localparam [2:0] P_IDLE = 3'd0,
                   P_ISO  = 3'd1,
                   P_INT  = 3'd2,
                   P_CTRL = 3'd3,
                   P_BULK = 3'd4,
                   P_END  = 3'd5;

  reg [1:0]  ty   [0:N_SLOT-1];
  reg [10:0] mp   [0:N_SLOT-1];
  reg [7:0]  iv   [0:N_SLOT-1];
  reg        en   [0:N_SLOT-1];
  // Frames since this endpoint was last served. Compared against its
  // interval to detect a missed deadline, which is the only thing in this
  // design that a schedule can get wrong without producing a wrong byte.
  reg [15:0] age  [0:N_SLOT-1];

  reg [2:0]  ph;
  reg [2:0]  scan;        // which slot the current phase is examining
  reg [15:0] used_r;
  reg [15:0] fnum_r;
  // ---- the round-robin pointer, and why it needs TWO registers ----
  //
  // A single pointer that advances once per examined slot advances N_SLOT
  // times per frame and therefore returns to exactly where it started --
  // it never rotates at all, and the same bulk endpoint wins every frame
  // forever. The bug is invisible in one frame and total over many.
  //
  // So rr_r is where the NEXT frame starts, updated only when an endpoint
  // is actually served, and bidx is the index the current frame is walking.
  reg [2:0]  rr_r;
  reg [2:0]  bidx;
  reg [2:0]  rr_seen;

  reg        ev_r;
  reg [2:0]  es_r;
  reg [1:0]  et_r;
  reg [15:0] eb_r;

  reg [31:0] fr_c, ent_c, per_c, blk_c, miss_c, over_c, starv_c;

  assign ent_valid  = ev_r;
  assign ent_slot   = es_r;
  assign ent_type   = et_r;
  assign ent_bytes  = eb_r;
  assign frame_busy = (ph != P_IDLE);
  assign frame_used = used_r;
  assign frame_num  = fnum_r;

  assign n_frames   = fr_c;
  assign n_entries  = ent_c;
  assign n_periodic = per_c;
  assign n_bulk     = blk_c;
  assign n_missed   = miss_c;
  assign n_overrun  = over_c;
  assign n_starved  = starv_c;

  // ---- is a periodic endpoint due in this frame? ----
  //
  // The interval is a power of two, so "every Nth frame" is a mask test
  // rather than a division. An endpoint with interval 1 is due every
  // frame; one with interval 8 is due when the low three bits are zero.
  function due_now;
    input [7:0]  interval;
    input [15:0] f;
    begin
      if (interval <= 8'd1) due_now = 1'b1;
      else                  due_now = ((f & {8'd0, (interval - 8'd1)}) == 16'd0);
    end
  endfunction

  function [15:0] cost_of;
    input [10:0] m;
    begin cost_of = {5'd0, m} + TXN_OH[15:0]; end
  endfunction

  integer k;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      ph      <= P_IDLE;
      scan    <= 3'd0;
      used_r  <= 16'd0;
      fnum_r  <= 16'd0;
      rr_r    <= 3'd0;
      bidx    <= 3'd0;
      rr_seen <= 3'd0;
      ev_r    <= 1'b0;
      es_r    <= 3'd0;
      et_r    <= X_CTRL;
      eb_r    <= 16'd0;
      fr_c    <= 32'd0;
      ent_c   <= 32'd0;
      per_c   <= 32'd0;
      blk_c   <= 32'd0;
      miss_c  <= 32'd0;
      over_c  <= 32'd0;
      starv_c <= 32'd0;
      for (k = 0; k < N_SLOT; k = k + 1) begin
        ty[k]  <= X_CTRL;
        mp[k]  <= 11'd0;
        iv[k]  <= 8'd1;
        en[k]  <= 1'b0;
        age[k] <= 16'd0;
      end
    end else begin
      ev_r <= 1'b0;

      // ---- table writes are only accepted between frames ----
      //
      // Reconfiguring mid-frame would change the schedule the host is
      // already executing, and the endpoint that had already been placed
      // would be served under its old parameters. The host does this
      // between frames, so the design refuses to do it any other way.
      if (cfg_wr && (ph == P_IDLE)) begin
        ty[cfg_slot] <= cfg_type;
        mp[cfg_slot] <= cfg_maxp;
        iv[cfg_slot] <= cfg_interval;
        en[cfg_slot] <= cfg_enable;
        age[cfg_slot] <= 16'd0;
      end

      case (ph)
        // =============================================================
        P_IDLE: begin
          if (frame_tick) begin
            ph      <= P_ISO;
            scan    <= 3'd0;
            used_r  <= 16'd0;
            rr_seen <= 3'd0;
            fr_c    <= fr_c + 32'd1;
            // Every enabled periodic endpoint ages by one frame. An
            // endpoint served this frame will have its age cleared below.
            for (k = 0; k < N_SLOT; k = k + 1)
              if (en[k] && ((ty[k] == X_ISO) || (ty[k] == X_INT)))
                age[k] <= age[k] + 16'd1;
          end
        end

        // =============================================================
        //  ISOCHRONOUS FIRST. It cannot retry, so it cannot be deferred:
        //  an isochronous transaction that does not happen in its frame
        //  does not happen at all.
        // =============================================================
        P_ISO: begin
          if (scan == N_SLOT - 1) ph <= P_INT;
          scan <= scan + 3'd1;

          if (en[scan] && (ty[scan] == X_ISO) && due_now(iv[scan], fnum_r)
              && ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
            ev_r   <= 1'b1;
            es_r   <= scan;
            et_r   <= X_ISO;
            eb_r   <= cost_of(mp[scan]);
            used_r <= used_r + cost_of(mp[scan]);
            age[scan] <= 16'd0;
            ent_c  <= ent_c + 32'd1;
            per_c  <= per_c + 32'd1;
          end
        end

        // =============================================================
        //  INTERRUPT SECOND. Reserved, but retryable, so it may be
        //  deferred within its interval -- which is exactly why it is
        //  placed after isochronous rather than before.
        // =============================================================
        P_INT: begin
          if (scan == N_SLOT - 1) ph <= P_CTRL;
          scan <= scan + 3'd1;

          if (en[scan] && (ty[scan] == X_INT) && due_now(iv[scan], fnum_r)
              && ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
            ev_r   <= 1'b1;
            es_r   <= scan;
            et_r   <= X_INT;
            eb_r   <= cost_of(mp[scan]);
            used_r <= used_r + cost_of(mp[scan]);
            age[scan] <= 16'd0;
            ent_c  <= ent_c + 32'd1;
            per_c  <= per_c + 32'd1;
          end
        end

        // =============================================================
        //  CONTROL THIRD, and it is placed before bulk rather than
        //  competing with it. Control is how the host changes the
        //  schedule; a frame full of bulk that leaves no room for
        //  control is a bus that cannot be reconfigured.
        // =============================================================
        P_CTRL: begin
          if (scan == N_SLOT - 1) begin
            ph   <= P_BULK;
            bidx <= rr_r;        // resume where the last frame left off
          end
          scan <= scan + 3'd1;

          if (en[scan] && (ty[scan] == X_CTRL)
              && ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
            ev_r   <= 1'b1;
            es_r   <= scan;
            et_r   <= X_CTRL;
            eb_r   <= cost_of(mp[scan]);
            used_r <= used_r + cost_of(mp[scan]);
            ent_c  <= ent_c + 32'd1;
          end
        end

        // =============================================================
        //  BULK LAST, ROUND-ROBIN, until the frame is full.
        //
        //  The rotating pointer is the ONLY fairness mechanism in the
        //  whole schedule. Bulk has no reservation, so without it one
        //  bulk endpoint would take the entire remainder of every frame
        //  forever and the others would never move a byte -- with no
        //  error reported anywhere, because nothing was promised.
        // =============================================================
        P_BULK: begin
          if (rr_seen == N_SLOT - 1) ph <= P_END;
          rr_seen <= rr_seen + 3'd1;
          bidx    <= bidx + 3'd1;

          if (en[bidx] && (ty[bidx] == X_BULK)) begin
            if ((used_r + cost_of(mp[bidx])) <= FRAME_BYTES) begin
              ev_r   <= 1'b1;
              es_r   <= bidx;
              et_r   <= X_BULK;
              eb_r   <= cost_of(mp[bidx]);
              used_r <= used_r + cost_of(mp[bidx]);
              ent_c  <= ent_c + 32'd1;
              blk_c  <= blk_c + 32'd1;
              // The next frame starts AFTER the last one served, which is
              // what makes the rotation real.
              rr_r   <= bidx + 3'd1;
            end else begin
              // Passed over for want of room. Counted, because "the frame
              // was full" and "this endpoint is being starved" look the
              // same in one frame and are different over many.
              starv_c <= starv_c + 32'd1;
            end
          end
        end

        // =============================================================
        P_END: begin
          ph     <= P_IDLE;
          fnum_r <= fnum_r + 16'd1;

          // ---- the deadline check, once per frame ----
          //
          // An endpoint whose age has passed its interval has missed a
          // deadline it was promised. Unreachable on a correct schedule,
          // counted so a run can publish zero.
          for (k = 0; k < N_SLOT; k = k + 1)
            if (en[k] && ((ty[k] == X_ISO) || (ty[k] == X_INT))
                && (age[k] > {8'd0, iv[k]}))
              miss_c <= miss_c + 32'd1;

          if (used_r > FRAME_BYTES) over_c <= over_c + 32'd1;
        end

        default: ph <= P_IDLE;
      endcase
    end
  end

endmodule

6. SystemVerilog RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_frame_sched -- SystemVerilog.
//
//  The frame phases become a named enumeration, which is worth more here
//  than in most designs: the ORDER of those phases IS the answer to the
//  interview question, and `P_ISO -> P_INT -> P_CTRL -> P_BULK` in a
//  waveform says the whole thing at a glance.
//
//  Chapter 27.5's admission control answered "can these endpoints
//  coexist". This answers the next question: "in what order, in THIS
//  frame" -- and the order is the whole answer:
//
//      Periodic traffic is placed FIRST, before anything that merely
//      wants to go fast. Bulk gets the remainder, and only the
//      remainder.
//
//  That is not a fairness policy. It is the mechanism by which the
//  guarantee made at admission time is actually kept: an endpoint
//  promised a slot in every frame gets it because it is placed before
//  the traffic that would otherwise fill the frame.
//
//  The second half of the answer is the one candidates omit: bulk is
//  served ROUND-ROBIN. Bulk has no reservation, so nothing stops one
//  bulk endpoint from taking the whole remainder of every frame forever
//  -- nothing except a rotating pointer, which is the only fairness
//  mechanism in the entire schedule.
// =====================================================================
module usb_frame_sched #(
  parameter int FRAME_BYTES = 1500,
  parameter int N_SLOT      = 8,
  parameter int TXN_OH      = 13     // per-transaction overhead
) (
  input  logic       clk,
  input  logic       rst_n,

  // ---- a new frame begins ----
  input  logic       frame_tick,

  // ---- the endpoint table ----
  input  logic       cfg_wr,
  input  logic [2:0] cfg_slot,
  input  logic [1:0] cfg_type,     // X_CTRL / X_ISO / X_INT / X_BULK
  input  logic [10:0]cfg_maxp,
  input  logic [7:0] cfg_interval, // frames between polls (periodic)
  input  logic       cfg_enable,

  // ---- the emitted schedule, one entry per cycle ----
  output logic       ent_valid,
  output logic [2:0] ent_slot,
  output logic [1:0] ent_type,
  output logic [15:0]ent_bytes,
  output logic       frame_busy,
  output logic [15:0]frame_used,
  output logic [15:0]frame_num,

  // ---- observability ----
  output logic [31:0]n_frames,
  output logic [31:0]n_entries,
  output logic [31:0]n_periodic,
  output logic [31:0]n_bulk,
  output logic [31:0]n_missed,      // must always read 0
  output logic [31:0]n_overrun,     // must always read 0
  output logic [31:0]n_starved      // bulk slots passed over while due
);

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

  // ---- the phases of a frame, in the order that keeps the promise ----
  // The ORDER of these is the answer to the interview question, and a
  // waveform showing P_ISO -> P_INT -> P_CTRL -> P_BULK says it at a glance.
  typedef enum logic [2:0] {
    P_IDLE = 3'd0,
    P_ISO  = 3'd1,
    P_INT  = 3'd2,
    P_CTRL = 3'd3,
    P_BULK = 3'd4,
    P_END  = 3'd5
  } phase_e;

  xfer_e     ty   [N_SLOT];
  logic [10:0] mp [N_SLOT];
  logic [7:0] iv  [N_SLOT];
  logic      en   [N_SLOT];
  // Frames since this endpoint was last served. Compared against its
  // interval to detect a missed deadline, which is the only thing in this
  // design that a schedule can get wrong without producing a wrong byte.
  logic [15:0] age [N_SLOT];

  phase_e    ph;
  logic [2:0] scan;        // which slot the current phase is examining
  logic [15:0] used_r;
  logic [15:0] fnum_r;
  // ---- the round-robin pointer, and why it needs TWO registers ----
  //
  // A single pointer that advances once per examined slot advances N_SLOT
  // times per frame and therefore returns to exactly where it started --
  // it never rotates at all, and the same bulk endpoint wins every frame
  // forever. The bug is invisible in one frame and total over many.
  //
  // So rr_r is where the NEXT frame starts, updated only when an endpoint
  // is actually served, and bidx is the index the current frame is walking.
  logic [2:0] rr_r;
  logic [2:0] bidx;
  logic [2:0] rr_seen;

  logic      ev_r;
  logic [2:0] es_r;
  xfer_e     et_r;
  logic [15:0] eb_r;

  logic [31:0] fr_c, ent_c, per_c, blk_c, miss_c, over_c, starv_c;

  assign ent_valid  = ev_r;
  assign ent_slot   = es_r;
  assign ent_type   = et_r;
  assign ent_bytes  = eb_r;
  assign frame_busy = (ph != P_IDLE);
  assign frame_used = used_r;
  assign frame_num  = fnum_r;

  assign n_frames   = fr_c;
  assign n_entries  = ent_c;
  assign n_periodic = per_c;
  assign n_bulk     = blk_c;
  assign n_missed   = miss_c;
  assign n_overrun  = over_c;
  assign n_starved  = starv_c;

  // ---- is a periodic endpoint due in this frame? ----
  //
  // The interval is a power of two, so "every Nth frame" is a mask test
  // rather than a division. An endpoint with interval 1 is due every
  // frame; one with interval 8 is due when the low three bits are zero.
  function automatic logic due_now(logic [7:0] interval, logic [15:0] f);
    begin
      if (interval <= 8'd1) due_now = 1'b1;
      else                  due_now = ((f & {8'd0, (interval - 8'd1)}) == 16'd0);
    end
  endfunction

  function automatic logic [15:0] cost_of(logic [10:0] m);
    return {5'd0, m} + 16'(TXN_OH);
  endfunction


  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      ph      <= P_IDLE;
      scan    <= 3'd0;
      used_r  <= 16'd0;
      fnum_r  <= 16'd0;
      rr_r    <= 3'd0;
      bidx    <= 3'd0;
      rr_seen <= 3'd0;
      ev_r    <= 1'b0;
      es_r    <= 3'd0;
      et_r    <= X_CTRL;
      eb_r    <= 16'd0;
      fr_c    <= 32'd0;
      ent_c   <= 32'd0;
      per_c   <= 32'd0;
      blk_c   <= 32'd0;
      miss_c  <= 32'd0;
      over_c  <= 32'd0;
      starv_c <= 32'd0;
      for (int k = 0; k < N_SLOT; k++) begin
        ty[k]  <= X_CTRL;
        mp[k]  <= 11'd0;
        iv[k]  <= 8'd1;
        en[k]  <= 1'b0;
        age[k] <= 16'd0;
      end
    end else begin
      ev_r <= 1'b0;

      // ---- table writes are only accepted between frames ----
      //
      // Reconfiguring mid-frame would change the schedule the host is
      // already executing, and the endpoint that had already been placed
      // would be served under its old parameters. The host does this
      // between frames, so the design refuses to do it any other way.
      if (cfg_wr && (ph == P_IDLE)) begin
        ty[cfg_slot] <= xfer_e'(cfg_type);
        mp[cfg_slot] <= cfg_maxp;
        iv[cfg_slot] <= cfg_interval;
        en[cfg_slot] <= cfg_enable;
        age[cfg_slot] <= 16'd0;
      end

      case (ph)
        // =============================================================
        P_IDLE: begin
          if (frame_tick) begin
            ph      <= P_ISO;
            scan    <= 3'd0;
            used_r  <= 16'd0;
            rr_seen <= 3'd0;
            fr_c    <= fr_c + 32'd1;
            // Every enabled periodic endpoint ages by one frame. An
            // endpoint served this frame will have its age cleared below.
            for (int k = 0; k < N_SLOT; k++)
              if (en[k] && ((ty[k] == X_ISO) || (ty[k] == X_INT)))
                age[k] <= age[k] + 16'd1;
          end
        end

        // =============================================================
        //  ISOCHRONOUS FIRST. It cannot retry, so it cannot be deferred:
        //  an isochronous transaction that does not happen in its frame
        //  does not happen at all.
        // =============================================================
        P_ISO: begin
          if (scan == N_SLOT - 1) ph <= P_INT;
          scan <= scan + 3'd1;

          if (en[scan] && (ty[scan] == X_ISO) && due_now(iv[scan], fnum_r)
              && ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
            ev_r   <= 1'b1;
            es_r   <= scan;
            et_r   <= X_ISO;
            eb_r   <= cost_of(mp[scan]);
            used_r <= used_r + cost_of(mp[scan]);
            age[scan] <= 16'd0;
            ent_c  <= ent_c + 32'd1;
            per_c  <= per_c + 32'd1;
          end
        end

        // =============================================================
        //  INTERRUPT SECOND. Reserved, but retryable, so it may be
        //  deferred within its interval -- which is exactly why it is
        //  placed after isochronous rather than before.
        // =============================================================
        P_INT: begin
          if (scan == N_SLOT - 1) ph <= P_CTRL;
          scan <= scan + 3'd1;

          if (en[scan] && (ty[scan] == X_INT) && due_now(iv[scan], fnum_r)
              && ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
            ev_r   <= 1'b1;
            es_r   <= scan;
            et_r   <= X_INT;
            eb_r   <= cost_of(mp[scan]);
            used_r <= used_r + cost_of(mp[scan]);
            age[scan] <= 16'd0;
            ent_c  <= ent_c + 32'd1;
            per_c  <= per_c + 32'd1;
          end
        end

        // =============================================================
        //  CONTROL THIRD, and it is placed before bulk rather than
        //  competing with it. Control is how the host changes the
        //  schedule; a frame full of bulk that leaves no room for
        //  control is a bus that cannot be reconfigured.
        // =============================================================
        P_CTRL: begin
          if (scan == N_SLOT - 1) begin
            ph   <= P_BULK;
            bidx <= rr_r;        // resume where the last frame left off
          end
          scan <= scan + 3'd1;

          if (en[scan] && (ty[scan] == X_CTRL)
              && ((used_r + cost_of(mp[scan])) <= FRAME_BYTES)) begin
            ev_r   <= 1'b1;
            es_r   <= scan;
            et_r   <= X_CTRL;
            eb_r   <= cost_of(mp[scan]);
            used_r <= used_r + cost_of(mp[scan]);
            ent_c  <= ent_c + 32'd1;
          end
        end

        // =============================================================
        //  BULK LAST, ROUND-ROBIN, until the frame is full.
        //
        //  The rotating pointer is the ONLY fairness mechanism in the
        //  whole schedule. Bulk has no reservation, so without it one
        //  bulk endpoint would take the entire remainder of every frame
        //  forever and the others would never move a byte -- with no
        //  error reported anywhere, because nothing was promised.
        // =============================================================
        P_BULK: begin
          if (rr_seen == N_SLOT - 1) ph <= P_END;
          rr_seen <= rr_seen + 3'd1;
          bidx    <= bidx + 3'd1;

          if (en[bidx] && (ty[bidx] == X_BULK)) begin
            if ((used_r + cost_of(mp[bidx])) <= FRAME_BYTES) begin
              ev_r   <= 1'b1;
              es_r   <= bidx;
              et_r   <= X_BULK;
              eb_r   <= cost_of(mp[bidx]);
              used_r <= used_r + cost_of(mp[bidx]);
              ent_c  <= ent_c + 32'd1;
              blk_c  <= blk_c + 32'd1;
              // The next frame starts AFTER the last one served, which is
              // what makes the rotation real.
              rr_r   <= bidx + 3'd1;
            end else begin
              // Passed over for want of room. Counted, because "the frame
              // was full" and "this endpoint is being starved" look the
              // same in one frame and are different over many.
              starv_c <= starv_c + 32'd1;
            end
          end
        end

        // =============================================================
        P_END: begin
          ph     <= P_IDLE;
          fnum_r <= fnum_r + 16'd1;

          // ---- the deadline check, once per frame ----
          //
          // An endpoint whose age has passed its interval has missed a
          // deadline it was promised. Unreachable on a correct schedule,
          // counted so a run can publish zero.
          for (int k = 0; k < N_SLOT; k++)
            if (en[k] && ((ty[k] == X_ISO) || (ty[k] == X_INT))
                && (age[k] > {8'd0, iv[k]}))
              miss_c <= miss_c + 32'd1;

          if (used_r > FRAME_BYTES) over_c <= over_c + 32'd1;
        end

        default: ph <= P_IDLE;
      endcase
    end
  end

endmodule

7. VHDL-2008 RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  usb_frame_sched -- VHDL-2008.
--
--  The frame phases are a real enumeration type, so the ORDER that is the
--  answer to this chapter's question is declared once, in one place, and
--  the compiler refuses to confuse a phase with a transfer type.
--
--  Constants and types are prefixed because VHDL identifiers are
--  case-insensitive: a package name 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 fs_pkg is
  type xfer_t is (XT_CTRL, XT_ISO, XT_INT, XT_BULK);

  -- The order of these IS the answer to the interview question.
  type phase_t is (PH_IDLE, PH_ISO, PH_INT, PH_CTRL, PH_BULK, PH_END);

  constant TXN_OH_C : natural := 13;

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

  type mp_arr is array (natural range <>) of unsigned(10 downto 0);
  type iv_arr is array (natural range <>) of unsigned(7 downto 0);
  type ag_arr is array (natural range <>) of unsigned(15 downto 0);
  type ty_arr is array (natural range <>) of xfer_t;
end package;

package body fs_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(t : xfer_t) return std_logic_vector is
  begin
    case t is
      when XT_CTRL => return "00";
      when XT_ISO  => return "01";
      when XT_INT  => return "10";
      when XT_BULK => return "11";
    end case;
  end function;

  function 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.fs_pkg.all;

entity usb_frame_sched is
  generic (
    FRAME_BYTES : natural := 1500;
    N_SLOT      : natural := 8;
    TXN_OH      : natural := 13
  );
  port (
    clk          : in  std_logic;
    rst_n        : in  std_logic;

    frame_tick   : in  std_logic;

    cfg_wr       : in  std_logic;
    cfg_slot     : in  std_logic_vector(2 downto 0);
    cfg_type     : in  std_logic_vector(1 downto 0);
    cfg_maxp     : in  std_logic_vector(10 downto 0);
    cfg_interval : in  std_logic_vector(7 downto 0);
    cfg_enable   : in  std_logic;

    ent_valid    : out std_logic;
    ent_slot     : out std_logic_vector(2 downto 0);
    ent_type     : out std_logic_vector(1 downto 0);
    ent_bytes    : out std_logic_vector(15 downto 0);
    frame_busy   : out std_logic;
    frame_used   : out std_logic_vector(15 downto 0);
    frame_num    : out std_logic_vector(15 downto 0);

    n_frames     : out std_logic_vector(31 downto 0);
    n_entries    : out std_logic_vector(31 downto 0);
    n_periodic   : out std_logic_vector(31 downto 0);
    n_bulk       : out std_logic_vector(31 downto 0);
    n_missed     : out std_logic_vector(31 downto 0);
    n_overrun    : out std_logic_vector(31 downto 0);
    n_starved    : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_frame_sched is
  signal ty  : ty_arr(0 to N_SLOT-1) := (others => XT_CTRL);
  signal mp  : mp_arr(0 to N_SLOT-1) := (others => (others => '0'));
  signal iv  : iv_arr(0 to N_SLOT-1) := (others => to_unsigned(1, 8));
  signal en  : std_logic_vector(N_SLOT-1 downto 0) := (others => '0');
  -- Frames since this endpoint was last served, compared against its
  -- interval. A missed deadline is the only thing a schedule can get wrong
  -- without producing a single wrong byte.
  signal age : ag_arr(0 to N_SLOT-1) := (others => (others => '0'));

  signal ph      : phase_t := PH_IDLE;
  signal scan    : natural range 0 to 7 := 0;
  signal used_r  : unsigned(15 downto 0) := (others => '0');
  signal fnum_r  : unsigned(15 downto 0) := (others => '0');

  -- TWO pointers, not one. A single pointer advanced once per examined slot
  -- advances N_SLOT times per frame and therefore returns to exactly where
  -- it started -- it never rotates, and the same bulk endpoint wins every
  -- frame forever. rr_r is where the NEXT frame starts and is updated only
  -- when an endpoint is actually served; bidx walks the current frame.
  signal rr_r    : natural range 0 to 7 := 0;
  signal bidx    : natural range 0 to 7 := 0;
  signal rr_seen : natural range 0 to 7 := 0;

  signal ev_r : std_logic := '0';
  signal es_r : natural range 0 to 7 := 0;
  signal et_r : xfer_t := XT_CTRL;
  signal eb_r : unsigned(15 downto 0) := (others => '0');

  signal fr_c, ent_c, per_c, blk_c, miss_c, over_c, starv_c
    : unsigned(31 downto 0) := (others => '0');

  -- The interval is a power of two, so "every Nth frame" is a mask test.
  function due_now(interval : unsigned(7 downto 0);
                   f        : unsigned(15 downto 0)) return boolean is
  begin
    if interval <= 1 then
      return true;
    else
      return (f and resize(interval - 1, 16)) = 0;
    end if;
  end function;

  function cost_of(m : unsigned(10 downto 0)) return unsigned is
  begin
    return resize(m, 16) + to_unsigned(TXN_OH_C, 16);
  end function;
begin

  ent_valid  <= ev_r;
  ent_slot   <= std_logic_vector(to_unsigned(es_r, 3));
  ent_type   <= code_of(et_r);
  ent_bytes  <= std_logic_vector(eb_r);
  frame_busy <= '0' when ph = PH_IDLE else '1';
  frame_used <= std_logic_vector(used_r);
  frame_num  <= std_logic_vector(fnum_r);

  n_frames   <= std_logic_vector(fr_c);
  n_entries  <= std_logic_vector(ent_c);
  n_periodic <= std_logic_vector(per_c);
  n_bulk     <= std_logic_vector(blk_c);
  n_missed   <= std_logic_vector(miss_c);
  n_overrun  <= std_logic_vector(over_c);
  n_starved  <= std_logic_vector(starv_c);

  main : process(clk, rst_n)
    variable cs : natural;
  begin
    if rst_n = '0' then
      ph      <= PH_IDLE;
      scan    <= 0;
      used_r  <= (others => '0');
      fnum_r  <= (others => '0');
      rr_r    <= 0;
      bidx    <= 0;
      rr_seen <= 0;
      ev_r    <= '0';
      es_r    <= 0;
      et_r    <= XT_CTRL;
      eb_r    <= (others => '0');
      fr_c    <= (others => '0');
      ent_c   <= (others => '0');
      per_c   <= (others => '0');
      blk_c   <= (others => '0');
      miss_c  <= (others => '0');
      over_c  <= (others => '0');
      starv_c <= (others => '0');
      ty  <= (others => XT_CTRL);
      mp  <= (others => (others => '0'));
      iv  <= (others => to_unsigned(1, 8));
      en  <= (others => '0');
      age <= (others => (others => '0'));

    elsif rising_edge(clk) then
      ev_r <= '0';

      -- Table writes are accepted only BETWEEN frames. Reconfiguring
      -- mid-frame would change a schedule the host is already executing,
      -- and an endpoint already placed would be served under its old
      -- parameters.
      if cfg_wr = '1' and ph = PH_IDLE then
        cs := to_integer(unsigned(cfg_slot));
        ty(cs)  <= xfer_of(cfg_type);
        mp(cs)  <= unsigned(cfg_maxp);
        iv(cs)  <= unsigned(cfg_interval);
        en(cs)  <= cfg_enable;
        age(cs) <= (others => '0');
      end if;

      case ph is
        when PH_IDLE =>
          if frame_tick = '1' then
            ph      <= PH_ISO;
            scan    <= 0;
            used_r  <= (others => '0');
            rr_seen <= 0;
            fr_c    <= fr_c + 1;
            -- Every enabled periodic endpoint ages by one frame; one served
            -- this frame has its age cleared below.
            for k in 0 to N_SLOT-1 loop
              if en(k) = '1' and periodic_t(ty(k)) then
                age(k) <= age(k) + 1;
              end if;
            end loop;
          end if;

        -- ISOCHRONOUS FIRST. It cannot retry, so it cannot be deferred: a
        -- transaction that does not happen in its frame does not happen.
        when PH_ISO =>
          if scan = N_SLOT - 1 then ph <= PH_INT; end if;
          if scan = N_SLOT - 1 then scan <= 0; else scan <= scan + 1; end if;

          if en(scan) = '1' and ty(scan) = XT_ISO
             and due_now(iv(scan), fnum_r)
             and (used_r + cost_of(mp(scan))) <= to_unsigned(FRAME_BYTES, 16) then
            ev_r     <= '1';
            es_r     <= scan;
            et_r     <= XT_ISO;
            eb_r     <= cost_of(mp(scan));
            used_r   <= used_r + cost_of(mp(scan));
            age(scan) <= (others => '0');
            ent_c    <= ent_c + 1;
            per_c    <= per_c + 1;
          end if;

        -- INTERRUPT SECOND. Reserved but retryable, so it may be deferred
        -- within its interval -- which is why it goes after isochronous.
        when PH_INT =>
          if scan = N_SLOT - 1 then ph <= PH_CTRL; end if;
          if scan = N_SLOT - 1 then scan <= 0; else scan <= scan + 1; end if;

          if en(scan) = '1' and ty(scan) = XT_INT
             and due_now(iv(scan), fnum_r)
             and (used_r + cost_of(mp(scan))) <= to_unsigned(FRAME_BYTES, 16) then
            ev_r     <= '1';
            es_r     <= scan;
            et_r     <= XT_INT;
            eb_r     <= cost_of(mp(scan));
            used_r   <= used_r + cost_of(mp(scan));
            age(scan) <= (others => '0');
            ent_c    <= ent_c + 1;
            per_c    <= per_c + 1;
          end if;

        -- CONTROL THIRD, before bulk rather than competing with it.
        -- Control is how the host changes the schedule; a frame full of
        -- bulk that leaves no room for control is a bus that cannot be
        -- reconfigured.
        when PH_CTRL =>
          if scan = N_SLOT - 1 then
            ph   <= PH_BULK;
            bidx <= rr_r;          -- resume where the last frame left off
          end if;
          if scan = N_SLOT - 1 then scan <= 0; else scan <= scan + 1; end if;

          if en(scan) = '1' and ty(scan) = XT_CTRL
             and (used_r + cost_of(mp(scan))) <= to_unsigned(FRAME_BYTES, 16) then
            ev_r   <= '1';
            es_r   <= scan;
            et_r   <= XT_CTRL;
            eb_r   <= cost_of(mp(scan));
            used_r <= used_r + cost_of(mp(scan));
            ent_c  <= ent_c + 1;
          end if;

        -- BULK LAST, ROUND-ROBIN, until the frame is full. The rotating
        -- pointer is the ONLY fairness mechanism in the whole schedule:
        -- bulk has no reservation, so without it one endpoint would take
        -- the entire remainder of every frame forever, with no error
        -- reported anywhere because nothing was promised.
        when PH_BULK =>
          if rr_seen = N_SLOT - 1 then ph <= PH_END; end if;
          if rr_seen = N_SLOT - 1 then rr_seen <= 0; else rr_seen <= rr_seen + 1; end if;
          if bidx = N_SLOT - 1 then bidx <= 0; else bidx <= bidx + 1; end if;

          if en(bidx) = '1' and ty(bidx) = XT_BULK then
            if (used_r + cost_of(mp(bidx))) <= to_unsigned(FRAME_BYTES, 16) then
              ev_r   <= '1';
              es_r   <= bidx;
              et_r   <= XT_BULK;
              eb_r   <= cost_of(mp(bidx));
              used_r <= used_r + cost_of(mp(bidx));
              ent_c  <= ent_c + 1;
              blk_c  <= blk_c + 1;
              -- The next frame starts AFTER the last one served, which is
              -- what makes the rotation real.
              if bidx = N_SLOT - 1 then rr_r <= 0; else rr_r <= bidx + 1; end if;
            else
              -- Passed over for want of room. Counted, because "the frame
              -- was full" and "this endpoint is starved" look the same in
              -- one frame and are different over many.
              starv_c <= starv_c + 1;
            end if;
          end if;

        when PH_END =>
          ph     <= PH_IDLE;
          fnum_r <= fnum_r + 1;

          -- The deadline check, once per frame. Unreachable on a correct
          -- schedule, counted so a run can publish zero.
          for k in 0 to N_SLOT-1 loop
            if en(k) = '1' and periodic_t(ty(k))
               and age(k) > resize(iv(k), 16) then
              miss_c <= miss_c + 1;
            end if;
          end loop;

          if used_r > to_unsigned(FRAME_BYTES, 16) then
            over_c <= over_c + 1;
          end if;
      end case;
    end if;
  end process;

end architecture;

8. The Testbench: Two Properties That Are Not About Transactions

Order is a property of a whole frame. No single entry is wrong; the sequence is. So the bench collects a frame's entries into an array and checks that priority never decreases across it — and it maps type to priority explicitly, because the numeric type encoding is arbitrary and the placement order is the thesis. Checking ent_type monotonicity would test the encoding by accident.

Fairness is a property of many frames and cannot be observed in one at all. So the bench counts, per bulk endpoint, how many consecutive frames it has gone unserved, and bounds it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   What one frame can tell you about fairness:  NOTHING.

     frame 41: bulk slot 3 served, slots 0,1,2,4..7 passed over

   Correct rotation and total starvation produce the
   IDENTICAL frame. Only the sequence of frames differs.

There is also a per-cycle check: the design's running frame_used must equal the bench's accumulation at every point inside the frame, not merely at the end. A scheduler that overshoots and then corrects passes an end-of-frame comparison.

Verilog-2005 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for usb_frame_sched.
//
//  Two properties here cannot be checked one transaction at a time.
//
//  ORDER is a property of a whole frame: every entry must belong to a
//  type of priority at least as low as the one before it. So the bench
//  collects a frame's entries and checks the sequence, not the items.
//
//  FAIRNESS is a property of many frames: a round-robin pointer cannot
//  be observed in one frame at all. So the bench counts how many frames
//  each bulk endpoint waits, and asserts a bound over the whole run.
// =====================================================================
`timescale 1ns/1ps
module tb_fs_v;

  localparam integer FRAME_BYTES = 1500;
  localparam integer N_SLOT      = 8;
  localparam integer TXN_OH      = 13;

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

  reg         clk = 1'b0, rst_n = 1'b0;
  reg         frame_tick = 1'b0;
  reg         cfg_wr = 1'b0;
  reg  [2:0]  cfg_slot = 3'd0;
  reg  [1:0]  cfg_type = X_CTRL;
  reg  [10:0] cfg_maxp = 11'd0;
  reg  [7:0]  cfg_interval = 8'd1;
  reg         cfg_enable = 1'b0;

  wire        ent_valid, frame_busy;
  wire [2:0]  ent_slot;
  wire [1:0]  ent_type;
  wire [15:0] ent_bytes, frame_used, frame_num;
  wire [31:0] n_frames, n_entries, n_periodic, n_bulk,
              n_missed, n_overrun, n_starved;

  usb_frame_sched #(.FRAME_BYTES(FRAME_BYTES), .N_SLOT(N_SLOT),
                    .TXN_OH(TXN_OH)) dut (
    .clk(clk), .rst_n(rst_n), .frame_tick(frame_tick),
    .cfg_wr(cfg_wr), .cfg_slot(cfg_slot), .cfg_type(cfg_type),
    .cfg_maxp(cfg_maxp), .cfg_interval(cfg_interval), .cfg_enable(cfg_enable),
    .ent_valid(ent_valid), .ent_slot(ent_slot), .ent_type(ent_type),
    .ent_bytes(ent_bytes), .frame_busy(frame_busy),
    .frame_used(frame_used), .frame_num(frame_num),
    .n_frames(n_frames), .n_entries(n_entries), .n_periodic(n_periodic),
    .n_bulk(n_bulk), .n_missed(n_missed), .n_overrun(n_overrun),
    .n_starved(n_starved)
  );

  always #5 clk = ~clk;

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

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

  // ---- the shadow table ----
  reg [1:0]  s_ty [0:N_SLOT-1];
  reg [10:0] s_mp [0:N_SLOT-1];
  reg [7:0]  s_iv [0:N_SLOT-1];
  reg        s_en [0:N_SLOT-1];

  // ---- per-frame collection ----
  reg [2:0]  f_slot [0:63];
  reg [1:0]  f_type [0:63];
  reg [15:0] f_byte [0:63];
  integer    f_n;
  reg [15:0] f_sum;

  // ---- the two headline counters ----
  integer n_order = 0;     // an entry out of priority order
  integer n_over  = 0;     // a frame that exceeded its byte budget

  // ---- fairness: frames since each bulk slot was last served ----
  integer bulk_wait [0:N_SLOT-1];
  integer worst_wait = 0;
  integer ph3_worst  = 0;
  reg     fair_check = 1'b0;
  integer g_frames = 0, g_entries = 0, g_periodic = 0, g_bulk = 0, g_starved = 0;

  // ---- exhaustive reach over (type, interval, frame phase) ----
  reg reach [0:127];
  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 frame=%0d: %0s", $time, frames, what);
      end
    end
  endtask

  // ---- priority, which is NOT the numeric type code ----
  //
  // The type encoding is arbitrary; the placement order is the design's
  // whole thesis. Mapping one to the other explicitly is how the bench
  // avoids checking the encoding by accident.
  function [1:0] prio;
    input [1:0] t;
    begin
      case (t)
        X_ISO:   prio = 2'd0;
        X_INT:   prio = 2'd1;
        X_CTRL:  prio = 2'd2;
        default: prio = 2'd3;   // bulk
      endcase
    end
  endfunction

  function due_f;
    input [7:0] interval;
    input [15:0] f;
    begin
      if (interval <= 8'd1) due_f = 1'b1;
      else                  due_f = ((f & {8'd0, (interval - 8'd1)}) == 16'd0);
    end
  endfunction

  // ---------------------------------------------------------------
  //  Configure one slot. Only legal between frames.
  // ---------------------------------------------------------------
  task cfg(input [2:0] sl, input [1:0] t, input [10:0] m,
           input [7:0] interval, input e);
    begin
      cfg_wr = 1'b1; cfg_slot = sl; cfg_type = t;
      cfg_maxp = m; cfg_interval = interval; cfg_enable = e;
      s_ty[sl] = t; s_mp[sl] = m; s_iv[sl] = interval; s_en[sl] = e;
      @(posedge clk); #1;
      cfg_wr = 1'b0;
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  Run one complete frame and check everything about it.
  // ---------------------------------------------------------------
  task run_frame;
    integer guard, a, p;
    reg [15:0] fnum_at_start;
    begin
      fnum_at_start = frame_num;
      f_n   = 0;
      f_sum = 16'd0;

      frame_tick = 1'b1;
      @(posedge clk); #1;
      frame_tick = 1'b0;
      steps = steps + 1;

      // ---- collect until the frame ends ----
      //
      // Bounded. An unbounded wait here is an infinite loop the moment a
      // mutation stops the phase machine advancing, and the run would
      // never reach the summary that says which mutation did it.
      guard = 0;
      while (frame_busy && (guard < 8 * N_SLOT + 32)) begin
        if (ent_valid) begin
          if (f_n < 64) begin
            f_slot[f_n] = ent_slot;
            f_type[f_n] = ent_type;
            f_byte[f_n] = ent_bytes;
          end
          f_sum = f_sum + ent_bytes;
          f_n   = f_n + 1;
        end
        // ---- checked EVERY cycle, not once per frame ----
        //
        // The design's running byte total must equal the bench's
        // accumulation at every point inside the frame, not merely at the
        // end. A scheduler that overshoots and then corrects would pass an
        // end-of-frame check and fail this one.
        ck(frame_used === f_sum[15:0],
           "the running byte total disagrees mid-frame");
        ck(!(ent_valid && !frame_busy),
           "an entry was emitted outside a frame");
        @(posedge clk); #1;
        steps = steps + 1;
        guard = guard + 1;
      end
      ck(guard < 8 * N_SLOT + 32, "the frame never ended");
      // one more cycle to catch an entry emitted on the last phase cycle
      if (ent_valid) begin
        if (f_n < 64) begin
          f_slot[f_n] = ent_slot; f_type[f_n] = ent_type; f_byte[f_n] = ent_bytes;
        end
        f_sum = f_sum + ent_bytes;
        f_n   = f_n + 1;
      end
      frames   = frames + 1;
      g_frames = g_frames + 1;

      // ---- PROPERTY 1: priority order across the whole frame ----
      for (a = 1; a < f_n && a < 64; a = a + 1)
        if (prio(f_type[a]) < prio(f_type[a-1])) begin
          n_order = n_order + 1;
          ck(1'b0, "an entry was placed before a higher-priority one");
        end
      ck(n_order == 0, "the frame was not in priority order");

      // ---- PROPERTY 2: each entry's cost is maxp + overhead ----
      for (a = 0; a < f_n && a < 64; a = a + 1)
        ck(f_byte[a] === ({5'd0, s_mp[f_slot[a]]} + TXN_OH),
           "an entry was charged the wrong number of bytes");

      // ---- PROPERTY 3: the frame never overruns ----
      if (f_sum > FRAME_BYTES) n_over = n_over + 1;
      ck(f_sum <= FRAME_BYTES, "the frame exceeded its byte budget");
      ck(n_over == 0, "a frame overran");
      ck(n_overrun === 32'd0, "the design detected its own overrun");

      // ---- PROPERTY 4: every DUE periodic endpoint that fits was served ----
      //
      // The liveness half. A schedule that places nothing is in perfect
      // priority order and perfectly within budget.
      for (a = 0; a < N_SLOT; a = a + 1)
        if (s_en[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT))
            && due_f(s_iv[a], fnum_at_start)
            && (({5'd0, s_mp[a]} + TXN_OH) <= FRAME_BYTES)) begin
          p = 0;
          for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
            if (f_slot[ri] == a[2:0]) p = 1;
          ck(p == 1, "a due periodic endpoint was not served in its frame");
        end

      // ---- PROPERTY 5: served ONLY when due ----
      //
      // The other half of property 4, and the half that is easy to forget:
      // a scheduler that serves every periodic endpoint every frame misses
      // no deadlines and violates nothing the liveness check looks at. It
      // is still wrong -- it spends bandwidth that was reserved for
      // somebody else, and for an isochronous endpoint it delivers data the
      // application has not produced yet.
      for (a = 0; a < N_SLOT; a = a + 1)
        if (s_en[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT))
            && !due_f(s_iv[a], fnum_at_start)) begin
          p = 0;
          for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
            if (f_slot[ri] == a[2:0]) p = 1;
          ck(p == 0, "a periodic endpoint was served in a frame it was not due");
        end

      // ---- PROPERTY 6: no missed deadlines, ever ----
      ck(n_missed === 32'd0, "the design reported a missed deadline");

      // ---- PROPERTY 7: the fairness bound, EVERY frame ----
      //
      // Checked per frame rather than once at the end. A single
      // end-of-phase check kills a non-rotating pointer exactly once, and
      // one kill is indistinguishable from luck.
      if (fair_check)
        for (a = 0; a < N_SLOT; a = a + 1)
          if (s_en[a] && (s_ty[a] == X_BULK))
            ck(bulk_wait[a] <= N_SLOT,
               "a bulk endpoint waited longer than the round-robin bound");

      // ---- fairness bookkeeping, checked over the whole run ----
      for (a = 0; a < N_SLOT; a = a + 1) begin
        if (s_en[a] && (s_ty[a] == X_BULK)) begin
          p = 0;
          for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
            if (f_slot[ri] == a[2:0]) p = 1;
          if (p) bulk_wait[a] = 0;
          else begin
            bulk_wait[a] = bulk_wait[a] + 1;
            if (bulk_wait[a] > worst_wait) worst_wait = bulk_wait[a];
          end
        end else begin
          bulk_wait[a] = 0;
        end
      end

      g_entries  = g_entries + f_n;
      g_periodic = n_periodic;
      g_bulk     = n_bulk;
      g_starved  = n_starved;
    end
  endtask

  task reset_dut;
    integer a;
    begin
      rst_n = 1'b0;
      frame_tick = 0; cfg_wr = 0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      for (a = 0; a < N_SLOT; a = a + 1) begin
        s_ty[a] = X_CTRL; s_mp[a] = 11'd0; s_iv[a] = 8'd1; s_en[a] = 1'b0;
        bulk_wait[a] = 0;
      end
      @(posedge clk); #1;
    end
  endtask

  integer ti, vi, fi, k, a4, pj;
  integer per_budget;
  reg [1:0] rt;
  reg [10:0] rm;
  reg [7:0] rv;
  reg re;
  reg [7:0] ivs [0:3];
  reg [1:0] tys [0:3];

  initial begin
    for (ri = 0; ri < 128; ri = ri + 1) reach[ri] = 1'b0;
    ivs[0] = 8'd1; ivs[1] = 8'd2; ivs[2] = 8'd4; ivs[3] = 8'd8;
    tys[0] = X_CTRL; tys[1] = X_ISO; tys[2] = X_INT; tys[3] = X_BULK;
    seed = 32'd27006;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- one endpoint of every
    //  (type, interval), observed through all 8 frame phases.
    //  4 x 4 x 8 = 128.
    // =============================================================
    for (ti = 0; ti < 4; ti = ti + 1)
    for (vi = 0; vi < 4; vi = vi + 1) begin
      reset_dut;
      cfg(3'd0, tys[ti], 11'd64, ivs[vi], 1'b1);
      for (fi = 0; fi < 8; fi = fi + 1) begin
        run_frame;
        ri = (ti * 32) + (vi * 8) + fi;
        reach[ri] = 1'b1;
      end
    end

    // =============================================================
    //  PHASE 2 (DIRECTED, EXHAUSTIVE) -- ORDER, over every
    //  assignment of the four types to four slots.
    //
    //  The types are placed in slots in a DIFFERENT order from the
    //  order they must be emitted in, so a scheduler that simply
    //  walked its table would produce the wrong sequence. 24
    //  permutations, and the emitted order must be identical in all.
    // =============================================================
    for (pj = 0; pj < 24; pj = pj + 1) begin
      reset_dut;
      // a simple permutation generator: rotate and swap
      cfg(3'd0, tys[(pj)      % 4], 11'd64, 8'd1, 1'b1);
      cfg(3'd1, tys[(pj / 4 + 1) % 4], 11'd64, 8'd1, 1'b1);
      cfg(3'd2, tys[(pj / 8 + 2) % 4], 11'd64, 8'd1, 1'b1);
      cfg(3'd3, tys[(pj / 12 + 3) % 4], 11'd64, 8'd1, 1'b1);
      run_frame;
      run_frame;
    end

    // =============================================================
    //  PHASE 3 (DIRECTED) -- FAIRNESS, which one frame cannot show.
    //
    //  EIGHT bulk endpoints of 800 bytes each. 813 bytes apiece means
    //  exactly ONE fits in a 1500-byte frame, so seven are passed over
    //  every single frame -- and the question is whether it is always
    //  the same seven.
    //
    //  The sizing matters. An earlier version used 400-byte endpoints,
    //  three of which fit, and the worst wait was 1 frame: a fixed
    //  priority would have looked almost as good as a rotation. Making
    //  only ONE fit forces the wait to N_SLOT-1 under a correct
    //  rotation and to UNBOUNDED under a fixed one.
    // =============================================================
    reset_dut;
    for (k = 0; k < N_SLOT; k = k + 1)
      cfg(k[2:0], X_BULK, 11'd800, 8'd0, 1'b1);
    worst_wait = 0;
    fair_check = 1'b1;
    for (k = 0; k < 64; k = k + 1) run_frame;
    fair_check = 1'b0;
    // ---- the bound is only PROVABLE for this configuration ----
    //
    // The pointer advances when an endpoint is served, so the wait is
    // bounded by N_SLOT only while at least one bulk endpoint fits in every
    // frame -- which is true here by construction (413 bytes into 1500) and
    // NOT true in general. In a frame whose periodic traffic fills the
    // budget no bulk is served, the pointer does not move, and every bulk
    // endpoint waits one frame longer.
    //
    // So the bound is asserted here, where it holds, and the run-wide worst
    // case is reported as an observation rather than checked against a
    // number it is not required to meet.
    ph3_worst = worst_wait;
    ck(ph3_worst <= N_SLOT,
       "a bulk endpoint waited longer than the round-robin bound");
    ck(ph3_worst > 0,
       "no bulk endpoint was ever passed over: fairness was not exercised");

    // =============================================================
    //  PHASE 4 (DIRECTED) -- periodic traffic must NOT be displaced
    //  by bulk, however much bulk there is.
    //
    //  One isochronous endpoint plus eight slots' worth of bulk. The
    //  isochronous endpoint must be served in every single frame.
    // =============================================================
    reset_dut;
    cfg(3'd0, X_ISO, 11'd1023, 8'd1, 1'b1);
    for (k = 1; k < N_SLOT; k = k + 1)
      cfg(k[2:0], X_BULK, 11'd400, 8'd0, 1'b1);
    for (k = 0; k < 32; k = k + 1) begin
      run_frame;
      a4 = 0;
      for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
        if (f_slot[ri] == 3'd0) a4 = 1;
      ck(a4 == 1, "bulk traffic displaced an isochronous endpoint");
    end

    // =============================================================
    //  PHASE 5 (DIRECTED) -- a configuration write mid-frame is
    //  ignored, because it would change a schedule already running.
    // =============================================================
    //  Swept over every slot and over every cycle of the frame at which
    //  the write could land, because one attempt kills the mutation once
    //  and one kill is indistinguishable from luck.
    for (pj = 0; pj < N_SLOT; pj = pj + 1)
    for (vi = 1; vi < 6; vi = vi + 1) begin
      reset_dut;
      cfg(pj[2:0], X_ISO, 11'd64, 8'd1, 1'b1);
      frame_tick = 1'b1;
      @(posedge clk); #1;
      frame_tick = 1'b0;
      steps = steps + 1;
      // advance into the frame by a varying number of cycles
      for (k = 0; k < vi; k = k + 1) begin @(posedge clk); #1; steps = steps + 1; end
      // mid-frame: try to disable the endpoint
      cfg_wr = 1'b1; cfg_slot = pj[2:0]; cfg_type = X_ISO;
      cfg_maxp = 11'd64; cfg_interval = 8'd1; cfg_enable = 1'b0;
      @(posedge clk); #1;
      cfg_wr = 1'b0;
      steps = steps + 1;
      // drain the frame
      k = 0;
      while (frame_busy && (k < 64)) begin @(posedge clk); #1; k = k + 1; end
      // the endpoint must still be enabled: the write was refused
      run_frame;
      a4 = 0;
      for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
        if (f_slot[ri] == pj[2:0]) a4 = 1;
      ck(a4 == 1, "a mid-frame configuration write took effect");
    end

    // =============================================================
    //  PHASE 6 (RANDOM) -- an arbitrary endpoint mix.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 3000; k = k + 1) begin
      if ((k % 8) == 0) begin
        // ---- the random phase applies ADMISSION CONTROL ----
        //
        // A scheduler cannot keep a promise the schedule never had room
        // for. Configuring an arbitrary mix overcommits the frame, the
        // periodic endpoints that do not fit miss their deadlines, and the
        // design correctly reports it -- 386 "missed deadline" errors in
        // the first run of this bench, none of them the scheduler's fault.
        //
        // Chapter 27.5's admission control is a PRECONDITION for this
        // chapter's guarantees, so the stimulus has to respect it.
        per_budget = 0;
        for (a4 = 0; a4 < N_SLOT; a4 = a4 + 1) begin
          rt = tys[urand(0) % 4];
          rm = (urand(0) % 2) ? 11'd64 : 11'd400;
          rv = ivs[urand(0) % 4];
          re = ((urand(0) % 4) != 0);
          if (re && ((rt == X_ISO) || (rt == X_INT))) begin
            if ((per_budget + rm + TXN_OH) > 1350) re = 1'b0;
            else per_budget = per_budget + rm + TXN_OH;
          end
          cfg(a4[2:0], rt, rm, rv, re);
        end
      end
      run_frame;
    end
`endif

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

    $display("steps=%0d checks=%0d frames=%0d reach=%0d/128 errors=%0d",
             steps, checks, g_frames, n_reach, errors);
    $display("[sched] entries=%0d periodic=%0d bulk=%0d starved=%0d",
             g_entries, g_periodic, g_bulk, g_starved);
    $display("[fairness] round-robin bound phase=%0d (<= %0d required), run-wide worst=%0d",
             ph3_worst, N_SLOT, worst_wait);
    $display("[the whole point] out-of-order entries = %0d, frame overruns = %0d",
             n_order, n_over);
    if (n_reach != 128) 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_frame_sched.
//
//  Two properties here cannot be checked one transaction at a time.
//
//  ORDER is a property of a whole frame: every entry must belong to a
//  type of priority at least as low as the one before it. So the bench
//  collects a frame's entries and checks the sequence, not the items.
//
//  FAIRNESS is a property of many frames: a round-robin pointer cannot
//  be observed in one frame at all. So the bench counts how many frames
//  each bulk endpoint waits, and asserts a bound over the whole run.
// =====================================================================
`timescale 1ns/1ps
module tb_fs_sv;

  localparam integer FRAME_BYTES = 1500;
  localparam integer N_SLOT      = 8;
  localparam integer TXN_OH      = 13;

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

  logic       clk = 1'b0, rst_n = 1'b0;
  logic       frame_tick = 1'b0;
  logic       cfg_wr = 1'b0;
  logic [2:0] cfg_slot = 3'd0;
  logic [1:0] cfg_type = X_CTRL;
  logic [10:0] cfg_maxp = 11'd0;
  logic [7:0] cfg_interval = 8'd1;
  logic       cfg_enable = 1'b0;

  logic       ent_valid, frame_busy;
  logic [2:0] ent_slot;
  logic [1:0] ent_type;
  logic [15:0] ent_bytes, frame_used, frame_num;
  logic [31:0] n_frames, n_entries, n_periodic, n_bulk,
               n_missed, n_overrun, n_starved;

  usb_frame_sched #(.FRAME_BYTES(FRAME_BYTES), .N_SLOT(N_SLOT),
                    .TXN_OH(TXN_OH)) dut (
    .clk(clk), .rst_n(rst_n), .frame_tick(frame_tick),
    .cfg_wr(cfg_wr), .cfg_slot(cfg_slot), .cfg_type(cfg_type),
    .cfg_maxp(cfg_maxp), .cfg_interval(cfg_interval), .cfg_enable(cfg_enable),
    .ent_valid(ent_valid), .ent_slot(ent_slot), .ent_type(ent_type),
    .ent_bytes(ent_bytes), .frame_busy(frame_busy),
    .frame_used(frame_used), .frame_num(frame_num),
    .n_frames(n_frames), .n_entries(n_entries), .n_periodic(n_periodic),
    .n_bulk(n_bulk), .n_missed(n_missed), .n_overrun(n_overrun),
    .n_starved(n_starved)
  );

  always #5 clk = ~clk;

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

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

  // ---- the shadow table ----
  logic [1:0] s_ty [0:N_SLOT-1];
  logic [10:0] s_mp [0:N_SLOT-1];
  logic [7:0] s_iv [0:N_SLOT-1];
  logic      s_en [0:N_SLOT-1];

  // ---- per-frame collection ----
  logic [2:0] f_slot [0:63];
  logic [1:0] f_type [0:63];
  logic [15:0] f_byte [0:63];
  integer    f_n;
  logic [15:0] f_sum;

  // ---- the two headline counters ----
  integer n_order = 0;     // an entry out of priority order
  integer n_over  = 0;     // a frame that exceeded its byte budget

  // ---- fairness: frames since each bulk slot was last served ----
  integer bulk_wait [0:N_SLOT-1];
  integer worst_wait = 0;
  integer ph3_worst  = 0;
  logic   fair_check = 1'b0;
  integer g_frames = 0, g_entries = 0, g_periodic = 0, g_bulk = 0, g_starved = 0;

  // ---- exhaustive reach over (type, interval, frame phase) ----
  logic reach [0:127];
  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 frame=%0d: %0s", $time, frames, what);
      end
    end
  endtask

  // ---- priority, which is NOT the numeric type code ----
  //
  // The type encoding is arbitrary; the placement order is the design's
  // whole thesis. Mapping one to the other explicitly is how the bench
  // avoids checking the encoding by accident.
  function automatic logic [1:0] prio(logic [1:0] t);
    begin
      case (t)
        X_ISO:   prio = 2'd0;
        X_INT:   prio = 2'd1;
        X_CTRL:  prio = 2'd2;
        default: prio = 2'd3;   // bulk
      endcase
    end
  endfunction

  function automatic logic due_f(logic [7:0] interval, logic [15:0] f);
    begin
      if (interval <= 8'd1) due_f = 1'b1;
      else                  due_f = ((f & {8'd0, (interval - 8'd1)}) == 16'd0);
    end
  endfunction

  // ---------------------------------------------------------------
  //  Configure one slot. Only legal between frames.
  // ---------------------------------------------------------------
  task cfg(input logic [2:0] sl, input logic [1:0] t, input logic [10:0] m,
           input logic [7:0] interval, input logic e);
    begin
      cfg_wr = 1'b1; cfg_slot = sl; cfg_type = t;
      cfg_maxp = m; cfg_interval = interval; cfg_enable = e;
      s_ty[sl] = t; s_mp[sl] = m; s_iv[sl] = interval; s_en[sl] = e;
      @(posedge clk); #1;
      cfg_wr = 1'b0;
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  Run one complete frame and check everything about it.
  // ---------------------------------------------------------------
  task run_frame;
    integer guard, a, p;
    logic [15:0] fnum_at_start;
    begin
      fnum_at_start = frame_num;
      f_n   = 0;
      f_sum = 16'd0;

      frame_tick = 1'b1;
      @(posedge clk); #1;
      frame_tick = 1'b0;
      steps = steps + 1;

      // ---- collect until the frame ends ----
      //
      // Bounded. An unbounded wait here is an infinite loop the moment a
      // mutation stops the phase machine advancing, and the run would
      // never reach the summary that says which mutation did it.
      guard = 0;
      while (frame_busy && (guard < 8 * N_SLOT + 32)) begin
        if (ent_valid) begin
          if (f_n < 64) begin
            f_slot[f_n] = ent_slot;
            f_type[f_n] = ent_type;
            f_byte[f_n] = ent_bytes;
          end
          f_sum = f_sum + ent_bytes;
          f_n   = f_n + 1;
        end
        // ---- checked EVERY cycle, not once per frame ----
        //
        // The design's running byte total must equal the bench's
        // accumulation at every point inside the frame, not merely at the
        // end. A scheduler that overshoots and then corrects would pass an
        // end-of-frame check and fail this one.
        ck(frame_used === f_sum[15:0],
           "the running byte total disagrees mid-frame");
        ck(!(ent_valid && !frame_busy),
           "an entry was emitted outside a frame");
        @(posedge clk); #1;
        steps = steps + 1;
        guard = guard + 1;
      end
      ck(guard < 8 * N_SLOT + 32, "the frame never ended");
      // one more cycle to catch an entry emitted on the last phase cycle
      if (ent_valid) begin
        if (f_n < 64) begin
          f_slot[f_n] = ent_slot; f_type[f_n] = ent_type; f_byte[f_n] = ent_bytes;
        end
        f_sum = f_sum + ent_bytes;
        f_n   = f_n + 1;
      end
      frames   = frames + 1;
      g_frames = g_frames + 1;

      // ---- PROPERTY 1: priority order across the whole frame ----
      for (a = 1; a < f_n && a < 64; a = a + 1)
        if (prio(f_type[a]) < prio(f_type[a-1])) begin
          n_order = n_order + 1;
          ck(1'b0, "an entry was placed before a higher-priority one");
        end
      ck(n_order == 0, "the frame was not in priority order");

      // ---- PROPERTY 2: each entry's cost is maxp + overhead ----
      for (a = 0; a < f_n && a < 64; a = a + 1)
        ck(f_byte[a] === ({5'd0, s_mp[f_slot[a]]} + TXN_OH),
           "an entry was charged the wrong number of bytes");

      // ---- PROPERTY 3: the frame never overruns ----
      if (f_sum > FRAME_BYTES) n_over = n_over + 1;
      ck(f_sum <= FRAME_BYTES, "the frame exceeded its byte budget");
      ck(n_over == 0, "a frame overran");
      ck(n_overrun === 32'd0, "the design detected its own overrun");

      // ---- PROPERTY 4: every DUE periodic endpoint that fits was served ----
      //
      // The liveness half. A schedule that places nothing is in perfect
      // priority order and perfectly within budget.
      for (a = 0; a < N_SLOT; a = a + 1)
        if (s_en[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT))
            && due_f(s_iv[a], fnum_at_start)
            && (({5'd0, s_mp[a]} + TXN_OH) <= FRAME_BYTES)) begin
          p = 0;
          for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
            if (f_slot[ri] == a[2:0]) p = 1;
          ck(p == 1, "a due periodic endpoint was not served in its frame");
        end

      // ---- PROPERTY 5: served ONLY when due ----
      //
      // The other half of property 4, and the half that is easy to forget:
      // a scheduler that serves every periodic endpoint every frame misses
      // no deadlines and violates nothing the liveness check looks at. It
      // is still wrong -- it spends bandwidth that was reserved for
      // somebody else, and for an isochronous endpoint it delivers data the
      // application has not produced yet.
      for (a = 0; a < N_SLOT; a = a + 1)
        if (s_en[a] && ((s_ty[a] == X_ISO) || (s_ty[a] == X_INT))
            && !due_f(s_iv[a], fnum_at_start)) begin
          p = 0;
          for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
            if (f_slot[ri] == a[2:0]) p = 1;
          ck(p == 0, "a periodic endpoint was served in a frame it was not due");
        end

      // ---- PROPERTY 6: no missed deadlines, ever ----
      ck(n_missed === 32'd0, "the design reported a missed deadline");

      // ---- PROPERTY 7: the fairness bound, EVERY frame ----
      //
      // Checked per frame rather than once at the end. A single
      // end-of-phase check kills a non-rotating pointer exactly once, and
      // one kill is indistinguishable from luck.
      if (fair_check)
        for (a = 0; a < N_SLOT; a = a + 1)
          if (s_en[a] && (s_ty[a] == X_BULK))
            ck(bulk_wait[a] <= N_SLOT,
               "a bulk endpoint waited longer than the round-robin bound");

      // ---- fairness bookkeeping, checked over the whole run ----
      for (a = 0; a < N_SLOT; a = a + 1) begin
        if (s_en[a] && (s_ty[a] == X_BULK)) begin
          p = 0;
          for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
            if (f_slot[ri] == a[2:0]) p = 1;
          if (p) bulk_wait[a] = 0;
          else begin
            bulk_wait[a] = bulk_wait[a] + 1;
            if (bulk_wait[a] > worst_wait) worst_wait = bulk_wait[a];
          end
        end else begin
          bulk_wait[a] = 0;
        end
      end

      g_entries  = g_entries + f_n;
      g_periodic = n_periodic;
      g_bulk     = n_bulk;
      g_starved  = n_starved;
    end
  endtask

  task reset_dut;
    integer a;
    begin
      rst_n = 1'b0;
      frame_tick = 0; cfg_wr = 0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      for (a = 0; a < N_SLOT; a = a + 1) begin
        s_ty[a] = X_CTRL; s_mp[a] = 11'd0; s_iv[a] = 8'd1; s_en[a] = 1'b0;
        bulk_wait[a] = 0;
      end
      @(posedge clk); #1;
    end
  endtask

  integer ti, vi, fi, k, a4, pj;
  integer per_budget;
  logic [1:0] rt;
  logic [10:0] rm;
  logic [7:0] rv;
  logic re;
  logic [7:0] ivs [0:3];
  logic [1:0] tys [0:3];

  initial begin
    for (ri = 0; ri < 128; ri = ri + 1) reach[ri] = 1'b0;
    ivs[0] = 8'd1; ivs[1] = 8'd2; ivs[2] = 8'd4; ivs[3] = 8'd8;
    tys[0] = X_CTRL; tys[1] = X_ISO; tys[2] = X_INT; tys[3] = X_BULK;
    seed = 32'd27006;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- one endpoint of every
    //  (type, interval), observed through all 8 frame phases.
    //  4 x 4 x 8 = 128.
    // =============================================================
    for (ti = 0; ti < 4; ti = ti + 1)
    for (vi = 0; vi < 4; vi = vi + 1) begin
      reset_dut;
      cfg(3'd0, tys[ti], 11'd64, ivs[vi], 1'b1);
      for (fi = 0; fi < 8; fi = fi + 1) begin
        run_frame;
        ri = (ti * 32) + (vi * 8) + fi;
        reach[ri] = 1'b1;
      end
    end

    // =============================================================
    //  PHASE 2 (DIRECTED, EXHAUSTIVE) -- ORDER, over every
    //  assignment of the four types to four slots.
    //
    //  The types are placed in slots in a DIFFERENT order from the
    //  order they must be emitted in, so a scheduler that simply
    //  walked its table would produce the wrong sequence. 24
    //  permutations, and the emitted order must be identical in all.
    // =============================================================
    for (pj = 0; pj < 24; pj = pj + 1) begin
      reset_dut;
      // a simple permutation generator: rotate and swap
      cfg(3'd0, tys[(pj)      % 4], 11'd64, 8'd1, 1'b1);
      cfg(3'd1, tys[(pj / 4 + 1) % 4], 11'd64, 8'd1, 1'b1);
      cfg(3'd2, tys[(pj / 8 + 2) % 4], 11'd64, 8'd1, 1'b1);
      cfg(3'd3, tys[(pj / 12 + 3) % 4], 11'd64, 8'd1, 1'b1);
      run_frame;
      run_frame;
    end

    // =============================================================
    //  PHASE 3 (DIRECTED) -- FAIRNESS, which one frame cannot show.
    //
    //  EIGHT bulk endpoints of 800 bytes each. 813 bytes apiece means
    //  exactly ONE fits in a 1500-byte frame, so seven are passed over
    //  every single frame -- and the question is whether it is always
    //  the same seven.
    //
    //  The sizing matters. An earlier version used 400-byte endpoints,
    //  three of which fit, and the worst wait was 1 frame: a fixed
    //  priority would have looked almost as good as a rotation. Making
    //  only ONE fit forces the wait to N_SLOT-1 under a correct
    //  rotation and to UNBOUNDED under a fixed one.
    // =============================================================
    reset_dut;
    for (k = 0; k < N_SLOT; k = k + 1)
      cfg(k[2:0], X_BULK, 11'd800, 8'd0, 1'b1);
    worst_wait = 0;
    fair_check = 1'b1;
    for (k = 0; k < 64; k = k + 1) run_frame;
    fair_check = 1'b0;
    // ---- the bound is only PROVABLE for this configuration ----
    //
    // The pointer advances when an endpoint is served, so the wait is
    // bounded by N_SLOT only while at least one bulk endpoint fits in every
    // frame -- which is true here by construction (413 bytes into 1500) and
    // NOT true in general. In a frame whose periodic traffic fills the
    // budget no bulk is served, the pointer does not move, and every bulk
    // endpoint waits one frame longer.
    //
    // So the bound is asserted here, where it holds, and the run-wide worst
    // case is reported as an observation rather than checked against a
    // number it is not required to meet.
    ph3_worst = worst_wait;
    ck(ph3_worst <= N_SLOT,
       "a bulk endpoint waited longer than the round-robin bound");
    ck(ph3_worst > 0,
       "no bulk endpoint was ever passed over: fairness was not exercised");

    // =============================================================
    //  PHASE 4 (DIRECTED) -- periodic traffic must NOT be displaced
    //  by bulk, however much bulk there is.
    //
    //  One isochronous endpoint plus eight slots' worth of bulk. The
    //  isochronous endpoint must be served in every single frame.
    // =============================================================
    reset_dut;
    cfg(3'd0, X_ISO, 11'd1023, 8'd1, 1'b1);
    for (k = 1; k < N_SLOT; k = k + 1)
      cfg(k[2:0], X_BULK, 11'd400, 8'd0, 1'b1);
    for (k = 0; k < 32; k = k + 1) begin
      run_frame;
      a4 = 0;
      for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
        if (f_slot[ri] == 3'd0) a4 = 1;
      ck(a4 == 1, "bulk traffic displaced an isochronous endpoint");
    end

    // =============================================================
    //  PHASE 5 (DIRECTED) -- a configuration write mid-frame is
    //  ignored, because it would change a schedule already running.
    // =============================================================
    //  Swept over every slot and over every cycle of the frame at which
    //  the write could land, because one attempt kills the mutation once
    //  and one kill is indistinguishable from luck.
    for (pj = 0; pj < N_SLOT; pj = pj + 1)
    for (vi = 1; vi < 6; vi = vi + 1) begin
      reset_dut;
      cfg(pj[2:0], X_ISO, 11'd64, 8'd1, 1'b1);
      frame_tick = 1'b1;
      @(posedge clk); #1;
      frame_tick = 1'b0;
      steps = steps + 1;
      // advance into the frame by a varying number of cycles
      for (k = 0; k < vi; k = k + 1) begin @(posedge clk); #1; steps = steps + 1; end
      // mid-frame: try to disable the endpoint
      cfg_wr = 1'b1; cfg_slot = pj[2:0]; cfg_type = X_ISO;
      cfg_maxp = 11'd64; cfg_interval = 8'd1; cfg_enable = 1'b0;
      @(posedge clk); #1;
      cfg_wr = 1'b0;
      steps = steps + 1;
      // drain the frame
      k = 0;
      while (frame_busy && (k < 64)) begin @(posedge clk); #1; k = k + 1; end
      // the endpoint must still be enabled: the write was refused
      run_frame;
      a4 = 0;
      for (ri = 0; ri < f_n && ri < 64; ri = ri + 1)
        if (f_slot[ri] == pj[2:0]) a4 = 1;
      ck(a4 == 1, "a mid-frame configuration write took effect");
    end

    // =============================================================
    //  PHASE 6 (RANDOM) -- an arbitrary endpoint mix.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 3000; k = k + 1) begin
      if ((k % 8) == 0) begin
        // ---- the random phase applies ADMISSION CONTROL ----
        //
        // A scheduler cannot keep a promise the schedule never had room
        // for. Configuring an arbitrary mix overcommits the frame, the
        // periodic endpoints that do not fit miss their deadlines, and the
        // design correctly reports it -- 386 "missed deadline" errors in
        // the first run of this bench, none of them the scheduler's fault.
        //
        // Chapter 27.5's admission control is a PRECONDITION for this
        // chapter's guarantees, so the stimulus has to respect it.
        per_budget = 0;
        for (a4 = 0; a4 < N_SLOT; a4 = a4 + 1) begin
          rt = tys[urand(0) % 4];
          rm = (urand(0) % 2) ? 11'd64 : 11'd400;
          rv = ivs[urand(0) % 4];
          re = ((urand(0) % 4) != 0);
          if (re && ((rt == X_ISO) || (rt == X_INT))) begin
            if ((per_budget + rm + TXN_OH) > 1350) re = 1'b0;
            else per_budget = per_budget + rm + TXN_OH;
          end
          cfg(a4[2:0], rt, rm, rv, re);
        end
      end
      run_frame;
    end
`endif

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

    $display("steps=%0d checks=%0d frames=%0d reach=%0d/128 errors=%0d",
             steps, checks, g_frames, n_reach, errors);
    $display("[sched] entries=%0d periodic=%0d bulk=%0d starved=%0d",
             g_entries, g_periodic, g_bulk, g_starved);
    $display("[fairness] round-robin bound phase=%0d (<= %0d required), run-wide worst=%0d",
             ph3_worst, N_SLOT, worst_wait);
    $display("[the whole point] out-of-order entries = %0d, frame overruns = %0d",
             n_order, n_over);
    if (n_reach != 128) 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_frame_sched (VHDL-2008).
--
--  Two properties here cannot be checked one transaction at a time.
--  ORDER is a property of a whole frame; FAIRNESS is a property of many
--  frames and cannot be observed in one at all. So the bench collects a
--  frame's entries and checks the sequence, and counts how many frames
--  each bulk endpoint waits and bounds it over the run.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.fs_pkg.all;

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

architecture sim of tb_fs_vhdl is
  constant FRAME_BYTES : natural := 1500;
  constant N_SLOT      : natural := 8;
  constant TXN_OH      : natural := 13;

  signal clk          : std_logic := '0';
  signal rst_n        : std_logic := '0';
  signal frame_tick   : std_logic := '0';
  signal cfg_wr       : std_logic := '0';
  signal cfg_slot     : std_logic_vector(2 downto 0) := (others => '0');
  signal cfg_type     : std_logic_vector(1 downto 0) := "00";
  signal cfg_maxp     : std_logic_vector(10 downto 0) := (others => '0');
  signal cfg_interval : std_logic_vector(7 downto 0) := x"01";
  signal cfg_enable   : std_logic := '0';

  signal ent_valid, frame_busy : std_logic;
  signal ent_slot  : std_logic_vector(2 downto 0);
  signal ent_type  : std_logic_vector(1 downto 0);
  signal ent_bytes, frame_used, frame_num : std_logic_vector(15 downto 0);
  signal n_frames, n_entries, n_periodic, n_bulk,
         n_missed, n_overrun, n_starved : std_logic_vector(31 downto 0);

  signal done : boolean := false;
begin

  dut : entity work.usb_frame_sched
    generic map (FRAME_BYTES => FRAME_BYTES, N_SLOT => N_SLOT, TXN_OH => TXN_OH)
    port map (
      clk => clk, rst_n => rst_n, frame_tick => frame_tick,
      cfg_wr => cfg_wr, cfg_slot => cfg_slot, cfg_type => cfg_type,
      cfg_maxp => cfg_maxp, cfg_interval => cfg_interval,
      cfg_enable => cfg_enable,
      ent_valid => ent_valid, ent_slot => ent_slot, ent_type => ent_type,
      ent_bytes => ent_bytes, frame_busy => frame_busy,
      frame_used => frame_used, frame_num => frame_num,
      n_frames => n_frames, n_entries => n_entries, n_periodic => n_periodic,
      n_bulk => n_bulk, n_missed => n_missed, n_overrun => n_overrun,
      n_starved => n_starved);

  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 frames  : natural := 0;

    type sl_arr is array (0 to N_SLOT-1) of std_logic;
    type nm_arr is array (0 to N_SLOT-1) of natural;
    variable s_ty : ty_arr(0 to N_SLOT-1) := (others => XT_CTRL);
    variable s_mp : nm_arr := (others => 0);
    variable s_iv : nm_arr := (others => 1);
    variable s_en : sl_arr := (others => '0');

    type f_sl is array (0 to 63) of natural;
    type f_ty is array (0 to 63) of xfer_t;
    variable f_slot : f_sl := (others => 0);
    variable f_type : f_ty := (others => XT_CTRL);
    variable f_byte : f_sl := (others => 0);
    variable f_n    : natural := 0;
    variable f_sum  : natural := 0;

    variable n_order : natural := 0;
    variable n_over  : natural := 0;

    variable bulk_wait : nm_arr := (others => 0);
    variable worst_wait, ph3_worst : natural := 0;
    variable fair_check : boolean := false;
    variable g_frames, g_entries, g_periodic, g_bulk, g_starved : natural := 0;

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

    variable rnd : unsigned(31 downto 0) := x"0006E4A7";
    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 frame=") & integer'image(frames)
                & string'(": ") & what);
          writeline(output, ln);
        end if;
      end if;
    end procedure;

    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;

    -- Priority, which is NOT the numeric type code. The encoding is
    -- arbitrary; the placement order is the design's whole thesis, and
    -- mapping one to the other explicitly is how the bench avoids checking
    -- the encoding by accident.
    function prio(t : xfer_t) return natural is
    begin
      case t is
        when XT_ISO  => return 0;
        when XT_INT  => return 1;
        when XT_CTRL => return 2;
        when XT_BULK => return 3;
      end case;
    end function;

    function due_f(interval : natural; f : natural) return boolean is
    begin
      if interval <= 1 then return true; end if;
      return (f mod interval) = 0;
    end function;

    procedure cfg(sl : natural; t : xfer_t; m : natural;
                  interval : natural; e : std_logic) is
    begin
      cfg_wr   <= '1';
      cfg_slot <= std_logic_vector(to_unsigned(sl, 3));
      cfg_type <= code_of(t);
      cfg_maxp <= std_logic_vector(to_unsigned(m, 11));
      cfg_interval <= std_logic_vector(to_unsigned(interval, 8));
      cfg_enable <= e;
      s_ty(sl) := t;  s_mp(sl) := m;  s_iv(sl) := interval;  s_en(sl) := e;
      wait until rising_edge(clk);
      wait for 1 ns;
      cfg_wr <= '0';
      steps := steps + 1;
    end procedure;

    procedure run_frame is
      variable guard, p : natural;
      variable fnum_at_start : natural;
    begin
      fnum_at_start := to_integer(unsigned(frame_num));
      f_n   := 0;
      f_sum := 0;

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

      -- Bounded. An unbounded wait is an infinite loop the moment a
      -- mutation stops the phase machine advancing, and the run would never
      -- reach the summary that says which mutation did it.
      guard := 0;
      while frame_busy = '1' and guard < 8 * N_SLOT + 32 loop
        if ent_valid = '1' then
          if f_n < 64 then
            f_slot(f_n) := to_integer(unsigned(ent_slot));
            f_type(f_n) := xfer_of(ent_type);
            f_byte(f_n) := to_integer(unsigned(ent_bytes));
          end if;
          f_sum := f_sum + to_integer(unsigned(ent_bytes));
          f_n   := f_n + 1;
        end if;
        -- checked EVERY cycle, not once per frame: a scheduler that
        -- overshoots and then corrects passes an end-of-frame check.
        ck(to_integer(unsigned(frame_used)) = f_sum,
           "the running byte total disagrees mid-frame");
        ck(not (ent_valid = '1' and frame_busy = '0'),
           "an entry was emitted outside a frame");
        wait until rising_edge(clk);
        wait for 1 ns;
        steps := steps + 1;
        guard := guard + 1;
      end loop;
      ck(guard < 8 * N_SLOT + 32, "the frame never ended");
      if ent_valid = '1' then
        if f_n < 64 then
          f_slot(f_n) := to_integer(unsigned(ent_slot));
          f_type(f_n) := xfer_of(ent_type);
          f_byte(f_n) := to_integer(unsigned(ent_bytes));
        end if;
        f_sum := f_sum + to_integer(unsigned(ent_bytes));
        f_n   := f_n + 1;
      end if;
      frames   := frames + 1;
      g_frames := g_frames + 1;

      -- PROPERTY 1: priority order across the whole frame
      for a in 1 to 63 loop
        if a < f_n then
          if prio(f_type(a)) < prio(f_type(a-1)) then
            n_order := n_order + 1;
            ck(false, "an entry was placed before a higher-priority one");
          end if;
        end if;
      end loop;
      ck(n_order = 0, "the frame was not in priority order");

      -- PROPERTY 2: each entry's cost is maxp + overhead
      for a in 0 to 63 loop
        if a < f_n then
          ck(f_byte(a) = s_mp(f_slot(a)) + TXN_OH,
             "an entry was charged the wrong number of bytes");
        end if;
      end loop;

      -- PROPERTY 3: the frame never overruns
      if f_sum > FRAME_BYTES then n_over := n_over + 1; end if;
      ck(f_sum <= FRAME_BYTES, "the frame exceeded its byte budget");
      ck(n_over = 0, "a frame overran");
      ck(to_integer(unsigned(n_overrun)) = 0,
         "the design detected its own overrun");

      -- PROPERTY 4: every DUE periodic endpoint that fits was served
      for a in 0 to N_SLOT-1 loop
        if s_en(a) = '1' and periodic_t(s_ty(a))
           and due_f(s_iv(a), fnum_at_start)
           and (s_mp(a) + TXN_OH) <= FRAME_BYTES then
          p := 0;
          for i in 0 to 63 loop
            if i < f_n and f_slot(i) = a then p := 1; end if;
          end loop;
          ck(p = 1, "a due periodic endpoint was not served in its frame");
        end if;
      end loop;

      -- PROPERTY 5: served ONLY when due. A scheduler that serves every
      -- endpoint every frame misses no deadlines and is still wrong: it
      -- spends bandwidth reserved for somebody else.
      for a in 0 to N_SLOT-1 loop
        if s_en(a) = '1' and periodic_t(s_ty(a))
           and not due_f(s_iv(a), fnum_at_start) then
          p := 0;
          for i in 0 to 63 loop
            if i < f_n and f_slot(i) = a then p := 1; end if;
          end loop;
          ck(p = 0, "a periodic endpoint was served in a frame it was not due");
        end if;
      end loop;

      -- PROPERTY 6: no missed deadlines, ever
      ck(to_integer(unsigned(n_missed)) = 0,
         "the design reported a missed deadline");

      -- fairness bookkeeping
      for a in 0 to N_SLOT-1 loop
        if s_en(a) = '1' and s_ty(a) = XT_BULK then
          p := 0;
          for i in 0 to 63 loop
            if i < f_n and f_slot(i) = a then p := 1; end if;
          end loop;
          if p = 1 then
            bulk_wait(a) := 0;
          else
            bulk_wait(a) := bulk_wait(a) + 1;
            if bulk_wait(a) > worst_wait then worst_wait := bulk_wait(a); end if;
          end if;
        else
          bulk_wait(a) := 0;
        end if;
      end loop;

      -- PROPERTY 7: the fairness bound, EVERY frame
      if fair_check then
        for a in 0 to N_SLOT-1 loop
          if s_en(a) = '1' and s_ty(a) = XT_BULK then
            ck(bulk_wait(a) <= N_SLOT,
               "a bulk endpoint waited longer than the round-robin bound");
          end if;
        end loop;
      end if;

      g_entries  := g_entries + f_n;
      g_periodic := to_integer(unsigned(n_periodic));
      g_bulk     := to_integer(unsigned(n_bulk));
      g_starved  := to_integer(unsigned(n_starved));
    end procedure;

    procedure reset_dut is
    begin
      rst_n <= '0';
      frame_tick <= '0'; cfg_wr <= '0';
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      s_ty := (others => XT_CTRL);
      s_mp := (others => 0);
      s_iv := (others => 1);
      s_en := (others => '0');
      bulk_wait := (others => 0);
      wait until rising_edge(clk);
      wait for 1 ns;
    end procedure;

    type nat4 is array (0 to 3) of natural;
    constant ivs : nat4 := (1, 2, 4, 8);
    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 per_budget : natural;
    variable rt : xfer_t;
    variable rm, rv : natural;
    variable re : std_logic;
    variable a4, p2 : natural;
  begin
    -- PHASE 1 (DIRECTED, EXHAUSTIVE) -- one endpoint of every
    -- (type, interval), observed through all 8 frame phases. 4 x 4 x 8 = 128
    for ti in 0 to 3 loop
      for vi in 0 to 3 loop
        reset_dut;
        cfg(0, tys(ti), 64, ivs(vi), '1');
        for fi in 0 to 7 loop
          run_frame;
          ri := ti*32 + vi*8 + fi;
          reach(ri) := '1';
        end loop;
      end loop;
    end loop;

    -- PHASE 2 (DIRECTED, EXHAUSTIVE) -- ORDER, over 24 assignments of the
    -- four types to four slots. The types sit in slots in a DIFFERENT order
    -- from the one they must be emitted in, so a scheduler that simply
    -- walked its table would produce the wrong sequence.
    for pj in 0 to 23 loop
      reset_dut;
      cfg(0, tys(pj mod 4),           64, 1, '1');
      cfg(1, tys((pj / 4 + 1) mod 4), 64, 1, '1');
      cfg(2, tys((pj / 8 + 2) mod 4), 64, 1, '1');
      cfg(3, tys((pj / 12 + 3) mod 4), 64, 1, '1');
      run_frame;
      run_frame;
    end loop;

    -- PHASE 3 (DIRECTED) -- FAIRNESS, which one frame cannot show.
    --
    -- EIGHT bulk endpoints of 800 bytes. 813 apiece means exactly ONE fits
    -- in a 1500-byte frame, so seven are passed over every frame -- and the
    -- question is whether it is always the same seven. The sizing matters:
    -- with three fitting, the worst wait is 1 frame and a fixed priority
    -- looks almost as good as a rotation.
    reset_dut;
    for k in 0 to N_SLOT-1 loop
      cfg(k, XT_BULK, 800, 0, '1');
    end loop;
    worst_wait := 0;
    fair_check := true;
    for k in 0 to 63 loop run_frame; end loop;
    fair_check := false;
    ph3_worst := worst_wait;
    ck(ph3_worst <= N_SLOT,
       "a bulk endpoint waited longer than the round-robin bound");
    ck(ph3_worst > 0,
       "no bulk endpoint was ever passed over: fairness was not exercised");

    -- PHASE 4 (DIRECTED) -- periodic must NOT be displaced by bulk
    reset_dut;
    cfg(0, XT_ISO, 1023, 1, '1');
    for k in 1 to N_SLOT-1 loop
      cfg(k, XT_BULK, 400, 0, '1');
    end loop;
    for k in 0 to 31 loop
      run_frame;
      a4 := 0;
      for i in 0 to 63 loop
        if i < f_n and f_slot(i) = 0 then a4 := 1; end if;
      end loop;
      ck(a4 = 1, "bulk traffic displaced an isochronous endpoint");
    end loop;

    -- PHASE 5 (DIRECTED) -- a mid-frame configuration write is ignored,
    -- swept over every slot and over every cycle at which it could land.
    for pj in 0 to N_SLOT-1 loop
      for vi in 1 to 5 loop
        reset_dut;
        cfg(pj, XT_ISO, 64, 1, '1');
        frame_tick <= '1';
        wait until rising_edge(clk);
        wait for 1 ns;
        frame_tick <= '0';
        steps := steps + 1;
        for k in 1 to vi loop
          wait until rising_edge(clk);
          wait for 1 ns;
          steps := steps + 1;
        end loop;
        cfg_wr <= '1';
        cfg_slot <= std_logic_vector(to_unsigned(pj, 3));
        cfg_type <= code_of(XT_ISO);
        cfg_maxp <= std_logic_vector(to_unsigned(64, 11));
        cfg_interval <= x"01";
        cfg_enable <= '0';
        wait until rising_edge(clk);
        wait for 1 ns;
        cfg_wr <= '0';
        steps := steps + 1;
        p2 := 0;
        while frame_busy = '1' and p2 < 64 loop
          wait until rising_edge(clk);
          wait for 1 ns;
          p2 := p2 + 1;
        end loop;
        run_frame;
        a4 := 0;
        for i in 0 to 63 loop
          if i < f_n and f_slot(i) = pj then a4 := 1; end if;
        end loop;
        ck(a4 = 1, "a mid-frame configuration write took effect");
      end loop;
    end loop;

    -- PHASE 6 (RANDOM) -- an arbitrary but ADMISSIBLE endpoint mix.
    --
    -- A scheduler cannot keep a promise the schedule never had room for, so
    -- the stimulus applies chapter 27.5's admission control before
    -- configuring: an arbitrary mix overcommits the frame and the periodic
    -- endpoints that do not fit miss deadlines that were never affordable.
    if not DIRECTED_ONLY then
      reset_dut;
      for k in 0 to 2999 loop
        if (k mod 8) = 0 then
          per_budget := 0;
          for a in 0 to N_SLOT-1 loop
            rt := tys(nxt mod 4);
            if (nxt mod 2) = 1 then rm := 64; else rm := 400; end if;
            rv := ivs(nxt mod 4);
            if (nxt mod 4) /= 0 then re := '1'; else re := '0'; end if;
            if re = '1' and periodic_t(rt) then
              if (per_budget + rm + TXN_OH) > 1350 then
                re := '0';
              else
                per_budget := per_budget + rm + TXN_OH;
              end if;
            end if;
            cfg(a, rt, rm, rv, re);
          end loop;
        end if;
        run_frame;
      end loop;
    end if;

    n_reach := 0;
    for i in 0 to 127 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'(" frames=") & integer'image(g_frames)
          & string'(" reach=") & integer'image(n_reach) & string'("/128")
          & string'(" errors=") & integer'image(errors));
    writeline(output, ln);
    write(ln, string'("[sched] entries=") & integer'image(g_entries)
          & string'(" periodic=") & integer'image(g_periodic)
          & string'(" bulk=") & integer'image(g_bulk)
          & string'(" starved=") & integer'image(g_starved));
    writeline(output, ln);
    write(ln, string'("[fairness] round-robin bound phase=")
          & integer'image(ph3_worst) & string'(" (<= ")
          & integer'image(N_SLOT) & string'(" required), run-wide worst=")
          & integer'image(worst_wait));
    writeline(output, ln);
    write(ln, string'("[the whole point] out-of-order entries = ")
          & integer'image(n_order) & string'(", frame overruns = ")
          & integer'image(n_over));
    writeline(output, ln);
    if n_reach /= 128 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;

9. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
(type × interval × frame phase) reached128 / 128128 / 128128 / 128
type-to-slot assignments swept24 / 2424 / 2424 / 24
mid-frame write attempts swept40 / 4040 / 4040 / 40
Steps115976115976115976
Frames run331233123312
Checks executed260898260898260799
schedule entries emitted128321283212685
periodic entries407440744103
bulk entries404840483781
bulk slots passed over for room512512451
round-robin worst wait (bounded phase)7 ≤ 87 ≤ 87 ≤ 8
out-of-order entries000
frame overruns000
ResultPASSPASSPASS

The ordering sweep is worth a word. Phase 2 assigns the four transfer types to four slots in 24 different arrangements, so the order the types sit in the table is almost never the order they must be emitted in. A scheduler that simply walked its table would pass a test where slot 0 happened to hold the isochronous endpoint and fail 23 of the 24.

10. Mutation Testing

#MutationVerilogSysVerVHDL
F1bulk is placed BEFORE periodic879987998246
F4no frame-budget check before placing a bulk entry676267626695
F5the due test ignores the interval476847684787
F3interrupt is placed before isochronous369836983657
F6a served endpoint's age is not cleared309030903098
F2the round-robin pointer never rotates386386393
F7a configuration write is accepted mid-frame808080
—unmutated baseline000

All seven die in all three languages.

F1 is the mutation this chapter exists for and it scores highest. Placing bulk first does not corrupt a byte: every entry is still charged correctly, the frame still fits, and the order is still internally consistent. What breaks is the guarantee — the isochronous endpoint that was admitted on the promise of a slot in every frame now gets whatever bulk left behind, which on a busy bus is nothing.

Directed against random

#V allV directedV randomVHDL allVHDL directedVHDL random
F18799313848682463137933
F238638603933930
F33698184351436571843473
F46762329643366953296366
F547683447344787344753
F63090100299030981002998
F78080080800

Every directed column is identical across Verilog and VHDL.

F2 and F7 have a random contribution of exactly zero, and that is the most informative pair of numbers in the chapter. Neither the round-robin bound nor the mid-frame-write refusal can be reached by stimulus that does not deliberately set them up: fairness needs a configuration in which the resource is scarce enough to force a choice, and the mid-frame write needs a write issued at a cycle a driver would never choose.

Random stimulus does not construct either situation, however long it runs. Those two properties exist in this suite only because somebody wrote a phase for them.

11. F2 Scored 1, and 1 Is Indistinguishable From Luck

The first run of this matrix gave the round-robin mutation a score of one.

One error, in 250,000 checks, for a defect that starves seven endpoints out of eight indefinitely. The reason was that the fairness bound was asserted once, at the end of the fairness phase, against the worst wait observed across it. One assertion, one failure.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Before:  run 64 frames; then once:
              assert worst_wait <= N_SLOT     -> 1 error

   After:   every frame, for every bulk slot:
              assert bulk_wait[slot] <= N_SLOT -> 386 errors

   Same property. Same stimulus. 386x the evidence.

The fix was not more stimulus. It was to check the property where it holds — at every frame, for every endpoint — rather than summarising it into a single number and checking that.

12. F4 and F5 Both Scored Zero, For Opposite Reasons

Two mutations survived the first run entirely, and the two diagnoses are worth contrasting because the fixes are nothing alike.

F4 was unreachable, not unchecked

F4 originally removed the frame-budget check from the isochronous placement. It scored zero because the check can never bind there: chapter 27.5's admission control caps periodic traffic at 1350 bytes of a 1500-byte frame, so isochronous placement has room by construction and the test it was guarded by is never false.

The fix was to move the mutation to where the check actually does work: bulk. Bulk is the unbounded type, the frame budget is the only thing that stops it overrunning, and F4 went from 0 to 6762.

F5 was genuinely unchecked

F5 makes every periodic endpoint due in every frame. It scored zero for a completely different reason: nothing in the suite said an endpoint may not be served early.

Property 4 asserts that a due endpoint is served. Serving one that is not due violates nothing it says. And the deadline tracker cannot object either — serving more often than required can only make deadlines easier to meet.

So the suite gained property 5: a periodic endpoint is served only when due. F5 went from 0 to 4768.

13. Two Design Bugs the Bench Found

The round-robin pointer never rotated. Covered above — a single pointer advanced once per examined slot advances N_SLOT times per frame and lands back where it began. Found because the fairness phase measured a worst wait of 63 frames where 7 was expected.

The random phase was asking for the impossible. The first random phase configured an arbitrary endpoint mix and the design reported 386 missed deadlines. The scheduler was right: the stimulus had overcommitted the frame, and a periodic endpoint that does not fit misses a deadline nobody could have honoured.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Chapter 27.5 answers:  can these endpoints coexist?
   Chapter 27.6 answers:  in what order, this frame?

   The second only has an answer if the first said YES.

   So the random phase applies admission control before
   configuring -- it is a PRECONDITION of the guarantees
   this chapter's design makes, not an optional extra.

14. Follow-Ups the Interviewer Will Ask

"What happens if the periodic traffic does not all fit?" It cannot happen, because admission control refused the endpoint that would not fit. If it does happen, the scheduler is being run outside its contract and some endpoint misses a deadline — which is exactly why the 90% cap exists.

"How does the host know a periodic endpoint is due?" The interval is a power of two, so "every Nth frame" is a mask test on the frame number — no division, no per-endpoint counter. That is why the interval must be a power of two.

"What stops one bulk endpoint starving the others?" A rotating pointer, and nothing else. There is no reservation to appeal to.

"Is the bulk wait bounded?" Only while at least one bulk endpoint fits in every frame. In a frame whose periodic traffic fills the budget, no bulk is served, the pointer does not advance, and every bulk endpoint waits one frame longer. The bound is N_SLOT plus the number of such frames — which is why this suite asserts it where it is provable and reports the run-wide worst case rather than checking it against a number it is not required to meet.

"Can the schedule be changed while a frame is running?" No. A write mid-frame would change a schedule the host is already executing, and an endpoint already placed would be served under its old parameters. The host reconfigures between frames.

"What is different at high speed?" Microframes of 125 µs, so a periodic endpoint can be polled eight times as often; the periodic cap drops to 80%; and split transactions appear, where a high-speed hub's transaction translator holds a full-speed transaction across several microframes.

"Where does this go wrong in real controllers?" Almost always the same two places: the ordering (bulk somewhere it should not be) and the rotation (a pointer that does not survive the frame boundary). Both produce a device that works on a quiet bus.

15. UVM: Checking a Sequence and a Trend

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Two of this design's properties are not about transactions at all, and a
// scoreboard built around a queue of expected items cannot express either.
//
// ORDER is a property of one frame's SEQUENCE.
// FAIRNESS is a property of MANY frames and is invisible in one.
//
// So this component buffers a frame, checks the sequence when the frame
// closes, and keeps a per-endpoint wait history across the whole run.
typedef enum bit [1:0] { XT_CTRL, XT_ISO, XT_INT, XT_BULK } xfer_e;

class sched_entry extends uvm_sequence_item;
  `uvm_object_utils(sched_entry)

  rand bit [2:0]  slot;
  rand xfer_e     xtype;
  rand bit [15:0] bytes;
  rand bit        frame_end;

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


class frame_monitor extends uvm_subscriber #(sched_entry);
  `uvm_component_utils(frame_monitor)

  localparam int N_SLOT      = 8;
  localparam int FRAME_BYTES = 1500;

  // ---- the frame being collected ----
  xfer_e   seq_type [$];
  bit [2:0] seq_slot [$];
  int       frame_bytes;

  // ---- run-wide ----
  int unsigned n_frames, n_entries, n_order_violations;
  int unsigned n_overrun;

  // ---- fairness history, which one frame cannot supply ----
  int bulk_wait [N_SLOT];
  bit is_bulk   [N_SLOT];
  int unsigned worst_wait;
  int unsigned n_bulk_passed_over;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  // Priority is NOT the numeric type code. The encoding is arbitrary; the
  // placement order is the design's thesis. Mapping one to the other
  // explicitly is how this avoids testing the encoding by accident.
  function int prio(xfer_e t);
    case (t)
      XT_ISO:  return 0;
      XT_INT:  return 1;
      XT_CTRL: return 2;
      XT_BULK: return 3;
    endcase
  endfunction

  function void write(sched_entry t);
    if (!t.frame_end) begin
      seq_type.push_back(t.xtype);
      seq_slot.push_back(t.slot);
      frame_bytes += int'(t.bytes);
      n_entries++;
      return;
    end

    // ---- the frame has closed: check the SEQUENCE ----
    for (int i = 1; i < seq_type.size(); i++)
      if (prio(seq_type[i]) < prio(seq_type[i-1])) begin
        n_order_violations++;
        `uvm_error("SCHED/ORDER",
          $sformatf("frame %0d: %s placed after %s -- reserved traffic must be scheduled before best-effort",
                    n_frames, seq_type[i].name(), seq_type[i-1].name()))
      end

    if (frame_bytes > FRAME_BYTES) begin
      n_overrun++;
      `uvm_error("SCHED/OVERRUN",
        $sformatf("frame %0d carried %0d bytes into a %0d-byte frame",
                  n_frames, frame_bytes, FRAME_BYTES))
    end

    // ---- fairness: updated per frame, checked per frame ----
    //
    // Checked HERE rather than summarised and checked once at the end. A
    // single end-of-run assertion kills a non-rotating pointer exactly once,
    // and one kill is indistinguishable from luck.
    for (int s = 0; s < N_SLOT; s++) begin
      bit served = 0;
      foreach (seq_slot[i]) if (seq_slot[i] == s[2:0]) served = 1;

      if (!is_bulk[s]) begin
        bulk_wait[s] = 0;
      end else if (served) begin
        bulk_wait[s] = 0;
      end else begin
        bulk_wait[s]++;
        n_bulk_passed_over++;
        if (bulk_wait[s] > worst_wait) worst_wait = bulk_wait[s];
        if (bulk_wait[s] > N_SLOT)
          `uvm_error("SCHED/FAIR",
            $sformatf("bulk slot %0d has gone %0d frames unserved: the round-robin pointer is not rotating",
                      s, bulk_wait[s]))
      end
    end

    n_frames++;
    seq_type.delete();
    seq_slot.delete();
    frame_bytes = 0;
  endfunction

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

    `uvm_info("SCHED",
      $sformatf("%0d frames | %0d entries | worst bulk wait %0d | %0d order violations",
                n_frames, n_entries, worst_wait, n_order_violations), UVM_LOW)

    // A run in which bulk was never passed over says nothing about
    // fairness: correct rotation and total starvation produce identical
    // frames when there is room for everybody.
    if (n_bulk_passed_over == 0)
      `uvm_error("SCHED/COV",
        "no bulk endpoint was ever passed over: fairness was never at risk, so zero violations means nothing")
    if (worst_wait == 0)
      `uvm_error("SCHED/COV",
        "no bulk endpoint ever waited a frame: the rotation was never exercised")
  endfunction
endclass

16. Common Misconceptions

"The host schedules whatever is ready." It places reserved traffic first, by type, in a fixed order. Readiness decides nothing about position.

"Bulk and interrupt compete." Interrupt has a reservation and is placed first. Bulk gets the remainder.

"Round-robin is a fairness nicety." It is the only fairness mechanism bulk has. Without it one endpoint takes every frame and no error is reported.

"A broken rotation would be obvious." Every individual frame is perfect. Only the sequence of frames shows it, and F2 scored 1 until the property was checked per frame.

"Bulk starvation is bounded by the protocol." Nothing in the protocol bounds it. The bound comes from the host's implementation, and only while at least one bulk endpoint fits per frame.

"Serving a periodic endpoint early is harmless." It spends bandwidth reserved for somebody else, and for isochronous it delivers data the application has not produced.

"The schedule can be updated any time." Between frames. A mid-frame write changes a schedule already executing.

"A missed deadline means the scheduler is wrong." Not if the schedule was overcommitted. Admission control is a precondition, and violating it produced 386 "missed deadline" errors that were entirely the stimulus's fault.

"An entry's cost is its packet size." Plus the per-transaction overhead, every time.

17. Exercises

1. Derive the four-phase order from the properties in chapter 27.5 alone. Each position should be forced, not chosen.

2. F2 makes the pointer advance once per examined slot. Show that with N_SLOT slots it returns to its starting value every frame, and give the one change to N_SLOT that would accidentally hide the bug.

3. The fairness phase was changed from four 400-byte endpoints to eight 800-byte ones. Compute the worst wait in each case and explain why only the second discriminates.

4. F4 scored zero because the check it mutated cannot fail. Distinguish a dead mutation from a surviving one, and say what each tells you to do.

5. F5 scored zero against a suite that checked "every due endpoint is served". Write the missing property and explain why serving early is a real defect rather than a harmless one.

6. The bulk wait bound is N_SLOT only while one bulk endpoint fits per frame. Derive the general bound, and design a scheduler change that restores a fixed one.

7. Add high-speed split transactions: a full-speed transaction held by a hub across several microframes. Which of the seven properties change, and what new one is needed?

18. Summary

IdeaWhy it matters
Periodic traffic is placed firstit is how admission control's promise is kept
ISO before INT because it cannot retrya missed isochronous frame is gone
CTRL before BULKso the bus stays reconfigurable
Bulk is round-robinthe only fairness mechanism in the schedule
A broken rotation is invisible per frameevery frame is perfect; the sequence is not
The rotation needs two pointersone advanced per slot never rotates at all
"Due" is a mask test on the frame numberwhich is why intervals are powers of two
Served when due, and only when dueliveness and safety are two properties
Serving early spends someone else's bandwidthand delivers data that does not exist yet
No configuration writes mid-frameit would change a schedule already running
Admission control is a preconditiona scheduler cannot honour an unaffordable promise
Map type to priority explicitlyor you test the encoding by accident
A summarised property is checked onceF2 scored 1 until it was checked per frame
A dead mutation ≠ a surviving oneone means unreachable, the other means unchecked
Make the resource scarce to test fairness3-of-4 fitting gives a worst wait of 1
128 states, 24 orderings, 7 mutations0 out-of-order entries in 260,898 checks

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o fs_v.out fs_v.v fs_v_tb.v && ./fs_v.out
SystemVerilogiverilog -g2012 -o fs_sv.out fs_sv.sv fs_sv_tb.sv && ./fs_sv.out
VHDL-2008 analysenvc --std=2008 -a fs_vhdl.vhd fs_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_fs_vhdl
VHDL-2008 runnvc --std=2008 -r tb_fs_vhdl
One mutationiverilog -g2005 -DMUT_F1 -o mm fs_v_mut.v fs_v_tb.v && ./mm
Directed only (Verilog)iverilog -g2005 -DDIRECTED_ONLY -o mm fs_v_mut.v fs_v_tb.v && ./mm
Directed only (VHDL)nvc --std=2008 -e -gDIRECTED_ONLY=true tb_fs_vhdl

All three implementations pass with 0 errors: 3312 frames across all 128 combinations of type, interval and frame phase; 24 assignments of the four types to slots, so the table order is almost never the emission order; a fairness phase sized so that exactly one bulk endpoint fits per frame, giving a worst wait of exactly 7 against a bound of 8; zero out-of-order entries and zero frame overruns in 260,898 checks; and every one of the seven mutations killed by directed stimulus alone, with all seven directed scores identical across languages.


Chapter 27.7 — Senior Host-Controller Architecture is the first of the four senior questions, and it is a whiteboard exercise: draw an xHCI host controller. Its central mechanism is the one that makes the whole thing work without a lock — a cycle bit that lets producer and consumer share a ring buffer and always agree on who owns each entry.

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.