Skip to content
VLSI Mentor

USB · Module 31

“Bulk Transfers Are Always Fastest”

Usually true, which is what makes it dangerous. Bulk has the largest packets and no rate limit, and on a quiet bus it wins every benchmark. A frame-budget allocator shows whose property throughput actually is.

1. The Belief

"Bulk is the high-throughput transfer type. If you want speed, use bulk — it has the biggest packets and nothing throttles it. Isochronous is for audio and interrupt is for tiny status reads."

2. Why An Intelligent Engineer Believes It

Because on the bus this engineer is measuring, it is true. Every part of the reasoning is sound.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    THE NUMBERS SUPPORT IT
      bulk maximum packet, high speed      512 bytes
      interrupt maximum packet, high speed  64 bytes typically
      isochronous per (micro)frame          capped at what was reserved

    THE RULES SUPPORT IT
      bulk has NO reserved bandwidth and NO interval limit, so nothing
      stops the host from issuing bulk transactions back to back as
      fast as the bus can carry them

    THE MEASUREMENT SUPPORTS IT
      benchmark a bulk endpoint on a bus with nothing else on it and
      you will measure the highest number USB can produce. Mass
      storage, network adaptors and every high-rate device use bulk,
      and they are right to.

    THE DOCUMENTATION SUPPORTS IT
      "bulk: large, non-time-critical transfers" is in every summary
      table ever printed, next to "isochronous: guaranteed bandwidth",
      and the reader takes "guaranteed" to mean "limited".

3. The Prediction It Makes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    IF BULK WERE ALWAYS FASTEST, THEN:

    1  bulk throughput would be a property of the DEVICE and its
       packet size -- measurable once, valid thereafter

    2  adding an unrelated device to the bus could not change a bulk
       endpoint's rate very much

    3  isochronous could never beat bulk, since it is capped per frame

    4  a bulk transfer would have a worst-case latency you could
       compute

    5  "use bulk for speed" would be advice that needs no conditions

Prediction 1 is the one that costs money, because it is the one that makes it into a datasheet.

4. The Counterexample

One bus, one measurement, taken twice.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    STEP 1  a high-speed bulk IN endpoint, alone on the bus.
            Measure. Call it 100%.

    STEP 2  plug in a USB audio interface or a webcam and START IT.
            It claims isochronous bandwidth at configuration time --
            an amount the host reserved before allowing the setting.

    STEP 3  measure the bulk endpoint again, unchanged.

    OBSERVED
      bulk throughput drops, and it drops by roughly the bandwidth the
      isochronous device reserved. Nothing is broken. Nothing NAKed
      incorrectly. No error counter moved. The host simply has less
      frame left to give away, and bulk is what it gives away LAST.

    PUSH IT FURTHER
      reserve enough periodic bandwidth -- the specification permits
      periodic transfers up to a large fraction of the frame -- and
      bulk approaches ZERO while remaining entirely correct.

Prediction 2 fails immediately. Prediction 3 fails in the same measurement: the isochronous stream's rate did not move at all, because it was reserved. The "capped" transfer type held its number and the "unlimited" one collapsed.

Prediction 4 fails structurally, not experimentally: bulk has no worst-case latency. A guarantee is exactly the thing it does not have. Isochronous and interrupt have bounded service intervals; bulk has a promise of delivery with no promise of when.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    THE FRAME, AND WHY THE ORDER IS THE WHOLE ANSWER

    |<--------------------- one frame's budget --------------------->|
    |                                                               |
    | ISOCHRONOUS reserved | INTERRUPT reserved | ...what is left... |
    |<---- claimed 1st ---->|<--- claimed 2nd -->|<-- BULK gets this -|

    o  the first two regions are sized at CONFIGURATION time, by the
       host, when it agreed to the alternate setting
    o  the third region is whatever remains, and it is not negotiated,
       not reserved and not guaranteed
    o  a device cannot enlarge the third region by asking, by being
       fast, or by having 512-byte packets
    o  if the first two fill the frame, the third is empty and every
       component is working correctly

5. The Corrected Model

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    THROUGHPUT IS NOT A PROPERTY OF A TRANSFER TYPE.
    It is the outcome of an ALLOCATION, and the host allocates.

    WHAT EACH TYPE ACTUALLY BUYS

      ISOCHRONOUS   a reservation, and no retries. You get your bytes
                    every frame or the frame is lost -- there is no
                    second chance and no handshake. Bounded rate,
                    bounded latency, NO delivery guarantee.

      INTERRUPT     a reservation of a service INTERVAL. Small
                    payloads, bounded latency, retried on error.
                    Bounded latency, low throughput.

      BULK          no reservation, no interval, largest packets,
                    retried on error, delivery guaranteed EVENTUALLY.
                    Unbounded latency, highest throughput AVAILABLE.

      CONTROL       a reserved minimum share, because enumeration must
                    always be possible. Never for throughput.

    SO THE CORRECT SENTENCE IS:

      bulk has the highest throughput CEILING and the weakest
      guarantee; isochronous has a fixed floor and no retries. Which
      is "fastest" depends entirely on what else is on the bus, and on
      whether you mean peak rate or worst case.

6. The Hardware Contract

The allocator is where the belief becomes checkable, because allocation order is structural: you can read it off the RTL.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    PURPOSE     given one frame's capacity and this frame's demands,
                divide the capacity between three classes of traffic

    INPUTS      clk, rst_n
                frame_start          one cycle: a new frame begins
                iso_req  [15:0]      isochronous bytes wanted
                intr_req [15:0]      interrupt bytes wanted
                bulk_req [15:0]      bulk bytes offered
                budget [15:0]        the frame's total capacity
                periodic_cap [15:0]  the most of it periodic traffic
                                     may claim

    OUTPUTS     iso_grant, intr_grant, bulk_grant  [15:0]
                periodic_over        periodic asked beyond its
                                     reservation
                bulk_starved         bulk asked and received nothing
                n_frames, n_bulk_starved
                tot_iso, tot_intr, tot_bulk  [31:0]

    AUTHORITATIVE STATE
                the registered grants and the running totals. The
                division itself is combinational and is sampled on
                frame_start.

    DERIVED     THE ORDER, and it is the chapter:
                  per_limit = min(periodic_cap, budget)
                  iso_g     = min(iso_req,  per_limit)
                  per_left  = per_limit - iso_g
                  intr_g    = min(intr_req, per_left)
                  used      = iso_g + intr_g
                  frm_left  = budget > used ? budget - used : 0
                  bulk_g    = min(bulk_req, frm_left)

    RESET       all grants and totals zero.

    PRIORITY    isochronous, then interrupt, then bulk. Bulk sees only
                what the FRAME has left after the reservations -- note
                which number frm_left subtracts from -- and that can
                legitimately be zero.

    WHY periodic_cap IS AN INPUT
                the fraction of a frame periodic traffic may claim
                differs between full-speed frames and high-speed
                microframes. The chapter states both numbers; the
                hardware takes one.

    LATENCY     one frame_start to one allocation, registered.

    BOUNDARY    periodic_cap > budget is CLAMPED, so grants never
                exceed the frame -- see the assumption below, which was
                found by a check rather than by inspection.
                bulk_req > 0 with frm_left == 0 raises bulk_starved,
                which is a REPORT and not an error.
                periodic asking beyond its reservation raises
                periodic_over, likewise an observation: a real host
                would have refused the configuration.

    ASSUMPTIONS periodic_cap is a fraction OF THE FRAME, so
                periodic_cap <= budget. The module CLAMPS rather than
                trusts, because a reservation larger than the frame
                would otherwise be granted -- bytes the frame does not
                contain.

    OMISSIONS   the transaction list, endpoint descriptors, retries,
                split transactions, NAK handling, frame numbers,
                microframes, packet overhead, bit stuffing, and the
                fact that a real host schedules TRANSACTIONS rather
                than bytes.

    MISCONCEPTION DEMONSTRATED
                "bulk is fastest, so serve it first" and
                "a reservation is a suggestion"

usb_frame_budget.v — the design, Verilog-2005

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_frame_budget -- where a frame's bytes actually go.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL / scheduling
//  MODEL. It is NOT a host controller. There is no transaction list, no
//  endpoint descriptors, no retry, no split transaction, no NAK
//  handling and no notion of packets at all. One frame's worth of
//  capacity is divided between three classes of traffic, once per
//  frame_start, and that is the whole module.
//
//  It exists because "which transfer type is fastest" is a question
//  about ALLOCATION, and allocation is the one part of the answer that
//  can be written down as arithmetic.
//
//  THE ORDER, WHICH IS THE ARCHITECTURE
//  ------------------------------------
//      1  periodic traffic (isochronous, then interrupt) is served
//         first, up to a RESERVATION CAP
//      2  bulk receives whatever the frame has left
//
//  Two consequences follow from those two lines, and they are the whole
//  of chapter 31.4:
//
//      on an EMPTY frame, bulk receives the entire budget and is
//      indeed the fastest thing available
//
//      on a LOADED frame, isochronous receives its reservation
//      REGARDLESS of how much bulk wants, and bulk receives the
//      remainder -- which can be nothing
//
//  Neither class is "faster". One has a guarantee and the other has
//  whatever is left, and which of those is better depends entirely on
//  what you are measuring.
//
//  SCOPE OF THE CAP: the reservation limit is an INPUT here rather than
//  a constant, because the fraction of a frame that periodic traffic
//  may claim differs between full-speed frames and high-speed
//  microframes. The chapter states both; the hardware takes a number.
//
//  ASSUMPTION, and it is CHECKED rather than assumed: periodic_cap is a
//  fraction OF THE FRAME, so periodic_cap <= budget. The module clamps
//  it rather than trusting it, because a reservation larger than the
//  frame would otherwise be granted -- bytes the frame does not
//  contain. See 31.4's account of how that clamp came to be written.
// =====================================================================
module usb_frame_budget (
  input  wire        clk,
  input  wire        rst_n,

  // One cycle: a new frame begins and the budget is redivided.
  input  wire        frame_start,

  // How many bytes each class would like this frame.
  input  wire [15:0] iso_req,
  input  wire [15:0] intr_req,
  input  wire [15:0] bulk_req,

  // The frame's total capacity, and the most of it periodic traffic
  // may claim.
  input  wire [15:0] budget,
  input  wire [15:0] periodic_cap,

  // What each class got.
  output wire [15:0] iso_grant,
  output wire [15:0] intr_grant,
  output wire [15:0] bulk_grant,

  // Periodic traffic asked for more than its reservation allows. The
  // host would have refused the endpoint at configuration time; here it
  // is an observable fact rather than an error.
  output wire        periodic_over,
  // Bulk wanted bytes and received none. NOT an error: it is the
  // defined behaviour of a class with no reservation, and it is the
  // measurement the misconception cannot survive.
  output wire        bulk_starved,

  output wire [15:0] n_frames,
  output wire [15:0] n_bulk_starved,
  output wire [31:0] tot_iso,
  output wire [31:0] tot_intr,
  output wire [31:0] tot_bulk
);

  reg [15:0] iso_r, intr_r, bulk_r;
  reg        over_r;
  reg [15:0] c_frames, c_starved;
  reg [31:0] t_iso, t_intr, t_bulk;

  // ---- the division, in the order the architecture requires --------
  // The FRAME is the hard limit and the reservation is a sub-limit
  // inside it. A reservation larger than the frame is a nonsensical
  // configuration, and the clamp below is what stops it becoming a
  // grant of bytes the frame does not contain. The intent check in the
  // testbench -- "grants never exceed the budget" -- is what found
  // this; the reference model computed the same over-grant and agreed.
  wire [15:0] per_limit = (periodic_cap > budget) ? budget : periodic_cap;

  // Isochronous first, capped by the reservation.
  wire [15:0] iso_g  = (iso_req  > per_limit) ? per_limit : iso_req;
  // Interrupt next, from whatever the reservation has left.
  wire [15:0] per_left = per_limit - iso_g;
  wire [15:0] intr_g = (intr_req > per_left) ? per_left : intr_req;
  // Bulk last, from whatever the FRAME has left. Note which number
  // this subtracts from: the frame budget, not the reservation.
  wire [15:0] used     = iso_g + intr_g;
  wire [15:0] frm_left = (budget > used) ? (budget - used) : 16'd0;
  wire [15:0] bulk_g = (bulk_req > frm_left) ? frm_left : bulk_req;

  wire over = ((iso_req + intr_req) > per_limit);

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      iso_r <= 16'd0; intr_r <= 16'd0; bulk_r <= 16'd0;
      over_r <= 1'b0;
      c_frames <= 16'd0; c_starved <= 16'd0;
      t_iso <= 32'd0; t_intr <= 32'd0; t_bulk <= 32'd0;
    end else if (frame_start) begin
      iso_r  <= iso_g;
      intr_r <= intr_g;
      bulk_r <= bulk_g;
      over_r <= over;
      c_frames <= c_frames + 16'd1;
      t_iso  <= t_iso  + {16'd0, iso_g};
      t_intr <= t_intr + {16'd0, intr_g};
      t_bulk <= t_bulk + {16'd0, bulk_g};
      if ((bulk_req != 16'd0) && (bulk_g == 16'd0))
        c_starved <= c_starved + 16'd1;
    end
  end

  assign iso_grant      = iso_r;
  assign intr_grant     = intr_r;
  assign bulk_grant     = bulk_r;
  assign periodic_over  = over_r;
  assign bulk_starved   = (bulk_r == 16'd0);
  assign n_frames       = c_frames;
  assign n_bulk_starved = c_starved;
  assign tot_iso        = t_iso;
  assign tot_intr       = t_intr;
  assign tot_bulk       = t_bulk;

endmodule

The clamp, and how it was found

per_limit = min(periodic_cap, budget) was not in the first version of this block, and the story is the reason this section exists.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    BEFORE      periodic_cap was used directly. With a cap larger than
                the budget, iso_g + intr_g could exceed the frame.

    THE DUT AND THE REFERENCE MODEL BOTH DID THIS, AND AGREED.
    Thousands of checks. Zero mismatches. A clean report.

    WHAT DISAGREED  the intent check, which says, in its own terms and
                    without consulting the model:

                      the three grants must never SUM to more
                      than the budget

    That one line is the only thing in the entire bench that knew the
    design was wrong.

This is exactly the failure 30.4 documented, where a set/clear expression was backwards in the design and in the model, and the two agreed for 39,108 checks under a comment saying what the design should do. Two artefacts written by the same person from the same sentence are one artefact. A reference model measures consistency; only an independently stated intent measures correctness.

7. The Testbench

The frame's capacity is 1,500 bytes and the reservation cap 1,350 — full speed at 90% — unless a phase changes them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    PHASE 1  INTENT -- two claims, stated in the architecture's terms
             and consulting no model:

      C1  AN EMPTY FRAME: bulk receives the whole 1,500 and
          isochronous receives nothing. THE MISCONCEPTION IS CORRECT
          HERE, and saying so is the point -- it is why the belief
          survives contact with a benchmark.

      C2  THE RELATIONAL CLAIM, run eight times. Take one periodic
          load. Run the frame with bulk_req = 0, then run the SAME
          periodic load with bulk_req = 9000, and require:
            o  iso_grant IDENTICAL in both runs
            o  bulk_grant STRICTLY LARGER in the second

          The first half says bulk demand cannot move a reservation.
          The second says the bench is actually varying something --
          without it, a design that granted bulk nothing at all would
          satisfy the first half perfectly.

      and running throughout every phase, the unconditional check:
            total grants never exceed the budget

    PHASE 2  the load grid -- see the axes below

    PHASE 3  SCENARIOS -- including a backlog that persists across
             frames, which phase 2 structurally cannot contain

C2 is the counterexample from section 4 written as a test, and note what it asserts: not a number, but a relation. "Isochronous is unmoved" and "bulk moved" are both claims about a pair of runs, which is the only shape in which "throughput is not a property of the device" can be stated at all.

tb_usb_frame_budget.v — the testbench, Verilog-2005

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  tb_usb_frame_budget -- Verilog-2005 testbench for usb_frame_budget.
//
//  PHASE 1 is the intent phase. It does not compare against a model; it
//  states the two architectural claims that together refute "bulk is
//  fastest" and requires them:
//
//      C1  on an EMPTY frame, bulk receives the entire budget
//      C2  on a LOADED frame, the isochronous grant does NOT depend on
//          how much bulk wants
//
//  C2 is the one that matters and it is a RELATIONAL property: it
//  compares two runs that differ only in bulk_req. No single-run check
//  can express it, and no reference model that computes the grants the
//  same way the design does can fail it independently.
//
//  PHASES
//    1 INTENT      C1 and C2, the second as a paired-run comparison
//    2 EXHAUSTIVE  the load grid: 5 iso x 4 bulk x 2 caps
//    3 SCENARIO    the named boundaries
//    4 RANDOM      supplementary, audited
// =====================================================================
`timescale 1ns/1ps

module tb_usb_frame_budget;

  reg        clk = 1'b0;
  reg        rst_n;
  reg        frame_start;
  reg [15:0] iso_req, intr_req, bulk_req, budget, periodic_cap;

  wire [15:0] iso_grant, intr_grant, bulk_grant;
  wire        periodic_over, bulk_starved;
  wire [15:0] n_frames, n_bulk_starved;
  wire [31:0] tot_iso, tot_intr, tot_bulk;

  usb_frame_budget dut (
    .clk(clk), .rst_n(rst_n), .frame_start(frame_start),
    .iso_req(iso_req), .intr_req(intr_req), .bulk_req(bulk_req),
    .budget(budget), .periodic_cap(periodic_cap),
    .iso_grant(iso_grant), .intr_grant(intr_grant), .bulk_grant(bulk_grant),
    .periodic_over(periodic_over), .bulk_starved(bulk_starved),
    .n_frames(n_frames), .n_bulk_starved(n_bulk_starved),
    .tot_iso(tot_iso), .tot_intr(tot_intr), .tot_bulk(tot_bulk)
  );

  always #5 clk = ~clk;

  // ---- the independent reference model ---------------------------
  // Written as the RULES, in words, then arithmetic:
  //   R1 periodic traffic is served first, within its reservation
  //   R2 isochronous takes precedence inside the reservation
  //   R3 bulk receives what the FRAME has left, not what the
  //      reservation has left
  reg [15:0] rm_iso, rm_intr, rm_bulk;
  reg [15:0] rm_frames, rm_starved;
  reg [31:0] rm_tiso, rm_tintr, rm_tbulk;

  integer chk_dir, chk_rnd, err, in_random;
  integer m_frames, m_empty, m_loaded, m_starved, m_over, m_bulk_full,
          m_setupfail;
  integer i, j, a, b, c;

  task bump; begin
    if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
  end endtask

  task ck;
    input [255:0] what;
    input [31:0]  got;
    input [31:0]  exp;
    begin
      bump;
      if (got !== exp) begin
        err = err + 1;
        if (!in_random && err <= 40)
          $display("  ** %0s: got %0d expected %0d  (t=%0t)", what, got, exp, $time);
      end
    end
  endtask

  function [15:0] mn;
    input [15:0] x;
    input [15:0] y;
    begin mn = (x < y) ? x : y; end
  endfunction

  task ref_step;
    reg [15:0] ig, il, tg, fl, bg, pl;
    begin
      if (!rst_n) begin
        rm_iso = 0; rm_intr = 0; rm_bulk = 0;
        rm_frames = 0; rm_starved = 0;
        rm_tiso = 0; rm_tintr = 0; rm_tbulk = 0;
      end else if (frame_start) begin
        // R0 the frame is the hard limit; the reservation lives inside
        //    it. This line was absent from both the design and this
        //    model, and they agreed with each other about the result.
        pl = mn(periodic_cap, budget);
        ig = mn(iso_req, pl);                           // R2
        il = pl - ig;
        tg = mn(intr_req, il);                          // R1
        fl = (budget > (ig + tg)) ? (budget - ig - tg) : 16'd0;
        bg = mn(bulk_req, fl);                          // R3
        rm_iso = ig; rm_intr = tg; rm_bulk = bg;
        rm_frames = rm_frames + 1;
        rm_tiso  = rm_tiso  + ig;
        rm_tintr = rm_tintr + tg;
        rm_tbulk = rm_tbulk + bg;
        if (bulk_req != 0 && bg == 0) rm_starved = rm_starved + 1;
        // tallies
        m_frames = m_frames + 1;
        if (iso_req == 0 && intr_req == 0) m_empty  = m_empty  + 1;
        else                               m_loaded = m_loaded + 1;
        if (bulk_req != 0 && bg == 0)      m_starved = m_starved + 1;
        if ((iso_req + intr_req) > pl) m_over = m_over + 1;
        if (bulk_req != 0 && bg == bulk_req) m_bulk_full = m_bulk_full + 1;
      end
    end
  endtask

  task cmp; begin
    ck("iso_grant",   {16'd0, iso_grant},   {16'd0, rm_iso});
    ck("intr_grant",  {16'd0, intr_grant},  {16'd0, rm_intr});
    ck("bulk_grant",  {16'd0, bulk_grant},  {16'd0, rm_bulk});
    ck("n_frames",    {16'd0, n_frames},    {16'd0, rm_frames});
    ck("n_starved",   {16'd0, n_bulk_starved}, {16'd0, rm_starved});
    ck("tot_iso",     tot_iso,  rm_tiso);
    ck("tot_bulk",    tot_bulk, rm_tbulk);
  end endtask

  // The claim, asserted directly: the three grants never exceed the
  // frame, and periodic never exceeds its reservation.
  task intent_check; begin
    bump;
    if ((iso_grant + intr_grant + bulk_grant) > budget) begin
      err = err + 1;
      $display("  ** INTENT VIOLATED: grants exceed the frame budget (t=%0t)", $time);
    end
    bump;
    if ((iso_grant + intr_grant) > periodic_cap) begin
      err = err + 1;
      $display("  ** INTENT VIOLATED: periodic exceeded its reservation (t=%0t)", $time);
    end
  end endtask

  task step; begin
    #1;
    @(posedge clk);
    ref_step;
    #1;
    cmp;
    intent_check;
    frame_start = 0;
  end endtask

  task hard_reset; begin
    rst_n = 0; frame_start = 0;
    iso_req = 0; intr_req = 0; bulk_req = 0;
    budget = 16'd1500; periodic_cap = 16'd1350;
    repeat (3) begin @(posedge clk); ref_step; end
    #1; rst_n = 1;
    @(posedge clk); ref_step; #1; cmp;
  end endtask

  task frame;
    input [15:0] iso;
    input [15:0] intr;
    input [15:0] blk;
    begin
      frame_start = 1; iso_req = iso; intr_req = intr; bulk_req = blk;
      step;
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 1 -- THE TWO CLAIMS.
  // -----------------------------------------------------------------
  integer paired_runs;
  reg [15:0] iso_a, iso_b, bulk_a, bulk_b;

  task phase_intent;
    begin
      hard_reset;
      paired_runs = 0;

      // C1 an empty frame: bulk gets everything. The misconception is
      //    CORRECT here, and saying so is the point -- it is why the
      //    belief survives.
      frame(16'd0, 16'd0, 16'd2000);
      ck("C1 bulk got the whole frame", {16'd0, bulk_grant}, 32'd1500);
      ck("C1 iso got nothing",          {16'd0, iso_grant},  32'd0);

      // C2 the relational claim. Run the SAME periodic load twice,
      //    changing only what bulk asks for, and require the
      //    isochronous grant to be identical. If bulk could take
      //    priority by wanting more, this is where it would show.
      for (i = 0; i < 8; i = i + 1) begin
        hard_reset;
        frame(16'd100 + i[15:0] * 16'd100, 16'd50, 16'd0);
        iso_a = iso_grant; bulk_a = bulk_grant;
        hard_reset;
        frame(16'd100 + i[15:0] * 16'd100, 16'd50, 16'd9000);
        iso_b = iso_grant; bulk_b = bulk_grant;
        ck("C2 iso is unmoved by bulk demand", {16'd0, iso_a}, {16'd0, iso_b});
        bump;
        if (bulk_b <= bulk_a) begin
          err = err + 1;
          $display("  ** C2: bulk asking for more did not get more (%0d vs %0d)",
                   bulk_a, bulk_b);
        end
        paired_runs = paired_runs + 1;
      end
      bump;
      if (paired_runs != 8) begin
        err = err + 1;
        $display("  ** intent: only %0d paired runs", paired_runs);
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 2 -- the load grid.
  //
  //  AXES:
  //    isochronous demand   0, 300, 700, 1200, 1600 bytes         5
  //    bulk demand          0, 200, 1500, 9000 bytes              4
  //    reservation cap      1350 (full-speed 90%), 1200 (80%)     2
  //    ---------------------------------------------------------------
  //                                            5 x 4 x 2 =       40
  //
  //  Interrupt demand is held at 50 rather than swept: within the
  //  reservation it behaves exactly as isochronous does, and sweeping
  //  it would multiply the grid without adding a case. That is a claim
  //  a reviewer can disagree with, which is why it is written here --
  //  30.2 §6.
  //
  //  WHAT IS ABSENT: time. Every frame here is independent. A backlog
  //  that persists across frames is phase 3.
  // -----------------------------------------------------------------
  reg [15:0] isos [0:4];
  reg [15:0] blks [0:3];
  reg [15:0] caps [0:1];

  task phase_sweep;
    begin
      isos[0]=16'd0; isos[1]=16'd300; isos[2]=16'd700;
      isos[3]=16'd1200; isos[4]=16'd1600;
      blks[0]=16'd0; blks[1]=16'd200; blks[2]=16'd1500; blks[3]=16'd9000;
      caps[0]=16'd1350; caps[1]=16'd1200;
      for (a = 0; a < 5; a = a + 1)
      for (b = 0; b < 4; b = b + 1)
      for (c = 0; c < 2; c = c + 1) begin
        hard_reset;
        periodic_cap = caps[c];
        bump;
        if (periodic_cap !== caps[c]) begin
          err = err + 1; m_setupfail = m_setupfail + 1;
          $display("  ** setup: cap not applied");
        end
        frame(isos[a], 16'd50, blks[b]);
        frame(16'd0, 16'd0, 16'd0);
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 3 -- the named boundaries.
  // -----------------------------------------------------------------
  task phase_scenarios;
    begin
      // S1 periodic exactly at the reservation: bulk gets the rest.
      hard_reset;
      frame(16'd1300, 16'd50, 16'd9000);
      ck("S1 iso got all it asked",  {16'd0, iso_grant},  32'd1300);
      ck("S1 interrupt too",         {16'd0, intr_grant}, 32'd50);
      ck("S1 bulk got the remainder",{16'd0, bulk_grant}, 32'd150);
      ck("S1 not flagged over",      {31'd0, periodic_over}, 32'd0);

      // S2 periodic one byte over: the reservation is a CAP, not a
      //    suggestion, and the excess is refused rather than taken
      //    from bulk.
      hard_reset;
      frame(16'd1301, 16'd50, 16'd9000);
      ck("S2 flagged over",          {31'd0, periodic_over}, 32'd1);
      ck("S2 periodic capped",       {16'd0, iso_grant + intr_grant}, 32'd1350);
      ck("S2 bulk still gets 150",   {16'd0, bulk_grant}, 32'd150);

      // S3 THE COUNTEREXAMPLE. Periodic fills the reservation
      //    completely and the frame has nothing left. Bulk asks for
      //    nine thousand bytes and receives zero -- for as many frames
      //    as this lasts.
      hard_reset;
      periodic_cap = 16'd1500;
      for (i = 0; i < 10; i = i + 1) frame(16'd1500, 16'd0, 16'd9000);
      ck("S3 bulk got nothing, ten frames running", {16'd0, n_bulk_starved}, 32'd10);
      ck("S3 while iso got everything", tot_iso, 32'd15000);
      ck("S3 and bulk got zero bytes",  tot_bulk, 32'd0);

      // S4 the mirror: an idle periodic load, ten frames, bulk takes
      //    the lot. Same design, same parameters, opposite verdict.
      hard_reset;
      for (i = 0; i < 10; i = i + 1) frame(16'd0, 16'd0, 16'd9000);
      ck("S4 bulk never starved",   {16'd0, n_bulk_starved}, 32'd0);
      ck("S4 and took every byte",  tot_bulk, 32'd15000);

      // S5 a tiny bulk request always fits when there is room, and
      //    never fits when there is not. "Fastest" is not a property
      //    of the request.
      hard_reset;
      frame(16'd0,    16'd0, 16'd1);
      ck("S5 one byte, empty frame",  {16'd0, bulk_grant}, 32'd1);
      hard_reset;
      periodic_cap = 16'd1500;
      frame(16'd1500, 16'd0, 16'd1);
      ck("S5 one byte, full frame",   {16'd0, bulk_grant}, 32'd0);
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 4 -- random, audited.
  // -----------------------------------------------------------------
  task phase_random;
    integer r;
    begin
      in_random = 1;
      hard_reset;
      for (j = 0; j < 3000; j = j + 1) begin
        r = {$random} % 100;
        if (r < 20) periodic_cap = ({$random} % 1600);
        if (r < 30) budget       = 16'd1000 + ({$random} % 800);
        frame(({$random} % 1700), ({$random} % 300), ({$random} % 4000));
      end
      in_random = 0;
    end
  endtask

  initial begin
    chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
    m_frames=0; m_empty=0; m_loaded=0; m_starved=0; m_over=0;
    m_bulk_full=0; m_setupfail=0;

    phase_intent;
    $display("  phase 1 intent      : %0d checks, %0d errors  (%0d paired runs)",
             chk_dir, err, paired_runs);
    phase_sweep;
    $display("  phase 2 exhaustive  : %0d checks, %0d errors  (40 combinations)", chk_dir, err);
    phase_scenarios;
    $display("  phase 3 scenarios   : %0d checks, %0d errors", chk_dir, err);
    $display("  ---- DIRECTED-ONLY  : %0d checks, %0d errors ----", chk_dir, err);
    phase_random;

    $display("");
    $display("  measured reachability (all phases)");
    $display("    frames scheduled ....... %0d", m_frames);
    $display("      with no periodic ..... %0d", m_empty);
    $display("      with periodic load ... %0d", m_loaded);
    $display("    frames starving bulk ... %0d", m_starved);
    $display("    bulk fully satisfied ... %0d", m_bulk_full);
    $display("    periodic over its cap .. %0d", m_over);
    $display("    setup failures ......... %0d", m_setupfail);
    $display("");
    $display("  directed checks ........ %0d", chk_dir);
    $display("  random checks .......... %0d", chk_rnd);
    $display("  TOTAL checks ........... %0d", chk_dir + chk_rnd);
    $display("  ERRORS ................. %0d", err);
    if (err == 0) $display("  PASS"); else $display("  FAIL");
    $finish;
  end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                          VERILOG   SYSTEMVERILOG   VHDL-2008
    phase 1 intent            291           291          291
    phase 2 load grid       1,331         1,331        1,331
    phase 3 scenarios       1,603         1,603        1,603
    ---- DIRECTED           1,603         1,603        1,603
    errors                      0             0            0
    TOTAL                  28,610        28,610       28,610

    measured reachability, Verilog run
      frames scheduled .................... 3,121
        with no periodic demand ............... 52
        with periodic load ................. 3,069
      frames in which bulk was STARVED ....... 191
      frames in which bulk was fully satisfied 587
      periodic demand over its reservation . 1,880
      paired intent runs ....................... 8

Three rows carry the argument.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    52 EMPTY FRAMES vs 3,069 LOADED ONES

      On the 52, bulk takes the entire budget and the belief is right.
      On the 3,069 it is not, and the belief was formed on a bus that
      looked like the 52.

    191 STARVATIONS, 0 ERRORS

      A state the belief calls a malfunction and the architecture calls
      correct operation, reached 191 times with nothing wrong.

    1,880 FRAMES WITH PERIODIC OVER ITS RESERVATION

      The case the clamp exists for. Without periodic_over as an
      OUTPUT this would be invisible; with it, "the configuration was
      refusable" is an observation rather than an inference.

The load grid, and its named axes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    AXES
      isochronous demand   0, 300, 700, 1200, 1600 bytes           5
      bulk demand          0, 200, 1500, 9000 bytes                4
      reservation cap      1350 (full-speed 90%), 1200 (80%)       2
      ---------------------------------------------------------------
                                              5 x 4 x 2 =         40

All 40 reached. Three of the axis values are chosen against the boundary rather than spread across a range, and that is the whole design of the grid:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    iso 1200   at the 1200 cap and BELOW the 1350 cap
    iso 1600   ABOVE both caps -- the over-demand case
    bulk 9000  far larger than any frame, so bulk is always truncated
    bulk    0  so "bulk got nothing" can be distinguished from
               "bulk asked for nothing"

8. SystemVerilog

usb_frame_budget_sv.sv — the design, SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_frame_budget_sv -- the same contract in SystemVerilog. Same
//  ports, same order of allocation, same reset, same latency.
//
//  The SVA block at the bottom is why this version matters to the
//  chapter: the claim that isochronous is unaffected by bulk demand
//  is written as a property in which bulk demand does not appear.
//
//  ------------------------------------------------------------------
//  Where a frame's bytes actually go.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL / scheduling
//  MODEL. It is NOT a host controller. There is no transaction list, no
//  endpoint descriptors, no retry, no split transaction, no NAK
//  handling and no notion of packets at all. One frame's worth of
//  capacity is divided between three classes of traffic, once per
//  frame_start, and that is the whole module.
//
//  It exists because "which transfer type is fastest" is a question
//  about ALLOCATION, and allocation is the one part of the answer that
//  can be written down as arithmetic.
//
//  THE ORDER, WHICH IS THE ARCHITECTURE
//  ------------------------------------
//      1  periodic traffic (isochronous, then interrupt) is served
//         first, up to a RESERVATION CAP
//      2  bulk receives whatever the frame has left
//
//  Two consequences follow from those two lines, and they are the whole
//  of chapter 31.4:
//
//      on an EMPTY frame, bulk receives the entire budget and is
//      indeed the fastest thing available
//
//      on a LOADED frame, isochronous receives its reservation
//      REGARDLESS of how much bulk wants, and bulk receives the
//      remainder -- which can be nothing
//
//  Neither class is "faster". One has a guarantee and the other has
//  whatever is left, and which of those is better depends entirely on
//  what you are measuring.
//
//  SCOPE OF THE CAP: the reservation limit is an INPUT here rather than
//  a constant, because the fraction of a frame that periodic traffic
//  may claim differs between full-speed frames and high-speed
//  microframes. The chapter states both; the hardware takes a number.
//
//  ASSUMPTION, and it is CHECKED rather than assumed: periodic_cap is a
//  fraction OF THE FRAME, so periodic_cap <= budget. The module clamps
//  it rather than trusting it, because a reservation larger than the
//  frame would otherwise be granted -- bytes the frame does not
//  contain. See 31.4's account of how that clamp came to be written.
// =====================================================================
module usb_frame_budget_sv (
  input  logic        clk,
  input  logic        rst_n,

  // One cycle: a new frame begins and the budget is redivided.
  input  logic        frame_start,

  // How many bytes each class would like this frame.
  input  logic [15:0] iso_req,
  input  logic [15:0] intr_req,
  input  logic [15:0] bulk_req,

  // The frame's total capacity, and the most of it periodic traffic
  // may claim.
  input  logic [15:0] budget,
  input  logic [15:0] periodic_cap,

  // What each class got.
  output logic [15:0] iso_grant,
  output logic [15:0] intr_grant,
  output logic [15:0] bulk_grant,

  // Periodic traffic asked for more than its reservation allows. The
  // host would have refused the endpoint at configuration time; here it
  // is an observable fact rather than an error.
  output logic        periodic_over,
  // Bulk wanted bytes and received none. NOT an error: it is the
  // defined behaviour of a class with no reservation, and it is the
  // measurement the misconception cannot survive.
  output logic        bulk_starved,

  output logic [15:0] n_frames,
  output logic [15:0] n_bulk_starved,
  output logic [31:0] tot_iso,
  output logic [31:0] tot_intr,
  output logic [31:0] tot_bulk
);

  logic [15:0] iso_r, intr_r, bulk_r;
  logic        over_r;
  logic [15:0] c_frames, c_starved;
  logic [31:0] t_iso, t_intr, t_bulk;

  // ---- the division, in the order the architecture requires --------
  // The FRAME is the hard limit and the reservation is a sub-limit
  // inside it. A reservation larger than the frame is a nonsensical
  // configuration, and the clamp below is what stops it becoming a
  // grant of bytes the frame does not contain. The intent check in the
  // testbench -- "grants never exceed the budget" -- is what found
  // this; the reference model computed the same over-grant and agreed.
  logic [15:0] per_limit;
  assign per_limit = (periodic_cap > budget) ? budget : periodic_cap;

  // Isochronous first, capped by the reservation.
  logic [15:0] iso_g;
  assign iso_g  = (iso_req  > per_limit) ? per_limit : iso_req;
  // Interrupt next, from whatever the reservation has left.
  logic [15:0] per_left;
  assign per_left = per_limit - iso_g;
  logic [15:0] intr_g;
  assign intr_g = (intr_req > per_left) ? per_left : intr_req;
  // Bulk last, from whatever the FRAME has left. Note which number
  // this subtracts from: the frame budget, not the reservation.
  logic [15:0] used;
  assign used     = iso_g + intr_g;
  logic [15:0] frm_left;
  assign frm_left = (budget > used) ? (budget - used) : 16'd0;
  logic [15:0] bulk_g;
  assign bulk_g = (bulk_req > frm_left) ? frm_left : bulk_req;

  logic over;
  assign over = ((iso_req + intr_req) > per_limit);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      iso_r <= 16'd0; intr_r <= 16'd0; bulk_r <= 16'd0;
      over_r <= 1'b0;
      c_frames <= 16'd0; c_starved <= 16'd0;
      t_iso <= 32'd0; t_intr <= 32'd0; t_bulk <= 32'd0;
    end else if (frame_start) begin
      iso_r  <= iso_g;
      intr_r <= intr_g;
      bulk_r <= bulk_g;
      over_r <= over;
      c_frames <= c_frames + 16'd1;
      t_iso  <= t_iso  + {16'd0, iso_g};
      t_intr <= t_intr + {16'd0, intr_g};
      t_bulk <= t_bulk + {16'd0, bulk_g};
      if ((bulk_req != 16'd0) && (bulk_g == 16'd0))
        c_starved <= c_starved + 16'd1;
    end
  end

  assign iso_grant      = iso_r;
  assign intr_grant     = intr_r;
  assign bulk_grant     = bulk_r;
  assign periodic_over  = over_r;
  assign bulk_starved   = (bulk_r == 16'd0);
  assign n_frames       = c_frames;
  assign n_bulk_starved = c_starved;
  assign tot_iso        = t_iso;
  assign tot_intr       = t_intr;
  assign tot_bulk       = t_bulk;


`ifdef SVA_ON
  // The allocation rules as properties. Icarus rejects SVA; under
  // Icarus the intent checks in the testbench enforce each of these.

  // SAFETY. The frame is the hard limit. This is the property that the
  // over-grant defect described in 31.4 violated, and that the reference
  // model agreed with the design about.
  property p_within_budget;
    @(posedge clk) disable iff (!rst_n)
      (iso_grant + intr_grant + bulk_grant) <= $past(budget);
  endproperty
  a_within_budget: assert property (p_within_budget);

  // SAFETY. Periodic never exceeds its reservation.
  property p_within_reservation;
    @(posedge clk) disable iff (!rst_n)
      (iso_grant + intr_grant) <= $past(periodic_cap);
  endproperty
  a_within_reservation: assert property (p_within_reservation);

  // THE ONE THE CHAPTER IS ABOUT. Isochronous gets what it asked for
  // whenever the reservation allows it -- whatever bulk wants. Bulk
  // demand does not appear in the antecedent OR the consequent, which
  // is the point.
  property p_iso_is_independent_of_bulk;
    @(posedge clk) disable iff (!rst_n)
      frame_start && (iso_req <= periodic_cap) && (periodic_cap <= budget)
        |=> (iso_grant == $past(iso_req));
  endproperty
  a_iso_is_independent_of_bulk: assert property (p_iso_is_independent_of_bulk);

  // AND ITS MIRROR. On an empty frame bulk gets everything it asked
  // for, up to the budget -- so the misconception is right, there.
  property p_bulk_takes_an_empty_frame;
    @(posedge clk) disable iff (!rst_n)
      frame_start && (iso_req == '0) && (intr_req == '0) && (bulk_req >= budget)
        |=> (bulk_grant == $past(budget));
  endproperty
  a_bulk_takes_an_empty_frame: assert property (p_bulk_takes_an_empty_frame);

  c_empty:     cover property (@(posedge clk) frame_start && iso_req == '0 && intr_req == '0);
  c_starved:   cover property (@(posedge clk) frame_start && bulk_req != '0 &&
                                              (iso_req + intr_req) >= budget);
  c_over:      cover property (@(posedge clk) frame_start &&
                                              (iso_req + intr_req) > periodic_cap);
  c_exact:     cover property (@(posedge clk) frame_start &&
                                              (iso_req + intr_req) == periodic_cap);
`endif


endmodule

tb_usb_frame_budget_sv.sv — the testbench, SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  tb_usb_frame_budget -- Verilog-2005 testbench for usb_frame_budget.
//
//  PHASE 1 is the intent phase. It does not compare against a model; it
//  states the two architectural claims that together refute "bulk is
//  fastest" and requires them:
//
//      C1  on an EMPTY frame, bulk receives the entire budget
//      C2  on a LOADED frame, the isochronous grant does NOT depend on
//          how much bulk wants
//
//  C2 is the one that matters and it is a RELATIONAL property: it
//  compares two runs that differ only in bulk_req. No single-run check
//  can express it, and no reference model that computes the grants the
//  same way the design does can fail it independently.
//
//  PHASES
//    1 INTENT      C1 and C2, the second as a paired-run comparison
//    2 EXHAUSTIVE  the load grid: 5 iso x 4 bulk x 2 caps
//    3 SCENARIO    the named boundaries
//    4 RANDOM      supplementary, audited
// =====================================================================
`timescale 1ns/1ps

module tb_usb_frame_budget_sv;

  logic      clk = 1'b0;
  logic      rst_n;
  logic      frame_start;
  logic [15:0] iso_req, intr_req, bulk_req, budget, periodic_cap;

  wire [15:0] iso_grant, intr_grant, bulk_grant;
  wire        periodic_over, bulk_starved;
  wire [15:0] n_frames, n_bulk_starved;
  wire [31:0] tot_iso, tot_intr, tot_bulk;

  usb_frame_budget_sv dut (
    .clk(clk), .rst_n(rst_n), .frame_start(frame_start),
    .iso_req(iso_req), .intr_req(intr_req), .bulk_req(bulk_req),
    .budget(budget), .periodic_cap(periodic_cap),
    .iso_grant(iso_grant), .intr_grant(intr_grant), .bulk_grant(bulk_grant),
    .periodic_over(periodic_over), .bulk_starved(bulk_starved),
    .n_frames(n_frames), .n_bulk_starved(n_bulk_starved),
    .tot_iso(tot_iso), .tot_intr(tot_intr), .tot_bulk(tot_bulk)
  );

  always #5 clk = ~clk;

  // ---- the independent reference model ---------------------------
  // Written as the RULES, in words, then arithmetic:
  //   R1 periodic traffic is served first, within its reservation
  //   R2 isochronous takes precedence inside the reservation
  //   R3 bulk receives what the FRAME has left, not what the
  //      reservation has left
  logic [15:0] rm_iso, rm_intr, rm_bulk;
  logic [15:0] rm_frames, rm_starved;
  logic [31:0] rm_tiso, rm_tintr, rm_tbulk;

  int  chk_dir, chk_rnd, err;
  bit  in_random;
  int  m_frames, m_empty, m_loaded, m_starved, m_over, m_bulk_full,
       m_setupfail;
  int  i, j, a, b, c;

  task bump; begin
    if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
  end endtask

  task ck(string what, logic [31:0] got, logic [31:0] exp);
    begin
      bump;
      if (got !== exp) begin
        err = err + 1;
        if (!in_random && err <= 40)
          $display("  ** %s: got %0d expected %0d  (t=%0t)", what, got, exp, $time);
      end
    end
  endtask

  function logic [15:0] mn(logic [15:0] x, logic [15:0] y);
    return (x < y) ? x : y;
  endfunction

  task ref_step;
    logic [15:0] ig, il, tg, fl, bg, pl;
    begin
      if (!rst_n) begin
        rm_iso = 0; rm_intr = 0; rm_bulk = 0;
        rm_frames = 0; rm_starved = 0;
        rm_tiso = 0; rm_tintr = 0; rm_tbulk = 0;
      end else if (frame_start) begin
        // R0 the frame is the hard limit; the reservation lives inside
        //    it. This line was absent from both the design and this
        //    model, and they agreed with each other about the result.
        pl = mn(periodic_cap, budget);
        ig = mn(iso_req, pl);                           // R2
        il = pl - ig;
        tg = mn(intr_req, il);                          // R1
        fl = (budget > (ig + tg)) ? (budget - ig - tg) : 16'd0;
        bg = mn(bulk_req, fl);                          // R3
        rm_iso = ig; rm_intr = tg; rm_bulk = bg;
        rm_frames = rm_frames + 1;
        rm_tiso  = rm_tiso  + ig;
        rm_tintr = rm_tintr + tg;
        rm_tbulk = rm_tbulk + bg;
        if (bulk_req != 0 && bg == 0) rm_starved = rm_starved + 1;
        // tallies
        m_frames = m_frames + 1;
        if (iso_req == 0 && intr_req == 0) m_empty  = m_empty  + 1;
        else                               m_loaded = m_loaded + 1;
        if (bulk_req != 0 && bg == 0)      m_starved = m_starved + 1;
        if ((iso_req + intr_req) > pl) m_over = m_over + 1;
        if (bulk_req != 0 && bg == bulk_req) m_bulk_full = m_bulk_full + 1;
      end
    end
  endtask

  task cmp; begin
    ck("iso_grant",   {16'd0, iso_grant},   {16'd0, rm_iso});
    ck("intr_grant",  {16'd0, intr_grant},  {16'd0, rm_intr});
    ck("bulk_grant",  {16'd0, bulk_grant},  {16'd0, rm_bulk});
    ck("n_frames",    {16'd0, n_frames},    {16'd0, rm_frames});
    ck("n_starved",   {16'd0, n_bulk_starved}, {16'd0, rm_starved});
    ck("tot_iso",     tot_iso,  rm_tiso);
    ck("tot_bulk",    tot_bulk, rm_tbulk);
  end endtask

  // The claim, asserted directly: the three grants never exceed the
  // frame, and periodic never exceeds its reservation.
  task intent_check; begin
    bump;
    if ((iso_grant + intr_grant + bulk_grant) > budget) begin
      err = err + 1;
      $display("  ** INTENT VIOLATED: grants exceed the frame budget (t=%0t)", $time);
    end
    bump;
    if ((iso_grant + intr_grant) > periodic_cap) begin
      err = err + 1;
      $display("  ** INTENT VIOLATED: periodic exceeded its reservation (t=%0t)", $time);
    end
  end endtask

  task step; begin
    #1;
    @(posedge clk);
    ref_step;
    #1;
    cmp;
    intent_check;
    frame_start = 0;
  end endtask

  task hard_reset; begin
    rst_n = 0; frame_start = 0;
    iso_req = 0; intr_req = 0; bulk_req = 0;
    budget = 16'd1500; periodic_cap = 16'd1350;
    repeat (3) begin @(posedge clk); ref_step; end
    #1; rst_n = 1;
    @(posedge clk); ref_step; #1; cmp;
  end endtask

  task frame(logic [15:0] iso, logic [15:0] intr, logic [15:0] blk);
    begin
      frame_start = 1; iso_req = iso; intr_req = intr; bulk_req = blk;
      step;
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 1 -- THE TWO CLAIMS.
  // -----------------------------------------------------------------
  int  paired_runs;
  logic [15:0] iso_a, iso_b, bulk_a, bulk_b;

  task phase_intent;
    begin
      hard_reset;
      paired_runs = 0;

      // C1 an empty frame: bulk gets everything. The misconception is
      //    CORRECT here, and saying so is the point -- it is why the
      //    belief survives.
      frame(16'd0, 16'd0, 16'd2000);
      ck("C1 bulk got the whole frame", {16'd0, bulk_grant}, 32'd1500);
      ck("C1 iso got nothing",          {16'd0, iso_grant},  32'd0);

      // C2 the relational claim. Run the SAME periodic load twice,
      //    changing only what bulk asks for, and require the
      //    isochronous grant to be identical. If bulk could take
      //    priority by wanting more, this is where it would show.
      for (i = 0; i < 8; i = i + 1) begin
        hard_reset;
        frame(16'd100 + 16'(i) * 16'd100, 16'd50, 16'd0);
        iso_a = iso_grant; bulk_a = bulk_grant;
        hard_reset;
        frame(16'd100 + 16'(i) * 16'd100, 16'd50, 16'd9000);
        iso_b = iso_grant; bulk_b = bulk_grant;
        ck("C2 iso is unmoved by bulk demand", {16'd0, iso_a}, {16'd0, iso_b});
        bump;
        if (bulk_b <= bulk_a) begin
          err = err + 1;
          $display("  ** C2: bulk asking for more did not get more (%0d vs %0d)",
                   bulk_a, bulk_b);
        end
        paired_runs = paired_runs + 1;
      end
      bump;
      if (paired_runs != 8) begin
        err = err + 1;
        $display("  ** intent: only %0d paired runs", paired_runs);
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 2 -- the load grid.
  //
  //  AXES:
  //    isochronous demand   0, 300, 700, 1200, 1600 bytes         5
  //    bulk demand          0, 200, 1500, 9000 bytes              4
  //    reservation cap      1350 (full-speed 90%), 1200 (80%)     2
  //    ---------------------------------------------------------------
  //                                            5 x 4 x 2 =       40
  //
  //  Interrupt demand is held at 50 rather than swept: within the
  //  reservation it behaves exactly as isochronous does, and sweeping
  //  it would multiply the grid without adding a case. That is a claim
  //  a reviewer can disagree with, which is why it is written here --
  //  30.2 §6.
  //
  //  WHAT IS ABSENT: time. Every frame here is independent. A backlog
  //  that persists across frames is phase 3.
  // -----------------------------------------------------------------
  logic [15:0] isos [5];
  logic [15:0] blks [4];
  logic [15:0] caps [2];

  task phase_sweep;
    begin
      isos[0]=16'd0; isos[1]=16'd300; isos[2]=16'd700;
      isos[3]=16'd1200; isos[4]=16'd1600;
      blks[0]=16'd0; blks[1]=16'd200; blks[2]=16'd1500; blks[3]=16'd9000;
      caps[0]=16'd1350; caps[1]=16'd1200;
      for (a = 0; a < 5; a = a + 1)
      for (b = 0; b < 4; b = b + 1)
      for (c = 0; c < 2; c = c + 1) begin
        hard_reset;
        periodic_cap = caps[c];
        bump;
        if (periodic_cap !== caps[c]) begin
          err = err + 1; m_setupfail = m_setupfail + 1;
          $display("  ** setup: cap not applied");
        end
        frame(isos[a], 16'd50, blks[b]);
        frame(16'd0, 16'd0, 16'd0);
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 3 -- the named boundaries.
  // -----------------------------------------------------------------
  task phase_scenarios;
    begin
      // S1 periodic exactly at the reservation: bulk gets the rest.
      hard_reset;
      frame(16'd1300, 16'd50, 16'd9000);
      ck("S1 iso got all it asked",  {16'd0, iso_grant},  32'd1300);
      ck("S1 interrupt too",         {16'd0, intr_grant}, 32'd50);
      ck("S1 bulk got the remainder",{16'd0, bulk_grant}, 32'd150);
      ck("S1 not flagged over",      {31'd0, periodic_over}, 32'd0);

      // S2 periodic one byte over: the reservation is a CAP, not a
      //    suggestion, and the excess is refused rather than taken
      //    from bulk.
      hard_reset;
      frame(16'd1301, 16'd50, 16'd9000);
      ck("S2 flagged over",          {31'd0, periodic_over}, 32'd1);
      ck("S2 periodic capped",       {16'd0, iso_grant + intr_grant}, 32'd1350);
      ck("S2 bulk still gets 150",   {16'd0, bulk_grant}, 32'd150);

      // S3 THE COUNTEREXAMPLE. Periodic fills the reservation
      //    completely and the frame has nothing left. Bulk asks for
      //    nine thousand bytes and receives zero -- for as many frames
      //    as this lasts.
      hard_reset;
      periodic_cap = 16'd1500;
      for (i = 0; i < 10; i = i + 1) frame(16'd1500, 16'd0, 16'd9000);
      ck("S3 bulk got nothing, ten frames running", {16'd0, n_bulk_starved}, 32'd10);
      ck("S3 while iso got everything", tot_iso, 32'd15000);
      ck("S3 and bulk got zero bytes",  tot_bulk, 32'd0);

      // S4 the mirror: an idle periodic load, ten frames, bulk takes
      //    the lot. Same design, same parameters, opposite verdict.
      hard_reset;
      for (i = 0; i < 10; i = i + 1) frame(16'd0, 16'd0, 16'd9000);
      ck("S4 bulk never starved",   {16'd0, n_bulk_starved}, 32'd0);
      ck("S4 and took every byte",  tot_bulk, 32'd15000);

      // S5 a tiny bulk request always fits when there is room, and
      //    never fits when there is not. "Fastest" is not a property
      //    of the request.
      hard_reset;
      frame(16'd0,    16'd0, 16'd1);
      ck("S5 one byte, empty frame",  {16'd0, bulk_grant}, 32'd1);
      hard_reset;
      periodic_cap = 16'd1500;
      frame(16'd1500, 16'd0, 16'd1);
      ck("S5 one byte, full frame",   {16'd0, bulk_grant}, 32'd0);
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 4 -- random, audited.
  // -----------------------------------------------------------------
  task phase_random;
    int r;
    begin
      in_random = 1;
      hard_reset;
      for (j = 0; j < 3000; j = j + 1) begin
        r = $urandom_range(99);
        if (r < 20) periodic_cap = 16'($urandom_range(1599));
        if (r < 30) budget       = 16'd1000 + 16'($urandom_range(799));
        frame(16'($urandom_range(1699)), 16'($urandom_range(299)), 16'($urandom_range(3999)));
      end
      in_random = 0;
    end
  endtask

  initial begin
    chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
    m_frames=0; m_empty=0; m_loaded=0; m_starved=0; m_over=0;
    m_bulk_full=0; m_setupfail=0;

    phase_intent;
    $display("  phase 1 intent      : %0d checks, %0d errors  (%0d paired runs)",
             chk_dir, err, paired_runs);
    phase_sweep;
    $display("  phase 2 exhaustive  : %0d checks, %0d errors  (40 combinations)", chk_dir, err);
    phase_scenarios;
    $display("  phase 3 scenarios   : %0d checks, %0d errors", chk_dir, err);
    $display("  ---- DIRECTED-ONLY  : %0d checks, %0d errors ----", chk_dir, err);
    phase_random;

    $display("");
    $display("  measured reachability (all phases)");
    $display("    frames scheduled ....... %0d", m_frames);
    $display("      with no periodic ..... %0d", m_empty);
    $display("      with periodic load ... %0d", m_loaded);
    $display("    frames starving bulk ... %0d", m_starved);
    $display("    bulk fully satisfied ... %0d", m_bulk_full);
    $display("    periodic over its cap .. %0d", m_over);
    $display("    setup failures ......... %0d", m_setupfail);
    $display("");
    $display("  directed checks ........ %0d", chk_dir);
    $display("  random checks .......... %0d", chk_rnd);
    $display("  TOTAL checks ........... %0d", chk_dir + chk_rnd);
    $display("  ERRORS ................. %0d", err);
    if (err == 0) $display("  PASS"); else $display("  FAIL");
    $finish;
  end

endmodule

The reservation guarantee becomes one property, and it is the most quotable line in the module:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  property p_iso_is_independent_of_bulk;
    @(posedge clk) disable iff (!rst_n)
      frame_start && (iso_req <= periodic_cap) && (periodic_cap <= budget)
        |=> (iso_grant == $past(iso_req));
  endproperty

Bulk demand appears in neither the antecedent nor the consequent, and that absence is the property. Beside it, the safety pair and the mirror:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  property p_within_budget;        // the frame is the hard limit
  property p_within_reservation;   // periodic never exceeds its cap
  property p_bulk_takes_an_empty_frame;   // and here the belief is RIGHT

p_within_budget is the one the missing clamp violated while the design and its reference model agreed with each other. p_bulk_takes_an_empty_frame is the belief's true half, written down as an obligation so that a "fix" which starved bulk on an idle frame would fail too.

And the belief becomes a property that a correct design fails, which is worth writing down in a comment and never enabling:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // p_bulk_always_gets_what_it_asks_for  --  bulk_req |=> bulk_grant == bulk_req
  // This is the misconception as an assertion. A correct allocator
  // violates it in 191 frames of this bench, and is right to.

A property that a correct design violates is the most precise possible statement of a misconception. It is also the reason 30.2 §9 insists a checker be tested for false positives: somebody will write that assertion in good faith, watch it fail, and "fix" the allocator.

9. VHDL-2008

usb_frame_budget.vhd — the design, VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  usb_frame_budget (VHDL-2008) -- the same contract. Same ports, same
--  order of allocation, same reset, same latency.
--
--  The clamp that makes the frame the outer bound is written with the
--  same explicitness here as in the other two: a reservation larger
--  than the frame is a configuration error, and the design refuses it
--  rather than granting bytes the frame does not contain. See 31.4
--  31.4 for how that line came to be written.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity usb_frame_budget is
  port (
    clk            : in  std_logic;
    rst_n          : in  std_logic;
    frame_start    : in  std_logic;
    iso_req        : in  unsigned(15 downto 0);
    intr_req       : in  unsigned(15 downto 0);
    bulk_req       : in  unsigned(15 downto 0);
    budget         : in  unsigned(15 downto 0);
    periodic_cap   : in  unsigned(15 downto 0);
    iso_grant      : out unsigned(15 downto 0);
    intr_grant     : out unsigned(15 downto 0);
    bulk_grant     : out unsigned(15 downto 0);
    periodic_over  : out std_logic;
    bulk_starved   : out std_logic;
    n_frames       : out unsigned(15 downto 0);
    n_bulk_starved : out unsigned(15 downto 0);
    tot_iso        : out unsigned(31 downto 0);
    tot_intr       : out unsigned(31 downto 0);
    tot_bulk       : out unsigned(31 downto 0)
  );
end entity usb_frame_budget;

architecture rtl of usb_frame_budget is
  signal iso_r, intr_r, bulk_r : unsigned(15 downto 0) := (others => '0');
  signal over_r                : std_logic := '0';
  signal c_frames, c_starved   : unsigned(15 downto 0) := (others => '0');
  signal t_iso, t_intr, t_bulk : unsigned(31 downto 0) := (others => '0');

  signal per_limit, iso_g, per_left, intr_g : unsigned(15 downto 0);
  signal used, frm_left, bulk_g             : unsigned(15 downto 0);
  signal over                               : std_logic;

  function mn (x, y : unsigned(15 downto 0)) return unsigned is
  begin
    if x < y then return x; else return y; end if;
  end function mn;
begin

  -- The frame is the hard limit; the reservation lives inside it.
  per_limit <= mn(periodic_cap, budget);

  iso_g    <= mn(iso_req, per_limit);
  per_left <= per_limit - iso_g;
  intr_g   <= mn(intr_req, per_left);
  used     <= iso_g + intr_g;
  frm_left <= budget - used when budget > used else (others => '0');
  bulk_g   <= mn(bulk_req, frm_left);

  over <= '1' when (iso_req + intr_req) > per_limit else '0';

  seq : process (clk, rst_n)
  begin
    if rst_n = '0' then
      iso_r <= (others => '0'); intr_r <= (others => '0');
      bulk_r <= (others => '0'); over_r <= '0';
      c_frames <= (others => '0'); c_starved <= (others => '0');
      t_iso <= (others => '0'); t_intr <= (others => '0');
      t_bulk <= (others => '0');
    elsif rising_edge(clk) then
      if frame_start = '1' then
        iso_r  <= iso_g;
        intr_r <= intr_g;
        bulk_r <= bulk_g;
        over_r <= over;
        c_frames <= c_frames + 1;
        t_iso  <= t_iso  + resize(iso_g, 32);
        t_intr <= t_intr + resize(intr_g, 32);
        t_bulk <= t_bulk + resize(bulk_g, 32);
        if bulk_req /= 0 and bulk_g = 0 then
          c_starved <= c_starved + 1;
        end if;
      end if;
    end if;
  end process seq;

  iso_grant      <= iso_r;
  intr_grant     <= intr_r;
  bulk_grant     <= bulk_r;
  periodic_over  <= over_r;
  bulk_starved   <= '1' when bulk_r = 0 else '0';
  n_frames       <= c_frames;
  n_bulk_starved <= c_starved;
  tot_iso        <= t_iso;
  tot_intr       <= t_intr;
  tot_bulk       <= t_bulk;

end architecture rtl;

tb_usb_frame_budget.vhd — the testbench, VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  tb_usb_frame_budget -- VHDL-2008 testbench for usb_frame_budget.
--  Phases 1-3 present the SAME directed stimulus as the other two
--  benches, so their directed counts must agree.
--
--  Phase 1 carries the RELATIONAL claim: two runs differing only in
--  bulk demand must produce the same isochronous grant. No single-run
--  check can express it.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;

entity tb_usb_frame_budget is
end entity tb_usb_frame_budget;

architecture sim of tb_usb_frame_budget is
  constant HALF : time := 10 ns;

  function b2i_over (s : std_logic) return integer is
  begin
    if s = '1' then return 1; else return 0; end if;
  end function b2i_over;

  signal clk          : std_logic := '0';
  signal rst_n        : std_logic := '0';
  signal frame_start  : std_logic := '0';
  signal iso_req, intr_req, bulk_req : unsigned(15 downto 0) := (others => '0');
  signal budget       : unsigned(15 downto 0) := to_unsigned(1500, 16);
  signal periodic_cap : unsigned(15 downto 0) := to_unsigned(1350, 16);
  signal iso_grant, intr_grant, bulk_grant : unsigned(15 downto 0);
  signal periodic_over, bulk_starved : std_logic;
  signal n_frames, n_bulk_starved : unsigned(15 downto 0);
  signal tot_iso, tot_intr, tot_bulk : unsigned(31 downto 0);
  signal done_flag : boolean := false;
begin

  dut : entity work.usb_frame_budget
    port map (clk => clk, rst_n => rst_n, frame_start => frame_start,
              iso_req => iso_req, intr_req => intr_req, bulk_req => bulk_req,
              budget => budget, periodic_cap => periodic_cap,
              iso_grant => iso_grant, intr_grant => intr_grant,
              bulk_grant => bulk_grant, periodic_over => periodic_over,
              bulk_starved => bulk_starved, n_frames => n_frames,
              n_bulk_starved => n_bulk_starved, tot_iso => tot_iso,
              tot_intr => tot_intr, tot_bulk => tot_bulk);

  clkgen : process
  begin
    while not done_flag loop
      clk <= '0'; wait for HALF;
      clk <= '1'; wait for HALF;
    end loop;
    wait;
  end process clkgen;

  stim : process
    variable rm_iso, rm_intr, rm_bulk : natural := 0;
    variable rm_frames, rm_starved    : natural := 0;
    variable rm_tiso, rm_tintr, rm_tbulk : natural := 0;
    variable chk_dir, chk_rnd, errs, shown : natural := 0;
    variable in_random : boolean := false;
    variable m_frames, m_empty, m_loaded, m_starved : natural := 0;
    variable m_over, m_bulk_full, m_setupfail, paired_runs : natural := 0;
    variable iso_a, iso_b, bulk_a, bulk_b : natural := 0;
    variable seed1 : positive := 901_337; variable seed2 : positive := 47_111;

    type nat5 is array (0 to 4) of natural;
    type nat4 is array (0 to 3) of natural;
    type nat2 is array (0 to 1) of natural;
    constant isos : nat5 := (0, 300, 700, 1200, 1600);
    constant blks : nat4 := (0, 200, 1500, 9000);
    constant caps : nat2 := (1350, 1200);

    procedure bump is
    begin
      if in_random then chk_rnd := chk_rnd + 1; else chk_dir := chk_dir + 1; end if;
    end procedure bump;

    procedure ck (what : string; got : integer; exp : integer) is
    begin
      bump;
      if got /= exp then
        errs := errs + 1;
        if (not in_random) and shown < 40 then
          shown := shown + 1;
          report "  ** " & what & ": got " & integer'image(got) &
                 " expected " & integer'image(exp) severity warning;
        end if;
      end if;
    end procedure ck;

    function mn (x, y : natural) return natural is
    begin
      if x < y then return x; else return y; end if;
    end function mn;

    procedure ref_step is
      variable ig, il, tg, fl, bg, pl : natural;
    begin
      if rst_n = '0' then
        rm_iso := 0; rm_intr := 0; rm_bulk := 0;
        rm_frames := 0; rm_starved := 0;
        rm_tiso := 0; rm_tintr := 0; rm_tbulk := 0;
      elsif frame_start = '1' then
        pl := mn(to_integer(periodic_cap), to_integer(budget));
        ig := mn(to_integer(iso_req), pl);
        il := pl - ig;
        tg := mn(to_integer(intr_req), il);
        if to_integer(budget) > (ig + tg) then fl := to_integer(budget) - ig - tg;
        else                                   fl := 0; end if;
        bg := mn(to_integer(bulk_req), fl);
        rm_iso := ig; rm_intr := tg; rm_bulk := bg;
        rm_frames := rm_frames + 1;
        rm_tiso := rm_tiso + ig; rm_tintr := rm_tintr + tg;
        rm_tbulk := rm_tbulk + bg;
        if to_integer(bulk_req) /= 0 and bg = 0 then
          rm_starved := rm_starved + 1;
          m_starved  := m_starved + 1;
        end if;
        m_frames := m_frames + 1;
        if iso_req = 0 and intr_req = 0 then m_empty := m_empty + 1;
        else                                 m_loaded := m_loaded + 1; end if;
        if (to_integer(iso_req) + to_integer(intr_req)) > pl then
          m_over := m_over + 1;
        end if;
        if to_integer(bulk_req) /= 0 and bg = to_integer(bulk_req) then
          m_bulk_full := m_bulk_full + 1;
        end if;
      end if;
    end procedure ref_step;

    procedure cmp is
    begin
      ck("iso_grant",  to_integer(iso_grant),  rm_iso);
      ck("intr_grant", to_integer(intr_grant), rm_intr);
      ck("bulk_grant", to_integer(bulk_grant), rm_bulk);
      ck("n_frames",   to_integer(n_frames),   rm_frames);
      ck("n_starved",  to_integer(n_bulk_starved), rm_starved);
      ck("tot_iso",    to_integer(tot_iso),    rm_tiso);
      ck("tot_bulk",   to_integer(tot_bulk),   rm_tbulk);
    end procedure cmp;

    procedure intent_check is
    begin
      bump;
      if (to_integer(iso_grant) + to_integer(intr_grant) +
          to_integer(bulk_grant)) > to_integer(budget) then
        errs := errs + 1;
        report "  ** INTENT VIOLATED: grants exceed the frame budget" severity warning;
      end if;
      bump;
      if (to_integer(iso_grant) + to_integer(intr_grant)) >
         to_integer(periodic_cap) then
        errs := errs + 1;
        report "  ** INTENT VIOLATED: periodic exceeded its reservation" severity warning;
      end if;
    end procedure intent_check;

    procedure step is
    begin
      wait for 1 ns;
      wait until rising_edge(clk);
      ref_step;
      wait for 1 ns;
      cmp;
      intent_check;
      frame_start <= '0';
    end procedure step;

    procedure hard_reset is
    begin
      rst_n <= '0'; frame_start <= '0';
      iso_req <= (others => '0'); intr_req <= (others => '0');
      bulk_req <= (others => '0');
      budget <= to_unsigned(1500, 16); periodic_cap <= to_unsigned(1350, 16);
      for i in 0 to 2 loop wait until rising_edge(clk); ref_step; end loop;
      wait for 1 ns; rst_n <= '1';
      wait until rising_edge(clk); ref_step; wait for 1 ns; cmp;
    end procedure hard_reset;

    procedure frame (iso, intr, blk : natural) is
    begin
      frame_start <= '1';
      iso_req  <= to_unsigned(iso, 16);
      intr_req <= to_unsigned(intr, 16);
      bulk_req <= to_unsigned(blk, 16);
      step;
    end procedure frame;

    impure function rnd (n : positive) return natural is
      variable x : real;
    begin
      uniform(seed1, seed2, x);
      return natural(real(n - 1) * x);
    end function rnd;

    variable r : natural;
  begin
    -- ---- PHASE 1 : the two claims ----
    hard_reset;
    paired_runs := 0;
    frame(0, 0, 2000);
    ck("C1 bulk got the whole frame", to_integer(bulk_grant), 1500);
    ck("C1 iso got nothing",          to_integer(iso_grant),  0);
    for i in 0 to 7 loop
      hard_reset;
      frame(100 + i * 100, 50, 0);
      iso_a := to_integer(iso_grant); bulk_a := to_integer(bulk_grant);
      hard_reset;
      frame(100 + i * 100, 50, 9000);
      iso_b := to_integer(iso_grant); bulk_b := to_integer(bulk_grant);
      ck("C2 iso is unmoved by bulk demand", iso_a, iso_b);
      bump;
      if bulk_b <= bulk_a then
        errs := errs + 1;
        report "  ** C2: bulk asking for more did not get more" severity warning;
      end if;
      paired_runs := paired_runs + 1;
    end loop;
    bump;
    if paired_runs /= 8 then
      errs := errs + 1;
      report "  ** intent: too few paired runs" severity warning;
    end if;
    report "  phase 1 intent      : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors  (" &
           integer'image(paired_runs) & " paired runs)";

    -- ---- PHASE 2 : 5 x 4 x 2 = 40 ----
    for a in 0 to 4 loop
      for b in 0 to 3 loop
        for c in 0 to 1 loop
          hard_reset;
          periodic_cap <= to_unsigned(caps(c), 16);
          -- A VHDL signal assignment is not visible on the next line.
          -- Without this wait the check below reads the PREVIOUS cap and
          -- reports a setup failure that did not happen. The same hazard
          -- cost Module 30 a bench defect; it is the commonest way a
          -- Verilog engineer's first VHDL testbench is wrong.
          wait for 1 ns;
          bump;
          if to_integer(periodic_cap) /= caps(c) then
            errs := errs + 1; m_setupfail := m_setupfail + 1;
            report "  ** setup: cap not applied" severity warning;
          end if;
          frame(isos(a), 50, blks(b));
          frame(0, 0, 0);
        end loop;
      end loop;
    end loop;
    report "  phase 2 exhaustive  : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors  (40 combinations)";

    -- ---- PHASE 3 : the named boundaries ----
    hard_reset;
    frame(1300, 50, 9000);
    ck("S1 iso got all it asked",   to_integer(iso_grant),  1300);
    ck("S1 interrupt too",          to_integer(intr_grant), 50);
    ck("S1 bulk got the remainder", to_integer(bulk_grant), 150);
    ck("S1 not flagged over",       b2i_over(periodic_over), 0);

    hard_reset;
    frame(1301, 50, 9000);
    ck("S2 flagged over",        b2i_over(periodic_over), 1);
    ck("S2 periodic capped",     to_integer(iso_grant) + to_integer(intr_grant), 1350);
    ck("S2 bulk still gets 150", to_integer(bulk_grant), 150);

    hard_reset;
    periodic_cap <= to_unsigned(1500, 16);
    for i in 0 to 9 loop frame(1500, 0, 9000); end loop;
    ck("S3 bulk got nothing, ten frames running", to_integer(n_bulk_starved), 10);
    ck("S3 while iso got everything", to_integer(tot_iso), 15000);
    ck("S3 and bulk got zero bytes",  to_integer(tot_bulk), 0);

    hard_reset;
    for i in 0 to 9 loop frame(0, 0, 9000); end loop;
    ck("S4 bulk never starved",  to_integer(n_bulk_starved), 0);
    ck("S4 and took every byte", to_integer(tot_bulk), 15000);

    hard_reset;
    frame(0, 0, 1);
    ck("S5 one byte, empty frame", to_integer(bulk_grant), 1);
    hard_reset;
    periodic_cap <= to_unsigned(1500, 16);
    frame(1500, 0, 1);
    ck("S5 one byte, full frame", to_integer(bulk_grant), 0);

    report "  phase 3 scenarios   : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors";
    report "  ---- DIRECTED-ONLY  : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors ----";

    -- ---- PHASE 4 : random ----
    in_random := true;
    hard_reset;
    for j in 0 to 2999 loop
      r := rnd(100);
      if r < 20 then periodic_cap <= to_unsigned(rnd(1600), 16); end if;
      if r < 30 then budget       <= to_unsigned(1000 + rnd(800), 16); end if;
      frame(rnd(1700), rnd(300), rnd(4000));
    end loop;
    in_random := false;

    report "  measured reachability (all phases)";
    report "    frames scheduled ....... " & integer'image(m_frames);
    report "      with no periodic ..... " & integer'image(m_empty);
    report "      with periodic load ... " & integer'image(m_loaded);
    report "    frames starving bulk ... " & integer'image(m_starved);
    report "    bulk fully satisfied ... " & integer'image(m_bulk_full);
    report "    periodic over its cap .. " & integer'image(m_over);
    report "    setup failures ......... " & integer'image(m_setupfail);
    report "  directed checks ........ " & integer'image(chk_dir);
    report "  random checks .......... " & integer'image(chk_rnd);
    report "  TOTAL checks ........... " & integer'image(chk_dir + chk_rnd);
    report "  ERRORS ................. " & integer'image(errs);
    if errs = 0 then report "  PASS"; else report "  FAIL" severity failure; end if;
    done_flag <= true;
    wait;
  end process stim;

end architecture sim;

The VHDL version earns its place here because the allocation is a sequence of signal assignments whose order of dependency is visible in the code, and because integer subtraction with a range constraint would have crashed rather than wrapped had the clamp been missing — a different way to find the same bug.

10. The Misconception As Hardware

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    MUT    THE BELIEF ENCODED                      V-DIR  SV-DIR  VH-DIR
    S-M1   bulk is fastest, so serve it first
           (bulk allocated before the
            reservations)                            218     218     218
    S-M2   a reservation is a suggestion
           (periodic grants clamped by what
            remains after bulk)                       78      78      78

BASE zero in all six columns; directed columns identical across the languages.

S-M1 is the belief promoted to policy. It allocates bulk from the full budget first, then gives the reservations whatever is left. Every number it produces is a legal-looking byte count and the total never exceeds the frame — so it is not caught by the total-grant check. It is caught 218 times by the checks that say isochronous gets what it reserved regardless of bulk — C2 and p_iso_is_independent_of_bulk — which are the only ones that encode the priority rather than the arithmetic.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    WHAT S-M1 WOULD LOOK LIKE IN A REAL SYSTEM

      bulk benchmarks beautifully -- better than the correct design
      audio clicks and video drops frames, intermittently, under load
      no error counter anywhere moves
      the bug is reported as an AUDIO problem

S-M2 scores 78 and is the subtler inversion: it keeps the order but makes the reservations yield. A device that reserved bandwidth and did not receive it, with no error raised, is the hardest class of USB bug to attribute, because the symptom appears on a different device from the defect.

11. What The Wrong Model Does To Debugging

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    SYMPTOM      "our bulk throughput halved in the field and we cannot
                  reproduce it"

    WRONG MODEL  bulk throughput is a property of our device
    WRONG QUESTION   what changed in our device / driver / firmware?
    WASTED ON        firmware versions, buffer sizes, packet alignment,
                     the host driver, a suspected silicon revision

    CORRECT MODEL  bulk receives what the frame has left
    THE FIRST QUESTION   what ELSE is on that bus, and is any of it
                         periodic?
    AND THE SECOND       what does the host's bandwidth allocation look
                         like -- how much periodic bandwidth is
                         committed on this controller?

    Typical resolution: a webcam, a headset, or a hub sharing the
    controller. Nothing in the device changed. Nothing was broken.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    SYMPTOM      "our isochronous stream glitches when the disk is busy"

    WRONG MODEL  bulk and isochronous compete on equal terms
    WRONG QUESTION   how do we give isochronous more priority?
    THE REAL QUESTION  is the reservation actually being honoured --
                       i.e. does the system behave like the design or
                       like S-M1?

    Because if reservations are honoured, bulk activity CANNOT affect
    an isochronous stream. If it does, something in the path is
    allocating in the wrong order, and that is a scheduling defect with
    a specific location -- not a priority to be tuned.

That second one is the payoff of having built S-M1. An engineer who has seen the mutation recognises its signature in the field: bulk unusually good, periodic intermittently bad, no errors anywhere.

12. Interview Reasoning

"Which USB transfer type gives you the best performance?"

I would want to split the question first, because "performance" is two different requirements and no transfer type wins both.

Bulk has the highest throughput ceiling: the largest packets, no rate limit, and the host will issue bulk transactions back to back with whatever frame time is available. That is why mass storage and network adaptors use it. But that last clause is the whole answer — bulk gets the bandwidth that nothing else reserved. It has no reservation and no bounded latency, so on a bus with a committed isochronous stream its throughput drops by roughly the reserved amount, and if periodic traffic fills the frame, bulk can legitimately go to zero with no error raised anywhere.

Isochronous is the opposite trade: a fixed reservation agreed at configuration time, bounded latency, and no retries — a corrupted frame is simply lost. So for worst-case guaranteed rate, isochronous wins and bulk does not compete, because bulk offers no guarantee at all. Interrupt buys a bounded service interval with small payloads.

So the answer I would give is: bulk for the highest average rate when you can tolerate an unbounded worst case; isochronous when you need a floor and can tolerate loss; interrupt when you need bounded latency on small data. And the practical warning — a bulk throughput number measured on an otherwise idle bus is not a specification, it is a best case, and it will be quoted back at you as a specification if you put it in a datasheet.

13. Exercises

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    1  THE ORDER
       Write the allocation order from memory and say which type's
       grant depends on which other grants.

    2  MEASUREMENT
       Design the experiment from section 4 as a repeatable test. What
       do you record, and what makes the result a RELATION rather than
       a number?

    3  LATENCY
       Bulk has no bounded latency. Construct a legal traffic pattern
       under which a single bulk transfer never completes, and say what
       (if anything) in the specification forbids it.

    4  VERILOG
       Add a fourth request type, control, with a reserved MINIMUM
       share. Where does it go in the order, and why must it be
       reserved at all?

    5  SYSTEMVERILOG
       Write the property that the control reservation in exercise 4
       is always honoured, and say what would break if it were not.

    6  VHDL
       Implement exercise 4. Say what a range-constrained integer would
       have done to the missing-clamp bug in section 6.

    7  TESTBENCH
       The second axis has values "below / at / above". Add a fourth
       value that is worth having, and justify it.

    8  MUTATION
       S-M1's signature is "bulk unusually good, periodic
       intermittently bad, no errors". Write a THIRD mutation whose
       signature is "everything slightly slow, nothing ever wrong", and
       predict its score.

    9  DEBUG
       A customer reports that your bulk device is slow only on one
       laptop. List the observations in order, and say which of them
       are about your device.

   10  REVIEW
       Write the design-review question from 30.4 that would have
       caught a datasheet claiming an idle-bus bulk number as a
       specification.

14. What Carries Forward

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    THE CORRECTION
    o  "always" is the wrong word. Bulk is the fastest type for the
       bandwidth NOBODY ELSE RESERVED
    o  throughput is not a property of a transfer type or a device; it
       is the outcome of an ALLOCATION the host performs each frame
    o  the allocation order is isochronous, interrupt, then bulk -- and
       bulk's grant is a function of the OTHER traffic
    o  bulk starved to zero, with no error anywhere, is correct
       operation
    o  "guaranteed bandwidth" means claimed FIRST, not limited
    o  FAST is two requirements: throughput and worst-case latency.
       Bulk maximises one and abandons the other; isochronous fixes
       both and abandons reliability

    THE HARDWARE
    o  bulk_req is compared against what REMAINS, and the remainder is
       computed without reference to bulk at all
    o  a starvation OUTPUT, because the state is legal and needs to be
       observable rather than prevented
    o  min(periodic_cap, budget): a clamp added because a check
       disagreed with both the design and its model

    THE METHOD
    o  a design and a reference model written from the same sentence
       are ONE artefact. Only an independently stated intent found the
       missing clamp
    o  an axis of RELATIONSHIPS (below / at / above) is worth more than
       an axis of numbers, and "at the bound" is not "past the bound"
    o  arithmetic properties are blind to priority inversion: three of
       the four obvious checks pass on both mutants
    o  the misconception written as an SVA property is a property a
       CORRECT design violates -- in 191 frames here

    THE DEBUG CONSEQUENCE
    o  "why did our throughput drop" is answered by looking at the rest
       of the bus, not at the device
    o  if bulk traffic affects an isochronous stream at all, the
       reservation is not being honoured, and that is a located defect
       rather than a priority to tune

The next belief is the one that wastes the most engineering days per year, and it is held mostly by people who have never watched a device fail to be recognised.

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.