Skip to content
VLSI Mentor

USB · Module 25

Transfer Errors

Retries mask the error rate — a link losing a third of its traffic reports a perfect transfer success rate, because the denominator everybody uses is the wrong one.

Chapter 25.3 looked at one endpoint answering one token. This chapter moves up one level, to the transfer built out of those transactions — and to the reason a transfer-level error count is close to useless.

1. A Transfer Is Not One Event on the Wire

A USB transfer is a sequence of attempts. The host issues a transaction; if it fails — CRC, timeout, bad PID — the host simply issues it again, up to a fixed budget. If any attempt succeeds, the transfer succeeded.

Three attempts, one transfer, one success

A sequence diagram between an application, a host controller and a device. The application requests a transfer. The host controller issues a transaction to the device; the response is corrupted and fails a CRC check. The host retries and the retry also fails. The host retries a third time and the device answers successfully. The host controller then returns success to the application, which never learns that two attempts were destroyed.A transfer that survived on its last attemptApplicationHost controllerDeviceread 512 bytesattempt 1corrupted — CRC16failsattempt 2 (retry)corrupted — CRC16failsattempt 3 (last inthe budget)512 bytes, intactsuccess — 512 bytes
Two transactions were destroyed on the wire and re-sent. The transfer completed, the driver returned success, and the application saw a normal read. Nothing above the host controller heard that anything happened at all.

2. Retries Mask the Error Rate

Count that transfer the way every tool counts it and you get:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   transfers attempted   1
   transfers failed      0
   transfer error rate   0.0%

   ...on a link that destroyed two out of three transactions.

The testbench in section 13 measures this rather than asserting it. Phase 2 runs 200 transfers, each of which fails once and then succeeds:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   PHASE2 transfer-level failures=0  attempt-level failures=200/400

Zero, against fifty percent.

3. The Denominator Is Attempts, Not Transfers

So the fix is a different denominator, and it is the whole of the chapter in one line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   WRONG    failed transfers
            ------------------
            total transfers

   RIGHT    failed attempts
            ------------------
            total attempts

The two differ by exactly the retry factor, and the retry factor is the thing you were trying to measure in the first place. The design exposes both counts — n_xfer as well as n_attempt — deliberately, because the useful diagnostic is not either number but their ratio.

In the Verilog run's random phase:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   attempts           39010
   transfers          27774        ratio 1.40

   failed attempts    12012
   failed transfers     776        ratio 15.5

   15 out of every 16 failures never reached the transfer layer.

4. A Retry That Succeeded Is Still Evidence

"It worked" is not "it was fine". A transfer that needed two attempts consumed three times the bus bandwidth, three times the power, and had one attempt left before it became a visible failure.

5. The Class Names the Layer

The second question a triage block has to answer is whose bug is it, and the error class answers it:

ClassLayerWhat it means
CRC16, bit-stuffPHYthe wire said something other than what was sent
timeout, bad PIDPROTOCOLnobody answered, or answered wrongly
STALL, babbleFIRMWAREthe device's software did this on purpose, or lost track

The useful output is not the total; it is the distribution. A hundred errors that are all CRC is a cable. A hundred split evenly across all six is a power-supply problem that looks like everything at once.

6. And the Layer Total Is Not the Largest Class

This one costs real time, and the arithmetic is the entire argument:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CRC       40     PHY
   timeout   30     PROTOCOL
   bad PID   30     PROTOCOL

   largest CLASS:   CRC, 40      ->  "it's the cable"
   largest LAYER:   PROTOCOL, 60 ->  it's the controller

   Same data. Different team. Different week.

Ranking classes instead of summing layers picks the wrong team about as often as not, and it does it in exactly the case where the evidence is ambiguous enough that nobody notices the method was wrong. The testbench drives this case directly in phase 5:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   PHASE5 largest class = CRC (40), dominant layer = 1 (phy=40 proto=60 fw=0)

7. Two Events in One Cycle Need Two Pulses

The block reports two independent things, and there is a cycle in which both happen:

  • the attempt that exhausts the retry budget is also
  • the attempt that takes a layer over its alarm threshold

Fold both into one err_code field and the last assignment wins. One of the two events is silently deleted, and which one depends on the order the branches happen to be written in.

8. What We Are Building

usb_error_triage — two denominators, seven classes, three layers

A block diagram of the error triage block. An attempt stream carrying an error class feeds two things: a retry budget tracker and a set of seven per-class counters. The retry budget produces the masked and transfer-failed reports on the shared error pulse. The class counters sum into three layer totals for PHY, protocol and firmware, and those totals drive both a dominant-layer output and a per-layer alarm that has its own separate pulse.Attemptone per try · err_classRetry budgetMAX_RETRY, then give upClass countersseven, one per classMASKED / XFER_FAILerr_pulse · err_codeLayer sumsPHY · PROTOCOL · FIRMWAREdominant_layerthe largest layer TOTALLayer alarmits own pulse, on thecrossing12
The attempt stream feeds the retry budget and the class counters. The class counters sum into three layer totals, and the layer totals drive both the dominant-layer ranking and the per-layer alarm. The alarm has its own pulse so that it can coincide with a retry-budget exhaustion without either report overwriting the other.

9. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_error_triage -- the two questions a transfer error log has to answer,
// and the reason the obvious way of asking both gives the wrong answer.
//
// QUESTION ONE: HOW BAD IS IT? AND RETRIES HIDE THE ANSWER
//
// A USB transfer is not one event on the wire. It is a sequence of
// ATTEMPTS: the host issues a transaction, and if it fails -- CRC, timeout,
// bad PID -- the host simply issues it again. Up to MAX_RETRY times. If any
// attempt succeeds, the TRANSFER succeeded, and nothing above the host
// controller ever hears that anything went wrong.
//
//     attempts:   FAIL  FAIL  OK
//     transfer:   SUCCESS
//     application sees: a normal, successful transfer
//
// So the error rate measured at the transfer layer is ~0 on a link that is
// losing one transaction in three. That is not a rounding error; it is a
// metric reporting the opposite of the truth, and it is why "the device
// works fine" and "the link is nearly dead" are routinely both true.
//
//     THE DENOMINATOR IS ATTEMPTS, NOT TRANSFERS.
//
// errors/transfers understates the damage by exactly the retry factor, and
// the retry factor is the thing you are trying to measure.
//
// A RETRY THAT SUCCEEDED IS STILL EVIDENCE
//
// "It worked" is not "it was fine". A transfer that needed two attempts
// consumed three times the bus bandwidth, three times the power, and had
// one attempt left before it became a visible failure. It is the single
// best leading indicator there is, and it is invisible in every
// success/failure metric.
//
// So a completed transfer that took any retries at all is REPORTED -- once
// per transfer, not once per attempt, which is chapter 25.3's rule.
//
// QUESTION TWO: WHOSE BUG IS IT? AND THE CLASS NAMES THE LAYER
//
// Six error classes, three layers:
//
//     PHY        CRC16, bit-stuff      the wire, the connector, the cable
//     PROTOCOL   timeout, bad PID      the controller, the schedule
//     FIRMWARE   STALL, babble         the device's software
//
// The useful output is not the total. It is the DISTRIBUTION, because the
// distribution names the layer, and the layer names the team. A hundred
// errors that are all CRC is a cable. A hundred errors split evenly across
// all six is a power-supply problem that looks like everything at once.
//
// AND THE LAYER TOTAL IS NOT THE LARGEST CLASS
//
// Forty CRC errors, thirty timeouts and thirty bad PIDs is SIXTY protocol
// errors against forty PHY errors -- the protocol layer, by a clear margin,
// even though the single largest class belongs to the PHY. Ranking classes
// instead of summing layers picks the wrong team about as often as not.
module usb_error_triage #(
  parameter integer MAX_RETRY    = 3,  // attempts before the host gives up
  parameter integer LAYER_ALARM  = 32  // errors in one layer before alarming
) (
  input  wire       clk,
  input  wire       rst_n,

  input  wire       attempt_valid, // one transaction attempt completed
  input  wire [2:0] err_class,     // 0 = it worked; see the localparams
  input  wire       eot,

  output wire [7:0] retry_depth,   // retries used by the transfer in flight
  output wire [7:0] worst_retry,
  output wire [1:0] dominant_layer,
  output wire       err_pulse,
  output wire [1:0] err_code,

  // ---- The layer alarm gets its OWN pulse, not a code on the shared one.
  //
  // A layer crossing its threshold and a transfer running out of retries
  // are independent events that happen in the same cycle all the time --
  // the attempt that exhausts the budget is also an attempt that counts
  // towards a layer. Folding both into one code field means the last
  // assignment wins and one of the two events is silently deleted.
  //
  // Two events, two pulses. It is one extra wire and it removes an entire
  // class of "we definitely reported that" arguments.
  output wire       alarm_pulse,
  output wire [1:0] alarm_layer,

  // ---- the two denominators, both exposed, deliberately ----
  output reg [31:0] n_attempt,     // the RIGHT denominator
  output reg [31:0] n_xfer,        // the WRONG one, kept so it can be
                                   // compared against the right one
  output reg [31:0] n_fail,        // failed ATTEMPTS
  output reg [31:0] n_xfer_fail,   // transfers that ran out of retries
  output reg [31:0] n_masked,      // transfers that succeeded ON A RETRY
  output reg [31:0] n_alarm,       // layer thresholds crossed (max 3)

  output reg [31:0] n_crc,
  output reg [31:0] n_stuff,
  output reg [31:0] n_timeout,
  output reg [31:0] n_pid,
  output reg [31:0] n_stall,
  output reg [31:0] n_babble,
  output reg [31:0] n_badclass,    // an encoding that is not an error class

  output reg [31:0] n_phy,
  output reg [31:0] n_proto,
  output reg [31:0] n_fw
);

  localparam [2:0] E_NONE  = 3'd0,  // the attempt succeeded
                   E_CRC   = 3'd1,  // PHY
                   E_STUFF = 3'd2,  // PHY
                   E_TMO   = 3'd3,  // PROTOCOL
                   E_PID   = 3'd4,  // PROTOCOL
                   E_STALL = 3'd5,  // FIRMWARE
                   E_BABBL = 3'd6,  // FIRMWARE
                   E_RSVD  = 3'd7;  // not an error class at all

  localparam [1:0] L_PHY = 2'd0, L_PROTO = 2'd1, L_FW = 2'd2, L_NONE = 2'd3;

  localparam [1:0] T_NONE      = 2'd0,
                   T_MASKED    = 2'd1,  // a transfer succeeded ON A RETRY
                   T_XFER_FAIL = 2'd2;  // retries exhausted

  // ---- The retry budget is compared at FULL WIDTH. ----
  //
  // The obvious way to write this is to keep the depth in two bits -- a
  // budget of 3 needs no more -- and compare against `MAX_RETRY[1:0]`. That
  // works, and it makes the module parameterised in name only: with
  // MAX_RETRY = 5 the part-select is 1, and so is `localparam [1:0] MAXR =
  // MAX_RETRY`. BOTH forms truncate, silently, and the block gives up after
  // one failed attempt while its parameter says five.
  //
  // Narrowing a parameter to the width its DEFAULT value happens to need is
  // the bug. The counter and the comparison are eight bits wide because the
  // parameter is an integer, and the testbench instantiates a second copy
  // with MAX_RETRY = 5 specifically to prove it.
  localparam [7:0] MAXR = MAX_RETRY;

  // ---- The class-to-layer map, written as a function. ----
  //
  // A function rather than a table of constants, so the reason each class
  // belongs where it does sits next to the assignment. E_RSVD maps to
  // L_NONE on purpose: an encoding that is not an error class must not be
  // attributed to a layer, because attributing it puts a bug on somebody's
  // desk on the strength of a value nobody defined.
  function [1:0] layer_of;
    input [2:0] c;
    begin
      case (c)
        E_CRC, E_STUFF: layer_of = L_PHY;    // the wire said the wrong thing
        E_TMO, E_PID:   layer_of = L_PROTO;  // nobody answered, or wrongly
        E_STALL, E_BABBL: layer_of = L_FW;   // the device's software did
        default:        layer_of = L_NONE;   // E_NONE and E_RSVD
      endcase
    end
  endfunction

  reg [7:0]  dep_r;
  reg [7:0]  worst_r;
  reg [1:0]  ec_r;
  reg        er_r;
  reg [2:0]  alarmed_r;   // one bit per layer: report the CROSSING, once
  reg        al_r;
  reg [1:0]  alay_r;

  assign alarm_pulse = al_r;
  assign alarm_layer = alay_r;
  assign retry_depth = dep_r;
  assign worst_retry = worst_r;
  assign err_pulse   = er_r;
  assign err_code    = ec_r;

  // ---- The dominant layer is the largest LAYER TOTAL. ----
  //
  // Not the layer owning the largest single class. Forty CRC against thirty
  // timeouts and thirty bad PIDs is a PROTOCOL problem, and ranking classes
  // reports the PHY.
  assign dominant_layer = ((n_phy   >= n_proto) && (n_phy >= n_fw))  ? L_PHY   :
                          ((n_proto >= n_fw))                        ? L_PROTO :
                                                                       L_FW;

  reg [7:0] dep_n;
  reg [7:0] worst_n;
  reg [1:0] ec_n;
  reg       er_n;
  reg [2:0] alarmed_n;
  reg       al_n;
  reg [1:0] alay_n;
  reg [1:0] lay_n;
  reg       fail_n, ok_n, xfer_n, xfail_n, mask_n;
  reg [31:0] phy_n, proto_n, fw_n;

  always @* begin
    dep_n     = dep_r;
    worst_n   = worst_r;
    ec_n      = T_NONE;
    er_n      = 1'b0;
    alarmed_n = alarmed_r;
    al_n      = 1'b0;
    alay_n    = L_NONE;
    lay_n     = layer_of(err_class);
    fail_n    = 1'b0; ok_n = 1'b0; xfer_n = 1'b0;
    xfail_n   = 1'b0; mask_n = 1'b0;
    phy_n     = n_phy; proto_n = n_proto; fw_n = n_fw;

    if (eot) begin
      // Nothing: the counters are the report.
    end else if (attempt_valid) begin
      if (err_class == E_NONE) begin
        // ---- The attempt succeeded, so the TRANSFER succeeded. ----
        ok_n   = 1'b1;
        xfer_n = 1'b1;
        if (dep_r != 8'd0) begin
          // ---- ...but it only succeeded because of a retry. ----
          //
          // The transfer layer will record a success and the application
          // will never hear about this. It is the best leading indicator
          // on the bus and it is invisible in every success/failure metric.
          er_n   = 1'b1; ec_n = T_MASKED;
          mask_n = 1'b1;
        end
        dep_n = 8'd0;
      end else begin
        // ---- A failed ATTEMPT. Counted here, in the right denominator. ----
        fail_n = 1'b1;

        if (lay_n == L_PHY)        phy_n   = n_phy   + 32'd1;
        else if (lay_n == L_PROTO) proto_n = n_proto + 32'd1;
        else if (lay_n == L_FW)    fw_n    = n_fw    + 32'd1;
        // L_NONE: E_RSVD. Counted as a failed attempt, because it was one,
        // and attributed to nobody, because nothing defines it.

        if (dep_r + 8'd1 >= MAXR) begin
          // ---- Retries exhausted. NOW the transfer has failed. ----
          //
          // This is the only error the layers above the host controller
          // ever see, and by the time it arrives MAX_RETRY-1 attempts have
          // already been thrown away unlogged.
          er_n    = 1'b1; ec_n = T_XFER_FAIL;
          xfail_n = 1'b1;
          xfer_n  = 1'b1;
          dep_n   = 8'd0;
        end else begin
          dep_n = dep_r + 8'd1;
          if (dep_n > worst_r) worst_n = dep_n;
        end

        // ---- The layer alarm: reported on the CROSSING, once. ----
        //
        // A layer that is over threshold stays over threshold, so alarming
        // on the state rather than the transition reproduces chapter 25.3's
        // flood exactly.
        if ((lay_n == L_PHY) && (phy_n >= LAYER_ALARM) && !alarmed_r[0]) begin
          al_n = 1'b1; alay_n = L_PHY;   alarmed_n[0] = 1'b1;
        end else if ((lay_n == L_PROTO) && (proto_n >= LAYER_ALARM)
                     && !alarmed_r[1]) begin
          al_n = 1'b1; alay_n = L_PROTO; alarmed_n[1] = 1'b1;
        end else if ((lay_n == L_FW) && (fw_n >= LAYER_ALARM)
                     && !alarmed_r[2]) begin
          al_n = 1'b1; alay_n = L_FW;    alarmed_n[2] = 1'b1;
        end
      end
    end
  end

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      dep_r     <= 8'd0;
      worst_r   <= 8'd0;
      ec_r      <= T_NONE;
      er_r      <= 1'b0;
      alarmed_r <= 3'd0;
      al_r      <= 1'b0;
      alay_r    <= L_NONE;
      n_attempt <= 32'd0; n_xfer      <= 32'd0; n_fail   <= 32'd0;
      n_xfer_fail <= 32'd0; n_masked  <= 32'd0; n_alarm <= 32'd0;
      n_crc     <= 32'd0; n_stuff     <= 32'd0; n_timeout <= 32'd0;
      n_pid     <= 32'd0; n_stall     <= 32'd0; n_babble  <= 32'd0;
      n_badclass<= 32'd0;
      n_phy     <= 32'd0; n_proto     <= 32'd0; n_fw      <= 32'd0;
    end else begin
      dep_r     <= dep_n;
      worst_r   <= worst_n;
      ec_r      <= ec_n;
      er_r      <= er_n;
      alarmed_r <= alarmed_n;
      al_r      <= al_n;
      alay_r    <= alay_n;

      if (attempt_valid && !eot) n_attempt <= n_attempt + 32'd1;
      if (xfer_n)  n_xfer      <= n_xfer      + 32'd1;
      if (fail_n)  n_fail      <= n_fail      + 32'd1;
      if (xfail_n) n_xfer_fail <= n_xfer_fail + 32'd1;
      if (mask_n)  n_masked    <= n_masked    + 32'd1;
      if (al_n)    n_alarm     <= n_alarm     + 32'd1;

      n_phy   <= phy_n;
      n_proto <= proto_n;
      n_fw    <= fw_n;

      if (fail_n) begin
        case (err_class)
          E_CRC:   n_crc     <= n_crc     + 32'd1;
          E_STUFF: n_stuff   <= n_stuff   + 32'd1;
          E_TMO:   n_timeout <= n_timeout + 32'd1;
          E_PID:   n_pid     <= n_pid     + 32'd1;
          E_STALL: n_stall   <= n_stall   + 32'd1;
          E_BABBL: n_babble  <= n_babble  + 32'd1;
          default: n_badclass<= n_badclass+ 32'd1;
        endcase
      end
    end
  end
endmodule

10. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_error_triage -- the two questions a transfer error log has to answer,
// and the reason the obvious way of asking both gives the wrong answer.
//
// QUESTION ONE: HOW BAD IS IT? AND RETRIES HIDE THE ANSWER
//
// A USB transfer is not one event on the wire. It is a sequence of
// ATTEMPTS: the host issues a transaction, and if it fails -- CRC, timeout,
// bad PID -- the host simply issues it again. Up to MAX_RETRY times. If any
// attempt succeeds, the TRANSFER succeeded, and nothing above the host
// controller ever hears that anything went wrong.
//
//     attempts:   FAIL  FAIL  OK
//     transfer:   SUCCESS
//     application sees: a normal, successful transfer
//
// So the error rate measured at the transfer layer is ~0 on a link that is
// losing one transaction in three. That is not a rounding error; it is a
// metric reporting the opposite of the truth, and it is why "the device
// works fine" and "the link is nearly dead" are routinely both true.
//
//     THE DENOMINATOR IS ATTEMPTS, NOT TRANSFERS.
//
// errors/transfers understates the damage by exactly the retry factor, and
// the retry factor is the thing you are trying to measure.
//
// A RETRY THAT SUCCEEDED IS STILL EVIDENCE
//
// "It worked" is not "it was fine". A transfer that needed two attempts
// consumed three times the bus bandwidth, three times the power, and had
// one attempt left before it became a visible failure. It is the single
// best leading indicator there is, and it is invisible in every
// success/failure metric.
//
// So a completed transfer that took any retries at all is REPORTED -- once
// per transfer, not once per attempt, which is chapter 25.3's rule.
//
// QUESTION TWO: WHOSE BUG IS IT? AND THE CLASS NAMES THE LAYER
//
// Six error classes, three layers:
//
//     PHY        CRC16, bit-stuff      the wire, the connector, the cable
//     PROTOCOL   timeout, bad PID      the controller, the schedule
//     FIRMWARE   STALL, babble         the device's software
//
// The useful output is not the total. It is the DISTRIBUTION, because the
// distribution names the layer, and the layer names the team. A hundred
// errors that are all CRC is a cable. A hundred errors split evenly across
// all six is a power-supply problem that looks like everything at once.
//
// AND THE LAYER TOTAL IS NOT THE LARGEST CLASS
//
// Forty CRC errors, thirty timeouts and thirty bad PIDs is SIXTY protocol
// errors against forty PHY errors -- the protocol layer, by a clear margin,
// even though the single largest class belongs to the PHY. Ranking classes
// instead of summing layers picks the wrong team about as often as not.
package usb_triage_pkg;
  typedef enum logic [2:0] {
    E_NONE  = 3'd0,   // the attempt succeeded
    E_CRC   = 3'd1,   // PHY
    E_STUFF = 3'd2,   // PHY
    E_TMO   = 3'd3,   // PROTOCOL
    E_PID   = 3'd4,   // PROTOCOL
    E_STALL = 3'd5,   // FIRMWARE
    E_BABBL = 3'd6,   // FIRMWARE
    E_RSVD  = 3'd7    // not an error class at all
  } err_class_e;

  typedef enum logic [1:0] {
    L_PHY   = 2'd0,
    L_PROTO = 2'd1,
    L_FW    = 2'd2,
    L_NONE  = 2'd3    // attributed to NOBODY, deliberately
  } layer_e;

  typedef enum logic [1:0] {
    T_NONE      = 2'd0,
    T_MASKED    = 2'd1,   // a transfer succeeded ON A RETRY
    T_XFER_FAIL = 2'd2    // retries exhausted
  } triage_e;
endpackage

module usb_error_triage
  import usb_triage_pkg::*;
 #(
  parameter int MAX_RETRY   = 3,   // attempts before the host gives up
  parameter int LAYER_ALARM = 32   // errors in one layer before alarming
) (
  input  logic       clk,
  input  logic       rst_n,

  input  logic       attempt_valid, // one transaction attempt completed
  input  err_class_e err_class,     // E_NONE means it worked
  input  logic       eot,

  output logic [7:0] retry_depth,   // retries used by the transfer in flight
  output logic [7:0] worst_retry,
  output layer_e     dominant_layer,
  output logic       err_pulse,
  output triage_e    err_code,

  // ---- The layer alarm gets its OWN pulse, not a code on the shared one.
  //
  // A layer crossing its threshold and a transfer running out of retries
  // are independent events that happen in the same cycle all the time --
  // the attempt that exhausts the budget is also an attempt that counts
  // towards a layer. Folding both into one code field means the last
  // assignment wins and one of the two events is silently deleted.
  //
  // Two events, two pulses. It is one extra wire and it removes an entire
  // class of "we definitely reported that" arguments.
  output logic       alarm_pulse,
  output layer_e     alarm_layer,

  // ---- the two denominators, both exposed, deliberately ----
  output logic [31:0] n_attempt,     // the RIGHT denominator
  output logic [31:0] n_xfer,        // the WRONG one, kept so it can be
                                   // compared against the right one
  output logic [31:0] n_fail,        // failed ATTEMPTS
  output logic [31:0] n_xfer_fail,   // transfers that ran out of retries
  output logic [31:0] n_masked,      // transfers that succeeded ON A RETRY
  output logic [31:0] n_alarm,       // layer thresholds crossed (max 3)

  output logic [31:0] n_crc,
  output logic [31:0] n_stuff,
  output logic [31:0] n_timeout,
  output logic [31:0] n_pid,
  output logic [31:0] n_stall,
  output logic [31:0] n_babble,
  output logic [31:0] n_badclass,    // an encoding that is not an error class

  output logic [31:0] n_phy,
  output logic [31:0] n_proto,
  output logic [31:0] n_fw
);

  // ---- The retry budget is compared at FULL WIDTH. ----
  //
  // The obvious way to write this is to keep the depth in two bits -- a
  // budget of 3 needs no more -- and compare against `MAX_RETRY[1:0]`. That
  // works, and it makes the module parameterised in name only: with
  // MAX_RETRY = 5 the part-select is 1, and so is `localparam [1:0] MAXR =
  // MAX_RETRY`. BOTH forms truncate, silently, and the block gives up after
  // one failed attempt while its parameter says five.
  //
  // Narrowing a parameter to the width its DEFAULT value happens to need is
  // the bug. The counter and the comparison are eight bits wide because the
  // parameter is an int, and the testbench instantiates a second copy with
  // MAX_RETRY = 5 specifically to prove it.
  localparam logic [7:0] MAXR = 8'(MAX_RETRY);

  // ---- The class-to-layer map, written as a function. ----
  //
  // A function rather than a table of constants, so the reason each class
  // belongs where it does sits next to the assignment. E_RSVD maps to
  // L_NONE on purpose: an encoding that is not an error class must not be
  // attributed to a layer, because attributing it puts a bug on somebody's
  // desk on the strength of a value nobody defined.
  function automatic layer_e layer_of(input err_class_e c);
    begin
      case (c)
        E_CRC, E_STUFF: layer_of = L_PHY;    // the wire said the wrong thing
        E_TMO, E_PID:   layer_of = L_PROTO;  // nobody answered, or wrongly
        E_STALL, E_BABBL: layer_of = L_FW;   // the device's software did
        default:        layer_of = L_NONE;   // E_NONE and E_RSVD
      endcase
    end
  endfunction

  logic [7:0] dep_r;
  logic [7:0] worst_r;
  triage_e    ec_r;
  logic       er_r;
  logic [2:0] alarmed_r;  // one bit per layer: report the CROSSING, once
  logic       al_r;
  layer_e     alay_r;

  assign alarm_pulse = al_r;
  assign alarm_layer = alay_r;
  assign retry_depth = dep_r;
  assign worst_retry = worst_r;
  assign err_pulse   = er_r;
  assign err_code    = ec_r;

  // ---- The dominant layer is the largest LAYER TOTAL. ----
  //
  // Not the layer owning the largest single class. Forty CRC against thirty
  // timeouts and thirty bad PIDs is a PROTOCOL problem, and ranking classes
  // reports the PHY.
  //
  // Written as a function rather than a chain of ternaries: a conditional
  // expression whose arms are enumeration literals needs an explicit cast
  // in SystemVerilog, and Icarus rejects it outright. if/else over the same
  // conditions needs no cast and reads better besides.
  function automatic layer_e dominant(input logic [31:0] p,
                                      input logic [31:0] r,
                                      input logic [31:0] f);
    begin
      if ((p >= r) && (p >= f)) dominant = L_PHY;
      else if (r >= f)          dominant = L_PROTO;
      else                      dominant = L_FW;
    end
  endfunction

  assign dominant_layer = dominant(n_phy, n_proto, n_fw);

  logic [7:0] dep_n;
  logic [7:0] worst_n;
  triage_e    ec_n;
  logic       er_n;
  logic [2:0] alarmed_n;
  logic       al_n;
  layer_e     alay_n;
  layer_e     lay_n;
  logic       fail_n, ok_n, xfer_n, xfail_n, mask_n;
  logic [31:0] phy_n, proto_n, fw_n;

  always_comb begin
    dep_n     = dep_r;
    worst_n   = worst_r;
    ec_n      = T_NONE;
    er_n      = 1'b0;
    alarmed_n = alarmed_r;
    al_n      = 1'b0;
    alay_n    = L_NONE;
    lay_n     = layer_of(err_class);
    fail_n    = 1'b0; ok_n = 1'b0; xfer_n = 1'b0;
    xfail_n   = 1'b0; mask_n = 1'b0;
    phy_n     = n_phy; proto_n = n_proto; fw_n = n_fw;

    if (eot) begin
      // Nothing: the counters are the report.
    end else if (attempt_valid) begin
      if (err_class == E_NONE) begin
        // ---- The attempt succeeded, so the TRANSFER succeeded. ----
        ok_n   = 1'b1;
        xfer_n = 1'b1;
        if (dep_r != 8'd0) begin
          // ---- ...but it only succeeded because of a retry. ----
          //
          // The transfer layer will record a success and the application
          // will never hear about this. It is the best leading indicator
          // on the bus and it is invisible in every success/failure metric.
          er_n   = 1'b1; ec_n = T_MASKED;
          mask_n = 1'b1;
        end
        dep_n = 8'd0;
      end else begin
        // ---- A failed ATTEMPT. Counted here, in the right denominator. ----
        fail_n = 1'b1;

        if (lay_n == L_PHY)        phy_n   = n_phy   + 32'd1;
        else if (lay_n == L_PROTO) proto_n = n_proto + 32'd1;
        else if (lay_n == L_FW)    fw_n    = n_fw    + 32'd1;
        // L_NONE: E_RSVD. Counted as a failed attempt, because it was one,
        // and attributed to nobody, because nothing defines it.

        if ((dep_r + 8'd1) >= MAXR) begin
          // ---- Retries exhausted. NOW the transfer has failed. ----
          //
          // This is the only error the layers above the host controller
          // ever see, and by the time it arrives MAX_RETRY-1 attempts have
          // already been thrown away unlogged.
          er_n    = 1'b1; ec_n = T_XFER_FAIL;
          xfail_n = 1'b1;
          xfer_n  = 1'b1;
          dep_n   = 8'd0;
        end else begin
          dep_n = dep_r + 8'd1;
          if (dep_n > worst_r) worst_n = dep_n;
        end

        // ---- The layer alarm: reported on the CROSSING, once. ----
        //
        // A layer that is over threshold stays over threshold, so alarming
        // on the state rather than the transition reproduces chapter 25.3's
        // flood exactly.
        if ((lay_n == L_PHY) && (phy_n >= LAYER_ALARM) && !alarmed_r[0]) begin
          al_n = 1'b1; alay_n = L_PHY;   alarmed_n[0] = 1'b1;
        end else if ((lay_n == L_PROTO) && (proto_n >= LAYER_ALARM)
                     && !alarmed_r[1]) begin
          al_n = 1'b1; alay_n = L_PROTO; alarmed_n[1] = 1'b1;
        end else if ((lay_n == L_FW) && (fw_n >= LAYER_ALARM)
                     && !alarmed_r[2]) begin
          al_n = 1'b1; alay_n = L_FW;    alarmed_n[2] = 1'b1;
        end
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      dep_r     <= 8'd0;
      worst_r   <= 8'd0;
      ec_r      <= T_NONE;
      er_r      <= 1'b0;
      alarmed_r <= 3'd0;
      al_r      <= 1'b0;
      alay_r    <= L_NONE;
      n_attempt <= 32'd0; n_xfer      <= 32'd0; n_fail   <= 32'd0;
      n_xfer_fail <= 32'd0; n_masked  <= 32'd0; n_alarm <= 32'd0;
      n_crc     <= 32'd0; n_stuff     <= 32'd0; n_timeout <= 32'd0;
      n_pid     <= 32'd0; n_stall     <= 32'd0; n_babble  <= 32'd0;
      n_badclass<= 32'd0;
      n_phy     <= 32'd0; n_proto     <= 32'd0; n_fw      <= 32'd0;
    end else begin
      dep_r     <= dep_n;
      worst_r   <= worst_n;
      ec_r      <= ec_n;
      er_r      <= er_n;
      alarmed_r <= alarmed_n;
      al_r      <= al_n;
      alay_r    <= alay_n;

      if (attempt_valid && !eot) n_attempt <= n_attempt + 32'd1;
      if (xfer_n)  n_xfer      <= n_xfer      + 32'd1;
      if (fail_n)  n_fail      <= n_fail      + 32'd1;
      if (xfail_n) n_xfer_fail <= n_xfer_fail + 32'd1;
      if (mask_n)  n_masked    <= n_masked    + 32'd1;
      if (al_n)    n_alarm     <= n_alarm     + 32'd1;

      n_phy   <= phy_n;
      n_proto <= proto_n;
      n_fw    <= fw_n;

      if (fail_n) begin
        case (err_class)
          E_CRC:   n_crc     <= n_crc     + 32'd1;
          E_STUFF: n_stuff   <= n_stuff   + 32'd1;
          E_TMO:   n_timeout <= n_timeout + 32'd1;
          E_PID:   n_pid     <= n_pid     + 32'd1;
          E_STALL: n_stall   <= n_stall   + 32'd1;
          E_BABBL: n_babble  <= n_babble  + 32'd1;
          default: n_badclass<= n_badclass+ 32'd1;
        endcase
      end
    end
  end
endmodule

11. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- usb_error_triage -- the two questions a transfer error log has to answer,
-- and the reason the obvious way of asking both gives the wrong answer.
--
-- QUESTION ONE: HOW BAD IS IT? AND RETRIES HIDE THE ANSWER
--
-- A USB transfer is not one event on the wire. It is a sequence of
-- ATTEMPTS: the host issues a transaction, and if it fails -- CRC, timeout,
-- bad PID -- the host simply issues it again, up to MAX_RETRY times. If any
-- attempt succeeds, the TRANSFER succeeded, and nothing above the host
-- controller ever hears that anything went wrong.
--
--     attempts:   FAIL  FAIL  OK
--     transfer:   SUCCESS
--     application sees: a normal, successful transfer
--
-- So the error rate measured at the transfer layer is ~0 on a link losing
-- one transaction in three. That is not a rounding error; it is a metric
-- reporting the opposite of the truth, and it is why "the device works
-- fine" and "the link is nearly dead" are routinely both true.
--
--     THE DENOMINATOR IS ATTEMPTS, NOT TRANSFERS.
--
-- A RETRY THAT SUCCEEDED IS STILL EVIDENCE
--
-- A transfer that needed two attempts consumed three times the bus
-- bandwidth and had one attempt left before it became a visible failure. It
-- is the best leading indicator there is and it is invisible in every
-- success/failure metric, so it is reported -- once per transfer, not once
-- per attempt, which is chapter 25.3's rule.
--
-- QUESTION TWO: WHOSE BUG IS IT? AND THE CLASS NAMES THE LAYER
--
--     PHY        CRC16, bit-stuff      the wire, the connector, the cable
--     PROTOCOL   timeout, bad PID      the controller, the schedule
--     FIRMWARE   STALL, babble         the device's software
--
-- The useful output is the DISTRIBUTION, because the distribution names the
-- layer and the layer names the team.
--
-- AND THE LAYER TOTAL IS NOT THE LARGEST CLASS
--
-- Forty CRC errors, thirty timeouts and thirty bad PIDs is SIXTY protocol
-- errors against forty PHY errors -- the protocol layer by a clear margin,
-- even though the single largest class belongs to the PHY.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package usb_triage_pkg is
  constant E_NONE  : std_logic_vector(2 downto 0) := "000";  -- it worked
  constant E_CRC   : std_logic_vector(2 downto 0) := "001";  -- PHY
  constant E_STUFF : std_logic_vector(2 downto 0) := "010";  -- PHY
  constant E_TMO   : std_logic_vector(2 downto 0) := "011";  -- PROTOCOL
  constant E_PID   : std_logic_vector(2 downto 0) := "100";  -- PROTOCOL
  constant E_STALL : std_logic_vector(2 downto 0) := "101";  -- FIRMWARE
  constant E_BABBL : std_logic_vector(2 downto 0) := "110";  -- FIRMWARE
  constant E_RSVD  : std_logic_vector(2 downto 0) := "111";  -- not a class

  constant L_PHY   : std_logic_vector(1 downto 0) := "00";
  constant L_PROTO : std_logic_vector(1 downto 0) := "01";
  constant L_FW    : std_logic_vector(1 downto 0) := "10";
  constant L_NONE  : std_logic_vector(1 downto 0) := "11";  -- nobody

  constant T_NONE      : std_logic_vector(1 downto 0) := "00";
  constant T_MASKED    : std_logic_vector(1 downto 0) := "01";
  constant T_XFER_FAIL : std_logic_vector(1 downto 0) := "10";

  -- The class-to-layer map, as a function, so the reason each class belongs
  -- where it does sits next to the assignment. E_RSVD maps to L_NONE on
  -- purpose: an encoding nobody defined must not put a bug on a team's desk.
  function layer_of (c : std_logic_vector(2 downto 0))
    return std_logic_vector;
end package;

package body usb_triage_pkg is
  function layer_of (c : std_logic_vector(2 downto 0))
    return std_logic_vector is
  begin
    case c is
      when E_CRC | E_STUFF   => return L_PHY;    -- the wire said wrong
      when E_TMO | E_PID     => return L_PROTO;  -- nobody answered, or wrongly
      when E_STALL | E_BABBL => return L_FW;     -- the device's software did
      when others            => return L_NONE;   -- E_NONE and E_RSVD
    end case;
  end function;
end package body;

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

entity usb_error_triage is
  generic (
    MAX_RETRY   : integer := 3;
    LAYER_ALARM : integer := 32
  );
  port (
    clk           : in  std_logic;
    rst_n         : in  std_logic;

    attempt_valid : in  std_logic;
    err_class     : in  std_logic_vector(2 downto 0);
    eot           : in  std_logic;

    retry_depth    : out std_logic_vector(7 downto 0);
    worst_retry    : out std_logic_vector(7 downto 0);
    dominant_layer : out std_logic_vector(1 downto 0);
    err_pulse      : out std_logic;
    err_code       : out std_logic_vector(1 downto 0);

    -- The layer alarm gets its OWN pulse, not a code on the shared one. A
    -- layer crossing its threshold and a transfer running out of retries are
    -- independent events that happen in the same cycle all the time, and
    -- folding both into one code field means the last assignment wins and
    -- one of the two is silently deleted.
    alarm_pulse    : out std_logic;
    alarm_layer    : out std_logic_vector(1 downto 0);

    n_attempt   : out unsigned(31 downto 0);  -- the RIGHT denominator
    n_xfer      : out unsigned(31 downto 0);  -- the WRONG one, exposed so it
                                              -- can be compared to the right
    n_fail      : out unsigned(31 downto 0);
    n_xfer_fail : out unsigned(31 downto 0);
    n_masked    : out unsigned(31 downto 0);
    n_alarm     : out unsigned(31 downto 0);

    n_crc       : out unsigned(31 downto 0);
    n_stuff     : out unsigned(31 downto 0);
    n_timeout   : out unsigned(31 downto 0);
    n_pid       : out unsigned(31 downto 0);
    n_stall     : out unsigned(31 downto 0);
    n_babble    : out unsigned(31 downto 0);
    n_badclass  : out unsigned(31 downto 0);

    n_phy       : out unsigned(31 downto 0);
    n_proto     : out unsigned(31 downto 0);
    n_fw        : out unsigned(31 downto 0)
  );
end entity;

architecture rtl of usb_error_triage is
  -- ---- The retry budget is compared at FULL WIDTH. ----
  --
  -- The obvious way to write this is to keep the depth in two bits -- a
  -- budget of 3 needs no more -- and compare against a two-bit conversion of
  -- the generic. That works, and it makes the entity parameterised in name
  -- only: to_unsigned(5, 2) is 1, so the block would give up after one
  -- failed attempt while its generic says five.
  --
  -- Narrowing a generic to the width its DEFAULT value happens to need is
  -- the bug. The testbench instantiates a second copy with MAX_RETRY = 5
  -- specifically to prove this one is not narrowed.
  signal dep_r     : unsigned(7 downto 0);
  signal worst_r   : unsigned(7 downto 0);
  signal ec_r      : std_logic_vector(1 downto 0);
  signal er_r      : std_logic;
  signal al_r      : std_logic;
  signal alay_r    : std_logic_vector(1 downto 0);
  signal alarmed_r : std_logic_vector(2 downto 0);

  -- Initialised at declaration. The dominant_layer assignment below is
  -- concurrent, so it evaluates once at time 0 before any reset has been
  -- applied; uninitialised counters make numeric_std's ">=" report a
  -- metavalue and return FALSE, which is a warning on every run that
  -- trains people to ignore warnings.
  signal att_c, xfer_c, fail_c, xfail_c, mask_c, alarm_c :
    unsigned(31 downto 0) := (others => '0');
  signal crc_c, stuff_c, tmo_c, pid_c, stall_c, babbl_c  :
    unsigned(31 downto 0) := (others => '0');
  signal bad_c : unsigned(31 downto 0) := (others => '0');
  signal phy_c, proto_c, fw_c :
    unsigned(31 downto 0) := (others => '0');
begin
  retry_depth <= std_logic_vector(dep_r);
  worst_retry <= std_logic_vector(worst_r);
  err_pulse   <= er_r;
  err_code    <= ec_r;
  alarm_pulse <= al_r;
  alarm_layer <= alay_r;

  n_attempt   <= att_c;
  n_xfer      <= xfer_c;
  n_fail      <= fail_c;
  n_xfer_fail <= xfail_c;
  n_masked    <= mask_c;
  n_alarm     <= alarm_c;
  n_crc       <= crc_c;
  n_stuff     <= stuff_c;
  n_timeout   <= tmo_c;
  n_pid       <= pid_c;
  n_stall     <= stall_c;
  n_babble    <= babbl_c;
  n_badclass  <= bad_c;
  n_phy       <= phy_c;
  n_proto     <= proto_c;
  n_fw        <= fw_c;

  -- ---- The dominant layer is the largest LAYER TOTAL. ----
  --
  -- Not the layer owning the largest single class. Forty CRC against thirty
  -- timeouts and thirty bad PIDs is a PROTOCOL problem, and ranking classes
  -- reports the PHY.
  dominant_layer <= L_PHY   when (phy_c >= proto_c) and (phy_c >= fw_c) else
                    L_PROTO when (proto_c >= fw_c) else
                    L_FW;

  process (clk, rst_n)
    variable l      : std_logic_vector(1 downto 0);
    variable dep_v  : unsigned(7 downto 0);
    variable wrst_v : unsigned(7 downto 0);
    variable ec_v   : std_logic_vector(1 downto 0);
    variable er_v   : std_logic;
    variable al_v   : std_logic;
    variable alay_v : std_logic_vector(1 downto 0);
    variable alm_v  : std_logic_vector(2 downto 0);
    variable phy_v, proto_v, fw_v : unsigned(31 downto 0);
  begin
    if rst_n = '0' then
      dep_r     <= (others => '0');
      worst_r   <= (others => '0');
      ec_r      <= T_NONE;
      er_r      <= '0';
      al_r      <= '0';
      alay_r    <= L_NONE;
      alarmed_r <= (others => '0');
      att_c     <= (others => '0');
      xfer_c    <= (others => '0');
      fail_c    <= (others => '0');
      xfail_c   <= (others => '0');
      mask_c    <= (others => '0');
      alarm_c   <= (others => '0');
      crc_c     <= (others => '0');
      stuff_c   <= (others => '0');
      tmo_c     <= (others => '0');
      pid_c     <= (others => '0');
      stall_c   <= (others => '0');
      babbl_c   <= (others => '0');
      bad_c     <= (others => '0');
      phy_c     <= (others => '0');
      proto_c   <= (others => '0');
      fw_c      <= (others => '0');
    elsif rising_edge(clk) then
      l      := layer_of(err_class);
      dep_v  := dep_r;
      wrst_v := worst_r;
      ec_v   := T_NONE;
      er_v   := '0';
      al_v   := '0';
      alay_v := L_NONE;
      alm_v  := alarmed_r;
      phy_v  := phy_c;
      proto_v:= proto_c;
      fw_v   := fw_c;

      if eot = '1' then
        null;
      elsif attempt_valid = '1' then
        att_c <= att_c + 1;

        if err_class = E_NONE then
          -- ---- The attempt succeeded, so the TRANSFER succeeded. ----
          xfer_c <= xfer_c + 1;
          if dep_v /= x"00" then
            -- ...but only because of a retry. The transfer layer records a
            -- success and the application never hears about this. It is the
            -- only trace of the damage anywhere in the system.
            er_v   := '1';
            ec_v   := T_MASKED;
            mask_c <= mask_c + 1;
          end if;
          dep_v := x"00";
        else
          -- ---- A failed ATTEMPT. Counted in the RIGHT denominator. ----
          fail_c <= fail_c + 1;

          case err_class is
            when E_CRC   => crc_c   <= crc_c   + 1;
            when E_STUFF => stuff_c <= stuff_c + 1;
            when E_TMO   => tmo_c   <= tmo_c   + 1;
            when E_PID   => pid_c   <= pid_c   + 1;
            when E_STALL => stall_c <= stall_c + 1;
            when E_BABBL => babbl_c <= babbl_c + 1;
            when others  => bad_c   <= bad_c   + 1;
          end case;

          if l = L_PHY then
            phy_v := phy_c + 1;
          elsif l = L_PROTO then
            proto_v := proto_c + 1;
          elsif l = L_FW then
            fw_v := fw_c + 1;
          end if;
          -- L_NONE: E_RSVD. Counted as a failed attempt, because it was
          -- one, and attributed to nobody, because nothing defines it.

          if (dep_v + 1) >= to_unsigned(MAX_RETRY, 8) then
            -- ---- Retries exhausted. NOW the transfer has failed. ----
            --
            -- This is the only error the layers above the host controller
            -- ever see, and by the time it arrives MAX_RETRY-1 attempts have
            -- been thrown away unlogged.
            er_v    := '1';
            ec_v    := T_XFER_FAIL;
            xfail_c <= xfail_c + 1;
            xfer_c  <= xfer_c + 1;
            dep_v   := x"00";
          else
            dep_v := dep_v + 1;
            if dep_v > wrst_v then
              wrst_v := dep_v;
            end if;
          end if;

          -- ---- The layer alarm: reported on the CROSSING, once. ----
          --
          -- A layer over threshold stays over threshold, so alarming on the
          -- state rather than the transition reproduces chapter 25.3's flood
          -- exactly.
          if (l = L_PHY) and (phy_v >= to_unsigned(LAYER_ALARM, 32))
             and alm_v(0) = '0' then
            al_v := '1'; alay_v := L_PHY;   alm_v(0) := '1';
            alarm_c <= alarm_c + 1;
          elsif (l = L_PROTO) and (proto_v >= to_unsigned(LAYER_ALARM, 32))
             and alm_v(1) = '0' then
            al_v := '1'; alay_v := L_PROTO; alm_v(1) := '1';
            alarm_c <= alarm_c + 1;
          elsif (l = L_FW) and (fw_v >= to_unsigned(LAYER_ALARM, 32))
             and alm_v(2) = '0' then
            al_v := '1'; alay_v := L_FW;    alm_v(2) := '1';
            alarm_c <= alarm_c + 1;
          end if;
        end if;
      end if;

      phy_c     <= phy_v;
      proto_c   <= proto_v;
      fw_c      <= fw_v;
      dep_r     <= dep_v;
      worst_r   <= wrst_v;
      ec_r      <= ec_v;
      er_r      <= er_v;
      al_r      <= al_v;
      alay_r    <= alay_v;
      alarmed_r <= alm_v;
    end if;
  end process;
end architecture;

12. Seeing a Transfer Survive on Its Last Attempt

Two failures, then a success — reported as MASKED

usb_error_triage — a success that cost three attempts

10 cycles
A ten-cycle waveform. Three attempts are driven: a CRC error, a second CRC error, and then a success. The retry depth rises from zero to one to two and returns to zero. The attempt counter reaches three and the failed-attempt counter reaches two, while the transfer counter reaches one. On the successful attempt the error pulse goes high with the code MASKED and the masked counter reaches one.attempt 1 fails: CRCattempt 1 fails: CRCattempt 3: the last one in the budgetattempt 3: the last one inthe budgetone transfer, one success, two failuresone transfer, one success,two failuresn_xfer says 0% — n_fail says 2 of 3n_xfer says 0% — n_failsays 2 of 3clkattempt_validerr_classCRCCRCNONENONENONENONENONENONENONENONEretry_depth0120000000n_attempt0123333333n_fail0122222222n_xfer0001111111err_pulseerr_code000MASKEDMASKEDMASKEDMASKEDMASKEDMASKEDMASKEDt0t1t2t3t4t5t6t7t8t9
The retry depth climbs with each failure and returns to zero when the transfer finally completes. The transfer counter records one success. The only record anywhere that this transfer cost three attempts is the MASKED report, and the only reason the failed attempts are counted at all is that the block counts attempts rather than transfers.

And the coincident case that section 7 is about:

The attempt that exhausts the budget is also the attempt that crosses the alarm

usb_error_triage — two events, two pulses, one cycle

10 cycles
A ten-cycle waveform. Three STALL attempts are driven in a row. The retry depth rises from zero to one to two. The firmware error count rises from twenty-nine to thirty to thirty-one and then to thirty-two. On the third failure the error pulse goes high with the code XFER_FAIL because the retry budget is exhausted, and at the same time the separate alarm pulse goes high with the layer FIRMWARE because the count has reached the alarm threshold of thirty-two.firmware errors already at 29firmware errors already at29third attempt: the budget is gonethird attempt: the budgetis gonetwo events, two pulses, one cycletwo events, two pulses, onecycleclkattempt_validerr_classSTALLSTALLSTALLSTALLSTALLSTALLSTALLSTALLSTALLSTALLretry_depth0120000000n_fw29303132323232323232err_pulseerr_code000XFER_FAILXFER_FAILXFER_FAILXFER_FAILXFER_FAILXFER_FAILXFER_FAILalarm_pulsealarm_layer000FWFWFWFWFWFWFWt0t1t2t3t4t5t6t7t8t9
Three firmware-class failures in a row. The third one both exhausts the retry budget and takes the firmware layer's error count to the alarm threshold. Both reports fire in the same cycle, on separate pulses — a single shared code field would have delivered exactly one of them.

13. The Testbenches

The oracle is a shadow model written from sections 2 to 7 rather than from the RTL, re-derived every cycle and compared against every output — twenty-five checks per cycle, including the two structural sums.

The exhaustive claim is the one worth reading carefully, because it is over sequences, not over values:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   A retry budget is a property of a SEQUENCE.

   The third consecutive failure ends a transfer.
   The first does not.

   So sweeping the 8 err_class encodings one at a time
   proves NOTHING about the budget.

   Phase 1 drives all 8^3 = 512 three-attempt sequences,
   each preceded by a success so it starts at depth 0.
   2048 attempts, every ordering of every class over the
   whole budget.

Six phases:

PhaseWhat it establishes
1all 512 three-attempt sequences — every path through the retry logic
2200 transfers that each fail once: 0 transfer failures, 200 of 400 attempts failed
3the give-up boundary, one attempt wide, checked on both sides
4the layer alarm fires once across 64 crossings of its threshold
540 CRC against 30+30 protocol: the layer wins, not the largest class
640000 random attempts with a realistic class mix
—a second instance with MAX_RETRY = 5, driven by the same stimulus

Verilog-2005 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// Testbench for usb_error_triage.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS OVER SEQUENCES, NOT OVER VALUES
//
// A retry budget is a property of a SEQUENCE. An attempt that fails means
// something different depending on what the previous two attempts did --
// the third consecutive failure ends a transfer and the first does not --
// so sweeping the eight err_class encodings one at a time proves nothing
// about the budget at all.
//
// Phase 1 drives all 8^3 = 512 possible three-attempt sequences, each one
// preceded by a success so that it starts from a known retry depth. That is
// every path through the retry logic that MAX_RETRY = 3 admits, and it
// includes every ordering of class and outcome within those paths.
module tb_et2_v;

  localparam integer MAX_RETRY   = 3;
  localparam integer LAYER_ALARM = 32;

  localparam [2:0] E_NONE  = 3'd0, E_CRC   = 3'd1, E_STUFF = 3'd2,
                   E_TMO   = 3'd3, E_PID   = 3'd4, E_STALL = 3'd5,
                   E_BABBL = 3'd6, E_RSVD  = 3'd7;

  localparam [1:0] L_PHY = 2'd0, L_PROTO = 2'd1, L_FW = 2'd2, L_NONE = 2'd3;

  localparam [1:0] T_NONE = 2'd0, T_MASKED = 2'd1, T_XFER_FAIL = 2'd2;

  reg        clk = 1'b0, rst_n = 1'b0;
  reg        attempt_valid = 1'b0, eot = 1'b0;
  reg  [2:0] err_class = 3'd0;

  wire [1:0] dominant_layer, err_code, alarm_layer;
  wire [7:0] retry_depth;
  wire [7:0] worst_retry;
  wire       err_pulse, alarm_pulse;
  wire [31:0] n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked, n_alarm;
  wire [31:0] n_crc, n_stuff, n_timeout, n_pid, n_stall, n_babble, n_badclass;
  wire [31:0] n_phy, n_proto, n_fw;

  usb_error_triage #(.MAX_RETRY(MAX_RETRY), .LAYER_ALARM(LAYER_ALARM)) dut (
    .clk(clk), .rst_n(rst_n),
    .attempt_valid(attempt_valid), .err_class(err_class), .eot(eot),
    .retry_depth(retry_depth), .worst_retry(worst_retry),
    .dominant_layer(dominant_layer),
    .err_pulse(err_pulse), .err_code(err_code),
    .alarm_pulse(alarm_pulse), .alarm_layer(alarm_layer),
    .n_attempt(n_attempt), .n_xfer(n_xfer), .n_fail(n_fail),
    .n_xfer_fail(n_xfer_fail), .n_masked(n_masked), .n_alarm(n_alarm),
    .n_crc(n_crc), .n_stuff(n_stuff), .n_timeout(n_timeout), .n_pid(n_pid),
    .n_stall(n_stall), .n_babble(n_babble), .n_badclass(n_badclass),
    .n_phy(n_phy), .n_proto(n_proto), .n_fw(n_fw)
  );

  // ---- A SECOND instance, with a different retry budget. ----
  //
  // The whole point of a parameter is that changing it changes the
  // behaviour. A module whose depth counter is two bits wide accepts
  // MAX_RETRY = 5, truncates it to 1, and gives up after one failed attempt
  // -- and every test written against the default value of 3 still passes.
  //
  // This instance is driven by exactly the same stimulus and checked at the
  // one place the budget is visible: where it gives up.
  wire [31:0] n5_xfer_fail;
  wire [7:0]  n5_depth;

  usb_error_triage #(.MAX_RETRY(5), .LAYER_ALARM(LAYER_ALARM)) dut5 (
    .clk(clk), .rst_n(rst_n),
    .attempt_valid(attempt_valid), .err_class(err_class), .eot(eot),
    .retry_depth(n5_depth), .worst_retry(), .dominant_layer(),
    .err_pulse(), .err_code(), .alarm_pulse(), .alarm_layer(),
    .n_attempt(), .n_xfer(), .n_fail(), .n_xfer_fail(n5_xfer_fail),
    .n_masked(), .n_alarm(),
    .n_crc(), .n_stuff(), .n_timeout(), .n_pid(),
    .n_stall(), .n_babble(), .n_badclass(),
    .n_phy(), .n_proto(), .n_fw()
  );

  always #5 clk = ~clk;

  // ---------------- the shadow model ----------------
  reg [7:0] m_dep;
  reg [7:0] m_worst;
  reg [1:0] m_ec, m_alay;
  reg       m_er, m_al;
  reg [2:0] m_alarm;
  reg [31:0] m_att, m_xfer, m_fail, m_xfail, m_mask, m_nal;
  reg [31:0] m_crc, m_stuff, m_tmo, m_pid, m_stall, m_babbl, m_bad;
  reg [31:0] m_phy, m_proto, m_fw;

  // Written from the chapter, not from the design. The map is the claim:
  // an encoding that is not an error class is attributed to NOBODY, because
  // attributing it puts a bug on somebody's desk on the strength of a value
  // nobody defined.
  function [1:0] lay;
    input [2:0] c;
    begin
      if (c == E_CRC || c == E_STUFF)        lay = L_PHY;
      else if (c == E_TMO || c == E_PID)     lay = L_PROTO;
      else if (c == E_STALL || c == E_BABBL) lay = L_FW;
      else                                   lay = L_NONE;
    end
  endfunction

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

  // reach: err_class (8) x the retry depth BEFORE the attempt (0..2)
  reg [0:0] reach [0:23];
  integer   n_reach;

  task ck;
    input [255:0] nm;
    input [31:0]  got, exp;
    begin
      checks = checks + 1;
      if (got !== exp) begin
        errors = errors + 1;
        if (errors < 25)
          $display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
                   $time, steps, nm, got, exp);
      end
    end
  endtask

  task model_step;
    reg [1:0] l;
    begin
      m_er = 1'b0; m_ec = T_NONE; m_al = 1'b0; m_alay = L_NONE;
      l = lay(err_class);

      if (eot) begin
        // nothing
      end else if (attempt_valid) begin
        m_att = m_att + 1;
        if (err_class == E_NONE) begin
          m_xfer = m_xfer + 1;
          if (m_dep != 8'd0) begin
            // The transfer succeeded, and it only succeeded because of a
            // retry. The transfer layer records a success; this is the only
            // place the cost is visible.
            m_er = 1'b1; m_ec = T_MASKED;
            m_mask = m_mask + 1;
          end
          m_dep = 8'd0;
        end else begin
          m_fail = m_fail + 1;
          case (err_class)
            E_CRC:   m_crc   = m_crc   + 1;
            E_STUFF: m_stuff = m_stuff + 1;
            E_TMO:   m_tmo   = m_tmo   + 1;
            E_PID:   m_pid   = m_pid   + 1;
            E_STALL: m_stall = m_stall + 1;
            E_BABBL: m_babbl = m_babbl + 1;
            default: m_bad   = m_bad   + 1;
          endcase
          if (l == L_PHY)        m_phy   = m_phy   + 1;
          else if (l == L_PROTO) m_proto = m_proto + 1;
          else if (l == L_FW)    m_fw    = m_fw    + 1;

          if (m_dep + 8'd1 >= MAX_RETRY) begin
            m_er = 1'b1; m_ec = T_XFER_FAIL;
            m_xfail = m_xfail + 1;
            m_xfer  = m_xfer  + 1;
            m_dep   = 8'd0;
          end else begin
            m_dep = m_dep + 8'd1;
            if (m_dep > m_worst) m_worst = m_dep;
          end

          // Its own pulse, so it can coincide with T_XFER_FAIL without
          // either event overwriting the other.
          if ((l == L_PHY) && (m_phy >= LAYER_ALARM) && !m_alarm[0]) begin
            m_al = 1'b1; m_alay = L_PHY;   m_alarm[0] = 1'b1; m_nal = m_nal+1;
          end else if ((l == L_PROTO) && (m_proto >= LAYER_ALARM)
                       && !m_alarm[1]) begin
            m_al = 1'b1; m_alay = L_PROTO; m_alarm[1] = 1'b1; m_nal = m_nal+1;
          end else if ((l == L_FW) && (m_fw >= LAYER_ALARM)
                       && !m_alarm[2]) begin
            m_al = 1'b1; m_alay = L_FW;    m_alarm[2] = 1'b1; m_nal = m_nal+1;
          end
        end
      end
    end
  endtask

  // The dominant layer, from the model's own totals, with the tie-break the
  // design documents: the lowest layer index wins.
  // Verilog-2005 requires at least one input port on a function, so the
  // model's own totals are passed in rather than read from module scope.
  function [1:0] dom;
    input dummy;
    begin
      if ((m_phy >= m_proto) && (m_phy >= m_fw)) dom = L_PHY;
      else if (m_proto >= m_fw)                  dom = L_PROTO;
      else                                       dom = L_FW;
    end
  endfunction

  task check_out;
    begin
      ck("retry_depth",    {24'd0, retry_depth},    {24'd0, m_dep});
      ck("worst_retry",    {24'd0, worst_retry},    {24'd0, m_worst});
      ck("err_pulse",      {31'd0, err_pulse},      {31'd0, m_er});
      ck("err_code",       {30'd0, err_code},       {30'd0, m_ec});
      ck("alarm_pulse",    {31'd0, alarm_pulse},    {31'd0, m_al});
      ck("alarm_layer",    {30'd0, alarm_layer},    {30'd0, m_alay});
      ck("dominant_layer", {30'd0, dominant_layer}, {30'd0, dom(1'b0)});
      ck("n_attempt",   n_attempt,   m_att);
      ck("n_xfer",      n_xfer,      m_xfer);
      ck("n_fail",      n_fail,      m_fail);
      ck("n_xfer_fail", n_xfer_fail, m_xfail);
      ck("n_masked",    n_masked,    m_mask);
      ck("n_alarm",     n_alarm,     m_nal);
      ck("n_crc",      n_crc,      m_crc);
      ck("n_stuff",    n_stuff,    m_stuff);
      ck("n_timeout",  n_timeout,  m_tmo);
      ck("n_pid",      n_pid,      m_pid);
      ck("n_stall",    n_stall,    m_stall);
      ck("n_babble",   n_babble,   m_babbl);
      ck("n_badclass", n_badclass, m_bad);
      ck("n_phy",      n_phy,      m_phy);
      ck("n_proto",    n_proto,    m_proto);
      ck("n_fw",       n_fw,       m_fw);
      // ---- the two structural invariants ----
      //
      // Every failed attempt is exactly one class; every class belongs to at
      // most one layer. A report that cannot be decomposed cannot be trusted
      // (chapter 23.4), and E_RSVD is why the LAYER sum excludes the
      // unattributed count rather than folding it into one of the three.
      ck("class sum",
         n_crc + n_stuff + n_timeout + n_pid + n_stall + n_babble + n_badclass,
         m_fail);
      ck("layer sum", n_phy + n_proto + n_fw, m_fail - m_bad);
    end
  endtask

  task step;
    begin
      if (attempt_valid && !eot)
        reach[{1'b0, err_class} * 3 + {30'd0, m_dep}] = 1'b1;
      model_step;
      @(posedge clk);
      #1;
      steps = steps + 1;
      check_out;
    end
  endtask

  task att; input [2:0] c;
    begin
      attempt_valid = 1'b1; eot = 1'b0; err_class = c;
      step;
    end
  endtask

  task nop;
    begin
      attempt_valid = 1'b0; eot = 1'b0;
      step;
    end
  endtask

  // Reset the DUT and the model together. Phases 4 and 5 both measure a
  // threshold crossing or a ranking, and both are meaningless on top of the
  // totals phase 1 leaves behind -- phase 1 drives every class hundreds of
  // times, so every layer alarm has already fired before either phase
  // starts. A phase that measures an accumulation needs a known accumulator.
  task reset_all;
    begin
      attempt_valid = 1'b0; clear_model; eot = 1'b0;
      rst_n = 1'b0;
      @(posedge clk); #1;
      rst_n = 1'b1;
      @(negedge clk);
    end
  endtask

  task clear_model;
    begin
      m_dep = 8'd0; m_worst = 8'd0; m_ec = T_NONE; m_er = 1'b0;
      m_al = 1'b0; m_alay = L_NONE; m_alarm = 3'd0;
      m_att=0; m_xfer=0; m_fail=0; m_xfail=0; m_mask=0; m_nal=0;
      m_crc=0; m_stuff=0; m_tmo=0; m_pid=0; m_stall=0; m_babbl=0; m_bad=0;
      m_phy=0; m_proto=0; m_fw=0;
    end
  endtask

  integer i, a, b, c, w;
  integer base_mask, base_xfail, base_att, base_xfer, base_fail, base_al;

  initial begin
    for (k = 0; k < 24; k = k + 1) reach[k] = 1'b0;
    clear_model;

    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(negedge clk);

    // ================= PHASE 1 -- all 512 three-attempt sequences ========
    //
    // Each sequence is preceded by a success so that it starts at retry
    // depth 0. Every ordering of every class over the whole retry budget,
    // which is the only way to cover a rule whose meaning depends on
    // history.
    for (a = 0; a < 8; a = a + 1)
      for (b = 0; b < 8; b = b + 1)
        for (c = 0; c < 8; c = c + 1) begin
          att(E_NONE);
          att(a[2:0]);
          att(b[2:0]);
          att(c[2:0]);
        end

    // ================= PHASE 2 -- retries MASK the error rate ============
    //
    // The headline, measured rather than asserted. 200 transfers, each of
    // which fails once and then succeeds. Every transfer succeeds, so the
    // transfer-level failure rate is EXACTLY ZERO, while one attempt in two
    // failed.
    att(E_NONE);                       // start clean
    base_att  = m_att;  base_xfer = m_xfer;
    base_fail = m_fail; base_xfail = m_xfail; base_mask = m_mask;
    for (i = 0; i < 200; i = i + 1) begin
      att(E_CRC);
      att(E_NONE);
    end
    if (m_xfail != base_xfail) begin
      errors = errors + 1;
      $display("FAIL a transfer failed when none should have");
    end
    if (m_xfer - base_xfer != 200) begin
      errors = errors + 1;
      $display("FAIL transfers %0d expected 200", m_xfer - base_xfer);
    end
    if (m_att - base_att != 400) begin
      errors = errors + 1;
      $display("FAIL attempts %0d expected 400", m_att - base_att);
    end
    if (m_fail - base_fail != 200) begin
      errors = errors + 1;
      $display("FAIL failed attempts %0d expected 200", m_fail - base_fail);
    end
    // ...and every one of those 200 transfers is reported as MASKED, which
    // is the only trace of the damage anywhere in the system.
    if (m_mask - base_mask != 200) begin
      errors = errors + 1;
      $display("FAIL masked %0d expected 200", m_mask - base_mask);
    end
    $display("PHASE2 transfer-level failures=%0d  attempt-level failures=%0d/%0d",
             m_xfail - base_xfail, m_fail - base_fail, m_att - base_att);

    // ================= PHASE 3 -- the give-up boundary ===================
    //
    // MAX_RETRY-1 failures then a success is a MASKED transfer and nothing
    // else. MAX_RETRY failures is a transfer failure and nothing else. The
    // boundary is one attempt wide and both sides of it are checked.
    att(E_NONE);
    base_xfail = m_xfail; base_mask = m_mask;
    for (i = 0; i < MAX_RETRY - 1; i = i + 1) att(E_TMO);
    if (m_xfail != base_xfail) begin
      errors = errors + 1;
      $display("FAIL gave up early");
    end
    att(E_NONE);
    if (m_mask != base_mask + 1) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY-1 failures then success was not MASKED");
    end
    base_xfail = m_xfail; base_mask = m_mask;
    for (i = 0; i < MAX_RETRY; i = i + 1) att(E_TMO);
    if (m_xfail != base_xfail + 1) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY failures did not fail the transfer");
    end
    if (m_mask != base_mask) begin
      errors = errors + 1;
      $display("FAIL a failed transfer was also reported as masked");
    end
    if (m_dep != 8'd0) begin
      errors = errors + 1;
      $display("FAIL retry depth not reset after giving up");
    end

    // ---- The parameter has to mean something. ----
    //
    // Four consecutive failures must NOT exhaust a budget of five, and the
    // fifth must. A two-bit depth counter fails the first of these, and no
    // test written against the default budget of 3 would ever notice.
    att(E_NONE);
    base_xfail = n5_xfer_fail;
    for (i = 0; i < 4; i = i + 1) att(E_TMO);
    if (n5_xfer_fail != base_xfail) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY=5 gave up after 4 attempts");
    end
    if (n5_depth != 8'd4) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY=5 depth is %0d after 4 failures, expected 4",
               n5_depth);
    end
    att(E_TMO);
    if (n5_xfer_fail != base_xfail + 1) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY=5 did not give up after 5 attempts");
    end
    att(E_NONE);

    // ================= PHASE 4 -- the layer alarm fires ONCE =============
    //
    // Enough firmware-class errors to cross LAYER_ALARM twice over. One
    // alarm, on the crossing.
    reset_all;
    base_al = m_nal;
    for (i = 0; i < 2 * LAYER_ALARM; i = i + 1) begin
      att(E_STALL);
      att(E_NONE);
    end
    if (m_nal != base_al + 1) begin
      errors = errors + 1;
      $display("FAIL firmware alarms %0d expected 1", m_nal - base_al);
    end

    // ================= PHASE 5 -- the layer total, not the largest class ==
    //
    // The case the chapter names: 40 CRC (PHY) against 30 timeouts and 30
    // bad PIDs (both PROTOCOL). The single largest CLASS is CRC. The
    // dominant LAYER is protocol, 60 to 40, and an implementation that
    // ranks classes reports the wrong team.
    //
    // Counted from a clean slate by resetting the DUT, because the earlier
    // phases have left large totals that would swamp the comparison.
    reset_all;
    for (i = 0; i < 40; i = i + 1) begin att(E_CRC); att(E_NONE); end
    for (i = 0; i < 30; i = i + 1) begin att(E_TMO); att(E_NONE); end
    for (i = 0; i < 30; i = i + 1) begin att(E_PID); att(E_NONE); end
    if (m_crc != 40 || m_tmo != 30 || m_pid != 30) begin
      errors = errors + 1;
      $display("FAIL phase 5 class counts %0d %0d %0d", m_crc, m_tmo, m_pid);
    end
    if (dominant_layer !== L_PROTO) begin
      errors = errors + 1;
      $display("FAIL dominant layer is %0d, expected PROTO (60 vs 40)",
               dominant_layer);
    end
    $display("PHASE5 largest class = CRC (%0d), dominant layer = %0d (phy=%0d proto=%0d fw=%0d)",
             m_crc, dominant_layer, m_phy, m_proto, m_fw);

    // The random phase is switchable, because a mutation score is only
    // interesting once it is DECOMPOSED. Phase 1 alone reaches all 24
    // class x depth situations, so the exhaustiveness proof still holds
    // with the random phase compiled out.
`ifndef DIRECTED_ONLY
    // ================= PHASE 6 -- random =================================
    //
    // The class is drawn ONCE into a variable: a chain of ternaries each
    // draw a new number, so the branches become independent events rather
    // than the nested distribution the code appears to describe.
    for (i = 0; i < 40000; i = i + 1) begin
      w = $unsigned($random) % 1000;
      if (w < 30) begin
        nop;
      end else if (w < 700) begin
        att(E_NONE);
      end else if (w < 810) att(E_CRC);
      else if (w < 860) att(E_STUFF);
      else if (w < 920) att(E_TMO);
      else if (w < 960) att(E_PID);
      else if (w < 980) att(E_STALL);
      else if (w < 995) att(E_BABBL);
      else att(E_RSVD);
    end

`endif

    // ================= the exhaustiveness proof ==========================
    n_reach = 0;
    for (k = 0; k < 24; k = k + 1) n_reach = n_reach + reach[k];
    if (n_reach != 24) begin
      errors = errors + 1;
      $display("FAIL class x depth reach %0d/24", n_reach);
      for (k = 0; k < 24; k = k + 1)
        if (!reach[k]) $display("  unreached class=%0d depth=%0d", k/3, k%3);
    end

    $display("steps=%0d checks=%0d reach=%0d/24 errors=%0d",
             steps, checks, n_reach, errors);
    $display("attempts=%0d transfers=%0d failed_attempts=%0d xfer_fail=%0d masked=%0d",
             n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked);
    $display("crc=%0d stuff=%0d timeout=%0d pid=%0d stall=%0d babble=%0d badclass=%0d",
             n_crc, n_stuff, n_timeout, n_pid, n_stall, n_babble, n_badclass);
    $display("phy=%0d proto=%0d fw=%0d alarms=%0d worst_retry=%0d",
             n_phy, n_proto, n_fw, n_alarm, worst_retry);
    $display("%0s: %0d errors in %0d checks",
             (errors == 0) ? "PASS" : "FAIL", errors, checks);
    $finish;
  end
endmodule

SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// Testbench for usb_error_triage.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS OVER SEQUENCES, NOT OVER VALUES
//
// A retry budget is a property of a SEQUENCE. An attempt that fails means
// something different depending on what the previous two attempts did --
// the third consecutive failure ends a transfer and the first does not --
// so sweeping the eight err_class encodings one at a time proves nothing
// about the budget at all.
//
// Phase 1 drives all 8^3 = 512 possible three-attempt sequences, each one
// preceded by a success so that it starts from a known retry depth. That is
// every path through the retry logic that MAX_RETRY = 3 admits, and it
// includes every ordering of class and outcome within those paths.
module tb_et2_sv;
  import usb_triage_pkg::*;

  localparam int MAX_RETRY   = 3;
  localparam int LAYER_ALARM = 32;

  logic       clk = 1'b0, rst_n = 1'b0;
  logic       attempt_valid = 1'b0, eot = 1'b0;
  err_class_e err_class = E_NONE;

  logic [7:0] retry_depth;
  layer_e     dominant_layer, alarm_layer;
  triage_e    err_code;
  logic [7:0] worst_retry;
  logic       err_pulse, alarm_pulse;
  logic [31:0] n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked, n_alarm;
  logic [31:0] n_crc, n_stuff, n_timeout, n_pid, n_stall, n_babble, n_badclass;
  logic [31:0] n_phy, n_proto, n_fw;

  usb_error_triage #(.MAX_RETRY(MAX_RETRY), .LAYER_ALARM(LAYER_ALARM)) dut (
    .clk(clk), .rst_n(rst_n),
    .attempt_valid(attempt_valid), .err_class(err_class), .eot(eot),
    .retry_depth(retry_depth), .worst_retry(worst_retry),
    .dominant_layer(dominant_layer),
    .err_pulse(err_pulse), .err_code(err_code),
    .alarm_pulse(alarm_pulse), .alarm_layer(alarm_layer),
    .n_attempt(n_attempt), .n_xfer(n_xfer), .n_fail(n_fail),
    .n_xfer_fail(n_xfer_fail), .n_masked(n_masked), .n_alarm(n_alarm),
    .n_crc(n_crc), .n_stuff(n_stuff), .n_timeout(n_timeout), .n_pid(n_pid),
    .n_stall(n_stall), .n_babble(n_babble), .n_badclass(n_badclass),
    .n_phy(n_phy), .n_proto(n_proto), .n_fw(n_fw)
  );

  // ---- A SECOND instance, with a different retry budget. ----
  //
  // The whole point of a parameter is that changing it changes the
  // behaviour. A module whose depth counter is two bits wide accepts
  // MAX_RETRY = 5, truncates it to 1, and gives up after one failed attempt
  // -- and every test written against the default value of 3 still passes.
  //
  // This instance is driven by exactly the same stimulus and checked at the
  // one place the budget is visible: where it gives up.
  logic [31:0] n5_xfer_fail;
  logic [7:0]  n5_depth;

  usb_error_triage #(.MAX_RETRY(5), .LAYER_ALARM(LAYER_ALARM)) dut5 (
    .clk(clk), .rst_n(rst_n),
    .attempt_valid(attempt_valid), .err_class(err_class), .eot(eot),
    .retry_depth(n5_depth), .worst_retry(), .dominant_layer(),
    .err_pulse(), .err_code(), .alarm_pulse(), .alarm_layer(),
    .n_attempt(), .n_xfer(), .n_fail(), .n_xfer_fail(n5_xfer_fail),
    .n_masked(), .n_alarm(),
    .n_crc(), .n_stuff(), .n_timeout(), .n_pid(),
    .n_stall(), .n_babble(), .n_badclass(),
    .n_phy(), .n_proto(), .n_fw()
  );

  always #5 clk = ~clk;

  // ---------------- the shadow model ----------------
  logic [7:0]  m_dep;
  logic [7:0]  m_worst;
  triage_e     m_ec;
  layer_e      m_alay;
  logic        m_er, m_al;
  logic [2:0]  m_alarm;
  int unsigned m_att, m_xfer, m_fail, m_xfail, m_mask, m_nal;
  int unsigned m_crc, m_stuff, m_tmo, m_pid, m_stall, m_babbl, m_bad;
  int unsigned m_phy, m_proto, m_fw;

  // Written from the chapter, not from the design. The map is the claim:
  // an encoding that is not an error class is attributed to NOBODY, because
  // attributing it puts a bug on somebody's desk on the strength of a value
  // nobody defined.
  // if/else rather than a chain of ternaries: a conditional expression whose
  // arms are enumeration literals needs an explicit cast in SystemVerilog,
  // and Icarus rejects it outright.
  function automatic layer_e lay(input err_class_e c);
    begin
      if (c == E_CRC || c == E_STUFF)        lay = L_PHY;
      else if (c == E_TMO || c == E_PID)     lay = L_PROTO;
      else if (c == E_STALL || c == E_BABBL) lay = L_FW;
      else                                   lay = L_NONE;
    end
  endfunction

  int errors = 0, checks = 0, steps = 0;
  int k;

  // reach: err_class (8) x the retry depth BEFORE the attempt (0..2)
  bit reach [24];
  int n_reach;

  task automatic ck(string nm, int unsigned got, int unsigned exp);
    checks++;
    if (got !== exp) begin
      errors++;
      if (errors < 25)
        $display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
                 $time, steps, nm, got, exp);
    end
  endtask

  task automatic model_step;
    layer_e l;
    begin
      m_er = 1'b0; m_ec = T_NONE; m_al = 1'b0; m_alay = L_NONE;
      l = lay(err_class);

      if (eot) begin
        // nothing
      end else if (attempt_valid) begin
        m_att = m_att + 1;
        if (err_class == E_NONE) begin
          m_xfer = m_xfer + 1;
          if (m_dep != 8'd0) begin
            // The transfer succeeded, and it only succeeded because of a
            // retry. The transfer layer records a success; this is the only
            // place the cost is visible.
            m_er = 1'b1; m_ec = T_MASKED;
            m_mask = m_mask + 1;
          end
          m_dep = 8'd0;
        end else begin
          m_fail = m_fail + 1;
          case (err_class)
            E_CRC:   m_crc   = m_crc   + 1;
            E_STUFF: m_stuff = m_stuff + 1;
            E_TMO:   m_tmo   = m_tmo   + 1;
            E_PID:   m_pid   = m_pid   + 1;
            E_STALL: m_stall = m_stall + 1;
            E_BABBL: m_babbl = m_babbl + 1;
            default: m_bad   = m_bad   + 1;
          endcase
          if (l == L_PHY)        m_phy   = m_phy   + 1;
          else if (l == L_PROTO) m_proto = m_proto + 1;
          else if (l == L_FW)    m_fw    = m_fw    + 1;

          if ((m_dep + 8'd1) >= 8'(MAX_RETRY)) begin
            m_er = 1'b1; m_ec = T_XFER_FAIL;
            m_xfail = m_xfail + 1;
            m_xfer  = m_xfer  + 1;
            m_dep   = 8'd0;
          end else begin
            m_dep = m_dep + 8'd1;
            if (m_dep > m_worst) m_worst = m_dep;
          end

          // Its own pulse, so it can coincide with T_XFER_FAIL without
          // either event overwriting the other.
          if ((l == L_PHY) && (m_phy >= LAYER_ALARM) && !m_alarm[0]) begin
            m_al = 1'b1; m_alay = L_PHY;   m_alarm[0] = 1'b1; m_nal = m_nal+1;
          end else if ((l == L_PROTO) && (m_proto >= LAYER_ALARM)
                       && !m_alarm[1]) begin
            m_al = 1'b1; m_alay = L_PROTO; m_alarm[1] = 1'b1; m_nal = m_nal+1;
          end else if ((l == L_FW) && (m_fw >= LAYER_ALARM)
                       && !m_alarm[2]) begin
            m_al = 1'b1; m_alay = L_FW;    m_alarm[2] = 1'b1; m_nal = m_nal+1;
          end
        end
      end
    end
  endtask

  // The dominant layer, from the model's own totals, with the tie-break the
  // design documents: the lowest layer index wins.
  // Verilog-2005 requires at least one input port on a function, so the
  // model's own totals are passed in rather than read from module scope.
  function automatic layer_e dom();
    begin
      if ((m_phy >= m_proto) && (m_phy >= m_fw)) dom = L_PHY;
      else if (m_proto >= m_fw)                  dom = L_PROTO;
      else                                       dom = L_FW;
    end
  endfunction

  task automatic check_out;
    begin
      ck("retry_depth",    retry_depth, m_dep);
      ck("worst_retry",    worst_retry, m_worst);
      ck("err_pulse",      err_pulse, m_er);
      ck("err_code",       err_code, m_ec);
      ck("alarm_pulse",    alarm_pulse, m_al);
      ck("alarm_layer",    alarm_layer, m_alay);
      ck("dominant_layer", dominant_layer, dom());
      ck("n_attempt",   n_attempt,   m_att);
      ck("n_xfer",      n_xfer,      m_xfer);
      ck("n_fail",      n_fail,      m_fail);
      ck("n_xfer_fail", n_xfer_fail, m_xfail);
      ck("n_masked",    n_masked,    m_mask);
      ck("n_alarm",     n_alarm,     m_nal);
      ck("n_crc",      n_crc,      m_crc);
      ck("n_stuff",    n_stuff,    m_stuff);
      ck("n_timeout",  n_timeout,  m_tmo);
      ck("n_pid",      n_pid,      m_pid);
      ck("n_stall",    n_stall,    m_stall);
      ck("n_babble",   n_babble,   m_babbl);
      ck("n_badclass", n_badclass, m_bad);
      ck("n_phy",      n_phy,      m_phy);
      ck("n_proto",    n_proto,    m_proto);
      ck("n_fw",       n_fw,       m_fw);
      // ---- the two structural invariants ----
      //
      // Every failed attempt is exactly one class; every class belongs to at
      // most one layer. A report that cannot be decomposed cannot be trusted
      // (chapter 23.4), and E_RSVD is why the LAYER sum excludes the
      // unattributed count rather than folding it into one of the three.
      ck("class sum",
         n_crc + n_stuff + n_timeout + n_pid + n_stall + n_babble + n_badclass,
         m_fail);
      ck("layer sum", n_phy + n_proto + n_fw, m_fail - m_bad);
    end
  endtask

  task automatic step;
    begin
      if (attempt_valid && !eot)
        reach[int'(err_class) * 3 + int'(m_dep)] = 1'b1;
      model_step;
      @(posedge clk);
      #1;
      steps = steps + 1;
      check_out;
    end
  endtask

  task automatic att(err_class_e c);
    begin
      attempt_valid = 1'b1; eot = 1'b0; err_class = c;
      step;
    end
  endtask

  task automatic nop;
    begin
      attempt_valid = 1'b0; eot = 1'b0;
      step;
    end
  endtask

  // Reset the DUT and the model together. Phases 4 and 5 both measure a
  // threshold crossing or a ranking, and both are meaningless on top of the
  // totals phase 1 leaves behind -- phase 1 drives every class hundreds of
  // times, so every layer alarm has already fired before either phase
  // starts. A phase that measures an accumulation needs a known accumulator.
  task automatic reset_all;
    begin
      attempt_valid = 1'b0; clear_model; eot = 1'b0;
      rst_n = 1'b0;
      @(posedge clk); #1;
      rst_n = 1'b1;
      @(negedge clk);
    end
  endtask

  task automatic clear_model;
    begin
      m_dep = 8'd0; m_worst = 8'd0; m_ec = T_NONE; m_er = 1'b0;
      m_al = 1'b0; m_alay = L_NONE; m_alarm = 3'd0;
      m_att=0; m_xfer=0; m_fail=0; m_xfail=0; m_mask=0; m_nal=0;
      m_crc=0; m_stuff=0; m_tmo=0; m_pid=0; m_stall=0; m_babbl=0; m_bad=0;
      m_phy=0; m_proto=0; m_fw=0;
    end
  endtask

  int i, a, b, c, w;
  int base_mask, base_xfail, base_att, base_xfer, base_fail, base_al;

  initial begin
    foreach (reach[q]) reach[q] = 1'b0;
    clear_model;

    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(negedge clk);

    // ================= PHASE 1 -- all 512 three-attempt sequences ========
    //
    // Each sequence is preceded by a success so that it starts at retry
    // depth 0. Every ordering of every class over the whole retry budget,
    // which is the only way to cover a rule whose meaning depends on
    // history.
    for (a = 0; a < 8; a = a + 1)
      for (b = 0; b < 8; b = b + 1)
        for (c = 0; c < 8; c = c + 1) begin
          att(E_NONE);
          att(err_class_e'(a[2:0]));
          att(err_class_e'(b[2:0]));
          att(err_class_e'(c[2:0]));
        end

    // ================= PHASE 2 -- retries MASK the error rate ============
    //
    // The headline, measured rather than asserted. 200 transfers, each of
    // which fails once and then succeeds. Every transfer succeeds, so the
    // transfer-level failure rate is EXACTLY ZERO, while one attempt in two
    // failed.
    att(E_NONE);                       // start clean
    base_att  = m_att;  base_xfer = m_xfer;
    base_fail = m_fail; base_xfail = m_xfail; base_mask = m_mask;
    for (i = 0; i < 200; i = i + 1) begin
      att(E_CRC);
      att(E_NONE);
    end
    if (m_xfail != base_xfail) begin
      errors = errors + 1;
      $display("FAIL a transfer failed when none should have");
    end
    if (m_xfer - base_xfer != 200) begin
      errors = errors + 1;
      $display("FAIL transfers %0d expected 200", m_xfer - base_xfer);
    end
    if (m_att - base_att != 400) begin
      errors = errors + 1;
      $display("FAIL attempts %0d expected 400", m_att - base_att);
    end
    if (m_fail - base_fail != 200) begin
      errors = errors + 1;
      $display("FAIL failed attempts %0d expected 200", m_fail - base_fail);
    end
    // ...and every one of those 200 transfers is reported as MASKED, which
    // is the only trace of the damage anywhere in the system.
    if (m_mask - base_mask != 200) begin
      errors = errors + 1;
      $display("FAIL masked %0d expected 200", m_mask - base_mask);
    end
    $display("PHASE2 transfer-level failures=%0d  attempt-level failures=%0d/%0d",
             m_xfail - base_xfail, m_fail - base_fail, m_att - base_att);

    // ================= PHASE 3 -- the give-up boundary ===================
    //
    // MAX_RETRY-1 failures then a success is a MASKED transfer and nothing
    // else. MAX_RETRY failures is a transfer failure and nothing else. The
    // boundary is one attempt wide and both sides of it are checked.
    att(E_NONE);
    base_xfail = m_xfail; base_mask = m_mask;
    for (i = 0; i < MAX_RETRY - 1; i = i + 1) att(E_TMO);
    if (m_xfail != base_xfail) begin
      errors = errors + 1;
      $display("FAIL gave up early");
    end
    att(E_NONE);
    if (m_mask != base_mask + 1) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY-1 failures then success was not MASKED");
    end
    base_xfail = m_xfail; base_mask = m_mask;
    for (i = 0; i < MAX_RETRY; i = i + 1) att(E_TMO);
    if (m_xfail != base_xfail + 1) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY failures did not fail the transfer");
    end
    if (m_mask != base_mask) begin
      errors = errors + 1;
      $display("FAIL a failed transfer was also reported as masked");
    end
    if (m_dep != 8'd0) begin
      errors = errors + 1;
      $display("FAIL retry depth not reset after giving up");
    end

    // ---- The parameter has to mean something. ----
    //
    // Four consecutive failures must NOT exhaust a budget of five, and the
    // fifth must. A two-bit depth counter fails the first of these, and no
    // test written against the default budget of 3 would ever notice.
    att(E_NONE);
    base_xfail = n5_xfer_fail;
    for (i = 0; i < 4; i++) att(E_TMO);
    if (n5_xfer_fail != base_xfail) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY=5 gave up after 4 attempts");
    end
    if (n5_depth != 8'd4) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY=5 depth is %0d after 4 failures, expected 4",
               n5_depth);
    end
    att(E_TMO);
    if (n5_xfer_fail != base_xfail + 1) begin
      errors = errors + 1;
      $display("FAIL MAX_RETRY=5 did not give up after 5 attempts");
    end
    att(E_NONE);

    // ================= PHASE 4 -- the layer alarm fires ONCE =============
    //
    // Enough firmware-class errors to cross LAYER_ALARM twice over. One
    // alarm, on the crossing.
    reset_all;
    base_al = m_nal;
    for (i = 0; i < 2 * LAYER_ALARM; i = i + 1) begin
      att(E_STALL);
      att(E_NONE);
    end
    if (m_nal != base_al + 1) begin
      errors = errors + 1;
      $display("FAIL firmware alarms %0d expected 1", m_nal - base_al);
    end

    // ================= PHASE 5 -- the layer total, not the largest class ==
    //
    // The case the chapter names: 40 CRC (PHY) against 30 timeouts and 30
    // bad PIDs (both PROTOCOL). The single largest CLASS is CRC. The
    // dominant LAYER is protocol, 60 to 40, and an implementation that
    // ranks classes reports the wrong team.
    //
    // Counted from a clean slate by resetting the DUT, because the earlier
    // phases have left large totals that would swamp the comparison.
    reset_all;
    for (i = 0; i < 40; i = i + 1) begin att(E_CRC); att(E_NONE); end
    for (i = 0; i < 30; i = i + 1) begin att(E_TMO); att(E_NONE); end
    for (i = 0; i < 30; i = i + 1) begin att(E_PID); att(E_NONE); end
    if (m_crc != 40 || m_tmo != 30 || m_pid != 30) begin
      errors = errors + 1;
      $display("FAIL phase 5 class counts %0d %0d %0d", m_crc, m_tmo, m_pid);
    end
    if (dominant_layer !== L_PROTO) begin
      errors = errors + 1;
      $display("FAIL dominant layer is %0d, expected PROTO (60 vs 40)",
               dominant_layer);
    end
    $display("PHASE5 largest class = CRC (%0d), dominant layer = %0d (phy=%0d proto=%0d fw=%0d)",
             m_crc, dominant_layer, m_phy, m_proto, m_fw);

    // ================= PHASE 6 -- random =================================
    //
    // The class is drawn ONCE into a variable: a chain of ternaries each
    // draw a new number, so the branches become independent events rather
    // than the nested distribution the code appears to describe.
    for (i = 0; i < 40000; i = i + 1) begin
      w = $unsigned($random) % 1000;
      if (w < 30) begin
        nop;
      end else if (w < 700) begin
        att(E_NONE);
      end else if (w < 810) att(E_CRC);
      else if (w < 860) att(E_STUFF);
      else if (w < 920) att(E_TMO);
      else if (w < 960) att(E_PID);
      else if (w < 980) att(E_STALL);
      else if (w < 995) att(E_BABBL);
      else att(E_RSVD);
    end

    // ================= the exhaustiveness proof ==========================
    n_reach = 0;
    for (k = 0; k < 24; k = k + 1) n_reach = n_reach + reach[k];
    if (n_reach != 24) begin
      errors = errors + 1;
      $display("FAIL class x depth reach %0d/24", n_reach);
      for (k = 0; k < 24; k = k + 1)
        if (!reach[k]) $display("  unreached class=%0d depth=%0d", k/3, k%3);
    end

    $display("steps=%0d checks=%0d reach=%0d/24 errors=%0d",
             steps, checks, n_reach, errors);
    $display("attempts=%0d transfers=%0d failed_attempts=%0d xfer_fail=%0d masked=%0d",
             n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked);
    $display("crc=%0d stuff=%0d timeout=%0d pid=%0d stall=%0d babble=%0d badclass=%0d",
             n_crc, n_stuff, n_timeout, n_pid, n_stall, n_babble, n_badclass);
    $display("phy=%0d proto=%0d fw=%0d alarms=%0d worst_retry=%0d",
             n_phy, n_proto, n_fw, n_alarm, worst_retry);
    $display("%0s: %0d errors in %0d checks",
             (errors == 0) ? "PASS" : "FAIL", errors, checks);
    $finish;
  end
endmodule

VHDL-2008 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Testbench for usb_error_triage (VHDL-2008).
--
-- The oracle is a shadow model held in process variables and written from
-- the chapter's rules rather than from the RTL, re-derived every cycle and
-- compared against every output.
--
-- THE EXHAUSTIVE CLAIM IS OVER SEQUENCES, NOT OVER VALUES
--
-- A retry budget is a property of a SEQUENCE: the third consecutive failure
-- ends a transfer and the first does not, so sweeping the eight err_class
-- encodings one at a time proves nothing about the budget at all. Phase 1
-- drives all 8^3 = 512 possible three-attempt sequences, each preceded by a
-- success so that it starts from a known retry depth.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_triage_pkg.all;

entity tb_et2_vhdl is
end entity;

architecture sim of tb_et2_vhdl is
  constant MAX_RETRY   : integer := 3;
  constant LAYER_ALARM : integer := 32;

  signal clk           : std_logic := '0';
  signal rst_n         : std_logic := '0';
  signal attempt_valid : std_logic := '0';
  signal err_class     : std_logic_vector(2 downto 0) := E_NONE;
  signal eot           : std_logic := '0';

  signal retry_depth    : std_logic_vector(7 downto 0);
  signal worst_retry    : std_logic_vector(7 downto 0);
  signal dominant_layer : std_logic_vector(1 downto 0);
  signal err_pulse      : std_logic;
  signal err_code       : std_logic_vector(1 downto 0);
  signal alarm_pulse    : std_logic;
  signal alarm_layer    : std_logic_vector(1 downto 0);

  signal n_attempt, n_xfer, n_fail             : unsigned(31 downto 0);
  signal n_xfer_fail, n_masked, n_alarm        : unsigned(31 downto 0);
  signal n_crc, n_stuff, n_timeout, n_pid      : unsigned(31 downto 0);
  signal n_stall, n_babble, n_badclass         : unsigned(31 downto 0);
  signal n_phy, n_proto, n_fw                  : unsigned(31 downto 0);

  -- ---- A SECOND instance, with a different retry budget. ----
  --
  -- The whole point of a generic is that changing it changes the behaviour.
  -- An entity whose depth counter is two bits wide accepts MAX_RETRY = 5,
  -- truncates it to 1, and gives up after one failed attempt -- and every
  -- test written against the default value of 3 still passes.
  --
  -- This instance is driven by exactly the same stimulus and checked at the
  -- one place the budget is visible: where it gives up.
  signal n5_xfer_fail : unsigned(31 downto 0);
  signal n5_depth     : std_logic_vector(7 downto 0);

  signal done : boolean := false;

  type int_array is array (natural range <>) of integer;
begin
  clk <= not clk after 5 ns when not done else '0';

  dut : entity work.usb_error_triage
    generic map (MAX_RETRY => MAX_RETRY, LAYER_ALARM => LAYER_ALARM)
    port map (
      clk => clk, rst_n => rst_n,
      attempt_valid => attempt_valid, err_class => err_class, eot => eot,
      retry_depth => retry_depth, worst_retry => worst_retry,
      dominant_layer => dominant_layer,
      err_pulse => err_pulse, err_code => err_code,
      alarm_pulse => alarm_pulse, alarm_layer => alarm_layer,
      n_attempt => n_attempt, n_xfer => n_xfer, n_fail => n_fail,
      n_xfer_fail => n_xfer_fail, n_masked => n_masked, n_alarm => n_alarm,
      n_crc => n_crc, n_stuff => n_stuff, n_timeout => n_timeout,
      n_pid => n_pid, n_stall => n_stall, n_babble => n_babble,
      n_badclass => n_badclass,
      n_phy => n_phy, n_proto => n_proto, n_fw => n_fw
    );

  dut5 : entity work.usb_error_triage
    generic map (MAX_RETRY => 5, LAYER_ALARM => LAYER_ALARM)
    port map (
      clk => clk, rst_n => rst_n,
      attempt_valid => attempt_valid, err_class => err_class, eot => eot,
      retry_depth => n5_depth, worst_retry => open,
      dominant_layer => open, err_pulse => open, err_code => open,
      alarm_pulse => open, alarm_layer => open,
      n_attempt => open, n_xfer => open, n_fail => open,
      n_xfer_fail => n5_xfer_fail, n_masked => open, n_alarm => open,
      n_crc => open, n_stuff => open, n_timeout => open, n_pid => open,
      n_stall => open, n_babble => open, n_badclass => open,
      n_phy => open, n_proto => open, n_fw => open
    );

  stim : process
    -- ---------------- the shadow model ----------------
    variable m_dep   : unsigned(7 downto 0) := (others => '0');
    variable m_worst : unsigned(7 downto 0) := (others => '0');
    variable m_ec    : std_logic_vector(1 downto 0) := T_NONE;
    variable m_er    : std_logic := '0';
    variable m_al    : std_logic := '0';
    variable m_alay  : std_logic_vector(1 downto 0) := L_NONE;
    variable m_alarm : std_logic_vector(2 downto 0) := (others => '0');
    variable c_att, c_xfer, c_fail, c_xfail, c_mask, c_nal : integer := 0;
    variable c_crc, c_stuff, c_tmo, c_pid                  : integer := 0;
    variable c_stall, c_babbl, c_bad                       : integer := 0;
    variable c_phy, c_proto, c_fw                          : integer := 0;

    variable errors, checks, steps : integer := 0;
    variable reach   : int_array(0 to 23) := (others => 0);
    variable n_reach : integer := 0;
    variable i_v, w_v : integer := 0;
    variable base_mask, base_xfail, base_att : integer := 0;
    variable base_xfer, base_fail, base_al   : integer := 0;
    variable ln : line;

    -- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
    variable lfsr : unsigned(31 downto 0) := x"A5C3F00D";

    impure function rnd32 return unsigned is
    begin
      lfsr := lfsr(30 downto 0) &
              (lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
      return lfsr;
    end function;

    -- Only the low 30 bits are converted: a full 32-bit unsigned does not
    -- fit in VHDL's INTEGER, and to_integer aborts the run rather than
    -- wrapping.
    impure function rnd_nat return integer is
      variable u : unsigned(31 downto 0);
    begin
      u := rnd32;
      return to_integer(u(29 downto 0));
    end function;

    procedure ck (nm : string; got, exp : integer) is
    begin
      checks := checks + 1;
      if got /= exp then
        errors := errors + 1;
        if errors < 25 then
          write(ln, string'("FAIL step=") & integer'image(steps) &
                    " " & nm & " got=" & integer'image(got) &
                    " exp=" & integer'image(exp));
          writeline(output, ln);
        end if;
      end if;
    end procedure;

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

    procedure model_step is
      variable l : std_logic_vector(1 downto 0);
    begin
      m_er := '0'; m_ec := T_NONE; m_al := '0'; m_alay := L_NONE;
      l := layer_of(err_class);

      if eot = '1' then
        null;
      elsif attempt_valid = '1' then
        c_att := c_att + 1;
        if err_class = E_NONE then
          c_xfer := c_xfer + 1;
          if m_dep /= x"00" then
            -- The transfer succeeded, and only because of a retry. The
            -- transfer layer records a success; this is the only place the
            -- cost is visible.
            m_er := '1'; m_ec := T_MASKED;
            c_mask := c_mask + 1;
          end if;
          m_dep := x"00";
        else
          c_fail := c_fail + 1;
          case err_class is
            when E_CRC   => c_crc   := c_crc   + 1;
            when E_STUFF => c_stuff := c_stuff + 1;
            when E_TMO   => c_tmo   := c_tmo   + 1;
            when E_PID   => c_pid   := c_pid   + 1;
            when E_STALL => c_stall := c_stall + 1;
            when E_BABBL => c_babbl := c_babbl + 1;
            when others  => c_bad   := c_bad   + 1;
          end case;
          if l = L_PHY then      c_phy   := c_phy   + 1;
          elsif l = L_PROTO then c_proto := c_proto + 1;
          elsif l = L_FW then    c_fw    := c_fw    + 1;
          end if;

          if (m_dep + 1) >= to_unsigned(MAX_RETRY, 8) then
            m_er := '1'; m_ec := T_XFER_FAIL;
            c_xfail := c_xfail + 1;
            c_xfer  := c_xfer  + 1;
            m_dep   := x"00";
          else
            m_dep := m_dep + 1;
            if m_dep > m_worst then
              m_worst := m_dep;
            end if;
          end if;

          -- Its own pulse, so it can coincide with T_XFER_FAIL without
          -- either event overwriting the other.
          if (l = L_PHY) and (c_phy >= LAYER_ALARM) and m_alarm(0) = '0' then
            m_al := '1'; m_alay := L_PHY;   m_alarm(0) := '1';
            c_nal := c_nal + 1;
          elsif (l = L_PROTO) and (c_proto >= LAYER_ALARM)
                and m_alarm(1) = '0' then
            m_al := '1'; m_alay := L_PROTO; m_alarm(1) := '1';
            c_nal := c_nal + 1;
          elsif (l = L_FW) and (c_fw >= LAYER_ALARM)
                and m_alarm(2) = '0' then
            m_al := '1'; m_alay := L_FW;    m_alarm(2) := '1';
            c_nal := c_nal + 1;
          end if;
        end if;
      end if;
    end procedure;

    -- The dominant layer from the model's own totals, with the tie-break the
    -- design documents: the lowest layer index wins.
    impure function dom return integer is
    begin
      if (c_phy >= c_proto) and (c_phy >= c_fw) then return 0;
      elsif c_proto >= c_fw then return 1;
      else return 2;
      end if;
    end function;

    procedure check_out is
    begin
      ck("retry_depth", to_integer(unsigned(retry_depth)), to_integer(m_dep));
      ck("worst_retry", to_integer(unsigned(worst_retry)), to_integer(m_worst));
      ck("err_pulse",   sl2i(err_pulse), sl2i(m_er));
      ck("err_code",    to_integer(unsigned(err_code)),
                        to_integer(unsigned(m_ec)));
      ck("alarm_pulse", sl2i(alarm_pulse), sl2i(m_al));
      ck("alarm_layer", to_integer(unsigned(alarm_layer)),
                        to_integer(unsigned(m_alay)));
      ck("dominant_layer", to_integer(unsigned(dominant_layer)), dom);
      ck("n_attempt",   to_integer(n_attempt),   c_att);
      ck("n_xfer",      to_integer(n_xfer),      c_xfer);
      ck("n_fail",      to_integer(n_fail),      c_fail);
      ck("n_xfer_fail", to_integer(n_xfer_fail), c_xfail);
      ck("n_masked",    to_integer(n_masked),    c_mask);
      ck("n_alarm",     to_integer(n_alarm),     c_nal);
      ck("n_crc",       to_integer(n_crc),       c_crc);
      ck("n_stuff",     to_integer(n_stuff),     c_stuff);
      ck("n_timeout",   to_integer(n_timeout),   c_tmo);
      ck("n_pid",       to_integer(n_pid),       c_pid);
      ck("n_stall",     to_integer(n_stall),     c_stall);
      ck("n_babble",    to_integer(n_babble),    c_babbl);
      ck("n_badclass",  to_integer(n_badclass),  c_bad);
      ck("n_phy",       to_integer(n_phy),       c_phy);
      ck("n_proto",     to_integer(n_proto),     c_proto);
      ck("n_fw",        to_integer(n_fw),        c_fw);
      -- ---- the two structural invariants ----
      --
      -- Every failed attempt is exactly one class; every class belongs to at
      -- most one layer. A report that cannot be decomposed cannot be trusted
      -- (chapter 23.4), and E_RSVD is why the LAYER sum excludes the
      -- unattributed count rather than folding it into one of the three.
      ck("class sum",
         to_integer(n_crc) + to_integer(n_stuff) + to_integer(n_timeout) +
         to_integer(n_pid) + to_integer(n_stall) + to_integer(n_babble) +
         to_integer(n_badclass), c_fail);
      ck("layer sum",
         to_integer(n_phy) + to_integer(n_proto) + to_integer(n_fw),
         c_fail - c_bad);
    end procedure;

    procedure step_one is
      variable idx : integer;
    begin
      if attempt_valid = '1' and eot = '0' then
        idx := to_integer(unsigned(err_class)) * 3 + to_integer(m_dep);
        reach(idx) := 1;
      end if;
      model_step;
      wait until rising_edge(clk);
      wait for 1 ns;
      steps := steps + 1;
      check_out;
    end procedure;

    procedure att (c : std_logic_vector(2 downto 0)) is
    begin
      attempt_valid <= '1'; eot <= '0'; err_class <= c;
      wait for 0 ns;
      step_one;
    end procedure;

    procedure nop is
    begin
      attempt_valid <= '0'; eot <= '0';
      wait for 0 ns;
      step_one;
    end procedure;

    procedure clear_model is
    begin
      m_dep := x"00"; m_worst := (others => '0'); m_ec := T_NONE;
      m_er := '0'; m_al := '0'; m_alay := L_NONE;
      m_alarm := (others => '0');
      c_att := 0; c_xfer := 0; c_fail := 0; c_xfail := 0; c_mask := 0;
      c_nal := 0;
      c_crc := 0; c_stuff := 0; c_tmo := 0; c_pid := 0;
      c_stall := 0; c_babbl := 0; c_bad := 0;
      c_phy := 0; c_proto := 0; c_fw := 0;
    end procedure;

    -- Reset the DUT and the model together. Phases 4 and 5 both measure a
    -- threshold crossing or a ranking, and both are meaningless on top of
    -- the totals phase 1 leaves behind -- phase 1 drives every class
    -- hundreds of times, so every layer alarm has already fired before
    -- either phase starts. A phase that measures an accumulation needs a
    -- known accumulator.
    procedure reset_all is
    begin
      attempt_valid <= '0'; eot <= '0';
      clear_model;
      rst_n <= '0';
      wait until rising_edge(clk);
      wait for 1 ns;
      rst_n <= '1';
      wait for 1 ns;
    end procedure;
  begin
    wait until rising_edge(clk);
    wait until rising_edge(clk);
    wait until rising_edge(clk);
    rst_n <= '1';
    wait for 1 ns;

    -- ================= PHASE 1 -- all 512 three-attempt sequences ========
    --
    -- Each sequence is preceded by a success so that it starts at retry
    -- depth 0. Every ordering of every class over the whole retry budget,
    -- which is the only way to cover a rule whose meaning depends on
    -- history.
    for a in 0 to 7 loop
      for b in 0 to 7 loop
        for c in 0 to 7 loop
          att(E_NONE);
          att(std_logic_vector(to_unsigned(a, 3)));
          att(std_logic_vector(to_unsigned(b, 3)));
          att(std_logic_vector(to_unsigned(c, 3)));
        end loop;
      end loop;
    end loop;

    -- ================= PHASE 2 -- retries MASK the error rate ============
    --
    -- The headline, measured rather than asserted. 200 transfers, each of
    -- which fails once and then succeeds. Every transfer succeeds, so the
    -- transfer-level failure rate is EXACTLY ZERO, while one attempt in two
    -- failed.
    att(E_NONE);
    base_att := c_att; base_xfer := c_xfer;
    base_fail := c_fail; base_xfail := c_xfail; base_mask := c_mask;
    for i in 0 to 199 loop
      att(E_CRC);
      att(E_NONE);
    end loop;
    if c_xfail /= base_xfail then
      errors := errors + 1;
      write(ln, string'("FAIL a transfer failed when none should have"));
      writeline(output, ln);
    end if;
    if c_xfer - base_xfer /= 200 then
      errors := errors + 1;
      write(ln, string'("FAIL transfers ") & integer'image(c_xfer - base_xfer)
                & " expected 200");
      writeline(output, ln);
    end if;
    if c_att - base_att /= 400 then
      errors := errors + 1;
      write(ln, string'("FAIL attempts ") & integer'image(c_att - base_att)
                & " expected 400");
      writeline(output, ln);
    end if;
    if c_fail - base_fail /= 200 then
      errors := errors + 1;
      write(ln, string'("FAIL failed attempts ")
                & integer'image(c_fail - base_fail) & " expected 200");
      writeline(output, ln);
    end if;
    -- ...and every one of those 200 transfers is reported as MASKED, which
    -- is the only trace of the damage anywhere in the system.
    if c_mask - base_mask /= 200 then
      errors := errors + 1;
      write(ln, string'("FAIL masked ") & integer'image(c_mask - base_mask)
                & " expected 200");
      writeline(output, ln);
    end if;
    write(ln, string'("PHASE2 transfer-level failures=")
              & integer'image(c_xfail - base_xfail)
              & "  attempt-level failures="
              & integer'image(c_fail - base_fail) & "/"
              & integer'image(c_att - base_att));
    writeline(output, ln);

    -- ================= PHASE 3 -- the give-up boundary ===================
    --
    -- MAX_RETRY-1 failures then a success is a MASKED transfer and nothing
    -- else. MAX_RETRY failures is a transfer failure and nothing else. The
    -- boundary is one attempt wide and both sides of it are checked.
    att(E_NONE);
    base_xfail := c_xfail; base_mask := c_mask;
    for i in 0 to MAX_RETRY-2 loop att(E_TMO); end loop;
    if c_xfail /= base_xfail then
      errors := errors + 1;
      write(ln, string'("FAIL gave up early")); writeline(output, ln);
    end if;
    att(E_NONE);
    if c_mask /= base_mask + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL MAX_RETRY-1 failures then success not MASKED"));
      writeline(output, ln);
    end if;
    base_xfail := c_xfail; base_mask := c_mask;
    for i in 0 to MAX_RETRY-1 loop att(E_TMO); end loop;
    if c_xfail /= base_xfail + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL MAX_RETRY failures did not fail the transfer"));
      writeline(output, ln);
    end if;
    if c_mask /= base_mask then
      errors := errors + 1;
      write(ln, string'("FAIL a failed transfer was also reported as masked"));
      writeline(output, ln);
    end if;
    if m_dep /= x"00" then
      errors := errors + 1;
      write(ln, string'("FAIL retry depth not reset after giving up"));
      writeline(output, ln);
    end if;

    -- ---- The generic has to mean something. ----
    --
    -- Four consecutive failures must NOT exhaust a budget of five, and the
    -- fifth must. A two-bit depth counter fails the first of these, and no
    -- test written against the default budget of 3 would ever notice.
    att(E_NONE);
    base_xfail := to_integer(n5_xfer_fail);
    for i in 0 to 3 loop att(E_TMO); end loop;
    if to_integer(n5_xfer_fail) /= base_xfail then
      errors := errors + 1;
      write(ln, string'("FAIL MAX_RETRY=5 gave up after 4 attempts"));
      writeline(output, ln);
    end if;
    if to_integer(unsigned(n5_depth)) /= 4 then
      errors := errors + 1;
      write(ln, string'("FAIL MAX_RETRY=5 depth is ")
                & integer'image(to_integer(unsigned(n5_depth)))
                & " after 4 failures, expected 4");
      writeline(output, ln);
    end if;
    att(E_TMO);
    if to_integer(n5_xfer_fail) /= base_xfail + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL MAX_RETRY=5 did not give up after 5 attempts"));
      writeline(output, ln);
    end if;
    att(E_NONE);

    -- ================= PHASE 4 -- the layer alarm fires ONCE =============
    reset_all;
    base_al := c_nal;
    for i in 0 to 2*LAYER_ALARM-1 loop
      att(E_STALL);
      att(E_NONE);
    end loop;
    if c_nal /= base_al + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL firmware alarms ")
                & integer'image(c_nal - base_al) & " expected 1");
      writeline(output, ln);
    end if;

    -- ================= PHASE 5 -- the layer total, not the largest class ==
    --
    -- 40 CRC (PHY) against 30 timeouts and 30 bad PIDs (both PROTOCOL). The
    -- single largest CLASS is CRC. The dominant LAYER is protocol, 60 to 40,
    -- and an implementation that ranks classes reports the wrong team.
    reset_all;
    for i in 0 to 39 loop att(E_CRC);  att(E_NONE); end loop;
    for i in 0 to 29 loop att(E_TMO);  att(E_NONE); end loop;
    for i in 0 to 29 loop att(E_PID);  att(E_NONE); end loop;
    if c_crc /= 40 or c_tmo /= 30 or c_pid /= 30 then
      errors := errors + 1;
      write(ln, string'("FAIL phase 5 class counts"));
      writeline(output, ln);
    end if;
    if dominant_layer /= L_PROTO then
      errors := errors + 1;
      write(ln, string'("FAIL dominant layer is ")
                & integer'image(to_integer(unsigned(dominant_layer)))
                & ", expected PROTO (60 vs 40)");
      writeline(output, ln);
    end if;
    write(ln, string'("PHASE5 largest class = CRC (") & integer'image(c_crc)
              & "), dominant layer = "
              & integer'image(to_integer(unsigned(dominant_layer)))
              & " (phy=" & integer'image(c_phy)
              & " proto=" & integer'image(c_proto)
              & " fw=" & integer'image(c_fw) & ")");
    writeline(output, ln);

    -- ================= PHASE 6 -- random =================================
    for i in 0 to 39999 loop
      w_v := rnd_nat mod 1000;
      if w_v < 30 then       nop;
      elsif w_v < 700 then   att(E_NONE);
      elsif w_v < 810 then   att(E_CRC);
      elsif w_v < 860 then   att(E_STUFF);
      elsif w_v < 920 then   att(E_TMO);
      elsif w_v < 960 then   att(E_PID);
      elsif w_v < 980 then   att(E_STALL);
      elsif w_v < 995 then   att(E_BABBL);
      else                   att(E_RSVD);
      end if;
    end loop;

    -- ================= the exhaustiveness proof ==========================
    n_reach := 0;
    for k in 0 to 23 loop n_reach := n_reach + reach(k); end loop;
    if n_reach /= 24 then
      errors := errors + 1;
      write(ln, string'("FAIL class x depth reach ") & integer'image(n_reach)
                & "/24");
      writeline(output, ln);
      for k in 0 to 23 loop
        if reach(k) = 0 then
          write(ln, string'("  unreached class=") & integer'image(k/3)
                    & " depth=" & integer'image(k mod 3));
          writeline(output, ln);
        end if;
      end loop;
    end if;

    write(ln, string'("steps=") & integer'image(steps)
              & " checks=" & integer'image(checks)
              & " reach=" & integer'image(n_reach) & "/24"
              & " errors=" & integer'image(errors));
    writeline(output, ln);
    write(ln, string'("attempts=") & integer'image(to_integer(n_attempt))
              & " transfers=" & integer'image(to_integer(n_xfer))
              & " failed_attempts=" & integer'image(to_integer(n_fail))
              & " xfer_fail=" & integer'image(to_integer(n_xfer_fail))
              & " masked=" & integer'image(to_integer(n_masked)));
    writeline(output, ln);
    write(ln, string'("crc=") & integer'image(to_integer(n_crc))
              & " stuff=" & integer'image(to_integer(n_stuff))
              & " timeout=" & integer'image(to_integer(n_timeout))
              & " pid=" & integer'image(to_integer(n_pid))
              & " stall=" & integer'image(to_integer(n_stall))
              & " babble=" & integer'image(to_integer(n_babble))
              & " badclass=" & integer'image(to_integer(n_badclass)));
    writeline(output, ln);
    write(ln, string'("phy=") & integer'image(to_integer(n_phy))
              & " proto=" & integer'image(to_integer(n_proto))
              & " fw=" & integer'image(to_integer(n_fw))
              & " alarms=" & integer'image(to_integer(n_alarm))
              & " worst_retry="
              & integer'image(to_integer(unsigned(worst_retry))));
    writeline(output, ln);
    if errors = 0 then
      write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
                & " checks");
    else
      write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
                integer'image(checks) & " checks");
    end if;
    writeline(output, ln);
    done <= true;
    wait;
  end process;
end architecture;

14. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
class × retry depth reached24 / 2424 / 2424 / 24
three-attempt sequences512 / 512512 / 512512 / 512
Steps427914279142791
Checks executed106977510697751069775
attempts390103901038956
transfers277742777428636
failed attempts120121201212034
failed transfers7767761714
masked transfers783778375196
CRC / bit-stuff4345 / 19004345 / 19004407 / 2000
timeout / bad PID2434 / 17272434 / 17272410 / 1651
STALL / babble811 / 588811 / 588779 / 588
undefined encoding207207199
PHY / PROTOCOL / FIRMWARE6245 / 4161 / 13996245 / 4161 / 13996407 / 4061 / 1367
layer alarms333
ResultPASSPASSPASS

The two rows in bold together are the chapter. 12012 failed attempts, 776 failed transfers: fifteen out of every sixteen failures never reached the layer where anybody was counting.

15. Mutation Testing

#MutationVerilogSysVerVHDL
R5every error attributed to the PHY213264213264212898
R3the retry depth is not reset by a success198997198997200316
R4the give-up test is > instead of >=174853174853180742
R2a transfer that succeeded on a retry is not reported594215942154139
R6the alarm fires on the state, not the crossing558725587256196
R1failures counted per transfer, not per attempt427844278442784
R7the dominant layer ranks the largest class670670390
—unmutated baseline000

All seven die in all three languages.

R1 scores 42784 in all three columns, and that number is not a coincidence. The run is 42791 steps long. From the first failed attempt onward, n_fail is permanently wrong, so it fails its check on every remaining cycle — 42791 minus the seven steps before the first failure. A counter that diverges and never resynchronises is caught on every subsequent step regardless of what the stimulus does, which is why the three columns agree exactly despite two different random streams.

R7 is the low one, at 670, and it deserves its place anyway. dominant_layer differs between the two ranking methods only when the largest single class belongs to a layer that is not the largest in total — an ambiguous case that random traffic produces rarely and that phase 5 constructs deliberately. A mutation with a small score is not a weak mutation; it is a mutation whose opportunity is rare, and the only thing that distinguishes "rare opportunity" from "nearly escaped" is whether the directed phases catch it.

Directed against random

#All phasesDirected onlyRandom
R142784278440000
R259421394755474
R319899712907186090
R417485310399164454
R521326412938200326
R655872346252410
R767041629

Every mutation is killed by directed stimulus alone, R7 included. Phase 1's 512 sequences and phase 5's constructed 40/30/30 do the work; the random phase adds volume and confirms nothing new, which is exactly the relationship those two kinds of stimulus should have.

16. Debugging Walkthrough: The Cable That Was Fine

The report. An industrial data logger drops its USB link roughly once a day. Field engineering has replaced the cable twice and the connector once. The customer's own monitoring shows a transfer success rate of 99.997%.

Step 1 — ask for the attempt-level number. It does not exist; the driver reports completions. Instrument at the host controller instead and count attempts. The attempt-level failure rate is 4.1%.

Step 2 — so the link is losing one transaction in twenty-four, continuously. The 99.997% figure was never wrong. It was answering a different question, and answering it accurately.

Step 3 — which layer? The distribution: 61% timeout, 22% bad PID, 14% CRC, 3% everything else. Ranked by class the largest single entry is timeout, which is a protocol class. Summed by layer: 83% PROTOCOL, 14% PHY. Both methods agree here, which is a relief and not a rule.

Step 4 — so it is not the cable. It was never the cable. A PHY problem shows up as CRC and bit-stuff errors, because that is what a damaged signal does. Timeouts and bad PIDs mean the transaction did not happen or came back malformed at the framing level, which is the controller or the schedule.

Step 5 — why once a day? The retry budget absorbs a 4.1% attempt failure rate almost perfectly: three independent failures in a row happens about once in 14,000 transfers. At the logger's transfer rate that is roughly once every twenty hours.

Step 6 — and why replacing the cable appeared to help. It did not. The failure interval is a tail event with a wide spread; "we changed something and it did not fail for two days" is the null hypothesis, not evidence.

The fix. A scheduling bug in the host controller driver under a specific interrupt load. Four hours to find once the attempt-level distribution existed; nine months and three cables before that.

17. UVM: Triage as an Analysis Component

Triage is not a scoreboard. A scoreboard answers did the right data arrive; triage answers what did it cost and whose fault is it, and those are different components with different lifetimes.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// An ATTEMPT, not a transfer. The distinction is the chapter: a sequence
// item that models a transfer has already thrown away the thing worth
// measuring, and no amount of analysis downstream gets it back.
class usb_attempt_item extends uvm_sequence_item;
  `uvm_object_utils(usb_attempt_item)

  rand err_class_e err_class;
  rand int unsigned xfer_id;    // which transfer this attempt belongs to

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

  // A realistic mix. Note that E_NONE dominates and that E_RSVD is
  // generated at all: an encoding that "cannot happen" is exactly the one
  // whose handling nobody has tested.
  constraint c_mix {
    err_class dist {
      E_NONE  := 670, E_CRC   := 110, E_STUFF := 50,
      E_TMO   := 60,  E_PID   := 40,  E_STALL := 20,
      E_BABBL := 15,  E_RSVD  := 5
    };
  }
endclass


// ---------------------------------------------------------------------
// The triage subscriber. Per-transfer retry state in an associative array
// keyed by transfer id, so several transfers can be in flight without
// their retry budgets running into each other -- the same structural
// argument as chapter 25.3's per-endpoint counters.
// ---------------------------------------------------------------------
class usb_triage_sub extends uvm_subscriber #(usb_attempt_item);
  `uvm_component_utils(usb_triage_sub)

  int unsigned MAX_RETRY   = 3;
  int unsigned LAYER_ALARM = 32;

  int unsigned depth [int];          // retry depth, per transfer in flight

  // BOTH denominators. The whole point is that they can be divided.
  int unsigned n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked;

  int unsigned n_class [err_class_e];
  int unsigned n_layer [layer_e];
  bit          alarmed [layer_e];

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

  // The map. A function, not a table, so the reason each class belongs
  // where it does is next to the assignment -- and E_RSVD lands on L_NONE
  // on purpose.
  function layer_e layer_of(err_class_e c);
    case (c)
      E_CRC, E_STUFF:   return L_PHY;
      E_TMO, E_PID:     return L_PROTO;
      E_STALL, E_BABBL: return L_FW;
      default:          return L_NONE;   // E_NONE and E_RSVD
    endcase
  endfunction

  function void write(usb_attempt_item t);
    layer_e l = layer_of(t.err_class);
    n_attempt++;
    if (!depth.exists(t.xfer_id)) depth[t.xfer_id] = 0;

    if (t.err_class == E_NONE) begin
      n_xfer++;
      if (depth[t.xfer_id] != 0) begin
        // A SUCCESS, reported. This is the only trace anywhere in the
        // system that the transfer cost more than one attempt, and it is
        // the only signal that moves before the failures start.
        `uvm_warning("TRIAGE/MASKED",
          $sformatf("xfer %0d succeeded on attempt %0d of %0d",
                    t.xfer_id, depth[t.xfer_id] + 1, MAX_RETRY))
        n_masked++;
      end
      depth.delete(t.xfer_id);
      return;
    end

    // A failed ATTEMPT. Counted here, in the right denominator.
    n_fail++;
    n_class[t.err_class]++;
    if (l != L_NONE) n_layer[l]++;
    // L_NONE is counted as a failure and attributed to NOBODY: putting an
    // undefined encoding on a team's desk costs them a week proving a
    // negative.

    if (depth[t.xfer_id] + 1 >= MAX_RETRY) begin
      `uvm_error("TRIAGE/XFER_FAIL",
        $sformatf("xfer %0d exhausted %0d attempts", t.xfer_id, MAX_RETRY))
      n_xfer_fail++;
      n_xfer++;
      depth.delete(t.xfer_id);
    end else begin
      depth[t.xfer_id]++;
    end

    // A separate report, because the attempt that exhausts a budget is
    // often also the attempt that crosses a threshold, and a shared code
    // field would deliver exactly one of the two.
    if (l != L_NONE && n_layer[l] >= LAYER_ALARM && !alarmed[l]) begin
      `uvm_error("TRIAGE/LAYER",
        $sformatf("%s layer has passed %0d errors", l.name(), LAYER_ALARM))
      alarmed[l] = 1;
    end
  endfunction

  // ---- THE CHECK THAT THE WHOLE COMPONENT EXISTS FOR ----
  //
  // Not "were there errors". A run with errors may be fine and a run
  // without visible failures may be a link on the edge of collapse. The
  // question is what the ATTEMPT-level rate was, and whether the retry
  // mechanism was doing all the work.
  function void report_phase(uvm_phase phase);
    real attempt_rate, xfer_rate;
    layer_e dom = L_NONE;
    int unsigned best = 0;
    super.report_phase(phase);

    attempt_rate = n_attempt ? (100.0 * n_fail) / n_attempt : 0.0;
    xfer_rate    = n_xfer    ? (100.0 * n_xfer_fail) / n_xfer : 0.0;

    // The dominant layer is the largest layer TOTAL. Forty CRC against
    // thirty timeouts and thirty bad PIDs is a PROTOCOL problem, and
    // ranking classes reports the PHY.
    foreach (n_layer[k])
      if (k != L_NONE && n_layer[k] > best) begin best = n_layer[k]; dom = k; end

    `uvm_info("TRIAGE",
      $sformatf("attempt-level %0.3f%% (%0d/%0d) | transfer-level %0.3f%% (%0d/%0d) | masked %0d | dominant %s",
                attempt_rate, n_fail, n_attempt,
                xfer_rate, n_xfer_fail, n_xfer,
                n_masked, dom.name()),
      UVM_LOW)

    // A link whose transfer-level rate is clean and whose attempt-level
    // rate is not has no margin left. It is not a passing result; it is a
    // failing result that the retry mechanism is currently concealing.
    if (xfer_rate < 0.01 && attempt_rate > 1.0)
      `uvm_error("TRIAGE/NO_MARGIN",
        $sformatf("transfer layer reports %0.3f%% while %0.2f%% of attempts failed — the budget is absorbing all of it",
                  xfer_rate, attempt_rate))
  endfunction
endclass

18. Common Misconceptions

"Our transfer success rate is 99.99%, so the link is healthy." It says nothing about the link. It says the retry budget has not been exceeded.

"Error rate is error rate; the denominator is a detail." The two denominators differ by the retry factor, which is the quantity being measured.

"A retry that worked is not a problem." It is one attempt from being a visible failure, and it is the only warning you get.

"Report every retry." Once per transfer, not once per attempt — or you rebuild 25.3's flood.

"The biggest error class tells you the layer." Forty CRC against thirty timeouts and thirty bad PIDs is a protocol problem, and the biggest class is CRC.

"An undefined encoding should be filed under the closest layer." It should be filed under no layer. Guessing costs a team a week.

"A shared error-code field is fine; the events don't overlap." That is a claim, and the cycle where it is false is usually the cycle you most needed both reports.

"CRC errors mean a bad cable." They mean the signal was corrupted. A bad cable is one cause; so is a power rail, a connector, and crosstalk from something else entirely.

"A mutation with a low score is a weak mutation." R7 scores 670 because its opportunity is rare, not because the check is weak. What matters is whether directed stimulus kills it.

19. Exercises

1. A link has an independent 4% per-attempt failure rate and a retry budget of 3. Compute the transfer-level failure rate, then recompute it for 8% and say what that implies about monitoring thresholds.

2. R1 scores exactly 42784 in all three languages against a 42791-step run. Explain the seven, and say what property of the mutation makes the three columns agree despite two different random streams.

3. The VHDL run has 12034 failed attempts and 1714 failed transfers; the Verilog run has 12012 and 776. Both are correct. Explain the factor of two in terms of the time distribution of failures rather than their rate.

4. Construct an error distribution in which ranking by class and summing by layer disagree, where the disagreement cannot be resolved by adding a fourth class to either layer.

5. Phase 1 drives 512 three-attempt sequences. Show that 512 is exactly right for MAX_RETRY = 3, and give the count for MAX_RETRY = 4.

6. The alarm has its own pulse. Construct the attempt sequence in which a shared err_code field would delete the transfer-failure report, and the one in which it would delete the alarm.

7. The retry depth was originally two bits wide. Write the smallest test that catches that, and explain why every test in phases 1 to 6 passes against it.

8. Add a fourth layer for host software errors. Which existing classes move, which do not, and what does the answer tell you about whether the class-to-layer map belongs in this block at all?

20. Summary

IdeaWhy it matters
A transfer is a sequence of attemptsthe wire-level event and the API-level event are different
Retries mask the error ratea transfer-level rate reads 0% on a badly broken link
The denominator is attemptserrors/transfers understates by exactly the retry factor
Expose both denominatorsthe useful diagnostic is their ratio, not either number
A masked transfer is still evidenceit is the only signal that moves before failures start
Report it once per transferonce per attempt rebuilds the flood
The class names the layerand the layer names the team
The distribution, not the totalall-CRC is a cable; evenly spread is a power rail
The layer total, not the largest class40 CRC loses to 30+30 protocol
An undefined encoding goes to no layerguessing costs a team a week proving a negative
Two events, two pulsesa shared code field is a mutual-exclusion claim
A phase measuring an accumulation needs a known accumulatorphase 4 observed 0 alarms until it reset
Narrowing a parameter to its default's widthMAX_RETRY = 5 truncates to 1, and the default still passes
A stale mutant produces a complete, plausible tablethe generator must abort loudly, and BASE must read 0
512/512 sequences, 24/24 class × depth7 mutations, all killed in 3 languages, all by directed stimulus

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o et2_v.out et2_v.v et2_v_tb.v && ./et2_v.out
SystemVerilogiverilog -g2012 -o et2_sv.out et2_sv.sv et2_sv_tb.sv && ./et2_sv.out
VHDL-2008 analysenvc --std=2008 -a et2_vhdl.vhd et2_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_et2_vhdl
VHDL-2008 runnvc --std=2008 -r tb_et2_vhdl
One mutationiverilog -g2005 -DMUT_R7 -o mm et2_v_mut.v et2_v_tb.v && ./mm
Directed onlyiverilog -g2005 -DDIRECTED_ONLY -o mm et2_v_mut.v et2_v_tb.v && ./mm

All three implementations pass with 0 errors: all 512 three-attempt sequences driven, all 24 class × retry-depth situations reached, 1069775 checks against an independently written shadow model, a second instance with a different retry budget to prove the parameterisation is real, and every one of the seven mutations killed by directed stimulus alone.


Chapter 25.5 — CRC Errors takes the largest of the PHY classes and builds it properly: real CRC5 and CRC16 generators, verified exhaustively over all 2048 token values, and the reason the distribution of CRC failures — not the count — is what names the layer underneath them.

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.