Skip to content
VLSI Mentor

USB · Module 24

USB Scoreboards

Two transfers carrying the same bytes are indistinguishable to a scoreboard that matches on value, so a duplicate delivery and a lost transfer cancel out — identity finds the partner, value checks it.

Chapter 24.1 asked "was that legal?" and 24.2 asked "did it happen in time?" This chapter asks the third question: "was that mine?"

1. Match on Identity, Not on Value

The obvious scoreboard keeps a list of expected payloads and, when something arrives, searches the list for a payload that matches.

That scoreboard cannot count.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   issue  tag 3 carrying 0x5A
   issue  tag 6 carrying 0x5A      <-- the SAME bytes. Very common:
                                       zero-length reports, keepalives,
                                       status polls, a cleared buffer.

   tag 3 completes ... TWICE.      a DUPLICATE delivery
   tag 6 never completes.          a LOST transfer

   A value-matching scoreboard sees two arrivals of 0x5A and
   two expectations of 0x5A. Everything matches. Both entries
   are retired.

   The report is clean, and two real bugs have cancelled
   each other out.

2. It Cannot Be a FIFO, Because Completion Is Out of Order

Different endpoints complete independently: a bulk transfer queued first can finish long after an interrupt transfer queued later. A scoreboard built as a queue reports an order violation on every interleaving, which is most of them.

But within an endpoint, order does hold — and that is worth checking, because a controller that completes an endpoint's transfers out of order has corrupted a stream that the host will not verify.

out of order across endpointslegal, and constant
out of order within an endpointa bug, and silent

3. A Retired Tag Is Not an Empty Tag

When a completion arrives for a tag that nothing is outstanding on, there are two very different situations:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   the tag was NEVER issued        -> UNEXPECTED
                                      something invented a completion
                                      out of nothing

   the tag was issued and has
   ALREADY completed               -> DUPLICATE
                                      something delivered twice

A table with one valid bit cannot tell them apart and reports both as "unexpected", which sends everybody looking in the wrong place. So each tag has three states, and the third exists purely so that the report names the right bug.

4. A Retry Is Not a Duplicate

USB retries. A NAKed OUT is re-sent with the same data and the same identity, and that is the protocol working. A scoreboard that treats the re-send as a second delivery reports a duplicate on every flow-controlled transfer — and per 24.1 §14, a scoreboard that cries wolf is a scoreboard somebody switches off.

So a completion flagged as a retry is counted and does not retire the entry. Only the final delivery does.

5. Every Departure Consumes Its Place in the Order

This is the one that is easy to get wrong and produces the worst symptom. A transfer can leave the table four ways: matched, payload wrong, out of order, or never returned at all. Only the first two look like progress, and the tempting implementation advances the endpoint's expected sequence number only on a match.

6. An Identity Is Reusable, Which Bounds How Late a Duplicate Can Be Caught

A finite tag space only works because tags are recycled: a retired tag can be issued again. That is legal and necessary, and it has a consequence worth stating rather than discovering.

An identity must be unique among the transfers that can be in flight at the same time — not unique for the whole run.

So a duplicate delivery of the first use of a tag, arriving after the tag has been re-issued, is attributed to the second use. That is not a defect in the scoreboard; it is the price of a finite identity space, and the way to buy margin is more tags, not cleverer bookkeeping.

7. What We Are Building

usb_scoreboard — two channels, one table, and six ways a transfer can be wrong

A transaction scoreboard. The issue channel records a transfer in a tag table along with its payload, endpoint and sequence number, or reports a reissue error if that identity is already outstanding. The completion channel indexes the table by the identity that came back and reports a match, a payload mismatch, or an in-endpoint order violation. A second completion for an already-retired identity is reported as a duplicate, and end-of-test reports whatever is still outstanding as missing.issue channela transfer went outREISSUEidentity already in usetag tablestate · payload · ep · seqMISSINGnever came backcompletionindexed by IDENTITYDUPLICATEdelivered twiceMATCHidentity and valueMISMATCHright partner, wrong bytesORDERwrong order in one endpointrecord + seqby identityRETIREDend of test12
The issue channel records an identity; the completion channel indexes by it. The table's three-state entries are what let a second delivery be named a duplicate rather than a surprise, and the drain reports whatever never came back.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  usb_scoreboard  #(N_TAG = 8, TW = 3, DW = 8)

  issue channel          completion channel
  -------------          ------------------
  exp_valid              act_valid
  exp_tag   IDENTITY     act_tag   IDENTITY
  exp_data               act_data
  exp_ep                 act_ep
                         act_retry   a re-send, not a result

  eot   drain and report

  ONE EVENT PER CHANNEL, NOT ONE PER CYCLE. An issue and a
  completion can happen in the same cycle and can BOTH be
  wrong, so a single reporting slot would have to drop one.
  Each channel gets its own pulse; the counters, which can
  advance twice in a cycle, are the source of truth.

8. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_scoreboard -- matching what came back against what went out, and the
// one decision that determines whether the scoreboard is worth having.
//
// MATCH ON IDENTITY, NOT ON VALUE
//
// The obvious scoreboard keeps a list of expected payloads, and when
// something arrives it searches the list for a payload that matches.
//
// That scoreboard cannot count.
//
//     issue  tag 3 carrying 0x5A
//     issue  tag 6 carrying 0x5A      <-- the SAME bytes. Very common:
//                                         zero-length reports, keepalives,
//                                         status polls, a cleared buffer.
//
//     tag 3 completes ... twice.      A DUPLICATE delivery.
//     tag 6 never completes.          A LOST transfer.
//
// A value-matching scoreboard sees two arrivals of 0x5A and two expectations
// of 0x5A. Everything matches. Both entries are retired. The report is clean,
// and two real bugs have cancelled each other out.
//
//     A DUPLICATE AND A LOSS ARE INDISTINGUISHABLE FROM A
//     PAIR OF CORRECT TRANSFERS IF YOU MATCH ON VALUE.
//
// So every entry carries an IDENTITY -- here a tag, in a real environment a
// transfer handle, a TRB pointer, a (endpoint, sequence) pair -- and the
// value is checked AFTER the identity has found the pair. Identity finds the
// partner; value checks the partner. Those are two different jobs and doing
// them with one comparison does neither.
//
// IT CANNOT BE A FIFO, BECAUSE COMPLETION IS OUT OF ORDER
//
// Different endpoints complete independently: a bulk transfer queued first
// can finish long after an interrupt transfer queued later. A scoreboard
// built as a queue reports an order violation on every interleaving, which
// is most of them.
//
// But WITHIN an endpoint, order does hold, and that is worth checking --
// because a controller that completes an endpoint's transfers out of order
// has corrupted a stream that the host will not verify.
//
//     out of order ACROSS endpoints   legal, and common
//     out of order WITHIN an endpoint a bug, and silent
//
// AND EVERY DEPARTURE CONSUMES ITS PLACE IN THE ORDER
//
// This is the part that is easy to get wrong and produces the worst symptom.
// A transfer can leave the table four ways: matched, payload wrong, out of
// order, or never returned at all. Only the first two look like "progress",
// and the tempting implementation advances the endpoint's expected sequence
// number only on a match.
//
// Do that and ONE lost transfer leaves a permanent hole. The endpoint's
// expected sequence number is stuck one behind for the rest of the run, so
// EVERY subsequent completion on that endpoint is reported as out of order:
//
//     1 missing transfer  ->  1 missing + N order violations
//
// and the report is then useless for finding the one that mattered. However
// a transfer leaves, it is gone, and its place in the order goes with it.
//
// A RETIRED TAG IS NOT AN EMPTY TAG
//
// This is the distinction that makes a duplicate visible at all. When a
// completion arrives for a tag that nothing is outstanding on, there are two
// very different situations:
//
//     the tag was NEVER issued    -> UNEXPECTED. Something invented a
//                                    completion out of nothing.
//     the tag was issued and has
//     ALREADY completed           -> DUPLICATE. Something delivered twice.
//
// A table with one valid bit cannot tell them apart and reports both as
// "unexpected", which sends everybody looking in the wrong place. So each
// tag has THREE states, and the third one exists purely so that the report
// names the right bug.
//
// ...AND AN IDENTITY IS REUSABLE, WHICH BOUNDS HOW LATE A DUPLICATE CAN BE
// CAUGHT
//
// A finite tag space only works because tags are recycled: a retired tag can
// be issued again. That is legal and necessary, and it has a consequence
// worth stating rather than discovering.
//
//     An identity must be unique among the transfers that can be in
//     flight AT THE SAME TIME -- not unique for the whole run.
//
// So a duplicate delivery of the FIRST use of a tag, arriving after the tag
// has been re-issued, is attributed to the second use. That is not a defect
// in the scoreboard; it is the price of a finite identity space, and the way
// to buy more margin is more tags, not cleverer bookkeeping.
//
// A RETRY IS NOT A DUPLICATE
//
// USB retries. A NAKed OUT is re-sent with the same data and the same
// identity, and that is the protocol working. A scoreboard that treats the
// re-send as a second delivery reports a duplicate on every flow-controlled
// transfer -- and per chapter 24.1, a scoreboard that cries wolf is a
// scoreboard somebody switches off.
//
// So a completion flagged as a retry is counted and does NOT retire the
// entry. Only the final delivery retires it.
//
// ONE EVENT PER CHANNEL, NOT ONE PER CYCLE
//
// An issue and a completion can happen in the same cycle and can BOTH be
// wrong, so a single reporting slot would have to drop one. Each channel
// gets its own pulse instead: the completion channel, the issue channel, and
// the drain. Every pulse is then single-valued, and the counters -- which
// can advance twice in a cycle -- are the source of truth.
module usb_scoreboard #(
  parameter integer N_TAG = 8,   // identities trackable at once
  parameter integer TW    = 3,   // tag width: 2**TW == N_TAG
  parameter integer DW    = 8    // payload width
) (
  input  wire          clk,
  input  wire          rst_n,

  // ---- the issue channel: a transfer was handed to the design ----
  input  wire          exp_valid,
  input  wire [TW-1:0] exp_tag,
  input  wire [DW-1:0] exp_data,
  input  wire          exp_ep,

  // ---- the completion channel: something came back ----
  input  wire          act_valid,
  input  wire [TW-1:0] act_tag,
  input  wire [DW-1:0] act_data,
  input  wire          act_ep,
  input  wire          act_retry,   // this delivery is a re-send, not a result

  input  wire          eot,         // end of test: drain and report

  output wire [TW:0]   outstanding,
  output wire [1:0]    tag_state,   // the state of the tag named by act_tag
  output wire          match_pulse,
  output wire          act_err_pulse,
  output wire [2:0]    act_err_code,
  output wire          exp_err_pulse,  // only one cause: a reissued tag
  output wire          miss_pulse,     // one per survivor of the drain

  output reg [31:0]    n_issued,
  output reg [31:0]    n_matched,
  output reg [31:0]    n_retries,
  output reg [31:0]    n_mismatch,
  output reg [31:0]    n_unexpected,
  output reg [31:0]    n_duplicate,
  output reg [31:0]    n_reissue,
  output reg [31:0]    n_missing,
  output reg [31:0]    n_order
);

  // ---- Three states per tag, and the third one is the whole point. ----
  localparam [1:0] T_EMPTY       = 2'd0,  // never issued, or long retired
                   T_OUTSTANDING = 2'd1,  // issued, not yet completed
                   T_RETIRED     = 2'd2;  // completed -- a further completion
                                          // is a DUPLICATE, not a surprise

  localparam [2:0] E_NONE       = 3'd0,
                   E_MISMATCH   = 3'd1,  // identity matched, payload did not
                   E_UNEXPECTED = 3'd2,  // a completion for a tag never issued
                   E_DUPLICATE  = 3'd3,  // a second completion for one tag
                   E_ORDER      = 3'd4;  // out of order WITHIN an endpoint

  reg [1:0]    st_r   [0:N_TAG-1];
  reg [DW-1:0] dat_r  [0:N_TAG-1];
  reg          ep_r   [0:N_TAG-1];
  reg [3:0]    seq_r  [0:N_TAG-1];

  // Per-endpoint sequence numbers. Issue order is recorded on the way in and
  // checked on the way out, which is what makes "in order within an
  // endpoint" a property rather than an aspiration.
  reg [3:0] iss_seq_r [0:1];
  reg [3:0] cmp_seq_r [0:1];

  reg [TW:0] cnt_r;
  reg [2:0]  aec_r;
  reg        mat_r, aer_r, eer_r, mis_r;

  // The drain walks the table in tag order. A pointer rather than a search,
  // so the drain is bounded and reports the survivors in a stable order.
  reg [TW:0] drain_r;

  assign outstanding   = cnt_r;
  assign tag_state     = st_r[act_tag];
  assign match_pulse   = mat_r;
  assign act_err_pulse = aer_r;
  assign act_err_code  = aec_r;
  assign exp_err_pulse = eer_r;
  assign miss_pulse    = mis_r;

  integer i;
  reg [1:0]    st_n   [0:N_TAG-1];
  reg [DW-1:0] dat_n  [0:N_TAG-1];
  reg          ep_n   [0:N_TAG-1];
  reg [3:0]    seq_n  [0:N_TAG-1];
  reg [3:0]    iss_n  [0:1];
  reg [3:0]    cmp_n  [0:1];
  reg [TW:0]   cnt_n, drain_n;
  reg [2:0]    aec_n;
  reg          mat_n, aer_n, eer_n, mis_n;
  reg          ret_n;

  always @* begin
    for (i = 0; i < N_TAG; i = i + 1) begin
      st_n[i]  = st_r[i];
      dat_n[i] = dat_r[i];
      ep_n[i]  = ep_r[i];
      seq_n[i] = seq_r[i];
    end
    iss_n[0] = iss_seq_r[0]; iss_n[1] = iss_seq_r[1];
    cmp_n[0] = cmp_seq_r[0]; cmp_n[1] = cmp_seq_r[1];
    cnt_n   = cnt_r;
    drain_n = drain_r;
    aec_n   = E_NONE;
    mat_n = 1'b0; aer_n = 1'b0; eer_n = 1'b0; mis_n = 1'b0;
    ret_n = 1'b0;

    if (eot) begin
      // ---- THE DRAIN. Whatever is still outstanding was never completed.
      //
      // It is not "in flight" and it is not "inconclusive": the run is over.
      // Chapter 24.2 makes the same point about assertion obligations, and
      // the failure here is worse, because a missing completion means data
      // the design accepted and never returned.
      if (drain_r < N_TAG[TW:0]) begin
        if (st_r[drain_r[TW-1:0]] == T_OUTSTANDING) begin
          st_n[drain_r[TW-1:0]] = T_EMPTY;
          cnt_n = cnt_n - 1'b1;
          mis_n = 1'b1;
          cmp_n[ep_r[drain_r[TW-1:0]]] =
            cmp_seq_r[ep_r[drain_r[TW-1:0]]] + 4'd1;
        end else begin
          // A RETIRED tag is returned to EMPTY silently. It is not a
          // failure -- it completed -- and leaving it RETIRED for ever
          // would mean each identity could be used exactly once.
          st_n[drain_r[TW-1:0]] = T_EMPTY;
        end
        drain_n = drain_r + 1'b1;
      end
    end else begin
      drain_n = {(TW+1){1'b0}};

      // ---- THE COMPLETION CHANNEL ----
      if (act_valid) begin
        case (st_r[act_tag])
          T_OUTSTANDING: begin
            if (act_data !== dat_r[act_tag]) begin
              // Identity found the partner; the payload is wrong. This is
              // the check everybody writes, and it only works because the
              // identity found the RIGHT partner first.
              aer_n = 1'b1; aec_n = E_MISMATCH;
              st_n[act_tag] = T_RETIRED;
              cnt_n = cnt_n - 1'b1;
              cmp_n[ep_r[act_tag]] = cmp_seq_r[ep_r[act_tag]] + 4'd1;
            end else if (act_ep != ep_r[act_tag]) begin
              // The tag came back on an endpoint it was not issued on. Not
              // a payload error and not a duplicate: the routing is wrong.
              aer_n = 1'b1; aec_n = E_UNEXPECTED;
              st_n[act_tag] = T_RETIRED;
              cnt_n = cnt_n - 1'b1;
              cmp_n[ep_r[act_tag]] = cmp_seq_r[ep_r[act_tag]] + 4'd1;
            end else if (act_retry) begin
              // ---- A RETRY IS NOT A DELIVERY. ----
              //
              // The entry stays outstanding, nothing is retired, and no
              // error is raised. Counting it lets the report distinguish
              // "the bus is busy" from "the bus is broken".
              ret_n = 1'b1;
            end else if (seq_r[act_tag] != cmp_seq_r[act_ep]) begin
              // ---- Out of order WITHIN an endpoint. ----
              aer_n = 1'b1; aec_n = E_ORDER;
              st_n[act_tag] = T_RETIRED;
              cnt_n = cnt_n - 1'b1;
              cmp_n[act_ep] = cmp_seq_r[act_ep] + 4'd1;
            end else begin
              mat_n = 1'b1;
              st_n[act_tag] = T_RETIRED;
              cnt_n = cnt_n - 1'b1;
              cmp_n[act_ep] = cmp_seq_r[act_ep] + 4'd1;
            end
          end

          T_RETIRED: begin
            // ---- A SECOND completion for a tag that already completed.
            // ---- This is the case a one-bit valid flag cannot name.
            aer_n = 1'b1; aec_n = E_DUPLICATE;
          end

          default: begin
            // Never issued. Something produced a completion out of nothing.
            aer_n = 1'b1; aec_n = E_UNEXPECTED;
          end
        endcase
      end

      // ---- THE ISSUE CHANNEL ----
      if (exp_valid) begin
        if (st_r[exp_tag] == T_OUTSTANDING) begin
          // ---- A tag reused while it is still outstanding. ----
          //
          // The identity space has been violated, and the consequence is
          // that the earlier transfer becomes untrackable -- it can never be
          // matched, because its identity now belongs to something else. A
          // scoreboard that silently overwrites loses that transfer AND
          // reports nothing, which is the worst of both.
          eer_n = 1'b1;
        end else begin
          st_n[exp_tag]  = T_OUTSTANDING;
          dat_n[exp_tag] = exp_data;
          ep_n[exp_tag]  = exp_ep;
          seq_n[exp_tag] = iss_seq_r[exp_ep];
          iss_n[exp_ep]  = iss_seq_r[exp_ep] + 4'd1;
          // Only counted here if the completion channel did not already
          // retire this same tag in this cycle -- which it cannot, because
          // a tag that was OUTSTANDING is caught above.
          cnt_n = cnt_n + 1'b1;
        end
      end
    end
  end

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_TAG; i = i + 1) begin
        st_r[i]  <= T_EMPTY;
        dat_r[i] <= {DW{1'b0}};
        ep_r[i]  <= 1'b0;
        seq_r[i] <= 4'd0;
      end
      iss_seq_r[0] <= 4'd0; iss_seq_r[1] <= 4'd0;
      cmp_seq_r[0] <= 4'd0; cmp_seq_r[1] <= 4'd0;
      cnt_r        <= {(TW+1){1'b0}};
      drain_r      <= {(TW+1){1'b0}};
      aec_r        <= E_NONE;
      mat_r        <= 1'b0;
      aer_r        <= 1'b0;
      eer_r        <= 1'b0;
      mis_r        <= 1'b0;
      n_issued     <= 32'd0;
      n_matched    <= 32'd0;
      n_retries    <= 32'd0;
      n_mismatch   <= 32'd0;
      n_unexpected <= 32'd0;
      n_duplicate  <= 32'd0;
      n_reissue    <= 32'd0;
      n_missing    <= 32'd0;
      n_order      <= 32'd0;
    end else begin
      for (i = 0; i < N_TAG; i = i + 1) begin
        st_r[i]  <= st_n[i];
        dat_r[i] <= dat_n[i];
        ep_r[i]  <= ep_n[i];
        seq_r[i] <= seq_n[i];
      end
      iss_seq_r[0] <= iss_n[0]; iss_seq_r[1] <= iss_n[1];
      cmp_seq_r[0] <= cmp_n[0]; cmp_seq_r[1] <= cmp_n[1];
      cnt_r   <= cnt_n;
      drain_r <= drain_n;
      aec_r   <= aec_n;
      mat_r   <= mat_n;
      aer_r   <= aer_n;
      eer_r   <= eer_n;
      mis_r   <= mis_n;

      if (exp_valid && !eot && !eer_n) n_issued  <= n_issued + 32'd1;
      if (mat_n)                       n_matched <= n_matched + 32'd1;
      if (ret_n)                       n_retries <= n_retries + 32'd1;
      if (eer_n)                       n_reissue <= n_reissue + 32'd1;
      if (mis_n)                       n_missing <= n_missing + 32'd1;

      // The per-cause counters on the completion channel are driven by the
      // SAME pulse as the channel's error, so they sum to it (chapter 23.4).
      if (aer_n) begin
        case (aec_n)
          E_MISMATCH:   n_mismatch   <= n_mismatch   + 32'd1;
          E_UNEXPECTED: n_unexpected <= n_unexpected + 32'd1;
          E_DUPLICATE:  n_duplicate  <= n_duplicate  + 32'd1;
          E_ORDER:      n_order      <= n_order      + 32'd1;
          default: ;
        endcase
      end
    end
  end
endmodule

9. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_scoreboard -- matching what came back against what went out, and the
// one decision that determines whether the scoreboard is worth having.
//
// MATCH ON IDENTITY, NOT ON VALUE
//
// The obvious scoreboard keeps a list of expected payloads, and when
// something arrives it searches the list for a payload that matches.
//
// That scoreboard cannot count.
//
//     issue  tag 3 carrying 0x5A
//     issue  tag 6 carrying 0x5A      <-- the SAME bytes. Very common:
//                                         zero-length reports, keepalives,
//                                         status polls, a cleared buffer.
//
//     tag 3 completes ... twice.      A DUPLICATE delivery.
//     tag 6 never completes.          A LOST transfer.
//
// A value-matching scoreboard sees two arrivals of 0x5A and two expectations
// of 0x5A. Everything matches. Both entries are retired. The report is clean,
// and two real bugs have cancelled each other out.
//
//     A DUPLICATE AND A LOSS ARE INDISTINGUISHABLE FROM A
//     PAIR OF CORRECT TRANSFERS IF YOU MATCH ON VALUE.
//
// So every entry carries an IDENTITY -- here a tag, in a real environment a
// transfer handle, a TRB pointer, a (endpoint, sequence) pair -- and the
// value is checked AFTER the identity has found the pair. Identity finds the
// partner; value checks the partner. Those are two different jobs and doing
// them with one comparison does neither.
//
// IT CANNOT BE A FIFO, BECAUSE COMPLETION IS OUT OF ORDER
//
// Different endpoints complete independently: a bulk transfer queued first
// can finish long after an interrupt transfer queued later. A scoreboard
// built as a queue reports an order violation on every interleaving, which
// is most of them.
//
// But WITHIN an endpoint, order does hold, and that is worth checking --
// because a controller that completes an endpoint's transfers out of order
// has corrupted a stream that the host will not verify.
//
//     out of order ACROSS endpoints   legal, and common
//     out of order WITHIN an endpoint a bug, and silent
//
// AND EVERY DEPARTURE CONSUMES ITS PLACE IN THE ORDER
//
// This is the part that is easy to get wrong and produces the worst symptom.
// A transfer can leave the table four ways: matched, payload wrong, out of
// order, or never returned at all. Only the first two look like "progress",
// and the tempting implementation advances the endpoint's expected sequence
// number only on a match.
//
// Do that and ONE lost transfer leaves a permanent hole. The endpoint's
// expected sequence number is stuck one behind for the rest of the run, so
// EVERY subsequent completion on that endpoint is reported as out of order:
//
//     1 missing transfer  ->  1 missing + N order violations
//
// and the report is then useless for finding the one that mattered. However
// a transfer leaves, it is gone, and its place in the order goes with it.
//
// A RETIRED TAG IS NOT AN EMPTY TAG
//
// This is the distinction that makes a duplicate visible at all. When a
// completion arrives for a tag that nothing is outstanding on, there are two
// very different situations:
//
//     the tag was NEVER issued    -> UNEXPECTED. Something invented a
//                                    completion out of nothing.
//     the tag was issued and has
//     ALREADY completed           -> DUPLICATE. Something delivered twice.
//
// A table with one valid bit cannot tell them apart and reports both as
// "unexpected", which sends everybody looking in the wrong place. So each
// tag has THREE states, and the third one exists purely so that the report
// names the right bug.
//
// ...AND AN IDENTITY IS REUSABLE, WHICH BOUNDS HOW LATE A DUPLICATE CAN BE
// CAUGHT
//
// A finite tag space only works because tags are recycled: a retired tag can
// be issued again. That is legal and necessary, and it has a consequence
// worth stating rather than discovering.
//
//     An identity must be unique among the transfers that can be in
//     flight AT THE SAME TIME -- not unique for the whole run.
//
// So a duplicate delivery of the FIRST use of a tag, arriving after the tag
// has been re-issued, is attributed to the second use. That is not a defect
// in the scoreboard; it is the price of a finite identity space, and the way
// to buy more margin is more tags, not cleverer bookkeeping.
//
// A RETRY IS NOT A DUPLICATE
//
// USB retries. A NAKed OUT is re-sent with the same data and the same
// identity, and that is the protocol working. A scoreboard that treats the
// re-send as a second delivery reports a duplicate on every flow-controlled
// transfer -- and per chapter 24.1, a scoreboard that cries wolf is a
// scoreboard somebody switches off.
//
// So a completion flagged as a retry is counted and does NOT retire the
// entry. Only the final delivery retires it.
//
// ONE EVENT PER CHANNEL, NOT ONE PER CYCLE
//
// An issue and a completion can happen in the same cycle and can BOTH be
// wrong, so a single reporting slot would have to drop one. Each channel
// gets its own pulse instead: the completion channel, the issue channel, and
// the drain. Every pulse is then single-valued, and the counters -- which
// can advance twice in a cycle -- are the source of truth.
package usb_sb_pkg;
  // Three states per tag, and the third one is the whole point: it is what
  // lets the report say DUPLICATE where a one-bit valid flag can only say
  // "unexpected", which sends everybody looking in the wrong place.
  typedef enum logic [1:0] {
    T_EMPTY       = 2'd0,   // never issued, or returned by the drain
    T_OUTSTANDING = 2'd1,   // issued, not yet completed
    T_RETIRED     = 2'd2    // completed -- a further completion is a
                            // DUPLICATE, not a surprise
  } tag_state_e;

  typedef enum logic [2:0] {
    E_NONE       = 3'd0,
    E_MISMATCH   = 3'd1,   // identity matched, payload did not
    E_UNEXPECTED = 3'd2,   // a completion for a tag never issued
    E_DUPLICATE  = 3'd3,   // a second completion for one identity
    E_ORDER      = 3'd4    // out of order WITHIN an endpoint
  } sb_err_e;
endpackage

module usb_scoreboard
  import usb_sb_pkg::*;
 #(
  parameter int N_TAG = 8,   // identities trackable at once
  parameter int TW    = 3,   // tag width: 2**TW == N_TAG
  parameter int DW    = 8    // payload width
) (
  input  logic         clk,
  input  logic         rst_n,

  // ---- the issue channel: a transfer was handed to the design ----
  input  logic         exp_valid,
  input  logic[TW-1:0] exp_tag,
  input  logic[DW-1:0] exp_data,
  input  logic         exp_ep,

  // ---- the completion channel: something came back ----
  input  logic         act_valid,
  input  logic[TW-1:0] act_tag,
  input  logic[DW-1:0] act_data,
  input  logic         act_ep,
  input  logic         act_retry,   // this delivery is a re-send, not a result

  input  logic         eot,         // end of test: drain and report

  output logic[TW:0]   outstanding,
  output tag_state_e   tag_state,   // the state of the tag named by act_tag
  output logic         match_pulse,
  output logic         act_err_pulse,
  output sb_err_e      act_err_code,
  output logic         exp_err_pulse,  // only one cause: a reissued tag
  output logic         miss_pulse,     // one per survivor of the drain

  output logic [31:0]    n_issued,
  output logic [31:0]    n_matched,
  output logic [31:0]    n_retries,
  output logic [31:0]    n_mismatch,
  output logic [31:0]    n_unexpected,
  output logic [31:0]    n_duplicate,
  output logic [31:0]    n_reissue,
  output logic [31:0]    n_missing,
  output logic [31:0]    n_order
);

  tag_state_e    st_r  [N_TAG];
  logic [DW-1:0] dat_r [N_TAG];
  logic          ep_r  [N_TAG];
  logic [3:0]    seq_r [N_TAG];

  // Per-endpoint sequence numbers. Issue order is recorded on the way in and
  // checked on the way out, which is what makes "in order within an
  // endpoint" a property rather than an aspiration.
  logic [3:0] iss_seq_r [2];
  logic [3:0] cmp_seq_r [2];

  logic [TW:0] cnt_r;
  sb_err_e     aec_r;
  logic        mat_r, aer_r, eer_r, mis_r;

  // The drain walks the table in tag order. A pointer rather than a search,
  // so the drain is bounded and reports the survivors in a stable order.
  logic [TW:0] drain_r;

  assign outstanding   = cnt_r;
  assign tag_state     = st_r[act_tag];
  assign match_pulse   = mat_r;
  assign act_err_pulse = aer_r;
  assign act_err_code  = aec_r;
  assign exp_err_pulse = eer_r;
  assign miss_pulse    = mis_r;

  int            i;
  tag_state_e    st_n  [N_TAG];
  logic [DW-1:0] dat_n [N_TAG];
  logic          ep_n  [N_TAG];
  logic [3:0]    seq_n [N_TAG];
  logic [3:0]    iss_n [2];
  logic [3:0]    cmp_n [2];
  logic [TW:0]   cnt_n, drain_n;
  sb_err_e       aec_n;
  logic          mat_n, aer_n, eer_n, mis_n;
  logic          ret_n;

  always_comb begin
    for (i = 0; i < N_TAG; i++) begin
      st_n[i]  = st_r[i];
      dat_n[i] = dat_r[i];
      ep_n[i]  = ep_r[i];
      seq_n[i] = seq_r[i];
    end
    iss_n[0] = iss_seq_r[0]; iss_n[1] = iss_seq_r[1];
    cmp_n[0] = cmp_seq_r[0]; cmp_n[1] = cmp_seq_r[1];
    cnt_n   = cnt_r;
    drain_n = drain_r;
    aec_n   = E_NONE;
    mat_n = 1'b0; aer_n = 1'b0; eer_n = 1'b0; mis_n = 1'b0;
    ret_n = 1'b0;

    if (eot) begin
      // ---- THE DRAIN. Whatever is still outstanding was never completed.
      //
      // It is not "in flight" and it is not "inconclusive": the run is over.
      // Chapter 24.2 makes the same point about assertion obligations, and
      // the failure here is worse, because a missing completion means data
      // the design accepted and never returned.
      if (drain_r < (TW+1)'(N_TAG)) begin
        if (st_r[drain_r[TW-1:0]] == T_OUTSTANDING) begin
          st_n[drain_r[TW-1:0]] = T_EMPTY;
          cnt_n = cnt_n - 1'b1;
          mis_n = 1'b1;
          cmp_n[ep_r[drain_r[TW-1:0]]] =
            cmp_seq_r[ep_r[drain_r[TW-1:0]]] + 4'd1;
        end else begin
          // A RETIRED tag is returned to EMPTY silently. It is not a
          // failure -- it completed -- and leaving it RETIRED for ever
          // would mean each identity could be used exactly once.
          st_n[drain_r[TW-1:0]] = T_EMPTY;
        end
        drain_n = drain_r + 1'b1;
      end
    end else begin
      drain_n = '0;

      // ---- THE COMPLETION CHANNEL ----
      if (act_valid) begin
        case (st_r[act_tag])
          T_OUTSTANDING: begin
            if (act_data !== dat_r[act_tag]) begin
              // Identity found the partner; the payload is wrong. This is
              // the check everybody writes, and it only works because the
              // identity found the RIGHT partner first.
              aer_n = 1'b1; aec_n = E_MISMATCH;
              st_n[act_tag] = T_RETIRED;
              cnt_n = cnt_n - 1'b1;
              cmp_n[ep_r[act_tag]] = cmp_seq_r[ep_r[act_tag]] + 4'd1;
            end else if (act_ep != ep_r[act_tag]) begin
              // The tag came back on an endpoint it was not issued on. Not
              // a payload error and not a duplicate: the routing is wrong.
              aer_n = 1'b1; aec_n = E_UNEXPECTED;
              st_n[act_tag] = T_RETIRED;
              cnt_n = cnt_n - 1'b1;
              cmp_n[ep_r[act_tag]] = cmp_seq_r[ep_r[act_tag]] + 4'd1;
            end else if (act_retry) begin
              // ---- A RETRY IS NOT A DELIVERY. ----
              //
              // The entry stays outstanding, nothing is retired, and no
              // error is raised. Counting it lets the report distinguish
              // "the bus is busy" from "the bus is broken".
              ret_n = 1'b1;
            end else if (seq_r[act_tag] != cmp_seq_r[act_ep]) begin
              // ---- Out of order WITHIN an endpoint. ----
              aer_n = 1'b1; aec_n = E_ORDER;
              st_n[act_tag] = T_RETIRED;
              cnt_n = cnt_n - 1'b1;
              cmp_n[act_ep] = cmp_seq_r[act_ep] + 4'd1;
            end else begin
              mat_n = 1'b1;
              st_n[act_tag] = T_RETIRED;
              cnt_n = cnt_n - 1'b1;
              cmp_n[act_ep] = cmp_seq_r[act_ep] + 4'd1;
            end
          end

          T_RETIRED: begin
            // ---- A SECOND completion for a tag that already completed.
            // ---- This is the case a one-bit valid flag cannot name.
            aer_n = 1'b1; aec_n = E_DUPLICATE;
          end

          default: begin
            // Never issued. Something produced a completion out of nothing.
            aer_n = 1'b1; aec_n = E_UNEXPECTED;
          end
        endcase
      end

      // ---- THE ISSUE CHANNEL ----
      if (exp_valid) begin
        if (st_r[exp_tag] == T_OUTSTANDING) begin
          // ---- A tag reused while it is still outstanding. ----
          //
          // The identity space has been violated, and the consequence is
          // that the earlier transfer becomes untrackable -- it can never be
          // matched, because its identity now belongs to something else. A
          // scoreboard that silently overwrites loses that transfer AND
          // reports nothing, which is the worst of both.
          eer_n = 1'b1;
        end else begin
          st_n[exp_tag]  = T_OUTSTANDING;
          dat_n[exp_tag] = exp_data;
          ep_n[exp_tag]  = exp_ep;
          seq_n[exp_tag] = iss_seq_r[exp_ep];
          iss_n[exp_ep]  = iss_seq_r[exp_ep] + 4'd1;
          // Only counted here if the completion channel did not already
          // retire this same tag in this cycle -- which it cannot, because
          // a tag that was OUTSTANDING is caught above.
          cnt_n = cnt_n + 1'b1;
        end
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_TAG; i++) begin
        st_r[i]  <= T_EMPTY;
        dat_r[i] <= '0;
        ep_r[i]  <= 1'b0;
        seq_r[i] <= 4'd0;
      end
      iss_seq_r[0] <= 4'd0; iss_seq_r[1] <= 4'd0;
      cmp_seq_r[0] <= 4'd0; cmp_seq_r[1] <= 4'd0;
      cnt_r        <= '0;
      drain_r      <= '0;
      aec_r        <= E_NONE;
      mat_r        <= 1'b0;
      aer_r        <= 1'b0;
      eer_r        <= 1'b0;
      mis_r        <= 1'b0;
      n_issued     <= 32'd0;
      n_matched    <= 32'd0;
      n_retries    <= 32'd0;
      n_mismatch   <= 32'd0;
      n_unexpected <= 32'd0;
      n_duplicate  <= 32'd0;
      n_reissue    <= 32'd0;
      n_missing    <= 32'd0;
      n_order      <= 32'd0;
    end else begin
      for (i = 0; i < N_TAG; i++) begin
        st_r[i]  <= st_n[i];
        dat_r[i] <= dat_n[i];
        ep_r[i]  <= ep_n[i];
        seq_r[i] <= seq_n[i];
      end
      iss_seq_r[0] <= iss_n[0]; iss_seq_r[1] <= iss_n[1];
      cmp_seq_r[0] <= cmp_n[0]; cmp_seq_r[1] <= cmp_n[1];
      cnt_r   <= cnt_n;
      drain_r <= drain_n;
      aec_r   <= aec_n;
      mat_r   <= mat_n;
      aer_r   <= aer_n;
      eer_r   <= eer_n;
      mis_r   <= mis_n;

      if (exp_valid && !eot && !eer_n) n_issued  <= n_issued + 32'd1;
      if (mat_n)                       n_matched <= n_matched + 32'd1;
      if (ret_n)                       n_retries <= n_retries + 32'd1;
      if (eer_n)                       n_reissue <= n_reissue + 32'd1;
      if (mis_n)                       n_missing <= n_missing + 32'd1;

      // The per-cause counters on the completion channel are driven by the
      // SAME pulse as the channel's error, so they sum to it (chapter 23.4).
      if (aer_n) begin
        case (aec_n)
          E_MISMATCH:   n_mismatch   <= n_mismatch   + 32'd1;
          E_UNEXPECTED: n_unexpected <= n_unexpected + 32'd1;
          E_DUPLICATE:  n_duplicate  <= n_duplicate  + 32'd1;
          E_ORDER:      n_order      <= n_order      + 32'd1;
          default: ;
        endcase
      end
    end
  end
endmodule

10. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- usb_scoreboard -- matching what came back against what went out, and the
-- one decision that determines whether the scoreboard is worth having.
--
-- MATCH ON IDENTITY, NOT ON VALUE
--
-- The obvious scoreboard keeps a list of expected payloads, and when
-- something arrives it searches the list for a payload that matches.
--
-- That scoreboard cannot count.
--
--     issue  tag 3 carrying 0x5A
--     issue  tag 6 carrying 0x5A      <-- the SAME bytes. Very common:
--                                         zero-length reports, keepalives,
--                                         status polls, a cleared buffer.
--
--     tag 3 completes ... twice.      A DUPLICATE delivery.
--     tag 6 never completes.          A LOST transfer.
--
-- A value-matching scoreboard sees two arrivals of 0x5A and two expectations
-- of 0x5A. Everything matches. Both entries are retired. The report is clean,
-- and two real bugs have cancelled each other out.
--
--     A DUPLICATE AND A LOSS ARE INDISTINGUISHABLE FROM A
--     PAIR OF CORRECT TRANSFERS IF YOU MATCH ON VALUE.
--
-- So every entry carries an IDENTITY -- here a tag, in a real environment a
-- transfer handle, a TRB pointer, a (endpoint, sequence) pair -- and the
-- value is checked AFTER the identity has found the pair. Identity finds the
-- partner; value checks the partner. Those are two different jobs and doing
-- them with one comparison does neither.
--
-- IT CANNOT BE A FIFO, BECAUSE COMPLETION IS OUT OF ORDER
--
-- Different endpoints complete independently: a bulk transfer queued first
-- can finish long after an interrupt transfer queued later. A scoreboard
-- built as a queue reports an order violation on every interleaving, which
-- is most of them.
--
-- But WITHIN an endpoint, order does hold, and that is worth checking --
-- because a controller that completes an endpoint's transfers out of order
-- has corrupted a stream that the host will not verify.
--
--     out of order ACROSS endpoints   legal, and common
--     out of order WITHIN an endpoint a bug, and silent
--
-- AND EVERY DEPARTURE CONSUMES ITS PLACE IN THE ORDER
--
-- This is the part that is easy to get wrong and produces the worst symptom.
-- A transfer can leave the table four ways: matched, payload wrong, out of
-- order, or never returned at all. Only the first two look like "progress",
-- and the tempting implementation advances the endpoint's expected sequence
-- number only on a match.
--
-- Do that and ONE lost transfer leaves a permanent hole. The endpoint's
-- expected sequence number is stuck one behind for the rest of the run, so
-- EVERY subsequent completion on that endpoint is reported as out of order:
--
--     1 missing transfer  ->  1 missing + N order violations
--
-- and the report is then useless for finding the one that mattered. However
-- a transfer leaves, it is gone, and its place in the order goes with it.
--
-- A RETIRED TAG IS NOT AN EMPTY TAG
--
-- This is the distinction that makes a duplicate visible at all. When a
-- completion arrives for a tag that nothing is outstanding on, there are two
-- very different situations:
--
--     the tag was NEVER issued    -> UNEXPECTED. Something invented a
--                                    completion out of nothing.
--     the tag was issued and has
--     ALREADY completed           -> DUPLICATE. Something delivered twice.
--
-- A table with one valid bit cannot tell them apart and reports both as
-- "unexpected", which sends everybody looking in the wrong place. So each
-- tag has THREE states, and the third one exists purely so that the report
-- names the right bug.
--
-- ...AND AN IDENTITY IS REUSABLE, WHICH BOUNDS HOW LATE A DUPLICATE CAN BE
-- CAUGHT
--
-- A finite tag space only works because tags are recycled: a retired tag can
-- be issued again. That is legal and necessary, and it has a consequence
-- worth stating rather than discovering.
--
--     An identity must be unique among the transfers that can be in
--     flight AT THE SAME TIME -- not unique for the whole run.
--
-- So a duplicate delivery of the FIRST use of a tag, arriving after the tag
-- has been re-issued, is attributed to the second use. That is not a defect
-- in the scoreboard; it is the price of a finite identity space, and the way
-- to buy more margin is more tags, not cleverer bookkeeping.
--
-- A RETRY IS NOT A DUPLICATE
--
-- USB retries. A NAKed OUT is re-sent with the same data and the same
-- identity, and that is the protocol working. A scoreboard that treats the
-- re-send as a second delivery reports a duplicate on every flow-controlled
-- transfer -- and per chapter 24.1, a scoreboard that cries wolf is a
-- scoreboard somebody switches off.
--
-- So a completion flagged as a retry is counted and does NOT retire the
-- entry. Only the final delivery retires it.
--
-- ONE EVENT PER CHANNEL, NOT ONE PER CYCLE
--
-- An issue and a completion can happen in the same cycle and can BOTH be
-- wrong, so a single reporting slot would have to drop one. Each channel
-- gets its own pulse instead: the completion channel, the issue channel, and
-- the drain. Every pulse is then single-valued, and the counters -- which
-- can advance twice in a cycle -- are the source of truth.
library ieee;
use ieee.std_logic_1164.all;

package usb_sb_pkg is
  -- Three states per tag, and the third one is the whole point: it is what
  -- lets the report say DUPLICATE where a one-bit valid flag can only say
  -- "unexpected", which sends everybody looking in the wrong place.
  type tag_state_t is (T_EMPTY, T_OUTSTANDING, T_RETIRED);

  type sb_err_t is (E_NONE, E_MISMATCH, E_UNEXPECTED, E_DUPLICATE, E_ORDER);

  function ts_code (s : tag_state_t) return std_logic_vector;
  function se_code (e : sb_err_t)    return std_logic_vector;
end package usb_sb_pkg;

package body usb_sb_pkg is
  -- Written out rather than derived from position, so the encodings are
  -- pinned to the same numbers the Verilog and SystemVerilog use.
  function ts_code (s : tag_state_t) return std_logic_vector is
  begin
    case s is
      when T_EMPTY       => return "00";
      when T_OUTSTANDING => return "01";
      when T_RETIRED     => return "10";
    end case;
  end function;

  function se_code (e : sb_err_t) return std_logic_vector is
  begin
    case e is
      when E_NONE       => return "000";
      when E_MISMATCH   => return "001";
      when E_UNEXPECTED => return "010";
      when E_DUPLICATE  => return "011";
      when E_ORDER      => return "100";
    end case;
  end function;
end package body usb_sb_pkg;

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

entity usb_scoreboard is
  generic (
    N_TAG : integer := 8;   -- identities trackable at once
    TW    : integer := 3;   -- tag width: 2**TW = N_TAG
    DW    : integer := 8    -- payload width
  );
  port (
    clk           : in  std_logic;
    rst_n         : in  std_logic;

    -- ---- the issue channel: a transfer was handed to the design ----
    exp_valid     : in  std_logic;
    exp_tag       : in  std_logic_vector(TW-1 downto 0);
    exp_data      : in  std_logic_vector(DW-1 downto 0);
    exp_ep        : in  std_logic;

    -- ---- the completion channel: something came back ----
    act_valid     : in  std_logic;
    act_tag       : in  std_logic_vector(TW-1 downto 0);
    act_data      : in  std_logic_vector(DW-1 downto 0);
    act_ep        : in  std_logic;
    act_retry     : in  std_logic;   -- a re-send, not a result

    eot           : in  std_logic;   -- end of test: drain and report

    outstanding   : out std_logic_vector(TW downto 0);
    tag_state     : out std_logic_vector(1 downto 0);
    match_pulse   : out std_logic;
    act_err_pulse : out std_logic;
    act_err_code  : out std_logic_vector(2 downto 0);
    exp_err_pulse : out std_logic;   -- only one cause: a reissued tag
    miss_pulse    : out std_logic;   -- one per survivor of the drain

    n_issued      : out std_logic_vector(31 downto 0);
    n_matched     : out std_logic_vector(31 downto 0);
    n_retries     : out std_logic_vector(31 downto 0);
    n_mismatch    : out std_logic_vector(31 downto 0);
    n_unexpected  : out std_logic_vector(31 downto 0);
    n_duplicate   : out std_logic_vector(31 downto 0);
    n_reissue     : out std_logic_vector(31 downto 0);
    n_missing     : out std_logic_vector(31 downto 0);
    n_order       : out std_logic_vector(31 downto 0)
  );
end entity usb_scoreboard;

architecture rtl of usb_scoreboard is

  type st_arr  is array (0 to N_TAG-1) of tag_state_t;
  type dat_arr is array (0 to N_TAG-1) of std_logic_vector(DW-1 downto 0);
  type seq_arr is array (0 to N_TAG-1) of unsigned(3 downto 0);
  type ep_arr  is array (0 to N_TAG-1) of std_logic;
  type eps_arr is array (0 to 1) of unsigned(3 downto 0);

  signal st_r  : st_arr  := (others => T_EMPTY);
  signal dat_r : dat_arr := (others => (others => '0'));
  signal ep_r  : ep_arr  := (others => '0');
  signal seq_r : seq_arr := (others => (others => '0'));

  -- Per-endpoint sequence numbers. Issue order is recorded on the way in and
  -- checked on the way out, which is what makes "in order within an
  -- endpoint" a property rather than an aspiration.
  signal iss_seq_r : eps_arr := (others => (others => '0'));
  signal cmp_seq_r : eps_arr := (others => (others => '0'));

  signal cnt_r   : unsigned(TW downto 0) := (others => '0');
  signal aec_r   : sb_err_t := E_NONE;
  signal mat_r, aer_r, eer_r, mis_r : std_logic := '0';

  -- The drain walks the table in tag order. A pointer rather than a search,
  -- so the drain is bounded and reports the survivors in a stable order.
  signal drain_r : unsigned(TW downto 0) := (others => '0');

  -- Accumulators are held as unsigned rather than as range-constrained
  -- integers: a constrained integer aborts simulation on overflow, which
  -- turns a mutation into a crash instead of a measured kill.
  signal c_i, c_m, c_rt, c_mm : unsigned(31 downto 0) := (others => '0');
  signal c_un, c_dp, c_ri, c_ms, c_or : unsigned(31 downto 0) := (others => '0');

begin

  outstanding   <= std_logic_vector(cnt_r);
  tag_state     <= ts_code(st_r(to_integer(unsigned(act_tag))));
  match_pulse   <= mat_r;
  act_err_pulse <= aer_r;
  act_err_code  <= se_code(aec_r);
  exp_err_pulse <= eer_r;
  miss_pulse    <= mis_r;

  n_issued     <= std_logic_vector(c_i);
  n_matched    <= std_logic_vector(c_m);
  n_retries    <= std_logic_vector(c_rt);
  n_mismatch   <= std_logic_vector(c_mm);
  n_unexpected <= std_logic_vector(c_un);
  n_duplicate  <= std_logic_vector(c_dp);
  n_reissue    <= std_logic_vector(c_ri);
  n_missing    <= std_logic_vector(c_ms);
  n_order      <= std_logic_vector(c_or);

  process (clk, rst_n)
    variable at_i, et_i, dr_i : integer;
    variable sat, set_st : tag_state_t;
    variable w_at : tag_state_t;
    variable w_at_en, w_et_en : boolean;
    variable ncnt, ndrn : unsigned(TW downto 0);
    variable niss, ncmp : eps_arr;
    variable naec : sb_err_t;
    variable nmat, naer, neer, nmis, nret : std_logic;
    variable aep_i, eep_i : integer;
  begin
    if rst_n = '0' then
      st_r  <= (others => T_EMPTY);
      dat_r <= (others => (others => '0'));
      ep_r  <= (others => '0');
      seq_r <= (others => (others => '0'));
      iss_seq_r <= (others => (others => '0'));
      cmp_seq_r <= (others => (others => '0'));
      cnt_r   <= (others => '0');
      drain_r <= (others => '0');
      aec_r   <= E_NONE;
      mat_r <= '0'; aer_r <= '0'; eer_r <= '0'; mis_r <= '0';
      c_i  <= (others => '0'); c_m  <= (others => '0');
      c_rt <= (others => '0'); c_mm <= (others => '0');
      c_un <= (others => '0'); c_dp <= (others => '0');
      c_ri <= (others => '0'); c_ms <= (others => '0');
      c_or <= (others => '0');
    elsif rising_edge(clk) then
      at_i  := to_integer(unsigned(act_tag));
      et_i  := to_integer(unsigned(exp_tag));
      aep_i := 0; if act_ep = '1' then aep_i := 1; end if;
      eep_i := 0; if exp_ep = '1' then eep_i := 1; end if;

      -- BOTH channels read the table as it stands at the start of the cycle.
      sat  := st_r(at_i);
      set_st := st_r(et_i);
      w_at_en := false; w_et_en := false; w_at := T_EMPTY;

      ncnt := cnt_r; ndrn := drain_r; naec := E_NONE;
      niss := iss_seq_r; ncmp := cmp_seq_r;
      nmat := '0'; naer := '0'; neer := '0'; nmis := '0'; nret := '0';

      if eot = '1' then
        -- ---- THE DRAIN. Whatever is still outstanding was never completed.
        --
        -- It is not "in flight" and it is not "inconclusive": the run is
        -- over. Chapter 24.2 makes the same point about assertion
        -- obligations, and the failure here is worse, because a missing
        -- completion means data the design accepted and never returned.
        if drain_r < to_unsigned(N_TAG, TW+1) then
          dr_i := to_integer(drain_r);
          if st_r(dr_i) = T_OUTSTANDING then
            st_r(dr_i) <= T_EMPTY;
            ncnt := ncnt - 1;
            nmis := '1';
            if ep_r(dr_i) = '1' then ncmp(1) := cmp_seq_r(1) + 1;
            else                     ncmp(0) := cmp_seq_r(0) + 1;
            end if;
          else
            -- A RETIRED tag is returned to EMPTY silently. It is not a
            -- failure -- it completed -- and leaving it RETIRED for ever
            -- would mean each identity could be used exactly once.
            st_r(dr_i) <= T_EMPTY;
          end if;
          ndrn := drain_r + 1;
        end if;
      else
        ndrn := (others => '0');

        -- ---- THE COMPLETION CHANNEL ----
        if act_valid = '1' then
          case sat is
            when T_OUTSTANDING =>
              if act_data /= dat_r(at_i) then
                -- Identity found the partner; the payload is wrong. This is
                -- the check everybody writes, and it only works because the
                -- identity found the RIGHT partner first.
                naer := '1'; naec := E_MISMATCH;
                w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
                if ep_r(at_i) = '1' then ncmp(1) := cmp_seq_r(1) + 1;
                else                     ncmp(0) := cmp_seq_r(0) + 1;
                end if;
              elsif act_ep /= ep_r(at_i) then
                -- The tag came back on an endpoint it was not issued on.
                -- Not a payload error and not a duplicate: the routing is
                -- wrong.
                naer := '1'; naec := E_UNEXPECTED;
                w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
                if ep_r(at_i) = '1' then ncmp(1) := cmp_seq_r(1) + 1;
                else                     ncmp(0) := cmp_seq_r(0) + 1;
                end if;
              elsif act_retry = '1' then
                -- ---- A RETRY IS NOT A DELIVERY. ----
                --
                -- The entry stays outstanding, nothing is retired, and no
                -- error is raised. Counting it lets the report distinguish
                -- "the bus is busy" from "the bus is broken".
                nret := '1';
              elsif seq_r(at_i) /= cmp_seq_r(aep_i) then
                -- ---- Out of order WITHIN an endpoint. ----
                naer := '1'; naec := E_ORDER;
                w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
                ncmp(aep_i) := cmp_seq_r(aep_i) + 1;
              else
                nmat := '1';
                w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
                ncmp(aep_i) := cmp_seq_r(aep_i) + 1;
              end if;

            when T_RETIRED =>
              -- ---- A SECOND completion for a tag that already completed.
              -- ---- This is the case a one-bit valid flag cannot name.
              naer := '1'; naec := E_DUPLICATE;

            when others =>
              -- Never issued. Something produced a completion out of
              -- nothing.
              naer := '1'; naec := E_UNEXPECTED;
          end case;
        end if;

        -- ---- THE ISSUE CHANNEL ----
        if exp_valid = '1' then
          if set_st = T_OUTSTANDING then
            -- ---- A tag reused while it is still outstanding. ----
            --
            -- The identity space has been violated, and the consequence is
            -- that the earlier transfer becomes untrackable -- it can never
            -- be matched, because its identity now belongs to something
            -- else. A scoreboard that silently overwrites loses that
            -- transfer AND reports nothing, which is the worst of both.
            neer := '1';
          else
            w_et_en := true;
            ncnt := ncnt + 1;
          end if;
        end if;

        -- Applied in the design's order: the completion channel's write
        -- first, the issue channel's second, so an issue to the same tag
        -- wins -- which it can only do if that tag was not outstanding.
        if w_at_en then st_r(at_i) <= w_at; end if;
        if w_et_en then
          st_r(et_i)  <= T_OUTSTANDING;
          dat_r(et_i) <= exp_data;
          ep_r(et_i)  <= exp_ep;
          seq_r(et_i) <= iss_seq_r(eep_i);
          niss(eep_i) := iss_seq_r(eep_i) + 1;
        end if;
      end if;

      cnt_r     <= ncnt;
      drain_r   <= ndrn;
      aec_r     <= naec;
      mat_r     <= nmat;
      aer_r     <= naer;
      eer_r     <= neer;
      mis_r     <= nmis;
      iss_seq_r <= niss;
      cmp_seq_r <= ncmp;

      if exp_valid = '1' and eot = '0' and neer = '0' then c_i <= c_i + 1; end if;
      if nmat = '1' then c_m  <= c_m  + 1; end if;
      if nret = '1' then c_rt <= c_rt + 1; end if;
      if neer = '1' then c_ri <= c_ri + 1; end if;
      if nmis = '1' then c_ms <= c_ms + 1; end if;

      -- The per-cause counters on the completion channel are driven by the
      -- SAME pulse as the channel's error, so they sum to it (chapter 23.4).
      if naer = '1' then
        case naec is
          when E_MISMATCH   => c_mm <= c_mm + 1;
          when E_UNEXPECTED => c_un <= c_un + 1;
          when E_DUPLICATE  => c_dp <= c_dp + 1;
          when E_ORDER      => c_or <= c_or + 1;
          when others       => null;
        end case;
      end if;
    end if;
  end process;

end architecture rtl;

11. Both Channels Read the Table as It Was

The completion channel retires an entry and the issue channel records one, and they can name the same tag in the same cycle. The design reads its registered table in both, so both see the state as it stood at the start of the cycle.

The first shadow model did not: it mutated its table inside the completion branch and then read it in the issue branch.

12. Seeing the Identity Test

Two transfers with identical payloads: one delivered twice, one never

usb_scoreboard — a duplicate and a loss, which a value-matcher cancels

10 cycles
A ten-cycle waveform. Two transfers with identical payload 0x5A are issued on tags 0 and 1, bringing outstanding to 2. A completion for tag 0 matches and outstanding falls to 1. A second completion for tag 0 raises act_err_pulse with code DUPLICATE. End of test then drains the table and reports tag 1 as missing.two transfers, identical payloadtwo transfers, identicalpayloadtag 0 matchedtag 0 matchedtag 0 again: DUPLICATEtag 0 again: DUPLICATEtag 1 never came backtag 1 never came backclkexp_tag0111111111act_validact_tag0000000000eotoutstanding0121111100match_pulseact_err_pulseact_err_codeNONENONENONENONENONEDUPNONENONENONENONEmiss_pulset0t1t2t3t4t5t6t7t8t9
Both issues carry the identical payload 0x5A — exp_valid and exp_data are omitted for width and are implied by exp_tag. Tag 0 completes at cycle 3 and is delivered again at cycle 5 — a duplicate. End of test at cycle 6 starts the drain, which reaches tag 1 at cycle 8 and reports it missing. A value-matching scoreboard shows two matches here and neither pulse.

13. The Testbenches

Two exhaustive sweeps — every tag state crossed with all 16 input combinations (48 pairs), and every table occupancy 0 to 8 — plus the phase the chapter exists for:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // ---- Phase C: THE IDENTITY TEST. Two transfers, SAME payload; one
    // ---- delivered twice, the other never.
    for (k = 0; k < 40; k = k + 1) begin
      b_m = n_matched;
      issue(3'd3, 8'h5A, 1'b0);
      issue(3'd6, 8'h5A, 1'b0);          // the SAME bytes
      complete(3'd3, 8'h5A, 1'b0, 1'b0); // match
      b_e = n_duplicate;
      complete(3'd3, 8'h5A, 1'b0, 1'b0); // DELIVERED TWICE
      check(n_duplicate == b_e + 1,
            "a second delivery of the same identity was not reported as a duplicate -- a scoreboard that matches on payload retires the OTHER transfer instead and both bugs vanish");
      b_e = n_missing;
      drain;                              // tag 6 never completed
      check(n_missing == b_e + 1,
            "a transfer that never completed was not reported as missing");
      check(n_matched == b_m + 1,
            "more matches were recorded than transfers actually completed");
    end

...and its false-positive counterpart, which is why the scoreboard cannot be a queue:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      issue(3'd0, 8'h70, 1'b0);          // endpoint 0 first
      issue(3'd1, 8'h71, 1'b1);          // endpoint 1 second
      complete(3'd1, 8'h71, 1'b1, 1'b0); // ...and it finishes FIRST
      complete(3'd0, 8'h70, 1'b0, 1'b0);
      check(n_order + n_unexpected + n_mismatch == b_e,
            "legal cross-endpoint reordering was reported as an error -- a scoreboard built as a queue flags every interleaving, which is most of them");

And one check that knows nothing about any particular bug:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // ---- CONSERVATION. Every transfer that was successfully issued left
    // ---- the table exactly once: matched, reported wrong, reported out of
    // ---- order, or reported missing at the drain. A scoreboard whose
    // ---- inputs and outputs do not balance has lost track of something,
    // ---- and this is the one check that notices without knowing WHICH.
    check(n_issued === n_matched + n_mismatch + n_order + n_missing + n_xdiv,
          "the transfers that left the table do not account for the transfers that entered it -- something was lost without being reported");

13.1 Verilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Testbench for usb_scoreboard (Verilog-2005).
//
// THE TEST THAT DEFINES THE CHAPTER
//
// Two transfers carrying the SAME PAYLOAD. One of them is delivered twice;
// the other is never delivered at all.
//
//     issue tag 3, 0x5A     issue tag 6, 0x5A
//     complete tag 3        complete tag 3 AGAIN
//     ...and tag 6 never completes.
//
//   an identity-matching scoreboard  1 match, 1 DUPLICATE, 1 MISSING
//   a value-matching scoreboard      2 matches, 0 errors
//
// The second one is not merely weaker. It is wrong in a way that cannot be
// fixed by running longer or by adding payloads, because the two bugs
// ANNIHILATE each other in its bookkeeping. That is mutation S1, and the
// three-line phase below is the whole argument for tagging.
//
// AND THE FALSE-POSITIVE SIDE
//
// Completion out of order ACROSS endpoints is legal and constant -- a bulk
// transfer queued first finishes after an interrupt transfer queued later.
// The suite drives that deliberately and requires ZERO errors, because a
// scoreboard that flags it is a scoreboard nobody leaves enabled.
//
// WHAT IS EXHAUSTIVE HERE
//
//   1. Every tag state (EMPTY / OUTSTANDING / RETIRED) crossed with all 16
//      combinations of {exp_valid, act_valid, act_retry, eot} = 48 pairs.
//   2. Every table occupancy from 0 to N_TAG = 9, each reached by real
//      issues.
`timescale 1ns/1ps
module tb_sb_v;

  localparam integer N_TAG = 8;
  localparam integer TW    = 3;
  localparam integer DW    = 8;

  localparam [1:0] T_EMPTY=2'd0, T_OUTSTANDING=2'd1, T_RETIRED=2'd2;
  localparam [2:0] E_NONE=3'd0, E_MISMATCH=3'd1, E_UNEXPECTED=3'd2,
                   E_DUPLICATE=3'd3, E_ORDER=3'd4;

  reg clk = 1'b0, rst_n = 1'b0;
  reg exp_valid = 1'b0, act_valid = 1'b0, act_retry = 1'b0, eot = 1'b0;
  reg exp_ep = 1'b0, act_ep = 1'b0;
  reg [TW-1:0] exp_tag = 3'd0, act_tag = 3'd0;
  reg [DW-1:0] exp_data = 8'd0, act_data = 8'd0;

  wire [TW:0] outstanding;
  wire [1:0]  tag_state;
  wire [2:0]  act_err_code;
  wire match_pulse, act_err_pulse, exp_err_pulse, miss_pulse;
  wire [31:0] n_issued, n_matched, n_retries, n_mismatch, n_unexpected,
              n_duplicate, n_reissue, n_missing, n_order;

  usb_scoreboard #(.N_TAG(N_TAG), .TW(TW), .DW(DW)) dut (
    .clk(clk), .rst_n(rst_n),
    .exp_valid(exp_valid), .exp_tag(exp_tag), .exp_data(exp_data), .exp_ep(exp_ep),
    .act_valid(act_valid), .act_tag(act_tag), .act_data(act_data),
    .act_ep(act_ep), .act_retry(act_retry), .eot(eot),
    .outstanding(outstanding), .tag_state(tag_state),
    .match_pulse(match_pulse), .act_err_pulse(act_err_pulse),
    .act_err_code(act_err_code), .exp_err_pulse(exp_err_pulse),
    .miss_pulse(miss_pulse),
    .n_issued(n_issued), .n_matched(n_matched), .n_retries(n_retries),
    .n_mismatch(n_mismatch), .n_unexpected(n_unexpected),
    .n_duplicate(n_duplicate), .n_reissue(n_reissue),
    .n_missing(n_missing), .n_order(n_order)
  );

  always #5 clk = ~clk;

  integer errors = 0, checks = 0;
  task check(input cond, input [1023:0] msg);
    begin
      checks = checks + 1;
      if (!cond) begin
        errors = errors + 1;
        if (errors <= 25)
          $display("FAIL @%0t: %0s | out=%0d tst=%0d m=%b ae=%b(%0d) ee=%b ms=%b",
                   $time, msg, outstanding, tag_state, match_pulse,
                   act_err_pulse, act_err_code, exp_err_pulse, miss_pulse);
      end
    end
  endtask

  // ------------------------------------------------------------------
  // The shadow scoreboard. Its own table, its own sequence numbers.
  // ------------------------------------------------------------------
  reg [1:0]    m_st  [0:N_TAG-1];
  reg [DW-1:0] m_dat [0:N_TAG-1];
  reg          m_ep  [0:N_TAG-1];
  reg [3:0]    m_seq [0:N_TAG-1];
  reg [3:0]    m_iss [0:1];
  reg [3:0]    m_cmp [0:1];
  reg [TW:0]   m_cnt, m_drain;
  reg [2:0]    m_aec;
  reg          m_mat, m_aer, m_eer, m_mis;
  integer m_i, m_m, m_rt, m_mm, m_un, m_dp, m_ri, m_ms, m_or;
  integer n_xdiv;   // completions retired by a wrong-endpoint error

  integer seen_ts [0:47];       // 3 tag states x 16 input combinations
  integer seen_oc [0:8];        // occupancy 0..N_TAG
  integer n_ts, n_oc, n_steps;

  task model_reset;
    integer j;
    begin
      for (j = 0; j < N_TAG; j = j + 1) begin
        m_st[j] = T_EMPTY; m_dat[j] = 8'd0; m_ep[j] = 1'b0; m_seq[j] = 4'd0;
      end
      m_iss[0] = 4'd0; m_iss[1] = 4'd0;
      m_cmp[0] = 4'd0; m_cmp[1] = 4'd0;
      m_cnt = 4'd0; m_drain = 4'd0; m_aec = E_NONE;
      m_mat = 1'b0; m_aer = 1'b0; m_eer = 1'b0; m_mis = 1'b0;
      m_i = 0; m_m = 0; m_rt = 0; m_mm = 0; m_un = 0;
      m_dp = 0; m_ri = 0; m_ms = 0; m_or = 0;
      for (j = 0; j < 48; j = j + 1) seen_ts[j] = 0;
      for (j = 0; j < 9;  j = j + 1) seen_oc[j] = 0;
      n_ts = 0; n_oc = 0; n_steps = 0; n_xdiv = 0;
    end
  endtask

  integer idx, scan;
  task step(input ev, input [TW-1:0] et, input [DW-1:0] ed, input eep,
            input av, input [TW-1:0] at, input [DW-1:0] ad, input aep,
            input ar, input eo);
    reg [1:0] nst, sat, set_, w_at;
    reg w_at_en, w_et_en;
    reg [3:0] ncmp0, ncmp1, niss0, niss1;
    reg [TW:0] ncnt, ndrn;
    reg [2:0] naec;
    reg nmat, naer, neer, nmis, nret;
    begin
      exp_valid = ev; exp_tag = et; exp_data = ed; exp_ep = eep;
      act_valid = av; act_tag = at; act_data = ad; act_ep = aep;
      act_retry = ar; eot = eo;
      #1;

      check(outstanding   === m_cnt,  "outstanding disagrees with the shadow scoreboard");
      check(tag_state     === m_st[at], "tag_state disagrees -- the three-state table is the thing that names a duplicate");
      check(match_pulse   === m_mat,  "the match pulse disagrees");
      check(act_err_pulse === m_aer,  "the completion-channel error pulse disagrees");
      check(act_err_code  === m_aec,  "act_err_code disagrees");
      check(exp_err_pulse === m_eer,  "the issue-channel error pulse disagrees");
      check(miss_pulse    === m_mis,  "the drain pulse disagrees");
      check(!(match_pulse && act_err_pulse),
            "a completion was reported as both a match and an error");
      check(outstanding <= N_TAG[TW:0],
            "more transfers are outstanding than there are identities");

      // ---- the occupancy must equal the number of OUTSTANDING entries.
      // ---- A count kept separately from the table can drift from it, and
      // ---- then neither can be trusted.
      idx = 0;
      for (scan = 0; scan < N_TAG; scan = scan + 1)
        if (m_st[scan] == T_OUTSTANDING) idx = idx + 1;
      check(m_cnt === idx[TW:0],
            "the occupancy counter disagrees with the table it is supposed to summarise");

      idx = m_st[at] * 16 + (ev ? 8 : 0) + (av ? 4 : 0) + (ar ? 2 : 0) + (eo ? 1 : 0);
      if (idx < 48) begin
        if (seen_ts[idx] == 0) begin seen_ts[idx] = 1; n_ts = n_ts + 1; end
      end
      if (seen_oc[m_cnt] == 0) begin seen_oc[m_cnt] = 1; n_oc = n_oc + 1; end
      n_steps = n_steps + 1;

      // ---- advance the shadow scoreboard ----
      ncnt = m_cnt; ndrn = m_drain; naec = E_NONE;
      nmat = 1'b0; naer = 1'b0; neer = 1'b0; nmis = 1'b0; nret = 1'b0;
      niss0 = m_iss[0]; niss1 = m_iss[1];
      ncmp0 = m_cmp[0]; ncmp1 = m_cmp[1];

      // BOTH channels read the state as it stands at the START of the
      // cycle, exactly as the design reads its registered table. Mutating
      // the model's table inside the completion branch and then reading it
      // in the issue branch is the classic read-after-write model bug: it
      // diverges only when the two channels name the SAME tag in the same
      // cycle, which random stimulus finds and directed stimulus does not.
      sat  = m_st[at];
      set_ = m_st[et];
      w_at_en = 1'b0; w_et_en = 1'b0; w_at = T_EMPTY;

      if (eo) begin
        if (m_drain < N_TAG[TW:0]) begin
          if (m_st[m_drain[TW-1:0]] == T_OUTSTANDING) begin
            m_st[m_drain[TW-1:0]] = T_EMPTY;
            ncnt = ncnt - 1'b1;
            nmis = 1'b1;
            if (m_ep[m_drain[TW-1:0]]) ncmp1 = m_cmp[1] + 4'd1;
            else                       ncmp0 = m_cmp[0] + 4'd1;
          end else begin
            m_st[m_drain[TW-1:0]] = T_EMPTY;
          end
          ndrn = m_drain + 1'b1;
        end
      end else begin
        ndrn = {(TW+1){1'b0}};
        if (av) begin
          nst = sat;
          if (nst == T_OUTSTANDING) begin
            if (ad !== m_dat[at]) begin
              naer = 1'b1; naec = E_MISMATCH;
              w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
              if (m_ep[at]) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
            end else if (aep != m_ep[at]) begin
              naer = 1'b1; naec = E_UNEXPECTED;
              w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
              if (m_ep[at]) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
              // Retired by a wrong-endpoint completion: it left the table,
              // so the conservation check at the end has to know about it.
              n_xdiv = n_xdiv + 1;
            end else if (ar) begin
              nret = 1'b1;
            end else if (m_seq[at] != m_cmp[aep]) begin
              naer = 1'b1; naec = E_ORDER;
              w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
              if (aep) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
            end else begin
              nmat = 1'b1;
              w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
              if (aep) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
            end
          end else if (nst == T_RETIRED) begin
            naer = 1'b1; naec = E_DUPLICATE;
          end else begin
            naer = 1'b1; naec = E_UNEXPECTED;
          end
        end
        if (ev) begin
          if (set_ == T_OUTSTANDING) begin
            neer = 1'b1;
          end else begin
            w_et_en = 1'b1;
            ncnt = ncnt + 1'b1;
          end
        end

        // Applied in the design's order: the completion channel's write
        // first, the issue channel's second, so an issue to the same tag
        // wins -- which it can only do if that tag was not outstanding.
        if (w_at_en) m_st[at] = w_at;
        if (w_et_en) begin
          m_st[et]  = T_OUTSTANDING;
          m_dat[et] = ed;
          m_ep[et]  = eep;
          m_seq[et] = m_iss[eep];
          if (eep) niss1 = m_iss[1] + 4'd1; else niss0 = m_iss[0] + 4'd1;
        end
      end

      m_cnt = ncnt; m_drain = ndrn; m_aec = naec;
      m_mat = nmat; m_aer = naer; m_eer = neer; m_mis = nmis;
      m_iss[0] = niss0; m_iss[1] = niss1;
      m_cmp[0] = ncmp0; m_cmp[1] = ncmp1;
      if (ev && !eo && !neer) m_i = m_i + 1;
      if (nmat) m_m  = m_m + 1;
      if (nret) m_rt = m_rt + 1;
      if (neer) m_ri = m_ri + 1;
      if (nmis) m_ms = m_ms + 1;
      if (naer) begin
        case (naec)
          E_MISMATCH:   m_mm = m_mm + 1;
          E_UNEXPECTED: m_un = m_un + 1;
          E_DUPLICATE:  m_dp = m_dp + 1;
          E_ORDER:      m_or = m_or + 1;
          default: ;
        endcase
      end

      @(posedge clk); #1;
      exp_valid = 1'b0; act_valid = 1'b0; act_retry = 1'b0; eot = 1'b0;
    end
  endtask

  task nop(input integer n);
    integer j;
    begin
      for (j = 0; j < n; j = j + 1)
        step(1'b0,3'd0,8'd0,1'b0, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b0);
    end
  endtask

  task issue(input [TW-1:0] t, input [DW-1:0] d, input e);
    begin step(1'b1,t,d,e, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b0); end
  endtask

  task complete(input [TW-1:0] t, input [DW-1:0] d, input e, input r);
    begin step(1'b0,3'd0,8'd0,1'b0, 1'b1,t,d,e, r,1'b0); end
  endtask

  // Drain the table the way the design provides for. The drain visits one
  // tag per cycle, so it takes N_TAG cycles -- a bounded walk, not a search.
  task drain;
    integer j;
    begin
      for (j = 0; j < N_TAG + 2; j = j + 1)
        step(1'b0,3'd0,8'd0,1'b0, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b1);
      check(outstanding === 4'd0, "the drain did not empty the table");
      nop(1);
    end
  endtask

  // A clean scoreboard: every tag EMPTY, both sequence spaces aligned.
  // A pristine table, reached the way the design provides for: the drain
  // visits every tag, reports the outstanding ones and returns the retired
  // ones to EMPTY. Nothing is forced.
  task fresh;
    integer j;
    begin
      drain;
      for (j = 0; j < N_TAG; j = j + 1)
        check(m_st[j] === T_EMPTY,
              "the drain did not return every tag to EMPTY -- a retired identity that stays retired can be used only once");
    end
  endtask

  integer k, j2, b_m, b_e, ts, cb, oc;

  initial begin
    model_reset;
    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(posedge clk); #1;

    // ---- Phase A: the state after reset ----
    check(outstanding === 4'd0, "reset left transfers outstanding");
    check(match_pulse === 1'b0, "reset asserted a match");
    check(act_err_pulse === 1'b0, "reset asserted a completion error");
    check(n_matched === 32'd0, "reset left the match counter non-zero");

    // ---- Phase B: clean traffic, in order, on both endpoints. ZERO errors.
    for (k = 0; k < 60; k = k + 1) begin
      b_m = n_matched;
      b_e = n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order;
      issue(3'd0, 8'h11, 1'b0);
      issue(3'd1, 8'h22, 1'b0);
      complete(3'd0, 8'h11, 1'b0, 1'b0);
      complete(3'd1, 8'h22, 1'b0, 1'b0);
      check(n_matched == b_m + 2, "clean in-order traffic was not matched");
      check(n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order == b_e,
            "clean in-order traffic produced an error");
      drain;
    end

    // ---- Phase C: THE IDENTITY TEST. Two transfers, SAME payload; one
    // ---- delivered twice, the other never.
    for (k = 0; k < 40; k = k + 1) begin
      b_m = n_matched;
      issue(3'd3, 8'h5A, 1'b0);
      issue(3'd6, 8'h5A, 1'b0);          // the SAME bytes
      complete(3'd3, 8'h5A, 1'b0, 1'b0); // match
      b_e = n_duplicate;
      complete(3'd3, 8'h5A, 1'b0, 1'b0); // DELIVERED TWICE
      check(n_duplicate == b_e + 1,
            "a second delivery of the same identity was not reported as a duplicate -- a scoreboard that matches on payload retires the OTHER transfer instead and both bugs vanish");
      b_e = n_missing;
      drain;                              // tag 6 never completed
      check(n_missing == b_e + 1,
            "a transfer that never completed was not reported as missing");
      check(n_matched == b_m + 1,
            "more matches were recorded than transfers actually completed");
    end

    // ---- Phase D: identity matched, payload wrong. Repeated across every
    // ---- single-bit corruption, so the comparison is checked on each bit
    // ---- rather than on one arbitrary pair of values.
    for (k = 0; k < DW; k = k + 1) begin
      b_e = n_mismatch;
      issue(3'd2, 8'hA5, 1'b0);
      complete(3'd2, 8'hA5 ^ (8'd1 << k), 1'b0, 1'b0);
      check(n_mismatch == b_e + 1,
            "a wrong payload on a matched identity was accepted -- a comparison that covers only part of the payload passes most corruptions");
      drain;
    end

    // ---- Phase E: a completion for a tag that was never issued. ----
    fresh;
    b_e = n_unexpected;
    complete(3'd7, 8'h33, 1'b0, 1'b0);
    check(n_unexpected == b_e + 1,
          "a completion for a never-issued identity was accepted");

    // ---- Phase F: a tag reused while still outstanding. ----
    fresh;
    b_e = n_reissue;
    issue(3'd4, 8'h44, 1'b0);
    issue(3'd4, 8'h55, 1'b0);
    check(n_reissue == b_e + 1,
          "an identity was reused while still outstanding and the first transfer was silently overwritten");
    check(outstanding === 4'd1,
          "the reissued transfer displaced the one already being tracked");
    drain;

    // ---- Phase G: out of order WITHIN an endpoint is a bug. ----
    for (k = 0; k < 40; k = k + 1) begin
      fresh;
      b_e = n_order;
      issue(3'd0, 8'h60, 1'b0);
      issue(3'd1, 8'h61, 1'b0);
      complete(3'd1, 8'h61, 1'b0, 1'b0);   // the SECOND one first
      check(n_order == b_e + 1,
            "completion out of order within one endpoint was accepted -- the stream is corrupted and the host will not check it");
      drain;
    end

    // ---- Phase H: out of order ACROSS endpoints is LEGAL. ----
    // ---- This is the false-positive check, and it is the reason the
    // ---- scoreboard cannot be a queue.
    fresh;
    for (k = 0; k < 40; k = k + 1) begin
      b_m = n_matched;
      b_e = n_order + n_unexpected + n_mismatch;
      issue(3'd0, 8'h70, 1'b0);          // endpoint 0 first
      issue(3'd1, 8'h71, 1'b1);          // endpoint 1 second
      complete(3'd1, 8'h71, 1'b1, 1'b0); // ...and it finishes FIRST
      complete(3'd0, 8'h70, 1'b0, 1'b0);
      check(n_matched == b_m + 2,
            "legal cross-endpoint reordering was not matched");
      check(n_order + n_unexpected + n_mismatch == b_e,
            "legal cross-endpoint reordering was reported as an error -- a scoreboard built as a queue flags every interleaving, which is most of them");
      drain;
    end

    // ---- Phase I: a RETRY is not a delivery. ----
    fresh;
    for (k = 1; k <= 5; k = k + 1) begin
      b_m = n_matched;
      b_e = n_duplicate + n_order;
      issue(3'd5, 8'h80, 1'b0);
      for (j2 = 0; j2 < k; j2 = j2 + 1)
        complete(3'd5, 8'h80, 1'b0, 1'b1);     // k re-sends
      check(outstanding === 4'd1,
            "a retry retired the transfer -- the real delivery will then look like a duplicate");
      complete(3'd5, 8'h80, 1'b0, 1'b0);       // the real delivery
      check(n_matched == b_m + 1, "the delivery after a run of retries was not matched");
      check(n_duplicate + n_order == b_e,
            "a retry was reported as a duplicate -- every flow-controlled transfer on a busy bus would be");
      drain;
    end

    // ---- Phase J: THE DRAIN. Data the design accepted and never returned.
    fresh;
    for (oc = 1; oc <= N_TAG; oc = oc + 1) begin
      b_e = n_missing;
      b_m = n_matched;
      for (k = 0; k < oc; k = k + 1) issue(k[TW-1:0], 8'h90 + k[DW-1:0], 1'b0);
      check(outstanding === oc[TW:0], "the table did not accept the issues offered to it");
      drain;
      check(n_missing == b_e + oc,
            "transfers still outstanding at end of test were not reported -- the run is over and nothing more is coming");
      check(n_matched == b_m, "a transfer that never came back was counted as a match");
      fresh;
    end

    // ---- Phase K: EXHAUSTIVE. Every tag state x every input combination. --
    for (ts = 0; ts < 3; ts = ts + 1) begin
      for (cb = 0; cb < 16; cb = cb + 1) begin
        fresh;
        // put tag 2 into the state under test, using the design's own means
        case (ts)
          0: ;                                              // EMPTY
          1: issue(3'd2, 8'hC0, 1'b0);                      // OUTSTANDING
          2: begin issue(3'd2, 8'hC0, 1'b0);
                   complete(3'd2, 8'hC0, 1'b0, 1'b0); end   // RETIRED
        endcase
        check(m_st[2] === ts[1:0],
              "the sweep could not reach the tag state it meant to reach");
        step(cb[3],3'd2,8'hC0,1'b0, cb[2],3'd2,8'hC0,1'b0, cb[1],cb[0]);
        step(cb[3],3'd2,8'hC0,1'b0, cb[2],3'd2,8'hC0,1'b0, cb[1],cb[0]);
      end
    end

    // ---- Phase L: random ----
    for (k = 0; k < 34000; k = k + 1)
      step(($unsigned($random) % 100) < 30, $random, $random, $random,
           ($unsigned($random) % 100) < 30, $random, $random, $random,
           ($unsigned($random) % 100) < 20,
           ($unsigned($random) % 1000) < 7);

    // ---- Phase M: and clean traffic afterwards, so the scoreboard is shown
    // ---- to still work rather than merely to have stopped.
    fresh;
    b_m = n_matched;
    b_e = n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order;
    for (k = 0; k < 120; k = k + 1) begin
      issue(3'd0, 8'hE0, 1'b0);
      issue(3'd1, 8'hE1, 1'b1);
      complete(3'd1, 8'hE1, 1'b1, 1'b0);
      complete(3'd0, 8'hE0, 1'b0, 1'b0);
      drain;
    end
    check(n_matched == b_m + 240, "the scoreboard stopped matching clean traffic");
    check(n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order == b_e,
          "clean traffic after the random phase produced errors");

    // ---- Final agreement ----
    check(n_issued     === m_i[31:0],  "n_issued disagrees with the model");
    check(n_matched    === m_m[31:0],  "n_matched disagrees with the model");
    check(n_retries    === m_rt[31:0], "n_retries disagrees");
    check(n_mismatch   === m_mm[31:0], "n_mismatch disagrees");
    check(n_unexpected === m_un[31:0], "n_unexpected disagrees");
    check(n_duplicate  === m_dp[31:0], "n_duplicate disagrees");
    check(n_reissue    === m_ri[31:0], "n_reissue disagrees");
    check(n_missing    === m_ms[31:0], "n_missing disagrees");
    check(n_order      === m_or[31:0], "n_order disagrees");

    // ---- CONSERVATION. Every transfer that was successfully issued left
    // ---- the table exactly once: matched, reported wrong, reported out of
    // ---- order, or reported missing at the drain. A scoreboard whose
    // ---- inputs and outputs do not balance has lost track of something,
    // ---- and this is the one check that notices without knowing WHICH.
    check(n_issued === n_matched + n_mismatch + n_order + n_missing + n_xdiv,
          "the transfers that left the table do not account for the transfers that entered it -- something was lost without being reported");
    check(n_ts == 48, "not every tag state was crossed with every input combination");
    check(n_oc == 9,  "not every table occupancy was reached");
    check(n_matched    > 32'd0, "nothing was ever matched");
    check(n_mismatch   > 32'd0, "a payload mismatch was never seen");
    check(n_unexpected > 32'd0, "an unexpected completion was never seen");
    check(n_duplicate  > 32'd0, "a duplicate delivery was never seen");
    check(n_reissue    > 32'd0, "a reused identity was never seen");
    check(n_missing    > 32'd0, "a missing completion was never seen");
    check(n_order      > 32'd0, "an in-endpoint order violation was never seen");
    check(n_retries    > 32'd0, "a retry was never seen");

    $display("REACH tagstate-x-input=%0d/48 occupancy=%0d/9 steps=%0d",
             n_ts, n_oc, n_steps);
    $display("COUNTERS issued=%0d matched=%0d retries=%0d mismatch=%0d unexpected=%0d duplicate=%0d reissue=%0d missing=%0d order=%0d",
             n_issued, n_matched, n_retries, n_mismatch, n_unexpected,
             n_duplicate, n_reissue, n_missing, n_order);
    $display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
    $finish;
  end
endmodule

13.2 SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Testbench for usb_scoreboard (SystemVerilog).
//
// THE TEST THAT DEFINES THE CHAPTER
//
// Two transfers carrying the SAME PAYLOAD. One of them is delivered twice;
// the other is never delivered at all.
//
//     issue tag 3, 0x5A     issue tag 6, 0x5A
//     complete tag 3        complete tag 3 AGAIN
//     ...and tag 6 never completes.
//
//   an identity-matching scoreboard  1 match, 1 DUPLICATE, 1 MISSING
//   a value-matching scoreboard      2 matches, 0 errors
//
// The second one is not merely weaker. It is wrong in a way that cannot be
// fixed by running longer or by adding payloads, because the two bugs
// ANNIHILATE each other in its bookkeeping. That is mutation S1, and the
// three-line phase below is the whole argument for tagging.
//
// AND THE FALSE-POSITIVE SIDE
//
// Completion out of order ACROSS endpoints is legal and constant -- a bulk
// transfer queued first finishes after an interrupt transfer queued later.
// The suite drives that deliberately and requires ZERO errors, because a
// scoreboard that flags it is a scoreboard nobody leaves enabled.
//
// WHAT IS EXHAUSTIVE HERE
//
//   1. Every tag state (EMPTY / OUTSTANDING / RETIRED) crossed with all 16
//      combinations of {exp_valid, act_valid, act_retry, eot} = 48 pairs.
//   2. Every table occupancy from 0 to N_TAG = 9, each reached by real
//      issues.
`timescale 1ns/1ps
module tb_sb_sv;
  import usb_sb_pkg::*;

  localparam int N_TAG = 8;
  localparam int TW    = 3;
  localparam int DW    = 8;

  logic clk = 1'b0, rst_n = 1'b0;
  logic exp_valid = 1'b0, act_valid = 1'b0, act_retry = 1'b0, eot = 1'b0;
  logic exp_ep = 1'b0, act_ep = 1'b0;
  logic [TW-1:0] exp_tag = '0, act_tag = '0;
  logic [DW-1:0] exp_data = '0, act_data = '0;

  logic [TW:0] outstanding;
  tag_state_e  tag_state;
  sb_err_e     act_err_code;
  logic match_pulse, act_err_pulse, exp_err_pulse, miss_pulse;
  logic [31:0] n_issued, n_matched, n_retries, n_mismatch, n_unexpected,
               n_duplicate, n_reissue, n_missing, n_order;

  usb_scoreboard #(.N_TAG(N_TAG), .TW(TW), .DW(DW)) dut (.*);

  always #5 clk = ~clk;

  int errors = 0, checks = 0;
  task automatic check(input logic cond, input string msg);
    begin
      checks++;
      if (!cond) begin
        errors++;
        if (errors <= 25)
          $display("FAIL @%0t: %0s | out=%0d tst=%0d m=%b ae=%b(%0d) ee=%b ms=%b",
                   $time, msg, outstanding, tag_state, match_pulse,
                   act_err_pulse, act_err_code, exp_err_pulse, miss_pulse);
      end
    end
  endtask

  // ------------------------------------------------------------------
  // The shadow scoreboard. Its own table, its own sequence numbers.
  // ------------------------------------------------------------------
  tag_state_e    m_st  [N_TAG];
  logic [DW-1:0] m_dat [N_TAG];
  logic          m_ep  [N_TAG];
  logic [3:0]    m_seq [N_TAG];
  logic [3:0]    m_iss [2];
  logic [3:0]    m_cmp [2];
  logic [TW:0]   m_cnt, m_drain;
  sb_err_e       m_aec;
  logic          m_mat, m_aer, m_eer, m_mis;
  int m_i, m_m, m_rt, m_mm, m_un, m_dp, m_ri, m_ms, m_or;
  int n_xdiv;   // completions retired by a wrong-endpoint error

  int seen_ts [48];             // 3 tag states x 16 input combinations
  int seen_oc [9];              // occupancy 0..N_TAG
  int n_ts, n_oc, n_steps;

  task automatic model_reset();
    int j;
    begin
      for (j = 0; j < N_TAG; j++) begin
        m_st[j] = T_EMPTY; m_dat[j] = 8'd0; m_ep[j] = 1'b0; m_seq[j] = 4'd0;
      end
      m_iss[0] = 4'd0; m_iss[1] = 4'd0;
      m_cmp[0] = 4'd0; m_cmp[1] = 4'd0;
      m_cnt = 4'd0; m_drain = 4'd0; m_aec = E_NONE;
      m_mat = 1'b0; m_aer = 1'b0; m_eer = 1'b0; m_mis = 1'b0;
      m_i = 0; m_m = 0; m_rt = 0; m_mm = 0; m_un = 0;
      m_dp = 0; m_ri = 0; m_ms = 0; m_or = 0;
      for (j = 0; j < 48; j++) seen_ts[j] = 0;
      for (j = 0; j < 9;  j = j + 1) seen_oc[j] = 0;
      n_ts = 0; n_oc = 0; n_steps = 0; n_xdiv = 0;
    end
  endtask

  int idx, scan;
  task automatic step(input logic ev, input logic [TW-1:0] et,
                      input logic [DW-1:0] ed, input logic eep,
                      input logic av, input logic [TW-1:0] at,
                      input logic [DW-1:0] ad, input logic aep,
                      input logic ar, input logic eo);
    tag_state_e nst, sat, set_, w_at;
    logic w_at_en, w_et_en;
    logic [3:0] ncmp0, ncmp1, niss0, niss1;
    logic [TW:0] ncnt, ndrn;
    sb_err_e naec;
    logic nmat, naer, neer, nmis, nret;
    begin
      exp_valid = ev; exp_tag = et; exp_data = ed; exp_ep = eep;
      act_valid = av; act_tag = at; act_data = ad; act_ep = aep;
      act_retry = ar; eot = eo;
      #1;

      check(outstanding   === m_cnt,  "outstanding disagrees with the shadow scoreboard");
      check(tag_state     === m_st[at], "tag_state disagrees -- the three-state table is the thing that names a duplicate");
      check(match_pulse   === m_mat,  "the match pulse disagrees");
      check(act_err_pulse === m_aer,  "the completion-channel error pulse disagrees");
      check(act_err_code  === m_aec,  "act_err_code disagrees");
      check(exp_err_pulse === m_eer,  "the issue-channel error pulse disagrees");
      check(miss_pulse    === m_mis,  "the drain pulse disagrees");
      check(!(match_pulse && act_err_pulse),
            "a completion was reported as both a match and an error");
      check(outstanding <= (TW+1)'(N_TAG),
            "more transfers are outstanding than there are identities");

      // ---- the occupancy must equal the number of OUTSTANDING entries.
      // ---- A count kept separately from the table can drift from it, and
      // ---- then neither can be trusted.
      idx = 0;
      for (scan = 0; scan < N_TAG; scan++)
        if (m_st[scan] == T_OUTSTANDING) idx = idx + 1;
      check(m_cnt === idx[TW:0],
            "the occupancy counter disagrees with the table it is supposed to summarise");

      idx = int'(m_st[at]) * 16 + (ev ? 8 : 0) + (av ? 4 : 0) + (ar ? 2 : 0) + (eo ? 1 : 0);
      if (idx < 48) begin
        if (seen_ts[idx] == 0) begin seen_ts[idx] = 1; n_ts = n_ts + 1; end
      end
      if (seen_oc[m_cnt] == 0) begin seen_oc[m_cnt] = 1; n_oc = n_oc + 1; end
      n_steps++;

      // ---- advance the shadow scoreboard ----
      ncnt = m_cnt; ndrn = m_drain; naec = E_NONE;
      nmat = 1'b0; naer = 1'b0; neer = 1'b0; nmis = 1'b0; nret = 1'b0;
      niss0 = m_iss[0]; niss1 = m_iss[1];
      ncmp0 = m_cmp[0]; ncmp1 = m_cmp[1];

      // BOTH channels read the state as it stands at the START of the
      // cycle, exactly as the design reads its registered table. Mutating
      // the model's table inside the completion branch and then reading it
      // in the issue branch is the classic read-after-write model bug: it
      // diverges only when the two channels name the SAME tag in the same
      // cycle, which random stimulus finds and directed stimulus does not.
      sat  = m_st[at];
      set_ = m_st[et];
      w_at_en = 1'b0; w_et_en = 1'b0; w_at = T_EMPTY;

      if (eo) begin
        if (m_drain < (TW+1)'(N_TAG)) begin
          if (m_st[m_drain[TW-1:0]] == T_OUTSTANDING) begin
            m_st[m_drain[TW-1:0]] = T_EMPTY;
            ncnt = ncnt - 1'b1;
            nmis = 1'b1;
            if (m_ep[m_drain[TW-1:0]]) ncmp1 = m_cmp[1] + 4'd1;
            else                       ncmp0 = m_cmp[0] + 4'd1;
          end else begin
            m_st[m_drain[TW-1:0]] = T_EMPTY;
          end
          ndrn = m_drain + 1'b1;
        end
      end else begin
        ndrn = '0;
        if (av) begin
          nst = sat;
          if (nst == T_OUTSTANDING) begin
            if (ad !== m_dat[at]) begin
              naer = 1'b1; naec = E_MISMATCH;
              w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
              if (m_ep[at]) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
            end else if (aep != m_ep[at]) begin
              naer = 1'b1; naec = E_UNEXPECTED;
              w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
              if (m_ep[at]) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
              // Retired by a wrong-endpoint completion: it left the table,
              // so the conservation check at the end has to know about it.
              n_xdiv++;
            end else if (ar) begin
              nret = 1'b1;
            end else if (m_seq[at] != m_cmp[aep]) begin
              naer = 1'b1; naec = E_ORDER;
              w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
              if (aep) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
            end else begin
              nmat = 1'b1;
              w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
              if (aep) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
            end
          end else if (nst == T_RETIRED) begin
            naer = 1'b1; naec = E_DUPLICATE;
          end else begin
            naer = 1'b1; naec = E_UNEXPECTED;
          end
        end
        if (ev) begin
          if (set_ == T_OUTSTANDING) begin
            neer = 1'b1;
          end else begin
            w_et_en = 1'b1;
            ncnt = ncnt + 1'b1;
          end
        end

        // Applied in the design's order: the completion channel's write
        // first, the issue channel's second, so an issue to the same tag
        // wins -- which it can only do if that tag was not outstanding.
        if (w_at_en) m_st[at] = w_at;
        if (w_et_en) begin
          m_st[et]  = T_OUTSTANDING;
          m_dat[et] = ed;
          m_ep[et]  = eep;
          m_seq[et] = m_iss[eep];
          if (eep) niss1 = m_iss[1] + 4'd1; else niss0 = m_iss[0] + 4'd1;
        end
      end

      m_cnt = ncnt; m_drain = ndrn; m_aec = naec;
      m_mat = nmat; m_aer = naer; m_eer = neer; m_mis = nmis;
      m_iss[0] = niss0; m_iss[1] = niss1;
      m_cmp[0] = ncmp0; m_cmp[1] = ncmp1;
      if (ev && !eo && !neer) m_i = m_i + 1;
      if (nmat) m_m  = m_m + 1;
      if (nret) m_rt = m_rt + 1;
      if (neer) m_ri = m_ri + 1;
      if (nmis) m_ms = m_ms + 1;
      if (naer) begin
        case (naec)
          E_MISMATCH:   m_mm = m_mm + 1;
          E_UNEXPECTED: m_un = m_un + 1;
          E_DUPLICATE:  m_dp = m_dp + 1;
          E_ORDER:      m_or = m_or + 1;
          default: ;
        endcase
      end

      @(posedge clk); #1;
      exp_valid = 1'b0; act_valid = 1'b0; act_retry = 1'b0; eot = 1'b0;
    end
  endtask

  task automatic nop(input int n);
    repeat (n) step(1'b0,3'd0,8'd0,1'b0, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b0);
  endtask

  task automatic issue(input logic [TW-1:0] t, input logic [DW-1:0] d,
                       input logic e);
    step(1'b1,t,d,e, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b0);
  endtask

  task automatic complete(input logic [TW-1:0] t, input logic [DW-1:0] d,
                          input logic e, input logic r);
    step(1'b0,3'd0,8'd0,1'b0, 1'b1,t,d,e, r,1'b0);
  endtask

  // Drain the table the way the design provides for. The drain visits one
  // tag per cycle, so it takes N_TAG cycles -- a bounded walk, not a search.
  task automatic drain();
    repeat (N_TAG + 2)
      step(1'b0,3'd0,8'd0,1'b0, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b1);
    check(outstanding === '0, "the drain did not empty the table");
    nop(1);
  endtask

  // A clean scoreboard: every tag EMPTY, both sequence spaces aligned.
  // A pristine table, reached the way the design provides for: the drain
  // visits every tag, reports the outstanding ones and returns the retired
  // ones to EMPTY. Nothing is forced.
  task automatic fresh();
    drain();
    for (int j = 0; j < N_TAG; j++)
      check(m_st[j] === T_EMPTY,
            "the drain did not return every tag to EMPTY -- a retired identity that stays retired can be used only once");
  endtask

  int k, j2, b_m, b_e, ts, cb, oc;

  initial begin
    model_reset();
    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(posedge clk); #1;

    // ---- Phase A: the state after reset ----
    check(outstanding === 4'd0, "reset left transfers outstanding");
    check(match_pulse === 1'b0, "reset asserted a match");
    check(act_err_pulse === 1'b0, "reset asserted a completion error");
    check(n_matched === 32'd0, "reset left the match counter non-zero");

    // ---- Phase B: clean traffic, in order, on both endpoints. ZERO errors.
    for (k = 0; k < 60; k++) begin
      b_m = n_matched;
      b_e = n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order;
      issue(3'd0, 8'h11, 1'b0);
      issue(3'd1, 8'h22, 1'b0);
      complete(3'd0, 8'h11, 1'b0, 1'b0);
      complete(3'd1, 8'h22, 1'b0, 1'b0);
      check(n_matched == b_m + 2, "clean in-order traffic was not matched");
      check(n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order == b_e,
            "clean in-order traffic produced an error");
      drain();
    end

    // ---- Phase C: THE IDENTITY TEST. Two transfers, SAME payload; one
    // ---- delivered twice, the other never.
    for (k = 0; k < 40; k++) begin
      b_m = n_matched;
      issue(3'd3, 8'h5A, 1'b0);
      issue(3'd6, 8'h5A, 1'b0);          // the SAME bytes
      complete(3'd3, 8'h5A, 1'b0, 1'b0); // match
      b_e = n_duplicate;
      complete(3'd3, 8'h5A, 1'b0, 1'b0); // DELIVERED TWICE
      check(n_duplicate == b_e + 1,
            "a second delivery of the same identity was not reported as a duplicate -- a scoreboard that matches on payload retires the OTHER transfer instead and both bugs vanish");
      b_e = n_missing;
      drain();                              // tag 6 never completed
      check(n_missing == b_e + 1,
            "a transfer that never completed was not reported as missing");
      check(n_matched == b_m + 1,
            "more matches were recorded than transfers actually completed");
    end

    // ---- Phase D: identity matched, payload wrong. Repeated across every
    // ---- single-bit corruption, so the comparison is checked on each bit
    // ---- rather than on one arbitrary pair of values.
    for (k = 0; k < DW; k++) begin
      b_e = n_mismatch;
      issue(3'd2, 8'hA5, 1'b0);
      complete(3'd2, 8'hA5 ^ (8'd1 << k), 1'b0, 1'b0);
      check(n_mismatch == b_e + 1,
            "a wrong payload on a matched identity was accepted -- a comparison that covers only part of the payload passes most corruptions");
      drain();
    end

    // ---- Phase E: a completion for a tag that was never issued. ----
    fresh();
    b_e = n_unexpected;
    complete(3'd7, 8'h33, 1'b0, 1'b0);
    check(n_unexpected == b_e + 1,
          "a completion for a never-issued identity was accepted");

    // ---- Phase F: a tag reused while still outstanding. ----
    fresh();
    b_e = n_reissue;
    issue(3'd4, 8'h44, 1'b0);
    issue(3'd4, 8'h55, 1'b0);
    check(n_reissue == b_e + 1,
          "an identity was reused while still outstanding and the first transfer was silently overwritten");
    check(outstanding === 4'd1,
          "the reissued transfer displaced the one already being tracked");
    drain();

    // ---- Phase G: out of order WITHIN an endpoint is a bug. ----
    for (k = 0; k < 40; k++) begin
      fresh();
      b_e = n_order;
      issue(3'd0, 8'h60, 1'b0);
      issue(3'd1, 8'h61, 1'b0);
      complete(3'd1, 8'h61, 1'b0, 1'b0);   // the SECOND one first
      check(n_order == b_e + 1,
            "completion out of order within one endpoint was accepted -- the stream is corrupted and the host will not check it");
      drain();
    end

    // ---- Phase H: out of order ACROSS endpoints is LEGAL. ----
    // ---- This is the false-positive check, and it is the reason the
    // ---- scoreboard cannot be a queue.
    fresh();
    for (k = 0; k < 40; k++) begin
      b_m = n_matched;
      b_e = n_order + n_unexpected + n_mismatch;
      issue(3'd0, 8'h70, 1'b0);          // endpoint 0 first
      issue(3'd1, 8'h71, 1'b1);          // endpoint 1 second
      complete(3'd1, 8'h71, 1'b1, 1'b0); // ...and it finishes FIRST
      complete(3'd0, 8'h70, 1'b0, 1'b0);
      check(n_matched == b_m + 2,
            "legal cross-endpoint reordering was not matched");
      check(n_order + n_unexpected + n_mismatch == b_e,
            "legal cross-endpoint reordering was reported as an error -- a scoreboard built as a queue flags every interleaving, which is most of them");
      drain();
    end

    // ---- Phase I: a RETRY is not a delivery. ----
    fresh();
    for (k = 1; k <= 5; k++) begin
      b_m = n_matched;
      b_e = n_duplicate + n_order;
      issue(3'd5, 8'h80, 1'b0);
      for (j2 = 0; j2 < k; j2++)
        complete(3'd5, 8'h80, 1'b0, 1'b1);     // k re-sends
      check(outstanding === 4'd1,
            "a retry retired the transfer -- the real delivery will then look like a duplicate");
      complete(3'd5, 8'h80, 1'b0, 1'b0);       // the real delivery
      check(n_matched == b_m + 1, "the delivery after a run of retries was not matched");
      check(n_duplicate + n_order == b_e,
            "a retry was reported as a duplicate -- every flow-controlled transfer on a busy bus would be");
      drain();
    end

    // ---- Phase J: THE DRAIN. Data the design accepted and never returned.
    fresh();
    for (oc = 1; oc <= N_TAG; oc++) begin
      b_e = n_missing;
      b_m = n_matched;
      for (k = 0; k < oc; k++) issue(3'(k), 8'h90 + 8'(k), 1'b0);
      check(outstanding === (TW+1)'(oc), "the table did not accept the issues offered to it");
      drain();
      check(n_missing == b_e + oc,
            "transfers still outstanding at end of test were not reported -- the run is over and nothing more is coming");
      check(n_matched == b_m, "a transfer that never came back was counted as a match");
      fresh();
    end

    // ---- Phase K: EXHAUSTIVE. Every tag state x every input combination. --
    for (ts = 0; ts < 3; ts++) begin
      for (cb = 0; cb < 16; cb++) begin
        fresh();
        // put tag 2 into the state under test, using the design's own means
        case (ts)
          0: ;                                              // EMPTY
          1: issue(3'd2, 8'hC0, 1'b0);                      // OUTSTANDING
          2: begin issue(3'd2, 8'hC0, 1'b0);
                   complete(3'd2, 8'hC0, 1'b0, 1'b0); end   // RETIRED
        endcase
        check(m_st[2] === tag_state_e'(ts),
              "the sweep could not reach the tag state it meant to reach");
        step(1'(cb[3]),3'd2,8'hC0,1'b0, 1'(cb[2]),3'd2,8'hC0,1'b0,
             1'(cb[1]),1'(cb[0]));
        step(1'(cb[3]),3'd2,8'hC0,1'b0, 1'(cb[2]),3'd2,8'hC0,1'b0,
             1'(cb[1]),1'(cb[0]));
      end
    end

    // ---- Phase L: random ----
    for (k = 0; k < 34000; k++)
      step($urandom_range(0,99) < 30, 3'($urandom()), 8'($urandom()), 1'($urandom()),
           $urandom_range(0,99) < 30, 3'($urandom()), 8'($urandom()), 1'($urandom()),
           $urandom_range(0,99) < 20,
           $urandom_range(0,999) < 7);

    // ---- Phase M: and clean traffic afterwards, so the scoreboard is shown
    // ---- to still work rather than merely to have stopped.
    fresh();
    b_m = n_matched;
    b_e = n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order;
    for (k = 0; k < 120; k++) begin
      issue(3'd0, 8'hE0, 1'b0);
      issue(3'd1, 8'hE1, 1'b1);
      complete(3'd1, 8'hE1, 1'b1, 1'b0);
      complete(3'd0, 8'hE0, 1'b0, 1'b0);
      drain();
    end
    check(n_matched == b_m + 240, "the scoreboard stopped matching clean traffic");
    check(n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order == b_e,
          "clean traffic after the random phase produced errors");

    // ---- Final agreement ----
    check(n_issued     === 32'(m_i),  "n_issued disagrees with the model");
    check(n_matched    === 32'(m_m),  "n_matched disagrees with the model");
    check(n_retries    === 32'(m_rt), "n_retries disagrees");
    check(n_mismatch   === 32'(m_mm), "n_mismatch disagrees");
    check(n_unexpected === 32'(m_un), "n_unexpected disagrees");
    check(n_duplicate  === 32'(m_dp), "n_duplicate disagrees");
    check(n_reissue    === 32'(m_ri), "n_reissue disagrees");
    check(n_missing    === 32'(m_ms), "n_missing disagrees");
    check(n_order      === 32'(m_or), "n_order disagrees");

    // ---- CONSERVATION. Every transfer that was successfully issued left
    // ---- the table exactly once: matched, reported wrong, reported out of
    // ---- order, or reported missing at the drain. A scoreboard whose
    // ---- inputs and outputs do not balance has lost track of something,
    // ---- and this is the one check that notices without knowing WHICH.
    check(n_issued === n_matched + n_mismatch + n_order + n_missing + 32'(n_xdiv),
          "the transfers that left the table do not account for the transfers that entered it -- something was lost without being reported");
    check(n_ts == 48, "not every tag state was crossed with every input combination");
    check(n_oc == 9,  "not every table occupancy was reached");
    check(n_matched    > 32'd0, "nothing was ever matched");
    check(n_mismatch   > 32'd0, "a payload mismatch was never seen");
    check(n_unexpected > 32'd0, "an unexpected completion was never seen");
    check(n_duplicate  > 32'd0, "a duplicate delivery was never seen");
    check(n_reissue    > 32'd0, "a reused identity was never seen");
    check(n_missing    > 32'd0, "a missing completion was never seen");
    check(n_order      > 32'd0, "an in-endpoint order violation was never seen");
    check(n_retries    > 32'd0, "a retry was never seen");

    $display("REACH tagstate-x-input=%0d/48 occupancy=%0d/9 steps=%0d",
             n_ts, n_oc, n_steps);
    $display("COUNTERS issued=%0d matched=%0d retries=%0d mismatch=%0d unexpected=%0d duplicate=%0d reissue=%0d missing=%0d order=%0d",
             n_issued, n_matched, n_retries, n_mismatch, n_unexpected,
             n_duplicate, n_reissue, n_missing, n_order);
    $display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
    $finish;
  end
endmodule

13.3 VHDL testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Testbench for usb_scoreboard (VHDL-2008).
--
-- THE TEST THAT DEFINES THE CHAPTER
--
-- Two transfers carrying the SAME PAYLOAD. One of them is delivered twice;
-- the other is never delivered at all.
--
--     issue tag 3, 0x5A     issue tag 6, 0x5A
--     complete tag 3        complete tag 3 AGAIN
--     ...and tag 6 never completes.
--
--   an identity-matching scoreboard  1 match, 1 DUPLICATE, 1 MISSING
--   a value-matching scoreboard      2 matches, 0 errors
--
-- The second one is not merely weaker. It is wrong in a way that cannot be
-- fixed by running longer or by adding payloads, because the two bugs
-- ANNIHILATE each other in its bookkeeping. That is mutation S1, and the
-- short phase below is the whole argument for tagging.
--
-- AND THE FALSE-POSITIVE SIDE
--
-- Completion out of order ACROSS endpoints is legal and constant -- a bulk
-- transfer queued first finishes after an interrupt transfer queued later.
-- The suite drives that deliberately and requires ZERO errors, because a
-- scoreboard that flags it is a scoreboard nobody leaves enabled.
--
-- WHAT IS EXHAUSTIVE HERE
--
--   1. Every tag state (EMPTY / OUTSTANDING / RETIRED) crossed with all 16
--      combinations of (exp_valid, act_valid, act_retry, eot) = 48 pairs.
--   2. Every table occupancy from 0 to N_TAG = 9, each reached by real
--      issues.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_sb_pkg.all;

entity tb_sb_vhdl is
end entity tb_sb_vhdl;

architecture sim of tb_sb_vhdl is

  constant N_TAG : integer := 8;
  constant TW    : integer := 3;
  constant DW    : integer := 8;

  signal clk   : std_logic := '0';
  signal rst_n : std_logic := '0';
  signal done  : boolean   := false;

  signal exp_valid, act_valid, act_retry, eot : std_logic := '0';
  signal exp_ep, act_ep : std_logic := '0';
  signal exp_tag, act_tag : std_logic_vector(TW-1 downto 0) := (others => '0');
  signal exp_data, act_data : std_logic_vector(DW-1 downto 0) := (others => '0');

  signal outstanding  : std_logic_vector(TW downto 0);
  signal tag_state    : std_logic_vector(1 downto 0);
  signal act_err_code : std_logic_vector(2 downto 0);
  signal match_pulse, act_err_pulse, exp_err_pulse, miss_pulse : std_logic;
  signal n_issued, n_matched, n_retries, n_mismatch : std_logic_vector(31 downto 0);
  signal n_unexpected, n_duplicate, n_reissue       : std_logic_vector(31 downto 0);
  signal n_missing, n_order                         : std_logic_vector(31 downto 0);

begin

  dut : entity work.usb_scoreboard
    generic map (N_TAG => N_TAG, TW => TW, DW => DW)
    port map (
      clk => clk, rst_n => rst_n,
      exp_valid => exp_valid, exp_tag => exp_tag, exp_data => exp_data,
      exp_ep => exp_ep,
      act_valid => act_valid, act_tag => act_tag, act_data => act_data,
      act_ep => act_ep, act_retry => act_retry, eot => eot,
      outstanding => outstanding, tag_state => tag_state,
      match_pulse => match_pulse, act_err_pulse => act_err_pulse,
      act_err_code => act_err_code, exp_err_pulse => exp_err_pulse,
      miss_pulse => miss_pulse,
      n_issued => n_issued, n_matched => n_matched, n_retries => n_retries,
      n_mismatch => n_mismatch, n_unexpected => n_unexpected,
      n_duplicate => n_duplicate, n_reissue => n_reissue,
      n_missing => n_missing, n_order => n_order
    );

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

  stim : process
    type st_arr  is array (0 to N_TAG-1) of tag_state_t;
    type dat_arr is array (0 to N_TAG-1) of std_logic_vector(DW-1 downto 0);
    type seq_arr is array (0 to N_TAG-1) of unsigned(3 downto 0);
    type ep_arr  is array (0 to N_TAG-1) of std_logic;
    type eps_arr is array (0 to 1) of unsigned(3 downto 0);
    type ts_arr  is array (0 to 47) of integer;
    type oc_arr  is array (0 to 8) of integer;

    variable errors, checks : integer := 0;

    -- ---- The shadow scoreboard. Its own table, its own sequence numbers. --
    variable m_st  : st_arr  := (others => T_EMPTY);
    variable m_dat : dat_arr := (others => (others => '0'));
    variable m_ep  : ep_arr  := (others => '0');
    variable m_seq : seq_arr := (others => (others => '0'));
    variable m_iss, m_cmp : eps_arr := (others => (others => '0'));
    variable m_cnt, m_drain : unsigned(TW downto 0) := (others => '0');
    variable m_aec : sb_err_t := E_NONE;
    variable m_mat, m_aer, m_eer, m_mis : std_logic := '0';
    variable m_i, m_m, m_rt, m_mm : integer := 0;
    variable m_un, m_dp, m_ri, m_ms, m_or : integer := 0;
    variable n_xdiv : integer := 0;

    variable seen_ts : ts_arr := (others => 0);
    variable seen_oc : oc_arr := (others => 0);
    variable n_ts, n_oc, n_steps : integer := 0;

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

    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;

    impure function rnd_slv (w : integer) return std_logic_vector is
      variable u : unsigned(31 downto 0);
    begin
      u := rnd32;
      return std_logic_vector(u(w-1 downto 0));
    end function;

    impure function rnd_bit return std_logic is
      variable u : unsigned(31 downto 0);
    begin
      u := rnd32;
      return u(7);
    end function;

    impure function rnd_lt (pct, base : integer) return std_logic is
    begin
      if (rnd_nat mod base) < pct then return '1'; else return '0'; end if;
    end function;

    procedure chk (cond : boolean; msg : string) is
    begin
      checks := checks + 1;
      if not cond then
        errors := errors + 1;
        if errors <= 25 then
          report "FAIL: " & msg &
                 " | out=" & integer'image(to_integer(m_cnt)) &
                 " code=" & integer'image(sb_err_t'pos(m_aec))
            severity note;
        end if;
      end if;
    end procedure;

    procedure step (ev : std_logic; et : std_logic_vector(TW-1 downto 0);
                    ed : std_logic_vector(DW-1 downto 0); eep : std_logic;
                    av : std_logic; at : std_logic_vector(TW-1 downto 0);
                    ad : std_logic_vector(DW-1 downto 0); aep : std_logic;
                    ar, eo : std_logic) is
      variable at_i, et_i, dr_i, idx : integer;
      variable sat, set_st, w_at : tag_state_t;
      variable w_at_en, w_et_en : boolean;
      variable ncnt, ndrn : unsigned(TW downto 0);
      variable niss, ncmp : eps_arr;
      variable naec : sb_err_t;
      variable nmat, naer, neer, nmis, nret : std_logic;
      variable aep_i, eep_i : integer;
    begin
      exp_valid <= ev; exp_tag <= et; exp_data <= ed; exp_ep <= eep;
      act_valid <= av; act_tag <= at; act_data <= ad; act_ep <= aep;
      act_retry <= ar; eot <= eo;
      wait for 1 ns;

      at_i  := to_integer(unsigned(at));
      et_i  := to_integer(unsigned(et));
      aep_i := 0; if aep = '1' then aep_i := 1; end if;
      eep_i := 0; if eep = '1' then eep_i := 1; end if;

      chk(unsigned(outstanding) = m_cnt,
          "outstanding disagrees with the shadow scoreboard");
      chk(tag_state = ts_code(m_st(at_i)),
          "tag_state disagrees -- the three-state table is the thing that names a duplicate");
      chk(match_pulse   = m_mat, "the match pulse disagrees");
      chk(act_err_pulse = m_aer, "the completion-channel error pulse disagrees");
      chk(act_err_code  = se_code(m_aec), "act_err_code disagrees");
      chk(exp_err_pulse = m_eer, "the issue-channel error pulse disagrees");
      chk(miss_pulse    = m_mis, "the drain pulse disagrees");
      chk(not (match_pulse = '1' and act_err_pulse = '1'),
          "a completion was reported as both a match and an error");
      chk(unsigned(outstanding) <= to_unsigned(N_TAG, TW+1),
          "more transfers are outstanding than there are identities");

      -- ---- the occupancy must equal the number of OUTSTANDING entries.
      -- ---- A count kept separately from the table can drift from it, and
      -- ---- then neither can be trusted.
      idx := 0;
      for scan in 0 to N_TAG - 1 loop
        if m_st(scan) = T_OUTSTANDING then idx := idx + 1; end if;
      end loop;
      chk(m_cnt = to_unsigned(idx, TW+1),
          "the occupancy counter disagrees with the table it is supposed to summarise");

      idx := tag_state_t'pos(m_st(at_i)) * 16;
      if ev = '1' then idx := idx + 8; end if;
      if av = '1' then idx := idx + 4; end if;
      if ar = '1' then idx := idx + 2; end if;
      if eo = '1' then idx := idx + 1; end if;
      if idx < 48 then
        if seen_ts(idx) = 0 then seen_ts(idx) := 1; n_ts := n_ts + 1; end if;
      end if;
      if seen_oc(to_integer(m_cnt)) = 0 then
        seen_oc(to_integer(m_cnt)) := 1; n_oc := n_oc + 1;
      end if;
      n_steps := n_steps + 1;

      -- ---- advance the shadow scoreboard ----
      ncnt := m_cnt; ndrn := m_drain; naec := E_NONE;
      nmat := '0'; naer := '0'; neer := '0'; nmis := '0'; nret := '0';
      niss := m_iss; ncmp := m_cmp;

      -- BOTH channels read the state as it stands at the START of the
      -- cycle, exactly as the design reads its registered table. Mutating
      -- the model's table inside the completion branch and then reading it
      -- in the issue branch is the classic read-after-write model bug: it
      -- diverges only when the two channels name the SAME tag in the same
      -- cycle, which random stimulus finds and directed stimulus does not.
      sat     := m_st(at_i);
      set_st  := m_st(et_i);
      w_at_en := false; w_et_en := false; w_at := T_EMPTY;

      if eo = '1' then
        if m_drain < to_unsigned(N_TAG, TW+1) then
          dr_i := to_integer(m_drain);
          if m_st(dr_i) = T_OUTSTANDING then
            m_st(dr_i) := T_EMPTY;
            ncnt := ncnt - 1;
            nmis := '1';
            if m_ep(dr_i) = '1' then ncmp(1) := m_cmp(1) + 1;
            else                     ncmp(0) := m_cmp(0) + 1;
            end if;
          else
            m_st(dr_i) := T_EMPTY;
          end if;
          ndrn := m_drain + 1;
        end if;
      else
        ndrn := (others => '0');
        if av = '1' then
          case sat is
            when T_OUTSTANDING =>
              if ad /= m_dat(at_i) then
                naer := '1'; naec := E_MISMATCH;
                w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
                if m_ep(at_i) = '1' then ncmp(1) := m_cmp(1) + 1;
                else                     ncmp(0) := m_cmp(0) + 1;
                end if;
              elsif aep /= m_ep(at_i) then
                naer := '1'; naec := E_UNEXPECTED;
                w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
                if m_ep(at_i) = '1' then ncmp(1) := m_cmp(1) + 1;
                else                     ncmp(0) := m_cmp(0) + 1;
                end if;
                -- Retired by a wrong-endpoint completion: it left the table,
                -- so the conservation check at the end has to know about it.
                n_xdiv := n_xdiv + 1;
              elsif ar = '1' then
                nret := '1';
              elsif m_seq(at_i) /= m_cmp(aep_i) then
                naer := '1'; naec := E_ORDER;
                w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
                ncmp(aep_i) := m_cmp(aep_i) + 1;
              else
                nmat := '1';
                w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
                ncmp(aep_i) := m_cmp(aep_i) + 1;
              end if;
            when T_RETIRED =>
              naer := '1'; naec := E_DUPLICATE;
            when others =>
              naer := '1'; naec := E_UNEXPECTED;
          end case;
        end if;
        if ev = '1' then
          if set_st = T_OUTSTANDING then
            neer := '1';
          else
            w_et_en := true;
            ncnt := ncnt + 1;
          end if;
        end if;

        -- Applied in the design's order: the completion channel's write
        -- first, the issue channel's second, so an issue to the same tag
        -- wins -- which it can only do if that tag was not outstanding.
        if w_at_en then m_st(at_i) := w_at; end if;
        if w_et_en then
          m_st(et_i)  := T_OUTSTANDING;
          m_dat(et_i) := ed;
          m_ep(et_i)  := eep;
          m_seq(et_i) := m_iss(eep_i);
          niss(eep_i) := m_iss(eep_i) + 1;
        end if;
      end if;

      m_cnt := ncnt; m_drain := ndrn; m_aec := naec;
      m_mat := nmat; m_aer := naer; m_eer := neer; m_mis := nmis;
      m_iss := niss; m_cmp := ncmp;
      if ev = '1' and eo = '0' and neer = '0' then m_i := m_i + 1; end if;
      if nmat = '1' then m_m  := m_m + 1; end if;
      if nret = '1' then m_rt := m_rt + 1; end if;
      if neer = '1' then m_ri := m_ri + 1; end if;
      if nmis = '1' then m_ms := m_ms + 1; end if;
      if naer = '1' then
        case naec is
          when E_MISMATCH   => m_mm := m_mm + 1;
          when E_UNEXPECTED => m_un := m_un + 1;
          when E_DUPLICATE  => m_dp := m_dp + 1;
          when E_ORDER      => m_or := m_or + 1;
          when others       => null;
        end case;
      end if;

      wait until rising_edge(clk);
      wait for 1 ns;
      exp_valid <= '0'; act_valid <= '0'; act_retry <= '0'; eot <= '0';
    end procedure;

    constant Z3 : std_logic_vector(TW-1 downto 0) := (others => '0');
    constant Z8 : std_logic_vector(DW-1 downto 0) := (others => '0');

    procedure nop (n : integer) is
    begin
      for j in 1 to n loop
        step('0',Z3,Z8,'0', '0',Z3,Z8,'0', '0','0');
      end loop;
    end procedure;

    procedure issue (t : std_logic_vector(TW-1 downto 0);
                     d : std_logic_vector(DW-1 downto 0); e : std_logic) is
    begin
      step('1',t,d,e, '0',Z3,Z8,'0', '0','0');
    end procedure;

    procedure complete (t : std_logic_vector(TW-1 downto 0);
                        d : std_logic_vector(DW-1 downto 0);
                        e, r : std_logic) is
    begin
      step('0',Z3,Z8,'0', '1',t,d,e, r,'0');
    end procedure;

    -- Drain the table the way the design provides for. The drain visits one
    -- tag per cycle, so it takes N_TAG cycles -- a bounded walk, not a
    -- search.
    procedure drain is
    begin
      for j in 1 to N_TAG + 2 loop
        step('0',Z3,Z8,'0', '0',Z3,Z8,'0', '0','1');
      end loop;
      chk(unsigned(outstanding) = 0, "the drain did not empty the table");
      nop(1);
    end procedure;

    -- A pristine table, reached the way the design provides for: the drain
    -- visits every tag, reports the outstanding ones and returns the retired
    -- ones to EMPTY. Nothing is forced.
    procedure fresh is
    begin
      drain;
      for j in 0 to N_TAG - 1 loop
        chk(m_st(j) = T_EMPTY,
            "the drain did not return every tag to EMPTY -- a retired identity that stays retired can be used only once");
      end loop;
    end procedure;

    function tg (n : integer) return std_logic_vector is
    begin
      return std_logic_vector(to_unsigned(n, TW));
    end function;

    function dt (n : integer) return std_logic_vector is
    begin
      return std_logic_vector(to_unsigned(n mod 256, DW));
    end function;

    variable b_m, b_e : integer := 0;
    variable ev_v, av_v, ar_v, eo_v : std_logic;
    variable ln : line;

  begin
    wait for 33 ns;
    rst_n <= '1';
    wait until rising_edge(clk);
    wait for 1 ns;

    -- ---- Phase A: the state after reset ----
    chk(unsigned(outstanding) = 0, "reset left transfers outstanding");
    chk(match_pulse = '0', "reset asserted a match");
    chk(act_err_pulse = '0', "reset asserted a completion error");
    chk(unsigned(n_matched) = 0, "reset left the match counter non-zero");

    -- ---- Phase B: clean traffic, in order, on both endpoints. ZERO errors.
    for k in 0 to 59 loop
      b_m := to_integer(unsigned(n_matched));
      b_e := to_integer(unsigned(n_mismatch)) + to_integer(unsigned(n_unexpected))
             + to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_reissue))
             + to_integer(unsigned(n_order));
      issue(tg(0), dt(16#11#), '0');
      issue(tg(1), dt(16#22#), '0');
      complete(tg(0), dt(16#11#), '0', '0');
      complete(tg(1), dt(16#22#), '0', '0');
      chk(to_integer(unsigned(n_matched)) = b_m + 2,
          "clean in-order traffic was not matched");
      chk(to_integer(unsigned(n_mismatch)) + to_integer(unsigned(n_unexpected))
          + to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_reissue))
          + to_integer(unsigned(n_order)) = b_e,
          "clean in-order traffic produced an error");
      drain;
    end loop;

    -- ---- Phase C: THE IDENTITY TEST. Two transfers, SAME payload; one
    -- ---- delivered twice, the other never.
    for k in 0 to 39 loop
      b_m := to_integer(unsigned(n_matched));
      issue(tg(3), dt(16#5A#), '0');
      issue(tg(6), dt(16#5A#), '0');            -- the SAME bytes
      complete(tg(3), dt(16#5A#), '0', '0');    -- match
      b_e := to_integer(unsigned(n_duplicate));
      complete(tg(3), dt(16#5A#), '0', '0');    -- DELIVERED TWICE
      chk(to_integer(unsigned(n_duplicate)) = b_e + 1,
          "a second delivery of the same identity was not reported as a duplicate -- a scoreboard that matches on payload retires the OTHER transfer instead and both bugs vanish");
      b_e := to_integer(unsigned(n_missing));
      drain;                                     -- tag 6 never completed
      chk(to_integer(unsigned(n_missing)) = b_e + 1,
          "a transfer that never completed was not reported as missing");
      chk(to_integer(unsigned(n_matched)) = b_m + 1,
          "more matches were recorded than transfers actually completed");
    end loop;

    -- ---- Phase D: identity matched, payload wrong. Repeated across every
    -- ---- single-bit corruption, so the comparison is checked on each bit
    -- ---- rather than on one arbitrary pair of values.
    for k in 0 to DW - 1 loop
      b_e := to_integer(unsigned(n_mismatch));
      issue(tg(2), dt(16#A5#), '0');
      complete(tg(2), std_logic_vector(unsigned(dt(16#A5#))
               xor shift_left(to_unsigned(1, DW), k)), '0', '0');
      chk(to_integer(unsigned(n_mismatch)) = b_e + 1,
          "a wrong payload on a matched identity was accepted -- a comparison that covers only part of the payload passes most corruptions");
      drain;
    end loop;

    -- ---- Phase E: a completion for a tag that was never issued. ----
    fresh;
    b_e := to_integer(unsigned(n_unexpected));
    complete(tg(7), dt(16#33#), '0', '0');
    chk(to_integer(unsigned(n_unexpected)) = b_e + 1,
        "a completion for a never-issued identity was accepted");

    -- ---- Phase F: a tag reused while still outstanding. ----
    fresh;
    b_e := to_integer(unsigned(n_reissue));
    issue(tg(4), dt(16#44#), '0');
    issue(tg(4), dt(16#55#), '0');
    chk(to_integer(unsigned(n_reissue)) = b_e + 1,
        "an identity was reused while still outstanding and the first transfer was silently overwritten");
    chk(unsigned(outstanding) = 1,
        "the reissued transfer displaced the one already being tracked");
    drain;

    -- ---- Phase G: out of order WITHIN an endpoint is a bug. ----
    for k in 0 to 39 loop
      fresh;
      b_e := to_integer(unsigned(n_order));
      issue(tg(0), dt(16#60#), '0');
      issue(tg(1), dt(16#61#), '0');
      complete(tg(1), dt(16#61#), '0', '0');   -- the SECOND one first
      chk(to_integer(unsigned(n_order)) = b_e + 1,
          "completion out of order within one endpoint was accepted -- the stream is corrupted and the host will not check it");
      drain;
    end loop;

    -- ---- Phase H: out of order ACROSS endpoints is LEGAL. ----
    -- ---- This is the false-positive check, and it is the reason the
    -- ---- scoreboard cannot be a queue.
    fresh;
    for k in 0 to 39 loop
      b_m := to_integer(unsigned(n_matched));
      b_e := to_integer(unsigned(n_order)) + to_integer(unsigned(n_unexpected))
             + to_integer(unsigned(n_mismatch));
      issue(tg(0), dt(16#70#), '0');           -- endpoint 0 first
      issue(tg(1), dt(16#71#), '1');           -- endpoint 1 second
      complete(tg(1), dt(16#71#), '1', '0');   -- ...and it finishes FIRST
      complete(tg(0), dt(16#70#), '0', '0');
      chk(to_integer(unsigned(n_matched)) = b_m + 2,
          "legal cross-endpoint reordering was not matched");
      chk(to_integer(unsigned(n_order)) + to_integer(unsigned(n_unexpected))
          + to_integer(unsigned(n_mismatch)) = b_e,
          "legal cross-endpoint reordering was reported as an error -- a scoreboard built as a queue flags every interleaving, which is most of them");
      drain;
    end loop;

    -- ---- Phase I: a RETRY is not a delivery. ----
    fresh;
    for k in 1 to 5 loop
      b_m := to_integer(unsigned(n_matched));
      b_e := to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_order));
      issue(tg(5), dt(16#80#), '0');
      for j in 1 to k loop
        complete(tg(5), dt(16#80#), '0', '1');     -- k re-sends
      end loop;
      chk(unsigned(outstanding) = 1,
          "a retry retired the transfer -- the real delivery will then look like a duplicate");
      complete(tg(5), dt(16#80#), '0', '0');       -- the real delivery
      chk(to_integer(unsigned(n_matched)) = b_m + 1,
          "the delivery after a run of retries was not matched");
      chk(to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_order)) = b_e,
          "a retry was reported as a duplicate -- every flow-controlled transfer on a busy bus would be");
      drain;
    end loop;

    -- ---- Phase J: THE DRAIN. Data the design accepted and never returned.
    fresh;
    for oc in 1 to N_TAG loop
      b_e := to_integer(unsigned(n_missing));
      b_m := to_integer(unsigned(n_matched));
      for k in 0 to oc - 1 loop
        issue(tg(k), dt(16#90# + k), '0');
      end loop;
      chk(unsigned(outstanding) = to_unsigned(oc, TW+1),
          "the table did not accept the issues offered to it");
      drain;
      chk(to_integer(unsigned(n_missing)) = b_e + oc,
          "transfers still outstanding at end of test were not reported -- the run is over and nothing more is coming");
      chk(to_integer(unsigned(n_matched)) = b_m,
          "a transfer that never came back was counted as a match");
      fresh;
    end loop;

    -- ---- Phase K: EXHAUSTIVE. Every tag state x every input combination. --
    for ts in 0 to 2 loop
      for cb in 0 to 15 loop
        fresh;
        -- put tag 2 into the state under test, using the design's own means
        case ts is
          when 0 => null;                                        -- EMPTY
          when 1 => issue(tg(2), dt(16#C0#), '0');                -- OUTSTANDING
          when others => issue(tg(2), dt(16#C0#), '0');
                         complete(tg(2), dt(16#C0#), '0', '0');   -- RETIRED
        end case;
        chk(m_st(2) = tag_state_t'val(ts),
            "the sweep could not reach the tag state it meant to reach");
        if (cb / 8) mod 2 = 1 then ev_v := '1'; else ev_v := '0'; end if;
        if (cb / 4) mod 2 = 1 then av_v := '1'; else av_v := '0'; end if;
        if (cb / 2) mod 2 = 1 then ar_v := '1'; else ar_v := '0'; end if;
        if  cb      mod 2 = 1 then eo_v := '1'; else eo_v := '0'; end if;
        step(ev_v,tg(2),dt(16#C0#),'0', av_v,tg(2),dt(16#C0#),'0', ar_v,eo_v);
        step(ev_v,tg(2),dt(16#C0#),'0', av_v,tg(2),dt(16#C0#),'0', ar_v,eo_v);
      end loop;
    end loop;

    -- ---- Phase L: random ----
    for k in 0 to 33999 loop
      ev_v := rnd_lt(30, 100);
      av_v := rnd_lt(30, 100);
      ar_v := rnd_lt(20, 100);
      eo_v := rnd_lt(7, 1000);
      step(ev_v, rnd_slv(TW), rnd_slv(DW), rnd_bit,
           av_v, rnd_slv(TW), rnd_slv(DW), rnd_bit, ar_v, eo_v);
    end loop;

    -- ---- Phase M: and clean traffic afterwards, so the scoreboard is shown
    -- ---- to still work rather than merely to have stopped.
    fresh;
    b_m := to_integer(unsigned(n_matched));
    b_e := to_integer(unsigned(n_mismatch)) + to_integer(unsigned(n_unexpected))
           + to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_reissue))
           + to_integer(unsigned(n_order));
    for k in 0 to 119 loop
      issue(tg(0), dt(16#E0#), '0');
      issue(tg(1), dt(16#E1#), '1');
      complete(tg(1), dt(16#E1#), '1', '0');
      complete(tg(0), dt(16#E0#), '0', '0');
      drain;
    end loop;
    chk(to_integer(unsigned(n_matched)) = b_m + 240,
        "the scoreboard stopped matching clean traffic");
    chk(to_integer(unsigned(n_mismatch)) + to_integer(unsigned(n_unexpected))
        + to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_reissue))
        + to_integer(unsigned(n_order)) = b_e,
        "clean traffic after the random phase produced errors");

    -- ---- Final agreement ----
    chk(to_integer(unsigned(n_issued))     = m_i,  "n_issued disagrees with the model");
    chk(to_integer(unsigned(n_matched))    = m_m,  "n_matched disagrees with the model");
    chk(to_integer(unsigned(n_retries))    = m_rt, "n_retries disagrees");
    chk(to_integer(unsigned(n_mismatch))   = m_mm, "n_mismatch disagrees");
    chk(to_integer(unsigned(n_unexpected)) = m_un, "n_unexpected disagrees");
    chk(to_integer(unsigned(n_duplicate))  = m_dp, "n_duplicate disagrees");
    chk(to_integer(unsigned(n_reissue))    = m_ri, "n_reissue disagrees");
    chk(to_integer(unsigned(n_missing))    = m_ms, "n_missing disagrees");
    chk(to_integer(unsigned(n_order))      = m_or, "n_order disagrees");

    -- ---- CONSERVATION. Every transfer that was successfully issued left
    -- ---- the table exactly once: matched, reported wrong, reported out of
    -- ---- order, or reported missing at the drain. A scoreboard whose
    -- ---- inputs and outputs do not balance has lost track of something,
    -- ---- and this is the one check that notices without knowing WHICH.
    chk(to_integer(unsigned(n_issued)) =
        to_integer(unsigned(n_matched)) + to_integer(unsigned(n_mismatch))
        + to_integer(unsigned(n_order)) + to_integer(unsigned(n_missing))
        + n_xdiv,
        "the transfers that left the table do not account for the transfers that entered it -- something was lost without being reported");

    chk(n_ts = 48, "not every tag state was crossed with every input combination");
    chk(n_oc = 9,  "not every table occupancy was reached");
    chk(to_integer(unsigned(n_matched))    > 0, "nothing was ever matched");
    chk(to_integer(unsigned(n_mismatch))   > 0, "a payload mismatch was never seen");
    chk(to_integer(unsigned(n_unexpected)) > 0, "an unexpected completion was never seen");
    chk(to_integer(unsigned(n_duplicate))  > 0, "a duplicate delivery was never seen");
    chk(to_integer(unsigned(n_reissue))    > 0, "a reused identity was never seen");
    chk(to_integer(unsigned(n_missing))    > 0, "a missing completion was never seen");
    chk(to_integer(unsigned(n_order))      > 0, "an in-endpoint order violation was never seen");
    chk(to_integer(unsigned(n_retries))    > 0, "a retry was never seen");

    write(ln, string'("REACH tagstate-x-input=") & integer'image(n_ts) &
              "/48 occupancy=" & integer'image(n_oc) &
              "/9 steps=" & integer'image(n_steps));
    writeline(output, ln);
    write(ln, string'("COUNTERS issued=") & integer'image(to_integer(unsigned(n_issued))) &
              " matched=" & integer'image(to_integer(unsigned(n_matched))) &
              " retries=" & integer'image(to_integer(unsigned(n_retries))) &
              " mismatch=" & integer'image(to_integer(unsigned(n_mismatch))) &
              " unexpected=" & integer'image(to_integer(unsigned(n_unexpected))) &
              " duplicate=" & integer'image(to_integer(unsigned(n_duplicate))) &
              " reissue=" & integer'image(to_integer(unsigned(n_reissue))) &
              " missing=" & integer'image(to_integer(unsigned(n_missing))) &
              " order=" & integer'image(to_integer(unsigned(n_order))));
    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 sim;

14. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
tag state x input48 / 4848 / 4848 / 48
table occupancy9 / 99 / 99 / 9
Steps400484004840048
Checks executed402204402204402204
transfers issued583958815774
matched506506527
retries (not duplicates)212165
— mismatch500650574868
— unexpected259230223
— duplicate496349354976
— reissue495450235201
— missing261261266
— in-endpoint order4745113
ResultPASSPASSPASS

The error counts dwarf the match count because the random phase drives both channels independently with random tags and payloads — almost nothing lines up by chance, which is the point of having it. The directed phases are where matches come from, and the conservation check ties the two populations together.

15. Mutation Testing

#MutationVerilogSysVerVHDL
S5a reused identity silently overwrites the one in flight584295990460712
S2no RETIRED state — a duplicate is reported as "unexpected"217882186121516
S1match on VALUE instead of identity34252986563
S7only the low nibble of the payload is compared9288918389
S4a retry retires the transfer112112756
S6the drain does not report what it found312312317
S3in-endpoint order is not checked184178382
—unmutated baseline000

All seven die in all three languages, all counts distinct.

S5 is the largest, which is right: overwriting an identity that is still in flight loses the earlier transfer and reports nothing, so it corrupts the table and every subsequent decision made from it.

S2 is the mis-classification mutation. It misses nothing — a duplicate is still reported — but it reports it as "unexpected", which sends the investigation to the wrong place. It scores 21 000 because the suite checks the code, not merely the pulse. A suite that only counted errors would score it zero.

16. Debugging Walkthrough: The Regression That Went Green When the Bug Got Worse

The report. A USB device controller has a scoreboard that has been clean for months. A firmware change then makes a bulk endpoint occasionally deliver the same packet twice. The scoreboard stays clean. A week later a second bug drops packets on the same endpoint — and the scoreboard still stays clean.

Step 1 — is the scoreboard connected? Yes: it reports matches, and the match count is right.

Step 2 — inject a deliberate corruption. Change one byte of one payload. The scoreboard catches it instantly. So it is working, for some definition of working.

Step 3 — inject a deliberate duplicate. It is caught... sometimes. Duplicating a packet with a unique payload is caught; duplicating one whose payload also occurs elsewhere in flight is not.

Step 4 — read the matching code. It searches the expected list for a payload equal to the one that arrived. It is a value matcher.

Step 5 — and that is why two bugs were quieter than one. The duplicate consumed an expectation belonging to a different transfer. On its own that left one expectation unmatched at the end of the test, which the drain would have reported — except the drain was only checking for an empty list, and the second bug (a dropped packet) supplied exactly the missing arrival to balance it.

Step 6 — the two bugs were each other's alibi. Adding the second bug made the report cleaner, because the counts now balanced. That is the signature to remember.

17. UVM: Where the Real Identity Comes From

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// In a UVM environment the identity is not invented by the scoreboard -- it
// is whatever the DESIGN already uses to correlate a request with its
// completion, and the scoreboard's job is to use the same one.
//
//   xHCI          the TRB pointer in the Transfer Event
//   a device      (endpoint, data toggle, frame)
//   an AXI bridge the transaction ID
//   a DMA engine  the descriptor address
//
// Where no such field exists, one must be ADDED to the monitors -- a
// sequence number stamped by the driver and carried through -- and that is
// a real cost that is worth paying. The alternative is the scoreboard in
// section 1.
class usb_xfer_item extends uvm_sequence_item;
  `uvm_object_utils(usb_xfer_item)

  // ---- THE IDENTITY. Not the payload. ----
  rand bit [15:0] xfer_id;
  rand bit [3:0]  ep;

  rand byte unsigned payload[];
  rand bit          is_retry;

  constraint c_len { payload.size() inside {[0:64]}; }

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

  // Deliberately NOT do_compare()'s default, which compares every field.
  // Identity comparison and payload comparison are separate operations and
  // the class exposes them separately so that a scoreboard cannot
  // accidentally conflate them.
  function bit same_identity(usb_xfer_item other);
    return (xfer_id == other.xfer_id) && (ep == other.ep);
  endfunction

  function bit same_payload(usb_xfer_item other);
    if (payload.size() != other.payload.size()) return 0;
    foreach (payload[i]) if (payload[i] != other.payload[i]) return 0;
    return 1;
  endfunction
endclass

class usb_xfer_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(usb_xfer_scoreboard)

  uvm_analysis_imp_exp #(usb_xfer_item, usb_xfer_scoreboard) exp_ap;
  uvm_analysis_imp_act #(usb_xfer_item, usb_xfer_scoreboard) act_ap;

  // An ASSOCIATIVE ARRAY keyed by identity, not a queue. Out-of-order
  // completion across endpoints is legal, so a queue is wrong; and a
  // linear search over payloads is the bug in section 1.
  usb_xfer_item outstanding[bit [15:0]];

  // The identities that have already completed. This is the third state
  // from the hardware table, and it is what lets a duplicate be NAMED.
  bit retired[bit [15:0]];

  // Per-endpoint issue and completion order.
  int unsigned iss_seq[bit [3:0]];
  int unsigned cmp_seq[bit [3:0]];
  int unsigned xfer_seq[bit [15:0]];

  int unsigned n_issued, n_matched, n_retries;
  int unsigned n_err[string];

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

  function void flag(string cause, string detail);
    n_err[cause]++;
    `uvm_error("SB", $sformatf("%s: %s", cause, detail))
  endfunction

  // ---- the issue channel ----
  function void write_exp(usb_xfer_item t);
    if (outstanding.exists(t.xfer_id)) begin
      // The identity space has been violated. The earlier transfer is now
      // untrackable: it can never be matched, because its identity belongs
      // to something else.
      flag("REISSUE",
        $sformatf("transfer id 0x%04h was issued again while still outstanding -- the earlier transfer can no longer be matched by anything",
                  t.xfer_id));
      return;
    end
    outstanding[t.xfer_id] = t;
    xfer_seq[t.xfer_id]    = iss_seq[t.ep]++;
    void'(retired.delete(t.xfer_id));   // recycling an identity is legal
    n_issued++;
  endfunction

  // ---- the completion channel ----
  function void write_act(usb_xfer_item t);
    if (!outstanding.exists(t.xfer_id)) begin
      if (retired.exists(t.xfer_id))
        flag("DUPLICATE",
          $sformatf("transfer id 0x%04h was delivered a second time", t.xfer_id));
      else
        flag("UNEXPECTED",
          $sformatf("a completion arrived for id 0x%04h, which was never issued",
                    t.xfer_id));
      return;
    end

    begin
      usb_xfer_item e = outstanding[t.xfer_id];

      // ---- A RETRY IS NOT A DELIVERY. Nothing is retired. ----
      if (t.is_retry) begin
        n_retries++;
        return;
      end

      // Identity found the partner. NOW compare the payload.
      if (!e.same_payload(t))
        flag("MISMATCH",
          $sformatf("id 0x%04h returned %0d bytes that do not match the %0d sent",
                    t.xfer_id, t.payload.size(), e.payload.size()));
      else if (xfer_seq[t.xfer_id] != cmp_seq[e.ep])
        flag("ORDER",
          $sformatf("id 0x%04h completed at position %0d on endpoint %0d, expected %0d -- within one endpoint, order is not optional",
                    t.xfer_id, xfer_seq[t.xfer_id], e.ep, cmp_seq[e.ep]));
      else
        n_matched++;

      // ---- However it left, it consumed its place in the order. ----
      //
      // Advancing cmp_seq only on a match leaves a permanent hole after the
      // first failure, and every later completion on that endpoint is then
      // reported out of order.
      cmp_seq[e.ep]++;
      retired[t.xfer_id] = 1;
      void'(outstanding.delete(t.xfer_id));
    end
  endfunction

  // ---- THE DRAIN. Data the design accepted and never returned. ----
  function void check_phase(uvm_phase phase);
    foreach (outstanding[id]) begin
      usb_xfer_item e = outstanding[id];
      n_err["MISSING"]++;
      `uvm_error("SB",
        $sformatf("transfer id 0x%04h on endpoint %0d was issued and never completed -- the run is over and nothing more is coming",
                  id, e.ep))
      cmp_seq[e.ep]++;
    end
  endfunction

  function void report_phase(uvm_phase phase);
    int unsigned total = 0;
    foreach (n_err[c]) total += n_err[c];

    `uvm_info("SB", $sformatf("issued=%0d matched=%0d retries=%0d errors=%0d %p",
                              n_issued, n_matched, n_retries, total, n_err), UVM_LOW)

    // ---- CONSERVATION. Everything that went in came out, once. ----
    if (n_issued != n_matched + n_err["MISMATCH"] + n_err["ORDER"]
                              + n_err["MISSING"])
      `uvm_error("CONSERVATION",
        $sformatf("%0d transfers were issued but only %0d are accounted for -- something left the table without being reported",
                  n_issued, n_matched + n_err["MISMATCH"] + n_err["ORDER"]
                                      + n_err["MISSING"]))

    // A scoreboard that matched nothing is not a passing scoreboard.
    if (n_matched == 0)
      `uvm_error("COVERAGE",
        "nothing was ever matched -- the scoreboard was either not connected or the test drove nothing, and either way its silence means nothing")
  endfunction
endclass

18. Common Misconceptions

"Comparing payloads is what a scoreboard does." Comparing payloads is the second half. Finding the right partner is the first, and doing both with one comparison does neither.

"Identical payloads are a corner case." Zero-length reports, keepalives, status polls and cleared buffers make them the common case.

"A queue is fine if the design completes in order." Across endpoints it does not, and a queue flags every legal interleaving.

"Order does not matter — the host will check." Within an endpoint it will not; the stream is simply corrupt.

"One valid bit per entry is enough." Then a duplicate and a phantom completion are the same report, and the investigation goes to the wrong place.

"A retry is a second delivery." It is the protocol working, and flagging it makes the scoreboard unusable on a busy bus.

"Advance the sequence number on success." One loss then makes every later completion on that endpoint look out of order.

"An identity must be unique for the whole run." Only among transfers that can be in flight together — which is what makes a finite tag space work.

"Errors going down means things are improving." For a value matcher it can mean two bugs are cancelling.

19. Exercises

1. Construct the smallest traffic pattern for which a value-matching scoreboard reports zero errors while two transfers are genuinely wrong. Then show it cannot be fixed by adding a third payload.

2. S2 scores 21 800 and misses nothing at all. Explain what it does report, why the suite catches it, and what a suite that only counted errors would conclude.

3. S1 scores 563 in all three languages with the random phase removed, and 3425 / 2986 / 563 with it. Work out what property of a mutation makes its directed score reproducible and its random score not.

4. Revert §5 — advance cmp_seq only on a match — and derive the number of order violations produced by one missing transfer followed by N good ones.

5. The drain returns RETIRED tags to EMPTY. Show the false positive that appears if it does not, and the missed duplicate that appears if retired is cleared too eagerly.

6. Write the conservation check for the UVM scoreboard in §17 including the retry path, and say why n_retries must not appear in it.

20. Summary

IdeaWhy it matters
Match on identity, then check valueidentity finds the partner; value checks it
Identical payloads are commonkeepalives, status polls, cleared buffers
A duplicate and a loss cancela value matcher gets quieter as things get worse
It cannot be a FIFOcross-endpoint reordering is legal and constant
...but in-endpoint order is a buga corrupt stream the host will not check
A retired tag is not an empty tagor a duplicate is reported as a surprise
A retry is not a deliveryor every flow-controlled transfer is flagged
Every departure consumes its order slotor one loss becomes N order violations
Identities are reusableunique among transfers in flight, not for the run
Both channels read the table as it wasa same-tag collision is the model bug random finds
Conservation catches the unanticipatedthings in must equal things out
Decompose scores into directed + randombefore calling a cross-language spread a finding
48/48 and 9/9, 402 204 checks7 mutations, all killed in 3 languages

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o sb_v.out sb_v.v sb_v_tb.v && ./sb_v.out
SystemVerilogiverilog -g2012 -o sb_sv.out sb_sv.sv sb_sv_tb.sv && ./sb_sv.out
VHDL-2008 analysenvc --std=2008 -a sb_vhdl.vhd sb_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_sb_vhdl
VHDL-2008 runnvc --std=2008 -r tb_sb_vhdl
One mutationiverilog -g2005 -DMUT_S1 -o mm sb_v_mut.v sb_v_tb.v && ./mm

All three implementations pass with 0 errors: every tag state crossed with every input combination, every table occupancy reached by real issues, and the conservation of transfers into and out of the table checked at the end of the run.


Chapter 24.4 — USB Functional Coverage asks what all of this has actually covered. The trap there is a reporting one: an unreachable bin and an untested bin look identical in a coverage report — both show 0% — and the two demand opposite responses. One needs a test written; the other needs the bin deleted, because chasing it wastes the effort that should have gone into the first.

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.